Skip to content

게임 클라이언트(Unity) 개발자 기술면접 준비에 도움이 되고자 작성한 글입니다.

License

Notifications You must be signed in to change notification settings

salt26/game-developer-interview

Repository files navigation

게임 클라이언트(Unity) 개발자 기술면접 대비

최초 작성일: 2024년 7월
최종 편집일: 2024년 12월

작성 기여자

저작권

본 자료를 자유롭게 배포하셔도 좋습니다.
배포 시에는 작성 기여자를 명시해 주세요.
단, 상업적 용도로는 사용하실 수 없습니다.

기여 규칙

본 자료의 2부 내용에는 오류가 있을 수 있습니다.
오류 수정 제안이나 내용 추가는 언제든지 환영입니다!
Pull Request를 날려주시면 검토 후 반영하겠습니다. 😊

목차

1부: 팁과 개인적 조언

2부: 공부 자료


큰 그림

일반적인 입사 절차

회사마다 다를 수 있으므로, 들어가려는 회사의 채용 사이트를 꼭 확인해 보시기 바랍니다.

  1. 서류 심사 (이력서, 자기소개서, 포트폴리오 등)
  2. 코딩 테스트 또는 과제 전형
  3. 실무진 기술면접
  4. 경영진 인성면접
  5. 처우 협의

기술면접 과정

보통 1시간 정도 소요됩니다.

  1. 자기소개 + 아이스 브레이킹
  2. 제출한 서류의 내용 검증 + 경험 질문 + 과제에 대한 질문
  3. 기술 질문
  4. 회사에 대해 궁금한 점 (역질문)

개인적 감상

Note

본 장의 내용은 어디까지나 제 경험에서 비롯한 "개인적 감상"이며, 주관적인 내용이 다수 포함되어 있습니다.
이는 모든 면접에 일반화되는 내용은 아닐 수 있습니다.
저의 경우 대학원 졸업 후 전문연구요원으로 지원하였으므로, 경력자나 학부 졸업자(신입)를 뽑는 면접과 상이할 수 있습니다.
참고만 하시기 바랍니다.

과제 또는 구현 테스트

알고리즘 문제를 푸는 코딩 테스트와는 다릅니다.
회사에 따라 다르지만, Unity를 사용해 짧은 시간(2시간 ~ 1주일) 동안 처음부터 게임을 개발할 것을 요구하는 전형에 대한 감상입니다.

  • 내공이 없으면 통과할 수 없다.

    • 문제 분석 및 이해, 코드 구조 설계, Unity C# 스크립팅, 알고리즘, 사용자 입력 처리, 시각화(애니메이션) 등을 종합적으로 요구한다.
    • 허수인 지원자를 가장 확실하게 거르는 전형이다. 알고보니 나도 허수였지만...
    • 편법으로 회사에서 내는 과제의 정보를 미리 입수하고 연습해 볼 수도 있겠지만, 이렇게 하면 합격에는 도움이 될지 몰라도 회사 가서 적응하기가 힘들 것이다.
  • 단순 구현 문제처럼 보여도 알고리즘 문제 풀이 기술을 요하도록 설계되어 있다.

    • 다행인 점은, 프로그램 구현을 충분히 많이 해본 사람이라면 따로 알고리즘 공부를 해서 갈 필요는 없다.
  • 필수 스펙을 시간 안에 전부 구현하는 사람도 매우 드물 것 같은데 선택 스펙도 굉장히 많이 달려있다.

    • 이런 점이 '누군가는 이걸 다 해내는데, 당신은 과연 다 할 수 있을까요?' 하는 무언의 압박을 준다.
  • 라이브 구현 테스트의 경우 시간이 매우 부족하므로 문제를 보고 첫 번째로 떠올린 설계로 끝까지 가야 한다.

    • 설계하는 데에 시간을 쏟다가는 제 시간 안에 구현을 다 못 한다.
    • 첫 설계가 괜찮은 설계이려면 정말 수도 없이 많은 프로젝트를 바닥부터 짜 봤어야 한다.
    • 인터넷 검색이 가능하더라도 검색할 시간이 아깝다. 손에 익은 구현 기술만으로 자급자족할 수 있어야 한다.
  • 짧은 시간 안에 게임 하나를 바닥부터 개발해서 혼자 완성하는 연습을 많이 해봐야 한다.

기술면접

  • 기술면접은 지원자의 내공을 측정하는 면접이 아니다.

    • 단기적인 단순 암기로 넘길 수 있는 면접이다.
    • 즉, 지원자가 우리 회사에 들어오기 위해서 '최근에' 공부를 하는 열의를 보였는가를 평가하는 자리이다.
    • 학교에서 가르쳐 준 적 없는 내용이 대부분이므로, "개발 많이 해 봤는데 나 정도면 충분히 붙겠지~" 하고 아무 준비 없이 가면 아무 대답도 하지 못할 것이다.
  • 단순한 지식을 묻는 질문보다, 본인이 직접 사용해 보고 경험해 보았는지 묻는 질문이 많이 나온다.

    • 아래 자료를 공부하면서, 시간을 들여 실습을 함께 진행할 것을 강력히 추천한다.
    • 외우기만 한 것과 써본 경험이 있는 것은 답변에서 그 차이가 드러난다.
  • 기술면접에서도 인성면접에서 할 법한 질문이 많이 나온다.

    • 자신이 쓴 서류의 내용에 대해 완벽히 이해하고 설명할 수 있어야 한다.
      • 자신이 했다고 서류에 적은 내용 중 어느 하나라도 기억나지 않는 것이 있다면 장담건대 100% 떨어진다.
        설명할 수 없는 경험은 서류에서 과감히 빼야 한다.
    • 회사의 인재상에 대해 숙지하고 가야 한다.
    • 평소에 "왜?"에 대한 질문을 많이 던져봐야 한다.
      • "왜 대학원 안 가고(또는 그만두고) 회사로 왔나요?"
      • "다른 개발 직군 많은데 왜 게임 업계로 왔나요?"
      • "왜 기획자도 서버 프로그래머도 아닌 클라이언트 프로그래머로 지원했나요?"
      • "왜 우리 회사여야 하나요?"
      • "회사에서 가장 배우고 싶은 것이 무엇인가요?"
      • "팀원과 갈등을 겪었던 경험이 있나요?"
      • "기획자가 정말 말도 안 되는 것을 꼭 구현해야 한다고 주장하고 이를 굽히지 않을 때, 프로그래머로서 어떻게 대처할 것인가요?"
  • 편하게 진행한다고 하지만 지원자 입장에서 암묵적인 압박을 받는다는 느낌이 들 수 있다.

    • 지원자 스스로 굉장히 자랑스럽게 여기고 잘 했다고 생각하는 것에 대해 "이런 부분이 아쉬운데 더 잘 할 수는 없었을까요?" 라고 묻는다.
  • 뽑는 입장이 한 번이라도 되어본 적이 있다면, 회사에서 필요한 사람이 바로 자신임을 어필하는 것이 중요하다는 것을 알 수 있다.

    • 모든 것은 직무 적합성으로 통한다.
      • 내가 이 직무에 있어서 뛰어난 사람이라는 것을 마음껏 자랑하자.
      • 회사마다 다르지만, 신입을 뽑을 때에는 지원자가 새로운 것을 학습하는 속도를 현재 가진 기술 및 지식의 수준보다 더 중요하게 본다.
      • 소통, 협업 능력 및 사회성도 중요하다.
    • 내가 하고 싶은 이야기보다 면접관이 듣고 싶어하는 이야기를 하는 것이 좋다.
      • 예: 피아노를 화려하게 정말 잘 치는데 지휘자를 보지 않는 반주자는 팀에서 원하는 반주자가 아닐 것이다.
      • 예: "전 나중에 창업을 하고 싶습니다!"라고 말한다면 면접관은 '금방 나갈 사람이구나' 하고 생각한다.
      • 다른 지원자에게서 들을 수 없는 자신만의 흥미로운 이야기가 있으면 좋다.
      • 돈에 대한 이야기는 면접이 아닌 처우 협의 단계에서 하는 것이 좋다.
    • 면접관을 미소 짓게 만들 줄 아는 호감형 사람들이 더 유리할 것이다.
      • 확실한 것은, 같이 있을 때 기분이 나빠지는 사람은 동료들의 생산성을 저하시키므로 뽑지 않을 것이다.
    • 회사를 칭송하는 아부를 많이 할 필요는 없다.
      • 이것이 지원자를 뽑아야 할 이유가 되지는 않는다.
    • 거짓말은 하면 안 된다.
      • 예: 활동적인 일을 좋아하지 않는데 "활동적인 일은 저에게 맡겨 주십시오!"라고 말하고 면접을 통과했다면 회사 가서도 원하지 않는 활동적인 일만 계속 맡게 될 것이다.
      • 면접에서 거짓말을 하는 사람은 언제든 거짓말을 할 수 있는 사람이라는 인식을 갖게 한다.
  • '나 같은 인재를 안 뽑으면 회사가 손해'라는 마음가짐으로 면접에 임하면 두려울 것이 없다.

    • 자신감 있는 모습을 보여줄 수 있고, 떨어져도 상처를 덜 받는다.
    • 그렇다고 이 말을 면접관에게 직접 하면 안 된다.
    • 그리고 준비를 하나도 안 하고 가도 안 된다.
  • 면접은 회사가 지원자를 평가하는 자리이기도 하지만, 지원자가 회사를 평가하는 자리이기도 하다.

    • 특히 실무진 면접의 면접관들은 입사하고 나면 동료가 될 사람들이다.
    • 면접 경험이 불쾌한 회사는 근무 경험도 불쾌할 가능성이 높다.
  • 내가 이 회사와 잘 맞을지 끊임없이 고민해야 한다.

    • 내가 포기할 수 있는 것과 포기할 수 없는 것이 무엇인지 알아야 한다.
      • 다른 직무로 발령되거나 전환배치가 일어나도 괜찮은가?
        예: 쿠버네티스 다룰 줄 안다고 했다가 DevOps로 납치되거나, 그래픽스 공부한 적 있다고 했다가 OpenGL 프로그래머로 납치되는 경우를 본 적이 있다.
      • 지방에 있는 스튜디오로 근무지가 옮겨져도 괜찮은가?
      • 회식 등 사내 친목 모임이 자주 열려도(또는 전혀 열리지 않아도) 괜찮은가?
      • 월급을 올려주는 대신 정말 하기 싫고 지루한 일을 하라고 하면 묵묵히 할 수 있는 사람인가?
    • 회사에 대해 궁금한 점을 물어보라고 하면, 내가 포기할 수 없는 것을 회사가 보장해 주는지 물어보면 좋다.
    • 개인적인 생각으로, 취업 과정은 있는 그대로의 내가 가장 빛날 수 있는 회사를 찾는 과정이다.
      • 면접관들이 내 능력을 온전히 알아봐 주려 하지 않는다면, 가서도 좋은 대우를 받기 어렵다.
      • 나와 잘 맞는 회사에서는 동료들이 내 능력을 인정해 주고 나를 필요로 하며, 나도 더 멋진 모습을 보여주고 싶어진다.
        이것이 불가능하다고 여기는 사람들도 있지만, 실제로 이런 회사에 다니고 있는 사람이 여기에 있다. 여러분에게도 일어날 수 있는 일이다!
    • 내 능력이 충분한데도 면접에서 떨어졌다면, 회사에서 당장 필요한 사람이 아니거나 서로 fit이 안 맞아서 그럴 수 있다.
      • Fit이 안 맞았다면 붙어서 갔어도 크게 고생하다가 언젠가 퇴사할 것이다.
        차라리 안 붙은 것을 다행으로 생각하는 편이 마음 편하다.
  • 취업은 운과 타이밍이 중요하다.

    • 회사 상황에 따라 지원자마다 다른 질문을 던지기도 한다.
    • 지원자를 걸러야 하는 상황이면 일부러 직무와 관련이 적은 세세한 내용을 질문하기도 한다.
      • 예: 한 채용 공고에서 필요한 인원을 모두 선발했는데 아직 전형을 진행하고 있는 지원자가 있을 경우
      • 이 경우 굉장히 어려운 질문을 받을 수 있고, 이 때문에 떨어지면 지원자가 실력이 없는 것이 아닌데도 공부를 덜 해서 떨어진 것이라고 스스로 믿게 될 수 있다.
      • 보통은 실력 부족이 원인이 아니고 지원하는 타이밍이 안 좋았던 것이다. 좌절할 필요 없다.
    • 지원자를 뽑고 싶은 상황이면 어느 면접에서나 나오는 질문들을 물어보는 편이다.
  • 퀴즈: 모르는 질문이 나왔을 때에는 어떻게 대답할 것인가?

    가. 아는 것처럼 최대한 둘러대며 그럴듯한 답을 이야기한다. (클릭하면 펼쳐집니다.)

    많은 지원자들이 최선을 다하는 태도를 보여주고자 이렇게 대답한다.
    그러나 면접관 입장에서는 가장 뽑고 싶지 않은 유형일 수 있다.
    같이 일하는 동료가 이런 사람이라면 검증되지 않은 불확실한 정보가 사실인 것처럼 전파될 수 있다.
    의도는 좋았을지 몰라도 거짓말을 하고 있는 것이다.
    또한 이런 습관은 자신의 부족함을 인정하지 않는 태도로 비칠 수 있으며, 선입견에 갇혀 있기 쉬워 자신의 성장에도 방해가 된다.

    나. 질문과 관련된, 알고 있는 다른 내용들을 설명하면서 질문에 대한 답은 모르겠다고 말한다. (클릭하면 펼쳐집니다.)

    최선을 다해 면접에 임하는 태도를 보여주는 동시에 거짓말은 하지 않는 답변이다.
    이러한 모습은 면접관에 따라 호불호가 갈릴 수 있다.
    요점만 간단히 전달하는 능력을 중요하게 보는 면접관에게는 다소 장황하고 초점을 잃은 답변이 불리하게 작용할 수 있다.
    그러나 꼼꼼하게 오류 없이 전달하는 능력을 중요시하는 면접관에게는 좋게 보일 것이다.
    자신이 가진 지식의 깊이를 구체적으로 드러내므로 면접관 입장에서 평가가 수월해지기도 한다.

    다. 깔끔하게 잘 모르겠다고 말한다. (클릭하면 펼쳐집니다.)

    지원자가 이렇게 대답하기는 쉽지 않다.
    실력이 부족하고 최선을 다하지 않는 사람처럼 보일 것에 대한 두려움 때문이다.
    그러나 사실 면접관에게 좋게 보일 수도 있는 대답이다.
    자신이 무엇을 모르는지 분명히 알고 있고, 정직하게 검증된 내용만을 전달하려 한다는 인상을 주기 때문이다.

    물론 너무 많은 질문에 대해 모른다고 답하면 실력 부족으로 떨어질 수 있다.

