[JPA 기본편] 8. 프록시와 연관관계 관리

2024. 5. 22. 17:21· Spring/자바 ORM 표준 JPA 프로그래밍
목차
  1. 프록시란?
  2. 프록시 객체의 초기화 과정
  3. 프록시의 특징
  4. 프록시 확인 방법
  5. 지연 로딩과 즉시 로딩
  6. 지연 로딩 : LAZY
  7. 즉시 로딩 : EAGER
  8. 영속성 전이(CASCADE)와 고아 객체
  9. 영속성 전이: CASCADE
  10. 고아 객체 - 제거
  11. 영속성 전이 + 고아 객체, 생명 주기

엔티티 조회는 2가지 방식으로 할 수 있다.

  1. em.find() : DB를 통해서 실제 엔티티 객체 조회
  2. em.getReference() : DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회(DB에 쿼리가 안나감)

프록시란?

  • 실제 클래스를 상속 받아서 만들어져서 실제 클래스와 겉 모양이 같다. 따라서 이론상으로 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
  • 프록시는 가짜 객체로 실제 객체의 참조(target)을 보관한다.
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

프록시 객체의 초기화 과정

member에서 em.getReference()로 프록시 객체를 가지고 왔다.

  1. member.getName()을 호출하면 MemberProxy의 getName()을 보는데 Member target에 값이 없다면?
  2. 그러면 JPA가 영속성 컨텍스트에 진짜 Member 객체를 가져오라는 요청을 한다.
  3. 영속성 컨텍스트는 DB를 조회를 하고,
  4. 실제 Entity를 생성한다.
  5. 그리고 MemberProxy에 생성된 실제 Member 엔티티를 연결을 해준다.

그러면 결론적으로 Client가 getName()을 하면 target의 진짜 getName()을 통해서 Member가 있는 getName()이 반환이 된다.

프록시의 특징

  • 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
  • 위의 초기화 과정에서도 볼 수 있듯이, 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는게 아니라 초기화하면  프록시 객체를 통해서 실제 엔티티를 참조하는 것이다.
  • 프록시 객체는 원본 엔티티를 상속받기 때문에 타입 체크할 때, == 비교 대신 instance of를 사용해야 한다.(비즈니스 로직을 사용할 때, 뭐가 넘어올지 확실하지 않기 때문) 또한 JPA에서 같은 인스턴스라는 == 비교에 대해서 같은 영속성 컨텍스트(트랜잭션 레벨) 안에서 조회하면 항상 같다고 나와야 한다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있다면, em.getReference()를 호출해도 실제 엔티티가 반환된다.
  • 준영속 상태(em.detach(member) 또는 em.close())일 때 프록시 초기화하면 문제 발생 → org.hibernate.LazyInitializationException

프록시 확인 방법

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

Member refMember = em.getReference(Member.class, member1.getId());  // Proxy
System.out.println("refMember = " + refMember.getClass());  // refMember = class hellojpa.Member$HibernateProxy$LSGI8cGq
refMember.getUsername();
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember));  // isLoaded = true

프록시 클래스 확인 방법 : entity.getClass().getName() 출력 → 그냥 찍어봐야 됨

Member refMember = em.getReference(Member.class, member1.getId());  // Proxy
System.out.println("refMember = " + refMember.getClass());  // refMember = class hellojpa.Member$HibernateProxy$LSGI8cGq

프록시 강제 초기화

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

Hibernate.initialize(refMember);  // 강제 초기화

지연 로딩과 즉시 로딩

지연 로딩 : LAZY

지연 로딩 내부 메커니즘

프록시 객체로 실제 엔티티 객체를 참조하는 것으로 내부 메커니즘을 보면 member1을 로딩할 때, team 엔티티 인스턴스는 지연 로딩으로 되어있으므로 프록시로 가져온다.

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

  1. em.find()로 member 객체를 가지고 왔을 대, Member와 Team이 LAZY 지연 로딩으로 세팅이 되어있으면 가짜 프록시 객체를 놓는다.
  2. 그 다음에 이 member에서 Team을 가져와서 team의 어떤 값을 실제 사용하는 시점에 DB 초기화가 되면서 이때 쿼리가 나간다.
@ManyToOne(fetch = FetchType.LAZY)  // 지연 로딩
@JoinColumn(name = "team_id")
private Team team;

즉시 로딩 : EAGER

즉시 로딩 내부 메커니즘

연관관계에 있는 객체까지 바로 조회하는 것으로 member1을 로딩을 할 때, 즉시로딩 EAGER로 세팅이 되어있으면 실제 team1 엔티티까지 조인해서 같이 가져온다.

프록시와 즉시 로딩 주의

  • 실무에서는 즉시 로딩을 사용하면 안된다. 가급적 지연 로딩만 사용하자. → 그냥 무조건 지연 로딩만 사용 해라!!
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. (테이블이 많이 엮여있을 때 어느 하나를 조회한다면 연관된 모든 테이블을 모두 조회하기 때문)
  • JPQL에서 N+1 문제를 일으킨다. (JPQL에서 SQL로 번역이 되면서 쿼리가 한 번 나가고, FetchType.EAGER로 설정되어 있으면 이거에 대한 쿼리가 한 번 더 나간다. ;; 이건 확실하게 이해한지는 모르겠어요...) (N+1 에서 1은 JPQL의 최초 쿼리를 의미하고, N은 이로 인한 추가 쿼리 N개가 나간다고 함을 의미한다.)
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩(EAGER)이므로 반드시 지연 로딩(LAZY)으로 설정해야 한다.
  • @OneToMany, @ManyToMany는 기본이 지연 로딩(LAZY)이다.

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

