상세 컨텐츠

본문 제목

[이펙티브 코틀린 정리]1장- 안전성(Safety)

이펙티브 코틀린 정리

by Tabris4547 2023. 8. 10. 15:55

본문

728x90

*본 글은 개인적으로 진행하는

코틀린 스터디를 하던 중 정리한 포스트입니다.

책의 예시코드 활용 및 학습내용을 각색했습니다.

저작권 문제가 있을 경우 바로 삭제가 될 수 있음을 미리 밝힙니다.

 

안전제일이라는 말이 있죠?

아무리 성능이 좋은 기계여도

안전성이 떨어진다면

사용자가 크게 다칠 수 있습니다.

프로그램 코드는 어떨까요??

코드를 열심히 짰는데

돌려보니 에러가 뭉탱이로 나온다면?

혹은 입력값이라든가

실행 케이스마다 결과가 다르다면?

의외로 프로그램을 짜다보면

"내가 의도하지 않게 작동하는"케이스가 참 많습니다.

이 책에서 나와있듯, 

코틀린은 안전한 언어이지만 정말로 안전하려면

개발자의 역량이 뒷받침되어야합니다.

1장은 어떻게하면 코틀린 코드를 안전하게 짜는지,

다시말해, 오류가 덜 발생하는 코드를 만드는 방법을 알려줍니다.

 

아이템1

가변성을 제한하라

 

val를 사용해라

 

"코틀린에서는 var타입보다는 val을 쓰는게 정석입니다"

코틀린의 걸음마를 때기 시작할 때

코틀린 고수분께 받은 피드백입니다.

C언어만 하더라도, 변수 선언하고 값을 바꾸는데에 크게 문제가 없었습니다.

int a=3; 이렇게 선언하고

a=5 이렇게 값을 변경해도 크게 문제가 없습니다.

그런데 코틀린은 선언할 떄

val,var이냐에 따라서 값 변경이 가능한지 아닌지가 달라집니다.

처음에는 var이 값을 변경할 수 있으니

무지성으로 var a:Int 이런 식으로 선언했는데

고수분이 이런 방식이 코틀린에서 쓰이는 방식이 아니라고 해주셨습니다.

 

val는 읽기 전용 프로퍼티입니다.

코틀린에서 val을 쓰는 이유는

가변성을 제한하면서 코드의 안전성을 높이는데에 있습니다.

fun main(args: Array<String>) {
    val a=10
    a=20 //잘못짠 코드
    
}

코틀리에서는 위의 케이스는 잘못된 문법입니다.

fun main(args: Array<String>) {
    val list= listOf("test")
    list.add("a")//잘못된 코드

}

이 케이스 역시도 잘못된 케이스입니다.

코틀린이 처음에 어려웠던 게

리스트는 저렇게 하면 추가가 안된다는 점입니다.

이 지점이 처음 코틀린을 배울 때 어려웠던 점입니다.

파이썬은 간단하게 add만 붙이면 되는데...

이건 그게 안 됩니다.

그럼 어떻게 해야하느냐

fun main(args: Array<String>) {
    val list= mutableListOf("test")
    list.add("a")

}

다음과 같이 mutableList로 선언해야 add를 할 수 있습니다.

 

우리는 여기서 이런 의문이 듭니다.

왜 굳이 읽기전용 val을 선언하는게 정석인가?

그건 바로 "코드예측"이 쉽기 때문입니다.

val 타입으로 지정한 것은 값이 바뀌지가 않기 때문에

고정의 값으로 생각하면 됩니다.

그래서 나중에 다른 변수가 나오더라도

"저건 고정되어있구나"하고 넘기면 됩니다.

 

읽기전용 컬랙션

vs 읽고 쓸 수 있는 컬랙션

코루틴은 컬렉션 계층이 설계된 언어체계 덕분에,

위의 그림과 같은 계층을 보입니다.

Iterable,Collection,Set,List 인터페이스는 읽기 전용이라

