본문 바로가기
Dev/Go

미들웨어 패턴

by 세대교체 2024. 7. 6.

여러 엔드포인트를 작성하다 보면 동일한 처리를 반복적으로 사용하는 경우가 있다. 또한, 모니터링 도구나 접근 로그 출력 등 투과적으로 접근해야 하는 처리도 있다.

 

이런 공통 처리를 작성하는 패턴으로 미들웨어 패턴이 있다. Go의 HTTP 서버에서도 미들웨어 패턴이 폭넓게 사용된다.

 

 

미들웨어를 만드는 법

Go로 애플리케이션이나 라이브러리를 설계하고 구현할 때는 표준 패키지의 시그니처나 인터페이스에 맞추어 구현할 때가 많다. 미들웨어 패턴을 구현할 때도 마찬가지다. Go의 미들웨어 패턴에서는 시그니처를 충족하도록 구현하는 것이 일반적이다.

 

 

이런 시그니처는 다음과 같은 이유로 재사용하기 좋다.

  • http.Handler 인터페이스를 충족하는 HTTP 핸들러 구현에 적용할 수 있다.
  • 같은 패턴의 미들웨어 구현을 통해서 호출하고 여러 미들웨어를 적용할 수 있다.

구현할 때에는 아래와 같은 함수를 만든다. 이 미들웨어에서는 인수를 받는 HTTP 핸들러 함수를 호출하기 전후에 시간 정보를 얻어서 실행 시간을 출력하고 있다.

이런 미들웨어 패턴에서는 HTTP 핸들러 전후에 별도 처리를 추가할 수 있다. 또한, 처리 전의 HTTP 요청 헤더에 추가 정보를 부여할 수도 있다.

 

 

 

추가 정보를 사용한 미들웨어 패턴 구현

XXX 타임값 또는 애플리케이션 실행 시 생성한 값을 이용해 미들웨어를 구현하고 싶지만, 미들웨어 패턴의 시그니처를 따르며 XXX 타임값을 인수로 전달할 수 없는 경우도 있다. 이럴 때는 미들웨어 패턴을 반환하는 함수를 구현하면 된다.

코드를 보면 이해하기 쉬울 것이다. 예를 들어 애플리케이션의 버전을 HTTP 헤더에 포함시키는 미들웨어는 아래와 같이 정의한다.

 

 

 

복원 미들웨어

아무리 테스트를 작성하더라도 배열 처리 중에서 panic을 발생시킬 가능성을 제로로 만들 수는 없다. 요청 처리 중에 발생한 panic을 다루는 것은 모든 핸들러가 구현해야 하는 것으로 미들웨어 패턴을 사용하는 경우가 많다. Go의 웹서버 구현은 대부분 요청마다 독립된 고루틴으로 처리하며 panic이 발생해도 요청 단위로 복원된다. 따라서 특정 고루틴에서 발생한 panic에 의해 서버가 이상 종료되는 경우는 없다. 하지만 이상 상태의 오류 응답이나 구조는 애플리케이션을 개발하는 조직에 따라 다른 사항을 사용할 수 있다.

 

아래 코드는 JSON 응답 바디에 panic 정보를 포함한 응답을 반환하는 복원 미들웨어(recovery middleware) 구현 예다. defer 문에서 panic이 발생할 경우 recover 함수를 사용해 그 내용을 JSON에 포함시켜 응답하도록 정의한다. defer 문은 선언한 후에 next 타임값과 ServeHTTP 메서드를 호출하고 있으므로 요청 처리 중에 panic이 발생한 경우 이 미들웨어에 의해 오류 응답이 반환된다.

 

 

 

접속 로그 미들웨어

Datadog 등의 SaaS를 사용하고 있다면 불필요하지만, 자체 미들웨어를 작성해서 로그에 요청 처리 개요를 기록하는 방법도 있다. 예를 들어 다음과 같은 데이터를 로그에 출력할 수도 있다.

  • 요청 처리 시작 시간
  • 처리 시간
  • 응답 상태 코드
  • HTTP 메서드 및 패스(path)
  • 쿼리 파라미터 및 헤더 정보

 

 

요청 바디를 로그에 남기는 미들웨어

Go의 HTTP 요청(*http.Request) 바디는 스트림 데이터 구조로 한 번 밖에 읽을 수 없다. 따라서 미들웨어 구현 내에서 요청 바디를 읽으면 후속 미들웨어나 HTTP 핸들러 처리 내에서 요청을 읽을 수 없게 된다. 만약 요청 바디 내용을 사용하는 미들웨어를 만드는 경우는 사전에 별도의 버퍼에 요청 바디를 복사하는 등 추가적인 처리가 필요하다.

 

