컴파일되는 예제는 믿어도 될까? — 블로그 코드 검증기

anlee·2026년 9월 20일
post-thumbnail

코드 원고와 테스트 체크리스트를 돋보기로 대조하는 설명용 일러스트

코드와 검증 기록을 대조하는 장면을 표현한 AI 생성 일러스트입니다. 실제 테스트 화면이 아닙니다.

블로그에 적힌 코드는 짧고 매끄럽게 읽혔다. 그런데 검증 프로젝트로 옮기면 다른 질문이 생겼다.

“생략한 클래스는 어디에 있지?”
“정상 요청 말고 잘못된 요청에도 설명한 대로 동작할까?”
“테스트가 통과한 코드와 실제로 게시된 코드가 같을까?”

9월 13~15일에 기존 기술 글을 정리하면서, 예제의 문법뿐 아니라 어떤 조건을 확인했고 무엇은 아직 확인하지 않았는지를 함께 기록했다. 이번 글은 API 응답 형식을 다시 설명하는 튜토리얼이 아니다. 예제를 검증 가능한 기록으로 바꾸며 정리한 기준이다.

기록의 범위

아래 사례는 당시 수정한 원고, 테스트 소스와 저장된 결과 보고서를 바탕으로 정리했다. 모든 기존 글의 코드를 검증했다는 뜻은 아니다. API 예제의 테스트 결과는 2026년 9월 13일 저장된 보고서이며, 이 글을 작성하면서 전체 테스트를 새로 실행한 결과는 아니다.

먼저 보는 요약: 검증에는 서로 다른 네 층이 있다

확인할 질문필요한 근거그것만으로 알 수 없는 것
코드가 실행될 준비가 됐는가?버전·의존성·타입을 갖춘 컴파일업무 규칙이 맞는지
허용하지 않는 입력을 거부하는가?정상값·경계값 테스트실제 DB의 제약과 동시성
HTTP 응답이 계약을 지키는가?상태·헤더·본문 검증실제 네트워크와 전체 보안 설정
독자가 같은 코드를 읽는가?원고와 저장된 본문 대조실행 환경의 모든 차이

컴파일, 테스트, 게시 확인은 같은 검사를 반복하는 일이 아니었다. 각각 다른 종류의 빈틈을 찾는 과정이었다.

1. 실행 가능한 예제와 설명용 발췌를 구분한다

기존 API 응답 글에는 DTO의 생성자만 보여 주고 // getters and setters로 끝낸 부분과, 주변 구현을 생략한 서비스 호출이 있었다. 설명용 발췌라면 생략할 수 있다. 다만 독자가 복사해서 실행할 수 있는 예제로 제시하려면 필요한 구현과 의존성이 함께 있어야 한다.

그래서 검증할 때는 실제 DTO·서비스·컨트롤러·예외 처리기를 구성했다. 원고에는 생략한 부분과 적용 조건을 구분해 적었다.

당시 Java 예제 검증 프로젝트의 기준은 Java 17과 Spring Boot 3.3.2 의존성 BOM이었다. 이는 당시의 재현 조건이며, 지금 새 프로젝트에 그 버전을 권장한다는 뜻은 아니다. 코드를 옮긴 환경의 버전을 기록해야 나중에 실패가 생겼을 때 API 변화인지 예제의 누락인지 구분할 수 있다.

작은 코드라도 다음 세 가지는 구별해 두는 편이 좋았다.

  • 독립 예제: 필요한 import와 구현, 실행 방법을 함께 제공한다.
  • 기존 코드의 발췌: 어떤 클래스·설정을 전제로 하는지 적는다.
  • 의사 코드: 책임이나 순서를 보여 주는 설명이며, 그대로 실행할 수 없음을 밝힌다.

2. 계산이 된다는 것과 입력 규칙이 맞다는 것은 다르다

금액 계산에서는 BigDecimal.multiply()에 정수 수량을 그대로 넘길 수 없다. 수량을 BigDecimal로 변환하는 것은 타입 문제를 해결한다. 하지만 수량이 0이거나 음수일 때의 정책까지 정해 주지는 않는다.

아래는 당시 검증에 사용한 작은 테스트 클래스다. Java 17과 JUnit Jupiter 의존성을 갖춘 프로젝트에서 src/test/java/OrderAmountTest.java에 놓는 코드다.

