🎯 F-lab Java 11-12주차 통합 학습 커리큘럼

11-12주차 자료의 모든 토픽을 두 주에 걸쳐 정리한 학습 경로.
1) 11주차 — JPA의 정체와 영속성 컨텍스트 (싱글톤 → SQL Mapper → ORM → 엔티티 매핑 → 영속성 컨텍스트)
2) 12주차 — 연관관계와 성능 최적화 (4가지 연관관계 → 프록시 → N+1 문제 → CASCADE → JPQL/QueryDSL)

7주차에서 JPA의 입문을 봤다면, 11-12주차는 JPA의 모든 메커니즘을 깊이 파헤친다.

자바·Spring 학습의 또 다른 정점이며, 실무에서 가장 자주 만나는 영역이다.


📊 학습 경로 한눈에 보기

[Part A — 11주차: JPA의 정체와 영속성 컨텍스트]
  [Phase 1] 싱글톤 + SQL Mapper의 역사
     ↓
  [Phase 2] ORM과 JPA의 본질
     ↓
  [Phase 3] 엔티티와 테이블 매핑 기초
     ↓
  [Phase 4] EntityManager + 영속성 컨텍스트
     ↓
  [Phase 5] 영속성 컨텍스트의 4가지 장점 ◄ 11주차 정점

[Part B — 12주차: 연관관계와 성능 최적화]
  [Phase 6] JPA 연관관계 4가지 (1:1, N:1, 1:N, N:M)
     ↓
  [Phase 7] mappedBy와 연관관계의 주인
     ↓
  [Phase 8] 프록시와 지연/즉시 로딩
     ↓
  [Phase 9] N+1 문제와 해결 ◄ 12주차 정점
     ↓
  [Phase 10] 영속성 전이 + JPQL/QueryDSL

총 10 Phase × 39 Unit — 8-9주차와 비슷한 분량의 통합 커리큘럼.

🔗 1~12주차 흐름 정리

주차주제핵심 변화
1주차OOP·JVM·GC·컬렉션·I/O 개론자바 큰 그림
2주차JVM 내부·바이트코드·G1 GC"어떻게 돌아가나"
3주차컬렉션·제네릭·함수형자바 표현력
4주차멀티스레딩·동시성·Executor동시성 정복
5주차Atomic + Spring IoC/DI 입문자바 → Spring 다리
6주차테스트 + 웹 인프라 + DB 접근 진화Spring 실전 환경
7주차JPA/ORM 입문 + 트랜잭션 추상화DB 추상화 입문
8주차프록시의 진화AOP 메커니즘
9주차Spring AOP 실전 + 트랜잭션 전파AOP 실전 활용
10주차트랜잭션 정리 + 빈 라이프사이클 함정 + 격리 수준트랜잭션 마무리
11주차 (지금)JPA의 정체와 영속성 컨텍스트JPA 메커니즘 완전 이해
12주차 (지금)연관관계 + 성능 최적화 (N+1 등)JPA 실전 활용

🗓️ 권장 학습 일정 (압축 14일)

DayPhase학습 목표
Week 1 (11주차)
1일차Phase 1싱글톤 + SQL Mapper 역사
2일차Phase 2ORM/JPA 본질 + Hibernate
3일차Phase 3엔티티 매핑 + 임베디드 타입
4-5일차Phase 4EntityManager + 영속성 컨텍스트 + 생명주기
6-7일차Phase 54가지 장점 (★ 정점)
Week 2 (12주차)
8-9일차Phase 64가지 연관관계
10일차Phase 7mappedBy와 연관관계의 주인
11일차Phase 8프록시와 지연/즉시 로딩
12-13일차Phase 9N+1 문제 + 해결책 (★ 정점)
14일차Phase 10CASCADE + JPQL + QueryDSL

여유 일정 (21일): Phase 5와 9에 각 +2일. 영속성 컨텍스트와 N+1은 직접 SQL 로그를 보며 체화.


🌱 Part A — 11주차: JPA의 정체와 영속성 컨텍스트

📚 Phase 1 — 싱글톤 + SQL Mapper의 역사

목표: JPA가 어떤 흐름의 산물인지를 SQL Mapper(iBatis/MyBatis)와의 비교로 이해한다.

Unit 1.1 — 싱글톤 패턴 본질 (5주차 복습+심화)

선수 지식: 5주차 Phase 8 (싱글톤 빈)

핵심 개념

싱글톤 패턴의 정의:

"클래스의 인스턴스가 단 하나만 존재 하도록 보장 + 전역 접근"

핵심 3요소:
1. static 변수로 유일 인스턴스 저장
2. private 생성자로 외부 생성 차단
3. getInstance() 정적 메서드로 반환

public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}  // private!
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

대표적인 싱글톤 사례:

  • Spring의 Bean 객체 (5주차)
  • JPA의 EntityManagerFactory ⭐ (이번 주차 핵심)
  • Database Connection Pool (HikariCP)
  • Logger, Caching System

자기 점검

  • 5주차 Spring 싱글톤과 GoF 싱글톤 패턴의 차이는?
  • private 생성자 없이 싱글톤이 가능한가?

Unit 1.2 — 멀티스레드 환경의 위험 + ThreadLocal 연결

선수 지식: Unit 1.1, 4주차 Phase 4, 10주차 Phase 4

핵심 위험

문제:

  • 싱글톤은 인스턴스가 하나
  • → 여러 스레드가 동시에 공유
  • → Race Condition 가능

해결 방향:
1. stateless 설계 (가장 권장) — 멤버 변수에 가변 상태를 두지 않음
2. synchronized (성능 비용)
3. Atomic (5주차 Phase 2 — CAS)
4. ThreadLocal (8주차 Phase 1 — 스레드별 독립 저장소)

JPA에서의 적용:

  • EntityManagerFactory = 싱글톤 (애플리케이션 전역)
  • EntityManager = 트랜잭션 단위 (스레드별로 다름)
  • → 동시성 문제를 EntityManager 분리로 회피

자기 점검

  • ILIC의 Service 빈에 List 멤버 변수를 두면? (힌트: 동시성 사고)
  • 4주차 ConcurrentHashMap을 멤버 변수로 두는 건 안전한가?

Unit 1.3 — iBatis → MyBatis (SQL Mapper의 진화)

선수 지식: 6주차 Phase 7 (JdbcTemplate)

핵심 개념

iBatis (Internet Based Abstraction for Tabular Information Systems):

  • SQL을 XML 파일에 분리
  • 자바 객체와 SQL 결과 매핑 자동화
  • iBatis 3.0 이후 개발 중단

MyBatis = iBatis의 후계자:

  • 어노테이션 기반 SQL 매핑 추가 (@Select, @Insert)
  • 동적 SQL (<if>, <foreach>)
  • 더 많은 데이터베이스 호환

JDBC vs MyBatis 비교:

JDBC 직접MyBatis
SQL 작성자바 코드 안XML 또는 어노테이션
자원 관리수동 (try/finally)자동
결과 매핑수동 (rs.getInt 등)자동 (resultType)
코드량많음적음

SQL Mapper의 한계:

  • ✅ 자원 관리 자동화
  • SQL은 여전히 직접 작성
  • 객체-관계 매핑은 수동
  • → 그래서 ORM(JPA)이 등장

자기 점검

  • JdbcTemplate(6주차)과 MyBatis의 위치는 같은가? (힌트: 둘 다 SQL Mapper)
  • 한국 SI 시장에서 MyBatis가 여전히 많이 쓰이는 이유는? (힌트: SQL 통제, 익숙함)

