[CS] 디자인 패턴 - 생성 패턴

팔랑이·2024년 12월 23일

CS

목록 보기
4/19
post-thumbnail

⛳️ 인프런 - cs 지식의 정석 강의를 듣고 학습한 내용입니다.


디자인 패턴

JAVA, JS 코드 위주로 설명을 해 주셔서 이해하는데 시간이 좀 걸리지만
그래서 따로 적어놨다 비교표...
이참에 공부해보는거지 머
코드 위주로 작성하며 이해하고, Swift랑도 비교해보며 작성

우선 생성 패턴들을 알아보자.

생성 패턴은 객체 생성의 다양한 방식을 제공하여, 생성 과정과 생성된 객체의 구조를 분리하는 패턴들이다.

싱글톤(singleton) 패턴

생성 패턴중 하나로 하나의 클래스에 하나의 인스턴스만 가지며, 어디서든 해당 인스턴스에 접근할 수 있는 패턴.
인스턴스 생성을 한번만 하기 때문에, 인스턴스 생성에 드는 코스트가 줄어든다.
하지만 의존성이 높아지고, 단위 테스트를 할 때 불편하다는 단점이 있음.

아래 예시 코드들을 보면, 싱글턴으로 선언한 객체로 여러 인스턴스를 만들어도 같은 객체임을 확인할 수 있다.

JAVA 싱글턴 예시 코드

class Singleton {
    private static class singleInstanceHolder {
        // 중첩 클래스 내부에 싱글턴 인스턴스 생성
        // final 키워드를 통해 read-only 즉, 다시 값이 할당되지 않도록 처리
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        // 중첩 클래스의 인스턴스를 반환 (최초 호출 시 생성 및 리턴됨)
        return singleInstanceHolder.INSTANCE;
    }
}

public class HelloWorld {
    public static void main(String[] args) {
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();

        // 두 인스턴스의 해시코드 출력
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());

        // 같은 객체임을 확인
        if (a == b) {
            System.out.println(true);
        }
    }
}

Java에서는 static으로 선언된 변수는 클래스 로딩과 동시에 초기화되기 때문에 lazy initialize를 위해 따로 처리를 해 줘야 한다.
위 예시에서는 중첩 클래스(Holder 패턴)를 사용해 싱글턴 인스턴스가 명시적으로 호출되기 전까지는 메모리에 올라가지 않게 처리했다.

Swift 싱글턴 예시 코드

Java와 다르게 Swift에서는 static으로 정적 변수를 선언할 경우, 별다른 처리가 없어도 lazy initialize가 기본적으로 적용된다.

class Singleton {
    private static let instance = Singleton()

    // private initializer를 통해 외부에서 직접 인스턴스를 생성하지 못하게 함
    private init() {}

    // 정적 메서드를 통해 인스턴스에 접근
    static func getInstance() -> Singleton {
        return instance
    }
}

JS 싱글턴 예시 코드

class Singleton {
    constructor() {
        if (!Singleton.instance) {
            Singleton.instance = this; // Singleton.instance가 없으면 현재 인스턴스를 저장
        }
        return Singleton.instance; // 항상 같은 인스턴스를 반환
    }

    getInstance() {
        return this; // 현재 인스턴스를 반환하는 메서드
    }
}

const a = new Singleton(); // 첫 번째 인스턴스 생성
const b = new Singleton(); // 두 번째 생성 시 기존 인스턴스를 반환

console.log(a === b); // true (a와 b는 같은 인스턴스를 참조)

자바스크립트는 아예 처음이라..어렵다
우선 저 instance 변수가 선언되지 않았는데 저기서 저렇게 해도 되는건가? 했는데
동적 언어라서 정적 속성을 런타임중에 처음 접근하거나 값을 할당하며 선언할 수 있다고 한다.

동작 방식

  • Singleton.instance가 처음에는 정의되지 않음
  • if (!Singleton.instance) 조건에서 undefined로 평가되므로, 조건이 참으로 판단됨
  • Singleton.instance = this 문장을 실행하면서 클래스에 instance라는 속성이 동적으로 추가된다
  • Singleton.instance가 이후에 계속 사용 가능, 이후 Singleton.instance가 이미 값(첫 번째 생성된 인스턴스)을 가지고 있으므로 다시 초기화되지 않는다.

팩토리 패턴

싱글턴과 같이 생성 패턴 중 하나로, 일반적으로 상속 관계에 있는 두 클래스 간의 관계를 통해 구현된다. 상위 클래스(인터페이스/추상클래스)가 중요한 뼈대를 결정하면 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴.

클라이언트는 구체적인 클래스 이름에 의존하지 않고 상위 클래서의 인터페이스만 사용하게 된다. 따라서 객체 생성 로직의 변경이나 확장이 용이함. 사용 이점 구체적인 예시는 밑에서.

JS 팩토리패턴 예시 코드

class CoffeeFactory {
  static createCoffee(type) {
    // 주어진 타입에 따라 factoryList에서 적절한 팩토리를 선택
    const factory = factoryList[type]
    // 선택한 팩토리의 createCoffee 메서드를 호출하여 커피 객체 생성
    return factory.createCoffee()
  }
}

// 각각 라떼와 에스프레소 커피를 표현하는 단순한 클래스
class Latte {
  constructor() {
    this.name = "latte"
  }
}
class Espresso {
  constructor() {
    this.name = "Espresso"
  }
}

// 각각 라떼와 에스프레소를 생성하는 팩토리 클래스로, 특정 타입의 커피 객체를 반환한다.
class LatteFactory extends CoffeeFactory{
  static createCoffee() {
    return new Latte()
  }
}
class EspressoFactory extends CoffeeFactory{
  static createCoffee() {
  	return new Espresso()
  }
}

