[android] Activity Scaffolding

개요top

안드로이드 개발의 가장 기초는 Activity를 만드는 것이겠죠. 습관적으로 만들어왔던 Activity에 대해서 좀 더 섬세하게 상황을 고려해봤습니다.
보다 안전하고 안드로이드 앱의 생명주기에 잘 맞출 수 있는 안정성을 확보하기 위해 Activity의 기초 뼈대를 새롭게 정의해보는 시간을 가져보죠.

생명주기의 엄청난 호출top

onCreate, onStart, onResume, onPause, onStop, onDestroy 등 주로 흐름에서 화면에 활성화되었다 안되었다 정도로 onResume, onPause가 발생하고 메모리가 부족해 OS가 임의로 내리면 onStop이 발생하는 정도라고 생각하기 쉽습니다만, 현실은 화면이 회전하면 onCreate부터 다시 호출되는 정도로 잦은 이벤트입니다.
게다가 실행모드설정에 따라 다르지만 많이 쓰는 singleTop인 경우 onNewIntent가 오히려 중심적인 콜백이 되기도 하죠.
이런 잦은 호출속에 api문서를 보면 더욱 가관인게 내부적인 인스턴스는 있던걸 반환할 수도 있고 완전히 재생성할 수도 있기 때문에 인메모리컨텍스트를 쓰지말라고 권고하고 있죠. 대표적으로 static에 잡아둔 객체같은 것이죠.

이에 대해 너무 방어적으로 작성하면 매번 너무 많은 일을 하는 식이 되어 앱이 느려지고 방만하게 대응하면 이런저런 경우에 앱이 크래쉬되버립니다. 적절한 방어를 하는게 아니라 생명주기를 정확히 이해하여 적재적소를 방어하는게 바른 해법이라고 할 수 있겠죠.

프로젝트에서의 경험과 다양한 책들의 내용을 포함하여 안정적인 회사의 스캐폴딩 구조를 만들어보고 있습니다.

onCreatetop

onCreate는 실은 잘 안일어나고 한번만 일어날것같은 인상입니다만 의외로 자주 호출되기 때문에 앱의 활성화에 관련된 초기화코드를 함부로 넣으면 다중 초기화가 일어나곤 합니다.
안드로이드의 생명주기에서 onCreate는 앱전체의 초기화를 하는 타이밍이라기보단 뷰를 리셋하는 타이밍에 가까운 것 같습니다. 따라서 여러 메모리객체나 데이터 초기화는 별도의 플래그를 통해 관리해야하는게 아닐까라는 생각을 자주합니다. 그 힌트는 역시 인자로 넘어오는 번들을 최대한 활용하는 것이겠죠.
우선 저희쪽 코드를 보시죠.

@Override
protected void onCreate(Bundle savedInstanceState){
  super.onCreate(savedInstanceState);

  // 1. 메인쓰레드에서의 예외를 통합처리 
  Thread.currentThread().setUncaughtExceptionHandler(
    new Thread.UnUncaughtExceptionHandler(){
      @Override
      public void uncaughtException(Thread thread, Throwable ex){
        //메인쓰레드의 예외처리
      }
    }
  );

  // 2. 객체컨텍스트적인 상태초기화 및 복원 
  if(savedInstanceState == null){
    //최초 초기화
  }else{
    //상태복원 및 부분초기화
  }

  // 3. 데이터를 바탕으로 뷰 초기화 
  setContentView(/*..*/);

  // 4. 뷰의 상세설정 
  onNewIntent(getIntent());
}
  1. 메인쓰레드예외처리 – 직접 생성한 쓰레드들이야 알아서 예외처리할 타이밍이 존재하지만 메인UI쓰레드는 onCreate시점에 currentThread로 캐치하는게 답인듯 싶습니다. 이 시점이라면 확실하게 현재 쓰레드는 메인UI쓰레드니까요. 앱크래쉬등 어려운 디버깅에 대한 힌트를 남기거나 일단 닥치고 리셋등의 작업들을 처리한 찬스를 갖게 됩니다.
  2. 데이터 초기화 – 앱이 돌아가기 위한 UI를 제외한 객체적인 초기화부분을 담당합니다. Bundle유무로 앱구동후 초기화가 되었는지 아닌지를 판별합니다. 위의 예에서는 null정도로만 검사했습니다만 else이후의 Bundle에 남긴 다양한 힌트를 바탕으로 부분상태 복원이나 재초기화 레벨을 정할 수 있게 됩니다. 우선 그림을 그리기 위해서는 바탕이 되는 데이터를 정리해주는 작업을 하는 부분입니다.
  3. 뷰생성 및 초기화 – 일단 레이아웃을 데이터로부터 적합하게 고르게 되면 뷰에 대한 설정은 하지 않고 초기화만 해주는 정도의 레벨입니다. 이벤트 리스너를 이 시점에 걸지 안걸지 등은 그게 고정적인 부분이라 1회성 초기화로 충분하다고 판단되면 이 시점에 해도 무방하고 각각 상황마다 다르다면 이 시점에 하지 않는게 좋다고 생각합니다.
  4. 뷰의 상세설정 – 뷰의 바탕을 만들었으면 거기에 상세하게 데이터를 공급하고 액션에 대한 상세한 설정을 하게 되는데 singleTop모드에서는 푸쉬나 노티에서 들어오는 경우 onCreate로 들어오지 않고 onNewIntent로 들어오는 경우가 생깁니다. 따라서 onCreate에서 안하고 onNewIntent시점으로 미루는거죠.

3, 4번의 분류는 그때그때 판단해야합니다만 푸쉬로 들어올때랑 앱이 런처로부터 실행될때 설정이 동일하지 않다면 당연하게도 onNewIntent쪽으로 보내는 식입니다.

