Blog

이펙티브 코틀린(2)

이펙티브 코틀린(2)

코틀린은 return, throw 모두 Nothing 을 반환하게 설계되어있다.

1-8. 적절하게 null 을 처리하라

방어적 프로그래밍 (Defensive Programming): 모든 가능성을 올바른 방식으로 처리하는 방법 → 상황을 처리할 수 있는 올바른 방법이 있을 때 굉장히 좋다. 공격적 프로그래밍 (Offensive Programming): 예상하지 못한 상황이 발생했을 때, 문제를 개발자에게 알려서 수정하게 만드는 방법 → require, check, assert 가 공격적 프로그래밍을 위한 도구이다. 공격적 과 방어적 이라는 이름 때문에 둘이 충돌하는것처럼 보이지만, 안전한 코드 작성을 위해 모두 필요한 요소이다.
null값이 부족하다 (lack of value) 를 의미한다
Property 가 null 이라는 것은 값이 설정되지 않거나 제거되었다는것을 나타낸다
null 은 최대한 명확한 의미를 갖고 있는것이 좋다
예시 코드

null 을 처리하는 방법

Safe Call
Safe Call 은 ?. 을 사용하여 처리할 수 있다
변수가 null 이 아니면 뒤에 선언된 변수 혹은 함수를 호출한다.
예시 코드
Smart Casting
변수가 null 이 아님을 체크하고 함수를 호출한다
코틀린의 규약 기능 (contracts feature) 을 지원한다,.
예시 코드
Elvis 연산자
오른쪽에 있는 return 또는 throw 를 포함한 모든 표현식이 허용된다
예시 코드

오류 throw 하기

예상하지 못한 케이스에서는 오류를 강제적으로 발생시켜주는것이 좋다.
오류를 강제로 발생시킬 때는 throw, not-null assertion, requireNotNull, checkNotNull 등을 활용한다.
예시 코드

not-null assertion 문제

not-null assertion 을 사용하면 자바에서 nullable 을 처리할 때 발생할 수 있는 문제가 동일하게 발생한다.
어떤 대상이 null 이 아니라고 생각하고 다루면, NPE (Null Point Exception) 이 발생한다.
not-null assertion 을 사용하면 예외가 발생했을 때 어떤 설명도 없는 제네릭 예외 (generic exception) 이 발생한다.
변수를 먼저 선언해놓고 초기화하는 경우에 not-null assertion 을 사용하는 경우가 있지만, 이 방법은 좋은 방법이 아니다
property 를 사용할 때는 계속 unpack 을 해야한다.
해당 property 가 이후에 의미 있는 null 값을 가질 가능성을 완전히 배제해버린다.
올바른 방법은 lateinit 혹은 Delegates.notNull 을 사용하는것이다.
명시적으로 오류는 NPE 보다는 훨씬 더 많은 정보를 제공해줄 수 있기 때문에 not-null assertion 을 사용하는 것보다 훨씬 좋다

의미 없는 nullability 피하기

nullability 는 처리하는 비용이 추가되기 때문에 필요한 경우가 아니라면 nullability 자체를 피하는것이 좋다.
nullability 피하는 방법
Class 에서 nullability 에 따라 여러 함수를 만들어서 제공한다
e.g) get , getOrNull
어떤 값이 Class 생성 이후에 확실히 설정된다는 보장이 있으면 lateinit 혹은 Delegates.notNull 을 사용하라
Collection 을 사용할 때 null 대신 Empty Collection 을 반환하라
nullable enum 과 None enum 은 완전히 다른 의미이다.

lateinit property, notNull Delegate

Class 가 생성 중 초기화 할 수 없는 Property 가 있으면 나중에 속성을 초기화할 수 있는 lateinit 한정자를 사용한다.
만약 초기화 전에 값을 사용하려고 하면 예외가 발생한다.
lateinit vs nullable
not-null assertion 연산자로 unpack 하지 않아도 된다
이후에 null 을 사용하고 싶을 때, nullable 로 만들 수 있다
Property 가 초기화 된 이후에는 초기화 되지 않은 상태로 돌아갈 수 없다
lateinit 은 Property 를 처음 사용하기 전에 반드시 초기화될거라고 예상되는 상황에 활용한다
e.g) Class 에 LifeCycle 이 명확한 경우
JVM 에서 Int, Long, Double, Boolean 과 같은 Primitive Type 으로 연결된 타입은 Property 를 초기화 해야 하기 때문에 lateinit 을 사용할 수 없다
이러한 경우에는 Delegates.notNull 을 사용한다.
예시 코드 (lateinit)
예시코드 (Delegates.notNull)
예시 코드 (Property Delegation)

