2025-09-22 23:26
-
클린 아키텍처는 소프트웨어의 유지보수성과 테스트 용이성을 높이기 위해 관심사를 분리하는 아키텍처 원칙이다.
-
의존성 규칙을 통해 내부 계층이 외부 계층의 변화에 영향을 받지 않도록 설계하여 유연하고 확장 가능한 시스템을 구축한다.
-
엔티티, 유스케이스, 인터페이스 어댑터, 프레임워크 및 드라이버의 네 가지 계층으로 구성되며, 각 계층은 명확한 책임을 가진다.
클린 아키텍처 완벽 핸드북 당신의 코드를 구원할 설계 철학
소프트웨어 개발의 세계는 끊임없이 변화한다. 새로운 프레임워크, 라이브러리, 기술이 매일같이 쏟아져 나온다. 이러한 변화의 홍수 속에서 어떻게 하면 흔들리지 않고 안정적이며, 오랫동안 가치를 유지하는 소프트웨어를 만들 수 있을까? 이 질문에 대한 가장 강력한 해답 중 하나가 바로 **클린 아키텍처(Clean Architecture)**다.
클린 아키텍처는 단순히 코드 몇 줄을 바꾸는 기술이 아니다. 소프트웨어를 설계하는 근본적인 철학이자, 시스템의 생명력을 길게 유지하고 개발자의 정신 건강을 지켜주는 강력한 나침반이다. 이 핸드북은 클린 아키텍처의 탄생 배경부터 핵심 구조, 실제 적용 방법, 그리고 심화 탐구까지 모든 것을 담아낼 것이다. 이 글을 끝까지 읽는다면, 당신의 코드는 더 이상 외부의 변화에 쉽게 흔들리지 않는 견고한 성채가 될 것이다.
1. 클린 아키텍처는 왜 세상에 나왔을까
클린 아키텍처의 탄생 배경을 이해하려면 소프트웨어 개발의 고질적인 문제점을 먼저 짚고 넘어가야 한다.
문제점 투성이의 개발 현실
프로젝트 초기에는 모든 것이 순조로워 보인다. 하지만 시간이 흐르고 기능이 추가되면서 코드는 점점 복잡해지고 서로 얽히기 시작한다. 우리는 이것을 스파게티 코드라고 부른다. 작은 기능 하나를 수정했을 뿐인데 예상치 못한 곳에서 버그가 터져 나온다. 데이터베이스를 바꾸거나, 특정 프레임워크를 업그레이드하는 것은 상상조차 하기 힘든 대공사가 된다.
이러한 문제의 근본 원인은 **관심사의 분리(Separation of Concerns)**가 제대로 이루어지지 않았기 때문이다. 비즈니스 로직, UI, 데이터베이스 접근 코드가 한데 뒤섞여 있으면, 어느 한 부분의 변화가 시스템 전체에 영향을 미치는 것은 당연한 수순이다.
-
취약성 (Fragility): 한 곳을 수정하면 다른 곳이 깨진다.
-
경직성 (Rigidity): 변경하기가 너무 어려워 새로운 기능 추가를 꺼리게 된다.
-
비이동성 (Immobility): 다른 시스템에서 재사용할 수 있는 코드가 거의 없다.
이러한 문제들은 개발 비용을 눈덩이처럼 불리고, 개발자의 번아웃을 초래하며, 결국 프로젝트의 실패로 이어진다.
로버트 C. 마틴의 외침 의존성을 역전시켜라
이런 혼란 속에서 소프트웨어 장인정신의 대가인 **로버트 C. 마틴(Robert C. Martin, 이하 ‘엉클 밥’)**은 여러 아키텍처 원칙들을 집대성하여 ‘클린 아키텍처’라는 이름으로 세상에 내놓았다. 그의 핵심 주장은 간단했다.
“좋은 아키텍처는 시스템을 유연하고, 테스트하기 쉽고, 유지보수하기 쉽게 만들어야 한다. 이를 위한 핵심은 **의존성 규칙(Dependency Rule)**을 지키는 것이다.”
클린 아키텍처의 심장에는 바로 이 의존성 규칙이 있다. 모든 소스 코드 의존성은 외부에서 내부로, 고수준 정책을 향해야 한다. 이것이 바로 클린 아키텍처의 알파이자 오메가다.
쉽게 비유하자면, 집을 짓는 것과 같다. 집의 가장 중요한 핵심은 ‘가족이 편안하게 사는 공간’이라는 **핵심 정책(비즈니스 로직)**이다. 벽지를 바꾸거나, 가구를 교체하거나, 심지어 외부 전기 시스템(프레임워크, DB)을 다른 회사 것으로 바꾼다고 해서 집의 본질적인 목적이 변해서는 안 된다. 클린 아키텍처는 이처럼 외부의 구체적인 세부 사항들이 내부의 핵심 비즈니스 로직에 영향을 주지 못하도록 설계하는 방법론이다.
2. 클린 아키텍처의 핵심 구조 파헤치기
클린 아키텍처는 동심원 형태의 계층 구조로 표현된다. 바깥쪽 원은 구체적인 기술(세부 사항)을, 안쪽 원은 추상적인 정책(핵심 로직)을 나타낸다.
| 계층 (Layer) | 설명 | 예시 |
|---|---|---|
| 엔티티 (Entities) | 전사적인 핵심 비즈니스 규칙을 캡슐화한다. 가장 일반적이고 높은 수준의 규칙이다. | User, Product, Order 객체와 그 안의 핵심 로직 |
| 유스케이스 (Use Cases) | 애플리케이션에 특화된 비즈니스 규칙을 담는다. 시스템의 모든 사용 사례(기능)를 표현한다. | CreateUser, PlaceOrder, GetProductDetails |
| 인터페이스 어댑터 (Adapters) | 유스케이스와 프레임워크 간의 데이터 형식을 변환하고 통신을 중재한다. | UserController, ProductPresenter, OrderRepository |
| 프레임워크 및 드라이버 (Frameworks) | 가장 바깥쪽 계층. 웹 프레임워크, 데이터베이스, UI 등 구체적인 기술들로 구성된다. | Spring Boot, React, MySQL, Kafka |
핵심 원칙 의존성 규칙 (The Dependency Rule)
앞서 언급했듯이 클린 아키텍처의 가장 중요한 규칙은 의존성의 방향이다.
안쪽 원은 바깥쪽 원에 대해 아무것도 알지 못한다.
-
엔티티는 유스케이스, 어댑터, 프레임워크에 대해 전혀 모른다.
-
유스케이스는 엔티티에 대해서는 알지만, 어댑터나 프레임워크에 대해서는 모른다.
-
어댑터는 유스케이스와 엔티티에 대해서는 알지만, 프레임워크에 대해서는 모른다.
-
오직 가장 바깥쪽 프레임워크 계층만이 내부 계층에 대한 의존성을 가질 수 있다.
하지만 여기서 의문이 생긴다. 유스케이스가 데이터베이스에 데이터를 저장하려면 데이터베이스에 대해 알아야 하는 것 아닐까? 아니다. 바로 이 지점에서 **의존성 역전 원칙(Dependency Inversion Principle)**이 마법을 부린다.
유스케이스는 구체적인 데이터베이스 기술(MySQLRepository)을 직접 참조하는 대신, 추상적인 인터페이스(UserRepository)에 의존한다. 그리고 실제 구현체는 바깥쪽 계층(프레임워크)에서 생성하여 안쪽으로 **주입(Dependency Injection)**해준다.
// 유스케이스 계층 (내부)
public interface UserRepository {
User findById(String id);
void save(User user);
}
public class CreateUserUseCase {
private final UserRepository userRepository;
public CreateUserUseCase(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void execute(User user) {
// ... 비즈니스 로직 ...
userRepository.save(user);
}
}
// 프레임워크 계층 (외부)
public class MySQLUserRepository implements UserRepository {
// ... JDBC, JPA 등 구체적인 기술로 구현 ...
}
// 애플리케이션 실행 시점
UserRepository repository = new MySQLUserRepository();
CreateUserUseCase useCase = new CreateUserUseCase(repository); // 의존성 주입
이 구조 덕분에, 나중에 데이터베이스를 MySQL에서 PostgreSQL로 바꾸더라도 CreateUserUseCase 코드는 단 한 줄도 수정할 필요가 없다. 그저 PostgreSQLUserRepository라는 새로운 구현체를 만들어 주입해주기만 하면 된다. 핵심 비즈니스 로직이 외부 기술 변화로부터 완벽하게 보호되는 것이다.
3. 클린 아키텍처 실제로 적용하는 방법
이제 이론을 넘어서 실제 프로젝트에 클린 아키텍처를 어떻게 적용할 수 있는지 단계별로 알아보자.
1단계: 핵심 도메인(엔티티) 정의하기
가장 먼저 해야 할 일은 시스템의 심장인 엔티티를 식별하고 정의하는 것이다. 엔티티는 특정 프레임워크나 데이터베이스 기술에 의존하지 않는 순수한 비즈니스 객체(POJO, Plain Old Java Object)여야 한다.
-
어떤 데이터와 그 데이터를 조작하는 핵심 규칙이 무엇인가?
-
이 시스템이 없다면 수동으로 처리해야 할 비즈니스 프로세스는 무엇인가?
예를 들어, 전자상거래 시스템이라면 Product, Order, Customer 같은 객체들이 엔티티가 될 것이다. Order 엔티티 안에는 ‘주문 총액은 상품 가격의 합계여야 한다’ 와 같은 핵심 비즈니스 규칙이 메서드로 포함될 수 있다.
2단계: 유스케이스(인터랙터) 설계하기
다음은 사용자가 시스템을 통해 달성하고자 하는 목표, 즉 유스케이스를 정의한다. 각 유스케이스는 하나의 특정 기능을 책임진다.
-
사용자는 이 시스템으로 무엇을 할 수 있는가?
-
‘사용자 등록하기’, ‘상품 주문하기’, ‘게시글 작성하기’ 등
유스케이스는 입력 데이터(Input Port)를 받아 엔티티를 조작하고, 결과 데이터(Output Port)를 출력하는 역할을 한다. 중요한 것은 유스케이스는 ‘어떻게’ 보여주고 ‘어떻게’ 저장할지에 대해서는 전혀 관심이 없다는 점이다. 오직 비즈니스 로직의 흐름을 지휘할 뿐이다.
3단계: 인터페이스(포트와 어댑터) 정의하기
유스케이스가 외부 세계와 통신할 수 있도록 **인터페이스(Ports)**를 정의한다. 이 인터페이스는 유스케이스 계층 내부에 위치한다.
-
Input Port: 유스케이스를 실행하기 위한 인터페이스 (
CreateUserUseCase인터페이스) -
Output Port: 유스케이스의 결과를 외부로 전달하기 위한 인터페이스 (
UserRepository인터페이스)
그리고 이 인터페이스에 대한 실제 구현체인 **어댑터(Adapters)**를 인터페이스 어댑터 계층에 만든다.
-
웹 컨트롤러: 사용자의 HTTP 요청을 받아 Input Port를 호출한다.
-
데이터베이스 리포지토리: Output Port를 구현하여 실제 데이터베이스 작업을 수행한다.
-
프레젠터: 유스케이스의 결과를 받아 UI가 이해할 수 있는 데이터 모델(ViewModel)로 변환한다.
4단계: 외부 계층(프레임워크) 구성하기
마지막으로 가장 바깥쪽 계층에서 모든 것을 조립한다.
-
웹 프레임워크(Spring Boot, Express 등)를 설정한다.
-
데이터베이스 연결 설정을 구성한다.
-
의존성 주입(DI) 컨테이너를 사용하여 어댑터 구현체들을 생성하고 유스케이스에 주입한다.
이러한 방식으로 계층을 나누고 의존성 규칙을 따르면, 각 부분은 독립적으로 개발하고 테스트할 수 있게 된다. 예를 들어, UI나 데이터베이스가 준비되지 않은 상태에서도 유스케이스와 엔티티의 비즈니스 로직을 완벽하게 테스트할 수 있다.
4. 클린 아키텍처 심화 탐구 및 현실적인 조언
클린 아키텍처는 강력하지만, 모든 상황에 적용해야 하는 만병통치약은 아니다. 몇 가지 심화 주제와 현실적인 조언을 통해 이해를 돕고자 한다.
클린 아키텍처 vs 헥사고날 아키텍처 vs 어니언 아키텍처
클린 아키텍처를 공부하다 보면 **헥사고날 아키텍처(Hexagonal Architecture, 포트와 어댑터 아키텍처)**나 **어니언 아키텍처(Onion Architecture)**와 같은 용어를 접하게 된다. 이들은 표현 방식과 세부 용어에 약간의 차이가 있을 뿐, 근본적으로 같은 목표를 추구한다.
-
핵심 목표: 비즈니스 로직을 외부 기술로부터 분리하고 보호한다.
-
핵심 원칙: 의존성 역전 원칙을 통해 의존성의 방향을 내부로 향하게 한다.
따라서 어떤 이름을 사용하든 그 뒤에 숨겨진 철학을 이해하는 것이 훨씬 중요하다. 이들은 모두 관심사 분리와 의존성 역전이라는 거인의 어깨 위에 서 있는 아키텍처다.
모든 프로젝트에 클린 아키텍처가 필요할까?
정답은 ‘아니오’다. 클린 아키텍처는 초기 설정에 상대적으로 많은 코드(인터페이스, DTO 등)를 요구한다. 따라서 다음과 같은 경우에는 오버 엔지니어링이 될 수 있다.
-
프로토타입 또는 단기 프로젝트: 빠르게 만들고 버릴 목적의 프로젝트에는 적합하지 않다.
-
매우 단순한 CRUD 애플리케이션: 복잡한 비즈니스 로직이 없는 단순한 시스템에는 더 간단한 계층형 아키텍처가 효율적일 수 있다.
하지만 다음과 같은 프로젝트라면 클린 아키텍처는 최고의 선택이 될 것이다.
-
장기적으로 유지보수해야 하는 대규모 시스템
-
비즈니스 로직이 복잡하고 자주 변경될 가능성이 있는 시스템
-
특정 프레임워크나 데이터베이스에 종속되고 싶지 않은 경우
-
높은 수준의 테스트 커버리지가 요구되는 시스템
현실적인 트레이드오프
클린 아키텍처를 적용하면 계층 간 데이터 전달을 위해 DTO(Data Transfer Object)를 사용하게 되는데, 이로 인해 보일러플레이트 코드가 증가할 수 있다. 엔티티, 유스케이스 요청/응답 모델, 뷰 모델 등 비슷한 구조의 객체들이 여러 개 생길 수 있다.
이는 유지보수성과 유연성을 얻기 위해 지불하는 비용이다. MapStruct나 ModelMapper 같은 라이브러리를 사용하면 이러한 변환 코드를 줄일 수 있지만, 결국 아키텍처의 이점과 구현의 복잡성 사이에서 현명한 트레이드오프를 결정하는 것이 중요하다.
결론: 변화를 두려워하지 않는 견고한 소프트웨어를 향하여
클린 아키텍처는 단순히 코드를 정리하는 기술적인 방법을 넘어선다. 그것은 변화에 대응하는 소프트웨어를 만드는 철학이다.
세상의 모든 것은 변한다. 비즈니스 요구사항도, 기술 트렌드도, 심지어 우리가 사용하는 프로그래밍 언어도 변할 수 있다. 클린 아키텍처는 이러한 변화의 파도가 덮쳐올 때, 우리의 핵심 비즈니스 로직이라는 소중한 자산을 안전하게 보호해주는 방파제 역할을 한다.
처음에는 다소 복잡하고 번거롭게 느껴질 수 있다. 하지만 이 원칙을 이해하고 꾸준히 적용해 나간다면, 당신의 소프트웨어는 시간의 시험을 견뎌내고 오랫동안 가치를 발하는 견고한 자산이 될 것이다. 더 이상 외부의 변화에 휘둘리지 말고, 소프트웨어의 본질에 집중하라. 클린 아키텍처가 그 길을 밝혀줄 것이다.