Search

JPA의 사실과 오해 (5) 동작관점의 연관관계

Tags
Study
JPA
Database
Last edited time
2025/07/14 13:42
2 more properties
Search
JPA의 사실과 오해 (5) 동작관점의 연관관계
Study
JPA
Database
JPA의 사실과 오해 (5) 동작관점의 연관관계
Study
JPA
Database

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를 가지고 데이터 조회를 같이해서 문제
분리해야함

1. 컬렉션 매핑

2. 상속 매핑