1-9. use 를 사용하여 리소스를 닫아라

더 이상 필요하지 않을 때 close 메소드를 호출해서 명시적으로 종료해야 하는 경우가 있다
e.g) InputStream, OutputStream, java.sql.Connection, java.io.Reader, java.new.Socket, java.util.Scanner
이러한 리소스들은 AutoCloseable 을 상속받는 Closeable 인터페이스를 구현하고 있다.
최종적으로 리소스에 대한 Reference 가 없어질 때 Garbage Collector 가 처리한다.
하지만, 굉장히 느리며 처리되기 전까지 리소스 유지 비용이 발생한다
더 이상 필요하지 않다면 명시적으로 close 메소드를 호출해 주는 것이 좋다.
예시 코드 (try-finally)
예시 코드 (use method)
예시 코드 (lambda)
예시 코드 (useLines)

정리

use 를 사용하면 Closeable, AutoCloseable을 구현한 객체를 쉽고 안전하게 처리할 수 있다
파일을 처리할 때는 파일을 한 줄씩 읽어 들이는 useLines 를 사용하는것이 좋다

1-10. 단위 테스트를 만들어라

단위 테스트는 개발자가 작성한 코드가 올바르게 동작한다는것을 보장할 수 있도록 해준다
제대로 동작하는지를 빠르게 피드백 해주기 때문에 개발하는 동안에 큰 도움을 준다
단위 테스트는 다음과 같은 내용을 확인한다
일반적인 유스 케이스 (happy path)
사용될거라고 예상되는 일반적인 케이스
일반적인 오류 케이스와 잠재적인 문제
제대로 동작하지 않을 거라고 예상되는 일반적인 부분
엣지케이스와 잘못된 아규먼트
테스트는 계속해서 축적되기 때문에 회귀 테스트도 쉽다.
회귀 테스트 (Regression Test): 기존에 누적된 테스트 케이스를 기반으로 전체 또는 부분을 반복적으로 테스트 하는 것

TDD (Test Driven Development)

개발 전에 테스트를 먼저 작성하고, 테스트를 통과시키는 것을 목적으로 하나하나 구현해 나가는 방식
TDD 는 3단계로 이루어져 있으며, 이를 반복적으로 수행한다
RED
단위테스트를 작성한다
GREEN
처음에는 코드가 작성되어 있지 않으므로 모든 단위 테스트가 실패한다.
테스트를 통과할 수 있게 코드를 작성한다
REFACTOR
중복 코드 제거, 일반화 등의 리팩토링을 수행한다

테스트의 장﹒단점

  장점
테스트가 잘 된 요소는 신뢰할 수 있다
리팩토링에 용이하다
수동으로 테스트하는 것보다 단위 테스트로 확인하는것이 훨씬 빠르다
  단점
단위 테스트를 작성하는 비용이 발생한다
장기적으로 좋은 단위 테스트는 디버깅 시간과 버그를 찾는데 소모되는 시간을 줄여준다
테스트에 적합한 코드로 조정해야 한다
좋은 단위 테스트를 작성하는것은 어렵다

단위 테스트를 작성해야 하는 부분

복잡한 로직
계속 수정이 일어나고 리팩토링이 일어날 수 있는 부분
비지니스 로직 부분
공용 API 부분
문제가 자주 발생하는 부분
수정해야 하는 프로덕션 버그

정리

