Spring

Spirng AOP

99duuk 2024. 9. 3. 00:48

 


객체 지향 프로그래밍은 r-s-c 같은 수직적인 로직은 잘 분리하지만, 수평적인. 즉, 같은 단계의 로직 분리나, rsc에서 각 계층이나 여러 클래스에 걸쳐 중복되는 코드가 발생할 수 있음
 
이러한 oop의 해결책으로 aop가 등장함.
 
 
비즈니스 로직에서 공통 기능을 모듈화하여 코드 중복을 제거하여 핵심 비즈니스 로직에 집중할 수 있게 해줌.

예를 들어 rsc가 3단계에서 모두 동일한 로직을 수행하는 상황에서는
굳이 3단계 모두에서 중복되는 코드를 작성할 필요 없이 한 단계에만 해당 로직을 작성하면 되고,
rsc가 여러 계층이나 클래스에 걸쳐 중복되는 코드 같은 경우에는 공통 기능을 모듈화하여 코드 중복을 제거해 핵심 비즈니스 로직에 집중할 수 있음.


이러한 aop는
* 메서드 실행시간이나 파라미터, 반환 값 등을 로깅
* 메서드 실행 전 권한 체크 같은 보안
* 데이터베이스 트랜잭션 관리
* 특정 예외처리
* 성능 모니터링
등에 사용될 수 있고,

 

이렇게 aop를 사용하면 코드가 간결해지거나 재사용성이 높아지고 유지보수가 용이해질 수 있음

 
하지만 복잡성이 증가할 수 있고, 코드의 흐름이 명시적으로 눈에 보이지 않아 디버깅이 어려울 수 있음. 
특히 런타임에 동작하는 aop 경우 문제 발생 시 원인 파악이 복잡함
또한 Spring AOP 경우 메서드 실행 시점에만 적용 가능하고
잘못된 적용은 예상치 못한 동작 발생시키고 여러 Aspect가 상호작용하면 복잡성이 크게 증가할 수 있음 
 
 


 

 
 객체 지향의 가장 큰 장점은 모듈화 시키고 이를 재활용함으로써 코드의 중복을 줄이고, 재사용성을 높이는 것인데, 
프로그램이 커지다보면 모듈 내에도 중복되는 코드가 생김
그 부분을 수정하게 되면, 다른 클래스에 있는 같은 코드도 수정하게 됨. => 유지 보수 측면에서 아쉬움
이런 것을 횡단 관심사(Crosscutting-Concerns) 라고 함. 
횡단 관심사들은 모듈을 횡단하면서 존재하게 됨.
AOP의 목적은 이런 횡단 관심사를 모듈화하는 방법을 제시하는 것임

https://github.com/devSquad-study/2023-CS-Study/blob/main/Spring/spring_psa_ioc_aop_pojo.md

 
 


 

AOP는 절차지향이나 객체지향 프로그래밍처럼 그 자체로 하나의 프로그램을 형성할 수 있는 것은 아님
AOP는 객체지향 코드 위에서 이루어지며 객체지향을 보조하는 역할을 함
 
초기 프로그래밍은 절차지향이었음
절차지향은 매우 직관적임 
소스코드를 위에서 아래로 훑으면서 실행됨
 
규모가 더 커짐
거대한 프로그램을 개발하고 유지보수하기 위한 방법론 필요해짐 이때 OOP가 등장함.
OOP는 객체(Object)라는 혁신적인 개념을 활용함으로써 큰 프로그램을 모듈 단위로 축소시켜 작성할 수 있게 함으로써 이 위기를 극복함
프로그램을 모듈화 시키고 이를 재활용하면서 코드의 중복을 줄이고 재사용성을 높이는 것임
하지만 
이런 모듈 내에서조차 중복되는 코드가 생김
이를 횡단 관심사(Crosscutting-Concerns)라고 함. (트랜잭션, 로깅, 성능 분석)

횡단 관심사


이런 횡단 관심사는 여러 모듈을 말 그대로 횡단하면서 존재하게 됨
 
AOP는 이런 횡단 관심사를 모듈화하는 방법을 제시하는 것임
이를 통해 코드의 중복을 제거하고 유지보수를 편리하게 하는 것임

https://3months.tistory.com/74

 
 
 
 
 
 


 

 

객체 지향은 모듈화하고 재사용성을 높이는데

규모가 커지다보면 이런 모듈에서도 코드 중복이 발생함

이런 중복을 횡단 관심사라고 함

횡단 관심사는 트랜잭션 관리, 로깅(실행시간이나 파라미터), 성능 분석 등이 있음

 

aop는 이런 횡단 관심사를 모듈화해, 한 곳에서 관리함으로써

코드의 중복을 제거하고 유지보수를 편리하게 하는 것임

 
 
 


AOP는 R-S-C(Repository-Service-Controller) 전체 계층을 가로지르는 관심사뿐만 아니라, 특정 계층 내에서의 공통 관심사에도 적용될 수 있음
예를 들어 
 

