본문 바로가기
사이드 프로젝트 기록

[EasyMarket] 트랜잭션, 꼭 서비스에서 시작해야 할까?

by 제우제우 2025. 2. 26.

애플리케이션에서 데이터베이스 작업은 대부분 트랜잭션 내에서 처리된다.
트랜잭션은 데이터의 일관성과 안정성을 보장하는 중요한 메커니즘으로, 이를 적절히 관리하지 않으면 성능 저하나 불필요한 리소스 소모를 초래할 수 있다.

특히, 트랜잭션의 시작 지점(entry point)을 잘못 설정하면 커넥션 풀 고갈 문제를 일으킬 수 있으며, 이로 인해 애플리케이션 전체 성능에 부정적인 영향을 미칠 수 있다.

따라서 트랜잭션을 적절하게 관리하는 것은 애플리케이션 성능에 매우 중요하다.

보통 트랜잭션의 선언은 서비스 계층에서 이루어지며, Spring에서는 @Transactional 애노테이션을 사용하여 트랜잭션을 관리한다.

나는 트랜잭션 최적화를 위해 서비스 계층에서 클래스 레벨에 @Transactional(readOnly = true) 옵션을 주어 데이터 조회 작업에 대해 성능을 최적화하려고 했다.

또한 JPA를 통해 프로젝트를 진행하면서, 데이터 변경 작업(insert, delete, update)이 필요한 경우에는 메소드 레벨에 @Transactional 애노테이션을 선언하여 그 범위를 최소화하고, 트랜잭션을 효율적으로 관리하도록 했다.

또한, 서비스 레이어에서 지연 로딩된 객체를 모두 초기화하여 커넥션이 트랜잭션 선언 이후로도 유지되지 않도록 하기 위해 OSIV(false) 옵션을 설정하였다.
이 옵션을 통해 트랜잭션 범위 외에서 지연 로딩이 발생하는 문제를 방지하고, 트랜잭션이 끝난 후에도 커넥션이 불필요하게 유지되지 않도록 했다.

지금까지 작은 개인 프로젝트를 하면서 모두 이렇게 설정하였다.

 

그렇다면 다음 코드에서 트랜잭션을 verificationEmail 메소드에서 시작하면 어떤 문제점이 있을까?

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

    public void verificationEmail(String email) {
        userRepository.findByEmail(email).ifPresent(user -> {
            throw new EmailAlreadyExistsException(EMAIL_ALREADY_EXISTS);
        });

        String otp = createOtp();

        EmailMessage message = EmailMessage.builder()
                .to(email)
                .message(otp)
                .build();

        emailService.sendEmail(message);
        redisUserRepository.saveOtp(email, otp);
    }
	// 코드 생략 ...
}

 

트랜잭션이 필요한곳은 userRepository에서 회원이 존재하는지 찾는곳에서만 트랜잭션이 필요하다. 

emailService.sendEmail() 메소드는 외부 API인 google stmp 서버를 통하여 이메일 전송을 한다. 

해당 영역까지 트랜잭션이 필요할까? 필요하지 않다. 심지어 외부 API여서 타임아웃을 걸어둔 시간만큼 계속 트랜잭션이 유지된다. 

하지만 서비스 레이어에서 트랜잭션을 시작하면 이메일 전송까지 트랜잭션이 유지되면서 불필요한 커넥션 점유가 발생한다.

그래서 나는 클래스 레벨에 트랜잭션 선언을 없애고 verificationEmail 메소드에는 트랜잭션을 적용하지 않도록 하였다. 

즉, 레포지토리 레이어에서 트랜잭션을 시작하도록 하였다.

@Service
@RequiredArgsConstructor
public class UserService {
    public void verificationEmail(String email) {
        userRepository.findByEmail(email).ifPresent(user -> {
            throw new EmailAlreadyExistsException(EMAIL_ALREADY_EXISTS);
        });

        String otp = createOtp();

        EmailMessage message = EmailMessage.builder()
                .to(email)
                .message(otp)
                .build();

        emailService.sendEmail(message);
        redisUserRepository.saveOtp(email, otp);
    }
	// 코드 생략 ...
}

 

지금까지는 서비스 레이어에서 트랜잭션을 시작하는 게 당연하다고 생각하여 클래스 레벨에 트랜잭션 애노테이션을 붙였지만

데이터 정합성을 맞추기 위해 하나의 트랜잭션으로 묶을 필요가 없는 경우라면 (ex: 송출금 프로세스) 

굳이 꼭 서비스 레이어에서 트랜잭션을 시작할 필요도 없고 성능을 위해 레포지토리에서의 트랜잭션 시작을 고려할 필요가 있다는 것을 느꼈다.

 

'사이드 프로젝트 기록' 카테고리의 다른 글

[EasyMarket] 세션 VS JWT 토큰 방식  (0) 2025.02.07