Java & Spring

자바 개발자를 위한 코틀린 입문 강의 정리

https://www.inflearn.com/course/java-to-kotlin/dashboard 를 수강하고 학습한 내용을 간단하게 정리한다.

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

var과 val의 차이점

  • var: 바꿀 수 있는 변수 (ex) var number1 = 10L
  • val: 바꿀 수 없는 변수 (ex) val number2 = 10L
  • 코틀린에서는 모든 변수에 수정 가능 여부(var/val)를 명시해주어야 한다.
  • 코틀린에서는 타입을 의무적으로 작성하지 않아도 된다.
    • 하지만 작성이 가능함 (ex) var number1: Long = 10L
  • 초기값을 지정해주지 않는 경우
    • var number1 -> 오류
      • 초기값을 지정해주지 않는 경우 무조건 타입을 명시해줘야 한다.
      • var number: Int
    • val 변수에 초기값을 지정해주지 않는 경우 처음에 한하여 초기값을 넣어줄 수 있다.
      • val number2:  Int
        number2 = 10L
  • val인 경우에도 컬렉션에 원소 추가가 가능하다. (Java 와 동일)

Kotlin에서의 Primitive Type

  • Java에서는 long은 primitive type, Long은 reference type
  • Kotlin에서는 구분하고 있지 않음. Long으로 표현해도 Kotlin에서 필요하다면 알아서 내부적으로 primitive type으로 변경해서 처리해줌
  • 즉, 프로그래머가 boxing/unboxing을 고려하지 않아도 되도록 kotlin이 알아서 처리해준다.

Kotlin에서의 nullable 변수

  • 기본적으로 모든 변수는 null이 들어갈 수 없음
  • var number3: Long? = null -> 이와 같이 ?를 붙여주는게 nullable 이라는 의미
  • Kotlin에서는 nullable 변수면 ?를 붙여줘야함

Kotlin에서의 객체 인스턴스화

  • Kotlin에서는 객체 인스턴스화를 할 때 new를 붙이지 않는다.
  • val person = Person("홍길동")

2. 코틀린에서 null을 다루는 방법

Kotlin에서의 null 체크

1. Java에서의 코드

public boolean startsWithA1(String str) {
    if (str == null) {
    	throw new IllegalArgumentException("null이 들어왔습니다.");
    }
	return str.startsWith("A");
}

 

2. Kotlin에서의 코드

fun startsWithA1(str: String?): Boolean {
    if (str == null) {
        throw IllegalArgumentException("null이 들어왔습니다.")
    }
    return str.startsWith("A")
}

// 아래와 같이 null 일 수 있는 값에 대해 함수 호출하려고 하면 오류남
fun startsWithA2(str: String?): Boolean {
    return str.startsWith("A") // 오류
}

// 이렇게 위에서 null 체크를 해주면 허용
fun startsWithA2(str: String?): Boolean {
    if (str == null) {
        return false
    }
    return str.startsWith("A") // 오류
}

Safe Call과 Elvis 연산자

null이 가능한 타입만을 위한 기능

1. Safe Call: null이지 않을 때만 호출. null인 경우 null로 리턴

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

2. Elvis 연산자: 앞의 값이 null이면 뒤의 값을 사용

val str: String? = "ABC"
str?.length ?: 0 // null이면 0

 

그래서 위의 코드를 kotlin스럽게 바꾸면?

fun startsWithA1(str: String?): Boolean {
    return str?.startsWith("A")
      ?: throw IllegalArgumentException("null이 들어왔습니다")
}

 

이것도 kotlin으로 바꿀 수 있음

// Java
public long calculate(Long number) {
    if (number == null) return 0;
}

// kotlin
fun calculate(number: Long?): Long {
    number ?: return 0
}

널 아님 단언!!

nullable type이지만, 절대 null이 될 수 없는 경우

fun startsWithA1(str: String?): Boolean {
    return str!!.startsWith("A")
}

-> 근데 만약 null이 들어오면? NPE가 남

플랫폼 타입

Kotlin에서 Java 코드를 가져다 사용할 때 어떻게 처리될까?

아래처럼 Java 상에서 Nullable이면 person.name 못넣음. NotNull 이면 넣을 수 있음

이처럼 Java의 Annotation을 사용하면 Kotlin에서는 이를 이해할 수 있다.

만약 annotation이 없다면? Kotlin에서는 알 수 없음. 이런 타입을 "플랫폼 타입"이라고 한다.

오류도 나지 않고 runtime에 NPE가 난다.

public class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    @Nullable
    public String getName() {
        return name;
    }
}
fun main() {
    val person = Person("AAA")
    startsWithA(person.name)
}

fun startsWithA(str:String): Boolean {
    return str.startsWith("A")
}

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

기본 타입

Java와 동일 Byte, Short, Int, Long, Float, Double

 

1. 코틀린에서는 선언된 기본값을 보고 타입을 추론한다. 

val num1 = 2 // int
val num2 = 2L // long

 

2. 코틀린에서는 기본 타입간의 변환은 명시적으로 이루어져야 한다. <-> Java에서는 암시적 변환

val num1 = 4

val num2: Long = num1 // Type mismatch compile error
val num2: Long = num1.toLong() // success

 

타입 캐스팅

기본 타입이 아닌 경우 타입 캐스팅

  value가 Type이면 value가 Type이 아니면
