공통 로직과 서비스 로직 분리 해보기. #3 Bean Post Processor와 @Aspect 도입

BaekGwa·2024년 9월 9일
0

Spring

목록 보기
5/9
post-thumbnail

공통 로직과 서비스 로직

이전에는 ProxyFactory를 통해서 프록시 객체를 생성하여 수동 Bean 등록하여 로직 분리를 진행 하였다.
이전 글

  • 이전까지 Spring 에서 제공해주는 ProxyFactory를 사용해서, CGLIB과 JDK 동적 프록시 기능을 간단하게 사용하여 프록시를 만들었습니다.
  • 이를 Bean에 수동등록 해서, Bean Container 에서 의존성 주입을 통해 받아오는 객체를 프록시 객체로 하여, 실행할 수 있도록 하였습니다.

문제점

  • 사실 문제점 이라기 보다는, 개선이 필요할 사항으로 보는게 더 맞을 것 같습니다. 다음과 같은 불편함이 존재합니다.
  • 프록시 객체를 자동으로 생성해주는건 엄청 편하지만, 자동으로 Bean 등록은 안되기 때문에 수동으로 등록해주어야 합니다.
    • 이는, Config 파일을 개발자가 등록해야되는 엄청난 귀찮음이 따릅니다.
  • 만약, Bean 등록은 Component Scan을 통해 등록이 되고 난 후, Bean을 프록시 객체로 바꿔치기 할 수 있다면???
  • 이 기능을 지원해주는 기능이 존재합니다!

Bean Post Processor

빈 후처리기 란?

스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장에 등록하기 직전에 조작할 때 사용 할 수 있는 Processor

  • 즉, TestRepository 라는 Interface를 구현한 객체인 TestRepositoryImpl@Repository Annotation 으로 자동 등록을 할 때, 실제 등록되는 객체를 ProxyTestRepository 객체를 등록할 수 있는 것입니다.

설정

  • 스프링에서 제공하는 빈 후처리기를 사용하기 위해서는 다음 라이브러리 추가가 필요합니다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
  • 해당 라이브러리를 등록하면, AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 자동 등록됩니다.
    • 해당 빈 후처리기는, 스프링 빈에 등록된 Advisor들을 찾아서 자동으로 적용 해줍니다.
    • Advisor 안에는 PointcutAdvice가 있으므로, Advisor만으로도 어떤 객체에 프록시를 등록하고 어떤 동작을 해야될 지 알 수 있습니다.

작동 과정

  • 다음과 같은 작동 과정으로 진행 됩니다.
    1. 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성합니다. @Bean, @Controller 등등
    2. 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에, 빈 후처리기에 전달합니다.
    3. 조회 : 모든 Advisor를 조회 합니다.
    4. 대상 체크 : 조회한 Advisor에 포함된 Pointcut을 사용해서 해당 객체가 프록시를 적용할 대상이 아닌지 확인힙니다.
    5. 적용 : 프록시 적용 대상이라면, 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록 합니다. 프록시 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록합니다.

예제 코드

예제에 사용한 코드 : Github

  • baekgwa.proxypattern.web.componentscan 참조
  • @Compnent를 통해 자동 Bean 주입을 진행하였습니다.
  • Bean에 등록된 TestController를 프록시 객체로 후처리 해보도록 하겠습니다.

Advisor 등록

  • 위에서 설명하였듯이, Advisor를 Bean에 등록하여, 자동으로 프록시 객체가 생성되도록 @Configuration 파일을 생성합니다.
@Configuration
@RequiredArgsConstructor
public class AutoProxyConfig {

    private final Logger logger;

    @Bean
    public Advisor loggerAdvisor() {
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("save", "saveName");
        LoggerAdvice advice = new LoggerAdvice(logger);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
  • 이후, 다음과같이, Config 파일을 Import 하여 줍니다.
@Import({AutoProxyConfig.class, ProxyFactoryConfig.class})
public class ProxyPatternApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyPatternApplication.class, args);

    }
}

결과 확인

  • http://localhost:8080/v2/save?name=0
  • 실행 결과
  • 다음과 같이 포인트컷에 해당되는 메서드는 실행되면서 로그가 남는 모습을 볼 수 있습니다.
  • 기존에, Controller는 컴포넌트 스캔에 등록되어 LogAdvice를 적용할 수 없었던 문제도 해결할 수 있습니다.
  • 그렇다면, pointcut에 해당되지 않는 메서드를 실행하면 어떻게 될까요?
  • http://localhost:8080/v2/health-check
  • 실행결과
  • 문제없이 "ok"는 반환 되지만, 아무런 로그가 남지 않습니다.
  • 이를 통해, 내가 원하는 곳에만 적절한 Logger를 남길 수 있는 자동 생성 프록시를 만들 수 있습니다.

