Search
Duplicate

프록시와 연관관계 관리

프록시

프록시란?
테이블을 조회해서 객체를 가져올 때 연관관계 객체는 안가져 오고 싶으면 어떻게 해야 할까?
em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
Member member = em.getReference(Member.class, 1L); System.out.println("member = " + member.getClass()); // HibernateProxy 객체
Java
복사
getReference() 메서드를 사용하면 진짜 객체가 아닌 하이버네이트 내부 로직으로 프록시 엔티티 객체 반환
내부 구조는 틀은 같지만 내용이 비어있다.
프록시 객체를 반환한다.
특징
실제 클래스를 상속받아서 만들어짐
실제 클래스와 겉 모양이 같다.
사용하는 입장에서는 진짜 객체인지 구분 필요가 없다(이론적으로)
프록시 객체는 실제 객체의 참조(target)를 보관한다.
프록시 객체를 호출(getName())하면 프록시 객체는 실제 객체의 메소드 호출
프록시는 처음 사용할 때 한 번만 초기화
프록시 객체를 초기화 할 때 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해 실제 엔티티에 접근 가능
프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함( ==비교 실패, 대신 instance of 사용)
m1.getClass() == m2.getClass() //false m1 instanceof Member // true m2 instanceof Member // true
Java
복사
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 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
Java
복사
이미 Member를 1차캐시에도 올라와 있는데, 프록시를 반환할 필요가 없다.
반대로 getReference()로 프록시객체를 가지고 있으면 실제로 find()를 했을때도 프록시 객체를 반환.
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
Member refMember = em.getReference(Member.class, member.getId()); System.out.println("refMember = "+ refMember.getClass());//Proxy em.detach(refMember); //em.clear refMember.getUsername(); //org.hibernate.LazyInitializationException
Java
복사
프록시 객체의 초기화
Member member = em.getRefernce(Member.class, "id1");//(1) member.getName();//(2)-
Java
복사
코드 라인에서 getReference()를 호출하면 프록시객체를 가져온 다음, getName()을 호출하면 JPA가 영속성 컨텍스트에 초기화 요청을 한다.
영속성 컨텍스트에서는 실제 db를 조회해서 가져온 다음 실제 Entity에 값을 넣어 생성한 다음 프록시 객체는 실제 엔티티를 연결해서 실제 엔티티를 반환한다.
그 이후에는 이미 초기화되어있는 프록시객체 이기에 해당 엔티티를 반환한다.
프록시 확인
프록시 인스턴스의 초기화 여부 확인 : PersistenceUnitUtil.isLoaded(Object entity) → entityManagerFactory.getPersistenceUnitUtil().isLoaded(object)
프록시 클래스 확인 방법
entity.getClass().getname() 출력(..javasist.. or HibernateProxy...)
프록시 강제 초기화
: org.hibernate.Hibernate.initialize(entity);
참고: JPA 표준은 강제 초기화 없음 강제 호출: method.getNameI();

즉시 로딩과 지연 로딩

지연 로딩

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조회 엔티티 반환 ...
Java
복사
연관관계에 있는 다른 엔티티를 사용하는 빈도수가 낮을 경우 지연로딩을 사용해 불필요한 엔티티 조회를 막을 수 있다.

즉시 로딩

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 객체 반환 ...
Java
복사
Member를 가져오는 시점에서 연관관계에 있는 Team까지 바로 가져오는 것을 즉시 로딩이라 한다.

프록시와 즉시로딩 주의

가급적 지연 로딩만 사용(특히 실무에서)
즉시 로딩을 적용하면 예상하지 못한 SQL이 발생 ex: 하나의 엔티티에 연관된 엔티티가 다수라면 find() 한 번 수행시 수십 수백개의 테이블을 한번에...
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. ex:
/*Member*/ @Entity public class Member{ ... @ManyToOne(fetch = FetchType.EAGER) //즉시로딩 사용 @JoinColumn(name="TEAM_ID") private Team team; ... } ... List<Member> members = em.createQuery("select m from Member m", Member.class) .getResultList(); //SQL: select * from Member //SQL: select * from Team where TEAM_ID = xxx ...
Java
복사
위 JPQL을 그대로 쿼리로 번역하게 되면 Member를 가져오기 위한 쿼리 수행 이후 바로 Member 내부의 Team을 가져오기 위한 쿼리를 다시 수행하게 된다 → N+1(1개의 쿼리를 날리면 +N개의 쿼리가 추가수행된다) 만약, Team과 같은 연관관계가 더 있다면?
@ManyToOne, @OneToOne은 기본이 즉시 로딩으로 되어 있다.→ 직접 전부 LAZY로 설정
@OneToMany, @ManyToMany는 기본이 지연 로딩

N+1의 해결책

1.
우선, 전부 지연로딩으로 설정한다.
그 다음 가져와야하는 엔티티에 한해서 fetch join을 사용해서 가져온다.
List<Member> members = em.createQuery("select m from Member m fetch join m.team", Member.class) .getResultList();
Java
복사
이렇게 JPQL을 실행하면 fetch join을 통해 Team 도 가져왔기 때문에 문제가 없다.

지연 로딩 활용

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; ... } ... 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번이나 해야한다.
Java
복사
위와 같이 persist를 세 번 호출해야 정상적으로 동작을 한다. 아주 아름답지 않은<?> 코드이다.
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; ... } ... 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된다.
Java
복사
parent만 persist해주니 그에 관련된 childList들도 같이 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); } ... } @Entity public class Child{ ... @ManyToOne @JoinColumn(name = "parent_id") private Parent parent; ... } ... 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 동작
Java
복사
고아 객체 - 주의
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
참조하는 곳이 하나일 때 사용해야함.
특정 엔티티가 개인 소유할 때 사용
@OneToOne, @OneToMany만 가능
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 마치 CascadeType.REMOVE처럼 동작한다.
Parent객체를 지우게 되면 Parent가 소유하고있는 ChildList에 속한엔티티들이 전부 같이 삭제된다.
영속성 전이 + 고아 객체, 생명 주기 (CascadeType.ALL + orphanRemoval=true)
스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기 관리가 가능하다.
도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
참고: Aggregate Root개념을 알기 위해 우선 Aggregate를 설명해야 한다. - 연관깊은 도메인들을 각각이 아닌 하나의 집합으로 다루는 것을 Aggregate라 한다. 즉 데이터 변경의 단위로 다루는 연관 객체의묶음. - Aggregate에는 루트(root)와 경계(boundary)가 있는데, 경계는 Aggregate에 무엇이 포함되고 포함되지 않는지를 정의한다. 루트는 단 하나만 존재하며, Aggregate에 포함된 특정 엔티티를 가르킨다. 경계안의 객체는 서로 참조가 가능하지만, 경계밖의 객체는 해당 Aggregate의 구성요소 가운데 루트만 참조할 수 있다.
참고: DDD에서 루트 엔티티는 전역 식별성 Global Identity을 가진 엔티티라고 합니다. Ex) 주문시스템의 Order Entity

실전 예제5 - 연관관계 관리

모든 연관관계를 지연 로딩으로
@ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 지연로딩으로 변경
Order → Delivery 를 영속성 전이 ALL로 설정
Order → OrderItem 을 영속성 전이 ALL로 설정