value is Type return true return false
value !is Type return false return true
value as Type Type으로 타입 캐스팅 예외 발생
value as? Type Type으로 타입 캐스팅 null
(+ value가 null이면 null)
// java
public void printAgeIfPerson(Object obj) {
    if (obj instanceof Person) {
        Person person = (Person) obj;
        System.out.println(person.getAge());
    }
}

// kotlin
fun printAgeIfPerson(obj: Any) {
    if (obj is Person) {
        // as Person은 생략 가능 
        // 스마트 캐스트: 컨택스트를 분석해서 Person인지 체크를 해줌 -> 타입 인지
        val person = obj as Person 
        println(person.age)
    }
}

Kotlin의 3가지 특이한 타입

Any

  • Java의 Object 역할 (모든 객체의 최상위 타입)
  • 모든 Primitive Type의 최상위 타입도 Any이다.
  • Any 자체로는 null을 포함할 수 없어 null을 포함하고 싶다면, Any?로 표현
  • Any에 equals / hashCode / toString 존재

Unit

  • Java의 void와 동일한 역할
  • void와 다르게 Unit은 그 자체로 타입 인자로 사용 가능하다. (void는 Void 클래스 사용 필요)
  • 함수형 프로그래밍에서 Unit은 단 하나의 인스턴스만 갖는 타입을 의미. 즉, 코틀린의 Unit은 실제 존재하는 타입이라는 것을 표현.

Nothing

  • 함수가 정상적으로 끝나지 않았다는 사실을 표현하는 역할
  • 무조건 예외를 반환하는 함수 / 무한 루프 함수 등
fun fail(message: String): Nothing {
    throw IllegalArgumentException(message)
}

String Interpolation, String indexing

문자열과 변수 연결

val person = Person("홍길동", 100)
println("이름 : ${person.name}")

 

여러 줄에 걸친 문자열

val withoutIndent = 
   """
      ABC
      123
      234
  """.trimIndent() // trimIndent는 앞에 공백 제거용
println(withoutIndent)

 

문자열의 특정 문자 가져오기

val str = "ABCDE"
val ch = str[1] // java에서는 charAt

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

단항 연산자 / 산술 연산자

Java와 동일

단, Java와 다르게 객체를 비교할 때 비교 연산자를 사용하면 자동으로 compareTo를 호출

비교 연산자와 동등성, 동일성

  • 동일성 Identity: 완전히 동일한 객체인가. 즉, 주소가 같은가
  • 동등성 Equality: 두 객체의 값이 같은가
  동일성 동등성
Java == equals
Kotlin === == (간접적으로 equals를 호출)

논리 연산자 / 코틀린에 있는 특이한 연산자

논리 연산자

  • Java와 동일
  • Java처럼 Lazy 연산을 수행 (if (fun1() || fun2()) 일 때 fun1이 true면 fun2는 실행X)

코틀린에 있는 특이한 연산자

  • in / !in : 컬렉션이나 범위에 포함되어 있다. 포함되어 있지 않다
  • a..b: a부터 b까지의 범위 객체를 생성
  • a[i]: a에서 특정 index i로 값을 가져온다
  • a[i] = b: a의 특정 index i에 b를 넣는다

연산자 오버로딩

코틀린은 객체마다 연산자를 직접 정의할 수 있다.

val money1 = Money(1_000L)
val money2 = Money(2_000L)

println(money1 + money2) // 3000 -> toString이 구현되어 있기 때문에 가능

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

if문

  • Java에서 if-else는 Statement인데, Kotlin에서는 Expression이다.
  • Statement: 프로그램의 문장, 하나의 값으로 도출되지 않는다
  • Expression: 하나의 값으로 도출되는 문장 (ex. Java의 3항 연산자)
  • Kotlin에서는 if-else를 expression으로 사용할 수 있기 때문에 3항 연산자가 없다.
fun validateScoreIsNotNegative(score: Int) { // Unit(=void) 리턴 생략
    if (score < 0) {
        throw IllegalArgumentException("${score}는 0보다 작을 수 없습니다") // new 없음
    }
}

// kotlin에서 if-else는 expression이기 때문에 그대로 return이 가능하다
fun getPassOrFail(sroce: Int): String {
    return if (score >= 50) {
        "P"
    } else {
       "F"
    }
}

 

Expression과 Statement

// java
if (0 <= score && score <= 100)

// kotlin
if (score in 0..100)
if (score !in 0..100)

switch와 when

Kotlin에서는 switch문이 없고 when문이 있다.

// kotlin에서는 swith가 없고 when을 사용한다.
fun getGradeWithSwitch(score: Int): String {
    return when (score) { // when도 expression
        in 90..99 -> "A"
        in 80..89 -> "B"
        in 70..79 -> "C"
        else -> "D"
    }
}

// 조건부에는 어떤 expression도 들어갈 수 있다.
fun startsWithA(obj: Any): Boolean {
  return when (obj) {
    is String -> obj.startsWith("A")
    else -> false
  }
}

// 조건부에는 여러개의 조건을 동시에 검사할 수 있다.
fun judgeNumber(number: Int) {
  when (number) {
    1, 0, -1 -> println("정답입니다.")
    else -> println("오답입니다.")
  }
}

// when에 들어오는 값은 없을 수도 있다. early return처럼 동작
fun judgeNumber2(number: Int) {
  when {
    number == 0 -> println("주어진 숫자는 0입니다")
    number % 2 == 0 -> println("주어진 숫자는 짝수입니다")
    else -> println("주어진 숫자는 홀수입니다")
  }
}

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

for-each문

