[JPA] Proxy

2023. 7. 18. 16:35·Backend/Spring Boot & JPA
목차
  1. 1. 프록시
  2. 2. 즉시 로딩과 지연 로딩
  3. 3. 지연 로딩 활용
  4. 4. 영속성 전이: CASCADE
  5. 5. 고아 객체(Orphan Object)
  6. 6. 영속성 전이 + 고아 객체, 생명주기
이 포스트는 김영한님의 "자바 ORM 표준 JPA 프로그래밍"을 참조하였음을 알립니다.
📕 목차

1. 프록시
2. 즉시 로딩과 지연 로딩
3. 지연 로딩 활용
4. 영속성 전이: CASCADE
5. 고아 객체(Orphan Object)
6. 영속성 전이 + 고아 객체, 생명주기
👀 Summary

• 객체 그래프 탐색을 위해 Proxy를 사용한다.
• 객체 조회 시에는 즉시 로딩과 지연 로딩이 있다.
• 객체 저장&삭제 시, 연관 객체를 함께 저장&삭제하려면 영속성 전이를 사용한다.
• 부모 객체와 연관관계가 끊어진 자식을 자동으로 삭제하려면 고아 객체 제거 기능을 사용한다.

1. 프록시

 

연관된 객체를 처음부터 전부 다 가져오지 않고, 실제 사용 시점에 데이터베이스에서 조회하는 방법이다.

JPA 표준 명세는 지연 로딩 구현 방법을 JPA 구현체(여기선 하이버네이트)에 위임했다.

하이퍼네이트는 지연 로딩 지원 방법으로 프록시와 바이트 코드를 수정하는 두 가지 방법을 제공한다.

 

📌 BASE
Member member = em.find(Member.class, "member1"); // 영속성 컨텍스트에 없으면 DB 조회

Member member = em.getReference(Member.class, "member1"); // 실제 사용 시점까지 조회 미룸

  • getReference() 메서드는 Entity를 실제 사용하는 시점까지 DB 조회를 미루는 대신 프록시 객체를 반환한다.
더보기

진짜로 Proxy 객체를 가져오나 궁금해서 직접 해봤다.

 

@DataJpaTest
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class FooTest {
    @Autowired
    TestEntityManager testEntityManager;

    EntityManager em;

    @BeforeEach
    void init() {
        em = testEntityManager.getEntityManager();
    }

    @Test
    @DisplayName("프록시 테스트")
    void testProxy() {
        Foo foo = Foo.builder()
                .username("foo")
                .build();
        testEntityManager.persist(foo);
        em.flush();
        em.clear();

        Foo proxy = em.getReference(Foo.class, foo.getId());
        System.out.println(proxy.getClass());

    }
}

HibernateProxy를 들고 오는 거 확인~

 

🟡 Proxy 특징

  • Proxy 클래스는 실제 클래스를 상속 받아서 만들어진다.
    • 사용자 입장에서는 진짜 객체인지, Proxy 객체인지 구분하지 않고 사용하면 된다.
  • Proxy 객체는 실제 객체에 대한 참조(target)를 보관한다.
    • Proxy 객체의 메서드를 호출하면, 실제 객체의 메소드를 호출하여 역할을 위임한다.
    • Proxy 객체를 초기화한다고 해서 실제 Entity로 바뀌는 것이 아니므로 타입 체크에 주의해야 한다.
  • 영속성 Context에 이미 실제 Entity가 있다면 Proxy가 아닌 실제 Entity를 반환한다.

 

🟡 Proxy 객체 초기화

Foo foo = em.getReference(Foo.class, 1);
foo.getName();
  1. Proxy 객체에 foo.getName()을 호출해서 실제 데이터를 조회한다.
  2. Proxy 객체는 영속성 Context에 실제 Entity가 없으면 생성을 요청한다. (초기화)
  3. 영속성 Context는 DB를 조회하여 실제 Entity 객체를 생성한다.
  4. Proxy 객체는 생성된 실제 Entity 객체의 참조를 Foo target 멤버 변수에 보관한다.
  5. Proxy 객체는 실제 Entity 객체의 getName()을 호출하여 결과를 반환한다.

여기서 주의할 점은 초기화는 영속성 Context의 도움을 받아야 가능하다는 점이다.

영속성 Context의 도움을 받을 수 없는 준영속 상태의 Proxy를 초기화하면 org.hibernate.LazyInitializationException 예외를 발생시킨다.

 

📌 식별자
Member member = em.find(Member.class, 1);
Team team = em.getReference(Team.class, 1); // 식별자 보관
team.getId(); // 초기화되지 않음

