Session per request 는 영속성 세션을 연결하고 라이프 사이클을 함께 요청하는 트랜잭션 패턴이다. 놀랍지도 않게, Spring 은 이 패턴의 자체구현인 OpenSessionInViewInterceptor 을 따르고, 이는 lazy association 을 다루는 것을 용이하게 하고, 개발생산성을 향상시킨다. 첫째로, 이 튜토리얼에서 우리는 내부적으로 interceptor 가 어떻게 동작하는지 배우고, 그리고 나서 이 논란 많은 패턴이 우리 애플리케이션에게 어떻게 양날의 검이 될 수 있는지 살펴볼 것이다.
2. Open Session in View 소개
OSIV 를 이해하는데 도움을 주기 위해 우리에게 들어온 요청이 있다고 가정해보자.
Spring 은 request 이 시작될 때 새로운 Hibernate Session 을 연다. 이러한 Session 들은 무조건적으로 database 에 연결되지는 않는다. (database 와 연결되지 않는 Session 들도 있다.)
Application 이 Session 이 필요한 모든 순간에, Application 은 이미 존재하는 Session 을 재사용 할 것이다.
요청이 끝나면, 동일한 interceptor 가 Session 을 닫는다.
처음에는, 이 기능을 활성화하는 것이 합리적일 수 있다. 어쨌든, framework 가 Session 생성과 종료를 알아서 다루기 때문에 개발자들은 이런 보이는 수준이 낮은 세부사항에 대해 걱정할 필요가 없다. 결국 이것은 개발 생산성을 북돋는다.
그러나 때때로, OSIV 는 production 에서 미묘한 성능 문제를 일으킬 수 있다.
2.1 Spring Boot
기본적으로, OSIV 는 Spring Boot Application 에서 활성화 되어있다. Spring Boot 2.0 부터는 명시적으로 구성하지 않은 경우 애플리케이션 시작 시 OSIV 가 활성화되어 있는 사실을 경고한다.
spring.jpa.open-in-view is enabled by default. Therefore, database
queries may be performed during view rendering.Explicitly configure
spring.jpa.open-in-view to disable this warning
spring.jpa.open-in-view configuration property 를 사용해서 OSIV 를 비활성화할 수 있다.
spring.jpa.open-in-view=false
2.2 Pattern or Anti-Pattern?
OSIV 를 향한 혼합된 반응들이 항상 있어왔다. OSIV 를 지지하는 측의 주요 주장은 개발자 생산성이며, 특히 lazy association 을 다룰 때이다.
반면에, database 성능 이슈가 OSIV 를 지지하지 않는 측의 주요 주장이다. 나중에, 우리는 두 주장 모두 자세하게 다룰 것이다.
3. Lazy Initialization Hero
OSIV 가 Session lifecycle 을 각 요청에 바인딩하기 때문에 Hibernate는 명시적인 @Transactional 서비스를 실행한 후에도 lazy association 을 해결할 수 있다.
이를 더 잘 이해하기 위해, 우리가 우리의 users 와 그들의 security permission 을 모델링 한다고 가정해보자.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
@ElementCollection
private Set<String> permissions;
// getters and setters
}
다른 one-to-many 와 many-to-many 관계들과 비슷하게, permissions 속성은 lazy collection 이다. 그리고 우리의 service 계층 구현에서, @Transactional 을 사용해 우리의 transactional boundary 를 명시적으로 정해보자.
@Service
public class SimpleUserService implements UserService {
private final UserRepository userRepository;
public SimpleUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional(readOnly = true)
public Optional<User> findOne(String username) {
return userRepository.findByUsername(username);
}
}
3.1 The Exception
우리 코드가 findOne 메서드를 호출할 때 우리가 기대하는 동작은 다음과 같다.
처음에 Spring proxy 가 call 을 가로채고 현재 transaction 을 가지거나 존재하지 않으면 새로 하나 만든다.
그리고 나서 Spring proxy 는 메서드 호출을 우리의 구현에 위임한다.
마지막으로, proxy 는 transaction 을 commit 하고 결과적으로 기반이 되는 session 을 닫는다. 결국 우리는 service 계층에서만 해당 세션을 필요로 한다.
findOne 메서드 구현에서, 우리는 permission collection 을 초기화하지 않는다. 그로므로, 우리는 findOne 메서드가 return 한 후 permissions 를 사용할 수 없다. 만약 이 속성을 순회(iterate)한다면, LazyInitializationException 을 받을 것이다.
3.2 Welcome to the Real World
"permissions" 속성을 사용할 수 있는지 확인하기 위해 간단한 REST 컨트롤러를 작성해 보자.
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{username}")
public ResponseEntity<?> findOne(@PathVariable String username) {
return userService
.findOne(username)
.map(DetailedUserDto::fromEntity)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
여기서, 우리는 entity 를 DTO 로 변환하는 동안 permissios 를 순회한다. 우리는 LazyInitializationException 이 발생할 것으로 예상하기 때문에 밑의 테스트는 통과되지 않아야 한다.
request 가 시작될 때 OSIV 가 새로운 세션을 생성하기 때문에, transactional proxy 는 새로운 것을 생성하는 대신에 현재 사용 가능한 세션을 사용한다.
그래서, 우리의 예상과는 다르게, 명시적인 @Transactional 밖에서도 permissions 속성을 사용할 수 있다. 더욱이, 현재 요청 범위 어디에서든지 이러한 종류의 lazy associations 를 가져올 수 있다.
3.3 On Developer Productivity
OSIV 가 활성화되지 않았다면, Transactional Context 에서 필수적인 모든 lazy association 를 일일이 초기화 해줘야 한다.
@Override
@Transactional(readOnly = true)
public Optional<User> findOne(String username) {
Optional<User> user = userRepository.findByUsername(username);
user.ifPresent(u -> Hibernate.initialize(u.getPermissions()));
return user;
}
이제 OSIV 가 개발자 생산성에 미치는 영향이 명백하다. 그러나 항상 개발자 생산성만이 중요한 것은 아니다.
4. Performance Villain
@Override
public Optional<User> findOne(String username) {
Optional<User> user = userRepository.findByUsername(username);
if (user.isPresent()) {
// remote call
}
return user;
}
여기서는 원격 서비스를 기다리는 동안 연결된 세션을 유지할 필요가 확실히 없기 때문에 @Transactional 을 지우고 있다.
4.1. Avoiding Mixed IOs
만약 @Transactional 을 지우지 않는다면 어떤 일이 발생하는지 살펴보자. 새로운 원격 서비스가 평소보다 약간 늦게 응답한다고 가정해보자.
먼저 Spring proxy 는 현재 세션을 얻거나 새로운 세션을 만든다. 어느 쪽이든, 이 세션은 아직 연결되지 않는다. 즉, pool 의 어떠한 커넥션도 사용하지 않는다.
user 를 찾기 위해 쿼리를 실행하면, 세션은 연결되고 pool 에서 커넥션을 빌린다.
만약 전체 메서드가 trasactional 이라면, 빌린 커넥션을 유지한 채 느린 원격 서비스를 호출한다.
이 기간동안, fetchOne 메서드 호출이 폭주한다고 상상해보자. 그런 다음, 잠시 후 모든 커넥션이 해당 api 호출을 기다릴 수 있다. 따라서 곧 데이터베이스 커넥션이 고갈될 수 있다.
transactional context 안에서 database IO 를 다른 타입의 IO 와 함께 사용하는 것은 bad smell 이고 어떤 대가를 치뤄서라도 피해야 한다.
어쨌든, 서비스에서 @Transactional 어노테이션을 지웠기 때문에 안전할 것이라고 예상된다.
4.2 Exhausting the Connection Pool
OSIV 가 활성화 중일 때, @Transactional 을 지우더라도 항상 현재 request 범위에 세션이 존재한다. 이 세션은 초기에 연결되지는 않더라도, 첫 database io 이후, 세션은 연결되고 request 가 끝날 때까지 유지된다. 그래서, 우리의 무해해 보이고 최적화 되어 보이는 서비스 구현은 OSIV 가 존재할 경우 재앙의 레시피가 될 수. 있다.
@Override
public Optional<User> findOne(String username) {
Optional<User> user = userRepository.findByUsername(username);
if (user.isPresent()) {
// remote call
}
return user;
}
OSIV 가 활성화 되어 있는 동안 발생하는 일은 다음과 같다.
요청이 시작될 때, 해당 필터는 새로운 세션을 생성한다.
findByUsername 메서드를 부를 때, 그 세션은 풀에서 커넥션을 빌린다.
그 세션은 request 가 끝날 때까지 유지된다.
우리의 서비스 코드가 커넥션 풀을 고갈시키지 않는다고 예상할지라도, OSIV 의 단순한 존재만으로 전체 Application 이 응답하지 않을 수 있다.
더 심각한 문제는 문제의 원인(느린 원격 서비스)과 증상(데이터베이스 커 넥션풀 )관련이 없다는 것이다. 이러한 작은 상관 관계 때문에, 이러한 성능 문제를 production 환경에서 진단하기가 어렵다.
4.3 Unnecessary Queries
안타깝게도, 연결 풀을 고갈시키는 것은 OSIV 와 관련된 유일한 성능 문제가 아니다.
세션이 request lifecycle 전체에 열려 있기 때문에, 일부 속성 탐색은 transaction context 외부에서 몇 가지 불필요한 쿼리를 추가로 유발할 수 있다. 이로 인해 n+1 select 문제가 발생할 수도 있으며, 최악의 경우 이를 production 에서까지 인지하지 못할 수 있습니다.
설상가상으로 세션이 auto-commit 모드에서 모든 추가 쿼리를 실행한다는 것이다. auto-commit 모드에서는 각 SQL 문이 트랜잭션으로 간주되어 실행 즉시 자동으로 commit 된다. 이로 인해 데이터베이스에 많은 압력이 가해진다.
5. Choose Wisely
OSIV 가 패턴인지 아니면 안티 패턴인지는 중요하지 않다. 여기서 가장 중요한 것은 우리가 살아가는 현실이다.
만약 우리가 단순한 CRUD 서비스를 개발 중이라면, OSIV 를 사용하는 것이 합리적일 수 있다. 왜냐하면 우리는 이러한 성능 문제를 전혀 마주치지 않을 수도 있기 때문이다.
반면에, 만약 우리가 많은 원격 서비스를 호출하거나 우리의 transaction context 외부에서 많은 작업이 이루어지고 있다면, OSIV 를 완전히 비활성화하는 것이 매우 권장된다.
의심스러울 때는 OSIV 없이 시작하는 편이 좋다. 나중에 필요하면 쉽게 활성화할 수 있기 때문이다. 반면에 이미 활성화된 OSIV 를 비활성화하는 것은 LazyInitializationExceptions 를 처리해야 할 수 있기 때문에 번거로울 수 있다.
중요한 점은 OSIV 를 사용하거나 무시할 때 발생하는 trade-off 에 대해 인식해야 한다는 것이다.
만약 OSIV 를 비활성화한다면, lazy associations을 다룰 때 잠재적인 LazyInitializationExceptions을 방지할 방법을 찾아야 한다. lazy associations을 처리하는 몇 가지 접근 방법 중에서, 여기서 두 가지를 나열해보겠다.
6.1 Entity Graph
Spring Data JPA 에서 쿼리 메서드를 정의할 때, 엔티티의 일부를 즉시 가져오기 위해 쿼리 메서드에 @EntityGraph 어노테이션을 사용할 수 있다.
1. 개요
Session per request 는 영속성 세션을 연결하고 라이프 사이클을 함께 요청하는 트랜잭션 패턴이다. 놀랍지도 않게, Spring 은 이 패턴의 자체구현인 OpenSessionInViewInterceptor 을 따르고, 이는 lazy association 을 다루는 것을 용이하게 하고, 개발생산성을 향상시킨다. 첫째로, 이 튜토리얼에서 우리는 내부적으로 interceptor 가 어떻게 동작하는지 배우고, 그리고 나서 이 논란 많은 패턴이 우리 애플리케이션에게 어떻게 양날의 검이 될 수 있는지 살펴볼 것이다.
2. Open Session in View 소개
OSIV 를 이해하는데 도움을 주기 위해 우리에게 들어온 요청이 있다고 가정해보자.
처음에는, 이 기능을 활성화하는 것이 합리적일 수 있다. 어쨌든, framework 가 Session 생성과 종료를 알아서 다루기 때문에 개발자들은 이런 보이는 수준이 낮은 세부사항에 대해 걱정할 필요가 없다. 결국 이것은 개발 생산성을 북돋는다.
그러나 때때로, OSIV 는 production 에서 미묘한 성능 문제를 일으킬 수 있다.
2.1 Spring Boot
기본적으로, OSIV 는 Spring Boot Application 에서 활성화 되어있다. Spring Boot 2.0 부터는 명시적으로 구성하지 않은 경우 애플리케이션 시작 시 OSIV 가 활성화되어 있는 사실을 경고한다.
spring.jpa.open-in-view configuration property 를 사용해서 OSIV 를 비활성화할 수 있다.
2.2 Pattern or Anti-Pattern?
OSIV 를 향한 혼합된 반응들이 항상 있어왔다. OSIV 를 지지하는 측의 주요 주장은 개발자 생산성이며, 특히 lazy association 을 다룰 때이다.
반면에, database 성능 이슈가 OSIV 를 지지하지 않는 측의 주요 주장이다. 나중에, 우리는 두 주장 모두 자세하게 다룰 것이다.
3. Lazy Initialization Hero
OSIV 가 Session lifecycle 을 각 요청에 바인딩하기 때문에 Hibernate는 명시적인 @Transactional 서비스를 실행한 후에도 lazy association 을 해결할 수 있다.
이를 더 잘 이해하기 위해, 우리가 우리의 users 와 그들의 security permission 을 모델링 한다고 가정해보자.
다른 one-to-many 와 many-to-many 관계들과 비슷하게, permissions 속성은 lazy collection 이다. 그리고 우리의 service 계층 구현에서, @Transactional 을 사용해 우리의 transactional boundary 를 명시적으로 정해보자.
3.1 The Exception
우리 코드가 findOne 메서드를 호출할 때 우리가 기대하는 동작은 다음과 같다.
findOne 메서드 구현에서, 우리는 permission collection 을 초기화하지 않는다. 그로므로, 우리는 findOne 메서드가 return 한 후 permissions 를 사용할 수 없다. 만약 이 속성을 순회(iterate)한다면, LazyInitializationException 을 받을 것이다.
3.2 Welcome to the Real World
"permissions" 속성을 사용할 수 있는지 확인하기 위해 간단한 REST 컨트롤러를 작성해 보자.
여기서, 우리는 entity 를 DTO 로 변환하는 동안 permissios 를 순회한다. 우리는 LazyInitializationException 이 발생할 것으로 예상하기 때문에 밑의 테스트는 통과되지 않아야 한다.
그러나, 이 테스트는 어떠한 exception 도 던지지 않고 통과된다.
request 가 시작될 때 OSIV 가 새로운 세션을 생성하기 때문에, transactional proxy 는 새로운 것을 생성하는 대신에 현재 사용 가능한 세션을 사용한다.
그래서, 우리의 예상과는 다르게, 명시적인 @Transactional 밖에서도 permissions 속성을 사용할 수 있다. 더욱이, 현재 요청 범위 어디에서든지 이러한 종류의 lazy associations 를 가져올 수 있다.
3.3 On Developer Productivity
OSIV 가 활성화되지 않았다면, Transactional Context 에서 필수적인 모든 lazy association 를 일일이 초기화 해줘야 한다.
이제 OSIV 가 개발자 생산성에 미치는 영향이 명백하다. 그러나 항상 개발자 생산성만이 중요한 것은 아니다.
4. Performance Villain
여기서는 원격 서비스를 기다리는 동안 연결된 세션을 유지할 필요가 확실히 없기 때문에 @Transactional 을 지우고 있다.
4.1. Avoiding Mixed IOs
만약 @Transactional 을 지우지 않는다면 어떤 일이 발생하는지 살펴보자. 새로운 원격 서비스가 평소보다 약간 늦게 응답한다고 가정해보자.
이 기간동안, fetchOne 메서드 호출이 폭주한다고 상상해보자. 그런 다음, 잠시 후 모든 커넥션이 해당 api 호출을 기다릴 수 있다. 따라서 곧 데이터베이스 커넥션이 고갈될 수 있다.
transactional context 안에서 database IO 를 다른 타입의 IO 와 함께 사용하는 것은 bad smell 이고 어떤 대가를 치뤄서라도 피해야 한다.
어쨌든, 서비스에서 @Transactional 어노테이션을 지웠기 때문에 안전할 것이라고 예상된다.
4.2 Exhausting the Connection Pool
OSIV 가 활성화 중일 때, @Transactional 을 지우더라도 항상 현재 request 범위에 세션이 존재한다. 이 세션은 초기에 연결되지는 않더라도, 첫 database io 이후, 세션은 연결되고 request 가 끝날 때까지 유지된다. 그래서, 우리의 무해해 보이고 최적화 되어 보이는 서비스 구현은 OSIV 가 존재할 경우 재앙의 레시피가 될 수. 있다.
OSIV 가 활성화 되어 있는 동안 발생하는 일은 다음과 같다.
우리의 서비스 코드가 커넥션 풀을 고갈시키지 않는다고 예상할지라도, OSIV 의 단순한 존재만으로 전체 Application 이 응답하지 않을 수 있다.
더 심각한 문제는 문제의 원인(느린 원격 서비스)과 증상(데이터베이스 커 넥션풀 )관련이 없다는 것이다. 이러한 작은 상관 관계 때문에, 이러한 성능 문제를 production 환경에서 진단하기가 어렵다.
4.3 Unnecessary Queries
안타깝게도, 연결 풀을 고갈시키는 것은 OSIV 와 관련된 유일한 성능 문제가 아니다.
세션이 request lifecycle 전체에 열려 있기 때문에, 일부 속성 탐색은 transaction context 외부에서 몇 가지 불필요한 쿼리를 추가로 유발할 수 있다. 이로 인해 n+1 select 문제가 발생할 수도 있으며, 최악의 경우 이를 production 에서까지 인지하지 못할 수 있습니다.
설상가상으로 세션이 auto-commit 모드에서 모든 추가 쿼리를 실행한다는 것이다. auto-commit 모드에서는 각 SQL 문이 트랜잭션으로 간주되어 실행 즉시 자동으로 commit 된다. 이로 인해 데이터베이스에 많은 압력이 가해진다.
5. Choose Wisely
OSIV 가 패턴인지 아니면 안티 패턴인지는 중요하지 않다. 여기서 가장 중요한 것은 우리가 살아가는 현실이다.
만약 우리가 단순한 CRUD 서비스를 개발 중이라면, OSIV 를 사용하는 것이 합리적일 수 있다. 왜냐하면 우리는 이러한 성능 문제를 전혀 마주치지 않을 수도 있기 때문이다.
반면에, 만약 우리가 많은 원격 서비스를 호출하거나 우리의 transaction context 외부에서 많은 작업이 이루어지고 있다면, OSIV 를 완전히 비활성화하는 것이 매우 권장된다.
의심스러울 때는 OSIV 없이 시작하는 편이 좋다. 나중에 필요하면 쉽게 활성화할 수 있기 때문이다. 반면에 이미 활성화된 OSIV 를 비활성화하는 것은 LazyInitializationExceptions 를 처리해야 할 수 있기 때문에 번거로울 수 있다.
중요한 점은 OSIV 를 사용하거나 무시할 때 발생하는 trade-off 에 대해 인식해야 한다는 것이다.
(trade-off : '[이율배반](http://terms.naver.com/ncrEntry.nhn?dicId=common_sense&ncrDocId=ba2_19-1-100)'의 의미를 가지는 용어로, 임금이나 물가의 안정과 [완전고용](http://terms.naver.com/ncrEntry.nhn?dicId=common_sense&ncrDocId=ba2_8-3-101)을 동시에 실현시키는 것이 힘들며 양자가 서로 상충하는 관계에 있음을 설명한다. https://terms.naver.com/entry.naver?docId=929839&cid=43667&categoryId=43667)
6. Alternatives
만약 OSIV 를 비활성화한다면, lazy associations을 다룰 때 잠재적인 LazyInitializationExceptions을 방지할 방법을 찾아야 한다. lazy associations을 처리하는 몇 가지 접근 방법 중에서, 여기서 두 가지를 나열해보겠다.
6.1 Entity Graph
Spring Data JPA 에서 쿼리 메서드를 정의할 때, 엔티티의 일부를 즉시 가져오기 위해 쿼리 메서드에 @EntityGraph 어노테이션을 사용할 수 있다.
여기서 우리는 기본적으로 lazy collection 인 permissions 속성을 즉시 로드하기 위해 ad-hoc entity graph 를 정의하고 있다.
동일한 쿼리에서 여러 프로젝션을 반환해야 하는 경우, 다른 엔티티 그래프 구성을 가진 여러 쿼리를 정의해야 한다.
6.2 Caveats When Using Hibernate.initialize()
어떤 사람들은 엔티티 그래프 대신에 필요한 곳마다 악명 높은 Hibernate.initialize()를 사용하여 lazy associations 를 가져올 수 있다고 주장할 수 있다.
그들은 이를 영리하게 활용하며 getPermissions() 메서드를 호출하여 가져오는 프로세스를 트리거할 것을 제안할 수도 있다.
두 가지 방법 모두 권장되지 않는데, 원래의 쿼리 외에도 (최소한) 하나의 추가 쿼리를 발생시키기 때문이다. 즉, Hibernate 는 user 와 그들의 권한을 가져오기 위해 다음과 같은 쿼리를 생성한다.
대부분의 데이터베이스는 두 번째 쿼리를 실행하는 데에 상당히 능숙하지만, 우리는 그 추가 네트워크 왕복을 피해야 한다.
반면에, 엔티티 그래프나 심지어 Fetch Joins 를 사용한다면, Hibernate 는 단 하나의 쿼리로 모든 필요한 데이터를 가져올 것입니다.
7. Conclusion