웹 프로그래밍/공부, 연습

[강좌]스프링 입문 - 7. AOP

이 글은 인프런에서 김영한 님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술"을 수강 후 개인적으로 공부한 내용을 정리한 게시글입니다. 잘못된 점이나 부족한 부분이 있다면 언제든 지적 부탁드립니다.

 

해당 강의는 이곳에서 수강할 수 있습니다.

 

이전 강의에서는 JPA와 스프링 데이터 JPA를 활용한 스프링 DB 접근 기술에 대해 알아보았습니다.

이번 강의에서는 AOP에 대해 학습합니다.

 

 

1. AOP가 필요한 상황

 

백엔드 구현에서의 관심사항은 두 가지 사항으로 나뉩니다.

공통 관심사항(cross-cutting concern)핵심 관심사항(core concern)으로 나뉘는데, 회원 가입, 회원 조회와 같은 비즈니스 로직과 같은 경우가 핵심 관심사항이고, 이러한 기능 외에 회원 가입이나 회원 조회의 기능을 실행하는데 걸리는 시간을 측정하는 것과 같은 기능은 공통 관심 사항으로 분류됩니다.

 

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

 

회원 가입과 회원 조회의 기능의 호출 시간을 구하기 위해서는 회원 서비스에 다음과 같은 코드를 작성해야 합니다.

 

