RE-Heat 개발자 일지

[JPA] [8] 프록시와 연관관계 관리 본문

백엔드/JPA

[JPA] [8] 프록시와 연관관계 관리

RE-Heat 2023. 8. 12. 23:58

https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

초급자를 위해 준비한 [웹 개발, 백엔드] 강의입니다. JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

[1] 프록시

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

엔티티를 조회할 때 연관된 엔티티가 항상 사용되는 건 아니다. 그런 엔티티를 함께 조회하는 건 효율적이지 않으므로 JPA는 엔티티가 실제로 사용될 때까지 데이터베이스 조회를 늦추는 지연 로딩 기능을 제공한다. 

그런데 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신 조회를 늦출 가짜 객체가 필요한데 이를 '프록시 객체'라 한다.

 

■ 프록시 기초

em.find() vs em.reference()

 

① em.find() : 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 통해 실제 엔티티 객체를 조회한다.

  • select 쿼리를 날릴 때 Member와 Team을 조인해서 가져오는 걸 확인할 수 있다.

 

② em.reference() : 실제 사용하는 시점까지 조회를 미루고 싶을 때 사용한다.

클릭하면 더 크게 볼 수 있습니다.

  • 실제로 사용되지 않은 좌측에선 select 쿼리가 날아가지 않는다.
  • 반면 getUserName()을 호출하기 위해선 조회하기 필요하므로 select 쿼리가 날아간다. 

 

■ 프록시 특징

  • 실제 클래스를 상속받아서 만들어짐
  • 실제 클래스와 겉모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨.

  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

 

  • 프록시 객체는 처음 사용할 때 한 번만 초기화

첫 번째가 영속성 컨텍스트에 넣는 걸 완료했으므로 두 번째엔 그 값을 가져오기만 하면 된다.

 

  • 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능

getClass() : before, after에도 같은 프록시가 호출된다.

  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크 시 주의해야 함(== 비교 실패, 대신 instance of 사용)

m1, m2에 어떤 프록시가 올지 모르므로 instanceof를 써야 한다.

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환

       이유 : ① 이미 실제 엔티티가 영속성 컨텍스트에 올라가 있으므로 굳이 프록시를 쓸 이유가 없다.

                 ② JPA는 영속 엔티티의 동일성을 보장해야 하므로 실제 엔티티 객체를 가져온다.

                      [JPA] [3] 영속성 관리 - 내부 동작 방식

       참고] 엔티티 동일성 보장 원칙에 따라 먼저 프록시를 찾으면 그 em.find()를 호출해도 프록시 반환

