AOP (Aspect Oriented Programming) [Spring]
반응형

본 게시글은 Seoul wiz의 신입SW인력을 위한 실전 자바(Java) 스프링(Spring)을 참고하여 작성한 글입니다.

 

자바는 객체 지향 프로그래밍, OOP (Object Oriented Programing)

AOP는 Aspect, 관점 지향. 시점을 중요시 여긴다는 뜻이다. (oop와 반대되는 말이 아님)

프로그래밍을 하면서 공통적인 기능 (ex) Log) 등이 많이 있는데, 상속을 통해 모든 모듈에 적용하는데,

근데 자바에서는 다중 상속이 불가능. 그리고 구현에 핵심 기능과 공통 기능이 섞이면 효율성이 떨어진다.

여기서 AOP가 등장한다. 공통 기능과 핵심 기능을 분리해놓고, 핵심 기능에 적용하는 공통 기능을 적용하는 방식이다.

[용어]
Aspect : 공통 기능
Advice : Aspect의 기능 자체
Jointpoint : Advice를 적용해야 하는 부분 (메소드 / 핵심 기능)
Pointcut : Jointpoint 중 실제로 Advice가 적용된 부분
Weaving : Advice를 핵심 기능에 적용하는 행동

 

Spring에서는 Proxy를 사용하여 AOP를 구현한다.

Advice (공통기능) -> (weaving) - > Pointcut(핵심기능) 이런 상황에서 중간에 Proxy 삽입.

공통기능 -> Proxy (여기서 공통기능 수행) -> 핵심기능 수행 -> 다시 Proxy로 (공통기능 수행) ... 이런식으로 작동한다.

 

XML 사용

1) pom.xml 에 의존설정 (AOP를 구현하겠다)

<!--AOP-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${org.aspectj-version}</version> <!--1.9.4 등 확인해보기-->
</dependency>

 

2) 공통 기능의 클래스 (Advice 역할 클래스)

import org.aspectj.lang.ProceedingJoinPoint;

public class LogAop {
    public Object log(ProceedingJoinPoint jp) throws Throwable{
        String str = jp.getSignature().toShortString();
        System.out.println(str + " 시작");
        long start = System.currentTimeMillis(); //시간을 체크하는 공통부분

        try{
            Object obj = jp.proceed(); //핵심 기능을 실행
            return obj;
        }
        finally{
            long end = System.currentTimeMillis(); //종료하는 체크부분
            System.out.println(str + " 종료");
            System.out.println(str + " 경과시간 : " + (end -start));
        }
    }
}

ProceedingJoinPoint, 즉 핵심기능을 매개변수로 받아와 실행하고

시작시간, 종료시간을 확인해(공통기능) 로그를 남기는 메소드이다.

한마디로 위의 프록시와 유사한 기능을 한다.

 

3) XML 설정 파일에 Aspect 설정

<beans ...
xmlns:aop ="http://www.springframework.org/schema/aop"
xsi:schemaLocation="....
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">

    <bean id = "logAop" class="com.birdmissile.demo.LogAop"/>

    <aop:config>
        <aop:aspect id ="logger" ref = "logAop">
            <aop:pointcut id ="pID" expression="within(com.birdmissile.demo.*)"/>
            <aop:around pointcut-ref = "pID" method ="log"/>
        </aop:aspect>
    </aop:config>

</beans>

(beans 부분에 aop에 대해서도 위와 같이 추가해줘야 한다.)

더보기

에러코드

[The prefix "aop" for element "aop:config" is not bound.

no declaration can be found for element 'aop:config']

 

aop의 aspect, 즉 id는 logger고, 참조하는건 logAop Bean

여기서 pointcut의 (핵심부분) id는 pID으로 설정. pointcut의 범위는 expression으로 표현.

-> com.birdmissile.demo 내의 모든 기능 (within)에 넣겠다고 명시한 것.

 

advice는 around 타입. 각 타입에 대한 설명은 아래와 같다.

종류 설명
before 메소드 실행 전에 advice 실행
after-returning 정상적으로 메소드 실행 후 advice 실행 (exception 없어야 함)
after-throwing 실행 중 exception 발생 시 advice 실행
after 실행 중 exception이 발생하여도 실행 후 advice 실행
around 실행 전/후 및 exception 발생 시 advice 실행 (before + after)

이후 레퍼런스 pID에 적용하고 메소드는 (위의 bean id logAop 중) "log" 메소드를 사용하겠다.

 