회사와의 Fit

  • 작성자의 주관적인 생각으로, 크게 두 종류의 인재상이 있다.
    • 어떤 인재를 원하는지는 회사마다, 그리고 직무마다 다르다.
  • 나와 맞지 않는 유형의 회사에 지원하면 면접에서 떨어질 가능성도 높고, 붙더라도 다니기 힘들다.
    • 유명하고 돈 많이 주는 곳이 가장 다니기 좋은 곳은 아닐 수 있다.
    • 당장 합격이 가장 중요하다면, 나를 속여서 회사에 맞추는 것이 나중에 감당 가능한 일인지 생각해 보는 것이 좋다.
  1. 대기업형 인재

    • 한 가지 일에 특화된 스페셜리스트를 원한다.
    • 튀려고 하지 않고 말 잘 듣는 사람을 원한다.
    • 뽑을 때 면접관이 직무와 직접적으로 관련 있는 능력에만 주로 관심을 가진다.
    • 거대한 조직에서 오래 서비스해 온 게임을 유지보수하게 된다.
    • 상사가 시키는 일을 하게 될 가능성이 높으며 직무가 잘 바뀌지 않는다.
    • 의견 개진이 어렵고 조직이 잘 변화하지 않는다.
    • 개인의 성장 욕구가 적고 안정적인 직장 생활을 원하는 사람에게 잘 맞다.
    • 학부 졸업 후 취업하는 경우에 비교적 잘 맞다.
      그러나 대기업 다니다가 대학원 가는 경우도 종종 있다.
    • 돈과 커리어를 쌓기 좋다.
  2. 스타트업형 인재

    • 이것저것 다 잘 하는 올라운더, 제너럴리스트를 원한다.
    • 대기업에 못 간 사람이 아니라 안 간 사람을 원한다.
      대기업에 합격할 실력이 충분히 되지만, 자신의 성향을 잘 이해하고 있고 자신의 의지로 대기업보다는 스타트업에 들어가기를 희망하는 사람들을 말한다.
    • 타 직군과도 소통을 잘 하는 사람을 원한다.
    • 뽑을 때 직무와 직접적으로 관련 없는 능력들도 중요하게 본다.
    • 작은 조직에서 새로운 게임을 처음부터 빠르게 만들게 된다.
    • 일을 주도적으로 찾고 스스로 배워서 해야 할 가능성이 높으며 여러 직무를 혼자 맡기도 한다.
    • 의견을 비교적 쉽게 개진할 수 있지만 그 일을 본인이 맡게 되는 경우가 많다.
    • 개인의 성장 욕구가 크고 도전적인 직무를 원하는 사람에게 잘 맞다.
    • 대학원 졸업 후 취업하는 경우에 비교적 잘 맞다.
    • 본인이 하고 싶은 일을 하게 될 가능성이 상대적으로 높다.
      그러나 그 일이 수익 창출로부터 자유로운 것은 아니다.

신규 입사자로서 느끼는 점

Note

첫 회사를 다닌 지 한두 달 된 신입의 지극히 주관적인 감상입니다.
저는 현재 게임 개발자를 돕는 도구를 만드는 일을 맡고 있습니다.
일반적인 게임 클라이언트 개발자와 경험이 상이할 수 있습니다.
'작성자는 이렇게 사는구나!' 하고 보시면 좋겠습니다.

  • 맡은 직무를 통해 추구하고자 하는 가치가 자신이 가진 가치관과 일치하는지가 굉장히 중요하다.

    • 어느 회사를 다니는지보다 어느 직무를 맡았는지가 더 중요하다.
      • 회사에 지원할 때에는 바닥부터 게임을 제작하며 라이브로 고객을 상대하는 경험을 해보고 싶었으나, 게임 콘텐츠와 관계 없는 개발자 도구를 만드는 직무를 맡게 될 것에 대해 아쉬움이 있었다.
      • 그러나 입사 후 일주일 만에, 이 부서에 오기를 잘했다는 생각이 들었다.
      • 기한이 촉박하고 성과에 대한 압력을 받는 게임 제작 부서나 라이브 운영 부서에 들어갔다면 성장할 여유도 없이 이미 다룰 줄 아는 기술에 더 숙달되기 위한 훈련만을 반복했을 것 같다.
      • 반면 현재 속한 연구 부서에서는 여유 있게 새로운 기술을 도입해 볼 수 있고, 개인의 성장을 도모할 수 있으며, 회사가 안고 있는 여러 문제점을 해결하는 과정에서 유의미한 기여를 할 수 있다.
    • 회색 직장인이 되지 않고 '깨어 있는 나'로 남아 있기 위해 고민을 하게 된다.
      • 나는 '코더'가 아니다. '프로그래머'이자 '아키텍처'이다.
      • 할 일이 명시적으로 주어지지 않아도, 회사가 나아가고자 하는 가치와 내가 추구하는 가치를 고려하여 스스로 문제를 정의하고 할 일을 찾아서 하는 사람이 되고자 한다.
    • 내가 가장 열정을 쏟고 싶은 일이 회사에서 맡은 일과 같아질 수 있을지 고민해보게 된다.
      • 회사에서 현재 맡은 일이 내 미래에도 분명 도움이 될 일이면서 다른 사람들을 기쁘게 하는 일임을 인식할 때 열정이 솟아난다.
      • 이러한 자기실현(자기일치)적 목표를 세울 수 있다면 힘들이지 않고 행복하게 일할 수 있다. 최적의 회사, 최적의 직무를 찾은 것이다.
      • 워라밸은 회사에서의 자기실현이 불가능할 때 고려하게 되는 '차선의 목표'라고 생각한다.
  • 면접을 준비하기 위해 공부했던 것보다 더 많은 것을 짧은 시간 동안 배우게 된다.

    • 처음 배우는 기술로 더듬더듬 작성한 코드가 성공적으로 돌아가고 거대한 프로젝트에 병합될 때의 뿌듯함은 이루 말할 수 없다.
    • 먼 훗날에 다른 회사를 가더라도 유용하게 쓰이는 기술이라고 생각하면 공부하는 것이 즐거워서 동료에게 질문을 끊임없이 하게 된다.
    • 실제로 요즘에는 쉬는 시간이나 퇴근 후에 개발 공부를 하는 날이 잦아졌다.
    • 회사 밖에서는 배우고 싶어도 배울 수 없었던 것들이 너무나도 많았음을 알게 된다.
    • 기술은 회사에 쌓이는 것이 아니다. 개인에게 남는 것이다.
  • 뛰어난 개발자는 열린 태도를 가진 개발자이다.

    • 자신이 알고 있던 것보다 더 효과적이고 효율적인 문제 해결 방법을 발견하였다면 과감히 새로운 것을 익히고 받아들일 자세가 필요하다.
      그러나 이는 무비판적으로 신기술을 수용하라는 뜻은 아니다.
    • 자신이 잘 아는 주제가 아니더라도 자연스레 관심이 가고, 타인의 생각을 궁금해하며 적극적으로 물어보는 사람이 성장에 유리하다.
    • 더이상 배울 것이 없다고 느낀다면 다른 직무나 다른 회사로 옮길 타이밍이다.
    • 더이상 배우고 싶지 않다는 생각이 든다면, 나라면 개발자를 그만둘 것이다. 연구실을 그만뒀던 것처럼.
  • 신입이 해야 할 역할이 분명히 있다.

    • 어떤 회사도 완벽한 조직일 수 없다. 누구나 사내의 문제점을 알지만, 이를 해결할 의지를 가지고 추진할 수 있는 사람은 신입밖에 없다.
    • 동료들이 경험이 많고 기술적으로 뛰어나다고 해도, 신입이 그들보다 잘 할 수 있는 영역이 분명 존재한다.
  • 회사라는 곳이 생각보다 행복한 곳일 수 있다.

    • 대학생이 막연히 갖는 두려움 중에, 회사에 꼰대들이 있고 이들이 신입을 무시하며 막 대할 것 같다는 생각들이 있다.
      • 실제로 출근길에 쓰러져 응급실에 실려 갔는데 당일 오후에 출근하라고 하는 강제 노역소도 있다. 그런 곳은 당장 그만둬라.
    • 하지만 오히려, 직원이 행복한 회사가 존재한다는 사실을 많은 사람들이 알았으면 한다.
      • 빠른 퇴근을 독려하고, 아플 때 병가 사용 방법을 먼저 알려주며, 질문을 하면 친절히 답해주고, 신입에게 칭찬을 아끼지 않는 동료들이 있다.
      • 신입을 보채지 않고 신입의 능력을 믿어주며 사내에서 좋은 경험을 다양하게 해볼 수 있도록 도와준다.
    • "나같은 사람도 받아준다고?" 해서 들어갔다가 한 달도 안 되어 퇴사를 고민할 바에는, 취업 기간이 길어지더라도 사람을 존중할 줄 알고 문화가 자신과 잘 맞아서 다니는 것이 즐거운 회사를 끝까지 찾아보는 것을 추천한다.
  • 신입이 원하는 회사에 들어가려면 경력직을 금방 따라갈 정도의 내공을 쌓아야 한다.

    • 신입이 첫 회사로 들어가기에는 대기업이 오히려 더 쉬울 수 있다.
      대기업은 신입을 대규모로 뽑아서 교육시킬 제도와 여유를 갖추고 있다.
    • 스타트업은 그럴 여유가 없기 때문에 입사하자마자 1인분이 가능한 사람을 원한다.
      그래서 경력직을 선호하지만 경력직만 뽑는 것은 아니다.
    • 취업과 성취를 목표로 공부하는 사람이 아닌, 새로운 것을 알게 되는 것이 재미있어서 스스로 파고드는 사람이 되면 내공은 저절로 쌓인다.
      • 단, 파고드는 방향이 맡게 될 업무와 잘 맞는 회사에 지원해야 한다.
    • 신입의 입사 전 경험은 이를 통해 내가 하고 싶은 일과 잘 할 수 있는 일이 무엇인지를 명확히 알게 되었다면 충분히 쌓은 것이다.
    • 무작정 스펙(남에게 보여지는 것)을 늘리기 위해 시간을 버릴 필요가 없다.
    • 신입이 경력직보다 실무 경험이 부족한 것은 너무나도 당연하다. 뽑는 사람들도 이를 참작하고 뽑으므로, 경험 부족에 연연하지 않아도 된다.

시작하기 전에

  • https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview
    • 본 자료 내용의 대부분은 위 링크에서 가져왔습니다.
      • 위 링크에는 C++ 및 Unreal에 대한 내용도 있습니다.
    • 본 자료 내용 중 위 링크에 없는, 새롭게 추가한 내용도 있습니다.
  • 나올 가능성이 높은 질문들은 ⭐ 중요! 표시를 달아두었습니다.

Warning

본 자료 내용에는 오류가 있을 수 있습니다.
모든 내용을 그대로 외우기보다는, 다른 자료도 찾아보고 내용을 검증하면서 공부하시면 학습에 도움이 될 것입니다.
그리고 시간이 충분하다면 여기서 얻은 기술을 Unity 프로젝트에 직접 적용해 보면서 자신의 것으로 흡수하시기 바랍니다.
겪어본 사람과 암기만 한 사람은 대답에서 그 차이가 드러납니다.

기술면접 대비 예상 질문

Note

볼드체는 나올 확률이 매우 높은 질문입니다.
Unreal을 사용하는 직무의 경우 C++과 Unreal에 대한 이해가 필요합니다. 이는 여기서 다루지 않습니다.

Unity & C# 스크립팅

Unity에서 사용하는 C# 버전

https://docs.unity3d.com/kr/2023.2/Manual/CSharpCompiler.html
https://docs.unity3d.com/kr/2022.3/Manual/CSharpCompiler.html
https://docs.unity3d.com/kr/2021.3/Manual/CSharpCompiler.html
https://docs.unity3d.com/kr/2020.3/Manual/CSharpCompiler.html

  • 2020.x까지는 C# 8.0 사용
  • 2021.x부터 2023.x까지는 C# 9.0 사용
    • 일부 기능 미지원
  • 현재 C#의 최신 버전은 12.0

Unity Lifecycle

