List
Search
1. 영속성 전이
1.1. 영속성 전이
•
영속성 전이는 캡슐화와 관련
•
객체 단위 영속성 처리의 번거로움
◦
객체들을 일일이 저장하거나 삭제하는것은 매우 번거로움
•
객체를 저장/삭제/수정할 때 연관관계로 연결된 객체도 함께 저장/수정 삭제
1.2. 도달 가능성에 의한 영속성
•
영속 상태의 객체 하위에 비영속 상태의 객체를 연결하는 경우 → 자동으로 영속 상태 전파
•
하나만 저장되면 아래 객체들이 같이 저장됨 → 캡슐화의 관점
•
예)
1.
id가 2인 Movie 객체 생성 후 영속 상태인 Screening 객체에 매핑
a.
이때 Movie 객체는 비영속 상태
2.
entityManger.flush()
a.
flush 시점에 Screening의 영속 상태가 Movie 객체에 전파
b.
Movie 객체 영속화
3.
Screening 객체를 저장하면 Movie 객체도 함께 저장됨
1.3. 영속성 전이 옵션
옵션 | 설명 |
CascadeType.PERSIST | 부모 엔티티를 EntityManager.persist()로 영속화할 때, 연관된 모든(자식) 엔티티도 함께 영속화됩니다. |
CascadeType.REMOVE | 부모 엔티티를 EntityManager.remove()로 삭제할 때, 연관된 모든 엔티티도 함께 삭제됩니다. |
CascadeType.DETACH | 부모 엔티티를 EntityManager.detach()로 영속성 컨텍스트에서 분리할 때, 연관된 모든 엔티티도 분리됩니다. |
CascadeType.MERGE | 부모 엔티티(비영속 또는 분리된 상태)를 EntityManager.merge()로 병합할 때, 연관된 모든 엔티티도 병합됩니다. |
CascadeType.REFRESH | 부모 엔티티를 EntityManager.refresh()로 새로 고침할 때, 연관된 모든 영속 엔티티도 최신 상태로 동기화됩니다. |
CascadeType.ALL | 위의 모든 옵션(PERSIST, REMOVE, DETACH, MERGE, REFRESH)이 적용되도록 설정하는 축약 옵션입니다. |
•
예제
◦
Screening을 저장/삭제할 때, Movie도 함께 저장/삭제
@Entity
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(cascade = {CascadeType.PERSIST,
CascadeType.REMOVE, })
@JoinColumn(name = "MOVIE_ID")
private Movie movie;
private int sequence;
private LocalDateTime screeningTime;
}
Java
복사
@Entity
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Integer runningTime;
private Money fee;
}
Java
복사
1.4. FK를 알지 못하는 객체를 통해 영속성이 전이될 경우
•
객체 참조와 FK 매핑이 다른 경우를 의미
◦
예) 1:N 관계에서 1쪽에서 객체를 참조하는 경우
1.4.1. 엔티티 매핑 구조
•
Movie에서 cascade 및 JoinColum 설정
// Movie 참조 필드가 전혀 없음
@Entity
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int sequence;
private LocalDateTime screeningTime;
}
Java
복사
@Entity
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Money fee;
@OneToMany(cascade = CadcadeType.PERSIST)
@JoinColumn(name="MOVIE_ID") // Screening 테이블의 FK 컬럼
private List<Screening> screenings = new HashSet<>();
// movie 객체에 Screening 추가 (양방향 설정)
public void addScreening(Screening screening) {
this.screenings.add(screening);
}
}
Java
복사
erDiagram SCREENING { bigint ID PK bigint MOVIE_ID FK int SEQUENCE datetime SCREENING_TIME } MOVIE { bigint ID PK string TITLE int RUNNING_TIME int FEE } SCREENING }o--|| MOVIE : "movie"
Mermaid
복사
•
예시 테스트 케이스
@DataJpaTest(showSql = false)
public class JpaPersistTest {
@Autowired
private EntityManager em;
@Test
public void persist() {
Movie movie = new Movie("한산", 120, Money.wons(10_000));
movie.addScreening(new Screening(1, LocalDateTime.of(2024, 12, 9, 9, 0)));
movie.addScreening(new Screening(2, LocalDateTime.of(2024, 12, 9, 11, 0)));
em.persist(movie);
em.flush();
}
}
Java
복사
1.4.2. 발생하는 SQL 흐름
•
Movie 저장
◦
Screening 에게 영속성 전이
insert into movie (fee, title, id) values (...);
SQL
복사
•
Screening들 먼저 INSERT (MOVIE_ID 없이)
◦
Screening 엔티티에는 movie에 대한 참조가 없음
◦
MOVIE_ID에 NULL 값을 저장
insert into screening (sequence, screening_time, id) values (...);
insert into screening (sequence, screening_time, id) values (...);
SQL
복사
•
자신을 가리키는 MOVIE의 ID를 찾아 MOVIE_ID에 업데이트
update screening set movie_id = :movieId where id = :screeningId;
update screening set movie_id = :movieId where id = :screeningId;
SQL
복사
1.4.3. 정리 및 권장사항
•
정리
◦
Screening 엔티티가 FK 컬럼(movie_id)를 직접 가지고 있지 않음
◦
INSERT 시점에 아무 정보도 넣을 수 없어서 JPA가 INSERT → UPDATE 2단계를 수행
•
단점
◦
불필요한 UPDATE SQL이 추가되어 성능 저하
◦
트랜잭션 로그(쓰기 양) 증가
•
해결책 → 양방향 매핑
◦
자식 쪽에서 FK 값을 알고 INSERT 때 한 번에 처리 가능
@Entity
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name="MOVIE_ID")
private Movie movie;
private int sequence;
private LocalDateTime screeningTime;
}
Java
복사
•
양방향 vs 단방향
◦
무엇이 좋다, 나쁘다는 없음
◦
상황에 따라 판단 필요
•
쿼리가 더 늘어나더라도 코드를 단방향으로 유지보수하는게 쉽다면 단방향 설정해도 됨
•
원칙적으로 1:N으로 추가되는 경우가 적어야 함
2. 즉시 로딩과 지연 로딩
2.1. 연관 관계의 조회
•
FetchType 옵션
◦
LAZY
◦
EAGER
•
EAGER
◦
객체를 즉시 조회
◦
타겟이 One일 경우 (OneToOne, ManyToOne)의 기본값
•
LAZY
◦
객체가 실제로 접근할때까지 객체 로딩을 최대한 연기
◦
타겟이 Many일 경우 (OneToMany, ManyToMany)의 기본값
•
지연 로딩은 영속성 컨텍스트의 존재에 기반
◦
지연 로딩 시에 영속성 컨텍스트가 존재하지 않을 경우 LazyInitializationException 발생
2.2. 즉시 로딩
•
OneToOne fetch 기본값은 EAGER
@Entity
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(cascade = CasecadeType.PERSIST,
fetch = FetchType.EAGER)
@JoinColumn(name="MOVIE_ID")
private Movie movie;
private int sequence;
private LocalDateTime screeningTime;
public Money getFixedFee() {
return movie.getFee();
}
}
Java
복사
@Entity
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Money fee;
}
Java
복사
•
Screening을 조회할 때 Movie도 함께 조회
•
이미 Movie가 로드 되어있으므로 getFixedFee() 에서 Movie에 접근하더라도 추가 쿼리가 실행되지 않음
@DataJpaTest(showSql = false)
public class JpaAssociationTest {
@Autowired
private EntityManager em;
@Test
public void one_to_one_loading() {
Movie movie = new Movie("한산", 120, Money.wons(10_000));
Screening screening = new Screening(movie, 1, LocalDateTime.of(2024, 12, 9, 9, 0));
em.persist(screening);
em.flush();
em.clear();
Screening loadedScreening = em.find(Screening.class, screening.getId());
assertThat(loadedScreening.getFixedFee()).isEqualTo(Money.wons(10_000));
}
}
Java
복사
SELECT
s1_0.id,
m1_0.id,
m1_0.fee,
m1_0.running_time,
m1_0.title,
s1_0.screening_time,
s1_0.sequence
FROM
screening s1_0
LEFT JOIN movie m1_0
ON m1_0.id = s1_0.movie_id
WHERE
s1_0.id = 1;
SQL
복사
2.3. 지연 로딩
@Entity
public class Screening {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(cascade = CasecadeType.PERSIST,
fetch = FetchType.LAZY)
@JoinColumn(name="MOVIE_ID")
private Movie movie;
private int sequence;
private LocalDateTime screeningTime;
public Money getFixedFee() {
return movie.getFee();
}
}
Java
복사
@Entity
public class Movie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Money fee;
}
Java
복사
양방향
•
FK를 가지고 있는 객체가 연관관계의 주인
•
반대에는 mappedBy
•
양쪽 모두 LAZY 로딩
•
FK를 알지 못하는 Movie에서 Screening 접근
◦
FK를 모르기 때문에 null 여부를 판단할 수 없음
▪
Proxy를 안만들려면 null인지를 알아야함
◦
Lazy 로딩이지만 Movie를 조회하는 순간 곧장 Screening 조회
•
OneToOne에서 FK를 가지고 있지 않은 Lazy는 결과적으로 Eager 처럼 동작함
외래키의 위치와 조회 방향을 기준으로 런타임에 프록시를 생성할 수 있는지에 따라 지연 로딩 가능 여부가 결정됨
•
JPA를 가지고 데이터 조회를 같이해서 문제
•
분리해야함