객체는 객체 그래프로 연관된 객체들을 탐색한다.
그런데 객체가 데이터베이스에 저장되어 있으므로 연관된 객체를 마음껏 탐색하기는 어렵다.
JPA 구현체들은 이 문제를 해결하려고 프록시라는 기술을 사용한다.
프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회할 수 있다.
프록시의 특징
지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체를 생성하는데 이것을 프록시 객체, 프록시 클래스라고 한다.
프록시 객체는 실제 클래스를 상속받아서 만들어지므로 실제 클래스와 걷 모양이 같다. 따라서 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
프록시 객체는 실제 객체에 대한 참조(target)를 보관한다.
그리고 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
프록시 객체의 초기화
프록시 객체는 위 지연로딩 예제처럼 car.getBrand()가 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이것을 프록시 객체의 초기화라 한다.
- 프록시 객체는 처음 사용할 때 한 번만 초기화된다.
- 프록시 객체를 초기화한다고 프록시 객체를 통해서 실제 엔티티로 바뀌는 것은 아니다.
- 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.
- 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 프록시 객체가 아닌 실제 엔티티를 반환한다.
- 조기화는 영속성 컨텍스으의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다.
하이버네이트에서는 org.hibernate.LazyInitializationException 예외를 발생시킨다.
// getReference는 엔티티를 프록시 객체로 조회한다.
Car car = em.getReference(Car.class, 1);
transaction.commit();
em.close(); // 영속성 컨텍스트 종료
// org.hibernate.LazyInitializationException 예외 발생
Brand brand = car.getBrand(); // 준영속 상태 초기화 시도
프록시와 식별자
엔티티를 프록시로 조회할 때 식별자(PK) 값을 파라미터로 전달하는데 프록시 객체는 이 식별자 값을 보관한다.
Car car = em.getReference(Car.class, 1); // 식별자 값 1보관
car.getId(); // 초기화되지 않음
프록시 객체는 식별자 값을 가지고 있으므로 식별자 값을 조회하는 car.getId()를 호출해도 프록시를 초기화 하지 않는다.
단 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY))로 설정한 경우에만 초기화하지 않는다.
엔티티 접근 방식을 필드(@Access(AccessType.FIELD)로 설정하면 JPA는 getId() 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화 한다.
프록시는 다음 코드처럼 연관관계를 설정할때 유용하게 사용할 수 있다.
Car car = em.find(Car.class, 1);
Brand brand = em.getReference(Brand.class, "2"); // SQL을 실행하지 않음
car.setBrand(brand);
Brand 객체를 불러올때 프록시 객체로 불러와 Car의 Brand를 설정 해주면 Brand는 식별자를 기억하고 있으므로 데이터베이스 접근을 줄일 수 있다.
프록시 확인
프록시를 사용하다보면 객체가 엔티티인지 프록시인지 햇갈릴때가 있는데 이를 매우 조심해야 한다.
JPA가 제공하는 PersistenceUnitUtil.isLoaded(Obejct entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다.
boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity);
System.out.println("isLoad = " + isLoad);
프록시 객체의 클래스명을 직접 출력해보면 ..javassis..라고 출력되는데 이를 확인해도 프록시 객체를 구분할 수 있다.
프록시 강제 초기화
JPA 표준에서는 프록시 가엦 초기화 메소드가 없기 때문에 하이버네이트의 initialize() 메소드를 사용하면 프록시를 강제로 초기화할 수 있다.
org.hibernate.Hibernate.initialize(order.getMember()); // 프록시 초기화
프록시를 사용한 지연로딩과 즉시로딩
자동차에 대한 엔티티와 차동차 브랜드란 엔티티를 예를 들어보자
자동차는 브랜드가 무조건 있기 때문에 자동차에 브랜드가 매핑되어 있을것이다.
만약 아반테의 크기(소형, 준중형, 중형, 대형 등)를 알고싶다고 했을때 굳이 브랜드를 찾지 않아도 된다.
굳이 현대(브랜드) 아반테(자동차명)를 검색하는것이 아닌 아반테(자동차명)만 검색해도 크기를 알 수 있다.
이럴때 브랜드를 조인해서 가져오는것이 아닌 차동자 정보만 가져오고 나중에 브랜드가 알고싶을때 브랜드를 찾아서 정보를 확인할 수 있도록 프록시를 사용한다.
처음부터 불필요한 정보를 모두 불러오는 것이 아닌 실제 필요할 경우에만 정보를 불러올 수 있도록 해준다.
이런 경우를 JPA에서는 지연로딩(Lazy Loading)이라고 하고,
처음부터 정보를 모두 불러오는 것, 즉 조인을 해서 불러오는것을 즉시로딩(Eager Loading) 이라고 한다.
아래에서 지연로딩, 즉시로딩 매핑 예제를 확인해보자
즉시 로딩(Eager Loading)
연관된 엔티티를 SQL 조인을 사용해서 한 번에 조회한다.
feach 속성을 FetchType.EAGER로 설정하면 된다.
@Entity
public class Brand {
private String brandName;
}
@Getter
@Entity
public class Car {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "BRAND_ID")
private Brand brand;
private String carSize;
private String carName;
}
// SELECT * FROM car JOIN brand ON car.brand_id = brand.brand_id WHERE car_id = 1;
Car car = em.find(Car.class, 1);
Brand brand = car.getBrand();
지연로딩(Lazy Loading)
지연 로딩은 연관된 엔티티를 프록시로 조회한다.
프록시 객체를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
feach 속성을 FetchType.LAZY로 설정하면 된다.
@Entity
public class Brand {
private String brandName;
}
@Entity
public class Car {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "BRAND_ID")
private Brand brand;
private String carSize;
private String carName;
}
// SELECT * FROM car WHERE car_id = 1;
Car car = em.find(Car.class, 1);
// SELECT * FROM brand WHERE brand_id = 2;
Brand brand = car.getBrand();
추전하는 방법은 모든 연관관계에 지연 로딩을 사용하고 개발이 어느정도 끝났을때 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하면 된다.
컬렉션에 즉시로딩(EAGER) 사용 시 주의점
컬렉션과 조인한다는 것은 1대N 조인이기 때문에 결과 데이터가 N쪽에 있는 수 만큼 증가하게 된다.
문제는 서로 다른 컬렉션을 2개 이상 조인할 때 발생한다.
예를 들어 A테이블이 B테이블, C테이블과 1대N 조인을 하게 되면 SQL 엄청나게 많은 SQL이 실행(B 데이터 수 x C 데이터 수)되어 성능 저하가 올 수 있다.
따라서 2개 이상의 컬렉션을 즉시 로딩으로 설정하는 것은 권장하지 않는다.
그리고 컬렉션 즉시 로딩은 항상 외부 조인(OUTER JOIN)을 사용해야 한다.
예를 들어 N대1 관계인 Car테이블과 Brand테이블을 조인할 때 Car테이블의 외래 키에 NOT NULL 제약조건을 걸어두면 모든 자동차는 브랜드가 있으므로 항상 내부 조인을 사용해도 된다.
반대로 Brand테이블에서 Car테이블로 1대N 관계를 조인할 때 자동차가 한개도 없는 브랜드를 조회하면 브랜드까지 조회가 되지 않는다.
따라서 JPA는 1대N 관계를 즉시 로딩할 때 항상 회부 조인을 사용해야 한다.
'JPA' 카테고리의 다른 글
고아 객체 제거: orphanRemoval (0) | 2022.01.11 |
---|---|
영속성 전이: CASCADE (0) | 2022.01.10 |
조인 테이블 매핑 (0) | 2022.01.05 |
복합 키와 식별, 비식별 관계 매핑 (0) | 2022.01.04 |
@MappedSuperclass를 이용한 객체 상속 (0) | 2021.12.31 |