본문 바로가기
공부/Kotlin

코틀린 기본내용을 모두 정리해보자

by JERO__ 2022. 8. 27.

 

코틀린을 다시 정리해보자!

우아한테크코스 서비스 근로 크루들과 ‘코틀린 인 액션’ 책으로 한번 공부를 하였지만 다시한번 초심의 마음으로 정리를 한번 해보고자 한다.

인프런 최태현님의 자바 개발자를 위한 코틀린 입문 강의를 보고 정리하였다.

 

코틀린에 관한 TMI

  • 코틀린은 IntelliJ를 만든 JetBrains 회사에서 만들었다.
    • IntelliJ가 Java로 작성되어 있는데 유지보수 하다가 화가 났다고한다.
  • 코틀린은 Java와 100% 호환 가능하다.
  • 코틀린은 정적 타입 언어이다. (구성 요소를 컴파일 시점에 알 수 있다)
  • 코틀린에서 별도의 지시어를 붙이지 않으면 모두 public 이다.

 

1. 변수 다루기

  1. 모든 변수는 val로 만들고 꼭 필요한 경우만 var로 변경한다.
  2. 코틀린에서의 원시 타입 long vs Long
    • 실행시 Long으로 보이지만 상황에 따라 코틀린이 원시타입으로 바꾸어 적절히 처리해준다.
  3. nullable 변수에 따라 ?를 추가한다.
  4. new 키워드를 사용하지 않는다.

 

2. null 다루기

1. Safe Call : null이 아니면 실행하고, null이면 실행하지 않는다.

  • null인 경우 값 자체가 null이 된다.

2. Elvis 연산자 : 앞의 연산 결과가 null이면 뒤의 값 사용 (null 값 변경) ?:

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

3. 널 아님 단언 : nullable type 이지만, 아무리 생각해도 null이 될 수 없는경우 !!

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

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

  • Java 코드를 읽으며 널 가능성을 확인할 수 있다.
  • NotNull, Nullable

 

3. Type을 다루는 방법 (is, !is, as, as?, Any, Unit)

1. 자바 처럼 암시적 변경이 불가능하다. 명시적으로 변환하자.

val number1 = 3        // Int
val number2: Long = number1.toLong()     // 명시적 변환

2. 타입변환(Java와 Kotlin을 비교해보자) - 스마트캐스트

if (obj instanceof Person) {
	Person person = (Person) obj;
}
if (obj is Person) {
	// val person = obj as Person
	println(obj.age)       // if에서 체크해주어서 스마트캐스트!
}
  • is, !is
  • as, as? (safe call과 비슷하다)
    • 타입이 맞으면 타입 캐스팅
    • 타입이 맞지 않으면 null
    • null이면 null

3. 타입 3가지

  • Any : 자바의 Object (모든 객체의 최상위 타입)
  • Unit : 자바의 Void, 타입 인자로 사용 가능하다.
  • Nothing : 함수가 정상적으로 끝나지 않음. 에러 실행
fun fail(message: String): Nothing {
	throw IlleagalArgumentException(message)
}

4. String 인덱스

  • 중괄호가 없어도 좋으나 중괄호를 사용하는게 좋다.
    • 가독성
    • 일괄 변환
    • 정규식 활용
val name = "제로"
println("이름 : {$name}")

 

4. 연산자를 다루는 방법

  1. 비교 연산자에서 Java와 다르게 자동으로 compareTo를 호출한다.
  2. 동등성, 동일성
    • 동일성 : 값이 같다 (자바에서 equals, 코틀린에서 ==)
    • 동일성 : 완전히 동일한 객체이다 (자바에서 ==, 코틀린에서 ===)

 

5. 제어문을 다루는 방법

  1. if 사용방법은 Java와 같다.
  2. 한 가지 다른 점이 있다. 값이 도출된다. 즉 바로 리턴할 수 있다.
    • Java의 If-else 는 Statement : 프로그램의 문장. 하나의 값으로 도출되지 않는다.
    • Kotlin의 If-else는 Expression : 하나의 값으로 도출되는 문장
