추상화(abstraction)은 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말합니다.
간단하게 말하자면, 추상화는 복잡성을 숨기기 위해 사용되는 형식을 의미합니다. 대표적인 예로는 인터페이스가 있습니다.
어떤 객체애 대한 추상화는 여러 가지가 나올 수 있고, 객체는 여러 형태로 추상화해서 표현할 수 있습니다. 추상화를 하려면 객체에서 무엇을 감추고 무엇을 노출해야 하는지를 결정해야 합니다.
프로그래밍에서의 추상화
프로그래밍에서 하는 일은 기본적으로 추상화입니다. 예를 들어 숫자를 입력하면, 이는 내부적으로 0과1이라는 형식으로 표현됩니다.
추상화를 설계한다는 것은 단순하게 모듈 또는 라이브러리로 분리한다는 의미가 아닙니다.
강력한 프로그래밍 언어들이 갖고 있는 기능 중 하나는 공통 패턴에 이름을 붙여서 추상화를 만드는 기능입니다. 예를 들어 함수, 델리게이트, 클래스 등이 대표적인 예입니다.
프로그래밍에서는 다음과 같은 목적으로 추상화를 사용합니다.
- 복잡성을 숨기기 위해
- 코드를 체계화하기 위해
- 만드는 사람에게 변화의 자유를 주기 위해
아이템 26 - 함수 내부의 추상화 레벨을 통일하라
컴퓨터는 매우 복잡한 창치이지만, 여러 계층의 다양한 요소로서 분할되어 있으므로 쉽게 사용할 수 있습니다.
개발자의 관점에서 컴퓨터의 가장 낮은 추상화 계층은 하드웨어입니다. 개발자는 프로세서 제어 명령을 하고 이는 원래 0과 1로 이루어지지만, 이를 쉽게 읽을 수 있게 어셈블리라는 언어로 표현합니다.
다만, 어셈블리로 프로그래밍하는 것은 매우 어렵기에 간단한 언어를 사용한 다음 변환할 수있는 컴파일러(Compiler)를 만들었습니다.
이런식의 계층이 잘 분리되면, 그 아래의 게층은 이미 완성되어 있으므로 해당 계층만 생각하면 됩니다. 즉, 개발자는 특정한 계층에서 작업하며 거기까지만 알아도 문제가 없습니다.
추상화 레벨
계층에 대해 레벨을 구분할 수 있는데, 보통 높은 레벨로 갈수록 물리 장치로부터 점점 멀어집니다. 프로그래밍에서는 높은 레벨일수록 프로세서로부터 멀어진다고 표현합니다.
높은 레벨일수독 단순함을 얻지만, 제어력을 잃습니다. 예를 들어 C언어는 메모리 관리를 직접할 수 없고, 자바는 가비지 컬렉터가 메모리를 관리해줍니다. 따라서 메모리 사용을 최적화하는 것이 굉장히 힘듭니다.
추상화 레벨 통일
코드도 추상화를 계층처럼 만들어서 사용할 수 있는데, 이를 위한 가장 기본적인 도구가 함수입니다.
함수또한 높은 레벨과 낮은 레벨을 구분해서 사용해야합니다. 이를 추상화 레벨 통일 원칙이라고 부릅니다.
함수가 어떤 식으로 동작하는지 확인하기 위해서는, '함수는 작아야 하며, 최소한의 책임만을 가져야 한다'라는 규칙을 지켜야 합니다. 또한, 어떤 함수가 다른 함수보다 복잡하다면 일부 부분을 추출해서 추상화하는 것이 좋습니다.
추가적으로 이런 형태로 함수를 추출하면, 재사용과 테스트가 쉬워집니다.
프로그램 아키텍처의 추상 레벨
추상화 계층이라는 개념은 함수보다 너 높은 레벨에서도 적용할 수 있습니다. 추상화를 구분하는 이유는 세부 사항을 숨김으로써 상호 운영성과 플랫폼 독립성을 얻기 위함입니다. 이는 세부적인 구현을 신경 쓰기보다는, 문제 중심으로 해결하려는 문제에 집중할 수 있게 합니다.
계층이 잘 분리된 프로젝트를 계층화가 잘 되었다고 부릅니다. 계층화가 잘 된 프로젝트를 좋은 프로젝트라고 부릅니다. 계층화가 잘 된 프로젝트는 어떤 계층 위치에서 코드를 보아도, 일관적인 관점을 얻을 수 있습니다.
정리
별도의 추상화 계층을 만드는 것은 knowledge를 체계화하고, 서스시스템의 세부 사항을 숨김으로써 상호 운영성과 플랫폼 독립성을 얻게 합니다.
아이템 27 - 변화로부터 코드를 보호하려면 추상화를 사용하라
함수와 클래스 등의 추상화로 코드를 숨기면, 사용자는 세부 사항을 알지 못해도 괜찮습니다.
이번 아이템에서는 추상화를 통해 변화로부터 코드를 보호하는 행위가 어떤 자유를 가져오는지 살펴봅니다.
상수
리터럴는 아무것도 설명하지 않는 그냥 값 자체입니다. 따라서 코드에서 반복적으로 등장할 때 문제가 됩니다.
이러한 리터럴을 상수 프로퍼티로 변경하면 의미를 부여할 수 있으며, 값을 변경해야 할 때 훨씬 쉽게 변경할 수 있습니다.
함수
많이 사용되는 알고리즘은 다음과 같이 간단한 확장 함수로 만들어서 사용할 수 있습니다.
fun Context.toast(
message : String,
duration : Int = Toast.LENGTH_LONG
){
Toast.makeText(this, message, duration).show()
}
이렇게 일반적인 알고리즘을 추출하면, 토스트를 출력하는 코드를 항상 기억해 두지 않아도 됩니다. 만약 토스트 대신 스낵바를 호출해야할 경우에는 스낵바를 호출하는 확장 함수를 만들고 한꺼번에 수정하면 됩니다.
하지만, 함수의 이름을 직접 바꾸는 것은 위험할 수 있습니다. 다른 모듈이 이 함수에 의존하고 있다면, 큰 문제가 발생합니다.
메시지의 출력 방법이 바뀔 수 있다는 것을 알고있다면, 이때부터 중요한 것은 메시지의 출력 방법이 아니라, 사용자에게 메시지를 출력하고 싶다는 의도 자체입니다. 따라서 메시지를 출력하는 더 추상적인 방법이 필요합니다.
토스트 출력을 토스트라는 개념과 무관한 더 높은 레벨의 함수로 옮겨봅시다.
fun Context.showMessage(
message : String
duration : MessageLength = MessageLength.Long
){
val taostDuration - MessageLength.Long
SHORT -> Length.LENGTH_SHORT
LONG -> Length.LEGTH_LONG
Toast.makeText(this, message, toastDuration).show()
}
컴파일러의 관점에는 큰 변화가 없습니다. 하지만 사람의 관점에서는 이름이 바뀌는 것은 큰 변화가 일어난 것입니다. 함수는 추상화를 표현하는 수단이며, 함수 시그니처는 이 함수가 어떤 추상화를 표현하고 있는지 알려줍니다. 따라서 의미있는 이름은 굉장히 중요합니다.
클래스
이전의 메시지 출력을 클래스로 추상화해 봅시다.
class MessageDisplay(val context: Context) {
fun showMessage(
message : String
duration : MessageLength = MessageLength.Long
){
val taostDuration - MessageLength.Long
SHORT -> Length.LENGTH_SHORT
LONG -> Length.LEGTH_LONG
Toast.makeText(this, message, toastDuration).show()
}
}
enum class MessageLength { SHORT, LONG }
//사용
val messageDisplay = MessageDisplay(context)
messageDisplay.show("Message")
클래스가 함수보다 더 강력한 이유는 상태를 가질 수 있으며, 많은 함수를 가질 수 있다는 점 때문입니다. 클래스는 함수보다 더 많은 자유를 제공하지만, 한계가 있습니다.
클래스가 final 이라면 해당 클래스는 더 이상 확장될 수 없으므로 그 타임 아래에 어떤 구현이 있는지 알 수 있습니다. 반면 open으로 선언된 클래스는 상속 가능하므로, 여러 가지 구현을 가질 수 있습니다.
이러한 특성은 더 많은 자유를 제공합니다. 더 많은 자유를 얻고자 한다면, 더 추상적이게 만들면 되고 이는 인터페이스 뒤에 클래스를 숨기는 방식으로 구현할 수 있습니다.
인터페이스
코틀린 표준 라이브러리는 거의 모든 것이 인터페이스로 표현됩니다.
라이브러리를 만드는 사람은 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 이를 노출하는 코드를 사용합니다. 즉, 인터페이스 뒤에 객체를 숨김으로써 실질적인 구현을 추상화하고, 사용자가 추상화된 것에만 의존하게 맏늘 수 있는 것입니다. 즉, 결합을 줄일 수 있습니다.
코틀린은 멀티 플랫폼에 따라 구현을 다르게 반환해주기 위해 인터페이스를 활용합니다.
ID 만들기
프로젝트에서 고유 ID를 사용해야 한다면 가장 간단한 방법은 어떤 정수 값을 계속 증가시키면서, 이를 ID로 활용하는 것입니다.
var nextId: Int = 0
val newId = nextId++
다만 이러한 코드는 Thread-safe 하지 않습니다. 그래도 이런 코드를 사용해야 한다면 일단은 함수를 사용하는 것이 좋고, 더 나아가 타입을 쉽게 변경할 수 있게 클래스를 사용하는 것이 더 좋습니다.
추상화가 주는 자유
추상화를 하는 몇 가지 방법은 아래와 같습니다.
- 상수로 추출한다.
- 동작을 함수로 래핑한다.
- 함수를 클래스로 래핑한다.
- 인터페이스 뒤에 클래스를 숨긴다.
- 보편적인 객체를 특수한 객체로 래핑한다.
이를 구현할 때는 여러 도구를 활용할 수 있습니다.
- 제네릭 타입 파라미터를 사용한다.
- 내부 클래스를 추출한다.
- 생성을 제한한다(팩토리 함수로만 객체를 생성할 수 있게 만드는 등)
하지만 추상화는 자유를 주지만, 코드를 이해하고 수정하기 어렵게 만듭니다.
추상화가 주는 문제
추상화를 하려면 코드를 읽는 사람이 해당 개념을 배우고, 잘 이해해야 합니다. 추상화는 비용이 발생하기에 극단적으로 모든 것을 추상화해서도 안됩니다.
추상화는 무한하게 할 수 있지만, 득보다 실이 많아질 것입니다.
생각한 것을 어느 정도 숨겨야 개발이 쉬워지는 것도 사실이지만, 너무 많은 것을 숨기면 결과를 이해하는 것 자체가 어려워집니다.
추상화를 이해하기 위해서는 예제를 살펴보는 것이 좋습니다. 단우이 테스트와 문서의 예제는 추상화를 어떻게 사용하는지 확실하게 보여줍니다.
어떻게 균형을 맞춰야 할까?
추상화는 자유를 주지만 이해하기 어렵게 만듭니다. 최상의 답은 존재하고 이는 다음과 같은 요소에 따라 달라질 수 있습니다.
- 팀의 크기
- 팀의 경험
- 프로젝트의 크기
- 특징 세트
- 도메인 지식
적절한 균형을 찾는 것은 어렵지만 몇 가지 규칙을 정의할 수는 있습니다.
- 많은 개발자가 참여하는 프로젝트는 객체 생성과 사용 방법을 변경하기 어렵기에 추상화를 사용하는 것이 좋습니다.
- 의존성 주입 프레임워크를 사용하면, 생성이 얼마나 복잡한지는 신경쓰지 않아도 됩니다.
- 테스트를 하거나, 다른 애플리케이션을 기반으로 새로운 애플리케이션을 만든다면 추상화를 사용하는 것이 좋습니다.
- 프로젝트가 작고 실험적이라면, 추상화를 하지 않고도 직접 변경해도 괜찮습니다. 문제가 발생했다면 빠르게 변경하면 됩니다.
정리
추상화는 단순하게 중복성을 제거해서 코드를 구성하기 위한 것이 아닙니다. 추상화를 코드를 변경해야 할 때 도움이 됩니다.
아이템 28 - API 안정성을 확인하라
프로그래밍에서는 안정적이고 최대한 표준적인 API를 선호합니다. 이유는 다음과 같습니다.
- API가 변경되고 업데이트 했다면, 여러 코드를 수동으로 업데이트 해야합니다. 라이브러리의 작은 변경은 이를 활용하는 다른 코드들의 많은 부분을 변경하게 만들 수 있습니다. 그래서 라이브러리가 변경되어도 이전 라이브러리를 유지하는 경우가 많습니다. 하지만 그럴수록 업데이트가 어려워지고 버그와 취약성 등이 발생할 수 있습니다. 따라서 개발자가 안정적인 라이브러리로 업데이트하는 것을 두려워한다는 것은 매우 좋지 않은 상황입니다.
- 사용자가 새로운 API를 배워야 합니다. 새로 배운다는 것은 어렵기에 안정적인 모듈부터 공부해두는 것이 좋습니다.
좋은 API를 한번에 설계하기는 어렵고 제작자는 이를 계속해서 개선하기 위한 변경을 원합니다. API를 안정적으로 유지하는 가장 간단한 방법은 이를 명확하게 알려주는 것입니다.
일반적으로 버전을 활용해서 안정성을 나타냅니다. 버전은 일반적으로 시멘틱 버저닝을 사용합니다.
시멘틱 버저닝은 버전을 세 번호로 나누어서 구성합니다. 0부터 시작해서 API에 다음과 같은 변경 사항이 있을 때 1씩 증가시킵니다.
- MAJOR 버전: 호환되지 않는 수준의 API 변경
- MINOR 버전: 이전 변경과 호환되는 기능을 추가
- PATCH 버전: 간단한 버그 수정
MAJOR을 증가시킬때는 MINOR와 PATCH를 0으로 돌려둡니다. MINOR를 증가시킬때는 PATCH를 0으로 돌립니다. 사전 배포나 빌드 메타데이터 등은 추가적인 레이블을 활용합니다.
메이저 버전이 0인경우는 초기 개발 전용 버전을 의미합니다. 이는 언제든 변경될 수 있으며 안정적이지 않다는 의미입니다.
안정적인 API에 안정적이지않은 요소를 추가할 때는 `Experimental` 어노테이션을 사용해서 사용자들에게 알려주는 것이 좋습니다.
@ExperimentalNewApi
suspend fun getUsers(): List<User> {
...
}
안정적인 API의 일부를 변경해야 한다면, 전환하는 데 시간을 두고 `Deprecated` 어노테이션을 활용해서 사용자들에게 알려줘야 합니다. `ReplaceWith`을 붙여주는 것도 좋습니다.
@Deprecated("Use suspending getUsers instead")
@ReplaceWith("getUsers()")
fun getUsers(callback: (List<User>) -> Unit) {
...
}
`Deprecated` 와 `ReplaceWith`을 사용했다면 이를 변경할 시간을 제공해줘야 합니다.
아이템 29 - API 안정성을 확인하라
불안정한 API를 활용할 때에는 로직과 직접 결합시키지 않고 외부 라이브러리 API를 Wrap해서 사용합니다.
랩해서 사용하면 다음과 같은 자유와 안정성을 얻을 수 있습니다.
- 문제가 있다면 Wrapper만 변경하면 되므로 쉽게 대응할 수 있습니다.
- 프로젝트의 스타일에 맞춰서 API 형태를 조정할 수 있습니다.
- 특정 라이브러리에서 문제가 발생하면 래퍼를 수정해서 다른 라이브러리를 사용하도록 쉽게 변경할 수 있습니다.
- 필요한 경우 쉽게 동작을 추가하거나 수정할 수 있습니다.
다음과 같은 단점도 있습니다.
- 래퍼를 따로 정의해야 합니다.
- 다른 개발자가 프로젝트를 다룰 때, 어떤 래퍼들이 있는지 확인해야 합니다.
- 래퍼들은 프로젝트 내부에서만 유효하므로 질문할 수 없습니다.
장점과 단점을 바탕으로 라이브러리를 선택해야 합니다. 만약 인기가 없고 새로 만들어진 라이브러리를 사용하기로 결정했다면, 클래스와 함수로 래핑하는 것을 고려해야 합니다.
아이템 30 - 요소의 가시성을 최소화하라
API를 설계할 때에는 가능한 한 간결한 API를 선호합니다.
일반적으로 어떤 수정을 가하기 위해서는 클래스 전체를 이해하고 있어야 합니다. 만약 보이는 요소 자체가 적다면, 유지보수하고 테스트할 것이 적습니다.
변경을 가할 때에는 기존의 것을 숨기는것보다 새로운 것을 노출하는 것이 쉽습니다. 공개적으로 노출되어 있는 요소들은 이미 여러 곳에서 사용되고 있을 것이므로 변경하는 것은 어렵습니다. 대신에 대체제를 제공해야 합니다.
하지만, 다른 개발자가 개발한 코드를 파악하기 위해서는 비즈니스 요구 사항이 무엇인지 빠르게 파악해야 합니다. 널리 사용되는 라이브러리라면, 일부 요소의 가시성을 제한할 경우 많은 분노를 얻을 수 있으므로 처음에는 작은 API로서 개발을 하도록 강제하는 것이 더 좋을 수있 습니다.
클래스의 상태를 나타내는 프로퍼티를 외부에서 변경할 수 있다면, 클래스는 자신의 상태를 보장할 수 없습니다. 클래스는 자신의 상태에 대한 규약등이 있을 수 있고, 이를 모르는 사람은 클래스의 상태를 마음대로 변경할 수 있으므로, 클래스의 불변성이 무너질 가능성이 있습니다.
일반적으로 코틀린에서는 접근자의 가시성을 제한해서 모든 프로퍼티를 캡슐화하는 것이 좋습니다.
서로서로 의존하는 프로퍼티가 있을 때는 객체 상태를 보호하는 것이 더 중요해집니다.
가시성이 제한될수록 클래스의 변경을 쉽게 추적할 수 있으며, 프로퍼티의 상태를 더 쉽게 이해할 수 있습니다. 이는 동시성을 처리할 때 중요합니다. 상태 변경은 병렬 프로그래밍에서 문제가 되므로 많은 것을 제한할수록 안전해집니다.
가시성 한정자 사용하기
내부적인 변경 없이 인터페이스를 유지하고 싶다면, 가시성을 제한하면 됩니다. 기본적으로 크랠스와 요소를 외부에 노출할 필요가 없다면, 가시성을 제한해서 외부에서 접근할 수 없게 만드는 것이 좋습니다.
클래스 맴버의 경우 다음과 같은 4개의 가시성 한정자를 사용할 수있습니다.
- public(default): 어디에서나 볼 수 있습니다.
- private: 클래스 내부에서만 볼 수 있습니다.
- protected: 클래스와 서브 캘래스 내부에서만 볼 수 있습니다.
- internal: 모듈 내부에서만 볼 수 있습니다.
톱레벨 요소에서는 세 가지 가시성 한정자를 사용할 수 있습니다.
- public(default): 어디에서나 볼 수 있습니다.
- private: 클래스 내부에서만 볼 수 있습니다.
- internal: 모듈 내부에서만 볼 수 있습니다.
참고로 모듈과 패키지를 혼동하는 개발자들이 있는데 의미가 전혀 다릅니다. 코틀린에서 모듈이란 함께 컴파일되는 코틀린 소스를 의미합니다. 따라서 다음을 의미합니다.
- Gradle 소스 세트
- Maven 프로젝트
- Intellij IDEA 모듈
- Ant 테스트 한 번으로 컴파일되는 파일 세트
가시성 한정자와 관련된 규칙은 DTO에는 적용하지 않는 것이 좋습니다. 데이터를 저장하도록 설계된 클래스는 숨길 이유가 없기 때문입니다. 따라서 프로퍼티를 사용할 수 있게 눈에띄게 만드는 것이 좋으며, 필요하지 않은 경우 프로퍼티를 그냥 제거하는 것이 좋습니다.
필자의 경험으로는 요소의 가시성은 최대한 제한적인 것이 좋다고 합니다.
아이템 31 - 문서로 규약을 정의하라
함수가 무엇을 하는지 명확하게 설명하고 싶다면 KDoc 주석을 붙여주는 것이 좋습니다.
일반적인 문제는 행위가 문서화되지 않고, 요소의 이름이 명확하지 않다면 이를 사용하는 사용자는 추상화 목펴가 아닌, 현재 구현에만 의존하게 된다는 것입니다. 이러한 문제는 예상되는 행위를 문서로 설명함으로써 해결합니다.
규약
예측되는 행위를 요소의 규약(contract of an element)라고 부릅니다. 규약이 적절하게 정의되어 있다면, 클래스를 만든 사람은 클래스가 어떻게 사용될지 걱정하지 않아도 되고 사용하는 사람은 어떻게 되어 있는지 걱정하지 않아도 됩니다.
규약을 설정하지 않으면 클래스를 사용하는 사람은 구현의 세부적인 정보에 의존해야 합니다.
주석을 써야 할까?
개발은 주석을 쓰지 않고도 읽을 수 있는 코드를 작성하는 프로그래밍 방식으로 바뀌었습니다. 하지만 극단적인 것은 좋지않습니다. 주석을 함께 사용하면 더 많은 규약을 설명할 수 있습니다.
주석을 다는 것보다 함수로서 추출하는 것이 좋습니다. 주석으로 설명해야 하는 부분이 있다면 이를 별도로 함수로 추출해 보세요. 그럼 훨씬 이해하기 쉬워질 것입니다.
KDoc 형식
주석으로 함수를 문서화할 때 사용되는 공식적인 형식을 KDoc라고 부릅니다. 모든 KDoc 주석은 /**로 시작해서 */로 끝납니다.
사용할 수 있는 태그는 아래와 같습니다.
- @param 함수 파라미터 또는 클래스, 프로퍼티, 함수 타입 파라미터
- @return 함수의 리턴 값을 문서화
- @constructor 클래스의 기본 생성자를 문서화
- @receiver 확장 함수의 리시버를 문서화
- @property <name> 명확한 이름을 가진 클래스의 프로퍼티를 문서화, 기본 생성자에 정의된 프로퍼티에 사용한다.
- @throws <class>, @exception <class> 메소드 내부에서 발생할 수 있는 예외를 문서화
- @sample <identifier> 정규화된 형식 이름을 사용해 함수의 사용 예를 문서화한다.
- @see <identifier> 특정한 클래스 또는 메소드에 대한 링크 추가
- @author 요소의 작성자 지정
- @since 요소에 대한 버전 지정
- @supress 이를 지정하면 만들어지는 문서에서 해당 요소가 제외된다. 외부에서는 사용할 수 있지만, 공식 API에 포함할 필요가 없는 요소에 지정
대괄호를 사용하면 관련된 요소 등에 연결할 수 있습니다.
아이템 32 - 추상화 규약을 지켜라
규약은 위반할 수도 있습니다. 때로는 규약을 어겨도 문제가 없는 경우가 있지만 이는 그것을 해도 괜찮다는 의미는 아닙니다.
규약은 보증과도 같습니다. 규약을 위반하면, 코드가 작동을 멈췄을 때 문제가 됩니다.
상속된 규약
클래스를 상속하거나, 다른 라이브러리의 인터페이스를 구현할 때는 규약을 반드시 지켜야 합니다
'스터디 > 이펙티브 코틀린' 카테고리의 다른 글
아이템 48 - 더 이상 사용하지 않는 객체의 레퍼런스를 제거하라 (0) | 2025.02.26 |
---|---|
[이펙티브 코틀린] 5장 - 객체 생성 (0) | 2025.02.04 |
[이펙티브 코틀린] 3장 - 재사용성 (0) | 2025.01.13 |
[이펙티브 코틀린] 1장 - 안정성 (0) | 2023.12.04 |