레이블이 리팩토링인 게시물을 표시합니다. 모든 게시물 표시
레이블이 리팩토링인 게시물을 표시합니다. 모든 게시물 표시

2019년 1월 18일 금요일

[안드로이드] 어떻게하면 안드로이드 UI의 재사용성을 극대화할 수 있는가? - 5가지 흔한 실수들(번역)

어떻게하면 안드로이드 UI의 재사용성을 극대화할 수 있는가? - 5가지 흔한 실수들

지난 몇 달 동안, 저는 그루폰에서 사용했던 UI 중에 몇가지를 다시 재사용해야할 상황들이 있었습니다. 이 과정에서, 우리가 만든 UI의 장점이 뭔지, 가장 흔하게 했던 실수가 무엇인지 생각해보게 되었습니다.
UI를 좋게 만든다는 건 뭘까요? "좋다"는 것은 상대적이겠지만, 어느 소프트웨어에서나 적용될 수 있는 다음의 3가지 원칙들에 대해서는 대부분 동의할 것입니다.
  • 가독성(Redablitiy) : 좋은 UI 컴포넌트는 읽기 쉬워야합니다. 안드로이드 스튜디오의 디자인탭을 열지 않아도, xml 파일만 보고도 명확하게 알 수 있어야 합니다.
  • 테스트 가능(Testable) : 모든 UI 컴포넌트는 외부모듈에 의존 없이 쉽게 테스트할 수 있어야합니다.
  • 재사용 가능(Reusable) : UI 컴포넌트는 기본 동작의 변화 없이도 확장되거나 재사용될 수 있어야합니다.
이 글에서, UI의 재사용성에 영향을 끼치는 5가지의 흔한 실수들을 살펴볼 것입니다. 그리고 재사용 가능한 UI가 코드를 어떻게 더 읽기 쉽고 테스트하기 쉽도록 만들어 주는지도 살펴보겠습니다.

1. 커스텀 뷰에서 비즈니스 모델을 바로 사용하는 사례

때때로, 시간을 절약하고 중복된 코드를 피하기 위해서 네트워크 / DB에서 사용했던 모델을 뷰에서도 재사용하곤 합니다. 다음의 예제를 보겟습니다.
// Network 모델
data class UserModel(
  val id: Int, 
  val name: String, 
  val age: Int, 
  val address: String, 
  val createdAt: Date, 
  val updatedAt: Date)

// Network 모델을 바로 View에 전달
userApiClient.getUser(userId)
          .subscribe(
  { userModel -> userView.render(userModel) },
  { error -> Log.e("TAG", "Failed to find user", error) }
          )
이 예제에서, useApiClient 메서드를 이용해 백엔드로부터 사용자의 정보를 가져와서 UserModel 이라는 모델객체에 데이터를 저장합니다. 그러고는 이 모델을 바로 UserView에 전달하고 있습니다. (이 뷰에서는 유저의 name과 address만 필요로 합니다.) 얼핏 보기에, 이는 매우 쉽게 원하는 바를 달성한 것 같지만, 재사용성을 떨어뜨리며 장기적으로 봤을 때 심각한 문제를 야기할 수도 있습니다.

왜 이것이 나쁜가?

  • 백엔드용 모델을 뷰에 그대로 사용하면 안되는 가장 큰 이유는, 이 뷰를 앱의 다른 부분에서 재사용할 수 없기 때문입니다. 즉, UI를 렌더링하려면 해당 백엔드의 결과가 필요하다는 의미가 됩니다.
  • 백엔드용 모델은 일반적으로 UI에서 필요한 내용보다 더 많은 데이터를 가지고 있습니다. 추가적인 정보는 추가적인 복잡도를 의미합니다. 모델에 의도하지 않게 많은 정보가 포함되어 있기 때문에, UI에서도 부적절한 정보를 사용할 수도 있습니다. 그렇기에, 이 예제에서는 다음과 같이 UI용 모델을 간소화할 수 있습니다.
    data class UserUiModel(val name: String, val address: String))
    
  • 백엔드에서 변경이 일어난다면 어떻게될지 장담할 수 없습니다. 앱개발자들은 종종 앱을 지원하는 백엔드는 절대 변경되지 않을 것이라고 가정하지만, 이는 매우 잘못된 생각입니다. 백엔드는 언제든 변경될 것이고, 앱에서는 그에 대한 대비가 되어 있어야 합니다. 백엔드에서 무언가가 변경되었기 때문에, UI가 변경되어서는 안됩니다.

이걸 어떻게 고칠까?

이 문제를 고치는건 쉽지만, 추가적인 코드를 필요로 합니다. UI용 모델과 백엔드용 모델 -> UI용 모델 변환 메서드를 추가적으로 구현해주어야 합니다. 위의 예제에서는 UserModel을 UserUiModel로 변경하기 위한 변환 메서드를 다음과 같이 적용해 주어야 합니다.
userApiClient.getUser(userId)
          .map{ userModel -> convertToUiModel(userModel) }
          .subscribe(
  { userUiModel -> userView.render(userUiModel) },
  { error -> Log.e("TAG", "Failed to find user", error) }
          )
          
