티스토리 뷰

728x90

 

 

글에 들어가기 전, 지극히 주관적인 내용임을 밝힌다.

'이렇게 생각할 수도 있구나!'라는 생각으로 가볍게 읽어주면 좋겠다.

 

 

 

우아한테크코스에서 MVP 아키텍처를 기반으로 <영화 극장 선택 미션>을 진행했다.

MVP 아키텍처 관련해서 리뷰어와 여러 의견을 주고 받았고, 덕분에 내 의견을 명확히 정립할 수 있었다.

내가 깨달은 내용들을 의식의 흐름대로 작성해보려고 한다.

 

 

 

리뷰어가 쏘아올린 작은 공..

처음으로 MVP 아키텍처를 적용해보면서, 리뷰어에게 아래와 같은 코멘트를 받았다. 

  • Activity에서 repository 를 presenter 로 넘겨주는 것은, Activity와 presenter 의 역할을 제대로 하지 못하고 있는 것으로 보입니다. view 에서 model 을 presenter 에 넘겨주는 형태이니까요!
  • SharedPreferences는 MVP 중 어느 영역에 속하는지, 그 이유도 같이 남겨주시면 좋겠습니다!
  • AlarmManager는 MVP 중 어느 영역에 속하는 것이 적절할지 고민해보고, 이유를 함께 남겨주세요!
  • presenter 에서 context 를 참조하면 안되는 것일까요?
  • presenter 는 context 를 참조해서는 안됩니다. 그런데, 참조해도 괜찮은 경우도 있습니다.

