유니티3D 개발에 대한 단상

많은 분들이 이사다시피 저는 서버 프로그래머입니다만, 습작이나 오픈 소스로 공개한 SDK등에서 간단한 게임들을 공개해온적이 있습니다. 기본적인 클라이언트 작업 이해도는 있는 편이지만, 실무에서 클라이언트 코딩을 해온 기간은 짧은편이지요. (렌더링을 제외한 로직 구현이나 네트워크 코드를 주로 작성해왔죠. UI와 인게임 코딩은 습작에서 해온게 대다수입니다.)

그런 제가 대략 8개월여간의 유니티를 통한 개발 경험에 대해서 정리해보고자 합니다.

  1. 씬과 메타 파일로 인하여, 동시 작업이 어렵다.
    • 4.x대까지의 유니티는 동시 작업이 불가능한 수준으로 보면 된다. 같은 파일에 대한 동시 작업은 피하는게 좋다. 이를 위한 소통 비용이 반드시 필요한 수준.
    • 이런 문제로 인하여, 구조상으로 결합도를 낮추려는 노력을 하게 되기도 하지만, 피치 못할 사정이나 소통의 오류가 발생했을 때 한명의 작업 이외에는 날려야되는 문제는 그다지 좋은 상황은 아니다.
    • 결국 우리팀은 파일별 작업자를 일감 분리를 통해 분리하는 룰을 가지도록 의도해, 통일한 파일 동시 수정을 피하곤 있으나, 많은 데이터를 아우르는 작업 자체를 기피하게 되는 큰 원인이기도 하다.
  2. 프리팹은 가급적 동적으로 사용하는 것이 좋다.
    • 씬에 올려두고 사용할 경우, 해당 프리팹 외부의 delegate도 물릴 수 있지만, 이렇게 될 경우 프리팹이 수정될 때 마다 delegate가 날라간다.[예: UIButton의 on_click]
    • 그래서 애초에 동적으로 연결하게끔 코드와 UI간의 연결 정보를 관리하면 이를 유연하게 대처할 수 있다.
    • 에셋 번들로 패치 시스템을 만들 때도 고려하면 이 방법이 더욱 좋다.
  3. NGUI나 UGUI 둘다 편하긴하나, 위의 단점들로 인해 잦은 변경에 유연하지 못하다.
    • 그럼에도 불구하고 UI 작업 자체는 여러모로 편의성 기능이 많고, 대다수의 UI 기능들이 이미 존재하는 것은 확실히 장점이다.
  4. C#스럽기보단 스크립트언어 다루듯이 접근해야 하는 부분이 많다.
    • C#의 기능들이 전부 구현되어 있는 게 아니다. 사실상 일부만 구현되어있따고 보는 것이 더 많다.
    • 유니티 자의적인 해석대로 구성되어있는 요소도 꽤 있다.
    • 사실상 C#의 문법만 가져왔다고 마음 먹는 것이 정신건강에 좋다.
  5. 코루틴은 최적화의 해결책이 아니다.
    • 멀티 쓰레드 쓰듯이 코루틴을 다룰 수는 없다.
    • 그렇기에 결국 블러킹함수를 많이 쓸 수록 문제가 생긴다.
    • 쓰레드에서는 유니티 함수를 사용할 수 없다.
    • 그래서 결국 최적화나 블러킹 상황을 줄이는 데에 한계가 있다.
  6. 씬과 프리팹 저장이 직관적이지 않다. * 런타임에 변경한 정보가 바로 반영되는 점, 씬 단위로 실행 할 수 있는 점, 일시정지 할 수 있는 점등은 장점이다.
    • 하지만 내가 유니티 툴에서 테스트한 상태 그대로 파일로 저장되어 있지 않을 수 있는 점은 외부 버전 관리 시스템 (git, svn 등)을 사용하는 입장에서 큰 혼란을 가져다 준다.
    • 그래서 위에 언급한 대다수의 정보를 동적으로 연결하게끔 관리한다면, 많은 문제를 우회할 수 있다.
  7. 모든 씬에서 정상동작하게 관리하는 것은 장단이 있다.
    • 모든 씬에서 기능이 정상동작하게 하기 위해서는, 코드의 선행 조건을 만족시키기 위해, 애매모호한 룰을 세우게 되는 경우가 있다.
    • 특히나 패치 시스템을 사용하고, 코드와 메타 데이터를 관리하는 경우가 특히 그런데, 가급적 실행해볼 수 있는 씬을 한정지어 스크립트의 전제 조건을 줄여주는 것이 좋다.
  8. 예외처리 규약은 C++이나 C#으로 직접 제작할 때와 다르게 고민해봐야 한다.
    • 유니티가 추상화 해놓은 레이어 위에서 예외처리 정책도 정해야 하기 때문에, 이를 감안한 예외 처리 룰을 세우는 것이 좋다.
    • 특히 STL과 다른 동작을 하는 컨테이너가 많다. C++에 익숙한 사용자는 더더욱 컨테이너에 대한 레퍼런스를 다시 읽어보아야 할 것이다.

