2023년 12월 15일 금요일

Kotlin_min_ebook_kth

Kotlin_min_ebook_kth

Kotlin 언어 개발 기초

alt_text

ⓒ 2021. Sugoi Group All rights reserved.
ⓒ 2021. provided by DX-NINJA, DX-JAPAN.

앱 개발 및 기타문의 : : cg99132@gmail.com
일본에서 프로그래머 취업하기 : (카카오톡 오픈 토크)

E-BOOK PDF 버젼 보기

1 코틀린 이란

alt_text

Kotlin은 Android Studio IDE 를 만든 개발사 JetBrains라는 회사에서 2011공개한 프로그래밍언어이다. 공개된지는 꽤 되었지만, 일반 프로그래머들에게는 잘 알려지지 않았다가, 2017년 구글이 안드로이드의 공식개발언어로서 코틀린을 추가함으로서 안드로이드 개발자들과 일반프로그래머들에게 알려지게 되었다.

지금까지의 안드로이드앱은 주로 JAVA 언어를 이용하여 개발하였는데, 구글이 오라클이라는 회사하고의 소송에서 java 사용권에 대해 패소하여 Android 개발언어로서 Java 사용의 한계를 느낀 구글이 Kotlin언어를 적극 채용하게 되었다는 소문이 있다.

구글이 Android 에 Kotlin을 채택한 주요 이유는 JAVA와의 100%호환, JetBrains라는 안정적인 개발툴확보, 모던한 언어스타일등 기존의 안드로이드 개발자들이 좀더 쉽고 안정적으로 앱개발을 이어나갈수 있도록 하기 위해서 라고 생각할수 있다.

사실 코틀린은 안드로이드OS만을 위해 개발된 언어가 아니고 서버프로그램등과 같은 풀스택 웹 개발, Android와 iOS앱, 그리고 임베디드와 IoT등 모든 개발을 다양한 플랫폼에서 사용이 가능한 언어이다.

따라서 안드로이드 관련 IoT, 임베디드 제품까지 사용가능능한 앱을 만들기 위해 코틀린을 배워 놓으면 모바일앱뿐만 아니라 서버와 화면단까지 영역을 넓힐수 있는 가능성이 있다.

Kotlin 2016년에 비로소 정식 버전이 출시된 이후에 현재는 1.2.51 버전으로 아직도 계속 진화중인 언어이다.

1.1 코틀린으로 개발할수 있는 것들

코틀린으로는 JVM이 가능한 환경이라면 백엔드의 서버용프로그램(JAVAEE,Tomcat등) 부터 라즈베리파이, 안드로이드 등과 같은 모바일 프로그램 , PC용데스크탑 프로그램, 웹용 Javascript 프로그램등 까지 전부 코틀린을 이용하여 만들수 있다.

JAVA와 100%호환성을 자랑하고 있기에, JAVA의 수많은 API 등을 참조하여 개발할수 있다.

  • 안드로이드
  • 톰캣기반의 응용프로그램 (Spring)
  • JAVA EE
  • Javascript
  • HTML5
  • IOS
  • 라즈베리파이

1.2.코틀린 특징

최근의 프로그램 언어 추세에 걸맞는 간결한 문법을 제공한다.

기존의 Java, c 등의 언어와는 달리, 형변환, 확장함수, 널안전성 등 개발자가 귀찮은 작업은 덜하게하고 좀더 간결하고 재미있게 기능을 확장해 나가면서 프로그래밍을 할수 있도록 도와준다.

사실 이러한 기능들이 코틀린 만의 특징은 아니고, 최근의 모던 프로그램언어의 특징이라 할수있다.

코틀린은 이러한 모던 프로그램적인 요소를 대부분 포함하고 추가로 JAVA 와의 100% 호환성이라는 무기로 기존의 자바프로그래머들이 새로운 형태의 프로그램언어에 쉽고 빠르게 적용할수 있도록 교두보 역할을 할것이다.

코틀린이 대표적인 장점은 다음과 같다.

1.2.1 JAVA와의 100%호환성

기존의 자바 개발자들이 가장 반기는 부분이 아닐까 생각된다. 코틀린으로 프로그래밍 하다가 자바 API 의 기능을 사용하고자 할때, 코틀린에서는 그냥 해당코드를 불러내서 사용하면된다. 반대의 경우도 JAVA에서 코틀린 코드를 사용할수도 있다.

이러한 동작이 가능한 이유는 프로그램 빌드시에 코틀린이 JVM바이트코드로 변환해 주기 때문에 코틀린 + JAVA 를 섞어가며 개발하는 것이 가능한 것이다.

1.2.2 간결한 문법

자바로 장황하게 표현해야 했던 중복작업을 줄이기 위해서 kotlin에서는 여러가지 간결한 문법을 제공한다.

중복되고 반복되는 작업을 boilerplate code 라고 하는데, 코틀린은 이러한 boilerplate code 를 최대한 줄여 개발시간을 조금이라도 단축할 수 있도록 간결하고 효율적인 문법을 제공한다.

1.2.3 널안정성

코틀린의 변수는 빈값을 가질수 있는것과 없는것, 즉 Nullable 과 NotNull 로도 나뉘는데 , Nullable 로 사용하면 해당 변수가 ‘널’ 이라는 특수한 값을 가지게 되어, 이를 개발시에 미리 검증할수 있어 컴파일시에 널에 값에 의해 프로그램이 다운되는 현상을 방지할수 있다.

예를들어 JAVA, C 프로그램들은 변수에 널 값이 들어가 있을 때 컴파일시에는 이를 감지 못하고, 프로그램실행시에 널 값에 의해 오류가 나면서 프로그램이 멈추게된다.

코틀린에서는 변수선언시에 특수한기호(물음표?) 를 두어 널값이 들어갈수도 있는 변수를 선언하도록 하고, 이를 프로그래밍 시에 개발자가 널값에 대해 미리 대응을 해야 프로그램이 실행가능하도록 컴파일 된다.

따라서 널안정성처리를 한 코드는 프로그램실행시(런타임)에 널 변수에 의해 프로그램이 멈추거나 하지않도록 할수 있다.

1.2.4 함수형프로그램, 확장함수,연산자 오버로드

코틀린은 함수형 프로그랭이 가능하다. 함수형 프로그램이란 사실상 모든 함수를 객체로 인식하여 값을 할당하고, 파라메터로 전달하고, 값을 반환받고게 가능하다는것을 의미한다. 주로 javascript등에서 고차함수라는 이름으로 구현되었던 기능이다.

코틀린에서도 이처럼 함수자체를 변수에 선언하거나, 파라메터, 반환값으로 전달할수 있어서, 좀더 다양한 프로그램기법을 구사할수있다.

이러한 연장선상에서 확장함수 기법도 마찬가지로 함수 자체를 하나의 객체로 인식하여 기존의 기능에 확장해 나갈수 있다.

C++프로그래머라면 연산자 오버로드가 어떤것인지 잘 알고 있으리라 생각된다. 코틀린에서도 연산자 오버로드가 가능하지만 C 와는 조금 다르다.

코틀린에서는 변수의 기본타입(Int, Double, String, Boolean등)이 모두 객체로 취급된다. 따라서 객체에 기능을 추가하는 방식으로 연산자를 오버로드하여 새로운 연산자를 만드는 등이 가능한다.

java 에서는 함수,즉 메소드 는 클래스안에 속해있어야 되는 존재였지만, 코틀린에서는 함수자체가 독립적으로 기능을 구현하고, 기존 기능을 확장할수 있으며, 객체의 내부 멤버함수로도 사용가능하도록 되어있다.

함수형 프로그랭의 유용한 방식인, 고차함수,익명함수,람다,인라인함수,클로저,제너릭, 꼬리회귀 등이 지원된다.

1.2.5 그 밖의 특징

  • 예외처리를 강제하지 않는다.
  • static 이 없고 companion Object 라는 것을 사용한다.
  • 람다를 지원한다.
  • lazy , init 등 변수 초기화에 대한 다양한 방법을 제공한다.
  • 타입추론이 가능하다.
  • 패키지 펀셩 이 가능하다.
  • infix 함수 선언로 인간이 알아보기 편한 중위표현이 가능하다.
  • switch 대신 좀더 강력한 when 을 사용한다.
  • in…range가 있다.
  • is 연산자가 있다.
  • if문이 변수선언에 사용될수도 있다.
  • new 키워드가 없다
  • 세미콜론이 필요없다.
  • 엘비스연산자가 있다.
  • 경량 쓰레드 coroutine이라는 것을 제공한다.
  • 그밖의 등등

2 코틀린 개발 환경 구축

2.1 설치안하고 온라인으로 바로 프로그램 해보기

intellij 회사가 제공하는 온라인 코틀린 놀이터(? playground)에 접속해서 기본적인 코틀린 문법을 테스트하고 실행해볼수 있다.

그리고 Example코드들도 준비되어 있어서 intellij 에디터를 설치하기 전이라면 코틀린의 문법에 대해 이해하기 위해서 미리 연습해 보는 것도 좋다.

alt_text
2.2 Intelli 설치

본격적으로 kotlin 프로그램을 개발하기 위해서는 intellJ IDEA 라는 데이터를 설치해서 개발하면 쉽고 빠르게 프로그램을 개발할수 있도록 도와 준다.

alt_text

사진은 intellJ 공식홈페이지에서 참고

2.3 안드로드이 스튜디어 설치

안드로이드 앱 개발자라면 Android Studio를 사용하여 개발하기를 권장한다. 안드로이드 스튜디오는 intelliJ 의 기능과 안드로이드앱을 만들기 위한 여러가지 편리한 기능이 탑재되어 있으므로 안쓸이유가 없다.

alt_text

3.코틀린 기초

3.1 변수 와 배열선언

3.1.1 변수

3.1.1.1 일반 변수의 선언 var, val

코틀린 에서의 변수 선언에는 val 과 var 키워드를 사용한 두가지로 나뉜다.

java 나 c에서는 변수의 형식(타입) 을 정하고 변수의 이름을 적었지만, 코틀린에서는 var, val 이 두개의 키워드와 타입을 적어서 변수를 선언한다.

이 둘의 차이는 다음과 같다.

  • var : variable 의 줄임말로 언제든지 값을 읽기/쓰기가 가능한 일반 변수(Mutable)
  • val : valuable 의 줄임말로 초기에 값을 선언후에는 읽기만 가능한 변수(Immutable )
*참고
프로그램용어 Mutable 과 Immutable 이라는 용어가 나오는 의미는 다음과 같다
Mutable : 변할수 있는 값
Immutable : 변할수없는 불변의 값

변수명은 문자나 밑줄로 시작해야하고 아래와 같은 변수명은 허용하지 않는다.

  • 숫자로 시작하는 변수명 (var 18age)
  • 공백 포함 (var age 18)
  • 대소문자구분(var mark 는 var Mark와 다른 변수이다)
  • @, #, %등의 특수기호는 포함할수 없다.(var age@18)
  • kotlin에 의해 예약된 단어들은 변수명으로 사용할수 없다.(class, var, val, as, when, try…)

프로그램에서 선언은 다음과 같이한다.

//var ( variable, Mutable ) 언제든지 값을 읽기/쓰기가 가능한 일반 변수
var myNickName: String = "Hello Nick"
var myMoney: Int = 10000
myNickName = "Hello Kim"   // var 라서 값을 변경하는데 문제없음

//val ( valuable, Immutable  ) 초기에 값을 선언후에는 읽기만 가능한 변수
val myName: String = "Hello"
val myHeart: Int = 1
 myName = "world"   // val 라서 값을 변경하면 에러가 남

//변수 선언시 타입을 쓰지 않아도 들어오는 값의 형식에 의해 자동으로 타입 추론 가능
var mySecondName = "Hello TH"   // String 타입을 지정하지 않아도 들어오는 값이 문자열이기 대문에 String타입의 변수가 됨.

일반적으로 프로그램내에서 수시로 값을 할당하거나 읽어올때 사용할 변수라면 var 를 사용하고, 딱 한번 지정된 값으로 계속 읽기만 해야 되는 상수( final)성격의 변수라면 val 를 지정하면 된다.

이전부터 프로그램을 해왔던 java 개발자라면 final 이라는 키워드를 지정하여 변수가 값이 변하지 않는 최종값이라고 선언해왔을것이다. 코틀린에서는 변수의 선언시 val라는 키워드를 사용한다고 생각하면된다.

3.1.1.2 상수

앞서 val라는 키워드를 설명할때 “읽기만 가능한 변수,Immutable” 이라고 설명했다.

이 때 타 언어개발경험자라면 변하지 않는 변수는 결국 상수 이기도 하니까 const 키워드를 떠올릴지도 모르겠다.

물론 코틀린에서도 const 를 이용하여 상수를 선언할수 있다.

프로그램에서 선언은 다음과 같이 한다.

//constraint , 즉 변수의 값을 정해놓고 사용하는 것이기 떄문에 val 로 선언한다.
const val MY_NAME:String = "kotlin" 
const val MY_NAME:Int = 123 

단 한가지 주의할게 있는데, const 키워드를 지정하면 해당변수는 컴파일 단계에서 변수의 값을 읽어서 해석되는데, 코틀린 처럼 변수에 함수등을 지정할수 있는 경우에는 컴파일 단계에서 함수의 반환형태를 알수가 없어서 에러가 나는 경우도 있다.

즉, 실행해야 결과를 알수 있는 함수를 변수값으로 할당하고 이를 다시 const 로 지정하게 되면 컴파일 단계에서 어떤 값이 될지 알수가 없어서 에러가 나게된다.

const val MY_NAME:String = "kotlin" //정상,컴파일 시에 값이 읽힌다.
val age:Int = getMyAge() //정상,런타임(실행)시에 값이 읽힌다.

//반면 아래처럼 선언하면 컴파일시에 에러메시지가 나온다.
const val MY_AGE:Int = getMyAge() //에러,컴파일 시에 값이 읽힌다.

fun getMyAge(): Int {
 return 7
}

fun main(args: Array<String>) {
 println("My Name $MY_NAME")
 println("My Age $age ")
}

3.1.1.3 Null 대입 가능 변수

Null이라는 것은 값이 없다, 즉 빈값 라는 의미인데, 이 null이라는게 Java와 C 등의 프로그램실행시에 불쑥 튀어나와서 프로그램실행을 멈추게 하는 아주 골칫거리이다.

코틀린에서는 적어도 실행중인 프로그램이 불시에 멈추는 것을 방지하기 위해서 개발자가 null값에 대해 프로그램 개발 단계에서 미리 예방을 할수 있도록 지원해 주는 데 이를위해 변수와 함수의 파라메터, 리턴값등에 빈값을 가질수 있도록 Nullable 키워드를 추가했다.

Nullable 키워드는 빈값, 즉 null 값을 허용하고자 하는 변수에 선언시 자료형의 맨끝에 ?(Question) 기호를 붙이면된다.

프로그램에서의 선언은 다음과 같다.

//  null값을 짚어넣을수 있는 변수
var myNickName: String? = null
//보통은 nullable사용시 변수명뒤에 ? 를 붙여서 사용한다.
println(myNickName?.length)

변수를 nullable 로 선언하고 사용시에 ? 를 붙이지 않거나, null 체크를 별도로 하지 않는다면 코틀린에서 컴파일 시에 다음과 같은 에러를 내면서 컴파일이 되지 안는다.

Error : Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

프로그래머는 해당 변수를 사용할때마다 미리 null 값을 체크하거나, null safe 기호(?)를 붙여서 변수의 값이 null 일때 는 그냥 null을 돌려주겠다는 지정을 해야한다.

즉 위의 소스 코드를 컴파일이 되도록 수정한다면 다음과 같다

//방법1. 프로그래머가 직접 null값 체크
if(myNickName != null){       
        println(myNickName.length)
}

//방법2. Safe Call Operator ? 을 변수명뒤에 추가
println(myNickName?.length)
-> null

//방법3. Elvis Operator ?: 를 사용해서 null일경우에 기본값지정
println(myNickName?.length ?: 0)
-> 0

//방법4. Force !! 사용해서 null일경우에도 강제로 변수사용
println(myNickName!!.length) 
-> 컴파일은 성공하지만 실행시 nullPointException 런타임 에러

코틀린 에서는 변수,함수 심지어 숫자까지 객체로서 인식하기 때문에, 기능의 확장, 사용시 null 포인트에 대해 더욱 민감하다. 따라서 개발시에 변수, 함수 리턴값 등에 널 값이 들어갈 확률이 조금이라도 있다면 nullable 로 선언해서 null에 대한 방지를 하는것이 좋다.

3.1.2 배열

코틀린에서의 배열은 원시타입형태와 박싱타입형태의 크게 두가지로 나뉜다.

원시타입배열은 Int, String,Byte,Char 등의 원시데이터타입에 대한 배열을 만들때 사용된다. 원시타입배열이 있는 이유는 배열의 선언과 사용시 각 배열의 원소에 대해 어떤 데이터 타입인지 검증하거나 변환하는 과정을 줄여서 빠르게 응답하도록 하기 위해서이다.

코틀린에서 배열을 선언하는 방법은 2가지로 Array클래스를 사용하는 방법과 Array라이브러리 함수를 사용하는 방법이있다.

3.1.2.1 Array클래스를 사용하여 배열 생성방법


코틀린에서는 배열이 Array클래스로 취급되기 되기 때문에, 배열의 개수와 초기화 파라메터를 지정해야 배열을 생성할수 있다.

//  정수형 값들을 가지는 배열을 선언
val myArray:Array<Int> = Array<Int>(10, {i -> i});
// null로 채워진 배열을 생성
val myArray:Array<Int?> = arrayOfNulls<Int>(10);

3.1.2.2 Array라이브러리 함수를 사용하여 배열생성방법

코틀린에서는 배열을 생성하기 위한 별도의 라이브러리 함수(Factory, 생성을 대신해주는함수)를 포함하고 있다.

코틀린에서의 배열은 Array클래스로 되어있기 때문에, 배열의 개수와 초기화 파라메터를 지정해야 배열을 생성할수 있다.

Array 라이브러리함수는 크게 3가지의 종류로 나뉜다.

  • 배열 선언시 자료형식을 지정하여 배열을 생성하는 함수(Boxing이 발생)

    arrayOf<자료형>, arrayOfNulls<자료형>

//  arrayOf 함수는 선언시 요소를 지정
val myArray:Array<Int> = arrayOf(4, 7, 1);

*Boxing이란 입력된 자료를 지정된 자료형태로 한번 감싸는 작업을 의미하는데 이로인해 배열요소에 접근할때 unBoxing이라는 과정을 거치게되어 처리 속도가 조금더디게 된다.

  • 자료형 자체로 배열을 생성하는 함수

    intArrayOf, doubleArrayOf, booleanArrayOf, StringArrayOf 등 기본형식의 자료형에 모두 대응

//  intArrayOf 함수는 정수형 자료의 배열을 생성
val myIntArray:IntArray = intArrayOf(4, 7, 1);
//  doubleArrayOf 함수는 실수형 자료의 배열을 생성
val myDoubleArray:DoubleArray = doubleArrayOf(0.1, 0.5, 1.7);
  • 컬렉션을 배열로 변환하여 생성하는 함수(Array 클래스의 확장함수)

    toIntArray, toLongArray, toDoubleArray, toFloatArray, toByteArray, toCharArray

