Solution Challenge 2024 참여 후기

Solution Challenge 2024

Solution Challenge 2024
Solution Challenge 2024

GDSC Solution Challenge 2024에 참여하게 되었다.

 

사실 작년 2023년에도 참여했었는데, 이때는 나를 포함한 팀원 모두가 첫 프로젝트라 많은 시행착오가 있었다. 하필 HIBL의 프로젝트 시기와도 동일하게 겹치는 시기라, 어려운 프로젝트 두 개를 동시에 진행하는 시기였다. 그래서 갈등도 있었고, 고생도 많았다.

 

그래서 올해 참여할 지 말지를 많이 고민했다. 원래는 고민 끝에 올해는 스킵하기로 마음먹었다.

 

사실 스킵하기로 하고서 여러 모로 아쉬움이 많이 남기는 했었다. 작년에 너무 고생했던 기억이 있어서, 도전할 용기는 많이 꺾였지만, 누군가 밀어주었으면 하는 바램이 있었다.

 

그러다가 이전에 데브옵스 스터디를 하면서 알게 된 분에게 권유를 받게 되었다. 팀원이 한 명 부족한데, 모바일 개발 인력으로 참여를 권유받았다.

 

솔… 직히 이런 권유를 받았으니 참여를 안 할 수가 없다. 원래는 거절하려고 했으나, 솔챌이 나를 부르는 소리가 마음 속 깊이 들려오는 데… 이걸 참아? 이틀 뒤에 재권유가 왔을 때 참여했다.

 

팀원 자체는 백엔드 2명, AI 한 명, 모바일 1명으로 안정적인 구성이었다. 한 가지 걱정이 되는 점이라면 디자이너의 부재인데, 솔루션 챌린지 자체가 기획과 디자인의 영역이 매우 큰 만큼 이 부분은 약점으로 느껴졌다. 하지만 다행히 팀에 디자인 능력자 분들이 두 분이나 계셔서, 약점인 줄 알았던 디자인 영역은 오히려 우리 팀의 강점으로 작용했다.

 

AI를 맡으신 분이 경험이나 실력이 많으셔서 만능이셨는데, 덕분에 AI 쪽 모델 개발이 매우 수월하게 진행됐다. 이쪽이 쉽게 해결이 되니깐 기획 과정에서 많은 것들을 시도할 수 있었고, 개발이 매우 편했던 것 같다.

기획

솔루션 챌린지는 기획이 제일 중요하다. 기획부터 시작하여 기획으로 끝난다. 구현은 사실 부차적인 문제이다.

 

심지어 작년 2023년, TOP 100위에 들었던 우리 학교의 한 팀은 구현을 끝마치지도 못한 채로 제출하였다. 그 팀은 아이디어가 매우 좋았는데(음식과 기부를 엮은 기획이었다), 구현을 끝내지 못한 게 아쉽다고 생각했었다. 구현을 못 하면 우승이 어려울 것이라 생각했다. 그런데 발표날 보니깐 구현이 안 되었어도, TOP 100에 들 수 있더라. 구현 여부와 관계 없이 아이디어로 승부보는 게 솔루션 챌린지이다.

 

그렇기 때문에 이번에는 개발을 급하게 시작하는 데 치중하지 않고, 기획에 많은 공을 들였다. 선술한 예의 팀은 심지어 중간 발표 이후 2월 달에 급하게 기획의 대전환을 거친 뒤에 매우 짧은 기간 동안 구현한 뒤 우승하였다. 앞에 사례에서 기획에 많은 시간을 할애해야 한다는 교훈을 얻었다.

헬스케어

기획 단계에서 다양한 아이디어들이 논의되었다. 가장 먼저 제시된 것은 의료 케어 서비스였다. 개발도상국에서 의료 서비스를 받지 못하는 사람들을 대상으로 모바일로 간편하게 이용할 수 있는 의료 케어 서비스 아이디어가 제시되었다. 아이디어를 제시하신 AI를 맡으신 분이 HCI 분야, 그리고 모바일 기기로 심장 펄스를 측정하는 기술 등을 연구하셨다고 했다. 이 연구 결과가 아이디어를 선정하는 데 영향을 주지 않았나 싶다.

초기에 시도된 헬스케어 앱의 피그마 디자인 일부
초기에 시도된 헬스케어 앱의 피그마 디자인 일부

 

매우 좋은 아이디어였는데, 치명적인 단점이라면 비슷한 아이디어가 이미 솔루션 챌린지 지난 회차에서 많이 제출되기는 했었다… 헬스 케어 이쪽은 워낙에 관심도가 높은 분야이다 보니깐 아이디어 자체도 많이 시도되었고, 이중 지난 솔루션 챌린지 TOP 100에 선정된 제출물들도 꽤 되었다.

작년에 제출된 솔루션이랑 아이디어가 겹치면 새롭게 상을 받기에는 한계가 있다. 게다가 여러 기술적인 한계(모바일 기기로 생체 데이터를 측정할 시 정확도가 아직은 높지 않다는 문제 등)가 있어, 이 아이디어를 보완할 새로운 시도가 필요했다.

알레르기

누가 말을 했는지는 기억이 안 나는데, 아마 백엔드를 맡으신 분으로 기억한다. 알레르기에 대한 아이디어를 제시했다. 정말 괜찮은 아이디어였는데, 바로 음식에 들어있는 알레르기를 쉽게 파악할 수 있도록 정보를 제공하는 아이디어였다. 알레르기 자체가 꽤 중요한 문제인만큼, 솔루션 챌린지에서 이목을 끌기에 괜찮은 주제같았다. 하지만 이 역시 한계가 두 가지 있었는데

  1. 알레르기를 유발할 가능성이 있는 식품의 첨가 여부를 어떻게 판단할 것인가? 기술적 한계가 있었다.
  2. 제일 중요하게 알레르기에 관련된 서비스가 이미 출시된 적이 있었다.

좋은 아이디어였으나 이마저 파기되었다.

안전 교육 앱

