네부캠 그룹프로젝트 - 7. (Bug) 백엔드에서 클라이언트 IP가 제대로 안읽힌다?

date
Dec 30, 2022
thumbnail
slug
naver-camp-project7
author
status
Published
tags
Project
summary
프록시 환경에서 IP주소 버그 해결(Feat. Nginx, Next.js)
type
Post
updatedAt
Jan 23, 2023 03:58 PM

1️⃣ 문제 상황

일일 조회수 증가는 아래와 같이 구현되어 있었다.
  1. 클라이언트에서 특정 사용자의 프로필 페이지에 접속
  1. 클라이언트의 IP 주소를 바탕으로 Redis에 조회를 한다. 이 때, nginx에서 reverse proxy로 백엔드로 요청이 오므로, nginx에서 실제 클라이언트 IP를 X-Forwarded-For 헤더로 설정한다.
  1. 백엔드에서 X-Forwarded-For헤더를 통해 실제 요청한 IP를 검증하게 된다.
  1. Redis의 set자료구조에 해당 IP가 존재한다면 pass, 존재하지 않으면 추가 후 조회수 +1
이 로직이 올바르게 동작하는 줄 믿고 있었다… 하지만 조금 지나고 나니 이상하게 분명 다른 IP에서 접속했음에도 조회수가 이상하게 오를 때가 있고, 안오를 때가 있었다.
각기 다른 지역에 사는 팀원들과 테스트해보니 4명이 접속해도 조회수가 4보다 적은 값이 올랐다. 분명 로직 어딘가에서 문제가 있었다.

2️⃣ 와… 네이버에서도 이용하는 서비스?!

벡엔드 측 로직을 다시 점검하기 시작했다. 하지만 아무리 찾아보아도 구현한 로직 상에서 틀린 부분이 없었고, 로컬 환경에서 테스트도 해보고, redis의 값들도 확인해봤지만 문제를 찾을 수 없었다.
결국 백엔드가 아니라 프론트엔드나 인프라쪽에서 문제의 원인을 찾기 시작했다. 이를 위해 먼저 Nginx Access 로그를 확인해봤다.
notion image
notion image
Nginx access 로그에서 118.67.130.xxx IP 주소가 유독 많이 나왔고, 해당 IP의 위치를 확인해보니 성남 네이버 그린팩토리였다. 처음에는 서비스 홍보가 잘 되어 ’와, 네이버에도 홍보가 되서 써보고 있구나...’ 라고 생각했으나 너무 비정상적으로 빈도가 높았다. 자세히보니 해당 IP는 우리 nCloud 인스턴스의 public IP였다…ㅠㅠ
분명 요청을 보낸 클라이언트 IP는 사용자 브라우저 측의 IP가 찍혀야하는데, 어째서 우리 서버 인스턴스의 IP가 나오는걸까?

3️⃣ 문제 원인 찾기(feat. next.js)

nginx, 프론트엔드, 백엔드 모두 구축해보니 문제의 원인에 대한 그림이 그려졌다. 현재 서버 인스턴스의 상황은 아래와 같았다.
  • 현재 프론트엔드 앱인 next.js와 백엔드 앱인 nest.js가 동일한 인스턴스에 존재한다.
  • Nginx 설정 상 dreamdev.me 호스트는 프론트엔드인 next.js로 프록시된다.
  • Nginx 설정 상 api.dreamdev.me 호스트는 백엔드인 nest.js로 프록시된다.
  • 프론트 앱인 next.js는 SSR을 지원하는 프레임워크로 이를 위해 자체적인 BFF 서버가 따로 돈다.사용자 프로필 페이지는 업데이트가 잦기 때문에 SSR 방식으로 렌더링된다.
그렇다… next.js에서 사용자 프로필 페이지를 렌더링할 시, getServerSideProps에서 axios로 백엔드에 요청을 보내는 것이 문제가 된다.
그렇게 되면 결국 백엔드 입장에서 요청을 받을 때, 요청한 클라이언트는 BFF 서버의 IP, 즉 nCloud 인스턴스 자체가 되기 때문이다. 이를 그림으로 표현하면 아래와 같다.
notion image

4️⃣ x-forwarded-for 헤더에 IP들을 append하도록 변경

문제 상황을 찾기 어려웠을 뿐이지, 막상 해결 방법은 어렵지 않아보였다.
  1. 프론트엔드 측(Next.js)에서 SSR 시에 x-forwarded-for 요청 헤더를 설정하여 진짜 클라이언트(브라우저)의 IP를 명시하여 axios 요청을 보낸다.
  1. nginx에서 요청을 받고, x-forwarded-for에 클라이이언트 IP를 추가한다.
  1. 백엔드측에서 이를 받아서 x-forwarded-for헤더의 맨 앞 IP를 대상으로 로직을 수행한다.
따라서 프론트엔드 담당 팀원에게 SSR axios요청 시 아래 코드처럼 x-forwarded-for 요청 헤더에 설정하여 보내달라고 했다.
export function ssrWrapper(namespaces, callback)=>{
  ...
  return async (context)=>{
    axiosInstance.defaults.headers.common['x-forwarded-for'] = context.req.headers['x-forwarded-for'];
    ...
  }
}
기존에는 nginx설정에서 $remote_addr로 단순히 클라이언트 IP(nCloud 인스턴스 IP)만 x-forwarded-for헤더로 설정했다.
이 설정값을 $proxy_add_x_forwarded_for로 변경하여 프론트엔드(next.js)가 설정한 x-forwarded-for에 추가된 실제 요청 IP를 덧붙이는 식으로 변경한다.
server {
  listen 443 ssl;
  server_name api.dreamdev.me;

  ssl_certificate /etc/letsencrypt/live/dreamdev.me/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/dreamdev.me/privkey.pem;

  location / {
    proxy_pass http://backend;
    # proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

5️⃣ 후기

문제 해결을 위해서 각 영역에서 얻을 수 있는 지식의 중요성이 크게 느껴졌다. 아마 next.js를 이전 회사에서 사용해보지 않았다면 문제 상황을 찾기 힘들었을 것이다.
프론트엔드 최적화보다는 백엔드 영역의 최적화가 훨씬 흥미롭지만, 내가 최종적으로 웹 엔지니어를 지망하는 이유도 이렇다. 프론트엔드와 백엔드를 모두 해보니 각자 얻을 수 있는 지식이 다르지만 서로 긴밀하기도 해서 웹 개발자로 살아가려면 모든 지식이 다 필요하다고 생각한다. (최근 유행하는 BFF를 보면, 저걸 프론트엔드가 담당해야하는지, 백엔드가 담당해야하는지도 헷갈리기 시작한다.)