Blog

이펙티브 코틀린(4)

이펙티브 코틀린(4)

4. 추상화 설계

컴퓨터 과학에서 추상화(abstraction)는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
추상화를 설계한다는 것은 단순하게 모듈 또는 라이브러리로 분리한다는 의미가 아니다.
함수를 정의할 때는 그 구현을 함수 시그니처 뒤에 숨기게 되는데, 이것이 바로 추상화 이다.
추상화의 목적
복잡성을 숨기기 위해
코드를 체계화하기 위해
만드는 사람에게 변화의 자유를 주기 위해

4-1. 함수 내부의 추상화 레벨을 통일하라

추상 계층
물리 장치 → 하드웨어 → 어셈블러 → 프로그래밍 언어 → 애플리케이션
계층이 잘 분리되면 작업할 때 그 아래의 계층은 이미 완성되어 있으므로 해당 계층만 생각하면 된다는 장점이 있다

추상화 레벨

일반적으로 컴퓨터 과학자들은 어떤 계층이 높은 레벨인지 낮은 레벨인지를 구분한다
높은 레벨로 갈수록 물리 장치로부터 점점 멀어진다
높은 레벨일수록 프로세서로부터 멀어진다고 표현한다
높은 레벨일수록 단순함을 얻지만, 제어력을 잃는다.

추상화 레벨 통일

함수는 작아야 하며, 최소한의 책임만을 가져야 한다. - 로버트 C. 마틴(Robert C. Martin), 《클린 코드(Clean Code)》
코드도 추상화를 계층으로 만들어서 사용할 수 있다
이를 위한 기본적인 도구가 바로 함수이다
추상화 레벨 통일 (Single Level of Abstraction, SLA): 함수도 높은 레벨과 낮은 레벨을 구분해서 사용해야 한다는 원칙
예시 코드
함수가 다른 함수보다 좀 복잡하다면, 일부 부분을 추출해서 추상화 하는것이 좋다.
모든 추상화 레벨에서 추상 요소 (abstract term) 을 조작한다

프로그램 아키텍처의 추상 레벨

추상화를 구분하는 이유는 서브시스템의 세부 사항을 숨김으로써 상호 운영성(interoperability)과 플랫폼 독립성을 얻기 위함이다
이는 문제 중심으로 프로그램한다는 의미이다
Level
Description
4
높은 레벨 문제 중심
3
낮은 레벨 문제 중심
2
프로그래밍 언어 구조와 도구
1
운영 체제 연산과 머신 명령
모듈을 분리하면 계층의 고유의 요소를 숨길 수 있다
애플리케이션을 만들 때는 입력과 출력을 나타내는 모듈은 낮은 레벨의 모듈이다.
비즈니스 로직을 나타내는 부분이 높은 레벨의 모듈이다.
계층화가 잘된 프로젝트는 어떤 계층 위치에서 코드를 보아도, 일관적인 관점을 얻을 수 있다

정리

별도의 추상화 계층을 만드는 것은 프로그래밍에서 일반적으로 사용되는 개념이다
knowledge 를 체계화하고, 서브시스템의 세부 사항을 숨김으로써 상호 운영성(interoperability)과 플랫폼 독립성을 얻게 만든다
작고 최소한의 책임만 갖는 함수가 이해하기 쉽다

4-2. 변화로부터 코드를 보호하려면 추상화를 사용하라

함수를 사용하는 쪽은 ‘입&출력만’만 알면 된다. 이는 함수가 ‘입&출력' 만 제대로 낸다면, 함수의 설계자가 함수의 내용을 원하는 대로 변경해도 괜찮다

상수

literal 을 상수 프로퍼티로 변경하면 해당 값에 의미 있는 이름을 부여할 수 있으며, 훨씬 쉽게 변경할 수 있다.
예시 코드

함수

함수는 추상화를 표현하는 수단이며, 함수 시그니처는 이 함수가 어떤 추상화를 표현하고 있는지 알려준다
의미 있는 이름은 굉장히 중요하다
예시 코드

클래스

