SVC 패턴의 소개와 가이드

저는 2013년 9월부터 안드로이드 개발을 시작한 개발자입니다. 이제 만 5년을 향해가고 있고 쥬니어에서 탈피중(?) 입니다.

그때부터 포스트/블로그/디스코/오디오클립앱을 개발, 서스테이닝을 했고, 대략 20개 정도의 프로젝트 소스들을 보면서 왜 안드로이드 개발이 다 재각각이며 유지보수가 쉽지 않은가에 대해서 많은 고민을 했었습니다.

특히 몇년전부터 핫해진 MVP, MVVM 등을 접하면서, 써보니 단순한것 같긴한데, 막상 실무에서 써보면 오히려 코드가 복잡해지고, 유지보수가 어려워지는 느낌은 지울 수 없었습니다.

MVP, MVVM은 모두 심플한 스펙에서는 꽤 생산적이고, 유지보수도 어렵지 않은 패턴입니다. 그러나 둘다 모두 Activity, Fragment를 View로 삼고 관련 로직들을 Activity, Fragment 내부에서 처리하기 때문에, Presenter와 ViewModel로 일부 로직을 분리하더라도, 시간이 지나면 지날수록 Activity와 Fragment의 코드는 스파게티가 되기 쉽습니다.

그러면 왜 Activity와 Fragment의 코드는 가독성이 떨어지고 복잡해지는걸까요? 제가 분석한 바로는 A&F(Activty, Fragment)는 3가지의 역할을 하고 있기 때문입니다.

  1. Screen의 역할
  2. Views의 역할
  3. Control Tower의 역할

그러면 각 역할은 어떤 것인지 좀더 설명을 하겠습니다.

1. Screen의 역할

Screen(A&F 그리고 Dialog)은 각 화면을 어플리케이션 내에서 할당을 받은 객체입니다.
이 객체는 아래의 로직들을 가지게 됩니다.

  1. 최초에 화면 전환이 되는 로직 (activity의 경우 overridePendingTransition )
  2. 최초에 화면전환된 이후에 이전 화면에서 넘겨주는 기본 데이터 세팅로직 (Intent나, Bundle에서 초기 데이터를 가져오는 로직)
  3. 라이프 사이클(onCreate, onDestory 등)
  4. 강제 종료를 대비한 복구 로직 (onSavedInstanceState, onRestoreInstanceState)
  5. 다른 화면으로 이동하고, 정보를 보내고, 받는 로직 (startActivity,startActivityForResult, onActivityResult , 기타 Fragment를 replace하는 것도 Screen 객체에서 하길 권장합니다.)
  6. 안드로이드 프레임워크에서 제공하는 추가 메뉴 기능들 (onOptionsItemSelected ,onCreateOptionsMenu 등)

2. Views의 역할

Views는 각 Screen이 inflate하는 최상위 타겟 xml파일에 들어있는 모든 View들을 제어하고, 로직을 관리합니다.

Activity의 경우 setContentView(레이아웃 xml), Fragment나 DialogFragment의 경우 onCreateView에서 리턴되는 View의 xml 안에 들어있는 View들입니다.

크게보면, 해당 Screen을 위한 거대한 커스텀뷰 라고 볼 수 있습니다.

Views이 역할은 다음과 같습니다.

  1. 내부 뷰에게 데이터를 전달하고, 렌더링을 수행
  2. 유저가 뷰에게 인터렉션 하는 것을 Control Tower에게 알림.

3. Control Tower의 역할

Control Tower는 각 화면에서 일어나는 다양한 이벤트들을 받고, 어떻게 처리할지 결정하는 관제탑입니다. 다양한 이벤트들은 아래와 같습니다.

  1. 라이프 사이클 변화 라던지
  2. 유저 인터렉션의 발생
  3. 등록한 각종 센서의 이벤트 감지
  4. 네트워크 상태의 변화

스펙이 간단하여 코드가 500줄 정도가 된다면, CT에서 모두 구현하는게 구현속도나, 유지보수 측면에서 쉬울겁니다. 그러나 CT의 역할이 500줄을 넘어간다면 각 스펙을 {specName}Controller 등으로 클래스 추출하고, CT에서는 {specName}Controller.doSomething()을 호출하는 형태로 로직을 분리하면 좋습니다.

ControlTower의 역할은 위에 설명드린대로 아래와 같습니다.

  1. 각 스크린에서 발생할 수 있는 모든 이벤트를 감지한다.
  2. 발생한 이벤트에 맞게 필요한 조치를 한다.
    1) 각 {specName}controller들에게 명령
    2) View에 정보를 넘겨서 렌더링 및 View상태 변경
    3) Repository나 ViewModel을 통해 데이터를 가져오고 갱신을 함.
    4) 기타 등등

A&F는 그동안 고생이 많았다

Activity와 Fragment는 그동안 너무 많은 역할을 감당하고 있었습니다.

때문에 이 역할을 덜어줘야겠다는 생각을 했고, SVC를 통해서 각각의 역할을 분리하게 되었습니다.

*PS: SVC 이름에 Model이 빠진 이유는 A&F를 SVC로 분리하는 점이 이 패턴에서 가장 중요한 포인트이기 때문입니다. Model과 관련된 설명은 글 제일 하단 깃허브 readme에 다이어그램과 함께 보충설명을 했습니다.