https://docs.unity3d.com/kr/current/Manual/ExecutionOrder.html

  • Awake -> OnEnable -> Start -> FixedUpdate -> Update -> LateUpdate -> OnApplicationPause

  • Awake

    • Enable 여부와 상관없이 호출된다.

    • 항상 가장 먼저 호출된다. 인스턴스 생성 또는 스크립트 로드 시 한 번 불린다.

    • Awake끼리는 호출 순서가 무작위이다.

    • 참조를 형성할 때 쓰인다. (클릭하면 예제 코드가 보입니다.)
      public static GameManager instance;
      void Awake()
      {
          instance = this;
      }
  • OnEnable

    • Start보다 일찍 불린다.
    • 스크립트가 재활성화될 때마다 불린다.
    • 오브젝트 풀링에 주로 사용한다.
  • Start

    • 해당 스크립트 컴포넌트가 Enable되어야 불린다.
    • 한 번 불린다.
  • FixedUpdate는 Update보다 일찍 불리며, 프레임 드랍이 생기더라도 물리 엔진의 고정된 주기에 따라 호출하지 못한 만큼 추가로 함수를 호출하여, 호출 횟수가 경과한 시간에 비례함을 보장한다.

    • FixedUpdate에서는 주로 물리 연산 처리를 한다.
    • Update에서는 주로 사용자 입력 처리를 한다.
  • Update와 LateUpdate는 호출 횟수가 같고 프레임마다 한 번씩 호출되며, 프레임 드랍의 영향을 받아 호출을 건너뛰는 경우가 있다.

    • LateUpdate는 Update보다 나중에 불린다.
    • 프레임 드랍: 한 프레임의 실행 시간 안에 연산을 다 수행하지 못한 경우 해당 프레임에 렌더링을 하지 못하는 현상이다. 화면 버벅임을 유발한다.

Stack / Heap Memory

아래 링크에 있는 가상 메모리 그림을 같이 보면서 공부하시면 좋습니다.
https://stackoverflow.com/questions/32418750/stack-and-heap-locations-in-ram
https://open4tech.com/concept-heap-usage-embedded-systems/

  • 실제 메모리(RAM)의 주소가 아니라, 한 프로세스에 할당된 가상 메모리에서의 주소 공간을 다룬다.
  • 높은 주소 쪽에 Stack이 있다.
    • 쌓일수록 아래로(낮은 주소 쪽으로) 내려온다.
    • 컴파일러는 호출 스택에 pop하고 push하는 머신 코드를 생성할 뿐이고, 이러한 instruction들이 스택을 관리한다.
    • 실행할 수 없다.
    • 값 타입을 저장한다.
    • 변수의 사용 범위를 벗어나면 금방 pop되어 수명이 짧은 편이다.
  • Stack보다 아래에 Heap이 있다.
  • 그 아래에 Static data가 있다. 쓸 수 있고 실행할 수 없다.
  • 그 아래에 Literals가 있다. 이는 읽기 전용이며 실행할 수 없다.
  • 그 아래에 Instructions가 있다. 이는 읽기 전용이며 실행할 수 있다.

C#에서의 얕은 복사 vs. 깊은 복사

  • 얕은 복사: 같은 힙 메모리 주소를 가리키도록 주소를 복사
  • 깊은 복사: 힙에 복사할 객체가 가진 메모리만큼을 새로 할당하여 복사하고 새 메모리 주소를 반환
  • C++과는 조금 다르다.
    • C++에서의 얕은 복사: 멤버의 값만 복사
    • C++에서의 깊은 복사: 멤버의 값 복사 + 포인터가 참조하는 대상까지 복사

C#과 Unity의 Garbage Collector

중요!

  • ".NET과 Unity의 GC가 어떻게 다른지 설명할 수 있나요?"
  • "GC.Collect() 함수를 명시적으로 호출해본 적이 있나요?"
  • "동적 할당을 줄여야 하는 이유는 무엇인가요?"

GC를 쓸 때의 장점

  • 사용자가 메모리를 관리할 필요가 없다. 편하다.
  • 메모리 누수가 일어나지 않는다.
  • 관리되는 힙에 효율적으로 저장한다. 메모리 압축을 한다. 메모리 단편화를 줄인다.
  • 한 객체가 다른 객체가 가진 메모리에 접근하는 일을 막아 메모리 안전성을 높인다.

.NET의 GC

https://learn.microsoft.com/ko-kr/dotnet/standard/garbage-collection/fundamentals

  • 세대 구분이 있다.
    • 0세대, 1세대, 2세대
    • 새로 생긴 객체들은 0세대에 넣는다.
    • 0세대에서 가장 자주 GC가 돌아간다.
    • GC로부터 한 번 살아남은 객체들은 세대가 1씩 오른다.
    • 0세대에서 돌려서 메모리를 확보할 수 없는 경우 1세대도 돌린다. 마찬가지로 1세대에서도 메모리를 확보할 수 없는 경우 2세대까지 돌린다. 즉, 2세대에서 GC가 돌아갔다면 1세대와 0세대에서도 GC가 돌아간 것이다.
    • 3세대도 있다. 대형 개체를 저장하는 힙이다. 여기서는 주소 이동이 거의 일어나지 않는다. 복사하면 오래 걸리기 때문이다. 이 3세대는 논리적으로는 2세대로 취급한다.
  • 관리되는 힙 영역이 있다.
  • Mark and Sweep 알고리즘으로 GC를 돌린다.
    • static 변수, 스레드 스택의 지역 변수, CPU 레지스터 등을 root로 잡는다.
    • 여기서부터 참조 가능한 모든 변수들을 탐색하면서 mark한다.
    • mark 페이즈가 끝나면 관리되는 힙 영역에 할당된 모든 참조 타입 변수를 탐색하면서 mark되지 않은 것들을 sweep한다.
    • sweep할 때 힙 압축을 수행한다. 만약 garbage가 있으면 그 위(그보다 높은 주소)에 저장된, mark된 메모리를 garbage가 있던 공간에 옮길 준비를 한다. 변경될 주소 포인터를 계산하고, 메모리를 복사하여 옮기는 작업을 수행한다.
    • 두 객체가 서로를 상호 참조하고 있어도, root로부터 시작하는 외부 개체와 연결되어 있지 않다면 Mark and Sweep 알고리즘에서 mark되지 않으므로 상호 참조가 문제가 되지 않는다.
  • GC가 돌아가는 중에는 모든 다른 스레드가 suspended 상태가 된다.

Unity의 GC

  • Boehm–Demers–Weiser garbage collector 알고리즘을 사용한다.
    • Mark and Sweep의 변형이다.
  • .NET의 GC와 무엇이 다른가?
    • 세대 구분이 없다.
    • 메모리 압축이 없다.
    • 별도의 GC 스레드 없이, 메모리 할당 스레드에서 돌아간다.
    • 아무튼 별로 안 좋다.
  • 왜 다른가?
    • 싱글 스레드 환경에서 사용하기 위해
  • 유의할 점
    • 메모리 최적화가 없기 때문에 19버전 이상에서 사용하는 점진적 GC를 사용하거나 오브젝트 풀링 등의 최적화 기법을 사용할 필요가 있다.

동적 할당을 줄여야 하는 이유

https://docs.unity3d.com/kr/current/Manual/performance-managed-memory.html

  • 공간적 이유
    • 동적 할당은 메모리(힙) 공간을 잡아먹는다.
      • 계속 쌓이면 out of memory 오류가 발생하여 크래시가 발생할 수 있다.
      • 특히 모바일 앱에서 메모리 최적화를 하지 않으면 10분만 켜 두어도 메모리를 1GB 이상 차지하다가 결국 운영체제에 의해 강제종료되는 경우가 생긴다.
    • 잦은 할당과 해제는 메모리 단편화를 일으킨다.
      • 힙에 빈 공간이 있음에도 이들이 분산되어 있어 새 할당을 한번에 넣을 공간이 없다면 힙을 2배씩 확장해야 한다.
  • 시간적 이유
    • GC가 돌아가는 동안 많은 연산을 한다.
      • GC.Collect()를 직접 호출하지 않는 한 언제 GC가 돌아갈지 모른다. Unity도 모르고 프로그래머도 모른다.
      • 다른 중요한 연산을 수행해야 할 때 마침 GC가 돌아가고 있으면 CPU 병목이 생겨 느려질 수 있다.
      • GC가 돌아가는 중에는 다른 모든 스레드가 일시 중단된다.
    • 힙을 압축하는 연산은 시간이 오래 걸린다.
      • 메모리 복사 및 붙여넣기를 하기 때문이다.
    • 동적 할당한 내용물을 메모리에 로드할 때에도 시간이 걸린다.

Unity에서 할당을 줄이는 습관

면접에서 물어볼 가능성이 높은 내용이면서, 실무에서도 프로그래머의 실력을 판가름하는 기본 소양입니다.
https://docs.unity3d.com/kr/current/Manual/performance-garbage-collection-best-practices.html

  • 임시 할당
    • 매 프레임마다 새로 힙에 할당하는 동적 메모리가 있다면 이를 줄여야 한다.
    • new는 메모리 최적화 시 최우선 제거 대상이다.
    • Update()에서 new를 한 번 사용하여 100 바이트씩 임시 할당을 해도 60 FPS 기준 초당 6KB의 할당이 이루어진다. 3분이면 1MB의 할당이 이루어진다.
  • 반복되는 문자열 연결
    • +로 문자열을 연결할 때의 문제점은 설명 참고
    • 특히 매 프레임마다 UI의 Text를 +로 연결해 업데이트하는 일을 피하기 위해 다음의 방법을 사용할 수 있다.
      • 매 프레임 조건을 확인하여 해당 조건이 만족될 때에만 텍스트를 업데이트한다.
      • 고정된 부분과 변하는 부분을 서로 다른 UI Text 오브젝트로 둔다.
  • 재사용 가능 오브젝트 풀 (object pool 패턴)
    • 게임오브젝트를 Destroy()하지 않고 SetActive()하여 재사용한다.
    • 그러나 오랫동안 쓰지 않을 게임오브젝트는 Destroy()로 unload하는 것이 좋다.
  • 컬렉션과 배열 재사용
    • ListDictionaryClear()하여 재사용한다.
  • 배열 값 반환 메서드
    • 매번 새로 배열을 만들어 반환하지 말고 기존 배열을 인자로 받아 수정하도록 한다.
  • 빈 배열 및 문자열 재사용 (null object 패턴)
    • 배열 값 기반의 메서드가 빈 세트를 반환해야 할 때 null 값 대신 빈 배열로 미리 할당된 정적 인스턴스를 반환하면 더 효율적이다.
    • 빈 문자열의 경우 설명 참고
  • LINQ 사용 줄이기
  • 클로저 및 익명 메서드
  • 박싱
  • 배열 기반 Unity API
    • 반복문에서는 배열을 반환하는 프로퍼티에 자주 접근하지 않는 것이 좋다.

    • 예: Input.touches (클릭하면 예제 코드가 보입니다.)
      // Bad C# script example: Input.touches returns an array every time it’s accessed
      for ( int i = 0; i < Input.touches.Length; i++ )
      {
        Touch touch = Input.touches[i];
      
          // …
      }
      // Better C# script example: Input.touches is only accessed once here
      Touch[] touches = Input.touches;
      
      for ( int i = 0; i < touches.Length; i++ )
      {
      
        Touch touch = touches[i];
      
        // …
      }
      // BEST C# script example: Input.touchCount and Input.GetTouch don’t allocate at all.
      int touchCount = Input.touchCount;  // access outside the loop
      
      for ( int i = 0; i < touchCount; i++ )
      {
        Touch touch = Input.GetTouch(i);
      
        // …
      }

C# constreadonly의 차이

  • const
    • 컴파일 타임에 변수가 값으로 대체된다.
    • 스택에 저장된다.
    • 선언할 때에만 값을 설정할 수 있다.
    • 값이 바뀌면 다시 빌드해야 한다.
    • 내장형 타입과만 사용할 수 있다.
  • readonly
    • 런타임 상수이다.
    • 힙에 저장된다.
    • 선언할 때나 생성자에서만 값을 설정할 수 있다.
    • 코드에 대한 참조를 유지하므로 값이 바뀌더라도 전체를 다시 빌드하지 않아도 된다.
    • 어떤 타입과도 사용할 수 있다. (사용자 정의 클래스 포함)
  • 일반적으로 const보다 readonly를 쓰는 것이 좋다.
    • const가 조금 빠르기는 하며, 다음의 경우에는 const를 사용해도 된다.
      • switch/case문 레이블
      • enum 정의
      • 프로퍼티의 매개변수

C# structclass 인스턴스의 차이

https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/struct

  • "클래스의 인스턴스 안에 들어있는 값 타입의 멤버 변수는 스택에 저장되나요, 힙에 저장되나요?"

  • "구조체의 인스턴스 안에 들어있는 참조 타입의 멤버 변수는 스택에 저장되나요, 힙에 저장되나요?"

  • struct(구조체) 인스턴스

    • 값 타입이다.
    • 스택에 저장된다.
    • 필드와 메서드를 가질 수 있다. (주의!)
    • 할당하거나 인수로 넘기거나 반환할 때 복사된다.
  • class(클래스) 인스턴스

    • 참조 타입이다.
    • 힙에 저장된다.
    • 필드와 메서드를 가질 수 있다.

C# Boxing & Unboxing

