Posts [JPA] OSIV
Post
Cancel

[JPA] OSIV

그동안 OSIV를 잘못 알았어서, 제대로 이해하기 위해 정리한다 !


OSIV?

  • Open Session In View
  • 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.
  • 영속성 컨텍스트가 살아있으면, 엔티티는 영속 상태로 유지된다. 따라서 뷰에서도 지연 로딩을 사용할 수 있다.


과거 OSIV - 요청당 트랜잭션

  • OSIV의 핵심은 뷰에서도 지연 로딩을 할 수 있게 하는 것이다.
  • 가장 단순한 구현 방법
    • 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작한다.
    • 요청이 끝날 때, 트랜잭션도 같이 끝낸다.
  • 이렇게 하면, 영속성 컨텍스트가 시작부터 마지막까지 살아있다. 따라서 조회한 엔티티도 영속 상태를 유지한다.
  • 그럼 뷰에서도 지연 로딩을 할 수 있어 엔티티를 미리 초기화할 필요가 없다.

past-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으로 트랜잭션을 시작할 때, 앞에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 쓴다.
    • 서비스 계층이 끝나면, 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 종료되지만 영속성 컨텍스트는 종료되지 않는다.
    • 컨트롤러와 뷰까지 영속성 컨텍스트가 유지된다. 따라서 조회한 엔티티는 영속 상태를 유지한다.
    • 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면, 영속성 컨텍스트를 종료한다. 이때는 플러시를 호출하지 않고, 바로 종료한다.

spring-osiv

트랜잭션 없이 읽기

  • 영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이뤄져야 한다.
  • 만약 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면, 아래의 예외가 발생한다.
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를 사용치 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같다. 그래서 이런 문제가 발생하지 않는다.


Reference

This post is licensed under CC BY 4.0 by the author.