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 한 줄이면 된다. 고민하지 말고 시작하자. 작은 테스트 하나부터.
- 15 Dec, 2025
테스트 자동화 프레임워크 선택: Selenium vs Playwright vs Cypress
프레임워크 전쟁의 시작 월요일 오전. 슬랙에 CTO 메시지가 떴다. "J님, Playwright 도입 검토 좀 해주세요." 커피 한 모금도 안 마셨다. 머리가 복잡해졌다. 우리 자동화 프레임워크는 Selenium 기반이다. 4년 전에 내가 직접 구축했다. 테스트 케이스 1200개. 커버리지 65%. 매일 밤 돌아간다. 문제없이 돌아간다. 그런데 최근 개발자들이 계속 물어본다. "Playwright는 왜 안 써요?" "Cypress가 더 빠르다던데요?" "Selenium은 옛날 거 아닌가요?" 짜증 났다. 그래서 제대로 정리하기로 했다.Selenium: 내 4년의 동반자 우리 프레임워크는 Selenium + Python + Pytest다. 현재 상태:테스트 케이스: 1200개 실행 시간: 약 3시간 (병렬 처리) 안정성: 85% (flaky test 때문에) 유지보수 시간: 주 10시간솔직히 말하면 Selenium은 늙었다. 2004년부터 있었다. 거의 20년 됐다. 장점이 명확하다:브라우저 지원이 최고다. Chrome, Firefox, Safari, Edge, 심지어 IE도 된다. 우리 고객 중 10%가 아직 IE 쓴다. 믿기지 않지만 사실이다.생태계가 방대하다. 스택오버플로우에 답이 다 있다. 에러 메시지 복사해서 검색하면 해결법 나온다. 이게 얼마나 중요한지 모른다.언어 선택의 자유. Python, Java, JavaScript, C#, Ruby. 뭐든 된다. 우리는 Python이다. 백엔드팀이 Python이라 코드 공유가 쉽다.모바일도 된다. Appium이 Selenium 기반이다. 웹/앱 자동화 프레임워크 통합이 가능하다.단점도 분명하다:느리다. WebDriver 통신 방식이 문제다. 브라우저 ↔ WebDriver ↔ 테스트 코드. 중간 단계가 많다. 네트워크 레이턴시가 쌓인다.셋업이 복잡하다. ChromeDriver 버전 맞추기가 지옥이다. Chrome 자동 업데이트되면 테스트 깨진다. CI 환경에서 드라이버 관리가 일이다.Flaky test 지옥. time.sleep(2) 이딴 게 코드에 수십 개다. 요소 로딩 타이밍 잡기가 어렵다. WebDriverWait 써도 완벽하지 않다.에러 메시지가 불친절하다. "Element not found". 그래서 어디서 왜 없는 건데? 디버깅에 시간 배로 든다.매주 flaky test 잡는데 3시간 쓴다. 진짜 스트레스다.Playwright: 떠오르는 강자 팀 막내가 사이드 프로젝트로 Playwright 써봤다고 했다. "진짜 빨라요. 셋업도 쉽고요." 그래서 직접 테스트해봤다. 일주일 동안 POC 진행했다. 첫인상이 강렬했다: # Selenium driver = webdriver.Chrome() driver.get("https://example.com") element = driver.find_element(By.ID, "button") element.click()# Playwright page.goto("https://example.com") page.click("#button")코드가 간결하다. 보일러플레이트가 적다. 장점:속도가 미쳤다. 브라우저랑 직접 통신한다. CDP(Chrome DevTools Protocol) 쓴다. WebDriver 없다. 같은 테스트가 40% 빠르다.자동 대기가 똑똑하다. page.click()하면 알아서 요소 나타날 때까지 기다린다. time.sleep() 필요 없다. Flaky test가 확 줄어든다.셋업이 쉽다. playwright install 하면 끝이다. 브라우저 바이너리를 직접 다운로드한다. 버전 걱정 없다.병렬 처리가 강력하다. 브라우저 컨텍스트 격리가 잘 된다. 세션 충돌 없다. 테스트 속도가 배로 빨라진다.디버깅 툴이 좋다. Playwright Inspector가 있다. 스텝별로 실행하고 요소 하이라이트된다. 스크린샷 자동 저장. 비디오 녹화도 된다.API 테스트도 된다. playwright.request로 API 호출 가능하다. E2E + API 통합 테스트가 한 프레임워크에서 된다.단점:브라우저 제한. Chromium, Firefox, WebKit만 된다. IE 안 된다. Safari는 WebKit이지만 진짜 Safari랑 다르다. 우리 10% 고객은?생태계가 작다. 2020년에 나왔다. 4년밖에 안 됐다. 스택오버플로우 답변이 적다. 이상한 버그 만나면 혼자 해결해야 한다.학습 곡선. Selenium 아는 사람이 많다. Playwright는 새로 배워야 한다. 팀 온보딩 시간이 든다.Microsoft 의존. MS가 만들었다. 오픈소스지만 결국 MS 생태계다. TypeScript 푸시가 강하다. Python 지원은 2등이다.실제로 로그인 테스트 5개를 마이그레이션 해봤다. 결과:작성 시간: Selenium 3시간 → Playwright 1.5시간 실행 속도: 45초 → 18초 Flaky 발생: 3회 → 0회솔직히 놀랐다.Cypress: 프론트엔드의 사랑 프론트엔드 개발자들은 Cypress를 좋아한다. "저희 로컬에서 개발하면서 바로 테스트 돌려요." 그게 Cypress의 철학이다. 개발자 경험에 집중한다. 특징:실시간 리로딩. 코드 저장하면 브라우저가 자동으로 테스트 다시 돌린다. TDD 하기 좋다.타임 트래블. 테스트 각 단계로 돌아갈 수 있다. DOM 스냅샷이 저장된다. 디버깅이 직관적이다.네트워크 모킹이 쉽다. cy.intercept()로 API 응답을 가짜로 만든다. 백엔드 없이 프론트 테스트 가능하다.DX가 최고다. 문서가 친절하다. 에러 메시지가 구체적이다. 커뮤니티가 활발하다.치명적 단점:단일 도메인만 된다. 탭 전환 안 된다. 다른 도메인 이동하면 꼬인다. OAuth 로그인 테스트가 어렵다. 우리는 Google 로그인 쓴다. 불가능하다.백엔드 테스트 약하다. API 테스트가 메인이 아니다. 프론트 중심이다.병렬 처리가 유료다. Cypress Dashboard 써야 한다. 월 75달러부터 시작이다. 오픈소스로는 순차 실행만 된다.브라우저 제한. Chrome, Firefox, Edge만 된다. Safari 안 된다.우리 테스트 시나리오 중 30%가 멀티 도메인이다. Cypress는 답이 안 나온다. 프론트 개발자들 로컬 테스트용으로는 좋다. E2E 메인 프레임워크로는 부족하다. 실전 비교: 같은 테스트를 세 가지로 공정한 비교를 위해 실험을 했다. 시나리오: 로그인 → 대시보드 → 리포트 생성 → 다운로드 → 로그아웃 복잡도 중간. 우리 일반적 테스트다. 개발 시간:Selenium: 4시간 (웨이트 튜닝에 1시간) Playwright: 2시간 Cypress: 3시간 (다운로드 처리 까다로움)실행 시간 (10회 평균):Selenium: 42초 Playwright: 18초 Cypress: 25초안정성 (50회 반복):Selenium: 43회 성공 (86%) Playwright: 50회 성공 (100%) Cypress: 48회 성공 (96%)코드 라인 수:Selenium: 85줄 Playwright: 52줄 Cypress: 58줄Playwright가 압도적이었다. 디버깅 시간 (의도적 버그 삽입):Selenium: 평균 12분 Playwright: 평균 5분 (Inspector 덕분) Cypress: 평균 6분 (타임 트래블 덕분)숫자는 거짓말 안 한다. 마이그레이션 시뮬레이션 CTO가 원한 건 현실적 검토였다. 우리 상황:테스트 케이스: 1200개 일일 커밋: 평균 25개 QA 팀: 4명 (자동화 담당 나 포함 2명) 예산: 넉넉하지 않음 타임라인: 분기별 목표 있음Playwright로 전환 시: Phase 1 (1개월):신규 기능 테스트만 Playwright로 작성 기존 Selenium 유지 병렬 운영 학습 기간 포함Phase 2 (2개월):Critical path 20% 마이그레이션 로그인, 결제, 회원가입 등 성공률 모니터링 팀 피드백 수집Phase 3 (3개월):나머지 80% 점진적 전환 주 30개씩 변환 Selenium deprecated 공지총 소요: 6개월 비용:개발 시간: 480시간 (나 + 동료) 급여 환산: 약 2400만원 CI 인프라 조정: 300만원 교육/학습: 무형 비용기대 효과:테스트 실행 시간: 3시간 → 1.2시간 (60% 단축) Flaky test: 15% → 3% (80% 감소) 유지보수 시간: 주 10시간 → 4시간 (60% 단축)ROI를 계산했다. 6개월 투자로 이후 매주 6시간 절약. 1년이면 312시간. 내 시급 3만원으로 계산하면 936만원. 2년이면 1872만원. 투자 대비 회수 기간: 약 18개월. 나쁘지 않다. 팀 설득 작업 수요일 오후. QA 팀 회의. "Playwright로 가는 거 어떻게 생각해?" 반응이 갈렸다. 매뉴얼 QA 출신 후배 (경력 2년): "Selenium도 이제 겨우 익숙해졌는데요... 또 배워야 해요?" 자동화 동료 (경력 5년): "좋긴 한데 IE 고객은요? 그냥 Selenium 4로 업그레이드하는 건요?" 신입 (경력 6개월): "저는 Playwright가 더 쉬운 것 같던데요. 학원에서 배울 때도 그게 더 쉬웠어요." 각자 입장이 다르다. 개발팀 회의도 했다. 프론트 리드: "좋아요. 저희는 Cypress 쓰고, QA는 Playwright 쓰고. 통합은 안 해요?" 백엔드 리드: "E2E가 빨라지면 저희 PR 리뷰가 빨라지나요? 그럼 찬성이에요." DevOps: "CI 파이프라인 다시 짜야 하는 거죠? 시간 주세요." CTO한테 보고했다. "투자 대비 효과가 명확하네요. 진행하세요. 단, 기존 테스트 안정성 떨어지면 안 됩니다." 압박이다. 현실적 결론 금요일 저녁. 결정을 내렸다. 선택: Playwright 이유:속도와 안정성. 숫자가 증명한다. 논쟁 여지 없다.장기 투자. 6개월 고생하면 이후 2년 편하다. 이직해도 이력서에 최신 기술 쓴다.팀 성장. 새 기술 배우는 게 동기부여된다. 다들 지루해하고 있었다.트렌드. 2024년 기준 Playwright가 대세다. GitHub 스타 60k. Selenium 30k 정체 중.단, 조건:IE 고객 예외 처리. 해당 기능만 Selenium 유지. 별도 파이프라인. 10%를 위해 90% 희생 안 한다.점진적 전환. 빅뱅 금지. 한 번에 바꾸면 망한다. 스프린트당 5% 목표.롤백 플랜. 실패하면 Selenium 복귀. 자존심 버린다.문서화. 모든 변경사항 기록. 다음 사람을 위해.Selenium을 버리는 게 아니다:모바일 테스트(Appium)는 계속 Selenium 기반 레거시 브라우저 테스트는 유지 스킬은 여전히 가치 있다Cypress는? 프론트팀 로컬 개발용으로 권장한다. E2E 메인은 Playwright. 역할이 다르다. 월요일부터 시작한다. 첫 주는 학습. 튜토리얼 돌리고 팀 세션 연다. 두려움 반 설렘 반이다. 1주일 후 실제로 시작했다. 신규 기능 "알림 설정" 테스트를 Playwright로 짰다. 소요 시간:예상: 3시간 실제: 5시간처음이라 헤맸다. 하지만 결과는 좋았다. Playwright 코드: def test_notification_settings(page): page.goto("/settings") page.click("text=알림 설정") page.check("#email-notification") page.click("button:has-text('저장')") # 자동 대기. time.sleep 없음. expect(page.locator(".success-message")).to_be_visible()깔끔하다. 읽기 쉽다. 같은 테스트 Selenium이었으면: def test_notification_settings(driver): driver.get("/settings") WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.LINK_TEXT, "알림 설정")) ).click() time.sleep(1) # 페이지 전환 대기 checkbox = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "email-notification")) ) checkbox.click() time.sleep(0.5) # 체크박스 애니메이션 driver.find_element(By.XPATH, "//button[contains(text(), '저장')]").click() time.sleep(2) # 저장 처리 대기 message = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, "success-message")) ) assert message.is_displayed()차이가 명확하다. 팀 동료가 코드 리뷰했다. "오... 진짜 짧네요. 이게 돌아가요?" 돌아간다. 50번 돌렸다. 50번 성공했다. 후배가 물었다. "저도 다음 테스트는 Playwright로 해봐도 돼요?" "해봐. 막히면 불러." 변화가 시작됐다. 2주 후 첫 위기 CI에서 Playwright 테스트가 터졌다. 로컬에서는 되는데 CI에서 실패한다. 클래식한 문제다. 에러: browserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium-1097/chrome-linux/chrome브라우저 바이너리가 없다. CI 설정 문제였다. 해결: # .github/workflows/test.yml - name: Install Playwright run: | pip install playwright playwright install --with-deps chromium--with-deps 옵션이 핵심이다. 시스템 의존성까지 설치한다. 다시 돌렸다. 성공했다. 하지만 시간이 오래 걸렸다. CI 실행 시간:Selenium: 15분 Playwright: 22분뭐가 문제인가. 프로파일링 했다. 브라우저 설치 시간이 7분이었다. 최적화: - name: Cache Playwright browsers uses: actions/cache@v3 with: path: ~/.cache/ms-playwright key: playwright-${{ hashFiles('**/requirements.txt') }}캐싱 추가했다. 결과:첫 실행: 22분 이후 실행: 8분Selenium보다 빠르다. DevOps 담당자가 만족했다. "CI 비용도 줄겠는데요?" GitHub Actions는 분 단위 과금이다. 월 500달러 쓰고 있었다. 40% 단축이면 월 200달러 절약이다. 연간 2400달러. 320만원. 부수 효과였다. 1개월 후 중간 점검 신규 테스트 25개를 Playwright로 짰다. 통계:평균 작성 시간: 2.2시간 (Selenium 대비 40% 단축) 평균 실행 시간: 15초 (Selenium 대비 65% 단축) Flaky 발생: 0건 팀 만족도: 5점 만점에 4.2점좋다. 하지만 문제도 있었다. 이슈 1: 스크린샷 용량 Playwright는 실패 시 자동으로 스크린샷을 찍는다. 좋은 기능이다. 하지만 CI 아티팩트 용량이 터졌다. 해결: # pytest.ini [pytest] playwright_screenshot = only-on-failure playwright_video = retain-on-failure필요한 것만 저장한다. 이슈 2: 팀 학습 곡선 매뉴얼 QA 출신 후배가 고전했다. "Locator가 뭐예요? Selector랑 다른 건가요?" Playwright의 개념이 Selenium과 달랐다. 해결: 매주 금요일 1시간 세션을 열었다.Week 1: Locator vs Selector Week 2: Auto-waiting 원리 Week 3: API testing Week 4: 디버깅 Tips효과가 있었다. 후배가 처음으로 Playwright 테스트를 혼자 완성했다. "생각보다 쉬워요. 웨이트 신경 안 써도 되니까 편해요." 성장이 보였다. 이슈 3: Selenium 테스트 방치 신규 테스트는 Playwright로 짠다. 기존 Selenium 테스트는 그대로다. 문제는 유지보수였다. 개발자가 UI를 바꾸면 Selenium 테스트가 깨진다. 고치기 귀찮다. Playwright로 다시 짜고 싶다. 하지만 계획은 점진적 전환이었다. 해결: 우선순위를 정했다.Critical path (결제, 로그인): 즉시 전환 자주 깨지는 테스트: 다음 분기 안정적인 테스트: 마지막급하게 하지 않는다. CTO한테 중간 보고했다. "순항 중입니다. 예상보다 빠릅니다." "좋네요. 근데 ROI는 언제 나와요?" "3개월 후부터 유지보수 시간이 줄어들 겁니다." "기대하겠습니다." 압박은 계속된다. 3개월 후 전환점 기존 Selenium 테스트 중 200개를 Playwright로 전환했다. 전체의 약 17%. 목표치(15%) 초과했다. 효과가 나타났다: 유지보수 시간:이전: 주 10시간 현재: 주 6.5시간Flaky test 비율:이전: 15% 현재: 8%CI 실행 시간:이전: 180분 현재: 145분숫자로 증명됐다. 결정적 사건: 대규모 UI 리뉴얼이 있었다. 디자인 시스템 전면 교체. 버튼, 입력창, 모달 전부 바뀌었다. Selenium 테스트 1000개가 다 깨졌다. 끔찍했다. 복구 시간 예상:Selenium: 약 80시간 (ID, 클래스명 다 바뀜) Playwright: 약 30시간 (텍스트 기반 Locator 많이 씀)실제로는:Selenium: 75시간 (4일) Playwright: 25시간 (1.5일)차이가 극명했다. CTO가 인정했다. "전환 잘한 것 같네요." 팀 사기도 올랐다. "다음 분기엔 더 많이 전환해볼까요?" 속도가 붙었다. 남은 과제들 아직 해결 못한 것들이 있다. 1. IE 고객 여전히 10%다. 줄지 않는다. 해당 기능은 Selenium 유지 중이다. 별도 파이프라인 돌린다. 언젠가는 IE 지원 중단할 것이다. 그때까지는 이중 운영이다. 2. 모바일 앱 Appium은 여전히 Selenium 기반이다. Playwright에 모바일 지원이 실험 단계에 있다. 아직 프로덕션 레디는 아니다. 당분간은 분
- 13 Dec, 2025
자동화 엔지니어 vs 개발자: 내 정체성은 뭘까
자동화 엔지니어 vs 개발자: 내 정체성은 뭘까 7년 차, 여전히 모르겠다 아침 10시. 출근해서 테스트 결과 확인했다. 밤새 돌린 E2E 테스트 327개 중 12개 실패. 로그 열어봤다. 8개는 타임아웃, 3개는 셀렉터 변경, 1개는 진짜 버그. 이 과정이 개발인지 QA인지 모르겠다. 7년 전 신입 때는 단순했다. 매뉴얼 QA. 클릭하고 확인하고 버그 리포트. 내 역할이 명확했다. QA는 QA였다. 4년 전 자동화로 넘어오면서 모호해졌다. 코드 짜고, 아키텍처 고민하고, 리팩토링하고. 이게 개발 아닌가? 어제 신입 개발자가 물었다. "J님은 개발자세요?" 잠깐 멈췄다. 답을 못 했다.매뉴얼 3년, 명확했던 시절 2018년. 첫 회사. QA팀 막내. 테스트 케이스 엑셀로 관리했다. 손으로 하나하나 클릭. 당시엔 단순했다. 기획서 보고 → 테스트 케이스 작성 → 실행 → 버그 리포트 → 회귀 테스트. 개발팀과 경계가 분명했다. 그들은 코드를 만들고, 우리는 검증했다. "QA는 품질의 파수꾼"이라는 말에 자부심 느꼈다. 버그 찾으면 뿌듯했다. 내 역할이 명확했다. 물론 힘들었다. 반복 작업. 야근. 회귀 테스트 지옥. 한 스프린트에 300개 테스트 케이스 손으로 돌렸다. 2년 차 되던 해, 팀장이 말했다. "자동화 배워볼래?" 그때는 몰랐다. 내 정체성이 흔들리기 시작할 줄. 자동화 시작, 코드와의 첫 만남 2021년. 새 회사로 이직. 자동화 포지션. 첫날 Selenium 설치했다. Python 기초 강의 들었다. 처음엔 간단했다. driver.find_element(By.ID, "login").click()"이거면 되네?" 싶었다. 3개월 후, 현실 직면했다.Flaky 테스트: 랜덤하게 실패하는 놈들 타임아웃 문제: Wait 조건 잡기 셀렉터 깨짐: UI 조금만 바뀌면 전부 수정 테스트 데이터 관리: DB 초기화는 어떻게?"이거 개발 아냐?" 생각했다. Page Object Model 배웠다. 디자인 패턴 공부했다. pytest fixture, conftest.py, 로그 관리, 리포트 생성. 6개월 후엔 프레임워크 설계했다. base_page.py 만들고, 공통 메서드 추출하고, config 관리하고. 동료 개발자가 코드 리뷰 달았다. "여기 중복 제거할 수 있어요." 그 순간 깨달았다. 나도 개발자처럼 일하고 있다는 걸.개발자인 듯 개발자 아닌 작년 봄. 개발팀 회의에 참석했다. 마이크로서비스 아키텍처 전환 논의. 프론트엔드 개발자: "API 스펙 바뀌면 통신 다시 짜야죠." 백엔드 개발자: "DB 마이그레이션 스크립트 필요해요." 나: "테스트 환경 구성은 어떻게 하죠?" 다들 고개 끄덕였다. 나도 의견 냈다. "서비스 간 통합 테스트가 복잡해질 텐데, 모킹 전략 필요합니다." 그 자리에선 동료였다. 개발자처럼. 회의 끝나고 슬랙 메시지 왔다. "J님, 회원가입 시나리오 손으로 한 번 테스트 부탁드려요." 순간 멈칫했다. 매뉴얼 테스트. 자동화했는데 왜 또 손으로? 물어봤다. "자동화 테스트로는 안 될까요?" "프로덕션 환경이라 자동화는 좀..." 아, 맞다. 나는 개발자가 아니구나. 급여는 개발자, 취급은... 연봉 협상 때. "자동화 엔지니어는 개발자급이니까 6500 드릴게요." 좋았다. 매뉴얼 QA 평균보다 1500 높았다. 근데 조직도를 보면 QA팀 소속. 팀명: "품질관리팀" 개발자 워크샵 있을 때. 초대 안 받았다. "개발 조직만 가는 거라서..." 컨퍼런스 지원 신청했다. "코드 짜시잖아요. 개발 컨퍼런스 가세요." 가서 발표했다. "E2E 테스트 자동화 프레임워크 구축기" 청중 질문: "근데 왜 개발팀에 안 계세요?" 답 못 했다. 사내 개발자 커뮤니티 있다. 가입 신청했다. "QA팀은... 음... 관심사가 다를 것 같아서..." 거절당했다. 급여는 개발자, 소속은 QA, 일은 둘 다. 나는 뭘까.SDET라는 새로운 선택지 6개월 전. 링크드인 헤드헌터 메시지. "SDET 포지션 관심 있으세요?" SDET. Software Development Engineer in Test. 처음 들어봤다. 찾아봤다.테스트 코드도 프로덕션 코드만큼 중요 개발팀 소속, 품질 책임 TDD, CI/CD 파이프라인 관리 테스트 인프라 개발"이거 나잖아?" JD 더 봤다.코딩 테스트 필수 자료구조, 알고리즘 능력 시스템 디자인 면접 프로덕션 코드 리뷰 참여긴장됐다. 내가 개발자 코딩 테스트를 통과할 수 있을까? LeetCode 시작했다. Easy 문제부터. Two Sum 풀었다. 30분 걸렸다. 개발자는 5분 만에 푸는 문제. "나는 아직 멀었구나." 코드는 짤 줄 알지만 내 GitHub 저장소.test-automation-framework: 스타 23개 api-testing-utils: 스타 8개 selenium-helper: 스타 15개전부 테스트 관련. 프로덕션 코드는? 없다. 기여한 오픈소스는? 테스트 툴만. 이력서 technical skills:Python, JavaScript (테스트용) Selenium, Appium, Pytest Jenkins, Docker (CI/CD) API Testing, E2E Testing개발자 이력서랑 비교했다.Python, JavaScript (프로덕션) Django, React AWS, Kubernetes RESTful API 설계, 마이크로서비스방향이 다르다. 작년에 프로덕션 코드 한 번 짰다. 테스트 환경 초기화 스크립트. 200줄. 시니어 개발자가 리뷰했다. "여기 에러 핸들링 약하네요." "로깅 레벨 잘못 잡았어요." "이건 유틸로 빼는 게 좋겠어요." 수정하는데 3일 걸렸다. 개발자들은 하루에 이런 코드 수백 줄 짠다. 나는 200줄에 3일. "나는 개발자가 아니구나." 다시 깨달았다. 정체성 혼란의 순간들 순간 1: 채용 공고 "QA 자동화 엔지니어 채용" 요구사항: 3년 이상 개발 경험 개발 경험? 나는 QA 경험 7년인데. 순간 2: 이직 면접 면접관: "본인은 QA 출신인가요, 개발 출신인가요?" 나: "QA로 시작했지만 지금은..." 면접관: "아, QA 출신이시네요." 탈락했다. 순간 3: 팀 회식 개발팀장: "J님은 뭐 하세요?" 나: "자동화 엔지니어요." 개발팀장: "아, 테스터?" 아니라고 하고 싶었다. 근데 맞는 말 같기도 하고. 순간 4: 연봉 협상 "QA는 올해 3% 인상입니다." "근데 저 코드 짜잖아요." "그래도 QA팀이니까요." 억울했다. 순간 5: 프로젝트 회고 PM: "개발은 잘 끝났고, QA는..." 나: "저도 개발했는데요. 테스트 인프라." PM: "아 네, QA 자동화 잘하셨어요." 개발으로 인정 안 받는 느낌. 양쪽에서 다 어중간한 QA 관점에서 보면: "J님은 매뉴얼 감각이 떨어져요." "요즘 손으로 안 해봐서 그래요." 손으로 안 하는 이유? 자동화했으니까. 근데 그게 단점이 된다. 후배 QA가 찾은 UI 버그. "이거 자동화 테스트에서 왜 안 잡았어요?" 시각적 요소. 픽셀 단위 레이아웃. 자동화로 잡기 어렵다. "자동화가 만능은 아니거든." "그럼 뭐 하러 자동화해요?" 할 말 없었다. 개발 관점에서 보면: "테스트 코드 품질이 낮아요." "프로덕션 코드처럼 관리해야죠." 노력했다. 리팩토링했다. 커버리지 올렸다. 근데 개발자가 보면 여전히 부족하다. "이런 건 디자인 패턴 쓰면 좋은데..." "성능 테스트는 k6가 낫지 않을까요?" 알고는 있다. 근데 시간이 없다. 테스트 케이스 늘리는 게 우선이니까. 양쪽에서 다 중간이다. QA 중에선 제일 코드 잘 짜는 사람. 개발자 중에선 제일 테스트만 하는 사람. 자동화의 함정 자동화 시작할 때 생각했다. "이거 다 자동화하면 나는 뭐 하지?" 4년 차. 답 나왔다. 자동화 유지보수. 개발팀이 UI 리뉴얼했다. 테스트 스크립트 380개 깨졌다. 2주 동안 고쳤다. 셀렉터 전부 수정. 끝나자마자 또 깨졌다. API 스펙 변경. Flaky 테스트. 랜덤 실패하는 놈들. 원인 찾는데 3일. 고치는데 1시간. "자동화하면 편할 줄 알았는데..." 유지보수가 개발보다 어렵다. 내가 짠 코드지만 6개월 후엔 낯설다. 주석 없으면 이해 못 한다. "이거 왜 이렇게 짰지?" 테스트 커버리지 80%. "나머지 20%는요?" "자동화 어려운 케이스예요." "그럼 손으로 해야죠." 결국 매뉴얼도 병행. 자동화 엔지니어인데 손으로 테스트. 이게 맞나 싶다. 커리어 고민, SDET로 갈까 링크드인 봤다. SDET 채용 공고.구글: SDET, L4, $180K 페이스북: Software Engineer, Testing, E5 넷플릭스: Senior SDET공통점: 개발팀 소속. 요구사항 봤다.코딩 테스트 (LeetCode Medium 이상) 시스템 디자인 테스트 전략 설계 프로덕션 코드 기여마지막이 관건이다. 프로덕션 코드. 내 경험:테스트 코드 4년 프로덕션 코드 0년JD에 "테스트 코드도 프로덕션 코드"라고 써있다. 위안 삼았다. 지원했다. 스타트업 SDET. 1차 코딩 테스트. Medium 2문제. 첫 문제: Binary Tree Level Order Traversal 45분 걸렸다. 제한 시간 30분. 탈락. "아직 멀었구나." LeetCode 매일 풀기 시작했다. 퇴근하고 2시간씩. 한 달 후 다시 지원. 다른 회사. 코딩 테스트 통과. 2차 기술 면접. "테스트 인프라 어떻게 설계하셨나요?" 대답했다. 내 경험 기반. 면접관이 고개 끄덕였다. "근데 프로덕션 API는 개발해보셨어요?" "...아니요." "SDET는 기능 개발도 하거든요." 또 탈락. 내가 원하는 건 뭘까 깊이 생각해봤다. 개발자가 되고 싶은가? 100% 아니다. QA로 남고 싶은가? 100% 아니다. 그럼 뭘 원하는가?코드로 문제 해결하고 싶다 품질에 대한 책임감을 유지하고 싶다 개발자와 동등하게 대우받고 싶다 테스트만 하는 사람은 되기 싫다 기능 개발만 하는 사람도 되기 싫다모순이다. SDET가 답일까? 어쩌면 맞다. 어쩌면 아니다. SDET 된다고 정체성 혼란 사라질까? 모르겠다. 결국 라벨 문제가 아닐 수도. "나는 무엇을 하는 사람인가"가 중요한 거지. 테스트 코드 짜는 개발자? 개발하는 QA? 둘 다 맞는 것 같다. 둘 다 틀린 것 같기도. 3개월 후, 작은 변화 포지션 타이틀 바꿨다. "QA 자동화 엔지니어" → "품질 엔지니어(Quality Engineer)" QA 빼니까 기분이 다르다. 팀 회의 때 말투 바꿨다. "이거 테스트해볼게요" → "이거 검증 로직 구현할게요" 사소하지만 차이 있다. 개발팀 코드 리뷰 참여 시작했다. "테스트 가능한 코드인가" 관점으로. "여기 의존성 주입하면 목킹 쉬울 것 같아요." 개발자: "오, 좋은데요?" PR 머지됐다. 내 리뷰로. 기여한 느낌. 처음이다. 사이드 프로젝트 시작했다. 간단한 웹앱. Todo 리스트. 프로덕션 코드 짜봤다. React, Node.js, MongoDB. 테스트 코드도 짰다. 당연히. 2주 만에 완성. 배포했다. "나도 개발할 수 있구나." 물론 프로 개발자 수준은 아니다. 근데 할 수 있다는 게 중요하다. 이력서 업데이트했다. "Full-stack 경험 있음 (사이드 프로젝트)" 거짓말은 아니다. 여전히 답은 모른다 지금도 모른다. 내가 뭔지. 출근해서 코드 짠다. 퇴근하고도 코드 짠다. 주말엔 LeetCode 푼다. 월요일엔 테스트 리포트 쓴다. 어떤 날은 개발자 같다. 어떤 날은 QA 같다. 근데 요즘은 괜찮다. 굳이 하나일 필요 있나? 하이브리드면 어때? 개발도 하고 테스트도 하는 사람. 품질도 책임지고 코드도 짜는 사람. 라벨이 뭐든 상관없다. 내가 하는 일이 중요하다. "자동화 엔지니어세요?" "네, 품질 엔지니어이기도 하고, 때론 개발자이기도 해요." 이제 이렇게 답한다. 더 이상 멈칫하지 않는다. SDET로 갈지 모른다. 안 갈 수도 있다. 중요한 건 계속 성장하는 것. 코드도, 테스트도, 품질도. 7년 차. 여전히 모르지만. 괜찮다. 계속 찾아가는 중이다.정체성은 명함이 아니라 내가 하는 일로 정의되는 거다.
- 09 Dec, 2025
Appium으로 모바일 자동화 시작하기: 안드로이드와 iOS의 차이
Appium 시작하고 3일 만에 깨달은 것 Appium 공부 시작했다. 회사에서 모바일 앱 자동화하라고 했다. 첫날은 설치만 했다. 두 번째 날은 에러만 봤다. 셋째 날 실행됐는데 안드로이드만 됐다. iOS는 또 다른 세계더라. 크로스 플랫폼이라고 했다. 같은 코드로 두 플랫폼 테스트 가능하다고 했다. 반은 맞다. 반은 거짓말이다. 기본 개념은 같다. 세부 구현은 완전히 다르다. 지금 Appium 3개월 썼다. 안드로이드와 iOS 둘 다 돌린다. 아직도 매일 새로운 이슈 만난다. 이게 모바일 자동화의 현실이다.안드로이드 시작: 상대적으로 쉬운 편 안드로이드부터 했다. 설정이 iOS보다 직관적이다. Android Studio 깔고, SDK 설치하고, AVD 만들면 된다. 에뮬레이터 띄우는 데 10분 걸렸다. UIAutomator2가 기본이다. 요소 찾기가 편하다. resource-id, content-desc, text로 찾으면 대부분 된다. driver.find_element(AppiumBy.ID, "com.app:id/button") 이렇게. 앱 빌드도 간단하다. APK 파일 하나면 끝이다. 개발자한테 디버그 APK 받아서 adb install app.apk 치면 설치된다. 권한 문제도 별로 없다. 실제 코드 짜보니까 금방 돌았다. driver.find_element(AppiumBy.ID, "username").send_keys("test") driver.find_element(AppiumBy.ID, "password").send_keys("1234") driver.find_element(AppiumBy.ID, "login_button").click()이거 5분 만에 작동했다. 문제는 버전이다. 안드로이드 파편화가 심하다. API 28, 29, 30, 31 다 테스트해야 한다. 에뮬레이터 5개 만들었다. 디스크 용량 50GB 날아갔다. 스크롤도 플랫폼마다 다르다. 새로운 UiScrollable 쓰면 되는데 문법이 특이하다. driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiScrollable(new UiSelector()).scrollIntoView(text("원하는텍스트"))') 처음엔 이게 뭔가 싶었다. 그래도 안드로이드는 괜찮다. 디버깅이 쉽다. Appium Inspector로 요소 바로 확인된다. xpath 복잡해도 일단 동작한다.iOS 진입: 벽이 높다 iOS 시작했다. 막혔다. 설정부터 복잡하다. Xcode 깔고, Command Line Tools 설정하고, WebDriverAgent 빌드해야 한다. 첫날 4시간 걸렸다. 제대로 안 됐다. Provisioning Profile이 문제였다. 서명 이슈다. 무료 Apple ID로는 7일마다 재빌드해야 한다. 회사 계정 쓰면 1년인데 설정이 어렵다. 시뮬레이터는 빠르다. 그런데 Mac만 된다. 윈도우 개발자는 테스트 못 돌린다. 우리 팀 절반이 윈도우 쓴다. 문제다. XCUITest가 기본 드라이버다. 요소 찾기가 다르다. 안드로이드처럼 ID 없다. accessibility identifier 써야 한다. driver.find_element(AppiumBy.ACCESSIBILITY_ID, "LoginButton") 개발자가 accessibility 안 넣으면 못 찾는다. 처음엔 xpath만 썼다. //XCUIElementTypeButton[@name="Login"] 느리고 불안정하다. 개발자한테 요청했다. 버튼, 텍스트 필드 타입이 XCUIElement로 시작한다. XCUIElementTypeButton, XCUIElementTypeTextField 이런 식이다. 안드로이드랑 완전히 다르다. 코드 재사용 안 된다. 실기기는 더 복잡하다. UDID 등록해야 한다. idevice_id -l 로 확인하고 capabilities에 넣는다. 케이블로 연결해야 한다. 무선은 불안정하다. 앱 설치도 까다롭다. .app 파일이 시뮬레이터용이다. 실기기는 .ipa 파일 필요하다. 서명도 맞아야 한다. 개발자한테 Ad-hoc 빌드 받아야 한다. iOS 13 이후로 권한 팝업이 많다. "Allow 'App' to access your location?" 이런 거. 자동으로 처리 안 된다. 직접 클릭 코드 짜야 한다. try: alert = driver.find_element(AppiumBy.XPATH, "//XCUIElementTypeButton[@name='Allow']") alert.click() except: pass예외 처리 안 하면 테스트 멈춘다. 한 달 걸렸다. iOS 자동화 제대로 돌리는 데. 안드로이드의 3배 시간 들었다. 아직도 가끔 서명 만료로 깨진다. 플랫폼별 Desired Capabilities: 설정이 다르다 안드로이드 capabilities는 간단하다. android_caps = { "platformName": "Android", "platformVersion": "12", "deviceName": "Pixel_5_API_31", "app": "/path/to/app.apk", "automationName": "UiAutomator2", "appPackage": "com.example.app", "appActivity": ".MainActivity" }appPackage랑 appActivity 넣으면 된다. APK 안 넣고 이미 설치된 앱 실행 가능하다. adb shell pm list packages 로 패키지명 확인한다. iOS는 복잡하다. ios_caps = { "platformName": "iOS", "platformVersion": "16.0", "deviceName": "iPhone 14", "app": "/path/to/app.app", "automationName": "XCUITest", "bundleId": "com.example.app", "xcodeOrgId": "TEAMID", "xcodeSigningId": "iPhone Developer", "udid": "device-udid-here" }실기기는 UDID 필수다. xcodeOrgId도 넣어야 한다. 서명 관련 설정 빠뜨리면 에러 난다. 처음엔 뭐가 필요한지 몰라서 하나씩 추가했다. autoAcceptAlerts 옵션이 있다. 권한 팝업 자동 승인한다. "autoAcceptAlerts": True근데 완벽하지 않다. 커스텀 팝업은 직접 처리해야 한다. noReset 옵션도 중요하다. "noReset": TrueTrue면 앱 상태 유지한다. 로그인 풀리지 않는다. False면 매번 초기화한다. 깨끗하지만 느리다. 테스트 목적에 따라 바꾼다. 로그인 테스트는 noReset False. 로그인 후 기능 테스트는 True. 두 플랫폼 동시에 돌리려면 코드 분기해야 한다. if platform == "android": caps = android_caps else: caps = ios_caps driver = webdriver.Remote("http://localhost:4723", caps)처음엔 하나로 합치려 했다. 안 됐다. 공통 부분만 베이스로 두고 나머지 분리했다. 유지보수가 더 쉬웠다.에뮬레이터 vs 실기기: 무엇을 선택할까 에뮬레이터부터 시작했다. 설정이 쉽다. Android Studio AVD Manager로 몇 번 클릭하면 만들어진다. Xcode 시뮬레이터도 바로 뜬다. 속도가 빠르다. 테스트 실행이 실기기보다 2배 빠르다. CI/CD 파이프라인에 넣기 좋다. Jenkins에서 돌리는데 문제없다. 비용이 없다. 무제한으로 만들 수 있다. 안드로이드 에뮬레이터 5개 띄워서 병렬 테스트 돌린다. 실기기면 불가능하다. 하지만 한계가 있다. 실제 사용자 환경이 아니다. 카메라, GPS, 블루투스 테스트 안 된다. 하드웨어 의존 기능은 실기기 필수다. 성능도 다르다. 에뮬레이터가 너무 빠르다. 실기기에서는 느린 애니메이션 기다려야 하는데 에뮬레이터는 바로 넘어간다. 타이밍 이슈 생긴다. 네트워크도 차이 난다. 에뮬레이터는 호스트 네트워크 쓴다. 실기기는 WiFi나 LTE다. 속도 다르다. 네트워크 지연 테스트는 실기기로 해야 한다. 우리 팀 전략은 이렇다.개발 중: 에뮬레이터로 빠르게 테스트 PR 머지 전: 실기기 1대로 스모크 테스트 릴리즈 전: 실기기 여러 대로 풀 테스트실기기 관리가 일이다. 충전 신경 써야 한다. 케이블 빠지면 테스트 멈춘다. 회사 돈 들여서 기기 팜 만들었다. 안드로이드 실기기 3대, iOS 2대 있다. 삼성, LG, 픽셀 / 아이폰 12, 13 주요 버전 커버한다. USB 허브로 다 연결했다. 동시 테스트 가능하다. Appium Grid 쓰면 병렬로 돌린다. 5대 동시 실행하면 30분 → 6분 걸린다. 기기 이름 라벨 붙였다. 헷갈린다. "Android_Samsung_S21", "iOS_iPhone13" 코드에서 deviceName으로 지정한다. 처음엔 실기기만 고집했다. 시간 오래 걸렸다. 지금은 적절히 섞는다. 상황에 맞게. 완벽한 정답은 없다. 요소 찾기 전략: ID vs Accessibility vs XPath 안드로이드는 resource-id가 최고다. driver.find_element(AppiumBy.ID, "com.app:id/login_button")빠르고 안정적이다. 화면 구조 바뀌어도 동작한다. 개발자한테 고유 ID 달라고 한다. 처음엔 안 넣어줬다. 설득했다. "자동화 안 되면 수동으로 계속 테스트해야 합니다" 그랬더니 넣어줬다. text로 찾는 건 위험하다. driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'text("로그인")')텍스트 바뀌면 깨진다. 다국어 지원하면 더 문제다. 영어 버전 테스트는 "Login"으로 바꿔야 한다. content-desc도 좋다. 접근성 레이블이다. driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button")안드로이드와 iOS 둘 다 쓸 수 있다. 크로스 플랫폼 코드 작성할 때 유용하다. iOS는 accessibility identifier가 표준이다. driver.find_element(AppiumBy.ACCESSIBILITY_ID, "LoginButton")개발자가 accessibilityIdentifier 속성 넣어줘야 한다. Swift 코드로 이렇게 넣는다고 한다. button.accessibilityIdentifier = "LoginButton"없으면 XPath 써야 한다. XPath는 최후의 수단이다. driver.find_element(AppiumBy.XPATH, "//XCUIElementTypeButton[@name='로그인']")느리다. 화면 구조 바뀌면 바로 깨진다. 3초 걸리던 게 XPath 쓰면 10초 걸린다. 복잡한 XPath는 더 나쁘다. driver.find_element(AppiumBy.XPATH, "//XCUIElementTypeOther[2]/XCUIElementTypeOther[1]/XCUIElementTypeButton[3]")이건 유지보수 불가능하다. 절대 쓰지 말 것. Class name으로도 찾는다. driver.find_element(AppiumBy.CLASS_NAME, "android.widget.Button")여러 개 나온다. 인덱스로 접근해야 한다. buttons = driver.find_elements(AppiumBy.CLASS_NAME, "android.widget.Button") buttons[2].click()위험하다. 버튼 순서 바뀌면 끝이다. 우리 팀 규칙은 이렇다.먼저 ID 또는 accessibility identifier 확인 없으면 개발자한테 요청 급하면 임시로 XPath, 나중에 리팩토링 text는 상수로 관리페이지 오브젝트 패턴 쓴다. class LoginPage: USERNAME = (AppiumBy.ID, "username") PASSWORD = (AppiumBy.ID, "password") LOGIN_BTN = (AppiumBy.ACCESSIBILITY_ID, "login_button") def login(self, user, pw): self.driver.find_element(*self.USERNAME).send_keys(user) self.driver.find_element(*self.PASSWORD).send_keys(pw) self.driver.find_element(*self.LOGIN_BTN).click()로케이터 한 곳에 모았다. 변경 쉽다. 테스트 코드는 깔끔하다. login_page = LoginPage(driver) login_page.login("test", "1234")처음엔 테스트 코드에 직접 썼다. 지옥이었다. 화면 하나 바뀌면 10개 파일 수정했다. 리팩토링하는 데 2주 걸렸다. 후회했다. 플랫폼별 제스처: 스와이프, 탭, 스크롤 터치 제스처가 핵심이다. 모바일은 마우스 없다. 안드로이드와 iOS 구현 방법이 다르다. 안드로이드 스와이프는 TouchAction 쓴다. from appium.webdriver.common.touch_action import TouchActionaction = TouchAction(driver) action.press(x=500, y=1000).wait(1000).move_to(x=500, y=300).release().perform()좌표 기반이다. 화면 크기마다 다르다. 상대 좌표로 바꿨다. size = driver.get_window_size() start_x = size['width'] * 0.5 start_y = size['height'] * 0.8 end_y = size['height'] * 0.2action.press(x=start_x, y=start_y).wait(1000).move_to(x=start_x, y=end_y).release().perform()모든 기기에서 동작한다. iOS는 좀 다르다. W3C Actions API 쓴다. driver.execute_script("mobile: swipe", {"direction": "up"})간단하다. 하지만 커스터마이징 어렵다. 정교한 스와이프는 여전히 좌표 써야 한다. from selenium.webdriver.common.actions import interaction from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions.pointer_input import PointerInputactions = ActionBuilder(driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch")) actions.pointer_action.move_to_location(500, 1000) actions.pointer_action.pointer_down() actions.pointer_action.pause(1) actions.pointer_action.move_to_location(500, 300) actions.pointer_action.release() actions.perform()복잡하다. 처음엔 이해 안 됐다. 스크롤은 더 까다롭다. 안드로이드는 UiScrollable 쓴다. driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(text("찾을텍스트"))')텍스트 찾을 때까지 스크롤한다. iOS는 그런 게 없다. 직접 구현해야 한다. def scroll_to_element(driver, element_id): max_swipes = 10 for i in range(max_swipes): try: element = driver.find_element(AppiumBy.ACCESSIBILITY_ID, element_id) return element except: size = driver.get_window_size() driver.execute_script("mobile: scroll", {"direction": "down"}) raise Exception(f"Element {element_id} not found after {max_swipes} swipes")무한 스크롤은 더 어렵다. 이전 요소 저장하고 같으면 멈춘다. prev_page_source = "" while True: current_page_source = driver.page_source if prev_page_source == current_page_source: break # 스크롤 로직 prev_page_source = current_page_source탭과 클릭은 거의 같다. element.click()가끔 안 될 때 있다. 좌표로 직접 탭한다. location = element.location size = element.size x = location['x'] + size['width'] / 2 y = location['y'] + size['height'] / 2action = TouchAction(driver) action.tap(x=x, y=y).perform()더블 탭은 빠르게 두 번 탭한다. action.tap(x=x, y=y).wait(100).tap(x=x, y=y).perform()롱 프레스는 press().wait().release()다. action.press(x=x, y=y).wait(2000).release().perform()2초 누른다. 컨텍스트 메뉴 나온다. 핀치 줌은 복잡하다. 두 손가락 시뮬레이션이다. 거의 안 쓴다. 필요하면 개발자한테 버튼 만들라고 한다. 제스처 코드는 헬퍼 함수로 만들었다. class GestureHelper: @staticmethod def swipe_up(driver): # 구현 @staticmethod def swipe_down(driver): # 구현 @staticmethod def scroll_to_text(driver, text): # 구현테스트에서 이렇게 쓴다. GestureHelper.swipe_up(driver) GestureHelper.scroll_to_text(driver, "설정")깔끔하다. 재사용 쉽다. 크로스 플랫폼 코드 작성: 공통과 분기 처음엔 한 코드로 두 플랫폼 테스트하려 했다. 불가능했다. 요소 찾기부터 다르다. 공통 부분만 추출했다. 테스트 로직이다. def test_login(driver): login_page = get_login_page(driver) login_page.enter_username("test") login_page.enter_password("1234") login_page.click_login() assert login_page.is_logged_in()get_login_page()가 플랫폼 구분한다. def get_login_page(driver): platform = driver.capabilities['platformName'] if platform == 'Android': return AndroidLoginPage(driver) else: return IOSLoginPage(driver)각 페이지 클래스는 같은 인터페이스 구현한다. class LoginPageBase: def enter_username(self, username): raise NotImplementedError def enter_password(self, password): raise NotImplementedError def click_login(self): raise NotImplementedError def is_logged_in(self): raise NotImplementedErrorclass AndroidLoginPage(LoginPageBase): USERNAME = (AppiumBy.ID, "com.app:id/username") PASSWORD = (AppiumBy.ID, "com.app:id/password") LOGIN_BTN = (AppiumBy.ID, "com.app:id/login") def enter_username(self, username): self.driver.find_element(*self.USERNAME).send_keys(username) # ...class IOSLoginPage(LoginPageBase): USERNAME = (AppiumBy.ACCESSIBILITY_ID, "UsernameField") PASSWORD = (AppiumBy.ACCESSIBILITY_ID, "PasswordField") LOGIN_BTN = (AppiumBy.ACCESSIBILITY_ID, "LoginButton") def enter_username(self, username): self.driver.find_element(*self.USERNAME).send_keys(username) # ...로케이터만 다르다. 메서드는 동일하다. 테스트 코드는 플랫폼 몰라도 된다. 제스처도 분기한다. class GestureHelper: @staticmethod def swipe_up(driver): platform = driver.capabilities['platformName'] if platform == 'Android': # 안드로이드 구현 else: # iOS 구현처음엔 if문 남발했다. 코드가 더러웠다. 지금은 클래스 분리했다. 깔끔하다. 설정 파일도 나눴다. # config/android.py ANDROID_CAPS = { "platformName": "Android", # ... }# config/ios.py IOS_CAPS = { "platformName": "iOS", # ... }pytest fixture로 드라이버 생성한다. @pytest.fixture def driver(request): platform = request.config.getoption("--platform") if platform == "android": caps = ANDROID_CAPS else: caps = IOS_CAPS driver = webdriver.Remote("http://localhost:4723", caps) yield driver driver.quit()명령어로 플랫폼 지정한다. pytest --platform=android pytest --platform=ios양쪽 다 돌리려면 parameterize 쓴다. @pytest.mark.parametrize("platform", ["android", "ios"]) def test_login(platform): driver = get_driver(platform) # 테스트 로직CI/CD에서는 병렬로 돌린다. jobs: android: runs-on: ubuntu-latest steps: - run: pytest --platform=android ios: runs-on: macos-latest steps: - run: pytest --platform=ios안드로이드는 우분투에서, iOS는 맥에서 돌린다. 동시에 실행된다. 시간 절반으로 줄었다. 완전한 크로스 플랫폼은 불가능하다. 70% 정도 공통화 가능하다. 나머지는 분기해야 한다. 처음부터 이렇게 설계하면 유지보수 쉽다. 실제로 겪은 문제들과 해결 키보드 문제가 제일 짜증났다. 안드로이드는 키보드 올라오면 화면 가린다. 아래쪽 버튼 못 찾는다. driver.hide_keyboard() 쓴다. element.send_keys("text") driver.hide_keyboard() driver.find_element(AppiumBy.ID, "submit").click()iOS는 키보드 내리기 어렵다. 화면 빈 곳 탭하거나 Return 키 쳐야 한다. driver.find_element(AppiumBy.XPATH, "//XCUIElementTypeButton[@name='Return']").click
- 07 Dec, 2025
자동화 테스트도 결국 버그다: 테스트 코드 리뷰 체크리스트
자동화 테스트도 결국 버그다: 테스트 코드 리뷰 체크리스트 오늘 아침 CI 알림 확인했다. 빌드 실패. 다시 돌렸다. 성공. 세 번째. 실패. Flaky 테스트다. 또. 테스트 코드에 버그가 있으면 프로덕션 코드 버그는 찾는다. QA니까. 테스트 코드 버그는? 누가 찾나. 지난달 일이다. 결제 로직 수정했다. 자동화 테스트 전부 통과. 배포했다. 다음 날 고객 문의 폭주. 결제 안 된다.테스트 코드를 봤다. assert response.status_code == 200 이게 전부였다. 실제 결제는 안 됐다. 응답만 200이었다. 테스트는 통과. 버그는 프로덕션. 이날 배웠다. 테스트 코드도 결국 코드다. 코드면 버그가 있다. 가짜 안심이 제일 위험하다 자동화 테스트가 있으면 안심한다. 당연하다. 그린 체크 보면 '됐다'고 생각한다. 근데 그 체크가 거짓말이면. 3개월 전 상황. 로그인 테스트 500개. 전부 통과. 근데 실제로는 아무도 로그인 못 했다. 이유? 테스트 환경에는 DB에 테스트 계정이 있었다. 프로덕션에는 없었다. 테스트는 항상 성공. 현실은 실패. def test_login(): response = login("test@test.com", "password") assert response.status_code == 200 # 실제 로그인 됐는지는 안 봄통과한다. 매번. 근데 의미가 없다. 내가 만든 테스트 코드 체크리스트 4년 동안 삽질하면서 만들었다. PR마다 이거 본다. 1. 진짜 검증하는가 # 나쁜 예 def test_user_creation(): response = create_user({"name": "test"}) assert response.status_code == 201# 좋은 예 def test_user_creation(): response = create_user({"name": "test"}) assert response.status_code == 201 user = get_user(response.data.id) assert user.name == "test" assert user.created_at is not None상태 코드만 보면 안 된다. DB도 봐야 한다. 실제 데이터도. 작년에 회원가입 테스트가 있었다. 201 반환하면 통과. 근데 DB에는 안 들어갔다. 트랜잭션이 롤백됐는데 응답은 보냈다. 테스트는 몰랐다. 201만 봤으니까.2. 독립적인가 테스트 순서 바꿔봤나. A 테스트가 B 테스트에 의존하면 안 된다. # 나쁜 예 def test_1_create_product(): global product_id product_id = create_product().iddef test_2_update_product(): update_product(product_id) # test_1에 의존이거 진짜 많다. 병렬 실행하면 깨진다. 순서 바뀌면 깨진다. 2년 전에 이것 때문에 3일 날렸다. test_1이 실패하면 test_2도 실패. test_2가 진짜 문제인지 몰랐다. 각 테스트마다 setup, teardown 해야 한다. 귀찮다. 근데 해야 한다. 3. 명확한가 6개월 후에 내가 봐도 이해되나. # 나쁜 예 def test_api(): r = call(1, 2, True) assert r == 3# 좋은 예 def test_discount_applies_to_premium_users(): user = create_premium_user() product = create_product(price=10000) order = create_order(user, product, use_discount=True) assert order.final_price == 9000 assert order.discount_amount == 1000변수명만 봐도 알아야 한다. 매직 넘버 쓰지 말고. 의도가 보여야 한다. 작년에 후배가 쓴 테스트 봤다. test_case_1, test_case_2, test_case_3 뭘 테스트하는지 몰랐다. 후배도. 4. 빠른가 10분 걸리는 테스트는 안 돌린다. 안 돌리면 의미 없다. 우리 팀 E2E 테스트. 처음엔 45분 걸렸다. 아무도 로컬에서 안 돌렸다. CI에서만 돌렸다. PR 올리고 45분 기다렸다. 지금은 12분. 병렬 처리했다. 불필요한 sleep 제거했다. fixture 재사용했다. # 나쁜 예 def test_workflow(): time.sleep(5) # "혹시 몰라서" check_status() time.sleep(5) # 또# 좋은 예 def test_workflow(): wait_until(lambda: status_is_ready(), timeout=10) check_status()sleep은 악이다. 대부분 필요 없다. 필요하면 조건부로 기다려야 한다. 5. 안정적인가 100번 돌려서 100번 같은 결과 나오나. Flaky 테스트가 제일 짜증난다. 가끔 실패한다. 이유 모르겠다. 다시 돌리면 통과한다.우리 팀 규칙. Flaky 테스트 발견하면 바로 비활성화. 고칠 때까지 안 돌린다. 믿을 수 없는 테스트는 없는 것보다 나쁘다. 실패해도 "또 Flaky겠지" 하면 끝이다. 진짜 버그도 무시하게 된다. 흔한 원인들:타이밍 이슈 (sleep으로 해결하려는 순간 졌다) 공유 리소스 (DB, 파일, 네트워크) 랜덤 데이터 시간 의존성 (datetime.now() 쓰면...) 외부 API실제 PR 리뷰에서 본 것들 Case 1: 아무것도 안 하는 테스트 def test_send_email(): send_email("test@test.com", "subject", "body") # assert 없음이거 놀랍게도 많다. 함수 호출만 한다. 검증 없다. 에러 안 나면 통과. 근데 이메일 안 가도 에러 안 난다. 테스트는 통과. 의미는 없다. Case 2: 너무 많이 테스트 def test_user_api(): # 100줄 # 10가지 검증 # 3개 API 호출 # ...하나 실패하면 뭐가 문제인지 모른다. 테스트 하나는 하나만 검증해야 한다. Case 3: 프로덕션 코드 복사 def calculate_discount(price): return price * 0.9 if price > 10000 else pricedef test_discount(): price = 15000 expected = price * 0.9 if price > 10000 else price assert calculate_discount(price) == expected이건 같은 로직을 두 번 쓴 거다. 둘 다 틀려도 통과한다. 기대값은 명확해야 한다. 계산하지 말고 적어야 한다. def test_discount(): assert calculate_discount(15000) == 13500 assert calculate_discount(5000) == 5000Case 4: try-except 남용 def test_error_handling(): try: dangerous_operation() assert False, "should raise error" except: pass # 통과어떤 에러든 잡는다. AssertionError도 잡는다. 테스트 로직 에러도 잡는다. def test_error_handling(): with pytest.raises(SpecificError): dangerous_operation()이렇게 해야 한다. 구체적인 에러만 기대한다. 테스트 코드 리뷰할 때 내가 보는 것 PR 올라오면 이것들 본다.커버리지가 아니라 의미90% 커버리지는 중요하지 않다 중요한 로직 제대로 검증하나가 중요하다네이밍test_1 같은 거 보이면 바로 코멘트 테스트명이 문서다픽스처 재사용같은 setup 코드 반복하면 안 된다 유지보수 지옥된다외부 의존성API 호출하면 mock해야 한다 DB는 트랜잭션 롤백해야 한다에러 케이스Happy path만 테스트하는 거 많다 실패 케이스가 더 중요하다지난주에 리뷰했던 PR. 성공 케이스 10개. 실패 케이스 0개. "에러 처리는요?" "그건 개발자가 잘 했을 거예요" 안 한다. 개발자는 Happy path만 생각한다. 우리가 엣지 케이스 봐야 한다. 자동화의 함정 자동화하면 다 된 것 같다. 아니다. 자동화는 반복 작업을 줄인다. 판단은 못 한다. 잘못된 자동화는 수동보다 나쁘다. 수동이면 사람이 본다. 뭔가 이상하면 안다. 자동화는 시킨 것만 한다. 틀려도 모른다. 2년 전에 UI 자동화 했다. 버튼 클릭하고 텍스트 확인했다. 텍스트는 맞았다. 근데 레이아웃이 깨졌다. 버튼이 화면 밖으로 나갔다. 텍스트만 보는 테스트는 통과했다. 배포하고 디자이너한테 혼났다. 시각적 회귀 테스트 추가했다. 스크린샷 찍어서 비교한다. 1픽셀 차이도 잡는다. 근데 이것도 문제다. 의도된 변경도 실패로 본다. 매번 베이스라인 업데이트해야 한다. 자동화는 만능이 아니다. 도구일 뿐이다. 테스트 코드도 리팩토링한다 프로덕션 코드는 리팩토링한다. 테스트 코드는? 안 한다. 대부분. "돌아가는데 뭐" 그러다가 테스트가 레거시가 된다. 수정하기 무섭다. 건드리면 깨진다. 우리 팀은 스프린트마다 "테스트 부채" 시간이 있다. 중복 제거한다. 불필요한 테스트 삭제한다. 느린 테스트 최적화한다. 지난달에 500개 테스트를 300개로 줄였다. 실행 시간은 반으로. 커버리지는 그대로. 중복이 많았다. 같은 걸 다르게 테스트했다. 통합했다. 코드 리뷰 템플릿 PR마다 이거 붙인다. ## 테스트 체크리스트- [ ] 각 테스트가 하나의 관심사만 검증하는가? - [ ] 테스트명이 의도를 명확히 드러내는가? - [ ] 테스트 간 의존성이 없는가? - [ ] 외부 의존성을 mock/stub 했는가? - [ ] 에러 케이스를 다루는가? - [ ] Flaky 가능성은 없는가? - [ ] 로컬에서 빠르게 실행되는가? (< 10초) - [ ] Magic number 대신 의미있는 상수를 사용하는가? - [ ] Setup/teardown이 적절한가? - [ ] 실제 버그를 잡을 수 있는 테스트인가?마지막 항목이 제일 중요하다. "실제 버그를 잡을 수 있는가" 이거 아니면 의미 없다. 결국 사람이 본다 자동화가 아무리 좋아도. 결국 사람이 판단한다. 테스트가 실패하면 누가 보나. 테스트가 이상하면 누가 고치나. 테스트가 의미 있는지 누가 판단하나. 전부 사람이다. 자동화는 반복 작업만 줄인다. 생각은 못 한다. 좋은 테스트 코드는 좋은 코드다. 읽기 쉽고. 유지보수 쉽고. 명확하고. 신뢰할 수 있고. 프로덕션 코드만큼 신경 써야 한다. 아니, 더 써야 한다. 테스트 코드가 틀리면 프로덕션 버그를 못 잡는다. 그게 제일 무섭다.오늘도 Flaky 테스트 하나 고쳤다. 원인은 타임아웃이었다. 3초를 5초로 늘렸다. 근본적 해결은 아니다. 언젠가 다시 깨질 것이다. 그때 또 고치겠지.
- 05 Dec, 2025
자동화 커버리지 80% vs 100%: 그 사이 어디가 현실일까
자동화 커버리지 80% vs 100%: 그 사이 어디가 현실일까 경영진의 질문 "자동화 커버리지 몇 퍼센트예요?" CTO가 물었다. 월요일 아침 10시, 스탠드업 미팅. "현재 73%입니다." "100% 언제쯤 가능할까요?" 순간 멈칫했다. 이 질문, 매번 나온다. "현실적으로 어렵습니다." "왜요? 다른 회사는 90% 넘는다는데." 설명해야 한다. 또.자동화 커버리지. 숫자 하나로 모든 걸 판단한다. 80%면 좋은 거다. 90%면 훌륭하다. 100%면 완벽하다. 정말 그럴까. 4년 동안 자동화 했다. 처음엔 나도 100%를 꿈꿨다. 지금은 안다. 불가능하다는 걸. 80%의 벽 우리 서비스, 테스트 케이스 2,400개. 자동화된 케이스 1,752개. 정확히 73%. 남은 648개. 왜 자동화 안 했을까. 이유는 명확하다. 첫째, UI 테스트의 함정. 로그인 페이지 자동화. 쉽다. driver.find_element(By.ID, "username").send_keys("test") driver.find_element(By.ID, "password").send_keys("1234") driver.find_element(By.ID, "login-btn").click()3줄이면 끝. 그런데 메인 페이지는 다르다. 애니메이션 들어간 대시보드. 차트가 동적으로 그려진다. 무한 스크롤. 레이지 로딩. 실시간 업데이트. 어떻게 자동화하나. # 차트 로딩 대기 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS, "chart-loaded")) )# 아니다, 10초론 부족하다 # 15초? 20초? # 네트워크 느리면 30초도 모자라다대기 시간을 늘리면 테스트가 느려진다. 줄이면 Flaky Test가 된다. 실패했다가 다시 돌리면 성공한다. 이게 버그인가 타이밍 문제인가.둘째, 써드파티 API. 결제 시스템. PG사 API 쓴다. 테스트 서버 있다. 좋다. 그런데 불안정하다. def test_payment(): response = requests.post( "https://testpay.pg.com/api/v1/payment", json={"amount": 10000} ) assert response.status_code == 200오전엔 성공한다. 오후엔 타임아웃. 저녁엔 502 Bad Gateway. 우리 코드 문제가 아니다. PG사 테스트 서버가 그렇다. 이걸 자동화에 넣으면 CI가 계속 깨진다. 개발자들이 묻는다. "이거 우리 버그예요?" "아니요, PG사 서버 문제예요." "그럼 테스트를 왜 돌려요?" 할 말이 없다. 결국 Mocking한다. @mock.patch('requests.post') def test_payment(mock_post): mock_post.return_value.status_code = 200 # 테스트 진행이게 진짜 테스트일까. 실제 PG사 API는 안 건드린다. 우리가 만든 가짜 응답만 테스트한다. 커버리지는 올라간다. 100개 케이스 추가. 실제 품질은? 모른다. 73%의 의미 우리 자동화 커버리지 73%. 이게 낮은 걸까. 계산해봤다. E2E 테스트: 120개 (자동화 48개, 40%) 풀 사용자 플로우. 회원가입부터 결제까지. 시나리오가 길다. 15분짜리도 있다. 중간에 하나만 깨져도 전체가 실패. 네트워크, 써드파티, 브라우저 버전, OS 차이. 변수가 너무 많다. 40% 자동화도 선방한 거다. 통합 테스트: 680개 (자동화 612개, 90%) API 테스트. 이건 쉽다. def test_user_api(): response = client.get("/api/users/1") assert response.status_code == 200 assert response.json()["name"] == "테스트"서버만 있으면 된다. UI 없다. 브라우저 없다. 빠르다. 안정적이다. 90% 자동화. 만족스럽다. 단위 테스트: 1,600개 (자동화 1,092개, 68%) 함수 단위 테스트. 개발자가 짜야 하는데 안 짠다. "QA가 테스트하잖아요." 이런 케이스는 우리가 자동화해도 의미 없다. 코드 레벨 테스트는 개발자 몫이다.전체 73%. E2E 40%, 통합 90%, 단위 68%. 피라미드 구조다. 정상이다. 그런데 경영진은 하나의 숫자만 본다. "73%요. 낮네요." 100%의 환상 전 직장 동료 H. 대기업 QA였다. 자동화 커버리지 95% 달성했다고 자랑했다. "어떻게 했어?" "UI 테스트 다 넣었지." "Flaky Test는?" "많아. 그냥 3번 돌려서 1번 성공하면 pass 처리." 이게 95%의 실체다. 불안정한 테스트를 숫자에 포함시킨다. 의미 없는 테스트도 넣는다. def test_homepage_loads(): driver.get("https://example.com") assert "Example" in driver.title이런 테스트. 10초 걸린다. 확인하는 건 타이틀뿐. 이게 1개 케이스로 카운트된다. 커버리지는 올라간다. 실효성은 낮다. 또 다른 회사 K. 자동화 커버리지 100% 달성. 비결이 뭘까. 테스트 케이스를 줄였다. 2,000개 → 800개. 자동화 어려운 케이스는 아예 삭제. "이건 매뉴얼로 하면 돼요." 남은 800개만 자동화. 800/800, 100%. 숫자는 예쁘다. 실제 커버리지는? 40%. 현실적인 목표 그래서 내 결론. 80%가 현실이다. 100%는 환상이다. 자동화 가능한 것: 80% 불가능한 것: 20% 20%는 이렇다.복잡한 UI 인터랙션드래그 앤 드롭 멀티터치 제스처 캔버스 기반 에디터 실시간 화상 채팅불안정한 외부 의존성써드파티 API 테스트 서버 결제 시스템 샌드박스 SNS 로그인 연동 이메일/SMS 발송 확인시각적 검증디자인 QA 애니메이션 자연스러움 반응형 레이아웃 미세 조정 다국어 텍스트 overflow탐색적 테스트예상 못 한 버그 찾기 사용자 관점 UX 검증 경계값 실험 직관적 이상함 감지이 20%는 사람이 한다. 비효율적이지 않다. 필수다. 80%를 지키는 법 그럼 80%는 어떻게 달성할까. 내 원칙 5가지. 1. 테스트 피라미드 따르기 E2E: 최소한만 (10%) 통합: 핵심 기능 (40%) 단위: 최대한 많이 (50%) E2E 많이 만들면 커버리지 빨리 오른다. 하지만 느리고 불안정하다. 단위 테스트가 기반이어야 한다. 2. Flaky Test 절대 용납 안 함 테스트가 불안정하면 즉시 수정하거나 삭제. "가끔 실패해요"는 없다. 실패하는 테스트는 신뢰를 깎는다. CI가 빨간색이면 모두가 무시한다. 3. Mocking의 한계 인정 Mocking은 도구다. 만능이 아니다. 외부 API는 Mock한다. 하지만 통합 테스트는 따로 둔다. 주 1회, 실제 테스트 서버 대상으로 돌린다. 느려도 괜찮다. 정확도가 중요하다. 4. ROI 계산하기 이 테스트 자동화하는 시간: 3시간 매뉴얼로 하는 시간: 5분 실행 빈도: 월 1회 3시간 투자해서 월 5분 절약. 의미 없다. 자주 실행하는 것만 자동화한다. CI에 들어갈 테스트, 회귀 테스트, 스모크 테스트. 5. 유지보수 고려 자동화 코드도 코드다. 관리해야 한다. UI 바뀌면 스크립트도 바뀐다. Page Object Pattern 쓴다. class LoginPage: def __init__(self, driver): self.driver = driver def enter_username(self, username): self.driver.find_element(By.ID, "username").send_keys(username) def enter_password(self, password): self.driver.find_element(By.ID, "password").send_keys(password)UI 변경되면 이 클래스만 수정. 테스트 코드 100개 건드릴 필요 없다. 경영진 설득하기 다시 CTO에게 설명했다. "100% 불가능한 이유 3가지 말씀드리겠습니다." 회의실 화이트보드. 피라미드 그렸다. "첫째, 테스트 피라미드 구조상 E2E는 소수여야 합니다. 우리는 40% 자동화했고, 이는 업계 평균 이상입니다." "둘째, 써드파티 의존성. 결제, 인증, 알림 시스템. 이들의 테스트 환경은 불안정합니다. Mock으로 대체 가능하지만, 실제 커버리지는 낮아집니다." "셋째, 시각적 요소와 탐색적 테스트. 이는 자동화로 대체 불가능합니다. 사람의 눈과 직관이 필요합니다." CTO가 고개를 끄덕였다. "그럼 현재 73%는?" "안정적으로 유지 가능한 최대치입니다. 80%까지는 올릴 수 있지만, 3개월 소요됩니다. 우선순위 조정 필요합니다." "신규 기능 개발이 우선인가요, 자동화 커버리지 증가가 우선인가요?" 침묵. "신규 기능이 우선입니다." "그렇다면 73% 유지하면서, 신규 기능에 대한 자동화는 즉시 추가하겠습니다. 분기마다 5%씩 증가 목표로 하겠습니다." 현실적인 제안. 승인됐다. 숫자의 함정 자동화 커버리지는 숫자다. 숫자는 거짓말을 한다. 95%인데 Flaky Test 투성이인 팀. 70%인데 모든 테스트가 안정적인 팀. 어느 쪽이 나을까. 당연히 후자다. 커버리지 높이기는 쉽다. 의미 없는 테스트 추가하면 된다. 케이스 줄이면 된다. Mock 남발하면 된다. 하지만 품질은 안 올라간다. 진짜 중요한 건 이거다. "이 자동화가 버그를 잡아낼 수 있나?" 73%의 테스트가 버그 100개 찾는다. 95%의 테스트가 버그 50개 찾는다. 나는 73%를 선택한다. 3개월 후 현재 커버리지: 77%. 4% 올렸다. 추가한 테스트: 96개. 모두 통합 테스트. API 위주. 안정적이다. CI에서 한 번도 안 깨졌다. E2E는 2개만 추가. 핵심 시나리오만. 회원가입-구매-환불. UI 변경 적은 부분만 골랐다. CTO가 물었다. "80% 언제 되나요?" "다음 분기요." "100%는?" "현실적으로 어렵습니다. 대신 77% 테스트가 모두 의미 있습니다. 한 번도 False Positive 없었습니다." 고개를 끄덕인다. "좋습니다. 품질이 더 중요하죠." 드디어 이해했다. 자동화의 본질 자동화는 수단이다. 목적이 아니다. 목적은 품질이다. 100% 자동화해도 버그 못 잡으면 무용지물. 50% 자동화해도 치명적 버그 잡으면 성공. 숫자에 집착하지 않는다. 대신 이것만 지킨다.모든 자동화 테스트는 안정적이어야 한다. 실패는 항상 실제 버그여야 한다. 빠르게 실행되어야 한다. 유지보수가 쉬워야 한다. 비용 대비 효과가 있어야 한다.이 5가지를 지키는 게 80%다. 100%는 욕심이다.오늘도 CI 빌드 통과. 77% 자동화, 0% Flaky. 이게 진짜 성공이다.