"테스트 코드요? 기능 만들기도 바빠서..." — 그 핑계, 오히려 시간을 잡아먹고 있습니다

TeamGrit·2025년 12월 12일

Interview-Question

목록 보기
7/12

안녕하세요, 이력서 기반 면접 준비를 도와드리는 QueryDaily 팀입니다.

"테스트 코드의 중요성은 알아요. 근데 기능 만들기도 바쁜데, 테스트 코드까지 짤 시간이 어디 있어요?"

혹시 이런 생각 해보신 적 있으신가요? 입사 전에는 TDD도 해보고, AssertJ로 테스트 코드도 작성했는데, 막상 실무에서는 데드라인에 쫓겨 Swagger로 직접 테스트하고 있는 분들. 정말 많으실 거예요.

오늘은 이 현실적인 고민에 대해 솔직하게 이야기해보려 합니다.


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 테스트는 매번 수동으로 해야 하고, 테스트 코드는 버튼 한 번이면 모든 테스트가 자동으로 실행됩니다.


"테스트 코드 터지면 수정해야 돼서 귀찮아요"

이 말씀, 정말 공감됩니다. 그런데 관점을 한번 바꿔볼까요?

❌ 귀찮음의 관점
"아, 또 깨졌네. 이것까지 언제 고치지?"

✅ 안전망의 관점
"어? 내 코드가 생각지도 못한 부분을 건드렸나 보네?
배포 전에 이걸 발견해서 정말 다행이다!"

테스트가 깨지는 것은 '귀찮은 일'이 아니라 '잠재적인 버그를 미리 알려주는 경고등' 입니다.

이 경고등이 촘촘할수록:

  • 운영 환경에서 터질 더 큰 문제를 막아줍니다
  • 자신감을 갖고 코드를 수정(리팩토링)할 수 있습니다
  • 퇴근 후 "내가 뭘 잘못 건드렸지?" 하는 불안감에서 벗어날 수 있습니다

"잘 작성된 테스트가 있으면 리팩토링을 자신 있게 할 수 있습니다."


현실적인 첫걸음: 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 테스트
      /----\     (비싸고, 느리고, 적게)
     /      \
    / 통합   \    통합 테스트
   /----------\   (중간)
  /            \
 /  단위 테스트  \  단위 테스트
/________________\  (싸고, 빠르고, 많이)

단위 테스트 vs 통합 테스트

구분단위 테스트통합 테스트
범위클래스/메소드 하나API 전체 (Controller → Service → Repository → DB)
속도매우 빠름상대적으로 느림
장점실패 시 문제 위치 즉시 파악"이 API는 DB까지 잘 동작한다"는 강한 자신감
단점컴포넌트 간 연동 문제 못 잡음설정이 복잡함

추천 전략

자신감과 효율(가성비)을 가장 빠르게 얻을 수 있는 것은 통합 테스트입니다.

  1. API를 하나 만들면, 해당 API가 잘 동작하는지 검증하는 통합 테스트 1~2개 작성
  2. API 내부 로직 중 특별히 복잡한 부분(예: 복잡한 할인율 계산)만 단위 테스트 추가
// 통합 테스트: 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 테스트는요?

E2E(End-to-End) 테스트는 브라우저 클릭부터 프론트엔드-백엔드-DB까지 전체 시스템을 테스트합니다.

결론: 백엔드 개발자의 주된 책임은 아닙니다.

  • 작성/유지보수 비용이 매우 비쌉니다
  • 작은 UI 변경에도 쉽게 깨집니다
  • 보통 QA팀이 담당하거나, 프론트엔드팀과 협력합니다

백엔드 개발자라면 단위/통합 테스트에 집중하세요.


"로컬에서 테스트 통과해도, 운영에서 문제없을까요?"

날카로운 질문입니다. 솔직하게 말씀드릴게요.

로컬 테스트 통과가 운영 환경에서의 무결성을 100% 보장하지는 않습니다.

로컬과 운영이 다른 이유

차이점설명
데이터운영 DB에는 상상도 못한 데이터가 있을 수 있음
인프라/설정네트워크 지연, 외부 API 응답, AWS 권한 문제 등
동시성수많은 사용자가 동시 요청할 때의 Race Condition

그럼에도 테스트를 하는 이유

로컬 테스트는 "우리가 통제할 수 있는 코드의 로직과 기능" 안에서 발생할 수 있는 대부분의 버그를 걸러주는 매우 효과적인 필터입니다.

이 필터를 통과해야, 인프라나 데이터 문제처럼 더 예측하기 어려운 다음 단계의 문제에 집중할 수 있습니다.

테스트는 '버그 제로'를 보장하는 은탄환(Silver Bullet)이 아니라, '배포에 대한 자신감을 높여주고, 잠재적인 위험을 최소화하는' 가장 중요한 개발 활동 중 하나입니다.


좋은 테스트 코드 작성법

1. 테스트는 '문서'다: 명확하고 읽기 쉽게

다른 개발자가 테스트 코드만 읽고도 "아, 이 기능은 이런 상황에서 이렇게 동작하는구나"를 바로 파악할 수 있어야 합니다.

// ❌ 의도가 불명확
@Test
fun createUserTest() { ... }

// ✅ 서술적으로 작성 (한글 메소드명 OK!)
@Test
fun `유저가_정상적인_정보로_회원가입하면_성공적으로_생성된다`() { ... }

@Test
fun `중복된_이메일로_회원가입하면_400_에러가_발생한다`() { ... }

2. Given-When-Then 구조 사용

@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)
}

3. 하나의 테스트는 하나의 목적만

// ❌ 여러 가지를 한꺼번에 검증
@Test
fun `회원가입과_로그인_테스트`() { ... }

// ✅ 목적별로 분리
@Test
fun `회원가입_성공_테스트`() { ... }