//  intArrayOf 함수는 정수형 자료의 배열을 생성
val myIntArray:IntArray = intArrayOf(4, 7, 1);
//  doubleArrayOf 함수는 실수형 자료의 배열을 생성
val myDoubleArray:DoubleArray = doubleArrayOf(0.1, 0.5, 1.7);

3.1.2.3 코틀린 배열 확장함수

코틀린에서는 배열을 좀더 쉽고 편하게 다루기 위한 배열관련의 무수히 많은 확장함수를 가지고 있다. 예를 들어 배열요소에 대해 최대값,최소값, 평균 구하기, 정렬, 분할,병합 등 개발시 꼭 필요한 기능이 포함되어 있는데 몇가지만 소개해 본다.

//  intArrayOf 함수는 정수형 자료의 배열을 생성
val myIntArray:IntArray = intArrayOf(4, 7, 1, 3, 5);

// max 함수는 배열요소중 가장 큰수를 리턴한다. 
myIntArray.max() // 결과 -> 7

// mmin 함수는 배열요소중 가장 큰수를 리턴한다. 
myIntArray.min() // 결과 -> 1

// first 함수는 배열요소중 가장 첫번째 요소값를 리턴한다. 
myIntArray.first() // 결과 -> 4

// last 함수는 배열요소중 가장 첫번째 요소값를 리턴한다. 
myIntArray.last() // 결과 -> 5

// last 함수는 배열요소중 가장 첫번째 요소값를 리턴한다. 
myIntArray.first() // 결과 -> 5

// filter 함수는 배열요소중  지정된 조건에 맞는 요소값들을 배열로 리턴한다. vmyIntArray.filter { it < 4 } // 결과 -> [1, 3]

// isEmpty 함수는 배열이 빈 배열인지 true, false 값으로 리턴한다.
vmyIntArray.isempty() // 결과 -> true

val deepArray = arrayOf(
    arrayOf(1),
    arrayOf(2, 3),
    arrayOf(4, 5, 6)
)

// deepArray함수는 다차원배열요소 1차원 배열로 변환하여 리턴한다. 
deepArray.flatten()

이 밖에도 joinTo, groupTo, sliceTo, reduce, map,sorted 와 같은 함수들이 있다.꼭 전부 외워야 할필요는 없지만, 어떤 확장함수기능을 지원하는지 코틀린 공식페이지에서 참고하면 좋다.

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-array/index.html

*코틀린에서 프로그램할때는 배열보다 컬렉션을 주로 사용하는 게 좋다. 단.아래의 두가지 경우에는 반드시 배열을 사용해야한다.

  • 자바로부터 배열로 값을 전달받았을경우
  • vararg 기능을 통해 함수의 인자를 배열로 넘겨받았을 경우

컬렉션은 고급기능에서 설명한다.

3.2 분기문과 반복문

코틀린에도 기본적인 if, for, while 의 분기문과 반복문이 있다. 다른 언어에는 분기문으로서 switch라는 것이 있는데 코틀린에서는 when 이라는 좀더 강력한 기능 으로 대신하고 있다.

분기문과 반복문은 프로그램의 짤때 매우 빈번히 사용되는 구문이므로 제대로 이해하고 확실한 사용법을 알아두는게 좋다.

3.2.1 IF 문

if 무은 “만약 00 라면 XX 할건데" 라는 표현식으로, 프로그램에서는 기본중의 기본 문법이다.

프로그램이 단순히 직선구조가 아니라면 if 문을 사용하여 여러가닥으로 나뉘게 되고, 각자 다른 결과를 얻게해준다.

코틀린에서도 if 문은 프로그램의 기본문법대로 동작한다. 추가로 특수한 기능을 하는데 , 변수선언시에 리턴값 돌려주는 기능도 한다. 이처럼 if 문이 값을 돌려주는 기능도 하기 떄문에 3 항 연산자( a ? b : c)가 없다.

기본적인 IF 문의 사용법은 다음과 같다.

val myAge:Int = 21 
//기본사용법은 다른 언어와 다르지 않다.
if ( myAge > 18 ) {
   println("어른 이군요")
} else {
   println("학생 이군요")
}

if문은 조건문이 아니라 조건식으로도 사용되는데, 이때는 if 문의 최종 라인에 반드시 리턴될 값을 지정하면된다.

val myAge:Int = 21 
//조건식으로 사용되려면 반드시 리턴값을 지정해야 한다.
val isAdult:Boolean = if ( myAge > 18 ) {
   println("어른 이군요")
  true
} else {
   println("학생 이군요")
  false
}

//위 처럼 간단한 조건식일 경우에는 좀더 줄여서 작성할수있다.
val isAdult:Boolean = if ( myAge > 18 ) true else false

//in 키워드를 이용할수 있다..
val isAdult:Boolean = if ( myAge in 10..19 ) true else false


//변수 뿐만 아니라 함수의 선언에서도 동일하게 작성할수 있다.
fun checkAdult(yourAge: Int, polisy : Int) = if ( yourAge> polisy ) true else false

3.2.2 For

for 문은“주어진 개수만큼 계속 반복" 라는 기본기능을 수행한다. “주어진 개수” 는 사용자가 숫자를 지정할수있고, 반복자(iterator)를 제공하는 모든 것에 대해 반복수행을 해준다.

코틀린에서는 in , step 지정자를 사용하여 반복한다.

기본적인 for 문의 사용법은 다음과 같다.

-범위지정자 (..) 을 사용하면 지정된 숫자의 범위수만큼 반복한다.

val myAge:Int = 21 
// 범위지정자 (..) 을 사용하여 1살 부터 myAge 살 까지 1 스텝씩 반복해서 출력.
for (i in 1..myAge step 1) {
    println("${i} 살 생일")
}
/* 결과
1 살 생일
2 살 생일
3 살 생일
...
21 살 생일
*/
  • 배열이나 컬렉션의 경우에는 자동으로 내부 요소를 반복한다
//배열이나 컬렉션의 경우에는 자동으로 요소를 반복해준다.
val myIntArray:IntArray = intArrayOf(7, 13, 17);
for (age in myIntArray) {
    println("${age} 살 에는 학교 졸업")
}
/* 결과
7 살 에는 학교 졸업
13 살 에는 학교 졸업
17 살 에는 학교 졸업
*/
val myIntList:List<Int> = myIntArray.asList();
for (age in myIntList) {
    println("${age} 살 에는 학교 입학")
}
/* 결과
7 살 에는 학교 입학
13 살 에는 학교 입학
17 살 에는 학교 입학
*/

//배열이나 컬렉션의 확장함수 withIndex를 이용하면 해당요소의 고요번호를 얻을수 있다.
for ((index,age) in myIntList.withIndex()) {
    println("순서 ${index} -> ${age} 살 에는 학교 입학")
}
/* 결과
순서  0 -> 7 살 에는 학교 입학
순서  1 -> 13 살 에는 학교 입학
순서  2 -> 17 살 에는 학교 입학
*/

for (i in 1 until 100) { ... }
for (x in 2..10 step 2) { ... }

-Map 자료형식과 같이 key, Value로 저장되어 있는 경우에도 자동으로 내부 요소를 반복한다.

//map 자료형식처럼 key와 value가 있는 경우에도 반복
val cars = mapOf("a" to "AUDI", "b" to "BMW", "c" to "CRISLER");
for (car in cars) {
    println("${car.key} is ${car.value}")
}
/* 결과
a is AUDI
b is BMW
c is CRISLER
*/

중첩된 for 문에서는 Label을 사용하여 중단점을 효과적으로 제어할수도 있다.
for 문을 사용할때에 break, continue를 사용하여 중단하거나 다음 아이템으로 건너뛰기를 할수 있는데 중첩된 for 문의 경우 처리가 매우 귀찮게 된다. java에서는 label을 지원하는데 kotlin에서도 사용할수 있다.

 first@ for (i in 1..5) {
        second@ for (j in 1..3) {
            println("i = $i; j = $j")
 
            if (i == 4)
                break@first
            if (i == 2)
                continue@first
        }
}
//두번째 for문을 반복할떄 i값이 2이면 첫번쨰 for문을 한번 건너뛰어 i를 3으로 이동한다.
//두번째 for문을 반복할떄 i값이 4이면 첫번쨰 for문을 중단하게 뒤에 그 영향으로 두번째 for문도 멈추게된다.    

3.2.3 While

while문은 “조건이 맞을때 까지 계속 반복" 이라는 기능을 수행한다.

다른 언어와 마찬가지로 두가지의 종류가 있는데 다음과 같다.

  • do…while
    일단 do 안의 내용을 한번 실행한 후에 while 조건이 참일동안 계속 do 안의 내용을 반복한다.
var isCorrect = true
var nexti = 0;
do {
        println("nexti -> ${nexti}")
    if (nexti++ > 1) {
        isCorrect = false
    }    
} while(isCorrect)


/* 결과
nexti -> 0
nexti -> 1
nexti -> 2*/

위의 예제에서는 do 구문의 내용을 먼저 실행하고 while조건을 검사하기 때문에 두번 출력된다.

  • while
    while조건이 참인지를 먼저 검사후에 while문의 내용을 반복한다.
var isCorrect = true
var nexti = 0;
while(isCorrect) {
    println("nexti -> ${nexti}")
    nexti++
    if (nexti > 1) {
        isCorrect = false
    }
} 

3.2.4 When

입력된 값에 대해 다수의 if 문을 만들기 번거로울때, 다른 언어에서는 switch라는 분기문을 사용한다.

코틀린 언어에는 switch문이 없는 대신에 When 이라는 분기문을 제공하는데, 기본적으로 여러조건에 따른 분기는 switch문과 같지만, 좀더 다양한 조건과 분기 처리가 가능한 새로운 분기문 형태이다.

특징은 다음과 같다.

  • 기본적으로 switch처럼 주어진 값에 맞는 분기문의 내용을 수정한다.
  • 주어진 조건 인자 값이 아무거나 올수 있다.( 숫자, 문자 ,객체, 함수)
  • 조건에 대한 검사는 개발자가 원하는 대로 지정할수 있다.
  • break 문이 필요없다.
  • 다중조건을 지정할수 있다.

when의 조건문은 다양하게 지정이 가능하다

  • 범위지정자를 조건으로 지정
val mySpeed = 78
    
when(mySpeed) {
    in 0 .. 60 -> print("저속운전")
    in 61 .. 70 -> print("모범운전")
    in 71 .. 100 -> print("과속운전")
    else -> print("운전중?")
}
  • 중복된 조건을 지정 하고 결과를 변수에 대입식으로 지정
val myArea = "한국"
    
val yourCountry = when(myArea) {
    "중국","한국","일본" -> "아시아"
    "영국","프랑스" -> "유럽"
    "토고","케냐" -> "아프리카"
    else -> "모르는 지역"
}

println("your country is ${yourCountry}");
  • when 에 인수를 생략하면 해당 조건이 참일경우에 분기
val myNumber = 15
println(when {
    Math.abs(myNumber) > 0 -> "양의 정수"
    Math.abs(myNumber) < 0 -> "음의 정수"
    else -> "영"
});

// 출력 -> 양의 정수
  • 조건과 분기문에 함수를 지정하고 결과값을 할당
fun compare(numA: Int, numB: Int) = if(numA > numB) true else false 
    
fun whichNumber(message: String) = "a 와 b 를 비교하면 ${message}" 

val a = 5
val b = 15
    
val result = when {
    compare(a, b) -> whichNumber("a가 크다")
    compare(b, a) -> whichNumber("b가 크다")
    else -> "같다"
};



// 출력 -> a 와 b 를 비교하면 b가 크다
  • Sealed class 와 같이 사용하면 간단히 state machine 을 만들수도 있다

3.3 함수 선언

특정 기능을 반복 수행하는 작업을 좀더 간단히 하기 위해서 함수라는 것을 사용한다.

예를 들어 커피숍에서 커피를 만들때, 직원이 커피원두를 직접 갈아서 물을끓이는 반복작업을 주문시마다 하게되면 하루에 팔수 있는 커피수는 한계가 있을 것이다. 이때 커피머신 이라는 기계를 사다놓으면 직원은 그냥 버튼만 누르면 원하는 커피를 만들어 낼수 있다.

함수는 바로 커피머신에 해당한다.

3.3.1 함수선언형식 , 인자와 리턴값

코틀린에서의 함수의 선언은 다음과 같다.

fun 함수이름(전달받을 인자): 함수가 돌려줄 값형식 { }

fun sayHello(myArg: String):Unit{
   print("Hello${myArg}")
}

java 나 c언에서는 함수이름의 앞에 “돌려받을 값의 형식을 지정했지만, 코틀린에서는 반대로 맨끝에 지정한다는 점 정도가 다르다.

여기까지는 다른 언어와 별반 다를거 없어 보이지만 사실 코틀린에서 함수란 또하나의 객체이기 때문에, 기존의 전통적인 함수를 기능이외에 다양한 형태로 확장하거나 이용하여 개발할수 있다. 고급기능 함수 다루기 에서 설명한다.

함수의 선언예

fun 함수명(인자: 인자타입): 리턴값타입 {
    return 리턴값
 }

 fun mySample(inArg: Int): Int {
    return inArg * 2
 }

코틀린에서는 함수를 위와 같이 간단한 코드는 간단하고 짧게 표현할수있다.

fun mySample(inArg: Int): Int = inArg * 2

리턴값을 타입추론 할수 있으므로 조금더 짧게 할수 있다.

fun mySample(inArg: Int) = inArg * 2

3.3.2 가변인자

함수를 선언시에 함수안에 여러개의 인자가 필요할떄, vararg 라는 지시어로 한번에 여러개의 인자를 받을수 있다.

vararg 를 사용하지 않는다면, 함수인자를 여러개 선언하거나, 배열로 받아서 처리해야 되는 불편함이 생기게 된다.

fun myInputer(input1: Int,  input2: Int,  input3: Int, input4: Int, input5: Int, input6: Int, input7: Int,
) {}

또는

fun myInputer(input: Array<Int>) {}

코틀린 에서 지원하는 vararg를 이용하면 여러개의 인자에 대한 처리를 간단하게 할수있다.
입력인자는 , 로 구분하여 입력한다. vararg로 입력받은 인자는 배열형식이다.

vararg 사용

fun myInputer(vararg inputs: Int): Int {
    var result = 0
    for(num in inputs){
        result += num
    }
    return result
}

fun main(args: Array<String>) {
    println(myInputer(1,2,3,4,5,6,7,8,9,10))
}

3.3.3 제너릭 타입

프로그램을 좀더 유연하고 다양성있게 제작하기 위해서 Generic이라는 것을 사용한다. 즉 타입을 일반화 시켜서 해당 타입에 대해 가능할것같은 처리로직을 미리 지정하여 하나의 함수를 좀더 범용적으로 사용할수 있도록 해준다.

주로 이니셜 T 를 쓰는데, Type의 줄임말이다. 함수는 어떤타입T가 들어올거라 예상하고 T타입으로 처리할수 있을만한 처리로직을 미리 구현한다. 물론 T가 어떤 모든 타입은 아니고 개발자가 예상할수 있는 타입이어야 한다.

이는 마치 자동차 공장에서 일반적인 어떤 타입의 자동차가 들어와도 바퀴를 떼고 붙일수 있도록 미리 라인을 범용성있게 구축해 놓는 것과 같다.

fun  <T> asList(vararg inputs: T): List<T> {
  val result = ArrayList<T>()
  for (num in inputs) {
    result.add(t)
  }
  return result
}

fun main(args: Array<String>) {
    val list = asList(1, 2, 3)
}
  • vararg 에 배열변수를 넘길때는 * 가 필요하다.
//수정필요.
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4) 

코틀린 에서는 함수를 1급객체로 취급하기 떄문에 함수에 대해서도 제너릭사용이 가능하다… 1급객체란 함수 자체가 연산에 사용되거나, 인수, 리턴값으로 취급 등을 지원하는 객체를 가리킨다.

따라서 java 보다는 함수자체로 함수 있는 것들이 많이 있는데. 다음 4.고급기능 에서 설명하겠다.

3.4 클래스와 프로퍼티

객체지향 프로그램이 빠질수 없는 것이 클래스와 프로퍼티이다. 현대의 대부분의 언어는 클래스와 프로퍼티의 개념을 지원하며, 프로그램을 좀더 체계적이고 견고하게 만들수 있도록 도와준다.

클래스는 단어그대로, 어떤일을 하는 서로 관련된 함수(메소드)들을 모아놓은 집합체이다.
클래스 형태로 프로그램을 하게 되면 다음과 같은 것들을 얻을수 있다.

  • 은닉화
  • 상속과 다형성
  • 캡슐화
  • 재산성

코틀린은 Java 100% 호환하기에 클래스의 기본 개념을 모두 지원한다.

추가로 클래스 기반으로 프로그램할때 여러번 반복하거나, 장황하게 써내려갔던 메소드선언 등의 작업을 줄이기 위해서 data 클래스, object 클래스, 등의 추가 문법을 제공한다.

클래스 선언의 기본 형태는 다음과 같다.

class  클래스이름 {
   
}

선언한 클래스의 사용은 다음과 같다.

java , c++ 등의 언어에서는 클래스를 사용할때 new 라는 지시어가 필요했는데, 코틀린에서는 new 를 하용하지 않고도 클래스를 사용할수 있다.

val className = ClassName()

보통 클래스에는 프로퍼티, 생성자, 함수(매소드) 등을 추가로 선언하는데 다음과 같다.

class 클래스이름 생성자함수(프로퍼티1 : 프로퍼티1 타입) {
  val 프로퍼티2: 타입 
  fun 함수 {

  }
}

코드로 표현 하면 다음과 같다.

class MyClassroom construction(teacherName: String) {
    var students = mutableListOf<String>()
    fun addStudent(studentName: String) {
         students.add(studentName)
    }
}

val myClassroom = MyClassroom("나선생")
myClassroom.addStudent("너학생")

java , C 개발자였다면 위의 코드에서 MyClassroom앞에 new키워드가 없다는 것을 눈치챘을지도 모르겠다. kotlin에서는 new 를 적지않아도 된다.

3.4.1 생성자

생성자함수란 클래스를 이용하여 객체를 생성할때 생성과 동시에 처리해야 될것이 있을때 생성자(constructor()) 라는 특수함 함수(메소드)를 지정하여 필요한 처리를 하도록 하는 프로그램 기법이다.

자바등의 언어에서는 클래스 이름과 생성자의 이름을 같은 이름으로 지정하여 객체생성시에 자동으로 해당 함수가 실행되도록 한다.

java, c++ 등의 프로그램에서 생성자를 선언하는 예. 오버라이딩의 예

class ClassName {
  public ClassName() {
    // 객체 생성과 동시에 처리해야 하는 부분
  }

  public ClassName(String name) {
    // 객체 생성과 동시에 처리해야 하는 부분
  }

  public ClassName(String name, Integer age) {
    // 객체 생성과 동시에 처리해야 하는 부분
  }
}

 ClassName myclass0 = new ClassName()
 ClassName myclass1 = new ClassName("생성과 동시에")
 ClassName myclass2 = new ClassName("너이름", 12)

