반응형

생성 패턴

-. Factory Pattern: 객체의 생성 동작을 별도의 Class로 분리해 처리하거나 별도의 Method를 호출해 처리

-. Singleton Pattern: 선언된 Class로 복수 객체를 생성할 수 없도록 제한하며, 제한된 단일 객체는 공유와 충돌 방지

-. Factory Method Pattern: Factory Pattern에 추상화를 결합해 객체의 생성과 사용을 분리하는 방식으로, 선언된 Class의 객체를 직접 Code로 생성하지 않고 별도로 준비한 추상 Method에 생성 위임

-. Abstract Factory Pattern: Factory Method Pattern 보다 좀 더 큰 그룹 단위 객체를 생성/관리하는 방식으로 Factory에 Interface를 활용해 객체를 생성함으로써 Factory를 Factory의 군(Family)으로 변경

-. Builder Pattern: 추상 Factory Pattern을 확장한 Pattern으로 복잡한 구조의 복합 객체를 생성하기 위한 단계를 정의하고 각 단계 별 수행 동작을 변경하면서 Builder Pattern으로 생성

-. Prototype Pattern: 새로운 객체를 생성하지 않고 기존의 객체를 복제하는 방식으로 자원을 절약하는 Pattern

 

1. Class

: 객체지향 프로그래밍(OOP, Object Oriented Programming)의 핵심 개념으로, Data와 Data를 조작하는 함수를 하나의 논리적 단위로 묶는 사용자 정의 Data Type

 

1.1 주요 구성요소

-. Member Variant(멤버 변수): Class 내에서 Data를 저장하기 위한 변수로 객체의 속성을 나타냄

-. Member Function(멤버 함수): Class 내의 Data(Member Variant)를 조작하거나 동작을 정의하는 함수

-. Access Specifier(접근 제한자): Class Member의 접근 권한 제어

   -. public: 외부에서 접근 가능

   -. private: Class 내부에서만 접근 가능

   -. protected: 파생 Class에서 접근 가능

-. Constructor(생성자): 객체 생성 시 자동으로 호출되는 함수로 Member Variant를 자동으로 초기화 하거나 객체 초기 설정에 사용

-. Destructor(소멸자): 객체가 소멸할 때 자동으로 호출되는 함수로 자원 해제 등에 사용

 

1.2 Class의 특징

-. Inheritance(상속): 기존 Class(부모 Class)의 속성과 가능을 다른 Class(자식 Class)가 물려받아 Code의 재사용성을 높임

-. Polymorphism(다형성): 같은 이름의 함수가 다른 동작을 수행할 수 있음

   e.g., 함수 Over loading, 가상함수, ...

-. Encapsulation(캡슐화): Data와 함수를 하나로 묶고 외부에서 직접 접근 제한

-. Abstraction(추상화): 실제 Code를 개발하기 전에 구체적인 내용은 배제하고 개략적인 정보만 선언하는 것으로 추상적 개념과 실제 Code를 분리하는 효과가 있어 향후 개발 중인 Code를 예정하고 선언만으로 주변 가능을 작성할 수 있다는 장점이 있음

#include <iostream>
using namesapce std;

class Hello{
public:
	Hello(){ cout << "Constructor called" << endl; }
    ~Hello(){ cout << "Destructor called" << endl; }
    void greeting(){ cout << "Hello" << endl; }
};

 

2. 객체(Object)

: 객체를 설계할 때는 어떤 유형의 객체를 생성할 지 미리 정의하는 것이 중요

 

2.1. new keyword

객체를 생성하는 예약어로 Compile, Interpreter 과정에서 선언된 Class에 따른 객체(Instance)를 생성하고 이를 Memory에 할당

선언된 Class를 기반으로 객체를 생성하는 Instance화 작업을 수행하며 객체 간에 강력한 의존 관계를 갖는 구조적 문제 발생

-> 생성 패턴을 이용해 해당 문제를 해결할 수 있으며, 특히 Factory Pattern은 객체 생성을 별개의 Class로 구축해 위임 처리

 

[참고] Compile vs Interpreter

더보기

-. Compile: 고급 프로그래밍 언어(C, C++, JAVA, ...)로 작성된 Code를 기계어로 번역하는 과정으로, 이 과정은 한 번에 이루어져 실행 가능한 독립적인 Binary File(Executable) 생성

   -. 장점: Compile 과정이 미리 완료되어 있어 속도가 빠르며, 배포 시 Source Code가 필요하지 않음

   -. 단점: Source Code가 수정되면 재 Compile이 필요하며, 개발 과정에서 Debugging이 어렵거나 느릴 수 있음

 

-. Interpreter: Source Code를 한 줄 씩 읽고 즉시 실행하는 방식으로, 실행 가능한 File을 생성하지 않으며 주로 Python, Javascript, Ruby 같은 언어에 사용

   -. 장점: Debugging과 Source Code 수정이 용이하며, 실행 파일 없이 Source Code를 바로 실행할 수 있어 개발 속도가 빠름

   -. 단점: Source Code를 실시간으로 해석해 실행 속도가 느릴 수 있으며, Source Code가 필요하기 때문에 배포 시 보안에 취약할 수 있음

 

2.2 객체 생성

1) Stack 기반 객체 생성: 객체가 Stack Memory에 생성되어 Scope를 벗어나면 객체가 자동으로 소멸되어 명시적으로 소멸자를 호출하거나 Memory를 해제할 필요 없음

#include <iostream>
using namespace std;

class Hello{
public:
    Hello(){ cout << "Constructor called" << endl; }
    ~Hello(){ cout << "Destructor called" << endl; }
    void greeting(){ cout << "Hello" << endl; }
};

int main(void){
    Hello helloObj; // Stack 기반 객체 생성
    helloObj.greeting(); // Member 함수 호출
    return 0; // Program 종료 시 helloObj의 소멸자가 자동 호출
}

 

Output

Constructor called
Hello
Destructor called

 

2) Heap 기반 객체 생성: 객체가 Heap Memory에 생성되어 delete 연산자를 사용해 명시적으로 Memory를 해제하지 않으면 Memory 누수 발생 가능

#include <iostream>
using namespace std;

class Hello{
public:
    Hello(){ cout << "Constructor called" << endl; }
    ~Hello(){ cout << "Destructor called" << endl; }
    void greeting(){ cout << "Hello" << endl; }
};

int main(void){
    Hello* helloObj = new Hello(); // Heap 기반 객체 생성
    helloObj->greeting(); // Member 함수 호출
    delete helloObj; // 객체 소멸(Memory 해제)
    return 0;
}

 

Output

Constructor called
Hello
Destructor called

 

특징 Stack 기반 객체 Heap 기반 객체
Memory 위치 Stack Heap
생성 방식 일반 선언
ClassName Obj;
new 연산자 사용
new ClassName;
소멸 방식 Scope를 벗어나면 자동 소멸 delete로 명시적 해제 필요
사용 권장 간단하고 일반적인 객체 생성 복잡하거나 동적 생명주기 관리 시
속도 빠름 상대적으로 느림
Memory 누수 위험 없음 있음(해제 누락 시)

 

3. 의존성

3.1 객체 지향

: 부여된 책임 간의 관계를 설정하고 상호 동작을 수행하는 것

* 관계(Relationship): 객체 간 동작을 위해 접근하는 것으로 개별 객체는 요구되는 역할을 수행하기 위한 책임이 부여됨

   1) 객체의 관계 성립 -> 객체 간 상호작용 발생

   2) 문제 해결을 위해 책임감 있는 객체는 각각의 Sub 책임을 가진 다른 객체에 소속된 문제 해결 위임

      -> 이 때 객체 간 대화(Message)를 주고 받으며 객체의 고유 기능 실행

 

