Kotlin

Martin·2023년 11월 18일
0
post-thumbnail

코틀린에서 변수를 다루는 방법

  • 모든 변수는 var / val을 붙여주어야 한다.
    • var : 변경 가능 / val : 변경 불가능 (read-only)
  • 타입을 명시적으로 작성하지 않아도, 타입이 추론된다.
  • Primitive Type과 Reference Type을 구분하지 않아도 된다.
  • null이 들어갈 수 있는 변수는 타입 뒤에 ? 를 붙여주어야 한다.
    • 아예 다른 타입으로 간주됨.
  • 객체를 인스턴스화 할 때 new를 붙이지 않아야 한다.

코틀린에서 Null을 다루는 방법

Safe Call (?) : null이 아니면 실행하고 null 이면 그대로 null(실행하지 않는다)

val str: String? = "ABC"
str.length // 불가능
str?.length // 가능

Elvis 연산자 (?:) : 앞의 연산 결과가 null 이면 뒤에 값을 사용

val str: String? = "ABC"
str?.length ?: 0

이 변수는 절대 null이 아니라고 단언

// 혹시나 null이 들어오면 NPE가 나오기 때문에
// 정말 null이 아닌게 확실한 경우에만 널 아님 단언!! 을 사용해야 한다.
fun startsWith(str: String?): Boolean {
	return str!!.startsWith("A")
}

코틀린에서 자바 클래스를 가져다 사용할 경우

// 자바 코드에 Null 정보에 대한 어노테이션을 사용하면 코틀린이 이해할 수 있음.
@NotNull 혹은 @Nullable
public String getName() {
	return name;
}

플랫폼 타입

  • 코틀린이 null 관련 정보를 알 수 없는 타입 (Runtime 시 Exception이 날 수 있다.)

  • 코틀린에서 null이 들어갈 수 있는 타입은 완전히 다르게 간주된다.

  • 한번 null 검사를 하면 non-null임을 컴파일러가 알 수 있다.

  • null이 아닌 경우에만 호출되는 Safe Call (?.) 이 있다.

  • null인 경우에만 호출되는 Elvis 연산자 (?:) 가 있다.

  • null이 절대 아닐때 사용할 수 있는 널 아님 단언 (!!) 이 있다.

  • Kotlin에서 Java 코드를 사용할 때 플랫폼 타입 사용에 유의해야 한다.

    • Java 코드를 읽으며 널 가능성을 확인 혹은 Kotlin으로 wrapping해서 자바 코드를 최소화

코틀린에서 Type을 다루는 방법

  • 코틀린의 변수는 초기값을 보고 타입을 추론하며, 기본 타입들 간의 변환은 명시적으로 이루어진다.
  • 코틀린에서는 is, !is, as, as?를 이용해 타입을 확인하고 캐스팅한다. (스마트 캐스팅 존재)
  • 코틀린의 Any는 Java의 Object와 같은 최상위 타입이다. null까지 포함 최상위는 Any?
  • 코틀린의 Unit은 Java의 void와 동일하다.
  • 코틀린에 있는 Nothing은 정상적으로 끝나지 않는 함수의 반환을 의미한다.
    • exception, infinite loop 등 (결론은 잘 사용안함)
  • 문자열을 가공할 때 ${변수} 와 “””(줄바꿈 가능)를 사용하면 깔끔한 코딩이 가능하다.
  • 문자열에서 문자를 가져올 때 Java의 배열처럼 []를 사용한다.

코틀린에서 연산자를 다루는 방법

  • 단항연산자(++), 산술연산자, 산술대입연산자(+=) 모두 Java와 동일하다.
  • 비교 연산자 사용법도 자바와 동일하다
    • 단, 객체끼리도 자동 호출되는 compareTo를 이용해 비교 연산자를 사용할 수 있다.
  • in, !in / a..b / a[i] / a[i] = b 와 같이 코틀린에서 새로 생긴 연산자도 있다.
  • 객체끼리의 연산자를 직접 정의할 수 있다.

