[Project] 2023 캡스톤 시각장애인을 위한 경로안내 앱
4학년 1학기 캡스톤디자인(2023.04~05.31) 까지의 앱 구현과 관계된 프로젝트 회고를 해보고자 한다.
What ?
캡스톤 디자인에서 궁극적으로 우리들(주니어들)이 느껴봐야 하는 주된 목적은 프로젝트의 기획부터 설계, 구현, QA까지의 개발 프로세스에 대해 전반적으로 체득하고,
나아가서 실무에서 부딪히는 ‘문제’ 에 대해서 스스로 정의하고, 이해하고, 고민하여 해결하는 과정인 즉 Trouble Shooting 을 해보는 것이라고 생각한다.
캡스톤 디자인 교과목에서 교수님께서는 정해져 있는 기업 선정 주제목록 에서 선택해서 주제를 결정하라고 하셨다.
팀원은 3명으로 고정하되, 반드시 하드웨어, 데이터베이스, 모바일 앱이 구현되어야 한다는 조건을 명시하셨고.. 캡스톤디자인 이라는 특성에 맞지 않게 강요에 의한 선택을 할 수 밖에 없었다.
그럼에도 불구하고 팀원들과 기업 선정 주제목록에서 선택하고, 상의한 끝에 시각장애인을 위한 스마트 디바이스를 만들어 보고자 결정하였다.
우리팀에서 정의한 문제는 ‘우리가 흔히 보아 왔던 점자 보도블럭이 갈수록 방치되고 유지보수 되지 않고 있으며, 시각장애인들이 사용하는 흰지팡이가 보행중에 만나는 모든 장애물을 회피할수 없다’ 로 설정하였다.
더욱이 보면서 가도 복잡한 길 속에서 앞이 보이지 않는 시각장애인이 외출을 꺼려 할것만도 같았다.
시각장애인의 안전한 보행을 도울 수 있는 스마트 장갑과 정확하고 빠른 길안내를 해줄 수 있는 앱으로 문제를 해결할 수 없을까?
How ?
먼저 구현이 필요한 기능들에 대해서 요구사항을 분석했다.
- 도보의 보행중에 네비게이션(길 안내)이 필요하다.
- 블루투스로 스마트장갑과 통신이 필요하다. ( 양방향 통신 )
- 블루투스와 스마트장갑의 자동연결이 필요하다.
- 시각장애인이 사용하는 앱인 만큼 앱 사용중에 음성인식과 음성출력이 필요하다.
하나하나 만들어온 내용을 정리해 보고자 한다.
네비게이션 기능
내가 만든 앱의 가장 핵심 Point 이자 가장 공을 많이 들였던 부분이 네비게이션 기능이다.
먼저, 네비게이션을 해주기 위해서는 최적화된 길을 안내해줄 데이터가 필요했다.
떠올랐던 api들은 네이버지도, 카카오맵, T맵 으로 3가지가 떠올랐는데 특히 TMap 의 경우 자동차 네비게이션에서 독보적인 1등이기 때문에 네비게이션에서 훨씬더 정확하고 최적화된 길안내를 해줄거라고 생각했다.
부모님이나 여러 다른 자차를 보유하신 분들은 거의다 스마트폰의 TMap으로 네비게이션을 사용하시는 모습을 봐왔었고, 그래서 가장 좋은 api를 제공해 줄거라고 생각했다.(물론 현실은 달랐다..)
경로 안내 api 중에서 보행자 경로안내 api를 제공해주고 있었고, 일일 무료 제공량도 1000건으로 프로젝트를 수행하는데 넉넉했기 때문에 이를 선택하고 구현을 시작했다.
첫번째 난관…
api docs에서 request 파라미터와 response 파라미터를 먼저 쭉 스캔했고, 그에 따른 DTO를 만들려고 했는데, response의 반환 타입이 표와 sample이 서로 달랐다.
여기서는 분명 string 타입이라고 적혀있지만, 우측 설명란에는 array인것으로 보이고 또 밑에 인덱스값과 여타 다른값들도 string 으로 되어있다.
하지만 실제로 sample code에서는 string이라고 되어있는 것들이 array이거나 Number 타입인것들이 꽤나 많았다.
그래서 무엇이 진짜인지 혼란스러웠었다.. 물론 sample code가 정답이었다.
특히 해당 coordinates 값은 type이 point일때는 특정 위치의 좌표인 (위도,경도)의 크기가1인 array 이지만,
type이 Linestring 일때는 Line 위의 Point들의 집합 형태이기 때문에 크기가 2이상인 (위도,경도) array이다. LineString의 경우 앞에 나온 Point 좌표 부터 다음에 나오는 Point 좌표 까지의 일종의 직선 보행자 도로의 특정 Point 좌표들의 묶음 이다.
일단 이러한 형태 자체가 TMap에서 정한게 아닌 GeoJson의 표준인것인데 이부분이 실제로 DTO를 구성하려면 타입이 Point 와 Linestring이 번갈아 가면서 나오기 때문에 Any타입으로 만들수 밖에 없다.
이전에 프로젝트를 진행할 때, Any 타입의 경우 실제로 무슨값이 들어올지 알 수 없기 때문에 지양해야 한다는 점을 알게된 터라.. 사실 이렇게 만드는게 맞는가? 라는 의문점이 있었다.
또한 만약 동료 개발자가 있었다면 동료와도 해당부분에 대한 설명이 추가로 필요하다는 점도 문제라고 생각한다.
어쨋든 Any타입이 아니고서야 안되기 때문에 Any타입으로 DTO를 구성하였다.
이렇게 하고 보니 Sample code 에서는 response가 Point와 LineString이 1:1로 존재하도록 되어 있어서 그냥 그런줄 알았다.. 하지만 실제로 구현하고 테스트해보니 Point와 LineString이 1:2인 경우도 존재했다.
이런부분들에 대한 docs에서의 설명이 좀 많이 부족하지 않은가 라는 생각이 들었다.
아무튼.. Data Layer에서 domain Model로 mapping을 할 때, 하나의 Point에 해당하는 LineString들과 해당 Point의 값들을 하나의 객체로 관리하고자 하였다.
Domain Model의 경우 비즈니스 룰에 집중해서 내가 만들고자 하는 비즈니스 로직에 필요한 형태로 추상화하여 모델링 하였다.
네비게이션 기능의 경우 출발지,목적지가 필요하고 그사이에 존재하는 LineString정보가 필요했고, 그외에 출발지에서의 회전정보, description(설명정보), 도로명, 등과같은 데이터들이 필요했고 그에따라 Domain model을 구성했다.
그리고, 각 response에 존재하는 파라미터들의 변수를 만들어놓고 arrayList에 add한 다음 Type이 Point가 될 때 마다 arrayList의 index값을 증가시켜 다음 그룹을 add하는 형태로 mapping 하였다.
두번째 난관…
데이터 가공이 끝났고, 이제 Tmap 에서 제공해주는 최적화된 경로에 맞게 현위치에 따라 네비게이션을 만들어 주면 됫었다.
어떻게 해줄까 고민한 끝에 Gps 좌표인 위치값을 비교해서 길안내를 해주면 되지 않을까? 라고 생각했다.
그럼 결국 핵심은 현재 가고 있는 길이 정상경로인가 그렇지 않은가 였다.
정상경로 라는 것은 최적화된 경로로 제공되는 Line 배열에 있는 gps좌표값들을 거쳐가는 것이기 때문에, 해당 Line의 특정 index의 좌표와 내 위치가 점점 가까워 지고 있다면? 정상경로 라고 생각했다.
그래서 현재 나의 위치에서 다음 목적지 까지 남은 거리를 recentDistance 라고 정의하고, gps 위치값이 바뀔 때 마다 recentDistance와 현재 내위치에서 다음 목적지 까지 남은 거리를 비교해서 recentDistance가 더 크면? 정상경로 로 구현하였다.
그리고 recentDistance 값에 따라 처음 이동한 거리가 5~15미터인경우 Tmap에서 받은 데이터의 현재 목적지 까지 경로에 대한 회전정보 와 남은거리를 음성출력해주고, 절반지점에서는 남은거리를 안내하고, 마지막에는 다음 Point지점에서의 회전정보와 남은거리를 안내하도록 하였다.
LineString의 경우 데이터를 직접 로그를 찍어서 구글지도에 gps를 적어보니 보통 직선 보행 경로였다. 그래서 중간에는 남은거리와 함께 직진 안내만 해주면 됬었다.
이렇게 잘 만들어진 네비게이션으로 행복하게 마무리를 하면 좋았겠지만.. 문제가 생겼다.
직선주행이 아니라 사선으로 가는 경우를 정상경로로 인식해 버린다.
이렇게만 말하면 이해가 어려우니..
다음 사진은 학교에서 집으로 가는 길인데.. 네비게이션에서 2번경로로 안내중이라고 생각해 보자.
2번 경로의 다음목적지 D까지의 거리는 1번경로를 향해도 줄어드는 형태이다. 왜냐하면 1번경로를 자세히보면 살짝 위로 가고 있기 때문에 실제로 거리는 아주조금 줄어든다.
이러한 부분이 여러군데에서 발생했고, 정상적인 네비게이션 이라면 정상경로가 아님을 사용자에게 알리고 현재 가고있는 방향에서 새로운 경로를 요청해서 안내해주어야 한다.
그래서 이문제를 해결하기 위해 방위정보를 생각했다.
현재 나의 위치가 바뀔때 마다 방위정보를 비교해서 경로에서 요구하는 방위와 다르면? 정상경로가 아니니 새로운 경로를 요청하는 방법으로 말이다.
이렇게 만들기 위해서는 출발지와 목적지의 gps 좌표값으로 방위정보를 알수 있어야 하는데 다행이 똑똑하신 과학자분들이 이미 수식으로 증명해두셔서 갖다쓰기로 했다..(감사합니다.)
그래서 LineString에서 제공하는 좌표들을 가지고 방위정보를 계산하고 스마트폰 내장 센서중에 자이드로센서와 가속도계센서로 방위각을 계산해서 비교하는 형태로 구현했다.
이렇게하면 또다른 문제가 생긴다.
위치정보가 바뀔때 마다 방위각을 계산해서 비교하는데, 스마트폰을 항상 곧게 들고 가는게 아니라 주머니에 넣든 손에 들고가든 하게 되면 방위각이 당연히 달라진다…
위에서 발생한 문제의 원인은 목적지까지의 거리가 줄어드는 양상은 맞지만 이동한 만큼 줄어드는게 아니라는 것이다.
즉, 10미터를 이동한다면 목적지 까지의 거리가 10미터가 줄어야 하지만, 1~2미터가 줄어들었다는 것이다.
따라서 해결할수 있는 방법은 현재위치를 가져오는 callback의 경우 10미터만큼 이동하면 다시 현위치를 가져오고 있기 때문에 목적지 까지의 거리가 10미터 만큼 줄어들지 않았다면? 방위각을 계산하고, 방위각이 달랐다면? 경로를 재요청하는 알고리즘을 만들수 있게 된다.
결국 최종적인 알고리즘을 정리해보자면 이렇다.
세번째 난관
이렇게 행복하게 네비게이션 앱을 완성하면 좋았겠지만.. 다른문제도 있었다.
gps위치값은 LocationManager 라는 클래스로 스마트폰 내장 gps 센서값으로 받아오고 있었는데 받아올수 있는 방법은 위성통신과 네트워크위치로 가져올수 있다.
위성통신의 경우 고주파이기 때문에 파장이 직진성을 띄고, 따라서 건물밖에서 사용이 가능하다. 또한 위성에서 바로 현위치를 보다 정확한 위치값으로 조회할수 있다.
반면에 네트워크로 가져오는 경우는 이전에 네트워크가 연결되었던 위치를 대략적 으로 가져오기 때문에 위성통신이 불가능한 건물안에서 효과적으로 사용할수 있다. 물론 오차가 심하다.
그래서 나는 보행중에 사용할 것이기 때문에 당연히 정확한 위치가 필요하기도 하여 위성통신 모드로 설정해 두었는데 테스트를 할때 이상하게 자꾸 gps위치가 통통 튀는 현상이 있었다.
심하게는 건너편 보행자도로에 내위치가 뜨기도하고, 평균적으로 옆의 자동차 도로에 자꾸 내위치가 떳다.
위 사진에서 빨간점이 내 현재위치라면, 파란점의 위치들로 자꾸 내위치가 조회됬다.
검색해본결과, gps로 위성통신을 할 경우 전리층, 대기권, 수치, 위성시계, 전파경로 등등에 대한 여러 이유로 오차가 발생한다고 한다.
이를 위한 해결법으로는 위성자체를 개선을 하거나 수신측에서 보정을 하는 방법이 있는데, 위성자체는 돈이 많이들기 때문에 보통 수신측의 보정으로 해결한다고 한다.
다행이도 구글에서는 fusedLocationProviderClient 라는 api로 보정된 위치를 제공해주고 있다.
추가적인 장점으로는 이 api는 위성통신이 불가능한 건물안과 같은 장소에서는 네트워크로 위치를 가져오고, 밖에서는 위성통신으로 위치를 자동으로 가져와주어서 보다 실용적으로 사용이 가능하다.
따라서 신나는 마음에 위로 구현해서 측정해보니 마찬가지로 오차가 있다..
물론 앞서 LocationManager 보다는 낫지만 역시 오차가 있고, 또한 신기하게도 아파트 주변에 갔을때 내위치가 순간적으로 아파트에 있는거로 뜨기도 햇었다.
또한.. 비오는날에는 오차가 훨씬더 심했다.. 대기권의 영향일것이다.
결국 해결하지는 못했지만 아마 다른 지도앱들에서는 도보의 좌표값들을 알고있으니 현위치와 가장가까운 도보의 좌표로 보정시키지 않을까 라는 생각은 든다.
물론, 그 데이터를 open 해주지 않았기 때문에 나는 따로 구현할 방법이 없다.
추후 다른방법이 있는지 찾아봐야 할것같다.
이렇게 네비게이션을 구현해 보았다. 물론 아주 nice하지는 않다..
위에서 서술한것 뿐만아니라 Tmap 데이터가 부정확해서 (지방이라 그런가…) 횡단보도가 있는데 표기가 안되있다던지, 정확히 그 위치로 가지 않아도되는데 가도록 데이터가 들어온다던지, 직선도로가 아닌 Linestring 이라던지.. 여러가지로 부정확한 데이터 들이 많았다. (네이버지도에는 그런것들이 잘 표기되어있었지만…)
블루투스와 스마트장갑과의 통신 과 자동연결
이부분은 같은 팀원 파트였는데 블루투스와 연결하는 부분은 내가 코드를 구성했었다.
블루투스와 연결할때는 방법이 두가지가 있는데, 양방향 통신이 목적이라면 Bluetooth Classic방법을 사용하고 단방향 통신이 목적이라면 BLE로 보다 더개선된 방법을 사용할 수 있다.
물론, 나는 양방향으로 통신해야 하기 때문에( 하드웨어 센서를 조작해야하고, 카메라로부터 사진을 받아와야함 ) bluetooth classic 방법으로 연결했다.
방법은 간단하다. 블루투스를 활성화시키고, 디바이스 이름과 같은 녀석과 연결이 될때 까지 연결을 시도한다.(자동연결 때문)
연결이 됬다면 연결시도를 중단하고, 연결이 해제되면 다시 연결을 시도한다. 이또한 설정화면을 만들어서 자동연결을 ON/OFF 가능하게 만들었다.
또한 자동연결을 위해서 broadcastReceiver를 activity 수명주기에 맞추어 register/unregister 하고, 내부에서 연결상태에 따라 처리하였다.
음성인식과 음성출력
음성인식의 경우 SpeechRecognizer 클래스를 이용했는데, 내앱은 하이빅스비, 헤이구글 처럼 상시 음성인식을 핫워드로 감지를 위해 대기하여야 하기 때문에 Service에 구현해서 백그라운드에서 계속 대기하도록 구현했다.
그리고 핫워드(비서) 가 감지되면 지정된 명령어에 따라 경로안내를 설정하게 된다.
문제점
이부분의 문제점은 AudioFocusing이다. 만약 앱의 이용자가 음악을 들으면서 가고 있다면, 음성인식이 무한루프로 돌면서 음악이 pause되게 된다.
해당부분의 해결방법을 찾아보았지만 해결이 불가능하다고 한다. 즉, 필요할때 마다 user의 action에 의해서 음성인식이 시작되었다가 종료되고 나서 음악이 수행되게 만들어야 한다는 것이다.
근데 이상하게도 speechRecognizer가 수행중인데도 TextToSpeech는 pause되지 않고 잘된다.. 왜이런지 알수가 없다…
아무튼 이부분 역시 미해결 되었다..
음성출력의 경우에는 TextToSpeech 클래스를 이용해서 음성을 출력할 수 있다. 이때도 마찬가지로 AudioFocusing 처리를 해주어야 하는데 여기에는 해결이 가능했다.
음악을 듣고 있는 상태에서 TextToSpeech가 발생하면 audio를 ducking 시켜서 음악이 pause되지 않고 소리가 작아졌다가 TextToSpeech 출력이 끝나면 원상복귀 시키도록 해결했다.
Archiecture
아키텍쳐의 경우 Clean + MVVM 구조로 구현했다. 이전까지의 프로젝트에서는 구글에서 제시하는 권장 아키텍쳐 패턴으로 구현했었는데 (사실 헤이딜러 기술블로그 를 보기 전까지는 구글권장 아키텍쳐가 클린아키텍쳐 인줄 알았다… 심지어 글쓴이님께서 그부분을 글에 적어두기도 했다.!!)
클린아키텍쳐는 레이어드 아키텍쳐이면서 구글권장 아키텍쳐와는 달리 도메인은 누구도 모르며, 비즈니스 룰이 있는곳으로 Presenter Layer에 그어떤녀석( IOS, AOS, Web)이 와서 연결해도 문제가 없어야하고,
Data layer에 그 어떤녀석(Database, cloud, server)으로 부터 데이터를 저장하든, 읽든 문제가 없어야 한다.
그래서 Presenter layer와 data layer가 domain layer를 알고 그에따라 Model을 mapping 시켜서 독립적으로 구성되어야 한다.
따라서, UI의 변경이 비즈니스 로직과 데이터로직에 영향을 주지않고 마찬가지로 데이터로직의 변경이 비즈니스로직과 UI에 영향을 주지도 않게 되어 유지보수, 확장성, 이식성이 높아진다.
또한 각 계층에서는 항상 다른계층의 추상화된 Interface와 통신하여 각 구현체를 몰라도되게 해야 한다.
이는 DIP(Dependency Inversion Principle)로 SOLID 5원칙중 하나로써, 저수준 모듈이 고수준 모듈에 의존하도록 만드는 것이다.
이 저수준이라는 것은 구현체로써 어떤 구체적인 것들을 말하고, 고수준 이라는 것은 추상화된 것을 말한다.
따라서 구체적인 것들에 의존하는게 아니라 추상화된 것들에 의존시켜서 구현체가 뭔지 모르게 하는게 핵심인 것이다.
구현체를 모른다는 의미는 누가와도 상관 없다는 것이고 즉 위에서 설명한대로 그 어떤녀석이 와도 연결되고, 수행되어 문제가 발생하지 않게 한다는 의미이다.
이를 모듈화라고 말할 수 있다.
내가 설계한 프로젝트의 구성도 이다.
각 연결된 화살표는 의존도의 방향이다.
Presenter layer에는 View ( Activity와 composable)와 ViewModel, State가 있다. 그리고 Presenter layer의 State를 Domain layer의 model로 또는 그반대로 mapping 해주는 mapper가 있다.
각 View에는 화면에 걸맞는 UI 로직이 존재하고, ViewModel에는 UI의 상태와 그와 관련된 로직들이 존재하며, Usecase의 interface를 가지게 되어 Domain layer와 통신하게 된다.
domain layer는 그 누구도 알지못하며 Usecase에 비즈니스 룰이 존재하게 된다. 그리고 repository Interface로 data layer와 통신하게 된다.
data layer에서는 repository 구현체가 각 Server 혹은 Local에서 원하는 데이터를 datasource 계층으로 부터 가져와서 Mapper에 정의된 형태로 Domain Model로 Mapping 하게 된다. 즉 data로직이 여기에 존재하게 된다.
DataSource 계층에서는 Local과 Server 그 어떤녀석으로 부터 데이터를 가져올지 모르게 하여 ( 어디로부터 데이터를 가져와도 상관없음 ) 확장성을 높인다.
댓글남기기