AOP 는 왜 필요한 걸까?

김법우·2022년 12월 2일
1

Nest.js

목록 보기
9/10
post-thumbnail

AOP 란??

Aspect Oriented Programming

AOP 는 Aspect Oriented Programming 의 약자이다. 단어 자체만 본다면 Aspect 는 관점, Oriented 는 지향하다 즉, 관점 지향적 프로그래밍이라고 할 수 있다. 관점을 중심으로 두고, 지향하는, 그런 프로그래밍을 말한다.

그렇다면 여기서 말하는 관점(Aspect)란 무엇일까? AOP 에서 말하는 Aspect 는 흩어진 관심사(concern)을 응집시킨 것을 말한다.

기존 객체지향 프로그래밍에서는 설계시 책임, 관심사에 따라 단일 책임을 지도록 클래스를 분리한다. 객체지향적 설계로 인해 응집도가 높아지고 결합도는 낮아진 모듈들은 수정과 변경에 대한 책임의 전파를 약화 시키므로 변화하는 요구사항에 대해 보다 안전한, 생산성 높은 프로그래밍이 가능해진다.

하지만 특정 클래스들에서 공통적으로 사용하는 기능이 있을 수 있다. 로깅, 트랜잭션, 캐싱, 보안 등등 클래스에서 비즈니스 로직에 부가적으로 들어가게 되는 기능들이다.

이렇게 공통적으로 사용하는 기능들이 파편화되어 여러 클래스에 산재되어 있게 되는데, 사실은 이것들은 하나의 책임을 지는 기능으로 묶을 수 있다. 즉 이런 파편화된 관심사를 모은 것을 Aspect 라고 한다.

AOP 는 대체 왜 등장하게 된걸까

서드파티 서비스로 요청을 보내는 경우 실패시 별도의 로그 파일을 저장해야하는 요구사항이 있다고 생각해보자. 이 요구사항에 따라 서드파티 서비스로 요청을 보낸 뒤 실패 응답을 받을 경우 로그 파일을 저장하기 위한 S3StreamLogger.error() 를 호출해야한다.

(이렇게 Aspect 의 구현체가 수행되는 시점을 Join Point 라고 한다)

// MessageService.ts
async sendKakaoMessage(...){
	try{
		... 각종 서드파티 요청 비즈니스 로직
	} catch(e){
	  >>> 실패시 s3 파일 로깅 로직 <<<
		S3StreamLogger.error(...)
	}
	...
}

// PushNotificationService.ts
async sendPushMessage(...){
	try{
		... 각종 서드파티 요청 비즈니스 로직
	} catch(e){
	  >>> 실패시 s3 파일 로깅 로직 <<<
		S3StreamLogger.error(...)
	}

	...
}

// WeatherService.ts
async getTodayWeather(...){
	try{
		... 각종 서드파티 요청 비즈니스 로직
	} catch(e){
	  >>> 실패시 s3 파일 로깅 로직 <<<
		S3StreamLogger.error(...)
	}

	...
}

우리는 MessageService, PushNotificationService, WeatherService 를 관심사에 맞게 잘 분리하여 단일 책임을 지도록 설계했다고 생각했지만, 혹시나 로그 파일을 저장하는 정책이 바뀌거나 함수의 구현이 수정되는 경우 S3StreamLogger.error() 를 사용하는 모든 서비스 클래스는 영향을 받게 된다. 이는 분리된 관심사에 의해 초래된 수정이 아니므로 단일 책임 원칙 자체를 위배하게 된다.

위에서는 단순히 로깅 기능에 대해서만 이야기했지만 실제로는 캐싱, 트랜잭션, 보안처리 등등 여러개의 부가 기능이 끼워져 있는 경우가 많으므로 사실상 수 많은 모듈들이 각종 부가 기능의 수정에 대해 책임을 져야하는 머리아픈 상황을 맞이하게 된다.

그렇다면, AOP 는 이 문제를 어떤식으로 해결할까?

