레포지토리

👷레포지토리
우리는 이전 포스팅에 Entity를 만들고, 엔티티를 통해 데이터 베이스 테이블까지 만들어봤다.
하지만 아쉽게도, 만들어진 테이블 현재 실제 데이터를 넣을 방법은 없다.
바로 오늘 배울 레포지토리가 그 과정에 중요한 역할을 하니 한번 살펴보자.
🧩 역할
레포지토리는 데이터베이스 테이블의 데이터들을 저장, 조회, 수정, 삭제등을 도와주는
인터페이스
데이터를 관리하는 다양한 메서드등을 제공해준다.
👨🔬분석
내 개인적인 스타일긴하지만, 무언가를 쓰기위해서는 최대한 분해해서, 흐름 파악을 해야할 듯
레포지토리 분해 ㄱㄱ

앞에서 말한 것 처럼, Repository는 결국 interface임, JPARepository도 역시
3가지 interface를 확장하고
각 interface 내부는 결국 데이터 베이스 테이블 조작을 위한 메서드들을 특징에 따라 그룹핑한 듯
여기서 중요한 점은, 바로 제네릭 타입으로 전달되는 T와 ID다.
T는 여기서 엔티티, ID는 T를 조회하기위한 ID값을 의미한다.
🤖구현
이제 실제로 만들어보자.
이전 포스팅에서 Question과 Answer모두 id값은 Int라는 것을 기억하자.
import org.springframework.data.jpa.repository.JpaRepository
interface QuestionRepository: JpaRepository<Question, Int>
interface AnswerRepository: JpaRepository<Answer, Int>
🧪테스트 코드를 통한 데이터 조작
이제 데이터 테이블을 조작해야하는데.. 할 UI가 없다..
커뮤니티처럼 질문 등록을하고 답변을 달아야하는데, 그걸 언제 만들어..
그래서 우리는 테스트 코드를 통해, UI없이 진행할 것!
@Autowired
- 명시된 타입에 대한 빈을 찾아 의존성을 주입해줌
- 의존성을 찾지 못하면 터짐
- 생성자를 통한 주입
- ✅의존성이 만족되지 않으면, 인스턴스가 만들어지지 못하도록 강조
- ✅테스트 코드 작성 시, 용이함
- ❌순환 참조를 해결하기 힘듬
- 필드를 통한 주입
- ✅간단함
- ❌의존관계가 눈에 잘 보이지 않아, 추적이 어려움
- setter를 통한 주입
- ✅선택적으로 필요한 경우에만 의존성이 사용됨
- ✅생성자 주입 시, 과도한 복잡성을, setter를 통한 의존성 주입과 병행하여, 분배할 수 있음
singleton으로 사용되는 경우에만 사용
🔎Repository 기본 제공 메서드 이용
- save
- findAll
package com.example.demo
import com.example.demo.Entity.Question
import com.example.demo.Repository.QuestionRepository
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import java.time.LocalDateTime.*
import kotlin.test.assertEquals
@SpringBootTest
class DemoApplicationTests(
@Autowired
val questionRepository: QuestionRepository
) {
@Test
fun test() {
val question = Question(
id = 0,
subject = "요즘 무슨게임하나요?",
content = "게임 추천 좀",
createDate = now(),
answerList = listOf()
)
val question2 = Question(
id = 1,
subject = "스프링부트 레포지토리란",
content = "스프링부트 레포지토리의 역할은....",
createDate = now(),
answerList = listOf()
)
questionRepository.save<Question>(question)
questionRepository.save<Question>(question2)
}
@Test
fun testFindAll() {
val questionList = questionRepository.findAll()
assertEquals(3, questionList.count())
val firstQuestion = questionList.get(0)
assertEquals(true, firstQuestion.subject.contains("스프링부트"))
}
}
🚀커스텀 메서드
여기서 JPA의 혁신적인 힘을 느꼈다..
바로 메서드 명을 잘 조합하면, 어느정도 복잡한 SQL구문도 자동으로 해줌
예를 들어보자.
현재 Question 레코드는 Subject라는 필드가 있고, 나는 Subject를 통한 데이터를 조회하고 싶다.
SQL문으로도 그렇게 어려구문은 아닌데, 아래와 같은 SQL문으로 조회할 수 있다.

만약 해당 메서드를 Repository에서는 어떻게 작성해야할까??
차근 차근 알아보자.
🧪예시 코드
첫번 째는, Subject 조건에 맞는 Question을 얻어보자.
- findBySubject라는 이름의 메서드를 만듬
- 파라미터로 subject값
- 결과로는 찾은 Question을 넘긴다.
- 못 찾을 경우를 대비해, nullabe을 해도되지만 일단 무조건 찾을 것으로 생각하고 Question으로 선언
// QuestionRepository.kt
package com.example.demo.Repository
import com.example.demo.Entity.Question
import org.springframework.data.jpa.repository.JpaRepository
interface QuestionRepository: JpaRepository<Question, Int> {
fun findBySubject(subject: String): Question
}
이후 테스트 코드 작성
@Test
fun testFindBySubject() {
val q = questionRepository.findBySubject("스프링부트 레포지토리란")
assertEquals(expected = 1, q.id)
}
여기서 실제 코드가 어떤 SQL문으로 도는 지, 확인하고 싶다면 application.yml 파일에
다음 플래그를 켜주자면 된다,


