[JPA] 6.프록시와 연관관계 관리
Updated:
프록시와 연관관계 관리
JPA에서 어려운 개념 중 하나인 프록시 및 프록시를 사용하는 즉시, 지연로딩에 대해 알아본다. 또한, CASCADE를 이용한 영속성 전이와 고아객체에 대해 살펴본다
배경지식
아래의 예시를 통해 프록시가 도입된 배경에 대해 알아보자

- 비즈니스 로직에 따라 Member 조회 시에 Team 정보가 필요할 수도 있고 필요없을 수도 있다
- JPA는 프록시를 이용한 지연로딩, 즉시로딩으로 위 문제를 해결한다
1. 프록시
1-1. 개념
간단히 말하자면 실제 엔티티를 상속받은 가짜 엔티티 객체이다
우선 아래의 간단한 코드를 통해 살펴보자
Member realFindMember = em.find(Member.class, 1L);
Member proxyFindMember = em.getReference(Member.class, member.getId());
System.out.println("proxyFindMember = " + proxyFindMember.getClass());
em.find()는 DB로부터 실제 엔티티 객체를 조회한다em.getReference()는 DB 조회를 미루고 프록시 엔티티 객체를 조회한다- 실제로
getClass()를 통해 출력된 것은Member$HibernateProxy처럼 프록시 객체이다
- 실제로

- 프록시 객체는 실제 객체를 상속받아서 만들어지므로 실제 클래스와 형태가 같다
- 프록시 객체는 실제 객체의 참조값을 가진다
- 프록시 객체의 메서드 호출 시에 참조값을 통해 실제 객체에 접근하여 메서드를 호출한다
- 단, 초기화 전까지 참조값은 null이다
Member proxyFindMember = em.getReference(Member.class, member.getId()); //프록시 객체 조회
proxyFindMember.getName(); //프록시 객체와 실제 엔티티 연결

