TIL_221017_자바 ORM 표준 JPA 프로그래밍_06 정리

창고·2022년 10월 17일
0

9. 프록시와 연관관계 관리

(1) 프록시

  • 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() vs em.getReference()
    • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
    • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
    Member findMember = em.getReference(Member.class, member.getId());
    System.out.println("findMember = " + findMember.getClass()); // 1
    System.out.println("findMember.id = " + findMember.getId()); // 2
    System.out.println("findMember.username = " + findMember.getUsername()); // 3
    
    // 1. HibernateProxy~~ 클래스로 나옴
    // 2. 이미 id값을 가지고 getReference를 하여 프록시 객체를 생성하였기 때문에 SQL 없이도 가져옴
    // 3. username 은 프록시 객체에 없는 데이터이므로 실제 데이터베이스를 조회 (쿼리가 생성, 호출됨)
  • 프록시 특징

    • 실제 클래스를 상속 받아서 만들어짐 -> 실제 클래스와 겉 모양이 같음
    • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨 (이론상)
    • 프록시 객체는 실제 객체의 참조(target)를 보관
    • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
  • 프록시 객체의 초기화

    • 프록시 객체 호출 후 메소드 호출 시 프록시 객체는 영속성 컨텍스트에 초기화 요청하며 영속성 컨텍스트는 DB를 조회한 후 실제 객체를 생성함. 이 후 프록시 객체는 실제 생성된 객체의 메소드를 호출하게 됨
Member member = em.getReference(Member.class, "id1");
member.getName();

  • 프록시의 특징 (아주 중요)

    • 프록시 객체는 처음 사용할 때 한 번만 초기화
    • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능 (바뀌는 것이 아니라 조회한 후 메소드나 값을 복사해서 쓴다고 생각하면 될지도)
    • 프록시 객체는 원본 엔티티를 상속 받으며 따라서 타입 체크 시 주의 해야 함 (== 비교 실패, 원본이 부모, 프록시가 자식이므로 비교가 불가능. instanceOf 사용)
    Member m1 = em.find(Member.class, member1.getId());
    Member m2 = em.find(Member.class, member2.getId());
    Member m3 = em.getReference(Member.class, member3.getId());
    
    System.out.println("m1 == m2" + (m1.getClass() == m2.getClass)); // true 반환
    System.out.println("m1 == m3" + (m1.getClass() == m3.getClass)); // false 반환
    System.out.println("m1 == m3" + (m3 instanceOf Member)); // true 반환  
    • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환 (이미 올려져 있는 객체가 있는데 굳이 한번 더 DB에서 조회해서 처리할 필요가 없기 때문 / JPA의 작동 매커니즘 상 m1과 reference를 비교하더라도 동일하다고 보장을 해줌) -> 그 역도 성립. 프록시로 먼저 생성할 경우 find()를 해도 프록시 객체를 반환
    Member m1 = em.find(Member.class, member1.getId());
    // 이미 영속성 컨텍스트에 올라감
    
    Member reference = em.getReference(Member.class, member1.getId());
    System.out.println("reference = " + reference.getClass()); // proxy 객체가 아닌 Member 호출
    
    System.out.println("a == a: " + (m1 == reference)); // JPA 동작 매커니즘 상 true로 보장
    • (중요) 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화할 경우 문제 발생 (실무에서 많이 발생) -> LazyInitializationException
    Member reference = em.getReference(Member.class, member1.getId());
    System.out.println("reference = " + reference.getClass());
    
    em.detach(reference); // 준영속 상태로 해제 (clear, close 등...)
    reference.getUsername(); // LazyInitializationException 발생
  • 프록시 확인

    • 프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity)
    • 프록시 클래스 확인 : entity.getClass().getName()
    • 프록시 강제 초기화 : org.hibernate.Hibernate.initialize(entity);
    • JPA 표준은 강제 초기화 없으며 강제 호출은 실제 객체의 메소드 호출로 강제 호출