변경을 위한 매서드는 따로 가지고 있지 않습니다.

반대로 그 아래의 MutableList,MutableSet,MutableMap등은

읽고 쓸 수 있는 컬랙션입니다.

여기서 주의할 점은

다운캐스트(형 변환)할 때입니다.

예를들어, List->MutableList로 변경한다고 치면

아예 새롭게 Copy를 통해 변환해야합니다.

fun main(args: Array<String>) {
    val list=listOf(1,2,3)
    val mutablelist=list.toMutableList()
    mutablelist.add(4)
}

다음과 같이, list를 복제(copy)한 후에

새로운 mutable컬렉션을 만들어야합니다.

 

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

 

c++,java,파이썬에서

class설계에 대해 배우다가 항상 의문이었던 부분이 있었습니다.

바로 private,public 변수 설정.

"왜 불편하게 private을 만들어서 변수 접근을 제한할까?"

"코드열면 클래스 내부를 다 볼 수 있는데, 왜 private설정해서 불편하게 만들까?"

 

그 당시에는 저 혼자 코드를 작업했기 때문에

private을 쓰는 것에 대한 의문점이 많았습니다.

이후에 계속 코드에 대한 공부를 하면서 그 이유를 알았습니다.

모든 코드가 전부 다 오픈이 되면 의미가 없어보일 수 있어도

상당히 많은 경우가 그렇지 않습니다.

만약에 라이브러리로 기능을 제공한다든가

꼭 값을 고정해야하는 경우에는 private이 의미가 있었습니다.

fun main(args: Array<String>) {
    class gamer{
        private var health:Int=100

        fun getHealth():Int
        {
            return this.health
        }

        fun upHealth(point:Int)
        {
            this.health+=point
        }
        
    }
}

예를들어, 게임관련 코드가 있습니다.

초기 캐릭터의 체력은 전부 다 100으로 시작합니다.

체력 증가 및 감소는 캐릭터 내부 함수를 통해서 수정이 가능합니다.

만약 누군가가 임의로

charecter.health+=100이렇게 해킹했다고 칩니다.

지금은 간단한 예시이기 때문에 크게 상관이 없어보일 수 있지만

프로그램이 복잡해질수록 문제가 커집니다.

만약에 upHealth를 한 후, 해당 캐릭터의 채력을 네트워크 DB에 저장한다고 가정해보겠습니다.

함수호출로 체력을 올리면 예상대로 DB에 저장이 되는데

그냥 야매로(?)chareacter.health를 불러서 체력을 올리면

DB에 저장이 되지 않습니다.

이런 게 쌓이면 버그가 발생하는 거고

결국 게임개발자가 자숙영상을 찍어올릴 수 있게 됩니다.

그러니 개발하다가 임의로 변경되면 안되는 부분에 private을 두는 경우가 가장 베스트입니다.

지금 저 클래스에서는 health가 var로 선언되어 변경이 가능한 부분입니다.

그러기 때문에 private으로 지정해주어 개발 중의 휴먼에러를 막고

코드가 유출될 경우 누군가의 악의적인 해킹을 막는 효과도 있습니다.

 

아이템2

변수의 스코프를 최소화하라

 

프로그래밍을 배울 때 의문이었던 개념 중 하나로

지역변수,전역변수가 있었습니다.

잘 몰랐을 때는 "그냥 전역변수에 모든 걸 다 때려박으면 되지,

왜 귀찮게 지역변수를 만들어서 헷갈리게 동작할까?"

라는 생각이 들었습니다.

이런 생각은 코드가 복잡해지는 상황을 만나게 되면

바로 해결되는 고민입니다.

fun main(args: Array<String>) {
    var key:Int=0
    
    fun example1()
    {
        while (key<10)
        {
            println(key)
            key+=1
        }
        
    }
    fun example2()
    {
        var ex:Int=0
        while (ex<10)
        {
            println(ex)
            ex+=1
        }

    }
}

