Peony의 기록 창고 🌼
article thumbnail
반응형

프록시

❓Member를 조회할 때 Team도 함께 조회해야 할까?

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Member member = em.find(Member.class, 1L);
            printMember(member);
//            printMemberAndTeam(member);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

    private static void printMember(Member member) { //회원만 출력
        System.out.println("member = " + member.getName());
    }

    private static void printMemberAndTeam(Member member) { //팀과 함께 출력
        String userName = member.getName();
        System.out.println("userName = " + userName);

        Team team = member.getTeam();
        System.out.println("team = " + team.getName());
    }

 

❓테이블을 조회해서 객체를 가져올 때 연관관계 객체는 안가져 오고 싶으면 어떻게 해야 할까?

em.find() VS em.getReference()

  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
Member member = em.getReference(Member.class, 1L); 
System.out.println("member = " + member.getClass()); // HibernateProxy 객체(가짜 클래스)

getReference() 메서드를 사용하면 진짜 객체가 아닌 하이버네이트 내부 로직으로 프록시 엔티티 객체 반환한다.

내부 구조는 틀은 같지만 내용이 비어있다. 내부에 target이 진짜 reference를 가리킨다.

 

프록시 특징

  • 실제 클래스를 상속 받아 만들어지며, 실제 클래스와 겉 모양이 같다.
  • 이론상으로 사용하는 입장에서는 진짜인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상)
  • 프록시 객체는 실제 객체의 참조(target)를 보관한다.
  • 프록시 객체를 호출(getName())하면 프록시 객체는 실제 객체의 메소드 호출한다.
  • 프록시는 처음 사용할 때 한 번만 초기화
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());

System.out.println("m1  : " + (m1 instanceof Member)); //true
System.out.println("m2  : " + (m2 instanceof Member)); //true
  • 프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해 실제 엔티티에 접근 가능.
  • 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크시 주의해야함( == : 비교 실패(type 다름). 대신 instance of 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = "+ m1.getClass());//Member

Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " reference.getClass()); //Member

m1 == reference //true

이미 Member를 1차 캐시에도 올라와 있는데, 프록시를 반환할 필요가 없다.

  • 반대로 getReference()로 프록시객체를 가지고 있으면 실제로 find()를 했을때도 프록시 객체를 반환한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태(detach, clear, close 등)일 때, 프록시를 초기화하면 문제 발생
    • LazyInitializationException 예외가 발생한다.
  • 한 트랜잭션 안에서 객체의 동일성을 보장 (프록시, 실제 객체 구별없이 동일성 보장 - 컬렉션 방식)

 

프록시 객체 초기화 과정

  • getReference()를 사용해서 조회하려는 값이 프록시가 갖고 있지 않은 값이면 영속성 컨텍스트에 초기화 요청을 하여 실제 객체에 값을 넣어줌.
  • 영속성 컨텍스트에서는 실제 db를 조회해서 가져온 다음 실제 Entity에 값을 넣어 생성한 다음 프록시 객체는 실제 엔티티를 연결해서 실제 엔티티를 반환한다.
  • 그 이후에는 이미 초기화되어있는 프록시 객체이기에 해당 엔티티를 반환한다.
Member findMember = em.getReference(Member.class, member.getId()); // 프록시 객체 생성 
System.out.println("findMember = " + findMember.getClass()); // ID는 갖고 있으므로 프록시에서 바로 반환 
System.out.println("findMember.id = " + findMember.getId()); // ID는 영속성 컨텍스트에 초기화 요청을 통한 DB접근 
System.out.println("findMember.userName() = " + findMember.getName());
  • 프록시의 동작과정을 다음과 같이 확인해볼 수 있다. getClass를 찍어보면 프록시 클래스임을 알 수 있다.

 

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인 :
    PersistenceUnitUtil.isLoaded(Object entity) → entityManagerFactory.getPersistenceUnitUtil().isLoaded(object)
  • 프록시 클래스 확인 방법
    entity.getClass().getname() 출력(..javasist.. or HibernateProxy...)
  • 프록시 강제 초기화
    : org.hibernate.Hibernate.initialize(entity);
  • 참고: JPA 표준은 강제 초기화 없음 강제 호출
    : method.getName();

 

즉시 로딩과 지연 로딩

지연 로딩

❓Member를 조회할 때 Team도 함께 조회해야 할까?

: member 정보만 사용하는 비즈니스 로직에서

지연 로딩 LAZY을 사용해서 프록시로 조회 fetch = FetchType.LAZY

/*Member*/
@Entity
public class Member{
    ...
    @ManyToOne(fetch = FetchType.LAZY) //지연로딩 사용
    @JoinColumn(name="TEAM_ID")
    private Team team;
    ...
}
...
Member m = em.find(Member.class, member1.getId()); //Member 객체 반환
System.out.println("m = "+ m.getTeam().getClass()); //Team$HibernateProxy객체 반환
m.getTeam().getName() // team을 실제로 사용하는 시점에서 초기화 (db조회)
...

 

즉시 로딩

❓Member를 조회할 때 Team도 함께 조회해야 할까?

: Member와 Team을 같이 쓰는 빈도가 높을 경우

즉시 로딩 EAGER를 사용해서 함께 조회 fetch = FetchType.EAGER

/*Member*/
@Entity
public class Member{
    ...
    @ManyToOne(fetch = FetchType.EAGER) //즉시로딩 사용
    @JoinColumn(name="TEAM_ID")
    private Team team;
    ...
}
...
Member m = em.find(Member.class, member1.getId()); //Member 객체 반환
System.out.println("m = "+ m.getTeam().getClass()); //Team 객체 반환
...

즉시 로딩 : Member를 조회하는 시점에서 Team도 같이 조회하는 것.(조인 사용)

 

프록시와 즉시로딩 주의

  • 가급적 지연 로딩만 사용 (특히 실무에서)
  • 즉시 로딩을 적용하면 예상하지 못한 SQL 발생
    : 하나의 엔티티에 연관된 엔티티가 많다면, find() 수행 시 수십, 수백개의 테이블을 한번에 불러와야 한다.
  • 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩으로 되어 있다. → 직접 전부 LAZY로 설정
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

 

지연 로딩 활용 - 이론

  • Member와 Team 은 자주 함께 사용 → 즉시 로딩
  • Member와 Order는 가끔 사용 → 지연 로딩
  • Order와 Product는 자주 함께 사용 → 즉시 로딩

 

지연 로딩 활용 - 실무

  • 모든 연관관계에 지연 로딩을 사용하자.
  • 실무에서 즉시 로딩을 사용하지 마라.
  • JPQL fetch 조인이나, 엔티티 그래프 기능을 사용해라.
  • 즉시 로딩은 내가 의도하지 않은 쿼리가 수행된다.

 

영속성 전이(CASCAD)와 고아 객체

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

ex) 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장.

 

