개괄

JPA 강의를 듣다 보면 나처럼 두뇌가 느리게 굴러가는 사람들은 이상한 결론에 도달할 것이다.

"음... Querydsl만 있으면 jpa를 완벽 해결할 수 있겠구만"

"음... fetch join만 사용하면 join과 관련된 최적화 이슈를 다 해결할 수 있겠구만"

맞는 말이긴 하다.

아마 "Spring Boot를 배워놓으면 Spring은 배우지 않아도 될거야, Spring Boot가 다 해결해주니까!"와 비슷한 개념일 것이다.

 

보통 N+1 문제를 해결하기 위해

ManyToOne을 Lazy Loading으로 걸어주고 페치 조인으로 추가쿼리가 나가지 않게끔 해결하곤 한다.

그러나 이게 습관이 되면 그냥 다 fetch join을 걸어준다.

그러나 fetch join은 연관된 정보를 미리 다 땡겨옴으로서 오히려 단순 조회에서는 성능이 더 안 나올때도 있다.

예를 들어 다음의 엔티티 테이블이 있다고 생각해보자.

 

[Project] 1 : N [Project_User] N : 1 [User]

 

Project와 User의 다대다 관계를 해결하기 위해 일대다 관계로 두번 나누었고, 행위엔티티를 기록하기 위해 Project_User라는 테이블을 만들어 주었다.

 

그리고 User에 있는 id와 연관된 모든 project를 가지고 오고 싶다고 생각해보자.

JPQL로 나타내면

"select p from Project p join p.projectUserList pu where pu.user.userId = :userid"일 것이다.

그러나 나는 여기에 join fetch를 걸어 주어서 다음과 같은 JPQL문이 나가게 되었다.

"select p from Project p join fetch p.projectUserList pu where pu.user.userId = :userid"

join fetch를 걸면 쿼리가 2방이 나가게 된다. 살펴보자.

Hibernate: 
    select
        ...
        projectuse1_.project_id as project_2_5_1_,
        projectuse1_.user_id as user_id3_5_1_,
        projectuse1_.project_id as project_2_5_0__,
        projectuse1_.id as id1_5_0__ 
    from
        project project0_ 
    inner join
        project_user projectuse1_ 
            on project0_.project_id=projectuse1_.project_id 
    where
        projectuse1_.user_id=?
Hibernate: 
    select
        ...
    from
        users user0_ 
    where
        user0_.user_id=?

project에 대한 정보와 projectuser에 대한 정보를 select하는 쿼리와 동시에 projectuser까지 전부 조회해버리는 쿼리가 나가서 결국 user에 대한 정보까지 긁어오는 것을 볼 수 있다.

그러나 user까지 조회할 것이 아니라면 단순히 join만 걸어주면 쿼리를 훨씬 절약하게 된다.

join만 걸고 나가는 쿼리는 다음과 같다.

Hibernate: 
    select
        ...
    from
        project project0_ 
    inner join
        project_user projectuse1_ 
            on project0_.project_id=projectuse1_.project_id 
    where
        projectuse1_.user_id=?

 

단순히 프로젝트에 대한 정보만 가지고 오는 것을 볼 수 있다.

물론 이 조회에서 더 나아가서 user info까지 말아서 데이터를 전송해야 하는 입장이면 join fetch가 맞다.

그러나 프로젝트에 대한 정보만 조회하는데 굳이 쿼리를 한 번 더 쓸 일은 아닌 것 같다.

 

결국 Join과 Fetch join에 대한 이해가 있어야 최적화도 가능하다.

 

뭐든지 알고 쓰자.

'DB > JPA' 카테고리의 다른 글

QueryDsl Custom Repository 명명 규칙  (0) 2023.03.31
왜 GenerationType.Identity를 써 주는 것일까?  (0) 2023.02.23

개괄

프로젝트 내의 jpql로 이루어진 조회 / 수정 쿼리들을 Querydsl을 이용한 쿼리로 migration하는 날

오늘 갑자기 CQS 원칙을 지키고 싶어져서 다음과 같이 Repository를 만들어 주었다.

 

repository

|-ProjectRepository

|-ProjectViewRepositoryCustom

|-ProjectViewRepositoryImpl

 

그리고 ProjectRepository 안의 userId로 projectResponseDto를 찾는 다음과 같은 쿼리도 변경해주어야 했다.

- ProjectRepository

@Query("select new com.ddalggak.finalproject.domain.project.dto.ProjectBriefResponseDto(p.projectId, p.projectTitle, p.thumbnail) from Project p join p.projectUserList pu where pu.user.userId = :userId")
List<ProjectBriefResponseDto> findAllinDtoByUserId(Long userId);

