Programming/Kotlin

Kotlin - 코틀린에 대해 (1)

긍정왕웹서퍼 2023. 3. 2. 23:57
728x90

 

     

    개요

    코틀린에 대해 공부하고자 찾아보다가 당근마켓에서 올라온 https://youtu.be/RBQOlv0aRl4 이라는 영상을 보고 해당 발표내용을 간단하게 정리해보았습니다. 발표내용을 그냥 보기보다 예제로 올려주신 코드를 간략히 추리며 포인트들만 정리해보고 공부해보았습니다.

     

     

     

    코틀린의 철학

    1. 간결성

    데이터 보관을 목적으로 사용하는 클래스가 필요할 때는 data class 를 정의한다. 이 data class는 property에 대한 getter, setter, equals, hashCode, toString등 같은 메소드를 컴파일 시점에 자동으로 생성해준다.

    data class Person(
    	val id:UUID,
    	val name: String,
    	val address: Address
    )
    

    표준 라이브러리의 풍부한 API와 고차 함수의 도움을 받아 간결하게 목적을 달성할 수 있다.

    val persons = repository.findByLastname("matthews")
    val filteredPersons = persons.filter { it.address.city == "Seoul" }
    

    단일표현 함수는 등호로 함수 정의와 바디를 구분하여 짧게 표현할 수 있다

    fun double(x: Int): Int = x * 2
    val doubled = double(2)
    

     

     

     

     

    2. 안정성

    null이 될 수 없는 값을 추적하며, NullPointException 발생을 방지할 수 있습니다.

    val nullable: String? = null // null이 될 수 있음을 명시
    val nonNullable: String = "" // null이 될 수 없음을 명시 
    

    타입 검사와 캐스트가 한 연산자를 통해 이뤄지며, ClassCastException과 같은 에러 발생을 방지합니다.

    val value = loadValue()
    if(value is String) {
    	value.uppercase(Locale.getDefault())
    }
    

    break 문이 없어도 되며, 열거형 같은 특별한 타입과 함께 쓰면 모든 값이 평가되었는지 확인할 수 있다.

    val scoreRange = when(CreditScore.EXCELLENT) {
    	CreditScore.BAD -> 300..629
    	CreditScore.FAIR -> 630..689
    	CreditScore.GOOD -> 690...719
    	CreditScore.EXCELLENT -> 720...850
    }
    
    enum class CreditScore {
    	BAD,FAIR,GOOD,EXCELLENT
    }
    

     

     

     

     

     

    코틀린 알아보기

    null이 들어올 수 있는 nullable 타입의 객체에서 null check를 해보겠습니다.

    fun from(posts: Array<Post?>): Array<PostDto> {
    	return posts.map({ post -> 
    		if (post == null) {
    			throw Error("Post object is null")
    		} // 기존의 java null check 에서 
    		**if (post?.id == null) { //?. 문법으로 null check
    			throw Error("Id field is null in post object")
    		}**
    		PostDto(
    			post?.id?: throw Error("Post object or id field is null"), 
    // 엘비스 연산자 ?: 를 사용해서 이 객체(값)이 null 이라면? : 다음 코드를 실행한다 like ifnull 
    			post?.text!!, // !! 느낌표 2개는 null이 아니라는 것을 단언한다. null인경우 NPE발생
    			post.author.id,
    			post.createAt,
    			post.updatedAt
    		)
    	}).toTypedArray()
    }

    분기문같은 길고 복잡한 코드를 안전하고 간결하게 변경할 수 있습니다.

    // before 
    fun mapItem(item: NewsItem<*>): NewsItemDto {
    	if (item is NewsItem.NewTopic) {
    		return NewsItemDto(item.content.title, item.content.author)
    	} else if (item is NewsItem.NewPost) {
    		return NewsItemDto(item.content.text, item.content.author)
    	} else {
    		throw IllegalArgumentException("This item cannot be converted")
    	}
    } 
    

     

     

     

     

    문(statment) 과 식(expression)

    이 코드를 보자면 if else if로 이어지는 구조로 분기를 돌며 조건들을 확인하는 코드입니다. 꽤나 복잡해 보이는 코드이구요, 이 코드를 보기전에 언어적인 장치로 문(statment) & 식(expression) 이 있습니다

    • 식 (expression) : 값을 만들어 낼 수 있는 것이거나, 다른 식의 하위 요소로 참여해 값을 만들어 낼 수 있습니다.
    • 문 (statment) : 자신을 둘러싼 가장 안쪽의 최상위 요소로서 존재하며 아무런 값을 만들어 낼 수 없습니다.

    Java에서 모든 제어구조는 statment 입니다. 즉 문으로써 값을 만들어 낼 수 없는 구조 입니다. 반면 Kotlin은 Loop를 제외한 모든 제어구조가 expression 으로 구성되어 있어 다양한 값을 만들어낼 수 있습니다.

    다시 위에 코드를 보면 전형적인 Java 제어구조의 statment 형태의 제어문입니다. 코틀린에서는 이 코드를 expression 형태로 만들어 낼 수 있습니다.

    // 위코드에선 분기문을 통해 확인하고 그 값을 return 했다면, 이 코드에선 일치하는 값을 바로 할당한다.
    fun mapItem(item: NewsItem<*>) = if (item is NewsItem.NewTopic) {
    		NewsItemDto(item.content.title, item.content.author)
    	} else if (item is NewsItem.NewPost) {
    		NewsItemDto(item.content.text, item.content.author)
    	} else {
    		throw IllegalArgumentException("This item cannot be converted")
    	}
    } 
    

    하지만 이렇게 작성해도 if else if 문 자체의 복잡함이 남아있는 듯 어지럽습니다. 이 코드를 좀 더 간결하게 할 수 있는게 Java에선 swtitch 문과 비슷한게 Kotlin when 문이 있습니다 이 when문을 통해 코드를 바꿔보자면

    fun mapItem(item: NewsItem<*>) = when (item) {
     is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.name)
     is NewsItem.NewPost -> NewsItemDto(item.c...t.title, item.c...t.author)
     else -> throw IllegalArgumentException("This item cannot be converted")
    } 
    

    확실히 간결해진 문법으로 가독성이 높아졌으며 when문 또한 expression (식)으로 값을 만들어 즉시 반환할 수 있습니다!

     

     

     

    sealed class

    이번엔 위 코드를 사용하는 예제를 통해 알아보겠습니다.

    abstract class NewsItem<out C> {
    	val type: String
    		get() = javaClass.simpleName
    	abstract val content: C
    
    	data class NewTopic(...) : NewsItem<TopicDetails>()
    	data class NewPost(...) : NewsItem<Post>()
    // 여기서 새로운 Sub 클래스만 추가한다면 
    	**data class NewLike(...) : NewsItem<Like>()**
    // NewLike라는 클래스가 추가되었다면 아래 mapItem function에서 Exception을 발생하게 됩니다.
    ****// 테스트코드나 QA가 꼼꼼히 되지않는다면 이 코드는 런타임때가 되어서야 발견될 것입니다.
    }
    
    // -> 
    
    fun mapItem(item: NewsItem<*>) = when (item) {
     is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.name)
     is NewsItem.NewPost -> NewsItemDto(item.c...t.title, item.c...t.author)
     else -> throw IllegalArgumentException("This item cannot be converted")
    } 
    

    위에 코드에 주석에서 말한것 처럼 테스트코드가 꼼꼼하지 않거나, 테스트가 늦었다면 이 코드는 Runtime시에나 발견되어 Exception을 발생하게 될 것입니다. 이를 코틀린에서는 안전하게 다룰 수 있도록 sealed class를 제공합니다.

     sealed class NewsItem<out C> {
    	val type: String
    		get() = javaClass.simpleName
    	abstract val content: C
    
    	data class NewTopic(...) : NewsItem<TopicDetails>()
    	data class NewPost(...) : NewsItem<Post>()
    }
    

    sealed class는 추상클래스와 비슷하나 제안된 클래스에 계층구조를 만들 때 사용될 수 있습니다. 쉽게 말하자면 클래스 외부에 자신을 상속한 클래스를 둘 수 없고, 상속하려면 반드시 seladed class 내부에 중첩해서 작성을 해야 합니다. 이렇게 중첩해서 작성하는 코드는 누가 자신을 상속받았는 지를 명확하게 알게 되므로 seladed class로 변경하게 된다면 when 식에서 sealed class의 모든 하위 class를 처리한다면 else 분기문이 필요가 없어집니다.

    fun mapItem(item: NewsItem<*>) = when (item) {
     is NewsItem.NewTopic -> NewsItemDto(item.c...t.title, item.c...r.name)
     is NewsItem.NewPost -> NewsItemDto(item.c...t.title, item.c...t.author)
    } 
    

     

     

     

     

    Default arguments (기본인자)

    class Post (
    	val id: Long? = null,
    	val text: String,
    	val author: AggregateReference<User, Long>,
    	val topic: AggregateReference<Topic, Long>,
    	val createdAt: Date = Date(),
    	val updatedAt: Date = Date()
    ) {
    	constructor(...)
    	constructor(...)
    }
    

    이렇게 기본 data class로 사용되는 Post 클래스를 constructor 로 생성자를 구현해야 했습니다. 하지만 코틀린은 이 코드를 간결하게 만들 수 있습니다.

    class Post (
    	val id: Long? = null,
    	val text: String,
    	val author: AggregateReference<User, Long>,
    	val topic: AggregateReference<Topic, Long>,
    	val createdAt: Date = Date(),
    	val updatedAt: Date = Date()
    )
    
    // data 클래스를 생성하고자 할 때
    // Default name을 지정해 특정 필드값으로 지정할 수 있습니다. 
    Post(
    	text = "...",
    	author = AggregateReference<User, Long>,
    	topic: AggregateReference<Topic, Long>
    )
    

    builder 패턴과 비슷하게 필드네임을 지정해서 값을 설정한다는 점에서 언어차원에서 builder패턴을 지원한다고 생각할 수 있습니다.

    일급함수, ktlint 와 코드컨벤션

    기존의 자바등 타 언어들에서 property등 환경변수를 가져와서 사용하거나 객체를 읽는 행위등을 자주 사용하게 됩니다. 코틀린에서는 이 역시 간결하게 작성할 수 있습니다. 이를 스코프(scope) 함수라고 합니다.

    return EmbedderDatabaseBuilder()**.apply** {
    	setType(type)
    	setScriptEncoding(scriptEncoding)
    	addScripts(*scripts ?: emptyArray())
    }.build()
    

     

     

     

     

    익명 (anonymous classes)

    TopicDetails.of(
    	topic,
    	object : AuthMapper {
    		override fun map(ref: .....) {
    			return ....
    		}
    	}
    )
    

    Object 키워드를 이용해서 AuthMapper를 익명 클래스 (혹은 익명 객체)로 작성해서 넘기는 방법이 있습니다. 이전 자바 개발자들은 람다가 나오기전까지는 이런방식을 사용했고 코틀린으로 넘어오면서 함수형 인터페이스를 사용할 수 있습니다.

    TopicDetails.of(
    	topic,
    	{ ref -> 
    		userRepository.findById(ref.id!!)
    	}
    ),
    ...
    
    fun interface AuthMapper {
    	fun map(ref: AggregateReference<>)....
    }
    

    이를 자바와 코틀린에서 함수형 인터페이스를 사용해 이런식으로 작성할 수도 있지만 코틀린에서 함수형 인터페이스를 더욱 쉽게 작성할 수 있도록 도와주며 코틀린에서 함수는 일급시민으로 함수를 일반 값이나 변수처럼 다룰 수 있습니다. 함수에 함수를 전달하거나 새로 함수를 생성하거나 하는등 다양한 방법으로 사용이 가능합니다.

    TopicDetails.of(
    	topic,
    	authMapper: (ref: AggregateReference<>) -> User
    ): ....
    

    이와 같이 함수를 정의할 때 id를 받아서 User를 반환하는 함수를 작성하게 되면 함수형 인터페이스 없이 바로 사용이 가능하게 됩니다. 이를 함수 타입이라고 합니다.

    'Programming > Kotlin' 카테고리의 다른 글

    Kotlin - 코틀린에 대해 (2)  (0) 2023.03.04