GoF 디자인 패턴 3 - [생성] 팩토리 패턴

김정환·2024년 9월 2일
0

GoF 디자인패턴

목록 보기
3/9

클래스와 객체지향


객체지향 프로그램을 실행하려면 클래스를 선언하는 과정이 필요하다.

클래스 선언 = 객체 생성의 청사진

클래스에 객체에서 처리할 행동을 결정한다.
(행동 : 객체에서 동작 수행해야 하는 처리 로직)

객체지향 (OOP : Object-Oriented Programming)

  • 70년대부터 사용한 개발 방법론으로 C++, Java에서 오래 전부터 도입해서 사용했다.
  • 최근 대규모 SW 설계에 유용한 OOP 개발 방식이 인기를 끌었다.
  • 서비스에는 다양한 사용자 경험이 요구되는 등 각종 사항들이 증가한다.
    • 프로그램의 기능이 복잡해지고 규모도 점점 커진다.
    • 코드에서 처리하는 일들이 다양해진다.
    • 절차지향적 개발의 한계

⇒ 이런 이유로 객체지향 개발과 디자인 패턴이 인기를 얻었다.

  • 현대 개발에서 OOP를 이해하지 않고는 고급 프로그래밍을 다루는 것이 어렵다.
    • 최근 인기 있는 프레임워크들을 사용하려면 객체지향 개발 방법을 숙지해야 한다.
    • 객체지향 방식을 사용해서 코딩하지 않으면 협업이 어려울 정도로 개발 환경에서 OOP가 보편화되었다.

클래스 Class

객체지향 개발의 시작 : 클래스 선언

  • OOP를 지원하는 언어에선 class 키워드로 클래스를 선언한다.
  • 클래스 파일 생성 시, 각 언어의 스타일 규격에 맞게 작성하는 것을 권장한다.

객체 Object

객체지향 코드의 실행 : 클래스를 통한 객체 생성

  • 객체 생성을 위한 키워드 new
    • new : 선언된 클래스로 객체를 생성하고 반환한다.
    • 컴파일 / 인터프리터 과정에서 선언된 클래스에 따라 객체를 생성하고
      이를 메모리에 할당한다.
  • 언어에서 객체를 생성하는 과정 = 인스턴스화 (Instantiate)
  • 인스턴스화로 생성된 객체 = 인스턴스 (Instance)

객체 사용

  • 클래스 → 데이터(property)와 함수(method)를 하나의 그룹으로 묶는다. = 캡슐화
    • 캡슐화된 객체의 함수를 사용하기 위해선 행위(method)를 호출해야 한다.

의존성


객체지향

  • 하나의 객체로 처리할 수 있는 일은 매우 제한적이다.
    • OOP = 수많은 객체의 집합을 설계하는 것
    • 생성된 다수의 객체는 상호 작업을 위한 관계를 설정해주어야 한다.
  • 관계 (Relation)
    • 객체 간 동작을 위해 접근하는 것.
    • 개별 객체는 요구되는 역할을 수행하기 위해 책임이 부여된다.
    • 객체지향은 부여된 책임 간의 관계를 설정한다. → 상호 동작을 수행하여 로직을 처리한다.
  • 객체의 관계 설정→ 객체 간에 상호작용이 발생한다.
    • 문제를 해결하기 위해 책임 있는 객체는 서브 책임을 가진 다른 객체에 소속된 문제 해결을 위임한다.
    • ⇒ 객체들은 이 과정에서 정보를 주고 받는다. = 대화 (Message)
  • 객체는 서로 메시지를 전달하며 고유의 기능을 실행한다.

의존

  • 객체지향에서 객체의 관계를 설정하는 것 = 의존
    • 객체지향에선 절차지향과 달리 해결해야 하는 기능과 관련된 책임을 캡슐화분배한다.
  • 객체는 각자 고유한 역할을 분담한다. → 하나의 객체만으로 동작하는 것은 의미가 없다.
    • 객체의 관계 = 객체 간 결합 = 의존성
    • ⇒ 캡슐화된 객체는 서로 관계를 맺고 대화를 통해 동작을 수행한다.
  • 의존성의 유형
    • 객체 내부에 다른 객체를 생성하는 것 ⇒ 두 객체 간 의존성 발생
    • 외부에 의해 결합 관계가 발생하는 것 ⇒ 의존성 주입

