JPA 8th Step

최보현·2022년 9월 18일
0

JPA

목록 보기
7/10
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 - 기본편 - sec08
출처 : JPA 기본편

프록시와 연관관계 관리

프록시

만약 멤버를 조회할 때 팀도 같이 조회하고 싶을 때가 생기면 어떻게 하면 좋을까?!

이걸 해결하려면, 프록시의 기초부터 알아봐야한다고 한다

프록시 기초

em.find() VS em.getReference()

em.find() => 데이터베이스를 통해서 실제 엔티티 객체 조회

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());


jpa가 데이터를 다 이미 조인해서와서 가져와줌, 값이 출력되긴 함

em.getReference() => 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
위에 코드와 동일하게 가되, 이제 getReference()로 변경할 경우

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush();
em.clear();

Member findMember = em.getReference(Member.class, member.getId());


결과가 출력되지 않음...

하지만!

Member member = new Member();
member.setUsername("hello");

em.persist(member);

em.flush();
em.clear();

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());


쿼리가 나가고 결과가 출력된 것을 볼 수 있음!

id의 경우, 이미 우리가 이 값을 찾을 때 파라미터로 값을 넣었기 때문에 쿼리가 나가지 않고 바로 출력되었음! 하지만, username의 경우에는 db에 있기 때문에 username을 실제로 호출하는 시점(getUsername())에 쿼리를 날려서 값을 가져옴!

실제로 findMember의 정체가 무엇인지 클래스를 출력해보면
class hellojpa.Member$HibernateProxy$odcVHpjy 라고 나옴! => 하이버네이트가 강제로 만든 가짜 클래스라는 뜻

프록시 특징

  • 실제 클래스를 상속 받아서 만들어지고 겉 모양이 같음
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
  • 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
  • 처음 사용할 때 한 번만 초기화!
  • 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것이 아님! 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 원본 엔티티를 상속받기 때문에 타입 체크시 주의해야 함( '=='비교는 실패, 대신 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

em.flush();
em.clear();

Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());


반대의 경우도 동일!

Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());

Member findMember = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass()); 

System.out.println("refMember == findMember: " + (refMember == findMember)); //JPA에서는 이 둘이 true가 나오도록 보장해줘야 함

그래서 둘다 proxy로 조회가 됨!

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터뜨림!)
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());

em.detach(refMember); //너 더이상 영속성 컨텍스트에서 관리안할거야 하고 끄집어냄!

refMember.getUsername(); //당연히 출력이 안되쥬

프록시 객체의 초기화 과정

프록시 확인하는 방법

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

즉시 로딩과 지연 로딩

다시 처음으로 돌아가서 우리가 멤버를 조회할 때 팀까지 같이 조회하고 싶어지면 어떻게 해야할까?

지연 로딩 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;
}
Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);

em.flush();
em.clear();

Member m = em.find(Member.class, member1.getId());

System.out.println("m = " + m.getTeam().getClass());
//프록시로 출력됨

m.getTeam().getName();
//프록시가 초기화됨, 실제 team을 사용하는 시점

근데 만약에 우리가 팀과 멤버가 항상 같이 나와야하는 경우라면 어떻게 하면 좋을까?!

즉시 로딩 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;
}

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

프록시와 즉시로딩 주의점

  • 가급적 지연 로딩만 사용(특히 실무!)
  • 즉시 로딩을 적용하면 예상치 못한 SQL 발생
  • 즉시 로딩은 JPQL에서 N+1 문제 발생
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩이기 때문에 LAZY로 설정해야 함!
  • @OneToMany, @ManyToMany는 기본이 지연 로딩
  • JPQL fetch 조인이나, 에니팉 그래프 기능을 사용하자!

영속성 전이: CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때 ex)부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

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

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

em.persist(parent);
em.persist(child1);
em.persist(child2);
//3번을 써야 쿼리가 다 나가서 저장이 됨

하지만 cascade를 써주게 되면 em.persist(parent)만 해도 child까지 다 저장이 됨!

저장

주의사항

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

종류

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

고아 객체

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

  • orphanRemoval = true
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);
//자식 엔티티를 컬렉션에서 제거

• DELETE FROM CHILD WHERE ID=?

주의사항

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

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

CascadeType.ALL + orphanRemovel=true

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

0개의 댓글