3.2 의존성(Dependency)

: SW 개발에서 한 Component(또는 객체)가 다른 Component에 의존하는 관계로, 한 Class가 자신의 작업을 수행하기 위해 다른 Class나 외부 Resource에 접근하거나 사용하는 경우 두 Class 사이에 의존성이 있다고 함

-> 의존성이 많으면 System이 복잡해지고 유지보수가 어려워질 수 있으므로, 필연적일 수 있으나 관리 및 최적화 하는 것이 중요

 

3.2.1 직접 의존성: 한 Class가 다른 객체를 직접 생성하거나 사용하는 경우

#include <iostream>
using namespace std;

class Engine{
public:
    void start(){ cout << "Engine started" << endl; }
};

class Car{
private:
    Engine engine; // Car Class가 Engine에 의존
    
public:
    void startCar(){
        engine.start(); // Engine 객체 사용
        cout << "Car is ready to drive" << endl;
    }
};

int main(){
    Car car;
    car.startCar();
    return 0;
}

 

Output

Engine started
Car is ready to drive

 

3.2.2 의존성이 많은 Code의 문제점

-. 높은 결합도(Coupling)

   : Class 내에서 Class 이름을 직접 지정해 객체를 생성하는 경우로, Class 간 결합도가 많으면 하나의 변경이 다른 Class에도 영향을 줄 가능성 커짐

   -> 강력한 객체의 결합 Code는 향후 유연한 Code 확장을 방해하고 변경과 수정을 어렵게 만드는 원인

-. 재사용성 저하

   : 특정 Class가 다른 Class에 강하게 의존하면 독립적으로 재사용하기 어려워짐

-. Test 어려움

   : 의존성이 강한 Class는 단위 Test(Unit Test)를 수행하기 어려워질 수 있음

 

3.2.3 의존성 관리 방법

   1) 의존성 주입(Dependency Injection, DI)

   : 객체가 필요한 의존성(다른 객체)을 직접 생성하지 않고 외부에서 주입 받는 설계 방식으로, 의존성 주입이 발생하면 객체는 일반이 아닌 복합 객체 형태의 모습을 갖게 됨

#include <iostream>
using namespace std;

class Engine{
public:
    void start(){ cout << "Engine started" << endl; }
};

class Car{
private:
    Engine* engine; // Pointer로 객체 참조
public:
    // 생성자 초기화 리스트로 Car Class 생성자에서 Engine 객체의 Pointer e를 전달 받아 멤버 변수 
    // engine에 초기화(Car 객체 생성 시 Engine 객체 전달 필요)
    Car(Engine* e): engine(e){} // 의존성 주입
    
    void startCar(){
        engine->start(); // Engine 객체 사용
        cout << "Car is ready to drive" << endl;
    }
};

int main(){
    Engine engine; // Engine 객체 생성
    Car car(&engine); // 의존성 주입
    car.startCar();
    return 0;
}

 

Output

Engine started
Car is ready to drive

 

[참고] 생성자 초기화 리스트(Syntax for Initializer List)

더보기

객체가 생성될 때 멤버 변수를 초기화하는 방법으로, 생성자 내부에서도 멤버 변수를 초기화할 수 있으나, 생성자 초기화 리스트가 더 효율적

Car(Engine* e){
    engine = e; // 생성자 내부에서 객체 초기화
}

 

   2) Interface 사용

   : Class 간 결합도를 줄이기 위해 Interface를 사용해 의존성을 느슨하게 만듦

   -> C++에서는 Interface를 직접적으로 지원하지 않지만 순수가상함수(Pure Virtual Function)를 가진 Class를 통해 Interface 구현

#include <iostream>
using namespace std;

// Interface 정의
class IEngine{
public:
    virtual void start() = 0; // 순수가상함수(Interface)
    virtual ~IEngine(){} // 가상 소멸자
{;

// Engine Class: IEngine Interface 구현
class Engine: public IEngine{
public:
    void start() override{
        cout << "Standard Engine started" << endl;
    }
};

// Electric Engine Class: IEngine Interface 구현
class Electric Engine: public IEngine{
public:
    void start() override{
        cout << "Electric Engine started silently" << endl;
    }
};

// Car Class
class Car{
private:
    IEngine* engine; // IEngine Interface 참조
public:
    Car(IEngine* e): engine(e){} // 의존성 주입
    void startCar(){
        engine->start(); // IEngine의 start() 호출
        cout << "Car is ready to drive" << endl;
    }
};

int main(){
    Engine standardEngine; // Standard Engine 객체 생성
    ElectricEngine electricEngine; // Electric Engine 객체 생성
    Car car1(&standardEngine); // Standard Engine 주입
    car1.startCar();
    Car car2(&electricEngine); // Electric Engine 주입
    car2.startCar();
    return 0;
}

 

Output

Standard Engine started
Car is ready to drive
Electric Engine started
Car is ready to drive

 

   3) 의존성 역전 원칙(DIP, Dependency Inversion Principle)

   : 고수준 Module은 저수준 Module에 의존하지 않고 모두 추상화에 의존해야 함

 

   4) DI Framework 사용

   : Google Guide와 같은 DI Framework 사용

 

3.3 복합 객체

하나의 객체가 다른 객체를 포함하거나 참조하는 구조로, 종속적이고 연관 관계를 가짐

 

1) 포함관계 (Composition)

: 강한 관계로 객체가 다른 객체를 직접 포함하며, 포함된 객체는 전체 객체와 생명주기 공유

#include <iostram>
using namespace std;

// Korean Class
class Korean{
public:
    string text() const { return "안녕하세요"; }
};

// Hello Class (포함관계)
class Hello{
private:
    Korean korean; // Korean 객체 직접 포함 (포함관계)
public:
    Hello(){}
    // greeting Method
    string greeting() const { return korean.text(); }
};

int main(){
    Hello hello; // Hello 객체 생성 (Hello 객체 생성되면서 Korean 객체도 함께 생성됨)
    cout << hello.greeting() << endl;
    return 0;
}

 

Output

안녕하세요

 

2) 집합 관계 (Aggregation)

: 느슨한 관계로, 객체가 다른 객체를 참조하거나 포인터로 소유하며 포함된 객체는 독집적으로 생성 및 소멸되어 생명주기를 공유하지 않음

#include <iostream>
#include <memory>
using namespace std;

class Korean{
public:
    string text() const { return "안녕하세요"; }
}

class Hello{
private:
    shared_ptr<Korean> korean; // Korean 객체 참조 (집합 관계)
public:
    Hello(shared_ptr<Korean> obj): korean(obj) {} // 외부에서 Korean 객체 전달 받음
    string greeting() const { return korean -> text(); )
};

int main(){
    shared_ptr<Korean> koreanObj = make_shared<Korean>(); // Korean 객체를 독립적으로 생성
    Hello hello(koreanObj); // Korean 객체를 Hello 객체에 전달(생명주기를 공유하지 않음)
    cout << hello.greeting() << endl;
    return 0;
}

Output

안녕하세요
구분 포함 관계 집합 관계
객체 간 관계 한 객체가 다른 객체를 완전하게 소유 한 객체가 다른 객체를 참조만 함
생명주기 포함된 객체는 포함하는 객체가 소멸될 때 함께 소멸 포함된 객체는 독립적으로 존재 가능
설계 방법 포함된 객체를 Class의 멤버변수로 직접 선언 포함된 객체를 포인터 또는 스마트포인터로 참조

 

3.3.2 복합객체의 장단점

 

1) 장점

   -. 모듈화: 전체와 부분으로 나누어 설계하면 Code의 재사용성과 유지보수성 증가

   -. 확장성: 객체를 구성하는 부분 객체를 교체하거나 확장하기 용이

   -. 캡슐화: 복합 객체 내부의 세부 사항을 숨기고 외부에는 필요한 동작만 제공

 

