안녕하세요, 이력서 기반 면접 준비를 도와드리는 QueryDaily 팀입니다.
"테스트 코드의 중요성은 알아요. 근데 기능 만들기도 바쁜데, 테스트 코드까지 짤 시간이 어디 있어요?"
혹시 이런 생각 해보신 적 있으신가요? 입사 전에는 TDD도 해보고, AssertJ로 테스트 코드도 작성했는데, 막상 실무에서는 데드라인에 쫓겨 Swagger로 직접 테스트하고 있는 분들. 정말 많으실 거예요.
오늘은 이 현실적인 고민에 대해 솔직하게 이야기해보려 합니다.

먼저 지금 하고 계신 Swagger 테스트 과정을 생각해볼까요?
1. 서버를 실행하고 Swagger UI를 엽니다
2. 특정 API 엔드포인트를 선택합니다
3. Request Body에 JSON 데이터를 직접 입력합니다
4. 'Execute' 버튼을 누릅니다
5. 응답으로 돌아온 HTTP 상태 코드와 Response Body를 '눈으로' 확인합니다
6. "음, 잘 동작하는군" 하고 판단합니다
테스트 코드는 이 과정을 그대로 코드로 옮겨놓은 것뿐입니다.
컴퓨터가 사람의 '눈'과 '판단'을 대신해주는 거죠.
@Test
fun `회원가입 API가 정상 동작한다`() {
// Given - Swagger에서 입력했던 그 JSON
val request = """
{
"email": "test@example.com",
"password": "password123"
}
""".trimIndent()
// When - Execute 버튼을 누르는 것
val result = mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(request)
)
// Then - 눈으로 확인하던 것을 코드가 대신
result.andExpect(status().isCreated)
}
차이점은 단 하나입니다. Swagger 테스트는 매번 수동으로 해야 하고, 테스트 코드는 버튼 한 번이면 모든 테스트가 자동으로 실행됩니다.
이 말씀, 정말 공감됩니다. 그런데 관점을 한번 바꿔볼까요?
❌ 귀찮음의 관점
"아, 또 깨졌네. 이것까지 언제 고치지?"
✅ 안전망의 관점
"어? 내 코드가 생각지도 못한 부분을 건드렸나 보네?
배포 전에 이걸 발견해서 정말 다행이다!"
테스트가 깨지는 것은 '귀찮은 일'이 아니라 '잠재적인 버그를 미리 알려주는 경고등' 입니다.
이 경고등이 촘촘할수록:
"잘 작성된 테스트가 있으면 리팩토링을 자신 있게 할 수 있습니다."
처음부터 TDD를 하거나 모든 코드에 테스트를 작성하는 건 현실적으로 어렵습니다.
이렇게 시작해보세요:
다음에 Swagger로 특정 API를 테스트하실 때, 방금 수동으로 테스트했던 그 케이스를 그대로 테스트 코드로 옮겨보는 겁니다.
// Swagger에서 회원가입 API 테스트했다면?
// 그 과정을 그대로 코드로!
@SpringBootTest
@AutoConfigureMockMvc
class UserApiTest {
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `정상적인 정보로 회원가입하면 201 응답을 받는다`() {
// Swagger에서 입력했던 것
val request = UserCreationRequest(
email = "test@example.com",
password = "password123"
)
// Execute 버튼 누르던 것
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
)
// 눈으로 확인하던 것
.andExpect(status().isCreated)
}
}
이렇게 접근하면, 지금 하고 계신 수동 테스트를 하나씩 자동화하면서 자연스럽게 테스트 코드와 다시 친숙해질 수 있습니다.
"단위 테스트부터? 통합 테스트부터? E2E 테스트도 해야 하나요?"
이 고민을 해결하기 위해 '테스트 피라미드(Testing Pyramid)' 라는 개념을 사용합니다.
/\
/ \ E2E 테스트
/----\ (비싸고, 느리고, 적게)
/ \
/ 통합 \ 통합 테스트
/----------\ (중간)
/ \
/ 단위 테스트 \ 단위 테스트
/________________\ (싸고, 빠르고, 많이)
| 구분 | 단위 테스트 | 통합 테스트 |
|---|---|---|
| 범위 | 클래스/메소드 하나 | API 전체 (Controller → Service → Repository → DB) |
| 속도 | 매우 빠름 | 상대적으로 느림 |
| 장점 | 실패 시 문제 위치 즉시 파악 | "이 API는 DB까지 잘 동작한다"는 강한 자신감 |
| 단점 | 컴포넌트 간 연동 문제 못 잡음 | 설정이 복잡함 |
자신감과 효율(가성비)을 가장 빠르게 얻을 수 있는 것은 통합 테스트입니다.
// 통합 테스트: API가 전체적으로 잘 동작하는지
@Test
fun `주문 생성 API가 정상 동작한다`() {
mockMvc.perform(post("/api/orders")...)
.andExpect(status().isCreated)
}
// 단위 테스트: 복잡한 비즈니스 로직만 따로
@Test
fun `VIP 회원은 15% 할인이 적용된다`() {
val calculator = DiscountCalculator()
val result = calculator.calculate(price = 10000, memberGrade = "VIP")
assertThat(result).isEqualTo(8500)
}
E2E(End-to-End) 테스트는 브라우저 클릭부터 프론트엔드-백엔드-DB까지 전체 시스템을 테스트합니다.
결론: 백엔드 개발자의 주된 책임은 아닙니다.
백엔드 개발자라면 단위/통합 테스트에 집중하세요.
날카로운 질문입니다. 솔직하게 말씀드릴게요.
로컬 테스트 통과가 운영 환경에서의 무결성을 100% 보장하지는 않습니다.
| 차이점 | 설명 |
|---|---|
| 데이터 | 운영 DB에는 상상도 못한 데이터가 있을 수 있음 |
| 인프라/설정 | 네트워크 지연, 외부 API 응답, AWS 권한 문제 등 |
| 동시성 | 수많은 사용자가 동시 요청할 때의 Race Condition |
로컬 테스트는 "우리가 통제할 수 있는 코드의 로직과 기능" 안에서 발생할 수 있는 대부분의 버그를 걸러주는 매우 효과적인 필터입니다.
이 필터를 통과해야, 인프라나 데이터 문제처럼 더 예측하기 어려운 다음 단계의 문제에 집중할 수 있습니다.
테스트는 '버그 제로'를 보장하는 은탄환(Silver Bullet)이 아니라, '배포에 대한 자신감을 높여주고, 잠재적인 위험을 최소화하는' 가장 중요한 개발 활동 중 하나입니다.
다른 개발자가 테스트 코드만 읽고도 "아, 이 기능은 이런 상황에서 이렇게 동작하는구나"를 바로 파악할 수 있어야 합니다.
// ❌ 의도가 불명확
@Test
fun createUserTest() { ... }
// ✅ 서술적으로 작성 (한글 메소드명 OK!)
@Test
fun `유저가_정상적인_정보로_회원가입하면_성공적으로_생성된다`() { ... }
@Test
fun `중복된_이메일로_회원가입하면_400_에러가_발생한다`() { ... }
@Test
fun `VIP 회원은 15% 할인이 적용된다`() {
// Given (어떤 상황이 주어졌을 때)
val calculator = DiscountCalculator()
val memberGrade = "VIP"
val originalPrice = 10000
// When (어떤 행동을 하면)
val discountedPrice = calculator.calculate(originalPrice, memberGrade)
// Then (이런 결과가 나와야 한다)
assertThat(discountedPrice).isEqualTo(8500)
}
// ❌ 여러 가지를 한꺼번에 검증
@Test
fun `회원가입과_로그인_테스트`() { ... }
// ✅ 목적별로 분리
@Test
fun `회원가입_성공_테스트`() { ... }
@Test
fun `회원가입_실패_테스트_중복된_이메일`() { ... }
@Test
fun `로그인_성공_테스트`() { ... }
각 테스트는 서로 영향을 주지 않고, 어떤 순서로 실행되어도 항상 동일한 결과를 내야 합니다.
@Transactional // 테스트 끝나면 DB 롤백
@SpringBootTest
class UserServiceTest {
@BeforeEach
fun setUp() {
// 각 테스트 전 데이터 초기화
userRepository.deleteAll()
}
}
내부 구현(private 메소드)이 아니라, 외부에 공개하는 기능(public 메소드)이 올바르게 동작하는지 테스트하세요.
내부 구현은 언제든 바뀔 수 있는 리팩토링 대상입니다. 내부 구현을 테스트하면, 리팩토링할 때마다 테스트 코드를 수정해야 하는 주객전도가 발생합니다.
| 도구 | 설명 |
|---|---|
| JUnit 5 | 자바 진영의 표준 테스트 프레임워크. 거의 모든 기업이 사용 |
| AssertJ | assertThat(결과).isEqualTo(기대값) 처럼 읽기 쉬운 검증문 작성 |
| 도구 | 설명 |
|---|---|
| Mockito | 가짜 객체(Mock)를 만들어주는 라이브러리. Repository를 가짜로 만들어서 Service 로직만 테스트 가능 |
| Kotest | Kotlin 프로젝트용. 더 코틀린 친화적인 문법 제공 |
| 도구 | 설명 |
|---|---|
| Spring Test & MockMvc | Spring Boot의 사실상 표준. 실제 서버 없이 API 테스트 |
| REST Assured | given().when().get("/api/users").then().statusCode(200) BDD 스타일 문법 |
| Testcontainers | 최근 가장 주목받는 기술. 테스트 실행 시 Docker로 실제 DB를 띄워서 테스트 |
JUnit 5 + AssertJ + Mockito + Spring Test + Testcontainers
특히 Testcontainers는 H2 같은 인메모리 DB가 아닌, 실제 운영 환경과 동일한 DB(MySQL, PostgreSQL)를 대상으로 테스트할 수 있어 신뢰도가 매우 높습니다.
핵심 포인트: 시간 절약, 자신감, 문서화 세 가지를 언급하세요.
"테스트 코드는 당장은 시간이 들지만, 장기적으로는 시간을 절약해줍니다. 리팩토링할 때 자신감을 갖고 코드를 수정할 수 있고, 테스트 코드 자체가 기능의 명세서 역할을 합니다. 또한 배포 전에 잠재적 버그를 발견할 수 있어 운영 환경의 장애를 예방합니다."
핵심 포인트: 범위와 목적의 차이를 설명하세요.
"단위 테스트는 클래스나 메소드 단위로 독립적으로 테스트하고, Mock을 활용해 외부 의존성을 제거합니다. 통합 테스트는 여러 컴포넌트가 함께 동작하는지, 특히 DB 연동까지 포함해서 검증합니다. 단위 테스트는 빠르고 문제 위치 파악이 쉽고, 통합 테스트는 실제 환경과 가까운 자신감을 줍니다."
핵심 포인트: 가독성, 독립성, 유지보수성을 언급하세요.
"첫째, 테스트 코드는 문서입니다. 다른 개발자가 읽고 기능을 이해할 수 있도록 Given-When-Then 구조로 명확하게 작성합니다. 둘째, 각 테스트는 독립적이어야 합니다. 실행 순서에 상관없이 동일한 결과를 내야 합니다. 셋째, 구현이 아닌 동작을 테스트해서 리팩토링 시 테스트 코드 수정을 최소화합니다."
핵심 포인트: H2와의 차이점, 신뢰도를 언급하세요.
"Testcontainers는 테스트 실행 시 Docker로 실제 DB를 띄워서 테스트합니다. H2 같은 인메모리 DB는 실제 DB와 SQL 문법이나 동작이 다를 수 있어서 '로컬에서는 됐는데 운영에서 안 된다'는 문제가 발생할 수 있습니다. Testcontainers를 사용하면 운영 환경과 동일한 DB로 테스트하므로 신뢰도가 높아집니다."
"테스트 코드 짤 시간이 없어요"라는 말, 사실은 "테스트 코드가 없어서 더 많은 시간을 쓰고 있다" 는 의미일 수 있습니다.

면접에서 테스트 관련 질문이 나온다면, 단순히 "중요하니까요"가 아닌 구체적인 이유와 전략을 말씀하실 수 있어야 합니다. 여러분의 이력서에 적힌 'JUnit', 'Mockito' 경험에서 어떤 꼬리 질문이 나올 수 있을까요? QueryDaily 에서 미리 연습해보세요.
#테스트코드 #JUnit #SpringTest #백엔드면접 #QueryDaily