현재 서버 개발에 있어 외부 연동은 필수요소가 되었고, 외부연동이 서비스에 미치는 영향도 무시할 수 없다. 연동하는 서비스에 장애가 발생하면 해당 서비스에도 영향을 받기 때문이다. 4장에서는 외부연동 시 장애 발생 상황 및 대응 방법 및 영향 최소화 방법 대한 내용을 정리할 예정이다.

 

1. 타임아웃

타임아웃은 응답시간과 깊이 관련되어 있고, 연동 서비스를 호출할 때 타임아웃을 적절히 설정하지 않으면 연동 서비스에 장애가 발생했을 때 전체 서비스의 품질이 급격히 나빠질 수 있다.

 

연동 서비스에 대한 타임아웃을 설정하지 않으면 다음과 같은 문제가 발생할 수 있다.

  • 연동 서버의 응답을 기다리느라 요청 대기가 발생하고
  • 이후에 들어온 요청 또한 대기가 발생하거나 
  • 대기 시간이 길어져 클라이언트가 재요청을 하는 등

 

타임아웃을 설정하면 다음과 같이 문제를 해결할 수 있다.

  • 연동 서버의 응답이 길어져 타임아웃이 발생하고
  • 그 다음에 들어온 요청을 처리할 수 있는 스레드가 존재해 요청 대기 없이 요청을 처리할 수 있다

 

사용자 입장에서는 타임아웃으로 인한 에러 화면을 보게되는데 장점은 다음과 같다.

  • 아무 반응 없는 무한 대기보다는 에러 화면이라도 보는 것이 낫고
  • 서버는 사용자 요청에 대해 자원(스레드 풀 같은)이 포화되기 전에 응답하게 되므로, 연동 서비스 문제가 다른 기능에 주는 영향을 줄일 수 있다.

 

2. 연결 타임아웃, 읽기 타임아웃

API 연동에서 통신 과정을 단순화하면 위 그림처럼 연결 - 요청 - 응답 - 종료 의 4단계를 거친다. 여기서 타임아웃은 1. 연결시도, 2.1 응답 전송 의 단계에 설정할 수 있다.

 

연결 타임아웃(Connection Timeout)

네트워크 연결에는 시간이 걸리는데, 네트워크 상황이나 연결할 서버의 상태에 따라 연결에 오랜 시간이 걸릴 수 있다. 연결에 시간이 오래 걸리면 대기 시간도 함께 증가하기 때문에 연결 시간 타임아웃을 설정하는 것이 좋다.

 

  • 추천 연결 타임아웃 : 3 ~ 5초

 

읽기 타임아웃(Read Timeout)

연결이 성공한 후 요청 전송 후 응답을 기다리는데, 이때 응답을 받기까지 시간이 오래 걸리면 대기 시간 문제가 다시 발생할 수 있다. 따라서 읽기 타임아웃을 설정해 응답 대기시간을 제한하는 것이 좋다.

 

  • 추천 읽기 타임아웃 : 5 ~ 30초

 

상황에 맞는 적절한 타임아웃 설정하기

읽기 타임아웃이 다소 길게 느껴질 수 있는데, 처음부터 1 ~ 3초 정도로 짧게 설정하면 타임아웃 에러가 자주 발생할 수 있고, 타임아웃 시간이 너무 짧으면 연동 서비스가 정상 처리됐음에도 불구하고 타임아웃 에러가 발생할 수 있다. 특히 상품 결제 연동 서비스일 경우, 카드 결제에 시간이 꽤 쓰이는데 읽기 타임아웃을 너무 짧게 설정한 경우 결제는 되었는데 상품은 결제되지 않았다는 에러가 발생할 수 있다. 따라서 결제처럼 민감한 기능의 경우는 읽기 타임아웃 시간을 약간 길게 설정해 간헐적으로 연동시간이 길어지더라도 정상적으로 처리할 수 있게 해야한다.

 

소켓 타임아웃과 읽기 타임아웃

읽기 타임아웃을 지정할 때는 실제로 설정하는 값이 무엇인지 확인해야 한다. 예를 들어 Apache HttpClient와 OkHttp가 있다.

 

