Light Blue Pointer
본문 바로가기
Developing/TIL(Develop)

Spring AOP

by Greedy 2024. 5. 8.

Aspect Oriented Programming

Aspect

Aspect: 여러 클래스나 모듈에 영향을 미치는 기능의 모듈화된 단위

로깅, 보안, 트랜잭션 관리와 같이 프로그램의 비즈니스 로직과 분리할 수 있는 부분을 묶은 것

주요 비즈니스 로직 안에 저런 코드들을 분산해서 모두 넣지 않고 한 곳에서 관리할 수 있게 한다.

In Spring AOP

aspect를 일반 클래스로 구현하는 방식(the schema-based approach)와 @Aspect 어노테이션으로 주석이 달린 일반 클래스(@AspectJ style)로 구현하는 방식 두가지가 있다.

Join point

어떤 method의 실행 / Exception을 handling할 때 와 같이 프로그램 실행 중에 만나는 어떤 포인트이다.

In Spring AOP )

Spring AOP에서는 항상 메서드의 실행을 나타낸다.

Advice

특정 Join Point에서 aspect가 취하는 작업

"around", "before", "after"와 같은 다양한 종류의 advice가 있다.

Spring과 같은 많은 AOP 프레임워크는 advice를 interceptor로 모델링하고 조인 포인트 주위에 interceptor 체인을 유지한다.

PointCut

Join Point와 일치하는 조건

Advice는 pointcut 표현식과 관련되어 있으며 pointcut에 의해 일치하는 모든 Join Point에서 실행됨

Pointcut 표현식에 의해 일치하는 Join Point의 개념은 AOP의 핵심이다.

Spring은 AspectJ pointcut expression language가 default 이다.

AspectJ pointcut expression language

https://www.baeldung.com/spring-aop-pointcut-tutorial

다음과 같이 사용된다

@Pointcut("execution(public String com.baeldung.pointcutadvice.dao.FooDao.findById(Long))")
@Pointcut("@args(com.baeldung.pointcutadvice.annotations.Entity)")
public void methodsAcceptingEntities() {}
@Pointcut("target(com.baeldung.pointcutadvice.dao.BarDao)")
@Pointcut("@target(org.springframework.stereotype.Repository)")

Introduction

기존의 클래스나 객체에 새로운 메서드나 필드를 추가하는 것

기존 클래스의 소스 코드를 변경하지 않고도 새로운 기능을 도입하거나 기존 기능을 확장할 수 있게 해준다

Spring AOP에서는 어떤 Target object에 대해 새로운 인터페이스(및 구현)를 도입할 수 있다

Spring AOP에서는 기존의 Bean 객체에 대해 새로운 인터페이스와 해당 구현을 도입할 수 있다.

이를 통해 기존의 빈 객체가 새로운 인터페이스를 구현하고 그에 따른 메서드를 제공할 수 있게 되는데, 이를 통해 캐싱과 같은 기능을 간편하게 적용할 수 있다. 이러한 방식을 통해 기존의 빈 객체를 변경하지 않고도 캐싱과 같은 기능을 추가할 수 있다.

AspectJ 커뮤니티에서는 Introduction을 Inter-type declaration(타입 간 선언)이라고도 한다.

타입 간에 새로운 요소를 도입한다는 개념을 강조한다.

Target object = Advised Object

하나 이상의 aspect에 의해 Advised되는 객체

Spring AOP는 runtime proxy들을 사용해서 구현되므로 이 객체들은 항상 proxied 객체이다

Proxied Object 프록시된 객체(Proxied Object)는 실제 객체(타깃 객체)에 대한 대리자 역할을 하는 객체이다. 프록시된 객체는 클라이언트가 실제 객체에 접근할 때 중간에서 요청을 처리하고 추가 동작을 수행할 수 있다. 이를 통해 AOP는 어플리케이션의 특정 관심사(로깅, 보안, 트랜잭션 관리 등)를 모듈화하고 재사용 가능한 방식으로 적용할 수 있게 된다. 프록시된 객체는 실제 작업을 수행하는 객체에 대한 래퍼(wrapper) 역할을 한다.

