스프링 핵심원리 고급편2

김파란·2024년 5월 5일

Spring

목록 보기
3/10

1. 빈 후처리기

  • 스프링에서 빈을 생성할때 빈 후처리기를 통해 빈 컨테이너로 등록되게 된다
  • 그래서 빈 후처리기를 통해 빈 객체를 조작하거나 다른 객체로 바꿔치기 가능

1). 자바에서 쓰는 빈 후처리기

@Slf4j
    @Configuration
    static class BeanPostProcessorConfig {
        @Bean(name = "beanA")
        public A a() {
            return new A();
        }

        @Bean
        public AToBPostProcessor helloPostProcessor() {
            return new AToBPostProcessor();
        }
    }

    @Slf4j
    static class A {
        public void helloA() {
            log.info("hello A");
        }
    }

    @Slf4j
    static class B {
        public void helloB() {
            log.info("hello B");
        }
    }

    @Slf4j
    static class AToBPostProcessor implements BeanPostProcessor {

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("beanName={} bean={}", beanName, bean);
            if (bean instanceof A) {
                return new B();
            }
            return bean;
        }
    }

2). 스프링에서 제공하는 빈 후처리기

  • aop 의존성만 추가하면 @Aspect도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다
  • 자동 프록시 생성기가 등록이 된다
    • 빈 후처리기가 스프링 빈에 자동으로 등록된다
    • Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {

    @Bean
    public Advisor advisor1(LogTrace logTrace) {
        // pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        // advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

}

포인트컷은 2가지에 사용된다

  • 프록시 적용 여부 판단 - 생성단계
    • 해당 빈이 프록시를 생성할 필요가 있는지 체크한다
    • 클래스 + 메서드 조건을 모두 비교해서 조건이 하나라도 맞으면 프록시 생성
  • 어드바이스 적용 여부 판단 - 사용단계
    • 어드바이스를 적용여부를 포인트컷을 보고 판단한다

2). @Aspect

  • 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다
  • 자동 프록시 생성기는 @AspectAdvisor로 변환해준다

자동 프록시 생성기

  • @AspectAdvisor로 변환해준다
    • 내부에 AdviserBuilder를 통해 어드바이저를 생성한다
  • 어드바이저를 기반으로 프록시를 생성한다
  • Advisor 빈을 다 조회한다
  • 컨테이너 생성시점에 생성된 @Aspect Advoser를 조회한다
@Slf4j
@Aspect
public class LogTraceAspect {
    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Around("execution(* hello.proxy.app..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        TraceStatus status = null;
        try {
            String message = joinPoint.getSignature().getName();
            status = logTrace.begin(message);
            // target 호출
            Object result = joinPoint.proceed();
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

3. AOP

  • 애플리케이션 로직은 크게 핵심 로직과 부가 기능으로 나눌수 있다
    • 핵심 기능
      • 해당 객체가 제공하는 고유의 기능, OrderService의 핵심기능은 주문 로직
    • 부가 기능
      • 핵심 기능을 보조하기 위해 제공되는 기능
      • 로그 추적 로직, 트랜잭션 기능
      • 부가 기능은 단독으로 사용되지 않고, 핵심 기능과 함께 사용된다

부가기능

  • 횡단 관심사라고 한다
  • 보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다

1). 소개

  • 부가기능을 핵심 기능에서 분리하고 한 곳에서 관리하도록 했다
  • Aspect: 부가기능과 부가기능을 어디에 적용할지 선택하는 기능을 합해서 하나의 모듈로 만들었다
    • AOP: Aspect-oriendted Programming

용어정리

  • 조인포인트
    • 어드바이스가 적용될 수 있는 위치
    • 조인포인트는 추상적인 개념
    • AOP를 적용할 수 있는 모든 지점이라고 생각하면 된다
    • 조인포인트는 항상 메소드 실행 지점으로 제한된다
  • 포인트컷
    • 조인포인트 중에서 어드바이스가 적용될 위치를 선별한다
    • 주로 AspectJ표현식을 사용해서 지정한다
  • 타겟
    • 실질적으로 어드바이스를 받는 객체, 포인트컷으로 결정
  • 어드바이스
    • 부가기능
    • Around, Before, After 같은 다양한 종류의 어드바이스가 있다
  • 애스펙트
    • 어드바이스 + 포인트컷을 모듈화 한것, @Aspect를 생각하면 된다
  • 어드바이저
    • 하나의 어드바이스와 하나의 포인트 컷으로 구성
  • 위빙
    • 포인트컷으로 결정한 타켓 조인 포인트에 어드바이스를 적용하는 것
    • 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가할 수 있다

2) AOP 적용 방식

