Layered architecture faults and improvement

오늘은 계층형 아키텍처의 문제점에 대해 알아보겠다.

 
계층형 아키텍처는 코드에 나쁜 습관들이 스며들기 쉽고..
시간이 지날수록 소프트웨어를 점점 더 변경하기 어렵게 만드는 수많은 허점들이 생긴다.
 

 
위는 전통적인 웹 애플리케이션의 구조이다.
의존성의 마지막은 영속성 계층이므로 애플리케이션은 데이터베이스에 의존하게 된다.
따라서, 어떤 요구사항을 구현할 때 데이터베이스의 구조를 먼저 생각하고
이를 토대로 도메인 로직을 구현하기 쉽다.
즉, 데이터베이스 주도 설계로 유도 되는 것이다.
 
하지만, 비즈니스 관점에서는 도메인 로직을 먼저 만들어야한다.
도메인 로직이 맞다는 것을 확인한 후에 이를 기반으로 영속성 계층과 웹 계층을 만들어야한다.
 
또한, ORM 프레임워크를 계층형 아키텍처와 결합하면
비즈니스 규칙을 영속성 관점과 섞고 싶은 유혹을 쉽게 받는다.
 

ORM 에 관리되는 Entity 들은 일반적으로 영속성 계층에 위치 시킨다.
Layered Architecture 에서는 아래 방향으로 의존성을 가질 수 있으므로
Domain 계층에서는 해당 Entity 에 의존성을 가질 수 있다.
 
 

이렇게 되면 뭐가 문제일까..

1. 영속성 계층과 도메인 계층 사이에 강한 결합이 쉽게 생긴다.

Service 는 Entity 를 비즈니스 모델처럼 사용할 수 있고.. (도메인 로직이 Entity 에 존재하기 쉬움)
Entity 는 영속성 계층의 모델이므로 영속성 계층의 책임도 존재한다.
즉, Entity 관점에서 변경 이유가 하나여야 한다는 SOLID 의 SRP 원칙 위배이다.
 
<참고>
영속성 계층의 책임은..
즉시 로딩을 할 것인지.. 지연 로딩을 할 것인지 등의 ORM 자체의 의존성도 있지만..
DB 연동기술의 의존성인 ORM 을 쓸 것인지.. SQL Mapper 를 쓸 것인지도 해당 될 수 있다..
 
또한, Service 가 Entity 를 의존하고 있기 때문에
Entity 가 변경되면 Service 코드도 변경되어야 할 확률이 높다.
의존한다의 표면적인 의미는 단순히  클래스 대 클래스로 정적인 의미도 있지만,
의존당하는 클래스의 코드가 변경되면 의존하는 클래스의 코드도 변경된다는 의미가 있는 것이다.
따라서, 영속성 계층의 코드가 변경되면 쭉 타고 올라가서 웹 계층까지 변경될 수 있다.
Layered Architecture 에서 하위 계층이 변경되면 잠재적으로
상위 계층도 변경될 가능성이 생긴다는 것이다.
 

2. 지름길을 택하기 쉬워진다.

Layered Architecture 에서 유일한 오피셜 규칙은
어떤 계층에서 같은 계층에 있는 컴포넌트나 아래에 있는 계층에만 접근 가능하다는 것이다.
그래서 계층형 아키텍처를 오랫동안 유지보수 하다보면 생기기 쉬운 상황중 하나가..
상위 계층에 위치한 컴포넌트에 접근해야 한다면 그냥 해당 컴포넌트를 계층 아래로 내려버리기 쉽다는 것이다.
 

시간이 지날 수록 위와 같은 형태가 될 것이다.
처음에는 하나만 내리다가 나중에는 점점 더 영속성 계층이 비대해지고
모든 것을 다하는 전지전능한 컴포넌트가 만들어질 가능성도 있다.
주로 어떤 계층에도 속하지 않을 것 처럼 보이는 헬퍼, 유틸리티 컴포넌트가 대상이다.
물론 핵심 도메인 로직도 아래로 내려버릴 수 있어서 예외는 아니다.
 

3. 테스트하기 어려워진다.

Layered Architecture 를 사용하면 계층을 건너뛰고 싶은 유혹이 생긴다.
 

위와 같이 Controller 에서 다이렉트로 영속성 계층을 의존하게 될 수 있는데..
이는 Web 계층에서 Domain 계층의 책임을 더하여 가지고 있는 것이다. (SRP 위배)
(단 하나의 필드를 조작하여도 이는 도메인 로직인데 웹 계층에서 구현한 것이다.)
처음에는 별 문제가 아닐 수도 있지만, 점점 도메인 로직이 Web 계층에 존재하게 되고
핵심 도메인 로직이 애플리케이션 전반에 걸쳐 퍼져나간다. (응집력 하락)
 
또한, Web 계층 테스트를 할 때, 영속성 계층도 Mocking 해야한다는 것을 의미한다.
테스트 코드 작성이 점점 복잡해질 것이다.
 

4. 넓은 서비스가 만들어질 수 도 있다.

앞서 핵심 도메인 로직이 웹 계층과 영속성 계층으로 퍼져나갈 수 있다고 했지만,
Layered Architecture 는 도메인의 너비에 대한 규칙도 없다.

따라서, 위와 같이 여러 유즈케이스를 담당하는 넓은 서비스가 만들어지기도 한다.
수많은 컨트롤러가 하나의 서비스를 의존하고..
그 하나의 서비스가 다양한 리포지토리를 의존하는 상황이다.
이 또한 SRP 위배이다.
 