Apache HttpClient

  • 네트워크 패킷 단위를 기준으로 소켓 타임아웃 설정
  • 소켓 타임아웃 != 전체 응답 시간에 대한 타임아웃
  • 따라서 소켓 타임아웃을 5초로 지정해도 전체 응답 시간은 5초 이상 걸릴 수 있다.

 

OkHttp의 호출 타임아웃(call timeout)

  • 읽기 타임아웃과는 별개로 호출 타임아웃(call timeout)을 설정 가능
  • 호출 타임아웃은 요청 시작부터 응답까지의 전체 시간 기준으로 설정됨

 

=> 소켓 타임아웃을 5초로, 호출 타임아웃을 10초로 설정하면 패킷은 계속 수신되지만 전체 처리 시간이 오래 걸리는 경우(10초 이상)에 타임아웃을 발생시킬 수 있다.

 

3. 재시도

외부 연동에 실패했을 때 처리 방법 중 하나는 재시도를 하는 것이다. 재시도를 통해 연동 실패를 줄일 수 있지만, 항상 재시도를 할 수 있는 것은 아니고 연동 API를 다시 호출해도 되는 조건인지 확인이 필요하다.

 

재시도 해도 되는 조건은 다음과 같다.

  • 단순 조회 기능
    • 단순 조회 기능은 포인트 중복 차감과 같은 데이터 문제가 발생하지 않으므로 재시도로 응답을 성공적으로 수신할 확률이 높다
  • 연결 타임아웃
    • 연결 타임아웃이 발생했다는 것은 연동 서비스에 아직 연결되지 않은 상태라는 뜻이고, 아직 연동 서비스가 요청을 처리하고 있지 않은 상태이므로 재시도를 통해 성공할 확률이 높다.
    • 읽기 타임아웃 시 재시도에 주의가 필요하다. 이 경우는 이미 연동 서비스가 요청을 처리하고 있는 중이기 때문이다.
  • 멱등성(idempotent)을 가진 변경 기능
    • 멱등성이란 연산을 여러번 적용해도 결과가 달라지지 않는 성질을 말한다.
    • 좋아요 기능을 예시로 들면 처음 좋아요를 누른다면 좋아요 수가 +1 되겠지만, 계속해서 좋아요를 누른다고해도 그 수가 증가하지 않는 것이 있다.

 

재시도 횟수와 간격

재시도 할 때는 재시도 횟수와 재시도 간격을 결정해야한다.

 

재시도 횟수의 경우 보통 1 ~ 2회 정도가 적당하다. 그래서 총 2 ~ 3회의 요청을 시도하게 되는데, 만약 모든 요청이 실패했다면 간헐적인 오류보다는 다른 근본적인 문제일 가능성이 크기에 재시도해도 실패할 확률이 높다. 

 

재시도 간격도 중요한데, 네트워크의 일시적 이상으로 일정 시간 동안 요청이 실패한다고 가정하자. 이때 재시도 간격을 설정하면 네트워크 일시적 이상이 종료된 이후 재시도를 시작할 수 있어 비교적 원활한 통신을 할 수 있게 된다. 이를 통해 연동 서버에 가해지는 부하를 일부 완화할 수 있다.

 

4. 재시도 폭풍(retry storm) 안티패턴

재시도를 통해 성공 가능성을 높일 수 있지만, 반대로 연동 서비스에는 더 큰 부하를 줄 수 있다. 만약 연동 서비스의 성능이 느린 상태라면 중복 요청은 부하를 유발할 수 있다. 따라서 재시도를 검토할 때는 연동 서비스의 성능 상황도 함께 고려해야한다.

 

 

5. 동시 요청 제한

선착순 이벤트와 같이 순간적으로 많은 트래픽이 몰릴 때 전체 요청을 연동 서비스에 다 전달할 경우 성능 저하나 부하가 발생할 수 있다. 이를 해결할 수 있는 방법은 연동 서비스에 요청을 일정 수준 이상으로 보내지 않는 것이다.

 

  • 연동 서비스의 최대 처리요청수가 100이라 가정
  • 본 서비스에 300개의 요청이 왔을 때
  • 연동 서비스로는 100개의 요청을 전달하고
  • 나머지 200개의 요청에는 503(Service Unavailable) 에러를 리턴

