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