Showing Posts From
테스트코드
- 28 Dec, 2025
자동화 테스트의 실행 시간이 1시간이 넘어갔을 때
자동화 테스트가 1시간 넘어간 날 아침 9시, 빨간 불 출근했다. 슬랙 알림이 12개다. "Test failed after 1h 42m" 젠킨스 파이프라인이 또 터졌다. 1시간 42분 돌다가 실패. 개발자들 머지 블록당했다. PM이 물어본다. "언제 고쳐지나요?" 모르겠다. 일단 커피부터.작년만 해도 20분이었다. 지금은 1시간 반. 테스트 케이스가 늘어날 때마다 시간도 늘었다. 선형적으로. 문제는 명확했다. 직렬 실행. 800개 테스트가 하나씩 돌아간다. 병렬 처리? 없다. 웨이트 타임? 넘친다. time.sleep(5) 가 237군데. 계산해봤다. 237 × 5초 = 1,185초. 20분이 그냥 기다리는 시간이다. 병렬화 시작 점심 먹고 작업 시작했다. 먼저 pytest-xdist 설치. pytest -n 44개 워커로 돌렸다. 기대했다. 1시간 반이면 20분 되겠지. 결과: 1시간 10분. 왜? 테스트 간 의존성. 3번 테스트가 1번 결과를 쓴다. 병렬로 돌면 깨진다. 격리가 안 돼 있었다. 2시간 걸려서 테스트 12개 수정. DB 픽스처를 각 테스트마다 독립적으로. 공유 상태 제거. 다시 돌렸다. pytest -n 8 --dist loadgroup8개 워커. 결과: 35분. 절반 넘게 줄었다. 개발자가 슬랙에 썸즈업 보냈다.웨이트 지옥 탈출 다음은 대기 시간. time.sleep(5) 를 전부 찾았다. 237개. 복붙의 역사다. # Before driver.click() time.sleep(5) assert element.is_displayed()# After driver.click() WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, "result")) )명시적 대기로 바꿨다. 조건 충족하면 바로 넘어간다. 5초 안 기다려도 된다. 평균 대기 시간이 5초에서 1.2초로 줄었다. 237개 × 3.8초 = 900초 절약. 15분이다. 실행 시간: 20분. 1시간 반에서 20분. 4.5배 빨라졌다. 테스트 선택 실행 그런데 문제가 있었다. PR 하나 올릴 때마다 800개 전부 돌린다. API 하나 고쳤는데 UI 테스트까지 돈다. 비효율이다. 테스트 태깅 시작했다. @pytest.mark.api def test_login_api(): pass@pytest.mark.ui def test_login_ui(): pass@pytest.mark.critical def test_payment(): pass파이프라인 3개로 분리.PR: critical만 (120개, 5분) Merge: api + critical (350개, 12분) Nightly: 전체 (800개, 20분)개발자들 피드백 속도가 확 빨라졌다. PR 올리고 5분이면 결과 나온다.여전히 느린 것들 20분도 여전히 길다는 얘기가 나왔다. 프로파일링 돌렸다. pytest-profiling 설치. pytest --profile결과 보고 놀랐다.test_bulk_upload: 3분 22초 (전체의 16%) test_data_migration: 2분 48초 test_report_generation: 2분 15초3개가 8분을 먹는다. 이건 E2E가 아니라 통합 테스트였다. 실제 10만 건 데이터로 돌리고 있었다. 분리했다. 통합 테스트는 nightly만. E2E는 샘플 데이터 100건으로. 실행 시간: 12분. 캐시와 재사용 다음 최적화. 셋업 시간. 매 테스트마다 브라우저 새로 띄운다. 로그인한다. 메인 페이지 간다. 매번. @pytest.fixture(scope="session") def authenticated_session(): driver = webdriver.Chrome() driver.get("https://app.test") login(driver) yield driver driver.quit()세션 스코프 픽스처. 로그인 한 번만 한다. 800번이 1번으로. 단, 조심해야 한다. 상태 오염. 각 테스트 전에 초기화 필요. @pytest.fixture(autouse=True) def reset_state(authenticated_session): authenticated_session.delete_all_cookies() authenticated_session.get("https://app.test/dashboard")효과: 2분 단축. 실행 시간: 10분. 도커 이미지도 캐싱했다. 테스트 환경 띄우는 데 3분 걸렸는데, 레이어 캐싱으로 30초로 줄었다. Flaky 테스트 지옥 빨라지니까 다른 문제가 보였다. 간헐적 실패. Flaky 테스트. 10번 돌리면 1번 실패한다. 병렬 실행하면서 더 자주 보인다. 타이밍 이슈, 레이스 컨디션. pytest --reruns 3 --reruns-delay 1실패하면 3번 재시도. 그런데 근본 해결은 아니다. 진짜 문제를 찾아야 했다. 일주일간 로그 분석.네트워크 타임아웃: 15건 엘리먼트 로드 타이밍: 23건 DB 트랜잭션 충돌: 8건하나씩 고쳤다. 명시적 대기 추가. 리트라이 로직. DB 격리. Flaky 비율: 12% → 2%. 안정화되니까 신뢰도가 올라갔다. 개발자들이 테스트 결과를 믿는다. 비용 계산 최적화 전후 비교했다. Before:실행 시간: 1시간 42분 하루 10회 실행 EC2 비용: 월 200달러 개발자 대기 시간: 하루 17시간After:실행 시간: 10분 하루 30회 실행 (더 자주 돌림) EC2 비용: 월 80달러 (병렬이지만 짧아서) 개발자 대기 시간: 하루 5시간월 120달러 절약. 더 중요한 건 개발자 12시간 절약. 한 달이면 240시간. 10명이면 2,400시간. 돈으로 환산하면 1억 넘는다. 모니터링 대시보드 지표 추적 시작했다. Grafana 대시보드 만들었다. 매일 확인한다.평균 실행 시간 P95 실행 시간 (상위 5% 느린 케이스) Flaky 비율 병렬화 효율 (이론상 시간 대비 실제) 테스트당 평균 시간그래프가 튀면 조사한다. 느린 테스트 주간 리포트. 팀 회의 때 공유. 개발자들이 테스트 성능도 신경 쓰기 시작했다. "이거 테스트 오래 걸리겠는데요?" 미리 물어본다. 문화가 바뀌었다. 다음 목표 10분도 길다. 다음 최적화 계획:테스트 샤딩 (여러 머신에 분산) 스마트 테스트 선택 (변경된 코드만) 시각적 회귀 테스트 병렬화 더 빠른 브라우저 (headless Chrome → Playwright)목표는 5분. PR 피드백 5분 이내. CTO가 물었다. "테스트를 줄일 수는 없나요?" 아니다. 품질은 타협 못 한다. 대신 빠르게 돌린다. 테스트 800개 유지. 실행 시간만 줄인다. 배운 것 1시간 반이 10분 됐다. 10배 빨라졌다. 한 번에 된 게 아니다. 2달 걸렸다. 매주 조금씩. 병렬화가 가장 효과 컸다. 4.5배. 단, 격리가 전제다. 명시적 대기가 두 번째. 불필요한 시간 제거. 15분 절약. 테스트 선택이 세 번째. 전부 돌릴 필요 없다. 상황에 맞게. 프로파일링이 핵심이다. 추측 말고 측정. 데이터로 판단. 지속적 관리가 필요하다. 한 번 최적화하고 끝이 아니다. 매주 모니터링. 가장 중요한 건, 빠른 피드백이 개발 속도를 올린다는 것. 테스트가 느리면 개발자들이 안 돌린다. 빠르면 자주 돌린다. 자동화의 가치는 속도다. 느린 자동화는 가치가 반감된다.오늘도 테스트는 10분 만에 끝났다. 예전엔 점심 먹고 와도 안 끝났는데.
- 21 Dec, 2025
API 자동화 테스트: UI 테스트보다 빠르고 안정적이다
API 자동화 테스트: UI 테스트보다 빠르고 안정적이다 E2E 테스트가 또 깨졌다 월요일 아침 10시. 출근하자마자 슬랙 알림 30개. "E2E 테스트 실패했어요." "CI 막혔는데요?" "배포 못 하나요?" 밤새 돌린 자동화 테스트. 50개 중 12개 실패. 로그 확인했다. 버튼 클릭 타임아웃. 페이지 로딩 느림. 셀레니움 드라이버 오류. 진짜 버그는 하나도 없다. E2E 테스트의 현실이다. Flaky 테스트. 불안정한 테스트. 실행할 때마다 결과가 다르다. 2년 전 내가 짠 UI 자동화 테스트. 커버리지 80% 목표로 만들었다. 지금은 유지보수가 더 큰 일이다. 개발자가 CSS 클래스 이름 바꾸면 테스트 10개 깨진다. 애니메이션 추가하면 대기 시간 조정해야 한다. 브라우저 업데이트되면 드라이버도 업데이트. 테스트 실행 시간도 문제다. 50개 테스트에 40분. CI에서 돌리면 배포가 느려진다. "이거 자동화 의미 있나요?" 개발팀장이 물었다. 할 말이 없었다.API 테스트로 갈아탔다 3개월 전. 테스트 전략을 바꿨다. E2E는 최소화. 핵심 시나리오만 10개. 나머지는 API 테스트로 대체했다. 테스트 피라미드. 이론으로만 알던 걸 실전에 적용했다. /\ UI (E2E) - 10개 / \ / API \ API - 150개 /______\ / Unit \ Unit - 500개 /__________\API 테스트가 중간 계층이다. 백엔드 로직 검증. UI 없이 빠르게. requests 라이브러리. Python으로 간단하다. import requestsresponse = requests.get('https://api.example.com/users/1') assert response.status_code == 200 assert response.json()['name'] == '홍길동'이게 전부다. 브라우저 띄울 필요 없다. 셀레니움 설치 안 해도 된다. 처음엔 개발자들이 의아해했다. "유저는 API 안 보는데요?" "UI 테스트가 더 실제랑 비슷하지 않나요?" 맞는 말이다. 하지만 목적이 다르다. E2E는 사용자 관점. 전체 플로우 확인. API 테스트는 비즈니스 로직 검증. 백엔드 동작 확인. 둘 다 필요하다. 비율이 문제다. 전에는 E2E 50개, API 20개. 지금은 E2E 10개, API 150개. 결과는 확실했다.속도가 다르다 API 테스트는 빠르다. E2E 1개 실행: 평균 48초 API 테스트 1개 실행: 평균 0.3초 160배 차이. 150개 API 테스트 실행 시간: 45초 전체 테스트 스위트 (E2E 10개 + API 150개): 9분 전에는 40분 걸렸다. 4배 빨라졌다. CI 파이프라인이 막히지 않는다. 개발자들이 PR 올리고 10분 안에 결과 본다. 배포 속도도 올랐다. 하루 3번 배포에서 10번으로. 속도만 빠른 게 아니다. 안정성도 다르다. E2E 테스트 성공률: 76% API 테스트 성공률: 99.2% 실패 원인도 명확하다. API 테스트가 실패하면 진짜 버그다.200이어야 하는데 500 JSON 필드 누락 비즈니스 로직 오류UI 타임아웃 같은 거 없다. 네트워크는 가끔 느리지만 재시도하면 된다. import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retrysession = requests.Session() retry = Retry(total=3, backoff_factor=0.5) adapter = HTTPAdapter(max_retries=retry) session.mount('https://', adapter)response = session.get('https://api.example.com/users')재시도 로직 3줄이면 된다. 셀레니움 암묵적 대기보다 확실하다. 개발자들 반응이 좋아졌다. "테스트 실패 알림 오면 진짜 봐야 할 것만 남았어요." "CI 빨라져서 피드백이 빠르네요." 내가 원한 게 이거다. 신뢰받는 자동화. 시나리오 커버리지 API 테스트로 뭘 검증하나. 우리 서비스는 전자상거래 플랫폼이다. 핵심 기능들. 1. 인증/인가 def test_로그인_성공(): response = requests.post( f'{BASE_URL}/auth/login', json={'email': 'test@test.com', 'password': 'password123'} ) assert response.status_code == 200 assert 'access_token' in response.json()def test_잘못된_비밀번호(): response = requests.post( f'{BASE_URL}/auth/login', json={'email': 'test@test.com', 'password': 'wrong'} ) assert response.status_code == 401 assert response.json()['error'] == 'Invalid credentials'2. 상품 조회 def test_상품_목록_조회(): response = requests.get(f'{BASE_URL}/products') assert response.status_code == 200 products = response.json() assert len(products) > 0 assert 'id' in products[0] assert 'name' in products[0] assert 'price' in products[0]def test_상품_상세_조회(): response = requests.get(f'{BASE_URL}/products/123') assert response.status_code == 200 product = response.json() assert product['id'] == 123 assert product['price'] > 03. 장바구니 def test_장바구니_추가(): token = get_auth_token() response = requests.post( f'{BASE_URL}/cart', headers={'Authorization': f'Bearer {token}'}, json={'product_id': 123, 'quantity': 2} ) assert response.status_code == 201def test_장바구니_조회(): token = get_auth_token() response = requests.get( f'{BASE_URL}/cart', headers={'Authorization': f'Bearer {token}'} ) assert response.status_code == 200 cart = response.json() assert cart['total_price'] > 04. 주문 def test_주문_생성(): token = get_auth_token() response = requests.post( f'{BASE_URL}/orders', headers={'Authorization': f'Bearer {token}'}, json={ 'items': [{'product_id': 123, 'quantity': 1}], 'address': '서울시 강남구', 'payment_method': 'card' } ) assert response.status_code == 201 order = response.json() assert order['status'] == 'pending' assert order['total_amount'] > 0이런 테스트들. 비즈니스 로직 검증. UI에서 하면 어떻게 되나.로그인 페이지 접속 (5초) 아이디/비밀번호 입력 (3초) 로그인 버튼 클릭 (2초) 메인 페이지 로딩 (4초) 상품 검색 (6초) 상품 클릭 (3초) 상세 페이지 로딩 (4초) 장바구니 추가 버튼 클릭 (2초) 장바구니 페이지 이동 (3초) 주문하기 버튼 클릭 (2초) 주문 정보 입력 (10초) 결제 버튼 클릭 (3초)총 47초. API로 하면 1.2초. 게다가 UI 테스트는 앞 단계가 실패하면 다음 검증을 못 한다. 로그인 실패하면 주문 테스트도 못 돌린다. API 테스트는 독립적이다. 각 엔드포인트 따로 검증.실전 구조 우리 API 테스트 구조. tests/ ├── api/ │ ├── auth/ │ │ ├── test_login.py │ │ ├── test_signup.py │ │ └── test_token.py │ ├── products/ │ │ ├── test_list.py │ │ ├── test_detail.py │ │ └── test_search.py │ ├── cart/ │ │ ├── test_add.py │ │ ├── test_update.py │ │ └── test_remove.py │ └── orders/ │ ├── test_create.py │ ├── test_list.py │ └── test_cancel.py ├── fixtures/ │ ├── auth.py │ └── data.py └── utils/ ├── api_client.py └── helpers.pyfixtures는 pytest fixture다. 재사용 가능한 설정. # fixtures/auth.py import pytest import requests@pytest.fixture def api_client(): return requests.Session()@pytest.fixture def auth_token(api_client): response = api_client.post( f'{BASE_URL}/auth/login', json={'email': 'test@test.com', 'password': 'test123'} ) return response.json()['access_token']@pytest.fixture def authorized_client(api_client, auth_token): api_client.headers.update({'Authorization': f'Bearer {auth_token}'}) return api_client이렇게 만들면 테스트가 간단해진다. def test_장바구니_조회(authorized_client): response = authorized_client.get(f'{BASE_URL}/cart') assert response.status_code == 200인증 로직 반복 안 해도 된다. utils는 공통 함수들. # utils/api_client.py class APIClient: def __init__(self, base_url): self.base_url = base_url self.session = requests.Session() def login(self, email, password): response = self.session.post( f'{self.base_url}/auth/login', json={'email': email, 'password': password} ) if response.status_code == 200: token = response.json()['access_token'] self.session.headers.update({'Authorization': f'Bearer {token}'}) return response def get(self, endpoint): return self.session.get(f'{self.base_url}{endpoint}') def post(self, endpoint, data): return self.session.post(f'{self.base_url}{endpoint}', json=data)이렇게 래핑하면 테스트가 더 읽기 쉽다. def test_주문_생성(): client = APIClient(BASE_URL) client.login('test@test.com', 'test123') response = client.post('/orders', { 'items': [{'product_id': 123, 'quantity': 1}], 'address': '서울시 강남구' }) assert response.status_code == 201중복 코드가 줄어든다. 데이터 관리 API 테스트의 어려움. 테스트 데이터. DB에 있는 데이터에 의존한다. 테스트마다 상태가 달라질 수 있다. 우리는 두 가지 방식을 쓴다. 1. 테스트 DB 초기화 각 테스트 전에 DB 리셋. @pytest.fixture(autouse=True) def reset_database(): # 테스트 전에 DB 초기화 db.reset() db.seed_test_data() yield # 테스트 후에 정리 db.cleanup()장점: 깨끗한 상태. 예측 가능. 단점: 느리다. DB 작업이 많으면 시간 걸림. 2. 동적 데이터 생성 테스트마다 고유 데이터 만들기. import uuiddef test_회원가입(): unique_email = f'test_{uuid.uuid4()}@test.com' response = requests.post( f'{BASE_URL}/auth/signup', json={ 'email': unique_email, 'password': 'test123', 'name': '테스터' } ) assert response.status_code == 201장점: 빠르다. 병렬 실행 가능. 단점: 데이터 쌓임. 정리 필요. 우리는 섞어서 쓴다. 읽기 작업은 시드 데이터. 쓰기 작업은 동적 생성. 그리고 테스트 후 정리. created_resources = []def test_상품_생성(): response = requests.post(f'{BASE_URL}/products', json={'name': '테스트상품'}) product_id = response.json()['id'] created_resources.append(('products', product_id)) assert response.status_code == 201@pytest.fixture(scope='session', autouse=True) def cleanup_resources(): yield # 모든 테스트 끝난 후 for resource_type, resource_id in created_resources: requests.delete(f'{BASE_URL}/{resource_type}/{resource_id}')완벽하진 않다. 하지만 관리 가능한 수준. 실패 시 디버깅 API 테스트가 실패하면 어떻게 하나. 로그가 중요하다. import logginglogging.basicConfig(level=logging.DEBUG)def test_주문_생성(): response = requests.post(f'{BASE_URL}/orders', json={'items': []}) logging.info(f'Request URL: {response.request.url}') logging.info(f'Request Body: {response.request.body}') logging.info(f'Response Status: {response.status_code}') logging.info(f'Response Body: {response.text}') assert response.status_code == 201실패하면 전체 요청/응답이 보인다. pytest는 실패한 테스트만 로그 보여준다. 성공하면 조용. 더 자세히 보려면 pytest 플러그인. # conftest.py import pytest@pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == 'call' and report.failed: # 실패한 테스트 정보 저장 if hasattr(item, 'funcargs'): for name, value in item.funcargs.items(): if hasattr(value, 'last_response'): print(f'\n마지막 응답: {value.last_response.text}')CI에서 돌릴 때는 Allure 리포트. pytest --alluredir=./allure-results allure serve ./allure-results예쁜 HTML 리포트. 요청/응답 전부. 스크린샷 대신 JSON. 디버깅이 E2E보다 쉽다. 브라우저 열 필요 없다. 로그만 보면 된다. 한계도 있다 API 테스트가 만능은 아니다. 못 잡는 버그들. 1. UI 버그버튼이 안 보임 CSS 깨짐 모바일 레이아웃 오류API는 정상이어도 화면은 문제일 수 있다. 2. 통합 이슈API는 각각 정상 하지만 함께 쓰면 문제예: 장바구니 API 정상. 결제 API 정상. 그런데 장바구니→결제 플로우에서 세션 유지 안 됨. 3. 타이밍 이슈비동기 처리 웹소켓 통신 실시간 업데이트API 테스트는 요청/응답만 본다. 실시간 상태 변화는 못 본다. 4. 브라우저 호환성Chrome은 되는데 Safari는 안 됨 이건 API 문제가 아니다그래서 E2E를 완전히 없앨 순 없다. 우리 전략.API 테스트: 비즈니스 로직, 백엔드 검증 E2E 테스트: 핵심 사용자 플로우 10개 시각 테스트: UI 변경 감지 (Percy 같은 툴) 모니터링: 실제 사용자 오류 추적 (Sentry)계층별로 역할 분담. CI 파이프라인 통합 Jenkins 파이프라인에 통합했다. pipeline { agent any stages { stage('Unit Tests') { steps { sh 'pytest tests/unit -v' } } stage('API Tests') { steps { sh 'pytest tests/api -v --junit-xml=results.xml' } post { always { junit 'results.xml' } } } stage('E2E Tests') { when { branch 'main' } steps { sh 'pytest tests/e2e -v' } } } }Unit → API → E2E 순서. API 실패하면 E2E 안 돌린다. 시간 절약. PR에서는 API까지만. E2E는 메인 브랜치에만. 병렬 실행도 한다. stage('API Tests') { parallel { stage('Auth Tests') { steps { sh 'pytest tests/api/auth -v' } } stage('Products Tests') { steps { sh 'pytest tests/api/products -v' } } stage('Orders Tests') { steps { sh 'pytest tests/api/orders -v' } } } }독립적인 테스트들이니까 가능하다. 실행 시간이 반으로 줄었다. 팀 설득 과정 처음엔 반대가 있었다. 개발팀장: "유저는 API 안 보잖아요." 프론트 개발자: "UI 테스트가 더 중요한 거 아닌가요?" QA 후배: "저는 셀레니움만 배웠는데..." 설득 방법.데이터로 보여줬다.E2E 실패율 24% API 실패율 0.8% 실행 시간 비교파일럿 프로젝트.인증 API만 먼저 자동화 2주 후 결과 공유 "이거 괜찮은데요?"점진적 적용.E2E 줄이고 API 늘리기 한 번에 바꾸지 않음교육.requests 라이브러리 세션 API 테스트 작성 가이드 페어 프로그래밍지금은 다들 만족한다. 개발자: "테스트 신뢰도 올라서 좋아요." QA 후배: "생각보다 쉽네요." 팀장: "배포 속도 빨라졌어요." 변화는 서서히. 유지보수 API 테스트도 코드다. 유지보수 필요하다. 관리 포인트. 1. 엔드포인트 변경 백엔드가 API 수정하면 테스트도 수정. # 전: /api/v1/products # 후: /api/v2/products# config.py에서 버전 관리 API_VERSION = 'v2' BASE_URL = f'https://api.example.com/api/{API_VERSION}'한 곳만 바꾸면 됨. 2. 응답 포맷 변경 JSON 구조 바뀌면 assertion 수정. # 전: {'user': {'name': '홍길동'}} # 후: {'data': {'user': {'name': '홍길동'}}}# 헬퍼 함수로 추상화 def get_user_name(response): return response.json()['data']['user']['name']assert get_user_name(response) == '홍길동'3. 인증 방식 변경 JWT에서 OAuth로 바꾸면 fixture 수정. @pytest.fixture def auth_token(): # JWT 방식 # response = login(email, password) # return response.json()['token'] # OAuth 방식 oauth = OAuth2Session(client_id) token = oauth.fetch_token(token_url) return token['access_token']4. 테스트 데이터 정리 쌓이는 데이터 주기적으로 삭제. # 매일 새벽 3시 0 3 * * * python scripts/cleanup_test_data.py월 1회 리뷰. 안 쓰는 테스트 삭제. 중복 테스트 합치기. 성과 숫자로 보는 변화. 전 (E2E 중심)테스트 개수: 50개 실행 시간: 40분 성공률: 76% 배포 횟수: 하루 3회 버그 탈출: 월 8건후 (API 중심)테스트 개수: 160개 (E2E 10 + API 150) 실행 시간: 9분 성공률: 98.5% 배포 횟수: 하루 10회 버그 탈출: 월 2건커버리지는 올리고 시간은 줄었다. 비용도 줄었다. CI 실행 시간 줄어서 인프라 비용 30% 감소. 개발자 만족도도 올랐다. 설문조사 결과. "테스트 신뢰한다": 45% → 89% "CI가 빠르다": 32% → 91% "테스트가 도움된다": 51% → 87% 내가 가장 만족하는 건. 야근이 줄었다. 전에는 E2E 실패 원인 찾느라 밤 늦게까지. 지금은 API 실패하면 5분 안에 원인 파악. Flaky 테스트 스트레스가 없어졌다. 추천 사항 API 테스트 시작하려면. 1. 작게 시작 한 번에 다 바꾸지 마라. 한 모듈부터. 인증 API 5개 테스트. 2주 돌려보기. 2. 개발자와 협업 API 문서 같이 보기. 엔드포인트 이해하기. Swagger 있으면 더 좋다. 3. 도구는 단순하게 requests만으로 충분하다. 복잡한 프레임워크 필요 없다. pytest + requests + 약간의 fixture. 4. CI 먼저 통합 로컬에서만 돌리지 마라. CI에서 자동 실행. 빠른 피드백이 핵심. 5. 문서화 API 테스트 작성 가이드 만들기. "이런 케이스는 이렇게" 예시. 6. 리팩토링 시간 확보 테스트도 코드다. 정리 시간 필요. 월 1회 리팩토링 데이.E2E는 느리고 불안정하다. API 테스트는 빠르고 안정적이다. 완전히 대체는 못 한다. 하지만 대부분을 대체할 수 있다. 테스트 피라미드 중간 계층. 여기가 가장 효율적이다. 셀레니움 스트레스에서 해방됐다. requests 한 줄이면 된다. 고민하지 말고 시작하자. 작은 테스트 하나부터.
- 11 Dec, 2025
Selenium Grid로 병렬 테스트 실행: 8시간이 2시간으로
Selenium Grid로 병렬 테스트 실행: 8시간이 2시간으로 월요일 오전, 테스트는 아직 돌고 있다 월요일 아침 10시. 출근했다. 주말에 돌린 회귀 테스트가 아직도 실행 중이다. 진행률 67%. 시작한 지 6시간째. 전체 테스트 케이스 1,200개. 순차 실행. 예상 완료 시간은 오후 1시. "이거 언제 끝나요?" PM이 묻는다. 세 번째 질문이다. "1시요." "배포는 2시인데요?" "알아요." 테스트 결과 보고 버그 리포트 작성하면 시간 빠듯하다. 혹시 테스트 실패하면? 다시 돌린다. 4시간 더. 배포는 미뤄진다. 매번 이랬다.문제는 명확했다. 순차 실행. 1,200개 테스트가 한 줄로 서서 기다린다. 크롬 브라우저 하나에서. 내 노트북에서. 한 테스트 평균 20초. 1,200개 × 20초 = 24,000초 = 6.6시간. 수학적으로 맞다. 하지만 현실적으로 틀렸다. 금요일 오후에 시작해도 월요일 아침까지. 주말 내내 내 노트북은 일한다. 나는 쉬는데. 비효율이다. "Grid 알아봐야겠다." 혼잣말했다. Grid의 원리: 일을 나눠주는 것 Selenium Grid는 간단하다. Hub 하나. Node 여러 개. Hub는 사장. Node는 직원. 테스트 케이스가 들어온다. Hub가 받아서 Node에게 배분한다. 여러 Node가 동시에 일한다. 병렬 처리. 예를 들면 이렇다. 1,200개 테스트. Node 4개. 각 Node가 300개씩 맡는다. 동시 실행. 시간은 1/4. 6시간이 1.5시간 된다. 이론상으로는. 실제로 Grid 구축하면서 배웠다. 이론과 실전은 다르다는 걸. 첫 시도는 Docker로 했다. Hub 컨테이너 하나. Node 컨테이너 4개. docker-compose.yml 작성. version: '3' services: selenium-hub: image: selenium/hub:4.15.0 ports: - "4444:4444" chrome-node-1: image: selenium/node-chrome:4.15.0 environment: - SE_EVENT_BUS_HOST=selenium-hubNode 4개 띄웠다. Chrome Node 2개. Firefox Node 2개. 브라우저 호환성 테스트도 같이. 터미널에서 확인. docker ps컨테이너 5개 돌고 있다. Hub 1개, Node 4개. 정상이다. Grid Console 접속. http://localhost:4444/ui 화면에 Node 4개 보인다. 각각 브라우저 아이콘. 상태 표시. 처음엔 신기했다.테스트 코드 수정: 병렬화 준비 Grid 띄웠다고 끝이 아니다. 테스트 코드를 수정해야 한다. 병렬 실행 가능하게. 기존 코드는 이랬다. from selenium import webdriverdriver = webdriver.Chrome() driver.get("https://example.com")로컬 크롬 브라우저 실행. Grid에서는 안 된다. RemoteWebDriver로 바꿨다. from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilitiesdriver = webdriver.Remote( command_executor='http://localhost:4444', desired_capabilities=DesiredCapabilities.CHROME )Hub 주소 지정. 브라우저 타입 지정. 이제 Grid Node에서 실행된다. 하지만 문제가 있었다. 테스트들이 서로 간섭했다. 예를 들면 로그인 테스트. 여러 테스트가 동시에 같은 계정으로 로그인. 세션 충돌. 테스트 실패. 해결책: 테스트 독립성. 각 테스트마다 다른 계정 사용. 또는 테스트 데이터 격리. import pytest@pytest.fixture def driver(): driver = webdriver.Remote(...) yield driver driver.quit()def test_login_user1(driver): # user1 계정 사용 passdef test_login_user2(driver): # user2 계정 사용 passPytest의 fixture 활용. 각 테스트마다 독립적인 driver. 테스트 끝나면 driver 종료. 다음은 병렬 실행 설정. Pytest-xdist 플러그인 사용. pip install pytest-xdist실행 명령어. pytest -n 4 tests/-n 4: 4개 워커로 병렬 실행. Pytest가 테스트를 나눠서 실행한다. 각 워커가 Grid Node에 요청. 처음 돌렸을 때. 터미널에 로그 쏟아진다. 4개 테스트가 동시에. 신기했다. 그리고 무서웠다. 제대로 돌아가는 건지. 첫 실행: 2시간 11분 금요일 오후 4시. 첫 Grid 테스트 실행. 전체 1,200개 케이스. 4개 Node. pytest -n 4 --html=report.html tests/시작했다. Grid Console 보면서 모니터링. Node 4개 모두 바쁘게 움직인다. 브라우저 열리고 닫히고. 테스트 케이스 이름 스쳐 지나간다. 진행 상황 실시간 업데이트. 50개... 100개... 200개... 중간에 멈칫한다. Node 하나가 응답 없음. 타임아웃. 재시작. 다시 진행. 500개... 800개... 1,000개...오후 6시 11분. 완료. 결과 확인. 실행 시간: 2시간 11분. 기존: 6시간 30분. 시간 단축: 66%. 성공한 건가? 애매했다. 이론상 1.5시간 예상했는데 2시간 넘었다. 원인 분석했다.Node 재시작 시간 약 15분 테스트 간 대기 시간 (동기화) Hub-Node 통신 오버헤드 일부 테스트가 다른 것보다 훨씬 김그래도 만족했다. 배포 전날 밤에 시작하면 아침에 결과 본다. 충분하다. 브라우저 매트릭스: Chrome, Firefox, Safari Grid의 진짜 장점은 여기서 나왔다. 여러 브라우저 동시 테스트. 기존에는 이랬다. Chrome으로 전체 테스트 → 6시간. Firefox로 다시 전체 테스트 → 6시간. 총 12시간. 하루가 걸렸다. 크로스 브라우저 테스트는 사치였다. 중요한 기능만 Firefox 확인. Grid로 바꾼 후. Chrome Node 2개. Firefox Node 2개. 동시 실행. @pytest.fixture(params=['chrome', 'firefox']) def driver(request): browser = request.param if browser == 'chrome': caps = DesiredCapabilities.CHROME else: caps = DesiredCapabilities.FIREFOX driver = webdriver.Remote( command_executor='http://localhost:4444', desired_capabilities=caps ) yield driver driver.quit()Pytest의 parametrize 기능. 같은 테스트를 다른 브라우저로. 실행하면 자동으로 두 번 돌아간다. Chrome 버전, Firefox 버전. 실제 실행 시간. Chrome 1,200개 + Firefox 1,200개 = 2,400개. 4개 Node로 병렬 실행. 약 3시간 30분. 기존 12시간이 3.5시간 됐다. 시간 단축 70%. 더 나갔다. Safari도 추가하고 싶었다. 문제는 Safari는 macOS에서만 돌아간다. 해결책: AWS EC2 Mac 인스턴스. 비싸다. 하지만 필요했다. mac1.metal 인스턴스 띄웠다. Safari Node 설치. Grid Hub에 연결. 이제 Node 5개. Chrome 2개, Firefox 2개, Safari 1개. 테스트 코드에 Safari 추가. @pytest.fixture(params=['chrome', 'firefox', 'safari']) def driver(request): # ...3개 브라우저 동시 테스트. 총 3,600개 케이스. 5개 Node. 약 4시간 30분. 크로스 브라우저 호환성 문제 많이 잡혔다. Safari에서만 깨지는 CSS. Firefox에서만 안 되는 JavaScript. Grid 없었으면 못 찾았을 버그들이다. Node 관리: 죽고 살리고 Grid 운영하면서 배웠다. Node는 자주 죽는다. 메모리 부족. 브라우저 크래시. 네트워크 타임아웃. 알 수 없는 오류. 처음엔 당황했다. 테스트 중간에 Node 하나 죽으면? 전체가 멈춘다. 재시작해야 한다. 모니터링 스크립트 만들었다. import requestsdef check_grid_health(): response = requests.get('http://localhost:4444/status') data = response.json() for node in data['value']['nodes']: if not node['availability'] == 'UP': print(f"Node down: {node['id']}") restart_node(node['id'])5분마다 실행. cron으로. Node 상태 확인. 죽으면 재시작. 자동 복구 시스템이다. 밤새 테스트 돌려도 안심. Docker로 구성해서 재시작이 쉬웠다. docker restart chrome-node-110초면 다시 살아난다. Grid Hub에 자동 재연결. 하지만 근본 원인도 찾아야 했다. 왜 죽는가? 가장 흔한 원인: 메모리 누수. 브라우저 driver를 제대로 종료 안 하면. 메모리 계속 쌓인다. def teardown(): try: driver.quit() except: pass # 이미 죽었을 수도항상 quit() 호출. try-except로 안전하게. 두 번째 원인: 타임아웃. 느린 페이지 로딩. 무한 대기. driver.set_page_load_timeout(30) driver.implicitly_wait(10)타임아웃 설정 필수. 30초 넘으면 실패 처리. 세 번째 원인: Stale Element. DOM 변경되는데 이전 element 참조. from selenium.common.exceptions import StaleElementReferenceExceptiontry: element.click() except StaleElementReferenceException: element = driver.find_element(...) element.click()재탐색 로직 추가. 이런 개선들로 Node 안정성 올라갔다. 24시간 연속 실행 가능해졌다. 비용: 클라우드 vs 온프레미스 Grid 구축하면서 비용 고민했다. 두 가지 선택지.클라우드: AWS, Azure, BrowserStack 온프레미스: 자체 서버우리는 하이브리드로 갔다. 기본 Node는 온프레미스. 사무실 구석에 서버 3대. Ubuntu 설치. Docker 세팅. Chrome Node 6개 돌린다. 비용: 서버 구매비 약 600만원. 전기세 월 10만원 정도. Safari 테스트만 클라우드. AWS EC2 mac1.metal. 시간당 약 1.5달러. 하루 8시간만 켠다. 월 약 360달러 = 45만원. BrowserStack도 고려했다. 월 200달러부터. 병렬 5개 기준. 편하다. 모든 브라우저 지원. 하지만 우리는 온프레미스 선택. 이유: 테스트 데이터 보안. 내부 API 테스트가 많았다. 외부로 나가면 안 되는 데이터. BrowserStack은 퍼블릭 사이트 테스트용으로만. 중요 테스트는 내부 Grid. 비용 정리.초기 투자: 600만원 (서버) 월 고정: 10만원 (전기) 월 변동: 45만원 (AWS Mac) 총 월 55만원기존에 QA 인력 늘리려면? 1명 추가 = 월 400만원. Grid가 훨씬 싸다. 실전 팁: 내가 배운 것들 1. Node 수는 테스트 특성에 맞춰라 처음엔 무작정 Node 많이 띄웠다. 10개 띄우면 10배 빠를 줄 알았다. 아니다. 테스트가 API 호출 많으면? 서버가 병목이다. Node 10개가 동시에 API 때리면. 서버 죽는다. 적절한 수를 찾아야 한다. 우리는 4-6개가 최적이었다. 2. 테스트 그룹화 빠른 테스트와 느린 테스트 분리. 유닛 테스트: 5분. E2E 테스트: 2시간. 따로 돌린다. pytest -n 4 tests/unit/ # 빠른 것 pytest -n 2 tests/e2e/ # 느린 것느린 테스트는 Node 적게. 한 Node가 오래 걸리는 테스트 하나 맡으면. 다른 Node는 놀고 있다. 밸런싱이 중요하다. 3. 실패 재실행 Grid에서 테스트 실패하면. 원인이 두 가지다. 진짜 버그 vs Flaky 테스트. Flaky는 재실행하면 성공한다. Pytest에 옵션 있다. pytest --reruns 2 --reruns-delay 1실패하면 2번 재시도. 1초 대기 후. 이것만으로 false positive 많이 줄었다. 4. 리포트 통합 4개 Node에서 나온 결과. 따로따로 보면 혼란스럽다. 통합 리포트 필요하다. pytest-html 사용. pytest -n 4 --html=report.html --self-contained-html하나의 HTML 파일. 모든 테스트 결과 정리. 실패 원인, 스크린샷 포함. 이걸 Slack으로 자동 전송. import requestsdef send_to_slack(report_url): webhook_url = "https://hooks.slack.com/..." message = { "text": f"테스트 완료: {report_url}" } requests.post(webhook_url, json=message)아침에 출근하면 Slack에 결과 있다. 5. 캐시 활용 테스트마다 매번 로그인하면 느리다. 로그인 상태를 캐시한다. @pytest.fixture(scope='session') def auth_token(): # 한 번만 로그인 token = login() return tokendef test_something(driver, auth_token): driver.add_cookie({'name': 'auth', 'value': auth_token})session scope fixture. 전체 테스트에서 한 번만 실행. 시간 많이 절약된다. 2개월 후: 8시간이 1시간 45분으로 지금은 Grid 없이 못 산다. 통계를 냈다.전체 테스트: 1,500개 (늘었다) 3개 브라우저 (Chrome, Firefox, Safari) 총 4,500개 케이스 실행 시간: 1시간 45분기존 순차 실행이었으면? 약 25시간. 시간 단축: 93%. 금요일 오후 3시에 시작. 5시 전에 결과 나온다. 배포 여유 있게 진행. 더 좋은 점. PR마다 테스트 돌린다. GitHub Actions에 Grid 연동. 개발자가 코드 푸시하면. 자동으로 테스트 실행. 10분 안에 결과. 버그가 프로덕션 가기 전에 잡힌다. 이게 QA의 목표다. 팀장이 물었다. "Grid 투자 대비 효과는?" 계산해봤다.시간 절약: 주 40시간 인건비 환산: 월 약 200만원 Grid 운영비: 월 55만원 순이익: 월 145만원ROI 264%. 숫자로 증명됐다. 다음 목표: Kubernetes와 Auto-scaling Grid는 끝이 아니다. 다음 단계를 본다. 지금은 고정 Node. 6개 Node가 항상 돌아간다. 테스트 없을 때도. 비효율이다. Kubernetes로 옮기고 싶다. Auto-scaling 설정. 테스트 요청 들어오면. Pod 자동 생성. 필요한 만큼만 Node 띄운다. 테스트 끝나면. Pod 자동 종료. 비용 절감. 아직 공부 중이다. Helm Chart 보고 있다. 복잡하다. 하지만 재밌다.Grid 덕분에 주말이 편해졌다. 노트북도 쉰다. 나도 쉰다.
- 10 Dec, 2025
테스트 실패 분석: 버그인가, 스크립트 문제인가 판단하는 법
테스트 실패 분석: 버그인가, 스크립트 문제인가 판단하는 법 10시 출근, 빨간 불 출근했다. 슬랙에 알림 12개. Jenkins에서 온 거다. "Build #247 Failed - 18 test cases" 커피 먼저 탔다. 보고 나면 점심까지 못 마신다. 경험으로 안다. 모니터 3개 켰다. 왼쪽에 Jenkins, 가운데 로그, 오른쪽에 코드. 이게 내 전투 준비다. 18개 실패. 많은 편이다. 어제 배포 있었나 확인했다. Dev팀이 프론트엔드 UI 수정했다. 아. 첫 느낌으로 70%는 스크립트 문제다. 나머지 30%가 진짜 버그일 확률.로그부터 읽는다 로그 읽기가 제일 중요하다. 근데 로그 해석은 경력이다. 첫 번째 실패 케이스 열었다. ElementNotInteractableException: Element <button id="submit-btn"> is not clickable at point (450, 320). Other element would receive the click: <div class="modal-overlay">이건 명백하다. 모달이 버튼을 가린 거다. 개발자가 모달 로직 바꿨나 보다. 두 번째 케이스. TimeoutException: Message: Expected condition failed: waiting for visibility_of_element_located((By.ID, "user-profile"))타임아웃. 이것도 익숙하다. 두 가지 가능성.페이지 로딩이 느려졌다 (서버 문제) Element ID가 바뀌었다 (UI 변경)세 번째부터는 패턴이 보인다. 전부 같은 에러. NoSuchElementException: Unable to locate element: {"method":"css selector","selector":".btn-primary"}클래스명이 바뀐 거다. .btn-primary를 .primary-button로 바꿨나. 개발자한테 슬랙 날렸다. "어제 버튼 클래스명 바꿨어?" "아 맞다. 디자인 시스템 통일한다고" "..." 이래서 QA가 스트레스받는다. 알려주지도 않는다.스크린샷 체크 로그만으로 부족할 때 스크린샷 본다. Selenium은 실패 시 자동 캡처하게 설정해뒀다. /screenshots/test_login_flow_20240115_0342.png 열었다. 화면에 로딩 스피너가 있다. 아직 페이지가 다 안 떴는데 테스트가 실행된 거다. 이건 스크립트 문제다. Wait 조건이 부족해. 현재 코드 확인했다. driver.find_element(By.ID, "username").send_keys("test@test.com") driver.find_element(By.ID, "password").send_keys("password123") driver.find_element(By.ID, "login-btn").click()바로 찾아서 클릭한다. Wait 없다. 고쳐야 한다. wait = WebDriverWait(driver, 10) username_field = wait.until( EC.element_to_be_clickable((By.ID, "username")) ) username_field.send_keys("test@test.com")이렇게 바꿔야 안정적이다. 다른 스크린샷도 봤다. 결제 페이지 테스트 실패 건. 화면에 에러 메시지가 떠 있다. "Invalid payment method" 이건 진짜 버그다. 로그가 아니라 화면으로 확인됐다. JIRA 티켓 생성했다. "[BUG] Payment fails with valid test credit card" 재현 스텝 써서 Dev팀에 할당. 스크린샷 3장 첨부했다. 개발자가 바로 이해하게. 재현 테스트의 기술 의심스러운 케이스는 직접 돌려본다. 자동화 테스트 결과를 100% 믿으면 안 된다. Flaky test가 있다. 터미널 열고 단일 테스트 실행했다. pytest tests/test_checkout.py::test_guest_checkout -v1차: PASSED 2차: PASSED3차: FAILED 3번 중 1번 실패. 전형적인 Flaky test다. 로그 다시 봤다. 네트워크 타임아웃이 원인이다. API 응답이 가끔 느리다. 서버 문제일 수도 있다. 재현율 체크를 위해 10번 돌렸다. for i in {1..10}; do pytest tests/test_checkout.py::test_guest_checkout; done10번 중 3번 실패. 30% 재현율. 이 정도면 스크립트 문제가 아니다. 서버나 네트워크 이슈다. 백엔드 개발자한테 알렸다. "Checkout API 응답 시간 체크해줄 수 있어? 간헐적으로 타임아웃 나." 답장 왔다. "어제 DB 인덱스 작업했는데 그것 때문일 수도" 역시. 버그 맞다.판단 기준표 4년 하면서 만든 내 기준이다. 스크립트 문제일 확률 높음:NoSuchElementException (90%) ElementNotInteractableException (85%) 같은 에러가 여러 테스트에서 발생 (95%) 최근 UI 배포 후 발생 (90%) 재현율 100% (80%)버그일 확률 높음:AssertionError (70%) 화면에 에러 메시지 표시 (95%) 예전엔 통과했던 테스트 (60%) 재현율 낮음, 간헐적 (40%) 특정 환경에서만 발생 (75%)애매한 경우:TimeoutException (50/50) StaleElementReferenceException (60% 스크립트) Network error (70% 버그) Screenshot이 공백 (디버깅 필요)물론 절대적이지 않다. 맥락을 봐야 한다. 어제 배포 있었나, 환경 설정 바뀌었나, 외부 API 문제인가. 실전 프로세스 내가 실제로 하는 순서다. 1. 슬랙 알림 확인 (30초)Jenkins 빌드 번호 체크 몇 개 실패했나 파악 급한 건지 판단2. Jenkins 대시보드 훑기 (2분)실패 케이스 이름 쭉 본다 패턴 찾는다 (같은 모듈? 같은 에러?) 어느 브랜치인지 확인3. 첫 번째 실패 로그 정독 (5분)에러 타입 확인 스택 트레이스 읽기 실패한 코드 라인 찾기4. 스크린샷 체크 (3분)화면 상태 확인 의도한 화면인지 판단 UI 변경 여부 파악5. 코드 리뷰 (5분)해당 테스트 스크립트 열기 Wait 조건 있나 확인 Selector가 정확한가 체크6. 재현 테스트 (10분)로컬에서 직접 실행 3회 이상 돌려보기 재현율 계산7. 판단 및 액션 (5분)스크립트 문제면 바로 수정 버그면 JIRA 생성 애매하면 개발자와 논의총 30분 정도 걸린다. 18개 실패면 오전 내내 이거다. 자주 속는 케이스들 경험으로 배운 함정들이다. 1. 타임아웃인 줄 알았는데 버그 로딩이 느린 게 아니라, API가 아예 안 돌아오는 거였다. 백엔드 500 에러. Wait을 늘려봤자 소용없다. 에러 로그 끝까지 읽어야 한다. 2. Flaky test인 줄 알았는데 스크립트 문제 가끔 실패하니까 Flaky라고 생각했다. 근데 아니었다. 페이지에 애니메이션이 있었다. 클릭 타이밍을 못 잡은 거다. time.sleep(1) 넣으니까 통과. 근데 이건 나쁜 코드다. EC.element_to_be_clickable로 바꿔야 한다. 3. 로컬에선 되는데 CI에서 실패 환경 차이다. CI 서버가 느리거나, 해상도가 다르거나, 브라우저 버전이 다르거나. Headless 모드 문제일 수도 있다. Docker 컨테이너에서 직접 돌려봐야 안다. 4. 에러 메시지를 믿었다가 낭패 "Element not found" 에러인데, 사실 iframe 안에 있었다. 에러 메시지만 보고 셀렉터 바꿨다가 시간 낭비. HTML 구조 직접 확인해야 한다. 개발자와 소통법 이게 제일 중요하다. 판단 끝나면 전달이다. 스크립트 문제일 때: 슬랙에 조용히 쓴다. "어제 UI 변경 건 때문에 테스트 18개 깨짐. 오늘 중으로 고칠게." 개발자 멘션 안 한다. 내가 처리할 거니까. 버그일 때: JIRA 티켓 먼저 만든다. 슬랙에 링크 공유. "[BUG-1234] 결제 실패 이슈 확인됨. 우선순위 체크 부탁드려." 재현 스텝을 명확히 쓴다.게스트로 장바구니 담기 체크아웃 페이지 이동 테스트 카드 입력 (4242 4242 4242 4242) 결제 버튼 클릭 결과: "Invalid payment method" 에러스크린샷 3장 첨부. 로그도 같이. 개발자가 "재현 안 되는데요?" 하면 환경 정보 준다.Browser: Chrome 120.0.6099 OS: Ubuntu 20.04 (CI server) Test account: test+guest@company.com API endpoint: staging.api.company.com애매할 때: 개발자 불러서 같이 본다. "이거 좀 봐줄 수 있어? 버그인지 스크립트 문제인지 모르겠어." 화면 공유하면서 재현한다. 같이 디버깅. 이게 제일 빠르다. 혼자 고민하지 말기. 오늘의 결과 18개 실패 분석 끝났다. 결과는 이렇다.스크립트 문제: 14개 (UI 변경으로 셀렉터 깨짐) 진짜 버그: 3개 (결제 API, 장바구니 버그 2건) Flaky test: 1개 (네트워크 타임아웃, 무시)14개는 내가 고친다. 클래스명 업데이트하면 된다. # Before driver.find_element(By.CLASS_NAME, "btn-primary")# After driver.find_element(By.CLASS_NAME, "primary-button")30분이면 끝난다. 정규표현식으로 일괄 치환. 버그 3건은 JIRA 티켓 만들었다. Critical 2개, Medium 1개. 개발팀 스탠드업에서 공유 예정. Flaky test는 일단 Ignore 처리. 나중에 네트워크 Retry 로직 추가할 거다. 점심 먹고 스크립트 수정 시작한다. 오후에는 신규 기능 테스트 작성해야 한다. 결제 모듈 v2가 나온다. 끝나지 않는다. QA는 계속된다.분석하는 30분이 하루를 결정한다. 빨간 불을 초록으로 바꾸는 게 내 일이다.
- 09 Dec, 2025
'자동화했는데 왜 버그가 나왔어요?': 내 억울함을 설득하기
"자동화했는데 왜 버그가 나왔어요?": 내 억울함을 설득하기 또 이 질문이다 오늘 아침 스탠드업 미팅. CTO가 물었다. "자동화 커버리지 80%잖아. 그럼 왜 프로덕션에서 버그 나와?" 억울했다. 진짜 억울했다. 설명했다. 자동화는 회귀 테스트용이고, 80%는 단위 테스트 기준이고, 이번 버그는 엣지 케이스였다고. CTO가 고개를 끄덕였다. 이해한 건지 모르겠다. 회의 끝나고 개발팀장이 슬랙을 보냈다. "자동화 더 늘려야 할까요?" 아니다. 문제는 자동화 양이 아니다. 이해의 문제다. 자동화 QA 4년 했다. 이 질문을 100번은 들었다. 이제는 설득하는 법을 알아야 한다. 내 시간을 위해서라도.자동화가 뭔지부터 다시 가장 큰 오해. "자동화했으면 모든 버그를 잡는다." 틀렸다. 자동화 테스트는 내가 짠 시나리오만 검증한다. 로그인 시나리오를 100개 짜면, 그 100개만 체크한다. 101번째 방법? 못 잡는다. 예를 들어보자. 우리 서비스에 로그인 자동화 테스트가 있다.올바른 ID/PW 입력 → 성공 틀린 PW 입력 → 실패 메시지 빈칸 입력 → 경고 메시지 SQL 인젝션 시도 → 차단이게 다 통과했다. 초록불이다. 그런데 지난주 프로덕션에서 버그가 났다. 특수문자가 30개 이상 들어간 비밀번호를 입력하면 서버가 멈췄다. 내 테스트에 없었다. 특수문자 3개는 테스트했다. 30개는 생각 못 했다. 이게 자동화의 한계다. 내가 상상한 것만 테스트된다. 경영진은 이걸 이해 못 한다. "80% 커버리지면 충분하지 않아?" 그 80%가 뭔지 모른다. 코드 라인 커버리지인지, 기능 커버리지인지, 유저 시나리오 커버리지인지. 우리 회사는 코드 라인 기준이다. 코드의 80%를 테스트 코드가 실행했다는 뜻. 그게 모든 버그를 잡는다는 뜻은 아니다. 개발자들은 이해한다. 테스트 커버리지 100%도 버그를 보장 못 한다는 걸. 근데 경영진은 숫자만 본다. 80%면 A학점이라고 생각한다.매뉴얼 테스트가 필요한 이유 두 번째 오해. "자동화했으면 매뉴얼 QA는 필요 없다." 이건 더 위험한 생각이다. 지난달 우리 회사가 신입 QA 채용을 안 하려고 했다. "자동화 있는데 왜 필요해?" 내가 막았다. 1시간짜리 문서를 만들어서 설명했다. 자동화는 반복 작업에 강하다.회귀 테스트: 매 배포마다 똑같은 시나리오 체크 스모크 테스트: 빌드 후 기본 기능 확인 API 테스트: 엔드포인트 검증근데 자동화가 못하는 게 더 많다. 탐색적 테스트 유저가 어떻게 쓸지 모르는 상황. 아무렇게나 눌러보는 거. 이건 스크립트로 못 짠다. 지난주 신기능 출시 전에 매뉴얼 QA 동료가 찾았다. 뒤로가기 버튼을 5번 연속 누르면 앱이 튕겼다. 내 자동화 스크립트엔 "뒤로가기 2번"까지만 있었다. 누가 5번을 누르나? 유저가 누른다. 짜증나서 막 누른다. UX/UI 테스트 버튼이 너무 작다. 폰트가 안 읽힌다. 색상이 이상하다. 이건 사람 눈으로 봐야 한다. Selenium으로 "버튼이 존재하는가?"는 체크할 수 있다. "버튼이 예쁜가?"는 못 한다. 작년에 디자인 개편했을 때. 자동화 테스트는 다 통과했다. 근데 매뉴얼 QA가 발견했다. 다크모드에서 글자가 안 보였다. 자동화는 "글자가 렌더링되는가?"만 체크했다. "글자가 보이는가?"는 체크 안 했다. 신규 기능 테스트 새 기능이 나오면 자동화 스크립트부터 짜야 한다. 그 전에 매뉴얼로 먼저 테스트한다. 뭘 자동화할지 모르니까. 경영진한테 설명했다. "자동화는 알고 있는 걸 반복하는 거예요. 매뉴얼은 모르는 걸 찾는 거고요." 통했다. 신입 QA 2명 뽑았다. 자동화의 실제 가치 그럼 자동화는 왜 하나? 가치가 없는 건 아니다. 명확한 가치가 있다. 시간 절약 회귀 테스트를 매뉴얼로 하면 3일 걸린다. 자동화로 하면 3시간이다. 이건 큰 차이다. 매주 배포하는 우리 회사. 매뉴얼로만 하면 QA팀이 테스트만 한다. 자동화 덕분에 다른 일을 한다. 일관성 사람은 실수한다. 피곤하면 놓친다. 점심 먹고 오면 집중력이 떨어진다. 스크립트는 안 그렇다. 새벽 3시에 돌려도 똑같이 체크한다. 작년에 우리가 찾았다. 매뉴얼 테스트에서 5번 중 1번은 실수가 있었다. 체크리스트 항목을 건너뛰거나 결과를 잘못 기록하거나. 자동화는 그런 실수가 없다. 짠 대로 돌아간다. 빠른 피드백 개발자가 코드를 푸시한다. 5분 뒤 Jenkins가 자동화 테스트를 돌린다. 10분 뒤 결과가 슬랙으로 온다. 깨진 부분이 있으면 바로 안다. 개발자가 지금 뭐 하는지 기억하는 시점에. 매뉴얼로 하면? 다음날 아침에 QA가 출근해서 테스트한다. 개발자는 이미 다른 작업 중이다. 컨텍스트 스위칭 비용이 크다. 반복 작업에서 해방 QA의 가장 큰 스트레스. 똑같은 테스트를 100번 하는 거. 로그인 테스트. 기능이 안 바뀌었는데 매번 해야 한다. 1년 동안 200번 했다. 자동화하면? 스크립트가 한다. 나는 새로운 걸 테스트한다. 우리 팀 후배가 말했다. "자동화 배우고 나서 일이 재미있어졌어요. 반복 작업이 줄어서요." 이게 자동화의 진짜 가치다. 모든 버그를 잡는 게 아니라, 알고 있는 버그를 효율적으로 체크하는 거다.경영진 설득법 구체적으로 어떻게 설득하나? 1. 숫자로 말한다 경영진은 숫자를 좋아한다. 추상적 설명 싫어한다. 잘못된 설명: "자동화는 한계가 있어요." 올바른 설명: "자동화로 회귀 테스트 200개 케이스를 체크합니다. 하지만 가능한 유저 시나리오는 10,000개 이상이에요." 잘못된 설명: "매뉴얼 테스트도 필요해요." 올바른 설명: "작년에 프로덕션 버그 30개 중 18개를 매뉴얼 QA가 찾았어요. 자동화는 12개였고요." 내가 만든 대시보드가 있다. 자동화 테스트 커버리지: 코드 라인 80% 매뉴얼 테스트 커버리지: 유저 시나리오 45% 버그 발견율: 자동화 40%, 매뉴얼 40%, 프로덕션 20%마지막 20%가 중요하다. "자동화해도 20%는 프로덕션에서 나옵니다." 2. 비용으로 환산한다 "자동화 더 늘리면 버그가 줄어들까요?" 아니다. 수익률이 떨어진다. 현재 자동화 커버리지 80%. 여기서 90%로 올리려면?추가 인력 1명 필요 3개월 소요 연간 유지보수 시간 2배그래서 얻는 건? 버그 발견율 5% 상승 예상. 비용 대비 효과가 안 맞는다. 차라리 그 시간에 매뉴얼 QA를 더 하는 게 낫다. 이렇게 설명하면 경영진이 이해한다. 자동화가 만능이 아니라는 걸. 3. 사례를 든다 추상적 설명은 안 먹힌다. 구체적 사례가 필요하다. "지난달 결제 버그 기억하세요? 자동화 테스트는 통과했어요. 왜냐면 '결제 성공' 시나리오만 있었거든요. 근데 실제 버그는 '결제 중 네트워크 끊김'이었어요. 이건 매뉴얼 QA가 찾았습니다." "작년 UI 개편 때요. 자동화는 100% 통과했어요. 근데 유저 불만이 쏟아졌죠. 버튼이 너무 작아서요. 자동화는 '버튼이 클릭 가능한가?'만 체크했거든요." 사례가 3개 이상 쌓이면 패턴이 보인다. 경영진이 납득한다. 4. 대안을 제시한다 문제만 지적하면 안 된다. 해결책도 줘야 한다. "자동화 커버리지를 더 올리는 대신, 리스크 기반 테스트를 강화하겠습니다."핵심 기능: 자동화 + 매뉴얼 둘 다 일반 기능: 자동화만 레거시 기능: 샘플링 테스트"탐색적 테스트 시간을 주 4시간으로 정례화하겠습니다."QA 전원이 동시에 신규 기능을 막 써보는 시간 발견된 버그는 즉시 자동화 스크립트에 추가"테스트 피라미드를 재정비하겠습니다."단위 테스트: 70% (개발자 담당) 통합 테스트: 20% (QA 자동화) E2E 테스트: 10% (QA 매뉴얼)대안이 있으면 경영진이 결정하기 쉽다. 개발팀 설득법 개발자들은 다르게 접근한다. 1. 코드로 보여준다 개발자는 말보다 코드를 믿는다. Flaky 테스트 문제가 있었다. 간헐적으로 실패하는 테스트. 개발팀이 물었다. "테스트 문제 아니에요?" 맞다. 테스트 코드 문제였다. # 문제 있는 코드 element = driver.find_element(By.ID, "submit") element.click()# 개선한 코드 wait = WebDriverWait(driver, 10) element = wait.until( EC.element_to_be_clickable((By.ID, "submit")) ) element.click()코드를 보여주니까 이해했다. "아, 로딩 타이밍 문제였네요." 테스트 코드도 코드다. 버그가 있다. 리팩토링이 필요하다. 개발자들은 이걸 이해한다. 2. 테스트 피라미드를 그린다 개발자들은 아키텍처를 좋아한다. 추상적 구조를 좋아한다. 화이트보드에 피라미드를 그렸다. /\ E2E (느림, 비쌈, 10개) / \ / \ Integration (중간, 50개) / \ /________\ Unit (빠름, 쌈, 500개)"E2E로 모든 걸 테스트하면 너무 느려요. 빌드 시간이 1시간 넘어요." 개발자들이 고개를 끄덕였다. "맞아요. CI 너무 느려지면 안 되죠." "그래서 단위 테스트를 개발자가 더 짜주시면, QA는 통합 테스트에 집중할게요." 이렇게 제안하니까 협력이 됐다. 3. 버그 우선순위를 함께 정한다 개발자와 QA가 싸우는 이유. 우선순위가 다르니까. 개발자: "이건 엣지 케이스예요. 무시해도 돼요." QA: "아니요. 치명적 버그예요." 이제는 함께 정한다. 매주 수요일 30분. 버그 티켓을 놓고 투표한다.Critical: 프로덕션 즉시 롤백 필요 High: 다음 배포 전 수정 필요 Medium: 다다음 배포 Low: 백로그의견이 다르면 토론한다. 데이터를 본다. 이 버그가 몇 명한테 영향을 주나? 얼마나 자주 발생하나? 개발자들이 납득하면 수정을 빨리 한다. 우선순위 싸움이 없으니까. 4. 자동화 한계를 함께 인정한다 개발자도 안다. 테스트가 완벽할 수 없다는 걸. "이번 버그는 제 자동화 테스트가 못 잡았어요. 다음부터는 이 케이스도 추가하겠습니다." 솔직하게 인정하니까 개발자도 솔직해졌다. "저도 단위 테스트를 더 꼼꼼히 짤게요. 이 함수는 테스트 커버리지가 낮았어요." 서로의 한계를 인정하면 협력이 된다. 책임 전가가 아니라 함께 개선한다. 내가 배운 것들 4년 동안 자동화하면서 깨달은 것들. 자동화는 도구다. 목표가 아니다. 초반에 나는 자동화 커버리지에 집착했다. 80%, 90%, 95%... 근데 중요한 건 버그를 줄이는 거다. 커버리지 숫자가 아니라. 지금은 전략적으로 접근한다. 뭘 자동화하고 뭘 매뉴얼로 하나?자주 바뀌는 UI: 매뉴얼 핵심 API: 자동화 복잡한 비즈니스 로직: 둘 다완벽한 테스트는 없다 처음엔 모든 버그를 잡으려고 했다. 불가능하다는 걸 배웠다. QA의 목표는 버그 제로가 아니다. 리스크 관리다. 치명적 버그는 출시 전에 잡는다. 사소한 버그는 우선순위를 낮춘다. 자원이 한정돼 있으니까. 설득은 데이터로 한다 "자동화가 필요해요"보다 "자동화로 회귀 테스트 시간을 3일에서 3시간으로 줄였어요"가 낫다. "매뉴얼 QA도 필요해요"보다 "작년 프로덕션 버그의 60%를 매뉴얼 QA가 찾았어요"가 낫다. 숫자가 없으면 의견이다. 숫자가 있으면 사실이다. QA는 품질 파수꾼이 아니라 품질 조력자다 예전엔 내가 게이트키퍼라고 생각했다. "이 버그 있으면 출시 안 돼요." 지금은 조력자라고 생각한다. "이 리스크가 있어요. 함께 결정해요." 품질은 QA 혼자 만드는 게 아니다. 개발자, 기획자, 디자이너 모두의 책임이다. QA는 그걸 보이게 만드는 사람이다. 마무리: 억울하지 않으려면 "자동화했는데 왜 버그 나와?" 이제 이 질문이 안 억울하다. 예상된 질문이니까. 대답이 준비돼 있다. "자동화는 알려진 시나리오를 체크합니다. 이번 버그는 새로운 패턴이었어요. 지금 이 케이스를 자동화 스크립트에 추가했습니다." 데이터를 보여준다.이번 스프린트 자동화 테스트: 1,234개 통과, 3개 실패 매뉴얼 테스트: 신규 기능 45개 체크, 버그 7개 발견 프로덕션 버그: 2개 (하나는 인프라, 하나는 엣지 케이스)"자동화 커버리지 80%는 코드 라인 기준입니다. 유저 시나리오 기준으로는 약 40%예요. 나머지는 매뉴얼과 프로덕션 모니터링으로 커버합니다." 명확하게 설명하면 이해한다. 경영진도, 개발자도. 억울할 필요 없다. 자동화의 가치와 한계를 모두 보여주면 된다. QA 자동화 엔지니어의 일은 모든 버그를 막는 게 아니다. 효율적으로 품질을 관리하는 거다. 그걸 설득하는 것도 내 일이다.오늘도 자동화 스크립트 3개 추가했다. 지난주 놓친 케이스들. 완벽하진 않지만, 조금씩 나아진다.
- 03 Dec, 2025
테스트 피라미드: 왜 E2E 테스트만 100개 돌리는 팀은 실패하는가
테스트 피라미드: 왜 E2E 테스트만 100개 돌리는 팀은 실패하는가 월요일 아침, 빌드가 깨졌다 출근했다. 슬랙에 알림 37개. "빌드 실패", "테스트 타임아웃", "CI 30분째 돌아가는 중". 아침 9시. 젠킨스 보니까 E2E 테스트가 2시간째 돌고 있다. 103개 테스트 중 47번째에서 멈췄다. 셀레니움이 로딩 스피너를 못 찾는다. 또. 옆자리 개발자가 말했다. "배포 언제 돼요? 오전 회의에서 발표해야 하는데." 나도 모른다. 테스트가 끝나봐야 안다. 이게 3일째다.작년 이맘때, 우리는 E2E를 사랑했다 입사 1년 전 이야기다. 그때 우리 팀은 E2E 테스트를 막 도입했다. PM이 말했다. "실제 사용자 시나리오를 테스트해야죠." 맞는 말이었다. 첫 E2E 테스트를 짰다. 로그인 → 상품 검색 → 장바구니 → 결제. 완벽했다. 실제 브라우저에서 돌아갔다. 버그를 3개 찾았다. 개발팀이 감탄했다. "역시 E2E네요!" 그래서 더 짰다. 회원가입 시나리오. 10개 케이스. 결제 시나리오. 15개 케이스. 마이페이지. 8개 케이스. 6개월 후, E2E 테스트가 103개였다. 실행 시간은 2시간 30분. 이때부터 문제가 시작됐다. 금요일 오후, 아무도 배포 안 하려는 이유 금요일 4시. 개발자가 PR 올렸다. "간단한 버그 픽스입니다. 머지 부탁드려요." 나도 간단해 보였다. CSS 한 줄 수정. 그런데 규칙이 있었다. "모든 PR은 E2E 테스트 통과 후 머지." 버튼 눌렀다. 젠킨스가 103개 테스트를 돌리기 시작했다. 2시간 기다렸다. 92번째 테스트에서 실패. "Element not found: 로그아웃 버튼." CSS 수정인데 왜 로그아웃 테스트가 깨지나. 로그 봤다. 타임아웃이었다. 페이지 로딩이 평소보다 2초 늦었다. 재실행했다. 이번엔 통과했다. 시계 봤다. 저녁 7시. 개발자는 퇴근했다. 금요일 배포는 없었다. 다음 주 월요일에 머지했다. CSS 한 줄 때문에 3일 걸렸다.화요일 점심, CTO가 물었다 회의실. CTO가 들어왔다. "배포 속도가 너무 느립니다. 경쟁사는 하루 10번 배포하는데." 개발 리드가 말했다. "테스트 때문입니다. 2시간 넘게 걸려요." CTO가 나를 봤다. "테스트를 줄일 수 있나요?" 나는 답했다. "어떤 테스트를요? 다 중요합니다." CTO가 물었다. "정말 103개가 다 필요한가요?" 솔직히 모르겠다. 6개월 동안 쌓다 보니 103개가 됐다. 어느 게 중요한지 구분 못 했다. 회의 끝나고 책상 돌아왔다. 구글링했다. "E2E test too slow". 테스트 피라미드가 나왔다. 마틴 파울러의 글이었다. 그때 깨달았다. 우리는 피라미드를 거꾸로 세우고 있었다. 테스트 피라미드, 내가 이해한 것 그림을 그렸다. 삼각형. 피라미드. 밑에서부터:유닛 테스트 (70%) 통합 테스트 (20%) E2E 테스트 (10%)우리 팀 현실:유닛 테스트 (5%) 통합 테스트 (5%) E2E 테스트 (90%)완전 거꾸로였다. 왜 피라미드 모양이어야 하나. 이유는 간단했다. 속도. 유닛 테스트: 0.1초. 통합 테스트: 2초. E2E 테스트: 90초. 안정성. 유닛 테스트: 깨지면 코드 문제. 통합 테스트: 깨지면 연동 문제. E2E 테스트: 깨지면... 네트워크? 타임아웃? 셀레니움? 모름. 유지보수. 유닛 테스트: 함수 이름 바꾸면 끝. 통합 테스트: API 스펙 바꾸면 수정. E2E 테스트: UI 바뀌면 셀렉터 다 깨짐. 계산해봤다. 103개 E2E를 70개 유닛 + 20개 통합 + 13개 E2E로 바꾸면. 실행 시간:기존: 2시간 30분 변경 후: 12분10배 빨라진다.수요일 오전, 설득 시작 개발팀 회의. 화이트보드에 피라미드 그렸다. "우리는 지금 이렇게 테스트합니다." 거꾸로 된 피라미드. "이래야 합니다." 정상 피라미드. 개발자 한 명이 물었다. "유닛 테스트로 사용자 시나리오를 어떻게 검증해요?" 좋은 질문이었다. "검증 안 합니다. 유닛 테스트는 함수를 테스트해요." 예시 들었다. 로그인 기능. 기존 E2E 테스트:브라우저 열기 로그인 페이지 가기 아이디 입력 비밀번호 입력 로그인 버튼 클릭 메인 페이지 확인 프로필 메뉴 확인 로그아웃 버튼 확인90초 걸렸다. 새로운 방식: 유닛 테스트 (3개):비밀번호 검증 함수 테스트 (0.1초) 토큰 생성 함수 테스트 (0.1초) 세션 저장 함수 테스트 (0.1초)통합 테스트 (2개):로그인 API 테스트 (2초) 토큰 검증 API 테스트 (2초)E2E 테스트 (1개):실제 로그인 시나리오 (30초)총 실행 시간: 34.3초. 기존 대비 62% 빠르다. 더 중요한 건 안정성이었다. E2E가 깨지면 원인 찾기 어렵다. 유닛이 깨지면 정확히 어느 함수가 문제인지 안다. 개발 리드가 말했다. "좋은데, 누가 유닛 테스트 짜죠?" "개발자들이요." 분위기가 싸해졌다. 목요일 오후, 반발 개발자 세 명이 찾아왔다. "유닛 테스트 짜는 시간 있으면 기능 개발하겠습니다." 예상했던 반응이다. 물었다. "지난주 버그 기억나요? 결제 금액 계산 오류." 기억한다는 표정이었다. "그거 유닛 테스트 있었으면 5분 만에 찾았어요. 우리는 2시간 걸렸죠." 계산해줬다. 한 달 버그 수정 시간: 40시간. 유닛 테스트 작성 시간: 주당 2시간, 한 달 8시간. 이득: 32시간. "기능 개발 시간 더 생깁니다." 한 명이 고개 끄덕였다. 다른 개발자가 물었다. "QA가 도와줄 건가요?" "유닛은 어렵습니다. 코드를 제가 다 모르니까. 대신 통합 테스트는 제가 짜겠습니다." 역할 분담했다.개발자: 유닛 테스트 (함수, 클래스) 나: 통합 테스트 (API, 데이터베이스) 나: E2E 테스트 (핵심 시나리오만)일주일 시범 운영 제안했다. CTO가 승인했다. 2주 후, 숫자로 말하기 스프레드시트 만들었다. 비교표. 기존 방식 (2주):총 테스트: 103개 (E2E 100개, 통합 3개) 실행 시간: 평균 2시간 18분 빌드 실패율: 23% (Flaky 테스트) 평균 배포 시간: PR 머지부터 4.2시간 버그 발견: 17개 프로덕션 버그: 5개새 방식 (2주):총 테스트: 187개 (유닛 132개, 통합 38개, E2E 17개) 실행 시간: 평균 14분 빌드 실패율: 8% 평균 배포 시간: 28분 버그 발견: 31개 프로덕션 버그: 2개숫자가 말해줬다. 테스트는 81% 늘었다. 시간은 90% 줄었다. 버그는 더 많이 찾았다. 프로덕션 버그는 60% 줄었다. 개발자들이 놀랐다. "유닛 테스트가 버그를 이렇게 많이 찾네요." 당연했다. 유닛 테스트는 엣지 케이스를 확인한다. E2E는 해피 패스만 확인한다. 예시. 결제 금액 계산 함수. E2E로는 1개 케이스만 확인했다. "10,000원 상품 + 2,500원 배송비 = 12,500원" 유닛으로는 12개 케이스 확인했다.음수 금액 0원 천억 원 소수점 null 값 할인 적용 쿠폰 중복 등등실제로 버그 3개 찾았다. 음수 금액 처리 안 됨. 쿠폰 중복 적용됨. 천억 원 넘으면 오버플로우. E2E로는 절대 못 찾을 버그들이었다. 한 달 후, E2E 17개 선택 기준 회의. "E2E 103개 중 17개만 남기기." 기준 세웠다. 남길 E2E:매출 직결 시나리오 (결제, 환불) 유닛/통합으로 커버 안 되는 것 (브라우저 특성) 여러 시스템 연동 필수 (결제사, 배송사) 사용자 경험 크리티컬 (회원가입, 로그인) 법적 필수 기능 (개인정보 처리)버릴 E2E:유닛으로 커버 가능 (계산, 검증) 통합으로 충분 (API 호출) UI만 다른 중복 케이스 (버튼 위치만 다름) Flaky한 테스트 (타임아웃 자주 남) 비즈니스 임팩트 낮음 (어드민 페이지 필터)103개 리스트 출력했다. 하나씩 분류했다. 결과:유닛으로 이동: 61개 통합으로 이동: 25개 E2E 유지: 17개17개 E2E 리스트:회원가입 (이메일 인증 포함) 로그인 (소셜 로그인 포함) 상품 검색 → 상세 → 장바구니 결제 (카드, 계좌이체, 카카오페이) 주문 조회 환불 신청 리뷰 작성 1:1 문의 배송지 변경 쿠폰 적용 포인트 사용 정기 구독 신청 구독 해지 비밀번호 찾기 회원 탈퇴 장바구니 비우기 최근 본 상품이 17개만 돌리면 35분 걸렸다. 충분했다. 핵심 사용자 플로우는 다 커버했다. 3개월 후, 새로운 문제 평화로웠다. 배포는 빨라졌다. 하루 평균 8번 배포했다. 그런데 새로운 문제가 생겼다. 유닛 테스트가 너무 많아졌다. 418개. 개발자들이 열심히 짰다. 너무 열심히 짰다. 실행 시간이 3분으로 늘었다. 예전엔 30초였다. 왜? 봤더니 중복 테스트가 많았다. 예시. 회원가입 API. 유닛 테스트:이메일 형식 검증 (유닛) 비밀번호 길이 검증 (유닛) 닉네임 특수문자 검증 (유닛)통합 테스트:이메일 형식 검증 (통합) 비밀번호 길이 검증 (통합) 닉네임 특수문자 검증 (통합)중복이었다. 개발자한테 말했다. "유닛에서 검증했으면 통합에서 또 할 필요 없어요." "그럼 통합에서는 뭘 테스트해요?" "연동이요. 데이터베이스에 제대로 저장되는지. 이메일이 발송되는지." 정리했다. 유닛 테스트 역할:비즈니스 로직 계산, 검증, 변환 빠른 피드백통합 테스트 역할:시스템 연동 데이터베이스, API, 외부 서비스 실제 환경 검증E2E 테스트 역할:사용자 시나리오 여러 시스템 통합 최종 확인역할이 명확해지니까 중복이 줄었다. 418개 유닛 → 312개. 실행 시간 3분 → 1분 20초. 6개월 후, 숫자 보고 CTO 보고. 반년 데이터. 테스트 현황:유닛: 312개 통합: 94개 E2E: 17개 총: 423개실행 시간:전체: 평균 16분 유닛만: 1분 20초 통합까지: 9분 E2E 포함: 16분배포 속도:하루 평균: 11.3번 6개월 전: 1.4번 증가율: 707%품질:프로덕션 버그: 월 2.1개 6개월 전: 월 8.7개 감소율: 76%개발 생산성:기능 개발 시간: 37% 감소 (버그 수정 시간 줄어서) 핫픽스: 월 1.2회 (기존 5.8회)CTO가 만족했다. "다른 팀에도 적용합시다." 복잡했다. 지금, 다른 팀 코칭 중 지금은 백엔드팀 코칭 중이다. 그들도 비슷했다. 통합 테스트 200개. 실행 시간 40분. 피라미드 그려줬다. "유닛 테스트 먼저 짜세요." 그들이 물었다. "API 테스트가 있는데 왜 유닛이 필요해요?" 설명했다. API 테스트는 느리다. 서버 띄우고, DB 연결하고, 요청 보내고. 한 테스트에 2초. 유닛 테스트는 빠르다. 함수만 호출. 한 테스트에 0.01초. 200배 차이. 더 중요한 건 디버깅이다. API 테스트가 깨지면:라우팅 문제? DB 문제? 비즈니스 로직 문제? 권한 문제? 네트워크 문제?모른다. 다 확인해야 한다. 유닛 테스트가 깨지면:정확히 이 함수가 문제.끝. 백엔드팀이 2주째 시도 중이다. 유닛 78개 추가했다. 통합 200개 → 143개로 줄였다. 실행 시간 40분 → 18분. 잘되고 있다. 내가 배운 것들 6개월간 배웠다. 1. E2E는 마지막 방어선이다. 모든 걸 E2E로 테스트하려고 하면 망한다. E2E는 비싸다. 느리고, 불안정하고, 유지보수 어렵다. 핵심만 남겨라. 2. 유닛 테스트가 제일 중요하다. 처음엔 안 믿었다. "사용자 시나리오를 어떻게 유닛으로 테스트해?" 하지만 버그의 80%는 로직 문제다. 로직은 유닛으로 잡는다. 3. 피라미드는 속도다. 10분 안에 피드백 받아야 한다. 그래야 개발자가 컨텍스트 유지한다. 2시간 후 피드백은 의미 없다. 4. 테스트도 코드다. 중복 제거해야 한다. 리팩토링 해야 한다. 유지보수 해야 한다. 테스트가 많다고 좋은 게 아니다. 5. 역할 분담이 명확해야 한다. 유닛, 통합, E2E의 역할을 모두가 이해해야 한다. 안 그러면 중복 테스트 천지가 된다. 6. 숫자로 말해야 한다. "E2E 줄입시다"는 설득 안 된다. "실행 시간 90% 줄고, 버그 60% 줄어듭니다"는 설득된다. 7. 점진적으로 바꿔야 한다. 한 번에 103개 다 못 바꾼다. 일주일 시범, 한 달 확대, 석 달 정착. 이게 현실이다. 여전히 어려운 것 완벽하진 않다. E2E 선택 기준이 주관적이다. "이건 E2E로 해야 해요" vs "통합으로 충분해요" 여전히 의견 갈린다. 내 기준:돈이 오가는가? → E2E UI 인터랙션이 복잡한가? → E2E API만 호출하는가? → 통합 함수 호출인가? → 유닛하지만 애매한 경우가 많다. 개발자 설득이 계속 필요하다. 신입이 들어온다. 또 설명해야 한다. "왜 이런 걸 유닛으로 짜요?" 피라미드 그림 그리는 게 일상이 됐다. Flaky 테스트는 여전하다. E2E 17개 중 3개는 가끔 깨진다. 네트워크 타임아웃. 타이밍 이슈. 재시도 로직 넣었다. 3번 재시도. 2번 성공하면 통과. 완벽한 해결은 아니다. 커버리지 압박. PM이 물어본다. "커버리지 몇 퍼센트예요?" "유닛 78%, 통합까지 합치면 86%." "왜 100% 아니에요?" 설명한다. "100% 커버리지가 목표가 아니에요. 중요한 로직 커버가 목표예요." 이해 안 할 때도 있다. E2E 유지보수는 여전히 힘들다. UI 바뀌면 E2E 깨진다. 버튼 ID 바뀜. 셀렉터 수정. CSS 클래스 바뀜. 다시 수정. 17개로 줄었지만 여전히 일이다. 금요일 저녁, 배포 금요일 5시. 개발자가 PR 올렸다. "새 기능: 위시리스트 공유" 리뷰했다. 머지 승인. CI 돌아간다. 유닛 312개: 1분 20초. 통과. 통합 94개: 7분 40초. 통과. E2E 17개: 6분 30초. 통과. 총 15분 30초. 배포 버튼 눌렀다. 프로덕션 올라갔다. 5시 17분에 시작해서 5시 33분에 끝났다. 개발자한테 슬랙 보냈다. "배포 완료. 확인해보세요." "벌써요? 감사합니다!" 퇴근했다. 6시 10분. 작년 같았으면 9시까지 E2E 돌아가는 걸 지켜봤을 것이다. 지금은 정시 퇴근한다. 테스트 피라미드 덕분이다.E2E 100개 돌리는 팀은 느리고, 불안정하고, 결국 무너진다. 피라미드는 거꾸로 세우는 게 아니다. 밑에서부터 차곡차곡 쌓는 거다. 그게 빠르고, 안정적이고, 지속 가능한 방법이다.