  • AOP의 3가지 적용 방식
  • 스프링 AOP는 런타임방식이고, AspectJ를 이용하면 나머지 두가지 방법을 사용할 수 있다

1. 컴파일 시점 (위빙)

  • 컴파일러를 사용해서 .class를 만드는 시점에 기능 로직 추가
  • 컴파일 시점에 부가기능을 적용하려면 특별한 컴파일러도 필요하고 복잡한다

2. 클래스 로딩 시점 (로드 타임 - 위빙)

  • .class 파일을 JVM에 저장하기 전에 조작할 수 있는 기능
  • 자바를 실행할 때 특별한 옵션을 통해 클래스 로더 조작기를 지정해야하는데 어렵고 복잡하다

3. 런타임 시점 (런타임 - 위빙)

  • 바이트코드를 실제 조작하기 때문에 해당 기능을 모든 지점에 다 적용할 수 있다
  • 프록시를 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다
    • 오버라이딩 개념으로 동작하기 때문에 생성자, static, 필드 값 접근에는 프록시 적용이 안된다
  • 스프링 컨테이너가 관리할 수 있는 스프링 빈에만 AOP를 적용할 수 있다

4. 포인트컷

  • public String hello.aop.order.aop.member.MemberServiceImpl.hello(String)
  • 반환타입 주소.메서드명(파라미터)
  • execution(접근제어자? 반환타입 선언타입? 메서드이름(파라미터) 예외?)
    • ?로 되어있는건 안적어도 된다

1). 포인트컷 분리

  • 포인트 컷 시그니처라고도 불린다
  • 메서드 반환 타입은 void 여야한다
  • public으로 하면 다른 패키지에서도 적용할 수 있다
    // hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){}

    // 클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..)))")
    private void allService(){}

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());// join point 시그니처
        return joinPoint.proceed();
    }

    // hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("allOrder() && allService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }
    
    // 다른 패키지에 있으면 주소모두를 써야한다
    @Around("hello.aop.order.aop.PointCuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());// join point 시그니처
        return joinPoint.proceed();
    }

    // hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("hello.aop.order.aop.PointCuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

2). 포인트컷 지시자

  • execution : 메소드 실행 조인 포인트를 실행
  • within : 특정 타입 내의 조인 포인트를 매칭
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this: 스프링 빈 객체(프록시)를 대상으로 하는 조인 포인트
  • target: Target 객체(실제 대상)를 대상으로 하는 조인 조인포인트
  • @target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인포인트
  • @within: 주어진 애노테이션이 있는 타입 타입 내 조인포인트
  • @anootation: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트
  • @args: 전달된 실제 인수의 런타임 타입의 애노테이션을 갖고 조인 포인트
  • bean: 스프링 전용 포인트컷 지시자, 빈 이름으로 포인트컷을 지정

(1). execution

  • public String hello.aop.order.aop.member.MemberServiceImpl.hello(String)
  • execution(접근제어자? 반환타입 선언타입? 메서드이름(파라미터) 예외?)
  • 부모 타입을 선언해도 그 자식 타입은 매칭된다
    • 부모 타입에 있는 메서드만 가능
    • 오버라이딩 한 메서드만 가능하다
  • 파라미터는 부모타입은 허용하지 않는다
    • Object같은건 안된다

