Win32 API/기초

Win32 API 기초 : Singleton (2)

시플마 2024. 6. 24. 08:08

싱글톤 패턴은 여러 방식으로 구현이 가능합니다.

 

아래 코드와 같이 

 

동적 할당을 진행하는 방식과

 

 

또는 아래 코드와 같이

 

CCore 객체를 하나 만들기 위한 GetInstance 함수 안에

정적 변수 g_Inst를 선언하고 해당 변수의 주솟값을

반환하는 방식입니다.

 

g_Inst 가 함수 안에 선언되어 함수 안에서만 접근이

가능하다고 생각할 수 있지만, 주솟값을 반환하기 때문에 

해당 함수 밖에서도 접근이 가능합니다.

 

이것이 포인터의 강력함이죠.

 

 

정적 변수이므로 g_Inst는 data 영역에 존재하게 되는데,

프로그램이 종료되면 알아서 소멸되기 때문에 

메모리 해제를 신경 쓰지 않아도 됩니다.

 

반대로 생각하면 메모리를 해제하고 싶어도 못한다는 것이죠.

 

그래서 큰 문제가 발생할 거 같지만 사실

프로그램에서 싱글톤으로 생성되는 객체는 일반적으로 

매니저 역할을 합니다. 실질적인 리소스를 참조하여

관리하는 형태이므로 이러한 객체의 크기는 크지 않아

메모리 해제를 신경 쓸 필요 없는 data 영역에 객체를 선언하는

방식으로 가도 문제가 없는 것이죠.

 

근데 매니저 성향을 가진 싱글톤 객체더라도 필요할 때

생성하고 필요 없으면 메모리를 해제하는 등 실시간으로

관리하고 싶다면 동적 할당 방식으로 진행하는 것이죠.

 

 


 

 

Core와 관련된 객체는 하나여야겠지만

매니저 형식을 띄는 또 다른 객체가 존재할 수는 있겠죠.

 

즉 싱글톤으로 생성되는 객체는 다양하게 존재할 수 있는 겁니다.

 

그럼 GetInstance 함수를 통해 

싱글톤 객체가 생성되는 과정은 그때마다 반복될 것이므로

함수로 만들어 주면 좋겠죠.

 

위 그림처럼 자료형만 다르게 해 주면 

재사용 가능한 코드가 될 것입니다.

 

 

이를 매크로 함수로 구현해 봅시다.

 

01. Header 필터 안에

 

define.h 파일을 생성합니다.

 

앞으로 해당 파일에 매크로나 정의할 것들을 모아 놓을 겁니다.

 

우선 매크로 함수가 무엇인지 알아봅시다.

 

위 코드에서 ADD가 바로 매크로 함수입니다.

 

전처리기 지시문 #define으로 인해

전처리 과정에서 ADD(a, b)라는 구문을 만나면

a + b로 코드를 치환하고 나서 컴파일 과정이 시작되죠.

 

즉 위 코드는 아래 코드와 같다고 볼 수 있습니다.

 

 

함수와 비슷해 보이죠?

 

Add 함수와 같이 간단한 동작을 하는 함수의 경우,

함수가 호출되고 제거되는 비용이 아깝습니다.

특히 자주 사용되는 함수라면 말이죠.

 

이때 매크로 함수를 사용하면 호출이나 제거 비용 없이

코드를 바로 치환하게 되므로 비용을 아낄 수 있는 것이죠.

 

 

주의할 점이 하나 있습니다.

 

함수와 매크로 함수는 아예 똑같이

동작할 거 같지만 꼭 그렇지 않다는 점입니다.

 

아래 코드를 보시면

 

52번째 줄과 54번째 줄의 코드를 통해 iCal에 

들어가는 값은 다릅니다.

 

52번째 줄에서 Add 함수로 인해 10 + 20 = 30이 

반환될 것이고 여기에 10을 곱해 300이 iCal에 대입(초기화)되겠죠.

 

54번째 줄에서 ADD 매크로 함수도 Add 함수처럼 작동할 거 같지만 

 

매크로 함수는 단순히 치환만 해 주기 때문에 

위 코드와 같다고 봐야 합니다. 

 

그래서 곱셈 연산이 먼저 진행되어 10 * 10 = 100에

20이 더해져 120이 iCal에 대입되겠죠.

 

 

본격적으로 싱글톤 객체를 만들기 위한

GetInstance 함수를 매크로 함수로 구현해 봅시다.

 

define.h 파일에 SINGLE이라는 매크로 함수를 만들었습니다.

 

SINGLE을 통해 type을 받으면 해당 type으로 GetInstance 함수와

같은 내용의 코드로 치환하여 실행하게 됩니다.

