[자바 ORM 표준 JPA 프로그래밍] Chapter 08

YUSHIN KIM·2025년 10월 26일

Chapter 08. 프록시와 연관 관계 관리

8.1 프록시

엔터티 조회 시 연관된 엔터티들이 항상 사용되는 것은 아니다.

예제 8.1. 회원 엔터티

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @ManyToOne
    private Team team;

	// getter, setter ...
}

예제 8.2. 팀 엔터티

@Entity
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

	// getter, setter ...
}

예제 8.3. 회원과 팀 정보를 출력하는 비즈니스 로직

@Test
@Transactional
void printUserAndTeam() {
    Member member = em.find(Member.class, memberId);
    Team team = member.getTeam();
    System.out.println("회원 이름: " + member.getUsername());
    System.out.println("소속 팀: " + team.getName());
}

예제 8.4. 회원 정보만 출력하는 비즈니스 로직

@Test
@Transactional
void printUser() {
    Member member = em.find(Member.class, memberId);
    System.out.println("회원 이름: " + member.getUsername());
}

printUser() 메서드는 팀 엔터티는 필요로 하지 않으므로 회원 엔터티 조회 시 팀 엔터티까지 조회하는 것은 비효율적이다. JPA는 이런 문제를 해결하고자 엔터티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 지연 로딩(Lazy Loading) 방식을 지원한다.

JPA 표준 명세는 지연 로딩의 구현 방법을 구현체에 위임했고, 하이버네이트는 프록시를 사용하는 방법과 바이트코드를 수정하는 방법을 제공한다.

8.1.1 프록시 기초

JPA에서 EntityManager.find()를 사용해 엔터티를 조회하면 영속성 컨텍스트에 해당 엔터티가 존재하지 않을 시 즉시 데이터베이스에 조회 쿼리를 전송한다. 하지만 EntityManager.getReference() 메서드를 사용하면 실제 엔터티 객체 대신 데이터베이스 접근에 대한 책임을 위임한 프록시 객체가 반환된다.

프록시의 특징

프록시 클래스는 실제 클래스를 상속한다. 그러므로 사용자 입장에서는 사용하는 객체가 진짜 객체인지 프록시 객체인지를 구분하지 않고 사용하면 된다.

프록시 객체는 위와 같이 실제 객체에 대한 참조(target)를 보관해 두고 프록시 객체의 메서드가 호출되면 그 동작에 대한 책임을 실제 객체에 위임(delegate)한다. 그러므로 프록시 객체의 메서드 호출은 실제 객체의 메서드 호출로 이어진다.

프록시 객체의 초기화

프록시 객체의 초기화란 엔터티가 실제 사용될 때 데이터베이스를 조회하여 실제 엔터티 객체를 생성하는 작업이다.

예제 8.5. 프록시 초기화 예제

@Test
@Transactional
void proxyInitialization() {
    Member member = em.getReference(Member.class, memberId);
    logger.info("Before Proxy initialization");
    member.getUsername();	// 1. getUsername();
    logger.info("After Proxy initialization");
}

예제 8.6. 프록시 클래스 예상 코드

class MemberProxy extends Member {
	
    Member target = null;
    
    public String getUsername() {
    	
        if (target == null) {
        	
            // 2. 초기화 요청
            // 3. DB 조회
            // 4. 실제 엔터티 생성 및 참조 보관
            this.target = ...;
        }
        
        // 5. target.getUsername();
        return target.getUsername();
    }
}

  1. 프록시 객체에 member.getUsername()을 호출해 실제 데이터 조회를 요청한다.
  2. 프록시 객체는 실제 엔터티 객체가 생성되어 있지 않으면 초기화를 통해 영속성 컨텍스트에 실제 엔터티 객체의 생성을 요청한다.
  3. 영속성 컨텍스트는 데이터베이스 조회를 통해 실제 엔터티 객체를 생성한다.
  4. 프록시 객체는 생성된 실제 엔터티 객체의 참조를 target 멤버 변수에 유지한다.
  5. 프록시 객체는 실제 엔터티 객체의 getUsername() 메서드를 호출해 응답을 반환한다.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 프록시 객체가 초기화되면 프록시 객체를 통해 실제 엔터티에 접근할 수 있게 된다.
  • 프록시 객체는 원본 엔터티를 상속받은 객체이므로 타입 체크 시 주의해야 한다.
  • 영속성 컨텍스트에 찾는 엔터티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 EntityManager.getReference() 메서드 결과로 프록시 객체가 아닌 실제 엔터티 객체가 반환된다.
  • 프록시 객체의 초기화에는 영속성 컨텍스트가 필요하므로 준영속 상태의 프록시를 초기화할 시 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킨다.

