Spring AOP (5)

AOP를 어느정도 공부하고 회사 업무에 써먹어 보기로 했다. 결제에 대한 로그 데이터를 DB에 Insert하는 로직을 AOP로 구현하고 코드 리뷰를 딱! 올렸다. AOP사용 시 주의 할 부분에 대한 리뷰가 달렸다. 아래의 리뷰와 같다.

AOP코드리뷰

메소드 명에 대한 와일드 카드 처리는 AOP를 설정한지 모르는 다른 개발자가 똑같은 prefix로 시작하는 경우 의도치 않은 Advice로직을 적용 시킬 수 있기 때문에 주의를 강조 했다.

이를 극복 할 수 있는 방법으로 Custom Annotation 클래스를 생성하여 메소드의 Annotation으로 pointcut을 잡아 보는 것을 알아보았다.

Custom Annotation

API에서 지정된 Annotation 이외에 개발자가 필요로 목적으로 정의한 Annotation을 말한다.

Annotation 생성하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.lang.annotation.*;

@Inherited //Annotation의 상속이 가능함
@Documented //JavaDoc 문서 추가 가능
@Retention(RetentionPolicy.RUNTIME) // 컴파일 이후에도 JVM에 의해서 참조가 가능
//@Retention(RetentionPolicy.CLASS) // 컴파일러가 클래스를 참조할 때까지 유효
//@Retention(RetentionPolicy.SOURCE) // 어노테이션 정보는 컴파일 이후 사라짐 (ex. lombok)
@Target({
ElementType.PACKAGE, // 패키지 선언시
ElementType.TYPE, // 타입 선언시
ElementType.CONSTRUCTOR, // 생성자 선언시
ElementType.FIELD, // 멤버 변수 선언시
ElementType.METHOD, // 메소드 선언시
ElementType.ANNOTATION_TYPE, // 어노테이션 타입 선언시
ElementType.LOCAL_VARIABLE, // 지역 변수 선언시
ElementType.PARAMETER, // 매개 변수 선언시
ElementType.TYPE_PARAMETER, // 매개 변수 타입 선언시
ElementType.TYPE_USE // 타입 사용시
})
public @interface CustomAnnotation { //interface에 @를 붙여주면 Annotation으로 등록
/* enum 타입을 선언할 수 있습니다. */
public enum Quality {BAD, GOOD, VERYGOOD}
/* String은 기본 자료형은 아니지만 사용 가능합니다. */
String value();
/* 배열 형태로도 사용할 수 있습니다. */
int[] values();
/* enum 형태를 사용하는 방법입니다. */
Quality quality() default Quality.GOOD;
}

Annotation Pointcut 지정하기

예시로 Annotation을 추가하여 Pointcut을 설정해 보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.aopexam;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
//AOP Advisor를 Component로 선언하여 ComponentScan이 되어야 AOP적용 가능
@ComponentScan(value={"com.example.aopexam", "com.example.custom.annotation"})
public class AopExamApplication {

public static void main(String[] args) {
SpringApplication.run(AopExamApplication.class, args);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.aopexam;

import com.example.custom.annotation.CustomLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/aop-test")
public class ApiController {

@Autowired
private ApiService apiService;

@GetMapping
@CustomLog //get 메소드에 @CustomLog추가
public void getTest() {
apiService.method(ApiType.GET);
}

@PostMapping
public void postTest() {
apiService.method(ApiType.POST);
}

@PutMapping
public void putTest() {
apiService.method(ApiType.PUT);
}

@DeleteMapping
public void deleteTest() {
apiService.method(ApiType.DELETE);
}

}

1
2
3
4
5
6
7
8
9
10
11
package com.example.custom.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) //메소드 어노테이션 적용
public @interface CustomLog {
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.custom.annotation;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true) //cglib 사용 -> class기반 proxy생성
@Component
public class CustomLogAdvisor {

//Annotation을 기준으로 pointcut설정
@Pointcut("@annotation(com.example.custom.annotation.CustomLog)")
public void customLogPointcut() {
}

@Before(value="customLogPointcut()")
public void pringLog() {
System.out.println("메소드 실행 전 무조건 이 로그를 보게 될 것이야...");
}
}

1
2
3
4
http://localhost:8080/api/aop-test

메소드 실행 전 무조건 이 로그를 보게 될 것이야...
[ApiService.method] apiType : GET