Server System/Software Architecture

클린 아키텍쳐 vs 헥사고날 아키텍쳐 (1)

알파해커 테크노트 2024. 6. 3. 22:06
반응형

클린아키텍쳐를 처음 소개한 Bob Martin의 블로그(blog entry about Clean Architecture)에 다음과 같은 말이 나온다(https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).

"The diagram at the top of this article [Clean Architecture] is an attempt at integrating all these architectures [including Hexagonal Architecture] into a single actionable idea."

 

즉, 헥사고날 아키텍쳐와 클린 아키텍쳐는 서로 다른 개념이 아니고, 헥사고날 아키텍쳐는 클린 아키텍쳐를 구체화한 모델이다.


 

헥사고날 아키텍쳐와 클린 아키텍쳐가 공통적으로 가지는 가장 주요한 목표는

  1. 외부 요소와 내부 도메인 로직을 구분하고,
  2. 외부 요소의 변화에 내부 도메인이 영향을 받지 않도록 하며,
  3. 내부 도메인 로직의 테스트 용이성을 높이는 것이다.

이를 수행하기 위해, 다음 2가지를 고려해야 한다.

 

(1) 비즈니스 로직과 외부 요소의 구분

예를 들면, 우리가 만들고자 하는 서비스가 데이터베이스에서 a라는 값과 b라는 값을 가져와서 더하고 그 결과를 응답하는 것이라고 해보자. 여기서 비즈니스 로직은 a와 b를 합하고 응답을 주는 것이다.

return a + b

 

이때 외부 요소는 데이터베이스이다. 외부 요소란 비즈니스 로직을 구현하기 위해 사용될 수 있는 서비스/프레임워크/도구를 의미한다고 볼 수 있다. 즉, 내가 어떠한 비즈니스 로직을 구현하기 위해 다른 누군가가 만들어놓은 서비스나 도구를 사용할 것이고, 그것이 대체 가능하다면 외부 요소라고 할 수 있다. (데이터베이스는 MySQL, DynamoDB, Redis 등 어떤 것이든 될 수 있다.)

 

(2) 의존성 방향을 통한 분리

그렇다면, 외부 요소는 왜 비즈니스 로직과 분리해야 할까. 우리가 비즈니스 로직을 구현하면서 가져다 쓰는 툴은 언제든지 바뀔 수 있기 때문이다. 예를 들면, 성능이나 비용 등의 이유로 언제든지 데이터베이스로 MySQL을 사용하다가 DynamoDB로 변경할 수도 있고, 그 과정이 조금이라도 쉬우려면 외부 요소가 비즈니스 로직과 분리되어 있어야 한다.

 

의존성 방향을 비즈니스 로직에서 외부 요소로 향하게 하지 않고, 반대로 외부 시스템이 내부 도메인 로직에 의존하도록 설계해야 한다. 그래야만 외부 요소와 비즈니스 로직을 분리하고, 외부 요소의 변경을 쉽게하며, 테스트 용이성을 높일 수 있다.

 

의존성 방향이란 누가, 누구를 사용하는지에 관련한 것이다.

 

 

아래의 코드는 의존성의 방향이 비즈니스 로직에서 외부 요소(DB)로 향하는 것이다. 코드는 크게 BusinessLogic과 MySQL이라는 두 개의 클래스로 구성되어 있다. BusinessLogic 클래스는 MySQL로 부터 a, b 두 개의 값을 받은 후 더하는 역할을 하고 있다.

이 때, 외부요소가 변경되어도 바뀌지 않는 핵심 비즈니스 로직은 “a+b”이고, MySQL은 외부요소이다. 그리고 작성된 코드를 보면 BusinessLogic이라는 클래스가 MySQL 객체를 생성하고, 해당 객체의 함수를 호출하여 “사용”하고 있다.

class BusinessLogic:
	def __init__():
		self._mysql = MySQL()
		
	def do():
		a = self._mysql.get_data("a")
		b = self._mysql.get_data("b")
		return a + b

