Skip to content

단순함을 향한 여정: 패턴, 어떻게 받아들여야할까?

나도 모르겠다

Makonea·2026년 3월 4일·16분

"디자인 방법론"이라는 아이디어 주변으로 하나의 거대한 학문 분야가 성장해 왔습니다.
그리고 저는 이러한 소위 디자인 방법론의 주요 주창자 중 한 명으로 칭송받아 왔습니다.
저는 이러한 일이 일어난 것에 대해 매우 유감스럽게 생각하며, 디자인 방법론이라는 주제를 연구 대상으로 삼는 것 자체를 거부한다고 공개적으로 밝히고 싶습니다.
왜냐하면 저는 디자인하는 행위의 실천과 디자인 연구를 분리하는 것은 터무니없다고 생각하기 때문입니다.
사실, 디자인 실천 없이 디자인 방법론만 연구하는 사람들은 거의 항상 좌절한 디자이너들입니다.
그들은 내면에 생명력이 없고, 무언가를 만들어내고자 하는 욕구를 잃었거나, 혹은 애초에 가져본 적이 없는 사람들입니다. (알렉산더, 1971)

들어가기에 앞서서

사람들은 흔히, 디자인 패턴을 만능 해결책이라고 말한다.
OOP가 만들어지고, GOF(4인방)의 디자인 패턴이라는 책이 나와 일종의 복음서로

나온 지금

많은 개발자는 패턴 책을 펼치며 구조 다이어그램만 보고 코드를 작성하다 보니, 유연성 없는 경직된 구현이 나오건 한다.

우리는 디자인 방법론과 패턴을 배우는데 있어서 '도식적인 이해'와 '실질적 경험'사이의 괴리사이에서 항상 흔들린다.

위 글의 알렉산더가 말했듯, 디자인의 실체에서 디자인 방법을 분해한다는 것은 결국 실체 없는 허상만 이야기하게 되는 것이다.

사실 패턴을 이야기하기 전에 Idiom등등 여러가지 이야기를 할 수 있지만 이 이야기는 나중에 이야기하고

이 글에서는 패턴에 대해서 논해보자고 한다.

(앨런케이의 03년도 이메일 이야기)


우리가 지금 이야기하는 패턴이 나오게 된 배경?

우리가 지금 이야기하는 패턴이 나오게 된 배경?

흔히 우리가 이야기하는 객체지향 프로그래밍(OOP)의 기원은 Simula에서 시작했지만, 일반적으로 첫 주창자라고 이야기되는 사람은 앨런 케이로써, 앨런 케이가 1967년 초에 사용하였다.

하지만 앨런 케이는 이후 03년 이메일에서 자신이 말한 객체지향 프로그래밍은 메시징, 캡슐화, 동적 바인딩이라고 말한다.

쉽게 말하면, 케이가 생각한 객체는 작은 컴퓨터 같은 것이다. 각 객체가 독립적으로 존재하고, 서로 메시지를 주고받으며 협력하는 구조다. 클래스 계층을 설계하는 것보다 "객체 사이에 어떤 메시지가 오가는가"가 핵심이었다.

하지만 이렇게 이야기하면 우리가 생각하는 그 OOP 개념이 아니다.

그럼 우리가 이야기하는 개념은 어디서 나온 것일까?

(실제 우리가 인식하고 있는 OOP 개념)

현재 우리가 쓰는 OOP는 그레디 부치(Booch)가 주창한 개념에 살을 붙인 개념들이다.

1. OOP는 객체 지향 프로그래밍은 알고리즘이 아닌, 객체를 논리적 구성 요소로 사용함

2. 각 객체는 어떤 클래스의 인스턴스

3. 클래스는 상속관계를 통해서 서로 관련되어 있다

어쨌건 보통 3요소를 캡슐화, 상속, 다형성이라 이야기한다.

이것이 우리가 흔히 이야기하는 객체지향이라는 이야기의 시작점이다.

그리고 이 객체지향 프로그래밍은 금방 프로그래밍의 대세가 생겼고, 여기서 이제 하나의 거대한 프로그래밍 고전이 나온다.

그것이 바로

GOF(Gang Of Four)

GOF

에릭 감마(Erich Gamma), 리처드 헬름(Richard Helm), 랄프 존슨(Ralph Johnson), 존 블리시데스(John Vlissides) 가 쓴 Design Patterns는 현대적인 프로그래밍의 불멸의 고전.

GOF에서는 23가지 디자인 패턴의 보여주고, 3가지 유형으로 나눈다.

3가지는