@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 {
            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에서(회원가입 또는 회원 조회에 실패하더라도 시간을 잴 수 있게) 메서드가 끝난 시점 시간을 재 두 시간의 차로 호출에 걸리는 시간을 측정하였습니다.

 

통합 테스트를 수행하여 동작을 확인할 수 있습니다.

 

 

테스트가 정상적으로 수행되고, 호출 시간 또한 출력되는 것을 확인할 수 있습니다.

웹 애플리케이션에서 직접 회원을 추가/조회하여 findMembers() 메서드의 호출 시간 또한 출력해 볼 수 있습니다.

 

위와 같은 방법으로 메서드에 코드를 추가하여 호출 시간을 구할 수 있지만, 여기에서 문제가 발생합니다.

해당 기능은 공통 관심 사항으로, 이러한 핵심 관심사항이 아닌 코드와 비즈니스 로직 코드가 섞여 유지 보수가 어려워질 뿐 아니라 호출 시간을 구하는 코드를 추가한 것처럼 모든 로직을 찾아가며 코드를 추가, 변경하는데 시간이 많이 걸린다는 단점이 있습니다. 이러한 로직은 별도의 공통 로직으로 만들기도 매우 어려워 비즈니스 로직과 관련없는 개발에서 불필요한 시간 소모가 발생합니다.

 

우리는 이런 상황에서 AOP의 필요성을 느끼게 됩니다.

 

 

2. AOP 적용

 

AOPAspect Oriendted Programming이란 의미로 위에서 말한것처럼 시간을 측정하는 로직과 같은 공통 관심 사항(cross-cutting concern)과 비지니스 로직인 핵심 관심 사항(core concern)으로 분리하여 개발하는 기술입니다.

 

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

 

다음은 위와 같은 방법을 사용했을 때 스프링 컨테이너의 모습입니다.

 

이미지 출처는 해당 강의 자료입니다

 

컨트롤러, 서비스, 리포지토리에서 각각 메서드를 호출할 때 시간 측정을 해야 하므로 시간 측정 로직이 각 과정에 작성되어야 했습니다. 

 

이제 AOP를 사용했을 때의 컨테이너 모습을 살펴봅니다.

 

이미지 출처는 해당 강의 자료입니다

 

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

 

이제 AOP를 사용하여 직접 로직을 분리해 봅니다.

먼저 이전에 작성했던 멤버 서비스의 시간 측정 로직을 지우고 프로잭트 아래에 aop폴더를 생성하고 TimeTraceAop파일을 생성한 후 다음과 같이 작성합니다.

 

package hello.hellospring.aop;

        import org.aspectj.lang.JoinPoint;
        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 객체인 joinPoint가 넘겨오면서 execute() 메서드가 호출됩니다. 

해당 메서드가 호출되면 일단 시작 시간을 재고 출력을 하고, try문 안의  joinPoint.proceed()가 호출될 때 우리가 실행하길 원하는 로직이 수행됩니다.(회원가입, 회원 목록 조회)

메서드가 종료되면 finally에서 종료 시간을 재고, 시간을 측정해 출력한 뒤 execute() 메서드가 종료됩니다.

 

이 메서드가 AOP 기능을 할 수 있게 하기 위해 @Aspect 애노테이션을 사용합니다.

AOP 또한 스프링 빈으로 등록되어야 하기 때문에 컴포넌트 스캔을 위해 @Component로 스프링 빈을 등록합니다.(또는 Config 파일에서 직접 @Bean을 통해 스프링 빈으로 등록할 수 있으며, 실무에서는 이러한 방법을 더 선호합니다. Config파일의 확인을 통해 어떤 AOP가 적용되어 있는지 확인이 가능하기 때문입니다.)

 

이제 웹 애플리케이션을 실행해서 시간 측정이 정상적으로 동작하는지 로그를 확인해 봅니다.

 

 

회원 가입 후 회원 목록으로 가입된 회원 확인까지 한 후의 로그입니다. 

컨트롤러, 서비스, 리포지토리 각각의 메서드가 호출될 때마다 시간 측정 메서드가 호출되어 START와 END를 출력한 것을 확인할 수 있습니다.

 

이렇게 AOP를 사용하여 시간 측정 로직을 분리했을때 다음과 같은 문제가 해결되었습니다.

핵심 관심사항과 공통 관심사항을 분리하여 시간을 측정하는 로직을 별도의 공통 로직으로 만들수 있었고 이로 인해 핵심 관심사항을 깔끔하게 유지할 수 있습니다.

만약 이러한 공통 관심사항 로직의 변경이 필요하면 이전에는 각 메서드에 추가했던 시간 측정 코드를 전부 찾아가 수정해야 하지만 이제는 해당 AOP 로직 하나만 수정하여 변경할 수 있습니다.

@Around를 사용하여 간편하게 원하는 적용 대상을 선택할 수 있습니다. 예를 들어 위 예시에서는 해당 프로젝트의 모든 메서드에 위 AOP를 적용했지만 만약 서비스의 메서드에만 이를 적용하고 싶다면 (* hello.hellospring.service.. *(..))와 같이 이를 명시하여 적용 대상을 선택할 수 있습니다.

 

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

 

이미지 출처는 해당 강의 자료입니다

 

AOP 적용 전 의존관계입니다.

회원 컨트롤러와 회원 서비스는 각 스프링 빈으로 컨테이너에 등록되어있고, 회원 컨트롤러는 회원 서비스를 의존하고 있습니다.

 

다음은 AOP 적용 후 의존관계입니다.

 

이미지 출처는 해당 강의 자료입니다

 

실제로 AOP를 적용하면 스프링은 회원 서비스가 스프링 빈으로 등록될 때 프록시(Proxy) 회원 서비스(가짜 회원 서비스)를 앞에 같이 세웁니다. 회원 컨트롤러에서 회원 서비스가 호출될 때 실제 회원 서비스가 아닌 프록시 회원 서비스가 호출되고, joinPoint.proceed()가 호출되면 실제 회원 서비스의 메서드들이 동작합니다.

(참고로 이러한 방식은 회원 컨트롤러에서 회원 서비스를 주입할 때 실제 회원 서비스의 코드를 복제하여 이러한 방식으로 동작할 수 있도록 스프링에서 조작하기 때문에 가능합니다.)

 

AOP를 적용하기 전후의 전체 모습을 확인해 봅니다.

 

이미지 출처는 해당 강의 자료입니다

 

@Around를 통해 프로젝트 전체 메서드에 AOP가 적용되었기 때문에 컨테이너에 스프링 빈으로 등록되는 것들이 모두 프록시와 같이 등록된 것을 확인할 수 있습니다. 

 

이러한 방식을 스프링에서는 프록시(Proxy) 방식의 AOP라고 하며, 실제 코드를 통해 proxy가 주입되는지 출력으로 확인해 봅니다.

 

간단히 기존의 MemberController의 생성자에 다음과 같은 코드를 추가하고 실행을 통해 확인해 봅니다

 

    @Autowired
    public MemberController(MemberService memberService){
        this.memberService = memberService;
        System.out.println("memberService = " + memberService.getClass());
    }

 

 

콘솔 창을 확인하면 memberService의 클래스가 $$Enhancer...과 같은 추가적인 것을 출력하는 것을 확인할 수 있습니다.

이는 실제로 스프링 컨테이너에서 프록시 서비스를 만들어 이를 주입한 것임을 알 수 있습니다.

 

 

이번 강의를 끝으로 드디어 스프링 입문 강의를 완강하였습니다.

다음 게시글은 해당 강의를 마무리하며 느낀 소감과 앞으로의 학습 계획을 포스팅해 보려고 합니다. 고생하셨습니다!