5. 동시 작업이 어렵다.

예를 들어..
어떤 하나의 유즈케이스를 개발하는데 3명의 개발자가
각각 Web, Domain, Persistence 계층을 담당한다고 하자..
동시에 개발을 시작할 수 있을까..?
1 ~ 4번까지의 문제 상황들이 존재한다면..
각 계층의 책임들이 뒤섞여있어서 개별적으로 작업을 진행할 수 없을 것이다.
 
어느 계층에서 어떤 책임을 가져야하는 것을 생각하는 것부터 이미 머리가 아프다
책임을 정확하게 나누고 각 계층의 인터페이스를 먼저 만들고 시작한다 하더라도..
데이터베이스 주도 설계는 영속성 로직과 도메인 로직이 뒤섞여 있을 확률이 높다. 
 
<참고>
외부 데이터베이스에 접근하는 데이터베이스 주도 설계가 아니고
내부 메모리 수준의 저장소를 사용한다면
영속성(?) 계층이 기술적 의존성이 없는 수준이기 때문에
Layered Architecture 도 나쁘지 않다고 생각한다.
 
또한, 여러 유즈케이스를 동시에 개발한다면..
4번의 경우 하나의 서비스에 동시에 코드 작업이 들어갈 확률이 존재하여
code conflict 까지 생길 수 있다.
 

결론.

Layered Architecture 는 오피셜 규칙이 빈약하여
시간이 지날수록 코드가 잘못된 방향으로 갈 가능성이 매우 높아진다.
물론, 몇가지 추가 규칙들을 정해서 완벽하게 따르고 잘못된 방향으로 가지 않는다면
유지보수 하기 좋은 코드될 수 있다.
 
 

개선

위에서 발생했던 문제들을 개선해보도록하자.
 

위 그림에서 문제는 Service 가 ORM Entity 를 의존하고 있는게 문제였다.
ORM Entity 를 도메인 모델로 사용하여
도메인 로직이 영속성 계층에 녹아들 확률이 높아짐
 

그래서 위와 같이 한번 바꿔보았다.
여전히 문제가 많다.
Entity 는 여전히 Repository 가 의존하는 대상이며, ORM Entity 일 것이다. (책임이 섞여있음, SRP 위배)
또한, Layered Architecture 의 계층간 의존성의 규칙을 위배(아래에서 위로 의존) 함과 동시에
순환의존성이 생겨버렸다.
 
 

그러면 위와 같이 바꾸면 어떨까..
Entity 가 두개로 나뉘어서 SRP 는 만족한 것 같다..
하지만 여전히 문제가 있다.
Service 가 ORM Entity 를 의존 하고 있기 때문에
ORM Entity 가 바뀌면 Service 코드도 변경되어야 한다.
 
이를 해결하기 위해서는 SOLID 의 의존성 역전 원칙(DIP)이 사용된다.
 

의존성 역전 원칙(DIP)은
어떤 클래스가 추상화(역할)에 의존해야하고 구현체(구현)에 의존하지 말라는 원칙이다.
이를 기술적 관점에서 보면..
위 다이어그램에서 본 것 처럼 코드 상의 어떤 의존성이든 그 방향을 바꿀 수 있다는 뜻이다.
 
Service 가 같은 계층에 존재하는 Repository 인터페이스(고수준 모듈)에 의존하고
Repository 구현체(저수준 모듈)가 인터페이스를 의존(구현)하였다.
의존성이 역전(DIP) 되면서 Repository 가 두개의 Entity 를 의존하고 있는 꼴이 되었다.
동시에 Service 는 더이상 영속성 계층으로의 의존성이 사라지게 되었다.
그래서 영속성 계층의 코드가 아무리 바뀌어도 도메인 계층의 코드는 변경할 필요가 없다. (OCP 원칙 만족)
 
<참고>
노파심에.. 이름이 Repository Interface 라 헷갈릴 수 있는데 현재 다이어그램에서 Repository Interface 는
Spring Data JPA 의 JpaRepository 를 상속한 인터페이스가 아니다.
Hexagonal Architecture 의 Port(out) 라 생각해야한다.
 
 
<참고>
사실 위의 개념, 기술만으로는 코드상에서 DIP, OCP 를 지킬 수 없다.
다형성 만으로 해결해야하기 때문이다. (new Repository Implementation 을 해줘야함)
그래서 의존성 주입이라는 개념이 나왔고..
의존성 주입 기술을 포함하여 OCP / DIP 원칙을 정확하게 지키도록 도와주는
아주 유명한 프레임워크가 바로 Spring Framework 이며
Spring Container (DI Container, IOC Container)가 바로 그것이다.
 
 

더 나아가면..

이제 개선 단계에서 최종 형태는 영속성 계층이 도메인 계층을 의존하는 형태로..
더 이상 Layered Architecture 가 아니다.
여기서 출발하는게 바로 Clean Architecture 이다.
 

'대규모 시스템 설계' 카테고리의 다른 글

시스템 설계 고민 2  (0) 2023.06.09
시스템 설계 고민 1  (0) 2023.06.08
Event-Driven 아키텍처 와 Pub/Sub 모델  (0) 2023.06.03
Hexagonal Architecture 정리  (0) 2023.05.15
CQRS Pattern  (0) 2023.02.15