[JPA] 프록시와 연관관계 관리 (JPA 기본편 by 김영한)

kimdoha·2023년 8월 30일
1

[JPA]

목록 보기
7/8
post-thumbnail

프록시와 연관관계 관리

JPA의 즉시 로딩과 지연 로딩을 이해하기 위해 프록시 개념을 먼저 이해해야 합니다.

프록시

프록시는 실제 클래스를 상속 받아서 만들어지며, 실제 클래스와 겉 모양이 같습니다. 실제 값이 필요해질 때까지 조회를 미룰 수 있습니다. 프록시 객체는 실제 객체의 참조(target)를 보관하고 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출합니다.

Proxy

em.find() vs em.getReference()

  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

프록시 객체 초기화

아래는 회원 연관관계에서 프록시 객체를 활용한 예시입니다.

Team team = new Team();
team.setName("teamA");

Member member = new Member();
member.setName("memberA");
member.setTeam(team);

em.persist(team);
em.persist(member);

em.flush();		// SQL 반영
em.clear();		// 영속성 Context 1차 캐시 초기화

Member referMember = em.getReference(Member.class, "id1"); // 프록시 객체
System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());
// referMember.getId() 메모리에 있기 때문에 DB 조회를 하지 않는다.
// referMember.getName() DB에 SELECT query를 보내서 값을 가져온다.

Proxy-Initialization

Member 프록시 객체는 getName() 메소드를 호출하기 위해 실제 객체의 참조를 갖기 위해서 영속성 Context를 통해 DB에 조회를 합니다. DB에서 가져온 정보로 실제 Entity 를 생성하고, 프록시 객체가 해당 Entity를 참조하도록 설정합니다.

프록시 객체 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화 됩니다.
  • 프록시 객체가 초기화 시 실제 엔티티로 바뀌지 않고 실제 엔티티 객체를 참조합니다.
  • 프록시 객체는 원본 엔티티를 상속하므로, 타입 체크시 == 대신 instance of 를 사용합니다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환받습니다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 LazyInitializationException 예외가 발생합니다.
Member referMember = em.getReference(Member.class, "id1");
em.detach(referMember);		// 영속성 Context에서 분리

System.out.println("referMember = " + referMember.getId() + ": " + referMember.getName());

프록시 인스턴스 초기화 여부 확인

  • PersistenceUnitUtil.isLoaded(Object entity) : 프록시 클래스 확인 방법
  • entity.getClass().getName() 출력 : 프록시 강제 초기화

즉시로딩과 지연로딩

Member와 Team을 자주 함께 사용한다면 즉시 로딩 EAGER를 사용해서 함께 조회합니다. 반면 비즈니스 로직에서 Member 정보만 필요하고 Team 정보가 당장 필요 없는 경우 지연 로딩 LAZY를 사용해서 프록시로 조회 하고 동작하도록 구현합니다.(성능상 유리)
Lazy-Loading

지연 로딩 LAZY

@Entity
public class Member {
    
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}
Member member = em.find(Member.class, 1L);
// Member의 id, name 필드 가져옴
System.out.println("member = " + member.getId() + ": " + member.getName());

Team team = member.getTeam();
// 연관관계 값을 요청한 경우, 그 때 DB에 query를 보내서 team 필드를 가져옴
System.out.println("team = " + team.getId() + ": " + team.getName());

즉시 로딩 EAGER

@Entity
public class Member {
    
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

주의 해야할 점은 실무에서 즉시 로딩 보다는 가급적 지연 로딩만 사용해야합니다.
즉시 로딩을 적용하면 예상하지 못한 거대한 SQL이 발생하고, 즉시 로딩은 JPQL에서 N+1 문제를 일으킵니다. 따라서 @ManyToOne, @OneToOne 은 기본 값이 즉시 로딩이므로 지연 로딩으로 설정해서 사용해야합니다.

N + 1 문제
Member 전체 조회를 했을때 하나의 쿼리(select * from Member m) 로 인해 N개의 추가 쿼리가(각각 member의 team을 가져오는 select문) 발생하는 문제

영속성 전이 : CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용합니다. 즉, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장합니다.

영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없으며 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공합니다.

Persist-Cascade

CASCADE의 종류

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE, REFRESH, DETACH

CASCADE 예제
Parent 엔티티 childList 필드에 영속성 전이 설정(cascade)을 하게 되면, parent 엔티티가 영속될 때 이와 연관된 childList도 함께 영속 상태가 됩니다.

  • Parent 엔티티
@Entity
public class Parent {
 
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  private List<Child> childList;

}
  • Child 엔티티
@Entity
public class Child {
  
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "parent_id")
  private Parent parent;
  Child child1 = Child.of("James Son");
  Child child2 = Child.of("James Daughter");

  Parent parent = Parent.of("James");
  parent.addChild(child1);
  parent.addChild(child2);

  parentRepository.save(parent);
  • 예제 결과
    Cascade-Result

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제합니다.

참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE 처럼 동작한다.

  • orphanRemoval = true
  • @OneToOne, @OneToMany 만 가능
@Entity
public class Parent {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
  private List<Child> childList;
}

Child child1 = Child.of("James Son");
Child child2 = Child.of("James Daughter");

Parent parent = Parent.of("James");
parent.addChild(child1);
parent.addChild(child2);

parentRepository.save(parent);
// Parent의 childList는 영속성 전이 설정이 되어있기 때문에, list에 속한 child도 영속화된다.

parent.getChildList().remove(0);
// 첫번째 자식 Entity 와의 연관관계를 끊었으므로 child1은 고아 객체가 된다.
// orphanRemoval = true 설정이 되어있기 때문에 고아 객체는 자동으로 삭제된다.

OrphanRemoval-Result

영속성 전이와 고아 객체의 생명주기

  • CascadeType.ALL + orphanRemoval=true

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

실전 연습

👉 실전 예제 코드

0개의 댓글

관련 채용 정보