본문 바로가기
개발/spring, spring boot

[spring] 스프링 코어 - AOP (Aspect Oriented Programming)

by 가시죠 2021. 1. 17.
반응형

spring AOP

 

스프링 코어 - AOP (Aspect Oriented Programming)

 

AOP 가 필요한 이유

일정규모 이상의 프로젝트 진행 시 소스코드 양이 많아지며, 업무 로직과 상관없는 로깅이나 캐시 같은 처리 내용이 소스코드 여기저기에 존재하게 된다. 

또한 공통으로 처리해야 하는 업무도 존재한다. (보안, 로깅, 트랜잭션관리, 모니터링, 캐시 처리, 예외 처리)

스프링 AOP를 사용하면 소스코드에서 공통적인 기능을 효율적으로 분리하고 원하는 시점에 실행하도록 할 수 있다.

 

AOP 개념

용어

용어 설명
애스펙트 (Aspect) AOP의 단위를 애스펙트라고 부름. 예를 들어 공통처리 업무 내용 중 "로그 출력", "모니터링"
조인포인트 (Join Point) AOP가 실행될 지점이나 시점. 메소드 단위로 조인 포인트를 구분한다.
어드바이스 (Advice) 특정 조인포인트에서 실행되는 부분으로, Before, After Returning, After Throwing, After, Around가 있다.
포인트컷 (PointCut) 많은 조인포인트 중 실제 어드바이스를 적용할 곳을 선별하기 위한 표현식. xml기반 또는 애너테이션 기반으로 정의
위빙 (Weaving) 애플리케이션 코드의 적절한 지점에 에스펙트를 적용하는 것을 말한다.
타겟 (Target) 어드바이스드 오브젝트 (Advised Object)라고도 하며, AOP 처리에 의해 처리흐름에 변화가 생긴 객체. AOP를 적용할 대상.

 

애스펙트, 조인포인트, 포인트컷, 어드바이스의 관계도

 

어드바이스 유형

어드바이스 설명
Before (이전) 조인 포인트 전에 실행, 예외발생 시 미실행
AfterReturning (정상이후) 조인 포인트 종료 후 실행, 예외발생 시 미실행
AfterThrowing (예외이후) 조인 포인트 예외 발생 시 실행, 정상 종료 시 미실행
After (이후) 조인 포인트 처리 완료 후 실행, 항상 실행
Around (실행전후) 조인 포인트 전, 후에 실행하며 대상 메서드를 권한을 가지고 있음, (가장많이 사용)

 

포인트컷의 다양한 형태

구분 설명
execution(@execution) 메서드를 기준으로 포인트컷을 설정
within(@within) 특정한 타입(클래스)을 기준으로 포인트컷을 설정
this 주어진 인터페이스를 구현한 객체를 대상으로 포인트컷을 설정
args(@args) 특정한 파라미터를 가지는 대상들만을 포인트컷으로 설정
@annotation 특정한 어노테이션이 적용된 대상들만을 포인트컷으로 설정

 

AOP 예제

보통의 스프링웹 구현 시 Controller, Service가 존재하는데 AOP는 Service에서 많이 사용하고, Controller 단에서는 보통 인터셉터나 필터를 사용한다.

maven 프로젝트로 생성하여 pom.xml 파일을 아래와 같이 수정

maven 프로젝트 생성 시 GroupId, ArtifactId, Version에 정확히 어떤 값을 넣어야 할 지 헷갈릴 수 있다.