프로그램이 올바르게 작동해야 한다는 것을 최우선적인 목표로 둔다
테스트 중에서 개발 과정에서 가장 효율적으로 활용할 수 있는 테스트는 단위 테스트이다.
비지니스 애플리케이션 등에서는 최소한 몇 개라도 단위 테스트가 꼭 필요하다
컴퓨터가 인식할 수 있는 코드는 바보라도 작성할 수 있지만, 인간이 이해할 수 있는 코드는 실력 있는 프로그래머만 작성할 수 있다 - 마틴 파울러(Martin Fowler), 《리팩터링》
코틀린은 간결성을 목표로 설계된 프로그래밍 언어가 아니라, 가독성 (readability) 을 좋게 하는 데 목표를 두고 설계된 프로그래밍 언어이다.
코틀린은 간결성은 가독성을 목표로 두고, 자주 쓰이는 반복적인 코드를 짧게 쓸 수 있게 했기 때문에 발생한 부가적인 효과일 뿐이다.

2-1. 가독성을 목표로 설계하라

개발자가 코드를 작성하는 데는 1분 걸리지만, 이를 읽는 데는 10분이 걸린다 - 로버트 마틴 (Robert C. Martin), 《클린 코드 (Clean Code)》

인식 부하 감소

가독성은 사람마다 다르게 느낄 수 있지만, 일반적으로 많은 사람의 경험인식에 대한 과학으로 만들어진 어느정도 규칙이 있다.
예시 코드
가독성이란 코드를 읽고 얼마나 빠르게 이해할 수 있는지를 의미한다
우리의 뇌가 얼마나 많은 관용구(구조, 함수, 패턴)에 익숙해져 있는지에 따라서 다르다
우리의 뇌는 패턴을 인식하고, 패턴을 기반으로 프로그램의 작동 방식을 이해한다
사용 빈도가 적은 관용구는 오히려 코드를 복잡하게 만든다
숙련된 개발자만을 위한 코드는 결코 좋은 코드가 아니다.
뇌는 기본적으로 짧은 코드를 빠르게 읽을 수 있겠지만, 익숙한 코드는 더 빠르게 읽을 수 있다.
예시 코드

극단적이 되지 않기

위의 let 으로 인해서 예상하지 못한 결과가 나올 수 있지만, 그렇다고 let 은 절대로 쓰면 안된다 라는 식의 극단적인 선택은 하지 않아야 한다.
let 은 좋은 코드를 만들기 위해서 다양하게 활용되는 인기있는 관용구이다.
가변 프로퍼티는 쓰레드와 관련된 문제를 발생시킬 수 있으므로, 스마트 캐스팅이 불가능하다.
이를 해결하는 방법은 여러가지가 있지만, 일반적으로 안전 호출 let 을 사용하다.
예시 코드
let 을 사용하는 경우
연산을 argument 처리 후로 이동시킬 때
예시 코드
데코레이터를 사용해서 객체를 랩할 떄
비용을 지불할만한 가치가 있는 코드들은 사용해도 괜찮다
정당한 이유 없이 복잡성을 추가하는 코드들에서는 문제가 될 수 있다.
물론, 어떠한 코드가 비용을 지붊할 만한 코드인지 아닌지는 논란이 있을 수 있지만 균형을 맞추는것이 중요하다.
예시 코드

컨벤션

연산자는 의미에 맞게 사용해야 한다
내장으로 제공되는 기능들은 다시 만들지 않고 사용하는것이 좋다
  예시 코드

2-2. 연산자 오버로드를 할 때는 의미에 맞게 사용하라

연산자 오버로딩은 굉장히 강력한 기능이지만, 함부로 사용하면 안된다
코틀린의 모든 연산자는 구체적인 이름을 가진 함수에 대한 별칭이다. → 연산자 대신 함수로도 호출할 수 있다
코틀린에서 각 연산자의 의미는 항상 같게 유지된다.
코틀린에서 연산자에 대응되는 함수 이름
  예시 코드

분명하지 않은 경우

관례를 충족하는지 아니지 확실하지 않은 경우 infix 를 활용한 확장 함수를 사용하거나 top-level function 를 사용하는것이 좋다
top-level function: 클래스 또는 다른 대상 내부에 있지 않고, 가장 외부에 있는 함수를 의미
예시 코드

규칙을 무시해도 되는 경우

도메인 특화 언어 (Domain Specific Language, DSL) 를 설계할 때는 무시해도 된다.
예시 코드

정리

