# Intro
백엔드 개발자로서 한 스텝 더 나아가기 위해서 정말 많은 고민을 했다. 기술스택 하나를 더 공부할지 현재 내가 많이 다루는 기술스택들 중에서 한 가지 기술을 골라 더 깊게 공부를 할지 아니면 프로그래밍적으로 더 공부를 할지..
나한테 어떻게 더 유리하게 적용될지 철저하게 수치로 표현하면서 계산을 했을 때, 기술 하나를 골라 더 깊게 공부를 하는게 더 이득이 많이 남을거라는 결론이 나오기는 했다.
근데.. 프로그래밍적으로 공부를 더 하고 싶었다. 더 정확하게 표현을 하자면 클린 아키텍처, 소프트웨어공학, OOP, FP, AOP 등 이런 내용들에 대해서 공부를 안한지 너무 오래되어서 더 하고 싶었던 것 같다.
그래서 클린 아키텍처, OOP, AOP 등 백엔드 프로그래밍을 공부하면서 MSA와 같이 공부하면 새로운 기술을 공부하면서 프로그래밍에 대해서 더 심도있게 공부를 하는게 아닐까? 라는 합리화 끝에 "MSA"를 하기로 마음 먹었다.
이 글은 도서나 인강, 유튜브를 보면서 정식으로 공부한 것이 아니고, 구글에 흩뿌려져있는 내용들을 덕지덕지 붙인 내용이다. 그렇기 때문에 도서로 공부하기 전에는 어려운 단어나 내용들은 빼고, 구글링해서 찾은 내용들을 기반으로 "이렇게 아키텍처를 구성하면 되겠지?" 라는 생각으로 실제로 구현해본 내용이다. 즉 이론보다는 실제 구현에 더 많은 고민을 했다.
# Use Case
백엔드 개발자나 컴퓨터공학 출신이라면 유스케이스에 대해서 한번쯤은 들어봤을 내용이다. 근데 이거를 프로그래밍에서 어떻게 구현을 해야 되는지 감이 오지 않았을 수 있다. 나또한 그랬다.
구글링을 했을 때 유스케이스에 대해서 많은 내용들이 있었지만 결국 학부 때 배운 유스케이스의 개념들을 포스팅을 했을 뿐이지 실제 구현체로 구현을 한 포스팅은 많이 없었다. Nest.js로는 거의 없었다고 볼 수 있었다.
이론, 당연히 중요하지만 개발자가 이론만 알고 구현을 할 줄 모르면 그 내용을 과연 프로젝트에 녹여낼 수 있을까? 내 경험상 절대적으로 불가능하다.
유스케이스의 '주요 요소'로 Actor, Goal, Scenario, Pre-conditions, Post-condition이 있다. 이 내용들은 다른 블로그들을 보면 금방 찾을 수 있을 것이다.
나는 이런 내용보다는 실제 구현에 포커스를 두고 공부를 했다.
## UseCase Layer
나는 개발자니까 프로그래밍적으로 생각을 해보자.
학부 때 봤던 소프트웨어공학 도서를 봐도 구글링 해서 찾은 내용들을 봐도 유스케이스의 핵심은 '캡슐화'이다.
"각각의 유스케이스는 비즈니스 로직을 캡슐화한다."
이 글을 봤을 때, 아무 생각이 안들었다... 도통 감이 오지 않았다. 하루 정도 고민을 했을 때 " '비즈니스 로직'을 캡슐화한다"는 내용을 보고, 서비스 모듈이 비즈니스를 담당하니까 그럼 서비스 모듈(Nest.js에서는 Provider, Spring Boot에서는 Bean)을 캡슐화한다는 건가? 라는 생각이 들었다. 70% 확신을 갖고 영어 레퍼런스까지 봐보면서 확인을 했었을 때, 얼추 내가 생각한 결론과 맞는 것 같다. 그래서 아래와 같이 User 모듈을 만들어봤다.
src
├── users
│ ├── controllers
│ │ └── users.controller.ts
│ ├── use-cases
│ │ └── create-user.use-case.ts
| | └── read-user.use-case.ts
| | └── update-user.use-case.ts
| | └── delete-user.use-case.ts
│ ├── repositories
│ │ └── users.repository.ts
│ ├── entities
│ │ └── user.entity.ts
│ └── users.module.ts
└── app.module.ts
CRUD를 원래 UserService 파일에서 모두 관리를 하고 있었다면, 지금까지 내가 찾아본 결과로는 위와 같이 나왔다.
위와 같이 구조를 만들자마자 딱 3가지가 바로 떠올랐다.
- 관심사 분리
- 단일 책임의 원칙
- 응집도 향상
User에 관한 서비스는 원래 한 파일로 관리를 하고 있었는데, 저렇게 만들면 누가 봐도 관심사가 분리되어 있다는 것을 알 수 있다. 소프트웨어공학에 좀 약한 나로서 저 3가지가 여기에 붙는게 맞는건지 확실하지는 않지만, 느낌이 딱 위에 3가지였다.
원래는 딱 여기까지만 공부하려고 했다. 근데 '단일 책임의 원칙'을 떠올렸으니.. SOLID 원칙을 봐야겠지..?
# SOLID 원칙
SOLID 원칙은 복습겸 내용을 한번 정리해봤다. -> Interface의 중요성
- 단일 책임의 원칙
- 각 클래스는 한 가지 일만 수행해야 한다 -> 위에 구조를 보면 하나의 클래스는 하나의 역할만 가지도록 구조를 만들어놨다
- 개방-폐쇄의 원칙
- 클래스는 확장에는 열려 있고 수정에는 닫혀 있어야 한다
- 개방 - 서비스 모듈을 만들 때 Interface를 이용해서 추상화한 후에 추상화한 Interface를 토대로 확장을 진행(implements)
- 폐쇄 - 수정을 할 때는 Interface를 수정하지 않고 비즈니스 로직만을 변경해야 한다. 즉 상위 계층의 인터페이스나 클래스의 구조를 수정하지 않고 새로운 기능을 확장해야 한다
- 리스코프 치환 원칙
- 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다
- 레포지토리의 인터페이스를 사용해 상속받은 다른 레포지토리를 사용하더라도 기존 동작이 유지되도록 설계해야 한다 -> 아직 잘 모르겠다... 더 공부해야 됨
- 인터페이스 분리 원칙
- 사용하지 않는 인터페이스에 의존하지 않아야 한다
- 즉 유저의 읽기와 쓰기 기능을 별도의 인터페이스로 나누어 필요한 기능만을 구현하는 것이 좋다
- 의존성 역전 원칙
- 고수준 모듈은 저수준 모듈에 의존하지 않고, 둘 다 추상화에 의존해야 한다.
- 즉 Interface로 구현해둔 추상화를 통해 레포지토리에 접근하고, Nest.js의 DI를 통해 주입받아 구현을 해야 한다
## SOLID 원칙에 의거한 위 예제 구조 리팩토링
src
├── users
│ ├── controllers
│ │ └── users.controller.ts
│ ├── use-cases
│ │ └── create-user.use-case.ts
| | └── read-user.use-case.ts
| | └── update-user.use-case.ts
| | └── delete-user.use-case.ts
│ ├── interfaces
│ │ └── user-repository.interface.ts
│ │ └── create-user.interface.ts
│ │ └── read-user.interface.ts
│ │ └── update-user.interface.ts
│ │ └── delete-user.interface.ts
│ ├── repositories
│ │ └── users.repository.ts
│ ├── entities
│ │ └── user.entity.ts
│ └── users.module.ts
└── app.module.ts
구조 설명
- use-cases -> 단일 책임의 원칙 만족
- interfaces
- Interface를 통해서 추상화 생성 -> 설명하기 위해 풀어서 적어놨지만 그냥 'Interface = 추상화'이다
- 추상화를 통해서 use-cases(비즈니스 로직) 코드 구현 -> 개방 원칙 만족
- 비즈니스 로직을 수정하고 확정하더라도 추상화는 수정되지 않거나 최소화 -> 폐쇄 원칙 만족
- CRUD 별로 interface를 따로 구현함 -> 인터페이스 분리 원칙 만족
- 비즈니스 로직은 추상화를 의존하고 있고 AppModule에 DI 주입 -> 의존성 역전 원칙 만족
Use-case와 SOLID 원칙에 의거하여 폴더 구조를 구성해보니까... 너무 복잡해진거 같아서 내가 공부한게 제대로 한건지 의구심이 들었다.
그래서 클린 아키텍처에 대해서 찍먹을 해봤다.
# Clean Architecture
클린 아키텍처에서의 폴더 구조는 각 레이어의 역할과 의존성을 명확히 구분하여 코드의 가독성, 유지보수성, 테스트 용이성을 높이기 위한 설계 패턴이다.
클린 아키텍처에는 domain, use-cases, infrastructures 라는 3개의 폴더 구조가 있다.
- domain은 핵심 비즈니스 로직 규칙 및 엔티티가 포함되어 있다.
- use-cases는 domain을 기반으로 특정 유스케이스를 구현한다
- 중요한 점은 오로지 domain에만 의존한다는 것이다.
- 그럼 위에 interface로 묶어둔 내용이 곧 domain에 해당이 된다는 것을 유추할 수 있다.
- 비즈니스 로직의 실행 흐름을 책임진다.
- infrastructures는 데이터베이스, 외부 API 호출, 기타 라이브러리와 같은 기술적 구현 세부사항이 포함된다
- domain과 use-cases에서 기술적 세부 사항을 분리하고 의존성을 역전시키기 위한 레이어이다.
## Use-case + SOLID 구조에 클린 아키텍처 구조를 결합한 폴더 구조
src
└── users
├── domain
│ ├── entities
│ │ └── user.entity.ts
│ ├── ports
│ │ ├── user-repository.interface.ts
│ │ └── message-broker.interface.ts
│ ├── events
│ │ └── user-created.event.ts
├── use-cases
│ ├── create-user.use-case.ts
│ ├── read-user.use-case.ts
│ ├── update-user.use-case.ts
│ └── delete-user.use-case.ts
├── infrastructures
│ ├── repositories
│ │ └── mysql-user.repository.ts
│ ├── messaging
│ │ └── kafka-producer.service.ts
├── controllers
│ └── users.controller.ts
└── users.module.ts
└── app.module.ts
이렇게 폴더 구조를 수정할 수 있을 것이다. 많이 달라진 점이 있다면 interfaces 폴더이다. 분명히 SOLID 원칙 중에서 인터페이스 분리 원칙에 의하면, 인터페이스는 개별적으로 나눠야 한다고 했는데 클린 아키텍처를 구성하면서 하나로 통합해버렸다.
클린 아키텍처에서는 보통 인터페이스를 개별로 추상화하지 않는다고 한다. 그 이유로는 클린 아키텍처는 단순화된 인터페이스 설계와 유연성을 유지하기 위함이라고 한다. 실제로 내가 인터페이스를 남발했을 때가 있었는데.. 많이 복잡해졌던 경험이 있었다. 근데 처음에 조금 복잡할 뿐이지 금방 익숙해지긴 한다. 아무튼 이러한 이유로 클린 아키텍처는 하나의 통합 인터페이스로 단순한 인터페이스 설계를 유지하는 것이 일반적인 이론이다(회사by회사, 팀by팀).
infrastructures 레이어에는 우리가 흔히 사용하던 DAO를 구성해주거나 외부 시스템과 상호작용을 해야할 때 위치하는 것이 적절하다
## domain/ports의 의미와 장점
- 명확한 역할: ports라는 용어는 이 레이어가 외부와의 상호작용을 위한 추상화된 입출력 지점이라는 의미를 잘 전달한다
- 확장성: ports 폴더에 데이터베이스 외에도 메시지 브로커, 외부 API, 파일 시스템 등 외부 시스템과 상호작용하는 인터페이스를 추가하는 것이 자연스러워진다
- 도메인 독립성: ports는 외부 시스템의 구체적인 구현을 숨기고 도메인 레이어가 이를 추상화된 형태로만 접근할 수 있게 해준다
# 마무리
코드로 구현한 것은 없지만, 이렇게 적어놓으면 대부분의 개발자들은 다 이해할 수 있을 것이라 생각이 된다.
오랜만에 공부한 내용도 있고, 처음 공부한 내용들도 있기 때문에 분명히 잘못 이해한 내용이 있을거 같은데, 만약 잘못 이해한게 있다면 댓글로 알려주시면 감사하겠습니다~~~
'Backend > Backend Engineering' 카테고리의 다른 글
| Nest.js를 활용하여 Login, AuthGuard 구현하기 (0) | 2024.10.26 |
|---|---|
| 요청이 백엔드로 전달되는 과정 (0) | 2024.08.31 |