생성 패턴(Creational Pattern)

구조 패턴(Structural Pattern)

행동 패턴(Behavioral Pattern)

생성 패턴

-객체를 생성하는데 관련된 패턴

-객체가 생성되는 과정의 유연성을 높이며 손쉬운 코드 유지.

예시:싱글톤, 추상팩토리,빌더,팩토리 메서드, 프로토타입

구조 패턴

-프로그램 구조에 관련된 패턴

-프로그램 내의 자료구조 또는 인터페이스 구조등 프로그램 구조를 설계하는데 활용 가능한 패턴

-클래스나 객체를 조합해 더 큰 구조를 만드는데 사용한다

예시:어댑터,브릿지,컴포짓,데코레이터,파사드,플라이웨이트,프록시

행동 패턴

-반복적으로 사용되는 객체들의 상호작용을 패턴화

-객체 사이에 알고리즘이나 책임분배에 관련

-결합도를 최소화(DeCoupling)

예시: 커맨드,인터프리터,이터레이터,미디에이터,메멘토,옵저버,스테이트,스트레티지,템플릿 메서드, 비지터

이 예시들은 폭발적인 인기를 끌었다.

그리고 이것은 OOP에서 여러 문제의 해결 방법을 보여주는 일종의 템플릿이 된다.

하지만 이 예시들이 폭발적인 인기를 끌면서 이러한 패턴 남용이 문제가 되고 있고,

이것이 '선배 프로그래머의 경험적 산물'이 아닌, 하나의 '공리'라고 받아들이는 경우가 많이 늘었다.

그리고 이 글은 프로그래밍 패턴은 '공리'가 아닌,

'선배 프로그래머가 만들어 낸 범용성 높은 도구'라는 관점에 힘을 보태는 글이다.

패턴은 알면 대응할 수 있는 가짓수가 늘어지는 것은 맞지만,

중요한 것은 주어진 상황이 맞닥뜨린 문제를 어떻게 해결하느냐이지.

패턴 카탈로그를 달달 외우는 것이 아니다.

(패턴을 활용한 리팩터링)

패턴을 이용해서 리팩터링을 시도하는 방법을 알려주는 조슈아의 명저에서도 다음과 같은 말이 나온다.

"패턴의 구조 다이어그램은 단지 예시일 뿐, 명세서가 아니라는 점을 아무리 강조해도 지나치지 않습니다. 그것은 우리가 가장 자주 보는 구현 방식을 묘사합니다.
따라서 구조 다이어그램은 여러분 자신의 구현 방식과 많은 공통점을 가질 가능성이 높지만, 차이점은 불가피하며 사실상 바람직합니다.
최소한 여러분은 참여자들의 이름을 여러분의 도메인에 적합하게 변경할 것입니다.
구현상의 장단점을 다양하게 조정하면,여러분의 구현 방식은 구조 다이어그램과 상당히 달라 보이기 시작할 수도 있습니다."
- 존 블리시데스

GOF의 저자 중 한명인 존 블리시데스가 한 말이다.

이 말의 요점은 무엇일까?

즉 우리가 쓰는 하나의 패턴도 여러가지 방법으로 구현할 수 있다는 것이다.

우선 간단하게 예시를 들어보자

// 전략 인터페이스
interface IDiscountStrategy
{
    double ApplyDiscount(double price);
}
 
// 구체적으로 구현한 전략 클래스1
class FixedDiscountStrategy : IDiscountStrategy
{
    private double discountAmount;
    public FixedDiscountStrategy(double amount)
    {
        discountAmount = amount;
    }
    public double ApplyDiscount(double price)
    {
        return price - discountAmount;
    }
}
 
// 구체적으로 구현한 전략 클래스2
class PercentageDiscountStrategy : IDiscountStrategy
{
    private double discountRate;
    public PercentageDiscountStrategy(double rate)
    {
        discountRate = rate;
    }
    public double ApplyDiscount(double price)
    {
        return price * (1 - discountRate);
    }
}
 
// 컨텍스트 클래스
class ShoppingCart
{
    private IDiscountStrategy discountStrategy;
 
    public ShoppingCart(IDiscountStrategy strategy)
    {
        discountStrategy = strategy;
    }
 
    public double CalculateTotalPrice(double originalPrice)
    {
        return discountStrategy.ApplyDiscount(originalPrice);
    }
}
 
//사용예시
ShoppingCart cart1 = new ShoppingCart(new FixedDiscountStrategy(1000));
ShoppingCart cart2 = new ShoppingCart(new PercentageDiscountStrategy(0.1));
 