class MySQL():
	def __init__():
		...
	
	def get_data(key):
		query = ...
		result = connection.execute(query) 
		return result

(위 코드는 의존 관계를 표현하기 위한 것으로 일부 설명에 불필요한 부분은 생략되어 있습니다.)

 

이 때, “사용”한다는 것이 “의존”한다는 것과 같은 의미라고 볼 수 있다. 왜냐하면, 만약 MySQL 클래스의 코드가 변경이 되면, BusinessLogic 클래스 코드를 수정해야 할 수도 있기 때문이다. 반대로 위와 같은 의존 관계에서는 BusinessLogic 클래스를 수정한다고 해서 MySQL 클래스의 코드는 수정할 필요가 없다.

이를 그림으로 나타내면 다음과 같이 표현할 수 있다.

 

 

위와 같은 의존 관계에 있다면, 만약 MySQL을 DynamoDB와 같이 다른 것으로 바꾸게 되었을때 BusinessLogic 클래스의 코드를 필수적으로 고치게 되어 있다.

 

그렇다면 BusinessLogic 클래스가 외부 요소로 부터 데이터를 가지고 와야 한다는 기능을 가지고 있으면서도, 외부 요소의 변경에도 영향이 없게 하려면 어떻게 해야할까. 의존 방향을 외부 요소가 비즈니스 로직이 있는 방향으로 향하게 “역전”시키면 된다.

 

외부 요소가 비즈니스 로직을 향하게 하려면, 아래와 같이 변경하면 된다. 비즈니스 로직이 있는 영역에 저장된 값을 가져오는 인터페이스를 만들고, 그 인터페이스를 외부 요소가 상속하는 방식으로 하면, 외부 요소인 MySQL이 비즈니스 로직 방향으로 의존하는 형태가 된다.

 

상속을 하는 방향도 의존 방향이다.

 

 

위 그림에서 비즈니스 로직의 영역을 BusinessLogic 클래스 뿐만 아니라 인터페이스 영역까지(분홍색 영역)로 확대하고, 그 중 외부 요소와의 인터페이스의 역할을 하는 DB 인터페이스를 통해, 외부요소인 MySQL이 상속할 수 있도록 하면, 외부 요소가 비즈니스 로직 방향으로 의존하게 만들 수 있다.

 

이것을 통해 의존성 “역전”이 만들어 진 것이고, MySQL이 아닌 DynamoDB와 같은 다른 외부 요소를 사용하고 싶다면, DB 인터페이스를 상속한 DynamoDB라는 새로운 클래스를 만들어 적용하면 된다. 그 과정에서 비즈니스 로직의 영역의 변경은 없다. 심지어 MySQL 클래스 코드 조차도.

 

이것이 DI(Dependency Injection)의 개념이고, 코드를 확장성에 열려있게 구현하는 방법이다.

 

위 의존성 관계를 코드로 표현하면 다음과 같다. BusinessLogic 코드 어디에도 MySQL과 같은 외부 요소에 대한 코드가 없다는 것을 알 수 있다. 다만, 인터페이스만이 존재한다.

 

class BusinessLogic:
	def __init__(db: DB):
		self._db = db
		
	def do():
		a = self._db.get_data("a")
		b = self._db.get_data("b")
		return a + b

class DB(metaclass=ABCMeta):
	@abstractmethod
	def get_data(key):
		pass

class MySQL(DB):
	def __init__():
		...
	
	def get_data(key):
		query = ...
		result = connection.execute(query) 
		return result

 

의존성 주입(DI, Dependency Injection)은 BusinessLogic 클래스를 생성하는 순간에 다음과 같이 외부 요소의 인스턴스를 전달함으로써 이루어진다.

 

business_logic = BusinessLogic(MySQL())

 

위와 같이 의존성 역전을 구현하는 방법이 디자인 패턴 중 어댑터 패턴(Adapter pattern)이다.

어댑터 패턴은 SOLID의 OCP, DIP 원칙을 활용한 것이다. https://guy-who-writes-sourcecode.tistory.com/31

 

 

 

반응형