AOP (Aspect-Oriented Programming)는 프로그래밍 패러다임의 하나로, 관심사를 분리하여 코드의 모듈화를 개선하기 위한 기법입니다.
이를 통해 코드의 가독성과 유지 보수성을 높일 수 있습니다.
쉽게 말하면 스프링부트에서는 DispatcherServlet을 우리가 제어할 수 없기 때문에 리플렉션하기 위해 AOP를제공해준다.
리플렉션을 통해 우리는 깃발(어노테이션)을 원하는 위치에 설정하고, 그 깃발이 있는 코드가 실행될 때 주로 핵심 비즈니스 로직과는 관계없는 공통 기능(로그 기록, 트랜잭션 관리, 보안 등)을 별도로 분리하여 코드를 간결하고 유지보수하기 쉽게 만들어줍니다. )을 별도로 분리하여 코드가 간결하다.
AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)에 대해 쉽게 설명해 드리겠습니다.
프로그래밍을 할 때, 우리는 다양한 기능들을 개발하고, 이 기능들이 서로 상호작용합니다. 이때, 특정 기능이나 관심사(예: 로깅, 보안 검사, 트랜잭션 관리 등)가 여러 모듈에 걸쳐 필요할 수 있습니다. 이러한 관심사를 전통적인 OOP(객체 지향 프로그래밍)로 처리하려 하면, 같은 코드를 여러 곳에 반복해서 작성해야 할 수 있고, 이는 코드의 중복, 유지보수의 어려움, 가독성 저하 등의 문제를 야기할 수 있습니다.
AOP는 이러한 문제를 해결하기 위해 나온 프로그래밍 패러다임입니다. AOP는 특정한 "관심사"를 모듈화하는 것을 목표로 하며, 이렇게 모듈화된 관심사를 "Aspect(관점)"라고 합니다. 그리고 이 관점을 애플리케이션의 적절한 "조인 포인트(Join points, 프로그램 실행 중 특정 지점)"에 적용하여, 원래의 코드를 변경하지 않고도 원하는 기능을 추가하거나 변경할 수 있게 해줍니다.
예를 들어, 로깅 기능을 애플리케이션의 여러 부분에 적용하고 싶다고 가정해 봅시다. AOP를 사용하면, 로깅을 위한 코드를 따로 작성("Aspect"로 정의)하고, 이 코드가 애플리케이션의 어느 부분에서 실행될지를 지정할 수 있습니다. 이렇게 하면, 실제 비즈니스 로직 코드에는 영향을 주지 않으면서도 로깅 기능을 쉽게 추가하거나 변경할 수 있게 됩니다.
AOP를 통해, 코드의 중복을 줄이고, 가독성과 유지 보수성을 높일 수 있으며, 애플리케이션의 여러 부분에 걸쳐 있는 관심사를 효율적으로 관리할 수 있게 됩니다.
Aspect 생성
로깅 기능(Advice)을 포함하는 Aspect를 정의합니다. (클래스 생성)
Advice 정의
메서드 실행 전후에 로그를 남기는 코드를 작성합니다. (수행할 메서드 생성)
Pointcut 설정
로깅 기능을 적용할 메서드를 결정합니다. 예를 들어, 모든 public 메서드에 로깅을 적용
하려면 해당하는 패턴을 Pointcut에 지정합니다. (깃발에 별칭주기)
Advice 적용
설정한 Pointcut에 따라 메서드 실행 전후에 로그를 남기는 Advice가 실행됩니다.
(PointCut(별칭) 적용)
AOP 적용 방법 3가지
첫번째 방법
깃발(어노테이션)을 만들고
그 깃발을 PointCut(별칭)으로 등록 한다.
Advice를 만든다. (메서드 행위)
PointCut을 Advice에 적용한다.
깃발만들기
package shop.mtcoding.aopstudy.config.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 Hello {
}
handler
package shop.mtcoding.aopstudy.config.advice;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class HelloAdvice {
// 깃발에 별칭주기
@Pointcut("@annotation(shop.mtcoding.aopstudy.config.annotation.Hello)")
public void hello(){}
// 매개변수에 접근해서 값을 찾는 것을 가능 - 값을 주입하려면 @Around 사용해야함
@Before("hello()")
public void helloAdvice(JoinPoint jp) throws Throwable {
Object[] args = jp.getArgs();
for (Object arg : args) {
if(arg instanceof String){
String username = (String) arg;
System.out.println(username+"님 안녕");
}
}
}
}
컨트롤러
package shop.mtcoding.aopstudy.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import shop.mtcoding.aopstudy.config.annotation.Hello;
@RestController
public class HelloController {
@GetMapping("/hello/v1")
public String v1(){
return "v1";
}
// http://localhost:8080/hello/v2?username=ssar
@Hello
@GetMapping("/hello/v2")
public String v2(String username){
System.out.println("username : 값 변경? : "+username);
return "v2";
}
}