📚 Phase 2 — ORM과 JPA의 본질

목표: ORM의 패러다임과 JPA-Hibernate의 관계를 명확히 잡는다.

Unit 2.1 — SQL Mapper vs ORM (패러다임 차이)

선수 지식: Phase 1, 7주차 Phase 2~3

핵심 비교

SQL Mapper (MyBatis, JdbcTemplate)ORM (JPA/Hibernate)
매핑SQL ↔ 객체객체 ↔ 테이블
SQL 작성개발자 직접JPA가 자동 생성
학습 곡선낮음높음
DBMS 종속성있음적음
복잡 통계 쿼리자유롭게어렵 (JPQL/네이티브 쿼리 필요)

SQL Mapper의 시각:

"SQL을 작성하고, 결과를 객체에 채워주세요"

ORM의 시각:

"객체를 다루세요. SQL은 제가 만들겠습니다"

객체-관계 패러다임 불일치:

  • 객체는 상속·다형성·연관 참조
  • 관계 DB는 행과 열·외래 키
  • → 둘 사이의 변환은 코드가 많아짐
  • 이 변환을 ORM이 자동화

자기 점검

  • 복잡한 통계 쿼리에서 ORM의 한계는?
  • SQL Mapper와 ORM을 한 프로젝트에서 같이 쓸 수 있는가? (힌트: YES, 실무에서 흔함)

Unit 2.2 — JPA = 표준 명세, Hibernate = 구현체

선수 지식: Unit 2.1

핵심 구조

JPA (Java Persistence API):

  • 명세(Specification) — 인터페이스만 정의
  • 직접 동작하는 프레임워크가 아니다
  • ex) EntityManager, EntityManagerFactory 인터페이스

JPA 구현체:

  • Hibernate (가장 대중적, ~95% 점유) ⭐
  • EclipseLink
  • DataNucleus
  • OpenJPA

의존 관계:

[Application Code]
       ↓
[JPA Interface]   ← javax.persistence (또는 jakarta.persistence)
       ↓
[Hibernate]      ← JPA 구현
       ↓
[JDBC]
       ↓
[DB]

ILIC 사례:

  • spring-boot-starter-data-jpa 의존성 추가
  • 내부적으로 Hibernate 자동 설정
  • 개발자는 JPA API로 코딩

Spring Data JPA와의 구분:

  • JPA: 자바 표준 인터페이스
  • Hibernate: JPA의 구현체
  • Spring Data JPA: JPA를 더 편리하게 쓰는 Spring의 추상화 (Repository 인터페이스 등)

자기 점검

  • 왜 JPA를 표준화했는가? (힌트: 구현체 교체 가능)
  • Hibernate를 직접 쓰지 않고 JPA로 코딩하는 이유는?

Unit 2.3 — 객체-관계 패러다임 불일치 + JPA 위치

선수 지식: Unit 2.2

핵심 불일치 5가지:

측면객체 (OOP)관계 DB
모델링상태 + 행동행과 열
상속있음없음
연관참조 (order.member)외래키
식별객체 동일성 (==)PK
데이터 타입풍부 (List, Map)제한적

불일치를 해결하는 방법 (JPA의 일):

  • 상속 → SINGLE_TABLE / JOINED / TABLE_PER_CLASS
  • 참조 → @ManyToOne, @OneToMany 어노테이션
  • 동일성 → 1차 캐시로 보장 (Phase 5에서)

JPA의 본질:

"객체와 RDB 사이의 번역기"

자기 점검

  • "패러다임 불일치"의 가장 명확한 사례를 본인 코드에서 찾아본다면?
  • ILIC의 Booking 객체와 bookings 테이블의 매핑이 자동화되는 이점은?

📚 Phase 3 — 엔티티와 테이블 매핑 기초

목표: 자바 객체 → DB 테이블 매핑의 기본 어노테이션을 손에 익힌다.

Unit 3.1 — @Entity와 엔티티의 조건

선수 지식: 7주차 Phase 4

핵심 개념

@Entity:

  • "JPA가 관리하는 객체"라는 표시
  • 이게 있어야 JPA가 인식 → 엔티티

엔티티의 필수 조건 ⭐ :

  • 기본 생성자 필수 (public 또는 protected)
  • 식별자 필드 (@Id)
  • final 클래스 사용 불가 (Hibernate가 프록시 만들어야 함)
  • enum, interface, inner class 불가
  • ❌ 저장할 필드에 final 사용 불가

기본 생성자가 왜 필요한가?:

  • JPA가 리플렉션 으로 객체 생성 (3주차 Phase 5)
  • → 기본 생성자가 있어야 함
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    public Member() {}  // 기본 생성자 필수!
    
    public Member(String name) {
        this.name = name;
    }
}

자기 점검

  • final 클래스를 엔티티로 만들면 어떤 에러가? (힌트: 프록시 생성 실패)
  • 8주차 Phase 5의 CGLIB와 어떻게 연결되는가?

Unit 3.2 — @Table 매핑

선수 지식: Unit 3.1

핵심 개념

기본 매핑:

  • 클래스명 → 테이블명 자동 매핑
  • Member 클래스 → member 테이블

@Table 으로 명시:

@Entity
@Table(name = "MBR")  // 실제 테이블명을 MBR로
public class Member {
    // ...
}

@Column 매핑 (7주차 Phase 4 복습):

@Column(name = "user_name", length = 100, nullable = false)
private String userName;

Spring Boot의 자동 변환 (7주차 Unit 4.5):

  • 자바 필드: userName (camelCase)
  • DB 컬럼: user_name (snake_case)
  • → 자동 매핑

자기 점검

  • @Table 을 생략하면 어떻게 되는가?
  • 한 엔티티가 여러 테이블에 매핑될 수 있는가? (힌트: @SecondaryTable)

Unit 3.3 — 임베디드 타입 (@Embeddable + @AttributeOverrides)

선수 지식: Unit 3.2

핵심 개념

임베디드 타입(Embedded Type):

  • "값 타입(Value Type)"의 일종
  • 여러 필드를 하나의 객체로 묶기
  • 코드 재사용성 ↑

예시 — 주소를 임베디드 타입으로:

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
    
    protected Address() {}  // 기본 생성자 필수
    
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
    // Getter만 (값 타입은 불변 권장)
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded
    private Address address;  // 임베디드 사용
}

DB 테이블 결과:

CREATE TABLE Member (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),
    city VARCHAR(255),     -- Address의 필드
    street VARCHAR(255),
    zipcode VARCHAR(255)
);

@AttributeOverrides — 같은 임베디드 타입을 여러 번 사용 시:

@Entity
public class Employee {
    @Id @GeneratedValue
    private Long id;
    
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "home_city")),
        @AttributeOverride(name = "street", column = @Column(name = "home_street")),
        @AttributeOverride(name = "zipcode", column = @Column(name = "home_zipcode"))
    })
    private Address homeAddress;
    
    @Embedded
    @AttributeOverrides({...})
    private Address workAddress;
}

값 타입의 원칙:

  • 불변(Immutable) — Setter 만들지 말고 생성자로만
  • 공유하지 말 것 — 부작용 방지

자기 점검

  • 값 타입을 가변으로 만들면 어떤 사고가? (힌트: 다른 엔티티에 영향)
  • ILIC에서 임베디드 타입으로 표현하기 좋은 도메인은? (힌트: 주소, 운임, 연락처)

