[Spring] Spring Data JPA

[verify$y]·2025년 6월 4일

Spring

목록 보기
13/16

💡 Spring 핵심 개념 인터뷰 Q&A


스프링 데이터란?

  • Spring에서 데이터 액세스 계층을 추상화한 프레임워크
  • JPA, MongoDB, Redis, JDBC 등을 일관된 방식으로 다룰 수 있게 도와줍니다.



스프링 데이터 기능

  • CRUD 리포지토리 자동 생성 (CrudRepository, JpaRepository)
  • @Query로 JPQL 또는 Native SQL 작성 가능
  • Pageable, Sort 등 페이징 기능 지원
  • Auditing (생성일/수정일 자동 처리)



Q1. Spring Data JPA를 사용하는 이유는?

  • 반복적인 CRUD 코드 없이, 인터페이스만 정의하면 기본 기능이 자동 생성되어 생산성이 높습니다.
  • JPQL도 메서드 이름으로 유추되어 개발 속도 향상에 기여합니다.


Q2. 메서드 이름만으로 쿼리를 만들 수 있다?

List<User> findByEmailAndStatus(String email, Status status);
  • 위 메서드는 다음 JPQL과 동일합니다
    → SELECT u FROM User u WHERE u.email = :email AND u.status = :status



Q3. @Query 어노테이션은 언제 쓰나

  • 복잡한 조건, 조인, 서브쿼리 등 → 메서드 이름만으로 해결 안 될 때 사용
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDate date);




Q4. 영속성 컨텍스트와 Spring Data JPA 관계?

  • Spring Data JPA는 JPA 위에서 동작하며, 영속성 컨텍스트(1차 캐시), 변경 감지(dirty checking), 지연 로딩 등의 JPA 기능을 그대로 사용합니다.




Q6. N+1 문제와 해결 방법?

N+1 문제란 무엇인가?

  1. 정의

    N+1 문제는 JPA나 Hibernate에서 하나의 조회 쿼리를 실행한 후, 그 결과로 가져온 엔티티 각각에 대해 연관된 데이터를 다시 조회하면서 N번의 추가 쿼리가 발생하는 문제를 말한다.

    즉, 총 1 + N번의 쿼리가 실행된다.


  2. 예시

  • Member(회원) 엔티티는 Team(팀) 엔티티와 다대일(N:1) 관계로 매핑되어 있다

List<Member> members = memberRepository.findAll();
for (Member member : members) {
    System.out.println(member.getTeam().getName());
}
  • 이 경우 동작 흐름은 다음과 같다.
  • memberRepository.findAll() → Member 전체를 조회하는 쿼리 1회 발생
  • 반복문에서 member.getTeam().getName() 호출 → Team 정보를 지연 로딩하면서 각 Member마다 추가 쿼리 1회 발생
  • 결과적으로 Member 수가 N개라면 총 1 + N 쿼리 실행


  1. 발생 원리
  • JPA는 기본적으로 연관관계를 LAZY(지연 로딩)로 설정한다.
  • 즉, 연관 엔티티는 실제 접근하는 시점에 SELECT 쿼리가 날아간다.
  • 이 방식은 메모리 효율을 고려한 설계지만, 연관 객체를 루프에서 반복 접근하면 N번의 쿼리가 쏟아지게 되어 성능 문제가 발생한다.



    N+1 해결방법

1. Fetch Join 사용

  • JPQL에서 fetch join을 명시적으로 사용하면 N+1 문제를 해결할 수 있다.

    
    @Query("SELECT m FROM Member m JOIN FETCH m.team")
    List<Member> findAllWithTeam();
    

장점

  • 한 번의 쿼리로 Member와 Team을 모두 로딩할 수 있어 N+1 문제 해결

fetch join 주의할점

  • 컬렉션(1:N) fetch join의 경우 데이터가 중복될 수 있고, 페이징 처리에 제한이 있다


2. EntityGraph 사용

  • EntityGraph는 fetch join을 대체하는 방식
  • 메서드 단위에서 선언적으로 연관 엔티티를 로딩할 수 있다.

@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
  • EntityGraph는 내부적으로 fetch join이 적용되지만, JPQL 없이 명시할 수 있어서 간결하고, 페이징도 안전하게 작동한다.


3. @BatchSize 설정

  • Hibernate에서 제공하는 성능 최적화 방법이다.
  • LAZY 로딩을 유지하면서도, 연관 엔티티들을 in 쿼리로 묶어서 한 번에 로딩할 수 있다.

@BatchSize(size = 100)
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
  • 또는 글로벌 설정(properties)

spring.jpa.properties.hibernate.default_batch_fetch_size=100
  • 이렇게 하면 Team을 하나씩 조회하는 대신, 다음과 같은 쿼리로 묶어서 가져온다.

SELECT * FROM team WHERE team_id IN (1, 2, 3, ..., 100)

장점
  • 페이징과 병행 가능하며, 실무에서 매우 유용

단점

  • 쿼리는 줄지만 여전히 2회 이상은 실행됨 (1 + 1(in))


4. QueryDSL 활용

  • 동적 조건이 필요한 경우 QueryDSL을 통해 fetch join 또는 where 절 최적화를 적용할 수 있다.
