[플러스 주차 개인과제] 레벨 2 문제 해결 과정

2025. 3. 13. 21:55·Framework/Spring
목차
  1. 1. JPA Cascade
  2. (1) 문제 요구사항
  3. (2) 문제 풀이
  4. 2. N+1 문제
  5. (1) 문제 요구사항
  6. (2) 문제 접근
  7. (3) 문제 해결
  8. 3. QueryDSL
  9. (1) 문제 요구 사항
  10. (2) 의존성 추가
  11. (3) JPAConfiguration 클래스 생성
  12. (4) 문제 해결 과정
  13. (5) 트러블 슈팅

1. JPA Cascade


(1) 문제 요구사항


 

 

 

(2) 문제 풀이


기존 코드에서는 Todo 엔티티를 저장할 때, 연관된 Manager 엔티티가 자동으로 저장되지 않습니다.

따라서 데이터베이스에 Manager 정보가 남지 않아, 별도로 저장해야 하는 번거로움이 있습니다.

 

이 문제를 해결 하기 위해서 Todo 엔티티의 managers필드에 cascade = CascadeType.PERSIST를 적용하였습니다.

 

[수정한 코드]

public class Todo extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String contents;
private String weather;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "todo", cascade = CascadeType.REMOVE)
private List<Comment> comments = new ArrayList<>();
// ✅ cascade 추가
@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)
private List<Manager> managers = new ArrayList<>();
public Todo(String title, String contents, String weather, User user) {
this.title = title;
this.contents = contents;
this.weather = weather;
this.user = user;
// ✅ Todo를 생성한 유저를 자동으로 Manager로 등록
this.managers.add(new Manager(user, this));
}
}

 

🔍 cascade란?
Cascade(영속성 전이)란 JPA에서 부모 엔티티가 특정 작업(저장, 삭제)등을 수행할 때, 
연관된 자식 엔티티도 함께 동일한 작업을 수행하도록 하는 기능입니다.
즉, 부모 엔티티의 작업이 자식 엔티티에게 자동으로 전이되는 것입니다.

 

 

 

2. N+1 문제


(1) 문제 요구사항


 

 

 

(2) 문제 접근


[comment 엔티티]

@Getter
@Entity
@NoArgsConstructor
@Table(name = "comments")
public class Comment extends Timestamped {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String contents;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "todo_id", nullable = false)
private Todo todo;
public Comment(String contents, User user, Todo todo) {
this.contents = contents;
this.user = user;
this.todo = todo;
}
}

 

현재 Comment 엔티티에는 User와 Todo 엔티티가 fetchType.Lazy 설정으로,

다대일(@ManyToOne) 연관관계를 맺고 있습니다.

 

이러한 상태에서 코멘트를 조회하는 메소드를 실행하면 N+1 문제가 발생할 수 있습니다.

그 이유는, JPA에서 LAZY로딩을 사용하면 기본적으로 연관된 엔티티(User, Todo)를 즉시 가져오지 않고,

실제로 접근하는 순간에 추가적인 쿼리를 실행하기 때문입니다.

 

[예시]
코멘트를 조회하는 1개의 쿼리 
(SELECT * FROM comments WHERE todo_id = ?;) 가 먼저 실행된 후,
각 Comment가 참조하는 User와 Todo를 조회하기 위해
N개의 추가적인 SELECT 쿼리가 발생하여
총 N+1개의 쿼리가 실행되는 문제가 발생합니다.

 

이러한 N+1문제는 쿼리 성능을 저하시킬 수 있으므로,

JOIN FETCH를 활용한 즉시 로딩(Eager Fetch) 또는 EntityGraph 등을 사용하여 최적화하는 것이 필요합니다. 

 

 

 

(3) 문제 해결


