[Effective Kotlin] 아이템 27. 변화로부터 코드를 보호하려면 추상화를 사용하라

Jimin Lim·2023년 8월 15일
0

Effective Kotlin

목록 보기
27/39
post-thumbnail

아이템 27

변화로부터 코드를 보호하려면 추상화를 사용하라

상수

//Before
fun isPasswordValid(text: String): Boolean {
    if (text.length < 7) return false
    // ...
}
//After
const val MIN_PASSWORD_LENGTH = 7

fun isPasswordValid(text: String): Boolean {
    if (text.length < MIN_PASSWORD_LENGTH) return false
    // ...
}

매직넘버를 상수로 추출하면

  1. 이름을 붙일 수 있다
  2. 전체에 퍼져있는 값을 수정하기 쉽다

함수

//Before
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
//After
fun Context.toast(
    message: String,
    duration: Int = Toast.LENGTH_SHORT
) {
    Toast.makeText(this, message, duration).show()
}

context.toast(message)

자주 쓰이는 부분을 확장 함수로 빼서 사용할 수 있다.

만약 toast가 아닌 snackbar로 출력해야 한다면 어떻게 하면 좋을까?

1) 함수 변경
위와 같이 snackbar를 이용해 확장함수로 만들고 기존의 Context.toast()Context.snackbar()로 일괄 변경한다. (이름을 직접 바꾸는 건 위험할 수 있다.)

2) 더 높은 레벨의 함수로 옮긴다.

fun Context.showMessage(
    message: String,
    duration: MessageLength = MessageLength.LONG
) {
    val toastDuration = when (duration) {
        MessageLength.SHORT -> Toast.LENGTH_SHORT
        MessageLength.LONG -> Toast.LENGTH_LONG
    }
    Toast.makeText(this, message, toastDuration).show()
}

enum class MessageLength { SHORT, LONG }

toast, snackbar 등 구체적인 명칭을 사용하지 않고 추상적인 함수명을 사용할 수 있다.

구현을 추상화할 수 있는 더 강력한 방법으로는 클래스가 있다.

클래스

enum class MessageLength { SHORT, LONG }

class MessageDisplay(val context: Context) {
    fun show(
        message: String,
        duration: MessageLength = MessageLength.SHORT
    ) {
        val toastDuration = when (duration) {
            MessageLength.SHORT -> Toast.LENGTH_SHORT
            MessageLength.LONG -> Toast.LENGTH_LONG
        }
        Toast.makeText(context, message, toastDuration).show()
    }
}

//사용
val messageDisplay = MessageDisplay(context)
massageDisplay.show("message")

클래스는 상태를 가지고 있어, 많은 함수를 가질 수 있다. context는 의존성 주입이 될 수 있으며, mock을 이용해 해당 클래스에 의존하는 다른 클래스의 기능을 테스트할 수 있다.

하지만 여전히 한계가 있으며, open을 사용해 서브클래스를 제공하거나 인터페이스를 사용하는 것이 더 자유를 제공할 수 있다.

인터페이스

인터페이스 뒤에 객체를 숨겨 실질적인 구현을 추상화하고 사용자가 추상화된 것에 의존하도록 하여 유연하게 사용할 수 있도록 한다.

enum class MessageLength { SHORT, LONG }

interface MessageDisplay {
    fun show(message: String, duration: MessageLength = MessageLength.SHORT)
}

class ToastDisplay(val context: Context): MessageDisplay {
    override fun show(message: String, duration: MessageLength) {
        val toastDuration = when (duration) {
            MessageLength.SHORT -> Toast.LENGTH_SHORT
            MessageLength.LONG -> Toast.LENGTH_LONG
        }
        Toast.makeText(context, message, toastDuration).show()
    }
}

ID 만들기(nextId)

var nextId: Int = 0

// 사용
val newId = nextId++

프로젝트에서 고유 id를 생성하는 코드를 작성할 때, 위와 같이 작성한다면

  1. 무조건 0부터 시작
  2. thread-safe 하지 않음

위와 같은 문제가 발생할 수 있다. 하지만 이 방법을 채택해야 한다면, 코드를 보호할 수 있게 함수와 클래스로 감싸는 것이 좋다.

data class Id(private val id: Int)

private var nextId: Int = 0
fun getNextId(): Id = Id(nextId++)

// 사용
val newId = getNextId()

추상화가 주는 자유

지금껏 내용을 정리하면 다음과 같다.

  1. 상수 추출
  2. 동작을 함수로 래핑
  3. 함수를 클래스로 래핑
  4. 인터페이스 뒤에 클래스 숨기기
  5. universal object를 specialistic object로 래핑

또한 이를 구현할 때 다음과 같은 도구를 활용할 수 있다.

  1. 제네릭 타입 파라미터 사용
  2. 내부 클래스를 추출
  3. 생성 제한 (팩토리 함수로만 객체 생성)

하지만 단점도 존재한다.

추상화의 문제

수많은 추상화를 적용해 코드를 복잡하게 만들 수 있다.
추상화를 잘 이해하려면 단위 테스트, 예제 등 잘 살펴보아야 한다....!

profile
💻 ☕️ 🏝 🍑 🍹 🏊‍♀️

0개의 댓글