Spring Boot 검증 루프 (Verification Loop)
PR 전, 주요 변경 사항 후, 배포 전에 실행하세요.
활성화 시점
- Spring Boot 서비스의 풀 리퀘스트를 열기 전
- 주요 리팩토링 또는 의존성 업그레이드 후
- 스테이징 또는 프로덕션을 위한 배포 전 검증
- 전체 빌드 → 린트 → 테스트 → 보안 스캔 파이프라인 실행
- 테스트 커버리지가 임계값을 충족하는지 검증할 때
1단계: 빌드 (Build)
mvn -T 4 clean verify -DskipTests
# 또는
./gradlew clean assemble -x test
빌드가 실패하면 중단하고 수정하세요.
2단계: 정적 분석 (Static Analysis)
Maven (일반적인 플러그인):
mvn -T 4 spotbugs:check pmd:check checkstyle:check
Gradle (설정된 경우):
./gradlew checkstyleMain pmdMain spotbugsMain
3단계: 테스트 + 커버리지 (Tests + Coverage)
mvn -T 4 test
mvn jacoco:report # 80% 이상 커버리지 확인
# 또는
./gradlew test jacocoTestReport
보고서:
- 전체 테스트 수, 통과/실패
- 커버리지 % (라인/브랜치)
단위 테스트 (Unit Tests)
모의 객체(Mocked Dependencies)로 서비스 로직을 격리하여 테스트:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
@Test
void createUser_validInput_returnsUser() {
var dto = new CreateUserDto("Alice", "[email protected]");
var expected = new User(1L, "Alice", "[email protected]");
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", "[email protected]");
when(userRepository.existsByEmail(dto.email())).thenReturn(true);
assertThatThrownBy(() -> userService.create(dto))
.isInstanceOf(DuplicateEmailException.class);
}
}
Testcontainers를 사용한 통합 테스트 (Integration Tests with 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", "[email protected]"));
var found = userRepository.findByEmail("[email protected]");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
}
MockMvc를 사용한 API 테스트 (API Tests with MockMvc)
전체 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", "[email protected]");
when(userService.create(any())).thenReturn(user);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Alice", "email": "[email protected]"}
"""))
.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단계: 보안 스캔 (Security Scan)
# 의존성 CVE
mvn org.owasp:dependency-check-maven:check
# 또는
./gradlew dependencyCheckAnalyze
# 소스 내 비밀키 검색 (Secrets in source)
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 in git history)
git secrets --scan # 설정된 경우
일반적인 보안 발견 사항 (Common Security Findings)
# System.out.println 확인 (로거를 사용하세요)
grep -rn "System\.out\.print" src/main/ --include="*.java"
# 응답에 원시 예외 메시지가 포함되었는지 확인
grep -rn "e\.getMessage()" src/main/ --include="*.java"
# 와일드카드 CORS 확인
grep -rn "allowedOrigins.*\*" src/main/ --include="*.java"
5단계: 린트/포맷 (Lint/Format) (선택적 게이트)
mvn spotless:apply # Spotless 플러그인을 사용하는 경우
./gradlew spotlessApply
6단계: Diff 리뷰 (Diff Review)
git diff --stat
git diff
체크리스트:
- 디버깅 로그가 남아있지 않은지 (
System.out, 가드 없는log.debug) - 의미 있는 오류 메시지와 HTTP 상태 코드
- 트랜잭션 및 검증이 필요한 곳에 적용되었는지
- 설정 변경 사항이 문서화되었는지
출력 템플릿 (Output Template)
검증 보고서
===================
빌드: [PASS/FAIL]
정적 분석: [PASS/FAIL] (spotbugs/pmd/checkstyle)
테스트: [PASS/FAIL] (X/Y 통과, Z% 커버리지)
보안: [PASS/FAIL] (CVE 발견: N개)
Diff: [X개 파일 변경]
전체: [준비됨 / 준비 안됨]
수정해야 할 이슈:
1. ...
2. ...
지속 모드 (Continuous Mode)
- 큰 변경 사항이 있을 때마다 또는 긴 세션에서 30~60분마다 단계를 재실행
- 빠른 피드백을 위해 짧은 루프 유지:
mvn -T 4 test+ spotbugs
기억하세요: 빠른 피드백이 늦은 놀라움보다 낫습니다. 게이트를 엄격하게 유지하세요—프로덕션 시스템에서는 경고를 결함으로 취급하십시오.