Introduce Assertion
GOAL
코드가 암묵적으로 가정하는 조건을 assertion으로 명시하여:
- 가정 위반 시 즉시 발견 (원인과 증상의 거리 단축)
- 주석보다 강력한 실행 가능한 문서
- 계약 기반 프로그래밍의 경량 버전
CONSTRAINTS
- 계열: Tidy — 후보 보고 후 승인 없이 적용 (
../../references/refactoring-procedure.md §0·§3-A)
- 동작 변경 금지: assertion 추가만 수행 (기존 로직 변경 없음)
- 테스트 수정 금지: assertion 추가가 테스트를 실패시키면 되돌리기
- 명시적 git add:
git add -A 금지, 변경된 파일만 명시
Assertion 도구 선택 (프로젝트 의존성 자동 감지)
프로젝트의 빌드 파일(build.gradle 또는 pom.xml)을 확인하여 자동 선택:
- Spring 의존성 있음 →
org.springframework.util.Assert
- Apache Commons 있음 →
org.apache.commons.lang3.Validate
- 둘 다 없음 →
java.util.Objects.requireNonNull + IllegalArgumentException
Java assert 키워드는 -ea 플래그가 필요하여 프로덕션에서 비활성화될 수 있으므로 사용하지 않음
적용 패턴
Before: 암묵적 가정 (Spring Assert)
public double calculateDiscount(double price, double rate) {
// price는 양수, rate는 0~1 사이여야 함 (주석 또는 아무것도 없음)
return price * rate;
}
After: assertion으로 가정 명시
import org.springframework.util.Assert;
public double calculateDiscount(double price, double rate) {
Assert.isTrue(price > 0, "price must be positive: " + price);
Assert.isTrue(rate >= 0 && rate <= 1, "rate must be between 0 and 1: " + rate);
return price * rate;
}
추가 예시: Apache Commons Validate
import org.apache.commons.lang3.Validate;
public String formatName(Customer customer) {
Validate.notNull(customer, "customer must not be null");
Validate.notBlank(customer.getFirstName(), "firstName must not be blank");
return customer.getFirstName() + " " + customer.getLastName();
}
추가 예시: 사후 조건 (결과 검증)
public int allocateSlots(int requested, int available) {
int allocated = Math.min(requested, available);
Assert.isTrue(allocated >= 0, "allocated slots must not be negative: " + allocated);
Assert.isTrue(allocated <= available, "allocated exceeds available: " + allocated + " > " + available);
return allocated;
}
적용 기준
적용 대상
- 메서드가 특정 조건을 가정하지만 명시하지 않은 경우
- 내부 메서드(private/package-private)의 전제 조건
- 계산 결과의 사후 조건 (결과값 범위 검증)
- 알고리즘의 불변식 (invariant)
- null이 아닌 것을 암묵적으로 가정하는 경우
적용 제외
- public API의 입력 검증: assertion이 아니라 명시적 예외(IllegalArgumentException 등)를 사용해야 함
- 비즈니스 규칙 검증: 도메인 로직으로 처리해야 할 것
- 이미 Guard Clause나 예외로 처리된 조건: 중복
- 외부 입력(사용자, API 응답): 시스템 경계는 명시적 검증 필요
주의사항
- assertion 실패 = 프로그래머의 버그 (예상치 못한 상황)
- 예외(Exception) = 예상 가능한 오류 상황 (사용자 입력 오류 등)
- 이 구분이 모호하면 사용자에게 질문
OUTPUT FORMAT
실행 절차
공통 골격(대상 파일 수집 → 후보 제시(계열별 승인 규칙) → 적용 → 테스트 → 커밋/되돌리기, 브랜치·PR이
필요한 조건)은 이 스킬 디렉터리 기준 ../../references/refactoring-procedure.md가 정본이다.
아래는 이 기법에 고유한 부분만 규정한다.
사전 확인: 프로젝트 의존성 (공통 절차 1단계 앞)
# Gradle 프로젝트
grep -l "spring" build.gradle 2>/dev/null || grep -l "commons-lang3" build.gradle 2>/dev/null
# Maven 프로젝트
grep -l "spring" pom.xml 2>/dev/null || grep -l "commons-lang3" pom.xml 2>/dev/null
결과에 따라 assertion 도구를 선택하고 사용자에게 안내:
프로젝트에서 Spring 의존성이 감지되었습니다.
org.springframework.util.Assert를 사용합니다.
후보 식별 (공통 절차 2단계)
- 암묵적 가정 패턴 탐지:
- null 참조 없이 메서드 호출하는 경우
- 범위 가정 (양수, 0~1, 비어있지 않음 등)
- 상태 가정 (초기화 완료, 특정 상태 등)
- 각 후보에 대해:
- 파일명 및 라인 번호
- 가정 내용 설명
- 추가할 assertion 코드
후보 제시 예시 (공통 절차 3단계)
발견된 후보 3개 (Spring Assert 사용):
1. PricingService.java:20
가정: price > 0, rate는 0~1
→ Assert.isTrue(price > 0, ...)
→ Assert.isTrue(rate >= 0 && rate <= 1, ...)
2. OrderProcessor.java:45
가정: order != null, order.getItems() 비어있지 않음
→ Assert.notNull(order, ...)
→ Assert.notEmpty(order.getItems(), ...)
→ 승인 없이 적용 (Tidy 계열)
리팩토링 적용 (공통 절차 4단계)
- import 문 추가
- 메서드 시작 부분에 assertion 추가
- (사후 조건인 경우) return 직전에 assertion 추가
커밋 메시지: refactor: introduce assertions in <클래스명> (공통 절차 6단계)
출력 예시
완료: Introduce Assertion (Spring Assert)
변경 내용:
- PricingService.java:20
전제 조건: Assert.isTrue(price > 0), Assert.isTrue(rate >= 0 && rate <= 1)
- OrderProcessor.java:45
전제 조건: Assert.notNull(order), Assert.notEmpty(order.getItems())
테스트: 모든 테스트 통과 (23 tests)
커밋: refactor: introduce assertions in PricingService, OrderProcessor
FAILURE CONDITIONS
공통 실패 조건(계열별 승인 규칙 위반, 테스트 실패 방치, 테스트 수정, 커밋 단위, git add -A, heredoc
한글 메시지)은 ../../references/refactoring-procedure.md에 있다. 아래는 이 기법에 고유한 것만.