728x90

AOP(Aspect Oriented Programming)

  • OOP를 보완하는 수단으로, 흩어진 Aspect를 모듈화할 수 있는 프로그래밍 기법

image

  • 동일한 기능이 흩어져 있으면 유지보수하는데 어려움이 존재
  • 각 클래스 내에 흩어진 관심사를 묶어서 모듈화
  • 애플리케이션 전체에 걸쳐 사용되는 기능을 재사용

 

AOP 주요 용어

  • Aspect : 관심사를 모듈화한 것
  • Target : 적용이 되는 대상
  • Advice : 해야할 일(실제 수행되는 코드)
  • Join point : 메서드 실행 시점(Advice를 실제로 실행하고자 하는 위치)
  • Pointcut : 대상 내에 어디에 적용이 되어야 하는지에 대한 정보(Join point를 선정하는 방법)
  • Weaving : Aspect가 target에 적용되는 전체적인 과정, PointCut으로 지정된 JoinPoint에 Advice가 적용되어 Target을 호출 시 AOP Proxy가 만들어지는 과정

image

AOP 구현체

 

AOP 적용 방법

  • 컴파일
  • 로드 타임
  • 런타임

 

Spring AOP가 사용하는 방법 → 런타임 (Dynamic Proxy 기법으로 구현)

  • A라는 Class 타입의 Bean을 생성할 때, A 타입의 Proxy Bean을 생성
  • AOP가 적용된 Target 메서드를 호출 할 때 실제 메서드가 호출되는 것이 아니라 Advice가 요청을 대신 랩핑(Wrraping) 클래스로써 받고 그 랩핑 클래스가 Target을 호출



Spring AOP : Proxy 기반 AOP

 

Spring AOP의 특징

  • 프록시 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용할 수 있음
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라 스프링 IoC와 연동

 

프록시 패턴

image

  • 프록시 패턴을 적용하는 이유 : 기존 코드 변경없이 접근 제어 또는 부가 기능 추가

 

만약 성능(시간)을 측정하는 기능을 추가해야 한다면?

@Service
public class EventServiceImpl implements EventService {

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");

        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();

        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Published an event");

        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
        System.out.println("Deleted an event");
    }
}
  • 성능(시간)을 측정하는 부가적인 기능을 추가할 경우 기존 코드를 수정해야 하는 상황이 발생
  • 프록시 패턴을 이용하여 기존 클래스를 두고 프록시 클래스를 작성하여 기능을 위임하고 부가적인 기능을 작성할 수 있음
@Primary
@Service
@RequiredArgsConstructor
public class ProxyEventServiceImpl implements EventService {

    private final EventServiceImpl eventServiceImpl;

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();
        eventServiceImpl.createEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();
        eventServiceImpl.publishEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
        eventServiceImpl.deleteEvent();
    }
}
  • 프록시 클래스를 활용하여 기능을 위임
  • 하지만 이 경우에 모든 클래스에 대해 프록시 클래스를 작성해야 하는 비용이 발생
  • 추가로 프록시 클래스 내에도 동일한 기능에 대한 중복 코드가 발생하고 다른 클래스에서 재사용이 어려움

 

Spring AOP 등장 배경

 

Proxy Bean 생성 과정

  1. 원본 클래스의 Bean이 등록
  2. AbstractAutoProxyCreator을 통해 원본 클래스를 감싸는 Proxy Bean을 생성
  3. Proxy Bean을 원본 클래스의 Bean 대신에 등록

 

@AOP

  • 애노테이션 기반의 스프링 AOP

 

의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

 

Aspect 정의 : @Aspect

  • Bean으로 등록해야 하므로 (컴포넌트 스캔을 사용한다면) @Componet도 추가

 

PointCut 정의

 

execution

  • ex. @Around("execution(* com.example..*.EventService.*(..))")
  • execution은 기존 코드를 완전히 건드리지 않고 aspect 내에 작성된 표현식으로 기능을 수행할 수 있음
  • 하지만 pointcut 조합이 어려움
    • ex. &&, ||, !

 

@annotation

  • ex. @Around("@annotation(PerfLogging)")
  • 애노테이션 내 @Retention
    • RetentionPolicy.CLASS : 애노테이션 정보가 바이트 코드까지 남아 있음 (default)
    • RetentionPolicy.SOURCE : 컴파일 후에 사라짐
    • RetentionPolicy.RUNTIME : 런타임까지 유지 (굳이 할 필요 없음)
  • 애노테이션을 정의하고, 해당 애노테이션을 원하는 메소드에 추가

 

Example

// aspect
@Aspect
@Component
public class PerfAspect {

