스프링 - AOP

Chooooo·2022년 8월 16일
0
post-thumbnail

이번 강의는 AOP에 대해서 학습할 것이다.

AOP

AOP(Aspect Oriented Programming)은 공통된 로직이나 기능을 모듈화하는 것이다.
(OOP(Objected Oriented Programming, 객체지향 프로그래밍)에서 좀 더 나아가 AOP를 적용하면서 핵심 관심 사항과 공통 관심 사항을 분리하며, 공통된 부분을 재사용할 수 있도록!)


AOP가 필요한 상황

백엔드 구현에서의 관심사항은 두 가지 사항으로 나뉜다.
공통 관심사항핵심 관심사항으로 나뉘는데 회원 가입, 회원 조회와 같은 비즈니스 로직과 같은 경우핵심 관심사항이고, 이러한 기능 외에 부가 기능 로직들이 공통 관심 사항이다. (ex 걸리는 시간을 측정하는 기능)

모든 메서드에 대한 호출 시간을 측정하고 싶을 때, 기존 모든 메서드의 시작과 끝에 시간을 측정하는 코드를 추가하여 이를 구현해야 한다.

회원 가입과 회원 조회의 기능의 호출 시간을 구하기 위해서는 회원 서비스에 아래 코드 작성

@Transactional
public class MemberService {
    private final MemberRepository memberRepository;

    public  MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    /**
     * 회원가입
     */
    public Long join(Member member){
        
        long start = System.currentTimeMillis();
        
        try {   //같은 이름인 중복 회원 No
            validateDuplicateMember(member);
            memberRepository.save(member);
            return member.getId();
        } finally{    //예외처리 발생 여부를 떠나서 실행하는 코드 
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("join " + timeMs + "ms");
        }
    }

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers(){
        
        long start = System.currentTimeMillis();
        
        try {
            return memberRepository.findAll();
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println(timeMs);
        }
    }
}

메서드 시작 전 start로 시간을 받고 finally부분에서 회원가입 or 회원조회에 실패하더라도 시간을 잴 수 있게 메서드가 끝난 시점 시간을 재 두 시간의 차로 호출에 걸리는 시간을 측정한다.
System.currentTimeMills()를 이용하여 시작과 끝의 시간을 측정하며 로직이 실행되는 시간을 구한다.
(통합 테스트를 수행하여 동작 확인 가능)

테스트가 정상적으로 수행되고, 호출 시간 역시 제대로 나온다. 위와 같은 방법으로 메서드에 코드를 추가하여 호출 시간을 구할 수 있지만 여기에서 문제가 발생한다.

*문제점

회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다(공통관심사항이잖아).
시간을 측정하는 로직은 공통 관심 사항이다.
시간을 측정하는 로직(공통관심사항)과 핵심 비즈니스의 로직(핵심관심사항)이 섞여서 유지보수가 어렵다. (문제점)
시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다. (문제점)
시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다

이러한 문제점 속에서 AOP의 필요성을 느끼게 된다!!!


AOP 적용

AOP(Aspect Oriented Programming)란, 위에서 말한 것처럼 시간을 측정하는 로직(공통 관심사항)과 비즈니스 로직(핵심 관심사항)으로 분리하여 개발하는 기술

위에서 각 메서드의 시간을 측정하는 코드를 작성할 때 각각의 메서드에 코드를 입력하는 방식으로 개발하였다면, AOP를 사용하면 시간을 측정하는 코드를 따로 분리하여 개발할 수 있다!!!

AOP 사용 x (AOP가 필요한 상황)

컨트롤러, 서비스, 리포지토리에서 각각 메서드를 호출할 때 시간 측정을 해야 하므로 시간 측정 로직이 각 과정에 작성되어야 했다. (모든 로직에... --> 문제점)


AOP 적용

시간 측정 로직(공통 관심사항)을 분리하여 로직을 적용하기를 원하는 곳에만 적용할 수 있게 됐어.

이제 AOP를 사용하여 로직 분리하자
(기존의 memeberService에는 핵심 관심 사항만 남기고 진행(시간 측정 로직 삭제하고) 프로젝트 아래에 aop폴더를 생성하고 TimeTraceAop파일 생성 후 아래 코드 작성)

  • 시간 측정 AOP 등록
    java/hello/hellospring/aop/TimeTraceAop.java
package hello.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class TimeTraceAop {
 @Around("execution(* hello.hellospring..*(..))")
 public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
 long start = System.currentTimeMillis();
 System.out.println("START: " + joinPoint.toString());
 try {
 return joinPoint.proceed();
 } finally {
 long finish = System.currentTimeMillis();
 long timeMs = finish - start;
 System.out.println("END: " + joinPoint.toString()+ " " + timeMs +
"ms");
 }
 }
}

