R.plurals는 단순한 if문이 아니다

안드로이드 리소스 중 plurals는 단어의 단수복수형을 처리해주는 리소스 타입입니다.

활용할 수 있는 대표적인 예가 1 friend, 2 friends가 있지요.

리소스는 아래와 같이 작성합니다.

    <plurals name="profile_label_friendsCount">
        <item quantity="one">%d friend</item>
        <item quantity="other">%d friends</item>
    </plurals>

이 리소스를 활용할 자바코드는 아래와 같겠죠.

    if (friendCount > 0)
        message = getResources().getQuantityString(R.plurals.profile_label_friendsCount, friendCount, friendCount);
    textViewFriendCount.setText(message);

지원되는 quantity는 총 6개입니다 (zero, one, two, few, many, other).
여기까지만 보면, “아! 그럼 0일 때 no friend라고 나오게 하려면 아래처럼 하면 되겠군”이라고 생각합니다.

    <plurals name="profile_label_friendsCount">
        <item quantity="zero">no friend</item> <!-- 추가 -->
        <item quantity="one">%d friend</item>
        <item quantity="other">%d friends</item>
    </plurals>

하지만 en-US 기준으로 “no friend”가 아닌 “0 friends”라고 나오는걸 알 수 있습니다.

하지만 이것은 버그가 아니랍니다 (code.google.com issue링크). 애초에 그렇게 만들어졌다는거죠. plurals라는 리소스 처리는, 언어마다의 숫자에 대한 표현이 문법적으로 다를 경우만 처리하는 것이지 if문의 대용으로 사용되는게 아닌거죠. 이는 API문서에도 주의를 주고 있습니다.

In English, a string for zero will be ignored even if the quantity is 0, because 0 isn’t grammatically different from 2, or any other number except 1 (“zero books”, “one book”, “two books”, and so on). Conversely, in Korean only the other string will ever be used.

위에 글을 읽어보면 영어는 one만 다르게 판단된다는군요. 한국어는 other만 취급되고요.

한편으론, 이건 무슨 기준으로 이런걸까요? StackOverflow의 한 답변에 의하면 이는 Unicode Common Locale Data Repository (일명 CLDR)라는 체계를 따르는 겁니다. 논리의 개념이 아닌 인간의 언어에 대한 개념으로 만들어진 것이지요. 이것은 오픈소스로 운영되고 있으며 근래 모던한 소프트웨어는 이 체계를 많이 따르고 있습니다.

어쨌거나 결론은, “No Friend”처리를 해야 할 때 우리는 아래처럼 평범한 if 문으로 처리해야 합니다.

    if (friendCount > 0)
        message = getResources().getQuantityString(R.plurals.profile_label_friendsCount, friendCount, friendCount);
    else // 친구 없을 때
        message = getString(R.string.profile_label_noFriend); // "no friend" string 표시
    textViewFriendCount.setText(message);
Advertisements

Fragment에서의 ActionBar 조작은 OnCreateView에서 하자

이 글은 Vardhan님의 포스트를 요약한 것입니다.

우리는 보통 ActionBar를 조작할 때 Fragment.OnCreate에 코드를 넣습니다.
아래와 같이 말이죠.

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
                ActionBar actionBar = getActivity().getSupportActionBar();
                actionBar.setTitle("첫화면");
    }

하지만 Fragment와 엮이면서 ActionBar를 Fragment에서 조작할 때는, Fragment.OnCreate에서 조작할 때 Activity NullPointerException이 발생할 수 있습니다.

그 이유는 아주 복잡한 Activity-Fragment 라이프사이클과 관계 때문인데, 그 중 Activity가 destroy 되고 다시 살아날 때를 가정하면 다음의 흐름을 거칩니다.

생성

MainActivity onCreate
          Fragment onAttach
          Fragment onCreate
          Fragment onCreateView
          Fragment onViewCreated
          Fragment onActivityCreated
MainActivity onStart
          Fragment onStart
MainActivity onResume
          Fragment onResume

파괴

          Fragment onStop
MainActivity onStop
          Fragment onDestroyView
          Fragment onDestroy
          Fragment onDetach
MainActivity onDestroy

복원

          Fragment onAttach
          Fragment onCreate // 문제 발생지점
MainActivity onCreate
          Fragment onCreateView
          Fragment onViewCreated
          Fragment onActivityCreated
MainActivity onStart
          Fragment onStart
MainActivity onResume
          Fragment onResume

문제는 마지막의 “복원” 과정에서 발생합니다. Fragment.onCreate를 호출했지만, ActionBar는 Activity.onCreate를 지나야 활성화가 되기 때문에 Null인 상태인겁니다.

참고로, 복원 과정을 쉽게 테스트하려면 개발자 옵션에 “액티비티 유지 안함” 옵션을 켠 후 앱을 구동 → 홈버튼을 눌렀다가 → 다시 앱을 켜면 복원 과정을 재현할 수 있습니다.

ASP.NET에서 hang이 걸릴 때 처음 파악해야 할 점

얼마전 우리는 Azure의 ASP.NET 서버(WebRole, WebSites 모두)에서 어느 순간 엄청난 랙이 발생하는 것을 경험했습니다. 서비스가 올라간 Azure WebSites는 허구헌날 죽었습니다. 게다가 여러 APM, 감지툴을 써도 그 원인을 모호하게 알려줘서 판단하기 어려웠습니다.

뭐라고 툴에서 나왔냐면 _DynamicModule_Microsoft.Owin.Host.SystemWeb.OwinHttpContext에서 ExecuteRequestHandler의 OnPreExecuteRequest state에 문제가 있다는 것입니다.

