ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring boot] JPA N + 1
    프로젝트/아카이뷰 2024. 1. 21. 21:02

    JPA N + 1 왜 궁금했을까❓

    ArchiVIEW 프로젝트에서 JPA를 활용하여 DB 데이터를 조회하고 있었다. console을 보니 MyBatis에서는 하나의 쿼리로 처리했던 것들이 여러 개의 쿼리로 나눠져서 나가고 있는 것을 확인했다. 이를 찾아보니 N + 1 문제라고 지칭하는 것을 알았고 이를 해결해보려고 한다.

     

     

    [Spring boot] JPA(Java Persistence API)

    JPA(Java Persistence API) 왜 궁금했을까❓SSAFY 1학기 프로젝트인 Share Your Trip을 할 때, MyBatis를 사용해서 DB에 Query를 날리곤 했는데 타입이나 변수명 등 Java 객체와 불일치 하는 경우가 많아 오류가 많

    pslog.co.kr

     

     

    [Spring boot] JPA save() & Dirty Checking

    JPA 메소드인 SAVE() 왜 궁금했을까❓JPA의 save()를 이용하여 객체를 데이터베이스에 저장하곤 했는데 Dirty Checking을 통해 저장하는 방식이 존재한다는 것을 알고 그 원리가 궁금해서 save()와 함께 학

    pslog.co.kr

    위 포스팅들을 통해 JPA에 대한 개념들을 이해할 수 있다.

    Hash Tag 조회 API

    태그에는 Job과 Cs가 존재하는데 Job과 Cs의 속성과 코드는 똑같기에 Job을 기준으로 설명하도록 하겠다.
    @Entity // 직무 대분류 테이블
    @Getter
    @Table(name = "job_main")
    public class JobMain {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
    
        @Column(name = "name", length = 64)
        @NotNull
        private String name;
    
        @OneToMany(mappedBy = "jobMain", fetch = FetchType.LAZY)
        private List<JobSub> jobSubList = new ArrayList<>();
    
        public CommonDto.jobMainDto toDto() {
            return CommonDto.jobMainDto.builder()
                    .name(name)
                    .jobSubList(jobSubList.stream()
                            .map(JobSub::getName)
                            .collect(Collectors.toList()))
                    .build();
        }
    }
    @Entity // 직무 소분류
    @Getter
    @Table(name = "job_sub")
    public class JobSub {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
    
        @Column(name = "name", length = 64)
        private String name;
    
        @ManyToOne
        @JoinColumn(name = "job_main_id")
        private JobMain jobMain;
    }
    @Override
    public CommonDto.tagResponseDto tagList() {
        List<CommonDto.csMainDto> csList = csMainRepository.findAll().stream()
                .map(CsMain::toDto)
                .toList();
        List<CommonDto.jobMainDto> jsList = jobMainRepository.findAll().stream()
                .map(JobMain::toDto)
                .toList();
        return CommonDto.tagResponseDto.builder()
                .csList(csList)
                .jsList(jsList)
                .build();
     }

    • 기존의 tagList Method의 코드를 보면 Spring Data JPA의 findAll을 이용하여 부모 태그를 조회하고 oneToMany로 엮여있는 SubTag를 지연 로딩을 통해 가져온다.
    • 과정을 보면, 메인 태그를 먼저 조회하고 subTag가 사용되면 해당 태그를 지연 로딩을 통해 DB에 접근하여 조회하고 있는 것을 확인할 수 있다.
    • 결국, 메인 태그 1개와 이와 연관된 서브 태그 N개의 쿼리가 발생하는 것을 알 수 있다. 해당 부분을 fetch join을 이용한다면 하나의 쿼리로 연관된 엔티티의 데이터들을 가져와 영속성 컨텍스트에 넣기 때문에 해결할 수 있을 것이다.
    • fetch join을 사용하기 위해 JPQL이 아닌 Query DSL을 사용했는데 그 이유는 Query DSL 포스팅에 작성해놓았다.
    @Override
    public List<CommonDto.jobMainDto> getJobTagList() {
        QJobMain qJobMain = QJobMain.jobMain;
        QJobSub qJobSub = QJobSub.jobSub;
    
        return factory.selectFrom(qJobMain)
                .leftJoin(qJobMain.jobSubList, qJobSub)
                .fetchJoin()
                //.distinct()
                .fetch().stream()
                .map(JobMain::toDto)
                .toList();
    }
    @Override
    public CommonDto.tagResponseDto tagList() {
        List<CommonDto.csMainDto> csList = csMainRepository.getCsTagList();
        List<CommonDto.jobMainDto> jsList = jobMainRepository.getJobTagList();
    
    	return CommonDto.tagResponseDto.builder()
               .csList(csList)
               .jsList(jsList)
               .build();
    }

    • 위와 같이 Query DSL을 사용하여 하나의 쿼리를 보낼 수 있었고 N + 1의 문제를 해결할 수 있었다.
    • 또한, Query DSL에서 distinct가 주석되어 있는 것을 확인할 수 있는데 hibernate 6.0 밑에 버전에서는 해당 코드가 존재해야 한다.
    • 그 이유는 메인-서브1, 메인-서브2, ... , 메인-서브N 식으로 카테시안 곱으로 인해 메인의 중복이 발생할 것이다.
    • 원하는 결과 값 - 소프트웨어 개발자-프론트엔드, 백엔드
    • 실제 결과 값- 소프트웨어 개발자-프론트엔드, 소프트웨어 개발자-백엔드
    • 이러한 문제를 해결하고 distinct를 사용하면 메인의 중복이 사라지고 원하는 결과 값을 출력할 수 있을 것이다.
    • SQL의 distinct와 Query DSL, JPQL의 distinct는 약간 다르다고 할 수 있는데 SQL은 row가 기준이고 Query DSL, JPQL은 중복되는 Entity를 제거시켜 준다.

    채용 공고 및 질문 검색 API

    @Override
    public List<RecruitDto.DetailListResponseDto> searchAll(RecruitDto.DetailListRequestDto requestDto) {
        BooleanBuilder builder = new BooleanBuilder();
        QRecruit recruit = QRecruit.recruit;
        QCompany company = QCompany.company;
    
        StringExpression start = Expressions.stringTemplate("FUNCTION('DATE_FORMAT', {0}, '%Y-%m')", recruit.start);
        StringExpression end = Expressions.stringTemplate("FUNCTION('DATE_FORMAT', {0}, '%Y-%m')", recruit.end);
    
        builder.and(start.eq(requestDto.getDate()).or(end.eq(requestDto.getDate())));
    
        if(requestDto.getCompanyId() != 0) {
            builder.and(recruit.company.id.eq(requestDto.getCompanyId()));
        }
    
        return factory
                .selectFrom(recruit)
                .leftJoin(recruit.company, company)
                .fetchJoin()
                .where(builder)
                .fetch().stream()
                .map(Recruit::toDetailListDto)
                .toList();
    }
Designed by Tistory.