객체 선택

  • 객체 내부에서 다른 객체를 생성하여 의존성이 발생한 경우
    • 만약 생성해야 할 클래스가 조건이나 상황에 따라 다르다면,
      ⇒ 조건에 맞게 구체적으로 직접 지정한 후 객체를 생성해야 한다.
    • 조건에 따라 생성되는 객체가 다르다면 객체를 유연하게 생성할 수 없다.
      • 만일 새로운 조건이 추가된다면 조건에 맞게 객체 생성 로직을 수정해야 한다.
  • 이처럼 객체를 어떻게 생성 처리할 지에 따라 의존성유연성이 결정된다.

강력한 결합

  • 객체의 메서드 안에 다른 객체를 생성하는 경우
    → 하나의 객체가 또 다른 객체를 생성 ⇒ 두 객체 간 결합 관계가 발생한다.
  • 메서드 코드 내에서 클래스 이름을 직접 지정해 객체를 생성하는 경우
    = 강력한 결합 관계 발생
    • 강력한 결합 관계 발생 시,
      클래스 이름 등이 변경될 때 이름을 통해 생성된 코드를 모두 직접 찾아 수정해야 한다.
  • 강력한 결합 코드의 영향
    • 향후 유연한 코드 확장을 방해한다.
    • 변경과 수정을 어렵게 만드는 원인이 된다.

클래스 변경

  • 객체의 의존성과 결합 관계의 문제점을 개선한 것 = 디자인 패턴
  • 팩토리 패턴 : 객체 생성 시 확장과 수정을 쉽게 하기 위한 설계 방법이다.
  • 객체지향에서 객체 간 의존성은 객체를 생성할 때마다 발생한다.
    • 코드에서 직접 생성한 객체 → 의존성이 강력하다 → 유지 보수 및 수정이 어렵다.
    • 의존 관계가 강력한 결합 관계는 느슨한 결합 관계로 변경해서 문제를 해결할 수 있다.
  • 팩토리 패턴은 클래스 구조를 변경하여 객체 구조를 느슨한 관계로 변경한다.

의존성 주입


객체의 의존성

  • 내부적 발생
  • 외부적 발생 → 외부적 요인으로 결합 = 의존성 주입
    • 의존성 주입 발생 ⇒ 객체는 복합 객체의 형태

복합 객체

  • 하나의 객체가 다른 객체의 정보를 가진 구조
    • 객체 정보는 클래스의 프로퍼티 값을 통해 다른 외부 객체를 가리킨다.
    • 복합 객체 → 종속적, 연관 관계를 갖는다.
  • ex) 객체 생성 시, 생성자에 매개변수를 통해 외부 객체를 전달 및 프로퍼티에 저장하는 경우

생성과 주입

  • 의존성 주입 : 객체에 또 다른 객체의 정보를 전달하는 것
    • 외부에서 전달 받은 객체(정보)를 내부 프로퍼티에 저장한다. ⇒ 복합 구조 형성

의존 관계의 문제점


팩토리 패턴의 목적 : 개체 생성 시 발생하는 강력한 의존 관계를 느슨하게 만드는 것

new 키워드

  • : 선언된 클래스를 기반으로 객체를 생성하는 인스턴스화 작업을 수행한다.
  • 객체지향에서 객체 생성은 피할 수 없는 과정이다.
    인스턴스화를 통해 객체가 생성되면 의존성도 발생한다.
    ⇒ new 키워드로 직접 객체를 생성 = 강력한 결합 관계의 코드
    - 하지만, new 키워드를 사용하지 않고 객체를 생성할 순 없다.
    - new 키워드는 객체를 생성하는 유일한 방법이다.

변화 대응

  • 객체 생성 과정에서 강력하지 않은 연관 관계를 만들 방법
    = new 키워드를 사용하지 않고 객체를 만들 방법
    = 생성 패턴
  • 생성 패턴 중에서도 팩토리 패턴은 객체의 생성을 별개의 클래스로 구축하여 위임 처리한다.

