이전 글에서 다루었던 이 책에 처음 내용은 기초 필수 수학으로 시작한다. 그 내용 중에서도 가장 먼저 등장하는 것이 벡터이다. 유니티에서 벡터는 굉장히 중요하고 자주 다루는 개념이다. 위치를 나타내거나 이동, 회전, 다양한 부분에 사용된다.

 

책에서는 벡터의 개념이나 기본적인 연산 방법, 단위 벡터, 내적, 외적, 직교화, 위치 벡터, 마지막으로 DirectXMath 라이브러리의 벡터에 대한 내용을 다룬다. 기본적이고 찾아보면 나오는 내용들이기 때문에 따로 정리하지는 않고, 이런 내용들을 보면서 생각한 내용과 추가로 찾아본 것들에 대해서 적어보고자 한다.

 

같은 벡터라도 기준계가 다르면 좌표 표현이 달라진다는 것이다.

...

우리가 어떤 벡터를 좌표로 규정하거나 식별할 때 그 좌표가 절대적인 수치들이 아니라 항상 어떤 기준계에 상대적인 수치이고, 3차원 컴퓨터 그래픽에서는 여러 기준계들을 사용하는 경우가 많으므로, 벡터를 다룰 때에는 주어진 벡터의 좌표가 현재 어떤 기준계에 상대적인지를 기억할 필요가 있다. 또한, 한 기준계에서의 벡터 좌표를 다른 기준계로 변환하는 방법도 알아야 한다.

 

먼저 이 내용을 보면서 유니티의 월드 좌표와 로컬 좌표, 그리고 스크린 좌표가 떠올랐다. 월드 좌표는 월드 내에서 물체의 절대적인 위치를 나타내고, 로컬 좌표는 객체의 부모를 기준으로 한 좌표를 나타낸다. 스크린 좌표는 화면 내에서 표현되는 좌표를 나타낸다. 다른 좌표계도 있을지는 모르겠는데, 주로 이 3가지를 많이 이용하는 것 같다. 분류를 나누기 위한 객체들의 부모는 특별한 이유가 없으면 (0, 0, 0)의 좌표로 설정해주는 것은 지금와서는 자연스러운 행동이지만 이런 것들을 염두에 두고 하는 행동인 셈이다. 스크린 좌표는 보통 UI를 나타낼 때 많이 사용하는 것 같은데, 월드 좌표로 치환하기 위한 ScreenToWorldPoint 등의 함수도 유니티에서 지원한다. 자주 사용하진 않지만 로컬 좌표를 월드 좌표를 변환하기 위한 TransformPoint라는 함수도 있다.

 

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sdaff1004&logNo=221192712213

_moveDirection = new Vector3(_inputX, 0, _inputZ).normalized;

그리고 벡터의 정규화에 대한 내용을 보면서 유니티에서 벡터의 정규화를 어떻게 하는지 궁금해졌다. 이전에도 이동 속도를 항상 동일하게 맞추기 위해서 등의 이유로 벡터를 정규화하곤 했다. 여기서 normalized가 어떻게 구현되어있는지를 살펴보았다.

 

// 요약:
        //     Returns this vector with a magnitude of 1 (Read Only).
        public Vector3 normalized
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get
            {
                return Normalize(this);
            }
        }

Vector3의 경우, 내부 함수인 Normalize를 호출하는 것을 알 수 있었다.

public static Vector3 Normalize(Vector3 value)
        {
            float num = Magnitude(value);
            if (num > 1E-05f)
            {
                return value / num;
            }

            return zero;
        }
        
public static float Magnitude(Vector3 vector)
        {
            return (float)Math.Sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
        }

Normalize 함수를 살펴보면 이렇다. Magnitude 함수를 이용하여 Vector의 크기를 구해주고, 전달받은 Vector의 value를 Vector의 크기로 나눈다. 위에서 본 식과 동일한 방법을 이용하고 있는 셈이다. 단, 이 과정에서 Vector의 크기가 1E-05f보다 커야 한다. 1E-05f는 0.00001를 다르게 표현한 것이다.

 

What’s wrong with Vector3.normalized, or my understanding of it? - Unity Answers

 

What’s wrong with Vector3.normalized, or my understanding of it? - Unity Answers

 

answers.unity.com

포럼을 살펴보면 이에 관한 내용이 나와있다. 내용을 정리하자면 부동소수점 오류로 인해 두 벡터의 값이 일정값 이상 차이가 나지 않으면 같다고 정의를 해놓은 것이라고 볼 수 있겠다. 실제로 유니티의 인스펙터에서 벡터 값을 15로 변경하면 15.0000001이라거나 14.99999999 등이 되어있는 모습을 자주 볼 수 있다. 따라서 이런 예외를 둔 것으로 보인다.

 

이 정규화에서 추가로 든 생각은, normalized는 결국 Normalize 함수를 호출하는데, 한 번 더 과정을 거치기 때문에 바로 Normalize 함수를 호출하는 것이 성능 면에서 더 좋지 않을까? 라는 생각이었다.

 

using UnityEngine;

public class VectorTest : MonoBehaviour
{
    System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();

    Vector3 testVector1 = new();
    Vector3 testVector2 = new();

    float runTimeNormalized;
    float runTimeNormalize;

    const long REPEAT_TIME = 10000000000;

    private void Start()
    {
        Debug.Log("Calculating Normalized Time: " + REPEAT_TIME);

        stopwatch.Start();
        ExecuteNormalized(REPEAT_TIME);
        stopwatch.Stop();
        runTimeNormalized = stopwatch.ElapsedMilliseconds;

        Debug.Log("Calculating Normalize Time: " + REPEAT_TIME);

        stopwatch.Restart();
        ExecuteNormalize(REPEAT_TIME);
        stopwatch.Stop();
        runTimeNormalize = stopwatch.ElapsedMilliseconds;

        Debug.Log("Normalized: " +  runTimeNormalized + "ms, Normalize: " + runTimeNormalize + "ms");
    }

    private void ExecuteNormalized(long repeat)
    {
        for (long i = 0; i < repeat; ++i)
        {
            testVector1 = testVector2.normalized;
        }
    }

    private void ExecuteNormalize(long repeat)
    {
        for (long i = 0; i < repeat; ++i)
        {
            testVector1 = Vector3.Normalize(testVector2);
        }
    }
}

실제로 각각 100억번씩 호출하고 소요되는 시간을 확인해보았다.

 

normalized를 호출한 경우는 161017ms, Normalize를 호출한 경우는 145271ms가 소요되었다.

Normalize를 바로 호출하는 것이 성능상으로 약 9.8%정도 빨랐다. 아마 Normalize에서도 Magnitude함수를 호출하지 않고 바로 계산한다면 성능을 더 향상시킬 수 있을 것이다. 하지만 이렇게 하지 않는 이유는 아마 각각의 함수가 하는 역할들을 나눠놓기 위함이 아닐까 싶다. 이런 방법을 통한 성능 향상은 분명 있겠지만, 굉장히 미미하다. 내 컴퓨터에서 사용한 경우, normalized 대신에 Normalize를 1번 사용하면 약 0.0000015746ms라는 성능 상의 이점이 생긴다. 1초에 60프레임 이라고 생각하면, 매 프레임마다 10000번 호출한다고 가정했을 때 1초(1000ms)에 0.94476ms 정도를 절약할 수 있다.

솔직히 실질적인 성능 상의 이점은 거의 없다고도 말할 수 있을 것 같다. 그래도 normalized 대신에 Normalize를 사용하는 것이 크게 어려운 것은 아니기 때문에 앞으로는 Normalize를 사용하도록 노력해야겠다. 만약 혹시라도 호출이 극단적으로 저렇게 많이 늘어나는 경우에는 함수를 커스텀해서 사용함으로써 추가로 성능 향상을 도모해볼 수도 있겠다.

 

 

유니티 Vector3 new 는 스택에 생성된다 :: 3DMP (tistory.com)

 

유니티 Vector3 new 는 스택에 생성된다

http://cafe.naver.com/unityhub/25942 아래 개미개발자님의 코드 스타일의 최적화 이슈의 문제에 질답이 오가던 중, 메테오님의 지적으로 그동안 제가 잘 못 알고 있었던게 있었더군요. 그래서 많은 분들

3dmpengines.tistory.com

new string("test"); 1000회 반복

 

new Vector3(); 1000회 반복

 

또, 위에서 Vector3를 사용할 때 new를 호출할 때마다 가비지가 생성되는 것은 아닌가? 하는 생각에 찾아보면서 새롭게 알게된 것이 있다. 결론을 먼저 말하면 Vector3는 new로 생성해도 가비지가 생기지 않는다. Vector3는 구조체이고, C#에서 구조체는 new로 할당해도 힙이 아니라 스택에 생성되기 때문이라고 한다. 직접 프로파일러를 이용하여 테스트를 해보니 확실히 new string()을 호출할 때는 가비지가 생겼지만, new Vector3()의 경우는 가비지가 생기지 않는 것을 확인할 수 있었다.

 

 

 

오늘은 책을 통해 벡터에 관한 개념을 다시 복습하고, 새로 알게 된 지식으로부터 생긴 궁금증을 2가지 해결할 수 있었다. 유니티에서 정규화를 하는 방법은 특별하지 않고 일반적인 방법을 사용하며, normalized로 정규화를 하는 것보다는 Normalize로 정규화를 하는 것이 정말 미세하게지만 성능이 조금 더 좋다. 그리고 Vector는 구조체이고, C#에서 구조체는 new로 할당해도 힙이 아닌 스택에 생성되기 때문에 가비지가 생기지 않는다는 것이다. 그리고 프로파일러의 존재는 알고 있었지만 사용해본 것은 처음인 것 같다. 앞으로도 종종 사용하면서 익숙해질 필요가 있을 것 같다.

 

마지막으로, new Vector()를 사용해도 가비지가 생성되지 않는다는 것을 새롭게 알게 되면서 여전히 가비지 컬렉터에 대한 이해가 모자란 부분이 있다고 느꼈다. 내일은 가비지와 가비지 컬렉터에 대해서 더 자세히 알아봐야겠다.

최근 일주일 간 무엇을 하면 좋을까 고민을 많이 했다. 향후 도움이 될만한 지식을 배우려고 이곳저곳 건드려봤는데 딱히 깊이 있는 공부를 하지는 못한 것 같다. 기존 프로젝트를 이어하기에는 팬게임의 한계로 인해 애매한 부분들이 있고, 새로운 프로젝트를 시작하기에도 중간에 끊길 가능성이 있어서 손을 대지는 못하고 있었다.

 

그러다가 오늘 중학교 시절 친구를 만나게 되었다. 우연찮게 프로필 사진을 보니 게임 개발을 하고 있던데, 주변 친구 중에 게임 개발자가 거의 없어서 굉장히 반가웠다. 중학교 때는 곧잘 어울려 다녔지만 졸업 이후로는 연락을 하지 않아서 거의 8년 만에 연락을 해봤는데, 다행히 친구도 굉장히 반가워했다.

