LabHub
시작하기
배우기 러닝패스 코스

EAI 중간 계층 만들기

타임아웃은 실패가 아니라 모름이다

LabHub 에서 이어서 보기

한 줄 요약

동기 중계는 전문을 받아 대상 시스템을 부르고 그 결과를 표준 응답코드로 바꿔 돌려주는 일이다. 가장 중요한 구분은 "실패했다" 와 "모른다" 다. 연결조차 못 했으면 보내지 않은 것이 확실하지만(E902), 보낸 뒤 답을 못 받았으면 대상이 처리했는지 아무도 모른다(E901). 이 둘을 같은 코드로 뭉개는 순간 이중 이체가 시작된다.

왜 이게 필요했나

채널(인터넷뱅킹)이 계정계를 직접 부르면, 채널은 계정계의 주소·HTTP 규약·오류 형식을 전부 알아야 한다. 계정계가 오류를 422 {"result":"INSUFFICIENT_FUNDS"} 로 주든 200 {"code":"E-17"} 로 주든 채널마다 해석 코드가 따로 생기고, 대상 시스템이 바뀔 때 채널 열 개를 고친다. 중계 계층이 이 해석을 한 곳에서 맡고 채널에는 표준 응답코드 하나만 돌려준다 — 잔액 부족은 어느 대상에서 왔든 B201 이다.

그런데 중계가 끼면 실패 지점이 하나 는다. 채널 → 허브 → 계정계 구간 어디서든 멈출 수 있고, 멈춘 자리에 따라 뜻이 완전히 다르다. 이 모듈의 계정계 픽스처는 현실과 똑같이 만들었다. 느려도 처리는 끝까지 한다. 허브가 2초 만에 포기해도 계정계는 4초째에 잔액을 뺀다. 이때 허브가 채널에 "실패" 라고 답하면 채널은 다시 보내고, 돈은 두 번 빠진다.

어떻게 동작하나

요청 한 건의 길. ① TCP 로 길이 4바이트를 정확히 읽고 그만큼 다시 읽는다(1모듈) ② 헤더를 파싱해 형식이 틀리면 E102 ③ 거래코드가 중계 대상이 아니면 E101(라우팅, 2모듈) ④ 본문을 레이아웃대로 JSON 으로 바꾼다(3모듈의 규칙, 이제 공통 라이브러리 lhconv) ⑤ 계정계 POST /v1/transfers 를 부른다 — GUID 를 본문과 X-GUID 헤더에 싣는다 ⑥ 결과를 표준 코드로 바꿔 응답 전문을 만든다(정상일 때만 응답 본문 45바이트).

결과를 네 갈래로 나눈다. HTTP 의미론(RFC 9110)에서 2xx 는 요청이 성공적으로 처리됐다는 뜻이고, 4xx 는 요청 쪽의 문제, 5xx 는 서버가 처리하지 못했다는 뜻이다. 계정계는 업무 거절을 422(RFC 9110 15.5.21, 요청 형식은 이해했지만 처리할 수 없는 내용)로 주고 본문의 result 로 이유를 가른다. 그래서 변환표는 상태코드와 업무코드를 함께 본다.

상황 알 수 있는 것 표준 코드
200 처리됨 0000
422 + INSUFFICIENT_FUNDS 등 거절됨(돈은 안 움직임) B201~B203
500 대상이 처리하지 못했다고 말했다 E500
응답을 기다리다 시간 초과 보냈다. 처리 여부는 모른다 E901
연결 거부·연결 시간 초과 보내지 못했다(미전송 확정) E902

타임아웃은 두 종류다. 연결을 맺는 동안의 시간 초과와, 요청을 보낸 뒤 응답을 기다리는 동안의 시간 초과는 뜻이 정반대다. 앞의 것은 요청 바이트가 한 바이트도 나가지 않았으니 다시 보내도 안전하다. 뒤의 것은 요청이 이미 대상에 도착했다. 파이썬 3.12 의 urllib.request.urlopen 은 이 둘을 다른 예외로 낸다(이 과정에서 실측) — 연결 단계의 문제는 URLError 로 감싸고(reasonConnectionRefusedErrorTimeoutError), 응답을 기다리다 난 시간 초과는 감싸지 않은 TimeoutError 로 올라온다. 예외 하나를 통째로 잡아 같은 코드로 바꾸면 이 구분이 사라진다.

타임아웃 예산은 안쪽으로 갈수록 짧아야 한다. 채널이 10초를 기다리는데 허브가 계정계를 15초 기다리면, 채널은 이미 포기하고 재전송했는데 허브는 아직 첫 요청을 붙들고 있다. 허브의 결과는 아무도 받지 않는다. 그래서 채널 > 허브 > 대상 순으로 예산을 줄이고, 허브는 자기 예산 안에서 반드시 무언가를 답한다 — 모르면 모른다(E901)고.

연결마다 따로 처리한다. 요청 하나를 처리하는 동안 다음 연결을 받지 않는 서버는, 느린 이체 하나 때문에 뒤의 모든 이체를 줄 세운다. 파이썬 표준 라이브러리의 socketserver.ThreadingTCPServer 는 연결마다 스레드를 하나씩 띄운다. 스레드는 한도 없이 늘어날 수 있으므로 11모듈에서 대상별 동시 처리 한도(벌크헤드)를 붙인다.

현장에서 만나는 모습

가장 비싼 사고는 E901 을 실패로 처리한 채널이다. "응답 시간 초과 — 다시 시도하세요" 라는 화면 문구 하나로 고객이 버튼을 한 번 더 누르고, 계정계에는 이체가 두 건 남는다. 그래서 결과 미확정 응답에는 재전송 금지결과 조회가 짝으로 붙는다(8모듈). 두 번째는 대상 시스템의 오류 메시지를 그대로 채널에 흘려보내는 허브다. 계정계 내부 오류 문구(스택 트레이스·테이블 이름)가 고객 화면에 뜨고, 채널은 대상마다 다른 문구를 해석하는 코드를 쌓는다. 세 번째는 로컬에서 늘 되던 중계가 운영에서 전문을 반씩 잃는 것 — 길이만큼 다 읽지 않고 recv 한 번으로 끝낸 코드다.

다음 실습에서 할 것

계정계 픽스처를 띄워 직접 불러 보고, 정의서를 보고 응답코드 변환표를 쓴다. 그다음 중계 서버 relay.py 를 단계마다 키운다 — 조각난 전문을 끝까지 읽는 뼈대, 정상 이체 중계, 업무 오류 변환, 읽기 타임아웃(E901), 연결 실패(E902), 동시 처리. 채점기는 당신의 relay.py 를 직접 띄우고 계정계 픽스처의 호출 통계로 몇 번 불렀는지까지 본다.