서비스마다 알림톡 보내는 코드를 따로 들고 있었다
서비스가 각자 발송 대행사를 직접 부르고 각자 발송 로직을 들고 있던 구조를 알림톡 서버 하나로 모으면서, 폴백과 캐시와 개발 환경 오발송 방지를 어디에 둘지 정한 과정을 적었다.
판매자에게 나가는 알림톡이 여러 종류다. 신청이 접수됐다, 지급이 됐다, 회수가 다가온다 같은 것들인데, 이 메시지를 만들어 보내는 코드가 서비스마다 따로 있었다.
각 서비스가 발송 대행사의 API를 직접 불렀고, 템플릿을 고르는 규칙도 실패했을 때 어떻게 할지도 각자 들고 있었다. 같은 일을 하는 코드가 여러 벌 있었다는 뜻이다.
같은 코드가 여러 벌이면 생기는 일
처음에는 중복 그 자체가 문제라고 생각했는데, 실제로 아픈 것은 그다음이었다. 대행사 쪽 규격이 바뀌거나 템플릿을 새로 등록하면 손댈 곳이 여러 군데가 되고, 그중 하나를 빠뜨려도 티가 나지 않는다.
발송이 실패했을 때의 동작도 서비스마다 달랐다. 어떤 곳은 예외를 삼키고 넘어갔고 어떤 곳은 로그만 남겼다. 그래서 "알림톡이 안 왔다"는 문의가 들어오면 어느 서비스에서 나간 것인지부터 찾아야 했고, 찾아도 실패했다는 기록이 남아 있지 않은 경우가 있었다.
자격증명도 마찬가지였다. 대행사를 직접 부르니 서비스마다 인증 정보를 들고 있어야 했고, 그 값을 바꾸려면 여러 서비스를 함께 배포해야 했다.
flowchart LR A["회원 서비스"] --> X["발송 대행사"] B["API 서버"] --> X C["배치"] --> X A -. "각자 템플릿 규칙" .- A B -. "각자 실패 처리" .- B C -. "각자 자격증명" .- C
이미 있는 서버에 붙이기로 했다
알림톡을 보내는 전용 서버가 이미 있었다. 다른 서비스들이 쓰고 있었고 대행사 연동과 발송 이력 저장이 거기 들어 있었으니, 새로 만드는 대신 그 서버가 우리 쪽도 맡게 하는 편이 빨랐다.
문제는 데이터베이스였다. 그 서버는 자기 서비스의 데이터베이스를 보고 있었는데 우리 쪽 발송 이력은 다른 데이터베이스에 남아야 했다. 그래서 데이터소스를 하나 더 붙이고, 어느 요청이 어느 데이터베이스로 갈지를 경로로 갈랐다.
새 서버를 만들지 않은 것이 결과적으로 맞았다. 발송 이력을 어떻게 남길지, 대행사 응답을 어떻게 해석할지 같은 것들이 이미 정해져 있어서, 우리가 정해야 할 것은 우리 도메인에 관한 것만 남았다.
서비스 쪽은 클라이언트 하나로 줄였다
서버가 생겼으니 서비스 쪽에서는 대행사를 부르던 코드를 전부 걷어내고 그 서버를 부르는 클라이언트 하나만 남겼다. 템플릿을 고르는 것도 실패를 판정하는 것도 서버가 하게 됐다.
이때 하나를 바꿨다. 인증 코드를 보내는 경로에서는 발송이 실패하면 예외를 던지게 했다. 다른 알림은 못 받아도 다음에 다시 보내면 되지만, 인증 코드는 그 자리에서 사용자가 기다리고 있으니 조용히 실패하면 안 됐다.
같은 발송이라도 실패했을 때의 무게가 다르다는 것을 이때 처음 코드로 구분했다.
흩어진 경로를 한 길로 모았다
서비스마다 하나씩 옮기는 데 두 달쯤 걸렸다. 한 번에 갈아엎지 않고 기존 경로를 남겨 둔 채 새 경로를 열고, 확인이 끝난 종류부터 옮겼다.
flowchart LR
A["회원 서비스"] --> N["알림톡 서버"]
B["API 서버"] --> N
C["배치"] --> N
N --> X["발송 대행사"]
N --> H[("발송 이력")]
옮기고 나니 발송 이력이 한곳에 모였다. "이 판매자에게 오늘 뭐가 나갔나"를 한 번의 조회로 답할 수 있게 된 것이 눈에 띄는 변화였다.
알림톡이 실패하면 문자로 넘겼다
알림톡은 상대가 카카오톡을 안 쓰거나 채널을 차단했으면 실패한다. 그런 경우에도 내용은 전달돼야 해서, 실패하면 문자로 넘어가는 폴백을 붙였다.
대행사가 알림톡과 문자를 하나의 흐름으로 묶어 주는 방식을 제공해서 그것을 썼다. 우리가 실패를 감지해 다시 보내는 것보다 안전하다고 봤는데, 우리가 재발송을 맡으면 실패 판정을 잘못했을 때 같은 내용이 두 번 나갈 수 있기 때문이다.
발송 요청
알림톡 템플릿 코드 · 치환 값
문자 발신번호 · 본문 <- 알림톡이 실패하면 여기로
두 채널을 한 요청에 함께 담아 보낸다
폴백을 붙이고 나서 두 가지를 더 막았다. 문자 발신번호가 설정돼 있지 않으면 요청을 만들기 전에 실패하게 했고, 문자 쪽 필드를 반만 채워 보내는 것도 막았다. 폴백이 설정되다 만 상태로 조용히 도는 것이 폴백이 아예 없는 것보다 나쁘다고 봤다.
템플릿을 사람이 대조하고 있었다
알림톡은 미리 등록해 둔 템플릿에 변수를 끼워 넣는 방식으로 나간다. 그래서 코드가 만들어 보내는 변수와 등록된 템플릿의 자리가 정확히 맞아야 하는데, 이걸 맞추는 일을 사람이 하고 있었다.
템플릿이 새로 등록되거나 문구가 바뀌면 개발자가 등록된 내용을 열어 보고 변수 이름과 개수를 코드와 맞춰 봤다. 눈으로 대조하는 일이라 틀리면 발송 요청이 거절된다. 더 나쁜 것은 그 거절이 발송 시점에야 드러난다는 점이었다. 판매자에게 나가야 할 알림이 템플릿 자리 하나가 어긋났다는 이유로 안 나가고, 그 사실은 나중에 알게 된다.
그래서 대조를 코드가 하게 했다. 보낼 때 대행사에서 그 템플릿을 조회해 어떤 변수 자리가 있는지 읽고, 우리가 가진 값을 그 자리에 맞춰 채운다. 사람이 맞추던 것을 등록된 템플릿 자체를 기준으로 삼아 자동으로 맞추는 것이다.
이 변경으로 템플릿 내용이 어긋나 발송이 거절되는 일이 사라졌다. 템플릿을 바꾼 사람이 개발자에게 알리지 않아도 되고, 알림 없이 바뀌었어도 조회한 값이 기준이 되니 그대로 나간다.
그 조회가 한도에 걸렸다
대신 발송할 때마다 조회가 한 번씩 붙었다. 발송량이 늘자 조회 한도를 넘겨 거절당하기 시작했다.
템플릿은 자주 바뀌지 않으니 메모리에 잠시 담아 두는 것으로 풀었다. 여기까지는 흔한 처리인데, 정하고 넘어가야 할 것이 하나 남았다. 조회가 실패했을 때 만료된 캐시라도 쓸 것인가였다.
조회 실패
429 · 5xx 일시적이다 -> 만료된 캐시라도 쓴다
401 · 403 인증이 잘못됐다 -> 빈 값을 반환한다
400 요청이 잘못됐다 -> 빈 값을 반환한다
일시적인 실패에는 오래된 값이라도 쓰는 것이 낫지만, 인증이나 설정이 잘못된 경우까지 캐시로 덮으면 그 문제가 드러나지 않는다. 발송은 계속되는데 원인은 감춰지는 상태가 가장 나쁘다고 판단해서, 오류의 성격에 따라 갈랐다.
개발하다 진짜 판매자에게 문자가 갈 뻔했다
이 작업에서 가장 신경 쓴 부분이다. 개발 환경에서 발송을 테스트하는데 대행사는 개발용과 운영용을 구분하지 않으니, 잘못 부르면 실제 번호로 진짜 메시지가 나간다.
| 겹 | 막는 것 |
|---|---|
| 프로필 가드 | 운영이 아닌 환경에서는 실제 발송 경로로 들어가지 않는다 |
| 발송 활성화 플래그 | 꺼져 있으면 요청을 만들지 않는다 |
| 수신번호 치환 | 그래도 나가야 하는 경우 지정된 테스트 번호로 돌린다 |
세 겹으로 둔 이유는 하나가 뚫려도 다음이 막게 하려는 것이었다. 설정 하나를 잘못 넣어 고객에게 메시지가 나가면 되돌릴 방법이 없다.
화이트리스트를 만들었다가 갈아엎었다
처음 만든 것은 화이트리스트였다. 템플릿 코드와 전화번호의 쌍을 미리 등록해 두고, 그 쌍이 맞을 때만 개발 환경에서 실제로 나가게 했다.
돌려 보니 쓰기가 나빴다. 테스트할 때마다 번호를 등록해야 했고, 사람이 바뀌거나 번호가 바뀌면 또 등록해야 했다. 안전하기는 한데 매번 걸리적거리니 결국 아무도 안 쓰고 다른 방법을 찾게 된다.
그래서 리다이렉트 방식으로 바꿨다. 허용한 템플릿 코드면 수신번호를 무엇으로 넣었든 지정된 테스트 번호로 돌려서 보낸다. 등록할 것이 번호가 아니라 템플릿 코드가 되니 한 번 설정해 두면 그다음부터는 손댈 일이 없다.
이전 (템플릿 코드, 전화번호) 쌍이 등록돼 있어야 발송
-> 테스트마다 번호 등록
이후 템플릿 코드만 허용 목록에 있으면 발송
-> 수신번호는 지정된 테스트 번호로 치환
안전 장치는 만들어 두는 것으로 끝나지 않고 계속 쓸 만해야 한다는 것을 여기서 배웠다. 쓰기 불편한 안전 장치는 사람이 우회하고, 우회하기 시작하면 없는 것과 같다.
그 뒤로 붙인 것들
모아 두니 그다음이 쉬워졌다. 긴 문자를 보내는 창구를 추가할 때도, 치환할 값을 요청에 담아 보내는 기능을 넣을 때도 한 군데만 고치면 됐다.
이게 통합의 실제 이득이었다. 중복이 줄어든 것보다, 새로 뭔가를 붙일 때 붙일 자리가 한 곳으로 정해져 있다는 쪽이 컸다.
모으는 것 자체는 절반이었다
돌아보면 서버 하나로 모으는 작업 자체는 어렵지 않았다. 클라이언트를 만들고 경로를 바꾸면 되는 일이었다.
시간이 간 곳은 그다음이었다. 실패했을 때 문자로 넘길지, 캐시가 오래됐을 때 쓸지, 개발 환경에서 어디까지 진짜로 보낼지 같은 것들은 모으기 전에는 각자 알아서 하던 것이라 정해진 답이 없었다. 한곳으로 모으는 순간 그 판단들도 한곳에서 내려야 했다.
멈추면 전부 멈추지만 멈춘 자리부터 다시 간다
흩어져 있을 때는 한 서비스의 발송이 막혀도 나머지는 나갔는데, 이제는 이 서버가 멈추면 모든 알림톡이 함께 멈춘다. 모으면서 얻은 것의 뒷면이다.
그런데 같은 통합이 그 답도 함께 줬다. 발송 이력을 한곳에 남기게 되면서 실패한 건에는 상태값이 붙었고, 서버가 다시 뜨면 그 상태를 보고 못 나간 것부터 이어서 내보내게 만들었다. 흩어져 있던 시절에는 어느 서비스가 어디까지 보냈는지를 모아 볼 방법 자체가 없어서, 멈춘 동안의 발송분은 사실상 사라지는 것이었다.
단일 지점이 된 것과 이어서 보낼 수 있게 된 것이 같은 변경에서 나왔다. 한곳으로 모으면 한곳이 멈출 때 전부 멈추지만, 무엇이 어디까지 나갔는지도 한곳에서 알게 된다.
다만 재발송이 모든 종류에 답인 것은 아니다. 인증 코드처럼 늦게 도착하면 의미가 없는 것은 나중에 다시 보내 봐야 소용이 없고, 그래서 그 경로만 실패 시 예외를 던지게 갈라 뒀다. 늦어도 되는 것과 늦으면 안 되는 것을 나누는 일은 그 한 종류에서 멈췄다.