member.setTeam(team);
  • Proxy는 식별자(pk) 값을 파라미터로 전달받아 보관하고 있다.
  • 따라서, getId()를 호출해도 Proxy를 초기화하지 않고 보관한 식별자 값을 돌려준다.
  • Entity 접근 방식이 @Access(AccessType.PROPERTY)라면 연관관계 설정할 때 유용하게 사용할 수 있다.

 

📌 프록시 확인
Foo proxy = em.getReference(Foo.class, foo.getId());

boolean isLoad = em.getEntityManagerFactory()
    .getPersistenceUnitUtil().isLoaded(proxy);

System.out.println("isLoad = " + isLoad);
System.out.println("fooProxy = " + proxy.getClass().getName());

  • JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메서드로 Proxy instance 초기화 여부를 알 수 있다.
    • true : 초기화 되었거나 proxy가 아닌 인스턴스
    • false : 초기화되지 않은 proxy
  • 조회한 Entity가 진짜 Entity인지 확인해보고 싶다면 getClass()를 호출해보면 된다.

 


2. 즉시 로딩과 지연 로딩

 

📌 즉시 로딩 (FetchType.EAGER)
@Entity
public class Foo {
    ...
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}
Foo foo1 = em.find(Foo.class, foo.getId());
Team team1 = foo1.getTeam();

  • @ManyToOne의 fetch 속성을 FetchType.EAGER로 지정한다.
  • Foo 객체 정보 조회 시, 연관관계에 있는 Team 객체 정보까지 한 번에 조회한다.
  • 즉시 로딩 최적화를 위해 가능하다면 JOIN 쿼리를 사용하여 한 번의 쿼리만 실행한다.

 

✒️ NULL 제약 조건과 JPA 조인 전략

조인 전략 중에서 가장 성능이 좋은 것은 내부 조인(INNER JOIN)이다.
하지만, JPA는 team에 소속되지 않은 foo가 있을 수 있으므로 외부 조인(LEFT OUTER JOIN)을 사용한다.

이 경우 2가지 방법으로 대처할 수 있다.
1. @JoinColumn(name = "TEAM_ID", nullable = false)
2. @ManyToOne(fetch = FetchType.EAGER, optional = false)

둘 중 한 가지 속성을 정의해주면, JPA는 내부 조인 쿼리로 변경해서 수행한다.

 

📌 지연 로딩 (FetchType.LAZY)
@Entity
public class Foo {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}
Foo foo1 = em.find(Foo.class, foo.getId());
Team team1 = foo1.getTeam();
System.out.println("team1 = " + team1.getClass());

  • @ManyToOne의 fetch 속성을 FetchType.LAZY로 지정한다.
  • foo 객체 조회 시 team 필드에 proxy 객체를 할당하여, 실제 사용할 때 database를 조회한다.

 

📌 Summary
  • 둘 중 알맞은 전략을 선택할 줄 알아야 한다.
  • 대부분 애플리케이션 로직에서 회원과 팀 엔티티를 같이 사용한다면 SQL 조인이 더 효율적일 것이다.

 


3. 지연 로딩 활용

 

📌 As-is

Client가 아니라 Member

🟡 모델 분석

  • 고객(Member) 
  • 팀(Team)
  • 주문내역(Order)
  • 상품정보(Product)

(기존 프로젝트에서 코드 치다가 Member 클래스가 겹쳐서 Client로 했었는데, 귀찮아서 프로젝트를 새로 파버렸다.)

 

🟡 애플리케이션 로직 분석 결과 (가정)

  • Member와 연관된 Team은 자주 함께 사용되어, EAGER 전략을 사용했다.
  • Member와 연관된 Order는 가끔 사용되어, LAZY 전략을 사용했다.
  • Order와 연관된 Product는 자주 함께 사용되어, EAGER 전략을 사용했다.

 

🟡 회원 Entity 코드

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String username;
    private Integer age;

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

    @OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
    private List<Order> orders = new ArrayList<>();
}
  • Member를 조회할 때, 연관관계가 정의된 Team도 함께 조회한다.
  • Member를 조회할 때, 연관관계가 정의된 Orders 결과를 proxy로 조회한다.

 

📌 Collection wrapper
Member member2 = em.find(Member.class, member1.getId());
Team team1 = member2.getTeam();
List<Order> orders = member2.getOrders();

System.out.println("team = " + team1.getClass().getName());
System.out.println("orders = " + orders.getClass().getName());
[output]

team = com.example.jpa.domain.Team
orders = org.hibernate.collection.spi.PersistentBag
  • 하이버네이트가 Entity를 영속화할 때, Entity 안의 Collection을 추적하고 관리할 목적으로 만드는 것
  • 원본 Collection을 하이버네이트가 제공하는 내장 컬렉션(PersistentBag)으로 변경한다.

 

