코틀린 완벽 가이드 - 2장 코틀린 언어 기초
2장 코틀린 언어 기초
이 장에서는 코틀린의 기본적인 문법 요소를 살펴보고, 변수를 정의하고 사용하는 방법에 대해서 배운다.
2.1 기본 문법
2.1.1 주석
코틀린은 자바와 마찬가지로 세 가지 주석을 지원한다.
- 한 줄짜리 주석 : //로 시작하며 줄이 끝나면 주석도 끝남
- 여러 줄 주석 : /*로 시작하고 */로 끝남
- KDoc 여러 줄 주석 : /** 로 시작하고 */로 끝난다. 자바독과 비슷한 리치 텍스트 문서를 생성하기 위해 사용
자바와 달리 아래와 같이 여러 줄 주석을 여러 번 내포시킬 수 있다.
/*
여러 줄 주석
/* 주석 안에 내포된 주석
*/
*/
2.1.2 변수 정의하기
변수를 정의하는 간단한 형태는 아래과 같다.
val timeInSeconds = 15
- val 키워드(keyword) : 값을 뜻하는 value에서 유래
- 변수 식별자(identifier) : 새 변수에 이름을 부여하고, 나중에 이를 가리킬 때 사용
- 변수의 초깃값(initial value)을 정의하는 식 : = 기호 뒤에 작성
변수 정의 뒤에는 세미콜론(;)을 붙이지 않으며, 쓸 수도 있지만 쓰지 않는 스타일을 더 권장한다. 변수명을 작성할 때는 자바와 동일하게 camel case(첫 글자 소문자, 이후 단어의 첫 알파벳 대문자)를 사용한다.
간단한 프로그램을 통해 코틀린에 대해서 알아보자.
fun main() {
// val a : int = readLine()!!.toInt()
val a = readLine()!!.toInt()
val b = readLine()!!.toInt()
println(a+b)
}
- readLine()은 표준 입력에서 한 줄을 읽어서 문자열로 반환해주는 표준 코틀린 함수
- !! 은 널 아님 단언(not-null assertion)으로, null인 경우 예외를 발생시킴
- toInt()는 코틀린 String이 제공하는 메서드로 String -> Int 변환하는 함수
- println()은 인자로 받은 값을 표준 출력으로 전달
타입을 지정하지 않아도 타입 추론(type Inference)라는 기능으로 인해 성공적으로 컴파일되고 실행된다. 타입 추론은 컴파일러가 코드의 문맥에서 타입을 도출해주는 언어 기능이다. 불필요한 타입 정보를 코드에 추가해서 코드가 지저분해지는 것을 막을 수 있다. 필요할 때는 타입을 명시해도 된다. 타입 명시 시에는 변수 이름 뒤에 콜론(:)을 표시하고, 그 뒤에 타입을 작성한다. 이 경우 초기값이 지정한 타입과 불일치할 경우 Error: assigning String value to Int variable 컴파일 에러를 발생시킨다. 추가로 변수의 초기값을 읽기 전에 반드시 변수를 초기화해야한다.
( 파이썬은 타입추론을 통해서 컴파일을 지원하다가 최근? pydentic을 통해 데이터 검증을 수행하고 있는 걸로 알고 있다. 타입추론과 타입지정에는 일장일단이 있을 것 같다.)
2.1.3 식별자
식별자는 변수나 함수 등 프로그램에 정의된 대상에 붙은 이름이다. 코틀린 식별자는 두 가지로 구분된다.
- 다음 규칙을 만족하는 임의의 문자열(자바 식별자와 비슷)
- 오직 문자, 숫자, 밑줄 문자(_)만 포함
- 숫자로 시작할 수 없음
- 밑줄로만 이뤄질 수 있음
- 하드 키워드(ex. val, fun 등)와 같은 미리 예약된 식별자는 사용할 수 없음
반면 소프트 키워드(ex. import)는 특별한 문맥에서만 키워드로 간주되고, 그런 문맥이 아닌 경우에는 일반적인 식별자로 사용 가능 - 하드/소프트 식별자 참고 자료 - 자바와 달리 달러 기호($)를 식별자 내에서 사용할 수 없음
- 작은역따옴표(') 인용 부호로 감싼 식별자 - 두 작은역따옴표 사이에는 빈 문자열을 제외한 임의의 문자열 사용 가능
새줄 문자나 작은역따옴표나 JVM에 예약된 .;[]/<>:\는 사용 불가능하다.
2.1.4 가변 변수
앞서 val 키워드를 사용하여 선언한 변수는 불변 변수이다. 불변 변수는 한번 초기화되면 다시는 값을 대입할 수 없는 변수이며, 자바의 final 변수와 비슷하다. 불변 변수를 사용하면 함수가 side effect를 일으키지 못하고, 함수형 스타일 코드를 장려할 수 있으며, 코드에 대한 추론이 쉬워지기 때문에 가능하면 불변 변수를 많이 사용해야한다.
필요한 경우에는 val 대신 var 키워드를 사용하여 가변 변수를 선언할 수 있다. 불변 변수에서 값을 초기화할 때 사용했던 = (대입 연산자)를 통해 값을 변경할 수 있다. 변수에 대입되는 타입은 불변/가변 변수와 상관없이 처음 추론된 변수 타입과 동일해야한다. 이와 더불어 복합 대입 연산(+=, -=, *=, /=, %=)도 지원한다.
자바와 달리 코틀린의 대입은 문(statement)이기 때문에 결과 값을 돌려주지 않는다. 예를 들어 a = b = c 코드를 자바에서 돌리면 b=c의 결과 값인 b를 return하여 a=b까지 진행되지만 코틀린은 b = c의 결과 값이 존재하지 않아 에러가 발생한다.
식(expression)과 문(statement)의 차이점은 반환값의 유무에 따라 다르다.(코틀린 페이지에도 관련된 문서가 있지만, 그것보다 블로그가 좀더 와닫는것 같다. - 참고자료 : express vs statement)
- 식(express) : 프로그래밍 언어가 다른 값을 생성하기 위해 해석하고 계산하는 하나 이상의 명시적 값, 상수, 변수, 연산자 및 함수의 조합
- 문(statment) : 수행할 작업을 표현하는 명령형 프로그래밍 언어의 가장 작은 독립형 요소
2.1.5 식과 연산자
코틀린 식은 다음과 같이 분류할 수 있다.
- 리터럴 : 각 타입에 속하는 구체적인 값을 표현(12, "csm")
→ 정해진 값을 출력하므로 식 ok - 변수/프로퍼티 참조와 함수 호출 : (a, readLine(), "abc".length, "12".toInt())
→ 코틀린 변수 및 프로퍼티 참조는 해당되는 값을 반환하고, 코틀린의 모든 함수는 반환값이 있으므로 식 ok - 전위와 후위 단항 연산(--a, ++b, c--)
→ 단항 연산한 결과를 반환하므로 식 ok - 이항 연산(a+b, 2*3, x<1)
→ 이항 연산한 결과를 반환하므로 식 ok
대입 연산의 경우 위에서 언급했듯이 반환 값을 돌려주지 않으므로 식이 아니다.
여러 연산자가 존재하면 우선 순위를 정하여 연산한다. 이러한 우선 순위는 아래의 표와 같다. 만약에 우선 순위가 같다면 왼쪽에서 오른쪽 순서대로 연산을 수행한다.
본류 | 연산자 | 예제 | 우선순위를 감안한 해석 |
후위 | ++ -- . | a*b++ | a*(b++) |
++b-- | ++(b--) | ||
a*b.foo() | a*(b.foo()) | ||
전위 | +-(부호) ++ -- ! | +a*b | (+a)*b |
++a*b | (++a)*b | ||
!a || b | (!a) || b | ||
곱셈 | * / % | a*b+c | (a*b)+c |
a - b%c | a-(b%c) | ||
덧셈 | + - (계산) | a + b and c | (a+c) and c |
중위 | 이름이 붙은 중위 연산자들 | a<b or b<c | (a < ( b or b)) < c |
a == b and b==c | (a == (b and b)) == b | ||
비교 | < > <= >= | a < b == b< c | (a < b) == (b< c) |
a < b && b<c | (a < b) && (b< c) | ||
동등 | == != | a ==b || b!= c | (a ==b) || (b!= c) |
논리곱 | && | a && b || c | (a && b) || c |
논리합 | || | a = b*c | a = (b*c) |
대입 | = += -= *= /= %= | a *= a + b | a *= (a + b) |
2.2 기본 타입
코틀린의 기본 타입은 자바의 원시 타입(primitive type)이라고 생각해도 되지만, 완벽하게 대응되지는 않는다. 자바에는 원시 타입(메서드의 스택 영역에 저장)과 클래스 기반의 참조 타입(동적으로 힙에 할당된 메모리에 저장)으로 구분된다. 코틀린에서는 문맥에 따라 원시 타입과 참조 타입을 가리키기 때문에 구분이 모호하다. 코틀린에서는 동일 타입이더라도 필요에 따라 암시적으로 박싱을 수행하여 그 차이를 바로 확인하기는 어렵기 때문이다.
※ Boxing : 값 형식의 타입을 참조 형식의 타입으로 변환하는 과정
Unboxing : 참조형식의 타입을 값 형식의 타입으로 변환하는 과정
코틀린 타입은 근본적으로 어떤 클래스 정의를 기반으로 만들어지며, 이로 인해 메서드와 프로퍼티를 제공할 수 있다.
코틀린에서는 하위 타입이라는 개념으로 타입을 계층화할 수 있다. A타입이 B 타입의 하위 타입이라는 말은 B 타입의 값이 쓰일 수 있는 모든 문맥에 A 타입의 값을 넣어도 아무 문제가 없다는 뜻과 동일히다. 모든 코틀린 타입은 내장 타입인 Any의 직간접적인 하위 타입이다.
2.2.1 정수 타입
코틀린에는 정수를 표현하는 네 가지 기본 타입이 있다.
이름 | 크기(바이트) | 범위 | 대응하는 자바 타입 |
Byte | 1 | -128 ... 127 | Byte |
Short | 2 | -32768 ... 32767 | Short |
Int | 4 | -2E31 ... 2E31-1 | Int |
Long | 8 | -2E63 ... 2E63-1 | Long |
리터럴 0 을 제외하고 숫자는 0으로 시작할 수 없으며, 음수는 리터럴이 아닌 단항 음수 연산자를 리터럴에 적용한 식이다. 각 타입에는 최댓값(MAX_VALUE)와 최솟값(MIN_VALUE)를 포함한 상수 정의가 들어있다. 간단한 코드를 통해 정수 타입에 대한 다양한 표현들(2진수, 16진수 등)을 알아보자.
// OK : split _ every three digits
val n = 34_721_189
// OK
val one: Byte = 1
// Semantic Error: The integer literal does not conform to the expected type Short
val tooBigForShort: Short = 100_000
// OK
val million = 1_000_000
// Semantic Error: The integer literal does not conform to the expected type Int
val tooBigForInt: Int = 10_000_000_000
// OK
val tenBillions = 10_000_000_000
// Semantic Error: The value is out of range
val tooBigForLong = 10_000_000_000_000_000_000
// OK : init Long with postfix L or l
val hundredLong = 100L
// OK : Generate binary number
val bin = 0b101010
// OK : Generate hexadecimal number
val hex = 0xF9
// Semantic Error: Unsupported [literal prefixes and suffixes]
val startWithZero = 0123
Short.MIN_VALUE
Short.MAX_VALUE
Int.MAX_VALUE
2.2.2 부동소수점 수
코틀린은 자바와 마찬가지로 IEEE_754 부동소수점 수 를 따르는 Float과 Double(기본 타입으로 사용)을 제공한다. 부동소수점 수는 소수점을 기준으로 정수 부분과 소수 부분으로 나눠지며, 정수 부분이 비어있는 경우 정수 부분을 0으로 간주한다. 하지만 소수점을 남기면서 소수 부분을 생략할 수는 없다. 코틀린은 과학적 표기법 - E 표기법을 허용한다. 간단한 코드를 통해 부동소수점에 대해 알아보자.
// OK : common use cases
val piFirst = 3.14
val oneFirst = 1.0
// OK : delete the zeros in the integer part
val quarter = .25
// Syntax Error : Expecting an element
val oneSecond = 1.
// OK : use scientific notation
val piSecond = 0.314E1
// OK : init float number
val piThird = 3.14f
val oneThird = 1f
// Semantic Error : The floating-point literal does not conform to the expected type Double
val pi: Double = 3.14f
// Special Constant
Float.MIN_VALUE
Double.MAX_VALUE
Double.NEGATIVE_INFINITY
Float.POSITIVE_INFINITY
Double.NaN
2.2.3 산술 연산
모든 수 타입은 기본 산술 연산을 지원한다. 기본 산술 연산에는 +(부호, 덧셈), -(부호, 뺄셈), *(곱셈), /(나눗셈), %(나머지)이 있다. 코틀린 1.5부터는 정수에 대해 floorDiv()와 mod() 메서드가 추가되었다.
- floorDiv() : 정수 나눗셈(/) 연산과 마찬가지로 한 수를 다른 수로 나눈 몫을 계산
- mod() : 정수 나머지(%) 연산과 마찬가지로 한 수를 다른 수로 나눈 나머지를 계산
println(7.floorDiv(4)) // 1
println((-7).floorDiv(4)) // -2
println(7.floorDiv(-4)) // -2
println((-7).floorDiv(-4)) // 1
println(7.mod(4)) // 3
println((-7).mod(4)) // 1
println(7.mod(-4)) // -1
println((-7).mod(-4)) // -3
각 이항 연산자는 모든 가능한 수 타입(6*6) 간의 연산을 제공하고, 산술 연산의 결과는 인자 중 더 큰 의미를 지니는 타입이 된다.
(Double > Float > Long > Int > Short > Byte 순)
연산을 할 때 나머지는 나누는 수와 0 사이에 있어야한다는 것을 명심하자.
2.2.4 비트 연산
Int와 Long에 대해서 비트연산을 지원하며, 코틀린 1.1부터는 Byte와 Short에도 or, xor, inv(점 표기법으로 메서드 호출)를 호출할 수 있다.
연산 | 뜻 | 예제 | 결과 | 해당하는 자바 연산 |
shl | 왼쪽 시프트 | 13 shl 2 | 52 : 0...0011_0100 | << |
shr | 오른쪽 시프트 | 13 shr 2 | 3: 0...0000_0011 | >> |
ushr | 부호 없는 오른쪽 시프트 | 13 ushr 2 | 3 : 0...0000_0011 | >>> |
and | 비트 곱(AND) | 13 and 19 | 1: 0...0000_0001 | & |
or | 비트 합(OR) | 13 or 19 | 31: 0...0000_1111 | ! |
xor | 비트 배타합(XOR) | 13 xor 19 | 30: 0...0001_1110 | ^ |
inv | 비트 반전(inversion) | 13.inv() | -14: 1...1111_0010 | ~ |
2.2.5 문자 타입 Char
코틀린 Char 타입은 유니코드 한 글자를 표현하며 16비트이다. 이 타입의 리터럴은 작은따옴표 사이에 문자를 넣거나 16진수 를 넣는 시퀀스를 이용하여 유니코드 문자를 직접 입력할 수도 있다.
val a = 'a'
val pi = '\u03c0'
특수 문자는 아래와 같이 사용할 수 있다.
- 새줄(new line) : \n
- 탭(tab) : \t
- 백스페이스(backspace) : \b
- 캐리지 리턴(carriage return) : \r
- 작은 따옴표(single quote) : \'
- 큰 따옴표(double quote) : \"
- 역슬래시(backslash) : \\
- 달러 표시(dollar sign) : \$
2.2.6 수 변환
각 수 타입마다 값을 다른 수 타입으로 변환하는 연산 목적 타입을 알기 쉽도록 정의(toByte(), toShort(), toLong() 등)되어있다. 자바와 달리 코틀린에서는 범위가 큰 타입이 사용돼야 하는 문맥에 범위가 작은 타입을 쓸 수 없다.
val n = 100
// Syntex Error : Type mismatch. Required:Long/Found:Int
val l: Long = n
이렇게 설계된 이유는 암시적인 박싱(값 형식 → 참조 형식) 때문이다. Int 값은 꼭 원시 타입의 값으로 표현된다는 보장을 할 수 없으며, 더 큰 범위의 타입으로 변경하는 경우 다른 박싱 타입의 값을 만들어낼 수 있는 가능성이 생기고, 이로 인해 동등성 요구 조건(아래 2.2.8 참고)을 만족시키지 못하게되면서 오류가 발생할 수 있기 때문이다.
아래 예제를 보면서 이해를 해보자.
fun main() {
val intValue: Int = 42
val longValue: Long = intValue // 미묘한 오류
println(longValue) // 출력 결과: 42
}
위의 예제에서 intValue를 Long 타입의 변수 longValue에 바로 대입하는 경우,
intValue를 Long 타입으로 변환되어 할당되는 것이 아니라, intValue가 자동으로 박싱되어 새로운 객체가 생성되고 이 객체의 참조가 longValue에 할당된다.
이렇게 되면 intValue와 longValue는 서로 다른 객체를 참조하게 되므로, 동등성 요구 조건을 만족시키지 못하게 되고, 값은 같지만 객체 자체는 서로 다른 상태가 된다.
2.2.7 불 타입과 논리 연산
코틀린은 참(true)이나 거짓(false) 중 하나로 판명되는 불 타입과 논리 연산을 제공한다. Boolean도 수 타입과는 다른 타입이며, 암시적으로든 명시적으로는 수로 변환할 수 없다. Boolean 타입이 지원하는 연산은 아래와 같다.
- ! : 논리 부정
- or, and, xor : 즉시 계산 방식의 논리합, 논리곱, 논리배타합
- ||, &&: 지연 계산 방식의 논리합, 논리곱
지연 연산은 (x==1) || (y/(x-1) != 1) 와 같은 식을 처리할 때 유용하게 사용할 수 있다. 만일 뒤의 표현식부터 계산한다면 0으로 나누는 오류가 발생할 수 있으며 이를 막기 위해 앞에서 부터 계산한다면 x==1 이면 true가 되고, x != 1 이라면 뒤의 식이 분모가 0이 될 수 없기 때문에 오류가 발생할 수없다.
2.2.8 비교와 동등성
지금까지 언급한 모든 타입은 비교 연산(==, !=, < , <=, >, >= )을 제공한다. 기본적으로 코틀린 타입은 두 인자가 모두 같은 타입일 때만 ==와 !=를 제공한다. 값이 박싱돼 있는지에 따라 동등성 연산이 다른 결과를 낳을 수 있는데, 코틀린에서는 박싱이 암시적으로 진행되기 때문에 타입 사이의 동등성 연산을 허용하면 혼란을 야기할 수 있기 때문이다. 하지만 모든 수 타입의 값은서로 <, <=, >, >=을 사용해 비교할 수 있다. Char와 Boolean 값도 비교 연산을 지원하지만 같은 타입의 값과만 비교 가능하다.
기본적으로 NaN은 그 어떤 값과도 같지 않다. Nan은 s111 1111 1xxx xxxx xxxx xxxx xxxx xxxx으로 표현되기 때문에 (s: 부호 비트, x: 0 or 1 값) 다른 NaN과도 같지 않으며, 무한대를 포함한 다른 어떤 값보다 작지 않다. Nan 값인지 판별하기 위해서는 isNan() 메서드를 사용해서 확인할 수 있다.
2.3 문자열
String 타입은 문자로 이뤄진 문자열이다. 코틀린 문자열도 불변이며, 문자를 바꾸고 싶다면 기존 문자열을 바탕으로 새로운 문자열을 만들어야 한다.
2.3.1 문자열 템플릿
코틀린은 여러 가지 식에서 문자열을 합성해내는 강력한 방법인 문자열 템플릿 기능을 지원한다. 문자열 내에 ${}의 중괄호 사이에 넣기만 하면, 어떤 올바른 코틀린 식이든 문자열에 넣을수 있다. 문자열 템플릿 안의 값은 자동으로 toString() 메서드를 통해 문자열로 변환된다.
import java.util.*
fun main() {
val name = readLine()
println("Hello, $name!\n Today is ${Date()}")
}
2.3.2 기본 문자열 연산
모든 String 인스턴스는 length(문자열에 든 문자 수 표현)와 lastIndex(문자열의 마지막 문자 인덱스 표현) 프로퍼티를 제공한다. 인덱스를 각 괄호에 넣는 연산자를 사용해 개별 문자에 접근할 수 있다. + 연산자를 사용해서 두 문자열을 연결할 수 있지만 일반적으로 문자열 템플릿이 더 간결하므로 문자열 템플릿을 사용하는 것이 좋다.
val str: String = "Hello!"
val name: String = "sumni"
str.length
str.lastIndex
val ch: Char = str[3]
val sentence = "$str $name"
자바에서는 참조 동등성(두 객체가 같은 인스턴스인지 확인)을 비교하기 위해 ==와 != 연산자를 사용했다. 코틀린에서는 ==가 기본적으로 equals()를 가리키는 편의 문법이다. 자바와 같이 참조 동등성을 확인하기 위해서는 ===, !== 연산자를 사용하면 된다.
문자열은 사전식 순서대로 정렬되며 <,>,<=,>= 같은 비교 연산자를 사용해 비교 가능하다.
연산 | 뜻 | 예제 | 결과 |
isEmpty isNotEmpty |
문자열이 비어있는지 검사 | "Hello".isEmpty() "".isEmpty() |
false true |
substring | 부분 문자열을 추출 | "Hello".substring(2) "Hello".substring(1,3) |
"llo" "el" |
startsWith endsWith |
접두사나 접미사를 검사 | "Hello".startsWith("Hel") "Hello".endsWith("lo") |
true true |
indexOf | 인자로 받은 문자나 문자열이 수신 객체인 문자열에 나타나는 첫 번째 인덱스를 반환 | "abcabc".indexOf('b') "abcabc".indexOf('b',2) |
1 4 |
2.4 배열
배열은 내장된 코틀린 데이터 구조로, 미리 정해진 숫자만큼 같은 타입의 원소를 모아서 저장하고 각각을 인덱스로 참조할 수 있게 해준다. 실제 코틀린/JVM 애플리케이션에서는 자바 배열로 코틀린 배열을 표현한다.
2.4.1 배열 정의하기
만드려는 배열의 크기를 미리 알 수 있다면 표준 함수 중 하나를 사용해 배열을 생성할 수 있다. 호출할 원소의 타입을 지정하는 제네릭 함수를 사용한다. 이와 더불어 인덱스로부터 원소를 만들어 낼 수도 있다. 인덱스를 표현하는 변수로 자동으로 선언되는 it을 사용하여 람다식을 사용한다.
val a = emptyArray<String>()
val b = arrayOf("hello", "world")
val c = arrayOf(1,4,9)
val size = 3
val d = Array(size) {(it + 1)*(it + 1)}
Array<Int> 를 사용하는 배열은 모든 수를 박싱하기 때문에 그다지 실용적이지 않다. 이로 인해 코틀린은 더 효율적인 ByteArray, ShortArray, IntArray, LongArray, FloatArray, DoubleArray, CharArray, BooleanArray라는 특화된 배열 타입을 제공한다.
디버깅 시에는 두 타입이 명확히 다른 것을 인지할 수 있는데, Array<Int>의 경우 각 배열의 원소가 참조값으로 이루어져 있으며,
IntArray의 경우 값 형식으로 이루어진 것을 확인할 수 있다.
2.4.2 배열 사용하기
배열은 size(문자열의 length에 해당)와 lastIndex 프로퍼티가 있고, 인덱스 연산으로 원소에 접근할 수 있다는 점에서 String 타입과 비슷하다. 하지만 String 타입과 달리 배열에서는 원소를 변경할 수 있다.
배열 타입의 변수 자체에는 실제 데이터에 대한 참조를 저장하며, 다른 배열을 대입하면 같은 데이터 집합을 함께 공유하게 된다. 독립적인 별도의 배열을 만들고 싶다면 copyOf() 함수를 사용해야 한다.
val sequares = arrayof(1,4,9,16)
sequares.size // 4
sequares.lastIndex //3
sequares[3] //16
// Update Array Element
sequares[2] = 100 // sequares: 1,4,100,16
// Copy Array
val numbers = sequares.copyOf() // numbers : 1,4,100,16
sequares.copyOf(2) // 1,4
sequares.copyOf(5) // 1,4,100,16,0 - 부족한 부분 0으로 채워짐
// Add array element(s)
val b = intArrayOf(1,2,3) + 4
val c = intArrayOf(1,2,3) + intArrayOf(5,6)
// Compare two array
intArrayOf(1,2,3).contentEquals(intArrayOf(1,2,3)) // true