간략하게 정리해본 유니티 사용시 팁과 사견이었습니다. 아마도 한참을 더 사용하게 될 거 같은데, 그에 따른 여러 판단은 생각 날 때 마다 정리해서 공유해보록 하겠습니다.

상향식 코드 분석과 하향식 코드 분석

꽤나 많은 상황에서 우리는 기존 코드를 분석해야 한다.

빌드업이라 불리는 프로젝트에 필요한 기능들을 만져나가는 과정에서도 우리는 라이브러리나, 오픈소스 코드등을 통해서 기존 코드를 분석해나가야 한다.

물론 이 과정은 섬세하게 만져나갈 여지가 있고, 그간의 결합도를 조절해나갈 여지가 있으므로 상대적으로 코드 분석의 여지가 적다.

심지어 사용하는 라이브러리들이 익숙하거나, generic하게 구현되어있다면 더더욱이 쉽고.

사실 가장 곤욕스러운 과정은 프로젝트 dependency 한 코드를 분석하게 될 때이다.

이 과정에서, 코드간의 결합도를 낮출려고 노력해온 흔적이 있는 경우는 그래도 상대적으로 쉬운 편에 속한다. 예를 들어 패킷으로만 다른 티어와 통신을하고, 패킷 핸들링 코드가 명확하다면 기존 코드가 난해하다해도 그 결합도를 풀어가는데에 상대적으로 수월하다.

하지만 진입점의 종류가 한 곳이고, 각 기능별로 핸들링 코드가 분리되어있음에도 객체간의 관계가 명확하지 않고, 쓸데 없이 복잡하게 보이는 일은 비일비재하다.

이럴때 당신은 어떻게 분석하는가? 또, 인수인계를 위해 어떻게 준비해야 하는가?

많은 코드 분석과정을 해본결과, 또 지켜본 결과 많은 사람들이 두가지 방법정도로 코드를 분석하더라.

상향식 코드분석과, 하향식 코드 분석이다.

큰 그림. 즉, 전체적인 구조 설계의 전제 파악, entry point 분석, 스레드 디자인 분석과 같은 프로젝트의 룰을 파악하는데에 주력하는 것을 하향식 코드 분석이라 부른다.

반대로 컨텐츠별로 하나씩 기능들을 분석하며, 실제 컨텐츠별 사용중인 클래스들을 분석해나가는 것을 상향식 코코드 분석으로 부른다.

이 두가지 모두 적절히 이루어지면 좋겠지만, 사실 대부분의 경우 입사나 부서이동 직후 업무를 바로 진행해야되는 경우가 수두룩 하다.

이럴 경우 어떤 방법을 먼저 채택할 것인가?

나의 경우는 항상 하향식 코드 분석을 선호해왔다.

프로젝트의 룰, 전제를 모르고선 다른 코드들의 디테일을 봤을 때, 이 코드가 과연 잘 작성된 코드인지, 혹은 자연스레 녹아든 코드인지 판단도 안된다고 여겨서이다.

그렇다고 상향식 코드 분석이 나쁘다고 볼 순 없다.

일관된 규칙으로 코드가 잘 관리되어왔고, 간결하면 간결할 수록, 상향식 코드 분석이 빛을 발하기 때문이다.

하지만 그런 상황은 그다지 자주 벌어지지 않는다. 어찌보면 작성되어있는 코드 양이 많으면 많을수록 이상론적인 이야기이고, 꽤나 많은 경우에는 하향식 코드 분석을 선행하고, 이후 상향식 코드 분석으로 전환하는 과정이 옳다고 본다.

여기에 문서화에 대한 이야기를 빼놓을 수 없다.

그렇다면 인수인계 및 자신의 작업 기록을 위한 문서화는 어떻게 해야 하는가?

나는 작업 규칙과, 설계 기준을 명세하면 충분하다고 생각한다. 각 작업의 디테일은 고생한 과정을 기반으로 기록해 둘 수록 좋다. 만약 두사람 이상이 같은 작업 과정에서 고생했다면, 그 작업 자체가 집중을 요하는 작업이거나, 혹은 실수의 여지가 많은 빈틈있는 기반 작업이 되었다는 것을 (혹은 그러한 툴이나 라이브러리, 코드 등을 이용하거나) 의미하기 때문이다.

겪었던 문제들에 대한 기록만 잘 취합되어 있더라도 상대적으로 많은 시간을 아낄 수 있다고 생각한다. 그래서 나는 버그 노트를 작성하기 시작했는데, 이에 대한 이야기는 다음 글에서 이어서 하려 한다.

버그 노트

