티스토리 뷰

Spring/JPA

프록시와 연관관계 관리

땅속 디그다 2022. 8. 23. 11:13

⭐ 프록시와 연관관계 관리

🚀 프록시

엔티티를 조화 할 때 연관된 엔티티가 항상 불러와지는 가에 대한 문제

🤔 프록시 그게 뭔데?

간단히 설명하자면 JAVA에서 프록시는 타겟의 기능을 확장하거나 타깃에 대한 접근을
제어하기 위한 목적으로 사용하는 클래스를 말한다.

프록시에 주요 기능

  1. 접근제어
    • 권한에 따른 접근 차단
    • 캐싱
    • 지연로딩
  2. 부가 기능 추가
    • 원래 서버가 제공하는 기능에 더 해서 부가 기능을 수행
      • ex) 요청 값이나, 응답 값을 중간에 변형
      • ex) 실행 시간을 측정해서 추가 로그를 남기기

후에 프록시에 대해서 더 자세히 포스팅 해보자

아무튼 JPA 또한 지연로딩과 캐싱, 그리고 Repeatable Read를 지원하기 위해 Entity 단에서
프록시를 사용한다.

JPA 에서 프록시 나오게 된 이유?

🤔 Member 와 Team 클래스가 있고 각각이 연관관계를 가지고 있다고 하자(N:1)

Member를 조회 할때 Team 클래스에 대한 참조를 항상 가지고 있어야 할까?

❗ 문제점 가지고 있지 않다면 Team이 필요한 시점에는 어떻게 동작해야 하는데??

Member 정보만 사용하는 곳에서 연관된 팀 엔티티까지 데이버테이스에서 조회하는 것은 효율
적이지 않다

JPA는 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법(지연 로딩) 을 제공 한다.

지연로딩을 사용하기 위해서는 "가짜객체" 즉 프록시 객체가 필요하다.

프록시 객체의 특징 (em.getReference())

  1. 프록시 객체는 실제 객체에 대한 참조(target)을 보관한다.
  2. 처음 사용할 때 한 번만 초기화 한다. (실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에)
    실제 엔티티 생성을 요청 = 초기화)
  3. 프록시 객체가 초기화 되면 프록시 객체를 통해 실제 엔티티에 접근이 가능하다.
  4. 프록시 객체는 원본 엔티티를 상속받은 클래스(프록시 객체)를 사용하므로 타입 비교시 == 오류가 발생
  5. 영속성 컨텍스트에 찾는 엔티티가 있으면 em.getReference() 를 호출해도 프록시가 아닌 실제 엔티티 반환
  6. 초기화는 영속성 컨텍스트의 도움을 받아야 가능하므로 준영속 상태일 때 프록시를 초기화 하면(target 주입)
    오류가 발생한다.

대략적인 흐름을 보기위해 이런식으로 프록시를 사용한다는 것을 보자.. (코드로!)

🖥 프록시를 사용하여 지연로딩 확인

public interface Subject {
    String doAction();
}

인터페이스를 가지고 있는 구현체는 target 과 proxy

public class Target implements Subject {

  @Override
  public String doAction() {
    System.out.println("데이터를 가져온다");
    sleep(1000);    // 데이터를 가져오는데에 걸리는 시간 1초
    return "data";
  }