알레르기도 그렇고, 헬스케어도 그렇고 공통적으로 사용자의 건강 정보와 연관되어 있고, 사용자로 하여금 이 정보를 보다 편리하게 접근할 수 있도록 도와주는 데 초점을 맞추고 있다.

 

그래서 내 머릿속에 교육 앱에 대한 아이디어가 떠올랐고, 그 중에서도 안전 교육 앱에 대한 아이디어를 제시했다.

 

언어 학습 앱 Duolingo
언어 학습 앱 Duolingo

안전 교육을 쉽게 전달하는 앱이다. 베이스 아이디어로 듀오링고가 떠올랐다.

 

듀오링고는 언어 교육 앱인데, 귀여운 자체 캐릭터(부엉이가 나온다)가 나와서 굉장히 친숙하게 언어 학습을 도와준다. 퀴즈 형식인데, 예전에 일본어를 배울 때 재미있게 사용한 기억이 난다(재미있게 사용만 했지 일본어를 배우지는 못 했다…).

 

여기서 아이디어를 차용해서, 귀여운 동물 캐릭터와 함께 안전을 쉽게 알려주는 퀴즈 앱을 제시했다. 예를 들어서 자체적인 캐릭터(사자나 호랑이같은 동물 캐릭터)를 만들고, 그 캐릭터가 CPR을 하는 모습을 보여준다든지… 그런 아이디어였다.

 

안전 교육에 대한 설문 조사
안전 교육에 대한 설문 조사

안전 교육은 정확한 정보를 전달하는 것도 중요하지만, 사용자에게 정보가 전달이 되는 것 자체도 중요하다. 대부분의 안전 교육 현장에서 내용에만 초점을 맞추고 있지, 이를 ‘어떻게’ 전달할 지에 대해서는 그다지 관심을 갖지 않는다.

 

생각해보라. 정작 위급한 상황일 때 필요한 지식이 없다면 무슨 소용이 있겠는가. 어렴풋하게라도 필요한 지식이 각인되는 게 중요하다.

 

지난 2023년 10월, 한국은 서울 이태원에서 150명이 사망하는 비극적인 사건을 겪었다. 전 국민이 놀라움과 충격을 공유했는데, 그중에는 ‘저게 어떻게 저렇게 많은 사람들이 사망하는 참사로 번질 수가 있느냐’는 놀라움이었다. 그 중에서도 기억에 남는 한 가지 글이 있다. 과거에 위기탈출 넘버원이라는 프로그램을 재조명하는 글이었다. 위기탈출 넘버원에서 사람들이 많을 때, 가슴 앞으로 팔을 X자를 그리면, 흉부의 공간을 확보하면서 압박을 최소화할 수 있음을 소개한 적이 있다. 만약에 사람들이 이 대처를 알았더라면, 발생한 피해를 조금이라도 더 최소화시킬 수 있었을 지도 모른다.

 

나 역시 이때 CPR을 제대로 배워봐야겠다고 생각하기도 했었고, 또 안전 교육이 중요하구나(위기탈출 넘버원이 오버스러운 게 많아서 그렇지 은근 유용했구나)라는 생각을 갖게 되었다.

 

사실 대부분의 사고가 그렇다. 적지 않은 사고들은 예방 가능했고, 이미 발생한 사고에 대해서는 피해를 최소화할 수 있었다.

 

피해의 최소화가 매우 중요하다. 후사고 분석을 통해 원인을 파악하고 예방 가능했다는 결론이 나더라도, 이는 사고 발생 이후의 분석일 뿐 그 당시의 현장에서는 아무 쓸모가 없다. 그 이후에 같은 사고가 발생하지 않도록 예방하는 데 초점을 맞추고 있지, 이미 일어난 사고를 되돌릴 수는 없기 때문이다. 물론 같은 사고가 다시 발생하지 않도록, 철저한 분석을 통해 원인을 파악하고 예방하는 것이 중요하다. 그러나 동시에 중요한 것은, 우리가 지금 파악하지 못 하는 원인으로 인해 피해가 발생하였을 때, 이를 어떻게 최소화할 것인지이다.

 

당연하지만 피해가 발생하고 이를 찾고 있으면 늦는다! 미리 알고 있어야 한다! 그렇기에 평소에 기본적인 안전 교육이 중요한 것이고, CPR 정도는 배워둬야 하는 이유이다.

 

안타깝게도 학교에서의 일을 생각해보면, 대부분의 안전 교육에서 학생들은 잠을 잔다. 혹은 집중력을 길게 이어가지 못하는 것이 현실이다. 최근의 교육은 사실 꽤 많이 개선되기는 했지만, 여전히 오래된 학교나 교육 현장에서는 지루한 안전 교육으로 인해 학생들이 쉽게 집중력을 잃는 문제점을 갖고 있다.

 

퀴즈 앱은 학생들에게 동적인 피드백을 즉각적으로 요구함으로써 집중력을 유지시킨다. 랭킹 시스템을 도입하거나, 혹은 반에서 사용 가능하도록, 선생님용 관리 페이지를 제작하는 등의 시도를 할 수 있겠다.

 

아이디어는 매우 호평을 받았고, 결론적으로 이 아이디어를 디벨롭하기로 결정했다. 이때가 1월 중반이었다.

 

CPR Pose Estimation
CPR Pose Estimation, 출처: https://www.mdpi.com/2076-3417/12/10/4847

또한 회의 과정에서 아이디어는 매우 발전되었다. 그냥 한 번 툭 던져본 건데, CPR을 동영상으로 찍으면 이를 피드백할 수 있는 시스템에 관한 아이디어였다. 원래 이 아이디어는 비현실적이라 생각했다. 왜냐면 이를 인공지능으로 자동화하기에는 어려울 것이라 생각했고, 사람이 수동으로 피드백할 경우 피드백을 제공할 동기가 필요했고, 또한 그 피드백을 제공하는 사람들 또한 전문가의 영역이여야 했다. 이는 단순히 헬스 커뮤니티에서 스쿼트 자세를 평가해달라는 것과 차원이 다른 문제다. CPR과 같은 의료 행위의 포즈 평가는 전문 의료인의 평가가 필요했고, 전문 의료인이 앱에서 다른 사람들의 자세를 피드백할 동기를 디자인하는 건 사실상 불가능했다.

 