코틀린에서 제어문을 다루는 방법

  • if / if - else / if - else if - else 모두 Java와 문법이 동일하다.
  • 단 조건문은 Kotiln에서는 Expression으로 취급된다.
    • 따라서 Kotlin에서는 삼항 연산자가 없다.

      fun getPassOrFail(score: Int): String {
      	return if (score >= 50) {
      		"P"
        } else {
      		"F"
        }
      }
  • Java의 switch는 Kotlin에서 when으로 대체되었고, when은 더 강력한 기능을 갖는다.

코틀린에서 반복문을 다루는 방법

  • for-each 문에서 Java는 : Kotlin은 in 을 사용한다.
  • 전통적인 for문에서 Kotlin은 등차수열(Type(Int)Progression 클래스를 상속)과 in을 사용한다.
  • 그 외 for문 문법은 모두 동일하다.
  • while문과 do while문은 동일하다.

코틀린에서 예외를 다루는 방법

  • try catch finally 구문은 문법적으로 완전히 동일하다.
    • Kotlin에서는 try catch가 expression이다.
  • Kotlin에서 모든 예외는 Unchecked Exception이다.
  • Kotlin에서는 try with resources 구문이 없다. 대신 코틀린의 언어적 특징을 활용해 close를 호출해준다.

코틀린에서 함수를 다루는 방법

함수의 문법은 Java와 다르다!

  • 접근지시어 fun 함수이름(파라미터): 반환타입 { }
  • body가 하나의 값(대입)으로 간주되는 경우 block을 없앨 수도 있고
    block이 없다면 반환 타입을 없앨 수도 있다.
public fun maxV1(a: Int, b: Int): Int {
	if (a > b) {
		return a
	}
	return b
}

// Kotlin에서 public은 생략 가능하다. 기본이 public 이다.
fun maxV2(a: Int, b: Int): Int {
	return if (a > b) {
		a
	} else {
		b
	}
}

fun maxV3(a: Int, b: Int): Int = 
	if (a > b) {
		a
	} else {
		b
	}
}

// = 을 사용하면 명시적인 반환 타입을 생략 가능
fun maxV4(a: Int, b: Int) = if (a > b) a else b
  • 함수 파라미터에 기본값을 설정해줄 수 있다.
  • 함수를 호출할 때 특정 파라미터를 지정해 넣어줄 수 있다. (builder의 특성)
  • 가변인자에는 vararg 키워드를 사용하며, 가변인자 함수를 배열과 호출할 때는 *(spread 연산자)를 붙여주어야 한다.

코틀린에서 클래스를 다루는 방법

  • 코틀린에서는 필드를 만들면 getter와 (필요에 따라) setter가 자동으로 생긴다.
    • 때문에 이를 프로퍼티(필드와 getter와 setter가 합쳐진)라고 부른다.
class Person(
	val name: String = "홍길동",
	var age: Int = 1
)
  • 코틀린에서는 주 생성자가 필수이다.
  • 코틀린에서는 constructor 키워드를 사용해 부생성자를 추가로 만들 수 있다.
    • 단, default parameter나 정적 팩토리 메서드를 추천한다.
  • 실제 프로퍼티들이 메모리에 존재하는 것과 무관하게 custom getter와 custom setter를 만들 수 있다. (프로퍼티 처럼 보이게)
  • custom getter, custom setter에서 무한루프를 막기 위해 field라는 키워드를 사용한다.
    • 이를 자기 자신을 가리키는 보이지 않는 field라고 해서 backing field 라고 부른다.
  • 실제 사용에 대한 팁) 객체의 속성이라면 custom getter, 그렇지 않다면 함수로 선언
class Person(
	val name: String = "홍길동",
	var age: Int = 1
) {
	val isAdult: Boolean
		get() = this.age >= 20

	// 혹은
	val isAdult: Boolean
		get() {
			return this.age >= 20
		}
}
  • Custom getter를 사용하면 자기 자신을 변형해 줄 수도 있음.