위의 예처럼 java 에서는 각각 인수가 다른 생성자를 만들어 , 객체의 생성시에 주어지는 인자의 개수에 따라 각기 다른 생성자가 실행되도록 할수 있다.

이런것을 오버로딩 이라고 한다.

위의 Java 의 예를 보면 같은 이름의 생성자가 3개나 선언되어 있는 것을 볼수 있다.

코틀린에서는 생성자함수를 좀더 짧고 간단히 사용할수 있도록 지원한다.

클래스의 선언부에 constructor 라는 함수를 이용해서 인자를 선언해주면 자동으로 생성자 함수가 인자의 개수만큼 만들어지며, 인자에 대한 getter 와 setter 가 자동으로 생성된다.

코틀린에서는 다음과 같이 선언한다.

class ClassName constructor(name:String,  age: Int){

}

줄여서 다음과 같이 할수도 있다.

class ClassName(name:String,  age: Int){

}

클래스 객체를 사용할 때는 다음과 같다.

class ClassName(name:String,  age: Int){

}

생성자 함수를 통해 전달받은 인자는 마치 함수에서 사용하는것 처럼 바로 사용할수 있다.

class ClassName(yourName: String) {
  val upperName = yourName.toUpperCase()
}

객체 생성시 좀더 다양한 작업을 해야 된다면 init 블럭안에 작성할수도있다.

class ClassName(yourName: String) {
  init {
    val upperName = yourName.toUpperCase()
    println("Initialized with value ${upperName}")
  }
}

생성자를 private 로 작성할경우가 있는데, 주로 인스턴스를 생성할 필요없는 공용클래스등을 만들때 private로 선언한다. 생성자가 private 로 선언된 클래스는 인스턴스를 만들어도 생성자가 private 이기 때문에 사용할수 없는 상태가 되어 버려, static 형태로만 사용가능한 클래스를 만들수 있다.

코틀린에서는 기본적으로 컨스트럭터에 지정된 인자의 수만큼 자동으로 오버로드 함수들을 만들어주지만, 때로는 사용자가 필요한 기능을 탑재한 컨스트럭터를 만들고 싶어 질떄가 있다.

이때는 서컨더리 콘스트럭터라는 것을 사용한다.

class myZipcode(val zipCode: String) {
   constructor(firstZipcode: String, lastZipcode:String): this("$firstZipcode - $lastZipcode")
}


val myzipcode = myZipcode("123", "456")
println(myzipcode.zipCode)

3.4.2 프로퍼티

클래스 내부에서 사용하는 변수로서 멤버변수라는 것을 프로퍼티라고 한다.

java 개발자라면 클래스의 멤버변수로서 선언하고, 외부에서 해당 변수를 접근하기 위해서는 getter 라는 함수와 setter 라는 함수를 만들어서 접근하도록 개발하였을 것이다.

기존의 자바에서 멤버변수 선언방법

class myClass {
    private Integer myAge;
    public Integer getAge() {
        return this.myAge;
    }
    public Integer setAge(Integer age) {
        this.myAge = age;
    }

}

위처럼 코딩을 하다보면 10 개 이상 의 멤버변수를 만들수 밖에 없는 상황이 생기는데 그렇게 되면 myClass의 전반부는 10개의 getter(), setter()함수가 지저분하게 널려있게된다.

코틀린에서는 이를 좀더 깔끔하게 표현할수 있도록 getter()와 setter()를 자동으로 생성해준다.

위의 자바 코드를 코틀린으로 바꾼다면 다음과 같다.

class myClass(var myAge:Int) {
}

constructor에서 선언하면서 myAge값을 대입하고 myAge의 getter 와 setter가 생성되고, 외부에서 접근가능하다.

굳이 외부에서 접근할 필요가 없는 멤버변수라면 아래와 같이 선언한다.

class myClass{
   var myAge:Int 
}

물론 이때도 getter(), setter()함수가 자동생성된다.

멤버변수를 가져올때(get)나 설정할때(set) 추가적인 프로그램 처리가 필요할경우 따로 지정할수 도있다.

class myClass(var myAge:Int){
   var baseYear: Int = 2018
   var myBirthYear:Int
         get() {
              return baseYear -  myAge
         } 
         set(currentYear: Int) {
             if (birthYear > 0 ) {
                 myAge = birthYear +  myAge
             }
         } 
}
// 자기자신의 값도 field 속성으로 가능하다.
var publicVar: String = "publicVar value" 
get() = field 
set(value) { field = "$value Changed" }  
  


fun main(args: Array<String>) {
    val mc =     myClass(25)
    println(mc.myBirthYear)
    mc.myBirthYear = 1994
    println(mc.myAge)
}
  • 확장 프로퍼티 (Extension Property)
    kotlin에서는 프로퍼티를 확장할수 있도록 확장 프로퍼티라는 기능을 제공한다. String, Int, ArrayList, 자신이 만든 클래스 등 모든 타입에 대해 확장 프로퍼티를 추가하는 것이 가능하며 확장하는주체(리시버) 에 정의되어 있는 프로퍼티도 읽고 쓸수 있다.

// 1.확장함수의 스코프가 전역범위에 지정되어있다면 프로그램전체에 영향을 미친다..
data class Money(var exchangeMoney: Int = 0) {}  
var Money.atJapan: Float  
    get() {  
        return exchangeMoney * 0.1f  
  }  
    set(japanMoney) {  
        this.exchangeMoney = (japanMoney * 10f).toInt()  
    }
/*
  
val moneyExchange = Money(1000)  
Log.e("atjapan", moneyExchange.atJapan.toString())  
moneyExchange.atJapan = 500f  
Log.e("atjkorea", moneyExchange.exchangedMoney.toString())
*/
    
// 2.확장프로퍼티의 스코프가 현재 클래스내에서 지정되었다면 현재 클래스로 제한한다.
class MainActivity : AppCompatActivity() {
    //Primitive 객체에 확장프로퍼티를 추가 
	val Float.dp: Int  
	    get() {  
	        return TypedValue.applyDimension(  
	            TypedValue.COMPLEX_UNIT_DIP,  // DP 일떄  
	  this,               // value * metrics.density 화면밀도곱해서  
	  resources.displayMetrics // metrics.density  
	  ).toInt()  
	    }  
	val Int.dp get() = toFloat().dp.toInt()
}

확장프로퍼티를 java 로 DeCompile해본다면

//1. 확장프로퍼티의 스코프가 전역범위에 지정되어있다면 프로그램전체에 영향을 미친다.
public static final float getAtJapan(@NotNull Money $this$atJapan) {  
  Intrinsics.checkNotNullParameter($this$atJapan, "$this$atJapan");  
  return (float)$this$atJapan.getExchangedMoney() * 0.1F;  
}  
 
public static final void setAtJapan(@NotNull Money $this$atJapan, float japanMoney) {  
  Intrinsics.checkNotNullParameter($this$atJapan, "$this$atJapan");  
  $this$atJapan.setExchangedMoney((int)(japanMoney * 10.0F));  
}
// 2.확장프로퍼티의 스코프가 현재 클래스내에서 지정되었다면 현재 클래스로 제한한다.
 
public final class MainActivity extends AppCompatActivity {
    public final int getDp(float $this$dp) {  
      Resources var10002 = this.getResources();  
      Intrinsics.checkNotNullExpressionValue(var10002, "resources");  
      return (int)TypedValue.applyDimension(1, $this$dp, var10002.getDisplayMetrics());  
   }  
     
   public final int getDp(int $this$dp) {  
      return this.getDp((float)$this$dp);  
   }
}

1 번의 경우 static 으로 전역 범위로 선언되어져서 어디서든지 Myney.atJapan 프로퍼티를 사용할수 있다.
2 번의 경우 Primitive 객체인 Int(숫자형) 에 확장선언했지만 클래스내에서만 사용할수 있다.

즉, 아래처럼 확장프로퍼티를 어디에서 정의하느냐에 따라서 스코프의 영향을 받는다

java 로 변환된 코드를 보면 알수 있겠지만 확장프로퍼티는 자바의 final 함수로 선언되며, 확장의 주체가 되는 객체는 “$리시버” 라는 형태로 인자를 받아서 리시버의 속성을 다시 이용해서 값을 읽거나 추가하고 있다는 것을 볼수 있다.
따라서 확장의주체, 즉 리시버 객체의 private 에는 접근할수없기 때문에 확장프로퍼티를 통해 프로퍼티를 수정하고자할때는 주의가 필요하다.

3.4.3 접근제한자

코틀린 에서도 접근 제한자를 통해 멤버변수와 함수의 접근을 제어할수 있다.

한 프로젝트를 여러사람이 같이 개발하다보면, 다른 모듈에서 멤버변수나 함수에 대해 변경할수 없게 하거나, 볼수 없게 하는 것이 필요한데 이때 접근제한자를 변수나 함수의 앞에 지정하면된다.

접근제한자는 총4개이며 다음과 같다

  • private : 클래스 내부에서만 사용되도록 한다. 외부에서는 절대 읽어낼수 없다.
  • public : 클래스객체를 통해 외부에서 모두 접근이 가능하다. 접근자를 별도로 선언안하면 이 접근자로 지정된다.
  • protected : 동일한 클래스의 내부에서만 사용하되, 종속된 하위 클래스에서도 사용할수있다.
  • internal : 같은 모듈 내부에서만 가능한다. AndroidStudio(IntellijIDEA 에디터) 에서는 프로젝트 하나를 하나의 모듈이라고 한다.

선언 및 사용은다음과 같다.

open class ParentClass{
   public var myname: String = "코틀린"
   private var myAge:Int = 9
   protected var myParent:String  = "JetBrain"
}

class ChildClass : ParentClass(){
  public var forMyParent: String = "나를 만든건 $myParent" //myParent 멤버변수는 하위클래스에서도 접근할수 있다.
  public var forMyParentAge: String = "내 나이는 $myAge" // 에러발생 ParentClass의 myAge 변수는 하위 클래스에서 접근 할수 없다.
}

fun main(args: Array<String>) {
   var kotParent = ParentClass()  
   println(kotParent.myname) 
   println(kotParent.myAge)  //에러 발생 Age 멤버변수는 외부에서 접근할수없다.
   println(kotParent.myParent)  //에러 발생 myParent멤버변수는 외부에서접근할수없다.

}

3.4.4 클래스 상속

앞서 클래스는 같은 그룹이나 멤버끼리 묶어놓은 하나의 객체라고 설명을 했었다. 그런데 프로그래밍을 하다보면 이미 생성된 클래스객체와 기본골격은 비슷하지만, 좀더 진화된 형태의 기능의 클랙스가 필요할때가 있다.

마치 부모의 특성을 물려받은 새로 태어난 강아지들 처럼, 강아지라는 기본 속성을 같지만, 태어난 녀셕 개개별로 무늬와 특징이 틀린것과 비교할수 있다.

강아지들은 부모개의 속성을 모두 상속받았지만, 고유의 무늬와 생김새,행동을 가지게 된다. 그래서 강아지들은 별도의 객체로 인식되지 부모개에서 떨어져나온 하나의 장기정도로 생각하는 사람은 없다.

상속에서도 마찬가지로, 부모클래스를 통해 파생된 자식클래스는 부모클래스의 속성과 특징을 모두 가짐과 동시에 자신만의 속성과 특징을 가지게 되는데 , 이렇게 자식클래스를 선언해서 좀더 다양한 객체를 생성할수 있도록 하는게 상속이다.

클래스를 상속하게 되면, 프로그래머는 상속에 따른 클래스 멤버변수와 함수에 대한 속성과 접근범위에 더욱 신경써야 한다.

그도 그럴것이 부모클래스의 특징을 모두 물려받았다고해서, 부모의 특징이 모두 나타나는것이 아니기 때문이다. 모두 나타나야 한다면 그건 복제이지 상속이 아니다.

때문에 자식클랙스는 부모클래스의 특징중에서 사용해야 될만한(진화할만한) 특징을 그대로 물려받거나, 또는 자식클랙스만의 특징으로 덮어씌우는(오버라이딩) 과정이 필요하다.

부모 클래스입장에서도 굳이 나쁜 것(유전적인 병이나 습관) 등은 자식클래스가 물려받지 않도록 할 필요가 있다. 이는 앞에서 배운 접근자중 ,private 것을 통해서 가능하다.

클래스의상속은 다음과 같이 한다.

open class 부모클래스명 (부모클래스의 컨스트럭터) {

}

class 자식클래스명(자식클래스의 컨스트럭터) : 부모클래스명 (부모클래스의 컨스트럭터) {

}

부모클래스는 class를 선언할때 상속을 해야할 자식 클래스를 설계했다면 open 이라는 키워드를 사용해야 한다. 이는 코틀린만의 특징인데, open 키워드가 붙어 있는것은 해당 클래스가 상속에 관해 열려있어서 어디선가 상속을 하고 있다라는것을 쉽게 파악할수 있다.

자식클래스는 class 선언후 : 기호뒤에 부모클래스명을 적어주면 된다. 추가로 자식클래스를 인스턴스로 생성시에 부모클래스의 어떤 컬스트럽터가 실행될지 적어주면된다.

open class Animal(val name: String) {
     var where:String = "지구"
     fun move( legs:Int) = "$where 동물 $name$legs 발로 움직인다."
}

class Dog(name: String): Animal(name) {
     fun sound() = "멍멍"

}

fun main(args: Array<String>) {
    val earthAnimal:Animal = Animal("인간")
    println(earthAnimal.move(2))
    
    
    val dog:Dog = Dog("개")
    println(dog.move(4))
    println("소리는 ${dog.sound()}")
}

/*결과
지구 동물 인간 은 2 발로 움직인다.
지구 동물 개 은 4 발로 움직인다.
소리는 멍멍
*/

dog를 선언시에 개 라는 이름을 주었는데, 상속할때의 부모클래스(animal)의 생성자를 실행시키기 때문에 Animal 클래스의 name 변수에 개가 대입되었다.

-오버라이드

다형성이라고도 하는 오버라이드는 부모클래스에 있는 멤버를 자식클래스에서 덮어씌워(override) 새로운 기능을 하도록 하는 것을 말한다.

즉 같은 부모개 한테서 물려받은 기능이라도 자식개 들은 조금씩 다른 경향을 가지게 된다. 이렇게 공통되지만 다른특성을 가질때 해당 기능만 오버라이드 함으로서 객체별로 특징을 정할수 있다.

선언은 부모 클래스에서 원래의 함수에 open키워드를 붙이고 ,자식클래스에서 같은 이름의 함수에 overrid 키워드를 붙이면 된다.

부모클래스에서 오버라이드 될 함수
open fun 함수명(인자:인자형식): 리턴값

자식클래스에서 오버라이드 할 함수
overrid fun 함수명(인자:인자형식): 리턴값

위의 예제중에 Dog 클래스에 sound 함수가 있다. 사실 모든 동물은 소리를 내는 것이기 때문에 부모클래스에서 sound함수를 선언해 주는게 나중에 다른 동물을 추가할때도 좋다.

이때, 부모클래스의 sound함수를 자식클래스에서 오버라이드 해서 동물 각자의 소리를 표현하면된다.

open class Animal(val name: String) {
     var where:String = "지구"
     fun move( legs:Int) = "$where 동물 $name$legs 발로 움직인다."
     open fun sound() = "워워워"

}

class Dog(name: String): Animal(name) {
     override fun sound() = "멍멍"
}

class Cat(name: String): Animal(name) {
     override fun sound() = "냐옹"
}

class Person(name: String): Animal(name) {
}

fun main(args: Array<String>) {
    val earthAnimal:Animal = Animal("모든")
    println(earthAnimal.move(0))
    println("소리는 ${earthAnimal.sound()}")
    
    val dog:Dog = Dog("개")
    println(dog.move(4))
    println("소리는 ${dog.sound()}")
        
    val cat:Cat = Cat("고양이")
    println(cat.move(4))
    println("소리는 ${cat.sound()}")
    
    val person:Person = Person("인간")
    println(person.move(2))
    println("소리는 ${person.sound()}")
}

/*결과
지구 동물 모든 은 0 발로 움직인다.
소리는 워워워
지구 동물 개 은 4 발로 움직인다.
소리는 멍멍
지구 동물 고양이 은 4 발로 움직인다.
소리는 냐옹
지구 동물 인간 은 2 발로 움직인다.
소리는 워워워
*/

Dog 클래스와 Cat 클래스 에서는 Animal 클래스를 계승받아 sound 함수를 override했기 때문에 각자의 소리가 출력됬다. 반면 Person 클래스는 sound 함수를 오버라이드 하지 않았기 때문에 부모 클래스의 sound 함수의 결과가 출력됬다.

3.4.5 추상클래스, 추상메쏘드

추상클래스라는 것은 어떤 집단의 공통된 부분을 추상화 시켜서 하위 클래스에서 반드시 구체적인 구현을 새로 정의하도록 하는 것이다. 마치 귀족이 되려면 반드시 지켜야 하는 추상적인 예절 같은거 라고 생각하면된다.

예를들어 위의 예제에서 animal 이라는 것은 실제로 세상에 “동물” 이라는 것이 존재하는것이니라, 동물의 특성을 가지는 것들이 존재하는것이다. 따라서 “동물” 이라는 개념을 추상화시켜고 동물의 특징을 가지는 것들에 대해 반드시 있어야 하는 기능을 각특성에 맞게 구현해야된다.

추상클래스와 추상메소드는 개념 적인 부분이기 때문에, 프로그램에서도 단지 어떤 특성을 가지게 될지 abstract 라는 지시어로 선언만 하면된다.

선언의 방법은 다음과 같다. abstract가 붙어 있는것은 하위 클래스에서 반드시 내용을 구현해야 되는것이다. 일반함수도 같이 기술할수 있따.

abstract class Animal(val name : String) {
    abstract fun see():String
    abstract fun eat():String
    abstract fun sound():String
    open fun move( legs:Int) = "$name$legs 발로 움직인다."
}

자 이제 모든 동물 들은 보고,먹고,소리내고,움직이는 것을 기본으로 해야한다. abstract 클래스로 만든 다는 의미는 이 클래스를 상속받는 자식클래스들은 모두 see(), eat(), sound(),move() 함수를 만들고 그 안에 각자의 행동을 적어야 한다는 강제성을 가진다.

추상클래스를 만들어서 같은 성격의 하위 클래스이 일관되게 기능을 구현하도록 유도함으로서 자칫 실수로 특정 동물의 기능을 빼먹는 다거나 하는 것을 줄일수 있도록 해준다.

animal클래스를 추상클래스로 구현해본다.

abstract class Animal(val name: String) {
      abstract fun see():String
      abstract fun eat():String
      abstract fun sound(what:String):String
      open fun move( legs:Int) = "$name$legs 발로 움직인다."
}

class Dog(name: String): Animal(name) {
    override fun see() = "$name 는 2개의 눈으로 본다."
    override fun eat() = "$name 는 아무거나 잘먹는다."
    override fun sound(what:String) = "$name 는 짖을 때는 $what 한다."
}

class Snake(name: String): Animal(name) {
    override fun see() = "$name 는 기분나쁜 눈으로 본다."
    override fun eat() = "$name 는 쥐를 먹는다."
    override fun sound(what:String) = "$name$what 소리를 낸다."
    override fun move(legs:Int) = "뱀은 다리가 $legs 이라서 기어다닌다."
}

