Skip to content

Fix/수정버튼 간헐적으로 표시되는 문제#181#182

Merged
Han-Joon-Hyeok merged 2 commits intomainfrom
fix/수정버튼-간헐적으로-표시되는-문제#181
Feb 3, 2025

Hidden character warning

The head ref may contain hidden characters: "fix/\uc218\uc815\ubc84\ud2bc-\uac04\ud5d0\uc801\uc73c\ub85c-\ud45c\uc2dc\ub418\ub294-\ubb38\uc81c#181"
Merged

Fix/수정버튼 간헐적으로 표시되는 문제#181#182
Han-Joon-Hyeok merged 2 commits intomainfrom
fix/수정버튼-간헐적으로-표시되는-문제#181

Conversation

@Han-Joon-Hyeok
Copy link
Contributor

@Han-Joon-Hyeok Han-Joon-Hyeok commented Feb 3, 2025

개요

프론트엔드 코드만의 문제인줄 알았으나, nginx 설정이 영향을 주었던 오류였습니다.
백엔드 개발에도 참고하시면 좋을 것 같아 모두 리뷰어로 설정했습니다.

결과물

image

운영 서버에서도 정상적으로 수정 버튼이 표시됩니다.

문제 현상

운영 서버의 /challenges/[challengeId]/main 페이지에서 본인이 작성한 게시물의 수정 버튼이 일부 게시물에만 표시되거나 아예 표시되지 않았습니다.

Image

원인 분석

1. 프론트엔드 코드

postsFeed 컴포넌트의 하위 컴포넌트인 postItem 컴포넌트에서 게시물 조회 API가 서버로 OPTION 메서드로 사전 요청(preflight)을 보냈지만 503 Service Unavailable 오류가 발생했습니다.

Image

해당 요청을 보낸 코드는 아래와 같습니다.

// postItem.tsx

const PostItem = ({ challengeId, contentDTO }: PostsFeedProps) => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [isPostAuthor, setIsPostAuthor] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const setToastPopup = useSetRecoilState(toastPopupAtom);
  const router = useRouter();

  useEffect(() => {
    const getPostInfo = async () => {
      try {
        const res = await apiManager.get(`/posts/${contentDTO.id}`);
        const data: ContentDTO = res.data.data;
        setIsPostAuthor(data.isPostAuthor);
      } catch (error) {
        console.error("Failed to fetch post info:", error);
        setToastPopup((prev) => ({
          // @ts-ignore
          message: error.data.message,
          top: false,
          success: true,
        }));
      } finally {
        setIsLoading(false);
      }
    };
    getPostInfo();
  }, [contentDTO.id, setToastPopup]);

// 하략

하지만 로컬 개발 환경에서는 OPTION 요청을 보내도 정상적으로 처리되었습니다.

Image

운영 서버와 로컬 개발 환경의 유일한 차이는 nginx 사용 여부였기에 nginx 로그를 찾아보기로 했습니다.

2. 백엔드 서버

백엔드 서버로 보내는 요청은 [브라우저] - [AWS CloudFront] - [EC2 nginx] - [EC2 Spring Boot] 순서로 전달됩니다.
nginx 설정 파일에는 DDoS 공격을 막기 위해 아래의 설정이 포함되어 있습니다.

limit_req_zone $binary_remote_addr zone=ddos_limit:10m rate=10r/s;

location / {
    limit_req zone=ddos_limit burst=10 nodelay;
    real_ip_header    X-Forwarded-For;
    set_real_ip_from 0.0.0.0/0;

    proxy_pass http://green;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
}

DDoS 공격을 방지하기 위한 옵션은 limt_req_zonelimt_req입니다.

limit_req_zone은 동일한 IP가 0.1초당 1개의 요청을 허용(rate=10r/s)한다는 것입니다. 만약, 0.1초 안에 2개 요청이 들어오면 먼저 도착한 요청은 처리하지만, 뒤에 도착한 요청은 처리하지 않습니다.

location 블럭의 limit_reqlimit_req_zone에서 명시한 rate보다 순간적으로 훨씬 많은 트래픽이 들어오는 경우를 허용하기 위해 사용합니다. 만약 0.1초 안에 11개 요청이 들어온다면 첫 요청은 처리하고, burst=10 옵션으로 인해 나머지 10개 요청은 크기가 10인 큐에 저장합니다. 그리고 큐에 저장된 요청은 0.1초마다 pop해서 요청을 처리합니다. 하지만 마지막 요청은 1초 후에 처리되기 때문에 클라이언트 입장에서는 느린 응답을 받게 되는 단점이 있습니다.