class Person(
	name: String = "홍길동",
	var age: Int = 1
) {
	val name = name
		get() = field.uppercase() // name.uppercase()는 getter의 무한 순환으로 인해 사용 불가
}
  • 하지만 실무에서는 custom getter에서 backing field를 쓰는 경우는 드물다. 아래와 같이 처리 가능하다.
fun getUppercaseName(): String {
	return this.age.uppercase()
}

// 혹은

fun getUppercaseName() = this.name.uppercase()
  • Custom setter의 경우는 아래와 같이 사용 가능하지만, Setter 자체를 지양하기 때문에 Custom setter도 잘 안쓴다.
var name = name
	set(value) {
		field = value.uppercase()
	}

코틀린에서 상속을 다루는 방법

  • final : override를 할 수 없게 한다. default로 보이지 않게 존재한다.

  • open : override를 열어 준다.

  • abstract : 반드시 override를 해야 한다.

  • override : 상위 타입을 오버라이드 하고 있다. (어노테이션이 아니라 키워드)

  • 상속 또는 구현을 할 때에 : 을 사용해야 한다.

  • 상위 클래스 상속을 구현할 때 생성자를 반드시 호출해야 한다.

  • override를 필수로 붙여야 한다.

  • 추상 멤버가 아니면 기본적으로 오버라이드가 불가능하다.

    • open을 사용해주어야 한다.
  • 상위 클래스의 생성자 또는 초기화 블럭에서 open 프로퍼티를 사용하면 예상하지 못한 버그가 생길 수 있다.

추상클래스

abstract class Animal(
	protected val species: String,
	protected open val legCount: Int
) {
	abstract fun move()
}
class Cat(
	species: String
) : Animal(species, 4) {
	
	override fun move() {
		println("고양이가 사뿐 사뿐 걸어가~")
	}
}
class Penguin(
	species: String
) : Animal(species, 2) {
	private val wingCount: Int = 2

	override fun move() {
		println("펭귄이 움직인다~ 꿱꿱")
	}
	
	// 추상 클래스에서 자동으로 만들어진 getter를 override
	// 추상 프로퍼티가 아닌 프로퍼티에 접근하기 위해선 open이 필요함
	override val legCount: Int
		get() = super.legCount + this.wingCount
}

인터페이스

interface Flyable {
	// 자바의 default가 필요 없음
	fun act() {
		println("파닥 파닥")
	}

	fun fly() // 추상 메서드
}
interface Swimable {

	// 코틀린에서는 backing field가 없는
	// 프로퍼티를 Interface에 만들 수 있다.

	val swimAbility: Int
	
	fun act() {
		println(swimAbility)
		println("어푸 어푸")
	}
}
class Penguin(
	species: String
) : Animal(species, 2) {
	private val wingCount: Int = 2

	...

	override fun act() {
		super<Swimable>.act()
		super<Flyable>.act()
	}

	override val swimAbility: Int
		get() = 3
}

클래스를 상속받을 때 조심해야할 점

fun main() {
	Derived(300)
}

open class Base(
	open val number: Int = 100
) {
	init {
		println("Base Class")
		println(number)
	}
}

class Derived(
	override val number: Int
) : Base(number) {
	init {
		println("Derived Class")
	}
}

예상되는 결과는 Base Class와 3을 출력하길 기대한다.
하지만 상위 클래스에서 println(number)를 호출하면 하위 클래스에 있는 number를 가져오는데
아직 상위 클래스에 constructor가 먼저 실행된 단계라서 하위 클래스의 number의 초기화가 이루어지지 않아서
0이라는 값이 출력된다.

따라서 상위 클래스에서 constructorinit 블럭에서는 final이 아닌 프로퍼티에는 접근하면 안된다.

코틀린에서 접근 제어를 다루는 방법

자바에서의 접근 제어자

  • public : 모든 곳에서 접근 가능
  • protected : 같은 패키지 또는 하위 클래스에서만 접근 가능
  • default : 같은 패키지에서만 접근 가능
  • private : 선언된 클래스 내에서만 접근 가능

