[C++] 클래스(Class)(3) 인터페이스(Interface)로서의 클래스 - 2

Gamchan Kang·2023년 4월 12일
0

C++

목록 보기
9/11

1. static

1-1 static vs local vs global

가장 헷갈리는 개념 중 하나가 위 세 키워드의 차이점이다. 결론부터 말하자면 static은 컴파일 타임(compile-time)에서 메모리에 저장(reserved)되고, 나머지 두 키워드는 local, global은 런타임(runtime)에서 메모리에 저장된다. C++은 컴파일러 언어이므로 컴파일 타임과 런타임이 분리됐는데, 이때문에 static 인스턴스가 global처럼 보이는 것이다.


1-2 static member

클래스에서 static을 쓰는 경우는 두가지로 나눌 수 있다. 멤버 혹은 클래스 자체를 static으로 줄 수 있다. 우선 멤버를 주는 경우를 보면 다음과 같다.

class StaticMemberClass {
	...
	static int staticMember;	// private, public 여부는 상관 없다
    ...
}

int StaticMemberClass::staticMember = 3;	// 초기화는 global 스코프로

여기서 조심할 내용은 초기화는 스코프 외부에서 된다는 점이다. 만약 클래스가 헤더 파일에 선언이 됐다면, 외부 소스 파일에서 초기화해야 한다.

이전에 예시를 든 직교좌표계를 보면 다음과 같다.

#include <iostream>
#include <vector>

class Point {
private:
    int x, y;
    static int pointCount;
public:
    Point(int x, int y) {
        this->x = x; this->y = y;
        Point::pointCount++;		// 새로운 객체가 생성될때마다 1씩 증가
    }
    static int getPointCount(){		// static은 private, public 상관없이 선언 할 수 있다.
    	return Point::pointCount;	// this->pointCount가 아니다.
    }
    void printPoint() {std::cout << "(" << this->x << ", " << this->y << ")" << std::endl;}
};

int Point::pointCount = 0;

int main() {
    std::vector<Point> points;

    for(int i=0; i<10; i++) {
        Point p(i, i);
        points.push_back(p);
        p.printPoint();
    }

	// static 멤버를 호출할 때 private 멤버는 여전히 호출할 수 없다.
    // public 멤버는 클래스::멤버로 호출한다.
    std::cout << "Point Count: " << Point::getPointCount() << std::endl;

    return 0;
}

위 코드를 간단히 리뷰하자면, point 객체가 선언될 때마다 생성자(constructor)로 pointCount가 1씩 증가한다. 컴파일 타임에 pointCount 변수가 할당됐으므로, pointCount 는 클래스 생성/소멸에 상관 없이 동작한다.


2. 싱글톤(singleton)

static 인스턴스는 시스템에서 유일하다. 컴파일 타임에서 메모리에 할당되고 런타임때는 다른 객체와 관련 없이 존재한다.(초기화는 런타임에서 한다.) 그렇다면 클래스 인스턴스가 static한 경우 즉, 그 시스템에서 유일한 경우는 어떨까? 클래스 인스턴스를 유일하게 만든 디자인 패턴이 싱글톤이다.


2-1 장단점

Pros 장점 Cons 단점
Global access: allowing all objects in the system to use the same instance
시스템 내 모든 객체가 같은 인스턴스에 접근
Controlled access:
multiple threads or processes need to access a shared resource.
멀티 스레드/프로세스가 공유 리소스에 접근
Hidden dependencies:
all objects that depend on the Singleton instance must reference it directly.
싱글톤 인스턴스 의존 객체는 직접 참고 해야함.
Memory management:
ensuring that there is only one instance of the resource that is shared among multiple objects.
다수 객체가 공유해도 리소스에 한 싱글톤 객체만 존재함.
Unit testing:
making it hard to substitute the Singleton instance with a mock object for testing.
테스트를 위해 대리 인스턴스를 만들기 힘듦
Lazy initialization:
can be created lazily, can save memory and resources.
늦게 만들어 질 수 있어, 메모리와 리소스 절약
Thread safety:
have to be accessed and modified correctly to avoid data races and synchronization issues.
데이터 레이스와 동기회 문제를 피하기 위해 정확히 접근/변경해야 함.


