티스토리 뷰

728x90
반응형

스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 김영한 인프런 강의 참고

이전 포스팅

Goals

  • AOP가 필요한 상황
  • AOP란?
  • AOP 적용해보기
  • AOP 용어 정리
  • 스프링의 AOP 동작 방식

 

AOP가 필요한 상황


AOP가 무엇인지 알아보기 전에 먼저 어떤 상황에서 AOP라는 기술이 필요한 지 예시를 들어보겠다.

만약 프로젝트를 진행하던 중, 회원 조회 메서드의 호출 시간을 측정해야 한다고 해보자.

service/MemberService.java

// ...

public List<Member> findMembers() {
    long start = System.currentTimeMillis();

    try{
        return memberRepository.findAll();
    } finally {
        long finish = System.currentTimeMillis();
        long timeMs = finish - start;
        System.out.println("findMembers " + timeMs + "ms");
    }
}

그런데 위 시간 측정 로직을 갑자기 모든 메서드 적용해야 하는 상황이 됐다.

service/MemberService.java

// ...

/**
 * 회원가입
 */
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("findMembers " + 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("findMembers " + timeMs + "ms");
    }
}

서비스 로직만 하더라도 벌써 혼란스러워 졌다. 이걸 모든 메서드에 적용하려고 하니 단순 코드의 반복을 없애고 싶다. 하지만 생각보다 쉽지 않다. 비즈니스 로직이 시간 측정 로직 사이에 껴있고, try 문으로 예외도 처리해 줘야 한다.

위와 같은 상황의 문제점을 찾아보자.

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

핵심 관심사(core concern) : 핵심 비즈니스 로직

공통 관심사(cross-cutting concern) : 보통 횡단 관심사라고 하며, 다수의 모듈에 공통적으로 나타나는 부분

 

위 상황을 말끔하게 해결해주는 기술이 바로 AOP다.

 

AOP란?


AOP(Aspect-Oriented Programming)는 프로그램 구조에 대한 또 다른 사고 방식을 제공하여 OOP(Object-Oriented Programming)를 보완한다. OOP에서 모듈화의 핵심 단위는 클래스인 반면, AOP에서는 모듈화 단위가 관점(aspect)이다.

Aspect는 여러 유형과 객체를 가로지르는 트랜잭션 관리와 같은 문제의 모듈화를 가능하게 한다. (이러한 관심(concern)은 종종 AOP 문헌에서 횡단 관심 사항(cross-cutting concern)이라 한다.)

쉽게 말해, 어떤 로직을 기준으로 핵심 관심사공통 관심사로 나눠보고 그 관점(Aspect)을 기준으로 각각 모듈화 하겠다는 의미다.

AOP에서 사용하는 용어들이 있다. 잘 와닿지 않지만, API 문서를 보더라도 해당 용어들로 설명되어 있기 때문에 어쩔 수 없이 알고는 있어야 한다.

주요 용어 정리

  • Aspect : 횡단 관심사가 모듈화된 것
  • Pointcut : Aspect의 적용 위치 지정자(타깃 클래스의 타깃 메서드 지정자)
  • Advice : Pointcut에 언제, 무엇을 적용할지 정의한 메서드
  • JoinPoint : 스프링 프레임워크가 관리하는 빈의 모든 메서드에 해당할 수 있으며, 코드 상에서는 호출된 객체의 메서드를 나타냄

이렇게 보면 개념이 어려우니 직접 적용해 보면서 알아보자.

 

AOP 적용해보기


먼저 AOP들을 모아놓는 패키지를 하위에 생성하고 시간 측정 AOP 클래스를 작성한다.

aop/TimeTraceAop.java

// ...

@Component
@Aspect
public class TimeTraceAop {
    
    @Around("execution(* com.example.practice.service..*(..))")
    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");
        }
    }
}

TimeTraceAop 클래스를 스프링 빈으로 등록하기 위해 @Component를 붙여 준다. 보통 위와 같은 특수한 클래스들은 SpringConfig에서 따로 관리하는 것이 좋다.

execute 메서드는 하나의 Advice가 된다.

  • @Aspect : 이 클래스를 이제 AOP에서 사용하겠다는 의미로 Aspect 지정
  • @Around : Pointcut의 5가지 종류 중 하나로, 지정된 패턴에 해당하는 메서드가 실행되기 전, 후 모두에서 동작 가능
  • @execution : 특정 메서드를 지정하는 패턴을 작성할 수 있는 방법. 여기에 지정된 메서드가 JointPoint가 된다.