fun main() {
  val numbers = listOf(1L, 2L, 3L)
  for (number in numbers) { // java에서는 :, kotlin에서는 in
    println(number)
  }
}

전통적인 for문 & Progression과 Range

  • downTo, step도 함수 (중위 호출 함수)
  • Kotlin에서 전통적인 for문은 등차수열을 사용한다.
// 1, 2, 3
fun main() {
  for (i in 1..3) {
    println(i)
  }
}

// 3, 2, 1
fun main() {
  for (i in 3 downTo 1) {
    println(i)
  }
}

// 두 칸씩 올라가는 경우
fun main() {
  for (i in 1..5 step 2) {
    println(i)
  }
}

 

while문

while, do-while문은 모두 java와 동일

var i = 1
while (i <= 3) {
  println(i)
  i++
}

7. Kotlin에서 예외를 다루는 방법

try catch finally 구문

java와 문법적으로 동일

fun parseIntOrThrow(str: String): Int {
  try {
    return str.toInt()
  } catch (e: NumberFormatException) {
    throw IllegalArgumentException("주어진 ${str}는 숫자가 아닙니다")
  }
}

// 실패 시 null 반환
fun parseIntOrThrowV2(str: String): Int {
  return try { // try-catch 문도 expression으로 취급
    str.toInt()
  } catch (e: NumberFormatException) {
    return null
  }
}

 

Checked Exception과 Unchecked Exception

Java에서는 IOException이 던져질 수 있다는 것을 표시해줘야 함

Kotlin에서는 아래에서 빨간줄 없음 -> Kotlin에서는 checked exception과 unchecked exception을 구분하지 않는다. 모두 Unchecked exception이다.

class FilePrinter {
  fun readFile() {
    val currentFile = File(".")
    val file = File(currentFile.absolutePath + "/a.txt")
    val reader = BufferReader(FileReader(file))
    println(reader.readLine())
    reader.close()
  }

try with resources

// Java
public void readFile(String path) throws IOException {
  try (BufferedReader reader = new BufferedReader(new FileReader(path))) { // 자동으로 외부자원 닫아줌
    System.out.println(reader.readLine())
  }
}

// Kotlin
// try with resources 대신 use라는 inline 확장 함수 사용
fun readFile(path: String) {
  BufferedReader(FileReader(path)).use { reader ->
    println(reader.readLin())
  }
}

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

함수 선언 문법

// Step1 두 정수를 받아 더 큰 정수를 반환하는 예제
fun max(a: Int, b: Int): Int {
  return if (a > b) {
    a
  } else {
    b
  }
}

// Step2 함수가 하나의 결과값이면 block 대신 = 사용 가능
fun max(a: Int, b: Int): Int =
  if (a > b) {
    a
  } else {
    b
  }
  
// Step3 함수를 쓸 때 중괄호 대신 =을 썼기 때문에 타입 추론이 가능해 return 타입 생략 가능
// block {}을 사용하는 경우에는 반환 타입이 Unit이 아니면 명시적으로 작성해주어야 함
fun max(a: Int, b: Int) = if (a > b) a else b

 

함수는 클래스 안에 있을 수도

파일 최상단에 있을 수도 있고

한 파일 안에 여러 함수들이 있을 수도 있다.

 

default parameter

밖에서 파라미터를 넣어주지 않으면 기본값을 사용하도록 해준다. (Java의 오버로드 기능을 대체. Kotlin에서 오버로드 있음)

// 주어진 문자열을 N번 출력
fun repeat(
  str: String,
  num: Int = 3,
  useNewLine: Boolean = true
) {
  for (i in 1..num) {
    if (useNewLine) {
      println(str)
    } else {
      println(str)
    }
  }
}

named argument (parameter)

장점: builder를 직접 만들지 않고 builder의 장점을 가지게 된다. 어떤 param에 어떤 값을 넣어주는지 명확해짐

참고: Kotlin에서 Java 함수를 가져다 사용할 대는 named argument를 사용할 수 없다. (JVM상에서 Java가 바이트 코드로 변환됐을 때 parameter 이름을 보존하고 있지 않기 때문)

fun main() {
  // 셋 모두 같은 결과
  repeat("Hello World")
  repeat("Hello World", 3, false)
  repeat("Hello World", useNewLine = false) // named argument
}

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

// 문자열을 N개 받아 출력
fun printAll(vararg strings: String) { // vararg
  for (str in strings) {
    println(str)
  }
}

fun main() {
  // string 여러개 넣어줄 때
  printAll("A", "B")
  
  // 배열로 넣어줄 때 * (spread 연산자: 배열 안에 있는 것들을 꺼내줌)
  var array = arrayOf("A", "B")
  printAll(*array)
}

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

클래스와 프로퍼티

// public이 default이기 때문에 생략 가능
// constructor에서 타입 선언되어 있기 때문에 필드에서 타입 생략 가능
// 코틀린에서는 필드만 만들면 getter, setter를 자동으로 만들어 줌 (프로퍼티 = getter + setter)
class Person constructor(name: String, age: Int) {
 val name
 var age
}

// constructor 생략 가능
class Person (name: String, age: Int) {
 val name
 var age
}

// 생성자에서 프로퍼티를 만들어 줄 수 있음
// body는 아무것도 없기 때문에 {} 생략 가능
class Person(
  val name: String, 
  var age: Int
  )
  
// 코틀린에선 .필드를 통해 getter와 setter를 바로 호출한다
// java 클래스에 대해서도 .필드로 getter, setter를 사용
fun main() {
  val person = Person("홍길동", 100)
  println(person.name)
  person.age = 10
}

생성자와 init

class Person(
  val name: String,
  val age: Int
) {
  // init 블록: 생성자가 호출되는 시점에 호출
  init {
    if (age <= 0) {
      throw IllegalArgumentException("ㅇㅇㅇ")
    }
    println("초기화 블록")
  }
  
  // 주생성자(primary constructor)는 반드시 존재해야 한다. 단, 파라미터가 하나도 없다면 생략 가능
  // 2번째 이후의 부생성자는 아래와 같이 만들어짐
  // 부생성자는 최종적으로 주생성자를 this로 호출해야 한다.
  constructor(name: String): this(name, 1) {
    println("첫 번째 부생성자")
  }
  
  // 부생성자는 body를 가질 수 있다.
  constructor(): this("홍길동", 1) {
    println("두 번째 부생성자")
  }
}

 

생성자의 호출은 역순으로 된다.

// 두 번째 부생성자로 객체를 생성한 경우 출력
초기화 블록
첫 번째 부생성자
두 번째 부생성자

 

다만, 추가 constructor를 만드는 것보다 default parameter를 kotlin에서는 권장한다.

class Person(
  val name: String = "홍길동",
  var age: Int = 1,
)

 

Converting과 같은 경우 부생성자를 사용할 수 있지만, 그보다는 정적 팩토리 메소드를 추천한다.

 

커스텀 getter, setter

커스텀 getter

class Person(
  val name: String,
  val age: Int
) {
  init {
    if (age <= 0) {
      throw IllegalArgumentException("ㅇㅇㅇ")
    }
    println("초기화 블록")
  }
  
  fun isAdult(): Boolean {
    return this.age >= 20
  }
  
  // custom getter: 위 함수를 아래와 같이 만들 수 있다.
  val isAdult: Boolean
    get() = this.age >= 20
  
  // 이름을 대문자로 바꿔서 반환
  val name: String = name // 주생성자에서 받은 name을 불변 프로퍼티 name에 바로 대입
    // name으로 하면 무한루프 발생 -> backing field 자기 자신을 가리키는 예약어
    get() = field.uppercase()
    
  // backing field 사용해서 custom getter 만들일 거의 없음
  val upperCaseName: String
    get() = this.name.uppercase()
}

 

커스텀 setter

class Person(
  val name: String,
  val age: Int
) {
  init {
    if (age <= 0) {
      throw IllegalArgumentException("ㅇㅇㅇ")
    }
    println("초기화 블록")
  }
  
  var name = name
    set(value) {
      field = value.uppercase()
    }
  
}

 

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

추상 클래스

Java, Kotlin 모두 추상 클래스는 인스턴스화 할 수 없다.

// 추상클래스
abstract class Animal(
  protected val species: String,
  // 프로퍼티를 override할 땐 추상 프로퍼티가 아니라면 open 붙여줘야 함
  protected open val legCount: Int,
) {
  abstract fun move()
}

// 상속받는다는 의미의 ':' (타입표시도 동일하지만 상속은 보통 앞에 띄어쓰기를 해줌)
class Cat(
  species: String
) : Animal(species, 4) { // 상위 클래스의 생성자에 값을 넣어줌
  
  // java는 annotation이었지만 Kotlin은 키워드
  override fun move() {
    println("고양이가 걸어간다")
  }
}

class Penguin(
  species: String
) : Animal(species, 2) {
  
  private val wingCount: Int = 2
  
  override fun move() {
    println("펭귄이 움직인다")
  }
  
  // getter를 Override하기
  override val legCount: Int
    // super: 상위 클래스에 접근
    get() = super.legCount + this.wingCount

}

인터페이스

