LineMemo

2020.02.10 (Mon) ~ 2020.02.24 (Mon)

아직까진 혼자 개발할 일이 많다보니 다른 사람이 내 코드를 보고 피드백 받을 일이 거의 없었다.
그러다 프로그래머스에서 라인 앱 개발 챌린지를 발견했는데, 내 코드를 리뷰받을 수 있는 좋은 기회라고 생각해서 참여하게 되었다.
마침 지원 대상이 경력 2년 미만의 앱 개발자여서 나에게 적합하다고 생각했다.

지원 방법이 간단해서 이름, 이메일 등 기본적인 정보만 적으면 바로 시작할 수 있었다.
지정된 기한(2020-02-24)까지 주어진 앱을 만들어서 제출하면 된다.

앱 세부사항 (조건)

  • 언어: Java, Kotlin 중 택1
  • minSdkVersion: 21
  • targetSdkVersion: 27 이상
  • 라이브러리 사용: 필요시 외부 라이브러리 사용이 가능하며, 사용시 출처(링크)를 주석으로 포함주세요. 단, AndroidX, Google 라이브러리는 출처를 적지 않아도 됩니다.

기능 요구사항

기능1: 메모 리스트
  1. 로컬 영역에 저장된 메모를 읽어 리스트 형태로 화면에 표시합니다.
  2. 리스트에서는 메모에 첨부되어 있는 이미지의 썸네일, 제목, 글의 일부가 보여집니다. (이미지가 n개일 경우, 첫 번째 이미지가 썸네일이 되어야 함)
  3. 리스트의 메모를 선택하면 메모 상세 보기 화면으로 이동합니다.
  4. 새 메모 작성하기 기능을 통해 메모 작성화면으로 이동할 수 있습니다.
기능2: 메모 상세 보기
  1. 작성된 메모의 제목과 본문을 볼 수 있습니다.
  2. 메모에 첨부되어 있는 이미지를 볼 수 있습니다. (이미지는 n개 존재 가능)
  3. 메뉴를 통해 메모 내용 편집 또는 삭제가 가능합니다.
기능3: 메모 편집 및 작성
  1. 제목 입력란과 본문 입력란, 이미지 첨부란이 구분되어 있어야 합니다. (글 중간에 이미지가 들어갈 수 있는 것이 아닌, 첨부된 이미지가 노출되는 부분이 따로 존재)
  2. 이미지 첨부란의 ‘추가’ 버븐을 통해 이미지 첨부가 가능합니다. 첨부할 이미지는 다음 중 한가지 방법을 선택하여 추가할 수 있습니다. 이미지는 0개이상 첨부할 수 있습니다. 외부 이미지의 경우, 이미지를 가져올 수 없는 경우(URL이 잘못되었거나)에 대한 처리도 필요합니다.
  3. 편집 시에는 기존에 첨부된 이미지가 나타나며, 이미지를 더 추가하거나 기존 이미지를 삭제할 수 있습니다.

디자인

디자인에 대한 사항은 주어지지 않아서 우선 디자인을 어떻게 할지 생각해봤다.

  1. 처음 앱을 실행하면 메모 리스트 화면이 보여지고, 우측 아래에 있는 FAB(Floating Action Button)을 누르면 메모 작성 화면으로 넘어간다.
  2. 메모 리스트에서 아이템을 누르면 메모 상세 보기 화면으로 넘어간다.
  3. 메모 상세 보기 화면에서 ActionBar에 있는 수정 버튼을 누르면 메모 편집 화면으로 넘어간다.

메모 편집 및 작성 화면은 하나의 Activity로 처리하는 게 좋을 것 같고, 메모 상세 보기 화면을 어떻게 할지 두가지 선택지가 생긴다.

  1. 메모 상세 보기 Activity를 따로 만든다.
  2. 메모 편집 및 작성 화면에서 메모 상세 보기까지 구현한다.

1번 방법으로 하면 개발자 입장에선 편한데 사용자 입장에선 새로운 화면으로 전환되는 과정에서 불편함을 느낄 것이다.
2번 방법으로 하면 사용자는 편하지만 보기 상태, 편집 상태를 하나의 액티비티에서 관리해야 하기 때문에 구조가 복잡해지고 코드가 한곳에 집약될 가능성이 있다.