그래서 실현 불가능할 줄 알았는데, 인공지능을 맡으신 분이 이게 가능할 지도 모른다고 말을 했다. Pose estimation에 관한 연구인데, 여러 pose estimation 모델을 통해 학습된 정답 CPR Pose와 사용자가 영상으로 남긴 Pose를 비교하여, 유사도를 검사한다면, 이 기능이 가능할 지도 모른다고 가능성을 열어주셨다.

 

영상을 평가하는 것은 단순히 이미지의 유사도를 검사하는 작업과 차원이 다르다고 한다. 일단 타임 스케일 자체가 달라서, 예를 들어서 원본 영상은 10초, 유저가 제출한 영상이 20초라면 어떻게 할 것인가? 그리고 포즈의 속도가 다르다든지 하는 문제도 있다. 그래서 어려울 줄 알았는데 인공지능을 맡으신 분께서 이러한 타임 스케일 역시 보정해줄 수 있는 평가 모델을 또 찾아오셨더라.

 

그래서 이 기능 역시 구현 목표에 넣기로 결정되었다.

앱 디자인

앱 디자인
Safy 앱 디자인 피그마

나는 디자인은 1도 모르기에 앱 디자인은 팀의 다른 두 분이 맡아주셨다. 두 분이 말아주신 디자인은 아주 훌륭했다.

 

사실 Flutter를 사용한다면 Material Design 3에 기댈 수도 있긴 하다. 하지만 Material 3는 결과가 나쁘지는 않게 나오겠지만 우리가 원하는 결과의 디자인이 나올 보장은 없다. 나도 실제로는 써본 적이 없기도 하고, 이걸 실제로 쓰는 프로젝트가 많을 지는 모르겠다. 그래서 그냥 두 분의 피그마를 기대하기로 했다. 결과는 대성공이다.

 

앱의 이름은 결론적으로 마감 직전에서야 정식으로 결정되었다. 이전에는 Little Ways to Live, SafetyEdu 등 중구난방으로 불렸는데, 마감 며칠 전에 Safety Awareness For Youth, 줄여서 SAFY로 결정되었다(싸피가 아닌 세이피로 읽는다).

 

한편 디자인 과정에서 우리에게 정식 디자이너가 없다는 한계는 들어났는데, 예를 들어 듀오링고처럼 캐릭터를 디자인에 활용하기 어렵다든지(그게 사실 어쩌면 핵심이었는데 ㅠㅠ), 혹은 앱 플로우가 꽤 많은 시간 동안 확정이 되지 않아 몇 번의 수정을 거쳤다든지 하는 문제들이 있었다.

 

예를 들어 기존에는 퀴즈를 5개 정도로 묶어 스테이지를 나누기로 했었는데, 여러 논의 사항을 거쳐 스테이지를 나누는 것도 폐기한다든지 등 변경 사항이 많았다.

 

그리고 실제 개발 과정에서, 기획 때 상의한 기능들이 구현되지 않은 경우도 있었다. 예를 들어 퀴즈의 난이도는 어느순간 개발 과정에서 제외되었고, 랭킹 시스템 역시 사라졌다. 이는 개발 기간이 너무 짧아서 생긴 문제이기는 하다. 너무 느긋하게 있었다…

중간 평가

GDSC Hongik에서 자체적으로 진행한 중간 평가가 있다. 이때 발표 내용을 토대로 멘토의 피드백을 받을 수 있다(라떼는 이런 거 없었는데 올해 GDSC는 정말 많이 발전했다).

 

나는 이 날 하필 공군을 위한 정보처리기능사 시험이 있어서 중간 평가에 같이 참여하지는 못 했다. 정보처리기능사 필기야 컴공 학생 입장에서는 아가 다루는 수준이므로, 다행히 5분만에 끝내고 우리 팀 발표를 지켜볼 수 있었다.

 

https://joonlee.notion.site/Design-Sprint-36ba9eac1cdb4b5897ee3a5fd24f84c9?pvs=4

 

Design Sprint 기획안 최종 | Notion

개요

joonlee.notion.site

작년과 마찬가지로 디자인 스프린트 기간 동안의 결과는 기획안으로 작성되어 제출되었다.

 

그리고 시간이 지나고, 설날이 끝난 후 멘토의 피드백이 등장하였다.

 

https://childlike-kumquat-540.notion.site/6-0c001ed4854a48d39ecd426e5f179ab4

 

솔루션 챌린지 6팀 피드백 | Notion

💻 프로젝트 컨셉 및 기획 의도

childlike-kumquat-540.notion.site

아니 근데 멘토 피드백 받기 위해서는 레포지토리를 public으로 바꿔놓아야 하는데, 내 레포를 그냥 private로 해놓고 있었다… 생각해보면 당연한 건데 신경을 못 쓰고 있었다. 나도 피드백 해줘요…

개발 과정

몇 가지 정리하고픈 개발 과정이 있다. 나중에 개별 글로 정리할 예정이다.

사실 모바일 쪽의 개발 자체는 순조롭게 진행되었다. 나도 이제 어느덧 복잡한 애니메이션이 있지 않은 한, 평면적인 앱은 수월하게 개발할 수 있는 레벨이 되었다.

아키텍처

아키텍처는 MVVM 구조를 채용했다. 정확히 말하면 MVVM을 노리고 만든 건 아닌데, 만들다 보니깐 MVVM에 가까워졌다.

 

정확히는 여기서 ViewModel은 클린 아키텍처의 Service Layer에 가까운 역할이다.

