[SpringBoot] 게시판 구현하기 9 (스프링의 다양한 기능 살펴보기 - AOP)
FRAMEWORK/Spring

[SpringBoot] 게시판 구현하기 9 (스프링의 다양한 기능 살펴보기 - AOP)

반응형

들어가기 전에

하기 포스팅은 "스프링부트 시작하기(김인우 저)" 책을 공부하며 적은 포스팅입니다. 이번 포스팅에서는 AOP에 대해 살펴보도록 하겠습니다.

AOP(Aspect Oriented Programming, 관점지향 프로그래밍)

AOP란, OOP(Object Oriented Programming, 객체지향 프로그래밍)를 더욱 OOP답게 사용하도록 도와주는 개념입니다.

객체지향이란, 기능과 데이터들을 모아 재사용이 가능한 객체로 캡슐화하는 것을 의미합니다. 이렇게 캡슐화된 클래스들을 프로젝트 내 여러 곳에서 반복적으로 재사용하여 생산성을 높일 수 있습니다.

하지만, 실제 어플리케이션을 개발하다보면 객체의 핵심코드 외에도 여러 기능이 들어 갑니다. 메소드 호출 전후의 로그, 데이터 검증 및 확인 로그, 예외처리 등 핵심 기능과 관계는 없지만 필요한 코드들이 삽입되어 객체의 모듈화가 어려워질 수 있습니다. 반복적인 코드를 삽입하지 않기 위해 만든 클래스도, 그 클래스를 반복적으로 사용하면서 코드의 재사용성이 떨어지고 생산성이 낮아지는 문제가 나타납니다.

AOP는 어플리케이션 전반에 사용되는 기능을 여러 코드에 쉽게 적용할 수 있도록 합니다. 예를 들어 로그, 권한 체크, 인증, 예외처리 등은 어플리케이션의 대부분에 적용되어야 하는 기능입니다. 기존 OOP에서는 공통적으로 사용하는 기능을 하나의 클래스로 만들어 놓더라도, 해당 기능이 필요한 모든 부분에서 클래스를 생성하고 필요한 메소드를 호출해야 했습니다. 즉, 공통 기능을 사용할 부분이 백 군데라면, 백 군데의 코드에 직접 공통 기능을 추가(아래 왼쪽 그림 - OOP)해야 했습니다.

OOP(왼쪽), AOP(오른쪽) 관점

위 그림에서 화살표는 하나의 기능을 구현하기 위해 필요한 작업을 나타냅니다. AOP는 "관점"이라는 개념을 통해 OOP를 OOP답게 사용할 수 있도록 했습니다. 즉, 대상을 바라보는 방향을 바꾸어보는 것입니다. 계정, 게시판, 계좌이체라는 핵심 기능 관점에서 보면 권한, 로깅, 트랜잭션 등의 부가 기능 사이에는 공통점이 없습니다. 하지만, 각 부가 기능의 관점에서 보면 핵심 로직이 무엇이든 상관 없이 해당 기능이 적용되어야 하는 시점만 알면 그 때 적용되면 된다는 공통점이 있습니다.

그래서 AOP 관점(위 오른쪽 그림)에서 보면, 권한, 로깅, 트랜잭션과 같은 부가 기능이 비즈니스 로직에 직접 삽입된 것이 아닌 외부에 있는 것을 알 수 있습니다. 즉, 각 부가 기능이 삽입될 시점만 확인하면 신규 비즈니스 로직이 추가되더라도 컨트롤러나 서비스와 같은 영역의 실행 시점에만 삽입되면 되는 것입니다.

AOP는 결국 공통된 기능을 재사용하는 것입니다. 물론, OOP도 공통된 기능을 하나의 객체로 만들고 이를 다른 객체에서 호출하는 식으로 구현됩니다. 하지만, 공통 기능을 구현하기 위해 다시 다른 객체의 기능이 필요하거나 객체에서 다른 객체를 계속 호출해야 하는 등 객체 간 종속성이 강한 경우가 있습니다. 이는 모듈화를 깔끔하게 하기 어렵습니다.

AOP를 이용하면, 다른 객체의 호출과 상관없이 각각의 기능에만 집중해서 모듈로 만들 수 있습니다. 필요한 지점에서 기능을 직접 삽입하면 됩니다. 즉, AOP는 비즈니스 로직을 구현한 코드에서 공통 기능 코드를 직접 호출하지 않습니다. AOP를 적용하면, 공통 기능과 비즈니스 기능을 따로 개발한 후 컴파일하거나 컴파일된 클래스를 로딩하는 시점 등에 AOP가 적용되어 비즈니스 로직 코드 사이에 공통 기능 코드가 삽입됩니다.

AOP 관련 용어 및 개념