준영속 상태와 초기화

준영속 상태의 프록시 객체 초기화

@Test
@Transactional
void detachedProxyInitialization() {
    Member member = em.getReference(Member.class, memberId);
    em.detach(member);

    Assertions.assertThrows(LazyInitializationException.class, () -> {
        member.getUsername();
    });
}

8.1.2 프록시와 식별자

엔터티를 프록시로 조회(EntityManager.getReference()) 시 식별자(PK) 값을 매개변수로 함께 전달하기 때문에 프록시 객체는 이 값을 내부에 유지한다. 그러므로 getId() 메서드를 호출해도 내부에 유지하고 있는 식별자 값을 반환할 뿐 프록시 객체의 초기화가 발생하지는 않는다.

레거시 하이버네이트에서는 엔터티 접근 방식이 프로퍼티인 경우와 필드인 경우 프록시 객체의 초기화 여부가 달라지지만, 최근의 하이버네이트에서는 엔터티 접근 방식과 상관없이 getId() 메서드에서 다른 필드에 대한 접근이 발생하지 않으면 프록시 객체의 초기화는 이루어지지 않는다.

8.1.3 프록시 확인

JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메서드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다. JPA 표준에는 없는 기능이지만 하이버네이트의 org.hibernate.Hibernate.initialize() 메서드를 사용하면 프록시 객체를 강제로 초기화할 수 있다.

그리고 조회한 엔터티가 진짜 엔터티 객체인지 프록시 객체인지 확인하려면 클래스 이름을 직접 출력해 보면 된다.

프록시 객체의 초기화 및 여부 확인

@Test
@Transactional
void proxyInitializationCheck() {
    Member member = em.getReference(Member.class, memberId);

    Assertions.assertEquals(false, em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(member));

    Hibernate.initialize(member);

    Assertions.assertEquals(true, em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(member));
}

8.2 즉시 로딩과 지연 로딩

JPA는 연관된 엔터티의 조회 시점을 선택할 수 있도록 두 가지의 로딩 방식을 제공한다.

  • 즉시 로딩
    • 엔터티 조회 시 연관된 엔터티도 함께 조회
    • @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩
    • 연관된 엔터티를 실제 사용할 때 조회
    • @ManyToOne(fetch = FetchType.LAZY)

지연 로딩에 프록시 객체가 활용된다.

8.2.1 즉시 로딩

즉시 로딩(EAGER LOADING)을 사용하려면 @ManyToOne 애너테이션의 fetch 속성을 FetchType.EAGER로 지정한다.

예제 8.7. 즉시 로딩 설정

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @ManyToOne(fetch = FetchType.EAGER)
    private Team team;
	
    // getter, setter ...
}

예제 8.8. 즉시 로딩 실행 코드

@Test
@Transactional
void entityLoading() {
    logger.info("Before Member entity loading");
    Member member = em.find(Member.class, memberId);
    logger.info("After Member entity loading");
    Team team = member.getTeam();
    logger.info("After Team entity loading");
}

예제 8.9. 즉시 로딩 실행 SQL

2025-10-28T21:15:53.708+09:00  INFO 1206431 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : Before Member entity loading
Hibernate: 
    select
        m1_0.id,
        t1_0.id,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.id=m1_0.team_id 
    where
        m1_0.id=?
2025-10-28T21:15:53.756+09:00  INFO 1206431 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Member entity loading
2025-10-28T21:15:53.757+09:00  INFO 1206431 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Team entity loading

