최초 작성일: 2024년 7월
최종 편집일: 2024년 12월
본 자료를 자유롭게 배포하셔도 좋습니다.
배포 시에는 작성 기여자를 명시해 주세요.
단, 상업적 용도로는 사용하실 수 없습니다.
본 자료의 2부 내용에는 오류가 있을 수 있습니다.
오류 수정 제안이나 내용 추가는 언제든지 환영입니다!
Pull Request를 날려주시면 검토 후 반영하겠습니다. 😊
회사마다 다를 수 있으므로, 들어가려는 회사의 채용 사이트를 꼭 확인해 보시기 바랍니다.
- 서류 심사 (이력서, 자기소개서, 포트폴리오 등)
- 코딩 테스트 또는 과제 전형
- 실무진 기술면접
- 경영진 인성면접
- 처우 협의
보통 1시간 정도 소요됩니다.
- 자기소개 + 아이스 브레이킹
- 제출한 서류의 내용 검증 + 경험 질문 + 과제에 대한 질문
- 기술 질문
- 회사에 대해 궁금한 점 (역질문)
Note
본 장의 내용은 어디까지나 제 경험에서 비롯한 "개인적 감상"이며, 주관적인 내용이 다수 포함되어 있습니다.
이는 모든 면접에 일반화되는 내용은 아닐 수 있습니다.
저의 경우 대학원 졸업 후 전문연구요원으로 지원하였으므로, 경력자나 학부 졸업자(신입)를 뽑는 면접과 상이할 수 있습니다.
참고만 하시기 바랍니다.
알고리즘 문제를 푸는 코딩 테스트와는 다릅니다.
회사에 따라 다르지만, Unity를 사용해 짧은 시간(2시간 ~ 1주일) 동안 처음부터 게임을 개발할 것을 요구하는 전형에 대한 감상입니다.
-
내공이 없으면 통과할 수 없다.
- 문제 분석 및 이해, 코드 구조 설계, Unity C# 스크립팅, 알고리즘, 사용자 입력 처리, 시각화(애니메이션) 등을 종합적으로 요구한다.
- 허수인 지원자를 가장 확실하게 거르는 전형이다.
알고보니 나도 허수였지만... - 편법으로 회사에서 내는 과제의 정보를 미리 입수하고 연습해 볼 수도 있겠지만, 이렇게 하면 합격에는 도움이 될지 몰라도 회사 가서 적응하기가 힘들 것이다.
-
단순 구현 문제처럼 보여도 알고리즘 문제 풀이 기술을 요하도록 설계되어 있다.
- 다행인 점은, 프로그램 구현을 충분히 많이 해본 사람이라면 따로 알고리즘 공부를 해서 갈 필요는 없다.
-
필수 스펙을 시간 안에 전부 구현하는 사람도 매우 드물 것 같은데 선택 스펙도 굉장히 많이 달려있다.
- 이런 점이 '누군가는 이걸 다 해내는데, 당신은 과연 다 할 수 있을까요?' 하는 무언의 압박을 준다.
-
라이브 구현 테스트의 경우 시간이 매우 부족하므로 문제를 보고 첫 번째로 떠올린 설계로 끝까지 가야 한다.
- 설계하는 데에 시간을 쏟다가는 제 시간 안에 구현을 다 못 한다.
- 첫 설계가 괜찮은 설계이려면 정말 수도 없이 많은 프로젝트를 바닥부터 짜 봤어야 한다.
- 인터넷 검색이 가능하더라도 검색할 시간이 아깝다. 손에 익은 구현 기술만으로 자급자족할 수 있어야 한다.
-
짧은 시간 안에 게임 하나를 바닥부터 개발해서 혼자 완성하는 연습을 많이 해봐야 한다.
-
기술면접은 지원자의 내공을 측정하는 면접이 아니다.
- 단기적인 단순 암기로 넘길 수 있는 면접이다.
- 즉, 지원자가 우리 회사에 들어오기 위해서 '최근에' 공부를 하는 열의를 보였는가를 평가하는 자리이다.
- 학교에서 가르쳐 준 적 없는 내용이 대부분이므로, "개발 많이 해 봤는데 나 정도면 충분히 붙겠지~" 하고 아무 준비 없이 가면 아무 대답도 하지 못할 것이다.
- 뒤에 나올 내용들을 공부하고 가면 된다.
-
단순한 지식을 묻는 질문보다, 본인이 직접 사용해 보고 경험해 보았는지 묻는 질문이 많이 나온다.
- 아래 자료를 공부하면서, 시간을 들여 실습을 함께 진행할 것을 강력히 추천한다.
- 외우기만 한 것과 써본 경험이 있는 것은 답변에서 그 차이가 드러난다.
-
기술면접에서도 인성면접에서 할 법한 질문이 많이 나온다.
- 자신이 쓴 서류의 내용에 대해 완벽히 이해하고 설명할 수 있어야 한다. ⭐
- 자신이 했다고 서류에 적은 내용 중 어느 하나라도 기억나지 않는 것이 있다면 장담건대 100% 떨어진다.
설명할 수 없는 경험은 서류에서 과감히 빼야 한다.
- 자신이 했다고 서류에 적은 내용 중 어느 하나라도 기억나지 않는 것이 있다면 장담건대 100% 떨어진다.
- 회사의 인재상에 대해 숙지하고 가야 한다.
- 평소에 "왜?"에 대한 질문을 많이 던져봐야 한다.
- "왜 대학원 안 가고(또는 그만두고) 회사로 왔나요?"
- "다른 개발 직군 많은데 왜 게임 업계로 왔나요?"
- "왜 기획자도 서버 프로그래머도 아닌 클라이언트 프로그래머로 지원했나요?"
- "왜 우리 회사여야 하나요?"
- "회사에서 가장 배우고 싶은 것이 무엇인가요?"
- "팀원과 갈등을 겪었던 경험이 있나요?"
- "기획자가 정말 말도 안 되는 것을 꼭 구현해야 한다고 주장하고 이를 굽히지 않을 때, 프로그래머로서 어떻게 대처할 것인가요?"
- 자신이 쓴 서류의 내용에 대해 완벽히 이해하고 설명할 수 있어야 한다. ⭐
-
편하게 진행한다고 하지만 지원자 입장에서 암묵적인 압박을 받는다는 느낌이 들 수 있다.
- 지원자 스스로 굉장히 자랑스럽게 여기고 잘 했다고 생각하는 것에 대해 "이런 부분이 아쉬운데 더 잘 할 수는 없었을까요?" 라고 묻는다.
-
뽑는 입장이 한 번이라도 되어본 적이 있다면, 회사에서 필요한 사람이 바로 자신임을 어필하는 것이 중요하다는 것을 알 수 있다.
- 모든 것은 직무 적합성으로 통한다.
- 내가 이 직무에 있어서 뛰어난 사람이라는 것을 마음껏 자랑하자.
- 회사마다 다르지만, 신입을 뽑을 때에는 지원자가 새로운 것을 학습하는 속도를 현재 가진 기술 및 지식의 수준보다 더 중요하게 본다.
- 소통, 협업 능력 및 사회성도 중요하다.
- 내가 하고 싶은 이야기보다 면접관이 듣고 싶어하는 이야기를 하는 것이 좋다.
- 예: 피아노를 화려하게 정말 잘 치는데 지휘자를 보지 않는 반주자는 팀에서 원하는 반주자가 아닐 것이다.
- 예: "전 나중에 창업을 하고 싶습니다!"라고 말한다면 면접관은 '금방 나갈 사람이구나' 하고 생각한다.
- 다른 지원자에게서 들을 수 없는 자신만의 흥미로운 이야기가 있으면 좋다.
- 돈에 대한 이야기는 면접이 아닌 처우 협의 단계에서 하는 것이 좋다.
- 면접관을 미소 짓게 만들 줄 아는 호감형 사람들이 더 유리할 것이다.
- 확실한 것은, 같이 있을 때 기분이 나빠지는 사람은 동료들의 생산성을 저하시키므로 뽑지 않을 것이다.
- 회사를 칭송하는 아부를 많이 할 필요는 없다.
- 이것이 지원자를 뽑아야 할 이유가 되지는 않는다.
- 거짓말은 하면 안 된다. ⭐
- 예: 활동적인 일을 좋아하지 않는데 "활동적인 일은 저에게 맡겨 주십시오!"라고 말하고 면접을 통과했다면 회사 가서도 원하지 않는 활동적인 일만 계속 맡게 될 것이다.
- 면접에서 거짓말을 하는 사람은 언제든 거짓말을 할 수 있는 사람이라는 인식을 갖게 한다.
- 모든 것은 직무 적합성으로 통한다.
-
'나 같은 인재를 안 뽑으면 회사가 손해'라는 마음가짐으로 면접에 임하면 두려울 것이 없다.
- 자신감 있는 모습을 보여줄 수 있고, 떨어져도 상처를 덜 받는다.
- 그렇다고 이 말을 면접관에게 직접 하면 안 된다.
- 그리고 준비를 하나도 안 하고 가도 안 된다.
-
면접은 회사가 지원자를 평가하는 자리이기도 하지만, 지원자가 회사를 평가하는 자리이기도 하다.
- 특히 실무진 면접의 면접관들은 입사하고 나면 동료가 될 사람들이다.
- 면접 경험이 불쾌한 회사는 근무 경험도 불쾌할 가능성이 높다.
-
내가 이 회사와 잘 맞을지 끊임없이 고민해야 한다.
- 내가 포기할 수 있는 것과 포기할 수 없는 것이 무엇인지 알아야 한다.
- 다른 직무로 발령되거나 전환배치가 일어나도 괜찮은가?
예: 쿠버네티스 다룰 줄 안다고 했다가 DevOps로 납치되거나, 그래픽스 공부한 적 있다고 했다가 OpenGL 프로그래머로 납치되는 경우를 본 적이 있다. - 지방에 있는 스튜디오로 근무지가 옮겨져도 괜찮은가?
- 회식 등 사내 친목 모임이 자주 열려도(또는 전혀 열리지 않아도) 괜찮은가?
- 월급을 올려주는 대신 정말 하기 싫고 지루한 일을 하라고 하면 묵묵히 할 수 있는 사람인가?
- 다른 직무로 발령되거나 전환배치가 일어나도 괜찮은가?
- 회사에 대해 궁금한 점을 물어보라고 하면, 내가 포기할 수 없는 것을 회사가 보장해 주는지 물어보면 좋다.
- 개인적인 생각으로, 취업 과정은 있는 그대로의 내가 가장 빛날 수 있는 회사를 찾는 과정이다. ⭐
- 면접관들이 내 능력을 온전히 알아봐 주려 하지 않는다면, 가서도 좋은 대우를 받기 어렵다.
- 나와 잘 맞는 회사에서는 동료들이 내 능력을 인정해 주고 나를 필요로 하며, 나도 더 멋진 모습을 보여주고 싶어진다.
이것이 불가능하다고 여기는 사람들도 있지만, 실제로 이런 회사에 다니고 있는 사람이 여기에 있다. 여러분에게도 일어날 수 있는 일이다!
- 내 능력이 충분한데도 면접에서 떨어졌다면, 회사에서 당장 필요한 사람이 아니거나 서로 fit이 안 맞아서 그럴 수 있다.
- Fit이 안 맞았다면 붙어서 갔어도 크게 고생하다가 언젠가 퇴사할 것이다.
차라리 안 붙은 것을 다행으로 생각하는 편이 마음 편하다.
- Fit이 안 맞았다면 붙어서 갔어도 크게 고생하다가 언젠가 퇴사할 것이다.
- 내가 포기할 수 있는 것과 포기할 수 없는 것이 무엇인지 알아야 한다.
-
취업은 운과 타이밍이 중요하다.
- 회사 상황에 따라 지원자마다 다른 질문을 던지기도 한다.
- 지원자를 걸러야 하는 상황이면 일부러 직무와 관련이 적은 세세한 내용을 질문하기도 한다.
- 예: 한 채용 공고에서 필요한 인원을 모두 선발했는데 아직 전형을 진행하고 있는 지원자가 있을 경우
- 이 경우 굉장히 어려운 질문을 받을 수 있고, 이 때문에 떨어지면 지원자가 실력이 없는 것이 아닌데도 공부를 덜 해서 떨어진 것이라고 스스로 믿게 될 수 있다.
- 보통은 실력 부족이 원인이 아니고 지원하는 타이밍이 안 좋았던 것이다. 좌절할 필요 없다.
- 지원자를 뽑고 싶은 상황이면 어느 면접에서나 나오는 질문들을 물어보는 편이다.
-
퀴즈: 모르는 질문이 나왔을 때에는 어떻게 대답할 것인가?
가. 아는 것처럼 최대한 둘러대며 그럴듯한 답을 이야기한다. (클릭하면 펼쳐집니다.)
많은 지원자들이 최선을 다하는 태도를 보여주고자 이렇게 대답한다.
그러나 면접관 입장에서는 가장 뽑고 싶지 않은 유형일 수 있다.
같이 일하는 동료가 이런 사람이라면 검증되지 않은 불확실한 정보가 사실인 것처럼 전파될 수 있다.
의도는 좋았을지 몰라도 거짓말을 하고 있는 것이다.
또한 이런 습관은 자신의 부족함을 인정하지 않는 태도로 비칠 수 있으며, 선입견에 갇혀 있기 쉬워 자신의 성장에도 방해가 된다.나. 질문과 관련된, 알고 있는 다른 내용들을 설명하면서 질문에 대한 답은 모르겠다고 말한다. (클릭하면 펼쳐집니다.)
최선을 다해 면접에 임하는 태도를 보여주는 동시에 거짓말은 하지 않는 답변이다.
이러한 모습은 면접관에 따라 호불호가 갈릴 수 있다.
요점만 간단히 전달하는 능력을 중요하게 보는 면접관에게는 다소 장황하고 초점을 잃은 답변이 불리하게 작용할 수 있다.
그러나 꼼꼼하게 오류 없이 전달하는 능력을 중요시하는 면접관에게는 좋게 보일 것이다.
자신이 가진 지식의 깊이를 구체적으로 드러내므로 면접관 입장에서 평가가 수월해지기도 한다.다. 깔끔하게 잘 모르겠다고 말한다. (클릭하면 펼쳐집니다.)
지원자가 이렇게 대답하기는 쉽지 않다.
실력이 부족하고 최선을 다하지 않는 사람처럼 보일 것에 대한 두려움 때문이다.
그러나 사실 면접관에게 좋게 보일 수도 있는 대답이다.
자신이 무엇을 모르는지 분명히 알고 있고, 정직하게 검증된 내용만을 전달하려 한다는 인상을 주기 때문이다.물론 너무 많은 질문에 대해 모른다고 답하면 실력 부족으로 떨어질 수 있다.
- 작성자의 주관적인 생각으로, 크게 두 종류의 인재상이 있다.
- 어떤 인재를 원하는지는 회사마다, 그리고 직무마다 다르다.
- 나와 맞지 않는 유형의 회사에 지원하면 면접에서 떨어질 가능성도 높고, 붙더라도 다니기 힘들다.
- 유명하고 돈 많이 주는 곳이 가장 다니기 좋은 곳은 아닐 수 있다.
- 당장 합격이 가장 중요하다면, 나를 속여서 회사에 맞추는 것이 나중에 감당 가능한 일인지 생각해 보는 것이 좋다.
-
대기업형 인재
- 한 가지 일에 특화된 스페셜리스트를 원한다.
- 튀려고 하지 않고 말 잘 듣는 사람을 원한다.
- 뽑을 때 면접관이 직무와 직접적으로 관련 있는 능력에만 주로 관심을 가진다.
- 거대한 조직에서 오래 서비스해 온 게임을 유지보수하게 된다.
- 상사가 시키는 일을 하게 될 가능성이 높으며 직무가 잘 바뀌지 않는다.
- 의견 개진이 어렵고 조직이 잘 변화하지 않는다.
- 개인의 성장 욕구가 적고 안정적인 직장 생활을 원하는 사람에게 잘 맞다.
- 학부 졸업 후 취업하는 경우에 비교적 잘 맞다.
그러나 대기업 다니다가 대학원 가는 경우도 종종 있다. - 돈과 커리어를 쌓기 좋다.
-
스타트업형 인재
- 이것저것 다 잘 하는 올라운더, 제너럴리스트를 원한다.
- 대기업에 못 간 사람이 아니라 안 간 사람을 원한다.
대기업에 합격할 실력이 충분히 되지만, 자신의 성향을 잘 이해하고 있고 자신의 의지로 대기업보다는 스타트업에 들어가기를 희망하는 사람들을 말한다. - 타 직군과도 소통을 잘 하는 사람을 원한다.
- 뽑을 때 직무와 직접적으로 관련 없는 능력들도 중요하게 본다.
- 작은 조직에서 새로운 게임을 처음부터 빠르게 만들게 된다.
- 일을 주도적으로 찾고 스스로 배워서 해야 할 가능성이 높으며 여러 직무를 혼자 맡기도 한다.
- 의견을 비교적 쉽게 개진할 수 있지만 그 일을 본인이 맡게 되는 경우가 많다.
- 개인의 성장 욕구가 크고 도전적인 직무를 원하는 사람에게 잘 맞다.
- 대학원 졸업 후 취업하는 경우에 비교적 잘 맞다.
- 본인이 하고 싶은 일을 하게 될 가능성이 상대적으로 높다.
그러나 그 일이 수익 창출로부터 자유로운 것은 아니다.
Note
첫 회사를 다닌 지 한두 달 된 신입의 지극히 주관적인 감상입니다.
저는 현재 게임 개발자를 돕는 도구를 만드는 일을 맡고 있습니다.
일반적인 게임 클라이언트 개발자와 경험이 상이할 수 있습니다.
'작성자는 이렇게 사는구나!' 하고 보시면 좋겠습니다.
-
맡은 직무를 통해 추구하고자 하는 가치가 자신이 가진 가치관과 일치하는지가 굉장히 중요하다.
- 어느 회사를 다니는지보다 어느 직무를 맡았는지가 더 중요하다.
- 회사에 지원할 때에는 바닥부터 게임을 제작하며 라이브로 고객을 상대하는 경험을 해보고 싶었으나, 게임 콘텐츠와 관계 없는 개발자 도구를 만드는 직무를 맡게 될 것에 대해 아쉬움이 있었다.
- 그러나 입사 후 일주일 만에, 이 부서에 오기를 잘했다는 생각이 들었다.
- 기한이 촉박하고 성과에 대한 압력을 받는 게임 제작 부서나 라이브 운영 부서에 들어갔다면 성장할 여유도 없이 이미 다룰 줄 아는 기술에 더 숙달되기 위한 훈련만을 반복했을 것 같다.
- 반면 현재 속한 연구 부서에서는 여유 있게 새로운 기술을 도입해 볼 수 있고, 개인의 성장을 도모할 수 있으며, 회사가 안고 있는 여러 문제점을 해결하는 과정에서 유의미한 기여를 할 수 있다.
- 회색 직장인이 되지 않고 '깨어 있는 나'로 남아 있기 위해 고민을 하게 된다.
- 나는 '코더'가 아니다. '프로그래머'이자 '아키텍처'이다.
- 할 일이 명시적으로 주어지지 않아도, 회사가 나아가고자 하는 가치와 내가 추구하는 가치를 고려하여 스스로 문제를 정의하고 할 일을 찾아서 하는 사람이 되고자 한다.
- 내가 가장 열정을 쏟고 싶은 일이 회사에서 맡은 일과 같아질 수 있을지 고민해보게 된다.
- 회사에서 현재 맡은 일이 내 미래에도 분명 도움이 될 일이면서 다른 사람들을 기쁘게 하는 일임을 인식할 때 열정이 솟아난다.
- 이러한 자기실현(자기일치)적 목표를 세울 수 있다면 힘들이지 않고 행복하게 일할 수 있다. 최적의 회사, 최적의 직무를 찾은 것이다.
- 워라밸은 회사에서의 자기실현이 불가능할 때 고려하게 되는 '차선의 목표'라고 생각한다.
- 어느 회사를 다니는지보다 어느 직무를 맡았는지가 더 중요하다.
-
면접을 준비하기 위해 공부했던 것보다 더 많은 것을 짧은 시간 동안 배우게 된다.
- 처음 배우는 기술로 더듬더듬 작성한 코드가 성공적으로 돌아가고 거대한 프로젝트에 병합될 때의 뿌듯함은 이루 말할 수 없다.
- 먼 훗날에 다른 회사를 가더라도 유용하게 쓰이는 기술이라고 생각하면 공부하는 것이 즐거워서 동료에게 질문을 끊임없이 하게 된다.
- 실제로 요즘에는 쉬는 시간이나 퇴근 후에 개발 공부를 하는 날이 잦아졌다.
- 회사 밖에서는 배우고 싶어도 배울 수 없었던 것들이 너무나도 많았음을 알게 된다.
- 기술은 회사에 쌓이는 것이 아니다. 개인에게 남는 것이다.
-
뛰어난 개발자는 열린 태도를 가진 개발자이다.
- 자신이 알고 있던 것보다 더 효과적이고 효율적인 문제 해결 방법을 발견하였다면 과감히 새로운 것을 익히고 받아들일 자세가 필요하다.
그러나 이는 무비판적으로 신기술을 수용하라는 뜻은 아니다. - 자신이 잘 아는 주제가 아니더라도 자연스레 관심이 가고, 타인의 생각을 궁금해하며 적극적으로 물어보는 사람이 성장에 유리하다.
- 더이상 배울 것이 없다고 느낀다면 다른 직무나 다른 회사로 옮길 타이밍이다.
- 더이상 배우고 싶지 않다는 생각이 든다면, 나라면 개발자를 그만둘 것이다.
연구실을 그만뒀던 것처럼.
- 자신이 알고 있던 것보다 더 효과적이고 효율적인 문제 해결 방법을 발견하였다면 과감히 새로운 것을 익히고 받아들일 자세가 필요하다.
-
신입이 해야 할 역할이 분명히 있다.
- 어떤 회사도 완벽한 조직일 수 없다. 누구나 사내의 문제점을 알지만, 이를 해결할 의지를 가지고 추진할 수 있는 사람은 신입밖에 없다.
- 동료들이 경험이 많고 기술적으로 뛰어나다고 해도, 신입이 그들보다 잘 할 수 있는 영역이 분명 존재한다.
-
회사라는 곳이 생각보다 행복한 곳일 수 있다.
- 대학생이 막연히 갖는 두려움 중에, 회사에 꼰대들이 있고 이들이 신입을 무시하며 막 대할 것 같다는 생각들이 있다.
- 실제로 출근길에 쓰러져 응급실에 실려 갔는데 당일 오후에 출근하라고 하는 강제 노역소도 있다. 그런 곳은 당장 그만둬라.
- 하지만 오히려, 직원이 행복한 회사가 존재한다는 사실을 많은 사람들이 알았으면 한다.
- 빠른 퇴근을 독려하고, 아플 때 병가 사용 방법을 먼저 알려주며, 질문을 하면 친절히 답해주고, 신입에게 칭찬을 아끼지 않는 동료들이 있다.
- 신입을 보채지 않고 신입의 능력을 믿어주며 사내에서 좋은 경험을 다양하게 해볼 수 있도록 도와준다.
- "나같은 사람도 받아준다고?" 해서 들어갔다가 한 달도 안 되어 퇴사를 고민할 바에는, 취업 기간이 길어지더라도 사람을 존중할 줄 알고 문화가 자신과 잘 맞아서 다니는 것이 즐거운 회사를 끝까지 찾아보는 것을 추천한다.
- 대학생이 막연히 갖는 두려움 중에, 회사에 꼰대들이 있고 이들이 신입을 무시하며 막 대할 것 같다는 생각들이 있다.
-
신입이 원하는 회사에 들어가려면 경력직을 금방 따라갈 정도의 내공을 쌓아야 한다.
- 신입이 첫 회사로 들어가기에는 대기업이 오히려 더 쉬울 수 있다.
대기업은 신입을 대규모로 뽑아서 교육시킬 제도와 여유를 갖추고 있다. - 스타트업은 그럴 여유가 없기 때문에 입사하자마자 1인분이 가능한 사람을 원한다.
그래서 경력직을 선호하지만 경력직만 뽑는 것은 아니다. - 취업과 성취를 목표로 공부하는 사람이 아닌, 새로운 것을 알게 되는 것이 재미있어서 스스로 파고드는 사람이 되면 내공은 저절로 쌓인다.
- 단, 파고드는 방향이 맡게 될 업무와 잘 맞는 회사에 지원해야 한다.
- 신입의 입사 전 경험은 이를 통해 내가 하고 싶은 일과 잘 할 수 있는 일이 무엇인지를 명확히 알게 되었다면 충분히 쌓은 것이다.
- 무작정 스펙(남에게 보여지는 것)을 늘리기 위해 시간을 버릴 필요가 없다.
- 신입이 경력직보다 실무 경험이 부족한 것은 너무나도 당연하다. 뽑는 사람들도 이를 참작하고 뽑으므로, 경험 부족에 연연하지 않아도 된다.
- 신입이 첫 회사로 들어가기에는 대기업이 오히려 더 쉬울 수 있다.
- https://github.com/Romanticism-GameDeveloper/GameDeveloper-Client-Interview
- 본 자료 내용의 대부분은 위 링크에서 가져왔습니다.
- 위 링크에는 C++ 및 Unreal에 대한 내용도 있습니다.
- 본 자료 내용 중 위 링크에 없는, 새롭게 추가한 내용도 있습니다.
- 본 자료 내용의 대부분은 위 링크에서 가져왔습니다.
- 나올 가능성이 높은 질문들은 ⭐ 중요! 표시를 달아두었습니다.
Warning
본 자료 내용에는 오류가 있을 수 있습니다.
모든 내용을 그대로 외우기보다는, 다른 자료도 찾아보고 내용을 검증하면서 공부하시면 학습에 도움이 될 것입니다.
그리고 시간이 충분하다면 여기서 얻은 기술을 Unity 프로젝트에 직접 적용해 보면서 자신의 것으로 흡수하시기 바랍니다.
겪어본 사람과 암기만 한 사람은 대답에서 그 차이가 드러납니다.
Note
볼드체는 나올 확률이 매우 높은 질문입니다.
Unreal을 사용하는 직무의 경우 C++과 Unreal에 대한 이해가 필요합니다. 이는 여기서 다루지 않습니다.
-
Unity & C# 스크립팅
- "최신 버전의 C#으로 Unity 스크립트를 작성하면 어떤 문제가 생기나요?"
- "Unity에서
Update()
와FixedUpdate()
와LateUpdate()
의 차이에 대해 설명해 보세요." - "값 형식과 참조 형식이 메모리에 어떻게 저장되는지 설명해 보세요."
- "C#에서 얕은 복사와 깊은 복사를 할 때 각각 메모리에서 어떤 일이 일어나는지 설명해 보세요."
- "C#과 Unity의 garbage collector가 서로 다른데, 어떤 차이가 있는지 설명해 보세요."
- "C#에서
const
와readonly
의 차이를 설명해 보세요." - "구조체의 인스턴스 안에 들어있는 참조 타입의 멤버 변수는 스택에 저장되나요, 힙에 저장되나요?"
- "C#에서 boxing이 일어나는 상황을 설명해 보세요."
- "직렬화된 형식으로서 JSON이 갖는 장점과 단점이 무엇인가요?"
- "C#에서
string
을+
로 연결할 때 생기는 문제점에 대해 설명해 보세요." - "C#에서 확장 메서드를 만드는 방법을 설명해 보세요."
- "C#에서
delegate
와event
의 차이점이 무엇인가요?" - "C#에서 클로저의 단점은 무엇일까요?"
- "Unity 메인 스레드가 아닌 스레드에서 UI를 변경하려면 어떻게 해야 하나요?"
- "Unity의 fake null에 대해 설명해 주세요."
- "C#의
List
와Dictionary
는 내부적으로 어떻게 구현되어 있나요?" - "C#의 LINQ에 대해 설명해 보세요."
- "C#의 Reflection에 대해 설명해 보세요."
- "어드레서블을 사용해 본 경험을 말씀해 주세요."
- "Unity 프로파일러를 사용해 본 경험에 대해 이야기해 주세요."
- "Unity로 개발하면서 언제 최적화를 해야 한다고 생각하나요?"
- "최근에 나온 Unity 6의 특징 중 하나를 소개해 보세요."
-
Unity 그래픽스
-
객체지향 프로그래밍
-
디자인 패턴
-
운영체제
-
데이터베이스
-
통신
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
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보다 나중에 불린다.
- 프레임 드랍: 한 프레임의 실행 시간 안에 연산을 다 수행하지 못한 경우 해당 프레임에 렌더링을 하지 못하는 현상이다. 화면 버벅임을 유발한다.
아래 링크에 있는 가상 메모리 그림을 같이 보면서 공부하시면 좋습니다.
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이 있다.
- 쌓일수록 위로(높은 주소 쪽으로) 올라간다.
- 프로그래머가 관리하며, 실행할 수 없다.
- 동적 할당한 객체(참조 타입)를 저장한다.
- Unity에서의 관리되는 메모리 시스템
- 그 아래에 Static data가 있다. 쓸 수 있고 실행할 수 없다.
- 그 아래에 Literals가 있다. 이는 읽기 전용이며 실행할 수 없다.
- 그 아래에 Instructions가 있다. 이는 읽기 전용이며 실행할 수 있다.
- 얕은 복사: 같은 힙 메모리 주소를 가리키도록 주소를 복사
- 깊은 복사: 힙에 복사할 객체가 가진 메모리만큼을 새로 할당하여 복사하고 새 메모리 주소를 반환
- C++과는 조금 다르다.
- C++에서의 얕은 복사: 멤버의 값만 복사
- C++에서의 깊은 복사: 멤버의 값 복사 + 포인터가 참조하는 대상까지 복사
⭐ 중요!
- ".NET과 Unity의 GC가 어떻게 다른지 설명할 수 있나요?"
- "
GC.Collect()
함수를 명시적으로 호출해본 적이 있나요?" - "동적 할당을 줄여야 하는 이유는 무엇인가요?"
- 사용자가 메모리를 관리할 필요가 없다. 편하다.
- 메모리 누수가 일어나지 않는다.
- 관리되는 힙에 효율적으로 저장한다. 메모리 압축을 한다. 메모리 단편화를 줄인다.
- 한 객체가 다른 객체가 가진 메모리에 접근하는 일을 막아 메모리 안전성을 높인다.
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 상태가 된다.
- 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가 돌아가는 중에는 다른 모든 스레드가 일시 중단된다.
- 힙을 압축하는 연산은 시간이 오래 걸린다.
- 메모리 복사 및 붙여넣기를 하기 때문이다.
- 동적 할당한 내용물을 메모리에 로드할 때에도 시간이 걸린다.
- GC가 돌아가는 동안 많은 연산을 한다.
면접에서 물어볼 가능성이 높은 내용이면서, 실무에서도 프로그래머의 실력을 판가름하는 기본 소양입니다.
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하는 것이 좋다.
- 게임오브젝트를
- 컬렉션과 배열 재사용
List
나Dictionary
를Clear()
하여 재사용한다.
- 배열 값 반환 메서드
- 매번 새로 배열을 만들어 반환하지 말고 기존 배열을 인자로 받아 수정하도록 한다.
- 빈 배열 및 문자열 재사용 (null object 패턴)
- 배열 값 기반의 메서드가 빈 세트를 반환해야 할 때
null
값 대신 빈 배열로 미리 할당된 정적 인스턴스를 반환하면 더 효율적이다. - 빈 문자열의 경우 설명 참고
- 배열 값 기반의 메서드가 빈 세트를 반환해야 할 때
- LINQ 사용 줄이기
- 클로저 및 익명 메서드
- 박싱
- C#에서는 세대 기반 GC 덕분에 큰 문제가 없지만 Unity에서는 문제가 될 수 있다.
- 자세한 예제는 boxing & unboxing 설명 참고
- 배열 기반 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); // … }
-
const
- 컴파일 타임에 변수가 값으로 대체된다.
- 스택에 저장된다.
- 선언할 때에만 값을 설정할 수 있다.
- 값이 바뀌면 다시 빌드해야 한다.
- 내장형 타입과만 사용할 수 있다.
readonly
- 런타임 상수이다.
- 힙에 저장된다.
- 선언할 때나 생성자에서만 값을 설정할 수 있다.
- 코드에 대한 참조를 유지하므로 값이 바뀌더라도 전체를 다시 빌드하지 않아도 된다.
- 어떤 타입과도 사용할 수 있다. (사용자 정의 클래스 포함)
- 일반적으로
const
보다readonly
를 쓰는 것이 좋다.const
가 조금 빠르기는 하며, 다음의 경우에는const
를 사용해도 된다.switch
/case
문 레이블enum
정의- 프로퍼티의 매개변수
https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/struct
-
"클래스의 인스턴스 안에 들어있는 값 타입의 멤버 변수는 스택에 저장되나요, 힙에 저장되나요?"
-
"구조체의 인스턴스 안에 들어있는 참조 타입의 멤버 변수는 스택에 저장되나요, 힙에 저장되나요?"
-
struct
(구조체) 인스턴스- 값 타입이다.
- 스택에 저장된다.
- 필드와 메서드를 가질 수 있다. (주의!)
- 할당하거나 인수로 넘기거나 반환할 때 복사된다.
-
class
(클래스) 인스턴스- 참조 타입이다.
- 힙에 저장된다.
- 필드와 메서드를 가질 수 있다.
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
-
값 타입
- C#의 primitives(
int
,float
등), 구조체(struct
), 열거(enum
) 타입이 여기에 속한다. System.ValueType
으로부터 상속된다.- 스레드 스택에 할당된다.
- https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/value-types
- C#의 primitives(
-
참조 타입
- C#의 클래스(
class
), 문자열(string
) 타입이 여기에 속한다. System.Object
또는System.String
으로부터 상속된다.- 힙에 할당되며 GC(garbage collector)가 관리한다.
- 이 힙 메모리 주소를 가리키는 주소 값은 스택에 저장된다.
- https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/reference-types
- C#의 클래스(
-
Boxing: 값 타입을 참조 타입으로 변환
-
Unboxing: 참조 타입을 값 타입으로 변환
-
Boxing과 unboxing은 동적 할당을 만드므로 비싸다.
-
대표적인 boxing의 예 (피해야 한다.)
-
object.Equals(object other)
사용int x = 1; object y = new object(); y.Equals(x); // 값 타입인 x를 참조 타입인 Object 타입으로 boxing하므로 인자 전달 시 할당 발생
-
struct
를 부모 인터페이스 클래스로 캐스팅struct
가 값 타입이므로 boxing이 일어난다.- https://medium.com/@swiftroll3d/avoiding-mistakes-when-using-structs-in-c-b1c23043fce0
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에서 어떤 클래스를 직렬화하려면 어떻게 해야 하는가?
- 추상 클래스나 일반 클래스가 아니어야 하고
- 클래스 앞에
[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.JSON
과UnityEngine.JsonUtility
를 사용할 수 있다.- 면접에서 이것까지 묻지는 않겠지만, 나중에 둘의 차이를 알아두면 좋습니다.
BinaryFormatter
는 보안 취약점(code injection)이 발견되었으므로 사용하지 않아야 한다.- 효과적인 직렬화를 위한 각종 서드 파티 라이브러리들이 있으니 찾아보면 좋습니다.
- 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이
string.IsNullOrEmpty()
를 쓰면 안전하다.
-
클래스의 현재 인스턴스를 가리킨다.
- 지역 변수와 필드를 구분할 때 필드에 대해
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
이다.- 다른 클래스에서는 함수를 등록하는 것만 가능하다.
Action
: 인자 타입이 T이고 반환 타입이 void인 함수 대리자 템플릿Func<T, TResult>
: 인자 타입이 T이고 반환 타입이 TResult인 함수 대리자 템플릿Predicate
: 인자 타입이 T이고 반환 타입이 bool인 함수 대리자 템플릿
-
아래와 같은 코드에서 불필요한 할당이 발생할 수 있다.
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
참고.
- 이 연산자는 atomic하기 때문에 멀티스레딩 환경에서도
- 대리자의 함수 목록이 비어있음을 확인하지 않고 호출하면
-
람다 식
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; };
-
클로저
- 익명 메서드 밖에서 정의된 변수를 익명 메서드 안에서 사용하는 경우
- https://medium.com/swlh/the-magic-of-c-closures-9c6e3fff6ff9
- https://medium.com/@mark.pelf/closures-in-c-demystified-ba989651080d
int i = 0; Action print = () => Console.WriteLine(i); // 람다 식 바깥의 지역 변수 i 포착 Func<int> increment = () => ++i; // 람다 식 바깥의 지역 변수 i 포착
-
C#의 메서드 참조(delegate)는 참조 형식이므로 힙에 할당된다.
- 즉, 익명 메서드이든 미리 정의된 메서드이든 메서드 참조를 인자로 전달하면 임시 할당이 발생한다.
-
익명 메서드를 클로저로 전환하면 메모리 양이 상당히 증가한다.
- 클로저를 만들면 정확한 값을 전달하기 위해 외부 범위 변수를 유지할 수 있는 익명 클래스를 만들고 이것을 인스턴스화하여 힙에 할당한다.
- 가급적 클로저보다는 익명 메서드를 쓰는 것이 좋다.
-
클로저는 boxing에 의한 할당인가?
-
static
키워드를 붙여 정적 익명 함수를 만들면 지역 변수를 실수로 포착하여 클로저가 되는 현상을 방지할 수 있다.- 면접에서 이런 것까지 묻지는 않을 것입니다.
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>
타입으로 정의한다.async Task<TResult>
를 사용하면TResult
타입의 반환값을 가진다.async void
와async Task
의 차이: https://stackoverflow.com/questions/12144077/async-await-when-to-return-a-task-vs-void
- Unity와 무관한 긴 작업을 처리할 때 유용하다.
- 예: 서버 API 호출, 큰 데이터 로드 등
- 메인 스레드가 아닌 스레드에서 실행된다. 멀티스레딩이다.
- 코루틴과 달리 실제로 병렬적이다. 따라서 Unity 로직의 실행을 가로막지 않는다.
- Unity의
MonoBehaviour
에서 파생된 native API는 멀티스레딩을 지원하지 않으며, 메인 스레드에서만 호출할 수 있다.- 예를 들어 UI를 변경해야 하는 경우 로직을 메인 스레드로 가져와 실행해야 한다.
- 함수를 실행한 오브젝트가 파괴되어도 취소 토큰을 사용하지 않는 한 끝까지 실행된다.
- Thread-safe하게 코드를 작성해야 한다.
- 서로 다른 스레드에서 공유 변수를 사용할 때에는
lock
을 잘 걸어야 한다.
- 서로 다른 스레드에서 공유 변수를 사용할 때에는
- 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로 오면서 기능이 더 추가되었지만 여전히 실전에서 사용하기에는 부족하다는 평가를 받는다.
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.Object
를Destroy()
하여 C++ 네이티브 엔진의 객체를null
로 만들고UnityEngine.Object
을null
인 것처럼 표시해도(fake null) .NET의System.Object
는null
이 아니다.- 해당
UnityEngine.Object
의 메타데이터가 잔존하고, 이를 가리키는 참조를 다른 게임오브젝트나 컴포넌트들이 여전히 가지고 있기 때문이다. - .NET GC가 돌기 전까지는 객체가 메모리에서 해제되지 않으며, GC가 실행되어도 참조받고 있지 않은 객체만을 찾아 지우기 때문에 이
UnityEngine.Object
의 base인 .NET의System.Object
가 즉시null
이 되지는 않는다. - 그러나 C#에서
Destroy()
를 호출하면 C++ 네이티브 엔진에서는 객체가null
이 된다.- 참고로 이 과정은 무겁다. C++에서 객체를 없애는 함수를 호출하여, 룩업을 수행하고 C# 스크립트 레퍼런스를 C++ 네이티브 레퍼런스로 전환하는 과정을 거친다.
- 즉,
Destroy()
호출 시 C++ 네이티브 엔진에서는 객체가null
이 되어 있지만 C#에서는System.Object
가null
이 아닌 상태로 남아 있고, 이것이 파괴된 C++의 객체를 가리키고 있게 된다. - 이때 개발자가
Destroy()
한 객체를 계속 사용하면 문제가 생긴다. - 따라서 실제로는
System.Object
가null
이 아니지만UnityEngine.Object
가null
인 것처럼 표시하여 이 객체를 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.Object
의null
여부를 검사하기 때문에UnityEngine.Object
가Destroy()
되었는지 확인할 때에는 사용할 수 없다.- 이들 연산자는 C# 문법 상 오버라이드할 수 없다.
-
Unity의
null
비교는 느리다.UnityEngine.Object
가 살아있는지 검사하는 로직이 무겁기 때문이다.- 이것이 살아있다는 것이 확실하면(
Destroy()
된 객체가 아님을 보장할 수 있다면) 다음 세 가지 방법으로null
비교 속도를 빠르게 할 수 있다.System.Object
로 캐스팅하는 방법System.Object.ReferenceEquals()
로 비교하는 방법- 패턴 매칭
is null
을 이용하는 방법
-
== null
vs.is null
== null
은 타입 별로 오버라이드된==
연산자를 호출하여 계산한다.- 따라서
UnityEngine.Object
을 비교할 때에는 느리다. == null
은 예외적인 상황을 내부에서 체크하므로 가장 안전하다.
- 따라서
is null
은 바로ceq
인스트럭션 연산을 적용하기 때문에 빠르다.- 다만
UnityEngine.Object
처럼==
이 오버라이드된 경우is null
을 사용하면 의도하지 않은 동작이 나타날 수 있다.
- 다만
-
속도 비교
is null
과ReferenceEquals(null)
은 속도가 비슷하게 가장 빠르다.- (가장 빠름)
is null
<Equals(a, null)
<a.Equals(null)
<a == null
(가장 느림)
?[]
(요소 액세스 null 조건부 연산자)- 예:
a?[x]
a
가null
이면null
을 반환한다.a
가null
이 아니면a[x]
의 값을 반환한다.x
가a
의 인덱스 범위 밖에 있는 경우IndexOutOfRangeException
을 띄운다.
==
이 오버라이드된 경우에?[]
을 사용하면 의도하지 않은 동작이 나타날 수 있다.
- 예:
??
(null coalescing operator)- 예:
return a ?? 3;
a
가null
이 아니면a
를 반환하고,null
이면 3을 반환
- 왼쪽 연산항이
null
이 아니면 오른쪽 연산항을 평가하지 않는다. ==
이 오버라이드된 경우에??
를 사용하면 의도하지 않은 동작이 나타날 수 있다.??
는 오버라이드할 수 없다.
- 예:
??=
(null coalescing assignment operator)- 예:
a ??= 3;
a
가null
일 때에만a
에 3을 대입
==
이 오버라이드된 경우에??=
을 사용하면 의도하지 않은 동작이 나타날 수 있다.??=
는 오버라이드할 수 없다.
- 예:
-
List<>
: 배열(ArrayList)- 용량(capacity)을 초과하여 삽입하는 경우 용량을 늘린 새 배열을 할당하고 기존 배열의 값을 복사한다. 따라서 시간 복잡도가
$O(n)$ 이다. 이를 피하려면 미리 사용할 만큼의 용량을 할당할 필요가 있다. -
TrimExcess()
를 쓰거나Capacity
를 직접 변경하여 낭비되는 공간을 줄이는 경우에도 새 배열을 할당한다. 따라서 시간 복잡도가$O(n)$ 이다. -
Remove()
는 배열의 용량을 변경하지 않는다.
- 용량(capacity)을 초과하여 삽입하는 경우 용량을 늘린 새 배열을 할당하고 기존 배열의 값을 복사한다. 따라서 시간 복잡도가
-
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< , >
가 더 빠르다. -
Keys
나Values
list를 반환해야 할 때SortedList
는 이 list를 그냥 반환하면 되고,SortedDictionary
는 list를 생성해서 반환해야 한다. -
SortedDictionary
는 generic이 아니다.
- 둘 다 검색은
-
List<KeyValuePair< , >>
의 용도- 이것은
SortedDictionary< , >
와 다르게, 삽입 순서를 보존한다. - generic이기도 하다.
- 이것은
-
Dictionary
와SortedDictionary
중 누가 더 나은가?- 삽입과 삭제는
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정답을 확인하려면 클릭하세요. (클릭하면 펼쳐집니다.)
-
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);
https://learn.microsoft.com/ko-kr/dotnet/fundamentals/reflection/reflection
- 로드된 어셈블리 내에 정의된 타입에 대한 정보를 런타임에 가져올 수 있다.
- 어셈블리에는 모듈이 포함되고, 모듈에는 타입이 포함되고, 타입에는 멤버가 포함된다.
- 리플렉션은 어셈블리, 모듈 및 타입을 캡슐화하는 개체를 제공한다.
- 동적으로 타입 인스턴스를 만들거나, 타입을 기존 개체에 바인딩하거나, 기존 개체에서 타입을 가져올 수 있다.
- 해당 타입의 메서드를 호출하거나 필드 및 프로퍼티에 접근할 수 있다.
- 탐색하는 시간이 굉장히 느리기 때문에 가급적 사용을 피해야 한다.
- 메서드 이름을 인자로 넣어 해당 메서드를 호출하는 함수는 내부적으로 Reflection을 사용하므로 피해야 한다.
- 예:
StartCoroutine("FadeOut");
대신StartCoroutine(FadeOut());
을 사용해야 한다.
- 예:
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 단위의 용량을 쉽게 넘길 수 있다.
- 모바일 스토어에서는 용량이 큰 빌드 파일의 업로드를 제한한다.
- Google Play Store:
.apk
100MB 이하 /.aab
4GB 이하 (200MB 초과 시 경고) - Apple App Store: 500MB 이하
- Google Play Store:
- 원격 콘텐츠 배포: 에셋을 필요할 때 다운로드받아 동적 로드
- 장점
- 게임 설치 시 사용자의 부담이 크게 완화된다. 특히 200MB 초과 경고가 뜨면 설치하지 않는 사용자 비율이 유의미하게 늘어난다.
- 사용자 기기 용량 부족 시 우리 앱이 삭제될 확률을 줄인다. 삭제할 앱을 찾을 때 용량이 큰 것부터 지우기 때문이다.
- 앱 업데이트 없이 빠르게 에셋을 패치할 수 있다. 버그를 수정한 빌드를 스토어에 올리려면 심사가 오래 걸리므로 배포가 늦고 그동안 고객 문의 폭탄을 받을 것이다.
- Lazy 다운로드가 가능하다. 튜토리얼 할 때부터 최종 콘텐츠를 받아 놓을 필요는 없다.
- 단점
- 프로그래머로서 관리가 까다롭다.
- 게임 설치 후에도 추가 다운로드 시간과 셀룰러 데이터가 요구된다.
- 새 버전 배포 시 CDN(콘텐츠 전송 네트워크)에도 새 에셋을 올리는 과정이 필요하다.
- CDN 비용이 든다.
- 장점
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()
함수가 호출된 시점에서 병목이 생긴다면 여기를 살펴본다. -
성능 문제를 찾아 해결하고 싶다면 여기를 읽어본다.
-
안드로이드 빌드를 만들고 모바일 하드웨어에서 프로파일링을 돌리는 방법
⭐ 중요!
-
"과거에 진행했던 프로젝트에서 최적화를 해본 경험을 말씀해 주세요."
-
"개발 중 언제 최적화를 진행하시나요?"
-
제 생각에, 최적화를 개발 초기 단계부터 생각할 필요는 없습니다.
- 최적화가 필요하다는 생각이 들면 정말 최적화가 꼭 필요한지 두 번 더 고민하세요.
- 일단 돌아가는 코드를 짜서 기능을 만든 후에, 프로그램을 돌려보면서 병목이 생기면 그때 최적화를 고려해도 늦지 않습니다.
- 무엇이 병목일지 모르는 상황에서 미리 대처해 봤자, 다른 곳에서 병목이 생기면 이전의 대처는 큰 의미가 없게 됩니다.
- 어떤 코드가 주어질 때 이것이 어떤 성능 문제를 일으킬 수 있는지 포착할 수 있고, 이를 어떻게 고쳐야 하는지 알고 있다면 충분합니다.
-
아래 세 문서를 모두 읽어보시기를 강력히 추천합니다!
-
깊게 공부하고 싶다면 아래 문서들도 읽어보세요.
https://unity.com/blog/unity-6-features-announcement https://unity.com/kr/blog/engine-platform/unity-6-preview-release
- Unity 6가 이 글을 편집하는 중에 출시되었습니다.
최신 동향을 묻는 질문으로 Unity 6에서 소개된 새 기능을 묻는 질문이 나올 수도 있습니다.
https://docs.unity3d.com/kr/2021.3/Manual/render-pipelines-overview.html
-
종류
- 빌트인 렌더 파이프라인: 예전 파이프라인. 커스터마이징 제한적
- 스크립터블 렌더 파이프라인 (SRP)
- 유니버설 렌더 파이프라인 (URP)
- 고해상도 렌더 파이프라인 (HDRP)
- 커스텀 렌더 파이프라인
-
개발 초기에 파이프라인을 잘 선택하여 결정하는 것이 중요하다.
-
과정
- 게임오브젝트의 트랜스폼을 월드 좌표계로 변환
- 카메라 시야 밖의 물체들을 렌더링 대상에서 제외
- 월드 좌표계를 카메라의 viewport 상 좌표로 변환
- 매터리얼의 셰이더 코드 실행하여 렌더링
- 렌더링한 결과를 '렌더 텍스처' 또는 '백 버퍼'에 저장
- '렌더 텍스처' 또는 '백 버퍼'에 있는 내용을 디바이스 화면에 출력
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를 통해 개체의 그룹화를 관리할 수 있다.
- 서로 다른 레이어는 서로 다른 배치에서 그려진다.
- https://docs.unity3d.com/kr/2021.3/Manual/class-SortingGroup.html
- 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 Packing
에Texture2D
, 스프라이트 에셋, 스프라이트가 담긴 폴더를 넣으면 해당 스프라이트가 동일한 설정을 가지고 하나의 아틀라스로 묶이게 된다.
- Asset > Create > Sprite Atlas 메뉴를 통해
- 아틀라스 성능 최적화
- 스프라이트가 씬에서 활성화될 때 Unity가 해당 스프라이트가 속한 스프라이트 아틀라스 전체를 로드한다. 이 크기가 너무 크고 씬에서 이 아틀라스의 텍스처를 거의 사용하지 않는 경우 오버헤드가 크다.
- 같은 씬에서 활성화하는 대부분의 스프라이트가 동일한 아틀라스에 속해 있도록 하는 것이 좋다.
- 아틀라스 팩 미리보기 기능을 통해 빈 공간이 과도하게 있는지 확인하고 줄이면 좋다. Max Texture Size를 줄이면 스프라이트 텍스처 크기를 줄이지는 않고 이 크기에 맞게 아틀라스의 빈 공간을 최대한 잘라낸다.
- 한 아틀라스에 묶인 텍스처의 매터리얼을 통일한다.
- 아틀라스의 크기를 2의 제곱수 크기(POT: power of two)로 맞춘다.
-
"과거에 진행한 프로젝트에서 텍스처 압축 방식은 어떤 것을 사용했나요?"
-
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)에서 주 텍스처가 가까워질 때 작은 디테일을 페이드 인 하여 텍스처가 흐릿하게 보이는 현상 방지
- 큐브 맵 (반사)
- 텍스처를 반사 맵(반사 프로브 또는 큐브맵 스카이박스)으로 사용하려면 Texture Shape를 Cube로 설정
- https://docs.unity3d.com/kr/2021.3/Manual/class-Cubemap.html
- 이방성 필터링
- Aniso 레벨을 높이면 지표각에서 보이는(기울인) 텍스처 품질 향상
- 바닥과 천장 텍스처에 사용하면 좋다.
- 노멀 맵
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)
- 다형성
- 상위 클래스 타입으로 하위 클래스 조작 가능
- 함수 오버로딩, 함수 오버라이딩
- 코드의 반복 줄임
- 단일 책임 원칙 (Single Responsibility Principle)
- 하나의 객체는 하나의 역할(책임)만을 가져야 한다.
- 추상화와 관련
- 개방-폐쇄 원칙 (Open-closed Principle)
- 수정에 닫혀 있고 확장에 열려 있어야 한다.
- 캡슐화, 상속, 다형성과 관련
- 리스코프 치환 원칙 (Liskov Substitution Principle)
- 자식 클래스는 부모 클래스로 대체할 수 있어야 한다.
- 상속, 다형성과 관련
- 인터페이스 분리 원칙 (Interface Segregation Principle)
- 필요하지 않은 기능을 의존하도록 강요하지 않아야 한다.
- 인터페이스의 모든 메서드를 구현하도록 강요하지 않아야 한다. (강요라고 느끼지 않게 필수적인 것만 인터페이스에 둔다.)
- 추상화와 관련
- 의존성 역전 원칙 (Dependency Inversion Principle)
- 추상적인 것에 구체적인 것이 의존해야 한다. 구체적인 것에 추상적인 것이 의존하면 안 된다.
- 부모 클래스에 자식 클래스가 의존해야 한다. 반대가 되면 안 된다.
- 추상화, 상속과 관련
- 종속성 주입을 적용하면 이 원칙을 지키는 데에 도움이 된다.
- 소프트웨어의 유지보수성, 재사용성, 확장성을 높이기 위해 이 원칙들을 지키면 좋다.
-
"인터페이스를 언제 사용하면 좋은가요?"
-
인터페이스와 추상 클래스와 가상 클래스의 차이를 직접 찾아보고 답해보시기 바랍니다.
- 종속성 주입과 함께 공부하면 좋습니다.
- 자식 클래스가 부모 클래스의 메서드를
override
하거나new
키워드로 숨길 수 있다. A
가virtual
클래스이고B
가A
를 상속하는 자식 클래스이며 둘이 같은 이름의 메서드를 구현하고 있을 때, 다음의 경우에 동작이 다르다.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
의 메서드가 호출된다.
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에는 해당 메서드가 부모의 것과 자신의 것 모두 들어있다.
⭐ 중요!
디자인 패턴 각각을 완벽하게 외우기보다, 아래 질문에 대해 생각해보면 좋습니다.
- "디자인 패턴을 적용해본 적이 있나요?"
- "디자인 패턴을 따로 외우고 익혀야 한다고 생각하나요?"
- 클래스 간 종속성을 줄이는 것이 왜 중요한가요?
- "멀티스레드 환경에서 싱글톤을 써본 적이 있나요? 어떤 점을 고려해야 하나요?"
- "싱글톤 패턴을 가급적 사용하지 않아야 하는 이유는 무엇인가요?"
- 클래스 간 종속성을 줄이는 것(디커플링)의 장점에 대해 생각해 보세요.
- 함수의 반환값으로
null
대신null
과 같은 역할을 하는 dummy 오브젝트를 생성하여 반환한다. - Dummy 오브젝트를 받으면 아무 연산도 수행하지 않도록 구현한다.
null
체크를 안 해도 된다.
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 패턴과 Dependency Injection 패턴의 차이점이 무엇인가요?"
-
추상화된 인터페이스를 두어서 구현이 바뀌거나 교환되더라도 호출에서 코드를 수정하지 않아도 되게 하는 방법
-
예:
Interact()
하나로Portal.Interact()
,Alter.Interact()
,Monster.Interact()
등을 수행할 수 있게 하는 방법
- 포장을 통해 접근하고, 그 내부는 다를 수 있는 패턴.
- [퍼사드]라고 읽는다.
- 중간에 인터페이스(facade)를 하나 두어, 실제 worker(클래스)들이 하는 일을 적당히 숨기면서도 간단한 인터페이스로 worker들에게 일을 줄 수 있는 패턴
- FSM(finite state machine)을 만들 때 주로 사용
- 여러 상태를 인터페이스로 묶어서 각 상태 별로 해당 상태에서 할 수 있는 일(함수)을 정의해주는 패턴
- 모든 state 패턴은 strategy 패턴이지만 역은 성립하지 않는다.
- State 패턴에서는 상태 파생 클래스들이 일할 때 context에 대한 참조를 갖는다. 그러나 strategy 패턴에서는 이러는 경우가 없다.
- 호환되지 않는 클래스의 인터페이스를 클라이언트(호출자)와 호환되도록 중간에 인터페이스를 두어 함수 시그니처 등을 변환해주는 패턴
- 상태 변화를 감지하는 이벤트 함수 등을 구독하는 옵저버들을 만들고, 상태 변화가 생길 때 자신을 구독하고 있는 옵저버들에게 신호를 보내 추가적인 업데이트를 할 수 있도록 한다.
- 콜백 함수를 만들 때 유용하다.
- 장점
- 결합이 느슨해진다.
- 단점
- 복잡해지고 느려진다.
- 멀티스레딩 환경에서 실행과 구독 취소가 동시다발적으로 발생하면 버그가 쉽게 생긴다.
Note
게임 클라이언트가 아닌 개발 직군에서는 많이 묻지만, 게임 클라이언트에서는 잘 묻지 않는 것 같기도 합니다.
학부 졸업생이 신입으로 입사하는 경우에는 물어볼 확률이 높습니다.
- 빅 엔디언
- 16진수 int
1A2B3C4D
를 주소가 낮은 위치부터 높은 위치 순서대로1A
,2B
,3C
,4D
순으로 메모리에 저장한다. - 사람이 읽기 쉽다.
- 컴퓨터가 읽기 어렵다.
- 16진수 int
- 리틀 엔디언
- 16진수 int
1A2B3C4D
를 주소가 낮은 위치부터 순서대로4D
,3C
,2B
,1A
순으로 저장한다. - 사람이 읽기 어렵다.
- 사칙연산에 유리하다.
- x86 시스템은 리틀 엔디언을 사용한다.
- 16진수 int
- ARM 같은 곳에서는 둘 다 사용하기도 한다.
- 프로세스: OS의 작업 단위
- 스택, 힙, 코드, 데이터를 모두 복사해 가진다.
- 다른 프로세스와 독립적으로 돌아간다. (공유 메모리를 사용하지 않는다면)
- context switching 등이 무겁다.
- 스레드: 프로세스 내에서의 실행 단위
- 스택만 복사하고 나머지 자원은 같은 프로세스 내에서 여러 스레드가 모두 공유한다.
- 가볍게 만들고 없앨 수 있다.
- 멀티프로세스 vs. 멀티스레드
- 프로세스보다 스레드가 가볍기 때문에 멀티스레드를 더 자주 사용하게 된다.
- 멀티프로세스든 멀티스레드든 동기화 이슈는 중요하다.
⭐ 중요!
- 외부 단편화
- 남은 메모리 총합은 충분한데 각각이 다 쪼개져 있어 하나의 큰 메모리 공간을 할당할 수 없는 경우
- 내부 단편화
- 많이 할당해놓고 쓰지 않는 경우
- 페이징할 때 발생하기도 한다.
- 외부 단편화 해결책
- 페이징
- 디스크 등의 보조 기억 장치를 활용해 메모리 일부를 일정한 페이지 단위로 쪼개 거기에 옮겨 놓고, 다시 불러오고 하는 방법
- 어느 주소에 있는지 기억해야 하므로 페이지 테이블을 관리해야 한다. 페이지 테이블은 가상 페이지 주소와 물리 메모리(디스크에 있는 경우 메모리의 프레임에 먼저 로드함) 상의 프레임 주소를 연결하는 정보를 가지고 있다.
- 디스크까지 내려가는 데 너무 시간이 오래 걸리므로 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
- 하나의 스레드가 mutex(lock) 객체를 갖는다.
- 다른 스레드나 프로세스는 누군가 이 mutex를 가지고 있는 동안 접근할 수 없다.
- 사용이 끝나면 mutex를 놓는다.
- Binary semaphore라고도 한다.
- Counting Semaphore
- 둘 이상의, 정해진 수의 스레드가 동시에 접근할 수 있다.
- 카운터를 두고 있으며, 이것이 0이 되면(정해진 수의 스레드가 사용 중이면) 더 들어갈 수 없다.
- 사용할 때 카운터를 1 내린다.
- 사용이 끝나면 카운터를 1 올린다.
- try를 뜻하는 P(사용할래요!)와 increment를 뜻하는 V(사용 끝났어요!)를 사용한다.
- P - critical section - V 순으로 사용해야 한다.
- 잘못 사용하면 데드락이 되거나 상호 배제에 실패할 수 있다.
- 사용 권한을 얻을 때까지 무한 루프를 돌면서 기다리거나, 이보다 효율적으로는 스레드를 sleep했다가 다른 스레드가 사용 권한을 놓을 때 sleep한 스레드를 깨우는 방식으로 구현할 수 있다.
⭐ 중요!
- A를 잡은 스레드가 B를 갖고 싶어하고, B를 잡은 스레드가 A를 갖고 싶어하는데, A와 B 모두 상호 배제가 필요한 자원이고, 서로가 자신이 가진 것을 놓을 생각이 없다면 데드락이 발생한다. 이때 누구라도 A와 B를 모두 잡는 경우는 평생 생기지 않는다.
- 다음 네 가지 조건을 모두 만족해야 데드락이 발생한다.
- 어떤 자원에 대해 상호 배제가 필요하다.
- 한 자원을 잡으면서 다른 자원을 기다리는 상황이 있다.
- 선취 불가능하다(non-preemptive). 다른 프로세스를 종료시킬 수 없다.
- 자원을 얻고자 하는 프로세스를 방향이 있는 그래프로 나타낼 때 사이클이 존재한다.
- 예: 어떤 곳에서는 A -> B 순으로 mutex를 잡고, 다른 곳에서는 B -> A 순으로 mutex를 잡는다면 사이클이 발생하여 데드락에 걸릴 수 있다.
- 위의 조건 중 하나라도 해결하면 데드락이 풀린다.
- 조건 1.은 없앨 수 없다. 상호 배제를 안 해도 된다면 mutex를 쓸 이유가 없다.
- 대부분은 조건 4.의 사이클을 제거하여 해결한다.
- 조건 3.에 대해 선취 가능하게 만들어 해결하는 방법도 있다.
- 데드락이 발생하면 해당 프로세스들을 차례로 강제 종료하여 해결해야 한다.
- 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비트 운영체제와 64비트 운영체제의 차이가 무엇인가요?"
-
둘의 차이는 사용할 수 있는 RAM의 크기이다.
- 32비트는 최대 4GB(=
$2^{32}$ bytes)의 메모리만 인식한다.
Windows에서는x86
으로 불린다. - 64비트는 최대 16EB(=
$2^{64}$ bytes)의 메모리를 인식한다. Windows에서는x64
로 불린다.
- 32비트는 최대 4GB(=
-
64비트 운영체제에서 64비트 프로그램을 돌리는 것이 32비트 프로그램을 돌리는 것보다 당연히 빠르다.
-
갤럭시 S24 등의 최신 모바일 기기에서는 32비트
.apk
를 실행할 수 없다.
- 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를 내 릴레이션에 두는 것
- DB 설계에 도움이 되는 내용입니다.
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와 REST의 차이