목차

반응형

목차

1. 개요

2. 프로젝트 생성과 세팅

3. 비 개발자와 공유

4. 주의사항

 

 

 

1. 개요

유니티를 사용하면 2명 이상의 사람과 프로젝트를 공유해야 하는 상황이 생긴다.

원래는 개발하는 환경에 따라 다양한 변수가 생겨서 이를 직접 겪어보고 숙지를 해야 하는데 이 글을 통해서 간접적으로 경험해보고 습득할 수 있도록 글을 작성해봤다. 

 

2. 프로젝트 생성과 세팅

유니티 프로젝트를 생성하고 Git Repository에 등록하는 것은 다른 상황과 동일하다.

특이한 점이라면 유니티 프로젝트 내에 Library나 상당한 용량을 차지하는 부분이 존재하는데 gitignore를 통하여 처리해야 한다.

 

유니티에서 ignore 하면 좋은 파일들에 대한 프리셋은 구글에 unity gitignore라고 쳐보면 예시가 많다.

바로 가져다가 써도 될 정도로 좋다.

 

주의할 점은 git은 파일 용량이 50mb가 넘어가는 파일을 올리게 되면 복잡하게 되니까 미연에 방지하도록 하는 것을 추천한다.

 

3. 비 개발자에게 공유

개발자끼리는 얘기가 빠르지만 비개발자(디자이너, 기획자)에게 프로젝트를 공유하고 사용하는 방법을 전달해야 한다.

깃 프로그램을 사용하기 위한 gui툴을 쓰는 것이 좋겠다. 주로 sourcetree나 fork를 사용한다.

 

공유해야 하는 사람은 크게 두 부류로 나뉜다.

프로젝트를 수정할 일이 있는 직군 : 디자이너, 프로그래머, 기획자

프로젝트를 수정할 일이 없는 직군 : QA, CS, PM

 

프로젝트를 수정할 일이 있는 직군에게는 깃을 스스로 사용할 수 있도록 깃의 Commit, Push, Fetch, Branch, Merge Branch, Local Changes, Discard 같은 개념에 대해서 알려주고 학습시켜야 한다.

프로젝트를 수정할 일이 없는 직군에게는 깃을 알려주면 좋긴 하지만 크게 효용성은 없을 것이기 때문에 단순히 최신 깃을 받아오고 Local Changes를 Discard 하는 정도만 알려줘도 되겠다.

 

4. 주의사항

프리팹과 씬

유니티 디자이너들은 프리팹에 대한 개념이 생소할 수 있다. 하지만 git을 이용해서 공동작업을 하기 위해서는 프리팹을 능숙하게 다루는 것이 필수다.

 

그 이유는 씬은 merge관리가 힘들지만 prefab은 merge 관리가 쉽기 때문이다.

정확히는 여러 명이 하나의 씬을 놓고 각자 작업을 해버리면 머지 컨플릭트 에러가 나는데 거의 머지가 불가능할 정도로 불친절하게 정보가 표출된다.

 

반면에 하나의 씬에 모든 것을 프리팹화 시켜놓고 각자 작업할 프리팹을 수정하며 목표로 하는 씬의 직접적인 수정은 가하지 않는다면 머지컨플릭트가 발생하지 않는다.

 

 

 

애니메이션 수정

가끔씩 무언가 수정했는데 local changes에 나타나지 않는 경우가 있다.

예를 들어 애니메이션을 수정한 경우 바로 local changes에 나오지 않는 것을 볼 수 있다.

이런 경우는 씬을 저장하면 해당 수정사항이 적용돼서 local changes에 나타난다.

 

 

씬의 인스펙터가 날아갔을 때

흔한 경우는 아니지만 예전에 내가 유니티가 오피셜로 라이브러리 폴더는 공유할 필요가 없다고 했지만 내가 직접 겪어보기도 했는데 라이브러리 폴더가 없는 상태에서 프로젝트를 공유하면 씬의 모든 인스펙터가 missing이 되는 현상이 발생하고는 한다.

그런 경우 유니티 프로젝트 reimport all을 해주면 간단하게 해결된다.

반응형
반응형

https://www.youtube.com/watch?v=4kVffWfmJ60

 

오늘은 네임드 개발자 오즈라엘님이 "성능 프로파일링과 최적화"라는 귀한 주제로 발표를 해주셨다. 두 시간짜리 영상을 열심히 보고 요약글을 올리는 나한테 감사하자

 

"쌍따봉 드립니다."

 

 

1.  프로파일링 유의사항

2. 유니티 프로파일러

2.1 다양한 병목현상

3. 프레임 디버거

4. 메모리 프로파일러

5. 텍스쳐 컴프레션

6. Xcode Instrument

7. 최적화 팁 & 트릭

 

 

 

 

 

 

 

 

 

 

 

 

1.  프로파일링 유의사항

타겟 플랫폼 선정

많은 팀이 간과하고 있는 것이 원래 최적화를 위해서 타겟 플랫폼과 타겟 디바이스를 정해야 한다.

무조건 좋은 퀄리티만 보여줄 수 없기 때문에 현실적으로 목표하고자 하는 디바이스와 플랫폼에 적합한 퍼포먼스를 보여줘야 한다.

사실 우리 회사도 타겟 플랫폼을 딱히 설정하고 개발을 시작하지 않는다. 하지만 가끔 게임 업계에서도 "ㅁㅁ게임은 동남아시아를 대상으로 한 게임이라서 저성능 기기를 타겟으로 개발을 한다...." 이런 말이 들려오는데 그런 환경이라면 합리적인 타겟 플랫폼 선정이 필수라고 생각이 든다.

 

최적화

최적화란 병목(다양한 원인)을 탐지하고 제거하는 반복 작업을 일컫는다.

 

위의 경우는 GPU 바운드 -> CPU는 할 일이 없지만  GPU에 할일이 많아서 렉걸림

따라서 이런 상황에서는 CPU를 최적화해도 개선이 전혀 안된다.

CPU 바운드 원인 : 로직, AI, 물리연산, 로딩, GC처리

GPU 바운드 원인 : 폴리곤, 텍스쳐, 필터링, 라이트, 프레임 버퍼, 쉐이더, 오버드로우

 

 

주기적인 최적화

프로파일링, 최적화는 개발 마지막 단계에서 하는 것이 아니라 정기적으로 하는 것이다.

그에 따른 빌드 및 QA 프로세스 정립은 필수다.

 

프로파일링 주의 사항

추측에 의한 최적화 금지

실제로 어떤 팀에서 자꾸 퍼포먼스가 안 나와서 어떤 개발자가 텍스쳐가 문제인 것 같아서 텍스쳐를 전부 하향평준화해달라고 아티스트한테 요청해서 적용했는데 해결이 안 됐다. 굉장히 비효율적인 업무방식이다.

 

디버거/프로파일러 툴의 적극적인 활용

결국 최적화는 최적화를 위한 툴들을 얼마나 잘 사용하느냐에 따라 달려있다.

적극적으로 툴을 사용해서 문제를 확인하고 처리하도록 하자.

스냅드래곤, xcode 프로파일러가 그렇게 좋다더라

 

유니티 에디터에서의 프로파일링은 단순 참고용

에디터에서 측정한 성능과 타겟 플랫폼에서 측정한 성능은 전혀 다르게 나올 수 있으니 맹신하면 안 된다.

 

자동화된 테스팅 환경 구축(ex:벤치마커)

매번 테스트할 때마다 빌드, 배포에 시간이 오래 걸리니까 프로젝트 초기에 자동화된 테스트 환경 구축을 하면 좋다.

 

쓰로틀링

모바일은 PC 달리 쿨러가 없어서발열 시 자체적인 하드웨어 성능 제한을 걸어서 성능 저하 발생

최신 휴대기기일수록 취약함(성능이 좋아지지만 사이즈는 그대로라서)

ex) 처음 빌드에는 괜찮았는데 몇번 테스트 다시해보니까 성능이 갑자기 저하되면 쓰로틀링일 있음

