| name | springboot-verification |
| description | Spring Boot 프로젝트를 위한 검증 루프입니다. PR이나 배포 전 빌드, 정적 분석, 커버리지 포함 테스트, 보안 점검, diff 리뷰를 수행합니다. |
| origin | ECC |
Spring Boot 검증 루프
PR 생성 전, 큰 변경 후, 배포 전에 실행합니다.
사용 시점
- Spring Boot 서비스의 pull request를 열기 전
- 대규모 리팩터링이나 의존성 업그레이드 이후
- 스테이징 또는 운영 배포 전 검증
- 전체 build -> lint -> test -> security scan 파이프라인을 돌릴 때
- 테스트 커버리지가 기준을 만족하는지 검증해야 할 때
1단계: 빌드
mvn -T 4 clean verify -DskipTests
./gradlew clean assemble -x test
빌드가 실패하면 중단하고 먼저 수정합니다.
2단계: 정적 분석
Maven(일반 플러그인):
mvn -T 4 spotbugs:check pmd:check checkstyle:check
Gradle(설정된 경우):
./gradlew checkstyleMain pmdMain spotbugsMain
3단계: 테스트 + 커버리지
mvn -T 4 test
mvn jacoco:report
./gradlew test jacocoTestReport
보고 항목:
- 총 테스트 수, 성공/실패 수
- 커버리지 % (line/branch)
단위 테스트
모의 의존성을 사용해 서비스 로직을 격리 테스트합니다.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
@Test
void createUser_validInput_returnsUser() {
var dto = new CreateUserDto("Alice", "alice@example.com");
var expected = new User(1L, "Alice", "alice@example.com");
when(userRepository.save(any(User.class))).thenReturn(expected);
var result = userService.create(dto);
assertThat(result.name()).isEqualTo("Alice");
verify(userRepository).save(any(User.class));
}
@Test
void createUser_duplicateEmail_throwsException() {
var dto = new CreateUserDto("Alice", "existing@example.com");
when(userRepository.existsByEmail(dto.email())).thenReturn(true);
assertThatThrownBy(() -> userService.create(dto))
.isInstanceOf(DuplicateEmailException.class);
}
}
Testcontainers 기반 통합 테스트
H2 대신 실제 데이터베이스를 대상으로 테스트합니다.
@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired private UserRepository userRepository;
@Test
void findByEmail_existingUser_returnsUser() {
userRepository.save(new User("Alice", "alice@example.com"));
var found = userRepository.findByEmail("alice@example.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
}
MockMvc 기반 API 테스트
전체 Spring 컨텍스트에서 컨트롤러 레이어를 테스트합니다.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired private MockMvc mockMvc;
@MockBean private UserService userService;
@Test
void createUser_validInput_returns201() throws Exception {
var user = new UserDto(1L, "Alice", "alice@example.com");
when(userService.create(any())).thenReturn(user);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Alice", "email": "alice@example.com"}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Alice"));
}
@Test
void createUser_invalidEmail_returns400() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Alice", "email": "not-an-email"}
"""))
.andExpect(status().isBadRequest());
}
}
4단계: 보안 점검
mvn org.owasp:dependency-check-maven:check
./gradlew dependencyCheckAnalyze
grep -rn "password\s*=\s*\"" src/ --include="*.java" --include="*.yml" --include="*.properties"
grep -rn "sk-\|api_key\|secret" src/ --include="*.java" --include="*.yml"
git secrets --scan
흔한 보안 점검 항목
grep -rn "System\.out\.print" src/main/ --include="*.java"
grep -rn "e\.getMessage()" src/main/ --include="*.java"
grep -rn "allowedOrigins.*\*" src/main/ --include="*.java"
5단계: 린트/포맷(선택 게이트)
mvn spotless:apply
./gradlew spotlessApply
6단계: Diff 리뷰
git diff --stat
git diff
체크리스트:
- 디버깅 로그가 남아 있지 않은가 (
System.out, 가드 없는 log.debug)
- 오류 메시지와 HTTP 상태가 의미 있게 설정돼 있는가
- 필요한 곳에 트랜잭션과 검증이 들어가 있는가
- 설정 변경이 문서화되어 있는가
출력 템플릿
VERIFICATION REPORT
===================
Build: [PASS/FAIL]
Static: [PASS/FAIL] (spotbugs/pmd/checkstyle)
Tests: [PASS/FAIL] (X/Y passed, Z% coverage)
Security: [PASS/FAIL] (CVE findings: N)
Diff: [X files changed]
Overall: [READY / NOT READY]
Issues to Fix:
1. ...
2. ...
연속 모드
- 큰 변경이 있을 때마다, 또는 장시간 세션에서는 30~60분마다 각 단계를 다시 실행합니다.
- 짧은 피드백 루프를 유지합니다. 예:
mvn -T 4 test + spotbugs
기억할 점: 빠른 피드백이 늦은 놀람보다 낫습니다. 게이트는 엄격하게 유지하고, 운영 시스템에서는 경고도 결함으로 취급합니다.