두 개의 함수는 동작결과는 동일합니다.

0~9까지 출력하는 동작을 수행합니다.

example1함수는 전역변수인 key를 건들고

example2함수는 지역변수인 ex를 건듭니다.

지금이야 매우 간단한 동작이기 때문에

크게 상관이 없어보일 수 있습니다.

그런데 코드가 길어지고 복잡해질수록 이야기가 달랍니다.

만약에 example1처럼 전역변수를 계속 써서 반복문을 돌린다면

프로그램을 짤 때 key값을 계속 염두해야합니다.

그러다가 사람이 작업을 하다보니

key값을 잘못생각해 휴먼에러가 날 수도 있고요.

또, 만약에 여러 사람과 함께 작업을 하다보면

저런 전역변수 하나 때문에 동료 개발자가 코드를 이해하기 어려워지는 경우도 생깁니다.

그러니 가능하면 변수의 스코프를 최소화해서

지역변수로 짜는 것이 좋습니다.

 

아이템3

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

 

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

플랫폼타입이란 자바 등의 다른 프로그래밍 언어에서

넘어온 타입들을 특수하게 다루는 타입입니다.

프로그래밍할 때는 주로 위의 예시처럼 !!를 붙여서 작업합니다.

!!의 의미는 not null을 단정하기 때문에

컴파일시 null error를 피할 수 있습니다.

하지만 실제로 해당 변수가 null인지 아닌지 확인할 수 없기 때문에

실제 코드를 동작하는 과정에서 안전성이 깨지는 경우가 많습니다.

 

아이템4

inferred 타입으로 리턴하지 말라

 

아이템5

예외를 활용해 코드에 제한을 걸어라

 

"stack over flow Error"

종종 재귀함수를 잘못활용하면 발생하는 에러메세지입니다.

함수가 종료되는 예외를 제대로 걸지 못하면

재귀가 무한히 반복되면서

결국에는 프로그램 작동에 문제가 생깁니다.

 

아이템6

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

 

본인이 만든 오류 메세지도 좋지만

표준 오류를 사용해야

예측 못한 상황에서의 오류도 함께 해결할 수 있습니다.

IIlegalArugmentException,IllegalStateException->require,check를 통해 throw할 수 있는 예외

IndexOutOfBoundsException->인덱스가 파라미터를 벗어났다는 예외(배열에 많이 쓰임)

CouncurrentModificationException->동시수정을 금지했는데 발생핬을 경우

UnsupportedOperationException->사용하려는 매서드가 현재 객체에서는 사용할 수 없는 경우

NoSuchElementException->사용자가 사용하려했던 요소가 존재하지 않음

 

아이템7

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

 

아이템8

적절하게 null을 처리하라

 

아이템9

use를 사용하여 리소스를 닫아라

 

아이템10

단위 테스트를 만들어라

 

"테스트는 출력문 결과로 확인하는거다"

프로그램 테스트할 때, 많은 사람들이 전체 프로그램을 빌드한 후

출력문으로 실행결과를 봅니다.

하지만 전체 프로그램을 전부 실행시키는 이런 방식은

프로그램 크기가 거대하면 거대할수록

비효율적입니다.

내가 확인하고 싶은 동작은 하나인데

run을 돌려서 100개의 동작이 함께 실행된다면

하나의 동작을 보려고 99개의 동작을 낭비하는 셈입니다.

그래서 단위테스트가 있습니다.

코틀린에서는 JUNIT을 사용해서

테스트코드를 작성할 수 있습니다.

테스트 코드를 작성에 대해서 '시간낭비'라는 의견도 있습니다.

하지만 복잡한 코드에 대해, 미리미리 동작을 하나하나 체크해서

전체 동작을 수행할 때 오류를 최소화할 수 있기 때문에

프로그램 규모가 커질수록 단위테스트의 의미가 점점 커집니다.

728x90

관련글 더보기

댓글 영역