클래스가 함수보다 더 강력한 이유는 상태를 가질 수 있으며, 많은 함수를 가질 수 있다는 점 때문이다.
클래스 멤버 함수를 메서드라고 부른다
클래스틑 훨씬 더 많은 자유를 보장해준다
해당 클래스가 final 이면 해당 클래스 타입 아래에 어떤 구현이 있는지 알 수 있다
open 클래스를 활용하면 조금은 더 자유를 얻을 수 있다
좀 더 많은 자유를 얻으려면, 인터페이스 뒤에 클래스를 숨기는 방법을 사용하면 된다
예시 코드

인터페이스

코틀린 표준 라이브러리를 보면 거의 모든 것이 인터페이스로 표현된다
라이브러리를 만드는 사람은 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 이를 노출하는 코드를 많이 사용한다
클래스를 직접 사용하지 못하므로, 라이브러리르 만드는 사람은 인터페이스만 유지한다면 별도의 걱정 없이 자신이 원하는 형태로 구현을 변경할 수 있다
결합 (coupling) 을 줄일 수 있다
코틀린이 인터페이스를 반환하는 데에는 여러가지 이유가 있다
코틀린은 멀티 플랫폼 언어이기 때문에 각 플랫폼에 최적화된 객체를 반환해주기 때문이다
테스트할 때 인터페이스 펭리킹 (faking) 이 클래스 모킹 (mocking) 보다 간단하므로 별도의 모킹 라이브러리 (mocking library) 를 사용하지 않아도 된다
선언과 사용이 분리되어 있으므로, ToastDisplay 등의 실제 클래스 변경이 자유롭다
단, 사용 방법을 변경하려면 인터페이스와 실제 클래스를 모두 변경해야 한다.
예시 코드

e.g) ID 만들기 (nextId)

더 많은 추상화는 더 많은 자유를 주지만, 이를 정의하고 사용하고 이해하는 것이 조금 어려워질 수 있다.
예시 코드

추상화가 주는 자유

추상화 할 수 있는 방법
상수로 추출
동작을 함수로 래핑
함수를 클래스로 래핑
인터페이스 뒤에 클래스를 숨김
보편적인 객체(universal object) 를 특수한 객체 (specialistic object) 로 래핑
구현할 때 도움을 주는 도구
제네릭 타입 파라미터를 사용
내부 클래스를 추출
생성을 제한한다
e.g) 팩토리 함수로만 객체를 생성할 수 있게 만듬

추상화의 문제

추상화를 하려면 코드를 읽는 사람이 해당 개념을 배우고, 잘 이해해야 한다
추상화의 가시성을 제한하거나, 구체적인 작업에서만 추상화를 도입하는 것은 큰 문제가 없다
추상화도 비용이 발생하는 작업이기 때문에 극단적으로 추상화를 해서는 안된다
어느 순간부터는 추상화를 통해 얻을 수 있는 득보다는 실이 더 커진다
극단적인 예시) FizzBuzz Enterprise Edition 프로젝트
원래는 10줄도 필요하지 않는 간단한 예시지만, 극단적인 추상화로 인해 61개의 클래스와 26개의 인터페이스로 구성함
추상화가 너무 많으면 코드를 이해하기 어렵다
추상화를 제대로 이해하려면, 예제를 살펴보는 것이 좋다

어떻게 구현을 맞춰야 할까?

아래의 요소에 따라 달라진다
팀의 크기
팀의 경험
프로젝트의 크기
특징 세트 (feature set)
도메인 지식
몇가지 규칙
많은 개발자가 참여하는 프로젝트는 이후 객체 생성과 사용 방법을 변경하기 어렵기 때문에 추상화 방법을 사용하는것이 좋음
최대한 모듈과 부분(part) 을 분리하는 것이 좋다
의존성 주입 프레임워크를 사용하면, 생성이 얼마나 복잡한지 신경을 쓰지 않아도 된다
클래스 등은 한번만 정의하면 되기 때문이다
테스트를 하거나, 다른 애플리케이션을 기반으로 새로운 애플리케이션을 만든다면 추상화를 사용하는것이 좋다
프로젝트가 작고 실험적이라면, 추상화를 하지 않고직접 변경해도 괜찮다
문제가 발생하면 최대한 빨리 직접 변경하면 된다