Unit 3.4 — ddl-auto + DDL/DML/DCL 참고

선수 지식: Unit 3.3

핵심 개념

spring.jpa.hibernate.ddl-auto 옵션:

옵션동작
create시작 시 기존 테이블 삭제 + 새로 생성
create-dropcreate + 종료 시 삭제
update변경 사항만 반영 (안전하지 않음)
validate스키마 매핑만 검증 (변경 없음)
none자동 처리 안 함

⚠️ 운영 환경 절대 금지 ⭐ :

  • create, create-drop, update데이터 손실 위험

개발 단계별 권장:

  • 로컬: create 또는 update
  • 스테이징: validate
  • 프로덕션: none (Flyway/Liquibase 사용)

SQL 분류 참고:

분류의미명령
DDL (Data Definition Language)골격 정의CREATE, ALTER, DROP, TRUNCATE
DML (Data Manipulation Language)데이터 조작SELECT, INSERT, UPDATE, DELETE
DCL (Data Control Language)권한·트랜잭션GRANT, REVOKE, COMMIT, ROLLBACK

자기 점검

  • 운영에서 ddl-auto=update가 위험한 이유 3가지는?
  • ILIC의 102개 테이블을 운영 환경에서 어떻게 관리하는 게 좋을까? (힌트: Flyway)

📚 Phase 4 — EntityManager와 영속성 컨텍스트

목표: JPA의 핵심 메커니즘 — 영속성 컨텍스트의 정체와 엔티티 생명주기를 이해한다.

Unit 4.1 — EntityManagerFactory (싱글톤)

선수 지식: Phase 1, 2

핵심 개념

EntityManagerFactory (EMF):

  • EntityManager생성하는 팩토리
  • 애플리케이션 전체에서 1개만 존재 (싱글톤)
  • 생성 비용이 크기 때문에 한 번만 생성

Spring Boot에서의 동작:

  • @SpringBootApplication 실행 시 자동 생성
  • 빈으로 등록 → DI 가능
  • 애플리케이션 종료까지 유지

자바 표준 사용 (참고):

EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-pu");

Spring 환경 (실무):

@PersistenceUnit
private EntityManagerFactory emf;  // 자동 주입

자기 점검

  • EntityManagerFactory가 싱글톤인 이유는?
  • Spring Boot가 EMF를 자동 등록하는 시점은? (힌트: 컨텍스트 초기화)

Unit 4.2 — EntityManager (트랜잭션 단위)

선수 지식: Unit 4.1, 7주차 Phase 7

핵심 개념

EntityManager (EM):

  • 영속성 컨텍스트를 관리하는 인터페이스
  • 트랜잭션 단위로 생성·소멸

Spring + @Transactional 조합:

  • 트랜잭션 시작 시 EM 자동 생성
  • 트랜잭션 종료 시 EM 자동 닫힘
  • 개발자는 직접 닫을 필요 X

핵심 메서드:

  • persist(entity): 영속화 (저장)
  • find(Class, id): 조회 (1차 캐시 활용)
  • getReference(Class, id): 프록시 반환 (Phase 8)
  • remove(entity): 삭제
  • merge(entity): 준영속 → 영속
  • detach(entity): 영속 → 준영속

중요 — 스레드 안전 X:

"EntityManager는 공유 X — 트랜잭션마다 별도 인스턴스"

이게 EntityManagerFactory(싱글톤)와의 결정적 차이.

자기 점검

  • 한 EntityManager를 여러 스레드에서 쓰면 어떤 문제가?
  • @Transactional 메서드 안에서 EM을 직접 close()하면? (힌트: 하지 마라)

Unit 4.3 — 영속성 컨텍스트 정의

선수 지식: Unit 4.2

핵심 개념

"엔티티 매니저 내부에서 동작하는 메모리 공간 — 엔티티를 보관하고 관리하는 곳"

위치:

EntityManager
   └── 영속성 컨텍스트 (Persistence Context)
         └── 1차 캐시 (엔티티 보관)

역할:

  • 엔티티의 생명주기 관리
  • 1차 캐시 제공
  • 변경 감지 (Dirty Checking)
  • 쓰기 지연

중요 통찰:

"JPA의 모든 신기한 동작은 영속성 컨텍스트 안에서 일어난다"

이게 Phase 5에서 설명할 4가지 장점의 출발점.

자기 점검

  • 영속성 컨텍스트와 1차 캐시는 같은 것인가? (힌트: 1차 캐시는 일부)
  • EntityManager가 닫히면 영속성 컨텍스트는 어떻게 되는가?

Unit 4.4 — 엔티티 생명주기 (비영속/영속/준영속/삭제)

선수 지식: Unit 4.3

핵심 4가지 상태 ⭐ :

new        ─persist()──> [영속(Managed)] ──remove()──> [삭제(Removed)]
[비영속]                       │
(Transient)                    │ detach()
                               │ close()
                               │ clear()
                               ↓
                       [준영속(Detached)]
                               │
                               │ merge()
                               ↓
                          [영속(Managed)]

4가지 상태 자세히:

상태정의영속성 컨텍스트DB
비영속(Transient)new로 생성, 영속화 X
영속(Managed)persist 또는 find로 관리 중(커밋 시 반영)
준영속(Detached)영속이었으나 분리됨(이전 데이터 존재)
삭제(Removed)remove로 삭제 예정✅ (제거 표시)(커밋 시 삭제)

상태 변경 메서드:

  • persist(entity): 비영속 → 영속
  • find(Class, id): → 영속
  • remove(entity): 영속 → 삭제
  • detach(entity): 영속 → 준영속 (개별)
  • clear(): 모든 영속 → 준영속
  • close(): EM 종료 → 모두 준영속
  • merge(entity): 준영속 → 영속

중요한 차이 ⭐ :

  • detach(): 영속성 컨텍스트에서만 분리 — DB는 그대로
  • remove(): DB에서도 삭제 (커밋 시)

자기 점검

  • 비영속 엔티티에 setter를 호출하면 DB에 반영되는가? (힌트: NO)
  • detach()와 close()의 차이는? (힌트: 범위)

📚 Phase 5 — 영속성 컨텍스트의 4가지 장점 (★ 11주차 정점)

목표: JPA가 왜 강력한지를 영속성 컨텍스트의 4가지 동작 메커니즘으로 이해한다. 면접·실무 단골.

Unit 5.1 — 1차 캐시

선수 지식: Phase 4

핵심 개념

1차 캐시:

  • 영속성 컨텍스트 내부의 엔티티 저장 공간
  • 같은 트랜잭션 안에서 DB 조회 회피

동작 흐름:

EntityManager em = emf.createEntityManager();
User user1 = em.find(User.class, 1L);  // ① DB 조회 + 1차 캐시 저장
User user2 = em.find(User.class, 1L);  // ② 1차 캐시에서 즉시 반환 (SQL 실행 X)

1차 캐시의 6단계:
1. 첫 조회: 1차 캐시 비어있음
2. DB에서 엔티티 조회
3. 1차 캐시에 저장
4. 결과 반환
5. 같은 엔티티 재조회 시 1차 캐시에서 반환
6. 객체 동일성 (==) 보장

범위:

  • 트랜잭션 단위 (EntityManager 생명 주기와 같음)
  • 트랜잭션 종료 시 1차 캐시도 사라짐