https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/built-in-types
https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/types/boxing-and-unboxing

  • 값 타입

  • 참조 타입

  • Boxing: 값 타입을 참조 타입으로 변환

  • Unboxing: 참조 타입을 값 타입으로 변환

  • Boxing과 unboxing은 동적 할당을 만드므로 비싸다.

  • 대표적인 boxing의 예 (피해야 한다.)

    • object.Equals(object other) 사용

      int x = 1;
      object y = new object();
      y.Equals(x);  // 값 타입인 x를 참조 타입인 Object 타입으로 boxing하므로 인자 전달 시 할당 발생
    • struct를 부모 인터페이스 클래스로 캐스팅

      SomeStructWithInheritance struct1 = new();
      IDisposable structDisposable = struct1;  // boxing
      
      List<IDisposable> disposables = new();
      disposables.Add(struct1);   // boxing
      
      public struct SomeStructWithInheritance : IDisposable
      {
          public int x;
      
          public void Dispose()
          {
              //some code
          }
      }
    • string.Concat(object a, object b, object c) 사용

      // 값 타입인 42와 true가 object 타입으로 boxing된 후에 string으로 변환된다.
      string answer = string.Concat("Answer", 42, true);

      [!NOTE]
      string.Concat()에는 여러 버전이 있고, 그 중 인자로 문자열만 받는 버전을 사용하면 boxing이 일어나지 않는다.

  • 참조 타입을 참조 타입으로 변환하는 경우는 boxing의 사례가 아니다.

  • 변수가 스택에 저장되는지 힙에 저장되는지는 boxing과 아무런 관련이 없다.

Unity Serialization / Deserialization

  • 직렬화
    • (동적 할당을 통해 저장된) 객체를 바이트 단위로 변환하여 데이터화하는 것
    • 직렬화를 하면 디스크에 저장하거나 네트워크를 통해 전송하기 용이하며 프로그램의 실행이 멈추어도 데이터를 보존할 수 있다.
  • 역직렬화
    • 역직렬화의 정의는 직접 생각해 보시기 바랍니다.
  • Unity에서 어떤 클래스를 직렬화하려면 어떻게 해야 하는가?
    • 추상 클래스나 일반 클래스가 아니어야 하고
    • 클래스 앞에 [Serializable] 데코레이터를 붙이고
    • 해당 클래스의 모든 필드가 직렬화 가능해야 하는데
    • int, bool, string 등의 primitive 타입은 모두 직렬화 가능하고
    • 열거형(enum)으로 정의된 타입도 직렬화 가능하고
    • Vector3, Color 등의 일부 Unity 내장 타입도 직렬화 가능하고
    • 구조체는 구조체 앞에 [Serializable] 데코레이터를 붙이면 직렬화 가능하다.
    • 추가로 UnityEngine.Object에서 파생된 오브젝트를 가리키는 참조도 직렬화 가능하다.
    • 다만 static, const, readonly는 직렬화되지 않으며
    • private 필드도 [SerializeField]가 붙어있지 않다면 직렬화되지 않는다.
  • JSON(JavaScript Object Notation)
    • 데이터를 직렬화한 대표적인 형식이다.
    • 장점
      • 가독성이 높다.
      • 다른 언어와 쉽게 호환된다.
      • 동적 타입을 사용하는 언어(JavaScript 등)에서 유리하다.
    • 단점
      • 용량이 크다.
        이는 동적 할당을 많이 만들고 GC가 일하게 한다는 뜻이다.
      • 직렬화 및 역직렬화 시간이 오래 걸린다.
        문자열 파싱 및 결합이 필요하기 때문이다.
      • Random access가 어렵다.
        특정한 key에 해당하는 값 하나를 가져오려면 전체를 파헤쳐야 한다.
      • 데이터 조작이 쉬워 보안에 취약하다.
    • Unity에서는 Newtonsoft.JSONUnityEngine.JsonUtility를 사용할 수 있다.
      • 면접에서 이것까지 묻지는 않겠지만, 나중에 둘의 차이를 알아두면 좋습니다.
  • BinaryFormatter보안 취약점(code injection)이 발견되었으므로 사용하지 않아야 한다.
  • 효과적인 직렬화를 위한 각종 서드 파티 라이브러리들이 있으니 찾아보면 좋습니다.

C# string

  • Immutable이다.
    • 문자열 값을 수정하면 새로운 값을 만들고 참조를 거기로 잇는다.
    • 안 쓰는 문자열 값은 GC가 처리한다.
  • 왜 이렇게 구현되어 있는가?
    • 멀티스레딩 환경에서 동기화를 모두 신경쓰는 것보다 readonly로 하는 것이 편하기 때문이다.
  • string을 자주 바꾸면 할당이 많이 일어난다.
    • 이럴 때에는 StringBuilder이나 Cysharp의 ZString을 사용하는 것이 낫다.
  • StringBuilder
    • 기본적으로 16글자를 담을 수 있는 버퍼를 잡는다.
    • 이 버퍼 안에서는 수정이 이루어져도 GC가 처리하지 않는다.
    • 기존 버퍼가 꽉 찼는데 append하는 경우 뒤에 새 버퍼를 만들고 링크하여 연결한다.
  • 문자열 보간
    • 문자열 앞에 $를 붙여주면 사용할 수 있다.
    • 예: text = $"(x, y) = ({pos.x}, {pos.y})"

문자열 할당을 줄이는 방법

  • 빈 문자열은 "" 대신 string.Empty를 사용한다.
  • string.Split()의 사용을 줄인다.
  • +로 문자열을 연결하지 않고 StringBuilder 또는 문자열 보간을 이용한다.
    • 예: string s = "a" + "b" + "c" + "d";의 코드에서는 "abcd"를 만들기 위해 "a", "ab", "abc"라는 불필요한 중간 결과물들이 할당된다.
    • System.Text.StringBuilder보다 할당을 줄인 서드 파티 라이브러리도 있다. (예: Cysharp의 ZString)

빈 문자열 확인

  • 속도가 가장 빠른 방법은 string.Length == 0을 확인하는 것이다.
    • 다만 이는 string이 null이 아님이 확실한 상황에서만 사용할 수 있다.
    • 평소에 빈 문자열을 반환할 때 null이나 ""보다는 string.Empty를 반환하는 것이 좋다.
  • string.IsNullOrEmpty()를 쓰면 안전하다.

this

  • 클래스의 현재 인스턴스를 가리킨다.

    • 지역 변수와 필드를 구분할 때 필드에 대해 this.필드를 사용하여 구분할 수 있다.
  • 생성자에서 : this()를 사용할 수 있다.

    • 생성자를 여러 개 만드는 경우, 중복되는 코드를 : this()로 정리할 수 있다.

    • this(int a)와 같이 인자도 입력할 수 있다. 그러면 그 생성자를 가리키게 된다.

    • 예제 코드 보기 (클릭하면 펼쳐집니다.)

      아래 코드를 그 아래의 코드로 바꿀 수 있다. 기능은 같다.

      class MyClass
      {
          int a;
          int b;
      
          public MyClass()
          {
              a = 10;
          }
      
          public Myclass(int b)
          {
              a = 10;
              this.b = b;
          }
      }
      class MyClass
      {
          int a;
          int b;
      
          public MyClass()
          {
              a = 10;
          }
      
          public Myclass(int b) : this()
          {
              this.b = b;
          }
      }
  • 정적 함수에서 인자에 this를 쓸 수 있다.

    • 예: public static void Shuffle<T>(this IList<T> list)
    • 이렇게 하면 확장 메서드를 만들 수 있다.
    • 사용 예 (둘 다 된다.)
      • Shuffle(list);
      • list.Shuffle();
    • 그러나 기존 클래스의 함수와 동일한 시그니처로 확장 메서드를 정의하면 호출되지 않는다. 컴파일 타임에 인스턴스 함수가 우선적으로 호출되고, 이것이 없으면 확장 메서드를 호출하기 때문이다.

delegate & event

  • delegate는 함수 대리자이다.
    • 함수를 타입처럼 취급하고 함수에 대한 참조를 갖는다.
    • 인자 수, 인자 타입, 반환 타입을 통해 정의된다.
      • 같은 함수 시그니처끼리는 모두 호환된다.
    • 호출할 함수 목록을 담을 수 있다.
    • 이것이 가리키는 함수들을 순서대로 모두 호출할 수 있다.
  • event는 선언한 클래스에서만 호출할 수 있는 delegate이다.
    • 다른 클래스에서는 함수를 등록하는 것만 가능하다.
  • Action: 인자 타입이 T이고 반환 타입이 void인 함수 대리자 템플릿
  • Func<T, TResult>: 인자 타입이 T이고 반환 타입이 TResult인 함수 대리자 템플릿
  • Predicate: 인자 타입이 T이고 반환 타입이 bool인 함수 대리자 템플릿

https://www.jacksondunstan.com/articles/3765

  • 아래와 같은 코드에서 불필요한 할당이 발생할 수 있다.

    void TakeDelegate(Action del)
    {
    }
    void MyFunction()
    {
    }
    TakeDelegate(MyFunction);
    • 퀴즈: 어디에서 할당이 일어났는지 맞혀보세요. (클릭하면 펼쳐집니다.)
      • 위 코드의 마지막 줄이 컴파일러에 의해 다음과 같이 변환된다.
      TakeDelegate(new Action(MyFunction));
  • 호출할 함수가 null인 문제

    • 대리자의 함수 목록이 비어있음을 확인하지 않고 호출하면 NullReferenceException이 발생한다.
    • if문으로 null 체크를 하는 것은 좋지 않다.
      • 멀티스레딩 환경에서 null 체크 통과 후 다른 스레드에서 등록 취소를 하면 대리자가 null인 경우가 생길 수 있다.
    • ?.(null conditional operator)를 사용하는 것이 스레드로부터 안전하다.
      • 이 연산자는 atomic하기 때문에 멀티스레딩 환경에서도 null 체크와 호출을 동시에 해준다.
      • 문제가 있다면, Unity에서는 ?.이 의도대로 동작하지 않을 수 있다. 자세한 내용은 C#과 Unity의 null 참고.

Lambda, Anonymous Method & Closure

  • 람다 식

    Action printLine = () => Console.WriteLine();
    Func<int, int> square = (x) => x * x;
  • 익명 메서드 (무명 메서드)

    Action printLine = delegate() { Console.WriteLine(); };
    Func<int, int> square = delegate(x) { return x * x; };
  • 클로저

    int i = 0;
    Action print = () => Console.WriteLine(i);  // 람다 식 바깥의 지역 변수 i 포착
    Func<int> increment = () => ++i;            // 람다 식 바깥의 지역 변수 i 포착
  • C#의 메서드 참조(delegate)는 참조 형식이므로 힙에 할당된다.

    • 즉, 익명 메서드이든 미리 정의된 메서드이든 메서드 참조를 인자로 전달하면 임시 할당이 발생한다.
  • 익명 메서드를 클로저로 전환하면 메모리 양이 상당히 증가한다.

    • 클로저를 만들면 정확한 값을 전달하기 위해 외부 범위 변수를 유지할 수 있는 익명 클래스를 만들고 이것을 인스턴스화하여 힙에 할당한다.
    • 가급적 클로저보다는 익명 메서드를 쓰는 것이 좋다.
  • 클로저는 boxing에 의한 할당인가?

  • static 키워드를 붙여 정적 익명 함수를 만들면 지역 변수를 실수로 포착하여 클로저가 되는 현상을 방지할 수 있다.

    • 면접에서 이런 것까지 묻지는 않을 것입니다.

Coroutine vs. 비동기 프로그래밍 vs. UniTask vs. Awaitable

https://gamedevbeginner.com/async-in-unity/
https://tistory.jeon.sh/59

  • 코루틴 (Coroutine)

    • IEnumerator 타입으로 정의한다.
      • 값을 반환할 수 없다.
    • 여러 프레임에 걸쳐 실행해야 하는 로직을 짤 때 유용하다.
      • 실행권을 놓았다가 나중에 이어서 실행하는 조건(yield)을 다양하게 정할 수 있다.
      • Update() 문보다 편리하게 가독성 높은 코드를 작성할 수 있다.
    • Unity 메인 스레드에서 돌아간다.
    • Non-blocking으로 병렬적으로 돌아가는 것처럼 보이지만 실제로는 순차적으로 돌아간다.
      • 함수를 쪼개서 중간중간에 다른 함수가 실행될 수 있도록 한 것이다.
      • 매우 오래 걸리는 작업을 코루틴에 넣으면 그만큼 게임이 끊기고 멈춘다.
    • 코루틴을 실행하는 컴포넌트가 Destroy()되면 실행이 멈춘다.
    • yield return 문에 쓰이는 new도 할당을 유발한다.
      • WaitForSeconds() 등을 캐싱하여 사용하는 것이 좋다.
  • 비동기 함수 (async / await)

    • async void 또는 async Task 또는 async Task<TResult> 타입으로 정의한다.
    • Unity와 무관한 긴 작업을 처리할 때 유용하다.
      • 예: 서버 API 호출, 큰 데이터 로드 등
    • 메인 스레드가 아닌 스레드에서 실행된다. 멀티스레딩이다.
      • 코루틴과 달리 실제로 병렬적이다. 따라서 Unity 로직의 실행을 가로막지 않는다.
    • Unity의 MonoBehaviour에서 파생된 native API는 멀티스레딩을 지원하지 않으며, 메인 스레드에서만 호출할 수 있다.
      • 예를 들어 UI를 변경해야 하는 경우 로직을 메인 스레드로 가져와 실행해야 한다.
    • 함수를 실행한 오브젝트가 파괴되어도 취소 토큰을 사용하지 않는 한 끝까지 실행된다.
    • Thread-safe하게 코드를 작성해야 한다.
    • WebGL에서 지원되지 않는 기능이다.
  • UniTask

    • https://github.com/Cysharp/UniTask
    • Cysharp에서 제공하는 서드 파티 라이브러리
    • Unity에서 비동기 프로그래밍을 구현할 때 유용하다.
    • Unity 메인 스레드에서 돌아간다.
    • Task를 사용할 때보다 할당을 줄일 수 있다.
      • 내부가 struct로 구현되어 있다.
    • UniTask 객체를 두 번 이상 await(재사용)할 수 없다.
    • WebGL에서도 호환된다.
  • Awaitable

    • https://docs.unity3d.com/kr/2023.2/Manual/AwaitSupport.html
    • Unity에서 async, await을 사용할 수 있게 해준다.
    • 백그라운드 스레드에서 실행하다가도 원할 때 메인 스레드로 돌아와 실행을 이어할 수 있다. 반대로도 가능하다.
      • 실행하는 스레드를 너무 자주 전환하는 것은 안 좋다.
    • UniTask와 달리 클래스로 구현되어 있어 할당이 발생한다.
    • 풀 시스템으로 관리되므로, 한 Awaitable 객체를 두 번 이상 await(재사용)할 수 없다.
    • Unity 2023.1 이후의 최신 버전에서만 사용할 수 있다.
      • Unity 6로 오면서 기능이 더 추가되었지만 여전히 실전에서 사용하기에는 부족하다는 평가를 받는다.