  • Java. Kotlin 모두 인터페이스를 인스턴스화 할 수 없다.
  • Kotlin에서는 backing field가 없는 프로퍼티를 Interface에 만들 수 있다.
interface Flyable {
  
  // default 키워드 없이 메소드 구현이 가능
  fun act() {
    println("파닥파닥")
  }
  
}

interface Swimable {

  // Kotlin에서는 backing field가 없는 프로퍼티를 Interface에 만들 수 있다.
  val swimAbility: Int
  
  fun act() {
    println("어푸어푸")
  }
  
}

class Penguin(
  species: String
) : Animal(species, 2), Swimable, Flyable { // interface 구현도 ':'를 사용
  
  private val wingCount: Int = 2
  
  override fun move() {
    println("펭귄이 움직인다")
  }
  
  override val legCount: Int
    get() = super.legCount + this.wingCount

  override fun act() {
    // 중복되는 인터페이스를 특정할 때 "super<타입>.함수"를 사용
    super<Swimable>.act()
    super<Flyable>.act()
  }
  
  override val swimAbility: Int
    get() = 3
}

 

 

클래스를 상속할 때 주의할 점

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")
  }
}

 

Derived(300) 하면?

Base Class
0
Derived Class
  • 왜 0일까?
  • Intellij 경고: Acessing non-final property 'number' in constructor
  • Derived를 인스턴스화하면 상위클래스인 Base의 생성자를 실행하는데, 이때 하위클래스인 Derived의 number를 호출하지만 초기화가 되어있지 않음
  • 즉, 상위 클래스를 설계할 때 생성자 또는 초기화 블록에 사용되는 프로퍼티에는 open을 피해야 한다