Console.WriteLine($"Fixed Discount: {cart1.CalculateTotalPrice(10000)}"); //Fixed Discount:9000
Console.WriteLine($"Percentage Discount: {cart2.CalculateTotalPrice(10000)}"); //Percentage Discount: 9000

흔히 쓰이는 전략패턴이다.

여기서 람다식을 적용을 해보자

그럼 인터페이스와 구체적인 정의를 실현하는 클래스 정의는 불필요하나

// 할인 전략 델리게이트(함수 타입 정의)
delegate double DiscountStrategy(double price);
 
// 컨텍스트 클래스
class ShoppingCart
{
    private DiscountStrategy discountStrategy;
 
    public ShoppingCart(DiscountStrategy strategy)
    {
        discountStrategy = strategy;
    }
 
    public double CalculateTotalPrice(double originalPrice)
    {
        return discountStrategy(originalPrice);
    }
}
 
// 사용 예시 (람다식으로 전략 직접 정의)
ShoppingCart cart1 = new ShoppingCart(price => price - 1000); // 고정 할인 전략(람다식)
ShoppingCart cart2 = new ShoppingCart(price => price * 0.9);   // 퍼센트 할인 전략(람다식)
 
Console.WriteLine($"Fixed Discount (Lambda): {cart1.CalculateTotalPrice(10000)}"); //Fixed Discount:9000
Console.WriteLine($"Percentage Discount (Lambda): {cart2.CalculateTotalPrice(10000)}"); //Percentage Discount: 9000

간단하게 코드량이 감소 하고, 다양한 전략을 람다식으로 쉽게 정의할 수 있고, 오히려 필요에 따라 동적으로 전략 변경이 더욱 유연해졌다!

이번에는 커맨드 패턴을 람다식/메서드 참조 패턴을 써보자

// 커맨드 인터페이스
interface ICommand
{
    void Execute();
}
 
// 구체 커맨드 클래스1
class LightOnCommand : ICommand
{
    private Light light;
    public LightOnCommand(Light light)
    {
        this.light = light;
    }
    public void Execute()
    {
        light.TurnOn();
    }
}
 
// 구체 커맨드 클래스2
class LightOffCommand : ICommand
{
    private Light light;
    public LightOffCommand(Light light)
    {
        this.light = light;
    }
    public void Execute()
    {
        light.TurnOff();
    }
}
 
// 수신자 클래스
class Light
{
    public void TurnOn() { Console.WriteLine("Light On"); }
    public void TurnOff() { Console.WriteLine("Light Off"); }
}
 
// 호출자 클래스
class RemoteControl
{
    private ICommand onCommand;
    private ICommand offCommand;
 
    public RemoteControl(ICommand on, ICommand off)
    {
        this.onCommand = on;
        this.offCommand = off;
    }
 
    public void PressOnButton() { onCommand.Execute(); }
    public void PressOffButton() { offCommand.Execute(); }
}
 
// 사용예시
Light livingRoomLight = new Light();
RemoteControl remote = new RemoteControl(new LightOnCommand(livingRoomLight), new LightOffCommand(livingRoomLight));
 
remote.PressOnButton();  // Light On
remote.PressOffButton(); // Light Off

이것을 다시 람다식으로 변형해보자!

// 커맨드 델리 게이트(void 반환, 매개 변수 없음)
delegate void Command();
 
// 수신자 클래스 (동일)
class Light
{
    public void TurnOn() { Console.WriteLine("Light On"); }
    public void TurnOff() { Console.WriteLine("Light Off"); }
}
 
// 호출자 클래스
class RemoteControl
{
    private Command onCommand;
    private Command offCommand;
 
    public RemoteControl(Command on, Command off)
    {
        this.onCommand = on;
        this.offCommand = off;
    }
 
    public void PressOnButton() { onCommand(); }
    public void PressOffButton() { offCommand(); }
}
 
// 사용 예시(람다식, 메서드 참조로 커맨드 직접 정의)
Light livingRoomLight = new Light();
RemoteControl remote = new RemoteControl(
    livingRoomLight.TurnOn,  // 메서드 참조 (LightOnCommand 대체)
    () => livingRoomLight.TurnOff() // Lambda식 (LightOffCommand 대체)
);
 
remote.PressOnButton();  // Light On
remote.PressOffButton(); // Light Off

다시 줄 수가 줄어들고, 다양한 커맨드를 메서드 참조로 쉽게 정의하고,

필요에 따라서 동적으로 커맨드를 변경 할 수 있게되었다!

자 이제 팩토리 패턴도 구현해보자

