JPA 프록시와 연관관계 관리

zwon·2023년 10월 4일
0

JPA

목록 보기
7/9

회원과 크루 예시를 그대로 가져가겠다.

@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne 
    @JoinColumn(name="crew_id")
    private Crew crew;
    ...
}
@Entity
public class Crew {
	@Id @GeneratedValue
    private Long id; //crewId
    
    private String name;    
    
    @OneToMany(mappedBy="crew") // Crew입장에서 Member는 1:N
    private List<Member> members = new ArrayList<>();
    ...
}

Member를 조회할 때 Crew도 함께 조회해야 할까?

Member member = em.find(Member.class, 1L);

printMember(member); // 1. Member만 조회
printMemberAndCrew(member); // 2. Member와 Crew를 함께 조회

만약 Member정보만 출력하고싶고 Crew는 출력하고싶지 않는 경우도 있을 것이다.
사용하지도 않는 정보인 Crew 정보까지 땡겨온다면 뭔가 깔끔하지 않다.
어떤 경우엔 Member와 Crew를 함께 가져오고싶고, 어떤 경우엔 Member만 가져오고싶을 땐 어떻게 해야할까?

JPA는 이러한 문제를 지연 로딩, 프록시 등을 통해 해결한다.


프록시

  • JPA는 em.find(...) 말고도 em.getReference()라는 메서드도 제공한다.
  • em.find(...)는 DB를 통해 실제 엔티티 객체를 조회한다.
  • em.getReference()는 DB 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
    즉 DB에 쿼리가 나가지 않는데 객체 조회가 되는 것이다.
Member member = new Member();
member.setUsername("hello");

em.persist(member);

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

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

tx.commit();
실행 결과
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (MEMBER_ID, createBy, createdDate, lastModifiedBy, lastModifiedDate, city, street, zipcode, team_TEAM_ID, name) 
        values
            (null, ?, ?, ?, ?, ?, ?, ?, ?, ?)
10월 04, 2023 1:32:44 오전 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/hellojpa;MODE=MYSQL]

Process finished with exit code 0
  • 실행 결과에서 볼 수 있듯이 select쿼리가 나가지 않았다.
  • 하지만 다음과 같이 getReference()를 통해 가져온 값을 직접 사용(getUsername())하면 쿼리문이 나간다.
...
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());
System.out.println("findMember.getUsername() = " + findMember.getUsername());
      
tx.commit();
findMember.getClass() = class hellojpa.Member$HibernateProxy$2gyJwktl
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_4_0_,
        member0_.createBy as createBy2_4_0_,
        member0_.createdDate as createdD3_4_0_,
        member0_.lastModifiedBy as lastModi4_4_0_,
        member0_.lastModifiedDate as lastModi5_4_0_,
        member0_.city as city6_4_0_,
        member0_.street as street7_4_0_,
        member0_.zipcode as zipcode8_4_0_,
        member0_.team_TEAM_ID as team_TE10_4_0_,
        member0_.name as name9_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
findMember.getUsername() = hello
  • select쿼리문이 나갔다.
  • findMember().getClass()를 통해 가져온 값을 보면 Proxy라는 단어가 있는 것을 볼 수 있다. 즉 하이버네이트가 만든 가짜 객체이다.

프록시 특징

  • 실제 클래스를 상속받아서 만들어졌다. 그래서 타입 비교시 instance of 사용
  • 그래서 실제 클래스와 겉 모양은 같다.
  • 프록시 객체는 실제 객체의 참조를 보관하고있어 프록시 객체를 호출(ex. findMember.getUsername())하면 프록시 객체는 실제 객체의 메서드를 호출한다.
  • 프록시 객체는 처음 사용할 때 한 번만 초기화한다.
  • 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는것이 아니라 프록시 객체를 통해 실제 엔티티에 접근이 가능해지는 것
  • 만약 getReference()로 조회할 객체가 영속성 컨텍스트에 존재한다면 실제 엔티티를 반환한다.
    • == 비교 시 true값을 반환해야하기 때문에
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화할 떄 문제가 발생한다.

프록시 확인 메서드

프록시 인스턴스 초기화 여부
entitiyManagerfactory.getPersistenceUnitUtil.isLoaded(Object entity) 

