본문 바로가기
Spring

[@Transactional은 만능이 아니다!] Checked Exception과 @Transactional: 초보 스프링 개발자를 위한 가이드

by daddydontsleep 2025. 1. 23.
728x90
728x90

사진: UnsplashChloé Lefleur

Checked Exception과 @Transactional: 초보 스프링 개발자를 위한 가이드

스프링(Spring) 프레임워크는 개발 생산성을 높이고, 복잡한 트랜잭션 관리 및 예외 처리를 간소화하는 데 강력한 도구를 제공합니다. 하지만 초보 개발자들은 Checked Exception과 트랜잭션 처리에 대해 자주 혼란스러워합니다. Checked Exception은 발생 시 반드시 처리해야 하는 특징을 가지며, 이는 코드의 안정성을 보장하지만 스프링의 기본 트랜잭션 관리 규칙과 충돌할 수 있습니다.

이 글에서는 Checked Exception과 @Transactional 어노테이션의 기본 개념을 설명하고, 트랜잭션 관리에서 Checked Exception의 중요성을 실제 애플리케이션 사례를 통해 다룹니다. 이를 통해 초보 개발자들이 보다 안정적이고 효과적으로 스프링 애플리케이션을 설계할 수 있도록 돕는 것을 목표로 합니다.


Checked Exception이란?

Java에서 Exception은 크게 Checked Exception과 Unchecked Exception(Runtime Exception)으로 나뉩니다. Checked Exception은 컴파일 시점에 반드시 처리되어야 하며, 이는 프로그램의 안정성을 높이는 데 기여합니다. 하지만 스프링 프레임워크의 트랜잭션 처리에서는 기본적으로 롤백되지 않는다는 점에서 주의가 필요합니다.

대표적인 Checked Exception

다음은 자주 사용하는 주요 Checked Exception들입니다:

1. 입출력 관련

  • IOException: 파일, 네트워크 등의 입출력 오류.
  • FileNotFoundException: 파일 경로가 잘못되었거나 파일이 존재하지 않을 때 발생.
  • EOFException: 데이터 스트림의 끝에 도달했을 때 발생.
  • InterruptedIOException: 입출력 작업이 중단되었을 때 발생.

2. 데이터베이스 관련

  • SQLException: 데이터베이스 액세스 오류.
    • BatchUpdateException: 대량 업데이트 중 오류.
    • SQLTimeoutException: 쿼리가 시간 초과로 실패.

3. 클래스 및 리플렉션 관련

  • ClassNotFoundException: 특정 클래스가 로드되지 않을 때.
  • CloneNotSupportedException: 객체가 복제 불가능한 경우.
  • InterruptedException: 쓰레드가 중단되었을 때.
  • ReflectiveOperationException: 리플렉션 작업 중 오류.

4. 기타

  • IllegalAccessException: 클래스나 멤버에 대한 접근 권한이 없을 때.
  • InstantiationException: 클래스의 인스턴스를 생성할 수 없을 때.
  • NoSuchMethodException: 호출하려는 메서드가 존재하지 않을 때.

Checked Exception과 @Transactional

기본적인 트랜잭션 롤백 규칙

스프링의 @Transactional 어노테이션은 기본적으로 RuntimeExceptionError에 대해서만 트랜잭션을 롤백합니다. Checked Exception은 별도로 설정하지 않으면 롤백되지 않습니다.

이 기본 동작은 다음과 같이 조정할 수 있습니다:

@Transactional(rollbackFor = Exception.class)
public void exampleMethod() throws IOException {
    // 비즈니스 로직
    throw new IOException("IO 오류 발생");
}

위 코드에서 rollbackFor 속성을 설정하면 Checked Exception도 롤백 대상에 포함됩니다.

Checked Exception 롤백이 중요한 상황

1. 데이터 정합성이 중요한 경우

  • 결제 트랜잭션: 결제 실패 시 포인트나 마일리지 변경 작업이 롤백되지 않으면 문제가 발생할 수 있습니다.
  • 재고 관리: 주문 처리 실패 시 재고가 잘못 업데이트되는 경우를 방지해야 합니다.

2. 외부 시스템 연동

  • API 통신 실패: 외부 API 호출 중 오류가 발생하면 이전 상태로 복원해야 합니다.
  • 파일 전송 오류: 업로드/다운로드 실패 시 트랜잭션 롤백이 필요합니다.
  • 메시지 큐 처리: 메시지 처리 실패 시 재처리를 위해 롤백해야 합니다.

3. 리소스 관리

  • 파일 업로드/다운로드: 업로드 중 실패하면 파일 삭제 작업이 필요할 수 있습니다.
  • DB 커넥션: 커넥션 풀의 무결성을 유지하려면 작업이 롤백되어야 합니다.
  • 네트워크 소켓: 네트워크 오류 발생 시 리소스를 적절히 정리해야 합니다.

