새로운 프로젝트에 합류했다. 개발은 어느 정도 진행된 상태였지만, 인수인계를 제대로 받지 못한 상황이었다.
게다가 새로운 인원들이 추가되면서 기존의 개발 방향을 전면 수정하고, 아예 새롭게 시작하기로 결정되었다.
그 과정에서 아키텍처 설계부터 다시 시작했고, 연차가 더 많으신 분들이 중심이 되어 클린 아키텍처(Clean Architecture) 로 방향을 정해주셨다.아직 클린 아키텍처에 대해 깊이 있게 이해하고 있는 건 아니지만, 이 프로젝트의 구조만큼은 확실히 익히고 가야겠다는 생각이 든다.
그래야 개발도 더 수월하게 따라갈 수 있고, 나에게 남는 것도 많을 것이라 생각한다. 그래서 이 아키텍처에 대해 정리해보려 한다.
계층형-아키텍처(Layerd-Architecture) 4월 6일자 수정
우선 나는 이 때까지 거의 밑과 같은 계층형-아키텍처(Layerd-Architecture) 의 구조로 프로젝트를 진행하였다.
src/
└── main/
└── java/
└── com/
└── example/
└── user/
├── controller/
│ └── UserController.java
├── service/
│ └── UserService.java
└── repository/
└── UserRepository.java
정리하자면, 소프트웨어 아키텍처에서 계층(레이어) 개념
Controller-Servcie-Repository는 전형적인 계층형 아키텍처(Layered Architecture)이다.
이 구조에서 "상위"와 "하위"는 네트워크나 사용자에 대한 거리를 기준으로 합니다:
- 컨트롤러(Controller) - 최상위 계층
- 클라이언트(사용자/외부 시스템)와 직접 통신
- HTTP 요청을 받고 응답을 반환
- 입력 데이터 검증, URL 라우팅 담당
- 서비스(Service) - 중간 계층
- 비즈니스 로직 처리
- 트랜잭션 관리
- 여러 레포지토리 조합하여 복잡한 작업 수행
- 레포지토리(Repository) - 최하위 계층
- 데이터베이스와 직접 통신
- 데이터 CRUD 작업 수행
- 데이터 영속성 담당
여기서 "상위"는 사용자나 외부 시스템에 가까울수록, "하위"는 데이터베이스나 영속성 계층에 가까울수록 적용된다.
정보와 제어의 흐름은 일반적으로 상위에서 하위로 이동합니다.
의존성 방향은 보통:
Controller → Service → Repository → Database
이렇게 상위 Layer는 하위 Layer에만 의존하는 단방향으로 흐르는 것이다.
즉,Controller는 Service를 알지만,Service는 Controller를 모른다. Service는 Repo를 알지만, Repo는 Service를 모른다.
이 구조로 프로젝트를 진행했을 떄 나의 생각은 다음과 같다 .
라고 생각 했지만 이 구조는 기능(feature) 중심으로 코드를 조직화한 방식이다 .
단순 각 기능 내부에서 MVC 또는 계층형 아키텍처의 요소들을 포함시킨 형태이다.
내가 했던 구조가 계층형 아키텍처와 다른 점
지금 현재 판단해보았을 때, 판전통적인 계층형 아키텍처와도 다르다고 생각한다.
- 수평적 vs 수직적: 전통적인 계층형 아키텍처는 전체 애플리케이션을 수평적 계층으로 나눈다.
(모든 컨트롤러가 한 계층에, 모든 서비스가 다른 계층에).
반면 이 구조는 먼저 수직적으로 기능별로 나눈 후, 각 기능 내에서 계층화한다. - 기술적 관심사 vs 기능적 관심사: 계층형 아키텍처는 기술적 관심사(프레젠테이션, 비즈니스 로직, 데이터 액세스)로 분리한다. 이
구조는 먼저 기능적 관심사로 분리한 후 기술적 관심사를 고려한다.
이 구조에 대한 이름과 패턴
이 접근 방식은 여러 이름으로 정의할 수 있어보인다.
- 기능 모듈화(Feature Modularization)
- 기능 중심 아키텍처(Feature-Centric Architecture)
- 수직 슬라이스 아키텍처(Vertical Slice Architecture)
- 패키지별 기능(Package by Feature) (대비: Package by Layer)
내가 생각한 장점과 단점
우선 장점은
1. 직관적인 네이밍으로 편리하다
2. 기능 중심으로 이루어져있고, 모두 같은 틀을 따르기 때문에 쉽게 알 수 있다.
단점이라면 ,
1. 테스트를 할 때 불편하였다. DB를 모두 종속 받다보니 Mocking을 하지 않는 이상 Test가 불가하였다.
2.또한 비즈니스 로직만을 테스트 하는 것이 어려웠다. 도메인 자체로직으로 사실상 만든 적이 없었다. 즉, 거의 통합 테스트로 진행하였다.
3.
public interface UserRepository extends JpaRepository<User, Long> { }
다음과 같은 코드를 쓴다고 가정하였을 떄, 우리 내부 DB는 또다른 외부 프레임워크인 JPA에 종속되는 것이므로 의존성 역전이 되버린다.
- 고수준 모듈이 저수준 모듈에 의존: Repository 인터페이스(도메인 계층의 일부)가 JPA(기술적 구현 세부사항)에 직접 의존하고 있다.
1. 추상화 관점에서의 고수준/저수준 • 고수준(High-level): 더 추상적이고, 비즈니스 규칙과 핵심 도메인에 가까운 요소 • 저수준 (Low-level): 더 구체적이고, 기술적 세부사항에 가까운 요소 이 관점에서는 도메인 계층이 고수준에 속한다.
비즈니스의 핵심 개념과 규칙을 다루기 때문이다.
2. 계층 구조 관점에서의 상위/하위 • 상위(Upper):
클라이언트(사용자)와 가까운 계층 (UI, Controller) • 하위(Lower): 데이터 소스와 가까운 계층 (Repository, DB) - 추상화와 세부 구현의 혼합: Repository가 JpaRepository를 상속함으로써, 도메인 계층의 추상화에 기술적 세부사항이 침투하게 된다.
public class DiaryController {
private final DiaryService diaryService;
public class DiaryService {
private final DiaryRepository diaryRepository;
public interface DiaryRepository extends JpaRepository<Diary, Long> {
//여기서 다시 내부가 외부를 향하고 있음
🤔 그렇다면 클린 아키텍처는 내가 생각한 이러한 단점을 디펜스 할 수 있는 설계를 갖추었는지 알아보자
클린-아키텍처(Clean-Architecture)
안쪽에 위치할수록 고수준 정책이며, 바깥쪽에 위치할 수록 저수준 정책을 의미한다.
Clean Architecture는 소프트웨어 시스템을 구성하는 방법에 대한 체계적인 접근법으로, 시스템을 여러 독립적인 계층으로 분리하여 유지보수성, 확장성 및 테스트 용이성을 향상시키는 아키텍처 패턴이다.
Clean-Architecture의 구성
엔티티 (Entities)
엔티티 계층은 도메인 계층으로도 불리며, Clean Architecture의 핵심부에 위치한다.
- 정의: 엔티티 계층은 하나 이상의 프로그램 간에 공유될 수 있다는 가정하에 만드는 수명이 긴 객체이다. (즉, 재사용의 가능성이 높다는 것을 인지하고 외부에 의해 변경될 가능성을 낮추어야 한다.)
- 역할: 이곳에는 Enterprise 규모의 비즈니스 데이터를 포함하거나 핵심이 되는 비즈니스 규칙을 캡슐화한다.
- 특성: 외부 계층의 변화에 영향을 받지 않도록 설계되며, 시스템의 가장 안정적인 부분이다.
- 예시: 사용자, 상품, 주문과 같은 비즈니스 객체와 이들의 핵심 비즈니스 로직이 여기에 포함된다.
유스케이스 (Use Cases)
유스케이스 계층은 애플리케이션의 특정 기능을 구현하는 비즈니스 로직을 포함한다.
- 정의: 애플리케이션 계층이라고도 하며, 애플리케이션 수준의 비즈니스 규칙을 담당한다.
- 역할: 엔티티와 상호작용하여 특정 비즈니스 목표를 달성하는 방법을 정의한다.
- 구성 요소:
- Use Case Interactor: 비즈니스 로직의 실행을 조정합니다.
- Use Case Input Port: 외부에서 유스케이스로 데이터를 전달하는 인터페이스
- Use Case Output Port: 유스케이스의 결과를 외부로 전달하는 인터페이스
- 특성: 특정 애플리케이션에 종속적이지만, 프레임워크나 UI와 같은 외부 요소와는 독립적입니다.
- 예시: 사용자 등록, 주문 처리, 결제 프로세스와 같은 애플리케이션 기능들이 여기에 속합니다.
인터페이스 어댑터 (Interface Adapters)
인터페이스 어댑터 계층은 내부 계층(엔티티, 유스케이스)과 외부 계층(프레임워크, 드라이버) 사이의 통신을 조정한다
- 정의: 이 계층은 내부 비즈니스 로직과 외부 도구 또는 프레임워크 사이의 번역자 역할을 한다.
- 구성 요소:
- Controllers: 외부 요청을 받아 내부 유스케이스로 변환합니다.
- Presenters: 유스케이스의 출력을 UI에 적합한 형식으로 변환합니다.
- Gateways: 데이터베이스나 외부 시스템과의 통신을 추상화합니다.
- 특성: 의존성 역전 원칙을 적용하여 내부 계층이 외부 계층에 의존하지 않도록 합니다.
- 예시: DTO(Data Transfer Objects), View Models, API Controllers 등이 여기에 해당합니다.
프레임워크 & 드라이버 (Frameworks & Drivers)
가장 외부에 위치한 계층으로, 시스템이 외부 세계와 상호작용하는 도구와 프레임워크를 포함합니다.
- 정의: 데이터베이스, 웹 프레임워크, 장치 드라이버 등 외부 도구와의 통합을 담당하는 계층
- 구성 요소:
- Web: 웹 프레임워크와 관련된 코드
- UI: 사용자 인터페이스 요소
- DB: 데이터베이스 접근 코드
- Devices: 외부 장치 연동 코드
- External Interfaces: 외부 시스템과의 통신 인터페이스
- 특성: 이 계층의 요소들은 쉽게 교체 가능하도록 설계되며, 내부 계층에 영향을 주지 않고 변경될 수 있다.
- 예시: Spring Framework, React, MySQL 드라이버, REST API 클라이언트 등이 여기에 속한다.
흐름 제어 (Flow of Control)
Clean Architecture에서의 제어 흐름은 다음과 같이 작동한다:
- 외부 요청이 Controller에 도착.
- Controller는 이 요청을 Use Case Input Port 형식으로 변환.
- Use Case Interactor는 비즈니스 로직을 실행하고, 필요시 Entities와 상호작용.
- 처리 결과는 Use Case Output Port를 통해 전달
- Presenter는 이 결과를 UI에 적합한 형식으로 변환.
이 흐름은 항상 외부에서 내부로, 그리고 다시 외부로 흐르며, 의존성은 항상 외부 계층에서 내부 계층으로 향한다
의존성 규칙 (Dependency Rule)
Clean Architecture의 핵심 원칙 중 하나는 의존성 규칙입니다:
- 내부 계층은 외부 계층에 대해 알지 못합니다.
- 모든 의존성은 안쪽으로만 향해야 합니다 (외부에서 내부로).
- 이 규칙을 통해 내부 계층은 외부 계층의 변화로부터 보호받을 수 있습니다.
Clean Architecture의 이점
- 모듈성: 각 계층이 독립적으로 개발, 테스트 및 유지보수될 수 있습니다.
- 테스트 용이성: 비즈니스 로직이 외부 의존성으로부터 분리되어 단위 테스트가 쉬워
- 유연성: 프레임워크, 데이터베이스 또는 UI와 같은 외부 요소를 쉽게 교체할 수 있습니다.
- 지속 가능성: 시스템의 핵심 비즈니스 로직이 기술적 변화로부터 보호됩니다.
Clean Architecture는 소프트웨어 시스템을 설계할 때 핵심 비즈니스 로직을 보호하고, 시스템을 더 유지보수하기 쉽고 확장 가능하게 만드는 효과적인 접근 방식입니다. 각 계층의 명확한 책임과 의존성 규칙을 이해하고 적용함으로써, 개발자는 시간이 지나도 변화에 쉽게 적응할 수 있는 견고한 시스템을 구축할 수 있습니다.
nw 프로젝트 기본 구조
src/
└── main/
└── java/
└── com/
└── nawanolja/
├── backend/
│ └── BackendApplication.java
├── core/
│ ├── dto/
│ └── exception/
└── module/
└── auth/
├── application/
│ ├── LoginService.java
│ └── SignupService.java
├── controller/
│ ├── dto/
│ └── AuthController.java
├── domain/
│ ├── vo/
│ └── User.java
└── security/
계층 구조와 의존성 방향
클린 아키텍처의 핵심 원칙은 의존성 방향이다. 의존성은 항상 안쪽(내부, 도메인)을 향해야 한다
- 도메인 레이어(가장 내부): 비즈니스 규칙과 엔티티, 가장 핵심적인 부분
- 애플리케이션 레이어(중간): 비즈니스 로직을 조율하는 서비스
- 인프라스트럭처 레이어(가장 외부): 외부 시스템과의 연동(DB, 웹 등)
domain 패키지와 vo 디렉토리
이미지에서 domain 아래 vo 디렉토리가 있는 것을 볼 수 있다. 이는 도메인 모델의 구성 요소를 분리한 것으로 보인다.
- domain: 핵심 비즈니스 개념을 표현하는 도메인 모델(User 엔티티)
- vo(Value Object): 값 객체로, 불변성을 가진 객체 예를 들어:
- Gender -성별이 적힌 Enum Class
- PassWord- 비밀번호 규칙과 암호화 로직이 포함된 값 객체
값 객체는 도메인 객체의 속성을 표현하면서 자체 검증 로직과 동작을 포함할 수 있어, 도메인 모델을 좀 더 디테일하게 잡아둘 수 있음
application 패키지
application 패키지는 유스케이스 구현체인 서비스 클래스들을 포함합니다. 이는 기존 구조의 service 계층과 유사하지만, 더 명확한 책임을 갖는다.
- LoginService: 로그인 유스케이스 구현
- SignupService: 회원가입 유스케이스 구현
각 서비스는 하나의 유스케이스에만 집중하므로 단일 책임 원칙을 더 잘 지킴
그렇다면 Domain과 Application Service의 차이를 간단히 정리하고 넘어가자.
Domain과 Application Service의 차이
- Application Service는 사용자의 요청에 따라 비즈니스 흐름(유스케이스)을 조합하고 실행하는 계층이다. 주로 하나의 작업 단위, 사용자의 액션을 처리하며, 도메인 객체들을 조합해 동작시킨다.
→ 예시: 배민에서 사용자가 "주문하기 버튼을 눌렀을 때 주문 흐름을 처리하는 로직"이 Application Service에 해당한다.
(결제 요청, 재고 확인, 배달 요청 등 여러 도메인 객체의 행위를 조합) - Domain Model은 시스템이 해결해야 할 핵심 비즈니스 개념과 규칙을 담당한다. 불변성과 일관성을 유지하며, 데이터와 함께 동작(비즈니스 로직)을 포함한다.
→ 예시: 배민의 "최소 주문 금액 제한", "배달 가능 지역 판단", "회원 등급에 따른 혜택 규칙" 등이 도메인에 해당한다.
(비즈니스 규칙이 바뀌면 이 부분만 수정)
쉽게 말해, Application은 "어떻게 동작할지(프로세스 중심)",Domain은 "무엇이 중요한지(비즈니스 규칙 중심)"를 다룬다.
Application이 흐름을 조합하고, Domain이 핵심 로직을 수행하는 구조.
Controller와 dto
controller 패키지는 API 엔드포인트를 정의하고, 그 안에 dto 디렉토리
- controller: HTTP 요청을 처리하고 응답을 반환
- dto(Data Transfer Object): 계층 간 데이터 전송을 위한 객체
DTO가 controller 내부에 있다는 것은 이 DTO들이 오직 해당 컨트롤러의 요청/응답 형식을 위한 것임을 명확히 한다.
이는 DTO의 사용 범위와 목적을 명확히 하는 데 도움이 된다
실제 구현 시 고려사항
패키지 구성 시작:
- 먼저 core와 module 패키지 구성
각 모듈별로 필요한 계층(domain, application, controller 등) 추가 - 도메인 모델 먼저 설계 후 다른 계층으로 확장
의존성 규칙 준수:
- 도메인은 어느 것에도 의존하지 않음
- 애플리케이션(서비스)은 도메인에만 의존
- 컨트롤러는 애플리케이션과 도메인에 의존 가능
- 외부 방향으로의 의존성은 인터페이스를 통해 역전시킴
레이어드 아키텍처와 클린 아키텍처의 차이점 요약
레이어드 아키텍처 | 클린 아키텍처 | |
중심 | 기술 (웹, 서비스, DB) | 도메인과 유스케이스 |
의존성 방향 | 위에서 아래로 (컨트롤러 → 서비스 → 리포지토리) | 바깥에서 안으로 |
코드 분리 방식 | 계층별로 패키지 나눔 | 역할과 책임 기준으로 나눔 |
확장성/테스트 용이성 | 제한적 | 유연하고 테스트 용이 |
🙇♂️ 마무리
새 프로젝트에 합류하면서 시야가 한층 넓어지는 걸 느낀다.
특히 같은 팀의 한 분이 의견을 나눌 때마다 항상 이렇게 말문을 여신다.
“정답은 없습니다.”
“먼저 의도와 목적을 말씀드릴게요.” 그 화법이 인상 깊었다.자연스럽게 더 귀 기울이게 되고, 말씀의 흐름을 따라가게 된다.
이번 프로젝트를 통해 많이 배우게 될 것 같아, 앞으로가 더 기대된다.
요즘은 프로젝트 외에도 CS, 네트워크 등 컴퓨터의 기본 지식도 함께 공부하고 있다. 개발자로 살아감에 있어 이런 ‘기초’가 얼마나 중요한지 점점 더 실감하는 중이다.
예전에는 내가 주로 다루는 상위 계층(응용 계층 중심)의 흐름만 알고 있었다면,지금은 그 아래 계층까지 포함한 전체적인 데이터 흐름과 구조에 대해 관심을 갖게 되었다. 직접 코드를 짜고 서버를 구축하면서,“이 요청이 어떤 경로를 거쳐 서버로 전달되고, 어떻게 응답이 돌아오는가” 를 고민하게 되었고, 그 덕분에 개발을 더 입체적으로 바라보게 되었다.
앞으로도 기본기를 놓치지 않고 꾸준히 공부하며 성장해가고 싶다.
'Project' 카테고리의 다른 글
[Project] 이음새 - 채팅 서비스 구현 (0) | 2024.08.24 |
---|