모모 서비스에서는 에러가 발생할 경우 추후에 장애 대응에 용이하도록 로그 기능을 구현하였다. 로그는 아주 간단하게는 System.out.println() 을 이용하여 터미널에 출력하여 구성할 수도 있지만, Spring 에는 로그 기능에 아주 적절한 기능을 제공한다. 바로 Spring 의 AOP 이다. AOP 는 서비스를 개발할 때 비즈니스 로직 뿐만 아니라 그 외의 부가적인 기능들을 구현할 때 주로 사용한다. 예를 들어 로그가 아주 적절한 예시이다. 로그는 실제 비즈니스에 영향이 전혀 없어야 하고 기능을 개발할 때 굳이 고려할만한 사항이 아니다. 이러한 기능들을 ‘관점’ 이라는 용어로 구분하여 중요한 비즈니스 로직과 분리하여 관리한다.

AOP 동작 과정

AOP 는 proxy 객체로 감싸져 동작한다. 프록시라는 용어는 프록시 서버에서 주로 사용하는데 실제로 서비스를 제공하는 서버로 접근하기 전 부가적인 기능을 거쳐 서비스 서버로 접근할 수 있도록 중계하는 서버이다. 프록시 객체도 프록시 서버와 유사하게 비즈니스 로직을 수행하는 실제 객체를 프록시로 감싸고, 실제 로직을 수행하기 전 후에 부가적인 기능을 동작시킨다.

일반 Pojo 객체 메서드 호출

일반 Pojo 객체 메서드 호출

Proxy 객체 메서드 호출

Proxy 객체 메서드 호출

Spring AOP 구현하기

Spring 의 AOP 를 사용하기 위해서는 AOP 에서 사용하는 용어들을 먼저 이해해야 한다.

Untitled

AOP 는 동작 과정을 이해하는 것은 어렵지만 구현 자체는 간단하다. Aspect 으로 분리하여 Advice 객체를 구현한 후 이를 Configuration 에 빈으로 등록해주면 된다. 로그 기능을 구현해줄 빈을 다음과 같이 구성해보았다.

ExceptionLogging.java

@RequiredArgsConstructor
@Aspect
public class ExceptionLogging {

    private final Logging logging;

    @Pointcut("execution(* com.woowacourse.momo.*.controller.*.*(..))")
    public void allControllerExecution() {
    }

    @Pointcut("@annotation(com.woowacourse.momo.support.logging.UnhandledErrorLogging)")
    public void exceptionMethod() {
    }

    @AfterThrowing(value = "allControllerExecution()", throwing = "exception")
    public void exceptionStackTrace(Exception exception) {
        logging.printStackTrace(exception);
    }

    @Around("exceptionMethod()")
    public Object exceptionMessage(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        logging.printExceptionPoint(joinPoint);
        return result;
    }
}

실제로 발생한 예외의 StackTrace 정보와 이후에 발생시키는 모모 커스텀 예외 메시지를 출력한다. 이전에 발생한 예외를 캐치하여 새로운 커스텀 예외를 던지기 때문에 StackTrace 정보와 메시지를 하나의 플로우에 담기 어려워 분리하였다. 둘의 발생 조건은 ‘예외가 발생했을 때’ 로 동일하다.

어느 지점에서 또는 어느 어노테이션이 동작할 때, 라는 조건이 담긴 포인트 컷을 구현한다. 작성해둔 포인트 컷을 Aspect 실행 조건과 함께 조인포인트에 연결한다.

AspectConfiguration.java

@Configuration
@EnableAspectJAutoProxy
public class AspectConfiguration {

    @Value("${momo-logging.file-path}")
    private String logFilePath;

    @Bean
    public FileLogManager fileLogManager() {
        return new FileLogManager(logFilePath);
    }

    @Bean
    public SlackLogManager slackLogManager() {
        return new SlackLogManager();
    }

    @Bean
    public Logging logging() {
        return new Logging(fileLogManager(), slackLogManager());
    }

    @ConditionalOnExpression("${momo-logging.show:true}")
    @Bean
    public ExceptionLogging exceptionLogging() {
        return new ExceptionLogging(logging()); // 위에 생성한 객체 빈 등록
    }
}