코틀린에서의 접근 제어자

  • public : 모든 곳에서 접근 가능

  • protected : 선언된 클래스 또는 하위 클래스에서만 접근 가능

  • internal : 같은 모듈에서만 접근 가능

  • private : 선언된 클래스 내에서만 접근 가능

  • Kotlin에서 패키지는 namespace 관리용이기 때문에 protected는 의미가 달라졌다.

  • Kotlin에서는 default가 사라지고, 모듈 간의 접근을 통제하는 internal이 새로 생겼다.

  • 생성자에 접근 지시어를 붙일 때는 접근 제어자 뒤에 constructor를 명시적으로 써주어야 한다.

  • 유틸성 함수를 만들 때는 파일 최상단을 이용하면 편리하다.

  • 프로퍼티의 custom setter에 접근 지시어를 붙일 수 있다.

class Car(
	internal val name: String,
	private var owner: String,
	_price: Int
) {
	var price = _price
		private set
}
  • Java에서 Kotlin 코드를 사용할 때 internalprotected는 주의해야 한다.
    • Internal은 바이트 코드 상 public이 된다. 따라서 Java에서는 Kotlin 모듈의 internal 코드를 가져올 수 있다.
    • Java는 같은 패키지의 Kotlin protected 멤버에 접근할 수 있다.

코틀린에서 object 키워드를 다루는 방법

  • Java의 static 변수와 함수를 만드려면, Kotlin에서는 companion object를 사용해야 한다.
  • companion object도 하나의 객체로 간주되기 때문에 이름을 붙일 수 있고, 다른 타입을 상속 받을 수 도 있다.
  • Kotlin에서 싱글톤 클래스를 만들 때 object 키워드를 사용한다.
  • Kotlin에서 익명 클래스를 만들 때 object : 타입을 사용한다.

코틀린에서 중첩 클래스를 다루는 방법

  • 클래스 안에 클래스가 있는 경우 종류는 두 가지 였다.
    • (Java 기준) static 을 사용하는 클래스
    • (Java 기준) static 을 사용하지 않는 클래스
  • 권장되는 클래스는 내부 클래스에서 static을 사용하는 클래스이다.
    • 코틀린에서는 이러한 가이드를 따르기 위해 클래스 안에 기본 클래스를 사용하면 바깥 클래스에 대한 참조가 없고
    • 바깥 클래스를 참조하고 싶다면, inner 키워드를 붙여야 한다.
  • 코틀린 inner class에서 바깥 클래스를 참조하려면 this@바깥클래스를 사용해야 한다.
class House(
	var address: String
) {
	var livingRoom = this.LivingRoom(10.0)

	// static이 아닌 클래스로 생성, 바깥 클래스 참조 가능
	inner class LivingRoom(
		private var area: Double
	) {
		val address: String
			get() = this@House.address
	}
}

코틀린에서 다양한 클래스를 다루는 방법

  • Kotlin의 Data Class를 사용하면 equals, hashCode, toString을 자동으로 만들어준다.
  • Kotlin의 Enum Class는 Java의 Enum Class와 동일하지만, when과 함께 사용함으로써 큰 장점을 갖게된다.
    • Enum Class는 추가적인 클래스를 상속받을 수 없으며, 인터페이스는 구현할 수 있으며, 각 코드가 싱글톤이다.
    • 컴파일러가 Enum Class의 모든 타입을 알고 있어 다른 타입에 대한 로직(else)을 작성하지 않아도 된다.
  • Enum Class보다 유연하지만, 하위 클래스를 제한하는 Sealed Class 역시 when과 함께 주로 사용된다.
    • Sealed Class, Sealed Interface는 컴파일 타임 때 하위 클래스의 타입을 모두 기억한다.
      즉, 런타임 때 클래스 타입이 추가될 수 없다.
    • 하위 클래스는 같은 패키지에 있어야 한다.
    • Enum과 다른 점
      • 클래스를 상속 받을 수 있다.
      • 하위 클래스는 멀티 인스턴스가 가능하다.