각 레이어는 다음과 같다.

  • Domain Layer: 도메인 모델과 백엔드와의 통신, 그리고 앱 데이터를 관리한다
    • model: 도메인 모델로, 앱 내에서 사용하는 모델과, 백엔드에서 오는 데이터 모델을 포함한다
      • 참고로 DTO와 앱에서 사용할 모델을 나눌 수도 있다. 그러나 나는 굳이 프론트엔드에서 DTO를 따로 가져가는 건 투머치라고 생각했는데, 중간에 API 명세가 한 번 바뀔 뻔 하면서 DTO를 분리할 걸 후회하기도 했다.
    • Repository: 백엔드 서버와 통신하는 레포지토리는 전부 Retrofit을 이용했고, Firebase와 통신하는 부분은 firebase instance를 주입받아서 구현하였다.
      • Firebase와 통신하는 부분의 경우, Firebase instance를 상위 서비스 레이어에서 직접 사용할 수도 있는데, 그냥 일관성을 위해 Firebase와 연결되는 통신 역시 Repository를 따로 만들었다. 이럴 경우 지금은 필요 없지만, Service Layer에서 Firebase와의 의존을 줄일 수 있다.
  • Service Layer: View Layer와 Domain Layer를 중계한다. 다만 여기서는 Service라는 이름을 직접적으로 사용하지는 않았다.
    • Provider: Repository를 주입받아서, 데이터의 상태를 관리한다. Riverpod을 통해 캐싱 및 여러 최적화를 구현했다.
      • 한편 Service라는 이름을 사용하지 않은 이유는 명확하다. 이 프로젝트에서는 Riverpod State Management를 적극적으로 채용했다. Riverpod에는 Provider라는 직관적인 이름으로 StateNotifier 클래스 등을 제공한다. 그래서 Service라는 이름 대신 Provider라는 이름을 채용했다.
  • View Layer: 말 그대로 유저에게 보여지는 View Layer이다.
    • Component: 자주 사용되는 위젯들을 이 폴더 안에 모아두었다.
    • View: Screen, 즉 Page를 이 폴더에 모아두었다. 이때 모든 페이지는 DefaultLayout을 정의하여, 기본 레이아웃 아래에서 화면을 그리도록 하였다.

한편 폴더를 나눌 때 feature-first 구조를 채택했다.

API

보통은 백엔드에서 API 명세를 정의하는데, 우리의 경우 빠르게 개발이 필요했고, 이때는 프론트엔드인 내가 백엔드에게 필요한 데이터를 요구하는 게 가장 빠르다. 내가 요구한 명세를 백엔드에서 받고 이를 구체적으로 명세화하는 과정인데, 별다른 이견은 없어서 바로 어셉트되었다.

 

내가 직접 요구하면서 한 가지 좋은 점은, API 설계 과정에서부터 전략적으로 추상화가 가능하도록 설계가 가능하다는 점이다. 이는 상속 구조를 적절히 활용만 한다면 매우 강력한 힘을 가질 수 있다.

 

그래서 리소스 설계를 할 때부터 플러터 코드의 설계를 염두해두고 API 리소스 설계를 진행했다.

 

DDD, 즉 도메인 주도 설계라는 용어가 있다. 농담 삼아서 이와 반대로 나는 API 설계 시 상위 아키텍처를 미리 염두해두고 도메인을 구성한 느낌이다.

 

뭐 당연히 나중에 가서 이것이 가진 치명적인 문제점이 발생했고… 그에 따라 조금 고생하긴 했다.

 

어떤 문제점이 발생했냐면, API가 변경되었을 시 대응이 안 된다. 예를 들어 들어오는 데이터의 구조가 달라질 때 문제가 발생한다.

 

원랜 서비스 레이어와 도메인 레어어를 분리하였기에, 데이터를 받는 DTO를 Repository에서 사용하고, 이를 서비스 단에서 잘 조합해서 기존에 사용하던 모델을 그대로 사용할 수 있다. 이것이 원래 MVVM의 구조에서 ViewModel이 해야 하는 일이다.

 

그런데 내가 너무 설계에만 집착한 나머지… 오히려 ViewModel이 이런 구조 변화에 대응하지 못하는 일이 벌어졌다.

 

물론 후에 상속 구조를 풀어서, 바뀐 API에 대해 ViewModel이 대응할 수 있도록, 그래서 바뀐 repository와 dto가 view 단에 영향을 미치지 않도록 수정하였다. 어려운 작업은 아니고 중간에 어댑터같은 거를 달아서 model과 ViewModel을 연결하였다.

 

하지만 Repository에 관해서, 쿼리 파리미터를 쓰도록 API 명세가 변경되었을 때 Repository도 추상화시켜놓았는데 이를 풀어야 했다. 그런데 이를 푸는 순간 이 Repository를 사용하는 상위 추상 StateNotifer(Service)도 상속을 풀어야 했고, 그래서 이 Provider를 사용하는 Component 역시 사용이 안 되는 불상사가 발생할 뻔 했다물론 이 문제는 선술했듯이 중간에 변환을 해주도록 계층 하나를 더 추가해주면 해결이 되는 문제이기는 하다. 그래서 어떻게든 View의 Layer까지 변화의 영향이 침범하는 문제는 막을 수 있었다.

 

결론적으로 API 명세는 롤백되었다. 하지만 바뀌려고 시도되었던 API 명세도 충분히 고민해볼 만한 명세이기는 하다. 왜냐면 바뀐 명세가 개발 초기에 논의된 방식이기는 했다. 물론 현실적으로 API 명세는 개발 초기에 정해진 약속이고, 개발이 마무리되는 시점에서 이를 변경할 시 구조에 영향을 미치는 것은 막을 수는 없다. 그래서 한 번 API가 정해진 이상 바뀌기 어려운 거고, 그렇기 때문에 처음에 컬렉션과 리소스의 구분에 대한 논의를 확실히 정한 뒤에 개발을 진행해야 한다.

 

하지만… 현실은 또한 언제나 이상적이지는 않다. 앞으로 개발을 하다보면 API의 구조 자체가 변화되는 일은 수없이 많을 것이다. 단순히 패스가 변화되는 것 뿐만 아니라 리소스 자체가 변화할 수도 있다. 모든 것이 전부 다 바뀔 수도 있다. 이에 대응할 줄 알아야 한다.

 