queryFactory.selectFrom(member)
    .join(member.team, team).fetchJoin()
    .where(member.name.eq("홍길동"))
    .fetch();
  • QueryDSL은 복잡한 동적 쿼리에도 fetch join을 안전하게 적용할 수 있어 자주 사용된다.

마무리 요약

  • N+1 문제는 실무에서 성능 저하의 주요 원인이므로 반드시 이해하고 있어야 한다
  • 단순 연관 조회에는 fetch join, 선언적인 접근에는 EntityGraph, 페이징 처리에는 @BatchSize가 유효하다
  • 복잡한 쿼리는 QueryDSL로 처리하며, 항상 쿼리 로그를 보고 실제 발생 쿼리를 점검하는 습관이 필요하다



Spring Data JPA

  • SQL 없이 도메인 기반으로 DB 연동 가능하게 해줍니다.

Repository 계층 계보

CrudRepository      // 기본 CRUD
 ↳ PagingAndSortingRepository // + 페이징/정렬
   ↳ JpaRepository            // + JPA 전용 기능



JPA 기능

  • 리포지토리 추상화 : JpaRepository<>을 상속하면 기본 CRUD를 자동생성
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByEmail(String email);
}

  • 메서드 이름 기반 자동 쿼리 : 자동 생성되는 JPQL 쿼리이며
    - 예를 들어, List findByEmail(String email)이면 SELECT u FROM User u WHERE u.email = :email 이런식으로 메서드 이름을 분석해서 다음과 같은 JPQL을 자동 생성합니다:


  • 쿼리 커스터마이징 : @Query 어노테이션을 사용하면 직접 쿼리를 작성할 수 있다.

Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDate date);
  • 페이징/정렬 : Page<T>, Slice<T> 리턴으로 페이지 처리할 수 있다.
Page<User> findByStatus(Status status, Pageable pageable);
  • Auditing : 생성일, 수정일을 자동으로 처리해준다. 엔티티에 별도 처리 없이 날짜 필드 자동 저장한다 @CreatedDate, @LastModifiedDate 어노테이션을 사용한다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

@CreatedDate : 생성일자자동생성, @LastModifiedDate : 수정일자 위 2개의 어노테이션은 이엔티티가 생성/수정될 때 현재 시간 등을 자동 주입합니다.

Auditing 안되는 경우

  • @EnableJpaAuditing 없으면 작동 안 함
  • 이 상태에서는 @CreatedDate@LastModifiedDate가 붙어 있어도, Hibernate는 해당 필드를 무시하고 값 주입을 하지 않습니다.



예시

전체 설정 클래스에 선언하는 방식

@SpringBootApplication
@EnableJpaAuditing
public class ProjApplication {
    public static void main(String[] args) {
        SpringApplication.run(QrLogiApplication.class, args);
    }
}

위는 ProjApplication 클래스에 @EnableJpaAuditing 어노테이션을 붙인다. 프로젝트가 소규모일떄 이런식으로 적용한다.

일반적인 어느정도의 규모가 있는 프로젝트에서는 configuration설정 클래스에 이 어노테이션을 붙여야한다.

별도의 설정 클래스에 선언하는 방식

@Configuration
@EnableJpaAuditing
public class JpaConfig {

		  ...
		  
}
  • 이 방식은 설정과 실행 클래스를 분리할 수 있어서, 유지보수 및 테스트에 더 유리합니다.
  • 특히 멀티모듈 구조에서는 common 또는 api/config 폴더에 위치시키면 깔끔합니다.



JPQL

  • Java Persistence Query Language의 약자로,J PA에서 사용하는 객체 지향 쿼리 언어입니다.

비교

예시 : SQL과 비교

  • SQL보다 객체 지향적이고, JPA의 표준 쿼리 언어
항목JPQLSQL
대상엔티티 (User)테이블 (user_table)
필드클래스 필드 (user.email)컬럼명 (user.email_address)
반환 타입엔티티 or DTO컬럼 집합
추상화 수준객체 지향적데이터베이스 지향적



Querydsl

  • Spring Data JPA에서 제공하는 메서드 쿼리(findByXxx)나 @Query는 정적 쿼리입니다.
  • 하지만 복잡한 검색 조건(조건이 2개 이상이며 유동적일 경우 등)이 있다면, Querydsl을 함께 사용하여 동적 쿼리를 작성하는 것이 좋습니다.



Querydsl는 동적쿼리

  • 동적쿼리란? 검색 조건이 사용자의 입력에 따라 다르게 적용될 수 있는 쿼리
  • JPQL은 정적쿼리라서 한계가 있는데 그 점을 동적쿼리인 QuertyDsl이 해결한다.



비교 : JPQL vs QueryDsl

조건추천 방식
단순한 조건 1~2개findByXxx, @Query (정적 쿼리)
조건이 유동적 (null 허용, 선택적 검색)Querydsl 사용 (동적 쿼리)
  • JPQL, QueryDsl을 병행 사용하면 돤다.



프로젝트 병행사용전략

  • 간단한 CRUD → JpaRepository 사용
  • 복잡한 검색 조건 → Querydsl 커스텀 리포지토리 사용
/// JPQL : 간단한 쿼리 
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom

/// QueryDSL: 복잡한 쿼리 
public interface UserRepositoryCustom {
    List<User> search(UserSearchCondition cond);
}
profile
welcome

0개의 댓글