네트워크 레이어를 만들다 보면 꼭 이런 순간이 옵니다.
요청 하나 보내는데 인증 토큰도 붙여야 하고, 로그도 남겨야 하고, 실패하면 재시도까지 해야 하죠.
그걸 전부 한 함수 안에 우겨넣다 보면 어느새 send() 하나가 200줄이 됩니다.
이럴 때 꺼낼 도구가 미들웨어 패턴입니다.
결론부터 말씀드릴게요. Swift에서 미들웨어는 데코레이터 패턴으로 각 기능을 감싸고, 책임 연쇄(Chain of Responsibility)로 그 감싼 것들을 순서대로 연결하면 가장 깔끔합니다. 둘은 경쟁 관계가 아니라 짝꿍이에요.
오늘은 이 조합을 왜 쓰는지, 코드로 어떻게 엮는지 실전 예제로 풀어보겠습니다.
미들웨어 패턴이 뭔가요?
미들웨어는 요청과 응답 사이에 끼어드는 중간 처리 계층입니다.
Alamofire의 RequestInterceptor나 서버 쪽 Vapor의 Middleware를 써보셨다면 이미 만나본 개념이에요.
핵심 아이디어는 하나입니다.
요청을 처리하는 본체는 그대로 두고, 그 앞뒤로 기능을 끼웠다 뺐다 할 수 있게 만든다.
인증, 로깅, 캐싱, 재시도 같은 기능을 각각 독립된 조각으로 만들어 두는 거죠.
그러면 이번 요청엔 인증만, 저번 요청엔 인증+캐싱 이런 식으로 자유롭게 조합할 수 있습니다.
데코레이터와 책임 연쇄, 뭐가 다른가요?
이 둘을 헷갈려 하시는 분이 많은데요.
짧게 정리하면 이렇습니다.
| 구분 | 데코레이터 | 책임 연쇄 |
|---|---|---|
| 목적 | 기존 객체에 기능을 덧씌움 | 여러 처리기를 줄 세워 순서대로 통과 |
| 관계 | 감싸는 구조(중첩) | 이어지는 구조(체인) |
| 통과 여부 | 항상 다음으로 넘김 | 중간에 멈출 수도 있음 |
| 미들웨어에서 역할 | 기능 하나하나의 구현 | 그 기능들을 연결·정렬 |
제가 직접 써보니 이 둘은 대립이 아니라 층위가 다른 이야기더라고요.
데코레이터는 “기능을 어떻게 붙이냐”이고, 책임 연쇄는 “붙인 것들을 어떤 순서로 태우냐”입니다.
그래서 둘을 같이 쓰면 서로의 약점을 메워줍니다.
실전 조합: 코드로 엮어보기
먼저 미들웨어의 공통 인터페이스를 정의합니다. 요청을 받아 다음 미들웨어로 넘기는 구조예요.
protocol Middleware {
// request를 받아 처리 후, next로 다음 단계에 넘긴다
func handle(_ request: Request,
next: (Request) async throws -> Response)
async throws -> Response
}
이제 각 기능을 데코레이터처럼 하나씩 만듭니다. 아래는 로깅 미들웨어예요.
struct LoggingMiddleware: Middleware {
func handle(_ request: Request,
next: (Request) async throws -> Response)
async throws -> Response {
print("➡️ 요청: \(request.url)")
let response = try await next(request) // 다음 단계로
print("⬅️ 응답: \(response.status)")
return response
}
}
여기서 next(request)를 호출하는 부분이 바로 책임 연쇄의 고리입니다.
자기 할 일(로그 찍기)을 하고 나머지는 다음 미들웨어에 넘기는 거죠.
마지막으로 여러 미들웨어를 하나의 체인으로 접어줍니다.
func buildChain(_ middlewares: [Middleware],
final: @escaping (Request) async throws -> Response)
-> (Request) async throws -> Response {
middlewares.reversed().reduce(final) { next, mw in
{ req in try await mw.handle(req, next: next) }
}
}
reduce로 뒤에서부터 감싸 나가는 게 포인트입니다.
배열을 [인증, 로깅, 재시도] 순으로 넣으면 요청은 그 순서대로 통과하고 응답은 역순으로 빠져나옵니다. 양파 껍질을 떠올리시면 딱 맞아요.
이렇게 짜면 뭐가 좋을까요?
제가 실제 프로젝트에 적용하고 느낀 장점을 정리해봤습니다.
-
기능 추가가 배열에 한 줄 넣는 일이 됩니다. 캐싱이 필요하면
CachingMiddleware()만 배열에 끼우면 끝이에요. -
순서를 바꾸기 쉽습니다. 인증을 로깅보다 먼저 태울지 나중에 태울지, 배열 순서만 바꾸면 됩니다.
-
테스트가 편합니다. 각 미들웨어가 독립적이라 하나씩 단위 테스트를 붙일 수 있어요.
-
본체가 깨끗해집니다. 200줄짜리
send()가 다시 열 줄 안쪽으로 돌아옵니다.
반대로 주의할 점도 있어요.
미들웨어가 너무 많아지면 요청 하나가 어디를 거치는지 추적이 어려워집니다.
그래서 저는 체인이 대여섯 개를 넘어가면 로깅 미들웨어를 맨 앞에 두고 통과 경로를 찍어보는 편입니다.
마무리
데코레이터로 기능을 만들고 책임 연쇄로 줄 세우는 이 조합은, 한 번 손에 익으면 네트워크 레이어뿐 아니라 이벤트 처리, 유효성 검사 같은 곳에도 그대로 써먹을 수 있습니다.
오늘 예제를 작은 토이 프로젝트에 붙여보시길 권합니다. 직접 껍질을 벗겨보면 개념이 훨씬 빨리 몸에 붙거든요. 즐거운 리팩터링 되세요!
