스프링 컨테이너의 기본 전략
- 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략 사용
- 트랜잭션을 시작할 때 영속성 컨텍스트(Persistance Context)를 생성하고 트랜잭션이 끝나면 영속성 컨텍스트를 종료합니다.
- 핵심 비즈니스 로직을 담당하는 Service 클래스에서 @Transactional 어노테이션을 통해 트랜잭션을 시작하게 되고, 서비스보다 상위 계층에 있는 Layer는 준영속 상태가 됩니다.
준영속 상태가 뭔지 설명드리기 전에 엔티티 생명주기에 대해 간략하게 설명드리겠습니다.
엔티티 생명주기
- 비영속(new/transient): new 키워드로 객체를 생성만 한 상태 (영속성 컨텍스트에서 관리 X, 1차 캐시, 변경 감지 등의 기능 적용 X)
- 영속(managed): 영속성 컨텍스트에 의해서 관리되어지는 상태(em.persist(object), em.find()..)
- 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태로 현재는 영속 상태가 아닌 상태(영속성 컨텍스트가 제공하는 기능 사용 X)
- 준영속 상태의 Entity는 식별자가 존재한다는 것을 보장하지만, 비영속 상태의 Entity는 보장받지 못합니다.
- 삭제(removed): 1차캐시와 데이터베이스에서 모두 삭제된 상
// repositoryA, repositoryB는 같은 영속성 컨텍스트에 접근
@Transactional
public void executeBusinessLogic() {
repositoryA.findAll();
repositoryB.findAll();
}
// repositoryC는 repositoryA과 repositoryB와 다른 영속성 컨텍스트에 접근
@Transactional
public void executeOtherBusinessLogic() {
repositoryC.findAll();
}
준영속 상태와 지연로딩(Lazy Loading)
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@ManyToOne(fetch = FetchType.LAZY)
@OnDelete(action = OnDeleteAction.CASCADE)
private Member member;
}
- Book 객체를 만드는 Controller에서 지연로딩 객체를 초기화하면 Exception 발생
- 해결 방안
- 글로벌 페치 전략: FetchType.LAZY -> FetchType.EAGER (N+1 Problem, 불필요한 엔티티 조회)
- JPQL Fetch Join
- 강제로 초기화
강제로 초기화 예시
@RequiredArgsConstructor
public class OrderService {
private final BookRepository bookRepository;
@Transactional
public Book findBook(final Long bookId) {
Book book = bookRepository.findOrder(bookId);
book.getMember().getName(); // 프록시 객체를 강제로 초기화
return book;
}
- 다음과 같이 비즈니스 로직에 프록시 객체를 초기화하는 역할이 같이 존재하면, 객체지향적이지 못한 코드가 됩니다. 그래서 Facade 계층을 추가해줘야 합니다.
- Facade 계층
- Presentation Layer와 Service Layer에서 Proxy 객체 초기화 담당
- 기존의 트랜잭션 시작을 서비스에서 진행했지만, Facade 계층에서 시작
- 의존관계 문제를 해결했지만, 계층이 추가되어 구조의 복잡도는 더욱 증가
- OSIV(Open Session In View)
- 영속성 컨텍스트를 View까지 열어두는 것
- 옵션을 false(고객 서비스 기반의 트래픽이 많은 실시간 API)에서 true( ADMIN처럼 커넥션을 많이 사용하지 않는 곳)로 바꿔주면 View에서도 지연로딩 사용이 가능해집니다.(default:true)
- Hibernate에서 부르는 용어이고, JPA에서는 OEIV라고 부릅니다.
- OSIV 장점
- OSIV를 사용하게 되면, 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없을 뿐만 아니라, 단순 엔티티 조회를 Controller에서 호출해도 괜찮습니다.
- OSIV: true
- 과거 OSIV: 요청 당 트랜잭션
- OSIV의 가장 단순한 구현은 클라이언트의 요청이 들어오자마자 filter나 interceptor에서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션을 끝냅니다.
- 영속성 컨텍스트가 처음부터 끝까지 살아있으므로 조회한 데이터도 영속 상태를 유지하기에 Facade 계층도 필요 없어집니다.
- 하지만, Controller, View에서 Entity 관련 정보를 수정할 수 있기에 DB의 변경이 아무 계층에서나 발생할 수 있는 문제가 존재합니다.
- 스프링 OSIV: 비즈니스 계층 트랜잭션
- Spring Framework가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV입니다.
- 클라이언트의 요청이 들어오면 Servlet Filter, Interceptor에서 영속성 컨텍스트를 생성합니다. 여기에서 트랜잭션이 시작되지는 않습니다.
- 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때, 미리 생성해 둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작합니다.
- 서비스 계층이 끝나면 트랜잭션 commit 및 영속성 컨텍스트의 flush가 실행되고 이를 통해 트랜잭션이 끝나더라도 영속성 컨텍스트는 유지될 수 있습니다.
- Filter, Interceptor로 요청이 돌아오면 Flush를 호출하지 않고 바로 종료합니다.
트랜잭션 없이 읽기
- 프리젠테이션 계층에서 엔티티를 수정한 직후에, 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생합니다.
- 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 롤백이 발생할 때, 문제가 생길 수 있습니다.
- 복잡한 화면은 Dto로 반환하는 것이 좋습니다.
- OSIV는 JVM을 벗어난 원격 상황에서 사용하지 않습니다.
- Json으로 생성한 API를 외부 API, 내부 API 두 가지로 나눌 수 있습니다.
- 외부 API는 변경이 자주 일어나므로 Dto를 사용
- 내부 API는 변경이 적으므로 OSIV를 사용
<참고 자료>
'Java > Spring' 카테고리의 다른 글
[Spring] JPA Auditing 기술을 사용하여 생성일자, 수정일자 컬럼의 데이터 형식을 깔끔하게 지정하는법 (0) | 2023.11.24 |
---|---|
[Spring] DTO, VO 이 둘의 차이에 대해 알아보자 (2) | 2023.11.17 |
[Spring] Filter, Interceptor, ArgumentResolver (0) | 2023.10.15 |
[Spring] Nginx를 이용하여 http(80 Port)로 들어오는 요청을 springboot(8080 Port)로 Redirect 시키기 (0) | 2023.09.27 |
[Spring] 계층형 디렉터리, 도메인형 디렉터리 구조 (0) | 2023.09.27 |