loaded : false
className : com.fistkim.springjpawhiteshipstudy.Member$HibernateProxy$63wbZVZy
loaded : false
Hibernate:
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_
from
member member0_
where
member0_.id=?
loaded : true
className : com.fistkim.springjpawhiteshipstudy.Member$HibernateProxy$63wbZVZy
EntityManager는 프록시 객체를 어떻게 할당하는가
위 코드에서 확인할 수 있듯이 entityManager는 아래 자원을 통해서 프록시 객체를 할당한다.
public <T> T getReference(Class<T> entityClass,
Object primaryKey);
EntityManager 는 프록시 객체를 식별자 값으로 관리한다
로그를 보면 id를 가져오는 시점에는 초기화가 발생되지 않지만, name 을 가져오는 시점에는 초기화가 발생하는 것을 확인할 수 있다. EntityManager 가 프록시 객체에 대해서 식별값으로 관리하고 있으며, 식별값만 사용할 경우 이미 갖고 있으니까 초기화를 시킬 필요가 없음을 의미한다.
초기화가 되었다 하더라도 프록시 객체는 트랜잭션 종료까지 계속 프록시 객체이다
초기화 여부와 상관없이 동일 트랜잭션 내에서 한 번 프록시 객체로 할당이 되었다면 트랜잭션 종료까지 계속 프록시 객체이다. 단지 초기화 여부에 따라 프록시 객체 내부에 실체에 대한 참조값이 할당이 되었느냐 안되었느냐의 차이가 있는 것이다.
조회 예제로 알아보는 EntityManager의 프록시 객체 활용 원리
// MemberProxy
Member member = em.getReference(Member.class, 1L);
member.getName();
class MemberProxy extends Member {
Member target = null;
public String getName(){
if(target == null){
// 초기화 요청, DB 조회, 실제 엔티티 생성 및 참조 보관
this.target = ...;
}
return this.target.getName();
}
}
만약에 프록시 객체에 할당하고자 하는 실체가 이미 영속성 컨텍스트에 존재한다면 프록시 객체가 할당되는 것이 아니라 실제 엔티티가 할당된다.
같은 맥락에서 만약 영속성 컨택스트의 도움을 받지 못하는 상황에 초기화 요청이 발생할 경우 LazyInitializationException 이 발생한다.
프록시가 컬렉션일 경우 : 컬렉션 래퍼
일대다 관계와 같은 것을 객체에 표현할 때 List 혹은 Set 을 사용하게 되는데, 이를 지연로딩하게 되면 하이버네이트는 하이버네이트가 제공하는 내장 컬렉션으로 이를 제공하는데 이를 컬랙션 래퍼라고 한다. 위에서 살펴본 프록시와 동일하게 동작한다.
하지만 List 와 Set 은 약간 다르게 동작한다. 중복에 대한 처리 여부가 List 와 Set 이 다르기 때문에 발생하는 차이인데 아래 코드에서 차이를 살펴보자.
List 인 경우
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
@Override
@Transactional
public void run(ApplicationArguments args) {
final Team team = this.entityManager.find(Team.class, 1L);
System.out.println("className : " + team.getMembers().getClass().getName());
System.out.println(this.isLoaded(team.getMembers()));
final Member newMember = new Member();
newMember.setTeam(team);
team.getMembers().add(newMember);
System.out.println(this.isLoaded(team.getMembers()));
}
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
className : org.hibernate.collection.internal.PersistentBag
false
false
Hibernate:
insert
into
member
(name, team_id)
values
(?, ?)
일단 컬랙션 래퍼의 이름이 PersistentBag 인 것을 알 수 있다. 새로운 member를 List 에 추가했지만 끝까지 members 는 초기화가 되지 않고 있다.
Set 인 경우
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Member> members = new HashSet<>();
@Override
@Transactional
public void run(ApplicationArguments args) {
final Team team = this.entityManager.find(Team.class, 1L);
System.out.println("className : " + team.getMembers().getClass().getName());
System.out.println(this.isLoaded(team.getMembers()));
final Member newMember = new Member();
newMember.setTeam(team);
team.getMembers().add(newMember);
System.out.println(this.isLoaded(team.getMembers()));
}
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
className : org.hibernate.collection.internal.PersistentSet
false
Hibernate:
select
members0_.team_id as team_id3_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.name as name2_0_1_,
members0_.team_id as team_id3_0_1_
from
member members0_
where
members0_.team_id=?
true
Hibernate:
insert
into
member
(name, team_id)
values
(?, ?)
컬랙션 래퍼의 이름이 PersistentSet 이다. List 때와 사용된 컬랙션 래퍼가 일단 다르다. 그리고 새로운 member 를 add 하는 순간 초기화가 발생한다.
왜냐하면 Set 은 내부 element 의 유일성을 보장해야하는데, 이를 위해서는 기존 컬랙션에 어떤 element 들이 있는지 알아야하기 때문이다. 따라서 element 를 add 하는 행위는 컬랙션 래퍼의 초기화를 유발한다.
그리고 정상적으로 쓰기지연 처리가 되는 모습을 확인할 수 있다.
cascade 옵션을 너무 기계적으로 써왔는데 이번 기회에 자세히 개념적으로 알아봤다.
/**
* (Optional) The operations that must be cascaded to
* the target of the association.
* <p> Defaults to no operations being cascaded.
*
* <p> When the target collection is a {@link java.util.Map
* java.util.Map}, the <code>cascade</code> element applies to the
* map value.
*/
CascadeType[] cascade() default {};
관계 정보를 표현하는 어노테이션의 속성 중 하나인데 주석 내용과 같이 target of association 에 operation이 이어지게(영향이 가도록) 할 것인지에 관한 것이다. 따로 설정해주지 않으면 기본적으로 아예 target of association 에 아무런 operation이 닿지 않는다.
(cascade 자체가 사전에서 '작은 폭포', '폭포처럼 흐르다' 와 같은 뜻을 가지고 있다.)
주의 해야할 점이 있는데 cascade 설정을 잘 해놓는다고 해도 관계의 주인 entity 에 실질적으로 참조를 할당 해놓지 않으면 데이터베이스 상에 외래키가 들어가지 않는다.
REMOVE 에 대해서만 잠깐 보충 설명을 하자면, parent 가 삭제 될때 cascade로 REMOVE 가 설정되어있지 않은 경우 외래키로 참조하고 있는 다른 테이블의 row 가 존재할 경우 해당 entity 가 삭제되지 않는다. 이는 특별히 다른게 아니라 원래 데이터베이스 상에서 참조하고 있는 row 가 있다면 delete 할 수 없는 상황 그대로인 것이다.
그런데 이 상황에서 cascade로 REMOVE 를 설정해주면 '참조하고 있는 것이 있다면 그것부터 delete 처리를 해줘서 궁극적으로 해당 entity 를 삭제할 수 있게' 알아서 처리해준다. 아래 로그를 보자.
@Override
@Transactional
public void run(ApplicationArguments args) {
final Team team = this.entityManager.find(Team.class, 1L);
this.entityManager.remove(team);
}
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
Hibernate:
select
members0_.team_id as team_id3_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.name as name2_0_1_,
members0_.team_id as team_id3_0_1_
from
member members0_
where
members0_.team_id=?
Hibernate:
delete
from
member
where
id=?
Hibernate:
delete
from
member
where
id=?
Hibernate:
delete
from
team
where
id=?
조회해서 삭제를 하는 로직인데 로그상에서 확인할 수 있듯이 이를 삭제하기 위해서 참조하고 있는 row 들을 가져와서 먼저 delete 를 수행하는 것을 볼 수 있다. cascade 로 REMOVE를 설정해주지 않으면 참조하고 있는 row 가 존재하는 경우 외래키 참조 에러가 발생한다.
고아객체
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, orphanRemoval = true)
private List<Member> members = new ArrayList<>();
부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다. 기본값이 false 이다. 컬렉션에서만 지우면 자식 엔티티에 대해서 delete 문이 수행된다.
@Override
@Transactional
public void run(ApplicationArguments args) {
final Team team = this.entityManager.find(Team.class, 1L);
final Member member = this.entityManager.find(Member.class, 1L);
team.getMembers().remove(member);
this.entityManager.merge(team);
}
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
Hibernate:
select
member0_.id as id1_0_0_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_
from
member member0_
where
member0_.id=?
Hibernate:
select
members0_.team_id as team_id3_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.name as name2_0_1_,
members0_.team_id as team_id3_0_1_
from
member members0_
where
members0_.team_id=?
Hibernate:
delete
from
member
where
id=?
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
조금 이상한 점이 책에서는 cascade 를 아무것도 지정해주지 않은 것처럼 나와 있으나, 아무것도 지정해주지 않으니 아예 동작을 하지 않았다. 하나하나 cascade 조합을 바꿔가면서 테스트 해봤는데 delete 는 아래와 같이 cascade 에 persist 가 포함되어야만 동작했다.
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, orphanRemoval = true, cascade = {CascadeType.PERSIST})
private List<Member> members = new ArrayList<>();
부모 객체에서 해당 컬랙션에 무엇인가 operation 을 했고 이것을 '영속화' 할 권한을 주는 것(= 해당 컬렉션에 가해진 operation 이 target entity 까지 cascade 되도록 설정) 하는 것이 필요하니까 그런 것 같다.
오히려 REMOVE 만 설정해두니 delete 가 발생하지 않았다. 착각하면 안되는 것이 cascade 는 결국 operation 이 전파되도록 할것이냐, 어떤 operation 들이 전파되도록 할 것이냐를 설정하는 것이고 이 operation 이라는 것은 해당 association 에 발생하는 것이 아니라 해당 부모 entity 에 발생하는 operation 을 의미한다.
그런 의미에서 고아객체를 설정하고 부모를 삭제하면 참조를 잃은 자식 객체들도 같이 삭제가 될 것이고, 이는 곧 cascade 에서 REMOVE 를 설정한 것과 동일한 의미 및 효과가 있다. 아래 코드를 확인하자.
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY, orphanRemoval = true)
private List<Member> members = new ArrayList<>();
@Override
@Transactional
public void run(ApplicationArguments args) {
final Team team = this.entityManager.find(Team.class, 1L);
this.entityManager.remove(team);
}
Hibernate:
select
team0_.id as id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.id=?
Hibernate:
select
members0_.team_id as team_id3_0_0_,
members0_.id as id1_0_0_,
members0_.id as id1_0_1_,
members0_.name as name2_0_1_,
members0_.team_id as team_id3_0_1_
from
member members0_
where
members0_.team_id=?
Hibernate:
delete
from
member
where
id=?
Hibernate:
delete
from
team
where
id=?
고아객체 사용시 주의사항
실무에서 고아객체를 사실 사용할 일이 없긴 했는데 책에서 주의사항을 알려주고 있고 내용이 합리적인 것 같아서 정리를 한다. 결국 고아객체를 설정한다는 것은 부모객체에 그만큼 권한을 주는 것인데 그러기 위해서는 부모 객체가 자식 객체의 라이프 사이클에 대해서 온전한 권한을 가지고 있을때만 허용해줘야한다.
만약 삭제된 자식 객체를 다른 객체가 사용해야한다면 이 객체의 동의 없이 마음대로 부모 객체가 지워버리는 것이 되기 때문이다.