스마트폰 쿨러로 식혀주면서 한번 측정하고 몇분뒤에 측정하는 것이 좋다.

실제로 오지현 센세는 열감지기로 핸드폰 온도 측정하고 테스트한다고

 

2. 유니티 프로파일러

커스텀 프로파일러 태그 지정

다들 잘 모르는 기능인데 위와 같이 특정 코드에 대해서 태그를 지정해서 프로파일러에서 태그에 따라 결과를 확인할 수 있다.

 

프로파일러 사용 예시 1)

프로젝트에서 프로파일러 타임라인 찍어보고 오래 걸리는 부분을 제거해주는 예시를 보여주었음.

 

프로파일러 사용 예시 2)

샘플 프로젝트인데 FPS가 6이 나오고 딱 봐도 포스트 프로세싱에 GPU 쪽 부하가 심한 것 같다고 생각은 들지만...!

 

프로파일러를 찍어보면 드로우 콜 1300개 CPU 쪽 문제임을 알 수 있다.

드로우 콜과 CPU 관계 설명

https://funfunhanblog.tistory.com/301

 

Unity) 드로우콜 DrawCall

1. 드로우콜 이란 간단하게 CPU가 GPU에게 렌더링 작업을 수행하도록 명령을 하는것이다. 게임은 실시간 렌더링 어플리케이션이다. 실시간으로 렌더링을 수행하기에, 한 프레임의 렌더링은 오브��

funfunhanblog.tistory.com

 

Profile Analyzer라고 새로 나온 기능이 있는 데 사용을 추천해주셨다.

 

 

 

GPU 프로파일링은 특정 플랫폼에서만 가능하고 쉽지 않다고 하였다.

스냅 드래곤, Xcode의 GPU 프로파일링 툴을 사용하는 것을 추천해주셨다.

 

 

2.1 다양한 병목현상

 

Fillrate 병목 현상

요즘 기기 해상도가 너무 좋아서 Fillrate로 인한 부하가 많다. 확인해보도록 하자

 

오버드로우 병목 현상

오버드로우는 말 그대로 가려진 뒷부분까지 그리는 것을 말한다. 오버드로우를 하게 되면 수많은 파티클이 중첩되면서 성능 저하를 일으킬 수 있다.

평범한 상황에서는 오버드로우로 인한 병목현상이 발생하지 않는다.

 

 

하지만 파티클에서 자주 발생하는 현상이기도 하다. Scene에서 Overdraw로 보기를 해서 오버드로우가 과하게 발생하는지 확인하도록 하자.

 

포스트 프로세싱 부하

포스트 프로세싱도 필레이트에 많은 부하를 준다.

포스트 프로세싱의 원리 : 특정 픽셀의 주변 픽셀을 가져와서 변화를 주는 샘플링 처리를 하기 때문에 필레이트와 굉장히 밀접한 연관성이 있다.

 

업스케일링 샘플링

높은 해상도를 포기하기 힘들다면 업스케일링 샘플링 기법을 추천해주셨다.

 

UI 부분의 해상도만 높은 상태를 유지하고 인게임 해상도를 낮추는 것이다.

실제로 작은 기기에서 보기 때문에 유저들은 쉽게 눈치채지 못한다.

 

오지현님의 업스케일링 샘플링에 대한 샘플 프로젝트 주소

https://github.com/ozlael/UpsamplingRenderingDemo

 

업스케일 샘플링 때문에 해상도가 낮은거지 이미지가 안좋은것이 아님!

이번에 새로 나온 기능인 URP에서는 기본적으로 업스케일링 샘플링을 공식적으로 지원한다.

위 사진은 극단적인 차이를 보여주기 위해서 인게임 해상도를 10%로 낮춘 상황이다.

 

텍스쳐 병목 현상

텍스쳐 퀄리티를 조절해서 병목 확인이 가능하다.

텍스쳐를 1/8 줄여서 플레이 해봤더니 게임의 렉이 없어졌다. -> 텍스쳐 병목이다.

 

3. 프레임 디버거

프레임 디버거를 통해서 어떻게 드로우 콜이 발생하는지 순차적으로 하나하나 볼 수 있다.

게임에서 드로우콜이 많이 발생하는 경우 확인하기 좋다.

 

또한 Batcing처리가 안되었을 때 그 이유도 나온다. ( Batcing에 대한 글은 아래 글 참고 )

https://funfunhanblog.tistory.com/302?category=818491

 

프레임 디버거를 이용한 드로우 콜 최적화 예시) 타워 디펜스

위와 같이 샘플 프로젝트를 플레이하다가 프레임 디버거로 드로우 콜을 확인해보았다.

 

드로우 콜은 245며 캐릭터 하나를 그리는데 캐릭터 이미지, 그림자, 체력바, 체력바 백그라운드 총 4개의 드로우 콜이 발생한다.

배칭이 전혀 안되어있다.

 

똑같은 성질의 이미지에 레이어를 추가해서 똑같은 애들끼리 똑같은 레이어를 적용시켜서 드로우 콜을 줄인다.

기존에는 그림자 하나를 그릴때 하나하나 그렸지만 레이어를 적용하면 그림자가 전부 한번에 그려진다.

나아가서 스프라이트 패킹, 배경 레이어를 추가하게 된다면 드로우콜을 줄일 있다.

 

 

4. 메모리 프로파일러

메모리 관리는 항상 중요하지만 모바일에서는 특히나 중요하다.

PC에서는 특정 어플리케이션에서 물리적으로 수용 가능한 메모리의 범주를 벗어나도 가상 메모리를 통해서해결할 수 있다.

그러나! 모바일에서는 메모리 시스템이 PC와는 달라서 특정 어플리케이션의 메모리 점유율이 높아지면 다른 어플리케이션의 메모리를 빼앗아서 사용하도록 설계되어있어서 메모리가 너무 커지는 순간 해당 앱을 강제종료 시켜버린다강제 종료시켜버린다. (이와 같은 이유로 모바일에서는 로딩화면에서 크러시가 많이 난다.)

실제로 내 게임도 아직 최적화를 거치지 않았는데 아이폰 개발하다가 옛날기종에서 메모리 650mb 넘어가서 어플리케이션이 강제 종료당했다.

 

메모리 프로파일러 예시 1)

메모리 프로파일러를 사용하면 모든 힙 메모리 영역을 트래킹 할 수 있다.

텍스쳐에서 어떤 텍스쳐가 많이 차지하는지 확인 가능하다. 사운드, 폰트 전부다 가능하다!

 

위의 경우는 확인해보니 dude라는 텍스쳐가 사이즈가 4096으로 설정되어있었다.

 

메모리 프로파일러 예시 2)

샘플 프로젝트인데 별거 없는 프로젝트지만 메시 하나가 20MB를 차지한다.

 

Read/Write Enabled : 스크립트를 통해서 메시에 접근할 있는지 여부(메시 변형 가능)

이것을 키게 되면 GPU, CPU 양쪽에 해당 데이터가 적재된다.

스크립트를 통해서 메시를 변경할일이 없으면 반드시 끄도록 하자

 

오디오 클립도 메모리에 올라갔을 때굉장히 큰경우가 있는데 Decompress On Load 음악 압축을 풀어서 메모리에 올리기 때문에 바꾸자.

아이폰에서는 mp3 압축돼서 메모리에 적재 가능하기때문에 mp3 권장

 

5. 텍스쳐 컴프레션

보통 게임에서 텍스쳐가 많은 부하를 차지한다.

 

텍스쳐 이미지의 복잡도, 포맷별로 적재되는 텍스쳐의 용량의 차이가 발생한다.

위의 사진을 보면 PNG, JPG는 1메가가 안되지만 인게임에서는 해당 텍스쳐가 4MB를 차지한다.

그 이유는 PNG, JPG, PSD로 저장되는 텍스쳐의 용량은 단순히 PC에서 저장되기 위한 용량을 말하는 것이지 GPU에서 사용하기 위한 포맷이 아니다.