(PR 링크 참고 -> https://github.com/woowacourse/android-movie-theater/pull/75)

 

 

코멘트를 읽자마자 매우 혼란에 빠졌다.

내가 알고있던 것들과 굉장히 대립되는 코멘트들이었기 때문이다.

나는 MVP에서 Model과 Presenter는 안드로이드 프레임워크에 독립적이어야 한다고 생각했다.

 

또한 다른 크루들의 PR을 참고해봤을 때, Presenter는 안드로이드 프레임워크에 독립적이어야 한다고 말하는 리뷰어들이 대부분이었다.

즉, 리뷰어마다 의견이 달랐기 때문에 더욱 혼란스러웠다.

레벨1에서 수없이 겪었듯이.. 리뷰어마다 의견이 다르다는 것은 "정답이 없다"는 것이다.

결국 직접 여러 방식들의 장단점을 겪어보고, 나만의 기준을 세워야겠다고 생각했다.

 

 

 

아키텍처에는 정답이 있을까?

위에서 적었듯이 나는 "Model과 Presenter는 안드로이드 프레임워크에 독립적이어야 한다고 생각"했다.

이때, "어떤 아키텍처는 ~~여야 한다"와 같은 명제는 매우 위험하다.

 

MVP, MVVM와 같은 아키텍처를 도입하는 목적은, 관심사 분리를 통해 변경사항에 유연하게 대처하기 위함이다.

이러한 목적에 초점을 두지 않고, 아키텍처의 엄격한 명제를 지키는 것에 초점을 두고 시간을 투자한다면 아키텍처를 도입하는 의미가 없다.

관심사 분리를 통해 변경사항에 유연하게 대처할 수 있다면, 아키텍처의 세부적인 규칙사항들은 개발자, 팀, 상황마다 다를 수 있다.

 

그동안 나는 아키텍처가 명확한 규칙이 있을 거라고 생각했고, 그 규칙에 얽매여서 아키텍처의 근본적인 목적을 잊고 있었다.

 

 

 

Model과 Presenter는 안드로이드 프레임워크에 독립적이어야 할까?

그러면 다시 돌아와서, Model과 Presenter는 안드로이드 프레임워크에 독립적이어야 할까?

 

먼저, Model의 종류에 대해 알아보자.

Model은 Domain Model, Ui Model, Data Model로 나눌 수 있다.

이 중 Domain Model은 프레임워크에 의존적이지 않은 순수한 도메인이다.

 

여기서 Data Model은 SharedPreferences, Room 등 과 같은 안드로이드 프레임워크에서 제공하는 라이브러리일 수 있다.

즉, Model은 안드로이드 프레임워크와 종속적일 수 있다.

 

 

나는 Model이 안드로이드 프레임워크에 독립적이어야 한다고 생각했기 때문에 SharedPreferences, AlarmManager 등을 Model로 생각하지 않았다. (사실 기능 구현에 급급해서 MVP 중 어떤 계층으로 분리할 생각을 하지 못했다;;)

그래서 SharedPreferences, AlarmManager를 액티비티에서 직접 생성하고 함수를 호출했다.

또한 SharedPreferences, AlarmManager는 context를 필요로 하기 때문에, 더더욱 액티비티에 존재해야 한다고 생각했다.

데이터를 저장하고, 알림을 설정하는 부분도 비즈니스 로직인데도 불구하고 말이다.

 

즉, 비즈니스 로직을 담당하는 SharedPreferences, AlarmManager는 Model이라는 결론을 내렸다.

그러면 MVP 관심사 분리에 맞게, SharedPreferences, AlarmManager를 Presenter에서 생성하고 함수를 호출해야 하지 않을까?

하지만 Presenter에서 이러한 객체들을 생성하려면, Presenter에 context를 전달해주어야 했다.

 

 

 

Presenter는 context를 알고 있어도 될까?

먼저 안드로이드의 context에는 크게 두 종류가 있다. application context와 activity context다.

acticity context를 Presenter에 전달해서 발생하는 문제점은 아래와 같다.

  • Presenter가 구체적인 뷰를 알게 된다. MVP의 핵심은 View를 추상화함으로써 Presenter가 구체적인 View를 알지 못하고, Presenter가 테스트 용이해지는 것이다. 하지만 acticity context를 알게 되면 MVP 핵심을 완전히 어기게 된다.
  • acitivity는 소멸되었는데 Presenter가 살아있는 경우, acitivity context가 제대로 소멸되지 못하고 메모리 릭이 발생한다.

 

그러면 application context를 전달해주면 어떨까?

acticity context를 전달했을 때의 문제점과 비교해보자.

  • Presenter가 구체적인 뷰를 알게 되나? -> application context는 앱 생명주기 전반에 걸쳐 살아있으므로 구체적인 뷰를 아는 것이 아니다.
  • context가 소멸되지 못하는 경우 메모리 릭이 발생하나? -> application context는 앱 생명주기 전반에 걸쳐 살아있으므로 메모리 릭이 발생하지 않는다. Application이 소멸되면 Presenter도 소멸되기 때문이다.

 

나는 위의 근거로 Presenter에 application context는 전달해도 되겠다는 결론을 내렸다.

application context를 전달함으로써 Presenter의 관심사를 명확히 할 수 있다면 말이다.

(물론 Presenter에서 application context만 들어오도록 제한할 수는 없기 때문에, 다른 개발자가 activity context를 넘길 수 있다..)

 

 

 

리팩터링

기존 코드

class SettingFragment : 
    BaseFragment<SettingContract.Presetner>(), 
    SettingContract.View {
    // ...
    private val ticketAlarm by lazy { TicketAlarm(requireContext()) }
    
    // ...

    override fun setTicketAlarm(ticket: Ticket) {
        ticketAlarm.setReservationAlarm(ticket)
    }

    override fun cancelTicketAlarm(ticket: Ticket) {
        ticketAlarm.cancelReservationAlarm(ticket)
    }
}

 

View인 Fragment에서 TicketAlarm이라는 객체를 생성하고, 이 객체의 함수를 직접 호출했다.

 

 

수정 후 코드

class SettingPresenter(
    private val view: SettingContract.View,
    applicationContext: Context,
    private val ticketAlarm: TicketAlarm = TicketAlarm(applicationContext),
    // ...
) : SettingContract.Presenter {

나는 SharedPreferences, AlarmManager(위 코드에서는 TicketAlarm)의 객체들을 싱글톤으로 구현했다.

싱글톤으로 구현하기 위해서는 application context를 전달하는 것이 적절하다. (activity context를 전달하면, 그 객체는 acticity 생명주기에 종속적이기 때문에 acticity가 소멸되면 객체도 사용할 수 없어진다.)

그래서 Presenter에서 application context를 생성자 파라미터로 받고, 이 context를 통해 객체들을 생성하도록 수정했다.

 

 

 

리팩터링 후 의문점

그런데 Presenter에 context를 전달하는 방식으로 리팩터링하고 보니, 의문이 드는 점이 생겼다.

 

 

1. 테스트가 어려워진다
context가 필요한 Repository, AlarmManager등을 presenter가 직접 생성하고 있기 때문에 이 모든 객체들을 모두 mocking 해주어야 하고, 테스트가 더 복잡해진다.

 

 

2. 추상화의 이점을 충분히 활용하지 못한다
기존 콘솔의 MVC에서는, Application.kt에서 Controller에 구체적인 구현체들을 주입해주었고, Controller에서는 추상적인 타입만 알 수 있었다.

그런데 현재와 같은 구조에서는 Presenter가 구현체를 직접 생성하고 있기 때문에, 구현체를 변경하려면 구현체를 사용하는 쪽인 Presenter를 직접 수정해야 한다.

class SettingPresenter(
    private val view: SettingContract.View,
    applicationContext: Context,
    private val ticketAlarm: TicketAlarm = TicketAlarm(applicationContext),
    // ...
) : SettingContract.Presenter {

일단 위 코드처럼 생성자 프로퍼티로 선언해서 실제 타입은 상위 인터페이스가 되고, 디폴트 값으로 구현체를 지정하도록 작성해주었다.

(테스트에서는 FakeRepository를 주입해주었기 때문에 추상화를 완전히 활용하지 못한 것은 아님)

 

 

3. presenter의 프로퍼티가 많아지고 복잡해진다
view에서 처리하던 비즈니스 로직 관련 함수 호출들을 presenter로 옮기고 보니, presenter의 프로퍼티가 많아졌고 복잡해졌다.

view, model는 최대한 읽기 쉬운 코드로 작성하고 presenter를 더럽히는 것이 맞다고 생각하지만, 구현한 방식이 맞는 건지 아직 확신이 서질 않았다.

 

 

아래는 리뷰어에게 답변 받은 사항이다.

 

1. 테스트가 어려워진다

context를 mocking해서 테스트하면 된다.

하지만 context를 너무 여러곳에 사용하고 깊게 가져간다면 테스트가 어려워질 수 있고, 설계 관점에서 다시 고민해볼 수 있겠다.

 

2. 추상화의 이점을 충분히 활용하지 못한다

이를 해결하기 위한 의존성 주입 라이브러리(Dagger, Koin, Hlit 등)들이 존재한다.

 

3. presenter의 프로퍼티가 많아지고 복잡해진다

MVC의 controller와 동일하게, view와 model을 연결해주는 역할을 하다보니 비대해지는 것이 맞다.

god-object가 되기 쉬우므로 항상 view나 model로 이동시킬 수 있는 코드가 없는지 확인한다.

 

 

 

마무리

약 한 달간 임시저장해두었던 글을 드디어 마무리한다..... (^^)

아키텍처 관련해서 내가 고민했던 과정들을 주절주절 나열해보았다.

MVP를 앞으로 쓰게 될지는 모르겠지만, 아키텍처와 안드로이드 context에 대한 고민은 앞으로도 계속 마주하게 될 것이라 생각한다.

추후 여러 문제를 겪으면서 내 생각이 완전히 바뀔 수도 있고, 누군가는 나와 반대되는 생각을 하고 있을지도 모르겠다.

하지만 다른 개발자가 내린 결론을 그대로 흡수하기 보다, 스스로 생각하며 내린 결론이기에 더욱 즐겁고 뜻깊었다 👍

 

이 글을 통해 전달하고 싶은/되새기고 싶은 것은 "아키텍처에서 명확한 정답을 찾으려하지 말자"는 것이다.

아키텍처의 구체적인 구현 방식은 개발자에 따라, 팀에 따라, 상황에 따라 다를 수 있다.

아키텍처를 위한 구현보다는, 아키텍처를 도입하는 가장 근본적인 목적을 되새기자.

 

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함