Layered Architecture의 OCP 한계를 넘어: Hexagonal Architecture (1)


🧐잠깐만 아키텍처가 뭔데?
'Architecture' 의 영단어 뜻은 ’건축학‘ 이라는 뜻을 가지며, 대한민국에서는 건축학을 그냥 건축학이라 부르지 영어로 잘 사용하지는 않습니다. 그에 반해, 아키텍처란 단어는 IT 분야에 자주 쓰이는 용어 입니다. 나무위키는 ‘아키텍처’를 다음과 같이 정의 했습니다.
목표 대상의 구성과 동작 원리, 구성 요소 간의 관계 및 시스템 외부 환경과의 관계를 설명하는 설계도
- 출처: namuwiki
단순히 목적성을 가진 설계라고 생각하면 좋을 것 같습니다. IT에서는 시스템이란 단어가 너무 추상적이기에 보다 구체적인 인간의 몸을 생각해도 좋습니다.
뇌에서 신경을 자극하면 근육이 움직이고, 결국 뼈가 움직이면서 걷는 동작이 이루어집니다. '걷기'라는 단순한 행동조차 여러 요소가 체계적으로 연결된 설계(아키텍처)를 기반으로 이루어집니다. IT 시스템에서도 마찬가지로, 구성 요소들이 유기적으로 연결되어 동작하는 방식이 바로 아키텍처라고 할 수 있죠.
😉우리가 알고 있는 Layerd Architecture
단방향으로 흘러가는 레이어드 아키텍처(Layerd Architecture)는 마치 계층으로 구성되어 ‘계층적 아키텍처’로 불리기도 합니다. 논리적인 개념으로 살펴 본다면 물리적인 서버에서 프레젠테이션(presentation), 비즈니스 로직(business logic), 데이터 엑세스(data access)의 3개의 논리적으로 계층을 구분할 수 있죠.
각 계층의 역할은 이렇습니다.
계층 | 역할 | Spring 생태계 |
프레젠테이션(presentation) | 화면 표현 및 전환 처리 | 컨트롤러(Controller) |
비즈니스 로직(business logic) | 비즈니스 개념, 규칙, 흐름 제어 | 서비스(Service) |
데이터 엑세스(data access) | 데이터 처리 | 레파지토리(Repository) |
즉, 많은 개발자가 흔히 Spring으로 API를 개발할 때, Controller→ Service → Repository(Mapper) → DB 흐름으로 익숙한 개발을 합니다. 이러한 방식이 레이어드 아키텍처의 전형적인 형태 입니다.
🤔 우리가 Layerd Architecture를 자주 접한 이유
레이어드 아키텍처는 단순하면서도 계층 간 응집도를 높이고, 의존도를 낮출 수 있습니다. 이는 레이어드 아키텍처가 추구하는 규칙을 보면 알 수가 있습니다.
상위 계층이 하위 계층을 호출하는 단방향성을 유지
상위 계층은 하위 여러 계층을 모두 알 필요 없이 근접 계층만 활용
상위 계층이 하위 계층에 영향을 받지 않게 구현
계층 간에 인터페이스를 호출하여 구현 클래스에 미의존하여 약한 결합을 유지
인터페이스를 호출함으로써 다양한 방식의 구현체를 선택적으로 적용할 수 있습니다. 이는 객체지향 설계 원칙 중 DIP(의존성 역전 원칙)을 만족하여, 결과적으로 유연성이 뛰어난 설계가 가능해 보입니다.
또한, 단방향 구조 때문에 학습 곡선이 다른 아키텍처에 비해 낮다는 장점도 있습니다. 덕분에 개발 생산성이 높고, 실무에서도 쉽게 적용할 수 있죠!
Spring Boot, Django, .NET 등 대부분 주요 웹 프레임워크에서는 기본적으로 레이어드 아키텍처를 따르는 구조를 제공합니다. 특히, Spring Boot에서는 Controller → Service → Repository 구조가 자연스럽게 정착되어 있기 때문에 많은 개발자가 익숙하게 사용합니다.
🤯 Layerd Architecture 한계점
레이어드 아키텍처의 Service와 Repository의 사이에서 OCP(개방 폐쇄의 원칙)의 한계점이 발생합니다.
OCP: Open-Closed Principle ( 개방 폐쇄의 원칙 )
- 소프트웨어 개체는 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다
- 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안 된다는 개념입니다.
현재 레이어드 아키텍처는 각 계층이 자신이 제공하는 기능에 대한 추상적인 인터페이스를 직접 정의하고 소유하는 구조입니다.
아래 폴더 구조와 코드의 예시를 보시죠.
# layerd 폴더 구조
layerd
├─controller
├─service
│ ├─impl
│ └─interface
└─repository
├─impl
└─interface
Controller는 Service의 인터페이스를 의존합니다.
Service의 구현체는 Repository의 인터페이스를 의존합니다.
// 비즈니스 서비스 클래스
@Service
@RequiredArgsConstructor
public class ServiceImpl implements ServiceInterface {
// 데이터 액세스
private final RepositoryInterface repositoryInterface;
..
}
// 데이터 액세스 인터페이스
public interface RepositoryInterface {
...
}
// 데이터 액세스 A 클래스
@Repository
public class RepositoryImpl_A implements RepositoryInterface {
// 데이터 액세스
...
}
만약 RepositoryImpl_B
라는 데이터 액세스 B 클래스로 변경되었을 때, 다음과 같이 변경하면 비즈니스 서비스 클래스는 변경이 없어 보입니다.
// 데이터 액세스 A 클래스
// @Repository (@Repository 제거)
public RepositoryImpl_A implements RepositoryInterface {
// 데이터 액세스
...
}
// 데이터 액세스 B 클래스
@Repository
public RepositoryImpl_B implements RepositoryInterface {
// 데이터 액세스
...
}
하지만 현재 RepositoryInterface
자체가 다른 Repository의 인터페이스로 바뀌어야 한다면 어떨까요?
ServiceImpl
은 현재 RepositoryInterface
를 의존하고 있기 때문에, 다른 Repository의 인터페이스로 바뀌어야 한다면 수정이 필요합니다.
예를 들어, 데이터 액세스 계층에 자주 사용되고 있는 MyBatis와 JPA 라이브러리가 있습니다. 현재 애플리케이션이 MyBatis를 적용하고 있다고 가정합니다. 이때 ServiceImpl
이 MyBatis 기반 MyBatisRepository
를 의존하고 있다면, 다음과 같이 작성됩니다.
@Service
@RequiredArgsConstructor
public class ServiceImpl implements ServiceInterface {
private final MyBatisRepository myBatisRepository ;
public List<Data> getData() {
return myBatisRepository.findAll();
}
}
// MyBatis 데이터 액세스 인터페이스
public interface MyBatisRepository {
List<Data> findAll();
}
// MyBatis 기반 Repository 구현체
@Repository
public class MyBatisRepositoryImpl implements MyBatisRepository {
private final SqlSession sqlSession;
...
}
특정 요구사항에 따라 JPA로 전환해야 하는 상황이 발생합니다. 그럼 하기와 같이 Service 클래스와 같이 수정이 필요합니다.
@Service
@RequiredArgsConstructor
public class ServiceImpl implements ServiceInterface {
// private final MyBatisRepository myBatisRepository 삭제 후 JPA로 변경
private final JpaDataRepository jpaDataRepository ;
public List<Data> getData() {
return jpaDataRepository.findAll();
}
}
// JPA 기반 Repository 인터페이스
public interface JpaDataRepository extends JpaRepository<Data, Long> {
}
MyBatis와 JPA는 데이터 액세스 방식이 다르므로, 기존 RepositoryInterface
를 MyBatis용 인터페이스와 JPA용 인터페이스로 변경해야 한다면 ServiceImpl도 이에 따라 변경이 필요합니다. 이는 OCP 원칙에 한계가 생기네요. 행위의 변화를 주고 싶은데, 변경이 필요합니다.
불편함이 생깁니다. 게다가 Service 계층은 비즈니스 로직을 담당하기에 해당 애플리케이션에 고수준 영역으로 분류됩니다. 고수준의 영역을 수정 없이, 행위의 변경을 자유롭게 할 수 있는 방법이 있을까요?
데이터 엑세스 계층에서 정의한 인터페이스를 비즈니스 로직 계층으로 옮기는 겁니다.
아래 그림처럼요!
새로운 DIP(의존성 역전 원칙)을 적용하여 의존 관계가 역전되었습니다.
비즈니스 로직 계층(고수준)에서 데이터 액세스 인터페이스를 정의함으로써, 기존의 위->아래 흐르던 의존 관계를 뒤집고, 데이터 액세스(저수준) 변경이 비즈니스 로직(고수준)에 영향을 주지 않도록 개선되었습니다.
위를 응용하여 확장성의 유연한 Hexagonal Architecture 가 탄생합니다.
2편에서 계속 보시죠 :)
Subscribe to my newsletter
Read articles from silberbullet directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

silberbullet
silberbullet
모든 언어와 솔루션에 고정관념을 버리고 접근하는 개발자