-> 이 에러 메시지를 통해 클라이언트에게 과부하 상황임을 클라이언트에 알려 알맞은 오류 메시지를 출력할 수 있다.

-> 이를 설계하기 위해선 연동 서비스의 수용량을 파악한 후 그에 맞춰 요청 수를 조절하는 것이 필요하다.

 

벌크 헤드(Bulkhead) 패턴

5. 동시 요청 제한에 적용한 방식이 벌크 헤더 패턴이라고 할 수 있는데,

벌크헤드 패턴은 각 구성 요소를 격리함으로써 한 구성 요소의 장애가 다른 구성요소에 영향을 주지 않도록 하는 설계 패턴이다.

 

6. 서킷 브레이커(Circuit Breaker)

연동 서비스가 정상 상태가 아닐 때 연동 서비스로 요청을 보내는 것이 아닌 내 서버에서 에러를 응답하는 것이 연동 서비스의 문제 영향을 최소화할 수 있는 방법이 될 수 있다. 이 역할을 서킷 브레이커가 한다.

 

서킷 브레이커는 누전 차단기와 비슷하게 동작한다. 과도한 오류가 발생하면 연동을 중지시키고 바로 에러를 응답한다. 이렇게 하면 연동 서비스로의 요청 전달을 차단할 수 있다. 

 

서킷 브레이커는 닫힘(Closed), 열림(Open), 반 열림(Half-Open)의 3가지 상태가 있다.

  • 닫힘 상태
    • 서킷 브레이커가 시작할 때 가지는 상태
    • 모든 요청을 연동 서비스에 전달
    • 외부 연동 과정에서 오류가 발생하기 시작할 때 지정한 임계치를 초과했는지 확인한다
  • 열림 상태
    • 실패 건수가 임계치를 초과하면 바뀌는 상태
    • 연동 요청을 수행하지 않고 바로 에러 응답을 리턴한다.
    • 지정된 시간 동안 유지된다.
  • 반열림 상태
    • 열림상태에서 지정한 일정 시간이 지나면 전환되는 상태.
    • 이 상태로 바뀌면 일부 요청에 한해 연동을 시도한다. 일정 개수 또는 일정 시간 동안 반 열림 상태를 유지하며 이 기간 동안 연동에 성공하면 닫힘 상태로 복귀한다. 반대로 연동에 실패하면 다시 열림 상태로 전환되어 연동을 차단한다.
  • 임계치
    • 시간 기준 오류 발생 비율(10초 동안 오류 비율이 50% 초과)
    • 개수 기준 오류 발생 비율(100개 요청 중 비율이 50% 초과)

 

빠른 실패(Fail Fast)

실패를 빠르게 감지하고, 문제가 있는 기능을 실행하지 않고 중단 시키는 방식으로, 서킷 브레이커가 사용하는 방식이다. 부하 가중을 방지하고 자원 낭비를 감소시켜 서비스의 안정성을 유지하는 효과가 있다.

 

7. DB 트랜잭션 범위 안에서 외부 연동 수행 시 - 트랜잭션 처리

회원 가입 연동을 예시로 하면 외부 연동이 성공적일 경우 사용자 정보가 DB에 성공적으로 저장되고 해당 정보가 외부 서비스의 저장소에도 정보가 성공적으로 저장될 것이다. 하지만 실패할 경우도 있는데 이때 각 케이스별로 어떻게 처리해야할지 알아보자.

 

DB 연동과 외부 연동을 함께 실행할 때는, 오류 발생 시 DB 트랜잭션 처리방법을 상황에 알맞게 판단해야 한다.  보통 다음 2가지 상황이다.

  • 외부 연동에 실패했을 때 트랜잭션을 롤백
  • 외부 연동은 성공했지만 DB 연동에 실패해 트랜잭션을 롤백

 

7.1) 외부 연동에 실패했을 때 트랜잭션 롤백

외부 연동에 실패했을 때 트랜잭션을 롤백하면, 변경한 데이터가 DB에 반영되지 않는다. 이 롤백을 통해 DB 데이터에 이상이 생기는 것을 방지할 수 있다. 하지만 읽기 타임아웃이 발생해 트랜잭션을 롤백할 때는, 외부 서비스가 실제로는 성공적으로 처리했을 가능성을 염두에 두어야 한다.