그런데 PreExecuteRequest는 OWIN 처리의 아주 앞부분에 해당하는 것으로 사실 개발자 입장에서 딱히 커스텀할 여지가 없는 부분입니다. OWIN의 Startup.cs파일에 app.Use(…); 부분이지요. 실행 중인 VM에 프로파일러를 통해 스레드를 덤프해보기도 했지만 딱히 메모리 누수나 hot spot은 안보였습니다. 문제를 해결하는 동안은 여러 인스턴스를 돌리면서 ACK가 느려진다는 것이 감지되면 IIS를 리스타트 하는 방법으로 버텼습니다.

원인은, ORM(Object-relational mapper)에서 detach를 안한 콘트롤러가 하나 있어서 그곳에 많은 활동을 한 유저가 요청하면 그에 대한 모~든 연결된 정보를 다 가져오려고 했기 때문입니다. ORM은 DB스키마를 클래스 오브젝트로 매핑해준다는 기본 기능 뒤에서 입출력시 object graph를 DB스키마와 맞추는 attach, 데이터를 가져올 때 오브젝트에 매핑하고 변경 사항 트래킹을 끊는 detach 과정이 있습니다. (보다 자세히는 Context라는 개념도 있지만 논외로 합니다)

attach/detach를 개발자가 의도적으로 사용할 경우 attach를 실패하는건 주로 UPSERT 과정인지라 마치 SQL INSERT문을 잘못 쓰는 것처럼 에러가 나며, detach를 안하면 어디까지 가져와야 하는지 몰라서 가능한한 많은 연관정보를 가져오게 됩니다. (물론 ORM 제조사, 설정따라 다를 수 있습니다) 중요한건, 제가 겪은 detach에 대한 문제는 테스트 과정에서는 안나왔다는 것입니다. 왜냐하면 테스트용 DB는 그렇게 레코드가 많지 않았기 때문입니다.

저희의 경우, 네이버 지식인 같은 서비스를 가정할 때 6000개의 답변을 한 유저의 경우 6000개 답변+6000개 질문+수천개 다른 답변+각 질문당 2개 이상의 태그+이미지 한 장 이상+댓글+수백명의친구+그 친구들의 질문+그 친구들의 답변+그 모든 사람의 프로필….=수백만개의 레코드를 줄줄줄 다 긁어불러와서 메모리로 올리려고 했던 것이지요. 반면 활동량이 적은 (대부분의) 유저들이 문제가 있는 콘트롤러에 요청할 때는 그래봤자 수백건 뿐일테니 문제가 거의 일어나지 않아서 어느 조건에서 나는지도 파악하기에 모호했습니다. Azure WebSites가 허구헌날 죽었던 이유는 작은 크기로 램이 금새 가득차서 자체적으로 리사이클을 실행했기 때문입니다.

이런 문제 해결에 가장 먼저 해야 할 것은 LeanSentry 블로그 글의 도움을 받았는데, 현재 IIS의 request queue에 작업중인 요청 목록을 보는 것입니다. 이에 비슷한 글은 IIS 공식홈에서도 볼 수 있습니다.

아래는 문제가 발생할 당시의 스크린샷입니다.

캡처2

이 창을 보려면 서버의 IIS관리자를 열고 해당 어플리케이션을 좌측 트리에서 클릭한 후 Worker Process라는 아이콘을 클릭합니다. 접속한 IP (모자이크 처리), 요청한 콘트롤러 주소, 현재 처리중인 state, 끝으로 각 요청 별 소요시간을 보여줍니다.

역시, 스크린샷을 보는 것처럼 유사해보이는 요청 몇개가 어마어마한 Time Elapsed 값을 가지고 있습니다. 그리고 APM툴들이 말하는 문제 부분을 동일하게 보여주고 있습니다. APM은 여기 써있는 것을 전달해주는 것이었겠죠. 우리는 이것을 보자마자 해당 콘트롤러를 가서 문제를 쉽게 해결할 수 있었습니다. 참고로 Time Elapsed 단위가 밀리세컨드로 알고 있는데 새로고침(화면에 Show All 버튼)을 하면 보통은 아무리 길어도 개당 200ms이하로 잠깐씩 보였다 사라집니다.

이 문제 해결과정에서 의아한건, DB를 가져오는 ORM 코드는 WebApi Controller의 거의 마지막 지점에 있는데 State 표시는 그보다 훨씬 앞 지점에 있다는 것입니다. IIS의 매커니즘을 잘은 모르지만 이러한 IIS의 정보를 그대로 보고하는 APM 툴을 문제 해결에 맹신하면 안된다는 교훈을 줬습니다.

더불어 Azure 웹서비스 운용에 한가지 팁이 생겼는데, Azure WebRole을 메인으로 개발하다가 안정화가 되면 Azure WebSites로 이전해서 운용하는 것을 추천합니다. 이유는 WebRole은 배포가 20분 가량으로 매우 느리지만 디버깅하기 좋으며 WebSites는 경쾌하지만 개발자에게는 투명하지 않고 자유도도 낮기 때문입니다.

저희는 WebSites 2 instance + WebRole 2 instance를 Traffic Manager(RoundRobin 전략)로 엮어서 사용하고 있습니다. WebRole을 적어도 하나 이상은 써야 맘이 놓이는게, 실제로 성능이 더 안정적이고 더 빠른 리스폰스가 오는 것을 확인했기 때문입니다.