fun main(args: Array<String>) {
    val dog:Dog = Dog("개")
    println(dog.see())
    println(dog.eat())
    println(dog.sound("멍멍"))
    println(dog.move(4))

    val snake:Snake = Snake("뱀")
    println(snake.see())
    println(snake.eat())
    println(snake.sound("쉭쉭"))
    println(snake.move(0))    
}

Dog, Snake클래스는 animal이라는 추상클래스를 상속받아서 추상메소드로 선언된 것들을 전부 override해서 구현해야만 한다.

하지만 Snake클래스에서는 뱀이 기어다닌다 라는 개별적인 특성이 있기때문에, 부모의 함수중 move함수를 오버라이드 해서 별도로 구현했다.

단,추상클래는 단독으로 선언할수 없다.

3.4.6 인터페이스

인터페이스는 기존의 클래스에 특정한 기능을 추가하기 위한 프로그램 기법이다.

abstract 는 같은 성질을 같는 클래스들에 대해 상속받는 클래스들이 동일한 구현을 해야 하는 강제사항이 있는반면, 인터페이스는 서로 다른 성질의 클래스들에게도 같은 구현을 할수 있도록 연관성을 가지게 하는 성격이 있다.

따라서 인터페이스는 부모-자식간의 상속 이라는 개념보다는 기존의 클래스에 끼워넣음 이라는 개념으로 프로그램한다.

앞의 예제에서 animal추상클래스에 날개가 있는 동물을 위해 fly라는 추상메소드를 추가한다면, 개나 고양이,쥐 같은 날개 없는 동물들도 어쩔수 없이 fly 라는 함수를 오버라이드 해서 구현해야만 한다. 이렇게 하다보면 수영하는 동물, 알을 낳는 동물 등 세계의 모든 동물에 대해 모든 특성을 abstract 클래스에 집어넣어 놓아야 되기 때문에 실제의 구현클래스(dog, cat, mouse,bird등) 에서 해당동물과 관련도 없는 메소드들을 오버라이드 해야만 한다.

interface 인터페이스명{
    fun 추상함수(인자:타입):리턴타입
}

이럴때 인터페이스를 통해, 특정 행동이 있는 경우에만 인터페이스의 추상메소드를 추가하여 해당 클래스가 내용을 구현할수 있도록 하면된다.

앞의 예제에 날 수 있는 동물들의 특징을 인터페이스로 추가하여 본다.

 abstract class Animal(val name: String) {
      abstract fun see():String
      abstract fun eat():String
      open fun move( legs:Int) = "$name$legs 발로 움직인다."

}

interface ICanNoise {
    fun sound(what:String):String
}

interface ICanFly {
    fun fly():String
}

interface ICanSwiming {
    fun swiming():String
}

class Dog(name: String): Animal(name), ICanNoise {
    override fun see() = "$name 는 2개의 눈으로 본다."
    override fun eat() = "$name 는 아무거나 잘먹는다."
    override fun sound(what:String) = "$name 는 짖을 때는 $what 한다."
}

class Bird(name: String): Animal(name), ICanNoise, ICanFly {
    override fun see() = "$name 는 2개의 눈으로 본다."
    override fun eat() = "$name 는 아무거나 잘먹는다."
    override fun sound(what:String) = "$name 는 짖을 때는 $what 한다."
    override fun fly() = "$name 는 날라 다녀요."
}

class Nalchi(name: String): Animal(name), ICanFly, ICanSwiming {
    override fun see() = "$name 는 2개의 눈으로 본다."
    override fun eat() = "$name 는 다른 물고기를 먹는다."
    override fun fly() = "$name 는 가끔 물위를 날기도 한다."
    override fun swiming() = "$name 는 물고기라 수영은 기본이다."
}



fun main(args: Array<String>) {
    val dog:Dog = Dog("개")
    println(dog.see())
    println(dog.eat())
    println(dog.sound("멍멍"))
    println(dog.move(4))

    val bird:Bird = Bird("새")
    println(bird.see())
    println(bird.eat())
    println(bird.sound("꽥꽥"))
    println(bird.move(2))
    println(bird.fly())

    val nalchi:Nalchi = Nalchi("날치")
    println(nalchi.see())
    println(nalchi.eat())
    println(nalchi.fly())
    println(nalchi.swiming())
    
}

기존의 Animal 클래스에 있던 sound 추상함수는 Nalchi물고기의 출현으로 인터페이스로 옮기게 되었다. 따라서 이제 소리를 내는 동물들은 추가로 ICanNoise 인터페이스를 부여받아서 구현해야 된다.

Bird클래스는 ICanFly 인터페이스를 부여 받았기 때문에, fly라는 능력이 생겼다.

Nalchi클래스는 물고기인데도 날수도 있고, 수영도 가능하지만, 소리를 내거나 하지는 않는다. 따라서 코드를 보면 ICanFly , ICanSwimming 두개의 인터페이스만 부여받아서 두가지의 능력을 추가로 가지게 되었다.

이렇게 부모클래스의 함수->추상클래스의 추상함수->인터페이스 추상함수로 점점 더 추상화 될수록 프로그램의 유연성은 좋아지게 된다.

3.4.7 companion object

코틀린에서는 클래스 객체의 생성없이 클래스함수나 프로퍼티를 참조하기 위해서 companion object 이라는 키워드를 사용한다. java 의 static 과 같은 기능을 한다.

class Util {
  companion object {
    fun doSomething(){
    ...
    }
  }
}
//호출은
Util.doSomething()

3.4.8 enum class

열거형 상수를 선언하는 일반적인 방법으로 enum class 를 사용한다. 열거형 상수란 1,2,3,4… 등과 같이 열거된 상수를 말하는데, 프로그램에서 이렇게 숫자등으로 상수를 쓰게되면 소스코드가 지저분해 지기 때문에 문자로 대신 표현하는데 이때 enum class 안에 상수대신 쓰일 문자를 지정하면된다.

enum class Direction { 
  NORTH, SOUTH, WEST, EAST
}

enum class Color(val rgb: Int) { 
  RED(0xFF0000),
  GREEN(0x00FF00),
  BLUE(0x0000FF) 
}

enum class ProtocolState { 
  WAITING {
    override fun signal() = TALKING 
  },
  TALKING {
    override fun signal() = WAITING
  };
  
  abstract fun signal(): ProtocolState 
}
public enum DayOfWeek {
  MONDAY(1),
  TUESDAY(2),
  WEDNESDAY(3),
  THURSDAY(4),
  FRIDAY(5),
  SATURDAY(6),
  SUNDAY(7);
  private final int dayNumber;
  
  private DayOfWeek(int dayNumber) {
    this.dayNumber = dayNumber;
  }
  public int getDayNumber() {
    return dayNumber;
  }
}
  
println(Direction.NORTH)

3.4.8 data class

클래스 객체를 이용해서 프로그램을 하다 보면, 같은 성질의 값들만 따로 묶어놓은 클래스가 필요할떄가 있다.예를 들어 데이터베이스로부터 읽어들인 유저의 개인정보를 별도의 클래스에 담아서 , List라는 콜력센에 넣고 이를 한번에 화면에 출력하거나 수정하는 작업을 하는 경우가 빈번히 발생한다.

java 에서라면 클래스를 생성한뒤 멤버변수를 선언하고 getter,setter등을 지정하는 번거로운 작업을 해야만했다.코틀린에서는 data class라는 별도의 클래스선언을 제공하여, 데이터만 관리하는 클래스를 쉽게 생성할수 있도록한다.

선언은 일반클래스랑 크게 다를게 없다.

data class 클래스명(인자1:타입 ,인자2:타입)

이렇게 선언만 해두면 코틀린에서는

  • equals:값의 동등비교 함수
  • hashCode: 값을 hash 라는 과정을 통해 빠르게 검색하고, 자료의 공간도 효율적으로 사용하게하는과정
  • toString: 값을 문자열로 보여주는 함수
  • copy: 값의 복사

등의 함수를 자동으로 생성해준다.

equals 는 저장된 서로다른 인스턴스 객체의 값이 같은지 다른지 확인하는 용도로 == 기호로 비교할수 잇다.

toString은 입력된 값을 콘솔등에서 확인할때 유용하다.

3.4.9 sealed class

class 선언앞에 sealed 라는 키워드를 붙이면 상속에 대해 제한된 클래스 그룹을 만들수 있다.

seald class는 같은 코틀린 작업파일 내에서만 상속이 가능하고 멤버가 모두 private 로 선언도니다.

sealed class는 enum class 의 확장형태로 enum class 보다 좀더 다양한 상태(state)값을 가지는 프로퍼티를 생성할수있다.

또한 objet 와 다른 클래스 도 계승할수 있다.

일단 sealed 처리된 클래스는 when 을 사용할때 sealed 를 상속받은 객체에 대한 조건을 모두 만족해야한다.

예를 들어 abstract 나 interface 에 대해 when 처리를 할경우에 abstract 나 interface 에 새로운 함수가 추가되어도 , when 에서는 알수가 없다. 따라서 보통은 when 의 else 절을 이용해서 에러처리를 하게 되는데, 이는 실행시 에러가 되어버려 프로그램이 멈추는 결과가된다.

이를 컴파일 단게에서 미리 방지 하기 위한 방법으로 sealed 클래스를 이용한다.

sealed 클래스를 상속받은 클래스가 추가되거나 ,변경될경우 when 에서 적절한 처리를 해 주지 않으면 컴파일 에러가 되기 때문에, 개발자가 사전에 상황을 파악하고 대응할수 있도록 해준다.

fun main(args: Array<String>) {
    print(eval(Expr.Sum(Expr.Const(12.44), Expr.Const(12.33))))
}

sealed class Expr {
    class Const(val number: Double) : Expr()
    class Sum(val e1: Expr, val e2: Expr) : Expr()
    class Const2(val number: Double) : Expr()
    object NotANumber : Expr()
}

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // seald 클래스를 이용하면 else가 없어도 되고, sealed class에 있지만 정의 안된 것이 있으면 미리 에러표시를 해준다.
}

3.5 코틀린의 예외처리

예외처리라는 것은 프로그램에서 오류가 발생되어 프로그램이 멈추거나 잘못된 데이터를 전달하는 것을 막기 위해 프로그래머가 임의적으로 미리 처리를 해두는 것을 말한다.

예를 들어 숫자가 들어가야할 변수에 문자를 넣거나, 빈값이 들어있는 변수를 가지고 뭔가를 처리하려고 할때 오류가 발생하는데, 이를 프로그래머가 미리 예상하고 오류가 발생했을때를 가정하여 그에 따른 보완책을 세우는 것이다.

3.5.1기본적인 try.catch.finally 의 처리

현재의 거의 모든 프로그램언어에서는 try.catch 라는 구문으로 예외처리를 하고 있다.
코틀린에서도 기본적으로 try.catch 라는 구문으로 예외처리를 할수 있는데, 추가로 finally라는 키워드를 지정하기도 한다.

//  코틀린에서의 try.catch.finally
try {
  //뭔가 잘못된 코드를 작성
} catch (e : Exception) {
  //try 부분에서 잘못된 코드에가 발견되견 이곳에서 에러를 잡아서 적절한 처리를 한다.
  println(e.message)

} finally {
  //에러가 있든 없든 무조건 실행되야 되는 처리가 있다면 이곳에 작성한다.
  //예를 들어, 열었던 파일을 무조건 닫아줘야 할때, 데이터베이스의 접속 해제 등
}

위의 코드는 현재의 코드블럭 안에서 예외처리를 하는것이기 때문에, 예외가 발생한 상위단게에서는 예외가 발생한지 모르는 경우가 있다.

이를 위해서 현시점에서 발생한 예외를 프로그램의 상위단계에 에러가 발생했다는 것을 알려주어야 하는데 이는 throw 라는 명령어를 이용한다.

fun main(args: Array<String>) {
    var yourName: String? ="코틀린"
    myException(yourName)
    
    yourName = null
    try {
        myException(yourName)
    }catch(e: Exception) {
        println(e.message)
    } finally {
        println("finally Cachted")
    }
}

fun myException(argName: String?){
    if (argName === null){
        throw Exception("에러발생: null 이란 이름은 없어요")
    }
    println("당신의 이름은 ${argName} 이군요")
}

위의 코드를 설명하자면, myException에 null이라는 값을 전달하면 throw Exception을 발생시켜서 myException을 실행한 상위 단계에 에러를 던지게(throw)된다.

따라서 myException을 실행한 상위단계에서 전달받은 에러에 대해 안전하게 try.catch로 잡아서 적절한 처리를 하게 된다.

만일 위의 소스코드에서try.tach가 없다면 상위단계에서 에러에 대한 처리가 없기 때문에, 프로그램의 에러라고 판단하고 프로그램이 멈추게된다.

코틀린에서는 예외처리는 모두 Throwable의 하위에 있으며 모든 예외는 Message, stacktrace, cause 멤버를 가진다 . 또한 java 에서 처럼 컴파일 단계에서 반강제적으로 오류에 대한 체크를 해야하는 checked exception 검사를 하는 것은 없다…

추가로 코틀린 특유의 expression 형태로 코드를 작성할수 있다.

expression형태로 작성하면 try.catch 는 항상 리턴값을 돌려줘야 한다.

fun main(args: Array<String>) {
  
    var yourName: String? ="코틀린"
    yourName= null
    val result = try{  myException(yourName)  } catch (e : Exception) {  null  }
    
    println(result)
  
}

fun myException(argName: String?){
   
    if (argName === null){
        throw Exception("에러발생: null 이란 이름은 없어요")
    }
    println("당신의 이름은 ${argName} 이군요")
   
}

3.5.2 kotlin 만의 runCatching

try-catch-finally 는 코드가 길어지고 자칫 지저분해 질수 있다. 에러를 캐치해서 별다른 처리를 해줄 필요가 없는 경우라면 runCatching을 사용하면 코드가 깔끔해진다.

val result: Result<T> = runCatching { MyFunction() /* T 형식을 반환해야함.*/  }

Result는 지정된 타입 T값을 가지는 결과클래스로서 isSuccess와 isFailure 두개의 프로퍼티를 가지므로 실행함수가 성공을 했는지, 에러가 나서 실패를 했는지 알수 있도록 해준다

fun justEmpty(): String {
    return ""
}

val double = runCatching { justEmpty().toInt() }
        .recover { 3 } // recover가 있으면 무조건 성공시킨다.
        .onSuccess { println(it) }
        .onFailure { println(it) }

print(double.isSuccess)

빈문자는 Int형으로 변환이 불가능 하여 에러를 발생했지만, Recover를 추가하여 성공 시키도록 했다.만일 recover를 제거하면 실패로 된다.

runCatching은 Result형을 반환하므로 get하여 값을 가져오게 되는데, kotlin에서는 아래와 같이 4가지 반환형 함수를 제공한다.

  • getOrThrow : 발생한 예외를 그대로 돌려준다.
  • getOrNull : 에러가 발생했다면 Null 값을 반환한다.
  • getOrDefault : 에러가 발생했다면 Default로 적은 값을 반환한다.
  • getOrElse : 에러가 발생했다면 else블럭 {} 에 지정된 기능을 수행한다.

4.고급기능

4.1. 컬렉션 다루기

배열이 단순히 데이터를 일렬로 저장하는 것에 불과하다면, 컬렉션은 데이터객체를 묶어서 관리 및 처리를 쉽게 하도록 할수 있다는 점에서 기능과 사용처가 다르다.

또한 배열은 선언시에 배열의 저장공간 크기를 지정해야되지만, 컬렉션은 요소의 크기에 따라 자동으로 늘어났다가 줄어들기 때문에 좀더 유연하게 자료구조를 다룰수 있게해준다.,

코틀린에서는 Java 의 컬력션, 즉 List, Set, Map 을 그대로 사용한다. 다만 코틀린에서는 읽기전용 컬렉션 과 읽기/쓰기 가능 컬렉션으로 나뉜다.

readonly mutable
listOf mutableListOf, arrayListOf
setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
mapOf mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf

읽기 전용 컬렉션들은 코틀린에서만 있는 것으로, 초기선언후 데이터가 변경되지 말아야 할 목록에 대해 사용하면된다.

코틀린에서는 java의 컬렉션을 그대로 이용하고 있지만, 함수의 확장 이라는 코틀린 특유의 기법으로 좀더 다양하게 컬렉션 객체를 다룰수 있다.

4.1.1 List 콜렉션

목록(List) 컬렉션 은 배열과 차이가 있다면 순서를 가지고 있다는 것이다. 데이터객체가 순서값을 가지고 있기때문에, 데이터의 추가,끼워넣기,가져오기, 삭제하기 등의 작업이 배열을 이용할때 보다 훨씬 빠르고 쉽게 할수 있다.

List 컬렉션 함수는 다음과 같다.

  • ListOf : 읽기전용 List 컬렉션
  • mutableListOf : 읽기/쓰기 가능한 리스트 컬렉션
  • arrayListof :읽기/쓰기 가능한 리스트 컬렉션

4.1.1.1 ListOf

읽기 전용의 List 콜렉션을 생성해 주는 함수이다.일단 초기에 생성된 값을 나중에 바꿀수 없다.

val list = listOf(1, 2, 3, 4, 5)
//list[0] = 6 이라고 변경하려고 시도하면 에러가 출력된다.
//물론 리스트에 요소를 추가하거나 삭제등을 시도해도 에러가 출력된다.
for (i in 0 until list.size) {
    println(list[i])
}
//결과 : [1, 2,3,4,5]

4.1.1.1 mutableListOf

읽기/쓰기가 가능한 List 콜렉션을 생성해 주는 함수이다.

val list = mutableListOf(1, 2, 3, 4, 5)
list[0] = 6 //  첫번째 요소의 값이 1 에서 6 으로 바뀐다.
list.add(7) // 리스트의 맨 마지막에 값이 7 인 요소를 추가한다.
for (i in 0 until list.size) {
    println(list[i])
}
//결과 : [6, 2,3,4,5,7]

4.1.1.2 arrayListOf

읽기/쓰기가 가능한 List 콜렉션을 생성해 주는 함수이다.mutableListOf와 동일하게 ArrayList를 만들어서 리턴하지만, 코틀린에서 제공하는 다양한 확장함수들을 이용하고자 한다면 mutableListOf를 사용하는것이좋다.

val list = arrayListOf(1, 2, 3, 4, 5)
list[0] = 6 //  첫번째 요소의 값이 1 에서 6 으로 바뀐다.
list.add(7) // 리스트의 맨 마지막에 값이 7 인 요소를 추가한다.
for (i in 0 until list.size) {
    println(list[i])
}
//결과 : [6, 2,3,4,5,7]

4.1.2 Set 콜렉션

Set콜렉션은 중복된 데이터를 허용하지 않는 콜렉션이다. 중복된 데이터를 허용하지 않기 때문에 주민등록번호, 회원 ID 등의 정보를 저장할 때 유용하다.

List나 Map 컬레션은 중복된 데이터를 삽입하는게 가능하지만 Set 컬렉션은 이를 하용하지 않기 때문에, 데이터를 삽입전에 중복데이터가 있는지 검사할 필요가없다.

단, List 컬렉션 처럼 순서대로 저장하는게 아니기 때문에, 순차적인 데이터를 자주 가져와야 하는 작업에는 부적합하다.