fun getGrade(score: Int): String {
	return if (score >= 90) {
		"A"
	} else if (score >= 80) {
		"B"
	} else if (score >= 70) {
		"C"
	} else {
		"D"
	}
}

3. when절 (자바의 switch)

fun getGrade(score: Int): String {
	return when(score / 10) {
		9 -> "A"
		8 -> "B"
		7 -> "C"
		else -> "D"
	}
}

// 다양한 분기 가능
fun getGrade(score: Int): String {
	return when(score) {
		in 90..99 -> "A"
		in 80..89 -> "B"
		in 70..79 -> "C"
		else -> "D"
	}
}
  • Enum Class, Sealed Class와 함께 사용할 경우 더욱더 진가를 발휘한다.

 

6. 반복문을 다루는 방법

1. for-each

val numbers = listOf(1L, 2L, 3L)
for (number in numbers) {
	println(number)
}

2. 정통적인 for문

  • downTo: 내려가는 경우
  • step : 증가값이 1이 아닌경우 지정 가능
for (i in 1..3) {
	println(i)
}

 

7. 예외를 다루는 방법

  1. try-catch-finally
    • 문법적으로 동일하다
    • Expression이다.
  2. 모두 Unchecked Exception 간주한다.
    • 자바의 경우 예외가 발생할 수 있는 메서드라면 throws를 통해 명시를 해주어야 한다(Checked Exception).
    • 코틀린은 구분하지 않는다.

 

8. 함수를 다루는 방법

= 문법 : 하나의 결과값이라면 =으로 대체할 수 있다. 반환타입을 생략할 수 있다(유추할 수 있기 때문이다).

public fun max(a: Int, b: Int): Int {
	return if (a > b) {
		a
	} else {
		b
	}
}
public fun max(a: Int, b: Int) = if (a > b) a else b

default parameter: 값이 들어오지 않으면 default로 설정한 값으로 들어간다.

public fun max(a: Int = 3, b: Int): Int {
	return a + b
}

named argument : 넣고싶은 파라미터에 명시적으로 넣어줄 수 있다.

가변인자: 같은 타입의 여러 파라미터 받기 vararg

public static void printAll(String... strings) {
	for (String str : strings){
		System.out.println(str);
	}
}
fun printAll(vararg strings: String) {
	for (str in strings){
		println(str);
	}
}
  • 배열을 printAll에 삽입한다면 *을 통해 값을 꺼내어 넣어줘야 한다.

 

9. 클래스를 다루는 방법

9-1. class와 property

  • 프로퍼티 = 필드 + getter + setter
  • 필드(var, val)만 만들면 getter, setter를 자동으로 만든다

9-2. 생성자와 init

  • init : 초기화 시점에 한 번 실행한다. 보통 검증할 때 사용한다.
  • constructor(파라미터)로 새로운 생성자를 추가할 수 있다. (부 생성자)
    • 최종적으로 this로 호출해야 한다.
    • 부 생성자보다는 default parameter를 권장한다.
    • 부 생성자보다 정적 팩토리 메소드를 사용한다.
    • 실무에서 부 생성자를 잘 사용하지 않는다.

9-3. custom getter/setter

두 가지 방법이 존재한다.

  • 함수처럼 만드는 방법 (객체의 속성이 아니라면)
fun isAdult(): Boolean {
	return this.age >= 20
}
  • 프로퍼티 처럼 만드는 방법 (객체의 속성이라면)
val isAdult: Boolean
	get() {
		return this.age >= 20
	}
val isAdult: Boolean
	get() = this.age >= 20

9-4. backing-field 사용

주생성자 getter를 변환해서 반환할 수 있다. field

class Person(
	name: String,       // val가 빠짐
	var age: Int
) {
	val name: String = name
		get() = field.uppercase()
}

하지만, 다음과 같이 구현할 수 있기 때문에 개발자에 따라 자주 사용되지 않기도 하다.

val name: String = get() = this.name.uppercase()

 

10. 상속을 다루는 방법

10-1. 추상클래스

