테스트와 품질 (Testing & Quality)

목차


테스트와 품질을 왜 같이 보나

백엔드 면접에서 테스트는 단순히 “assert를 쓸 줄 아는가”를 묻는 주제가 아닙니다.

보통은 다음을 같이 봅니다.

  • 변경 안전성: 기능을 고친 뒤 기존 동작이 깨졌는지 빨리 알 수 있는가
  • 설계 품질: 테스트하기 쉬운 구조로 모듈이 분리되어 있는가
  • 운영 관점: 배포 전에 어떤 위험을 줄였는가
  • 우선순위 판단: 모든 것을 다 테스트하려 하지 않고, 중요한 경로를 골라 검증하는가

즉, 테스트는 품질을 측정하는 도구이면서 동시에 품질 좋은 구조를 유도하는 장치이기도 합니다.


단위 테스트, 통합 테스트, 계약 테스트

테스트 종류는 보통 “무엇을 검증하는가”와 “어디까지 실제 의존성을 포함하는가”로 나눠서 설명하면 좋습니다.

테스트 종류 주 검증 대상 장점 주의점
단위 테스트 (Unit Test) 함수, 클래스, 작은 모듈 빠르고 실패 원인 파악이 쉬움 외부 시스템과의 실제 연결 문제는 놓칠 수 있음
통합 테스트 (Integration Test) DB, 메시지 브로커, 외부 API 연동 실제 연결 문제를 조기에 발견 느리고 환경 관리 비용이 큼
계약 테스트 (Contract Test) 서비스 간 요청/응답 계약 팀 간 인터페이스 깨짐을 조기에 탐지 계약 범위와 버전 관리가 필요

단위 테스트

단위 테스트는 가장 작은 동작 단위를 빠르게 검증하는 테스트입니다.

  • 잘 맞는 대상: 순수 함수, 도메인 규칙, 검증 로직, 계산 로직
  • 장점: 실행이 빠르고, 실패 지점을 찾기 쉽습니다
  • 주의점: DB 연결, 직렬화 포맷, 실제 네트워크 오류는 잘 드러나지 않습니다

통합 테스트

통합 테스트는 여러 컴포넌트를 실제에 가깝게 붙여서 검증합니다.

  • 잘 맞는 대상: ORM 쿼리, 트랜잭션, 캐시 연동, 메시지 발행/소비
  • 장점: 실환경에 가까운 결함을 잡을 수 있습니다
  • 주의점: 속도가 느리고, 테스트 데이터와 환경 격리가 중요합니다

계약 테스트

계약 테스트는 서비스 간 인터페이스가 약속한 형태를 유지하는지 검증합니다.

  • 잘 맞는 대상: REST API, 이벤트 스키마, gRPC 인터페이스
  • 장점: 프론트엔드-백엔드, 서비스 A-B 간의 호환성 문제를 줄일 수 있습니다
  • 주의점: 계약 문서와 실제 구현이 같이 관리되지 않으면 금방 낡습니다

좋은 답변은 세 종류를 단순 정의로 끊지 않고,
빠른 피드백은 단위 테스트에서 얻고, 실제 연동 위험은 통합/계약 테스트로 보완한다는 흐름으로 설명하는 답변입니다.


테스트 더블과 목 객체

테스트 더블(Test Double)은 실제 의존성을 대신하는 대체물입니다.

종류를 너무 세세하게 외우기보다, 왜 쓰는지와 언제 과한지 설명하는 편이 좋습니다.

  • Dummy: 자리를 채우기만 하는 값
  • Stub: 미리 정한 값을 돌려주는 대체 객체
  • Fake: 간단한 메모리 기반 구현처럼 실제를 단순화한 객체
  • Mock: 어떤 호출이 일어났는지 상호작용까지 검증하는 객체
  • Spy: 호출 여부나 인자를 기록하는 객체

면접에서는 특히 Stub/Fake/Mock의 차이를 물어볼 수 있습니다.

  • Stub: 결과를 고정해서 제어하고 싶을 때 적합
  • Fake: 실제에 가까운 흐름을 가볍게 검증할 때 적합
  • Mock: 특정 메서드 호출 여부 자체가 중요할 때 적합

주의할 점도 분명합니다.

  • 장점: 외부 시스템 없이 빠르게 테스트 가능
  • 장점: 실패 상황을 의도적으로 만들기 쉬움
  • 단점: 내부 구현 세부사항에 너무 강하게 묶이면 리팩터링에 약해짐
  • 단점: Mock이 많아질수록 테스트가 설계보다 상호작용 검증에 치우칠 수 있음

실무에서는 “Mock을 얼마나 잘 쓰는가”보다
어디까지 가짜로 대체해도 되고, 어디서부터는 실제 연동을 봐야 하는가를 판단하는 쪽이 더 중요합니다.


회귀 테스트와 품질 신호

회귀 테스트(Regression Test)는 기존에 되던 기능이 나중 변경으로 깨지지 않았는지 확인하는 테스트입니다.

보통 다음 영역에서 중요합니다.

  • 버그 재발 방지: 한번 장애가 난 시나리오는 테스트로 고정
  • 핵심 사용자 경로 보호: 로그인, 결제, 주문, 배치 처리 같은 핵심 흐름 보호
  • 리팩터링 안전성: 내부 구조를 바꿔도 외부 동작은 유지되는지 확인

