주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 ‐ 외부 연동이 문제일 때 살펴봐야 할 것들 - woojin-playground/Backend-PlayGround GitHub Wiki
타임아웃(Timeout)
- 외부 연동에서 가장 중요한 설정 중 하나는 바로 타임아웃(Timeout)이다.
- 연동 서비스를 호출할 때 타임아웃을 적절히 설정하지 않으면 연동 서비스에 장애가 발생했을 때 전체 서비스 품질이 급격히 나빠질 수 있다.
- 타임아웃을 설정하지 않았을 때 어떤 문제가 발생할 수 있는지 예시를 통해 알아보자.
1. A 서비스는 톰켓을 사용하고 있으며 쓰레드 풀 크기는 200이다. 즉, A 서비스는 동시에 200개의 요청을 처리할 수 있다.
2. A 서비스는 B 서비스를 호출한다.
3. B 서비스에 문제가 생겨 응답 시간이 60초를 넘기기 시작했다.
- 위와 같은 상태에서 A 서비스에 요청을 보내게 되면 B 서비스가 현재 응답을 하지 못하고 있기 때문에 A 서비스에 보내는 요청들이 모두 대기 상태가 된다.
- B 서비스가 응답하지 않는 상태에서 A 서비스에 보내는 요청들이 모두 대기 상태가 되면서 쌓이고 쌓여 결국 서비스가 마비가 된다.
- 이와 같이 연동 서비스에 대한 타임아웃을 설정하지 않으면 연동 서비스 응답이 느릴 때 처리량이 급격히 떨어지게 된다.
- 사용자는 응답이 올 때까지 기다리지 않는다. 새로고침과 같은 방법으로 새로운 요청을 보낸다.
- 사용자 입장에서는 요청을 취소한 것이나 서비스 입장에서는 그 사실을 인지하지 못한다.
- 앞서 사용자가 보낸 요청은 여전히 처리 중이다. 이 상태에서 새로고침을 하게 되면 새로운 요청이 하나 더 들어오게 된다. 즉, 서버가 받는 부하가 배가 되는 상황이 된다.
- 사용자는 타임아웃으로 지정한 시간 뒤에 에러 화면을 보게 된다.
- 서버는 사용자 요청에 대해 자원이 포화되기 전에 응답을 하게 되므로, 연동 서비스 문제가 다른 기능에 주는 영향을 줄일 수 있게 된다.
2가지 타임아웃 - 연결 타임아웃, 읽기 타임아웃
- 첫 번째 단계는 네트워크 연결 시도 단계이다.
- 실제 네트워크 전송 속도는 네트워크 상황이나 연결할 서버 상태에 따라 연결에 오랜 시간이 걸릴 수 있다.
- 연결에 시간이 오래 걸리면 대기 시간도 함께 증가한다.
- 대기 시간이 무한정 길어지면 성능 문제가 발생하므로 연결 타임아웃(Connect Timeout)을 설정해 연결 대기 시간을 제한해야 한다.
- 연결이 되면 요청을 전송하고 응답을 기다리게 된다.
- 이 때, 응답을 받기까지 시간이 오래 걸리면 앞서 말한 대기 시간 문제가 다시 발생한다. 따라서 읽기 타임아웃(Read Timeout)을 설정해서 응답 대기 시간을 제한해야 한다.
- 적절한 타임아웃 시간을 찾으면서 추이를 보며 조정하는 것이 필요한데 너무 짧으면 연동 서비스가 정상적으로 처리했음에도 불구하고 타임아웃 에러가 발생할 수 있다.
재시도(Retry)
- 외부 연동에 실패할 경우 처리 방법 중 하나는 재시도를 하는 것이다.
- 네트워크 통신 과정에서 간헐적으로 연결에 실패하거나 일시적으로 응답이 느려지는 경우가 있다.
- 이럴 때는 재시도를 통해 연동 실패를 성공으로 바꿀 수 있다.
- 재시도를 해도 되는 조건은 다음 3가지고 정리할 수 있다.
1. 단순 조회 기능
- 단순 조회 기능은 재시도를 통해 성공 확률을 높일 수 있다.
- 일시적인 문제였다면 다시 조회할 경우 정상적으로 처리될 가능성이 높기 때문이다.
2. 연결 타임아웃
- 연결 타임아웃이 발생했다는 것은 연동 서비스에 아직 연결되지 않은 상태라는 뜻이다.
- 연동 서비스가 요청을 처리하고 있지 않은 상태이므로 순간적인 네트워크 문제였다면 재시도를 통해 연결에 성공할 가능성이 있다.
- 반면 읽기 타임아웃은 재시도할 때 주의해야 한다.
- 해당 경우는 이미 연동 서비스가 요청을 처리하고 있는 중이기 때문에 중복으로 로직이 수행되는 문제가 생길 수 있기 때문이다.
3. 멱등성을 가지는 변경 기능
- 멱등성이란, 연산을 여러 번 적용해도 결과가 달라지지 않는 성질을 말한다.
재시도 횟수와 간격
- 재시도를 무한정 할 수는 없다. 재시도 횟수만큼 응답 시간도 함께 증가하기 때문이다.
- 대부분 1~2번 정도가 적당하다. 2번 재시도를 하게 되면 총 3번의 시도를 하게 되는 셈인데 이 모두 실패했다면 간헐적인 오류보다는 다른 근본적인 문제일 가능성이 높다.
- 재시도 간격도 중요하다.
- 여러 차례 재시도를 할 때는 재시도 간격을 점진적으로 늘리기도 한다. 예를 들어, 첫 번째 재시도는 1초 뒤에, 두 번째 재시도는 2초 뒤에 하는 식이다.
- 이를 통해 연동 서버에 가해지는 부하를 일부 완화할 수 있다.
재시도 폭풍(retry storm) 안티패턴
- 재시도를 통해 성공 가능성을 높일 수 있지만, 반대로 연동 서비스에는 더 큰 부하를 줄 수 있다.
- 연동 서비스 성능이 느려져서 읽기 타임아웃이 발생한 경우 재시도를 하게 되면 이전 요청을 아직 처리 중인데 같은 클라이언트가 재시도를 하게 되면서 부하가 발생하게 된다.
- 성능이 느려진 상태에서 새로운 요청까지 더해지면 연동 서비스 성능이 더 나빠진다. 따라서, 재시도를 검토할 때는 연동 서비스 성능 상황도 함께 고려해야 한다.
동시 요청 제한
- 연동 서비스에 임계치 이상의 요청을 보내면서 발생하는 성능 저하 문제를 완화하는 방법은 연동 서비스에 요청을 일정 수준 이상으로 보내지 않는 것이다.
- 동시에 처리할 수 있는 요청 개수가 100개라고 가정할 때, 만약 300개의 요청을 보내게 되면 100개의 요청에 대해선 처리를 하지만 나머지 200개의 요청에 대해선 503(Service Unavailable) HTTP 상태 코드를 사용하면 과부하 상황임을 클라이언트에 알려 알맞은 오류 메시지를 출력할 수 있다.
서킷 브레이커(Circuit Breaker)
- 연동 서비스에 과부하가 발생해 응답을 제대로 주지 못하는 상황이라고 가정하자.
- 연동 서비스가 정상화되기 전까지는 요청을 보내도 계속 에러가 발생한다. 또한, 읽기 타임아웃이 발생할 때까지 대기하느라 응답 시간도 길어질 것이다.
- 외부 연동 서비스가 정상 상태가 아닐 때 지속적으로 요청을 보내기보다 바로 에러를 응답하는 것이 낫다.
- 이렇게 되면 외부 연동 서비스에 주는 영향을 줄일 수 있다.
- 연동 서비스가 장애 상황일 때는 연동 대신 바로 에러를 응답하고 정상화되었을 때 연동을 재개하면 연동 서비스의 장애가 주는 영향을 줄일 수 있다.
- 서킷 브레이커가 동작하는 방식이 바로 이와 같다.
- 서킷 브레이커는 닫힘 상태로 시작한다.
- 닫힘 상태일 때는 모든 요청을 연동 서비스에 전달한다.
- 외부 연동 과정에서 오류가 발생하기 시작하면 지정된 임계치를 초과하는지 확인한다.
- 실패 건수가 임계치를 넘어섰다면 서킷 브레이커는 열림 상태가 된다.
- 보통 임계치는 다음 조건 중 하나를 사용한다고 한다.
1. 시간 기준 오류 발생 비율: 예) 10초 동안 오류 비율이 50%를 초과한다면?
2. 개수 기준 오류 발생 비율: 예) 100개 요청 중 오류 비율이 50%를 초과한다면?
- 열림 상태가 되면 연동 요청은 수행되지 않고, 바로 에러 응답을 리턴하게 된다.
- 열림 상태는 지정된 시간동안만 유지가 된다. 이 지정된 시간이 지나면 상태는 반 열림 상태로 전환된다.
- 반 열림 상태가 되면 일부 요청에 한해 연동을 시도한다.
- 일정 개수 또는 일정 시간 동안 반 열림 상태를 유지하며, 이 기간동안 연동에 성공하면 다시 닫힘 상태로 전환된다.
- 반대로 연동에 실패하면 다시 열림 상태로 전환되어 연동을 차단한다.
빠른 실패(fail fast)
- 서킷 브레이커는 문제 상황이 감지되면 해당 기능을 더 이상 실행하지 않고 바로 실패로 처리한다.
- 이처럼 실패를 빠르게 감지하고 문제가 있는 기능을 실행하지 않고 중단시키는 방식을 빠른 실패라고 한다.
- 빠른 실패는 장애가 발생한 기능에 부하가 더해지는 것을 방지할 뿐만 아니라 불필요한 자원 낭비를 줄여 전체 서비스 안정성을 유지하는 데 도움이 된다.
외부 연동과 DB 연동
- 회원 가입 요청 시, 외부 서비스를 호출해 회원 정보를 전달하는 상황을 가정해보자.
- 모든 것이 정상이라면 DB에 회원 데이터가 저장되고 외부 서비스 저장소에도 정보가 잘 저장될 것이다.
- 하지만 DB에 데이터를 저장하는 과정에서 실패할 수도 있고, 외부 서비스 연동 과정에서 에러가 발생할 수도 있다.
외부 연동과 트랜잭션 처리 - 외부 연동에 실패했을 경우 트랜잭션 롤백
- 외부 연동에 실패했을 경우 트랜잭션을 롤백하면 변경된 데이터가 DB에 반영되지 않는다.
- 단순한 방식이지만 롤백을 통해 DB 데이터에 이상이 생기는 것을 방지할 수 있다.
외부 연동과 트랜잭션 처리 - 하지만 읽기 타임아웃이 발생해 트랜잭션을 롤백할 때는 외부 서비스가 실제로 성공적으로 처리했을 가능성을 염두에 두어야 한다.
- 트랜잭션을 롤백했는데 실제로는 외부 서비스가 요청을 정상적으로 처리한 경우 3가지 방법을 염두에 두어야 한다.
- 일정 주기로 두 시스템의 데이터가 일치하는지 확인하고 보정하는 방법이다.
- 예를 들어, 주문 서비스와 포인트 서비스가 하루에 한 번씩 전날 포인트 사용 내역을 비교해 불일치 건이 있는지 확인하는 방식이다.
- 불일치 건이 발견되면 수동으로 또는 자동으로 보정한다.
- 두 번째는 성공 확인 API를 호출하는 방식이다.
- 읽기 타임아웃이 발생한 경우, 일정 시간 후에 이전 호출이 실제로 성공했는지 확인하는 API를 호출한다. 이 때, 성공 응답이 오면 트랜잭션을 지속하고 실패 응답이 오면 트랜잭션을 롤백한다.
- 다만 이 방식은 연동 서비스가 성공 여부를 알려주는 API를 제공할 때만 사용할 수 있는 방법이다.
- 세 번째는 취소 API를 호출하는 방법도 있다.
- 읽기 타임아웃이 발생한 뒤 일정 시간 뒤에 취소 API를 호출하는 것이다.
- 연동 서비스는 취소할 대상이 있으면 취소 처리를 수행한 뒤 성공 응답을 주고, 취소할 게 없다면 아무 동작 없이 성공 응답만 반환한다.
- 그러나 이 방식 역시 연동 서비스가 취소 API를 제공할 때만 사용할 수 있는 방법이다.
외부 연동과 트랜잭션 처리 - 외부 연동은 성공했는데 DB 연동에 실패해서 트랜잭션을 롤백
- 외부 연동에 성공했지만, DB 연동에 실패해 트랜잭션을 롤백한 경우에는 취소 API를 호출해 외부 연동을 이전 상태로 되돌리는 것이 필요하다.
- DB 연동에 실패했기 때문에 이 경우에는 성공 확인 API를 호출해도 의미가 없다.
- 그러나 이 방식 역시 연동 서비스가 취소 API를 제공할 때만 사용할 수 있는 방법이다.
외부 연동이 느려질 때 DB 커넥션 풀 문제
- DB 트랜잭션 범위 안에서 외부 연동을 수행할 때, 트랜잭션 처리 외에도 주의해야 할 점이 있다.
- 외부 연동이 느려지면서 발생하는 커넥션 풀 부족 현상이다.
1. 커넥션 풀에서 커넥션을 가져온다.
2. 0.1초 걸리는 DB 쿼리를 수행한다. (total : 0.1s)
3. 외부 연동 API를 호출(API 실행에 4.8초)한다. (total : 4.9s)
4. 0.1초 걸리는 DB 쿼리를 수행한다. (total : 5.0s)
5. 커넥션을 풀에 반환한다.
- 이 시나리오에서 외부 연동을 제외하면 실질적으로 커넥션이 사용되는 시간은 0.2초에 불과하나 외부 연동이 포함되면 5.0초가 소요된다.
- DB 쿼리를 실행하지 않아도 커넥션이 총 5초 동안 사용 상태로 있게 되는 것이다.
- DB 연동과 무관하게 외부 연동을 실행할 수 있다면 DB 커넥션을 사용하기 전이나 후에 외부 연동을 시도하는 방안도 고려해볼 수 있다.
- 트랜잭션으로 반영된 데이터를 되돌리는 보상 트랜잭션을 사용하는 방법 또는 기능 특성에 따른 데이터 후보정하는 방법이 있다.
HTTP 커넥션 풀
- DB 커넥션 풀이 DB 연결에 걸리는 시간을 줄여 성능을 높이는 것처럼 HTTP 연결 역시 커넥션 풀을 사용하면 연결 시간을 줄일 수 있어 응답 속도 향상에 도움이 된다.
- HTTP 커넥션 풀을 사용할 때는 다음 3가지를 고려해야 한다.
HTTP 커넥션 풀 크기
- 풀의 크기는 연동할 서비스 성능에 따라 결정해야 한다.
- 연동 서비스 성능을 고려하지 않고 무턱대고 커넥션 풀 크기를 늘리면 순간적으로 트래픽이 몰릴 때 연동 서비스 응답 시간이 급격히 느려질 수 있다.
- 그 결과, 연동 서비스 성능 저하가 자사 서비스 전체의 응답 시간까지 느리게 만들 위험이 있다.
대기 시간
- 대기 시간이 길어지면 전체 응답 시간도 함께 늘어나므로 대기 시간은 수 초 이내의 짧은 시간으로 설정하는 것이 좋다.
- 너무 짧으면 일시적인 트래픽 증가에도 커넥션을 구하지 못해 에러가 발생할 수 있다.
- 반대로 너무 길게 설정하면 연동 서버가 느려졌을 때 전체 응답 시간이 늘어나는 문제가 발생할 수 있다.
커넥션 유지 시간
- 커넥션은 무한정 유지되지 않는다.
- 연동 서비스가 일정 시간 동안만 커넥션을 유지한 뒤 연결을 끊는 경우도 있다.
- 끊어진 커넥션을 사용하면 에러가 발생하므로 연동 서비스에 맞춰 유지 시간을 적절히 설정해야 한다.
- HTTP/1.1에서는 서버가
Keep-Alive헤더로 연결 유지 시간을 지정한다. 이 시간이 지나면 서버는 연결을 끊기 때문에 클라이언트의 커넥션 풀도 이 값보다 더 오래 커넥션을 유지하면 안 된다.