올해 처음으로 늦잠을 자 지각을 해버렸다..













해야할 일이 있거나 약속이 있으면 무조건
늦잠을 안자고 번뜩일어나는데
오늘은 알람소리를 못들었다…..
수면패턴이 조금 바뀌어서 그런가 싶기도하고

구내염도 난 것 보면
스트레스관리와 수면관리좀 해야할 것 같다.


어제는 AOP의 간단한 용어들과\
Advice의 타입별 사용방법을 알아보았었다.
오늘은 Pointcut 표현식과 JoinPoint 특징과

자바코드로 예제를 직접짜보고 Spring 적용 변경해보는
실습을 혼자만들어서 해보자!


Pointcut 표현식

Pointcut은 어제 알아보았듯이
우리가 JoinPoint에서 어느 부분을 쓸지 정해주는 부분이다
AspectJ 이용해 우리는 표현식을 사용할 수 있다.

import org.aspectj.lang.annotation.*;

어제 보았던 예제를 보자

@Around("execution(* com.aop.section2aopcalculate..*(..))")
public Object AroundLog(ProceedingJoinPoint joinPoint) throws Throwable{
    System.out.println("--> Aspect 실행 : " + joinPoint.getSignature().getName());
    try {
        return joinPoint.proceed();
    }finally {
        System.out.println("--> Aspect 종료 : " + joinPoint.getSignature().getName());
    }
}

Adivce 타입은 @Around이고

Pointcut 부분은 (“execution(* com.aop.section2aopcalculate..*(..))”)에
해당한다. 첫번째는 ‘포인트컷 지시자(PCD)’ 이고 뒤에는 내 패키지내 경로이다.

execution : 포인트컷 지시자
com.aop.section2aopcalculate..*(..) : 패키지 경로, 클래스 메서드 명

주로 execution을 가장 많이 사용한다.

Type Description
execution 메서드 실행 JoinPoint를 매칭한다.
within 특정 타입 내의 JoinPotin를 매칭한다.
args 인자가 주어진 타입의 인스턴스인 JoinPoint
this Spring Bean을 대상으로 하는 JoinPoint
target Target 객체를 대상으로 하는 JoinPoint
bean Spring 전용 PCD이고 Bean의 이름으로 Pointcut을 지정한다.

&&, || , ! 같은 표현식을 사용해 결합할 수 도 있다.
어제 예제를 가져와서 아래와 같은 경로에 있다고 가정하자.














NewCalculate라는 클래스는
com.aop.section2aopcalculate.calculate 패키지 안에 경로가 지정되어있다.

예를 들어 하나의 클래스의 메서드만 동작시키고 싶을 경우 (우린 add메서드를 가정하자)
아래와 가은 방식으로 표현할 수 있다.

("execution(* com.aop.section2aopcalculate.calculate.NewCalculate.add(..))")

여기서 결합식을 사용해 우리가 add메서드와 sub메서드를 둘다 실행시킬 수 있다.

("execution(* com.aop.section2aopcalculate.calculate.NewCalculate.add(..)) || execution(* com.aop.section2aopcalculate.calculate.NewCalculate.sub(..))")

|| 표현식으로 결합식을 사용하면 add,sub 메서드가 둘다 지정되어
실행되는 모습을 볼 수 있다.


JoinPoint

JoinPoint는 우리가 포인트컷으로 지정한 곳이고
추상적인 개념으로 AOP를 적용할 수 있는 지점을 의미한다.

  1. Advice가 적용될 수 있는 위치, 메소드 실행, 생성자 호출, 필드값 접근 같은 프로그램 실행 중 지점을 나타낸다.
  2. AspectJ를 사용해서 컴파일 시점과 클래스 로딩 시점에 적용하는AOP는 바이트코드를 실제 조작하기 때문에 해당 기능을 모든 지점에 다 적용할 수 있다.
  3. 프록시 방식을 사용하는 Spring AOP는 메서드 실행 지점에만 AOP 적용이 가능하다.
  4. 프록시는 메서드 오러라이딩 개념으로 동작한다.
  5. 프록시를 사용하는 Spring AOP의 JoinPoint는 메서드 실행으로 제한된다.