여기에 대응하기 위해서 레이어를 구분한 것인데, 정작 레이어를 구분했어도 대응에 미비했다.

 

상속 구조를 너무 타이트하게 만드는 데 집중하다보면, 이상적일 때는 모든 코드가 톱니바퀴처럼 완벽하게 맞물려서 돌아가다가도, 약간의 변화를 주어야 할 때는 전체 구조를 해체시켜야 하는 문제점이 발생한다. 그렇기 때문에 우리는 레이어를 나누고, 각 레이어 간 의존성을 최대한 낮추는데 집중한다. 레이어를 애써 나눠놨는데 여기서 코드가 아름답게 맞물려 돌아가는 것을 보고자 레이어 간 결합도를 높이는 선택은 주객전도다.

 

이번 경험을 통해 매우 큰 교훈을 얻었다. 또한 ViewModel에서의 적절한 처리를 통해 View까지 영향이 가는 것을 방어하였으니, 미비하지만 이렇게 대응하면 되겠구나 느끼기도 하였다. 큰 경험이다.

채택된 기술 스택

내가 가장 잘할 수 있는 스택들로 구성했다. 그래서 새롭게 기술을 학습해야 하는 부담은 적었다.

 

여기에는 AI를 담당한 분의 배려가 들어가있었다. 이 분은 원래 모바일 개발로 팀에 들어오셨다. 문제는 이 분은 네이티브 개발, 즉 Swift와 Kotlin 쪽 개발만 진행하셨다.

 

하지만 나의 경우는 Flutter 외에 할 수 있는 것이 없다. Spring 백엔드가 두 명이나 있는데 django로 백엔드 개발을 할 수도 없는 노릇이니깐.

 

새롭게 네이티브 개발을 배울 수도 없는 노릇이여서, 기술 스택은 Flutter로 정하고 모바일 개발의 기술 스택을 내가 원하는 것으로 정할 수 있었다.

 

그에 따라 AI를 맡은 분의 학습 부담은 늘어났다. 그래서 모바일 개발에 직접적으로 참여하기 보다, AI 모델 개발 및 Firebase functions와 GCP ML 구성을 주도적으로 맡으셨고, 플러터 개발의 경우 Pose Estimation 관련 기능만 맡으셨다.

 

일단 여기가 Solution Challenge인 이상 Flutter로 하는 것이야 기정 사실이고, 나 역시 애초에 Flutter 개발로 권유를 받고 들어왔으니 여기에 변동은 없다. 그 외 세부적인 주요 기술 스택을 정해야 하는데

  • Riverpod: 상태 관리로는 Riverpod을 채택하였다. 이유는 가장 많이 사용했고, 가장 제대로 배운 상태 관리 툴이라서 그렇다. GetX와 비교하면, GetX는 배울 때 제대로 배우지 못한 것 같아서 조금 어려움을 겪었다.
  • Go Router: URL로 페이지 라우팅을 관리할 수 있는 패키지이다. Flutter의 위젯 트리 특유의 단점을 상쇄시킬 수 있다.
  • Freezed, JsonSerializable, Retrofit: Code Generation의 대표적인 3인방이다.

코팩 님 강의로 플러터를 입문한 사람이라면 알겠지만 강의 속 바로 그 구성이다.

 

그런데 사실 위 구성은 거의 표준이 되어 가고 있는 듯 하다. 최근에 내가 관심을 갖는 Flutter 앱 중에서, Lichess 모바일 앱의 v2 버전 개발이 있다. 나중에 기회가 되면 어떻게 개발하는 지 분석하고 블로그에 공유하고자 하는데, 여기서도 riverpod, go router, retrofit 등의 기술을 똑같이 사용하고 있다.

Firebase와 GCP

Firebase는 알면 알수록 좋다. 사용하기 편하고, 가격도 싸다. Firebase는 후에 제대로 각잡고 공부해보는 것도 좋을 듯 하다.

Authentication

나는 이번에 Firebase Auth만을 사용했다.

 

사실 프론트엔드 입장에서는 서버에서 직접 토큰 방식 로그인을 구현하든, Firebase Auth를 사용하든 별 차이는 없다. 그러나 백엔드에서 JWT를 구현하는 게 시간이 걸릴 수 있기에 이를 절약하고자 Firebase Auth를 사용하자고 했는데, 오히려 이것이 백엔드 측에서 유저 테이블이 없어지면서 어떻게 처리해야 할 지 고민을 한 것 같다.

 

내가 예전에 개인 프로젝트를 만들고 있을 때 백엔드를 구현할 때는 이 문제를 이렇게 처리했다.

 

먼저 나는 원래 User 테이블과 Auth 테이블을 분리한다. Auth 테이블은 순수 Authentication에 관한 내용만 저장하고, User 테이블은 Auth 기능 외에 유저 정보에 대한 데이터를 저정한다.

 

대형 서비스로 갈 수록 User 테이블과 Auth 테이블은 분리된다고 한다. 기능이 많아질 수록 User 테이블에 변화가 일어나게 되면, Auth 서비스에도 영향을 미치기 때문이다. 그리고 Auth 등 User 등 만약에 어느 한 쪽 DB를 샤딩하거나 레플리케이션하게 된다면 분리되어 있는 게 무조건 유리하다. 특히 Auth가 그런데, 예를 들어 Authetication 서비스를 Redis 데이터베이스에 저장하고, 이를 샤딩하거나 지역별로 레플리케이션을 만든다고 가정하자. 그렇다면 다른 User 컬럼 데이터까지 전부 Redis에 넣을텐가? 아닐 것이다. 확장성에 유리함이 많은 만큼 Auth와 User 테이블은 분리해서 나쁠 것이 없다. 그리고 엄밀히 따지면 정규화의 입장에서도, Auth와 User의 기능은 구분되어야 한다고 생각한다.

 

