Blog

이펙티브 코틀린(3)

이펙티브 코틀린(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 프로그램 개발