// 백엔드모델 -> UI모델 변환 메서드
fun convertToUiModel(userModel : UserModel) : UserUiModel {
    // Some Code
    return userUiModel
}
새 구현코드로 인해 모델 변환로직에 대한 추가적인 복잡도가 생기기는 했지만, 이 조금의 복잡도 덕분에 백엔드와 UI의 의존관계를 끊어버릴 수 있습니다. 새로운 구현패턴 덕분에, 백엔드에서 불필요한 데이터를 UI로 전달하지 않으며, 자유롭게 뷰를 재사용할 수 있게 되었습니다.

2.레이아웃 하나에 너무 몰려있는 사례(Monolithic View)

모든 엑티비티 UI가 하나의 문서에 포함된 레이아웃 xml파일을 본 적 있습니까? 불행하게도... 이는 안드로이드에서 매우 흔한 패턴이며, 많은 문제들을 야기합니다.

왜 이것이 나쁜가?

다음과 같은 이유로 위의 3가지 원칙에 위배됩니다.
  • 너무 많은 정보와 많은 레이어가 존재하기 때문에, 이러한 파일은 가독성이 떨어집니다.
  • xml의 부분들을 독립적으로 테스트할 수 없습니다. 우리는 에스프레소에서 실행되는 통합테스트의 부분으로 UI를 테스트하려는 경향이 있습니다. 통합테스트를 하는건 좋지만, UI와 비즈니스 로직이 함꼐 테스트되기 때문에 문제가 발생했을 때, 발견하기가 더 어려워집니다. 하나의 큰 레이아웃 xml파일을 여러개의 작은 조각으로 나누고, UI에서 비즈니스 로직을 제거한다면 UI만 테스트를 수행할 수 있습니다. 이러한 테스트는 통합테스트보다 더 세밀한 수준에서 UI의 문제들을 찾아낼 수 있습니다.
  • xml 부분들을 독립적으로 재사용할 수 없습니다. 그렇기때문에 재사용을 하려면 copy & paste를 할 수밖에 없습니다. 시간이 지날수록 중복된 컴포넌트들이 생겨서 앱의 일관성이 떨어지는 결과를 가져옵니다.

이걸 어떻게 고칠까?

모든 UI로직을 하나의 xml에 넣는 것은 마치 하나의 엑티비티에 모든 비즈니스 로직이 들어가있는 것과 같습니다. 비즈니스로직을 구성할 때 DAO, 모델, 백엔드 클라이언트를 구성하는 것과 동일한 방법으로 UI를 더 작게 쪼개야 합니다. 커스텀 뷰<include><merge>를 잘 활용하여 UI를 쪼개야 합니다. 하나에 몰려있는 xml 파일을 UI 컴포넌트가 재사용될때까지 쪼개지않고 기다린다면 심각한 이슈에 봉착할 수 있습니다. 정작 고쳐야할 때가 되었을 때, UI가 엑티비티 / 프레그먼트와 너무 강하게 결합되어 있어서 리팩토링하기 더 어려워지고 앱의 기능에까지 영향을 끼칠 것입니다.
다음은 실제 오픈소스 프로젝트에서 사용되는 레이아웃을 가져온 것입니다. 제가 프로퍼티들을 좀 지우고, 주석을 추가하여 레이아웃을 좀 더 읽기 쉽도록 수정했습니다.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ... >
  <android.support.design.widget.AppBarLayout ... >

    <!-- ToolBar -->
    <android.support.v7.widget.Toolbar ... >
      <RelativeLayout ...
        <TextView/>
        <TextView/>
      </RelativeLayout>
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <ScrollView ... >
    <FrameLayout ... >
      <LinearLayout ... >

        <!-- Header -->
        <LinearLayout ... >
          <TextView ... />
          <TextView ... />
        </LinearLayout>

        <!-- User Message -->
        <LinearLayout ... >
          <TextView ... />
        </LinearLayout>

        <!-- User Information -->
        <LinearLayout ... >
          <TextView ... />
        </LinearLayout>

        <!-- Option selector -->
        <LinearLayout ... >
          <TextView ... />
          <Spinner ... />
        </LinearLayout>
      </LinearLayout>

      <!-- progress overlay -->
      <FrameLayout ... >
        <ProgressBar ... />
      </FrameLayout>
    </FrameLayout>
  </ScrollView>
