광란의슈가슈가룬 2025. 7. 21. 20:32

잡 큐가 뭔지 알아보기 전에 커맨드 패턴이 무엇인지 먼저 알아보도록 하자.

 

지금 만들고 있는 서버의 구조는 각 스레드들이 일감을 받아오고 그 스레드가 함수를 실행하거나 하는 방식이다.

식당을 예로 들면 직원이 손님한테 가서 주문을 받은 후 직원이 직접 주방에 들어가 요리를 하는 것이다. 이 방식이 잘못된 건 아니지만 만약 손님이 많아지게 되고 주방에 조리대는 1개밖에 없다면 1명의 직원만 요리를 하고 나머지 직원은 기다려야 되는 것이다.

 

그럼 이런 방식을 생각해볼 수 있다. 주방장이 따로 있고 직원은 주문서를 받아 전달만 해주는 것이다. 그렇게 되면 직원과 주방장의 일을 구분하여 나눌 수 있다. 또한 주문을 받은 시점과 요리를 하는 시점이 다를 수 있다는 것도 가장 큰 특징이다. 만약 주방장이 받은 주문이 너무 많다면 우선순위에 따라 순차적으로 처리하면 된다. 이때 직원은 다른 일을 수행할 수 있으므로 효율적이라고 볼 수 있다. 그리고 아직 요리가 시작하기 전이라면 주문서의 내용을 수정할 수도 있다.

 

이것이 커맨드 패턴의 핵심이다. 요청을 캡슐화해서 보내는 것이다. 이렇게 된다면 모든 스레드들이 락을 잡고 공유자원에 접근하는 것이 아니라 그 일을 담당하는 스레드에서 요청을 던지고 자신은 다른 일을 마저 하러 갈 수 있게 된다.

 

그래서 지금부터는 이 이론을 토대로 락을 걸지 않고 Job(Task)를 만들어 건내주는 방식으로 수정해 볼 것이다.

방식은 굉장히 많은데 가장 원시적인 방법부터 단계별로 하나씩 나아가보자.

// [요청 내용] : 1번 플레이어에게 10만큼 힐
// 행동 : heal
// 인자 : 1번 유저, 힐량 10

이런 요청이 있다고 생각해보자. 

 

Job.h

#pragma once
class IJob
{
public:
	virtual void Excute() {}
};

class HealJob : public IJob
{
public:
	virtual void Excute() override
	{
		//_target 찾아서
		//_target->AddHp(_heaValue)

		cout << _target << "에게 힐 " << _healValue << endl;

	}

public:
	uint64 _target = 0;
	uint32 _healValue = 0;
};

main.cpp

// Test Job
{
	// [요청 내용] : 1번 플레이어에게 10만큼 힐
	// 행동 : heal
	// 인자 : 1번 유저, 힐량 10

	HealJob healJob;
	healJob._target = 1;
	healJob._healValue = 10;

	healJob.Excute();
}


// Job

기본 아이디어는 이렇다. HealJob이라는 요청을 만들어서 Excute를 해주면 요청이 실행된다.

 

이 방법은 직관적이지만 단점이 있다. 일감이 늘어날 때마다 개별 클래스를 하나하나 만들어야 하고 일감을 만들 때마다 객체를 만들어야 된다는 것이다. 그냥 생각해봐도 그렇게 효율적이지는 않아 보인다. 지금은 알아가는 단계이므로 일단 이 방식으로 전에 만들었던 채팅 서버 코드를 수정해 볼 것이다.

 

먼저 Job.h에 JobQueue라는 클래스를 만들어주어 일감을 순차적으로 받도록 해준다.

using JobRef = shared_ptr<IJob>;

class JobQueue
{
public:
	void Push(JobRef job)
	{
		WRITE_LOCK;
		_jobs.push(job);
	}

	JobRef Pop()
	{
		WRITE_LOCK;
		if (_jobs.empty())
			return nullptr;

		JobRef ret = _jobs.front();
		_jobs.pop();

		return ret;
	}

private:
	USE_LOCK;
	queue<JobRef> _jobs;
};

 

그리고 일감을 하나하나 클래스로 만들어야하기 때문에 Room을 수정해준다.

 

Room.h

#pragma once
#include "Job.h"

class Room
{
	friend class EnterJob;
	friend class LeaveJob;
	friend class BroadcastJob;

private:
	// 싱글 스레드처럼 코딩
	void Enter(PlayerRef player);
	void Leave(PlayerRef player);
	void Broadcast(SendBufferRef sendBuffer);

public:
	// 멀티스레드 환경에서는 일감으로 접근

	void PushJob(JobRef job) { _jobs.Push(job); }
	void FlushJob();

private:
	map<uint64, PlayerRef> _players;

	JobQueue _jobs;
};

extern Room GRoom; //테스트용

// Room Jobs
class EnterJob : public IJob
{
public:
	EnterJob(Room& room, PlayerRef player) : _room(room), _player(player)
	{
	}

	virtual void Excute() override
	{
		_room.Enter(_player);
	}

public:
	Room& _room;
	PlayerRef _player;
};

class LeaveJob : public IJob
{
public:
	LeaveJob(Room& room, PlayerRef player) : _room(room), _player(player)
	{
	}

	virtual void Excute() override
	{
		_room.Leave(_player);
	}

public:
	Room& _room;
	PlayerRef _player;
};

class BroadcastJob : public IJob
{
public:
	BroadcastJob(Room& room, SendBufferRef sendBuffer) : _room(room), _sendBuffer(sendBuffer)
	{
	}

	virtual void Excute() override
	{
		_room.Broadcast(_sendBuffer);
	}

public:
	Room& _room;
	SendBufferRef _sendBuffer;
};

Room.cpp

#include "pch.h"
#include "Room.h"
#include "Player.h"
#include "GameSession.h"

Room GRoom;

void Room::Enter(PlayerRef player)
{
	_players[player->playerId] = player;
}

void Room::Leave(PlayerRef player)
{
	_players.erase(player->playerId);
}

void Room::Broadcast(SendBufferRef sendBuffer)
{
	for (auto& p : _players)
	{
		p.second->ownerSession->Send(sendBuffer);
	}
}

일감을 만들어서 큐에 푸시하는 것으로 만들었기 때문에 cpp를 보면 락을 걸지 않은 것을 볼 수 있다. 싱글스레드처럼 코딩하면 된다.

 

주의해야 할 점은 외부에서 바로 함수에 접근하는 것이 아니라 Job이라는 일을 만들어서 집어넣고 Excute로 실행해야 한다는 것이다. 또한 락을 아예 걸지 않는 것은 아니고 큐에 푸시, 팝을 할 때 잠깐이나마 락을 걸게 된다. 그래도 스레드들의 경합을 줄일 수 있다.

 

오늘의 방식은 새로운 함수가 만들어질 때마다 Job을 하나씩 만들어줘야한다는 불편함이 있었다. 다음에는 이런 1세대 방식보다 조금 더 원활하게 Job을 만들고 관리하는 방법을 알아볼 것이다.