2) 단점

   -. 의존성 증가: 복합객체와 구성요소 간의 강한 의존성으로 인해 변경이 전체에 영향을 줄 수 있음

   -. 복잡성 증가: 객체 간의 관계를 설계하고 관리하는 데 추가적인 노력 필요

 

4 생성 패턴(Creational Pattern)

4.1 new keyword

: 선언된 Class를 기반으로 객체를 생성하는 Instance화를 수행하며 객체 간에 강력한 의존 관계를 갖는 구조적 문제 발생

 

4.2 생성 패턴(Creational Pattern)

: 객체 생성을 위임해 별개의 Class로 분리하고 Program 내에서 객체 생성이 필요한 경우 분리 설계된 Factory 객체에 생성을 위임함으로써 느슨한 결합 관계로 변경

* 공장(Factory): Design Pattern에서 객체를 생성하고 캡슐화해 위임하는 것

 

5 Factory Pattern

: 객체 생성 로직을 캡슐화해 Client Code가 직접 객체의 생성 방법을 알 필요 없도록 하는 Design Pattern

즉, 객체를 생성하는 공장을 만드는 Pattern으로 객체 생성을 위한 Interface를 제공하면서 실제 생성 작업은 Sub-Class나 Factory Class에서 수행해 느슨한 결합 관계로 변경

-. 장점: 개발 과정에서 Class의 이름이 변경되어도 Code를 일일히 수정하지 않고 Factory 객체를 통해 쉽게 변경 가능(유연성과 확장성 개선)

-. 단점: 객체 생성을 위임하는 데 별도의 Class가 필요

   -> 단순 팩토리로 이 단점 보완 가능

 

5.1 느슨한 결합

: 객체 생성은 객체 간 결합 관계를 발생시키고, 객체 간에 의존성 부여

-> Factory Pattern에서는 Code에서 직접 Class의 이름을 지정해 객체를 생성하지 않으며, 별도의 객체에 필요한 객체를 생성하도록 책임을 위임하는 과정에서 중복 코드 정리

#include <iostream>
#include <memory>
using namespace std;

// 1 제품 Class 정의 (Korean & English)
class Language{
public:
    // 객체의 불필요한 변경을 방지하고, Code의 안정성과 최적화를 향상시키기 위해 상수(const) 사용
    virtual string text() const = 0; // 순수가상함수
    virtual ~Language(){} // 가상소멸자
};

class Korean: public Language{
public:
    string text() const override { return "안녕하세요"; }
};

class English: public Language{
public:
    string text() const override { return "Hello"; }
};

// 2 Factory Class 정의
class Factory{
public:
    static unique_ptr<Language> getInstance(const string& type){
        cout << "Factory: 객체를 생성합니다" << endl;
        
        if(type == "ko") return make_unique<Korean>(); // 스마트포인터를 사용해 객체 반환
        else if(type == "en") return make_unique<English>();
        return nullptr; // 잘못된 type이면 nullptr 반환
    }
};

 

[참고] Static Method

더보기

Class의 Member 변수나 Method가 객체(Instance)에 종속되지 않고, Class 자체에 속하도록 만들 때 사용

즉, Class의 Instance를 생성하지 않고도 사용할 수 있는 변수나 Method를 정의할 때 유용

 

1) 객체를 생성하지 않고도 호출 가능

: Static Method는 Class의 Instance를 생성하지 않아도 바로 호출 가능

2) Memory 절약 (불필요한 객체 생성 방지)

: static keyword를 사용하면 객체 없이도 Class 자체에서 직접 함수 호출이 가능해 Memory 사용량을 줄일 수 있음

-> Factory Pattern에서는 단순히 객체를 생성해 반환하는 역할만 수행하므로 굳이 Factory 객체를 만들 필요 없음

3) 전역적인 기능 제공 (Utility Function)

: Static Method는 특정 객체의 상태와 무관하게 동작하는 함수를 만들 때 유용

즉, Class의 상태를 변경하지 않는 함수(Utility Function)일 경우, static을 붙이면 객체 없이도 호출할 수 있음

class Logger{
public: // 객체 없이 호출 가능
    static void log(const string& message){
        cout << "[LOG]: " << message << endl;
    }
};

int main(){
    Logger::log("Program이 시작되었습니다");
}

4) Factory Pattern에서 객체 생성 통일

: Factory Pattern에서는 객체 생성을 Captulation 해 통일된 방식으로 관리

-> Factory Pattern에서 객체 생성을 담당할 때, 굳이 Factory 객체를 만들 필요가 없으므로 static method로 정의하는 게 좋음

5) Sigleton Pattern과 함께 사용 가능

* Singleton Pattern: Program에서 단 하나의 객체만 존재하도록 제한하는 Pattern

-> static method를 사용해 전역적으로 하나의 객체만 유지할 수 있도록 함

class Singleton{
private:
    static unique_ptr<Singleton> instance; // 정적변수
    Singleton(){} // 생성자 Private으로 숨김
public:
    static Singleton& getInstance(){ // 정적 Method
        if(!instance) instance = make_unique<Singleton>();
        return *instance;
    }
    
    void showMessage(){ cout << "Singleton 객체입니다" << endl; }
};

// 정적변수 초기화
unique_ptr<Singleton> Singleton::instance = nullptr;

int main(){
    Singleton& s1 = Singleton::getInstance();
    Singleton& s2 = Singleton::getInstance();
    s1.showMessage();
    cout << (&s1 == &s2) << endl;
}

Output

Singleton 객체입니다
1

 

 

[참고] 스마트 포인터(Smart Pointer)

더보기

C++ 표준 Library에서 제공하는 Pointer 객체로, 일반 포인터(*)와 비슷하게 동적 Memory를 관리하지만, Memory 해제를 자동으로 처리해 Memory 누수(Memory leak) 방지

 

1) 자동으로 Memory 관리

: 스마트포인터는 객체가 더 이상 필요하지 않을 때 소멸자에서 메모리를 자동으로 해제해 delete 호출을 직접 할 필요 없이 메모리 누수 방지

2) 예외 안정성

: 예외가 발생하거나 함수가 비정상으로 종료될 경우에도 자동으로 메모리 해제

 

* 스마트 포인터 종류

-. std::unique_ptr

: 객체의 유일한 소유권을 가지며 다른 unique_ptr에 복사할 수 없고 이동만 가능

#include <iostream>
#include <memory>

class Test{
public:
    Test(){ std::cout << "Test created" << endl; }
    ~Test(){ std::cout << "Test destroyed" << endl; }
};

int main(){
    std::unique_ptr<Test> ptr = std::make_unique<Test>();
    return 0; // ptr이 범위를 벗어나면 Test 객체를 자동으로 파괴
}

Output

Test created
Test destroyed

 

 -. std::shared_ptr

: 객체의 공유된 소유권을 관리하며 참조 카운트를 사용해 객체에 대한 참조가 모두 소멸되었을 때 메모리 해제

#include <iostream>
#include <memory>

class Test{
public:
    Test{ std::cout << "Test created" << endl; }
    ~Test{ std::cout << "Test destroyed" << endl; }
};

int main(){
    std::shared_ptr<Test> ptr1 = std::make_shared<Test>();
    std::shared_ptr<Test> ptr2 = std::make_shared<Test>();
    
    // ptr1과 ptr2가 Test 객체 공유
    std::cout << "Use count: " << ptr1.use_count() << endl; // 참조 카운트
    return 0;
}

 Output

Test created
Use count: 2
Test destroyed

 

-. week_ptr

: shared_ptr의 순환 참조 문제를 해결하기 위해 사용되며 객체를 참조하되, 참조 카운트를 증가시키지 않으며 객체가 파괴

 

