fix: 스캔되는 스프링 컴포넌트의 생성자를 하나로 고정한다
새 활동 어댑터가 생성자를 둘 갖고 있었다 — 하나는 운영용, 하나는 테스트가 id 생성기를 넣기 위한 것. 둘 중 어느 것에도 @Autowired 가 없어 컴포넌트 스캔은 고르지 못하고 기본 생성자를 찾다가 실패했다. 컴파일도, 단위 테스트도, 실제 PostgreSQL 위에서 도는 통합 테스트 26개도 전부 통과했다. 그 어느 것도 애플리케이션 컨텍스트를 띄우지 않기 때문이다. 운영에서 파드가 CrashLoopBackOff 로 들어갔고, 그때서야 드러났다. 생성자를 하나로 줄이고 — id 는 어댑터가 만들면 되고 통합 테스트는 그 값을 볼 필요가 없다 — 같은 실수를 다시 못 하게 D20 규칙을 세운다: 스캔되는 컴포넌트는 생성자가 하나이거나, 여럿이면 그중 하나에 @Autowired 가 붙어야 한다. 규칙이 실제로 잡는지 결함을 되돌려 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
4c14f1eb8f
commit
ca63d7d3ec
+6
-8
@@ -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<UUID> idGenerator;
|
||||
|
||||
/*
|
||||
* 생성자는 하나뿐이어야 한다. 한때 테스트용 id 생성기를 받는 두 번째 생성자가 있었고, 그러면
|
||||
* 컴포넌트 스캔이 어느 것을 쓸지 정하지 못해 기본 생성자를 찾다가 실패한다 — 컴파일도 테스트도
|
||||
* 통과하고 운영에서 기동만 못 한다. id 는 여기서 만들면 되고, 통합 테스트는 그 값을 볼 필요가 없다.
|
||||
*/
|
||||
public JdbcProjectActivityRepositoryAdapter(JdbcClient jdbcClient) {
|
||||
this(jdbcClient, UUID::randomUUID);
|
||||
}
|
||||
|
||||
JdbcProjectActivityRepositoryAdapter(JdbcClient jdbcClient, Supplier<UUID> 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,"
|
||||
|
||||
+63
@@ -2158,4 +2158,67 @@ class CleanArchitectureTest {
|
||||
.filter(value -> value instanceof Number number && number.intValue() == SqlTypes.UUID)
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring 이 생성자를 고를 수 있어야 한다.
|
||||
*
|
||||
* <p>이 규칙은 사고 하나에서 나왔다. 한 어댑터가 생성자를 둘 갖고 있었고 — 하나는 운영용, 하나는 테스트가 id 생성기를 넣기 위한 것 — 둘 중 어느 것에도
|
||||
* {@code @Autowired} 가 없었다. 컴포넌트 스캔은 고르지 못하고 기본 생성자를 찾다가 실패한다. 컴파일도, 단위 테스트도, 실제 PostgreSQL 위에서
|
||||
* 도는 통합 테스트도 전부 통과했다. 그 어느 것도 컨텍스트를 띄우지 않기 때문이다. 운영에서 기동만 못 했다.
|
||||
*
|
||||
* <p>그래서 여기서 막는다: 스캔되는 스프링 컴포넌트는 생성자가 하나이거나, 여럿이면 그중 하나에 {@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<JavaClass> 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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user