Effective Kotlin #1 안정성

yeji·2022년 11월 2일
0

Effective Kotlin

목록 보기
1/7
post-thumbnail

01부 1장. 안정성

코틀린을 사용하는 이유? : 안정성(safety)
kotlin은 다양한 설계 지원을 통해서 애플리케이션의 잠재적인 오류를 줄여준다.
kotlin이 안정성을 위해 제공하는 기능들을 살펴볼 수 있다.

Limit mutability (가변성 제한)

class BankAccount {
	var balance = 0.0 // 해당 변수로 상태 확인 가능
	private set

	fun deposit(depositAmount: Double) {
		balance += depositAmount
	}
	
	@Throws(InsufficientFunds::class)
	fun withdraw(withdrawAmount: Double) {
		if (balance < withdrawAmount) {
			throw InsufficientFunds()
		}
		balance -= withdrawAmount
	}
}

class InsufficientFunds : Exception()
val account = BankAccount()
println(account.balance) // 0.0
account.deposit(100.0)
println(account.balance) // 100.0
account.deposit(50.0)
println(account.balance) // 50.0

가변성 제한의 필요성

  • status 변경이 많아지면 프로그램 이해와 디버깅 힘들다.
    • 클래스가 예상하지 못한 상황 또는 오류를 발생시키는 경우 큰 문제가 됨
  • 가변성(mutability)이 있으면, 코드의 실행을 추론하기 어렵다.
    • 시점에 따라 값이 달라지기에 실행 예측 어렵다. 또한, 한 시점에 확인한 값이 계속 동일하게 유지된다고 확신할 수 없다.
  • 멀티스레드 프로그램일 때는 적절한 동기화가 필요하다.
    • 둘 이상의 스레드가 공유 자원에 접근하는 등 race condition 제어 필요
    • 변경이 일어나는 모든 부분에서 충돌이 발생할 수 있다.
  • 테스트하기 어렵다. 모든 상태를 테스트해야 하므로, 변경이 많으면 많을 수록 더 많은 조합을 테스트해야 한다.
  • 상태 변경이 일어날 때 이러한 변경을 다른 부분에 알려야 하는 경우가 있다.
    - ex. 정렬되어 있는 리스트에 가변 요소를 추가할 때, 요소에 변경이 일어날 때마다 리스트 전체를 다시 정렬해야 한다.
    공유 상태를 관리하는 것은 힘든 일이다.
val lock = Any()
    var num = 0
    for (i in 1..1000) {
        thread {
            Thread.sleep(10)
            synchronized(lock) {
                num += 1
            }
        }
    }

    Thread.sleep(5000)
    print(num) // 1000

가변성은 생각보다 단점이 많아 이를 완전하게 제한하는 프로그래밍 언어도 있다. 가변성은 시스템의 상태를 나타내기 위한 중요한 방법이다. 하지만 변경이 일어나야 하는 부분을 신중하고 확실하게 결정하고 사용해야 한다.

  • 정통 순수 함수형 프로그래밍 언어 haskell
  • pure function으로만 프로그래밍을 하며 이는 곧 side effect를 일으킬 수 없게됨
    • pure function
      같은 입력에 대해 항상 같은 출력을 반환하는 함수.
      다른 요인에 따라 결과가 변경되지 않는 side effect가 없음. -> 함수에서 인자를 변경하거나 프로그램의 상태를 변경하지 않음

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

코틀린은 가변성을 제한할 수 있게 설계되어 있다.

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

