본문 바로가기

JAVA/Spring-boot

[Spring boot] AOP 적용 사례 (feat. AOP로 회원 인증을 구현해보자!)

반응형

안녕하세요, 도토리입니다.

잘못된 정보가 있거나 오류가 있으면 언제든지 댓글로 이야기 해주세요!! 😅

 

회사에서 음... 딱 AOP를 적용하면 좋겠을 부분이 있어서 구글링하면서 적용하려고 했는데.. 책에서 logging만 다룬건지.. 많은 글들이  logging 관련해서 HttpServeletRequest를 toString으로 찍어 로그찍는것들로만 예시를 들어두었길래 나는 간단하게 회원 인증으로 소개를 해보려고한다. (혹시.. 제가 적용한 방법이 잘못되었거나, 안좋은 예시라면 댓글 부탁드립니다.)

 

AOP란?

먼저 예제를 설명하기 전에, 간단하게라도 AOP가 무엇인지에 대해서 알아보자!

 

AOP는 Aspect Oriented Programming으로 아주 예전부터 있었던 개념이다. (우리 회사의 아주 오래된 20년도 더된 레거시 시스템에도 적용되어있던 개념이라고 한다...)

 

Aspect Oriented Programming을 직역하자면 관점지향프로그래밍인데, 이는 객체지향을 보완하는 것으로 프로그램의 구조를 관점 기준으로 공통된 모듈로 분리하는 방법이다.

 

정말 말 그대로 관점으로써 공통된 부분을 빼서 적용하겠다는것이다. 

아주 대표적으로 로깅과, 인증 & 인가 이런것들이 있다. 

 

 

AOP를 어디에 어떻게 적용하는데?

저의 사례를 예시로 들면 아래와 같이 모든 API를 콜하는데 있어 고유의 고객번호를 검증하는 로직들이 모든 API마다 필요했는데, 이는 누가봐도 공통으로 빼고싶은 로직이다.

public register(xxxRequest req) throws BusinessException {
		// 로직 ~~~
		custService.validateCustomer(req.getCustNo);
		// 로직 ~~~
	}
	
	// 생략

	public cancel(xxxRequest req) throws BusinessException {
		// 로직 ~~~
		custService.validateCustomer(req.getCustNo);
		// 로직 ~~~
	}

	// 생략

	public bundleRegister(xxxRequest req) throws BusinessException {
		custService.validateCustomer(req.getCustNo);
		// 로직 ~~~
	}

 

 

처음에는 하나, 둘 씩 검증하는 로직이 들어가다 위와같이 여러곳에서 이 로직을 공통적으로 사용한다면 이는 AOP를 적용하여 custService에 의존함으로써 발생하는 복잡도를 낮출 수 있다.

 

또한, 위와같이 다른 클래스에 의존하여 메서드를 직접 호출해서 사용해야하지만, AOP는 Spring framework에 도움을 받아서 코드를 끼워넣을 수 있다.

 

즉, 개발자는 코드를 작성할 때 명시적으로 관점 객체를 생성하지 않으며, 또한 메서드도 직접 호출하지 않는다. 단지 관점으로 구현된 클래스에는 어떤 부분에 기능을 끼워넣을것인지 설정하면 된다.

 

이렇게 클래스 밖에서 관점의 기능이 실행되고 서로 완전히 분리된 형태를 '관심의 분리(Separation Of Concerns)'라고 한다.

 

 

AOP 관련 용어들 정리

먼저 AOP를 설정하는데에 필요한 용어들을 아래와 같이 정리할 수 있다. (처음에는 무슨소리인가 싶다.. 한 두어번 읽고 실제로 적용해보면서 이해하는것이 더 빠를것 같다)

 

- 대상 객체(target Obejct): 공통 모듈을 적용할 대상을 의미한다. 아래의 예시를 들어 설명하자면, 예를들어 @AuthenticationTarget이라는 어노테이션이 달린 곳에 AOP를 적용한다고 하면 어노테이션이 달려있는 cancel이라는 메서드가 대상 객체이다.

@Slf4j
@ApplicationService
@RequiredArgsConstructor
public class DeliveryService {
	@AuthenticationTarget
	@Transactional
	public CommonResponse cancel(XXXCancelRequest req) throws BusinessException {
	
	}
}

 

-관점(aspect):AOP 프로그래밍으로 작성한 공통 모듈과 적용될 위치 정보의 조합을 의미함. 관점은 어드바이스와 포인트 컷을 합친것으로 AuthenticationAspect.java 클래스를 관점이라고하며, 인증 공통 모듈인 어드바이스(AuthenticationAspect안에 로직)와 적용대상의 메서드 위치 정보가 있는 포인트 컷을 포함하고 있음

 

- 어드바이스(advice): 어플리케이션의 공통 로직이 작성된 모듈을 의미함. 스프링 AOP에서 어드바이스는 메서드 형태이며 여기서는 인증 로직을 의미함.

어드바이스 + 포인트 컷(적용될 위치) = 관점 이다 

 