package com.luckyvicky.woosan.global.aop;

// 필요한 클래스들을 import
import com.luckyvicky.woosan.global.config.distinct.DataSourceContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

// 이 클래스가 Aspect임을 나타내는 어노테이션
@Aspect
// 이 클래스를 Spring의 컴포넌트로 등록하여 bean으로 관리
@Component
public class DataSourceAspect {

    // 서비스 계층의 메소드 실행 전에 동작하며, SlaveDBRequest 어노테이션이 있는 경우에만 적용
    @Before("execution(* com.luckyvicky.woosan..*.service..*.*(..)) && @annotation(com.luckyvicky.woosan.global.annotation.SlaveDBRequest)")
    public void setReadDataSourceType() {
        // DataSourceContextHolder를 통해 현재 스레드의 데이터 소스를 slave로 설정
        DataSourceContextHolder.setDataSourceType("slaveDataSource");
    }

    // 서비스 계층의 메소드 실행 전에 동작하며, SlaveDBRequest 어노테이션이 없는 경우에 적용
    @Before("execution(* com.luckyvicky.woosan..*.service..*.*(..)) && !@annotation(com.luckyvicky.woosan.global.annotation.SlaveDBRequest)")
    public void setWriteDataSourceType() {
        // DataSourceContextHolder를 통해 현재 스레드의 데이터 소스를 master로 설정
        DataSourceContextHolder.setDataSourceType("masterDataSource");
    }
}
package com.luckyvicky.woosan.global.annotation;

// 필요한 어노테이션 관련 클래스들을 import
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 이 어노테이션을 메소드나 클래스에 적용할 수 있음을 지정
@Target({ElementType.METHOD, ElementType.TYPE})
// 이 어노테이션 정보가 런타임까지 유지됨을 지정
@Retention(RetentionPolicy.RUNTIME)
// SlaveDBRequest라는 이름의 커스텀 어노테이션을 정의
public @interface SlaveDBRequest {
    // 현재는 추가적인 속성 없이 마커 어노테이션으로 사용
}

 

처럼 데이터베이스 연결 선택이라는 횡단 관심사는 
여러 서비스 메서드에 공통적으로 적용되어야 하는 기능임
 
데이터베이스 선택 로직을 각 서비스 메서드에 직접 작성하지 않고,
별도의 Aspect로 분리함
이를 통해 서비스는 각자의 비즈니스 로직에만 집중함.
 
@SlaveDBRequest 어노테이션을 통해 어떤 데이터 소스를 사용할지 결정함.
이는 선언적 방식으로 db연결을 제어할 수 있게 해줌
 
이러한 AOP 설정으로 각 서비스 메서드마다 데이터 소스 선택 로직을 반복 작성할 필요가 없어짐
만약 데이터소스 선택 로직 변경이 필요하면 해당 Aspect만 수정하면 됨
 
∴ 데이터베이스 연결 선택이라는 횡단 관심사를 비즈니스 로직에서 분리하여 별도로 관리함으로써,
코드의 모듈성과 재사용성을 높임! 
 

 
 
 
※ 이처럼 aop는 Repository-Service-Controller 전체 계층을 가로지르는 관심사 뿐만 아니라,
특정 계층 내에서의 공통 관심사에도 적용될 수 있음
=> 횡단 관심사는 전체 어플리케이션을 관통할 수도 있고, 특정 계층이나 모듈에 국한될 수도 있음
 
+ 해당 상황에서는 별도의 AOP 의존성 추가 없이도AOP 
 
 
 
 


package com.test.util.annotation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HelloAOP {
}
package com.test.util.aop;

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

@Aspect
@Component
public class LoggingAspect {

    @Around("@annotation(com.test.util.annotation.HelloAOP)")
    public Object helloAOP(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("================================================================================================================================================");
        System.out.println("#### Hello AOP!!");
        String methodName = joinPoint.getSignature().getName();
        System.out.println("#### Executing method: " + methodName);
        long startTime = System.currentTimeMillis();

        Object result = joinPoint.proceed();

        long endTime = System.currentTimeMillis();

        System.out.println("#### Method " + methodName + " executed in " + (endTime - startTime) + "ms") ;

        System.out.println("================================================================================================================================================");
        return result;
    }
}

/**
 * <메서드 실행 전>
 * "Hello AOP!!"
 * 실행되는 메서드 이름 로깅
 * 메서드 실행 시작 시간 기록
 *
 * <메서드 실행 후>
 * 실행 종료 시간 기록
 * 메서드 실행 시간 계산하고 출력
 *
 * <결론>
 * ===> 로깅과 성능 측정이라는 횡단 관심사 모듈로 캡슐화함
 * ===> Pointcut : @HelloAOP 어노테이션 적용된 메서드에만 이 로직 적용
 * ===> 비즈니스 로직과 분리
 *
 */