5.2 동적 팩토리(Dynamic Factory) Pattern

: 분리된 Factory Class의 객체를 통해 필요로 하는 모든 객체의 생성 위임

-> Code 자체에서 생성되는 강력한 의존 관계를 분리하고 느슨한 의존 관계로 변경

// 3 Hello Class 정의
class Hello{
public:
    string greeting(const string& type){
        unique_ptr<Language> lang = Factory::getInstance(type); // Factory를 호출해 객체 생성
        if(lang){ return lang->text() }
        return "유효하지 않은 타입입니다";
    }
};

 

전체 Code

#include <iostream>
#include <memory>
using namespace std;

// 1 제품 Class 정의 (Korean & English)
class Language{
public:
    // 객체의 불필요한 변경을 방지하고, Code의 안정성과 최적화를 향상시키기 위해 상수(const) 사용
    virtual string text() const = 0; // 순수가상함수
    virtual ~Language(){} // 가상소멸자
};

class Korean: public Language{
public:
    string text() const override { return "안녕하세요"; }
};

class English: public Language{
public:
    string text() const override { return "Hello"; }
};

// 2 Factory Class 정의
class Factory{
public:
    static unique_ptr<Language> getInstance(const string& type){
        cout << "Factory: 객체를 생성합니다" << endl;
        
        if(type == "ko") return make_unique<Korean>(); // 스마트포인터를 사용해 객체 반환
        else if(type == "en") return make_unique<English>();
        return nullptr; // 잘못된 type이면 nullptr 반환
    }
};

// 3 Hello Class 정의
class Hello{
public:
    string greeting(const string& type){
        unique_ptr<Language> lang = Factory::getInstance(type); // Factory를 호출해 객체 생성
        if(lang){ return lang->text() }
        return "유효하지 않은 타입입니다";
    }
};

// 4 Main 함수(Client Code)
int main(){
    Hello hello;
    cout << hello.greeting("ko") << endl;
    cout << hello.greeting("en") << endl;
    cout << hello.greeting("fr") << endl;
    return 0;
}

Output

Factory: 객체를 생성해 반환합니다
안녕하세요
Factory: 객체를 생성해 반환합니다
Hello
Factory: 객체를 생성해 반환합니다
유효하지 않은 타입입니다

 

5.3 단순 팩토리(Simple Factory) Pattern = 정적 팩토리(Static Factory) Pattern

: Factory Pattern의 특징과 처리 로직을 간략하게 작성한 것으로 별개의 Factory Pattern 객체를 생성하는 것이 아닌 자신의 객체에 필요한 객체를 생성하는 전용 Method 추가

즉, 객체 생성 로직이 단일함수에 캡슐화

#include <iostream>
#include <memory>
using namespace std;

// Korean Class 정의
class Korean{
public:
    string text() const{ return "안녕하세요"; }
};

// Hello Class 정의
class Hello{
public:
    // greeting Method
    string greeting() const{
        // Factory Method 호출해 Korean 객체 생성
        unique_ptr<Korean> ko = factory();
        return ko->text();
    }
    
    // Factory Method (정적 Method)
    static snique_ptr<Korean> factory(){
        return make_unique<Korean>() // C++에서는 동적 객체를 반환할 때 스마트 포인터를 사용하는
                                     // 것이 메모리 관리 측면에서 안전
    }
};

// main 함수
int main(){
    Hello hello;
    cout << hello.greeting() << endl;
    return 0;
}

Output

안녕하세요

-. 장점: 객체 생성 과정이 복잡하지 않은 경우 추가 Class 파일을 생성하지 않고도 Factory Pattern 적용 가능

 

6 정리

Factory Pattern
: 객체 생성 시 확장과 수정을 쉽게 하기 위한 설계 방법으로 객체 생성 과정을 분리해 처리하기 때문에 객체 생성 과정에서 발생하는 new keyword의 문제점을 해결하고 느슨한 객체 생성 관리
다양한 Class의 객체 생성을 쉽게 처리하며 생성하는 객체를 정의할 수 없거나 변경이 있는 경우 객체 생성을 매우 유용하게 관리 가능
-> Factory Pattern이 Proxy Pattern과 결합하면 객체 생성을 위임받을 때 권한에 따라 접근하는 것을 제어할 수 있으며, 특정 객체 생성에서 보안 또는 권한 등의 처리가 필요할 때 응용 가능
[ Problem ] 객체지향개발에서 객체 간 의존성은 객체를 생성할 때마다 발생하며 Code에서 직접 생성한 객체는 의존성이 강력해 유지보수와 수정이 어려움
[ Solve ] 의존 관계가 강력한 결합 관계를 느슨한 결합 관계로 변경
[ 장점 ] 객체 생성을 다른 객체에 위임함으로써 내부적 결합을 제거하고 동적으로 객체 관리 가능
            객체 생성 처리를 동적으로 위임하므로 향후 Class가 추가되거나 변경돼도 Code를 쉽게 수정 가능
[ 단점 ] Factory Pattern을 사용하면 객체를 직접 생성하는 것과 달리 Factory 객체를 통해 호출하는 처리 과정이 한 단계 더 필요하므로, 이는 불필요한 호출 증가로 Program 성능 저하를 초래할 수 있음

Simple Factory Pattern (단순 팩토리 패턴)
: Method를 통해 객체 생성을 관리해 가장 간단하고 깔끔하게 Class의 객체를 생성하는 의존적인 연관관계 해소 가능

 

 
쉽게 배워 바로 써먹는 디자인 패턴
■객체지향 프로그래밍 설계 원칙, 패턴을 알면 개발이 보인다 디자인 패턴은 어느 날 갑자기 생겨난 방식이 아니다. 객체지향 개발 과정에서 겪는 다양한 이슈를 종합해보면 서로 비슷한 유형의 문제다. 개발자들은 서로 자신의 경험을 바탕으로 문제를 해결해나갔다. GoF는 객체지향 설계 시 발생하는 문제점을 목록화하여 24가지 패턴으로 정리한다. 이 책은 디자인 패턴의 개념과 동작 원리를 설명한다. 각 장을 24가지 패턴으로 구성하였으며 누구나 이해할
저자
이호진
출판
한빛미디어
출판일
2020.10.05
반응형

'SW > Design Pattern' 카테고리의 다른 글

0. Design Pattern  (1) 2024.12.25
반응형

* 창발(Energence): 지속적인 관찰을 통해 특정한 패턴을 발견하는 것

 

1 객체지향

: 1980년대 부터 존재했던 소프트웨어 개발 방법론으로 큰 프로젝트 개발과 유지보수를 보다 쉽게 하기 위해 도입
e.g., 스몰토크, C++, JAVA, PHP

 

2 디자인 패턴(Design Pattern)

: 객체지향 구현 문제를 해결하기 위해 도입됐으며, 해결 방법을 재사용하는 것으로 객체지향 개발에서 디자인 패턴이 해결하는 주요 문제는 객체 간 관계와 소통 처리

 

3 SOLID 설계 원칙

: 유지보수성과 확장성을 극대화하기 위한 객체지향 설계 지침으로, C++에서는 다형성(Polymorphism), 추상 클래스(Abstract Class), 인터페이스 설계 등을 활용해 구현

* 다형성(Polymorphism)

더보기

같은 Interface(기본 Class)를 통해 다양한 구현을 사용하며, 기본 Class에 대한 의존성을 유지하면서도 구현을 독립적으로 관리 가능

 

3.1 SRP(Single Responsibility Principle, 단일 책임의 원칙)

: 하나의 클래스는 하나의 기능(책임)만 담당하며, 변경 이유도 하나여야 함

 

ERROR CASE
: 하나의 Class가 계산, 출력, 저장 등 여러 책임을 가짐