( \ (역슬래시)를 사용하면 줄바꿈을 하여도

매크로의 정의가 끊기지 않아, 코드의 가독성을 높일 수 있습니다.)

 

 

Manager 역할을 하는 객체는

모두 싱글톤으로 만들 겁니다.

 

Manager 필터를 만들어서 그 안에 

KeyMgr 필터를 만들어서 키 입력과 관련된

매니저 클래스를 만들어 줄 겁니다.

 

단축키 Ctrl + Shift + X를 통해 클래스 마법사를

열고 CKeyMgr.cpp 파일을 만듭니다.

 

CKeyMgr.cpp 파일에 define.h 파일을 포함시키고

 

만들어진 클래스에 SINGLE 매크로 함수를 이용해

CKeyMgr 자료형으로 이루어진 싱글톤 객체를 만듭니다.

 

 


 

 

define.h 파일에는 매크로나 전처리기 등

모든 파일에서 사용될 요소들을 모아 놓은 곳입니다.

 

어차피 모든 파일에서 사용될 define.h 파일을

전처리기 지시문 include을 통해 하나씩 포함시킬 필요 없이,

 

미리 컴파일된 헤더를 이용하면 됩니다.

 

프로젝트(Client)를 우클릭하여 속성에 들어간 후, 

C/C++에서 미리 컴파일된 헤더를 클릭합니다.

 

그러면 위와 같은 창이 나오는데 "미리 컴파일된 헤더 사용 안 함"을

만들기로 바꾸고 미리 컴파일된 헤더 파일을 pch.h로 수정합니다.

 

그리고 적용 후 확인을 누릅니다.

 

실제로 pch.h 파일이 존재해야 하므로 

pch.h 파일을 만들어 줍니다.

 

만든 pch.h 파일에 

 

define.h 파일을 포함시켜 줍니다.

 

미리 컴파일된 헤더를 설정하면 규칙이 하나 생깁니다.

 

모든 cpp 파일에 pch.h 파일을 include하여

포함시켜야 합니다. 없으면 오류가 발생하죠.

 

모든 cpp 파일에 pch.h 파일을 포함시켰다면

이제 헤더 파일에서 define.h 파일을 포함시키는 

include 구문을 제거해도 잘 작동합니다.

 

이렇게 보면 결국 헤더 파일에 define.h 파일을

포함시킬 필요는 없어졌지만 모든 cpp 파일에

pch.h 파일을 포함시켜야 하니 여전히 번거롭다고

느껴질 수 있지만, Client 속성 페이지에서 미리 컴파일된 헤더를

설정하고 나면 앞으로 클래스 마법사를 통해 생성한

클래스의 cpp 파일은 자동으로 pch.h를 포함하고 있게 됩니다.

 

 

이러한 이유 말고도 미리 컴파일된 헤더를

사용하는 이유는 또 있습니다.

 

미리 컴파일된 헤더를 사용하지 않으면 

각 cpp 파일에 필요한 헤더를 일일이 포함시켰습니다.

 

이러면 완성된 헤더 파일 (윈도우에서 제공하는 표준 라이브러리, 컨테이너 등)

일지라도 다른 파일에서 변경이 발생하면 모두 다시 컴파일하여 검사를 진행합니다.

 

 

미리 컴파일된 헤더를 사용하면

 

모든 cpp 파일이 미리 컴파일된 헤더 파일(pch.h)을 참조하고

미리 컴파일된 헤더 파일에 필요한 헤더 파일을

포함시켜 주기만 하면 되죠. 

 

이때 미리 컴파일된 헤더 파일에 포함된 헤더 파일 중

완성된 헤더 파일은 변경점이 없는 한 다시 컴파일하지 않아

컴파일 속도가 올라가는 장점이 있습니다.

 

 

 

 


 

 

다시 CCore 클래스로 돌아와서

 

초기화 함수 Init을 추가했습니다.

 

반환형이 int인 이유는 초기화에

실패했을 경우 알려주기 위해서입니다.

 

main 함수에서 

 

40번째 줄에 있는 윈도우 생성 구문 이후,

56번째 줄에 있는 메시지 루프 시작 전,

 

그 사이 45번째 줄에서 CCore형 싱글톤 객체를

생성하고 초기화해 줄 겁니다.

 

 

Init 함수의 정의는 CCore.cpp 파일에 합니다.

 

초기화에 성공하면 S_OK를 반환할 겁니다.

 

 

F12를 눌러서 S_OK의 의미를 확인해 보면

 

정수 0이네요. S_FALSE의 경우 정수 1이고요.

 

 

E_FAIL이라는 것도 있습니다.

 

