Effective Kotlin #4 추상화 설계

yeji·2022년 11월 19일
0

Effective Kotlin

목록 보기
4/7

02부 04장. 추상화 설계 (Abstraction design)

추상화(Abstraction) : 복합한 자료, 모듈 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것

프로그래밍에서의 추상화

많은 개발자는 프로그래밍에서 하는 모든 일이 추상화라는 것을 종종 잊어버린다.

  • Number를 입력하면 이는 내부적으로 0과 1이라는 복잡한 형식으로 표현
  • String을 입력하면 모든 문자가 UTF-8과 같은 복잡한 형식으로 표현

추상화를 설계한다는 것은 단순하게 모듈 또는 라이브러리로 분리한다는 의미가 아니다.

  • 함수를 정의할 때는 그 구현을 함수의 시그니처 뒤에 숨기게 되는데, 이것이 바로 추상화 이다.

추상화와 자동차 (car metaphor)

자동차를 운전할 때 모든 요소들이 어떻게 작동해야하는지 이해할 필요가 없다. 자동차를 조종하는 인터페이스(핸들, 페달 등)를 사용하는 방법만 알면 된다. 이는 자동차의 종류와 크게 관계가 없다.
프로그래밍에서는 다음과 같은 목적으로 추상화를 사용한다.

  • 복잡성 숨기기
    • (3장 재사용성)공통 로직과 공통 알고리즘을 표현하기 위해서 functions, classes, delegates를 추출하는 것이 중요하다.
  • 코드를 체계화
    • (item 26: 함수 내부의 추상화 레벨을 통일하라)
  • 만드는 사람에게 변화의 자유를 주기 위해
    • (item 27: 변화로부터 코드를 보호하려면 추상화를 사용하라)

item 26. 함수 내부의 추상화 레벨을 통일하라

추상화 계층이 잘 분리되면 무엇이 좋을까? -> 해당 계층만 생각하면 된다.
어떤 계층에서 작업할 때 그 아래의 계층은 이미 완성되어 있으므로 전체를 이해할 필요가 없어진다.

추상화 레벨

  • 추상화 레벨은 높은 레벨로 갈수록 물리 장치로부터 멀어진다.
  • 높은 레벨일수록 걱정해야 하는 세부 내용들이 적어진다.
    • ex. C에서는 메모리 관리를 직접하지만, Java는 GC가 자동으로 메모리 관리를 해주기에 메모리 사용을 최적화하는 것이 어렵다.

추상화 레벨 통일

  • 코드도 추상화를 계층처럼 만들어서 사용할 수 있다.
  • 함수를 통해 높은 레벨과 낮은 레벨을 구분하여 사용할 수 있다. -> Single Level of Abstraction, SLA)원칙

추상화 이점

  • 재사용, 테스트 용이
  • 가독성 높음
  • 이해 쉬움 (필요한 경우에만 낮은 레벨의 코드를 살펴보면 된다.)

함수는 작아야 하며, 최소한의 책임만 가져야 한다. 함수가 복잡하면 일부분을 추출해서 추상화하는 것이 좋다.

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

  • 추상화를 구분하는 이유 : 서브시스템의 세부 사항을 숨김으로써 상호운영성(interoperability)과 플랫폼 독립성을 얻을 수 있음 (문제 중심으로 프로그래밍)
  • 모듈 시스템에서 입출력 모듈(낮은레벨모듈)과 비즈니스 로직(높은레벨모듈)을 잘 분리한다.

item 27. 변화로부터 코드를 보호하려면 추상화를 사용하라

