카오스 엔지니어링 도입 배경
24시간 내내 운영되는 이커머스 서비스에서는 언제든지 장애가 발생할 수 있습니다. 그 원인도 매우 다양합니다.
- 인프라 문제
- 코드 문제
- 설정 문제
- 데이터 문제
- 휴먼 에러
이처럼 다양한 원인으로 인해, QA 팀이 배포 코드를 아무리 철저히 테스트하더라도 그것만으로는 서비스의 완전한 안정성을 보장할 수는 없습니다.
장애가 발생하면 이를 해결하기 위해 많은 인력이 투입되고, 서비스 중단이나 고객 불편으로 이어질 수 있습니다. 복구가 지연되거나 데이터가 꼬이는 등 예기치 못한 상황이 발생할 가능성도 있습니다. 예측 가능한 장애라 하더라도, 사전 테스트를 하지 않으면 실제 문제로 이어질 수 있습니다.
이에 따라, 올리브영은 최소한 예측 가능한 장애에 대해서는 서비스 안정성을 확보하기 위해 카오스 엔지니어링을 도입하기로 결정했습니다.
카오스 엔지니어링이 무엇이길래 필요한가?
위에서 계속 카오스 엔지니어링에 관해서 이야기 했는데 그렇다면 카오스엔지어링이 무엇이길래 서비스의 안정도를 높이는 데 도움이 된다고 이야기하는 걸까요?
앞서 카오스 엔지니어링을 언급했는데, 이것이 왜 서비스 안정성을 높이는 데 도움되는 걸까요? 간단히 말하면 일부러 장애를 일으켜 시스템이 잘 버티는지 실험하는 것입니다. 예를 들어, DB를 일시적으로 꺼보거나 네트워크 지연을 인위적으로 발생시켜 서비스가 이를 어떻게 처리하는지 관찰하는 방식입니다. 가령, 하나의 DB가 다운되었을 때 그 DB에 의존하는 서비스는 어떻게 될까요?
- 서비스가 완전히 중단될까요?
- 성능 저하가 발생할까요?
- 아니면 정상적으로 동작할까요?
이러한 상황은 직접 장애를 발생시켜보기 전까지는 추측에 불과합니다.
만약 실제 장애 상황에서 서비스가 중단된다면, 이는 사용자 경험에 치명적인 영향을 미칠 수 있습니다.
하지만 사전에 카오스 엔지니어링을 통해 테스트를 진행했다면 이중화를 구성한다든지, 장애 대응책을 마련해둘 수 있었을 것입니다.
카오스 엔지니어링은 단순히 장애를 일으키는 것이 아닙니다. 시스템의 복원력(Resilience) 을 높이고 취약점을 사전에 발견해 개선하는 것을 목표로 하는 방법론입니다.
실험 종류
그렇다면 카오스 엔지니어링 실험은 어떤 종류가 있을까요? 대표적인 유형은 다음과 같습니다.
- Application Level: 애플리케이션 내부에서 발생할 수 있는 예외나 오류 상황을 유도합니다. 애플리케이션의 비정상적인 동작을 유도하여 시스템의 안정도와 복원력을 테스트합니다.
예: API 호출 실패, null 반환 등 - Host Level: 시스템이 실행되는 호스트 서버 자체에 영향을 주는 실험입니다. 서버의 리소스나 상태를 조작해 복원력을 테스트합니다.
예: DB 다운, 프로세스 종료 등 - Resource Attack: 서버 리소스를 고갈시키거나 과부하 상태를 만드는 실험입니다. 서비스 병목 현상이나 자원 부족 문제 재현에 유용한 방법이죠.
예: CPU 100% 사용, 메모리 누수 등 - Network Attack: 네트워크 연결 문제나 성능 저하를 유도합니다. 네트워크 장애를 시뮬레이션하여 복원력을 테스트합니다.
예: 네트워크 단절, 패킷 손실 등 - AZ (Availability Zone) Attack: 특정 AZ 또는 리전의 장애 상황을 시뮬레이션합니다. 이 역시 시스템의 복원력을 테스트합니다.
예. AZ 단절, 리전 단절 등 - Region Attack: 대규모 리전 장애 상황을 시뮬레이션합니다. 시스템 복원력을 테스트하는 또다른 유형입니다.
예. 리전 단절, 대규모 장애 등 - People Attack: 사람의 실수나 잘못된 행동을 시뮬레이션합니다.
예: 잘못된 설정, 오배포 등
이 외에도 다양한 유형이 존재하지만, 올리브영에서는 Application Level, Host Level, Resource Attack을 우선적으로 사용하기로 했습니다.
이번 글에서는 이 중에서도 Application Level 테스트에 대해 다루겠습니다.
Application Level 테스트
올리브영에서 Application Level 테스트를 1순위로 진행한 이유는 이전에 발생한 장애 유형 중에 null exception이 있었기 때문입니다.
API 응답이 "key":"value" 형태로 전달될 때 일부 필드의 value가 null인 상황은 흔히 발생할 수 있습니다.
머리로는 "특정 key의 value만 null이라면, 그 부분만 안 나오고 나머지는 정상적으로 보이겠지?"라고 생각할 수 있지만, 실제로는 null exception으로 인해 전체 페이지가 렌더링되지 않는 문제가 발생할 수 있습니다.
이런 경우를 방지하기 위해서 카오스 엔지니어링을 도입하여 API의 key에 null 값을 넣어주는 실험을 진행하게 되었습니다.
테스트 목표
테스트를 시작하기에 앞서 이 테스트의 목표를 명확하게 정하는 것이 중요합니다.
그래야 문제를 발생시켰을 때 생각하는 기대결과 수준에 따라서 수정 여부가 결정되기 때문입니다.
예를들어 null값이 넘어왔을때
- 해당 API를 사용하는 페이지가 정상적으로 보여야 한다. default 데이터로 null 값이 넘어온 필드가 보여져야 한다.
- 해당 API를 사용하는 페이지가 정상적으로 보여야 한다. 단, null 값이 넘어온 필드에 대해서는 보여지지 않아야 한다.
- 해당 페이지는 표시할 수 없으므로 다른 페이지로 이동시킨다.
등의 목표를 세울 수 있을 것입니다. 따라서 목표에 따라서 수정 방향도 달라질 것이 보일것입니다.
올리브영은 **해당 API를 사용하는 페이지가 정상적으로 보여야 한다. 단, null 값이 넘어온 필드에 대해서는 보여지지 않아야 한다.**는 목표로 설정하였습니다.
테스트 방법
Application Level 테스트를 진행하기 앞서 목표를 정했습니다.
"API의 value에 null이 넘어오더라도 해당 정보를 사용하는 영역만 노출되지 않고 서비스는 정상적으로 이용 가능해야 한다."
이런 목표를 달성할 실험이라면 어떻게 진행할 수 있을까요? 가장 먼저 생각난 방법은 이렇습니다.
- google overwrite contents (https://developer.chrome.com/docs/devtools/overrides?hl=ko)
- charles, fiddler와 같은 프록시 툴을 사용하여 API의 필드에 null 값을 넣어주는 방법
- proxy를 실제로 만드는 방법 (mitmproxy - https://mitmproxy.org/)
- 실제 데이터를 입력하는 방법
간단한 테스트라면 1, 2번 방법이 적합합니다. 하지만 필드 값이 많으면 하나하나 수동으로 수정하며 테스트하는 번거로움이 있습니다.
일회성 테스트에는 이 방법이 유용할 수 있으나, 올리브영에서는 이번 테스트뿐만 아니라 향후 Application Level 테스트를 프로세스화해 진행할 계획이었습니다. 이러한 이유로 매번 수동 작업이 필요한 1, 2번 방식은 리소스가 과도하게 소모될 것으로 판단했습니다.
4번 방식의 경우 API가 어떨 때 null 값을 넘겨주는지까지는 정의되지 않았기 때문에 데이터 수정으로 null을 넘겨주기는 어렵다고 판단했습니다.
그래서 3번, mitmproxy를 이용해 proxy를 만드는 방법으로 테스트 진행을 결정했습니다.
mitmproxy 구성
일단 mitmproxy를 이용한 간단한 플로우를 보면 아래와 같습니다.

mitmproxy는 HTTP(S) 트래픽을 가로채고 수정할 수 있는 강력한 도구입니다. 이를 통해 API 요청과 응답을 조작하여 null 값을 주입하는 테스트를 수행할 수 있습니다.
mitmproxy에서 제공하는 기본 함수는 다음과 같습니다.
🔹 request(flow: HTTPFlow)
/// 클라이언트 → 서버로 가는 요청을 가로챌 때 호출
def request(flow: http.HTTPFlow):
if flow.request.pretty_url.endswith("/api/data"):
flow.request.headers["X-Test-Header"] = "Test"
🔹 response(flow: HTTPFlow)
/// 서버 → 클라이언트로 가는 응답을 가로챌 때 호출
def response(flow: http.HTTPFlow):
if "example.com" in flow.request.pretty_url:
flow.response.headers["X-Debug"] = "modified"
flow.response.text = flow.response.text.replace("Hello", "Hi")
🔹 error(flow: HTTPFlow)
/// 네트워크 오류, 처리 실패 등 에러가 발생했을 때 호출
def error(flow: http.HTTPFlow):
print(f"Error occurred: {flow.error}")
🔹 clientconnect / clientdisconnect
/// 클라이언트가 연결/종료할 때 호출됨
def clientconnect(connection):
print(f"Client connected: {connection}")
def clientdisconnect(connection):
print(f"Client disconnected: {connection}")
이 외에도 다양한 유형의 함수를 기본적으로 제공합니다.
null exception 테스트를 위해서는 응답(Response)을 변환해야 했기 때문에, response(flow: HTTPFlow) 함수를 중심으로 테스트를 진행했습니다. 우선 테스트할 API를 선별해 목록 파일로 만들었습니다. 그리고 mitmproxy를 실행할 때 이 파일을 불러와 테스트했습니다. 각 API의 응답 값에서 필드를 하나씩 골라가며 null 값을 자동으로 주입해 테스트가 진행되도록 구성했습니다.
response code중 일부를 발췌하여 흐름을 설명해보겠습니다.
def response(flow: http.HTTPFlow):
if api_list[i] in flow.request.url and flow.response.content:
print(f"🌐 요청 URL: {flow.request.url}")
# 응답 데이터 파싱 (bytes → str 변환 후 JSON 로드)
data = json.loads(flow.response.content.decode("utf-8"))
# "data" 필드가 딕셔너리인지 확인
if isinstance(data, dict) and isinstance(data.get("data"), dict):
data_fields = list(data["data"].keys()) # "data" 하위 필드 리스트 가져오기
# API에 있는 data를 모두 확인한다
if field_index <= len(data_fields)-1:
key_to_null = data_fields[field_index] # 현재 필드를 `null`로 설정
data["data"][key_to_null] = None
///api/data의 응답값
{
"name": "Alice",
"age": 30,
}
///api/data2의 응답값
{
"city": "seoul",
"company": "oliveyoung",
}
이렇게 두 API가 호출된다고 가정하면, 이들을 테스트대상 API 목록 파일에 선언합니다.
/api/data
/api/data2
이후 proxy를 실행하여 모바일 기기와 proxy를 연결하면, 대상 API가 호출될 때 proxy가 가로채서 응답 값을 변환하게 됩니다.
- /api/data 처음 호출 시
{
"name": null,
"age": 30,
}
두 번째 호출 시
{
"name": "Alice",
"age": null,
}
을 반환하게 됩니다. 이렇게 /api/data의 마지막 value까지 null로 변환 되면 /api/data2도 동일하게, value 값을 null로 순차 변환합니다.
이렇게 /api/data의 마지막 value까지 null로 변환하면 /api/data2도 동일하게, value 값을 null로 순차 변환합니다. Proxy를 만든 후 테스트를 진행하니 반자동 테스트가 되었고, 시간을 아끼면서 다른 영역들로 확장하기에 유용했습니다.
실제 올리브영도 테스트 도중에, 에러 바운더리 작업을 1차 진행했음에도 문제가 발견되었는데요.

검출된 부분들은 QA팀에서 리포팅해 조치하도록 처리하였습니다. 앞으로 전시 영역은 에러 바운더리 작업을 진행하여 null exception이 발생하지 않도록 하기 위해 정기 검증 및 프로젝트 프로세스화를 진행할 예정입니다.
* 여기서 잠깐! 에러 바운더리란 무엇일까요?
React 앱에서 컴포넌트 중 하나라도 오류가 발생하면 전체 앱이 중단될 수 있습니다. 이 때 React 컴포넌트인 에러 바운더리를 사용하면 문제가 있는 부분만 격리하여 사용자에게는 대체 UI를 보여주고, 앱의 나머지 부분은 계속 작동할 수 있도록 합니다.

마무리
QA에서 하는 업무는 주로 정상적인 동작을 가정하지만, 카오스 엔지니어링은 반대로 비정상적인 동작을 가정하기 때문에 어떻게 보면 기존 QA 업무와 반대의 경우를 발생시킨다고 볼 수 있습니다.
- "예측할 수 없는 장애에도 흔들리지 않는 시스템을 만들기 위해"
- 정상 조건뿐만 아니라 비정상 상황에서도 품질을 보장하기 위해
- 개발자, SRE와 함께 서비스 안정성, 회복력을 검증하고 강화하기 위해
- 실제 서비스 중 장애가 발생해도 사용자에게 최소한의 영향을 주도록 사전 대비하기 위해
- 그리고 무엇보다, 문제가 터졌을 때 놀라지 않기 위해
카오스 엔지니어링을 도입해 보는 건 어떨까요?
이번에는 카오스 엔지니어링의 개념과 Application Level 테스트를 다뤘습니다. 다음에는 Host Level 테스트를 다루는 포스트로 다시 찾아오겠습니다!
이번 포스트로 올리브영 QA Engineer에 관심이 생기셨나요? 우리와 함께 일할 동료를 열린 마음으로 기다리고 있습니다.