class Invoice{
public:
	void calculateTotal(){
    	// 계산 로직
    }
    
    void printInvoice(){
    	// 출력 로직
    }
    
    void saveToDatabase(){
    	// 저장 로직
    }
};

 

GOOD CASE

: 각각의 책임을 별도의 Class로 분리

class InvoiceCalculator{
public:
	void calculateTotal(){
    	// 계산 로직
    }
};

class InvoicePrinter{
public:
	void printInvoice(){
    	// 출력 로직
    }
};

class InvoiceRepository{
public:
	void saveToDatabase(){
    	// 저장 로직
    }
};

 

3.2 OCP(Open/Close Principle, 개방 폐쇄 원칙)

: 코드는 확장에 열려 있고 수정에는 닫혀있어야 하며, 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있도록 설계

 

ERROR CASE

: 도형의 종류가 늘어날 때 마다 calculateArea method 수정 필요

class Shape{
public:
	std::string type;
};

class AreaCalculator{
public:
	double calculateArea(const Shape& shape){
    	if(shape.type == "circle"){
        	// 원의 면적 계산
        } else if(shape.type == "rectangle"){
        	// 사각형의 면적 계산
        }
        return 0;
    }
};

 

GOOD CASE

: 다형성을 활용해 새로운 도형을 추가할 때 기존 코드를 수정하지 않음

class Shape{
public:
	virtual double calculateArea() const = 0; // 순수 가상함수
	virtual ~Shape() = default;
};

class Circle: public Shape{
public:
	double radius;
	Circle(double r): radius(r){}
	double calculateArea() const override{
		return 3.14 * radius * radius;
    }
};

class Rectangle: public Shape{
public:
	double width, height;
	Rectangle(double w, double h): width(w), height(h){}
	double calculateArea()const override{
		return width * height;
    }
};

void printArea(const Shape& shape){
	std::cout << "Area: " << shape.calculateArea() << std::endl;
}
더보기

-. virtual

: 해당 함수가 가상함수(Virtual Function)임을 의미하며, 기본 Class(Base Class)에서 선언된 이 함수는 파생 Class(Derived Class)에서 재정의 할 수 있음.

동일한 기본 Class를 상속 받더라도 파생 Class 마다 가상함수의 구현부를 다르게 정의할 수 있으며, 이를 통해 다형성 실현

 

-. virtual double calculateArea() const = 0;

: 함수의 구현부가 없음을 명시하며, 이렇게 선언된 함수는 순수 가상함수(Pure Virtual Function)로 선언되어 반드시 파생 Class에서 구현되어야 함

 

-. 순수 가상함수(Pure Virtual Function)

: 기본 Class에서는 Interface만 제공하고 실제 구현은 파생 Class에서 정의하도록 강제함.

이 경우 기본 Class는 추상 Class(Abstract Class)로 변환되며, 추상 Class는 Interface화 할 수 없음

 

-. virtual ~Shape() = default;

: 소멸자(Destructor)로 객체가 삭제되거나 수명이 끝날 때 호출되는 Class의 특별한 member function으로, Class의 자원을 정리하거나 Memory 해제를 위해 사용

소멸자는 함수의 일정이므로 함수 정의처럼 이름 뒤에 ( )를 사용하고, 매개변수를 가질 수 없으므로 항상 빈 괄호로 선언

 

-. override

: 해당 함수가 순수 가상함수인지 여부와 상관 없이 부모 Class에서 선언된 가상함수를 재정의한다는 것을 명시적으로 나타냄

부모 Class에서 virtual로 선언된 함수만 override keyword로 재정의 가능하며, override keyword를 사용하지 않아도 재정의는 가능하나, override를 사용하면 Compiler가 재정의 여부를 확인하고 함수명이나 Signature가 다르면 Compile Error 발생

 

[ 참고 ] 상속과 선언의 상관 관계

더보기

-. 부모 Class에서 virtual로 선언된 함수: 자식 Class에서 선택적으로 재정의 가능하며, 재정의하지 않는 경우 부모 Class의 구현 그대로 사용

-. 부모 Class에서 순수가상함수로 선언된 함수: 자식 Class에서 반드시 선언 및 구현이 필요하며 자식 Class가 이를 구현하지 않으면 자식 Class도 추상 Class가 되며 Instance 화 불가능

class Work{
public:
	virtual void work1(){ // 기본 구현 제공
    	std::cout << "Base work!" << std::endl;
    }
    virtual void work2() = 0;
};

class Human: public Work{
	// work1()을 선언 및 구현하지 않아도 Error 없음
    // work2()는 선언해주지 않으면 compile error 발생
    void work2() override{
    	std::cout << "I am working!" << std::endl;
    }
};

int main(){
	Human h;
    h.work1(); // 부모 Class의 work() 호출: "Base work!"
    h.work2(); // "I am working!"
    return 0;
}

 

3.3 LSP(Liskov Substitution Principle, 리스코프 치환 원칙)

: 자식 Class는 부모 Class의 역할을 대체할 수 있어야 하며, 부모 Class의 동작을 깨뜨리거나 위반하지 않는 설계 필요

 

ERROR CASE

: Penguin Class가 Bird의 역할을 대체하지 못함

class Bird{
public:
	virtual void fly(){
		std::cout << "I can fly!" << std::endl;
	}
};

class Penguin: public Bird{
public:
	void fly() override{
		throw std::logic_error("Penguin can't fly!");
	}
};

 

GOOD CASE

: Bird와 FlyingBird로 역할 분리

class Bird{
public:
	virtual void eat(){
    	std::cout << "I can eat!" << std::endl;
    }
    virtual ~Bird() = default; // 소멸자
};

class FlyingBird: public Bird{
public:
	virtual void fly(){
    	std::cout << "I can fly!" << std::endl;
    }
};

class Penguin: public Bird(){
public:
	void eat() override{
    	std::cout << "I am a penguin and I eat fish!" << std::endl;
    }
};

class Sparrow: public FlyingBird{
public:
	void fly() override{
    	std::cout << "I am a sparrow and I can fly!" << std::endl;
    }
};

[ 참고 ] 소멸자(Destructor)

더보기

객체가 삭제되거나 수명이 끝날 때 호출되는 Class의 특별한 member 함수로, Class의 자원을 정리하거나 memory 해제를 위해 사용

소멸자는 함수의 일종이므로 함수 정의처럼 이름 뒤에 ( )를 사용하고 매개변수를 가질 수 없으므로 항상 빈 괄호로 선언

 

가상 소멸자(Virtual Destructor)

: Class가 상속될 때 자식 Class에서 자원을 적절히 해제하도록 설계된 소멸자로, 부모 Class의 소멸자를 virtual로 선언하면 부모 Class를 통해 파생 Class 객체를 삭제할 때 파생 Class의 소멸자도 호출

소멸자를 virtual로 선언하면 상속 받는 Class에서 동적 할당된 객체가 안전하게 삭제되며, 부모 Class의 소멸자가 가상함수가 아니면 부모 Class의 pointer로 파생 Class 객체를 삭제할 때 부모 Class의 소멸자만 호출하고 파생 Class의 소멸자는 호출되지 않아 memory 누수 발생 가능

→ default를 사용해 compiler에게 기본 소멸자를 생성하도록 지시해 개발자가 특별히 소멸자의 구현을 작성하지 않아도 기본 동작(자원 해제)을 자동으로 수행

 

3.4 ISP(Interface Segregation Principle, 인터페이스 분리의 원칙)

: Client는 자신이 사용하지 않는 Method에 의존하지 않아야 하며, 큰 Interface를 작고 구체적인 Interface로 분리

 

ERROR CASE

: 로봇은 eat() Method가 필요하지 않지만 Worker Interface 때문에 구현해야 함

