나는 Platform 팀에서 근무하며 Platform 서버를 운영하고 있다. 내가 운영하고 있는 서버들 중 레거시 서버가 많은데 레거시 서버들은 다른 TF에서 개발한 서버이고 우리 팀에서는 이관만 받은 상황이다. 레거시 서버들은 추가 개발 계획이 전혀 없고 리소스를 최소로 투입하며 운영만 하기로 조직 내에서 의사 결정이 되었다. 문제는 TF에서 개발할 당시에는 기능 개발에 몰두하여 모니터링, 방어로직 등에 대한 안정성이 부족한 상황이다. 그래서 장애 대응하는데에 많은 리소스가 투입되고 있고 우리의 목표인 리소스를 최소로 투입하며 운영하기
를 달성할 수 없는 상황이다. 이 목표를 달성하기 위해서 이전에는 무늬만 마이크로 서비스를 모놀리식 서비스로 합치는 작업을 진행했고 오늘은 외부 API를 안전하게 사용하기 라는 주제로 작업 내용을 기록하려고 한다.
먼저 내가 운영하고 있는 서버의 구조는 다음과 같다.
보다시피 운영중인 서버는 DB와 상호작용하는 부분이 전혀 없이 순수하게 타 API 서버의 응답값으로만 서비스를 운영하고 있다. 즉, 데이터 원친이 100% 외부 API인 서버이다. 이런 구조의 서버를 운영하며 많은 어려움을 마주했는데 가장 큰 어려움은 다음과 같다.
나는 대기업에서는 이런 일이 없을 줄 알았다. 모든게 체계적으로 운영되며 철저하게 버저닝되고 하위 호환성을 보장해줄거라고 믿었다. 얼마 지나지 않아 나의 환상과 선입견이 모두 깨졌다. 대기업이든 스타트업이든 사람이 운영하기 때문에 언제든지 어떤 상황이 닥쳐도 이상하지 않다. 그래서 우리는 대비해야한다.
그런데 생각해보자. 우리는 DB를 운영하지 않아서 백업 데이터가 없다. 게다가 한국에서는 해당 데이터를 제공하는 또 다른 Vendor도 없다. 즉, 우리가 사용하고 있는 외부 API 서버가 한국의 유일한 데이터 원천이다. 이런 상황에서 어쩌란말인가? 데이터가 없는데 어떻게 서비스를 하란 말인까? 그렇다고 손 놓고 있을 수도 없는 노릇이다.
API의 모든 필드가 언제든지 누락될 수 있다고 생각해야한다. 그러면 누락 되었을 때 어떻게 대처할지 결정해야 하는데 이건 기획팀과 긴밀한 소통이 필요하다. 예를들어 전체 응답값 중 특정 필드만 누락되었다면 기본값으로 응답해도 될지 결정이 필요하다.
더 구체적인 예시를 들어보자.
오늘 날씨 어때?
라는 요청에는 오늘 기온은 15도이고, 미세먼지는 15마이크로미터입니다
라는 응답을 준다고 하자. 이때 필요한 데이터는 기온
과 미세먼지
데이터이다. 이 데이터들은 하나의 API가 제공하고 있고, 미세먼지
데이터만 누락되었다고 하자.
이런 경우에 아무런 방어 로직이 없었다면 NPE
가 발생하고 미리 정의해둔 에러 응답을 내려주게 될 것이다. 이것도 아주 훌륭한 대처이지만 몇가지를 더 추가하면 좋을 것 같다.
첫번째로, 어떤 필드가 누락되었는지에 대한 디버그 로그가 필요하다. 디버그 로그가 잘 남고 있다면 장애가 발생 했을 때 원인 파악하는데에 아주 큰 도움이 될 수 있다.
SpringBoot 코드를 통해 예시를 살펴보자
// Data Class
public class WeatherData {
private Integer temperature;
private Double dust;
}
많이 간소화 되었지만 위와 같은 응답 DTO가 있다고 하자. 이 응답 DTO에 deserialize 될 API의 응답은 아래와 같을 수 있다.
먼저 정상적인 응답값이다.
@Test
public void 정상적인_데이터() throws IOException {
final String goodData = "{ \"temperature\": 26, \"dust\": 15.5 }";
WeatherData actual = repository.getOne(goodData);
assertThat(actual.getDust(), is(15.5));
assertThat(actual.getTemperature(), is(26));
}
문제 없이 deserialize 되고 정상적으로 동작할 것이다. 그런데 이렇게 멀쩡하게 정수형이던 응답이 소수형으로 변경되서 올 수도 있다.
@Test
public void 갑자기_분위기_소수형() throws IOException {
final String missedData = "{ \"temperature\": 26.5, \"dust\": 15.5 }"; // temperature가 소수로 응답됨
WeatherData actual = repository.getOne(missedData);
assertThat(actual.getDust(), is(15.5));
assertThat(actual.getTemperature(), is(26));
}
우리가 ObjectMapper
설정으로 소수 -> 정수
로 변형을 허용하지 않았다면, 위의 상황에서 아래와 같은 에러가 발생한다.
또 정수형이던 값이 갑자기 문자열로 변경되서 올 수 있다.
@Test
public void 갑자기_분위기_String() throws IOException {
final String missedData = "{ \"temperature\": \"-\", \"dust\": 15.5 }"; // 갑자기 temperature가 String으로 응답됨
WeatherData actual = repository.getOne(missedData);
assertThat(actual.getDust(), is(15.5));
assertThat(actual.getTemperature(), is(26));
}
놀랍게도 API 문서 어디에도 값이 없을 때는 "-"
으로 응답 된다는 말이 없는 상황이지만 그럼에도 불구하고 실제로 저런 어이없는 응답을 받을 수 있다.
장애가 발생 했을 때 디버그 로그가 없다면 애먼 로직에서 장애를 파악하고 있을 것이다. 그래서 API 응답값을 Parsing 하는 시점에 항상 try-catch
, Optinal
같은 방어로직을 작성해두자.
나는 Optional
을 선호하지 않으니 try-catch
을 사용할껀데 Jackson ObjectMapper
는 JsonProcessingException
라는 super exception class
가 정의되어 있다.
해당 예외를 잡아서 현재 외부 API의 어떤 값이 누락되었는지 명시적으로 남기자. 추후 장애 대응하는데에 큰 도움이 될 것이다.
물론 예외를 throw 해도 된다. 그런데 무작정 예외를 밖으로 던지기만 한다면 중간에 어떠한 try-catch
에 의해서 예외가 intercept 되어 엉뚱한 로그가 남아 장애 파악이 더 어려워질 수 있다.
우리는 남이 개발한 서버를 이관 받았기 때문에 프로그램의 흐름을 정확하게 파악하고 있지 않다. Parsing 시점에 예외처리하거나 CheckedException 을 발생시켜서 꼭 예외를 처리하도록 하자.
어떤 API는 파라미터에 의해서 응답하는 값이 달라질 수 있다. 그리고 날씨 알려줘
라는 요청에는 기온
,미세먼지
파라미터만 필요하다.
그런데 어떠한 이유로 날씨 알려줘
라는 요청에 기온
, 미세먼지
, 황사
, 강수확률
파라미터를 요청하고 있을 수 있다. 이때 응답값 중 황사
응답값이 누락될 수 있다.
날씨 알려줘
는 황사
정보가 필요없는데도 불구하고 황사
가 누락되었으니 에러 응답이 발생하게 된다. 요청에 필요한 정보 외의 지나치게 많은 응답값을 호출하는 경우 SPOF가 될 수 있다.
API를 적절하게 호출하고 있는지 점검이 필요한 부분이다.
현재 내가 운영하고 있는 서버는 불특정 다수가 출시를 위해서 빠르게 개발해놓은 서버이다. 그래서 로직들이 어떻게 얽혀있고, 어떤 이유로 이렇게 작성되어있는지 확인할 수가 없다.
게다가 이 서버를 추가 개발할 계획은 없고 VM에 프로세스를 실행 하는 것만으로도 서비스 할 수 있는 수준으로 운영하기 위해서 많은 리소스를 투자하고 있다. 그 과정에서 모니터링, 코드 구조, 로깅 등에서 재미있는 경험을 많이 하고 있다.
이 프로젝트를 다루며 재미있는 경험을 많이하고 있는데 다음 포스팅에서는 유의미한 모니터링 지표 수집하기에 대해서 포스팅할 계획이다.
프로젝트를 하루빨리 개선해서 0의 리소스로 운영하고, 남은 리소스를 신사업 개발에 쏟아붓고 싶다.