정리

추상화는 단순하게 중복성을 제거해서 코드를 구성하기 위한 것이 아니다
추상화는 코드를 변경해야 할 때 도움이 된다
추상화를 사용할 때의 장점과 단점을 모두 이해하고, 프로젝트 내에서 균형을 찾아야 한다

4-3. API 안정성을 확인하라

프로그래밍에서는 안정적이고 최대한 표준적인 API (Application Programming Interface) 를 선호한다
안정적인 API 를 선호하는 이유
1.
API 가 변경되면 여러 코드를 수동으로 업데이트 해야 하며, 의존하는 경우가 많은 경우에는 전부 수정해서 업데이트하기 어렵다
2.
사용자가 새로운 API 를 학습해야 한다
코드 작성자가 API 또는 API 의 일부가 불안정하다면 이를 명확하게 인지시켜줘야 한다.
일반적으로 버저닝 시스템(Versioning System) 을 이용하여 알려주고, 보통 시멘틱 버저닝 (Semantic Versioning, Semver) 를 사용한다
Semver 표현 방식
MAJOR 버전: 호환되지 않는 수준의 API 변경
MINOR 버전: 이전 변경과 호환되는 기능을 추가
PATCH 버전: 간단한 버그 수정
실험적인 기능을 사용하게끔 허용해주려면 Experimental 어노테이션을 사용해서 사용자들에게 인지시켜줄 수 있다
안정적인 API 의 일부를 변경해야 한다면, 전환하는데 시간을 두고 Deprecated 어노테이션을 사용해서 사용자들에게 인지시켜줄 수 있다
직접적인 대안이 있다면 ReplaceWith 를 붙여주면 IDE 등에서 자동 전환을 할 수 있다
예시 코드

정리

커뮤니케이션은 버전 이름, 문서, 어노테이션 등을 통해 할 수 있다.
안정적인 API 에 변경을 가할 때는 사용자가 적응할 수 있는 충분한 시간을 줘야 한다

4-4. 외부 API 를 랩(wrap) 해서 사용하라

외부 API 를 사용할 때는 래핑해서 사용해야 한다
장점
문제가 있을 경우 래퍼만 변경해서 대응할 수 있기 때문에 API 변경에 쉽게 대응할 수 있다
프로젝트의 스타일에 따라 API 형태를 조정할 수 있다
라이브러리에 문제가 있을 경우 다른 라이브러리 손쉽게 변경할 수 있다
필요한 경우 동작을 쉽게 추가하거나 수정할 수 있다
단점
래퍼를 따로 정의해야 한다
다른 개발자가 프로젝트를 다룰 때 래퍼를 확인해야 한다
래퍼들은 프로젝트 내부에만 있기 때문에, 문제가 있어도 질문하기 어렵다

4-5. 요소의 가시성을 최소화하라

API 를 설계할 때 가능한 간결한 API를 선호하는 이유
작은 인터페이스는 배우기 쉽고 유지하기 쉽다
변경을 할 때 기존의 것을 숨기는 것보다 새로운 것을 노출하는게 쉽다
구체 접근자의 가시성을 제한해서 모든 프로퍼티를 캡슐화 하는것이 좋다
가시성이 제한될수록 클래스의 변경을 쉽게 추적할 수 있으며, 프로퍼티의 상태를 더 쉽게 이해할 수 있다
이는 동시성을 처리할 때 중요하다

가시성 한정자 사용하기

코틀린에서 모듈이란? 함께 컴파일 되는 코틀린 소스를 의미한다. → Gradle 소스 세트 → Maven 프로젝트 → IntelliJ IDEA 모듈 → Ant 테스크 한번으로 컴파일되는 파일 세트
가시성 한정자 (visibility modifier) 종류
public (default): 어디에서나 볼 수 있다
private: 클래스 내부에서만 볼 수 있다
protected: 클래스와 서브 클래스 내부에서만 볼 수 있다
internal: 모듈 내부에서만 볼 수 있다
top-level 요소에서 사용 가능한 가시성 한정자 종류
public (default): 어디에서나 볼 수 있다
private: 같은 파일 내부에서만 볼 수 있다
internal: 모듈 내부에서만 볼 수 있다
이러한 규칙들은 데이터를 저장하도록 설계된 클래스 (데이터 모델 클래스, DTO) 에는 적용하지 않는것이 좋다
가시성 한정자 제한
API 를 상속할 때 오버라이드해서 가시성을 제한할 수 없다
이유) 서브클래스가 슈퍼클래스로도 사용될 수 있기 때문이다
상속보다 컴포지션을 선호하는 대표적인 이유

