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