포인트 컷

  • 해당 Advisor를 등록할 때, NameMatchMethodPointcut을 등록하였는데, 이를 조건으로 프록시를 등록합니다.
  • 사실, 이 포인트 컷은 등록할때 한번, 사용할때 한번 사용됩니다.
  • 만약, TestControllerV2 처럼, healthCheck() 메서드는 포인트컷이 적용되지 않는 범위라면, 생성은 하되 사용할때는 사용되지 않습니다.
  • 일딴 프록시 객체는 하나라도 해당되면 만들어 놓고, 사용은 하지 않을 수도 있다는 뜻 입니다.

포인트 컷의 중요성

  • 아름답게, 모든 빈을 조회하며 자동으로 프록시 객체를 적용 할 수 있었습니다.
  • 정말 아름답지만, 안타까운점이 있습니다.
  • 스프링에는 많은 Bean이 자동으로 등록되고 있고, 이중에는 우리가 등록한 이름과 겹치는 이름이 존재 할 수 있습니다.
  • 이 경우에, 의도한 목적과 다르게 다른 객체가 프록시로 등록 될 수도 있습니다.
  • 이는, 우리가 원하지 않은 사이드 이팩트를 만들 수도 있고, 리소스의 낭비 입니다.
  • 따라서, 엄밀하게 패키지 경로 까지 지정할 수 있어야 합니다.

AspectJExpressionPointcut

적용해보기

  • 이번에는 다양한 Pointcut 중, AspectJ 표현식을 사용해서 생성 해보겠습니다.
  • 꽤, 다양한 문법? 이 존재 하기 때문에 다른 포스팅에서 자세한 방법을 다루도록 하겟습니다.
  • 새로운 Configuration을 등록해서 AspectJ 표현식을 통해 생성 해보겠습니다.
package baekgwa.proxypattern.gloabl.config.v3_autoproxy;

import baekgwa.proxypattern.gloabl.config.advice.LoggerAdvice;
import baekgwa.proxypattern.gloabl.logger.Logger;
import lombok.RequiredArgsConstructor;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class AutoProxyConfigV2 {

    private final Logger logger;

    @Bean
    public Advisor loggerAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
                "execution(* baekgwa.proxypattern.web.componentscan..*(..)) && "
                        + "!execution(* baekgwa.proxypattern.web.componentscan..healthCheck(..))");

        LoggerAdvice advice = new LoggerAdvice(logger);

        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
  • 간단하게, componentscan 하위 패키지에 모든 메서드 (모든 파라미터)가 만족되고, healthCheck 메서드는 아닌 것 이라는 조건 입니다.
  • 이렇게 패키징 경로를 설정하여 Advisor를 만들면, 의도하지 않은 프록시 등록을 방지할 수 있습니다!

@Aspect 사용

예제에 사용한 코드 : Github

  • baekgwa/proxypattern/gloabl/config/advice/LoggerAdviceAspect 참조
  • 위에서 알아본, Advisor 객체를 직접 만들어 Bean에 등록해줘도 되지만, @Aspect 애너테이션을 통해 Advisor를 추가하는 방법 또한 존재 합니다.

사용해 보기

  • 다음과 같이 새로운 Advisor를 만들어 보겠습니다.
  • 기존 Advisor과 다르게, @Aspect Annotation을 추가하고, @Around에 AspectJ 표현식을 통해 포인트 컷을 등록해야 합니다.
@Aspect
@RequiredArgsConstructor
public class LoggerAdviceAspect {

    private final Logger logger;

    @Around("execution(* baekgwa.proxypattern.web.componentscan..*(..)) && "
            + "!execution(* baekgwa.proxypattern.web.componentscan..healthCheck(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        String message = joinPoint.getSignature().toShortString();
        LogInfo logInfo = logger.start(message);

        Object result = joinPoint.proceed();

        logger.end(logInfo);

        return result;
    }
}
@SpringBootApplication(scanBasePackages = "baekgwa.proxypattern.web")
@Import(ProxyFactoryConfig.class)
public class ProxyPatternApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyPatternApplication.class, args);
    }

    @Bean
    public Logger logger(){
        return new LoggerImpl();
    }

    @Bean
    public LoggerAdviceAspect loggerAdviceAspect(){
        return new LoggerAdviceAspect(logger());
    }

}

결과

  • .getSignature()를 통해 가져온 값은 다음과 같이 우리가 표현한 식과 비슷하게 나왔습니다.
  • 또한, @Repository, @Controller 등, @Componenet를 통해 사용할 객체를 등록 하였습니다. 하지만 실제로 실행되는 객체는 프록시 객체가 실행되었습니다.
  • 그리고 별도의 Advisor를 등록하지 않고, @Aspect 를 통해 Advisor를 생성 할 수 있었습니다.
  • 맨처음 v1_proxy와 비교하면, 엄청난 발전이 있었고, 사용하기 엄청 간단해졌습니다.

다음으로

  • 다음으로는 Spring AOP에 대해서 조금더 학습하고 AspectJ 표현식의 다양한 방법에 대해서 더 알아보도록 하겠습니다.
profile
현재 블로그 이전 중입니다. https://blog.baekgwa.site/

0개의 댓글