티스토리 뷰

 

우리는 객체지향 프로그래밍을 기반으로 협업과 오픈소스 라이브러리 개발에서 발생하는 여러 문제를 사전에 미리 해결하기 위해 여러 디자인 패턴들을 사용합니다. 이들의 주요 목적은 코드의 구조를 체계적으로 분리하여 개발, 유지보수, 테스트를 용이하게 하는 것입니다.

 

사실 이러한 사실들을 거의 대부분의 개발자가 인지하고 있을 것입니다, 하지만 해당 글에서는 이러한 패턴들을 사용했을 때 기존 코드와의 차이점, 대비되는 여러 예시들 그리고 여러 장점, 단점들을 자세히 살펴볼 예정입니다.

 

해당 글은 GoF의 생성 패턴(Creational Pattern)을 다룹니다.

 

싱글톤 (Singleton)

어떠한 클래스의 인스턴스를 전역적으로 관리하기 위해서 널리 사용되는 디자인 패턴 중 하나입니다, 이 패턴을 사용하면 클래스의 인스턴스가 프로그램 실행 중에(런타임 중에) 단 하나만 존재하도록 보장하여 메모리 효율성을 높일 수 있습니다. 즉, 매번 새로운 인스턴스를 생성하거나 삭제하는 대신, 이미 생성된 단 하나의 인스턴스를 공유하여 사용하게 됩니다.

 

여기서 여러분들은 흔히 정적 초기화를 생각할 수도 있을 것입니다, 다만 주의할 점은 인스턴스를 사용하지 않더라도 생성되어 메모리 낭비가 발생할 수 있다는 것입니다. (이는 프레임워크나 다른 기타 라이브러리들에서 매우 중요한 사항입니다, 실제 서비스 개발 환경에서도 데이터베이스 연결 객체나 설정 파일 읽기 객체를 싱글톤으로 만드는 경우, 여러 곳에서 동일한 객체를 공유하여 효율성을 높일 수 있습니다.)

 

반면 싱글톤은 처음에 참조할 때 뒤늦게 런타임 환경에서 초기화되기 때문에 이러한 문제가 발생하지 않습니다, 개발자들은 흔히 이를 게으른 초기화(Lazy Initialization)라고 부르기도 합니다.

 

또한 정적 초기화는 개발자가 별도의 식별자를 사용하여 인스턴스를 별도로 생성해야 하기 때문에, 개발자가 실수로 여러 개의 인스턴스를 생성할 수 있는 가능성이 존재합니다. (하나만 생성해야 된다는 조건을 강제하지 않기 때문에 문제가 발생할 수 있다는 뜻입니다.)

 

해당 디자인 패턴을 구현하는 간단한 예시를 보도록 하겠습니다.

 

class AdBinding {
    private static _instance: AdBinding;

    // 싱글톤 클래스의 생성자는 외부에서 호출될 수 없습니다.
    private constructor() {}
    
    static get instance(): AdBinding {
    	return this._instance ?? (this._instance = new AdBinding());
    }

    load(id: string): void {
        // ... 생략
    }
}

// 이러한 방식으로 인스턴스를 참조합니다.
AdBinding.instance.load("AD-15621232-AF#ETDXCFE");

 

해당 코드를 보시면 클래스의 생성자가 private이며 이로 인해 서브 클래스로 확장할 수 없습니다, 해당 코드를 그림으로 나타내면 다음과 같습니다.

 

 

싱글톤 패턴은 특정 상황 또는 대부분의 상황에서 유용할 수 있지만, 모든 경우에 적합한 것은 아닙니다. 코드의 복잡성과 유연성을 고려하여 싱글톤 패턴을 사용할지 말지를 결정하는 것도 중요합니다. (싱글톤 패턴에 대한 대안으로는 의존성 주입(Dependency Injection)을 고려해보는 것이 좋습니다.)

 

팩토리 메서드 (Factory Method)

인스턴스 생성 부분을 추상화하여 어떠한 객체를 생성할지 결정하는 책임을 서브클래스에게 위임하는 패턴입니다. 이는 실제 생성 작업은 서브 클래스에 위임하여 외부에서 메서드를 호출하여 인스턴스를 생성할 수 있도록 하기 위해 널리 사용되는 디자인 패턴 중 하나입니다.

 

즉, 이를 현실 세계에 비유하면 동물은 아기를 낳는다라는 상식만을 정의하고 구체적으로 어떠한 개체를 낳는다는 것은 전적으로 동물의 확장 개념인 인간이 정의한다라는 것으로 비유될 수 있습니다.

 

해당 디자인 패턴을 구현하는 간단한 예시들을 보도록 하겠습니다.

 

abstract class Animal {
    abstract createChild(parent: Animal): Animal;
}

class Cat extends Animal {
    createChild(parent: Cat): Cat {
        return new Cat();
    }
}

class Human extends Animal {
    createChild(parent: Human): Human {
        return new Human();
    }
}
abstract class ScrollState {
    ...
}

abstract class ScrollController {
    abstract createState(initialValue: number): ScrollState;
}

class AndroidScrollState extends ScrollState {
    ...
}

class AndroidScrollController extends ScrollController {
    createState(initialValue: number) {
    	return new AndroidScrollState(initialValue);
    }
}

 

해당 코드들을 보면 클래스가 인스턴스를 생성하는 기능을 추상화한 것을 볼 수 있습니다, 위 첫번째 코드를 그림으로 나타내면 다음과 같습니다.

 

 

추상 팩토리 (Abstract Factory)

팩토리 메서드와 마찬가지로 인스턴스 생성 부분을 추상화하여 외부에서 이를 참조하여 인스턴스를 생성할 수 있게 하기 위해 널리 사용되는 디자인 패턴들 중 하나입니다.

 

