본문 바로가기

Language/Spring

[AOP] AOP란?

728x90
AOP(Aspect Oriented Programming)의 개념

  • 관점 지향 프로그래밍
  • 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나눠보고 그 관점을 기준으로 각각 모듈화
    • 핵심적인 관점 : 개발자가 적용하고자 하는 핵심 로직
    • 부가적인 관점 : 핵심 로직을 수행하기 위해 필요한 DB 연결(JDBC), 로깅, 파일 입출력

 

AOP의 등장 배경
  • 회사 상사가 회원 가입 하는 시간을 측정하는 로그를 남겨 달라고 지시
코드 수정 전
public void join(JoinRequest joinRequest) {
	memberRepository.save(joinRequest.toMember());
}
코드 수정 후
public void join(JoinRequest joinRequest) {
	long begin = System.currentTimeMillis();
	try{
		memberRepository.save(joinRequest.toMember());
	} finally {
		log.info("join spent {} ms", System.currentTimeMillis() - begin);
	}
}
  • 이후 상사가 시간을 측정하는 코드를 모든 Service에 적용하라고 지시하였는데, 작업 중 공통적인 코드를 발견하게 됨
회원 가입 코드
public void join(JoinRequest joinRequest) {
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	try{
		memberRepository.save(joinRequest.toMember());
	} finally {
		stopWatch.stop();
		log.info("join spent {} ms", stopWatch.getLastTaskTimeMillis());
	}
}
로그인 코드
public void join(LoginRequest loginRequest) {
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	try{
		memberRepository.findByUsername(LoginRequest.getUsername());
		...
	} finally {
		stopWatch.stop();
		log.info("join spent {} ms", stopWatch.getLastTaskTimeMillis());
	}
}
  • 핵심 로직
    • try{} 구문 안에 있는 로직
    • Ex) memberRepository.save(), memberRepository.findByUsername()
  • 부가 기능(로깅)
    • try{} 구문을 제외한 나머지
    • Ex) StopWatch, finally{} 구문
AOP의 필요성
  • 공통적인 부가 기능을 여러 메서드에 반복적으로 작성하는 것은 코드 중복을 초래
  • AOP는 이러한 공통 부가 기능(예: 로깅, 성능 측정 등)을 핵심 로직에서 분리하여 관리하기 위해 등장한 개념
  • AOP를 활용하면 코드 중복 없이 공통 로직을 모듈화하여 적용 가능
  • 따라서, AOP를 적용하면 이와 같은 부가 기능을 별도로 관리하고, 필요한 비즈니스 로직에 공통 부가 기능을 쉽게 적용할 수 있어 유지보수성과 가독성이 크게 향상

AOP의 용어
  • Aspect
    • 공통적으로 쓰이는 부가 기능의 코드를 모듈화
      Ex) 로깅, 트랙잭션 관리, 예외 처리
  • Target
    • Aspect가 적용 되는 대상 (Class, Method 등)
    • 특정 기능이 부여될 코드
  • Advice
    • Aspect 기능에 대한 실제 구현체
      → 어떤 부가 기능인지 정의
    • 다양한 어노테이션을 사용하여 특정 시점에 Advice가 실행되도록 지정 가능
      • @Before : 타겟 메서드가 호출되기 전에 실행
      • @After : 타겟 메서드가 종료된 후(결과에 상관없이) 실행
      • @AfterReturning : 타겟 메서드가 성공적으로 리턴 된 후 실행
      • @AfterThrowing : 타겟 메서드 수행 중 예외가 발생한 경우 실행
      • @Around : 타겟 메서드의 호출 전과 후에 모두 실행
  • Join Point
    • Adivce가 Target에 적용될 수 있는 지점
      Ex) 메서드 호출, 생성자 호출, 필드 접근 등
    • 스프링 AOP는 메서드 실행 시점만 적용
  • Point Cut
    • Advice가 적용될 Join Point의 조건을 정의
      → 즉, 어떤 메서드에 Advice를 적용할지 상세하게 설정 가능
    • 메서드 이름 패턴이나 특정 패키지의 메서드에만 적용하도록 설정 가능
  • Proxy
    • 클라이언트와 실제 타겟 객체 사이에 존재하는 중개 객체
    • 클라이언트는 Proxy를 통해 타겟 메서드를 호출하게 되며, Proxy는 그 과정에서 부가 기능을 수행
    • Spring에서는 DI를 통해 Proxy 객체가 주입
      → DI를 통해 타겟 대신 클라이언트에게 주입되며 클라이언트의 메소드 호출을 대신 받아서 타겟에 위임하며 이 과정에서 부가 기능을 부여

 

