inblog logo
|
LifeLog, DevLog
    Spring

    Spring의 주요 강점 - 2, Aspect

    AOP에 관하여
    KYJTHEYJ's avatar
    KYJTHEYJ
    Nov 25, 2025
    Spring의 주요 강점 - 2, Aspect
    Contents
    AOPAdvice 종류 예시PointCut 표현식JointPointProceedingJointPoint적용 예시로깅 예시성능 모니터링권한 체크파라미터 검증

    AOP

    Aspect-Oriented Programming, 관점지향 프로그래밍

    관심사를 분리하여 활용하는 프로그래밍 방식

    • 횡단 관심사 (크로스-커팅 코너스)

      • 여러 곳에서 반복되는 부가 기능 (로깅, 트랜잭션, 보안, 성능 측정 등등..)

    • 핵심 관심사 (코어 코너스)

      • 실제 비즈니스 로직

    관점에 대해 4가지 정의를 할 수 있다

    1. 관점 (Aspect)

      1. @Aspect 를 사용하여 관점 분리

    2. 결합점 (JointPoint) → 부가 기능이 적용될 수 있는 지점

      1. 호출 시점

      2. 실행 시점

      3. 예외 발생 시점

    3. 어드바이스 (Advice) → 언제 부가 기능을 실행할 지

      1. @Before → 메서드 실행전

      2. @After → 실행 후 (예외 미상관)

      3. @AfterReturning → 메서드 정상 종료 후

      4. @AfterThrowing → 예외 발생 후

      5. @Around → 메서드 실행 전후 (가장 강력)

    4. 포인트컷 (PointCut) → 어디에 적용할 지

      1. execution(* com.example.service.*.*(..)) → service 패키지의 모든 메서드

      2. @annotation(com.example.Logging) → @Logging 어노테이션이 붙은 메서드

      3. within → 클래스/패키지 별로 적용할 때

    Advice 종류 예시

    Before

    @Aspect
    @Component
    public class SecurityAspect {
        
        @Before("execution(* com.example.service.*.*(..))")
        public void checkSecurity(JoinPoint joinPoint) {
            System.out.println("보안 체크 시작");
            
            User currentUser = SecurityContext.getCurrentUser();
            if (currentUser == null) {
                throw new UnauthorizedException("로그인이 필요합니다");
            }
            
            System.out.println("보안 체크 완료: " + currentUser.getName());
            
            // 실행되는 메서드가 마저 실행 됨
        }
    }
    
    // 실행 순서:
    // 1. checkSecurity() 실행 (보안 체크)
    // 2. 실제 메서드 실행

    AfterReturning (정상 종료 후)

    @Aspect
    @Component
    public class AuditAspect {
        
        @AfterReturning(
            pointcut = "execution(* com.example.service.UserService.save(..))",
            returning = "result" // -> 결과를 받아와서 활용
        )
        public void logAfterSave(JoinPoint joinPoint, Object result) {
            System.out.println("사용자 저장 성공!");
            System.out.println("저장된 사용자: " + result);
            
            // 감사 로그 기록
            auditLogger.log("USER_SAVED", result);
        }
    }
    
    // 실행 순서:
    // 1. save() 메서드 실행
    // 2. 정상 종료되면 logAfterSave() 실행

    • AfterThrowing (예외 발생 후)

    @Aspect
    @Component
    public class ExceptionAspect {
        
        @AfterThrowing(
            pointcut = "execution(* com.example.service.*.*(..))",
            throwing = "ex" //-> 실행 중인 메서드의 예외처리 에러를 받아옴
        )
        public void handleException(JoinPoint joinPoint, Exception ex) {
            String methodName = joinPoint.getSignature().getName();
            
            System.err.println("예외 발생!");
            System.err.println("메서드: " + methodName);
            System.err.println("예외: " + ex.getMessage());
            
            // 알림 발송
            notificationService.sendAlert(
                "메서드 " + methodName + "에서 예외 발생: " + ex.getMessage()
            );
        }
    }
    
    // 실행 순서:
    // 1. 메서드 실행
    // 2. 예외 발생하면 handleException() 실행
    // 3. 예외는 계속 전파됨

    After (무조건 후실행)

    @Aspect
    @Component
    public class ResourceAspect {
        
        @After("execution(* com.example.service.*.*(..))")
        public void cleanup(JoinPoint joinPoint) {
            System.out.println("리소스 정리 시작");
            
            // 성공하든 실패하든 무조건 실행
            resourceManager.cleanup();
            
            System.out.println("리소스 정리 완료");
        }
    }
    
    // 실행 순서:
    // 1. 메서드 실행
    // 2. 성공/실패 상관없이 cleanup() 실행

    Around (메서드의 실행 전체 시점 제어)

    @Aspect
    @Component
    public class PerformanceAspect {
        
        @Around("execution(* com.example.service.*.*(..))")
        public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
            
            // ========== 메서드 실행 전 ==========
            String methodName = joinPoint.getSignature().getName();
            System.out.println("시작: " + methodName);
            long startTime = System.currentTimeMillis();
            
            Object result = null;
            try {
                // ========== 메서드 실행 ==========
                result = joinPoint.proceed();  // 이 호출이 실제 메서드 실행!
                
            } catch (Exception e) {
                System.err.println("예외 발생: " + e.getMessage());
                throw e;
            } finally {
                // ========== 메서드 실행 후 (무조건) ==========
                long endTime = System.currentTimeMillis();
                System.out.println("종료: " + methodName);
                System.out.println("실행 시간: " + (endTime - startTime) + "ms");
            }
            
            return result;
        }
    }
    
    // @Around는 메서드 실행을 완전히 제어 가능!
    // - 실행 전후 처리
    // - 예외 처리
    // - 결과 변경
    // - 실행 자체를 막을 수도 있음

    PointCut 표현식

    execution

    execution(접근제어자 리턴타입 패키지.클래스.메서드(파라미터))

    // 모든 public 메서드
    execution(public * *(..))
    
    // set으로 시작하는 모든 메서드
    execution(* set*(..))
    
    // service 패키지의 모든 메서드
    execution(* com.example.service.*.*(..)) 
    
    // service 하위 모든 패키지    
    execution(* com.example.service..*.*(..))
    
    // Service로 끝나는 클래스의 모든 메서드
    execution(* com.example..*Service.*(..))
    
    // 파라미터가 Long 1개인 메서드     
    execution(* com.example.service.*.*(Long))
    
    // 파라미터가 없는 메서드
    execution(* com.example.service.*.*())

    @annotation

    // 커스텀 어노테이션을 우선 정의
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Logging {
    }
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PerformanceCheck {
    }
    
    // 관점 작성
    // Aspect
    @Aspect
    @Component
    public class CustomAspect {
        
        // @Logging 붙은 메서드에만 적용
        @Around("@annotation(com.example.annotation.Logging)")
        public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("로그 시작");
            Object result = joinPoint.proceed();
            System.out.println("로그 종료");
            return result;
        }
        
        // @PerformanceCheck 붙은 메서드에만 적용
        @Around("@annotation(com.example.annotation.PerformanceCheck)")
        public Object checkPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
            long start = System.currentTimeMillis();
            Object result = joinPoint.proceed();
            long end = System.currentTimeMillis();
            System.out.println("실행 시간: " + (end - start) + "ms");
            return result;
        }
    }
    
    // 사용
    @Service
    public class UserService {
        
        @Logging  // 이 메서드만 로깅
        public User findById(Long id) {
            return userRepository.findById(id).orElseThrow();
        }
        
        @PerformanceCheck  // 이 메서드만 성능 측정
        public void complexOperation() {
            // ...
        }
        
        // 어노테이션 없으면 AOP 적용 안 됨
        public void simpleOperation() {
            // ...
        }
    }

    within (클래스/패키지 기반)

    @Aspect
    @Component
    public class PackageAspect {
        
        // service 패키지의 모든 메서드
        @Before("within(com.example.service.*)")
        public void beforeService() {
            System.out.println("Service 호출");
        }
        
        // UserService의 모든 메서드
        @Before("within(com.example.service.UserService)")
        public void beforeUserService() {
            System.out.println("UserService 호출");
        }
    }

    JointPoint

    Aspect 메서드의 파라미터로 JointPoint 를 받고 있다
    JointPoint 는 AOP가 적용된 메서드의 실행 시점 정보를 가진 객체이다
    메서드 이름, 파라미터 등의 정보를 제공 하는 실행 시점의 정보가 담겨 있다

    @Slf4j
    @Aspect
    @Component
    public class TestAspect {
        @Before("execution(* kyj.practice.demo.*.service.*.*(..))")
        public void testPrintText(JoinPoint joinPoint) {
    		Signature signature = joinPoint.getSignature();
            log.info("AOP TEST : {}", signature.getName());
            Object[] objects = joinPoint.getArgs();
            log.info("AOP TEST2 : {}", objects);
            Object target = joinPoint.getTarget();
            log.info("AOP TEST3 : {}", target);
    		Object target2 = joinPoint.getThis();
            log.info("AOP TEST4 : {}", target2);
        }
    }

    joinPoint.getSignature()

    메서드의 정보에 대해

    getName() → 메서드 이름
    getDeclaringTypeName() → 선언한 클래스의 전체 이름 (아티팩트 명 까지 다)
    getDeclaringTypeName().getSimpleName() → 클래스명만

    jointPoint.getArgs()

    파라미터 값에 대해

    파라미터 값들을 받아오므로 Object[] 형식을 지님

    jointPoint.getTarget()

    실행 메서드의 대상 객체에 대해 (실제 객체를 받아오는 것)

    getClass().getName() 을 더 할 경우 대상 객체의 클래스 이름을 받아옴

    jointPoint.getThis()

    대상 객체의 프록시 객체를 받아옴

    jointPoint.getKind()

    실행되는 JointPoint 종류를 받아옴

    메서드가 실행되는 것이면 method-excution 이라는 String을 얻음

    ProceedingJointPoint

    @Around에서만 사용가능한 JointPoint 로, 메서드의 실행 여부 까지도 제어 가능

    이 때는 proceed() 를 통해 실행을 제어 해주어야한다

    @Aspect
    @Component
    @Slf4j
    public class AroundAspect {
        
        @Around("execution(* com.example.service.*.*(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            // JoinPoint의 모든 메서드 사용 가능
            String methodName = joinPoint.getSignature().getName();
            Object[] args = joinPoint.getArgs();
            
            log.info("메서드 시작: {}", methodName);
            log.info("파라미터: {}", Arrays.toString(args));
            
            long startTime = System.currentTimeMillis();
            
            // 실제 메서드 실행 **
            Object result = joinPoint.proceed();
            
            long executionTime = System.currentTimeMillis() - startTime;
            
            log.info("메서드 종료: {}", methodName);
            log.info("실행 시간: {}ms", executionTime);
            log.info("반환값: {}", result);
            
            return result;
        }
    }
    
    //------------------------------------------
    
    // 파라미터의 수정이 일어난 경우
    @Around("execution(* com.example.service.*.*(..))")
    public Object modifyAndProceed(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        
        // 파라미터 수정
        if (args.length > 0 && args[0] instanceof String) {
            args[0] = ((String) args[0]).toUpperCase();
        }
        
        // ⭐ 수정된 파라미터로 실행
        Object result = joinPoint.proceed(args);
        
        return result;
    }

    적용 예시

    AOP 적용전

    @Service
    public class UserService {
        
        public User findById(Long id) {
            // ========== 부가 기능 시작 ==========
            System.out.println("=== 메서드 시작: findById ===");
            long startTime = System.currentTimeMillis();
            
            // ========== 핵심 로직 ==========
            User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException());
            
            // ========== 부가 기능 종료 ==========
            long endTime = System.currentTimeMillis();
            System.out.println("실행 시간: " + (endTime - startTime) + "ms");
            System.out.println("=== 메서드 종료: findById ===");
            
            return user;
        }
        
        public void save(User user) {
            // ========== 부가 기능 시작 ==========
            System.out.println("=== 메서드 시작: save ===");
            long startTime = System.currentTimeMillis();
            
            // ========== 핵심 로직 ==========
            userRepository.save(user);
            
            // ========== 부가 기능 종료 ==========
            long endTime = System.currentTimeMillis();
            System.out.println("실행 시간: " + (endTime - startTime) + "ms");
            System.out.println("=== 메서드 종료: save ===");
        }
        
        public void delete(Long id) {
            // ========== 부가 기능 시작 ==========
            System.out.println("=== 메서드 시작: delete ===");
            long startTime = System.currentTimeMillis();
            
            // ========== 핵심 로직 ==========
            userRepository.deleteById(id);
            
            // ========== 부가 기능 종료 ==========
            long endTime = System.currentTimeMillis();
            System.out.println("실행 시간: " + (endTime - startTime) + "ms");
            System.out.println("=== 메서드 종료: delete ===");
        }
    }
    • 메서드의 소요 시간을 로깅하기 위해 계속 같은 코드 반복 중

    AOP 적용 후

    // 우선 핵심 로직만 남김
    @Service
    public class UserService {
        
        private final UserRepository userRepository;
        
        public User findById(Long id) {
            return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException());
        }
        
        public void save(User user) {
            userRepository.save(user);
        }
        
        public void delete(Long id) {
            userRepository.deleteById(id);
        }
    }
    
    // 부가 기능 분리
    @Aspect // 관점 어노테이션
    @Component
    public class LoggingAspect {
        
        // UserService의 모든 메서드에 적용
        @Around("execution(* com.example.service.UserService.*(..))")
        public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
            
            // ========== 메서드 실행 전 ==========
            String methodName = joinPoint.getSignature().getName();
            System.out.println("=== 메서드 시작: " + methodName + " ===");
            long startTime = System.currentTimeMillis();
            
            // ========== 실제 메서드 실행 ==========
            Object result = joinPoint.proceed();
            
            // ========== 메서드 실행 후 ==========
            long endTime = System.currentTimeMillis();
            System.out.println("실행 시간: " + (endTime - startTime) + "ms");
            System.out.println("=== 메서드 종료: " + methodName + " ===");
            
            return result;
        }
    }

    로깅 예시

    @Aspect
    @Component
    @Slf4j
    public class LoggingAspect {
        
        @Around("execution(* com.example.controller.*.*(..))")
        public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
            
            // 요청 정보
            String className = joinPoint.getTarget().getClass().getSimpleName();
            String methodName = joinPoint.getSignature().getName();
            Object[] args = joinPoint.getArgs();
            
            log.info("▶ API 호출: {}.{}", className, methodName);
            log.info("▶ 파라미터: {}", Arrays.toString(args));
            
            // 실행
            Object result = joinPoint.proceed();
            
            // 응답 정보
            log.info("◀ 응답: {}", result);
            
            return result;
        }
    }
    
    // 출력:
    // ▶ API 호출: UserController.createUser
    // ▶ 파라미터: [UserRequest(name=홍길동, email=hong@email.com)]
    // ◀ 응답: UserResponse(id=1, name=홍길동)

    성능 모니터링

    @Aspect
    @Component
    @Slf4j
    public class PerformanceAspect {
        
        @Around("execution(* com.example.service.*.*(..))")
        public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
            
            String methodName = joinPoint.getSignature().toShortString();
            long startTime = System.currentTimeMillis();
            
            try {
                Object result = joinPoint.proceed();
                return result;
                
            } finally {
                long endTime = System.currentTimeMillis();
                long executionTime = endTime - startTime;
                
                // 성능 기록
                log.info("⏱ {}: {}ms", methodName, executionTime);
                
                // 느린 쿼리 알림 (1초 이상)
                if (executionTime > 1000) {
                    log.warn("느린 메서드 감지: {} ({}ms)", methodName, executionTime);
                    alertService.sendSlowMethodAlert(methodName, executionTime);
                }
            }
        }
    }

    권한 체크

    // 커스텀 어노테이션
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequireRole {
        String value();  // 필요한 권한
    }
    
    // Aspect
    @Aspect
    @Component
    public class SecurityAspect {
        
        @Before("@annotation(requireRole)")
        public void checkRole(RequireRole requireRole) {
            
            String requiredRole = requireRole.value();
            User currentUser = SecurityContext.getCurrentUser();
            
            if (currentUser == null) {
                throw new UnauthorizedException("로그인이 필요합니다");
            }
            
            if (!currentUser.hasRole(requiredRole)) {
                throw new AccessDeniedException(
                    "권한이 없습니다. 필요한 권한: " + requiredRole
                );
            }
            
            log.info("권한 체크 성공: {} ({})", currentUser.getName(), requiredRole);
        }
    }
    
    // 사용
    @Service
    public class AdminService {
        
        @RequireRole("ADMIN")
        public void deleteUser(Long userId) {
            // ADMIN 권한 있어야만 실행됨
            userRepository.deleteById(userId);
        }
        
        @RequireRole("MANAGER")
        public void updateProduct(Product product) {
            // MANAGER 권한 있어야만 실행됨
            productRepository.save(product);
        }
    }

    파라미터 검증

    @Aspect
    @Component
    @Slf4j
    public class ParameterValidationAspect {
        
        @Before("execution(* com.example.service.*.*(..))")
        public void validateParameters(JoinPoint joinPoint) {
            String methodName = joinPoint.getSignature().getName();
            Object[] args = joinPoint.getArgs();
            
            // 파라미터 null 체크
            for (int i = 0; i < args.length; i++) {
                if (args[i] == null) {
                    log.error("메서드 {}: {}번째 파라미터가 null입니다", methodName, i);
                    throw new IllegalArgumentException(
                        String.format("%s의 %d번째 파라미터가 null입니다", methodName, i)
                    );
                }
            }
            
            // 특정 타입 검증
            for (Object arg : args) {
                if (arg instanceof String) {
                    String strArg = (String) arg;
                    if (strArg.isEmpty()) {
                        log.error("메서드 {}: 빈 문자열이 전달되었습니다", methodName);
                        throw new IllegalArgumentException("빈 문자열은 허용되지 않습니다");
                    }
                }
            }
        }
    }
    Share article
    Contents
    AOPAdvice 종류 예시PointCut 표현식JointPointProceedingJointPoint적용 예시로깅 예시성능 모니터링권한 체크파라미터 검증

    LifeLog, DevLog - https://github.com/KYJTHEYJ

    RSS·Powered by Inblog