List
Search
1. 구조 관점의 JPA
1.1. 객체 관계 임피던스 불일치
•
구조 관점의 JPA란 도메인 객체 모델과 DB 스키마 사이의 매핑을 해주는 것을 의미함
classDiagram class Movie_DOMAIN_ENTITY { - id : int - title : string - runningTime : int - feeAmount : decimal + getter() + setter() } class MOVIE_DB_SCHEMA { ID : int TITLE : string RUNNING_TIME : int FEE_AMOUNT : decimal } Movie_DOMAIN_ENTITY <--> MOVIE_DB_SCHEMA : 매핑
Mermaid
복사
1.2. JPA 매핑의 종류
1.2.1. 기본 매핑
•
엔티티와 테이블, 필드와 컬럼의 1:1 기본 매핑
•
ex) @Entity, @Table, @Column
1.2.2. 식별자 매핑
•
객체의 식별자(Identity Field)와 데이터베이스의 주 키(Primary Key) 매핑
•
ex) @Id, @GeneratedValue
1.2.3. 연관관계 매핑
•
객체 간의 참조와 데이터베이스의 외래키(Foreign Key) 매핑
◦
외래키와 객체 참조를 상호 변환
•
ex) @ManyToOne, @OneToMany, @OneToOne, @ManyToMany, @JoinColumn, @JoinTable
classDiagram %% 객체 참조(왼쪽) class Screening_DOMAIN_ENTITY { id : int sequence : int whenScreened : datetime movie : Movie } class Movie_DOMAIN_ENTITY { id : int title : string fee : decimal } Screening_DOMAIN_ENTITY --> Movie_DOMAIN_ENTITY : 객체 참조(movie) %% 외래키(FK, 오른쪽) class SCREENING_DB_SCHEMA { ID : int MOVIE_ID (FK) : int SEQUENCE : int SCREENING_TIME : datetime } class MOVIE_DB_SCHEMA { ID : int TITLE : string RUNNING_TIME : int FEE_AMOUNT : decimal } SCREENING_DB_SCHEMA --> MOVIE_DB_SCHEMA : FK(외래키)
Mermaid
복사
1.2.4. 컬렉션 매핑
•
컬렉션(1:N, N:M)과 포함 값(Value Object, Embeddable) 매핑
◦
ex) @ElementCollection, @Embedded, @Embeddable
•
객체와 테이블 사이의 구조 반전
◦
객체 모델: Movie가 Screening들을 컬렉션으로 소유 (1:N)
◦
테이블 구조: Screening 테이블이 Movie의 외래키(FK)로 연결됨 (N:1)
→ JPA의 매핑에서 객체 참조와 외래키의 구조적 차이와 반전을 이해하는 것이 중요
•
예시)
◦
객체 모델
▪
Movie 엔티티가 여러 Screening 엔티티를 컬렉션(리스트)로 보관하는 구조
◦
객체 예시 (컬렉션 참조)
classDiagram class Movie { id : int title : string fee : decimal screenings : List~Screening~ } class Screening { id : int sequence : int whenScreened : datetime } Movie "1" --> "*" Screening : 보유
Mermaid
복사
Movie(id=1)
├─ Screening(id=1)
├─ Screening(id=2)
└─ Screening(id=3)
Plain Text
복사
•
테이블 구조
◦
실제로는 Screening 테이블이 Movie의 ID를 외래키로 가지고 있는 구조
erDiagram MOVIE ||--o{ SCREENING : has MOVIE { int ID string TITLE int RUNNING_TIME decimal FEE_AMOUNT } SCREENING { int ID int MOVIE_ID int SEQUENCE datetime SCREENING_TIME }
Mermaid
복사
ID (Screening) | MOVIE_ID |
1 | 1 |
2 | 1 |
3 | 1 |
1.2.5. 포함 값 매핑
•
객체는 Money라는 값 객체로 구성(내부 값으로 보유)
•
DB에는 단일 컬럼(MIN_ORDER_PRICE)에 저장됨
•
JPA에서 @Embedded 또는 @Embeddable로 매핑
classDiagram class Shop { id : ShopId name : String minOrderPrice : Money } class Money { amount : decimal currency : String } Shop --> Money : 값 타입(임베디드)
Mermaid
복사
erDiagram SHOP { int ID PK string NAME decimal MIN_ORDER_PRICE }
Mermaid
복사
1.2.6. 상속 매핑
•
설명: 객체 지향의 상속 구조와 데이터베이스 테이블 매핑
•
ex) @Inheritance, @DiscriminatorColumn, 전략(단일 테이블, 조인, 구현 클래스별)
•
예시
도메인 객체
단일 테이블 전략
조인 전략
구현 클래스마다 테이블 전략
1.2.7. 방향성 및 구조적 차이
•
단방향 참조와 양방향 참조 등 다양한 클래스 구조로 매핑할 수 있음
매핑을 잘 활용하기 위해서는 동작 관점에 대한 이해가 필요함.
아래에서 부터는 동작 관점의 JPA 대해서 알아보도록 하자.
2. 동작 관점의 JPA
2.1. 영속성 컨텍스트의 구성
•
영속성 컨텍스트는 작업단위 + 식별자 맵으로 구성되어 있음
2.1.1. 작업 단위(Unit Of Work)
•
정의
◦
비즈니스 트랜잭션의 영향을 받는 엔티티 목록을 유지 관리
◦
변경 사항 기록 및 동시성 문제 해결 조정
•
해설
◦
트랜잭션 내 어떤 어떤 일을 해야돼 라는 것을 정의
2.1.2. 식별자 맵 (Identity Map)
•
정의
◦
로드된 모든 객체를 맵(Map)에 유지하여 각 객체가 한번만 로드되도록 처리
◦
객체를 참조할 때 맵을 사용하여 객체 조회
•
해설
◦
DB에서 객체를 읽거나, DB에 객체를 저장하는 과정의 모든 객체를 다 저장
◦
캐시 역할도 함
▪
로드된 모든 객체를 맵에 유지하여 각 객체가 한 번만 로드되도록 처리
3. 엔티티 상태 전이 (엔티티 생명주기)
•
JPA의 동작 관점을 이해하기 위해서는 엔티티 상태 전이에 대한 이해가 필요함
•
엔티티 매니저가 JPA 메서드를 호출하면 상태 전이가 발생함
3.1. 영속 상태 (Persistent) & 비영속 상태 (Transient)
3.1.1. 정의
•
영속 상태(Persistent)
◦
엔티티가 영속성 컨텍스트에 저장되어 JPA가 관리하는 상태
◦
엔티티 매니저가 객체를 추적하며, 변경 사항이 자동으로 데이터베이스에 반영될 수 있음
•
비영속 상태(Transient)
◦
엔티티가 영속성 컨텍스트에 저장되지 않아 JPA가 전혀 관리하지 않는 상태
◦
단순한 자바 객체로서, 데이터베이스와 아무런 연관이 없음
3.1.2. 영속 상태로 전이하는 경로
•
영속 상태로 전이하는 경로
1.
객체를 조회하는 경우
2.
비영속 상태의 persist 하는 경우
3.1.3. 영속 상태 전이 경로 (1) 객체를 DB로 부터 조회하는 경우
•
엔티티 매니저에서 아래 메서드를 통해 객체를 갖고오면 객체가 영속 상태가 됨
◦
find()
◦
getReference()
◦
Query::getResultList()
◦
Query::getSingleList()
•
예시
1.
1번 Screening 객체 조회 find()
2.
식별자 맵에 1번 Screening이 존재하는지 확인
3.
식별자 맵에 없으면 데이터베이스 조회
4.
데이터베이스에서 조회한 원본 객체를 식별자 맵에 추가
•
이때, 원본을 기반으로 스냅샷을 별도로 생성
5.
원본 객체를 반환
•
해설
◦
영속 상태는 영속성 컨텍스트에서 관리 되고 있는 상태
◦
영속 상태는 JPA 관점에서 식별자 맵에 들어가있다는 것을 의미
◦
여기서 영속 상태란 JPA의 관점이며, DB와는 관계가 없음
◦
영속상태는 JPA가 “내가 관리해야하는 대상이구나” 라는 것을 의미
3.1.4. 영속 상태 전이 경로 (2) 비영속 상태의 객체를 persist 하는 경우
1.
어플리케이션 레벨에서 객체를 새로 생성함
•
Screening newScreeing = new Screening()
•
JPA의 관점에서 JPA는 해당 객체를 모름. 그래서 일단 Transient (비영속) 상태라고 정의
2.
persist()
•
entityManager.persist(newScreening)
•
위 메서드를 통해 영속성 컨텍스트에 persist 하여 식별자 맵에 등록
•
영속(Persistent) 상태로 변경
•
식별자 맵에 들어가면 영속(Persistent) 상태라고 볼 수 있음
3.
쓰기 지연 SQL 저장소에 INSERT 쿼리 등록
4.
영속성 컨텍스트 플러시(Flush)
•
persist() 실행 시점에 쿼리를 실행하지 않음
•
플러시됨 시점에 영속성 컨텍스트의 수정 사항을 DB에 동기화하여 쿼리 실행
•
일반적으로 영속성 컨텍스트는 트랜잭션이 커밋될때 자동으로 플러시됨
•
이때, 영속성 컨텍스트의 변경사항을 보내는 것이며 플러시되어도 영속 상태의 객체들은 여전히 남아 있음
◦
트랜잭션이 종료될 때 영속성 컨텍스트가 초기화 됨
3.2. 제거 상태 (Removed)
3.2.1. 정의
•
엔티티가 영속성 컨텍스트에서 삭제 대상으로 표시된 상태
•
트랜잭션 커밋 또는 flush 시 데이터베이스에서도 삭제됨
3.2.2. 예시
1.
영속성 컨텍스트에 영속 상태의 객체 존재
2.
entityManager.remove()
a.
영속성 컨텍스트에서 객체 제거 (식별자맵에서 객체 제거)
b.
쓰기 지연 SQL 저장소에 DELETE 쿼리 등록
3.
entityManager.flush()
a.
데이터베이스 레코드 삭제 쿼리 플러시
b.
DELETE 쿼리 실행
•
영속 상태를 제거 상태로 변경 예시 코드
@DataJpaTest(showSql = false)
public class JpaPersistenceContextTest {
@Autowired
private EntityManager em;
@Test
public void remove() {
DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null);
// 객체 영속화됨 (Transient -> Persitent)
em.persist(policy1);
// 쿼리 실행: insert into discount_policy (...policy1...) values...
em.flush();
// 영속화된 객체를 제거 상태로 변경
em.remove(policy1);
// 쿼리 실행: Delete from discount_policy where id = 1
em.flush();
}
}
Java
복사
3.3. 준영속 상태 (Detached)
3.3.1. 정의
•
한때 영속 상태였지만, 영속성 컨텍스트에서 분리되어 JPA가 더 이상 관리하지 않는 상태
•
객체는 여전히 존재하지만, 변경 사항이 자동으로 데이터베이스에 반영되지 않음
•
영속성 컨텍스트 내에 등록된 영속 객체를 비우는 것을 의미
◦
영속성 컨텍스트에 넣었다가 빼는 것
◦
더이상 영속성 컨텍스트에 관리되지 않으므로 자동 변경 감지가 동작하지 않음
•
실제로는 거의 쓰지 않으며, 사이드 이펙트가 발생할 수 있으므로 쓰지 않는 것을 권장함
3.3.2. 예시
•
entityManger.clear
◦
영속성 컨텍스트 내 모든 엔티티를 제거하여 준영속 상태로 변경
•
entityManger.detach
◦
영속성 컨텍스트 내 개별 엔티티를 제거하여 준영속 상태로 변경
•
entityManger.merge
◦
영속성 컨텍스트에 재등록 하는 것
◦
이때 내부적으로 SELECT 쿼리를 실행시켜 객체를 최신 상태로 유지시킴
◦
merge 과정에서 객체의 상태에 null 등이 들어간 경우 null로 머지될 수 있으므로 안쓰는 것이 좋음
3.4. 비영속 상태와 준영속 상태의 차이
영속성 컨텍스트에서 엔티리를 제거한다면 영속성 컨텍스트에서 관리되지 않는다는 점만 보면 비영속 상태와 다를 바없는데 별도의 준영속 상태로 두는 이유는 무엇일까?
3.4.1. 기본 개념 비교
•
비영속 상태(Transient)
◦
엔티티 객체가 생성되었지만 아직 영속성 컨텍스트와 전혀 관계가 없는 상태
◦
즉, 순수한 자바 객체 상태로 가 해당 객체를 인식하지 못함
◦
데이터베이스와 매핑된 적이 없는 새로운 객체
•
준영속 상태(Detach)
◦
이전에 영속 상태였다가 영속성 컨텍스트에서 분리된 상태
◦
한번은 영속성 컨텍스트에 의해 관리되었던 이력이 있는 객체
◦
이미 식별자(ID)를 갖고 있으며 데이터베이스에 저장된 적이 있는 객체
3.4.2. 핵심 차이점
•
이력 측면
◦
비영속: 영속성 컨텍스트와 한 번도 관계를 맺은 적이 없음
◦
준영속: 한 번이라도 영속 상태였던 적이 있음(관계 이력 있음)
•
식별자(ID) 보유
◦
비영속: 식별자 값이 없을 수 있음 (설정했더라도 DB와 매핑되지 않음)
◦
준영속: 반드시 식별자 값을 가지고 있음(이미 DB에 저장된 적이 있음)
•
영속화 방법
◦
비영속: persist() 메서드로 영속화
◦
준영속: merge() 메서드로 재영속화
3.4.3. 준영속 상태가 필요한 이유
식별자(ID)의 존재와 그 의미 때문
•
DB와의 연결고리
◦
준영속 객체는 ID를 갖고 있음. 이 ID는 DB의 특정 레코드와 연결되어있음을 의미
◦
만약 이 객체를 영속화해야 할때, JPA는 ID를 보고 DB와 연결되었음을 인지하고 UPDATE 를 준비 (merge 동작의 핵심)
•
비영속 객체와의 구분
◦
만약 준영속 객체를 비영속 객체로 취급한 경우, JPA는 이 객체가 DB에 이미 존재하는 레코드에 해당하는지 알 방법이 없음
◦
persist() 를 호출하면 JPA는 이 객체를 완전히 새로운 데이터를 인식하고 INSERT 시도
◦
이는 대부분의 경우 PK 중복 오류를 발생시키거나 원치 않는 데이터 삽입으로 이어짐
•
merge() 의 역할
◦
준영속 상태는 merge() 메서드와 밀접한 관련이 있음
◦
merge() 는 준영속 객체의 ID를 사용하여 영속성 컨텍스트에서 해당 ID를 가진 영속 객체를 찾거나 DB에서 조회한 후 준영속 객체의 변경사항을 영속 객체에 병합(반영)하는 역할을 함
◦
이 과정은 객체가 준영속 객체 (ID를 가지며, DB와 연결고리를 가진 상태)이기에 가능
결론적으로, 준영속 상태는 "DB에 대응하는 레코드는 있지만, 지금 당장 JPA가 관리하고 있지는 않은" 상태를 명확히 표현하기 위해 존재하며, 특히 merge()를 통한 데이터 수정 및 병합 시나리오에서 필수적인 개념임
4. 영속성 컨텍스트의 추가 기능
4.1. 트랜잭션을 지원하는 쓰기 지연(Transactional Write-Behind)
•
persist 실행 시점에 쿼리를 전송하지 않고 트랜잭션을 커밋할 때 모아둔 쿼리를 한번에 DB에 보내는 방식
•
persist() 는 DB에 저장하라는 것이 아님
•
DB에 저장할 수 있게 영속성 컨텍스트에 등록해주세요 라는 의미
•
예시 코드 1) ID가 SEQUENCE 타입인 객체 저장
public class DiscountPolicy {
@Id @GeneratedValue(generator = "discount_seq")
private Long id;
// 중략
}
@DataJpaTest(showSql = false)
public class JpaPersistenceContextTest {
@Autowired
private EntityManager em;
@Test
public void sequence_transactional_write_behind() {
DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null);
DiscountPolicy policy2 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.PERCENT_POLICY, Money.wons(1000), null);
// 객체 영속화됨 (Transient -> Persitent)
// select ext value for discount_seq
em.persist(policy1);
em.persist(policy2);
// 영속성 컨텍스트 플러시. 이때 전체 쿼리가 일괄 실행됨
// insert into discount_policy (...policy1...) values...
// insert into discount_policy (...policy2...) values...
em.flush();
}
}
Java
복사
•
예시 코드 2) ID가 IDENTITY 타입인 객체 저장
public class Movie {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 중략
}
@DataJpaTest(showSql = false)
public class JpaPersistenceContextTest {
@Autowired
private EntityManager em;
@Test
public void identity_transactional_write_behind() {
Movie movie1 = new Movie(1L, "영화1", 120, Money.wons(10000));
Movie movie2 = new Movie(1L, "영화2", 120, Money.wons(10000));
// Auto Increment ID 채번을 위해 `persist` 시점에 INSERT 쿼리 실행
// insert into movie (... 영화1...) values ...
// insert into movie (... 영화2...) values ...
em.persist(movie1);
em.persist(movie2);
em.flush();
}
}
Java
복사
4.2. 보장된 객체 식별자 범위 제공(Guaranteed Scope Object Identity)
•
1차 캐시 (First Level Cache) 역할
•
예시
1.
영속 상태의 1번 Screening 객체를 다시 조회 find()
2.
식별자 맵에 1번 Screening이 존재하는지 확인
3.
존재한다면 해당 객체를 반환
•
예시 코드 - 캐싱된 데이터 조회
@DataJpaTest(showSql = false)
public class JpaPersistenceContextTest {
@Autowired
private EntityManager em;
@Test
public void first_level_cache() {
DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null);
// 객체 영속화됨 (Transient -> Persitent)
em.persist(policy1);
// 영속성 컨텍스트 플러시. 이때 쿼리가 일괄 실행됨
// insert into discount_policy (...policy1...) values...
em.flush();
// 트랜잭션이 종료 되지 않았으므로 여전히 영속성 컨텍스트 내 영속 객체가 존재함
// 따라서 find 시점에 SELECT 쿼리가 실행되지 않음
DiscountPolicy loadedPolicy = em.find(DiscountPolicy.class, policy1.getId());
assertThat(policy1).isEqualTo(loadedPolicy);
}
}
Java
복사
4.3. 자동 변경 감지 (Automatic Dirty Checking)
JPA는 기본적으로 업데이트를 자동으로 해줌 (em.update 등이 필요 없음)
1.
DB에서 조회하여 영속성 컨텍스트 내 원본과 스냅샷을 생성하여 (영속화) 원본 반환함
2.
클라이언트가 영속 객체의 상태를 수정하는 경우, 영속성 컨텍스트 내 원본 객체가 수정됨
3.
플러시 시점에 원본 객체와 1번의 스냅샷 객체를 비교하여 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 등록
4.
INSERT, UPDATE DELETE 쿼리 정렬 후 DB 반영
•
예시 코드
@DataJpaTest(showSql = false)
public class JpaPersistenceContextTest {
@Autowired
private EntityManager em;
@Test
public void automatic_dirty_checking() {
DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null);
// 객체 영속화됨 (Transient -> Persitent)
em.persist(policy1);
// 쿼리 실행: insert into discount_policy (...policy1...) values...
em.flush();
// 영속성 컨텍스트 내 원본 객체 변경
policy1.setAmount(Money.wons(2000));
// 원본과 스냅샷 비교 후 UPDATE 쿼리 등록 후 쿼리 실행
// persist 메서드를 호출하지 않아도 자동으로 변경 감지 후 UPDATE 쿼리 실행
// Update discount_policy Set amount=20000 where id = 2
em.flush();
}
}
Java
복사
5. 2가지 핵심 영속성 컨텍스트 메커니즘
5.1. 도달 가능성에 의한 영속성 (Persistence by Reachability)
•
해설
◦
영속 상태의 객체 하위에 비영속 상태의 객체를 연결하는 경우 → 자동으로 영속 상태 전파
◦
하나만 저장되면 아래 객체들이 같이 저장됨 → 캡슐화의 관점
•
예시
1.
id가 2인 Movie 객체 생성 후 영속 상태인 Screening 객체에 매핑
a.
이때 Movie 객체는 비영속 상태
2.
entityManger.flush()
a.
flush 시점에 Screening의 영속 상태가 Movie 객체에 전파
b.
Movie 객체 영속화
3.
Screening 객체를 저장하면 Movie 객체도 함께 저장됨
5.2. 지연 로딩 (Lazy Loading)
•
즉시 로딩(Eager Loading)
◦
특정 객체를 조회할 때 연관 하위 객체를 함께 조회하게 함
◦
즉시 로딩 방법은 꼭 조인이 아닐 수도 있음
•
지연 로딩(Lazy Loading)
◦
즉시 로딩을 사용할 경우 불필요하게 연관 하위 객체가 항상 조회됨
◦
따라서 실제 조회하고자 하는 대상이 필요할때까지 조회를 미루는 것을 의미
◦
이를 미루기 위해 실제 엔티티를 생성하지 않고 프록시 객체를 생성
•
예시
1.
Screening 1 조회
a.
이때 하위 객체인 Movie는 Proxy 타입으로 존재
2.
Screening 에서 Movie로 접근
a.
DB에서 Movie 로드
b.
Proxy 객체를 Movie 객체 변경하고 영속상태로 변경
•
프록시 객체
◦
프록시 객체는 실제 엔티티를 상속받은 가상의 객체임
▪
하이버네이트가 런타임에 동적으로 생성하는 특수한 클래스의 인스턴스
▪
프록시 객체는 실제로 바이트코드 조작 라이브러리를 통해 런타임에 동적으로 생성
◦
(ManyToOne 연관관계의 경우)최초 생성 시점에는 ID 값만 가지고 있음
•
주의 사항
◦
영속성 컨텍스트의 생명주기는 트랜잭션 단위임
◦
따라서, 트랜잭션이 끝난 이후 하위 객체를 조회하려고 할 경우 LazyInitializationException 발생
6. 쿼리 언어
6.1. JPQL (Jakarta Persistence Query Language)
6.1.1. 정의
•
정의
◦
JPA에서 사용하는 객체 지향 쿼리 언어
◦
SQL과 유사하지만, DB 테이블이 아닌 엔티티 객체/속성을 대상으로 동작
◦
타입 세이프(type-safe)하고, 코드 리팩토링에도 유리함
◦
복잡한 조건, join 등도 모두 지원
•
JPQL vs SQL
◦
JPQL: select p from Product p where p.price > 10000
◦
SQL: select * from product where price > 10000
◦
JPQL은 객체(엔티티), SQL은 테이블
•
파라미터 바인딩
◦
:name(Named), ?1(Positional) 두 가지 방식 지원
◦
SQL Injection 예방, 쿼리 재사용성/유지보수성↑
6.1.2.JPQL 코드 예시
•
:policyId, :movieId 처럼 Named Parameter를 사용해서 가독성이 좋음
•
테이블명이 아니라 엔티티명/필드명을 쿼리에서 사용
// DiscountConditionJpaDAO: 할인 조건 엔티티를 정책ID로 조회
public class DiscountConditionJpaDAO implements DiscountConditionDAO {
private EntityManager em;
@Override
public List<DiscountCondition> selectDiscountConditions(Long policyId) {
return em.createQuery(
"select c from DiscountCondition c where c.policyId = :policyId", DiscountCondition.class)
.setParameter("policyId", policyId)
.getResultList();
}
}
// DiscountPolicyJpaDAO: 영화 ID로 할인 정책을 조회
public class DiscountPolicyJpaDAO implements DiscountPolicyDAO {
private EntityManager em;
@Override
public DiscountPolicy selectDiscountPolicy(Long movieId) {
return em.createQuery(
"select p from DiscountPolicy p where p.movieId = :movieId", DiscountPolicy.class)
.setParameter("movieId", movieId)
.getSingleResult();
}
}
Java
복사
6.1.3. JPQL에서 파라미터 바인딩 방식
•
Named Parameter 방식
.createQuery("select ... where c.policyId = :policyId", ...)
.setParameter("policyId", policyId)
Java
복사
•
Positional Parameter 방식
.createQuery("select ... where p.movieId = ?1", ...)
.setParameter(1, movieId)
Java
복사
6.1.4. JPQL 실행 시 데이터 동기화
•
JPQL 쿼리 실행 전에는 JPA가 자동으로 flush를 실행
◦
영속성 컨텍스트(메모리)의 변경 내용을 DB에 반영한 후 쿼리 실행
◦
항상 최신 데이터 기준으로 쿼리 결과 반환
•
find() 등 JPA 표준 메서드는 1차 캐시(메모리) 값을 우선 사용
◦
DB와 다를 수 있음
•
예시
@DataJpaTest(showSql = false)
public class JpaTest {
@Autowired
private EntityManager em;
@Test
public void query_and_flush() {
Movie movie1 = new Movie("영화1", 120, Money.wons(10000L));
Movie movie2 = new Movie("영화2", 120, Money.wons(10000L));
em.persist(movie1);
em.persist(movie2);
em.flush();
em.clear();
// 엔티티 조회 후 fee 변경 (아직 flush 안함)
Movie found1 = em.find(Movie.class, movie1.getId());
found1.setFee(Money.wons(15000));
// JPQL 실행 (쿼리 실행 전 flush가 자동 발생하여 DB에 변경 반영)
Movie result = em.createQuery(
"select m from Movie m where m.id = :movieId", Movie.class)
.setParameter("movieId", movie1.getId())
.getSingleResult();
assertThat(result.getTitle()).isEqualTo("영화1");
}
}
Java
복사
•
JPA 표준 메서드와 JPQL의 차이
JPA 표준 메서드(find 등) | JPQL/Native Query 등 | |
데이터 소스 | 1차 캐시(영속성 컨텍스트) 우선 | DB에서 읽음 |
flush 동작 | 자동 flush 발생하지 않음 | 쿼리 실행 전 자동 flush |
결과 값 | 메모리의 최신 값 | DB의 최신 값 (flush 반영) |
6.2. Spring Data JPA
6.2.1. 정의
•
JPA(Java Persistence API)를 더 쉽게 사용하고, 데이터 접근 레이어(Repository)를 자동으로 구현해주는 Spring 프로젝트
•
메서드 이름만 지으면 쿼리가 자동 생성
•
CRUD, 페이징, 정렬 등 수많은 기능 내장
•
복잡한 JPQL 없이도 대부분의 쿼리를 작성 가능
6.2.2. 메서드 이름으로 쿼리 자동 생성
•
별도의 JPQL 쿼리문 작성 없이 메서드명만으로 구현체와 쿼리 생성
•
작성 규칙
sfind[query|read|get] + [엔티티 이름] + By + 변수 이름 + [키워드] + (파라미터 목록)
Plain Text
복사
•
예시 코드
List<Movie> findByTitleLike(String title);
Java
복사
•
자동 생성되는 JPQL
select m from Movie m where m.title like :title
SQL
복사
6.2.3. 메서드 이름으로 자동 쿼리 생성의 한계
•
아래의 경우 자동 쿼리 생성에는 한계가 존재
◦
복잡한 조건(AND, OR, BETWEEN, JOIN 등) 조건이 많아질 때
◦
쿼리의 가독성이 떨어질 정도로 메서드 이름이 너무 길어질 때
•
직접 쿼리문을 명시하는것이 훨씩 명확
◦
@Query
▪
복잡한 쿼리, 서브쿼리, JOIN 등 메서드 이름으로 표현 불가한 경우 직접 명시
▪
가독성↑, 유지보수↑
@Query("select m from Movie m join m.director d where d.name = :directorName and m.rating > :rating")
List<Movie> findByDirectorNameAndRating(
@Param("directorName") String directorName,
@Param("rating") int rating
);
Java
복사
◦
@Modifying
▪
JPQL로 데이터를 직접 변경하는 경우(@Query와 같이 사용)
▪
Spring Data JPA가 트랜잭션, 변경 감지 등을 올바르게 처리할 수 있게 해줌
@Modifying
@Query("update Movie m set m.rating = :rating where m.title = :title")
int updateRatingByTitle(@Param("rating") int rating, @Param("title") String title);
Java
복사
6.2.4. Repository 인터페이스 계층 구조
•
Spring Data JPA의 주요 인터페이스 상속 구조
◦
Repository: 최상위 마커 인터페이스
◦
CrudRepository: 기본 CRUD 기능 제공 (save, findById, delete 등)
◦
PagingAndSortingRepository: 페이징, 정렬 기능 추가
◦
JpaRepository: JPA 기능+배치, flush, 벌크 등 확장 제공
◦
(Optional) QueryByExampleExecutor: 예시(Example) 객체로 동적 쿼리 제공
Repository<T, ID>
↑
CrudRepository<T, ID>
↑
PagingAndSortingRepository<T, ID>
↑
JpaRepository<T, ID>
Plain Text
복사
•
예시 계층 구조
<interface> Repository<T,ID>
└─<interface> CrudRepository<T,ID>
└─<interface> PagingAndSortingRepository<T,ID>
└─<interface> JpaRepository<T,ID>
Plain Text
복사