Spring Framework

[Spring Core] AOP(Aspect Oriented Programming) - 1편 (기본 개념·용어·예시)

kgvovc 2021. 4. 5. 16:50
반응형

AOP - 1편

소프트웨어를 개발할 때 소스코드의 규모가 커지다 보면 로깅이나 캐시와 같이 비즈니스 로직과는 크게 관련 없는 처리 내용이 소스코드의 여기저기에 산재하기 쉽다. 다음은 본래 메서드의 기능과 상관없이 메서드의 시작과 끝을 로그로 기록하는 예다.

 

  • 메서드의 시작과 끝을 로깅한 예
public class UserServiceImpl implements UserService {
    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
    
    public User findOne(String username) {
        log.debug("메서드 시작: UserServiceImpl.findOne 인수 = {}", username);
        // 생략
        log.debug("메서드 종료: UserServiceImpl.findOne 반환값 = {}", user);
        return user;
    }

}

 

이런 코드가 많아지면 나중에 로그 포맷이나 로그 레벨을 변경해야 하는 경우 모든 코드를 뒤져가며 수정해야 한다. 이런 일은 DRY(Do not Repeat Yourself) 원칙에 어긋나고, 향후 발생할 수 있는 변경에도 취약하며, 미처 수정 작업이 완료되지 않고 남는 부분이 있다면 시스템의 부정합을 일으키는 잠재적인 원인이 될 수 있다.

 

 

이렇게 구현하고자 하는 비즈니스 로직과는 다소 거리가 있으나 여러 모듈에 걸쳐 공통적이고 반복적으로 필요로 하는 처리 내용을 횡단 관심사(Cross-Cutting Concern)라고 부른다. 다음은 대표적인 횡단 관심사다.

  • 보안
  • 로깅
  • 트랜잭션 관리
  • 모니터링
  • 캐시 처리
  • 예외 처리

 

또한 프로그램 안에서 횡단 관심사에 해당하는 부분을 분리해서 한 곳으로 모으는 것을 횡단 관심사의 분리(Separation Of Cross-Cutting Concerns)라고 하고, 이를 실현하는 방법을 관점 지향 프로그래밍(Aspect Oriented Programming, AOP)이라 한다.

 

 

 

 

 

[AOP의 개요]

AOP관점 지향 프로그래밍(Aspect Oriented Programming)을 의미하는 약자로, 여러 클래스에 흩어져 있는 횡단 관심사를 중심으로 설계와 구현을 하는 프로그래밍 기법이다.

 

AOPDI와 함께 스프링 프레임워크의 중요한 기능 중 하나다. 지금까지는 DI를 활용해 클래스의 인스턴스를 생성하고, 인스턴스 간의 의존 관계를 맺는 처리를 애플리케이션 코드에서 분리할 수 있었다. 여기에 AOP를 활용하면 이 인스턴스들이 필요로 하는 공통적인 기능을 외부에서 집어넣을 수 있게 된다. 달리 말하자면 애플리케이션 코드에서 공통적인 기능을 분리해 내는 것이라고 할 수 있다.

 

 

AOP 개념

우선 AOP와 관련된 대표적인 용어와 이들 간의 관계를 살펴보자.

 

  • 애스펙트(Aspect): AOP의 단위가 되는 횡단 관심사에 해당한다. AOP의 예로 자주 언급되는 '로그를 출력한다', '예외를 처리한다', '트랜잭션을 관리한다'와 같은 관심사가 Aspect다.

    Aspect(관심사 클래스) = Advice(공통 관심 기능을 언제?) + Pointcut(공통 관심 기능을 어디에?(어떤 클래스에?))

 

  • 조인 포인트(Join Point): 횡단 관심사가 실행될 지점이나 시점(메서드 실행이나 예외 발생 등)을 말한다. 조인 포인트AOP를 구현한 라이브러리에 따라 사양이 다를 수 있는데 참고로 스프링 프레임워크의 AOP에서는 메서드 단위로 조인 포인트를 잡는다.

    스프링에서는 그냥 메서드 정보를 담고 있는 객체라고 생각하면 됨.

 

  • 어드바이스(Advice): 특정 Join Point에서 실행되는 코드로, 횡단 관심사실제로 구현해서 처리하는 부분이다. Advice에는 Around, Before, After 등의 여러 유형이 있는데, 뒤에서 좀 더 자세히 다루겠다.

    Advice = 횡단 관심사 구현 코드 + 이 코드를 언제 처리할 지(@Before, @After Returning, @After Throwing, @After, @Around)

 

  • 포인트컷(Pointcut): 수많은 Join point 중에서 실제로 Advice를 적용할 곳을 선별하기 위한 표현식(expression)을 말한다. 일종의 Joint point의 그룹이라 볼 수도 있다. 스프링 AOP에서는 Pointcut을 정의할 때 XML 기반 설정 방식으로 빈 정의 파일을 만들거나, 애너테이션 기반 설정 방식으로 소스코드에 주석 형태로 정의한다.

    Poincut = 어떤 클래스에 횡단 관심사를 적용할 것인가?

 

  • 위빙(Weaving): 애플리케이션 코드의 적절한 지점에 Aspect를 적용하는 것을 말한다. AOP 구현 라이브러리에 따라 Weaving하는 시점이 다를 수 있는데, 컴파일 시점이나 클래스 로딩 시점, 실행 시점 등의 다양한 Weaving 시점이 있다. 참고로 스프링 AOP는 기본적으로 실행 시점에 weaving한다.

 

  • 타깃(Target): AOP 처리에 의해 처리 흐름에 변화가 생긴 객체를 말한다. Advised Object라고도 한다.

 

 

 

 

 

 

