본문 바로가기

FastCampus - 한번에 끝내는 Java|Spring 웹 개발/03 스프링 입문

스프링 Ch 5 스프링 좀 더 - AOP (2) - 패스트캠퍼스 챌린지 6일차

오늘은 AOP 맛보기 2강을 본다는 걸 핑계로, eclipse 재단의 aspectj 에 대한 근본적인 이해를 시도했습니다. 무엇보다도 AOP (1) 강의에서 나왔던 Pointcut 지점의 표현식이 완전 블랙박스여서 나중에 활용하려고 하더라도 전혀 쓸 수 없는 상태이기 때문에 언젠가 한 번은 알아뒀어야 할 부분이니, 나온 김에 배워두고 가는 게 좋겠는 생각이 들더라구요.

kotlin 으로 하는 실습은 덤이구요. 뭐 별 차이는 없드라구요 ㅋㅋ

Eclipse Aspectj

검색을 해보니 스프링에는 aspectj 말고도 스프링 전용의 좀 더 단순한 Spring AOP 도 있는 모양입니다. 그 쪽은 이번엔 제쳐두고.

starting aspectj 만 읽어보겠습니다. 이 이상으로 들어가는 건 불필요해보이네요.

Aspectj: Why

  • crosscutting concern 은 객체지향으로 담기에 어려워서.
  • -> 여러 클래스 / 메소드에 걸쳐 실행될 내용을, "객체 단위로" 기술하는 방법을 제시

Aspectj: 핵심 개념

  • Joint Point: a well-defined point in the program flow
    잘 정의된 프로그램의 한 지점, 그러니까 훅을 걸 프로그램의 실제 지점을 말합니다.
  • Pointcut: picks out certain join points and values at those points
    특정 Joint Point 와 그 지점에 있는 값들을 잡아챕니다. 훅을 걸 지점을 정의한 걸 말하는 모양이네요.
  • Advice: is code that is executed when a join point is reached (These are the dynamic parts of AspectJ)
    Pointcut 에서 정의했던 Joint Point 에 도달하면 실행할 함수들입니다. 동적인 부분이라는 설명이 있던데 잘 이해가 안 되니 일단 패스

Aspectj: 구성요소:
Pointcut - 낚아챌 지점 정의

Pointcut 문법은 몇 개 예시를 보면 금방 알 것 같이 생겼습니다. (문서에서 챙겨왔음)

// 단일 call Pointcut 정의는 생략 (밑에 보세요)

// 여러 Pointcut의 묶음으로 정의도 가능
pointcut move():
    call(void FigureElement.setXY(int,int)) ||
    call(void Point.setX(int))
    ;

// 와일드카드도 가능
call(void Figure.make*(..))
call(public * Figure.* (..))

// 다른 Pointcut 을 대상으로 잡을수도 있음
cflow(move())

fastcampus 강의에서는 execution 포인트를 지정했지만, 시작하기 문서에는 call 만 소개합니다. 둘의 차이점은 문서에 있네요.

https://www.eclipse.org/aspectj/doc/released/progguide/language-joinPoints.html#call-vs-execution

이 둘은 실행 시점이 미묘하게 다르므로, Advice에서 잡을 수 있는 문맥이 다릅니다.

  • call: 함수의 본문이 아직 실행되기 이전의 시점에 낚아챕니다.
  • execute: 함수의 본문이 이미 실행된 다음에 낚아챕니다.

이것만으로는 실사용시의 차이점을 잘 모르겠지만 지금은 저 둘의 차이점이 중요하지 않은 상황이므로 건너뛰겠습니다. 필요해질 때 보면 되겠죠!

Aspectj: 구성요소:
Advice - 낚아챘을 때 실행할 함수

좀 많습니다.

  • before
  • after
    • 그냥 after (finally 와 같음)
    • after returning
    • after throwing
  • around: 튜토리얼에서 설명하지 않습니다. 호출 여부 자체를 막을 수 있다는걸로 봐서 manual call visitor 처럼 직접 호출하는 구조인가봅니다.

지점을 선언하는 방법, 패러미터를 정의하는 방법 (다른 pointcut 인 this / target / args 를 활용해서 값을 가져오는 방법) 등이 설명이 되어있는데, 아마 어노테이션으로 하는 거랑 문법이나 방법이 좀 다를겁니다. 일단 참고로만 기록을 해두겠습니다. 자세한 건 문서를 확인.

after(FigureElement fe, int x, int y) returning:
        call(void FigureElement.setXY(int, int))
        && target(fe)
        && args(x, y) {
    System.out.println(fe + " moved to (" + x + ", " + y + ")");
}

Aspectj: 구성요소:
Inter-type declaration: 정적 컴파일되는 AOP Interface Mixin

컴파일타임에 클래스 계층구조를 바꾸거나 인터페이스를 추가하거나 한답니다. 인터페이스를 만들고 그 구현을 상속받는 방식이라나요? 생긴게 딱 Mixin 같습니다.

