
개요
비즈니스 로직 구현 중 반복되는 코드가 있나요? 로깅, 트랜잭션 관리, 보안 검증, 성능 측정처럼 여러 메서드에서 같은 코드를 반복해야 한다면 Spring AOP(Aspect-Oriented Programming)가 답입니다.
AOP를 사용하지 않으면 비즈니스 로직이 횡단 관심사로 오염되어 코드가 복잡해지고 유지보수가 어려워집니다. AOP를 사용하면 이런 공통 관심사를 분리하여 깔끔한 구조를 유지할 수 있습니다. 이 글에서는 @Aspect 어노테이션을 사용해 Spring AOP를 구현하고, 실전 예제(로깅, 성능 측정, 예외 처리)로 즉시 활용할 수 있는 방법을 알아봅니다.
Spring AOP란?
AOP(Aspect-Oriented Programming)는 횡단 관심사(Cross-Cutting Concerns)를 분리하여 코드 중복을 제거하는 프로그래밍 패러다임입니다.
횡단 관심사란?
횡단 관심사는 애플리케이션의 여러 곳에서 반복되는 공통 로직입니다. 비즈니스 로직과는 무관하지만, 모든 메서드에서 필요한 작업들입니다.
📌 비즈니스 로직 vs 횡단 관심사
| 구분 | 비즈니스 로직 | 횡단 관심사 |
|---|---|---|
| 정의 | 기능의 핵심 목표 달성 | 모든 기능에 공통으로 필요한 작업 |
| 예시 | 주문 생성, 상품 조회 | 로깅, 성능 측정, 보안 검증 |
| 발생 빈도 | 메서드마다 다름 | 모든 메서드에서 반복 |
| 변경 시기 | 비즈니스 요구사항 변경 시 | 기술적 요구사항 변경 시 |
| 책임 | 도메인 로직 처리 | 기술적 횡단 관심사 처리 |
🔍 횡단 관심사의 종류
1. 로깅 (Logging)
└─ 메서드 실행, 파라미터, 반환값, 예외 기록
2. 성능 측정 (Performance Monitoring)
└─ 메서드 실행 시간, 느린 쿼리 감지, 응답 시간 기록
3. 보안 검증 (Security)
└─ 권한 확인, 데이터 접근 제어, 암호화
4. 트랜잭션 관리 (Transaction Management)
└─ 데이터 일관성 보장, 커밋/롤백
5. 감사 로깅 (Audit Logging)
└─ 누가, 언제, 무엇을 변경했는지 기록
6. 캐시 관리 (Caching)
└─ 캐시 저장, 무효화, 갱신
7. 예외 처리 (Exception Handling)
└─ 공통 예외 처리, 에러 로깅
8. 재시도 로직 (Retry Logic)
└─ 실패 시 자동 재시도
왜 분리해야 할까?
분리하지 않으면: 모든 메서드가 비즈니스 로직 + 로깅 + 성능 측정 + 보안 검증 코드로 복잡해집니다.
분리하면:
- ✅ 비즈니스 로직이 깔끔해짐
- ✅ 횡단 관심사는 한 곳에서 관리
- ✅ 코드 중복 제거
- ✅ 유지보수와 테스트가 쉬워짐
관심사의 분리
변경전: 비즈니스 로직에 횡단 관심사가 섞여 있음
// 로깅, 성능 측정, 예외 처리가 비즈니스 로직과 섞여 있음
@Service
public class OrderService {
public void createOrder(Order order) {
long startTime = System.currentTimeMillis();
try {
// 로깅
logger.info("주문 생성 시작: " + order.getId());
// 실제 비즈니스 로직
orderRepository.save(order);
// 성능 측정
long elapsed = System.currentTimeMillis() - startTime;
logger.info("주문 생성 완료. 소요 시간: " + elapsed + "ms");
} catch (Exception e) {
// 예외 처리
logger.error("주문 생성 실패", e);
throw new BusinessException("주문을 생성할 수 없습니다.");
}
}
}
변경후: AOP로 관심사 분리
// 비즈니스 로직만 깔끔하게
@Service
public class OrderService {
public void createOrder(Order order) {
orderRepository.save(order);
}
}
// AOP Aspect에서 로깅, 성능 측정, 예외 처리 담당
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* OrderService.*(..))")
public Object logAndMeasure(ProceedingJoinPoint joinPoint) throws Throwable {
// 로깅, 성능 측정 등 횡단 관심사 처리
}
}
핵심 개념:
- Aspect: 횡단 관심사를 구현한 클래스 (@Aspect)
- Pointcut: AOP를 적용할 위치 (메서드, 클래스 등)
- Advice: 특정 Pointcut에서 실행할 코드 (@Before, @After, @Around 등)
- Join Point: Aspect가 적용될 수 있는 지점
@Aspect를 사용한 Spring AOP 구현
1단계: 의존성 추가
pom.xml에 Spring AOP 의존성을 추가합니다.
<!-- Spring Boot AOP 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2단계: Aspect 클래스 생성
Aspect 클래스에 횡단 관심사를 구현합니다.
// 로깅과 성능 측정을 담당하는 Aspect
@Aspect
@Component
public class PerformanceLoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceLoggingAspect.class);
// Pointcut: OrderService의 모든 메서드
@Pointcut("execution(* com.example.service.OrderService.*(..))")
public void orderServiceMethods() {
}
// Advice: 메서드 실행 전후에 로깅과 성능 측정
@Around("orderServiceMethods()")
public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();
logger.info("📝 메서드 시작: {}", methodName);
try {
// 실제 메서드 실행
Object result = joinPoint.proceed();
long elapsed = System.currentTimeMillis() - startTime;
logger.info("✅ 메서드 완료: {} (소요 시간: {}ms)", methodName, elapsed);
return result;
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - startTime;
logger.error("❌ 메서드 실패: {} (소요 시간: {}ms)", methodName, elapsed, e);
throw e;
}
}
}
// 사용
@Service
public class OrderService {
private OrderRepository orderRepository;
public void createOrder(Order order) {
orderRepository.save(order);
}
public Order getOrder(Long id) {
return orderRepository.findById(id).orElse(null);
}
}
// 실행
orderService.createOrder(new Order(1L, "Product A"));
orderService.getOrder(1L);
Output:
📝 메서드 시작: createOrder
✅ 메서드 완료: createOrder (소요 시간: 12ms)
📝 메서드 시작: getOrder
✅ 메서드 완료: getOrder (소요 시간: 5ms)
실전 예제: 다양한 Advice 유형
1. @Before: 메서드 실행 전
@Aspect
@Component
public class AuthenticationAspect {
// @RequestMapping이 있는 컨트롤러 메서드 실행 전 권한 검증
@Before("execution(* com.example.controller..*(..))")
public void validateAuthentication(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String userRole = getCurrentUserRole(); // 현재 사용자 권한 조회
if (!"ADMIN".equals(userRole)) {
throw new UnauthorizedException("접근 권한이 없습니다.");
}
logger.info("✅ 권한 검증 완료: {}", methodName);
}
}
2. @After: 메서드 실행 후
@Aspect
@Component
public class AuditAspect {
// 모든 save 메서드 실행 후 감사 로그 기록
@After("execution(* *.save(..))")
public void logAudit(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String userName = getCurrentUserName();
LocalDateTime timestamp = LocalDateTime.now();
logger.info("📋 감사 로그: {} 사용자가 {} 메서드를 실행했습니다. ({})",
userName, methodName, timestamp);
}
}
3. @AfterReturning: 메서드가 정상 반환된 후
@Aspect
@Component
public class CacheInvalidationAspect {
@Autowired
private CacheManager cacheManager;
// save, update, delete 메서드 실행 후 캐시 무효화
@AfterReturning("execution(* *.save(..)) || execution(* *.update(..)) || execution(* *.delete(..))")
public void invalidateCache(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
// 캐시 전체 정리
cacheManager.getCache("orders").clear();
logger.info("🗑️ 캐시 무효화: {} 메서드 실행 후 캐시를 정리했습니다.", methodName);
}
}
4. @AfterThrowing: 메서드에서 예외 발생 시
@Aspect
@Component
public class ExceptionHandlingAspect {
// RuntimeException 발생 시 에러 로깅
@AfterThrowing(pointcut = "execution(* com.example.service..*(..))", throwing = "ex")
public void logException(JoinPoint joinPoint, RuntimeException ex) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
logger.error("🔥 예외 발생: {}.{} - {}", className, methodName, ex.getMessage());
// 예외 정보를 모니터링 시스템에 전송
// sendToMonitoringSystem(className, methodName, ex);
}
}
5. @Around: 메서드 실행 전후 모두 처리 (가장 강력함)
@Aspect
@Component
public class TransactionAndSecurityAspect {
// 트랜잭션 관리 + 성능 모니터링 + 보안 검증
@Around("execution(* com.example.service..*(..))")
public Object handleTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
// 1️⃣ Before: 보안 검증
if (!hasPermission()) {
throw new SecurityException("접근 권한이 없습니다.");
}
// 2️⃣ Before: 트랜잭션 시작
long startTime = System.currentTimeMillis();
logger.info("🔄 트랜잭션 시작: {}", methodName);
try {
// 실제 메서드 실행
Object result = joinPoint.proceed();
// 3️⃣ After: 트랜잭션 커밋
long elapsed = System.currentTimeMillis() - startTime;
logger.info("✅ 트랜잭션 커밋: {} ({}ms)", methodName, elapsed);
return result;
} catch (Exception e) {
// 4️⃣ AfterThrowing: 트랜잭션 롤백
logger.error("⚠️ 트랜잭션 롤백: {} - {}", methodName, e.getMessage());
throw e;
}
}
}
Pointcut 표현식 가이드
Pointcut은 정규식 같은 패턴으로 AOP를 적용할 위치를 지정합니다.
// 1. 특정 클래스의 모든 메서드
@Pointcut("execution(* com.example.service.UserService.*(..))")
// 2. 특정 패키지 아래 모든 클래스의 모든 메서드
@Pointcut("execution(* com.example.service..*(..))")
// 3. 특정 메서드명
@Pointcut("execution(* *..*.get*(..))") // get으로 시작하는 모든 메서드
// 4. 특정 어노테이션이 있는 메서드
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
// 5. 특정 파라미터 타입
@Pointcut("execution(* *..*(..)) && args(Long)") // Long 파라미터를 받는 메서드
// 6. 조합 (OR)
@Pointcut("execution(* *.save(..)) || execution(* *.update(..))")
// 7. 조합 (AND, NOT)
@Pointcut("execution(* com.example.service..*(..)) && !@annotation(NoAop)")
성능 비교: AOP 적용 전후
// AOP 적용 전: 메서드마다 로깅/성능 측정 코드 중복
for (int i = 0; i < 1000; i++) {
orderService.createOrder(order);
orderService.getOrder(i);
}
// 결과: 1000개 메서드 × 10줄 = 10,000줄 중복 코드
// AOP 적용 후: Aspect 한 곳에서 관리
for (int i = 0; i < 1000; i++) {
orderService.createOrder(order); // Aspect가 자동으로 로깅, 성능 측정
orderService.getOrder(i);
}
// 결과: 비즈니스 로직만 깔끔하게, Aspect에서 횡단 관심사 처리
효과:
- 코드 라인 수: 10,000줄 → 100줄 (99% 감소!)
- 유지보수성: 향상 (한 곳에서 관리)
- 테스트 용이성: 향상 (로직과 관심사 분리)
맺음말
Spring AOP는 다음과 같은 상황에서 활용하세요:
- 로깅 - 모든 메서드 실행 추적
- 성능 측정 - 메서드 실행 시간 자동 기록
- 트랜잭션 관리 - @Transactional 어노테이션 처리
- 보안 검증 - 메서드 실행 전 권한 확인
- 캐시 관리 - 데이터 변경 시 캐시 무효화
- 예외 처리 - 공통 예외 처리 로직
@Aspect를 사용하면 이 모든 것을 깔끔하게 구현할 수 있습니다. 중요한 것은 적절한 Pointcut 설정입니다. 너무 광범위하게 설정하면 성능이 떨어질 수 있으므로, 필요한 메서드만 정확하게 지정하세요.
'프로그래밍 PROGRAMMING > 자바 JAVA AND FRAMEWORKS' 카테고리의 다른 글
| [JAVA/SPRING] Spring AOP 순환 참조: 원인 분석과 해결책 (0) | 2026.02.12 |
|---|---|
| [JAVA/SPRING] Java AOP Aspect 심화: 동적 프록시와 Pointcut 최적화 (0) | 2026.02.11 |
| [JAVA] Caffeine 캐시 성능 최적화: 히트율 90% 달성하기 (0) | 2026.02.09 |
| [JAVA/SPRING] Git에 비밀번호 올리지 마세요: 스프링 부트 민감 정보 관리 전략 (Jasypt, Vault) (0) | 2026.02.07 |
| [JAVA/REDIS] Redis를 이용해서 중복요청 방지하는 방법 (1) | 2026.02.06 |