이펙티브 코틀린(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 함수를 호출하는 구조를 취할 수 있지만, 이는 잘못된 방식이다
예시 코드
상속된 규약
•
클래스를 상속하거나, 다른 라이브러리의 인터페이스를 구현할 때는 규약을 반드시 지켜야 한다
정리
•
프로그램을 안정적으로 유지하려면 규약을 잘 지켜야 한다