구분 설명 예제
GroupID 고유 구분값으로 package 명명 규칙을 따르도록 한다. com.tistory.kisspa.aop
ArtifactId 버전 정보를 생략한 jar 파일의 이름으로 보통 프로젝트명을 입력한다. aop-sample
Version 버전으로 default 값은 1.0-SNAPSHOT 1.0-SNAPSHOT

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tistory.kisspa.aop</groupId>
    <artifactId>aop-sample</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 프로퍼티 값 -->
    <properties>
        <java-version>1.8</java-version>
        <org.springframework-version>5.2.2.RELEASE</org.springframework-version>
        <org.aspectj-version>1.9.5</org.aspectj-version>
        <org.slf4j-version>1.7.25</org.slf4j-version>
    </properties>

    <dependencies>
        <!-- 스프링 컨텍스트 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${org.springframework-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${org.springframework-version}</version>
        </dependency>
        <!-- AOP를 위한 aspectj -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${org.aspectj-version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${org.aspectj-version}</version>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
        <!-- 로깅을 위한 slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>

</project>

 

테스트를 위해 SampleService, SampleServiceImpl, LogAdvice 파일을 생성

SampleService.java

package com.tistory.kisspa.service;

public interface SampleService {
    Integer plus(String args1, String args2) throws Exception;
}

 

SampleServiceImpl.java

package com.tistory.kisspa.service;

import org.springframework.stereotype.Service;

@Service
public class SampleServiceImpl implements SampleService {

    public Integer plus(String args1, String args2) throws Exception {
        return Integer.parseInt(args1) + Integer.parseInt(args2);
    }
}

 

LogAdvice.java

package com.tistory.kisspa.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;

@Aspect
@Slf4j
@Component
public class LogAdvice {

    /**
     * SampleService로 시작하는 클래스의 실행전에 수행
     */
    @Before( "execution(* com.tistory.kisspa.service.SampleService*.*(..))")
    public void logBefore() {
        log.info("before..");
    }

    /**
     * SampleService로 시작하는 plus 메서드 실행전에 수행
     * 인자값을 제어할 수 있다.
     * @param str1
     * @param str2
     */
    @Before("execution(* com.tistory.kisspa.service.SampleService*.plus(String, String)) && args(str1, str2)")
    public void logBeforeWithParam(String str1, String str2) {
        log.info("str1 : "+str1);
        log.info("str2 : "+str2);
    }

    /**
     * SampleService로 시작하는 클래스에서 Exception 발생 시 수행
     * @param exception
     */
    @AfterThrowing(pointcut = "execution(* com.tistory.kisspa.service.SampleService*.*(..))", throwing = "exception")
    public void logExceptoin(Exception exception) {
        log.info("AfterThrowing 에서 Exception...........");
        log.info(getPrintStackTrace(exception));
    }

    /**
     * SampleService로 시작하는 클래스의 전, 후에 수행
     * 인자값이나 오류시 처리, 처리 이후를 제어 할 수 있다. 
     * @param pjp
     * @return
     */
    @Around("execution(* com.tistory.kisspa.service.SampleService*.*(..))")
    public Object logTime(ProceedingJoinPoint pjp) {
        long start = System.currentTimeMillis();

        log.info("Target : "+pjp.getTarget());
        log.info("param :"+ Arrays.toString(pjp.getArgs()));

        //invoke method
        Object result = null;

        try {
            result = pjp.proceed();
        } catch (Throwable e) {
            log.info("Around 에서 Exception Catch...........");
            log.info(getPrintStackTrace(e));
        }

        long end = System.currentTimeMillis();

        log.info("TIME (sec) : "+ (end-start)/1000);

        return result;

    }

    /**
     * [UTIL] Exception의 printStackTrace를 문자열로 저장하여 리턴
     * @param e
     * @return
     */
    public static String getPrintStackTrace(Exception e) {

        StringWriter errors = new StringWriter();
        e.printStackTrace(new PrintWriter(errors));

        return errors.toString();

    }

    /**
     * [UTIL] Throwable의 printStackTrace를 문자열로 저장하여 리턴
     * @param e
     * @return
     */
    public static String getPrintStackTrace(Throwable e) {

        StringWriter errors = new StringWriter();
        e.printStackTrace(new PrintWriter(errors));

        return errors.toString();

    }
}

 

resource/root-context.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
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <!-- 어노테이션 설정이라고 명시 -->
    <context:annotation-config></context:annotation-config>
    
    <!-- 컴포넌트 스캔범위 지정 -->
    <context:component-scan base-package="com.tistory.kisspa.service"></context:component-scan>
    <context:component-scan base-package="com.tistory.kisspa.aop"></context:component-scan>

    <!-- 테스트할 빈 등록 -->
    <bean id="SampleService" class="com.tistory.kisspa.service.SampleServiceImpl"/>

    <aop:aspectj-autoproxy/>
</beans>

 

MainClass에서 SampleService호출하여 AOP 로그 확인

package com.tistory.kisspa;

import com.tistory.kisspa.service.SampleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class MainClass {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
                "root-context.xml");

        SampleService service = (SampleService) applicationContext.getBean("SampleService");

        try {

            log.info(String.valueOf(service.plus("123", "456")));

            log.info(String.valueOf(service.plus("AAA", "456")));

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static List<Object> getInstantiatedSigletons(ApplicationContext ctx) {
        List<Object> singletons = new ArrayList<Object>();

        String[] all = ctx.getBeanDefinitionNames();

        ConfigurableListableBeanFactory clbf = ((AbstractApplicationContext) ctx).getBeanFactory();
        for (String name : all) {
            Object s = clbf.getSingleton(name);
            if (s != null)
                singletons.add(s);
        }

        return singletons;

    }
}

 

실행로그

[main] INFO com.tistory.kisspa.aop.LogAdvice - Target : com.tistory.kisspa.service.SampleServiceImpl@6743e411
[main] INFO com.tistory.kisspa.aop.LogAdvice - param :[123, 456]
[main] INFO com.tistory.kisspa.aop.LogAdvice - before..
[main] INFO com.tistory.kisspa.aop.LogAdvice - str1 : 123
[main] INFO com.tistory.kisspa.aop.LogAdvice - str2 : 456
[main] INFO com.tistory.kisspa.aop.LogAdvice - TIME (sec) : 0
[main] INFO com.tistory.kisspa.MainClass - 579
[main] INFO com.tistory.kisspa.aop.LogAdvice - Target : com.tistory.kisspa.service.SampleServiceImpl@6743e411
[main] INFO com.tistory.kisspa.aop.LogAdvice - param :[AAA, 456]
[main] INFO com.tistory.kisspa.aop.LogAdvice - before..
[main] INFO com.tistory.kisspa.aop.LogAdvice - str1 : AAA
[main] INFO com.tistory.kisspa.aop.LogAdvice - str2 : 456
[main] INFO com.tistory.kisspa.aop.LogAdvice - Around 에서 Exception Catch...........
[main] INFO com.tistory.kisspa.aop.LogAdvice - java.lang.NumberFormatException: For input string: "AAA"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.parseInt(Integer.java:615)
	at com.tistory.kisspa.service.SampleServiceImpl.plus(SampleServiceImpl.java:9)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
	at com.tistory.kisspa.aop.LogAdvice.logTime(LogAdvice.java:67)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
	at com.sun.proxy.$Proxy19.plus(Unknown Source)
	at com.tistory.kisspa.MainClass.main(MainClass.java:21)

[main] INFO com.tistory.kisspa.aop.LogAdvice - TIME (sec) : 0
[main] INFO com.tistory.kisspa.MainClass - null

 

반응형

댓글