위 처럼 3.1에 읽기 타임아웃이 발생했는데, 그 이후에 3.2 포인트 차감 처리가 발생할 수도 있다. 실제 연동 서비스에 문제가 있던 것이 아니라 처리 시간이 조금 더 걸렸을 뿐인 것이다. 이처럼 타임아웃이 발생해 실패인 줄 알고 롤백했을 때, 즉 트랜잭션을 롤백했는데 서비스가 실제로는 성공한 경우 다음 2가지를 고려할 수 있다.

 

  • 일정 주기로 두 시스템의 데이터가 일치하는지 확인하고 보정하는 방법
    • 예시로 주문 서비스와 포인트 서비스가 하루에 한 번씩 전날 포인트 사용 내역을 비교해 불일치 건이 있는지 확인하고, 불일치 건이 발견되면 수동 혹은 자동으로 보정하는 방식이다.
  • 성공 확인 API를 호출하는 방식
    • 읽기 타임아웃이 발생한 경우, 일정 시간 후에 이전 호출이 실제로 성공했는지 확인하는 API를 호출한다.
    • 성공 응답이 오면 트랜잭션을 지속하고
    • 실패 응답이 오면 트랜잭션을 롤백한다
    • 주의할 점이 이 방식은 연동 서비스가 성공 여부를 알려주는 API를 제공할 때만 사용 가능하다
  • 번외) 취소 API 호출하는 방식
    • 읽기 타임아웃이 발생 후 일정 시간 후에 취소 API를 호출하는 방식이다
    • 연동 서비스는 취소할 대상이 있으면 취소 처리를 수행한 뒤 성공 응답을 주고
    • 취소할 게 없다면 아무 동작 없이 성공 응답만 반환한다
    • 이 때 연동 처리를 취소했으므로 트랜잭션도 롤백한다
    • 이 또한 연동 서비스에서 해당 API를 제공한 경우에만 가능하다

 

성공/취소 API 호출 방식의 단점

성공/취소 API 호출하는 방식은 단점이 있는데,

  • 연동 서비스에서 해당 API를 제공해야한다는 점
  • 이 API들을 호출하는 과정에서도 읽기 타임아웃 발생 가능하다는 점
  • 데이터 불일치 발생 가능하다는 특징이 있다.

따라서 두 시스템 간 데이터 일관성이 중요한 기능이라면 정기적으로 데이터 일치를 확인하는 프로세스를 갖추는 것이 바람직하다.

 

7.2) 외부연동 성공했는데 DB 연동에 실패해서 트랜잭션 롤백

외부 연동은 성공했지만, DB 연동에 실패해 트랜잭션을 롤백한 경우는 취소 API를 호출해 외부 연동을 이전 상태로 되돌리는 것이 필요하다. DB 연동에 실패했기 때문에 이 경우에는 성공 확인 API를 호출해도 의미가 없다.

 

취소 API가 없거나 취소에 실패할 수도 있기 때문에 데이터 일관성이 중요한 서비스라면 일정 주기로 데이터가 맞는지 비교하는 프로세스를 갖추는 것이 좋다.

 

외부 연동에서 실패하든, 트랜잭션이 실패하든 각 실패 상황에서 어떻게 대응해야하는지 알게 되었는데, 한 가지 궁금증이 생겼다.

Q. 취소에 실패한 경우라면 주기적인 데이터 비교로 문제를 해결할 수 있는데, 취소 API를 제공하지 않는 경우라면 데이터를 이전으로 다시 돌릴 수 없는 거 아닌가? 이 경우에는 어떻게 해야할까?

이것에 관련해선 좀 찾아봐야겠다.

 

8. DB 트랜잭션 범위 안에서 외부 연동 수행 시 - 외부 연동이 느려질 때 DB 커넥션 풀 문제

DB 트랜잭션 범위 안에서 외부 연동을 수행할 때 트랜잭션 처리 외에 커넥션 풀 부족 현상도 고려해야한다. 

 

