Skip to content
Minhyung Park

effective kotlin - Use abstraction to protect code against changes

kotlin, programming1 min read

TL;DR

  • 추후 코드 변화에 좀더 대응하기 쉬운, 변화에 대해 면역을 가지고 있는 코드를 작성하기 위해서 추상화를 사용하자
  • 하지만, 너무 높은 수준의 추상화는 코드를 이해하는데 어려움을 주게 된다
  • 적절한 수준의 추상화는 거의 아트의 경지이다 🎴

추상화의 방법

  • Extracting Constant
  • Wrapping behavior into a function
  • Wrapping function into a Class
  • Hiding a class behind an Interface
  • Wrapping universal objects into specialistic

Constant

상수값이 그대로 코드에 박혀서 반복적으로 사용된다면 문제가 될수 있습니다.

상수를 프로퍼티나 value로 빼는것의 이점

  • 이름을 붙여줘 이해를 돕는다
  • 추후의 값 변경에 도움을 준다
1// password validation 함수
2// before
3fun isPasswordValid(text: String): Boolean {
4 if(text.length < 7) return false
5 // ...
6}
7
8// after
9const val MIN_PASSWORD_LENGTH = 7
10
11fun isPasswordValid(text: String): Boolean {
12 if(text.length < MIN_PASSWORD_LENGTH) return false
13 // ...
14}

Function

다음으로 예시를 통해서 함수를 이용한 추상화를 알아보자.

유저들에게 토스트 메시지를 띄워주는 기능을 추가하려고 한다고 해보자

1// extenstion function을 이용한 한단계 추상화
2// before
3Toast.makeText(this, message, Toast.LENGTH_LONG).show
4
5// after
6fun Context.toast(
7 message: String,
8 duration: Int = Toast.LENGTH_LONG
9) {
10 Toast.makeText(this, message, duration).show()
11}
12
13context.toast(message)

common algorithm을 빼는 것은 여러 이점을 준다 (참고)

하지만 위의 코드에서 대비할수 없는 변화들이 있다. 예를 들어, toast message로 보여주던 부분들을 snackbar message 형태로 변경해야 한다고 하자.

가장 쉽게 생각할수 있는 대응은 내부구현을 snackbar를 보여주도록 변경하고 함수의 이름을 변경하는 것이다.

1fun Context.snackbar(
2 message: String,
3 duration: Int = Snackbar.LENGTH_LONG
4) {
5 // ...
6}

하지만, 이 같은 대응은 여러 문제들을 초래하게 됩니다. 그 중 가장 큰 문제는 사용하는 모든 쪽에서의 변경이 불가피하다는 것입니다. 따라서 이러한 문제를 해결하기 위해 한단계 높은 함수를 사용하여 추상화를 할 수 있습니다.

1fun Context.showMessage(
2 message: String,
3 duration: MessageLength = MessageLength.LONG
4) {
5 val toastDuration = when(duration) {
6 SHORT -> Toast.LENGTH_SHORT
7 LONG -> Toast.LENGTH_LONG
8 }
9 Toast.makeText(this, message, toastDuration).show()
10}
11
12enum class MessageLength { SHORT, LONG }

실제 구현부분은 숨겨둠으로서, 좀 더 변화에 강한 코드를 만들수 있습니다. snackbar 형태로 변경하려 한다면, 내부 구현을 변경하는 것 만으로 할 수 있습니다.

위에서 보여주는 가장 큰 변화는 네이밍입니다. 함수라는 것 자체는 추상화를 표현하는 것이기 때문에 이름은 굉장히 중요합니다. 따라서, 그에 맞는 이름을 붙여주는 것이 중요합니다.

하지만, 여기에서도 한계가 있습니다. 함수라는 것 자체는 상태를 가질수 없습니다. 따라서 더 높은 추상화를 위해서 class를 사용하는 법을 알아봅시다.

Class

1class MessageDisplay(val context: Context) {
2
3 fun show(
4 message: String,
5 duration: MessageLength = MessageLength.LONG
6 ) {
7 val toastDuration = when(duration) {
8 SHORT -> Toast.LENGTH_SHORT
9 LONG -> Toast.LENGTH_LONG
10 }
11 Toast.makeText(this, message, toastDuration).show()
12 }
13}
14
15// usage
16val messageDisplay = MessageDisplay(context)
17messageDisplay.show("message")

class는 function 보다 더 강력한 추상화를 표현할수 있는데 이에는 몇가지 이유가 있다.

  1. 상태를 가질수 있다.
  2. 여러 함수들을 노출시킬수 있다.

또한 class 추상화의 이점들도 있습니다.

  1. DI 프레임 워크를 사용한다면, 인스턴스 생성을 직접하지 않아도 된다.
  2. 다른 클래스의 기능 테스트를 위해서 클래스를 mock할수 있습니다.

하지만 클래스 추상화도 한계점이 있습니다. final class는 정확한 타입아래에 있기 때문에 정확한 구현을 알게 됩니다. 오픈 클래스의 경우에는 하위 클래스에 위임할수 있기때문에 좀더 유연하지만, 해당 클래스와 강하게 결합되어있습니다.

따라서 우리는 interface를 사용해 더 높은 추상화를 이룰수 있습니다.

interface

코틀린의 stdlib은 대부분 interface로 표현됩니다.

이러한 이유는

  1. 사용자가 클래스를 직접 사용하지 않으므로 인터페이스가 동일하게 유지되는 한 구현을 변경할수 있습니다.
  2. 인터페이스 뒤에 숨김으로서 실제 구현은 추상화되고 사용자는 이 추상화에만 의존한다
1interface MessageDisplay {
2 fun show(
3 message: String,
4 duration: MessageLength = MessageLength.LONG
5 )
6}
7
8class ToastDisplay(val context: Context): MessageDisplay {
9
10 override fun show(
11 message: String,
12 duration: MessageLength = MessageLength.LONG
13 ) {
14 val toastDuration = when(duration) {
15 SHORT -> Toast.LENGTH_SHORT
16 LONG -> Toast.LENGTH_LONG
17 }
18 Toast.makeText(this, message, toastDuration).show()
19 }
20}

인터페이스를 이용해서 더 많은 자유도를 얻었습니다.

결론적으로 사용과 선언의 결합도를 낮추게 되었습니다. 그로 인해, 좀더 변화에 유연한 구조를 가지게 되었습니다.

하지만, interface를 변경하게 되는 일이 생긴다면, 모든 implement 하고 있는 class들을 변경해야한다는 단점이 있습니다.