본문 바로가기
Spring/JPA

#8 프록시와 연관관계 관리

by 히포파타마스 2021. 8. 29.

프록시와 연관관계 관리

 

● 프록시와 즉시 로딩, 지연 로딩

객체는 객체 그래프로 연관된 객체들을 탐색한다. 

그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다.

JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다.

 

프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회할 수 있다.

 

● 영속성 전이와 고아 객체

JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체 제거라는 편리한 기능을 제공한다.

 

 

 

 

1. 프록시

[Member와 Team]

위 예제와 같이 Member가 Team을 필드로 갖고 있다고 하자.

 

만약 Member를 데이터 베이스에서 조회하면 이때, Team도 조회해야 할까?

 

예를 들어, Member를 조회했지만 team은 사용하지 않고 다른 필드만 조회하는 경우도 있을 것이다.

이런 상황에서 Member를 조회할 때 항상 team도 동시에 조회하는 것은 필요 없는 SQL을 발생시키므로 비효율적이다.

 

JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다.

그런데 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 바로 이것을 프록시 객체라 한다.

※ JPA 표준 명세는 지연 로딩의 구현 방법을 JPA 구현체에 위임했다. 프록시 방식은 하이버네이트 구현체에 대한 내용이다.

 

 

 

■ 프록시 기본 원리

JPA에서 식별자로 엔티티 하나를 조회할 때는 find()를 사용한다.

이 메서드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.

이 방식으로 엔티티를 직접 조회하면 엔티티를 실제 사용하든 하지 않든 데이터베이스를 조회하게 된다.

 

엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶다면 EntityManager.getReference() 메서드를 사용하면 된다.

이 메서드를 호출할 때 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다.

대신에 데이터베이스 접근을 위임한 프록시 객체를 반환한다.

 

[프록시 반환]

 

프록시는 실제 클래스를 상속 받아서 만들어지기 때문에 실제 클래스와 겉 모양이 같다.

이 때문에 사용자 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.

 

 

프록시 객체는 실제 객체의 참조(target)을 보관한다.

그리고 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다.

 

[프록시의 참조 방식]

 

 

□ 프록시 객체의 초기화

프록시 객체는 member.getName() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이를 프록시 객체의 초기화라고 한다.

 

[프록시 초기화 방식]

 한

● 프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다.

● 프록시 객체는 실제 엔티티가 생성되어있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청한다(초기화).

● 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.

● 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 보관한다.

● 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.

 

 

 

■ 프록시의 특징

● 프록시 객체는 처음 사용할 때 한 번만 초기화 된다.

 

● 프록시 객체를 초기화 한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.

 

● 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getReference() 를 호출해도 프록시가 아닌 실제 데이터를 반환한다.

 

● 초기화는 영속성 컨텍스트의 도움을 받아야한다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.

 

 

□ 프록시 객체 타입 체크

프록시 객체는 어디까지나 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.

ex) em.getReference()로 member를 조회했을 때, 찾아진 member는 원래 member 데이터와 같지 않다(프록시 객체이기 때문).

 

때문에 프록시를 사용할 때는 equal()과 hashcode()를 오버라이드로 재정의 해주어야 동일성이 인정된다.  

 

 

 

 

2. 즉시 로딩과 지연 로딩

■ 즉시 로딩

엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 방식을 뜻한다.

 

예를 들어, Member를 조회하면 동시에 Team도 한 번에 조회된다.

 

[즉시 로딩 설정 - FetchType.EAGER]

@Entity
public class Member {
	@Id @GeneratedValue 
	private Long id;

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

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

@ManyToOne의 fetch 속성을 FetchType.EAGER로 지정하면 즉시 로딩이 사용된다.

 

즉시 로딩을 사용하면 JPA 구현체는 되도록이면 조인을 사용해서 SQL 한번에 함께 조회하려 한다.

 

※ @ManyToOne과 @OneToOne은 기본 fetch 전략이 EAGER이다.

 

 

□ 즉시 로딩 주의사항

● 가급적 지연 로딩만 사용(특히 실무)

 

● 즉시 로딩을 적용하면 예상치 못한 SQL이 발생한다.

 

● 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

ex) 엔티티를 조회하면 연관된 모든 엔티티가 한번에 조회된다.

 