패턴 :  [접근제어자] 리턴타입 [패키지&클래스.]메서드이름(파라미터) [throws 예외]

위 패턴을 이용해 예시 코드를 해석해 보면,

  • 리턴타입 : *(모두)
  • 패키지 : com.example.practice.service 하위 모든 패키지
  • 메서드이름 : *(모든 메서드)
  • 파라미터 : (..)(0개 이상)
  • ProceedingJoinPoint : 스프링 컨테이너가 넘겨준 핵심 관심사(위 패턴에 해당하는 JointPoint) 메서드 정보
  • joinPoint.proceed() : JointPoint를 이 위치에서 호출. 반환 값으로는 JointPoint의 실행 결과 값이 담긴 Object

 

테스트를 실행해 보면,

MemberService의 모든 메서드에서 시간 측정 로직이 적용된 것을 확인할 수 있다.

먄약 모든 메서드에 적용해보고 싶으면 execute()의 문자열에서 service를 지워주고 ..*만 남기면 된다.

@Around("execution(* com.example.practice..*(..))")

이는 practice 패키지의 모든 메서드에 적용하라는 의미다.

로그를 보면, 어떤 클래스의 메서드들이 어떤 순서로 호출되는지 확인할 수 있다.

이제 우리는

AOP 적용 전

위와 같던 상황을 아래와 같이 바꿀 수 있다.

AOP 적용 후

결과적으로 위의 문제들을 다음과 같이 해결할 수 있다.

  • 회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다.
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
  • 핵심 관심 사항을 깔끔하게 유지할 수 있다.
  • 변경이 필요하면 공통 관심 사항 로직만 변경하면 된다.
  • 원하는 적용 대상을 선택할 수 있다.

 

스프링의 AOP 동작 방식


어떻게 비즈니스 로직을 시간 측정 로직 사이에 끼워 넣을 수 있었을까? 우리가 AOP를 적용하기 전에는 memberControllermemberService를 의존하는 의존 관계가 직접 맺어져 있었다.

AOP 적용 전

하지만 AOP를 적용하면 스프링 컨테이너가 프록시 객체(bean)를 생성한 뒤 이 Proxy 객체를 DI해서 의존 관계를 간접적으로 맺어버린다. 즉 DI를 가로채는 것이다.

AOP Proxy 객체는 우리가 위에서 정의한 TimeTraceAop가 스프링 컨테이너에 등록된 스프링 빈이며, 실제 memberService를 의존하는 의존 관계를 맺게 된다.

memberController는 프록시 memberService를 주입 받으므로, 메서드 호출 시에도 Proxy 객체의 메서드들을 호출하게 된다. 콘솔로 memberController가 실제로 주입된 Proxy를 호출하는 지 확인할 수 있다.

controller/MemberController.java

// ...

@Controller
public class MemberController {
    
    private final MemberService memberService;

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

// ...

MemberService뒤에 $$SpringCGLIB이 붙은 것을 확인할 수 있는데, SpringCGLIB이라는 라이브러리가 생성한 Proxy 객체라는 의미다.

그럼 실제 memberService는 언제 호출되는 것일까? 바로 프록시 memberService에서 joinPoint.proceed() 메서드가 호출되는 시점이다.

결국 모든 메서드를 JoinPoint로써 AOP를 적용하게 되면 다음과 같은 의존 관계가 된다.

이러한 AOP 기술이 가능한 이유에는 근본적으로 DI에 있다. 우리가 만약 new 키워드를 통해 의존 관계를 직접 맺어 줬다면, 스프링 컨테이너가 프록시 기술을 통해 의존 관계를 변경할 수 없었을 것이다.

스프링 컨테이너가 모든 스프링 빈들을 관리하고, 의존 관계를 맺어주기 때문에 AOP같은 기술의 응용이 가능한 것이다.

 

이것으로 Spring Boot 입문은 끝났다. 김영환님의 무료 강의 덕분에 큰 틀에서의 Spring Boot를 이해할 수 있었다. 처음 Spring을 공부하시는 분이라면 적극 추천. 이제 Spring Boot를 활용해서 간단한 CRUD 프로젝트를 진행할 예정이다.(기회가 된다면 포스팅 하겠습니다!)

 

Reference

728x90
반응형
댓글