개발자가 편하면 사용자가 귀찮고, 개발자가 귀찮으면 사용자가 편하다.

이런 맥락의 글을 어디선가 본 것이 기억났다.
그리고 대부분의 메모 앱이 2번 방법으로 하고 있다는 것을 발견해서 2번 방법으로 구현 하기로 결정했다.

구현

MVVM이라는 것을 공부한지 얼마 안됐을 때여서 실제 프로젝트에 써볼 기회가 없었다.
그래서 이번 기회에 제대로 구현해보기로 마음 먹었다.

앱에서 필요한 액티비티는 두개이다.

  1. ListActivity: 처음 실행했을 때 나오는 액티비티. DB에 저장된 메모들을 RecyclerView로 띄어준다.
  2. EditActivity: 메모를 작성 / 수정 / 삭제 하거나 ListActivity에서 메모를 클릭하면 메모 상세정보를 보여줄 액티비티.

난관

개발 중 만난 몇 가지 문제점들, 난관들을 해결한 과정

첫 번째: MVVM에서의 이벤트 처리

MVVM 구조에서는 ViewModel이 View에 대한 종속성이 있으면 안된다.
여기서 한가지 난관에 봉착했다.
ListActivity에서 메모 추가 버튼을 누르면 ListViewModel에서 그 이벤트를 받는데, ViewModel에는 View에 대한 종속성을 넣을 수 없어서 startActivity를 할 수가 없었다.
구글링을 해보다가 몇가지 방법을 찾았다.
우선 기초는 Activity에서 ViewModel를 관찰하다가 변화가 생기면 그에 따른 이벤트를 실행해주는 것이다.
이를 구현하는 몇가지 방법이 있는데, 나는 그 중 Event wrapper 방식을 택했다.

// ListViewModel
private val _startActivity = MutableLiveData<Event<Boolean>>()
val startActivity: LiveData<Event<Boolean>> = _startActivity

fun onAddButtonClick() {
    _startActivity.value = Event(true)
}
// ListActivity
viewModel.startActivity.observe(this, EventObserver {
    startActivity(Intent(this, EditActivity::class.java))
})

챌린지가 끝난 이후에 블로그 글로 정리해놨다. 글 보기

두 번째: ListActivity에서 startActivityForResult를 쓰게 된 이유

ListActivity에서 메모 추가 버튼을 누르면 EditActivity가 시작되고, EditActivity에서 새로운 메모를 작성하면 finish되면서 다시 ListActivity가 보여진다.
이 때 ListActivity는 새로운 메모가 추가되었다는 것을 인지하고 리스트를 업데이트 해야 한다.

가장 쉬운 방법은 ListActivity의 onStart()가 호출되면 메모 리스트를 다시 불러오는 것이다. 하지만 ListActivity의 onStart()는 메모 리스트에 변화가 없을 때도 호출될 가능성이 있고, 그 때마다 DB에서 리스트를 불러오는 작업을 다시 해야하기 때문에 매우 비효율적이다.

두 번째 방법은 ListActivity와 EditActivity가 공유하는 ViewModel을 만드는 것인데, 불필요한 ViewModel로 코드가 복잡해질 가능성이 있기에 좋은 방법은 아니다.

그래서 선택한 방법은 ListActivity에서 startActivityForResult로 EditActivity를 시작하는 것이다.

// ListActivity에서 EditActivity를 시작하고,
startActivityForResult(intent, REQUEST_CREATE_MEMO)
// EditActivity에서 새로운 메모를 저장한 후에 setResult 해주면,
private fun onMemoSaved(memo: Memo) {
    val intent = Intent().apply { putExtra(KEY_MEMO_ITEM, memo) }
    setResult(Activity.RESULT_OK, intent)
    finish()
}
// 다시 ListActivity에서 받아서 ViewModel에 전달해준다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    val memo = data?.getSerializableExtra(KEY_MEMO_ITEM) as? Memo
        ?: return super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == Activity.RESULT_OK) {
        viewModel.onMemoCreated(memo)
    }
}