우리가 알고 있는 텍스쳐 이미지는 GPU에서 사용하기 위해 다시 ETC, ASTC 포맷으로 전환이 일어난다.

 

또한 텍스쳐 압축 방식에 따라 미세하게 다른 차이가 발생하기도 한다.

대표적으로 사용하는 텍스쳐 압축 방식에 대하여 간단히 알아보자

 

 

PVRTC : 원본 이미지를 소수 픽셀로 뽑아내서 압축 -> 이미지가 뭉개지는 현상 발생

 

ETC : 이미지를 색상과 밝기로 나눠서 색상은 낮은 해상도, 밝기는 높은 해상도로 압축하는 방식

(인간의 눈은 색상보다 밝기에 더 민감한 원리를 이용)

 

ETC1 ETC2 있는데 ETC2 좀더 블록현상이 완화됨

 

주의할 점 : 완전 옛날 기기에서 ETC2 포맷을 사용하면 메모리에 원본 텍스쳐가 올라가버린다.

동남아 시장을 타겟으로 하면 ETC2 포맷을 사용하면 안된다.

 

PVRTC 조금 번지고 블러링 되는 느낌이 있다

ETC 의도하지 않은 색상들이 발생한다.(색상과 밝기를 나눠서 처리하기 때문에 옷깃 부분에 검은색 부분이 유난히 더 많이 보인다.)

위와 같이 텍스쳐 압축에 따라 달라지는 특징을 알아둬야 메모리를 절약하면서 퀄리티를 살릴 있다.

 

ASTC(Adaptive Scalable Texture Compression)라는 자유롭게 압축 단위를 바꾸는 압축 기법도 있다.

 

 

좌측 이미지는 캐릭터 이미지라서 민감하기 때문에 최대한 압축을 덜하는 4x4 방식을 택한다면 오른쪽 블러 이미지는 압축을 크게 해도 눈치채기 힘들기 때문에 12x12로 크게 압축을 하는 전략을 말해주셨다.

 

텍스쳐의 해상도는 2의 제곱수로 만들어야 하는 이유도 설명해주었다.

200x200짜리 이미지를 만들어도 어차피 256x256으로 변환돼서 256x256 - 200x200만큼의 공간이 낭비된다는 것이다.

 

 

 

6. Xcode Instrument

유니티 프로파일러에서는 없던 세부 내용까지 있다.

어떤 과정을 통해서 네이티브로 할당되는것까지 확인이가능하다.

 

 

7. 최적화 팁 & 트릭

여태까지 말한 것을 토대로 타겟 플랫폼에 따라 에셋의 세팅도 달라져야 한다는 것을 충분히 이해하리라 생각한다.

이런 식으로 실수 없도록 모든 에셋에 대하여 임포트 옵션을 설정할 수 있다.

 

적절한 targetFrameRate

  • 퍼즐게임이면 굳이 60프레임을 찍을 필요가 없다. : 30으로 해도 된다. -> 발열을 낮출 있음
  • 인게임에서는 60하고 UI에서는 30으로 하는 전략도 있다.
  • Adaptive Performance 통해서 CPU, GPU 부하를 탐지해서 타겟프레임 레이트를 적절하게 바꿔주는 것도 가능하다.

 

에셋 비동기 로딩 사용

AssetBundle.LoadFromFileAsync()

AssetBundle.LoadAssetAsync()

끊김 없이 에셋을 부르기 위한 방법

 

사용하지 않는 Monobehaviors 메소드 제거

  • Start, Update, FixedUpdate 제거
  • Update안의 내용이 비어있더라도 함수를 계속해서 호출하기 때문에 사용하지 않으면 지워줘야 한다.

 

Update 1000개 vs 매니저 패턴 1개

  • 성능상의 차이가 없다고 생각할 수 있지만 유니티 에디터와 C++을 왔다 갔다 하면서 함수 자체만으로 발생하는 오버헤드가 있기 때문에 매니저 패턴을 이용하는 것이 좋다.

 

비싼 API 호출 주의

  • GameObject.Find, GetComponent, Camera.main, Debug.Log
  • 위의 함수들은 잦은 호출 시 성능 저하를 일으킬 수 있는 API라서 제한적인 상황에서 사용해야 한다.
  • 따라서 자주 쓰이는 변수의 경우 Awake나 Start에서 캐싱해놓고 사용하도록 하자

 

Allocating API 주의

  • GetComonents, Animator.parameters와 같이 List 형태의 데이터를 받아오는 API를 호출하게 되면 해당 데이터를 담아두는 변수를 그 즉시 생성하기 때문에 사용할 때마다 계속해서 리스트를 생성하는 것이기 때문에 유의해서 사용해야 한다. 

 

정적 데이터 파싱 주의

  • 인게임에서 필요할때마다 xml, json 불러오는것보다 so 바꿔서 처리하는게 좋다.
반응형
반응형

유니티에서 Scripts Only Build를 하려고 하면

Patcing disabled due Strip Engine Code라고 하면서 빌드가 비활성화 된다.

 

Project Settigns > Player > Configuration > Scripting Backend를 IL2CPP에서 Mono로 바꿔주면 된다.

 

 

반응형
반응형

https://www.youtube.com/watch?v=Kel7NIP2AOg

유니티 데브윅스에서 발표한 주제 "유니티 워크플로 속도 향상을 위한 기능 소개"에 대한 간략한 요약을 하겠다.

 

발표 개요

1. 플레이 모드

2. 애셋 임포트

3. 애셋 관리

4. 디바이스 프리뷰

5. 패치 빌드

6. 기타

 

 

 

 

 

 

 

발표 개요

유니티로 개발을 하다보면 유니티의 자체적인 이슈로 개발자의 시간을 잡아먹는 부분이(애셋 임포트, 자체적인 빌드 타임 등) 존재한다.

특히 그중에서도 빌드와 관련된 부분에 대하여 발표자님께서 대안에 대하여 말씀하셨다.

모든 문제에 대하여 완벽한 대안은 아니더라도 앞으로 계속해서 고쳐나갈 것이고 유니티 내부 개발진들도 사용자에게 더욱 편한 환경을 제공하기 위하여 장기적으로 개발을 염두에 두고 있다고 한다.

여러 가지 대안들 중에서도 크게 다섯 가지의 대안과 개선안을 발표해주셨다.

 

 

 

1. 플레이 모드

에디터에서 플레이를 누르면 걸리는 시간(현재 프로젝트의 크기에 따라 수초~수십 초)을 단축

 

사용 방법은 Project Settings > Enter Play Mode Settings > Enter Play Mode Options를 체크하면 된다.

Reload Domain과 Reload Scene에 대한 설명은 아래 있다.

 

(실제로 실행을누르자마자 바로 실행이 될 정도로 엄청난 체감효과를 자랑한다.)

 

원리

유니티 에디터단에서 게임을 실행하면 위와 같은 단계를 거치게 되는데 특정 단계 중 도메인 리로드, 씬 리로드 부분에서 상당수 시간을 차지한다.

도메인 리로드 : 스크립트 연관, 스크립트의 deserialization, serialization, 물리 디스크 File IO, 어셈블 로드

씬 리로드 : 씬의 오브젝트 연관, 에디팅 모드에서 씬의 게임 오브젝트를 바로 사용하는 것이 아니라 전부다 destroy 하고 다시 생성

 

위의 두 단계를 최대한 생략, 단축해버리는 것이다.

해당 기능에서 Reload Domain과 Reload Scene를 체크하게 되면 해당 부분을 스킵하지 않게 된다.

 

주의사항

해당 기능은 Experimental 단계라서 무결성을 보장하지 않는다. 다음과 같이 알려진 부작용도 가지고 있다.

 - HDRP에서 버그가 많음

 - 써드파티를 고려하고 제작된 기능이 아님 : 써드파티에서 무수한 오류를 맛보게 될 수 있음.

 - 스태틱 필드 리셋 안됨 : 게임을 시작할때마다 스태틱 필드가 초기화돼야 하지만 그렇지 아니함.

 

질문

