이펙티브 코틀린 Item 27: 변화로부터 코드를 보호하려면 추상화를 사용하라

woga·2023년 7월 30일
0

코틀린 공부

목록 보기
30/54
post-thumbnail

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

상수

리터럴은 아무것도 설명하지 않는다. 그래서 반복적으로 코드에 등장할 때 가독성 측면이나 유지보수 측면에서 자유롭지 못한다.

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
    // ...
}

위 코드처럼 상수로 빼내면 훨씬 쉽게 이해 가능하다

이렇게 되면 최소 길이를 변경하기도 쉽다 함수 내부 로직을 이해못해도 상수 값만 변경하면 된다.
만약 이런 숫자가 프로젝트 전체에서 참조해서 쓰고 있는데 상수로 빼지 않았다면 변경하기 힘들었을 것이다.
상수로 추출하면,

  • 이름을 붙일 수 있고
  • 나중에 해당 값을 쉽게 변경할 수 있다.

함수

만약 앱에 토스트 메세지를 자주 출력해야한다고 가정해보자

Toast.makeText(this, message, Toast.LENGTH_LONG).show()

이걸 간단한 확장 함수로 만들어서 쓸 수도 있다.

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

// use
context.toast(message)

// activity or sub of context
toast(message)

이렇게 하면 토스트를 출력하는 방법이 변경되도 확장 함수 부분만 수정하면 되므로 유지보수성이 향상된다
만약 토스트가 아니라 스낵바라는 다른 형태의 방식으로 출력해야 한다면 스낵바를 출력하는 확장 함수를 만들고 수정하면 된다.

하지만 이런 해결 방법은 좋지 않다. 내부적으로만 사용하더라도, 함수의 이름을 직접 바꾸는 것은 위험하다.

메세지의 출력 방법이 바뀔 수 있다는 것을 알고 있다면, 이 때부터 중요한 것은 메세지의 출력 방법이 아니라, 사용자에게 메세지를 출력하고 싶다는 의도 자체다 따라서 메세지를 출력하는 더 추상적인 방법이 필요하다.

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 -> Length.SHORT
            LONG -> Length.LONG
        }
        Toast.maekText(context, message, toastDuration).show()
 	}
 }

클래스가 함수보다 더 강력한 이유는 상태를 가질 수 있으며 많은 함수를 가질 수 있다는 점 때문이다. 또한 mock 객체를 활용해서 해당 클래스에 의존하는 다른 클래스의 기능을 테스트할 수 있따

이처럼 클래스는 많은 자유를 보장해준다. 여전히 한계가 있다. 더 많은 자유를 얻으려면 인터페이스를 쓰자!

인터페이스

코틀린 표준 라이브러리를 읽어보면 거의 모든 것이 인터페이스로 표현된다는 걸 확인할 수 있따

  • listOf 함수는 List를 리턴.
  • 컬렉션 처리 함수는 Iterable 또는 Collection의 확장 함수로서 List, Map, 등을 리턴(이또한 인터페이스)
  • 프로퍼티 위임은 ReadOnlyProperty or ReadWriteProperty 뒤에 숨겨진다. 이 또한 인터페이스
  • 함수 lazy는 Lazy 인터페이스를 리턴

라이브러리를 만드는 사람은 내부 클래스의 가시성을 제한하고 인터페이스를 통해 이를 노출하는 코드를 많이 사용한다.
라이브러리를 만드는사람은 인터페이스만 유지하다가 구현을 맘대로 변경할 수 있다.
즉, 인터페이스 뒤에 객체를 숨겨 실질적인 구현을 추상화하고 사용자가 추상화된 것에만 의존하게 만들어 결합을 줄일 수 있다.

ID 만들기

프로젝트에서 고유 ID를 정의해본다고 해보자
가장 간단한 법은 어떤 정수 값을 계속 증가시켜 ID로 활용하는 것이다.

var nextId: Int = 0

val newId = newxtId++

그런데 이런 코드가 사용되면 위험하다

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

//use
val newId = getNextId()

생성 방식의 변경으로부터는 보호되지만 (variable한 변수가 Private이 되어서) ID 타입변경 등은 대응하지 못한다. 만약 String으로 바뀐다면? 그래서 쉽게 변경할 수 있게 클래스를 사용하는 것이 좋다

data class Id(private val id: Int)

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

더 많은 추상화는 더 많은 자유를 주지만 이를 정의하고 사용하고 이해하는 것이 조금 어려워졌다

추상화가 주는 자유

[추상화를 하는 몇 가지 방법]

  • 상수로 추출한다

  • 동작을 함수로 래핑한다

  • 함수를 클래스로 래핑한다

  • 인터페이스 뒤에 클래스를 숨긴다

  • 보편적인 객체를 특수한 객체로 래핑한다

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

  • 제네릭 타입 파라미터를 사용한다

  • 내부 클래스를 추출한다

  • 생성을 제한한다 (팩토리 함수로만 객체 생성 가능 등)

다만 추상화는 자유를 주지만 코드를 이해하고 수정하기 어렵게 만듭니다.

추상화의 문제

코드 읽는 사람이 해당 개념을 배우고 잘 이해해야 한다.
물론 추상화의 가시성을 제한하거나 구체적인 작업에서만 추상화를 도입하는 것은 큰 문제가 없다. 크래서 큰 프로젝트에서는 잘 모듈화해야한다. 극단적으로 모든 것을 추상화해서는 안된다

이 극단적인 예시 중 하나가 FizzBuzz Enterprise Edition이라는 프로젝트다. FizzBuzz 같은 간단한 문제에 수많은 추상화를 적용해서 코드를 얼마나 복잡하게 만들 수 있는 가에 대한 풍자다.
보면 알겠지만 코드를 굉장히 이해하기 어렵다.

그러므로 요소를 사용하는 방법을 보여주는 단위 테스트와 문서의 예제만 있다면 확실하게 이해시킬 수 있다

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

추상화는 자유를 주지만 어떻게 코드가 돌아가는지 이해하기 어렵게 만든다 아래 조건들에 따라 달라질 수 있다

  • 팀의 크기

  • 팀의 경험

  • 프로젝트의 크기

  • 특징 세트

  • 도메인 지식

프로젝트 균형에 따라 다르다. 이 적절한 균형을 찾는 것은 거의 감각에 의존하는 예술과도 같다
그래도 사용할 수 있는 몇 가지 규칙을 정리해보자

  • 많은 개발자가 참여하는 프로젝트는 추상화 방법을 사용해보자 더불어 모듈과 part를 분리하는 것이 좋다
  • DI 프레임워크를 사용하자
  • 테스트를 하거나 다른 앱 기반으로 새로운 앱을 만든다면 추상화를 사용하자
  • 프로젝트가 작고 실험적이라면 추상화를 하지 않고도 직접 변경해도 괜찮다

항상 무언가 변화할 수 있다고 생각하는 것이 좋다. 이런 경험들을 토대로 어느 정도 감이 잡힌다.

정리

추상화는 단순하게 중복성을 제거해서 코드를 구성하기 위한 것은 아니다. 추상화를 사용하는 것은 코드를 변경해야 할 때 도움이 된다. 다만 추상적인 구조를 사용하면 결과를 이해하기 어렵기 때문에 장단점을 모두 이해하고 프로젝트 내에서 균형을 찾아야 한다. 추상화가 너무 많거나 적은 것은 좋지 않다.

profile
와니와니와니와니 당근당근

1개의 댓글

comment-user-thumbnail
2023년 7월 30일

정보 감사합니다.

답글 달기