Search
〰️

개인 프로젝트에 Fetch Join 적용 → 성능 최적화하기

배경

부끄럽게도ㅠㅡㅠ 실전! 스프링부트와 JPA 활용편 #2와 자바 ORM 표준 JPA 프로그래밍 강의를 수강하면서, 이전에 JPA Study + 개인 프로젝트 성으로 간단히 만들었던 Rest API가 성능 최적화가 전혀 되어있지 않음을 깨달았다..ㅎ  
... 객체 그래프를 탐색할때마다 Query가 미친듯이 나갔던 것이다...
대표적으로 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을 적절히 사용하자!

참고