● @ManyToOne과 @OneToOne은 기본이 즉시 로딩이기 때문에 지연 로딩으로 변경해줄 필요가 있다.

 

 

 

■ 지연 로딩

연관된 엔티티를 실제 사용할 때 조회하는 방식이다.

 

엔티티를 실제 사용하는 시점에 JPA가 SQL을 호출해서 엔티티를 조회한다.

 

[지연 로딩 설정 - FetchType.LAZY]

@Entity
public class Member {
	@Id @GeneratedValue 
	private Long id;

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

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

@ManyToOne의 fetch 속성을 FetchType.LAZY로 설정하면 지연 로딩이 적용된다.

 

지연 로딩을 적용하면 엔티티를 조회할 때 데이터베이스를 조회하지 않고 프록시 객체를 반환한다.

 

 

 

 

3. 영속성 전이 : CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶다면 영속성 전이를 사용하면 된다.

 

[Parent, Child 관계]

예제와 같이 Parent와 Child의 관계가 일대다라고 할 때 Parent에 영속성 전이를 적용할 수 있다.

이 경우, Parent만 영속 상태로 만들면 관련된 Child 모두 영속 상태로 만들 수 있다.

※ 일대다 관계도 영속성 전이를 사용할 수 있다.

 

[CASCADE 활용]

@OneToMany와 같은 연관관계를 나타내는 어노테이션에 cascade=CascadeType.PERSIST 를 사용하면 영속성 전이가 적용된다.

Parent를 영속화하면 Parent의 childeren에 포함된 child들도 영속화된다.

 

CASCADE는  PERSIST 외에 부모를 제거할 때 자식도 제거되는 REMOVE, PERSIST와 REMOVE를 둘 다 적용하는 ALL 도 있다.

 

※ 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다. 단지 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공해주는 것뿐이다.

 

 

 

 

4. 고아 객체

JPA에서는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제할 수 있다.

 

CASCADE 속성과 같이 연관관계를 나타내는 어노테이션에 orphanRemoval = true 를 적용하면 고아 객체가 자동으로 제거된다.

[고아 객체 제거 활용]

 

Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(0);
//자식 엔티티를 컬렉션에서 제거

parant에는 orphanRemoval = true 가 적용되어있다.

parent의 Children에 child가 저장되어있다고 했을 때, 위의 예제와 같이 children에서 child를 제거하면 parent와 child의 관계가 끊기게 된다.

이때, orphanRemoval = true 속성이 적용되어있기 때문에 DELETE SQL이 나가서 고아 객체가 된 child를 데이터베이스에서 삭제한다. 

 

 

 

■ 고아 객체 주의점

● 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.

◎ 때문에 참조하는 곳이 하나일 때 사용해야 한다.

 

● 특정 엔티티가 개인 소유할 때 사용되는 것이 권장된다.

 

● @OneToOne, @OneToMany만 사용 가능하다.

 

● 부모가 제거되면 이는 자식과 연결이 끊긴 것이기 때문에 자식도 함께 제거된다.

◎ 이는 CascadeType.REMOVE와 동일하게 동작한다.

 

 

 

■ 영속성 전이 + 고아 객체, 생명주기

스스로 생명주기를 관리하는 엔티티는 개별적으로 persist()나 remove()로 제거하면 된다.

 

CascadeType.ALL과 orphanRemovel=true을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있다.

ex) 부모를 영속, 제거함에 따라 자식도 영속, 제거되고 부모와 관계가 끊기면 자식은 삭제된다.

'Spring > JPA' 카테고리의 다른 글

#10 객체지향 쿼리 언어  (0) 2021.09.01
#9 값 타입  (0) 2021.08.30
#7 고급 매핑  (0) 2021.08.28
#6 다양한 연관관계 매핑  (0) 2021.08.28
#5 연관관계 매핑 기본  (0) 2021.08.27

댓글