버그는 프로그래머의 숙명이다.

그 섬세하게 만들려한 IOS, OS X에도 버그는 종종 있으며, 사실 많은 사람이 M$라 부르지만 나의 경우는 매우 감사하고 있는 MS의 경우에도 버그는 많다. 구글도 예외는 아니고. (구글 apps 초창기 패치할 때 마다  언어 관련, IME 관련 문제를 겪게 했던 일은 나름 유명한 일화다.)

이러한 회사는 분명히 업계의 엘리트가 모여있을 텐데 어째서 이렇게 버그가 나오는걸까?

어찌보면, 버그는 당연한 숙명이다.

하나의 어플리케이션을 작성하기 위해, 잘 작성된 위지윅 툴을 사용하더라도, 버그가 없다고 단정 지을 수 없다. 하물며, 코드가 들어가고, 각종 코드를 재사용하고, 다른 코드를 얹는 과정에서는 복잡도와 결합도는 증가하고, 당연히 버그가 생길 여지도 많아진다.

자 그렇다면 버그를 없애려는 노력과 기반은 당연히 마련해야 할테고… (나의 디버깅 관련 글들은 대부분 이 주제에 대한 글로 이루어져 있다) 그렇게 했는데도 발생한 버그들에 대해선 어떻게 대처해야 할 것인가?

나는 그 대안으로 기록. 즉 버그 노트를 작성해야 한다고 생각한다.

꽤나 많은 프로그래머들이 버그를 부끄러워한다. 숨기려고도 하고, 괴로워 하기도 하고.

하지만 나의 경우는 꽤나 많은 버그가 환경에 영향을 준다고 생각한다. 또, 기존에 작성된 코드가 버그의 수나 버그의 수위를 조절한다고 생각하고.

자, 그렇다면 버그는 부끄러워 할 대상이 아니고 개선해야 될 대상이다.

그럼 하나 질문.

개선을 위한 습관으로  어떤 노력을 해보셨나요?

흔히들 하는 핑계가 경력이 쌓이면 저절로 고쳐져요같은 경험론이다. 주변 지인들과 수많은 얘기와 경험 해보고 내린 결론은 절대 버그는 경험과 비례하지 않는다. (물론 절대적 경험치나 기본기가 부족한 경우는 버그를 양산하기도 한다. 혹은 절대적으로 촉박한 일정도 그런 문제를 만들기도 하고)

오히려 위에 언급한 시스템이 버그를 더 줄여든다.

추가적으로 개인적인 노력도 반드시 필요하다고 생각한다. 나의 경우는 그런 의미로 버그 노트를 작성해왔다. 대략 9년여 되가는 이 습관은, 나의 버그 수를 줄이진 않았어도 같은 종류의 버그 재발은 급격히 줄여줬는데, 자체적인 회고를 주기적으로 거치는 것도 병행하는 덕택일거다.

난 반드시 자신의 버그를 돌아봐야 한다고 생각한다. 그러기 위해 버그 노트를 작성하고, 그 과정에서 기록되어야 할 것은 다음과 같다.

  • 현상
  • 추정
  • 과정
  • 원인
  • 해결책

위 항목을 잘 채우다보면, 잘못된 해결책이나, 미흡한 원인 파악, 잘못된 추정등이 밝혀진다. (종종 잘못된 현상 보고도 있긴 하고)

그런 과정을 반복하다보면, 자신의 잘못된 습관이 보인다. 그 습관을 고쳐나갈 수록, 퀄리티 높은 프로그래머가 된다고 생각한다.

물론 이런 노력없이도, 좋은 퀄리티의 프로덕트를 만들어 내는 분들이라면 괜찮지만, 아니라면 버그를 줄이기 위해 버그 노트를 써보면 어떨까?

코딩 컨벤션

내가 코드를 작성할 때 신경쓰는 코딩 규약들을 정리해본다.

소유권

  • 객체의 소유 주체는 (생성과 소멸의 관리 주체는) 하나로 규정한다.
  • 객체의 생성,소멸 스레드도 하나로 규정한다.
  • 다른 클래스에서 호출되어야만 하는 메소드를 만들지 말라.
  • Has a 관계가 명확하다면, 이 관계를 혼란 시킬 수 있는 메소드는 절대 만들지 마라.

진입점

  • 진입 점은 명확하게. 한곳으로 관리하자. 
  • 만약 다양한 진입 점이 있을 수 밖에 없다면, 다른 각각의 진입 점에서 왔음을 알 수 있게 하라.

중복

  • 중복을 허용하지 말라.

단일 규칙

  • 두 가지 이상의 규칙을 허용하지 말라. 새로운 규칙을 도입하고 싶다면, 기존 규칙대로 작성된 전부를 고쳐라.
  • 두 가지 이상의 규율이 존재하는 것은, 코드 작성/분석 등 모든 과정에서 해가 된다.