매칭조건

  • 접근제어자?: public
  • 반환타입: String
  • 선언타입?: hello.aop.order.aop.member.MemberServiceImpl
  • 메서드 이름: hello
  • 파라미터: (String)
  • 예외? : 생략
	// public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
    // 전체를 쓰는 방법
    pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");

    // 가장 많이 생략한 방법
    pointcut.setExpression("execution(* *(..))");
    
    // .하나만 붙인다면 정확하게 그 패키지에 있는것만 쓴다
    pointcut.setExpression("execution(* hello.aop.member.*(..))"); 
    
    // .. 점두개면 그 하위패키지 전부다 
    // 서브패키지 아래에 있는 메서드까지 선택하려면 ..으로 해야한다 .은 안됨
    pointcut.setExpression("execution(* hello.aop..*.*(..))"); 
    
    // 이름으로 찾기
    pointcut.setExpression("execution(* hello(..))");

    // 패턴으로 찾기
    pointcut.setExpression("execution(* hel*(..))");
    
    // 첫번째 파라미터만 String이고 나머지는 상관없다 (파라미터가 2개든 3개든 상관없음)
    pointcut.setExpression("execution(* hello..*.*(String, ..))");
    

(2). Within

  • execution에서 타입부분만 사용한다
  • 부모 타입을 지정하면 안된다. 정확하게 타입이 맞아야 한다
    • 인터페이스를 선정하면 안된다
	@Test
    void withinExact(){
        pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
    }

(3). args

  • 기본문법은 executionargs 부분과 같다
  • args는 부모 타입을 허용
  • 메서드들의 파라미터만 보고 판단한다
@Test
    void args(){
        pointcut.setExpression("args(Object, ..)");
        pointcut.setExpression("args(String)");
        pointcut.setExpression("args(*)");
    }

(4). @target, @within

  • 애노테이션를 보고 AOP를 건다
  • @target
    • 자식에 걸어도 부모의 메서드까지 전부 어드바이스 적용된다
  • @within
    • 자기 자신의 클래스에 정의된 메서드만 어드바이스 적용된다
  • args, @args, @target 같은 포인트컷 지시자는 모든곳에 프록시를 걸기때문에 단독사용하지 말자
	// @target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 설정, 부모 타입의 메서드도 적용
    @Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop")
    
    // @within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 설정,부모 타입의 메서드는 적용되지 않음
    @Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop")

(5). @annotation, @args

// annotation을 가지고 확인
@Around("@annotation(hello.aop.member.annotation.MethodAop)")

@args는 안씀

(6). bean

  • 빈 이름으로 적용
@Around("bean(orderSerivce) || bean(*Repoisitory)")

(7). 매개변수 전달

  • 타입이 맞는 애들만 들어온다
  • 포인트컷 이름과 매개변수 이름을 맞춰야 한다
	// 직접 가져오는 방법은 가독성도 안좋고 좋지 않다
	@Around("execution(* hello.aop.member.**.*(..))")
    private Object logArgs(ProceedingJoinPoint joinPoint) throws Throwable {
        Object arg1 = joinPoint.getArgs()[0];
        log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), arg1);
        return joinPoint.proceed();
    }
    
	// String인 얘들만 들어온다
	@Around("hello.aop.order.aop.PointCuts.orderAndService() && args(arg,..)")
    public Object doTransaction(ProceedingJoinPoint joinPoint, String arg) throws Throwable {

        log.info("[트랜잭션 시작] {} args={}", joinPoint.getSignature(),arg);
        return joinPoint.proceed();
    }
    
    // 타겟은 실제 객체
 	@Before("allService() && target(obj)")
    public void logArgs3(JoinPoint joinPoint, MemberService obj) {
        log.info("[this], obg={}",joinPoint.getArgs(), obj.getClass());
    }
	// this는 프록시
    @Before("allService() && this(obj)")
    public void logArgs4(JoinPoint joinPoint, MemberService obj) {
        log.info("[this], obg={}",joinPoint.getArgs(), obj.getClass());
    }
    
    // annotation, within, target, annotation다 가져올 수 있다
    @Around("execution(* hello.aop.member.**.*(..) && @witnin(annotation))")
    private Object targetArgs(ProceedingJoinPoint joinPoint, ClassAop annotation) throws Throwable {
        log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), annotation);
        return joinPoint.proceed();
    }