SetOf 컬렉션 함수는 다음과 같다.

  • setOf :읽기 전용의 컬렉션
  • mutableSetOf : 읽기/쓰기 가능한 컬력센
  • hashSetOf: 읽기/쓰기가 가능한 컬렉션, hash Code 를 키값으로 이용하여 데이터 수집이 빠르다 ,
  • linkedSetOf; 읽기/쓰기가 가능한 컬렉션, 입력된 순서대로 데이터를 저장하기 때문에 데이터 수직이 순서를 보장한다.
  • sortedSetOf; 읽기/쓰기가 가능한 컬렉션, 데이터를 추가할때 비교가능한 객체의 데이터를 기반으로 객체의 순서를 지정하여 저장한다. 따라서 객체를 탐색시 객체간의 순서에 준하여 객체를 수집할수 있다.

4.1.2.1 SetOf

읽기 전용의 Set 콜렉션을 생성해 주는 함수이다.일단 초기에 생성된 값을 나중에 바꿀수 없다.

val setof = setOf(1, 2, 1, 4, 5, 3, 4)
//set에 요소를 추가하거나 삭제등을 시도해도 에러가 출력된다.
for (item in setof) {
    println(item)
}
//결과 : 12453
//중복되었던 데이터 1과 4가 하나만 남게되었다.

4.1.2.2 mutableSetOf

읽기/쓰기가 가능한 Set 콜렉션을 생성해 주는 함수이다. 단 요소를 추가할때 컬렉션에 이미 있는 요소라면 추가되지 않고 무시된다.

val setof = mutableSetOf(1, 2, 1, 4, 5, 3, 4)
setof.add(7); // 값이 7 인 요소를 추가한다.
setof.add(5); // 값이 7 인 요소를 추가하지만 중복데이터라 추가되지않는다.

for (item in setof) {
    println(item)
}
//결과 : 124537
//중복되었던 데이터 1과 4가 하나만 남게되었다.

4.1.2.3 hashSetOf

읽기/쓰기가 가능한 Set 콜렉션을 생성해 주는 함수이다. 내부적으로 hash code 를 이용해 데이터를 수집시에 조금 더 빠르게 불러내는게 가능하다.

val setof = hashSetOf(2, 21, 1, 4, 5, 3, 4) 
setof.add(7); // 값이 7 인 요소를 추가한다.
setof.add(5); // 값이 5 인 요소를 추가하지만 중복데이터라 추가되지않는다.

for (item in setof) {
    println(item)
}
//결과 : 123421567

//중복되었던 데이터 1과 4가 하나만 남게되었다.

4.1.2.4 linkedSetOf

읽기/쓰기가 가능한 Set 콜렉션을 생성해 주는 함수이다. 내부적으로 hash code 를 이용해 데이터를 수집시에 조금 더 빠르게 불러내는게 가능하다.

val setof = hashSetOf(2, 21, 1, 4, 5, 3, 4) 
setof.add(7); // 값이 7 인 요소를 추가한다.
setof.add(5); // 값이 5 인 요소를 추가하지만 중복데이터라 추가되지않는다.

for (item in setof) {
    println(item)
}
//결과 : 123421567

//중복되었던 데이터 1과 4가 하나만 남게되었다.

4.1.2.4 SortedSetOf

읽기/쓰기가 가능한 Set 콜렉션을 생성해 주는 함수이다. 객체를 저장할때 객체의 값이 비교가능하다면 순서에 맞게 정렬(sorted) 한다음 저장한다. 순서를 정하는 과정은 자동으로 지정되며, 임의의 순서에 입력가능하지는 않다.

데이터를 수집시에 정렬된 상태로 수집할수 있다.

val setof = hashSetOf(2, 21, 1, 4, 5, 3, 4) 
setof.add(7); // 값이 7 인 요소를 추가한다.
setof.add(5); // 값이 5 인 요소를 추가하지만 중복데이터라 추가되지않는다.

for (item in setof) {
    println(item)
}
//결과 : 123421567

//중복되었던 데이터 1과 4가 하나만 남게되었다.

4.1.3 Map 콜렉션

Map콜렉션은 키 와 값으로 된 한쌍을 객체로 저장하는 특별한 컬렉션이다.

key-value방식으로 저장하기 때문에, 요소의 삽입과 검색이 빠르지만, 요소의 순서를 보관하지는 않는다.

map 컬렉션에서의 키값을 중복이 되지 않는 유일한 값이며, 밸류값은 중복되어도 상관이없다.

mapOf 컬렉션 함수는 다음과 같다.

  • mapOf: 읽기만 가능한 컬렉션
  • mutableMapOf: 읽기/쓰기가 가능한 컬렉션
    vhashMapOf: 읽기/쓰기가 가능한 컬렉션, hashing(키를 스트링이 아닌 hashing 된 정수형태의 값으로 검색) 이라는 검색방법을 사용하기 때문에 많은 양의 데이터를 검색할때 유용하다.
  • linkedMapOf; 읽기/쓰기가 가능한 컬렉션, 입력된 순서대로 데이터를 저장하기 때문에 데이터 수직이 순서를 보장한다.
  • sortedMapOf; 읽기/쓰기가 가능한 컬렉션, 데이터를 추가할때 비교가능한 객체의 데이터를 기반으로 객체의 순서를 지정하여 저장한다. 따라서 객체를 탐색시 객체간의 순서에 준하여 객체를 수집할수 있다.

4.1.2.1 mapOf

읽기 전용의 Set 콜렉션을 생성해 주는 함수이다.일단 초기에 생성된 값을 나중에 바꿀수 없다.

val mapof = mapOf("key1" to "value1", "key2" to "value2")
   
for (item in mapof) {
    println(item)
}
  
println(mapof.get("key2"))  //키값이 key2 인 요소의 값을 얻는다

//결과 : 
key1=value1
key2=value2
value2

4.1.2.2 mutableMapOf

읽기/쓰기가 가능한 Map콜렉션을 생성해 주는 함수이다. 단 요소를 추가할때 컬렉션에 이미 있는 요소라면 추가되지 않고 무시된다.

val mapof = mutableMapOf("key1" to "value1", "key2" to "value2")
mapof.put("key1", "value10") //key1 의 요소의 값을 바꾼다.
mapof.put("key3", "value3")     //key3요소를 추가한다.
mapof.put("key4", "value2")    //key4요소의 값을 key2 의 값과 같은 값으로 추가한다.

for (item in mapof) {
    println(item)
}
  
println(mapof.get("key2")) 

//결과 : 124537
//중복된 키의 key1 의 값이 변경되었다.

4.1.2.3 hashMapOf

읽기/쓰기가 가능한 Map콜렉션을 생성해 주는 함수이다. 내부적으로 hash code 를 이용해 데이터를 수집시에 조금 더 빠르게 불러내는게 가능하다.

val mapof = mutableMapOf("key1" to "value1", "key2" to "value2")
mapof.put("key1", "value10") //key1 의 요소의 값을 바꾼다.
mapof.put("key3", "value3")     //key3요소를 추가한다.
mapof.put("key4", "value2")    //key4요소의 값을 key2 의 값과 같은 값으로 추가한다.

for (item in mapof) {
    println(item)
}
  
println(mapof.get("key2")) 

//결과 : 124537
//중복된 키의 key1 의 값이 변경되었다.

4.1.2.4 linkedMapOf

읽기/쓰기가 가능한 Map 콜렉션을 생성해 주는 함수이다. 요소의 추가시에 순서를 기억하고 있기 때문에, 순서대로 요소를 불러들일수 있다.

val mapof = linkedMapOf("key5" to "value5", "key2" to "value2")
mapof.put("key1", "value10") 
mapof.put("key3", "value3")     
mapof.put("key4", "value2")    
for (item in mapof) {
    println(item)
}
  
println(mapof.get("key2")) 

4.1.2.4 SortedMapOf

읽기/쓰기가 가능한 Set 콜렉션을 생성해 주는 함수이다. 객체를 저장할때 객체의 값이 비교가능하다면 순서에 맞게 정렬(sorted) 한다음 저장한다. 순서를 정하는 과정은 자동으로 지정되며, 임의의 순서에 입력가능하지는 않다.

데이터를 수집시에 정렬된 상태로 수집할수 있다.

val mapof = sortedMapOf("key5" to "value5", "key2" to "value2")
mapof.put("key1", "value10") 
mapof.put("key3", "value3")     
mapof.put("key4", "value2")    
for (item in mapof) {
    println(item)
}
  
println(mapof.get("key2"))  

//결과 : 123421567

//key값에 의해 순서대로 정렬되어 저장되기 때문에 , 출력시에도 순대로 출력된다..

4.1.4 유용한 확장 메서드

kotlin 에서는 콜렉션은 좀더 유연하게 다루도록 하기위해서 몇가지 확장메소드를 지원한다.

4.1.4.1 map

콜렉션의 전체요소를 순회하여 사용자가 각 요소에 대해 추가처리를 할수 있도록 해주고 List형태로 값을 되돌려 준다.

val cities = listOf("Seoul", "Pari", "Hongkong")

val aweSomeCities = cities.map {
   "Awesome $it!" //
}

4.1.4.2 filter

콜렉션의 전체요소를 순회하여 사용자가 지정한 조건에 맞는 요소들만 필더링해서 List형태로 값을 되돌려 준다.

val cities = listOf("Seoul", "Pari", "Hongkong")

val aweSomeCities = cities.filter {
   it.contains("kong")
}

map과 중첩해서 사용할수도 있다.

val aweSomeCities = cities.filter {
   it.contains("kong")
}.map {
   "Awesome $it!"
}

4.1.4.3 asSequence

콜렉션을 sequence로 만들어 주어 각 콜렵센 요소를 순차적으로 처리할수 있도록 해준다. filer 와 map 을 중복해서 사용하면 filter의 조건이 끝난다음 map이 실행되는데, asSequence로 하면 filter에서 조건이 맞는 요소1개를 만나는 순간 map에 해당 요소를 처리하도록 하고, 다시 filer의 다음요소로 가는 순으로 처리할수 있다.

val cities = listOf("Seoul", "Pari", "Hongkong")
var i = 0

val aweSomeCities = cities.asSequence().filter {
   i++i
   it.contains("o")
}.map {
   "Awesome $i $it!"
}

Awesome 1 Seoul! //asSequence가 없다면 filter를 3번 먼저 거치니 3이 된다.
Awesome 3 Hongkong!

4.2. 함수 다루기

코틀린은 완전한 함수형 프로그래밍 이 가능하도록 되어있다. 이는 단순히 함수를 이용하여 프로그래밍하는것이 아니라, 함수자체를 인자로 넘긴다는지, 기존 함수를 확장한다는지, 함수자체를 리턴값으로 돌려주는 것이 가능하다는 것이다.

코틀린에서는 함수형 프로그래밍의 장점인 고차함수,익명함수,람다,인라인함수,클로저,꼬리회귀, 제너릭 등의 모든 기능을 제공한다.

4.2.1 함수형프로그래밍

OOP개념이 등장하면서 ,java, C++등의 언어에서는 클래스 기반으로 프로그래밍 하는게 표준으로 자리잡았다.

그러나 OOP기반의 프로그래밍은 하나의 클래스에 여러개의 메소드들을 보관하고 있기 때문에, 구조가 복잡해지고, 구현과 보수의 측면에서 매우 번거로웠던게 사실이다.

반면 최근 유행이되고 있는 함수형 프로그래밍은 다음과 같은 장점을 가진다.

  • 코드가 간결해진다.
  • 부수효과가 없다.
  • 모듈성과 재사용성을 높인다.
  • 테스트하기가 쉽다.

4.2.1.1 함수 선언 간략히 하기 expression body

코틀린에서는 함수의 선언을 좀더 간략히 할수 잇는 문법을 제공한다.
expression body 라고 하는데, 함수의 구현부를 하나의 식 형태로 작성할수 있다.
표현식에서는 리턴형식이 생략되기도 하는데, 코틀린에서 컴파일시에 자동으로 형식을 추론해서 붙여주기 때문이다.

fun sum(a: Int, b: Int): Int {
  return a+b;
}//위의 함수는 아래처럼 간략하게 표현이 가능하다.
fun sum(a: Int, b: Int) = a + b

expression body 를 응용해서 아래와 같은 다양한 형식으로 함수를 간략화 시킬수 있다.

//when 을 이용해 expression body 를 작성한 예
fun typeCheck(type: String) = when(type){
    "string"-> "ABCD"
    "int" -> 10
    "float" -> 10f
    else -> {
        "Error"
    }
}

//제곱근 구하는 함수
fun powerOf(value: Double, power: Double = 2.0) = Math.pow(value, power)

//조건절을 통한 함수
fun isBlankString(arg: String) = if (arg.trim() == "") true else false

4.2.1.2 여러개의 함수 인자를 한번에 받기

함수에서 여러개의 인자를 받기 위해서는 필요한 수만큼 인자를 지정해 주어야한다.
코틀린 에서는 vararg 라는 지정어로 여러개의 인자를 ㅂ열의 형태로 받을 수 있다.

fun showStrings(suffix: String, vararg items: String) {
    for(item in items) {
        print(item + " ")
    }
    print(suffix)
    println()
}

fun somewhereElse() {
    showStrings("!", "Hello", "Kotlin", "World") x
}
// prints "Hello Kotlin World !"

showStrings 함수의 item 인자는 vararg(variation argument) 로 지정되어 있어서, 호출부에서 입력받은 여러개의 인자값을 하나의 items 배열에 쌓아두고, 함수내에서는 배열의 형태로 이용할수 있다.

4.2.1.3 함수안에서 내부 함수 선언하기

함수안에 내부함수를 선언해서 사용할수 있다. 내부에 선언된 함수는 절대로 공개되지 않으며 변수의 스코프는 상위 함수로 제한된다. 이러한 내부함수의 기능을 이용하여 크로져와 커링기법을 구현할수 있다.

fun showStrings2(suffix: String, vararg items: String): String {
    val sb = StringBuffer()
    fun appendToBuffer(item: String) {
        sb.append(item)
        sb.append(" ")
    }
    for(item in items) { appendToBuffer(item) }
    appendToBuffer(suffix)
    return sb.toString()
}

....

fun somewhereElse() {
    showStrings2("!", "Hello", "World") // returns "Hello World !"
}

4.2.1.3 재귀 꼬리 함수의 작성

프로그램의 재귀 를 하기 위해서 사용했던 전통적인 방식은 함수의 내부에서 자신의 함수를 호출하는 것으로 작성다. 이러한 작동 방식은 자칫 무한 루프로 빠지기 쉬운데 이를 방지하기 위해서 코틀린 에서는 재귀꼬리 라는 지시어를 통해 컴파일 시에 좀더 안전한 코드 방식으로 최적화 준다.

tailrec fun factorial(n: Int, run: Int = 1): Long {
    return if (n == 1) run.toLong() else factorial(n-1, run*n)
}

위의 코드에서는 factorial을 계속호출하여 호출된 함수의 수만큼 스택에 쌓여있을거라 예상되는데, tailrec 지시어를 붙여주면 코틀린에서 자동으로 루프(while)형태의 코드로 변환하여 준다.

단 tailrec 를 붙인다고 무조건 최적화 되는것은 아니고, 조건이 있는데 함수의 마지막 작업으로 함수를 호출해야 하며, 호출 에 추가적인 수식 코드가 있을때는 사용할수 없다.

4.2.1.4 확장함수

코틀린에서는 기존의 함수의 기능을 확장할수 있는 확장함수라는 기능을 제공한다.
기본적으로 코틀린의 최상위 객체는 Any이기 때문에 이곳에 추가하면 모든 객체에 확장 함수를 추가하는게 된다.
이는 마치 javascript 의 prototype 을 추가하는것처럼 기존의 함수에 간단히 기능을 추가하는 것과 같은 개념이다.

fun main(args: Array<String>) {
    println("Hello".getLonggerString("Hi"))
}

fun String.getLonggerString(x: String) : String {
    return if(this.length > x.length) this else x
}

 Hello

위처럼 코틀린의 기본 자료 형식에도 함수를 추가할수 있다.
물론 본인 작성한 함수에도 확장함수를 적용함수 있다.

  • 확장함함수를 변수에 대입하여 변수이름으로 호출할수 있다.
    javascript개발자라면 그다지 놀라지 않을수도 있겠지만 java 개발자라면 꽤나 신선하게 느낄수 있는 기능이다.
//전역 범위로 확장함수를 만든다.
fun String.payYo(money: Int) {  
    print("$this : $money")  
}
class Example() {
    val pay: String.(Int) -> Unit = String::payYo
    fun payAll() {
        // 아래의 4개가 모두 가능하다.
	    // 특정 함수기능인 이중클론 오퍼레이터를 이용하여 함수를 실행가능하다.
		(String::payYo)("김", 1000)
		// 변수에 할당된 확장함수를 이용  
		"이".pay(700)  
		// 변수를 함수처럼 사용
		pay("박", 1500)
		// 메소드를 강제로 실행시키는 invoke 를 이용하여 실행 
		pay.invoke("최", 100)
    }
}
  • 확장함함수는 리시버(호출객체)가 있어서 확장함수의 기능이 리시버에 한정되지만, 리시버에 의존하지 않는 확장함수도 만들수 있다.
abstract class globalPay {  
    abstract fun getRatio(money: Int):Float  
    abstract fun getName():String  
}  
  
class atChina : globalPay() {  
    override fun getRatio(money: Int):Float { return 0.7f}  
    override fun getName(): String { return "China" }  
}  
class atKorea : globalPay(){  
    override fun getRatio(money: Int):Float { return 1f}  
    override fun getName(): String { return "Korea" }  
}  
class atJapan : globalPay(){  
    override fun getRatio(money: Int):Float { return 0.9f}  
    override fun getName(): String { return "Japan" }  
}

val payWithGlobal: globalPay.(Int) -> Unit = ::payGlobal

atKorea().payWithGlobal(100)  
atChina().payWithGlobal(100)  
atJapan().payWithGlobal(100)
  • 확장함수의 스코프는 지역/전역을 나뉜다. 아래 예제처럼 특정 클래스내에서 선언한 확장함수는 해당 클래스범위로 스코프가 한정된다.
class Example {  
    fun String.method2(i: Int) {  
        ...  
    }  
    ...  
    "helllo".method2(1) // 가능    
}  
"helllo".method2(1) // String에 method2 가 지정되어 있지만 외부에서는 호출 불가능
  • 특정경우에는 확장함수보다 확장 프로퍼티로 더 간단하게 할수도 있다.
data class BannerItem(val id: Int, var isShow: Boolean)
val bannerItems = arrayListOf(
        BannerItem(1, true),
        BannerItem(2, false),
        BannerItem(3, true),
)
val ArrayList<BannerItem>.filterShowing
    get() = this.filter { it.isShow }

// 사용
bannerItems.filterShowing.forEach {  
  print("${it.id}")  
}  

4.2.1.5 infix 함수

infix지시어를 통해 확장된 중위 표현을 사용할 수 있다.

infix fun Int.multiply(x: Int): Int {
    return this * x
}

val multiply = 3 multiply 10

4.3. 람다식 프로그램

