Skip to content

필사 모드: 속성 기반 테스트 실전 — 예제가 못 잡는 버그를 잡는 법

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — jqwik이 다시 첫 페이지에 오른 날

2026년 상반기, Java용 속성 기반 테스트 라이브러리 jqwik이 Hacker News 첫 페이지에 올랐습니다. 발단은 라이브러리 자체의 새 기능이 아니라, 한 개발자가 올린 경험담이었습니다. AI 코딩 에이전트가 작성한 날짜 처리 코드가 단위 테스트를 모두 통과했는데, jqwik으로 속성 테스트를 돌리자 몇 초 만에 윤년 경계에서 깨지는 입력을 찾아냈다는 내용이었습니다. 댓글란은 "Hypothesis로 똑같은 경험을 했다", "fast-check가 우리 파서의 10년 묵은 버그를 찾았다"는 간증으로 채워졌고, GeekNews에도 번역 요약이 올라와 길게 토론이 이어졌습니다.

타이밍이 절묘했습니다. 2026년은 AI 에이전트가 코드의 상당 부분을 작성하는 시대입니다. Claude Code나 Codex 같은 도구가 수 시간짜리 작업을 자율로 수행하면서, "그 코드가 정말 맞는지 사람이 어떻게 확인하는가"가 업계의 핵심 질문이 되었습니다. 예제 몇 개를 통과하는 코드는 AI가 얼마든지 만들어냅니다. 문제는 예제 사이의 빈 공간입니다. 속성 기반 테스트(Property-Based Testing, PBT)는 정확히 그 빈 공간을 자동으로 탐색하는 기법이고, 그래서 지금 재조명받고 있습니다.

이 글에서는 PBT의 핵심 개념을 정리하고, 속성을 발견하는 패턴 카탈로그를 제시한 뒤, Python(Hypothesis), Java(jqwik), JavaScript(fast-check) 세 언어로 동작하는 예제를 만들어 봅니다. 그리고 실패 재현, 상태 머신 테스트, CI 통합, AI 코드 검증과의 시너지까지 실무 관점에서 다룹니다.

예제 기반 테스트의 한계

우리가 매일 쓰는 테스트는 예제 기반입니다.

def test_add():

assert add(2, 3) == 5

assert add(-1, 1) == 0

assert add(0, 0) == 0

이 방식의 문제는 세 가지입니다.

1. 테스트하는 입력은 작성자가 떠올린 입력뿐입니다. 작성자가 떠올리지 못한 입력(빈 문자열, 유니코드 결합 문자, 정수 오버플로 경계, 윤년 2월 29일)은 영원히 테스트되지 않습니다.

2. 구현자와 테스트 작성자가 같은 사람이면 같은 맹점을 공유합니다. 구현하며 생각하지 못한 케이스는 테스트에서도 생각하지 못합니다.

3. 예제는 "이 입력에서 이 출력"만 말할 뿐, 코드가 지켜야 할 일반 법칙을 표현하지 못합니다.

속성 기반 테스트는 발상을 뒤집습니다. 구체적 예제 대신 "모든 유효한 입력에 대해 성립해야 하는 성질(속성)"을 선언하고, 프레임워크가 수백 개의 무작위 입력을 생성해 그 성질을 깨뜨리는 반례를 찾습니다.

핵심 개념 — 속성, 제너레이터, 슈링킹

PBT의 실행 흐름은 다음과 같습니다.

+-------------+ +--------------+ +-----------+ +--------------+

| 속성 정의 | --> | 제너레이터가 | --> | 속성 검사 | --> | 통과: 반복 |

| "모든 x에 | | 무작위 입력 | | (assert) | | (기본 100회) |

| 대해 P(x)" | | 생성 | | | +--------------+

+-------------+ +--------------+ +-----+-----+

| 실패

v

+---------------------+

| 슈링킹(Shrinking): |

| 실패를 유지하면서 |

| 입력을 최소화 |

+----------+----------+

v

"최소 반례: x = 0" 보고