추상화를 통해 변화로부터 코드를 보호할 수 있다.
추상화의 방식 3가지
1. 상수
- 리터럴은 아무것도 의미하지 않기에 코드에서 반복적으로 동작할 때 문제가 된다.
- 리터럴을 상수 프로퍼티로 변경하면 해당 값에 의미 있는 이름을 붙일 수 있고, 나중에 상수의 값을 쉽게 변경할 수 있다.
2. 함수
- 함수는 추상화를 표현하는 수단이며, 함수 시그니처는 이 함수가 어떤 추상화를 표현하고 있는지 알려쥑에 의미있는 이름을 붙이는 것이 중요하다.
- 함수는 상태가 없고, 시그니처를 변경하면 프로그램 전체에 큰 영향을 준다는 단점이 있다.
3. 클래스
- 클래스는 상태를 가질 수 있으며, 많은 함수를 가질 수 있다. (필드, 메서드)
- 의존성 주입 프레임워크를 이용하면 클래스 생성을 위임할 수도 있다.
- mock 객체를 활용해서 해당 클래스에 의존하는 다른 클래스의 기능을 테스트할 수 있다.
4. 인터페이스
- 코틀린은 거의 모든 것을 인터페이스로 표현
- ex. listOf은 List 인터페이스를 리턴한다.
- ex. collection 처리 함수는 Iterable 또는 Collection의 확장 함수로서, List, Map 등을 리턴
- ex. 프로퍼티 위임도 인터페이스. ReadOnlyProperty, ReadWriteProperty 뒤에 숨겨진다. 실질적인 클래스는 일반적으로 private이다. 함수 lazy는 Lazy 인터페이스를 리턴
- 인터페이스 뒤에 객체를 숨겨 구현을 추상화하고 사용자가 추상화된 것에만 의존하도록 만들어서 결합을 줄인다.
- 테스트 시 모킹보다 간단하게 인터페이스 페이킹을 사용할 수 있다.

추상화의 문제

  • 추상화는 거의 무한하게 할 수 있다. 그러나 너무 많은 것을 숨기면 결과를 이해하기 어려워진다.
  • 추상화를 이해하려면 예제(단위테스트, 문서)를 잘 살펴보자
    따라서, 추상화가 너무 많지도 적지도 않게 균형을 잘 유지해야하고, 팀의 크기나 경험, 프로젝트의 크기 feature set, 도메인 지식 등에 따라 추상화 정도를 조절해야 한다.

item 28. API 안정성을 확인하라

세상에 있는 모든 자동차의 운전 방법이 다르다면, 자동차를 운전하기 전에 운전 방법을 배워야 할 것이다. 이처럼 일시적으로만 사용되는 인터페이스를 배우는 것은 재사용할 수 없기에 의미 없는 일이다.

안정적이고 최대한 표준적인 API를 사용해야 한다.

  • 라이브러리의 작은 변경이 이를 활용하는 다른 코드에 영향을 미치기 때문
  • 사용자가 새로운 API를 배워야 하기 때문

좋은 API를 한번에 설계할 수 없다. 점진적으로 개선해나가야하기에 변경이 필요하다.

API가 불안정하다면, 명확하게 알려주어야 한다.

  • 시멘틱 버저닝(Semantic Versioning)
    • MAJOR : 호환되지 않는 수준의 API 변경
    • MINOR : 이전 변경과 호환되는 기능 추가
    • PATCH : 간단한 버그 수정
      -> MAJOR.MINOR.PATCH 형태로 붙이고, 1씩 증가시키는 형태로 버저닝
  • 안정적인 API에 새로운 요소 추가 시 어노테이션 이용하기
    • @Experimental: 안정적이지 않음
    • @Depreated: 더 이상 사용되지 않음 (단, 사용자가 적응할 시간을 충분히 주자.)
    • ReplaceWith: 직접적인 대안 제시

item 29. 외부 API를 wrap해서 사용하라

많은 프로젝트가 잠재적으로 불안정하다고 판단되는 외부 라이브러리 API를 wrap해서 사용한다.
API wrapping 장점과 자유

  • 문제가 있다면 wrapper만 수정. API 변경에 쉽게 대응 간으
  • 프로젝트 스타일에 맞춰 API 형태 조정 가능
  • 특정 라이브러리에서 문제 발생 시 wrapper를 수정해서 다른 라이브러리를 사용하도록 코드를 쉽게 변경
  • 필요한 경우 쉽게 동작을 추가하거나 수정
    API wrapping 단점
  • wrapper를 매번 따로 정의
  • 새로운 개발자가 작업할 때 wrapper의 내용 이해 필요
  • 내부에서 사용하기에 문제가 생겨도 외부에 질문할 수 없음

item 30. 요소의 가시성을 최소화하라

