1. 개요
객체는 객체 그래프로 연관된 객체를 탐색하게 됩니다. 하지만 객체가 데이터베이스에 저장되어 있으므로 연관된 자유롭게 객체를 탐색하기가 어렵습니다. 이를 해결하기 위해 JPA 구현체는 프록시 기술을 사용합니다. 이 기술을 통해 연관된 객체를 처음부터 데이터베이스에서 조회하는 대신 실제 사용 시점에 조회할 수 있습니다.
또한, 함께 사용하는 객체들은 조인을 통해 함께 조회하는 것이 효과적인데 JPA는 이를 위해 즉시로딩과 지연 로딩이라는 두 가지 로딩 전략을 지원합니다. 그렇다면 지금부터 프록시, 지연로딩, 즉시로딩이 무엇인지 알아보도록 하겠습니다.
2. 프록시란?
프록시란 ' 대신하다' 라는 의미를 가지고 있습니다. 그렇다면 JPA의 프록시 기술은 무엇일까요?
프록시는 지연 로딩의 경우 사용하게 됩니다. 지연 로딩은 연관된 엔티티를 실제 사용하는 시점에 조회할 수 있는 기술로, 이 때 실제 엔티티 객체 대신에 데이터 베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이 것을 프록시 객체라고 합니다.
프록시 클래스는 실제 클래스를 상속 받아 만들어 집니다. 프록시 객체는 실제 객체에 대한 참조를 가지고 있기에 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출하게 됩니다.
엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하는데 프록시는 식별자 값을 가지고 있어 getId()를 호출해도 프록시를 초기화하지 않는다.(@Access(AccessType.PROPERTY)) 경우만 해당함
2-1 프록시 초기화
프록시 객체는 실제 사용될 때 데이터베이스를 조회해 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라고 합니다.
프록시 초기화 과정
1. 실제 사용을 위해 메서드를 호출합니다.
2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청합니다(초기화)
3. 영속성 컨텍스트는 데이터베이스를 조회해 실제 엔티티 객체를 생성합니다.
4. 프록시 객체는 생성된 실제 엔티티의 참조를 보관합니다.
5. 프록시 객체는 참조를 통해 실제 엔티티 객체의 메소드를 호출해 결과를 반환합니다.
2-2 프록시의 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화됩니다.
- 프록시 객체 초기화 시 실제 엔티티로 바뀌는 것이 아닌 프록시 객체를 통해 실제 엔티티에 접근할 수 있습니다.
- 영속성 컨텍스트에 엔티티가 이미 있으면 데이터베이스 조회가 필요 없기 때문에 프록시가 아닌 실제 엔티티를 반환합니다.
- 영속성 컨텍스트의 도움을 받아야 초기화가 가능하기 때문에 준영속 상태의 프록시를 초기화하면 문제가 발생합니다.
3. 즉시 로딩
즉시로딩은 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 방법입니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
//~~~~~
}
즉시 로딩을 사용하기 위해 fetch 속성을 FetchType.EAGER로 지정해줍니다. 이 후 회원을 조회해 보겠습니다.
회원을 조회한 순간 팀도 함께 조회 되는걸 볼 수 있습니다. 이 때 대부분의 JPA 구현체는 즉시로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용합니다. 또한 INNER JOIN이 아닌 LEFT OUTER JOIN을 사용하는 것을 볼 수 있는데 Null의 가능성 때문입니다.
외부 조인보다 내부 조인이 성능과 최적화에서 더 유리하기 때문에 내부 조인을 사용하기 위해서 외래키에 NOT NULL 제약 조건을 설정해주면 JPA는 외부 조인 대신 내부 조인을 사용하게 됩니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id", nullable = false)
private Team team;
//~~~~~
}
NOT NULL 제약조건을 추가해준 후 Member을 다시 조회해보면 위와 같이 INNER JOIN을 사용하는 것을 볼 수 있습니다.
4. 지연 로딩
지연로딩은 연관된 엔티티를 프록시로 조회하는 방법으로 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회합니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
//~~~~~
}
지연로딩을 사용하기 위해 FetchType을 LAZY로 변경해줍니다.
이 후 멤버를 조회해 보겠습니다.
즉시 로딩과는 다르게 회원만 조회하고 팀은 조회하지 않는 모습을 볼 수 있습니다.
대신에 팀 엔티티는 프록시로 조회하게 됩니다.
// team은 프록시 객체입니다.
System.out.println("Team class: " + member.getTeam().getClass());
이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미루게 되고 실제 데이터가 필요한 순간이 되어서야 데이터베이스를 조회해 프록시 객체를 초기화 합니다.
// 팀 이름에 접근하는 순간 실제 DB에서 데이터를 로딩합니다.
System.out.println("Team name: " + member.getTeam().getName());
위 실행 결과를 보면 실제 데이터가 필요한 순간 팀을 조회하는 것을 볼 수 있습니다.
5. 마무리
JPA의 프록시와 지연로딩, 즉시 로딩 전략은 데이터베이스와 애플리케이션 간 성능을 최적화하는 데 중요한 역할을 합니다. 각각의 전략의 장단점을 잘 이해하고 상황에 맞게 사용하는 것이 중요합니다.
- 지연 로딩은 사용하면 초기 로딩 시간이 단축되고 메모리 사용을 줄일 수 있지만 프록시 객체 접근시 예상치 못한 LazyInitializationException이 발생할 수 있어 데이터 베이스 연결이 유지되는 범위 내에서 접근하도록 주의해야 합니다.
- 즉시 로딩은 필요한 데이터를 한 번에 가져와 편리하지만 연관된 모든 데이터를 한 번에 로딩하기에 불필요한 성능 저하를 초래할 수 있습니다.
추천하는 방법은 기본적으로 지연로딩을 사용하되 실제 사용 상황에 따라 필요한 곳에서 즉시 로딩을 적용해 최적화를 진행하는 것을 추천합니다.
참고
자바 ORM 표준 JPA 프로그래밍
'Backend > JPA' 카테고리의 다른 글
[JPA] 트랜잭션 범위의 영속성 컨텍스트 (0) | 2024.06.18 |
---|---|
[JPA] Spring Data JPA란? (0) | 2024.06.12 |
[JPA] 영속성 컨텍스트 (0) | 2023.07.24 |
[JPA] JPA란 무엇일까? (0) | 2023.07.17 |
[JPA] @JpaDataTest에서 Auditing 기능 사용하기 (0) | 2023.06.23 |