코 딩 조 아
홈
태그
방명록
코 딩 조 아 프로필 이미지
김빡주

취미 올리는 곳

  • 분류 전체보기 (65)
    • 창작마당 (6)
      • 게임 (1)
      • 블로그 스킨 (0)
    • 공부 (44)
      • 프로그래밍 언어 (19)
      • 게임 개발 (6)
      • 그래픽스 (1)
      • CS (18)
    • 일지 (14)
    • 연습 (1)

통계

전체 방문자 Today : Yesterday :
#플래시8 #게임 제작 #스레드 #연구실 #color클래스 #C++ #메타프로그래밍 #연습작 #프로그래밍 #프로세스 #2진리터럴 #복사대입연산자 #복사생성자 #유피의 소원 #오목 #타입 추론 #WinAPI #roblox #로블록스 #auto #던파모작 #유니티 #게임제작 #던파 #멀티 프로세스 #CS #멀티 스레드 #포인터 #구조체 #리터럴
«   2026/08   »
일 월 화 수 목 금 토
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
  • AI로 나만의 게임엔진을 만들어보자 (3) - 게임 프레임워크 구조 2026.07.11
  • AI로 나만의 게임엔진을 만들어보자 (2) - 무수히 많은 패치 2026.07.11
  • AI로 나만의 게임엔진을 만들어보자 (1) - 현재 상황 2026.06.05 1
  • 멀티플레이 오델로 게임 2026.05.19
  • [게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 12~16일차 (완성) 2026.05.19 2
  • [C++] 나만의 스마트 포인터 만들기 (Safe Pointer) 2026.04.10 2
  • [게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차 2026.04.01 1
  • [게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차 2026.03.26 2
  • [C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성 2026.03.25
  • [게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 3일차 2026.03.19 3
  • [게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차 2026.03.18 1
  • [게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 1일차 2026.03.16 4
  • [네트워크] 네트워크 엔디안 2026.03.12
  • [네트워크] TCP와 UDP 2026.03.12
  • [네트워크] OSI 7계층 2026.03.11
  • [Roblox] Roblox에 대해 알아보자.araboja 2026.01.27 4
  • [C++] 가상 함수 테이블 2025.07.13 5
  • [C++] 오늘의 삽질기 - 메모리 풀 2025.07.11 3
  • [C++] static_cast와 dynamic_cast의 동작 2025.07.06 2
  • 행동 트리(Behavior Tree, BT) 2025.07.05 3
  • [C++] 다중 상속에서의 업캐스팅, 그리고 포인터 보정 2025.07.01 1
  • ImGui NodeEditor 집중 탐구 2025.06.09
  • [자체 엔진 개발] 파일 디렉터리 이벤트 감시자 2025.05.20
  • LastLight 2025.03.05 4
  • [유니티] 유니티의 모델 렌더링 구조를 하나하나 파헤쳐보자 2024.12.19
  • [C++] 오늘의 삽질 - inline 키워드에 대하여. 2024.12.19 3
  • [그래픽스] 다양한 블러 효과 2024.12.10
  • [CS] IPC(Inter Process Communication) 2024.12.02
  • [CS] 페이지 교체 알고리즘 2024.11.25
  • [CS] 데드락(Deadlock) 2024.11.25

AI로 나만의 게임엔진을 만들어보자 (3) - 게임 프레임워크 구조

김빡주 | 2026. 7. 11. 19:18 |

안녕하세요. 저에요.

오늘은 게임 프레임워크 구조에 대해 조금 정리해보려고 끄적여봅니다.

 

아직 확정은 아니지만, Unity와 비슷하게 씬-오브젝트-컴포넌트 구조를 채택했습니다.

이유는 직관적이라서....

Godot의 노드 방식도 좋긴한데, 너무 개발자스러워서 저는 Unity방식을 채용했습니다.

 


씬

게임 프레임워크의 시작이기도 해서 그런지 씬이 상당히 무겁습니다.

게임인스턴스 관련은 모두 씬이 관리한다고 보면 됩니다.

차근차근 설명해보자면,

 

씬은 pre-load를 위해 참조되어있는 에셋을 들고 있습니다.

참조되어있는 에셋을 저장. 나중에 에셋 패킹에도 사용합니다.

 

또한, 오브젝트와 각 컴포넌트 타입 수를 체크해서 해당 수의 1.5배 만큼의 메모리 풀을 사전에 할당하도록 설계했습니다.

 

개발 초기엔 ECS가 궁금해서 인스턴스 관리를 ECS로 하도록 AI보고 시켰는데, 실제로 뭔가 만들긴 하더라구요.

근데 확장성이 너무 떨어져서 포기했습니다. ECS를 몰라서 뭐라 대안을 주기도 어렵구요.

그래서 ECS를 폐기하고 메모리 풀 방식을 채용했습니다.

고정타입 메모리 풀에 청크방식을 하이브리딩해 많은 할당에도 끄덕없게 만들었습니다.


오브젝트

오브젝트는 컴포넌트들을 소유가 아닌 참조만하고 있습니다.

앞서 설명한거처럼, 실 메모리는 씬이 할당하고있기 때문입니다.

SafePtr은 소유가 아닌 참조입니다.

 

Unity랑 다른 점은, Transform을 컴포넌트가 아니라 그냥 고정 멤버 변수로 두었다는 점입니다.

 

나중에 오브젝트는 따로 Enum을 통해 모드를 두어서 관리할까도 생각중입니다.

다형성이 아닌 이유는, 불필요하기 때문입니다. 불필요한 다형성은 온갖 성능 오버헤드와 불편함만 야기합니다.

계획중인 모드의 종류와 역할은 다음과 같습니다.

  • World Object: 특정 기능이나 비주얼라이징 등과 같이 월드 상에 존재해서 영향을 줘야하는 오브젝트
  • UI Object: 월드와 상관없이 화면 해상도에 영향을 줘야하는 오브젝트. 이 경우엔 Transform을 다른 방식으로 해석할 듯.
  • Logical Object: 위치가 필요없는 특정 시스템이나 로직만 돌리기 위한 오브젝트. Transform을 에디터에서 띄우지 않음.

컴포넌트

컴포넌트는 중요한 점이 하나 있습니다.

바로 엔진 컴포넌트와 사용자 컴포넌트(스크립트)로 나뉜다는 건데요.

엔진 컴포넌트는 정적이지만 스크립트는 라이브 컴파일을 가변성을 띄기 때문에 두개의 차이를 좀 뒀습니다.

일단 라이프사이클은 스크립트에만 있도록 만들었습니다. (마치 Unity의 Monobehavior처럼요.)

또한 스크립트는 Component지만, ComponentBase를 상속받지 않습니다. 경계를 확실히 하기 위해서요.

그럼 엔진 컴포넌트는 어떻게 기능을 작동하냐? 그건 System이 담당합니다. System이 원하는 타이밍에 원하는 동작을 수행합니다. (예를 들어 물리나, 트랜스폼 등)

Component에 라이프 사이클이 없다면서 OnCreate랑 OnDestroy는 왜 있냐? 라고 하실 수 있는데요?

일단은 메모리풀 개념이기도 하고, 직접 생성/소멸자를 호출하기엔 메모리 풀의 정리 타이밍과 실제 게임에서의 정리의 타이밍이 다를 수 있기에 따로 콜백을 만들었습니다.


구조 이해하려고 소스파일 뒤지는 김에, 메모해두면 기억에 더 남겠다 싶어서 적어봤습니다.

그럼 이만

'일지' 카테고리의 다른 글

AI로 나만의 게임엔진을 만들어보자 (2) - 무수히 많은 패치  (0) 2026.07.11
AI로 나만의 게임엔진을 만들어보자 (1) - 현재 상황  (1) 2026.06.05
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 12~16일차 (완성)  (2) 2026.05.19
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차  (1) 2026.04.01
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차  (2) 2026.03.26

AI로 나만의 게임엔진을 만들어보자 (2) - 무수히 많은 패치

김빡주 | 2026. 7. 11. 02:56 |

안녕하세요. 저에요.

오랜만입니다.

취업준비하느라, 노느라, 숨쉬느라 바빴습니다.


자체엔진도 최근에 바빠서 안만들다가, 최근에 다시 좀 건들고있습니다.

클로드 코드 Fable5가 나온 기념으로...

리팩터링도 많이하고... 카메라 컬링도 구현하고...

나름 한거 좀 많습니다. 유기 안했습니다.

그냥 포스팅각이 너무 나중에 온 것...


에디터 관련 편의 기능도 추가하고...

에셋 다른거 참조 못하게, 대충 이런 기능같은걸 하는 위젯들을 만들었다는 뜻.


Light & Shadow도 추가하고...

형식상 디렉셔널라이트도 있는데, 2D환경에서 어캐 쓸지 모르겠습니다. 고민이 많습니다. 2D엔진 공부를 더 해야겠군요


아 맞다, Text 렌더링도 추가했습니다.

폰트패밀리를 만들어 사용해야한다해서 에셋 종류가 또 추가됐습니다. 박수!!!! (ㅠ)

 


인풋 추상화 레이어를 처리하기 위한 인풋 시스템도 제작했습니다.

언리얼 방식을 채택할까, 유니티 방식을 채택할까 고민을 매우 많이했는데,

생각해보니 내가 만드는 엔진인데 왜 굳이 상용 엔진 아키텍처를 따라가야하는지 의문이 들어 저만의 방식으로 만들었습니다.

InputHandler의 템플릿 인자는 각각 레이어 이름, 레이어 오더를 뜻합니다.

어떤 인풋 레이어를 쓸거고 오더를 설정할 수 있게 합니다.

HandleInput에서 true를 반환하게 하면 레이어 블로킹을 하여 다른 인풋을 막을 수 있습니다.

InputHandler를 상속받으면 해당 스크립트는 인풋 시스템을 통해 인풋스테이트를 폴링받을 수 있습니다.

 

대충 이렇게 씀

...적고 생각해보니 이상하네?

레이어 이름으로 순서 구분하는건데 오더는 왜 있지?

나중에 고쳐야겠당.


 

사운드 플레이어랑 사운드 이펙터도 만들었고....

(이거 진짜 개쩌는데 소리 녹음하려고 영상찍기는 귀찮아서 넘김...)


버그랑 오역도 많고 스크립트도 가끔 빌드 먹통이 될 때가 있는데...

만들고 싶은건 많아서 참 답답하네요.

처음 만들때 심혈을 기울여야하는데 자꾸만 귀찮아서 한번 대충하다가 그 버그가 제 꼬리를 잡네요.

 

다들 AI쓸 때 정말 복잡한 프로젝트는 한땀한땀 작업합시다...

 

마지막으로 제 엔진으로 첫 게임(테트리스)을 만들어봤는데,

그거 구경시켜드리고 마무리할게요.

 

아 맞다, 설명은 못드렸는데 네트워크도 추가했는데,

TCP랑 UDP하이브리드로 해서 지연에 민감한 패킷은 Reliable UDP를 통해 처리하도록 만들었읍니다.

그럼 TCP를 안쓰고 Reliable UDP를 쓰면 되는거 아님? 이라고 하실텐데,

웹이랑 윈도우가 통신하려면 UDP소켓은 웹에서 지원하지 않기 때문에 TCP를 쓰긴 해야된데여...

저도 잘은 몰?루. 그냥 클로드군이 그렇다네요.

어쨋든 나중에 테트리스를 네트워크를 통해 멀티로 만들어보려구여.

 

앞으로 UI처리는 어떻게 할지도 고민이고... 머리가 아파온다. 그만하고싶다.

약간 로블록스 방식으로 갈거같긴한데 더 고민 좀 해보겠읍니다.

 

그럼 이제 진짜 빠잉.

'일지' 카테고리의 다른 글

AI로 나만의 게임엔진을 만들어보자 (3) - 게임 프레임워크 구조  (0) 2026.07.11
AI로 나만의 게임엔진을 만들어보자 (1) - 현재 상황  (1) 2026.06.05
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 12~16일차 (완성)  (2) 2026.05.19
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차  (1) 2026.04.01
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차  (2) 2026.03.26

AI로 나만의 게임엔진을 만들어보자 (1) - 현재 상황

김빡주 | 2026. 6. 5. 17:00 |

AI로 게임엔진을 얼마나 만들 수 있을까 테스트해보기로 했다.

그냥 티스토리에 웹게임 올려보기 용도로 쓰려고 간단하게 만들려고 했었다.

처음엔 그냥 절대 안될거라 믿고 대충대충 설렁설렁했는데, 해보니까 가능성이 보여서 일지를 써보기로 했음.

 

엔진 방향성을 간략히 설명해보자면,

  • 2D 게임 엔진 - 하지만 차후 3D를 위한 기본 구조는 짜둠.
  • 멀티 플랫폼 지원 - 현재 Window와 Web을 지원(확인까지 완료) 차후 모바일도 확장하기 위해 구조를 남겨둠.
  • 다양한 렌더러 지원 - 현재 WebGPU, DX11, Vulkan을 지원한다. (하지만 선택하는건 아직 안만듬.)
  • Guizmo 자체 제작 - ImGuizmo는 내가 알기로 3D만 있다. 따라서 2D버전을 자체 제작함. (물론 AI가)
  • 다이나믹 스크립팅 지원 - 당연한거긴 하지만, 다이나믹 스크립팅을 위해 라이브 컴파일을 구현했다. 또한 프로퍼티 문법을 제공하여 에디터에서나 직렬화 용도로 편하게 쓸 수 있다.
  • 리소스 최적화 및 암호화를 위한 에셋 패킹 지원 - 말그대로 에셋을 패킹하여 하나의 파일로 만든다.
  • 로컬라이징 지원
  • 사운드 이펙트 지원 - 사운드 이펙트를 설정할 수 있고, 파일화하여 사운드에 적용시킬 수 있다.
  • 멀티 카메라 뷰 지원 - 하나의 씬에 여러 각도에서의 카메라를 한 화면에 여러 뷰를 띄울 수 있다.

차후 개발해야할 or 고려중인 사항

  • 2D 라이트 & 그림자(?)
  • 타일맵
  • 스프라이트 애니메이션
  • 애니메이션 프레임 이벤트 및 다이나믹 콜라이더(아마 못할지도)
  • 모드 및 플러그인을 어떻게 지원할지? (어차피 나만 쓸거지만 그냥 다른 엔진은 어떤 식으로 지원하는지 궁금해서 구현해볼듯)

 

현재 진행중인 엔진의 간략한 모습이다.

좌측은 Guizmo / 우측은 시뮬레이션&물리

 

좌측은 빌드&에셋 패킹 / 우측은 라이브 컴파일&프로퍼티


 

시작한지는 2주가 지났고, 첫 1주동안은 솔직히 건성건성했다. 명령도 대충하고 AI쓰는법도 제대로 몰랐다.
근데 AI도 쓰면서 느는건지 요령이 생기기 시작한다.

첫 주에는 Codex Pro와 Cladue Pro 각각 하나씩 사용했는데, 토큰이 부족해서 Cladue Pro를 다른 계정에 더 결제했다. ㄱㅡ

Codex와 Claude의 자강두천

 

 

Codex는 보통 멀티 플랫폼을 위해 브릿지 구현과 렌더러 구현을 주로 시키고, 나머지는 전반적인 코드 리뷰 및 최적화를 시켰다.

Claude는 보통 나머지 주요 기능 구현에 주로 썼고

나는 에디터 프레임워크 제공과 레이아웃만 조금 만져주곤, 나머지는 일시키고 유튜브 시청을 했다.

 

말만 들어보면 나는 유튜브 시청이나 하면서 개꿀빠는 사람으로 보이겠지만, 농담식으로 말한거다...

나도 바쁘다... 나같은 빡통은 AI한테 일시키는게 매우 어려움.

작업에 대한 업무 지시를 할 때 균일한 작업 결과를 보증하기 위해 매일 md파일을 수정해가며, 예외사항을 하나하나 말해줘야한다.

또한, AI가 짠 코드를 수시로 검토해야한다. 어느샌가 정신차려보면 처음 작업 방향성과 멀어져가는걸 가끔 볼 수 있다.

 

내가 잘못 지시해서 하루종일 리팩토링한적도 있고, AI가 방향성에서 벗어나서 하루종일 리팩토링한적도 있다.

물론 AI가 방향성을 벗어난건 내 잘못이 맞다. 결국 전부 내 잘못...

 

솔직히 지금 개발 상황으로도 간단한 게임은 그냥 만들 수 있는 정도다.

나같은 허접플머도 이런 엔진을 만들 수 있는 시대다.

점점 1인 개발은 늘어갈거고, 항상 시대에 뒤쳐지면 안되겠슴...

 

무튼 개발 일지를 간간히 작성해보겠읍니다...

'일지' 카테고리의 다른 글

AI로 나만의 게임엔진을 만들어보자 (3) - 게임 프레임워크 구조  (0) 2026.07.11
AI로 나만의 게임엔진을 만들어보자 (2) - 무수히 많은 패치  (0) 2026.07.11
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 12~16일차 (완성)  (2) 2026.05.19
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차  (1) 2026.04.01
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차  (2) 2026.03.26

멀티플레이 오델로 게임

김빡주 | 2026. 5. 19. 22:55 |

https://github.com/taku7664/Othello-Game/releases/tag/v1.0.0

 

Release Othello-Game · taku7664/Othello-Game

오델로 게임 ImGui를 기반으로 만든 간단한 멀티플레이 오델로 게임입니다. 친구와 같이 무슨 게임을 할지 고민하면서 간단하게 시간을 떼우고 싶을 때, 같이 오델로 한판 어떤가요? 가이드가 있

github.com

TCP 리슨 서버를 기반으로 멀티플레이 오델로 게임을 만들었습니다.

GUI 프레임 워크는 ImGui를 활용하여 제작하였습니다.

 

맛있게 즐겨주세요.

[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 12~16일차 (완성)

김빡주 | 2026. 5. 19. 22:17 |

안녕하세요. 저에요.

드디어 미루고 미뤄왔던 오델로 게임을 완성했어요.

 

결과 미리보기


게임 플레이

게임 로직 구현 및 가시성 개선

오델로 게임 플레이 로직을 구현했다.

주요 기능은 기본 게임 규칙과 더불어 무르기, 항복을 구현했다.

또한, 가시성 요소를 추가했다.

착수가 가능/불가능 칸을 구별하는 사각형을 렌더했다.

착수가 가능한 칸을 마우스로 호버 시 어느 돌이 바뀌는지 미리 볼 수 있도록 표시했다.

 

팀전 기능 추가

특별하게 팀전을 만들었다. 애초에 방 인원을 바꿀 수 있게 만든 이유가 팀전을 만들어보고 싶어서이다.

놀랍게도 팀 인원 밸런스가 안맞아도 시작할 수 있다.

 

각종 요청 처리 팝업

상대방의 동의가 필요한 경우 투표 팝업을 열도록 했다.

 


로비 설정

플레이어 순서 조정 기능을 추가했다.

드래그&드랍으로 조정할 수 있게 해보려했지만, 너무 막노동이라 포기했다.


후기

네트워크를 아예 모르는 상태로 만들다보니 삽질도 많이 하고, 버그도 많이 발견되고 너무 힘들었다...

중간에 정말 포기하고 싶었던 때도 많았는데, 결국 완성은 했다.

 

하지만 얻은 것도 많다.

일단 멀티플레이 기반 게임을 만드려면, 이벤트 시스템 기반 프로그래밍을 잘 해야된다는 것을 깨달았다.

멀티 플레이게임을 잘 만드려면 프로그래밍을 할 때 이런 사고방식이 필요하다.

회사로 예를 들자면,

상사(호스트)가 부하 직원들(클라이언트)에게 일을 시켰다. => (브로드 캐스트)
직원은 일을 한다. => (패킷 처리)
직원이 일 결과를 상사에게 보고해야한다. => (동기화)

다시는 직접 소켓 프로그래밍해서 게임 안만들거다요. 흑흑

'일지' 카테고리의 다른 글

AI로 나만의 게임엔진을 만들어보자 (2) - 무수히 많은 패치  (0) 2026.07.11
AI로 나만의 게임엔진을 만들어보자 (1) - 현재 상황  (1) 2026.06.05
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차  (1) 2026.04.01
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차  (2) 2026.03.26
[C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성  (0) 2026.03.25

[C++] 나만의 스마트 포인터 만들기 (Safe Pointer)

김빡주 | 2026. 4. 10. 16:50 |

C++을 하다보면 제일 많이 직면하는 문제가 있다.

그것은 댕글링 포인터문제.

처음 C++을 배울땐 그냥 짜는놈이 관리를 잘하면 되는거 아님? 이라는 생각을 했던 과거의 나였지만....

하지만 실전은 그리 쉽지 않았다... 리소스 매니지먼트에서 특히나 그런 문제를 많이 겪었다.

 

그래서 과거에는 소유 권한의 객체가 shared_ptr을, 받아 쓰는 객체는 weak_ptr을 주어 사용했다.

이러면 shared_ptr의 컨트롤 블럭에 의해 원본 객체가 삭제되면 weak_ptr은 원본 객체가 삭제된걸 알 수 있기 때문이다.

하지만 weak_ptr의 lock에는 스레드 세이프를 위해 원자적 연산을 하는 것으로 알고 있었지만 나는 싱글 스레드 환경이었기 때문에 필요가 없었다.

또한, 위와 같은 경우엔 소유자는 무조건 한명이기 때문에 shared_ptr의 레퍼런스 카운팅 기능이 필요하지 않았다.

따라서 실제 사용용도랑 매우 다르게 사용하는 중이었고, 성능도 매우 떨어진다 볼 수 있었다.

 

그리고 시간이 지나 면접준비를 할 때 이런 질문을 받았다.

shared_ptr과 weak_ptr을 사용해본 적이 있으신가요?

...대답할 수가 없었다...

왜냐하면 shared_ptr과 weak_ptr은 상호 참조 문제를 방지하기 위해 생긴거지, 위에 언급한 유효 객체 판단을 위해 만들어진게 아니기 때문이다.

 

그래서 내가 직접 만들어 보기로 했다.

원자적 연산을 필요로 하지 않고, 강한 참조 카운팅도 하지 않는 나만의 스마트 포인터를 말이다.

따라서 베이스는 unique_ptr로 하되, 내부에 컨트롤 블럭을 만들고 약한 참조 카운트만 재는 방식을 사용했다.

SafeCount = 약한 참조 카운트

 

그렇게 나온 결과물

 

사용은 이렇게 한다. 기존의 스마트포인터랑 매우 비슷하게 만들었다.

 

성능 비교. 뭔가 있어보이는 이유는 테스트케이스를 코파일럿에게 만들어달라해서...

디버그
릴리즈

위에서부터 역참조, 유효성 검사 + 역참조, 유효성 검사 순으로 진행했다.

횟수는 10000개의 객체를 200번 순회하였고, 5번의 웜업 후에 실제 해당 순회를 20번 반복하여 평균을 낸 값이다.

그 외에 생성과 삭제도 기존 스마트포인터보다 빨랐었다.

 

확실히 원시 포인터가 빠르긴 하다.

아무리 weak_ptr보다 빠르다 해도 결국 원시포인터의 4~5배 가량 느리니 쓰는데는 주의하는게 맞다.

원시포인터가 생각보다 많이 빨라서 당황했지만, 릴리즈에선 기대정도의 성능은 나오는 듯 하다.

앞으로 종종 쓸 것 같다. ㅎㅎ;;;

 

'공부 > 프로그래밍 언어' 카테고리의 다른 글

[C++] 가상 함수 테이블  (5) 2025.07.13
[C++] static_cast와 dynamic_cast의 동작  (2) 2025.07.06
[C++] 다중 상속에서의 업캐스팅, 그리고 포인터 보정  (1) 2025.07.01
[C++] 멀티 쓰레딩 프로그래밍을 해보자  (0) 2024.10.14
[C++] 2진 리터럴(Binary literals)에 대해 알아보자.araboza (C++14)  (2) 2024.09.16

[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차

김빡주 | 2026. 4. 1. 21:50 |

MFC를 쓰다가 너무 짜증나서 ImGui로 갈아엎었다.

MFC를 쓰려던 이유는 렌더러를 만들기 싫어서였는데, 사실 렌더러를 만들 정도까지가 아니어도 ImGui는 사용이 가능하기 때문에 왜 쓰고있었는지 모르겠다.

아무튼 또 바보같게 ImGui로 갈아엎으면서 부분부분 리팩토링을 진행하다보니 또 뻘짓거리를 했다.

이렇게 된거 기왕 만드는 게임 열심히 때깔좋게 만들어보기로 했다.

지금까지의 성과 미리 보기

나름 UI배치가 이뻐진 모습.

게임창과 채팅창 사이의 컬럼바를 조종하여 화면비를 조정할 수 있다.

화이트모드랑 다크모드 둘 다 지원할 예정 ㅎㅎㅎ


ImGui 라이브러리 추가 및 적용

나름 졸업프로젝트로 ImGui기반 에디터를 만들어봤다고, 내 자존심이 도킹라이브러리를 사용하라고 내면속에서 외쳤다.
근데 막상 도킹UI를 적용해보니 화면 분할 시 렉도 컸고, 별로 예쁘지도 않았다.
그렇다고 도킹을 빼긴 어려워서 도킹플래그를 끄고 쓰기로 했다.

ImGui윈도우 프레임 워크

주석은 급한대로 못썼다...

졸플때 썼던 프레임워크를 살짝 수정했다.

기존의 부실했던 콜백과 이상하던 함수 네이밍을 수정하고, 내부를 깔끔히 정리했다.

콜백에 맞춰 사용하면 되게끔 설계했다.

 

문자열 인코딩 이슈

ImGui는 와이드 문자열을 사용하지 않고, 멀티바이트 문자열을 사용합니다.

따라서 기존 MFC는 와이드 문자열을 주로 사용했기에 전부 와이드 문자열을 다뤘지만, ImGui로 바꿔버리면서 문자열 처리를 전부 바꿔야했다.

근데 일반적인 멀티바이트 문자열은 한글을 표현하지 못한다.

그럼 어떻게 해야하냐? 바로 u8로 인코딩을 해야한다...

일반적인 문자열은 "hello world!"라면 utf-8 인코딩을 하려면 u8"hello world!"로 해줘야한다.

하아....

근데 방법이 따로 있긴하다. 그건 파일 자체를 utf-8인코딩을 기본 설정으로 하면 된다.

editconfig를 만들어 인코딩 설정을 해주는 방식이다. 검색하면 많이 나온다.


패킷 구조 미세 변경

원래 패킷마다 enum타입을 줘서 Recieve한 패킷을 구분하는데 썼지만, 패킷을 추가할 때 마다 enum을 추가해주는게 너무 힘들었다.
따라서 그냥 typeid의 name을 통해 문자열로 구분하기로 했다.
속도는 조금 느리겠지만, 생산성이 좋아졌다. 와~

typeid(T).name()에 +15를 하는 이유는 앞에 class랑 Packet::네임스페이스를 건너띄고 클래스 이름만 구분하려고 했기 때문이다.


앞으로 할 것 (우선 순위별 정리)

  1. 참가하기를 눌렀을 때 실패할 시 예외처리
  2. 최대 인원보다 많이 들어오면 방 거절
  3. 게임 방 상태 구분 (대기, 시작)
  4. 초기 플레이어의 색 지정
  5. 호스트의 플레이어 관리(강퇴) 및 게임 시작
  6. 호스트의 게임 보드 칸 설정 기능
  7. 게임 진행 및 판정
  8. 무르기 및 상대의 승인

'일지' 카테고리의 다른 글

AI로 나만의 게임엔진을 만들어보자 (1) - 현재 상황  (1) 2026.06.05
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 12~16일차 (완성)  (2) 2026.05.19
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차  (2) 2026.03.26
[C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성  (0) 2026.03.25
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 3일차  (3) 2026.03.19

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차

김빡주 | 2026. 3. 26. 00:19 |

지금까지의 성과 미리 보기


4~6일 차 - 서버 & 게임 프레임워크 구조 변경

UDP -> TCP로 변경함과 동시에 서버 프레임워크 구조를 변경했다.
TCP로 바꾼 이유는, 실시간이 중요한 게임이 아닌 이상 TCP를 주로 사용한다는 얘기를 들어서다.
심지어 Reliable UDP같이 UDP를 TCP처럼 구현하지 않는 이상, 안정성이 떨어져 패킷 유실이 꽤나 심하다는 이야기를 들었다.
Reliable UDP를 구현할 자신이 없기도 했고, 내 게임은 TCP로도 차고 넘치기 때문에 변경하기로 했다.

 

패킷의 구조 변경:

PacketHeader를 상속받아 각 패킷을 완성시키는 구조에서,

PacketHeader와 PacketBody를 나누는 방식으로 변경했다.

이유는 채팅을 보낼 때 가변 길이의 문자를 패킷으로 보내기 위해선 고정 사이즈 struct를 사용하기 힘들었다고 판단했기 때문이다.

따라서 PacketHeader에 Body의 크기를 보내서 크기만큼 읽는 방식으로 변경했다.

 

패킷의 Send & Recieve의 과정:

처음 설계할 땐 패킷을 큐에 모으지 않고 바로바로 Send 하는 방식에서,

하지만 큐에 모아서 업데이트 때 한 번에 보내는 방식으로 변경했다.

이유는 한번에 모아 보내는 것이 성능적으로도 빠를 것이라고 판단했고, 게임 루프 측면에서도 큐에 모아서 보내는 게 라이프사이클 면에서도 일관적일 것이라고 판단했기 때문이다.


또한, 서버 프레임워크 구조를 변경하면서 자연스럽게 게임 프레임워크 구조도 고민을 하게 되었다.
네트워크 친화적 프로그래밍을 위해선 이벤트 기반의 프로그래밍은 기본이고, 클라이언트와 서버의 영역을 적절히 나눠야 된다고 생각했다.
게임 프레임워크는 전반적으로 이렇게 구성되어 있다.

 

게임 매니저:

방을 만들거나 들어가는 등의 편의성 API를 제공한다.

또한 게임 승리, 패배 등의 판단 로직을 담당하기도 한다.

 

플레이어:

서버와 클라이언트 간 중계자 역할을 한다.

채팅을 보낼 때 서버에 메세지 패킷을 서버에 보내고,

게임을 할 때 필요한 API에 대한 패킷을 서버에 보낸다던가 등의 역할을 한다.

 

게임 룸:

서버와 연관이 없는 로컬 객체이다.

다만, 서버에 의해 받은 패킷을 통해 상태를 동기화시켜 준다.

또한 Dialog의 중계자이기도 하다. 자신 외에는 GameRoom Dialog를 최대한 알 필요가 없도록 해당 객체가 해당 Dialog의 동기화를 담당한다.

새로운 유저가 들어올 때 방의 정보를 보내야 하는데, 이 객체를 토대로 저장된 정보를 서버가 클라이언트에 보내주게 된다.

 

게임 보드:

서버와 연관이 없는 로컬 객체이다.

게임 룸과 동일하게 서버에 의해 받은 패킷을 통해 상태를 동기화시켜 준다.

무슨 돌이 어떤 좌표에 놓여있는지 판단하기 위한 컨테이너만 들고 있는 객체이다.


7일 차 - 커넥션(세션) 관련 버그 수정

정확히는 7일~8일 오전까지 겪은 문제다....
구조 변경하느라 정신이 많이 힘들었는데, 이 문제까지 겹치니 멘탈 잡기가 힘들었지만 해결했다. ㅎ

 

문제는 다음과 같았다.

호스트 네트워크 객체는 커넥션 정보를 모아 std::vector에 관리하는데,

커넥션이 1명까진 문제없는데, 2명부턴 문제가 생기는 것이었다.

select() 함수에서 10038 에러가 떴는데, 이 에러는 소켓이 아닌 객체를 소켓으로 사용하여 난 에러다.

분명 소켓 객체에 INVALID_SOCKET이 아닌 것도 확인했는데 말이다.

알고 보니 재할당 과정에서 소멸자가 호출되는데, 소멸자에서 closesocket을 하고 있었다....

자세한 내용은 아래 적어놨다.

[C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성 :: 코 딩 조 아


8일 차 - 게임 방  및 채팅 구현, 자잘한 디테일 깎기

이제 서버 관련 버그 및 구조를 대충 확립했으므로, 드디어 게임을 만들 수 있다.
하지만 서버 관련 빈약한 부분이나 예외 사항을 확인하기 위해 게임을 만들기 전에 채팅부터 먼저 만들어보기로 했다.

 

 

GameRoom Dialog에 Item 추가:

일단 빠른 채팅을 위해 대충 레이아웃을 짜놨다.

게임 보드 부분은 임의의 Frame을 설치해 놓고, 프레임의 크기와 위치에 맞게 보드를 렌더링 할까 생각 중이다.

나중에 보드 부분을 어떻게 처리할지 더 고민해 봐야겠다.

 

접속 및 채팅 송수신 체크:

처음에 봤던 사진 바로 이것이다.

접속 및 채팅이 성공적으로 되는 것을 확인했다.

이제 클라이언트-서버가 지속적인 Timeout 패킷을 주고받으며 LostSession을 체크하여 접속이 끊어진 상태도 체크할 예정이다.

 

Item 부분 디테일 추가:

게임 방을 만들어 게임 방이 올라와 있는 상태에서 추가적인 방을 만들기를 방지하기 위해 Item위젯을 비활성화시켰다.

 

Tab키를 눌렀을 때 UI 내비게이션 순서가 꼬여있길래 정리했다.

'일지' 카테고리의 다른 글

[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 12~16일차 (완성)  (2) 2026.05.19
[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차  (1) 2026.04.01
[C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성  (0) 2026.03.25
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 3일차  (3) 2026.03.19
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차  (1) 2026.03.18

[C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성

김빡주 | 2026. 3. 25. 14:37 |

 

원래도 알고 있던 개념이었지만, 막상 이것저것 코딩하면서 머리가 복잡해지니까 자꾸 놓치는 개념이다...

std::vector가 재할당 시 처리하는 방식과, 이동 연산자의 중요성을 다시 한번 알아보자...


이런 코드가 있다고 쳐보자.

static int i = 0;

class Test
{
public:
    Test(int i) : myI(new int) { *myI = i; }
    ~Test() { delete myI; myI = nullptr; }


private:
    int* myI = nullptr;

};

std::vector<Test> gArr;

int main()
{
    gArr.emplace_back(1);
    gArr.emplace_back(2);
    gArr.emplace_back(3);
}

요악하자면, 단순 struct에 소멸자에서 Close를 자동으로 해주는 정도?

그리고 아래 줄에는, 전역 변수 gArr가 있다.

이 상태로 gArr에 계속 객체를 emplace_back을 해주면 어떻게 될까?

당연히 각 객체의 myI는 순서대로 1, 2, 3이 담겨있을 것이다! 라고 짰으나,

...결론은 gArr내부 객체의 myI의 내부 참조에는 쓰레기 값이 담겨있다.

 

이 이유는 다음과 같다 std::vector는 재할당 시 새로운 메모리에 객체를 옮기는 작업을 해야하는데,

이 과정에서 객체에 이동 연산자가 정의되어있지 않으면 객체를 복사하여 새로운 메모리에 붙인 후 기존 메모리의 객체는 소멸을 시키게 된다.

때문에 myI는 겉으로 보기엔 정상이지만, 이미 Close가 호출되어 실제 논리상으로는 위험한 동작을 하고 있었다는 것이다.

 

해결하려면 어떻게 해야할까?

그것은 바로 이동 연산자를 구현하는 것이다.

#include <vector>

static int i = 0;

class Test
{
public:
    Test(int i) 
        : myI(new int) 
    { 
        *myI = i; 
    }
    ~Test() 
    { 
        if (myI)
        {
            delete myI; 
            myI = nullptr;
        }
    }
    Test(Test&& other) noexcept 
        : myI(other.myI)
    {
        other.myI = nullptr;
    }
    Test& operator=(Test&& other) noexcept
    {
        myI = other.myI;
        other.myI = nullptr;
    }

private:
    int* myI = nullptr;

};

std::vector<Test> gArr;

int main()
{
    gArr.emplace_back(1);
    gArr.emplace_back(2);
    gArr.emplace_back(3);
}

구조체에 이동 연산자를 정의해봤다.

결과는 바뀔까?

매우 잘 적용된 모습이다...


 

이걸 겪게 된 계기는, tcp 네트워크 프로그래밍을 하다가 겪게 된 것인데,

truct Connection
{
public:
	Connection();
	~Connection();
    
	int						ID = 0;
	SOCKET					Socket = INVALID_SOCKET;
	SOCKADDR_IN				Address = {};
	std::vector<Buffer>		SendQueue;
	std::vector<Buffer>		RecvQueue;
	Buffer					RecvBuffer;
};

이런 객체가 있고, 이걸 담고있는 vector를 호스트에게 쥐어줬던 상태였다. (소멸자에는 closesocket 함수를 호출)

웬걸? 디버깅엔 분명 Socket에 제대로 값이 들어간 것을 확인할 수 있는데, 막상 네트워크 함수를 사용하니 WSAError 10038이 뜬다.

Windows 소켓 오류 코드(Winsock2.h) - Win32 apps ❘ Microsoft Learn

소켓이 아닌 곳에서 작업을 했다는 것이다.

뭔 소리지? 분명 나는 소켓을 초기화해서 커넥션 정보에 넣어줬는데. 왠걸?

소멸자에서 closesocket함수가 호출되고 있었던 것이다.

사실 소멸자는 호출되는게 맞는데, 문제는 이미 복사해간 객체가 이미 닫은 소켓을 가리키고 있어서 생기는 문제였다.

따라서 이동연산자를 만들어주고, 기존 객체는 INVALID_SOCKET으로 만들어주어 고치게 되었다.

'일지' 카테고리의 다른 글

[게임 개발] ImGui를 활용한 간단한 오델로게임 제작 - 9~11일차  (1) 2026.04.01
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차  (2) 2026.03.26
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 3일차  (3) 2026.03.19
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차  (1) 2026.03.18
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 1일차  (4) 2026.03.16

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 3일차

김빡주 | 2026. 3. 19. 02:10 |

게임

게임 프레임워크 구상

항상 고민하는 문제다.

Manager를 어떻게 배치하고 가져올 것인가.... 싱글톤이면 누가 초기화하냐에 따라 순서가 꼬일 수도 있다.

어차피 간단하게 만드는 게임이니까, 단순하게 하기로 했다.

그냥 이걸 두고 전역적으로 가져와 쓰기로 했다.

이러면 안쓰는 매니저도 무조건 메모리에 올라온다는 단점이 있지만, 요즘 시대에 그정도 메모리가 얼마나 신경쓰인다고.

널체크도 안해도 되고 역참조도 안함 개편함 굳ㅋ

 

게임 로직적 프레임워크도 대충 구상을 마쳤다.

게임적 프레임워크는 크게 GameRoom - GameBoard - Player로 나누었다.

맨날 혼자 개발하면 무작정 구현하다 구조가 꼬여 갈아엎는 경우가 많았다.

따라서 인터페이스부터 먼저 전부 만들고 구현은 나중에 하기로 했다.

또한 혼자 개발하면서 못된 습관이 들지 않도록 지금이라도 협업한다는 생각으로 개발에 임하기로 했다.

  1. 함수 작명을 직관적으로 하기.
  2. 주석을 꾸준히 쓰기. (잘하는 개발자는 주석이 필요 없도록 짠다는데, 현실적으로 말이 안된다고 생각한다.)
  3. 캡슐화에 신경쓰기.
  4. 나중에 고칠 일을 최소화 하도록 함수 매개변수나 구조를 잘 생각하고 짜기.

 

TimeManager 추가

 

 


서버

생각을 바꿨다...

UDP를 쓸 이유가 전혀 없는데 쓰는건 고집이다.

그래서 TCP로 바꾸기로 했다...

근데 구조를 너무 바꾸느라 헛짓거리만 했다.

내일 자세히 써야지...

'일지' 카테고리의 다른 글

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 4~8일차  (2) 2026.03.26
[C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성  (0) 2026.03.25
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차  (1) 2026.03.18
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 1일차  (4) 2026.03.16
[C++] 오늘의 삽질기 - 메모리 풀  (3) 2025.07.11

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차

김빡주 | 2026. 3. 18. 01:19 |

서버

StreamQueue 추가

예전에 만들었던 버퍼용 원형큐를 다듬어서 추가했다. 이거에 한 시간을 소요하다니. ㄱㅡ

대충 테스트해 본 결과

Server::StreamQueue gStreamQueue(5);

void DoSomething()
{
	Debug::Log log("DoSomething()");
	gStreamQueue.WriteData("Hello", 5);
	{	// 앞 3개 빼서 읽기
		char buf[3];
		gStreamQueue.ReadData(buf, 3);
	}
	// 다시 5개 쓰기. 하지만 스트림큐의 capacity는 5이고, 버퍼에 2개가 남아있어서 3개만 쓰여질 것임.
	gStreamQueue.WriteData("12345", 5);

	// 스트림 큐에 있는 모든 데이터 출력해보기
	while (false == gStreamQueue.IsEmpty())
	{
		BYTE data;
		if(gStreamQueue.PopFront(data))
		{
			Debug::Log::WriteLine("Data: ", (char)data);
		}
		else
		{
			Debug::Log::WriteLine("Failed to pop front data.");
		}
	}
}

int main()
{
	Debug::Log log("main()");
	DoSomething();
	return 0;
}

잘 나오는 모습. 야호~

 

서버 테스트

해보지 못했으나 접속까진 잘 되는듯?

내일은 게임 로직 및 패킷 처리에 대해서 작성해야겠다.


MFC

 

다중 다이얼로그

게임 엔진에서의 씬을 구현하기 위해서 좋은 방법이 없나 했는데, 그냥 게임 윈도우를 추가로 띄우기로 했다.

뭐 날고기는 고수들은 혀를 찰수도 있겠지만, 내 생각엔 이게 MFC에선 라이트한 방법 중 가장 최선인거같다. (MFC 더 공부하기 싫어)

아무래도 대기실에 들어간 순간 타이틀 윈도우는 비활성화하는 방향으로 가야할 듯하다.

 

위젯 배치

여러 다이얼로그를 만들어서 방 참가하기와 방 만들기를 제작했다.

후에 입력된 데이터를 토대로 네트워크에 연결해야하는데, mfc 에딧 컨트롤은 와이드문자열을 사용하기 때문에 변환과정이 필요했다.

그래서 유틸리티 라이브러리를 만들었는데 알 수 없는 외부참조가 뜬다... 오랜만에 이 문제를 겪으니까 전혀 문제를 모르겠다.

내일 찾아봐야겠다 ㅠㅠ

찾았다...

결국 컴파일에러 내고선 잠을 못자겠어서 끙끙 싸맸는데, l 하나를 빼먹어서 지금까지 구현을 몰랐던거다...

에휴... 코딩 오래하니까 눈이 침침해져서 찾기도 힘들다...

'일지' 카테고리의 다른 글

[C++] 오늘의 삽질기 - std::vector와 이동 연산자의 중요성  (0) 2026.03.25
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 3일차  (3) 2026.03.19
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 1일차  (4) 2026.03.16
[C++] 오늘의 삽질기 - 메모리 풀  (3) 2025.07.11
[C++] 오늘의 삽질 - inline 키워드에 대하여.  (3) 2024.12.19

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 1일차

김빡주 | 2026. 3. 16. 22:22 |

네트워크를 공부해본 기념(?)으로 지식을 활용해 간단한 네트워크 게임을 만들어보기로 했습니다.

그렇게 선정된 게임이 바로 오델로게임.

7일안에 하는게 목표인데 될지는 모르겠습니다. 암튼 ㄱㄱ


RAII패턴을 활용한 간단한 로그 라이브러리 제작

또 게임만들랬더니 이상한거 만들고 있다. (ㄱㅡ)
하지만 나는 디버그를 중요시하기 때문에 합리화했다. ㅋㅋ

구현해보고자 하던 디버거가 있었는데, 그게 바로 RAII패턴을 활용한 디버거.

RAII패턴을 채택한 이유는 프로젝트가 커지면 디버깅때 함수의 호출 흐름이 눈으로 확인하기가 힘들었고,

흐름을 직관적으로 볼 수 있도록 스코프를 만들어 출력하면 쉽게 구분할 수 있지 않을까? 라는 생각을 했다.

 

Log객체를 생성하면 생성자에서 자동으로 TabSpacing을 늘려주고,

Log객체가 삭제되면 소멸자에서 자동으로 TabSpacing을 줄여줘서,

실제 함수를 작성할 때처럼 스코프를 구분하기 편하도록 만들었다.

더보기

Log.h

namespace Debug
{
	class Log
	{
	public:
		Log(const char* scope);
		~Log();

		static void BeginSpace();
		static void EndSpace();

		template<typename... Args>
		static void Write(Args&&... log);
		template<typename... Args>
		static void WriteLine(Args&&... log);

	private:
		static int tabSpacing;
	};

	template<typename... Args>
	static void Log::Write(Args&&... log)
	{
		#ifdef _DEBUG
		for (int i = 0; i < tabSpacing; ++i)
		{
			std::cout << '\t';
		}
		(std::cout << ... << std::forward<Args>(log));
		#endif
	}

	template<typename... Args>
	static void Log::WriteLine(Args&&... log)
	{
		#ifdef _DEBUG
		Write(std::forward<Args>(log)...);
		std::cout << '\n';
		#endif
	}
}

Log.cpp

#include "pch.h"
#include "Log.h"

int Debug::Log::tabSpacing = 0;

Debug::Log::Log(const char* scope)
{
#ifdef _DEBUG
	WriteLine(scope);
	WriteLine("{");
	BeginSpace();
#endif
}

Debug::Log::~Log()
{
#ifdef _DEBUG
	EndSpace();
	WriteLine("}");
#endif
}

void Debug::Log::BeginSpace()
{
#ifdef _DEBUG
	++tabSpacing;
#endif
}

void Debug::Log::EndSpace()
{
#ifdef _DEBUG
	if (tabSpacing > 0)
	{
		--tabSpacing;
	}
	else
	{
		assert(false && "Invalid Log Scope.");
	}
#endif
}

 

동작은 당연히도 잘 되는 모습...

근데 가독성이 생각보다 좋진 않아서 실망스러웠다.

 

나중에 덤프까지 고려해본다면 Wirte함수에 중요도를 넣는다던가 해서 덤프에 적을거만 따로 빼는 로직도 구상해볼 수 있겠네여.

타임스탬프도 넣어야하고... 할게 너무 많아서 일단 디버거는 여기까지.


서버 프레임워크 구상

네트워크 게임은 처음 만들어봐서 어떻게 서버 프레임워크를 구상해야할지 감이 잘 안온다.
이런 경우 보통 내 생각을 굳건히 믿고 따라가는데, 보통 후회하는 경우가 더러 있었다.
내 선택이니까 후회는 안하고 싶은데 이건 공부용이잖아...
그래서 좀 고민을 많이 하고있고, 앞으로도 프레임워크가 바뀔 가능성이 있다.

 

서버는 리슨 서버구조를 채택하기로 했다.

간단히 만드려면 한명이 방을 파고 다른 한명이 해당 호스트의 IP주소를 입력해 1대1을 펼치는 방식을 구상했는데, 딱 리슨 서버랑 맞는 것 같다.

namespace Server
{
	class ListenServer
	{
	public:
		ListenServer() = default;
		~ListenServer() = default;

		bool StartHost(u_short port);
		bool StartClient(const char* hostIP, u_short port);

	private:
		bool StartupServer();
		bool ShutdownServer();

		bool SetupSocket(const char* hostIP, u_short port);

	private:
		bool        mInitialized	= false;
		SOCKET		mSocket			= INVALID_SOCKET;
		SOCKADDR_IN mLocalAddress	= { 0 };
		SOCKADDR_IN mHostAddress	= { 0 };
	};
}

따라서 서버 객체는 이렇게 정리하고 내부 초기화와 기능을 구현했다.

더보기
#include "pch.h"
#include "ListenServer.h"

namespace Server
{
	bool ListenServer::StartupServer()
	{
		bool bIsSuccess = true;
		Debug::Log log("ListenServer::Startup()");

		if (mInitialized)
		{
			return true;
		}

		WSADATA wsaData{};
		if (WSAStartup(MAKEWORD(2, 2), &wsaData) != NO_ERROR)
		{
			log.WriteLine("[WARN] WSAStartup failed with error: ", WSAGetLastError());
			bIsSuccess = false;
		}

		if(bIsSuccess)
		{
			mInitialized = true;
			mSocket = INVALID_SOCKET;
			mLocalAddress = { 0 };
		}

		return bIsSuccess;
	}

	bool ListenServer::ShutdownServer()
	{
		bool bIsSuccess = true;
		Debug::Log log("ListenServer::Shutdown()");

		if (false == mInitialized)
		{
			return true;
		}

		if (INVALID_SOCKET != mSocket)
		{
			if (closesocket(mSocket) != NO_ERROR)
			{
				log.WriteLine("[WARN] closesocket failed with error: ", WSAGetLastError());
				bIsSuccess = false;
			}
		}

		if (WSACleanup() != NO_ERROR)
		{
			log.WriteLine("[WARN] WSACleanup failed with error: ", WSAGetLastError());
			bIsSuccess = false;
		}

		if (bIsSuccess)
		{
			mInitialized = false;
			mSocket = INVALID_SOCKET;
			mLocalAddress = { 0 };
		}

		return bIsSuccess;
	}

	bool ListenServer::SetupSocket(const char* hostIP, u_short port)
	{
		Debug::Log log("ListenServer::SetupSocket()");
		log.WriteLine("Try Port: ", port);

		mSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if (INVALID_SOCKET == mSocket)
		{
			log.WriteLine("[WARN] socket failed with error: ", WSAGetLastError());
			return false;
		}

		u_long ulMode = 1; // 0 = Blocking, 1 = Non-Blocking
		if (ioctlsocket(mSocket, FIONBIO, &ulMode) != NO_ERROR)
		{
			log.WriteLine("[WARN] ioctlsocket() failed with error: ", WSAGetLastError());
			return false;
		}

		mLocalAddress.sin_family = AF_INET;
		mLocalAddress.sin_port = htons(port);
		if (nullptr == hostIP)
		{
			mLocalAddress.sin_addr.s_addr = htonl(INADDR_ANY);
		}
		else if(inet_pton(AF_INET, hostIP, &(mLocalAddress.sin_addr)) != 1)
		{
			log.WriteLine("[WARN] inet_pton failed to invalid IP address");
			return false;
		}

		if (bind(mSocket, (SOCKADDR*)&mLocalAddress, sizeof(mLocalAddress)) == SOCKET_ERROR)
		{
			log.WriteLine("[WARN] bind failed with error: ", WSAGetLastError());
			return false;
		}

		return true;
	}

	bool ListenServer::StartHost(u_short port)
	{
		Debug::Log log("ListenServer::StartHost()");

		if (mSocket != INVALID_SOCKET)
		{
			log.WriteLine("[WARN] Socket already exists. Please shutdown the server before starting a new host.");
			return false;
		}

		if (StartupServer() && SetupSocket(nullptr, port))
		{
			return true;
		}
		else
		{
			ShutdownServer();
		}

		return false;
	}

	bool ListenServer::StartClient(const char* hostIP, u_short port)
	{
		Debug::Log log("ListenServer::StartClient()");
		log.WriteLine("Try Host IP : ", hostIP);
		log.WriteLine("Try Port    : ", port);

		if (mSocket != INVALID_SOCKET)
		{
			log.WriteLine("[WARN] Socket already exists. Please shutdown the server before starting a new host.");
			return false;
		}

		mHostAddress.sin_port = htons(port);
		if (inet_pton(AF_INET, hostIP, &(mHostAddress.sin_addr)) != 1)
		{
			log.WriteLine("[WARN] inet_pton failed to invalid hostIP adress");
			mHostAddress = { 0 };
			return false;
		}

		if (StartupServer() && SetupSocket(hostIP, port))
		{
			
			return true;
		}
		else
		{
			ShutdownServer();
		}

		return false;
	}
}

 


MFC 애플리케이션 공부

굳이 MFC를? 이라고 물으면 나도 할말은 없다. 하지만 이건 네트워크 공부용 간단한 프로젝트인걸...
굳이 프레임워크를 처음부터 만들기엔 너무 투머치기 때문에 MFC를 선택했다.
사실 이전에도 MFC를 딱 한번 활용해서 TCP 기반의 채팅 프로그램을 만들어본적이 있는데, 다른 공부도 겸하면서 급하게 하다보니까 다 까먹어버렸다.
그러므로 MFC공부부터 해보자.

 

MFC의 winMain 진입점은?

MFC를 켰을 때 가장 당황하는 부분이 있다.

그것은 바로 main 진입점이 안보인다는 것...

하지만 그것은 정상이다. 왜냐하면 MFC프레임워크에서 대신 진입점 처리를 담당하기 때문이다.

세세한 초기화는 신경쓰지말고 필요한 기능은 가상함수 콜백으로 다 제공해줄테니 거기서 구현해라!! 인것 같다... 아니면 말고...


내일은 MFC 애플리케이션의 게임 GUI 배치와 서버 로직 테스트를 해봐야겠다.

 

 

'일지' 카테고리의 다른 글

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 3일차  (3) 2026.03.19
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차  (1) 2026.03.18
[C++] 오늘의 삽질기 - 메모리 풀  (3) 2025.07.11
[C++] 오늘의 삽질 - inline 키워드에 대하여.  (3) 2024.12.19
[그래픽스] 다양한 블러 효과  (0) 2024.12.10

[네트워크] 네트워크 엔디안

김빡주 | 2026. 3. 12. 16:31 |

현대시대에서 하드웨어는 메모리에 데이터를 메모리에 저장할 때 두가지 방식을 씁니다.

데이터를 큰 바이트부터 저장하는 빅엔디안 방식과,

데이터를 작은 바이트부터 저장하는 리틀엔디안 방식이 있습니다.

 

그렇다면, 각기 다른 하드웨어에서 데이터를 저장하는 순서가 다르다면 어떻게 될까요?

당연히 문제가 생깁니다...

따라서, 네트워크 환경에서는 모든 데이터를 BigEndian을 표준으로 정하게 되었습니다.

이것을 Network Byte Order라고 명칭합니다.

반대로, CPU에서 데이터를 저장하는 순서를 Host Byte Order라고 명칭합니다.

 

 

따라서 네트워크 개발시에는 개발 환경이 리틀엔디안인 경우, 보낼 때 빅엔디안 방식으로 데이터를 변경해 보내야 합니다.

C++ 환경에서는 보통 이런식으로 변환합니다.

// 보낼때 데이터 변환
uint16_t port = htons(8080); // htons = host to network short

// 받을때는 반대로
uint16_t port = ntohs(packet_port);

물론, 일부 엔진이나 라이브러리는 변환을 자동처리 해주기 때문에 유의해서 변환하도록 하는게 중요합니다.

 

 

 

 

자료 조사 및 이미지 출처: https://ko.wikipedia.org/wiki/%EC%97%94%EB%94%94%EC%96%B8

'공부 > CS' 카테고리의 다른 글

[네트워크] TCP와 UDP  (0) 2026.03.12
[네트워크] OSI 7계층  (0) 2026.03.11
[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 데드락(Deadlock)  (0) 2024.11.25

[네트워크] TCP와 UDP

김빡주 | 2026. 3. 12. 15:55 |

안녕하세요? 저에요.

저번에는 OSI 7계층에 대해서 글을 써봤는데요?

오늘은 네트워크 프로토콜, 즉 TCP와 UDP에 대해서 알아볼겁니다.

 

각 장치간 데이터를 주고 받기 위해선 데이터를 어떤 형식으로 포장했는지에 대한 규칙이 필요한데요, 이를 프로토콜이라고 합니다.

이 글에는 주로 언급되는 양방향 통신인 TCP와 단방향 통신인 UDP에 대해서 다뤄보겠습니다.


TCP

TCP는 연결 지향 프로토콜이기 때문에 데이터를 보내기 전에 서로 준비가 되어있는지를 확인하는 과정을 거치는데, 이 과정을  3-way handshake라고 합니다.

 

3-way handshake의 과정은 다음과 같습니다.

1. 클라이언트 → 서버 (SYN):
클라이언트가 서버에게 TCP 연결을 요청하는 SYN 패킷을 전송한다.

2. 서버 → 클라이언트 (SYN + ACK):
Listen 상태의 서버는 요청을 받고 연결 요청을 수락하는 SYN+ACK 패킷을 클라이언트에게 전송한다.

3. 클라이언트 → 서버 (ACK):
클라이언트가 서버의 응답을 확인하고 ACK 패킷을 보내며 연결이 성립되고, 이후 양방향 데이터 통신이 가능해진다.

TCP는 패킷 순서를 보장하고 유실된 패킷을 재전송하는 등 신뢰성을 보장하는 기능이 있기 때문에 UDP에 비해 지연이 발생할 수 있습니다.

 

장점:

1. 패킷의 순서가 보장됩니다.
2. 유실된 패킷을 감지하고 재전송을 요청할 수 있습니다.

단점:

1. 안정성 검사를 거치는 과정때문에 속도가 느립니다.
2. 헤더에 여러 정보가 포함되어 헤더 크기가 비교적 큽니다.


UDP

UDP는 비연결 프로토콜입니다. 때문에 대상이 준비가 되어있든 말든 일단 데이터를 보냅니다.

서로간 통신을 하지 않는다는점에서 이점과 단점이 명확합니다.

 

장점:

1. 일방적으로 보내기 때문에 TCP에 비해 지연이 적습니다.

2. 헤더에 필요한 정보가 별로 없어 TCP에 비해 헤더 크기가 작습니다.

 

단점:

1. 패킷의 순서가 보장되지 않습니다.

2. 유실된 패킷을 감지하거나 재전송할 수 있는 기능이 없습니다.


정리

항목 TCP UDP
연결 방식 연결 지향 비연결
신뢰성 있음 없음
순서 보장 있음 없음
재전송 있음 없음
헤더 크기 비교적 큼 비교적 작음

 

 

'공부 > CS' 카테고리의 다른 글

[네트워크] 네트워크 엔디안  (0) 2026.03.12
[네트워크] OSI 7계층  (0) 2026.03.11
[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 데드락(Deadlock)  (0) 2024.11.25

[네트워크] OSI 7계층

김빡주 | 2026. 3. 11. 17:58 |

헬로 저에요.

저번에 각 프로세스끼리의 통신에 대해서 간단히 다뤘던 적이 있었는데요,

동일한 장치 내에서 이루어지기 때문에 통신은 맞지만 네트워크는 아니었습니다.

그래서 오늘은 네트워크에 대해서 써보려고 합니다.

레스기릿...


네트워크란?

네트워크란, 물리적으로 독립되어 있는 두 장치(노드)가 서로 연결(링크)되어 데이터를 주고 받는 것을 말합니다.

노드는 네트워크 공간 상에서 데이터를 송수신할 수 있는 장치입니다. (ex. 프린터, 컴퓨터, 핸드폰 등)
링크는 각 노드간을 연결하는 통신 수단입니다. (ex. 와이파이, 블루투스, 광케이블 등)
도로망으로 생각하면 쉽습니다. 노드는 도시, 링크는 도로...

OSI 7계층

이미지 출처: https://connect.kinx.net/article/69

OSI 7계층은 네트워크 환경에서 데이터를 전송하는 과정을 7개의 단계로 나누어 각 단계를 계층(Layer)으로 분류한 모델입니다.

 

이처럼 계층을 나눈 이유는 크게 두가지가 있습니다.

첫 번째는, 다양한 네트워크 관련 하드웨어와 소프트웨어가 개발되면서 일어날 호환성 문제를 막기 위해 표준화한 모델입니다.

두 번째는, 네트워크 통신 과정을 역할별로 분리하여 모듈화해 목적에 맞게 사용자에게 통신 수단을 제공하기 위함입니다.

이러한 계층 구조를 사용하게 되면 각 단계의 역할을 명확하게 구분할 수 있어 시스템 설계와 유지보수가 용이해집니다.

더불어 네트워크 통신 과정을 계층별 용어로 구분하기 때문에 흐름이 한눈에 보기 쉽고, 의사소통을 명확하게 할 수 있다는 장점도 있습니다.

 

각 계층에 대한 간단한 역할은 다음과 같습니다.

계층 이름 역할
7 Application 사용자가 네트워크 서비스를 쉽게 사용할 수 있도록 통신 인터페이스 제공
6 Presentation 암호화/복호화, 압축 등 데이터 표현 방식 처리
5 Session 통신 세션의 상태를 관리
4 Transport 포트 번호를 이용해 목적지 프로세스에 데이터 전달 (TCP/UDP)
3 Network IP를 활용해 목적지 호스트로 데이터(패킷) 전송, 네트워크 간의 최적의 경로 설정
2 Data Link 같은 네트워크 내 인접 노드 간 데이터(프레임) 전달
1 Physical 데이터(Bits)를 전기적 신호로 변환해 같은 전송 매체끼리 주고받음

 


다음엔 TCP와 UDP에 대해 알아볼거에요... 와~

'공부 > CS' 카테고리의 다른 글

[네트워크] 네트워크 엔디안  (0) 2026.03.12
[네트워크] TCP와 UDP  (0) 2026.03.12
[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 데드락(Deadlock)  (0) 2024.11.25

[Roblox] Roblox에 대해 알아보자.araboja

김빡주 | 2026. 1. 27. 12:37 |

안녕하세요. 저에요.

하루하루 빠르게 바뀌는 메타를 따라가지 못하는 저를 보면서 두려움을 느끼게 되었습니다.

그래서 MZ해지기로 결심하였습니다.

네, 요즘 MZ들 사이에서 유행하는 Roblox를 해보았습니다.(엄근진)


"Roblox를 했다"는 틀린 표현이다?

제가 앞서 "Roblox를 했다."라는 표현을 썼는데요, 사실 이 표현은 틀린 표현입니다.

왜냐하면 Roblox는 게임이 아니라 플랫폼이기 때문이죠.

 

네, Roblox는 사용자 제작 경험. 즉 UGC기반 플랫폼입니다.

자유롭게 창작하고, 자유롭게 공유하고, 자유롭게 즐긴다.

라고 설명할 수 있겠네요.

Roblox Player 내부


Roblox는 왜 유행하는 것인가?

그럼 Roblox는 왜 이렇게 유명해진 걸까요?

당연히도 여러 이유가 있습니다.

아쉽지만 저는 전문가가 아니기 때문에 자세한 분석보단, 경험으로 느낀 점을 말해보겠습니다.


1. 단순한 게임부터, 무거운 게임까지 전부 있다.

Roblox에는 여러 콘텐츠들이 존재합니다.

각자의 게임 취향도 다양하지만, 상황에 맞게 하고 싶은 게임이 달라지는데요.

  • 내가 킬링타임용으로 가볍게 머리 식히고 싶어! 
  • 친구와 함께 격투 게임, 협동 게임 을 하고 싶어!
  • 뭔가 진득한 RPG도 해보고 싶은데?

Roblox는 이를 가볍게 충족해 줍니다.

 

2. 나만의 공간을 부여해 준다.

Roblox는 사용자 나이를 확인하는 절차가 있습니다. 왜일까요?

바로 미성년자를 보호하기 위함과, 적합한 커뮤니케이션 공간을 마련해 주기 위해서입니다.

저야 성인이지만, 저도 어렸을 때 어른의 눈길을 피해서 나만이 아는 공간에서 친구와 놀았던 경험이 있습니다.

Roblox를 하며 그런 경험을 주려고 했던 게 강하게 느껴졌습니다.(Roblox가 그걸 의도한 지는 모르겠습니다.)

 

3. 게임은 독립적이지만, 인맥은 공유한다.

"내가 A게임을 통해 만난 친구가 있는데, A게임을 접었어. 그 친구는 뭐 하고 살까?"

라는 경험을 해보신 적이 있으신가요? (저는 있음)

하지만, Roblox안에서는 다른 콘텐츠를 해도 인맥을 공유하기 때문에, 자유롭게 연락이 가능합니다.

 

4. 사용자가 자신의 캐릭터를 만들어나간다.

코디라는 것은 게임에서도 중요한 역할을 하는데요,

Roblox는 캐릭터의 신체 커스터마이징부터, 외형까지 전부 커스텀할 수 있습니다.

각자의 캐릭터를 꾸미고, 여러 컨텐츠를 통해 서로의 캐릭터를 감상하거나 구경하는 경험을 제공합니다.


이렇게 보니까 왜 인기가 많은지 좀 실감했습니다.


사실 저도 초등학생 때부터 프로그래머를 꿈꿔왔는데요?

여러 네이버 카페에서 조곤조곤 서로의 창작물을 공유하던 경험이 있습니다. 일부는 지금까지도 연락을 하고 있구요.

지금도 그 네이버 카페가 없었다면 게임 개발을 꿈꿀 수 있었을까?라는 생각을 합니다.

 

내가 어렸을 때 Roblox가 흥행했다면, 뭔가 더 쉽게 개발을 하며 내 아이디어들을 뽐내볼 수 있지 않았을까! 하는 생각도 들었습니다.

창작가들은 누군가에게 내 창작물을 공유할 기회가 없으면 흥미가 떨어질 수밖에 없다고 생각합니다.

정말 좋은 취지에서 잘 만든 플랫폼이라고 생각합니다.

 

 

게시글에 사용한 이모티콘 출처: 밤우 스피키콘

'공부 > 게임 개발' 카테고리의 다른 글

행동 트리(Behavior Tree, BT)  (3) 2025.07.05
ImGui NodeEditor 집중 탐구  (0) 2025.06.09
[자체 엔진 개발] 파일 디렉터리 이벤트 감시자  (0) 2025.05.20
[유니티] 유니티의 모델 렌더링 구조를 하나하나 파헤쳐보자  (0) 2024.12.19
[Visual Studio] 헤더를 미리 컴파일 하는 방법  (0) 2024.04.11

[C++] 가상 함수 테이블

김빡주 | 2025. 7. 13. 23:54 |

2025/12/19 게시글 수정/업데이트

2026/01/21 게시글 수정/업데이트

 

안녕하세요. 저에요.

오늘은 C++에서 가상 함수 테이블이 왜 중요한지에 대해 이야기해보려고 해요.

가상 함수 테이블은 C++에서 다형성을 지원하는 핵심 요소 중 하나에요.

어느정도냐면, 이걸 모르면 "나는 전혀 C++하고 있지 않아!" 상태입니다.

 

하지만 모르니까 글을 누르셨겠죠?

모른다니... 전혀 C++하고 계시지 않습니다.

바로 시작해 볼게요.

 

★ ★ ★ 중요한 포인트를 볼드체로 해둘테니 꼭꼭 외워두시길 바랍니다요!!! ★ ★ ★


가상 함수 테이블이란?

가상 함수 테이블은 런타임중에 객체가 자신의 타입에 맞는 함수를 실행할 수 있는 런타임 다형성을 지원해주는 역할을 수행해요.

멤버 함수에 virtual 키워드를 붙이는 순간, 컴파일러는 이 객체를 다형 객체라고 판단하고 가상 함수 테이블을 만듭니다.

 

코드를 보면서 봐볼까요?

class VirtaulBase
{
public:
	virtual ~VirtaulBase() = default;
	virtual void func1() { std::cout << "VirtualBase::func1" << std::endl; }
	virtual void func2() { std::cout << "VirtualBase::func2" << std::endl; }
	virtual void func3() { std::cout << "VirtualBase::func3" << std::endl; }
	virtual void func4() { std::cout << "VirtualBase::func4" << std::endl; }
};

class VirtualDerived : public VirtaulBase
{
public:
	virtual ~VirtualDerived() { std::cout << "VirtualDerived::~VirtualDerived" << std::endl; }
	void func1() override { std::cout << "VirtualDerived::func1" << std::endl; }
	void func2() override { std::cout << "VirtualDerived::func2" << std::endl; }
	void func3() override { std::cout << "VirtualDerived::func3" << std::endl; }
	void func4() override { std::cout << "VirtualDerived::func4" << std::endl; }
};

int main()
{
	VirtaulBase* pBase = new VirtualDerived;
	pBase->func1();
	pBase->func2();
	pBase->func3();
	pBase->func4();
	delete pBase;
}

이런 코드가 있을 때, VirtualDerived의 가상 함수 테이블은 어떻게 되어있을까요?
컴파일러마다 다를테지만 MSCV기준 디버깅을 통해 볼 수 있습니다.

함수 주소를 가지고있는 배열이라고 보시면 됩니다.

 

개인적으로 궁금해서 조사해본 TMI를 조금 주저리 해보자면,

더보기

1. 나는 함수만 호출했는데 함수의 인덱스를 어떻게 알고 찾아가지? 하는 의문

컴파일러가 컴파일 타임에 가상 함수를 인덱싱하여 배열에 나열한 후,
가상 함수를 호출하는 코드를 바이너리화 할 때 코드 자체를 바꿔버리는 것 같습니다.

아마도 (*(pBase->vptr[함수 인덱스]))(pBase); 이런식의 코드로 바이너리화해버리는 것 같습니다.

2. __vftable의 배열 크기가 6인데 왜 갯수가 하나 모자라지? 하는 의문

-1번 인덱스를 쓰기 때문이다. (사실 -1을 인덱싱하는건 맞는 문법은 아니긴한데)

-1번 인덱스에는 RTTI의 Completed Object Locater의 정보가 있는 포인터가 담겨있다.

컴파일러마다 다를 수도 있다고도 말하는데, 일단은 MSVC 컴파일러는 이렇게 처리하나보다.

정확히


가상 함수 테이블의 단점

 

그럼 버추얼 테이블은 무적 아님? 개사긴데? 마구마구 써주마!!!

 

절!!! 대!!! 아니다요!!!!

 

버추얼 테이블은 런타임 다형성을 구현하는 데 필수적이지만, 성능에 영향을 미칠 수 있어요.

가상 함수는 호출할 때마다 가상 함수 테이블을 참조하여 어떤 함수를 호출할지 결정하기 때문에 일반 함수 호출보다 비용이 비쌉니다.

하지만 가상 함수를 안쓴다는건 사실 말이 안돼요. 쓸 때는 적절히 사용하는 올바른 프로그래머가 될 수 있도록 해야합니다.


마치며

버추얼 테이블은 C++의 다형성에서 매우 중요한 요소다요.

이걸로 당신은 C++ 걸음마를 한 걸음 떼셨다요.

긴 글 읽어주느라 수고많았습니다요.

다음에 또 만나용.

 

Good Bye

 

'공부 > 프로그래밍 언어' 카테고리의 다른 글

[C++] 나만의 스마트 포인터 만들기 (Safe Pointer)  (2) 2026.04.10
[C++] static_cast와 dynamic_cast의 동작  (2) 2025.07.06
[C++] 다중 상속에서의 업캐스팅, 그리고 포인터 보정  (1) 2025.07.01
[C++] 멀티 쓰레딩 프로그래밍을 해보자  (0) 2024.10.14
[C++] 2진 리터럴(Binary literals)에 대해 알아보자.araboza (C++14)  (2) 2024.09.16

[C++] 오늘의 삽질기 - 메모리 풀

김빡주 | 2025. 7. 11. 19:59 |

안녕하세요. 저에요.

오늘은 여유가 생겨서 나중에 메모리 풀을 구현해보고 싶다는 로망을 실현해 보기로 했어요.

차후 자체 엔진에 도입할 수도 있기 때문이지요.

 

결론부터 말하면 제가 꿈에 그리던 메모리 풀은 사실상 쓰레기였어요. ㄱ-

 

그렇기에 적어보는 글이다요.

삽질에 대한 내용을 적는 일기 개념이기 때문에 메모리 풀이 뭔지는 설명 안 하겠다요.

 

본론으로 바로 들어가자면,

가장 유명하고도 성능 좋은 메모리 풀은 하나의 객체에 대한 타입만을 메모리 풀링해주는 방식이에요.

왜냐하면, 하나의 객체에 대한 메모리만을 풀링 해주면 객체의 사이즈가 고정이기 때문에 메모리 단편화 현상을 아예 없앨 수 있기 때문이에요. 단편화 현상이 없으면 조각화 현상에 대해서 코스트를 투자하지 않아도 되기 때문에 가장 빠르다고 알려진 메모리 풀 패턴이에요.

제가 짜본 코드로 같이 봐볼까요?

더보기
/// 메모리 풀에서 관리되는 모든 객체는 이 클래스를 상속해야 함
class Instance
{
public:
	Instance() = default;
	virtual ~Instance() = default;
	size_t mMemoryIndex = (size_t)-1;
};

/// 고정 크기 메모리 풀
template<typename T>
class MemoryPool
{
	friend class Instance;
public:
	MemoryPool(size_t elementSize, size_t capacity) : mElementSize(elementSize), mCapacity(capacity)
	{
		mMemory = static_cast<char*>(malloc(elementSize * capacity));
		assert(mMemory);

		// 초기 free index 테이블 구성
		for (size_t i = 0; i < capacity; ++i)
		{
			mFreeIndices.push(i);
		}
	}

	~MemoryPool()
	{
		free(mMemory);
	}

	template<typename... Args>
	T* Alloc(Args&&... args)
	{
		if (sizeof(T) > mElementSize) {
			printf("Error: Object size exceeds pool element size.\n");
			return nullptr;
		}

		if (mFreeIndices.empty()) {
			printf("Error: MemoryPool is full.\n");
			return nullptr;
		}

		size_t index = mFreeIndices.top();
		mFreeIndices.pop();
		void* mem = mMemory + (index * mElementSize);
		T* obj = new (mem) T(std::forward<Args>(args)...);
		obj->mMemoryIndex = index;
		return obj;
	}

	void Free(T* instance)
	{
		if (!instance) return;

		size_t index = instance->mMemoryIndex;
		assert(index < mCapacity);
		instance->~T();
		mFreeIndices.push(index);
	}

private:
	size_t mElementSize;
	size_t mCapacity;
	char* mMemory;
	std::stack<size_t> mFreeIndices;
};

사실 이 코드도 특이한 방식이긴 해요.

Instance객체를 상속해야 하는 방식으로 작성되었는데, 이는 추후 자체 엔진에 Instance Manager를 만들어서 풀링 하려고 했기 때문이에요.

하지만 저는 이게 너무 싫었어요. (홍대병)

이 방식은 풀링할 객체 타입이 많아질수록 메모리 풀을 해당 객체 수만큼 만들어줘야 했기 때문인데요?

예를 들어, Transform객체와 Object객체...등 메모리 풀링을 해야 할 객체가 만들어질 때마다 Transform 메모리 풀, Object 메모리 풀... 전부 만들어줘야 하기 때문이에요.

그래서 모든 타입을 풀링할 수 있도록 설계해 봤습니다.

이름하야 Custom Dynamic Memory Pool ! ! !

하지만 모든 타입을 풀링 하려면 필연적으로 조각화 현상이 생기는데요.

때문에 객체를 삭제할 때마다 해당 객체의 뒤에 위치한 메모리를 memmove로 당겨오는 방식(vector의 erase처럼)을 채택했습니다.

코드로 볼까요?

더보기
// 내부 관리용 객체: 메모리 위치, 사이즈, 실제 객체 포인터 등을 담는다
class Instance
{
public:
    void* mPtr = nullptr;
    int    mID = -1;
    size_t mOffset = 0;
    size_t mSize = 0;
};

class MemoryPool
{
public:
    MemoryPool(size_t capacity)
        : mCapacity(capacity), mUsedMemory(0)
    {
        mMemory = static_cast<char*>(malloc(capacity));
        assert(mMemory && "Memory allocation failed");
    }

    ~MemoryPool()
    {
        free(mMemory);
    }

    // 객체 할당
    template <typename T, typename... Args>
    int Alloc(Args&&... args)
    {
        size_t size = sizeof(T);
        size_t start = mUsedMemory;
        size_t end = mUsedMemory + size;

        if (end > mCapacity) {
            std::cerr << "MemoryPool Full!\n";
            return -1;
        }

        void* mem = mMemory + start;
        T* obj = new (mem) T(std::forward<Args>(args)...);

        Instance meta;
        meta.mID = ++UID;
        meta.mOffset = start;
        meta.mSize = size;
        meta.mPtr = obj;

        mInstanceMap[meta.mID] = meta;
        mInstanceList.push_back(&mInstanceMap[meta.mID]);

        mUsedMemory += size;
        return meta.mID;
    }

    // 객체 접근
    template <typename T>
    T* Get(int handle) {
        auto it = mInstanceMap.find(handle);
        if (it == mInstanceMap.end()) return nullptr;
        return reinterpret_cast<T*>(mMemory + it->second.mOffset);
    }

    // 객체 해제 + 조각화 제거
    void Free(int handle) {
        auto it = mInstanceMap.find(handle);
        if (it == mInstanceMap.end()) return;

        Instance& instance = it->second;
        size_t offset = instance.mOffset;
        size_t size = instance.mSize;
        size_t tail = offset + size;
        size_t remain = mUsedMemory - tail;

        // 메모리 이동
        memmove(mMemory + offset, mMemory + tail, remain);

        // 모든 Instance의 offset 수정 및 포인터 갱신
        for (auto& meta : mInstanceList) {
            if (meta->mOffset > offset) {
                meta->mOffset -= size;
                meta->mPtr = mMemory + meta->mOffset;
            }
        }

        // 리스트에서 제거
        mInstanceList.erase(
            std::remove_if(mInstanceList.begin(), mInstanceList.end(), [&](const Instance* i) {
                return i->mID == instance.mID;
                }),
            mInstanceList.end()
        );
        mInstanceMap.erase(it);

        mUsedMemory -= size;
    }

private:
    size_t mCapacity;
    size_t mUsedMemory;
    char* mMemory;

    std::vector<Instance*> mInstanceList;
    std::unordered_map<int, Instance> mInstanceMap;
};

급하게 짜느라 조잡하긴 하다...

짜고 보니까 문제가 생각보다 많았습니다.


1. memmove가 기대 이하의 퍼포먼스를 보여줬다.

일단 memmove를 제대로 공부하지 않고 멋대로 쓴 게 패착이었습니다.

당연히 저수준 메모리 이동은 비용이 적을 줄 알았는데, 생각보다 느렸습니다. ㅋㅋ

근데 아직도 왜 이렇게 느린지 모르겠는데, 이건 더 공부해 봐야겠네요.

아무튼 매우 느리답니다.

참고로 memcpy로 교체도 해봤는데 속도는 똑같았습니다.


2. 메모리만 옮겨서 끝이 아니라 메타 정보도 바꿔줘야 했다.

일단 모든 타입에 메모리를 풀링 하려다 보니, 각 타입마다 사이즈가 달라 메모리를 풀링 하기 어려웠습니다.

때문에 해당 메모리의 Offset과 Size 등의 메타정보가 있어야 메모리를 해제해 줄 수 있었는데요.

객체를 삭제하면 뒤에 있는 메모리를 옮겨오는 것까진 했는데, 그럼 뒤에 있는 객체의 메타 정보도 하나하나 수정해줘야 했습니다.

그래서 이미 이때부터 아...망했구나를 직감했습니다.


3. 메모리 이동 시 버추얼 테이블이 깨진다.

이건 좀 충격이었습니다...

속도 다 버리고 구현이라도 해보자는 느낌으로 구현 다 해놨는데. 자꾸 Free 할 때 터지는 거예요. 디버깅으로 보니까 버추얼 테이블이 NULL이더라구요.

추후 자체 엔진에 도입하기 위해 Instance라는 객체를 상속받는 구조로 설계했기 때문에 버추얼 테이블이 있어야 상속 트리를 타고 소멸자가 호출되는 구조인데, 버추얼 테이블이 NULL라 터지는 것이었습니다.

결과적으로, 메모리를 옮기는 건 저수준 작업이라 버추얼 테이블은 안 옮겨진다고 하네요.... 때문에 상속 기반 객체는 메모리 풀링에 사용할 수 없었습니다.

제가 2번에 언급했던 메타 정보를 하나하나 바꿔줘야 했던 것처럼, 버추얼 테이블도 옮겨주는 작업이 필요한데? 사실상 불가능하다고 합니다.

그래서 상속도 안 하고 사용하게 만들었습니다... 못쓰더라도 끝은 봐야 하니까...


End. 속도 비교

대망의 속도 비교가 빠질 수 없겠죠? 사실 이미 망한 거 알고 있지만 테스트해봤습니다.

 

(Custom Dynamic Memory Pool vs New/Delete)

... 그만 알아보도록 하자.


그래서 제 인생은 망했습니다.

그냥 new/delete나 씁시다. 와~~

애매하게 만들 거면 만들지 말기로...

 


번외 1. 고정 타입 메모리 풀의 성능은 어떠할까?

아까 고정 타입 메모리 풀 코드도 올렸는데요?

이건 성능이 어떻게 나올까요?

(Fixed Type Memory Pool vs New/Delete)

 

와 ㅋㅋㅋㅋ

역시 유명한 패턴엔 이유가 있습니다. 그냥 이거나 써야겠다. 와~~


번외 2. 왜 저는 님 코드 복사해서 해봤는데 차이가 별로 안 나요?

디버그 말고 릴리즈로 실행해 보세요.

비주얼스튜디오 디버그는 생각보다 사람 손을 탈수록 많이 느리답니다?

비단 메모리 풀만이 아니라 이런저런 코드도 디버그에서 매우 느린 경우가 많아요.

'일지' 카테고리의 다른 글

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차  (1) 2026.03.18
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 1일차  (4) 2026.03.16
[C++] 오늘의 삽질 - inline 키워드에 대하여.  (3) 2024.12.19
[그래픽스] 다양한 블러 효과  (0) 2024.12.10
[C++] 오늘의 삽질 - Color 클래스를 만들어보자  (0) 2024.09.13

[C++] static_cast와 dynamic_cast의 동작

김빡주 | 2025. 7. 6. 22:01 |

안녕하세요. 저에요.

저번에 다중 상속에서의 캐스팅 주의점에 대해 알아봤는데요.

이제 캐스팅은 단순 타입만 명시적으로 바꾸는 게 아니라, 오프셋에 맞게 주소 값을 바뀔 수 있다는 것을 알게 되었습니다.

오늘은 어떻게 그런 식으로 캐스팅이 동작하는지 알아볼 거예요. (대충 지루하고 현학적인 이야기다요)

C++스타일의 cast에는 여러 종류가 있는데요.

오늘은 그중에서 가장 많이 쓰이는 cast은 static_cast와 dynamic_cast에 대해 알아봅시다.


● static_cast

static_cast에 대해 많은 오해를 하고 있는 부분이 있어요.

그건 바로 static_cast는 컴파일 타임에 에러를 검출해 낸다는 것인데요.

근데 써보신 분들은 이런 경험을 겪으신 적이 있을 겁니다.

난 분명 static_cast를 썼고, 컴파일도 제대로 되었는데, 런타임 중에 static_cast에서 터졌어!!

 

분명 컴파일 타임에 에러를 검출해 준다 했는데... 컴파일이 되었는데 터졌어.... 뭐지... 뭔가 일어나고 있음...

 

이런 경우를 겪으신 분 계신가요? (전 없긴함)

제대로 알고 써야 합니다!!

static_cast는 컴파일 타임에서 A와 B에 대한 상속 관계에 대한 합리성만 판단합니다.

무슨 말인지 예시를 들어보자면, (대충 지루하고 현학적인 예시)

영희는 철수에게 생일선물을 주려고 한다.
영희는 철수가 닌텐도를 가지고 싶다는 이야기를 들었다..
하지만 철수가 무슨 기종을 가지고 싶은지 명확히 모른다.....
그래서 영희는 최신 기종인 닌텐도 스위치를 선물하기로 했다!
아뿔싸! 철수가 가지고 싶었던 기종은 닌텐도 DS였다!!!!

이런 상황인 것이에요.

런타임 중에 static_cast가 터졌다는 것은,

닌텐도 DS든, 닌텐도 Wii든, 닌텐도 스위치든 전부 닌텐도라 할 수 있는데요.

영희는 이런 상황에 닌텐도 스위치가 닌텐도라는 합리성만 판단하고 위험하게 닌텐도 스위치를 선물한 겁니다.

 

코드로 표현하자면 이런 거죠.

class Nintendo {};
class Nintendo_Switch : public Nintendo {};
class Nintendo_DS : public Nintendo {};

// 1. 철수: 아 닌텐도가 가지고 싶다!
Nintendo_DS* ds = new Nintendo_DS(); 

// 2. 영희: 음... 철수가 닌텐도가 가지고 싶다 했지...
Nintendo* nt = ds; // Nintendo로 업 캐스팅

// 3. 영희: 그럼 최신 기종인 닌텐도 스위치를 선물해야겠다!
Nintendo_Switch* sw = static_cast<Nintendo_Switch*>(nt); // 컴파일러는 허용, 그러나 런타임에서는 잘못된 다운캐스팅

// 4. 철수: ㄱ-

컴파일러 입장에선 Nintendo_Switch* ⇐ Nintendo* 관계가 성립하므로 캐스트 자체는 허용되지만, 실제로 ds는 Nintendo_Switch의 주소를 가리키는 게 아니기 때문에 알 수 없는 동작을 하게 됩니다.(보통은 터짐)

 

그러면 이런 상황에서 안전하게 확인하는 방법이 뭘까요?

그건 바로 dynamic_cast입니다!


● dynamic_cast

dynamic_cast는 말 그대로 런타임 캐스팅입니다.

왜 런타임일까요? 그건 바로 RTTI를 이용해 타입 정보를 확인하여 유효성 검사를 실시하기 때문인데요.

RTTI란?
Run-Time Type Information의 약자로, 컴파일러가 생성한 런타임에서 사용할 객체들의 정보를 포함한 메커니즘입니다.

 

때문에 dynamic_cast는 RTTI를 통해 상속 관계를 확인하여 올바른 관계를 확인하여 안전한 타입 캐스팅을 지원합니다.

실패할 시 터지지 않고 반환 값을 nullptr을 반환해 실패 여부도 확인할 수 있죠.

그럼 무조건 dynamic_cast 써야겠네? 무적이네?

절!!! 대!!! 아닙니다!!!!

상속 관계를 확인하는 과정이 빠를 리가 없겠죠?

그래서 dynamic_cast는 속도가 느립니다. 생각보다 훨씬요.

때문에 상속 관계가 복잡하고 깊어지면 더 느려집니다.

 

그래서 자체 엔진 제작할 때 일화를 들려드리자면,

GetComponent<>() 함수 같은 것들에 dynamic_cast를 썼었는데....

테스트해 보니까 속도가 좀 느리더라구요....

그래서 다음에 만들 엔진엔 타입별 ID를 해싱해서 부여하고, 해시 맵으로 성능향상을 노려볼까 합니다.


● 마치며

이런 과정을 통해 cast함수들은 오프셋을 받아와 주소 재보정을 거치게 됩니다.

그래서 저희는 캐스팅 후에 주소 값이 달라진 걸 저번 포스팅을 통해 확인했구요.

 

이상 저였습니다. 감사합니다.

'공부 > 프로그래밍 언어' 카테고리의 다른 글

[C++] 나만의 스마트 포인터 만들기 (Safe Pointer)  (2) 2026.04.10
[C++] 가상 함수 테이블  (5) 2025.07.13
[C++] 다중 상속에서의 업캐스팅, 그리고 포인터 보정  (1) 2025.07.01
[C++] 멀티 쓰레딩 프로그래밍을 해보자  (0) 2024.10.14
[C++] 2진 리터럴(Binary literals)에 대해 알아보자.araboza (C++14)  (2) 2024.09.16

행동 트리(Behavior Tree, BT)

김빡주 | 2025. 7. 5. 23:19 |

안녕하세요 접니다.
게임 개발을 하다 보면 어떻게든 몬스터든, NPC든 AI를 도입해야 할 때가 오는데요.
예를 들어 보스몬스터의 행동 패턴, NPC의 이동 패턴 등이 있습니다.
그럼 당신은 이 패턴들을 하나하나 하드코딩할 것인가?

그러지 말고 행동 트리를 사용하도록 하자.
 
앞서, 메이플스토리 월드 Document에 행동 트리 문서를 통해 공부했습니다.
행동 트리를 활용한 AI 만들기: MapleStory Worlds Creator Center

 

MapleStory Worlds Creator Center

메이플스토리 월드의 크리에이터 센터입니다.

maplestoryworlds-creators.nexon.com

(여담으로, 메이플스토리 월드가 Documents를 진짜 정성 들여 작성해 놨습니다. 굳이 메이플스토리 월드를 사용하지 않더라도, 엔진 개발 관련 좋은 정보가 많으니 가끔 찾아보는 것도 좋을 것 같습니다.)


●행동 트리(Behavior Tree)란?

행동 트리는 노드 + 트리 구조의 행동 제어 모델입니다.

출처: MapleStory Worlds Creator Center

"행동 제어 모델이 뭔데? 로봇임?" 싶지만 사실 자료구조 중 하나일 뿐입니다.
행동 트리는 Root노드를 시작으로, 순서에 맞게 각 노드를 처리하게 됩니다.
처리할 노드는 일반적으로 3가지 상태를 가지게 되는데요.

상태 설명
Success 노드의 작업이 의도에 맞게 완수(처리)된 상태.
Failure 노드의 작업이 의도와 맞지 않게 완수(처리)된 상태.
Running 노드의 작업이 처리 중인 상태.

 
 
노드의 종류는 크게 2가지가 있습니다.
바로 Action Node와 Composite Node가 있는데요.


Action(Leaf) Node

: 객체의 실질적인 행동을 처리하는 노드입니다.
하위(자식) 노드를 가지지 않으므로, Leaf Node라고도 불립니다.
 

Composite Node

: 하위(자식) 노드의 흐름을 제어하는 노드입니다.
Composite Node는 그 안에서도 일반적으로 3가지의 종류를 가지는데요.
1. Sequence Node: 자식 노드가 Failure를 반환하기 전까지 순서대로 Node를 실행합니다. 자식 노드가 Failure를 반환하는 경우 그 즉시 실행을 멈추고 Failure를 반환합니다. 모든 자식노드가 Success를 반환한 경우 Success를 반환합니다.
2. Selector Node: 자식 노드가 Success를 반환하기 전까지 순서대로 Node를 실행합니다. 자식 노드가 Success를 반환하는 경우 그 즉시 실행을 멈추고 Success를 반환합니다. 모든 자식노드가 Failure를 반환한 경우 Failure를 반환합니다.
3. Parallel Node: 자식 노드를 조건 없이 동시에(혹은 순서대로) 실행합니다.
성공/실패를 판단하는 기준은 설정된 정책(예: 성공 기준 수, 실패 기준 수 등)에 따라 달라집니다.
예를 들어, 자식 노드 중 일정 수 이상이 Success를 반환하면 전체를 Success로 간주할 수 있고, 반대로 일정 수 이상이 Failure를 반환하면 전체를 Failure로 판단할 수 있습니다.
이 노드는 복수의 행동을 병렬적으로 처리해야 할 때 사용됩니다.

 


 


 

●행동트리의 단점?

행동 트리는 그러면 무조건 좋은걸까요?
그건 또 아닌데용
행동 트리도 명확한 단점이 있는데요.



복잡한 트리 관리: 트리위 규모가 커지면 디버깅이 복잡해질 수 있습니다.

상태 전이의 가시성이 떨어짐: 상태 간 전이 흐름이 FSM보다 가시성이 떨어집니다.

런타임 비용 증가: 매 프레임마다 트리를 순회하며 조건 검사를 하는데 이는 성능 면에서 영향을 끼칩니다.


 


●마치며

사실 행동 트리와 FSM 중 뭘 써야한다는 정답은 명확하지 읺은 것 같습니다.
프로젝트를 하며 느낀건데, 행동 트리 로직을 짜는 것이 FSM 생각보다 접근성이 높은 것 같습니다.
또한 FSM만큼 상태에 대한 전이가 시각적으로 명확하게 보이지도 않은 것 같구요.
그래서 나중에 자체엔진을 만들 땐 FSM과 행동 트리를 섞어 쓰는 하이브리드 전략도 기용해볼 것 같습니다.

 


이상 저였습니다. 감사합니다.

'공부 > 게임 개발' 카테고리의 다른 글

[Roblox] Roblox에 대해 알아보자.araboja  (4) 2026.01.27
ImGui NodeEditor 집중 탐구  (0) 2025.06.09
[자체 엔진 개발] 파일 디렉터리 이벤트 감시자  (0) 2025.05.20
[유니티] 유니티의 모델 렌더링 구조를 하나하나 파헤쳐보자  (0) 2024.12.19
[Visual Studio] 헤더를 미리 컴파일 하는 방법  (0) 2024.04.11

[C++] 다중 상속에서의 업캐스팅, 그리고 포인터 보정

김빡주 | 2025. 7. 1. 19:04 |
오늘 C++에 대한 빨간약을 또 하나 먹었다...

 

안녕하세요. 저에요.

오늘도 슬프게 코딩을 하던 도중 C++ 빨간약을 먹었는데요?

다중 상속에 대한 것인데, 매우 간과하고 사용하고 있었어요.

C++의 세계란 알아도 끝이 없네요.

 

함께 알아가보자.


상속의 구조

객체 지향 프로그래밍에서 상속은 매우 중요한 개념인데요.

이런 신기한 상속은 컴파일러가 어떤 식으로 처리할까요?

 

상속은 최상단 상속 객체부터 차근차근 메모리 레이아웃을 배치하게 되는데요. (알고계셨다구요? 그럼 바로 아래 파트로 넘어가시죠.)

그게 무슨 소리임? 하실까봐 예시 코드를 하나 가져왔습니다.

class A 
{ 
public: 
    int a_member; 
};
class B 
{
public: 
    int b_member; 
};
class C : public A, public B 
{
public: 
    int c_member; 
};

 

A와 B를 상속받은 클래스 C가 있다고 칩시다.
그럼 메모리 레이아웃은 어떻게 형성될까요?

바로 C를 만들면서 상속한 순서대로 레이아웃이 형성됩니다.

 

바로 아래처럼 형성되는데요.

...저 같은 놈 말은 안믿는다고요?

 

그럼 비주얼 스튜디오 메모리 레이아웃을 당장 켜보세요.

그럼 제 말대로 나옵니다. 굿.

 

그럼 검증해볼까요?

int main()
{
	C* c = new C;
	std::cout << "a_member : " << &c->a_member << std::endl;
	std::cout << "b_member : " << &c->b_member << std::endl;
	std::cout << "c_member : " << &c->c_member << std::endl;
	return 0;
}

위 예시 클래스를 이용해 main함수를 작성해 보았습니다.

결과는 어떻게 나올까요?

더보기

결과:

4byte 단위 오프셋이 제대로 적용된 모습입니다.

여담으로, C의 상속 순서를 뒤집으면 어떻게 될까요?

더보기

이렇게 상속하면...

이렇게 뜬답니다?
와~~~ 신기하다~~~

 

지금까지 상속의 구조를 알아봤는데요.

"이게 업캐스팅이랑 뭔 상관임? 내 시간 돌려놓으셈!!!"

 

다음 파트에서 설명해보겠습니다...


다중 상속에서의 업캐스팅? 알고 쓰자!

위 설명에 사용한 예제에 이어 다음과 같은 코드를 추가해봤습니다.

c를 할당하고, c를 A객체로 업캐스팅한 a와 B객체로 업캐스팅한 b가 있습니다.

class A { public: int a_member; };
class B { public: int b_member; };
class C : public B, public A { public: int c_member; };

int main()
{
	C* c = new C;
	A* a = c;
	B* b = c;
	void* aPtr = static_cast<void*>(a);
	void* bPtr = static_cast<void*>(b);
	void* cPtr = static_cast<void*>(c);
	std::cout << "a: " << aPtr << std::endl;
	std::cout << "b: " << bPtr << std::endl;
	std::cout << "c: " << cPtr << std::endl;
	return 0;
}

 

결과는 어떨까요?

똑같지 않을까요?

더보기

결과:

c == a, c != b

충격적이게도 c와 c를 업캐스팅한 a는 같지만 c를 업캐스팅한 b는 다르다...

결과를 보셨나요?

 

왜 이런 결과가 나왔을까요.

사실 앞서 한 상속의 구조 설명을 잘 읽어보면 답이 있습니다. (괜히 설명한게 아니라고)

c는 사실 사실 가장 첫 상속 객체의 시작 주소를 가리키는데요.

C 클래스는 A->B순서로 상속받았으니 사실상 C에 속한 A 상속 객체의 시작 주소를 가리키고 있는겁니다.


그래서 결과적으로,

c(생성한 원본 C 객체) >> (C 클래스는 C::A의 시작 주소다.)

a(C객체에서 업캐스팅한 A객체) >> (C::A의 시작 주소) 

이므로 c == a가 되는 것이고,

b(C객체에서 업캐스팅한 B객체) >> (C::B의 시작 주소 (C의 시작 주소(A)에서 A객체의 메모리 크기만큼 건너 뛴 주소)

이므로 c != a가 되는 것입니다.

따라서 b는 c혹은a의 + 4byte(A객체의 크기)만큼의 주소를 가지게 되는거죠.


사실 이렇게 보면 당연한 소린데, 왜 이런 생각을 안했는지 모르겠네요. 쩝


마치며,

근데 뭔가 이상하지 않나요?

제가 아는 캐스팅은 주소는 안바뀌고, 해당 자료형을 바꿔서 사용하는 줄 알았는데?

c의 주소를 b로 캐스팅하니까 주소가 바뀐거 아닙니까?

그렇다는 것은 캐스팅 메소드가 알아서 주소를 C::B의 오프셋을 적용해서 반환해줬다는 건데.... 이거 괜찮은거 맞음...?

그것에 대해선 다음 포스팅에 다뤄보겠습니다.

 

이상 저였습니다. 감사합니다.

'공부 > 프로그래밍 언어' 카테고리의 다른 글

[C++] 가상 함수 테이블  (5) 2025.07.13
[C++] static_cast와 dynamic_cast의 동작  (2) 2025.07.06
[C++] 멀티 쓰레딩 프로그래밍을 해보자  (0) 2024.10.14
[C++] 2진 리터럴(Binary literals)에 대해 알아보자.araboza (C++14)  (2) 2024.09.16
[C++] auto의 타입 추론에 대하여 알아보자.araboza (C++11)  (0) 2024.09.13

ImGui NodeEditor 집중 탐구

김빡주 | 2025. 6. 9. 11:34 | 댓글 -

보호된 게시글입니다. 비밀번호를 입력하세요.

[자체 엔진 개발] 파일 디렉터리 이벤트 감시자

김빡주 | 2025. 5. 20. 10:34 | 댓글 -

보호된 게시글입니다. 비밀번호를 입력하세요.

LastLight

김빡주 | 2025. 3. 5. 11:12 | 댓글 -

보호된 게시글입니다. 비밀번호를 입력하세요.

[유니티] 유니티의 모델 렌더링 구조를 하나하나 파헤쳐보자

김빡주 | 2024. 12. 19. 16:39 |

애니메이션

기본적으로 유니티의 애니메이션은 다음으로 구성되어 있다.

Pose : 각 한 모션에 대한 애니메이션의 키 값(각 프레임별로 메쉬가 어느 위치에 있는지)들로 구성된 확장자.

pose구성도

Pose Controller : 각 Pose들을 FSM식으로 구성하여 하나의 모델 애니메이션 사이클을 나타내는 정보. 

Pose Controller 구성도

Animator Component : Pose Controller를 참조해 모션을 직접 컨트롤 하는 개체.

Animator Component 구성

 

'공부 > 게임 개발' 카테고리의 다른 글

[Roblox] Roblox에 대해 알아보자.araboja  (4) 2026.01.27
행동 트리(Behavior Tree, BT)  (3) 2025.07.05
ImGui NodeEditor 집중 탐구  (0) 2025.06.09
[자체 엔진 개발] 파일 디렉터리 이벤트 감시자  (0) 2025.05.20
[Visual Studio] 헤더를 미리 컴파일 하는 방법  (0) 2024.04.11

[C++] 오늘의 삽질 - inline 키워드에 대하여.

김빡주 | 2024. 12. 19. 11:14 |

내가 inline키워드를 명시적으로 쓰는 타입의 프로그래머인데, 링커에러가 뜬다? 그러면 이 글을 보시길 바란다.

 

3D자체엔진을 제작하려고 3D리소스 래핑을 하는 도중에 inline에 대하여 엄청난 오용을 하고 있었다는 사실을 깨달았다.

// Material.h
// FBX를 로드할 때 가져오는 정보. 
class MaterialResource : public IGraphicsResource
{
	using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
	explicit MaterialResource(std::wstring_view _name);
	virtual ~MaterialResource();
public:
	inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture);
	inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType);
private:
	MaterialMapArray mMaterialMaps;
};

// Material.cpp
MaterialResource::MaterialResource(std::wstring_view _name)
	: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
inline void MaterialResource::SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture)
{
	mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture;
}
inline std::shared_ptr<Texture> MaterialResource::GetMaterialMap(eMaterialMapType _mapType)
{
	return mMaterialMaps[static_cast<UINT>(_mapType)];
}

이런 클래스가 있다.

이 클래스는 일단 SetMaterialMap() 함수를 사용하면 에러를 이렇게 낸다.

개같은...링커에러

그렇다. 그 악명높은 링커에러가 뜬다.

근데 정의와 구현부도 제대로 되어있고, 헤더가 꼬인거도 아니고....당연하게도 main문은 있다.

애초에 SetMaterialMap() 함수를 사용한 코드에서 나는 에러다. 해당 코드를 지우면 제대로 빌드가 된다.

 

뭐가 문제임? 하는 사람도 있을거고,

왜 이따구로 짬? 하는 사람도 있을거다...

일단 확실한건, 대부분은 "뭐가 문제임?" 혹은 "inline이 뭐임?" 하는 반응이 대부분일 것이다.

내가 아는 inline의 역할은 컴파일 단계에서 함수 호출을 없애고, 호출 위치에 함수의 코드를 그대로 붙여넣어 함수 호출비용을 줄이는 것으로 알고있었다.

물론 이것도 맞다. 하지만 보조적으로 한 가지 역할이 더 있다.

그건 바로 여러 파일에서 #include한 함수가 중복 정의되어도 괜찮다고 링커에게 알려주는 역할이라는 것이다......

약간 처음 들으면 애매할 수 있는데, 이해하고 보면 당연한 말이다.

이해를 돕기 위해 gpt쌤의 말을 가져오면,

더보기

inline을 쓰면 헤더 파일에서 구현해야 하는 이유는 컴파일러와 링커의 작동 방식 때문입니다. 이를 차근차근 설명드릴게요.


1. 컴파일러는 각 번역 단위에서 따로 작동

  • C++에서 코드를 컴파일할 때 **각 소스 파일(.cpp)**은 독립적인 "번역 단위"로 처리됩니다.
  • inline 함수는 컴파일러에게 "이 함수는 어디에서든 중복 정의될 수 있지만, 중복되어도 괜찮다"는 뜻을 전달합니다.

중요: inline 함수는 중복 정의를 허용하지만, 컴파일러는 이를 보장하기 위해 번역 단위마다 해당 함수의 정의를 알고 있어야 합니다.


2. 헤더 파일은 모든 번역 단위에 포함됨

  • 헤더 파일에 inline 함수를 정의하면 이를 포함하는 모든 번역 단위에서 동일한 함수 정의가 생깁니다.
  • 이 정의가 동일하다면, 링커는 이를 하나로 합쳐 문제가 없게 처리합니다.

예제:

cpp
코드 복사
// header.h inline int Add(int a, int b) { return a + b; }
  • 이 코드가 포함된 각 .cpp 파일마다 Add 함수의 정의가 복사됩니다.
  • 링커는 "이건 모두 동일한 함수다!"라고 인식하고 하나로 합칩니다.

3. CPP 파일에 정의하면 발생하는 문제

  • inline 함수는 헤더 파일에 정의되지 않으면 다른 번역 단위에서 해당 정의를 볼 수 없습니다.
  • 링커는 다른 번역 단위에서 함수의 정의를 찾을 수 없고, "이 기호를 찾을 수 없다"는 에러를 발생시킵니다.

문제 상황:

cpp
코드 복사
// header.h inline int Add(int a, int b); // 선언만 있고 구현 없음 // source.cpp #include "header.h" int Add(int a, int b) { // 구현이 cpp 파일에만 존재 return a + b; }
  • 링커는 Add 함수가 헤더에 정의되어 있어야 한다고 가정하지만, 실제 정의는 CPP에 있으므로 충돌합니다.

4. inline의 의미와 동작

  • inline의 의미: 함수가 호출될 때 **코드 삽입(inline expansion)**으로 대체될 수 있다.
  • 컴파일러가 함수의 코드를 호출자 위치에 직접 삽입하려면, 함수의 **정의(body)**를 컴파일 시점에 알아야 합니다.
  • 따라서 inline 함수는 반드시 모든 번역 단위에서 동일한 정의를 볼 수 있도록 헤더 파일에 구현해야 합니다.

5. 중복 정의를 피하기 위한 설계

  • 일반적인 함수는 여러 번 정의되면 링커가 에러를 발생시킵니다(One Definition Rule 위반).
  • 그러나 inline 함수는 특별히 "같은 정의라면 여러 번 있어도 괜찮다"는 규칙이 있습니다.
  • 이 규칙을 제대로 활용하려면, 모든 번역 단위에서 동일한 정의가 필요하므로 헤더 파일에 구현해야 합니다.

요약: 헤더에 구현해야 하는 이유

  1. 모든 번역 단위에서 동일한 함수 정의를 보장하기 위해.
  2. 함수 호출 위치에 직접 코드 삽입하려면, 컴파일러가 함수의 정의를 알고 있어야 하기 때문에.
  3. 중복 정의로 인한 링킹 에러를 방지하기 위해.

따라서, inline 함수는 헤더 파일에 정의를 작성하고, CPP 파일에서는 구현을 배제해야 안정적으로 동작합니다! 😊

따라서, 쉽게 말하면 템플릿이랑 컴파일과정이 비슷하다고 볼 수 있습니다.

템플릿도 헤더에 구현을 하지 않으면 에러가 뜨므로.....

 

그래서 이렇게 바꾸니까 되었습니다.

class Texture;

// FBX를 로드할 때 가져오는 정보. 
class MaterialResource : public IGraphicsResource
{
	using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
	explicit MaterialResource(std::wstring_view _name);
	virtual ~MaterialResource();
public:
	inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture) {
		mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture; 
	}
	inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType) { 
		return mMaterialMaps[static_cast<UINT>(_mapType)];
	}
private:
	MaterialMapArray mMaterialMaps;
};
MaterialResource::MaterialResource(std::wstring_view _name)
	: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
MaterialState::MaterialState(MaterialResource* _pMaterialResource)
	: mMaterialResource(_pMaterialResource)
{
	for (int mapType = 0; mapType < static_cast<UINT>(eMaterialMapType::SIZE); ++mapType)
	{
		mCBuffer.UseMap[mapType] = TRUE;
	}
}

cpp에 구현을 없애고 h에 구현을 했습니다.

 

근데 걍 느낀게....

처음에 inlline 굳이 쓴건, 어차피 컴파일러가 자동으로 inline화를 판단하지만, inline키워드를 씀으로써 const키워드처럼 필요없지만 (물론 const는 항상 필요없는건 아니다) 명시적으로 넣어주는 습관을 가지려고 쓴건데... 이렇게 복잡한 키워드인지 몰랐다.

하...걍 inline써도 컴파일러가 inline화 무시할 때도 있고, inline안써도 컴파일러가 알아서 inline화 시킬 때도 있는데,
걍 쓰지 맙시다.....

 

'일지' 카테고리의 다른 글

[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 2일차  (1) 2026.03.18
[게임 개발] MFC를 활용한 간단한 오델로게임 제작 - 1일차  (4) 2026.03.16
[C++] 오늘의 삽질기 - 메모리 풀  (3) 2025.07.11
[그래픽스] 다양한 블러 효과  (0) 2024.12.10
[C++] 오늘의 삽질 - Color 클래스를 만들어보자  (0) 2024.09.13

[그래픽스] 다양한 블러 효과

김빡주 | 2024. 12. 10. 11:18 | 댓글 -

보호된 게시글입니다. 비밀번호를 입력하세요.

[CS] IPC(Inter Process Communication)

김빡주 | 2024. 12. 2. 01:01 |

 

사진출처: IPC


IPC가 뭘까?

예전에 프로세스에 관해 포스팅을 했었을 때, 각 프로세스는 독립적인 주소공간을 가진다고 했었습니다. 즉 프로세스끼리는 서로의 메모리를 공유할 수 없다는 뜻인데요. 진짜일까요?

당연하게도 프로세스는 서로 데이터를 공유할 수 있는 방법이 있습니다. 그걸 바로 IPC통신이라고 하는데요.

커널은 IPC자원(커널 객체)와 IPC메서드 등을 관리 및 제공하며, 개발자는 운영 체제가 제공하는 IPC자원과 API, 인터페이스를 사용하여 프로세스간 통신을 구현할 수 있습니다.


IPC의 종류

IPC통신에도 많은 종류가 있으며, 상황에 따라 맞는 방법을 선택해 사용하는 것이 중요합니다.


익명 파이프(Anonymous Pipe)

특징

  • 부모-자식 프로세스 간의 단방향 통신을 위해 사용합니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 부모-자식 프로세스 간의 파이프 연결이 다른 IPC설계보다 쉽습니다.

단점

  • 기본적으로 단방향 통신이므로 프로세스가 읽기, 쓰기를 둘다 하려면 두 개의 파이프를 만들어야 합니다.
  • 각 프로세스가 부모-자식 관계가 아니면 사용이 불가능합니다.

사용법

더보기

CreatePipe 함수(namedpipeapi.h) - Win32 apps | Microsoft Learn

 

CreatePipe 함수(namedpipeapi.h) - Win32 apps

익명 파이프를 만들고 파이프의 읽기 및 쓰기 끝에 핸들을 반환합니다.

learn.microsoft.com

익명 파이프 생성 예시

#include <iostream>
#include <Windows.h>

int main()
{
    HANDLE hRead, hWrite;

    // 이 예제는 부모-자식 프로세스에 대한 구분이 없으므로 실제로 사용할 땐, CreateProcess로 부모 프로세스가 자식 프로세스를 생성해야한다.
    // 익명 파이프 생성
    if (CreatePipe(&hRead, &hWrite, NULL, 0))
    {
        std::cout << "익명 파이프 생성 성공" << '\n';
    }
    else
    {
        std::cerr << "익명 파이프 생성 실패" << '\n';
        return 1;
    }

    // 쓰기 쓰레드: 데이터를 파이프에 쓰기
    const char* data = "Hello, Anonymous Pipe!";
    DWORD bytesWritten;

    if (WriteFile(hWrite, data, strlen(data) + 1, &bytesWritten, NULL))
    {
        std::cout << "파이프에 데이터 쓰기 성공: " << bytesWritten << " 바이트" << '\n';
    }
    else
    {
        std::cerr << "파이프 쓰기 실패" << '\n';
    }

    // 읽기 쓰레드: 데이터를 파이프에서 읽기
    char buffer[128] = { 0 };
    DWORD bytesRead;

    if (ReadFile(hRead, buffer, sizeof(buffer) - 1, &bytesRead, NULL))
    {
        std::cout << "파이프에서 데이터 읽기 성공: " << buffer << '\n';
    }
    else
    {
        std::cerr << "파이프 읽기 실패" << '\n';
    }

    // 커널 객체 반납
    CloseHandle(hRead);
    CloseHandle(hWrite);

    return 0;
}

이름 있는 파이프(Named Pipe)

특징

  • 부모-자식 프로세스가 아닌 프로세스와의 단방향 통신을위해 사용합니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.
  • 이름을 통해 특정 파이프를 식별합니다.
  • 여러 프로세스가 동일한 파이프에 접근 가능합니다.
  • 기본적으로 단방향 통신이지만, 운영체제에 따라 양방향이 가능합니다.

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 부모-자식 프로세스 관계에 제한되지 않습니다.

단점

  • 기본적으로 단방향 통신이므로 프로세스가 읽기, 쓰기를 둘다 하려면 두 개의 파이프를 만들어야 합니다.
  • 파이프의 설정과 관리가 익명 파이프보다 복잡합니다.

사용법

더보기

CreateNamedPipeA 함수(winbase.h) - Win32 apps | Microsoft Learn

 

CreateNamedPipeA 함수(winbase.h) - Win32 apps

ANSI(CreateNamedPipeA) 함수(winbase.h)는 명명된 파이프의 인스턴스를 만들고 후속 파이프 작업에 대한 핸들을 반환합니다.

learn.microsoft.com

네임드 파이프 생성 예시 (읽기, 쓰기는 없음)

#include "pch.h"
#include <iostream>

int main()
{
	return 0;
	HANDLE hPipe = CreateNamedPipe(
		L"\\.\pipe\MyPipe",			 // 파이프 이름(\\.\pipe\의 뒤에 이름)
		PIPE_ACCESS_DUPLEX,          // 양방향 통신
		PIPE_TYPE_BYTE | PIPE_WAIT,  // 바이트 스트림과 동기식 통신
		1,                           // 인스턴스 개수
		512, 512,                    // 출력/입력 버퍼 크기
		0,                           // 기본 타임아웃
		NULL                         // 보안 속성
	);
	CloseHandle(hPipe);
	return 0;
}

메시지 큐 (Message Queue)

특징

  • 커널이 관리하는 메시지 큐를 통해 데이터를 주고받습니다.
  • 데이터를 FIFO(First In, First Out) 방식으로 처리합니다.
  • 송신과 수신이 독립적으로 동작하는 비동기성으로 동작합니다.
  • 운영체제가 메시지 큐의 생성 및 관리를 담당합니다. 

장점

  • 운영 체제에서 기본적으로 API및 인터페이스를 제공하여 사용이 쉽습니다.
  • 비동기적 데이터 통신방식이므로 송신자와 수신자가 동시에 동작할 필요 없음.

단점

  • 운영체제마다 API가 다르므로 주의해야 합니다.
  • 메시지 타입과 큐 ID등의 메세지를 명확히 전송해야 합니다.

사용법

더보기

C-C++ 코드 예제: 큐 만들기 | 마이크로소프트 런

 

C-C++ Code Example: Creating a Queue

C-C++ Code Example: Creating a Queue Article 10/19/2016 In this article --> Applies To: Windows 10, Windows 7, Windows 8, Windows 8.1, Windows Server 2008, Windows Server 2008 R2, Windows Server 2012, Windows Server 2012 R2, Windows Server Technical Previe

learn.microsoft.com


공유 메모리 (Shared Memory)

특징

  • 한 메모리 영역을 여러 프로세스가 공유하여 사용하는 방식입니다.
  • 이는 서로의 메모리 주소공간을 공유한다는 것이 아니라, OS가 공유가능한 메모리 공간을 할당하여 여러 프로세스에게 접근 가능하게 해줍니다.

장점

  • 프로세스가 데이터를 메모리에 복사하는 것이 아닌, 직접 쓰고 읽으므로 속도가 빠릅니다.
  • 여러 프로세스가 동일한 데이터를 읽기, 쓰기가 가능합니다.
  • 다른 IPC에 비해 비교적 큰 데이터를 처리하기가 간편합니다.

단점

  • 데이터의 동기화 문제에 주의해야 합니다.
  • 메모리에 직접 접근하므로 보안에 취약할 수 있습니다.
  • 할당한 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

사용법

더보기

명명된 공유 메모리 만들기 - Win32 apps | Microsoft Learn

 

명명된 공유 메모리 만들기 - Win32 apps

데이터를 공유하기 위해 여러 프로세스는 시스템 페이징 파일이 저장하는 메모리 매핑 파일을 사용할 수 있습니다.

learn.microsoft.com

쓰기

#include <windows.h>
#include <iostream>

// 쓰는 프로세스
int main() 
{
    // 공유 메모리 생성
    HANDLE hMapFile = CreateFileMapping(
        INVALID_HANDLE_VALUE,          // 물리적 파일이 아닌 메모리 사용
        nullptr,                       // 기본 보안 속성
        PAGE_READWRITE,                // 읽기/쓰기 가능
        0,                             // 고위 메모리 크기 (사용 안 함)
        256,                           // 공유 메모리 크기 (256 바이트)
        L"SharedMemoryExample");       // 공유 메모리 이름

    // 공유 메모리 매핑
    LPVOID pBuf = MapViewOfFile(
        hMapFile,                      // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        0, 0, 0);                      // 전체 영역 매핑

    // 데이터 쓰기
    const char* message = "Hello!";
    CopyMemory(pBuf, message, strlen(message) + 1);
    Sleep(5000);
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);

    return 0;
}

읽기

#include <windows.h>
#include <iostream>

// 읽는 프로세스
int main() 
{
    Sleep(1000);
    // 공유 메모리 열기
    HANDLE hMapFile = OpenFileMapping(
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        FALSE,                         // 자식 프로세스 상속 여부 (아니오)
        L"SharedMemoryExample");       // 공유 메모리 이름 (생성 시와 동일해야 함)

    // 공유 메모리 매핑
    LPVOID pBuf = MapViewOfFile(
        hMapFile,                      // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,           // 읽기/쓰기 액세스
        0, 0, 0);

    // 데이터 읽기
    std::cout << "공유 메모리에서 데이터를 읽었습니다: " << static_cast<char*>(pBuf) << std::endl;

    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
}

소켓 (Socket)

해당 사진은 TCP에 대한 예시사진

특징

  • 네트워크 인터페이스를 통해 두 프로세스가 데이터를 주고받는 방식입니다.
  • 데이터를 양방향으로 주고받을 수 있습니다.
  • TCP(연결 지향)방식과 UDP(비연결 지향)방식이 있습니다.

장점

  • 같은 시스템 내에서의 통신 뿐만이 아니라, 네트워크를 통한 다른 컴퓨터와의 통신이 가능합니다.
  • 대상이 서로 다른 운영체제여도 통신이 가능합니다.
  • 실시간으로 데이터 전송이 가능합니다.

단점

  • 소켓 통신은 패킷의 처리, 예외 처리 등에 대한 구현이 필요하므로 복잡합니다.
  • 네트워크 자원을 사용하기 때문에 연결 수가 많아질수록 리소스 사용이 급격히 많아집니다.
  • 데이터를 암호화하지 않으면 보안에 취약할 수 있습니다.

사용법

더보기

socket 함수(winsock2.h) - Win32 apps | Microsoft Learn

 

socket 함수(winsock2.h) - Win32 apps

소켓 함수는 특정 전송 서비스 공급자에 바인딩된 소켓을 만듭니다.

learn.microsoft.com

 

소켓 서버/클라이언트 예시 코드

 

*서버

#include<iostream>
#include<string>
#include<winsock2.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 6060
#define BUFFER_SIZE 100

SOCKET gListen, gAccept;
SOCKADDR_IN gAddress;
CHAR buffer[BUFFER_SIZE] = { 0 };

// 서버
int main()
{
    // 윈속 초기화
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    if ((gListen = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    gAddress.sin_family = AF_INET;
    gAddress.sin_addr.s_addr = INADDR_ANY;
    gAddress.sin_port = htons(PORT);

    // 소켓 바인딩
    if (bind(gListen, (PSOCKADDR)&gAddress, sizeof(gAddress)) == SOCKET_ERROR)
    {
        printf("bind() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    // 연결 요청 수신 대기
    if (listen(gListen, 5))
    {
        printf("listen() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    std::cout << "Waiting for a connection..." << std::endl;

    // 연결 수락
    if ((gAccept = accept(gListen, NULL, NULL)) == INVALID_SOCKET)
    {
        printf("accept() failed with error %d\n", WSAGetLastError());
        return -1;
    }

    std::cout << "Connection!" << std::endl;

    // 받기
    if (recv(gAccept, buffer, sizeof(buffer), 0) <= 0)
    {
        printf("WSARecv() failed with error %d\n", WSAGetLastError());
        return -1;
    }
    std::cout << "Message received: " << buffer << std::endl;

    // 종료
    closesocket(gListen);
    closesocket(gAccept);
    WSACleanup();
    return 0;
}

 

*클라이언트

#include<iostream>
#include<string> 
#include<WinSock2.h> 
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 6060
#define ADDR "127.0.0.1"
#define BUFFER_SIZE 100

SOCKET gSocket;
SOCKADDR_IN gAddress;
CHAR buffer[BUFFER_SIZE] = {0};

// 클라
int main()
{
    Sleep(1000);
    // 윈속 초기화
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("socket() failed with error %d\n", WSAGetLastError());
        return -1;
    }
    gAddress.sin_family = AF_INET;
    gAddress.sin_port = htons(PORT);

    if (inet_pton(AF_INET, ADDR, &(gAddress.sin_addr)) != 1)
    {
        printf("Invalid IP address %d\n", WSAGetLastError());
        return -1;
    }
    else
    {
        std::cout << "Server IP address: " << ADDR << std::endl;
        if ((gSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
        {
            printf("socket() failed with error %d\n", WSAGetLastError());
            return -1;
        }
        if (connect(gSocket, (SOCKADDR*)&gAddress, sizeof(gAddress)) < 0)
        {
            printf("connect() failed with error %d\n", WSAGetLastError());
            return -1;
        }
        std::cout << "Connected to server!" << std::endl;
        std::string sendMessage;

        std::cout << "Input massge: ";
        std::cin >> sendMessage;

        /* 5. 메시지 전송 */

        send(gSocket, sendMessage.c_str(), sendMessage.length(), 0);

        closesocket(gSocket);
    }
    WSACleanup();
    return 0;
}

메모리 맵 (Memory Map)

특징

  • 파일을 프로세스에 매핑시켜 파일의 메모리에 접근하여 읽거나 쓰는 기법입니다.
  • 파일을 매핑할 때 실제 물리 메모리 페이지에 해당 파일 데이터를 로드하고, 이 물리 메모리를 가상 메모리 주소로 매핑합니다.
  • 공유 메모리와 비슷하나 다른 점은, OS가 메모리 공간을 할당해주는 것이 아닌, 파일의 메모리를 읽고 쓴다는 것입니다.

장점

  • 프로세스가 데이터를 메모리에 복사하는 것이 아닌, 직접 쓰고 읽으므로 속도가 빠릅니다.
  • 여러 프로세스가 동일한 데이터를 읽기, 쓰기가 가능합니다.
  • 다른 IPC에 비해 비교적 큰 데이터를 처리하기가 간편합니다.

단점

  • 데이터의 동기화 문제에 주의해야 합니다.
  • 메모리에 직접 접근하므로 보안에 취약할 수 있습니다.
  • 할당한 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다.

번외1) 메모리 맵 vs 파일 I/O

파일을 읽고 쓰는거면 파일IO(std::filesystem 등)랑 다른게 뭘까요? 몇몇 차이점이 존재합니다.

1. 메모리 맵은 실제 메모리 공간을 사용하는 기법이나, 파일I/O는 파일의 데이터를 버퍼로 복사하고, 버퍼로 씁니다.

2. 메모리 맵은 파일의 용량에 상관없이 필요한 데이터만 로드하지만, 파일IO는 모든 데이터를 직접 로드하고 쓰기 때문에 느립니다.

번외2) 그러면 수십 기가바이트의 파일을 어떻게 로드할까?

앞서, 메모리 맵은 파일의 용량에 상관없이 필요한 데이터만 로드한다고 했는데요. 어떤 식으로 그렇게 로드할 수 있는걸까요?

간단합니다! 필요한 데이터가 있는 페이지만 로드하게 됩니다. 때문에 파일의 용량이 아무리 커도 속도에 영향을 받지 않습니다.

 

 

사용법

더보기

CreateFileMappingA 함수(winbase.h) - Win32 apps | Microsoft Learn

 

CreateFileMappingA 함수(winbase.h) - Win32 apps

지정된 파일에 대한 명명되거나 명명되지 않은 파일 매핑 개체를 만들거나 엽니다. (CreateFileMappingA)

learn.microsoft.com

MapViewOfFile 함수(memoryapi.h) - Win32 apps | Microsoft Learn

 

MapViewOfFile 함수(memoryapi.h) - Win32 apps

호출 프로세스의 주소 공간에 파일 매핑 보기를 매핑합니다.

learn.microsoft.com

 

메모리 맵 사용 예시

#include <iostream>
#include <windows.h>

#define FILE_PATH "example.txt"
#define BUFFER_SIZE 256

int main() {
    // 파일 열기(없으면 생성)
    HANDLE hFile = CreateFileA(
        FILE_PATH,                       // 파일 경로
        GENERIC_READ | GENERIC_WRITE,    // 읽기/쓰기 권한
        0,                               // 공유 모드 없음
        NULL,                            // 보안 속성
        OPEN_ALWAYS,                     // 파일이 없으면 생성
        FILE_ATTRIBUTE_NORMAL,           // 일반 파일 속성
        NULL                             // 템플릿 파일 없음
    );

    if (hFile == INVALID_HANDLE_VALUE) {
        std::cerr << "CreateFile failed: " << GetLastError() << std::endl;
        return -1;
    }

    // 파일 매핑 생성
    HANDLE hMapFile = CreateFileMappingA(
        hFile,                // 파일 핸들
        NULL,                 // 보안 속성
        PAGE_READWRITE,       // 읽기/쓰기 권한
        0,                    // 파일 크기 상위 32비트
        BUFFER_SIZE,          // 파일 크기 하위 32비트
        NULL                  // 이름 없음
    );

    if (hMapFile == NULL) {
        std::cerr << "CreateFileMapping failed: " << GetLastError() << std::endl;
        CloseHandle(hFile);
        return -1;
    }

    // 매핑된 메모리 접근
    LPVOID pBuf = MapViewOfFile(
        hMapFile,               // 메모리 매핑 핸들
        FILE_MAP_ALL_ACCESS,    // 접근 권한
        0,                      // 오프셋 상위 32비트
        0,                      // 오프셋 하위 32비트
        BUFFER_SIZE             // 매핑 크기
    );

    if (pBuf == NULL) {
        std::cerr << "MapViewOfFile failed: " << GetLastError() << std::endl;
        CloseHandle(hMapFile);
        CloseHandle(hFile);
        return -1;
    }

    // 데이터 쓰기
    strcpy_s((char*)pBuf, BUFFER_SIZE, "Hello, File Mapping!");

    std::cout << "Data written to file via mapping: " << (char*)pBuf << std::endl;

    // 메모리 해제
    UnmapViewOfFile(pBuf);
    CloseHandle(hMapFile);
    CloseHandle(hFile);

    return 0;
}

 

 


 

'공부 > CS' 카테고리의 다른 글

[네트워크] TCP와 UDP  (0) 2026.03.12
[네트워크] OSI 7계층  (0) 2026.03.11
[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 데드락(Deadlock)  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19

[CS] 페이지 교체 알고리즘

김빡주 | 2024. 11. 25. 19:12 |

참고 : [운영체제] 페이지 교체 알고리즘

저번에 페이징에 대해서 포스팅을 했던적이 있는데요?

페이지 교체 알고리즘에 대해선 안다뤘었습니다.

그래서 이번 기회에 한번 다뤄보려고 합니다.

바로 ㄱㄱ


페이지 교체 알고리즘

페이지 교체 알고리즘은 운영체제가 가상 메모리를 효율적으로 관리하기 위해 사용하는 방법 중 하나로, 메모리 부족 상황에서 어떤 페이지를 교체할지 결정하는 역할을 합니다. 페이지 교체 알고리즘은 무수히 많지만, 하지만 오늘은 그중 많이 다루는 몇가지만 다루려고 합니다.

페이지 교체 알고리즘들.png


 

1. FIFO (First In, First Out)

  • 개념: 가장 먼저 들어온 페이지를 가장 먼저 교체합니다. (큐를 사용해서 구현)
  • 장점: 구현이 간단합니다.
  • 단점: 오래된 페이지가 여전히 많이 사용되는 경우 성능 저하 유발합니다. 페이지 수가 늘어날 수록 page fault가 발생하는 빈도가 늘어나는 경우(Belady's Anomaly)가 생길 수 있습니다.

2. OPT (Optimal)

4번 시점에서 이후에 A와 B 페이지가 사용될 것을 알고 있기 때문에 C 페이지를 대상 페이지로 선정해 스왑 아웃시킨다.
  • 개념: 앞으로 가장 오래 사용되지 않을 페이지를 교체합니다.
  • 장점: 가장 적은 페이지 폴트를 보장합니다. (이론상 최강).
  • 단점: 미래의 페이지 접근 패턴을 알아야 하는데, 이는 실질적으로 구현이 불가능하다고 합니다.

3. LRU (Least Recently Used)

  • 개념: 가장 오랫동안 사용되지 않은 페이지를 교체합니다.
  • 장점: FIFO보다 성능이 좋습니다.
  • 단점: 최근 사용 기록을 저장하고 갱신하는 비용이 발생합니다.

4. LFU (Least Frequently Used)

  • 개념: 사용 빈도가 가장 낮은 페이지를 교체합니다.
  • 장점: 자주 사용되는 페이지를 보호할 수 있습니다.
  • 단점: 페이지의 사용 빈도를 저장하고 갱신하는 비용이 발생하게 됩니다. 최신 페이지가 비교적 불리한 방향이므로 특정 상황에서 성능 저하가 생길 수 있습니다.

5. Clock (Second Chance)

위 그림과 같이 대상 페이지를 가리키는 포인터를 사용하는데, 이 포인터가 큐의 맨 아래로 내려가면 시계처럼 다음번에는 다시 큐의 처음을 가리키게 된다.
교체 시, 비트가 0일 경우 교체, 1일 경우 다음 포인터로 넘어가서 다시 검사

  • 개념: FIFO 변형으로, 각 페이지에 사용 비트를 사용합니다. (1이면 사용 중)
  • 장점: 최근에 사용된 페이지는 교체되지 않습니다. LRU처럼 과거 사용 기록을 저장하지 않아도 됩니다.
  • 단점: 참조 비트만으로는 페이지가 얼마나 자주 사용되었는지, 최근에 사용되었는지 구체적으로 알 수 없습니다. 구현이 비교적 복잡하고 페이지의 사용 비트를 저장 및 갱신하는 비용이 발생합니다.

현대 OS는 어떤 알고리즘을 주로 사용할까?

일반적으로 운영체제는 LRU와 Clock을 실제로 사용하는 경우가 많습니다.

LRU와 Clock을 혼용하여 가장 오랫동안 사용되지 않은 페이지를 교체하며, 최근에 참조된 페이지를 보존하는데 효과적이도록 페이지 교체를 수행한다고 합니다.

'공부 > CS' 카테고리의 다른 글

[네트워크] OSI 7계층  (0) 2026.03.11
[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 데드락(Deadlock)  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19
[CS] MMU, TLB  (0) 2024.11.04

[CS] 데드락(Deadlock)

김빡주 | 2024. 11. 25. 01:47 |

아안뇨오엉하세요엉ㅇ

저번에 뮤텍스와 세마포어에 대해서 글을 적어봤는데요?

동기화기법은 신이고 무적같지만 사실 허점이 하나 있습니다.

바로 프로세스나 스레드간 교착 상태에 빠질 수 있다는 점인데요...

이 현상을 바로 데드락이라고 합니다!

데드락이 뭔지 같이 알아봅시당


데드락(Deadlock)이 뭘까?

데드락은 둘 이상의 프로세스나 스레드가 각각 서로 점유하고 있는 자원을 기다릴 때 무한 대기에 빠지는 상황을 일컫습니다.

아래 사진은 데드락이 생기는 과정을 사진으로 간단하게 나타낸 것입니다.

 

자원 A를 가진 스레드1이 있고,

자원 B를 가진 스레드2가 있을때,

스레드1: 야 B내놔ㅋ

스레드2: 싫어 A내놔ㅋ

이러고 있는거임ㅋㅋ


데드락이 생기는 조건?

데드락은 다음 네 가지 조건이 모두 만족될 때 발생합니다:

  1. 상호 배제(Mutual Exclusion):
    • 자원은 동시에 하나의 프로세스만 사용할 수 있습니다.
  2. 점유와 대기(Hold and Wait):
    • 프로세스는 자신이 이미 점유하고 있는 자원을 유지하면서, 추가로 다른 자원을 요청하며 대기합니다.
    • 예: 스레드1은 자원 A를 점유한 상태에서 자원 B를 요청
  3. 비선점(No Preemption):
    • 점유한 자원은 해당 프로세스가 작업을 완료하기 전까지 강제로 빼았지 못합니다.
    • 예: 프로세스가 사용하는 자원은 작업 완료 전까지 다른 프로세스가 가져갈 수 없음.
  4. 순환 대기(Circular Wait):
    • 프로세스들이 순환적으로 자원을 요청하며 서로 대기합니다.
    • 예: A → B → C → A 순환 구조로 자원을 기다리는 상황.

 

세상에 이런 상황이 있다고?

놀랍게도 이런 상황이 나올 수 있고, 생각보다 간단하게 나옵니다.

아래는 데드락이 발생할 수 있는 예제코드입니다.

더보기
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1, mutex2;

void Worker1() 
{
    mutex1.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 다른 스레드가 실행될 여유를 줌
    mutex2.lock();

    mutex1.unlock();
    mutex2.unlock();
}

void Worker2() 
{
    mutex2.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 다른 스레드가 실행될 여유를 줌
    mutex1.lock();

    mutex2.unlock();
    mutex1.unlock();
}

int main() {
    std::thread t1(Worker1);
    std::thread t2(Worker2);

    t1.join();
    t2.join();

    return 0;
}

이렇게 되면 worker1은 mutex1을 통해 임계영역에 접근을 했지만, mutex2를 통해 또 다른 자원에 접근하고 싶지만, 이미 worker2가 mutex2를 통해 해당 자원을 점유한 상태입니다.
(따라서 초딩같이 싸우고 있다는거임ㅋㅋ)


데드락을 해결하려면?

데드락을 해결하려면 무슨 방법이 있을까요?


1) 예방(Prevention):

데드락 발생 조건 중 하나를 제거하여 문제를 사전에 방지.

  • 상호 배제 제거: 자원을 공유 가능하게 설계 (단, 현실적으로 어려움).
  • 점유와 대기 제거: 모든 자원을 한 번에 요청하도록 설계.
  • 비선점 제거: 자원을 강제로 해제하거나 다른 프로세스로 넘김.
  • 순환 대기 제거: 자원 요청 순서를 정해 순환 대기를 방지.

2) 회피(Avoidance):

자원 할당 시 데드락 발생 가능성을 분석해 안전한 상태(Safe State)만 유지.

  • 은행원 알고리즘(Banker's Algorithm):
    • 은행원이 고객에게 대출을 승인하기 전에 시스템 상태를 분석하는 방식에서 이름이 유래했습니다. 이를 프로세스와 자원 할당 문제에 대입하면 다음과 같은 원리로 작동합니다.
      • 안전 상태(Safe State): 모든 프로세스가 작업을 완료하고 자원을 해제할 수 있는 상태입니다. 즉 모든 요청이 처리 가능한 상태.
      • 불안전 상태(Unsafe State): 특정 자원 요청이 승인을 받으면 다른 프로세스가 자원을 기다리면서 데드락이 발생할 가능성이 있는 상태입니다.

3) 탐지(Detection):

데드락이 발생했는지 확인하고, 이를 해결하기 위한 전략을 사용하는 방법입니다. 탐지는 데드락을 허용한 상태에서 주기적으로 시스템의 자원 상태를 검사하여 데드락을 확인합니다. 탐지 후에는 데드락을 해결하기 위한 적절한 조치를 취합니다.

  • 시스템 상태 검사
    • 자원 할당 상태와 프로세스의 요청 상태를 바탕으로 Deadlock Detection Algorithm을 실행합니다. 데드락의 주요 조건인 사이클(Cycle) 또는 교착 상태(Wait-for Graph)를 탐지합니다. 
    • 입력 데이터를 통해 현재 상태를 보내줍니다. 
      • Available (가용 자원): 현재 시스템의 남아 있는 자원 수.
      • Allocation (현재 할당): 각 프로세스에 이미 할당된 자원의 양.
      • Request (현재 요청): 각 프로세스가 추가로 요청한 자원의 양.

4) 회복(Recovery):

데드락 탐지 이후 이미 발생한 데드락을 해결하는 과정입니다. 시스템이 데드락 상태임을 확인한 후, 프로세스를 종료하거나 자원을 회수하여 데드락을 해소하는 방법입니다.

  • 데드락 회복 전략
    • 프로세스 강제 종료(Termination) : 데드락 상태를 해소하기 위해 프로세스를 강제로 종료하여 자원을 해제하는 방법입니다.
    • 자원 강제 회수 (Resource Preemption) : 데드락 상태를 해소하기 위해 프로세스를 강제로 종료하여 자원을 해제하는 방법입니다.
    • 데드락에 얽히지 않은 프로세스 우선 실행 : 데드락 상태의 프로세스는 중단하거나 대기시키고, 데드락에 얽히지 않은 다른 프로세스부터 실행합니다.

데드락을 해결하는 방법을 알아봤지만, 결국엔 알고리즘과 상태검사로 인해 성능 오버헤드가 발생할 수 있습니다.

가장 좋은 방법은 데드락이 발생하지 않도록 예방하는 것이 중요하다고 생각합니다!

무엇이든 예방이 중요하다...


현대 OS는 데드락을 어떻게 처리할까?

현대의 운영체제, 즉 UNIX를 포함한 대부분의 OS는 일반적으로 데드락 탐지나 회복을 직접적으로 처리하지 않는 경우가 많습니다. 대신, OS는 데드락이 발생할 가능성을 낮추거나 개발자나 시스템 관리자가 이 문제를 처리하도록 설계하는 데 중점을 둡니다.


현대 OS가 데드락을 처리하지 않는 이유는?

(1) 성능 오버헤드

  • 데드락을 탐지하거나 예방하는 알고리즘은 복잡한 연산을 포함하며, 이를 실시간으로 처리하려면 시스템 리소스에 큰 부하가 걸릴 수 있습니다.
  • 특히 멀티스레드 환경이나 분산 시스템에서는 데드락 상태를 정확히 탐지하는 것이 어렵고, 탐지 비용이 높습니다.

(2) 일반화된 해결책 부족

  • 데드락은 특정 상황에서 발생하므로, 모든 프로그램에 대해 범용적으로 작동하는 데드락 처리 방법을 제공하기 어렵습니다.
  • 데드락을 해결하기 위한 프로세스 종료나 자원 회수는 애플리케이션의 상태를 망가뜨릴 수 있어, OS가 임의로 조치하는 것은 적절하지 않을 수 있습니다.

현대 OS는 데드락 처리를 시스템 수준에서 자동화하지 않고, 다음과 같은 방식을 통해 간접적으로 문제를 다룹니다:

  1. 개발자가 예방적 설계를 할 수 있도록 동기화 메커니즘과 도구 제공.
  2. 시스템 로그와 디버깅 툴로 데드락 원인 파악 지원.
  3. 사용자에게 프로세스 종료와 같은 수동적 해결책 제공.

운영체제는 성능과 안정성을 위해 데드락 관련 문제를 개발자의 책임으로 위임하는 방향으로 설계되었습니다.

따라서 우리 개발자는 데드락이 발생하지 않도록 신중히 코드를 짤 필요가 있습니다...ㅇㅅㅇ

'공부 > CS' 카테고리의 다른 글

[CS] IPC(Inter Process Communication)  (0) 2024.12.02
[CS] 페이지 교체 알고리즘  (0) 2024.11.25
[CS] 뮤텍스(Mutex)와 세마포어(Semaphore)  (2) 2024.11.19
[CS] MMU, TLB  (0) 2024.11.04
[CS] 세그멘테이션  (0) 2024.11.04

티스토리툴바