JoinPoint 인터페이스 기능
.getArgs() : 전달된 인자를 배열로 반환한다.
.getThis() : AOP 프록시 객체를 반환한다.
.getTarget() : AOP가 적용된 대상 객체를 반환한다. (비지니스 메소드를 포함하는 객체반환)
.toString() : 방법에 대한 유용한 설명을 인쇄한다.
.getSignature() : 메서드에 대한 설명을 반환한다.
-> getSignature().getName() : 호출한 메서드의 이름을 반환한다. (String)
-> getSignature().toLongString() : 호출한 메서드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함해서 반환한다. (String)
-> getSignature().toShoryString() : 호출한 메서드 시그니처를 축약한 문자열로 반환한다. (String)

ProceedingJoinPoint 인터페이스 기능
.proceed() : 다음 어드바이스나 타겟을 호출한다.


Java 코드로 AOP 작성해보기

조금 더 AOP에 대해 심도있게 이해해보기 위해서
셀프과제를 만들었다.

  1. Java 코드로 AOP를 작성해보는 시간을 가져보았다.
  2. 공통 관심 사항을 메서드 실행 시간 측정으로 두었고
  3. 핵심 로직은 점수 합산과 평균을 구하는 것으로 지정했다.
  4. 메서드 실행시 AOP가 동작하는지 확인








총 4개의 Class를 만들어 테스트를 해보았다.

<Exam.interface / 메서드 인터페이스 추상화>

public interface Exam {
    int total();
    float avg();
}

Score.class에서 구현하게 추상화를 해둔 Interface
가장 큰 이유는 아래에 Aop.class에서
Proxy.newProxyInstance의 매개변수로 인터페이스를 사용해야하기 때문

<Score.class / 핵심 로직(합산,평균)>

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Score implements Exam{
    private int kor;
    private int eng;
    private int math;
    private int com;

    public Score(int kor, int eng, int math, int com) {
        this.kor = kor;
        this.eng = eng;
        this.math = math;
        this.com = com;
    }
    @Override
    public int total(){
        System.out.println("--> total method 시작");
        int result = kor+eng+math+com;
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            System.out.println(e);
        }
        System.out.println(result);
        return result;
    }
    @Override
    public float avg(){
        System.out.println("--> avg method 시작");
        float result = (kor+eng+math+com) / 4.0f;
        try{
            Thread.sleep(2000);
        }catch (Exception e){
            System.out.println(e);
        }
        System.out.println(result);
        return result;
    }
}

Lombok @Getter,@Setter를 사용하여
Getter,Setter의 긴 코드를 생략했습니다.

메서드 시간측정을 위해 중간 중간에 Thread.sleep();을
이용해 메서드의 머무는 시간을 늘려주었습니다.

각각메서드의 시작을 알리는 코멘트를 프린트하였고
메서드 실행 결과값을 출력하였습니다.

<Aop.class / 공통관심사 코드>

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Aop {
        // Proxy를 이용한 AOP 구현 방법
        public Exam aopApply(Exam exam){
            Exam proxy = (Exam) Proxy.newProxyInstance(Score.class.getClassLoader(),
                    new Class[]{Exam.class},
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            System.out.println("START: " + 0  + "ms" + " / " +getClass());

                            long start = System.currentTimeMillis();
                            Object result = method.invoke(exam, args);
                            long end = System.currentTimeMillis();

                            System.out.println("END: " + (end-start) + "ms" + " / " +getClass());
                            return result;
                        }
                    });
            return proxy;
        }
}

동적 프록시(Dynamic Proxy)를 사용하여 코드를 작성했다.
동적프록시는 런타임 시점에 프록시 클래스를 만들어주는 방식이다.
사용하는 이유는 프록시 타겟코드의 수정없이 접근 제어 , 부가기능을 추가하려고 사용한다.
즉, 우리가 AOP를 사용하는 목적과 부합한다!!!
Proxy.newProxyInstance() 메서드로 Proxy를 생성하는데 3개의 파라미터를 가진다.

  • loader : 프록시 클래스를 정의하는 클래스 로더
  • interfaces : 프록시 클래스가 구현하는 인터페이스 리스트
  • h : 메서드 호출을 처리하는 핸들러
  1. 프록시 클래스를 지정 : Score.class.getClassLoader()
  2. 프록시 클래스가 구현하는 인터페이스를 지정 : new Class[]{Exam.class}
  3. 메서드 호출을 처리하는 핸들러 구현 : new InvocationHandler(){}
    .invoke 메서드를 Override 하여 구현하기