execute() 메서드의 내용은 기존에 위에서 작성했던 시간을 측정하는 코드. try문 안에 joinPoint.proceed()를 반환하는 것만 달라. @Around 어노테이션을 사용해서 시간 측정 로직을 어떤 부분에 적용하고 싶은지 나타내고 회원가입, 회원목록조회 등의 메서드가 호출될 때마다 ProceedingJoinPoint 객체인 joinPoing가 넘겨오면서 execute() 메서드가 호출된다. (그래서 시간을 측정.)
해당 메서드가 호출되면 일단 시작 시간을 재고 출력을 하고, try문 안의 joinPoint.proceed()가 호출될 때 실행하길 원하는 로직이 수행된다(핵심 관심사항 - 회원가입, 회원목록조회)
메서드가 종료되면 finally에서 종료시간 재서 시간 측정 및 출력 후 execute()메서드가 종료된다.

이 메서드가 Aop기능을 할 수 있도록 하기 위해 @Aspect 어노테이션 사용.
@Aspect 어노테이션을 component를 포함하고 있는 것이 아니므로 스프링 빈 등록 과정이 필요하다. (또는 SpringConfig에서 직접 @Bean을 통해 스프링 빈으로 등록할 수 있으며, 실무에서 이런 방법을 더 선호한다.) SpringConfig파일의 확인을 통해 어떤 Aop가 적용되어 있는지 확인이 가능하기 때문.

실행결과를 로그를 통해 확인해 보자. (웹 애플리케이션 실행해서 시간 측정이 제대로 동작하는지 확인해)

컨트롤러, 멤버서비스, 리포지토리 각각의 메서드가 호출될 때마다 시간 측정 메서드가 호출되어 START와 END를 출력을 제대로 하기에 시간 측정이 잘 진행된다.

이렇게 AOP를 사용하여 시간 측정 로직(공통 관심사항)을 분리했을 때 위 문제 해결.

해결

  • 회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
  • 핵심 관심 사항을 깔끔하게 유지할 수 있다.
  • 변경이 필요하면 이 로직만 변경하면 된다. (이전에는 각 메서드에 추가했던 시간 측정 코드를 전부 찾아가서 수행해야 하지만 이제는 해당 AOP 로직 하나만 수정하여 변경할 수 있다)
  • 원하는 적용 대상을 선택할 수 있다. (@Around 어노테이션 사용)

스프링 컨테이너에서의 의존관계를 확인하여 AOP의 동작 방식을 확인하자

AOP 적용 전 의존관계

회원 컨트롤러, 회원 서비스는 스프링 빈으로 컨테이너에 등록되어 있고 의존관계.

AOP 적용 후 의존관계

실제로 AOP를 적용하면 스프링은 회원 서비스가 스프링 빈으로 등록될 때 프록시 회원 서비스(가짜 회원 서비스)를 앞에 같이 세운다. 회원 컨트롤러에서 회원 서비스가 호출될 때 실제 회원 서비스가 아닌 프록시 회원 서비스가 호출되고, joinPoint.proceed()가 호출되면 실제 회원 서비스의 메서드들이 동작한다.
(참고로 이러한 방식은 회원 컨트롤러에서 회원 서비스를 주입할 때 실제 회원 서비스의 코드를 복제하여 이러한 방식으로 동작할 수 있도록 스프링에서 조작하기 때문에 가능)

전체 모습 비교

AOP 적용 전 전체 그림

AOP 적용 후 전체 그림

@Around를 통해 프로젝트 전체 메서드에 AOP가 적용되었기 때문에 컨테이너에 스프링 빈으로 등록되는 것들이 모두 프록시와 같이 등록된 것을 확인할 수 있다!
이러한 방식을 스프링에서는 프록시 방법(Proxy)의 AOP라고 한다.

실제 Proxy가 주입되는지 콘솔에 출력해서 확인하기

콘솔 창을 확인하면 memberService의 클래스가 $$Enhancer와 같은 추가적인 것을 출력하는 것을 확인할 수 있다.
이것은 스프링 컨테이너에서 프록시 memeberService만들어 이를 주입한 것이다.


다음으로

지금까지 스프링으로 웹 애플리케이션을 개발하는 방법에 대해서 얇고 넓게 학습했다. 이제부터는 각각의
기술들을 깊이있게 이해해야 한다.
거대한 스프링의 모든 것을 세세하게 알 필요는 없다. 우리는 스프링을 만드는 개발자가 아니다. 스프링을
활용해서 실무에서 발생하는 문제들을 잘 해결하는 것이 훨씬 중요하다. 따라서 핵심 원리를 이해하고,
문제가 발생했을 때, 대략 어디쯤 부터 찾아들어가면 될지, 필요한 부분을 찾아서 사용할 수 있는 능력이 더
중요하다.

이 글은 강의 : 김영한 - "스프링 입문-코드로 배우는 스프링 부트, 웹 MVC, DB접근기술"을 듣고 정리한 내용입니다.

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글