-
[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(); }
'프로젝트 > 아카이뷰' 카테고리의 다른 글
[Spring boot] Query DSL 동적 쿼리 (0) 2024.01.23 [Spring boot] REST, REST API, RESTful (0) 2024.01.22 [Spring boot] Query DSL (0) 2024.01.20 [Spring boot] JPA save() & Dirty Checking (0) 2024.01.19 [Spring boot] NoArgsConstructor에 Protected를 쓰는 이유 (0) 2024.01.18