상속 관련 지시어 정리

  • final: override를 할 수 없게 한다. default로 보이지 않게 존재한다.
  • open: override를 열어 준다.
  • abstract: 반드시 override 해야 한다.
  • override: 상위 타입을 오버라이드 하고 있다.

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

자바와 코틀린의 가시성 제어

  • Java의 기본 접근 지시어는 default
    Kotlin의 기본 접근 지시어는 public
  • Kotlin에서는 패키지라는 개념을 접근 제어에 사용하지 않기 때문에 모듈을 접근 제어하는 기능이 생겼다.
  Java   Kotlin
public 모든 곳에서 접근 가능 public 모든 곳에서 접근 가능
protected 같은 패키지 또는 하위 클래스에서만 접근 가능 protected 선언된 클래스 또는 하위 클래스에서만 접근 가능
-> Kotlin에서는 Java와 다르게 패키지를 namespace를 관리하기 위한 용도로만 사용. 가시성 제어에는 사용되지 않기 때문.
default 같은 패키지에서만 접근 가능 internal 같은 모듈에서만 접근 가능
-> 모듈: 한 번에 컴파일 되는 kotlin 코드
private 선언된 클래스 내에서만 접근 가능 private 선언된 클래스 내에서만 접근 가능

코틀린 파일의 접근 제어

코틀린은 .kt 파일에 변수, 함수, 클래스 여러개를 바로 만들 수 있다.

파일에서의 접근 제어
public default값. 어디서든 접근할 수 있다.
protected 파일(최상단)에는 사용 불가능
-> protected가 선언된 클래스 또는 하위 클래스에서 사용되는 지시어이기 때문
internal 같은 모듈에서만 접근 가능
private 같은 파일에서만 접근 가능

다양한 구성요소의 접근 제어

클래스에서의 접근 제어
public default값. 어디서든 접근할 수 있다.
protected 선언된 클래스 또는 하위 클래스에서만 접근 가능
internal 같은 모듈에서만 접근 가능
private 선언된 클래스 내에서만 접근 가능
생성자에서의 접근 제어
public default값. 어디서든 접근할 수 있다.
protected 선언된 클래스 또는 하위 클래스에서만 접근 가능
internal 같은 모듈에서만 접근 가능
private 같은 파일에서만 접근 가능
  • 생성자에 접근 지시어를 붙이려면 constructor를 써줘야 함
    ex) class Cat internal constructor (...)
프로퍼티에서의 접근 제어
public default값. 어디서든 접근할 수 있다.
protected 파일(최상단)에는 사용 불가능
-> protected가 선언된 클래스 또는 하위 클래스에서 사용되는 지시어이기 때문
internal 같은 모듈에서만 접근 가능
private 같은 파일에서만 접근 가능

Java와 Kotlin을 함께 사용할 경우 주의할 점

  • Internal은 바이트 코드 상 public이 된다. 때문에 Java 코드에서는 Kotlin 모듈의 internal 코드를 가져올 수 있다.
  • Kotlin의 protected와 java의 protected는 다르다. java는 같은 패키지의 Kotlin protected 멤버에 접근할 수 있다.

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

static 함수와 변수

companion object

  • static과 유사한 역할. 클래스와 동행하는 유일한 오브젝트
  • companion object도 하나의 객체로 간주. 따라서 이름을 붙일 수도 있고, interface를 구현할 수도 있다. (이름을 안쓴다면 'Companion' 이라는 이름이 생략된 것)
  • companion object에 유틸성 함수를 넣어도 되지만, 최상단 파일을 활용하는 것을 추천.
class Person private constructor(
  var name: String,
  var age: Int,
) {

  // kotlin에는 static이 없음 -> companion object
  companion object Factory {
  
    // const가 없으면 런타임 시에 0이 할당되지만
    // const를 붙이면 컴파일 시에 0이 할당됨
    // 즉, 진짜 상수를 표현하고 기본타입과 string에만 붙여줄 수 있다.
    private const val MIN_AGE = 0
    fun newBaby(name: String): Person {
      return Person(name, MIN_AGE)
    }
    
    // Java에서 호출할 때 
    // Person.Companion.secondBaby("abc");
    // Person.secondBaby("abc"); // JvmStatic 붙이면 다음과 같이 Java에서 호출할 수 있다.
    @JvmStatic
    fun secondBaby(name: String): Person {
      return Person(name, MIN_AGE + 1)
    }
  }

}

싱글톤

Kotlin에서는 싱글톤 클래스를 만들기 위해 object만 붙여주면 된다.

fun main() {

  println(Singleton.a)
  Singleton.a += 10
  
}

object Singleton {
  var a: Int = 0
}

익명 클래스

특정 인터페이스나 클래스를 상속받은 구현체를 일회성으로 사용할 때 쓰는 클래스

// Movable이라는 interface가 있다고 할 때
fun main() {
  moveSomething(object : Movable {
    override fun move() {
      println("move")
    }
    
    override fun fly() {
      println("fly")
    }
  })
}

private fun moveSomething(movable: Movable) {
  movable.move()
  movable.fly()
}

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

중첩 클래스의 종류

Java에서?

  1. Static을 사용하는 중첩 클래스: 클래스 안에 static을 붙인 클래스
  2. Static을 사용하지 않는 중첩 클래스
    1. 내부 클래스: 클래스 안의 클래스
    2. 지역 클래스: 메소드 안의 클래스
    3. 익명 클래스: 일회성 클래스

코틀린의 중첩 클래스와 내부 클래스