@Query("SELECT DISTINCT c FROM Comment c JOIN FETCH c.user JOIN FETCH c.todo WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);

JOIN FETCH를 사용하여, N+1 문제를 해결하였습니다.

 

Comment 엔티티는 User와 Todo 엔티티와 각각 다대일(@ManyToOne) 관계를 맺고 있으므로,

두 엔티티를 함께 조회할 수 있도록 JOIN FETCH를 적용하였습니다.

 

또한, DISTINCT 키워드를 사용하여 쿼리 실행 시 중복된 Comment 객체가 조회되는 문제를 방지하고,

불필요한 데이터 중복을 제거하여 더 효율적인 데이터 조회가 가능하도록 개선하였습니다.

 

 

 

3. QueryDSL


(1) 문제 요구 사항


TodoService.getTodo 메소드

 

 

 

(2) 의존성 추가


// build.gradle에 QueryDSL 의존성 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

 

 

 

(3) JPAConfiguration 클래스 생성


import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JPAConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}

config 패키지에 JPAConfiguration 클래스를 생성하였다.

 

 

(4) 문제 해결 과정


🔍 QueryDSL이란?
엔티티의 매핑 정보를 활용하여 쿼리에 적합한 전용 클래스(Q클래스)로 재구성해주는 기술입니다.
타입 안정성을 보장하면서 SQL, JPQL을 더 편리하게 작성할 수 있습니다.
JPAQueryFactory를 활용하여 Q클래스를 이용한 동적 쿼리를 쉽게 작성할 수 있습니다.
🔍 JPAQueryFactory란?
QueryDSL에서 제공하는 쿼리 실행 도구로,
Q클래스를 활용하여 동적으로 쿼리를 생성하고 실행할 수 있도록 도와줍니다.

 

 

[수정 전 TodoRepository의 쿼리]