</android.support.design.widget.CoordinatorLayout>
프로퍼티들을 지우고 주석을 추가했는데도, xml 파일을 읽기가 쉽지는 않습니다. 각 컴포넌트들이 어디에 위치되어 있는지 이해하기 어렵도록 하는 중첩 레이아웃들이 여러개 있습니다. 주석 없이는 각 태그들이 어떻게 관계되어 있고, 무엇을 의미하는지 파악하기가 어렵습니다.
다음 예제는 위의 레이아웃을 커스텀 뷰를 사용하여 재구성한 것입니다.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ... >
  <android.support.design.widget.AppBarLayout ... >
    <CompanyToolbar ... />
  </android.support.design.widget.AppBarLayout>

  <ScrollView ... >
    <FrameLayout ... >
      <LinearLayout ... >
        
        <InformationHeader ... />

        <UserMessageView ... />

        <UserInformationView ... />
        
        <MultipleChoiceView ... />
      </LinearLayout>

      <ProgressOverlay ... >
    </FrameLayout>
  </ScrollView>
</android.support.design.widget.CoordinatorLayout>
각 기능별로 커스텀 뷰를 생성하여, 재사용할 수 있고 추후에 리팩토링할 때 사이드이펙드의 염려도 거의 없도록 변경했습니다. 클래스 명이 직관적이기 때문에 주석은 더이상 필요하지 않습니다. 만약 모든 엑티비티들에서 ProgressOverlay 컴포넌트를 사용하여 새로운 프로그레스 애니메이션을 구현해야 한다면 얼마나 쉬워질 지 상상해보세요.
그루폰에서 지향하는 작은 UI 컴포넌트 단위의 접근은 Atomic Design (저자 Brad Frost)로부터 영향을 받았습니다. 만약 당신이 UI에 특히 관심이 많다면, 이 책을 꼭 읽어보기를 추천합니다.

3.커스텀 뷰에 비즈니스 로직이 있는 사례

이전 사례에서, 우리는 커스텀 뷰를 구현했을 때 얻을 수 있는 장점에 대하여 이야기 했었습니다. 하지만, 커스텀 뷰를 주의하여 사용하지 않는다면 양날의 검과 같습니다. 로그를 추가하고, 의사결정 로직을 넣는 등.. 자칫 커스텀 뷰에 비즈니스 로직을 결합하여 사용하기가 쉽습니다. 이는 얼핏 보기에 코드를 잘 분류한 것 같지만, 사실은 그렇지 않습니다..

왜 이것이 나쁜가?

  • 뷰에 로직이 결합되어 있으면, 특정 사용사례를 벗어났을 경우, 재사용하기 어렵습니다. 뷰를 완전하게 재사용하려면, 뷰 자체는 단순하며, 비즈니스로직에 무관한 채로 남아있어야 합니다.정말 잘 구현된 뷰는 어떠한 의사결정 없이 상태값만을 전달받고, 그에 따라 렌더링 되어야 합니다.
  • 비즈니스 로직이 추가되면, 뷰를 테스트하기가 어려워집니다. 잘 구현된 뷰는 더 작은 UI 컴포넌트여야 합니다. 각 뷰들을 따로 분리하여 자동화된 테스트를 쉽게 수행할 수 있어야 합니다.

이걸 어떻게 고칠까?

뷰와 비즈니스 로직을 분리하는 데에는 많은 방법들이 존재합니다. 이 중 하나는, 주로 선호되는 아키텍쳐를 적용하는 것입니다. MVP를 이용한다면 Presenter에 비즈니스 로직을 모을 수도 있고, MVVM을 이용한다면 ViewModel에 비즈니스 로직을 모을 수도 있습니다.
그루폰에서는 이 문제를 해결하기 위해, 한 방향으로의 데이터 흐름만 존재하는 MVI패턴을 적용했습니다. MVI에서 비즈니스 로직은 Intent의 일부가 될 것이고, 이는 불변의 상태객체를 생산할 것이며, 이 상태를 이용해 뷰를 렌더링 할 것입니다.
만약 한 방향으로의 데이터 흐름에 관심이 있고, 재사용 가능한 UI 컴포넌트를 구현하고 싶다면, Pratyush Kshirsagar / Yichen WU 분이 쓴 Cross view communication using Grox 포스트를 꼭 읽어보기를 권합니다. 이 블로그에서 어떻게 한 방향의 데이터 흐름이 UI 구성에 도움이 될지 잘 설명해주고 있습니다.

4.과도한 최적화 사례