User 테이블과 Auth 테이블이 분리되었다면, 우리는 이제 Auth 테이블이 MSA의 구조처럼 분리되는 상황을 해봐야 한다. 이 경우 기존의 서버에서는 Auth 서버로 API 호출을 통해 Authentication의 인증이 유효한 지를 체크할 것이다.

 

이를 구현하는 아키텍처는 여러 가지가 있다. 예를 들어, API Gateway Server가 있고, 그 뒤에 Authentication Server, File Server가 동시에 존재한다고 가정하자. 파일 서버는 인가된 유저의 파일만 업로드해야 한다. API Gateway가 있으니, 먼저 API Gateway에서 Auth Server로 JWT 토큰의 인증/인가를 검사한 뒤에, 적절하다면 File Server로 Request를 통과시키는 방법이 있다(내가 구현했던 방식이다).

 

아니면 굳이 API Gateway를 만들지 않고, 그냥 File Server에서 직접 Auth Server로 API 호출을 통해 JWT를 검사하는 로직이 있다(이 프로젝트에서 백엔드가 이런 방식을 채택한 것으로 보인다).

 

Firebase Auth를 이용한다면, 바로 이 Auth Server를 분리한 것과 똑같은 효과를 얻게 된다.

 

다만 여기서 주의해야 하는 것은 User 테이블과 Auth 테이블의 동기화 문제이다. 회원 가입 시 이를 File Server에서 어떻게 알 것인가? 이 부분은 일단 프론트 단에서 유저가 회원 가입을 할 시, 이 사실을 Firebase Auth 뿐만 아니라 다른 서버로 알리는 방법이 있다.

 

다른 방법으로는 가능하다면, 유저가 회원가입하더라도 아무 동작이 없으면, 해당 유저의 레코드가 생성되지 않아도 되는 방법이 있다. 이전 개인 플젝의 디비 구조에서는 이게 가능했다.

 

하지만 이 프로젝트에서는 디비 구조상 유저의 퀴즈 풀이 여부를 저장해야 하기에, User : Quiz 테이블 사이 User-Quiz 테이블이 존재해야 한다. 아마 여기서 User-Quiz 테이블이 UserId를 FK로 가져야 하는데, 이 FK로 가질 키가 없으니깐 문제가 발생한 것으로 보인다.

이를 해결하는 검증된 방법이 있는지는 모르겠다. 그런 상황에서 이를 해결하느라 고생한 것 같다.

Firebase Functions

Firebase Functions를 이용해서, Pose estimation 모델을 돌리는 작업은 AI 팀원 분이 담당하였다.

매우 신기했다. GCP ML과 연계하여 서버리스로 빠르게 개발을 완료하시는 걸 보고 멋지다고 생각하기도 했다.

이거 어디서 많이 봤는데…

GCP 가격
PTSD On...

이거 어디서 많이 본 상황이다. 그때도 비용에 관련해서 꽤 사고가 많이 터졌었다.

 

이번에도 크게 다르지 않다. DB를 일반 서버에 올리는 게 아니라, 아주 Flex한 옵션인 완전 관리형 데이터베이스 서비스를 사용하였고, 또 VM 인스턴스 역시 무료 버전이 아닌 유료 VM을 사용하여 배포되었다.

 

아쉬운 점은 이 프로젝트에서 사용된 Firebase는 Blaze 요금제, GCP는 3개월 동안 무료로 사용할 수 있어 비싼 서비스를 이용한다고 하더라도 돈을 안 낼 수 있었다. 한 번의 방어선이 있었던 셈이다. 그런데 왜 돈을 내게 되었냐고? 서비스가 프로젝트 GCP 계정이 아닌, 배포자 개인의 계정으로 따로 배포되었기 때문이다. 이거 정말… 6개월 전의 공포가 생각난다.

 

이런 일은 어떻게 막을 수 있었을까?

이런 일은 보통 개인의 문제라기 보다는 팀 전체의 문제에서 비롯된다. 배포 시 소통이 부족했고, 역할을 제대로 분배하는 데 실패했다. 배포는 특히 비용에 관련된 부분인만큼 팀 전체가 집중해서 진행해야 했다. 작년의 사례를 미리 경험한 나인만큼 특히 더 방지할 수 있는 기회가 있었던 것 같다.

최종 제출

올해도 역시나 기한은 연장되었다

원래는 2월 22일까지 제출이었다. 그러나 역시나 얘네들, 25일까지로 기한 연장하였다.

 

22일 전날에 밤 새가면서 개발을 하였는데, 아침 6시에 연장되었다는 말 듣고 바로 취침하러 갔다.

사실 천운이기도 한게, 연장 안 되었다면 개발 절대 다 못 끝냈다.

 

웃긴 점은 기능의 90%는 제출 열흘 전에 완성되었다. 그 중에서도 60%는 마감 5일 전에 전부 완성되었다.

 

특히 22일 마감 직전 매우 유튜브 플레이어, 혹은 카메라 녹화라는 네이티브 기능을 쓰는 페이지가 두 개 있었는데, 이 두 페이지는 불과 8시간만에 구현되었다. 1월 달에는 단순 메인 페이지 하나를 2주에 걸쳐서 느긋~하게 만들었는데, 역시 마감 직전이 되어서야 사람은 급해지나 보다. 원래 개발이 이런 건가…

영상 찍기

하필 25일, 제출해야 하는데 역시나 문제가 터지더라. 코드가 그리 바뀐 게 없는데 앱이 녹화 중에 꺼진다든지 하는 문제가 보고되었다. 사실 굉장히 어이가 없기도 하고 억울했던 게 아니 코드가 바뀌지 않았는데 왜 갑자기 안 되는 거야… 라고 플러터에게 떼써봐도 상황은 바뀌지 않는다. 어떻게든 문제가 발생할 수 있는 부분을 뗌빵하고, 영상을 찍어야 한다.

 

원래 영상을 찍기로 한 분의 녹화가 불가능해져서, 급하게 나의 폰으로 영상을 녹화하기로 했다.