예제로 알아보자. Animal / Cat, Penguin

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("고양이가 사뿐사뿐 걸어가~")
	}

	override val legCount: Int {
		get() = super.legCount + this.wingCount
	}
}
  • 추상 프로퍼티가 아니라면 상속받을 때, open을 붙여야 한다.

10-2. 인터페이스

펭귄이 인터페이스에 대한 의존을 가지는 예시를 알아보자

interface Flyable {
	
	fun act() {
		println("파닥파닥")
	}
}
class Penguin(
	species: String
) : Animal(species, 2), Flyable {

	private val wingCount: Int = 2

	override fun move() {
		println("고양이가 사뿐사뿐 걸어가~")
	}

	override val legCount: Int {
		get() = super.legCount + this.wingCount
	}

	// 인터페이스 추가됨
	override fun act() {
		super<Flyable>.act()
	}
}

 

11. 접근 제어를 다루는 방법

11-1. 가시성 제어

종류

  • public : 모든 곳 접근 가능
  • protected : 선언된 클래스, 하위 클래스만 접근 가능 (자바의 같은패키지 개념X)
  • internal : 같은 모듈에서만 접근 가능
    • 모듈: 한 번에 컴파일 되는 Kotlin 코드
  • private : 선언된 클래스 내에서만

11-2. 접근 제어

  1. 생성자 : 가시성 제어를 넣기 위해 constructor 를 명시적으로 적어주어야 한다.
  2. 프로퍼티 : getter/setter 를 한번에 부여하느냐, setter에만 가시성을 부여하느냐로 나뉠 수 있다.
class Car(
	internal val name: String,    // 한 번에 접근 지어서 정하기
	_price: Int                     
) {
	
	var price = _price
		private set                 // 가시성 적용
}

 

12. object 키워드를 다루는 방법 (자바의 static)

12-1. companion object (자바의 static 함수와 변수)

object를 통해 static 처럼 사용할 수 있다.

class Person private constructor(
	private val name: String,
	private val age: Int
) {

	companion object {
		private const val MIN_AGE = 0
		fun newBaby(name: String): Person {
			return Person(name, MIN_AGE)
		}
	}
}

이름도 지정할 수 있다.

companion object Factory{
	private const val MIN_AGE = 0
	fun newBaby(name: String): Person {
		return Person(name, MIN_AGE)
	}
}

자바에서 사용한다면 @JvmStatic을 붙여야 한다!

12-2. 싱글톤: object

싱글톤 : 클래스의 인스턴스가 단 하나이다.

object Singleton {
	var a: Int = 0
}

12-3. 익명클래스(일회성)

자바에서는 익명클래스를 만들어 함수의 인자로 던질 수 있었다. (@Override를 한 뒤에 새롭게 정의한 것을 한번 사용)

fun main() {
	moveSomething(object : Moveable {
		override fun move() {
			println("움직인다~")
		}
		override fun fly() {
			println("난다~")
		}
	})
}
private fun moveSomething(moveable: Movable) {
	movable.move()
	movable.fly()
}

 

13. 중첩 클래스를 다루는 방법

실무 서버 개발에서 많이 사용된 부분은 아니다. 간혹 사용된다.

중첩클래스

  • static을 사용하는 중첩클래스
  • static을 사용하지 않는 중첩클래스 (클래스 안에 클래스를 만들 때는 static 클래스를 사용하자. by 이펙티브자바)
    • 내부 클래스
    • 지역 클래스
    • 익명 클래스

코틀린에서는 위의 Guide를 충실히 따른다

즉, 기본적으로 바깥 클래스를 참조하지 않는다. 바깥 클래스를 참조하고 싶다면 inner 키워드를 추가한다.

class House(
	var address: String,
	var livingRoom: LivingRoom = LivingRoom(10.0)
) {
	class LivingRoom(
		private var area: Double
	)
}

 

14. 다양한 클래스를 다루는 방법

다양한 클래스가 존재한다.

  • Data class
  • Enum class
  • Sealed Class, Sealed Interface

하나씩 살펴보자

14-1. Data class (DTO)

계층간의 데이터를 전달하기 위한 DTO는 다음과 같은 내용이 필요하다.

  • 데이터(필드)
  • 생성자, getter
  • equals, hashCode
  • toString