🟡 org.hibernate.collection.spi.PersistentBag

/**
 * An unordered, unkeyed collection that can contain the same element
 * multiple times. The Java collections API, curiously, has no {@code Bag}.
 * Most developers seem to use {@code List}s to represent bag semantics,
 * so Hibernate follows this practice.
 *
 * @apiNote Incubating in terms of making this non-internal.  These contracts
 * will be getting cleaned up in following releases.
 *
 * @author Gavin King
 */
@Incubating
public class PersistentBag<E> extends AbstractPersistentCollection<E> implements List<E> {

	protected List<E> bag;

	/**
	 * The Collection provided to a PersistentBag constructor
	 */
	private Collection<E> providedCollection;
    ...
}
  • orders 같은 Collection을 Proxy 객체가 아닌, PersistentBag이 지연 로딩을 처리한다.
  • PersistentBag 또한 proxy 역할을 수행하므로 이하 Proxy로 통일.
  • PersistentBag이 초기화를 하는 시점
    • member.getOrders()를 호출해도 Collection은 초기화되지 않는다.
    • member.getOrders().get(0)처럼 실제 데이터 조회할 때 DB를 조회하여 초기화한다.

 

📌 JPA default fatch strategy
💡 모든 연관관계에 지연 로딩을 사용하고, 어느정도 개발 완료 단계에서 최적화용으로 즉시 로딩을 적용하라.
  • @ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)
  • @OneToMany, @ManyToMay : 지연 로딩(FetchType.LAZY)

연관된 Entity가 하나면 즉시 로딩, Collection이면 지연 로딩을 사용한다.

 

📌 컬렉션에 FetchType.EAGER 주의 사항
  • 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않는다.
    • JPA는 조회된 결과를 메모리에서 필터링해서 반환한다.
    • N,M 일대다 관계에서 너무 많은 데이터를 반환하거나 성능 저하를 유발할 수 있다.
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용하라.
    • 외래키에 not null 제약이 있다면 항상 내부 조인을 사용해도 되지만, 아니라면 사용하지 마라.
  • @ManyToOne, @OneToOne (기본 EAGER)
    • (optional = false): 내부 조인
    • (optional = true): 외부 조인
  • @OneToMany, @ManyToMany (기본 LAZY)
    • (optional = false): 외부 조인
    • (optional = true): 외부 조인

 


4. 영속성 전이: CASCADE

 

💡 JPA에서 Entity를 저장할 때 연관된 모든 Entity는 영속 상태여야 한다.

JPA는 CASCADE 옵션으로 영속성 전이(transitive persistence) 기능을 제공한다.

쉽게 말해 부모 Entity 저장 시, 자식 Entity도 함께 저장할 수 있다.

만약, 영속성 전이가 없다면 부모 1, 자식 2를 저장하기 위해선 다음과 같은 코드가 필요할 것이다.

@Test
@DisplayName("Transitive Persistence 테스트")
void testTransitivePersistence() {
    /* init */
    Parent parent = new Parent();
    em.persist(parent);

    Child child1 = new Child();
    child1.setParent(parent);
    em.persist(child1);

    Child child2 = new Child();
    child2.setParent(parent);
    em.persist(child2);
}

 

📌 영속성 전이: 저장

@Entity
@Getter
@NoArgsConstructor
public class Parent {
    ...
    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
}
@Test
@DisplayName("Transitive Persistence 테스트")
void testTransitivePersistence() {
    /* init */
    Parent parent = new Parent();

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

    child1.setParent(parent);
    child2.setParent(parent);

    em.persist(parent);
}

  • CascadeType.PERSIST : 부모를 영속화할 때, 연관 자식들도 함께 영속화
  • 영속성 전이는 편리함을 제공하는 기능이지, 연관관계 매핑과는 관련이 없다.

 

