본문 바로가기

Spring/스프링 입문

06. AOP

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술에 대해 공부하고, 정리한 내용입니다.

1) AOP가 필요한 상황

 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 특정 기능을 애플리케이션의 여러 부분에 걸쳐 적용할 때 매우 유용한 프로그래밍 패러다임입니다. 특히, 모든 메소드의 호출 시간을 측정하거나, 특정 작업(예: 회원 가입, 회원 조회 등)에 대한 성능 모니터링과 같은 공통 관심 사항을 처리할 때 AOP가 필요한 상황이 발생합니다.

 

(1) 모든 메소드의 호출 시간 측정

 예를 들어, 애플리케이션의 성능을 분석하고자 할 때 모든 메소드의 실행 시간을 측정하고 싶다고 가정해봅시다. 이 작업을 수동으로 하려면 각 메소드에 시간 측정 로직을 추가해야 합니다. 하지만 이 방법은 몇 가지 문제가 있습니다:

  1. 중복 코드: 모든 메소드에 동일한 시간 측정 코드를 추가해야 하므로 코드 중복이 발생합니다.
  2. 유지 관리: 메소드가 추가되거나 변경될 때마다 시간 측정 코드도 함께 수정해야 하므로 유지 관리가 어려워집니다.
  3. 핵심 로직 침해: 시간 측정 코드는 해당 메소드의 핵심 기능과는 직접적인 관련이 없으므로 코드의 가독성과 핵심 로직의 명확성이 저하됩니다.
  4. AOP를 사용하면, 이러한 문제를 해결할 수 있습니다. AOP는 시간 측정과 같은 공통 관심 사항을 애플리케이션의 핵심 로직에서 분리하여, 별도의 '관점(aspect)'으로 정의하게 합니다. 그리고 이 관점을 필요한 지점에 자동으로 적용(weaving)하여, 모든 메소드에 대한 시간 측정 기능을 효율적으로 구현할 수 있게 해줍니다.

(2) 공통 관심 사항 vs 핵심 관심 사항

 애플리케이션 개발 시, 핵심 비즈니스 로직(핵심 관심 사항)과 로깅, 보안, 트랜잭션 관리 등의 공통적으로 적용되는 기능(공통 관심 사항)을 구분하여 관리하는 것이 중요합니다.

  • 핵심 관심 사항(Core Concern): 애플리케이션의 주된 기능과 비즈니스 로직을 의미합니다. 예를 들어, 회원 가입, 회원 정보 조회 등이 여기에 해당합니다.
  • 공통 관심 사항(Cross-Cutting Concern): 여러 모듈이나 기능에서 공통적으로 필요한 기능을 의미합니다. 로깅, 보안 검사, 성능 모니터링 등이 대표적인 예입니다.
  • AOP는 이러한 공통 관심 사항을 애플리케이션의 다른 부분에 중복 없이 적용할 수 있도록 해줍니다. 예를 들어, 회원 가입이나 회원 조회와 같은 비즈니스 로직의 실행 전후에 성능을 측정하고 싶다면, AOP를 사용하여 이러한 성능 측정 기능을 별도의 관점으로 정의하고 자동으로 적용할 수 있습니다. 이로써 개발자는 핵심 비즈니스 로직에 집중할 수 있으며, 코드의 중복을 줄이고 유지 보수성을 향상시킬 수 있습니다.
  • 결론적으로, AOP는 코드의 중복을 방지하고, 유지 보수를 용이하게 하며, 애플리케이션의 가독성과 깔끔함을 유지하면서 공통적으로 사용되는 기능을 효과적으로 관리할 수 있게 해줍니다.


2) AOP가 필요한 예시 코드

package hello.hellospring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TimeTraceAop {

    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());

        try {
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString()+ " " + timeMs + "ms");
        }
    }
}

 위 코드는 MemberService 클래스 내의 join 메소드(회원가입)와 findMembers 메소드(전체 회원 조회)에서 실행 시간을 측정하는 로직을 포함하고 있습니다. 이 로직은 메소드의 실행 시작 시간과 종료 시간을 기록하여, 그 차이를 통해 메소드 실행에 걸린 시간을 계산하고 출력합니다.

 

(1) 문제점

  • 비핵심 관심 사항의 혼입: 코드에서 볼 수 있듯이, 회원가입과 회원 조회라는 핵심 비즈니스 로직에 시간 측정이라는 비핵심 관심 사항이 혼입되어 있습니다. 이는 코드의 가독성과 유지보수성을 저하시킵니다.
  • 코드 중복: 비슷한 시간 측정 로직이 여러 메소드에 중복되어 나타나고 있습니다. 이는 코드의 중복을 초래하며, 유지보수 시 시간 측정 로직의 변경이 필요할 때 각 메소드를 일일이 찾아 수정해야 하는 번거로움이 있습니다.
  • 유지보수의 어려움: 시간 측정 로직을 변경하고자 할 때, 해당 로직이 어디에, 어떻게 분포되어 있는지 파악하고 모두 수정해야 합니다. 이는 비효율적이며 오류를 발생시킬 수 있는 여지를 높입니다.

 

(2) 해결 코드: 시간 측정 AOP 등록

package hello.hellospring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class TimeTraceAop {

    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());

        try {
            return joinPoint.proceed();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString()+ " " + timeMs + "ms");
        }
    }
}