class Worker{
public:
	virtual void work() = 0;
	virtual void eat() = 0;
};

class Robot: public Worker{
public:
	void work() override{
    	std::cout << "I am working!" << std::endl;
    }
    void eat() override{
    	// 로봇은 eat를 사용할 필요가 없음
    }
};

 

GOOD CASE

: Interface를 분리해 필요하지 않은 의존성 제거

class Workable{
public:
	virtual void work() = 0;
    virtual ~Workable() = default;
}

class Eatable{
public:
	virtual eat() = 0;
    virtual ~Eatable() = default;
};

class Human: public Workable, public Eatable{
public:
	void work() override{
    	std::cout << "I am working!" << std::endl;
    }
    void eat() override{
    	std::cout << "I am eating!" << std::endl;
    }
};

class Robot: public Workable{
public:
	void work() override{
    	std::cout << "I am working!" << std::endl;
    }
};

 

3.5 DIP(Dependency Inversion Principle, 의존관계 역전의 원칙)

: 상위 모듈은 하위 모듈에 의존해서는 안되며 둘 다 추상화에 의존해야 하며, 구체 Class 대신 Interface나 추상 Class에 의존하는 설계 지향

 

ERROR CASE

: Computer Class가 특정 구현(Keyboard, Monitor)에 의존

class Keyboard{};
class Monitor{};

class Computer{
private:
	Keyboard keyboard;
    Monitor monitor;
};

 

GOOD CASE

: Interface에 의존하도록 수정

class InputDevice{
public:
	virtual void input() = 0;
    virtual ~InputDevice() = default;
};

class DisplayDevice{
public:
	virtual void display() = 0;
    virtual ~DisplayDevice() = default;
};

class Keyboard: public InputDevice{
public:
	void input() override{
    	std::cout << "Keyboard Input" << std::endl;
    }
};

class Monitor: public DisplayDevice{
public:
	void display() override{
    	std::cout << "Monitor display" << std::endl;
    }
};

class Computer{
// private으로 선언해 캡슐화 함으로써 외부 코드가 inputDevice와 displayDevice 포인터에 직접 접근하거나 변경하지 못하도록 보호
private:
	InputDevice* inputDevice; // 순수가상함수로 정의된 Interface로, 실제로는 이를 구현한 자식 Class(Keyboard)가 할당됨
    DisplayDevice* displayDevice; // 순수가상함수로 정의된 Interface로, 실제로는 이를 구현한 자식 Class(Monitor)가 할당됨
    
public:
	// 생성자에서 두 개의 포인터 매개변수를 받아 초기화하고, 외부에서 장치 객체를 생성해 전달하므로 Computer Class는 특정 입력/출력 장치의 구현에 직접 의존하지 않음
	Computer(InputDevice* input, DisplayDevice* display): inputDevice(input), displayDevice(display){}
    void use(){
    	// inputDevice와 displayDevice의 동작을 호출하며 inputDevice와 displayDevice가 가리키는 구체적인 구현에 따라 use()의 동작이 달라짐
    	inputDevice->input();
        displayDevice->display();
    }
};

int main(){
	Keyboard keyboard; // Keyboard 객체 생성
    Monitor monitor; // Monitor 객체 생성
    
    Computer computer(&keyboard, &monitor); // Computer 객체는 Keyboard와 Monitor 객체의 주소를 전달받아 초기화
    computer.use(); // 1) keyboard.input() 실행 -> "Keyboard input" 출력
                   //  2) monitor.display() 실행 -> "Monitor display" 출력
    return 0;
}

* 캡슐화

더보기

Class의 내부 구현(Data와 Method)을 숨기고, 외부에서 접근할 수 있는 공용 Interface (public)만 제공하는 원칙으로 내부 상태를 외부로부터 보호해 Class 사용자가 필요 이상으로 내부 구조를 알 필요 없도록 설계

-. Data 보호: 외부 코드가 inputDevice와 displayDevice Pointer에 직접 접근하거나 변경하지 못하도록 보호하며 잘못된 수정으로 인한 예기치 않은 동작 방지

-. 내부구현 변경에 유연: inputDevice와 displayDevice가 private member라면 내부 구현을 변경해도 외부 코드에 영향을 주지 않음

-. 명확한 책임 분리: Computer Class는 입력 장치와 출력 장치를 관리하고 사용하는 책임을 가지며 외부에서 직접 member 변수에 접근하면 책임이 모호해 질 수 있음

 

[ 참고 ] 객체 생성 시 Life Cycle

더보기

부모 Class의 객체가 별도로 생성되었다면 부모 Class의 Life Cycle은 자식 Class와 독립적이나, 자식 Class 객체는 반드시 부모 Class의 Life Cycle 내에서 동작

 

1) 부모 Class와 자식 Class의 객체가 별도로 생성된 경우

: 두 객체는 서로 다른 Memory 공간에 존재하므로 부모 Class 객체를 삭제해도 자식 Class의 객체에는 아무런 영향을 미치지 않음

#include <iostream>
using namespace std;

class Parent{
public:
	Parent() {cout << "Parent Constructor" << endl;}
    ~Parent() {cout << "Parent Destructor" << endl;}
};

class Child: public Parent{
public:
	Child() {cout << "Child Constructor" << endl;}
    ~Child() {cout << "Child Destructor" << endl;}
};

int main(void){
	Parent* parentObj = new Parent(); // 부모 Class 객체 생성
    Child* childObj = new Child(); // 자식 Class 객체 생성
    delete parentObj; // 부모 객체 삭제(Child는 여전히 유효)
    child Obj -> ~Child(); // 수동으로 자식 객체 소멸
    return 0;
}

 

결과

Parent Constructor
Parent Constructor
Child Contructor
Parent Destructor
Child Destructor
Parent Destructor

 

2) 부모 Class Pointer로 자식 Class를 관리하는 경우

: 부모 Class Pointer를 사용해 자식 Class 객체를 참조하고 있다면 부모 Class Pointer로 객체를 삭제할 때 부모 Class의 소멸자만 호출

이 때, 부모 Class의 소멸자가 가상소멸자(virtual)로 선언되지 않았다면 자식 Class의 소멸자가 호출되지 않아 메모리 누수 발생 가능

int main(){
	Parent* parentPtr = new Child(); // 부모 Pointer로 자식 객체 참조
    delete parentPtr; // 부모 포인터로 객체 삭제
    return 0;
}

 

결과

Parent Constructor
Child Constructor
Parent Destructor

 

3) 가상소멸자 사용하는 경우

: 부모 Class의 소멸자를 가상소멸자(virtual)로 선언하면 부모 Pointer를 사용하더라도 자식 Class의 소멸자가 먼저 호출되며, 상속 관계에서 객체를 안전하게 삭제하기 위한 권장 방법

