패턴 카드를 읽는 법
패턴 카드를 읽는 방법에 대한 공지
· Memo 이용 안내 · 23 min read

모든 모델은 근사치이다.
본질적으로 모든 모델은 틀렸지만, 일부는 유용하다
그러나 모델의 근사적 특성을 항상 염두에 두어야 한다.
-통계학자 조지 박스
좋은 프로그램이란 무엇인가. 많은 상황에 이상 없이 대처하는 것이다.
이 얼마나 어렵고 황당무계한가.
상황이 무엇인지, 대처가 무엇인지, 많다는 것이 어느 정도인지 이 전부가 사실 경험으로 축적될 수 밖에 없다.
디자인 패턴 카탈로그는 이 질문을 피할 수 없다. 카탈로그 자체가 반복되는 상황을 모아 이름 붙인 물건이기 때문이다.
보통의 패턴 카드 한 장은 상황에 대한 묘사다. 어떤 시나리오를 미리 가정하고, 그 안에서 반복되는 문제와 구조를 적어 둔 정적인 물건이다.
반면 실제 프로그래밍은 계속 움직인다. 라이브러리의 계약이 바뀌고, 하드웨어 예산이 모자라고, 외부 서비스가 느려지며, 배포와 롤백의 조건도 설계를 바꾼다.
유행도 바뀌며 사용자 경험도 세대를 바뀌어가며 바뀐다.
좋은 유저 경험 설계가 바뀌면 팔기위한 프로그램도 바뀔 수 밖에 없다.
그러므로 카탈로그가 하는 일은 무한한 현실을 담을 수 없다.
그럼 프로그래밍과 카탈로그가 하는 일은 무엇인가?
반복되는 상황을 추려서, 그중 공통으로 말할 수 있는 부분을 공통화하는 일이다.
이 글은 내가 쓴 메모가 무엇이며, 그리고 정적인 카드를 동적인 프로젝트에서 어떻게 써야 하는지를 다룬다.
카탈로그는 손실 압축이다
패턴,
그러니까 공통화는 압축이다.
압축은 언제나 무엇을 남기고 무엇을 버릴지 정하는 것이다.
카탈로그가 남기는 것은 구조이며, 참여자, 책임, 호출 관계, 분리할 대상, 이름 붙은 선택지등을 남긴다. 이것들은 다이어그램으로 그릴 수 있고 다른 프로젝트로 옮길 수 있다.
이름이 있으므로 회의에서 긴 설명 대신 한 단어로 부를 수도 있다.
패턴 카탈로그의 궁극적인 용도는 많은 정보를 한번에 압축하는 소통 수단으로 쓰인다.
카탈로그의 효용은 상당 부분 여기서 나온다.
하지만 카탈로그가 버리는 것은 그 구조가 좋은 답이 되게 만들었던 국소 조건이다.
오른쪽 열의 항목은 모두 국소적이다.
이 팀, 이 저장소, 이 분기, 이 운영 환경에만 해당한다.
카탈로그에 넣으면 다음 독자아게는 잘못 되는 것이다. 카탈로그가 그것을 버린 것은 게으름이 아니라 옮길 수 있는 것만 남기기 위한 압축의 결과다.
다만, 카드를 받는 사람이 왼쪽만 보고 오른쪽이 있었다는 사실을 잊는 데 있다. 구조만 남으면 해법이 먼저 보이고, 그 해법을 답으로 만들었던 조건은 사라진다. 그렇기때문에 프로그래머는 늘 생각해야만 한다.
이 패턴이 가정하는 시나리오가 나에게 존재할 것인가? 내 프로젝트에 어울리는가? 내가 설계한 유저 경험(UX)에 정말로 일치하는가? 그리고 자신의 코드에 합치면서 변형해야만 한다.
그렇다면 카탈로그에 적힌 패턴들은 현실의 제약 앞에 무너지는, 쓸모없는 죽은 지식에 불과한가?
절대 그렇지 않다. 조지 박스의 명언처럼 "모든 모델은 틀렸지만, 유용하다." 패턴이 유용한 이유는 그것이 완벽한 정답이라서가 아니라,
'변형을 위한 기준점(Baseline)'을 제공하기 때문이다.
망망대해에서 자신만의 아키텍처를 밑바닥부터 발명하는 것은 막대한 인지적 비용과 리스크를 동반한다. 패턴은 우리가 서 있는 출발점을 정의해 준다.
"우리는 여기서 전략(Strategy) 패턴을 쓴다"라고 선언하는 순간, 팀원들의 머릿속에는 즉각적으로 캡슐화된 알고리즘의 교체 구조가 압축 해제된다.
수십 장의 다이어그램과 긴 회의가 단 한 단어로 대체되는 커뮤니케이션의 극단적인 효율화다. 카탈로그가 제공하는 '손실 압축'은 그 자체로 언어가 된다.
프로그래머의 일은 패턴을 숭배하는 것이 아니라, 카탈로그라는 '손실 압축된 표준'을 프로젝트의 한가운데로 밀어넣은 뒤, 우리 팀의 숙련도, 서버의 지연시간, 프로젝트 매니저의 기획 요구사항이라는 압축과정에서 버려졌던 현실의 파편들을 다시 끌어와 우리만의 형태로 기꺼이 일그러뜨리는 것이다.
우리는 패턴을 그대로 쓰기 위해 배우는 것이 아니다. 안전하고 우아하게 부수고 비틀기 위해 패턴을 배운다.
결국 좋은 프로그래머란 패턴이라는 정적인 지도를 맹신하는 자가 아니라, 지도를 들고 동적인 영토에 발을 들이밀며 현실의 지형에 맞게 기꺼이 지도를 찢어 고칠 수 있는 사람이다.
내가 패턴 카드를 쓰는 이유는 단순히 내가 겪었던 상황들에 대해서 작성하는 것도 있지만, 추후에 내 잘못된 구현들을 복기 하기 위함도 있다.
버려진 것에는 이미 이름이 있다
Christopher Alexander라는 건축가는 패턴에 대해서 맥락, 그 맥락에서 반복되는 문제, 해법의 관계를 기술하였다. 프로그래밍에서 패턴이라는 명칭은 이 유명한 건축가의 방법론에서 가져왔다.
해법은 혼자 있지 않다.1
알렉산더(Alexander)가 설명한 맥락에는 서로 충돌하거나 당기는 힘들이 있다. 비용과 성능, 독립성과 단순성, 지역 적응과 표준화처럼 동시에 만족하기 어려운 조건들이다. 패턴은 어떤 배치가 그 힘들 사이에서 받아들일 만한 균형을 만들 때 붙는 이름이다.
알렉산더에게 '문제(Problem)'란 단순히 에러가 나는 상태가 아니라, '상충하는 요구 조건들(Forces)이 팽팽하게 당기고 있는 상태'를 의미했으며, 해법(Solution)은 하늘에서 떨어지는 것이 아니라, 이 상충하는 힘들이 타협점을 찾아 안착하는 물리적 '균형점(Resolution)'이라고 말하였다.
힘(Forces), 그러니까 요구 조건의 배치가 먼저 있고 해법은 그 배치에 대한 응답으로 나온다. 그러나 카드를 받는 사람은 보통 해법을 먼저 보고, 이 해법이 맞는 상황인지를 나중에 묻는다. 때로는 묻지 않는다.
자신이 처한 '힘'과 제약조건을 왜 묻지 않는 걸까? 자신이 처한 모든 사항을 정리하고, 그 상황에 맞추어서 생각하는 것은 추상화로 겹겹이 있는 프로젝트 위에서 어느 지점까지 추상화를 벗겨내야할지 생각하는 고통스러운 지점이기때문이다.
어떨때는 추상화 위에서 사고해도 충분하지만, 어떨때는 그 인지적 부하를 벗길 줄 알아야한다. 프로그래머의 제 1원칙이란 사람마다 다르지만, 나에게 있어서 1원칙은 무엇을 교환할까? 트레이드 오프에 대한 것이다.
어떤 시기에 무엇을 교환할지가 경험으로 만들어지고 프로그래머가 진정으로 생각해야하는 것이다. 어떤때는 성능을 얻기 위해 메모리를 내어주어야하고, 어떤때는 유지 보수성을 위해서 성능을 희생하여야 한다.
실제 프로그램은 제약 때문에 동적이다
패턴 카드가 추상화한 상황 위에는 프로그래머 처한 상황에 따라 다른 실제 프로젝트의 제약이 올라간다.
같은 요구사항도 제약이 다르면 다른 설계를 작성해야한다.
제약은 프로그램이 될 코드 조각들을 깍아내는 끌(Chisel)이다.
상황 | 처음 떠오르는 해법 | 실제 제약 | 바뀌는 설계 |
|---|---|---|---|
큰 파일 변환 | 전부 메모리에 올려 처리 | 메모리 상한과 동시 요청 | 스트리밍, 청크, 역압력 |
외부 결제 호출 | 실패하면 즉시 재시도 | 중복 결제 위험 | 멱등성 키, 제한된 재시도, 보상 절차 |
대량 목록 조회 | 도메인 객체를 전부 구성 | N+1 조회와 지연 시간 | 필요한 열만 조회, 페이지네이션, 읽기 모델 |
화면 갱신 | 서버 응답 뒤에만 상태 변경 | 높은 지연과 사용자 조작 | 임시 UI 상태, 서버 결과와의 정합화 |
- 상황
큰 파일 변환
- 처음 떠오르는 해법
전부 메모리에 올려 처리
- 실제 제약
메모리 상한과 동시 요청
- 바뀌는 설계
스트리밍, 청크, 역압력
- 상황
외부 결제 호출
- 처음 떠오르는 해법
실패하면 즉시 재시도
- 실제 제약
중복 결제 위험
- 바뀌는 설계
멱등성 키, 제한된 재시도, 보상 절차
- 상황
대량 목록 조회
- 처음 떠오르는 해법
도메인 객체를 전부 구성
- 실제 제약
N+1 조회와 지연 시간
- 바뀌는 설계
필요한 열만 조회, 페이지네이션, 읽기 모델
- 상황
화면 갱신
- 처음 떠오르는 해법
서버 응답 뒤에만 상태 변경
- 실제 제약
높은 지연과 사용자 조작
- 바뀌는 설계
임시 UI 상태, 서버 결과와의 정합화
항상 스트리밍하거나 항상 재시도 정책을 쓸 이유도 없다. 굳이 스크립트 레벨에서 처리 가능한 문제에 복잡하게 정책(Policy)니 할 필요도 없다.
어느 제약이 설계를 바꾸는지 언제나 인지하여야만 한다. 제약을 무시한 이상적인 코드는 현실에서 느려지거나, 중복 실행하거나, 장애 뒤에 복구 경로를 잃는다.
프로그램이라는 통은 변경 축을 소유해야만 한다.
변경을 흡수한다는 말은 모든 것을 커다란 추상화 뒤에 숨기는 일이 아니다.
좋은 경계는 한 종류의 변경 축과 그에 딸린 규칙을 함께 소유한다.
소프트웨어 공학은 그 답은 저마다의 멘탈모델에 다르지만, '변동성을 캡슐화(Encapsulation of Volatility)'라는 시점에서도 볼 수 있다.
무엇이 바뀔 수 있는지, 누가 그 변화를 결정하는지, 실패하면 어디까지 되돌릴지를 한곳에 둔다. 경계가 없으면 정책은 호출부마다 흩어진 if, 예외 처리, 재시도, 환경 분기로 나타낸다. 반대로 경계가 너무 크면 서로 무관한 변경이 한 모듈에 묶인다.
그러므로 패턴을 쓴다는 말은 구조를 도입한다는 뜻보다, 변경 축과 책임을 어느 경계에 둘지 정한다는 뜻에 가깝고, 그것이 프로그래머의 역할이다.
정적인 카드를 동적으로 쓰는 방법
내가 작성하는 패턴 카드가 정적이라는 사실은 바꿀 수 없다.
아무리 현실을 모방하여 지도를 그린다 한 들 실시간으로 변화하는 지형 전체를 다 담을 수 없기때문이다.
그러기에 패턴 카드를 해법으로 읽지 말고 주장으로 읽어야만 한다. 모든 패턴은 무엇이 바뀌고 무엇이 바뀌지 않는지에 대한 주장을 하나씩 깔고 있다.
정동우라는 프로그래머가 과거에 이렇게 생각하고 구현했던 것을 당신은 다르게 생각하고 반박해야만 한다. 그리고 미래의 나 또한 과거의 정동우의 주장과 싸워야 한다. 그때의 정답이 지금 나의 정답이 아니기때문이다.
주장으로 바꾸면 흔들어 볼 수 있다. 이벤트 전달 카드를 예로 들면 다음 질문이 나온다.
통지 순서가 바뀌어도 되는가?
통지 중 예외가 나면 나머지 구독자는 어떻게 되는가?
구독자가 통지 중에 구독을 해제하면 어떻게 되는가?
통지가 여러 스레드에서 동시에 일어나면 어떤 계약을 지키는가?
해제되지 않은 구독자는 언제 회수되는가?
통지는 사라져도 되는가, 반드시 도달해야 하는가?
카드는 이 질문에 흔들릴 것이다. 또는 카드를 쓸때 이 질문들을 생각도 못했을 수 있다. 핵심은 카드가 가정한 힘의 배치와 지금의 배치가 다를 뿐이다. 단일 스레드, 적은 수신자, 통지 유실이 치명적이지 않은 곳에서는 그 카드가 여전히 좋은 답일 수 있고, 미래의 답에는 아닐 수 있다.
카드를 받았을 때 할 일은 해법을 바로 적용하는 일이 아니라, 카드가 가정한 배치를 복원하고 현재 배치와 비교하는 일이다.
카탈로그가 맥락을 버렸다라도 프로그래머는 그 프로젝트 안에서는 맥락을 보존해야만 한다.
예를 들어 저장소 접근 경계를 둔 이유가 저장 매체 교체 계획 때문인지, 테스트 때문인지, 단순한 관행 때문인지 적혀 있으면 나중에 그 결정을 다시 평가할 수 있다. 적혀 있지 않으면 구조만 남고, 아무도 건드리지 못한다.
여러 패턴은 같은 경계를 만든다
카드 하나는 국소적인 상황을 다룬다.
그리고 그 카드들이 모였을때 경계가 된다. 아키텍처와 레이어는 여러 카드가 같은 힘을 공유할 때 나온다.
카드가 “이 변화는 여기서 흡수한다”는 주장이라면, 아키텍처는 그런 주장들이 어떤 것들을 모을 수 있는지, 어떤 것은 나누어야하는지를 결정한다.
패턴들을같은 폴더에 넣었다고 아키텍처가 되는 것은 아니다. 카드들이 같은 외부 의존성의 실패와 변경을 함께 다루고, 그 정책의 최종 결정권자가 하나일 때 하나의 경계가 된다. 호출자는 공급자의 세부 오류나 재시도 횟수를 알지 않고 그 경계의 계약만 알면 된다.
레이어도 같은 방식으로 생기며, 화면 상태와 사용자 입력은 화면 가까이에 두고, 유스케이스의 순서와 권한 확인은 애플리케이션 쪽에 두며, 도메인 규칙은 저장 매체와 분리하고, 네트워크와 데이터베이스의 세부사항은 바깥으로 보낼 수 있다. 이 분리가 맞는 이유는 이름이 익숙해서가 아니다.
각 부분의 변경 빈도, 실패 방식, 소유자, 테스트 방법이 다르기 때문이다.
그러므로 프로그래밍 아키텍처는 무엇인가? 단순히 대응할 수 있는 패턴의 갯수가 많은 게 아니다. 여러 국소 해법이 더 높은 층위의 상황에 함께 답하게 만든 포괄적인 결과다. 패턴이 하나씩 맞더라도 서로 다른 경계의 정책을 한곳에 섞으면 아키텍처는 나빠질 수 있다.
경계를 올릴지 판단할 때는 다음을 생각해봐야한다.
이 카드들이 실제로 같은 이유로 함께 바뀌는가?
이 카드들이 다루는 실패와 자원 예산의 소유자가 같은가?
이 정책을 호출부마다 반복하지 않고 한 계약으로 감출 수 있는가?
경계를 만든 뒤에도 내부 정책을 관찰하고 재평가할 수 있는가?
답이 대부분 아니면 아직은 카드 여러 장일 뿐이다. 억지로 레이어 이름을 붙이면 구조만 커지고, 변경 축은 여전히 흩어진다. 중요한 것은 자신의 논리 안에서 얼마나 일관적인 문제 구분이 생겼냐이다.
만약 자신의 논리안에서 또는 조직의 논리안에서 그리고 현실의 물리적 제약과 동기화되어, 그 문제 구분이 일관적이라면 그것이 아키텍쳐인 것이다.
그렇기에 패턴은 결론일 뿐이다.
Christopher Alexander는 1996년 OOPSLA 기조연설에서 소프트웨어 패턴이 아이디어를 교환하는 형식으로는 유용하지만, 일관된 전체를 생성하는 능력과 그것이 인간의 삶에 주는 영향을 충분히 다루지 못했다고 지적했다.2
그 지적은 패턴을 버리라는 뜻이 아니다. 개별 패턴들을 모으는 일과, 그 패턴들로 하나의 일관된 결과를 만들어 내는 일은 서로 다르다는 뜻이다.
패턴은 이미 정리된 결론이다. 그러나 결론만 모아 둔다고 해서 좋은 프로그램이 만들어지는 것은 아니다. 그 결론을 낳은 질문과 제약, 상충하는 힘과 선택의 과정이 복원되지 않으면, 패턴은 맥락을 잃은 구조물로 남는다. 서로 다른 질문에서 나온 결론들을 한곳에 모았다는 이유만으로 그것들이 하나의 전체를 이루는 것도 아니다.
좋은 프로그램이 인간의 삶에 영향을 준다는 말은 추상적인 예술론이 아니다. 사용자가 자신의 문제를 더 매끄럽게 해결하도록 하고, 그 결과로 비즈니스의 가치를 만들며, 시스템을 운영하는 개발자의 새벽을 더 안전하게 지켜 준다는 철저히 실용적인 의미다.
패턴의 집합이 ‘일관된 전체’가 되려면 시스템이 놓인 물리적 제약 -비용, 지연 시간, 레거시, 장애 가능성-과 사용자 경험을 먼저 따라야 한다. 프로그램의 목적은 완벽한 패턴을 전시하는 데 있지 않다.
패턴은 현실을 압축한 지도다. 프로그래머의 일은 그 지도를 숭배하는 것이 아니라, 지도에서 빠진 제약과 질문을 다시 복원하여 현재의 영토에 맞게 고치는 일이다.
각주
- Christopher Alexander, Sara Ishikawa, Murray Silverstein et al., [A Pattern Language: Towns, Buildings, Construction](https://arl.human.cornell.edu/linked%20docs/Alexander_A_Pattern_Language.pdf), Oxford University Press, 1977. ↩
- Christopher Alexander, “The Origins of Pattern Theory, the Future of the Theory, And the Generation of a Living World,” OOPSLA keynote, 1996. Transcript ↩