AOP Proxy

aspect contracts (advice method execution 같은 것들)을 구현하기 위해 AOP framework에 의해 만들어진 객체

In Spring AOP )

AOP proxy 는 JDK dynamic proxy(default) 혹은 CGLIB proxy이다

JDK dynamic proxy

JDK 동적 프록시(JDK dynamic proxy)는 자바 프로그래밍 언어에서 제공하는 기능 중 하나이다. 이를 사용하면 런타임에 인터페이스를 구현하는 객체의 프록시를 생성할 수 있다. JDK 동적 프록시는 java.lang.reflect 패키지에 있는 Proxy 클래스를 사용하여 생성된다. 이는 인터페이스를 구현하는 객체에 대해서만 프록시를 생성할 수 있다는 제한이 있다. 이런 제한은 인터페이스를 활용한 프록시 생성에 매우 유용하며, Spring AOP에서 기본적으로 사용된다.

CGLIB proxy 인터페이스가 아닌 클래스를 프록시화 할때 사용한다

Weaving

다른 응용 프로그램 유형이나 객체에 aspect를 링크하여 Target object를 생성하는 것

컴파일 시, 로드 시 또는 실행 시에 수행된다

In Spring AOP )

Spring AOP는 다른 pure Java AOP 프레임워크와 마찬가지로 실행 시에 weaving을 수행한다.

Spring에서 Advice의 종류

Before advice

조인 포인트 앞에서 실행되는 어드바이스

실행 흐름을 조인 포인트로 진행시키지 않는다는 점에서 특정 예외를 던지지 않는 한 실행을 막을 수 없다

After returning advice

메서드가 예외를 던지지 않고 정상적으로 완료될 때 실행되는 어드바이스

After throwing advice

메서드가 예외를 던져서 종료될 때 실행되는 어드바이스

After (finally) advice

조인 포인트가 정상적인 리턴 또는 예외 리턴에 관계없이 실행되는 어드바이스

Around advice

메서드 호출(method invocation)과 같은 조인 포인트를 둘러싸는 어드바이스

메서드 호출 전후에 사용자 정의 동작을 수행

조인 포인트로 진행할지 또는 어드바이스된 메서드 실행을 바로 종료시킬지 여부를 선택

Around advice보다 적은 실행량의 advice로 원하는 기능을 수행할 수 있다면 그걸 쓰는 것이 권장된다

예를 들어, 메서드의 반환 값으로 캐시를 업데이트해야 하는 경우, around 어드바이스 대신에 after returning 어드바이스를 구현하는 것이 더 좋다.

가장 적게 동작하는 어드바이스 유형을 사용하면 잠재적인 오류 가능성이 줄어들며 더 간단한 프로그래밍 모델을 만들 수 있다

포인트컷에 의해 일치하는 조인 포인트의 개념은 AOP의 핵심이며, 이는 단순히 가로채기만 제공하는 이전 기술과 구분됩니다. 포인트컷을 통해 어드바이스를 객체 지향적 계층 구조와 독립적으로 대상으로 지정할 수 있습니다. 예를 들어, 서비스 계층의 모든 비즈니스 작업에 선언적 트랜잭션 관리를 제공하는 around 어드바이스를 적용할 수 있습니다.

Spring AOP의 특징

순수 자바로 구현되어 특별한 컴파일 과정이 필요하지 않다

클래스 로더 계층 구조를 제어할 필요가 없어 서블릿 컨테이너나 애플리케이션 서버에서 사용하기에 적합

Spring AOP는 메서드 실행 조인 포인트만 지원

필드 인터셉션은 현재 지원하는 기능이 아니지만 미래에 추가될 수 있다.

필요에 따라 필드 접근과 업데이트 조인 포인트를 조언해야 할 경우, AspectJ와 같은 언어를 사용해야 한다

Spring AOP는 다른 AOP프레임워크와 다르게 구현되어 있다

주 목적은 AOP 구현과 Spring IoC 간의 밀접한 통합을 제공하여 엔터프라이즈 애플리케이션에서 일반적인 문제를 해결하는 것이다