C#과 Unity의 null

https://stackoverflow.com/questions/62678228/why-does-c-sharp-null-conditional-operator-not-work-with-unity-serializable-vari
https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object
중요!

  • "Unity의 fake null에 대해 설명해 보세요."

  • null은 [널]이라고 읽는다.

  • Unity 엔진의 백엔드는 C++(IL2CPP 또는 Mono)로 짜여 있고, Unity C#의 문법들은 이 C++ 엔진과 소통하기 위한 개발자 API이다.

  • UnityEngine.ObjectDestroy()하여 C++ 네이티브 엔진의 객체를 null로 만들고 UnityEngine.Objectnull인 것처럼 표시해도(fake null) .NET의 System.Objectnull이 아니다.

    • 해당 UnityEngine.Object의 메타데이터가 잔존하고, 이를 가리키는 참조를 다른 게임오브젝트나 컴포넌트들이 여전히 가지고 있기 때문이다.
    • .NET GC가 돌기 전까지는 객체가 메모리에서 해제되지 않으며, GC가 실행되어도 참조받고 있지 않은 객체만을 찾아 지우기 때문에 이 UnityEngine.Object의 base인 .NET의 System.Object가 즉시 null이 되지는 않는다.
    • 그러나 C#에서 Destroy()를 호출하면 C++ 네이티브 엔진에서는 객체가 null이 된다.
      • 참고로 이 과정은 무겁다. C++에서 객체를 없애는 함수를 호출하여, 룩업을 수행하고 C# 스크립트 레퍼런스를 C++ 네이티브 레퍼런스로 전환하는 과정을 거친다.
    • 즉, Destroy() 호출 시 C++ 네이티브 엔진에서는 객체가 null이 되어 있지만 C#에서는 System.Objectnull이 아닌 상태로 남아 있고, 이것이 파괴된 C++의 객체를 가리키고 있게 된다.
    • 이때 개발자가 Destroy()한 객체를 계속 사용하면 문제가 생긴다.
    • 따라서 실제로는 System.Objectnull이 아니지만 UnityEngine.Objectnull인 것처럼 표시하여 이 객체를 Unity에서 사용하지 못하게 하는 것이다.
    • 이를 위한 장치로써 Unity에서 ==!=를 오버라이드하여 C++ 객체의 존재 유무를 확인하고 System.Object와 무관하게 null 여부를 반환한다.
    • 이를 fake null이라고 한다.
    • Unity에서 Destroy()한 오브젝트에 접근하는 경우 NullReferenceException이 아니라 MissingReferenceException이 뜨는데, 이는 Unity의 fake null 로직이 있기 때문이다.
  • 그러나 is null(pattern matching), ?.(null conditional operator), ??(null coalescing operator)의 경우 UnityEngine.Object가 아니라 System.Objectnull 여부를 검사하기 때문에 UnityEngine.ObjectDestroy()되었는지 확인할 때에는 사용할 수 없다.

    • 이들 연산자는 C# 문법 상 오버라이드할 수 없다.
  • Unity의 null 비교는 느리다.

    • UnityEngine.Object가 살아있는지 검사하는 로직이 무겁기 때문이다.
    • 이것이 살아있다는 것이 확실하면(Destroy()된 객체가 아님을 보장할 수 있다면) 다음 세 가지 방법으로 null 비교 속도를 빠르게 할 수 있다.
      1. System.Object로 캐스팅하는 방법
      2. System.Object.ReferenceEquals()로 비교하는 방법
      3. 패턴 매칭 is null을 이용하는 방법
  • == null vs. is null

    • == null은 타입 별로 오버라이드된 == 연산자를 호출하여 계산한다.
      • 따라서 UnityEngine.Object을 비교할 때에는 느리다.
      • == null은 예외적인 상황을 내부에서 체크하므로 가장 안전하다.
    • is null은 바로 ceq 인스트럭션 연산을 적용하기 때문에 빠르다.
      • 다만 UnityEngine.Object처럼 ==이 오버라이드된 경우 is null을 사용하면 의도하지 않은 동작이 나타날 수 있다.
  • 속도 비교

    • is nullReferenceEquals(null)은 속도가 비슷하게 가장 빠르다.
    • (가장 빠름) is null < Equals(a, null) < a.Equals(null) < a == null (가장 느림)

https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-

  • ?[](요소 액세스 null 조건부 연산자)
    • 예: a?[x]
      • anull이면 null을 반환한다.
      • anull이 아니면 a[x]의 값을 반환한다.
      • xa의 인덱스 범위 밖에 있는 경우 IndexOutOfRangeException을 띄운다.
    • ==이 오버라이드된 경우에 ?[]을 사용하면 의도하지 않은 동작이 나타날 수 있다.

https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/operators/null-coalescing-operator

  • ??(null coalescing operator)
    • 예: return a ?? 3;
      • anull이 아니면 a를 반환하고, null이면 3을 반환
    • 왼쪽 연산항이 null이 아니면 오른쪽 연산항을 평가하지 않는다.
    • ==이 오버라이드된 경우에 ??를 사용하면 의도하지 않은 동작이 나타날 수 있다.
    • ??는 오버라이드할 수 없다.
  • ??=(null coalescing assignment operator)
    • 예: a ??= 3;
      • anull일 때에만 a에 3을 대입
    • ==이 오버라이드된 경우에 ??=을 사용하면 의도하지 않은 동작이 나타날 수 있다.
    • ??=는 오버라이드할 수 없다.

List & Dictionary

  • List<>: 배열(ArrayList)
    • 용량(capacity)을 초과하여 삽입하는 경우 용량을 늘린 새 배열을 할당하고 기존 배열의 값을 복사한다. 따라서 시간 복잡도가 $O(n)$이다. 이를 피하려면 미리 사용할 만큼의 용량을 할당할 필요가 있다.
    • TrimExcess()를 쓰거나 Capacity를 직접 변경하여 낭비되는 공간을 줄이는 경우에도 새 배열을 할당한다. 따라서 시간 복잡도가 $O(n)$이다.
    • Remove()는 배열의 용량을 변경하지 않는다.
  • Dictionary< , >: 해시 테이블
  • HashSet<>: 해시 테이블
  • SortedSet<>: 레드-블랙 트리
  • SortedList< , >: 배열
  • SortedDictionary< , >: 레드-블랙 트리

https://stackoverflow.com/questions/3070644/ordered-list-of-keyvaluepairs

  • SortedList<TKey, TValue>SortedDictionary<TKey, TValue>의 차이

    • 둘 다 검색은 $O(\log{n})$
    • 삽입, 삭제에서 차이가 있다.
      • SortedList< , >는 삽입, 삭제가 $O(n)$
      • SortedDictionary< , >는 삽입, 삭제가 $O(\log{n})$
    • 메모리는 SortedList< , >가 더 적게 차지한다.
    • 정렬된 데이터로부터 자료구조가 생성된 경우에는 SortedList< , >가 더 빠르다.
    • KeysValues list를 반환해야 할 때 SortedList는 이 list를 그냥 반환하면 되고, SortedDictionary는 list를 생성해서 반환해야 한다.
    • SortedDictionary는 generic이 아니다.
  • List<KeyValuePair< , >>의 용도

    • 이것은 SortedDictionary< , >와 다르게, 삽입 순서를 보존한다.
    • generic이기도 하다.
  • DictionarySortedDictionary 중 누가 더 나은가?

    • 삽입과 삭제는 Dictionary가 더 빠르다.
    • 검색에 있어서 SortedDictionary가 아주 조금 더 빠르다.
    • 보통은 Dictionary를 쓰는 것이 낫다.
  • Hash Table

    • 운이 좋으면 $O(1)$만에 삽입, 삭제, 검색이 가능하다.
    • 운이 나쁘면(해시 함수가 계속 충돌하면) 최악의 경우 $O(n)$이 걸린다.
    • 충돌 시 linear probing(+1, +2, +3, ...), quadratic probing(+1, +4, +9, ...), double hashing(두 개의 해시 함수 사용)을 통해 내부를 채워 나간다.
  • Red-black Tree

    • self-balancing binary search tree
    • 삽입, 삭제, 검색 모두 $O(\log{n})$이다.
  • 퀴즈: C#의 Queue는 내부적으로 어떤 자료구조로 구현되어 있을까요?
    가. ArrayList
    나. Doubly linked list
    다. Hash table
    라. Red-black tree

    정답을 확인하려면 클릭하세요. (클릭하면 펼쳐집니다.)

C# LINQ

https://learn.microsoft.com/ko-kr/dotnet/csharp/linq/

  • Language-Integrated Query

  • [링크]라고 읽는다.

  • LINQ 예제 코드 (클릭하면 펼쳐집니다.)
    string sentence = "the quick brown fox jumps over the lazy dog";
    // Split the string into individual words to create a collection.
    string[] words = sentence.Split(' ');
    
    // Using query expression syntax.
    var query = from word in words
                group word.ToUpper() by word.Length into gr
                orderby gr.Key
                select new { Length = gr.Key, Words = gr };
    
    // Using method-based query syntax.
    var query2 = words.
        GroupBy(w => w.Length, w => w.ToUpper()).
        Select(g => new { Length = g.Key, Words = g }).
        OrderBy(o => o.Length);
    
    foreach (var obj in query)
    {
        Console.WriteLine("Words of length {0}:", obj.Length);
        foreach (string word in obj.Words)
            Console.WriteLine(word);
    }
    
    // This code example produces the following output:
    //
    // Words of length 3:
    // THE
    // FOX
    // THE
    // DOG
    // Words of length 4:
    // OVER
    // LAZY
    // Words of length 5:
    // QUICK
    // BROWN
    // JUMPS

https://learn.microsoft.com/ko-kr/dotnet/csharp/tutorials/working-with-linq https://medium.com/@qjfrntop12/linq-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-bc2c60dfca4f

  • 장점
    • 코드를 짧게 하여 가독성을 높인다.
    • 확장 메서드를 만들 수 있다.
    • 지연 계산(lazy evaluation)을 수행한다.
      • 결과 값을 사용하거나 ToList(), ToArray() 따위를 호출하면 그제서야 연산을 수행한다.
      • 중간 결과물을 캐싱하려면 ToList(), ToArray() 따위를 호출해야 한다.
      • 지연 계산은 임시 할당을 비교적 적게 만드므로 대형 컬렉션을 다룰 때 성능이 좋다.
      • 때로는 즉시 계산이 지연 계산보다 유용할 때도 있고, 지연 계산의 특징 때문에 의도한 대로 작동하지 않을 수 있다.
  • 성능 상 단점
    • 불필요한 할당을 많이 만든다.
    • 예를 들어, 아래와 같은 코드에서는 w.ToUpper(), GroupBy(), Select() 등의 메서드 호출마다, 최종 결과물에서 쓰이지 않는 중간 결과물이 발생한다.
    • ToArray(), ToList() 등도 메모리 및 시간 성능에 안 좋은 영향을 준다.
    • 클로저를 만들기도 쉽다.
    • for 문(권장) 또는 foreach 문으로 변환하여 메모리를 최적화할 수 있다.
var query2 = words.
    GroupBy(w => w.Length, w => w.ToUpper()). // w.ToUpper(), GroupBy() 할당 발생
    Select(g => new { Length = g.Key, Words = g }). // Select() 할당 발생
    OrderBy(o => o.Length);

C# Reflection

https://learn.microsoft.com/ko-kr/dotnet/fundamentals/reflection/reflection

  • 로드된 어셈블리 내에 정의된 타입에 대한 정보를 런타임에 가져올 수 있다.
  • 어셈블리에는 모듈이 포함되고, 모듈에는 타입이 포함되고, 타입에는 멤버가 포함된다.
  • 리플렉션은 어셈블리, 모듈 및 타입을 캡슐화하는 개체를 제공한다.
    • 동적으로 타입 인스턴스를 만들거나, 타입을 기존 개체에 바인딩하거나, 기존 개체에서 타입을 가져올 수 있다.
    • 해당 타입의 메서드를 호출하거나 필드 및 프로퍼티에 접근할 수 있다.
  • 탐색하는 시간이 굉장히 느리기 때문에 가급적 사용을 피해야 한다.
  • 메서드 이름을 인자로 넣어 해당 메서드를 호출하는 함수는 내부적으로 Reflection을 사용하므로 피해야 한다.
    • 예: StartCoroutine("FadeOut"); 대신 StartCoroutine(FadeOut());을 사용해야 한다.