용어 의미
관점(Aspect) 공통적으로 적용될 기능(@Aspect)을 의미합니다. 횡단 관심사의 기능이라고 할 수 있으며 한 개 이상의 포인트컷과 어드바이스의 조합으로 만들어집니다.
어드바이스(Advice) 관점의 구현체로 조인포인트에 삽입되어 동작하는 것을 의미합니다. 스프링에서 사용하는 어드바이스는 동작하는 시점에 따라 다섯 종류로 구분됩니다.
조인포인트(Joinpoint) 어드바이스를 적용하는 지점을 의미합니다. 스프링 프레임워크에서 조인포인트는 항상 메소드 실행 단계만 가능합니다.
포인트컷(Pointcut) 어드바이스를 적용할 조인포인트를 선별하는 과정이나 그 기능을 정의한 모듈을 의미합니다. 정규표현식이나 AspectJ의 문법을 이용해 어떤 조인포인트를 사용할 것인지 결정합니다.
타깃(Target) 어드바이스를 받을 대상을 의미합니다.
위빙(Weaving) 어드바이스를 적용하는 것을 의미합니다. 즉, 공통 코드를 원하는 대상에 삽입하는 것을 뜻합니다.

AOP 적용하기

앞서 글로 AOP의 개념을 정리해보았지만, 코드를 통해 구현해보는 것이 확실하게 개념 잡는데 도움이 될 것입니다. 여기서는 코드 전체에 적용될 기능 중, 로그 출력 AOP를 적용해보겠습니다. 출력할 로그는 컨트롤러, 서비스, 매퍼의 메소드가 실행될 때 각 메소드의 경로 및 이름입니다.

먼저, src/main/java/board 패키지에 aop 패키지를 생성하고, LoggerAspect.java에 LoggerAspect 클래스를 작성합니다.

package board.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Component;

@Component
@Aspect
public class LoggerAspect {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    @Around("execution(* board..controller.*Controller.*(..)) or execution(* board..service.*Impl.*(..)) or execution(* board..mapper.*Mapper.*(..))")
    public Object logPrint(ProceedingJoinPoint joinPoint) throws Throwable {
        String type = "";
        String name = joinPoint.getSignature().getDeclaringTypeName();
        if(name.indexOf("Controller") > -1) {
            type = "Controller  \t:  ";
        }
        else if(name.indexOf("Service") > -1) {
            type = "ServiceImpl  \t:  ";
        }
        else if(name.indexOf("Mapper") > -1) {
            type = "Mapper  \t\t:  ";
        }
        log.debug(type + name + "." + joinPoint.getSignature().getName() + "()");
        return joinPoint.proceed();
    }
}
  • @Aspect: Aspect 어노테이션을 이용해 자바 코드에서 AOP를 설정합니다.
  • @Around("execution(\* board..controller.\*Controller.\*(..)) or execution(\* board..service.\*Impl.\*(..)) or execution(\* board..mapper.\*Mapper.\*(..)"): @Around 어노테이션으로 해당 메소드가 실행될 시점(어드바이스)을 정의합니다.
    • 어드바이스 어노테이션 종류는 5가지가 있으나, 이 코드에서는 대상 메소드의 실행 전후 또는 예외 발생 시점에 사용할 수 있는 @Around 어노테이션 적용
    • execution적용할 메소드를 포인트컷 표현식으로 명시할 때 사용
  • log.debug(type + name + "." + joinPoint.getSignature().getName() + "()"): 실행되는 메소드의 이름을 이용해 controller, service, mapper를 구분한 후 실행되는 메소드 이름을 출력합니다.

Logger 참고 사항

위 코드에서는 Logger를 직접 선언하고 사용했지만 lombok을 사용하면 annotaion을 사용해 따로 Logger를 선언하지 않고 사용할 수 있습니다.

// AS-IS
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggerAspect {
    private Logger log = LoggerFactory.getLooger(this.getClass());
    ...
    log.debug(...);
}

// TO-BE
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LoggerAspect {
    log.debug(...);
}

AOP 적용 결과 확인하기

locolhost:8080/board/openBoardList.do 호출 시 아래와 같이 이클립스 실행 로그가 뜨는 것을 확인할 수 있습니다.

AOP 적용

AOP 설정 전, LoggerInterceptor가 출력하는 Request URI를 시작(preHandle 인터셉터 - 컨트롤러 시작 전 작동)으로 Logger Aspect에서 설정한 Controller, ServiceImpl, Mapper가 각각 출력됩니다. 이를 통해, 사용자의 요청을 처리하기 위해 수행되는 메소드 흐름을 한 번에 확인할 수 있습니다.

앞서 AOP에 대해 글로 설명할 때, 모든 로직에 적용되는 공통 기능을 외부에서 삽입할 수 있다고 했습니다. 위 예시에서 보면 공통적으로 적용할 로그 기능을 LoggerAspect라는 외부 클래스에 정의하고 어플리케이션의 실행 시점에 해당 기능이 삽입되어 실행되었습니다.