프로퍼티 = 필드 + 접근자

  • 클래스 내부의 변수 선언은 자바에서는 필드 선언만을 의미하지만, 코틀린에서는 프로퍼티 선언을 의미한다. (즉, 필드 뿐만 아니라 접근자 메서드도 알아서 생성. val은 private 필드+getter, var는 private 필드+getter+setter)
  1. 읽기 전용 프로퍼티(val)
    val로 선언된 프로퍼티는 마치 value처럼 동작한다.
    • 일반적인 방법으로는 값이 변하지 않는다.
    • 읽고 쓸 수 있는 프로퍼티는 var로 만든다.
    • 읽기 전용 프로퍼티가 mutable 객체를 담고있다면 내부적으로 변할 수 있다. (읽기전용과 가변성을 구분)
    val a = 10
    a = 20 // 오류
    val list = mutableListOf(1, 2, 3) // 읽기 전용, 재할당 불가
    • 읽기 전용 프로퍼티는 다른 프로퍼티를 활용하는 사용자 정의 getter로도 정의할 수 있다.
    var name: String = "Marcin"
    var surname: String = "Moskala"
    val fullName get() = "$name $surname" -> 커스텀 접근자
    • var 프로퍼티를 사용하는 val 프로퍼티는(fullName) var 프로퍼티가 변할 때 변할 수 있다.
      • var는 getter와 setter를 모두 제공하지만, val은 변경이 불가하므로 getter만 제공한다. 그래서 val을 var로 오버라이드 할 수 있다.
      • override 키워드를 붙여 변수 오버라이딩 가능
      • val과 var** https://www.bsidesoft.com/8201
        코틀린에서 val, var의 의미가 단순히 불변값을 의미하는 것이 아니라 얼마든지 val이었던 것을 구상레이어에서 var로 바꿀 수 있다.
    • val은 읽기 전용 프로퍼티지만, 불편(immutable)을 의미하는 것은 아니다.
    • 완전히 변경할 필요가 없다면 final 프로퍼티를 사용 (const val은 완전히 immutable임)
      • val : 선언한 데이터의 값 변경 x, 런타임에 값이 결정 됨 기본형과 참조타입 할당 가능, 상황에 따라 val을 여러 값으로 초기화 가능 (읽기 전용만)
      • const : 상수. 값 변경 x, 컴파일 시 값이 결정되기에 Int, Double같은 기본형과 String만 가능 (읽기 전용 + 불변)
    • val은 정의 옆에 상태가 바로 적히므로, 코드의 실행을 예측하는 것이 훨씬 간단하며, 스마트 캐스트 등의 추가적인 기능을 활용할 수 있다.
    • getter로 정의한 val은 스마트 캐스트할 수 없다. getter를 활용하므로, 값을 사용하는 시점의 name에 따라서 다른 결과가 나올 수 있기 때문이다.
      • 스마트 캐스트 : is처럼 변수 타입 검사+캐스팅 한번에 해주는 것.
        if (fullName != null) 처럼 null을 검사하면 조건문 내에서는 null이 아님이 확인됨. 코틀린 컴파일러가 자동으로 String?을 String으로 변경해주기 때문
  2. 가변 컬렉션과 읽기 전용 컬렉션 구분하기

    출처 : https://kotlinlang.org/docs/collections-overview.html#collection-types
  • 읽기 전용 인터페이스 : Iterable, Collection, Set, List
  • 읽고 쓸 수 있는 인터페이스 : MutableIterable, MutableCollection, MutableSet, MutableList
  1. 데이터 클래스의 copy
  • immutable 객체사용의 장점
    - 한 번 정의된 상태가 유지되므로, 코드를 이해하기 쉽다.
    - 공유했을 때 충돌이 따로 이루어지지 않으므로, 병렬처리를 안전하게 할 수 있다.
    - immutable 객체에 대한 참조는 변경되지 않으므로, 쉽게 캐시할 수 있다.
    - 방어적 복사본을 만들 필요가 없다.
    - 생성자의 인자로 받은 객체의 복사본을 만들어 내부 필드를 초기화하거나, getter메서드에서 내부의 객체를 반환할 때, 객체의 복사본을 만들어 반환하는 것.
    방어적 복사를 사용할 경우, 외부에서 객체를 변경해도 내부의 객체는 변경되지 않는다.
    - 다른 객체를 만들 때 활용하기 좋다.
    - ‘set’ 또는 ‘map의 키’로 사용할 수 있다. 참고로 mutable 객체는 이러한 것으로 사용할 수 없다. 해시 테이블은 처음 요소를 넣을 때 요소의 값을 기반으로 버킷을 결정하기 때문에, 변경이 일어나면 내부에서 요소를 찾을 수 없게 되어 버린다.

변수 스코프를 최소화

프로퍼티보다는 지역변수를 사용하는 것이 좋다.

최대한 좁은 스코프를 갖게 변수를 사용한다. 반복문 내부에서만 변수가 사용된다면 변수를 반복문 내부에 작성하는 것이 좋다.