연산자 오버로딩은 그 이름의 의미에 맞게 사용해야 한다
연산자 의미가 명확하지 않다면 연산자 오버로딩은 사용하지 않는것이 좋다 → 대신 일반 함수를 사용하기 바란다
연산자 같은 형태로 사용하고 싶다면 infix 확장 함수 또는 top-level function 을 활용하는것이 좋다

2-3. Unit? 을 리턴하지 말라

Unit? 은 Unit 또는 null 이라는 값을 가질 수 있다.
Boolean 과 Unit? 타입은 서로 바꿔서 사용할 수 있다
예시 코드
Unit? 으로 boolean 을 표현하는것은 오해의 소지가 있으며, 예측하기 어려운 오류를 만들 수 있다
기본적으로 Unit? 을 반환하거나 이를 기반으로 연산하지 않는 것이 좋다

2-4. 변수 타입이 명확하지 않은 경우 확실하게 지정하라

코틀린은 개발자가 타입을 지정하지 않아도 알아서 타입을 지정해서 넣어주는 수준 높은 타입 추론 시스템을 갖추고 있다
이는 개발 시간을 줄여주고, 타입이 명확할 때 코드가 짧아지므로 가독성이 크게 향상된다
하지만, 유형이 명확하지 않을 때는 남용하지 않는것이 좋다.
코드를 읽으면서 함수 정의를 보며 타입을 확인하면 되지 않나? 라고 생각할 수 있지만, 이는 곧 가독성이 떨어짐을 의미한다.
가독성 향상 이외에 안전을 위해서도 타입을 지정하는것이 좋다.
예시 코드

2-5. 리시버를 명시적으로 참조하라

함수와 프로퍼티를 지역 또는 탑레벨 변수가 아닌 다른 리시버로부터 가져온다는것을 나타낼 때가 있다.
예시 코드 ( class method → this )
확장 리시버 (확장 메서드에서의 this) 를 명시적으로 참조하게 할 수 있다
예시 코드
스코프 내부에 둘 이상의 리시버가 있는 경우, 리시버를 명시적으로 나타내는게 좋다.
일반적으로 also 또는 let 을 사용하는 것이 nullable 값을 처리할 때 훨씬 좋은 선택지이다
어떤 리시버를 명시적으로 작성하게되면 코드를 안전하게 사용할 수 있을 뿐만 아니라 가독성도 향상된다.
예시 코드

DSL 마커

코틀린 DSL 을 사용할 때는 여러 리시버를 가진 요소들이 중첩되더라도, 리시버를 명시적으로 붙이지 않는다
DSL 은 원래 그렇게 사용하도록 설계되었다
기본적으로 모든 스코프에서 외부 스코프에 있는 리시버의 메서드를 사용할 수 있다
하지만, 이렇게 하면 코드에 문제가 발생할 수 있다
예시 코드
잘못된 사용을 막으려면, 암묵적으로 위부 리시버를 사용하는 것을 막는 DslMarker 라는 메타 어노테이션을 사용하면 된다.
예시 코드
DSL 마커는 가장 가까운 리시버만을 사용하게 하거나, 명시적으로 외부 리시버를 사용하지 못하게 할 때 활용할 수 있는 굉장히 중요한 메커니즘이다
DSL 설계에 따라서 사용 여부를 결정하는 것이 좋다

정리

코드를 짧게 적을 수 있다는 이유만으로 리시버를 제거하지 않아야 한다
여러개의 리시버가 있는 경우 명시적으로 적어주는게 좋다
명시적으로 리시버를 적어주면 가독성이 향상된다
DSL 에서 외부 스코프에 있는 리시버를 명시적으로 적도록 강제하고 싶으면 DslMarker 메타 어노테이션을 사용한다.

2-6. 프로퍼티는 동작이 아니라 상태를 나타내야 한다

