개요
지금까지 3-tier layered architecture를 사용했다. 프로젝트의 규모가 커질수록 어플리케이션은 복잡해졌고 수정이나 기능의 확장이 매우 힘들었다. 이때부터 객체지향에 대한 본질적인 공부와 고민을 시작했다. 그 결과 가장 중요한 도메인에 집중하지 못하고 있었다는 것을 알았고, 아키텍처를 개선하게 되었다.
이번 기록에선 3-tier layered architecture를 사용하면서 구체적으로 어떤 점이 불편했는지 그리고 이를 해결하기 위해 어떤 고민을 했고, 개선된 아키텍처를 이용해 어떻게 불편한 점을 개선했는지에 중점을 두려고 한다.
3-tier layered architecture
3-tier layered architecture는 처음 서버 개발을 시작할 때 많이 사용하는 아키텍처다.
요청 핸들러인 Controller와 비즈니스 로직을 처리하는 Service, 데이터를 영속화하는 Repository로 각 계층을 구현한다.
쉽고 빠르게 어플리케이션을 개발할 수 있다는 장점이 있지만, 복잡한 어플리케이션을 구현하기엔 너무 단순한 구조다.
단점
낮은 확장성과 유지보수성
먼저 위 구조로 어플리케이션을 개발한다고 해보자. 가장 먼저 어떤 계층을 개발할까?
- 엔드 포인트를 고민하여 Controller를 먼저 개발한다.
- 엔티티와 레포지토리를 먼저 개발한다.
1번의 경우 프레임워크와 API 설계 아키텍처에 의존하게 된다.
2번의 경우 DB 위주의 사고를 하게 된다.
또한 개발 순서와 상관 없이 3-tier architecture에서는 Service에서 핵심 로직을 모두 처리하게 되고, 결국 DB 중심적이고 절차지향적인 어플리케이션이 된다. 이것이 어플리케이션의 확장성과 유지보수를 저해시키는 핵심적인 문제점이다.
낮은 Testablility
위 구조의 어플리케이션을 테스트한다고 해보자.
가장 중요한 로직을 가지는 Service는 복잡한 의존관계를 가지고 있어서 소형 테스트가 힘들다. 테스트한다고 해도 인수 테스트나 슬라이스 테스트와 같은 중형 테스트이므로 시간도 오래 걸리고, 가장 중요한 로직을 테스트하지 못하니 디버깅도 힘들다.
이 또한 위에서 언급한 DB 중심적이고 절차지향적인 설계 때문이다.
개선 방향
그렇다면 이를 어떻게 개선해야 할까?
유연한 어플리케이션을 설계하기 위해선 객체 간의 협력 관계를 고려해야 한다. 이를 통해 각 객체들의 책임을 명확히 하고, 각 책임이 어떤 행동을 하게 할 것인지 고민해야 한다. 즉 도메인에 집중해야 한다.
객체지향의 사실과 오해(2)
이상한 나라의 객체 2장에서는 이상한 나라의 앨리스 이야기를 이용해 객체가 가지는 특징을 쉽게 서술한다. 덕분에 디미터의 법칙을 쉽게 이해할 수 있었다. [OOP] 디미터의 법칙 개요 최근 계속
choi-records.tistory.com
도메인에 집중하기 위해 도메인 계층을 두고, data access 계층은 infrastructure 계층으로 변경해 보자.
위와 같은 아키텍처로 설계하면 위 문제점들을 해결할 수 있다.
개선된 layered architecture
위에서 user interface 즉 표현 계층은 3-tier layered architecture의 표현 계층과 유사하다.
그렇다면 개선된 layered architecture에서 application 영역과 infrastructure 영역은 각각 어떤 역할을 할까?
application
3-tier layered architecture에서 가장 큰 문제점은 Service 객체가 핵심 로직을 처리한다는 것 그리고 이로 인해 도메인(객체) 간 유기적인 협력 관계 구성을 하지 못한다는 것이었다.
개선된 layered architecture에선 service 객체의 역할은 오직 도메인 모델에 로직 수행을 위임하는 것이다.
쉽게 말해 도메인 객체 간 협력의 장을 만들어주는 것이다.
infrastructure
그렇다면 infrastructure 계층은 어떤 역할을 하는 계층일까?
infrastructure 계층의 역할에 대해 이해하기 위해선 DIP에 대해 이해해야 한다.
[OOP] SOLID원칙 정리
개요 Java 진영에서 새로운 지식을 습득할 때 OOP의 특징과 SOLID 원칙이 항상 핵심이 된다. 따라서 이에 대해 확실한 이해가 있어야 본질적으로 깊게 해당 지식을 이해할 수 있다고 생각한다. 이번
choi-records.tistory.com
앞서 언급한 것 중 DIP 적용에 중요한 것은 아래와 같다.
- Service 객체는 도메인 객체 간 협력의 장을 만들어준다.
- 협력 관계를 고려해 객체의 책임을 명확히 하고 어떤 행동을 하게 할 것인지 정해야 한다.
Service 객체가 도메인 객체 간 협력의 장을 만들기 위해선 도메인 객체들에 의존해야 한다.
그리고 각 도메인 객체는 저마다 명확한 책임(역할)을 가진다.
여기서 만약 특정 도메인의 구현이 변경된다면 어떻게 될까?
도메인이 변경되면, Service 객체는 해당 도메인에 의존하고 있으므로 같이 수정해줘야 할 것이다. 이는 도메인을 교체하거나 수정하는 데 많은 비용이 든다는 것을 의미한다. 이런 문제는 Service 객체가 Domain 객체에 강하게 결합되어 있기 때문에 발생한다.
결과적으로 Service 객체와 Domain 객체 간 의존을 느슨하게 해줘야 하고, 여기에 필요한 아이디어가 DIP이다.
Service라는 고수준 모듈이 저수준 모듈인 Domain의 구현체에 의존하는 것이 아닌 Domain의 역할을 추상화함으로써 Service는 추상화된 Domain의 역할에 의존하고, 이 역할을 infrastructure 계층에 구현해 Service와 Domain 간 의존을 느슨하게 해야 한다.
Service 객체에서 교통수단 Domain을 이용한다고 해보자.
위 그림은 Service 객체가 Train이라는 구체적인 교통수단에 대해 강결합되어 있는 상황이다. DIP를 이용해 보자.
Service 객체는 Transportation이라는 추상화된 역할에 의존하고 infrastructure 계층에서 역할을 구현함으로써 도메인 구현을 쉽게 교체할 수 있고, 도메인 구현의 수정이 Service 객체에 영향을 주지 않게 되었다.
따라서 Infrastructre 계층은 구현 기술에 대한 것을 다루는 계층이라고 할 수 있다.
장점
그렇다면 개선된 layered architecture의 장점은 무엇일까?
DIP를 이용함으로써 OCP와 DIP를 만족하게 되었다. 따라서 확장성과 유지 보수성이 높아진다.
또한 테스트 시에 Domain의 fake 객체를 구현해 Service 객체를 쉽게 소형 테스트할 수 있게 되었다.
Service 객체뿐만 아니라 핵심 로직을 가지는 각 Domain 객체에 대해서도 소형 테스트를 함으로써 비교적 수월하게 어플리케이션의 신뢰성을 높일 수 있다.
도메인 객체에 집중하는 아키텍처이므로 도메인 객체 간 협력 관계를 먼저 고려해 어플리케이션을 설계할 수 있다.
마무리
아키텍처에 대해 깊게 공부한 것이 아니기 때문에 부족한 점이 많은 글입니다.
틀린 부분에 대한 지적이나 피드백은 환영입니다.