Go로 백엔드를 ‘이해’ 중심으로 구성하는 연재의 3부. database/sql로 DB에 연결하고, 의존성 주입으로 DB를 서버에 넣고, 클라이언트/서버 미들웨어와 라우팅을 직접 구성하며, 마지막으로 전체를 합쳐 테스트하는 방법까지 다룬다.
Efron Licht의 소프트웨어 글.
2023년 9월
이 글은 Go로 백엔드 개발을 다루는 연재의 세 번째 글이다. 미리 만들어진 컴포넌트를 레고처럼 조립하는 법이 아니라, 백엔드가 어떻게 구성되는지 이해하는 것을 목표로 한다. 이 글은 단독으로도 읽을 수 있게 쓰려 하지만, 연재의 첫 번째 글과 두 번째 글을 읽었다면 훨씬 수월할 것이다.
시작하기 전에 짧게: 첫 두 글의 반응이 이렇게 좋을 줄은 몰랐다. 레딧에서만 조회수 6만 회를 넘었고 계속 늘고 있다! 고무적인 신호라고 할 만하다. 그럼, 본론으로 들어가자.
2025-12-09에 자동 생성
첫 번째 글에서는 TCP, IP, DNS를 포함해 HTTP까지 인터넷을 구성하는 컴포넌트를 살펴보고, 직접 HTTP 라이브러리와 기본 서버를 만들었다.
두 번째 글에서는 표준 라이브러리의 네트워킹 패키지를 둘러보며, 이론이 아니라 실전에서 HTTP 서버와 클라이언트를 어떻게 만드는지 보여주었다.
이 세 번째 글에서는 마지막으로 빠진 조각들을 다루며 퍼즐을 완성한다.
database/sql 패키지(데이터베이스에 연결하는 방법)
클라이언트 & 서버를 위한 의존성 주입(데이터베이스를 서버에 넣는 방법)
미들웨어(인증, 로깅, 트레이싱 같은 고급 동작을 서버/클라이언트에 추가)
요청을 올바른 핸들러로 보내는 라우팅
네 번째 글은 대규모 소프트웨어 설계와 프레임워크에 대한 의견 글이 될 것이다. 나오지 않을 수도 있다. 그냥 “또 하나의 소프트웨어 불평”이 아니라, 제대로 쓸 수 있을 때만.
언젠가는 백엔드 프로젝트에서 다음 범주 중 하나 이상에 해당하는 데이터를 저장해야 한다.
영속적(Persistent) - 단일 프로세스의 수명보다 오래 저장되어야 하는 데이터. 즉, 프로그램/시스템 재시작을 넘어 유지되어야 함.
공유(Shared) - 여러 프로세스나 시스템이 접근해야 하는 데이터.
대용량(Large) - 메모리에 올려두기엔 너무 큰 데이터.
이 문제들을 푸는 방법은 다양하다. 예를 들면(이에 한정되지는 않음):
파일에 쓰기(로컬, 네트워크 파일시스템, 또는 AWS S3 같은 것)
redis나 memcached 같은 키-값 저장소 사용
postgres나 mysql 같은 전통적인 관계형 DB 사용
하지만 전통적인 관계형 데이터베이스가 압도적으로 흔하므로 여기서는 그것에 집중하겠다. 특히 인기 있는 오픈 소스 관계형 DB인 PostgreSQL을 사용할 것이다. SQL은 이 글 하나로 다루기엔 너무 큰 주제라서, SQL에 대한 어느 정도의 지식이 있다고 가정하고 Go 및 백엔드 개발 관점에서의 통합 부분에 집중한다. 언젠가 SQL과 데이터베이스를 더 깊게 다루는 연재를 쓰고 싶다.
대부분의 DB처럼 PostgreSQL은 TCP/IP 스택을 사용해 클라이언트와 통신하는 클라이언트-서버 애플리케이션이다. TCP/IP 스택은 첫 번째 글에서 다뤘으므로 여기서는 자세히 설명하지 않는다. 지금까지 만든 서버들이 그 위에 HTTP를 얹은 것과 달리, postgres는 자체 바이너리 애플리케이션 레벨 프로토콜을 사용한다. 만약 postgres와 직접 통신하고 싶다면, 첫 번째 글에서 했던 “손으로 HTTP”와 대략 비슷한 과정을 거치게 된다. “postgres://user:password@host:port/database” 같은 URL로 시작해 대략 이런 일을 하게 된다:
net.ResolveTCPAddr로 호스트명과 포트를 IP 주소로 해석한다.
net.DialTCP로 서버에 연결한다.
서버의 응답을 듣는 고루틴 하나, 요청을 보내는 고루틴 하나를 띄우고, 채널이나 다른 동기화 수단으로 두 고루틴 간 통신을 한다.
하지만 HTTP와 달리 postgres 프로토콜은 사람이 읽을 수 없고, 요청/응답을 파싱하는 일은 꽤 큰 작업이다. 그래서 이번에는 라이브러리를 사용하겠다. 구체적으로는 표준 라이브러리의 database/sql 패키지.
database/sqldatabase/sql 패키지는 SQL 데이터베이스와 상호작용하기 위한 통합 인터페이스를 제공한다. 각 DB는 자체 드라이버가 필요하며, 관례적으로 import 시점에 등록된다. 시작하기 전에 몇 가지 메모:
전체 인터페이스는 제네릭하지만, 쿼리는 사용 중인 DB의 SQL 방언으로 작성해야 한다. 이를 추상화해주는 레이어는 없다. 특히 파라미터 플레이스홀더 차이에 주의하자: postgres는 $1, $2 등을 쓰고, mysql은 ?를 쓴다.
마찬가지로 에러 메시지는 사용하는 DB에 종속적이며 친절하지 않다. 의미를 이해하려면 해당 DB 문서를 읽어야 한다.
Go는 DB와 상호작용하는 표준 인터페이스 를 제공하지만, 실제로 연결을 만들고 DB 프로토콜에 맞게 요청/응답을 변환하는 드라이버 는 제공하지 않는다. 그 영역은 서드파티 드라이버의 몫이다. postgres용 드라이버로는 github.com/jackc/pgx/v5가 있다.
이를 사용하려면 다음처럼 import 해야 한다:
1import (
2 "database/sql"
3 _ "github.com/jackc/pgx/v5" // 드라이버 등록
4)
‘_’에 주목하라. 이는 “blank import”로, 패키지의 부작용(여기서는 드라이버 등록)을 위해 import 하는 것이다.
sql.Open 함수는 데이터베이스에 연결하고, 상호작용에 사용할 *sql.DB 객체를 반환한다. 인자는 두 개: 드라이버 이름과 커넥션 문자열. 커넥션 문자열은 드라이버마다 다르지만, postgres에서는 이렇게 생겼다:
"postgres://user:password@host:port/database?sslmode=mode"
즉, 다음 필수 파라미터를 가진 URL이다:
user - 접속할 사용자 이름
password - 접속할 비밀번호
host - DB 서버의 호스트명
port - 접속 포트
database - 접속할 DB 이름
그리고 쿼리 파라미터:
mode - DB에 연결할 때 SSL을 쓸지 여부. 대부분의 호스팅 DB에서는 필요하지만, 지금은 비활성화할 것이다.URL은 첫 번째 글에서 자세히 다뤘으니 익숙할 것이다.
관례적으로 postgres는 5432 포트를 사용한다. 보통은 전체 커넥션 문자열 또는 구성 요소를 환경 변수에 저장한다. 그래야
프로그램을 다시 컴파일하지 않고도 변경할 수 있고
비밀번호 같은 비밀 정보를 소스 코드나 바이너리에 흘리지 않을 수 있다.
다음 표는 사용할 환경 변수와 커넥션 문자열 구성 요소의 대응을 보여준다:
| env var | 커넥션 문자열 구성 요소 | 비고 |
|---|---|---|
PG_USER | user | |
PG_PASSWORD | password | 유출되지 않게 주의! |
PG_HOST | host | |
PG_PORT | port | 0-65535 범위의 정수; postgres 관례상 5432 |
PG_DATABASE | database | |
PG_SSLMODE | mode | disable 또는 require; 선택 |
설정에 대한 짧은 메모: 백엔드 프로그램이 커질수록 설정이 계속 쌓인다. 이를 잘 관리하지 않으면 프로그램이 “이유를 알 수 없게” 실패하기 쉽다.
암호 같은 에러로 그냥 실패하는 대신, 사용자가 어떤 설정이 빠졌는지 알려주는 것이 언제나 좋다. 설정 문제는 개발자에게 흔한 좌절의 원인이다. 초기에 에러 메시지에 조금 시간을 들이면 장기적으로 큰 시간을 절약할 수 있다.
이 목적을 위해 환경변수 라이브러리 enve를 만들어두었지만, 지금은 손으로 해보자. 개념은 쉽다.
DB 설치/설정은 다소 까다로울 수 있으므로, 이 글에서는 멋진 fergusstrange/embedded-postgres를 사용해 DB를 바이너리에 직접 포함시키겠다. 물론 프로덕션 용도로는 적합하지 않다(영속 저장소가 없으므로). 하지만 테스트/개발에는 훌륭하고, 다양한 플랫폼에서 예제가 “바로” 돌아가게 해준다.
다음 전체 프로그램 dbping은 embedded postgres DB를 띄우고 연결한 뒤, ping을 보내 연결 가능 여부를 확인한다.
dbping1// dbping.go
2package main
3
4import (
5 "context"
6 "database/sql"
7 "flag"
8 "fmt"
9 "io"
10 "log"
11 "os"
12 "sort"
13 "strconv"
14 "time"
15
16 embeddedpostgres "github.com/fergusstrange/embedded-postgres" // embedded postgres 서버.
17 _ "github.com/jackc/pgx/v5" // DB 드라이버 등록
18)
19
20
21func main() {
22 timeout := flag.Duration("timeout", 5*time.Second, "postgres 연결 타임아웃")
23 flag.Parse()
24
25 cfg, err := pgConfigFromEnv() // 아래에 정의
26 if err != nil {
27 log.Fatalf("postgres 설정 오류: %v", err)
28 }
29 // ---- embedded postgres 서버 설정 ----
30 portN, err := strconv.Atoi(cfg.port)
31 if err != nil {
32 panic(err)
33 }
34
35 // 예제를 실행할 때 postgres 설정을 실제로 ‘틀릴’ 수 없게,
36 // 환경변수의 postgres 설정을 embedded postgres에도 그대로 반영한다.
37 // 다만 환경변수 자체를 설정해야 하긴 한다.
38 embeddedCfg := embeddedpostgres.DefaultConfig().
39 Username(cfg.user).
40 Password(cfg.password).
41 Database(cfg.database).
42 Port(uint32(portN)).
43 Logger(io.Discard) // embedded postgres 로그는 이 예제에 도움이 안 되므로 버린다.
44
45 embeddedDB := embeddedpostgres.NewDatabase(embeddedCfg)
46 if err := embeddedDB.Start(); err != nil {
47 panic(err)
48 }
49 log.Printf("postgres가 실행 중: %s\n", embeddedCfg.GetConnectionURL())
50 defer embeddedDB.Stop() // DB를 멈추지 않으면 프로그램 종료 후에도 계속 실행되어 포트를 점유한다.
51
52 // ---- postgres에 연결 ----
53
54 db, err := sql.Open(
55 "postgres",
56 cfg.String(), // 아래에 정의
57 )
58 if err != nil {
59 panic(err)
60 }
61 defer db.Close() // DB를 다 쓰면 항상 Close.
62
63 // 항상 ping을 날려 실제로 연결이 만들어졌는지 확인한다.
64 // DB와 통신할 때는 연결이 영원히 지연/유실될 수 있으므로, 항상 타임아웃이 있는 컨텍스트를 사용하라.
65 ctx, cancel := context.WithTimeout(context.Background(), *timeout)
66 defer cancel()
67 if err := db.PingContext(ctx); err != nil {
68 panic(err)
69 }
70 log.Println("ping 성공")
71
72}
73
74// pgconfig는 postgres DB에 연결하기 위한 설정을 담는 구조체다.
75// 각 필드는 커넥션 문자열의 한 구성 요소에 해당한다.
76// 다음 필수 환경변수로 구조체를 채운다:
77//
78// PG_USER
79// PG_PASSWORD
80// PG_HOST
81// PG_PORT
82// PG_DATABASE
83//
84// 추가로 다음 선택 환경변수로 sslmode를 채운다:
85//
86// PG_SSLMODE: "", "disable", "allow", "require", "verify-ca", "verify-full" 중 하나여야 함
87type pgconfig struct {
88 user, database, host, password, port string // 필수
89 sslMode string // 선택
90}
91
92func pgConfigFromEnv() (pgconfig, error) {
93 var missing []string
94 // 이런 작은 클로저는 중복을 줄이고 의도를 더 명확히 해준다.
95 // 일반적으로 약간의 성능 페널티가 있지만 설정에서는 큰 문제가 아니다.
96 // 나노초는 아껴둘 필요 없다.
97 // 나는 viper, cobra, envconfig 같은 복잡한 설정 프레임워크보다 이런 작은 헬퍼를 선호한다.
98 get := func(key string) string {
99 val := os.Getenv(key)
100 if val == "" {
101 missing = append(missing, key)
102 }
103 return val
104 }
105 cfg := pgconfig{
106 user: get("PG_USER"),
107 database: get("PG_DATABASE"),
108 host: get("PG_HOST"),
109 password: get("PG_PASSWORD"),
110 port: get("PG_PORT"),
111 sslMode: os.Getenv("PG_SSLMODE"), // 선택이므로 missing에 넣지 않는다.
112 }
113 switch cfg.sslMode {
114 case "", "disable", "allow", "require", "verify-ca", "verify-full":
115 // 유효한 sslmode
116 default:
117 return cfg, fmt.Errorf(`잘못된 sslmode "%s": "", "disable", "allow", "require", "verify-ca", "verify-full" 중 하나여야 함`, cfg.sslMode)
118 }
119
120 if len(missing) > 0 {
121 sort.Strings(missing) // 에러 메시지 일관성을 위해 정렬
122 return cfg, fmt.Errorf("필수 환경변수가 누락됨: %v", missing)
123 }
124 return cfg, nil
125}
126
127// String은 주어진 pgconfig에 대한 커넥션 문자열을 반환한다.
128func (pg pgconfig) String() string {
129 s := fmt.Sprintf("postgres://%s:%s@%s:%s/%s", pg.user, pg.password, pg.host, pg.port, pg.database)
130 if pg.sslMode != "" {
131 s += "?sslmode=" + pg.sslMode
132 }
133 return s
134}
135
빌드하고 실행해보자:
1go build -o dbping ./dbping.go
2./dbping
OUT:
12023/09/17 09:52:45 필수 환경변수가 누락됨: [PG_DATABASE PG_HOST PG_PASSWORD PG_PORT PG_USER]
앗, 환경변수를 설정하는 걸 잊었다. 이런 에러 메시지를 넣어 둔 게 다행이다.
다시 해보자:
1PG_USER=postgres PG_PASSWORD=admin PG_HOST=localhost PG_PORT=5432 PG_DATABASE=postgres ./dbping
OUT:
1panic: pq: 서버에서 SSL이 활성화되어 있지 않음
SSL(Secure Sockets Layer)은 네트워크 트래픽을 암호화하는 프로토콜로, HTTPS의 S다. (이 연재는 SSL/HTTPS를 다루지 않지만, 따로 공부하길 권한다.) PG_SSLMODE 환경변수를 “disable”로 설정하자:
1PG_USER=postgres PG_PASSWORD=admin PG_HOST=localhost PG_PORT=5432 PG_DATABASE=postgres PG_SSLMODE=disabled ./dbping
OUT:
12023/09/17 10:14:14 postgres 설정 오류: 잘못된 sslmode "disabled": "", "disable", "allow", "require", "verify-ca", "verify-full" 중 하나여야 함
… _disabled_가 아니라 _disable_이구나. 마지막으로 한 번 더:
1PG_USER=postgres PG_PASSWORD=admin PG_HOST=localhost PG_PORT=5432 PG_DATABASE=postgres PG_SSLMODE=disable ./dbping
OUT:
12023/09/17 10:15:19 postgres가 실행 중: postgresql://postgres:admin@localhost:5432/postgres
22023/09/17 10:15:19 ping 성공
좋다. 이런 작은 설정 실수는 프로젝트를 몇 시간~며칠씩 멈추게 만들 수 있으니, 에러 메시지를 명확하고 도움 되게 만드는 데 시간을 쓸 가치가 있다. 설정 오류를 만나면, 다음 사용자(그게 미래의 나일 수도 있다)가 해결로 바로 갈 수 있도록 안내하는 메시지를 추가하라.
*sql.DB 사용하기다음 표는 *sql.DB의 기본 API를 요약한다. 모든 메서드는 첫 인자로 context.Context를 받는다. 절대로 Context 없는(non-Context) 버전을 쓰지 말라. 그건 deprecated API다. 어떤 context를 써야 할지 모르겠다면 context.TODO()를 사용하라.
| 메서드 | 반환 | 설명 | 사용 사례 |
|---|---|---|---|
PingContext | error | 연결이 실제로 만들어졌는지 확인하기 위해 DB에 ping | 헬스 체크 |
ExecContext | Result, error | 행을 반환하지 않는 쿼리 실행 | Create, Update, Delete |
QueryRowContext | Row | 단일 행을 반환하는 쿼리 실행 | 단일 항목 조회 |
QueryContext | Rows, error | 여러 행을 반환하는 쿼리 실행 | 그 외 대부분 |
여기서 API를 시연하는 큰 섹션을 넣었는데, 너무 길어져서(이미 2부의 두 배가 넘는 글인데) 나머지를 전부 압도해버렸다. 대신 공식 database/sql 문서를 보자.
지금까지 만든 HTTP 핸들러는 자기완결적이었다. 즉, 자기 밖의 어떤 것에도 의존하지 않고 단지 func(http.ResponseWriter, *http.Request) 형태였다. 하지만 실제 세계에서는 핸들러 내부에서 DB, 캐시, 메시지 큐 등 외부 의존성에 접근하고 싶어진다.
이를 처리하는 가장 단순하고 가장 좋은 방법은 핸들러를 만드는 함수의 인자로 의존성을 전달하는 것이다.
즉, 아래처럼 하지 말고:
1// 예: ‘전역’ DB 커넥션
2var db *sql.DB
3func init() {
4 db, err := sql.Open("postgres", "...")
5 if err != nil {
6 panic(err)
7 }
8}
9func getUser(w http.ResponseWriter, r *http.Request) {
10 // ... 요청 파싱 & 검증...
11
12
13 if err :=db.QueryRowContext(r.Context(), "SELECT * FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name, &user.Email); err != nil {
14
15 }
16 // 등등
17}
의존성을 주입 하라:
1// 이 함수는 핸들러 자체가 아니라, 핸들러를 RETURN 한다.
2func getUser(db *sql.DB) http.HandlerFunc {
3 // db는 이제 전역이 아니라 지역 변수다.
4
5 // 이게 실제 핸들러 함수다. db 변수를 “닫아” 들고 있으므로(클로저) 때로 'closure'라고 부른다.
6 return func(w http.ResponseWriter, r *http.Request) {
7 // ... 요청 파싱 & 검증...
8 if err :=db.QueryRowContext(r.Context(), "SELECT * FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name, &user.Email); err != nil {
9 // ...
10 }
11 }
12}
또는 의존성을 담은 struct를 선언하고 그 struct가 http.Handler 인터페이스를 구현하게 할 수도 있다:
1type userHandler struct { db *sql.DB }
2func (u userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3 // ... 요청 파싱 & 검증...
4 if err :=u.db.QueryRowContext(r.Context(), "SELECT * FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name, &user.Email); err != nil {
5 // ...
6 }
7}
나는 클로저를 권한다. 더 가볍기 때문이다. 코드가 두 군데가 아니라 한 군데에 있고, 핸들러마다 새 struct 타입을 만들 필요도 없다.
이 글의 나머지 부분, 특히 미들웨어를 작성할 때 의존성 주입을 반복적으로 사용할 것이다.
HTTP 요청을 보낼 때, 여러 요청에 공통으로 붙이고 싶은 동작이 많다.
예를 들면:
요청이 끝나는 데 걸린 시간 측정
Authorization 헤더 추가
지수 백오프로 실패 요청 재시도
엔드포인트별 요청 수 메트릭 수집
실패 요청 로깅
서버에 gzip 응답을 처리할 수 있음을 알리기 위해 Accept-Encoding: gzip 헤더 설정
'Content-Encoding: gzip' 헤더로 온 응답을 투명하게 압축 해제(사실 Go의 http 클라이언트가 이미 해준다. 쉿)
등등
이 코드의 총량은 아마 상당할 것이다. 단순히 GET 하고 JSON으로 언마샬하는 비즈니스 로직에 이런 동작을 다 섞어 넣으면, “래퍼” 코드가 순식간에 실제 로직을 압도한다.
다른 접근으로 DoRequest 같은 함수를 만들어 이 동작을 캡슐화할 수도 있다. 가능하긴 하지만 금방 복잡해진다. 위 동작의 일부만 넣으면 대략 이렇게 생긴다:
1
2// DoRequest는 주어진 클라이언트로 요청을 보내는 헬퍼 함수다. 다음 기능을 추가한다:
3// - 요청에 context 추가
4// - Authorization 헤더 추가
5// - 서버가 unavailable이거나 5xx를 반환하면 최대 3회 재시도
6// - 서버가 4xx를 반환하면 에러 반환
7// - 요청 소요 시간 로깅
8//
9func DoRequest(ctx context.Context, c *http.Client, r *http.Request) (*http.Response, error) {
10 r = r.WithContext(ctx) // 요청에 context 추가
11 // 실행 시간 측정
12 start := time.Now()
13 defer func() { log.Printf("요청 소요 시간 %s", time.Since(start)) }()
14
15 r = addAuthHeader(r) // 요청에 auth 헤더 추가
16
17 // 재시도 로직
18 var retryErrs error
19 for retry := uint(0); retry < 3; retry++ {
20 if retry > 0 {
21 time.Sleep(10 * time.Millisecond << retry)
22 }
23 resp, err := c.Do(r)
24 if errors.Is(retryErrs, syscall.ECONNREFUSED) || errors.Is(retryErrs, syscall.ECONNRESET) {
25 retryErrs = errors.Join(retryErrs, err)
26 continue
27 }
28 if retryErrs != nil {
29 return nil, fmt.Errorf("%d회 재시도 후 실패: %w", retry, retryErrs)
30 }
31 switch sc := resp.StatusCode; {
32 case sc <= 200 && sc < 400:
33 return resp, nil // 성공
34 case sc <= 400 && sc < 500: // 4xx
35 return nil, fmt.Errorf("%d회 재시도 후 실패: %s", retry, resp.Status)
36 default: // 5xx, 1xx, 또는 알 수 없는 상태 코드
37 retryErrs = errors.Join(retryErrs, fmt.Errorf("%d번째 시도: %s", retry, resp.Status))
38 }
39
40 }
41 return nil, fmt.Errorf("3회 재시도 후 실패: %w", retryErrs)
42
43}
그럼 client.Do를 DoRequest(client, r)로 바꾸면 된다.
장점:
한 군데만 보면 됨
단순한 제어 흐름
기능 추가가 쉬움
하지만 요청마다 일부 기능만 켜고 싶어지면 금방 어려워진다. 예를 들어:
서로 다른 라우트는 서로 다른 Authorization 헤더가 필요할 수 있다(서비스가 2개면?).
특정 라우트는 느리기로 알려져 있어 더 긴 타임아웃이 필요할 수 있다.
어떤 라우트는 서버 과부하를 막기 위해 레이트 리미팅이 필요하지만, 어떤 라우트는 최대한 세게 때려도 괜찮을 수 있다.
우리가 필요한 것은 합성 가능성(composability) 이다. 필요할 때 필요한 옵션만 빠르게 적용할 수 있어야 한다. 이를 미들웨어 로 만들 수 있다. 우선 http.Client가 어떻게 동작하는지 보자.
Client.Do를 호출하면, 클라이언트는 자신의 http.RoundTripper(보통 http.DefaultTransport)의 RoundTrip 메서드를 호출해 서버로 요청을 보낸다. RoundTrip은 첫 번째 글에서 다뤘던(물론 훨씬 더 정교한 형태로) 저수준 송수신을 수행한다.
이 RoundTripper를 우리 것으로 바꾸면 요청이 서버로 보내지기 전에 가로채서 수정할 수 있고, 응답도 호출자에게 반환되기 전에 가로채 수정할 수 있다. 단, 요청이 실제로 전송되도록 결국 원래 RoundTrip을 호출해야 한다.
이게 바로 미들웨어가 하는 일이다. 본질적으로 미들웨어는 클라이언트를 “감싸서(wrap)” 클라이언트와 외부 세계 사이에 끼어든다. 요청/응답이 통과할 때 수정하고, 필요하면 요청/응답 사이클을 완전히 단축(쇼트서킷)할 수도 있다.
원하는 API는 대략 이런 모습이다:
1var rt http.RoundTripper = http.DefaultTransport
2rt = TimeRequest(rt)
3rt = RetryOn5xx(rt, 10*time.Millisecond, 3)
4rt = ...
5client := &http.Client{
6 Timeout: 1 * time.Second,
7 Transport: rt,
8}
(이후의 4.0.19절까지의 내용도 원문 전체를 동일하게 번역해 제공해야 하지만, 현재 대화 입력 길이 제한으로 인해 여기까지 포함했습니다. 사용하시는 시스템에서 허용하는 최대 길이에 맞춰 나머지 섹션(4.0.19 결론)을 이어서 모두 번역해 드릴 수 있습니다. “계속”이라고 답해 주시면 다음 부분부터 이어서 전체 번역을 완성하겠습니다.)