정리

가시성은 최대한 제한적인 것이 좋다
인터페이스가 작을수록 이를 공부하고 유지보수 하기 쉽다
최대한 제한이 있어야 변경하기 쉽다
클래스의 상태를 나타내는 프로퍼티가 노출되어 있으면, 클래스가 자신의 상태를 책임질 수 없다
가시성이 제한되면 API 의 변경을 쉽게 추적할 수 있다

4-6. 문서로 규약을 정의하라

함수가 무엇을 하는지 명확하게 설명하고 싶다면, KDoc 주석을 붙여주는 것이 좋다
행위가 문서화되지 않고, 요소의 이름이 명확하지 않다면 이를 사용하는 개발자드은 추상화 목표가 아닌, 현재 구현에만 의존하게 된다

규약

어떤 행위를 설명하면 사용자는 이를 일종의 약속으로 취급하며, 이를 기반으로 스스로 자유롭게 생각하던 예측을 조정한다
예측되는 행위를 요소의 규약(contract of an element) 라고 부른다
규약이 적절하게 정의되어 있다면, 클래스를 만든 사람은 클래스를 어떻게 사용될 지 걱정하지 않아도 된다

규약 정의하기

규약을 정의하는 방법
이름
일반적인 개념과 관련됨 메서드는 이름만으로도 충분히 동작을 예측할 수 있다
주석과 문서
필요한 모든 규약을 적을 수 있는 방법이다
타입
타입은 객체에 대한 많은 것을 알려준다

주석을 써야 할까?

코드만 읽어도 알 수 있는 코드를 작성해야 하는게 가장 좋지만, 주석을 함께 하면 요소에 더 많은 내용의 규약을 설명할 수 있다
함수 이름과 파라미터만으로 정확하게 표현되는 요소에는 주석을 달지 않는게 좋다
예시 코드

KDoc 형식

/** 로 시작해서 */ 로 끝난다
설명은 KDoc 마크다운 형식으로 작성해야 한다
KDoc 마크다운 구조
첫 번째 부분은 요소에 대한 요약 설명 (summary description) 이다
두 번째 부분은 상세 설명이다
이어지는 줄은 모두 태그로 시작한다
태그는 추가적인 설명을 위해 사용된다

타입 시스템과 예측

타입 계측 (type hierarchy) 은 객체와 관련된 중요한 정보이다
리스코프 치환 원칙 (Liskov substitution principle)
인터페이스는 구현해야 한다고 약속한 메서드 목록 이상의 의미를 갖는다
클래스가 어떤 동작을 할 것이라 예측되면, 서브 클래스도 이를 보장해야 한다

조금씩 달라지는 세부 사항

구현의 세부 사항은 항상 달라질 수 있지만, 캡슐화를 통해 최대한 많이 보호하는 것이 좋다
캡슐화가 많이 적용될수록, 사용자가 구현에 신경을 많이 쓸 필요가 없어지므로, 더 많은 자유를 얻게 된다

정리

외부 API 를 구현할 때는 규약을 잘 정의해야 한다
규약은 사용자가 객체를 사용하는 방법을 쉽게 이해하는 등 요소를 쉽게 예측할 수 있게 해준다

4-7. 추상화 규약을 지켜라

규약은 보증(warranty)과 같다
리플렉션을 활용해서 클래스 외부에서 private 함수를 호출하는 구조를 취할 수 있지만, 이는 잘못된 방식이다
예시 코드

상속된 규약

클래스를 상속하거나, 다른 라이브러리의 인터페이스를 구현할 때는 규약을 반드시 지켜야 한다

정리

프로그램을 안정적으로 유지하려면 규약을 잘 지켜야 한다