Spring Core/03. AOP

02. Aspect 작성과 동작

  • -
반응형

문제 상황의 인식

이번 포스트에서는 간단한 Aspect를 작성해 보고 어떤 방식으로 동작하는지 살펴보자. 

어느 일하기 싫은 날! 힘들지만 다음과 같은 멋진 빈을 만들었다.

package com.doding.aoptest.simple;

import java.util.Calendar;
import org.springframework.stereotype.Component;
import lombok.Data;

@Data
@Component
public class SimpleBean {
  private String name ;
  private Calendar now ;
}

 

SimpleBean 등록 후 단위 테스트

위 빈이 잘 동작하는지 테스트 해보자. 테스트만 통과하면 퇴근 각이다.!

package com.doding.aoptest.simple;

@SpringBootTest
@Slf4j
public class SimpleBeanTest {
  @Autowired
  SimpleBean sbean;

  @Test
  public void testBean() {
    assertEquals(sbean.getClass(), SimpleBean.class);
    log.debug("sbean type: {}", sbean.getClass().getName());
  }

  @Test
  public void testSetName() {
    String newName = "홍길동";
    sbean.setName(newName);
    assertEquals(sbean.getName(), newName);// sBean의 name은 newName과 같다.
  }

  @Test
  public void testSetNow() {
    Calendar newNow = Calendar.getInstance();
    sbean.setNow(newNow);
    assertEquals(sbean.getNow(), newNow); // sBean의 now는 newNow와 같다.
  }
}

 

단위테스트는 잘 통과할 것이다. 여기서 주의 깊게 살펴볼 부분은 testBean에서 출력한 log 부분이다. 물론 실제 구현체가 SimpleBean이었기 때문에 이상한 점은 하나도 없다.

10:37:07 [DEBUG] c.d.aoptest.simple.SimpleBeanTest.testBean.23 
- sbean type: com.doding.aoptest.simple.SimpleBean

 

아무튼 우리가 만든 멋진 비즈니스 로직들은 잘 동작한다.  그런데 지나가던 부장님이 한마디 쓱 던지고 가신다.

"속성에 값이 할당되는 중요한 상황에 로깅이 하나도 없네?"

 

요청인 즉 "property 값이 변경되는 시점에 log를 남겨서 상황을 기록하라!"는 건데 하필이면 오늘 만든 setter 메서드가 100개가 넘고 이것들을 다 찾아서 로그를 남기는 막노동은 정말 하기 싫은 상황이다. 더군다나 lombok으로 생성된 녀석들인데 메서드를 다시 다 만들어야 하나?

 

도와줘요 AOP!!

 

Aspect 작성

이제 부장님 지시사항대로 setXXX가 호출될 때마다 log를 출력할 수 있도록 aop 프로그래밍을 해보자.

dependency 추가

aop를 위해서 추가적인 라이브러리가 필요하다. www.mvnrepository.com에서 spring boot starter aop를 검색해 보자.

spring boot starter aop

최신 버전 선택 후 maven용 코드를 복사해서 pom.xml에 추가한다. 물론 이때 version 정보는 삭제한다. 대부분 의존성의 버전은 Spring Boot가 관리한다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
  <!-- <version>3.2.2</version> -->
</dependency>
라이브러리를 사용할 때 버전 정보를 삭제할지 말아야 할지 헷갈린다면 일단 지워보고 지웠을 때 아래와 같이 오류가 표시된다면 다시 버전을 입력하자.
commons-io는 SpringBoot에서 관리하지 않는다.

 

@Aspect를 이용한 aspect 작성

다음과 같이 Aspect를 만들어보자.

package com.doding.aoptest.aspects;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@Aspect // 이 클래스는 Advice를 담고 있다.
public class PropMonitorAspect {

  @Before("execution(void com.doding..*.set*(*))")
  public void beanPropertyChange(JoinPoint jp) {
    log.debug("AOP 동작: {}", jp.getSignature());
  }
}

 

PropMonitoryAspect를 aspect를 담고 있는 빈으로 구성하기 위해 @Component@Aspect를 선언한다. Aspect 안에 있는 메서드 하나 하나가 advice 하나를 의미한다.

@Beforeadvice 실행 시점을 의미하는 애너테이션으로 타겟의 메서드가 실행되기 전을 의미한다. @Before의 value 속성으로는 pointcut 정보가 있어서 대상 메서드(join point)를 지정한다. pointcut 작성 방법은 뒤에 상세히 다룬다.

메서드 바디에는 실행할 advice 코드 즉 횡단 관심사를 작성한다. 여기서는 AOP가 적용되는 메서드의 signature를 출력할 계획이다. 

Aspect의 구성

더보기

참고로 만약 legacy spring을 사용한다면 추가로 EnableAspectJAutoProxy라는 애너테이션이 설정에 추가되어야 한다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

	boolean proxyTargetClass() default false;
	boolean exposeProxy() default false;
}

 

다시 한번 단위 테스트를

Aspect 작성이 끝났다면 다시 한번 다시한번 단위테스트를 실행해 보고 바뀐 점을 살펴보자.

15:53:52 [DEBUG] c.d.aoptest.simple.SimpleBeanTest.testBean.25 
         - sbean type: com.doding.aoptest.simple.SimpleBean$$SpringCGLIB$$0
15:53:52 [DEBUG] c.d.a.aspects.PropMonitorAspect.beanPropertyWillChange.18 
         - AOP 동작: void com.doding.aoptest.simple.SimpleBean.setName(String)
15:53:52 [DEBUG] c.d.a.aspects.PropMonitorAspect.beanPropertyWillChange.18 
         - AOP 동작: void com.doding.aoptest.simple.SimpleBean.setNow(Calendar)

 

테스트 결과를 살펴보면 놀랍게도 setName, setNow가 호출될 때마다 우리가 설정해 놓은 AOP가 잘 동작함을 알 수 있다.

어떻게 이런 마법 같은 일이 가능할까? 그 비밀은 sbean의 타입에 있다. AOP가 적용되기 전에 출력한 타입은 분명 SimpleBeanImpl이었는데 여기서는 이름도 이상한 녀석이 나와있다. 어떻게 된 걸까?

 

AOP의 비밀 proxy

 

proxy란?

AOP의 비밀은 proxy 객체가 가지고 있다. 

proxy란 '대리/대신' 이런 뜻을 가지고 있는데 겉에서 봤을 때는 원본(target)과 똑같이 생겼는데 실제로는 메서드 호출 전/후 등에 끼어들어서 무언가 부가적인 작업을 하는 객체를 의미한다.

스프링은 빈 생성 후 pointcut에 기반해서 AOP 적용 대상인지 검토하고 대상이라면 프락시 클래스를 생성한 후 기존의 빈(target bean) 대신 proxy를 관리 대상으로 삼는다.

proxy 클래스는 target 빈을 상속 받아서 메서드를 재정의 하기 때문에 외부에서는 프록시의 메서드를 부르는지 target빈의 메서드를 부르는지 알지 못한다. proxy는 이렇게 타겟으로 가는 요청을 가로챈 후 @Before, @After 등 호출 시점에 맞춰 target의 메서드 호출 전/후에 advice 코드를 실행한다.

반응형

'Spring Core > 03. AOP' 카테고리의 다른 글

06. AOP 작성 주의 사항  (0) 2024.02.16
05. Spring 내부의 AOP들  (0) 2021.10.29
04. advice의 타입  (0) 2020.06.20
03. pointcut 작성  (0) 2020.06.20
01. AOP 기본 컨셉  (0) 2020.06.18
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.