1. 프록시(Proxy)


JPA의 Proxy 특징은 다음과 같습니다.

  • 프록시 객체는 처음 사용할 때 한 번만 초기화합니다.
  • 프록시 객체를 초기화할 때, 프록시 객체가 실제 Entity로 바뀌는 것은 아닙니다. (프록시 클래스의 필드로 Entity를 가집니다.)
    초기화되면 Proxy 객체를 통해서 실제 Entity에 접근할 수 있습니다.
  • Proxy 객체는 Entity 클래스를 상속받습니다. 따라서 타입 비교시 주의해야 합니다. (instance of 사용)
  • 영속성 컨텍스트에 찾는 Entity가 이미 있으면 em.getReference()를 호출해도 실제 Entity가 반환됩니다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생합니다.
    (이 경우 Hibernate는 예외를 던집니다.)

 

JPA의 Proxy 확인 관련 유틸성 클래스와 메서드로는 다음과 같은 것들이 있습니다.

// 프록시 인스턴스의 초기화 여부를 확인합니다.
entityManagerFactory.getPersistenceUtil.isLoaded(Object entity);

// Proxy 클래스를 확인합니다.
entity.getClass.getName();

// Proxy를 강제 초기화합니다.
Hibernate.initialize(entity);

2. 지연 로딩


아래와 같은 Entity가 있다고 해보겠습니다.

@Entity
public class Member extends BaseEntity {
	@Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private String userName;
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

위 Entity 클래스를 .find()로 불러올때 Team도 같이 JOIN해서 불러와야할까요?
만약 Member 엔티티와 연관 관계를 가지는 엔티티가 10개가 있다면 10개의 테이블에 JOIN을 날리는 상황이 발생하게 될 것입니다.
이렇게 되면 성능상 굉장한 부담이 될 텐데요, 이를 방지하기 위해 JPA는 지연 로딩이라는 기법을 사용합니다. 지연 로딩이란 Member를 불러올때 JOIN을 하는게 아닌, 해당 연관 관계 필드를 사용하는 시점에 값을 로딩하는 것을 말합니다. 

@ManyToOne(fetch = FetchType.LAZY) 와 같이 fetch 옵션으로 지연 로딩을 설정해줄 수 있습니다.
참고로 @ManyToOne, @OneToOne은 fetch 옵션의 기본 값이 FetchType.EAGER이고, @OneToMany, @ManyToMany는 기본 값이 FetchType.LAZY입니다.

  • @ManyToOne(fetch = FetchType.LAZY): 연관 관계 필드를 사용하는 시점에 JOIN하여 값을 로딩합니다.
  • @ManyToOne(fetch = FetchType.EAGER): Member를 .find()할 때, JOIN으로 연관 관계 필드 값을 즉시 로딩합니다. 

 

위와 같이 연관 관계 필드를 즉시 로딩할 수도 있고, 지연 로딩할 수도 있는데요, 어떠한 경우든 지연 로딩 사용이 권장됩니다.
그 이유는 JPQL로 값을 채우는 과정에서 발생하는 N+1 문제 때문입니다.

entityManager.createQuery("select m from Member m", Member.class)
	.getResultList();

연관 관계 필드를 즉시 로딩으로 설정한 후, 위와 같이 Member 엔티티를 모두 조회해오게 되면 각각의 멤버마다 팀 필드를 채우기 위한 쿼리가 추가로 나가게 됩니다. 즉 불러온 Member 엔티티가 N개라면 팀 필드의 값을 세팅하기 위해 추가로 N개만큼의 쿼리가 발생합니다.

2. 영속성 전이(cascade)


혹시 특정 Entity를 .persist()로 영속 상태를 만들 때 연관 관계 필드의 값으로 세팅된 Entity도 같이 영속 상태로 만들 수 있을까요?

Team team new Team("회계팀");
Member member1 = new Member("홍길동");
Member member2 = new Member("김길동");
team.addMember(member1);
team.addMember(member2);

// 아래와 같이 .persist()를 3번 호출해줘야 합니다.
entityManager.persist(team);
entityManager.persist(member1);
entityManager.persist(member2);

위 코드를 보시면 생성한 Entity 객체에 대해 모두 .persist()를 호출해줘야 합니다. (너무 번거롭습니다.)
대신에 team만 영속 상태로 만들면 team 내에 연관 관계 필드로 세팅된 멤버들까지 자동으로 영속 상태로 만들어주면 편하지 않을까요?
@ManyToOne 혹은 @OneToMany에 cascade 옵션을 사용해 이를 해결할 수 있습니다.

@Entity
public class Team {
    // ... 생략
    
    @OneToMany(mappedby = "team", cascade = CascadeType.ALL)
    List<Member> members = new ArrayList<>();
}

위와 같이 설정해주게 되면 .persist(team); 만 해주면 자동으로 연관된 멤버들을 영속 상태로 만들어줍니다.
그럼 이 영속성 전이라는게 엄청 편해보이는데 항상 사용해도 되는 걸까요?
Team과 Member처럼 1:N 관계에서 1쪽을 부모, N쪽을 자식이라고 해보겠습니다.
아래와 같이 두 가지 조건을 만족할 때 위 cascade 옵션을 사용해야 합니다.

  1. 부모와 자식의 Life Cycle이 동일할 때. (CRUD시 항상 같이 움직여야 합니다.)
  2. 자식을 소유하는게 오롯이 부모 뿐일 때. (위 1번 조건과 유사한 맥락입니다. 이래야 Life Cycle이 완전히 동일시 되기 때문입니다.)

3. 고아 객체


부모 Entity와 연관 관계가 끊어진 자식 Entity를 고아 객체라고 합니다.
다음과 같이 부모 Entity의 컬렉션에서 자식 Entity를 제거하면 고아 객체가 됩니다.

Parent parent = entityManager.find(Parent.class, id);
parent.getChildren().remove(0); // 자식 Entity를 컬렉션에서 제거합니다.

이때, 부모 Entity 쪽에서 orphanRemoval = true 라고 옵션을 설정해주면 고아 객체에 대해 DELETE 쿼리를 날립니다.

DELETE FROM child WHERE id = ?

영속성 전이와 고아 객체 처리를 동시에 해주면 다음과 같은 이점이 생깁니다.
즉, CascadeType.ALL + orphanRemoval = true

  • 스스로 생명 주기를 관리하는 Entity는 entityManager.persist()로 영속화, entityManager.remove()로 제거합니다.
  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있습니다.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용합니다.