반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item27. 변화로부터 코드를 보호하려면 추상화를 사용하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item27. 변화로부터 코드를 보호하려면 추상화를 사용하라

어리둥절범고래 2023. 11. 20. 22:52
반응형

추상화를 통해 변화로부터 코드를 보호하는 행위가 어떤 자유를 가져오는지 살펴보겠습니다.

 

 

상수

 

리터럴을 상수 프로퍼티로 변경하면 해당 값에 의미있는 이름을 붙일 수 있으며, 상수의 값을 변경해야 할 때 훨씬 쉽게 변경할 수 있습니다.

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

여기서 숫자 7은 사실 '비밀번호의 최소 길이'를 나타냅니다.

const val MIN_PASSWORD_LENGTH = 7

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

 

이처럼 상수로 빼낸다면 훨씬 쉽게 이해할 수 있으며, 변경하기도 쉽습니다.

 

 

함수

 

많이 사용되는 알고리즘은 다음과 같이 간단한 확장 함수로 만들어서 사용할 수 있습니다.

fun Context.toast(
    message: String,
    duration: Int = Toast.LENGTH_LONG
) {
   Toast.makeText(this, message, duration).show()
}

// usage
context.toast(message)
// activity
toast(message)

만약 토스트가 아니라 스낵바라는 다른 형태의 방식으로 출력하고 싶다면, 확장함수의 이름을 Context.toast()에서 Context.snack()로 바꾸고 내부 구현을 변경할 수도 있지만, 이런 해결 방법은 좋지 않습니다.

이때부터 중요한 것은 메시지의 출력 방법이 아니라, 사용자에게 메시지를 출력하고 싶다는 의도 자체입니다.

위 함수를 showMessage라는 높은 레벨의 함수로 옮겨보겠습니다.

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

enum class MessageLength { SHORT, LONG }

가장 큰 변화는 함수의 이름이며 개발자의 관점에서는 이름이 바뀌면 큰 변화가 일어난 것으로, 의미 있는 이름은 굉장히 중요합니다.

함수는 상태를 유지하지 않으며, 시그니처를 변경하면 프로그램 전체에 큰 영향을 줄 수 있습니다.

 

 

클래스

 

이전의 메시지 출력을 클래스로 추상화해 보겠습니다.

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

// usage
val messageDisplay = MessageDisplay(context)
messageDisplay.show("Message")

클래스는 상태를 가질 수 있으며, 많은 함수를 가질 수 있어서 강력합니다.

 

  • 의존성 주입 프레임워크를 사용하면 클래스의 생성을 위임할 수 있다.
@Inject lateinit var messageDisplay: MessageDisplay
  • mock 객체를 활용하여 테스트할 수 있다.
val messageDisplay: MessageDisplay = mock()
  • 더 다양한 종류의 메서드를 만들 수 있다.
messageDisplay.setChristmasMode(true)

 

 

인터페이스

 

코틀린 표준 라이브러리는 거의 모든 것이 인터페이스로 표현됩니다.

listOf 함수는 List를 리턴합니다. 여기서 List는 인터페이스이며, listOf는 팩토리 메서드라고 할 수 있습니다.

인터페이스로 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 이를 노출하면 사용자가 클래스를 직접 사용하지 못하게 됩니다.

인터페이스 뒤에 객체를 숨김으로써 실질적인 구현을 추상화하고, 사용자가 추상화된 것에만 의존하게 만들 수 있습니다.

즉, 결합(coupling)을 줄일 수 있는 것입니다.

interface MessageDisplay {
    fun show() {
        message: Int,
        duration: MessageLength = LONG
    }
}

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

또 다른 장점은 테스트할 때 인터페이스 페이킹(faking)이 클래스 모킹(mocking)보다 간단하므로, 별도의 모킹 라이브러리(mocking library)를 사용하지 않아도 된다는 것입니다.

 

 

ID 만들기(nextId)

 

프로젝트에서 고유 ID(unique ID)를 사용해야 하는 상황이라면, 보통 정수 값을 계속 증가 시키면서 이를 ID로 활용하는 방식을 사용합니다.

이는 스레드-안전하지 않으며, 변경으로부터 코드를 보호할 수 있게 함수를 사용하는 것이 좋습니다.

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

// usage
val newId = getNextId()

이렇게 구현하면 ID 변경으로부터는 보호되지만, ID 타입 변경에 대응하지 못합니다.

이후 ID 타입을 쉽게 변경할 수 있게 클래스를 사용하는 것이 좋습니다.

data class Id(private val id: Int)

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

 

 

추상화가 주는 자유

 

지금까지 설명한 추상화를 하는 방법들을 정리하면 다음과 같습니다.

 

  • 상수로 추출한다.
  • 동작을 함수로 래핑한다.
  • 함수를 클래스로 래핑한다.
  • 인터페이스 뒤에 클래스를 숨긴다.
  • 보편적인 객체를 특수한 객체로 래핑한다.

이를 구현할 때는 여러 도구를 활용할 수 있습니다.

 

  • 제네릭 타입 파라미터를 사용한다.
  • 내부 클래스를 추출한다.
  • 생성을 제한한다(팩토리 함수로만 객체를 생성할 수 있게 만드는 등).

 

 

추상화의 문제

 

추상화는 무한하게 사용할 수 있지만, 어느 순간부터 득보다 실이 많아집니다 (이를 풍자한 FizzBuszz Enterprise Edition이라는 프로젝트가 있을 정도).

추상화가 너무 많으면 코드를 이해하기 어려우며, 추상화를 이해하려면 예제를 살펴보는 것이 좋습니다.

요소를 사용하는 방법을 보여주는 단위 테스트와 문서의 예제는 추상화가 어떻게 사용되는지 확실하게 보여줍니다.

 

 

어떻게 균형을 맞춰야 할까?

 

그렇다면 어느 정도로 추상화를 해야 적절한 균형을 만들 수 있을지 몇 가지 규칙을 정리해 보면 다음과 같습니다.

 

  • 많은 개발자가 참여하는 프로잭트는 추상화를 사용하여 최대한 모듈과 부분(part)를 분리하는 것이 좋다(아래 이미지 헤이딜러 앱의 모듈구조 일부).

Ted Park  헤이딜러 안드로이드팀은 어떻게 일하나요? 참조.

  • 의존성 주입 프레임워크를 사용하면, 생성이 얼마나 복잡한지는 신경 쓰지 않아도 된다.
  • 테스트를 하거나, 다른 애플리케이션을 기반으로 새로운 애플리케이션을 만든다면 추상화를 사용하는 것이 좋다.
  • 프로젝트가 작고 실험적이라면, 추상화를 하지 않고 직접 변경해도 괜찮다.

정리

 

  • 추상화가 너무 많지도 적지도 않게 균형을 잘 유지하자.
  • 팀의 크기/경험, 프로젝트의 크기, feature set, 도메인 지식 등에 따라서 추상화 정도를 조절하자.
반응형