이전 포스팅에서 무중단 배포를 rolling 방식, blue-green 방식으로 구현했었다. 그렇게 해놓고 보니, 서버가 이중화 되었을 때도 기능들이 제대로 동작하는지 테스트 하고 싶어졌다. 단일 서버였다가 서버 개수가 두 개 이상으로 늘어나면 각각의 요청이 동일한 서버가 아닌 다른 서버로 연결 되기 때문에 예기치 않은 문제들이 생길 수 있다.
특히 서버 내부의 메모리로 관리하는 자료가 있다면 문제가 발생하기 쉽다. 이 경우 서버의 메모리마다 관리하는 정보가 각각 달라지는데 만약 요청을 서로 다른 서버가 처리하게 되면 동일한 결과가 나오지 않게 되기 때문이다. stateful 한 서버가 되는 것이다. 예를 들어 로그인 정보를 각각의 서버에서 저장하고 있게 되면 로그인 정보를 가지고 있지 않은 서버에 요청을 보냈을 때 로그인 인식이 되지 않는 문제가 발생한다. 또한 채팅 서버 같은 경우 내장 메세지 브로커가 인메모리의 구독정보를 가지고 메세지 브로킹을 수행하는데, 별도로 서버간 메세지 브로커를 두지 않게 되면 서로 다른 서버에 웹소켓 연결을 하고 있는 유저간에는 실시간 소통이 불가능하다. (이 경우 대화방을 다시 로드해야 메세지들을 확인 할 수 있다.)
이러한 이유에서 미리 서버 다중화를 고려하여 인메모리(Concurrent Hash Map) 에서 수행하던 구독정보 관리를 외부의 DB(Redis) 에서 수행하도록 하고, 메세지 브로커도 레디스로 관리하게끔 설정했다. 그래서 이중화에 대한 대비가 아주 잘 되어있을 거라고 흡족해했다...ㅎㅎ 하지만 로그인 처리부터 문제가 생겨버려서 약간 당황했고, 조금 헤맸지만 아무래도 문제의 원인이 뚜렷하기 때문에 (서버 이중화) 금세 문제를 해결할 수 있었다.
1. 문제의 원인
다른 것들은 전부 잘 동작하고 있었는데 카카오 로그인이 정상적으로 진행되지 않고 에러를 일으키고 있었다. OAuth 로그인 과정에서 예외가 발생하게 되면 해당 예외는 OAuthLoginFailureHandler 라는 핸들러에 의해 핸들링 되는데, 기존에는 다른 처리를 하지 않고 리다이렉트 주소에 ?failure 라는 쿼리 스트링을 전달하도록 해놓았다.
그나마 이걸 해둬서 문제 부분으로 바로 접근 할 수 있었지만, 사실 조금 게으른 조치사항이었다. 한번도 이 부분에서 예외가 발생한 적이 없어서 핸들러만 추가해놓고 별다르게 관심을 가지지 않았던 것이었다. 정확한 문제를 파악하기 위해 에러 로그를 추가했고 이유를 찾을 수 있었다.
AuthenticationFailureHandler 클래스
public class OAuthLoginFailureHandler implements AuthenticationFailureHandler {
private final String redirectUrl = "https://naejango.site/oauth/KakaoCallback";
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
log.error(exception.getMessage()); // 로깅 추가
response.sendRedirect(redirectUrl + "?failure"); // 기존의 처리 방법
}
}
로그를 확인해 보니 [authorization_request_not_found] 라는 에러 문구를 출력하고 있었다. 스택 트레이스도 남길까하다가 이와 관련하여 발생하는 문제는 서버 내부 문제라기 보다는 OAuth 통신 규약을 잘 지키지 못한 문제일 것 같아서 그냥 안해놓았는데, 더 자세한 원인을 파악할 수 있도록 추가 해놓는 게 좋을 것 같다.
2. 해결
[authorization_request_not_found] 를 해석하자면 "인증 요청을 찾을 수 없다"는 이야기인데, 서버 이중화로 이 문제가 발생했고 요청을 찾을 수 없다는 것이 에러 문구라면 분명 "Authorization 요청 정보를 서버의 어딘가에서 가지고 카카오 인증 서버로 요청을 보냈는데 응답은 다른 서버로 가게 되었다" 라는 것으로 해석할 수 있을 것 같다.
사실 이전에 시큐리티로 이런저런 시행착오를 해봐서 대충 짐작을 하고 있었다. 현재 프로젝트에서 구현한 OAuth 로그인은 인가코드와 JWT 를 카카오 인증 서버와 주고 받고 하는 flow 를 백엔드에서 전부 처리하고 있는데, 이 방법 대신 인가코드를 프론트가 받아서 백엔드의 응답 엔드포인트에 다시 찔러 넣어 주면 또 다시 백엔드와 카카오 서버가 나머지 과정을 이어가지 않을까? 라는 생각에 시도 했었다. 그러나 백엔드 서버에서는 카카오 서버와의 통신시 요청 사항이 담긴 데이터를 서버 자체적으로 가지고 있기 때문에 오류가 났었다.
이를 해결하는 방식은 여러가지가 있겠지만, 요청과 응답이 동일한 서버로 가게 한다면 쉽게 해결 될 수 있을 것 같았다. 나는 부하 분산 알고리즘 중 동일한 ip 를 동일한 서버로 고정시키는 ip hash 방식으로 쉽게 해결할 수 있을 것 같다는 생각이 들었다. (이전 글에서 이야기한 바와 같이 NGiNX 를 더 다뤄보고 싶은 생각이 있기도 했다.) 설정이 생각보다 정말 쉬웠다. 그냥 업스트림 블록에 ip_hash 라고 명시해주면 끝이었다. 물론 얼핏 듣기로 동시에 여러 알고리즘을 섞어 쓰기도 한다고 하던데 뭐... 엄청나게 변주를 줄 일이 있을까?
이렇게 설정하고 NGiNX 를 리로드 하니 잘 작동한다. 역시 내 예감이 맞은 듯 하다. 한가지 아쉬운 것은 스택 트레이스를 그냥 다 출력해서 좀 더 뜯어보고 싶은데 못했다는 점이다.
3. 결론
솔직히 자신만만 했지만 서버 이중화로 인해서 예기치 못한 문제들이 많을 것이라고 생각했다. 그런데 이 로그인을 처리하고 곧 이어 채팅이나 알림까지 서로 다른 서버로 연결 설정을 해놓고 (컴퓨터에서는 개발자 포트 8082로, 하나는 스마트폰으로 실험) 확인하니 오류없이 다 잘 작동한다. 아직까지 모든 에러를 전부 해결했다 라고 말 할 수는 없어도 우려했던 것 보다 너무 말끔하게 동작하니 정말 뿌듯하다. 서버 다중화를 대비한 레디스, 디자인 패턴 등이 이제서야 빛을 발하는 것 같다.