Spring Boot 검증 루프은(는) 무엇을 하나요?
PR 전, 주요 변경 후, 배포 전에 실행하세요.
활성화 시점
- Spring Boot 서비스의 풀 리퀘스트를 열기 전
- 주요 리팩토링 또는 의존성 업그레이드 후
- 스테이징 또는 운영 환경 배포 전 검증
- 전체 빌드 → 린트 → 테스트 → 보안 스캔 파이프라인 실행
- 테스트 커버리지가 임계값을 충족하는지 확인
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 # 80%+ 커버리지 확인
# 또는
./gradlew test jacocoTestReport
보고:
- 전체 테스트, 통과/실패
- 커버리지 % (라인/브랜치)
단위 테스트
Mockito로 의존성을 모킹하여 서비스 로직을 격리 테스트합니다:
@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를 사용한 통합 테스트
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 테스트
전체 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단계: 보안 스캔
# 의존성 CVE
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 히스토리)
git secrets --scan # 설정된 경우
일반적인 보안 발견 사항
# 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단계: 린트/포맷 (선택적 게이트)
mvn spotless:apply # Spotless 플러그인 사용 시
./gradlew spotlessApply
6단계: 변경 사항 리뷰
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 통과, Z% 커버리지)
Security: [PASS/FAIL] (CVE 발견: N)
Diff: [X개 파일 변경]
Overall: [READY / NOT READY]
수정해야 할 이슈:
1. ...
2. ...
연속 모드
- 중요 변경 사항이 있을 때 또는 긴 세션에서 30~60분마다 단계 재실행
- 빠른 피드백을 위해 짧은 루프 유지:
mvn -T 4 test+ spotbugs
기억하세요: 빠른 피드백이 늦은 놀라움보다 낫습니다. 게이트를 엄격하게 유지하고, 프로덕션 시스템에서는 경고를 결함으로 처리하세요.