선생님, 개발을 잘하고 싶어요.

[kotlin] 안드로이드 개발에 필요한 최소의 코틀린 강좌 (part2) 본문

강좌/kotlin 강좌

[kotlin] 안드로이드 개발에 필요한 최소의 코틀린 강좌 (part2)

알고싶은 승민 2019. 9. 7. 00:35

도입

 part2에서는 흐름 제어, 함수 정의, 코틀린만의 클래스에 대해서 알아볼 건데요. java, c#, c++과 차이가 나는 부분만 중점적으로 다룰 예정입니다. 따로 다루지 않은 사용법은 링크된 참고 링크를 참조해 주세요.

 

코드 작성은 코틀린 플레이그라운드상에서 진행하시거나, 실제 안드로이드 프로젝트를 생성 후 작성해주시면 됩니다.

 

그리고 해당 포스트는 코틀린 공식 홈페이지와, 커니의 코틀린을 참조해서 만들었다는 점을 말씀드립니다. (+ 필자의 경험 :) )

 

 

Kotlin Playground: Edit, Run, Share Kotlin Code Online

 

play.kotlinlang.org

다루는 것

  • 코틀린 문법
    • 선언 (변수, 값 선언) [part1]
    • 자료형 (Nullable) [part1]
    • 리스트 (Mutable) [part1]
    • 흐름 제어 (when, for) [part2]
    • 함수 정의 [part2]
    • 코틀린만의 클래스 정의 (data class, object) [part2]
    • 상속 [part3]
    • 람다식 [part4]
  • 알아두면 인생이 편해지는 코틀린 유틸리티
    • String Templates [part 5]
    • 리스트 유틸리티 (스트림 함수) [part 5]
    • 범위 지정 함수 (let, apply, with, run, also) [part6]
  • 코틀린은 왜? (사견이 많이 들어간 칼럼입니다.)
    • 왜 코틀린은 불변성에 집중했을까?
    • 왜 코틀린은 nullable에 집중했을까?

흐름 제어 (for, when)

when (switch문과 if-else문의 대체)

공식 참조 링크 : https://kotlinlang.org/docs/reference/control-flow.html#when-expression

 

 모든 프로그래밍 언어에서 흐름 제어는 중요합니다. 분기문, 반복문은 개발을 하다 보면 빈번하게 사용하게 됩니다. 가독성을 중요하게 생각하는 코틀린은 이런 흐름 제어를 좀 더 직관적으로 변경하였는데요. 자바 코드와 kotlin 코드를 비교해 봅시다.

int key = getKey();

switch (key) {
    case 1:
    case 2:
    case 4:
    	System.out.println("similar key");
        break;
    case 3:
    	System.out.println("correct key");
        break;
    default:
        System.out.println("wrong key");
}
val key = getKey()

when (key) {
    // 여러 개의 case값은 쉼표로 구분
    1,2,4 -> {
        println("similar key")
    }
    3 -> {
        println("correct key")
    }
    // default 대신 else를 사용
    else -> {
        println("wrong key")
    }
}

보시다시피 코틀린의 when은 case 여러 개를 작성할 때 더 가독성이 좋은 코드를 만들어냅니다.

또한 코틀린은 각 조건을 표현식으로 작성할 수 있습니다. 해당 표현식이 true일 때 발생하는 것이죠. (여러 개의 if-else문을 작성하는 작업처럼 생각하시면 됩니다.)

val key = 1
val similarKeys = listOf(1,2,4)

// 이 if-else문 과
if (similarKeys.contains(key)) {
    println("similar key")
}
else if (isCorrectKey(key)) {
    println("correct key")
}
else {
    println("wrong key")
}

// 이 when문은 완벽히 똑같은 행동을 한다.
// when뒤에 괄호가 없어도 사용할 수 있다.
when  {
    // 조건식을 사용해서 여러가지로 분기할 수 있다.
    similarKeys.contains(key) -> println("similar key")
    isCorrectKey(key) -> println("correct key")
    else -> println("wrong key")
}

 