대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다. JPA는 연관된 엔터티가 선택적인 관계(nullable = true)이면 외부 조인, 필수적인 관계(nullable = false)이면 내부 조인을 사용한다.

8.2.2 지연 로딩

지연 로딩(LAZY LOADING)을 사용하려면 @ManyToOne 애너테이션의 fetch 속성을 FetchType.LAZY로 지정한다.

예제 8.10. 지연 로딩 설정

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
	
    // getter, setter ...
}

예제 8.11. 지연 로딩 실행 코드

@Test
@Transactional
void entityLoading() {
    logger.info("Before Member entity loading");
    Member member = em.find(Member.class, memberId);
    logger.info("After Member entity loading");
    Team team = member.getTeam();
    logger.info("After Team entity loading");
    team.getName();
    logger.info("After Team entity use");
}

지연 로딩 실행 SQL

2025-10-28T21:32:12.635+09:00  INFO 1277030 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : Before Member entity loading
Hibernate: 
    select
        m1_0.id,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.id=?
2025-10-28T21:32:12.723+09:00  INFO 1277030 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Member entity loading
2025-10-28T21:32:12.725+09:00  INFO 1277030 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Team entity loading
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        team t1_0 
    where
        t1_0.id=?
2025-10-28T21:32:12.731+09:00  INFO 1277030 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Team entity use

지연 로딩 시 연관된 엔터티의 실제 객체가 영속성 컨텍스트에 이미 있으면 프록시 객체를 사용할 필요가 없다. 이 경우 JPA는 프록시 객체가 아닌 실제 객체를 사용한다.

8.2.3 즉시 로딩, 지연 로딩 정리

  • 지연 로딩(LAZY): 연관된 엔터티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
  • 즉시 로딩(EAGER): 연관된 엔터티를 즉시 조회한다. 하이버네이트는 가능하면 SQL 조인을 사용해 한 번에 조회한다.

8.3 지연 로딩 활용

사내 주문 관리 시스템을 개발한다고 가정한다.

애플리케이션의 모델은 다음과 같이 구성된다.

  • 회원(Member)은 팀(Team) 하나에만 소속될 수 있다. (N : 1\text{N : 1})
  • 회원(Member)은 여러 주문 내역(Order)을 가진다. (1 : N\text{1 : N})
  • 주문 내역(Order)은 상품 정보(Product)를 가진다. (N : 1\text{N : 1})

비즈니스 로직의 패턴은 다음과 같다.

  • Member와 연관된 Team은 자주 함께 사용되었다. 그래서 즉시 로딩으로 설정했다.
  • Member와 연관된 Order는 가끔 사용되었다. 그래서 지연 로딩으로 설정했다.
  • Order와 연관된 Product는 자주 함께 사용되었다. 그래서 즉시 로딩으로 설정했다.

예제 8.12. 회원 엔터티

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @ManyToOne(fetch = FetchType.EAGER)
    private Team team;

    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Order> orders;
    
    // getter, setter ...
}

예제 8.13. 회원을 조회할 때 실행된 SQL

2025-10-28T22:14:11.374+09:00  INFO 1453121 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : Before Member entity loading
Hibernate: 
    select
        m1_0.id,
        t1_0.id,
        t1_0.name,
        m1_0.username 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.id=m1_0.team_id 
    where
        m1_0.id=?
2025-10-28T22:14:11.432+09:00  INFO 1453121 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Member entity loading
2025-10-28T22:14:11.432+09:00  INFO 1453121 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Team entity loading
2025-10-28T22:14:11.432+09:00  INFO 1453121 --- [ch08-jpa-proxy] [           main] jpabook.Ch08JpaProxyApplicationTests     : After Team entity use

회원과 팀은 즉시 로딩(FetchType.EAGER)으로 설정했기 때문에 회원 조회 시 하이버네이트는 조인 쿼리를 생성하여 한 번에 조회한다.

8.3.1 프록시와 컬렉션 래퍼