AOP 를 적용하게 되면 먼저, 파편화된 공통 기능을 하나의 모듈 혹은 클래스로 처리한다. 처음에 이야기한 “관점”에 대해서만 책임을 지도록 응집화 시키는 것이다. 그 다음은 공통 기능을 사용할 각각의 클래스, 모듈에 덮어씌워서 사용한다.

S3StreamLogger(...) {

	>>> 서드파티에 푸시 메세지 전송 요청 <<<
	await sendPushMessage(...)
	
	>>> 실패시 s3 파일 로깅 로직 <<<
	this.error(...) <

}

내가 공통 기능을 덮어씌운다고 이해한 이유는 어떤 식으로든 기존의 비즈니스 로직이 영향을 받지 않을려면 비즈니스 로직이 해당 공통 기능에 대해 몰라야하고, 개념적으로 의존성 관계에서 바깥쪽에 위치해야하기 때문이다.

각각의 공통 기능이 어떤 값을 받아서 어떠한 로직을 수행하고 어떤 값을 반환시켜야하는지에 따라 뭘 덮어쓸지 적절히 선택해야한다. 예를 들어 위에서 이야기한 로깅의 경우에는 각 비즈니스 로직을 호출하는 API endpoint 즉 controller 클래스에서 요청이 들어오고 나가는 과정을 덮어씌워서 해결 할 수도 있고, service 클래스의 각 메서드를 덮어씌워서 처리 할 수도 있다.

Nest.js 에서는 ..!

Nest.js 에서는 공식문서에서 말하기를 Interceptor 를 AOP 에서 영감을 받아 만들었다고 한다.

출처 : https://docs.nestjs.com/interceptors

  • 메서드 실행 전/후에 추가 로직 바인딩
  • 함수에서 반환된 결과 변환
  • 함수에서 발생한 예외 변환
  • 기본 기능 동작 확장
  • 특정 조건에 따라 함수를 완전히 재정의합니다(예: 캐싱 목적).

목적에서 볼 수 있듯이 각 항목은 공통된 관심사이며, 이런 관심사들을 들어오고 나가는 요청/응답을 처리하는 controller 클래스에 적용 할 수 있다.

마치며

다음 포스팅에서는 Nest.js 에서 interceptor 를 사용해 로깅이라는 관심사를 처리하는 방법과, Method Decorator 를 통해 캐싱을 수행하는 방법에 대해 간단히 다뤄보고자 한다.

Method Decorator 의 경우에는 두 달전쯤 Nest.js 밋업을 듣다가 Toss 개발팀에서 발표한 AOP 모듈에 대해 보다가 알게 되었다. 그때 발표가 너무 재미있어 Nest.js 공식 레포에 올리셧다는 풀리퀘 코드도 찾아보고, 발표 자료도 다시 보면서 직접 구현해봤던 기억이 난다.

그 당시에는 지금 처럼 부가 기능이 많아지지 않아서 도입에 큰 필요성을 느끼지 못했는데, 지금은 미리 해둘걸 하는 생각도 든다 ㅠㅠ. AOP 는 개념상으로 단순해 보이지만, 실제로 구현되었을 때의 가치는 어마무시하다. 비즈니스 로직이 순전히 자신의 관심사만 처리하고, 자신의 요구사항 수정에 따라서만 변경되는 것, 다양한 부가 기능을 블럭처럼 끼웟다 뺼 수 있다는 것은 정말 강력한 것 같다.

참고 자료 😃

https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/

https://code-lab1.tistory.com/193

https://atoz-develop.tistory.com/entry/Spring-스프링-AOP-개념-이해-및-적용-방법

http://www.incodom.kr/spring/AOP

https://tasddc.tistory.com/m/129

https://docs.nestjs.com/interceptors

profile
개발을 사랑하는 개발자. 끝없이 꼬리를 물며 답하고 찾는 과정에서 공부하는 개발자 입니다. 잘못된 내용 혹은 더해주시고 싶은 이야기가 있다면 부디 가르침을 주세요!

0개의 댓글