아래 코드는 요청 바디를 로그에 남기는 미들웨어 구현이다. 요청 바디를 읽은 후에는 후속 핸들러에서 다시 요청 바디를 읽더라도 문제가 없도록 http.Request.Body를 재설정하고 있다. io.NopCloser 함수는 Close 메서드 구현을 충족시켜야 할 때 유용한 함수다. *bytes.Buffer 타입값에는 Close 메서드가 없지만, io.NopCloser 함수를 래핑 해서 Close 메서드를 부여할 수 있다.

 

 

이런 식으로 구현하는 것은 성능에 영향을 주므로 주의해야 한다. 엔드포인트 처리 시작 전에 모든 요청 바디를 별도의 버퍼에 복사한다는 것은, 원래는 스트림으로 처리할 수 있더라도 미들웨어 처리 부분에서 요청을 받아 끝나야 한다는 것을 의미한다. 또한, 미들웨어 구현은 적용한 엔드포인트가 실행될 때마다 반드시 실행된다는 문제가 있다.

 

요청 바디를 매번 복사하는 미들웨어를 사용할 때는 미들웨어 적용 전후로 성능에 영향이 없는지 메트릭을 확인하도록 하자. 단순 계산하면 평소보다 두 배 이상의 메모리가 필요하므로 이미지 등의 BLOB을 포함한 요청을 처리할 때는 주의하도록 하자.

 

 

상태 코드 및 응답 바디를 저장하는 미들웨어

http.Handler 타입의 시그니처로 응답을 나타내는 http.ResponseWriter 인터페이스는 읽기 관련 메서드를 가지고 있지 않다. 따라서 그대로 사용하면 응답 바디와 상태 코드를 이용할 수 없다. 이 정보들을 로그에 출력하려면 래퍼 구조체를 준비해야 한다.

 

아래 코드는 응답 내용을 후킹(hooking)하는 함수다. 후속 미들웨어, HTTP 핸들러에 전달할 http.ResponseWriter 타입값을 래핑 하므로 응답 결과를 접속 로그 미들웨어에서 확인할 수 있다.

 

 

 

요청 바디나 응답 바디를 로그에 기록한다는 것

요청이나 응답 바디 내용을 남길 때는 해당 엔드포인트가 전송할 수 있는 데이터에 기밀 정보가 포함돼 있지 않은지 충분히 검증할 필요가 있다.

 

로그는 대부분 클라우드 서비스에 전송 된다. 사용자를 등록하는 엔드포인트 요청에는 개인 정보가 포함될 수 있으며 응답에는 패스워드가 포함될 가능성도 있다. 이런 정보들이 순순히 텍스트로 로그 보관 위치에 저장되는 것은 매우 위험하다. 요청 바디나 응답 바디를 로그에 남길 때는 성능 외에도 정보 관리 측면에서도 주의를 기울여야 한다.

 

 

context.Context 타입값에 정보를 부여하는 미들웨어

미들웨어에서는 *http.Request 타입값도 처리할 수 있다. *http.Request 타입값의 WithContext 메서드나 Clone 메서드를 사용하면 context.Context 타입값에 정보를 부여할 수도 있다.

 

*http.Request 타입의 Context 메서드를 통해 얻을 수 있는 context.Context 타입값은 함수 호출 시에 전달하도록 설계하며 애플리케이션의 작동 구현에서 미들웨어를 사용하면 context.Context 타입값에 부여된 정보를 사용할 수 있다.

 

context.Context 타입값에는 일반적인 값뿐만 아니라 객체도 저장할 수 있다.

이 기법을 이용하면 다음과 같은 아이디어도 실현할 수 있다.

  • 애플리케이션 버전 정보 등을 포함시키기
  • 사전 정의된 Logger 타입값을 재사용하기

출처

https://www.yes24.com/Product/Goods/124624242

 

Go 언어로 배우는 웹 애플리케이션 개발 - 예스24

베테랑 고퍼가 알려주는 Go 언어 핸즈온 가이드Go 언어로 REST API 웹 애플리케이션을 개발할 때 반드시 알아야 할 지식을 알려준다. 전반부에는 웹 애플리케이션을 개발하기 전에 알아야 할 Go 언

www.yes24.com