아무튼 지금은 중요하지 않을 것 같습니다. 예시도 생략.

AOP 1강의 블랙박스: Aspect 와일드카드 문법

@Pointcut("execution(* com.example.aop.controller..*.*(..))")

https://www.eclipse.org/aspectj/doc/released/progguide/quick-typePatterns.html

  • * 기호: 하위 패키지까지는 매칭하지 않습니다.
    An embedded * in an identifier matches any sequence of characters, but does not match the package (or inner-type) separator ".".
  • .. 기호: 패키지 구분자 사이의 문자열을 모두 매칭합니다.
    An embedded .. in an identifier matches any sequence of characters that starts and ends with the package (or inner-type) separator ".".

대체 뭔 차이야...???

https://stackoverflow.com/a/12303934

* 은 요소 하나고 .. 은 그 사이 죄다래요. 그럼 저 Pointcut 정의는 ..* 로 끝났어도 아무 문제가 없었을 내용이네요...

직접 해보자: kotlin REST

아무리 해도 JSON 이 반환이 안 되더라구요. 아무 생각 없이 Around 어드바이스를 넣어서 그랬습니다. ResponseEntity<>는 필요없었지만 그것도 까먹었고 말이죠

잘 돌아가네요. 남은건 강의를 마저 보는 것 뿐입니다...

@RestController
@RequestMapping("/api/greeting")
class HelloApi {

    @GetMapping("/{name}")
    fun greeting(@PathVariable name: String): String {
        return "Hello, ${name}!"
    }

    @PostMapping
    fun addGreeting(@RequestBody user: User): User {
        return user
//        return ResponseEntity.ok(user)
    }
}
@Component
@Aspect
class AspectLogger {
    @Pointcut("execution(* com.fastcampus.aop_demo..* (..) )")
    private fun cut() {}

    @Before("cut()")
    fun before_cut() {
        println("before_cut")
    }
}

AOP "실무 사례" (1)

이제 joinPoint가 뭔지 그 의미를 아니까 헤매일 이유가 없습니다.

@Component
@Aspect
class AspectLogger {
    @Pointcut("execution(* com.fastcampus.aop_demo..* (..) )")
    private fun cut() {}

    @Before("cut()")
    fun before_cut() {
        println("before_cut")
    }

    @AfterReturning("cut()", returning = "returnObj")
    fun cut_returning(joinPoint: JoinPoint, returnObj: Any) {
        println("class:: ${joinPoint.target.javaClass.simpleName}")
        println("method:: ${(joinPoint.signature as? MethodSignature)?.method?.name}")
        val params = joinPoint.args?.map {
            "type ${it.javaClass.simpleName} / value $it"
        }?.joinToString()
        println("args:: ${params ?: "(none)"}")

        println("return:: ${returnObj.javaClass.simpleName}")
        println(returnObj)
    }
}

AOP "실무 사례" (2) - 커스텀 어노테이션 만들기

어노테이션 만들기

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {}

kotlin 어노테이션 만들기 - java 어노테이션과 호환된다고 하네요.

https://kotlinlang.org/docs/annotations.html

@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Timer

@Retention: 어노테이션을 어디에 저장할지 결정할 수 있습니다. SOURCE: 소스에만, BINARY: 바이너리에도, RUNTIME: 바이너리 뿐만이 아니라 런타임에까지 유지 (디폴트)

@Aspect
@Component
class TimerLogger {

    @Pointcut("execution( * com.fastcampus.aop_demo..*(..) )")
    private fun cutAll() = Unit

    @Pointcut("@annotation(com.fastcampus.aop_demo.cross.Timer)")
    private fun cutTimer() = Unit

    @Around("cutAll() && cutTimer()")
    fun around(jointPoint: ProceedingJoinPoint) {
        val watch = StopWatch()

        watch.start()

        val time = measureTimeMillis {
            val result = jointPoint.proceed()
        }

        watch.stop()

        println("time: $time")
        println("time (spring): ${watch.totalTimeMillis}")
    }
}

타이머는 스프링에도 있고 기존에 알고있던 kotlin의 것도 있습니다. 어노테이션을 만드는 거부터가 신기한데 이미 AOP가 있으니까 거기에서 사용하는 것도 놀랍네요.

AOP "실무 사례" (2) - 값 변환하기

예시로, 특정 외부기관이 암호화를 해서 보내주는 경우에 어노테이션만으로 적용할 수 있게 만드는 게 나왔습니다. 의외로 @Before, @After 에서도 가능하네요. 역시 jointPoint 를 사용해서 호출 전/후에 처리합니다. execution 포인트컷인 건 동일하구요.

user.email 이 base64로 인코딩되어있다고 가정하네요. java.utils.Base64 에 내장 기능으로 있었군요

이 부분은 지금은 실습할 필요가 없을 것 같습니다. 나중에 필요할 때 보면 되겠죠. 노트에만 정리해야겠네요.

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online