자기 점검

  • 1차 캐시가 트랜잭션 단위인 이유는?
  • ILIC의 동일 트랜잭션에서 같은 엔티티를 5번 조회 시 SQL은 몇 번 실행되는가?

Unit 5.2 — 동일성 보장

선수 지식: Unit 5.1

핵심 개념

User user1 = em.find(User.class, 1L);
User user2 = em.find(User.class, 1L);
System.out.println(user1 == user2);  // true!

왜 가능한가:

  • 1차 캐시에서 같은 인스턴스 반환
  • 객체 동일성(==) 보장

객체 동등성 vs 동일성:

  • 동등성(equals): 값이 같음
  • 동일성(==): 인스턴스가 같음

JPA는 트랜잭션 안에서 동일성을 보장한다.

의의:

  • "같은 엔티티는 같은 객체" 라는 객체지향적 직관 유지
  • DB 행 = 자바 객체 라는 1:1 매핑

자기 점검

  • 다른 트랜잭션에서 조회한 같은 ID의 엔티티는 == 비교가 true일까? (힌트: NO)
  • equals/hashCode를 엔티티에 어떻게 구현해야 하는가? (힌트: ID 기반)

Unit 5.3 — 쓰기 지연

선수 지식: Unit 5.2

핵심 개념

쓰기 지연(Write-Behind):

  • persist() 호출 시 즉시 INSERT 안 함
  • 트랜잭션 커밋 시점에 한꺼번에 SQL 실행

예시:

em.getTransaction().begin();

em.persist(memberA);  // INSERT SQL 안 보냄
em.persist(memberB);  // INSERT SQL 안 보냄
em.persist(memberC);  // INSERT SQL 안 보냄
// 여기까지 DB는 아무것도 모름

em.getTransaction().commit();
// → 커밋 직전에 INSERT SQL 3개를 한꺼번에 보냄!

내부 동작:

  • 영속성 컨텍스트 안에 SQL 저장소 존재
  • persist 시 SQL을 저장소에만 모음
  • commit 시 모든 SQL을 DB에 전송

효과:

  • 배치 처리 최적화 가능 (Hibernate 설정으로 batch 사이즈 조정)
  • 네트워크 왕복 ↓
  • 성능 ↑

자기 점검

  • 쓰기 지연이 작동하지 않는 경우는? (힌트: IDENTITY 전략 — 7주차 Phase 4)
  • 100개 INSERT를 1번의 네트워크 호출로 처리할 수 있는 옵션은? (힌트: hibernate.jdbc.batch_size)

Unit 5.4 — 변경 감지 (Dirty Checking) ⭐⭐⭐

선수 지식: Unit 5.3

핵심 개념 — JPA의 가장 신기한 동작

em.getTransaction().begin();

Member member = em.find(Member.class, 1L);  // 영속 상태
member.setUsername("hi");  // setter 호출만!
member.setAge(10);

em.getTransaction().commit();
// → UPDATE 쿼리 자동 실행!

em.update() 호출 X. setter만 호출했는데 UPDATE 가 일어났다.

내부 동작 — 스냅샷 비교:
1. 엔티티가 영속성 컨텍스트에 들어올 때 스냅샷 저장 (1차 캐시 옆)
2. setter 호출 → 엔티티 변경
3. 커밋 시점에 스냅샷과 현재 엔티티 비교
4. 차이가 있으면 → UPDATE SQL 자동 생성
5. 쓰기 지연 저장소에 추가
6. DB 반영

비교 흐름:

[조회 시점]                     [커밋 시점]
엔티티: {name: "A", age: 20}    엔티티: {name: "B", age: 25}
스냅샷: {name: "A", age: 20}    스냅샷: {name: "A", age: 20}
                                       ↓
                               [차이 검출] → UPDATE SQL 생성

중요한 함정:

  • 준영속 엔티티는 변경 감지 Xmerge() 또는 다시 영속화 필요
  • 영속성 컨텍스트 밖에서 변경한 값은 자동 반영 안 됨

자기 점검

  • "변경 감지"가 작동하는 전제 조건은?
  • 명시적으로 변경 감지를 비활성화하려면? (힌트: Hibernate.unproxy(), readOnly)

Unit 5.5 — 플러시 + 2차 캐시 보너스

선수 지식: Unit 5.4

플러시(Flush):

"영속성 컨텍스트의 변경 내용을 DB에 반영하는 시점"

플러시 발생 3가지 시점:
1. em.flush() 직접 호출
2. 트랜잭션 커밋 직전 (자동)
3. JPQL 쿼리 실행 직전 (자동)

3번이 중요한 이유:

  • 변경 감지로 메모리에만 있는 변경을
  • JPQL 실행 전 DB에 반영해야 일관된 결과

플러시 ≠ 커밋:

  • 플러시: SQL을 DB로 전송 (메모리 → DB 동기화)
  • 커밋: 트랜잭션 종료 (영구 반영)
  • → flush 후에도 rollback 가능

2차 캐시 (보너스):

  • 애플리케이션 전역 캐시 (1차 캐시는 트랜잭션 단위)
  • 여러 트랜잭션이 공유
  • 별도 설정 필요 (Ehcache, Redis, Hazelcast)

왜 복사본을 반환하는가:

  • 캐시된 객체를 직접 반환 → 동시성 충돌
  • → 트랜잭션마다 독립 복사본 반환

1차 vs 2차 캐시:

1차 캐시2차 캐시
범위트랜잭션애플리케이션 전역
기본 제공❌ (별도 설정)
객체 반환같은 인스턴스복사본
동시성안전복사본으로 격리

자기 점검

  • JPQL 실행 전 자동 플러시가 일어나는 이유는?
  • ILIC에서 2차 캐시를 도입한다면 어떤 데이터가 적합한가? (힌트: 마스터 데이터, 변경 적음)

⚡ Part B — 12주차: 연관관계와 성능 최적화

📚 Phase 6 — JPA 연관관계 4가지

목표: 객체 참조와 외래 키를 어떻게 매핑하는지 4가지 관계로 정리한다.

Unit 6.1 — 연관관계의 분류 + 단방향 vs 양방향

선수 지식: Phase 5

핵심 개념

4가지 연관관계:

  • 1:1 (@OneToOne)
  • N:1 (@ManyToOne) — 가장 많이 사용 ⭐
  • 1:N (@OneToMany)
  • N:M (@ManyToMany) — 실무에서 잘 안 씀

단방향 vs 양방향:

  • 단방향: 한 엔티티에서만 다른 엔티티 참조 (A → B)
  • 양방향: 양쪽이 서로 참조 (A ↔ B)

핵심 통찰:

"양방향은 사실 단방향 2개"

객체 입장: A → B 와 B → A 의 두 참조

DB 입장: 외래 키 1개로 충분

→ 그래서 연관관계의 주인 개념이 등장 (Phase 7)

자기 점검

  • 양방향이 객체 입장에서는 자연스러운데 DB 입장에서는 왜 그렇지 않은가?
  • 단방향만으로 충분한 시나리오는? (힌트: 거의 모든 경우)

Unit 6.2 — 1:1 (OneToOne) 매핑

선수 지식: Unit 6.1

핵심 개념

언제 사용:

  • 회원 → 사물함
  • 사용자 → 프로필
  • 부가 정보

FK 위치 결정:

  • 자주 조회되는 쪽에 FK 권장
  • 회원이 사물함을 자주 조회 → 회원에 FK