// 제품 인터페이스
interface IProduct
{
    void DoSomething();
}
 
// 구체 제품 클래스 1
class ConcreteProductA : IProduct
{
    public void DoSomething() { Console.WriteLine("Product A"); }
}
 
// 구체 제품 클래스 2
class ConcreteProductB : IProduct
{
    public void DoSomething() { Console.WriteLine("Product B"); }
}
 
// 팩토리 인터페이스
interface IFactory
{
    IProduct CreateProduct();
}
 
// 구체 팩토리 클래스 1
class ConcreteFactoryA : IFactory
{
    public IProduct CreateProduct() { return new ConcreteProductA(); }
}
 
//  구체 팩토리 클래스 2
class ConcreteFactoryB : IFactory
{
    public IProduct CreateProduct() { return new ConcreteProductB(); }
}
 
// 클라이언트 코드
class Client
{
    public void UseProduct(IFactory factory)
    {
        IProduct product = factory.CreateProduct();
        product.DoSomething();
    }
}
 
// 사용 예시
Client client = new Client();
client.UseProduct(new ConcreteFactoryA()); // Product A
client.UseProduct(new ConcreteFactoryB()); // Product B

이것을 람다식과 딕셔너리를 이용해서 리팩터해보자!

// 제품 인터페이스
interface IProduct
{
    void DoSomething();
}
 
// 구체 제품 클래스
class ConcreteProductA : IProduct
{
    public void DoSomething() { Console.WriteLine("Product A"); }
}
 
class ConcreteProductB : IProduct
{
    public void DoSomething() { Console.WriteLine("Product B"); }
}
 
// 팩토리 델리게이트 (제품 인터페이스 반환, 매개변수 없음)
delegate IProduct ProductFactory();
 
// 팩토리 딕셔너리
class ProductFactoryDictionary
{
    private Dictionary<string, ProductFactory> factories = new Dictionary<string, ProductFactory>();
 
    public void RegisterFactory(string productName, ProductFactory factory)
    {
        factories.Add(productName, factory);
    }
 
    public IProduct CreateProduct(string productName)
    {
        if (factories.ContainsKey(productName))
        {
            return factories[productName]();
        }
        return null; // 예외처리
    }
}
 
// 클라이언트 코드
class Client
{
    public void UseProduct(ProductFactoryDictionary factoryDict, string productName)
    {
        IProduct product = factoryDict.CreateProduct(productName);
        if (product != null)
        {
            product.DoSomething();
        }
    }
}
 
// 사용 예시 (람다식으로 팩토리 등록)
ProductFactoryDictionary factoryDict = new ProductFactoryDictionary();
factoryDict.RegisterFactory("A", () => new ConcreteProductA()); // 람다식으로 팩토리 등록
factoryDict.RegisterFactory("B", () => new ConcreteProductB()); // 람다식으로 팩토리 등록
 
Client client = new Client();
client.UseProduct(factoryDict, "A"); // Product A
client.UseProduct(factoryDict, "B"); // Product B

딕셔너리를 사용해서 팩토리를 런타임에 동적으로 등록하고 관리가 용이해져서 설정 파일 또는 외부 데이터 소스에서 팩토리 정보를 읽기 쉬워서, 더 유연해졌다고 할 수 있다.

이 글을 보다보면 사람들은 말할 것이다

그렇다면 이렇게

'람다식을 쓰는 방법이 정답이라고 주장하고 싶은 거야?'

아니다.

람다식을 쓴다는 것은 기본적으로 일반적 OOP에서 FP를 임시방편으로 문법적 설탕을 더해주는 것이니

혹자는 FP를 쓰라고 말할수도 있지만,

OOP말고 문제 해결에 다른 '지향'을 쓸 수도 있다는 것을 말하는거지만

이것은 내가 말하는 본질이 아니다.

람다식을 쓴다는 것은 또 다른 문제를 낳는다.

'팀의 코드 일관성을 손상하기 쉽다'라는 것이다.

코드를 사용할때 기본적으로 우리는 소프트웨어의 규칙에 따른다.

이것은 코딩 컨벤션과 코드의 가이드를 쓰는 것이고, 기본적으로 팀 코드의 원칙은

'남이 봤을때 한 사람이 쓴 것처럼' 보이는 것이 일반적으로 올바르게 작성된 코드라고 한다.(프로그램을 하나의 소설 책이라고 가정한다면 문체가 바뀌는 프로그램보단 한 명이 쓴것처럼 가지런히 정리된게 더 낫지 않겠는가?)