- 속성(Property): 코드가 지켜야 할 일반 법칙입니다. "정렬 결과의 길이는 입력과 같다", "인코딩 후 디코딩하면 원본이 나온다" 같은 것들입니다.

- 제너레이터(Generator): 무작위 입력을 만드는 부품입니다. 정수, 문자열 같은 기본형부터 "유효한 이메일을 가진 사용자 객체" 같은 복합 구조까지 조합으로 만듭니다. 좋은 프레임워크는 경계값(0, -1, 빈 문자열, NaN, 최대 정수)을 의도적으로 자주 섞습니다.

- 슈링킹(Shrinking): 반례를 찾은 뒤 그것을 사람이 이해할 수 있는 최소 크기로 줄이는 과정입니다. "길이 847의 문자열에서 실패"가 아니라 "빈 문자열에서 실패"로 보고해 주는 것이 슈링킹의 가치이며, 사실 PBT 도구의 품질은 슈링킹 품질이 좌우합니다.

속성을 발견하는 법 — 패턴 카탈로그

PBT 도입의 최대 장벽은 "우리 코드에 무슨 속성이 있는지 모르겠다"입니다. 다행히 속성은 대부분 다음 패턴 중 하나로 발견됩니다.

| 패턴 | 공식 | 적용 예 |

| --- | --- | --- |

| 라운드트립 | decode(encode(x)) == x | 직렬화, 압축, 암호화, 파서-프린터 |

| 불변식 | 연산 후에도 항상 참인 성질 | 정렬 후 길이 불변, 잔액 합계 불변 |

| 멱등성 | f(f(x)) == f(x) | 정규화, 중복 제거, UPSERT |

| 모델 대조 | 단순한 참조 구현과 결과 비교 | 최적화 코드 vs 나이브 코드 |

| 교환/결합 법칙 | f(a, b) == f(b, a) 등 | 병합, 집계, CRDT |

| 사후조건 | 결과가 만족해야 할 조건 | 검색 결과는 모두 질의를 포함 |

| 오라클 비교 | 신뢰할 수 있는 기존 구현과 비교 | 표준 라이브러리, 레거시 시스템 |

| 예외 부재 | 유효 입력에서 절대 크래시하지 않음 | 모든 공개 API의 최소 속성 |

이 중 라운드트립과 불변식만 익혀도 실무 코드의 절반에 적용할 수 있습니다. 그리고 마지막 줄의 "예외 부재"는 가장 과소평가된 속성입니다. "어떤 입력에도 처리되지 않은 예외가 없다"는 속성 하나만으로도 파서와 입력 검증 코드의 버그를 무더기로 찾을 수 있습니다.

실전 1 — Python Hypothesis

금액 계산이라는 실무 단골 소재로 시작합니다. 장바구니 합계에 할인율을 적용하고 센트 단위로 반올림하는 함수를 검증해 봅시다.

cart.py

from decimal import Decimal, ROUND_HALF_UP

def apply_discount(total_cents: int, discount_percent: int) -> int:

"""총액(센트)에 할인율(0-100)을 적용해 센트 단위로 반올림."""

if not 0 <= discount_percent <= 100:

raise ValueError("discount_percent must be between 0 and 100")

if total_cents < 0:

raise ValueError("total_cents must be non-negative")

discounted = Decimal(total_cents) * (Decimal(100 - discount_percent) / Decimal(100))

return int(discounted.quantize(Decimal("1"), rounding=ROUND_HALF_UP))

속성 테스트는 다음과 같습니다.

test_cart.py

from hypothesis import given, settings, strategies as st

from cart import apply_discount

@given(total=st.integers(min_value=0, max_value=10**12),

pct=st.integers(min_value=0, max_value=100))

def test_discount_never_negative_and_never_exceeds_total(total, pct):

result = apply_discount(total, pct)

불변식 1: 할인 결과는 음수가 될 수 없다

assert result >= 0

불변식 2: 할인 결과는 원래 총액을 넘을 수 없다

assert result <= total

@given(total=st.integers(min_value=0, max_value=10**12))

