본문 바로가기
📚 Kotlin

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

by GroovyArea 2023. 1. 29.

자바를 공부하며 객체지향을 제대로 공부하기 시작했고, 객체지향의 4대 특성 중 '추상화'의 참된 의미를 알기까지 시간이 걸렸다.
결론은 많이 공부하고 생각하며 확장하고 적용해 봐야 그 의미를 비로소 이해하리라 생각한다.

상수

const val NAME_PREFIX = "prefix"

fun getName() : String = NAME_PREFIX + this.name

가장 간단한 추상화 방법이다.
본 코드에서 String 리터럴 값을 따로 빼서 상수화를 시키면 이해도 쉬울 뿐 아니라. 변경하기도 훨씬 수월하다.

리터럴 값에 의미 있는 이름을 붙일 수 있고, 변경이 쉽다.

함수

특정 행위를 함수로 선언하면, 재사용이 가능하다.
코틀린에서는 간단히 확장 함수를 만들어서 여러 곳에서 재사용할 수 있어, 추상화를 엣지 있게 적용할 수 있다.

함수로 추상화를 하는 경우에는, 제한이 많다.
이를 테면, 함수의 네이밍과 파라미터의 네이밍도 신경 쓸 부분이다.
클라이언트에서 해당 함수를 사용할 때의 상황도 고려해야 한다는 의미이다.

fun getArabicaBean(function: (Bean) -> Bean) : Bean {
	//...
}

아라비카 원두를 가져오는 함수이다.
비교적 추상화 레벨이 낮은 방법이다.
원두의 이름이 바뀌었을 때는 함수를 하나 더 만들 수 있지만, 좀 더 높은 레벨의 추상화 함수로 이를 대체할 수 있다.

fun getBean(function: (Bean) -> Bean) : Bean {
	//...
}

함수의 이름을 추상화시켰다.

함수는 추상화의 한 방법이다. 함수의 시그니처는 어떤 추상화를 하는지 표현하는 것이므로, 의미 있는 네이밍이 중요하다.
하지만 특정 행위를 일컫으므로, 시그니처 자체를 변경하면 프로그램에 영향을 줄 수 있기에 제약이 있는 편이다.

클래스

강력한 추상화의 방법 클래스.
특정 행위를 실행하는 함수와 달리 상태를 가질 수 있고, 많은 함수를 캡슐화 할 수 있기에 더 강력하다.

클래스가 final 이라면 하위 타입에 어떤 구현체가 있는 지 알 수 있고,
open을 활용하면 서브 클래스를 대신 활용할 수 있다.

이처럼 함수보다는 추상화의 자유가 높지만, 아쉽게도 한계가 있다.

인터페이스

인터페이스는 가히 추상화 그 자체라 표현할 수 있다.
코드의 규격화, 클래스의 가시성 제한, 느슨한 결합 등등 이점이 넘쳐난다.

추상화된 인터페이스에 의존하는 클라이언트와 클라이언트가 알 필요가 없는 다양한 구현체를 구현하는 것은 참으로 신박하다.
즉, 클래스를 인터페이스 뒤에 숨긴다.

interface WorkOutRoutine {

	fun workOut(
    	part: Part,
        vararg sports: Sport
    )
}

class ChestWorkOutRoutine : WorkOutRoutine {

	override fun workOut(
    	part: Part,
        vararg sports: Sport
    ) {
    	// ...
    }
}

운동 루틴에 대한 인터페이스를 정의했다.
다양한 부위를 조질 수 있기에, 운동 부위 중 가슴 운동의 루틴을 정의하는 객체인 클래스를 만들고 인터페이스를 구현했다.

이렇게 하면 더 많은 자유를 얻을 수 있다.
가슴뿐 아니라 등, 하체, 어깨 운동 루틴에 대한 객체를 구현할 수도 있다.
확장에 아주 유연해지는 것이다.

또, 테스트 코드를 작성할 때 인터페이스 페이킹이 클래스 모킹보다 간단하므로 별도의 모킹 라이브러리를 사용하지 않아도 된다.
이 외에도 장점은 수도 없이 많다.

의미 있는 객체로 매핑하기

id를 컬럼으로 가지는 Entity 객체가 있다.
id는 int 로 정의하고 사용할 수도 있다.

class Person(
	val id : Int = 0
) 

// 외부 사용 시
val nextId = 0
val newId = nextId++

이렇게 하면 Thread Safe 하지가 않다.
id는 고유의 값인데, 멀티스레드 환경에서는 그 의미를 충족하기 어렵다.

이러한 문제점을 해결하기 위해 private으로 제한하고 함수를 사용할 수도 있지만, id의 타입이 변경될 경우 변화에 대처하기 어렵다.

의미있는 클래스로 정의해보자.

data class Id(
	private val id : Int
) 

private var nextId = 0

fun getNextId() : Id = Id(nextId++)

의미 있는 타입으로 매핑해줬다.
내부적으로 id가 어떠한 타입으로 변경되더라도 Id 객체 내부만 손 봐주면 된다.
추상화가 참 좋긴 좋다.

추상화 정리

  • 상수 정의
  • 특정 행위를 함수로 정의
  • 함수를 클래스로 래핑
  • 인터페이스를 정의하여 클래스를 숨김
  • 보변적인 객체를 특수한 객체로 래핑

이 외에도,

  • 제네릭 타입 파라미터 이용
  • 내부 클래스 추출
  • 객체 생성을 제한(팩토리 함수 이용)

하지만,

극단적인 추상화는 필요 없이 코드의 양이 많아지게 되고,
코드를 이해하기 어렵게 만든다.
구체적인 작업에서는 추상화가 의미가 있지만, 큰 프로젝트의 경우는 모듈화를 잘 해야한다.

적당한 상한선에서 추상화를 진행하는 것으로 하자.

팀의 크기,
팀의 경험,
프로젝트의 크기,
도메인 지식

등의 균형을 잘 살펴가며 적절한 추상화 수준을 정의하고 이용하는 게 바람직한 추상화의 방법이다.

반응형