프록시와 연관관계 관리

OneTwoThree·2023년 7월 31일
0

출처


프록시


위와 같은 상황에서 Member를 조회할 때 Team도 같이 DB에서 조회를 해야할까?

Member만 사용하고 Team은 사용 안하는 경우 둘다 조회하면 손해다.
JPA는 이것을 지연 로딩과 프록시로 해결한다.

프록시 기초

em.find() : 데이터베이스를 통해 실제 엔티티 객체 조회
em.getReference() : 데이터베이스 조회를 미루는 가짜 엔티티 객체 조회

getReference()를 호출하는 시점에는 db에 쿼리를 안한다.
그 값이 실제 사용되는 시점에는 쿼리가 나간다.

getReference()는 껍데기는 똑같은데 안이 텅텅 빈 프록시 객체를 반환함. 그리고 target이 진짜 엔티티 객체를 가리킴

프록시는 실제 엔티티 객체를 상속해서 만들어진다.
실제 클래스와 겉 모양이 같다.
사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

프록시 특징

프록시 객체는 실제 객체의 참조 target을 보관한다.

프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

처음에 getName을 호출하면 target에 값이 없기 때문에 영속성 컨텍스트에 초기화를 요청한다. 영속성 컨텍스트가 DB를 조회해서 실제 Entity 객체를 생성하고 target과 연결해준다.

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 프록시를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것이 아니고 프록시 객체를 통해 실제 엔티티를 조회할 수 있게 되는 것임
  • 프록시 객체는 원본 엔티티를 상속받음. 타입 체크시 == 비교가 아닌 instance of 를 사용해야함
    • jpa에서 객체 비교를 할 때는 웬만하면 instance of를 사용해라
    • m1 instanceof Member
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 호출한다.
  • jpa에서 같은 em에서 같은 트랜잭션 내에서 같은 pk 값으로 가져온 객체를 == 비교하면 항상 true가 되어야 함
    • 이러한 이유 때문에 프록시로 한번 조회 하면 em.find()해도 프록시를 조회한다
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 프록시를 초기화하면 문제 발생
    • em.detach(refMember)
    • 프록시에 대한 초기화 요청은 영속성 컨텍스트를 통해 일어나기 때문
    • LazyInitializationException
  • 프록시 초기화 여부 확인
    • EntityManagerFactory를 통해 프록시 인스턴스의 초기화 여부 확인 가능 PersistenceUnitUtil.isLoaded(Object entity)
    • Hibernate.initalize(refMember) 로 강제 초기화 가능
    • JPA 표준에는 강제 초기화가 없다

즉시 로딩과 지연 로딩

다시 예시로 돌아가서 Member만 조회할 것인데 Team 까지 조회하면 손해다. JPA는 지연로딩이라는 옵션을 제공한다

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Team team;

이렇게 할 경우 Team은 프록시객체로 조회하고 Member만 db에서 조회한다. 지연 로딩으로 설정하면 연관된 객체를 프록시로 가져온다.
team.getName()으로 실제 Team을 사용하는 시점에 Team이 초기화된다.
비즈니스 로직 상 Member를 조회했을 때 Member만 사용하고 Team은 거의 쓰지 않는다면 지연 로딩으로 설정하는게 맞다.

Member와 Team을 자주 함께 사용할 수도 있다. 그러면 즉시로딩으로 바꿀 수 있다.

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

설정값을 Eager로 주면 즉시 로딩을 한다.
한번에 진짜 객체를 전부 가져온다.
즉 Member를 조회할 때 Team까지 같이 가져온다.
대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회한다.

주의점

  • 실무에서는 가급적 지연 로딩만 사용할 것
  • 즉시 로딩을 적용하면 예상하지 못한 SQL 발생
    • 연관관계가 많으면 조인이 엄청 많아지고 성능 문제 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다
    • JPQL은 SQL로 그대로 번역해서 쿼리가 나가기 때문에 Member가 10개 조회되면 10개만큼의 Team을 조회하는 쿼리가 나가게 된다.
    • 최초 쿼리 1개 때문에 N개의 추가 쿼리(데이터 개수만큼)이 나가게 된다.
  • @ManyToOne @OneToOne은 기본이 즉시 로딩이다.
    • Lazy로 설정해야 한다
  • @OneToMany @ManyToMany는 기본이 지연 로딩이다

영속성 전이 CASCADE

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

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "parent")
    List<Child> childList = new ArrayList<>();
    
    public void addChild(Child child){
        childList.add(child);
        child.setParent(this);
    }

}

addChild는 연관관계 편의 메소드이다.

@Entity
public class Child {

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

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

}

이런 경우 Parent 하나에 Child 객체 2개가 있을 경우, 아래와 같이 영속성 컨텍스트에 모두 persist 해줘야 한다.

            Parent parent = new Parent();
            Child child1 = new Child();
            Child child2 = new Child();
            parent.addChild(child1);
            parent.addChild(child2);
            em.persist(parent);
            em.persist(child1);
            em.persist(child2);

Parent 중심으로 코드를 작성하는데 Child까지 persist 해야 하는 귀찮은 상황이 발생한다.

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    List<Child> childList = new ArrayList<>();

Parent에서 위와 같이 수정해준다.
그러면 em.persist(parent)만 해도 child1,child2가 persist 된다.
연관관계와 상관없이 Parent를 persist 할 때 Child까지 persist 하고 싶으면 Parent의 Child 필드에 cascade = CascadeType.ALL)를 걸어주면 된다.

주의

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

CASCADE의 종류

  • ALL : 모두 적용 (라이프사이클 맞춤)
  • PERSIST : 영속 (저장할 때만 라이프사이클 맞춤)

Parent - Child 처럼 Parent가 Child 여럿을 관리하고 Child랑 관련된게 Parent 하나일 때는 이렇게 사용해도 된다.
하지만 Child랑 연관된 객체가 또 있으면 CASCADE를 사용하면 안된다.

  • 단일 엔티티에 완전히 종속적일 때만 사용해야 한다

고아 객체

고아 객체 제거는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 것이다.

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, 
    orphanRemoval = true)
    List<Child> childList = new ArrayList<>();

orpahnRemoval=true 속성으로 사용한다.

Parent가 관리하는 컬렉션 리스트에서 Child가 제거되면 Child에 대한 delete쿼리가 나간다.

주의점

  • 참조하는 곳이 하나일 때 사용해야 한다
  • 특정 엔티티가 개인 소유할 때 사용한다
  • @OneToOne , @OneToMany만 가능
  • CASCADE REMOVE와 같이 작동한다 (ALL도 마찬가지)

CascadeType ALL + orphanRemoval=true 같이 사용하면?

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()em.remove() 로 관리
  • 두 옵션을 모두 키면 부모 엔티티로 자식의 생명주기를 관리할 수 있다.
    • Parent만 persist하고 remove 해도 자식이 관리가 된다

0개의 댓글