[Spring] Transactional의 남용성

yb__char·2024년 3월 9일
0
post-thumbnail

트랜잭션 남용성

만약 우리가 코드를 작성하다가

// 과연 이것이 적절한 트랜잭션인가?
@Transactional 
public void processPost(Long userId, String title, String content, MultipartFile image) {
    // 유저 조회 - select
    User user = userRepository.getById(userId);

    // 게시글 생성 - insert
    Post post = new Post(title, content);
    postRepository.save(post);

    // 게시글 이미지 업로드
    s3Uploader.upload(image);

    // 해당 유저 구독자들에게 푸시 알람 전송
    notifyService.notify(userId, postId, reportCount);
}

다음 고민거리가 생기게 된다.

  • 게시글 이미지가 100MB라면 어떻게 될까?
    • 100MB라는 용량으로 업로드가 실패되면, post 생성도 rollback 되는 현상
    • 또한 100MB라는 용량으로 계속 트랜잭션 로딩이 걸린 경우
  • 푸시 서버가 장애가 나면 어떻게 될까요?
    • 장애가 발생되면 이전 수행되었던 메서드들도 rollback이 되는 현상
    • 푸시가 계속 지속되는 경우
    • 반대로, 푸시 서버가 다운되는 경우

그래서 외부에서 연결되는 s3Uploader.upload, notifyService.notify은 따로 제외를 시켜야 한다고 생각한다.

외부 서버에서 정상적인 응답을 받기 전까지 해당 트랜잭션은 무한이 대기 중일 것이다.
만약 게시글 작성이 짧은 시간내에 수백건 수에 달하는 요청이 들어왔을 시, DB 뿐만 아니라 서블릿의 커넥션 풀도 고갈되면서 서버는 다운이 될 거라 예상한다.
이렇게 될 경우, 해당 DB를 참조하는 다른 멀티모듈과 같은 서비스들도 장애가 발생할 것이다.

그렇다면 트랜잭션 대상을 메서드로 분리하면 되겠네?

아니요...

public void processPost(Long userId, String content, MultipartFile image) {

    // 게시글 이미지 업로드 - 이미지 업로드가 10초 걸린다면?
    s3Uploader.upload(image);

    uploadPostTransaction(userId);
            
    // 해당 유저 구독자들에게 푸시 알람 전송 - 푸시 서버가 고장난다면?
    notifyService.notify(userId, postId, reportCount);
}

@Transactional
private void uploadPostTransaction(Long userId) {
    // 유저 조회 - select
    User user = userRepository.getById(userId);

    // 게시글 생성 - insert
    Post post = new Post(user);
    postRepository.save(post);
}

이처럼 트랜잭션이 발생되는 메서드 호출이 발생해도 안된다.
첫 번째 기본적으로 Spring AOP는 public 메서드에만 동작한다. 결국 프록시 패턴으로 인해 외부에서 해당 메서드를 호출해야 하기 때문이다.
그래서, public으로 바꿔봤는데, 동작하지 않는다... 이제부터 멘붕...

logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=debug

트랜잭션 로그를 확인할 수 있는 옵션을 켜보았다.

public void processPost() {
    System.out.println("트랜잭션 시작 전");
    uploadPostTransaction();
    System.out.println("트랜잭션 종료 후");
}

@Transactional // 열어줘..
public void uploadPost() {
    System.out.println("트랜잭션 로직 시작");
    System.out.println("트랜잭션 로직 종료");
}

안열린다.
동일한 Bean 내에서 순수한 함수가 @Transactional이 선언된 public 메서드를 호출해도, 트랜잭션은 적용되지 않는다.
Spring AOP의 내부 동작을 본다면 이유를 찾아볼 수 있다고 한다.

첫 호출이 original 객체의 proccessPost 메서드를 호출하게 되고, 해당 메서드 내부에서 uploadPost를 호출하게 되면 같은 객체를 내부에서 호출하게 되는 원리이다.

그렇게 되면 Spring 프록시 객체를 호출하지 않게 되기 때문에, 내부 메서드를 호출할 때는 실제로 코드에서 호출되는 메서드가 프록시 객체의 메서드인지 원본 객체의 프록시인지 확인해야 할 필요가 있는 것이다.

별도의 Bean객체로 분리를 한다면?

사실 답이 정답이란건 조금 공부했다면 알 것이다.
상위 객체는 새로운 별도의 프록시 객체를 가리킬 것이고, 문제없이 트랜잭션을 실행할 수 있을 것이다.

public void processPost(Long userId, String content, MultipartFile image) {
        // 게시글 이미지 업로드 - 이미지 업로드가 10초 걸린다면?
        s3Uploader.upload(image);

        // 포스트 생성
        postCommand.postCreateCommand(userId, content);

        // 해당 유저 구독자들에게 푸시 알람 전송 - 푸시 서버가 고장난다면?
        notifyService.notify(userId, postId, reportCount);
    }
}

// 별개의 클래스(빈)으로 분리
@Component
class PostCommand() {

	@Transactional
    public void postCreateCommand(Long userId) {
        // 유저 조회 - select
        User user = userRepository.getById(userId);

        // 게시글 생성 - insert
        Post post = new Post(user);
        postRepository.save(post);
    }
}

그렇다면 여기서 불필요한 클래스가 사용된다면 @Async 어노테이션으로 비동기 처리도 가능하다.
하지만 동기 처리를 하거나, 해당 로직을 트랜잭션에 포함되는 경우는 없어야 할 것이다.
추가로 TransactionTemplate를 활용하는 방법도 존재하겠지만 요즘 커리어리나 링크드인에 토비님, 영한님이 작성한 테스트 @Transactional에 대해 읽어보면 활용하기 어려우거나 새로운 챌린지가 될 듯하다...

profile
안녕하세요 백엔드 개발자 차윤범입니다 :)

0개의 댓글

관련 채용 정보