1. 개요

장기적인 시스템을 개발할 때 우리는 가변적 인 환경을 기대해야합니다.

일반적으로 우리의 기능적 요구 사항, 프레임 워크, I / O 장치, 심지어 우리의 코드 디자인까Map 다양한 이유로 변경 될 수 있습니다. 이를 염두에두고 Clean Architecture는 우리 주변의 모든 불확실성을 고려할 때 유지 관리가 용이 ​​한 코드에 대한 지침 입니다.

이 기사에서는 Robert C. Martin의 Clean Architecture 에 따라 사용자 등록 API의 예를 작성합니다 . 엔티티, 사용 사례, 인터페이스 어댑터 및 프레임 워크 / 드라이버와 같은 원래 레이어를 사용합니다.

2. 클린 아키텍처 개요

클린 아키텍처는 SOLID , 안정적인 추상화 등과 같은 많은 코드 디자인과 원칙을 컴파일합니다  . 그러나 핵심 아이디어는 비즈니스 가치에 따라 시스템을 수준으로 나누는 것입니다  . 따라서 가장 높은 수준에는 비즈니스 규칙이 있으며 낮은 수준은 각각 I / O 장치에 가까워집니다.

또한 레벨을 레이어로 변환 할 수 있습니다. 이 경우 반대입니다. 내부 레이어는 최고 수준과 동일합니다.

이를 염두에두고 비즈니스에 필요한만큼의 수준을 가질 수 있습니다. 그러나 항상 의존성 규칙을 고려하면 높은 수준이 낮은 수준에 의존해서는 안됩니다 .

3. 규칙

사용자 등록 API에 대한 시스템 규칙 정의를 시작하겠습니다. 첫째, 비즈니스 규칙 :

  • 사용자의 비밀번호는 5 자 이상이어야합니다.

둘째, 적용 규칙이 있습니다. 사용 사례 또는 스토리와 같이 다양한 형식이 될 수 있습니다. 스토리 텔링 문구를 사용합니다.

  • 시스템은 사용자 이름과 암호를 수신하고 사용자가 존재하지 않는지 확인하고 생성 시간과 함께 새 사용자를 저장합니다.

데이터베이스, UI 또는 이와 유사한 것에 대한 언급이 없는지 확인하십시오. 우리 비즈니스는 이러한 세부 사항에 대해 신경 쓰지 않기 때문에 코드도 마찬가지입니다.

4. 엔티티 레이어

깨끗한 아키텍처에서 알 수 있듯이 비즈니스 규칙부터 시작하겠습니다.

interface User {
    boolean passwordIsValid();

    String getName();

    String getPassword();
}

그리고 UserFactory :

interface UserFactory {
    User create(String name, String password);
}

우리 는 두 가지 이유로 사용자 팩토리 방법을 만들었습니다  . 재고에에 안정적인 추상화 원리 와 사용자 생성을 분리합니다.

다음으로 두 가지를 모두 구현해 보겠습니다.

class CommonUser implements User {

    String name;
    String password;

    @Override
    public boolean passwordIsValid() {
        return password != null && password.length() > 5;
    }

    // Constructor and getters
}
class CommonUserFactory implements UserFactory {
    @Override
    public User create(String name, String password) {
        return new CommonUser(name, password);
    }
}

비즈니스가 복잡한 경우 가능한 한 명확하게 도메인 코드를 작성해야합니다 . 따라서이 레이어는 디자인 패턴 을 적용하기에 좋은 곳 입니다. 특히, 도메인 중심의 디자인은 고려되어야한다.

4.1. 단위 테스트

이제 CommonUser를 테스트 해 보겠습니다 .

@Test
void given123Password_whenPasswordIsNotValid_thenIsFalse() {
    User user = new CommonUser("Baeldung", "123");

    assertThat(user.passwordIsValid()).isFalse();
}

보시다시피 단위 테스트는 매우 명확합니다. 결국 모의가 없다는 것은이 레이어에 좋은 신호입니다 .

일반적으로 여기서 모의에 대해 생각하기 시작하면 엔티티와 사용 사례를 혼합 할 수 있습니다.

5. 사용 사례 계층

사용 사례는 시스템 자동화와 관련된 규칙 입니다. Clean Architecture에서는 인터랙 터라고합니다.

5.1. UserRegisterInteractor

먼저 UserRegisterInteractor를 빌드하여 어디로 가는지 볼 수 있습니다. 그런 다음 사용 된 모든 부품을 만들고 논의합니다.