Activity, Fragment를 SVC로 나누었을때 좋은점 7가지

  1. Activity와 Fragment 의 역할을 덜어 View 코드와 Screen코드가 섞이고, 거대해 지는 것을 막는다.
  2. 잘 분리된 로직을 통해서 로직 가독성을 높인다.
  3. Views 내부에서 작업할때 비지니스 로직은 전혀 신경 안써도 된다. (viewsAction만 생각하면됨)
  4. MVP에서 주로 중복작업하던 인터페이스 재생성 작업을 안해도 된다. (view, presenter 인터페이스들)
  5. MVVM에서 명령 수행을 위한 command 필드들을 만들 필요가 없다. (ViewModel은 데이터 관리용으로만 사용)
  6. “A Activity” , “B Fragment”에서 “C Views”를 동시에 사용이 가능하다. (하나의 xml로 재사용 가능)
  7. 명확한 네이밍을 통해 직관적이고 확실한 이해를 할 수 있다.

오픈소스 소개

어제 naver 공식 github에 svc 프로젝트와 , svc-template 프로젝트가 오픈소스로 공개되었습니다.

아직 지속적인 개발 단계라 0.0.2-alpha5버전이고, 올해 10월쯤 1.0.0 릴리즈 목표를 잡고 있습니다.

0.0.2-alpha5버전으로도 충분히 상용 앱을 만드는데는 문제가 없으실거에요. 편하게 가져다가 쓰시고, fork해서 변형해서 쓰셔도 되고, 쓰시면서 문제가 되는 점이나, 개선시키고 싶으신분들은 아래와 같이 참여 부탁드립니다.

  1. 이슈등록
  2. 또는 Pull Request

깃헙 주소는 이 글 끝에 공유합니다. (끝까지 봐주세요!)

디펜던시 적용 방법

  1. 프로젝트 최상위 build.gradle에 jcenter()를 포함시켜주세요.
allprojects {
repositories {
jcenter() //add this line
}
}

2. app build.gradle에 아래의 디펜던시를 추가시켜주세요

implementation "com.naver.android.svc:svc:0.0.2-alpha5"

3. 끝입니다.

코딩 가이드

1. Screen을 생성합니다.

1) Activity는 SvcActivity를 상속
 2) Fragment는 svcFragment를 상속
 3) DialogFragment는 svcDialogFragment를 상속
 4) views와 controlTower의 네이밍을 제네릭으로 미리 넣어줍니다.
 5) 생성자를 함수에 추가해줍니다.

class StatisticFragment : SvcFragment<StatisticViews, StatisticControlTower>() {
override fun createViews() = StatisticViews()
override fun createControlTower() = StatisticControlTower(this, views)
}

2. Views를 생성합니다.

1)유저 인터렉션이 없는 경우에는 Views를 상속
 2) 유저 인터렉션이 있다면 ActionViews를 상속
 3) Screen과 ViewsAction을 제네릭으로 선언
 4) inflate대상 xml 아이디를 파라미터로 명시합니다.
 5) 유저인터렉션은 viewsAction.doSomething(parameter, touchedPositionX) 등을 통해서 CT에게 알려줍니다.

class StatisticViews : ActionViews<StatisticFragment, StatisticViewsAction>() {
override val layoutResId = R.layout.fragment_statistic

override fun onCreated() {
screen.name.setOnClickListener {
viewsAction.onNameClicked()
}
}

fun setName(name: String) {
screen.name.text = name
}
}

3. ViewsAction를 생성합니다.

1) 유저가 발생시킬 수 있는 케이스를 미리 함수로 정의합니다.

interface StatisticViewsAction : ViewsAction {
fun onNameClicked()
}

4. ControlTower를 생성합니다.

1) 만들었던 Screen과 Views를 제네릭 선언, 생성자에 넣어줍니다.
 2) 발생하는 ViewsAction를 implement해줍니다.

class StatisticControlTower(screen: StatisticFragment, views: StatisticViews) : ControlTower<StatisticFragment, StatisticViews>(screen, views), StatisticViewsAction {
override fun onNameClicked() {
showToast("NameClicked")
}

override fun onCreated() {
views.setName("St")
}
}

5. 기본틀 완성!!

Template로 더 쉽게

  1. 아래의 깃을 clone
https://github.com/naver/svc-template.git

2. 해당 폴더의 install.sh를 command line으로 수행

3. 안드로이드 스튜디오 재시작.

4. new -> SVC -> 원하는 메뉴를 선택

5. 화면의 이름과 author입력 후 Finish 클릭.

6. 작업끝!

Github 공개!

오픈소스화를 회사 안에서 시작하는게 쉽진 않았지만, 긍정적으로 봐주시고 도와주신 많은 분들이 계셔서 시작할 수 있게 되었습니다.

특별히 오디오클립앱에서 이 패턴으로 작업할때 많은 도움을 주시고 ViewsAction 개념을 도입해주신 임원석님께 감사드리구요, 오픈소스를 “why not?” 이라며 허락해주신 우상훈님과, 팀에서 패턴 적용에 대해서 긍정적으로 검토해주시고, 오픈소스 오픈을 위해 커뮤니케이션을 도와준 장준영님, 안병원님께 감사하고, 또 패턴 구조 다이어그램화 및 문서화에 도움주신 정희주님께도 감사합니다. (뭔가 시상식 같은 느낌이네요)

마지막으로 집에서 각종 작업을 할때 옆에서 응원해준 와이프와 강우, 윤우에게 너무 고맙습니다. 말썽은 이제 적당히 부렸으면 좋겠네여 ㅋㅋ

정말 마지막으로 관심가져주신 분들과 이 글을 읽어주신 분들께 진심으로 감사드립니다!!!

더 자세한 설명은 아래 레파지토리에 (영문으로) 되어있고요.
꼭 한번 써봐주시면 감사하겠습니다. (Star도 눌러주시면 더 감사하겠습니다 ㅜㅜ 구걸구걸)

이제 시작이고 절반을 달렸네요. 앞으로 더 쉽고 견고하게 앱을 만들 수 있도록 프로젝트를 발전시켜 나가겠습니다.

고맙습니다.