Hyeyeon blog

이펙티브 코틀린 - 1장 안정성 본문

개발/Android

이펙티브 코틀린 - 1장 안정성

Hyeyeon.P 2024. 3. 6. 00:41
반응형

이펙티브 코를린을 읽고 내용을 정리해봅니다. 
나도모르게 사용하고있는 안티패턴을 경계하려는 마음을 한가득 담아... 🥹


1장 안정성

  1. 가변성 제어하기
  2. 변수의 스코프 최소화 
  3. 플랫폼 타입 사용 지양하기
  4. inferred 타입으로 리턴하지 않기 
  5. 예외를 활용하여 코드에 제한걸기
  6. 사용자 정의 오류보다 표준 오류 사용하기
  7. 결과 부족이 발생한 경우, null과 Failure 사용하기
  8. 적절하게 null 처리하기 
  9. use를 사용하여 리소스 닫기

1.  가변성 제한하기 (가변지점 제한):

val, immutable 클래스/프로퍼티 사용
변경이 필요한 대상은 immutable data class로 만들고 copy() 활용
컬렉션의 상태 저장할 시에는 읽기전용 컬렉션 사용 
mutable 객체는 private으로 사용 (외부노출X) 
mutable 리스트 대신 mutable 프로퍼티를 사용: 프로퍼티 자체가 변경 가능한 지점인 경우, 멀티스레드 안정성 더 좋음

val list1: MutableList<Int> = mutableListOf()
var list2: List<Int> = listOf() <-- 추천 (Int: immutable 객체)
var list3 = mutableListOf() <-- 최악

2. 변수의 스코프 최소화 => 프로그램 추적 및 관리가 쉬움 

프로퍼티보다 지역 변수 사용 (최대한 작은 스코프로 변수 사용)
변수 선언 시 if, when, elvis(?:) 등을 사용하여 초기화하기 

여러 프로퍼티 설정 시, 구조분해 선언 활용

// 나쁜 예
val desc: String
val color: Int
if (degrees < 5){
	desc = "cold"
    color = Color.BLUE
} else {
	desc = "hot"
    color = Color.RED
}

// 나은 예 
val (desc, color) = when {
	degrees < 5 -> "cold" to Color.BLUE
    else -> "hot" to Color.RED
}

3. 플랫폼 타입 사용 지양하기

플랫폼타입: 다른 언어에서 코틀린 코드로 넘어와서 nullable 여부를 알수 없는 타입
자바와 코틀린을 같이 사용한다면, 자바 코드에 @Nullbable, @NotNull 어노테이션 붙여서 사용 
statedType 사용 지향 => 오류 발생 위치가 명확하여 파악이 쉬움 

// 지향
fun statedType() {
	val value: String = JavaClass().value // NPE
    ...
    println(value.length)
}

// 지양 (플랫폼타입)
fun platformType() {
	val value = JavaClass().value 
    ...
    println(value.length) // NPE
}

4. inferred 타입으로 리턴하지 않기

타입이 확실하면 명시적으로 지정하기 

open class Animal
class Zebra: Animal()

fun main() {
	var animal: Animal = Zebra()
	// var animal = Zebra() 일 경우, 아래 라인에서 Type mismatch 발생 
	animal = Animal()
}

5. 예외를 활용하여 코드에 제한걸기 

   1) Argument 제한

   require()로 제한 조건을 확인하고, 조건에 미충족 시 IllegalArgumentException을 throw함.
   함수 실행 전에 사전 검사용으로 사용. 

fun sendEmail(..){
	requireNotNull(user.email)
	require(isValidEmail(user, email))
}

fun factorial(n:Int) {
	require(n>=0)
	..
}

   2) 상태 제한

   특정 조건에 만족해야만 함수를 실행할 경우, check() 활용.
   지정한 예측이 틀렸을 경우, IllegalArgumentException 발생함. 
   require 블록 뒤에 배치하여 사용.
   함수 실행 중에 중간 검사용으로 사용. 

fun next(): T { 
	check(isOpen)
	checkNotNull(token)
}

   3) Assert 계열 함수 사용 

  단위 테스트 시 함수가 잘 구현됬는지 확인하는 방법 (assertEqual 등)

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

다른 사람들이 코드를 더 쉽게 파악하기위해 표준 오류 사용을 지향

7. 결과 부족이 발생한 경우, null과 Failure를 사용

try-catch 블록 내부에 코드를 배치하면 컴파일러가 할 수 있는 최적화가 제한됨.
충분히 예측 가능한 범위의 오류는 null, Failure를 사용하고, 예측이 어려운 오류는 예외를 throw하여 처리.
Result 같은 공용체(union type)을 리턴하면 when으로 처리할 수 있음. => try-catch보다 효율적임.  
sealed result 클래스는 추가 정보를 전달할 때 사용하고, null은 그렇지 않을 경우에 사용. 
nullable을 리턴하면 안됨. getOrNull, Elvis 연산자 등으로 무엇이 리턴되는지 예측할 수 있도록 해야함.