저는 아직까지 성능에 대해 언급하지 않았습니다. 어쩌면, 좋은 UI를 만드는 원칙에 왜 성능이라는 요소가 포함되지 않았을 지 의문인 분들도 있을 것입니다. 성능이 중요하다고 대체적으로 생각하겠지만, 가독성, 재사용성, 테스트 가능성이 복잡한 최적화코드보다도 훨씬 중요합니다. 우리가 성능이 훨씬 뛰어난 어셈블러 코드를 직접 쓰지 않고, 프로그래밍 언어를 사용하는 이유와 비슷합니다.
안드로이드에서는, Nested Layout / Double Taxation 이 UI 퍼포먼스에 영향을 끼치는 두가지 주요 원인으로 꼽고 있습니다. 이러한 이유 때문에, 중첩 레이아웃을 피하고 ConstraintLayout을 쓰도록 당부하는 포스트나 팟캐스트들이 끊임없이 쏟아져 나왔습니다. 물론 ConstraintLayout은 RelativeLayout보다도 강력하고, 내부적으로 Double Taxation 이슈도 발생하지 않는 멋진 도구이지만, 보통 성능 이슈들은 대부분 극단적인 상황일 때 주로 발생합니다.
ConstraintLayout에 대한 포스트나 팟캐스트들을 보면, UI를 구성할 때 대부분 하나의 ConstraintLayout만을 사용하여 구현하라고 제시하고 있습니다.

왜 이것이 나쁜가?

  • 하나의 ConstraintLayout만 사용하여 UI를 구성한다면, 이는 위에서 언급했던 하나의 레이아웃에 너무 몰려있는 사례가 될 것입니다. 이미 언급했다시피, 이는 가독성도 떨어지고, 재사용할 수 없으며, 테스트하기도 어려운 상황이 됩니다.
  • UI를 가장 중요한 것으로 다루지는 않고 있습니다. 우리는 모든 비즈니스 로직을 엑티비티나 프레그먼트 내부에 구현하도록 하지 않을 것입니다. 이는 UI를 하나의 xml 파일에 두지 않는 것과 같은 이유입니다.

이걸 어떻게 고칠까?

성능을 희생하는 코드는 항상 차선책이 되어야 합니다. 과도한 최적화를 방지하기 위해, 개발 프로세스의 일부로 성능 테스트를 추가할 필요가 있습니다. 이 테스트를 통해 성능상의 이슈가 있다는 것을 바로바로 확인하며, 문제를 해결할 방법이 없을 때만 Monolithic 뷰 구조를 도입해야 합니다. 로직과 너무 많이 바인딩되거나, 혹은 UI를 필요 이상으로 다시 그려서 UI의 성능 문제를 일으키는지의 빈도를 테스트 결과로 확인한다면 아마 깜짝 놀랄 것입니다.

5.코드리뷰에서 UI를 소홀히하는 사례

코드리뷰 하는건 매우 어렵고, 시간을 많이 소모합니다. 특히 xml 파일들은 이해하기에 쉽지는 않습니다. 이러한 이유들 떄문에, 코드리뷰시에 UI를 소홀히하고는 합니다.

왜 이것이 나쁜가?

  • UI는 유저에게 첫인상입니다. 일관되고 깔끔하고 잘 구조화된 UI는 유저들이 앱에 더 머무르게 합니다.
  • UI를 가장 중요한 것으로 다루지는 않고 있습니다. UI는 앱의 절반을 차지하고 있습니다. 비즈니스 로직이 중요하여 리뷰하는 만큼, xml과 뷰들도 리뷰하는데 시간을 투자해야 합니다.

이걸 어떻게 고칠까?

리뷰 프로세스를 개선하기 위한 몇가지 방법이 있습니다.
리뷰어로써
  • xml을 이해할 수 없다는 것을 인지해도 매우 괜찮습니다. 상황적인 이해가 충분하지 않거나, UI가 매우 복잡할 수 있습니다. 작성자에게 도움을 요청하세요.
  • 작성자에게 xml을 잘게 쪼개도록 요청하는걸 나쁘게 여기지 마세요. 분명 덩치가 큰 클래스나 메서드에도 똑같은 요청을 했을 것입니다.
  • UI에 대하여 코드리뷰를 시작하세요.
  • 머티리얼 디자인의 가이드라인과 친숙해지세요. 저는 항상 이런 확인을 합니다. "프레스 상태일 떄 리플효과가 필요한가요?", "우리 버튼에 그림자가 필요한가요?"
코드 작성자로써
  • 풀리퀘스트에 스크린샷을 추가하세요. 동료 리뷰어가 UI 코드를 파악하는데 더 수월해집니다.
  • 디자이너에게 현재 구현결과에 대해 리뷰를 요청하세요. 디자이너는 UI를 리뷰할 수 있는 가장 좋은 리뷰어 입니다. 가능하다면 개발주기의 일찍 리뷰를 요청하세요.
  • 하나의 xml 파일에 몰빵하지 마세요. 위에서도 수없이 언급했듯이 매우 좋지 않습니다. 작은 UI 컴포넌트가 리뷰하기에 더 쉽고 더 낫습니다.
  • UI 전용 풀리퀘스트를 생성하세요. UI 전용 풀리퀘스트에 UI 관련된 내용만 올려서 리뷰어가 온전히 UI 리뷰에만 집중할 수 있게 됩니다.

결론

