본문 바로가기
TIL

[231129] 프록시와 영속성, 스프링 데이터 JPA

by 진진리 2023. 11. 29.
728x90

프록시와 연관관계

  • 지연 로딩: 연관된 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법
  • 프록시: 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이를 프록시 객체라고 함
    • Member member = em.find(Member.class, "member1");
      • 이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하지 않아도 데이터베이스를 조회(영속성 컨텍스트에 엔티티가 없는 경우)
    • Member member = em.getReference(Member.class, "member1");
      • JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않음
      • 대신 데이터베이스 접근을 위임한 프록시 객체를 반환
  • 프록시의 특징
    • 실제 클래스를 상속받아서 만들어짐
    • 실제 객체에 대한 참조(target)을 보관
    • 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출
  • 프록시 객체의 초기화
    • 프록시 객체는 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성
    • 처음 사용될 때 한 번만 초기화됨
    • 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있음
    • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 프록시가 아닌 실제 엔티티를 반환
    • 초기화는 영속성 컨텍스트의 도움을 받아야 가능. 준영속 상태의 프록시를 초기화하면 문제가 발생.
      • 하이버네이트는 org.hibernate.LazyInitializationException 예외를 발생시킴
  • 추천하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것 -> 어느정도 완성한 후 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화 !

 

영속성 전이: CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용
  • JPA는 CASCADE 옵션으로 영속성 전이를 제공 - 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장 가능
  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
    1. 영속성 전이: 저장
      • Parent.class에서 cascade = CascadeType.PERSIST 설정
        • child1.setParent(parent);
        • child2.setParent(parent);
        • em.persist(parent);  //부모 저장, 연관된 자식들 저장
      • 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공
      • 연관관계를 매핑하는 것과 관련 없음
    2. 영속성 전이: 삭제
      • 부모와 지식 엔티티를 모두 제거하려면 하나씩 제거해야 함
      • cascade = CascadeType.REMOVE 설정
        • Parent findParent = em.find(Parent.class, 1L);
        • em.remove(findParent);
      • 자식을 먼저 삭제하고 부모를 삭제
  • 고아 객체 제거 : JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공
    • 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티틱가 자동으로 삭제됨
    • Parent.class에서 orphanRemoval = true 설정
      • Parent parent1 = em.find(Parent.class, id);
      • parent1.getChildren().remove(0); // 자식 엔티티를 컬렉션에서 제거
      • 제거된 자식 엔티티는 자동으로 삭제됨
    • 이 기능은 참조하는 곳이 하나일 때만 사용해야 함
    • 따라서 orphanRemoval은 @OneToOne, @OneToMany에만 사용할 수 있다.
    • 고아 객체 제거는 부모를 제거하면 자식이 고아가 되므로 자식도 함께 제거된다. 이것은 CascadeType.REMOVE를 설정한 것과 같다.
  • 영속성 전이 + 고아 객체
    • CascadeType.ALL + orphanRemoval = true
    • 부모 엔티티를 통해서 자식의 생명주기를 관리
      • 자식을 저장하려면 부모에 등록만 하면 된다.
      • 자식을 삭제하려면 부모에서 제거하면 된다.

 

스프링 데이터 JPA

  • SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성
    • application.properties에 DB 정보를 전달해주면 이를 토대로 EntityManagerFactory가 생성
    • @PersistenceContext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용할 수 있음
@PersistenceContext
EntityManager em;
  • Spring의 트랜잭션
    • @Transactional 애너테이션을 클래스나 메서드에 추가하면 트랜잭션 개념을 적용할 수 있음
    • readOnly = true 옵션: 트랜잭션에서 데이터를 읽기만 할 때 사용. 읽기 작업에 대한 최적화를 수행.
  • JPA를 사용하여 DB에 데이터를 저장, 수정, 삭제 하려면 트랜잭션 적용이 반드시 필요
  • 트랜잭션 전파
    • 기본 옵션은 REQUIRED: 부모 메서드에 트랜잭션이 존재하면 자식 매서드의 트랜잭션은 부모 트랜잭션에 합류
  • 스프링 컨테이너의 기본 전략
    • 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같다.
    • 보통 서비스 계층에 @Transactional 어노테이션을 선언해서 트랜잭션을 시작
      • 트랜잭션이 같으면 엔티티 매니저가 달라도 같은 영속성 컨텍스트를 사용한다.
      • 트랜잭션이 다르면 엔티티 매니저가 같아도 다른 영속성 컨텍스트를 사용한다.
    • 컨트롤러 로직(준영속 상태)에서 지연 로딩 시 예외 발생
      1. 뷰가 필요한 엔티티를 미리 로딩해두는 방법
        1. 글로벌 페치 전략 수정: 즉시 로딩으로 변경
          • 단점: 사용하지 않는 엔티티를 로딩, N+1 문제 발생
        2. JPQL 패치 조인: N+1 문제 해결
          • 단점: 프리젠테이션 계층이 데이터 접근 계층을 침범
        3. 강제로 초기화
          • order.getMember().getName(); // 프록시 객체를 강제로 초기화
          • 하이버네이트 사용: org.hibernate.Hibernate.initialize(order.getMember());  // 프록시 초기화
          • 프록시 초기화 역할을 서비스 계층이 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 함 : 프리젠테이션 계층이 서비스 계층을 침범하는 상황
          • 서비스 계층에서 프리젠테이션 계층을 위한 프록시 초기화 역할을을 분리해야 함 -> FACADE 계층 추가
      2. OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
        • 영속성 컨텍스트를 뷰까지 살아있게 열어둠으로써 지연 로딩을 뷰에서도 사용 가능하게 함
        • 스프링 프레임워크가 제공하는 OSIV 라이브러리
        • 동작 원리
          1. 클라이언트의 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성
          2. 서비스 계층에서 트랜잭션을 시작하면 앞에서 생성해둔 영속성 컨텍스트에 트랜잭션을 시작
          3. 서비스 계층이 끝나면 영속성 컨텍스트를 플러시한다. 이때 트랜잭션만 종료하고 영속성 컨텍스트를 살려둔다.
          4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지
          5. 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료. 이때 플러시를 호출하지 않고 바로 종료.
        • 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.
        • 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. = 트랜잭션 없이 읽기