(2) 즉시 로딩과 지연 로딩

  • 그래서 Member를 조회할 때 Team도 함께 조회해야 하냐고 -> 비즈니스 로직마다 다름
  • 함께 조회할 필요 없다! -> 지연 로딩 LAZY를 사용, 적용된 객체를 프록시로 조회
    • 아래의 상황에서 Member만 조회할 경우 team은 프록시이므로 실제 조회, 사용할 필요가 없기 때문에 별도로 조회, 쿼리 발생 X
    • 아래의 상황에서 Member와 Team을 같이 조회/사용할 경우 team은 프록시이지만 실제로 사용을 해야 하므로 초기화 진행, 쿼리 발생
@Entity
public class Member {

...

	@ManyToOne(fetch = FetchType.LAZY) // team을 프록시로 조회
	@JoinColumn(name = "TEAM_ID)
	private Team team;

  • 함께 조회해야 한다! -> 즉시 로딩 EAGER를 사용, 함께 조회
@Entity
public class Member {

...

	@ManyToOne(fetch = FetchType.EAGER) // 함께 조회
	@JoinColumn(name = "TEAM_ID)
	private Team team;

  • JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회

  • 프록시와 즉시 로딩 시 주의 (가장 중요함)

    • 가급적 지연 로딩만 사용 (특히 실무에서)
    • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
    • 즉시 로딩은 JPQL에서 N+1 문제를 일으킴 -> 일단 모두 지연 로딩으로 설정한 후 추후 JPQL의 fetchJoin을 사용하여 여러 객체를 한번에 가져옴
    List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
    
    // JPQL 작성 시 SQL로 번역되어서 쿼리 발생 -> 1번
    // 위의 쿼리로 Member를 조회해보니 Team이 즉시 로딩이 걸려 있음 -> 반드시 가져와야 됨 -> 한번 더 조회 (쿼리 또 발생)
    // Member만 가져오려고 했지만 (1번) 쿼리가 2번 발생
    • @ManyToOne, @OneToOne은 기본이 즉시 로딩 -> LAZY로 설정 -> 프로젝트 수정 필요
    • @OneToMany, @ManyToMany는 기본이 지연 로딩
  • 지연 로딩 활용 - 실무

    • 모든 연관관계에 지연 로딩을 사용해라!
    • 실무에서 즉시 로딩을 사용하지 마라!
    • JPQL fetch 조인이나 엔티티 그래프 기능을 사용해라!
    • 즉시 로딩은 상상하지 못한 쿼리가 나간다

(3) 영속성 전이 : CASCADE

  • 즉시 로딩, 지연 로딩과는 관계 없음! (오해 ㄴㄴ)
  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 (예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 경우)
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

// cascade를 하지 않았을 경우 예시

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

Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

// 3번 persist를 진행해야 함
em.persist(parent);
em.persist(child1);
em.persist(child2);

// cascade를 사용할 경우
Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();

// 부모를 persist를 해도 자식 객체가 전부 영속화됨
em.persist(parent);
  • 영속성 전이 : CASCADE 주의
    • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
    • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
  • CASCADE의 종류
    • ALL : 모두 적용
    • PERSIST : 영속
    • REMOVE : 삭제
    • 이외 MERGE, REFRESH 등이 있으나 ALL, PERSIST가 많이 쓰임
  • 실무에서 사용하는 두 가지 조건
    • 자식이 부모 하나에게서 다뤄질 때 (소유권이 부모 하나인 경우) 에 사용할 수 있으나 자식이 여러 테이블, 객체에서 다루게 되면 사용하지 않는 것이 좋음)
    • 자식과 부모의 생명 주기가 거의 동일할 때

(4) 고아 객체

  • 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • orphanRemoval = true
  • DELETE 쿼리가 발생
@OneToMany(mappedBy="parent", orphanRemoval = true)
  • 고아 객체 주의 사항
    • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
    • 참조하는 곳이 하나일 때 사용해야 함!
    • 특정 엔티티가 개인 소유할 때 사용 가능
    • @OneToOne, @OneToMany만 사용 가능
    • 개념적으로 부모를 제거할 경우 자식은 고아가 됨. CascadeType.REMOVE처럼 동장

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

  • CascadeType.ALL + orphanRemoval=true
  • 스스로 생명 주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 할 경우 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계 (DDD)의 Aggregate Root 개념 구현 시 유용
profile
공부했던 내용들을 모아둔 창고입니다.

0개의 댓글