전치 검사, 후치 검사

  • 메소드는 정상적으로 실행되기 위한 선결 조건에 대한 검사를 해야 한다.
  • 메소드는 정상적으로 실행되었을 때의 기대 값을 충족 시키는지 검사해야 한다.

호출 주체

  • 누가 호출해도 문제가 없는 메소드만 외부로 개방하라.
  • 만약 특정 클래스에서만 호출해야 정상 동작한다면, 잘못된 설계이고, 잘못된 사용법을 제공하는 것이다.

호출 순서

  • 호출 순서에 영향을 주는 코드는 지양하라.
  • 피치 못할 사정으로 함수의 호출 순서가 일정해야만 한다면, 정해진 호출순서대로 실행하는 상위 함수를 만들고, 내부를 숨겨라.

(서평) 레거시 코드 활용 전략

최근 레거시 코드 활용 전략이라는 책을 읽고 있다.

업계에서 흔히 레거시 코드라 불리는 코드를 많이 만져보게 되죠. 굳이 라이브팀이 아니더라도, 자주 만나게 된다.

내 주변에서도 흔히 사용하는 용어로써의 레거시 코드는 복잡한 코드, 결합도 높은 코드, 제약이 많은 코드, 너무 긴 메소드 등을 통칭하는 용도로 쓰인다

대략 외국에서도 낡은 코드, 유효하지 않게 된 코드 등을 지칭할 때도 쓰는 거 보면, 레거시 코드의 개념이 부정적인 것은 확실한 가보다.

저도 그렇게 좋지 않은 코드에 대한 통칭으로 레거시 코드라는 용어를 사용해오던 찰나에 레거시 코드 활용 전략 (Working Effectively with LEGACY CODE) 라는 책을 읽게 되었다.

이 책에서의 레거시 코드에 대한 정의는 저에게 큰 공감이 했다.

“내게 있어서 레거시 코드는 테스트 루틴이 없는 단순한 코드일 뿐으로, 난 그 정의에 대해 유감을 가져왔다. 코드가 얼마나 훌륭하게 작성되어 있는지 여부와는 상관없이 테스트 루틴이 없는 코드는 불량 코드다. 얼마나 멋지게 작성되어 있는가와 객체지향의 사용 여부, 그리고 캡슐화의 정도도 참작 요소가 전혀 되지 못한다. 테스트 루틴이 있으면 코드의 동작을 빠르고 검증 가능하게 변경시킬 수 있다. 하지만 테스트 루틴이 없으면 실제로 우리 코드가 더 나아지는지 더 나빠지는지를 알 수 없게 된다.”

사실 좋은 코드의 기준은 너무 다양하다.

다른 사람이 작성해둔 코드 베이스 위에 작업해야 되는 경우, 선택지는 더 좁아진다. 내가 원치 않는 결합도, 내가 원치 않는 클래스 구조, 내가 원치 않는 기준으로 코드가 작성되어져 있고, 그 룰을 깨지 않는 선에서 작업해야 하는 경우가 많기 때문이다.

코드의 디테일을 보는 기준이 사람마다 다른 것도, 좋은 코드가 무엇인지의 논지를 흐리게 된다.

누군가는 속도, 누군가는 메모리 사용량, 누군가는 코드의 가독성 (나열한 것 중에 가장 주관적인 지표이기도 한)을 지표로 삼기 때문이다.

이러한 주관적 여지가 있는 기준이 아닌, 명확한 기준인 테스트 루틴이 포함된 코드냐는 우리가 지향할 목표를 명확히 해준다.

결국엔 유닛 테스트를 강조하고, 기존 작성된 코드에 유닛 테스트를 붙이자는 이야기에 귀결된다.

그러기 위한 테크닉에는 여러가지가 있고, 그 중 결합도 낮추기 같은 것은 경험과 언어에 대한 능숙도에 귀결되기도 한다.

결합도란 다양한 언어의 특성과 표현력에 따라 맺어지기 때문이다.

실례로, 루비의 경우는 결합도 높게 작성해야 되는 프로그램에는 그리 적합치 않은데, 다행히도 그렇게 억지로 짜려고해도 쉽지 않다.

반대로 C++의 경우 적절하게 (이 말이 굉장히 모호한 말임을 인정한다) 쪼개져 있지 않은 경우, 슈퍼 메소드나, 슈퍼 클래스, 또는 클래스간 관계에 따른 지나치게 높은 결합도를 보여주곤 하니 말이다.

특히나 코드 작성자의 의도를 강조하기 위해 클래스간 결합도를 일부러 높은 코드도 수두룩하게 보아왔다.

그런 코드에 결합도를 낮추는 일은 쉬운 일이 아니다.

그렇기에 이런 책이 나온게 아닌가 싶다.

Test Driven Refactoring을 알리기 위해서 말이다.