spring

[spring] @Transactional인데 readOnly를 곁들인..

엄지성 2024. 12. 7. 22:58

Spring을 사용하여 개발하다 보면 서비스 로직에 빠질 수 없는 것이 @Transactional 어노테이션이다. 하지만 이것에도 많은 옵션이 있다는 걸 아셨나요? 많은 것 중에 api의 성능을 높일 수도 있는 옵션은 readOnly를 알아보고자 합니다.

 

@Transactional(readOnly = true)

Spring에서 AOP를 사용하여 @Transactional을 사용할 수 있는데 여기서 위와 같이 readOnly를 true로 하게 된다면 읽기 전용으로 변경이 가능하다. 하지만 주의해야 될 점이라고 하자면 readOnly라는 옵션에서 직관적으로 알 수 있듯이 서비스 자체가 데이터를 DB에서 읽기만 하는 것에 적용을 해야 된다는 것이다. 만약 CUD 작업을 해야 되는 서비스이라면 데이터를 추가, 수정, 삭제하여도 추가 및 변경한 것이 DB에 반영되지 않는 문제가 생길 수 있다. 이런한 옵션을 적용하면 어떤 이점이 존재하는지 알아보자.

 

성능 최적화

트랜잭션을 읽기 전용으로 설정한다면 데이터를 읽기만 하기 때문에 쿼리와 캐싱에 최적화할 수 있다. 일반적으로 readOnly를 적용하지 않았을 때는 엔티티의 수정사항이 존재하거나 하지 않아도 영속성 컨텍스트에 존재하는 1차 캐시에 존재하는 스냅샷과 엔티티를 비교하게 됩니다. 그런 후 변경사항이 존재하면 1차 캐시에 엔티티를 적용시키고 수정된 사항을 DB에 또 저장하는 작업을 거치게 되는데 이것이 데이터를 변경하지 않아도 똑같은 플로우가 적용된다는 점입니다. 하지만 readOnly를 true로 설정하게 되면 Dirty checking이 비활성화 처리가 되어 불필요하게 엔티티 상태를 추적하는 작업이 줄어드는 이점이 존재합니다.

 

데이터 일관성

일반적으로 트랜잭션을 사용하여 DB의 데이터의 일관성과 무결성을 보장하기 위해 사용하는 용도로 사용되는데 트랜잭션을 읽기 전용으로 설정하면 실수로 데이터를 수정해서 일관성을 위반할 가능성이 낮아지게 됩니다. 여기서 의문이 생길 수도 있는 것이 readOnly를 사용하면 무조건 데이터의 수정사항이 반영되지 않는다?라고는 보장할 수 없습니다.

readOnly를 사용하고 서비스 로직에서

@Service
public class MyService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional(readOnly = true)
    public void updateEntityWithReadOnly() {
        MyEntity entity = entityManager.find(MyEntity.class, 1L);
        entity.setName("지성이면 감천");

        entityManager.flush();
    }
}

 

위와 같이 코드를 작성한다면 DB에 setName으로 수정한 "지성이면 감천" 이라는 값이 들어가게 됩니다. 왜 이러냐면 readOnly의 크게 작용하는 점이 영속성 컨텍스트의 flush의 유무라고 생각합니다. readOnly를 true로 사용하게 된다면 변경된 데이터를 반영하는 sql 쿼리를 쓰기 지연 저장소에 저장하여도 마지막에 flush가 작동하지 않아 DB에 데이터가 변경되지 않는다는 점입니다.

 

 

 

 

 

 

 

하지만 무언가 이상하지 않나요?

사실 위에 설명은 잘못되었습니다. 제가 위에 설명할 때 readOnly를 적용하게 된다면 Dirty Checking이 비활성화된다고 설명했습니다. 그렇다면 엔티티를 수정하였다고 쓰지 지연 저장소에 수정하는 sql 쿼리가 저장될까요? 당연히 안됩니다. 영속성 컨텍스트는 엔티티가 수정이 되었는지 삭제를 하였는지 전혀 알 수 없습니다. 왜냐면 readOnly를 true로 해놓아서 Dirty Checking도 못 하는 바보가 되었거든요. 그래서 예제를 다시 살펴보겠습니다.

