JPA로 테이블을 설계하면서 우선 지킬 것이 몇 가지 있었다.
1. 일단 LAZY로 선언
EAGER로 선언하면 연관 관계에 있는 Entity까지 전부 쿼리를 날려서 조회하기 때문에 쓰이지 않는 경우에 너무 자원 소모가 심하다고 생각했다. 그래서 프록시 객체로 조회하도록 LAZY를 우선으로 사용했다.
2. 연관 관계를 사용해야 할 경우 FETCH JOIN을 사용해서 하나의 쿼리로 처리하기.
결국 LAZY를 사용해도 해당 객체에 접근하게 될 경우 쿼리를 날려서 가져오게 되면서 N+1 문제가 발생한다.
이를 방지하기 위해 FETCH JOIN을 사용해서 하나의 쿼리로 연관 관계에 있는 객체를 가져오도록 만들었다.
그러다가 문득 FETCH JOIN을 안쓰면 진짜 쿼리가 N+1로 날라가는지 궁금해서 테스트를 해봤다.
// 스크럼 팀 조회
public ScrumRoomListResponseDTO findScrums(String accessToken, Long teamId) {
Long userId = jwtUtil.getUserId(accessToken);
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("유저 없음"));
Team team = teamRepository.findById(teamId)
.orElseThrow(() -> new TeamNotFoundException("팀이 존재하지 않습니다."));
InviteTeamList inviteTeamList = inviteTeamListRepository.findByUserAndTeamAndParticipantIsTrue(user, team)
.orElseThrow(() -> new NonParticipantUserException("해당 유저가 팀에 참여하지 않았습니다."));
Optional<List<Scrum>> byTeam = scrumRepository.findByTeamWithUserFetchJoin(team);
List<Scrum> scrums = byTeam.orElse(new ArrayList<>());
List<ScrumRoomDTO> scrumRoomDTOList = new ArrayList<>();
for(Scrum s : scrums){
ScrumRoomDTO scrumRoomDTO = ScrumRoomDTO.builder()
.scrumId(s.getId())
.name(s.getName())
.profileImage(s.getUser().getProfileImage())
.maxMember(s.getMaxMember())
.currentMember(s.getCurrentMember())
.nickname(s.getUser().getNickname()).build();
scrumRoomDTOList.add(scrumRoomDTO);
}
return new ScrumRoomListResponseDTO(scrumRoomDTOList);
}
위 코드는 해당 스크럼의 팀을 조회하는 service다.
현재 Scrum이라는 Entity에는 User가 1:1 관계로 설정돼있다.
s.getUser().getProfileImage() 이 부분에서 살펴보면 스크럼 -> 유저 -> 프로필 사진 순으로 접근하게 된다.
결국 스크럼이라는 객체에서 유저라는 객체를 꺼내 프로필 사진을 찾는 건데, FETCH JOIN을 사용하지 않을 경우 예상대로라면 Scrum 조회 쿼리, User 조회 쿼리 이렇게 2개가 나가야한다.
그래서 우선 FETCH JOIN을 제거하기로 했다.
@Query("SELECT s FROM Scrum s JOIN FETCH s.user WHERE s.team = :team AND s.deleteDate IS NULL")
Optional<List<Scrum>> findByTeamWithUserFetchJoin(Team team);
위 코드를 아래로 변환
@Query("SELECT s FROM Scrum s WHERE s.team = :team AND s.deleteDate IS NULL")
Optional<List<Scrum>> findByTeamWithUserFetchJoin(Team team);
위 코드가 FETCH JOIN을 적용한 코드고 아래는 그냥 단순하게 team와 삭제시간이 널인 객체를 찾아오도록 했다.
이렇게 service를 동작시키면
내가 예상했던 거와는 다르게 User를 조회하는 쿼리가 나가지 않는다.
혹시 몰라서 FETCH JOIN을 붙이고 다시 동작시켜보면
이렇게 User가 join이 돼서 쿼리가 하나로 나가는 걸 볼 수 있다.
여기서 1차 캐시가 생각이 났다.
이미 내 서비스에서는 User라는 객체를 조회해서 1차 캐시에 저장해놓는다.
그렇기에 Scrum 객체에서 User를 조회한다고 하면 1차 캐시에 이미 User가 있기 때문에 추가 쿼리를 날리지 않는 것이다.
결국 FETCH JOIN을 사용하지 않아도 문제가 없다는 결론이 내려졌다.
무조건 FETCH JOIN을 써야 N+1 문제를 방지하고 리소스적으로 이득을 보는 줄 알았는데 그게 아닌 경우도 있었다.
'기타' 카테고리의 다른 글
어떻게 HTTP는 로그인된 상태를 판단할까? (0) | 2024.02.07 |
---|---|
분명 JPA에서는 쿼리를 모았다가 날리는 걸로 알았는데? (0) | 2024.02.06 |
JWT를 쿠키에 저장해서 사용하는 방법 (0) | 2023.12.31 |
인텔리제이에서 Lombok을 사용해보자.(IntelliJ) (0) | 2023.06.14 |