  private void sleep(int millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Subject를 사용하는 Client Class를 확인하자

우리는 Client가 어떤 subject를 사용하는지 모르게 하는 것이 포인트이며,

public class Client {

  Subject subject;

  public Client(Subject subject) {
    this.subject = subject;
  }

  public void execute() {
    subject.doAction();
  }
}

프록시를 사용하지 않고 Test 진행

@Test
void test() {
    RealSubject realSubject = new RealSubject();
    Client client = new Client(realSubject);
    client.execute();
    client.execute();
    client.execute();
}

데이터를 가져온다고 가정한다면 윗 코드는 3초의 시간이 걸린다.

프록시를 통해 캐싱을 진행 해보자

 

🖥 캐싱을 하기 위한 프록시 클래스

public class Proxy implements Subject {

  private Subject subject;
  private String value; // 캐싱 할 value

  public Proxy(Subject subject) {
    this.subject = subject;
  }

  @Override
  public String doAction() {

    System.out.println("프록시 실행");

    if (value == null) {
      value = subject.doAction();
      // 데이터 조회값 캐싱
    }

    return value;
  }
}

프록시를 사용해서 데이터 접근 시간 줄임

@Test
  void test() {
    RealSubject realSubject = new RealSubject();
    Proxy proxy = new Proxy(realSubject);
    Client client = new Client(proxy);
    client.execute();
    client.execute();
    client.execute();
  }

프록시를 도입하여 3초에서 1초로 시간이 변경 되었다.

대략적으로 이런 흐름을 통해서 영속성컨택스트의 1차 캐싱을 지원하게 된다.

✅ 프록시 초기화 과정

  1. 프록시 객체의 메소드를 호출해서 실제 데이터를 조회
  2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을
    요청 (초기화)
  3. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
  4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버 변수에 보관한다.
  5. 프록시 객체는 실제 엔티티 객체의 메서드를 호출해서 결과를 반환한다.

JPA 식별자와 프록시

엔티티를 프록시로 조회 할 때 식별자 (PK) 값을 파라미터로 전달하는데, 프록시 객체는 이식별자
값을 보관한다

따라서 Member의 식별자 값이 member.getId()를 해도 프록시는 초기화 되어있지 않는다

프록시 초기화 여부 확인

JPA에서 제공하는 PersistenceUtil.isLoaded 사용하여 초기화 유무 확인

🚀 즉시로딩과 지연로딩

차이를 보고 무엇이 더 효과적일지 결정하자

🤙 즉시 로딩

엔티티를 조회 할 때 연관된 엔티티도 함께 조회 한다.

즉시 로딩을 사용하려면 @OneToMany 기준으로 FetchType을 Eager로 지정한다.

@ManyToOne 의 default는 Eager이다

즉시 로딩을 할경우 회원과 팀을 조인해서 쿼리 한 번으로 두 엔티티를 모두 조회한다.

성능 생각 해보기

Member 의 외래키가 Nullable 할 경우 => 외부 조인을 실행
NotNull 일 경우 (@JoinColumn(nullable = true)) 내부 조인 사용
성능 상 조금이라도 이점을 가져가고 싶다면 외래키 제약 추가

✅ 지연 로딩

연관된 엔티티를 실제 사용하는 시점에 JPA가 SQL을 호출해서 조회한다.

  1. 연관된 엔티티는 proxy로 되어있다. (영속성 컨텍스트에 존재 하지않을 경우)
  2. 프록시 초기화를 통해 실제 엔티티를 가져온다.

🤔 지연 로딩과 컬렉션

하이버네이트 기준으로 엔티티를 영속 상태로 만들때 엔티티에 컬렉션이 존재하면 컬렉션을
추적, 관리 목적으로 원본 컬렉션을 내장 컬렉션으로 변경한다(컬렉션 래퍼)

이 컬렉션 래퍼가 컬렉션에 대해 지연 로딩을 처리한다.

참고로 batchSize 설정이 되어있다면 같은 엔티티 클래스의 컬렉션 래퍼를 in 절로
한번에 초기화 한다.

😥 지연 로딩 vs 즉시 로딩

컬렉션을 가지는 엔티티의 경우 즉시 로딩하는 것을 권장하지 않는다.

  • em.find()에서는 문제가 터지지 않으나 JPQL 로 조회 시에 1+N 문제가 발생할 수 있다.
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
  • 지연로딩은 개발자의 비지니스 로직에 따라서 join fetch나 batch로 해결 할 수 있다

    하지만 즉시 로딩은 그러하지 않는다.

그러면 컬렉션을 안가지는 경우에는?

이 또한 권장 되지 않는다.

지연로딩은 개발자의 비지니스 로직에 따라서 join fetch나 batch로 해결 할 수 있다

하지만 즉시 로딩은 그러하지 않는다.

추가적으로 내가 모르는 사이에 join이 나가고 하는 것이기 때문에 sql 에 대한 최적화가 어려워진다
고로 지연로딩을 사용하는 것이 정답에 가깝다!

🚀 영속성 전이 CASCADE

정말 말그대로 영속을 전이시키는 내용이다. 특정 엔티티를 영속상태로 만들때 연관된 엔티티도
함께 영속 상태로 만들고 싶을 경우 사용한다.

JPA는 cascade 옵션으로 영속성 전이를 제공한다.

🖥 1:N에서의 Cascade

@Entity
public class Parent {
    ...
    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST) // 부모 영속화할 때 연관된 자식들도 함께 영속화 시키는 옵션
    private List<Child> children = new ArrayList<Child>();
    ...
}

// 사용 할 때
private static void saveWithCascade(EntityManager em) {
    Child child1 = new Child();
    Child child2 = new Child();
    Parent parent = new Parent();
    child1.setParent(parent);       //연관관계 추가
    child2.setParent(parent);       //연관관계 추가
    parent.getChildren().add(child1);
    parent.getChildren().add(child2);
    // 부모 저장, 연관된 자식들 저장
    em.persist(parent);
}

다양한 cascade 옵션이 존재하며 주로 부모가 자식의 대부분을 control 하며
자식에서의 연관관계가 (참조하고 있는 경우) 하나의 엔티티일때 cascade 옵션을 사용한다.

🚀 고아 객체

=> 1:N or 1:1 에서만 사용가능

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공

부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제

 

🖥 orphanRemove

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<Child>();
    ...
}

'Spring > JPA' 카테고리의 다른 글

JDBC 사용해보기  (0) 2022.08.23
JPA - 값 타입  (0) 2022.08.23
고급 매핑  (0) 2022.08.23
JPA - 연관관계 이해하기  (0) 2022.08.03
JPA - 엔티티 매핑  (0) 2022.08.03
댓글
11-08 10:33
Total
Today
Yesterday
링크