스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 영속성과 트랜잭션을 대신 관리해주므로 편리하게 애플리케이션을 개발할 수 있다. 하지만, 컨테이너 환경에서 동작하는 JPA의 내부 동작 방식을 이해하지 못하면 문제가 발생했을 때 해결하기 쉽지 않다.
트랜잭션 범위의 영속성 컨텍스트
스프링이나 J2EE 컨테이너 환경에서는 컨테이너가 제공하는 전략을 따라야 한다.
스프링 컨테이너의 기본 전략
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 사용한다. 말 그대로 트랜잭션과 영속성 컨텍스트의 생명주기가 같다라는 의미이다. 트랜잭션이 시작되면 영속성 컨텍스트가 생성되고 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.
같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
스프링 프레임워크를 사용하면 보통 비즈니스 로직이 있는 서비스 계층에 @Transactional
을 선언해 트랜잭션을 시작한다. 외부에서 보면 단순히 해당 메서드를 호출하는 것 같지만 실제로는 트랜잭션 AOP가 먼저 실행된다.
위의 그림을 살펴보면 스프링 트랜잭션 AOP가 대상 메서드를 호출하기 전에 먼저 트랜잭션을 시작하고 대상 메서드가 정상 종료되면 트랜잭션을 커밋하면서 종료한다. 이때 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 데이터베이스에 변경 내용을 반영한 후에 데이터베이스 트랜잭션을 커밋한다. 만약 예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이 때는 플러시를 호출하지 않는다.
트랜잭션의 범위의 영속성 컨텍스트를 조금 더 구체적으로 살펴보자.
트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다
해당 전략은 여러 곳에서 엔티티 매니저를 주입받아도 트랜잭션이 같다면 항상 같은 영속성 컨텍스트를 사용한다.
트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다
스프링 컨테이너는 위와같이 여러 스레드에 동시에 요청이 와도 스레드마다 다른 영속성 컨텍스트를 할당한다. 스프링이나 J2EE 컨테이너의 가장 큰 장점은 트랜잭션과 복잡한 멀티 스레드 상황을 컨테이너가 대신 처리해준다는 점이다. 따라서, 개발자는 싱글 스레드 애플리케이션처럼 단순하게 개발할 수 있고 결과적으로 비즈니스 로직에 더 집중할 수 있다.
준영속 상태와 지연 로딩
트랜잭션은 보통 서비스 계층에서 사용되므로 서비스 계층의 메소드가 종료되면 트랜잭션 역시 종료된다. 이에 따라 영속성 컨텍스트도 비워지기 때문에 서비스 계층에서 컨트롤러로 반환된 엔티티는 준영속 상태의 엔티티이다.
만약 연관관계가 있는 엔티티를 지연로딩으로 설정했을 경우 프레젠테이션 계층에서는 엔티티가 준영속 상태이므로 변경감지와 지연 로딩이 동작하지 않는다.
class OrderController{
public String view(Long orderId){
Order order = orderService.findOne(orderId);
Member member = order.getMember();
member.getName(); // 지연 로딩 시 예외 발생!
}
}
준영속 상태와 변경 감지
보통 변경 감지 기능은 서비스 계층에서 비즈니스 로직을 수행할 때 동작한다. 단순히 데이터를 보여주기만 하는 프리젠테이션 계층에서는 딱히 사용될 일이 많이 없다. 오히려 변경 감지 기능이 프리젠테이션 계층에서까지 작동한다면, 애플리케이션 계층이 가지는 책임이 모호해지고 데이터를 어디서 어떻게 변경했는지 찾아야 하므로 애플리케이션 유지보수하기가 어렵다. 따라서, 변경감지 기능이 프리젠테이션 계층에서 동작하지 않는 것은 특별히 문제가 되지 않는다.
준영속 상태와 지연 로딩
준영속 상태의 가장 골치 아픈 점은 지연 로딩이 작동하지 않는다는 점이다. 지연 로딩을 설정한 엔티티를 뷰에 렌더링하면 처음에는 아직 초기화되지 않은 프록시 객체를 가져온다. 이 후 실제 엔티티를 사용하는 시점에서 영속성 컨텍스트의 도움을 받아 엔티티를 조회해야 하는데 영속성 컨텍스트가 없으므로 지연 로딩을 할 수 없다.
준영속 상태의 지연 로딩 문제를 해결하는 방법은 크게 2가지가 있다.
- 뷰가 필요한 엔티티를 미리 로딩해두는 방법
- OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법
뷰가 필요한 엔티티를 미리 로딩해두는 방법을 먼저 살펴보자.
이 방법은 이름 그대로 영속성 컨텍스트가 살아있을 때 필요한 엔티티를 미리 다 로딩하거나 초기화해서 반환하는 방법이다. 따라서, 준영속 상태에서 지연로딩이 발생하지 않는다.
해당 방법은 어디서 미리 로딩하느냐에 따라 3가지 방법이 있다.
- 글로벌 페치 전략 수정
- JPQL 페치 조인
- 강제로 초기화
글로벌 페치 전략 수정
가장 간단한 방법은 글로벌 페치 전략을 EAGER
로 변경하는 것이다.
@Entity
public class Order{
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩 전략
private Member member;
}
위와 같이 설정하고 엔티티 매니저로 주문 엔티티를 조회하면 연관된 회원도 항상 함께 로딩한다. 그러나 이렇게 글로벌 페치 전략을 즉시 로딩으로 바꾸면 2가지 단점이 존재한다.
글로벌 페치 전략에 즉시 로딩 사용 시 단점
사용하지 않는 엔티티를 로딩한다.
특정 화면에서는 연관된 엔티티가 필요하지 않을 수 있다. 하지만, 즉시 로딩으로 설정해두면 해당 화면에서는 필요하지 않은 엔티티까지 조회하게 된다.
N + 1 문제가 발생한다.
JPA를 사용하면서 성능상 가장 조심해야하는 것이 바로 N + 1 문제다. 글로벌 페치 전략을 즉시 로딩으로 바꾸고 엔티티를 조회할 때
1)em.find()
를 통해 조회하는 것과
2)JPQL을 사용해서 조회하는 방법이 있다.
em.find()
를 사용하면 JOIN 쿼리로 연관된 엔티티를 조회한다. 그러나 JPQL을 사용해 다음과 같이 주문들을 가져온다면 문제가 발생한다.
List<Order> orders =
em.creatQuery("select o from Order o", Order.class)
.getResultList(); // 연관된 모든 엔티티를 조회한다.
/*
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을 만든다.
코드를 분석하면 내부에서 다음과 같은 순서로 동작한다.
select o from Orders o
JPQL을 분석해select * from Order
SQL을 생성한다.- 데이터베이스에서 결과를 받아 order 엔티티 인스턴스들을 생성한다.
- 글로벌 페치 전략이 즉시 로딩이므로 연관된 member도 로딩해야 한다.
- 연관된 member를 영속성 컨텍스트에서 찾는다.
- 만약 영속성 컨텍스트에 없으면
SELECT * FROM MEMBER WHERE ID = ?
SQL을 조회한 order 엔티티 수만큼 실행한다.
이처럼 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N + 1 문제라고 한다. N + 1이 발생하면 조회 성능에 치명적이므로 최우선 최적화 대상이다. 이런 N + 1문제는 JPQL 페치 조인으로 해결할 수 있다.
JPQL 페치 조인
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
페치 조인을 사용하면 위와 같이 조인이 일어나 N + 1 문제가 발생하지 않는다.(글로벌 페치 전략보다 페치 조인이 우선된다.)
페치 조인은 N + 1 문제를 해결하면서 화면에 필요한 엔티티를 미리 로딩하는 현실적인 방법이다.
JPQL 페치 조인의 단점
페치 조인이 현실적인 대안이긴 하지만 무분별하게 사용하면 화면에 맞춘 리포지토리 메서드가 증가할 수 있다. 결국 프리젠테이션 계층이 데이터 접근 계층을 침범하는 것이다.
이에 대한 대안은 페치 조인을 사용한 하나의 메서드만을 두고 사용하는 것이다. 물론 연관 엔티티를 사용하지 않는 화면에서는 약간의 로딩 시간이 더 증가하겠지만 조인을 사용해 한 번의 쿼리로 필요한 데이터를 조회하므로 성능에 미치는 영향은 미비하다.
강제로 초기화
해당 방법은 영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다.
class OrderService{
@Transactional
public Order findOrder(Long id){
Order order = orderRepository.findOrder(id);
order.getMember().getName(); // 프록시 객체를 강제로 초기화
return order;
}
}
글로벌 페치 전략을 지연 로딩으로 두면 연관 엔티티를 프록시 객체로 조회한다. 프록시 객체는 getName()
과 같이 실제 값을 사용하는 시점에 초기화되는데 위처럼 강제로 프록시 객체를 초기화하면 프리젠테이션 계층에서도 이미 초기화되었기 때문에 준영속 상태에서도 사용할 수 있다.
하이버네이트를 사용하면 initalize()
메소드를 사용해서 프록시를 강제로 초기화할 수 있다.
org.hibernate.Hibernate.initialize(order.getMember()); //프록시 초기화
참고로 JPA 표준에는 프록시 초기화 메소드가 없다. JPA 표준에는 단지 초기화 여부만을 확인할 수 있다.
PersistenceUnitUtil persistenceUnitUtil =
em.getEntityManagerFactory().getPersistenceUnitUtil();
boolean isLoaded = persistenceUnitUtil.isLoaded(order.getMember());
위와 같이 프록시를 초기화 하는 역할을 서비스 계층이 담당하면 뷰가 필요한 엔티티에 따라 서비스 계층의 로직을 변경해야 한다. 프리젠테이션 계층이 서비스 계층을 침범하는 상황이다. 따라서, 비즈니스 로직을 담당하는 서비스 계층에서 프리젠테이션 계층을 위한 프록시 초기화 역할을 분리해야 한다. FACADE 계층이 그 역할을 담당해줄 것이다.
FACADE 계층 추가
위와 같이 프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두는 방법이다. 이제 뷰를 위한 프록시 초기화는 이 계층에서 수행한다. 결과적으로 FACADE 계층을 도입해서 서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있다. 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로 트랜잭션을 FACADE 계층에서 시작해야 한다.
FACADE 계층의 역할과 특징
- 프리젠테이션 계층과 서비스 계층의 논리적인 의존성을 분리해준다.
- 프리젠테이션 계층에서 필요한 프록시 객체를 초기화한다.
- 서비스 계층을 호출해서 비즈니스 로직을 실행한다.
- 리포지토리를 직접 호출해서 뷰가 필요로 하는 엔티티를 찾는다.
FACADE 계층을 사용해서 서비스 계층과 프리젠테이션 계층 간에 논리적인 연관관계를 제거했다. 하지만 실용적인 관점에서 보면 FACADE 계층의 최대 단점은 프리젠테이션 계층과 서비스 계층 사이에 계층이 하나 더 끼어든다는 점이다. 결국 더 많은 코드를 작성하게 되고 FACADE 계층에서는 단순 호출만 하는 위임 코드가 더 많을 것이다.
준영속 상태와 지연 로딩의 문제점
뷰를 개발할 때 필요한 엔티티를 미리 초기화하는 방법은 생각보다 오류가 발생할 확률이 높다. 보통 뷰를 개발할 때는 엔티티만 보고 개발하기 때문에 퍼사드나 서비스 계층까지 열어보는 것은 상당히 번거롭고 놓치기 쉽기 때문이다. 결국 영속성 컨텍스트가 없는 뷰에서 초기화되지 않은 엔티티를 조회하는 실수를 하게 되고 LazyInitializationException
이 발생하게 된다.
퍼사드 계층을 사용한다 해도 퍼사드 계층에서 뷰에 딱딱 맞아떨어지게 초기화해서 조회하려면 퍼사드 계층에 여러 종류의 조회 메소드가 필요하다.
결국 모든 문제가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다. 이를 해결하는 방법은 OSIV를 사용하는 것이다.
OSIV
OSIV(Open Session In View)는 영속성 컨텍스틀 뷰까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지된다. 따라서 뷰에서도 지연 로딩을 사용할 수 있다.
과거 OSIV: 요청 당 트랜잭션
OSIV의 핵심은 뷰에서도 지연 로딩이 가능하도록 하는 것이다. 가장 단순한 구현 방법은 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 것이다. 이를 요청 당 트랜잭션(Transaction Per Request)방식의 OSIV라고 한다.
이렇게 하면 처음부터 끝까지 영속성 컨텍스트가 살아있으므로 조회한 엔티티도 영속 상태를 유지한다. 그러므로 지연로딩을 사용할 수 있어 퍼사드 계층없이도 뷰에 독립적인 서비스 계층을 유지할 수 있다.
요청 당 트랜잭션 방식의 OSIV 문제점
요청 당 트랜잭션 방식의 OSIV 문제점은 프리젠테이션 계층에서 엔티티를 수정할 수 있다는 점이다.
public class MemberController{
public String vieMember(Long id){
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안상의 이유로 고객 이름 대체
model.addAttribute("member", member);
...
}
}
위의 예제를 보면 컨트롤러에서 고객 엔티티의 이름을 변경해서 렌더링할 뷰에 넘겨줬다. 개발자의 의도는 단순히 이름을 대체하는 것이였지만 영속성 컨텍스트가 살아있으므로 뷰를 렌더링한 후에 트랜잭션을 커밋한다. 그 결과 데이터베이스의 고객이름이 XXX로 변경되고 만다.
따라서, 프리젠테이션 계층에서 엔티티를 수정하지 못하도록 막아야 한다. 그 방법들은 아래와 같다.
- 엔티티를 읽기 전용 인터페이스로 제공
- 엔티티 래핑
- DTO만 반환
엔티티를 읽기 전용 인터페이스로 제공
이 방법은 엔티티를 직접 노출하지 않고 읽기 전용 메소드만 제공하는 인터페이스를 프리젠테이션 계층에 제공하는 방법이다.
interface MemberView{
public String getName();
}
class Member implements MemberView{
...
}
class MemberService{
public MemberView getMember(Long id){
return MemberRepository.findById(id);
}
}
위와 같이 실제 엔티티를 인터페이스 타입으로 업캐스팅해서 제공했다. 프리젠테이션 계층에서는 읽기 전용 메소드만 있는 인터페이스를 사용하기 때문에 엔티티를 수정할 수 없다.
엔티티 래핑
class MemberWrapper{
private Member member;
public MemberWrapper(Member member){
this.member = member;
}
// 읽기 전용 메소드만 제공
public String getName(){
return member.getName();
}
}
해당 방법은 엔티티의 읽기 전용 메서드만 가지고 있는 엔티티 래퍼 클래스를 만들고 이것을 프리젠테이션 계층에 반환하는 방법이다.
DTO만 반환
가장 전통적인 방법으로 데이터 전달만을 위한 객체를 만들어 반환하는 것이다. 이 방법을 사용할 경우 OSIV를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO클래스를 하나 더 만들어야 한다.
지금까지 설명한 방법 모두 코드량이 상당히 증가한다는 단점이 있다. 해당 방법들은 모두 요청 당 트랜잭션 방식의 OSIV에 해당한다. 최근에는 거의 사용하지 않는다. 최근에는 이런 문제들을 보완해서 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다. 스프링 프레임워크가 제공하는 OSIV도 바로 이 방식이다.
스프링 OSIV: 비즈니스 계층 트랜잭션
스프링 프레임워크가 제공하는 OSIV 라이브러리
스프링 프레임워크의 spring-orm.jar는 다양한 OSIV 클래스를 제공하므로 서블릿 필터, 스프링 인터셉터 중 어느쪽에서 OSIV를 적용할지 선택해 클래스를 사용하면 된다.
스프링 OSIV 분석
스프링 OSIV는 요청 당 트랜잭션방식에서 문제가 되었던 프리젠테이션 계층에서 엔티티를 수정하는 것을 어느정도 해결했다.
스프링 프레임워크가 제공하는 OSIV는 OSIV를 사용하긴 하지만 비즈니스 계층에서만 트랜잭션을 사용한다.
클라이언트의 요청이 들어오면 영속성 컨텍스트를 생성한다. 이 때 트랜잭션은 시작하지 않는다. 서비스 계층에서 트랜잭션을 시작하면 이미 생성해둔 영속성 컨텍스트에서 트랜잭션을 시작한다. 비즈니스 계층 호출이 끝나면 해당 트랜잭션을 커밋하고 영속성 컨텍스트를 닫지 않는다. 이후 클라이언트의 요청이 끝나면 영속성 컨텍스트를 종료한다.
조금 더 자세히 보면 다음과 같다.
- 클라이언트의 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하지 않고 영속성 컨텍스트를 생성한다.
- 서비스 계층에서
@Transactional
로 트랜잭션을 시작할 때 1에서 생성해둔 영속성 컨텍스트에 트랜잭션을 시작한다. - 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이 때 트랜잭션은 끝나지만 영속성 컨텍스트는 종료하지 않는다.
- 프리젠테이션 계층에서까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
- 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스를 종료한다. 이 때 플러시를 호출하지 않고 종료한다.
트랜잭션 없이 읽기
영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 한다. 그렇지 않으면 TransactionRequiredException
이 발생한다.
엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이것을 트랜잭션 없이 읽기(Nontransactional reads)라고 한다. 프록시를 초기화하는 것도 지연 로딩을 하는 것도 모두 조회 기능이므로 트랜잭션 없이 읽기가 가능하다.
스프링이 제공하는 OSIV를 사용하면 프리젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없다. 기존 문제를 해결했다. 또한, 지연 로딩은 조회기능이므로 트랜잭션 없어도 지연 로딩이 가능하다.
class MemberController{
public String viewMember(Long id){
Member member = memberService.getMember(id);
member.setName("XXX"); // 보안상의 이유로 대체
model.addAttribute("member", member);
...
}
}
위와 같이 작성해도 2가지 이유로 엔티티의 변경이 일어나지 않는다.
- 서비스 계층을 호출한 이후 종료될 때 이미 트랜잭션이 커밋되었고 그에 따라 영속성 컨텍스트가 플러시되었다. 또한, 고객의 이름이 변경되었더라도 스프링이 제공하는 OSIV는 요청이 반환될 때 영속성 컨텍스트를 플러시하지 않고 종료한다.
- 만약 프리젠테이션 계층에서 강제로 영속성 컨텍스트를 플러시하려고 해도 트랜잭션이 없으므로 예외가 발생한다.
따라서, 위 예제에서 프리젠테이션 계층에서 엔티티를 수정했지만 데이터베이스에 변경 내용이 반영되지 않는다.
스프링 OSIV 주의사항
스프링 OSIV를 사용하면 프리젠테이션 계층에서 엔티티를 수정하더라도 데이터베이스에 반영하지 않는데 한 가지 예외가 있다. 프리젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.
class MemberController{
public String viewMember(Long id){
Member member = memberService.getMember(id);
member.setName("XXX");
memberService.biz(); // 비즈니스 로직 수행, 문제 발생
return "view";
}
}
class MemberSerivice{
@Transactional
public void biz(){
//... 비즈니스 로직 수행
}
}
위와 같이 엔티티를 변경한 다음에 서비스 계층에서 비즈니스 로직을 실행하면 문제가 발생한다. 따라서, 트랜잭션이 있는 모든 비즈니스 로직을 먼저 호출하고 그 결과를 조회하는 순서로 실행하면 이런 문제는 발생하지 않는다.
스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 이런 문제가 발생한다. OSIV를 사용하지 않는 트랜잭션의 범위의 영속성 컨텍스트 전략은 영속성 컨텍스트의 생명주기가 트랜잭션의 생명주기와 같아서 이런 문제가 발생하지 않는다.
OSIV 정리
스프링 OSIV의 특징
- OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 유지된다. 따라서, 한 번 조회한 엔티티는 요청이 끝날 때까지 영속상태를 유지한다.
- 엔티티의 수정은 서비스 계층에서만 동작한다. 트랜잭션이 없는 프리젠테이션 계층에서는 지연 로딩과 같은 트랜잭션 없이 읽기만 가능하다.
스프링 OSIV 단점
- OSIV를 사용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있어 주의해야 한다. 특히 트랜잭션 롤백 시 주의해야 한다.
- 위에서 설명했듯이 프리젠테이션 계층에서 엔티티를 수정하고 서비스 계층에서 비즈니스 로직을 수행하면 데이터베이스에 수정 내용이 반영될 수 있으므로 주의해야 한다.
- 프리젠테이션 계층에서 지연 로딩에 의한 SQL이 실행된다. 따라서, 성능 튜닝시에 확인해야할 부분이 넓다.
OSIV를 사용하는 것이 만능은 아니다
OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다. 하지만 복잡한 화면을 구성할 때 OSIV를 사용하는 것이 효과적이지 않은 경우가 많다. JPQL을 사용해 DTO로 조회하는 것이 더 나을 수도 있다.
OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다
OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다. JSON이나 XML을 생성할 때는 지연 로딩을 사용할 수 있지만 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하다. 결국 클라이언트가 필요한 데이터를 모두 JSON으로 생성해서 반환해야 한다. 보통 Jackson이나 Gson같은 라이브러리를 사용해서 객체를 JSON으로 변환하는데, 변환 대상 객체로 엔티티를 직접 노출하거나 DTO를 사용해서 노출한다.
이렇게 JSON으로 수정한 API는 한 번 정의하면 수정하기 어려운 외부 API와 언제든지 수정할 수 있는 내부 API로 나눌 수 있다.
- 외부 API
- 외부에 노출한다.
- 한 번 정의하면 수정이 어렵다.
- 서버와 클라이언트를 동시에 수정하기 어렵다
- ex) 타 팀, 타 기업과 협업하는 API
- 내부 API
- 외부에 노출하지 않는다.
- 언제든지 변경이 가능하다.
- 서버와 클라이언트를 동시에 수정할 수 있다.
- ex) 같은 프로젝트에 있는 화면을 구성하기 위한 AJAX 호출
엔티티는 생각보다 자주 변경된다. 엔티티를 JSON 변환 대상 객체로 사용하면 엔티티를 변경할 때 노출되는 JSON API도 변경된다. 따라서, 외부 API는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전하다. 내부 API는 서버와 클라이언트를 동시에 수정할 수 있어 엔티티를 직접 노출하는 방법도 괜찮다.
너무 엄격한 계층
class OrderController{
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
public String orderRequest(Order order, Model model){
long id = orderService.order(order); // 상품 구매
// 리포지토리 직접 접근
Order orderResult = orderRepository.findOne(id);
model.setAttribute("order", orderResult);
...
}
}
@Transactional
class OrderService{
@Autowired OrderRepository orderRepository;
public Long order(order){
//... 비즈니스 로직
return orderRepository.save(order);
}
}
@Transactional
class OrderRepository{
@PersistenceContext EntityManager em;
public Order findOne(Long id){
return em.find(Order.class, id);
}
}
OSIV를 사용하기 전에는 프리젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화했어야 한다. 하지만 OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없다. 따라서, 단순한 엔티티 조회는 컨트롤러에서 직접 호출해도 아무런 문제가 없다.
정리
스프링이나 J2EE 컨테이너 환경에서는 트랜잭션 범위의 영속성 컨텍스트 전략이 적용된다. 해당 전략의 유일한 문제점은 프리젠테이션 계층에서 엔티티의 지연 로딩을 할 수 없다는 점이다.
스프링 OSIV를 사용하면 이런 문제들을 해결할 수 있다. 또한, 기존 OSIV 방식에서 문제였던 프리젠테이션 계층에서의 엔티티 수정 문제도 해결된다.
'JPA' 카테고리의 다른 글
12장_스프링 데이터 JPA (0) | 2024.04.02 |
---|---|
11장_웹 애플리케이션 제작 (0) | 2024.04.01 |
Java ORM 표준 - JPA 프로그래밍 10장 (0) | 2024.03.24 |
Java ORM 표준 - JPA 프로그래밍 9장 (0) | 2024.01.19 |