import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class OrderAmountTest {
    static BigDecimal total(BigDecimal price, int quantity) {
        if (price == null
                || price.signum() < 0
                || quantity <= 0) {
            throw new IllegalArgumentException(
                "Invalid order input");
        }
        return price.multiply(
            BigDecimal.valueOf(quantity));
    }

    @Test
    void calculatesAmount() {
        BigDecimal result = total(
            new BigDecimal("12.50"), 3);
        assertEquals(0,
            new BigDecimal("37.50").compareTo(result));
    }

    @Test
    void rejectsInvalidQuantity() {
        assertThrows(IllegalArgumentException.class,
            () -> total(BigDecimal.TEN, 0));
        assertThrows(IllegalArgumentException.class,
            () -> total(BigDecimal.TEN, -1));
    }

    @Test
    void rejectsInvalidPrice() {
        assertThrows(IllegalArgumentException.class,
            () -> total(null, 1));
        assertThrows(IllegalArgumentException.class,
            () -> total(new BigDecimal("-1"), 1));
    }
}

여기서는 가격은 0 이상, 수량은 양수라는 예제의 규칙을 선택했다. 환불·부채처럼 음수가 유효한 업무에 그대로 적용할 보편 규칙은 아니다.

결과 비교에도 기준이 있다. BigDecimal.equals()는 값과 스케일을 함께 비교하고, compareTo()는 수치의 대소를 비교한다. 이 테스트의 목적은 금액의 수치 확인이어서 compareTo() == 0을 사용했다. 소수 둘째 자리까지의 표현 자체가 계약이라면 스케일이나 직렬화 결과도 별도로 검사해야 한다. Java 17 BigDecimal 문서

이 작은 테스트는 주문 금액 계산 함수를 확인한다. 재고 차감, 주문 저장, 반올림 정책, 결제와 환불까지 검증하는 주문 시스템 테스트는 아니다. 입력 가격 0은 구현상 허용하지만, 위 테스트에는 그 경계를 직접 확인하는 사례가 빠져 있다는 점도 구분할 수 있다.

3. JSON만 맞으면 HTTP 응답도 맞을까?

공통 응답을 도입하면 본문의 모양에 눈이 가기 쉽다.

{
  "status": "FAILURE",
  "message": "Request could not be processed",
  "data": null
}

하지만 같은 JSON이라도 HTTP 상태를 모두 500으로 바꾸거나 필요한 헤더를 버리면 계약은 달라진다.

예를 들어 GET만 제공하는 경로에 POST를 보냈다면, 해당 예제에서는 405 Method Not Allowed가 나와야 한다. 405 응답에는 지원하는 메서드를 알리는 Allow 헤더가 필요하다. 이는 자체 JSON 형식의 취향이 아니라 HTTP의 요구 사항이다. RFC 9110 §15.5.6

검증 프로젝트에서는 컨트롤러와 예외 처리기를 직접 등록하고, 응답의 상태·헤더·본문을 같이 확인했다. 다음은 테스트 클래스의 관련 메서드 발췌다. DemoUserService·UserController·ApiExceptionHandler는 기존 API 공통 응답 글의 구현을 전제로 한다. 아래 코드만으로 완결된 애플리케이션은 아니다.

ObjectMapper mapper = new ObjectMapper();

MockMvc mvc(DemoUserService service) {
    return MockMvcBuilders
        .standaloneSetup(new UserController(service))
        .setControllerAdvice(new ApiExceptionHandler())
        .build();
}

@Test
void methodNotAllowedKeepsHeader() throws Exception {
    var response = mvc(new DemoUserService())
        .perform(post("/api/v1/users/1"))
        .andExpect(status().isMethodNotAllowed())
        .andReturn().getResponse();

    assertTrue(response.getHeader("Allow")
        .contains("GET"));
    assertEquals("FAILURE",
        mapper.readTree(response.getContentAsString())
            .get("status").asText());
}

본문을 공통 형식으로 바꾸더라도 프레임워크가 정한 상태와 헤더를 보존하는지가 핵심이다. 또 이 테스트는 Allow에 GET이 포함됐는지만 확인한다. 허용 메서드 전체 집합이 정확히 같은지까지 단언한 테스트는 아니다.

당시 API 예제에서 확인한 사례는 다음과 같다.

요청 또는 상황테스트의 주요 확인 항목
존재하는 사용자 조회200, SUCCESS, 사용자 ID와 이름
없는 사용자 조회404, FAILURE
ID가 0인 요청400, FAILURE
숫자 대신 문자열 ID400, FAILURE
지원하지 않는 POST 요청405, Allow에 GET 포함, FAILURE
서비스에서 예상 밖 예외 발생500, 안전한 오류 메시지, 내부 예외 문자열 비노출

