AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 공통 관심 사항(Cross-Cutting Concerns)을 모듈화하여 핵심 비즈니스 로직과 분리할 수 있게 해주는 프로그래밍 패러다임을 뜻한다.
애플리케이션에서는 로깅, 트랜잭션 처리, 예외 처리등과 같은 공통 기능들이 자주 등장한다. 이러한 공통 기능들을 모든 서비스 로직 안에 반복해서 작성하면 코드 중복이 증가하고, 유지보수가 어려워지며 어떤게 핵심 로직인지 단번에 알기 어렵다.
예를 들어, 메서드 시작 시간과 메서드 종료 시간을 체크해서 걸린 시간을 출력하는 기능을 메서드에 넣으려면
/** 글 등록 **/
@Transactional
public BoardDTO createBoard(BoardDTO boardDTO) {
long start = System.currentTimeMillis(); //메서드 시작 시간
System.out.println("글 작성 메서드 시작");
// userId(PK)를 이용해서 User 조회
if (boardDTO.getUser_id() == null)
throw new IllegalArgumentException("userId(PK)가 필요합니다!");
User user = userRepository.findById(boardDTO.getUser_id())
.orElseThrow(() -> new IllegalArgumentException("작성자 정보가 올바르지 않습니다."));
Board board = new Board();
board.setTitle(boardDTO.getTitle());
board.setContent(boardDTO.getContent());
board.setUser(user);
Board saved = boardRepository.save(board);
long end = System.currentTimeMillis();
log.info("글 작성 완료 시간 = " + (end-start));
return toDTO(saved);
}
/** 게시글 수정 **/
@Transactional
public BoardDTO updateBoard(Long boardId, BoardDTO dto) {
long start = System.currentTimeMillis(); //메서드 시작 시간
System.out.println("글 수정 메서드 시작");
Board board = boardRepository.findById(boardId)
.orElseThrow(() -> new IllegalArgumentException("게시글 없음: " + boardId));
board.setTitle(dto.getTitle());
board.setContent(dto.getContent());
boardRepository.save(board);
long end = System.currentTimeMillis();
log.info("글 수정 완료 시간 = " + (end-start));
return toDTO(board);
}
위의 코드와 같이 모든 메서드에 메서드 시작 시간과 종료시간에 관련된 로직을 작성해야한다. 지금은 2개의 메서드에만 적용했지만, 이런 메서드가 수백, 수천개라면 어떻게 할 것인가? 수백 수천개의 메서드에 저 코드들을 다 넣는다고 생각하면 눈앞이 깜깜해진다.
이럴 때 쓰는것이 AOP이다.
AOP를 적용하면 아래 코드와 같이 공통 기능이 담긴 클래스를 작성한 후 AOP 관련 어노테이션과 함께 공통 기능 로직을 작성해주면 공통 기능을 따로 메서드 안에 작성하지 않아도 공통 기능 로직이 처리가 된다.
@Slf4j
@Component
@Aspect //공통으로 관리하고 싶은 기능을 담당하는 클래스에 붙이는 어노테이션
public class LogAspect {
@Pointcut("execution(* org.example.backendproject.board.service..*(..)) || execution(* org.example.backendproject.Auth.service..*(..))")
public void method(){}
//AOP를 적용할 클래스
@Around("execution(* org.example.backendproject.board.service..*(..)) || execution(* org.example.backendproject.Auth.service..*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
log.info("[AOP_LOG] {} 메서드 호출 시작 ", methodName);
Object result = joinPoint.proceed();
return result;
} catch (Exception e) {
log.error("[AOP_LOG] {} 메서드 예외 {}", methodName, e.getMessage());
return e;
} finally {
long end = System.currentTimeMillis();
log.info("[AOP_LOG] {} 메서드 실행 완료 시간 = {}", methodName, end - start);
}
}
}
이제부터 이런 문제점을 해결해주는 AOP에 대해 자세히 알아보도록 하자.
AOP의 핵심 구성 요소는 아래와 같이 총 다섯가지로 나뉘어진다.
| 구성 요소 | 설명 | 예시 |
|---|---|---|
@Aspect | 이 클래스가 Aspect임을 정의 | @Aspect public class LoggingAspect { ... } |
Advice | 공통 기능(로직)이 실행되는 시점 | @Before, @After, @Around, @AfterThrowing, @AfterReturning |
JoinPoint | Advice가 적용 가능한 지점 (메소드 실행 등) | 메소드 실행 전/후 |
Pointcut | JoinPoint를 선별하는 조건 | execution(* com.example.service..*(..)) |
Weaving | Aspect를 실제 객체에 적용하는 작업 | Spring은 런타임에 프록시 방식으로 적용 |
공통 기능(로깅, 보안 검사, 트랙잭션 등)을 모듈화한 클래스를 의미.
@Aspect어노테이션을 사용한다.
@Aspect //공통 기능을 정의하는 클래스임을 알려줌
@Component
public class LoggingAspect {
// 공통 기능들 정의됨
}
Aspect 안에 있는 실제 로직. 언제, 어떤 방식으로 실행할 지 정의함
| 종류 | 설명 | 사용 예시 |
|---|---|---|
@Before | 메서드 실행 전 수행 | 로그 출력, 인증 검사 |
@AfterReturning | 메서드 성공적으로 리턴 후 수행 | 리턴값 로그 |
@AfterThrowing | 메서드 예외 발생 시 수행 | 예외 로깅 |
@After | 메서드 종료 후 (정상/예외 관계 없이) | 리소스 정리 |
@Around | 실행 전후를 모두 감쌈 | 성능 측정, 커스텀 인증, 트랜잭션 |
@Before("execution(* com.example.service..*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("▶️ " + joinPoint.getSignature());
}
Advice가 적용될 수 있는 지점 중 하나로, Spring AOP는 기본적으로 메서드 실행만 지원한다.
getSignature())getArgs())getTarget())public void logBefore(JoinPoint joinPoint) {
System.out.println("메서드 이름: " + joinPoint.getSignature().getName());
System.out.println("인자들: " + Arrays.toString(joinPoint.getArgs()));
}
Advice를 어떤 메서드에 적용할 지 지정하는 표현식 -> 즉, 어디에 적용할 것인가를 필터링 하는 역할
// com.example.service 패키지 이하 모든 클래스의 모든 메서드
@Pointcut("execution(* com.example.service..*(..))")
| 표현식 | 의미 |
|---|---|
* | 모든 반환 타입 |
.. | 하위 패키지 또는 모든 파라미터 |
execution(...) | 포인트컷 지정 문법 |
Aspect를 어디에, 언제, 어떻게 적용할지 결정하고 실제로 반영하는 작업 ->Spring에서는 런타임에 프록시 객체를 만들어서 위빙을 수행함
| 방식 | 설명 |
|---|---|
| 프록시 기반 (Spring AOP) | 클래스 또는 인터페이스를 감싼 프록시를 런타임에 생성 |
| 바이트코드 조작 (AspectJ) | 컴파일 타임 또는 로드 타임에 바이트코드 변경 |
com.example.service 패키지 아래의 모든 서비스 메소드 실행 전/후에 로그를 남기고 싶다면?
@Slf4j
@Aspect //공통 로직을 가진 클래스임을 명시
@Component
public class LoggingAspect {
// 포인트컷: 서비스 패키지 내 모든 메서드 지정
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}
// 메서드 실행 전 Advice
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
log.info("▶️ 메서드 시작: {}", joinPoint.getSignature());
}
// 메서드 정상 종료 후 Advice
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
log.info("✅ 메서드 종료: {}, 반환값: {}", joinPoint.getSignature(), result);
}
// 예외 발생 시 Advice
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
log.error("❌ 예외 발생: {}, 메시지: {}", joinPoint.getSignature(), ex.getMessage());
}
}
▶️ 메서드 시작: String com.example.service.MemberService.getMemberName(Long)
✅ 메서드 종료: String com.example.service.MemberService.getMemberName(Long), 반환값: 사용자_1
-- 또는 예외 발생 시 --
▶️ 메서드 시작: String com.example.service.MemberService.getMemberName(Long)
❌ 예외 발생: String com.example.service.MemberService.getMemberName(Long), 메시지: ID는 null일 수 없습니다.
@Slf4j
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* org.example.backendproject.board.service..*(..))
//AOP를 적용할 클래스
@Around("execution(* org.example.backendproject.board.service..*(..)) || execution(* org.example.backendproject.Auth.service..*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
log.info("[AOP_LOG] {} 메서드 호출 시작 ", methodName);
Object result = joinPoint.proceed();
return result;
} catch (Exception e) {
log.error("[AOP_LOG] {} 메서드 예외 {}", methodName, e.getMessage());
return e;
} finally {
long end = System.currentTimeMillis();
log.info("[AOP_LOG] {} 메서드 실행 완료 시간 = {}", methodName, end - start);
}
}
}