코틀린에서 배열과 컬렉션을 다루는 방법

  • 코틀린에서는 컬렉션을 만들 때도 불변/가변을 지정해야 한다.
  • List, Set, Map 에 대한 사용법이 변경, 확장되었다.
    • 예) example.withIndex(), example.keys, example[1] = “test”, example.entries
  • Java와 Kotlin 코드를 섞어 컬렉션을 사용할 때에는 주의해야 한다.
    • Java에서 Kotlin 컬렉션을 가져갈 때는 불변 컬렉션을 수정할 수도 있고, non-nullable 컬렉션에 null을 넣을 수도 있다.
    • Kotlin에서 Java 컬렉션을 가져갈 때는 플랫폼 타입을 주의해야 한다. (Kotlin 코드로 wrapping 하는 식으로 처리)

코틀린에서 다양한 함수를 다루는 방법

  • Java 코드가 있는 상황에서, Kotlin 코드로 추가 기능을 개발하기 위해 확장 함수와 확장 프로퍼티가 등장했다.
fun 확장하려는클래스.함수이름(파라미터): 리턴타입 {
	// this를 이용해 실제 클래스 안의 값에 접근
}

val 확장하려는클래스.프로퍼티이름: 리턴타입 {
	get() 혹은 set() = ...
  • 확장함수는 원본 클래스의 private, protected 멤버 접근이 안된다!
  • 멤버함수, 확장함수가 같은 시그니처를 가지고 있다면 멤버함수에 우선권이 있다.
  • 확장함수는 현재 타입을 기준으로 호출된다.
  • Java에서는 static 함수를 쓰는 것 처럼 Kotlin의 확장함수를 쓸 수 있다.
  • 함수 호출 방식을 바꿔주는 infix 함수가 존재한다. 3 add 4
  • 함수를 복사-붙여넣기 하는 inline 함수가 존재한다.
  • Kotlin에서는 함수 안에 함수를 선언할 수 있고, 이를 지역함수라고 부른다.

코틀린에서 람다를 다루는 방법

Kotlin에서 람다를 만드는 방법

// 람다를 만드는 방법 1
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
	return fruit.name == "사과"
}

// 람다를 만드는 방법 2
val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과" }

Kotlin에서 람다를 직접 호출하는 방법

// 람다를 직접 호출하는 방법 1
isApple(Fruit("사과", 1000))

// 람다를 직접 호출하는 방법 2
isApple.invoke(Fruit("사과", 1000))
  • 함수는 Java에서는 2급 시민이지만, 코틀린에서는 1급 시민이다.
    • 때문에, 함수 자체를 변수에 넣을 수도 있고 파라미터로 전달할 수도 있다.
  • 코틀린에서 함수 타입은 (파라티머 타입, …) → 반환 타입 이었다.
  • 코틀린에서는 람다를 두 가지 방법으로 만들 수 있고, { } 방법이 더 많이 사용된다.
  • 함수를 호출하며, 마지막 파라미터인 람다를 쓸 때는 소괄호 밖으로 람다를 뺄 수 있다.
    filterFruits(fruits) { fruit -> fruit.name == "사과" }
    
    // it로 생략 가능
    filterFruits(fruits) { it.name == "사과" }
  • 람다의 마지막 expression 결과는 람다의 반환 값이다.
  • 코틀린에서는 Closure를 사용하여 non-final 변수도 람다에서 사용할 수 있다.
    • 그 이유는 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획해서 그 정보를 가지고 있다.

코틀린에서 컬렉션을 함수형으로 다루는 방법

  • filter / filterIndexed(필터에서 index가 필요할 시)
  • map(객체에서 특정 필드만 꺼내볼 때) / mapIndexed / mapNotNull
  • all (조건을 모두 만족하면 true, 아니면 false) / none (all의 반대) / any (하나라도 만족하면 true)
  • count (List의 size랑 동일) / sortedBy / sortedbyDescending / distinctBy (람다를 가지고 중복을 제거함)
  • first (무조건 null이 아님) / firstOrNull (첫 번째 값 또는 null) / last / lastOrNull
  • groupBy