하지만 람다식을 저렇게 써버리기 시작하면 이제 코드의 일관성을 유지하기위해 나머지 부분도 람다식을 써야하는 문제점이 생긴다.

그리고 FP가 항상 정답은 아닌 것이, 기본적으로 메모리 사용량을 추가로해서 불필요한 오버헤드 또한 엄연히 존재한다.(물론 비동기 프로그래밍에서 FP를 사용하는 것은 데이터 레이스를 방지하는 좋은 방법중 하나이지만 지금 말하고자하는 것은 그것이 아니다)

실제로 OOP에서도 일부 패턴이 더 나은 패턴으로 진화하는 경우가 있다.

가령 싱글톤 패턴으로 쓰였던 것 중 일부는 서비스 로케이터 패턴이라는 상대적으로 새로운 패턴으로 대체된다거나

(하지만 싱글톤, 서비스로케이터 둘 다 특정상황을 제외하면 안티 패턴으로 간주되곤해서 DI 주입을 하는 방식도 많이 활용된다)

빌더 패턴도 Fluent Builder라는 변형이 나온다거나.

이는 패턴을 명시적 암기만 해서는 나오지 않았을 변형들이다.

(그래디 부치, 내가 밥벌어먹고 살게해주신 분)

그렇다면 왜 이런 변형과 대체가 가능한 것일까?

앞서 앨런 케이와 그레디 부치의 OOP가 서로 다른 것이었다는 이야기를 했다. GoF의 디자인 패턴 23가지를 살펴보면, 상당수가 클래스 상속 구조에서 발생하는 경직성을 우회하기 위한 해법이다. Strategy 패턴은 상속 대신 구성(Composition)으로 행위를 교체하고, Decorator 패턴은 상속 없이 기능을 확장한다. 즉 GoF 패턴은 "부치식 OOP가 만들어낸 문제를 부치식 OOP 안에서 푸는 방법"이라고 할 수 있다.

다시 말하면, 디자인 패턴은 객체지향이라는 특정 패러다임 안에서 반복적으로 부딪히는 구조적 문제에 대한 경험적 해법이지, 프로그래밍 전체를 관통하는 보편적 진리가 아니다. 위에서 람다식으로 패턴을 변형할 수 있었던 것도, FP라는 다른 패러다임의 도구를 빌려왔기 때문이다. 그리고 같은 OOP 안에서도 싱글톤이 DI로, 빌더가 Fluent Builder로 진화한 것은 원래의 패턴이 불완전한 해법이었기 때문이 아니라, 해결해야 할 맥락이 달라졌기 때문이다.

결국 내가 하고자 하는 말은

'팀의 컨벤션에 맞추어 항상 더 나은 정답을 갈구해야한다'라는 것이다.

패턴을 배우는 길에서 패턴에 심취하게 되는 것은 아마도 피할 수 없을 것입니다.
사실, 우리 대부분은 실수를 통해 배우니까요. 저 또한 여러 번 패턴에 심취했던 경험이 있습니다.
패턴의 진정한 즐거움은 그것들을 현명하게 사용하는 데서 비롯됩니다.
리팩토링은 중복 제거, 코드 단순화, 그리고 코드의 의도를 명확히 전달하는 데 집중하도록 도와줌으로써 우리가 그렇게 할 수 있도록 돕습니다.
패턴이 리팩토링을 통해 시스템 속으로 자연스럽게 녹아들 때, 패턴을 과도하게 사용하는 과잉 설계의 위험은 줄어듭니다.
리팩토링 실력이 향상될수록, 패턴의 진정한 즐거움을 발견할 가능성은 더욱 커질 것입니다

마치며...

패턴의 진정한 즐거움은 그것들을 현명하게 사용하는데서 시작된다.

다들 패턴을 쓰기 전 다시 한번 더 생각하면서 써보는게 어떨까?

그럼 자신만의 새로운 방식을 찾을 수 있을지도 모른다.

나만의 지론이지만,

배움과 실천은 인생의 해상도를 높인다고 생각한다.

더 넓은 시각은 의심에서 시작되고, 의심은 새로운 배움으로 나아간다.

그렇게 새로운 배움이 추가될 수록 우리는 점점 더 주변의 사물들을 알아가고,

그 사물이 주는 의미들이 더 깊게 들어오는 것이다.

우리의 앞은 보이지 않는 불확실한 미래가 있지만, 뒤돌아봤을때 봤을 풍경이 높은 해상도를 가지고 아름답다면 그것만으로도 충분히 재밌지 않을까?

그럼 이 글을 마친다.