Spring AOP 기능은 보통 Spring IoC 컨테이너와 함께 사용한다

Spring AOP로는 도메인 객체와 같이 매우 세분화된 객체를 쉽게 또는 효율적으로 Advise하는 것이 어렵거나 효율적이지 않다. 이러한 경우에는 AspectJ를 쓰는 것이 낫다

Spring 프레임워크의 중심 원칙 중 하나는 비침입성(non-invasiveness)이다. 그렇지만 호환도 된다~

비침입성(non-invasiveness) 비즈니스나 도메인 모델에 프레임워크별 클래스와 인터페이스를 강제로 도입시키지 않아야 한다는 아이디어

그러나 Spring 프레임워크는 특정한 상황에서 Spring 프레임워크별 종속성을 도입할 수 있게 한다

왜냐하면 특정한 상황에서는 다른 언어로 특정 기능을 읽거나 코드를 작성하는 것이 더 간단하기 때문이다

예를 들어, 어노테이션 기반 접근 방식을 더욱 직관적으로 이해하고 특정 기능을 표현하는 데 어노테이션을 사용하는 것이 더 쉬운 경우, Spring AOP에서 @AspectJ 주석 스타일 접근 방식을 사용하는 것이 좋을 수 있다. 반면에 보다 선언적인 XML 구성 스타일을 선호하거나 팀이 이미 XML 기반 구성에 많은 투자를 한 경우, Spring XML 구성 스타일 접근 방식이 더 편리할 수 있다.

AOP와 Reflection

리플렉션은 런타임에 클래스의 정보를 조사하고 수정할 수 있는 능력을 제공한다.

이를 통해 클래스의 메서드, 필드, 생성자 등에 접근하고 조작할 수 있다.

AOP에서는 리플렉션을 통해 프록시 객체를 생성하고, 프록시 객체를 활용하여 관심사를 모듈화하고 조인 포인트를 가로채어 추가 동작을 삽입합니다. 이렇게 하면 코드의 중복을 줄이고, 각각의 모듈이 명시적으로 관리되어 유지보수가 용이해진다.

Spring AOP에서는 리플렉션을 사용하여 동적 프록시를 생성하고 관점 지향적인 기능을 추가한다.

Reflection in Java

리플렉션은 실행 시간에 메서드, 클래스 및 인터페이스의 동작을 조사하거나 수정하는 데 사용되는 API이다.

객체가 속한 클래스에 대한 정보와 해당 클래스의 메서드를 확인하고 객체를 사용하여 실행할 수 있는 메서드를 가져올 수 있다.

접근 제어자와 관계없이 실행 시간에 메서드를 호출할 수도 있다.

클래스, 생성자 및 메서드에 대한 정보를 가져오고, 메서드 이름과 매개변수 유형을 알고 있다면 리플렉션을 사용하여 메서드를 호출할 수 있다.

이를 위해 getDeclaredMethod()와 invoke() 메서드를 사용한다.

리플렉션을 사용하면 클래스, 생성자 및 메서드에 대한 정보를 테이블 형식으로 얻을 수 있다. 메서드의 이름과 매개변수 유형을 알고 있다면 리플렉션을 사용하여 메서드를 호출할 수 있다.

  1. getDeclaredMethod()
  2. invoke()

 

리플렉션 설명을 읽으니까 이제서야 왜 내가 AOP를 쓸 때 (Error Handling 등)에 항상 class 를 지정해줘야 했는지 알게되었다

@ExceptionHandler(GlobalException.class)
    public ApiResponse<?> handleGlobalException(GlobalException globalException) {
        ErrorCode errorCode = globalException.getErrorCode();
        return new ApiResponse<>(errorCode.getHttpStatus(), errorCode.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<List<String>> handleMethodArgumentNotValidException(
        BindingResult bindingResult) {
        List<String> errors = bindingResult.getFieldErrors().stream()
            .map((FieldError fieldError) -> fieldError.getField() + " "
                + fieldError.getDefaultMessage())
            .toList();
        return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "입력값이 잘못되었습니다", errors);
    }