서비스로직과 트랜잭션 분리 [SPRING]

2022. 5. 12. 16:51데이터베이스

반응형

이 글은 'Real MySQL 8.0 개발자와 DBA를 위한 MySQL 실전 가이드'를 보고 학습한 내용을 정리한 글입니다.

 


 

트랜잭션 또한 DBMS의 커넥션과 동일하게 꼭 필요한 최소한의 코드에만 적용하는 것이 좋습니다. 즉, 트랜잭션의 범위를 최소화 하라는 의미입니다.

 

 

다음은 유저가 어느 사이트에 회원가입하는 폼을 서버에 전송하여 그 요청을 처리하는 내용입니다. 책에서는 조금 더 복잡한 예제가 수록되었지만 가독성을 위해 단순화했습니다.


  • 처리 시작

    => 데이터베이스 커넥션 생성(또는 풀에서 가져오기)

    => 트랜잭션 시작

  • 사용자의 요청 데이터 검증
  • DB에 접근하여 데이터 저장 요청 (회원가입)
  • 유저 가입에 대한 알림 메일 발송

    => 트랜잭션 종료(COMMIT)

    => 데이터베이스 커넥션 종료(또는 풀에 반납하기)

  • 처리 완료

 

위 처리 절차를 spring + java 코드로 작성한다면,

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService_V1 {

    private final MemberRepository memberRepository;

    @Transactional // (exclude = {validateForm(), sendEmail())
    public void join(Member member) {
        //transaction begin

        validateForm(member);
        memberRepository.save(member);
        sendEmail(member);

        //commit, transaction end
    }
    
    private void validateForm(Member member) {
        // 아주 찰나의 검증로직
    }

    private void sendEmail(Member member) {
        // 외부 네트워크 통신 로직
    }


}

이런 코드가 나오게 됩니다. 많은 개발자들이 습관에 따라 이런 코드를 작성하는데, 이는 DBMS의 트랜잭션 처리에 악영향이 가는 코드입니다.

 

1. service단의 비즈니스 로직이 시작하는 곳에서 바로 트랜잭션을 시작합니다.

  • DBMS에 접근하는 로직이 시작하지 않았지만 다른 로직도 하나로 합쳐 트랜잭션 단위로 처리합니다. 특히 스프링에서는 '@Transactional' 어노테이션을 사용해서 개발자가 편리하게 메서드나 클래스 단위로 트랜잭션을 적용할 수 있는데, 이 때문에 개발자가 수동으로 transaction의 시작과 끝을 설정하는 것 보다 리스크가 커집니다. 위의 validateForm() 메서드는 DB와는 전혀 관계없는 로직입니다. 아주 찰나의 시간이라고 하더라도, 커넥션을 의미없이 점유하는 시간이 늘어나게 되므로 트랜잭션의 범위에서 빼는 것이 좋습니다. 커넥션을 소유하는 시간이 길어질 수록 언젠가 나중에는 커넥션을 얻기 위해 대기하는 상황이 발생할 수도 있기 때문입니다.

2. 트랜잭션 내에 외부 서버와 통신하는 로직이 존재합니다.

  • 메일 전송이나 FTP 파일 전송 작업 혹은 네트워크를 통해 원격 서버와 통신하는 등과 같은 작업은 무조건! 어떻게 해서든! DBMS의 트랜잭션 내에서 제거하는 것이 좋습니다. 프로그램이 실행되는 동안 외부 서버와 통신할 수 없는 상황이 발생한다면 WAS뿐 아니라 DBMS 서버까지도 위험해진다. exception이 발생하면 차라리 다행일 수도 있다.

 

 

위의 처리 절차를 개선해본다면 다음과 같다.


  • 처리 시작
  • 사용자의 요청 데이터 검증

    => 데이터베이스 커넥션 생성(또는 풀에서 가져오기)

    => 트랜잭션 시작

  • DB에 접근하여 데이터 저장 요청 (회원가입)

    => 트랜잭션 종료(COMMIT)

    => 데이터베이스 커넥션 종료(또는 풀에 반납하기)

  • 유저 가입에 대한 알림 메일 발송
  • 처리 완료

 

다시 한 번, 위 처리 절차를 spring + java 로 작성해본다면,

 

@Service
@RequiredArgsConstructor
public class MemberService_V2 {

    private final MemberRepository memberRepository;

    public void join(Member member) {
        validateForm(member);
        // transaction begin
        saveMember(member);
        //transaction end
        sendEmail(member);
    }

    @Transactional
    public void saveMember(Member member) {
        memberRepository.save(member);
    }

    private void validateForm(Member member) {
        // 아주 찰나의 검증로직
    }
    
    private void sendEmail(Member member) {
        // 외부 네트워크 통신 로직
    }
    
}

 

트랜잭션 메서드를 다시 분리하고 Controller에서는 변화 없이 join()을 호출하면 된다.

 

트랜잭션이 최소화 되었고, 그에 따라 DBMS의 부담이 줄었다. 멋지다!

반응형