본 정리 내용은 김영한님의 "자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 중요한 내용이 판단된 부분은 강의자료의 사진자료를 사용하였습니다.
JPA의 프록시
em.find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.
em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
getReference()를 호출하는 시점에는 데이터베이스에 select 쿼리를 하지 않는다. 그런데 getReference()가 실제 사용되는 시점에 쿼리가 나간다.
프록시는 실제 클래스를 상속 받아서 만들어진다. 실제 클래스와 겉 모양이 같다. 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상으로는)
프록시 객체는 실제 객체의 참조(target)를 보관한다. 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
실제 사용되는 시점(사진에서는 1.getName())에서 프록시 객체는 getName()의 값을 아직 알지 못한다. 그래서 영속성 컨텍스트에 초기화를 요청한다. 그러면 영속성 컨텍스트는 DB에 들러서 실제 Entity를 생성해준다. 그리고 참조값을 가지고 있던 target에다가 실제 생성한 Entity를 연결해준다.
프록시의 특징
- 프록시 객체는 처음 사용할 때 한번만 초기화한다.
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다! 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한것이다.
- 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다. (== 비교 실패, 대신 instance of 사용) (아래 사진 예시참고)
- 위와 같이 하면 첫번째 println은 "findMember1==findMember2 : false" 가 출력되고, 두번째와 세번째 println은 각각 true가 출력된다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다. (아래 예제)
- 그 반대도 마찬가지이다. 처음에 em.getReference()로 조회하면 em.find()해도 프록시 객체를 반환한다. (아래 예제)
왜 이런 결과를 얻는 것일까?
1. 1차 캐시에 있기 때문에
2. 마치 컬렉션에 있는 값을 비교하는 것처럼 한개의 트랜젝션 안에서 같은 pk값을 갖는 엔티티는 같은 엔티티임을 보장해준다. 즉, ==비교를 true로 만들기 위해서 영속성 컨텍스트 안에 있으면 프록시가 아니라 실제 엔티티를 반환하게 된다. (이 개념은 매우 중요하다.)
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
(하이버네이트는 org.hibernate.LazyInitializationException예외를 터뜨린다.)
- 하이버네이트 5.4.0.Final 버전까지는 단순히 세션(엔티티매니저)이 끝나면 예외가 터졌었는데, 5.4.1.Final 이상버전부터는 트랜젝션이 유지가 되면 Lazy로딩을 사용할 수 있도록 최적화 되었다. 버전이 5.4.1.Final 이상이라면 위의 예제와 같은 경우, 트랜잭션을 종료하지 않은 상태에서 세션(엔티티메니저)를 닫았기 때문에 예외가 터지지 않는다.
프록시 확인을 도와주는 유틸리티 메서드
- 프록시 인스턴스의 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity) - 프록시 클래스 확인 방법
entity.getClass().getName()출력 - 프록시 강제 초기화
org.hibernate.Hibernate.intialize(entity);
-> 참고 : 이건 하이버네이트가 지원하는 초기화 방법이고, JPA 표준에는 강제 초기화가 없음. 위에 예제들에서 했던 것처럼 member.getUserName(); 처럼 강제 호출 해야함.
즉시 로딩과 지연 로딩
Member와 Team이 다대일(@ManyToOne)로 연관관계 매핑이 되어있다고 가정해보자. 우리가 만든 프로그램에서 Member와 Team정보가 모두 필요한 경우보다 Member만 조회할 경우가 더 많다고 한다면 Member 엔티티를 조회할 때 항상 Team을 Join해서 조회해야할까? 단순히 Member정보만 사용하는 비즈니스 로직에서는 매우 손해일 것이다.
이럴때 FetchType을 LAZY로 하면 Member를 로딩할 때 Team은 프록시객체를 이용해서 지연로딩을 한다.
그 후 실제 Team의 속성을 실제로 사용하는 시점(Team을 불러오는 시점이 아니라 사용하는 시점이라는 것을 유의)에 프록시 객체가 초기화 되면서 값을 가지고 온다.
쉽게 말하자면 지연로딩일 때 Member를 로딩하면 Team에 프록시 객체를 넣어둔 상태로 Join하지 않고 Member만 가지고 오는 Select쿼리가 나가고, 실제 사용한다고 하면 그 때서야 Team을 가져오기 위한 Select 쿼리가 나간다.
반대로 Member와 Team을 거의 항상 같이 사용한다면? FetchType을 EAGER로 하여 Member와 Team을 Join하는 즉시로딩을 할 수 있다. 이 때 team을 조회하면 프록시가 필요없기 때문에 실제 객체가 나온다.
코드 예제를 보자. 예제 코드에는 Member가 JpaMember, Team이 JpaTeam이다.
연관관계의 주인인 JpaMember클래스이다. 그 안에 JpaTeam jpateam이 존재하고 fetch를 LAZY로 해줌으로써 지연로딩설정을 해주었다.
지연로딩으로 설정한 상태로 첫번째 println에서 findMember.getJpateam().getClass()하여 JpaMember안의 JpaTeam을 가져오려고 하고 있다.
(=== 는 실제 이 부분에서 쿼리가 나가는지 구분하기 위한 선)
두번째 쿼리에서는 findMember.getJpateam().getName()하여 JpaTeam의 이름을 실제로 호출하고 있다. 실제 사용되는 시점이다.
첫번째는 프록시 객체가 나왔고,
두번째는 실제 사용되는 시점이기 때문에 Select쿼리가 나가고 JpaTeam의 이름을 가지고 왔다.
프록시와 즉시로딩 주의
- 가급적 지연로딩만 사용하자
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. join sql이 너무 많이 발생할 수도 있다.
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
JPQL은 SQL과 1:1 매칭이 되기 때문에 예를 들어, "select m from Member m" 과 같은 JPQL을 작성하면 "select * from Member"와 같은 SQL이 나간다. 그런데 Member의 Team이 즉시로딩으로 되어 있으면 값을 불러올 때 모두 채워져 있어야하기 때문에 "select * from Team"와 같은 SQL이 한번 더 나간다. N+1의 1은 최초쿼리, N은 최초쿼리로 부터 파생된 쿼리들이다.
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 LAZY로 설정을 해주어야 한다.( ~ToOne시리즈는 모두 LAZY로 설정해 주어야한다고 기억하자)
영속성 전이와 고아 객체
영속성 전이 : CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을 때 사용한다.
예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 것이다.
위와 같은 관계인 Parent와 Child가 있다.
아래 예제코드에는 Getter Setter가 생략되어 있는데 있어야 한다..
Parent클래스에 있는 addChild 메서드는 연관관계 편의 메서드이다.
연관관계의 주인인 Child에는 당연히 값이 들어가야 하고, 객체 지향적인 관점에서 Parent에도 값이 들어가야 하기 때문에 연관관계 편의 메서드를 만들어서 Child값을 집어넣을 때 양쪽에 값을 모두 넣을 수 있는 메서드를 만들어서 관리한다.
@OneToMany의 속성으로 cascade를 설정해주었다.
(아래 transaction.commit() 있다고 하자.)
테스트 코드에서 child1과 child2를 추가해주었고, em.persist(parent); 를 해주었다. casecade속성이 없었다면 parent만 영속성 컨텍스트에 영속되면서 insert쿼리가 한개 나가지만 casecade속성으로 인해서 연관된 엔티티도 함께 영속상태로 만들기 때문에 child2개도 모두 영속상태가 되어 insert쿼리가 3개 나간다.
- 주의점 -
1. 부모 엔티티와 자식 엔티티의 라이프 사이클이 거의 유사하거나 동일할 때만 사용해야 한다.
2. 소유자가 하나일때만 사용해야 한다.(부모 엔티티만 자식엔티티를 소유할 때)
고아객체
참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아객체로 보고 삭제하는 기능이다.
컬렉션에서 빠진 객체는 연관관계가 삭제된다. orphanRemoval = true 옵션으로 사용한다.
@OneToMany의 속성으로 orphanRemoval = true을 해주었다.
em.flush(); em.clear(); 해줌으로써 영속성 컨텍스트(정확히는 쓰기 지연 SQL저장소에 쌓여있던 쿼리)에 있는 쿼리를 모두 날리고 영속성 컨텍스트를 모두 비웠다.
그 후 em.find() 하여 DB에 접근하여 Parent 객체를 가져왔다. 그리고 첫번째 자식 엔티티를 컬렉션에서 삭제하였다.
그 결과 Delete 쿼리가 나간다.
- 주의점 -
1. 참조하는 곳이 하나일 때, 특정 엔티티가 개인 소유일 때만 사용해야 한다.
2. @OneToOne, @OneToMany만 사용가능하다.
3. 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CasecadeType.REMOVE 처럼 동작한다.
영속성 전이 + 고아객체, 생명주기
CasecadeType.ALL + orphanRemoval = true
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거할 수 있다. 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
- 이는 도메인 주도 설계(DDD)의 Aggregate Root개념을 구연할 때 유용하다.
'JAVA > Spring JPA' 카테고리의 다른 글
JPA의 값 타입 (0) | 2022.01.05 |
---|---|
상속관계 매핑과 매핑 정보 상속(@MappedSuperclass) (0) | 2021.12.30 |
다양한 연관관계 매핑 (0) | 2021.12.29 |
단방향 연관관계와 양방향 연관관계 (0) | 2021.12.28 |
엔티티 매핑 (0) | 2021.12.25 |