[Spring] 스프링 AOP, 트랜잭션, 예외 처리

스프링 AOP, 트랜잭션, 예외 처리

스파르타 코딩 클럽의 Spring 심화반 5주 차 내용인 스프링 AOP, 트랜잭션, 예외 처리를 정리한다.

핵심 내용

  • AOP의 개념 이해와 사용방법
  • DB 트랜잭션 이해
  • 스프링 예외 처리방법

AOP의 개념 이해와 사용방법

AOP (Aspect Oriented Programming)

AOP : 관점 지향 프로그래밍

AOP의 사용 이유

핵심 기능부가기능을 개발한다고 하자.
핵심 기능은 말 그대로 API가 수행해야 하는 핵심적인 기능이고, 부가기능은 부가적으로 구현해야 될 로깅, 인증과 같은 기능이다.
모든 핵심 기능에 부가기능을 추가해야 된다고 생각할 때, 핵심 기능의 수가 너무 많으면 그 핵심 기능에 하나하나 부가기능을 넣어야 하고, 이때 실수를 할 가능성과 추후에 부가기능을 유지 보수함에 있어서 어려움이 생긴다.
이때 AOP는 다른 관점(Aspect)을 가진 부가기능핵심 기능을 분리하여 부가기능 중심으로 설계, 구현하도록 도와준다.

OOP(객체지향 프로그래밍) VS AOP

OOP는 핵심 기능을 모듈화 하는 프로그래밍 AOP는 부가기능(로깅, 트랜잭션 ...)을 모듈화하는 프로그래밍 따라서 AOP는 OOP를 대체해 주는 것이 아닌 보완 해주는 프로그래밍이다.

어드바이스, 포인트 컷

  • 어드바이스 : 부가기능
  • 포인트 컷 : 부가기능을 적용할 위치

동작 원리

스프링(디스패쳐 서블릿)이 핵심 기능에 접근하기 전에, 프록시 객체를 중간에 삽입하는 원리로 AOP가 동작한다.

소스 예시


@Component // 스프링 IoC 에 빈으로 등록
@Aspect
public class UserTimeAop {
    private final UserTimeRepository userTimeRepository;

    public UserTimeAop(UserTimeRepository userTimeRepository) {
        this.userTimeRepository = userTimeRepository;
    }

    @Around("execution(public * com.sparta.springcore.controller..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        // 측정 시작 시간
        long startTime = System.currentTimeMillis();

        try {
            // 핵심기능 수행
            Object output = joinPoint.proceed();
            return output;
        } finally {
            // 측정 종료 시간
            long endTime = System.currentTimeMillis();
            // 수행시간 = 종료 시간 - 시작 시간
            long runTime = endTime - startTime;
            // 로그인 회원이 없는 경우, 수행시간 기록하지 않음
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
                // 로그인 회원 -> loginUser 변수
                UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
                User loginUser = userDetails.getUser();

                // 수행시간 및 DB 에 기록
                UserTime userTime = userTimeRepository.findByUser(loginUser);
                if (userTime != null) {
                    // 로그인 회원의 기록이 있으면
                    long totalTime = userTime.getTotalTime();
                    totalTime = totalTime + runTime;
                    userTime.updateTotalTime(totalTime);
                } else {
                    // 로그인 회원의 기록이 없으면
                    userTime = new UserTime(loginUser, runTime);
                }

                System.out.println("[User Time] User: " + userTime.getUser().getUsername() + ", Total Time: " + userTime.getTotalTime() + " ms");
                userTimeRepository.save(userTime);
            }
        }
    }
}

@Around 어노테이션을 통해 AOP를 적용할 범위를 정해준다.
joinPoint.proceed(); 로 핵심 기능을 수행한다.

DB 트랜잭션의 이해

트랜잭션

트랜잭션: 데이터베이스에서 데이터에 대한 하나의 논리적 실행단계

ACID (원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어

출처: 위키백과

트랜잭션의 특징은 다음과 같다.

  • 더 이상 쪼갤 수 없는 최소단위의 작업
  • 모두 저장되거나, 아무것도 저장되지 않거나를 보장한다.

트랜잭션과 AOP

트랜잭션을 적용하기 위해선 트랜잭션 매니저 객체를 통해 트랜잭션을 만들어준 뒤, try, catch문으로 트랜잭션이 성공하면 commit, 실패하면 rollback을 적용해 줘야 한다.
하지만 스프링에선 @Transactional 이라는 어노테이션을 선언해 주면, 트랜잭션에 대한 프록시 객체를 자동으로 만들어주어 따로 트랜잭션에 대한 코드를 구현하지 않아도 트랜잭션이 적용된다.

현업에서의 DB 운영 방식

  • 쓰기 전용 DB(Primary)와 읽기 전용 DB(Replica)를 구분
  • Primary는 쓰기 전용으로써 @Transactional(readOnly = false)(기본값이 false)
  • Replica는 읽기 전용으로써 @Transactional(readOnly = true)
  • Primary에 문제가 생겼을 때, Replica 중 1개가 Primary가 되어 DB가 정상 운영된다.

스프링 예외 처리 방법

클래스 생성

  • 특정 예외를 상속받는 클래스를 만든 뒤, 예외의 메세지를 super()로 받아준다.
  • @RestControllerAdvice(레스트 컨트롤러에만 적용) 어노테이션을 붙인 클래스를 만들어 특정 예외 처리를 전역적으로 관리해 준다.
  • 예외 처리할 예외 클래스를 @ExceptionHandler 어노테이션의 value에 넣어주고 메소드를 만들어준다.

예시

public class ApiRequestException extends IllegalArgumentException {
    public ApiRequestException(String message) {
        super(message);
    }

    public ApiRequestException(String message, Throwable cause) {
        super(message, cause);
    }
}


@RestControllerAdvice
public class ApiExceptionHandler {
    
    @ExceptionHandler(value = { ApiRequestException.class })
    public ResponseEntity<Object> handleApiRequestException(ApiRequestException ex) {
        ApiException apiException = new ApiException(
                ex.getMessage(),
                // HTTP 400 -> Client Error
                HttpStatus.BAD_REQUEST
        );

        return new ResponseEntity<>(
                apiException,
                // HTTP 400 -> Client Error
                HttpStatus.BAD_REQUEST
        );
    }
}