재사용 가능한 UI 컴포넌트를 만드는건 어려운 일이 아니지만 원칙을 필요로 합니다. 이 원칙은 우리가 화면 관점에서의 생각을 버리고, 컴포넌트 자체와 컴포넌트간의 관계의 관점에서 생각하도록 요구합니다. UI를 재사용하면 새로운 기능들을 보다 빠르게 개발할 수 있습니다. 또한, 재사용되는 컴포넌트들은 대체적으로 버그를 수정했으며, 최초 개발할때 생각하지 못하는 여러 상황들에 대해 고려가 되어 있을 것입니다.
위에서 논의했던 팁들을 요약하자면 다음과 같습니다.
  • 비즈니스모델을 뷰에서 사용하는건 매우 좋지 않습니다. 항상 백엔드와 비즈니스로직으로부터 뷰를 분리하세요.
  • 하나의 xml에 모든 레이아웃을 몰아넣지 마세요. 이는 코드의 가독성을 떨어뜨리고, 테스트하기 어렵게 만들며, 앱의 일관성을 깨뜨리는 원인이 되기도 합니다.
  • 비즈니스 로직을 UI에 포함하지 마세요. 로깅, 의사결정 로직 등을 뷰에 포함하지 마세요. 뷰는 오로지 불변 상태값을 전달받고 그에 따라 렌더링 하는 역할만을 수행해야 합니다.
  • 코드품질을 희생하여 얻은 최적화는 항상 차선책이어야 합니다. 중첩 레이아웃을 피하기 위한 Monolithic 뷰(ex ConstraintLayout)구성은 대안이 없을 때에만 사용하는 것이 좋습니다.
  • UI 코드를 항상 최우선으로 대우하세요. 어느 기능이든지 UI개발을 가장 먼저 시작하고, 디자이너와 팀 동료들에게 가능한 빨리 독립적으로 UI 리뷰를 받을 것을 권장합니다.

2016년 11월 9일 수요일

[자바] Effective Java 4장 - 클래스와 인터페이스2

클래스와 인터페이스2

6. 추상클래스 대신 인터페이스를 사용한다.

  • 인터페이스의 필요성 : 자바는 다중상속이 불가능한 대신, 인터페이스를 이용하여 구현해야 할 메서드를 규정할 수 있다. 인터페이스는 다중 구현이 가능하다.
  • 인터페이스의 장점
    • 메인 자료형을 보조할 서브 자료형으로 이용가능하다.(상속관계에 상관없이 인터페이스를 추가할 수 있으므로)
    • 상속에 얽매이지 않는 자료형을 구현할 수 있도록 한다.
    • 추상클래스보다는 타입결합이 약하기 때문에 의존성 주입 관계에 적절하다.
  • 인터페이스 사용시 주의점
    • 인터페이스에서 정의되는 모든 접근권한은 public이기 때문에, API로써 한번 공개되면 수정이 거의 불가능하다.
  • 추상 골격 구현 : 제공하고자 하는 인터페이스와, 그 인터페이스의 핵심 기능을 미리 구현한 추상클래스를 제공하는 방법으로, 개발자는 그 추상클래스에 부족한 부분만 채움으로써 완전한 기능을 하는 클래스를 구현할 수 있다.
  • 자바 8버전부터는 인터페이스에 default 메서드를 이용하면 메서드 구현부를 제공할 수 있다.

7.인터페이스는 자료형을 정의할 때만 사용한다.

  • 인터페이스를 사용하는 목적 : 해당 클래스의 객체가 어떤 일을 수행하는지를 클라이언트에게 알리기 위한 목적
  • 잘못된 인터페이스 사용의 예
    • 상수 인터페이스 - 인터페이스 내부에 static final 필드만 존재하는 인터페이스

8.태그 달린 클래스 대신 클래스 상속을 활용한다.

  • 태그 달린 클래스? : 하나의 클래스가 2가지 이상의 기능으로 동작할 때, 어떤 기능으로 동작할지를 필드값으로 구분하는 클래스
  • 태그 달린 클래스의 단점
    • 여러 기능이 혼합되어 있어서 코드 가독성이 떨어진다.
    • 여러 기능을 위해 많은 필드가 존재하기 때문에, 한 객체가 자신의 기능에 불필요한 필드를 가지므로 메모리 낭비다.
    • 새로운 기능을 추가하려면 소스파일을 수정해야 한다.
  • 클래스 상속을 통해 기능단위로 클래스를 분리하면 태그 클래스의 모든 단점을 제거할 수 있다.

9.전략을 표현하고 싶을 때는 함수 객체를 사용한다.

  • 자바에서는 함수포인터의 역할을 객체가 대신한다.
    • 필요한 메서드를 정의한 인터페이스를 준비하고, 그 인터페이스를 구현한 객체를 전달
  • 전략 패턴 : 어떤 기능을 구현할 때, 중요한 메인 로직을 상황에 따라 변경할 수 있도록 함수(메서드)를 전달받아 그 함수에게 메인 로직을 위임하는 패턴
  • ex : 배열의 정렬방법을 compare라는 메서드에 위임
