개요
@GetMapping("/{userId}/followers")
public ResponseEntity<List<FollwerResponse>> getFollowers(
@PathVariable("userId") Long userId,
@PageableDefault(size = 10, sort = "userId", direction = Direction.ASC) Pageable pageable
){
User user = userFindService.findById(userId);
List<FollwerResponse> response = followService.getFollowers(user, pageable);
return ResponseEntity.ok(response);
}
프로젝트 코드를 검토하면서 위의 코드를 확인할 수 있었습니다.
교대 근무 중 @Transactional
주석이 사용되기 때문에 메서드 논리는 트랜잭션 내에서 작동합니다.
Spring 컨테이너는 기본적으로 트랜잭션 지속성 컨텍스트 전략을 사용합니다.
이 전략은 트랜잭션이 시작될 때 지속성 컨텍스트가 시작되고 트랜잭션이 종료될 때 지속성 컨텍스트가 닫히는 것을 의미합니다.
그리고 동일한 트랜잭션 내에서 동일한 지속성 컨텍스트가 항상 액세스됩니다.
지속성 컨텍스트 전략을 알면서 다음과 같은 질문이 생겼습니다.
- ID에 해당하는 사용자를 찾고 팔로워를 찾은 후 두 개의 지속성 컨텍스트를 사용합니까?
- 두 개의 지속성 컨텍스트를 사용하는 경우 사용자가 지속성 컨텍스트를 종료하고 의도하지 않은 작업을 수행하거나 사용자를 다시 탐색하지 않습니까?
귀하의 질문에 대한 답변을 요약하고자 합니다.
트랜잭션 지속성 컨텍스트
거래
거래데이터베이스에서 수행되는 작업 단위입니다.
단일 논리적 작업을 수행하기 위한 하나 이상의 쿼리로 구성됩니다.
구매 상황이 있을 때 제품에 대한 비용을 지불하고 구매하는 것은 논리적인 작업이 될 수 있습니다.
트랜잭션 내에서 모든 작업은 성공적으로 완료되어야 합니다.
중간에 문제가 발생하여 작업이 실패하면 모든 작업을 취소하고 이전 상태로 돌아갑니다.
거래를 안전하게 처리 ACID
원칙을 준수합니다.
- 원자성: 트랜잭션 내의 모든 작업은 완료되거나 전혀 수행되지 않아야 합니다.
부분적으로 실행되거나 중단될 수 없습니다. - 일관성: 트랜잭션이 실행을 완료하면 일관된 데이터베이스 상태가 유지되어야 합니다.
- 격리: 여러 트랜잭션이 동시에 실행되는 경우에도 트랜잭션이 다른 트랜잭션에 영향을 주지 않아야 합니다.
- 내구성: 트랜잭션이 성공적으로 완료되면 결과가 무기한 반영되어야 합니다.
지속성 컨텍스트
지속성 컨텍스트엔터티를 지속적으로 저장하는 환경입니다.
엔터티가 엔터티 관리자에 의해 저장되거나 검색될 때 엔터티는 지속성 컨텍스트에서 저장되고 관리됩니다.
지속성 컨텍스트는 엔티티 관리자를 생성할 때 생성됩니다.
지속성 컨텍스트는 엔티티 관리자를 통해 액세스하고 관리할 수 있습니다.
지속성 컨텍스트를 이해하려면 엔터티의 수명 주기와 지속성 컨텍스트의 속성을 알아야 합니다.
Spring 컨테이너의 기본 전략
위 그림과 같이 트랜잭션이 시작되면 지속성 컨텍스트가 시작되고 트랜잭션이 종료되면 컨트롤러 계층에서 하나의 논리 연산에 두 개의 지속성 컨텍스트가 사용되기 때문에 지속성 컨텍스트가 종료될 때 문제가 발생합니다.
그렇다면 하나의 서비스가 모든 논리적 작업을 수행해야 할까요? 그렇다면 서비스별 역할 분리가 제대로 이뤄지지 않는 것일까요?
OSIV
보기에서 세션 열기(OSIV)지속성 컨텍스트를 보기에 열어 두는 것을 의미합니다.
요청이 들어오면 Servlet Filter 또는 Spring Interceptor가 트랜잭션을 시작하여 지속성 컨텍스트를 생성하고 요청이 종료되면 트랜잭션과 지속성 컨텍스트가 함께 종료됩니다.
이 메서드를 사용하면 요청이 완료될 때까지 지속 컨텍스트가 존재하므로 조회 중인 엔터티의 지속 상태를 유지할 수 있습니다.
그러나 위의 방법은 View와 같은 Presentation Layer의 Entity가 수정될 수 있다는 문제점이 있다.
이를 방지하기 위해 엔터티를 읽기 전용 인터페이스, 엔터티 래핑 또는 DTO 반환으로 노출할 수 있지만 거의 사용되지 않습니다.
이는 Spring Framework가 이를 보완하는 OSIV를 제공하기 때문입니다.
Spring 프레임워크에서 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV이다.
작동 원리는 다음과 같습니다.
- 요청이 들어오면 서블릿 필터 또는 Spring 인터셉터에서 지속성 컨텍스트가 생성됩니다.
그러나 트랜잭션이 시작되지 않습니다. - 서비스 계층에서 트랜잭션이 시작되면 이전에 생성된 지속성 컨텍스트를 검색하여 트랜잭션이 시작됩니다.
- 서비스 계층이 종료되면 트랜잭션을 커밋하고 지속성 컨텍스트를 지웁니다.
트랜잭션은 종료되지만 지속성 컨텍스트는 종료되지 않습니다. - 지속성 컨텍스트가 보존되기 때문에 쿼리된 엔터티는 지속성 상태를 유지합니다.
- 요청으로 돌아오면 지속성 컨텍스트를 닫습니다.
그 시점에서 플러시를 호출하지 않고 즉시 종료됩니다.
지속성 컨텍스트를 통한 모든 변경은 트랜잭션 내에서 이루어져야 합니다.
트랜잭션 없이 엔터티를 변경하고 지속성 컨텍스트를 플러시하면 예외가 발생합니다.
트랜잭션 없이 엔터티 검색이 가능하며 이를 트랜잭션 없는 읽기라고 합니다.
프록시를 초기화하는 레이지 로딩도 조회 기능이므로 비트랜잭션 읽기가 가능합니다.
졸업 증서
위의 정보를 사용하여 질문에 답할 수 있습니다.
1. id와 일치하는 사용자를 검색한 후, 두 개의 지속성 컨텍스트를 사용하여 팔로워를 검색합니까?
OSIV로 인해 요청에서 응답까지 지속성 컨텍스트가 사용됩니다.
2. 두 개의 지속성 컨텍스트를 사용하는 경우 사용자가 지속성 컨텍스트를 떠나 의도하지 않은 작업을 수행하거나 사용자를 다시 탐색하지 않습니까?
지속성 컨텍스트가 종료되지 않았기 때문에 사용자는 지속성 컨텍스트에 존재합니다.
따라서 후속 서비스에서 새로운 트랜잭션을 시작할 때 의도하지 않은 조치를 취하거나 다시 확인하지 않습니다.
그렇다면 무조건 OSIV를 사용해도 괜찮을까요? 별로. OSIV에도 단점이 있습니다.
spring.jpa.open-in-view
의 기본 전략 true
오전. 이 전략은 너무 오랫동안 데이터베이스 연결 리소스를 사용하기 때문에 트래픽에 중요한 응용 프로그램에 대한 실시간 연결이 부족할 수 있습니다.
결국 실패 확률이 높아진다.
또한 지속성 컨텍스트는 여러 트랜잭션 간에 공유될 수 있습니다.
트랜잭션 롤백 상황에서는 각별히 주의하십시오. 트랜잭션 롤백은 데이터베이스의 반사만 롤백하며 변경된 Java 개체를 원래 상태로 복원하지 않습니다.
따라서 롤백할 때 데이터베이스의 데이터는 원래 상태로 복원되지만 개체는 지속성 컨텍스트에서 수정된 상태로 유지됩니다.
트랜잭션이 변경되지 않고 롤백된 지속성 컨텍스트를 사용하는 것은 위험합니다.
따라서 새 지속성 컨텍스트를 만들어 사용하십시오. EntityManager.clear()
호출하여 초기화한 후 사용해야 합니다.
위와 같은 문제를 방지하기 위해 Spring Framework는 지속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 설정하여 트랜잭션이 롤백될 때 지속성 컨텍스트를 초기화하여 잘못된 지속성 컨텍스트를 사용하는 문제를 방지합니다.
OSIV의 단점이 있기 때문에 실무에서는 OSIV를 끈 상태에서 command와 query를 분리하는 방식을 사용한다고 합니다.
두 사람의 이해관계를 명확히 분리한 결정은 유지보수 관점에서 충분히 일리가 있다고 한다.
참조
- Java ORM 표준 JPA 프로그래밍