- 위 코드에서
getReference()를 통해 가져온 프록시 객체에는target값에 null이 들어있다 - 프록시 객체에서 메서드 호출 시에 영속성 컨텍스트로 초기화 요청을 하게 된다. 영속성 컨텍스트는 DB에 접근하여 실제 엔티티를 가져와 프록시 객체의
target값에 넣어준다
1-2. 특징
-
프록시 객체는 처음 초기화된 이후 다시는 초기화되지 않는다
-
초기화 시에 프록시 객체가 실제 엔티티로 바뀌는 것이 아니라 참조값에 실제 엔티티 값이 들어가는 것이다
- 따라서 타입 체크시에
==비교 대신instance of를 사용해야 한다
- 따라서 타입 체크시에
-
JPA는 동일한 PK값을 통해 조회한 객체는 == 연산자를 통해 비교했을 때 반드시 true 값이 나오도록 만든다
Member reference = em.getReference(Member.class, member.getId()); System.out.println("reference = " + reference.getClass()); Member findMember = em.find(Member.class, member.getId()); System.out.println("findMember = " + findMember.getClass()); System.out.println("reference == findMember: " + (reference == findMember)); //true- 위 코드에서
em.getReference()는 프록시 객체를 반환하는 것이 당연하다 em.find()를 통해서 실제 엔티티 객체를 반환한다고 생각하기 때문에 2개의 객체를 비교했을때false값이 나올 거라 예상하는 것이 일반적이다- 하지만, JPA는 동일한 PK로 조회한 객체는
==비교 시에 항상true값이 나오게 만들기 때문에em.find()는 프록시 객체를 반환하도록 한다
- 위 코드에서
- 위와 동일한 이유로 조회하는 엔티티가 영속성 컨텍스트(1차 캐시)에 이미 존재하면 em.getReference()는 프록시가 아닌 실제 엔티티 객체를 반환한다
- 영속성 컨텍스트의 지원을 받지 않는 준영속 상태에서는 프록시 초기화 시에 문제가 발생한다
org.hibernate.LazyInitializationException예외가 발생한다- 실무에서 정말 많이 발생하는 에러이니 알아두자!!!
2. 즉시 로딩과 지연 로딩
2-1. 지연 로딩
Member 조회 시에 Team까지 조회되는 것을 방지하고 싶다면 아래와 같이 Member 엔티티 코드를 수정하면 된다
@Entity
@Getter @Setter
public class Member {
...
@ManyToOne(fetch = FetchType.LAZY) //수정부분
@JoinColumn(name = "team_id")
private Team team;
...
}
이제 테스트 코드를 살펴보자
@Test
@Rollback(value = false)
@Transactional
void proxyTest() {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("lim");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getTeam().getClass() = " + findMember.getTeam().getClass());
System.out.println("==================================");
System.out.println("findMember.getTeam().getName() = " + findMember.getTeam().getName());
System.out.println("==================================");
}
- Member의 Team 필드값을 지연 로딩으로 설정했으므로
em.find()로 Member 조회 시에 Team은 조회하지 않는다- 실제로 출력을 보면 조인을 사용한 Team 조회를 수행하지 않는다
- Team 엔티티는 Member 조회 시에 프록시로 가져온 후 실제 Team 엔티티 사용 시점에 초기화하여 사용한다
- Team 엔티티를 가져오는 시점이 아닌 Team 내부의 무언가를 실제로 사용할 때 초기화한다
- 실제로 출력을 보면
findMember.getTeam().getName()호출 시에 영속성 컨텍스트가 DB에 실제 엔티티를 조회하는 SQL문이 수행된다
2-2. 즉시 로딩
Member 조회 시에 Team까지 한번에 조회하고 싶다면 아래와 같이 Member 엔티티 코드를 수정하면 된다
@Entity
@Getter @Setter
public class Member {
...
@ManyToOne(fetch = FetchType.EAGER) //수정부분
@JoinColumn(name = "team_id")
private Team team;
...
}
- Member의 Team 필드값을 즉시 로딩으로 설정했으므로
em.find()로 Member 조회 시에 조인하여 Team까지 가져온다 - 프록시를 사용하지 않고 Member 조회 시에 Team까지 실제 엔티티로 가져온다
2-3. 실무상 주의점
- 모든 연관관계에서 지연 로딩을 사용하자!!!
- 즉시 로딩은 JPQL에서 N + 1 문제를 유발한다
- 해결책(지연 로딩 사용을 전제)
- JPQL fetch 조인
- 엔티티 그래프
- 해결책(지연 로딩 사용을 전제)
- @ManyToOne, @OneToOne은 default가 즉시 로딩이므로 지연 로딩으로 바꿔주자!!!
@OneToMany,@ManyToMany는 default가 지연 로딩이다
3. 영속성 전이: CASCADE
앞의 프록시나 즉시 로딩, 지연 로딩과는 관계없는 부분이다. 특정 엔티티를 영속 상태로 만드는 상황에서 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다
3-1. 개념
실제 코드를 통해 알아보자. 우선 Parent와 Child 엔티티 코드를 살펴보자
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
}
@Entity
@Getter @Setter
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
public void setParent(Parent parent) {
this.parent = parent;
parent.getChildList().add(this);
}
}
- Parent와 Child가 일대다 관계를 가진다
- 일대다 관계에서 다쪽인 Child가 연관관계의 주인이다
- 연관관계 편의 메서드를 이용하여 Parent와 Child 객체 모두 참조값을 넣어주었다
- 추가적으로,
CascadeType.ALL옵션을 통해 Parent를 영속 상태로 만들면 자동으로 Child 또한 영속 상태가 되도록 설정하였다
다음은 테스트 코드이다
@Test
@Transactional
@Rollback(value = false)
void cascadeTest() {
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
child1.setParent(parent);
child2.setParent(parent);
em.persist(parent);
}
CascadeType.ALL옵션을 사용한다면, 위와 같이em.persist()를 통해 Parent만 영속 상태로 만들어주면 자동으로 Child도em.persist()가 수행되어 영속 상태가 된다em.persist(parent)만으로em.persist(child1),em.persist(child2)모두 수행한 결과를 만들어낼 수 있다
3-2. 종류
종류는 많지만 실제로는 아래 2가지를 주로 사용한다
- ALL: 모든 LifeCycle에 적용
- PERSIST: 저장 시에만 적용
3-3. 주의점
- CASCADE를 통해 자동으로 영속 상태로 만들고자 하는 엔티티가 다른 엔티티 하나랑만 연관관계에 있을 경우에만 사용하는 것이 좋다
- CASCADE를 적용할 엔티티가 중요한 도메인이어서 다른 엔티티들과 많은 연관관계를 맺고 있을 경우에는 사용하지 않는 것이 좋다
4. 고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티는 자동으로 삭제해주는 기능을 제공한다
4-1. 개념
아래 코드를 통해 살펴보자
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
}
- Child 엔티티는 변화가 없고 Parent 엔티티에
orphanRemoval = true속성을 추가하였다
다음은 테스트 코드이다
@Test
@Transactional
@Rollback(value = false)
void cascadeTest() {
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
child1.setParent(parent);
child2.setParent(parent);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
}
- 맨 마지막줄 코드를 통해 Parent 엔티티와 연관관계가 끊었다
- Parent 엔티티와 연관관계가 끊어진 Child 엔티티는 자동으로 DELETE SQL문이 호출되어 삭제된다
4-2. 주의점
- CASCADE와 마찬가지로 해당 엔티티를 참조하는 곳이 하나일 때만 사용하는 것이 좋다
cascade = CascadeType.ALL옵션과orphanRemoval = true옵션을 함께 사용하면 부모 엔티티를 통해 자식의 LifeCycle을 관리할 수 있다
Leave a comment