반응형
Notice
Recent Posts
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- Kotlin
- socket-client
- ozd
- firebase-storage
- Android
- socket.io
- JNI
- ActivityResult-API
- google-login
- Dva
- Firebase
- TIZEN
- 오즈뷰어
- mosquitto
- AWS
- git
- 워치
- socket-server
- gradle
- BottomSheetDialog
- NoSuchMethodError
- cloud-firestore
- OZViewer
- Java8
- hung-up
- firebase-database
- mqtt
- git-push
- Galaxy Watch
- Flavors
Archives
- Today
- Total
Hyeyeon blog
이펙티브 코틀린 - 1장 안정성 본문
반응형
이펙티브 코를린을 읽고 내용을 정리해봅니다.
나도모르게 사용하고있는 안티패턴을 경계하려는 마음을 한가득 담아... 🥹
1장 안정성
- 가변성 제어하기
- 변수의 스코프 최소화
- 플랫폼 타입 사용 지양하기
- inferred 타입으로 리턴하지 않기
- 예외를 활용하여 코드에 제한걸기
- 사용자 정의 오류보다 표준 오류 사용하기
- 결과 부족이 발생한 경우, null과 Failure 사용하기
- 적절하게 null 처리하기
- use를 사용하여 리소스 닫기
1. 가변성 제한하기 (가변지점 제한)
- val, immutable 클래스/프로퍼티를 사용하여, 상태 변경을 최소화합니다.
변경이 필요한 경우에는 immutable data class를 만들고 copy() 를 활용하여 새로운 인스턴스를 생성합니다. - 컬렉션의 상태를 저장할 시에는 읽기 전용(read-only) 인터페이스를 사용하여 외부에서의 수정을 방지합니다.
- mutable 컬렉션 객체는 private으로 선언하여 숨기고, 외부에는 immutable 객체를 노출합니다.
이렇게 변경 가능한 지점을 최소화하여 멀티스레드 환경에서도 안전한 상태 관리가 가능해집니다.
class TodoList {
// mutable 객체는 private
private var _tasks: List<String> = emptyList()
// 외부에는 immutable List로만 노출
val tasks: List<String> get() = _tasks
// 내부 상태 변경 시, 새 리스트로 교체
fun addTask(task: String) {
_tasks = _tasks + task
}
}
2. 변수의 스코프 최소화
- 변수의 범위를 최소화하면 프로그램 추적 및 관리가 쉬워집니다.
- 가능한 한 클래스의 프로퍼티 대신 지역변수를 사용하여, 변수의 범위를 최대한 좁게 유지합니다.
ㄴ 프로퍼티: 클래스 레벨에 선언된 변수 - 변수 선언 시 if, when, 엘비스 연산자(?:) 등을 활용하여 초기화합니다.
여러 프로퍼티 설정 시, 구조분해 선언을 활용하여 해당 함수에서만 사용하기
// 나쁜 예
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. 플랫폼 타입 사용 지양하기
- 플랫폼 타입(platformType)은 다른 언어에서 코틀린 코드로 온 타입으로, nullable 여부를 알 수 없는 타입입니다.
이로 인해 런타임에서 NullPointerException(NPE)이 발생할 수 있는 위험이 있습니다. - 자바와 코틀린을 함께 사용하는 경우,
자바 코드에 @Nullbable, @NotNull 어노테이션을 명시하면, 코틀린에서 null 가능성을 명확히 인식할 수 있어 보다 안전한 null 처리가 가능합니다. - 코틀린에서는 명시적 타입(statedType) 사용을 지향합니다.
명시적 타입으로 선언하면, 타입 관련 오류가 발생했을 때 오류 발생 위치를 쉽게 파악할 수 있어 디버깅이 용이해집니다.
// 지향
// JavaClass().value에 null이 들어왔을 때, 변수에 할당되는 시점에 NPE 발생
fun statedType() {
val value: String = JavaClass().value // NPE
...
println(value.length)
}
// 지양 (플랫폼타입)
// JavaClass().value에 null이 들어왔을 때, 변수를 참조하는 시점에 NPE 발생
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 예외를 발생시켜 함수 실행을 중단합니다.
- 주로 함수 실행 전에 사전 검증용으로 활용합니다
fun sendEmail(..){
requireNotNull(user.email)
require(isValidEmail(user, email))
}
fun factorial(n:Int) {
require(n>=0)
..
}
2) 상태 제한
- 특정 조건에 만족해야만 함수를 실행할 경우, check() 함수를 사용하여 조건을 검증합니다.
- 조건을 만족하지 않을 경우, IllegalStateException 예외가 발생합니다.
- 주로 require() 검증 이후, 함수 실행 도중 중간 검사용으로 배치합니다
fun next(): T {
check(isOpen)
checkNotNull(token)
}
3) Assert 계열 함수 사용
- 단위 테스트 시 함수가 올바르게 동작하는 지 검증하기위해 사용됩니다 (assertEqual 등)
- assertEquals 같은 함수를 활용해 기대값과 실제 결과를 비교합니다.
- 테스트 코드 내에서 함수의 정확성을 자동으로 확인할 수 있도록 도와줍니다
6. 사용자 정의 오류보다는 표준 오류를 사용
- 불필요한 사용자 정의 오류를 지양하고 표준 오류를 사용하면, 다른 사람들이 코드를 더 쉽게 이해할 수 있습니다.
7. 결과 부족이 발생한 경우, null과 Failure를 사용
- 예측 가능한 오류는 null 또는 Failure 를 사용하여 처리합니다.
예측이 어려운 오류는 예외를 throw하여 처리합니다. - try-catch 블록 내부에 코드를 배치하면 컴파일러 최적화가 제한될 수 있습니다.
- Result 같은 공용체(union type)를 반환하면 when으로 깔끔하게 처리할 수 있어, try-catch보다 효율적입니다.
sealed 클래스 형태의 Result는 추가 정보를 전달할 때 사용하고, 단순 실패 시에는 null을 사용합니다. - 함수에서 nullable 타입을 반환하는 것은 피하고, getOrNull(), 엘비스 연산자(?:) 등을 사용해 반환값을 명확히 예측할 수 있도록 합니다
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을 안전하게 처리: Elvis 연산자(?:)와 스마트 캐스팅
- 아래의 코드와 같이, Elvis 연산자(?:)와 스마트 캐스팅을 활용하여 null을 안전하게 처리할 수 있습니다.
printer?.print() // 안전호출
if (printer != null) printer.print() // 스마트 캐스팅
2) 오류 throw 하기
- 위 코드에서 printer가 null일 경우, print() 메서드는 호출되지 않기 때문에 개발자가 오류를 찾기 어렵습니다.
- throw, !, requireNotNull, checkNotNull 등을 활용하여 명시적으로 오류를 발생시키는 것이 좋습니다.
3) not-null assertion(!!) 관련 문제
- !! 연산자는 NullPointerException(NPE)을 발생시킬 수 있으므로, lateinit 혹은 Delegates.notNull와 같은 대안을 고려하는 것이 좋습니다.
- !! 연산자가 의미있는 경우는 드물며, nullability가 명확하게 표현되지 않는 라이브러리를 사용할 때와 같은 특수 상황에서만 사용하는 것이 바람직합니다.
4) 의미없는 nullability 피하기
무의미한 null 사용은 !! 연산자 사용 혹은 많은 예외처리로 인해 코드의 가독성을 떨어뜨릴 수 있으므로, 지양해야 합니다.
이러한 null 사용을 피하기 위해 다음과 같은 방법을 고려할 수 있습니다.
- 클래스에서 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() // Int 타입의 프로퍼티
private var fromNoti: Boolean by Delegates.notNull() // Boolean 타입의 프로퍼티
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
'개발 > Android' 카테고리의 다른 글
이펙티브 코틀린 - 3장 재사용성, 4장 추상화 설계 (0) | 2024.04.08 |
---|---|
이펙티브 코틀린 - 2장 가독성 (0) | 2024.03.10 |
[Android] JNI 오류 - error=2, No such file or directory (0) | 2023.02.25 |
[Android] Github Actions으로 Android CI 구현하기 (0) | 2023.01.04 |
[Android] DataBinding으로 RecyclerView 만들기 (리스트 높이 제한 방법) (0) | 2022.12.31 |
Comments