프록시 클래스 확인 방법
entity.getClass().getName() 

프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity)

약간 헷갈린다면 직접 em.find(...), em.getReference(...)해서 값을 가져온 다음 타입 비교를 해보자.


프록시에 대해 알아보았고 프록시에 대한 이해가 있어야 즉시 로딩, 지연 로딩을 깊이 이해할 수 있다.
맨 위에서 언급한 Member를 조회할 때 Crew도 함꼐 조회해야하는지에 대한 질문의 답을 이제 알아보자.

지연 로딩

  • Member의 정보만 필요한 경우 Crew를 함께 땡겨올 필요가 없으니 JPA는 지연로딩이라는 옵션을 제공한다.
  • @ManyToOne(fetch = FetchType.LAZY)
@Entity
public class Member extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String username;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name="crew_id")
  private Crew crew;
}
  • FetchType.LAZY로 조회하는 경우 Member클래스만 DB에서 조회하고 Crew는 프록시 객체로 조회한다.
  • 그래서 프록시 객체로 가져온 crew의 값을 직접 사용하는 순간 DB에 쿼리가 나간다.
  • 즉 지연 로딩으로 설정하면 연관된 정보들은 프록시 객체로 가져온다.

즉시 로딩

  • 지연 로딩과 다르게 연관된 것들도 프록시 객체가 아닌 Join해서 DB에서 실제로 조회하도록 하고싶으면 즉시 로딩을 사용하면 된다.
@Entity
public class Member extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String username;

  @ManyToOne(fetch = FetchType.EAGER)
  @JoinColumn(name="crew_id")
  private Crew crew;
}
  • 즉시로딩보단 지연로딩을 사용하도록 하자.
  • 즉시로딩은 예상치 못한 SQL이 발생하고 N+1문제를 일으키기때문이다.
    • N+1문제에 대해서는 따로 정리할 생각
  • @ManyToOne, @OneToOne은 기본값이 즉시 로딩이어서 값을 변경해줘야함
  • @OneToMany, @ManyToMany는 기본값이 지연 로딩이다.

직접 즉시 로딩, 지연 로딩을 해보면서 나가는 쿼리문을 눈으로 보는 것을 추천한다.


영속성 전이 cascade

  • 영속성 전이란 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용

cascade = CasecadeType.ALL

  • CasecadeType.ALL은 연관된 엔티티까지 모두 영속 상태로 만들어준다.
  • 즉 em.persist(parent)할 경우 child까지 다 persist된다.
  • 하지만 연관관계 매핑과는 관련이 없고 단순히 연관된 엔티티도 함께 영속화하는 편리함을 제공
@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 = new ArrayList<>();
  ...
}
@Entity
public class Child{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @ManyToOne
  @JoinColumn(name = "PARENT_ID")
  private Parent parent;
  ...
}
  • 아래 코드처럼 em.persist(ch1);을 따로 안해주더라도 영속성 전이 옵션을 통해 ch1,ch2도 영속 상태로 만들 수 있다.
Child ch1 = new Child();
Child ch2 = new Child();

Parent parent = new Parent();
parent.addChild(ch1); // 연관관계 편의 메서드
parent.addChild(ch2); // 연관관계 편의 메서드

em.persist(parent);
      
tx.commit();
  • CasecadeType.ALL뿐만 아니라 PERSIST, REMOVE 등이 있는데 이와 관련한 부분은 우선 넘어가겠다. 필요할 때 찾아쓰도록.

고아 객체

  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라고 하는데 이러한 객체를 자동으로 삭제하고 싶을 땐 다음과 같은 설정을 하면된다.
  • 주의할 점은 참조하는 곳이 하나일 때 , 특정 엔티티가 개인 소유할 때, @OneToOne, @OneToMany만 가능하다.
@Entity
public class Parent {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToMany
  (mappedBy = "parent",
  cascade = CascadeType.ALL,
  orphanRemoval = true) //이 부분
  private List<Child> childList = new ArrayList<>();
  ...
}
@Entity
public class Child{

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @ManyToOne
  @JoinColumn(name = "PARENT_ID")
  private Parent parent;
  ...
}

자바 ORM 표준 JPA 프로그래밍-기본편을 학습하면서 정리한 블로그입니다.

profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글