다양한 AOP 적용 방법
  • AspectJ : 컴파일 단계나 바이트코드 조작을 통해 AOP를 구현하는 대표적인 프레임워크
  • Spring AOP : 프록시 패턴을 이용AOP 기능을 제공하는 프레임워크
컴파일 시점 (컴파일 타임 위빙)
  • A.java -- (AOP) → A.class(AspectJ)
    • A.java 파일을 컴파일하면서 AspectJ가 AOP 코드를 삽입하여 A.class 파일에 부가 기능을 포함
  • 즉, 컴파일 단계에서 부가 기능이 클래스에 삽입되기 때문에 이후 코드 수정 없이 바로 실행
바이트코드 조작 (로드 타임 위빙)
  • A.java → A.class -- (AOP) → 메모리(AspectJ)
  • A.java 파일이 일반적으로 컴파일된 후 AspectJ가 A.class 파일을 메모리에 로드할 때 바이트코드를 수정하여 부가 기능을 삽입
  • 클래스 로더를 통해 클래스가 JVM에 로드되는 시점을 가로채서 바이트코드를 조작하며, .java 파일과 최종 실행되는 .class 파일 내용이 달라질 수 있음

이 방법을 사용하는 이유

  1. 컨테이너 독립성
    • DI 컨테이너나 Spring을 사용하지 않는 환경에서도 AOP 적용이 가능
  2. 더 강력하고 유연한 AOP
    • 프록시 방식보다 더 다양한 곳에 부가 기능을 삽입 가능
    • 메서드 호출뿐만 아니라 생성자, 필드 접근, 정적 초기화 등에도 적용 가능
프록시 패턴 (Spring AOP)

 

  • 프록시의 역할
    • 클라이언트와 타겟 객체 사이에 위치하여 타겟 객체에 대한 메서드 호출을 가로채고, 메서드 호출 전후로 부가 기능 추가
    • Spring DI 컨테이너를 통해 프록시 객체를 빈으로 등록하고 주입하여 실행
  • 프록시 방식의 장점
    • 스프링 컨테이너와 JDK의 다이나믹 프록시 혹은 CGLIB 프록시를 통해 쉽게 AOP를 구현
    • 특별한 환경이나 추가 기술이 필요하지 않으며, Spring 컨테이너에 의존해 동작하기 때문에 Spring 환경에서 빠르게 적용 가능
  • 제한
    • 프록시는 메서드 호출을 대상으로 하므로 메서드 이외의 요소(예: 필드 접근, 생성자 호출 등)에는 AOP를 적용 불가
    • 이와 같은 제한으로 인해 유연성은 AspectJ의 바이트코드 조작 방식보다 낮지만, 필요한 곳에만 AOP를 빠르게 적용하기에는 충분

 

 

 

 

 

AOP를 사용하지 않는 기본적인 시간 측정 Bean(ExampleService) 구현
인터페이스 구현
public interface ExampleService { 
    public void start();
    public void process();
    public void end(); 
}
구현체(ExampleServiceImpl) 구현
@Component
public class ExampleServiceImpl implements ExampleService {
    @Override
    public void start() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try{
            Thread.sleep(1000);
            System.out.println("start");
        }catch(Exception e){
            e.printStackTrace();
        }
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
    }

    @Override
    public void process() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try{
            Thread.sleep(1000);
            System.out.println("processing");
        }catch(Exception e){
            e.printStackTrace();
        }
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
    }

    @Override
    public void end() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try{
            Thread.sleep(1000);
            System.out.println("ended");
        }catch(Exception e){
            e.printStackTrace();
        }
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
    }
}
테스트 코드
class ExampleServiceImplTest {
    @Test
    public void exampleTest() throws Exception {
        ExampleService service = new ExampleServiceImpl();
        service.start();
        service.process();
        service.end();
    }
}

 

