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 또는 데이터베이스가 작동하는 방식에 대해 어떠한 가정도하지 않습니다. 그러나 우리는 UserDsGateway 및 UserPresenter를 사용 하고 있습니다 . 그렇다면 우리는 그들을 어떻게 알 수 있습니까? 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를 사용하여 적용하는 사용 사례를 만들었습니다.
그래도 우리는 몇 가지 원칙을 제쳐 두었습니다. 그러나 그들 모두는 같은 방향으로 인도합니다. 작성자의 말을 인용하여 요약 할 수 있습니다. "좋은 설계자 는 결정되지 않은 결정의 수를 최대화해야합니다 .", 경계를 사용하여 세부 사항으로부터 비즈니스 코드를 보호 하여이를 수행했습니다 .