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

kys95·2022년 10월 12일
0

스프링 AOP

Aspect Oriented Programming의 약자로, 관점 지향 프로그래밍

AOP 사용이유

어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다!!

예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.

모든 핵심 기능에 부가기능을 추가해야 된다고 생각할 때, 핵심 기능의 수가 너무 많으면 그 핵심 기능에 하나하나 부가기능을 넣어야 하고, 이때 실수를 할 가능성과 추후에 부가기능을 유지 보수함에 있어서 어려움이 생긴다.

이때 AOP는 다른 관점(Aspect)을 가진 부가기능과 핵심 기능을 분리하여 부가기능 중심으로 설계, 구현하도록 도와준다.

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

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

AOP 주요 개념

  • Aspect: Advice + PointCut로 AOP의 기본 모듈
  • Advice: Target에 제공할 부가 기능을 담고 있는 모듈
  • Target: Advice가 부가 기능을 제공할 대상 (Advice가 적용될 비즈니스 로직)
  • JointPoint: Advice가 적용될 위치
    • 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용 가능
  • PointCut: Target을 지정하는 정규 표현식

동작 원리

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

소스 예시

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

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

    @Around("execution(public * com.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(); 로 핵심 기능을 수행한다.

Spring AOP

Spring AOP는 기본적으로 프록시 방식으로 동작한다. 프록시 패턴이란 어떤 객체를 사용하고자 할 때, 객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상객체에 접근하는 방식을 말한다.

Spring AOP는 왜 프록시 방식을 사용하는가?

그렇다면 Spring은 왜 Target 객체를 직접 참조하지 않고 프록시 객체를 사용할까?
프록시 객체 없이 Target 객체를 사용하고 있다고 생각해보자. Aspect 클래스에 정의된 부가 기능을 사용하기 위해서, 우리는 원하는 위치에서 직접 Aspect 클래스를 호출해야 한다. 이 경우 Target 클래스 안에 부가 기능을 호출하는 로직이 포함되기 때문에, AOP를 적용하지 않았을 때와 동일한 문제가 발생한다. 여러 곳에서 반복적으로 Aspect를 호출해야 하고, 그로 인해 유지보수성이 크게 떨어진다.

그래서 Spring에서는 Target 클래스 혹은 그의 상위 인터페이스를 상속하는 프록시 클래스를 생성하고, 프록시 클래스에서 부가 기능에 관련된 처리를 한다. 이렇게 하면 Target에서 Aspect을 알 필요 없이 순수한 비즈니스 로직에 집중할 수 있다.

예를 들어 다음 코드의 logic() 메서드가 Target이라면,

public interface TargetService{
    void logic();
}

@Service 
public class TargetServiceImpl implements TargetService{
    @Override 
		public void logic() {
        ...
}}

Proxy에서 Target 전/후에 부가 기능을 처리하고 Target을 호출한다.

@Service 
public class TargetServiceProxy implements TargetService{ 
		// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
		TargetService targetService = new TargetServiceImpl();
		...

		@Override 
		public void logic() {
        // Target 호출 이전에 처리해야하는 부가 기능
				
        // Target 호출
		targetService.logic();

        // Target 호출 이후에 처리해야하는 부가 기능
    }
}

사용하는 입장에서는 Target 객체를 사용하는 것처럼 Proxy 객체를 사용할 수 있다.

@Service 
public class UseService{ 
		// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
		TargetService targetService = new TargetServiceProxy();
		...
		
		public void useLogic() {
        // Target 호출하는 것처럼 부가 기능이 추가된 Proxy를 호출한다.
				targetService.logic();
    }
}

트랜잭션과 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
        );
    }
}

결론

  • AOP는 흩어진 관심사를 별도의 클래스로 모듈화하는 프로그래밍 방법을 말하며, OOP를 더욱 잘 지킬 수 있도록 도움을 준다.
  • Spring AOP는 프록시 객체를 자동으로 생성해주어, Aspect/Advice에 직접적으로 의존하지 않게 해준다.
  • @Transactional 도 Spring AOP 중 하나로 프록시 방식으로 동작한다.
    • 원본 클래스/인터페이스를 상속 받아 프록시를 생성하기 때문에 접근 제어가 private으로 되어 있으면 안된다.
    • 객체 외부에서 처음으로 진입하는 메서드에 트랜잭션 처리가 되어 있어야, 해당 요청을 프록시 객체가 대신 처리할 수 있다. 따라서 트랜잭션은 객체 외부에서 처음 진입하는 메서드를 기준으로 동작한다.
profile
어제의 나보다 나은 사람이 되자

0개의 댓글