친구는 군대에서 게임 기획을 하고, 그 기획을 바탕으로 창업 지원을 받아 현재 창업을 했다고 한다. 만들고 있는 게임의 소개도 받았는데, 게임의 세계관이라든지 캐릭터, 로비 등 여러 시스템들이 맞물려서 잘 짜여져 있어 완성도가 높게 느껴졌다. 당연한 얘기겠지만 괜히 투자를 받을 수 있었던 게 아니구나, 싶었다. 아무튼, 친구의 이야기를 듣기도 하고 내 이야기를 하기도 하고 많은 이야기를 나눴다. 카톡으로 연락하면서 소개차 블로그를 보내줬는데 블로그나 코드도 많이 보고온 것 같았다. 덕분에 긍정적인 피드백을 많이 들을 수 있었다. 나야 내 나름대로 해온 것인데, 다른 사람, 그것도 개발자에게 좋은 이야기를 많이 듣게 되어서 기분이 좋았다. 내가 걸어온 길이 잘못되진 않았구나 싶기도 했다. 친구는 회사를 다녀본 적은 없어서 내가 회사에 재직하던 시절의 이야기를 듣고 많이 도움이 되었다고 해주었고, 나도 도움을 줄 수 있어서 기뻤다. 옛날에는 그냥 같이 별 생각이 놀던 친구라 약간은 가벼운 마음으로 나갔는데, 거의 개발 관련 이야기를 나눴던 것 같다. 이런 경험이 별로 없어서 조금 힘들기도 했지만 재밌고 유익한 시간이었다. 내가 지금 고민하고 있는 것들에 대해서 다른 관점에서 이야기를 듣기도 하면서 조금 생각을 정리해볼 수 있었던 시간이기도 했다.

 

 

먼저 나에게 있어서 최우선적인 문제는 병역의 해결이다. 병역으로 인해 여러 선택지들이 막혀있으니 향후 선택지의 폭을 넓히기 위해서라도 병역은 시급히 해결해야 할 문제이다. 지금 면접을 본 곳에서 만약 떨어지면 새로운 프로젝트를 시작해서 출시 경험을 만들어볼까 싶었는데, 아마 혼자서는 시간도 많이 걸리고 유의미한 결과물을 내기도 힘들 것이다. 그리고 친구의 말마따나 현재 게임을 출시하여 운영을 하고 있는 회사라면 어딜 가도 배울 것들이 많이 있을 것이기 때문에 지금 시점에서는 병역을 해결하면서 경력도 쌓고, 회사 생활을 경험해보는 것이 좋을 것이라고 생각했다.

 

다음으로는 나의 성장 방향에 대해서 생각해보았다. 최근에 서버 관련 지식도 있으면 도움이 되지 않을까 하는 생각에 서버 관련 공부를 하기도 했다. 겉핥기 식이라 조금 더 깊게 파봐야되나 싶기도 했는데, 당장은 좋은 생각은 아닌 것 같다고 생각했다. 물론 클라이언트 쪽도 잘 다루면서 서버도 할 줄 안다면 좋겠지만 클라이언트 프로그래머로서도 많이 부족한데 서버 공부를 한다는 것이 주객전도인 것 같아서 우선은 클라이언트 프로그래머로서 공부를 더 하는 것이 더 좋을 것 같다고 생각했다.

그리고 클라이언트 프로그래머로서 더 공부를 할만한 것들을 생각해보았다. 게임에 활용되는 수학 지식이나, 그래픽스 관련 지식을 공부할 수도 있고, 자료구조를 복습, 정리하고 활용해보면서 직접 성능 테스트를 해보는 것도 괜찮을 것 같다고 생각했다. C#에 대해서도 사실 모르는 부분이 많고, 가비지 컬렉터에 대해서 더 깊게 파보는 것도 좋을 것 같다. 3D 포트폴리오도 빠른 기간 내에 개발한 것이다 보니 더 다양한 기능들을 구현해보는 것도 괜찮을 것 같다. 이렇게 생각하니 뭘 하면 좋을까 생각했던 나 자신이 조금 무지했던 것처럼 느껴지기도 한다.

 

정리하자면 향후 계획은 이렇다. 현재 면접을 본 곳에 붙는다면 좋겠지만, 떨어진다면 다른 기업에 지원해보겠다. 그리고 합격 여부와는 별개로 클라이언트 프로그래머로서 전문성을 높이기 위한 공부를 지속적으로 해나갈 것이다.

 

이전에 그래픽스 관련 공부를 하기 위해 책을 샀다가 얼마 보지 못하고 접어놨었는데, 이 책 초반에 나와있는 게임 관련 기초 수학 지식 부분도 다시 보고, 렌더링 파이프라인와 셰이더 관련 파트 위주를 먼저 공부할 생각이다. 아마 게임 엔진을 제대로 다뤄본 지금 다시보면 감회가 새로울 것 같다. 지난 3달간 개인적으로는 눈부신 성장을 했다고 느끼는데, 앞으로도 이런 성장을 이어나갈 수 있도록 노력해야겠다.

나에게는 자동차와 관련된 취미가 있다. 아버지의 영향을 많이 받은 취미인데, 아버지께서는 어렸을 적부터 자동차에 대해 관심이 아주 많으셨다. 젊은 시절 자동차 관련 잡지를 구독하시기도 하고, 고등학교 때 면허도 따시고 열심히 돈을 모아서 오토바이를 사셨다고도 한다. 구입한 다음 날 밖에 나와보니 누가 훔쳐갔다는 슬픈 이야기가 있긴 하지만, 오토바이 사고 등 흉흉한 이야기에 대해 많이 들은 나로써는 어쩌면 다행이었을지도 모르겠다는 생각이 든다. 그때 아버지의 심정은 이루 말할 수 없었겠지만 말이다.

 

나도 어릴 적에 아버지를 따라 밖에 나가 아파트 주차장에 주차된 차들을 보며 이름을 외우곤 했다. 어째서 이게 시작됐는지는 모르겠지만, 아버지께서 차 이름을 알려주시고, 이후 내가 알아보는 것이 기특해서 종종 그런 시간을 가지신 것이 아닌가 싶다. 종종 밤 중에 바깥에 나가 차 이름을 배우곤 했다.

 

내가 본격적으로 차와 가까워진 것은 아마 운전 면허를 따면서인 것 같다. 3년 전 여름, 아버지의 말씀도 있으시고 나도 나중에 차를 몰려면 면허 정도는 따놓는 것이 좋지 않으려나 하는 생각으로 운전 면허 학원에 등록했다. 배우고 시험을 치면서 심적으로 꽤나 부담되거나 힘들기도 했었는데, 그래도 장내기능 2번, 도로주행 1번 시험을 봐서 1종 보통 면허를 취득했다. 면허를 취득한 이후로는 아버지의 특훈(?)이 있었다. 면허를 따고 바로 혼자서 운전을 하는 것은 무리가 있다고 생각하셨고, 나도 마찬가지로 그렇게 느꼈기 때문에 아버지가 조수석에 타시고 옆에서 운전하는 것을 지켜보면서 이것저것 알려주셨다. 시내, 국도, 고속도로 그리고 주간, 야간 이렇게 3 * 2 총 6가지 경우에서 각각 10시간씩 총 60시간 강습을 목표로 잡으셨다. 시내에서는 지하차도를 끼고 하는 유턴이라든지, 지금은 터널이 뚫려서 많이 이용하지 않는 죽령고개에서 산길, 연속되는 급커브, 엔진 브레이크를 활용하는 방법이라든지, 커브를 돌면서 가속하는 타이밍 등 다양한 것들을 배웠다. 지금 생각해보면 아마 아버지 나름대로 커리큘럼을 짜셨던 것 같다. 총 60시간이긴 하지만, 상대적으로 신경쓸 것들이 적은 고속도로나 국도는 10시간을 전부 채우지는 않았던 것 같다. 이후로는 몇 번인가 혼자서 운전을 하기도 했다.

 

이외에는 드라이빙 센터에 따라가서 각종 프로그램들에 참여하기도 했다. 처음에는 인천의 BMW 드라이빙 센터에서 비기너 프로그램에 참여했던 것 같다. 비기너 팩은 증서가 따로 안 보이는데, 면허 취득이 2020년 8월 4일이고 스타터팩 참여가 2020년 9월 20일이니 아마 이 사이일 것 같다. 비기너 프로그램은 잘 따라갔는데, 스타터팩은 트랙을 그렇게 잘 타진 못해서 이수증을 못 받고, 참여증만 받았다. 아마 1~2년 정도 운전 경력이 있는 사람들을 대상으로 하는 프로그램이다보니, 1달차가 따라가기에는 조금 힘든 부분도 있지 않았나 싶다. 그래도 이런 것들을 하면서 시트 포지션이라든지, 브레이크를 끝까지 밟고, 풋 레스트에 둔 발로 지탱한다든지, 시야 처리, 오버스티어에 대해서 카운터 스티어를 넣는 방법 등 다양한 것들을 배웠던 것 같다.

 

이후에는 인제 스피디움으로 가서 현대 드라이빙 아카데미 Level1을 이수하였다. 해당 프로그램에 대해 찾아보니 아마 아반떼를 탔던 것 같은데, 미니 쿠페S나 BMW 3시리즈와 비교하면 확실히 가속 등에 대한 반응이 조금 아쉬운 느낌이었던 것 같다.

 

1년 후에는 BMW 드라이빙 센터의 스타터 팩에 재도전했고, 이전의 경험도 있고 경력도 쌓여서 그런지 생각보다 수월했다.

 

시간이 조금 지나 올해 2월에는 M코어에 참여하여 이수증을 받았다. 프로그램 중간에 드리프트가 있는데, 차를 지속적으로 미끄러트리는 것이 상당히 어려웠다. 핸들을 풀었다가 다시 감아야 하는데, 이 타이밍을 놓치기 일쑤였다. 그래도 미끄러지는 상태를 1번 정도는 이을 수 있었다. 보통 처음 하는 사람들은 미끄러트리는 것도 제대로 못하는 경우가 많은데, 그것에 비하면 굉장히 잘 한 것이라는 얘기를 들었다. 트랙을 타는 실력도 많이 늘었던 것 같다. 이때 파악한 나의 문제는 너무 완벽하게 코너를 돌려는 것이었다. 코스를 따라가다보면 콘이 있는데, 콘에 잘 붙이고 최대한 커브를 잘 틀기 위해 그곳에만 집중하다보니 다음 구간에 대한 인지가 늦어져서 대비 또한 늦어졌다. 완벽하게 하려는 생각을 접어두고 현재 코너는 주변시를 활용하고, 다음 지점을 보며 트랙을 타니 확실히 실력이 많이 느는 것을 느낄 수 있었다. 아무튼, M코어는 다행히도 한 번에 이수증을 딸 수 있었다.

아버지께서 인텐시브라는 프로그램도 같이 하자고 몇 번 그러셨는데, 가격도 너무 비싸고 거의 하루종일 하는 프로그램이다보니 체력적으로 부담이 클 것 같아서 여러 차례 사양했다. 다른 프로그램들도 가격이 꽤 나가는 편이었지만, 나름대로 합리적인 가격이고 일반적인 상황에서 경험해보기 힘든 것들을 많이 배울 수 있는 기회라고 생각한다.

 

 

https://store.steampowered.com/app/1190000/Car_Mechanic_Simulator_2021/

 

Car Mechanic Simulator 2021 on Steam

Work your way to a service empire. Get your hands dirty in a highly realistic simulation game that pays major attention to details. Pay a visit to a new Auction house and buy cars in various conditions. Expand your range of services by investing in a new w

store.steampowered.com