class Parent{
public:
	Parent() {cout << "Parent Constructor" << endl;
    virtual ~Parent() {cout << "Parent Destructor" << endl;}
};

class Child: public Parent{
public:
	Child() {cout << "Child Constructor" << endl;
    ~Child() {cout << "Child Destructor" << endl;
};

int main(){
	Parent* parentPtr = new Child(); // 부모 Pointer로 자식 객체 참조
    delete parentPtr; // 부모 Pointer로 객체 삭제
    return 0;
}

 

결과

Parent Constructor
Child Constructor
Child Destructor
Parent Destructor

 

4) Smart Pointer를 사용한 객체의 Life Cycle 관리

: C++11 부터 제공되는 Smart Pointer(std::shared_ptr, std::unique_ptr)를 사용하면 객체의 Life Cycle을 안전하게 관리 가능

 

4 GoF(Gang of Four)

: Erich Gamma, Richard Helm, Ralph Johnson, John Vissides의 저서인 'GoF의 디자인패턴'을 가리키는 용어로, 처음 소프트웨어 공학에서 사용되는 패턴을 정리한 사람들의 별칭

객체지향의 문제점을 분석해 24개의 패턴으로 분류

 

5 패턴의 요소

: 24개로 분리된 디자인 패턴은 공통된 4가지 요소를 가짐

 

5.1 이름(Pattern Name)

: 패턴 이름을 통해 피턴의 용도를 직관적으로 이해 가능

 

5.2 문제(Problem)

: 각 패턴은 해결하고자 하는 문제를 갖고 있으며, 이러한 문제들은 패턴 적용을 고려해야 하는 시점을 암시

 

5.3 해법(Solution)

: 문제점을 인식했다면 객체 요소 간 관계를 정리하고 객체들을 추상화 및 나열함으로써 해결 방법을 찾음

 

5.4 결과(Consequence)

: 디자인 패턴은 알려진 문제점들을 해결하는 데 효과적이지만 모든 코드에서 디자인 패턴이 유용하다고 볼 수는 없으므로 꼭 필요한 경우를 생각해 적절히 분배해 사용해야 함

 

6 디자인 패턴의 사용 목적

6.1 통일성

: 디자인 패턴은 개발 방법을 정의함으로써 보다 통일화된 코드를 작성할 수 있음

 

6.2 실체화(Reification)

: 구현(Implementation) 보다는 디자인을 의미하며, 구조만으로 패턴을 파악하는 것은 불가능하므로 의도를 파악하는 것이 중요

 

6.3 유지보수 및 방어적 설계

: 향후 추가되는 코드를 수정하기 쉽게 변경할 수 있도록 구현해야 하며, 오랫동안 문제 없이 유지보수를 하기 위해 변경 가능한 디자인(Design for change)으로 설계해야함

코드를 작성하면서 어떤 부분이 향후 수정될 것으로 예측된다면 해당 기능을 방어적으로 처리할 수 있도록 코드가 설계되어야 하며, 지속적인 리팩토링 작업을 수행해 주어야 함

 

디자인 패턴 vs 처리 성능
성능 최적화를 위해서는 많은 함수 호출과 객체 간 호출이 적을 수록 좋으나 디자인 패턴에서는 코드의 가독성과 유지보수를 위해 객체의 메소드를 분리하며 호출도 자주 발생하게 되므로 패턴을 너무 많이 사용하는 경우 잦은 메소드 호출로 인해 성능이 저하될 수 있음
반응형

'SW > Design Pattern' 카테고리의 다른 글

1. 생성패턴 - Factory Pattern  (0) 2025.02.10
반응형

단지번호붙이기

티어 : Silver 1
시간 제한 : 1 초
메모리 제한 : 128 MB
알고리즘 분류 : 그래프 이론, 그래프 탐색, 너무 우선 탐색, 깊이 우선 탐색

 

문제

<그림 1>과 같이 정사각형 모양의 지도가 있다. 1은 집이 있는 곳을, 0은 집이 없는 곳을 나타낸다. 철수는 이 지도를 가지고 연결된 집의 모임인 단지를 정의하고, 단지에 번호를 붙이려 한다. 여기서 연결되었다는 것은 어떤 집이 좌우, 혹은 아래위로 다른 집이 있는 경우를 말한다. 대각선상에 집이 있는 경우는 연결된 것이 아니다. <그림 2>는 <그림 1>을 단지별로 번호를 붙인 것이다. 지도를 입력하여 단지수를 출력하고, 각 단지에 속하는 집의 수를 오름차순으로 정렬하여 출력하는 프로그램을 작성하시오.

 

 

 

입력

첫 번째 줄에는 지도의 크기 N(정사각형이므로 가로와 세로의 크기는 같으며 5≤N≤25)이 입력되고, 그 다음 N줄에는 각각 N개의 자료(0혹은 1)가 입력된다.

 

출력

첫 번째 줄에는 총 단지수를 출력하시오. 그리고 각 단지내 집의 수를 오름차순으로 정렬하여 한 줄에 하나씩 출력하시오.

 

예제 입출력


Algorithm

BFS
1. 그래프 구현
2. 단지에 포함되어있고(graph[x][y] == 1), 방문한 적이 없다면(not visited[x][y]) bfs 수행
-> 이 때 home_cnt 라는 리스트에(각 단지의 아파트 개수) 마지막 원소(0) append
3. bfs 수행하면서 아파트 하나씩 만날 때 마다 home_cnt의 마지막 원소 증가
4. bfs 수행 완료 후 home_cnt의 마지막 원소가 0인 경우(시작 위치 제외하고 만난 아파트가 없는 경우) manually 1 증가
5. home_cnt 리스트 sort 후 length, 원소 하나씩 출력

 

Code

from collections import deque
queue = deque()
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]

def bfs(graph, sx, sy, visited):
    queue.append((sx, sy))

    while queue:
        x, y = queue.popleft()

        for dir in range(4):
            nx = x + dx[dir]
            ny = y + dy[dir]

            # graph 벗어나면 continue
            if nx < 0 or nx > N-1 or ny < 0 or ny > N-1:
                continue

            # 0이면 continue
            if not graph[nx][ny]:
                continue

            # 방문한 적 있으면 continue
            if visited[nx][ny]:
                continue

            # 방문 기록
            cnt_home[-1] += 1
            visited[nx][ny] = True
            queue.append((nx,ny))
    
N = int(input())
graph = []
for i in range(N):
    graph.append(list(map(int, input())))

cnt_home = []
visited = [[False for _ in range(N)] for _ in range(N)]
for x in range(N):
    for y in range(N):
        queue.clear()
        if graph[x][y] and not visited[x][y]:
            cnt_home.append(0)
            bfs(graph, x, y, visited)
            if not cnt_home[-1]:
                cnt_home[-1] += 1

cnt_home.sort()
print(len(cnt_home))
for i in cnt_home:
    print(i)

메모리: 34184 KB
시간: 64 ms

반응형

'백준 > Python' 카테고리의 다른 글

[백준 2178] 미로 탐색 Python  (0) 2023.09.10
[백준 11403] 경로 찾기 Python  (0) 2022.07.09
[백준 1080] 행렬 Python  (0) 2022.07.09
[백준 15903] 카드 합체 놀이 Python  (0) 2022.07.06
[백준 2583] 영역 구하기 Python  (0) 2022.07.06
반응형

미로 탐색

티어 : Silver 1
시간 제한 : 1 초
메모리 제한 : 192 MB
알고리즘 분류 : 그래프 이론, 그래프 탐색, 너비 우선 탐색

 

문제

N×M크기의 배열로 표현되는 미로가 있다.

1 0 1 1 1 1
1 0 1 0 1 0
1 0 1 0 1 1
1 1 1 0 1 1

미로에서 1은 이동할 수 있는 칸을 나타내고, 0은 이동할 수 없는 칸을 나타낸다. 이러한 미로가 주어졌을 때, (1, 1)에서 출발하여 (N, M)의 위치로 이동할 때 지나야 하는 최소의 칸 수를 구하는 프로그램을 작성하시오. 한 칸에서 다른 칸으로 이동할 때, 서로 인접한 칸으로만 이동할 수 있다.

위의 예에서는 15칸을 지나야 (N, M)의 위치로 이동할 수 있다. 칸을 셀 때에는 시작 위치와 도착 위치도 포함한다.

 

입력

첫째 줄에 두 정수 N, M(2 ≤ N, M ≤ 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.

 

출력

첫째 줄에 지나야 하는 최소의 칸 수를 출력한다. 항상 도착위치로 이동할 수 있는 경우만 입력으로 주어진다.

 

예제 입출력


Algorithm

BFS
1. 그래프 구현
2. BFS 돌면서 문제 조건에 따라 갈 수 없는 곳이 아니라면 큐에 모두 추가
3. 도착지점에 도착하면 이동 횟수 return

 

Code

from collections import deque
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]

def bfs(graph, sx, sy, ex, ey, visited):
    queue = deque()
    queue.append((sx, sy))