2-2 패턴

class Singleton {		// C++는 static 클래스를 선언할 수 없다. 객체만 가능.
	
    static Singleton s;	// private/ public 무관하다.
    // 생성자/소멸자/복사/이동 정의 고려
    // operator delete
    public:
    	Singleton& setSingleton(int a) {
        	static Singleton s
            // 다른 private 멤버 초기화
            return s;
        }
        ...
};

싱글톤 클래스를 제대로 이해하기 위해서는 생성자/소멸자를 정확히 알아야 한다. 싱글톤 클래스 객체가 선언되면, 일반 기본 생성자가 호출되어 객체를 생성한다. 문법으로 보기에는 마치 재귀(recursion)와 같이 객체와 멤버 객체로 코드가 구성된다.

나 ❓

Imagine that class instance using singleton design pattern. if I deconstruct this instance, does static instance remain in stack

🇰🇷 싱글톤 디자인 패턴을 사용하는 클래스 인스턴스를 가정하자. 내가 이 객체를 소멸하면, static 인스턴스는 스택 영역에 남아있는가?

ChatGPT ↩️

In the singleton design pattern, there is only one instance of the class throughout the lifetime of the program, and it is typically implemented using a static member variable.

🇰🇷 싱글톤 패턴에서는 프로그램 수명동안 인스턴스는 하나만 존재하며 이는 통상 static 멤버 변수로 구현된다.


When the program terminates or the singleton instance is destructed, the static instance is destroyed along with the rest of the static variables. This means that the memory used by the static instance is freed, and the variable is no longer accessible.

🇰🇷 프로그램을 종료하거나 static 인스턴스가 소멸하면, 다른 static 변수와 같이 파괴된다. 이 말은
  1. static 인스턴스를 사용하는 메모리가 해제되고,
  2. 변수에 더 이상 접근할 수 없음
  을 의미한다.


If the singleton instance is destructed explicitly by calling its destructor, the static instance is also destructed along with it. If the singleton instance is destructed implicitly, such as when the program terminates, the static instance is also destructed along with all the other static variables.

🇰🇷 소멸자 호출로 명시적으로 소멸하면, static 인스턴스도 같이 소멸한다. 프로그램 종료와 같이 암묵적으로 소멸하면 다른 static 변수와 같이 static 인스턴스도 같이 소멸한다.


However, it is important to note that if the singleton instance creates any dynamically allocated memory or resources, those resources must also be released before the instance is destroyed. Failure to do so can result in memory leaks or other issues.

🇰🇷 만약 싱글톤 인스턴스가 동적 할당된 메모리나 리소스에 만들어졌을 경우, 인스턴스를 없애기 전 이런 리소스를 해제해야 한다. 잘못하면 메모리 누수나 다른 문제가 생길 수 있다.


In summary, when a singleton instance is destructed, its static member variable is also destroyed and the memory used by the variable is freed.

🇰🇷 요약하자면, 싱글톤 객체가 소멸할 때, static 멤버 변수 또한 없어지고 메모리는 해제된다.


아직 생성자와 소멸자, 복사, 이동, 연산자 오버로딩에 대한 공부가 부족해서 자세한 내용은 향후 포스팅에서 다룰 예정이다.


3. 빌더(builder)

클래스에 초기화할 내용이 너무 많을 경우에는 어떨까? 혹은 같은 내용으로 다른 객체를 만든다면? 빌더는 인스턴스 생성을 위한 일종의 조립 설명서 객체이다.
builder
위 사진으로 예시를 들면 method1은 instance의 component1을 초기화하고 method2는 component2를 method3은 component3을 초기화한다. 이를 코드로 나타내면 다음과 같다.