이제 간단하게 ID와 닉네임을 필드로 가지고, getInfo 메소드로 id와 닉네임을 출력하는

Visiter 클래스를 정의하고, xml에도 등록한다.

이후 메인 메소드에서 아래와 같이 getInfo메소드를 호출하면

public static void main(String[] args) {

    AbstractApplicationContext ctx = new GenericXmlApplicationContext("classpath:aop.xml");
    Visiter vs = ctx.getBean("visiter", Visiter.class);
    vs.getInfo();

    ctx.close();
}

 

다음과 같이 getInfo 메소드의 호출 전에 등록한 공통기능(log 메소드)가 실행되어 시작을 수행하고, 핵심기능을 수행하고(getInfo) 다시 공통기능이 실행됨을 확인할 수 있다.

마치 공통기능이 언제 들어갈지 생각하면서 핵심기능의 실행을 보기 때문에 관점 지향인 것 같다.

 

JAVA 사용

1) 의존 설정 (pom.xml)

위의 xml에서와 마찬가지로 pom.xml에 의존 설정 항목을 추가한다.

 

2) 공통 기능의 클래스 (Advice 역할 클래스)

이때 @Aspect 어노테이션을 이용한다.

@Aspect
public class LogAop {

    @Pointcut("within(com.birdmissile.demo.*)")
    private void pcutMethod(){
    }

    @Around("pcutMethod()")
    public Object log(ProceedingJoinPoint jp) throws Throwable{
        String str = jp.getSignature().toShortString();
        System.out.println(str + " 시작");
        long start = System.currentTimeMillis();

        try{
            Object obj = jp.proceed();
            return obj;
        }
        finally{
            long end = System.currentTimeMillis();
            System.out.println(str + " 종료");
            System.out.println(str + " 경과시간 : " + (end -start));
        }
    }
}

필수적으로 @Pointcut을 명시한 메소드가 정의되어 있어야 한다. 굳이 내용이 있을 필요는 없다.

공통 기능 메소드에 @Around로 직접 명시하고, 위의 pointcut 메소드를 연결해준다.

이를 통해 공통기능이 -> pointcut 메소드를 찾아서 -> 범위를 확인하고 적용될 수 있게 된다.

이렇게 하기 귀찮으면 그냥 바로 expression을 명시할 수도 있다.

@Around("within(com.birdmissile.demo.*)")

 

3) XML파일에 자동으로 프록시 설정을 해주는 키워드를 추가한다.

<aop:aspectj-autoproxy/>

프록시를 자동으로 생성해주겠다 (@Aspect 붙은 애를 알아서 만들어라)

 

AspectJ 표현식

위에서 Pointcut을 정할 때 사용한 within(...) 부분이 AspectJ 문법이라고 한다.

가장 기본적으로 * 는 모든, .는 현재, ..는 0개 이상을 표현하는 것이다.

Execution

 //public void인 모든 get 메소드
@Pointcut("execution(public void get*(..))")

//앞에는 상관없음 (*라), com.abc.cd 패키지에 있는 파라미터가 없는() 모든 메소드
@Pointcut("execution(* com.abc.cd.*.*())") 

//얘는 심지어 ..이기 때문에 com.abc.cd 패키지와 그 하위까지 모두 포함
@Pointcut("execution(* com.abc.cd..*.*())") 

//얘는 UIUI를 명시. 이 안의 모든 메소드
@Pointcut("execution(* com.abc.cd.UIUI.*())") 

Within

//com.abc.cd 패키지 내의 모든 메소드 
@Pointcut("within(com.abc.cd.*)") 

//얘는 하위 패키지까지 
@Pointcut("within(com.abc.cd..*)") 

//UIUI 클래스 내의 모든 메소드
@Pointcut("within(com.abc.cd.UIUI)") 

Bean

//vvs 빈에 적용
@Pointcut("bean(vvs)")

//~~~vvs로 끝나는 빈에 적용
@Pointcut("bean(*vvs)") 

XML에서는 위에서 적용했던 것처럼 요로코롬 할 수 있다.

<aop:aspect id ="ID" ref = "beanRef">
    <aop:pointcut id ="xxx" expression="execution(* com.abc.cd..*.*())"/>
    <aop:before pointcut-ref = "xxx" method ="method"/>
</aop:aspect>

 

전문가가 아니라 정확하지 않은 지식이 담겨있을 수 있습니다.
언제든지 댓글로 의견을 남겨주세요!

 

반응형