(8). this, target

  • 적용 타입 하나만 정확하게 지정해야 한다.
  • *같은 타입 사용 X, 부모 타입은 허용
  • this
    • This는 프록시 객체를 대상으로 한다
    • JDK 동적 프록시를 적용했을 때 MemberServiceImpl라는 구체 클래스를 지정하면 MemberService인터페이스 기반으로 구현된 새로운 클래스가 프록시 객체로 생성되기 때문에 MemberServiceImpl은 AOP 적용대상이 안된다
    • CGLIB는 구체클래스기반이기 때문에 구체 클래스를 지정해도 AOP 적용대상이 된다
  • target
    • Target은 객체(실제 대상)를 대상으로 한다
    • target은 인터페이스랑 구체 클래스 둘다 사용가능하다
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

5). 어드바이스

  1. @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스
  2. @Before: 조인 포인트 실행 이전에 실행
  3. @After Returning: 조인 포인트가 정상 완료후 실행
  4. @After throwing: 메서드가 예외를 던지는 경우 실행
  5. @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Slf4j
@Aspect
public class AspectV6Advice {
  // hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("hello.aop.order.aop.PointCuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            // @Before
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            // @AfterReturning
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            // @AfterThrowing
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            // @After
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

    @Before("hello.aop.order.aop.PointCuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }

    @AfterReturning(value = "hello.aop.order.aop.PointCuts.orderAndService()", returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) { // 이름이 같아야 매칭
        log.info("[return] {} return={}", joinPoint.getSignature(), result);
    }

    @AfterThrowing(value = "hello.aop.order.aop.PointCuts.orderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[return] {} message={}", joinPoint.getSignature(), ex);
    }

    @After(value = "hello.aop.order.aop.PointCuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {} ", joinPoint.getSignature());
    }
}

1). 어드바이스 순서

  • 메서드로는 순서를 보장하지 않는다
  • 그래서 다른 클래스로 빼거나 내부 클래스를 이용
@Slf4j
@Aspect
public class AspectV5Order {
    @Aspect
    @Order(2)
    public static class LogAspect{
        @Around("hello.aop.order.aop.PointCuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

   @Aspect
   @Order(1)
   public static class TxAspect{
       // hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
       @Around("hello.aop.order.aop.PointCuts.orderAndService()")
       public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
           try {
               log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
               Object result = joinPoint.proceed();
               log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
               return result;
           } catch (Exception e) {
               log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
               throw e;
           } finally {
               log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
           }
       }
   }
}

6. 스프링 AOP 주의사항

(1). 클래스 내부에서 메서드가 메서드를 호출하면 프록시 적용이 안됨

// 대안 1. 생성자로 하면 오류가 나고 setter로 나중에 주입받는다
@Slf4j
@Component
public class CallServiceV1 {

    private CallServiceV1 callServiceV1;

    @Autowired // 
    public void setCallServiceV1(CallServiceV1 callServiceV1) {
        this.callServiceV1 = callServiceV1;
    }

    public void external() {
        log.info("call external");
        callServiceV1.internal(); //외부 메서드 호출
    }

    public void internal() {
        log.info("call internal");
    }
}

// 대안 2. Provider로 나중에 지연조회
```java
@Slf4j
@Component
public class CallServiceV2 {

//    private final ApplicationContext applicationContext;
    private final ObjectProvider<CallServiceV2> callServiceProvider;

    public CallServiceV2(ObjectProvider<CallServiceV2> callServiceProvider) {
        this.callServiceProvider = callServiceProvider;
    }

    public void external() {
        log.info("call external");
		//CallServiceV2 callServiceV2 = applicationContext.getBean(CallServiceV2.class);
        CallServiceV2 callServiceV2 = callServiceProvider.getObject();
        callServiceV2.internal(); //외부 메서드 호출
    }

    public void internal() {
        log.info("call internal");
    }
}

2). 프록시 기술과 한계

  • 인터페이스가 없으면 JDK 동적 프록시가 불가능하다
    • 구체 클래스는 프록시를 못하기 때문에 캐스팅이 안된다
  • CGLIB는 인터페이스, 구체클래스 둘다 사용가능하므로 스프링은 기본적으로 CGLIB를 사용한다
    • 대상 클래스에 기본 생성자가 필수다
      • 자식 클래스는 생성자 첫줄에 부모 클래스 기본 생성자를 호출이 자동이다

0개의 댓글