[JPA] 프록시? 즉시로딩? 지연로딩?!?

드코딩·2024년 8월 15일
0

JPA

목록 보기
4/4
post-thumbnail

프록시

JPA를 사용하면 객체는 연관관계가 있는 다른 객체를 필드로 가져 참조하는 방식으로 관계를 형성한다.

특징

  • 프록시 객체는 처음 사용할 때 한 번만 사용함
  • 프록시 객체를 초기화할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다.
    초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속받음 , 따라서 타입 체크 시 주의해야 한다. (== 비교가 아닌 instanceof 사용)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때 , 프록시를 초기화하면 문제 발생 could not init proxy error, Lazyinit error 등등
    • 프록시 인스턴스의 초기화 여부 확인 PersistenceUnitUtil.isLoaded(Object entity)
    • 프록시 클래스 확인 방법 entity.getClass().get

프록시와 식별자

엔티티를 프록시로 조회할 때 PK 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.

School school = em.getReference(School.class, "School1"); 
school.getId(); //초기화되지 않음

이미 PK를 가지고 있었기 때문에 프록시 객체는 실제 객체로 초기화되지 않는다.

🚨단 엔티티 접근 방식을 프로퍼티 (@Access(AccessType.PROPERTY)) 로 설정한 경우에만 초기화하지 않는다.

(@Access(AccessType.FIELD)) 로 설정하면 JPA는 getId() 메소드가 id 만 조회하는 메소드인지 다른 필드까지 활용하는 것인지 모르기 때문에 프록시 객체를 초기화한다.

프록시는 다음 코드처럼 연관관계를 설정할 때 유용하다.

Member member = em.find(Member.class, "member1");
Team team = em.getReference(Team.class, "team1"); // SQL을 실행하지 않음
member.setTeam(team);

연관관계를 설정할 때는 식별자 값만 사용하기 때문에 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있다.이러한 경우에는 엔티티 접근 방식을 필드로 설정해도 프록시를 초기화하지 않는다.

즉시 로딩과 지연 로딩

프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다.

  • 지연 로딩이 뭘까? 연관된 엔티티를 실제 사용할 때 조회하는 것이다. Member 객체가 Team을 가지고 있다면 단순 Member의 name이 필요해서 find() 했는데
    필요 없는 Team 객체까지 find() 하는 쿼리를 날린다면 추가 비용이 발생해서 아쉽기 때문에 이를 방지할 수 있다.
  • 즉시 로딩은 뭘까? 엔티티를 조회할 때 연관된 엔티티도 함께 조회 지연 로딩과는 다르게 Member 객체를 find() 해서 가져오는 시점에 Team 객체를 가져오는 쿼리를
    같이 날려서 바로 가져온다. (실무에서는 즉시 로딩 비추천이라고 한다. 이유는 추가적으로 공부해보자)

즉시 로딩

즉시 로딩(EAGER LOADING)을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.EAGER 로 지정하여 사용할 수 있다.

@Entity
public class Member {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
	private Team team;
}

위 예제 코드와 같이 코드를 작성하고 아래와 같이 get 요청을 해보자

Member member = em.find(Member.class, "member1");
Team team = member.getTeam();

이 코드는 EAGER 설정이 되어있기 때문에 회원을 조회하는 순간 팀도 함께 조회한다.
이때 회원과 팀 두 테이블을 조회해야 하기 때문에 쿼리를 2번 실행할 것 같지만, 대부분의 JPA 구현체는
즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용
하기 때문에 여기서는 회원과 팀을 조인해서
쿼리 한번으로 두 엔티티르 모두 조회한
다.

이후 member.getTeam() 을 호출하면 이미 로딩된 Team1 엔티티를 반환한다.

💡 NULL 제약조건과 JPA 조인 전략 NULL 제약조건을 조인하는 FK에 설정하지 않는다면 조인 성능이 상대적으로 떨어지는 외부조인 쿼리를 날린다. 하지만 NOT NULL 조건을 명시한다면 내부 조인으로 쿼리를 날려서 성능과 최적화에 더 유리하다. `@JoinColumn(nullable = true); NULL 허용` → 외부 조인 사용 `@JoinColumn(nullable = false); NULL 불허` → 내부 조인 사용 `@ManyToOne.optional = false` → 내부 조인 사용

지연 로딩

지연 로딩을 사용하려면 FetchType.LAZY 로 지정한다.

컬렉션에 FetchType.EAGER 사용 시 주의점

  • 컬렉션을 하나 이상 즉시로딩하는 것은 권장하지 않는다. 컬렉션과 조인한다는 것은 데이터베이스 테이블로 보면 일대다 조인이다. 일대다 조인은 결과 데이터가 다 쪽에 있는 수만큼 증가하게 되는데, 문제는
    서로 다른 컬렉션을 2개 이상 조인할 때 발생한다. 예를 들어 A 테이블을 N, M 두 테이블과 일대다 조인하면 SQL 실행 결과가 N 곱하기 M
    이 되면서 너무 많은 데이터를 반환할 수 있어 성능 저하 이슈가 발생할 수 있다.
  • 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다. 다대일 관계인 회원 테이블과 팀 테이블을 조인할 때 회원 테이블의 외래 키에 Not null 제약 조건을
    걸어두면 모든 회원은 팀에 소속되므로 항상 내부조인을 사용해도 된다. 하지만 반대로 팀 테이블에서
    회원 테이블로 일대다 관계를 조인할 때 회원이 한 명도 없는 팀을 내부 조인하면 팀까지 조회되지 않는
    문제가 발생한다. 데이터베이스 제약조건으로 이런 상황을 막을 수 없기 때문에 JPA는 일대다 관계를 즉시 로딩할 때 항상
    외부 조인을 사용한다.

💡 **추가적인 나의 생각**

예를 들어서 주문이라는 클래스는 두 개의 컬렉션을 가지고 있다고 가정하자.

그럼 주문 객체를 DB에서 가져올 때 (조회할 때) 항상 A 컬렉션의 N개와B 컬렉션의 M 개
를 모두 가져오기 때문에 N M 개를 가져와야 한다.
하지만 이 문제를 LAZY 설정으로 하면 해결할 수 있을까? N
M 개의 조회를 해야 한다는 사실은
바뀌지 않을 것이다. 하지만 Order 클래스의 컬렉션 정보가 필요한 게 아닌 순수 Order 객체의 필
드 값이 필요하다면 N * M 개를 조회하지 않기에 조금은 효율적이지 않을까 싶다.

같은 접근으로 외부조인 문제도 LAZY조건도 외부조인을 해야하는 것을 피할 수 없지만
필요할 때만 하는 방식으로 성능적인 면에서 더 유연하고 효율적이다.

N * M 을 해결하기 위해서는 각 개별 쿼리를 만들어주는 것으로 해결 가능할 것 같다.


참고
1. 인프런 JPA 기본편 - 김영한
2. 자바 ORM 표준 JPA 프로그래밍 - 김영한

⭐틀린 내용 수정,지적은 언제나 환영합니다.⭐

0개의 댓글

관련 채용 정보