기능 실행에 5초가 걸리는 상황이라고 가정하면, 시나리오는 다음과 같다.

  • 커넥션 풀에서 커넥션을 가져온다
  • 0.1초 걸리는 DB 쿼리를 실행한다
  • 외부 연동 API를 호출하는데 4.8초를 소요한다
  • 0.1초 걸리는 DB 쿼리를 실행한다
  • 커넥션을 풀에 반환한다

위 시나리오에서 실제 쿼리 실행 시간은 0.2초(4%) 밖에 안 걸리고 나머지 96%인 4.8초는 외부 연동을 위해 사용한다. 즉, DB 쿼리를 실행하지 않아도 커넥션이 점유된 상태가 지속되는 것이다. 이는 외부 연동 시간이 증가할 경우 DB 처리 시간은 동일한데 DB 커넥션 풀이 포화되는 문제가 발생할 수 있다.

 

해결방법으로는 DB 연동과 무관하게 외부 연동을 실행하는 것이다. DB 커넥션을 사용하기 전이나 후에 외부 연동을 시도하는 방안이 있다.

  • 전제조건 : DB 연동과 무관하게 외부 연동이 실행 가능한 상황
  • 방법 : 1. 외부 연동 후 DB 커넥션, 2. DB 커텍션 후 외부 연동
  • 효과 : 외부 연동 시간이 길어지더라도 DB 커넥션 풀 포화 문제를 예방할 수 있다
  • 주의할 점 1 : 외부 연동이 트랜잭션 범위 밖에서 실행되기 때문에 트랜잭션 커밋 후 외부연동 실패 시 롤백 불가능하다
  • 주의할 점 2 : 실패한 외부 연동에 대한 후처리 반드시 고민해야한다
    • 보상 트랜잭션을 사용한다(트랜잭션으로 반영된 데이터 되돌림)
    • 데이터 후보정(기능 특성 고려가 필요하다)

 

9. HTTP 커넥션 풀

DB 커넥션 풀이 DB 연결에 걸리는 시간을 줄여 성능을 높이는 것처럼 HTTP 연결도 커넥션 풀을 사용하면 연결 시간을 줄일 수 있어 응답 속도 향상에 도움이 된다.

 

HTTP 커넥션 풀을 사용할 때는 다음 3가지를 고려해야 한다.

  • HTTP 커넥션 풀의 크기
    • 풀의 크기는 연동할 서비스의 성능에 따라 결정해야 한다. 연동 서비스의 성능을 고려하지 않고 무턱대고 커넥션 풀 크기를 늘리면 순간적으로 트래픽이 몰릴 때 연동 서비스의 응답 시간이 급격히 느려질 수 있다. 그 결과, 연동 서비스의 성능 저하가 우리 서비스 전체의 응답 시간까지 느리게 만들 수 있다. 따라서 커넥션 풀의 크기를 설정할 때는 반드시 연동 서비스의 처리 능력을 고려해야 한다.
  • 풀에서 HTTP 커넥션을 가져올 때까지 대기하는 시간
    • 대기 시간이 길어지면 전체 응답 시간도 함께 늘어나므로 대기 시간은 수 초 이내의 짧은 시간으로 설정하는 것이 좋다
    • 1 ~ 5초가 적당하다고 한다. 너무 짧을 경우(예 : 0.1초) 일시적인 트래픽 증가에도 커넥션을 구하지 못해 에러가 발생할 수 있고, 반대로 너무 길게(예 : 10초) 설정하면 연동 서버가 느려졌을 때 전체 응답 시간이 늘어나는 문제가 발생할 수 있다
  • HTTP 커넥션을 유지할 시간(keep alive)
    • 연동 서비스가 일정 시간 동안만 커넥션을 유지한 뒤 연결을 끊는 경우도 있기에, 연동 서비스에 맞춰 유지 시간을 적절히 설정해야 한다.
    • 예) HTTP/1.1의 Keep-Alive 헤더로 연결 유지 시간 지정. 이 시간이 지나면 서버는 연결을 끊기 때문에 클라이언트의 커넥션 풀도 이 값보다 더 오래 커넥션을 유지하면 안된다

 

 

 

 

 

 

+ Recent posts