AOP (Aspect Oriented Programming)

양성준·2025년 3월 24일

스프링

목록 보기
19/49

AOP

"공통(횡단) 관심사를 모듈화하여 핵심 비즈니스 로직과 분리시키는 것"
OOP가 객체 단위로 코드를 구성한다면, AOP는 관심사 단위로 코드를 구성

  • 모든 계층에 횡단 관심사가 따로 코드로 존재한다면, 중복코드가 늘어나고, 유지보수가 어려워지고, 비즈니스 로직이 모호해진다.
    => 계층 전역을 관통하는 부가 기능 로직(로깅, 트랜잭션, 보안 등)을 '횡단 관심사'로 두고, 이를 모듈화(Aspect)하여 비즈니스 로직과 분리!
  • 용어 정리
    • Aspect : 공통 기능을 모듈화한 단위 (로깅, 캐싱, 보안 등)
    • JoinPoint : Aspect가 적용될 수 있는 지점 (메서드의 실제 실행 시점)
    • Advice : 실제 실행되는 공통 로직 (@Before, @After, @Aroung)
    • Pointcut : Advice가 적용될 JoinPoint를 선별하는 표현식 (적용될 메소드)
    • Weaving : Aspect와 핵심 로직을 결합하는 과정
    • Proxy : AOP의 핵심 기법, 대상 객체를 감싸는 중간 객체로, 메서드 호출 전후에 Advice를 대신 실행 + 원본 객체의 메서드 호출
  • 클라이언트는 Target이 아닌 프록시 객체에 요청을 보냄
    • 프록시는 필요한 Advice를 실행한 후, 실제 비즈니스 로직(Target Object)을 실행함
    • AOP가 적용된 객체는 객체 생성 시점에 원본 객체가 아닌 프록시 객체가 IoC Container에 등록됨 -> 원본 객체의 메소드를 호출 (원본 객체는 등록 X)
  • Spring에서는 @EnableAspectJAutoProxy 설정을 통해 AOP 프록시 생성을 활성화함.
  • 보통은 스프링에서 만들어놓은 AOP Annotation을 가져다가 사용!
    (프록시 생성을 자동으로 설정하므로, @EnableAspectJAutoProxy 필요 X)

실행흐름 예시
1. 클라이언트가 userServiceImpl.registerUser() 호출
2. Spring이 userServiceImpl의 프록시 객체를 먼저 실행
3. 프록시 객체에서 @Before Aspect를 먼저 실행
4. Aspect가 끝난 후 userServiceImpl.registerUser() 호출

사용 예시 (직접 구현)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ServiceCache {
}


@Aspect
@Component
public class ServiceCacheAspect {

    private final Map<String, Object> cache = new ConcurrentHashMap<>();

    @Around("@annotation(com.sprint.mission.springdemo.pr1.cache.ServiceCache)") // ServiceCache라는 어노테이션이 달려있는 메소드에만 Aspect 적용
    public Object cacheResult(ProceedingJoinPoint joinPoint) throws  Throwable {
        String cacheKey = generateKey(joinPoint);

        if (cache.containsKey(cacheKey)) {
            return cache.get(cacheKey);
        }

        Object result = joinPoint.proceed();

        cache.put(cacheKey, result);
        return result;
    }

    // 캐시의 키값을 직접 생성 - 같은 메소드가 같은 인자를 가지면 같은 키값을 가지도록!
    private String generateKey(ProceedingJoinPoint joinPoint) {
        StringBuilder key = new StringBuilder();
        key.append(joinPoint.getSignature().toShortString()); // 메소드 이름을 받아와서 문자열로 바꾸기
        for (Object arg : joinPoint.getArgs()) {
            key.append("-").append(arg.toString()); // 호출하는 인자들도 하나씩 다 붙여줌
        }
        return key.toString();
    }
}

@Service
public class UserServiceImpl implements UserService {
    @Override
    @ServiceCache
    public User getUser(UUID userId) {
        return userRepository.findById(userId);
    }
    }
  • AOP를 직접 만들 때, 일반적으로 적용해야하므로 Annotation을 구현해서 사용! (Annotation이 붙은 곳 어디든 사용 가능)
  • 어떤 메소드에든 호출할 수 있도록 Object 타입을 다룸
  • 제너릭은 객체를 정의할 때 사용하는건데, AOP의 경우 객체를 사용하는 것이므로 제너릭 사용은 어렵다!
  • 키값을 메소드 + 인자로 생성하여, 같은 메소드가 같은 인자를 가지면 같은 키값을 가지도록 함
    => 이 형태로 만들면, 어떤 메소드든 다 적용 가능!
  • ProceedingJoinPoint는 AOP (Aspect-Oriented Programming)에서 Around Advice를 구현할 때 사용되는 객체로,
    원래 실행될 대상 메서드를 감싸고, 실행 여부를 제어할 수 있도록 해주는 기능 / @Around에만 사용 가능
    (원본 객체에 대한 정보가 없으므로, 객체의 메소드를 직접 호출할 수가 없음)
    ex) 여기서는 @ServiceCache를 붙인 getUser 메소드를 감쌈
  • joinPoint.getArgs() - 메소드 호출 파라미터를 가져옴
  • joinPoint.proceed() - 메소드 실행

사용 예시 (Spring에서 제공)

    @Transactional  // Spring에서 제공하는 AOP 어노테이션!
    public void register(String username) {
        userRepository.save(new User(username));
        
        // 일부러 예외 발생시켜보기 (롤백 테스트)
        if (username.equals("error")) {
            throw new RuntimeException("등록 중 에러 발생!");
        }

        pointRepository.save(new Point(username, 100));
    }
  • @Transactional이 붙은 register() 메서드는 프록시 객체에 의해 감싸지고,
    내부적으로 트랜잭션을 시작 -> 메서드 성공 시 커밋 / 실패 시 롤백이 됨
  • Spring이 내부적으로 AOP로 구현한 기능으로, 어노테이션을 통해 그냥 가져다 쓸 수 있다!
 @Service
public class ProductService {

    // 실제로는 JPA repository에서 가져온다고 가정
    @Cacheable("products")  // 캐시 이름: "products"
    public Product getProductById(Long productId) {
        System.out.println("DB 호출: getProductById " + productId);
        return new Product(productId, "상품 " + productId);
    }
}
  • 메서드 실행 전 캐시 조회 -> 캐시가 있다면 저장된 값 반환, 없다면 원본 객체의 메서드를 실행하고, 결과를 캐시에 저장
  • 이후 동일 파라미터로 호출되면 메서드는 실행되지 않고 캐시에서 반환됨

Spring에서 제공하는 AOP 어노테이션

  • @Transactional : 트랜잭션 시작/커밋/롤백 처리
  • @Async : 비동기 작업 처리 (스레드풀 기반 비동기 호출)
  • @Cacheable : 메서드의 결과를 캐시하거나 캐시 무효화 처리
  • @Scheduled : 스케쥴러 등록
  • @PreAuthorize, @PostAuthorize : 메서드 실행 전/후 권한 체크

=> 해당 어노테이션들을 붙이면, 프록시 객체를 만들고 내부 로직에 따라 해당 로직(Advice)을 수행한 후, 원본 객체의 메서드 실행!

참고 - https://velog.io/@kai6666/Spring-Spring-AOP-%EA%B0%9C%EB%85%90

profile
백엔드 개발자를 꿈꿉니다.

0개의 댓글