-ProjectViewRepositoryCustom

List<ProjectBriefResponseDto> findProjectAllByUserId(Long userId);

-ProjectViewRepositoryImpl

@Override
	public List<ProjectBriefResponseDto> findProjectAllByUserId(Long userId) {
		return queryFactory.select(new QProjectBriefResponseDto(
				project.projectId,
				project.projectTitle,
				project.thumbnail
			))
			.from(project)
			.join(project.projectUserList, projectUser)
			.where(projectUser.user.userId.eq(userId))
			.fetch();
	}

분명히 연관관계도 문제가 없었고, 모든게 잘 되었다고 생각했는데 뜻하지 않은 오류를 만났다.

 

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'projectController' defined in file [~/ProjectController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'projectService' defined in file [~/ProjectViewRepositoryCustom.findProjectAllByUserId(java.lang.Long)! No property 'userId' found for type 'Project'

ProjectViewRepositoryCustom 내부에 있는 findProjectAllByUserId 메소드의 파라미터로 받는 userId가 Project 내부에 있지 않아 UnsatisfiedDependencyException이 걸린 것이었다.

 

아니 그럼 Querydsl에서는 join문 못쓰나? 라고 생각해봤지만 그럴 리가 없었다.

그렇다고 user의 id값을 projectdto에 넣어주는것도 말이 되지 않았다.

그래서 Project 안에 있는 ProjectUser라는 행위 엔티티에서 User를 뽑아내어 어떻게든 UserId와 연결지어주려는

장장 3시간의 혈투가 시작되었다.

 

join문을 걸었다가 inner join을 걸었다가 join on을 걸었다가 참 많은 삽질 끝에 메소드 구현체를 지워봤는데도 똑같은 오류가 떴다.

 

결국

List<ProjectBriefResponseDto> findProjectAllByUserId(Long userId);

이 문장에서 오류가 있던 것이었는데, 이걸 어떻게 고치나 열심히 고민하다가 결국 흑마법을 사용하기로 결심했다.

그래서 혹시 Project와 Repository의 연결에 문제가 있는게 아닐까 싶어서

ProjectViewRepositoryCustom -> ProjectRepositoryCustom으로 리팩터링해봤더니 된다...

 

또, 과거에는 ProjectRepositoryCustom을 상속받으려면 ProjectRepositoryImpl이라고 했어야 했는데, 이제 ~CustomImpl도 가능하다.

 

결론

다음과 같이 명명을 해 주어야 한다.

  • Custom하려는 Repo는 상속받을 Repository와 이름이 같아야 한다.
    • 만약 CQS를 시행하고자 했으면 조회를 위한 Repository를 ProjectViewRepository로 따로 만들어 주어야 한다.
  • 이제는 Custom Repo를 구현하는 Impl class에는 CustomImpl로 명명해 이름에 일관성이 있게 하자.

'DB > JPA' 카테고리의 다른 글

Join과 fetch join, 알고 쓰자.  (0) 2023.03.31
왜 GenerationType.Identity를 써 주는 것일까?  (0) 2023.02.23

보통 JPA를 이용할 때 인강을 들으면 무조건

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

이 두개를 써준다. 왜 써줄까?

먼저 @Id에 대한 설명을 보자.

@Id를 붙이면 JPA에서 자동으로 PK라고 인식을 해 준다. 그리고 래퍼 타입, date class, bigdecimal 등으로 써 주어야 함을 강제한다.

@GeneratedValue를 써 주면 우리가 ++sequence를 해 주지 않아도, Auto-Increment 덕에 알아서 하나씩 올라간다.

Generation Type에는  Identity, Auto, Sequence등이 있는데, 보통은 겹치지 않는 Identity를 사용한다.

Auto를 사용하게 되면 다음과 같은 일이 일어난다.

게시글, 댓글, 댓글 순으로 게시물을 저장할 경우 우리는 보통

게시글1, 댓글1, 댓글2로 가져가길 원하지만, Auto 타입을 주면

게시글1, 댓글2, 댓글3이라는 문제가 일어난다.

 

나중에 테이블 전략과 db의 입장에서 다시 설명하기로 하고, 오늘은 이렇게 넘어가도록 하자.

'DB > JPA' 카테고리의 다른 글

Join과 fetch join, 알고 쓰자.  (0) 2023.03.31
QueryDsl Custom Repository 명명 규칙  (0) 2023.03.31

+ Recent posts