해당 내용은 우아한형제들 기술 블로그를 참고하여, 원글인 Robert C.Martin의 블로그의 내용을 추가하여 작성하였습니다.
의존성 규칙
아키텍처는 다양한 구조가 존재하고 그 구조마다 세부사항은 다르지만, 관심의 분리라는 동일한 목표를 가진다.
그리고 해당 아키텍쳐들은 소프트웨어 계층을 나누어 이러한 분리를 달성한다.
이 조건을 달성하기 위해선 Robert C.Martin은 다음과 같은 5가지를 준수해야 한다고 얘기합니다.
- 프레임워크와 독립적이어야한다. 아키텍처는 일부 기능이 포함된 소프트웨어 라이브러리의 존재에 의존하지 않는다.
이러한 점은 프레임워크를 도구로서 사용할 수 있게 만들어주며, 종속적이지 않게 만들어준다. - 외부요소(UI,DB,Server)와는 별개로 테스트환경이 구성되어있어야한다.
- UI와 독립적으로 비즈니스 규칙을 변경하지 않더라도 웹 UI를 콘솔 UI로 바꿀 수 있다.
- DB와 독립적으로 운영되어야하며, DB가 바뀐다고해서 시스템의 결함이 발생해서는 안된다
- 외부 기관과 독립적이다.
여기서 공통적으로 강조되는 것은 바로 의존성의 탈피이다. 아키텍처는 변화에 유연해야하며, 종속적이어서는 안된다는 것이다.
또한, 아키텍처에 대한 공통적인 규칙이 존재하는데 모든 소스코드 의존성은 반드시 외부에서 내부로, 고수준 정책을 향해야한다.
즉, 위의 규칙들을 종합하자면 다음과 같이 정의 할 수 있다.
비즈니스 로직을 담당하는 코드들은 DB 또는 Web 같이 구체적인 세부사항에 의존하지 않아야한다.
이러한 점을 통해
비즈니스 로직(고수준 정책)
은세부사항(저수준 정책)
의 변경에 영향을 받지 않도록 할 수 있다.
이 의존성 규칙을 지키기 위해 Robert C.Martin의 블로그에서는 "What data crosses the boundaries", "Crossing boundaries"와 같은 상황 개념을 얘기한다.
이를 실제 프로젝트에 적용하려면 무엇을 주의해야하는지 알아보자.
💡 고수준, 저수준의 개념
- 고수준 : 상위 수준의 개념, 추상화된 개념
- 예시 : 데이터를 저장하거나, 실제저장 로직(비즈니스 로직)이 아닌 개념적으로만 나타냄
- 저수준 : 추상화된 개념을, 실제 어떻게 구현할지에 대한 세부적인 개념구현
- 예시 : RDB에 데이터를 저장하는 로직, 실제 비즈니스 로직
The Dependency Rule
동심원은 소프트웨어의 다양한 영역을 나타낸다. 일반적으로 더 깊이 들어갈수록 소프트웨어의 수준이 높아집니다. 외부 원은 메커니즘입니다. 내부 원은 정책입니다.(원문 직역)
위의 말의뜻은 앞서말한 고수준과 저수준 개념 중 고수준의 아키텍처를 지향할수록 소프트웨어의 수준을 높일 수 있다는 것이다.
그리고 이를, 잘 작동하도록 만드는 규칙은 The Dependency Rule
이다.
해당 규칙의 의의를 한줄로 정리하자면 "소스 코드 종속성은 안쪽만 가르킬 수 있다" 로 정의된다.
이 말은 즉, 고수준의 함수 또는 클래스,변수,엔티티(이하, 오브젝트라고 칭함)는 저수준의 오브젝트에 대해서 인식(언급)하고 있어도 되지만, 그 반대의 경우가 발생해서는 안된다.
이는 OOP중 LSP(리스코프-치환-원칙) 과 유사한 점을 가진다.
Entity
해당 글에서 정의하는 Entity
는 우리가 흔히 JPA에서 다루는 Entity와 비슷한 개념이지만, 다른 개념으로 다루어진다.
비즈니스 로직에 연관된 개체로서 가장 일반적이고 높은 수준의 캡슐화를 보장하는 메소드 또는 변수의 집합으로 취급된다.
또한, 이는 운영되며 로직에 의해 영향을 받아 변경이 일어나는 것을 지향해야한다.
실제 우리가 자주사용하는 개념인 JPA
의 Entity에서도 데이터의 무결성을 보장하기 위해 Lombok
에서 제공하는 @Data
의 사용을 지양한다던가(물론 여러 어노테이션을 상속구현한 어노테이션이기에 쓸데 없이 무거워지는 것을 막기 위함이 더크다.)
Setter
를 사용하지 않는 등 개념을 사용하기에 비즈니스로직과 연관된 객체로서 직접적인 변경을 최소화 시켜야한다.
Use Cases
UseCase 계층의 경우 애플리케이션의 핵심 비즈니스 로직을 구현하는 계층이다.보통 Business Layer라고 부른다.
그렇기에 시스템의 모든 로직은 DB 또는 UI,Framework와 같은 외부요소에 의해 영향을 받지 않게 설계되어야한다.
Framework and Drivers
앞서말한 동심원의 바깥족 레이어, 즉 저수준에 속해야하는 것은 DB 또는 웹 프레임워크와 같은 요소로 구성되며,
그 이유는 실제 비즈니스 로직과는 거리가 있는 요소 거리가 먼 것들을 배치한다.
🤔 엥? DB는 밀접하잖아요?
그렇다. DB는 실제 비즈니스 로직에서 뗄래야 뗄수 없는 요소이지만, 대부분 DB를 외부에서 사용하기에 저수준에 포함되어도 된다고 Robert C.Martin은 말한다.
What data cross the boundaries
해당 토픽의 주된 주제는 경계(Layer)간의 데이터를 전달할 때 무엇을 전달해야하는가이다.
기본적으로 Layer는 크게 Controller
,Service
,Repo
로 분리된다.
필자의 경우 보통 Controller에서 DTO(Data Transfer Object)
를 Request 받아 이를 Servie Layer에서 가공해 ResponseEntity
를 Response하는 구조를 선호한다.
우아한형제들의 기술블로그에서도 아래와 같이 얘기한다.
의존성 규칙을 지키기 위해서는 우리가 사용하는 단순하고, 고립된 형태의 데이터 구조를 사용하는 것을 추천합니다.
만약 DB의 형식의 데이터 구조 또는 Framework에 종속적인 데이터 구조가 사용되게 된다면, 이러한 저수준의 데이터 형식을 고수준에서 알아야 하기 때문에 의존성 규칙을 위반하게 됩니다.
나도 이를 알기에 DTO와 같은 개념을 사용하였지만 이게 클린 아키텍처를 방해할 수도 있다는 생각이 들었다. 나와 같은 생각을 한 우아한형제들도 이에 관한 포스팅을 하였는데 그내용은 아래와같다.
1. DTO간의 중복 코드
제목 그대로의 경우가 발생한다.
기본적으로 CRUD API를 생성할 때 서로다른 Domain 혹은 다른 성질(Get,Post,Put,Delete)의 API를 생성하더라도 Request 또는 Response 되는 DTO가 같은 경우를 개발을 하다보며, 종종 마주칠 수 있다.
이때마다, 나 또한 들었던 생각이 "과연, Entity의 보호와 의존성을 탈피하기 위해 DTO를 사용하더라도 무의미하게 Class의 수가 증가하게 된다면 그것은 과연 클린 아키텍처인가?" 라는 생각이 들었다.
예시
public class CreateRequest {
private String name;
private LocalDate startDate;
private LocalDate endDate;
...
}
public class UpdateRequest {
private String name;
private LocalDate startDate;
private LocalDate endDate;
...
}
이에 관해서 <클린 아키텍처> 도서에서 좋은 내용을 언급해준다.
해당 내용은 중복에도 종류가 존재한다는 것이다.
- 진짜 중복
- 한 인스턴스가 변경되면, 동일 변경내용을 모든 복사본에 반드시 적용해야하는 경우
- 우발적 중복(가짜 중복)
- 중복으로 보이는 두 코드의 영역이 각자 경로로 발전(= 즉, 따로 사용되는 경우)한다면, 이 두코드는 진짜 중복이아니다.
포스팅에서는 이를 대부분 사용되는 저장 / 수정 DTO의 중복은 우발적 중복에 속한다고 판단한다라고 적혀있다.
이는 대부분 초기값이 같을 뿐 규모가 커지며, 다른 이유로 변경될 가능성이 크기 때문이다.
그렇기에 앞서말한 진짜 중복
이 아닐 경우에는 DTO를 분리하는 것이 좋은 구조라고 판단된다.
2. Entity가 DTO를 직접 참조하는 문제
Entity를 생성할 때, Controller로 부터 생성된 Request DTO를 이용하는 경우가 존재한다.
보통의 경우 DTO를 통해 다른 경계로 전달하기 때문에 의존성 규칙을 준수했다라고 생각할 수 있다. 나 또한 그렇게 생각해왔었다.
하지만, 앞서말한 고수준 저수준의 개념을 다시 한번 상기했을 때, Entity가 직접 DTO를 참조하게 된다면 DTO의 변경에 따라 Entity가 변경되거나, Error를 일으키는 직접적인 원인이 될 수도 있다.
또한 Entity가 DTO를 직접 참조하는 것은 SRP(단일-책임-원칙)을 위반할 수도 있다.
Entity의 경우 DB 또는 비즈니스 로직과 관련된 작업을 수행하는데, DTO의 변경이 Entity의 변경을 초래할 수도 있기 때문이다.
이는 결국 Entity 클래스의 역할을 확장하거나 수정해야 한다는 것을 의미하므로 SRP 원칙에 위배된다.
따라서, 이러한 문제를 예방하기 위해선 Entity를 생성하는 시점을 UseCase 계층에서 생성한다면 DTO에 의해 Entity의 변경을 막을 수 있다.
🤔 혹시, Service Layer에서 DTO로 변환하여 Controller Layer로 반환하는 것은요?
이는 Entity가 DTO를 직접 참조하는 것과는 다르다. 해당 경우 비즈니스 로직 수행 후 DTO로 변환만하여 Response 하기에 해당 문제와는 관련이 없다.
또한 해당방법을 사용할 경우 역할의 분리로 인한 코드의 유지보수성과 확장성을 향상시킬 수 있다는 장점이 존재한다.
Crossing boundaries
두 번째로는 Crossing boundaries
이다.
제어의 흐름은 원의 내부에서 외부로 향할 수 있는데 소스코드의 의존성은 이렇게 될 경우 의존성 규칙을 위배하게 되므로, 의존성 역전의 원칙을 이용하여 이를 해결해야 한다는 이야기이다.
즉, 고수준의 정책이 저수준의 정책에 의존해서는 안된다는 말이다.
예시
일반적으로 조회요청이 발생하면 이를 처리하기 위해 제어의 흐름자체는 보통아래와 같이 처리된다.
해당 제어 흐름과 같이 소스코드 의존성을 가지게된다면 Service
가 구체적인 RDBMS를 구현한 구현체(저수준 정책)을 직접 참조하게되어 의존성 규칙에 위반이 발생한다.
이를 해결하기 위해선 DIP(의존성 역전의 원칙)을 적용하면 간단히 해결가능하다.
추상화된 Repository 인터페이스를 두어서 Service가 이를 참조하고, 구체적인 RDBRepository가 이러한 인터페이스를 구현하게 된다면 소스코드의 의존성을 역전시켜, 의존성 규칙에 위반이 발생하지 않는다.
DIP를 통해, Service는 DB의 변경으로 부터 자유로워지고, DB의 구체적인 세부사항을 알 필요도 사라진다.
Interface 선언 시 주의점
DIP를 적용해서 인터페이스를 분리하면 구체적인 세부사항을 분리시킬 수 있는 장점을 얻었지만 유의해야할 점으로 인터페이스를 분리시켜야한다는 것이 있다.
인터페이스를 작성할 때는 클라이언트가 필요로 하는 메서드를 기반으로 분리되어야한다.
그렇지 않고 구현체 클래스를 직접적으로 참조할 경우, 클라이언트가 필요로하지 않는 메서드까지 노출하게된다.
현재 나 또한 프로젝트에서 해당 구조를 사용하고 있고, 이전에는 직접 구현체에 직접 참조하는 방법을 사용하였었는데 나눌 경우 목적에 맞는 메서드만을 사용하게된다는 장점도 존재한다 생각한다.
만약, 이를 지키지 않는다면 하나의 변경을 통해 영향을 받을 수 있는 클라이언트에 수가 늘어난다고 생각한다.
예시
public interface UpdateService {
long updateMemberName(...);
long updateTitleName(...);
long updateChatroomName(...);
}
해당 구조의 경우 update와 관련된 로직을 하나의 Interface에 몰아서 작성했기에, Client와 관련된 영향을 줄수 있는 요소가 많아지게된다.
이를 변경하면 아래와 같이 하나의 Interface에 하나의 실행로직만을 부여함으로서 목적성과, 의존성 문제로 부터 자유로워진다.
public interface UpdateMemberService {
long updateMemberName(...);
}
public interface UpdateBoardService{
long updateTitleName(...);
}
public interface UpdateChatService{
long updateChatroomName(...);
}
결론
사실 의존성 규칙이라는 점을 지키지 않더라도 코드에는 이상이 따로 발생되지 않고 하나하나 신경쓰기에는 까다로운부분이 많다.
예를 들어 인터페이스의 사용목적 분리 또한 무분별하게 Class의 수만 늘리는게 맞는 선택인가?라는 고민도 반드시 동반된다.
하지만, 확장성
과 유지보수성
을 고려하였을 때 클린 아키텍처를 지향할수록 개발자에게 편의적인 환경이 구축된다고 생각된다.
그러니, 어렵다고 기피하기 보다는 준수하였을 때의 이점을 고려하며 적절히 섞어 사용하는 방법을 지향하고자한다.