OSIV란?
OSIV란 Open Session In View의 약자로, 뷰 계층에서도 JPA의 지연로딩 전략을 사용할 수 있게 해주는 기술이다.
JPA는 기본적으로 트랜잭션 안에서 동작한다. 즉, 영속성 컨텍스트의 생명주기는 트랜잭션의 생명주기와 같다. 트랜잭션이 시작될 때 영속성 컨텍스트가 만들어지고, 트랜잭션이 커밋되거나 롤백될 때 영속성 컨텍스트가 종료된다.
그렇기 때문에 일반적으로 비즈니스 로직이 위치한 서비스 계층에서 트랜잭션이 시작되고 종료된다. 영속성 컨텍스트도 서비스 계층에서만 유효하기 때문에 컨트롤러에서는 영속성 컨텍스트로 관리되었던 준영속 상태의 객체를 지연 로딩할 수 없다. 트랜잭션과 영속성 컨텍스트가 모두 이미 종료되었기 때문이다. 이런 문제를 OSIV를 사용하면 쉽게 해결할 수 있다.

위 그림처럼 OSIV를 사용하지 않았을 경우에는 트랜잭션과 영속성 컨텍스트의 생명주기가 같다.

OSIV를 사용하면 영속성 컨텍스트가 트랜잭션의 생명주기와 달라져 컨트롤러에서도 영속성 컨텍스트를 사용할 수 있다.
준비하기
스프링 부트를 사용하면 자동으로 OSIV가 작동된다.
spring.jpa.open-in-view=true
application.properties에 위와 같은 코드를 작성하면 OSIV를 사용할 수 있다. 하지만 기본값으로 true이기 때문에 굳이 작성할 필요는 없다.
@Entity
@Getter
@Builder
@Table(name = "users")
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id")
private Role role;
@OneToMany(mappedBy = "user")
private List<Post> post;
@Temporal(value = TemporalType.TIMESTAMP)
private Timestamp createdAt;
}
ㄴ User 엔티티
@Slf4j
@Repository
@RequiredArgsConstructor
public class UserRepository {
private final EntityManager entityManager;
public User findOneByName(User user) {
String jpql = "SELECT u FROM User u WHERE u.name LIKE :name";
User foundUser = entityManager.createQuery(jpql, User.class)
.setParameter("name", user.getName())
.setFirstResult(0)
.setMaxResults(1)
.getSingleResult();
return foundUser;
}
}
ㄴ UserRepository
@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void join(User user) {
userRepository.save(user);
}
public User findByName(User user) {
return userRepository.findOneByName(user);
}
}
ㄴ UserService
@Slf4j
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final EntityManagerFactory entityManagerFactory;
@ResponseBody
@GetMapping("/user/test")
public String join() {
User user = User.builder().name("민수").build();
// 회원 조회
User foundUser = userService.findByName(user);
// 지연 로딩 프록시 객체 확인
boolean isPostLoaded = entityManagerFactory.getPersistenceUnitUtil().isLoaded(foundUser.getPost());
boolean isRoleLoaded = entityManagerFactory.getPersistenceUnitUtil().isLoaded(foundUser.getRole());
log.info("isPostLoaded = {}", isPostLoaded);
log.info("isRoleLoaded = {}", isRoleLoaded);
log.info("----------------");
// OSIV 객체 그래프 탐색
foundUser.getPost().size();
foundUser.getRole().getName();
isPostLoaded = entityManagerFactory.getPersistenceUnitUtil().isLoaded(foundUser.getPost());
isRoleLoaded = entityManagerFactory.getPersistenceUnitUtil().isLoaded(foundUser.getRole());
log.info("isPostLoaded = {}", isPostLoaded);
log.info("isRoleLoaded = {}", isRoleLoaded);
return "test";
}
}
ㄴ UserController
User 엔티티를 보면 Role과 Post 엔티티와 연관 관계에 있는 것을 알 수 있다. Role은 FetchType.LAZY로 설정해 주었고, 일대다 컬렉션의 경우 기본값이 LAZY이기 때문에 따로 설정해주지 않았다. 이렇게 설정했으니 나중에 User 엔티티를 조회했을 때 Role과 Post 엔티티는 프록시 객체로 생성되어 지연 로딩을 수행할 수 있을 것이다.
그럼 지연 로딩이 제대로 수행되는지 확인해보자.
OSIV 미사용
spring.jpa.open-in-view=false
application.properties에 위와 같이 작성하면 OSIV를 비활성화할 수 있다.

