Operation/System Architecture

MSA 간 동기로 API 호출 시 문제점 (feat. Read Timeout, ..)

JaeHoney 2023. 2. 25. 11:53

MSA간 메시지를 주고 받는 것이 필요할 때가 있다. MSA에서 아래 서버가 있다고 가정하자.

  • 송금 시스템
  • 메일 알림 시스템

송금 시스템은 송금이 완료되면 메일로 알림을 보낸다.

 

가장 쉬운 방법은 동기 API 호출을 떠올릴 수 있다.

동기 API 호출 - 문제점

내가 다니는 회사의 레거시한 시스템의 경우 동기 API 호출을 통해 MSA에서 메시지를 주고받고 있다.

 

짐작 가능한 단점은 당연히 아래의 문제가 있다.

  • 서비스 간 성능 및 장애를 공유하게 된다.
    • (1개 시스템의 장애가 MSA 전체의 처리량 저하 및 장애로 이어질 수 있다.)
  • 서비스 간의 강결합이 생긴다. (메일 알림이 실패하면 송금도 실패함)

사실 이번 글에서 다루고자하는 내용은 해당 내용보다는 아래의 내용이다.

 

동기로 API 호출을 한다고 해서 절대 안전한 것이 아니다! 동기로 해도 비동기로 했을 때처럼 문제가 발생할 수 있다.

동기 API 호출이 안전하지 않다..?

송금 시스템에서 송금이 완료된 후 메일 알림 시스템으로으 요청을 보냈는데 Read Timeout이 났다고 가정하자.

 

(참고로 Request Timeout의 종류는 대표적으로 3가지가 있다.)

  • Connection Timeout
  • Socket Timeout
  • Read Timeout(응답이 너무 오래 지연되는 경우)

Read Timeout의 경우에는 외부 API의 로직(메일 발송) 자체는 완료되었지만, 응답을 내려주려고 하는 중 Read Timeout으로 인해 Client 측의 예외가 발생할 수 있다.

 

이 경우 메일 알림이 발송이 되었는 지 안되었는 지 Client 측에서는 파악할 수 없게 된다.

  • (트랜잭션을 공유한다면) 송금을 실패처리 해야 하는 지 여부를 파악하기 힘들다.
  • (트랜잭션을 분리했다면) 메일 알림이 실제로 보내졌는 지 파악하기 어렵다.
    • (재발송 필요 여부를 판단할 수 없다.)

해결 방법

요청 내용 저장

DB를 활용하면 이를 해결할 수 있다. DB에 외부 API 요청 내용 을 저장하고,

 

요청을 처리하는 서버 로직의 트랜잭션에서 해당 요청의 상태 Flag를 변경해줄 수 있다.

로그 저장

또 다른 방법으로는 요청을 처리하는 서버에서 트랜잭션 내부에서 메일 알림 발송과 더불어 DB에 메일 알림을 발송했다는 로그를 저장하는 것이다.

 

이 경우 나중에는 클라이언트인 메인 시스템에서 DB의 로그를 조회해서 발송이 실제로 이뤄졌는지 확인할 수 있다.

 

이렇게 해야 비로소 서로 다른 서비스 간 안전하게 기능을 수행할 수 있다.

멱등성 (추천)

위에서 DB를 활용하는 두 방식은 문제가 있다. 요청을 처리하는 측에서 Flag를 설정하거나 로그가 남지만, 해당 기록이 없다고 해서 재시도해서는 안될 수도 있다.

 

이전에 보냈던 요청에 대한 기록이 없다고 해서 다시 요청을 보냈는데, 첫 번째 보냈던 요청이 실행중이었다면 2개의 요청을 수행하게 된다.

 

즉, 해당 요청이 성공했는 지 계속 모니터링을 하면서 확실히 실패 처리가 났을 때 재시도를 해야한다.

  • 요청을 처리하는 시스템이 외부 시스템이라면 지난 요청의 기록을 확인 하는 것도 어렵다.

 

아래 링크에서는 토스 증권에서 이슈를 해결하기 위한 처리가 나온다.

 

내용을 정리하면 토스 증권에서는 외부 시스템을 호출할 때 Unique한 ID를 발급해서 멱등한 요청을 보내게 된다.

 

동일한 주문이 여러개 생성되지 않게 되어서, 이전 주문 요청의 성공/실패 여부가 중요하지 않게 되고, 요청을 성공할 때까지 배치를 통해 재시도를 하면 된다.

추가 문제

동기로 호출할 경우에 대한 다양한 문제를 완화하기 위한 방법이 아래 영상(최범균님 영상)에 잘 정리되어있다! (Timeout, Bulk head, Fail fast 등에 대해 다룸)

아래에서는 해당 강연 내용과 기존 지식을 간략히만 소개한다.

  • TimeOut
    • 다른 서비스의 장애 및 성능을 공유받기 싫다면 타임 아웃을 반드시 설정하는 것이 좋다.
    • (Connection Timeout: 1초~5초, Read Timeout: 1초~3초가 적당) - 서비스에 따라 상이하다.
  • Bulk head
    • 각 시스템을 호출하는 부분을 별도의 커넥션 풀이 관리하도록 한다. - 성능과 장애가 시스템에 전파되지 않도록 해야 한다.
  • Fail fast
    • 특정 외부 시스템의 오류가 지속될 시 일정 시간동안 기능 실행을 차단한다.
  • Retry
    • 재시도가 필요한 경우 지수 백오프(Exponential BackOff) 전략을 많이 사용한다.
    • 대기시간을 지수적으로 늘려가면서(2초 / 4초 / 8초 / 16초 등) 재시도
    • 서버 부하를 줄이고 요청 성공률을 높일 수 있다.

비동기

개발을 잘하는 팀의 얘기를 들어보면 (네카라쿠배당토..) 하는 말이 있다. 

  • 각 서비스는 자신의 일만 잘하면 된다!
  • (비동기 호출로 다른 시스템을 호출하도록 아키텍처를 구현했기에 가능하다.)

(시스템 복잡도는 올라간다는 단점이 있지만!) 재처리/트랜잭션 등에 제약이 약하면 가능한 다른 시스템을 호출할 때는 비동기로 호출하는 것이 좋다.

 

아래는 비동기로 MSA 간 트랜잭션을 사용하는 방법에 대해 이전에 정리한 글이다. (참고)