diff --git a/build.gradle b/build.gradle index 1d92d52..6a5952b 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.16' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-database-postgresql' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0' testRuntimeOnly 'com.h2database:h2' testCompileOnly 'org.projectlombok:lombok' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/chaeso/zip/server/sample/application/SampleService.java b/src/main/java/chaeso/zip/server/sample/application/SampleService.java index 08817b3..4fa12d3 100644 --- a/src/main/java/chaeso/zip/server/sample/application/SampleService.java +++ b/src/main/java/chaeso/zip/server/sample/application/SampleService.java @@ -1,46 +1,32 @@ package chaeso.zip.server.sample.application; +import chaeso.zip.server.sample.application.dto.CreateSampleCommand; import chaeso.zip.server.sample.application.dto.SampleResponse; -import chaeso.zip.server.sample.domain.Sample; -import chaeso.zip.server.sample.domain.SampleNotFoundException; -import chaeso.zip.server.sample.domain.SampleRepository; import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; /** - * 샘플 애플리케이션 서비스. 트랜잭션 경계와 유스케이스 흐름을 담당하며 도메인 객체에 작업을 위임한다. + * 샘플 애플리케이션 서비스. 유스케이스 진입점(인터페이스)으로, 구현은 {@link SampleServiceImpl} 가 담당한다. * *
컨벤션: *
컨벤션: + *
컨벤션: + *
계층 규칙: presentation → application → domain (역방향 의존 금지) + */ +@AnalyzeClasses( + packages = "chaeso.zip.server", + importOptions = ImportOption.DoNotIncludeTests.class) +class LayerDependencyTest { + + @ArchTest + static final ArchRule 계층_의존_방향을_지킨다 = Architectures.layeredArchitecture() + .consideringOnlyDependenciesInLayers() + .layer("Presentation").definedBy("..presentation..") + .layer("Application").definedBy("..application..") + .layer("Domain").definedBy("..domain..") + .whereLayer("Presentation").mayNotBeAccessedByAnyLayer() + .whereLayer("Application").mayOnlyBeAccessedByLayers("Presentation") + .whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Presentation"); + + @ArchTest + static final ArchRule 도메인은_애플리케이션_표현_계층에_의존하지_않는다 = noClasses() + .that().resideInAPackage("..domain..") + .should().dependOnClassesThat().resideInAnyPackage("..application..", "..presentation..") + .as("도메인 계층은 상위 계층(application/presentation)에 의존해서는 안 된다"); + + @ArchTest + static final ArchRule 도메인은_웹_계층에_의존하지_않는다 = noClasses() + .that().resideInAPackage("..domain..") + .should().dependOnClassesThat().resideInAnyPackage("org.springframework.web..") + .as("도메인 계층은 Spring Web 에 의존해서는 안 된다"); + + @ArchTest + static final ArchRule 컨트롤러는_표현_계층에만_위치한다 = classes() + .that().areAnnotatedWith(RestController.class) + .should().resideInAPackage("..presentation..") + .as("@RestController 는 presentation 패키지에만 위치해야 한다"); + + @ArchTest + static final ArchRule 서비스는_애플리케이션_계층에만_위치한다 = classes() + .that().areAnnotatedWith(Service.class) + .should().resideInAPackage("..application..") + .as("@Service 는 application 패키지에만 위치해야 한다"); +} diff --git a/src/test/java/chaeso/zip/server/sample/application/SampleServiceTest.java b/src/test/java/chaeso/zip/server/sample/application/SampleServiceTest.java index 1887e84..471271d 100644 --- a/src/test/java/chaeso/zip/server/sample/application/SampleServiceTest.java +++ b/src/test/java/chaeso/zip/server/sample/application/SampleServiceTest.java @@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import chaeso.zip.server.sample.application.dto.CreateSampleCommand; import chaeso.zip.server.sample.application.dto.SampleResponse; import chaeso.zip.server.sample.domain.Sample; import chaeso.zip.server.sample.domain.SampleNotFoundException; @@ -27,14 +28,14 @@ class SampleServiceTest { private SampleRepository sampleRepository; @InjectMocks - private SampleService sampleService; + private SampleServiceImpl sampleService; @Test @DisplayName("샘플을 생성하면 저장 후 응답을 반환한다") void create() { given(sampleRepository.save(any(Sample.class))).willAnswer(invocation -> invocation.getArgument(0)); - SampleResponse response = sampleService.create("채소"); + SampleResponse response = sampleService.create(new CreateSampleCommand("채소")); assertThat(response.name()).isEqualTo("채소"); } diff --git a/src/test/java/chaeso/zip/server/sample/presentation/SampleControllerTest.java b/src/test/java/chaeso/zip/server/sample/presentation/SampleControllerTest.java index 659a652..365ee11 100644 --- a/src/test/java/chaeso/zip/server/sample/presentation/SampleControllerTest.java +++ b/src/test/java/chaeso/zip/server/sample/presentation/SampleControllerTest.java @@ -1,12 +1,13 @@ package chaeso.zip.server.sample.presentation; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import chaeso.zip.server.sample.application.SampleService; +import chaeso.zip.server.sample.application.dto.CreateSampleCommand; import chaeso.zip.server.sample.application.dto.SampleResponse; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; @@ -37,7 +38,7 @@ class SampleControllerTest { @Test @DisplayName("샘플 생성 요청이 성공하면 201 과 공통 응답 포맷을 반환한다") void create_success() throws Exception { - given(sampleService.create(anyString())) + given(sampleService.create(any(CreateSampleCommand.class))) .willReturn(new SampleResponse(1L, "채소", LocalDateTime.now(), LocalDateTime.now())); mockMvc.perform(post("/api/v1/samples") diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml index 71bc9b9..56d3f5f 100644 --- a/src/test/resources/application.yaml +++ b/src/test/resources/application.yaml @@ -1,4 +1,6 @@ spring: + flyway: + enabled: false datasource: url: jdbc:h2:mem:chaeso-zip;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver