
해당 글을 시작하기 전 JPA 프로그래밍에 대한 기본 개념이 부족하다면
[JPA 프로그래밍] 기본 개념 정리 를 참고 부탁드립니다 ✅
스프링에서 JPA 를 사용하게 되면 스프링 컨테이너가 트랜잭션과 영속성 컨텍스트를 관리해주므로 애플리케이션을 손쉽게 개발할 수 있습니다.
그러나, 내부 동작원리를 모르고 사용한다면 여러 문제가 발생할 수 있겠죠?🥲
이번 글에서는 OSIV(Open-Session-In-View) 에 대해 알아보겠습니다 💡
먼저 트랜잭션에 대해 구체적으로 알고 가보겠습니다.
트랜잭션 개념이 생소하다면 트랜잭션에 대해 알고 갑시다. 을 참고 부탁드립니다 🙆🏻
JPA는 객체와 테이블을 매핑해주는 자바 진영의 기술입니다.
즉 객체 지향적 관점에서 데이터베이스를 다루기 위해 사용되는 API 라고 생각 할 수 있겠죠?
그렇기 때문에 데이터베이스와 관련된 다양한 어노테이션을 통해 데이터를 조작할 수 있습니다.
가장 대표적인 어노테이션으로 @Transactional 이 있습니다.
스프링 MVC 패턴에서 repository layer 에서는 기본적으로 트랜잭션이 적용되어 있습니다.
그렇기 때문에 정상적으로 처리가 되지 않으면 rollback 기능을 사용할 수 있으며 이런 방식으로 하나의 작업 단위를 일관적으로 처리합니다 ✅
스프링 컨테이너는 JPA 영속성 컨텍스트를 지원할 때, 기본 전략으로 트랜잭션 범위의 영속성 컨텍스트를 사용합니다.
즉 트랜잭션이 시작하면 영속성 컨텍스트가 시작되고, 트랜잭션이 끝나면 영속성 컨텍스트가 종료하는 것을 의미합니다 🔨
보통 비즈니스 로직을 시작하는 Business Layer에 @Transaction 어노테이션을 사용하여 트랜잭션을 시작합니다. 해당 어노테이션이 존재시 호출한 영역(메소드 or 클래스) 직전에 스프링의 트랜잭션 AOP 가 먼저 동작합니다 ✅
트랜잭션 동작원리에 대한 더 명확한 이해를 위해 제가 진행중인 [계좌 시스템 프로젝트]의 코드 일부분을 분석해보겠습니다 🧐
계좌 시스템 개발 API 1편 을 참고 부탁드립니다.
// 계좌 관련 Controller
@RestController
@RequiredArgsConstructor
public class AccountController {
private final AccountService accountService;
// 계좌 생성
// CreateAccount.Request -> Accont -> Account -> CreatedAccount.Response
@PostMapping("/account")
public CreateAccount.Response createAccount(
@RequestBody @Valid CreateAccount.Request request){
return CreateAccount.Response.FromAccountDto(
accountService.createAccount( // accountService 메소드 호출
request.getUserId(),
request.getInitialBalance())
);
}
// 계좌 관련 Service
@Transactional
public AccountDto createAccount(Long userId, Long initialBalance) {
// 사용자가 있는지 조회
// 계좌의 번호 생성(저장된 최신 계좌번호 + 1)
// 계좌를 저장하고, 정보 저장
AccountUser accountUser = accountUserRepository.findById(userId).orElseThrow(() ->
new AccountException(ErrorCode.USER_NOT_FOUND));
validateCreateAccount(accountUser);
String newAccountNumber = accountRespository.findFirstByOrderByIdDesc()
.map(account -> (Integer.parseInt(account.getAccountNumber())) + 1 + "")
.orElse("1000000000");
// Service -> Controller ( Entity -> Dto)
// 1회성 변수 사용 주의!
return AccountDto.fromEntity(accountRespository.save(Account.builder()
.accountUser(accountUser)
.accountNumber(newAccountNumber)
.accountStatus(IN_USE)
.balance(initialBalance)
.registeredAt(LocalDateTime.now())
.build()));
}
위 코드에서 Controller 단에서 createAccount 메소드 호출 하는 경우, Service 단의 createAccount 메소드를 호출 하기 전, 스프링의 트랜잭션 AOP가 먼저 작동하여 트랜잭션을 시작합니다 💡
트랜잭션의 대상 메소드인 createAccount가 종료되면 트랜잭션을 커밋하면서 종료합니다.
커밋을 할 때, flush 를 통해 영속성 컨텍스트의 엔티티 상태를 데이터베이스에 반영을 먼저 합니다.
여기서 만약 예외가 발생한다면 트랜잭션을 롤백하여 모든 내용을 취소하게 됩니다.
이처럼 영속성 컨텍스트는 트랜잭션 범위와 같은 생명 주기를 가집니다 🙆
트랜잭션은 보통 Service 계층에서 시작하므로 Service 계층이 시작되는 순간 트랜잭션이 생성되고, Service 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료됩니다.
앞에서 설명한 바와 같이 Service 계층에서 조회한 엔티티는 Service layer와 Repository layer에서는 영속성 컨텍스트에 의해 관리됩니다.
하지만 Controller layer 혹은 view 에서는 영속성 컨텍스트가 유지되지 않음으로 준영속 상태가 됩니다.
그렇기 때문에 Controller layer 나 view 에서는 영속성 컨텍스트에서 제공하는 기능을 사용하지 못합니다 🥲
@Entity
public class Member {
@Id
private Long id;
@ManyToOne(fetch = FetchType.lazy) // 지연 로딩
private Team team; // Team 과 다대일 연관관계
...
}
@RestController
public class MemberController {
@PostMapping("/~~")
public void addMember(@RequestBody MemberInput.Request request) {
Member member = memberService.add(request); // 저장한 Member 엔티티 반환
Team team = member.getTeam();
System.out.println(team.getName()); // 지연 로딩 시 예외 발생
}
해당 코드를 분석해보겠습니다.
memberService의 add 메소드로부터 반환된 Member 엔티티는 영속성 컨텍스트가 종료되었기 때문에 '준영속' 상태입니다 그렇기에 member.getTeam()를 통해 지연 로딩 기능을 사용할시 예외가 발생합니다.
그 이유는 지연로딩 기능은 영속 상태일때만 가능하기 때문입니다 🔥
이처럼 JPA 의 기본 전략인 트랜잭션 범위의 영속성 컨텍스트는 트랜잭션 범위 밖에서는 영속성 컨텍스트의 기능을 사용하지 못한다는 문제점이 있습니다 ⛔️
하지만 어떤 경우에서는 Controller나 View 에서 반환된 엔티티와 연관된 엔티티를 사용한다는 등의 영속성 컨텍스트가 필요한 경우가 있습니다.
OSIV는 영속성 컨텍스트가 트랜잭션 범위를 넘어선 레이어까지 열어 두는 기능을 의미합니다.
즉 쉽게 말해, 클라이언트에게 보여지는 View 나 Controller까지 영속성 컨텍스트를 유지하는 기능입니다. 기존 Transaction이 종료되면서 닫히는 영속성 컨텍스트의 생존 주기를 늘리면서 View나 Controller에서 사용하지 못했던 영속성 컨텍스트의 기능을 사용 할 수 있습니다 🎁
그러나 여기서 문제점이 존재합니다 ⛔️
영속성 컨텍스트를 Presentation Layer 까지 열어두게 되면 해당 Layer 에서도 엔티티를 수정 가능하고 이는 영속성 컨텍스트의 '변경 감지'의 기능에 의해 DB 에도 반영 될 수 있습니다.
만약 보안상의 이유로 엔티티 그대로가 아니라 엔티티를 수정해서 사용자에게 보여줘야 할 때, 개발자의 의도와는 다르게 수정한 내용이 DB 까지 반영 될 수 있다는 문제점이 있습니다.

이러한 문제점이 존재하기에 스프링 조금 다른 특성의 OSIV 기능을 제공합니다 💡
스프링 프레임워크가 제공하는 OSIV는 비즈니스 계층에서만 트랜잭션을 사용하는 방식을 채택한다.
간단하게 정리하자면 OSIV 기능은 기존 트랜잭션 범위의 영속성 컨텍스트와는 달리, 트랜잭션 보다 영속성 컨텍스트의 생명 주기를 늘리는 방식이라고 할 수 있습니다. 또한 영속성 컨텍스트 종료 시점에 플러시를 호출하지 않음으로 Presentation Layer 에서 진행되는 작업은 DB에 반영되지 않습니다 👨💻
그러나 스프링 OSIV 에도 단점이 존재합니다 😖