더보기
 @Around("@annotation(com.portal.util.annotation.HelloAOP)")
    public Object helloAOP(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("================================================================================================================================================");
        System.out.println("#### Hello AOP!!");
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();

        // 1. 메서드 파라미터 로깅
        Object[] args = joinPoint.getArgs();
        System.out.printf("#### Method '%s' of class '%s' called with arguments: '%s'", methodName, className, Arrays.toString(args));
        System.out.println(" ");


        // 2. 실행 시간 로깅
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        System.out.println("#### Method " + methodName + " executed in " + (endTime - startTime) + "ms") ;


        // 3. 반환값 로깅 (필요한 경우)
        if (result != null) {
            System.out.printf("Method '%s' of class '%s' returned: '%s'", methodName, className, result.toString());
            System.out.println(" ");
        }

        System.out.println("================================================================================================================================================");
        return result;
    }

로 수정함

이렇게 만들어두고 
 
컨트롤러 호출되니까

#### dwkim-Hello Controller
================================================================================================================================================
#### Hello AOP!!
#### Executing method: selectTest
#### dwkim-Hello Service

... /* mapper-test-hello.xml selectTest */

	Query...
    Query...
    Query...
   	Query...
    ...
    
 {executed in 17 msec}
 #### Method selectTest executed in 34ms
================================================================================================================================================

라고 로그 찍히는 걸 확인했다..
 
 
 
여기서 "Hello AOP!!" 출력은 핵심 비즈니스 로직은 아니지만, 메서드 실행에 "횡단 관심사"로 추가된 기능이다.
 

Around라 실행 전에 로그 찍고 후에 로그 찍고 실행 전후 시간을 비교해서 실행 시간을 확인할 수 있음
 
Before이었다면 실행 후에 뭘 할 수 없으니 서비스 코드 실행 전에 로그 찍고 시작 시간만 확인할 수 있고, After이라면 실행 전엔 뭘 할 수 없으니 서비스 코드 실행 후에 로그 찍고 종료 시간만 확인할 수 있는겨
 

  •  @Around:
    • 메서드 실행 전과 후 모두에 접근할 수 있습니다.
    • 실행 시간 측정, 전후 로깅, 예외 처리 등 가장 유연한 처리가 가능합니다.
    • ProceedingJoinPoint.proceed()를 통해 원본 메서드의 실행을 제어할 수 있습니다.
  • @Before:
    • 메서드 실행 전에만 동작합니다.
    • 시작 로그, 파라미터 검증, 시작 시간 기록 등에 사용할 수 있습니다.
    • 하지만 메서드 실행 후의 정보(실행 시간, 반환값 등)에는 접근할 수 없습니다.
  • @After:
    • 메서드 실행 후에 동작합니다 (정상 종료와 예외 발생 모두 포함).
    • 종료 로그, 리소스 정리 등에 사용할 수 있습니다.
    • 메서드 실행 전의 정보나 반환값에는 접근할 수 없습니다.
  • @AfterReturning:
    • 메서드가 정상적으로 반환된 후에 동작합니다.
    • 반환값을 확인하거나 수정할 수 있습니다.
    • 하지만 실행 전 정보나 실행 시간은 알 수 없습니다.
  • @AfterThrowing:
    • 메서드 실행 중 예외가 발생했을 때 동작합니다.
    • 예외 로깅, 추가적인 예외 처리 등에 사용할 수 있습니다.

 


 


[ ProceedingJoinPoint와 JoinPoint 의 차이 ]

 

  • ProceedingJoinPoint: @Around 어드바이스에서만 사용 가능
  • JoinPoint: @Before, @After, @AfterReturning, @AfterThrowing 어드바이스에서 사용 가능

 
 

  • ProceedingJoinPoint: proceed() 메소드를 통해 원본 메소드의 실행을 제어할 수 있음
  • JoinPoint: 원본 메소드의 실행을 제어할 수 없음

 
 

  • ProceedingJoinPoint: 더 유연한 처리 가능 (예: 조건에 따라 메소드 실행 여부 결정)
  • JoinPoint: 메소드 실행 전후 정보에만 접근 가능
더보기

ProceedingJoinPoint를 사용하면 원본 메소드의 실행 전, 실행 중, 실행 후의 모든 단계에서 세밀한 제어가 가능

로깅, 보안 체크, 성능 모니터링, 트랜잭션 관리 등 다양한 횡단 관심사 구현에서 매우 유용

 

만약 트랜잭션 처리가 엉망인 어떤 메서드가 있다면, ProceedingJoinPoint를 적절히 사용했을 때, 트랜잭션의 순서를 컨트롤할 수 있다거나 뭐...

 
 


만약
로그인 정보 같은 공통 파라미터 같은 걸
각 서비스에서 전부 다 필요로한다~?

그럼 각 서비스 코드마다 추가해줄 필요 없이 
aop 하나 만들어줘서 어노테이션만 달아주면 싹싹김치~로 파라미터 뽑아먹을 수 있다~~~~ 짜잔~