코틀린의 프로퍼티와 자바의 필드는 비슷해보이지만 서로 완전히 다른 개념이다
둘 다 데이터를 저장한다는 점은 같으나, 프로퍼티에는 더 많은 기능들이 있다.
기본적으로 프로퍼티는 settger, getter 를 가질 수 있다
예시 코드
var 을 사용해서 만든 프로퍼티는 getter, setter 를 정의할 수 있으며, 이러한 프로퍼티를 파생 프로퍼티(derived property) 라고 부른다
코틀린의 모든 프로퍼티는 디폴트로 캡슐화되어있다
프로퍼티는 필드가 필요 없다. 오히려 개념적으로 접근자를 나타낸다
프로퍼티는 본질적으로 함수이다
코틀린은 인터페이스에도 프로퍼티를 정의할 수 있다
예시 코드
예시 코드
프로퍼티로 알고리즘의 동작을 나타내는것은 좋지 않기 때문에 함수로 구현하는것이 좋다
예시 코드
원칙적으로 프로퍼티는 상태를 나가내거나 설정하기 위한 목적으로만 사용하는것이 좋고, 다른 로직은 포함하지 않아야 한다
프로퍼티 대신 함수로 사용하는것이 좋은 경우
연산 비용이 높거나, 복잡도가 O(1) 보다 큰 경우
비즈니스 로직 (애플리케이션의 동작) 을 포함하는 경우
결정적이지 않은 경우
동일한 동작을 연속적으로 두 번 했는데 다른 값이 나오는 경우
변환의 경우
Int.toDouble() 와 같은 행위
getter 에서 프로퍼티의 상태 변경이 일어나야 하는 경우
상태를 추출/설정할 때는 프로퍼티를 사용해야 한다
예시 코드

2-7. 이름 있는 아규먼트를 사용하라

아규먼트의 의미가 명확하지 않은 경우에는 이름 있는 아규먼트 (named argument) 를 사용하는게 좋다
예시 코드
이름 있는 아규먼트의 장점
이름을 기반으로 값이 무엇을 나타내는지 알 수 있다
파라미터 입력 순서와 상관 없으므로 안전하다

디폴트 아규먼트

프로퍼티가 디폴트 아규먼트를 가질 경우, 항상 이름을 붙여서 사용하는것이 좋다
일반적으로 함수이름은 필수 파라미터들과 관련되어 있기 때문에 기본값을 갖는 옵션 파라미터 (optional paramter) 의 설명이 명확하지 않다

같은 타입의 파라미터가 많은 경우

파라미터가 모두 다른 타입이면 위치를 잘못 입력한 경우 에러가 발생하여 문제를 쉽게 찾을 수 있지만, 같은 타입일 경우 문제를 찾기 어렵다.
예시 코드

함수 타입 파라미터

일반적으로 함수 타입 파라미터는 마지막 위치에 배치하는 것이 좋다
모든 함수 타입 아규먼트는 이름 있는 아규먼트를 사용하는것이 훨씬 이해하기 쉽다
예시 코드

정리

이름 있는 아규먼트는 기본 값들을 생략할 때만 유용한것이 아니다.
이름 있는 아규먼트는 개발자가 코드를 읽을 때도 편리하게 활용되며, 코드의 안정성을 높일 수 있다
함수에 같은 타입의 파라미터가 여러개 있는 경우, 옵션 파라미터가 있는 경우에는 이름 있는 아규먼트를 사용하는게 좋다

2-8. 코딩 컨벤션을 지켜라

Coding Convensions 를 보면 알 수 있는 것처럼 코틀린은 굉장히 잘 정리된 코딩 컨벤션을 가지고 있다.
컨벤션을 지켜야 하는 이유
어떤 프로젝트를 접해도 쉽게 이해할 수 있다
다른 외부 개발자도 프로젝트의 코드를 쉽게 이해할 수 있다
다른 개발자도 코드의 작동 방식을 쉽게 추측할 수 있다
코드를 병합하고, 한 프로젝트의 코드 일부를 다른 코드로 이동하는것이 쉽다
코틀린 컨벤션 도구
자주 위반되는 규칙 ( 클래스와 함수의 형식 )
예시 코드
팀이 다른 규칙을 사용하기로 결정할 수 있지만, 프로젝트의 컨벤션은 반드시 지켜 주는 것이 좋다.
코딩 컨벤션을 확실하게 읽고, 정적 검사기 (static checker) 를 활용해서 프로젝트의 코딩 컨벤션 일관성을 유지해야 한다.