for (for-each가 기본으로~)

공식 참조 링크 : https://kotlinlang.org/docs/reference/control-flow.html#for-loops

 

for-each는 모든 Collections(리스트, 집합 등 아이템들의 모음)의 모든 아이템에 순차적으로 접근하는 방법으로 java, c#에서 제공하던 기능입니다. (c++ 11도 iterator를 사용해 제공했죠.) 이러한 언어들이 for-each를 부차적으로 지원하지만, 코틀린의 for문은 전부다 for-each문입니다. 

 

텍스트로 적는 것보다 한 줄 코드를 보는 게 낫겠죠. (코틀린 플레이그라운드에서 직접 이것저것 해보는 게 최고이긴 합니다.)

 

val list: List<Int> = listOf(1,2,3)

// 배열에 들어있는 값을 순차적으로 반환한다.
for (item in list) {
    print (item.toString() + ",")
} // 1,2,3, 출력

// 위의 식은 다음의 자바코드와 같습니다.
for (int i=0; i<list.size(); i++) {
    System.out.print(list.get(i).toString() + ",")
}

item의 자료형은 컴퓨터가 알아서 파악합니다. 따라서 우리는 쉽게 해당 item만 사용하면 되죠. in 뒤에 Set, Map 등 다양한 어떤 자료형이 와도 순차적으로 아이템에 접근할 수 있습니다. 편리하죠?

 

그런데 우리는 꼭 Collection의 아이템에 접근하기 위해서만 for문을 사용하지 않습니다. 몇 번 반복할 것인가?를 표현하기 위해서도 for문을 사용하죠. 이를 위해 kotlin은 Range라는 신기한 녀석을 제공합니다. 코드를 보면서 이해해 봅시다.

for (i in 0..3) {
    print (i.toString() + ",")
} // 0,1,2,3, 출력

for (i in 0..6 step 2) {
    print (i.toString() + ",")
} // 0,2,4,6, 출력

for (i in 3 downTo 0) {
    print (i.toString() + ",")
} // 3,2,1,0, 출력

for (i in 6 downTo 0 step 2) {
    print (i.toString() + ",")
} // 6,4,2,0, 출력   

0.. 3, 3 downTo 0 모두 Range객체를 반환합니다. 그 의미는 위의 코드가 동작하는 바와 같죠.

 

0..3 은 순차적으로 0부터 시작해서 3까지 증가하는 범위를 의미합니다.

step함수를 사용해서 0부터 시작해서 6까지 2씩 증가하는 범위를 만들 수 도 있습니다.

 

반대로 감소하는 범위를 만들고 싶은 경우도 downTo를 사용해서 쉽게 지원합니다.

3 downTo 0 은 순차적으로 3부터 시작해서 0까지 감소하는 범위를 만들고,

마찬가지로 step함수를 사용해 감소하는 크기를 설정할 수 있죠.

 

Range에 대한 자세한 정보는 공식 문서 : https://kotlinlang.org/docs/reference/ranges.html를 참고해주세요.

 

함수

공식 참조 링크 : https://kotlinlang.org/docs/reference/functions.html

 

코틀린에선 함수를 선언할 때 fun 키워드를 사용합니다.

기본적으로 함수 정의의 형태입니다. 코드를 확인하며 같이 해보죠. 

// Unit은 "아무것도 반환하지 않는다"는 뜻입니다. 다른 언어에서 void와 같은 녀석이죠.
fun foo(): Unit {

}

// Unit 반환값은 생략 가능합니다. 아무것도 하지 않는 반환은 쓸 필요도 없습니다. 
fun foo2() {

}

// 함수 이름은 add
// Int형 a와 b라는 매개변수를 받고
// Int형 반환값을 가지는 함수라는 의미입니다.
fun add(a: Int, b: Int): Int {
    return a+b
}

// 사용은 다른 프로그래밍 언어와 다르지 않습니다.
val sum: Int = add(1,2) // sum == 3

 

함수 정의하는 법 자체는 쉽습니다. 이제 한번 해당 함수를 어떻게 사용할 수 있을지 생각해보세요.

