오르카의 아틀리에

얼마 전 건강검진에서 간 수치가 높게 나오고 백내장이 의심된다고 했다. 한동안 개인 공부 겸으로 진행했던 DIY VR Controller를 진행하지 못하고 있다가 재검사 후 정상 판정이 나와서 마음 놓고 다시 진행해 보았다. 진행하면서 가장 짜증 났던 부분이 몇 개가 있었는데 그중 가장 유용할 것 같은 문제와 문제 해결법을 공유해보고자 한다.


블루투스의 데이터가 손실된다고?

DIY VR Controller는 <센서-아두이노-블루투스-안드로이드-유니티>가 서로 연결되어 작동한다. 센서와 아두이노에서는 MPU-9250 DMP 라이브러리를 통해 해결하였고, 안드로이드와 유니티는 유니티에서 안드로이드용 플러그인 제작방법을 익히면서 해결하였다. 문제는 블루투스와 안드로이드에 있었다. 블루투스에서 전달받은 데이터가 유니티 위에서 손실된 값으로 보여진 것이다. 아니 병렬 통신도 아니고 직렬 통신인 블루투스에서 데이터 손실이? 의아해했지만 눈에 보이는 값을 보면 손실이 일어나는 것이 분명했다. 



처음에는 Baud Rate의 문제일까 싶어 Static한 문자열을 넘겨받는 실험을 해보았고, 결과는 위 그래프와 같다. 각 Baud Rate를 바꾸어가며 일정량 이상의 패킷을 전송했고, 그중 손상된 패킷과 그렇지 않은 패킷을 카운팅하여 비율로 나타내었다.


19200 rate에서 유독 많은 손실률을 보인 것만 뺀다면 대략적으로 10% 내외의 손실률을 보유하고 있었다. 일단, Baud Rate는 문제가 아니었다. 하지만 10%라는 손실률은 유저를 거슬리게 하는데 충분하다. VR은 대략 90fps를 유지하는 것이 권장되고 있고 측정 결과 DMP 라이브러리를 사용하면 약 0.02초에 한번씩 패킷을 송출하게 된다. VR이 대략 90fps로 렌더링 된다고 가정했을 때 10%인 9프레임 내외가 이상한 값을 렌더링하게 된다면 충분히 눈에 거슬릴 수 있는 수준이었다.


문제는 유니티?!

아두이노에서 송출되는 데이터는 문제가 될 여지가 없어 보인다. 그렇다면 블루투스-안드로이드 영역이나 안드로이드-유니티 영역에서 문제가 발생하는 것일 텐데 처음에는 수정과 확인이 편리한 유니티 영역부터 다시 검토해보았다. 하지만 유니티 역시 문자열로 받은 데이터를 바이트로 변환하고 새로 큐에 저장하여 사용하였기 때문에 별로 문제가 되는 부분을 찾기 힘들었다. 결국, 알아보기 편하게 데이터 해석 로직을 안드로이드 코드로 전부 이전하고 다시 한 번 천천히 코드의 흐름을 읽으면서 파악해 나가기로 했다.


공식... 예제님?!

역시 천천히 분석해보면 답은 나오는 법! 그간 플러그인은 'BluetoothChat'이라는 안드로이드 예제를 참고하여 만들었는데 이 예제에 문제점이 있었다. 해당 예제는 사람이 블루투스를 이용하여 짧은 문자열을 주고받기 위한 예제였기 때문에 데이터를 연속적으로 빠르게 주고 받는 경우에 대한 대비가 되어있지 않았다. (예제는 최대한 간결한게 좋으니 이해는 하지만.... 이건 또 이거대로 빡치네)


예제를 살펴보면 Main Thread가 가지고 있는 Sub Thread에서 블루투스와의 커넥션을 유지하고 블루투스의 메세지를 읽어 Main Thread에 보내 처리할 수 있도록 하고 있었다. 문제는 여기서 발생하는데 버퍼 데이터를 그냥 참조해서 넘기고 있었다는 것... 그렇다, 서로 다른 두 개의 Thread에서 데이터를 참조 형식으로 공유하고 있어 Main Thread가 버퍼를 해석하고 유니티로 전송하는 도중에 Sub Thread가 다시 버퍼를 초기화하고 메세지를 읽어올 수 있었다. (OS 시간에 배웠던 대참사 캐이스가 눈앞에서 벌어지는 상황이었다.)


해결법

해결법은 원론적으로 보면 'Mutex'같은 것을 사용하는 방법이 있겠지만 조금 다른 트릭을 사용했다. 바로 큐와 Arraycopy를 이용하는 방법이다. 버퍼를 읽어드린 뒤 바로 버퍼와 같은 길이의 배열을 동적 할당하고 큐에 저장한다. 그 뒤에 Arraycopy를 이용하여 버퍼에 있는 내용을 복사하면 끝! 큐에 들어간 이 친구를 꺼내주면서 Main Thread에 넘기고 처리하면 Sub와 Main이 같은 값을 이용하여 작업하지만 메모리 영역은 겹치지 않아 안전하게 작업을 처리할 수 있게 된다. 코드로 보면


사실 큐를 사용하는 것은 별 의미 없는 부분일 수도 있지만 순수하게 new로 할당한 위치만을 저장했다가 핸들러로 넘기고 싶어서 사용했다. 이런 방법 이외에도 애초에 데이터를 읽어서 저장하는 버퍼를 원형 큐로 제작하여 만들 수도 있을 것 같다.