4. 보안 및 인증

  • 인증서 검증: 검증 실패 시 후속 작업을 롤백해야 합니다.
  • 암호화/복호화: 데이터 처리 중 오류가 발생하면 복원 불가능한 상태를 방지해야 합니다.
  • 토큰 관리: 토큰 발급 및 삭제 작업이 정확히 관리되어야 합니다.

5. 배치 프로세스

  • 대량 데이터 처리: 대량 처리 중 오류가 발생하면 모든 작업을 롤백해야 데이터 정합성을 유지할 수 있습니다.
  • 스케줄링 작업: 스케줄러에서 발생한 오류는 다시 실행할 수 있도록 롤백해야 합니다.
  • 리포트 생성: 리포트 생성 중 오류가 발생하면 중간 데이터는 롤백해야 합니다.

@Transactional은 만능이 아니다

스프링의 @Transactional은 강력하지만, 전적으로 의존해서는 안 됩니다. 다음은 주의할 점입니다:

  1. 적용 범위: @Transactional은 기본적으로 메서드 레벨에서 작동하며, 동일한 클래스 내에서 호출하는 다른 메서드에는 영향을 미치지 않습니다.

    • 예시:

      @Service
      public class MyService {
          @Transactional
          public void publicMethod() {
              privateMethod(); // 트랜잭션이 적용되지 않음
          }
      
          private void privateMethod() {
              // 일부 작업 수행
          }
      }

      위의 경우, privateMethod는 트랜잭션이 적용되지 않습니다. 트랜잭션이 필요한 작업은 반드시 외부에서 호출 가능한 public 메서드로 분리해야 합니다.

  2. 프록시 기반 제한: @Transactional은 기본적으로 프록시 객체를 통해 동작하기 때문에, private 메서드나 동일 클래스 내 호출에는 적용되지 않습니다.

    • 해결 방법: 트랜잭션이 필요한 메서드는 다른 빈(bean)으로 분리하여 호출합니다.
  3. 비동기 작업: @Transactional은 별도의 쓰레드에서 실행되는 비동기 작업에는 영향을 주지 않습니다.

    • 예시:
      @Transactional
      public void processData() {
          CompletableFuture.runAsync(() -> {
              // 트랜잭션이 적용되지 않음
              someRepository.save(data);
          });
      }
      위 코드에서 비동기 블록 내부의 작업은 트랜잭션의 영향을 받지 않습니다. 비동기 작업에 트랜잭션을 적용하려면 수동으로 트랜잭션 관리를 해야 합니다.
  4. 읽기 전용 트랜잭션: @Transactional(readOnly = true)를 설정한 경우 데이터베이스 쓰기 작업은 실패할 수 있습니다.

    • 예시:
      @Transactional(readOnly = true)
      public void fetchDataAndSave() {
          List<Entity> entities = someRepository.findAll();
          entities.add(new Entity()); // 쓰기 작업 시 예외 발생
      }
      읽기 전용 트랜잭션은 주로 조회 작업에만 사용해야 하며, 쓰기 작업이 포함된 경우 적절히 설정을 변경해야 합니다.

현업에서 사용하는 Checked Exception 처리 방법

  1. Exception 변환 패턴: Checked Exception을 Unchecked Exception으로 변환하여 트랜잭션 롤백을 유도합니다.

    @Transactional
    public void processFile(MultipartFile file) {
        try {
            // 파일 처리 로직
            Files.copy(file.getInputStream(), Paths.get("/upload/" + file.getOriginalFilename()));
        } catch (IOException e) {
            throw new RuntimeException("파일 처리 중 오류가 발생했습니다.", e);
        }
    }
  2. rollbackFor 속성 사용: 트랜잭션 롤백이 필요한 Checked Exception을 명시적으로 선언합니다.

    @Transactional(rollbackFor = SQLException.class)
    public void saveData() throws SQLException {
        // 데이터 저장 로직
    }
  3. AOP 기반 예외 처리: 공통 예외 처리를 AOP로 구현하여 중복 코드를 줄입니다.

    @Aspect
    @Component
    public class ExceptionHandlingAspect {
        @Around("execution(* com.example..*(..))")
        public Object handleException(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                return joinPoint.proceed();
            } catch (IOException e) {
                throw new RuntimeException("AOP에서 처리된 IOException", e);
            }
        }
    }

결론

Checked Exception과 @Transactional은 스프링 애플리케이션의 안정성을 높이는 중요한 도구입니다. 초보 개발자는 기본 동작을 이해하고, 상황에 맞는 적절한 설정과 처리를 통해 데이터 정합성과 안정성을 유지하는 애플리케이션을 설계해야 합니다. 작은 차이가 큰 결과를 낳을 수 있다는 점을 항상 염두에 두고 코드를 작성하세요.

728x90
300x250