이름이 뭔지, 매개변수는 뭔지, 반환 값이 뭔지를 알면 됩니다.

// 함수들의 사용법을 읽을 수 있나요?

fun subString(str: String, start: Int, end: Int): String {
    //
}

fun isChecked(view: View): Boolean {
    //
}

fun updateUI(data: Data) {
    //
}

fun onCreate(intent: Intent?) {
    //
}

 

코틀린만의 클래스 정의 (data class, object) 

코틀린은 여러 가지 귀찮은 구현체들을 문법적으로 프로그래머가 손쉽게 작성하는 것을 도와주는 언어인데요. 다음에 소개해드리는 클래스들은 "정말로"당신의 수명을 늘려줄 거라고 확신할 수 있습니다.

data class

공식 참조 링크 : https://kotlinlang.org/docs/reference/data-classes.html

 

작업을 하시다 보면 멤버 변수만 의미 있는 클래스를 만들어야 하는 경우가 있습니다.

java에서 책의 식별자, 작가, 출판사 이름, 현재 절판되었는지 여부를 다루는 클래스를 만든다고 생각해보세요. (단 4가지의 속성입니다.)

 

그리고 아래는 그 구현체입니다. (심호흡하고 그냥 넘기세요.)

class BookData {
    // 책의 식별 아이디 (변경 불가능 하다, setter가 없다)
    private int id;

    // 책의 작가 (변경 불가능 하다, setter가 없다)
    private String writer;

    // 책의 출판사
    private String publisher;

    // 책이 현재 절판되었는지 여부
    private Boolean isOutOfPrint;

    public int getId() {
        return id;
    }

    public String getWriter() {
        return writer;
    }

    public String getPublisher() {
        return publisher;
    }

    public void setPublisher(String publisher) {
        this.publisher = publisher;
    }

    public Boolean getOutOfPrint() {
        return isOutOfPrint;
    }

    public void setOutOfPrint(Boolean outOfPrint) {
        isOutOfPrint = outOfPrint;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BookData bookData = (BookData) o;
        return id == bookData.id &&
                Objects.equals(writer, bookData.writer) &&
                Objects.equals(publisher, bookData.publisher) &&
                Objects.equals(isOutOfPrint, bookData.isOutOfPrint);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, writer, publisher, isOutOfPrint);
    }

    @Override
    public String toString() {
        return "BookData{" +
                "id=" + id +
                ", writer='" + writer + '\'' +
                ", publisher='" + publisher + '\'' +
                ", isOutOfPrint=" + isOutOfPrint +
                '}';
    }
}

멤버 변수마다 getter, setter의 정의를 해주어야 하고,
정상적으로 값을 비교하기 위해 equals를 override,
데이터 객체를 logger와 같은 곳에 남기기 위해서 toString을 override,
데이터 객체를 Collection의 key값으로 넘기기 위해 hashCode를 override 해야 하죠.

참고로 생성자 경우의 수를 생각하면 끔찍합니다.


이 코드는 요즘 IDE는 자동 생성을 지원합니다만. 

만약 멤버 변수가 하나 늘어난다면...?

그때마다 equals, toString, hashCode를 변경해주어야 하고, getter, setter를 추가해줘야 합니다. (welcome to hell)

  코틀린은 이런 자바 프로그래머를 구원하고자 data class를 도입하게 됩니다. 두말할 것 없죠. 위의 요구사항에 맞는 클래스를 코틀린으로 구현해보죠. (겁먹지 마세요.)

data class BookData(
    val id: Int, // setter가 없는 요소는 val로 선언하여 변경 불가능 하게 하였습니다. 
    val writer: String,
    var publisher: String, // setter와 getter 둘다 있는 경우는 var로 선언하여 변경 가능하도록 하였습니다.
    var isOutOfPrint: Boolean
)

위의 코드와 아래 코드는 정확히 같은 동작을 하게 됩니다. 놀랍죠? 보기도 편하고, 코드 작성도 편하고, 심지어 멤버 변수를 추가하는 것은 누워서 떡먹기입니다.

 

