안녕하세요. 커뮤니케이션 앱 LINE의 모바일 클라이언트를 개발하고 있는 Ishikawa입니다.
저희 회사는 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰고 있습니다. 이를 위해 다양한 노력을 하고 있는데요. 그중 하나가 Review Committee 활동입니다.
Review Committee에서는 머지된 코드를 다시 리뷰해 리뷰어와 작성자에게 피드백을 주고, 리뷰하면서 얻은 지식과 인사이트를 Weekly Report라는 이름으로 매주 공유하고 있습니다. 이 Weekly Report 중 일반적으로 널리 적용할 수 있는 주제를 골라 블로그에 코드 품질 개선 기법 시리즈를 연재하고 있습니다.
이번에 블로그로 공유할 Weekly Report의 제목은 '사상누각'입니다.
사상누각
다음 UserProfileViewData는 '사용자 프로필'을 표시하기 위한 UI 모델입니다.
class UserProfileViewData private constructor( val userName: String, val emailAddress: String, val profileImageUri: Uri?, val optionalStatusMessage: String? )이 인스턴스를 생성하기 위해 다음과 같은 빌더를 제공한다고 가정해 봅시다.
class UserProfileViewData private constructor( ... ) { class Builder { private var userName: String? = null private var emailAddress: String? = null private var profileImageUri: Uri? = null private var optionalStatusMessage: String? = null fun userName(value: String): Builder = also { userName = value } fun emailAddress(value: String): Builder = also { emailAddress = value } fun profileImageUri(value: Uri): Builder = also { profileImageUri = value } fun optionalStatusMessage(value: String): Builder = also { optionalStatusMessage = value } fun build(): UserProfileViewData { val nonNullUserName = checkNotNull(userName) val nonNullEmailAddress = checkNotNull(emailAddress) return UserProfileViewData(nonNullUserName, nonNullEmailAddress, profileImageUri, optionalStatusMessage) } } }여기서 also는 인수를 호출한 후 수신자를 반환하는 함수입니다. 즉 also { userName = value }는 value를 userName에 대입한 후 this를 반환합니다. 또한 checkNotNull은 인수가 null이면 IllegalStateException을 발생시키고, null이 아니라면 인수를 그대로 반환하는 함수입니다.
위 코드의 문제는 무엇일까요?
견고한 기초 위에 세우기
특별한 이유가 없다면 빌더 패턴 대신 생성자나 팩토리 함수를 사용하는 것이 더 바람직한 경우가 많습니다. 생성자나 팩토리 함수를 사용하면 꼭 전달해야 하는 인수를 누락해 발생하는 버그를 쉽게 방지할 수 있기 때문입니다.
위 예시의 경우 UserProfileViewData의 생성자를 그대로 사용하면 충분합니다. 앞서 빌더에서는 userName과 emailAddress가 필수였지만 이를 전달하지 않아도 컴파일 에러는 발생하지 않고 런타임 에러만 발생하는데요. 런타임 에러가 아니라 컴파일 에러로 결함을 검출할 수 있게 만들면 코드가 더 견고해집니다.
물론 라이브러리나 플랫폼 상황에 따라 어쩔 수 없이 빌더 패턴을 사용해야 하는 경우도 있으며(Object–relational mapper 등), 그 밖에도 다음과 같은 상황에서는 빌더 패턴이 필요할 수 있습니다.
- 필수 인수가 적고, 기본값이 존재하는 인수가 많은 경우
- '생성 중'인 상태를 다른 클래스나 함수에 전달해야 하는 경우
- 데코레이터 등에 대해 '마지막 작업'을 정의하고 싶은 경우
1번과 2번의 경우 다른 방법으로 대체할 수 있는 경우도 있는데요. 하나씩 살펴보겠습니다.
1: 기본값이 존재하는 인수가 많은 경우
만약 선택적 인수가 많다면 프로그래밍 언어에 따라 기본 인수를 사용할 수 있습니다. 또한 인수 제공 패턴이 제한적이라면 생성자를 오버로드하는 것도 한 가지 방법이 될 수 있습니다.
class UserProfileViewData( val userName: String, val emailAddress: String, val profileImageUri: Uri? = null, val optionalStatusMessage: String? = null )기본 인수를 사용할 수 없는 프로그래밍 언어에서는 빌더 패턴도 하나의 선택지입니다. 이 경우 필수 인수를 빌더 생성자로 전달하면 더욱 견고한 코드를 만들 수 있는데요. 다음 빌더에서는 필수 인수인 userName과 emailAddress를 빌더의 생성자 인수로 전달받습니다.
class Builder( private val userName: String, private val emailAddress: String ) { private var profileImageUri: Uri? = null private var optionalStatusMessage: String? = null ... fun build(): UserProfileViewData { return UserProfileViewData(userName, emailAddress, profileImageUri, optionalStatusMessage) } }2: '생성 중' 상태를 처리해야 하는 경우
아래와 같이 '생성 중'인 상태를 다른 함수 등에 전달하고 싶은 경우도 있습니다.
fun caller() { val builder = Builder() ... setStatusMessage(builder) val viewData = builder.build() ... } fun setStatusMessage(builder: Builder) { ... val statusMessage = ... builder.optionalStatusMessage(statusMessage) }그런데 위 코드의 setStatusMessage의 인수 builder는 출력 인