JPA OSIV

3 minute read

김영한 : 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화 강의를 공부한 내용입니다.


JPA OSIV

OSIV는 영속성 컨텍스트와 DB 커넥션을 언제까지 유지할지에 대한 설정이다.

Hibernate는 Open Session In View라 부르고,
JPA는 Open EntityManager In View,
Spring은 Open In View라고 부르지만,
관례상 모두 OSIV라 한다.

JPA를 사용하면 Application 시작 시점에 Warnning 로그를 남긴다.

OSIV가 enable로 설정 되어있을 경우 DB와의 커넥션(성능)에 부담이 갈 수 있다는
경고 로그이다.  (스프링 부트의 OSIV 기본전략은 enable이다.)

2021-07-24 21:16:01.825  WARN 4660 --- [  restartedMain] JpaBaseConfiguration$JpaWebConfiguration : 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

OSIV Enable

spring.jpa.open-in-view=true

우선 코드를 보자.

@RestController
@RequiredArgsConstructor
public class UsersController {

    private final UsersService usersService;
    private final EntityManager em;


    @PostMapping("/signUp")
    public ResponseEntity<SignUpResponseDto> signUp(@RequestBody SignUpRequestDto request) {
        Users users = usersService.signUp(request);
        System.out.println("영속 상태면 true : " + em.contains(users));

        users.setName("DirtyChecking!");

        Users findUser = em.find(Users.class, 1L);

        return ResponseEntity.ok(new SignUpResponseDto(findUser.getId(), findUser.getName()));
    }

}
@Service
@RequiredArgsConstructor
public class UsersService {

    private final UsersRepository usersRepository;
    private final ModelMapper modelMapper;

    @Transactional
    public Users signUp(SignUpRequestDto user) {
        Users saveUser = modelMapper.map(user, Users.class);
        return usersRepository.save(saveUser);
    }

}
curl -X POST http://localhost:8080/signUp -H 'Content-Type:Application/json' -d '{"name": "LEE"}' 
{"id":1,"name":"DirtyChecking!"}

usersService.signUp엔 @Transactional이 있다.
여기서 signUp 메서드가 종료되면 트랜잭션도 종료된다.

그런데, signUp으로 요청을 날리면,
em.contains의 결과는 true이고, DirtyChecking에 의해 name의 값이 변경되었다.

분명 서비스에서 트랜잭션이 끝났는데도 컨트롤러 메서드에서도 영속상태가 유지되었고, 변경감지가 작동했다.
지연로딩도 마찬가지이다.

이러한 이유는 기본적으로 트랜잭션이 시작할 때 영속성 컨텍스트는 DB 커넥션을 가진다.
OSIV가 Enable 상태면, 트랜잭션이 끝나더라도
Response가 반환되기 전까지 영속성 컨텍스트와 DB 커넥션을 유지하기 때문이다.


문제점

OSIV가 Enable이라면 너무 오랜시간동안 DB 커넥션을 가지게 된다.
이는 장애가 발생할 수 있는 치명적인 문제점이다.


OSIV Disable

spring.jpa.open-in-view=false
OSIV를 끄면 트랜잭션이 종료될 때 영속성 컨텍스트를 닫고,
DB 커넥션도 반환한다.

그렇기 때문에 커넥션 리소스를 낭비하지 않는다.

@PostMapping("/signUp")
public ResponseEntity<SignUpResponseDto> signUp(@RequestBody SignUpRequestDto request) {
    Users users = usersService.signUp(request);
    System.out.println("영속 상태면 true : " + em.contains(users));

    users.setName("DirtyChecking!");

    Users findUser = em.find(Users.class, 1L);

    return ResponseEntity.ok(new SignUpResponseDto(findUser.getId(), findUser.getName()));
}
curl -X POST http://localhost:8080/signUp -H 'Content-Type:Application/json' -d '{"name": "LEE"}' 
{"id":1,"name":"LEE"}

아까와 동일한 조건이지만 이번에는 OSIV를 false로 설정했다.
변경감지가 동작하지 않았다.


문제점