지연 로딩 설정 시 실제 엔터티 객체 대신 프록시 객체를 사용한다.

주문 내역 조회 테스트 코드

@Test
@Transactional
void collectionWrapperCheck() {
    Member member = em.find(Member.class, memberId);
    List<Order> orders = member.getOrders();
    Assertions.assertInstanceOf(PersistentBag.class, orders);
}

하이버네이트는 엔터티를 영속 상태로 만들 때 엔터티 필드 중 컬렉션 타입이 포함되어 있으면 이를 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션(컬렉션 래퍼, org.hibernate.collection.internal.PersistentBag)으로 변환한다.

컬렉션 객체 접근 테스트 코드

@Test
@Transactional
void collectionLoading() {
    Member member = em.find(Member.class, memberId);
    List<Order> orders = member.getOrders();
    Assertions.assertEquals(2, orders.size());
    for (Order order : orders) {
        Assertions.assertNotNull(order.getProduct());
    }
}

member.getOrders()와 같이 컬렉션 객체를 단순 조회하기만 하면 데이터베이스 쿼리가 전송되지 않지만, orders.size()와 같이 컬렉션 객체의 메서드를 호출하면 데이터베이스 쿼리가 전송된다. 주문 내역과 상품은 즉시 로딩으로 설정했기 때문에 데이터베이스에 쿼리를 즉시 전송한다.

8.3.2 JPA 기본 페치 전략

fetch 속성의 기본 설정 값은 다음과 같이 매핑 애너테이션별로 상이하다.

  • @ManyToOne, @OneToOne: 즉시 로딩(FetchType.EAGER)
  • @OneToMany, @ManyToMany: 지연 로딩(FetchType.LAZY)

JPA의 기본 페치(fetch) 전략은 연관된 엔터티가 단일 객체면 즉시 로딩, 컬렉션이면 지연 로딩을 채택하는 것이다. 컬렉션을 로딩하는 것은 비용이 많이 들고 자칫하면 필요하지 않은 다수의 엔터티 조회로 인해 부하가 발생할 수 있기 때문이다.

서적에서 권장하는 방법은 모든 연관 관계에 지연 로딩을 사용하는 것이다. 그리고 애플리케이션 개발이 어느 정도 완료 단계에 도달하면 실제 사용하는 상황을 고려해 즉시 로딩이 필요한 곳에만 적용하면 된다. SQL을 사용하면 단일 SELECT 쿼리를 JOIN 쿼리로 변경해야 하기 때문에 이런 유연한 최적화는 어렵다.

8.3.3 컬렉션에 FetchType.EAGER 사용 시 주의점

컬렉션을 하나 이상 즉시 로딩하는 것은 권장되지 않는다.

즉시 로딩이 설정된 컬렉션이 2개 이상이면 하이버네이트는 데이터베이스로부터 카티션 곱 결과를 읽어와 애플리케이션 레벨에서 필터링한다. 이로 인해 데이터베이스에서 처리해야 할 레코드가 기하급수적으로 증가하고, 애플리케이션에서도 필터링을 수행해야 하기 때문에 이중적인 부하가 발생하여 엄청난 비효율성이 야기된다.

컬렉션 즉시 로딩은 항상 외부 조인(OUTER JOIN)을 사용한다.

다대일 관계에서 단일 엔터티는 not null 제약 조건을 설정하여 하이버네이트가 내부 조인 쿼리를 생성하도록 유도할 수 있다. 하지만 일대다 관계에서 컬렉션을 즉시 로딩으로 조회할 때는 연관된 엔터티가 존재하지 않을 수 있기 때문에 하이버네이트가 항상 외부 조인 쿼리를 생성하게 된다.

  • @ManyToOne, @OneToOne
    • (optional = false): 내부 조인
    • (optional = true): 외부 조인
  • @OneToMany, @ManyToMany
    • (optional = false): 외부 조인
    • (optional = true): 외부 조인

8.4 영속성 전이: CASCADE

영속성 전이(transitive persistence) 기능을 사용하면 특정 엔터티를 영속 상태로 만들 때 연관된 엔터티도 함께 영속 상태로 만들 수 있다. JPA는 CASCADE 옵션으로 영속성 전이를 지원한다.