또, 자동차 구조나 정비에 관한 지식도 얕게나마 알고 있다. 아버지께서 엔진 오일을 교체하시는 작업을 도운 경험이 있는데, 당시에는 지식이 많이 없어서 단순히 옆에서 보조하는 역할을 했다. 시간이 지나서 Car Mechanic Simulator라는 게임을 접하게 됐는데, 차의 문제를 점검하고 고장난 부품을 교체하거나 수리하여 돈을 버는 자동차 수리공을 체험해볼 수 있는 게임이다. 약 10대 정도의 차를 완전 분해했다가 다시 조립하면서 자동차의 구조에 대해서 조금이나마 알게 되었다. 아버지께 이 얘기를 했는데, 한동안 자동차의 구조라든가 원리에 대해서 많이 이야기를 듣게 되었다. 엔진이 어떤 원리로 작동하는지... 옛날 엔진부터 최근 엔진까지, 전륜구동이나 후륜구동, 이에 따른 엔진의 위치라든지, 이런 것이나 브레이크를 밟을 때 유압으로 도움을 준다든지, 핸들을 돌릴 때 도움을 받는 파워 스티어링이라든지, 여러 이야기를 들었다. 내가 게임에서 정비하는 모습을 하는 것을 옆에서 지켜보시기도 했는데, 신기하리만치 다 알고 계셨다. 게임 내에서 디스크 브레이크에 일반 디스크 브레이크와 디스크 브레이크 환기 라는 게 있는데, 실제 명칭은 Ventilated Disc Brake라고 한다. 환기가 되도록 구멍을 뚫어서 디스크 브레이크의 열을 더 잘 식혀줄 수 있는 디스크 브레이크라는 이야기를 들었다. 이것 이외에도 이런 것들에 대해서 물어보는 것마다 하나하나 너무 잘 알고 계시고 많이 얘기를 해주셔서 좀 놀라기도 했고, 많은 궁금증이 해결되었던 기억이 있다. 이전부터 기계 장치 등에 관심이 많으셔서 이것저것 기계들의 동작 원리를 알려주시기도 하셨는데, 그런 성향에 대해 확실하게 잘 느낄 수 있었다. 나도 이런 지식들을 자주 들어 많은 것들의 원리를 알게 되었고, 덕분에 새롭게 접하는 것들에 대해서도 금방 이해할 수 있게 된 것 같다.

 

 

마지막으로 F1과 관련된 이야기를 빼놓을 수는 없겠다. 아버지께서 자동차에 관심이 많으시다보니 F1 경기도 종종 챙겨보신다. 특히나 올해부터는 쿠팡플레이에서 중계를 해주기 때문에, 내가 가입한 쿠팡 계정으로 종종 보시곤 한다. 나는 어렴풋이 정도로밖에는 기억이 안 나지만, 약 10년 전 쯤에 국내에서 치뤘던 F1 경기장에도 갔었다고 한다. 아무튼, 나도 종종 옆에서 경기를 지켜보곤 하는데 그러면서 Qualifying 1, 2, 3와 본선의 구조, 그리고 스프린트 등 F1 경기의 흐름에 대한 설명을 듣기도 하고, DRS나 헤일로, 세이프티 카 관련 내용이라든지, 피트인 관련 전략이든지 등의 이야기를 듣기도 했다. 지금은 아는 내용이라서 옆에서 그냥 같이 경기를 지켜보곤 한다. 저번 주에 했던 경기도 보았는데, 최근에는 레드불이 상당히 강세인 것 같다. 아마 드라이버의 실력도 좋긴 하겠지만, DRS를 켜지도 않았는데 직선 코스에서 페라리를 추월하는 모습을 보며 페라리 선수들에게 연민의 감정이 느껴졌다. 더 좋은 차가 앞으로 나가는 것은 당연하지만, 선수 입장에서 불합리하게 느껴지진 않을까, 싶었다. ㅋㅋㅋ. 이후에도 거의 30초에 가까운 차이를 내며 달려나가는 모습에 조금 맥이 빠지기도 했다. 그래도 선수들이 치열하게 달리는 모습과 엎치락뒤치락 하는 모습, 세이프티카가 나오기를 기다리며 피트에 들어가지 못하는 선수들을 보기도 하면서 재밌는 시간을 보냈던 것 같다.

 

 

이 이야기는 취미와는 조금 거리가 있는 이야기지만, 차의 기능 고장으로 사고가 날 뻔한 경험이 있다. 아마 약 2년 전의 일인 것으로 기억한다. 아버지께서 차를 바꾸시면서 이전에 쓰시던 차를 할아버지께서 사용하셨는데, 할아버지께서 나이가 드시면서 운전을 안 하시게 되고 내가 면허를 따다보니 차를 내가 쓰게끔 해주시겠다고 하셨다. 차에 ABS 관련 노란등이 들어와서 공식 서비스 센터에 맡겼는데, 계속 같은 문제가 지속되고 빨간등까지 들어오기도 했다. 마지막 수리를 맡겼을 때는 몇km의 테스트 주행도 마치고 이상이 없다고 들었는데, 차를 끌고 고속도로로 진입하려는 찰나에 차의 액셀이 반응하지 않는 증상을 겪었다. 액셀을 밟아도 2~3초간 아무런 반응이 없다가 갑자기 RPM이 치솟으면서 차가 급발진하는 증상을 겪고 아 이건 뭔가 잘못됐다 싶어서 차를 유턴시켰다. 돌아오는 길이 2차선 길이었는데, 교차로 너머 횡단보도 앞 2차선에 트럭을 세워둔 것을 늦게 인지해서 교차로를 지나가면서 1차선으로 끼어들어야 했다. 1차선에서 오던 차가 택시였는데, 처음에는 기다려주다가 내가 머뭇대니 갑자기 속도를 확 내면서 지나갔다. 나는 그때 액셀을 밟아도 반응이 없어서 제발 앞으로 가달라고 빌고 있었다. 확 튀어나오는 택시를 보며 이거 잘못하면 사고나겠다 싶어서 핸들을 급히 반대로 꺾고 브레이크를 밟았던 것 같다. 아마 그 상황에서 RPM이 튀면서 차가 급발진했으면 사고가 났을 것 같다. 시내라서 속도가 빠르진 않았지만 위험한 순간이었다. 다행히 이후로는 집에 오면서 같은 문제가 발생하지 않았고, 이 차는 곧바로 폐차를 했다. 10년 가까이 썼던 것으로 기억하는데, 관리가 잘 되어있어서 겉으로 보기엔 깔끔했지만 내부 회로에 문제가 있었던 것 같다. 이미 수리비를 꽤 들였지만 이 문제를 해결하려면 중고차 값보다 더 많은 돈이 들어갈 가능성이 농후하여 폐차를 선택하셨던 것 같다.

 

 

 

이전까지 딱히 의식하고 있진 않았지만, 자동차도 분명히 내가 가지고 있는 취미 중에 하나라는 것을 느꼈다. 엄청나게 관심이 많거나 아주 즐기는 정도까지는 아니지만 어느정도 흥미를 가지고 즐기는 취미이고, 무엇보다 아버지와 공통의 관심사로 같이 시간을 보낼 수 있는 좋은 취미라고 생각한다.

'잡담' 카테고리의 다른 글

블루 아카이브 1.5주년 페스티벌(온리전) 후기  (1) 2023.05.23
향후 계획  (0) 2023.05.07
합정 애니플러스 & 홍대 애니메이트 방문  (0) 2023.04.16
작혼 첫 역만 달성 - 쓰안커  (0) 2023.04.07
작혼 작걸 달성  (0) 2023.04.05

모바일 게임과 PC 게임 사이에는 큰 차이가 존재한다. 모바일 게임과 PC 게임의 차이는 어떤 것이 있는지, 이런 차이가 국내 모바일 게임 시장에서 어떤 형태로 나타나는지 나름대로의 생각을 정리해보고자 한다.

 

모바일 게임과 PC 게임의 차이

1. 플레이 환경

모바일 게임과 PC 게임 사이의 가장 큰 차이는 플레이 환경이라고 생각한다.

플레이 환경 차이의 첫 번째는 화면 크기이다. 점점 큰 화면을 가진 모바일 기기가 등장하고, 갤럭시 노트, 아이패드 등의 태블릿도 존재하지만 그럼에도 불구하고 모바일 기기는 휴대성이 중요하기 때문에 모니터의 크기를 넘기는 힘들다. 보통은 스마트폰만 가지고 있는 경우가 많기 때문에 모바일 게임에 비해서 PC 게임은 10배 이상 큰 화면으로 즐길 수 있다는 장점이 있다.

두 번째로는 컨트롤 환경이다. PC는 키보드와 마우스라는 입력 장치가 존재한다. 때문에 다양한 입력이 가능하다는 점과, 입력의 정밀성이 높으며 손가락의 피로도가 적다는 장점이 있다. 또한 키보드와 마우스를 누르면서 느낄 수 있는 타격감 또한 중요하다고 생각한다.

세 번째로는 하드웨어의 성능 차이이다. 모바일 기기의 성능이 비약적으로 성장한 것은 틀림없는 사실이지만, 여전히 고성능 PC의 성능을 뛰어넘기는 힘들다. 고작 부품 하나인 그래픽카드의 크기가 스마트폰 몇 대를 합쳐야 겨우 비슷한 크기가 될 정도이니 말 다 했다고 볼 수 있겠다. 이외에도 차이점들이 많이 있겠지만, 이 세 가지가 플레이 환경에 가장 큰 영향을 주는 요소라고 생각한다.

 

 

안타까운 얘기지만, 이런 요소들로 인해 모바일 게임은 선천적으로 PC 게임에게 게임성으로 승부를 하는 것 자체가 불가능하다. 모바일 기기의 작은 화면은 유저의 시야에서 극히 일부분을 차지하고, 더욱이 화면이 작기 때문에 PC에 비해 그래픽 요소들이 훨씬 적게 배치될 수 밖에 없다. 그마저도 조작이나 각종 정보를 알려주는 UI가 화면에서 큰 비중을 차지하게 된다. 이런 점들은 유저의 몰입을 저하시키는 요인이고, 나아가 게임성을 떨어뜨릴 수 밖에 없는 요인이기도 하다.

 

마우스 혹은 키보드가 주는 조작의 즐거움도 빼놓을 수 없는 중요한 요인 중 하나이다. 대표적으로 FPS게임에서 마우스만큼 대중적이고 편한 입력 기기는 아직까지 없는 것 같다. 모바일 환경에서는 화면의 중앙 상당 공간을 드래그하여 조준을 해야하는데 이것이 굉장히 힘들고, 콘솔도 마찬가지로 조이스틱을 돌려서 조준하는 것이 매우 어렵다. 때문에 이 두 플랫폼에서는 FPS 게임에 에임 보정이 들어가 있는 경우가 많다. 특히나 모바일 환경에서는 조준과 발사에 동시에 두 손가락이 사용되기 때문에 이동을 하면서 조준, 발사를 하는 것이 정말 어려운 일이다. 때문에 모바일 FPS 게임을 하는 사람들은 조작을 도와주는 보조 기기를 구매하여 플레이하는 경우도 있다.

마찬가지로 키보드도 입력에 최적화 된 장치이기 때문에 최대한 편하고 즐겁게 입력할 수 있도록 설계되어있어 편의성이 최대 장점인 터치와는 비교할 수 없을 정도의 즐거움 제공한다. 메이플스토리에서 사냥을 하는 모습을 보면, 키보드의 방향키와 Alt키, Ctrl키, Shift키 +@를 반복하여 누르면서 똑같은 사냥을 반복하는 모습을 볼 수 있다. 옆에서 지켜보면 대체 왜 저런 걸 하고 있나, 싶지만 막상 해보면 생각보다 재밌다. 물론 게임 내에서 볼 수 있는 그래픽 효과라든지, 보상 등도 영향이 있겠지만 키보드의 역할도 상당히 크다고 생각한다. 만약 모바일 기기로 똑같은 플레이를 한다면 상당수의 유저가 떠날 것이다. 우선 키보드의 경우 비슷한 위치에서 손가락을 바꿔가며 키를 누르기 때문에 손가락의 피로도가 적지만, 모바일 기기의 경우 엄지 손가락만을 사용하는 경우가 많기 때문에 엄지 손가락을 반복하여 움직인다면 피로도가 상당히 심할 것이다. 또, 키보드를 누를 때 주는 즐거움이 사라질 것이기 때문에 기존에 비해 유저가 느끼는 즐거움이 줄어들 것이다. 대표적으로 마우스와 키보드가 큰 영향을 주는 두 게임을 예로 들어봤는데, 극히 일부 게임을 제외한다면 키보드와 마우스를 사용하지 않는 것이 유저 경험을 떨어뜨리는 요인이 될 수 있다고 본다.

 

