김영한의 ORM 표준 JPA 프로그래밍(기본편) - 프록시, 지연로딩, Cascade, 고아 객체

2025. 2. 7. 00:26·김영한의 ORM 표준 JPA 프로그래밍(기본편)

Question

  • em.find()와 em.getReference()의 동작 차이는?
  • getReference가 실제 동작하는 순서를 설명하시오
  • 다음 코드의 결과값은?
    Member refMember = em.getReference(Member.class, member1.getId());
    Member findMember = em.find(Member.class, member.getId());
    System.out.println(refMember == findMember);
  • 다음 코드의 문제점은?
    em.persist(member1)
    em.flush();
    em.clear();
    Member refMember = em.getReference(Member.class, member1.getId());
    em.detach(refMember);
    refMember.getUsername();
  • @ManyToOne, @OneToOne의 Default FetchType은?
  • 즉시로딩의 문제점은?
  • Cascade를 사용할 때 주의점은?
  • 고아 객체를 사용할 때 주의점은?
  • Cascade + 고아 객체를 함께 사용할 때의 장점은?

 

em.find() 와 em.getReference()의 실제 동작 차이

  • em.find()의 경우 호출시 영속성 컨텍스트에 데이터가 없을 경우 바로 DB에 쿼리를 날림
  • em.getReference()의 경우 호출시 영속성 컨텍스트에 데이터가 없더라도 DB에 쿼리를 날리지 않고 나중에 해당 데이터가 실제로 필요할 때 DB에 쿼리를 날림
  • getReference는 프록시 기법을 활용해서 조회를 뒤로 미룰 수 있음

 

프록시

  • getReference와 같은 함수를 호출하면 실제 클래스를 상속받는 프록시 클래스가 생성됨
  • 실제 클래스와 겉 모습이 같고, 사용자 입장에서는 구분이 되지 않음
  • 실제 동작 순서
    • 프록시 객체의 getName()을 호출
    • 프록시 객체는 PersistenceContext를 통해 엔티티가 초기화 되었는지 확인
    • 이를 위해, 프록시는 영속성 컨텍스트를 통해 해당 엔티티의 데이터가 이미 존재하는지 확인함
    • 데이터가 영속성 컨텍스트에 존재하지 않는 경우 DB를 조회해 데이터를 조회해오고, Member에게 getName()을 호출해 데이터를 조회해옴
    • 데이터가 영속성 컨텍스트에 존재하는 경우 Member에게 바로 getName()을 호출해 데이터를 조회해옴
    • 초기화 이후에 프록시 객체는 바로 Member 객체에게 함수를 호출함

 

프록시 특징

  • 처음 사용할 때 한 번만 초기화
  • 초기화를 했다고 실제 엔티티를 바뀌는 것은 아니고, 프록시를 통해 실제 엔티티에 접근이 가능한 것
  • 프록시 객체는 원본 엔티티를 상속받은 것이라, 실제로는 다르므로 타입 체크시 == 비교가 아닌 instance of를 사용해야 함
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환함
    • JPA는 같은 것을 조회한 경우 무조건 == 연산이 true로 나오도록 함
    • 예를 들어 아래 코드는 true를 반환함
      Member refMember = em.getReference(Member.class, member1.getId());
      Member findMember = em.find(Member.class, member.getId());
      System.out.println(refMember == findMember);
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생
    • getReference 이후 detach, clear, close 처럼 해당 객체를 준영속 상태로 만들어 버리고 나서 초기화하면 문제가 발생
    • 그 이유는, 준영속 상태로 전환된 경우, 엔티티 매니저와의 연결이 끊겨 영속성 컨텍스트를 사용할 수 없는 상태가 되기 때문
    • 반면에, em.find는 항상 새로운 영속 상태의 엔티티를 반환하여 이러한 문제를 회피 가능
    • 예를 들어, 아래 코드는 could not initialize proxy 오류가 발생
      em.persist(member1)
      em.flush();
      em.clear();
      Member refMember = em.getReference(Member.class, member1.getId());
      em.detach(refMember);
      refMember.getUsername();
    • 이는 이런 단적인 예보다도, 트랜잭션이 끝나면 영속성 컨텍스트가 초기화되면서 문제가 발생할 수 있음
  • 실제로 getReference는 많이 사용이 되지 않지만, 프록시 개념 자체가 추후 지연 로딩 개념을 이해하는데 매우 중요

 

프록시 관련 함수

  • 프록시 인스턴스의 초기화 여부 확인: PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법: entity.getClass().getName()
  • 프록시 강제 초기화: org.hibernate.Hibernate.initialize(entity)
  • JPA 표준에는 강제 초기화가 없어 member.getName()으로 강제 호출

 

 