def test_zero_discount_is_identity(total):

사후조건: 0% 할인은 항등 함수다

assert apply_discount(total, 0) == total

@given(total=st.integers(min_value=0, max_value=10**12))

def test_full_discount_is_zero(total):

사후조건: 100% 할인은 항상 0이다

assert apply_discount(total, 100) == 0

@given(total=st.integers(min_value=0, max_value=10**12),

pct=st.integers(min_value=0, max_value=100))

def test_discount_is_monotonic(total, pct):

불변식 3: 할인율이 클수록 결과는 작거나 같다

if pct < 100:

assert apply_discount(total, pct + 1) <= apply_discount(total, pct)

만약 구현을 부동소수점으로 작성했다면(Decimal 대신 float), 단조성 테스트가 큰 금액에서 반올림 오차로 깨지는 반례를 Hypothesis가 찾아냅니다. 그리고 슈링킹 덕분에 "total=10000000001, pct=33에서 실패" 같은 거대한 반례가 아니라 사람이 디버깅할 수 있는 최소 반례로 보고됩니다.

복합 구조 제너레이터도 간단합니다.

유효한 주문 객체를 생성하는 전략

order_strategy = st.builds(

dict,

order_id=st.uuids().map(str),

items=st.lists(

st.builds(dict,

sku=st.text(alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", min_size=8, max_size=8),

qty=st.integers(min_value=1, max_value=999),

unit_price_cents=st.integers(min_value=1, max_value=10**7)),

min_size=1, max_size=50,

),

)

@given(order=order_strategy)

def test_order_total_equals_sum_of_lines(order):

total = calculate_order_total(order)

expected = sum(i["qty"] * i["unit_price_cents"] for i in order["items"])

assert total == expected

실전 2 — Java jqwik

JUnit 5 플랫폼 위에서 동작하는 jqwik은 어노테이션 기반이라 기존 Java 프로젝트에 자연스럽게 녹아듭니다. 라운드트립 패턴으로 직렬화를 검증하는 예제입니다.

class CsvCodecProperties {

@Property

void encodeThenDecodeIsIdentity(@ForAll @StringLength(max = 200) String field) {

// 라운드트립: 어떤 문자열이든 인코딩 후 디코딩하면 원본

String encoded = CsvCodec.encodeField(field);

String decoded = CsvCodec.decodeField(encoded);

assertThat(decoded).isEqualTo(field);

}

@Property

void encodedFieldNeverBreaksRowStructure(

@ForAll @Size(min = 1, max = 10) List<@StringLength(max = 50) String> fields) {

// 불변식: 필드에 쉼표/개행/따옴표가 있어도 행 구조가 유지된다

String row = CsvCodec.encodeRow(fields);

List<String> parsed = CsvCodec.parseRow(row);

assertThat(parsed).isEqualTo(fields);

}

@Property

void normalizeIsIdempotent(@ForAll String input) {

// 멱등성: 정규화를 두 번 해도 한 번과 같다

String once = CsvCodec.normalize(input);

String twice = CsvCodec.normalize(once);

assertThat(twice).isEqualTo(once);

}

@Provide

Arbitrary<String> koreanText() {

// 커스텀 제너레이터: 한글 음절 범위를 포함한 문자열

return Arbitraries.strings()

.withCharRange('가', '힣')

.withCharRange('a', 'z')

.ofMaxLength(100);

}

@Property

void roundTripWithKorean(@ForAll("koreanText") String field) {

assertThat(CsvCodec.decodeField(CsvCodec.encodeField(field))).isEqualTo(field);

}

}

CSV 인코더를 직접 짜 본 분이라면 짐작하시겠지만, 이 라운드트립 속성은 따옴표 안의 따옴표, 필드 끝의 개행, 빈 필드와 null의 구분 같은 고전적 버그를 거의 확실하게 찾아냅니다. 예제 기반 테스트로 이 모든 조합을 나열하는 것은 사실상 불가능합니다.

실전 3 — JavaScript fast-check

fast-check는 Jest/Vitest와 자연스럽게 결합합니다. 모델 대조 패턴으로 최적화된 함수를 나이브 구현과 비교하는 예제입니다.

// 참조 구현: 느리지만 명백히 올바른 버전

function mergeIntervalsNaive(intervals: Array<[number, number]>): Array<[number, number]> {

const points = new Set<number>();

for (const [s, e] of intervals) {

for (let i = s; i < e; i++) points.add(i);

}

// 연속 구간으로 다시 묶기 (작은 범위에서만 사용)

const sorted = [...points].sort((a, b) => a - b);

const out: Array<[number, number]> = [];

for (const p of sorted) {

const last = out[out.length - 1];

if (last && last[1] === p) last[1] = p + 1;

else out.push([p, p + 1]);

}

return out;

}

describe("mergeIntervals", () => {

it("최적화 구현은 나이브 구현과 항상 같은 결과를 낸다", () => {

fc.assert(

fc.property(

fc.array(

fc.tuple(fc.integer({ min: 0, max: 100 }), fc.integer({ min: 0, max: 100 }))

.map(([a, b]) => (a <= b ? [a, b] : [b, a]) as [number, number]),

{ maxLength: 30 }

),

(intervals) => {

const fast = mergeIntervals(intervals);

const slow = mergeIntervalsNaive(intervals);

return JSON.stringify(fast) === JSON.stringify(slow);

}

)

);

});

it("결과 구간은 항상 정렬되어 있고 겹치지 않는다", () => {

fc.assert(

fc.property(

fc.array(fc.tuple(fc.nat(1000), fc.nat(1000)), { maxLength: 50 }),

(raw) => {

const intervals = raw.map(([a, b]) => (a <= b ? [a, b] : [b, a]) as [number, number]);

const merged = mergeIntervals(intervals);

for (let i = 1; i < merged.length; i++) {

// 불변식: 이전 구간의 끝 < 다음 구간의 시작

if (merged[i - 1][1] >= merged[i][0]) return false;

}

return true;

}

)

);

});

});

모델 대조 패턴의 매력은 "정답"을 몰라도 된다는 점입니다. 느리지만 명백히 올바른 구현 하나만 있으면, 최적화 버전이 그것과 동일하게 동작하는지를 수백 가지 입력으로 검증할 수 있습니다. 성능 최적화 PR의 회귀 방지 장치로 특히 강력합니다.

실패 재현 — 시드 고정과 반례 데이터베이스

무작위 테스트의 고전적 걱정은 "어제는 실패했는데 오늘은 통과하면 어쩌지"입니다. 현대 PBT 도구들은 이 문제를 해결해 두었습니다.

Hypothesis: 실패한 반례는 .hypothesis/examples 디렉터리에 자동 저장되어

다음 실행에서 가장 먼저 재시도된다 (반례 데이터베이스)

특정 반례를 영구 회귀 테스트로 못 박을 수도 있다

from hypothesis import example

@given(st.text())

@example("") # 과거에 실패했던 반례를 명시적으로 고정

@example("\x00")

def test_normalize_roundtrip(s):

assert denormalize(normalize(s)) == s

// jqwik: 실패 시 시드가 출력되고, 같은 시드로 재현할 수 있다

@Property(seed = "8723648723648")

void reproducesFailure(@ForAll String input) { /* ... */ }

// jqwik은 또한 실패 반례를 .jqwik-database에 저장해 다음 실행에서 우선 재시도한다

// fast-check: 실패 리포트에 seed와 path가 출력된다

fc.assert(prop, { seed: 1042, path: "0:0:1" }); // 정확히 그 반례부터 재실행

CI에서 권장하는 운영 방식은 이렇습니다. 실패 로그에 찍힌 시드/반례를 그대로 example 또는 seed로 코드에 박아 회귀 테스트로 승격시키는 것입니다. 이렇게 하면 무작위성이 "재현 불가능한 flaky"가 아니라 "영구적인 회귀 스위트를 자동으로 채굴하는 장치"가 됩니다.

상태 머신 테스트 — stateful testing

지금까지는 순수 함수를 테스트했지만, 실무의 어려운 버그는 상태가 있는 코드(캐시, 커넥션 풀, 장바구니, DB 레이어)에 삽니다. 상태 머신 테스트는 "무작위 연산 시퀀스"를 생성해, 실제 구현과 단순 모델을 나란히 실행하며 매 단계 일치를 검증합니다.

LRU 캐시를 dict 모델과 대조하는 상태 머신 테스트 (Hypothesis)

from hypothesis import strategies as st

from hypothesis.stateful import RuleBasedStateMachine, rule, invariant

from lru import LRUCache

CAPACITY = 8

class LRUCacheMachine(RuleBasedStateMachine):

def __init__(self):

super().__init__()

self.real = LRUCache(capacity=CAPACITY)

self.model = {} # 모델: 순서를 기억하는 dict (파이썬 dict는 삽입 순서 유지)

@rule(key=st.integers(0, 20), value=st.integers())

def put(self, key, value):

self.real.put(key, value)

self.model.pop(key, None)

self.model[key] = value

if len(self.model) > CAPACITY:

oldest = next(iter(self.model))

del self.model[oldest]

@rule(key=st.integers(0, 20))

def get(self, key):

expected = self.model.get(key)

if expected is not None:

모델에서도 최근 사용으로 갱신

self.model.pop(key)

self.model[key] = expected

assert self.real.get(key) == expected

@invariant()

def size_never_exceeds_capacity(self):

assert self.real.size() <= CAPACITY

TestLRUCache = LRUCacheMachine.TestCase

이 테스트가 실패하면 Hypothesis는 "put(3, 1), put(4, 2), get(3), put(5, 9)에서 실패" 같은 최소 연산 시퀀스로 슈링킹해 보고합니다. LRU 갱신 누락, 용량 경계 off-by-one 같은 버그는 이 방식 앞에서 살아남기 어렵습니다. fast-check도 commands API로, jqwik도 action chains로 같은 패턴을 지원합니다.

실무 적용 영역 — 어디에 먼저 쓸 것인가

PBT가 특히 강한 영역을 우선순위로 정리합니다.

| 영역 | 추천 속성 패턴 | 기대 효과 |

| --- | --- | --- |

| 파서/포맷터 | 라운드트립, 예외 부재 | 입력 코너 케이스 대량 발굴 |

| 직렬화/역직렬화 | 라운드트립, 스키마 호환 | 버전 간 호환성 깨짐 조기 발견 |

| 금액/수량 계산 | 불변식, 단조성, 합계 보존 | 반올림/오버플로 버그 차단 |

| 정규화/중복 제거 | 멱등성 | 이중 적용 버그 차단 |

| 자료구조 구현 | 모델 대조, 상태 머신 | 경계 조건 망라 |

| 동시성 코드 | 상태 머신 + 무작위 인터리빙 | 재현 어려운 레이스 탐지 보조 |

| API 입력 검증 | 예외 부재, 사후조건 | 보안 관점의 견고성 향상 |

반대로 PBT가 비효율적인 영역도 있습니다. 외부 시스템과의 통합 자체(실제 결제 게이트웨이 호출 등), 픽셀 단위 UI, "올바름"의 정의가 주관적인 로직(추천 순위 등)에는 무리하게 적용하지 않는 것이 좋습니다.

CI 통합 — 실행 시간과 flaky 관리

PBT를 CI에 넣을 때의 두 가지 걱정, 실행 시간과 flaky를 설정으로 다스립니다.

Hypothesis: 프로파일로 로컬/CI/야간을 분리

from hypothesis import settings, HealthCheck

settings.register_profile("dev", max_examples=50)

settings.register_profile("ci", max_examples=200, deadline=None,

suppress_health_check=[HealthCheck.too_slow])

settings.register_profile("nightly", max_examples=2000)

실행 시: HYPOTHESIS_PROFILE=ci pytest

운영 원칙은 다음과 같습니다.

1. PR 게이트에서는 예제 수를 적당히(100~200), 야간 빌드에서 크게(수천) 돌립니다. 버그 발굴은 야간이, 회귀 방지는 PR이 담당하는 분업입니다.

2. 시간 의존/네트워크 의존을 제너레이터에서 제거합니다. flaky의 주범은 무작위성이 아니라 숨은 비결정성(현재 시각, 외부 호출)입니다.

3. 실패 시 시드와 반례를 로그에 남기도록 리포터를 설정하고, 발견된 반례는 위에서 말한 대로 example로 승격합니다.

4. deadline(케이스당 시간 제한)은 CI 머신 성능 편차로 인한 거짓 실패를 만들기 쉬우므로 CI에서는 끄거나 넉넉하게 잡습니다.

AI 코드 검증과의 시너지 — 2026년의 관점

이번 재조명의 핵심 동력을 따로 짚을 가치가 있습니다. AI가 작성한 코드와 PBT는 구조적으로 궁합이 좋습니다.

1. AI 코드의 실패 양상은 "그럴듯한 90% + 미묘하게 틀린 10%"입니다. 시연용 예제는 통과하지만 경계 조건에서 틀리는 패턴이 전형적인데, 이것이 정확히 PBT가 잡도록 설계된 버그 클래스입니다.

2. 속성은 사양이고, 사양은 사람이 쥐고 있어야 합니다. 구현은 에이전트에게 맡기더라도 "라운드트립이 성립해야 한다", "합계가 보존되어야 한다"는 속성 정의는 사람이 작성하면, 리뷰의 초점이 코드 한 줄 한 줄에서 사양 검증으로 올라갑니다.

3. 에이전트 루프의 자동 채점기가 됩니다. 에이전트에게 "이 속성 테스트를 통과할 때까지 수정하라"고 지시하면, 속성 테스트가 사람 대신 반례를 들이밀며 루프를 돌립니다. 예제 테스트보다 과적합(테스트에만 맞춘 하드코딩)이 훨씬 어렵다는 점이 중요합니다. 무작위 입력에는 하드코딩으로 대응할 수 없기 때문입니다.

단, AI에게 속성 작성까지 통째로 맡기는 것은 주의해야 합니다. 구현과 같은 오해를 공유한 속성은 같이 틀린 채로 통과합니다(자기 일치 함정). 속성의 출처는 요구사항과 도메인 지식이어야 하며, 그것이 사람이 기여하는 부분입니다.

도입 가이드 — 기존 테스트에 점진적으로

빅뱅 도입은 필요 없습니다. 다음 순서를 권합니다.

1. 1주차: 기존 코드에서 라운드트립이 성립하는 함수 쌍(직렬화, 인코딩)을 하나 골라 속성 테스트 1개를 추가합니다. 도구 설치와 CI 연동까지 이 한 개로 검증합니다.

2. 2주차: "예외 부재" 속성을 공개 API 두세 곳에 추가합니다. 의외의 크래시를 찾으면 팀 설득 자료가 됩니다.

3. 3주차: 버그가 자주 나는 모듈 하나에 불변식 속성을 설계합니다. 이때 팀과 함께 "이 모듈이 지켜야 할 법칙이 뭐지?"를 토론하는 것 자체가 설계 리뷰 효과를 냅니다.

4. 그 이후: 새 버그가 보고될 때마다 "이 버그를 잡았을 속성은 무엇이었나"를 회고에 추가합니다. 속성 테스트는 버그 사후 분석에서 가장 빨리 늘어납니다.

함정 — 이렇게 쓰면 실패한다

- 과도한 제너레이터 복잡도: 유효 입력을 만들기 위해 제너레이터에 비즈니스 로직을 복제하기 시작하면 위험 신호입니다. 제너레이터가 구현만큼 복잡해지면 제너레이터의 버그를 테스트하는 꼴이 됩니다. 이때는 생성 후 필터링보다 "단순한 입력을 생성해 공개 API로 유효 상태를 만들기"가 정석입니다.

- 동어반복 속성: 구현 코드를 속성에 그대로 복사하면(assert f(x) == 구현과 같은 수식) 아무것도 검증하지 않습니다. 속성은 구현과 다른 각도(라운드트립, 모델 대조)에서 와야 합니다.

- filter 남용: 생성된 입력의 99%를 버리는 필터는 테스트를 느리게 하고 입력 분포를 왜곡합니다. 필터 대신 구성적으로 생성하십시오(짝수가 필요하면 정수를 생성해 2를 곱하기).

- 슈링킹을 무시한 커스텀 생성: map 기반 변환은 슈링킹이 따라오지만, 외부 난수로 직접 만든 값은 슈링킹이 되지 않아 거대한 반례를 받게 됩니다. 프레임워크의 조합자 안에서 만드는 것이 원칙입니다.

- 100% 대체 환상: PBT는 예제 테스트의 대체재가 아니라 보완재입니다. 의도를 문서화하는 대표 예제와, 공간을 탐색하는 속성 테스트는 역할이 다릅니다.

체크리스트

- [ ] 라운드트립이 성립하는 함수 쌍에 라운드트립 속성이 있는가

- [ ] 공개 API에 "유효 입력에서 예외 부재" 속성이 있는가

- [ ] 금액/수량 코드에 불변식(음수 금지, 합계 보존, 단조성)이 정의되어 있는가

- [ ] 발견된 반례가 example/seed로 영구 회귀 테스트로 승격되는 절차가 있는가

- [ ] CI 프로파일(PR 빠르게, 야간 깊게)이 분리되어 있는가

- [ ] 제너레이터가 구현보다 단순한가 (복잡해지면 설계 재검토)

- [ ] 상태가 있는 핵심 컴포넌트에 상태 머신 테스트가 있는가

- [ ] 속성 정의의 출처가 요구사항/도메인 지식인가 (구현 복사가 아니라)

- [ ] AI가 작성한 코드의 머지 조건에 속성 테스트 통과가 포함되는가

마치며

속성 기반 테스트는 1999년 QuickCheck에서 시작된 오래된 기법이지만, 2026년의 우리에게 새로운 이유로 필요해졌습니다. 사람이 코드를 다 쓰던 시절에는 "내가 생각 못 한 입력"이 문제였다면, AI가 코드를 쓰는 시대에는 "아무도 깊이 생각하지 않은 구현"이 양산되는 것이 문제이기 때문입니다. 예제는 그 구현이 그럴듯하다는 것만 확인해 주지만, 속성은 그 구현이 법칙을 지킨다는 것을 확인해 줍니다.

거창하게 시작할 필요 없습니다. 여러분 코드베이스에서 인코딩-디코딩 쌍 하나를 찾아 라운드트립 속성 하나를 추가해 보십시오. 높은 확률로, 그 테스트는 첫 주에 여러분이 몰랐던 버그 하나를 선물할 것입니다.

참고 자료

- jqwik 공식 사이트: https://jqwik.net/

- jqwik 사용자 가이드: https://jqwik.net/docs/current/user-guide.html

- Hypothesis 공식 문서: https://hypothesis.readthedocs.io/

- Hypothesis — What is property-based testing?: https://hypothesis.works/articles/what-is-property-based-testing/

- fast-check GitHub: https://github.com/dubzzz/fast-check

- fast-check 공식 문서: https://fast-check.dev/

- QuickCheck 원논문 (Claessen & Hughes, 2000): https://dl.acm.org/doi/10.1145/351240.351266

- Hypothesis GitHub: https://github.com/HypothesisWorks/hypothesis

- jqwik GitHub: https://github.com/jqwik-team/jqwik

- Hacker News (PBT 관련 토론 다수): https://news.ycombinator.com/

- GeekNews: https://news.hada.io/

현재 단락 (1/284)

2026년 상반기, Java용 속성 기반 테스트 라이브러리 jqwik이 Hacker News 첫 페이지에 올랐습니다. 발단은 라이브러리 자체의 새 기능이 아니라, 한 개발자가 올린...

작성 글자: 0원문 글자: 12,260작성 단락: 0/284