마지막으로 하드웨어의 성능 차이가 만드는 차이에 대해 말해보겠다. 모바일 게임은 PC 게임에 비해 모든 성능이 뒤쳐진다. RAM 용량도 작기 때문에 최적화가 제대로 되지 않은 게임은 플레이를 하다가 튕기기 일쑤다. 따라서 모바일 게임을 개발할 때는 최적화를 하기 위해 다양한 방법을 사용하는데, 성능에서 가장 큰 부분을 차지하는 것이 그래픽이기 때문에 모바일 게임을 만들 때는 캐릭터 모델링 등의 퀄리티를 다소 낮추는 방법을 많이 사용한다고 한다. 폴리곤의 수를 줄이고 텍스쳐의 크기를 줄이는 등 작업을 하면 당연히 용량이 줄어들고 성능은 향상되겠지만, 그만큼 게임의 퀄리티가 떨어진다는 얘기이기도 하다. 고스트 오브 쓰시마와 같은 PC게임에서도 더 나은 유저 환경을 위해 중요성이 떨어지는 풀 등의 텍스쳐 퀄리티를 낮추는 방법을 사용하는데, 모바일 게임에서는 말할 것도 없을 것이다. 만약 퀄리티를 낮추지 않는다면 게임의 프레임이 떨어져 유저 경험을 저하시키는 요인이 될 수도 있다.

 

정리하자면, 모바일 게임과 PC 게임의 플레이 환경 차이는 크게 화면 크기, 마우스와 키보드와 같은 컨트롤 환경, 하드웨어의 성능 차이에서 비롯된다. 화면 크기가 작은 모바일 게임은 PC 게임에 비해 유저의 몰입이 떨어질 수밖에 없고, 조작의 불편함은 유저 경험을 떨어뜨리는 원인이 된다. 하드웨어 성능이 낮은 모바일 게임은 PC 게임보다 게임의 퀄리티가 낮을 수밖에 없다. 이런 것들은 모바일 게임이 PC 게임에 비해서 게임성이 떨어질 수 밖에 없는 이유이기도 하다.

 

2. 접근성

모바일 게임이 가지는 장점도 있다. 바로 접근성이다. 설치, 실행, 플레이의 접근성에 대해서 이야기해보겠다.

 

먼저 설치에 대한 비교이다. PC 게임의 경우, 설치를 하기 위해 전용 툴이 필요한 경우도 많고, 설치에 걸리는 시간도 상당히 길다. 또한, 게임들이 여기저기 흩어져있어 존재조차 알기 어려운 게임들이 많다. 많은 게임들을 모아놓은 스팀이라는 플랫폼이 있긴 하지만, 모든 게임이 모여있는 것도 아니고 대부분의 게임들이 유료라는 문제가 있다. 반면에 모바일 게임의 경우 플레이 스토어 혹은 앱 스토어라는 자체 어플이 반드시 깔려있다. PC 게임에 비해 용량이 작기 때문에 설치에 걸리는 시간도 짧은 편이고, 하나의 스토어에 거의 모든 어플들이 모여있는데다가 상당수의 게임들이 무료이기 때문에 PC 게임에 비하면 접근성이 월등히 좋다. 게임들을 구경하다가 관심이 가면 가볍게 설치했다가 잠깐 플레이해보고 지우는 경우도 많다. 즉, PC 게임에 비해서 모바일 게임의 설치에 대한 접근성이 월등히 좋다는 장점이 있다.

 

실행의 경우, PC게임은 게임 아이콘을 찾아 클릭하며, 전용 앱을 실행하며 로딩을 기다리고, 실행 버튼을 누르고 게임 실행을 잠시 기다리며, 게임이 실행된 이후에도 로딩을 기다리고 로딩이 완료되면 비로소 게임을 할 수 있다. 반면에 모바일 게임은 게임 아이콘을 누르면 로딩 후에 바로 게임을 할 수 있다. 아이콘을 찾을 필요 없이 푸쉬 알림을 눌러서 바로 실행할 수도 있다. 개인적인 생각이지만 PC 게임은 실행하는 것 자체가 힘들게 느껴질 때가 있다. 아무튼, 모바일 게임이 PC 게임에 비해 실행 자체도 훨씬 간편하다는 장점이 있다.

 

마지막으로 플레이에 대한 접근성이다. PC 게임은 플레이를 하기 위해 컴퓨터 앞에 앉아야 한다. 이 상태에서 이동하기도 불가능하다. 반면, 모바일 게임의 경우 어디서든 플레이할 수 있다는 장점이 있다. 집 밖은 물론이고, 지하철 등을 타고 이동할 때나, 앉아서는 물론이고 서서도 플레이할 수 있으며, 조금은 위험하지만 움직이면서 플레이를 하는 것도 가능하다. 원한다면 침대에 누워서 플레이를 할 수도 있고, 밥을 먹으면서 하는 사람들도 있다. 플레이의 접근성이 모바일 게임이 가진 가장 큰 장점이라고 본다.

 

https://gsis.kwdi.re.kr/statHtml/statHtml.do?orgId=338&tblId=DT_1ID0511R
https://www.gallup.co.kr/gallupdb/reportContent.asp?seqNo=1309

 

추가로, PC를 다루는 사람보다 모바일 기기를 다루는 사람들이 더 많다는 점도 있다. 2021년 기준 60대 이상의 1개월 이내 컴퓨터 이용자 비율은 42.2%, 70대 이상은 14.3%에 불과하지만 60대 이상의 스마트폰 사용률은 2021년 기준 83%, 2022년 6월에는 90%까지 상승했다. 마찬가지로 50대의 1개월 이내 컴퓨터 이용자 비율은 71%에 불과하지만, 같은 시기의 스마트폰 이용자 비율은 99%이다. 물론 모든 유저가 게임을 플레이한다고 할 수는 없겠지만, 그것은 컴퓨터도 마찬가지이다. 기기를 보유한 사람이 많다는 얘기는 유저풀이 그만큼 넓다는 이야기이기도 하고, 해당 기기에서 이용 가능한 게임의 접근성이 더 높다는 얘기도 될 수 있다.

 

 

정리하자면, PC 게임은 모바일 게임과 비교하여 설치, 실행 과정이 복잡하고 느리다. 마찬가지로 플레이를 할 수 있는 환경도 모바일에 비해서 상당히 제한되어 있으며 절대적인 유저풀도 좁기 때문에 PC 게임은 모바일 게임이 가진 접근성이 훨씬 떨어질 수 밖에 없다는 것이다.

 

 

모바일 게임과 PC 게임의 차이와 국내 모바일 게임 시장

이제 모바일 게임과 PC 게임의 차이를 생각하며 국내 모바일 게임 시장을 살펴보겠다. 모바일 게임은 PC 게임에 비해 플레이 환경이 열악하기 때문에 유저를 몰입시키기도 힘들고, 컨트롤도 불편하며 게임성도 떨어질 수밖에 없다. 반면에 접근성이 훌륭하다는 장점을 가지고 있다. 따라서 이런 접근성이라는 장점을 극대화시키는 방향으로 발전해온 것으로 보인다.

 

https://www.mobileindex.com/mi-chart/weekly-rank/revenue

 

국내 마켓에서 최상위권을 차지하고 있는 MMORPG 게임들은 전부 자동사냥이 있는 것으로 파악된다. 열악한 플레이 환경을 극복하기 위해 MMORPG에서 가장 많은 시간이 드는 사냥을 자동으로 전환한 것이다. 설치와 실행, 플레이도 간편하기 때문에 빠르게 접속해서 자동사냥을 걸어놓고 방치하는 것도 어렵지 않다. 좋은 접근성을 활용해서 생각날 때마다 들어가서 아이템들을 정리해주는 등 보상을 얻는, 유저에게 즐거움을 주는 부분만 남겼다. 모바일에 많이 존재하는 방치형 게임도 이와 비슷한 부류라고 생각한다.

 

블루 아카이브와 같은 게임의 경우, 유저의 게임 플레이를 제한하여 의도적으로 가벼운 플레이를 하게끔 만들어서 서브 게임으로서의 포지션을 노린 것으로 보인다. 모바일 게임은 설치, 실행, 플레이의 접근성이 뛰어나기 때문에 컴퓨터 게임을 하면서 플레이를 할 수도 있고, 다른 모바일 게임을 하면서도 중간에 잠깐씩 플레이하기도 좋다. 적은 경쟁 요소로 유저의 피로도를 줄이면서도 약간의 경쟁 요소는 유지하고, 또 오타쿠들의 충성심을 이용하여 매출도 잘 뽑아내고 있다.

 

우마무스메 프리티 더비와 같은 게임의 경우, 조작의 피지컬적인 요소를 배제하여 조작을 편리하게 만들었다. 단순히 터치만을 이용해 조작을 하며, 유저가 게임 내에서 영향을 미치는 것은 유저가 어떤 선택을 해서 어떤 요소를 터치하느냐이다. 한 손으로도 충분히 플레이를 할 수가 있으며, 언제든지 중간에 멈추거나 재개할 수도 있다. 하지만 경마라는 주제의 특성상 경쟁 요소가 많은 부분을 차지하고 있고, 특히 챔피언스 미팅의 존재로 인해 체감상 경쟁이 차지하는 비율이 크다. 여기서 우승하기 위해서는 꾸준한 플레이와 투자가 필요하다. 가치가 높은 게임 플레이는 하루에 5번까지 가능한데, 대략 2시간 혹은 그 이상이 걸린다. 투자의 경우, 흔히 가챠라고 얘기하는 뽑기가 필요한데, 뽑기의 난이도가 어렵고 부담감이 매우매우 크다. 이런 요소들로 인해 유저가 많은 피로도를 느끼게 되는 게임이기도 하다. 하지만 이런 단순한 조작만으로도 메인 게임의 자리를 노릴 수 있다는 점이 상당히 흥미로운 게임이다.

 

반대로 탕탕특공대, 궁수의 전설이나 각종 퍼즐, 러닝 액션 게임과 같이, 피지컬적인 요소는 유지하면서 조작을 편리하게 만든 게임들도 있다. 이런 게임들은 보통 한 손으로도 플레이가 가능하며, 탭이나 슬라이드 등 간편한 조작이지만 유저의 피지컬적인 요소를 유지하면서 편리하고 또 즐겁게 게임을 할 수 있게끔 만들었다.

 

이외에는 FIFA모바일이나, 원신 등과 같이 컴퓨터에 비해 다소 불편하지만 모바일에 친화적인 플레이 방식을 이식하고, 게임 플레이 자체도 컴퓨터에서 하는 것과 거의 동일하게 이식한 게임도 존재한다. 모바일 게임에서도 좋은 게임성을 원하는 유저들도 있을 것이기 때문에, 이런 유저들을 타겟으로 한 게임으로 생각된다.

 

 

결과적으로 모바일 플랫폼에서 매출 순위가 높은 게임들을 살펴보았을 때, 게임성을 약간 포기하되 접근성이라는 장점을 많이 활용한 게임들이 대부분을 차지하는 것 같다. 물론 뛰어난 게임성을 가진 게임들도 있지만, 이런 게임들은 대부분 PC에 이미 있는 게임을 모바일로 이식한 게임이기 때문에 예외로 두어도 무방할 것 같다. 