이렇게 AOP를 이용해 기존 로직 변화 없이 원하는 시점에 코드를 삽입할 수 있습니다.

AOP 주요 개념

어드바이스

어드바이스는 관점의 구현체로 조인포인트에 삽입되어 동작하는 것을 의미합니다. 스프링에서 사용하는 어드바이스는 동작하는 시점에 따라 다섯 종류로 구분됩니다.

종류 어노테이션 설명
Before Advice @Before 대상 메소드가 실행되기 전에 적용할 어드바이스를 정의합니다.
After returning Advice @AfterReturning 대상 메소드가 성공적으로 실행되고 결과값을 반환한 후 적용할 어드바이스를 정의합니다.
After throwing Advice @AfterThrowing 대상 메소드에서 예외가 발생했을 때 적용할 어드바이스를 정의합니다. try/catch문의 catch와 비슷한 역할을 합니다.
After Advice @After 대상 메소드의 정상적인 수행 여부와 상관없이 무조건 실행되는 어드바이스를 정의합니다. 즉, 예외가 발생하더라도 실행되기 때문에 자바의 finally와 비슷한 역할을 합니다.
Around Advice @Around 대상 메소드의 호출 전후, 예외 발생 등 모든 시점에 적용할 수 있는 어드바이스를 정의합니다. 가장 범용적으로 사용할 수 있는 어드바이스입니다.

포인트컷

어드바이스를 적용할 조인포인트를 선별하는 과정이나 그 기능을 정의한 모듈을 의미합니다. 정규표현식이나 AspectJ 문법을 이용해 어떤 조인포인트를 사용할 것인지 결정합니다. 포인트컷을 표현할 수 있는 명시자에는 여러 종류가 있지만, 가장 많이 사용하는 명시자는 execution입니다.

execution

  • 가장 대표적이고 강력한 지시자로 접근 제어자, 리턴 타입, 타입 패턴, 메소드, 파라미터 타입, 예외 타입 등을 조합해 가장 정교한 포인트컷을 만들 수 있습니다.
  • *: 모든 값이라는 의미로, select*라고 쓴다면 select로 시작하는 모든 메소드가 선택됩니다.
  • ..: 0개 이상이라는 의미로, 파라미터, 메소드, 패키지 등 모든 것의 0개 이상을 뜻합니다. 패키지 구조를 표현할 때에는 하위 모든 패키지를 의미하고, 파라미터를 표현할 때는 파라미터 개수와 관계없이 모든 파라미터를 의미합니다.
execution(void select*(..))
// 리턴 타입은 void이고, 메소드 이름은 select로 시작하며 파라미터는 0개 이상인 모든 메소드가 호출될 떄
execution(* board.controller.*())
// board.controller 패키지 밑 파라미터가 없는 모든 메소드가 호출될 때
execution(* board.controller.*(..))
// board.controller 패키지 밑 파라미터가 0개 이상인 모든 메소드가 호출될 때
execution(* board..select*(*))
// board 패키지의 모든 하위 패키지에 있는 select로 시작하고 파라미터가 한 개인 모든 메소드가 호출될 때
execution(* board..select*(*,*))
// board 패키지의 모든 하위 패키지에 있는 select로 시작하고 파라미터가 두 개인 모든 메소드가 호출될 때
execution(* board..controller.*Controller.*(..))
// board 패키지 하위 모든 패키지(..) 중 controller 패키지의 Controller로 끝나는 클래스의 파라미터가 0개 이상인 모든 메소드가 호출될 때
execution(* board..service.*Impl.*(..))
// board 패키지 하위 모든 패키지(..) 중 service 패키지의 Impl로 끝나는 클래스의 파라미터가 0개 이상인 모든 메소드가 호출될 때
execution(* board..mapper.*Mapper.*(..))
// board 패키지 하위 모든 패키지(..) 중 mapper 패키지의 Mapper로 끝나느 클래스의 파라미터가 0개 이상인 모든 메소드가 호출될 때
  • 포인트컷 표현식은 and와 or를 조합해서 사용할 수 있고, and와 or은 각각 &&와 ||로 표현할 수 있습니다.

within

특정 타입에 속하는 메소드를 포인트컷으로 설정합니다.

within(board.service.boardServiceImpl)
// board.service 패키지 밑에 있는 boardServiceImpl 클래스의 메소드가 호출될 때
within(board.service.*ServiceImpl)
// board.service 패키지 밑에 있는 ServiceImpl로 끝나는 클래스의 메소드가 호출될 때

bean

스프링의 빈 이름의 패턴으로 포인트컷을 설정합니다.

bean(boardServiceImpl)
// boardServiceImpl이라는 빈의 메소드가 호출될 때
bean(*ServiceImpl)
// ServiceImpl로 끝나는 빈의 메소드가 호출될 때
반응형