빌더 패턴에서 조립품은 여러가지이지만, 조립 설명서는 유일하기 때문에 싱글톤/모노스테이트로 빌더를 디자인하기도 한다.


3-1 장단점

Pros Cons
Provides a clear separation of concerns between the construction process and the representation of the object being constructed.
생성 과정과 생성된 객체의 표현을 명확하게 구분
Increases code complexity by requiring the creation of additional classes and interfaces.
추가 클래스와 인터페이스가 필요 -> 코드가 복잡해진다.
Allows for more readable and maintainable code by providing a step-by-step approach to creating an object.
객체를 만들때 단계별 접근 -> 유지/보수에 용이하고 가독성이 좋음
Can be overkill for simple object creation scenarios, where the object can be easily constructed using a constructor.
쉬운 생성자에서 빌드 사용은 과도함
Provides more control over the object creation process by allowing the construction of complex objects to be broken down into smaller, more manageable steps.
복잡한 객체 생성를 단계별로 쪼갬 -> 각 단계가 다루기 쉬워지고 생성 과정을 제어할 수 있음
Requires more code to be written, which may result in increased development time and cost.
더 많이 코드를 작성해야함 -> 개발 시간, 비용이 증가함
Provides flexibility in the order and type of construction steps by allowing different variations of the object to be created.
객체 생성에 변주를 줄 때 유연성 증가
May not be suitable for all object creation scenarios, particularly those where the construction of the object is not complex or where the order of construction steps is fixed.
객체가 복잡하지 않거나, 생성 과정이 고정된 객체에는 적합하지 않음


2-2 패턴

class instance {
private:
	int component1, component2, component3;
public:
	friend class builder;
}

class builder {
private:
	instance inst;
public:
	builder& method1(int a) {
    	this->inst.component1 = a;
        return *this;
    }
	builder& method2(int b) {
    	this->inst.component2 = b;
        return *this;
    }
	builder& method3(int c) {
    	this->inst.component3 = c;
        return *this;
    }
}

int main() {
	...
    builder b;
    b.method1(1).method2(2).method3(3);
    ...
}
나 ❓

what is the situation needed multiple builder process in single instance?

🇰🇷 한 객체를 다른 빌더로 지어야 할 경우는 어떤 상황인가?

ChatGPT ↩️

The situation where multiple builder processes are needed for a single instance typically arises when the object being constructed has multiple configuration options or variations that need to be set during the construction process.

🇰🇷 구성 옵션,변수가 많아 생성 과정에서 설정돼야하는 경우이다.


For example, consider a car manufacturing factory that produces cars with different configurations and options such as engine type, transmission, interior finish, and exterior color. To create a car with a specific set of options, multiple builders may be needed to construct the different components of the car, and then assemble them together into a final product.

🇰🇷 예시 1. 자동차 생산 공장
  자동차 생산에 옵션이 많음; 엔진 종류, 변속기, 인테리어 마감, 외부 색상
여러 가지 옵션을 만족하는 자동차를 생산하기 위해 여러가지 빌더가 필요하다. 그리고 옵션을 조립해 최종 완성품으로 만든다.


Another example is when constructing a complex data structure, such as a tree or a graph, where each node or vertex can have different attributes and properties. In this case, multiple builders may be needed to create different nodes or vertices with specific properties, and then link them together to form the final data structure.

🇰🇷 예시 2. 복잡한 데이터 구조를 생성할 때
  각 노드, 꼭짓점 별로 고유 성질, 속성을 가진다. 이럴 경우 특징에 맞는 빌더가 필요하다. 그리고 각각 노드/꼭짓점을 이어 데이터 구조를 형성한다.


In both of these scenarios, the use of multiple builders can help simplify the construction process and make it more modular and flexible, allowing for different variations of the object to be created easily.

🇰🇷 각 예시에서 다양한 빌더는 생성 과정을 단순하게 만들고, 각 과정이 모듈식이며 유연하다. 따라서 다른 변주를 주는 객체를 쉽게 생성할 수 있다.


profile
Someday, the dream will come true

0개의 댓글