val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
  • associateBy 중복되지 않는 키를 가지고 Map을 만들 때 사용
val map: Map<Long, Fruit> = fruits.associateBy { fruit -> fruit.id }

flatMap / flattern (List<List>를 List로 바꿀 때)

코틀린의 이모저모

  • 타입에 대한 별칭을 줄 수 있는 typealias 라는 키워드가 존재한다.
  • Import 당시 이름을 바꿀 수 있는 as import 기능이 존재한다.
  • 변수를 한 번에 선언할 수 있는 구조분해 기능이 있으며 componentN 함수를 사용한다.
    • data class에서만 사용 가능하며 componentN 함수를 구현 시 data class가 아닌 class 에서도 사용 가능하다.
    • for ((key, value) in map.entries) { } 와 같은 key, value를 꺼내는 방식도 구조분해이다.
  • for문, while문과 달리 forEach에는 breakcontinue를 사용할 수 없다.
    • Label을 통해서 break와 continue를 구현할 수 있지만 코드 가독성을 해칠 수 있기 때문에 사용하지 않는 것을 추천한다.
  • takeIf (주어진 조건을 만족하면 그 값이, 그렇지 않으면 null이 반환)와 takeUnless (주어진 조건을 만족하지 않으면 그 값이, 그렇지 않으면 null이 반환)를 활용해 코드 양을 줄이고 method chaning을 활용할 수 있다.
  • 라벨을 사용한 Jump 역시 코드 흐름이 위 아래로 변경되면서 복잡도가 올라가기 때문에 사용하지 않는 것을 추천한다.

코틀린의 scope function

코틀린의 scope function은 일시적인 영역을 만들어 코드를 더 간결하게 하거나, method chain에 활용된다.

  • let
    • 하나 이상의 함수를 call chain 결과로 호출할 때

      val strings = listOf("APPLE", "CAR")
      strings.map { it.length }
      	.filter { it > 3 }
      	.let(::println)
    • non-null 값에 대해서만 code block을 실행시킬 때

      val length = str?.let {
      	println(it.uppercase())
      	it.length
      }
    • 일회성으로 제한된 영역에 지역 변수를 만들 때

      val numbers = listOf("one", "two", "three", "four")
      val modifiedFirstItem = numbers.first()
      	.let { firstItem ->
      		if (firstItem.length >= 5) firstItem else "!$firstItem!"
      	}.uppercase()
      println(modifiedFirstItem)
  • run
    • 객체 초기화와 반환 값의 계산을 동시에 해야할 때
      객체를 만들어 DB에 바로 저장하고, 그 인스턴스를 활용할 때

    • 반복되는 생성 후 처리는 생성자, 프로퍼티, init block으로 넣는 것이 좋다.

      val person = Person("홍길동", 100).run(personRepository::save)
  • apply
    • 객체 그 자체가 반환된다.

    • 객체 설정을 할 때에 객체를 수정하는 로직이 call chain 중간에 필요할 때

      fun createPreson(
      	name: String,
      	age: Int,
      	hobby: String
      ): Person {
      	return Person(
      		name = name,
      		age = age,
      	).apply {
      		this.hobby = hobby
      	}
      }
  • also
    • 객체를 수정하는 로직이 call chain 중간에 필요할 때

      mutableListOf("one", "two", "three")
      	.also { println("four 추가 이전 지금 값: $it") }
      	.add("four")
  • with
    • 특정 객체를 다른 객체로 변환해야 하는데,
      모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때

      // this를 생략할 수 있어 필드가 많아도 코드가 간결해진다.
      return with(person) {
      	PersonDto(
      		name = name
      		age = age
      	)
      }

0개의 댓글

관련 채용 정보