김동형수 개발기

이펙티브 코틀린 - 1부 1장 본문

책 스터디/[진행] 이펙티브 코틀린

이펙티브 코틀린 - 1부 1장

김동형수 2023. 10. 24. 10:01

1장 안정성

코틀린을 선택하는 이유는 안정성이다.
코틀린은 다양한 설계를 통해 잠재적인 오류를 줄여 준다.
크래시가 적으면 사용자와 개발자 모두에게 좋고, 상당한 비즈니스 가치를 제공한다.
이번 장의 기본 목적은 오류가 덜 발생하는 코드를 만드는 것이다.

아이템1: 가변성을 제한하라

읽고 쓸 수 있는 프로퍼티는 var 또는 mutable 객체를 사용하면 상태를 가질 수 있다.
가변성을 가지는 상태를 사용할 때 단점

  1. 프로그램을 이해하고 디버그하기 힘들어진다.
  2. 가변성이 있으면, 코드의 실행을 추론하기 어려워진다.
  3. 멀티스레드 프로그래밍일 때는 적절한 동기화가 필요하다.
  4. 테스트하기 어렵다.
  5. 상태 변경이 일어날 때, 이러한 변경을 다른 부분에 알려야 하는 경우가 있다.

가변성은 생각보다 단점이 많아서 이를 완전하게 제한하는 프로그래밍 언어도 있다. 그것은 순수 함수형 언어다.

코틀린에서 가변성 제한하기

  • immutable 객체
  • 읽기 전용 프로퍼티 (val)
  • 가변 컬렉션과 읽기 전용 컬렉션 구분하기
  • 데이터 클래스 copy

읽기 전용 프로퍼티 (val)
코틀린은 val를 사용해 읽기 전용 프로퍼티를 만들 수 있다.
코틀린의 프로퍼티는 기본적으로 캡슐화되어있고, 추가적으로 사용자 정의 접근자를 가질 수 있다.
상속의 과정에서 val를 var로 오버라이드 할 수 있다.
일반적으로 var보다 val를 많이 사용한다.
val은 읽기 전용 프로퍼티지만, 변경할 수 없음을 의미하는 것은 아니다. 완전히 변경할 필요가 없다면 final 프로퍼티를 사용하는게 좋다.
스마트 캐스트 : null인지 검사하면 조건문 본문 내부에서는 해당 변수가 null이 아니라는 것이 확인된 것이다. 컴파일러가 nullable에서 null-safety로 변경해준다. 이를 스마트 캐스트라고 부른다.
 
가변 컬렉션과 읽기 전용 컬렉션 구분하기
Iterable, Collection, Set, List 인터페이스는 읽기 전용
MutableIterable, MutableCollection, MutableSet, MutableList 인터페이스는 읽고 쓸 수 있다.
읽기 전용 컬렉션이 내부의 값을 변경할 수 없다는 의미는 아니다.
코틀린이 내부적으로 immutable하지 않은 컬렉션을 외부적으로 immutable하게 보이게 만들어서 얻어지는 안정성이다.
컬렉션 다운캐스팅(immutable -> mutable 형변환)은 계약을 위반하고 추상화를 무시하는 행위다.
읽기 전용에서 mutable로 변경해야 한다면, 복제를 통해서 새로운 mutable 컬렉션을 만드는 방법을 활용해야 한다.
 
데이터 클래스의 copy
장점

  1. 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉽다.
  2. immutable 객체는 공유했을 때도 충돌이 따로 이루러지지 않으므로, 병렬 처리를 안전하게 할 수 있다.
  3. immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.
  4. 방어적 복사(생성자를 통해 초기화할때 새로운 객체로 감싸서 복사하는 방법)를 만들 필요가 없다.
  5. immutable 객체는 다른 객체를 만들 때 활용하기 좋다
  6. immutable 객체는 set, map의 키로 사용할 수 있다.

mutable객체를 키로 사용하면, 변경했을 때 hashcode값이 변경되서 set, map의 key로 사용할 수 없다.
immutable객체를 변경할땐 함수를 제공해서 수정한 새로운 객체를 만들어 낼 수 있어야 한다.
위 작업은 귀찮은 일이기 때문에 data 한정자를 사용, copy 메서드를 활용한다.
 

다른 종류의 변경 가능 지점

읽기 전용 프로퍼티의 mutable 객체 vs 가변형 프로퍼티에 immutable 객체
첫번째 경우는 멀티스레드 처리가 이루어질 경우, 내부적으로 적절한 동기화가 되어 있는지 확실하게 알 수 없으므로 위험하다.
두번째 경우가 멀티스레드 처리에서 안정성이 더 좋다고 할 수 있다.
 
mutable 리스트 대신 mutable 프로퍼티를 사용하면 사용자정의 setter, delegate(Delegates.observable)를 활용해서 변경을 추적할 수 있다.
mutable 프로퍼티에 읽기 전용 컬렉션을 넣어 사용하는 것이 쉽다. 객체를 변경하는 메서드 대신 세터를 사용하면 되고, 이를 private로 만들 수도 있다.
 