하지만…

당신의 USB 디버깅 나의 비활성화로 대체되었다
당신의 USB 디버깅 나의 비활성화로 대체되었다

아니 평소에는 USB 디버깅 잘만 켜져 있는데 왜 갑자기 안 되는 거야…?

 

정말 뜬금없이 내 휴대폰에서 USB 디버깅 옵션이 비활성화되었다. 이때 내가 느낀 심정은 충격과 공포 그 자체다. 어떻게 다시 활성화하는 지도 모르겠다.

 

정말 다행히 학교에서 빌린 아이패드가 있어서 아이패드에 대신 연결하기로 했다. 문제는 내가 아이폰을 쓰는 것도 아니여서, iOS 실기기에 연결해보는 것은 이번이 처음이었다. 인터넷에 검색해가면서 어찌저찌 연결에 성공은 했으나…

 

정말 기도 메타였다. 나 역시 앱이 뜬금없는 타이밍에 꺼지는 문제가 계속되었다. 다시 켜보려 해봐도 방금 전까지 빌드가 성공했는데, 다시 빌드 실패가 된다든지… XCode에서 안 되니깐 vscode에서 되었고, 그러다가 다시 꺼졌다. 그래서 다시 빌드했는데 이번에는 vscode에서 빌드가 실패하고 xcode에서 된다든지… 이런 상황이 몇 번이고 반복되었다. 그래서 마지막으로 되었을 때 정말 기도 메타로 꺼지지 말아주세요 하고 앱 시연을 녹화했다.

 

내가 녹화한 부분은 CPR을 진행하는 부분이다. 내가 CPR 하는 장면을 카메라로 녹화하고, 이를 서버로 올리면 그에 대한 결과를 반환받는다.

 

여기서도 멀쩡히 작동하던 firebase functions에서 파일을 못 찾는다든지 문제가 발생했다.

 

원래 firebase storage로 유저의 영상이 업로드되고 나면, 이후에 firebase functions에서 이를 수집해서 pose estimation model을 돌린 뒤 결과를 다시 반환하는 구조인데, functions에서 storage의 파일을 찾지 못한다. 분명 업로드가 완료된 이후에 firebase functions를 호출하는데, 파일이 없다니 그저 황당할 뿐이다. await를 안 한 것도 아니고…

 

파일이 100% 업로드가 된다 하더라도, 어쩌면 즉시 firebase functions를 호출 시 스토리지에서 파일을 찾지 못할 가능성도 있다. 그래서 임시로 해결한 방법은 그냥 2초를 기다리는 것이다.

  Future<void> uploadAndSubmit({
    required Id actionId,
    required File file,
    required String videoUrl,
  }) async {
    await upload(
      actionId: actionId,
      file: file,
    );

    // wait for 3 seconds preventing server cannot find the uploaded file
    await Future.delayed(const Duration(seconds: 2));

    await submit(
      actionId: actionId,
      videoUrl: videoUrl,
    );
  }

원래는 upload, submit 함수를 따로따로 호출했는데, 이를 통합하는 하나의 함수를 만든 뒤에 여기서 2초간 기다린 후 submit을 진행했다. 솔직히 근본적인 솔루션은 절대 아니고, 이런 방식은 결코 좋은 방법은 아니지만… 어쩔 수 없다.

 

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

최종 제출 영상이다. AI를 맡으신 분이 제작하셨고, 내가 녹화한 건 1:25s 부근부터 나온다. 아주 열심히 CPR 하는 모습이 인상적이다.

이때가 한국 시간으로 새벽 11시였다. 밤에 집에서 패딩으로 간이 토르소처럼 만들고 열심히 CPR 연기하는 게 참… 웃겼다.

서류

과거에는 영어로 서류를 작성하는 게 참 부담이었는데, 이제는 GPT와 여러 언어 모델 기반 번역기(deepL 등)의 등장으로 전보다 훨씬 쉬워졌다. 영어로 내가 직접 작성해도 되지만, 이제는 한국어로 작성한 다음에 GPT 혹은 deepL을 돌리는 게 더 빨라졌다. 정말… 세상의 혁명이다.

 

나는 서류 작성은 부분적으로만 도와주었다. 거의 대부분은 AI 분이 영상을 제작하시면서 서류까지 마무리하셨고, 다소 빈약한 부분이랑 수정이 필요한 한 두 질문만 답을 추가하였다.

제출!

최종 제출 구글 서브미션 폼
최종 제출 구글 서브미션 폼
팀 레포지토리
팀 레포지토리

이렇게 해서 최종적으로 제출하게 되었다!

길다면 길고 짧다면 짧은 지난 두 달 간의 여정이 끝나게 되었다!

후기

클라우드 중요하더라

그래서 난 전역하자 마자 GCP, AWS까지 전부 배울 예정이다. 아예 제대로 각잡고 교육 이수할 생각이다.

 

지난 NCP 스터디는 솔직히 말해서 수박 겉핡기에 지나지 않았다. 이번 프로젝트를 계기로, 클라우드를 제대로 활용할 수 있는 사람과 활용하지 못하는 사람의 개발자로서의 격차가 얼마나 큰 지를 느꼈다.

테스트 중요하더라

테스트 코드 작성이 중요하다는 건 누구나 안다. 그런데 테스트 코드를 정작 짜는 사람은 그렇게 많지는 않았던 것 같다. 그중 하나가 나다.

특히 플러터인만큼 테스트 코드를 작성하는데 있어서 이질적인 것도 있다. 프론트엔드의 테스트 코드는 어떻게 짜야 하지? 라는 고민이 든다.

 

그걸 공부하기 위해서 아주 오래전에 비싼 강의도 구매했었는데, 놀랍게도 반 년 째 안 보고 있다. 봐야겠다. 오히려 프론트엔드에서의 테스트가 아주 중요한 것 같다.

 

