테스트와 품질¶
🎯 이 장에서 배우는 것¶
- 테스트 피라미드(단위→통합→E2E)
- Spring Boot 테스트 전략과 계산기 테스트 실습
- curl로 API 수동 검증
단계: 3단계 — 고급·품질 · 앞 장: java-study-ch08 · 다음 장: java-study-ch10
따라 하는 법: 위에서 아래로 읽으며 코드를 직접 쳐본다. 9.2 계산기 테스트를 직접 작성하고, 9.3에서 curl로 API를 찔러본다. 방법론: entity-tdd.
실습 프로젝트는 두 개: 9.2 계산기는 ch01 1.2에서 만든 순수 Java 프로젝트
hello-java에, 9.1·9.3의 Spring 실습(✏️)은 ch06 6.1에서 start.spring.io로 만든demo프로젝트에 작성한다. 본문에 나오는 필자의 실무 저장소(도서 대출 API) 코드는 읽기 자료일 뿐 실습에 필요하지 않다.
9.0 테스트와 품질¶
🎯 목표: 테스트 피라미드와 품질 전략의 큰 그림을 잡는다.
개요¶
이 문서는 책의 테스트와 품질 챕터를 안내하는 문서입니다. 테스트는 기능이 다 만들어진 뒤 붙이는 보너스 작업이 아니라, 변경을 두려워하지 않게 만드는 구조적 장치입니다.
왜 중요한가¶
Java와 Spring 실무에서는 기능 구현보다 유지보수가 더 오래 지속됩니다. 이때 테스트가 없으면 리팩터링, 버그 수정, 의존성 교체가 모두 위험한 작업이 됩니다.
필자의 실무 저장소도 이미 테스트를 여러 층으로 나눠 사용하고 있습니다. 도메인 모델 테스트, 서비스 단위 테스트, @WebMvcTest 기반 컨트롤러 테스트, @DataJpaTest 기반 리포지토리 테스트, @SpringBootTest 통합 테스트가 함께 존재합니다. 이 장은 그 구조를 읽는 법을 익히는 데 목적이 있습니다.
이 챕터에서 다루는 범위¶
- Spring Boot 테스트 도구 구성
- 테스트 피라미드와 레이어별 테스트 선택
- 단위 테스트와 슬라이스 테스트의 역할
- 작은 예제를 통해 보는 테스트 가능한 구조
curl을 활용한 API 수동 검증과 자동화 테스트의 경계
읽는 순서¶
Spring Boot 테스트 전략계산기 테스트 기초API 수동 검증: curl 활용
이 챕터를 읽을 때 체크할 것¶
- 모든 테스트를
@SpringBootTest로 해결하려 하지 않는가 - 테스트 대상이 서비스인지, 웹 계층인지, 저장소인지 먼저 구분하는가
- 수동 확인과 자동화 테스트를 서로 대체재가 아니라 보완재로 보는가
정리¶
좋은 테스트 전략은 테스트가 많아 보이는 구조가 아니라, 어떤 문제를 어떤 레벨에서 검증할지 분명한 구조입니다.
한 줄 정리¶
테스트와 품질 챕터의 핵심은 변경 비용을 줄이는 검증 구조를 만드는 것입니다.
9.1 Spring Boot 테스트 전략¶
🎯 목표: Spring Boot 테스트 전략(단위·슬라이스·통합)을 적용한다.
개요¶
이 문서는 Spring Boot 프로젝트에서 테스트를 어떤 레벨로 나누고, 어떤 도구를 선택해야 하는지 정리한 가이드입니다. 핵심은 테스트를 많이 쓰는 것이 아니라, 가장 적절한 범위로 검증하는 것입니다.
왜 중요한가¶
Spring Boot는 웹, 데이터, 보안, 설정이 함께 묶인 프레임워크라서 테스트도 한 가지 방식으로 해결되지 않습니다. 모든 테스트를 무겁게 만들면 느리고, 모든 테스트를 가볍게 만들면 실제 동작을 놓치게 됩니다.
1. 기본 출발점¶
Spring Boot는 spring-boot-starter-test를 통해 테스트에 필요한 기본 구성을 제공합니다. 초중급 단계에서는 먼저 이 스타터가 어떤 테스트 도구를 묶어 주는지 이해하는 것이 좋습니다. 이 스타터 하나로 이 장에서 계속 만나게 될 세 도구가 함께 들어옵니다 — @Test를 붙인 메서드를 찾아 실행해 주는 테스트 프레임워크 JUnit 5, 진짜 협력 객체 대신 가짜(목, mock) 객체를 만들어 끼워 주는 라이브러리 Mockito, 서버를 띄우지 않고 가짜 HTTP 요청을 보내 컨트롤러를 검증하는 MockMvc(Spring Test 소속)입니다.
2. 테스트를 나누는 기본 기준¶
단위 테스트¶
Spring 컨테이너 없이, 클래스 하나나 협력 객체 몇 개만 검증합니다. 가장 빠르고, 실패 원인을 좁히기 쉽습니다.
슬라이스 테스트¶
웹 계층이나 JPA 계층처럼 특정 레이어만 잘라서 검증합니다. 단위 테스트보다 실제 프레임워크와 더 가깝고, 전체 통합 테스트보다 가볍습니다.
통합 테스트¶
여러 레이어를 함께 붙여 실제 동작을 확인합니다. 설정, 트랜잭션, 직렬화, 보안 필터 같은 경계 지점을 검증할 때 필요합니다.
3. Spring Boot에서 자주 쓰는 테스트 애노테이션¶
@WebMvcTest¶
컨트롤러, JSON 직렬화, 요청/응답 매핑 같은 웹 계층 검증에 적합합니다. 서비스와 리포지토리까지 전부 띄우는 대신, 웹 레이어에 집중합니다.
@DataJpaTest¶
리포지토리와 JPA 매핑, 쿼리 동작을 검증할 때 적합합니다. 영속성 계층만 빠르게 확인하고 싶을 때 유용합니다.
@SpringBootTest¶
애플리케이션 전체를 실제와 가깝게 띄웁니다. 가장 강력하지만 가장 무겁기 때문에, 꼭 필요한 경계 검증에 집중해서 써야 합니다.
4. 실무에서 추천하는 기본 조합¶
- 도메인 로직과 서비스 규칙: 단위 테스트
- 컨트롤러 요청/응답 검증:
@WebMvcTest - JPA 매핑과 조회 검증:
@DataJpaTest - 설정, 보안, 전체 흐름 검증:
@SpringBootTest이 조합이 중요한 이유는, 테스트 실패 원인을 빨리 좁히면서도 실제 동작 검증을 놓치지 않기 때문입니다.
실무 저장소에서 실제로 쓰는 조합¶
필자의 실무 저장소는 추상적인 권장 조합이 아니라 아래 패턴을 실제로 사용합니다.
- 서비스 단위 테스트:
@ExtendWith(MockitoExtension.class)+ 목 객체 (AuthServiceImplTest,LoanServiceImplTest) - 컨트롤러 슬라이스 테스트:
@WebMvcTest+@MockitoBean+MockMvc(LoanControllerTest,BookControllerTest) - JPA 슬라이스 테스트:
@DataJpaTest+TestEntityManager(LoanRepositoryTest,BookRepositoryTest) - 통합 테스트:
@SpringBootTest(OrderServiceIntegrationTest,AopLoggingIntegrationTest)
아래 세 블록은 참고 코드(실무 저장소 발췌 스텁)입니다. 조합을 읽는 용도이며, 이 장의 실습 대상이 아닙니다. 직접 작성하는 실습은 9.2에서 진행합니다.
@ExtendWith(MockitoExtension.class)
class AuthServiceImplTest {
@Mock private AuthenticationManager authenticationManager;
@Mock private JwtTokenProvider jwtTokenProvider;
@InjectMocks private AuthServiceImpl authService;
}
@WebMvcTest(LoanController.class)
@AutoConfigureMockMvc(addFilters = false)
class LoanControllerTest {
@MockitoBean private LoanService loanService;
@Autowired private MockMvc mockMvc;
}
@DataJpaTest
class LoanRepositoryTest {
@Autowired private TestEntityManager entityManager;
@Autowired private LoanRepository loanRepository;
}
예상 결과
서비스 테스트는 빠르게 유스케이스 오케스트레이션을 검증한다.
컨트롤러 테스트는 보안 필터를 끈 상태에서 HTTP 계약을 검증한다.
리포지토리 테스트는 실제 JPA 매핑과 쿼리 동작을 확인한다.
5. 수동 검증은 왜 여전히 필요한가¶
자동화 테스트가 있더라도 curl, Swagger, Postman 같은 수동 검증은 여전히 의미가 있습니다. 특히 인증 헤더, 실제 JSON 바디, 운영과 유사한 요청 흐름은 수동 점검이 빠를 때가 많습니다. 다만 수동 검증은 회귀 방지를 대신하지 못하므로, 자동화 테스트와 역할을 분리해야 합니다.
6. 자주 하는 실수¶
- 모든 테스트를
@SpringBootTest로만 작성하는 것 - 컨트롤러 테스트에서 서비스 내부 로직까지 다 검증하려는 것
- 테스트 데이터 준비가 너무 복잡해져 본론이 흐려지는 것
- 성공 케이스만 작성하고 실패 케이스를 빼는 것
공식 문서 참고¶
- Spring Boot Testing Reference
- Spring Framework Testing
- Spring Boot Test Auto-configuration Annotations
테스트 실행 방법 (명령)¶
테스트는 IDE의 ▶ 버튼으로도 실행할 수 있지만, 빌드 도구 명령으로 실행해 결과를 직접 확인하는 습관을 들입니다.
./mvnw test # 전체 (Windows: mvnw.cmd)
./mvnw test -Dtest=LoanControllerTest # 특정 클래스만
# 또는 Gradle
./gradlew test # 전체 (Windows: gradlew.bat)
./gradlew test --tests "*LoanControllerTest" # 특정 클래스만
- Maven은
Tests run: N, Failures: 0이, Gradle은BUILD SUCCESSFUL이 출력되면 정상입니다. - 실패하면 리포트를 확인합니다: Maven은
target/surefire-reports/*.txt, Gradle은build/reports/tests/test/index.html.
✏️ Spring Boot 테스트 전략 직접 해보기¶
@WebMvcTest로 컨트롤러 한 개를 단위 테스트해 보라.
애플리케이션 전체를 띄우지 않고도 웹 계층만 잘라 검증할 수 있다는 것을 직접 체감하는 과제입니다. 위 3번에서 읽은 슬라이스 테스트가 실제로 어떻게 생겼는지 가장 작은 컨트롤러로 확인합니다.
실습 순서
- 파일 생성 — ch06 6.1에서 만든
demo프로젝트에 컨트롤러src/main/java/com/example/demo/practice/PingController.java와 테스트src/test/java/com/example/demo/practice/PingControllerTest.java를 만듭니다. -
뼈대 입력 — 아래 두 뼈대를 그대로 입력합니다.
파일:
src/main/java/com/example/demo/practice/PingController.javapackage com.example.demo.practice; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PingController { @GetMapping("/ping") public String ping() { return "pong"; } }파일:
src/test/java/com/example/demo/practice/PingControllerTest.javapackage com.example.demo.practice; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(PingController.class) class PingControllerTest { @Autowired MockMvc mockMvc; @Test void ping_returns_200() throws Exception { mockMvc.perform(get("/ping")).andExpect(status().isOk()); // 1) 응답 바디가 "pong"인지 content().string(...)으로 함께 검증 // 2) 없는 경로 GET /nope 가 404인지 새 @Test 메서드로 검증 } } -
하나씩 구현 — 주석의 과제를 한 항목씩 구현합니다.
- 실행·확인 —
./mvnw test -Dtest=PingControllerTest(Gradle이라면./gradlew test --tests "*PingControllerTest") — 테스트 1개가 통과하는지 확인합니다. — 추가할 때마다 다시 실행해 통과 여부를 확인합니다.
정리¶
테스트 전략은 기술 선택 문제가 아니라 경계 설정 문제입니다. 무엇을 어디까지 검증할지 먼저 정하면, 애노테이션과 도구 선택은 그 다음에 자연스럽게 따라옵니다.
한 줄 정리¶
Spring Boot 테스트의 핵심은 가장 작은 비용으로 가장 큰 회귀 위험을 줄이는 검증 레벨을 고르는 것입니다.
9.2 계산기 테스트 기초¶
🎯 목표: 계산기 예제로 단위 테스트 기초를 익힌다.
개요¶
이 문서는 작은 계산기 예제를 통해 테스트 가능한 코드가 어떤 구조를 요구하는지 설명하는 실습 문서입니다. 핵심은 계산기를 만드는 것이 아니라, 테스트가 가능하도록 책임을 분리하는 과정을 이해하는 데 있습니다.
왜 중요한가¶
테스트는 완성된 코드에 붙이는 마지막 장식이 아닙니다. 테스트를 쓰려는 순간, 입력 파싱과 계산 규칙, 예외 처리, 출력 책임을 분리해야 한다는 사실이 드러납니다. 그래서 작은 예제일수록 구조 개선 연습에 적합합니다.
실습 목표¶
- 연산 규칙을 순수한 계산 로직으로 분리하기
- 입력 파싱과 계산 책임을 나누기
- 성공 케이스와 실패 케이스를 JUnit 5로 검증하기
이 절의 파일들은 java-study-ch01 1.2에서 만든 순수 Java 프로젝트 hello-java에 com.example.ch09 패키지로 작성합니다. JUnit 5 의존성 추가와 실행 방법은 절 끝의 "프로젝트에 두고 실행하기"에서 안내합니다.
1. 처음 코드가 왜 테스트하기 어려운가¶
계산기의 첫 코드는 흔히 콘솔 입력, 문자열 파싱, 계산, 출력이 모두 main 메서드 하나에 붙어 있는 형태가 됩니다. 이런 코드는 테스트가 어렵습니다.
- 입력을 직접 넣기 어렵습니다.
- 계산 규칙만 따로 검증하기 어렵습니다.
- 예외 상황을 세밀하게 확인하기 어렵습니다. 즉, 문제는 계산기가 아니라 책임 분리 실패입니다.
2. 첫 단계는 계산 규칙을 메서드로 분리하는 것이다¶
파일: src/main/java/com/example/ch09/Calculator.java
package com.example.ch09;
public class Calculator {
public int calculate(int left, String operator, int right) {
return switch (operator) {
case "+" -> left + right;
case "-" -> left - right;
case "*" -> left * right;
case "/" -> {
if (right == 0) {
throw new IllegalArgumentException("0으로 나눌 수 없습니다.");
}
yield left / right;
}
default -> throw new IllegalArgumentException("지원하지 않는 연산자입니다.");
};
}
}
3. 입력 파싱은 별도 책임으로 분리한다¶
파일: src/main/java/com/example/ch09/Expression.java
파일: src/main/java/com/example/ch09/ExpressionParser.java
package com.example.ch09;
public class ExpressionParser {
public Expression parse(String input) {
String sanitized = input.replace("(", "")
.replace(")", "")
.trim();
String[] tokens = sanitized.split(" ");
if (tokens.length != 3) {
throw new IllegalArgumentException("수식 형식이 올바르지 않습니다.");
}
return new Expression(
Integer.parseInt(tokens[0]),
tokens[1],
Integer.parseInt(tokens[2])
);
}
}
4. JUnit 5 테스트 예제¶
이제 분리해 둔 계산 규칙을 JUnit 5로 검증합니다. 코드에 처음 나오는 세 요소만 미리 알아 두면 됩니다 — assertEquals는 기대값과 실제 결과가 같은지, assertThrows는 지정한 예외가 실제로 발생하는지 확인하는 단언(assertion) 메서드이고, @DisplayName은 테스트 결과에 표시될 이름을 사람이 읽는 문장으로 붙이는 애노테이션입니다.
파일: src/test/java/com/example/ch09/CalculatorTest.java
package com.example.ch09;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class CalculatorTest {
private final Calculator calculator = new Calculator();
@Test
@DisplayName("덧셈을 계산한다")
void addsNumbers() {
assertEquals(8, calculator.calculate(5, "+", 3));
}
@Test
@DisplayName("0으로 나누면 예외가 발생한다")
void rejectsDivisionByZero() {
assertThrows(IllegalArgumentException.class,
() -> calculator.calculate(5, "/", 0));
}
@Test
@DisplayName("지원하지 않는 연산자는 예외가 발생한다")
void rejectsUnsupportedOperator() {
assertThrows(IllegalArgumentException.class,
() -> calculator.calculate(5, "^", 2));
}
}
- 정상 흐름을 먼저 검증합니다.
- 실패 케이스를 명시적으로 검증합니다.
- 콘솔 출력이 아니라 핵심 규칙을 직접 검증합니다.
5. 어디까지 테스트해야 하는가¶
초중급 단계에서는 아래 순서로 보는 것이 좋습니다.
- 계산 규칙 테스트
- 파싱 규칙 테스트
- 두 객체를 엮는 작은 애플리케이션 서비스 테스트
콘솔
main메서드까지 무리하게 테스트하려 하기보다, 핵심 규칙을 먼저 보호하는 편이 훨씬 효과적입니다.
6. 이 예제가 Spring Boot 테스트와 이어지는 이유¶
이 계산기 예제는 아주 작지만, 실제 Spring Boot 구조와 동일한 감각을 요구합니다.
- Controller는 입력을 받고
- Service는 규칙을 처리하고
- Parser나 Mapper는 변환을 담당합니다.
테스트 전략도 같습니다. 규칙은 단위 테스트로, 웹 요청/응답은 슬라이스 테스트로, 전체 흐름은 통합 테스트로 검증합니다.
필자의 실무 저장소에서도 같은 감각이 보입니다. 아래 두 블록은 참고 코드(실무 저장소 발췌 스텁)로, 이 장의 실습 대상이 아닙니다.
@DisplayName("Loan 엔티티 테스트") class LoanTest { @Test @DisplayName("회원 정보가 null이면 검증 실패") void memberShouldNotBeNull() { // Bean Validation 기반 도메인 규칙 검증 } }
자주 하는 실수¶
- 출력 결과만 눈으로 보고 테스트 코드를 생략하는 것
- 성공 케이스만 테스트하고 실패 케이스를 빼는 것
- 테스트하려고 하기보다, 테스트가 어려운 구조를 그대로 유지하는 것
공식 문서 참고¶
프로젝트에 두고 실행하기¶
위 Calculator·Expression·ExpressionParser·CalculatorTest를 실제로 실행하려면 JUnit 5 의존성과 표준 위치가 필요합니다. 프로젝트는 java-study-ch01 1.2에서 만든 Maven/Gradle 프로젝트 hello-java를 그대로 사용하고, 이 장의 코드는 com.example.ch09 패키지에 둡니다.
의존성 (빌드 파일):
// build.gradle
test { useJUnitPlatform() }
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
<!-- pom.xml — Spring Boot 프로젝트면 spring-boot-starter-test 하나로 충분 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
파일 배치 — 프로덕션 코드와 테스트 코드를 나눕니다:
src/main/java/com/example/ch09/Calculator.java ← 계산 규칙
src/main/java/com/example/ch09/Expression.java ← 수식 record
src/main/java/com/example/ch09/ExpressionParser.java ← 입력 파싱
src/test/java/com/example/ch09/CalculatorTest.java ← 테스트 코드
실행:
mvn test -Dtest=CalculatorTest # 1장 archetype 프로젝트에는 mvnw 래퍼가 없음 (Windows도 mvn 동일)
# 또는 Gradle
./gradlew test --tests "CalculatorTest" # Windows: gradlew.bat
- Maven은
Tests run: 3, Failures: 0이, Gradle은CalculatorTest > addsNumbers() PASSED와BUILD SUCCESSFUL이 출력되면 정상입니다.
자주 나는 에러 → 원인 확인:
| 증상 | 원인 확인 |
|---|---|
테스트가 0개 실행됨 (Tests run: 0 / NO-SOURCE) |
테스트 파일이 src/test/java 아래에 있는지, 클래스명이 *Test 형태인지 확인합니다. |
error: package com.example.ch09 does not exist |
디렉터리 경로(com/example/ch09/)와 package 선언이 일치하는지 확인합니다. |
cannot find symbol: class Calculator |
Calculator가 src/main/java/com/example/ch09/에 있고 테스트와 같은 패키지인지 확인합니다. |
| Gradle에서 JUnit 5 테스트를 찾지 못함 | build.gradle에 test { useJUnitPlatform() }이 있는지 확인합니다. |
정리¶
좋은 테스트 예제는 화려한 프레임워크보다, 책임이 잘 나뉜 작은 코드에서 시작합니다. 계산기 같은 작은 문제를 테스트 가능하게 바꾸는 과정은 이후의 서비스, 컨트롤러, 리포지토리 테스트로 그대로 이어집니다.
한 줄 정리¶
테스트 가능한 코드의 핵심은 테스트 기술보다 먼저, 책임이 나뉜 구조를 만드는 것입니다.
9.3 API 수동 검증: curl 활용¶
🎯 목표: curl로 API를 수동 검증한다.
개요¶
이 문서는 curl을 사용해 HTTP API를 직접 검증하는 방법을 정리한 가이드입니다. 자동화 테스트가 있어도, 실제 요청과 응답을 눈으로 확인해야 하는 순간은 계속 존재합니다. 핵심은 수동 검증을 많이 하는 것이 아니라, 어떤 문제를 curl로 빨리 확인하고 어떤 문제를 자동화 테스트로 남길지 구분하는 것입니다.
먼저 — 서버를 띄우고 시작한다¶
curl은 떠 있는 서버에 요청을 보내는 도구입니다. 다른 터미널에서 서버를 먼저 띄운 뒤, 이 터미널에서 curl 명령을 실행합니다. 참고로 본문 3~6번의 예시 URL은 이 장 앞에서 소개한 필자의 실무 저장소(도서 대출 API) 기준의 읽기 자료라 그대로 실행되지 않아도 괜찮고, 직접 실행하는 실습은 절 끝에서 ch06 6.1의 demo 프로젝트로 진행합니다.
# 별도 터미널에서 (포그라운드로 계속 떠 있음, 종료는 Ctrl+C)
./mvnw spring-boot:run # Windows: mvnw.cmd spring-boot:run
# 또는 ./gradlew bootRun (Windows: gradlew.bat bootRun)
# → 로그에 "Started ...Application" 이 뜨면 8080 준비 완료
Windows 주의: PowerShell에서
curl은Invoke-WebRequest의 별칭이라 아래-X/-d문법이 깨집니다.curl.exe로 명시하거나 Git Bash에서 실행합니다.
왜 중요한가¶
테스트 코드는 회귀 방지에 강하지만, 실제 HTTP 요청을 한 번에 점검하는 데는 curl이 더 빠를 때가 많습니다. 특히 아래 상황에서는 수동 검증이 유용합니다.
- 인증 헤더를 포함한 실제 요청을 바로 보내 보고 싶을 때
- JSON 바디와 상태 코드를 빠르게 확인하고 싶을 때
- 로컬 프로파일, 포트, 프록시, CORS 이전 단계 문제를 확인할 때
- Swagger UI 없이도 재현 가능한 요청 스크립트를 남기고 싶을 때
이 문서의 역할¶
이 문서는 자동화 테스트를 대체하지 않습니다. 책 기준에서 curl은 아래 역할에 가깝습니다.
- API 설계가 실제 요청/응답으로 어떻게 보이는지 확인합니다.
- 인증, 직렬화, 상태 코드 같은 경계 지점을 빠르게 점검합니다.
-
버그 재현 절차를 텍스트로 남깁니다. 반대로 아래는 자동화 테스트가 더 적합합니다.
-
반복 실행이 필요한 회귀 검증
- 비즈니스 규칙 검증
- 레이어별 실패 원인 추적
1. 수동 검증 전에 먼저 확인할 것¶
좋은 curl 테스트는 명령보다 사전 조건이 먼저 분명해야 합니다.
- 애플리케이션이 어떤 프로파일로 실행 중인가
- 서버 주소와 포트는 무엇인가
- 필요한 인증 토큰이 준비되었는가
- 테스트용 데이터가 이미 있는가, 아니면 먼저 만들어야 하는가
- 성공 기준이 응답 바디인지, 상태 코드인지, 둘 다인지
이 기준이 없으면
curl명령이 많아져도 문서 품질은 올라가지 않습니다.
2. 가장 먼저 보는 것은 상태 코드입니다¶
초중급 단계에서 가장 흔한 실수는 JSON 바디만 보고 성공 여부를 판단하는 것입니다. 하지만 API 검증의 첫 줄은 상태 코드입니다.
200 OK: 조회나 수정이 정상 처리되었는가201 Created: 생성 요청이 새 리소스를 만들었는가400 Bad Request: 잘못된 요청 형식을 제대로 거부하는가401 Unauthorized: 인증 없는 요청을 막는가403 Forbidden: 권한 없는 사용자를 막는가404 Not Found: 없는 리소스 접근을 적절히 처리하는가
3. 기본 curl 패턴¶
아래는 실무에서 가장 자주 쓰는 요청 패턴 다섯 가지입니다. 모든 명령에 공통으로 붙는 -i 옵션은 응답 바디만이 아니라 상태 줄과 헤더까지 함께 출력하게 합니다 — 2번에서 강조한 상태 코드를 매번 확인하기 위한 장치입니다.
GET 요청¶
JSON 바디를 포함한 POST 요청¶
curl -i -X POST http://localhost:8080/api/books \
-H "Content-Type: application/json" \
-d '{
"title": "Effective Java",
"author": "Joshua Bloch"
}'
인증 헤더 포함 요청¶
예상 결과
유효한 사용자 또는 관리자 토큰이면 현재 로그인 사용자의 대출 목록이 반환된다.
이 프로젝트에서는 `@AuthenticationPrincipal CustomUserDetails`를 통해 토큰에서 복원된 `memberId`를 사용한다.
PATCH 요청¶
curl -i -X PATCH http://localhost:8080/api/books/1 \
-H "Content-Type: application/json" \
-d '{
"title": "Effective Java 3rd"
}'
DELETE 요청¶
여기서 중요한 것은 명령어 자체보다, 각 요청이 어떤 HTTP 의미를 가지는지 알고 보내는 것입니다.
4. 추천 검증 순서¶
책 기준에서 가장 실수 적은 순서는 아래와 같습니다.
- 서버 기동과 포트 확인
- 공개 조회 API로 기본 연결 확인
/api/auth/login으로 토큰 발급 확인- 보호된 사용자 API 검증
- 보호된 관리자 API 검증
- 잘못된 입력으로
400확인 - 인증 실패와 권한 부족 흐름 확인
- 없는 ID로 요청해서
404확인 이 순서가 좋은 이유는, 환경 문제와 도메인 문제를 섞지 않게 해 주기 때문입니다.
필자의 실무 저장소(이하 "현재 저장소") 기준으로는 아래 순서가 특히 자연스럽습니다.
GET /api/books/{id}
→ POST /api/auth/login
→ GET /api/client/loans
→ PUT /api/admin/members/{id}/promote
관리자 토큰과 일반 사용자 토큰을 나눠 보면 hasRole("ADMIN") 규칙 검증까지 한 번에 이어집니다.
5. 인증 API는 헤더와 실패 케이스를 같이 봅니다¶
인증이 들어가는 API는 성공 요청만 보면 부족합니다. 최소한 아래 세 가지는 같이 확인하는 편이 좋습니다.
- 토큰이 있을 때 정상 동작하는가
- 토큰이 없을 때 요청이 차단되는가
- 권한이 부족할 때
403 Forbidden이 나는가 즉, 인증 API 검증은 "요청이 된다"가 아니라 경계가 올바르게 막히는지까지 포함해야 합니다.
현재 저장소는 AuthenticationEntryPoint와 AccessDeniedHandler를 별도로 커스터마이징하지 않았습니다. 따라서 보호된 API에서 토큰이 없을 때의 정확한 실패 응답은 실제 실행 환경에서 확인하는 편이 안전합니다. 반면 인증된 USER 토큰으로 /api/admin/**를 호출했을 때 권한 부족으로 차단되는 흐름은 반드시 확인해야 합니다.
curl -i -X PUT "http://localhost:8080/api/admin/members/1/promote" \
-H "Authorization: Bearer USER_ACCESS_TOKEN"
예상 결과
관리자 권한이 없는 사용자 토큰이면 `403 Forbidden`으로 차단된다.
현재 보안 설정의 `requestMatchers("/api/admin/**").hasRole("ADMIN")` 규칙이 실제로 동작하는지 확인하는 가장 짧은 검증이다.
6. 예제 프로젝트에 적용하는 방법¶
도서 대여 같은 예제 프로젝트에서는 아래 흐름으로 검증 문서를 만들면 좋습니다.
- 회원가입 또는 기존 테스트 계정 확인
- 로그인 후 JWT 토큰 발급
- 공개 API와 보호된 API를 구분해 호출
- 대출 생성
- 내 대출 조회
- 반납 또는 상태 변경
- 권한 부족 요청으로
403확인 - 삭제 후
404확인 핵심은 특정 도메인이 아니라, 사전 데이터 생성 → 정상 흐름 → 오류 흐름 순서가 보이는 것입니다.
로그인 실패도 별도 검증 가치가 있습니다.
curl -i -X POST "http://localhost:8080/api/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "hong@test.com",
"password": "wrong-password"
}'
예상 결과
인증 실패 시 토큰은 발급되지 않는다.
현재 저장소는 `GlobalExceptionHandler`에서 `AuthenticationException`을 받아 `401 Unauthorized`와 `AUTHENTICATION_FAILED` 형태의 응답으로 정리한다.
7. curl 문서를 남길 때 좋은 형식¶
좋은 수동 검증 문서는 아래 네 가지를 함께 남깁니다.
- 사전 조건
- 요청 명령
- 기대 상태 코드
- 기대 결과 요약 예를 들면 아래처럼 적는 편이 좋습니다.
이 정도만 있어도 나중에 버그 재현 문서로 바로 재사용할 수 있습니다.
자주 하는 실수¶
- 서버를 띄우지 않고 요청부터 보내는 것
- 인증 API인데 토큰 없이 성공만 기대하는 것
- 사전 데이터 생성 없이 수정/삭제부터 시도하는 것
- 상태 코드를 보지 않고 응답 바디만 확인하는 것
- 수동 검증 절차를 남기지 않아 같은 버그를 다시 재현하지 못하는 것
공식 문서¶
✏️ API 수동 검증 직접 해보기¶
실행 중인 API에 curl로 GET·POST 요청을 보내 응답을 확인하라.
본문에서 읽기만 한 요청 패턴을 내 손으로 실행해 보는 과제입니다. 응답 바디보다 상태 코드를 먼저 확인하는 습관과, 검증 절차를 텍스트로 남기는 습관을 여기서 들입니다.
실습 순서
- 파일 열기 — ch06 6.1에서 만든
demo프로젝트의src/main/java/com/example/demo/practice/PingController.java(9.1 실습에서 생성)를 엽니다. 없다면 9.1 실습 순서대로 먼저 만듭니다. - 수정 — POST를 받을 메서드를 추가합니다. 예:
@PostMapping("/echo")+@RequestBody String body를 그대로 반환. - 실행 —
./mvnw spring-boot:run으로 서버를 띄웁니다. - 요청 — 새 터미널에서
curl -i http://localhost:8080/ping(GET)과curl -i -X POST http://localhost:8080/echo -H "Content-Type: text/plain" -d "hello"(POST)를 보내고, 위 7번 형식처럼 목적·요청·기대 상태 코드를 함께 기록합니다.
정리¶
curl 기반 수동 검증의 핵심은 명령을 많이 아는 것이 아니라, 어떤 요청을 어떤 순서로 보내야 API의 정상 흐름과 실패 흐름을 빠르게 확인할 수 있는지 아는 것입니다. 자동화 테스트와 충돌하는 작업이 아니라, 자동화 전에 경계 문제를 빨리 드러내는 실무 도구로 보는 편이 맞습니다.
한 줄 정리¶
curl 수동 검증의 핵심은 사전 조건, 요청, 상태 코드, 기대 결과를 한 세트로 관리하는 것입니다.