해외의 경우는 우리나라와는 약간 다르기도 한 것 같은데, PC 보급률이라든지, 이동시간이라든지, PC방의 대중성 등 다양한 요인들이 있을 것 같다. 하지만 적어도 우리나라에서 모바일 게임은 접근성을 잘 활용한 게임이 주류인 것으로 보인다. 물론 접근성을 잘 활용하면서 게임성을 얼마나 잘 살리느냐가 관건일 것이다.

 

 

 

이야기가 참 길었던 것 같다. 요약하자면 모바일 게임은 PC 게임에 비해 플레이 환경이 열악하지만 접근성이 좋기 때문에 이런 장점을 활용하기 위한 형태로 모바일 게임들이 발전하였고, 그 결과 주류 모바일 게임들의 형태가 지금과 같아졌다는 것이다. 자동 사냥을 통한 방치형 게임이라든가, 애초부터 서브 게임 포지션을 노리고 가볍게 만든 게임이라든가, 피지컬적인 요소를 배제한 게임도 있고, 조작을 단순하게 만든 게임도 있다. 어찌되었든 결과적으로는 게임이 전체적으로 단순해지는 형태로 변했다. 국내 모바일 게임은 게임성이 떨어진다며 비판하는 사람들도 있고, 개인적으로도 아쉬운 부분이 있다고 생각하지만 이렇게 생각해보니 어쩔 수 없다고도 느껴지는 것 같다. 앞으로 모바일 게임을 개발하면서 지금 정리한 내용들을 바탕으로 어떻게 하면 모바일 게임이 가진 접근성을 최대한 이끌어낼 수 있을지 생각하면서 개발해야겠다.

Tucker님의 "게임 네트워킹의 이해" 영상을 시청한 내가 Race Condition이라는 것에 대해 내가 알고 있었던 내용은, 멀티 쓰레드 방식에서 공유 자원에 접근하는 순서에 따라 결과가 달라질 수 있다는 것이었다. 또한 해당 영상에서는 "Locking을 쓰다보면 서로 간에 경쟁이 심해져서 Race Condition이 발생한다" 고 설명을 해서 Lock을 건 상태에서 발생하는 문제라고 이해하고 있었다. 그렇기 때문에 Race Condition 문제를 어떻게 해결하느냐? 라는 질문에 대해서 전혀 답을 낼 수 없었다. Lock을 걸은 상태이기 때문에 들어오는 순서에 따라 결과가 달라진다면, 들어오는 순서를 항상 일정하게 보장해줘야 할텐데, 언제 누가 들어올지도 모르는 상황에서 순서를 정한다는 것이 말이 안된다고 생각했기 때문이다.

사실 Race Condition은 Lock을 걸지 않은 상태에서 공유 자원에 동시 접근할 때 생기는 문제이다. 완전히 잘못 이해하고 있었다. 다른 개념일지는 모르겠지만, 적어도 찾아본 바에 의하면 영상에서 개념을 잘못 설명해준 것이다. 간단하게 해결책은 Lock을 걸어준다. 라고도 할 수 있을 것 같은데, 나는 Lock이 이미 걸려있는 상태에서 발생하는 문제라고 인식을 했기 때문에 완전히 번지수가 틀렸던 것이다.

 

그래서 오늘은 Race Condition, 그리고 이에 관련된 동기화, 임계 영역, 스핀락, 뮤텍스, 세마포어에 대해 자세히 알아보고 또 정리하고자 한다.

 

경쟁상황 (Race Condition), 뮤텍스, 세마포어 (tistory.com)

 

경쟁상황 (Race Condition), 뮤텍스, 세마포어

경쟁 상황(Race Condition)이란? 동시에 여러 개의 프로세스가 동일한 자료를 접근하여 조작하고, 그 실행 결과가 접근이 발생한 특정 순서에 의존하는 상황을 경쟁 상황(race condition)이라고 합니다.

yongc.tistory.com

https://youtu.be/vp0Gckz3z64

https://youtu.be/gTkvX2Awj6g

맨 위 링크의 글과 2개의 유튜브 영상을 참조했다. 글도 잘 적혀있는 것 같은데, 이해가 살짝 어려운 부분이 있어서 쉬운코드님의 유튜브 영상을 함께 참조했다. 확실히 유튜브 영상이 잘 모르는 사람들을 배려해주는 경향이 있어서 이해하기 편한 것 같다.

 

 

가장 먼저 Race Condition이란, 여러 프로세스/쓰레드가 동시에 같은 데이터를 조작할 때 타이밍이나 접근 순서에 따라 결과가 달라질 수 있는 상황을 의미한다.

https://yongc.tistory.com/53#2.%20%EA%B2%BD%EC%9F%81%20%EC%83%81%ED%99%A9%20%EC%98%88%EC%8B%9C

위와 같은 코드에서 thread_1을 쓰레드1이 실행하고, thread_2를 쓰레드2가 실행한다고 가정하면, 순서에 상관없이 각각 총 1000만씩을 더하고 빼기 때문에 count는 0이 될 것이라고 생각할 수 있지만, 실제로는 그렇지 않을 가능성이 있다.

 

https://yongc.tistory.com/53#2.%20%EA%B2%BD%EC%9F%81%20%EC%83%81%ED%99%A9%20%EC%98%88%EC%8B%9C

count++와 같이 ++ 연산자는 왼쪽 프로세스 A에 있는 3줄의 동작을 수행한다. 문제는 이 일련의 동작을 수행하는 도중에 컨텍스트 스위칭이 발생할 수 있다는 것이다. 프로세스 A에서 수행을 하다가 중간에 멈추고 프로세스 B를 수행하게 되는 것이다. 그러면 프로세스 B를 돌면서 -1을 1000만번 반복하여 count에 -1000만이 들어가게 된다. 문제는 이 다음인데, 위의 사진에서 컨텍스트 스위칭이 발생한 지점에서 다시 실행한다면, 레지스터에 저장된 +1이라는 값이 count에 들어가는 부분부터 시작하게 된다. 즉, count가 +1로 덧씌워지는 것이다. 프로세스 B에서 연산했던 값이 사라지는 셈이다. 이 경우, 2개의 프로세스가 완료되었을 때 count에는 1000만이라는 값이 저장된다.

반대로, 프로세스 B가 먼저 실행되다가 동일한 상황에서 A로 컨텍스트 스위칭이 발생한다면, 2개의 프로세스가 완료되었을 때 count에는 -1000만이라는 값이 저장될 것이다. 즉, 접근 순서에 따라 결과가 달라진 것이다. 만약 컨텍스트 스위칭이 "레지스터가 count에 값 반환" 이후에 일어난다면, 결과는 0이 될 것이다. 이 경우는 타이밍에 따라 결과가 달라지는 셈이다.

 

따라서 Race Condition에서 중요한 것은 동기화를 하는 것이다. 동기화란, 여러 프로세스/쓰레드를 동시에 실행해도 공유 데이터의 일관성을 유지하는 것이다. 싱글 코어의 경우, 위의 예에서 인터럽트가 발생하지 않도록 해주는 방법이 있다. 다만 멀티 코어의 경우에는 여러 코어에서 동시에 프로세스를 실행할 때 문제가 발생할 수 있기 때문에 활용하기 어려운 방법이다. 가령 "레지스터에 count 할당" 부분을 두 코어에서 동시에 실행한다면, 두 프로세스가 돌아도 값은 1번만 증가할 것이다. 따라서 현실적인 방법은 count++ 혹은 count-- 부분에 한 번에 한 쓰레드만 접근할 수 있도록 제한을 걸어주는 것이다. 이를 위해 임계 영역(critical section)이라는 것을 설정한다. 임계 영역이란, 공유 데이터의 일관성을 보장하기 위해 하나의 프로세스/쓰레드만 진입해서 실행 가능한 영역을 의미한다.

https://youtu.be/vp0Gckz3z64

위 사진처럼, critical section에 진입하기 전에, entry section에서 현재 진입이 가능한지 여부를 체크하고, 진입이 가능하다면 다른 쓰레드가 접근하지 못하도록 막아준다. 이후 critical section을 실행하고 벗어날 때 exit section에서 다른 쓰레드가 접근할 수 있도록 작업을 해준다. 이 세 가지 sction을 제외한 section이 remainder section이다. 이렇게 하면 한 번에 하나의 쓰레드만 임계 영역에 접근할 수 있게 되는 것이다.

또한, 이 방식을 제대로 활용하기 위해서 지켜야하는 3가지 조건이 있다. 상호 배제(mutual exclusion), 진행(progress), 한정된 대기(bounded waiting)이다. 먼저 상호 배제는 한 번에 하나의 프로세스/쓰레드만 임계 영역에 접근할 수 있도록 해야한다는 것이다. 진행은 임계 영역이 비어있고, 임계 영역에 진입을 원하는 프로세스/쓰레드가 있을 경우, 실행할 수 있도록 해야 한다는 것이다. 마지막으로 한정된 대기는, 어떤 프로세스/쓰레드가 임계 영역에 들어가기 위해 무한정 대기하는 상태가 되면 안된다는 것이다. 우선순위 스케줄링에서는 대기 시간에 따라 우선순위를 높여주는 방식을 사용해서 이것을 방지하는데, 임계 영역을 구현할 때도 이런 식으로 기아 상태가 발생하지 않도록 방지해줘야 한다는 것이다.

 

https://youtu.be/vp0Gckz3z64

 

mutual exclusion을 보장하는 방법은 lock을 사용하는 것이다. 이전 사진의 entry section에서 acquire lock으로 lock을 획득하여 lock을 획득한 프로세스/쓰레드만 critical section에 접근할 수 있다. 이후 exit section에서 release lock으로 lock을 풀어주어 다른 프로세스/쓰레드가 접근할 수 있도록 한다. 이 lock을 거는 방법은 크게 3가지가 있는데, 각각 스핀락, 뮤텍스, 세마포어 방법이다. 

 

*아래 코드들은 이해를 돕기 위한 예시이며 구체적인 동작 방식은 OS, 프로그래밍 언어에 따라 다를 수 있음

 

https://youtu.be/vp0Gckz3z64

먼저 스핀락(spinlock)방식에 대해서 살펴보겠다. 위 사진에서는 lock 0일 때 임계 영역에 접근이 가능해진다. 만약 lock이 1이라면 while문을 계속 반복해서 돌면서 lock을 얻어올 때 까지 대기하게 된다. 임계 영역에 이미 진입한 프로세스/쓰레드가 있다면 임계 영역 실행을 마치고 lock을 0으로 초기화시켜준 이후에 다른 프로세스/쓰레드가 접근할 수 있게 된다.

 

+ TestAndSet는 CPU의 atomic 명령어로, 실행 중간에 간섭받거나 중단되지 않으며, 같은 메모리 영역에 대해 동시에 실행되지 않는다. 즉, lock을 얻어오는 요청을 동시에 실행한다고 하더라도 동시에 실행되지 않는다.

 

이 방식의 문제점은 lock을 얻어오기 위해 무한정 대기하기 때문에 CPU를 낭비한다는 점이다. 그동안 다른 작업을 처리할 수도 있는데 말이다. 때문에, lock을 얻기 위한 목록인 queue에 등록하고, lock 준비되면 queue에 있는 프로세스/쓰레드 중 하나를 깨우는 방식이 바로 뮤텍스(mutex) 방식이다.

 

https://youtu.be/gTkvX2Awj6g