람다함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수(Anonymous functions)를 지칭하는 용어이다. 다소 복잡하지않은 처리과정을 별도의 함수나 메소드로 분리 하기 번거로울때 람다 함수를 이용하여 좀더 간결하게 코딩할수 있다.

함수형 프로그래밍을 지원하는 코틀린에서는 고차 함수에 인자(argument)로 전달되거나 고차 함수가 돌려주는 결과값으로 람다 함수를 자주 쓰인다.

특히나 컬섹션을 다룰 때, 컬렉션의 가 요소에 대한 처리를 람다함수로 처리하면 좀더 편리하다.

함다식이 사용이 간편한 대신 너무 많이 사용하면 코드의 해석이 어려워지고 , 디버깅시에 에러를 추척하는 콜스택을 확인하기 어려워 지기 때문에 가급적 필요한 곳에만 사용하는 것이 좋다.

다음과 같은 규칙이 있다.

  • 람다 함수도 함수이기 때문에 { }으로 감싸서 표현해야 한다.
  • { } 안에 -> 표시가 있으며 -> 왼쪽은 매개변수, 오른쪽은 함수 내용이다.
  • 매개변수 타입을 선언해야 하며 추론할 수 있을 때는 생략할 수 있다.
  • 함수의 반환값은 함수 내용의 마지막 표현식이다.
  • 바로실행함수로서 뒤에 () 를 붙일수 있다.
  • 람다에서 자기 자신은 it 이라고 표현한다.
 fun sum(x1: Int, x2: Int): Int {
   return x1 + x2
 }

val sum1 = { x1: Int, x2: Int -> x1+x2 }

//java 코드라면 이렇게 하는것을 
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        view.setAlpha(0.5f);
    }
});
//OnClickListener를 람다로 표현한다
button.setOnClickListener {
    view -> view.alpha = 0.5f
}

it의 경우는 함수의 변수가 한 개여야 만 허용된다.

button.setOnClickListener {
    it.alpha = 0.5f
}

4.4. 고차함수

1급 객체란?

아래 3 가지조건을 충족한다면 1급 객체라고 할수 있다.

  • 변수나 데이타에 할당 할 수 있어야 한다.
  • 객체의 인자로 넘길 수 있어야 한다.
  • 객체의 리턴값으로 리턴 할수 있어야 한다.

High-order-function이란 2가지 중 하나이상을 만족하는 함수를 말합니다.

  • 함수를 파라미터로 전달 받는 함수
  • 함수를 리턴하는 함수

고차 함수(High-Order Function, 고계 함수)란, 매개변수로 함수를 전달받거나 함수를 반환하는 함수를 말한다.

fun hoFun(x1: Int, argFun: (Int) -> Int){
  val result = argFun(10)
  println("x1 : $x1, someFun1 : $result")
 }

hoFun(10, {x -> x * x })

4.5. 특정키워드

4.5.1 as , as?

어떤 데이터타입형식의 변수를 호환성있는 다른 타입으로 강제로 바꾸고자 할때 as 카워드를 사용한다. as 키워드는 두가지 역활을 한다.

  • 타입캐스팅 : 객체의 형변화하는 역활을 한다. 이를 타입캐스팅이라고 하는데, 기존의 자료타입을 다른형식으로 강제적으로 변환하고 싶을 때 사용한다.
    as 로 타입캐스팅할때 주의할점은 변환할 것이 null 일 때는 에러가 발생하므로 ? 연산자를 붙여서 null값이 대입될수 있도록 한다.
val x: String = y as String     
val x2: String? = y2 as String? //y2 의 값이 없을때는 x2 는 null 값이된다.
val x2: String? = y2 as? String //또는 as에 ? 연산자를 붙여서 사용하기도한다..
  • 패키지 import 시의 alias : 패키지를 import 할때 같은 이름의 패키지가 잇을때, 이를 해결하기 위해서 as 키워드를 사용한다.
import Car.Engine
import MotorCycle.Engine as engine

4.5.2 is , !is

앞의 객체의 형식이 뒤의 객체형식으로 변환(캐스팅)이 가능한지를 판단해 주는 연산자이다. 형변환이 가능하다면 true 리턴하고, 가능하지않다면 false 를 리턴한다. as키워드는 강제로 형변환하는 과정을 하지만 is는 형변환에 대한 검사만 하기때문에, null값 에러가 발생하지않는다. !is 는 반대의 개념으로 앞의 객제의 형식과 뒤의 객체의 형식이 다를 때 true 가 된다. java 의 instanceOf와 같다.
is키워드로 검사한다음에는 아래예제처럼 해당 타입에서 사용가능한 메쏘드를 바로 사용할수 있다.

fun isTypeName(obj: Any) {  
    if (obj is Int) {  
        obj.toULong()  
        print("Type = Integer")  
    } else if (obj is Float) {  
        obj.plus(1f)  
        print("Type = Float")  
    } else if (obj is String) {  
        obj.length  
  print("Type = String")  
    }  
}

4.5.3 in 연산자

컬렉션 객체에 해당 값이 있는지(포함하고 있는지) 빠르게 판단해준다.

val marks = intArrayOf(1, 2, 3, 4, 5)  
var a = 3  
if (a in marks) {  
    println("$a 이 포함되어있음.")  
} else {  
    println("$a 은 없음.")  
}

4.5.4 typealias

typealias 는 객체의 이름대신에 별칭(alias)를 사용할수 있도록 해준다.어떤객체든지 이름이 너무 긴 클래스,함수 등을 좀더 짧은 이름으로 바꾸고 싶을때 사용한다.

class A {
    inner class Inner
}

class B {
    inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner

4.5.5 lateinit

초기화 지연 프로퍼티(Late-initialized property) 이라고 하며 클래스의 프로퍼티를 나중에 초기화 하기 위한 키워드 이다.

클래스를 선언할때, 프로퍼티는 반드시 어떤값(null포함)을 가지고 있어야한다. 그런데 클래스에서 모든 프로퍼티에 대한 초기값을 억지로 부여했다가 그 값에 의해 의도치 않는 결과가 될수도 있다.

lateinit 은 클래스의 프로퍼티값을 선언시에 일단은 초기값을 부여하지 않고, 나중에 필요할때만 값을 지정해서 사용할수 있도록 해준다.

lateinit 선언된 프로퍼티는 값을 지정하여 줄때 비로소 메모리에 공간이 할당되기 때문에 클래스의 인스턴스 생성단계에서 불필요하게 메모리가 낭비 되지 않고 실행시간도 빨라진다.

제약사항은 다음과 같다.

  • var(mutable) 프로퍼티만 사용 가능
  • non-null 프로퍼티만 사용 가능
  • 커스텀 getter/setter가 없는 프로퍼티만 사용 가능
  • primitive type 프로퍼티는 사용 불가능
  • 클래스 생성자에서 사용 불가능
  • 로컬 변수로 사용 불가능
lateinit var name:String

println(name) //에러 발생. 값을 할당하지 않았다(null).
name ="kim"
println(name) //kim

4.5.6 lazy

lateinit 은 나중에 언젠가는 초기화값을 부여 줄거라는 의미였다면, lazy 키워드는 프로퍼티를 호출하는 순간 {} 블럭안의 값을 초기화 시키는 키워드 이다.

단, lazy는 {} 블럭안의 내용을 실행하는 함수형 키워드이기 때문에, 만일 {} 블럭의 내용이 실행되는 중간에 동시에 프로퍼티에 접근한다면 동기화의 문제가 생긴다.

이를 해결하기 위해서 코틀린에서는 3가지의 옵션을 제공한다.

  • SYNCHRONIZED : 한 스레드만 값을 계산할 수 있으며 모든 스레드는 같은 값을 읽음.
  • PUBLICATION : 초기화 과정을 여러 스레드가 동시에 수행할 수 있으나 만약 다른 스레드에서 초기화하여 할당된 값이 있다면 그 값을 반환.
  • NONE : 별도의 동기화 처리가 없음.

제약사항은 다음과 같다.

  • val(immutable) 프로퍼티만 사용 가능
  • primitive type에도 사용 가능
  • 커스텀 getter/setter가 없는 프로퍼티만 사용 가능
  • Non-null, Nullable 둘 다 사용 가능
  • 클래스 생성자에서 사용 불가능
  • 로컬 변수에서 사용 가능

lazy키워드는 단어 그대로 , 객체에 대한 선언을 늦게 한다는 의미이다.코틀린 에서는 객체에 메모리를 할당하는 초기화단계를 늦출수 있도록 해서 프로그램의 실행시간을 조금이라도 빠르게 하고 메모리를 효율적으로 사용할수 있도록 했다.

lazy선언된 객체가 프로그램내에서 한번도 사용되지 않는다면 해당 객체에 필요한 메모리 공간도 절약하게 된다.

val name:String by lazy{
    //이곳에 변수가 호출되어 초기화될때 필요한 한번만 처리를함.
    print("이건 처음호출때에만 출력될껄")
    //마지막 라인이 변수에 할당될 값
    "kim"
}
 
println(str) // 메시지출력후 kim 값 할당
println(str) // kim

4.5.7 by (Delegate)

lazy키워드를 사용할때 by라는 것도 같이 사용하는데 이 by 키워드를 사용하면 디자인패턴의 Delegate패턴을 kotlin에서 자동으로 만들어 준다.

클린코드에서 상속보다는 구성을 하도록 권장하는데 인터페이스를 이용하면 상속을 하지 않고도 구성이 가능하다. 하지만 인터페이스는 정의된 모든 함수를 똑같이 구현하지 않으면 안되기 때문에 매번 불필요한 boilate코드가 작성할수 밖에 없다.

by 키워드를 이용하면 인터페이스에 대한 구현체를 대리자(delegate)로 지정하여 불필요하게 동일한 구현을 하지 않아도 된다.

interface IBox {
   fun getWidth() : Int
   fun getHeight() : Int
}

open class TransparentWindow() : IBox {
   override fun getWidth(): Int {
       return 100
   }

   override fun getHeight() : Int{
       return 150
   }
}

class UI(box: IBox) : IBox by box {
   //override fun getWidth() 와  override fun getHeight()를 하지않아도 된다.
}

by 키워드를 이용하면 인터페이스에 대한 구현체를 대리자(delegate)로 지정하여 불필요하게 동일한 구현을 하지 않아도 된다.

by 키워드와 Delegates.observable()를 이용하면 변수의 값의 변화를 감지해서 이전값과 새값을 이용하여 또다른 추가 처리등을 할수도 있다.

var alwaysPlus10 = 0
var max: Int by Delegates.observable(0) { property, oldValue, newValue ->
   println("$oldValue changed to $newValue")
   alwaysPlus10 = newValue + 10
}

max = 100
print(alwaysPlus10)        // 110

4.5.8 Reified

generic을 사용하되 함수내에서 제너릭타입에 대한 참조를 하고 싶을때 사용한다. <reified T> 형식으로 사용하고 inline 키워드를 같이 사용하여야 된다.

inline fun <reified T> function(argument: T)

일반적으로 generic 타입은 정해지지않았기 때문에 강제캐스팅을 하거나 체크를 해서 해당타입에 대한 적절한 처리를 하는데, reified와 inline을 붙여 함수를 정의하게되면 컴파일시에 입력값과 출력값 에대한 타입을 체크하여 generic함수를 생성하여 준다.

inline fun <reified T> function(argument: Any):T 

와 같이 정의했다면

val result : Int = function(10) 과
val result : String = function(“A”) 을 하나의 함수로도 정의가 가능하다.

4.6. 연산자 오버로드

Kotlin에서 기본으로 제공하는 산술 연산자 plus, minus 등을 +, -로 접근한다. 이러한 기법을 Kotlin에서는 Convention이라고 한다.

연산자 오버로드를 하는 이유는 자주 쓰는 복잡한 로직을 단순화 시켜서 프로그램을 좀더 깔끔히 하기우해서이다. 단 너무 남발하면 나중에 소스코드 분석이 어려워질수 있으니 적절하게 사용하는 것이 좋다.

대부분 산술 연산자는 오버로드가 가능하고 , List와 Map에 접근할 때 사용하는 []등에 도 가능하다

오버로딩 가능한 산술 연산자 오버로드시 사용할 함수이름.

Function code
plus a + b
minus a - b
div a / b
rem a % b
times a * b
not !a
unaryPlus +a
unaryMinus -a
inc ++a, a++
dec –a, a–

연산자를 Overloading 할 수 있는 방법은 아주 간단하다.
위의 산술 연산자 표 정의 function 중에 operator 키워드 만 추가하면 확장이 가능한데 아래와 같다.

operator fun Int.plus(b: Int) = this + b

산술 연산자 확장하기

data class LocationData(val a: Int, val b: Int) {

    operator fun plus(item: Position): LocationData {
        return LocationData(a + item.a, b + item.b)
    }
}


fun sumLocation() {
   val locationOne = LocationData(1, 2)
   val locationTwo = LocationData(3, 4)
   println(locationOne + locationTwo)
}
//  (a=4, b=6) 이 출력 된다.

4.7.Destructuring Declarations(분할)

kotlin의 특징 중 하나인 Destructuring Declarations이다. 즉, 어떤

val position = Position(10, 20)으로 선언하고, println(position.a), println(position.b)로 접근하는 게 아닌 아래와 같은 접근이 가능하다.

data class Person(val id:Long, val name:String, val age:Int)

val (userId, userName, userAge) = Person(14, "kim", 18)
println(userId)
println(userName)
println(userAge)

for 문에서도 사용가능하다.

for  ((a,  b)  in  collection)  {  ...  }

map에서는 key/value을 한 번에 아래와 같이 받아서 사용한다…

val map = mutableMapOf(0 to "A", 1 to "B", 2 to "C")
for ((key, value) in map) {
    println("$key to $value")
}

함수의 리턴값을 사용할수 있다.

fun getPersonInfo = Person(14, "kim", 18)
val (userId, userName, userAge) = getPersonInfo()

4.8. 어노테이션과 리플렉션

Kotlin에서 자주사용되는 어노테이션은 다음과 같다.

4.8.1 어노테이션

  • @JvmName
    kotlin코드가 java코드로 변환될때 일부 코드는 같은 이름거나 허용할수 없는 이름으로 변환하려고 시도 하기 때문에 컴파일에러가 날수 있다. 이때 java쪽에서 생성될 이름을 지정하여 에러를 방지할수 있다.
    사용예 @JvmName(“fooListInt”)

  • @JvmStatic
    companion 에 선언된 변수를 java에서 사용하려면 Foo.Companion.getMe() 처럼 사용해야 하는데, 이를 좀더 java의 static 변수처럼 사용하고자 할때 이것을 붙여주면 Foo.getMe()와 같이 java에서 사용할수 있게 해준다. static변수의 get,set 함수를 클래스에 만들어 준다.
    사용예 : @JvmStatic var me : String = 0

  • @JvmField
    클래스 변수는 java코드로 변환될때 getter/setter 함수를 속성으로 만들어 주는데, 이것을 붙여주면 getter/setter를 생성않는 단순 필드만 만들어 준다.
    사용예 : @JvmField var me : String = 0

  • @Throws
    코틀린에서는 Throws가 없기때문에 이 어노테이션을 이용하여 throw를 지정할수있다.
    사용예 : @Throws(Exception::class)

  • @JvmOverloads
    코틀린의 생성자 선언방식과 java로 변환하였을때의 생성자는 다르기 때문에, 여러개의 인자를 가진 overload 된 생성자를 지정하기 위해서 사용한다. 코틀린코드에서의 생성자의 인자가 3개이면 java쪽에는 각기다른 인자를 가진 생성자가 3개 생성된다.
    사용예 : @JvmOverloads constructor( var a: Int, var b: Int, var c: Int) { }

4.8.2 리플렉션

java 에서의 Reflection이란 말그대로 다른 소스나 앱에 포함된 클래스나 메소드,멤버변수등을 투영시켜서 목록을 가져오고 데이터를 변경할수 있는 기능이다.

참조하고자 하는 소스내의 클래스이름을 알고 있다면 정의된 메소드랑 멤버변수등을 알아낼수 있으며, 알아낸정보를 이용하여 해당소스의 데이터를 조작할수 있다.

kotlin에서는 KClass, :: 로 상대의 클래스와 변수,함수등을 참고할수 있다.

리플렉션은 주로 라이브러리 프로젝트에서 생성할때 사용한다. 제작중인 라이브러리에서 다른 외부 라이브러리를 포함하지 않고 단지 참조만 해서 처리결과만 받아오고 싶은때는 리플렉션 기능을 이용하여 해당 패키지의 클래스이름을 가져와서 method, 변수등을 invoke(발생)하는 방식으로 처리하면 제작중인 라이브러리에서 다른 라이브러리를 포함하지 않고도 배포가 가능하다.

예를 들어 현재 제작중인 라이브러리가 firebase를 참고해야할때, firebase를 직접 포함하지 않고 com.google.firebase.messaging 등과 같이 클래스이름과 getToken함수의 정보를 reflection참조후 invoke하여 firebase 의 토큰값을 얻어낼수 있는등에 사용된다.

class InvokeMethod(val className: String,val methodName: String) {  
  
    fun getClassInfo(): Class<*>? {  
        return runCatching {  
  Class.forName(className);  
        }.getOrNull()  
    }  
  
    fun getMethodInfo(classInfo:Class<*>?, parameterTypes:Any?): Method? {  
        return runCatching {  
  parameterTypes?.let {  
  classInfo?.getMethod(methodName, parameterTypes as Class<*>?);  
            }?: run {  
  classInfo?.getMethod(methodName);  
            }  
 }.getOrNull()  
    }  
  
    fun getMethodFromObject(obj:Any?, methodName:String): Method? {  
        return runCatching {  
  obj?.javaClass?.getMethod(methodName)  
        }.getOrNull()  
    }  
  
    fun <T> invokeM(parameterTypes:Any?, vararg argParam:Any?): T? {  
        val method = getMethodInfo(getClassInfo(), parameterTypes)  
  
  
        return runCatching {  
  if (argParam.isEmpty()) {  
                method?.invoke(null) as T?  
            } else {  
                method?.invoke(argParam.first(), *argParam.drop(1).toTypedArray()) as T?  
            }  
        }.getOrNull()  
    }  
  