영속성 전이 : 저장

@Entity
public class Parent{
    ...
    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child){
        childList.add(child);
        child.setParent(this);
    }
    ...
}
@Entity
public class Child{
    ...
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
    ...
}

/*main*/
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);
em.persist(child1);
em.persist(child2);// persist 3번

이 코드에서는 persist를 3번이나 해야한다. 이는 효율적이지 못해 보인다.

persist 한 번으로 child 까지 같이 persist는 불가능 할까?

 

영속성 전이(CASCADE)를 이용한 엔티티 저장 방법

@Entity
public class Parent{
    ...
    @OneToMany(mappedBy = "parent", cascade=CascadeType.ALL)//영속성 전이 속성(CASCADE)사용
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child){
        childList.add(child);
        child.setParent(this);
    }
}
@Entity
public class Child{
    ...
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
    ...
}

/*main*/
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);

em.persist(parent);// parent만 persist 해주면 child도 같이 persist된다.

CASCADE 종류

  • ALL: 모두 적용(모든 곳에서 맞춰야 하면)
  • PERSIST: 영속(저장할 때만 사용 할 것이면)
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: DETACH

 

❓그럼 영속성 전이(CASCADE)는 언제 써야 할까?

: 전이 될 대상이 한 군데에서만 사용된다면 써도 된다.

하지만, 해당 엔티티(Child)가 특정 엔티티(Parent)에 종속되지 않고 여러군데서 사용된다면

사용하지 않는게 좋다.

  • 라이프 사이클이 동일할 때
  • 단일 소유자 관계일 때

 

고아 객체

고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제 : orphanRemoval = true

@Entity
public class Parent{
    ...
    @OneToMany(mappedBy = "parent", cascade=CascadeType.ALL, orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();

    public void addChild(Child child){
        childList.add(child);
        child.setParent(this);
    }
    ...
}

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

em.persist(parent);// parent만 persist -> child도 같이 persist

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

Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // orphanRemoval 동작

고아 객체 - 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
  • 참조하는 곳이 하나일 때 사용해야함.
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
  • 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 마치 CascadeType.REMOVE처럼 동작한다.
  • Parent객체를 지우게 되면 Parent가 소유하고있는 ChildList에 속한엔티티들이 전부 같이 삭제된다.

 

영속성 전이 + 고아 객체, 생명 주기 (CascadeType.ALL + orphanRemoval=true)

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기 관리가 가능하다.
  • 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용

 

Aggregate Root

: 연관깊은 도메인들을 각각이 아닌 하나의 집합으로 다루는 것. 즉 데이터 변경의 단위로 다루는 연관 객체의 묶음.

Aggregate에는 루트(root)와 경계(boundary)가 있는데, 경계는 Aggregate에 무엇이 포함되고 포함되지 않는지를 정의한다. 루트는 단 하나만 존재하며, Aggregate에 포함된 특정 엔티티를 가르킨다. 경계안의 객체는 서로 참조가 가능하지만, 경계밖의 객체는 해당 Aggregate의 구성요소 가운데 루트만 참조할 수 있다.

실전 예제

글로벌 페치 전략 설정

  • 모든 연관관계를 지연로딩(fetch = FetchType.LAZY)으로 변경하자.
    @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연 로딩으로 변경하자.

 

영속성 전이 설정

@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>()
  • Order 엔티티의 Delivery와 OrderItem을 영속성 전이 ALL 설정

 

이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.

 

 

반응형
profile

Peony의 기록 창고 🌼

@myeongju