diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/management/JdbcProjectActivityRepositoryAdapter.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/management/JdbcProjectActivityRepositoryAdapter.java index 7ff1005..758b0b4 100644 --- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/management/JdbcProjectActivityRepositoryAdapter.java +++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/management/JdbcProjectActivityRepositoryAdapter.java @@ -9,7 +9,6 @@ import java.sql.SQLException; import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.function.Supplier; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Repository; @@ -30,15 +29,14 @@ public class JdbcProjectActivityRepositoryAdapter implements ProjectActivityRepo + " related_resource_type, related_resource_id, occurred_at, version"; private final JdbcClient jdbcClient; - private final Supplier idGenerator; + /* + * 생성자는 하나뿐이어야 한다. 한때 테스트용 id 생성기를 받는 두 번째 생성자가 있었고, 그러면 + * 컴포넌트 스캔이 어느 것을 쓸지 정하지 못해 기본 생성자를 찾다가 실패한다 — 컴파일도 테스트도 + * 통과하고 운영에서 기동만 못 한다. id 는 여기서 만들면 되고, 통합 테스트는 그 값을 볼 필요가 없다. + */ public JdbcProjectActivityRepositoryAdapter(JdbcClient jdbcClient) { - this(jdbcClient, UUID::randomUUID); - } - - JdbcProjectActivityRepositoryAdapter(JdbcClient jdbcClient, Supplier idGenerator) { this.jdbcClient = jdbcClient; - this.idGenerator = idGenerator; } private static ProjectActivityView map(ResultSet rs, int rowNum) throws SQLException { @@ -85,7 +83,7 @@ public class JdbcProjectActivityRepositoryAdapter implements ProjectActivityRepo @Override public ProjectActivityView create(CreateProjectActivityCommand command) { - UUID id = idGenerator.get(); + UUID id = UUID.randomUUID(); jdbcClient .sql( "INSERT INTO project_activity (id, project_id, activity_type, title, summary," diff --git a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java index 74be600..e89c68c 100644 --- a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java +++ b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java @@ -2158,4 +2158,67 @@ class CleanArchitectureTest { .filter(value -> value instanceof Number number && number.intValue() == SqlTypes.UUID) .isPresent(); } + + /** + * Spring 이 생성자를 고를 수 있어야 한다. + * + *

이 규칙은 사고 하나에서 나왔다. 한 어댑터가 생성자를 둘 갖고 있었고 — 하나는 운영용, 하나는 테스트가 id 생성기를 넣기 위한 것 — 둘 중 어느 것에도 + * {@code @Autowired} 가 없었다. 컴포넌트 스캔은 고르지 못하고 기본 생성자를 찾다가 실패한다. 컴파일도, 단위 테스트도, 실제 PostgreSQL 위에서 + * 도는 통합 테스트도 전부 통과했다. 그 어느 것도 컨텍스트를 띄우지 않기 때문이다. 운영에서 기동만 못 했다. + * + *

그래서 여기서 막는다: 스캔되는 스프링 컴포넌트는 생성자가 하나이거나, 여럿이면 그중 하나에 {@code @Autowired} 가 붙어 있어야 한다. + */ + @ArchTest + static final ArchRule SPRING_COMPONENTS_HAVE_AN_UNAMBIGUOUS_CONSTRUCTOR = + classes() + .that() + .areAnnotatedWith("org.springframework.stereotype.Repository") + .or() + .areAnnotatedWith("org.springframework.stereotype.Service") + .or() + .areAnnotatedWith("org.springframework.stereotype.Component") + .or() + .areAnnotatedWith("org.springframework.web.bind.annotation.RestController") + .should(haveAConstructorSpringCanChoose()) + .as( + "D20: a scanned Spring component must have exactly one constructor, or mark one" + + " @Autowired — otherwise component scan falls back to a no-arg constructor that" + + " does not exist and the application fails to start"); + + private static ArchCondition haveAConstructorSpringCanChoose() { + return new ArchCondition<>("have a constructor Spring can choose") { + @Override + public void check(JavaClass item, ConditionEvents events) { + var constructors = + item.getConstructors().stream() + .filter(constructor -> !constructor.getModifiers().contains(JavaModifier.SYNTHETIC)) + .toList(); + if (constructors.size() <= 1) { + return; + } + boolean autowired = + constructors.stream() + .anyMatch( + constructor -> + constructor.getAnnotations().stream() + .anyMatch( + annotation -> + annotation + .getRawType() + .getName() + .equals( + "org.springframework.beans.factory.annotation.Autowired"))); + if (!autowired) { + events.add( + SimpleConditionEvent.violated( + item, + item.getName() + + " declares " + + constructors.size() + + " constructors and marks none @Autowired;" + + " component scan cannot choose one")); + } + } + }; + } }