    fun <T> invokeFromMethod(method:Method?, vararg argParam:Any?): T? {  
        return runCatching {  
  if (argParam.isEmpty()) {  
                method?.invoke(null) as T?  
            } else {  
                method?.invoke(argParam.first(), *argParam.drop(1).toTypedArray()) as T?  
            }  
  
        }.getOrNull()  
    }  
}

//사용예
InvokeMethod("com.sdk.developer.Config", "getDeveloperConfig").invokeM<Int>(null)

4.9. 입출력기본

Kotlin은 자바의 입출력과 같은 방식으로 print와 scanner, readinput 등을 이용한다.
kotlin을 이용해서 서버측의 콘솔대응 프로그램등을 만들때 사용한다.

4.9.1 출력

Java의 System.out.print(println) 을 래핑한 함수 print(println)을 사용한다.

print("Hello World")
println("Hello World")

4.9.2 입력

kotlin 에서는 사용자로부터 스트링버퍼를 입력받는 readLine 과 스트링과 정수,실수형 같은 자료형을 입력받는 Scanner함수를 사용한다.

//readLine
var input = readLine()!!
print("입력값은  $input")

//Scanner
val readerSc = Scanner(System.'in')
print("이름?")
val name = readerSc.nextLine()

print("성적?")
val jumsu = readerSc.nextInt()

print("이름은 $name , 성적은 $jumsu")

5.코루틴

Android 에서 서버통신, 파일읽기등의 작업을 AsyncTask라는 비동기 기능을 간편하게 이용했는데, API 30부터는 공식적으로 비추천이 되었다. 구글이Kotlin First를 외치고 있는 만큼 Kotlin에서도 적절한게 있을까 헀는데, Coroutine이라는 경량 쓰레드관리 기능을 제공하여 주어서 좀더 안정적이고 편리하게 비동기 작업을 할수 있도록 도와준다.

https://developer.android.com/kotlin/coroutines?hl=ko

Coroutine은 Kotlin언어에서 사용할수 있는 기능으로서 RxJava만큼 다양하지는 않지만 일단 가볍게 사용할수 있다는 점에서 좋은 면도 있는것 같다. 또한 안드로이드의 사이클과 연계되어 메모리 누수가 되지않도록 해준다는 점에서 사용을 권장할만하다.

5.1 Coroutine vs Thread

하나의 프로세스실행단위를 쓰레드라고 하는데, 크게보면 프로그램내에서 작은 서브프로그램을 돌리는 것과 비슷하기 떄문에 별도의 레지스터등과 같이 자원을 소비하게된다. 따라서 쓰레드 추가/삭제 등을할떄는 자원관리처리 떄문에 시스템에 무리를 줄수있기 때문에 모바일환경에서는 주의해서 사용해야 한다.

Coroutine은 Kotlin에서 관리하는 쓰레드자원에서 작은 Job단위로 별도의 작업루틴을 생성하고 사용하도록 하여 쓰레드자원을 죄대한으로 효율적으로 이용하면서 비동기 작업이 가능하도록 한다.

개발자는 복잡한 쓰레드에 신경안쓰고 간단히 Coroutine사용방법대로 하면된다.

5.2 주요 키워드

Coroutine을 사용하기 위해서 기본적으로 알아야 할 주요 키워드는 아래와 같다.

  • CoroutineScope : 코루틴의 실행의 범위를 지정한다. 해당스코프 안에서는 실행되고 안드로이드경우에는 라이프싸이클과 연동할수도 있다.

  • CoroutineContext : 실행중인 코루틴이 처리 실행할위치를 지정한다. Dispatcher를 지정하여 DEFAULT, IO, MAIN, JOB, DISPATCHER등 처리할 작업에 맞는 값을 지정한다.

  • launch (job, async, deferred) : 코루틴을 만들고 실행해주는 코루틴 빌더함수이다.

Coroutine이라는 단어만 붙었을 뿐이지 scope, context,dispacther , launch, job 등은 쓰레드처리에 자주나오는 용어이다.

5.3 간단히 사용해보기

Coroutine을 Kotlin에서 꼭 사용해야 되는 것은 아니기에 사용하기 위해서는 coroutine라이브러리를 추가해야한다.

5.3.1 app build gradle 에 coroutine 라이브러리를 추가한다.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

5.3.2 Activity에서는 다음과 같이 한다.

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       //스코프 -(콘텍스트)-실행
       CoroutineScope(Main).launch {
           Toast.makeText(baseContext, "main thread", Toast.LENGTH_SHORT).show()
       }
   }
}

안드로이드의 메인쓰레드에 coroutine을 바로 launch하여 toast메시지를 출력한다. 메인쓰레드는 coroutine요청이 오면 메인쓰레드풀에 직업넣고 순서가 오면 실행한다.

5.3.3 IO, Default Context에서의 제약을 확인.

앞의 소스에 IO, Default Context를 추가하여 어떤 제약이 있는지 보자.

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       // 스코프 -(콘텍스트)-실행
       CoroutineScope(Main).launch {
           Toast.makeText(baseContext, "main thread", Toast.LENGTH_SHORT).show()
       }

       CoroutineScope(IO).launch {
           // IO Context,즉 백그라운드 처리에서는 Main 쓰레드를 갱신항수 없다.
           Toast.makeText(baseContext, "main thread", Toast.LENGTH_SHORT).show()
       }

       CoroutineScope(Default).launch {
           // Default Context,즉 백그라운드 처리에서는 Main 쓰레드를 갱신항수 없다.
           Toast.makeText(baseContext, "main thread", Toast.LENGTH_SHORT).show()
       }

   }
}

앱이 실행되지 않고 에러가 나면서 종료하게 된다. 원인은 toast는 화면에 뭔가를 그리는 역활을 하기 때문에 Main쓰레드에서 실행되어야 하지만 IO, Default Context에서 실행된 코루틴 스코프에서는 메인쓰레드에 접근이 안되기 때문에 에러가 난다.

5.3.4 withContext 로 컨텐스트를 변경하여 문제를 해결하자.

앞서 보았던것 처럼 CoroutineContext에 따라서 어떤 처리 로직을 넣을 지 잘 판단해야 한다. Coroutine에서는 유연하게 각각의 Context를 옮겨가며 처리할수 있는데 다음과 같다.

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       //스코프 -(콘텍스트)-실행
       CoroutineScope(Main).launch {
           Toast.makeText(baseContext, "main thread", Toast.LENGTH_SHORT).show()
       }

       CoroutineScope(IO).launch {
           val resultStr = "result from API with Retrofit " // Retrofit등 외부에서 뭔가 가져왔다고 치자.

           delay(1000)

           // 새로운 코루틴을 생성해서 실행한다.
           // 경고, IO쓰레드에서 Main쓰레드를 부르는 것은 안좋다.Main쓰레드에서 IO쓰레드를 읽도록 하자.  
           CoroutineScope(Main).launch {
               Toast.makeText(baseContext, resultStr, Toast.LENGTH_SHORT).show()
           }

       }

       CoroutineScope(Default).launch {
           val resultStr = "result from Big Logic" // 길게 걸리는 처리의 결과라고 치자.

           delay(2000)

           // withContext로 잠시 Main에서 실행하도록 해준다.
           // 경고, IO쓰레드에서 Main쓰레드를 부르는 것은 안좋다.Main쓰레드에서 IO쓰레드를 읽도록 하자.  
           withContext(Main) {
               Toast.makeText(baseContext, resultStr, Toast.LENGTH_SHORT).show()
           }
       }
   }
}

이제 에러는 안나고 1,2,3 초에 걸쳐서 메시지 나타나게 된다. 기존의 Async, doing background 처리할때와 비교하면 직관적이고 간단하다.

5.4 Coroutine Scope & Coroutine Context

코루틴을 실행시에 scope, 범위를 지정하여 job단위로 실행할수 있도록 한다. 따라서 해당 스코프에서 에러가 나더라도 전체 코루틴의 에러가 아니라 해당 스코프내에서만 에러로 처리되어 다른 스코프의 작업들은 진행될수 있도록 해준다.

CoroutineContext는 코루틴이 어떤 쓰레드에서 실행이 될건지를 결정하는 역활을 한다. 지정하는 값은

Dispatcher를 통해 하는 데 다음과 같은 종류가 있다.

  • Dispatchers.Main : 메인(주로 UI) 스레드에서 동작하도록 한다
  • Dispatchers. IO : 별도의 쓰레드에서 동작하도록 하여 외부네트워크접속, 파일제어 등 비동기 작업을 할떄 주로 사용된다.
  • Dispatchers.Default : CPU 자원을 다소 사용해야되는 긴 작업은 메인쓰레드에서 처리하기엔 무리가 있기 때문에 백그라운드에서 동작하도록 한다.

5.5 runBlocking &Launch & async

코루틴의 스코프와 컨텍스트를 지정했다면 이를 실행해야 하는데 코루틴빌더형식의 함수인 launch와 async 를 통해 실행할수 있다.

  • launch(): launch는 코루틴을 바로 실행하고, 실행된 코루틴을 제어하기 위해서 job 객체를 리턴한다.
  • async() : 코루틴 스코프내에서 처리결과에 대한 반환값이 필요할때 사용된다. 따라서 해당 스코프의 처리가 모두 끝날때 까지 기다려야 하기 때문에 Deferred<T> (*T는 반환값형) 형태로 리턴한다.

5.5.1 runBlocking & coroutineScope

쓰레드를 블럭(점유해서 쓰레드를 독점해서사용) 하는 함수로서 순서상 반드시 먼저 실행해야 될때가 필요할때 사용된다. 단 안드로이드의 메인쓰레드에서 사용하게 되면 ANR오류가 발생하고 앱이 종료될수 있으므로 주의가 필요하다.

runBlocking {
   print("this thread blocked")
}

5.5.2 launch

launch함수는 코루틴을 생성(코루틴 생성빌더이다)하고 실행한다. 메인쓰레드를 블록시키지 않고 코루틴에서 몰래? 실행한다.

val job = CoroutineScope(Main).launch {
   ...
}

val jobIO =  CoroutineScope(IO).launch {
   ...
}

반환된 job 객체에 join(), cancel() 을 이용하여 처리완료를 작업들을 기다리거나 작업을 취소할수 있다.

GlobalScope.launch {
   val job = launch {
       var i = 0
       while (i < 3) {
           delay(1000L)
           println("Coroutine running $i")    // 0,1,2 수를  출력, 출력순서 2
           i++
       }
   }

   println("test #1")    // 출력순서 1
   job.join()        // 스코프가 실행완료 될때 까지 대기
   println("test #2")    // 출력순서 3
}

5.5.3 async

async 도 코루틴을 생성하고 실행한다. launch 와 다른점은 반환 객체가 deferred(지연된 결과)를 반환 한다는 것이다. 또한 deferred객체에 반환값의 타입(T) 를 지정하여 코루틴스코프가 최종적으로 반환하는 실제 결과값을 받을수 있도록 해준다. 예를 들어 데이터접속후 반환된 값등…

val jobDeferred = CoroutineScope(Main).async {
   ...
}

val jobDeferredIO =  CoroutineScope(IO).async {
   ...
}

async는 deferred<T>를 반환하기때문에 await함수로 결과가 나올때 까지 기다렸다가 값을 전달받을수 있다.

GlobalScope.launch {
   val deferred = async {
       var i = 0
       while(i < 5) {
           delay(500L)
           i++
       }

       "result"
   }

   val text = deferred.await()    // deferred 스쿠프가 끝날때 까지 대기 후 결과값을 받음
   println(text)

}

5.5.4 launch/async의 실행을 지연

launch/async 함수는 코루틴을 생성하고 바로 실행하게 된다. 코루틴을 지정만 하고 실행은 따로 하고 싶을떄가 있는데 CoroutineStart.LAZY 인자를 지정함으로서 가능하다.

GlobalScope.launch {
   // 출력함
   val job = launch() {
       print("Coroutine launch Started")
   }

   // 출력안함
   val jobLazy = launch(start = CoroutineStart.LAZY) {
       print("Coroutine launch LAZY Started")
   }

   // 호출되는 순간 출력함
   val deferred = async(start = CoroutineStart.LAZY) {
       print("Coroutine async LAZY Started")
       "result"
   }
   val text = deferred.await()    // deferred 코루틴 끝날때 까지 대기 후 결과값을 받음
   println(text)

}

5.6 GlobalScope & ActivityScope & LocalScope

코루틴의 스코프는 global과 실행블록스코프가 있다. 여기에 안드로이드의 라이프사이클을 고려한 Activity Scope가 있다

  • GlobalScope : Application전반에 걸친 스코프
  • ActivityScope : Activity의 생명주기에 맞춘 스코프(onDestroy 에서 해체가 필요)
  • LocalScope : launch, async로 지정한 스코프

GlobalScope 는 앱전반에 걸쳐 있으므로 사용자가 앱을 사용하는 동안 처리해야될 작업에 대해 지정하는게 좋다. 예를 들어 사용자행동분석하고자 할때 여러앱에 걸쳐 버튼등을 터치할때 globalScope에 지정된 잡으로 외부api 에 값을 전달하고자 할때 유용하다.

ActivityScope는 앱의 활동주기 내에서만 선언하여 Activity의 종료와 함께 잡이 제거되도록 한다. 외부api에서 가져온 목록을 activity화면에 출력할때는 코루틴스코프를 activity로 하는게 좋다.

LocalScope는 launch,async로 지정된 브럭에서만 유용한 스코프이기 때문에 별도로 제거해줄 필요는 없다.

class MainActivity : AppCompatActivity(), CoroutineScope  {
   // 2. ActivityScope by CoroutineScope Interface
   private lateinit var myjob: Job
   override val coroutineContext: CoroutineContext
       get() = Dispatchers.Main + myjob

   override fun onDestroy() {
       super.onDestroy()
       myjob.cancel() // Activity 종료 시 코루틴을 중단해주자
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       myjob = Job()

       // 1. GlobalScope
       GlobalScope.launch {
           print("Coroutine launch Started")
       }

       // 3. LocalScope
       CoroutineScope(Main).launch {
           Toast.makeText(baseContext, "main thread", Toast.LENGTH_SHORT).show()
       }
   }
}

5.7 delay vs sleep

코루틴에서 실행을 잠시 지연시키는 함수로서 delay와 sleep 가 있다. 이 둘의 차이점은 다음과 같다.

  • delay : 현재 스코프블럭에서만 지연이 영향이 있도록 하고, 같은 스코프전체의 다른 다른 코루틴 블럭은 실행되도록 한다.
  • sleep : 현재 지정된 스코프전체에 지연이 발생되도록 하여 현재스코프블럭이 끝날때까지 같은 스코프의 다른 코루틴이 실행을 지연한다.
GlobalScope.launch(context = Dispatchers.Main) {
   println("coroutine 1")
   delay(5000)
   println("waited me?")
}
GlobalScope.launch(context = Dispatchers.Main) {
   println("coroutine 2")
}

실행은 coroutine 1 -> coroutine 2 -> waited me? 순으로 출력된다. 반면

GlobalScope.launch(context = Dispatchers.Main) {
   println("coroutine 1")
   sleep(5000)
   println("waited me?")
}
GlobalScope.launch(context = Dispatchers.Main) {
   println("coroutine 2")
}

실행은 coroutine 1 -> waited me? -> coroutine 2 순으로 출력된다. GlobalScope의 전체 코루틴에 5초간 지연이 발생했다

5.8 suspend & resume

코루틴은 기본적으로 동시에 여러개가 실행될수 있돌고 되어 있다. 따라서 어떤 함수내에서 코루틴을 사용한다면 함수읠 실행이 함수내에 선실행했던 코루틴 보다 빨리 종료 될수도 있다.
runblock이나 join,await등을 이용하면 어느정도 제어도 가능하지만 여러개의 코루틴이 순차적인 실행을 보장해 주지는 않는다.

함수내에서 다른 함수를 실행하고 반환값을 받아서 다시 계속실행하는 고전적인 방법에는 callback을 이용하는 것이 있지만, 여러개의 callback 을 중첩하게 되면 관리도 힘들뿐더러 콜백지옥이라는 것을 맛보게 된다.

코루틴의 suspend함수는 현재의 실행중인 코루틴을 일단 정지 시켜서 다른 코루틴을 수행하도록 하고, 수행이 끝나면 정지했던 원래의 코루틴을 resume할수 있도록 해준다.

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)


       val scope = CoroutineScope(Main)
       scope.launch {
           loadData()
           processingData()
           displayData()
       }

   }

   suspend fun loadData() {
       withContext(IO) {
           val data = "Api 로 부터 데이터 취득"
           for (a in 1..10000) {

				if (a == 5000) {
				   yield()
				}

               print("pi 로 부터 데이터 취 ${a} ")
	        }
       }
   }

   suspend fun processingData() {
       withContext(IO) {
       val data = "취득된 데이터를 가공"
       }
   }

   suspend fun displayData() {
           Toast.makeText(baseContext,     "데이터 표시", Toast.LENGTH_SHORT).show()
   }
}

IO스코프에서 실행된 코루틴은 loadData, processingDate, displayData 함수를 호출한다. 이들 함수들은 suspend 키워드가 지정되어 있기 때문에 suspend함수를 만나는 순간 현재의 코루틴은 잠시 중단(쉰다)하고 suspend함수가 실행이 끝난 지점에 다시 재개하게된다.

따라서 위의 예제에서는 loadData의 긴처리가 끝난다음에 processing -> display순으로 해서 화면에 toast를 출력한다.

callback으로 구현해야 했다면 각각의 함수에 callback을 인자로 전달하여 중첩시켜서 실행해야 했어야 됬지만 suspend함수를 이용하여 깔끔하게 처리할수 있다.

Appendix

Coding conventions & Style Guide

Kotlin coroutine & Kotlin Flow

Kotlin coroutine

코루틴은 co-routine 이라는 의미로 프로그램의 실행단위인 작은 routine 들을 동시에 실행가능하도록 해주는 기술이다. 코틸린 뿐아니라 Golang, C# 등에서도 비슷한 개념의 고루틴,코루틴 이 있다.

간단예제부터 보자

fun main(args: Array<String>) {  
	GlobalScope.async {  
	    delay(1000L)  
	    println("async!")  
	}  
	GlobalScope.launch {  
	    delay(2000L)  
	    println("launch!")  
	}  
	runBlocking {  
	  delay(3000L)  
	}  
	println("last Hello,")  
}

launch, async, runBlocking 은 모두 코루틴 블럭을 만들어 주는 빌더이다. 위와 같이 코루틴블럭을 만들고 코루틴으로 처라할 로직을 내부에서 실행하면 동기(runBlocking) 혹은 비동기(launch,async)로 코루틴을 실행해준다.

runBlocking

runBlocking{ } 빌더는 쓰레드 자체를 중단(blocking) 시켜서 { } 안의 내용을 처리한다. 의도적이지 않은 이상 루틴이던 쓰레드이던 블럭시킨다음 뭔가를 처리한다는것은 그만큼 자원을 들고 있어야 되기 때문에 효율적이지 못하다 그래서 코루틴에서는 runBlocking { } 은 꼭 필요할 떄만 사용을 권장하고 있다.

runBlocking은 다른 코루틴 안에서도 사용할수 있다.

GlobalScope.launch {  
  runBlocking {  
	  delay(1000L)  
  }  
  delay(1500L)  
  println("launch!")  // 1 + 1.5 초 후에 출력된다.
}

main 함수에서 코루틴을 사용하기 위해서 runBlocking { } 을 사용한 예제이다.

fun main(args: Array<String>) = runBlocking {
 GlobalScope.launch {    
     delay(1500L)  
     println("launch!")  
 }    
 println("last Hello,")
 delay(2000L)  // 중단 함수 delay 는 코루틴블럭 에서만 가능하다. 
}

launch & job

launch로 쏘아올린? 코루틴블럭을 실행하다가 중단하고싶거나 실행이 완료될때 까지 기다리고 싶을때는 launch { } 의 반환 값형식인 Job 을 이용하면 된다.
Job 클래스에는 launch 로 실행한 코루틴이 실행중인지, 끝났는지 등의 상태값등과 하위 Clild Job 들과의 연계(join) 등을 관리할수 있다.

앞서 예제에서 main함수가 launch{} 를 실행하기도 전에 바로 끝나버리는 것을 막기위해서 delay(2000L) 함수를 사용했지만, job 인스턴스를 이용하면 launch { } 가 실행이 끝날때 까지 기다리는것도 가능하다.

