본문 바로가기

Back-End/JPA

프록시와 연관관계 관리(JPA)

프록시란 무엇인가?

프록시는 실제 엔티티 대신 가짜 객체를 반환해서 필요한 시점에 데이터를 조회하는 개념이다.

em.find() : 데이터베이스에서 실제 엔티티를 즉시 조회한다.
em.getRegerence() : 데이터베이스에 바로 접근하지 않고 프록시 객체(가짜 객체)를 반환한다.

// em.find() 예제
Member member = em.find(Member.class, 1L);
System.out.println(member.getName()); // 데이터베이스에서 즉시 조회

// em.getReference() 예제
Member proxyMember = em.getReference(Member.class, 1L);
System.out.println(proxyMember.getName()); // 이 시점에 데이터베이스 조회 (프록시 초기화)
  • em.find()는 바로 쿼리를 실행하지만, em.getReferencer()는 필요할 때 데이터를 가져온다.

프록시 특징

  • 실제 클래스를 상속받아 생성됨
    -> 겉모습은 같아서 진짜 객체인지 프록시인지 구분하지 않고 사용 가능.
  • 프록시 초기화
    -> 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
    -> 초기화되면 실제 엔티티를 데이터베이스에서 가져오지만, 프록시 객체 자체는 유지가 된다.
  • 타입 비교 주의
    -> 프록시 객체는 원본 엔티티를 상속 받기 때문에 == 비교는 실패하고, instanceOf로 비교해야함.
  • 영속성 컨텍스트(1차 캐시)에 찾는 엔티티가 이미 있으면, em.getReference()를 호출해도 실제 엔티티 반환.
    반대로 JPA에서는 같은 트랜잭션 안에서 항상 같은 엔티티를 보장해주어야 하기 때문에 먼저 getReference를 호출하고 나중에 find를 호출해도 find는 진짜 엔티티가 아닌 프록시 엔티티로 반환이된다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
Member member = em.getReference(Member.class, 1L);

if (member instanceof Member) {
    System.out.println("프록시 객체임에도 instanceof는 true 반환");
}

System.out.println(member.getClass()); // 출력: 프록시 클래스

 

즉시 로딩(EAGER) VS 지연 로딩(LAZY)

지연 로딩(LAZY)
프록시를 사용해서 필요한 시점에만 연관된 데이터를 가져온다.

@Entity
public class Member {
    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
    private Team team;
}

Member member = em.find(Member.class, 1L); // team 데이터는 조회되지 않음
System.out.println(member.getTeam().getName()); // 이 시점에서 team이 초기화됨

즉시 로딩(EAGER)
연관된 데이터를 즉시 가져온다.

@Entity
public class Member {
    @ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
    private Team team;
}

Member member = em.find(Member.class, 1L); // team 데이터도 함께 조회됨

프록시와 즉시로딩 주의 (실무에서 중요)

  • 실무에서는 모든 연관관계에 지연 로딩을 사용해라!
  • 실무에서 즉시 로딩을 사용하지 마라!
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
  • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

영속성 전이(CASCADE)

부모 엔티티를 저장할 때 연관된 자식 엔티티도 함께 저장되도록 설정할 수 있다.

@Entity
public class Parent {
    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> children = new ArrayList<>();

    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
}

@Entity
public class Child {
    @ManyToOne
    private Parent parent;
}

// 영속성 전이 예제
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();

parent.addChild(child1);
parent.addChild(child2);

em.persist(parent); // parent와 함께 child1, child2도 저장됨

영속성 전이 주의!

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화 하는 편리함을 제공할 뿐 그 이상 그 이하도 아니다.
  • 특정 엔티티가 개인 소유할 때만 사용하는 것이 좋다.

고아객체 

부모와의 관계가 끊어진 자식 엔티티를 자동으로 삭제하고 싶을 때 사용한다.

// 설정
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();

//예제
Parent parent = em.find(Parent.class, 1L);
parent.getChildren().remove(0); // 자식 엔티티가 삭제됨

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

  • CascadeType.ALL + orphanRemovel = true
  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계의 Aggregate Root 개념을 구현할 때 유용

실무 팁 요약

  1. 모든 연관관계는 지연 로딩(LAZY)을 기본으로 사용하라.
  2. 즉시 로딩(EAGER)은 예측하지 못한 쿼리를 발생시켜 성능 문제를 일으킬 수 있다.
  3. CasCadeType과 oprhanRemoval은 특정 엔티티를 소유할 때만 사용하라.