
서버구축 - 소프트웨어 설계
이번 주차는 백엔드 구조 설계 중 다양한 아키텍처에 대하여 학습하고, 이중 프로젝트에 적절한 방법을 적용하고, 일관된 레이어의 흐름을 바탕으로 실제로 기능을 TDD를 활용하여 구현하는 것을 목표로 하였습니다.
클린 아키텍처
- 핵심 개념: 의존성 규칙(Dependency rule)에 따라 안쪽 도메인 계층이 바깥 인프라 계층에 의존하지 않음
- 계층 구성
- Entities (도메인)
- Use Cases
- Interface Adapters(컨트롤러, 프레젠터)
- Frameworks, Drivers(DB, UI)
- 의존성의 흐름: 안쪽 -> 바깥쪽 흐름 금지
- 장점
- 도메인 독립성 유지
- 테스트 용이
- 변화에 유연함
- 단점
레이어드 아키텍처
- 핵심 개념: 계층 간 수직 의존성을 전제로한 구조(Controller -> Service -> Repository)
- 계층 구성
- Presentation Layer
- Application Layer
- Domain Layer
- Infrastructure Layer
- 의존성 흐름: 상위 계층이 하위 계층에 의존하는 형태(OOP의 일반적 DI 활용)
- 장점
- 학습 및 설계가 비교적 쉬움
- 단순 서비스에 적합
- 단점
- 도메인 로직 침투 위험
- 테스트 및 확장 어려움
SOLID 원칙
SOLID는 객체지향 설계의 5가지 원칙으로, 클린 아키텍처의 핵심 개념과 직결됩니다.
-
SRP (단일 책임 원칙)
- 하나의 클래스는 하나의 책임만 가져야 한다.
- 도메인, 유스케이스, 컨트롤러 등을 분리하여 책임을 명확히 함.
-
OCP (개방-폐쇄 원칙)
- 확장에는 열려있고 변경에는 닫혀있어야 한다.
- 인터페이스 기반 설계, 전략 패턴, DI 등을 통해 구현.
-
LSP (리스코프 치환 원칙)
- 하위 클래스는 상위 타입을 대체할 수 있어야 한다.
- 타입 안정성과 인터페이스 일관성을 유지.
-
ISP (인터페이스 분리 원칙)
- 클라이언트는 사용하지 않는 인터페이스에 의존하면 안 된다.
- 인터페이스는 작고 명확해야 함.
-
DIP (의존 역전 원칙)
- 상위 모듈이 하위 모듈에 의존하지 않도록, 추상화에 의존해야 함.
- 클린 아키텍처의 핵심 원칙이며, 인프라가 도메인을 참조하지 않고 인터페이스를 통해 연결됨.
도메인 모델링
비즈니스 규칙과 개념을 코드로 표현하는 작업이며, 가장 안쪽의 계층에서, 외부 변화에 영향을 받지 않아야 합니다.
-
도메인 모델링 구성 요소
- Entity: ID 등으로 구분되는 식별 가능한 객체, 상태를 갖고 변경이 가능함
- Value Object: 고유 식별자 없이 값으로만 비교되는 객체, 불변적으로 다루는 것이 일반적
- Aggregate: 엔티티와 값 객체를 하나의 논리적 단위로 묶은 것.
- Domain Service: 엔티티 만으로 선언하기 어려운, 복잡한 도메인 로직(비즈니스), 외부 연동
- Domain Event: 도메인에 발생한 사건, 도메인에 다른 애그리거트와 느슨하게 결합되어 연계
-
Rich Domain Model 지향
- 도메인을 단순 데이터만 담긴 저장소 처럼 사용하는 것이 아니라, 핵심 로직을 직접 처리하도록 하는 형태를 지향
- 로직을 모두 Service에 뭉쳐 놓는 것이 아닌, 객체가 스스로 로직을 처리
적용된 아키텍처
전체 계층 구조
Controller → (Facade) → Service → Domain/Repository
- 일반 기능:
Controller → Service → Repository
- 복합 로직(
User, Order): Controller → Facade → 각 Service → 각 Repository
1. controller
클라이언트로부터의 HTTP 요청을 받아 처리하는 계층입니다.
요청 검증 및 DTO 변환, 응답 생성의 책임을 가집니다.
*Controller.java: HTTP 요청 핸들러 클래스
*Api.java: API 명세용 인터페이스
*Dto.java: 요청(Request)/응답(Response) DTO 정의
하위 디렉토리: coupon, order, product, user
2. facade
여러 Service 간의 흐름을 조율하는 계층입니다.
복잡한 트랜잭션 처리나 도메인 간 협업이 필요한 경우 이 계층을 통해 로직을 단순화합니다.
UserFacade: UserService, BalanceService 조합
CouponFacade: UserService, CouponService 조합
OrderFacade: UserService, OrderService, BalanceService, CouponService, ProductService 조합
3. application
비즈니스 로직을 담당하는 계층입니다.
단일 책임 원칙에 따라 기능별 Service로 나누어져 있으며,
일부 복잡한 도메인(User, Order)은 별도의 Facade를 통해 여러 Service를 조합합니다.
*Service.java: 각 도메인 단위의 비즈니스 처리 로직
user, order, coupon: 복합 흐름을 다루기 위해 facade 계층 사용
하위 디렉토리: balance, coupon, order, product, user
4. domain
실제 데이터베이스와 연결되는 계층입니다.
JPA 기반의 Entity, Repository로 구성되어 있으며 각 도메인은 하위 디렉토리로 구분됩니다.
Entity: @Entity로 선언된 JPA 객체
Repository: JpaRepository 상속 인터페이스
하위 도메인: balance, coupon, order, product, user
5. dto/common
공통적으로 사용하는 DTO가 위치합니다. 주로 에러 응답 등의 형식을 정의합니다.
CustomErrorResponse.java: 표준 에러 응답 포맷
6. exception
예외 처리를 담당하는 계층입니다. 사용자 정의 예외, 글로벌 예외 핸들러 등을 포함합니다.
7. infrastructure/external - 설계 기획, 미구현
카카오, Slack 알림과 같이 외부 연동을 위한 Service가 위치합니다.
설계 의도 요약
- 레이어드별/도메인별 패키지 구성: 각 도메인이 담당하는 책임이 명확하게 분리
- 단순 도메인과 복합 도메인의 처리 방식 분리: 유지보수성과 테스트 용이성, 기능 확장을 고려하여
Facade 도입
- 계층 간 의존성 최소화:
Controller는 Service 또는 Facade에만 의존하며, 내부 도메인에 대한 직접 접근은 하지 않음
피드백 분석
-
의존성이 역전되는 구조가 존재함
- Controller에 선언되어있는 Response, Request DTO를 그대로 다른 레이어에서도 사용하는 부분이 존재했습니다.
- 레이어의 설계를 할 때, 레이어 마다 같은 속성값을 사용하니 가져다 쓰게된 단순한 이유였는데, 그렇게 되면, Controller 계층의 파일을 Service나 Repository에서 보게 되는 부분이 잘못된 구조였습니다.
- DTO를 추가한다면, 레이어별로 별도로 지정을 해야했다는 것을 학습하게 되었습니다.
-
TDD를 지키지 못함
- 각 레이어, 기능 마다 단위 테스트가 존재하지 않는 상태로 제출을 하게되었습니다.
- 항해를 신청했던 이유가 단순한 기능 구현만 배우려는 것이 목적이 아니었는데, 다시 기능 구현에만 급급한 상태로 과제를 진행해버렸습니다.
- TDD 개발 방식에 맞게, 추가하려는 기능에 대한 단위 테스트를 먼저 작성하고, 그 다음 기능 구현을 진행했어야 했습니다.
- 초반 Product 관련 기능은 테스트를 먼저 진행했는데, 과제 2일 정도를 Controller 테스트 세팅하는 것에서 막혀있다보니, 막바지엔 테스트를 놔버린게 실수였습니다.
- 다시 한번, 제가 이 과정을 신청했던 이유에 대하여 생각하는 계기가 되었습니다.
마치며
이번 과제에서 처음으로 fail을 받게 되어 아쉬운 점이 있었지만,
한편으로는 프론트엔드를 블랙뱃지를 받았으니, 백엔드도 무조건 블랙뱃지를 받아야한다는 압박감에서 벗어나,
제 백엔드 구현 수준은 프론트엔드 만큼은 아니다라는 현재 상태를 상기하는 계기가 되었습니다.
다음 과제도 통과를 목적으로 하겠지만, 과제의 요구 사항에 대하여 집중하는 방식으로 접근하도록 하겠습니다.