그림과 같이 부모 엔터티와 자식 엔터티가 일대다 관계를 맺고 있다고 가정한다.

예제 8.14. 부모 엔터티

@Entity
public class Parent {
    
    @Id
    @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<>();
}

예제 8.15. 자식 엔터티

@Entity
public class Child {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    private Parent parent;
}

이런 관계에서 부모 엔터티 1개, 그와 관련된 자식 엔터티 2개를 저장하려면 다음과 같은 코드를 작성해야 한다.

예제 8.16. 부모 자식 저장

@Test
@Transactional
void saveWithoutCascade() {

    Parent parent = new Parent();
    em.persist(parent);

    Child child1 = new Child();
    child1.setParent(parent);
    parent.getChildren().add(child1);
    em.persist(child1);

    Child child2 = new Child();
    child2.setParent(parent);
    parent.getChildren().add(child2);
    em.persist(child2);
}

JPA에서 엔터티를 저장할 때 연관된 모든 엔터티는 영속 상태여야 한다. 그러므로 부모 엔터티를 영속 상태로 만들면 자식 엔터티도 각각 영속 상태로 만들어야 하는 번거로운 과정이 추가된다. 이때 영속성 전이 기능을 사용하여 부모 엔터티만 영속 상태로 만들더라도 연관된 자식 엔터티까지 한 번에 영속 상태로 만들 수 있다.

8.4.1 영속성 전이: 저장

CASCADE 옵션 적용

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
}

예제 8.17. CASCADE 저장 코드

@Test
@Transactional
void saveWithCascade() {

    Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    child1.setParent(parent);
    child2.setParent(parent);
    parent.getChildren().add(child1);
    parent.getChildren().add(child2);

    em.persist(parent);

    Parent foundParent = em.find(Parent.class, parent.getId());
    Child foundChild1 = em.find(Child.class, child1.getId());
    Child foundChild2 = em.find(Child.class, child2.getId());

    Assertions.assertEquals(foundParent, parent);
    Assertions.assertEquals(foundChild1, child1);
    Assertions.assertEquals(foundChild2, child2);
}

테스트 코드에서는 부모 엔터티만 영속화했지만 CascadeType.PERSIST로 설정한 자식 엔터티까지 함께 영속화되는 것을 확인할 수 있다. 영속성 전이는 이처럼 엔터티를 영속화할 때 연관된 엔터티도 함께 영속화하는 편리함을 제공한다.

8.4.2 영속성 전이: 삭제

앞서 살펴본 예제에서는 영속성 전이가 영속화 시에만 적용되게 설정했으므로 연관된 엔터티들을 모두 제거하려면 다음과 같은 코드를 작성해야 한다.

연관된 엔터티 삭제

Parent foundParent = em.find(Parent.class, 1L);
Child foundChild1 = em.find(Child.class, 1L);
Child foundChild2 = em.find(Child.class, 2L);

em.remove(foundChild1);
em.remove(foundChild2);
em.remove(foundParent);

이러한 방식은 외래 키 제약 조건을 신경써서 코드를 작성해야 하기 때문에 번거롭다. 이때도 CascadeType.REMOVE 옵션의 영속성 전이 기능으로 부모 엔터티 삭제 시 연관된 자식 엔터티까지 함께 삭제할 수 있다.

부모 자식 삭제

@Test
@Transactional
void deleteWithCascade() {
    Parent parent = em.find(Parent.class, 1L);
    logger.info("Before delete");
    em.remove(parent);
    em.flush();
    logger.info("After delete");
}

테스트 로그

2025-10-29T22:22:17.221+09:00  INFO 875837 --- [ch08-jpa-proxy] [           main] jpabook.domain.CascadeTests              : Before delete
Hibernate: 
    select
        c1_0.parent_id,
        c1_0.id 
    from
        child c1_0 
    where
        c1_0.parent_id=?
Hibernate: 
    /* delete for jpabook.domain.Child */delete 
    from
        child 
    where
        id=?
