Reference. 자바 ORM 표준 JPA 프로그래밍
책 목차 및 이전 글
더보기
들어가기 전 JPA 특징, Q&A
1. JPA 소개
3. 영속성 관리
4. 엔티티 매핑
4.1 - 4.3 @Entity, @Table, 다양한 매핑
4.4 - 4.5 데이터베이스 스키마 자동 생성, DDL 생성 기능
5. 연관관계 매핑 기초
6. 다양한 연관관계 매핑
7. 고급매핑
8. 프록시와 연관관계 관리
9. 값 타입
9.3~5 값 타입과 불변 객체, 값 비교, 값 타입 컬렉션
10. 객체지향 쿼리 언어
11. 웹 애플리케이션 제작
11.1 프로젝트 환경설정
11.2 도메인 모델과 테이블 설계
11.3 애플리케이션 구현
12. 스프링 데이터 JPA
12.1~3 스프링 데이터 JPA 소개, 공통 인터페이스 기능
12.5~10 명세, 사용자 정의 리포지토리, Web 확장...
13. 웹 애플리케이션과 영속성 관리
13.2 준영속 상태와 지연 로딩
- 스프링이나 J2EE 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용
- 보통 서비스 계층에서 시작하므로 서비스 계층과 함께 영속성 컨텍스트도 종료
- 서비스와 레포지토리 계층에선 영속성 컨텍스트에 관리
컨트롤러나 뷰같은 프리젠테이션 계층에서는 준영속 상태
@Entity public class Order { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) //지연 로딩 전략 pirvate Member member; //주문 회원 }
- 영속성 컨텍스트 전략을 사용하면 트랜잭션이 없는 프리젠테이션 계층은 준영속 상태 따라서, 감지와 지연로딩이 동작하지 않음
class OrderController { public String view(Long orderId) { Order order = orderService.findOne(orderId); Member member = order.getMember(); member.getName(); //지연 로딩 시 예외 발생 } }
준영속 상태와 변경 감지
- 변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층까지만 동작하고 영속성 컨텍스트가 종료된 프리젠테이션 계층에서는 동작하지 않음 (트랜잭션이 서비스 계층에 동작)
- 비즈니스 로직은 서비스 계층에서 끝내고 프리젠테이션 계층은 데이터를 보여주는데 집중 따라서 변경 감지 기능이 프리젠테이션 계층에 동작하지 않아도 특별한 문제는 없음
준영속 상태와 지연 로딩
- 준영속 상태의 가장 골치 아픈 문제는 지연 로딩 기능이 동작하지 않는다는 점
- 연관된 엔티티를 사용하려고 할 때 준영속 상태라 지연 로딩을 할 수 없는 문제가 발생
- 준영속 상태의 지연 로딩 문제를 해결하는 방법은 크게 2가지
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법 (어디서 미리 로딩하느냐에 따른 3가지 방법)
- 글로벌 페치 전략 수정
- JPQL 페치 조인(fetch join)
- 강제로 초기화
- OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법 (어디서 미리 로딩하느냐에 따른 3가지 방법)
13.2.1 글로벌 페치 전략 수정
- 가장 간단한 방법은 글로벌 페치 전략을 지연로딩에서 즉시 로딩으로 변경
@Entity public class Order { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.EAGER) //즉시 로딩 전략 private Member member; //주문 회원 ... }
프리젠테이션 로직
Order order = orderService.findOne(orderId); Member member = order.getMember(); member.getName(); //이미 로딩된 엔티티
- 엔티티 메니저로 주문 엔티티를 조회하면 연관된 member 엔티티도 항상 함께 로딩
Order order = em.find(Order.class, orderId); List<Order> orders = em.createQuery("select o from Order o");
- order와 orders 모두 연관된 member 엔티티를 미리 로딩해서 사용가능
글로벌 페치 전략에 즉시 로딩 사용 시 단점
- 사용하지 않는 엔티티를 로딩
- order와 member 둘 다 필요해서 글로벌 전략을 즉시 로딩으로 설정하면 order만 필요한 경우에도 즉시 로딩으로 인해 member도 함께 조회되는 상황이 발생
- N+1 문제가 발생
- JPA를 사용하면서 성능상 가장 조심해야 하는 것이 N+1 문제
em.find()
로 엔티티를 조회할 때 즉시 로딩이면 JOIN 쿼리를 사용해 연관된 엔티티도 조회//즉시 로딩 예제 Order order = em.find(Order.class, 1L); //실행된 SQL select o.*, m.* from Order o left outer join Member m on o.MEMBER_ID=m.MEMBER_ID where o.id=1
em.find()
의 경우 글로벌 즉시 로딩 전략이 좋아보이지만, JPQL을 사용할 때 문제가 발생//JPQL 즉시 로딩 예제 List<Order orders = em.createQuery("select o from Order o", Order.class) .getResultList(); //연관된 모든 엔티티를 조회 //실행된 SQL select * from Order //JPQL로 실행된 SQL select * from Member where id = ? //EAGER로 실행된 SQL select * from Member where id = ? //EAGER로 실행된 SQL select * from Member where id = ? //EAGER로 실행된 SQL select * from Member where id = ? //EAGER로 실행된 SQL ....
- JPA가 JPQL을 분석해서 SQL을 생성할 때 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용하면서 문제 발생
- 따라서 즉시 로딩이든 지연 로딩이든 JPQL 쿼리 자체에 충실하게 SQL을 생성
- JPA가 JPQL을 통해 내부에서 동작하는 순서
select o from Order o
JPQL을 분석해select * from Order
SQL을 생성
- 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성
- Order.member의 글로벌 페치 전략이 즉시 로딩이므로 order를 로딩하는 즉시 연관된 member도 로딩
- 연관된 member를 영속성 컨텍스트에서 조회
- 만약 영속성 컨텍스트에 없으면
select * from Member where id = ?
SQL을 조회한 order 엔티티 수만큼 실행
- JPA가 JPQL을 분석해서 SQL을 생성할 때 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용하면서 문제 발생
- N+1은 SQL을 상당히 많이 호출되므로 치명적, 이런 N+1문제는 JPQL 페치 조인으로 해결
13.2.2 JPQL 페치 조인
- 글로벌 페치 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에 영향을 주므로 비효율적
- 페치 조인은 JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택 가능
페치 조인 사용 전
JPQL: select 0 from Order o SQL: select * from Order
페치 조인 사용 후
JPQL: select o from Order o join fetch o.member SQL: select o.*, m.* from Order o join Member m on o.MEMBER_ID=m.MEMBER_ID
- 페치 조인은 조인 명령어 마지막에 fetch를 넣어주면 됌 페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 함께 조회
- 페치 조인은 N+1 문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩 (현실적인 방법)
JPQ 페치 조인의 단점
- 페치 조인이 현실적인 대인이지만 무분별하게 화면에 맞춘 레포지토리는 메소드가 증가
- 프레젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범
- 화면 A를 위해 order만 조회하는
repository.findOrder()
- 화면 B를 위해 order와 member를 페치 조인 조회하는
repository.findOrderWithMember()
- 화면 A를 위해 order만 조회하는
- 각각 메소드를 호출하면 최적화는 할 수 있지만 뷰와 레포지토리 간 논리적인 의존관계 발생
- 다른 대안은 repository.findOrder() 하나만 만들고 페치조인으로 함께 로딩하는 방법
13.2.3 강제로 초기화
- 영속성 컨텍스트가 있을 때 프리젠테이션 계층이 필요한 엔티티를 강제 초기화 후 반환
프록시 강제 초기화
class OrderService { @Transactional public Order findOrder(id) { Order order = orderRepository.findOrder(id); order.getMember().getName(); //프록시 객체를 강제로 초기화 return order; } }
- 지연 로딩으로 설정하면 연관된 엔티티를 프로시 객체로 조회, 실제 사용하는 시점에 초기화
order.getMember()
까지만 호출하면 단순히 프록시 객체만 반환, 초기화는 하지 않음
- 프록시 객체는
member.getName()
처럼 실제 값을 사용하는 시점에 초기화
- 초기화 한 것은 준영속 상태에서도 사용이 가능, 하이버네이트의 initialize() 로 강제 초기화
- 비즈니스 로직을 담당하는 서비스 계층에서 프레젠테이션 계층을 위한 프록시 초기화 역할 분리가 필요, FACADE 계층이 그 역할을 담당
13.2.4 FACADE 계층 추가
- 프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두는 방법
- FACADE 계층을 도입해서 서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리
- 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로 FACADE에서 트랜잭션을 시작
FACADE 계층의 역할과 특징
- 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리
- 프리젠테이션 계층에서 필요한 프록시 객체를 초기화
- 서비스 계층을 호출해서 비즈니스 로직을 실행
- 레포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 탐색
FACADE 예시
class OrderFacade {
@Autowired OrderService orderService;
public Order findOrder(id) {
Order order = orderService.findOrder(id);
//프리젠테이션 계층이 필요한 프록시 객체를 강제로 초기화
order.getMember().getName();
return order;
}
}
class OrderService {
public Order findOrder(id) {
return orderRepository.findOrder(id);
}
}
- OrderService에 있떤 프록시 초기화 코드를 OrderFacade로 이동
- FACADE 계층을 이용해 서비스 계층과 프리젠테이션 계층 간에 논리적 의존 관계를 제거
- 프리젠테이션 계층을 위한 초기화 코드는 모두 FACADE가 담당
- 단점은 계층이 추가됨으로 많은 코드를 작성, 단순 서비스 계층 호출만 하는 위임 코드가 다수
13.2.5 준영속 상태와 지연 로딩의 문제점
- 뷰를 개발할 때 필요한 엔티티를 미리 초기화하는 방법은 생각보다 오류 가능성이 높음
- FACADE나 서비스 클래스까지 열어 보는 것이 번거롭기 떄문에 놓치기 쉬움
- 로직과 뷰가 물리적으로 나누어져 있지만 논리적으로는 서로 의존한다는 문제가 발생
- FACADE를 사용해 어느 정도 해소할 수 있지만 상당히 번거로움
- 최적화된 엔티티를 맞아떨어지게 조회하려면 여러 종류의 메소드가 필요
//화면 A는 order만 필요하다 getOrder(); //화면 B는 order, order.member가 필요하다 getOrderWithMember(); //order, order.orderItems가 필요하다 getOrderWithOrderItems() //order, order.member, order.orderItems가 필요하다 getOrderWithMemberWithOrderItems()
- 모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 떄문에 발생
'개발서적 > 자바 ORM 표준 JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 13.4 너무 엄격한 계층 (0) | 2021.09.21 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 13.3 OSIV (0) | 2021.09.21 |
[자바 ORM 표준 JPA 프로그래밍] 13.1 트랜잭션 범위의 영속성 컨텍스트 (0) | 2021.09.21 |
[자바 ORM 표준 JPA 프로그래밍] 12.5~10 명세, 사용자 정의 리포지토리, Web 확장... (0) | 2021.09.21 |
[자바 ORM 표준 JPA 프로그래밍] 12.4 쿼리 메소드 기능 (0) | 2021.09.21 |