뮤텍스 방식에서는 lock을 얻어올 수 없다면 현재 쓰레드를 큐에 넣고, 임계 영역을 마칠 때, 큐에 대기 중인 쓰레드가 있다면 그 중에 하나를 깨운다. 없다면 lock을 풀어서 다른 쓰레드가 접근할 수 있도록 해준다. guard는... 위에 왜 저렇게 쓰여 있는지나 영상을 봐서는 이해하기가 조금 어려운데, 검색해본 바에 의하면 C++11부터는 guard를 호출할 때 lock이 호출되고, guard가 소멸할 때 unlock이 호출된다는 것 같다. unlock을 까먹을 가능성을 차단해줘서 조금 더 편리하게 사용할 수 있는 것 같다. C++17부터는 scoped_lock이라는 것이 다중 잠금이 가능하여 lock_guard를 대체할 수 있는 것 같다.

 

보통은 뮤텍스 방법이 CPU를 놀리지 않기 때문에 안정적이고 효율적인 방법이지만, 멀티 코어 환경이고 임계 영역 내에서의 작업이 컨텍스트 스위칭보다 더 빨리 끝난다면 스핀락이 더 빠를 수 있다고 한다. 뮤텍스 방법은 큐에 대기 중인 쓰레드를 깨워서 컨텍스트 스위칭을 하기 때문에, 이것보다 빠르게 임계 영역에서의 작업이 끝나면 더 빠르게 다음 쓰레드로 넘어갈 수 있다는 것이다. 싱글 코어에서는 스핀락에서도 컨텍스트 스위칭이 필요하기 때문에 이점이 전혀 없다고 한다.

 

https://youtu.be/gTkvX2Awj6g

마지막으로 세마포어 방법은 signal mechanism을 가진, 하나 이상의 프로세스/쓰레드가 임계 영역에 접근 가능하도록 하는 장치를 이야기한다. 뮤텍스 방법과의 차이점은, lock이 wait이 되었고, unlock이 signal이라는 이름으로 변경되었다. 그리고 lock을 1, 0으로 설정해주는 것이 아니라 1만큼 더하고 뺀다. 초기 value가 1이라면 동시에 1개만 접근할 수 있지만, 초기 value를 2, 3, 4 등등으로 설정하여 한 번에 초기 value값 만큼의 프로세스/쓰레드가 임계 영역에 접근할 수 있도록 설정할 수 있다. 그리고 세마포어 방법은 signal mechanism을 가졌기 때문에 순서를 정해줄 때 사용할 수 있다.

 

https://youtu.be/gTkvX2Awj6g

위 그림의 경우, task2가 끝난 이후에 wait()를 통해 task1이 끝난 이후 전송되는 signal()을 기다린다. 이 경우, 반드시 task1이 실행된 이후에 task3가 실행된다. task3의 작업을 하기 위해 task1이 실행되어야 하는 경우에 이렇게 구현할 수 있다는 것이다.

 

 

마지막으로, 세마포어 방식의 초기 값이 1인 경우에 뮤텍스와 동일하게 보일 수 있지만, 실제로는 차이가 있다고 한다. 먼저 세마포어 방식은 우선순위가 높은 스레드가 락을 해제할 수 있다. 반면에 뮤텍스는 락의 소유자만 락을 해제할 수 있기 때문에, 어떤 프로세스가 락을 가진 상태에서 컨텍스트 스위칭이 되어 우선순위가 높은 프로세스가 해당 락에 접근하게 되면, 우선순위가 낮은 프로세스에 묶이게 되는 문제가 생긴다. 이를 해결하기 위해서 우선순위가 높은 프로세스가 얻으려고 하는 락을 가진 프로세스의 우선순위를 접근하려는 프로세스와 동등하게 높여주는 방식을 사용한다고 한다. 따라서 뮤텍스는 priority inheritance 속성을 가진다고 한다.

 

 

 

이렇게 Race Condition에 대해서 자세히 알아보고 정리해보았다. 새로운 것에 대해서 알아가는 것이 힘들기도 하지만 또 한편으로는 재밌기도 하고, 또 나의 지식을 확장시키는 일이기 때문에 만족감도 든다. 다만 알아갈수록 내가 모르는 영역이 참 많구나 하는 것을 새삼 깨닫기도 한다. 그래도 오늘 하루 또 새로운 지식을 얻은 것에 대해서 만족한다.

이전에 보았던 Tucker님의 게임 네트워킹의 이해 영상을 마저 시청하였다. 오늘은 Socket Programming과 MMORPG 서버 구조 편까지 전부 시청하여 마무리 정리까지 해보려고 한다.

 

https://youtu.be/aWq9C7RARJI

 

 

1. Socket Programming

컴퓨터의 네트워킹은 NIC에서 이루어진다. NIC에서 데이터를 주고 받는 것을 제어할 수 있게 만들어주는 것을 Socket이라고 하고, 이 Socket을 이용하여 Programming을 하는 것을 Socket Programming이라고 한다. 자세히 보면 복잡하긴 하지만 궁극적으로 추구하는 것은 Input과 Output을 다루는 것이다. 이 I/O를 다룰 떄 중요한 기능 2가지가 Read와 Write이다. Read와 Write를 하는 방식은 크게 2가지가 있는데, 동기식과 비동기식이다. 동기식은 Read를 요청하면 완료될 때까지 대기하는 방식이다. C언어에서 scanf를 사용하면 입력을 받아올 수 있는데, 입력이 될 때까지 프로그램이 잠시 멈춘다. 입력이 들어오면 해당 지점에서 프로그램을 재개하는 식이다. 이 방식의 문제점은 커넥션이 1개 뿐이라면 기다려도 상관없지만, 커넥션이 많아진다면 여러 커넥션을 처리해야하는 서버의 입장상 하나의 커넥션을 위해 계속 대기하고 있는 것이 굉장히 비효율적이라는 것이다. 때문에 입력이 올 때까지 기다리는 것이 아니라, 요청을 보내놓고 다른 일을 처리하다가 입력이 들어오면 일을 처리하도록 해야 한다. 이것이 바로 비동기식이다. 비동기식에는 크게 2가지 방법이 있는데, Select와 WIndows의 IOCP이다. Select 방식은 Read와 Write를 할 수 있는지 지속적으로 확인을 하고, 사용할 수 있는 상태라면 직접 처리하는 방식이다. IOCP 방식은 이런 작업을 OS에게 요청하고 위임하는 방식이다. 요청을 보낼 때 OS에게 요청을 하고, 해당 요청에 대한 응답이 오면 OS가 해당 응답을 받아서 Queue에 쌓아놓는다. 작업을 진행하기 위해서 Queue를 확인하고, 응답이 왔다면 바로 처리할 수 있다. Select 방식은 구현하기 쉽다는 장점이 있고, IOCP 방식은 성능면에서 효율적이라는 장점이 있다. 두 방식 모두 많이 사용하지만, 커넥션 수가 많아지면 IOCP 방식을 사용하는 것이 좋다고 한다.

 

 

2. MMORPG 서버 구조

MMORPG 서버 구조에 대한 이야기는 싱글 쓰레드/멀티 쓰레드와 싱글 프로세스/멀티 프로세스에 대한 이야기가 전부인 것 같다. 그런데 이 이야기가 약간 복잡하다. 먼저 MMORPG 서버의 특징은 작업량이 많다는 것이다. 플레이어가 적게는 수십, 많게는 수천 명을 한꺼번에 컨트롤해야하고, NPC라든지, 흔히 바닥이라고 얘기하는 Control Zone 등 아주 많은 정보를 처리해야한다. 서버는 패킷 처리는 물론이고, 매 프레임마다 Actor(플레이어, NPC 등)들을 업데이트시키는 Tick작업을 해줘야하고, 그 외 경매장 등의 시스템도 처리해야한다. 게임을 10프레임으로 돌린다고 하면 초당 10번의 Tick작업을 해줘야하고, 그 말은 100ms의 시간 안에 모든 작업을 끝내야 한다는 것이다. 만약 5000명의 Actor를 처리해야 한다면 하나의 Actor당 처리시간이 0.02ms밖에 주어지지 않는다. 이것은 굉장히 어려운 일이기 때문에 이를 보완하기 위해 여러 방법들을 사용한다. 먼저 멀티 쓰레드를 활용하는 방식이다. 쓰레드 수를 4개로 늘린다면 1개당 100ms라는 시간이 주어지는 것이기 때문에, 4개의 쓰레드를 동시에 돌린다면 400ms의 시간이 주어지는 셈이다. 서버의 부담이 1/4로 줄어든다. 물론 멀티 쓰레드를 돌리면 신경써야 할 것들이 많아서 4개로 늘린다고 성능이 4배로 늘어나진 않겠지만, 잘 구현해놓면 싱글 쓰레드에 비해 부담이 훨씬 줄어들 것이다. 다만 문제가 있다면, 멀티 쓰레드는 방금 언급했다시피 신경써야 할 것들이 상당히 많아서 구조를 잘 짜놔야 한다는 것이다.

 

먼저 싱글 쓰레드는 Work Thread가 1개인 것을 이야기한다. 즉, 네트워크의 I/O를 처리하는 쓰레드는 따로 있고, 들어온 데이터들을 Queue에 계속 저장한다. Work Thread에서는 이것들을 활용해서 Tick 작업을 해주는 것이다. 단점이 있다면 잘 구현된 멀티 쓰레드에 비해서 처리 가능한 Actor 수가 적을 수 있다는 점이지만, 굉장히 Simple 구조를 가지고 있기 때문에 멀티 쓰레드 환경에서 일어나는 많은 문제들이 일어나지 않는다. 그렇기 때문에 싱글 쓰레드라고 해서 무조건 나쁜 것은 아니라고 한다. 이 싱글 쓰레드에 Dedicate Thread를 추가하여 DB나 경매장만 담당하는 Thread를 추가하는 방법이 있다. 이 방법은 싱글 쓰레드의 Simple함을 유지하면서 싱글 쓰레드의 부담을 줄여줄 수 있다는 장점이 있지만, 여전히 처리 가능한 Actor 수의 한계가 있다.

멀티 쓰레드는 Work Thread가 여러 개인 것을 이야기한다. 싱글 쓰레드와 마찬가지로 네트워크의 I/O를 처리하는 쓰레드는 따로 있고, 저장된 Queue에서 데이터를 가져와 Tick 작업을 여러 쓰레드에서 하는 것이다. 멀티 쓰레드에서는 많은 문제들이 발생할 수 있는데, 한 쓰레드에서 공유 자원에 접근할 때, 다른 쓰레드에서 접근하지 못하도록 Locking을 걸어줘야 한다. 가령 hp가 50인 상태에서 동시에 두 적에게 10데미지, 20데미지를 받는다고 치자. 그러면 체력이 20이 되어야겠지만, hp가 50인 상태를 동시에 가져가버려서 한 쓰레드는 10데미지를 입혀 체력을 40으로 갱신하고, 다른 쓰레드는 현재 hp를 50으로 받았기 때문에 20 데미지를 입혀서 체력이 30이 된 것을 거기에 덮어씌운다면 정상적으로 처리되지 않을 것이다. 때문에 다른 쓰레드에서 접근하지 못하도록 Locking을 해줘야한다. 그런데 MMORPG는 Actor간에 Interaction이 굉장히 빈번하게 일어나기 때문에 이 문제를 어떻게 다루느냐가 굉장히 중요한 문제이다. 여기서 구역을 나누는, 구획화를 어떻게 하느냐가 중요하다.

 

