티스토리 뷰
개요
@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
어노테이션을 사용하였기에 메서드 로직은 트랜잭션 안에서 동작하게 된다. 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 시작하고, 트랜잭션이 종료될 때 영속성 컨텍스트가 종료된다는 뜻이다. 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
영속성 컨텍스트 전략을 알고 있었기에 다음과 같은 의문이 들게 되었다.
- ID에 해당하는 유저를 조회 후 팔로워를 조회하는데, 2개의 영속성 컨텍스트를 사용하는 걸까?
- 2개의 영속성 컨텍스트를 사용한다면, user가 영속성 컨텍스트에서 벗어나 의도하지 않은 동작이 수행되거나, user를 다시 조회하게 되는 것이 아닐까?
질문에 대한 답을 정리해보고자 한다.
트랜잭션 범위의 영속성 컨텍스트
트랜잭션
트랜잭션(Transaction)은 데이터베이스(Database)에서 수행되는 작업의 단위를 말한다. 하나의 논리적인 작업을 완료하기 위해 하나 이상의 쿼리(Query)로 구성된다. 물건을 구입하는 상황이 있을 때, 돈을 지불하고 물건을 산다는 하나의 논리적인 작업이 될 수 있다. 트랜잭션 내에서는 모든 작업이 성공적으로 완료되어야 한다. 중간에 문제가 발생하여 작업이 실패할 경우, 모든 작업을 취소하고 이전 상태로 되돌릴 수 있도록 한다.
트랜잭션을 안전하게 처리하기 위해서는 ACID
라는 원칙을 준수해야 한다.
- 원자성(Atomicity): 트랜잭션 내의 모든 작업은 모두 완료되거나, 전혀 실행되지 않아야 한다. 부분적으로 실행되거나 중단되어서는 안 된다.
- 일관성(Consistency): 트랜잭션이 실행이 완료되면 일관성 있는 데이터베이스 상태로 유지되어야 한다.
- 고립성(Isolation): 동시에 여러 트랜잭션이 수행되더라도 각각 트랜잭션은 다른 트랜잭션에 영향을 주지 않아야 한다.
- 지속성(Durability): 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 반영되어야 한다.
영속성 컨텍스트
영속성 컨텍스트(Persistence Context)는 엔티티를 영구 저장하는 환경이다. 엔티티 매니저로 엔티티를 저장하거나 조회하면 해당 엔티티는 영속성 컨텍스트에 보관하고 관리한다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다. 엔티티 매니저를 통해 영속성 컨텍스트에 접근할 수 있고, 영속성 컨텍스트를 관리할 수 있다.
영속성 컨텍스트를 이해하기 위해서는 엔티티의 생명주기, 영속성 컨텍스트의 특징 등을 알아야 하지만, 지금은 위의 질문을 정리하기 위한 글이기에 자세한 설명은 넘어가도록 한다.
스프링 컨테이너의 기본 전략
위의 그림과 같이 트랜잭션이 시작할 때, 영속성 컨텍스트가 시작되고 트랜잭션이 종료될 때, 영속성 컨텍스트가 종료되면 컨트롤러 레이어에 존재하는 하나의 논리적인 작업에 2개의 영속성 컨텍스트를 사용하게 되기에 문제가 발생할 것만 같다. 그러면 하나의 서비스에서 논리적인 작업의 모든 역할을 수행해야만 할까? 그렇다면 서비스마다 역할의 분리가 제대로 이루어지지 않는 것이 아닐까?
OSIV
OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 의미이다.
요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하면서 영속성 컨텍스트를 만들고, 요청이 끝날 때 트랜잭션과 영속성 컨텍스트를 함께 종료한다. 해당 방식을 사용한다면 요청이 종료될 때까지 영속성 컨텍스트가 존재하고 있기에 조회한 엔티티의 영속 상태를 유지할 수 있다.
하지만 위의 방법은 뷰와 같은 프레젠테이션 계층에서 엔티티를 수정할 수 있는 문제가 발생한다. 이를 막기 위해서 엔티티를 읽기 전용 인터페이스로 제공하거나, 엔티티 레핑, DTO 반환 등의 방법을 사용할 수 있지만, 거의 사용하지 않는다. 스프링 프레임워크가 이를 보완한 OSIV를 제공하기 때문이다.
스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서 트랜잭션을 사용하는 OSIV이다. 동작 원리는 아래와 같다.
- 요청이 들어오면 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 하지만 트랜잭션은 시작하지 않는다.
- 서비스 계층에서 트랜잭션을 시작할 때 미리 생성한 영속성 컨텍스트를 찾아와 트랜잭션을 시작한다.
- 서비스 계층이 종료되면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시 한다. 트랜잭션은 종료되지만, 영속성 컨텍스트는 종료되지 않는다.
- 영속성 컨텍스트는 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
- 요청으로 다시 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다.
영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야 한다. 트랜잭션 없이 엔티티를 수정하고 영속성 컨텍스트를 플러시 하면 예외가 발생한다. 엔티티 조회는 트랜잭션이 없어도 가능한데, 이를 트랜잭션 없이 읽기라고 한다. 프록시를 초기화하는 지연 로딩도 조회 기능이므로 트랜잭션 없이 읽기가 가능하다.
결론
위의 내용을 바탕으로 질문에 대한 답을 해볼 수 있다.
1. ID에 해당하는 유저를 조회 후 팔로워를 조회하는데, 2개의 영속성 컨텍스트를 사용하는 걸까?
OSIV로 인해 요청부터 응답까지 하나의 영속성 컨텍스트를 사용한다.
2. 2개의 영속성 컨텍스트를 사용한다면, user가 영속성 컨텍스트에서 벗어나 의도하지 않은 동작이 수행되거나, user를 다시 조회하게 되는 것이 아닐까?
영속성 컨텍스트가 종료되지 않았기에 user는 영속성 컨텍스트에 존재한다. 따라서 팔로우 서비스에서 새로운 트랜잭션을 시작할 때 의도하지 않은 동작이 수행되거나, 다시 조회하지 않는다.
그렇다면 OSIV는 무조건 사용해도 괜찮을까? 그렇지 않다. OSIV도 단점이 존재한다. spring.jpa.open-in-view
의 기본 전략은 true
이다. 이 전략은 너무 오랫동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 결국 장애 발생 가능성이 높아지게 된다.
또한, 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 한다. 특히 트랜잭션 롤백 상황에서 주의해야 한다. 트랜잭션을 롤백하는 것은 데이터베이스의 반영사항만 롤백하는 것이지 수정한 자바 객체까지 원상태로 복구하지 않는다. 그렇기에 롤백이 발생하면 데이터베이스의 데이터는 원래대로 복구되지만, 객체는 수정된 상태로 영속성 컨텍스트에 남아 있다. 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하기에 새로운 영속성 컨텍스트를 생성해서 사용하거나, EntityManager.clear()
를 호출해서 초기화한 다음 사용해야 한다.
스프링 프레임워크는 위의 문제를 예방하기 위해 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 설정하면 트랜잭션 롤백 시 영속성 컨텍스트를 초기화해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.
OSIV의 단점들이 존재하기에 실무에서는 OSIV를 끈 상태로 Command와 Query를 분리하는 방법을 사용한다고 한다. 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미가 있다고 한다.
User user = userQueryService.findById(userId);
postService.createPost(user)
만약에 OSVI를 끈 채로 특정 유저가 게시글을 작성하는 로직을 Command와 Query를 분리하여 위와 같이 컨트롤러에 작성하고자 한다면, postService에서 user는 준영속 상태로 존재하게 된다.
createPost 트랜잭션 안에서 준영속 상태인 user를 사용할 수 있지만, user의 데이터를 변경하더라도 아무런 변화가 발생하지 않는다. 이런 경우 Post에서 user를 참조할 때 참조값을 넣는 정도로 사용할 수 있다. 만약 user의 데이터도 변경해야 한다면 트랜잭션 안에서 다시 user를 조회해야 한다.
참고
- 자바 ORM 표준 JPA 프로그래밍
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] @DataJpaTest 데이터베이스 환경 문제 (3) | 2023.07.07 |
---|---|
[Spring Boot] 프로메테우스, 그라파나를 이용한 스프링 부트 모니터링 (3) | 2023.07.02 |
[Spring] Asciidoctor를 통해 Spring Rest Docs 자동 생성 (2) | 2023.02.04 |
동아리 홈페이지 투표 개발 3 - 개발 및 적용 (0) | 2022.10.14 |
동아리 홈페이지 투표 개발 2 - WebSocket과 STOMP (0) | 2022.10.05 |