단방향 매핑:

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "locker_id")  // FK는 Member 테이블에
    private Locker locker;
}

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;
}

양방향 매핑:

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;
    
    @OneToOne(mappedBy = "locker")  // 주인이 아님
    private Member member;
}

자기 점검

  • 1:1을 1:N과 구분 짓는 명확한 비즈니스 규칙은?
  • FK를 양쪽 어디에 두든 결과는 같은가? (힌트: 성능 차이 있음)

Unit 6.3 — N:1 (ManyToOne) — 가장 많이 사용 ⭐

선수 지식: Unit 6.2

핵심 개념

가장 자주 등장하는 관계:

  • 회원(N) → 팀(1)
  • 주문(N) → 회원(1)
  • 댓글(N) → 게시글(1)

FK 위치:

  • N(다수) 쪽에 FK 위치 ← 자연스러움
  • 연관관계의 주인은 @ManyToOne
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)  // 지연 로딩 권장!
    @JoinColumn(name = "team_id")  // FK는 Member에
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
}

⚠️ 주의 사항:
1. FK는 N쪽에 — 항상
2. fetch = FetchType.LAZY 권장 — N+1 방지 (Phase 9)
3. 잘못된 양방향 설정 시 무한 루프 — toString, JSON 직렬화 시

자기 점검

  • @ManyToOne 의 기본 fetch 전략은? (힌트: EAGER — 위험!)
  • 왜 LAZY로 명시해야 하는가?

Unit 6.4 — 1:N (OneToMany)

선수 지식: Unit 6.3

핵심 개념

보통 양방향에서 N:1 + 1:N 조합으로 사용:

  • N:1 쪽이 주인 (FK 관리)
  • 1:N 쪽은 mappedBy (읽기 전용)
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "team")  // 주인이 아님!
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "team_id")  // 주인 (FK 관리)
    private Team team;
}

@OneToMany 가 주인이 되는 경우는?:

  • 비추천 (추가 UPDATE 쿼리 발생)
  • 단방향만 필요할 때 사용 가능

⚠️ 주의 사항:

  • mappedBy 없으면 → 조인 테이블 자동 생성 (비효율)
  • 항상 @ManyToOne 쪽이 주인이어야 함

자기 점검

  • @OneToMany를 주인으로 만들면 추가 UPDATE가 발생하는 이유는?
  • 'mappedBy = "team"' 의 "team"은 어디를 가리키는가? (힌트: Member 엔티티의 필드명)

Unit 6.5 — N:M (ManyToMany) — 중간 엔티티 패턴 ⭐

선수 지식: Unit 6.4

핵심 개념

N:M의 직접 사용 — 비추천:

@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();

왜 안 좋은가:

  • 자동 조인 테이블에 부가 정보 추가 불가
  • 등록일, 상태, 수량 등 못 넣음
  • 실무에서 거의 안 씀

중간 엔티티 패턴 (실무 표준):

@Entity
public class MemberProduct {  // 또는 Order로 이름 변경
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
    
    private int amount;          // 부가 정보!
    private int price;           // 부가 정보!
    private LocalDateTime orderDateTime;  // 부가 정보!
}

원칙:

"N:M은 항상 1:N + N:1로 풀어라"

의미 부여:

  • MemberProductOrder 같이 비즈니스 의미 있는 이름으로 변경
  • 단순 연결 테이블이 아닌 도메인 엔티티로 격상

자기 점검

  • ILIC에서 N:M로 표현하고 싶지만 중간 엔티티가 더 나은 사례는? (힌트: 사용자-권한, 책-저자)
  • 중간 엔티티의 PK 전략은? (힌트: 단일 PK 또는 복합 PK)

📚 Phase 7 — mappedBy와 연관관계의 주인

목표: 양방향 연관관계의 가장 어려운 개념을 정리한다.

Unit 7.1 — 연관관계의 주인이 필요한 이유

선수 지식: Phase 6

핵심 문제

양방향은 객체 입장: A → B + B → A (2개 참조)
DB 입장: FK 1개로 표현

문제:

  • A에서 변경하는 것과 B에서 변경하는 것 중 어느 것을 DB에 반영?
  • 둘 다 반영하면 충돌 / 비효율

해결:

"한쪽만 주인 으로 정하고, 그 쪽만 DB FK 관리"

다른 쪽은 읽기 전용

규칙:

  • N쪽이 주인 (@ManyToOne 쪽)
  • 1쪽은 mappedBy

자기 점검

  • 데이터 무결성 측면에서 왜 한쪽만 주인이어야 하는가?
  • 두 쪽이 모두 FK를 변경하면 어떤 사고가? (힌트: 충돌, 일관성 깨짐)

Unit 7.2 — mappedBy 사용법

선수 지식: Unit 7.1

핵심 개념

mappedBy:

  • 주인이 아닌 쪽에 명시
  • 값은 주인 엔티티의 필드명
@OneToMany(mappedBy = "team")  
//                    ↑ Member 엔티티의 team 필드를 가리킴
private List<Member> members;

의미:

"나는 주인이 아니고, 저쪽(Member.team)이 주인이다"

JPA가 하는 일:

  • 주인이 아닌 쪽: 읽기 전용 — FK 변경 안 함
  • 주인 쪽만 보고 DB 동기화

자주 하는 실수:

  • 주인이 아닌 쪽에서 변경하고 DB 반영 기대
  • → 변경 안 됨 → 디버깅 어려움

연관관계 편의 메서드 (실무 표준):

@Entity
public class Member {
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);  // 양쪽 동기화!
    }
}

자기 점검

  • mappedBy의 값 "team"이 잘못된 필드명이면? (힌트: 컴파일 OK, 런타임 에러)
  • 양쪽을 항상 같이 바꿔야 하는 이유는? (힌트: 1차 캐시 일관성)

Unit 7.3 — N:1 + 1:N 양방향 모범 사례

선수 지식: Unit 7.2

핵심 패턴

가장 흔한 양방향 — 회원과 팀:

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;  // 주인 (FK 관리)
    
    // 편의 메서드
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "team")  // 주인 X
    private List<Member> members = new ArrayList<>();
}

5가지 권장 사항:
1. ✅ N쪽이 주인 (@ManyToOne)
2. ✅ fetch = FetchType.LAZY 명시
3. ✅ 컬렉션은 필드 초기화 (new ArrayList<>())
4. ✅ 편의 메서드로 양쪽 동기화
5. ✅ Lombok @ToString 에서 컬렉션 제외 (무한 루프 방지)

자기 점검

  • 단방향 매핑으로 시작하고 필요 시 양방향으로 확장하라는 조언의 의미는?
  • 양방향이 무한 루프를 일으키는 시나리오는? (힌트: toString, JSON 직렬화)

📚 Phase 8 — 프록시와 지연/즉시 로딩

목표: JPA의 프록시 메커니즘과 로딩 전략 — 8-9주차 프록시와 다른 맥락의 프록시.

Unit 8.1 — em.find() vs em.getReference()

선수 지식: Phase 5

핵심 차이:

em.find()em.getReference()
반환실제 엔티티프록시 객체
SQL 실행즉시지연 (필드 접근 시)
타입UserUser$Proxy1 (CGLIB)

예시:

User user = em.find(User.class, 1L);  // 즉시 SELECT
System.out.println(user.getName());   // SQL 실행 X (이미 로드됨)

User proxyUser = em.getReference(User.class, 1L);  // SQL 안 함
System.out.println(proxyUser.getName());           // 여기서 SELECT 실행