Static을 사용하는 중첩 클래스: 바깥 클래스와 연결안됨. OuterClass.this.field1 이렇게 불러올 수 없음

Static을 사용하지 않는 중첩 클래스 - 내부 클래스: 바깥 클래스와 연결. OuterClass.this.field1 이렇게 불러올 수 있음

-> Static을 사용하는 내부 클래스를 사용하라

 

왜?

1. 내부 클래스는 숨겨진 외부 클래스 정보를 가지고 있어, 참조를 해지하지 못하는 경우 메모리 누수가 생길 수 있고, 이를 디버깅 하기 어려움

2. 내부 클래스의 직렬화 형태가 명확하게 정의되지 않아 직렬화에 있어 제한이 있다.

 

그래서 Kotlin에서는 이 규칙을 따르고 있다.

// 그냥 내부 클래스 쓰면 연결되지 않은 중첩 클래스가 만들어짐
class JavaHouse(
  prviate val address: String,
  private val livingRoom: LivingRoom
) {
  class LivingRoom(
    private var area: Double,
  )
}

// inner를 붙이면 연결된 중첩 클래스가 만들어짐 (권장되지 않는 클래스 안의 클래스)
class House(
  prviate val address: String,
  private val livingRoom: LivingRoom
) {
  inner class LivingRoom(
    private var area: Double,
  ) {
    val address: String
      get() = this@House.address // 외부 클래스 접근
  }
}

 

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

Data Class

계층간의 데이터를 전달하기 위한 DTO(Data Transfer Object)

// data를 붙이면 자동으로 equals, hashCode, toString 을 만들어줌
data class PersonDto(
  val name: String,
  val age: Int,
)

Enum Class

추가적인 클래스를 상속받을 수 없다. 인터페이스는 구현할 수 있으며, 각 코드가 싱글톤이다.

enum class Country(
  private val code: String,
) {
  KOREA("KO"),
  AMERICA("US")
  ;
}

Enum + When 을 함께 썼을 경우 

  • 읽기 쉬운 코드
  • 컴파일러가 country의 모든 타입을 알고 있어 다른 타입에 대한 로직(else)을 작성하지 않아도 된다.
  • Enum에 변화가 있으면 IDE단에서 warning을 준다.
fun handleCountry(country: Country) {
  when (country) {
    Country.KOREA -> TODO()
    Country.AMERICA -> TODO()
  }
}

Sealed Class, Sealed Interface

상속이 가능하도록 추상클래스를 만들고 싶은데, 외부에서는 이 클래스를 상속받지 않았으면 좋겠을 경우

-> 하위 클래스를 봉인(seal)하자!

 

  1. 컴파일 타임 때 하위 클래스의 타입을 모두 기억한다. 즉, 런타임 때 클래스 타입이 추가될 수 없다.
  2. 하위 클래스는 sealed class와 같은 패키지에 있어야 한다.
  3. Enum과 다른점: 클래스를 상속받을 수 있고 하위 클래스는 멀티 인스턴스가 가능하다. (<-> 싱글톤) 
sealed class Car(
  val name: String,
  val price: Long
)

class Avante : Car("아반떼", 1_000L)
class Sonata : Car("쏘나타", 1_000L)

// enum과 유사해서 when과 함께 주로 사용된다.
private fun handleCar(car: Car) {
  when (car) {
    is Avante -> TODO()
    is Sonata -> TODO()
  }
}

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

배열

fun main() {
  val array = arrayOf(100, 200)
  
  for (i in array.indices) {
    println("${i} ${array[i]}")
  }
  
  for ((idx, value) in array.withIndex()) {
    println("$idx $value")
  }
}

코틀린에서의 Collection - List, Set, Map

  • 가변(Mutable) 컬렉션: 컬렉션에 element를 추가, 삭제할 수 있다.
    • MutableList, MutableSet, MutableMap
  • 불변 컬렉션: 컬렉션에 element를 추가, 삭제할 수 없다.
    • List, Set, Map
    • 불변 컬렉션이라 하더라도 reference type인 element의 필드는 바꿀 수 있다.
// list
fun main() {
  val numbers = listOf(100, 200)
  val emptyList = emptyList<Int>() // 타입추론이 가능하면 <>를 생략 가능
 
  println(numbers[0])
  
  for (number in numbers) {
    println(number)
  }
  
  for ((idx, number) in numbers.withIndex()) {
    println("$index $number")
  }
  
  // 가변List
  // 기본 구현체는 arrayList
  val mutableNumbers = mutableListOf(100, 200)
  mutableNumbers.add(300)
}
// Set
fun main() {
  val numbers = setOf(100, 200)
  
  for (number in numbers) {
    println(number)
  }
  
  for ((index, number) in numbers.withIndex()) {
    println("$index $number")
  }
}
// Map
fun main() {
  val map = mutableMapOf<Int, String>()
  map[1] = "MONDAY"
  map.put(2, "TUESDAY")
  
  mapOf(1 to "MONDAY", 2 to "TUESDAY")
  
  for (key in map.keys) {
    println(key)
    println(map[key])
  }
  
  for ((key, value) in map.entries) {
    println(key)
    println(value)
  }
}

