Jpa
Jpa OSIV (Open Session In View)
무대포 개발자
2021. 4. 22. 20:17
728x90
반응형
source 는 Github 에 있습니다.
개념
- Open Session In View
- 보통 DB 트랜잭션이 시작될 때, 영속성 컨텍스트에서 데이터베이스 Connection 을 가져옵니다. 그러면 언제 Connection 을 반환할까요?
- 아래 예시를 통해 보면 @Transactional 이 시작할 때, setAutoCommit(false) 가 명령어가 수행됩니다.
- 그 이후 영속성 컨텍스트는 Connection 을 가져오고, 쿼리를 수행하고 @Transactional 선언된 메소드가 종료됩니다.
- @Transactional 메소드가 종료된 후, connection 을 반환하고 영속성 컨텍스트는 종료됩니다.
- 하지만 OSIV 가 켜져있으면 @Transactional 메소드가 종료된 이후에도 영속성 컨텍스트가 종료되지 않습니다. (기본 값은 enable 입니다.)
- Rest API 의 경우는 Response 를 반환할 때까지, MVC 화면의 경우는 view 를 렌더링해서 반환할 때까지 기다렸다가 Connection 을 반환하고 영속성컨텍스트도 종료됩니다.
@Service
public class Test {
@Transactional
public xxx xxx(...) {
xxxUpdate(...);
xxxInsert(...);
xxxDelete(...);
}
}
왜 OSIV 가 필요할까요?
- 먼저 아래 예시에서 OSIV 가 꺼져있다고 가정을 하겠습니다.
- MemberApiContoller Get Method 호출 --> MemberService.getMembers() --> MemberApiController 에서 getAccount() 호출
- getAccount() 호출 순간 에러 발생합니다.
- failed to lazily initialize a collection of role: org.example.jpa.domain.Member.Account, could not initialize proxy - no Session"
- 에러가 발생하는 이유는 OSIV 가 꺼져있으면 @Transactional 메소드인 MemberService.getMembers() 가 끝나는 순간 트랜잭션, 영속성 컨텍스트이 종료됩니다.
- 영속성컨텍스트가 종료되면서 지연로딩을 사용할 수 없으니 위와 같은 에러가 발생합니다.
- OSIV 가 켜져있다면 MemberApiController 에서 지연로딩을 호출해도 에러가 발생하지 않을 것입니다.
- 즉, 로직 내에서 지연로딩을 아무런 거리낌 없이 쓰고 싶을 때, OSIV 가 필요할 것입니다.
- 또는 트랜잭션이 시작됐다가 종료됐다가 하는 환경에서 하나의 영속성 컨텍스트로 계속해서 사용할 수 있으면 좋습니다. 왜냐하면 Connection 을 반환하고 가져오고 하는 과정이 빈번하게 반복되기 때문입니다.
application.yml
spring:
jpa:
open-in-view: false
@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@Column
private String name;
@Column
private String telNo;
@Column
private int age;
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Account> accounts = new ArrayList<>();
@Builder
public Member(Long id, String name, String telNo, int age) {
this.id = id;
this.name = name;
this.telNo = telNo;
this.age = age;
}
}
@Entity
@Getter
@NoArgsConstructor
public class Account {
@Id
@GeneratedValue
@Column(name = "account_id")
private Long id;
@Column
private String accountNo; // 계좌번호
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@Builder
public Account(String accountNo, Member member) {
this.accountNo = accountNo;
setMember(member);
}
public void setMember(Member member) {
this.member = member;
member.getAccounts().add(this);
}
}
@Slf4j
@RequiredArgsConstructor
@RestController
public class MemberApiController {
private final MemberService memberService;
/**
* osiv 테스트 api
* osiv 옵션이 꺼져있으면 account 가져올 때 에러남.
* MemberService 의 @Transaction 이 종료된 순간 영속성 컨텍스트가 종료되기에.
* @return
*/
@GetMapping("/api/training1/members")
public List<Member> getMembers1() {
List<Member> list = memberService.getMembers();
list.forEach(member -> {
log.info("account.size : {}", member.getAccounts().size());
});
return list;
}
}
@Slf4j
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Transactional(readOnly = true)
public List<Member> getMembers() {
return memberRepository.findAll();
}
}
OSIV 가 켜져있다면 위 예시는 어떻게 동작할까요?
- REST API 를 통해 들어올 때, 영속성 컨텍스트를 생성합니다. (트랜잭션은 시작하지 않습니다)
- MemberApiService 에서 @Transactional 을 만나 영속성 컨텍스트를 가져와서 트랜잭션을 시작합니다.
- 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 flush 합니다. 이 때, 트랜잭션은 종료되지만, 영속성 컨텍스트는 close 하지 않습니다.
- MemberApiController 에서 getAccounts() 를 조회할 때, 지연로딩이 발동해서 데이터를 읽기가 가능합니다.
- 이 때, 주의할게 트랜잭션이 종료되면 데이터에 대한 CUD 가 불가능하지만, 데이터 읽기는 가능합니다.
- 즉 트랜잭션 범위 밖에서 영속성 컨텍스트는 엔티티를 조회할 수 있습니다.
- Rest API response 를 client 에 던져주고 영속성 컨텍스트는 종료 됩니다.
OSIV 은 언제 사용해야하며, 언제 사용하지 말아야 할까요?
- OSIV 의 특징은 명확합니다. 영속성 컨텍스트를 트랜잭션이 끝나도 붙들고 있기에 Connection 과 같은 반환이 느려집니다.
- Connection 반환이 느려질 경우 대용량 Request 가 들어올 경우 Connection 이 부족할 수 있습니다.
- 그렇기에 대용량 Request 환경에서는 OSIV 를 사용하기 어려울 것입니다.
- 적은 Request 가 들어오는 환경이면, 지연로딩을 위해서 OSIV 를 적용할지 고려해볼 수는 있습니다.
- 또한, 영속성 컨텍스트가 종료됐다가 생성됐다가. Connection 이 반환됐다가 가져왔다하는 상황에서 OSIV 는 좋다고 생각합니다.
어떤 것이 최선인가?
- Command 와 Query 를 분리합니다.
- 여기서 얘기하는 Command 는 데이터를 변경시킬 수 있는 CUD 를 의미하고, Query 는 조회를 의미합니다.
- 즉, 데이터가 변경되는 부분의 서비스와 데이터를 조회만 하는 서비스를 분리해서 관리한다면 OSIV 를 꺼도 크게 문제 없습니다.
- 아래 예시처럼 Command 는 Command Service 에서 처리하고, Query 는 QueryService 에서 처리하면 됩니다.
- 이렇게 함으로써 QueryService 는 읽는 것에만 집중하고, Command 는 데이터를 변경하는 것에만 집중합니다.
- 유지보수가 편해지고, sourceCode 를 보기가 쉬워집니다.
- QueryService 에서 데이터를 전부 읽어온다면 지연로딩을 신경안써도 될 것입니다.
- 만약 Command 와 Query 를 조합해야하는 경우가 있으면 Controller 에서 각각 호출해서 처리하면 됩니다.
@Service
@RequiredArgsConstructor
public class MemberCommandService {
private final MemberRepository memberRepository;
public Long saveMember(Member member) {
Member result = member.save(member);
return result.getId();
}
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberQueryService {
private final MemberRepository memberRepository;
public List<Member> getMembers() {
return memberRepository.findAll();
}
}
Reference
- 자바 ORM 표준 JPA