실행 순서는 다음과 같다.
- 트랜잭션 시작
- 영속성 컨텍스트 시작
- 회원 조회 쿼리 수행
- 트랜잭션 종료
- 커밋
- 영속성 컨텍스트 종료
실행 중간에 예외가 발생한 것을 알 수 있다. 해당 예외는 LazyInitializationException으로 영속성 컨텍스트가 이미 종료된 상태에서 지연 로딩을 사용하여 Role과 Post 엔티티를 사용하려고 했기 때문에 발생한 것이다. 그렇기 때문에 컨트롤러에서 해당 엔티티를 사용하려면 트랜잭션이 종료되지 않은 상태인 서비스 계층에서 미리 User 엔티티에서 Role과 Post를 사용한 상태에서 넘겨야 한다. 이는 코드를 추가로 작성해야 해서 상당히 번거로운 작업이 될 수 있다.
OSIV를 사용하면 해당 문제를 해결할 수 있는지 살펴보자.
OSIV 사용

실행 순서는 다음과 같다.
- 영속성 컨텍스트 생성
- 트랜잭션 시작
- 회원 조회 쿼리 수행
- 트랜잭션 종료
- 커밋
- 트랜잭션이 종료되었지만 영속성 컨텍스트 종료 X
지연 로딩 전략을 사용하기 때문에 Post와 Role이 프록시 객체로 생성되어 아직 초기화되지 않은 것을 알 수 있다. 사용한 뒤에 다시 조회해 보면 조회 쿼리와 함께 초기화되었음을 알 수 있다. 컨트롤러에서도 영속성 컨텍스트에 접근해 지연 로딩을 할 수 있게 된 것이다!
그런데 만약 컨트롤러에서 엔티티 객체를 수정하면 어떻게 될까? DB에 반영이 될까?
잘 생각해 보면 해답을 알 수 있다. 영속성 컨텍스트의 데이터가 DB에 저장되는 시점이 언제일까?
- 직접 flush() 호출
- 커밋으로 인한 자동 flush() 호출
- JPQL 쿼리 수행
위의 상황에서 영속성 컨텍스트의 데이터가 DB에 반영된다.
그런데 JPA는 기본적으로 조회 이외의 기능을 수행하려면 트랜잭션 내에서 실행되어야 한다. 그렇기 때문에 트랜잭션이 이미 종료된 상태인 컨트롤러에서 엔티티 객체의 내용을 수정한다고 해도 실제로 DB에 반영되지는 않는다.

만약 flush를 강제로 수행하려고 하면

위와 같은 예외가 발생한다. 트랜잭션이 없는 상태에서 flush()하려고 했기 때문이다.
마무리
OSIV는 굉장히 유용한 기술이지만 주의해야 할 점도 있다.
독립된 트랜잭션을 여러 개 실행한다면 영속성 컨텍스트를 공유한다는 점이 그 예 중 하나라고 할 수 있다. 그리고 상황에 따라서 Facade 계층이나 DTO를 사용해서 미리 로딩된 엔티티를 컨트롤러로 넘기는 방법이 더 효과적일 수 있으니 무조건 OSIV를 사용하기보다는 자신의 현재 상황에 따라서 더 나은 방법을 선택하여 사용하도록 하자.
'공부 > JPA' 카테고리의 다른 글
| [JPA] JPA 테스트 중 available: expected at least 1 bean which qualifies as autowire candidate. 에러 해결하기 (0) | 2024.05.08 |
|---|---|
| [JPA] 엔티티 그래프(EntityGraph) 사용하기 (0) | 2024.03.11 |
| [JPA] LazyInitializationException 해결하기 (0) | 2024.02.28 |
| [JPA] OneToOne에서의 N + 1 문제 (0) | 2024.02.27 |
| [JPA] 프록시 (Proxy) (0) | 2024.02.24 |