공부하고 기록하는, 경제학과 출신 개발자의 노트

학습일지/Language

Spring AOP 요약

inspirit941 2020. 12. 7. 19:50
반응형

www.inflearn.com/course/spring-framework_core#

 

스프링 프레임워크 핵심 기술 - 인프런

이번 강좌는 스프링 부트를 사용하며 스프링 핵심 기술을 학습합니다 따라서 스프링 부트 기반의 프로젝트를 사용하고 있는 개발자 또는 학생에게 유용한 스프링 강좌입니다. 중급이상 웹 개발

www.inflearn.com

Spring 핵심기술의 AOP 부분 정리.

 

Spring AOP

스프링 AOP 구현체 제공, 자바에서 제공하는 구현체 (aspect-j) 와 연동. 스프링 txn, 캐시 등 여러 기능에 적용됨

흩어진 Aspect를 모듈화할 수 있는 프로그래밍 기법을 의미함. OOP의 보완관계.

스크린샷 2020-12-07 오후 1 37 07

여러 클래스에 걸쳐서 비슷한 코드를 사용해야 하는 경우

  • Transaction 처리. setAutoCommit false 설정 -> 쿼리 생성 후 실행 -> commit or 롤백. 이런 코드를 기존 서비스코드에 감싸는 형태로 사용함
  • 성능관련 로깅. Class A / Class C의 특정 부분에 로깅 메소드를 적용해야 할 경우, 비슷한 코드가 두 군데에 사용되는 식.

이렇게 각각 Concern마다 코드변경이 일어남. 만약 이렇게 된 코드에서 수정이 필요할 경우, 로직이 적용된 각각의 클래스를 전부 찾아서 변경해줘야 한다. (로그 로직 변경 -> 로깅이 적용된 모든 영역을 찾아가서 일일이 코드변경을 해야 함)

따라서 AOP는 Aspect라는 개념을 적용해 해결. 필요한 로직을 Aspect로 정의하고, 해당 로직이 어느 클래스에 적용되어야 하는지 등록함.

AOP 용어

  • Aspect - 일종의 모듈. 해당 모듈에 Advice / Pointcut 정보가 있음
    • Advice: 해야 할 일 (로직)
    • PointCut: 어디에 적용될 것인지.
  • target - 적용될 대상 클래스 (위 사진의 경우 A, B, C)
  • join Point: advice가 실행되는 시점이 언제인지. (언제 끼워넣을 것인지)
    • method 실행시점 - 가장 많이 사용됨.
    • 생성자 호출 직전, 필드에서 값을 가져갔을 때 등등.... 여러 분기점이 있음. join point의 subset이 pointcut이라고 이해해도 됨.

AOP 구현체 - 자바

  • AspectJ : 다양한 기능 제공
  • Spring AOP : 상대적으로 국한된 기능.

AOP의 적용방법

  • Compile Time : 자바파일을 클래스 파일로 변경할 때 적용. advice가 포함된 형태로 바이트코드를 생성하는 방식.
  • Loading Time : 클래스는 변경 없이 컴파일 완료, 클래스 파일을 로딩하는 시점 (jvm 메모리에 올라가는 시점)에 advice가 같이 올라가는 것.
  • Runtime : Spring AOP가 사용하는 방법. 스프링 내에서 A라는 Bean을 생성할 때.
    • A라는 타입의 Proxy Bean를 생성
    • 프록시 bean이 해당 메소드를 호출하기 직전에 aspect의 advice 실행.

Spring AOP : 프록시 기반 AOP.

Spring AOP는

  • 프록시 기반의 AOP 구현체
  • 스프링 Bean에만 AOP를 적용할 수 있음.
  • 모든 AOP기능을 제공하는 게 목적이 아니라, 스프링 IoC와 연동해서 엔터프라이즈 애플리케이션에서 가장 흔한 문제를 해결하는 게 목적임

프록시 패턴

  • 기존 코드 변경 없이 접근제어 or 부가 기능을 추가할 수 있는 기능
public interface EventService {
    void createEvent();
    void publishEvent();
    void deleteEvent();
}

@Service
public class SimpleEventService implements EventService {

    @Override
    public void createEvent() {
        Thread.sleep(1000);
        System.out.println("create Event fin");
    }
    @Override
    public void publishEvent() {
        Thread.sleep(2000);
        System.out.println("publish Event fin");
    }
    @Override
    public void deleteEvent() {
        System.out.println("delete Event fin");
    }
}

@Component
public class AppRunner implements AppllicationRunner {
    @Autowired
    EventService eventService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        eventService.createEvent();
        eventService.publishEvent();
        eventService.deleteEvent();
    }
}

이렇게 된 상황에서, service의 createEvent / publishEvent에는 코드 실행시간 측정로직을 추가하고 싶은 경우.

proxy를 담당할 객체를 생성한다.