최악의 방식은 mutable 프로퍼티에 mutable 컬렉션을 사용하는 것이다.
 

변경 가능 지점 노출하지 말기

mutable 객체를 외부에 노출하는 것은 굉장히 위험하다.
노출해야 한다면, 두 가지 방법으로 처리한다.

  1. 리턴되는 mutable 객체를 복제
  2. 읽기 전용 슈퍼타입으로 업캐스트(mutable -> immutable 형변환)하여 가변성을 제한

 
효율성 때문에 mutable 객체를 사용해야하는 경우가 있다. 이때는 멀티스레드 때에 더 많은 주의를 기울여야 한다.
(책에는 immutable 객체를 멀티스레드에서 더 주의하라고 나와있는데, 내용상 잘못된 듯 하다.)
 

아이템2: 변수의 스코프를 최소화하라

  • 프로퍼티보다 지역변수를 사용하는 것이 좋다.
  • 최대한 좁은 스코프를 갖게 변수를 사용

스코프를 좁게 만드는 것이 좋은 이유는 많지만, 가장 중요한 이유는 프로그램을 추적하고 관리하기 쉽기 때문이다.
변수의 스코프 범위가 너무 넓으면, 다른 개발자에 의해서 변수가 잘못 사용될 수 있다.
변수를 정의할 때 초기화 되는 것이 좋다. if, when, try-catch Elvis 표현식 등을 활용하면 최대한 변수를 정의할 때 초기화 할 수 있다.
 

캡처링

람다에서는 스코프 외부 변수를 사용할 때 캡처를 하는데, 코드가 원치 않게 동작할 수 있다.

아이템3: 최대한 플랫폼 타입을 사용하지 말라

NPE는 코틀린에서 null-safety 메커니즘으로 인해 거의 찾아보기 힘들다.
다른 프로그래밍 언어에서 넘어온 타입들을 플랫폼 타입이라 한다.
플랫폼 타입을 사용할 때는 항상 주의를 기울여야 한다.
자바 코드를 직접 조작할 수 있다면, @Nullable, @NotNull 어노테이션을 붙여서 사용한다.
 
플랫폼 타입을 사용하는 코드는 해당 부분 뿐만아니라, 활용하는 곳까지 영향을 줄 수 있는 위험한 코드다.
연결되어있는 생성자,메서드,필드에 어노테이션을 활용하는 것이 좋다.

아이템4: inferred 타입으로 리턴하지 말라

타입추론 : 대입되는 값에 의해서 타입이 결정되는 것
 
타입을 확실하게 지정해야 하는 경우 명시적으로 타입을 지정해야 한다는 원칙만 갖고 있으면 된다.
inferred 타입은 프로젝트가 진전될 때, 제한이 너무 많아지거나 예측하지 못한 결과를 낼 수 있다.

아이템5: 예외를 활용해 코드에 제한을 걸어라

코틀린에서는 코드의 동작에 제한을 걸 때 다음과 같은 방법을 사용한다.

  • require
  • check
  • assert
  • return, throw와 활용하는 Elvis 연산자

장점

  • 문서를 읽지 않은 개발자도 문제를 확인할 수 있다.
  • 예상치 못한 동작을 하지 않고 throw를 한다.
  • 코드가 어느정도 자체적으로 검사가 된다. 단위 테스트를 줄일 수 있다.
  • 스마트 캐스트를 활용해서 캐스트를 적게 할 수 있다.

아규먼트

아규먼트는 제한을 거는 코드를 많이 사용한다.
일반적으로 require 함수 사용, 만족하지 못할 때 IllegalArgumentException을 throw 한다.
 

상태

상태와 관련된 제한을 걸 대는 일반적으로 check 함수를 사용한다.
상태가 올바르지 않으면 IllegalArgumentException을 throw 한다.
함수 전체에 대한 어떤 예측이 있을 때는 일반저긍로 require 블록 뒤에 배치한다. check를 나중에 하는 것이다.
 

Assert 계열 함수 사용

구현 문제로 발생할 수 있는 추가적인 문제를 예방하려면, 단위테스트를 사용하는 것이 좋다.
애플리케이션 실행에서는 assert가 예외를 throw하지 않는다.
 
nullability와 스마트 캐스팅
require와 check 블록으로 어떤 조건을 확인해서 true가 나왔다면, 이후도 true일거라 가정한다.
null 검사, is 키워드를 사용한 타입검사를 한 뒤에 스마트 캐스팅이 된다.
requireNotNull, checkNotNull 변수를 언팩하는 용도로 활용할 수 있다.
 
return과 throw를 활용하는 Elvis 연산자는 nullable을 확인할 때 괴장히 많이 사용되는 관용적인 방법이다.
 

아이템6: 사용자 정의 오류보다는 표준 오류를 사용하라

가능하다면 직접 오류를 정의하는 것보다는 최대한 표준 라이브러리의 오류를 사용하는 것이 좋다.
표준 라이브러리의 오류는 많은 개발자가 알고 있으므로, 이를 재사용하는 것이 좋다.