스프링 프레임워크에서 지원하는 Advice 유형

스프링 AOP에서는 아래의 표와 같이 다섯 가지 Advice를 활용할 수 있다.

 

스프링 프레임워크에서 이용 가능한 Advice

Advice설명
BeforeJoin Point 전에 실행된다. 예외가 발생하는 경우만 제외하고 항상 실행된다.
After ReturningJoin Point가 정상적으로 종료한 후에 실행된다. 예외가 발생하면 실행되지 않는다.
After ThrowingJoin Point에서 예외가 발생했을 때 실행된다. 예외가 발생하지 않고 정상적으로 종료되면 실행되지 않는다.
AfterJoin Point에서 처리가 완료된 후 실행된다. 예외 발생이나 정상 종료 여부와 상관없이 항상 실행된다.
AroundJoin Point 전후에 실행된다.

 

 

 

 

 

 

 

 

 

[스프링 AOP]

스프링 프레임워크 안에는 AOP를 지원하는 모듈로 스프링 AOP가 포함돼 있다. 스프링 AOP에는 DI 컨테이너에서 관리하는 빈들을 타깃(Target)으로 Advice를 적용하는 기능이 있는데, Join PointAdvice를 적용하는 방법은 Proxy 객체를 만들어서 대체하는 방법을 쓴다. 그래서 Advice가 적용된 이후, DI 컨테이너에서 빈을 꺼내보면 원래 있던 빈 인스턴스가 아니라 Proxy 형태로 Advice 기능이 덧입혀진 빈이 나온다.

 

 

스프링 AOP에는 실제 개별 현장에서 폭넓게 사용돼온 AspectJ라는 AOP 프레임워크가 포함돼 있다. AspectJAspectAdvice를 정의하기 위한 애너테이션이나 Pointcut 표현 언어(Pointcut Expression Language), Weaving 메커니즘 등을 제공하는 역할을 한다. 참고로 AspectJ에서는 컴파일할 때, 클래스를 로드할 때, 실행할 때와 같이 다양한 Weaving 시점을 지원하지만, 스프링 AOP는 이 가운데 실행 시점의 Weaving을 기본으로 지원한다. 그래서 컴파일이나 클래스 로드 시점에 Weaving을 하기 위한 추가적인 설정을 따로 하지 않아도 된다.

 

한편, 스프링 AOP의 기능을 활용하려면 메이븐pom.xml에 다음과 같이 의존 관계를 정의해야 한다.

 

  • pom.xml
<dependency>
	<groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<dependency>
	<groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

 

이제 간단하게 Aspect를 구현해보자.

 

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

@Aspect 
@Component
public class MethodStartLoggingAspect {
    @Before("execution(* *..*ServiceImpl.*(..))")  
    // 이름이 ServiceImpl로 끝나는 클래스의 모든 public 메서드를 대상으로 한다.
    public void startLog(JoinPoint jp) {
        System.out.println("메서드 시작: " + jp.getSignature());
    }
    /* JoinPoint 객체(org.aspectj.lang.JoinPoint)를 통해 
       실행 중인 메서드의 정보(메서드 이름, 반환값이나 타입, 인수 등)를 확인할 수 있다. */
}

 

이렇게 만든 Aspect를 동작시키려면 AOP를 활성화해야 하며, 자바 기반 설정 방식에서는 @EnableAspectJAutoProxy 애너테이션을 사용한다.

 

  • 스프링 AOP 활성화(자바 기반 설정 방식)
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
public class AppConfig {
    // 생략
}

 

XML 기반 설정 방식에서는 AOP 관련 네임스페이스와 스키마 정보를 추가한 다음, <aop:aspectj-autoproxy> 요소를 설정하면 된다.

 

  • 스프링 AOP를 활성화하는 XML 파일 정의 예
<?xml version="1.0" encoding="UTF-8"?>
<beans
	xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    	http://www.springframework.org/schema/beans/spring-beans.xsd
    	http://www.springframework.org/schema/context
    	http://www.springframework.org/schema/context/spring-context-4.3.xsd
    	http://www.springframework.org/schema/aop
    	http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <context:component-scan base-package="com.example" />
    <aop:aspectj-autoproxy />
    <!-- 생략 -->
</beans>

 

이렇게 설정한 상태에서 UserServicefindOne 메서드를 실행한다고 가정해 보자.

 

  • AOP가 적용된 빈의 메서드 실행
UserService userService = context.getBean(UserService.class);
userService.findOne("spring");

 

AOP 기능이 정상적으로 동작했다면 다음과 같은 결과를 확인할 수 있을 것이다.

 

  • AOP가 적용된 빈의 메서드 실행 결과
메서드 시작: User com.example.demo.UserServiceImpl.findOne(String)
반응형