class UserRegisterInteractor implements UserInputBoundary {

    final UserRegisterDsGateway userDsGateway;
    final UserPresenter userPresenter;
    final UserFactory userFactory;

    // Constructor

    @Override
    public UserResponseModel create(UserRequestModel requestModel) {
        if (userDsGateway.existsByName(requestModel.getName())) {
            return userPresenter.prepareFailView("User already exists.");
        }
        User user = userFactory.create(requestModel.getName(), requestModel.getPassword());
        if (!user.passwordIsValid()) {
            return userPresenter.prepareFailView("User password must have more than 5 characters.");
        }
        LocalDateTime now = LocalDateTime.now();
        UserDsRequestModel userDsModel = new UserDsRequestModel(user.getName(), user.getPassword(), now);

        userDsGateway.save(userDsModel);

        UserResponseModel accountResponseModel = new UserResponseModel(user.getName(), now.toString());
        return userPresenter.prepareSuccessView(accountResponseModel);
    }
}

보시다시피 모든 사용 사례 단계를 수행하고 있습니다. 또한 이 레이어는 개체의 춤을 제어하는 ​​역할을합니다. 그럼에도 불구하고 우리는 UI 또는 데이터베이스가 작동하는 방식에 대해 어떠한 가정도하지 않습니다. 그러나 우리는 UserDsGatewayUserPresenter를 사용 하고 있습니다 . 그렇다면 우리는 그들을 어떻게 알 수 있습니까? UserInputBoundary 와 함께 입력 및 출력 경계 이기 때문입니다.

5.2. 입력 및 출력 경계

경계는 구성 요소가 상호 작용할 수있는 방법을 정의하는 계약입니다. 입력 경계는 외부 층에 우리의 유스 케이스를 노출 :

interface UserInputBoundary {
    UserResponseModel create(UserRequestModel requestModel);
}

다음으로 외부 레이어를 사용하기위한 출력 경계가 있습니다 . 먼저 데이터 소스 게이트웨이를 정의 해 보겠습니다.

interface UserRegisterDsGateway {
    boolean existsByName(String name);

    void save(UserDsRequestModel requestModel);
}

둘째, 뷰 발표자 :

interface UserPresenter {
    UserResponseModel prepareSuccessView(UserResponseModel user);

    UserResponseModel prepareFailView(String error);
}

참고 우리가 사용하고있는 의존성 반전 원칙을 같은 데이터베이스 및 사용자 인터페이스 등의 세부 사항에서 우리의 사업이 무료로 만들기 위해 .

5.3. 디커플링 모드

계속하기 전에 경계가 시스템의 자연적 분할을 정의하는 계약 인지 확인하십시오  . 그러나 우리는 또한 우리의 응용 프로그램이 어떻게 전달 될 것인지 결정해야합니다.

  • 모 놀리 식 – 일부 패키지 구조를 사용하여 구성 가능
  • 모듈을 사용하여
  • 서비스 / 마이크로 서비스 사용

이를 염두에두고  어떤 디커플링 모드로든 깨끗한 아키텍처 목표를 달성 할 수 있습니다 . 따라서 현재와 ​​미래의 비즈니스 요구 사항에 따라 이러한 전략을 변경할 준비를해야합니다 . 디커플링 모드를 선택한 후에는 경계에 따라 코드 분할이 이루어져야합니다.

5.4. 요청 및 응답 모델

지금까지 인터페이스를 사용하여 여러 계층에서 작업을 만들었습니다. 다음으로 이러한 경계를 넘어 데이터를 전송하는 방법을 살펴 보겠습니다.

모든 경계가 String 또는 Model 객체 만 처리하는 방법에 유의하십시오 .

class UserRequestModel {

    String login;
    String password;

    // Getters, setters, and constructors
}

기본적으로 단순한 데이터 구조 경계를 넘을 수 있습니다 . 또한 모든 모델 에는 필드와 접근 자만 있습니다 . 또한 데이터 개체는 내부에 속합니다. 따라서 의존성 규칙을 유지할 수 있습니다.

그런데 왜 비슷한 물건이 그렇게 많을까요? 반복되는 코드는 두 가지 유형이 될 수 있습니다.

  • 거짓 또는 우발적 인 복제 – 각 개체는 변경해야하는 다른 이유가 있으므로 코드 유사성은 우연입니다. 이를 제거하려고하면 단일 책임 원칙을 위반할 위험이  있습니다.
  • 진정한 복제 – 같은 이유로 코드가 변경됩니다. 따라서 제거해야합니다.

