- [Spring] AOP: 관점 지향 프로그래밍의 이해2025년 03월 16일 18시 06분 02초에 업로드 된 글입니다.작성자: nickhealthy
오늘은 Spring 프레임워크의 중요한 기능 중 하나인 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)에 대해 알아보겠습니다. AOP는 객체 지향 프로그래밍(OOP)을 보완하는 프로그래밍 패러다임으로, 애플리케이션의 핵심 비즈니스 로직과 부가 기능을 깔끔하게 분리할 수 있게 해줍니다.
AOP가 필요한 이유
소프트웨어 개발에서 주로 비즈니스 로직에 집중합니다. 예를 들어 OrderService라는 클래스가 있다면, 주문 처리라는 핵심 기능에 집중해야 합니다. 하지만 실제 애플리케이션은 이런 핵심 기능 외에도 로깅, 보안, 트랜잭션 관리 등 여러 부가 기능들이 필요합니다.
이러한 부가 기능들은 대부분 애플리케이션 전반에 걸쳐 사용되는 공통 관심사(cross-cutting concerns) 입니다. 만약 이런 부가 기능들을 각 비즈니스 로직 코드에 직접 삽입한다면, 다음과 같은 문제가 발생합니다:
- 코드 중복: 동일한 부가 기능 코드가 여러 클래스에 반복되어 나타납니다.
- 코드 복잡성 증가: 핵심 비즈니스 로직과 부가 기능이 섞여 코드 가독성이 떨어집니다.
- 유지보수 어려움: 부가 기능의 변경이 필요할 때 모든 클래스를 수정해야 합니다.
예를 들어, 메서드 실행 시간을 측정하는 로직을 100개의 메서드에 추가하려면 100곳을 모두 수정해야 합니다. 또한 예외 처리를 위해 모든 메서드에 try-catch-finally 구문을 추가해야 할 수도 있습니다.
AOP의 개념
AOP는 이런 문제를 해결하기 위해 등장했습니다. AOP는 애플리케이션의 핵심 기능과 부가 기능을 분리하여 관리할 수 있게 해줍니다. 핵심 기능은 핵심 기능대로 개발하고, 부가 기능은 별도의 모듈로 개발한 후 이를 필요한 곳에 적용할 수 있습니다.
AOP에서는 이런 부가 기능을 관점(aspect) 이라고 합니다. Spring에서는 @Aspect 애노테이션을 통해 관점을 정의할 수 있습니다. 이렇게 분리된 부가 기능은 필요한 시점에 적용되어 결과적으로 코드 중복을 줄이고 핵심 비즈니스 로직에 집중할 수 있게 해줍니다.
중요: AOP는 OOP를 대체하는 것이 아니라, OOP를 보완하는 기술입니다.
Spring AOP와 AspectJ
Spring AOP는 AspectJ라는 AOP 프레임워크의 일부 기능을 사용합니다. AspectJ는 완전한 AOP 기능을 제공하지만, Spring AOP는 AspectJ의 일부 기능만 사용하여 더 간단한 방식으로 AOP를 구현합니다. AspectJ 프레임워크를 사용해 Spring AOP에서 지원하지 않는 필요한 기능을 모두 사용할 수 있으면 좋겠지만, AspectJ 프레임워크를 익히는 데는 시간도 오래 걸리고 Spring AOP만으로도 충분한 기능을 제공합니다.
AOP 적용 방식
AOP는 크게 세 가지 방식으로 적용할 수 있습니다:
1. 컴파일 시점 위빙(Compile-time Weaving)
- 소스 코드(.java)를 컴파일하여 클래스 파일(.class)로 만드는 시점에 AOP를 적용합니다.
- AspectJ 컴파일러를 사용합니다.
- 장점: 모든 지점에 AOP 적용 가능, 성능 영향 최소화
- 단점: 별도의 컴파일러 필요, 복잡한 설정
2. 클래스 로딩 시점 위빙(Load-time Weaving)
- 클래스 파일(.class)이 JVM에 로딩되는 시점에 AOP를 적용합니다.
- Java의 Instrumentation 기능을 사용합니다.(참고: 대부분의 모니터링 툴이 이 기능을 통해 만들어집니다.)
- 장점: 모든 지점에 AOP 적용 가능
- 단점: 성능 영향 있음, 추가 설정 필요
3. 런타임 시점 위빙(Runtime Weaving)
- 프로그램 실행 중에 AOP를 적용합니다.
- 프록시 패턴을 이용합니다.
- 장점: 간단한 설정, Spring과 통합 용이
- 단점: 메서드 실행 지점에만 적용 가능, 일부 제약 있음
Spring AOP는 기본적으로 런타임 시점 위빙을 사용합니다. 이는 프록시 패턴을 이용하여 구현되며, 프록시는 원본 객체를 감싸서 부가 기능을 추가하는 역할을 합니다.
Spring AOP 프록시
Spring은 AOP를 구현하기 위해 두 가지 프록시 기술을 사용합니다:
- JDK 동적 프록시: 인터페이스 기반 프록시
- CGLIB 프록시: 클래스 상속 기반 프록시
Spring은 타겟 객체가 인터페이스를 구현하고 있으면 JDK 동적 프록시를 사용하고, 그렇지 않으면 CGLIB 프록시를 사용합니다.
AOP 주요 용어
1. 조인 포인트(Join point)
- 프로그램 실행 중 AOP가 적용될 수 있는 지점
- 메서드 실행, 생성자 호출, 필드 값 변경, 예외 발생 등이 조인 포인트가 될 수 있음
- Spring AOP에서는 메서드 실행 지점만 조인 포인트로 사용
2. 포인트컷(Pointcut)
- 조인 포인트 중에서 실제로 AOP를 적용할 지점을 선별하는 표현식
- AspectJ 포인트컷 표현식을 사용하여 정의
3. 타겟(Target)
- AOP가 적용되는 대상 객체
4. 어드바이스(Advice)
- 실제로 부가 기능을 구현한 코드
- 종류: Around(전후), Before(전), After(후), AfterReturning(정상 반환 후), AfterThrowing(예외 발생 후)
5. 애스펙트(Aspect)
- 포인트컷 + 어드바이스를 모듈화한 것
- Spring에서는 @Aspect 애노테이션을 통해 정의
6. 어드바이저(Advisor)
- Spring AOP에서 사용되는 개념으로, 하나의 포인트컷과 하나의 어드바이스를 가짐
7. 위빙(Weaving)
- AOP가 적용되는 과정
- 컴파일 시점, 클래스 로딩 시점, 런타임 시점에 이루어질 수 있음
Spring AOP 사용 예제
이제 Spring AOP를 사용하여 간단한 예제를 만들어보겠습니다. 이 예제에서는 메서드 실행 시간을 측정하는 부가 기능을 추가해보겠습니다.
1. 필요한 의존성 추가
<!-- pom.xml (Maven) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2. 애스펙트 클래스 작성
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class TimeTraceAspect { @Around("execution(* com.example.service..*(..))") 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"); } } }
- `@Aspect`: 이 클래스가 애스펙트임을 나타냅니다.
- `@Component`: 스프링 빈으로 등록합니다.
- `@Around`: 어드바이스 유형을 지정합니다. 메서드 실행 전후에 로직을 수행합니다.
- `execution(* com.example.service..*(..))`는 포인트컷 표현식(AspectJ)으로, com.example.service 패키지와 그 하위 패키지의 모든 메서드에 적용됩니다.
3. 서비스 클래스 작성
import org.springframework.stereotype.Service; @Service public class OrderService { public void createOrder(String itemName) { System.out.println("주문 생성: " + itemName); // 비즈니스 로직... } public void cancelOrder(Long orderId) { System.out.println("주문 취소: " + orderId); // 비즈니스 로직... } }
4. 실행결과
START: execution(void com.example.service.OrderService.createOrder(String)) 주문 생성: 맥북 END: execution(void com.example.service.OrderService.createOrder(String)) 5ms START: execution(void com.example.service.OrderService.cancelOrder(Long)) 주문 취소: 1 END: execution(void com.example.service.OrderService.cancelOrder(Long)) 3ms
어드바이스 유형
Spring AOP에서 제공하는 어드바이스 유형은 다음과 같습니다.
1. @Before
조인 포인트 실행 이전에 실행됩니다.
중요한 건 `@Before` 호출 이후 조인 포인트 `proceed()`가 실행되어 자동으로 뒷단의 로직을 처리합니다.
@Before("execution(* com.example.service.*.*(..))") public void before(JoinPoint joinPoint) { System.out.println("Before: " + joinPoint.getSignature()); }
2. @After
조인 포인트가 정상 또는 예외에 관계없이 실행(finally)됩니다.
@After("execution(* com.example.service.*.*(..))") public void after(JoinPoint joinPoint) { System.out.println("After: " + joinPoint.getSignature()); }
3. @AfterReturning
조인 포인트가 정상 완료후 실행됩니다.
- `returning` 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 합니다.
- `returning` 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행합니다.(부모 타입을 지정하면 모든 자식 타입은 인정)
- 예를 들어, 매개변수 타입을 String으로 지정했을 때, 포인트컷에 적용되는 AOP 중 String과 그 하위 타입을 반환하는 메서드만 실행합니다.
@AfterReturning(value = "execution(* com.example.service.*.*(..))", returning = "result") public void afterReturning(JoinPoint joinPoint, Object result) { System.out.println("AfterReturning: " + joinPoint.getSignature() + ", 결과: " + result); }
4. @AfterThrowing
메서드 실행이 예외를 던져서 종료될 때 실행됩니다.
- `throwing` 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 합니다.
- `throwing` 절에 지정된 타입과 맞는 예외를 대상으로 실행한다.(부모 타입을 지정하면 모든 자식 타입은 인정된 다.)
@AfterThrowing(value = "execution(* com.example.service.*.*(..))", throwing = "ex") public void afterThrowing(JoinPoint joinPoint, Exception ex) { System.out.println("AfterThrowing: " + joinPoint.getSignature() + ", 예외: " + ex.getMessage()); }
5. @Around
메서드 실행 전후에 실행됩니다. 가장 강력한 어드바이스 유형으로, 메서드 실행 여부를 직접 제어할 수 있습니다.
@Around("execution(* com.example.service.*.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { try { // Before System.out.println("Around 시작: " + joinPoint.getSignature()); Object result = joinPoint.proceed(); // 실제 메서드 실행 // AfterReturning System.out.println("Around 종료: " + joinPoint.getSignature() + ", 결과: " + result); return result; } catch (Exception e) { // AfterThrowing System.out.println("Around 예외: " + joinPoint.getSignature() + ", 예외: " + e.getMessage()); throw e; } finally { // After System.out.println("리소스 해제: " + joinPoint.getSignature()); } }
Spring AOP의 제약사항
Spring AOP를 사용할 때 알아두어야 할 몇 가지 제약사항이 있습니다.
- 메서드 실행 지점만 지원
- Spring AOP는 메서드 실행 조인 포인트만 지원합니다. 필드 접근, 생성자 호출 등의 조인 포인트는 지원하지 않습니다.
- 프록시 방식의 한계
- 자기 호출(self-invocation)에는 AOP가 적용되지 않습니다. 즉, 같은 클래스 내에서 메서드를 호출할 때는 프록시를 거치지 않기 때문에 AOP가 작동하지 않습니다.
- 프록시 객체를 통해 호출해야만 AOP가 적용됩니다.
- final 클래스와 메서드
- final 클래스는 상속이 불가능하므로 CGLIB 프록시를 생성할 수 없습니다.
- final 메서드는 오버라이딩이 불가능하므로 AOP가 적용되지 않습니다.
- private 메서드
- private 메서드는 외부에서 호출할 수 없으므로 AOP가 적용되지 않습니다.
정리
Spring AOP는 애플리케이션의 핵심 비즈니스 로직과 부가 기능을 깔끔하게 분리할 수 있게 해주는 강력한 기능입니다. 로깅, 트랜잭션 관리, 보안, 성능 측정 등과 같은 공통 관심사를 모듈화하여 코드 중복을 줄이고 유지보수성을 향상시킬 수 있습니다.
Spring AOP는 전체 AspectJ의 기능 중 일부만 사용하지만, 대부분의 경우에는 충분한 기능을 제공합니다. 더 강력한 AOP 기능이 필요하다면 AspectJ를 직접 사용할 수도 있습니다.
AOP를 적절히 활용하면 애플리케이션의 구조를 보다 깔끔하게 유지할 수 있으며, 개발자는 핵심 비즈니스 로직에 집중할 수 있게 됩니다. 다만, AOP는 강력한 도구인 만큼 남용하면 오히려 애플리케이션의 복잡성을 증가시킬 수 있으므로 적절한 상황에서 사용하는 것이 중요합니다.
AOP는 객체 지향 설계의 원칙 중 하나인 "단일 책임 원칙(SRP)"을 지키는 데 도움이 됩니다. 각 클래스는 자신의 핵심 책임에만 집중하고, 부가적인 기능은 애스펙트를 통해 분리함으로써 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
다음글이 없습니다.이전글이 없습니다.댓글