Hibernate: 
    /* delete for jpabook.domain.Child */delete 
    from
        child 
    where
        id=?
Hibernate: 
    /* delete for jpabook.domain.Parent */delete 
    from
        parent 
    where
        id=?
2025-10-29T22:22:17.247+09:00  INFO 875837 --- [ch08-jpa-proxy] [           main] jpabook.domain.CascadeTests              : After delete

애플리케이션 코드에서는 부모 엔터티 하나만 삭제했지만 DELETE SQL 3개가 생성되었음을 확인할 수 있다. 삭제 순서는 외래 키 제약 조건을 고려하여 구성된다.

8.4.3 CASCADE의 종류

CascadeType 열거형에는 다양한 옵션이 존재한다.

예제 8.18. CascadeType 코드

public enum CascadeType {
    ALL,		// 모든 옵션 적용
    PERSIST,	// 영속화
    MERGE,		// 병합
    REMOVE,		// 삭제
    REFRESH,	// REFRESH
    DETACH;		// DETACH
}

PERSIST, REMOVE 옵션의 영속성 전이는 각각 em.persist(), em.remove()를 호출할 때가 아닌 영속성 컨텍스트 플러시 시 발생한다.

8.5 고아 객체

JPA는 부모 엔터티와 연관 관계가 끊어진 고아 객체(ORPHAN)를 자동으로 삭제하는 기능을 제공한다. 이 기능을 활용해 부모 엔터티의 컬렉션에서 자식 엔터티의 참조만 제거하면 자식 엔터티가 자동으로 삭제되게 할 수 있다.

예제 8.19. 고아 객체 제거 기능 설정

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}

고아 객체 제거

@Test
@Transactional
void deleteOrphan() {
    Parent parent = em.find(Parent.class, 1L);
    logger.info("Before delete");
    parent.getChildren().remove(0);
    em.flush();
    logger.info("After delete");
}

테스트 로그

2025-10-29T22:35:46.723+09:00  INFO 921948 --- [ch08-jpa-proxy] [           main] jpabook.domain.CascadeTests              : Before delete
Hibernate: 
    select
        c1_0.parent_id,
        c1_0.id 
    from
        child c1_0 
    where
        c1_0.parent_id=?
Hibernate: 
    /* delete for jpabook.domain.Child */delete 
    from
        child 
    where
        id=?
2025-10-29T22:35:46.747+09:00  INFO 921948 --- [ch08-jpa-proxy] [           main] jpabook.domain.CascadeTests              : After delete

고아 객체 제거는 참조가 제거된 엔터티 객체는 다른 곳에서 참조하지 않는 고아 객체라고 가정하고 삭제하는 기능이다. 그러므로 고아 객체로 판단되는 엔터티 객체가 다른 곳에서 참조될 시 문제가 발생할 수 있다. 때문에 orphanRemoval 옵션은 @OneToOne, @OneToMany 매핑 애너테이션에서만 사용할 수 있다.

8.6 영속성 전이 + 고아 객체, 생명주기

CascadeType.ALL, orphanRemoval = true를 동시에 사용하면 부모 엔터티를 통해 자식 엔터티를 영속화하고 삭제할 수 있다. 즉, 자식 엔터티의 생명주기를 부모 엔터티가 관리할 수 있다.

영속성 전이는 DDD의 Aggregate Root 개념 구현 시 사용하면 편리하다.

8.7 정리

  • JPA 구현체들은 객체 그래프를 자유롭게 탐색할 수 있도록 프록시 기술을 지원한다.
  • 객체 조회 시 연관된 객체를 즉시 로딩하는 방법과 지연 로딩하는 방법이 있다.
  • 영속성 전이 기능을 사용해 객체 저장 또는 삭제 시 연관된 객체까지 함께 저장하거나 삭제할 수 있다.
  • 고아 객체 제거 기능을 사용해 부모 엔터티와 연관 관계가 끊어진 자식 엔터티를 자동으로 삭제할 수 있다.
profile
안녕하세요

0개의 댓글