객체 지향 프로그래밍은 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 하나 만들어줘서 어노테이션만 달아주면 싹싹김치~로 파라미터 뽑아먹을 수 있다~~~~ 짜잔~
'Spring' 카테고리의 다른 글
ServletResponse.isComitted(), FilterChain (with GPT) (2) | 2024.12.19 |
---|---|
(지피티 선생님의) 람다와 일급 객체, FP 특강 (0) | 2024.12.12 |
WebFlux 첨들어봄 (1) | 2024.12.10 |
Spring MVC 설정 파일 이해하기 : web.xml & dispatcher-servlet.xml (1) | 2024.09.03 |
ModelAndView vs @ResponseBody (0) | 2024.09.02 |