    while queue:
        x, y = queue.popleft()

        for dir in range(4):
            nx = x + dx[dir]
            ny = y + dy[dir]

            # 도착지에 도착하면 return
            if nx == ex and ny == ey:
                return visited[x][y] + 1
            
            # graph 밖으로 나가면 continue
            if nx < 0 or nx > N-1 or ny < 0 or ny > M-1:
                continue

            # 방문한 적 있으면 continue
            if visited[nx][ny]:
                continue

            # 0이면 continue
            if not graph[nx][ny]:
                continue

            # 방문 기록
            visited[nx][ny] = visited[x][y] + 1
            queue.append((nx, ny))

N, M = map(int, input().split())

graph = []
visited = [[0 for _ in range(M)] for _ in range(N)]
for _ in range(N):
    graph.append(list(map(int, input())))

print(bfs(graph, 0, 0, N-1, M-1, visited)+1)

메모리: 34168 KB
시간: 76 ms

반응형
반응형

깊이 우선 탐색(DFS, Depth First Search)

특정 Data를 탐색할 때 깊이가 깊은 것을 우선적으로 탐색하는 알고리즘으로 Stack 자료구조를 사용

사실 Stack을 사용하지 않아도 구현이 가능한데, 이는 컴퓨터가 구조적으로 항상 Stack의 원리를 사용하기 때문이다.

 

깊이 우선 탐색(DFS) Sequence

더보기

Step 1: 시작 Node를 Stack에 넣고 방문 처리

Step 2: Stack의 최상단 Node 확인

Step 3: 최상단 Node에 방문하지 않은 인접 Node가 있다면 그 Node를 Stack에 넣고 방문 처리

           방문하지 않은 인접 Node가 없으면 Stack에서 최상단 Node 삭제

Step 4: Step 2로 이동

 

Code

#include <iostream>
#include <stack>
#include <vector>
using namespace std;

#define MAX 7
int visited[7];
vector <int> a[8];

void dfs(int x){
    // 이미 방문한 적 있으면 return
    if(visited[x]) return;

    // 방문 처리
    visited[x] = true;
    cout << x << " ";

    // 인접 노드 방문
    for(int i=0;i<a[x].size();i++){
        int y = a[x][i];
        dfs(y);
    }
}

int main(void){
    a[1].push_back(2);
    a[1].push_back(3);

    a[2].push_back(1);
    a[2].push_back(3);
    a[2].push_back(4);
    a[2].push_back(5);

    a[3].push_back(1);
    a[3].push_back(2);
    a[3].push_back(6);
    a[3].push_back(7);

    a[4].push_back(2);
    a[4].push_back(5);

    a[5].push_back(2);
    a[5].push_back(4);

    a[6].push_back(3);
    a[6].push_back(7);

    a[7].push_back(3);
    a[7].push_back(6);

    dfs(1);
}

출력

1 2 3 6 7 4 5
반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 너비 우선 탐색(BFS, Breadth First Search)  (0) 2023.04.09
[Algorithm] 큐(Queue)  (0) 2023.04.09
[Algorithm] 스택(Stack)  (0) 2023.04.09
[Algorithm] 심화 정렬 문제  (0) 2023.04.09
[Algorithm] 계수 정렬(Counting Sort)  (0) 2023.04.09
반응형

너비 우선 탐색(BFS, Breadth First Search)

특정 Data를 탐색할 때 너비를 우선으로 하여 탐색을 수행하는 알고리즘

최단 경로를 찾아준다는 점에서 최단 길이를 보장해야할 때 많이 사용되며, Queue를 이용한다.

e.g., 미로 찾기

 

너비 우선 탐색(BFS) Sequence

더보기

Step 1: 시작 Node를 큐에 삽입하고 방문 처리

Step 2: Queue에서 하나의 Node를 꺼냄

Step 3: 연결된 Node 중 방문하지 않은 Node를 방문하고 큐에 삽입

Step 4: Step 2로 이동

∴ BFS 수행 순서: 1 2 3 4 5 6 7

 

Code

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

#define MAX 7
int visited[7]; // 방문 유무를 표시할 배열
vector<int> a[8];

void bfs(int start){
    queue<int> q;

    // 시작 노드 큐에 추가
    q.push(start);
    visited[start] = true;

    // 큐가 빌 때까지 반복
    while(!q.empty()){
        int x = q.front(); q.pop();
        cout << x << " ";

        // 현재 큐에서 꺼낸 Node와 인접한 노드 모두 방문
        for(int i=0;i<a[x].size();i++){
            int y = a[x][i];
            // 방문한 적 없는 노드이면 큐에 추가 후 방문 처리
            if(!visited[y]){
                q.push(y);
                visited[y] = true;
            }
        }
    }
}

int main(void){
    a[1].push_back(2);
    a[1].push_back(3);

    a[2].push_back(1);
    a[2].push_back(3);
    a[2].push_back(4);
    a[2].push_back(5);

    a[3].push_back(1);
    a[3].push_back(2);
    a[3].push_back(6);
    a[3].push_back(7);

    a[4].push_back(2);
    a[4].push_back(5);

    a[5].push_back(2);
    a[5].push_back(4);

    a[6].push_back(3);
    a[6].push_back(7);

    a[7].push_back(3);
    a[7].push_back(6);

    bfs(1);

    return 0;
}

 

출력

1 2 3 4 5 6 7
반응형

'Algorithm' 카테고리의 다른 글

[Algorithm] 깊이 우선 탐색(DFS, Depth First Search)  (0) 2023.04.09
[Algorithm] 큐(Queue)  (0) 2023.04.09
[Algorithm] 스택(Stack)  (0) 2023.04.09
[Algorithm] 심화 정렬 문제  (0) 2023.04.09
[Algorithm] 계수 정렬(Counting Sort)  (0) 2023.04.09
반응형

큐(Queue)

먼저 들어온 Data가 먼저 처리되는 선입선출(First In First Out, FIFO), 후입후출(Last In Last Out, LILO) 자료구조

 

큐(Queue) Sequence

더보기

명령: 삽입(7) → 삽입(5) → 삽입(4) → 삭제( ) → 삽입(6) → 삭제( )

삽입 7
삽입 5
삽입 4
삭제
삽입 6
삭제

 

Code

#include <iostream>
#include <queue>
using namespace std;

int main(void){
    queue<int> q;
    q.push(7);
    q.push(5);
    q.push(4);
    q.pop();
    q.push(6);
    q.pop();
    while(!q.empty()){
        cout << q.front() << " ";
        q.pop();
    }
    return 0;
}

출력

4 6
반응형
반응형

스택(Stack)

가장 마지막에 들어온 Data가 가장 먼저 처리되는 후입선출(Last In First Out, LIFO), 선입후출(First In Last Out, FILO) 자료구조

 

Stack 동작 Sequnce

더보기

명령: 삽입 (7) → 삽입(5) → 삽입(4) → 삭제() → 삽입(6) → 삭제()

삽입 7
삽입 5
삽입 4
삭제
삽입 6
삭제

 

Code

#include <iostream>
#include <stack>
using namespace std;

int main(void){
    stack<int> s;
    s.push(7);
    s.push(5);
    s.push(4);
    s.pop();
    s.push(6);
    s.pop();
    while(!s.empty()){
        cout << s.top() << " ";
        s.pop();
    }
    return 0;
}

출력

5 7
반응형

+ Recent posts