트랜잭션
Spring에서 사용하는 @Transactional 어노테이션도 AOP의 개념이 적용된 것 입니다. 트랜잭션을 적용해야 하는 메서드 혹은 클래스에 @Transactional 어노테이션만 작성해주면 됩니다.
AOP 개념이 적용된 Transaction 처리에서는 아래와 같은 장점이 있습니다.
- 메서드에는 서비스의 비즈니스 로직만을 작성할 수 있기 때문에, 비즈니스 로직에 집중된 코드를 작성할 수 있습니다.
- 반복적인 Transaction 처리에 대해, @Transactional 어노테이션을 통해 중복된 트랜잭션 처리 코드를 제거할 수 있습니다.
@Transactional 남용의 문제점
@Transactional 어노테이션은 너무나도 편리합니다.
매번 트랜잭션의 시작과 종료를 코드로 명시하지 않아도, Spring AOP를 통해 간단하게 어노테이션으로 트랜잭션 범위를 설정합니다.
공통적인 횡단 관심사를 해결하는 Spring AOP의 대표적인 사례라고 볼 수 있겠습니다.
그렇지만 데이터 관점의 트랜잭션을 실제 비즈니스 로직 관점에서 적용하다 보면, 분명 트랜잭션 적용 범위에 대한 괴리가 발생하게 됩니다.
다음 게시글 작성 예시를 보면서 해당 매서드의 범위가 실제로 이상적인 트랜잭션의 범위인지 고민해봅시다.
@Transactional // 적절한 트랜잭션인가?
public void processPost(Long userId, String content, MultipartFile image) {
// 유저 조회 - select
User user = userRepository.getById(userId);
// 게시글 생성 - insert
Post post = new Post();
postRepository.save(post);
// 게시글 이미지 업로드
s3Uploader.upload(image);
// 해당 유저 구독자들에게 푸시 알람 전송
notifyService.notify(userId, postId, reportCount);
}
다음 고민거리가 생기게 됩니다.
- 게시글 이미지가 100MB라면 어떻게 될까요?
- 푸시 서버가 장애가 나면 어떻게 될까요?
트랜잭션을 짧게 가져가자 - undo log
트랜잭션을 짧게 가져가야 하는 이유는 MVCC 의 내부 구현 때문에 그렇습니다.
Oracle이나 MySQL 등 RDBMS를 보면, MVCC 구현을 위해서 Undo Log를 활용합니다.
언두 로그는 트랜잭션의 롤백을 지원하고, 트랜잭션끼리의 격리를 위해 설계된 매커니즘입니다.
또한 읽기를 위한 SELECT 절의 경우 트랜잭션으로 인한 Lock을 기다리지 않고, 잠금 없는 일관된 읽기를 가능하게 해줍니다.
그런데 이 언두 로그는 해당 인덱스(컬럼)를 기준으로 트랜잭션이 중첩되면 로그 또한 중첩되서 쌓이게 되며, 해당 컬럼에 대해서 아무 트랜잭션이 없을 때까지 정리되지 않습니다. 그래서 트랜잭션을 오래 물고 있는 경우 다음과 같은 상태가 됩니다.
언두 로그가 과도하게 쌓이게 되면 잠금 없는 일관된 읽기를 위해 탐색 비용이 발생해 읽기 성능이 떨어지고, 또한 만약 다른 트랜잭션들이 동일한 자원을 필요로 하는 경우 데드락이 발생할 가능성이 높아집니다.
외부 연결은 트랜잭션에서 제외하자
트랜잭션의 길이를 짧게 가져가야 하는 것에 대한 필요성을 알았다면, 왜 외부 연결을 트랜잭션에서 제외해야 하는지에 대해 알게 됩니다.
외부 서버에서 정상적인 응답을 받기 전까지 해당 트랜잭션은 무한이 지속될 것 입니다.
만약 게시글 작성이 몇 백개가 일어난다면, DB 뿐만 아니라 서블릿의 커넥션 풀도 고갈되면서 서버는 바로 먹통이 될 것이고,
이렇게 되면 해당 DB를 참조하는 다른 서비스들도 같이 장애가 발생합니다.
트랜잭션 분리하기 - 주의! 매서드로 분리하기는 안됩니다!
그렇다면 간단하게 매서드로 빼내서 @Transactional을 붙여주는 건 어떨까요?
동일한 Bean 내에서 순수한 함수가 @Transactional이 선언된 public 메서드를 호출해도, 트랜잭션은 적용되지 않습니다.
스프링 AOP 의 내부 동작을 본다면 이유를 찾아볼 수 있습니다.
첫 호출이 original 객체의 proccessPost 매서드를 호출하게 되고, 해당 매서드 내부에서 uploadPost를 호출하게 되면 같은 객체를 내부에서 호출하게 됩니다.
그렇게 되면 프록시 객체를 호출하지 않게 되기 때문에, 내부 매서드를 호출할 때는 실제로 코드에서 호출되는 매서드가 프록시 객체의 매서드인지 원본 객체의 프록시인지 확인해야할 필요가 있습니다.