📌 영속성 전이: 삭제
@OneToMany(mappedBy = "parent", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<Child> children = new ArrayList<>();
em.remove(parent);
  • CascadeType.REMOVE : 부모를 삭제할 때, 연관 자식들도 함께 삭제
  • 부모 Entity만 삭제하면 연관된 자식 Entity도 함께 삭제할 수 있다.
    • DELETE SQL을 3번 실행한다.
    • 외래 키 제약 조건을 고려하여 자식을 먼저 삭제하고 부모를 삭제한다.
  • REMOVE 설정이 되어 있지 않으면 부모 Entity만 사라지지만, 외래 키 제약조건으로 인해 DBMS에서 외래키 무결성 예외가 발생한다.

 

📌 CASCADE 종류
/**
 * Defines the set of cascadable operations that are propagated 
 * to the associated entity.
 * The value <code>cascade=ALL</code> is equivalent to 
 * <code>cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}</code>.
 *
 * @since 1.0
 */
public enum CascadeType { 

    /** Cascade all operations */
    ALL, 

    /** Cascade persist operation */
    PERSIST, 

    /** Cascade merge operation */
    MERGE, 

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /** Cascade detach operation */   
    DETACH
}

 


5. 고아 객체(Orphan Object)

 

JPA는 부모 Entity Collection에서 자식 Entity 참조만 제거하면, 자식 Entity가 자동으로 삭제되는 기능을 지원한다.

 

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

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
}
@Test
@DisplayName("Orphan Removal 테스트")
@Transactional
void testOrphanRemoval() {
    /* init */
    ...

    /* delete */
    System.out.println("===== delete =====");
    Parent parent1 = em.find(Parent.class, parent.getId());
    System.out.println("removeChildren : " +  parent1.getChildren().remove(0));
}

@AfterEach
void tearDown() {
    Parent parent = em.find(Parent.class, 1);
    System.out.println("check DB : " +  parent.getChildren());

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

  • orphanRemoval = true : Collection에서 Entity 제거 시, DB의 데이터도 삭제
  • 참조가 제거된 Entity는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제한다.
  • 고아 객체 제거 기능은 영속성 컨텍스트 flush 단계에서 적용된다.
  • 부모를 제거하면 자식도 같이 제거되므로 CascadeType.REMOVE와 같다.

 


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

 

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
  • 자식을 저장하려면 부모에 등록만 하면 된다. (CASCADE)
  • 자식을 삭제하려면 부모에서 제거하면 된다. (orphanRemoval)

즉, 두 옵션을 모두 제공하면 부모 Entity에서 자식의 Life-Cycle을 관리할 수 있다.

 

저작자표시 비영리 (새창열림)
  1. 1. 프록시
  2. 2. 즉시 로딩과 지연 로딩
  3. 3. 지연 로딩 활용
  4. 4. 영속성 전이: CASCADE
  5. 5. 고아 객체(Orphan Object)
  6. 6. 영속성 전이 + 고아 객체, 생명주기
'Backend/Spring Boot & JPA' 카테고리의 다른 글
  • [Spring Boot] Service Layer 분리에 대하여
  • [JPA] Value Type
  • [JPA] Advanced Mapping
  • [JPA] Association Mapping
나죽못고나강뿐
나죽못고나강뿐
싱클레어, 대부분의 사람들이 가는 길은 쉽고, 우리가 가는 길은 어려워요. 우리 함께 이 길을 가봅시다.
  • 나죽못고나강뿐
    코드를 찢다
    나죽못고나강뿐
  • 전체
    오늘
    어제
    • 분류 전체보기 (451) N
      • Computer Science (59)
        • Git & Github (4)
        • Network (17)
        • Computer Structure & OS (13)
        • Software Engineering (5)
        • Database (9)
        • Security (4)
        • Concept (7)
      • Frontend (21)
        • React (13)
        • Android (4)
        • iOS (4)
      • Backend (75) N
        • Spring Boot & JPA (48) N
        • Django REST Framework (14)
        • MySQL (8)
        • Nginx (1)
        • FastAPI (4)
      • DevOps (24)
        • Docker & Kubernetes (11)
        • Naver Cloud Platform (1)
        • AWS (2)
        • Linux (6)
        • Jenkins (0)
        • GoCD (3)
      • Coding Test (112)
        • Solution (104)
        • Algorithm (7)
        • Data structure (0)
      • Reference (134)
        • Effective-Java (90)
        • Pragmatic Programmer (0)
        • CleanCode (11)
        • Clean Architecture (2)
        • Test-Driven Development (4)
        • Relational Data Modeling No.. (0)
        • Microservice Architecture (2)
        • 알고리즘 문제 해결 전략 (9)
        • Modern Java in Action (0)
        • Spring in Action (0)
        • DDD start (0)
        • Design Pattern (6)
        • 대규모 시스템 설계 (6)
        • JVM 밑바닥까지 파헤치기 (4)
      • Service Planning (2)
      • Side Project (5)
      • AI (0)
      • MATLAB & Math Concept & Pro.. (1)
      • Review (15)
      • Interview (1)
      • IT News (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃
  • 공지사항

    • 취업 전 계획 재조정
    • 취업 전까지 공부 계획
    • 앞으로의 일정에 대하여..
    • 22년 동계 방학 기간 포스팅 일정
    • 중간고사 기간 이후 포스팅 계획 (10.27~)
  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
나죽못고나강뿐
[JPA] Proxy

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

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

블로그 게시글

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

모든 영역

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

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