추후에 메모 수정, 삭제도 resultCode와 requestCode로 나눠서 구현했다. 코드 보기

결과

결과는 불합격이었지만 상위 2퍼센트라는 것에 놀랐다.

아쉬운점

1. Model 객체가 POJO로 구성되지 않은 부분이 아쉽습니다.
사실 POJO라는 것을 이번에 처음 알게 되었다.

POJO란 객체지향적인 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 말한다.
-토비의 스프링

나는 ImageItemMemo라는 두개의 모델을 만들었는데, 둘 모두 Serializable를 implements 하고 있고, Memo 모델은 사실 Room을 사용하기 위한 엔티티이기 때문에 Room에 종속되어 있다. 하지만 이를 View 부분에서도 그대로 사용하고 있다는 것은 내 생각에도 문제가 있어 보인다. View에서는 View에 맞는 또 하나의 모델을 만들고 이를 매핑해주는 Mapper를 구현했으면 더 좋았을 것 같다는 생각을 했다.

2. LiveData의 observeForever를 사용해주셨는데, removeObserver를 호출해주지 않고 있습니다.
이부분이다.
처음 한번은 무시하고 그 다음부터 변경사항이 있으면 hasChanges를 true로 바꿔서 뒤로가기 버튼을 눌렀을 때 저장 팝업을 띄우기 위해 작성한 코드이다. observeForever를 사용하면 안 좋다는 것, 사용을 한다고 해도 removeObserver를 꼭 해야한다는 것을 알고 있는데도 이정도는 괜찮겠지 하는 마음으로 했다. 하지만 이런 코드가 반복되면 언젠가는 문제가 발생하기 마련이다.
onClear에서 removeObserver를 호출하면 되는 간단한 문제였다.

3. 외장 스토리지에 읽기쓰기 권한요청을 하는 코드는 없는데, 매니페스트에는 읽기 쓰기 권한이 명시되어 있습니다.
외장 스토리지가 아니라 내장 스토리지만 사용하는데 왜 쓴건지 모르겠다… (아마 삽질하면서 썼을 가능성이 높다.)

4. !!은 크래시를 유발할 수 있어서 사용을 자제해주는 것이 좋습니다.
!!은 남용하면 굉장히 위험하다. 사용된 모든 경우는 내가 판단했을 때 절대로 null일 수 없는 상황에서만 쓴 것인데, 몇 달이 지난 지금 보니 바로 이해가 안되는 부분이 있기도 하다. 그러니 내 코드를 처음 보는 사람은 당연히 위험해 보일 것이다. 그리고 항상 내가 예상하지 못한 상황이 발생할 수 있기 때문에 위험한 코드는 쓰지 않는 것이 좋다.

5. 메모에 등록되어 있던 링크 이미지의 url이 깨져 있고, 앱의 캐시영역이 삭제되었을 경우 썸네일 영역의 로딩인디케이터 애니메이션이 끝없이 나타납니다. 이정도까지 테스트를 할 줄은 생각치도 못했다. 그리고 아마 이미지를 등록할 당시에는 정상이었지만 나중에 그 url에 있는 이미지가 깨진 경우인 것 같은데, 내가 재현할 방법이 없다..

이 피드백들을 받으면서 전체적으로 가장 많이 느낀 것은 시니어 프로그래머들의 꼼꼼함과 섬세함이다.
나중에 발생할 수도 있는 문제들을 사전에 파악하고, 가능한 모든 경우를 생각할줄 아는 것이 진정한 프로그래밍 실력이라고 할 수 있지 않을까.

결론

2주 가까이 되는 시간 동안 이걸 하느라 많은 시간을 쏟았다.
애초에 지원한 이유도 정말 취직하려고 한게 아니라 피드백을 받고 싶어서였기 때문에 비록 많은 시간을 썼다고 하더라도 절대 후회하지 않을 것이다.
이 경험을 통해서 프로그래밍 할 때 작은 부분 하나하나 신경써야한다는 것을 느꼈고, 몇 개월이 지난 지금은 그래도 이때보다는 나아진 것 같다.
앞으로도 꾸준히 노력할 것이다.

Leave a comment