Unity Addressable

https://docs.unity3d.com/Packages/[email protected]/manual/index.html
https://unity.com/kr/blog/technology/tales-from-the-optimization-trenches-saving-memory-with-addressables
https://medium.com/pinkfong/unity-addressable-asset-%EB%A5%BC-%EC%99%9C-3017f3fa2edc

  • "과거에 진행한 프로젝트에서 어드레서블을 사용해 본 경험을 말씀해 주세요."

  • "원격으로 리소스 콘텐츠를 배포할 때의 장점은 무엇인가요?"

  • Unity에서 에셋을 런타임에 동적으로 로드하는 방법은 세 가지가 있다.

    • Resources 폴더
    • 에셋 번들 (Asset bundle)
    • 어드레서블 (Addressable)
  • 그 중 어드레서블은 최근에 나온 에셋 관리 방법이다.

  • 에셋을 원격으로 배포하고 동적으로 로드해야 하는 이유?

    • 빌드 과정 중 에셋 패킹 과정이 있다.
    • 빌트인 에셋: 모든 에셋을 빌드 파일에 담아서 배포
      • 장점: 게임 설치 파일에 이미 들어있기 때문에 설치 후 인터넷 없이도, 추가 다운로드 없이도 플레이할 수 있다.
      • 단점: 빌드 파일(.apk, .aab, .ipa 등)이 GB 단위의 용량을 쉽게 넘길 수 있다.
    • 모바일 스토어에서는 용량이 큰 빌드 파일의 업로드를 제한한다.
    • 원격 콘텐츠 배포: 에셋을 필요할 때 다운로드받아 동적 로드
      • 장점
        • 게임 설치 시 사용자의 부담이 크게 완화된다. 특히 200MB 초과 경고가 뜨면 설치하지 않는 사용자 비율이 유의미하게 늘어난다.
        • 사용자 기기 용량 부족 시 우리 앱이 삭제될 확률을 줄인다. 삭제할 앱을 찾을 때 용량이 큰 것부터 지우기 때문이다.
        • 앱 업데이트 없이 빠르게 에셋을 패치할 수 있다. 버그를 수정한 빌드를 스토어에 올리려면 심사가 오래 걸리므로 배포가 늦고 그동안 고객 문의 폭탄을 받을 것이다.
        • Lazy 다운로드가 가능하다. 튜토리얼 할 때부터 최종 콘텐츠를 받아 놓을 필요는 없다.
      • 단점
        • 프로그래머로서 관리가 까다롭다.
        • 게임 설치 후에도 추가 다운로드 시간과 셀룰러 데이터가 요구된다.
        • 새 버전 배포 시 CDN(콘텐츠 전송 네트워크)에도 새 에셋을 올리는 과정이 필요하다.
        • CDN 비용이 든다.

Unity 프로파일러

https://docs.unity3d.com/kr/2021.3/Manual/Profiler.html
https://docs.unity3d.com/kr/2021.3/Manual/OptimizingGraphicsPerformance.html
https://learn.unity.com/tutorial/diagnosing-performance-problems-2019-3?language=en&courseId=5c87de35edbc2a091bdae346#648abf6eedbc2a6ad72aff24

  • "과거에 진행했던 프로젝트에서 프로파일러를 써본 경험을 말씀해 주세요."

  • "모바일 기기의 발열량을 낮춰야 하는 이유는 무엇인가요?"

  • CPU, GPU, Garbage Collection Profiling 등이 가능하다.

  • 대부분은 Rendering 관련 이슈일 것이다. 이 경우 CPU가 병목인지 GPU가 병목인지 찾아야 한다.

    • Gfx.WaitForPresent 함수에서 병목이 생긴다면 이것은 CPU가 GPU 처리를 기다리고 있다는 뜻이다. 이때는 GPU가 병목이다.
    • 여기를 보면서 문제를 찾고 해결을 시도한다.
  • CPU가 병목인 경우 다음을 시도한다.

    • GPU에 보낼 오브젝트 수 줄이기
    • 3D 모델의 Transform 계층구조 최적화
    • 동적 조명과 실시간 그림자 피하기
    • 동일한 매터리얼 및 아틀라스 사용
    • Canvas가 변하는 빈도에 따라 UI 분리
  • GPU가 병목인 경우 다음이 문제일 수 있다.

    • Fill Rate
    • Overdraw
    • 메모리 대역폭
    • Vertex Processing
  • GC.Collect() 함수가 호출된 시점에서 병목이 생긴다면 여기를 살펴본다.

    • Physics가 병목인 경우 여기를 살펴본다.
    • 사용자 스크립트가 병목인 경우 여기를 살펴본다.
  • 성능 문제를 찾아 해결하고 싶다면 여기를 읽어본다.

  • 안드로이드 빌드를 만들고 모바일 하드웨어에서 프로파일링을 돌리는 방법

Unity 성능 최적화

중요!

Unity 6

https://unity.com/blog/unity-6-features-announcement https://unity.com/kr/blog/engine-platform/unity-6-preview-release

  • Unity 6가 이 글을 편집하는 중에 출시되었습니다.
    최신 동향을 묻는 질문으로 Unity 6에서 소개된 새 기능을 묻는 질문이 나올 수도 있습니다.

Unity 그래픽스

그래픽 렌더링 파이프라인

https://docs.unity3d.com/kr/2021.3/Manual/render-pipelines-overview.html

  • 종류

    • 빌트인 렌더 파이프라인: 예전 파이프라인. 커스터마이징 제한적
    • 스크립터블 렌더 파이프라인 (SRP)
      • 유니버설 렌더 파이프라인 (URP)
      • 고해상도 렌더 파이프라인 (HDRP)
      • 커스텀 렌더 파이프라인
  • 개발 초기에 파이프라인을 잘 선택하여 결정하는 것이 중요하다.

  • 과정

    1. 게임오브젝트의 트랜스폼을 월드 좌표계로 변환
    2. 카메라 시야 밖의 물체들을 렌더링 대상에서 제외
    3. 월드 좌표계를 카메라의 viewport 상 좌표로 변환
    4. 매터리얼의 셰이더 코드 실행하여 렌더링
    5. 렌더링한 결과를 '렌더 텍스처' 또는 '백 버퍼'에 저장
    6. '렌더 텍스처' 또는 '백 버퍼'에 있는 내용을 디바이스 화면에 출력

드로우 콜 최적화

https://docs.unity3d.com/kr/2021.3/Manual/optimizing-draw-calls.html
중요!

  • 드로우 콜: 그래픽스 API가 화면에 그릴 내용과 그릴 방법을 알려주는 것
    • 드로우 콜은 draw primitive call(메시 버텍스 계산)과 렌더 상태 설정을 합친 것이다.
  • 드로우 콜을 호출하기 전에 준비 단계가 있는데, 이때 GPU의 렌더 상태 변경(SetPass call: 다른 매터리얼로 전환 등)에 리소스가 많이 든다.
  • 렌더 상태 변경 수 줄이는 법
    • 드로우 콜 전체 수를 줄인다.
    • 동일한 렌더 상태(매터리얼 통일 등)를 사용하여 다수의 드로우 콜을 수행하는 경우 이들을 묶어 처리한다.
  • 드로우 콜과 렌더 상태 변경 최적화의 이점
    • 전력량과 발열량 감소
    • 유지보수성 향상: 더 많은 게임 오브젝트 추가 가능
  • 최적화 방법
    • GPU 인스턴싱: 동일한 메시 사본 여러 개를 동시에 렌더링
    • 드로우 콜 배칭(batching): 메시를 결합하여 드로우 콜 횟수 감소
      • 정적 배칭: 정적 게임 오브젝트의 메시를 미리 결합
      • 동적 배칭: 동일한 설정(동일한 수와 타입의 속성을 저장하는 버텍스)을 공유하는 메시 버텍스를 그룹화하여 한 번의 드로우 콜로 렌더링
    • 메시 수동 결합: 여러 메시를 단일 메시로 수동 결합
    • SRP 배처(batcher): 스크립터블 렌더 파이프라인 사용 시 활용 가능

배치 렌더링

  • 여러 개체를 한 번의 그래픽스 API 호출(드로우 콜)로 결합하여 한번에 렌더링하는 기법

  • 장점

    • 렌더링 함수 호출 횟수 감소: 동일한 매터리얼과 설정을 갖는 개체들을 그룹화하여 한 번의 API 호출로 그려내므로 오버헤드를 줄인다.
    • 그래픽 카드 친화적: 많은 수의 개별적인 요청보다 한 번의 큰 요청을 병렬적으로 처리하는 데 특화된 그래픽 카드의 성능을 최대한으로 활용한다.
    • FPS 향상
    • 메모리 사용량 감소: 호출 횟수가 줄어들기 때문에 남는 메모리를 다른 곳에 더 활용할 수 있게 된다.
  • 배치 렌더링 활용 방법

    • 매터리얼 공유
    • 레이어 정렬: Sorting Layers와 Order in Layer를 통해 개체의 그룹화를 관리할 수 있다.
    • GPU 인스턴스화: 동일한 메시와 매터리얼을 사용하는 여러 개체를 하나의 그래픽 API 호출로 처리할 수 있다.
  • 퀴즈: UGUI의 Image 컴포넌트의 Source Image가 None으로 설정되어 있으면 성능이 굉장히 낮아집니다. 이유는 무엇일까요? (클릭하면 펼쳐집니다.)
    • 이것이 None이면 배칭이 일어나지 않기 때문에 객체 하나 당 한 번씩의 드로우 콜이 추가된다.
      만약 이러한 Image 객체가 100개 있다면 100번의 드로우 콜이 추가되는 것이다.
      이는 Frame Debugger를 실행하고 실험해 보면 직접 확인할 수 있다.

    • 이를 방지하려면 단색 네모를 표현할 때 Source Image로 아무 스프라이트라도 지정해 주어야 한다.

    • Material이 None으로 설정되어 있는 것은 기본 매터리얼을 사용한다는 뜻이므로 괜찮다.

스프라이트 아틀라스

https://docs.unity3d.com/kr/2021.3/Manual/class-SpriteAtlas.html
중요!

  • 여러 개의 텍스처를 단일 텍스처로 결합하여 한 번의 드로우 콜로 처리하는 기법
  • 큰 성능 소모 없이 패킹된 텍스처에 동시에 접근할 수 있다.
  • 사용법
    • Asset > Create > Sprite Atlas 메뉴를 통해 *.spriteatlas 파일을 생성한다.
    • Objects for PackingTexture2D, 스프라이트 에셋, 스프라이트가 담긴 폴더를 넣으면 해당 스프라이트가 동일한 설정을 가지고 하나의 아틀라스로 묶이게 된다.
  • 아틀라스 성능 최적화
    • 스프라이트가 씬에서 활성화될 때 Unity가 해당 스프라이트가 속한 스프라이트 아틀라스 전체를 로드한다. 이 크기가 너무 크고 씬에서 이 아틀라스의 텍스처를 거의 사용하지 않는 경우 오버헤드가 크다.
    • 같은 씬에서 활성화하는 대부분의 스프라이트가 동일한 아틀라스에 속해 있도록 하는 것이 좋다.
    • 아틀라스 팩 미리보기 기능을 통해 빈 공간이 과도하게 있는지 확인하고 줄이면 좋다. Max Texture Size를 줄이면 스프라이트 텍스처 크기를 줄이지는 않고 이 크기에 맞게 아틀라스의 빈 공간을 최대한 잘라낸다.
    • 한 아틀라스에 묶인 텍스처의 매터리얼을 통일한다.
    • 아틀라스의 크기를 2의 제곱수 크기(POT: power of two)로 맞춘다.

텍스처

https://docs.unity3d.com/kr/2021.3/Manual/Textures.html

  • "과거에 진행한 프로젝트에서 텍스처 압축 방식은 어떤 것을 사용했나요?"

  • 3D 오브젝트의 메시 표면에 걸쳐 적용되는 비트맵 이미지

  • 텍스처는 매터리얼을 사용해 오브젝트에 적용할 수 있고, 매터리얼은 셰이더를 사용해 메시 표면의 텍스처를 렌더링한다.

  • 텍스처는 2의 제곱수 크기(POT)로 만들어야 한다.

    • 예: 32x32, 64x64, 128x128, 256x256
    • 정사각형이 아니어도 된다.
  • 2의 제곱수 크기가 아니면(NPOT) 다음의 문제가 생긴다.

    • 모바일 기기 또는 텍스처 압축 방식에 따라 POT로 변환한 후, NPOT 원본과 변환된 POT를 둘 다 로드한다.
    • 변환 시간과 로드 시간이 오래 걸리고 메모리도 많이 잡아먹는다.
  • 텍스처 압축 포맷

    • 대표적인 압축 포맷으로 DTX5, ASTC, ETC2 등이 있다.
    • 플랫폼 및 기기에 따라 사용해야 하는 텍스처 압축 포맷이 다르다.
      • 기기에서 지원하는 포맷을 사용하면 별도의 변환 없이 바로 GPU에서 처리할 수 있다.
      • 기기에서 지원하지 않는 포맷을 사용하면, 비압축 포맷으로 변환한 다음, 압축된 원본과 비압축 포맷을 둘 다 로드한 상태에서 비압축 포맷을 처리한다.
        경우에 따라서는 앱 시작 시 30초 이상 검은 화면에서 머물러 있는 경우도 발생할 수 있다.
      • 같은 플랫폼이라도 구형 기기에서는 지원하는 포맷이 다를 수 있다.
    • Unity에서는 플랫폼마다 다른 기본 텍스처 포맷을 제공한다.
      • 텍스처에 대해 잘 모르면 기본 텍스처 포맷을 사용해도 무방하다.
      • 최적화가 필요하면 텍스처 압축 포맷을 공부해야 한다.
    • https://docs.unity3d.com/kr/2023.1/Manual/class-TextureImporterOverride.html
    • https://docs.unity3d.com/kr/2023.1/Manual/texture-compression-formats.html
    • https://docs.unity3d.com/kr/2018.4/Manual/class-TextureImporterOverride.html
  • 텍스처는 2D 스프라이트, 메시, 파티클 시스템, GUI, 지형 높이 맵(Terrain Heightmap)에 사용된다.