프록시 객체 확인:

System.out.println(proxyUser.getClass().getName());
// 출력: com.sun.proxy.$Proxy1

자기 점검

  • 8-9주차의 JDK 동적 프록시와 같은 메커니즘인가? (힌트: 비슷하지만 다른 목적)
  • getReference()로 받은 후 EntityManager가 닫히면? (힌트: LazyInitializationException)

Unit 8.2 — JPA 프록시 메커니즘

선수 지식: Unit 8.1

핵심 동작 흐름:

Step 1 — getReference() 호출:

User proxyUser = em.getReference(User.class, 1L);
// 프록시 객체 반환
// proxyUser.target = null (실제 엔티티 미로드)

Step 2 — 필드 접근 시점:

proxyUser.getName();
// ↓ 이 순간:
// 1. 영속성 컨텍스트에 실제 엔티티 로드 요청
// 2. DB SELECT 실행
// 3. 영속성 컨텍스트에 실제 엔티티 보관
// 4. proxyUser.target = 실제 엔티티 연결
// 5. proxyUser.target.getName() 호출

프록시의 특징:

  • 원본 엔티티를 상속한 클래스 (CGLIB 사용)
  • ID는 가지고 있음 (생성 시 받음)
  • 실제 데이터는 target 참조

⚠️ 주의 — 비교 시 instanceof 사용:

if (user instanceof User) { ... }   // 프록시도 잡힘 ✅
if (user.getClass() == User.class) { ... }  // 프록시는 false ❌

자기 점검

  • 프록시가 원본 클래스를 상속하는 이유는? (힌트: 다형성)
  • final 클래스가 엔티티가 될 수 없는 이유와 연결되는가? (힌트: YES)

Unit 8.3 — 지연 로딩 (LAZY)

선수 지식: Unit 8.2

핵심 개념

지연 로딩(Lazy Loading):

  • 연관 엔티티를 즉시 조회하지 않음
  • 사용 시점 에 SQL 실행
  • 프록시 객체로 참조만 유지
@Entity
public class Member {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}

동작 시나리오:

Member member = em.find(Member.class, 1L);
// SELECT * FROM member WHERE id = 1;
// member.team = 프록시 객체 (Team$Proxy1)

System.out.println(member.getTeam());     // SQL X (프록시만 출력)
System.out.println(member.getTeam().getName());  
// ↑ 이 시점에 SELECT * FROM team WHERE id = ?;

언제 사용:

  • 거의 모든 ManyToOne, OneToOne 관계
  • 사용 안 할 가능성 있는 연관 엔티티

자기 점검

  • 지연 로딩의 SQL 실행 시점을 정확히 언제인가?
  • ILIC의 운임 견적 조회 시 어떤 연관 엔티티를 LAZY로 둘까?

Unit 8.4 — 즉시 로딩 (EAGER) — 주의 ⭐

선수 지식: Unit 8.3

핵심 개념

즉시 로딩(Eager Loading):

  • 연관 엔티티를 즉시 함께 조회
  • JOIN 또는 추가 SELECT
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
Member member = em.find(Member.class, 1L);
// SELECT m.*, t.* FROM member m JOIN team t ON ... WHERE m.id = 1;
// 또는: SELECT * FROM member WHERE id = 1; + SELECT * FROM team WHERE id = ?;

연관관계별 기본값 (외워야 함) ⭐ :

관계기본 fetch
@ManyToOneEAGER (위험!)
@OneToOneEAGER (위험!)
@OneToManyLAZY
@ManyToManyLAZY

⚠️ 즉시 로딩의 문제 ⭐ :

  1. N+1 문제 — Phase 9에서 자세히
  2. JPQL에서 예상치 못한 쿼리 폭증
  3. 항상 모든 연관을 가져옴 — 성능 저하

실무 결론:

"모든 연관관계를 LAZY로 명시"

EAGER가 필요하면 fetch join (Phase 9)

@ManyToOne(fetch = FetchType.LAZY)  // 항상 명시!
@OneToOne(fetch = FetchType.LAZY)   // 항상 명시!

자기 점검

  • 왜 ManyToOne의 기본값이 EAGER인가? (힌트: 직관적이지만 위험)
  • ILIC에서 EAGER 때문에 어떤 사고가 가능한가?

📚 Phase 9 — N+1 문제와 해결 (★ 12주차 정점)

목표: 실무에서 가장 자주 만나는 JPA 성능 문제를 4단계로 해결한다.

Unit 9.1 — N+1 문제 정의 (즉시/지연 두 시나리오)

선수 지식: Phase 8

핵심 정의

N+1 문제:

"한 번의 쿼리(1)를 실행한 후, 추가로 N개의 쿼리 가 실행되는 문제"

즉시 로딩(EAGER) 시 발생:

// User에 @OneToMany(fetch = EAGER) private Set<Article> articles;
List<User> users = em.createQuery("SELECT u FROM User u").getResultList();

실행되는 SQL:

-- (1) User 100명 조회
SELECT * FROM user;

-- (N=100) User마다 article 조회
SELECT * FROM article WHERE user_id = 1;
SELECT * FROM article WHERE user_id = 2;
SELECT * FROM article WHERE user_id = 3;
... (100)

총 101개의 쿼리!

지연 로딩(LAZY) 시에도 발생 가능:

List<User> users = em.createQuery("SELECT u FROM User u").getResultList();
// (1) User 100명 조회 — LAZY라서 article은 안 가져옴

for (User user : users) {
    System.out.println(user.getArticles().size());
    // ↑ getter 호출 시점에 100번 SQL 실행!
}

결국 같은 N+1 문제

핵심 통찰:

"단순히 LAZY로 바꾼다고 해결 안 됨. JPQL/getter 패턴 자체 가 문제"

자기 점검

  • "유저가 100명이면 100번 쿼리 실행이 당연한가?"의 답은? (힌트: NO)
  • N+1을 인지하는 가장 빠른 방법은? (힌트: SQL 로그 활성화)

Unit 9.2 — join vs fetch join

선수 지식: Unit 9.1

핵심 차이 ⭐ :

일반 JOIN — N+1 해결 안 됨:

List<Member> members = em.createQuery(
    "SELECT m FROM Member m JOIN m.team t", Member.class)
    .getResultList();

실행되는 SQL:

SELECT m.*  -- ← Member만 SELECT, Team은 미포함
FROM Member m
JOIN Team t ON m.team_id = t.id;

→ Team은 영속성 컨텍스트에 안 들어옴 → getTeam() 호출 시 추가 SQL → N+1 여전!

FETCH JOIN — 한 번에 해결 ⭐ :

List<Member> members = em.createQuery(
    "SELECT m FROM Member m JOIN FETCH m.team", Member.class)
    .getResultList();

실행되는 SQL:

SELECT m.*, t.*  -- ← Team도 함께 SELECT!
FROM Member m
JOIN Team t ON m.team_id = t.id;

→ Team도 영속성 컨텍스트에 들어옴 → getTeam() SQL X → N+1 해결!

JOIN vs FETCH JOIN 정리:

JOINFETCH JOIN
결과Member만Member + Team
영속성 컨텍스트Member만둘 다
getter 시 추가 쿼리발생없음

자기 점검

  • LAZY 설정을 유지하면서 fetch join을 쓰는 게 왜 좋은가?
  • ILIC의 어떤 쿼리에 fetch join이 필요할까? (힌트: 운임 + 운임 항목)