@Service
public class MyService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional(readOnly = true)
    public void updateEntityWithMerge() {
        MyEntity entity = entityManager.find(MyEntity.class, 1L); // 영속 상태
        entity.setName("지성이면 감천"); // 엔티티 수정

        entityManager.merge(entity); // 수정된 엔티티 병합
        entityManager.flush(); // 변경 사항을 DB에 반영
    }
}

 

이게 올바른 코드입니다. merge를 하므로써 수정된 엔티티를 명시적으로 병합이 가능합니다. 또한, merge는 원래 있던 엔티티에 수정된 사항을 병합하는 방식이고 또 다른 방법으로는 persist가 있습니다. 이것은 병합하는 것과는 달리 하나를 새로 추가하는 방식이므로 만약 위와 같은 코드라면 merge가 더 적합하다고 할 수 있습니다. 하지만 이렇게까지 데이터를 수정하고 싶다면 readOnly를 사용하지 않는 것도 좋은 방법입니다.

 

가독성 향상

우리는 readOnly를 사용하는 이유는 간단하게 데이터를 단지 읽기 작업만 하기 위해입니다. 이 방법은 직관적이면서 명확하게 확인이 가능합니다. 이로 인해 같이 협업하는 개발자 팀원 혹은 코드를 염탐하는 개발자들도 보다 쉽게 읽기 전용인 것을 알 수 있습니다.

 

주의해야 될 점

언제나 좋은 점도 있다면 단점과 조심해야 될 것이 존재하기 마련입니다. 또한, 읽기 작업이라고 당연히 이것을 써야된다고 생각하는 것은 성능에 문제를 야기할 수 있습니다. 그것 중에 말할 주의할 점은 Optimistic Lock입니다.

 

Optimistic Lock

Optimistic Lock은 두 개의 트랜잭션이 동일한 시간에 동일한 데이터를 수정하려고 시도할 때 발생될 수 있는 충돌을 방지하는 데 사용되는 락 방식입니다. 이 방법은 트랜잭션의 대부분이 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법입니다. 이게 무슨 락임?

 

낙관적 락을 사용하는 방법은 비교적 간단합니다. Entity에 @Version을 사용하여 적용이 가능합니다.

이런 방법으로 적용한다면 Version을 적용한 칼럼에 버전 번호가 자동으로 업데이트되어 락을 하는 방법입니다.

 

만약 readOnly를 true로 설정한 서비스에서 엔티티를 수정한다고 가정해보면, 해당 트랜잭션이 엔티티를 수정하는 것이 아니라 읽기 전용으로 설정하였기 때문에 버전 번호를 확인하지 못하는 상황이 생길 수 있다. 이때 충돌을 감지하지 못하여 수정된 사항을 그대로 반영해 버린다면 데이터 불일치 문제가 생긴다는 점이다.

 

예를 들어 A 트랜잭션이 엔티티를 읽고 수정한 뒤, 다른 B 트랜잭션이 수정하려고 시도하는 경우 낙관적 락의 충돌이 감지되지 않고, A의 변경사항이 B 변경사항에 덮어쓰여지는 상황이 발생한다. 이러면 데이터의 일관성의 문제가 생길 수 있습니다. 그리하여 readOnly로 설정되어 있다면 엔티티를 변경하지 않고 단지 읽기 작업만 하게 하는 것이 이상적이다.

 

정리

이렇게 readOnly를 사용한다면 얻는 이점과 주의사항을 정리하였다. 하지만 트래픽이 많다면 성능의 차이는 미미할 수 있다. 마치 암달의 법칙처럼..하지만 나중에는 도움이 되지 않을까라고 생각이 든다. 개인적으론 한편으론 read 작업만 이뤄진다면 트랜잭션을 사용하지 않는 것도 좋을 것 같다고 생각이 든다. 코드에 고민사항이 해결되었으면 좋겠다.

'spring' 카테고리의 다른 글

[spring] 스프링 시큐리티? 무슨 역할을 할까  (0) 2024.08.11
[spring] @Bean과 @Component  (0) 2024.08.04
[spring] IoC와 DI?  (0) 2024.06.07
[spring] @Controller VS @RestController 차이점  (0) 2024.04.03