영속성 전이: CASCADE

: 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.

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

영속성 전이: 저장

CASCADE를 사용 가능 전제 2가지

  • Life Cycle이 같을 때
  • 단일 소유자일 때(연관관계가 하나일 때)

CASCADE 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH
  • DETACH
@Entity
@Getter @Setter
public class Parent {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)  // parent를 persist() 할 때, ChildList에 있는 것들을 모두 persist() 한다.
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }
}

@Entity
@Getter @Setter
public class Child {

    @Id
    @GeneratedValue
    private Long id;
    private String name;

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

// 실행 코드
Child child1 = new Child();
Child child2 = new Child();

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

em.persist(parent);  // child1과 child2가 모두 자동으로 생성

tx.commit();

고아 객체 - 제거

: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것으로 orphanRemoval = true를 사용한다.

참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능으로 @OneToOne, @OneToMany만 가능하다.

단, 참조하는 곳이 하나이고, 특정 엔티티가 개인 소유할 때 사용할 수 있다.

참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
@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<>();  // 이 컬렉션에서 빠진 것은 삭제가 됨

    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }
}

// 실행 코드

/* ... 이전과 동일 ... */

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

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);

tx.commit();

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

CascadeType.ALL(영속성 전이) + orphanRemoval=true(고아 객체)를 함께 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다.

이 방식은 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.

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

// 실행 코드
Parent parent = new Parent();
Child child1 = new Child();
parent.addChild(child); // 연관관계 매핑

// 부모만 등록,제거하면 자식은 자동으로 등록,제거됨
em.persist(parent);
em.remove(parent); // 전체 자식들 제거
parent.getChildList().remove(index:0); // 부모를 통해서 하나의 자식만 제거할 때

[출처]

https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 8장을 듣고 정리한 내용입니다.

728x90

'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글

[JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법  (0) 2024.06.23
[JPA 기본편] 9. 값 타입  (0) 2024.06.21
[JPA 기본편] 7. 고급 매핑  (0) 2024.05.20
[JPA 기본편] 6. 다양한 연관관계 매핑  (0) 2024.05.19
[JPA 기본편] 5. 연관관계 매핑 기초  (0) 2024.05.18
  1. 프록시란?
  2. 프록시 객체의 초기화 과정
  3. 프록시의 특징
  4. 프록시 확인 방법
  5. 지연 로딩과 즉시 로딩
  6. 지연 로딩 : LAZY
  7. 즉시 로딩 : EAGER
  8. 영속성 전이(CASCADE)와 고아 객체
  9. 영속성 전이: CASCADE
  10. 고아 객체 - 제거
  11. 영속성 전이 + 고아 객체, 생명 주기
'Spring/자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
  • [JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법
  • [JPA 기본편] 9. 값 타입
  • [JPA 기본편] 7. 고급 매핑
  • [JPA 기본편] 6. 다양한 연관관계 매핑
kyung.Kh
kyung.Kh
Dev..studynotekyung.Kh 님의 블로그입니다.
kyung.Kh
Dev..studynote
kyung.Kh
전체
오늘
어제
05-25 13:33
  • 분류 전체보기 (68)
    • Algorithm PS (29)
      • Baekjoon Online Judge (29)
      • Programmers (0)
    • Computer Science (4)
      • Databse (0)
      • Operating System (0)
      • Computer Network (0)
      • Computer Architecture (0)
      • Algorithm (4)
    • Spring (29)
      • Spring Boot (1)
      • 스프링 핵심 원리 - 기본편(인프런 김영한) (7)
      • Java (1)
      • 자바 ORM 표준 JPA 프로그래밍 (20)
    • Project (2)
      • 문제 & 해결 (2)
    • Book (3)
      • 객체지향의 사실과 오해 (3)
    • 우하한테크코스 (1)
      • precourse (1)

최근 글

인기 글

블로그 메뉴

    태그

    • 재귀
    • Spring
    • 스프링부트
    • 그리디
    • 스프링 기본편
    • 스프링
    • 알고리즘
    • 객체지향
    • 해시를 사용한 집합과 맵
    • DP
    • 백준
    • Union-Find
    • 인프런
    • 구현
    • 스프링 김영한
    • BFS
    • JPA
    • springboot
    • Graph
    • dfs
    hELLO · Designed By 정상우.v4.2.2
    kyung.Kh
    [JPA 기본편] 8. 프록시와 연관관계 관리
    상단으로

    티스토리툴바

    단축키

    내 블로그

    내 블로그 - 관리자 홈 전환
    Q
    Q
    새 글 쓰기
    W
    W

    블로그 게시글

    글 수정 (권한 있는 경우)
    E
    E
    댓글 영역으로 이동
    C
    C

    모든 영역

    이 페이지의 URL 복사
    S
    S
    맨 위로 이동
    T
    T
    티스토리 홈 이동
    H
    H
    단축키 안내
    Shift + /
    ⇧ + /

    * 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.