JPA — *자바 객체로 DB 다루기*
JPA — *자바 객체로 DB 다루기*
🎯 이 lesson 을 읽고 나면
이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.
- ▸✅ JpaRepository 만 상속해서 0줄 코드로 CRUD 구현
- ▸✅ N+1 문제 → @EntityGraph 또는 JOIN FETCH 로 해결
- ▸✅ Lazy vs Eager + 실무에서 무조건 Lazy 인 이유
학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.
JPA 가 뭐냐 — *SQL 안 쓰고도 DB 사용*
핵심 한 줄
JPA (Java Persistence API) = 자바 객체를 DB 테이블에 자동 매핑 해주는 표준. SQL 직접 쓰지 않고 자바 메서드 호출만 으로 CRUD 가능.
옛 방식 — JDBC 의 고통
매번 연결 관리·SQL 작성·결과 매핑 다 직접. 500개 테이블이면 수만 줄 의 반복 코드.
새 방식 — JPA 의 마법
SQL 한 줄 없이 자바 객체 만 다루면 DB 가 알아서 동기화됩니다. "객체로 일하고 DB 는 잊는다" — JPA 의 핵심 사상.
Hibernate vs JPA
- ▸JPA — 표준 명세 (인터페이스). javax.persistence 패키지
- ▸Hibernate — 구현체. 가장 인기 (95%+ 점유). EclipseLink·OpenJPA 도 있음
Spring Data JPA = Spring 이 JPA 를 더 쉽게 쓰게 해주는 추상화. 실무 표준 스택.
Entity 매핑 — 객체와 테이블 짝짓기
관계 매핑 — 외래키를 객체 참조로
DB 의 외래키 관계 를 자바 객체 참조 로 변환. order.getUser().getName() 처럼 자연스럽게 탐색.
영속성 컨텍스트 — JPA 의 비밀병기
JPA 는 영속성 컨텍스트 (Persistence Context) 라는 1차 캐시 를 가집니다. 한 트랜잭션 안에서 같은 엔티티는 1번만 DB 조회.
Dirty Checking — JPA 가 시작 시점 vs 종료 시점 의 객체를 비교해서 변경된 필드만 UPDATE. setter 호출만으로 자동 저장됩니다.
한 번 정리
- ▸JPA = 자바 객체 ↔ DB 테이블 자동 매핑 표준
- ▸Hibernate = 가장 인기 구현체
- ▸Spring Data JPA = Spring 이 더 쉽게 해주는 추상화
- ▸Entity 어노테이션으로 매핑, 관계는 객체 참조, 변경은 자동 감지
Spring Data JpaRepository — *쿼리 자동 생성*
핵심 한 줄
JpaRepository 인터페이스만 정의하면 기본 CRUD 메서드 가 자동 만들어집니다. 게다가 메서드 이름만 봐도 쿼리를 추측해 자동 생성.
기본 CRUD — 즉시 사용 가능
이 한 줄로 다음 모든 메서드 가 제공됩니다:
쿼리 한 줄 안 짜고도 전체 CRUD 가능. 마법 같죠.
쿼리 메서드 — 이름이 곧 쿼리
규칙: find·exists·count·delete 로 시작 + By + 필드명·조건어. Spring 이 파싱해서 쿼리 만들어줍니다.
@Query — 복잡한 쿼리
이름 규칙으로 표현 안 되면 직접:
JPQL (JPA Query Language) — SQL 비슷하지만 테이블 이름이 아닌 엔티티 이름 사용. DB 독립적.
native SQL 도 가능:
QueryDSL — 동적 쿼리의 표준
@Query 도 한계가 있습니다 — 조건이 동적 일 때 (검색 필터). 그럴 땐 QueryDSL:
타입 안전하고 컴파일 타임에 검증. Spring Data JPA 와 함께 실무 표준 입니다.
페이징 — 자동 지원
Pageable 만 인자로 받으면 자동으로 LIMIT·OFFSET 처리. 응답 헤더에 페이징 정보 도 자동.
한 번 정리
- ▸
JpaRepository만 상속하면 기본 CRUD 무료 - ▸메서드 이름으로 쿼리 자동 생성
- ▸복잡하면
@Query또는 QueryDSL - ▸페이징·정렬도 Pageable 한 줄로
N+1 문제 — *JPA 의 가장 흔한 함정*
문제 상황
사용자 100명이면 총 101번 쿼리. 사용자 1만명이면 1만 1번. 페이지 응답 시간이 5초 → 50초 로 폭증.
이게 N+1 문제. JPA 의 지연 로딩 (Lazy Loading) 이 의도와 다르게 동작하는 클래식 함정.
왜 발생하나
JPA 는 연관 객체 를 기본적으로 Lazy 로 로드합니다. user.getOrders() 를 처음 호출하는 순간 DB 에 추가 쿼리. 한 번에 모두 가져오면 불필요한 데이터 가 많을 수 있으니 필요할 때만 가져오는 거죠.
하지만 반복문 안 에서 호출하면 매번 새 쿼리 → N+1.
해결책 1 — Fetch Join
JOIN 으로 한 번에 모두 가져옵니다. 1번 쿼리로 끝.
생성되는 SQL:
해결책 2 — @EntityGraph
특정 메서드에만 eager 로드 적용. Fetch Join 보다 깔끔 합니다.
해결책 3 — @BatchSize
연관 데이터를 N+1 이 아닌 N/배치 단위 로 가져옵니다.
사용자 1000명 → 10 쿼리 (100개씩). 완전 해결은 아니지만 큰 개선.
컬렉션 두 개 Fetch Join — 위험
컬렉션 두 개를 동시 Fetch Join 하면 데이터 카테시안 곱 (M × N) 으로 폭발. 해결:
- ▸하나만 Fetch Join + 나머지는 BatchSize
- ▸또는 DTO Projection 으로 필요한 것만
DTO Projection — 더 깊은 최적화
Entity 전체 말고 필요한 필드만 조회:
엔티티 매핑·Lazy 로딩 완전 우회. 가장 빠른 옵션입니다.
진단 — 로그로 확인
또는 더 강력한 P6Spy — 실제 파라미터 값까지 보여줍니다. 모든 쿼리를 눈으로 확인 하면서 N+1 발생 즉시 발견.
한 번 정리
- ▸N+1 은 Lazy 로딩 + 반복문 의 만남
- ▸해결: Fetch Join > @EntityGraph > @BatchSize > DTO Projection
- ▸show-sql 로 항상 쿼리 추적
- ▸컬렉션 2개 Fetch Join 은 위험 — 하나만
🤖 AI 에게 이렇게 요청해보세요
이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.
- ▸"이 findAll + 반복문이 N+1 인데 @EntityGraph 로 해결해줘"
- ▸"이 Entity 를 UserResponse DTO 로 변환하는 static factory 메서드 추가해줘"
- ▸"JpaRepository 에 findByEmailAndActive 메서드 시그니처 추가해줘"
왜 이게 토큰을 줄이나
개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.