생성 위임

  • 생성 패턴
    • 객체 생성을 위임하여 별개의 클래스로 분리
    • 객체 생성 과정을 담당할 별도의 클래스를 선언
    • ⇒ 프로그램 내, 필요한 객체를 생성하고 관리하는 캡슐화된 클래스를 선언한다.
  • 프로그램 내, 객체 생성이 필요한 경우, 분리 설계된 팩토리 객체에 생성을 위임한다.
    • 실제 코드가 new 키워드를 직접 사용하지 않고 팩토리 객체를 호출하는 것으로 대체한다.
    • 디자인 패턴에서 객체를 생성하고 캡슐화하여 위임하는 것 = 공장 (Factory)

객체 공장

  • (개발자마다 객체 생성 방법이 다양하지만) 팩토리 패턴은
    • new 키워드를 코드에 직접 작성하지 않는다.
    • 일정한 규격에 맞춘 메서드나 함수를 이용한다.
    • ⇒ 공장에서 물건 찍어 내 듯 객체를 생성한다.
      = 객체 생성 처리를 규격화하여 관리한다.
  • 디자인 패턴은 강한 연관 관계를 느슨한 관계로 변환한다.
    느슨한 관계는 보다 다양한 코드 변화를 처리할 수 있다.
  • 팩토리는 객체의 생성 작업을 분리한다.
    • 분리된 객체의 생성 처리는 크게 2가지로 나뉜다.
      • 클래스로 분리 : 일반적인 팩토리.
        개별 클래스로 분리해 위임을 처리하는 방법이다.
      • 메서드로 분리 : 단순 팩토리 (Simple Factory)라고 한다.
        기존 클래스에 생성 메서드만 추가해 객체를 생성, 처리한다.

팩토리 패턴


객체 생성을 위임할 수 있는 클래스를 정의한다.

  • 팩토리 패턴의 클래스 :
    • 객체 생성을 담당한다.
    • 객체의 생성을 느슨한 관계가 되도록 처리한다.

느슨한 결합

  • 코드에서 직접 클래스의 이름을 지정해 객체를 생성하지 않는다.
    별도의 객체에 필요한 객체를 생성하도록 책임을 위임한다.
// Factory.h
#include "../objs/Object.h"

#ifndef FACTORY_H
#define FACTORY_H

class Factory
{
public:
    Factory(){};
    ~Factory(){};
    static Object get_instance() { return Object(); }; 
};

#endif // !FACTORY_H

// main.cpp
#include "factory/Factory.h"

int main() {

    Object obj = Factory::get_instance();
    obj.set_name("OBJ_1");
    cout << obj.get_name() << endl;

    return 0;
}

동적 팩토리

  • 팩토리 패턴은 분리된 factory 객체를 통해 필요로 하는 모든 객체의 생성을 위임한다.
  • 코드에서 직접 new 키워드를 사용하거나 직접 필요한 객체를 사용하지 않는다.
    ⇒ 강력한 결합 관계 발생을 줄인다.
    • 기존의 클래스를 수정하여,
      Factory 클래스의 get_instance() 메서드를 정적(static) 타입으로 호출한다.
  • 팩토리 패턴으로 강력한 의존 관계 문제가 해결되었다.

장단점

  • 단점 : 팩토리 패턴 사용으로 객체를 직접 생성하는 것과 달리
    Factory 객체를 통해 호출(call)하는 처리 과정이 한 단계 더 필요해짐.
    ⇒ 불필요한 호출 증가로 프로그램 성능 저하를 초래한다.
  • 장점 : 객체 생성을 다른 객체에 위임함으로써
    • 내부적인 결합을 제거했다. = 강력한 의존 관계 문제가 해결
    • 동적으로 객체를 관리할 수 있다.

클래스의 선택

  • Factory 객체에서 Object 객체 외에도 비슷한 성격의 다른 클래스 ObjectOther객체를 생성해야 하는 경우
    (= 새로운 객체가 추가된 경우)
  • get_instance() 메서드에 구분을 위한 매개변수와 매개변수에 따라 다른 객체를 생성하도록 조건을 추가한다.