저장된 ApiResponseArticleTest 결과는 6개 실행, 실패 0, 오류 0, 건너뜀 0이었다. 보고서의 시각은 2026년 9월 13일 22:12 KST다. “오류가 없다”보다 “이 여섯 조건에서 이 단언을 통과했다”가 결과를 더 정확하게 설명한다.

4. MockMvc 통과는 어디까지의 근거인가

MockMvc는 실제 서버를 띄우지 않고 Spring MVC의 요청 처리를 시험한다. 컨트롤러 메서드를 직접 호출하는 것과 달리 매핑·타입 변환·메시지 변환·예외 처리 등의 경로를 확인할 수 있다. Spring MockMvc 개요

다만 위에서 선택한 standaloneSetup은 컨트롤러와 필요한 구성을 직접 등록하는 방식이다. 실제 애플리케이션의 Spring 설정 전체를 불러온 것이 아니다. Spring 문서도 standalone 테스트 외에 실제 MVC 설정을 확인하는 통합 테스트가 필요함을 설명한다. MockMvc 설정 방식

따라서 위의 6개 테스트 결과로 다음까지 완료했다고 말할 수는 없다.

  • 실제 DB 조회, 트랜잭션과 락의 동작
  • 등록하지 않은 Spring Security 필터 체인의 인증·인가
  • 프록시·TLS·실제 네트워크를 거치는 요청
  • 운영 설정 차이, 부하와 동시 요청의 영향

이 사례에서 standalone 방식을 선택한 이유는 작게 구성한 API 예제의 상태·헤더·본문을 집중해서 확인하기 위해서다. 실제 애플리케이션에 붙일 때는 그 환경의 설정과 외부 의존성을 포함한 검증을 추가해야 한다.

5. 마지막 검증 대상은 게시된 글이다

테스트가 통과해도 원고를 복사하는 과정에서 코드가 빠질 수 있다. 수정 전 코드와 수정 후 코드가 함께 남거나, 긴 본문의 뒷부분이 저장되지 않으면 독자가 읽는 내용은 검증한 예제와 달라진다.

이번 정리에서는 원문과 수정본을 따로 보존하고, 저장된 본문에서 제목·코드 블록·소제목·태그·URL 등을 확인했다. 편집 중 본문이 중복된 글은 복원한 뒤 최종 본문과 소제목의 중복 여부를 다시 대조했다.

이 과정은 다음처럼 연결된다.

원문 보존
   ↓
실행 조건과 생략 범위 명시
   ↓
검증 프로젝트의 예제 + 실패·경계값 테스트
   ↓
결과 보고서와 확인하지 않은 범위 기록
   ↓
수정 원고 ↔ 실제로 저장된 본문 대조

텍스트 대조와 화면 확인도 다르다. DOM에서 이미지 주소가 있다는 사실만으로 그림의 글자가 읽히거나 모바일 표가 편하게 보인다고 단정할 수는 없다. 스크린샷이나 실제 화면으로 보지 못한 부분은 시각적 검수까지 끝났다고 쓰지 않는 편이 맞다.

다음 예제를 올리기 전에 확인할 것

  • 실행 예제·발췌·의사 코드 중 무엇인지 밝혔는가?
  • 버전과 필요한 구현·의존성을 찾을 수 있는가?
  • 정상값뿐 아니라 거부할 입력과 허용할 경계를 확인했는가?
  • HTTP 예제라면 상태·헤더·본문을 함께 검사했는가?
  • 결과의 날짜와 검증하지 않은 범위를 적었는가?
  • 저장된 글의 코드·소제목·이미지가 원고와 맞는가?

마치며

예제를 검증하면서 바뀐 것은 “테스트 완료”라는 표시보다 문장의 범위였다.

계산 함수 테스트로 주문 전체가 안전하다고 말하지 않기. MockMvc 테스트로 운영 설정까지 확인했다고 말하지 않기. 원고를 수정했다는 이유로 게시된 내용도 같다고 가정하지 않기.

좋은 예제는 짧게 읽히는 코드이면서, 어디까지 믿어도 되는지 알 수 있는 코드여야 한다. 다음 글에서는 이번에 남겨 둔 경계 중 하나인 “실제 DB를 붙였을 때 테스트의 결론이 어떻게 달라지는가”를 작은 사례로 다뤄 보려 한다.

0개의 댓글