결과


AOP를 이용해 시간측정 Bean(ExampleService) 개선 - Execution expression
공통 모듈 분석
  • 이전 코드(ExampleServiceImpl)에서 StopWatch를 이용해 성능 측정을 하는부분이 start, process, end 메서드에 모두 포함 (공통 코드)
  • 따라서 해당 코드를 공통 모듈로 묶어서 정의 가능
공통 모듈 추출 및 구현
@Component
@Aspect
public class PerfAspect {
    // io.security.corespringsecurity.aopsecurity.ExampleService 클래스내의 모든 메서드에 공통 모듈을 적용
    @Around(value = "execution(* io.security.corespringsecurity.aopsecurity.ExampleService.*(..))")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object retVal = pjp.proceed();
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
        return retVal;
    }
}

 

  • @Component : Spring이 이 클래스를 빈으로 관리하도록 지정
  • @Aspect : 이 클래스가 AOP의 Aspect임을 나타냄
  • @Around(value = "execution(* io.security.corespringsecurity.aopsecurity.ExampleService.*(..))")
    • @Around : 이 Advice는 대상 메서드의 실행 전과 후에 실행
    • execution : 이 Pointcut 표현식은 메서드의 실행 시점을 잡아내는 데 사용
    • 표현식 구성
      • * : 반환 타입 지정, 여기서는 *이므로 모든 반환 타입을 대상
      • io.security.corespringsecurity.aopsecurity.ExampleService : 특정 패키지와 클래스 경로, 이 클래스의 모든 메서드에 AOP가 적용
      • * : 메서드명 지정, *로 되어 있어 ExampleService 클래스의 모든 메서드가 포함
      • ( .. ) : 메서드의 파라미터를 지정, ( .. )은 모든 타입과 개수의 파라미터를 허용한다는 의미
    • 의미 : 이 코드는 ExampleService 클래스의 모든 메서드가 실행될 때마다, 해당 메서드 실행 전후로 @Around Advice가 적용

 