0x가 붙었으므로 16진수임을 알 수 있습니다.

 

또한 최상위 비트가 맨 앞 숫자가 8이므로

 

2진수로 확인해 보면 음수인 것을 알 수 있습니다.

( 10진수에서 숫자 0 ~ 9으로 모든 숫자를 표현할 수 있죠?

마찬가지로 16진수는 0 ~ 15로 수를 표현할 수 있습니다.

4개의 비트를 묶어 숫자 하나를 표현한다고 생각하면 됩니다. )

 

 

윈도우에서 제공하는 매크로 중

 

FAILED 매크로가 있습니다. 

 

이 매크로에 들어오는 hr이라는 값이 0보다 작으면,

즉 음수이면 true를 반환합니다.

 

FAILED(E_FAIL) 와 같은 코드를 작성하면

true가 나올 겁니다. E_FAIL은 음수이니까요.

 

 

윈도우에서 제공하는 함수의 반환값은

보통 HRESULT형입니다. 약자 hr로 표현합니다.

 

8번째 줄의 if 문에서 FAILED 매크로가 실행되면

S_OK는 0이므로 false가 반환될 겁니다. 

 

그럼 이 값이 hr에 반환이 되는 식의 방식이죠.

 

 

FAILED 매크로를 이용하여 

 

초기화 함수인 Init 함수의 성공 여부를 알 수 있도록

코드를 작성하였습니다. 

 

MessageBox라는 매크로가 보입니다.

 

메시지 상자를 띄우는 매크로죠.

 

첫 번째 인자로 윈도우 핸들을 받는데 

메인 윈도우 핸들값을 넣어도 되고

아무것도 안 넣어도 상관없습니다.

 

두 번째 인자는 메시지 상자 속에 

출력될 문자열을 입력하면 됩니다.

 

세 번째 인자는 메시지 상자의 이름입니다.

 

네 번째 인자는 메시지 상자의 옵션입니다.

위 코드처럼 MB_OK를 넣어 주면

메시지 상자에 확인 버튼이 나옵니다.

 

해당 if 문 안의 코드가 실행되었다는 것은

초기화를 실패했다는 의미이므로

프로그램을 종료하기 위해 FALSE를 반환합니다.

 

 

그럼 이제 Init 함수가 

 

실패했을 때 상황을 실험하기 위해 

우선 E_FAIL을 반환하게 하고 

 

 

실행하였더니 

 

위와 같은 메시지 상자가 출력되었습니다.

 

 

 

 


 

 

 

CCore 클래스로 만든 객체의 멤버 함수인 

Init 함수를 통해 무엇을 해 줄 것이냐면

 

윈도우 핸들 정보와 윈도우의 해상도를 초기화해 줄 겁니다.

 

이를 위해 CCore 클래스에 두 개의 멤버 변수를 추가합니다.

 

38번째 줄에 추가된 멤버 변수는

메인 윈도우의 핸들을 저장하는 멤버 변수입니다.

 

39번째 줄에 추가된 멤버 변수는

메인 윈도우의 해상도를 지정하기 위한 멤버 변수죠.

 

 

42번째 줄에서 확인할 수 있듯이

Init 함수가 두 개의 인자를 받도록 수정하였습니다.

 

Init 함수의 정의를 살펴보면

 

인자로 받은 핸들값을 멤버 m_hWnd에 대입하고,

인자로 받은 해상도값을 멤버 m_ptResolution에

대입하면 될 겁니다.

 

물론 아직 완성된 코드는 아니기 때문에

멤버 변수에 값을 넣었다고 해서 실제 윈도우에 

인자로 받은 값이 적용되지는 않습니다.

 

 

 


 

 

main 함수에서 Core 객체를 초기화하고

 

실패한 경우 프로그램을 종료합니다.

 

Core 객체가 정상적으로 초기화되었다면

이후 메시지 while 문으로 들어가게 되겠죠.

 

그리고 메시지가 발생하지 않는 대부분의 시간은

72번째 else 문 안의 코드가 실행될 겁니다. 

 

여기서 바로 Core가 활약하는 구간이죠.

 

CCore 클래스에 멤버 함수 progress를 추가합니다.

이 멤버 함수 progress에서 프로그램이 동작할 겁니다.

 

 

74번째 줄에 있는 코드처럼 else 문 안에 

 

Core 객체의 멤버 함수 progress가 호출되도록 하면

메시지가 들어온 잠깐의 시간을 제외한 대부분의 시간에서

progress 함수가 돌면서 프로그램이 동작하겠죠.

 

 

 


강의 출처 : https://www.youtube.com/watch?v=4lP0VUD7YmA