오른쪽 결과를 보면 예상한 SQL 쿼리가 보인다.
너무 신기함.. interface에 그냥 메서드 이름만 느낌있게 썼는데 구현코드가 어디서.. 참 JPA 편하다.
하나 더, 해보자 이번엔 Subject + Content 두 가지 조건으로 해보자.
똑같이 interface에 메서드명 신경써서 작성, 후 Test코드 작성
interface QuestionRepository: JpaRepository<Question, Int> {
fun findBySubject(subject: String): Question
fun findBySubjectAndContent(subject: String, content: String): Question
}

예상한 Query문이 확인됨
그러면 여기서 결론은 어느정도 Spring & JPA쪽에서 정해놓은 메서드 규칙이 있다는 거임
그거는 아래에 정리해놨ㅇ
📑SQL 연산자와 메서드명 조합
1. 조건 정의 키워드 (Criteria Keywords)
| 키워드 (Keyword) | JPQL/SQL 의미 | 설명 | 예시 메서드 (e.g.) |
| And | AND | 여러 조건을 모두 만족 (논리곱) | findByLastname**And**Firstname(...) |
| Or | OR | 여러 조건 중 하나라도 만족 (논리합) | findByLastname**Or**Firstname(...) |
| Is, Equals | = | 정확히 일치하는 값 (기본 비교) | findByFirstname**Is**(...) |
| Not | <> | 주어진 값과 같지 않은 경우 | findByLastname**Not**(...) |
| Between | BETWEEN | 두 값 사이에 있는 경우 (경계값 포함) | findByAge**Between**(int age1, int age2) |
| LessThan | < | 주어진 값보다 작은 경우 | findByAge**LessThan**(int age) |
| GreaterThan | > | 주어진 값보다 큰 경우 | findByAge**GreaterThan**(int age) |
| After, Before | > 또는 < | 시간/날짜가 이후(After) 또는 이전(Before)인 경우 | findByDate**After**(Date date) |
| IsNull, IsNotNull | IS NULL / IS NOT NULL | 해당 속성 값이 NULL인지 아닌지 확인 | findByTitle**IsNull**() |
| Like, NotLike | LIKE / NOT LIKE | 와일드카드(%)를 사용한 패턴 일치 여부 | findByTitle**Like**(String title) |
| StartingWith | LIKE '값%' | 지정된 문자열로 시작하는 경우 | findByEmail**StartingWith**(String prefix) |
| EndingWith | LIKE '%값' | 지정된 문자열로 끝나는 경우 | findByEmail**EndingWith**(String suffix) |
| Containing | LIKE '%값%' | 지정된 문자열을 포함하는 경우 | findByAddress**Containing**(String keyword) |
| In, NotIn | IN / NOT IN | 주어진 컬렉션/배열 안에 값이 (없는지) 있는지 확인 | findByStatus**In**(Collection<Status> statuses) |
| True, False | = true / = false | Boolean 타입 속성이 True 또는 False인지 확인 | findByActive**True**() |
| IgnoreCase | UPPER(...) | 비교 시 대소문자를 무시하도록 설정 | findByFirstname**IgnoreCase**(String name) |
2. 결과 제한 및 정렬 키워드 (Result Limiting & Ordering)
| 키워드 (Keyword) | 기능 | 설명 | 예시 메서드 (e.g.) |
| OrderBy | 정렬 | 결과 집합을 지정된 속성으로 정렬 | findByLastname**OrderBy**AgeDesc() |
| First, Top | 결과 개수 제한 | 쿼리 결과 중 최대 N개의 결과만 반환 | find**First**10ByLastname(...) |
🤝@Transactional
시작할 때 자동으로 트랜잭션을 시작하고,
메서드가 정상 끝나면 COMMIT, 에러 나면 ROLLBACK
아래 코드를 실행시키면, 사진과 같은 오류가 뜬다.
이 코드는 ID가 2인 질문 엔티티를 찾은 후, 연관된 answerList를 구하는 코드
왜, 여기서 LazyinititalzationException이 날까??
또 사진을 보면 proxy라는 내용도 나오는데, 이거는 별도의 포스팅으로 정리해서 링크를 첨부하겠다.
@Test
fun getAnswersByQuestion() {
val q = questionRepository.findById(2).get()
val answerList = q.answerList
assertEquals(3,answerList.count())
}

이유는 다음과 같다.
- findById를 통해 Question 객체를 조회 후, DB 세션이 끊어짐
- 여기서 q.answerList는 이미 세션이 끊어 졌기 때문에 오류가 발생함
- 이렇게 데이터를 필요한 시점에 가죠오는 방식을 Lazy식이라고 함
- 이 문제는 테스트 코드이기 떄문에 발생하는 문제, 실제 서버는 JPA 프로그램들을 실행할 때는 DB 세션이 종료되지 않음
이와 같이, 테스트 코드를 수행할 때, 이런 오류를 가장 쉽게 방지할 수 있는 방법은
@Transactional을 사용하면, 메서드 끝날 떄까지 DB세션이 유지됨
출처
https://lenditkr.github.io/kotlin/junit/
Kotlin으로 테스트 코드를 작성해보자
요즘 핫하게 떠오르고 있는 Kotlin으로 JUnit 기반의 테스트 코드를 작성해 본 경험을 공유합니다. - Colin
lenditkr.github.io