위 코드는 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)를 사용하여 시간 측정 로직을 어플리케이션 전반에 걸쳐 적용하는 방법을 보여줍니다. 이 방식을 통해 공통 관심 사항과 핵심 관심 사항을 명확히 분리할 수 있습니다. 코드의 핵심 요소는 다음과 같습니다.

  • @Component: 스프링 컨테이너에 의해 관리되는 빈으로 등록하기 위한 어노테이션입니다.
  • @Aspect: 클래스가 AOP의 관점(Aspect)을 구현한 것임을 나타냅니다.
  • @Around("execution(* hello.hellospring..*(..))"): AOP의 어드바이스(Advice) 유형 중 하나로, 지정된 대상(여기서는 hello.hellospring 패키지 및 하위 패키지에 있는 모든 메소드)의 실행 전후에 로직을 적용할 지점을 정의합니다.
  • ProceedingJoinPoint joinPoint: 현재 실행 중인 대상 메소드에 대한 정보를 포함하고 있으며, joinPoint.proceed() 메소드를 호출함으로써 실제 대상 메소드를 실행합니다.
  • 시간 측정 로직: 메소드 실행 전후의 시간을 측정하고, 시작과 종료 시간, 그리고 총 실행 시간을 로깅합니다.

 

(3) 해결된 문제

  • 공통 관심 사항과 핵심 관심 사항의 분리: 회원가입, 회원 조회 등의 핵심 비즈니스 로직에서 시간 측정과 같은 공통 관심 사항을 분리함으로써, 각각의 로직을 더 깔끔하게 유지할 수 있습니다.
  • 중복 코드 제거: 시간 측정 로직을 AOP를 통해 하나의 장소에 중앙 집중화함으로써, 코드 전반에 걸쳐 중복되는 로직을 제거할 수 있습니다.
  • 유지보수성 향상: 시간 측정 로직에 변경이 필요한 경우, AOP를 정의한 이곳만 수정하면 되므로, 유지보수가 용이해집니다.
  • 적용 대상 선택의 유연성: @Around 어노테이션에 지정된 포인트컷 표현식을 변경함으로써, 시간 측정 로직을 적용할 클래스나 메소드의 범위를 유연하게 조정할 수 있습니다.

이처럼 AOP를 활용하면, 애플리케이션의 유지보수성을 크게 향상시키고, 공통적으로 사용되는 기능들을 효율적으로 관리할 수 있습니다.


3) 스프링의 AOP 동작 방식 설명

 스프링에서 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)를 적용하게 되면, 애플리케이션의 의존관계 및 구성 요소 간의 상호작용 방식이 변경됩니다. 이 변화는 주로 프록시 패턴을 통해 이루어집니다. AOP를 적용하기 전과 후의 의존관계 및 전체 구성을 쉽게 이해하기 위해, 아래에 상세하고 구체적으로 설명합니다.

 

(1) AOP 적용 전 의존관계

  AOP 적용 전에는 스프링 컨테이너 내에서 컴포넌트들이 직접적으로 서로를 참조합니다. 예를 들어, memberControllermemberService를 직접 참조하고, memberServicememberRepository를 직접 참조합니다. 이 구조에서는 모든 컴포넌트가 서로를 직접 호출하며, 추가적인 로직 없이 순수한 비즈니스 로직만을 실행합니다.

 

(2) AOP 적용 후 의존관계

 AOP가 적용되면, 스프링 컨테이너는 각 컴포넌트 사이에 프록시(Proxy) 객체를 삽입합니다. 프록시 객체는 실제 객체를 감싸는 래퍼(wrapper) 역할을 하며, 실제 객체의 메서드 호출 전후에 추가적인 로직(예: 시간 측정, 로깅, 트랜잭션 관리 등)을 실행할 수 있습니다.

  • memberController 호출 시, 스프링 컨테이너는 memberController의 프록시를 먼저 통과하게 됩니다.
  • memberControllermemberService를 호출할 때, 실제 memberService 대신 memberService의 프록시를 호출합니다.
  • 마찬가지로, memberServicememberRepository를 호출할 때, 실제 memberRepository 대신 그 프록시를 호출합니다.

이 프록시 방식은 AOP의 핵심적인 구현 메커니즘으로, 스프링 컨테이너가 관리하는 빈(Bean)에 투명하게 적용됩니다.

 

(3) AOP 적용 전과 후의 전체 그림 비교

  • AOP 적용 전: 컴포넌트들은 서로를 직접 참조합니다. memberControllermemberService를, memberServicememberRepository를 직접 참조하며, 추가적인 중간 로직이나 처리 없이 서로의 메서드를 호출합니다.

  • AOP 적용 후: 스프링 컨테이너는 각 컴포넌트 사이에 프록시 객체를 삽입합니다. 이제 memberController, memberService, memberRepository 각각의 호출은 해당 컴포넌트의 프록시를 통과하게 됩니다. 프록시는 실제 컴포넌트를 호출하기 전후에 추가적인 로직(예: 로깅, 시간 측정)을 실행할 수 있게 해주며, 이를 통해 AOP가 구현됩니다.

 이 방식을 통해, 스프링은 비즈니스 로직과 공통 관심 사항을 분리하여 관리할 수 있게 되며, 코드의 중복을 줄이고 유지보수성을 향상시킬 수 있습니다.