Enter Play Mode 사용시 UGUI 가 제대로 업데이트가 안됩니다!

아직 실험단계라서 유니티에서 제공하는 모든 부분이 검증되지 않았습니다. 버그 제보 시 처리하도록 하겠습니다.

 

Domain Reload와 Scene Reload 체크시 기존과 차이가 없나요?

완전히 똑같게 됩니다.

 

프로토타입 단계, 소규모 프로젝트에서 정말 유용하게 사용 가능하다고 생각한다.

 

 

 

2. 애셋 임포트

에셋 임포트 하는 시간을 단축, 개선

 

설정 방법(유니티 2019.3 버전부터는 자동으로 Version2로 설정된다.)

Project Settings > Editor > Asset Pipeline > Version 2로 설정

 

사실 해당 설정에 대한 기본값이 최신 버전을 사용하는 사람이라면 Version 2로 되어있어서 이미 사용 중이라는 것을 눈치채지 못하고 있을 수 있다.(내가 그렇다)

기존 방식에서는 타겟 플랫폼을 스위칭하면 기존 캐싱이 전부 다 사라지는데 Version 2부터는 플랫폼마다 캐싱 데이터가 남아있어서 빠른 타겟 플랫폼의 스위칭이 가능하다.

 

원리

유니티 앤젠 내부에서 리팩토링을 많이 한 기능이라고 한다. 유니티 사용자는 크게 눈치채지 못할 수 있다.

애셋을 임포트하는 알고리즘 또한 개선하였다.

 

주의사항

해당 기능을 사용하게 되면 디스크 용량을 더 크게 사용할 수 있다. 그래서 에디터를 재시작하면 안 쓰는 데이터는 자동으로 삭제하도록 개발하였다.

 

개발자 코멘트

앞으로도 계속해서 발전시켜나갈 부분이며 두 가지 개발중인 기능이 있다.

Background import

말그대로 지금은 애셋을 임포팅 하면 유니티 포커스를 프리징 해서 작업을 못하도록 막는데 백그라운드에서 해당 작업이 진행되도록 개발 중

On-demand import

백그라운드 임포트와 찰떡궁합인 기능인데 임포팅 하는 리소스의 우선순위를 현재 유저가 작업하고 있는 부분에 맞춰서 임포팅하는 기능이다.

 

 

3. 애셋 관리

애셋의 물리적인 위치에 구애받지 않고 자체적인 애셋의 경로를 등록함으로써 코드상에서 보다 일관성 있고 쉽게 애셋에 액세스

 

기존 애셋 시스템에서는 아래와 같이 주어진 상황마다 애셋에 도달하기 위한 주소가 바뀐다.

 - 애셋을 서버에서 리모트로 받은 경우

 - 애셋이 리소스 폴더에 있는 경우

 - 애셋을 애셋 번들로 접근하는 경우

...

 

 

어드레서블 애셋 시스템을 사용하면 애셋에 자체적인 접근 주소를 부여하여 맥락과 관계없이 애셋에 접근이 가능하다.

 

에셋 프로파일

에셋 프로파일을 통하여 애셋에 대한 상황을 설정하여 테스트해 볼 수 있는 기능이 생겼다.

 

애셋 프로파일러

이미지는 없지만 애셋 프로파일러라는 기능도 소개해주었는데 특정 애셋별로 몇 번 사용했고 언제 사용됐는지 확인하는 기능(최적화)

 

 

 

 

 

4. 디바이스 프리뷰

디바이스 별로 어떻게 렌더링이 되는지 확인하는 기능(해상도 대응, 노치 확인)

 

 

 

실제 내 프로젝트에도 적용시켜봤다. 뒷목;;

개발자뿐만 아니라 디자이너들도 알면 좋은 기능이라고 생각한다.

 

사용 방법은 아래 블로그 글에 잘 나와있다.

https://skuld2000.tistory.com/79

 

[Unity] 유니티에 새로 추가되는 디바이스 시뮬레이터(Device Simulator) 소개

1. 유니티 디바이스 시뮬레이터 (Unity Device Simulator) 유니티 디바이스 시뮬레이터(Unity Device Simulator)는 유니티 에디터에서 개발중인 게임을 실행했을 때 Game 윈도우에서 실행되는 대신 Game XCode 나..

skuld2000.tistory.com

 

 

주의사항

CPU, GPU와 같이 성능적인 부분은 재현되지 않는다.

 

 

5. 패치 빌드

변경된 스크립트 부분만 업데이트 형태로 디바이스에 설치하며 빌드 시간을 단축시키는 기능

 

Scripts Only Build > Patch And Run을 하게 되면 프로젝트의 스크립트 중에서도 변경이 발생한 부분만 빌드하여 디바이스에 패치 형태로 빌드를 한다.

 

위와 같이 빌드 시간을 크게 단축할 수 있다.

 

주의사항

Scripts Only Build는 안드로이드만 지원한다. (망해라 ios ^^)

 

 

 

6. 기타

안드로이드 로그 캣을 유니티에서 지원한다.

 

이전에 Unity Remote라는 기능이 있었는데 해당 기능을 계승하는 Unity Live Link라는 기능을 개발하고 있다.

유니티 에디터에서 디바이스에 연결하여 디바이스에 구동 중인 프로젝트를 수정 

 

ㅋㅋㅋ 발표 중에 자꾸 오지현 님이 강사님 놀려서 개 웃겼다. ㅋㅋㅋㅋㅋㅋ

자꾸 놀려서 마지막에는 결국 관리계정이 그만하라고 했다 ㅋㅋㅋㅋ 개웃 ㅋㅋㅋㅋ

반응형
반응형

개요

유니티 빌드시 아래 오류가 나서 multidex 설정을 했음에도 불구하고 계속 이 에러가 나는 사람을 대상으로 하는 글이다.

The number of method references in a .dex file cannot exceed 64K

 

https://stackoverflow.com/questions/36785014/the-number-of-method-references-in-a-dex-file-cannot-exceed-64k-api-17

 

The number of method references in a .dex file cannot exceed 64k API 17

