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) 문제 요구 사항


(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/