본문 바로가기

Trouble Shooting

[Trouble Shooting] 트랜잭션 롤백 문제 : 서비스 분리와 트랜잭션 전파의 중요성

1. 로그인 시도 코드 (비밀번호 일치 확인 | 틀렸을 경우 로그인 시도 횟수 증가 | 6회 이상 틀렸을 경우 계정 잠금)

/**
     * 로그인
     * @param request
     * @return
     */
    @Transactional
    public String signInAuth(AuthSignInRequestDto request, HttpServletResponse response) {

        // 직원 로그인
        if (request.getRole().equals(UserRole.MASTER.name()) || request.getRole().equals(UserRole.MANAGER.name())) {

            // 가입 여부 확인
            Employee findEmployee = employeeRepository.findByUsername(request.getUsername()).orElseThrow(() ->
                    new GlobalCustomException(ErrorCode.USER_NOT_FOUND));

            // 비밀번호 일치 확인
            if (!passwordEncoder.matches(request.getPassword(), findEmployee.getPassword())) {
                throw new GlobalCustomException(ErrorCode.EMPLOYEE_PASSWORD_BAD_REQUEST);
            }


            String token = jwtUtil.createToken(findEmployee.getId().toString(), findEmployee.getUsername(), findEmployee.getRole().name());

            // 응답 헤더에 토큰 추가
            response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);

            return "login successful";

        } else { // 회원 로그인

            // 가입 여부 확인
            Customer findCustomer = customerRepository.findByUsername(request.getUsername()).orElseThrow(() ->
                    new GlobalCustomException(ErrorCode.USER_NOT_FOUND));

            // 비밀번호 6회 이상 오류로 계정 잠김 에러
            if (findCustomer.isAccountLock()) {
                throw new GlobalCustomException(ErrorCode.ACCOUNT_LOCKED);
            }

            // 비밀번호 일치 확인 | 틀렸을 경우 로그인 시도 횟수 증가 | 6회 이상 틀렸을 경우 계정 잠금
            if (!passwordEncoder.matches(request.getPassword(), findCustomer.getPassword())) {
                // 로그인 시도 횟수 +1 증가 | 계정 잠금 활성화
                userService.updateLoginAttempts(request.getUsername());


                throw new GlobalCustomException(ErrorCode.CUSTOMER_PASSWORD_BAD_REQUEST);

            }

            // 로그인 성공 시 시도 횟수 초기화
            findCustomer.resetLoginAttemps();

            String token = jwtUtil.createToken(findCustomer.getId().toString(), findCustomer.getUsername(), findCustomer.getRole().name());

            // 응답 헤더에 토큰 추가
            response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);

            return "login successful";
        }
    }

 

 

2. 로그인 시도 횟수 증가 및 계정 잠금 활성화

    /**
     * 로그인 시도 횟수 +1 증가
     * 계정 잠금 활성화
     * @param username
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateLoginAttempts(String username) {

        Customer findCustomer = customerRepository.findByUsername(username).orElseThrow(() -> new GlobalCustomException(ErrorCode.USER_NOT_FOUND));

        findCustomer.loginAttempsCount();
        if (findCustomer.getLoginAttempts() >= 6) {
            findCustomer.accountLock();
        }
    }

 

# 문제 상황

1번 코드(로그인 처리)와 2번 코드(로그인 시도 횟수 증가 및 계정 잠금 처리)가 같은 서비스 내에 있을 때, 로그인 시 1번에서 예외가 발생하면 2번 코드까지 트랜잭션이 롤백되는 문제가 발생했습니다. 이로 인해 비밀번호가 틀렸을 경우, 시도 횟수가 증가되지 않고 전체 작업이 롤백되어 로그인 시도 횟수가 제대로 처리되지 않는 상황이었습니다.

하지만 1번과 2번 코드를 각각 다른 서비스에 위치시키니, 1번에서 에러가 발생해도 2번은 롤백되지 않고 정상적으로 시도 횟수가 증가되었습니다. 이로 인해 전체 트랜잭션 관리가 개선되었습니다.

문제 원인

  1. 트랜잭션 범위: 두 메서드가 같은 서비스 내에 있으면 기본적으로 하나의 트랜잭션으로 처리됩니다. 이때 1번 코드에서 예외가 발생하면, 트랜잭션 전체가 롤백되기 때문에 2번 코드의 작업도 자동으로 롤백됩니다.
  2. Propagation.REQUIRES_NEW 사용: 2번 코드에서 Propagation.REQUIRES_NEW를 적용해 새로운 트랜잭션을 강제하고자 했지만, 같은 서비스 내에서는 트랜잭션 관리자가 이 설정을 제대로 적용하지 못한 상황이었습니다. 같은 트랜잭션 안에서는 모든 작업이 롤백되도록 설계되기 때문입니다.

해결 과정

1번과 2번 코드를 각각 다른 서비스에 위치시켜, 두 코드가 각각 독립적인 트랜잭션으로 동작하도록 변경하였습니다. 이로 인해 1번 코드에서 발생한 예외가 2번 코드에 영향을 주지 않게 되었고, 비밀번호 오류 시에도 로그인 시도 횟수가 정상적으로 증가하도록 수정되었습니다.

해결 전략

  • 트랜잭션 분리: 트랜잭션 전파 속성을 활용하여, 중요한 작업이나 독립적인 로직은 별도의 서비스로 분리하고, Propagation.REQUIRES_NEW를 사용해 새로운 트랜잭션을 시작하도록 처리했습니다.
  • 서비스 분리: 1번과 2번 코드가 다른 서비스에 위치하게 되면서, 트랜잭션이 서로 독립적으로 관리되고, 각각의 에러가 다른 작업에 영향을 미치지 않게 처리되었습니다.

결과 및 교훈

  1. 트랜잭션 관리: 트랜잭션 경계 내에서 처리해야 할 작업을 분리하는 것이 중요합니다. 특히, 독립적인 작업은 서로 다른 트랜잭션에서 수행되도록 설계하는 것이 유지보수와 확장성에 유리합니다.
  2. 트랜잭션 전파 속성: Propagation.REQUIRES_NEW는 새로운 트랜잭션을 시작하지만, 같은 서비스 내에 있을 때는 트랜잭션의 경계가 올바르게 적용되지 않을 수 있습니다. 따라서 이러한 상황에서는 서비스를 분리하여 트랜잭션 관리자가 트랜잭션을 독립적으로 관리하도록 해야 합니다.
  3. 에러 처리: 에러 발생 시 모든 작업을 롤백하는 것이 항상 좋은 것은 아닙니다. 부분적인 작업은 롤백하지 않도록 설계하는 것이 필요할 때도 있으며, 이를 위해 적절한 트랜잭션 전파와 분리된 로직 관리가 중요합니다.