Data class PersonDto(
	val name: String,
	val age: Int,
)

14-2. Enum class

다음과 같은 특징을 가진다

  • 클래스를 상속받을 수 없다.
  • 인터페이스는 구현할 수 있으며 각 코드가 싱글톤이다.
enum class Country (
	private val code: String
) {

	KOREA("KO")
	AMERICA("US")
	;
}

만약 다음과 같이 코드가 많아진다면? else 로직 처리가 애매해진다.

fun handleCountry(JavaCountry country) {
	if (country == JavaCountry.KOREA) {
		// 로직 처리
	}
	if (country == JavaCountry.AMERICA) {
		// 로직 처리
	}
}

when으로 극복한 경우, else를 사용하지 않아도 된다.

fun handleCountry(JavaCountry country) {
	when (country) {
		Country.KOREA -> TODO()
		Country.AMERICA -> TODO()
	}
}

14-3. Sealed Class, Sealed Interface

어디에 사용될까?

  • 추상화가 필요한 Entity or DTO에 사용한다.

개념을 알아보자

sealed : 봉인된

Sealed Class는 상속이 가능하도록 추상클래스를 만들까하지만, 외부에서는 이 클래스를 상속받지 않도록 함 → 하위 클래스 봉인

  • 하위 클래스는 같은 패키지에 존재한다
  • 컴파일 타임 때 하위 클래스의 타입을 모두 기억한다. 런타임때 클래스 타입이 추가될 수 없다.
  • Enum과 다른점
    • 클래스를 상속받을 수 있다.
    • 하위 클래스는 멀티 인스턴스가 가능하다.

예제로 살펴보자

abstract와 기능이 비슷해보인다.

sealed class HyundaiCar(
	val name: String,
	val price, Long,
)

class Avante : HyundaiCar("아반떼", 1_000L)
class Sonata : HyundaiCar("소나타", 2_000L)
class Grandeur : HyundaiCar("그렌저", 3_000L)
private fun handleCar(car: HyundaiCar) {
	when (car) {
		is Avante -> TODO()
		is Sonata -> TODO()
		is Grandeur -> TODO()
	}
}

 

15. 배열과 컬렉션을 다루는 방법

15-1. 배열

잘 사용하지 않는다. 이펙티브 자바에서도 배열보다 리스트를 사용하라고 말한다.

val array = arrayOf(100, 200)
for ((idx, value) in array.withIndex()){
	println("${idx} ${value}")
}

15-2. Collection - List, Set, Map

컬렉션을 만들어 줄 때, 불변인지, 가변인지 설정해야 한다. 불변이라 하더라도 값 수정은 가능하다(삽입, 제거는 불가능).

List

우선 불변 리스트로 만들고, 꼭 필요한 경우에만 가변 리스트로 바꾸자.

// 1. 불변
val numbers = listOf(100, 200)      // 값을 추론할 수 있기에 <Int> 생략가능
val emptyList = emptyList<Int>()    // 어떤 값이 들어올 지 모르기에 명시

// 2. 가변
val numbers = mutableListOf(100, 200)
numbes.add(300)
numbers.get(0)
numbers[0]              // 위와 같다.
// withIndex 사용이 가능하다.
for ((idx, value) in array.withIndex()){
	println("${idx} ${value}")
}

set과 map

  • set의 사용법은 List와 같다.
  • map
val oldMap = mutableMapOf<Int, String>()   // 타입 명시
oldMap[1] = "MONDAY"
oldMap[2] = "TUESDAY"

// 아래와 같이 생성도 가능하다
mapOf(1 to "MONDAY", 2 to "TUESDAY")
// 사용법 1
for (key in oldMap.keys) {
	println(oldMap[key])
}

// 사용법 2
for ((key, value) in oldMap.entries) {
}

15-3. 컬렉션의 null 가능성

? 위치에 따라 의미가 다르다.

  • List<Int?>
  • List<Int>?
  • List<Int?>?

 

16. 다양한 함수를 다루는 방법

16-1. 확장함수 : 함수를 사용하는 새로운 방법

