추상화
추상화(abstraction)는 프로그래밍 세계에서 중요한 개념 중 하나입니다. 컴퓨터 과학에서 추상화(abstraction)는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말합니다.
즉, 추상화는 복잡성을 숨기기 위해 사용되는 단순한 형식을 의미하며, 대표적으로 사용되는 추상화로는 인터페이스가 있습니다.
- 인터페이스는 클래스라는 복잡한 것에서 메서드와 프로퍼티만 추출해서 간단하게 만들었으므로 클래스의 추상화라고 할 수 있습니다.
어떤 대상(객체)에 대한 추상화는 여러 가지가 나올 수 있습니다.
위 사진들은 집을 추상화한 예시 두가지이며, 중점을 두는 대상이 다릅니다.
추상화를 하려면 객체에서 무엇을 감추고 무엇을 노출해야 하는지를 결정해야 합니다.
프로그래밍에서의 추상화
프로그래밍에서 하는 모든 일은 추상화입니다.
예를 들어, 사람이 숫자를 입력하면 컴퓨터 내부적으로 0과 1이라는 복잡한 형식으로 표현하는 것도 추상화입니다.
추상화를 설계한다는 것은 단순히 모듈 또는 라이브러리로 분리한다는 의미는 아니며, 함수를 정의할 때 그 구현을 함수 시그니처 뒤에 숨기는 것이 추상화 중 하나입니다.
추상화와 자동차
자동차 내부에는 엔진, 서스펜션 등 여러 요소들이 존재하여 자동차가 움직일 수 있게 만들어줍니다. 운전을 할 때 이러한 요소들에 대해 실시간으로 이해하고 조정해야 한다면 운전은 굉장히 어려울 것입니다. 운전자는 자동차 종류와 상관 없이 자동차를 조종하는 인터페이스(핸들, 페달 등)를 사용하는 방법만 알면 됩니다.
자동차는 굉장히 잘 만들어진 인터페이스로, 내부적으로 복잡한 요소들이 많아도 쉽게 사용할 수 있도록 해줍니다. 추상화는 내부적으로 일어나는 모든 일을 마법처럼 숨겨줍니다.
프로그래밍에서는 아래와 같은 목적으로 추상화를 사용합니다.
- 복잡성을 숨기기 위해(3장 재사용성)
- 코드를 체계화하기 위해(아이템 26)
- 만드는 사람에게 변화의 자유를 주기 위해(아이템 27)
아이템 26 - 함수 내부의 추상화 레벨을 통일하라
컴퓨터는 복잡한 장치입니다. 하지만, 이러한 복잡함이 여러 계층에 다양한 요소로 분할되어 있어 쉽게 사용할 수 있습니다.
계층이 잘 분리되면 어떤 점이 좋을까요?
특정 계층에서 작업할 때 그 아래 계층은 이미 완성되어 있으므로, 해당 계층만 생각하면 된다는 점이 장점입니다.
예를 들어, 어셈블리 언어, JVM 바이트 코드를 몰라도 프로그래밍할 수 있습니다.
추상화 레벨
프로그래밍에서는 일반적으로 추상화 레벨이 높을 수록, 프로세서로부터 멀어진다고 표현합니다. 높은 레벨일수록 걱정해야 하는 세부적인 내용이 적어 단순함을 얻을 수 있지만, 제어력(control)을 잃습니다.
- 예를 들어, C언어는 메모리 관리를 직접할 수 있어 다루기 까다롭지만, 자바는 가비지 컬렉터가 자동으로 메모리를 관리해 편리합니다. 다만 자바는 메모리 사용 최적화가 어렵습니다.
추상화 레벨 통일 원칙(Single Level of Abstraction Principle)
컴퓨터 계층과 같이, 코드도 추상화를 계층처럼 만들어 사용할 수 있습니다. 함수는 코드를 추상화할 때 사용하는 기본적인 도구입니다.
컴퓨터 계층에서 레벨을 확실하게 구분해서 사용해야 하는 것처럼, 함수도 레벨을 잘 구분해서 사용해야 하며 이를 추상화 레벨 통일(Single Level of Abstraction, SLA) 원칙이라고 합니다.
단일 함수에 모든 행동을 다 기술하게 되면, 해당 함수가 하는 행위의 세부적인 내용을 하나하나 신경써야 하므로 읽고 이해하는 것이 어렵습니다. 그래서, 요즘은 아래와 같이 함수를 계층처럼 나누어 사용합니다.
class CoffeeMachine() {
fun makeCoffee() {
// 기존에는 함수로 빼낸 아래 항목들을 makeCoffee 함수에 모두 작성함
boilWater()
brewCoffee()
pourCoffee()
pourMilk()
}
private fun boilWater() {}
private fun brewCoffee() {}
private fun pourCoffee() {}
private fun pourMilk() {}
}
- 위처럼 함수를 계층처럼 나누어 사용하면, 읽고 이해하기 쉬우며 낮은 레벨(boilWater, brewCoffee 등)에 대해 이해가 필요할 경우, 해당 함수만 따로 살펴볼 수 있습니다.
- 또한, 함수 추출 시 재사용과 테스트가 쉬워집니다.
함수는 간단해야 하며, 최소한의 책임을 가져야 합니다. 만약, 어떤 함수가 다른 함수보다 복잡하다면, 일부 부분을 추출해서 추상화하는 것이 좋습니다.
어플리케이션을 만들 때는 입력과 출력을 나타내는 모듈(백엔드의 HTTP 요청 처리 등)은 낮은 레벨의 모듈이며, 비즈니스 로직을 나타내는 부분이 높은 레벨의 모듈입니다.
아이템 27 - 변화로부터 코드를 보호하려면 추상화를 사용하라
물 위를 걷는 것과 명세서로 소프트웨어를 개발하는 것은 쉽다. 둘 다 동결되어 있다면... - Edward V. Berard
상수(constant value)
리터럴은 아무것도 설명하지 않습니다. 이러한 리터럴을 상수 프로퍼티로 변경하면 해당 값에 의미 있는 이름을 붙일 수 있어, 상수 값 변경 시 쉽게 변경이 가능합니다.
fun isPasswordValid(text: String): Boolean {
if (text.length < 7) return false
// ...
}
fun isPasswordValid(text: String): Boolean {
if (text.length < MIN_PASSWORD_LENGTH) return false // 리터럴을 의미 있는 상수로 변경
// ...
}
private val MIN_PASSWORD_LENGTH = 7
위 예시에서 숫자 7은 비밀번호 최소 길이를 나타내겠지만, 주석이 없다면 진짜 최소 길이여서 그런 것인지 알 수가 없습니다. 이럴 때 리터럴을 상수 프로퍼티로 리팩토링하면 명확하게 비밀번호 최소 길이임을 이해할 수 있습니다. 또한, 두 번 이상 같은 의미로 사용되는 값이라면 상수로 추출하면 이후 해당 값이 변경되어야 할 때 변경이 용이합니다.
상수로 추출하면 이름을 붙일 수 있고, 나중에 해당 값을 쉽게 변경할 수 있습니다.
함수
어플리케이션 개발 시, 사용자에게 토스트 메시지를 자주 출력해야 해서 이를 함수로 만들었다고 가정합니다.
fun Context.toast(message: String, duration: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, message, duration).show()
}
이후에 토스트 대신에 스낵바라는 형식으로 출력하는 것으로 전체적으로 변경을 하고자 할 때, 함수 내부에서 토스트를 호출하는 부분만 스낵바로 변경하는 것이 아닌, 함수 이름도 바뀌어야 합니다. 이는 위에서 정의한 함수 이름을 그대로 쓰면 잘못된 동작으로 오해할 수 있기 때문입니다.
fun Context.snackbar(message: String, duration: Int = SnackBar.LENGTH_LONG) {
SnackBar.makeText(this, message, duration).show()
}
하지만, 내부적으로 사용하더라도 함수 이름을 직접 바꾸는 것은 위험합니다. 만약, 다른 모듈이 이 함수에 의존하고 있다면, 다른 모듈에서 큰 문제가 생길 수도 있기 때문입니다.
여기서 중점을 두는 포인트를 메시지 출력 방법이 아닌 사용자에게 메시지를 출력하고 싶다는 의도 자체로 옮겨야 합니다.
따라서, 메시지를 출력하는 더 추상적인 방법이 필요하며, 아래와 같이 메소드를 추상화할 수 있습니다. 이렇게 추상화하면, 토스트나 스낵바와 무관하게 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 }
위처럼 변경할 경우, 메시지를 출력하고자 하는 함수임을 사용자에게 명확하게 전달할 수 있습니다. 함수는 매두 단순한 추상화이지만, 제한이 많습니다. 예를 들어 함수는 상태를 유지하지 않으며 함수 시그니처를 변경하면 프로그램 전체에 영향을 줄 수 있습니다. 구현을 추상화하는 더 강력한 방법으로는 클래스가 있습니다.
클래스
클래스가 함수보다 더 강력한 추상화인 이유는, 상태와 많은 함수를 가질 수 있다는 점 때문입니다. 하지만, 여전히 한계가 있어 좀 더 자유를 얻기 위해서는 인터페이스를 통해 추상화를 해야 합니다.
인터페이스
코틀린 표준 라이브러리를 읽어보면, 거의 모든 것이 인터페이스로 표현된다는 것을 확인할 수 있습니다.
- listOf 함수는 List를 리턴합니다. List는 인터페이스입니다.
- 컬렉션 처리 함수는 Iterable 또는 Collection의 확장 함수로, List, Map 등을 리턴합니다. 이들 역시 인터페이스입니다.
- 프로퍼티 위임은 ReadOnlyProperty 또는 ReadWriteProperty 뒤에 숨겨집니다. 이들도 인터페이스입니다.
라이브러리를 만드는 사람은 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 이를 노출하는 코드를 많이 사용합니다. 이렇게 하면 사용자가 클래스를 직접 사용하지 못하므로, 라이브러리를 만드는 사람은 인터페이스만 유지한다면 내부 구현은 원하는대로 구현할 수 있습니다.
인터페이스 뒤에 객체를 숨김으로써 실질적인 구현을 추상화하고, 사용자가 추상화된 것에만 의존하게 만들 수 있습니다.
즉, 결합(coupling)을 줄일 수 있습니다.
참고로, 코틀린이 클래스가 아닌 인터페이스를 리턴하는 이유에는 멀티 플랫폼 언어라는 점도 있습니다. 코틀린/JVM, 코틀린/JS 등 구현이 상이한 리스트를 리턴해야 하기 때문에 인터페이스를 반환합니다.
- 각 플랫폼의 네이티브 리스트를 사용해 속도를 높일 수 있고, 어떤 플랫폼을 사용해도 List 인터페이스에 맞춘 상태이므로 차이 없이 사용이 가능합니다.
추상화의 문제
추상화는 많은 장점들도 제공하지만, 자유를 줌으로써 코드를 이해하고 수정하기 어렵게 만듭니다.
어떤 방식으로든 추상화를 하게 되면 코드를 읽는 사람이 해당 개념을 배우고 잘 이해해야 합니다. 따라서 추상화도 비용이 있는 작업이기 때문에 극단적으로 모든 것을 추상화해서는 안 됩니다.
추상화는 거의 무한하게 할 수 있지만, 어느 순간부터 득보다 실이 많아질 것이기 때문에 균형을 잘 잡아야 합니다.
추상화는 많은 것을 숨길 수 있는 테크닉이지만, 너무 많은 것을 숨기면 결과를 이해하기 어려울 수 있습니다.
어떻게 균형을 맞출까?
아래 요소들에 따라 추상화 균형은 달라질 수 있으며, 상황에 맞는 적절한 균형을 찾아야 합니다. 추상화가 너무 많거나 너무 적은 상황 모두 좋은 상황은 아닙니다.
- 팀의 크기
- 팀의 경험
- 프로젝트의 크기
- 특징 세트(feature set)
- 도메인 지식
완벽한 균형을 찾는 것은 어렵지만, 균형을 맞출 때 사용할 수 있는 몇 가지 규칙은 다음과 같습니다.
- 많은 개발자가 참여하는 프로젝트는 이후에 객체 생성과 사용 방법을 변경하기 어렵습니다.
- 따라서, 추상화 방법을 사용하면 좋고, 최대한 모듈과 부분(part)을 분리하면 좋습니다.
- 의존성 주입 프레임워크를 사용하면, 생성이 얼마나 복잡한지는 신경 쓰지 않아도 됩니다.
- 테스트를 하거나, 다른 어플리케이션 기반으로 새로운 어플리케이션을 만든다면 추상화를 사용하는 것이 좋습니다.
- 프로젝트가 작고 실험적이라면, 추상화 하지 않고 직접 변경해도 괜찮습니다.
개발을 할 때에는 항상 무언가 변화할 수 있다고 생각하는 것이 좋습니다.
아이템 28 - API 안정성을 확인하라
사용자는 API의 안정성에 대해 잘 알고 있어야 하며, 안정적인 API를 사용하는 것이 가장 좋습니다. API를 사용할 때에는 해당 모듈/라이브러리를 만드는 사람과 커뮤니케이션을 하여 정확하게 이해하고 사용해야 합니다.
API 개발자는 API에 변경을 가할 때에는 사용자가 적응할 시간을 충분히 주고 업데이트해야 하며, 업데이트 시 API 버전이나 문서, 어노테이션 등을 통해 서로 소통을 해야 합니다.
아이템 29 - 외부 API를 랩(wrap)해서 사용하라
외부 라이브러리 API를 사용할 때, 잠재적으로 불안정하다고 판단되면 랩(wrap)해서 사용하는 것이 좋습니다.
- 라이브러리가 안정적이라고 판단하는 심플한 기준은, 버전 번호와 사용자 수입니다.
- 일반적으로 라이브러리 사용자가 많을수록 안정적입니다. (좀 더 신중하게 변화를 일으키게 됨)
랩(wrap)한다는 것은 어떤 의미일까?
예를 들어, android 프로젝트에서 Picasso 라이브러리를 활용해 특정 url에서 이미지를 로드해 보여준다고 하면 아래와 같은 로직을 사용해야 합니다.
Picasso.get()
.load(url)
.into(imageView)
위와 같은 이미지 로드를 어플리케이션 내 여러 곳에서 호출이 필요할 때, 아래와 같이 메소드로 뽑아 사용할 수 있으며, 아래와 같은 메소드를 랩(wrap)했다고 볼 수 있습니다.
fun ImageView.loadImage(url: String) {
Picasso.get()
.load(url)
.into(this)
}
만약, 이제 기존 이미지 대신 GIF 이미지를 로드하는 것으로 전체적으로 어플리케이션 수정이 필요하다면 아래와 같이 메소드 내용만 수정하여 전체 어플리케이션에 변화를 꾀할 수 있습니다.
fun ImageView.loadImage(url: String) {
Glide.with(context)
.load(url)
.into(this)
}
위와 같은 예시 외에도, springboot 프로젝트를 진행할 때에 잠재적으로 실패할 수 있는 외부 API에 대해 회로 차단기(circuit breaker)를 설정하는 것도 랩(wrap)한다고 볼 수 있습니다.
랩(wrap)할 경우 장점
- 문제가 있다면, 래퍼(wrapper)만 변경하면 되므로, API 변경에 쉽게 대응 가능
- 프로젝트 스타일에 맞춰 API 형태 조정 가능
- 특정 라이브러리에 문제 발생 시, 래퍼를 수정해서 다른 라이브러리를 사용하도록 코드 변경 가능
- 필요한 경우 쉽게 동작을 추가하거나 수정 가능
랩(wrap)할 경우 단점
- 래퍼를 따로 정의해야 함
- 다른 개발자가 프로젝트를 다룰 때, 래퍼에 대해 확인이 필요함
- 래퍼들은 프로젝트 내부에서만 유효하기 때문에, 문제가 생겨도 외부에 질문을 할 수 없음
아이템 30 - 요소의 가시성을 최소화하라
변경을 가할 때는 기존의 것을 숨기는 것보다 새로운 것을 노출하는 것이 쉽습니다. 일반적으로 외부에 노출되어 있는 요소들은 공개 API의 일부이며, 가시성과 관련된 제한을 변경하는 것이 어렵습니다. 따라서, 처음에는 작은 API로서 개발을 하도록 강제하는 것이 좋습니다.
세터만 private으로 만드는 코드
클래스의 상태를 나타내는 프로퍼티를 외부에서 변경할 수 있도록 열어두면, 클래스는 자신의 상태를 보장할 수 없습니다. 보통 클래스마다 상태에 대한 규약이 있는데, 누구나 변경할 수 있도록 열게 되면 규약을 모르는 사람이 잘못 변경하여 클래스의 불변성이 무너질 수 있습니다.
코틀린에서는 아래와 같이 세터만 private으로 만드는 코드를 많이 사용하여, 구체 접근자의 가시성을 제한해 프로퍼티를 캡슐화합니다.
class CounterSet<T>(
private val innerSet: MutableSet<T> = setOf()
) : MutableSet<T> by innerSet {
var elementsAdded: Int = 0
private set // setter를 private으로 하여 외부에서 변경할 수 없도록 함(가시성 제한)
override fun add(element: T): Boolean {
elementsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
elementsAdded += elements.size
return innerSet.addAll(elements)
}
}
위 예시를 보면, elementsAdded의 세터를 private으로 만듬으로써, 변경이 일어날 수 있는 곳을 한정하였습니다.
- 가시성을 제한함으로써 클래스의 변경을 쉽게 추적하며, 프로퍼티의 상태를 더 쉽게 이해할 수 있습니다.
- 이는 동시성(concurrency)을 처리할 때 중요합니다.
가시성 한정자
가시성 한정자의 큰 제한은, API를 상속할 때 오버라이드해서 가시성을 제한할 수는 없다는 점입니다.
- 이는 서브클래스가 슈퍼클래스로도 사용될 수 있기 때문이며, 이로 인해 상속보다 컴포지션을 선호합니다.
클래스 멤버의 가시성 한정자
- public(디폴트): 어디에서나 볼 수 있습니다.
- private: 클래스 내부에서만 볼 수 있습니다.
- protected: 클래스와 서브클래스 내부에서만 볼 수 있습니다.
- internal: 모듈 내부에서만 볼 수 있습니다.
- 다른 모듈에서 사용될 여지가 있는 코드일 경우, 공개하고 싶지 않을 때 internal을 사용하면 됩니다.
톱레벨 요소의 가시성 한정자
- public(디폴트): 어디에서나 볼 수 있습니다.
- private: 같은 파일 내부에서만 볼 수 있습니다.
- internal: 모듈 내부에서만 볼 수 있습니다.
모듈이란?
함께 컴파일되는 코틀린 소스를 의미하며, 아래와 같다고 볼 수 있습니다.
- 그레이들(Gradle) 소스 세트
- 메이븐(Maven) 프로젝트
- 인텔리제이(IntelliJ) IDEA 모듈
- 엔트(Ant) 태스크 한 번으로 컴파일되는 파일 세트
코틀린은 지역적으로만 사용되는 요소는 private으로 만드는 것이 좋다는 컨벤션이 있습니다.
하지만, 이러한 규칙은 데이터를 저장하기 위한 클래스(DTO)에는 적용하지 않는 것이 좋습니다. 데이터를 저장하도록 설계된 클래스는 숨길 필요가 없기 때문입니다.
아이템 31 - 문서로 규약을 정의하라
규약
어떤 행위를 설명하면 사용자는 이를 일종의 약속으로 취급하며, 이를 기반으로 스스로 자유롭게 생각하던 예측을 조정합니다.
이처럼 예측되는 행위를 요소의 규약(contract of an element)이라고 부릅니다.
규약을 정의하는 것은 API 개발자/사용자 모두에게 좋은 일입니다.
- 규약이 적절히 정의되어 있다면, 클래스를 만든 사람은 클래스가 어떻게 사용될지 걱정하지 않아도 됩니다.
- 따라서 규약만 지킨다면 원하는 부분을 마음대로 수정할 수 있습니다.
- 클래스를 사용하는 사람은 클래스가 내부적으로 어떻게 구현되어 있는지 걱정하지 않아도 됩니다.
- 클래스의 구현을 믿고 그 내용을 의존해서 다른 무언가를 만들 수 있습니다.
따라서, 서로가 규약을 존중한다면 독립적으로 작업해도 모든 것이 정상적으로 기능할 것입니다.
대표적인 규약은 아래와 같습니다.
- 이름: 일반적인 개념과 관련된 메소드는 이름만으로 동작을 예측할 수 있습니다.
- 주석과 문서: 필요한 모든 규약을 적을 수 있는 강력한 방법입니다.
- 타입: 타입은 객체에 대한 정보를 제공합니다. 자주 사용되는 타입이라면, 타입만 보아도 어떻게 사용하는지 알 수 있지만 익숙치 않은 타입이라면 문서에 추가로 설명을 해주어야 합니다.
주석을 써도 될까?
클린코드 책을 통해 주석 없이도 읽을 수 있는 코드를 작성해야 한다는 점이 널리 알려졌습니다. 코드만 읽어도 알 수 있는 코드를 만드는 것은 중요하지만, 너무 극단적인 것은 좋지 않습니다.
- 주석은 함수/클래스에 더 많은 내용의 규약을 설명할 수 있으며, 문서를 자동 생성하는 데에도 많이 사용됩니다.
- 참고로, 코틀린 표준 라이브러리의 모든 public 함수들을 살펴보면 주석을 통해 규약을 정리해둔 것을 확인할 수 있습니다.
KDoc
주석을 통해 문서화할 때 코틀린에서 사용되는 공식적인 형식은 KDoc이며, 이는 Javadoc과 동일한 역할을 합니다.
모든 KDoc 주석은 Javadoc과 마찬가지로 /**로 시작하여 */로 끝납니다. 또한, 이 사이의 모든 줄은 보통 *로 시작합니다.
KDoc 주석의 구조는 아래와 같습니다.
- 첫 번째 부분은 요소에 대한 요약 설명(summary description)입니다.
- 두 번째 부분은 상세 설명입니다.
- 이어지는 줄은 모두 태그로 시작합니다. 이러한 태그는 추가적인 설명을 위해 사용됩니다.
- 사용할 수 있는 태그는 KDoc 공식 문서를 참고하면 됩니다.
KDoc에서 관련된 요소 등에 링크를 걸고자 할 때에는 아래 예시처럼 대괄호를 사용하면 됩니다.
참고로, 코틀린 코딩 컨벤션에 따르면 @param과 @return tag 사용을 지양하는 것이 좋습니다.
해당 태그들을 사용하는 대신, 파라미터와 리턴값에 대한 설명을 주석에 직접 적고, 파라미터가 언급된 곳마다 링크를 추가하는 것을 권장합니다.
타입 시스템과 예측
타입 계층(type hierarchy)은 객체와 관련된 중요한 정보입니다. 클래스와 인터페이스에는 여러 가지 예측이 들어가며, 클래스가 어떤 동작을 할 것이라 예측되면 해당 클래스의 서브클래스도 이를 보장해야 합니다.
- 리스코프 치환 원칙(Liskov substitution principle): S가 T의 서브타입이라면, 별도의 변경이 없어도 T 타입 객체를 S 타입 객체로 대체할 수 있어야 한다.
사용자가 클래스의 동작을 확실하게 예측하려면, 공개 함수에 대한 규약을 잘 지정해야 합니다. 이때, 주석을 통해 클래스 동작을 예측할 수 있습니다.
조금씩 달라지는 세부 사항
구현의 세부 사항은 달라질 수 있지만, 최대한 외부에 노출하는 영역을 줄여 캡슐화하는 것이 좋습니다. 캡슐화가 많이 적용될수록, 사용자가 구현에 신경쓸 부분이 많이 줄어들어 많은 자유를 얻을 수 있습니다.
아이템 32 - 추상화 규약을 지켜라
규약은 개발자들의 단순한 합의입니다. 따라서, 한쪽에서 규약을 위반할 수도 있습니다. 하지만, 무언가를 할 수 있다는 것이 그것을 해도 괜찮다는 의미는 아닙니다. 규약 위반을 하는 것은 프로젝트 내부에 시한 폭탄을 설치한 것과 같습니다.
규약은 보증(warranty)과 같기 때문에, 규약을 위반하면 코드가 작동을 멈췄을 때 문제가 될 수 있습니다. 규약을 깰 수 밖에 없다면, 이를 잘 문서화해야 합니다. 이러한 정보는 코드 유지보수 시 큰 도움이 되며, 그 사람은 몇 년 뒤의 당신이 될 수 있습니다.
클래스를 상속하거나, 다른 라이브러리의 인터페이스를 구현할 때는 규약을 반드시 지켜야 합니다.
아래 예시는 모든 클래스는 equals와 hashCode 메소드를 가진 Any 클래스를 상속받고, 규약을 갖고 있는데 equals만 구현하고 hashCode가 제대로 구현되지 않아 Set에서 중복을 허용해버린 예시입니다.
class Id(val id: Int) {
override fun equals(other: Any?) =
other is Id && other.id == id
}
val set = mutableSetOf(Id(1))
set.add(Id(1))
set.add(Id(2))
print(set.size) // 3
경고에서 볼 수 있듯이, hashCode()가 없어서 제대로 Set으로서 동작하지 않은 것입니다. 따라서, 아래와 같이 hashCode를 추가로 구현하면 정상적인 결과를 확인할 수 있습니다.
스터디 준비
궁금한 점
- 아이템 27
- P176 팀에서 코딩 컨벤션으로 지정한 추상화 수준(?)이 있는지 궁금합니다.
- 아이템 28
- 팀 내에서 라이브러리 버전업을 어떤 경우에 하시는지 궁금합니다. 보통, springboot 버전에 따라 각각의 라이브러리 버전이 엮인 경우가 많은데, springboot나 jdk 버전업을 하셨다면 관련해서 공유해주시면 좋을 것 같습니다.
- 프로젝트가 계속 들어오거나 하면 버전업하기 쉽지 않은데, 어떻게 버전업을 녹이는지 궁금합니다. 특히, QA 지원은 어떤식으로 받으시는지도 궁금합니다.
- 아이템 29
- 외부 API를 랩(wrap)한다는 것에, circuitBreaker를 통해 API가 에러가 많이 발생할 때 호출안하도록 하는 것도 포함될 수 있는 것일까?
- circuitBreaker 문서 확인 시, 함수 호출을 circuit breaker로 wrap한다고 작성되어 있는 것으로 보아 포함되는 것으로 보임
- 외부 API를 랩(wrap)한다는 것에, circuitBreaker를 통해 API가 에러가 많이 발생할 때 호출안하도록 하는 것도 포함될 수 있는 것일까?
- 아이템 32
- P204 hashCode와 equals 구현에 일관성이 없다는게 어떤 의미일까? (6장 읽을 때 한 번 살펴볼 것)
고찰
- 아이템 26
- 확실히 하나의 서비스에 모든 행위를 넣는 것보다, 각 행위를 일으키는 주체에 함수를 각각 구현하여 서비스에서는 해당 함수를 호출만 하면 작은 단위로 테스트하기 좋았습니다. 앞으로도, 이 부분을 고려하면서 최대한 하는 역할이 적도록 함수를 구성해야 할 것 같습니다.
- 아이템 27
- P164 물 위를 걷는 것과 명세서로 소프트웨어 개발하는 것은 쉽다. 둘 다 동결되어 있다면.. 이라는 말이 너무 와닿았습니다.
- 요구사항이 명확하게 픽스될 때도 있지만, 프로젝트 단위가 커지면 엮인 조직도 많아져 서로의 입장차가 생기는 것 같습니다. 그 사이에서 조율하다보면 정책이 중간에 바뀌기도 해서 작업 자체는 간단하더라도 변경될 여지가 있다는 점에서 불안하게 개발하게 되는 경우가 많았던 것 같습니다.
- P164 물 위를 걷는 것과 명세서로 소프트웨어 개발하는 것은 쉽다. 둘 다 동결되어 있다면.. 이라는 말이 너무 와닿았습니다.
참고자료
- 이펙티브 코틀린(마르친 모스칼라 저)
- Item 28 - consider wrapping external apis
- Kotlin Docs - Coding conventions > Documentation comments
- FizzBuzzEnterpriseEdition
'PROGRAMMING LANGUAGE > KOTLIN' 카테고리의 다른 글
[Effective Kotlin] 5장 객체 생성 (0) | 2024.07.17 |
---|---|
[Kotlin Coroutine] 코루틴을 활용한 플로우와 상태 흐름 관리 (0) | 2024.07.13 |
[Kotlin Coroutine] 코루틴과 멀티스레딩 환경에서의 최적화 (0) | 2024.07.04 |
[Effective Kotlin] 3장 재사용성 (0) | 2024.06.30 |
[Kotlin Coroutine] kotlinx-coroutines-test를 활용한 coroutine 테스트 (0) | 2024.06.29 |