이펙티브 코틀린(3)
•
재사용성 (Resuability): 기존 시스템에 추가적인 기능을 덧붙이거나 수정하여, 기존 시스템을 그대로 사용할 수 있는 능력
3-1. knowledge 를 반복하여 사용하지 말라
모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을 만한 표현 양식을 가져야 한다
( Every piece of knowledge must have a single, unambiguous, authoritative representation within a system )
- 《실용주의 프로그래머(Pragmatic Programmer)》
knowledge 는 일반적으로 표현하는 ‘지식' 과 다르게 ‘의도적인 정보' 를 나타내는 개념이다
•
프로젝트에서 이미 있던 코드를 복사해서 붙여넣고 있다면, 무언가가 잘못된 것이다.
◦
◦
DRY 원칙 《실용주의 프로그래머》
▪
Don’t Repeat Yourself
◦
WET 안티패턴
▪
We Enjoy Typing, Waste Everyone’s Time or Write Everthing Twice
◦
SSOT
▪
Single Source of Truth
knowledge
•
프로젝트를 진행할 때 정의한 모든 것이 knowledge 이다.
◦
종류
▪
알고리즘의 작동 방식
▪
UI 의 형태
▪
우리가 원하는 결과
◦
표현 방식
▪
코드
▪
설정
▪
템플릿
•
프로그램에서 가장 중요한 2개의 knowledge
1.
로직 (logic)
•
프로그램이 어떤식으로 동작하는지와 프로그램이 어떻게 보이는지
•
시간이 지나면서 지속적으로 변함
2.
공통 알고리즘 (common algorithm)
•
원하는 동작을 하기 위한 알고리즘
•
최적화를 하거나, 같은 카테고리의 더 빠른 알고리즘으로 변경할 수 있지만 동작은 크게 변하지 않음
모든 것은 변화한다
•
변화는 우리가 예상하지 못한 곳에서 일어난다
◦
UI 디자인과 기술 표준 등은 훨씬 빠르게 변화한다
•
변화하는 이유
1.
회사가 사용자의 요구 또는 습관을 더 많이 알게 되었다
2.
디자인 표준이 변화했다
3.
플랫폼, 라이브러리, 도구 등이 변화해서 이에 대응해야 한다
•
변화할 때 가장 큰 적은 knowledge 가 반복되어 있는 부분이다
◦
knowledge 반복은 프로젝트의 확장성 (scalable) 을 막고, 쉽게 깨지게 (fragile) 만든다
•
반복적인 knowledge 를 줄일 수 있는 다양한 도구들과 기능들을 활용하는게 좋다
◦
e.g.) Hibernate(ORM), Exposed(DAO)
언제 코드를 반복해도 될까?
한가지 유용한 휴리스틱!
→ 비즈니스 규칙이 다른 곳(source) 에서 왔는지 확인하는 방법이 있다
→ 비즈니스 규칙이 다른 곳에서 왔다면, 독립적으로 변경될 가능성이 높다
•
knowledge 가 비슷해보이지만 실질적으로 다른 knowledge 인 경우는 반복을 줄이면 안된다
◦
신중하지 못한 추출은 변경을 더 어렵게 만든다
•
두 코드가 같은 knowledge 를 나타내는지 다른 knowledge 를 나타내는지는 함께 변경될 가능성이 높은가? 따로 변경될 가능성이 높은가? 에 대한 질문으로 어느정도 결정할 수 있다
단일 책임 원칙 (Single Responsibility Principle, SRP)
단일 책임 원칙이란?
클래스를 변경하는 이유는 단 한 가지 여야 한다. (A class should have only one reason to change)
- 로버트 C. 마틴(Robert C. Martin), 《클린 아키텍처(Clean Architecture)》
•
두 액터(actor)가 같은 클래스를 변경하는 일은 없어야 된다.《클린 아키텍처(Clean Architecture)》
◦
액터는 변화를 만들어 내는 존재 (source of change) 를 의미한다
◦
액터는 서로의 업무와 분야에 대해서 잘 모르는 개발자들로 비유된다
•
서로 다른곳에서 사용하는 knowledge 는 독립적으로 변경할 가능성이 높다
•
다른 knowledge 는 분리해 두는 것이 좋다
◦
그렇지 않으면, 재사용해서는 안되는 부분을 재사용하려고 할 수 있기 때문이다
예시 코드
정리
•
공통 knowledge 가 있다면, 이를 추출해서 변화에 대비해야 한다
•
여러 요소에 비슷한 부분이 있는 경우, 추출하는 것이 좋다
•
의도하지 않은 수정을 피하거나, 다른곳에서 조작하는 부분이 있다면 분리해서 사용하는것이 좋다
•
비슷해 보이는 코드는 모두 추출하려는 경향이 있지만, 극단적인 것은 언제나 좋지 않다
3-2. 일반적인 알고리즘을 반복해서 구현하지 말라
여기서 알고리즘이란?
특정 프로젝트에 국한된 것 (비지니스 로직을 포함하는 것이 아닌 것)이 아니라, 수학적인 연산, 수집 처리처럼 별도의 모듈 또는 라이브러리로 분리할 수 있는 부분을 의미한다
•
이미 구현되어 있는 함수를 사용하는것이 좋다
◦
코드 작성 속도가 빨라진다
◦
구현체 코드를 따로 읽지 않아도, 함수의 이름 등만 보고도 무엇을 하는지 확실하게 알 수 있다
◦
직접 구현할 때 발생할 수 있는 실수를 줄일 수 있다
◦
작성자가 한 번만 최적화를 잘하면, 함수를 활용하는 모든 곳이 최적화의 혜택을 받을 수 있다
◦
직접 구현하지 말고 있는거 잘 구현되어 있는걸 사용합시다... 제발..
예시 코드
표준 라이브러리
•
일반적인 알고리즘은 대부분 다른 사람들이 정의해 놓았다. 그중에서 가장 대표적인 라이브러리가 바로 stdlib 이다.
◦
확장 함수를 활용해서 만들어진 굉장히 거대한 유틸리티 라이브러리이다
•
내부적으로 제공해주는 기능들을 잘 사용하면 훨씬 좋은 코드를 작성할 수 있다
예시 코드
나만의 유틸리티 구현하기
•
상황에 따라서 표준 라이브러리에 없는 알고리즘이 필요한 경우 범용 유틸리티 함수 (universal utility function) 으로 정의하는 것이 좋다.
◦
여러 번 사용되지 않는다고 해도, 일반적으로 잘 알려진 수학적 개념이고 함수명을 통해 어떠한 기능인지 대부분의 개발자들이 예측이 가능하기 때문에 범용 유틸리티 함수로 정의해두는것이 좋다.
예시 코드
•
동일한 결과를 얻는 함수를 여러 번 만드는 것은 잘못된 일이다.
◦
모든 함수는 테스트되어야 한다
◦
개발자들이 이 함수의 존재를 알 수 있어야 한다
◦
유지보수되어야 한다
◦
함수를 만들 때는 이러한 비용이 들어갈 수 있다는 것을 전제해야 한다.
•
많이 사용되는 알고리즘을 추출하는 방법으로는 top-level function, property delegation, class 등이 있다
•
확장 함수의 장점
◦
함수는 상태를 유지하지 않으므로 행위를 나타내기 좋다
▪
Side-Effect 가 없는 경우에는 더 좋다
◦
top-level function 과 비교해서 확장 함수는 구체적인 타입이 있는 객체에만 사용을 제한할 수 있다
◦
수정할 객체를 아규먼트로 전달받아 사용하는 것보다는 확장 리시버로 사용하는 것이 가독성 측면에서 좋다
◦
확장 함수는 객체에 정의한 함수보다 객체를 사용할 때, 자동 완성 기능 등으로 제안이 이루어지므로 쉽게 찾을 수 있다
정리
•
일반적인 알고리즘을 반복해서 만들이 말아야 한다
◦
대부분 표준 라이브러리에 정의되어 있을 가능성이 높다
•
표준 라이브러리에 없는 일반적인 알고리즘이나 특정 알고리즘을 반복해서 사용해야 하는 경우에는 프로젝트 내부에 직접 정의해야 한다
◦
일반적으로 이런 알고리즘들은 확장 함수로 정의하는것이 좋다
3-3. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라
•
코틀린은 코드 재사용과 관련해서 프로퍼티 위임(Property Delegation) 이라는 기능을 제공한다
◦
대표적인 예로 지연 프로퍼티가 있다.
•
지연 프로퍼티 (lazy property)
◦
이후에 처음 사용하는 요청이 들어올 때 초기화 되는 프로퍼티를 의미한다
◦
필요할 때마다 이를 복잡하게 구현해야 하지만, 코틀린의 stdlib 는 lazy 프로퍼티 패턴을 쉽게 구현할 수 있게 lazy 함수를 제공한다
예시 코드
•
Observable 패턴
◦
프로퍼티의 변경 사항을 감지해서 처리하고 싶은 경우 사용한다
◦
stdlib 의 observable delegate 를 기반으로 간단하게 구현할 수 있다
예시 코드
•
일반적으로 프로퍼티 위임 메커니즘을 활용하면 다양한 패턴들을 만들 수 있다
◦
자바 등에서는 어노테이션을 많이 활용해야 하지만, 코틀린은 프로퍼티 위임을 사용해서 간단하고 type-safe 하게 구현할 수 있다
예시 코드
•
프로퍼티 위임을 위해서 프로퍼티 델리게이트를 직접 추가할 수 있다
◦
val 의 경우 getValue 를, var 의 경우 getValue, setValue 연산을 정의해야 한다.
◦
프로퍼티가 톱레벨에서 사용될 때는 this 대신 null 로 변경된다
◦
프로퍼티에 대한 레퍼런스는 이름, 어노테이션과 관련된 정보 등을 얻을 때 사용된다
◦
컨텍스트는 함수가 어떤 위치에서 사용되는지와 관련된 정보를 제공해준다
◦
getValue, setValue 메섣가 여러 개 있어도 컨텍스트를 활용하므로 상황에 따라 적절한 메서드가 선택된다
예시 코드 (1)
예시 코드 (2)
•
확장 함수를 이용해서도 프로퍼티 위임을 구현할 수 있다
예시 코드
•
코틀린에서 유용하게 사용되는 프로퍼티 델리게이트
◦
lazy
◦
Delegates.observable
◦
Delegates.vetoable
◦
Deleagtes.notNull
정리
•
프로퍼티 델리게이트는 프로퍼티와 관련된 다양한 조작을 할 수 있으며, 컨텍스트와 관련된 대부분이 정보를 갖고 있다
•
다양한 프로퍼티의 동작을 추출해서 재사용 할 수 있다
3-4. 일반적인 알고리즘을 구현할 때 제네릭을 사용하라
•
타입 아규먼트를 사용하면 함수에 타입을 전달할 수 있다
•
타입 아규먼트를 사용하는 함수(즉, 타입 파라미터를 갖는 함수) 를 제네릭 함수 (Generic Function) 라고 부른다.
◦
타입 파라미터를 사용하면 개발자는 여러 가지 이득을 얻지만, 프로그램은 실질적인 이득이 없다.
▪
JVM 바이트 코드의 제한으로 인해, 컴파일 시점에 제네릭과 관련된 정보는 사라진다
▪
런타임 때 어떤 이득도 얻을 수 없다
예시 코드
•
제네릭은 기본적으로 List<String> 또는 Set<User> 처럼 구체적인 타입으로 컬렉션을 만들 수 있게 클래스와 인터페이스에 도입된 기능이다
제네릭 제한
•
타입 파라미터의 중요한 기능 중 하나는 구체적인 타입의 서브타입만 사용하게 타입을 제한하는 것이다
◦
제네릭을 선언할 때 콜론 뒤에 슈퍼 타입을 넣어서 제한을 걸 수 있다
예시 코드
•
타입에 제한이 걸리므로, 내부에서 해당 타입이 제공하는 메서드를 사용할 수 있다
•
많이 사용하는 제한으로는 Any 가 있다
◦
이는 nullable 이 아닌 타입을 의미한다
예시 코드
•
둘 이상의 타입 제한을 걸 수 있다
예시 코드
정리
•
타입 파라미터는 구체 자료형 (concrete type) 의 서브타입을 제한할 수 있다
•
특정 자료형이 제공하는 메서드를 안전하게 사요할 수 있다
3-5. 타입 파라미터의 섀도잉을 피해라
섀도잉 (shadowing): 프로퍼티와 파라미터가 같은 이름을 가져서 지역 파라미터가 외부 스코프에 있는 프로퍼티를 가르키는 현상
•
섀도잉 현상은 클래스 타입 파라미터와 함수 타입 파라미터 사이에서도 발생할 수 있다
◦
개발자가 제네릭을 제대로 이해하지 못할 때, 이와 관련된 다양한 문제들이 발생한다
예시 코드
예시 코드
정리
•
타입 파라미터 섀도잉이 발생한 코드는 이해하기 어려울 수 있고, 예상치 못한 문제가 발생할 수 있으므로 피해야한다.
3-6. 제네릭 타입과 variance 한정자를 활용하라
•
Generic 이란?
◦
Class 또는 Method 에서 매개변수에사용되는 자료형의 정의를 객체 생성 시 정하게 하여 타입에 대한 안정성을 높이는 방법
•
Generic 의 장점
1.
Type Casting is vitable
2.
Type Safety
3.
Compile time safety
•
Type Bound: Genric 에서 사용할 수 있는 타입의 범위를 지정
•
Type Bound (Parameter Variance)의 종류
◦
불변성 (무공변성, invariant)
◦
공변성 (covariant)
◦
반공변성 (contravariant)
무공변성 (invariant)
•
상속 관계와 상관없이 자신의 타입만 허용한다.
•
Kotlin 에서 따로 지정해주지 않으면 기본적으로 모든 제네릭 클래스는 무공변이다.
예시 코드
공변성 (covariant)
•
자기 자신과 자식의 객체를 허용한다.
•
Kotlin 는 out 한정자를 사용한다.
예시 코드
반공변성 (contravariant)
•
자기 자신과 부모의 객체만 허용한다.
•
Kotlin 에서는 in 한정자를 사용한다.
예시 코드
•
코틀린의 함수 타입의 모든 파라미터 타입은 contravarriant 이다
◦
모든 리턴 타입은 covariant 이다
◦
함수 타입을 사용할 때는 자동으로 variacne 한정자가 사용된다
variance 한정자의 안정성
•
자바의 배열은 covariant 이다.
예시 코드 (런타임 오류)
•
Kotlin 은 public in 한정자 위치에 covariant 타입 파라미터가 오는 것을 금지한다
예시 코드
•
접근 제어자를 private 로 제한하면 오류가 발생하지 않는다.
◦
객채 내부에서는 업캐스트 객체에 covariant 를 사용할 수 없기 때문이다
예시 코드
•
convariant 는 public out 한정자 위치에서도 안전하므로 따로 제한하지 않는다
◦
안정성의 이유로 생성되거나 노출되는 타입에만 covariant 를 사용한다
◦
일반적으로 producer 또는 immutable 데이터 홀더에 많이 사용된다
예시 코드
•
out (covariant) 위치는 암묵적인 업캐스팅을 허용한다
예시 코드
•
Kotlin 은 contravariant 타입 파라미터를 Public out 한정자 위치에 사용하는것을 금지하고 있다.
variance 한정자의 위치
•
선언 부분 ( 일반적으로 이 위치에서 사용 )
◦
클래스와 인터페이스 선언에 한정자가 적용
예시 코드
•
클래스와 인터페이스를 활용하는 위치
◦
특정한 변수에만 variance 한정자가 적용
예시 코드
•
특정 인스턴스에만 variance 한정자를 적용해야 할 경우
예시 코드
정리
•
타입 파라미터의 기본적인 variance 의 동작은 invariant 이다
•
out 한정자는 타입 파라미터를 covariant 하게 만든다
•
in 한정자는 타입 파라미터를 contravariant 하게 만든다
•
List 와 Set 의 타입 파라미터는 covariant 이다
•
Map 에서 값의 타입 파라미터는 covariant 이다
•
Array, MutableList, MutableSet, MutableMap 의 타입 파라미터는 invariant 이다
•
함수 타입의 파라미터 타입은 contravariant 이다. 리턴 타입은 covariant 이다
•
리턴만 되는 타입에는 covariant 를 사용한다
•
허용만 되는 타입에는 contravariant 를 사용한다
3-7. 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라
•
코틀린으로 멀티 플랫폼 개발이 가능하다
•
가능한 공통 모듈들을 추출해서 사용하면 다양한 플랫폼에서도 손쉽게 사용할 수 있다
•
예시
◦
Kotlin/JVM 을 사용한 백엔드 개발: Spring, Ktor 등
◦
Kotlin/JS 를 사용한 웹 사이트 개발 - React 등
◦
Kotlin/JVM 을 사용한 안드로이드 개발 - Android SDK 등
◦
Kotlin/Native 를 통해 Object-C/Swift 로 iOS 프레임워크 개발
◦
Kotlin/JVM 을 사용한 테스크톱 개발 - TornadoFX 등
◦
Kotlin/Native 를 사용한 라즈베리파이, 리눅스, macOS 프로그램 개발