프록시 + 영속성 전이/고아객체

김강현·2023년 3월 25일
0

ORM-JPA

목록 보기
6/9
post-thumbnail

프록시

find, getReference

em.find() 을 사용하면, 그 순간 DB 호출 함수를 실행하고, 해당 값을 반환해준다.

그런데, jpa에서는 em.getReference() 라는 함수를 지원해준다.

Member findMember = em.find(Member.class, member.getId());
// System.out.println("findMember.getId() = " + findMember.getId());
// System.out.println("findMember.getUsername() = " + findMember.getUsername());

위 코드에서는 findMember의 그 어떤 데이터들도 활용이 되지 않지만, DB 호출 함수는 실행이 되고, findMember에는 그 값이 들어가 있다. 아래는 getReference() 사용 예제이다.

Member findMember = em.getReference(Member.class, member.getId());
// System.out.println("findMember.getId() = " + findMember.getId());
// System.out.println("findMember.getUsername() = " + findMember.getUsername());

위 코드에서 우리는 findMember의 그 어떤 데이터들도 활용하지 않는다. 이때 EntityManager는 DB 호출 함수를 실행하지 않는다.
그럼 findMember에 반환되는 값은 대체 무엇인가!

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());

이 함수를 실행해보면

findMember = class org.example.Member$HibernateProxy$VZBUc9hc

이와 같은 결과가 나오는데, Member는 맞는데 뒤에 Hibernate(jpa 구현체)의 Proxy 라고 나와있다.
즉 Hibernate 가 만들어낸 가짜 클래스 라는 뜻이다.

@Entity 어노테이션이 걸려있는 class가 있고, Proxy라는 객체가 이 Entity를 상속받는다.
(타입 비교시 조심할 것!!, 덧. 이미 영속성컨텍스트에 올라와 있으면, Entity를 바로 반환)


Proxy안에는 Entity target 이 있음.

[ 호출 사용시 ]
1. Proxy에서 실제 데이터 요청이 들어온다.
2. Proxy는 영속성 컨텍스트에 요청을 한다. (초기화 요청)
3. 영속성 컨텍스트는 DB 호출 함수를 실행한다.
4. 영속성 컨텍스트는 실제 Entity를 생성한다.
5. 그 Entity의 값을 1.의 데이터 요청에 반환한다.

덧. find(), reference()를 같이 쓰게 되는 경우.
같은 id 값으로 조회가 되는 것이라면, 둘다 같은 type으로 반환 됨.
둘다 Entity 거나, 둘다 Proxy거나!!

(+) 준영속, 비영속 상태가 되면, proxy 초기화는 불가능해짐!

프록시 상태 확인

isLoaded

해당 프록시가 초기화가 되었는지 아닌지를 확인할 수 있다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
...

Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(findMember)); // false
System.out.println("findMember.getUsername() = " + findMember.getUsername());
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(findMember)); // true
...

EntityMangerFactory 의 함수인 getPersistenceUnitUtil().isLoaded() 을 활용하여 proxy의 초기화 여부를 확인 할 수 있음.

강제 초기화

프록시 내부 메소드를 사용하여 초기화를 하는 것이 아닌, 직접 초기화 함수 호출

Member findMember = em.getReference(Member.class, member.getId());
Hibernate.initialize(findMember); // 프록시 강제 초기화

(덧) jpa 표준엔 강제호출은 없음. hibernate가 제공해주는 것임!

즉시로딩, 지연로딩

지연 로딩 (Lazy Loading)

<필요성>
흠... Member는 Team을 가지고 있기는 한데, 모든 Member가 불러질때 꼭 Team을 받을 필요는 없는데... 그렇다고 필요할때마다 Team을 호출하는 함수를 만들기도 귀찮고... 좋은 방법 없을까?
아! 그럼 Team을 프록시로 설정 하면 되겠구나!!

Member.java (`ManyToOne(fetch = FetchType.LAZY) 추가)

@Entity
@Table(name = "MEMBER")
public class Member{
    @Id @GeneratedValue
    @Column(name = "MEMEBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

Team.java

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    @Column(name = "TEAMNAME")
    private String name;
}
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getUsername() = " + findMember.getUsername());
// 이때까지도, Team 데이터 호출하지 않음. 이때 team은 proxy객체로 저장되어 있음
System.out.println("findMember Team Class = " + findMember.getTeam().getClass()); // Team$HibernateProxy$prypi1Yf
System.out.println("findMember.getTeamName = " + findMember.getTeam().getName()); // Team 호출

@ManyToOne(fetch = FetchType.LAZY) 를 활용하면,
해당 Column 의 변수는 Proxy 로 가져온다는 것을 알 수 있음!!

즉시 로딩

흠 근데 사실... Member를 부르고, Team을 사용하는게 전체 90% 정도는 되는걸? 거의 왠만하면 같이 쓰는데... 그냥 Member 부를때 다 땡기자!

Member.java team에 @ManyToOne(fetch = FetchType.EAGER) 를 추가하면 된다.
이렇게 하면 getClass() 하면 Proxy가 나오던 지연 로딩과는 다르게, 바로 Entity class로 나오는 것을 알 수 있다!!

김영한 강사님 추천!!

가급적이면 모든 연관관계를 지연로딩으로 사용해라.
즉시로딩을 적용하면 예상하지 못한 SQL 이 발생함!
즉시로딩은 JPQL 에서 N + 1 문제를 일으킨다.
==> 상상치 못한 쿼리가 날아감!!

@ManyToOne, @OneToOne 기본이 즉시 로딩 -> LAZY 로 설정할 것!!
@OneToMany, @ManyToMany 기본이 지연 로딩
추후 fetch join 이라고, 화면별로 필요하게 같이 가져올 수 있도록 설정 가능!

영속성 전이 (CASCADE)

지금껏 배운 영속성 컨텍스트에 persist 하는 방법으로 이런 식으로 가능하다.
그런데 이쯤 되면, 뭔가 불편하지 않은가?

Parent parent = new Parent("부모");

Child child1 = new Child("child1");
Child child2 = new Child("child2");
Child child3 = new Child("child3");

parent.addChild(child1);
parent.addChild(child2);
parent.addChild(child3);

em.persist(parent);
em.persist(child1);
em.persist(child2);
em.persist(child3);

parent 하나만 등록하면 child 는 좀 알잘딱깔쎈 안되나?

아래와 같이 CascadeType.ALL 을 넣어주면,

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

Child child1 = new Child("child1");
Child child2 = new Child("child2");
Child child3 = new Child("child3");

parent.addChild(child1);
parent.addChild(child2);
parent.addChild(child3);

em.persist(parent); // child 들은 persist 가 자동으로 됨!
// em.persist(child1); 
// em.persist(child2);
// em.persist(child3);

없애는 경우에도, child 같이 지워짐!

em.remove(parent);

그저 편의성을 위한 것임. 연관관계 매핑하고는 일체 상관 없음!!!!
하나의 부모가 자식들을 관리할때는 매우 유용! 다른 경우는 선택적 사용!

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

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동 삭제!
orphanRemoval = true

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(1); // 실제 Delete 함수 호출!

해당 옵션을 넣지 않으면, remove 함수는 그저 객체 단에서만 작동하게 된다.

  • 참조하는 곳이 하나 일때 사용
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany 에서만 사용 가능

영속성 전이 + 고아 객체

CascadType.ALL + orphanRemovel=true

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

요약

  • 모든 연관관계를 지연 로딩으로
  • 영속성 전이 ALL + 고아객체 설정
profile
this too shall pass

0개의 댓글