트랜잭션밖에서는 지연로딩도 처리할 수 없다.
지연로딩의 경우는 아예 500 에러가 발생한다.

@RestController
@RequiredArgsConstructor
public class OrdersController {

    private final OrdersService ordersService;
    private final UsersService usersService;

    @PostMapping("/saveOrders")
    public ResponseEntity<?> saveItem(@RequestBody SaveOrdersRequestDto request) {
        Users users = usersService.findById(request.getUserId());
        ordersService.saveOrders(users, request);

        return ResponseEntity.ok(request.getName());
    }

}
@Service
@RequiredArgsConstructor
public class OrdersService {

    private final OrdersRepository ordersRepository;
    private final ModelMapper modelMapper;

    public void saveOrders(Users users, SaveOrdersRequestDto request) {
        Orders item = modelMapper.map(request, Orders.class);
        item.relationSetUsers(users);
        ordersRepository.save(item);
    }

}
@Entity
@Getter @Setter
public class Users {

    @Id @GeneratedValue
    private Long id;

    @Column(name = "USER_NAME")
    private String name;

    @OneToMany(mappedBy = "users", fetch = FetchType.LAZY)
    private List<Orders> orders = new ArrayList<>();

}
@Entity
@Getter @Setter
public class Orders {

    @Id @GeneratedValue
    private Long id;

    @Column(name = "ORDER_NAME")
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "USER_ID")
    private Users users;

    public void relationSetUsers(Users users) {
        this.users = users;
        users.getOrders().add(this);
    }

}

먼저 컨트롤러는 유저를 조회해온다.
근데 유저 엔티티의 Orders는 @OneToMany로 매핑되어 있다.
(@OneToMany의 기본 전략은 지연로딩이다.)

지연로딩이 적용되어 해당 부분의 쿼리는 아래와 같이 발생한다.

hibernate.SQL                        : 
    select
        users0_.id as id1_1_0_,
        users0_.user_name as user_nam2_1_0_ 
    from
        users users0_ 
    where
        users0_.id=?

이제 서비스의 saveOrders 메서드를 호출하면
item.relationSetUsers(users) 이 부분에서 문제가 생긴다.

public void relationSetUsers(Users users) {
    this.users = users;
    users.getOrders().add(this);
}

이 메서드는 프록시 객체인 users.orders를 초기화 시킨다.
OSIV가 꺼진 현 상태에선 프록시 객체를 초기화 시켜주지 못해 에러가 발생하는 것이다.

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.jparest.test.domain.Users.orders, could not initialize proxy - no Session
at com.example.jparest.test.domain.Orders.relationSetUsers(Orders.java:24) ~[classes/:na]
	at com.example.jparest.test.service.OrdersService.saveOrders(OrdersService.java:25) ~[classes/:na]
	at com.example.jparest.test.controller.OrdersController.saveItem(OrdersController.java:29) ~[classes/:na]

OSIV가 false라면 모든 지연로딩을 트랜잭션 안에서 처리해야된다.


해결방법

일반적으로 트랜잭션은 서비스에서 사용하고,
컨트롤러에서는 사용하지 않기 때문에 발생한 문제이다.

이러한 문제를 처리하는 서비스를 따로 만들어서
관심사를 명확하게 분리하고, 트랜잭션 안에서 관리하는 방법으로 해결한다.

보통 성능 문제는 INSERT, UPDATE 쿼리보단 SELECT 쿼리에서 문제가 발생할 수 있다.
그렇기 때문에 분리한 서비스는 읽기 전용으로 만들어 사용한다.


언제 써야할까?

사실 코드를 작성하는 입장에서는 OSIV가 켜져있는게 훨씬 편하다.
하지만 성능을 생각한다면 OSIV가 꺼져있는게 낫다.

물론 언제나 상황에 따라 다르겠지만,
사용량이 많은 실시간성 API는 OSIV를 끄고,
ADMIN 처럼 많이 사용되지 않는 서비스는 OSIV를 키고 개발하는 것이 좋을 것 이다.


Tags:

Categories:

Updated:

Leave a comment