지연 로딩

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
  • Member를 조회할 때 Member 안에 있는 Team도 함께 조회를 해야 하는지에 대한 의문
  • 실제로 사용할 때 조회를 하고, 사용을 하지 않을 수도 있으니 미리 조회는 하지 않는게 지연 로딩
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

 

즉시 로딩

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

 

  • Member와 Team을 자주 함께 사용한다면 항상 부르고 싶을 수 있음.
  • 이 때는 EAGER를 통해 즉시 로딩 활용할 수 있음.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩

 

즉시 로딩의 주의점

  • 실무에서는 즉시로딩은 거의 사용하지 않음
  • 상상하지 못한 많은 쿼리가 나갈 수 있음
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킴
    • 예를 들어 select m from Member m; 이런 JPQL은 모든 멤버를 조회하고 각 멤버가 또 팀을 조회하므로 엄청나게 많은 조회 쿼리가 나가게 됨
  • 때때로 정말 즉시로딩을 하고 싶다면 JPQL에 fetch를 통해 즉시 로딩 할수있게 하는 방법을 활용하거나 그래프 기능 사용
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 LAZY 세팅을 꼭 해줘야 함

 

 

영속성 전이: CASCADE

@Entity
public class Parent {
    @Id
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
    ...
}
  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔팉이도 함께 영속 상태로 만들고 싶은 경우
  • 예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
  • 위 코드처럼 선언하면, Parent에 여러 children을 입력한 후 parent만 저장해도 children이 모두 저장됨
  • 편리함을 제공하지만 생각지 못하게 전파가 될 수 있으므로 주의해서 사용해야 함
    • 일반적으로 사용을 한다면 일대다 관계에서 사용함
    • "다"들이 참조하는게 "일" 밖에 없을 경우에만 사용해야함
    • 다른데서 관리하는 곳이 있으면 복잡성이 증가해 예상치 못한 문제가 발생할 수 있기 때문.
  • CASCADE 종류
    • ALL: 모두 적용
    • PERSIST: 영속
    • REMOVE: 삭제
    • MERGE: 병합
    • REFRESH: REFRESH
    • DETACH: DETACH

 

고아 객체

@Entity
public class Parent {
    @Id
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
    ...
}
  • orphanRemoval = true
  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
  • 부모를 삭제한 경우에도 자식도 삭제됨(CascadeType.REMOVE와 동일)
  • 이 또한 CASCADE처럼 참조하는 곳이 하나일 경우에만 사용해야 함
  • 특정 엔티티만이 해당 엔티티를 소유한 경우
  • @OneToOne, @OneToMany만 가능

 

CASCADE + 고아 객체, 생명주기

  • CascadeType.ALL + orphanRemoval=true 를 함께 사용하기
  • 부모 엔티티를 통해 자식의 생명 주기를 관리할 수 있음
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용

'김영한의 ORM 표준 JPA 프로그래밍(기본편)' 카테고리의 다른 글

김영한의 ORM 표준 JPA 프로그래밍(기본편) - JPQL  (0) 2025.02.08
김영한의 ORM 표준 JPA 프로그래밍(기본편) - 값타입, 임베디드 타입, 값 타입 컬렉션  (3) 2025.02.08
김영한의 ORM 표준 JPA 프로그래밍(기본편) - 상속관계 맵핑과 - @MappedSuperclass  (1) 2025.02.06
김영한의 ORM 표준 JPA 프로그래밍(기본편) - 다대일, 일대다, 일대일, 다대다  (1) 2025.02.05
김영한의 ORM 표준 JPA 프로그래밍(기본편) - 연관관계 단방향 맵핑, 양방향 맵핑  (1) 2025.02.05
'김영한의 ORM 표준 JPA 프로그래밍(기본편)' 카테고리의 다른 글
  • 김영한의 ORM 표준 JPA 프로그래밍(기본편) - JPQL
  • 김영한의 ORM 표준 JPA 프로그래밍(기본편) - 값타입, 임베디드 타입, 값 타입 컬렉션
  • 김영한의 ORM 표준 JPA 프로그래밍(기본편) - 상속관계 맵핑과 - @MappedSuperclass
  • 김영한의 ORM 표준 JPA 프로그래밍(기본편) - 다대일, 일대다, 일대일, 다대다
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      requset scope
      WAS
      락
      JPQL
      Thread
      단방향 맵핑
      고급
      typequery
      gesingleresult
      @discriminatorvalue
      빈 후처리기
      @args
      페치 조인
      양방향 맵핑
      @within
      자바
      조회 성능 최적화
      프록시 팩토리
      hibernate5module
      @discriminatorcolumn
      jdk 동적 프록시
      jpq
      버퍼
      cglib
      Target
      프록시
      스레드
      log trace
      김영한
      reentarantlock
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 ORM 표준 JPA 프로그래밍(기본편) - 프록시, 지연로딩, Cascade, 고아 객체
    상단으로

    티스토리툴바