안녕하세요. 커뮤니케이션 앱 LINE의 모바 일 클라이언트를 개발하고 있는 Ishikawa입니다.
저희 회사는 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰고 있습니다. 이를 위해 다양한 노력을 하고 있는데요. 그중 하나가 Review Committee 활동입니다.
Review Committee에서는 머지된 코드를 다시 리뷰해 리뷰어와 작성자에게 피드백을 주고, 리뷰하면서 얻은 지식과 인사이트를 Weekly Report라는 이름으로 매주 공유하고 있습니다. 이 Weekly Report 중 일반적으로 널리 적용할 수 있는 주제를 골라 블로그에 코드 품질 개선 기법 시리즈를 연재하고 있습니다.
이번에 블로그로 공유할 Weekly Report의 제목은 '나쁜 열거가 좋은 계층을 몰아낸다'입니다.
나쁜 열거가 좋은 계층을 몰아낸다
어떤 서비스의 '사용자 계정 유형'으로 다음과 같은 열거형이 정의돼 있다고 가정해 보겠습니다.
enum class AccountType { FREE, PERSONAL, UNLIMITED }
로컬 저장소나 데이터베이스, 네트워크를 통한 API를 사용해 해당 값을 읽고 쓰는 경우 '컨버터'나 '매퍼'라는 메커니즘을 사용해 언어별 객체 및 인터페이스 정의 언어 또는 프로토콜에 정의된 바이트 열로 상호 변환할 수 있습니다. 예를 들어 Android 애플리케이션 개발에서는 Room이라는 지속성 라이브러리를 이용할 수 있으며, TypeConverter
라는 메커니즘을 사용해 컨버터를 쉽게 구현할 수 있습니다.
하지만 다음 TypeConverter
사용 예시에는 문제가 있습니다. 어떤 점이 문제일까요?
class AccountTypeConverter {
@TypeConverter
fun fromStringValue(typeString: String): AccountType = AccountType.valueOf(typeString)
@TypeConverter
fun toStringValue(type: AccountType): String = type.name
}
class AccountTypeConverter {
@TypeConverter
fun fromIntValue(typeInt: Int): AccountType = AccountType.entries[typeInt]
@TypeConverter
fun toIntValue(type: AccountType): Int = type.ordinal
}
부패 방지층 역할을 하는 컨버터
해당 코드의 문제점은 name
과 ordinal
이라는 열거형 속성을 사용했기 때문에 컨버터가 부패 방지층 역할을 하지 못한다는 것입니다. 이 때문에 외부(인터페이스 정의 언어나 프로토콜)의 변경 사항이 열거형을 사용하는 코드에 영향을 미치며, 그 반대로 영향이 전파되기도 합니다.
만약 데이터베이스나 원격 API에서 사용하는 값(이하, 외부에서 사용하는 값)이 변경되면 열거형 정의도 변경돼 열거형을 사용하는 코드에도 영향을 미칩니다. 반대로 열거형을 사용하는 측 사정으로 열거형에 다른 이름을 부여하거나 정의 순서를 바꾸고 싶어질 수도 있지만, 이름이나 순서를 변경하는 것은 외부에서 사용하는 값에 직접 영향을 미치기 때문에 마음대로 변경할 수 없습니다.
ordinal
을 사용하면 발생하는 구체적인 문제를 예로 들어보겠습니다. BUSINESS
라는 새로운 AccountType
을 추가하려고 한다고 가정해 봅시다. 가격 설정이나 기능 면에서 볼 때 BUSINESS
는 PERSONAL
과 UNLIMITED
의 중간으로 정의하는 것이 타당하다고 하겠습니다.
enum class AccountType { FREE, PERSONAL, BUSINESS, UNLIMITED }
그런데 위와 같이 변경하면 UNLIMITED.ordinal
의 값도 변경됩니다. 따라서 외부에서 사용하는 값도 업데이트해야 합니다.
name
을 사용할 때도 비슷한 문제가 발생합니다. 예를 들어 리브랜딩을 위해 FREE
, PERSONAL
, UNLIMITED
라는 열거형 이름을 BRONZE
, SILVER
, GOLD
로 변경하고 싶다고 가정해 봅시다. 만약 해당 변경을 그대로 진행한다면 이전에 외부에서 사용 중인 값이 깨져버립니다.
이와 같이 ordinal
과 name
을 사용할 때의 무서운 점은 열거형을 사용하는 측에서 편의를 위해 간단한 리팩토링만 진행해도 의도치 않게 버그가 발생한다는 것입니다. 열거형을 사용하는 입장에서는 언뜻 봐서는 해당 변경 사항이 외부에서 사용하는 값에 영향을 준다는 것을 파악하기 어려울 것입니다.
부패하지 않도록 랩을 씌우기
해당 문제를 해결하기 위해서는 외부에서 사용하는 값과 열거형 선언을 분리하는 것이 좋습니다. 아래와 같이 외부에서 사용하는 값을 열거형 속성으로 정의하는 방법이 있습니다. 이를 통해 AccountType
열거형의 이름이나 순서 변경으로부터 외부에서 사용하는 값을 보호할 수 있습니다.
enum class AccountType(val dbValue: String) {
FREE("free"),
PERSONAL("personal"),
UNLIMITED("unlimited");
companion object {
val DB_VALUE_TO_TYPE_MAP: Map<String, AccountType> =
entries.associateBy(AccountType::dbValue)
}
}
만약 AccountType
을 사용하는 코드에서 dbValue
를 숨겨야 하는 경우라면 컨버터 클래스를 별도로 정의하고 해당 클래스에서 dbValue
와 DB_VALUE_TO_TYPE_MAP
에 해당하는 함수 및 속성을 정의하는 게 좋습니다.
예외: 그 자리에서 먹으면 부패하지 않는다
열거형을 외부에서 사용하는 값으로 변환하지 않고 임시 변환으로만 사용하는 경우라면 name
이나 ordinal
을 사용해도 문제가 발생할 가능성이 적습니다. 예를 들어, 인 메모리 캐시로 열거형이 아닌 정수를 저장하는 경우 등을 생각해 볼 수 있습니다. 단, 이 경우에도 타입 안전성의 이점을 누리기 위해서라도 가능한 한 name
이나 ordinal
이 아닌 열거형 자체를 사용하시기 바랍니다.
한 줄 요약: 외부에서 정의된 값을 변환하는 코드에서는 내부 값과 외부 값을 서로 독립적으로 정의한다.
키워드:
value conversion
,externally defined value
,enum