[Spring] AOP(Aspect-Oriented Programming)

·2025년 6월 29일

Spring

목록 보기
26/26

💡AOP(Aspect-Oriented Programming)란?

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 공통 관심 사항(Cross-Cutting Concerns)을 모듈화하여 핵심 비즈니스 로직과 분리할 수 있게 해주는 프로그래밍 패러다임을 뜻한다.


🤔왜 AOP가 필요한데?

애플리케이션에서는 로깅, 트랜잭션 처리, 예외 처리등과 같은 공통 기능들이 자주 등장한다. 이러한 공통 기능들을 모든 서비스 로직 안에 반복해서 작성하면 코드 중복이 증가하고, 유지보수가 어려워지며 어떤게 핵심 로직인지 단번에 알기 어렵다.
예를 들어, 메서드 시작 시간과 메서드 종료 시간을 체크해서 걸린 시간을 출력하는 기능을 메서드에 넣으려면

     /** 글 등록 **/
    @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의 핵심 구성 요소

AOP의 핵심 구성 요소는 아래와 같이 총 다섯가지로 나뉘어진다.

구성 요소설명예시
@Aspect이 클래스가 Aspect임을 정의@Aspect public class LoggingAspect { ... }
Advice공통 기능(로직)이 실행되는 시점@Before, @After, @Around, @AfterThrowing, @AfterReturning
JoinPointAdvice가 적용 가능한 지점 (메소드 실행 등)메소드 실행 전/후
PointcutJoinPoint를 선별하는 조건execution(* com.example.service..*(..))
WeavingAspect를 실제 객체에 적용하는 작업Spring은 런타임에 프록시 방식으로 적용

1️⃣Aspect (관점)

📌정의

공통 기능(로깅, 보안 검사, 트랙잭션 등)을 모듈화한 클래스를 의미. @Aspect 어노테이션을 사용한다.

☑️예시

@Aspect	//공통 기능을 정의하는 클래스임을 알려줌
@Component
public class LoggingAspect {
    // 공통 기능들 정의됨
}

2️⃣Advice (공통 기능 로직)

📌정의

Aspect 안에 있는 실제 로직. 언제, 어떤 방식으로 실행할 지 정의함

🔧관련 어노테이션

종류설명사용 예시
@Before메서드 실행 수행로그 출력, 인증 검사
@AfterReturning메서드 성공적으로 리턴 후 수행리턴값 로그
@AfterThrowing메서드 예외 발생 시 수행예외 로깅
@After메서드 종료 후 (정상/예외 관계 없이)리소스 정리
@Around실행 전후를 모두 감쌈성능 측정, 커스텀 인증, 트랜잭션

☑️예시

@Before("execution(* com.example.service..*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("▶️ " + joinPoint.getSignature());
}
  • @Before(...) : 타겟 메서드가 실행되기 바로 전에 실행되는 것을 의미
  • "execution( com.example.service..(..))" : 포인트컷 표현식.
    어떤 메서드에 적용할지 범위를 설정하는 것.
    ➡️com.example.service 및 그 하위 패키지의 모든 클래스의 모든 메서드 실행 전에 logBefore()가 실행된다는 의미

3️⃣JoinPoint (실행 지점 정보)

📌정의

Advice가 적용될 수 있는 지점 중 하나로, Spring AOP는 기본적으로 메서드 실행만 지원한다.

🔍용도

  • 어떤 메서드가 실행됐는지 (getSignature())
  • 어떤 인자가 들어왔는지 (getArgs())
  • 실제 호출 객체는 뭔지 (getTarget())

☑️예시

public void logBefore(JoinPoint joinPoint) {
    System.out.println("메서드 이름: " + joinPoint.getSignature().getName());
    System.out.println("인자들: " + Arrays.toString(joinPoint.getArgs()));
}

4️⃣Pointcut (적용 대상 필터)

📌정의

Advice를 어떤 메서드에 적용할 지 지정하는 표현식 -> 즉, 어디에 적용할 것인가를 필터링 하는 역할

☑️예시

// com.example.service 패키지 이하 모든 클래스의 모든 메서드
@Pointcut("execution(* com.example.service..*(..))")
표현식의미
*모든 반환 타입
..하위 패키지 또는 모든 파라미터
execution(...)포인트컷 지정 문법

5️⃣Weaving (위빙)

📌정의

Aspect를 어디에, 언제, 어떻게 적용할지 결정하고 실제로 반영하는 작업 ->Spring에서는 런타임에 프록시 객체를 만들어서 위빙을 수행함

🧠방식

방식설명
프록시 기반 (Spring AOP)클래스 또는 인터페이스를 감싼 프록시를 런타임에 생성
바이트코드 조작 (AspectJ)컴파일 타임 또는 로드 타임에 바이트코드 변경

✅AOP 적용해보기

com.example.service 패키지 아래의 모든 서비스 메소드 실행 전/후에 로그를 남기고 싶다면?

📍Aspect 클래스 작성

@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일 수 없습니다.

📍@Around를 이용해 메서드 실행 전후를 모두 감싸려면?

@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);
        }
    }
}
profile
배우고 기록하며 성장하는 백엔드 개발자입니다!

0개의 댓글