팩토리 메서드와의 차이점은 "전용 클래스로 분리하여 캡슐화를 진행하는가"입니다.

 

음... 말만 들어서는 감이 잘 오지 않을 것입니다, 따라서 이를 현실 세계에 비유해서 설명해 보겠습니다. 추상 팩토리는 공장이라는 그 존재 자체를 추상화하는 것이며 팩토리 메서드는 단순히 제품을 생성하는 기능만을 추상화하는 것입니다.

 

더보기
  • 동물은 아기를 낳는다 = 팩토리 메서드
  • 컴퓨터를 만드는 공장 = 추상 팩토리

 

해당 디자인 패턴을 구현하는 간단한 예시들을 보도록 하겠습니다.

 

abstract class ComputerFactory {
    abstract createCPU(): CPU;
    abstract createGPU(): GPU;
    abstract createRAM(): RAM;
    
    // ... 그 외 생략
}

class SuperUltraComputerFactory extends ComputerFactory {
    createCPU(): CPU {
    	return new Ryzen9_7950X3D();
    }
    
    // ... 그 외 생략
}

abstract class CPU {
    constructor(cores: CPUCore[]) { ... }
}

class Ryzen9_7950X { ... }
class Ryzen9_7950X3D { ... }

class Computer {
    constructor(public cpu: CPU, public gpu: GPU, public ram: RAM) { ... }
}

class ComputerProvider {
    constructor(public factorys: ComputerFactory[]) { ... }
    
    createAll(): Computer[] {
    	return this.factorys.map(f => new Computer(f.createCPU(), f.createGPU(), f.createRAM()))
    }
}

 

해당 코드를 간단하게 그림으로 나타내면 다음과 같습니다.

 

 

슈퍼 울트라 컴퓨터 공장(Super-Ultra-Computer-Factory)은 추상적 개념인 "컴퓨터 공장"의 실존적 개념이며 따라서 컴퓨터 부품인 어떠한 CPU와 GPU 그리고 RAM 등을 생산하는지 모두 정의되어 있다는 것을 볼 수 있습니다.

 

빌더 (Builder)

불규칙하고 많은 생성자 매개변수들을 옵션화해야 할 때 사용하는 디자인 패턴입니다, 흔히 많이들 알고 있는 프레임워크인 Android Jetpack Compose의 Modifier 클래스와 그 외 다른 자바 라이브러리들에서 해당 패턴을 많이 사용합니다.

 

// 생성자를 사용하여 매개변수들을 초기화하는 경우
new Modifier(null, null, Colors.Red, 15, null, null, 5);

// 빌더(Builder) 패턴을 사용하는 경우
new Modifier()
    .background(Colors.Red)
    .padding(15)
    .clip(RoundedCornerShape(5));

 

사용 예시를 보면 해당 패턴은 주로 네임드 파라미터 생성자(Constructor with named parameters)를 지원하지 않는 언어에서만 주로 쓰이는 디자인 패턴이라는 것을 대충 알 수 있습니다. (따라서 Flutter와 같은 Dart 기반들은 잘 사용하지 않습니다, 필요 없으니까, 하지만 예외 상황은 항상 있습니다)

 

다음은 해당 디자인 패턴을 구현하는 간단한 예시를 보도록 하겠습니다.

 

class Modifier {
    private _background?: Background | Color;
    private _padding?: number;
    private _clip?: CornerShape;

    background(value: Background | Color): Modifier {
        return this._background = value, this;
    }
    
    padding(value: number): Modifier {
    	return this._padding = value, this;
    }
    
    clip(value: CornerShape): Modifier {
    	return this._clip = value, this;
    }
}

 

참고로 해당 코드에서 구현된 Modifier는 실제 Android Jetpack Compose의 구현되는 동작과 다릅니다, 코틀린(Koltin)에서는 다트(Dart)와 같이 매개변수들을 옵션화시킬 수 있습니다, 하지만 이를 사용하지 않고 빌더 패턴을 사용하는 이유는 순차적으로 증복되는 여러 동작들을 정의할 수 있게 하기 위함입니다.

 

new Modifier()
    .background(Colors.Red)
    .padding(10)
    .background(Colors.Blue)
    .padding(15)

 

해당 코드와 같이 기존 생성자로는 구현하지 못하는 기능을 빌더 패턴으로 간단하게 해결할 수 있다는 것을 알 수 있습니다.

 

프로토타입 (Prototype)

인스턴스의 깊은 복사를 수행하는 메서드를 인터페이스화하는 패턴입니다, 흔히 이를 복제체(Clone) 생성이라고 부릅니다. (자신과 동일한 복제체를 만드는 것을 깊은 복사라고 합니다.)

 

별도의 인터페이스를 통해 깊은 복사를 인터페이스화하면 외부에서 복제될 수 있는 객체만 강제하도록 할 수 있으니 해당 패턴을 사용하는 경우가 많습니다. (즉, 복사가 가능한 객체인지 불가능한 객체인지 명확하게 구분할 수 있다는 의미입니다.)

 

또한 인터페이스는 대부분 코드의 일관성을 유지하기 위한 역할을 합니다, 따라서 이 패턴을 사용하는 경우에는 복제체를 생성할 때의 어느 메서드를 호출할 지 결정하는 과정에서 일관성을 유지할 수 있다는 것을 의미합니다. (createClone... clone... getClone... fetchClone 이런 일관성 없는 식별자 이름으로 코드가 작성되는 것을 방지합니다.)

 

interface Cloneable {
    // Returns the clone that is with the same info.
    createClone(): Cloneable;
}

class Human implements Cloneable {
    constructor(public age: number) {}

    createClone(): Human {
    	return new Human(this.age);
    }
}

 

Happy coding in the another world!

 

 

https://mttankkeo.tistory.com/18