두번째 방법
이미 만들어져 있는 깃발을 PointCut(별칭)으로 등록 한다.
Advice를 만든다. (메서드 행위)
PointCut을 Advice에 적용한다
advice
package shop.mtcoding.aopstudy.config.advice;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import shop.mtcoding.aopstudy.config.exception.MyValidationException;
import java.util.HashMap;
import java.util.Map;
// Aspect로 동작하기 위한 클래스임을 선언하는 어노테이션입니다.
@Aspect
// Spring의 컴포넌트로 등록되도록 하는 어노테이션입니다.
@Component
public class ValidAdvice {
// Pointcut을 선언하는 메소드입니다. @PostMapping 어노테이션이 지정된 메소드를 대상으로 합니다.
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMapping() {
}
// Pointcut을 선언하는 메소드입니다. @PutMapping 어노테이션이 지정된 메소드를 대상으로 합니다.
@Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
public void putMapping() {
}
// @PostMapping 또는 @PutMapping 어노테이션이 지정된 메소드가 실행되기 전에 실행되는 메소드입니다.
// 메소드의 매개변수 중에 BindingResult가 있는 경우, 해당 객체에 에러가 있는지 검사합니다.
// 에러가 있으면 에러 정보를 Map에 담아 MyValidationException을 던집니다.
@Before("postMapping() || putMapping()")
public void validationAdvice(JoinPoint jp) throws Throwable {
Object[] args = jp.getArgs();
for (Object arg : args) {
if (arg instanceof BindingResult) {
BindingResult bindingResult = (BindingResult) arg;
if (bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
// BindingResult에 포함된 모든 FieldError를 순회하면서 에러 정보를 Map에 담습니다.
for (FieldError error : bindingResult.getFieldErrors()) {
errorMap.put(error.getField(), error.getDefaultMessage());
}
// 발생한 에러 정보를 담은 Map과 함께 MyValidationException을 throw 합니다.
throw new MyValidationException(errorMap);
}
}
}
}
저게 결국에는 서버 실행시 포스트 매핑과 풋 매핑에 관한 매개변수에 대한 값을 파싱해서 맵에 던져주고 가공해서 MyValidationException의 위임한다는 거다.
컨트롤러
@PostMapping("/valid")
public String join(@Valid JoinInDto joinInDto, BindingResult bindingResult){
return "ok";
}
JoinDTO
package shop.mtcoding.aopstudy.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Getter @Setter
public class JoinInDto {
@NotNull //NULL 이면 안된다/
private String username;
@NotEmpty // 값이 비어 있으면 안된다.
private String password;
@NotEmpty // 값이 비어 있으면 안된다.
@Size(min = 4, max = 10) //4자 이상 10자 이하
private String email;
}
MyValidationException
package shop.mtcoding.aopstudy.config.exception;
import lombok.Getter;
import java.util.Map;
@Getter
public class MyValidationException extends RuntimeException {
private Map<String, String> erroMap;
public MyValidationException(Map<String, String> erroMap) {
this.erroMap = erroMap;
}
}

세번째 방법
깃발을 생성하지 않고, 특정한 패턴이 수행될 때 정의된 advice를 실행하게 할 수도 있다.
Spring Framework에서는 XML을 이용하여 Aspect-Oriented Programming (AOP)을 설정하는 방법이 있습니다.
그러나 Spring Boot에서는 주로 어노테이션 기반의 AOP를 사용하며, execution expression을 이용하여 pointcut을 등록할 수 있습니다.
하지만, 직접적으로 execution expression을 이용하여 pointcut을 등록하려면 여러가지 설정을 해주어야 합니다.
대신, Spring Boot에서는 AspectJ를 사용하여 간편하게 pointcut을 등록할 수 있는 기능을 제공합니다.
AspectJ는 Java와 비슷한 문법을 가지며, execution expression을 이용하여 pointcut을 등록하는 방법이 있습니다.
execution expression(메서드 Pointcut 지점 설정 시 사용 때)
// 이 포인트컷은 AspectJ의 포인트컷 표현식을 사용하여 메서드 실행 시점을 지정합니다.
// execution 키워드는 메서드 실행을 나타내며, 이를 통해 특정 메서드를 대상으로 포인트컷을 정의할 수 있습니다.
// 여기서 *는 모든 리턴 타입을 나타냅니다.
// com.example.service.MyService는 패키지명과 클래스명을 지정하며, MyService 클래스를 가리킵니다.
// save(..)는 메서드명과 메서드 시그니처를 나타내며, 이는 save 메서드를 가리킵니다.
// 괄호 안에(메서드의 매개변수 타입이 들어가야됨) ..을 사용하여 모든 매개변수 타입을 허용하는 것을 의미합니다.
@Pointcut("execution(* com.example.service.MyService.save(..))")
public void saveMethod() {}
메서드 매개변수가 여러개 일 때
@Pointcut("execution(* com.example.service.MyService.save(java.lang.String, java.lang.Integer))")
public void saveMethodWithStringAndIntegerParams() {}
execution expression(컨트롤러 Pointcut 지점 설정 시 사용 때)
@Pointcut("execution(* com.example.controller.UserController.*(..))")
public void userControllerMethods() {}
포인트 컷 표현식 문법


Share article