8. 마무리와 도전과제¶
축하합니다. 빈 프로젝트에서 시작해 진짜 지도, 진짜 검색, 진짜 저장이 되는 앱을 끝까지 만들었습니다. 이 장에서는 새 코드를 거의 쓰지 않습니다. 대신 우리가 만든 것의 전체 그림을 한 발 떨어져서 보고, 스스로 앱을 키워 나갈 도전과제를 남깁니다.

우리가 만든 것 — 전체 구조 복습¶
버킷맵의 부품 전체를 한 장에 그리면 이렇다.
[ 화면 4개 ]
┌──────────────┐ 장소 찾기 ┌────────────────┐ 장소 탭 ┌──────────────────────┐
│ MainActivity │ ───────────▶ │ SearchActivity │ ─────────▶ │ PlaceDetailActivity │
│ 지도 + 마커 │ │ 검색 + 목록 │ │ 상세 + 찜/도장/빼기 │
└──────┬───────┘ └───────┬────────┘ └──────────┬───────────┘
│ 내 장소 │ │
▼ │ 인터넷 │ 저장/수정/삭제
┌──────────────────┐ ▼ ▼
│ MyPlacesActivity │ ┌─────────────────┐ ┌─────────────────┐
│ 가볼 곳/다녀온 곳│ │ [ 인터넷 부품 ] │ │ [ 저장소 부품 ] │
└────────┬─────────┘ │ PlacesApi │ │ SavedPlace │
│ 장소 탭 │ ApiClient │ │ SavedPlaceDao │
└──────────▶ 상세 │ SearchRequest │ │ AppDatabase │
│ SearchResponse │ │ (bucketmap.db) │
│ → Place 로 변환│ └─────────────────┘
└─────────────────┘
화면 사이의 이동은 전부 2장에서 배운 명시적 인텐트이고, 화면 사이의 데이터 전달은 전부 extra 이다.
인터넷 부품은 6장의 Retrofit, 저장소 부품은 7장의 Room 이다.
우리가 만든 파일 전부를 역할 한 줄씩으로 정리하면 이렇다. 파일 이름만 보고 역할이 떠오르면 이 과정을 완전히 소화한 것이다.
| 파일 | 역할 | 만든 장 |
|---|---|---|
MainActivity.java |
지도 + 버킷리스트 마커 + 두 화면으로 가는 현관 | 2·5·7장 |
SearchActivity.java |
검색어 입력 → API 호출 → 결과 목록 | 2·4·6장 |
PlaceDetailActivity.java |
장소 상세 + 찜/도장/빼기 | 2·3·7장 |
MyPlacesActivity.java |
저장한 장소를 칩으로 걸러 보여 주는 목록 | 2·4·7장 |
SearchResultAdapter.java |
검색 결과(Place)를 목록 줄로 그리는 어댑터 | 4장 |
SavedPlaceAdapter.java |
저장 장소(SavedPlace)를 목록 줄로 그리는 어댑터 | 7장 |
Place.java |
검색 결과 한 곳을 담는 우리 앱의 모델 | 4장 |
PlacesApi.java |
"서버에 이런 요청을 보낸다"는 Retrofit 선언 | 6장 |
ApiClient.java |
Retrofit 을 하나만 만들어 나눠 주는 창구 + 사진 주소 조립 | 6장 |
SearchRequest.java |
서버로 보내는 요청 몸체 (JSON 이 될 모양) | 6장 |
SearchResponse.java |
서버가 돌려주는 응답 (JSON 에서 올 모양) | 6장 |
SavedPlace.java |
버킷리스트 한 곳 = 데이터베이스 표의 한 줄 | 7장 |
SavedPlaceDao.java |
넣고/꺼내고/고치고/지우는 방법 선언 | 7장 |
AppDatabase.java |
데이터베이스 본체 (bucketmap.db) | 7장 |
이번에는 데이터 하나가 앱을 통과하는 여행을 따라가 보자. 검색어 "palace" 가 주황 마커가 되기까지:
SearchActivity가 검색어를SearchRequest객체에 담고, Retrofit 이 그것을 JSON 으로 바꿔 구글 서버로 보낸다.- 구글 서버가 JSON 으로 응답하면, Gson 이 그것을
SearchResponse객체로 바꿔 준다. toPlaces()가 바깥 세상의 모양(SearchResponse)을 우리 앱의 모양(Place)으로 변환하고, 어댑터가 목록으로 그린다.- 목록에서 한 줄을 탭하면
Place의 값들이 extra 에 실려PlaceDetailActivity로 이동한다. - [가보고 싶어요] 를 누르면 extra 값들이
SavedPlace로 조립되어 Room 을 통해bucketmap.db파일에 저장된다. - 지도로 돌아오면
MainActivity의onResume()이 DB 를 다시 읽고(findAll), 상태에 맞는 색의 마커로 그린다.
이 여행에서 알 수 있듯, 좋은 앱은 "화면"과 "데이터"가 분리되어 있다.
화면은 데이터를 보여 줄 뿐이고, 진실은 항상 데이터베이스에 있다.
그래서 어느 화면에서 무엇을 바꾸든 — 다른 화면들은 onResume() 에서 DB 를 다시 읽는 것만으로 저절로 맞는 모습이 된다.
완성 점검 체크리스트¶
배포 전의 개발자처럼, 앱의 전체 동선을 순서대로 점검해 봅니다. 하나라도 다르게 동작하면 해당 챕터로 돌아가 코드를 대조하십시오.
| # | 해보기 | 이렇게 되면 성공 |
|---|---|---|
| 1 | 앱 실행 | 지도가 뜨고, 위치 권한 허용 시 내 위치로 이동한다 (5장) |
| 2 | [장소 찾기] → 검색어 입력 → [검색] | 진짜 장소 목록이 나온다 (6장) |
| 3 | 장소 탭 | 사진·주소·평점이 있는 상세 화면이 열린다 (6장) |
| 4 | [가보고 싶어요] | 토스트가 뜨고 버튼이 [다녀왔어요][빼기] 로 바뀐다 (7장) |
| 5 | 지도로 복귀 | 주황 마커가 생겼다 (7장) |
| 6 | 주황 마커 탭 → [다녀왔어요] | 상태가 "다녀온 곳"이 되고, 지도의 마커가 초록으로 바뀐다 (7장) |
| 7 | [내 장소] → 칩 전환 | [가볼 곳]/[다녀온 곳] 목록이 각각 맞게 나온다 (7장) |
| 8 | 상세에서 [빼기] | 마커와 목록에서 사라진다 (7장) |
| 9 | 앱 완전 종료 후 재실행 | 저장한 장소가 그대로 남아 있다 (7장) |
도전과제¶
여기서부터는 정답 코드가 없습니다. 지금까지 배운 부품만으로 전부 만들 수 있습니다 — 막히면 해당 챕터를 다시 펴 보십시오. 스스로 만든 기능이 하나 늘 때마다, 이 앱은 점점 "따라 만든 앱"이 아니라 여러분의 앱이 됩니다.
[self Test 1] - 내 장소에서 고른 곳을 "지도에서 보기"
상세 화면에 [지도에서 보기] 버튼을 추가하고, 누르면 메인 지도가 그 장소로 이동하게 만들어 보시오.
- (힌트) 마커는 이미 지도에 있으니, 카메라만 옮기면 된다. 5장의
CameraUpdate.scrollTo()를 떠올릴 것. - (힌트) 메인으로 "돌아가면서" 데이터를 전하는 방법이 고민이라면 —
Intent를 새로 만들어MainActivity로 보내고,onCreate또는onNewIntent에서 extra 를 읽는 방법을 찾아볼 것.
[self Test 2] - 최근 검색어
검색 화면을 열면 최근에 검색했던 검색어들이 보이게 만들어 보시오.
- (힌트) 검색어도 저장할 데이터이다 — 두 번째
@Entity클래스와 그 DAO 를 만들고,AppDatabase의entities배열에 추가한다. - (힌트) 표의 구성을 바꾸면 데이터베이스
version을 올리고 이사 계획(마이그레이션)을 짜야 하는데, 그것은 아직 배우지 않았다. 학습 단계에서는 앱을 삭제하고 다시 설치하면 데이터베이스가 새로 만들어진다 — 개발자들이 실제로 쓰는 우회법이다.
[self Test 3] - 친구에게 장소 공유하기
상세 화면에 [공유] 버튼을 추가하고, 누르면 장소 이름과 주소를 메시지 앱 등으로 보낼 수 있게 만들어 보시오.
- (힌트) 지금까지 쓴 인텐트는 "어느 화면으로 갈지"를 콕 집는 명시적 인텐트였다. 공유는 반대로 "이 일을 할 수 있는 앱 아무나"를 부르는 암시적 인텐트를 쓴다 —
Intent.ACTION_SEND를 검색해 볼 것.
[self Test 4] - 데이터베이스를 메인 스레드 밖으로
7장의 경고 박스를 기억하는가? allowMainThreadQueries() 를 지우고도 앱이 돌아가게 만들어 보시오.
- (힌트)
Executors.newSingleThreadExecutor()로 일꾼 스레드를 하나 만들어 DB 작업을 맡긴다. - (힌트) 일꾼 스레드에서는 화면을 만질 수 없다 — 결과를 화면에 반영할 때는
runOnUiThread(...)로 감싼다. 6장에서 Retrofit 의enqueue가 해 주던 일을 이번에는 직접 하는 것이다.
[self Test 5] - 방문 소감 메모
다녀온 장소에 "여기 최고!" 같은 한 줄 소감을 남길 수 있게 만들어 보시오.
- (힌트)
SavedPlace에memo필드를 추가하고, 상세 화면에EditText와 저장 버튼을 단다. DAO 에는 memo 만 고치는@Query("UPDATE ...")를 하나 추가한다. - (힌트) 이것도 표의 구성이 바뀌는 일이다 — self Test 2 와 같은 이유로 앱 삭제 후 재설치가 필요하다.
앱을 세상에 내놓으려면 — 배포 소개¶
지금 버킷맵은 에뮬레이터와 내 폰에서만 돌아갑니다. 다른 사람들이 설치하게 하려면 Play 스토어에 배포하는 과정을 거친다.
- Android Studio 의 Build > Generate Signed App Bundle / APK 로 서명된 AAB 파일을 만든다 — 서명은 "이 앱은 내가 만들었다"는 도장이다.
- Google Play Console 에 개발자 등록을 하고(1회 등록비 필요) AAB 를 올린 뒤 심사를 받는다.
배포는 그 자체로 한 과정이 될 만큼 내용이 많아, 이 과정에서는 여기까지만 소개합니다. 중요한 것은 — 여러분이 만든 이 앱은 기술적으로는 이미 스토어에 올릴 수 있는 완성품이라는 사실입니다.
다음 단계¶
더 공부하고 싶은 사람을 위한 예고편입니다.
- ViewBinding — 우리는 화면 부품을 전부
findViewById로 찾았다. 화면이 커지면 이 호출이 수십 개가 되는데, ViewBinding 을 켜면 안드로이드가 부품들을 찾아 둔 클래스를 대신 만들어 준다. - 화면 회전과 ViewModel — 검색 결과가 나온 상태에서 에뮬레이터를 가로로 돌려 보면, 결과가 사라진다. 회전할 때 안드로이드가 액티비티를 부수고 새로 만들기 때문이다. 회전에도 데이터를 살려 두는 전용 보관소가 ViewModel 이고, 여기서부터가 실무 앱 구조(아키텍처)의 세계이다.
마치며¶
3장에서 색 하나 바꾸고 기뻐하던 앱이, 이제 인터넷과 대화하고 데이터베이스를 다루는 앱이 되었습니다. 이 과정에서 여러분이 정말로 배운 것은 지도나 검색이 아니라 — 큰 문제를 챕터 하나 크기로 쪼개서, 매번 실행되는 상태를 유지하며 나아가는 방법입니다. 다음에 만들고 싶은 앱이 생기면, 이번처럼 화면부터 그리고, 부품을 하나씩 붙여 나가면 됩니다.
버킷맵의 첫 번째 목적지로, 여러분은 어디를 담겠습니까? 🧡