이를 쉽게 해결할 수 있는 방법이 멀티 프로세스이다. 싱글 쓰레드, 멀티 쓰레드는 각각 하나의 프로세스 안에 존재하는 쓰레드의 수를 이야기한다. 즉, 프로세스를 여러 개 돌리고 각각의 프로세스를 싱글 쓰레드로 돌리면 멀티 쓰레드와 같이 성능을 향상시킬 수 있고, 각각의 프로세스는 자원을 공유하지 않기 때문에 멀티 쓰레드에서 일어나는 문제들도 발생하지 않게 된다. 다만, 이 경우 문제가 있는데, 각 프로세스에서 처리하는 정보들에 바로 접근할 수 없기 때문에, A프로세스에 존재하는 유저는 B프로세스에 존재하는 유저를 공격할 수 없다는 문제가 있다. 이것은 대륙을 나눠서 각각의 대륙을 하나의 프로세스가 처리하게 하고, 유저가 넘어갈 때 로딩을 적용해서 다른 프로세스로 넘겨주는 방식을 사용하여 해결할 수 있다. 그게 아니라 만약 하나의 대륙에서 구획을 나누는 방식을 사용한다면 구획과 구획이 만나는 가장자리에 서있는 유저들을 처리하기 위해서 각종 눈속임을 써야한다는 문제가 있다. 때문에 로딩 등 끊김이 없는 Seamless 월드를 만들 계획이 있다면 멀티 쓰레드를 활용하는 것이 좋다고 한다.

 

싱글 프로세스에서는 기기 성능을 향상시키기 위해서는 좋은 기기를 한 대 구입해야한다. 이를 Scale Up이라고 한다. 그러나 기기의 성능이 점점 좋아질수록 성능이 조금만 좋아져도 가격이 기하급수적으로 올라가고, 머신의 성능과 처리 성능이 비례하여 증가하지 않기 때문에 이 방법에는 한계가 있다. 아마 처리하는데 고정적으로 들어가는 비용들이 있다보니 머신 성능과 처리 성능이 비례하지 않는 것 같다. 보통 머신 성능이 좋아질수록 처리 성능의 증가폭은 줄어든다고 한다. 반면에 멀티 프로세스는 적당한 성능과 가격의 기기를 여러 대 들여놓는 Scale Out 방식을 사용하면 되는 것이기 때문에, 싱글 프로세스에 비해 비용 부담이 상당히 적다. 머신은 싸다 라는 말이 있다는데, 비싼 돈을 주고 코딩을 잘 하는 프로그래머를 고용하는 것 보다, 머신의 수를 늘리고 싼 프로그래머를 기용하는 것이 더 효율적이라는 얘기가 있다고도 한다. 멀티 프로세스를 구현하기 위해서 실제로 머신을 구입할 필요는 없고, AWS와 같이 클라우드를 통해 비용을 지불하고 머신의 성능을 가져다가 쓰는 방법이 있다고 한다. 또한 가용성이라는 것이 있는데, 가령 싱글 프로세스로 서버가 돌아갈 때 이 프로세스가 죽으면 해당 서버는 완전히 다운되어버린다. 멀티 프로세스로 서버를 돌린다면 하나의 프로세스가 죽어도 나머지 프로세스로 서버가 돌아갈 수 있다는 것이다. 구현 내용에 따라 속도가 느려지거나 일부 지역이 봉쇄될 수는 있겠지만, 전체 서버의 다운은 막을 수 있따는 것이다. 이것이 가용성이 높다고 한다. 즉 싱글 프로세스는 가용성도 낮고 비용도 높지만, 멀티 프로세스는 가용성도 좋고 비용도 적다. 때문에 Scale Up 방식에서 Scale Out 방식으로 변화하고 있다고 한다. 물론 싱글 프로세스의 장점도 있는데, 구조가 단순하고, 위에서 언급한 Seamless 월드, 즉 하나의 통합된 월드를 만들 수 있다. 더욱이, 버전업을 할 때 각 프로세스에 배포를 해야하는 멀티 프로세스와는 달리 프로세스 하나만 버전업을 하면 되고, 수십~수백 개의 프로세스를 사람이 직접 관리하기는 힘든 멀티 프로세스와 달리 싱글 프로세스는 사람이 관리할 수 있을 정도의 규모가 될 수 있다는 것이다. 멀티 프로세스는 이렇게 많은 프로세스를 가진 서버가 여러 개 있을 수 있기 때문에 사람이 직접 관리하기는 힘들고, 이를 자동으로 관리해줘야한다고 한다. 이를 위한 툴들이 Jenkins, Ansible, Chef 등 아주 많다고 한다. 이런 것들을 전문으로 하는 DevOps라는 직업군까지 생겼다고 할 정도이다. 하지만 이렇게까지 해서라도 가용성과 비용적인 측면 때문에 멀티 프로세스를 많이 사용한다고 하고, 마찬가지로 이런 문제 때문에 싱글 프로세스도 완전히 안 좋은 방법은 아니라는 것이다. 멀티 프로세스를 사용하는 방법은 아까 말했듯이 구역을 나누는 Zone 방법이 있고, 하나의 구역에 채널을 여러 개 두는 Channel 방법이 있다고 한다. 또, 두 가지 방법을 모두 사용할 수 있다. 이 경우 프로세스들이 너무 많아져서 이 프로세스들을 관리하는 서버가 따로 필요할 수도 있고, 배포나 빌드, 이슈를 모니터링하여 판단하고 복구하는 작업 등 많은 문제들이 발생한다고 한다. 이런 것들을 잘 컨트롤해야 좋은 멀티 프로세스 서버를 만들 수 있다고 한다.

추가로, 멀티 프로세스에서는 각각의 프로세스에 싱글 쓰레드를 쓸 수도 있고, 멀티 쓰레드를 쓸 수도 있지만, 싱글 프로세스 방식에서는 아무래도 성능 상의 문제 때문에 멀티 쓰레드를 사용할 수 밖에 없다고 한다.

 

이제 마지막으로 멀티 쓰레드에 대한 얘기이다. 멀티 쓰레드를 사용할 때, 코어 수가 늘어날수록 더 많은 쓰레드들이 자원을 점유하려고 하기 때문에 성능 증가 폭이 상당히 줄어들고, 코딩 하기에 따라 오히려 성능이 줄어들 수도 있다. 떄문에 싱글 쓰레드도 현명한 선택일 수 있다. 그럼에도 불구하고 멀티 쓰레드를 사용하려면 이런 문제로 인해 공유 자원에 대한 접근을 잘 관리할 필요가 있다. 만약 무식하게 구현하여 자원에 마구 접근하고, 접근할 때마다 마구 Lock을 건다면 성능도 떨어지고, 무분별한 접근과 Lock으로 인해 대기 시간도 늘어나고, Deadlock이 발생할 가능성도 높아진다. 또, Race Condition이라는, 어떤 쓰레드가 먼저 접근하는지에 따라 다른 문제가 발생하여 Timing 문제가 발생할 수도 있다. 때문에 공유 자원을 어떻게 분배할 것이냐에 대한 기준을 세워야 한다. 첫 번째로는 Actor를 기준으로 나눌 수 있다. AKKA 방식이라고 하는데, Actor를 기준으로 쓰레드를 나눈다. 때문에 각각의 Actor가 할 일은 해당 Actor를 가진 쓰레드가 처리하면 되는 것이다. 상호작용을 할 때는, 다른 Actor에게 메세지를 보내고, 해당 Actor가 그에 대해 응답을 한다. 즉, 직접적으로 자원에 접근하는 것이 아니라 공유 자원에 대한 여러 문제가 발생하지 않는다. 하지만 메세지를 보내고 그에 대한 응답이 와야 처리를 할 수 있기 때문에 비동기식이라는 문제점이 있다. 또한, A가 B를 공격하기 위해 데이터를 받아오고 처리하는 도중에 C가 B를 공격하기 위해 데이터를 받아온다면, A의 공격이 B에게 적중하기 전의 데이터가 들어갈 것이기 때문에 Timing 관련 이슈가 생긴다는 것이다. 마찬가지로 Lock을 걸면 이런 문제가 해결되겠지만, Lock을 걸면 성능이 저하되고, Deadlock이 발생할 수도 있고 많은 문제들이 발생한다. 메세지를 사용한다면 복잡도가 올라가고 비동기식이 된다는 문제점이 있다. 결국 모든 것은 일장일단인 것이다. Trade Off라고도 하는데, 어떤 것의 장점을 취하려고 한다면 그에 대한 단점도 따라오는 것이고, 어떤 것의 단점을 없애기 위해 장점을 포기해야할 수도 있는 것이다. 안정성을 올리면 성능이 떨어지는 것이 대표 예라고 할 수 있겠다. ECS 방식도 있는데, Entity, Component, System 방식이라고 한다. Entity는 Conponent를 갖고 있는 객체이고, Component는 데이터만을 가지고 있다. 그리고 System이 각 Entity가 가진 Component의 데이터를 사용해서 기능을 처리하는 식으로 나누는 것이다. 이 경우 객체에 어떤 Component를 붙이느냐에 따라서 플레이어, NPC, 몬스터, 자유자재로 만들 수 있기 떄문에 확장성이 좋아지고, 데이터의 Locality가 좋아진다고 한다. 데이터의 Locality란, 같은 데이터가 얼마나 같은 공간에 밀집해있느냐이다. 가령 Render System이 화면을 그릴 때, 화면을 그리는 것과 관련된 Component들을 전부 반복문을 돌려 한꺼번에 처리하면 같은 공간에 데이터들이 모여있게 될 것이다. 캐시에 데이터를 올릴 때, 일정 영역의 데이터를 한꺼번에 올리기 때문에 이렇게 데이터의 Locality가 좋아지면 성능이 향상된다는 것이다. 특히 게임에서는 화면을 그리는 것이 성능의 90%를 차지할 정도로 많은 성능을 요구하기 때문에, 이런 성능의 향상이 크게 작용할 수 있다. 다만, 문제가 있다면 하나의 시스템이 하나의 Component만 가지고 있는 것이 아니라, Component를 공유할 수 있기 때문에 마찬가지로 점유의 문제가 생길 수도 있고, 다른 시스템에게 물어봐야 하는 상황이 올 수 있기 때문에 AKKA 방식과 마찬가지로 메세지로 처리해야할 수도 있다. 또한 ECS 방식이 만들어진지 오래 되지 않아서 아직까지 많이 사용되지 않고 정보도 그렇게 많지는 않은 것 같다.

 

 

 

드디어 정리가 끝난 것 같다. 이로써 게임 네트워킹의 이해를 전부 시청하고 정리하게 되었다. 내 기억에 의존하거나, 기억이 안 나는 부분은 다시 듣기도 하고, 내 방식대로 풀어서 설명했기 때문에 잘못된 부분이 있을 수 있다. 그래도 들으면서 이렇게 글로 나름대로 풀어서 쓰는 것이 이해에 상당히 도움이 된 것 같다. 코드를 짜거나 본 것도 아니고, 아마 많은 부분 중에서도 빙산의 일각, 하물며 겉핥기 식일 테지만 이런 기초적인 개념이라도 있는 것과 이것조차 없는 것은 굉장히 큰 차이라고 생각하기 때문에 나로서는 굉장히 만족스러운 시간이었다. 또 많은 키워드를 얻은 것 같다. 당장에는 필요없을지 모르겠지만, 어떤 형태로든 향후에 도움이 될 것이라고 생각한다. 아무튼 이로써 게임 네트워킹의 이해 정리를 마무리하겠다.

지난번에 이어서 Tucker님의 게임 네트워킹 이해를 시청한 내용을 정리해보겠다.

 

https://youtu.be/CfzYq3GTlX8

 

1. P2P, Relay Server, 홀펀칭

 

영상에서 먼저 P2P에 대해 설명하기 전에 인터넷의 유래에 대한 설명을 먼저 한다.

