Go 시니어 테스트·코드품질 기술면접
새 면접Q. 대규모 Go 마이크로서비스 프로젝트에서 테스트 피라미드(Test Pyramid)를 구성할 때, 단위 테스트, 통합 테스트, E2E 테스트의 적정 비율과 각 계층의 테스트 범위를 어떻게 설정하시겠습니까? 특히 외부 의존성이 많은 마이크로서비스 환경에서 테스트 실행 시간과 안정성 사이의 균형을 맞추기 위한 전략을 구체적으로 설명해주세요.
테스트 비용, 피드백 속도, 커버리지의 트레이드오프를 고려하고, 각 계층에서 검증해야 할 것과 하지 말아야 할 것을 명확히 구분하세요.
일반적으로 70% 단위 테스트, 20% 통합 테스트, 10% E2E 테스트 비율을 권장하지만, 마이크로서비스 특성상 통합 테스트 비중을 25-30%까지 높입니다. 단위 테스트는 비즈니스 로직과 알고리즘에 집중하며, 외부 의존성은 모두 mock/stub으로 처리합니다. 통합 테스트는 실제 DB, 메시지 큐와의 상호작용을 검증하되 testcontainer를 활용해 격리된 환경에서 실행합니다. E2E 테스트는 핵심 사용자 시나리오만 선별하여 CI/CD 파이프라인에서 병렬 실행합니다. 테스트 실행 시간 단축을 위해 통합 테스트는 build tag로 분리하고, 개발자 로컬에서는 단위 테스트만 빠르게 실행하도록 구성합니다. 또한 contract testing(Pact 등)을 도입해 서비스 간 인터페이스 검증을 통합 테스트에서 분리하여 독립적으로 실행 가능하게 합니다.
- • 70-20-10 비율을 마이크로서비스 특성에 맞게 조정
- • testcontainer로 통합 테스트 격리
- • build tag로 테스트 계층 분리
- • contract testing 도입으로 서비스 간 의존성 검증
CI/CD 파이프라인에서 테스트 실행 시간이 30분 이상 소요되어 배포 속도가 느려질 때 테스트 계층을 재구성하여 피드백 루프를 단축합니다.
통합 테스트에서 데이터베이스 트랜잭션 롤백 전략과 테스트 데이터 격리 방법을 어떻게 구현하시겠습니까?
Q. 레거시 Go 코드베이스에 TDD(Test-Driven Development)를 점진적으로 도입하려고 합니다. 테스트가 전혀 없는 기존 코드에 대해 어떤 순서와 전략으로 테스트를 추가하고 리팩토링을 진행하시겠습니까? 특히 데이터베이스 접근 로직과 비즈니스 로직이 강하게 결합된 코드를 테스트 가능하게 만드는 구체적인 방법을 설명해주세요.
Characterization Test로 기존 동작을 보호하고, 의존성 주입으로 결합도를 낮추는 단계적 접근을 고려하세요.
먼저 Golden Master Testing 또는 Characterization Test를 작성해 현재 시스템의 동작을 있는 그대로 캡처하여 리팩토링 중 회귀를 방지합니다. 다음으로 Strangler Fig 패턴을 적용해 새로운 기능은 TDD로 개발하고, 기존 코드는 수정이 필요할 때만 테스트를 추가합니다. 데이터베이스 결합 코드는 Repository 패턴으로 분리하고 인터페이스를 도입해 비즈니스 로직을 순수 함수로 추출합니다. 의존성 주입을 위해 생성자 함수에서 인터페이스를 받도록 리팩토링하고, 단위 테스트에서는 mock을 주입합니다. 큰 함수는 Extract Method 리팩토링으로 작은 단위로 분해하여 각각 테스트를 작성합니다. 팀 전체가 TDD에 익숙하지 않다면 페어 프로그래밍과 코드 리뷰를 통해 학습 곡선을 완화하고, 테스트 커버리지 목표를 단계적으로 높여갑니다(예: 신규 코드 80% → 전체 60% → 전체 80%).
- • Characterization Test로 기존 동작 보호
- • Strangler Fig 패턴으로 점진적 전환
- • Repository 패턴으로 DB 로직 분리
- • 의존성 주입으로 테스트 가능성 확보
테스트 없는 레거시 시스템을 유지보수하면서 버그가 반복적으로 발생할 때 안전한 리팩토링을 위해 테스트를 점진적으로 추가합니다.
TDD를 적용했을 때 개발 속도가 느려진다는 팀원의 반대에 어떻게 대응하시겠습니까?
Q. Go에서 Mock, Stub, Fake, Spy의 차이를 설명하고, 각각 어떤 상황에서 사용하는 것이 적절한지 구체적인 예시와 함께 설명해주세요. 또한 testify/mock, gomock 같은 mocking 라이브러리를 사용하는 것과 수동으로 인터페이스 구현체를 만드는 것의 장단점을 비교해주세요.
각 테스트 더블의 검증 목적과 복잡도를 고려하여 상황에 맞는 선택 기준을 제시하세요.
Mock은 호출 검증에 집중하며 특정 메서드가 정확한 인자로 호출되었는지 확인할 때 사용합니다(예: 이메일 발송 서비스). Stub은 미리 정의된 응답을 반환하며 외부 API 응답을 시뮬레이션할 때 사용합니다. Fake는 실제 동작하는 간단한 구현체로 인메모리 DB처럼 동작하며 통합 테스트에서 유용합니다. Spy는 실제 객체를 감싸서 호출을 기록하며 부분적 검증이 필요할 때 사용합니다. testify/mock이나 gomock은 복잡한 인터페이스의 mock을 빠르게 생성하고 호출 검증 코드를 간결하게 작성할 수 있지만, 테스트가 구현 세부사항에 결합되어 리팩토링 시 깨지기 쉽습니다. 수동 구현은 코드가 길어지지만 필요한 동작만 구현하여 테스트 의도가 명확하고 유지보수가 쉽습니다. 일반적으로 간단한 인터페이스는 수동으로, 복잡한 인터페이스는 라이브러리를 사용하되 행위 검증보다 상태 검증을 우선합니다.
- • Mock은 호출 검증, Stub은 응답 제공, Fake는 간단한 실제 구현
- • 라이브러리는 편리하지만 구현 결합도 증가
- • 상태 검증이 행위 검증보다 안정적
- • 간단한 인터페이스는 수동 구현 선호
결제 게이트웨이 같은 외부 서비스 연동 코드를 테스트할 때 실제 API 호출 없이 다양한 응답 시나리오를 검증합니다.
외부 HTTP API를 호출하는 클라이언트 코드를 테스트할 때 httptest 패키지를 어떻게 활용하시겠습니까?
Q. 시니어 개발자로서 팀의 코드 리뷰 문화를 개선하려고 합니다. 효과적인 코드 리뷰를 위한 체크리스트와 리뷰어/작성자의 역할을 정의하고, 리뷰가 형식적이거나 지나치게 오래 걸리는 문제를 해결하기 위한 구체적인 방안을 제시해주세요. 또한 Go 언어 특성을 고려한 코드 리뷰 포인트도 함께 설명해주세요.
리뷰의 목적을 명확히 하고, 자동화 가능한 부분과 사람이 판단해야 할 부분을 구분하세요.
코드 리뷰 체크리스트는 기능 정확성, 테스트 커버리지, 에러 처리, 성능, 보안, 가독성 순으로 우선순위를 둡니다. 작성자는 PR 설명에 변경 이유와 영향 범위를 명확히 작성하고 self-review로 명백한 실수를 먼저 제거합니다. 리뷰어는 24시간 내 1차 피드백을 제공하며, nitpick과 blocking issue를 구분합니다. 형식적 리뷰를 방지하기 위해 golangci-lint, gofmt, go vet을 CI에서 자동 실행하여 스타일 논쟁을 제거합니다. PR 크기는 400줄 이하로 제한하고, 큰 변경은 설계 리뷰를 먼저 진행합니다. Go 특화 포인트로는 error wrapping 일관성, context 전파, goroutine leak 가능성, race condition, 불필요한 포인터 사용을 중점 검토합니다. 팀 내 코딩 컨벤션 문서를 만들고 정기적으로 업데이트하며, 반복되는 리뷰 코멘트는 linter 규칙이나 문서로 전환합니다.
- • 자동화 가능한 스타일 검사는 CI로 처리
- • PR 크기 제한과 명확한 설명 필수
- • Go 특화 검토 항목 정의
- • 리뷰 우선순위와 응답 시간 가이드 설정
코드 리뷰가 병목이 되어 배포가 지연되거나, 형식적인 LGTM만 받고 버그가 프로덕션에 배포되는 문제를 개선할 때 적용합니다.
주니어 개발자의 PR에서 구조적 문제를 발견했을 때, 어떻게 피드백을 제공하여 학습 기회로 만들 수 있을까요?
Q. 테스트 커버리지 지표(라인 커버리지, 브랜치 커버리지, 뮤테이션 커버리지)의 의미와 한계를 설명하고, 커버리지 80%를 달성했지만 여전히 버그가 자주 발생하는 상황에서 테스트 품질을 실질적으로 개선하기 위한 방법을 제시해주세요. Go에서 go test -cover 외에 활용할 수 있는 커버리지 분석 도구와 기법도 함께 설명해주세요.
커버리지는 필요조건이지 충분조건이 아니며, 테스트의 질적 측면을 함께 고려해야 합니다.
라인 커버리지는 실행된 코드 줄을 측정하지만 모든 분기나 조건을 검증하지 않습니다. 브랜치 커버리지는 if/else 같은 분기를 모두 테스트했는지 확인하며 더 엄격합니다. 뮤테이션 커버리지는 코드를 의도적으로 변경했을 때 테스트가 실패하는지 확인하여 테스트의 실효성을 검증합니다. 높은 커버리지에도 버그가 발생하는 이유는 edge case 미검증, assertion 부족, 통합 지점 미테스트 때문입니다. 개선 방법으로 경계값 분석(boundary value analysis)과 등가 분할(equivalence partitioning)을 적용하고, property-based testing(gopter)으로 예상치 못한 입력을 테스트합니다. go-mutesting으로 뮤테이션 테스트를 실행하여 테스트의 견고성을 확인합니다. 커버리지 목표는 팀 합의로 정하되, 핵심 비즈니스 로직은 95% 이상, 유틸리티 코드는 70% 이상으로 차등 적용합니다. codecov 같은 도구로 PR별 커버리지 변화를 추적하고, 커버리지가 감소하는 PR은 자동으로 차단합니다.
- • 커버리지 지표는 필요조건이지 충분조건 아님
- • 경계값 분석과 property-based testing으로 보완
- • 뮤테이션 테스트로 테스트 품질 검증
- • 핵심 로직과 유틸리티 코드 차등 목표 설정
커버리지 80%를 달성했지만 프로덕션에서 null pointer dereference 같은 기본적인 버그가 발생할 때 테스트 전략을 재검토합니다.
레거시 코드에서 테스트하기 어려운 부분의 커버리지를 강제로 높이는 것과 핵심 로직만 집중 테스트하는 것 중 어느 전략을 선택하시겠습니까?
Q. 1000줄이 넘는 복잡한 Go 함수를 리팩토링해야 합니다. 여러 책임이 섞여 있고, 전역 변수에 의존하며, 에러 처리가 일관되지 않고, 테스트가 없는 상태입니다. 안전하게 리팩토링을 진행하기 위한 단계별 전략과 각 단계에서 적용할 리팩토링 기법을 구체적으로 설명해주세요. 특히 리팩토링 중 기능이 깨지지 않도록 보장하는 방법을 포함해주세요.
테스트 작성 → 작은 단위로 분해 → 의존성 제거 순서로 진행하며, 각 단계마다 테스트로 검증하세요.
첫 단계로 현재 함수의 입출력을 기준으로 Approval Test를 작성하여 다양한 입력에 대한 현재 동작을 캡처합니다. 다음으로 Extract Method 리팩토링으로 의미 있는 작은 함수들을 추출하되, 한 번에 하나씩만 추출하고 테스트를 실행하여 동작이 유지되는지 확인합니다. 전역 변수 의존성은 함수 파라미터로 명시적으로 전달하도록 변경하고, 의존성 주입 패턴을 적용합니다. 에러 처리는 Go 1.13 error wrapping으로 통일하고, sentinel error나 custom error type으로 명확히 정의합니다. 추출된 작은 함수들은 각각 단위 테스트를 작성하여 독립적으로 검증합니다. 복잡한 조건문은 Early Return 패턴으로 중첩을 줄이고, 매직 넘버는 상수로 추출합니다. SRP(Single Responsibility Principle)를 적용해 각 함수가 하나의 책임만 갖도록 분리하며, 최종적으로 원래 함수는 여러 작은 함수를 조합하는 orchestrator 역할만 하도록 만듭니다. 모든 리팩토링은 작은 커밋으로 나누어 진행하고 각 커밋마다 테스트를 통과시킵니다.
- • Approval Test로 현재 동작 보호
- • Extract Method로 점진적 분해
- • 전역 변수를 파라미터로 전환
- • 작은 커밋과 지속적 테스트 실행
레거시 시스템의 핵심 비즈니스 로직을 수정해야 하는데 코드가 너무 복잡하여 변경 영향도를 파악할 수 없을 때 안전한 리팩토링을 진행합니다.
리팩토링 중 기존 동작이 변경되어야 하는 버그를 발견했다면 어떻게 처리하시겠습니까?
Q. Go 마이크로서비스의 통합 테스트를 설계할 때, PostgreSQL, Redis, Kafka 같은 외부 의존성을 어떻게 다루시겠습니까? testcontainers-go를 활용한 컨테이너 기반 테스트와 실제 인프라를 사용하는 방법의 장단점을 비교하고, 테스트 데이터 준비(fixture), 테스트 간 격리, 병렬 실행, CI 환경에서의 실행 전략을 포함한 전체 설계를 제시해주세요.
테스트 신뢰성, 실행 속도, 유지보수 비용의 균형을 고려하여 환경별로 다른 전략을 적용할 수 있습니다.
testcontainers-go를 사용하면 개발자 로컬과 CI 환경에서 동일한 테스트를 실행할 수 있고, 테스트마다 깨끗한 상태의 의존성을 제공받아 격리가 보장됩니다. 단점은 컨테이너 시작 시간이 추가되고 Docker 환경이 필요하다는 것입니다. 실제 인프라 사용은 프로덕션과 동일한 환경에서 테스트하지만 테스트 간 간섭과 데이터 정리가 복잡합니다. 권장 전략은 로컬과 CI에서는 testcontainers를 사용하고, staging 환경에서는 실제 인프라로 E2E 테스트를 실행합니다. 테스트 데이터는 test fixture 파일(JSON/YAML)로 관리하거나 factory 패턴으로 생성하며, 각 테스트 시작 시 seed 데이터를 주입합니다. 테스트 격리는 각 테스트마다 별도 데이터베이스 스키마나 네임스페이스를 사용하거나, 트랜잭션을 시작하고 테스트 후 롤백합니다. 병렬 실행은 t.Parallel()을 사용하되 공유 리소스 접근은 뮤텍스로 보호합니다. CI에서는 testcontainers 이미지를 미리 pull하여 시작 시간을 단축하고, 테스트를 패키지별로 분산 실행합니다.
- • testcontainers로 로컬/CI 환경 통일
- • 테스트마다 격리된 데이터베이스 스키마 사용
- • fixture 파일 또는 factory 패턴으로 데이터 준비
- • 병렬 실행과 이미지 캐싱으로 속도 개선
로컬에서는 통과하지만 CI에서 간헐적으로 실패하는 통합 테스트의 불안정성을 해결하기 위해 환경 일관성을 확보합니다.
Kafka 같은 메시지 큐를 사용하는 비동기 처리 로직의 통합 테스트에서 메시지 전달을 어떻게 검증하시겠습니까?
아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요!