본문 바로가기
ORM/JPA

[JPA] 프록시와 연관관계 관리

by 젊은오리 2023. 4. 24.
728x90

[김영한님의 자바 ORM 표준 JPA 프로그래밍을 학습 후 정리한 내용입니다.]

순서

  1. 프록시
  2. 즉시 로딩과 지연 로딩
  3. 영속성 전이(CASCADE)와 고아 객체

 

1. 프록시

엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다. 연관 관계의 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다.

JPA는 이러한 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데, 이것을 지연로딩이라고 한다. 그런데 지연로딩 기능을 사용하려면 실제 엔티티 객체 대상에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데, 이것을 프록시 객체라고 한다.

em.find() vs em.getReference()

  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 프록시 엔티티 객체 조회

 

프록시 특징

  • 실제 클래스를 상속 받아서 만들어졌기 떄문에 실제 클래스와 겉 모양이 같다.
  • 프록시 객체는 처음 사용할 때 한번만 초기화된다.
  • 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것이 아니다. 그저 실제 엔티티에 접근할 수 있는 것이다.
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있다면 em.getReferece()를 호출해도 실제 엔티티를 반환한다.
  • 준영속 상태의 프록시를 초기화하면 문제가 발생한다. (영속 상태만 가능하다.)

 

프록시 객체의 초기화 과정

 

2. 즉시 로딩과 지연 로딩

회원 엔티티를 조회할 때 연관된 팀 엔티티도 함께 데이터베이스에서 조회하는 것이 좋을까? 아니면 회원 엔티티만 조회해 두고 팀 엔티티는 실제 사용하는 시점에 데이터베이스에서 조회하는 것이 좋을까? 상황에 따라 다를 수 있기 때문에 정답은 없다. 연관된 엔티티를 조회하는 시점에 따라 2가지 방법이 있다.

  • 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다. @ManyToOne(fetch = FetchType.EAGER)
  • 지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회한다. @ManyToOne(getch = FetchType.LAZY)

Default패치 전략

  • @ManyToOne - EAGER
  • @OneToOne - EAGER
  • @OneToMany - LAZY
  • @ManyToMany - LAZY

 

즉시 로딩(Eager Loading)

즉시 로딩은 엔티티를 불러올 때 해당 엔티티와 연관된 테이블도 한번에 조회하는 방법이다. 아래 SQL 예시는 회원을 불러 올 때 연관된 팀 테이블도 조회하는 예시이다.

SELECT
    M.MEMBER_ID AS MEMBER_ID,
    M.TEAM_ID AS TEAM_ID,
    M.USERNAME AS USERNAME,
    T.TEAM_ID AS TEAM_ID,
    T.NAME AS NAME
FROM MEMBER M 
LEFT OUTER JOIN TEAM T
	  ON M.TEAM一ID=T.TEAM一ID
WHERE
    M.MEMBER_ID='member1'

현재 회원 테이블에 TEAM_ID 외래 키는 NULL 값을 허용하고 있다. 따라서 팀에 소속되지 않은 회원이 있을 가능성이 있다. 팀에 소속하지 않은 회원과 팀을 내부 조인 하면 팀은 물론이고 회원 데이터도 조회할 수 없다. 하지만 외부 조인보다 내부 조인이 성능과 최적화에서 더 유리하다. 그렇다면 내부 조인을 사용하려면 어떻게 해야 할까?

외래 키에 NOT NULL 제약 조건을 설정하면 값이 있는 것을 보장한다. NOT NULL을 표현하는 방법은 두 가지가 있다.

  • @JoinColumn(name = "TEAM_ID", nullable = false)
  • @ManyToOne(fetch = FetchType.EAGER, optional = false)

정리하자면 JPA는 선택적 관계면 외부 조인을 사용하고 필수 관계면 내부 조인을 사용한다.

 ※주의 - 즉시 로딩을 적용하면 예상치 못한 SQL이 발생한다. 또한 JPQL에서 N+1문제를 발생시키기 때문에 실무에서 가급적 지연 로딩을 사용해야 한다. 

 

지연 로딩(Lazy loading)

지연 로딩은 엔티티를 불러올 때 연관된 테이블은 프록시 객체로 불러오고, 실제 연관된 엔티티를 사용할 때 진짜 엔티티로부터 값을 가져오는 방법이다.

아래 그림과 같이 em.find로 member를 조회하면, Team엔티티를 프록시(가짜) 객체로만 불러오고, team.getName(); 시점에 프록시 초기화가 되어 DB로부터 값을 가져온다.

 

 

3. 영속성 전이(CASCADE)와 고아 객체

영속성 전이(CASCADE)는 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함꼐 영속 상태로 만들고 싶을 때 사용한다. ex)부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

[영속성 전이 : 저장]

@Entity
public class Parent {
	...

	@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
	private List<Child> children = new ArrayList<Child>();
}

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

CASCADE 종류

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : 리프레시
  • DETACH : 준영속

 

고아 객체

  • 고아 객체 제거 : 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능
  • orphanRemoval = true

[사용 예시]

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<Child>();
    ...
}
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거

※주의 - 참조하는 곳이 하나일 때 사용해야 한다. 즉, 특정 엔티티가 개인 소유일 때 사용해야 하며, @OneToOne, @OneToMany만 가능하다. 고아 객체 제거 기능을 활성화 하면(orphanRemoval = true) 부모를 제거할 때 자식도 함께 제거가 되므로 CascadeType.REMOVE처럼 동작한다.

정리

  • JPA 구현체들은 객체 그래프를 마음껏 탐색할 수 있도록 지원하는데 이때 프록시 기술을 사용한다.
  • 객체를 조회할 때 연관된 객체를 즉시 로딩하는 방법을 즉시 로딩이라 하고, 연관된 객체를 지연해서 로딩하는 방법을 지연 로딩이라 한다.
  • 객체를 저장하거나 삭제할 때 연관된 객체도 함께 저장하거나 삭제할 수 있는데 이것을 영속성 전이라 한다.
  • 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하려면 고아 객체 제거 기능을 사용하면 된다.
728x90

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

[JPA] 값 타입 정리  (0) 2023.04.26
[JPA] 다양한 연관관계 매핑  (1) 2023.04.24
[JPA] 연관관계 매핑 기초  (0) 2023.04.24
[JPA] 엔티티 매핑  (0) 2023.04.24
[JPA] OSIV 성능 최적화 정리  (0) 2023.04.18

댓글