텍스처 타입 및 임포트에 대한 자세한 내용 (클릭하면 펼쳐집니다.)
  • 텍스처 타입
    • Default
    • 노멀 맵: 컬러 채널을 실시간 노멀 매핑에 적합한 포맷으로 변환할 때 사용
    • 에디터 GUI 및 레거시 GUI: HUD 또는 GUI 컨트롤에서 사용
    • 스프라이트(2D 및 UI)
    • 커서
    • 쿠키: 빌트인 렌더 파이프라인에서 씬의 광원 쿠키로 사용
    • 라이트맵: 특정 포맷으로 인코딩이 가능해지고 텍스처 데이터에 대해 포스트 프로세싱 단계 수행 가능
    • 단일 채널: 하나의 채널만 필요한 경우
  • 텍스처 임포트 시 고려 사항
    • 노멀 맵
      • 노멀 맵 셰이더에서 로우 폴리곤 모델에 디테일이 더 많이 포함된 것처럼 보이게 하는 데 사용
    • 알파 맵
      • 알파(투명도) 정보만 포함하는 텍스처
    • 디테일 맵
      • 지형(terrain)에서 주 텍스처가 가까워질 때 작은 디테일을 페이드 인 하여 텍스처가 흐릿하게 보이는 현상 방지
    • 큐브 맵 (반사)
    • 이방성 필터링
      • Aniso 레벨을 높이면 지표각에서 보이는(기울인) 텍스처 품질 향상
      • 바닥과 천장 텍스처에 사용하면 좋다.

Mip Map & LOD

https://docs.unity3d.com/kr/2021.3/Manual/texture-mipmaps-introduction.html

  • 밉맵 (Mip map)
    • 원본 텍스처에서 2의 거듭제곱만큼 가로와 세로 크기를 축소한 낮은 해상도의 텍스처 버전
    • 3D 씬에서 오브젝트를 렌더링할 때, 더 높은 mip 레벨(고해상도)은 카메라에 가까운 오브젝트에 사용되고, 더 낮은 mip 레벨(저해상도)은 더 먼 오브젝트에 사용된다.
      • 매번 새로 샘플링(크기 조절)하지 않고 미리 캐시해 놓는 것
      • 렌더링 작업 속도를 늘리고 렌더링 아티팩트를 줄일 수 있다.
    • 밉맵을 사용하면 전체 텍스처 용량을 33% 늘린다.
      • 항상 같은 크기로만 렌더링되는 UI 텍스처 등은 밉맵을 사용하지 않는 것이 유리하다.

https://docs.unity3d.com/kr/current/Manual/LevelOfDetail.html
https://docs.unity3d.com/kr/current/Manual/class-LODGroup.html

  • Level Of Detail (LOD)
    • 카메라와 3D 오브젝트 사이의 거리에 따라 오브젝트의 메시를 얼마나 자세히 표현할지를 정의한 것이다.
    • 아주 멀리 있어서 작게 보이는 3D 메시의 렌더링해야 할 버텍스 수를 줄여 성능을 최적화한다.

객체지향 프로그래밍

네 가지 속성

  • 추상화
    • 하나의 객체가 하나의 역할을 맡도록
    • 역할과 구현의 분리
    • 인터페이스 / 추상 클래스
  • 상속
    • 클래스 간 위계질서 만들기
    • 코드의 공통된 부분을 재사용 가능하게
  • 캡슐화
    • public, protected, private
    • Property (getter, setter)
  • 다형성
    • 상위 클래스 타입으로 하위 클래스 조작 가능
    • 함수 오버로딩, 함수 오버라이딩
    • 코드의 반복 줄임

5원칙 (SOLID 원칙)

  1. 단일 책임 원칙 (Single Responsibility Principle)
    • 하나의 객체는 하나의 역할(책임)만을 가져야 한다.
    • 추상화와 관련
  2. 개방-폐쇄 원칙 (Open-closed Principle)
    • 수정에 닫혀 있고 확장에 열려 있어야 한다.
    • 캡슐화, 상속, 다형성과 관련
  3. 리스코프 치환 원칙 (Liskov Substitution Principle)
    • 자식 클래스는 부모 클래스로 대체할 수 있어야 한다.
    • 상속, 다형성과 관련
  4. 인터페이스 분리 원칙 (Interface Segregation Principle)
    • 필요하지 않은 기능을 의존하도록 강요하지 않아야 한다.
    • 인터페이스의 모든 메서드를 구현하도록 강요하지 않아야 한다. (강요라고 느끼지 않게 필수적인 것만 인터페이스에 둔다.)
    • 추상화와 관련
  5. 의존성 역전 원칙 (Dependency Inversion Principle)
    • 추상적인 것에 구체적인 것이 의존해야 한다. 구체적인 것에 추상적인 것이 의존하면 안 된다.
    • 부모 클래스에 자식 클래스가 의존해야 한다. 반대가 되면 안 된다.
    • 추상화, 상속과 관련
    • 종속성 주입을 적용하면 이 원칙을 지키는 데에 도움이 된다.
  • 소프트웨어의 유지보수성, 재사용성, 확장성을 높이기 위해 이 원칙들을 지키면 좋다.

interface vs. abstract class vs. virtual class

  • "인터페이스를 언제 사용하면 좋은가요?"

  • 인터페이스와 추상 클래스와 가상 클래스의 차이를 직접 찾아보고 답해보시기 바랍니다.

C# 다형성

  • 자식 클래스가 부모 클래스의 메서드를 override하거나 new 키워드로 숨길 수 있다.
  • Avirtual 클래스이고 BA를 상속하는 자식 클래스이며 둘이 같은 이름의 메서드를 구현하고 있을 때, 다음의 경우에 동작이 다르다.
    • A a = new A();
      • A의 메서드가 호출된다.
    • A a = new B();
      • B의 메서드를 override로 정의한 경우, B의 메서드가 호출된다.
      • B의 메서드를 new로 정의한 경우, A의 메서드가 호출된다.
    • B b = new B();
      • B의 메서드가 호출된다.
    • A a = new B(); B b = (B) a;
      • B의 메서드를 new로 정의한 경우, B의 메서드가 호출된다.
      • 아무튼 B의 메서드가 호출된다.

C# VTable

https://ko.wikipedia.org/wiki/%EA%B0%80%EC%83%81_%EB%A9%94%EC%86%8C%EB%93%9C_%ED%85%8C%EC%9D%B4%EB%B8%94
https://www.csharpstudy.com/DevNote/Article/28

  • "virtual class와 이를 상속한 클래스가 있고 자식 클래스에서 부모 클래스의 메서드를 override했을 때, 이 두 메서드의 주소가 메모리에서 어떤 자료구조로 관리되나요?"

  • Virtual table(VTable)은 가상 메서드(virtual 또는 abstract)를 갖는 클래스를 상속하여 해당 메서드를 override할 때 생긴다.

    • 메서드 포인터를 저장하는 배열이다.
    • Heap 상 객체의 Type Handle이 가리키는 곳의 Method Table 메타데이터 안에 들어있다.
  • 클래스의 객체가 생성될 때, 컴파일러가 이 VTable에 대한 포인터(vpointer)를 객체의 숨은 멤버로 추가한다.

  • C#의 모든 클래스는 System.Object의 자식이고 4개의 가상 메서드(ToString(), Equals(), GetHashCode(), Finalize())를 자신의 VTable 안에 가진다.

    • 여기에 추가로, 자신의 부모 클래스가 가진 가상 메서드를 자신의 VTable 안에 가진다.
    • 가장 부모의 것부터 자식 클래스 자신의 메서드까지 계층 순서대로 메서드 포인터 슬롯을 갖게 된다.
  • 메서드 override 시 자식 클래스의 VTable에는 해당 메서드가 부모의 것 대신의 자신의 것으로 들어간다. 부모의 메서드는 자식의 VTable에 남아있지 않다.

  • 메서드를 new로 숨길 시 자식 클래스의 VTable에는 해당 메서드가 부모의 것과 자신의 것 모두 들어있다.

디자인 패턴

중요!
디자인 패턴 각각을 완벽하게 외우기보다, 아래 질문에 대해 생각해보면 좋습니다.

  • "디자인 패턴을 적용해본 적이 있나요?"
  • "디자인 패턴을 따로 외우고 익혀야 한다고 생각하나요?"
  • 클래스 간 종속성을 줄이는 것이 왜 중요한가요?

Singleton 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/SingletonPattern.md

  • "멀티스레드 환경에서 싱글톤을 써본 적이 있나요? 어떤 점을 고려해야 하나요?"
  • "싱글톤 패턴을 가급적 사용하지 않아야 하는 이유는 무엇인가요?"
    • 클래스 간 종속성을 줄이는 것(디커플링)의 장점에 대해 생각해 보세요.

Null Object 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/NullObjectPattern.md

  • 함수의 반환값으로 null 대신 null과 같은 역할을 하는 dummy 오브젝트를 생성하여 반환한다.
  • Dummy 오브젝트를 받으면 아무 연산도 수행하지 않도록 구현한다.
  • null 체크를 안 해도 된다.

Dependency Injection

https://learn.microsoft.com/ko-kr/dotnet/core/extensions/dependency-injection
https://medium.com/@avinash.dhumal/understanding-dependency-injection-a-practical-guide-with-c-examples-aee44eacee32
https://learn.microsoft.com/ko-kr/dotnet/architecture/modern-web-apps-azure/architectural-principles#dependency-inversion

  • 신입 입사 면접에서 물어볼 가능성은 낮지만, 실무에서 굉장히 자주 사용되므로 공부할 수 있을 때 공부하고 익히는 것을 추천합니다.

  • 개인에 따라 dependency injection을 디자인 패턴으로 취급하지 않기도 합니다.

  • 클래스 A가 다른 클래스 B의 인스턴스를 필요로 할 때(종속성이 있을 때), 클래스 A의 코드에 하드코딩하여 B의 인스턴스를 생성하지 않고, A의 생성자의 인자를 통해 외부에서 생성된 B의 인스턴스를 주입받도록 하는 설계이다.

    • 만약 B의 인스턴스 대신 B와 같은 인터페이스(또는 부모 클래스)를 구현하는 클래스 C의 인스턴스를 A가 필요로 한다면, 종속성 주입을 통해 A의 코드를 고치지 않고도 A가 B 또는 C의 인스턴스 중 하나를 자유롭게 종속성으로 가지도록 만들 수 있다.
    • A가 갖는 종속성 B가 다른 종속성(D, E, ...)을 가질 때에도 종속성 주입을 사용하면 종속성을 차례로 해결하여 완전한 종속성 그래프를 A에게 반환한다.
  • 객체 지향 프로그래밍의 5원칙 중 '의존성 역전 원칙'을 해결하는 데에 도움을 준다.

  • 제어 반전(inversion of control)을 일으킨다.

    • 일반적인 제어의 흐름은 프로그래머가 외부 라이브러리 메서드를 호출하는 것이지만, 제어 반전이 일어나면 외부 라이브러리에서 프로그래머가 주입하는 인스턴스를 참조한다.
더 많은 디자인 패턴 알아보기 (클릭하면 펼쳐집니다.)

Strategy 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/StrategyPattern.md

  • "Strategy 패턴과 Dependency Injection 패턴의 차이점이 무엇인가요?"

  • 추상화된 인터페이스를 두어서 구현이 바뀌거나 교환되더라도 호출에서 코드를 수정하지 않아도 되게 하는 방법

  • 예: Interact() 하나로 Portal.Interact(), Alter.Interact(), Monster.Interact() 등을 수행할 수 있게 하는 방법

Proxy 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/ProxyPattern.md

  • 포장을 통해 접근하고, 그 내부는 다를 수 있는 패턴.

Facade 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/FacadePattern.md

  • [퍼사드]라고 읽는다.
  • 중간에 인터페이스(facade)를 하나 두어, 실제 worker(클래스)들이 하는 일을 적당히 숨기면서도 간단한 인터페이스로 worker들에게 일을 줄 수 있는 패턴

State 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/StatePattern.md

  • FSM(finite state machine)을 만들 때 주로 사용
  • 여러 상태를 인터페이스로 묶어서 각 상태 별로 해당 상태에서 할 수 있는 일(함수)을 정의해주는 패턴
  • 모든 state 패턴은 strategy 패턴이지만 역은 성립하지 않는다.
    • State 패턴에서는 상태 파생 클래스들이 일할 때 context에 대한 참조를 갖는다. 그러나 strategy 패턴에서는 이러는 경우가 없다.

Adapter 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/AdapterPattern.md

  • 호환되지 않는 클래스의 인터페이스를 클라이언트(호출자)와 호환되도록 중간에 인터페이스를 두어 함수 시그니처 등을 변환해주는 패턴