onSaveInstanceStatetop

코드의 위치상 onCreate다음에 onSaveInstanceState를 배치하는 이유는 위의 데이터 초기화 타이밍에 Bundle에 기록된 내용을 사용하게 되는데 그 기록이 바로 여기서 이뤄지기 때문에 왔다갔다 안하고 참고하기 쉽게 해주기 위해서입니다.

@Override
protected void onSaveInstanceState(Bundle outState){
  super.onSaveInstanceState(outState);
  // 보존할 상태 저장
}

결국 얼마나 정성들여 번들관리하는가가 반대로 onCreate의 부하를 줄여주는가로 나타나는지라 앱의 고속화를 위해서는 상당히 신경써야하는 부분이 아닌가 싶습니다.

onNewIntenttop

singleTop모드에서 주인공은 역시 onNewIntent메소드입니다. 노티, 푸쉬 등에서 다양한 경로로 앱에 진입하면 앱이 최초 구동은 되지만 구동된 이후부터는 이쪽으로만 호출되기 때문입니다. 저희 회사는 납품하는 제품중 거의 80~90%가 singleTop인 경우였습니다. 해서 아예 스케폴딩을 singleTop기준으로 전개하게 되었습니다.
위에서 상기한대로 뷰에 대한 상세한 설정의 책임을 집니다. 아주 쉬운 예를 들어보죠. 만약 게시판앱이 있다면 그냥 런처에서 실행하면 리스트뷰가 뜨도록 되어있을 것입니다. 하지만 푸쉬알람에서 들어오면 상세뷰가 보여지는 식으로 뷰가 이미 전환되어있어야할 것입니다. 이 경우 onCreate는 리스트화면과 뷰화면만 준비하고 onNewIntent에서 인텐트가 런쳐인 경우는 리스트뷰를 활성화시키고 푸쉬로 온 경우는 해당 글의 상세뷰를 보이도록 조정해야할 것입니다. 바로 이런 식의 분리죠.

Intent에 대해서는 크게 getAction레벨에서 분리한뒤 일괄 데이터는 JSON으로 관리하는 편입니다. 자주 쓰는 Intent 템플릿을 포함하는 코드는 다음과 같습니다.

@Override
protected void onNewIntent( Intent intent ){
  super.onNewIntent(intent);
  if(intent.getAction().equals(Intent.ACTION_MAIN)){
    //런처에서 온 경우
  }else{
    //데이터는 일괄로 DATA라는 키에 JSON형식으로 관리함
    JSONObject data = new JSONObject(new JSONTokener(intent.getStringExtra(DATA));

    //액션레벨이하에서는 flag로 구분
    switch (intent.getFlags()){
    case GCM: //각 상수에 매칭되는 플래그별 처리
    case PUSH:
    }
  }
}

어디까지 action으로 처리하는가는 그때그때 다릅니다. 하지만 귀찮아서 많은 경우 flag로 구분하여 data로 정리합니다.
그 외에 scheme이나 category 등은 의외로 많이 안쓰게 되더라구요.

onResumetop

회사에서는 거의 onStart는 안건드리는걸 원칙으로 하고 있고 onNewIntent 이후의 작업은 onResume이 담당합니다.
만약 카메라앱이라면 카메라를 onPause시점에 해지하기 때문에 재활성화하는 작업을 반드시 onResume에서 해야합니다. 이건 onNewIntent에서 뷰레벨의 초기화하는 것과는 다른 종류죠. 비슷한 상황으로 오픈지엘을 쓸 경우 지엘컨텍스트에 대한 재평가와 렌더루프활성화도 이 시점에서 합니다. 오픈지엘이 아니라도 캔버스 쓰는 서페이스뷰등도 다 마찬가지 이슈들이 생겨서 onResume에 의외로 할 일이 많이 생기는 편입니다.

@Override
protected void onResume(){
  super.onResume();
  // 포그라운드 자원 재확보 및 캘리브레이션
}

onPausetop

onResume과 쌍으로 백그라운드 들어갈때 해지해야하는 화면이나 루프, 쓰레드, 통신금지 등의 처리가 상당히 있습니다. 사실 절전을 고려하면 onResume과 onPause쌍으로 처리할 일은 굉장히 많아집니다.
하지만 onPause에는 한가지 임무가 더 있는데, onPause이후가 onSaveInstanceState이므로 번들에 쓸 내용에 대해 정리를 미리할 내용이 있다면 여기서 1차적으로 정리해두는 것입니다. 특히 화면에 관련된 것들은 포그라운드 자원해제를 해버리면 값을 알 수 없게 되므로 그 전에 먼저 사전 데이터 작업을 해둘 필요가 있습니다.

@Override
protected void onPause(){
  super.onPause();
  // 번들 사전준비
  // 포그라운드 자원 해제
}

onDestroytop

이 시점의 가장 중요한 요소는 점유자원 정리입니다. DB연결자원이나 IO핸들등의 모니터 종류를 점검하여 꼼꼼하게 해지해주는 작업이 필요합니다.

@Override
protected void onDestroy(){
  super.onDestroy();
  // 점유 자원정리
}

결론top

위의 생명주기에 대한 템플릿은 꽤나 유용하게 써먹고 있으며 사실 저 스캐폴딩만으로도 많은 오류나 불안정성을 잡는데 도움을 받았습니다.
하지만 실제 실무에서는 저 생명주기를 다시 커버하는 라이브러리함수가 있어 2차적으로 한 번 더 래핑해서 쓰고 생명주기 간의 인메모리 상태 유지도 왠만하면 Activity에 필드를 두기보다는 라이브러리쪽에서 기억하게 만드는 편입니다.