@Query("SELECT t FROM Todo t " +
"LEFT JOIN t.user " +
"WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

 

[문제 해결 과정]
1️⃣ TodoQueryRepository 인터페이스 생성
2️⃣ TodoQueryRepositoryImpl 클래스에서 QueryDSL 적용
3️⃣ 기존 TodoRepository에 TodoQueryRepository 상속
4️⃣ 서비스 코드 수정 없이 QueryDSL 적용 완료

 

 

 

1️⃣ TodoQueryRepository 인터페이스 생성

public interface TodoRepositoryQuery {
Optional<Todo> findByIdWithUser(long todoId);
}

QueryDSL을 활용한 동적 쿼리를 별도로 관리하기 위해

쿼리 전용 리포지토리 인터페이스를 생성하였습니다.

 

QueryDSL을 사용할 메소드만 정의 하여, 기존 JpaRepository와 분리하였습니다.

 

 

 

2️⃣ TodoQueryRepositoryImpl 클래스에서 QueryDSL 적용

📌 QueryDSL의 장점
JPQL과 달리 문자열을 사용하지 않아, 가독성이 뛰어난 코드를 작성할 수 있고,
컴파일 단계에서 오류를 감지할 수 있어, 타입 안정성을 보장할 수 있습니다.

 

package org.example.expert.domain.todo.repository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.todo.entity.Todo;
import java.util.Optional;
import static org.example.expert.domain.todo.entity.QTodo.todo;
import static org.example.expert.domain.user.entity.QUser.user;
@RequiredArgsConstructor
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery{
private final JPAQueryFactory jqf;
@Override
public Optional<Todo> findByIdWithUser(long todoId) {
Todo result = jqf.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne();
return Optional.ofNullable(result);
}
}

 

@RequiredArgsConstructor 어노테이션을 사용하여

JPAQueryFactory를 생성자 주입 방식으로 주입하였습니다.

이를 통해 QueryDSL을 활용한 쿼리를 작성할 수 있습니다.

 

 selectFrom(todo)를 사용하여 todo 테이블을 선택하고,

leftJoin(todo.user, user).fetchJoin()을 통해 todo 테이블과 user 테이블을 조인 하였습니다.

user는 fetchType = LAZY로 설정되어 있어,

fetchJoin()을 추가하여N+1 문제를 예방하였습니다.

 

또한, 특정 Todo를 조회하기 위해 where(todo.id.eq(todoId)) 조건을 추가하였습니다.

eq(todoId)를 사용하여 입력된 todoId와 동일한 ID를 가진 데이터를 조회하도록 설정하였습니다.

 

조회된 결과는 fetchOne()을 사용하여 가져옵니다.

fetchOne()은 단일 결과만 반환하는 메서드로,

여러 개의 결과가 있을 경우 예외가 발생하며, 값이 없을 경우 null을 반환합니다.

 

따라서, 반환 타입을 Optional<Todo>로 설정하여 null 처리를 안전하게 할 수 있도록 하였습니다.

 

🤔 Optional.ofNullable() 메소드란?
Optional.ofNullable()은 null을 안전하게 처리하기 위한 Java 8의 Optional 유틸리티 메서드입니다.
• null이 아닌 값이 들어오면 Optional<T>을 반환합니다.
• null이 들어오면 Optional.empty()를 반환하여 NullPointerException(NPE) 발생을 방지합니다.

 

 

 

(5) 트러블 슈팅


‼️ 문제 상황 발생
의존성을 주입하였음에도, 
RepositoryImpl 클래스에서 Q클래스가 import 되지 않는 문제가 발생하였습니다.

 

1️⃣ 해결 시도 1 - 설정 변경

설정 - Annotation Processors - Enable annotation processing

Enable annotation processing 옵션은 Annotation Processors를 활성화하는 설정입니다.

컴파일 단계에서 특정 어노테이션을 처리하여 코드 생성 및 변환을 수행하는 기능으로,

Lombok, QueryDSL 등의 어노테이션 기반 라이브러리를 정상적으로 사용할 수 있도록 도와줍니다.

이를 통해 Q클래스를 자동으로 생성하고,

@Getter, Setter 등의 어노테이션을 자동으로 처리할 수 있습니다.

 

하지만 위의 설정을 활성화 하였음에도, 문제가 해결되지 않았습니다.

 

 

 

2️⃣ 해결 시도 2 - 설정 변경 ➡️ 성공!

설정 - Build Tools - Gradle - Build and Run

 

 

 

✅ main 클래스 하위 generated 패키지에 Q클래스가 생성된 것을 확인 

일반적으로 Q클래스는 빌드 과정에서 자동 생성되는 파일이므로, Git에 업로드하지 않는 것이 권장됩니다.

따라서, Q클래스가 포함된 디렉터리를 .gitignore에 추가하여 버전 관리에서 제외하였습니다.

### Qclass ###
/src/main/generated/

 

 

 

 

728x90
저작자표시 비영리 변경금지
  1. 1. JPA Cascade
  2. (1) 문제 요구사항
  3. (2) 문제 풀이
  4. 2. N+1 문제
  5. (1) 문제 요구사항
  6. (2) 문제 접근
  7. (3) 문제 해결
  8. 3. QueryDSL
  9. (1) 문제 요구 사항
  10. (2) 의존성 추가
  11. (3) JPAConfiguration 클래스 생성
  12. (4) 문제 해결 과정
  13. (5) 트러블 슈팅
'Framework/Spring' 카테고리의 다른 글
  • [플러스 주차 개인과제] QueryDSL을 사용하여 검색 기능 구현하기
  • JwtUtil 클래스 생성하기 : 코드 설명
  • 토큰 기반 인증과 JWT: 액세스 토큰과 리프레시 토큰의 핵심 이해
  • JPA를 사용한 조회 기능 구현 - 날씨와, 일정 날짜 필터링
leonie.
leonie.
  • leonie.
    leveloper
    leonie.
  • 글쓰기 관리
    • 분류 전체보기 N
      • Language
        • Java
      • Git
      • CS N
      • CodingTest
        • [프로그래머스] 자바
      • Framework
        • Spring
      • Information
      • DBMS
        • Redis
        • SQL
      • AWS
      • OS
        • Mac
      • 자격증 N
        • 정보처리기사 N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    코딩테스트
    JPA
    스프링
    springboot
    알고리즘
    자바
    의존성주입
    Hibernate
    Java
    프로그래머스
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
leonie.
[플러스 주차 개인과제] 레벨 2 문제 해결 과정
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.