-
Notifications
You must be signed in to change notification settings - Fork 0
Description
3.3 소스 코드로 이해하기: 전형적인 시나리오에서 클린 아키텍처의 핵심을 도출하자
3.3.1 전형적인 시나리오
클린 아키텍처는 특정한 구현 패턴을 지칭하고 있는 것이 아닙니다. <클린 아키텍처>의 22장에서는 전형적인 시나리오를 그림과 같이 제시하고 있습니다.
이 시나리오에 따른 구현 과정을 확인하고, 클린 아키텍처의 본질을 구체적으로 파악해 보겠습니다.
화살표의 모양과 의미
일반 화살표는 참조를 의미합니다.
변수로 이용하거나 반환값으로 하거나 다양한 형태로 객체를 참조합니다.
끝이 흰색인 화살표는 일반화를 의미합니다.
인터페이스와 그 구현 클래스의 관계 등을 가리킵니다.
제어 흐름 확인하기
- Controller가 사용자의 입력을 받아 Input Boundary에 데이터를 넘겨준다.
- Input Boundary 구현인 Use Case Interactor가 처리를 맡고 Data Access Interface 등을 활용해 목적을 달성하고 처리 결과를 Output Boundary에 넘겨준다.
- Output Boundary 구현인 Presenter가 처리를 맡아 Output Data에서 View Model 데이터를 생성한다.
- View가 View Model을 참조해 화면을 표시한다.
Controller와 주변 객체
- Input Data
- Input Boundary
- Controller
Input Data 구현 확인하기
사용자 입력은 응용 프로그램이 원하는 형식으로 변환됩니다.
Input Data는 애플리케이션에 전달할 데이터이며, 여기서 말하는 애플리케이션은 특정한 목적을 위한 프로그램입니다.
사용자가 조작하는 인터페이스는 다양합니다.
이런 인터페이스 하나하나에 애플리케이션이 일일이 대응하는 것은 그다지 바람직한 것은 아닙니다.
그래서 아래 코드와 같이 통일된 형식의 데이터를 우선 정의합니다.
public record TypicalCreateInputData(String data) {}이 데이터 타입을 공개함으로써 인터페이스가 통일된 규격의 Input Data로 가공됩니다.
Input Boundary 구현 확인하기
Boundary는 경계를 의미하며, Input Boundary는 인터페이스와 애플리케이션의 경계에 있습니다.
Input Boundary 오른쪽 위에 있는 < I >는 프로그래밍 언어 기능으로 제공되는 인터페이스를 의미합니다.
따라서 Input Boundary를 구현하면 대략 아래 코드와 같습니다.
public interface TypicalCreateInputBoundary {
void handle(TypicalCreateInputData inputData);
}Controller 구현 확인하기
Controller는 MVC(Model-View-Controller)에서의 컨트롤러와 동일한 역할을 합니다.
MVC의 컨트롤러는 사용자의 입력값을 애플리케이션을 위해 변환합니다.
따라서 Controller는 사용자가 입력한 데이터를 애플리케이션이 원하는 형태로 변환하여 Input Boundary에 전달하는 것이 주요 역할입니다.
Use Case Interactor와 주변 객체
- Output Data
- Output Boundary
- Entities
- Data Access Interface
- Use Case Interactor
Output Data 구현 확인하기
처리 결과를 확인하기 위한 인터페이스는 다양합니다.
Output Data 오른쪽 위에도 가 있습니다.
이것도 Input Data와 마찬가지로 POJO에서 애플리케이션의 Output Data를 정의합니다.
public record TypicalCreateOutputData(UUID id, String data, LocalDateTime createdAt) {}Output Boundary 구현 확인하기
Input Boundary는 입력 인터페이스와 애플리케이션의 경계로 정의되는 반면, Output Boundary는 애플리케이션과 출력 인터페이스의 경계로 정의됩니다.
< I >가 표시되어 있으므로 Output Boundary는 인터페이스로 구현합니다.
public interface TypicalCreateOutputBoundary {
void handle(TypicalCreateOutputData outputData);
}Entities 구현 확인하기
Entities는 비즈니스 규칙을 캡슐화한 객체로, 도메인 객체로 불리는 것과 동일한 개념입니다.
에릭 에반스의 <도메인 주도 설계>에는 창고와 관련한 예시가 있습니다.
여기서 창고는 화학약품 창고로 다양한 화학약품이 보관되어 있습니다.
화학약품은 폭발할 수 있는 물질이라서 강화된 컨테이너가 필요하다는 조건이 있습니다.
하나의 단순한 예로써, 그러한 규칙 등이 기술되는 객체를 Entities라고 표현합니다.
Data Access Interface 구현 확인하기
Entities에 소속된 객체도 수명주기가 존재한다면 이를 데이터 저장소에 저장하고 읽을 수 있어야 합니다.
시스템에 필요한 저장 처리를 위한 객체가 Data Access Interface입니다.
대표적인 것은 데이터베이스 접근을 추상화하는 DAO(Data Access Object) 패턴이나 도메인 객체를 주체로 하여 데이터에 접근하는 리포지터리 패턴 등입니다.
Use Case Interactor
Use Case Interactor는 애플리케이션 사용자의 목적에 따라 다양하며, 다양한 엔터티들을 잘 활용해서 사용자의 목적에 맞는 유스케이스를 실행하는 역할을 수행합니다.
Data Access 구현 확인하기
public class JpaSampleRepository implements SampleRepository {
private final SampleDataModelJpaRepository jpaRepository;
...생략...
@Override
public void save(Sample sample) {
var dataModel = SampleDataModel.builder()
.id(sample.getSampleId().value())
.data(sample.getData()).build();
jpaRepository.save(dataModel);
}
}위 코드는 Data Access Interface인 SampleRepository를 구현한 객체로, 저장 기술로는 JPA(Java Persistence API)를 이용하고 있습니다.
Presenter 구현 확인하기
Presenter는 Output Boundary를 구현하는 형태로 정의합니다.
이번에는 웹을 이용하므로 View Model은 HTTP 방식으로 합니다.
아래 코드에서는 View Model인 TypicalPostResponseModel에 Output Data의 데이터를 사용하여 필드에 저장하고 있습니다.
View Model을 전달받고자 하는 객체는 result 메서드를 호출하여 해당 값을 가져오는 구조입니다.
public class TypicalCreatePresenter implements TypicalCreateOutputBoundary {
private TypicalPostResponseModel viewModel;
@Override
public void handle(TypicalCreateOutputData outputData)
{
viewModel = new TypicalPostResponseModel(outputData.id(), outputData.data());
}
public TypicalPostResponseModel result() {
return viewModel;
}
}DI 컨테이너 설정하기
DI 컨테이너를 활용하면 일괄적으로 객체를 생성할 수 있습니다.
객체의 생성과 이용은 별개의 관심사로, 이처럼 DI 컨테이너를 이용하면 관심사를 분리하여 느슨한 결합을 구현할 수 있습니다.
처리 흐름 확인하기
...여태까지 설명한 내용의 흐름 요약
HTTP 응답은 어떻게 할 것인가
일반적인 MVC 흐름을 따르는 웹 프레임워크에서는 Controller가 HTTP 응답 데이터를 생성합니다.
그러므로 Controller는 결과를 받아야 하는데, Controller에서 Output Data나 View Model로 화살표가 나가지 않습니다.
이 시나리오를 충실하게 재현하려면 별도의 처리 로직이 필요합니다.
샘플 프로젝트에서는 Presenter에서 HTTP 응답을 생성하도록 구현하고 있습니다.
3.3.2 실천으로 이어지는 변화
프레임워크가 제공하는 일반적인 기법과는 달리, 인터셉터를 이용해 HTTP 응답을 작성하는 방식에 거부감을 느끼는 분들도 있을 것입니다.
특히 많은 개발자가 컨트롤러의 처리와 HTTP 응답이 어떻게 연결되는지 혼란스러워할 수 있습니다.
앞에서 제시한 기법은 어디까지나 전형적인 시나리오를 충실히 재현하기 위한 선택이었습니다.
하지만 클린 아키텍처가 제안하는 것은 전형적인 시나리오에 따라야 한다는 것이 아닙니다.
뭔가 맞지 않는 점이 있으면 어떻게든 상황에 맞게 수정해도 문제없으니, 이러한 클린 아키텍처의 취지에 따라 형태를 조금 바꿔 보겠습니다.
클린 아키텍처의 본질을 생각하기
클린 아키텍처가 요구하는 것은 단순합니다.
동심원의 그림과 같이 모든 의존 관계가 바깥에서 안쪽을 향하게 함으로써 업무 로직과 같은 상위 계층이 결정권을 가지게 하는 것입니다.
바꿔 말하면, 외부에 있는 인터페이스나 데이터 저장소 등과 같은 하위 수준이 업무 로직을 휘두르게 하고 싶지 않다는 것입니다.
방법은 DTO
소프트웨어에서 계층을 넘어 데이터를 전달하는 방법으로 전용 객체를 이용하는 데이터 전송 객체(DTO, Data Transfer Object)가 있습니다.
Input Data는 인터페이스와 애플리케이션의 경계를 넘을 때 DTO를 사용 합니다.
설명한 예제는 HTTP 기술에 의존하지 않고도 데이터 객체를 이용해 인터페이스와 애플리케이션 간에 데이터를 주고받을 수 있습니다.
변화된 프로그램 확인하기
Input Boundary와 Use Case Interactor
우선 객체가 바로 반환값을 돌려주도록 수정해야 합니다.
값을 반환하게 되면서 Output Boundary가 사라졌습니다.
따라서 Output Boundary와 Presenter는 불필요해졌습니다.
public interface PracticalCreateInputBoundary {
PracticalCreateOutputData handle(PracticalCreateInputData inputData);
}
public class PracticalCreateInteractor implements PracticalCreateInputBoundary {
private final SampleRepository sampleRepository;
...생략...
@Override
public PracticalCreateOutputData handle(PracticalCreateInputData inputData) {
var sample = new Sample(new SampleId(), inputData.data());
sampleRepository.save(sample);
return new PracticalCreateOutputData(
sample.getSampleId().value(),
sample.getData()));
}
}Controller의 변화
Presenter의 역할은 이제 Controller가 수행합니다.
@RestController @RequestMapping('api/practical')public class PracticalController {
private final PracticalCreateInputBoundary createInputBoundary;
...생략...
@PostMapping
public PracticalPostResponseModel post(@RequestBody PracticalPostRequestModel request) {
var inputData = new PracticalCreateInputData(request.data());
var outputData = createInputBoundary.handle(inputData);
return new PracticalPostResponseModel(outputData.id(), outputData.data());
}
}로버트 C. 마틴이 동심원의 그림이나 전형적인 시나리오에서 Presenter를 정의하고 있는지 유추해 보면, UI를 분리하여 처리하려는 것과 관련된 것이라고 볼 수 있습니다.
클린 아키텍처는 웹에만 국한된 것이 아니기 때문에 UI를 의식한 Presenter의 존재가 전형적인 시나리오에 포함되어 있다고 생각됩니다.
에피소드
이 아키텍처의 형태를 지킴으로써 결과적으로는 개발이나 유지보수에는 분명히 좋은 영향을 미치고 있습니다.
코드의 수정 이력을 봐도 엉뚱한 곳을 고쳐야 했던 상황은 보이지 않습니다.
이는 의존성이 항상 내부를 향하도록 설계된 구조 덕분입니다.
덕분에 세부적인 부분에서 수정이 필요할 때 그 영향은 주로 동심원 구조의 바깥쪽에 국한됩니다.
물론 수정이 추상적인 부분이라도 그 수정 내역은 정확하게 전달됩니다.
또한 유스케이스들이 분리되어 있어, 불필요한 의존 관계가 발생하지 않다 보니 좋은 결과를 가져왔습니다.
운영 중인 몇 명의 구성원에게 피드백을 받아보았는데, 한결같이 학습 비용은 많이 들었지만 그만큼 운영 단계에서 효율적으로 사용할 수 있다고 하였습니다.
의존 방향이 적절하고 객체의 책임이 분산되다 보니 무언가를 변경할 때도 영향 범위를 파악하기가 쉬웠기 때문입니다.
3.3.3 요약
사람들은 코드를 통해 다양한 감정을 느낍니다.
지저분하게 작성된 코드를 통해 반면교사로 삼고, 깔끔하게 잘 작성된 코드를 보면 감동합니다.
이렇듯 코드는 형태를 바꾸어 가면서 미래의 제품으로 이어지고, 좋은 아키텍처는 좋은 제품의 든든한 울타리가 됩니다.
Metadata
Metadata
Assignees
Labels
Projects
Status