시작과 끝을 알리는 코멘트를 추가했으며
메서드 호출 전후로 시간을 측정하여 차액을 표시하게 해두었다.

<Application.class / 클라이언트 요청부분>

public class Application {
    public static void main(String[] args) {
        Aop aop = new Aop();
        Exam exam = new Score(100,80,80,90);
        Exam proxy = aop.aopApply(exam);
        
        System.out.println("=======================");
        proxy.total();
        System.out.println("=======================");
        proxy.avg();
        System.out.println("=======================");
    }
}
/* 출력
=======================
START: 0ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
--> total method 시작
350
END: 1049ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
=======================
START: 0ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
--> avg method 시작
87.5
END: 2004ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
=======================
*/

전부 구현을 완료했으니 동작을 해보자!
Application을 main메서드를 만들어 동작시킬 수 있게 해주었다.
먼저 각각 점수를 입력하기위해 Exam타입의 Score객체를 만들었다.

만듣 객체는 Aop클래스의 aopApply 매개변수로 객체가 전달되어
해당 객체로 반환받은 Exam타입의 proxy 참조변수를 이용하여
.total(), .avg()를 호출 할 수 있다.

출력 결과를 보면 알 수 있듯이 main 메서드에서
proxy.total() , proxy.avg()를 실행시켰는데
사이에 우리가 측정하고자하는 공통관심 코드가 적용된 것을 볼 수 있다.

여기서 중요한점 !!
우리는 Score.class의 있는 코드를 전혀 수정하지 않아도
공통관심코드가 핵심코드 사이에 햄버거마냥
딱 끼워져서 동작하는 모습을 볼 수 있다는 점!! ★★

우리가 수백개의 메서드를 추가하여도 공통관심코드를 통해
메서드가 호출되기때문에 AOP가 잘 적용되어졌다고 볼 수 있을 것 같다.

Spring을 이용해 AOP 작성해보기

조건은 위에서 작성한 것과 동일하다 이번에는 인터페이스를 이용하지 않고
Score의 내용을 분리한다음에 Spring의 @Aspect를 이용하여
위와 동일하게 동작하게 만들어보려한다.










총 4개의 클래스로 구성했고
패키지명은 위와 같이 경로를 지정했으니 아래코드에서 헷갈리지말자!
코드로 내용을 확인해보자.

<Score.class / 점수 관리 객체>

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Score {
    private int kor;
    private int eng;
    private int math;
    private int com;

    public Score(int kor, int eng, int math, int com) {
        this.kor = kor;
        this.eng = eng;
        this.math = math;
        this.com = com;
    }
}

위와 동일하게 Lombok을 이용해
Getter,Setter를 만들고 후에 클라이언트에서 객체를 만들어
점수를 넣고 테스트하려고 만든 클래스이다.

<NewlecService / 핵심 로직(합산,평균)>

import org.springframework.stereotype.Service;

@Service
public class NewlecService {

    public int total(Score score){
        System.out.println("--> total method 시작");
        int result = score.getKor()
                +score.getEng()
                +score.getMath()
                +score.getCom();
        System.out.println(result);
        try{
            Thread.sleep(1000);
        }catch (Exception e){
            System.out.println(e);
        }
        return result;
    }

    public float avg(Score score){
        System.out.println("--> avg method 시작");
        float result = (score.getKor()
                +score.getEng()
                +score.getMath()
                +score.getCom()) / 4.0f;
        System.out.println(result);
        try{
            Thread.sleep(2000);
        }catch (Exception e){
            System.out.println(e);
        }
        return result;
    }
}