// FactoryOther.h
#ifndef FACTORY_OTHER_H
#define FACTORY_OTHER_H

#include <iostream>
using namespace std;

class Object;

class FactoryOther
{
public:
    static const string TYPE_A;
    static const string TYPE_B;
    FactoryOther();
    ~FactoryOther();
    static Object get_instance(string); // 매개변수 추가
};

#endif // !FACTORY_H

// FactoryOther.cpp
~
Object FactoryOther::get_instance(string type){

    if(type == FactoryOther::TYPE_A){
        cout << "instantiate Type A" << endl;
        return Object();
    }
    else if (type == FactoryOther::TYPE_B){
        cout << "instantiate Type B" << endl;
        return ObjectOther();
    }
    
}
~

// main.cpp
#include "objs/Object.h"
#include "factory/FactoryOther.h"
#include "factory/FactoryOther.cpp"

int main() {

		// 매개변수를 이용해서 원하는 객체를 생성한다.
    Object obj2 = FactoryOther::get_instance(FactoryOther::TYPE_A);

    return 0;
}
  • 생성할 객체를 선택할 수 있는 조건 로직을 추가한다.
    • 외부에서 매개변수 값을 받아서 처리
      ⇒ 조건에 따라 객체를 선택적으로 생성하고 반환할 수 있다.
    • 조건이 추가되고 변경될 때마다 다른 객체를 생성하고 반환할 수 있다.

형식 안정성

  • 팩토리 클래스는 프로그램 내부에서 필요한 객체를 생성하는 역할을 수행한다.
    다수의 객체를 생성할 때는 이를 선택하기 위해 매개변수를 전달한다.
    - 매개변수의 타입은 대부분 문자열을 사용한다.
    - 전달받은 매개변수를 판별하지 못해 정확히 생성해야 할 객체를 결정하지 못하는 경우
    (조건 불일치)
    - ⇒ 프로그램이 안전하게 동작하도록 하기 위해 상수(const)를 사용하기도 한다.
const string FactoryOther::TYPE_A = "TYPE_A";
const string FactoryOther::TYPE_B = "TYPE_B";

단순 팩토리 Simple Factory


팩토리 패턴의 특징과 처리 로직을 간략하게 작성한 것

기존의 단점이었던 별개의 Factory 객체를 생성하지 않는다.
대신, 자신의 객체에 필요한 객체를 생성하는 전용 메서드를 추가한다.

메서드

  • 전형적인 팩토리 패턴은 생성, 관리하는 객체를 별도의 클래스로 분리하여 위임한다.
    • → 새로운 클래스가 늘어날수록 추적 관리할 코드가 많아진다.
  • 단순 팩토리는 이런 관점에서 등장해서 별도의 팩토리 클래스를 생성하지 않고,
    자기 자신의 클래스에 객체 생성을 처리할 수 있는 전용 메서드를 추가한다.

단순 팩토리

  • 객체 생성 과정이 복잡하지 않을 경우,
    추가 클래스 파일을 생성하지 않고도 팩토리 패턴을 적용할 수 있다.
  • 단순 팩토리 = 정적(static) 팩토리 패턴

장점과 단점


장점

  1. 생성과 관련된 모든 처리를 별도의 클래스 객체로 위임할 수 있다.
    • 사용과 생성을 분리하는 과정에서 중복된 코드를 정리하는 효과가 있다
  2. 유연성과 확장성이 개선된다.
    • 개발 과정에서 클래스 이름 변경 시, 일일이 수정하지 않아도 된다.
      팩토리 객체를 통해서 손쉽게 변경할 수 있다.
  3. 어떤 객체를 생성하지 모르는 초기 단계 코드에 매우 유용하다.

단점

  1. 객체 생성을 위임하는데 별도의 새로운 클래스가 필요하다
  2. 관리할 클래스 파일이 늘어난다.
    • 이를 보완하기 위해 단순 팩토리를 사용한다.

UML

Factory Pattern Class Diagram

profile
만성피로 개발자

0개의 댓글