# AOP 관점지향 프로그래밍
스프링에서 AOP(Aspect Oriented Programming)는 복잡한 비즈니스 로직 속에서 중복되는 공통 기능을 분리하고 핵심 로직만 깔끔하게 유지하도록 돕는 중요한 개념이다.
특정 기능을 내가 바라보는 관점에서 모듈화 하여 한 곳에서 관리하는 방식으로 이해하면 접근하기 쉽다.
# AOP란?
AOP는 여러 모듈에 걸쳐 반복적으로 등장하는 공통 관심사를 따로 분리하여 핵심 로직에 영향을 주지 않고 한 곳에서 처리할 수 있게 해주는 기법이다.
💡API 진입 시 입력값 출력
💡메소드 실행 시간 측정
💡예외 발생 기록
💡사용자 인증/권한 체크
💡캐싱
❗이런 기능을 매번 직접 코드에 넣으면 중복이 생기고 유지보수가 어렵다.
👉AOP는 이런 공통 로직을 한 곳에서 처리해 준다
# AOP 핵심 Annotation 정리
| Annotation | 설명 |
| @Aspect | AOP를 정의하는 클래스 |
| @Pointcut | 공통 로직을 적용할 지점(메소드, 클래스 등)을 지정 |
| @Before | 대상 메소드 실행 전 |
| @After | 대상 메소드 실행 후 |
| @AfterReturning | 대상 메소드 성공적으로 종료 후(리턴 값 접근 가능) |
| @Around | 메소드 실행 전·후 모두 감싸서 처리 |
# Pointcut 설정 방식
Pointcut 표현식은 AOP 핵심 중 하나이며 어떤 메서드에 AOP를 적용할지 정하는 규칙이다.
execution(* com.example.aop_.controller..*.*(..))
controller 패키지 및 하위 패키지 전체
모든 클래스
모든 메소드
모든 파라미터
다음과 같이 특정 클래스만 지정할 수도 있다.
execution(* com.example.aop_.controller.Mapper.*(..))
특정 메서드만 지정하는 방식도 가능하다.
execution(String com.example.aop_.controller.Mapper.todo(String, int))
# RestController 실습 예제
@RequestMapping(path = "/apis")
@RestController
public class TestRestController {
@GetMapping(path = "/user/{id}")
public String findUser(@PathVariable String id,
@RequestParam String name) {
System.out.println("controller in findUser func()");
return "id="+id+", name="+name;
}
@PostMapping(path = "/user")
public ResponseEntity<UserDto> createUser(@RequestBody UserDto userDto) {
System.out.println("controller in createUser func()");
return ResponseEntity.ok(userDto);
}
}
UserDto
public class UserDto {
private String id;
private String pw;
private String email;
// getter/setter 생략
@Override
public String toString() {
return "UserDto{" +
"id='" + id + '\'' +
", pw='" + pw + '\'' +
", email='" + email + '\'' +
'}';
}
}
# 실무형 AOP – Logging
Logging AOP는 가장 많이 사용하는 기능 중 하나이다.
클래스명, 메서드명, 파라미터를 자동으로 출력하도록 구현할 수 있다.
@Aspect
@Component
public class LoggingAop {
@Pointcut("execution(* com.example.newbie.controller..*.*(..))")
private void cut(){}
@Before("cut()")
public void before(JoinPoint joinPoint) {
StringBuilder sb = new StringBuilder();
sb.append("class = ")
.append(joinPoint.getTarget().getClass().getName())
.append(", method = ")
.append(joinPoint.getSignature().getName())
.append("\n");
Arrays.stream(joinPoint.getArgs()).forEach(o -> {
sb.append("arg type = ").append(o.getClass().getName()).append(", ");
sb.append("arg value = ").append(o).append("\n");
});
System.out.println(sb.toString());
}
}
# AOP에서 파라미터 조작 + AfterReturning 처리
예를 들어 이메일 값을 인코딩했다가 다시 디코딩하는 로직을 AOP에서 처리할 수 있다.
@Before("cut2(userDto)")
public void before(JoinPoint joinPoint, UserDto userDto) {
userDto.setEmail(Base64.getEncoder().encodeToString(userDto.getEmail().getBytes()));
}
@AfterReturning(value = "cut2(userDto)", returning = "returnObj")
public Object afterReturn(JoinPoint joinPoint, Object returnObj, UserDto userDto) {
if (returnObj instanceof ResponseEntity response) {
var dto = (UserDto) response.getBody();
var decodeStr = new String(Base64.getDecoder().decode(dto.getEmail().getBytes()));
dto.setEmail(decodeStr);
}
return returnObj;
}
이러한 방식으로 AOP는 컨트롤러 로직을 전혀 손대지 않으면서도 데이터를 가공할 수 있다.
# @Timer – 실행 시간 측정 AOP
Annotation을 직접 만들어 시간 측정 기능을 제공할 수 있다.
1) Annotation 정의
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Timer {}
2) Timer AOP
@Aspect
@Component
public class TimerAop {
@Pointcut("@annotation(com.example.newbie.annotation.Timer)")
private void enableTimer(){}
@Around("enableTimer()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch sw = new StopWatch();
sw.start();
joinPoint.proceed();
sw.stop();
System.out.println("total time=" + sw.getTotalTimeSeconds());
}
}
3) Controller에서 사용
@Timer
@DeleteMapping("/user")
public void deleteUser(@RequestParam String id) throws InterruptedException {
Thread.sleep(2000);
}
# AOP 캐싱 – 동일 요청을 빠르게 처리
특정 메서드가 시간이 오래 걸린다면 AOP로 캐싱을 구현해 두면 성능을 크게 향상할 수 있다.
@Aspect
@Component
public class CashingAop {
private final Map<String, Object> map = new HashMap<>();
@Around("execution(* com.example.newbie.service.*.*(..))")
public Object cashingResult(ProceedingJoinPoint joinPoint) throws Throwable {
String key = generateCacheKey(joinPoint);
if (map.containsKey(key)) {
System.out.println("Returning cached key: "+key);
return map.get(key);
}
Object result = joinPoint.proceed();
map.put(key, result);
return result;
}
private String generateCacheKey(JoinPoint joinPoint) {
return joinPoint.getSignature().toString();
}
}
서비스 메서드
public String computeVal(String input) {
Thread.sleep(1000);
return "Compute value = " + input;
}
'spring' 카테고리의 다른 글
| JPA(=Java Persistance API) (0) | 2025.12.01 |
|---|---|
| Thymeleaf (0) | 2025.12.01 |
| Validation (0) | 2025.12.01 |
| Spring boot 기초 (0) | 2025.11.28 |
| Web? (0) | 2025.11.28 |