@Test
fun `회원가입_실패_테스트_중복된_이메일`() { ... }

@Test
fun `로그인_성공_테스트`() { ... }

4. 테스트는 독립적이고 반복 가능해야

각 테스트는 서로 영향을 주지 않고, 어떤 순서로 실행되어도 항상 동일한 결과를 내야 합니다.

@Transactional  // 테스트 끝나면 DB 롤백
@SpringBootTest
class UserServiceTest {

    @BeforeEach
    fun setUp() {
        // 각 테스트 전 데이터 초기화
        userRepository.deleteAll()
    }
}

5. 구현이 아닌 '동작'을 테스트하라

내부 구현(private 메소드)이 아니라, 외부에 공개하는 기능(public 메소드)이 올바르게 동작하는지 테스트하세요.

내부 구현은 언제든 바뀔 수 있는 리팩토링 대상입니다. 내부 구현을 테스트하면, 리팩토링할 때마다 테스트 코드를 수정해야 하는 주객전도가 발생합니다.


자주 사용하는 테스트 도구 (Java/Kotlin)

공통 기반

도구설명
JUnit 5자바 진영의 표준 테스트 프레임워크. 거의 모든 기업이 사용
AssertJassertThat(결과).isEqualTo(기대값) 처럼 읽기 쉬운 검증문 작성

단위 테스트

도구설명
Mockito가짜 객체(Mock)를 만들어주는 라이브러리. Repository를 가짜로 만들어서 Service 로직만 테스트 가능
KotestKotlin 프로젝트용. 더 코틀린 친화적인 문법 제공

통합 테스트

도구설명
Spring Test & MockMvcSpring Boot의 사실상 표준. 실제 서버 없이 API 테스트
REST Assuredgiven().when().get("/api/users").then().statusCode(200) BDD 스타일 문법
Testcontainers최근 가장 주목받는 기술. 테스트 실행 시 Docker로 실제 DB를 띄워서 테스트

현대적인 테스트 스택 (빅테크/유니콘 추천)

JUnit 5 + AssertJ + Mockito + Spring Test + Testcontainers

특히 Testcontainers는 H2 같은 인메모리 DB가 아닌, 실제 운영 환경과 동일한 DB(MySQL, PostgreSQL)를 대상으로 테스트할 수 있어 신뢰도가 매우 높습니다.


면접 예상 질문

Q1. "테스트 코드를 작성하는 이유가 뭔가요?"

핵심 포인트: 시간 절약, 자신감, 문서화 세 가지를 언급하세요.

"테스트 코드는 당장은 시간이 들지만, 장기적으로는 시간을 절약해줍니다. 리팩토링할 때 자신감을 갖고 코드를 수정할 수 있고, 테스트 코드 자체가 기능의 명세서 역할을 합니다. 또한 배포 전에 잠재적 버그를 발견할 수 있어 운영 환경의 장애를 예방합니다."

Q2. "단위 테스트와 통합 테스트의 차이점은?"

핵심 포인트: 범위와 목적의 차이를 설명하세요.

"단위 테스트는 클래스나 메소드 단위로 독립적으로 테스트하고, Mock을 활용해 외부 의존성을 제거합니다. 통합 테스트는 여러 컴포넌트가 함께 동작하는지, 특히 DB 연동까지 포함해서 검증합니다. 단위 테스트는 빠르고 문제 위치 파악이 쉽고, 통합 테스트는 실제 환경과 가까운 자신감을 줍니다."

Q3. "테스트 코드 작성 시 가장 중요하게 생각하는 것은?"

핵심 포인트: 가독성, 독립성, 유지보수성을 언급하세요.

"첫째, 테스트 코드는 문서입니다. 다른 개발자가 읽고 기능을 이해할 수 있도록 Given-When-Then 구조로 명확하게 작성합니다. 둘째, 각 테스트는 독립적이어야 합니다. 실행 순서에 상관없이 동일한 결과를 내야 합니다. 셋째, 구현이 아닌 동작을 테스트해서 리팩토링 시 테스트 코드 수정을 최소화합니다."

Q4. "Testcontainers를 사용해보셨나요? 왜 사용하나요?"

핵심 포인트: H2와의 차이점, 신뢰도를 언급하세요.

"Testcontainers는 테스트 실행 시 Docker로 실제 DB를 띄워서 테스트합니다. H2 같은 인메모리 DB는 실제 DB와 SQL 문법이나 동작이 다를 수 있어서 '로컬에서는 됐는데 운영에서 안 된다'는 문제가 발생할 수 있습니다. Testcontainers를 사용하면 운영 환경과 동일한 DB로 테스트하므로 신뢰도가 높아집니다."


정리

  1. Swagger 테스트는 '자동화되지 않은 테스트'다 — 그 과정을 코드로 옮기는 것부터 시작하세요
  2. 테스트가 깨지는 건 귀찮은 일이 아니라 안전망이다 — 배포 전에 버그를 발견한 것
  3. 통합 테스트부터 시작하세요 — API별로 1~2개씩, 복잡한 로직만 단위 테스트 추가
  4. 좋은 테스트는 문서다 — Given-When-Then 구조, 서술적 메소드명, 하나의 목적
  5. 현대적 스택 — JUnit 5 + AssertJ + Mockito + Spring Test + Testcontainers

"테스트 코드 짤 시간이 없어요"라는 말, 사실은 "테스트 코드가 없어서 더 많은 시간을 쓰고 있다" 는 의미일 수 있습니다.


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


👉 팀그릿 더 알아보기


#테스트코드 #JUnit #SpringTest #백엔드면접 #QueryDaily

profile
우리는 당신의 가능성을 믿는 사람들입니다. '되는 사람'이 되는 방법을 이야기합니다.

0개의 댓글