Observer 패턴

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DesignPattern/ObserverPattern.md

  • 상태 변화를 감지하는 이벤트 함수 등을 구독하는 옵저버들을 만들고, 상태 변화가 생길 때 자신을 구독하고 있는 옵저버들에게 신호를 보내 추가적인 업데이트를 할 수 있도록 한다.
  • 콜백 함수를 만들 때 유용하다.
  • 장점
    • 결합이 느슨해진다.
  • 단점
    • 복잡해지고 느려진다.
    • 멀티스레딩 환경에서 실행과 구독 취소가 동시다발적으로 발생하면 버그가 쉽게 생긴다.

운영체제

Note

게임 클라이언트가 아닌 개발 직군에서는 많이 묻지만, 게임 클라이언트에서는 잘 묻지 않는 것 같기도 합니다.
학부 졸업생이 신입으로 입사하는 경우에는 물어볼 확률이 높습니다.

리틀 엔디언 vs. 빅 엔디언

  • 빅 엔디언
    • 16진수 int 1A2B3C4D를 주소가 낮은 위치부터 높은 위치 순서대로 1A, 2B, 3C, 4D 순으로 메모리에 저장한다.
    • 사람이 읽기 쉽다.
    • 컴퓨터가 읽기 어렵다.
  • 리틀 엔디언
    • 16진수 int 1A2B3C4D를 주소가 낮은 위치부터 순서대로 4D, 3C, 2B, 1A 순으로 저장한다.
    • 사람이 읽기 어렵다.
    • 사칙연산에 유리하다.
    • x86 시스템은 리틀 엔디언을 사용한다.
  • ARM 같은 곳에서는 둘 다 사용하기도 한다.

프로세스 vs. 스레드

  • 프로세스: OS의 작업 단위
    • 스택, 힙, 코드, 데이터를 모두 복사해 가진다.
    • 다른 프로세스와 독립적으로 돌아간다. (공유 메모리를 사용하지 않는다면)
    • context switching 등이 무겁다.
  • 스레드: 프로세스 내에서의 실행 단위
    • 스택만 복사하고 나머지 자원은 같은 프로세스 내에서 여러 스레드가 모두 공유한다.
    • 가볍게 만들고 없앨 수 있다.
  • 멀티프로세스 vs. 멀티스레드
    • 프로세스보다 스레드가 가볍기 때문에 멀티스레드를 더 자주 사용하게 된다.
    • 멀티프로세스든 멀티스레드든 동기화 이슈는 중요하다.

Memory Fragmentation

중요!

  • 외부 단편화
    • 남은 메모리 총합은 충분한데 각각이 다 쪼개져 있어 하나의 큰 메모리 공간을 할당할 수 없는 경우
  • 내부 단편화
    • 많이 할당해놓고 쓰지 않는 경우
    • 페이징할 때 발생하기도 한다.
  • 외부 단편화 해결책
    • 페이징
      • 디스크 등의 보조 기억 장치를 활용해 메모리 일부를 일정한 페이지 단위로 쪼개 거기에 옮겨 놓고, 다시 불러오고 하는 방법
      • 어느 주소에 있는지 기억해야 하므로 페이지 테이블을 관리해야 한다. 페이지 테이블은 가상 페이지 주소와 물리 메모리(디스크에 있는 경우 메모리의 프레임에 먼저 로드함) 상의 프레임 주소를 연결하는 정보를 가지고 있다.
      • 디스크까지 내려가는 데 너무 시간이 오래 걸리므로 TLB 등의 버퍼를 활용한다. TLB는 페이지 테이블에 대한 캐시이다.
      • https://ko.wikipedia.org/wiki/%ED%8E%98%EC%9D%B4%EC%A7%95
    • 압축 (조각 모음)
      • 오래 걸린다.
    • 통합
      • 근처에 있는 메모리를 하나로 연결한다.
  • 내부 단편화 해결책
    • 세그멘테이션
      • 변동 크기로 잘라 관리한다.
      • "필요한" 만큼만 할당한다.
      • 외부 단편화가 생길 수 있으므로 그냥 이럴 바에는 페이징을 한다.
  • 두 단편화를 모두 해결하는 방법
    • 메모리 풀 사용
      • 매 번 새로운 메모리를 할당하는 것이 아니라, 풀(pool)에서 메모리를 빌려주고 다시 반환받으면 재사용할 수 있도록 하는 방법이다.
      • 필요한 만큼만 할당하므로 내부 단편화가 일어나지 않는다.
      • 메모리를 할당 해제한 후에 재사용할 수 있으므로 외부 단편화가 일어나지 않는다.

가상 메모리

  • 메모리 가상화를 하는 이유
    • 사용자에게는 메모리가 무한한 것처럼 보여준다.
    • 실제로는 보조 기억 장치(디스크 등)를 활용하여 부족한 메모리 공간을 관리한다.
  • MMU(Memory Management Unit)을 통해 관리
  • Page fault
    • 원하는 페이지가 메모리에 없고 디스크에 있을 때 발생
  • 페이지 교체 정책
    • 메모리에 남길 페이지와 디스크로 보낼 페이지를 결정한다.
    • LRU (Least Recently Used)
  • TLB (Translation Lookaside Buffer)
    • 자주 쓰이는 페이지의 물리 주소를 기억한다.

Mutex & Semaphore

  • Mutex
    • 하나의 스레드가 mutex(lock) 객체를 갖는다.
    • 다른 스레드나 프로세스는 누군가 이 mutex를 가지고 있는 동안 접근할 수 없다.
    • 사용이 끝나면 mutex를 놓는다.
    • Binary semaphore라고도 한다.
  • Counting Semaphore
    • 둘 이상의, 정해진 수의 스레드가 동시에 접근할 수 있다.
    • 카운터를 두고 있으며, 이것이 0이 되면(정해진 수의 스레드가 사용 중이면) 더 들어갈 수 없다.
      • 사용할 때 카운터를 1 내린다.
      • 사용이 끝나면 카운터를 1 올린다.
    • try를 뜻하는 P(사용할래요!)와 increment를 뜻하는 V(사용 끝났어요!)를 사용한다.
      • P - critical section - V 순으로 사용해야 한다.
    • 잘못 사용하면 데드락이 되거나 상호 배제에 실패할 수 있다.
  • 사용 권한을 얻을 때까지 무한 루프를 돌면서 기다리거나, 이보다 효율적으로는 스레드를 sleep했다가 다른 스레드가 사용 권한을 놓을 때 sleep한 스레드를 깨우는 방식으로 구현할 수 있다.

Deadlock (교착 상태)

중요!

  • A를 잡은 스레드가 B를 갖고 싶어하고, B를 잡은 스레드가 A를 갖고 싶어하는데, A와 B 모두 상호 배제가 필요한 자원이고, 서로가 자신이 가진 것을 놓을 생각이 없다면 데드락이 발생한다. 이때 누구라도 A와 B를 모두 잡는 경우는 평생 생기지 않는다.
  • 다음 네 가지 조건을 모두 만족해야 데드락이 발생한다.
    1. 어떤 자원에 대해 상호 배제가 필요하다.
    2. 한 자원을 잡으면서 다른 자원을 기다리는 상황이 있다.
    3. 선취 불가능하다(non-preemptive). 다른 프로세스를 종료시킬 수 없다.
    4. 자원을 얻고자 하는 프로세스를 방향이 있는 그래프로 나타낼 때 사이클이 존재한다.
    • 예: 어떤 곳에서는 A -> B 순으로 mutex를 잡고, 다른 곳에서는 B -> A 순으로 mutex를 잡는다면 사이클이 발생하여 데드락에 걸릴 수 있다.
  • 위의 조건 중 하나라도 해결하면 데드락이 풀린다.
    • 조건 1.은 없앨 수 없다. 상호 배제를 안 해도 된다면 mutex를 쓸 이유가 없다.
    • 대부분은 조건 4.의 사이클을 제거하여 해결한다.
    • 조건 3.에 대해 선취 가능하게 만들어 해결하는 방법도 있다.
  • 데드락이 발생하면 해당 프로세스들을 차례로 강제 종료하여 해결해야 한다.

CPU 스케줄러 알고리즘

  • FCFS: First Come First Serve
    • 먼저 온 것부터 먼저 처리
    • Non-preemptive하다.
    • 긴 프로세스가 오면 짧은 프로세스를 오랫동안 실행할 수 없다.
  • SJF: Shortest Job First
    • 가장 짧은 일을 우선적으로 처리한다.
    • Preemptive하게 할 수도, 아니게 할 수도 있다.
    • Preemptive한 경우 긴 프로세스는 계속 처리되지 못한다. (Starvation)
    • 얼마나 걸릴지를 예측해야 하는데, 이전에 실행했던 프로세스의 예상 수행 시간과 실제 수행 시간을 바탕으로 예측한다.
  • Priority
    • 프로세스마다 나름의 우선순위를 둔다.
    • 우선순위가 높은 프로세스가 오면 하던 걸 멈추고 그걸 먼저 한다.
      • Preemptive하다.
    • 역시 starvation 문제가 있고, 이를 해결하기 위해 오랫동안 실행이 안 되면 aging을 도입해 우선순위를 조금씩 높여준다.
  • RR: Round Robin
    • 일정 시간 단위를 정하고 이보다 넘어가면 무조건 다른 프로세스로 바꿔 실행한다.
    • 시간 단위가 q이고 N개의 프로세스가 있으면 한 프로세스가 (N-1) * q 이상 기다리는 경우는 없다.
    • 우선순위를 부여하지 않는다.
  • SRTF: Shortest Remaining Time First
    • 가장 짧게 남은 프로세스를 먼저 처리한다.
    • Preemptive하다.
    • 새 프로세스가 들어올 때마다 스케줄을 다시 계산한다.
    • 역시 starvation 문제가 있다.
    • CPU burst time을 측정하기 어렵다.

32비트 vs. 64비트 운영체제

  • "32비트 운영체제와 64비트 운영체제의 차이가 무엇인가요?"

  • 둘의 차이는 사용할 수 있는 RAM의 크기이다.

    • 32비트는 최대 4GB(= $2^{32}$ bytes)의 메모리만 인식한다.
      Windows에서는 x86으로 불린다.
    • 64비트는 최대 16EB(= $2^{64}$ bytes)의 메모리를 인식한다. Windows에서는 x64로 불린다.
  • 64비트 운영체제에서 64비트 프로그램을 돌리는 것이 32비트 프로그램을 돌리는 것보다 당연히 빠르다.

  • 갤럭시 S24 등의 최신 모바일 기기에서는 32비트 .apk를 실행할 수 없다.

데이터베이스

Key

  • Key: attributes의 집합.
  • Candidate key: 유일성과 최소성을 만족하는, primary key가 될 수 있는 모든 key의 집합.
  • Primary key: candidate key 중 하나로, 모든 레코드를 구분할 수 있으며 NULL일 수 없다.
  • Alternate key (Unique key): candidate key 중 primary key가 아닌 것들
  • Superkey: 유일성은 만족하지만 최소성을 만족하지 못하는 attributes의 집합
  • Foreign key: 다른 릴레이션(표)의 레코드를 참조하기 위해 그 레코드의 primary key를 내 릴레이션에 두는 것

정규형

https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview/blob/main/DB/%EC%A0%95%EA%B7%9C%ED%98%95.md

  • DB 설계에 도움이 되는 내용입니다.

네트워크 & 통신

OSI 7계층

https://www.cloudflare.com/ko-kr/learning/ddos/glossary/open-systems-interconnection-model-osi/

  • 직군에 상관없이 물어볼 수 있는, 네트워크에 대한 기초 내용입니다.

멀티플레이어 게임의 구조

https://docs-multiplayer.unity3d.com/netcode/current/terms-concepts/network-topologies/
https://www.photonengine.com/ko-kr/fusion

  • "게임 서버, 멀티플레이어 게임 또는 웹·앱 백엔드를 구현해 본 경험이 있나요?"

  • 각 토폴로지의 장점과 단점을 공부하고 어떤 상황에 어떤 토폴로지가 적절한지 말할 수 있으면 좋습니다.

  • 네트워크 토폴로지

    • 전용 서버(dedicated server)
    • 플레이어 호스트
    • 분산 권한(distributed authority)

원격 프로시저 호출 (RPC)

  • "플레이어가 재화를 획득하는 로직을 서버에 두지 않고 클라이언트에 두면 어떤 장점과 단점이 있나요?"

  • "클라이언트가 요청을 보내지 않고도 서버에서 일방적으로 메시지를 클라이언트에게 보내는 경우가 있다면, 클라이언트에서 이 메시지를 받기 위해 어떻게 구현해야 할까요?"

  • "RPC 요청을 보냈는데 응답을 받는 과정에서 연결이 끊겨 서버가 이를 처리했는지 클라이언트가 알 수 없는 경우가 있습니다. 이러한 상황에서 요청이 서버에서 단 한 번만 처리됨을 보장하려면 어떻게 해야 하나요?"

  • "실시간 온라인 게임에서는 서버에 요청을 보내고 응답을 받을 때까지의 지연 시간이 짧을수록 좋습니다. 클라이언트에서 반응성을 높이기 위한 방법을 제안해 보세요."

  • RPC가 무엇의 약자인지, 어떤 상황에서 필요한지 등을 공부하면 좋습니다.

  • RPC와 REST의 차이

About

게임 클라이언트(Unity) 개발자 기술면접 준비에 도움이 되고자 작성한 글입니다.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published