@Transactional(readOnly = true)/ 쓰는 것이 좋은가?

Coodori·2024년 2월 1일
0

Study

목록 보기
9/10
post-thumbnail

문제상황

평소 Transactional를 사용하고 readOnly= true 옵션을 쓰면서 최적화가 된다는 사실만 알고 어떤 이유로 최적화가 되는지 이해하질 못했다.

그나마 이해하고 있는 것은 직접 실습해본 Master-Slave 구조로 복제본 DB를 이용하여 장애를 복구하고 Slave DB를 읽기모드로 작동시켜 조회시에 최적화를 이뤄낼 수있다.

또한 readOnly 가 붙어있는 트랜잭션만 Slave에서 조회를 해서 조회를 DB측에서도 락을 걸지 않고 빠르게 조회를 한다.

하지만 정확하게 알고 넘어가고 싶어서 해당 글을 정리한다.

@Transactional(readOnlt = true)

1.JPA 관련하여

JPA 영속성 컨텍스트가 수행하는 더티체킹에 관련

영속성 컨텍스트는 Entity 조회시 SnapShot를 저장

트랜잭션이 Commit 될때 초기 상태의 정보를 가지는 SnapShot 과 Entity의 상태를 비교하여 변경된 내용을 자동으로 update query를 생성하고 쓰기 지연 저장소에 저장한다.

이때 readOnly = true 옵션을 설정하게 되면 JPA의 세션 플러시모드를 MANUAL로 설정한다.

MANUAL 모드는 트랜잭션 내에서 사용자가 수동으로 flush를 호출하지 않으면 flush가 자동으로 수행되지 않는 모드이다.

flush() 없이는 update query가 나가질 않는다.

트랜잭션 Commit시 영속성 컨텍스트가 자동으로 flush 되지 않으므로 조회용으로 가져온 Entity의 예상치 못한 수정을 방지 + 변경감지를 사용하지 않으니 Snapshot 만들지 않는다.

하지만 커넥션을 살펴보자

JpaTransactionManager 내부에서 Connection을 반납하는 코드는 AbstractPlatformTransactionManager의 processCommit()의 finally 구문에서 실행

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
   try {
      
      (프록시가 진짜 객체에게 동작을 실행시키는 코드)

   }
   finally {
      cleanupAfterCompletion(status);
   }

즉 트랜잭션에서 커넥션이 반환되는 시점은 commit() 이후 시점 즉 프록시 입장에서 생각해보면 핵심서비스 계층에서 작성한 로직이 전부 끝나고 수행이된다.

고로 DB 조회 이후 시간이 소요되는 비즈니스 로직이 있다면 해당 API 에서 Connection을 점유하고 있기에 Connection Pool 운영에 문제가 생길 수 있음

무분별하게 @Transactional 을 사용하는 것은 메모리 관리 측면/ 데이터 베이스 커넥션 측면에서 오히려 좋지 않을 수도 있다.

외전) 커넥션을 효율적으로 사용하려면 어떻게 해야할까??

스프링은 기본적으로 OSIV 옵션을 true로 가져간다.
우리는 false로 가져간다.

그렇게 될 경우 @Transactional 범위 안에 있지 않는 Lazy Loading 동작은 수행되지 않으므로 주의해야한다.

OSIV 는 DB 커넥션 시작 지점부터 API 응답이 완전히 종료 될때까지 영속성 컨텍스트와 DB 커넥션을 유지하도록한다.
이 때문에 컨트롤러에서 해당 객체를 조작하여도 영속성 컨텍스트에서 조작이 가능한 것이다.
JPA는 지연로딩을 많이 활용하기에 Service 레이어를 벗어난 곳에서도 DB와 커넥션이 발생할 가능성이 있기에 이렇게 기본 옵션으로 적용된다.

코드의 가독성 증가

readOnly=true 옵션이 붙어있다면 직관적으로 조회용 메서드인지 판단할 수 있다.
가독성 측면에서도 향상이된다.

@Transactional
public Member joinMember(Member member) {
    Long memberId= memberRepository.save(member);
    ...
    ..
    ..
    return member;
}

@Transactional(readOnly = true)
public Member getMember(Long memberId) {
    Optional<Member> member = memberRepository.findById(memberId);
    ...
    ..
    ..
    return member;
}

Replication 부하 분산

데이터 베이스 장애를 빠르게 복구하고 트래픽을 분산하기 위해서 복제본 데이터베이스를 운용하는 레플리케이션 방식을 사용

레플리케이션은 Master-Slave 구조로 복제본 DB를 함께 운용함으로써, Master DB의 장애 발생 시 Slave DB를 Master DB로 승격시켜 장애를 빠르게 복구할 수 있으며, 조회 작업은 Slave DB에서 수행하고 수정 작업은 Master DB에서 수행함으로써 트래픽을 분산할 수 있다는 장점이 있다.

이때 readOnly=true 설정이 되어있다면 Slave DB에서 데이터를 가져오도록 동작하게 할 수 있다. 레플리케이션 목적에 맞게 트래픽 분산을 온전하게 적용할 수 있다.

결론

Lazy Loading, replication 등의 이점을 살리고 트랜잭션 범위 내에서 수행해야하는 동작을 보장하기위해 @Transactional(readOnly= true) 옵션을 쓰자

하지만 너무 무분별하게 사용하면 스냅샷 유지와 데이터베이스 커넥션의 고갈을 야기할 수 있으니 Commit()시점에 커넥션 반납을 생각하여 필수 조회시에만 트랜잭션을 걸어주는 가용 가능한 코드를 작성하자

profile
https://coodori.notion.site/0b6587977c104158be520995523b7640

0개의 댓글