fun String.lastChar(): Char {     // 확장하려는 클래스: String
	return this[this.length - 1]
}
val str = "ABC"
println(str.lastChar())       // C
  • 멤버함수와 확장함수 둘 다 선언되어있다면 멤버함수가 우선적으로 호출된다.
  • 원본 클래스의 private, protected 멤버 접근이 안된다.
  • 현재 타입을 기준으로 호출된다.

16-2. infix함수 : 함수를 호출하는 새로운 방법

downTo, step도 함수이다.

  • 변수.함수이름(argument)
  • 변수 함수이름 argument (infix함수)
infix fun Int.add2(other: Int): Int {
	return this + other
}
3.add2(4)
3 add2 4   // 둘 다 가능

 

17. 람다를 다루는 방법

17-1. 람다 만들기

두 가지 방법이 있다.

// 1
val isApple = fun(fruit: Fruit): Boolean {
	return fruit.name == "사과"
}

// 2
val isApple = { fruit: Fruit -> fruit.name == 사과 }
// 파라미터타입 -> 반환타입을 명시적으로했을 때
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
	return fruit.name == "사과"
}

17-2. 람다 호출방법

// 1
isApple(Fruit("사과", 1000))
// 2
isApple.invoke(Fruit("사과", 1000))

예제

private fun filterFruits(
	fruits: List<Fruit>, filter: (Fruit) -> Boolean
): List<Fruit> {

	val results = mutableListOf<Fruit>()
	for (fruit in fruits) {
		if (filter(fruit)) {
			results.add(fruit)
		}
	}
	return results
}
// { fruit: Fruit -> fruit.name == 사과 }를 
// { it.name == 사과 } 로 표현
filterFruits(fruits, { it.name == 사과 })
  • fruit를 명시해주는게 좋다!

Closure

  • 코틀린에서 Closure를 사용하여 non-final 변수도 람다에서 사용할 수 있다. (자바는 final 변수만 가능하다)
  • 코틀린에서는 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고있다.
var fruitName = "바나나"
fruitName = "수박"
filterFruits(fruits) { it.name == fruitName }

 

18. 컬렉션을 함수형으로 다루는 방법

다음 내용은, 이러한 내용이 있구나 정도로 알고 필요할 때 찾아서 사용해보자.

18-1. filter

private fun filterFruits(
	fruits: List<Fruit>, filter: (Fruit) -> Boolean
): List<Fruit> {

	return fruits.filter(filter)
}
  • filter / filterIndexed / map / mapIndexed / mapNotNull
  • all / none / any
  • count / sortedBy / distinctBy

18-2. List를 Map으로 변경해보자

val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
  • 과일이름 : key
  • 해당과일List : value
val map: Map<String, Fruit> = fruits.associateBy { fruit -> fruit.id }
  • id : key
  • 과일 : value

 

19. scope function (let, run, also, apply, with)

람다를 사용해 일시적인 영역을 만들고 코드를 더 간결하게 만들거나, method chaning에 활용하는 함수를 scope function이라고 한다.

fun printPerson(person: Person?) {
	person?.let {
		println(it.name)           // it는 person임
		println(it.age)
	}
}

확장함수

  • let / run : 람다를 받아 람다 결과를 반환한다.
    • let : it 사용 (생략 불가능, 이름변경 가능)
    • run : this 사용 (생략 가능)
  • also / apply : 객체 그 자체를 반환한다.
    • also : it 사용
    • apply : this 사용
// value에 age가 들어온다.
val value1 = person.let {
	it.age
}
val value2 = person.run {
	this.age
}

// value에 person이 들어온다.
val value3 = person.also {
	it.age
}
val value4 = person.apply {
	this.age
}

확장함수가 아닌 with

  • with : this 사용 / 생략가능
with(person) {
	println(name)
	println(this.age)
}
  • apply : Test Fixture를 만들 때 활용할 수 있다.
fun createPerson(
	name: String,
	age: Int,
	hobby: String,
): Person {
	return Person (
		name = name,
		age = age,
	).apply {
		this.hobby = hobby
	}
}

댓글