Java 코드로 작성한 Score.class와 동일한 부분이다.
메서드 실행시 코멘트로 알려주고 쓰레드 딜레이를 넣어
메서드 실행 시간을 늘려주었다.
@Service를 이용해 Bean에 등록해 스프링 컨테이너가 관리한다.

<TimeTraceAop.class / 공통 관심 코드>

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect // AOP
@Component // Bean 등록
public class TimeTraceAop {

    @Around("execution(* com.AopExample.Aop_Example..*(..))")
     public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
        long start = System.currentTimeMillis();
        System.out.println("START: " + 0  + "ms" + " / " +getClass());
        try {
            return joinPoint.proceed();
        }finally {

            long end = System.currentTimeMillis();
            System.out.println("END: " + (end-start) + "ms" + " / " + getClass());
        }
     }
}

동일하게 시간을 측정하는 공통관심 클래스이다.
@Aspect 어노테이션으로 AOP 사용을 알려주고
@Component로 Bean을 등록했다.

@Around Adive타입을 이용해 메서드를 만들었고
Pointcut은 Aop_Example 패키지 전체를 메서드로 지정했다.
joinPoint.proceed();를 이용해 메서드를 호출하고 있고
위 아래에 시간을 측정하는 코드를 두었다.

<AopExampleApplication.class / 스프링 부트실행, 클라이언트코드>

import com.AopExample.Aop_Example.aopSpring.NewlecService;
import com.AopExample.Aop_Example.aopSpring.Score;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@SpringBootApplication
public class AopExampleApplication {
   public static void main(String[] args) {
      SpringApplication.run(AopExampleApplication.class, args);
      ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopExampleApplication.class);
      NewlecService newlecService = applicationContext.getBean("newlecService", NewlecService.class);

      Score myScore = new Score(100,80,80,90);

      System.out.println("=======================");
      newlecService.total(myScore);
      System.out.println("=======================");
      newlecService.avg(myScore);
      System.out.println("=======================");
   }
}
/* 출력
=======================
START: 0ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
--> total method 시작
350
END: 1029ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
=======================
START: 0ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
--> avg method 시작
87.5
END: 2006ms / class com.AopExample.Aop_Example.aopSpring.TimeTraceAop
=======================
*/

스프링 부트 실행 클래스 아래에 클라이언트 요청 코드도 같이두었다.
Java코드로 작성한 것과 다른점은 Java코드에선 proxy 객체를 이용했고
Spring은 스프링컨테이너(ApplicationContext)에서
.getBean으로 객체를 가져와 컨테이너에 있는 객체로 메서드를 호출했다.

현재 스프링 컨테이너에 등록된 Bean은
com.AopExample.Aop_Example.AopExampleApplication\(EnhancerBySpringCGLIB\)62eea1e6@236ab296
com.AopExample.Aop_Example.aopSpring.NewlecService@5c84624f
com.AopExample.Aop_Example.aopSpring.TimeTraceAop@63034ed1
이렇게 3개로 확인해볼 수 있다.

AopExampleApplication은 뒤에 EnhancerBy~~이렇게 붙여져있는데
스프링에 빈을 등록할때 바이트 코드를 조작한 객체가 스프링 컨테이너에 등록되기 때문이다.
AOP 기술과 싱글톤 보장의 이유기도하다.
@Configuration도 동일한 기능을 제공해주고
@SpringBootApplication을 사용하였기때문에 내부에
@SpringBootConfiguration이 있기 때문에 위와 같이 바이트 코드가 조작된 객체로 보이는 것이다.


오늘은 이렇게 Pointcut 표현식과 JoinPoint에 대해 간단히 알아봤고
Java로 작성한 AOP기술을 이용한 예제와
Spring으로 작성한 AOP기술을 이용한 예제
이렇게 2개를 비교해서 알아보았다.
동일한 동작을 하는 코드로 작성하였고
이렇게 두개를 비교하면서 프로그램을 타이핑해보니
조금더 이해가 쏙쏙되는 느낌이다.

오늘 공부는 여기서 끝!


오늘의 커피량: ☕️ ☕️ ☕️ ☕️
오늘의 점심: 제육볶음, 밥