구현체 공통 모듈 삭제
@Component
public class ExampleServiceImpl implements ExampleService {
    @Override
    public void start() {
        try {
            Thread.sleep(1000);
            System.out.println("start");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void process() {
        try {
            Thread.sleep(1000);
            System.out.println("processing");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void end() {
        try {
            Thread.sleep(1000);
            System.out.println("ended");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
테스트 코드
@SpringBootTest
class ExampleServiceImplTest {
    @Autowired
    ExampleService service;

    @Test
    public void exampleTest() throws Exception {
        service.start();
        service.process();
        service.end();
    }
}
  • 이전 코드와 차이점
    • @Aspect 를 적용하기 위해 @SpringBootTest 어노테이션을 추가해서 해당 @Aspect를 등록해주고 ExampleService DI

AOP를 이용해 시간측정 Bean(ExampleService) 개선 - Annotation
  • 구현체의 특정 메서드들만 부가기능을 부여하기 위해 기존 Execution expression 방식을 쓰면 표현식을 다 작성해야하기 때문에 번거로움
  • 따라서 Annotation을 이용하여 편하게 적용
어노테이션 구현

 

  • @Documented
    • 이 어노테이션을 사용하는 메서드의 문서화에 포함될 수 있도록 지정
  • @Target(ElementType.METHOD)
    • 이 어노테이션은 메서드에만 적용할 수 있도록 제한
  • @Retention(RetentionPolicy.CLASS)
    • 이 어노테이션 정보는 컴파일된 클래스 파일에 포함되지만, 런타임 시에는 JVM에 로드되지 않음
    • 런타임에 사용하려면 RetentionPolicy.RUNTIME으로 설정

 

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

 

Aspect 클래스 수정 (Execution Expression → Annotation)
@Component
@Aspect
public class PerfAspect {
    @Around("@annotation(PerfLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object retVal = pjp.proceed();
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
        return retVal;
    }
}

 

  • @Around("@annotation(PerfLogging)")
    • @PerfLogging 어노테이션이 적용된 메서드를 대상으로 AOP Advice를 적용하겠다는 의미
  • logPerf 메서드
    • ProceedingJoinPoint pjp : 대상 메서드를 호출할 수 있는 객체, 이를 통해 원래 메서드를 실행하면서 부가 기능 추가
    • StopWatch : 메서드 실행 시간을 측정하기 위해 StopWatch를 사용
    • pjp.proceed() : 원래의 대상 메서드를 호출
    • stopWatch.prettyPrint() : 메서드의 실행 시간 측정 결과를 출력
구현체(ExampleServiceImpl)에 어노테이션 추가
@Component
public class ExampleServiceImpl implements ExampleService {
    @PerfLogging
    @Override
    public void start() {
        try {
            Thread.sleep(1000);
            System.out.println("start");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @PerfLogging
    @Override
    public void process() {
        try {
            Thread.sleep(1000);
            System.out.println("processing");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void end() {
        try {
            Thread.sleep(1000);
            System.out.println("ended");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

  • 동작 방식
    1. AOP 설정 확인
      • PerfAspect 클래스에 정의된 @Around("@annotation(PerfLogging)") 어드바이스@PerfLogging 어노테이션이 적용된 메서드들을 대상으로 함
    2. @PerfLogging이 적용된 메서드 찾기
      • @PerfLogging 어노테이션이 붙은 start와 process 메서드PerfAspect의 logPerf 메서드에 의해 감싸짐
      • end 메서드에는 @PerfLogging이 없기 때문에, AOP가 적용되지 않음
    3. 메서드 호출 시 AOP 실행
      • start나 process 메서드를 호출하면 AOP 프레임워크는 해당 메서드를 직접 실행하지 않고 logPerf 어드바이스 메서드를 먼저 실행
    4. 성능 측정
      • logPerf 메서드에서 StopWatch를 사용해 메서드가 시작하는 시점부터 끝나는 시점까지의 시간을 측정
      • ProceedingJoinPoint의 pjp.proceed() 호출원래 메서드(start 또는 process)가 실행
      • 메서드가 완료되면 StopWatch를 종료하고, 소요 시간을 로그로 출력

Ex) exampleService.start();

 

  1. logPerf 메서드가 start 메서드를 감싸고 실행
  2. StopWatch가 시작되고 start 메서드가 실행
  3. start 메서드는 1초 대기 후 "start"를 출력하고 종료
  4. StopWatch가 중지되고, 실행 시간을 출력
실행 결과

 


 

 

 

 

AOP를 이용해 시간측정 Bean(ExampleService) 개선 - Bean
@Component
@Aspect
public class PerfAspect {
    @Around("bean(exampleServiceImpl)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object retVal = pjp.proceed();
        stopWatch.stop();
        System.out.println(stopWatch.prettyPrint());
        return retVal;
    }
}
bean(beanName)
  • bean()은 AOP의 포인트컷 표현식 중 하나로, 특정 이름의 빈에 대해서만 Aspect를 적용하도록 지정
  • exampleServiceImpl이라는 이름의 빈에 대해 logPerf 메서드를 실행하도록 설정
  • 이 빈의 모든 메서드에 대해 AOP가 적용

 

참고 자료

https://hyunki99.tistory.com/69

 

[Spring] 자바 스프링 AOP란?, AOP 개념 정리 (프록시, AspectJ)

https://www.youtube.com/watch?v=Hm0w_9ngDpM 우아한테크코스 제이님의 AOP 테코톡을 정리해 봤습니다. AOP는 Aspect-Oriented-Programming의 약자로 관점 지향 프로그래밍이라는 뜻입니다. 이름만 봐서는, OOP(객체 지

hyunki99.tistory.com

https://catsbi.oopy.io/fb62f86a-44d2-48e7-bb9d-8b937577c86c

 

AOP(Aspect Oriented Programming)

1. 개요

catsbi.oopy.io

 

728x90