응애개발자
article thumbnail
728x90

문제상황

ERD구조는 위와 같은 상황에서 유저들의 목록을 가져오는데 부서이름과 직급이 필요했다.

데이터베이스에서는 회원 총 21명, 부서 5개, 직책 5개가 존재했다.

ResponseDTO

그래서 리스트 각각에는 밑과 같은 ResponseDTO정보들을 넣어주었다.

@Data
@Builder
public class UserListResponseDTO {
    private Long id;
    private String name;
    private String email;
    private Role role;
    private String profileImage;
    private String departmentName;
    private String positionName;

    //DTO메서드로 재사용성을 높임
    public static UserListResponseDTO toDTO(User user) {
        return UserListResponseDTO.builder()
                .id(user.getId())
                .name(user.getName())
                .email(user.getEmail())
                .role(user.getRole())
                .profileImage(user.getProfileImage())
                .departmentName(user.getDepartment().getDepartment_name())
                .positionName(user.getPosition().getPosition_name())
                .build();
    }
}

 

Service

   @Override
    @Transactional(readOnly = true)
    public List<UserListResponseDTO> listUsers() {
        return userRepository.findAll().stream()
                .map(UserListResponseDTO::toDTO)
                .collect(Collectors.toList());
    }

 

이처럼 서비스를 통해 유저 목록을 조회할 때, 각 유저에 대해 부서와 직급 정보를 가져오는 과정에서 N+1 문제가 발생했습니다. 구체적으로는 다음과 같은 쿼리들이 실행되었습니다:

더보기

N+1문제

연관관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(부서 5개, 직급 5개) 만큼 연관된 엔티티를 조회하기 위해 추가적으로 발생하는 문제

  1. 유저 21명을 조회하는 쿼리 1번
  2. 각 유저에 대한 부서 정보를 조회하는 쿼리 5번
  3. 각 유저에 대한 직급 정보를 조회하는 쿼리 5번

총 11번의 쿼리가 발생했다. 이는 각 유저에 대해 부서와 직급 정보를 지연 로딩(Lazy Loading)으로 가져오기 때문에 발생하는 문제입니다. 동일한 부서와 직급을 가진 유저들이 많아 실제 쿼리 실행 횟수는 줄어들었지만, 여전히 비효율적이었다. 

 

따라서 이 부분을 조금 더 효율적으로 쿼리를 한번만 날려서 모든 정보를 가져오고 싶었기에 Fetch Join을 사용하였다.

 

해결방법  : Fetch Join

유저를 가져오는데 유저와 연관된 부서 그리고 직책을 가져오는 Fetch Join을 사용하였다.

Repository

    @Query("SELECT u FROM User u JOIN FETCH u.department JOIN FETCH u.position")
    List<User> findAllWithDepartmentAndPosition();

 

Service

    @Override
    @Transactional(readOnly = true)
    public List<UserListResponseDTO> listUsers() {
        List<User> users = userRepository.findAllWithDepartmentAndPosition();
        return users.stream()
                .map(UserListResponseDTO::toDTO)
                .collect(Collectors.toList());
    }

이처럼 Fetch Join을 사용하여 한번의 쿼리 요청으로 유저와 연관된 부서와 직책을 가져올 수 있다. 이렇게 함으로써 N+1문제를 해결하고 성능을 향상시킬 수 있었다.

성능 테스트

추가적으로 성능에서 얼마나 차이가 발생할지 궁금하여 Jmeter를 활용하여 성능에서 얼마나 차이가 나는지 확인해보았다.

10000명이 1초에 1번씩 요청을 보냈을때 의 경우 N+1문제가 발생했을때와 FetchJoin으로 쿼리를 최적화했을때 차이를 알아보았다.

 

 

FetchJoin을 사용했을때

 

FetchJoin을 사용하지 않았을때

 

오류 부분에서는 내 서버(노트북)가 효율이 좋지 않아서 발생한것이고 중요한 점은 처리량 부분에서 패치조인을 사용했을때 결국 사용하지 않았을때보다 성능 부분에서 조금 더 향상되었다.

 

결국 패치조인을 사용한다면 db connection이 더 일어나기 때문에 db에 부하를 주는것보다 커넥션을 줄이는 것이 낫다는 결론이 났다.

profile

응애개발자

@Eungae-D

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!