const factoryList = { LatteFactory, EspressoFactory }
const main = () => {
  // "LatteFactory" 타입으로 라떼 커피를 주문
  const coffee = CoffeeFactory.createCoffee("LatteFactory")
  console.log(coffee.name) // latte
}
main()

팩토리 패턴 사용 이점

JS 코드를 통해 팩토리 패턴의 사용 이점을 알아보자.

1. 객체 생성 로직을 캡슐화할 수 있다.

  • 클라이언트 코드는 new Latte() 또는 new Espresso()를 사용할 필요 없이 단순히 타입을 전달하여 원하는 객체를 얻을 수 있다.
  • 객체 생성 로직을 변경해도 클라이언트 코드를 수정하지 않아도 된다. 예를 들어,
class Latte {
    constructor() {
        this.name = "latte"
        this.size = "medium"
    }
}
class Espresso {
    constructor() {
        this.name = "Espresso"
        this.size = "small"
    }
}

이렇게 초기화 로직을 추가하거나

class LatteFactory extends CoffeeFactory {
    static createCoffee() {
        const coffee = new Latte()
        coffee.extraShot = false
        return coffee
    }
}

class EspressoFactory extends CoffeeFactory {
    static createCoffee() {
        const coffee = new Espresso()
        coffee.doubleShot = true
        return coffee
    }
}

이렇게 속성을 추가하더라도 클라이언트 코드는 const coffee1 = CoffeeFactory.createCoffee("LatteFactory"), const coffee2 = CoffeeFactory.createCoffee("EspressoFactory") 그대로 수정 없이 사용할 수 있다.

2. 유지보수가 용이하다.

  • 새로운 커피 타입을 추가하고 싶다면, 기존 코드를 수정하지 않고 새로운 팩토리와 클래스만 추가하면 된다.
  • 예를 들어, "CappuccinoFactory"와 "Capuccino" 클래스를 기존 코드의 수정 없이 추가할 수 있는데,
  • 이는 개방-폐쇄 원칙(OCP, Open-Closed Principle)을 따른다.
class Cappuccino {
    constructor() {
        this.name = "Cappuccino"
    }
}

class CappuccinoFactory extends CoffeeFactory {
    static createCoffee() {
        return new Cappuccino()
    }
}

factoryList.CappuccinoFactory = CappuccinoFactory
const coffee = CoffeeFactory.createCoffee("CappuccinoFactory")
console.log(coffee.name) // Cappuccino

Java, Swift 팩토리패턴 예시 코드

개념은 똑같으니 Java, Swift는 코드로만 보고 넘어가기로.

Java

Javascript 코드와 다르게 단일 팩토리 방식으로 구현
지피티한테 물어봤는데

  • Java는 명확한 타입 선언과 클래스를 중심으로 설계가 이루어져서 모든 로직을 단일 팩토리에 집중시키는 경우가 많고,
  • Javascript는 유연한 설계가 가능하므로 객체 생성 로직이 분리된 모듈형 설계가 가능해서 그렇다고 하는데

굳이 그런 이유가 아니라면...댓글 부탁드립니다

enum CoffeeType {
	LATTE,
	ESPRESSO
}

// 커피의 기본적인 속성을 정의한 추상 클래스
abstract class Coffee {
	protected String name;
	public String getName() {
		return name;
	}
}

// 각각 Coffee 클래스를 상속하는 커피 클래스
class Latte extends Coffee {
	public Latte() {
		name = "latte";
	}
}
class Espresso extends Coffee {
	public Espresso() {
		name = "Espresso";
	}
}

// 커피 생성로직을 캡슐화한 팩토리 클래스
class CoffeeFactory {
	public static Coffee createCoffee(CoffeeType type) {
		switch (type) {
			case LATTE:
				return new Latte();
			case ESPRESSO:
				return new Espresso();
			default:
				throw new IllegalArgumentException("Invalid coffee type: " + type);
		}
	}
}

// 메인 클래스
public class Main {
	public static void main(String[] args) {
		Coffee coffee =
CoffeeFactory.createCoffee(CoffeeType.LATTE);
		System.out.println(coffee.getName()); // latte
	}
}

Swift

Swift는 커피별 팩토리 클래스를 만드는 방식으로 작성해봤다.

//기본 커피 클래스, 라떼, 에스프레소 클래스 정의
class Coffee {
	var name: String
    
    init(name: String) {
    	self.name = name
    }
}

class Latte: Coffee {
	init() {
    	super.init(name: "Latte")
    }
}

class Espresso: Coffee {
	init() {
    	super.init(name: "Espresso")
    }
}

// Coffee 클래스를 반환하는 프로토콜 정의
protocol CoffeeFactoryProtocol {
	static func createCoffee() -> Coffee
}

// CoffeeFactoryProtocol 준수하는 팩토리 클래스 정의
class LatteFactory: CoffeeFactoryProtocol {
	static func createCoffee() -> Coffee {
    	return Latte() // Latte 객체 생성
    }
}

class EspressoFactory: CoffeeFactoryProtocol{
	static func createCoffee() -> Coffee {
    	return Espresso() // Espresso 객체 생성
    }
}

// CoffeeFactory 클래스 생성
class CoffeeFactory {
	static func createCoffee(factoryType: CoffeeFactoryProtocol.Type) -> Coffee {
    	return factoryType.createCoffee()
    }
}

// 클라이언트 코드
let latte = CoffeeFactory.createCoffee(LatteFactory.self)
let espresso = CoffeeFactory.createCoffee(EspressoFactory.self)

print(latte.name) // Latte
print(espresso.name) // Espresso
profile
정체되지 않는 성장

0개의 댓글