import kotlin.Result

fun main() {
    val result1 = divide(10, 2)
    val result2 = divide(10, 0)
    
    result1.fold(
        onSuccess = { value -> println("Result of division: $value") },
        onFailure = { exception -> println("Exception occurred: ${exception.message}") }
    )
    
    result2.fold(
        onSuccess = { value -> println("Result of division: $value") },
        onFailure = { exception -> println("Exception occurred: ${exception.message}") }
    )
}

fun divide(a: Int, b: Int): Result<Int> {
    return if (b == 0) {
        Result.failure(ArithmeticException("Division by zero"))
    } else {
        Result.success(a / b)
    }
}

8. 적절하게 null을 처리하기

   1) null을 안전하게 처리: Ellvis 연산자와 스마트 캐스팅

printer?.print() // 안전호출
if (printer != null) printer.print() // 스마트 캐스팅

   2) 오류 throw 하기

  위 코드에서 printer가 null일 경우, print()를 호출하지 않기 때문에 개발자가 오류를 찾기 어려움. 
  throw, !, requireNotNull, checkNotNull 등을 활용하여 개발자에게 오류를 강제로 발생시켜주는 것이 좋음.

   3) not-null assertion(!!) 관련 문제

   !! 연산자는 NPE를 발생시킬 수 있기 때문에, lateinit 혹은 Delegates.notNull을 사용.
   !! 연산자가 의미있는 경우는 드물며, nullability가 제대로 표현되지 않는 라이브러리 사용할 때 정도에만 사용해야함. 
   ㄴ 코틀린을 대상으로 설계된 API를 사용하는데 !! 연산자를 사용하면 이상하게 생각해야함.
   일반적으로 !! 연산자 사용을 피해야함. 

   4) 의미없는 nullability 피하기

   null은 중요한 메시지를 전달하는데 사용될 수 있기에, 의미가 없어 보일 때는 null 사용을 지양해야함.
  무의미한 null 사용 시, !! 연산자 사용 혹은 많은 예외처리로 코드가 더러워짐. 
   * nullability 피하는 방법
   - 클래스에서 nullability에 따라 여러 함수를 제공하기도 함. (ex: List의 get, getOrNull 함수) 
   - 클래스 생성 이후에 확실하게 설정된다는 보장이 있는 경우엔 lateinit 혹은 Delegates.notNull 사용
   - 빈 컬렉션 대신 null 리턴 금지. 요소가 없는 경우엔 빈 컬렉션을 사용함.
   - nullable enum은 별도로 처리해야하는 경우이며, None enum은 정의에 없으므로 필요한 경우에 사용하는 쪽에서 추가해서  활용하라는 의미임.

// nullable enum 예시
enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

fun printDirection(direction: Direction?) {
    if (direction != null) {
        println("Direction: $direction")
    } else {
        println("Direction is null")
    }
}

// None enum 예시
// None enum은 "No option selected"과 같은 상황에서 사용됩니다.
enum class Option {
    NONE, OPTION1, OPTION2
}

fun processOption(option: Option) {
    when(option) {
        Option.NONE -> println("No option selected")
        Option.OPTION1 -> println("Option 1 selected")
        Option.OPTION2 -> println("Option 2 selected")
    }
}

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

   lateinit은 선언 후 반드시 초기화될 것을 예상하는 상황에서 활용함.
   lateinit을 사용할 수 없는 경우(Int, Long 등 기본 타입과 연결된 타입)은 Delegates.notNull 사용.

private var dId: Int by Delegates.notNull()
private var fromNoti: Boolan by Delegates.notNull()

override fun onCreate(..) {
	dId = intent.extras.getInt(D_ID_ARG)
	fromNoti = intent.extras.getBoolean(FROM_NOTI_ARG)
}

// 프로퍼티 위임으로 아래와 같이 사용 가능 (2부 3장에 나옴) 
private var dId: Int by arg(D_ID_ARG)

9. use를 사용하여 리소스 닫기

리소스에 대한 레퍼런스가 없어질 때 가비지 컬렉터가 처리하나, 처리가 느리며 그동안 리소스 유지비용이 많이 들어감.
따라서 명시적으로 close 메서드를 호출해주는 것이 좋음. (ex: InputStream, java.sql.Connection, java.io.Reader 등)
try-finally로 처리할 경우, 두 블록에서 오류 발생 시 둘 중 하나에만 전파됨.
Closable 객체에 use 함수를 사용하면 해당 블록이 수행된 후 자동으로 객체를 닫아서 안전하게 처리할 수 있음. 

// 기존
reader = File(filename).bufferedReader()
try {
	return reader.lineSequence().sumBy { it.length }
} finally {
	reader.close()
}

// 변경 
File(filename).bufferedReader().use { reader ->
	val content = reader.readText()
	println("File content: $content")
}

 

728x90
Comments