람다는 값이 단 한 번만 할당되는 지역변수만을 캡처할 수 있으며, 만일 람다에서 캡처되는 지역변수의 값을 재할당되는 경우 컴파일 에러가 발생한다. 즉, 명시적으로 final로 선언되었거나, 실질적으로 final인 지역변수만 람다식 바디에 들어올 수 있다는 것이다.
자바와 달리 코틀린 람다 안에서는 final 변수가 아닌 본래의 변수에 접근이 가능하며, 변경도 가능하다.
람다 캡처링(capturing lambda)이란 간단히 말해 이처럼 파라미터로 넘겨받은 데이터가 아닌 "람다식 외부에서 정의된 변수"를 참조하는 변수를 람다식 내부에 저장하고 사용하는 동작을 의미한다.
https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/

최대한 플랫폼 타입을 사용하지 말라

@Nullable 어노테이션이 붙어 있다면, 이를 nullable로 추정하고, String?으로 변경하면 된다. @Notnull 어노테이션이 붙어있다면, String으로 변경하면 된다.

자바에서 모든 것이 nullable일 수 있으므로 최대한 안전하게 접근한다면, 이를 nullable로 가정하고 다루어야 한다. 하지만 어떤 메서드는 null을 리턴하지 않을 것이 확실할 수 있다.
not-null이 확실한 경우 단정하는 !!을 붙인다.

val users: List<List<User>> = UserRepo().groupedUsers!!.map { filterNotNull() } 

플랫폼 타입?
코틀린은 자바 등 다른 프로그래밍 언어에서 넘어와 nullable 여부를 알 수 없는 타입들을 특수하게 다룬다. 이러한 타입을 플랫폼 타입이라 부른다. 플랫폼 타입은 이름뒤에 ! 기호를 붙여서 표기한다.

// Java
public class UserRepo {
	public User getUser() {
		//...
	}
}

// Kotlin
val repo = UserRepo()
val user1 = repo.user        // user1의 타입은 User!
val user2: User = repo.user  // user2의 타입은 User
val user3: User? = repo.user // user3의 타입은 User?

val users: List<User> = UserRepo().users
val users: List<List<User>> = UserRepo().groupedUsers

자바를 코틀린과 함께 사용할 때, 자바 코드를 직접 조작할 수 있다면 가능한 @Nullable과 @NotNull 어노테이션을 붙여서 사용하면 좋다.
코틀린에서도 관련한 코드를 작성할 수 있지만, 플랫폼 타입은 안전하지 않으므로 최대한 빨리 제거하는 것이 좋다.
JavaClass().value

inferred type으로 리턴하지 말라

inferred 타입은 정확하게 오른쪽에 있는 피연산자에 맞게 설정된다는 것을 기억해야 한다. 절대로 슈퍼클래스 또는 인터페이스로는 설정되지 않는다.

예외를 사용해 코드에 제한 걸기

사용자 정의 오류보다는 표준 오류를 사용

최대한 표준 라이브러리 오류를 사용하는 것이
많은 개발자들이 인지하고 있어 재사용 하기 좋다.

  • 표준 오류
    • IllegalArgumentException
    • IndexOutOfBoundsException
    • ConcurrentModificationException
    • UnsupportedOperationException
    • NoSuchElementExceptoin
    • RuntimeException

결과 부족이 발생할 경우 null과 Failure를 사용

null을 처리해야한다면 safe call(?.)이나 Elvis 연산자(?:) 사용 가능

  • 결과가 부족한 상황 예시
    • 서버로부터 데이터를 읽어 들이려고 했는데 네트워크 오류로 받지 못한 경우
    • 조건에 맞는 요소가 없는 경우
    • 텍스트를 파싱해서 객체를 만들려고했는데 텍스트의 형식이 맞지 않는 경우
  • 처리 방법 2가지
    - null 또는 실패를 나타내는 sealed 클래스
    코틀린의 모든 예외는 unchecked 예외다.
    예외는 예외적인 상황을 처리하기 위해서 만들어졌으므로 명시적인 테스트만큼 빠르게 동작하지 않는다.
    try-catch 블록 내부에 코드를 배치하면 컴파일러 최적화가 제한된다.

적절하게 null을 처리

의미없는 nullability 피하기

  • null 대신에 빈 컬렉션으로 리턴하기

use를 사용하여 리소스 닫기

단위 테스트 만들기

단위 테스트에서 확인하는 내용

  • 유스케이스
  • 오류 케이스와 잠재적인 문제
  • 엣지 케이스와 잘못된 아규먼트
profile
🐥

0개의 댓글