그동안 OSIV를 잘못 알았어서, 제대로 이해하기 위해 정리한다 !
OSIV?
- Open Session In View
- 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
- 영속성 컨텍스트가 살아있으면, 엔티티는 영속 상태로 유지된다. 따라서 뷰에서도 지연 로딩을 사용할 수 있다.
과거 OSIV - 요청당 트랜잭션
- OSIV의 핵심은 뷰에서도 지연 로딩을 할 수 있게 하는 것이다.
- 가장 단순한 구현 방법
- 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작한다.
- 요청이 끝날 때, 트랜잭션도 같이 끝낸다.
- 이렇게 하면, 영속성 컨텍스트가 시작부터 마지막까지 살아있다. 따라서 조회한 엔티티도 영속 상태를 유지한다.
- 그럼 뷰에서도 지연 로딩을 할 수 있어 엔티티를 미리 초기화할 필요가 없다.
문제점
- 해당 OSIV는 프레젠테이션 계층에서 엔티티를 변경할 수 있다는 문제점을 가진다.
- e.g. 유저를 출력할 때, 유저 이름을 보안상의 이유로 “dani”로 변경해서 출력해야 한다면?
1
2
3
4
5
6
7
8
9
10
11
12
13
class UserController {
public String readById(Long id) {
User user = userService.readById(id);
// 여기서 유저 이름을 변경한다.
user.setName("dani");
model.setAttribute("user", user);
...
}
}
- 개발자는 뷰에서만 유저 이름을 “dani”로 변경하고 싶었다. 실제 데이터베이스의 유저 이름을 변경할 의도는 아니었다.
- 요청당 트랜잭션 방식의 OSIV는 뷰를 렌더링한 뒤에 트랜잭션을 커밋한다. 영속성 컨텍스트가 플러시된다는 의미이다.
- 영속성 컨텍스트의 변경 감지가 작동한다. 데이터베이스의 유저 이름이 “dani”로 변경되는 심각한 문제를 초래한다.
- 서비스 계층처럼 비즈니스 로직을 수행하는 곳에서 데이터를 변경하는 것은 당연하다. 그러나 프레젠테이션 계층에서 데이터를 잠시 변경했다고, 실제 데이터베이스에 이 내용이 반영되는 건 애플리케이션의 유지보수를 어렵게 만든다.
해결 방안
- 프레젠테이션 계층에서 엔티티를 수정하지 못하게 막는다.
- 엔티티를 읽기 전용 인터페이스로 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface UserView {
// 읽기 전용 메소드만 제공한다.
String getName();
}
@Entity
class User implement UserView {
...
}
class UserService {
public UserView readById(Long id) {
return userRepository.findById(id);
}
}
- 엔티티를 래핑한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class UserWrapper {
private User user;
public UserWrapper(user) {
this.user = user;
}
// 읽기 전용 메소드만 제공한다.
public String getName() {
return user.getName();
}
}
- DTO만 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class UserDto {
private String name;
public String getName() {
return name;
}
public void setName() {
this.name = name;
}
}
class UserController {
public UserDto readById(Long id) {
...
UserDto userDto = new UserDto();
userDto.setName("dani");
return userDto;
}
}
- 이는 모두 코드량이 늘어난다는 단점이 있다.
- 한편 이어지는 OSIV를 활용하면, 코드량을 유지하며 앞선 문제점을 보완할 수 있다.
스프링 OSIV - 비즈니스 계층 트랜잭션
- 이 OSIV는 이름 그대로 영속성 컨텍스트를 뷰까지 열어두지만, 트랜잭션은 비즈니스 계층에서만 이용한다는 뜻이다.
- 동작 원리
- 클라이언트의 요청이 들어오면, 서블릿 필터나 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단 트랜잭션을 시작하지는 않는다.
- 서비스 계층에서 @Transactional으로 트랜잭션을 시작할 때, 앞에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 쓴다.
- 서비스 계층이 끝나면, 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 종료되지만 영속성 컨텍스트는 종료되지 않는다.
- 컨트롤러와 뷰까지 영속성 컨텍스트가 유지된다. 따라서 조회한 엔티티는 영속 상태를 유지한다.
- 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면, 영속성 컨텍스트를 종료한다. 이때는 플러시를 호출하지 않고, 바로 종료한다.
트랜잭션 없이 읽기
- 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이뤄져야 한다.
- 만약 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면, 아래의 예외가 발생한다.
1
caused by: javax.persistence.TransactionRequiredException
- 엔티티를 변경하지 않고, 단순히 조회만 할 때는 트랜잭션이 없어도 된다.
- 프록시를 초기화하는 지연 로딩도 조회 기능이다. 따라서 트랜잭션 없이 읽기를 할 수 있다.
- 스프링이 제공하는 OSIV는 프레젠테이션 계층에 트랜잭션이 없다. 그래서 엔티티를 수정할 수 없다. 이는 기존 OSIV의 프레젠테이션 계층에서 엔티티를 수정할 수 있는 단점을 해소했다.
- 또한, 트랜잭션 없이 읽기를 활용해서 프레젠테이션 계층에서 지연 로딩을 이용할 수 있다.
- 앞의 예제를 다시 살펴본다.
- 이전과 달리, 여기서는 영속성 컨텍스트의 플러시가 일어나지 않는다.
- 스프링의 OSIV에서 서블릿 필터나 스프링 인터셉터는 요청이 끝날 때 플러시를 호출하지 않고, 영속성 컨텍스트만 종료한다.
- 만약 플러시를 강제로 호출해도, 트랜잭션 범위 밖이므로 데이터를 수정할 수 없다는 예외를 던진다.
- 따라서 프레젠테이션 계층에서 영속 상태의 엔티티를 수정했지만, 이 내용이 실제 데이터베이스에 반영되지는 않는다.
1
2
3
4
5
6
7
8
9
10
11
class UserController {
public String readById(Long id) {
User user = userService.readById(id);
// 엔티티의 상태를 변경한다.
user.setName("dani");
model.setAttribute("user", user);
}
}
주의사항
- 프레젠테이션 계층에서 엔티티를 수정하고, 트랜잭션을 시작하는 서비스 계층을 호출하면 문제가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserController {
public String readById(Long id) {
User user = userService.readById(id);
// 엔티티의 상태를 변경한다.
user.setName("dani");
// 비즈니스 로직을 수행한다.
userService.businessLogic();
...
}
}
class UserService {
@Transactional
public void businessLogic() {
...
}
}
- 가장 단순한 해결 방법은 트랜잭션이 있는 비즈니스 로직을 모두 수행하고, 그 다음에 엔티티를 변경하면 된다.
- 보통 컨트롤러는 아래 코드처럼 구성한다. 그래서 이런 문제는 거의 발생하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserController {
public String readById(Long id) {
// 비즈니스 로직을 먼저 수행한다.
userService.businessLogic();
User user = userService.readById(id);
// 엔티티의 상태를 변경한다.
user.setName("dani");
...
}
}
class UserService {
@Transactional
public void businessLogic() {
...
}
}
- 스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션에서 공유할 수 있어 조심해야 한다.
- OSIV를 사용치 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같다. 그래서 이런 문제가 발생하지 않는다.