정리하면 data class는 자동으로

  • toString()을 내부적으로 생성합니다. 그래서 toString으로 데이터와 연관된 문자열을 볼 수 있습니다.
  • equals()를 내부적으로 생성합니다. 그래서 데이터 값을 기준으로 객체의 동등성을 판단할 수 있습니다.
  • hashCode()를 내부적으로 생성합니다. 그래서 데이터 값을 기준으로 적당한 hashCode를 만들어냅니다.

object

공식 참조 링크 : https://kotlinlang.org/docs/reference/object-declarations.html#object-declarations

 

그다음은 싱글톤 클래스입니다. 싱글톤 클래스... 프로그램에서 단 하나만 있어야 하는 클래스를 의미합니다. 자바에서 싱글톤 클래스를 구현하는 방법은 여러 가지가 있습니다.

 

일 예로 생성자를 private로 설정하고, 클래스 내부에 전역 instance객체를 관리하는 방법이 있겠죠. 사실 굉장히 다양한 방법들이 있고, 몇 구현은 멀티 스레드 환경에 취약 한구현을 가지고 있습니다. 

 

한 번 간단한 예를 보고 가죠.

class Singleton {
    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    private Singleton() {
        // private로 선언해서 외부에서 해당 클래스의 객체를 만들 수 없다.
    }
}

// 외부에서 사용하면 다음과 같죠.

Singleton.getInstance().someFunction();

 

이 정도면 괜찮습니다. 하지만 해당 싱글톤 구현은 멀티 스레드 환경에선 문제가 발생합니다. instance를 생성하는 코드에서 문제가 발생할 수 있거든요. 

 

복잡합니다. 여기서 더 좋게 구현을 바꾸는 것은 가능하지만 귀찮죠. 그리고 쓸모없는 코드가 늘어납니다. 코틀린이 이러한 싱글톤 클래스도 편하게 만들 수 있는 문법을 제공합니다.

// 단지 object 키워드만 사용하면 됩니다! (심지어 쓰레드 안전합니다, 위에 코드보다 더 견고한 개발을 하신겁니다.)
object Singleton {
	fun someFunction()
}

// 외부에선 이렇게 사용하죠.
Singleton.someFunction()

사실 코틀린에선 싱글톤에 대한 개념만 알아도, 해당 개념을 구현하는데 신경 쓰지 않고 바로 적용해서 프로그래밍이 가능합니다. (좋쥬?)

 

주저리

이번 포스트를 살펴보시면서 코틀린이 프로그래머의 생산성을 얼마나 늘려줄지 감이 오셨으면 좋겠습니다.

  앞의 두 가지 흐름 제어, 함수 선언에선 코틀린이 기존의 언어 문법을 얼마나 가독성 좋게 변경했는지, 얼마나 유연하게 변경했는지 느끼셨으면 좋겠습니다.

  마무리로 다룬 data class와 object는 코틀린이라는 언어가 "프로그래머가 실제로 귀찮게 작업해야 하는 코드들"을 언어 차원에서 해결하려고 노력한 언어라는 사실을 말해줍니다. 그리고 저는 이러한 방식을 kotlin-like 하다고 생각하고 있습니다.

 

다음 포스트 때는 갑자기 확 어려워져서 lambda에 대해서 다루도록 하겠습니다. 함수에 대한 깊은 이해를 하고 있다고 가정하고 들어가야 하기 때문에 설명하기도 어렵고 이해하기도 어려운 개념입니다만 (사실 어떻게 설명해야 할지 감이 잘 안 오네요.) 단언컨대 코틀린에서 가장 편하고 유용하고 자주 사용하고 있는 기능이라고 말씀드리겠습니다.

 

다음 포스트 때 만나요~ 

 


[part1] 선언, 자료형, 리스트

[part2] 흐름 제어, 함수 정의, 코틀린만의 클래스 (we're here)

[part3] 클래스 심화, 확장

[part4] 확장 함수

Comments