아이템7: 결과 부족이 발생할 경우 null과 Failure를 사용하라

함수가 원하는 결과를 만들어 낼 수 없을 때 처리하는 메커니즘은 아래의 두 가지다.

  • null 또는 실패를 나타내는 sealed result 클래스를 리턴한다.
  • 예외를 throw 한다.

충분히 예측할 수 있는 범위의 오류는 null과 Failure를 사용하고, 예측하기 어려운 예외적인 범위의 오류는 예외를 throw 해서 처리하는 것이 좋다.
 
null값과 sealed 클래스는 명시적으로 처리해야 하며, 애플리케이션의 흐름을 중지하지도 않는다.
추가적인 정보를 전달해야 한다면 sealed result를 사용하고, 그렇지 않으면 null을 사용하는 것이 일반적이다.

아이템8: 적절하게 null을 처리하라

  • nullable 타입은 세 가지 방법으로 처리한다.
  • ?., 스마트 캐스팅, Elvis 연산자 등을 활용해서 안전하게 처리한다.
  • 오류를 throw한다.
  • 함수 똔느프로퍼티를 리팩터링해서 nullable 타입이 나오지 않게 바꾼다.

null을 안전하게 처리하기

null을 안전하게 처리하는 방법 중 널리 사용되는 방법은 안전 호출과 스마트 캐스팅이 있다.
공격적, 방어적 프로그래밍은 서로 충돌이 되는 것처럼 보이지만, 둘 다 필요하다.
 
오류를 throw 할 때는 throw, !!, requireNotNull, checkNotNull 등을 활용한다.

not-null assertion(!!)과 관련된 문제

어떤 대상이 null이 아니라고 생각하고 다루면, NPE 예외가 발생합니다.
!!는 사용하기 쉽지만, 좋은 해결 방법은 아니다.
명시적 오류는 제네릭 NPE보다 훨씬 더 많은 정보를 제공해줄 수 있으므로, !! 연산자를 사용하는 것보다 훨씬 좋습니다.
일반적으로 !!연산자는 사용을 피해야한다.
 

의미없는 nullability 피하기

필요한 경우가 아니라면, nullability 자체를 피하는게 좋다.
의미가 없을 때 null을 사용하지 않는 것이 좋다.

  • nullability를 피할 때 사용할 수 있는 몇 가지 방법
  • 클래스에서 nullability에 따라 여러 함수를 만들어서 제공할 수 있다.
  • 어떤 값이 클래스 생성 후 확실하게 설정된다는 보장이 있으면, lateinit과 notNull 델리게이트를 사용
  • 빈 컬랙션 대신 null을 리턴하지 말아라. 빈 컬랙션을 사용해라
  • nullable enum과 None enum은 다르다. 필요한 경우 None을 추가해서 활용할 수 있다.

lateinit 프로퍼티와 notNull 델리게이트

nullable 필드 선언 후 할당하는 것은 실행 전에 설정될 거라는 것이 명확하므로 의미없는 코드가 사용된다고 할 수 있다.
나중에 속성을 초기화 할 수 있는 lateinit 한정자를 사용한다.
 
lateinit을 사용할 수 없는 경우 Int, Long, Double, Boolean 과 같은 원시타입 프로퍼티를 초기화 해야하는 경우
Delegates.notNull을 사용한다.

아이템9: use를 사용하여 리소스를 닫아라

AutoCloseable을 상속받는 Closeable 인터페이스를 구현하는 클래스들은 최종적으로 레퍼런스가 없어질 때 가비지 컬렉터가 처리한다.
명시적으로 close해주는게 좋다.
전통적으로 try-finally 블록을 사용해서 처리
finally 블록 내부에 오류가 발생하면, 둘 중 하나만 전파된다.
하지만 이를 직접 구혀하려면 코드가 길고 복잡해진다.
표준 라이브러리에 use라는 이름의 함수가 포함되어 있다.
 
use를 사용하면 Closeable/AutoCloseable을 구현한 객체를 쉽고 안전하게 처리할 수 있다.

아이템10: 단위 테스트를 만들어라

사실코드를 안전하게 만드는 가장 궁극적인 방법은 다양한 종류의 테스트를 하는 것이다.
 
단위테스트의 장점

  • 테스트가 잘 된 요소는 신뢰할 수 있다.
  • 리팩터링 하는 것이 두렵지 않다.
  • 수동 테스트 하는 것보다 단위 테스트로 확인하는 것이 빠르다.

단점

  • 단위 테스트를 만드는 데 시간이 걸린다.
  • 테스트를 활용할 수 있게 코드를 조정해야 한다.
  • 좋은 단위 테스트를 만드는 작업이 어렵다.

가장 중요한 것은 애플리케이션이 진짜로 올바르게 동작하는지 확인하는 것이다. 이것이 테스트다.
비즈니스 애플리케이션 등에서는 최소한몇 개라도 단위 테스트가 꼭 필요하다.

Comments