이를 해결하기 위해 nodelay 옵션을 사용합니다. 0.1초 안에 11개의 요청이 들어와도 11개 요청을 모두 처리합니다. 하지만 여전히 큐에는 10개의 요청을 넣습니다. nodelay 옵션을 사용하지 않을 때와 달리 0.1초 간격으로 1개의 요청을 pop 해서 처리하는 대신 단순히 0.1초마다 큐에서 pop만 하여 rate를 0.1초로 유지하는 역할을 수행합니다. 즉, 0.1초 안에 들어오는 11개의 요청은 처리할 수 있지만, 12개의 요청은 마지막 1개의 요청을 처리할 수 없습니다. 0.1초 안에 12개의 요청이 들어왔다면 11개의 요청은 처리하고, 10개의 요청은 큐에 저장되지만, 마지막에 도착한 요청 1개는 큐에 남은 공간이 없기 때문에 처리를 하지 못하는 것입니다.

이번 상황에서 오류가 발생하는 이유는 0.1초 안에 12개 이상 요청이 들어왔기 때문입니다. nginx 공식문서를 참고해보니 허용 요청 수를 초과하면 기본적으로 503 오류를 반환한다고 나옵니다.

Image

실패한 요청에 대해 nginx 로그를 확인해보니 아래와 같았습니다.

2025/02/03 12:52:44 [error] 447#447: *3992 limiting requests, excess: 10.350 by zone "ddos_limit", client: 218.50.111.206, server: api.habitpay.link, request: "OPTIONS /api/posts/69 HTTP/1.1", host: "api.habitpay.link", referrer: "https://habitpay.link/"
218.50.111.206 - - [03/Feb/2025:12:52:44 +0000] "OPTIONS /api/posts/69 HTTP/1.1" 503 599 "https://habitpay.link/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" "218.50.111.206"

excess: 10.350 by zone "ddos_limit"를 통해 큐의 크기(burst) 10을 초과한 10.350개가 들어왔고, 0.1초 안에 약 12개 요청이 들어왔다는 것을 알 수 있습니다.

운영 서버 페이지에서 발생한 오류와 nginx 오류를 비교해보니 동일한 것을 확인했습니다.

Image

3. 결론

  • 프론트엔드의 postsFeed 컴포넌트에서 게시물 목록 조회 API(api/challenges/[challengeId]/posts?size=10&page=1)는 한 번의 요청으로 10개의 게시물을 가져옵니다.
  • 하지만 postsFeed 컴포넌트의 하위 컴포넌트인 postItem 컴포넌트에서 상위 컴포넌트에서 전달한 게시물 10개에 대해 중복된 게시물 조회 요청(/api/posts/[postId])을 보냈습니다. 이때, OPTION 요청은 10개가 동시에 발생하는데, 이 요청이 발생하기 전에 페이지 전체를 불러올 때 공지사항 조회, 챌린지 정보 조회 API도 호출되다보니 0.1초 안에 12개 이상 요청이 nginx에 가서 503 오류가 발생했습다.
  • 그러다보니 postItem 컴포넌트 내부에서 게시물 조회 요청이 성공한 글의 isPostAuthor 상태는 true가 되었지만, 요청이 실패한 글의 상태는 초기값인 false로 유지되었습니다. 그래서 본인이 작성한 게시물 중에서도 일부만 게시물 수정 버튼이 보였고, 마치 간헐적으로 게시물 수정 버튼이 표시되는 것으로 보였던 것입니다.

문제 해결 방향

  1. postItem 컴포넌트에서 게시물 조회 로직 제거합니다. postsFeed컴포넌트에서 contentDTO를 props로 내려주기 때문에 postItem 컴포넌트에서 중복으로 요청할 필요 없습니다.
  2. 개발 환경에서도 nginx를 사용하도록 하여 운영 서버와 최대한 동일한 환경으로 구성하고, 발생할 수 있는 오류를 사전에 발견하겠습니다.

참고자료

@Han-Joon-Hyeok Han-Joon-Hyeok added the bug Something isn't working label Feb 3, 2025
@Han-Joon-Hyeok Han-Joon-Hyeok self-assigned this Feb 3, 2025
@Han-Joon-Hyeok Han-Joon-Hyeok linked an issue Feb 3, 2025 that may be closed by this pull request
@Han-Joon-Hyeok Han-Joon-Hyeok merged commit c20075a into main Feb 3, 2025
1 check passed
@Han-Joon-Hyeok Han-Joon-Hyeok deleted the fix/수정버튼-간헐적으로-표시되는-문제#181 branch February 3, 2025 16:59
@404yonara
Copy link
Contributor

오 이런식으로 ddos공격을 막는것이군요. 부모컴포넌트로부터 이미 정보를 다 받는데, isPostAuthor를 받는다고 쓸데없는 api요청을 했었네요.!
수고많으셨습니다. 배포까지 하는 준한님이라서 운영서버와 개발서버의 차이점을 분석할수있었던 것같아요 멋지십니다. 덕분에 좋은공부합니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix/수정버튼 간헐적으로 표시되는 문제

2 participants