컬렉션의 null 가능성, Java와 함께 사용하기

  • List<Int?>: 리스트에 null이 들어갈 수 있지만, 리스트는 절대 null이 아님
  • List<Int>?: 리스트에는 null이 들어갈 수 없지만, 리스트는 null일 수 있음
  • List<Int?>?: 리스트에 null이 들어갈 수도 있고, 리스트가 null일 수도 있음
  • Kotlin쪽의 컬렉션이 Java에서 호출되면 컬렉션 내용이 변할 수 있음을 감안해야 한다.
  • 혹은, 코틀린 쪽에서 Java에도 있는 기능인 Collections.unmodifiableXXX()를 활용하면 변경 자체를 막을 수는 있다.

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

확장함수

Java로 만들어진 라이브러리를 유지보수, 확장할 때 Kotlin 코드를 덧붙이고 싶은 니즈

-> 어떤 클래스안에 있는 메소드처럼 호출할 수 있지만, 함수는 밖에 만들 수 있게 하자

// String 클래스를 확장
fun String.lastChar(): Char {
  return this[this.length - 1]
}

// 사용할 때는 원래 String에 있는 멤버함수 처럼 사용할 수 있다.
val str: String = "ABC"
str.lastChar()
  • 확장함수는 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다.
  • 멤버함수와 확장함수의 시그니처가 같다면 멤버 함수를 우선적으로 호출한다.
  • 확장함수는 현재 타입을 기준으로 호출된다.

infix 함수

변수.함수이름 대신 '변수 함수이름 argument' 이렇게 호출해 주는 것

// 중위함수
infix fun Int.add2(other: Int): Int {
  return this + other
}

// 중위함수 호출 시 
3 add2 4

inline 함수

함수가 호출되는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복사하는 것

inline fun Int.add(other: Int): Int {
  return this + other
}
  • 함수를 파라미터로 전달할 때에 오버헤드를 줄일 수 있다.
  • 하지만 inline 함수의 사용은 성능 측정과 함께 신중하게 사용되어야 한다.

지역함수

함수 안에 함수를 선언하는 것

fun createPerson(..): Person {
  fun validateName(name: String, fieldName: String) {
    if (...)
  }
  
  validateName("name", "fieldname")
  
  return Person()
}
  • 함수로 추출하면 좋을 것 같은데, 이 함수를 지금 함수 내에서만 사용하고 싶을 때
  • depth가 깊어지기도 하고, 코드가 그렇게 깔끔하지는 않다.

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

코틀린에서의 람다

코틀린에서는 Java와 다르게 함수가 그 자체로 값이 될 수 있다. 변수에 할당할수도, 파라미터로 넘길 수도 있다

// 함수 이름이 없음 - 람다 (타입은 생략 가능)
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
  return fruit.name == "사과"
}

val isApple2 = { fruit: Fruit -> fruit.name == "사과" }

// 호출하는 방법
isApple(fruits[0])
isApple.invoke(fruit[0])

// parameter로 넘겨주는 경우
filterFruits(fruits, { fruit: Fruit -> fruit.name == "사과" })
// 가장 마지막 파라미터로 람다가 들어가는 경우 밖으로 빼줄 수도 있음
filterFruits(fruits) { fruit: Fruit -> fruit.name == "사과" }
// 타입 생략 가능
filterFruits(fruits) { fruit -> fruit.name == "사과" }
// parameter가 1개인 경우 it으로 사용 가능
filterFruits(fruits) { it.name == "사과" }

Closure

자바에서

String targetFruitName = "바나나";
targetFruitName = "수박";
filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName()));

// Variable used in lambda expression should be final or effectively final 오류

 

Kotlin에서

var targetFruitName = "바나나"
targetFruitName = "수박"
filterFruits(fruits) { it.name == targetFruitname }
  • 코틀린은 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.
  • 이러한 데이터 구조를 Closure라고 부른다.

다시 try with resources

fun readFile(path: String) {
  BufferedReader(FileReader(path)).use { reader ->
    println(reader.readLine())
  }
}

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

필터와 맵

val apples = fruits.filter { fruit -> fruit.name == "사과" }
val apples = fruits.filterIndexed { idx, fruit ->
  println(idx)
  fruit.name == "사과"
}
val applePrices = fruits.filter { fruit -> fruit.name == "사과" }
  .map { fruit -> fruit.price }
val applePrices = fruits.filter { fruit -> fruit.name == "사과" }
  .mapIndexed { idx, fruit ->
    println(idx)
    fruit.currentPrice
  }
val values = fruits.filter { fruit -> fruit.name == "사과" }
  .mapNotNull { fruit -> fruit.nullOrValue() }

다양한 컬렉션 처리 기능

// 조건 모두 만족시 true
val isAllApple = fruits.all { fruit -> fruit.name == "사과" }
// 조건을 모두 불만족시 true
val isAllApple = fruits.none { fruit -> fruit.name == "사과" }
// 조건을 하나라도 만족하면 true
val isAllApple = fruits.any { fruit -> fruit.name == "사과" }

fruits.count()
fruits.sortedBy { fruit -> fruit.currentPrice }
fruits.sortedByDescending { fruit -> fruit.currentPrice }
fruits.distinctBy { fruit -> fruit.name }.map { fruit -> fruit.name }

fruits.first() // 첫번째 값을 가져오는데 null이면 안됨 (exception)
fruits.firstOrNull() // 첫번째 값을 가져오는데 없으면 null
fruits.last()
fruits.lastOrNull()

List를 Map으로

// 과일이름 -> List<과일>
val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
// id -> 과일
val map: Map<Long, Fruit> = fruits.associatedBy { fruit -> fruit.id }
// 과일이름 -> List<출고가>
val map: Map<String, List<Long>> = fruits
  .groupBy({ fruit -> fruit.name }, { fruit -> fruit.factoryPrice })