인터넷이 군사목적으로 탄생했다는 것은 알고 있었지만, 조금 더 재밌는 내용을 알게 되었다. 요약하자면, 미국과 소련의 냉전기에 서로에게 미사일 혹은 핵 타격을 안정적으로 수행하기 위해 인터넷 통신망이 구축되었다는 것이다. 만약 미사일 발사 회선이 하나로만 이루어져있으면 적국의 스파이가 해당 회선을 끊어버리고 선제 타격을 하는 경우 대응을 못하지만, 회선을 복잡하게 구축해서 하나의 회선에 문제가 생겨도 다른 회선을 통해 접근할 수 있다면 대응 사격을 할 수 있을 것이다. 따라서 선제 공격을 하면 대응 사격을 받을 것이므로, 서로가 서로를 견제하며 공격하지 못하는 상황이 되는 것이다. 이 이야기를 왜 하셨을까 생각해봤는데, 이 직후에 IPv4 이야기가 등장한다. 아마 당시에는 군사 목적으로 쓰였기 때문에 많은 IP주소가 필요하지 않았다는 이야기를 하려고 하신 것으로 추정된다.

아무튼, IPv4는 약 43억 개의 IP주소를 할당할 수 있고, 현대에 와서는 컴퓨터가 너무 많아져서 43억개로는 부족하여 IPv6로 넘어간다고 배웠는데, 마냥 그렇지만은 않은 것 같다. 최근에는 게이트웨이와 같은 큰 단위의 기기에만 고정 IP를 할당하고, 그 밑에 있는 기기에는 private한 가상의 IP주소를 부여한다는 것이다. 이렇게 하면 IP주소의 여유분도 확보할 수 있고, 내부 컴퓨터를 외부로부터 숨겨주는, 보안의 기능도 할 수 있다고 한다. 다만, 이 경우 문제가 생기는데 내부에 있는 기기가 가상의 IP 주소를 가지고 있기 때문에 외부에서 IP 주소로 해당 컴퓨터 연결할 수 없다는 것이다.

그러면 외부에 있는 컴퓨터가 게이트웨이 속의 숨겨진 컴퓨터에 어떻게 연결을 할 수 있을까? 그것은 바로 숨겨진 컴퓨터가 먼저 외부의 고정 IP를 가진 컴퓨터에 패킷을 보내는 것이다. 이 경우, 중간에 있는 게이트웨이와 같은 NAT 장비가 테이블을 만들어서 내 private IP주소와 목적지 IP 주소를 저장한다. 그리고 이것을 포트번호로 기억한다. 이것을 포트포워딩이라고 한다. 그러면 외부에 내가 속한 게이트웨이의 고정 IP주소와 포트 번호를 전달하기 때문에, 해당 게이트웨이에 포트 번호와 함께 전송을 보내면 게이트웨이가 포트 번호를 참조하여 private IP주소로 전달을 해주는 것이다. 그리고 포트포워딩은 여러 차례 될 수 있다. 게이트웨이가 라우터를 포트포워딩하고, 라우터가 공유기를 포트포워딩하고, 공유기가 우리들이 쓰는 일반적인 컴퓨터를 포트포워딩하는 것이다.

그래서 P2P란 무엇인가 하면, Peer To Peer의 줄임말로, 클라이언트와 클라이언트가 직접 연결하는 방식을 말한다. 이 방식의 문제는, 위에서 설명했듯 고정 IP주소가 없는 클라이언트에게 먼저 패킷을 보내는 것이 불가능하다는 것이다. 때문에 두 클라이언트가 STUN 서버라고 하는 고정 IP주소를 가진 서버에 패킷을 보낸다. 그러면 STUN 서버는 각각이 속한 게이트웨이의 public IP주소와 포트 번호를 상대방에게 알려주는 것이다. 그러면 서로 연결할 수 있게 되는 것이다. 이것을 홀펀칭이라고 한다. 그런데 여기서 또 문제가, 게이트웨이가 포트포워딩을 할 때, 출발지는 물론이고 목적지도 저장한다는 것이다. 즉, 해당 포트 번호는 STUN서버가 목적지이기 때문에 다른 곳에서 보낸 패킷을 전달하지 않을 수 있다는 것이다. 이것은 순전히 게이트웨이 마음이라고 한다. Relay 서버는 TURN 서버라고도 하는 것 같은데, 간단하게 서버가 플레이어 간 보낸 패킷을 서로에게 전달해주면 되기 때문에 이런 문제가 발생하지 않는다. 그래서 먼저 STUN 서버를 통해 홀펀칭을 시도해보고, 실패하면 TURN 서버를 사용한다고 한다. 홀펀칭 또한 UDP를 기반으로 한 기술인데, Reliable UDP와 마찬가지로 옛날에는 일부 큰 기업에서만 사용하던 비밀 기술이었으나, 현재는 널리 퍼진 기술이라는 것 같다.

 

 

2. Deterministic과 게임핵

 

Deterministic 방식은 이전에 정리했듯 같은 입력이 들어오면 같은 처리를 보장해주는 것이다. 즉, 입력에 강하게 의존한다는 것이다. 영상에서는 예시로 FPS를 들었는데, 내 입장에서는 잘 이해가 되지 않아서 내가 들으면서 생각했던 격투 게임으로 예를 들어보겠다. 영상에서 얘기한 것과 거의 부합하는 내용인 것 같은데, 예시가 틀릴 수도 있다. 철권같은 격투 게임의 경우 적에게 공격을 당하면 피격 모션을 재생하며 공중에 붕 뜨거나 날아간다. 이 도중에 상대방을 공격할 수 없다. 아마 이 도중에 공격 버튼을 누르면 내부 검사를 통해 입력이 무시되어 상대방에게 전달되지 않겠지만, 내부의 검사 코드를 조작하여 피격 중인데도 불구하고 공격 버튼 입력이 상대에게 전달됐다고 해보자. 아마 상대는 신나게 콤보를 때려넣다가 갑자기 맞는 상황이 연출될 것이다. 때문에 이런 입력을 검증하는 작업이 필요하다.

P2P 방식으로 연결된다면, 상대 클라이언트에서 해당 입력을 검증한다. 이 경우, 클라이언트는 게임의 Rule과 현재 상태 등을 가지고 있기 때문에 상대방이 공격할 수 없는 상황이라는 것을 알고 있고, 해당 입력이 부정한 입력이라는 것을 캐치해낼 수 있다.

Relay 서버를 통하는 방법을 사용한다면, 중계 서버에서 부정을 감지하기 위해서는 게임의 Rule과 현재 상태를 가지고 있어야 한다. 단, 이 경우에는 하나의 중계 서버가 여러 개의 게임을 서비스할 수 없다는 단점이 있다. 그렇기 때문에 하나의 중계 서버를 통해 여러 개의 게임을 서비스하기 위해 중계 서버는 패킷을 서로에게 전달해주는 역할만을 하는 것이 좋다는 것 같다.

어떤 방식을 이용하든 검증하는 방법이 있을텐데, 이 검증하는 방법을 악용할 수도 있다는 모양이다. 가령 플레이어가 1초에 10m를 움직일 수 있다고 하면, 오차 범위로 12~13m까지는 허용되게끔 한다는 것이다. 10m로 칼같이 잡아버리면 작은 오류로 아주 작은 오차가 발생해도 핵으로 인식할 수 있기 때문이다. 때문에 핵은 이런 것을 역이용해서 걸리지 않을 정도로만 빠르게 움직이게 할 수도 있다는 것이다.

또, 해킹에는 외부툴을 이용해서 해킹을 하는 외부 해킹이 있고, 내부의 코드를 조작하는 내부 해킹이 있다고 한다. 외부 해킹은 외부 툴을 막는 nProtect 등의 프로그램 등을 사용해서 해킹 프로그램을 실행하지 못하게 하는 방법이 있다. 내부 해킹의 경우, exe파일이 게임의 코드를 기계어로 번역한 것이기 때문에, 이 기계어를 역어셈블리에 넣고 복원하여 코드를 변조시키는 방법을 사용한다고 한다. 이를 막기 위해서 파일 사이즈를 검증하기도 하고, 코드 전체를 Hash코드화해서 Hash코드가 변경되지 않았는지 확인하는 방법도 있다. 또, 코드를 암호화하여 실행할 때 복호화하는 방식도 있다고 한다. 어찌되었든, 해킹의 방어가 강해질수록 해커의 공격도 강해지고, 결국 해커에게 선공권이 있기 때문에 핵이 뚫리는 것은 어쩔 수 없는 일이라는 것 같다. 단, 뚫릴 때마다 방어를 강화하여 겹겹이 씌워 해커를 귀찮게 만든다고 한다.

 

Server Authority 방식에 대한 영상도 보았는데, 내용이 조금 적고 다음 내용과 연계되는 내용인 것으로 보여 다음에 정리할 때 같이 정리해야겠다.

오늘은 이전에 3D RPG게임을 apk로 빌드해서 테스트해본 것처럼 이전에 개발하던 TSCB를 apk로 빌드해보았다.

 

private void Update()
        {
            if (!_isAvailableMove) return;

            _moveSpeed = 400f * Time.deltaTime;

            float inputX = Input.GetAxis("Horizontal");
            float inputY = Input.GetAxis("Vertical");

            Vector2 moveDirection = new Vector2(inputX, inputY).normalized;

            if (_joystickInputX != 0 && _joystickInputY != 0) moveDirection = new Vector2(_joystickInputX, _joystickInputY);

            if (moveDirection.x > 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.x >= 350) moveDirection.x = 0;
            else if (moveDirection.x < 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.x < -350) moveDirection.x = 0;

            if (moveDirection.y > 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.y >= 500) moveDirection.y = 0;
            else if (moveDirection.y < 0 && _midoriPlane.GetComponent<RectTransform>().anchoredPosition.y <= -500) moveDirection.y = 0;

            moveDirection = moveDirection.normalized;

            _midoriPlane.GetComponent<RectTransform>().anchoredPosition += moveDirection * _moveSpeed;

            // Z키를 눌러 총알을 발사
            if (Input.GetKey(KeyCode.Z))
            {
                OnClickAttackButton();
            }
            if (Input.GetKeyDown(KeyCode.X))
            {
                OnClickSkillButton();
            }
        }

그러기위해 필요한 작업이 있었는데, 모바일에서 터치로 조작할 수 있도록 조작 기능을 추가하였다.

이를 위해서 3D RPG 개발에 사용했던 조이스틱 기능을 가져왔다. 또, 이전에 이동이 normalized 되어있지 않아 대각선으로 이동할 때 이동속도가 더 빠른 문제가 있었다. 때문에 normalized 작업을 해주었다.

 

공격과 스킬을 사용할 때 버튼을 눌러서 입력할 수 있도록 공격과 스킬 기능을 함수로 분리하여 키를 누르거나 버튼을 눌렀을 떄 작동하도록 하였다.

 

그리고 이번에도 화면 비율 관련 이슈가 있었는데, Canvas Scaler를 조절해주고, 배경이 비는 부분 등의 가로 크기를 늘려줘서 화면에서 위화감이 나타나지 않도록 수정했다.

이전에 사용하던 안드로이드 기기를 이용하여 테스트한 화면이다. 가로 비율이 조금 넓어서 배경을 늘려주는 작업이 필요했다. 전체적으로 기능이 동일하게 작동했는데, 슈팅게임의 조작 부분이 살짝 아쉬웠다. 조이스틱 크기가 너무 작아서 조작이 약간 불편했고, 공격의 경우 버튼을 계속 터치해야하는 불편함이 있었다. 때문에 내일 조이스틱 크기를 키우고 공격 버튼을 누르고 있으면 입력을 유지할 수 있도록 수정할 계획이다.

+ Recent posts