728x90
N+1 문제
@Transactional(readOnly = true)
fun getUserLoanHistories(): List<UserLoanHistoryResponse> {
return userRepository.findAll().map { user ->
UserLoanHistoryResponse(
name = user.name,
books = user.userLoanHistories.map { history ->
BookHistoryResponse(
name = history.bookName,
isReturn = history.status == UserLoanStatus.RETURNED
)
}
)
}
}
- Service 코드를 보게 되면 왜 N+1 문제가 발생하는지 알 수 있다.
- 최초 User를 조회하는 findAll에서 1번의 쿼리가 발생하고, 조회해온 User를 map 함수로 돌며, history에 접근할 때마다 쿼리가 N번 발생하게 된다(N+1 문제)
- 만약 처음 findAll로 조회해온 데이터가 1000개면 총 1001번의 쿼리가 발생한다.
- List<UserLoanHistory> 같은 경우 처음에 가짜 객체(lazy loading)를 가지고 오기 때문에 해당 데이터를 접근할 때 DB에 SQL을 보내는 방식이다.
- 이를 해결하기 위해서는 Join 문법을 사용하면 된다.
SQL Join
쿼리 한번으로 2개 이상의 테이블의 결과를 한 번에 볼 수 있다.

2개의 테이블을 조인해 보자!!
Inner Join
select * from user
join user_loan_history on user.id = user_loan_history.user_id

- 별칭을 주어서 테이블 명을 줄 수 있다.
- 두 테이블 모두 존재하는 데이터만 출력되는것을 확인할 수 있다(User 테이블 id=3인 데이터는 x)
left Join
select * from user u
left join user_loan_history ulh on u.id = ulh.user_id

- 기준 테이블에 데이터가 있고, 대상테이블에 데이터가 없는 경우에도 데이터가 null로 출력이 된다.
- User 테이블 id=3인 데이터도 확인할 수 있다.
FetchJoin
Repository
interface UserRepository : JpaRepository<User, Long> {
fun findByName(name: String): User?
@Query("select distinct u from User u left join fetch u.userLoanHistories")
fun findAllWithHistories(): List<User>
}
Service
@Transactional(readOnly = true)
fun getUserLoanHistories(): List<UserLoanHistoryResponse> {
return userRepository.findAllWithHistories().map { user ->
UserLoanHistoryResponse(
name = user.name,
books = user.userLoanHistories.map { history ->
BookHistoryResponse(
name = history.bookName,
isReturn = history.status == UserLoanStatus.RETURNED
)
}
)
}
}
Log

- @Query 애노테이션을 통해 JPQL을 직접 작성한다.
- "select distinct u from User u left join fetch u.userLoanHistories"
- OuterJoin을 하게 되면, 데이터가 N개가 되게 되는데(N 쪽으로 데이터가 맞춰짐) 이를 1개의 데이터에 Mapping 시키고 싶다면 distinct를 사용하면 된다
- distinct를 사용하지 않는다면 @BatchSize를 통해 한번에 여러 개의 데이터를 읽어 올 수도 있는데, 자세한 건 JPA 프로그래밍 책을 보자
- left join fetch를 통해 UserLoanHistory 정보를 가져와 실제 UserLoanHistory 객체로 만들어 준다
깔끔한 코드로 리펙토링
1. 동반 객체 메서드(of)를 통해 생성하기
2. isReturn 커스텀 getter() 만들기
UserLoanHistoryResponse
data class UserLoanHistoryResponse(
val name: String, //유저 이름
val books: List<BookHistoryResponse>
){
companion object{
fun of(user: User): UserLoanHistoryResponse{
return UserLoanHistoryResponse(
name = user.name,
books = user.userLoanHistories.map(BookHistoryResponse::of)
)
}
}
}
data class BookHistoryResponse(
val name: String, //책 이름
val isReturn: Boolean,
){
companion object{
fun of(history:UserLoanHistory): BookHistoryResponse{
return BookHistoryResponse(
name = history.bookName,
isReturn = history.isReturn,
)
}
}
}
UserService
@Transactional(readOnly = true)
fun getUserLoanHistories(): List<UserLoanHistoryResponse> {
return userRepository.findAllWithHistories().map(UserLoanHistoryResponse::of)
}
UserLoanHistory
@Entity
class UserLoanHistory(
@ManyToOne
val user: User,
val bookName: String,
@Enumerated(EnumType.STRING)
var status: UserLoanStatus = UserLoanStatus.LOANED, //Enum으로 설계하는것이 더 좋다
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
) {
val isReturn:Boolean
get() = this.status == UserLoanStatus.RETURNED
fun doReturn() {
this.status = UserLoanStatus.RETURNED
}
companion object {
fun fixture(
user: User,
bookName: String = "이상한 나라의 엘리스",
status: UserLoanStatus = UserLoanStatus.LOANED,
id: Long? = null,
): UserLoanHistory {
return UserLoanHistory(
user = user,
bookName = bookName,
status = status,
id = id,
)
}
}
}
- BookHistoryResponse와 UserLoanHistoryResponse에 정적 펙토리 of 메서드를 만들고 이를 활용하도록 코드를 수정한다.
- isReturn 같은 경우 custom getter에서 status 값이 RETURNED와 같은지를 판단해주는 로직을 작성하여 반환하도록 한다(도메인 객체에 로직을 넣어두면 나중에 재활용이 가능하다)
- Service 계층은 이제 본래 목적인 트랜잭션 관리, repository를 통한 도메인 조회 및 제어에 집중할 수 있고, 데이터를 Dto에 매핑하는 역할은 Dto에 넘기게 된 것이다.
- 유지보수도 더 쉽고, 용이해졌다
728x90
'실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기' 카테고리의 다른 글
| Ch04. 요구사항 추가(책 통계, QueryDSL) - 책 통계 테스트 코드와 리펙토링 (0) | 2022.11.07 |
|---|---|
| Ch04. 요구사항 추가(책 통계, QueryDSL) - 책 통계 요구사항 추가 (0) | 2022.11.07 |
| Ch03. 요구사항 추가(type, 대출현황) - 유저 대출 현황 - 테스트 코드 작성 (0) | 2022.11.06 |
| Ch03. 요구사항 추가(type, 대출현황) - 도서 대출 현황 요구사항 추가 (0) | 2022.11.06 |
| Ch03. 요구사항 추가(type, 대출현황) - UserLoanHistory의 isReturn을 Enum으로 변경하기 (0) | 2022.11.05 |