    // @Around("execution(* com.example..*.EventService.*(..))") // execution
    @Around("@annotation(PerfLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}

// interface
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerfLogging {
}

// service
@Service
public class EventServiceImpl implements EventService {

    @PerfLogging
    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    ...
}



cf. pointcut 관련

 

참고

728x90

'Spring' 카테고리의 다른 글

[Spring] @Cacheable, @CachePut, @CacheEvict  (0) 2024.08.17
728x90

Overview

캐시는 서버의 부담을 줄이고, 성능을 높이기 위해 사용되는 기술이다

  • 예를 들어 어떤 요청을 처리하는데 계산이 복잡하거나 혹은 DB에서 조회하는게 오래 걸리는 등에 적용하여 결과를 저장해두고 가져옴으로써 빠르게 처리할 수 있다.
    • 캐시는 값을 저장해두고 불러오기 때문에 반복적으로 동일한 결과를 반환하는 경우에 용이하다.
  • 만약 매번 다른 결과를 돌려줘야 하는 상황에 캐시를 적용한다면 오히려 성능이 떨어지게 된다.
    • 오히려 캐시에 저장하거나 캐시를 확인하는 작업 때문에 부하가 생기기 때문이다.
  • 그러므로 캐시는 동일한 결과를 반환하는 반복적인 작업과 시간이 오래 걸려서 서버(애플리케이션)에 부담이 되는 경우에 적용하면 좋다.

스프링은 AOP 방식으로 편리하게 메소드에 캐시를 적용하는 기능을 제공하고 있다.

 

 

Spring에서 캐시 적용하기

스프링에서는 `@Cacheable`, `@CachePut`, `@CacheEvict`어노테이션을를 활용해서 캐시를 적용할 수 있다.

 

@EnableCaching

@Cacheable과 같은 어노테이션 기반의 캐시 기능을 사용하기 위해서 `@EnableCaching`를 추가한다.

@EnableCaching
@Configuration
public class CacheConfig {
    ...
}

 

캐시를 관리해줄 CacheManager를 Bean으로 등록한다.

 

스프링 공식 문서 - 캐시 추상화 (Cache Abstarction)에서 현재 지원되는 다양한 매니저의 종류를 확인할 수 있다.

 

 

@Cacheable

캐시를 저장/조회를 설정한다.

  • 보통 메소드에 적용한다.
    • 클래스나 인터페이스에 지정할 수도 있지만 그런 경우는 극히 적다.
  • 캐시에 데이터가 없을 경우에는 기존의 로직을 실행한 후에 캐시에 데이터를 추가한다.
  • 캐시에 데이터가 있으면 캐시의 데이터를 반환한다.

메소드의 파라미터가 1개인 경우

@Cacheable("todayWebtoon")
public Webtoon getTodayWebtoon(String webtoonNo) {
    // logic
}

 

파라미터로 넘어온 `webtoonId`를 기준으로 `todayWebtoon` 캐시에서 값을 조회한다.

  • 값이 없으면 logic을 실행하고, 반환 값을 저장한다.
  • 값이 있으면 저장된 값을 반환한다.

만약 파라미터가 없는 경우 디폴트 값을 key로 활용하면 된다.

 

메소드의 파라미터가 여러 개인 경우

@Cacheable(value = "todayWebtoon", key = "webtoonNo")
public Webtoon getTodayWebtoon(String webtoonNo, Date date) {
    // logic
}

 

파라미터가 여러 개인 경우 key를 지정해준다.


메소드의 파라미터가 객체인 경우

@Cacheable(value = "todayWebtoon", key = "#webtoon.webtoonNo")
public Webtoon getTodayWebtoon(Webtoon webtoon, Date date) {
    // logic
}

 

Key값의 지정에는 SpEL이 사용된다. 그렇기 때문에 만약 파라미터가 객체라면 다음과 같이 하위 속성에 접근하면 된다.

 

 

@CachePut

캐시에 값을 저장하는 용도로만 사용된다.

@CachePut(value = "todayWebtoon", key = "webtoonNo")
public Webtoon updateWebtoon(String webtoonNo) {
    // logic
}

 

실행 결과를 캐시에 저장하지만, 조회 시에 저장된 캐시의 내용을 사용하지는 않고 항상 메소드의 로직을 실행한다.

 

@CacheEvict

캐시를 제거하는 용도로 사용된다.

 

만약 값이 달라지거나 현재 저장된 값이 무의미한 값이라면 제거되어야 한다.

 

캐시를 제거하기 위해서는 크게 두 가지 방법이 존재한다.

  • 일정한 주기로 캐시를 제거
  • 값이 변할 때 캐시를 제거

예를 들어 하루에 한 번씩 바뀌는 정보라면 batch 또는 scheduled 기능을 이용하여 일정 시간 주기로 캐시를 삭제한다.


특정 캐시 내의 값을 모두 제거하는 경우

@CacheEvict(value = "todayWebtoon", allEntries = true)
public void clearTodayWebtoon() {
    // logic    
}

 


캐시 내 특정 값만 제거하는 경우

@CacheEvict(value = "todayWebtoon", key = "webtoon.webtoonNo")
public void clearWebtoon(Webtoon webtoon) {
    // logic    
}

 

728x90

'Spring' 카테고리의 다른 글

[Spring] AOP (Aspect Oriented Programming)  (0) 2024.08.28

+ Recent posts