JPA에 대해 - 프록시와 연관관계 관리

쿠우·2022년 12월 29일
0

1) 프록시

프록시를 왜 배울까?

Member와 Team이 연관관계이고 Member를 조회할 때 Team도 함께 조회해야 할까?

  • 둘 다 출력 -> 이 때는 팀과 멤버 둘 다 가져와야한다.
public void printUserAndTeam(String memberId) {
 Member member = em.find(Member.class, memberId);
 Team team = member.getTeam();
 System.out.println("회원 이름: " + member.getUsername());
 System.out.println("소속팀: " + team.getName());
}
  • 회원만 출력 -> 이 때도 연관관계라고 사용하지 않는 팀까지 가져오면 손해다.
public void printUser(String memberId) {
 Member member = em.find(Member.class, memberId);
 Team team = member.getTeam();
 System.out.println("회원 이름: " + member.getUsername());
}

상황에 맞게 사용하기위하여 지연로딩과 즉시로딩을 알아야하고 이를 기초로 프록시 매커니즘을 이해해야한다.

프록시 특징

프록시 기초

  • em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
  • 실제 클래스를 상속 받아서 만들어짐 , 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

프록시 객체의 초기화 흐름

Member member = em.getReference(Member.class, “id1”);
member.getName();
    1. 클라이언트가 엔티티 메소드 호출
    1. 프록시 객체가 생성 되고 초기화(영속성 컨텍스트에)를 요청한다
    1. 영속성 컨텍스트를 거쳐 DB에 조회 되고 실제 Entity 생성된다.
    1. 생성된 Entity에 프록시의 Member target이 걸리고 target.getName()이 호출된다.

프록시 특징

여기서 중요한 점은 프록시 반영하던 엔티티를 반영하던 동일한 트랜잭션 내에서는 동일한 객체의 주소를 반영 할 수 있게 유지시켜준다는게 JPA의 성질 (어차피 다루는건 똑같이 다룬다.)

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • getReference()를 통해 먼저 프록시를 만들고 반환했으면 find()로도 프록시가 호출된다.
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림/ 실무에서 많이 만난다.)

프록시 확인 메소드들

  • 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법 :
    entity.getClass().getName() 출력(..javasist.. or
    HibernateProxy…)
  • 프록시 강제 초기화 :
    org.hibernate.Hibernate.initialize(entity);

2) 지연로딩과 즉시로딩

지연 로딩 LAZY을 사용해서 프록시로 조회

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

   @Column(name = "USERNAME")
   private String name;

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

위와 같이 만들고

  • find()로 조회 했을 때 Team은 fetch 속성이 LAZY로 지연로딩되어 프록시로 가져오게 된다.
Member member = em.find(Member.class, 1L);
  • 그리고 사용시점에서 초기화(DB 조회)
Team team = member.getTeam();
team.getName(); 

즉시 로딩 EAGER를 사용해서 함께 조회

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

   @Column(name = "USERNAME")
   private String name;

   @ManyToOne(fetch = FetchType.EAGER) //**
   @JoinColumn(name = "TEAM_ID")
   private Team team;
   ..
 }
  • Member조회시 항상 Team도 조회
  • JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회

프록시와 즉시로딩 주의

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

3) 영속성 전이: CASCADE 와 고아객체

영속성 전이: CASCADE에 대해

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때
    ex) 1대다 관계의 부모(1) 엔티티를 저장할 때 자식(다) 엔티티도 함께 저장.
    @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

영속성 전이: CASCADE - 주의!

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐

CASCADE의 종류

  • CascadeType.ALL : 저장과 삭제의 라이프 사이클을 함께 할 것인지.

  • CascadeType.PERSIST : 저장만 함께 할 것인지.

  • CascadeType.REMOVE : 삭제만 함께 할 것인지.

  • 실무에서 잘 사용 안하는 것들:

MERGE: 병합
REFRESH: REFRESH
DETACH: DETACH

CASCADE 그럼 언제 사용하나?

  • 하나의 부모가 자식들을 관리할 때 (단일 소유자 일 때)
    ex) 게시판과 첨부파일의 경로
  • 다른 데랑 연관관계가 있을때는 사용하면 안된다. 운영이 힘들어짐
  • 라이프 사이클이 거의 유사할 때 사용한다.

고아 객체

  • 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST , orphanRemoval = true) 이렇게 사용
  • 자식 엔티티를 컬렉션에서 제거 했을 때, 테이블에서도 같이 사라진 것을 확인 가능
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);

고아 객체 - 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야함! (특정 엔티티가 개인 소유할 때 사용)
  • @OneToOne, @OneToMany만 가능
  • 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.

영속성 전이와 고아객체 동시 사용에서 얻는 의미

자식을 부모만을 이용하여 생명주기 관리를 가능하게한다!

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

0개의 댓글