자바를 공부하며 객체지향을 제대로 공부하기 시작했고, 객체지향의 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 객체 내부만 손 봐주면 된다.
추상화가 참 좋긴 좋다.
추상화 정리
- 상수 정의
- 특정 행위를 함수로 정의
- 함수를 클래스로 래핑
- 인터페이스를 정의하여 클래스를 숨김
- 보변적인 객체를 특수한 객체로 래핑
이 외에도,
- 제네릭 타입 파라미터 이용
- 내부 클래스 추출
- 객체 생성을 제한(팩토리 함수 이용)
하지만,
극단적인 추상화는 필요 없이 코드의 양이 많아지게 되고,
코드를 이해하기 어렵게 만든다.
구체적인 작업에서는 추상화가 의미가 있지만, 큰 프로젝트의 경우는 모듈화를 잘 해야한다.
적당한 상한선에서 추상화를 진행하는 것으로 하자.
팀의 크기,
팀의 경험,
프로젝트의 크기,
도메인 지식
등의 균형을 잘 살펴가며 적절한 추상화 수준을 정의하고 이용하는 게 바람직한 추상화의 방법이다.
'📚 Kotlin' 카테고리의 다른 글
[Effective Kotlin] 아이템 36. 상속보다는 컴포지션을 사용하라 (0) | 2023.01.30 |
---|---|
[Effective Kotlin] 아이템 32. 생성자 대신 팩토리 함수를 사용하라 (2) | 2023.01.29 |
[Effective Kotlin] 아이템 24. 제네릭 타입과 variance 한정자를 활용하라 (1) | 2023.01.27 |
[Effective Kotlin] 아이템 23. 타입 파라미터의 섀도잉을 피하라 (0) | 2023.01.25 |
[감상문] Kotlin In Action을 읽고 (0) | 2023.01.17 |