모델마다 책임이 다르기 때문에 우리는 이러한 모든 객체를 얻었습니다.

5.5. UserRegisterInteractor 테스트

이제 단위 테스트를 만들어 보겠습니다.

@Test
void givenBaeldungUserAnd12345Password_whenCreate_thenSaveItAndPrepareSuccessView() {
    given(userDsGateway.existsByIdentifier("identifier"))
        .willReturn(true);

    interactor.create(new UserRequestModel("baeldung", "123"));

    then(userDsGateway).should()
        .save(new UserDsRequestModel("baeldung", "12345", now()));
    then(userPresenter).should()
        .prepareSuccessView(new UserResponseModel("baeldung", now()));
}

보시다시피 대부분의 사용 사례 테스트는 엔터티 및 경계 요청을 제어하는 ​​것입니다. 그리고 인터페이스를 통해 세부 사항을 쉽게 조롱 할 수 있습니다.

6. 인터페이스 어댑터

이 시점에서 우리는 모든 사업을 마쳤습니다. 이제 세부 사항을 연결해 보겠습니다.

우리의 사업은 가장 편리한 데이터 형식만을 다루어야하며 , DB 또는 UI와 같은 외부 에이전트도 처리해야합니다. 그러나이 형식은 일반적으로 다릅니다 . 이러한 이유로 인터페이스 어댑터 계층은 데이터 변환을 담당합니다 .

6.1. JPA를 사용하는 UserRegisterDsGateway

먼저 JPA사용 하여 사용자 테이블 을 매핑 해 보겠습니다 .

@Entity
@Table(name = "user")
class UserDataMapper {

    @Id
    String name;

    String password;

    LocalDateTime creationTime;

    //Getters, setters, and constructors
}

보시다시피 Mapper의 목표는 개체를 데이터베이스 형식에 매핑하는 것입니다.

다음으로 엔티티를 사용 하는 JpaRepository :

@Repository
interface JpaUserRepository extends JpaRepository<UserDataMapper, String> {
}

우리가 spring-boot를 사용할 것이라는 점을 감안할 때 이것이 사용자를 저장하는 데 필요한 전부입니다.

이제 UserRegisterDsGateway 를 구현할 시간입니다 .

class JpaUser implements UserRegisterDsGateway {

    final JpaUserRepository repository;

    // Constructor

    @Override
    public boolean existsByName(String name) {
        return repository.existsById(name);
    }

    @Override
    public void save(UserDsRequestModel requestModel) {
        UserDataMapper accountDataMapper = new UserDataMapper(requestModel.getName(), requestModel.getPassword(), requestModel.getCreationTime());
        repository.save(accountDataMapper);
    }
}

대부분의 경우 코드는 그 자체로 말합니다. 메서드 외에도 UserRegisterDsGateway의 이름을 기록해 둡니다 . 대신 UserDsGateway선택하면 다른 사용자 사용 사례가 인터페이스 분리 원칙 을 위반하게 될 것 입니다.

6.2. 사용자 등록 API

이제 HTTP 어댑터를 만들어 보겠습니다.

@RestController
class UserRegisterController {

    final UserInputBoundary userInput;

    // Constructor

    @PostMapping("/user")
    UserResponseModel create(@RequestBody UserRequestModel requestModel) {
        return userInput.create(requestModel);
    }
}

보시다시피 여기서 유일한 목표는 요청을 받고 클라이언트에 응답보내는 것 입니다.

6.3. 응답 준비

응답하기 전에 응답 형식을 지정해야합니다.

class UserResponseFormatter implements UserPresenter {

    @Override
    public UserResponseModel prepareSuccessView(UserResponseModel response) {
        LocalDateTime responseTime = LocalDateTime.parse(response.getCreationTime());
        response.setCreationTime(responseTime.format(DateTimeFormatter.ofPattern("hh:mm:ss")));
        return response;
    }

    @Override
    public UserResponseModel prepareFailView(String error) {
        throw new ResponseStatusException(HttpStatus.CONFLICT, error);
    }
}

우리  UserRegisterInteractor는  발표자를 만들기 위해 우리를 강요했다. 그러나 프레젠테이션 규칙은 어댑터 내에서만 적용됩니다. 게다가, henever 뭔가 테스트 하드, 우리는 검증과로 분할한다 겸손 객체 .  따라서  UserResponseFormatter를 사용하면 프레젠테이션 규칙을 쉽게 확인할 수 있습니다.