em.reference()를 먼저 했더니 em.find()도 프록시 객체를 쓰는 걸 확인할 수 있다.

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
    • (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

실무에서 많이 만나게 되는 그림.

 

■ 프록시 객체의 초기화

  1. getName()을 호출하는데, 프록시 객체의 Member target에 값이 없음
  2. 영속성 컨텍스트에 실제 엔티티 생성을 요청(초기화)
  3. 영속성 컨텍스트는 DB를 조회해 실제 엔티티 객체를 생성
  4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 target 멤버변수에 보관
  5. 이후 프록시 객체는 실제 엔티티 객체의 메소드를 호출해 결과를 반환한다.

 

■ 프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인
    : PersistenceUnitUtil.isLoaded(Object entity) → entityManagerFactory.getPersistenceUnitUtil().isLoaded(object)

  • 프록시 클래스 확인 방법
    entity.getClass().getname() 출력(..javasist.. or HibernateProxy...)
  • 프록시 강제 초기화
    : org.hibernate.Hibernate.initialize(entity);

select 쿼리문이 조인돼서 날아간다.

  • 참고: JPA 표준은 강제 초기화 없음
    강제 호출: method.getName();

 

[2] 지연 로딩과 즉시 로딩

실무에선 가급적 지연 로딩만 사용해야 한다
  • 지연 로딩 : 연관된 엔티티를 실제로 사용할 때까지 조회를 미룬다.
    • 설정 방법 : @ManyToOne(fetch=FetchType.LAZY)
  • 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
    • 설정 방법 : @ManyToOne(fetch=FetchType.EAGER)

 

■ 지연 로딩

Member

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();

 

JpaMain & 로그화면

  • 지연로딩으로 Member만 호출
  • Team은 프록시로 가져 옴.

  • 프록시를 강제호출하면 그때 초기화(DB 조회)한다.

■ 즉시 로딩

 

Member

@Entity
@NoArgsConstructor
@Getter
@Setter
public class Member extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn
    private Team team;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();

 

JpaMain & 로그화면

  • 즉시 로딩 시 조인을 사용해서 Member와 Team을 한 번에 조회한다.

 

■ 프록시와 즉시 로딩 주의사항

  • 가급적 지연 로딩만 사용(특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
    • 연관된 엔티티가 다수라면 수많은 테이블을 한 번에 조회해 문제가 발생한다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

        ① JPQL을 그대로 번역해 Member를 가져오기 위한 select 쿼리를 수행함.

        ② 그런데 즉시 로딩으로 돼 있으므로 Member 내부의 Team을 가져오기 위한 쿼리를 다시 수행함.

  • @ManyToOne, @OneToOne은 기본이 즉시 로딩  -> 직접 전부 LAZY로 설정해야 한다.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

 

참고 ] N + 1 문제 해결책

① 우선 FetchType을 지연 로딩으로 설정한다. (fetch = FetchType.LAZY)

② 이후 JPQL의 페치 조인을 쓰면 Member와 Team을 조인해 한 번에 가져온다. 

 

■ 지연 로딩 활용 - 실무

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

 

 

[3] 영속성 전이(CASCADE)와 고아 객체

영속성 전이 : 부모 엔티티를 저장할 때 자식 엔티티도 저장하고 싶을 때 사용한다.
고아 객체 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제한다.

 

■ 영속성 전이(CASCADE)

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이(transitive persistence)기능을 사용하면 된다. JPA는 CASCADE옵션으로 영속성 전이를 제공한다. 

 

Parent

@Entity
public class Parent{
	...
    @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
    private List<Child> children=new ArrayList<Child>();
    ...
}

주의점 :

영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다.

엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 기능을 제공할 뿐이다.

 

▶ CASCADE의 종류

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

 

Child

@Entity
@Getter
@Setter
public class Child{
    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

 

JpaMain & 로그화면

  • 영속성 전이를 사용하면 child를 따로 em.persist하지 않더라도 parent가 em.persist될 때 알아서 처리된다.
  • 연쇄 작용이라고 이해하면 된다.

 

■ 고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 고아 객체 제거라 부른다.

 

Parent

@Entity
@Getter
@Setter
public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();
  • orphanRemoval = true로 고아객체 설정

JpaMain & 로그 화면

  • parent1.getChildren().remove(0); 로 자식 엔티티를 컬렉션에서 제거할 수 있다.
  • 실제로 두 개 들어가야 할 child 중 하나가 지워진 것을 확인할 수 있다.

 

■ 고아 객체 - ※ 주의 ※

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

parent가 remove되니 child도 같이 delete 쿼리가 날아가는 걸 알 수 있다. 조심해야 하는 이유

 

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

    (CascadeType.ALL + orphanRemoval=true)

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음

Child의 영속화와 제거 모두 부모에게 달려있다.

 

참고] 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용

- 연관 깊은 도메인들을 하나의 집합으로 다루는 것을 Aggregate라 한다. 

- Aggregate에는 루트(root)와 경계(boundary)가 있는데, 경계는 Aggregate에 어떤 것을 포함할지 여부를 정의한다.

루트는 단 하나만 존재하며, Aggregate에 포함된 특정 엔티티를 가리킨다. 경계 안의 객체는 서로 참조가 가능하지만, 경계밖의 객체는 해당 Aggregate의 구성요소 가운데 루트만 참조할 수 있다.

출처 : https://catsbi.oopy.io/820584fe-b34c-442f-b9bb-32dae8ec4ef0

 

[4] 실전예제 5 - 연관관계 관리

글로벌 패치 전략 설정

① 모든 연관관계를 지연 로딩으로

② @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경

 

영속성 전이 설정 추가

① Order → Delivery를 영속성 전이 ALL로 설정
② Order → OrderItem을 영속성 전이 ALL로 설정

 

  • @ManyToOne, @OneToOne 애노테이션의 fetch 설정을 지연 로딩으로 변경
    • LAZY는 FetchType을 static화 해서 썼기 때문
  • Order의 delivery, orderItem의 cacade 부문이 ALL로 설정

'백엔드 > JPA' 카테고리의 다른 글

[JPA] [9] 값 타입 (하편)  (0) 2023.08.13
[JPA] [9] 값 타입 (상편)  (0) 2023.08.13
[JPA] [7] 고급 매핑  (0) 2023.08.11
[JPA] [6] 다양한 연관관계 매핑  (0) 2023.08.10
[JPA] [5] 연관관계 매핑 기초  (1) 2023.08.09