// id -> 출고가
val map: Map<Long, Long> = fruits
  .associateBy({ fruit -> fruit.id }, { fruit -> fruit.factoryPrice })

중첩된 컬렉션 처리

fruitsInList.flatMap { list -> 
  list.filter { fruit -> fruit.factoryPrice == fruit.currentPrice }
}
// List<List<Fruit>> 을 List<Fruit>로 변경
fruitsInList.flatten()

19. 코틀린의 이모저모

Type Alias와 as import

typealias FruitFilter = (Fruit) -> Boolean
fun filterFruits(fruits: List<Fruit>, filter: FruitFilter) {}

data class UltraSuperGuardianTribe(val name: String)
typealias USGTMap = Map<String, UltraSuperGuardianTribe>
// 다른 패키지의 같은 이름 함수를 동시에 가져오고 싶은 경우
import com.a.printlnHelloWorld as aPrintlnHelloWorld
import com.b.printlnHelloWorld as bPrintlnHelloWorld

fun main() {
  aPrintHelloWorld()
  bPrintHelloWorld()
}

구조분해와 componentN 함수

// 구조분해: 복합적인 값을 분해하여 여러 변수를 한 번에 초기화하는 것
// data class는 componentN을 자동으로 만들어주기 때문에 별도 설정없이 이렇게 사용이 가능
val person = Person("가나다", 100)
val (name, age) = person

// 위와 같은 코드
val person = Person("가나다", 100)
val name = person.component1()
val age = person.component2()

// data class가 아닌데 구조분해를 사용하고 싶은 경우: componentN을 구현
class Person(
  val name: String,
  val age: Int
) {
  operator fun component1(): String {
    return this.name
  }
  
  operator fun component2(): Int {
    return this.age
  }
}

Jump와 Label

  • return: 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환된다
  • break: 가장 가까운 루프가 제거된다
  • continue: 가장 가까운 루프를 다음 step으로 보낸다
// forEach문에서는 continue, break를 쓸 수 없다.
// break를 꼭 쓰고 싶다면 다음과 같이 작성해야 한다.
run {
  numbers.forEach { number ->
    if (number == 2) {
      return@run
    }
  }
}

// continue를 꼭 쓰고 싶다면 다음과 같이 작성해야 한다.
number.forEach { number ->
  if (number == 2) {
    return@forEach
  }
}
  • Label: 특정 expression에서 라벨이름@을 붙여 하나의 라벨로 간주하고 break, continue, return 등을 사용하는 기능
// break문이 가장 가까운 for문이 아닌 loop가 붙은 가장 상위의 for문을 멈춤
loop@ for (i in 1..100) {
  for (j in 1..100) {
    if (j == 2) {
      break@loop
    }
  }
}

TakeIf와 TakeUnless

fun getNumberOrNull(): Int? {
  return if (number <= 0) {
    null
  } else {
    number
  }
}

// takeIf: 주어진 조건을 만족하면 그 값이, 그렇지 않으면 null이 반환된다.
fun getNumberOrNull(): Int? {
  return number.takeIf { it > 0 }
}

// takeUnless: 주어진 조건을 만족하지 않으면 그 값이 그렇지 않으면 null이 반환된다.
fun getNumberOrNull(): Int? {
  return number.takeUnless { it <= 0 }
}

20. 코틀린의 scope function

scope function이란 무엇인가

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

// let: 확장함수. 람다를 받아, 람다 결과를 반환한다.
fun printPerson(person: Person?) {
  person?.let {
    println(it.name)
    println(it.age)
  }
}

scope function의 분류

let, run, also, apply, with

// 결과: age, it 사용
person.let { it.age }
// 결과: age, this 사용
person.run { this.age }
// 결과: person, it 사용
person.also { it.age }
// 결과: person, this 사용
person.apply { this.age }

// this: 생략이 가능한 대신, 다른 이름을 붙일 수 없다.
person.run { age }
// it: 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.
person.let { p -> p.age }

// with: this 사용
with(person) {
  println(name)
  println(this.age)
}

 

언제 어떤 scope function을 사용해야 할까

// 1. let: 하나 이상의 함수를 call chain 결과로 호출 할 때
val strings = listOf("a", "b")
strings.map { it.length }
  .filter { it > 3 }
  .let(::println)
  
// 2. let: non-null 값에 대해서만 code block을 실행시킬 때
val length = str?.let {
  println(it.uppercase())
  it.length
}

// 3. let: 일회성으로 제한된 영역에 지역 변수를 만들 때
val numbers = listOf("a", "b")
val modifiedFirstItem = numbers.first()
  .let { firstItem -> if (firstItem.length >= 5) firstItem else '.' }.uppercase()
// run: 객체 초기화와 반환 값의 계산을 동시에 해야 할 때
val person = Person("a", 100).run(personRepository::save)
// apply: Test Fixture를 만들 때
fun createPerson(name: String, age: Int, hobby: String): Person {
  return Person(
    name = name,
    age = age,
  ).apply {
    this.hobby = hobby
  }
}

// apply: call chain을 수정할 때
person.apply { this.growOld() }.let { println(it) }
// also: 객체를 수정하는 로직이 call chain 중간에 필요할 때
mutableListOf("a", "b")
  .also { println(it) }
  .add("four")
// with: 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때
return with(person) {
  PersonDto(
    name = name,
    age = age,
  )
}

 

728x90
반응형