Unit 9.3 — fetch join + 페이징의 OneToMany 함정

선수 지식: Unit 9.2

핵심 함정 ⭐ :

@ManyToOne + 페이징 — 정상 동작:

List<Member> members = em.createQuery(
    "SELECT m FROM Member m JOIN FETCH m.team ORDER BY m.id", Member.class)
    .setFirstResult(0)
    .setMaxResults(3)  // ✅ 정상
    .getResultList();
SELECT m.*, t.* FROM Member m JOIN Team t ON ... ORDER BY m.id LIMIT 3;

→ Member 기준 페이징 OK

@OneToMany + 페이징 — 데이터 조회 문제 발생 ⭐ :

List<Team> teams = em.createQuery(
    "SELECT t FROM Team t JOIN FETCH t.members", Team.class)
    .setFirstResult(0)
    .setMaxResults(3)  // ❌ 위험!
    .getResultList();

무엇이 문제인가:

-- 의도: Team 3개
-- 실제 SQL:
SELECT t.*, m.* FROM Team t JOIN Member m ON t.id = m.team_id LIMIT 3;

LIMIT 3JOIN 결과의 행 수 에 적용됨!

  • Team A(멤버 3명) → 3개 행 → LIMIT 3에 모두 잡힘
  • Team B와 C는 결과에 없음!

Hibernate의 대응:

  • 메모리에서 페이징 (DB로는 모든 데이터 로드)
  • 전체 데이터를 메모리로!
  • 경고 로그: firstResult/maxResults specified with collection fetch; applying in memory!

왜 이 문제가 일어나는가:

"OneToMany는 1행이 N행으로 늘어남. 페이징의 단위가 깨짐"

자기 점검

  • ILIC의 "최근 주문 10건 + 주문 항목" 조회 시 이 함정에 걸리는가?
  • 메모리 페이징의 위험은? (힌트: OOM)

Unit 9.4 — @BatchSize 해결 ⭐

선수 지식: Unit 9.3

핵심 해결

@BatchSize:

  • LAZY 유지 + IN 쿼리로 묶어서 조회
  • N+1 해결 + OneToMany 페이징 문제 해결
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    @BatchSize(size = 10)  // 최대 10개씩 묶어서
    @OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
    private List<Member> members;
}

페이징 + @BatchSize 사용:

List<Team> teams = em.createQuery(
    "SELECT t FROM Team t ORDER BY t.id", Team.class)
    .setFirstResult(0)
    .setMaxResults(3)  // ✅ Team 기준 정확한 페이징
    .getResultList();

for (Team team : teams) {
    team.getMembers().size();
}

실행되는 SQL:

-- (1) Team 페이징 조회
SELECT id FROM Team ORDER BY id LIMIT 3;

-- (2) IN 절로 한 번에 멤버 조회
SELECT * FROM Member WHERE team_id IN (1, 2, 3);

2번의 쿼리로 끝! (1+N → 2 또는 1+N/배치사이즈)

전역 설정:

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

⚠️ 주의 — fetch join과 함께 쓰지 말 것:

  • fetch join이 우선 → @BatchSize 무시

3가지 해결책 비교 ⭐ :

도구적합 상황
fetch joinManyToOne 단건, 페이징 없는 컬렉션
@BatchSizeOneToMany + 페이징, 일반 LAZY 최적화
EntityGraph동적으로 fetch 전략 결정

자기 점검

  • batch size를 너무 크게 설정하면 어떤 문제가? (힌트: IN 절 한도)
  • ILIC에서 default_batch_fetch_size를 어떻게 설정하는 게 좋을까?

📚 Phase 10 — 영속성 전이 + JPQL/QueryDSL

목표: JPA 학습을 마무리하는 4가지 도구.

Unit 10.1 — CASCADE (영속성 전이)

선수 지식: Phase 5, 6

핵심 개념

CASCADE:

  • "부모의 작업이 자식에게 전파"
  • 부모 저장 → 자식 자동 저장
  • 부모 삭제 → 자식 자동 삭제
@Entity
public class Parent {
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> children = new ArrayList<>();
}

CASCADE 옵션:

옵션동작
ALL모든 작업 전파
PERSIST저장만 전파
REMOVE삭제만 전파
MERGE병합만 전파
REFRESH새로고침
DETACH분리

실무 사례:

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);  // child1, child2도 자동 저장!

언제 사용:

  • 부모-자식 관계가 명확 할 때 (라이프 사이클 같이감)
  • 단일 부모 가 자식을 소유 (다른 곳에서 자식을 참조 X)
  • 예: 게시글-댓글, 주문-주문아이템

자기 점검

  • 여러 곳에서 참조되는 자식에 CASCADE.REMOVE를 걸면? (힌트: 사고)
  • ILIC에서 CASCADE가 적합한 관계는? (힌트: 주문-주문항목)

Unit 10.2 — orphanRemoval (고아 객체)

선수 지식: Unit 10.1

핵심 개념

orphanRemoval:

  • 부모-자식 관계가 끊어진 자식 자동 삭제
  • 자식 컬렉션에서 제거 시 → DB에서도 삭제
@Entity
public class Parent {
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
    
    public void removeChild(Child child) {
        children.remove(child);
        child.setParent(null);
    }
}

동작:

Parent parent = em.find(Parent.class, 1L);
parent.removeChild(parent.getChildren().get(0));
// → DELETE 자동 실행

CASCADE.REMOVE vs orphanRemoval ⭐ :

CASCADE.REMOVEorphanRemoval
트리거부모 자체 삭제자식 컬렉션 제거
사용 시점em.remove(parent)parent.children.remove(child)

둘 다 있어야 완벽한 부모-자식 관리:

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)

⚠️ 주의:

  • orphanRemoval은 단일 소유자 관계 일 때만
  • 다른 곳에서 참조되는 자식에 쓰면 사고

자기 점검

  • 부모를 삭제하면 자식도 삭제되어야 하지만 자식만 따로 삭제하지 않는다면 어떤 옵션이? (힌트: CASCADE.REMOVE만)
  • ILIC의 운임 견적과 운임 항목 관계에 어떤 조합이 적합한가?

Unit 10.3 — JPQL의 본질

선수 지식: Phase 5

핵심 개념

JPQL (Java Persistence Query Language):

  • JPA가 제공하는 객체 지향 쿼리 언어
  • 테이블이 아닌 엔티티 객체 대상
List<User> users = em.createQuery(
    "SELECT u FROM User u WHERE u.age > 20", User.class)
    .getResultList();

JPQL → SQL 변환:

SELECT * FROM user WHERE age > 20;

SQL과의 차이:

  • 대상: 엔티티 vs 테이블
  • 대소문자: 엔티티 이름은 case-sensitive
  • 자동 매핑: 결과를 엔티티 객체로

SQL이 필요한 시점:

  • 매우 복잡한 통계 쿼리
  • DB 특화 기능 사용
  • 네이티브 쿼리: em.createNativeQuery(...)

자기 점검

  • 왜 JPA가 자체 쿼리 언어를 만들었는가?
  • JPQL과 SQL을 함께 쓸 수 있는가? (힌트: YES — Native Query)

Unit 10.4 — QueryDSL (실무 표준) ⭐

선수 지식: Unit 10.3

핵심 개념

QueryDSL:

  • JPQL을 자바 코드로 작성
  • 컴파일 시점에 오류 검출
  • 동적 쿼리 작성에 강력