품질을 볼 때 테스트 개수만 세는 것은 충분하지 않습니다. 함께 봐야 할 신호가 있습니다.

  • 실패율: 배포 전에 실패하는 테스트가 얼마나 자주 나오는가
  • 플레이크 비율: 같은 코드인데 테스트가 불안정하게 흔들리는가
  • 실행 시간: 피드백이 너무 늦어져 개발 흐름을 막고 있지 않은가
  • 핵심 경로 커버: 중요한 비즈니스 흐름을 정말 보호하고 있는가
  • 장애 후 보강: 실제 장애 원인이 테스트에 반영되는가

코드 커버리지(Coverage)는 참고 신호가 될 수 있지만, 품질 자체를 보장하지는 않습니다.

  • 의미 있는 점: 전혀 검증되지 않는 큰 공백을 찾는 데는 유용함
  • 한계: 분기와 예외 케이스를 제대로 검증하지 않아도 숫자는 높게 나올 수 있음

면접에서는 “커버리지를 몇 퍼센트로 맞춘다”보다
핵심 경로, 자주 깨지는 경계, 장애 재발 방지 케이스를 우선 보호한다는 식으로 설명하는 편이 좋습니다.


무엇을 어떤 층에서 검증할까

백엔드 시스템은 모든 검증을 한 층에 몰아넣기보다, 각 층에 맞는 책임을 나눠 두는 편이 효율적입니다.

주 검증 대상 적합한 테스트
도메인 로직 계산, 상태 전이, 정책 규칙 단위 테스트
애플리케이션 서비스 유스케이스 조합, 트랜잭션 경계 단위 테스트 + 일부 통합 테스트
인프라 계층 DB, 캐시, 메시지 브로커, 외부 API 통합 테스트
서비스 간 인터페이스 요청/응답 스키마, 이벤트 포맷 계약 테스트
사용자 흐름 핵심 시나리오 전체 소수의 E2E 테스트

예를 들어:

  • 할인 정책 계산은 단위 테스트가 잘 맞습니다.
  • 주문 생성 시 트랜잭션과 DB 제약은 통합 테스트가 더 적합합니다.
  • 외부 결제 서비스 응답 포맷은 계약 테스트로 깨짐을 빨리 잡는 편이 좋습니다.

즉, 질문의 핵심은 “테스트 종류를 많이 아는가”가 아니라
어떤 위험을 어디서 가장 싸게 검증할 수 있는가입니다.


테스트 비용과 트레이드오프

테스트는 많을수록 무조건 좋은 것이 아니라, 비용 구조를 같이 봐야 합니다.

  • 단위 테스트: 빠르고 싸지만 실제 연동 결함은 놓칠 수 있음
  • 통합 테스트: 더 현실적이지만 느리고 유지 비용이 큼
  • E2E 테스트: 가장 사용자 흐름에 가깝지만 가장 느리고 깨지기 쉬움

좋은 테스트 전략은 보통 다음 균형을 지향합니다.

  • 빠른 피드백: 대부분의 로직은 단위 테스트로 보호
  • 핵심 연동 검증: DB, 메시지, 외부 API는 통합 테스트로 보완
  • 최소한의 핵심 시나리오: E2E는 소수만 유지

이때 흔한 실수는 다음과 같습니다.

  • 모든 것을 E2E로 풀려는 경우: 느리고 원인 파악이 어려워짐
  • 단위 테스트만 많은 경우: 실제 환경 문제를 놓침
  • Mock에 과도하게 의존하는 경우: 내부 구현 바꾸면 테스트가 같이 깨짐

면접에서는 “테스트를 많이 작성합니다”보다
빠른 테스트와 실제 연동 검증을 어떻게 분리하는지를 말할 수 있어야 답변이 단단해집니다.


코드 예시

아래 예시는 저장소를 Fake로 대체해 서비스 로직을 단위 테스트하는 간단한 패턴입니다.

from dataclasses import dataclass


@dataclass
class Order:
    user_id: int
    amount: int


class FakeOrderRepository:
    def __init__(self) -> None:
        self.saved = []

    def save(self, order: Order) -> None:
        self.saved.append(order)


class OrderService:
    def __init__(self, order_repository: FakeOrderRepository) -> None:
        self.order_repository = order_repository

    def create_order(self, user_id: int, amount: int) -> Order:
        if amount <= 0:
            raise ValueError("amount must be positive")

        order = Order(user_id=user_id, amount=amount)
        self.order_repository.save(order)
        return order


def test_create_order_saves_valid_order():
    repository = FakeOrderRepository()
    service = OrderService(repository)

    order = service.create_order(user_id=1, amount=1000)

    assert order.amount == 1000
    assert repository.saved == [order]

이 예시에서 중요한 포인트는 다음과 같습니다.

  • 실제 DB 없이도 핵심 비즈니스 규칙을 빠르게 검증할 수 있습니다.
  • 저장 여부를 확인하려고 무거운 통합 환경을 띄우지 않아도 됩니다.
  • 다만 실제 SQL, 트랜잭션, 제약 조건은 별도의 통합 테스트가 필요합니다.

면접 포인트

  • 단위 테스트, 통합 테스트, 계약 테스트는 경쟁 관계가 아니라 서로 다른 위험을 나눠서 검증하는 관계입니다.
  • 테스트 더블은 속도를 높여 주지만, 실제 연동 문제를 모두 대체하지는 못합니다.
  • 회귀 테스트는 실제 장애나 버그를 다시 못 일으키게 고정하는 장치로 설명하는 편이 좋습니다.
  • 커버리지는 보조 지표일 뿐이고, 핵심 경로 보호와 장애 재발 방지가 더 중요한 품질 신호입니다.
  • 좋은 답변은 무엇을 어떤 층에서 왜 검증하는지, 그리고 그 비용이 얼마인지 함께 설명합니다.

참고 자료