@Test
void givenDateAnd3HourTime_whenPrepareSuccessView_thenReturnOnly3HourTime() {
    UserResponseModel modelResponse = new UserResponseModel("baeldung", "2020-12-20T03:00:00.000");
    UserResponseModel formattedResponse = userResponseFormatter.prepareSuccessView(modelResponse);

    assertThat(formattedResponse.getCreationTime()).isEqualTo("03:00:00");
}

보시다시피, 우리는 모든 로직을 뷰로 보내기 전에 테스트했습니다. 따라서 테스트하기 어려운 부분에는 겸손한 대상 만 있습니다.

7. 드라이버 및 프레임 워크

사실, 우리는 일반적으로 여기서 코딩하지 않습니다. 이는이 계층이 외부 에이전트에 대한 가장 낮은 수준의 연결을 나타 내기 때문 입니다. 예를 들어 H2 드라이버는 데이터베이스 또는 웹 프레임 워크에 연결합니다. 이 경우 의존성 주입 프레임 워크 spring-boot 를 사용할 것 입니다. 따라서 시작 지점이 필요합니다.

@SpringBootApplication
public class CleanArchitectureApplication {
    public static void main(String[] args) {
      SpringApplication.run(CleanArchitectureApplication.class);
    }
}

지금까지 우리는 비즈니스에서 스프링 어노테이션 사용하지 않았습니다  . 스프링 구체적인 어댑터를 제외하고 우리로, UserRegisterController . 이것은 우리가  spring-boot를 다른 세부 사항으로 취급 해야 하기 때문 입니다.

8. 끔찍한 메인 클래스

드디어 마지막 작품!

지금까지 안정적인 추상화 원칙을 따랐습니다 . 또한 우리 는 제어반전을 통해 외부 에이전트로부터 내부 레이어를 보호했습니다 . 마지막으로 모든 개체 생성과 사용을 분리했습니다. 이 시점에서 나머지 의존성생성하고 프로젝트에 주입하는 것은 우리에게 달려 있습니다 .

@Bean
BeanFactoryPostProcessor beanFactoryPostProcessor(ApplicationContext beanRegistry) {
    return beanFactory -> {
        genericApplicationContext(
          (BeanDefinitionRegistry) ((AnnotationConfigServletWebServerApplicationContext) beanRegistry)
            .getBeanFactory());
    };
}

void genericApplicationContext(BeanDefinitionRegistry beanRegistry) {
    ClassPathBeanDefinitionScanner beanDefinitionScanner = new ClassPathBeanDefinitionScanner(beanRegistry);
    beanDefinitionScanner.addIncludeFilter(removeModelAndEntitiesFilter());
    beanDefinitionScanner.scan("com.baeldung.pattern.cleanarchitecture");
}

static TypeFilter removeModelAndEntitiesFilter() {
    return (MetadataReader mr, MetadataReaderFactory mrf) -> !mr.getClassMetadata()
      .getClassName()
      .endsWith("Model");
}

우리의 경우  모든 인스턴스를 생성하기 위해 spring-boot  의존성 주입 을 사용하고 있습니다. @Component를 사용하지 않기 때문에  루트 패키지를 스캔하고 Model 객체 만 무시 합니다 .

이 전략은 더 복잡해 보일 수 있지만 DI 프레임 워크에서 비즈니스를 분리합니다. 반면에 메인 클래스는 우리의 모든 시스템을 지배했습니다 . 이것이 클린 아키텍처가 다른 모든 것을 포용하는 특수 레이어에서 고려하는 이유입니다.

9. 결론

이 기사에서 우리는 밥 삼촌의 깨끗한 아키텍처가 어떻게 많은 디자인 패턴과 원칙 위에 구축 되었는지 배웠습니다  . 또한 Spring Boot를 사용하여 적용하는 사용 사례를 만들었습니다.

그래도 우리는 몇 가지 원칙을 제쳐 두었습니다. 그러나 그들 모두는 같은 방향으로 인도합니다. 작성자의 말을 인용하여 요약 할 수 있습니다. "좋은 설계자  는 결정되지 않은 결정의 수를 최대화해야합니다 .", 경계를 사용하여 세부 사항으로부터 비즈니스 코드를 보호 하여이를 수행했습니다  .

평소처럼 전체 코드는 GitHub에서 사용할 수  있습니다 .