fun main(args: Array<String>) = runBlocking {
 val myJob =GlobalScope.launch {    
     delay(1500L)  
     println("launch!")  
 }    
 println("last Hello,")
 myJob.join() // 현재 블럭에서 이시점에 myJob 을 join(끼어들게) 해서 실행한다. 
}

Suspend 중단함수

suspend 키워드를 함수선언의 맨앞에 지정하면 코루틴을 위한 중단함수로 인식힌다.
중단함수는 launch { } 등과 같은 코루틴블럭에서 복잡한 로직을 길게 짤고 싶을때 해당 기능을 함수화 시키는 것을 가능하게 해준다.
현재 코루틴 스코프에서 중단 함수를 만나면 실행중인 부분에서 임시로 중지(블럭이 아니라 중단) 하고 중단함수로 이동해서 함수의 내용을 처리한다면 다시 원래의 스코프로 돌아와서 실행을 이어나가는 방식이다.

  
fun main(args: Array<String>) = runBlocking {  
  launch {  
  doWorld()  
    }  
  println("Hello,")  
}  
  
suspend fun doWorld() {  
    delay(1000L)  
    println("World!")  
}

CoroutineContext & CoroutineScope

CoroutineContext은 Coroutine을 어떻게 작동 할 것인지를 정의하는 것이다. 따라서 Coroutine 실행시에는 어떤 CoroutineContext에서 실행하도록 할건지 지정해야한다.

CoroutineConext를 지정하기 위해서는 CouroutineScope를 구현한 곳에서 CoroutineConext 를 지정하면 되는데,

//1.CoroutineScope함수를 사용
val scope =  CoroutineScope(Dispatchers.Default)
//2.코루틴빌더에 인자로서 사용
scope.launch(Dispatchers.IO){
    //3.withContext로 컨텍스트를 임시로 지정하여 사용
    withContext(Dispatchers.Main) {    
    }
}

이처럼 Coroutine은 CoroutineScope로 코루틴이 실행될 범위를 지정하면서, 코루틴이 동작을 수행할 CoroutineContext를 설정하여 해당 Context에서 동작되는 방식이다.

따라서 각 코루틴 실행블록은 반드시 CoroutineScope와 CoroutineContext를 가지고 있다.

CoroutineContext 요소

CotourinContext 로 지정가능한 것에는 기본적으로 다음과 같은 것들이 있다.

  • job : 하나의 코루틴을 job(일) 단위로 구분한 Context로서 상태를 제어하거나 하위잡들을 고나리할수 있는 기능이 있다.
  • CoroutineDIspatcher : 코루틴을 주요 쓰레드(메인UI쓰레드, IO쓰레드등) 에 dIspatch(전달,연결) 한 미리 정의된 Context 세트이다. 예를 들어 CoroutineDispatcher.Main이라고 지정하면 Android, javaFx,Swing 등의 메인쓰레드를 Context로 다루도록 한다.
  • CoroutineName : 코루틴블럭을 만들때 마다 코루틴 스코프와 코루틴 컨텍스트가 세트로 지정되는데, ㅅ지정된 Context에 이름을 부여할수 있도록 해준다. 단 디버그모드에서 확인용이란다
  • CoroutineExceptionHandler : 코루틴에서 캡쳐하지 못한 에러를 할수 있다.
  • CoroutineContext.Element : 코루틴 컨텍스트는 Element의 한개이상의 조합으로 이루어져 있다. CoroutineScope생성시 CoroutineContext를 아무것도 지정안했다면 EmptyCoroutineContext 가 단독으로 구성된 컨텍스트가 생성된다. CoroutineContext 는 여러가지 Element들을 각각의 key 로 구분하여, 조합된 key로 하나의 코루틴구별 단위로 사용한다.

CoroutineContext 는 여러개의 Element 조합으로 생성할수도 있다.
launch() { } -> EmptyCoroutineContext 생성
launch(Dispachers.IO) { }-> IO 작업을 수행할 Context 지정
launch(Dispachers.IO + coroutineName(“bitmapLoader”)) { } -> IO작업을 수행할 Context에 birmapLoader이름을 부여하여 생성
( + 연산자는 elements를 추가하는 연산자로 지정되어있다)

CoroutineContext는 중첩된 scope 내에서는 하위 계승되기 떄문에, 누가하나 에러나 취소가 되면 전부 취소되어 버린다. 따라서 스코프 생성시 각각 별도의 context를 지정하면 하위 스코프에서 에러나 취소가 되어도 부모스코프에는 영향이 없도록 해줄필요가 있다.

scope.launch {
    launch ( Job ()) { //Job()으로 새로운 컨텍스트 생성
        // 이곳에서 취소나 에러가 나도 이 스코프의 컨텍스트만 영향을 받는다.
    }
}

CoroutineScope 요소

코루틴을 만들때 CoroutineScope를 사용하여, 코루틴블럭에서 실행될 범위를 만들어줘야 한다. 즉, 내가 만들 루틴이 어떤환경(Context) 에서 어떤 범위(Scope) 에서 동작할건지를 정해야 된다.
CoroutineScope 인터페이스는 단순히 CoroutineContext만 변수로 가지고 있는데, 만일 자신만의 클래스에 CoroutineScope를 구현하고 싶다면 아래와 같이 CoroutineContext를 override하면된다.

class myScope : CoroutineScope {    

private val job = Job ()
    override val coroutineContext = Job () + Dispatchers . Default
    fun testCancle() {
        job.cancel () // 흔히 볼 처리
    }
}

val scope = myScope ()
scope.testCancle()

launch 등으로 새로운 코루틴빌더를 사용하면 자동으로 스코프가 생성되고, 빈 컨텍스트가 지정된다.

Channels

서로다른 스코프에 있는 코루틴들이 데이터를 주고 받기 위한 방법으로 채널이라는 것을 제공한다. 채널은 Blocking Queue과 비슷한 방식으로 동작한다.
Blocking Queue란 먼저 들어온 큐에 담긴 루틴(쓰레드)를 대기상태로 두고, 해당큐를 요청하여 큐가 비어있을때 다시 다음 루틴을 큐에 담아 놓는방식이다.

채널…TV 의 채널과 같은 거라 생각하면 된다. 생산자(produce)는 방송국이고 채널을 통해서 소비자(consumer) 가 보내주는 방송을 시청하는 방식이라고 생각하면된다.
만일 생방송이라면 방송분을 10분정도 미리 보내놓고 수신기에 그 뒤의 방송분으 순차적으로 보내는 것도 가능한데 이때는 Buffered Channel 을 이용한다.

채널에서 송신측과 수신측은 꼭 같은 지역, 즉 스코프에 있지 않아도 된다. 송신측과 수신측의 채널변수만 동일 하다면 해당변수를 통해서 어떤 지역이든 송/수신이 가능하다.

val channel = Channel<Int>()  
CoroutineScope(Dispatchers.Default).launch {  
  for (x in 1..5) {  
        println("channel.send()")  
        channel.send(x * x)  
    }  
}  
  
runBlocking { //수신이 끝날때까지 기다려주자  
  repeat(5) {  
  delay(1000)  
        println(channel.receive())  
    }  
    /*  
	val num = channel.receive() // 1.하나씩 수신할때 
	for(x in channel) { // 2.for-loop 로 수신할떄
	    println(x) } 
	channel.consumeEach { // 3.consumeEach 로 수신할때  
	    println(it)
	}  
	 */
  println("Done!")  
}

채널은 종료 기능이 있어서 송신측메시지를 모두 보낸다음에 송신한다음에 채널을 닫는것이 가능한데 Blocking Queue라는 것을 생각하면 모든 송신이 끝나는 시점은 모든 수신이 끝나는 시점이라고 생각할수 있기 때문에 종료전에는 반드시 모든 메시지가 수신된다라는 보장이 있다.

val channel = Channel<Int>()
launch {
for (x in 1..5) channel.send(x * x)
	channel.close() 
}
for (y in channel) println(y)
println("Done!")

채널과 같이 보내주는 역활과 받는 역활을 하는 것을 프로듀서-컨슈머패턴(옵서버패턴)이라고 하는데, 코루틴에서는 이를 쉽게 해주기 위해서 produce 라는 빌더와 consumeEach라는 확장함수를 제공한다.

fun main(args: Array<String>) = runBlocking<Unit> {  
  // kotlin의 확장함수를 이용해서 runBlocking스코프내에서 produceSquares함수를 사용할수 있게 했다.
  val squares = produceSquares(5)  
  // 소비함수 consumeEach는 kotlin에서 제공한다.
  squares.consumeEach { println(it) }  
  println("Done")  
}  
  
fun CoroutineScope.produceSquares(max: Int): ReceiveChannel<Int> = produce {  
  for (x in 1..max) {  
        // 채널큐에 값을 보내는 함수 send 를 이용한다. 
        send(x * x)  
  }  
}

채널을 통해 데이터를 주고 받을때 최종수신처가 데이터를 받기 전에 중간에 추가적인 처리를 거치게 하고 싶을때는 Pipeline 패턴, 즉 수신받아서 처리후 다시 결과를 송신시켜 처리할수 있다.

fun main(args: Array<String>) = runBlocking<Unit> {  
  val numbers = produceNumbers(5)  
    val doubledNumbers = produceDouble(numbers)  
    doubledNumbers.consumeEach { println(it) }  
  println("Done")  
}  
  
fun CoroutineScope.produceNumbers(max: Int): ReceiveChannel<Int> = produce {  
  for (x in 1..max) {  
        send(x)  
    }  
}  
// Int형의 데이터를 수신받아서 produce 에서  다시 cosumer 에 흘려보낸다  
fun CoroutineScope.produceDouble(numbers: ReceiveChannel<Int>): ReceiveChannel<Double> = produce {  
  numbers.consumeEach { send(it * 2) }  
}

Buffered channels

채널을 생성시에 버퍼를 지정하지 않으면 produce 송신측과 consumer 수신측이 서로 1:1로 대응하기 때문에 송신/수신 쪽에서 데이터전달이 완료되야 다음 송/수신이 이루어지게 된다.
버퍼형식은 다음과 같다.

  • UNLIMITED : 버퍼에 제한이 없어서 송신측에서 버퍼최대치(Int.MAX_VALUE) 만큼을 한번에 보내놓는다.
  • RENDEZVOUS : 버퍼가 없는 상태라서, consume 측에서 소비가 이루어져야 다음 데이터가 들어올수 있다.
  • CONFLATED : 큐의 마지막 들어온 데이터만 수신한다.
  • BUFFERED : 미리 정해진 64개의 버퍼를 지정한다.
  • 직접입력 : 버퍼의 개수를 직접입력할수 있다.

기본값은 RENDEZVOUS(랑데뷰 1:1) 이다.

fun main(args: Array<String>) = runBlocking<Unit> {  
  val channel = Channel<Int>(4)  
  
    val sender = launch {  
  repeat(10) {  
  print("Try to send $it : ")  
            channel.send(it)  
            print("Done\n")  
        }  
 }  
  delay(1000)  
    sender.cancel()  
}

Channel Selector

두개 이상의 송신채널이 있고 이를 하나의 수신자가 받는 코루틴에서 두개의 코루틴중에 하나씩 골라서(selector) 받을 있게 도와준다. 코틀린의 When 처럼 메시지타입에 따라서 구분할수 있다.

  
fun CoroutineScope.fizz() = produce<String> {  
  while (true) {  
        delay(300)  
        send("Fizz")  
    }  
}  
var fizzCount = 0  
fun CoroutineScope.buzz() = produce<Int> {  
  while (true) {  
        delay(500)  
        send(++fizzCount)  
    }  
}  
  
suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<Int>) {  
    select<Unit> {  
      fizz.onReceive { value ->  
          println("fizz -> '$value'")  
      }  
       buzz.onReceive { value ->  
          println("fizzCount -> '$value'")  
        }  
      }
 }
  
runBlocking {  
  val fizz = fizz()  
   val buzz = buzz()  
  repeat(7) {  
      selectFizzBuzz(fizz, buzz)  
  }  
  GlobalScope.coroutineContext.cancelChildren()  
}

이밖에 Selecting to send, Selecting deferred values, Switch over a channel of deferred values 기법이 있다.

Kotlin Flow

자세한 설명은 Kotlin Flow페이지를 참고
중단함수 suspend function는 스코프내에서 코루틴을 잠시 중단하고 비동기적작업을 한다음 하나의 결과값을 다시 돌려주는 기능을 한다.
그런데 비동기 적으로 여러번 값을 어딘가에 돌려줄 필요가 있을때는 Flow를 이용해서 간단하게 작업할수 있다.
Flow 는 flow { } 블럭 에서 선언하거나, 이미 있는 리스트가능한 목록을 asFlow함수로 변경하여 사용할수 도 있다.

 // 선언 
fun foo2(): Flow<Int> = flow {  
  for (i in 1..3) {  
        delay(100)  
        emit(i)  
    }  
}
//사용
runBlocking {  
  foo2().collect { value -> println(value) }  
}

결과값이 1,2,3 으로 순차적으로 흘려(flow)보내주는 것을 볼수 있다.
송신/ 수신 하는 것이 채널과 비슷해보이지만 Cold Stream으로 되어 있어서 요청( collect) 할때 송신측에서 한번에 모두 흘려보내는 것이라는게 다르다. 채널은 hot Stream 이라고 한다.
따라서 collect하는 시점에 flow 함수에 정의된 자료들이 하나씩 순서대로 흘려보내지고(emit 방출하기도 한다고함), 이를 collect { } 블럭에서 하나,하나에 대한 데이터처리를 하면된다.
List나 Array, 또는 Iterator 등과 같이 순차적으로 구성되어 있는 자료를 비동기적으로 하나씩 처리해야 할 필요가 있을때 사용한다.

flowOF, asFlow

기존의 list나 iterator를 flow로 변환하고자 할때는 flowOf(고정된 리스트등에 사용) 또는 .asFlow(확장함수, 다양한 collection에 사용) 으로 변경해서 flow처리를 할수 있다.
flow는 map, filter 등 collection함수 들도 사용할수 있다.

    (1..3).asFlow().map { it -> (it*2) }.collect { value -> println(value) }

transform

flow 블럭에서는 아이템을 하나씩 꺼내서 처리하도록 되어 있다. 이때 꺼낸 아이템을 흘려보내기 전에 추가적인 처리를 하고 싶다면 transform { } 블럭으로 지정해야 한다. 즉 , flow내의 아이템을 방출(emit) 하기전에 아이템의 값과 형식등에 변화를 주고 싶을때 사용한다.

val paramFlow = flowOf("kim", "blabla")  
runBlocking {  
  paramFlow  
        .searchFromTwoApi()  
        .collect {  // 아이템의 it 타입은 Any타입이된다.
                println("$it")  
        }  
  println("done")  
}
  
fun Flow<String>.searchFromTwoApi() = transform { request ->  
  emit("length of $request is ")  
    emit(request.length)  
}

Buffering

Flow은 기본적으로 아이템을 하나씩 꺼내서 방출하는 방식인데, 버퍼링을 이용하면 flow안의 아이템들을 버퍼(버퍼크기만큼)에 담아두고 한번에 버퍼만큼 방출하도록 한다.

 runBlocking {  
    val time = measureTimeMillis {  
    simple()  
      .buffer() // buffer emissions, don't wait  
      .collect { value ->  
              delay(300) // pretend we are processing it for 300 ms  
              println(value)  
       }  
 }  
 println("Collected in $time ms")  
}

fun simple(): Flow<Int> = flow {  
  
  for (i in 1..3) {  
        delay(100) // pretend we are asynchronously waiting 100 ms
        emit(i)  
    }  
}

위와 같이 한다면 버퍼의기본값(64개) 아이템만큼 버퍼에 담아서 방출하기 때문에, 방출시 100ms 지연했던것이 한번만 적용되고 64개를 모아서 방출(emit)하게 되어 총 실행시간이 1000ms (100 +300*3)정도가 된다.
만인 버퍼링을 안했다면 300 +900 = 1200ms 정도가 되었을것이다.
버퍼의 종류는 channel과 같다 ( UNLINITED, BUFFERED, CONFLATED, RENDEZVOUS, 사용자정의 ) 채널부분 설명을 참고하자.

Composing multiple flows

Zip

서로 다른 두개이상의 flow 를 하나로 묶어서(zip) 수집(collect)하도록 도와준다. flow는 서로 아이템의 개수가 짝을 이루어야 한다. 짝이 맞지 않을 때는 적은 수에 맞추어 방출된다.

val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms
val startTime = System.currentTimeMillis() // remember the start time 
nums.zip(strs) { a, b -> "$a -> $b" } // compose a single string with "zip"
    .collect { value -> // collect and print 
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 
// 묶어서 전송하기 때문에 가장 긴 시간 400ms 에 송출이되어 수집된다.
Combine

Zip은 두개의 아이템이 일치되는 시점으로 방출시간을 정하지만, Combine은 각각의 flow들이 묶여서 방출가능한지 알아보고 가능하다면 순서에 상관없이 무조건 묶어서 방출을 하는 방식이다.

val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms          
val startTime = System.currentTimeMillis() // remember the start time 
nums.combine(strs) { a, b -> "$a -> $b" } // compose a single string with "combine"
    .collect { value -> // collect and print 
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 
1 -> one at 452 ms from start 
2 -> one at 651 ms from start 
2 -> two at 854 ms from start 
3 -> two at 952 ms from start 
3 -> three at 1256 ms from start

위와 같은 결과가 되는 이유는 nums(300ms) 와 strs(400ms) 각각 방출시간이 달라서 최최 1회때 nums가 방출하려고 하지만 처음에는 strs(400ms)를 기다려야 묶여서 방출될수 있었다. 그후 nums는 300ms후에 두번째아이템을 방출하는데 strs는 아직 다음방출시간이 안되었기떄문에 첫번쨰 방출아이템을 들고있다.
하지만 combine에서는 일단 짝을 이루면 내보내기 때문에 일단 방출이 이루어진다. nums의 3번째 방출시간이 되기 전에 strs가 두번째 방출시간이 되어 또 nums의 두번째 방출아이템을 묶어서 방출한다. 이후 900ms 시간이 되면 다시 nums의 방출시간이 되고 strs 와 묶어서 방출한다.
결혼사기 정보회사의 소개 시스템만들때 매우 유용하겠다.

Flatterning flows

Flow를 처리하다보면 Flow<Flow> 와 같이 중첩된 flow를 처리해야하는경우가 있는데 이를 평평하게 펴주어 하나의 값으로 처리할수 있도록 해준다.

flatMapConcat, flattern, flatMap, flatMapMerge, flattenConcat, flatMapConcat 등의 함수가 있다.

flattenConcat, flatMapConcat

[1]Flow<[2]Flow> 와 같은 flow가 있을때, [1]Flow의 요소하나가 방출될때 [2]Flow의 전체요소를 같이붙여서(Concat) 평면화(flat)시켜 방출한다.
[1]1,[2]a,b,c
[1]2,[2]a,b,c
[1]3,[2]a,b,c

만일 flatten을 하지않았다면
[1]1,[2]Flow
[1]2,[2]Flow
[1]3,[2]Flow

가 된다.

0 comments:

댓글 쓰기