JPQL의 문제:

  • 문자열 기반 → 오타가 런타임 에러
  • 동적 쿼리 작성 어려움

QueryDSL의 해결:

List<User> users = queryFactory
    .selectFrom(qUser)
    .where(qUser.age.gt(20))
    .orderBy(qUser.name.asc())
    .offset(10)
    .limit(5)
    .fetch();

자동 SQL 변환:

SELECT * FROM user WHERE age > 20 ORDER BY name ASC LIMIT 5 OFFSET 10;

QueryDSL의 장점 ⭐ :
1. 타입 안전 — 컴파일 시점 오류 검출
2. 자동완성 — IDE 지원
3. 동적 쿼리BooleanBuilder, 조건문 자유롭게
4. 재사용성 — 메서드로 추출 가능

실무 표준 조합:

[Spring Data JPA] - 단순 CRUD, 메서드 이름 쿼리
       +
[QueryDSL] - 복잡 쿼리, 동적 쿼리
       =
실무 JPA 프로젝트

ILIC 사례 (가능성):

  • 단순 CRUD: Spring Data JPA Repository
  • 복잡 검색 (운임 다중 조건 조회): QueryDSL

자기 점검

  • QueryDSL 학습 비용이 있음에도 권장되는 이유는?
  • ILIC의 운임 검색 조건이 6개라면 QueryDSL이 어떻게 도움이 되는가? (힌트: 동적 where)

🎓 종합 자기 점검 (11-12주차 졸업 시험)

Part A: 11주차

SQL Mapper와 ORM

  1. SQL Mapper와 ORM의 매핑 대상 차이는?
  2. JPA, Hibernate, Spring Data JPA의 관계는?
  3. 객체-관계 패러다임 불일치 5가지 측면은?

엔티티 매핑

  1. @Entity의 4가지 조건은?
  2. 임베디드 타입을 가변으로 만들면 어떤 사고가?
  3. ddl-auto의 운영 환경 권장값은?

EntityManager와 영속성 컨텍스트

  1. EntityManagerFactory와 EntityManager의 생명주기 차이는?
  2. 영속성 컨텍스트의 정의를 한 문장으로?
  3. 엔티티 4가지 상태와 전이 메서드는?

4가지 장점 (★)

  1. 1차 캐시의 동작 6단계는?
  2. 동일성 보장이 가능한 이유는?
  3. 쓰기 지연이 작동하지 않는 경우는?
  4. 변경 감지(Dirty Checking)의 내부 동작은? (스냅샷)
  5. 플러시 발생 3가지 시점은?
  6. 1차 캐시와 2차 캐시의 차이 4가지는?

Part B: 12주차

연관관계

  1. 4가지 연관관계와 각각의 사용 시점은?
  2. 단방향과 양방향의 객체 입장 vs DB 입장 차이는?
  3. N:M을 실무에서 안 쓰는 이유와 대안은?

mappedBy

  1. 연관관계의 주인이 필요한 이유는?
  2. mappedBy의 값이 가리키는 것은?
  3. 양쪽을 함께 변경해야 하는 이유는? (힌트: 1차 캐시)

프록시와 로딩

  1. em.find()와 em.getReference()의 차이는?
  2. 프록시 비교에 instanceof를 쓰는 이유는?
  3. 4가지 연관관계의 기본 fetch 전략은? (외울 것)
  4. 모든 연관관계를 LAZY로 명시하라는 권장의 이유는?

N+1 문제 (★)

  1. N+1 문제의 정의를 한 문장으로?
  2. 즉시 로딩과 지연 로딩 모두에서 N+1이 발생하는 이유는?
  3. JOIN과 FETCH JOIN의 결정적 차이는?
  4. OneToMany + 페이징 + fetch join 사용 시 발생하는 함정은?
  5. @BatchSize가 OneToMany 페이징 함정을 어떻게 해결하는가?

CASCADE와 쿼리 도구

  1. CASCADE.REMOVE와 orphanRemoval의 차이는?
  2. JPQL이 SQL과 다른 본질적 차이는?
  3. QueryDSL을 권장하는 이유 3가지는?

📌 학습 운영 팁

9-섹션 마스터 프롬프트로 깊이 파야 할 Unit

★★★ 면접·실무 단골 (반드시):

  • Unit 5.4 — 변경 감지(Dirty Checking) 내부 동작
  • Unit 6.3 — N:1 ManyToOne (가장 자주 사용)
  • Unit 7.2 — mappedBy와 연관관계의 주인
  • Unit 8.4 — LAZY/EAGER 기본값과 권장
  • Unit 9.1~9.4 — N+1 문제와 4가지 해결책
  • Unit 10.4 — QueryDSL

★★ 매우 권장:

  • Unit 5.1 — 1차 캐시
  • Unit 5.5 — 플러시
  • Unit 6.5 — N:M 중간 엔티티 패턴
  • Unit 8.2 — 프록시 메커니즘

Phase별 진도 체크리스트

[ Part A — 11주차 ]
[ ] Phase 1 — 싱글톤 + SQL Mapper의 역사 (Unit 1.1~1.3)
[ ] Phase 2 — ORM/JPA 본질 (Unit 2.1~2.3)
[ ] Phase 3 — 엔티티 매핑 (Unit 3.1~3.4)
[ ] Phase 4 — EntityManager + 영속성 컨텍스트 (Unit 4.1~4.4)
[ ] Phase 5 — 4가지 장점 (Unit 5.1~5.5)  ★ 11주차 정점

[ Part B — 12주차 ]
[ ] Phase 6 — 4가지 연관관계 (Unit 6.1~6.5)
[ ] Phase 7 — mappedBy (Unit 7.1~7.3)
[ ] Phase 8 — 프록시와 로딩 (Unit 8.1~8.4)
[ ] Phase 9 — N+1 문제와 해결 (Unit 9.1~9.4)  ★ 12주차 정점
[ ] Phase 10 — CASCADE + JPQL/QueryDSL (Unit 10.1~10.4)

[ ] 종합 자기 점검 33문항 통과

11-12주차의 두 정점

11주차 정점 — Phase 5 (4가지 장점):

  • JPA의 핵심 가치를 이해하는 지점
  • 1차 캐시 → 동일성 → 쓰기 지연 → 변경 감지의 연쇄
  • "왜 JPA가 강력한가"의 답

12주차 정점 — Phase 9 (N+1 문제):

  • JPA 실무의 최대 함정
  • fetch join, @BatchSize 의 결정
  • 면접·실무 모두에서 가장 중요

1~12주차 통합 흐름의 절정 (다시)

Java 학습의 또 하나의 클라이맥스:

  • 1~3주차: 자바 언어
  • 4주차: 동시성
  • 5주차: Spring 입문
  • 6주차: DB 접근 진화
  • 7주차: JPA 입문 + 트랜잭션
  • 8-9주차: AOP 메커니즘 (★)
  • 10주차: 트랜잭션 마무리
  • 11-12주차: JPA 완전 정복 (★★)

8-9주차가 AOP의 정점, 11-12주차가 JPA의 정점

학습 시 주의

이번 2주차는 반드시 SQL 로그를 켜고 학습하세요:

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

특히 Phase 5 (4가지 장점) 와 Phase 9 (N+1) 는 SQL 로그 없이는 이해 불가능. 직접 실행하면서 쿼리가 어떻게 나가는지를 시각적으로 확인하세요.


profile
Software Developer

0개의 댓글