@Primary // 동일한 타입 bean이 있을 경우 우선순위로 설정.
@Service
public class ProxySimpleEventService implements EventService {
    // 프록시가 실제로 접근할 클래스 - 예시의 경우 SimpleEventService 객체 - 를 주입받는다.
    // EventService 타입을 주입받아도, 사용할 객체 이름이 simpleEventService일 경우 괜찮음
    @Autowired
    SimpleEventService simpleEventService


// 여기서 필요한 로직들을 추가로 구현하는 식.
    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();
        // delegation. 실제 클래스에서 필요한 로직은 불러온다.
        simpleEventService.createEvent();
        System.out.println(System.currentTimeMillis() - begin);
    }
    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();

        simpleEventService.publishEvent();

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

    }
    @Override
    public void deleteEvent() {
        simpleEventService.deleteeEvent();
    }

}

단, 이렇게 만들 경우

  • 프록시 내에서도 중복코드 발생
  • 매번 이렇게 프록시를 생성해야 하는가? 모든 메소드의 delegation은?
  • 여기 말고도 다른 클래스에도 로직을 적용하고 싶다면, 클래스마다 프록시 생성?

그래서 등장한 게 Spring AOP.

  • 스프링 IoC 컨테이너가 제공하는 기반시설 + Dynamic 프록시 사용.

  • Dynamic Proxy : 동적으로 프록시 객체를 생성하는 방법.

    • 자바는 인터페이스 기반 프록시 생성을 지원함.
    • CGlib은 클래스 기반 프록시도 지원.
  • Spring IoC : BeanPostProcessor가 있다. Bean 등록이 끝난 후에 특정 작업을 수행할 수 있는 인터페이스. 여기서 기존 Bean을 대체하는 Dynamic Proxy Bean을 만들어 등록한다.

즉, 위에서 한 것처럼 Proxy 객체를 일일이 개발자가 생성하는 대신 동적으로 프록시 객체를 생성하며, 동적으로 생성된 Bean을 스프링 IoC에서 Bean으로 등록하는 것.

@AOP

어노테이션 기반의 스프링 @AOP.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

로 디펜던시 추가.

// Aspect 정의 후 Bean으로 등록
@Component
@Aspect
public class PerfAspect {
    // 두 가지 정보가 필요함 - advice, pointcut


    // Advice의 적용타입 정의 어노테이션.
    // pointcut 이름을 부여하거나 pointcut 자체를 정의해 사용.
    // around : 실행할 메소드를 감싼 형태로 적용한다는 의미. 다용도로 쓰일 수 있다.
    @Around("execution(* com.inspirit941..*.EventService.*(..))")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        // advice 영역.
        long begin = System.currentTimeMillis();
        // > 원래 프로세스 실행
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}

단, 위와 같이 execution으로 정의하면 해당 클래스 내의 모든 메소드에 적용된다. deleteEvent 메소드에는 AOP를 적용하고 싶지 않을 경우?

-> annotation을 정의해 처리.

@Retention(RetentionPolicy.CLASS) // 컴파일한 클래스 파일에도 이 annotation 정보가 있어야 한다는 의미. (source로 변경하면 컴파일 시 값이 사라짐.)
@Target(ElementType.METHOD)
@Documented // 문서화하려면 사용
public @interface PerLogging {

}

// 이 어노테이션을, 로그를 붙이고 싶은 컨트롤러 메소드 위에 추가한다.

// Aspect에서는 아래와 같이 코드를 변경함. 이 편이 좀더 유용하다.
@Component
@Aspect
public class PerfAspect {

    @Around("@annotation(PerLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        // advice 영역.
        long begin = System.currentTimeMillis();
        // > 원래 프로세스 실행
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }

    // 메소드 실행 전에만 뭐 하면 된다 = before
    @Before("bean(클래스명)")
    public void hello() {
        System.out.println("before");
    }
}

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts

Null Safety

Spring 5에서 추가된 기능. 컴파일 타임에 최대한 nullpointerException을 막기 위해.

  • NonNull
  • Nullable
  • NonNullApi(패키지 레벨 설정)
  • NonNullFields(패키지 레벨 설정)
public class DummyService {
    @NonNull // 리턴 시 Null 허용 x
    public String createEvent(@NonNull String name) { // 입력값에 Null 허용 x
        return "hello " + name;
    }
}

IDE에 실제로 이 설정이 효과를 내도록 하려면

  • Preference -> Compiler에 보면 Add runtime assertion for notnull-annotated methods and parameters 체크박스에 체크
  • Configure annotation 버튼 클릭해서 스프링 관련 어노테이션을 추가해주면 된다. nullable, nonnull 등등.

이러고 IDE 껐다 켜면 적용됨

"패키지 파일" 위에 @NonNullApi를 등록하기도. 이러면 패키지 파일의 모든 파라미터 / 리턴타입에 nonNull 설정한 것과 동일함.

  • 필요한 곳에만 nullable을 붙이는 식으로 작업할 수 있음.
@NonNullApi
package com.inspirit941.test;
반응형