- 포인트 컷(point cut): 어드바이스를 적용할 위치를 선정하는 설정을 의미. 어드바이스는 포인트컷으로 적용될 위치가 결정되고, 그 시점에 어드바이스가 실행됨. 

포인트 컷은 포인트 컷 표현식을 사용하거나 혹은 어노테이션을 지정하여 설정할 수도 있음

 

- 조인 포인트(join point): 어드바이스가 포인트컷의 위치를 읽어 끼워들어져간 곳을 조인포인트라고 한다. 포인트 컷은 조인 포인트를 선정하는걸 의미하고, 어드바이스가 적용된 즉, 끼워진 포인트가 조인 포인트이다.

 

- 위빙(weaving): 조인포인트에 실행할 코드인 어드바이스를 끼워 넣는 과정을 위빙이라고 함.

 

- 프록시 객체(proxy object): 스프링 AOP는 관점 클래스와 대상 클래스의 기능을 조합하기 위해서 동적으로 프록시 객체를 만듬. 이 프록시 객체가 인증인가를 담당하는 Aspect 어드바이스 코드를 실행하고, 다시 기존 로직의 메서드를 실행하는 역할을 함.

 

 

예제로 AOP 적용해보자

위에서 쭉 설명을 한것처럼, 공통으로 인증 인가 관련된 로직(어드바이스)을 AOP에 적용하고 싶은것이다. 

@Aspect
@Component
@AllArgsConstructor
public class AuthenticationAspect {

	private final CustomerService customerService;

	@Pointcut("@annotation(com.xxx.yyyy.support.annotations.AuthenticationExample)")
	public void pointCut() {
	}

	@Before("pointCut() && args(..,@RequestBody body)")
	public void authenticate(Object body) throws Throwable {
		Field field = body.getClass().getDeclaredField("custNo");
		field.setAccessible(true);
		String custNo = field.get(body).toString();
		customerService.validateCustomer(custNo);
		// ~~~
	}
}

 

처음에 보이는 @Pointcut("@annotation(com.xxx.yyyy.support.annotations.AuthenticationExample)") 어노테이션은 pointCut 표현식으로 만들어 진것이 아닌 @AuthenticationExample이라는 어노테이션이 달린 곳에 어드바이스 로직을 적용하겠다는 뜻이다. 

 

하지만 위치를 지정하더라도 로직에 정말 필요한것은 실제 요청으로 들어온 고객번호가 필요한것이다.

 

아래의 어드바이스를 자세히 살펴보면 위에서 지정한 pointCut()과 argument로 @RequestBody로 지정된 body값을 받아온다. 

해당 body값으로 받아오고 모든 request에 포함되어 있는 custNo라는 고객 번호를 아래 코드와 같이 받아올 수 있다.

@Before("pointCut() && args(..,@RequestBody body)")
	public void authenticate(Object body) throws Throwable {
		Field field = body.getClass().getDeclaredField("custNo");
		field.setAccessible(true);
		String custNo = field.get(body).toString();
		customerService.validateCustomer(custNo);
		// ~~~
	}

 

해당 custNo를 받아와서 validateCustomer이라는 로직을 매번 pointCut에 정의해둔 위치의 메서드를 실행하기전 이 AOP가 request를 가져와 수행할 것이다. 

 

이러면 @AuthenticationExample라는 이 어노테이션 하나로 공통적으로 작성되던 코드의 양을 줄일 수 있다.

 

물론 pointCut 표현식으로 위치를 지정할 수 있으며 아래와 같이 Logging을 적용하기도한다.

@Slf4j
@Aspect
@Component
public class AccessLoggingAspect {

    @Pointcut("execution(* *..*Controller.*(..)) && !@annotation(com.xxx.yyyy.support.annotations.SkipLogging)")
    public void pointCut() {
    
    }

    @Around("pointCut() && args(..,@RequestBody body)")
    public Object accessLogging(ProceedingJoinPoint joinPoint, Object body) throws Throwable {
    		// 생략
    }
}

위의 logging 예시에서는 SkipLogging이라는 Annotation이 달려있는곳을 제외한 모든 Controller에 적용되는것을 뜻한다.

 

 

마무리

사실 @Before, @After, @Around 등 더 설명하고 이해해야할 부분이 많지만.. AOP에 대해서 조금이라도 이해하고 있다면 다 아는 내용이라 생각하고, 설명을 skip 하였음 (다른 좋은 블로그들이 많아서..)

 

대부분에 예제들이 logging을 다루었길래.. 간단하게 예제를 만들어서 이렇게도 공통 로직을 AOP로 담아서 적용할 수 있음을 보여주고 싶어서 작성했는데 많은 사람들에게 도움이 되었으면 좋겠다...

 

이런말을 왜쓰나 싶었는데 글을 게시해보니 댓글 하나하나가 소중하게 느껴지네요..

도움이 되셨다면 댓글하나 부탁드려요 ㅎㅎ

 

반응형