Arrays.sort(stringArray, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

10.중첩 클래스는 가능하면 static으로 선언한다.

  • 외부 클래스에 종속되는 중첩 클래스가 아니라면 static으로 선언해야 한다.
  • non-static 중첩 클래스는 외부 클래스의 참조를 유지해야 하기 때문에, 성능&공간 측면에서 static 중첩 클래스보다 안좋다.
  • ex 외부클래스 참조 예시
class A {
    class B {
        public void test() {
            // A.this 문법으로 외부 클래스의 객체를 참조
            A.this.print();
        }
    }
    
    void print() {
        System.out.println("class A");
    }
}

2016년 11월 4일 금요일

[자바] Effective Java 3장 - 모든 객체의 공통 메서드

모든 객체의 공통 메서드

1. equals를 재정의할 때는 일반 규약을 따른다.

  • equals() : 주어진 객체를 비교하여 그 객체가 같은지 여부를 반환하는 메서드
  • 가장 확실한 방법은 equals() 메서드를 재정의하지 않는 것이다. 이 경우, 그 클래스의 객체는 유일한 객체가 된다.
  • equals() 메서드를 안써도 되는 조건(하나라도 만족한다면 재정의할 필요 없다.)
    • 각각의 객체가 고유하다.
    • 클래스에 논리적 동일성 검사방법이 있든지 없든지 상관없다.
    • 상위 클래스의 equals()를 하위 클래스에서도 잘 사용할 수 있다.
  • equals()메서드를 재정의할 필요가 있는 조건
    • 논리적 동일성을 지원해야 할 때(ex 문자열객체 비교)
    • 상위 클래스의 equals()가 하위 클래스에 맞지 않을 때
  • 대부분의 자바 API 클래스는 equals() 메서드가 재정의되어 있다고 가정하고 구현되어 있다.
  • equals()메서드를 재정의하기 위해 점검해야 할 규칙
1. == 연산자를 사용하여 equals()의 인자가 자기 자신인지 검사한다.
2. instanceof 연산자를 이용해 인자의 자료형이 정확한지 검사한다.
3. 2에서 검사한 인자를 정확한 자료형으로 변환한 후에 검사를 진행한다.
4. 현재 객체와 비교하고자 하는 객체의 필드 각각이 일치하는지 검사한다.(중요)
5. equals() 메서드를 구현할 때, hashCode()도 재정의하여 구현한다.

2. equals를 재정의할 때는 반드시 hashCode도 재정의한다.

  • hashCode() : 객체를 구분하는 해쉬값을 반환하는 메서드
  • equals() 메서드가 같다고 판정한 두 객체의 hashCode() 값은 반드시 같아야 한다.
    • equals() 메서드를 구현하고, hashCode() 메서드를 구현하지 않으면 자바의 해쉬기반 API에서 오동작을 일으키게 된다.
  • 해쉬함수를 만들 때, equals() 메서드에서 비교를 위해 사용된 필드를 이용하도록 한다. 그래야 같은 필드값일 때 해쉬값이 똑같이 나오게 된다.
  • hashCode()메서드를 재정의하기 위한 규약
1. 프로그램이 실행중일 때, 같은 객체의 hashCode메서드를 여러 번 호출하는 경우 언제나 동일 결과가 나와야 한다.(종료되었다가 재시작될 때에는 다른 값이 나와도 된다.)
2. equals 메서드가 같다고 판단한 두 객체의 hashCode메서드 결과는 같아야 한다.
3. equals 메서드가 다르다고 판단한 두 객체의 hashCode메서드 결과는 같아도 되고 달라도 되지만, 다를경우 해시테이블의 성능이 향상된다는 것을 기억한다.

3. toString은 항상 재정의한다.

  • toString() : 주어진 객체를 문자열로 반환하는 메서드.
  • Object.toString() 메서드는 "클래스명@해시코드" 형태이므로 유용하지 않다.
  • toString()메서드는 제약,규약이 존재하지 않지만, 잘 정의한 메서드는 클래스를 좀 더 유용하게 이용하도록 도와준다.
    • 객체 내의 중요 정보(필드)를 모두 담아 반환
    • 문자열의 내용을 하눈에 파악할 수 있도록
    • toString()메서드에서 제공하는 정보를 파싱용으로 이용하면 안되므로, 제공하는 모든 정보는 프로그래밍적으로 가져올 수 있도록 제공한다.

4. clone을 재정의할 때는 신중하게 한다.

  • clone() : 객체를 복사하여 반환하는 메서드. Cloneable인터페이스를 구현하지 않으면 CloneNotSupportedException을 던진다.
  • clone()메서드를 재정의하기 위한 규약
1. final 이 아닌 클래스에서 재정의할 때는 반드시 super.clone()을 호출해 얻은 객체를 반환해야 한다.
2. clone()을 구현할 때 원래 객체를 손상시키지 않도록 하고, 복사본의 불변식도 제대로 만족시켜야 한다.
3. 객체복사를 굳이 제공해야 한다면, clone()메서드 보다는 복사생성자 or 팩터리 메서드를 제공하는 방안을 검토한다.

5. Comparable 구현을 고려한다.

  • compareTo() : equals() 메서드와 비슷하게, 두 객체를 비교하여 순서를 반환하는 메서드이다. 이 메서드는 Comparable 인터페이스 내부에 존재한다.
  • 시스템에서 제공하는 정렬, 검색 등의 편의성 메서드들은 내부적으로 모두 compareTo() 메서드를 이용한다. 그러므로, 객체간의 순서가 중요한 클래스는 가급적 구현하는 것이 좋다.
  • compareTo() 메서드를 재정의하기 위한 규약
1. 객체간 비교순서를 바꿔도 객체간의 대소관계는 그대로 유지되어야 한다.
2. compareTo를 이용한 동치 검사 결과는 일반적으로 equals 메서드 실행 결과와 같아야 한다.

2016년 11월 3일 목요일

[자바] Effective Java 2장 - 객체의 생성과 삭제

객체의 생성과 삭제

1.생성자 대신 Static Factory 메서드를 이용할 수 있는지 고려하라

  • Static Factory 메서드 : 생성자 대신 객체를 생성해주는 static 메서드.
  • ex
public class A {
 public static A getInstance() {
  return new A();
 }
}
  • 장점
    • 생성자와는 달리 메서드의 이름을 지정해 줄 수 있어서 코드의 가독성이 올라간다.
    • 생성자처럼 매번 새로운 객체를 생성할 필요는 없다. 동일한 객체의 요청이 잦은 경우에는 객체를 생성해두고 캐싱한 후에 필요할 때마다 캐싱된 객체를 반환한다.
    • 생성자와는 달리 return 타입이 자유로워서 유연한 코드의 작성이 가능하다.
  • 단점
    • 모든 생성자를 private으로 감추고 Static Factory 메서드만 지원할 경우 서브클래스를 생성할 수 없다.
    • Static Factory 메서드가 다른 일반 Static 메서드와 확연히 구분되지 않는다.

2.생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라

  • Static Factory 메서드, 생성자는 선택적 인자가 많은 상황에 적절한 대응이 어렵다.
  • 대안
    1. 점층적 생성자 패턴
      • 모든 선택적 인자에 대한 생성자를 준비하여 필요한 생성자를 호출하도록 하는 방법.
      • 인자가 많아지면 코드 가독성이 떨어진다.
    2. 자바빈 패턴
      • 인자없는 생성자로 객체를 생성한 뒤 setter 메서드로 각 인자를 넣는 방법.
      • 1회성 함수 호출로 객체 생성을 완료할 수 없으므로 객체 일관성이 일시적으로 깨진다.
      • 변경 불가능(immutable) 객체를 만들 수 없는 단점이 있다.
    3. Builder 패턴
      • 별도의 빌더 객체가 선택적 인자들을 받아서 객체를 생성하도록 하는 방법
// Builder 패턴 예시
public class A {
 private final int requiredField1;
 private final int requiredField2;
 private final int optionalField1;
 private final int optionalField2;
 private final int optionalField3;

 public static class Builder {
  // 필수 멤버
  private final int requiredField1;
  private final int requiredField2;
  // 선택 멤버
  private int optionalField1 = 0;
  private int optionalField2 = 0;
  private int optionalField3 = 0;

  public Builder(int required1, int required2) {
   this.requiredField1 = required1;
   this.requiredField2 = required2;
  }

  public Builder optionalField1(int optional1) {
   this.optionalField1 = optional1;
   return this;
  }
  public Builder optionalField2(int optional2) {
   this.optionalField2 = optional2;
   return this;
  }
  public Builder optionalField3(int optional3) {
   this.optionalField3 = optional3;
   return this;
  }

  public A build() {
   return new A(this);
  }
 }

 public A(Builder builder) {
  this.requiredField1 = builder.requiredField1;
  this.requiredField2 = builder.requiredField2;
  this.optionalField1 = builder.optionalField1;
  this.optionalField2 = builder.optionalField2;
  this.optionalField3 = builder.optionalField3;
 }
}

// 이용
A obj = new A.Builder(1, 2).optionalField1(3).optionalField2(4).optionalField3(5).build();
  • Builder 패턴 장점
    • 인자에 불변식을 적용할 수 있다.
    • 빌더 객체는 여러개의 가변인자를 받을 수 있다.
    • Static Factory 메서드나 생성자보다 유연하다.
  • Builder 패턴 단점
    • 객체를 생성하기 위해 빌더 객체를 생성하는 오버헤드가 있다.
    • 빌더 패턴은 많은 코드를 요구하기 때문에 인자가 충분히 많은 상황에서 사용해야 한다.

3.싱글턴 패턴을 사용할 때, private 생성자나 enum 자료형으로 설계하라

  • 싱글턴 패턴의 단점
    • 싱글턴 객체를 Mock으로 대체하기가 어렵기 때문에 테스트가 어렵다.
    • 생성자를 private으로 만들더라도 리플렉션 API를 이용하면 private 생성자를 호출할 수 있다.
    • 직렬화에 대한 처리를 해주어야 한다.
//Private으로 선언된 생성자 호출하는 예제
public class PrivateInvoker {
 public static void main(String[] args) throws Exception {
  //리플렉션 setAccessible 메서드를 통해 private으로 선언된 생성자 호출 권한을 획득
  Constructor<?> con = Private.class.getDeclaredConstructor()[0];
  con.setAccessible(true);
  Private p = (Private) con.newInstance();
 }
}

class Private {
 private Private() {
  System.out.println("이것은 private 생성자 입니다.");
 }
}
  • 싱글턴 패턴 구현방법 3가지
    1. private 생성자 + public static 필드
    2. private 생성자 + static factory 메서드
    3. enum 자료형(JDK 1.5부터 가능)
// 1.private 생성자 + public static 필드
public class SingletonClass {
 public static final SingletonClass INSTANCE = new SingletonClass();
 private SingletonClass() {
  ...
 }
 public void leaveTheBuilding() {
  ...
 }
}

// 2.private 생성자 + static factory 메서드
public class SingletonClass {
 private static final SingletonClass INSTANCE = new SingletonClass();
 public static SingletonClass getInstance() {
  return INSTANCE;
 }
 private SingletonClass() {
  ...
 }
 public void leaveTheBuilding() {
  ...
 }
}

// 3.enum 자료형
public enum SingletonEnum {
 INSTANCE;
 
 public void leaveTheBuilding() {
  ...
 }
}

4.객체 생성이 필요 없을 때는 private 생성자를 사용하라

  • static field들이 모여있는 클래스를 설계할 경우에는 객체를 생성할 필요가 없다.
  • 객체 생성을 못하게 하려면 private 생성자를 클래스에 명시적으로 선언해 준다.
  • 필요없는 생성자를 선언한 것이므로 Javadoc 등으로 주석처리를 해두면 좋다.
public class UtilityClass {
 // 기본 생성자가 자동 생성되지 않도록 하여 객체 생성 방지
 private UtilityClass() {
  throw new AssertionError();
 }
}

5.불필요한 객체는 만들지 말라

  • 기능적으로 동일한 객체는 필요할 때마다 만드는 것보다 캐싱하여 재사용하는 편이 낫다.
    • Static 초기화 블록을 이용할 것을 고려한다.
  • Wrapper 클래스의 Auto Boxing기능을 이용하면 객체가 추가로 생기게 된다.(자동 객체화)
    • 필요한 경우가 아니라면 자동객체화로 불필요한 객체가 생성되지 않도록 하자.
//Auto-Boxing
Integer intObject = 5;      //Integer객체가 생성됨.
  • 생성비용이 싼 객체라면 코드 가독성을 고려하여 Object Pool 사용을 자제한다.

6.유효기간이 지난 객체의 참조는 폐기하라

  • 자바의 메모리 누수 : 객체의 참조가 해제되지 않아서 가비지 컬렉터가 해당 객체를 수집하지 못하여 메모리에 남아있는 현상
    • 자바에서 한 객체가 누수될 경우, 그 객체뿐만 아니라 그 객체가 참조하고 있는 모든 객체가 함께 누수된다.
  • 해결방법
    1. 쓸 일 없는 객체의 참조는 무조건 null로 만든다.
    2. 해당 객체의 scope를 최대한 좁게 만들어서, 그 객체가 스스로 scope 밖으로 벗어나게 설계한다.
    3. WeakReference를 이용하거나, 내부적으로 WeakReference를 이용해 구현된 API를 이용한다.

7.finalize() 사용을 피하라

  • Java의 finalize는 C++의 destructor와 성격이 다르다.
    • C++ Destructor : 객체에 할당된 메모리를 해제하는 역할. 생성자의 정 반대
    • Java finalize : 객체가 GC에 의해 수집되기 직전에 마지막으로 호출될 메서드
  • finalize를 사용하면 안되는 이유
    1. 언제 실행될 지 예측할 수 없다. 심지어 실행되지 않을 수도 있다.
    2. 프로그램의 성능이 심각하게 저하된다.
  • finalize를 사용하기 적합한 곳
    1. 명시적 종료 메서드(close() 등) 호출을 잊을 경우를 대비할 경우
    2. 자바의 native 메서드를 통해 기능 수행을 위임하는 네이티브 객체의 경우
  • finalize를 사용해야할 상황이라면 상위 객체의 finalize를 반드시 호출해준다.