I am building an app with SugarORM Library but when I try to build the project for API 17 (didn't check for others) it shows build error. Information:Gradle tasks [:app:assembleDebug] :app:pre...

stackoverflow.com

위 게시물에 설명이 자세하게 나와있다.

 

하지만 나는 설정대로 했지만 계속해서 The number of method references in a .dex file cannot exceed 64K 오류가 났다.

처음에는 특정 라이브러리 문제인가 싶었는데 다른문제였다.

 

해결방법

multiDexEnabled true를 mainTemplate.gradle 파일에 작성하라고 써있지만 그것이 아니라

 

Custom Launcher Gradle Template을 활성화한다음 생성되는 gradle 파일에다가 multiDexEnabled true를 해주면 해결된다.

 

반응형
반응형

목차

1. 원리 파악하기

1.1 검색 조건 정의

1.2 엑셀 데이터를 행 단위로 쪼개기

 

1.3 행 단위로 쪼갠 데이터를 열 단위로 다시 쪼개기

 

2. 데이터 매칭을 위한 키

 

 

 

 

 

1. 원리 파악하기

지난번 강좌에서 TestData로 데이터를 불러오는 부분에 대해서 설명을 하고자 한다.

해당 설명을 들어가기 전에 앞서 스크립터블 오브젝트라는 것에 대한 이해를 하고 있어야 한다.

스크립터블 오브젝트는 추후에 설명하는 글을 써야겠다. 일단은 모른다면 친절한 글이 많으니까 이해하고 오도록 하자

 

TestData라는 오브젝트의 스크립트를 열어보면 크게 두 부분으로 나뉜다.

 

테스트 데이터 객체에 대한 정의

public class TestData : ScriptableObject
{
    public string associatedSheet = "";
    public string associatedWorksheet = "";

    public List<string> items = new List<string>();
    
    public List<string> Names = new List<string>();
    
    //1.3 긁어온 행 데이터를 조회하는 부분
    internal void UpdateStats(List<GSTU_Cell> list, string name)
    {
        items.Clear();
        int math=0, korean=0, english=0;
        for (int i = 0; i < list.Count; i++)
        {
            switch (list[i].columnId)
            {
                case "Math":
                {
                    math = int.Parse(list[i].value);
                    break;
                }
                case "Korean":
                {
                    korean = int.Parse(list[i].value);
                    break;
                }
                case "English":
                {
                    english = int.Parse(list[i].value);
                    break;
                }
            }
        }
        Debug.Log($"{name}의 점수 수학:{math} 국어:{korean} 영어:{english}");
    }

}

 

생성된 테스트 객체의 커스텀 에디터(스크립터블 오브젝트의 인스펙터 부분이라고 생각하면 된다.)

[CustomEditor(typeof(TestData))]
public class DataEditor : Editor
{
    TestData data;

    void OnEnable()
    {
        data = (TestData)target;
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        GUILayout.Label("Read Data Examples");

        if (GUILayout.Button("Pull Data Method One"))
        {
            UpdateStats(UpdateMethodOne);
        }
    }

//1.1 GSTU_Search 객체를 생성하는 부분
    void UpdateStats(UnityAction<GstuSpreadSheet> callback, bool mergedCells = false)
    {
        SpreadsheetManager.Read(new GSTU_Search(data.associatedSheet, data.associatedWorksheet), callback, mergedCells);
    }

    void UpdateMethodOne(GstuSpreadSheet ss)
    {
        //1.2 행 데이터를 긁어오기
        foreach (string dataName in data.Names)
            data.UpdateStats(ss.rows[dataName], dataName);
        EditorUtility.SetDirty(target);
    }
    
}

 

 

DataEditor부분에서 생성된 Pull Data Method One 버튼을 클릭하면 TestData의 UpdateMethodOne을 호출한다고 생각하면된다.(정확히는 TestDataEditor의 UpdateStats 함수를 호출하고 결과 콜백을 TestData의 UpdateMethodOne으로 넘겨준다가 맞다. 해당 강좌의 주제는 엑셀이라서 이런 건 몰라도 크게 상관없다.)

 

중점적으로 봐야 하는 부분은 세 곳이다.

 

1.1 검색 조건 정의

void UpdateStats(UnityAction<GstuSpreadSheet> callback, bool mergedCells = false)
{
	SpreadsheetManager.Read(new GSTU_Search(data.associatedSheet, data.associatedWorksheet), callback, mergedCells);
}

여기서 new GSTU_Search로 GSTU_Search 객체를 생성하는 부분이 매우 중요하다.

데이터 검색을 하기 전에 조건을 설정하는 부분이다.

 

GSTU_Search 클래스의 정의를 보자.

public class GSTU_Search
    {
        public readonly string sheetId = "";
        public readonly string worksheetName = "Sheet1";

        public readonly string startCell = "A1";
        public readonly string endCell = "Z100";

        public readonly string titleColumn = "A";
        public readonly int titleRow = 1;

        public GSTU_Search(string sheetId, string worksheetName)
        {
            this.sheetId = sheetId;
            this.worksheetName = worksheetName;
        }

        public GSTU_Search(string sheetId, string worksheetName, string startCell)
        {
            this.sheetId = sheetId;
            this.worksheetName = worksheetName;
            this.startCell = startCell;
        }

        public GSTU_Search(string sheetId, string worksheetName, string startCell, string endCell)
        {
            this.sheetId = sheetId;
            this.worksheetName = worksheetName;
            this.startCell = startCell;
            this.endCell = endCell;
        }

        public GSTU_Search(string sheetId, string worksheetName, string startCell, string endCell, string titleColumn, int titleRow)
        {
            this.sheetId = sheetId;
            this.worksheetName = worksheetName;
            this.startCell = startCell;
            this.endCell = endCell;
            this.titleColumn = titleColumn;
            this.titleRow = titleRow;
        }
    }

 

 

다양한 형태의 GSTU_Search 생성자가 있는데 검색 시작 셀을 지정하고 워크시트의 이름을 정하는 등 다양한 기능을 내포하고 있다.

가장 기본 형태의 GSTU_Search를 생성하였기 때문에 Sheet1 이름을 가진 시트의 A1 셀부터 Z100셀까지 쭈욱 긁어오는 기본적인 검색을 하는 것이다.

일단 검색을 하려면 검색 객체를 만들어야 한다는 것만 알아두고 넘어가자 계속 설명할 것이다.

 

1.2 엑셀 데이터를 행 단위로 쪼개기

데이터를 불러왔으면 데이터의 행단 위로 쪼개서 분석을 해야 한다.

 

void UpdateMethodOne(GstuSpreadSheet ss)
    {
        //data.UpdateStats(ss.rows["Jim"]);
        foreach (string dataName in data.Names)
            data.UpdateStats(ss.rows[dataName], dataName);
        EditorUtility.SetDirty(target);
    }

data.Names에는 Jim, Jay, Jack이라는 행 데이터의 키 역할을 하는 값들이 들어있어서 ss.rows[dataName] 을 통해 행중에서 Jim, Jay, Jack 즉 3 개행의 데이터를 뽑아낸 것이다.

 

1.3 행 단위로 쪼갠 데이터를 열 단위로 다시 쪼개기

뽑아낸 행 데이터를 다시 열 단위로 쪼개서 조회해야 한다.

열 단위 데이터 조회하는 부분은 총 세 가지의 샘플 코드가 있다.

세 가지 형태의 샘플 코드를 한번 봐보자.

//1번 방식
internal void UpdateStats(List<GSTU_Cell> list)
    {
        items.Clear();

        for (int i = 0; i < list.Count; i++)
        {
            switch (list[i].columnId)
            {
                case "Health":
                    {
                        health = int.Parse(list[i].value);
                        break;
                    }
                case "Attack":
                    {
                        attack = int.Parse(list[i].value);
                        break;
                    }
                case "Defence":
                    {
                        defence = int.Parse(list[i].value);
                        break;
                    }
                case "Items":
                    {
                        items.Add(list[i].value.ToString());
                        break;
                    }
            }
        }
    }

//2번 방식
    internal void UpdateStats(GstuSpreadSheet ss)
    {
        items.Clear();
        health = int.Parse(ss[name, "Health"].value);
        attack = int.Parse(ss[name, "Attack"].value);
        defence = int.Parse(ss[name, "Defence"].value);
        items.Add(ss[name, "Items"].value.ToString());
    }

//3번 방식
    internal void UpdateStats(GstuSpreadSheet ss, bool mergedCells)
    {
        items.Clear();
        health = int.Parse(ss[name, "Health"].value);
        attack = int.Parse(ss[name, "Attack"].value);
        defence = int.Parse(ss[name, "Defence"].value);

        //I know that my items column may contain multiple values so we run a for loop to ensure they are all added
        foreach (var value in ss[name, "Items", true])
        {
            items.Add(value.value.ToString());
        }
    }

방법은 다르지만 결국은 똑같은 시트의 Health, Attack, Defence, Items를 긁어오는 것이다.

그냥 이중에 하나만 잘 써도 충분하다. 나는 사실 첫 번째 방법만 계속 써서 다른 두 가지 방법이 있는지 몰랐을 정도로 잘 썼다.

 

internal void UpdateStats(List<GSTU_Cell> list, string name)
    {
        items.Clear();
        int math=0, korean=0, english=0;
        for (int i = 0; i < list.Count; i++)
        {
            switch (list[i].columnId)
            {
                case "Math":
                {
                    math = int.Parse(list[i].value);
                    break;
                }
                case "Korean":
                {
                    korean = int.Parse(list[i].value);
                    break;
                }
                case "English":
                {
                    english = int.Parse(list[i].value);
                    break;
                }
            }
        }
        Debug.Log($"{name}의 점수 수학:{math} 국어:{korean} 영어:{english}");
    }

위의 코드는 첫 번째 방법을 조금 변형한 것이다.

행 단위 데이터를 이루고 있는 열 단위의 데이터 중에서 switch문을 통하여 칼럼 값이 내가 필요한 Math, Korean, English 부분만을 긁어오는 것이다.

 

 

2. 데이터 매칭을 위한 키

이쯤에서 데이터 매칭을 위한 키가 필요하다는 부분을 한번 짚고 넘어가도록 하겠다.

 

당신이 엑셀 아래와 같은 데이터 시트가 있다고 하자.

 

  체력 공격력 방어력
원숭이 99 9 9
군인 99 10 10
고릴라 9999 9999 9999

위 시트의 데이터를 긁어오는 것은 어렵지 않다 충분히 구현할 수 있다.

문제는 캐릭터를 생성할 때마다 시트를 긁어와서 넣어주면 너무 비효율적이기 때문에 한번 긁어와 놓고 어딘가에 저 데이터를 저장해놓고 사용해야 한다는 점이다.

어떻게 할 것인가? 한번 고민해보도록 하자.

우리 강의는 시청자가 고민할 시간을 제공한다.

 

 

 

 

 

 

 

 

 

 

 

 

 

바로 스크롤을 슉슉 내렸는가 사실 나라도 그랬을 것 같다.

여하튼 나의 경우에는 스크립터블 오브젝트를 만들어놓고 해당 스크립터블 오브젝트에 엑셀에서 긁어온 값들을 넣어놓고 사용한다. 그리고 그 스크립터블 오브젝트를 전부 다 들고 있고 관리하는 로컬 데이터베이스 클래스가 있다.

요 구조에 대해서는 나중에 또 실습을 해볼 것이다.

그전에 지금 얘기하고 있는 중요한 부분 데이터 매칭을 위한 키를 정해야 한다는 점에 대해서 얘기하겠다.

 

엑셀에서 데이터를 긁어왔는데 긁어온 데이터는 스스로 목적지를 찾아서 들어가 주지 않는다.

즉 유니티 내부에서 저장해놓을 곳과 엑셀 데이터의 매칭을 위한 약속을 해야 한다는 것이다.

이 부분을 깔끔하게 정하지 않으면 더러운 하드코딩으로 악순환이 돼버릴 수 있다.

 

기본적으로는 절대로 변하지 않는 데이터에 대한 키 값을 정해야 한다.(또한 키가 키 외에 다른 역할을 하지 않도록 하자)

아래 표를 보면 원숭이, 군인, 고릴라는 키 역할을 하면서 동시에 게임 내부에서 오브젝트 명까지 맡고 있다. 충분히 바뀌지 않을 수 있지만 오브젝트 명은 언제든지 바뀔 수 있다. 그렇기 때문에 분리를 해야 한다.

  체력 공격력 방어력
원숭이 99 9 9
군인 99 10 10
고릴라 9999 9999 9999

  이름 체력 공격력 방어력
char_monkey 숭숭이 99 9 9
char_solider 군인 99 10 10
char_gorilla 고륄라 9999 9999 9999
  이름 체력 공격력 방어력
001 숭숭이 99 9 9
002 군인 99 10 10
003 고륄라 9999 9999 9999

이런식으로 분리를 하자는 것이다. 기왕이면 키만 봐도 어떤 데이터인지 알 수 있는 char_monkey 형태가 좋은 것 같다. 나는 001 형식으로 인덱스 값을 부여해서 사용하는데 문제는 딱히 없다.

 

 

유니티 구글 스프레드 시트 연동 - 설정

유니티 구글 스프레드 시트 연동 - 원리 파악

 

반응형
반응형

목차

1. 개요

2. 유니티 플러그인 정보

3. 설정

 

 

1. 개요

유니티에서 구글 스프레드 시트를 연동하는 방법에 대하여 글을 써본다.

 

이미 구글에 관련 게시물들이 올라와있는데 왜 또 쓰는 거냐?(이 게시물은 다른 게시물들과 뭐가 다른 거냐?)

나는 원래 이미 다뤄진 이슈는 잘 안 다루지만 구글 스프레드 시트 연동하는 포스팅을 봤는데 대부분이 정말 초기 세팅 부분만 정리해놓고 그 이후 활용에 대한 내용이 없었다. 그 부분을 널리 알리고자 쓰게 되었다.

 

어떤 경우에 쓰는가?

게임을 혼자서 만들면 굳이 구글 엑셀 시트까지 연동할 필요 없이 넣어주면 된다. 협업 환경에서 기획자가 밸런스나 수치 데이터를 조절할 때마다 내가 할 수는 없으니까, 이를 쉽게 조절할 수 있도록 만드는 경우에 사용한다.

 

어디까지 구글 시트로 연동해야 하는가?

행여나 구글 시트로 연동한다고 했을 때 게임의 수치적인 모든 것을 스프레드 시트로 관리하는 것이 꼭 능사가 아닐 수도 있다.

선별적으로 생각해서 정말로 시트로 관리할 가치가 있는 부분만 시트로 관리하도록 하자. 별로 수정할 일도 없는 부분을 구글 시트로 빼느라 고생하지 말자는 뜻이다.

 

참고사항

참고로 이번 주제에서는 데이터 읽기만 할 것이다. 쓰기는 안한다.

주변의 조언을 종합해서 짧게 말하자면 엑셀 데이터를 긁어오는 것은 개발단계, 관리수준에서 데이터를 쉽게 관리하기 위함이지 게임이 출시되고 나서 인게임에서 엑셀로 데이터를 긁어오는 상황을 위한 것이 아니다.(보안 취약) 따라서 엑셀 시트와 관련된 부분을 전처리하는것이 좋겠다.

 

2. 플러그인 정보

 

https://assetstore.unity.com/packages/tools/utilities/google-sheets-to-unity-73410

 

Google Sheets To Unity | 유틸리티 도구 | Unity Asset Store

Use the Google Sheets To Unity from Greener Games on your next project. Find this utility tool & more on the Unity Asset Store.

assetstore.unity.com

 

유니티에서 해당 플러그인을 설치하자

 

정상적으로 설치가 끝나면 Window -> GSTU 가 생긴다.

 

3. 설정

이 게시물에서는 이 부분이 제일 중요하지만 지루하니까 간단명료하게 설명하겠다.

 

https://console.developers.google.com/

위 페이지 접속, 구글 계정으로 로그인

(사용하고자 하는 구글 스프레드 시트가 로그인된 구글 계정에서 접속 가능해야 한다.)

 

최초 로그인 시 뜨는 부분 : 안 뜨면 넘겨도 됨

최초로 접속한다면 위와 같이 뜬다. 체크하고 동의하자

 

현재 프로젝트가 아예 없어서 새로 생성해야 한다.

 

생성하고 나서 해당 프로젝트로 들어가서 서비스 검색창에 google sheets를 검색해서 들어간 뒤 사용을 눌러서 구글 스프레드 시트를 활성화한다.

 

사용을

 

왼쪽 메뉴 버튼을 눌러서 "API 및 서비스 > 사용자 인증 정보" 로 들어간다.

 

사용자 인증 정보 만들기 > OAuth 클라이언트 ID를 누른다.

 

 

ID를 만들기 전에 프로젝트 정보를 입력해야 한다고 뜬다.

 

외부를 선택하고 애플리케이션 이름을 정한다.

 

위와 같이 선택한다.

 

그러면 클라이언트 ID와 비밀번호가 나온다. 위 데이터를 바로 사용해야 하기 때문에 현재 페이지에서 움직이지 말고 내버려두자

 

유니티로 돌아가서 Window > GSTU > Open Config 로 들어간다.

 

Private 탭에서 위와 같이 복사해서 정보를 기입하고 Port Number는 8080으로 하고 Build Connection을 눌러주자

 

프로젝트를 생성한 구글 계정과 동일한 계정으로 로그인 하자.

 

그러면 위와 같은 창이 뜨는데 "고급 설정 > 이동"을 해준다.

 

권한을 요청하는데 수락해준다.

 

연동되었다고 뜬다. 끝이다. 한 번만 해주면 된다.

 

 

3. 테스트 시트 연동, 데이터 불러오기

그럼 시트를 연동해보도록 하자

 

먼저 프로젝트에서 구글 시트 플러그인을 불러온다.

 

그리고 아래 파일을 다운로드하여서 유니티에서 불러오자.

ReadTest.unitypackage
0.00MB

 

그러면 Google Sheets to Unity > Test > TestData 파일이 있을 것이다.

해당 파일을 클릭하고 Associated Sheet와 Associated Worksheet를 실제 시트에 맞는 데이터로 채워줘야 한다.

 

구글 스프레드시트로 가서 페이지를 하나 만든다.

 

데이터의 형태는 위와 같이 적어놓는다.

 

주소에서 시트의 키와 시트의 이름을 복사하고 아까 유니티의 창에다가 넣고 Pull Data Method One을 눌러준다.

 

위와 같이 데이터가 정상적으로 읽힌다면 성공이다.

엑셀 시트에서 값을 수정하고 읽어오면 바로 바뀐다.

 

다음 포스트는 스프레드 시트를 읽어오는 코드에 대해서 설명하고 복잡한 데이터를 읽어오는 연습을 해보겠다.

 

 

유니티 구글 스프레드 시트 연동 - 설정

유니티 구글 스프레드 시트 연동 - 원리 파악

반응형
반응형

1. 개요

2. 메모리 누수란?

3. 메모리 누수 확인

4. 노드 크롬 디버거

5. 누수 발생 확인

 

 

 

 

수많은 글들이 노드의 메모리 누수에 대한 글들을 설명하고 해결방법, 대안을 포스팅했지만 정말 겉만 번지르르하고 도움이 안 됐다.

사실 좀 화가 났다. 왜냐면 나는 개발을 다 해놓고 앱을 돌려보니까 메모리 누수가 발생했는데 어디서 누수가 발생했는지 감도 안 잡히고 찾아보면 대부분의 게시물이 '메모리 누수는 스택과 힙 영역에서 머시기머시기 -> 정말 간단한 테스트 코드로 메모리 할당된 거 보여주면서 요런 식으로 메모리 누수가 발생하고 이렇게 하면 안 됩니다.~~' 이렇게 끝난다. 뭐 어따쓰라고 ㅡㅡ

그래서 직접 부딪혀보면서 메모리 누수를 찾고 해결해내었다. 결론부터 말하자면 개발을 엄청 잘한다고 메모리 누수를 피해 갈 수 있는 것이 아니다. 왜냐면 정말 많은 사람들이 사용하는 라이브러리에서도 엄청난 메모리 누수가 발생하였기 때문이다.

즉 당신은 메모리 누수가 발생하였으면 직접 덤프를 까보며 분석하고 해결하는 능력을 길러야 한다.

앞으로 포스팅할 글들은 실제 개발하다가 메모리 누수가 발생하였고 누수에 대한 확인, 해결방법을 제시하도록 하겠다.

꽤나 가치 있는 게시물이 될 것이라고 장담한다.

 

2. 메모리 누수란?

메모리 누수에 대한 간단한 설명을 하겠다.

 

당신은 온라인 게임을 만들었다. 한 명의 유저가 접속하면 유저에 대한 객체를 생성하는데 1MB가 필요하다.

 

 

축하한다. 당신이 만든 온라인 게임에 유저 100명이 들어왔다. 100MB의 메모리를 할당받았다.

 

 

안타깝게도 당신의 게임이 너무 빨리 질려버린 탓에 50명의 유저가 종료를 해버렸다.

그래서 50개의 유저 데이터를 파괴하고 50MB의 메모리를 다시 돌려받아야 한다.

GC는 이러한 과정 속에서 사용이 완료되고 다시 사용할 수 있도록 메모리를 수거하고 다시 사용 가능하도록 관리한다.

 

여기서 문제가 발생한다. 정체불명의 이유로 인해서 유저 50명이 접속을 종료해도 GC가 판단하기에 50명의 데이터를 담 고 있던 메모리가 아직 사용이 완료됐다고 판단하지 않아서 메모리를 수거하지 않고 계속 남아있는 것이다.

 

 

이것이 계속 반복되면 결국 앱은 무한대로 메모리를 점유하게 될 것이고 언젠가 앱이 강제로 종료될 것이다.

 

메모리 할당 -> 사용 끝 -> GC가 수거 못함 -> 아무도 사용 안 하지만 계속 할당된 채로 남아있음 -> 반복 -> 서버 폭발

 

3. 메모리 누수 확인

그러면 실제 개발하다가 메모리 누수가 발생하면 어떻게 되는지 확인해보자

일단 먼저 본인의 앱에서 메모리 누수가 발생한다는 것을 인지해야 한다.

왜냐하면 당신의 앱에서도 충분히 메모리 누수가 발생하고 있지만 그 정도가 

나는 메모리 누수에 대한 개념이 부족한 때라서 메모리 누수를 인지하는데 상당히 오래 걸렸다.

 

작업 관리자에서 Node가 차지하는 메모리를 보면 2000mb인 것을 볼 수 있다. 말도 안 되는 상황이지 않는가?

 

 

4. 노드 크롬 디버거

먼저 노드의 메모리 누수를 확인하기 위해서는 노드 크롬 디버거를 사용해야 한다.

노드 크롬 디버거를 사용하는 방법은 다음과 같다.

 

node --inspect 시작 파일

 

그러면 위와 같은 메시지가 추가로 나온다. 포트가 9229여야 하는데 꼭 9229일 필요는 없지만 구글 크롬에서 기본값으로 부착되는 포트가 9229라서 편하다

 

구글 크롬으로 들어간다.

 

URL에 chrome://inspect로 접근한다.

 

위와 같이 하단에 구동 중인 서버가 보인다. inspect를 눌러서 디버거를 켠다.

 

Memory > Profiles의 메모리 부분을 보면 현재 애플리케이션이 사용 중인 메모리를 실시간으로 볼 수 있다.

저 상태에서 메모리 부하를 일으키는 작업을 실행해보자 메모리가 하늘 높은 줄 모르고 계속 치솟는다.

 

그러다가 앱이 멈추면서 특정 부분에서 break를 한다. 우측을 보면 Paused before potential out-of-memory crash라고 나와있다. 메모리 부하가 심해서 임종 직전에 멈춘 것이라고 한다.

 

 

5. 누수 발생 확인

메모리 누수가 발생하는 원인을 확인해보는 여러 가지 방법을 설명하도록 하겠다.

 

메모리 스냅샷을 이용하여 부하를 일으키는 메모리는 어떤 종류인지 확인해보겠다.

메모리 부하를 일으킨 다음에 메모리 스냅을 떠서 용량이 많이 차지하는 덩어리를 한번 까보는 것이다.

 

일단 아무것도 하지 않은 위 상태에서 스냅샷을 한번 찍는다.

 

 

이전 단계와 마찬가지로 부하를 일으켜보고 어느 정도 됐다 싶으면 스냅을 한번 더 찍는다.

 

두 개의 스냅이 저장됐다.

 

각 스냅의 Statics를 확인해보자.

 

좌측은 정상적인 어플리케이션 메모리의 분포 상황이고 우측은 딱봐도 이상하리만큼 Strings로 꽉찬 메모리 분포 상황이다. 이정도면 엄청나게 많은 string을 만들어놓고 gc 처리를 못해서 메모리 부하가 생긴다고 킹리적 갓심이 가능한 상황이다. ㅇㅈ?ㅇㅈ

 

 

이번에는 저 많은 string이 어떤 부분에서 발생하는지 확인해보도록 하자.

 

앱을 다시 시작하고 부하를 주지 않은 상태에서 Allocation sampling 을 선택하고 Start를 누르고 어느정도 부하가 되면 Stop을 눌러서 결과를 살펴보자.

Allocation sampling은 메모리 부하를 주는 자바스크립트의 메쏘드를 살펴보는 기능이다.

 

Tree형식으로 확인해보자

 

혼자서 무려 메모리 사이즈의 99.99%를 차지하는 불순한 녀석이 보인다. 저 녀석의 하위 트리를 따라내려가보면서 쭈욱 살펴보도록 하자

 

jsonparse라는 내가 설치한 라이브러리가 해당 문제를 일으키고 있으며 해당 라이브러리의 문제를 일으키는 메쏘드들을 확인할 수 있다.

 

그러면 마지막으로 메모리 누수가 발생하는데 어떤 형태의 데이터가 쌓이는지 그 값들을 직접 확인해보자

 

다시한번더 어플리케이션의 초기상태, 부하상태의 스냅샷을 각각 찍어보자

 

All Obejcts를 두 스냅샷 사이이로 해주고 Summary를 Comparison으로 해주자

 

 

그러면 두 스냅샷 사이에서 생성된 데이터를 볼 수 있다.

 

Alloc.Size로 내림차순 정렬을 하고 상단의 트리를 열어보자

 

그러면 어 저거 내가 생성한 데이터인데 왜 저기있지? 싶은것이 있으면 원인을 찾은 것이다.

분명 저 데이터는 처리되고 말소되어서 GC가 수거해가야하는데 수거하지 못하게끔 어딘가에 계속 맞물려있는 것이다.

이렇게 해서 원인을 찾아봤고 다음은 해결하는 방법에 대해서 작성해보겠다.

 

 

Nodejs - 메모리 JavaScript heap out of memory

Nodejs - 메모리 할당 사이즈 변경

Nodejs - 메모리 누수 확인

반응형
반응형

Nodejs의 기본 메모리 사이즈를 512MB 초과하면 자동으로 앱이 종료된다.

기본 메모리 사이즈를 늘리는 방법에 대해서 알아보자.

 

커맨드라인 환경에서는 node --max-old-space-size=12000 app.js 로 하면 된다.

숫자는 mb를 의미하는데 12000이면 대략 12GB의 메모리까지 허용하겠다는 의미다.

 

 

 

 

메모리를 늘려서 앱에 5.2GB의 메모리가 할당되어 있는 모습이다.

 

추가로 인텔리제이에서는 어떻게 설정하고 간단하게 실행하는지도 알아보자

 

우측 상단 Add Configuraiton

 

+ 클릭

 

Node.js 선택

 

우측과 같이 설정을 해주자.

NODE_OPTIONS=--max-old-space-size=1000

=뒤에 오는 숫자는 할당될 메모리다. 단위는 MB

 

 

만들어진 설정을 선택하고 시작하면된다.

 

 

Nodejs 메모리 관련 글

Nodejs - 메모리 JavaScript heap out of memory

Nodejs - 메모리 할당 사이즈 변경

Nodejs - 메모리 누수 확인

반응형
반응형

1. 오류

2. 해결방법

 

 

 

1. 오류

노드를 하다 보면 아래와 같은 오류를 만날 수 있다.

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

<--- Last few GCs --->


[26828:0000020CB062E7D0]   706683 ms: Scavenge 7849.4 (8214.7) -> 7839.2 (8214.7) MB, 556.0 / 0.0 ms  (average mu = 0.131, current mu = 0.014) allocation failure 
Writing Node.js report to file: report.20200214.173258.26828.0.001.json[26828:0000020CB062E7D0]   708430 ms: Scavenge 7851.5 (8214.7) -> 7842.8 (8215.7) MB, 1247.6 / 0.0 ms  (average mu = 0.131, current mu = 0.014) allocation failure 
[26828:0000020CB062E7D0]   708535 ms: Scavenge 7855.6 (8215.7) -> 7846.4 (8218.7) MB, 32.1 / 0.0 ms  (average mu = 0.131, current mu = 0.014) allocation failure 


<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 00007FF742BAB21D]
Security context: 0x0314ac3808d1 <JSObject>
    1: /* anonymous */ [000002D987624109] [C:\Users\Desktop\project\node_modules\jsonparse\jsonparse.js:~127] [pc=000001C121A8459D](this=0x0314ac3b5539 <Parser map = 0000007B595C11A9>,0x02a34c9a8251 <Uint8Array map = 00000236682E5379>)
    2: /* anonymous */ [00000314AC3B0549] [C:\Users\Desktop\project\node_modules\JSONStream\inde...


Node.js report completed
 1: 00007FF741FAF22F napi_wrap+125087
 2: 00007FF741F4FAA6 public: bool __cdecl v8::base::CPU::has_sse(void)const __ptr64+35302
 3: 00007FF741F50776 public: bool __cdecl v8::base::CPU::has_sse(void)const __ptr64+38582
 4: 00007FF742767D4E private: void __cdecl v8::Isolate::ReportExternalAllocationLimitReached(void) __ptr64+94
 5: 00007FF742750371 public: class v8::SharedArrayBuffer::Contents __cdecl v8::SharedArrayBuffer::Externalize(void) __ptr64+833
 6: 00007FF74261BDEC public: static void __cdecl v8::internal::Heap::EphemeronKeyWriteBarrierFromCode(unsigned __int64,unsigned __int64,class v8::internal::Isolate * __ptr64)+1436
 7: 00007FF742627040 public: void __cdecl v8::internal::Heap::ProtectUnprotectedMemoryChunks(void) __ptr64+1312
 8: 00007FF742623B64 public: static bool __cdecl v8::internal::Heap::PageFlagsAreConsistent(class v8::internal::HeapObject)+3204
 9: 00007FF742619373 public: bool __cdecl v8::internal::Heap::CollectGarbage(enum v8::internal::AllocationSpace,enum v8::internal::GarbageCollectionReason,enum v8::GCCallbackFlags) __ptr64+1283
10: 00007FF7426179E4 public: void __cdecl v8::internal::Heap::AddRetainedMap(class v8::internal::Handle<class v8::internal::Map>) __ptr64+2452
11: 00007FF742638C7D public: class v8::internal::Handle<class v8::internal::HeapObject> __cdecl v8::internal::Factory::NewFillerObject(int,bool,enum v8::internal::AllocationType,enum v8::internal::AllocationOrigin) __ptr64+61
12: 00007FF74239CF71 public: class v8::internal::interpreter::JumpTableTargetOffsets::iterator & __ptr64 __cdecl v8::internal::interpreter::JumpTableTargetOffsets::iterator::operator=(class v8::internal::interpreter::JumpTableTargetOffsets::iterator && __ptr64) __ptr64+1681
13: 00007FF742BAB21D public: virtual bool __cdecl v8::internal::SetupIsolateDelegate::SetupHeap(class v8::internal::Heap * __ptr64) __ptr64+544781
14: 000001C121A8459D 

Process finished with exit code 134

궁금할 것도 없이 과도한 메모리 점유로 인하여 js 엔진이 폭발해버린 것이다.

 

 

노드를 돌리고 있는데 노드의 메모리가 엄청나게 올라간다.

아니 도대체 뭘하길래 저렇게 올라가느냐...?

내가 하고 있는 작업은 10GB 정도 되는 json 데이터를 stream pipe로 가져와서 읽는 작업을 한다.

개발 초기단계에는 몰랐지만 두곳에서 엄청난 메모리 누수가 발생하고 있어서 데이터를 읽을수록 메모리 점유가 계속해서 올라가는 문제가 있었다.

 

노드의 기본 메모리 사이즈는 512MB다.

 

 

 

2. 해결방법

근본적인 생각해볼만한 것은 두 가지가 있다.

1. 더 큰 메모리 할당

2. 앱 메모리 누수 개선

 

1번의 경우 노드 옵션 설정이기 때문에 정말 간단하다.

정말 어려운 것은 2번이다. 노드가 어디에서 메모리 누수가 일어나는지 확인하는 것이 여간 쉽지 않고 어디 나와있지도 않은 부분이다.

앱 메모리 누수를 개선하는 방법에 초점을 맞춰 글을 써 나가도록 하겠다.

 

 

 

Nodejs 메모리 관련 글

Nodejs - 메모리 JavaScript heap out of memory

Nodejs - 메모리 할당 사이즈 변경

Nodejs - 메모리 누수 확인

반응형