API 설계 시 가능한 한 간결한 API를 선호한다.
작은 인터페이스의 장점

  • 클래스 이해하고 배우기 쉽다.
  • 유지보수, 테스트 용이
  • 변경이 일어날 경우 기존의 것을 숨기는 것보다 새로운 것을 노출하는 것이 더 쉽다.
  • 가시성과 관련된 제한을 변경하는 것은 더 어렵기에 변경을 신중하게 고려해야 하며, 변경할 경우에는 대체재를 제공해야 한다.
  • 기존에 제공하던 API 가시성을 축소하면 여러 사용자가 분노할 수 있다. 처음에는 작은 API로서 개발하도록 강제하는 것이 좋다.
  • 구체 접근자의 가시성을 제한해서 모든 프로퍼티를 캡슐화하는 것이 좋다. 접근제어자를 잘 활용해야 외부에서의 임의 수정을 최소화할 수 있다.
// 외부에서 set 접근 가능한 경우
var myField: Int = 0

// 외부에서 set 접근 불가한 경우
var myField: Int = 0
  private set

결론

  • 접근자의 가시성을 제한해서 모든 프로퍼티를 캡슐화하는것이 좋다.
  • 가시성 제한될수록 클래스의 변경을 쉽게 추적 가능
  • 프로퍼티의 상태를 더 쉽게 이해할 수 있다.
  • 동시성(concurrency)을 처리할 때 중요
    가시성 한정자 사용하기
  • 클래스 멤버
    • public : 어디에서나 접근 가능
    • private : 클래스 내부에서만
    • protected : 클래스와 서브 클래스 내부에서만
    • internal : 모듈 내부에서만
  • 톱레벨 요소
    • public : 어디에서나 접근 가능
    • private : 같은 파일 내부에서만
    • internal : 모듈 내부에서만

모듈과 패키지 혼동X! 의미가 전혀 다르다.

  • 코틀린에서 모듈 : 함께 컴파일되는 코틀린 소스를 의미 (ex. radle source set, maven project, intellij idea module, ant file set)
  • 만약 모듈이 다른 모듈에 의해서 사용될 가능성이 있다면 internal로 요소 숨김. 요소가 상속을 위해 설계되어 있고 동일한 파일/클래스에서만 사용하게 만들고 싶다면 private 사용
  • DTO는 적용하지 않는 것이 좋다. 데이터를 저장하도록 설계된 클래스는 숨길 이유가 없기 때문.

API를 상속할 때 override해서 가시성을 제한할 수는 없다. 이는 서브클래스가 슈퍼클래스로도 사용될 수 있기 때문 -> 상속보다 컴포지션을 선호하는 이유

item 31. 문서로 규약을 정의하라

함수가 무엇을 하는지 명확하게 설명하고 싶다면 KDoc 주석을 붙여주는 것이 좋다.
KDoc 형식

  • KDoc은 /*로 시작해 /으로 끝난다.
  • 첫 번째 부분은 요소에 대한 요약 설명
  • 두 번째 부분은 상세 설명
  • 이어지는 줄은 태그와의 조합
    주요 태그
  • @param 함수 파라미터 또는 클래스, 프로퍼티, 함수 타입 파라미터를 문서화
  • @return 함수의 리턴 값을 문서화
  • @throws 메서드 내부에서 발생할 수 있는 예외에 대한 문서화
  • @see 특정한 클래스 또는 메서드에 대한 링크 추가. 문서 내용 중간에서 링크가 필요하다면 []로 대체 가능

규약을 설정하지 않는다면? 클래스를 사용하는 사람은 스스로 할 수 있는 것과 없는 것을 모르므로, 구현의 세부적인 정보에 의존하게 된다. -> 원래 사용 목적과 의도를 모르기에 구현을 망치게 된다.

규약 정의 방법

  • 이름
  • 주석과 문서
  • 타입

클래스가 어떤 동작을 할 것이라 예측되면, 그 서브클래스도 이를 보장해야 한다. -> 리스코프 치환 원칙
S가 T의 서브타입이라면, 변도의 변경이 없어도 T 타입 객체를 S 타입 객체로 대체할 수 있어야 한다.

item 32. 추상화 규약을 지켜라

규약은 보증(warranty)와 같다. 규약을 위반하면 코드가 작동을 멈췄을 때 문제가 된다.
프로그램을 안정적으로 유지하고 싶다면 규약을 잘 지켜야 한다. 지켜지지 않을 경우 프로그램 동작에 문제가 생길 수 있다.

profile
🐥

0개의 댓글