AOP VS OOP
객체 지향 프로그래밍(OOP)
OOP의 주요 개념:
클래스(Class) : 객체의 설계도. 속성과 메서드를 정의하는 틀입니다.
객체(Object) : 클래스를 통해 생성된 실체로, 실제로 동작하는 프로그램의 단위입니다.
상속(Inheritance) : 기존 클래스를 확장하여 새로운 클래스를 만드는 기능입니다.
다형성(Polymorphism) : 동일한 이름의 메서드가 다른 동작을 수행할 수 있게 하는 기능입니다.
캡슐화(Encapsulation) : 객체의 속성과 메서드를 외부에서 직접 접근하지 못하도록 숨기는 기능입니다.
추상화(Abstraction) : 복잡한 시스템을 단순화하여 핵심 기능만 노출하는 기능입니다.
OOP의 장점:
코드 재사용 : 상속과 다형성 등을 통해 코드의 재사용이 용이합니다.
모듈화 : 각 클래스가 독립적인 역할을 가지며, 모듈 단위로 개발과 테스트가 가능합니다.
유지보수성 : 캡슐화 덕분에 내부 구현을 변경하더라도 외부에 미치는 영향이 적습니다.
OOP의 한계:
횡단 관심사(Cross-Cutting Concerns) 처리의 어려움 : 로깅, 보안, 트랜잭션 관리 등과 같은 기능은 여러 클래스나 모듈에 걸쳐 사용되며, 이를 처리하기 위해서는 중복된 코드가 발생할 수 있습니다. 이는 코드의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다.
관점 지향 프로그래밍(AOP)
AOP의 주요 개념:
애스펙트(Aspect) : 횡단 관심사를 모듈화한 것. 예를 들어, 로깅 기능을 애스펙트로 정의하여 여러 클래스에서 공통으로 사용할 수 있습니다.
조인 포인트(Join Point) : 애스펙트가 적용될 수 있는 실행 지점. 메서드 호출, 예외 발생 등이 이에 해당합니다.
포인트컷(Pointcut) : 조인 포인트 중에서 실제로 애스펙트를 적용할 지점을 정의한 것.
어드바이스(Advice) : 애스펙트에서 실행되는 실제 코드. 메서드 실행 전후 또는 예외 발생 시에 동작할 수 있습니다.
위빙(Weaving) : 애스펙트를 실제 코드에 적용하는 과정입니다. 이 과정은 컴파일 타임, 로드 타임, 또는 런타임에 이루어질 수 있습니다.
AOP의 장점:
횡단 관심사의 모듈화: 로깅, 보안, 트랜잭션 관리 등과 같은 공통 기능을 하나의 애스펙트로 정의하여 여러 클래스에 쉽게 적용할 수 있습니다.
코드 간소화 : 핵심 비즈니스 로직에서 공통 관심사를 분리하여 코드를 간결하게 유지할 수 있습니다.
유지보수성 : 공통 관심사를 애스펙트로 모듈화하면, 이를 관리하고 수정하는 것이 훨씬 쉬워집니다.
AOP의 한계:
복잡성 증가 : AOP는 기존 OOP와 함께 사용되므로, 시스템의 복잡성을 증가시킬 수 있습니다.디버깅의 어려움: 애스펙트가 적용된 코드의 흐름을 추적하기 어려워 디버깅이 복잡해질 수 있습니다.
언제 OOP와 AOP를 사용해야 할까?
OOP는 애플리케이션의 주요 비즈니스 로직과 구조를 정의하는 데 사용됩니다. 클래스와 객체를 통해 시스템의 기본 동작을 모델링하고 설계하는 데 적합합니다.
AOP는 여러 모듈에 걸쳐 있는 공통 기능(로깅, 보안, 트랜잭션 관리 등)을 효율적으로 처리해야 할 때 사용됩니다.
OOP로 해결하기 어려운 횡단 관심사를 분리하여 코드의 복잡성을 줄이고 유지보수성을 높이는 데 도움을 줍니다.
OOP와 AOP의 비교
목적 | 객체를 중심으로 시스템을 모델링 | 횡단 관심사를 분리하여 모듈화 |
핵심 개념 | 클래스, 객체, 상속, 다형성, 캡슐화 | 애스펙트, 조인 포인트, 포인트컷, 어드바이스, 위빙 |
주요 사용 사례 | 비즈니스 로직 구현 | 로깅, 보안, 트랜잭션 관리, 예외 처리 |
코드 재사용 | 상속과 다형성을 통해 코드 재사용 | 횡단 관심사를 애스펙트로 모듈화하여 코드 재사용 |
유지보수성 | 객체 단위로 코드가 분리되어 있어 유지보수성이 좋음 | 공통 기능이 모듈화되어 있어 횡단 관심사의 유지보수가 용이 |
적용 방식 | 비즈니스 로직의 구현에 직접 사용 | 기존 코드에 애스펙트를 추가하여 적용 |
AOP Annotaion
1. @Aspect
설명: @Aspect 애노테이션은 이 클래스가 "애스펙트(Aspect)"임을 나타냅니다. 애스펙트는 횡단 관심사(cross-cutting concerns), 즉 여러 클래스나 모듈에서 공통적으로 사용되는 기능(예: 로깅, 보안, 트랜잭션 관리 등)을 모듈화한 것입니다.
사용법: 이 애노테이션을 사용함으로써 Spring AOP가 이 클래스를 애스펙트로 인식하고, 지정된 포인트컷(Pointcut)과 어드바이스(Advice)에 따라 동작하게 됩니다.
2. @Component
설명: @Component 애노테이션은 이 클래스가 Spring의 컴포넌트 스캔에 의해 자동으로 빈(bean)으로 등록되도록 합니다. 이 클래스를 애스펙트로 사용하려면 빈으로 등록되어야 하므로 필수적으로 사용됩니다.
사용법: 이 애노테이션 덕분에 Spring은 이 클래스를 애플리케이션 컨텍스트에 등록하고 관리할 수 있습니다.
3. @Before
설명: @Before 애노테이션은 특정 메서드가 호출되기 전에 실행되는 어드바이스를 정의합니다.
사용법: 이 애노테이션을 사용한 logBefore 메서드는 com.msaProjectMenu01.menu.controller 패키지 내의 모든 메서드가 실행되기 전에 동작합니다. 포인트컷 표현식 "execution(* com.msaProjectMenu01.menu.controller.*.*(..))"에 의해 어떤 메서드가 대상이 되는지 정의됩니다.
4. @After
설명: @After 애노테이션은 특정 메서드가 실행된 후에 실행되는 어드바이스를 정의합니다.
사용법: 이 애노테이션을 사용한 logAfter 메서드는 com.msaProjectMenu01.menu.controller 패키지 내의 모든 메서드가 실행된 후에 동작합니다.
5. @AfterReturning
설명: @AfterReturning 애노테이션은 메서드가 정상적으로 실행된 후, 즉 예외가 발생하지 않고 반환된 후에 실행되는 어드바이스를 정의합니다.
사용법: 이 애노테이션을 사용한 logAfterReturning 메서드는 지정된 메서드가 반환하는 결과를 로깅합니다. returning 속성을 통해 반환된 값을 받을 변수명을 지정할 수 있습니다.
6. @AfterThrowing
설명: @AfterThrowing 애노테이션은 메서드 실행 중 예외가 발생했을 때 실행되는 어드바이스를 정의합니다.
사용법: 이 애노테이션을 사용한 logAfterThrowing 메서드는 메서드 실행 중 발생한 예외를 로깅합니다. throwing 속성을 통해 발생한 예외 객체를 받을 변수명을 지정할 수 있습니다.
7. @Around
설명: @Around 애노테이션은 메서드 실행 전후 또는 예외 발생 시에 실행되는 어드바이스를 정의합니다. 이 어드바이스는 가장 유연하며, 메서드 실행 전과 후의 작업을 모두 처리할 수 있습니다.
사용법: logAround 메서드는 메서드 실행 전과 후에 각각 로깅을 하고, 메서드를 실제로 실행하기 위해 joinPoint.proceed()를 호출합니다.
logExecutionTime 메서드는 메서드 실행 시간을 측정하고 로깅하는 역할을 합니다. 이 메서드 역시 joinPoint.proceed()를 통해 메서드를 실행한 후, 소요 시간을 계산합니다.
@Around 어드바이스는 메서드의 반환 값을 조작할 수도 있으며, 필요에 따라 메서드의 실행을 제어할 수 있습니다.
8. 포인트컷 표현식
포인트컷 표현식: "execution(* com.msaProjectMenu01.menu.controller.*.*(..))"와 같은 표현식은 AOP에서 메서드의 실행 지점을 지정합니다. execution(* 패키지.클래스.메서드(..)) 형식을 사용하여 특정 메서드나 클래스에서 어드바이스를 적용할 지점을 선택할 수 있습니다.
EX)
- "execution(* com.msaProjectMenu01.menu.controller.*.*(..))": 이 표현식은 com.msaProjectMenu01.menu.controller 패키지 내의 모든 클래스의 모든 메서드에 어드바이스를 적용한다는 의미입니다.
- "execution(* com.menu.service..*(..))": 이 표현식은 com.menu.service 패키지 및 하위 패키지 내의 모든 메서드에 어드바이스를 적용합니다.
AOP 적용방법
저는 Spring Boot 3.3.3버전과, Gradle을 이용하였습니다.
1) 의존성 주입
implementation 'org.springframework.boot:spring-boot-starter-aop'
2) 패키지 구성
Spring Boot의 경우 application 디렉토리 포함한 경로가 Component Scan 경로이기에 위와 같이 해야만 been 객체로 컨테이너로 올라가 사용할수가 있습니다.
저의 경우 MsaProjectMenu01Application.java 같은 경로 core/aop 폴더안에 LoggingAspect.java, TransactionAspect.java 파일 두개를 생성하였습니다.
3) LoggingAspect
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
/**
* Before: 대상 “메서드”가 실행되기 전에 Advice를 실행합니다.
*
* @param joinPoint
*/
@Before("execution(* com.msaProjectMenu01.menu.controller.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
logger.info("Before: " + joinPoint.getSignature().getName());
}
/**
* After : 대상 “메서드”가 실행된 후에 Advice를 실행합니다.
*
* @param joinPoint
*/
@After("execution(* com.msaProjectMenu01.menu.controller.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
logger.info("After: " + joinPoint.getSignature().getName());
}
/**
* AfterReturning: 대상 “메서드”가 정상적으로 실행되고 반환된 후에 Advice를 실행합니다.
*
* @param joinPoint
* @param result
*/
@AfterReturning(pointcut = "execution(* com.msaProjectMenu01.menu.controller.*.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("AfterReturning: " + joinPoint.getSignature().getName() + " result: " + result);
}
/**
* AfterThrowing: 대상 “메서드에서 예외가 발생”했을 때 Advice를 실행합니다.
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "execution(* com.msaProjectMenu01.menu.controller.*.*(..))", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
logger.info("예외 발생: " + joinPoint.getSignature().getName() + " exception: " + e.getMessage());
}
/**
* Around : 대상 “메서드” 실행 전, 후 또는 예외 발생 시에 Advice를 실행합니다.
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("execution(* com.msaProjectMenu01.menu.controller.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
logger.info("실행 메소드: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
logger.info("실행 후 메소드: " + joinPoint.getSignature().getName());
return result;
}
/**
* Around advice to calculate the execution time of methods in the specified packages.
*
* @param joinPoint
* @return Object (result of the method execution)
* @throws Throwable
*/
@Around("execution(* com.msaProjectMenu01.menu.controller..*(..)) || execution(* com.menu.service..*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object proceed = joinPoint.proceed(); // Execute the method
long executionTime = System.currentTimeMillis() - startTime;
logger.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
return proceed;
}
}
4) TransactionAspect
@Aspect
@Component
public class TransactionAspect {
private static final Logger logger = LoggerFactory.getLogger(TransactionAspect.class);
private final TransactionTemplate transactionTemplate;
public TransactionAspect(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
@Around("execution(* com.msaProjectMenu01.menu.service..*(..))") // 서비스 계층의 모든 메서드에 적용
public Object applyTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
return transactionTemplate.execute(status -> {
try {
logger.info("Transaction started for method: {}", joinPoint.getSignature());
Object result = joinPoint.proceed(); // 메서드 실행
logger.info("Transaction successful for method: {}", joinPoint.getSignature());
return result;
} catch (Throwable throwable) {
logger.error("Transaction failed for method: {}", joinPoint.getSignature(), throwable);
status.setRollbackOnly();
throw new RuntimeException(throwable);
}
});
}
@Before("execution(* com.msaProjectMenu01.menu.service..*(..))")
public void beforeTransaction() {
logger.info("Transaction started");
}
}
5) JoinPoint 메서드
메서드 | 설명 |
getArgs() | 대상 메서드의 인자 목록을 반환합니다. |
getSignature() | 대상 메서드의 정보를 반환합니다. |
getSourceLocation() | 대상 메서드가 선언된 위치를 반환합니다. |
getKind() | Advice의 종류를 반환합니다. |
getStaticPart() | Advice가 실행될 JoinPoint의 정적 정보를 반환합니다. |
getThis() | 대상 객체를 반환합니다. |
getTarget() | 대상 객체를 반환합니다. |
toString() | JoinPoint의 정보를 문자열로 반환합니다. |
toShortString() | JoinPoint의 간단한 정보를 문자열로 반환합니다. |
toLongString() | JoinPoint의 자세한 정보를 문자열로 반환합니다. |
실행
9000/api/board api를 호출
아래 이미지를 보면 로그와 트랜잭션이 정상적으로 사용되는걸 볼 수 있다.
'WEB > Spring' 카테고리의 다른 글
Spring Cloud Config Server 설정하기1(Spring Cloud Config) (0) | 2024.09.12 |
---|---|
Spring Boot JWT(JSON Web Token) 설정하기 (1) | 2024.09.05 |
Spring Boot로 MSA 개발하기 (5) | 2024.08.30 |
스프링 IOC컨테이너 - DI, DL (0) | 2024.03.23 |
스프링에서 Service ServiceImpl 사용하는 이유 (0) | 2024.03.23 |