패턴 용어에 집착하기보다 언어가 제공하는 개념과 의도, 단순함에 초점을 맞추자는 주장. 패턴은 종종 이미 우리가 매일 쓰는 함수·인터페이스·클로저 같은 개념의 다른 이름일 뿐이며, 이름보다 문제와 목표가 더 보편적이라는 지적.
자, 나랑 같이 커맨드 패턴의 위키피디아 소개를 읽어보자:
객체 지향 프로그래밍에서 커맨드 패턴은, 어느 시점에 동작을 수행하거나 이벤트를 트리거하는 데 필요한 모든 정보를 캡슐화한 객체를 사용하는 행위(행동) 디자인 패턴이다.
이걸 나는 뭐라고 부르냐고? 함수다.
이 정보에는 메서드 이름, 그 메서드를 소유한 객체, 그리고 메서드 매개변수 값들이 포함된다.
여기 당신의 커맨드가 있다:
const command = () => object.method(arg1, arg2);
// 혹은 PHP, Rust, C++에서도...
내가 모르는 디자인 패턴 얘기를 들을 때마다, 뭔가 중요한 개념을 놓친 게 아닐까 겁부터 난다. 그리고 검색해보면 내가 프로그래밍을 시작한 이래로 무의식 중에 써 왔던 것들이더라. 그래, 나 오늘도 데이터 스토어 패턴을 썼어(변수 말이야).
왜 이렇게 짜증이 나는지 설명하기 어려운 이유는, 우리가 애초에 뭘 얘기하는지 정의하기가 어렵기 때문이다.
“이터레이터”는 패턴이라고 불리지만, “미디에이터”가 패턴인 것과는 같은 의미의 패턴이 아니다. 이터레이터는 프로그래밍 언어나 생태계에 의해 형식화된 인터페이스다. 미디에이터는 클래스 계층의 템플릿이다. 이터레이터는 경직돼 있고 추상화 경계를 넘어 사용하려면 같은 인터페이스를 구현해야 한다. 미디에이터는 그저 모범 사례, 권고안일 뿐이다. 그런데 왜 둘 다 행위 패턴이라고 부르는지 모르겠다.
실제로 내가 “이터레이터”라고 말할 때, 디자인 패턴을 뜻하는 게 아니라 IEnumerator
를 구현한 타입을 뜻한다 — 설령 언어가 인터페이스를 지원하지 않아도 내 머릿속에서는 그렇게 생각한다. 난 이터레이터를 설계 문제를 푸는 수단이나 아키텍처의 부품으로 생각하지 않는다. 그건 그저 목적을 위한 수단일 뿐이다. 내 알 바엔, 컬렉션마다 제공하는 forEach
메서드가 클로저를 받는 방식도 반복의 또 다른 타당한 접근이다 — 유일한 큰 차이는 이터레이터가 언어와 생태계 차원의 지원을 더 잘 받는다는 점이고, 그래서 그걸 쓰는 것뿐이다.
일반적으로 패턴을 쓰지 말라는 것도 아니고, 패턴에 이름 붙이는 게 나쁘다고 말하는 것도 아니다. 말하고 싶은 건 “패턴”이라는 단어 자체가 무의미하다는 것, 특히 “글루” 패턴은 너무 가변적이라 이름 붙이는 것 자체가 별 의미가 없다는 것이다.
글루 패턴의 문제는, 이터레이터처럼 _개념_을 형성하지 못한다는 데 있다 — 손에 잡히는 가치를 더하지 않는다. 예를 들어 “전략(strategy)” 패턴이 정말로 의미하는 건 “interface
를 써라”이다. 인터페이스로 구현된다는 얘기가 아니라, 애초에 인터페이스가 존재하는 주된 이유가 그거라는 말이다. 그걸 두고 굳이 다른 용어가 또 필요한가?
요지는 이렇다: 패턴은 _유용_하지만, 그것에 _집중_하는 건 말이 안 된다. 난 “컬렉션을 반복한다”라고 하지 “이터레이터를 쓴다”고 하지 않는다. 난 “추상화를 만든다”라고 하지 “전략을 쓴다”고 하지 않는다. 난 “클로저를 넘긴다”라고 하지 “커맨드를 만든다”고 하지 않는다.
좋은 설계는 보이지 않는다. 방해돼서는 안 된다.
당신이 “파사드”라고 말하는 순간, 내 머리는 끼이익 하고 멈춰선다. 난 대부분의 시간을 코드 작업에 쓰고, 그건 코드에서 보는 표현이 아니기 때문이다. 그걸 해석하려면, 인간 언어에서 프로그래밍 언어로 기어를 바꿔 번역해야 한다. 그래야만 내 설계에 그게 어떻게 들어맞는지 온전히 받아들일 수 있다. 이건 방해가 되지 말아야 한다는 원칙과 정반대다.
내가 그 과정을 거치지 않는 건 파사드가 뭔지 몰라서도, 써 본 적이 없어서도 아니다. 내게 문제가 생기면, 난 타입 계층을 노려보며 코드가 타입 체크되도록 연결 고리를 추가한다. 그 연결 고리들은 대부분 파사드, 어댑터, 전략 등으로 귀결되곤 하지만, 문제를 패턴에 대응시켜 딱 맞는 걸 찾거나, 심지어 패턴을 적용할 생각조차 거의 하지 않는다 — 우연히 그렇게 될 뿐이다. 그러니 패턴 이름과 구현 사이의 정신적 연결은 좀처럼 강해지지 않는다.
그래서 누군가 패턴 이름을 입에 올리면, 난 수학 숙제를 하며 단계별 전개를 일일이 공들여 적는 10살짜리를 떠올린다. 학습 중에는 분명 도움이 되고, 그 밑바탕 개념들도 알면 유익하다. 하지만 제발 그런 자질구레한 걸로 내 시간을 낭비하진 말자. 전개 다섯 줄 한 번에 해도 난 따라간다. 그걸 데코레이터라고 부르지 말고 그냥 뉴타입을 만들어줘, 제발. 네 숙제를 검사하고 있는 기분은 느끼고 싶지 않다.
패턴에 이름을 붙이면 복잡한 개념을 간명하게 소통할 수 있다는 주장도 있다. 좋아, 해보자. 당신이 “팩토리”라고 하면, 나는 function newFoo
라고 쓴다. 당신이 “어댑터”라고 하면, 나는 interface FooWrapper
라고 쓴다. 당신이 “프로토타입”이라고 하면, 나는 function clone
이라고 쓴다.
패턴 용어와 실제 코드 사이에는 큰 불일치가 있다. 내 뇌는 꽤 작아서 함수나 클로저 같은 개념은 담을 수 있지만, 같은 걸 뜻하는 다른 말들 — “커맨드”, “액션” — 까지 집어넣으라면, 차라리 더 유용한 걸 담는 데 쓰고 싶다.
소통에서 최악은, 더 잘 알려진 동의어가 있는데도 뜬금한 단어를 쓰는 것이다. 여러분이 모나드를 싫어하는 이유도 같다. (그게 이중잣대라는 말은 아니다. 난 모나드를 좋아하지만, 펑터가 뭐였는지는 늘 헷갈린다.)
가끔은, 언어가 부르는 “프로토타입”과 프로토타입 패턴이 별 접점이 없는 웃긴 상황도 생긴다. 자바스크립트 너 말이야. Class.prototype
과 프로토타입 패턴은, 둘을 열심히 곱씹어 같은 거라고 우겨도, 실제로는 거의 겹치지 않는다.
이름이 생태계 표준과 일치한다면, 당연히 그걸 쓰자. 예컨대 트리 구조를 순회하는 트레이트라면 Visitor
라고 불러도 좋다. 21세기의 주류는 일급 함수가 널리 쓰이니 Factory
같은 트레이트가 필요할 일은 없어야겠지만, 무슨 이유에서건 필요하다면 그렇게 불러도 좋다. 다만 그것들을 _패턴_으로 생각하지 말고, 관용적 이름으로 생각하자.
전체적으로, 단순함에는 가치가 있다. “트레이트에 fn new
”가 더 길긴 하지만, 번역 과정을 거치지 않아도 된다면 “추상 팩토리”보다 더 쉽게 해석될 수 있다.
당신이 어떤 표현을 선호하든, 제발 의도를 직접 말해 달라. 내가 알고 싶은 건 당신이 푸는 문제다. 예를 들어 “X를 Y로부터 분리(decoupling)한다”. 그걸 푸는 데 선택한 패턴이 뭔지는 보통 자명하니 굳이 말하지 않아도 된다.
그리고 이건 전적으로 언어 의존적이기도 하다. 이런 식으로 패턴이 공통된 어휘를 형성하진 못한다. 목표는 다르다.
예를 들어, 함수형 언어로 바꾸면 지연 초기화는 구현만 달라지는 게 아니라 — 언어에 가변성이 없다면 개념 자체가 성립하지 않는다. “초기화”라는 개념이 없으니까. 지연 _계산_은 있을 수 있겠지만, 그건 같은 게 아니다.
생태계가 전역 변수에 익숙하다면 싱글턴은 전혀 필요 없다. 상속에 기반한 패턴은 상속보다 합성을 선호하는 언어에서는 쓸 수 없다. OOP 패턴은 Rust 같은 비 OOP 언어에서는 우스꽝스러워 보이고, 브랜디드 라이프타임 같은 많은 Rust 패턴은 다른 언어에서는 상상도 못 할 개념을 가리킨다. OOP 언어끼리도 차이가 너무 커서, 델리게이트 같은 패턴은 언어 간 접근법을 잇기 위해서만 존재하기도 한다.
추상 팩토리 위키피디아 페이지는 이렇게 시작한다:
소프트웨어 공학에서 추상 팩토리 패턴은, 공통된 주제를 가진 개별 팩토리들을 구체 클래스를 지정하지 않고 캡슐화함으로써, 관련 객체들의 “패밀리”를 그들의 구체 클래스를 강제하지 않고 생성할 방법을 제공하는 디자인 패턴이다. 이 패턴에 따르면, 클라이언트 소프트웨어 컴포넌트는 추상 팩토리의 구체 구현을 생성하고, 그 팩토리의 일반화된 인터페이스를 사용해 그 패밀리를 이루는 구체 객체들을 생성한다.
이 문단을 이해하려면, 문장 조각 하나하나에 진짜 집중해야 한다. 마지막으로 이런 기분을 느낀 건 법률문서를 읽을 때였다. 추상 팩토리를 맨날 쓰는 나도 이 문장을 해석하려 하면 머리가 아프다. 웃을 일이 아니다.
fn new() -> Foo
.fn new() -> Box<dyn FooTrait>
.fn new_foo() -> Box<dyn FooTrait>
.self
를 받는지 아닌지 알려주지 않는다. 아마 제네릭이 보편화되기 전, 추상 타입을 구현하는 유일한 방법이 가상 메서드였을 때 쓰인 말일지도.fn new_foo() -> impl FooTrait
이거나, 혹은 type Foo: FooTrait; fn new_foo() -> Self::Foo;
같은 모양을 말하는 걸 수도.이제 시그니처가 보이니, 너무나 자명하다는 걸 알겠다. 그리고 그래서 쓸모가 없다. 왜냐면 단순한 반복적 설계를 해도 똑같은 결론에 도달했을 테니까.
그런데 왜 기사 하나에 텍스트가 10페이지나 되고, UML 다이어그램에, 빽빽한 개요 문단, 장황한 구현 예제가 들어가 있나? 패턴에 관한 글이 오버엔지니어링돼 있다는 게 아이러니하지 않은가? 정말 더 나은 글쓰기 방법이 정말로 없었단 말인가? 하찮은 걸 추상적으로 곱씹을 거라면, 차라리 범주론을 공부하겠다.
당신 주변에도 목적이나 이유는 이해하지 못한 채 수학 “규칙”을 외워버린 사람들이 꽤 있을 것이다. 그러면 규칙의 중요성을 과대평가(카고 컬트 같은 행태)하거나, 과소평가(예: 단위가 어떻게 작동하는지 이해하지 못함; 여러분 중 일부는 같은 함정에 빠질 수도 있다)하게 된다. 그들은 아마도 훌륭한 수학자라면 큰 수를 암산한다고 믿을 것이다. 당신 은 PEMDAS 같은 자질구레한 건 집중할 가치가 없다는 걸 알지 몰라도, 초보자는 그렇지 않다.
좋은 아키텍처는 프로그래밍 언어가 제공하는 도구들을 조합해 요구사항(유지보수성, 확장성 포함!)을 만족하는 필요 최소한을 하는 것으로 얻어진다. 결과가 기존 패턴과 일치할 수도 있지만, 처음부터 패턴 목록에서 고를 필요는 없다.
초보자에게 패턴을 가르치면, 그들은 그것을 종교처럼 따르기 시작하고, 오버엔지니어링된 자바 스파게티가 나온다. 그럴 수 있다. 초보자는 원래 그렇다. 하지만 균형은 더 나아질 수 있다. 패턴을, SOLID 같은 밑바탕 원칙의 현실 적용 예시 로만 가르치고, 따라야 할 “패턴”으로는 부르지 않는 식으로 말이다.
그리고 많은 패턴은 맨땅에서도 쉽게 고안할 수 있다. 예컨대 HTML을 다루는 자바스크립트 라이브러리를 작성한다고 해 보자. 여기저기서 document.createElement
를 쓴다. 그러다 이 코드를 서버 사이드에서도 동작하게 하고 싶어진다. 브라우저의 document
를 쓸지, 셈(shim) 라이브러리의 document
를 쓸지 선택을 코드에 박아두고 싶지 않다. 대신 그 선택을 사용자에게 맡기고 싶다(내가 아는 것보다 더 나은 폴리필을 그들이 알고 있을 수도 있으니까! 누가 알겠나). 그래서 document
를 인자로 받기로 한다. 어차피 이미 거기서 메서드를 호출하고 있으니, 싸게 먹히는 수정이다. 이제 document
는 추상 팩토리다.
물론 좋은 구현으로 수렴하는 데 시간이 걸릴 수 있지만, 적어도 그런 접근을 고려해 보는 건 아마 좋은 생각일 것이다.
패턴은 역사적으로는 분명 의미가 있었다. 클로저, 인터페이스, 일급 함수가 주류 언어에 항상 있었던 건 아니니까. 그때는 많은 패턴이 비자명했고, 그래서 등장하는 게 놀랍지 않다. 하지만 좋은 아키텍처를 키우는 데 너무 서툴러 농담거리로 전락한 그 자바조차, 그런 기능들을 최소 10년은 갖고 있었다. 그러니 이제 “커맨드”와 “스트래티지”는 폐기해도 되지 않겠나, 제발?
널리 쓰이는 패턴을 아는 건 확실히 유용하지만, 실제 코드 경험과 약간의 민첩함만 있으면, 기본 개념만 알고 있다는 전제하에, 이론으로 패턴 공부에 얼마나 시간을 쏟았는지와 무관하게 금방 따라갈 수 있다. 패턴은 초보자를 위한 일시적 기억 보조 정도로 좋지, 필수 용어는 아니다. 사용 사례 한두 개만 보여주고는, 제발 그 패턴 이름은 다시 꺼내지 마라.