배경
•
부끄럽게도ㅠㅡㅠ 실전! 스프링부트와 JPA 활용편 #2와 자바 ORM 표준 JPA 프로그래밍 강의를 수강하면서, 이전에 JPA Study + 개인 프로젝트 성으로 간단히 만들었던 Rest API가 성능 최적화가 전혀 되어있지 않음을 깨달았다..ㅎ
•
•
대표적으로 JPA를 잘못 사용한 예인 N+1 Problem이 바로 내 코드의 문제점이었고, 학습한 JPA Fetch Join을 통해 성능을 조금 끌어올려보았다. (머쓱)
◦
영한님께서 말씀하시길 실무에서 처음 JPA를 사용할 때 많이들 범하는 실수... 라고 하셨는데 정말로 정확히 나의 케이스와 일치했다..^^;;
프로젝트의 Entity구성과 API는 아래와 같다.
AS-IS
Domain
•
Member (사용자)
public class Member {
@OneToMany(mappedBy = "accountHolder")
private List<Account> accounts = new ArrayList<>();
}
Java
복사
•
Account (계좌)
public class Account implements Serializable {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_holder", foreignKey = @ForeignKey(name = "fk_account_to_member"))
private Member accountHolder;
@OneToMany(mappedBy = "account")
private List<AccountHistory> accountHistories = new ArrayList<>();
}
Java
복사
•
AccountHistory (계좌 거래내역)
public class AccountHistory {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_number", referencedColumnName = "account_number", foreignKey = @ForeignKey(name = "fk_history_to_account"))
private Account account;
}
Java
복사
연관관계
Member : Account = 1 : N (양방향), LAZY
Account : AccountHistory = 1 : N (양방향), LAZY
Controller / DTO
•
AccountController
◦
findAllAccounts() : 전체 계좌 목록 조회
@GetMapping
public ResponseEntity<List<AccountResponse>> findAllAccounts() {
List<AccountResponse> accounts = accountService.findAllAccounts();
return ResponseEntity.ok(accounts);
}
Java
복사
◦
AccountResponse : 전체 계좌 목록 반환 DTO
public class AccountResponse {
private String accountNumber;
private MemberResponse accountHolder;
}
Java
복사
→ 전체 계좌 목록 조회 API 호출 시, accountNumber(계좌번호)와 accountHolder(계좌 소유주) 반환
Service
•
AccountService
◦
findAllAccounts() : 전체 계좌 목록 조회
public List<AccountResponse> findAllAccounts() {
return AccountResponse.toList(accountRepository.findAll());
}
Java
복사
Repository
•
AccountRepository
◦
findAll() : JpaRepository의 findAll();
▪
전체 instance(all entities) return
결과
1) 전체 계좌 목록 조회 API 호출
2) findAll 메소드 실행
3) Account 테이블 조회
→ 총 50건의 Account 조회
4) Member 테이블 조회
•
3)에서 조회한 Account의 AccountHolder 조회 (Member 조회)
◦
Entity Manager에 각 AccountHolder의 값이 존재하지 않으므로, 조회 쿼리 호출!
→ 50건의 Account에 대한 AccountHolder(Member) 50건 조회
즉, Account 50건에 대한 각 각의 AccountHolder를 조회하는 쿼리 호출
※ 1 (Account 조회) + N (Member조회) 문제 발생
AccountResponse의 경우, 연관관계를 기준으로 Account와 AccountHolder 객체에 대한 연관관계만 조회하면 되므로 1+50번(N)의 쿼리만 호출되지만, Account + AccountHistory + AccountHolder 등 여러 연관관계에 대한 객체 그래프 탐색이 필요하다면?
→ 1+N+N+N ..... 의 쿼리가 호출되게 되어 성능 저하를 유발한다.. 
1+N문제를 Fetch Join을 통해 해결해보자!
TO-BE
•
Presentation Layer는 문제가 없으므로 별도의 로직 변경은 없음!
Service
•
AccountService
◦
findAllAccounts() : 전체 계좌 목록 조회
▪
기본 findAll 메소드 → Member와 함께 Account를 조회하는 Repository Method 호출
public List<AccountResponse> findAllAccounts() {
return AccountResponse.toList(accountRepository.findAllWithMember());
}
Java
복사
Repository
•
AccountRepository
◦
findAllWithMember() : Account 조회 시 Member와 Fetch Join
@Query("select a " +
"from Account a " +
"join fetch a.accountHolder ah")
List<Account> findAllWithMember();
Java
복사
결과
1) 전체 계좌 목록 조회 API 호출
2) findAllWithMember 메소드 실행
3) Account + Member 테이블 Fetch Join하여 한 번에 조회
•
inner join을 통해 Account와 Member를 join하여 조회한 값을 모두 Entity Manager에 올린다.
◦
모든 Account의 AccountHolder값이 영속성 컨텍스트에 존재하므로, 이후 별도 조회 쿼리 발생하지 않음
→ 하나의 Join Query만 호출! 어플리케이션의 성능 
연관관계가 복잡하게 얽혀 있는 객체를 다량으로 조회할 때는 1+N 문제가 발생되지 않도록 Fetch Join을 적절히 사용하자!