한편 화면을 개발할 때, 토스같은 경우 자체적인 모듈 시스템으로 프론트엔드 개발 시 화면을 테스트하고 개발한다는 글을 본 적이 있다. 매우 감명깊었고 원하는 기술이었다. 왜냐면 특히 동적인 화면, 혹은 진입 시까지 오래 걸리는 화면을 개발할 때, 그렇게 분리된 컴포넌트 없이는 개발이 참 난항을 겪기 때문이다. 파일 업로드 시 % 변화 화면은 어딘가에 픽스해두어야 하고, 또 진입 시 오래 걸리는 화면은, 개발 과정에서 실수로 에러가 나서 hot reload를 하는 순간 다시 그 패스로 찾아가야 한다. 이 얼마나 원시적인가…

 

이런 테스트 코드, 모듈러 시스템에 관한 내용 말고도 심지어 API 연결 역시 테스트화할 수 있다. POSTMAN의 mock 서버를 이용하면 되는데, 이번 프로젝트에서 이 기능을 굉장히 유용하게 적용하였다. 진작에 사용할 걸 그랬다. 백엔드에서 개발이 늦어지니깐, 바로 목 서버를 구축해서 API 연결을 테스트하고 개발을 진행했는데, 아주 만족스러웠다.

상태 관리를 제대로 아는 것, 너무나 중요하더라

Riverpod을 적극적으로 사용했다. 하지만 과연 제대로 사용하였는가는 잘 모르겠다. 정확히는, 설계를 제대로 하였는가에 대한 질문에 답을 못 하겠다.

 

캐싱을 하는 것은 좋다. 그런데 오히려 캐싱을 생각하느라 성능적인 문제를 야기한 코드가 눈에 너무 많이 보인다. 예를 들어 Action에 대해 User Submission의 상태를 제공하는 Provider가 있다. 이때 파일 업로드 상태는, 그냥 별개의 Provider로 제공하는 게 더 좋았을 것이라는 생각이 든다. 각 id 별로 파일 업로드 상태를 따로 구분할 필요가 없다. 현재의 구현에서는 파일 업로드, 결과 대기, 결과 점수 모델이 전부 하나의 상태 동류들로 관리된다.

 

이게 왜 문제냐면 결국 id에 맞는 상태를 찾아 매핑해야 하는데, 파일 업로드같은 경우 워낙 빠르게 상태가 변하다 보니깐 StateNotifier의 상태 변화 속도가 파일 업로드 변화 속도를 가끔씩 놓친다. 그럴 때 로직 상 state가 아주 잠깐(한 틱 정도) ErrorState로 변하게 되는데, 유저 입장에서는 0.1초 정도 순간적으로 에러 화면을 보게 된다. 그리 기분 좋은 경험은 아니다.

 

그래서 위 두 개의 교훈에서 얻은 결론은? 이전에 사두었던 Code with Andrea의 강의를 정독하자… 그리고 테스트 관련된 책도 좀 읽자.

미루지 말자

기획 기간 제외하고 순수 개발 기간은 약 한 달 반 정도였다.

 

이중 90%의 기능은 마감 2주일 안에 개발되었다.

 

그중에서도 60%의 기능은 불과 3일만에 개발되었다.

 

기한이 다행히 연장되어서 망정이지, 22일까지였으면 완성도 못 했다.

 

물론 개발이 딜레이될 수 밖에 없던 몇 가지 사정들이 있었고(어떤 기능이 개발되어야 다음 기능을 개발하는데, 그 기능 개발이 딜레이된다든가 하는 문제들), 내 개인적으로도 시험 등 여러 일정들이 겹치긴 했었다. 뭐 근데 그건 다 핑계고 솔직히 유튜브 행복하게 볼 시간에, 2시간 정도만이라도 야무지게 개발에 투자했으면 지금쯤 앱 출시도 했겠다…

 

직장인들 9-6 worklife를 유지한다는 게 정말 대단할 따름이다.

입대 전 마지막 추억이 될 것 같다

입대 전 시간을 나름 불태우면서 보낼 수 있어서 기분이 좋았다.

 

솔직히 말해서 작년은 슬럼프 기간이었다. 2학기 막바지에 가서는 컴공이 과연 나의 길이 맞는 지조차 의심이 들었다. 내가 이 직업을 소망한 것이 1-2년 된 것도 아니고, 15살 때부터 간절히 소망해왔고 배워왔던 길이다. 그렇기에 내가 과연 이 길을 걷는 것이 맞는 것인가에 대한 의문이 들었을 때 꽤 힘들었다. 진심 반 농담 반, 그러나 꽤 깊게 전과도 생각했다. 컴공이 나의 길이 아닌 것 같았다.

 

하지만 이번 방학 때 충분히 쉬면서, 개발을 하니깐 알겠더라. 나는 개발하는 것을 좋아하고, 코드를 짜고 프로그래밍하는 것에 재미와 보람을 느낀다. 내가 좋아하는 개발을 할 때 행복감과 성취감을 느낄 수 있더라.

 

다시금 배우는 것이 즐거워졌고, 지식을 흡수하는 게 너무나 재밌어졌다. 코드를 설계하고, 머리를 쥐어짜내며 코드가 맞물려 돌아가게끔 쌓아올릴 때 도파민이 흘러 넘치더라. 전과 고민은 취소다.

결국 모두가 역할을 맡아주었기에 완성할 수 있었다

각자 맡은 바의 역할을 포기하지 않고 끝까지 다 해주어서 끝까지 제출하는 데 성공하지 않았나 싶다.

  • 기획부터 디자인을 발전시키고, AI 모델을 개발하시고 팀을 안정적으로 매니징한 jlstdio 님 + 영상 만들고 발표하고, 서류 작성하느라 수고 많았습니다
  • 백엔드 개발을 맡고 발생한 문제를 빠르게 해결한 alsrudursla
  • 디자인을 맡고, 기획적인 부분에서 놓치고 있는 부분을 짚어주신 DamWon-KIM 님 + 배포 과정 중 발생한 문제 해결하느라 고생 많았습니다

모두 고생 많았습니다.