본문 바로가기

책 요약하기/Do it! 코틀린 프로그래밍

#1일차. 1장, 2장 2021-03-11

1장 코틀린 시작하기

코틀린의 main() 함수는 프로그램의 실행 진입점

자바 같은 객체 지향 언어에서 프로그램을 실행하려면 최소한 하나의 클래스와 그 안에 main() 함수가 있어야 하지만 코틀린은 선언한 클래스가 없는데도 불구하고 main() 함수와 println() 함수를 통해 콘솔에 문자열 "Hello Kotlin"을 출력하고 있습니다. 코틀린 코드는 JVM에서 실행되며, main() 함수가 있는 파일 이름을 기준으로 자바 클래스가 자동 생성됩니다. [Tools > Kotlin > Show Kotlin ByteCode > Decompile]

 

프로그램의 메모리 영역

프로그램이 사용하는 메모리 영역에는 여러 가지가 있습니다. 명령어가 들어가는 코드 영역과, 프로그램이 컴파일되면 문자열이나 정적 변수나 문자열 등이 들어가는 정적 메모리 영역인 데이터(Data) 영역이 있습니다. JVM에서는 이 영역을 메서드 정적 영역(Method Static Area)으로도 부릅니다. 또 실행 중 생성되는 객체는 동적 메모리 영역인 힙(Heap)이라고 불리는 곳에 만들어 집니다.
추가적으로 코드 블록인 중괄호({ })안에 사용한 변수나 함수 호출 블록은 임시로 쓰이는 메모리 영역인 스택(Stack)에 들어가며, 중괄호 블록이 끝나면 임시로 사용한 변수는 스택에서 제거됩니다.
데이터 영역은 정해져 있어서 실행 중에 오류가 날 가능성은 적습니다. 힙과 스택은 프로그램이 동작하는 도중에 너무 많이 메모리를 할당하는 객체가 있다면 Out of Memory 오류가 날 수 있고, 함수 호출이 재귀적으로 너무 많이 일어나면 Stack Overflow 오류가 발생할 수 있습니다.
JVM을 사용하는 프로그램에는 동적 메모리 영역의 객체가 사용된 뒤 아무 참조가 없으면 자동으로 삭제하는GC(Garbage Collector)가 있습니다. GC란 일종의 쓰레기 청소부 역할을 해서, 우리도 모르게 쓸모없는 객체를 치워 주는 일을 한다고 생각하면 됩니다.

01장 마무리 문제

Q1. (NPE)NullPointException은 변수나 객체의 초기화가 이루어지지 않은 상태에서 그 변수에 접근할 때 발생하는 예외 오류입니다.

Q2. 코틀린 언어의 특징이 아닌 것은?

  1. 자료형에 대한 오류를 미리 잡을 수 있는 정적 언어입니다.
  2. 널 포인터로 인한 프로그램의 중단을 예방할 수 있습니다.
  3. 코틀린은 객체 지향 프로그래밍 언어로만 사용됩니다.
  4. 다양한 플랫폼에서 작동하도록 만들어졌습니다.

답 : 코틀린은 객체 지향 프로그래밍 뿐만아니라 함수형 프로그래밍도 가능합니다.

Q3. 프로그램의 실행 진입점인 main( ) 함수에서 매개변수를 통해 프로그램 외부의 인자를 받아들이려면 함수의 선언을 fun main(args:Array<String>) 과 같이 해야합니다.

 

2장 변수와 자료형, 연산자

코틀린(Kotlin)에서 프로젝트(Project)는 모듈(Module), 패키지(Package), 파일(File)로 구성되어 있습니다.

코틀린 프로젝트 트리에서 생성된 패키지 내에 파일을 생성하지 않으면 default 패키지 내에 자동 생성됩니다.
코틀린 파일 이름과 클래스 이름이 같으면 .kt가 생략됩니다.

기본 패키지 활용하기

패키지 이름 설명
kotlin.* Any, Int, Double 등 핵심 함수와 자료형
kotlin.text.* 문자와 관련된 API
kotlin.sequences.* 컬렉션 자료형의 하나로 반복이 혀용되는 개체를 열거
kotlin.ranges.* if문이나 for문에서 사용할 범위 관련 요소
kotlin.io.* 입출력 관련 API
kotlin.collections.* List.Set.Map등의 컬렉션
kotlin.annotation.* 애노테이션 관련 API

 

사용자 클래스 사용하기

패키지의 이름(com.example.hellow)과 함께 패키지의 요소를 import 키워드와 함께 적으면 됩니다.

// 내가 생성한 Person 클래스를 다른 패키지에서 사용 할 때 
import com.example.hellow.Person

val person: Person = Person("Hellow", 26)
println(person.name)

 

사용자 클래스 별명 붙이기

Person 클래스에 User라는 별명을 붙여서 다음과 같이 사용합니다.

import com.tistory.mrdevelop.Person as User

val person: User = User("Hellow", 26)
println(person.name)

 

변수 이름 생성 시 주의사항

  • 변수 이름은 123abc와 같이 숫자로 시작하면 안 됩니다.
  • 변수 이름에는 while, if와 같이 코틀린에서 사용되는 키워드는 쓸 수 없습니다.
  • 변수 이름은 의미 있는 단어를 사용하여 만드는 것이 좋습니다.
  • 여러 단어를 사용하여 변수 이름을 지울 때 카멜 표기법을 사용하는 것이 좋습니다

 

카멜 표기법이란
카멜 표기법(Camel Expression)이란 여러 단어로 된 변수 이름을 지정할 때 첫 번째 글자는 소문자로 쓰고 나머지는 각 단어의 첫 번째 글자를 대문자로 써서 단어를 구별하는 방법입니다. 예를 들어 책의 수를 저장하는 변수로 nuberOfBooks라고 이름을 지을 수 있습니다. 단어가 붙어 있는 모양이 낙타의 등과 같아서 붙인 이름입니다. 보통 변수, 함수의 첫 번째 글자는 소문자로, 클래스(또는 인터페이스)의 첫 번째 글자는 대문자로 표기합니다.

 

코틀린의 자료형은 참조형 자료형
보통 프로그래밍 언어는 기본형 자료형과 참조형 자료형으로 구별되는데 코틀린은 참조형 자료형을 사용합니다. 기본형(Primitive Data Type)은 가공되지 않은 순수한 자료형을 말하며 프로그래밍 언어에 내장되어 있습니다. 참조형(Reference Type)은 객체를 생성하고 동적 메모리 영역에 데이터를 둔 다음 이것을 참조하는 자료형을 말합니다. 자바에서는 int, long, float, double 등 기본형과 String, Date와 같은 참조형을 모두 사용하지만 코틀린에서는 참조형만 사용합니다. 참조형으로 선언한 변수는 성능 최적화를 위해 코틀린 컴파일러에서 다시 기본형으로 대체됩니다. 따라서 코틀린에서는 참조형을 기본형으로 고려하는 등의 최적화를 신경 쓰지 않아도 됩니다.

따라서 코틀린은 겉으로는 참조형을 사용하는 것 같지만 컴파일 과정을 거치면 참조형이 기본형으로 바뀌므로 컴파일러가 자동으로 최적화를 수행합니다.

 

정수 자료형

부호가 있는 자료형
val num01: Long = 123L // Long 타입
val num02: Int = 0x0f // 16진수 Int타입
val num03: Int = 0b00001011 // 2진수 Int타입

부호가 없는 자료형
val num04: UInt = 153u  // UInt타입 (UShort, ULong 등 접미사로 u만 붙이면 됨
val num05: ULong = 12uL // ULong타입

// 언더바(언더스코어)로 자릿값 구분이 가능하다
val number = 1_000_000
val cardNum = 1234_1234_1234_1234L
val hexVal = 0xAB_CD_EF_12
val bytes = 0b1101_0010

 

실수 자료형

val exp01: Double = 3.14 // Double형
val exp02: Float = 3.14F // Float형 (접미사 F 추가)

// 부동소수점 표기법
// 3.14 * 10^16 => (3.14 : 가수, 10 : 밑수, 16 : 지수)
val exp03: Double = 3.14E+16
val exp04: Double = 3.14e16

 

아스키 코드와 유니코드란?
컴퓨터에서 보통 영문 위주의 문자를 표현할 때는 아스키코드(ASCII Code)를 사용합니다. 아스키코드는 1바이트로 문자를 표현합니다. 1바이트는 2^8(256)이므로 아스키코드로는 256개의 문자를 표현할 수 있습니다. 그런데 자바나 코틀린에서는 다양한 언어를 표현하기 위해 2바이트로 문자를 표현하는 유니코드를 사용합니다. 2바이트는 2^16(65536)이므로 65536개의 문자를 표현할 수 있습니다. 유니코드(Unicode)는 이스케이프 문자 \u와 16진수 4자리를 이용하여 문자를 표현합니다. 예를 들어 '한'이라는 문자는 변수에 \uD55C를 할당하면 됩니다.
val ch1 = '\uD55C'

 

문자열 자료형 선언과 저장 방식 이해하기

fun main() {
    var str1: String = "hello"
    var str2 = "World"
    var str3 = "hello"
    
    println("str1 === str2: ${str1 === str2}")
    println("str1 === str3: ${str1 === str3}")
}

위의 코드는 실제로 메모리 상에 다음과 같이 저장됩니다. 실제 데이터(Hello, World)는 힙 영역의 String Pool에 저장되며 각 데이터의 주소는 스택 영역에 저장되며 str1의 주소 : A1, str2의 주소 : A2, str3의 주소 : A1 (A1, A2는 예시)

주소 A1 : str3  
주소 A2 : str2 hello(주소 : A1)
주소 A1 : str1 World(주소 : A2)
스택 힙(String Pool)

지금 str1과 str3에는 같은 문자열이 저장되어 있는데 이런 경우에는 "hello"를 스택에 2번 저장하는 것보다 이미 저장된 값을 활용하는 것이 효율적입니다. 그래서 코틀린은 힙 영역의 String Pool이라는 공간에 문자열인 "Hello"를 저장해 두고 이 값을 str1, str3이 참조하도록 만듭니다. 결과적으로 str3의 참조 주소는 str1과 동일하므로 참조 비교를 위해 === 연산자를 사용하면 true가 반환됩니다. 이렇게 문자열 자료형은 String Pool을 이용해 필요한 경우 메모리 공간을 재활용합니다.

// 중괄호를 사용해도 큰따옴표나 $를 표현할 수 있습니다.
val special2 = "${'"'}${'$'}9.99${'"'}" // "$9.99"

 

형식화된 다중 문자열 사용하기

문자열에 줄바꿈 문자, 탭 등의 특수문자가 포함된 문자열은 다음과 같이 표현합니다.

val num = 10
val formattedString = """		
    var a = 6				/* var a = 6
    var b = "Kotlin"			   var b = "Kotlin"
    println(a + num)			   println(a + num)
    """							*/
println(formattedString)

 

자료형에 별명 붙이기

typealias라는 키워드를 사용하면 됩니다.

typealias Username = String
val user: Username = "Hellow"

 

변수에 null 할당하기

코틀린은 일반적으로 변수 생성 시 null을 허용하지 않습니다. null 할당을 허용하려면(nullable) 자료형 뒤에 물음표(?) 기호를 명시해야합니다.

var a: String? = null
var b: Int? = null

 

Safe Call(?.)과 Elvis Operator(?:)

var str1: String? = null
println(str1.length) // 컴파일 전에 오류가 납니다.
println(str1?.length) // null이 출력됩니다. null이 할당되어 있을 경우 검사하여 안전하게 호출합니다.
println(str1!!.length) // NPE강제 발생 (!!)기호는 단정 기호 : 값을 단정함
val str1: String? = null
println(str1?.length?:0) // str1?.length 출력 시 str1에 null이 있으므로 null을 출력하지만
// (?:) Elvis Operator를 사용하여 null이면 :뒤에 있는 값을 출력하게 할 수 있습니다.

 

기본형과 참조형 자료형의 비교 원리

자료형을 비교할 때는 단순히 값만 비교하는 방법과 참조 주소까지 비교하는 방법이 있습니다. 단순히 값만 비교할 때는 이중 등호(==)를 사용하고 참조 주소를 비교하려면 삼중 등호(===)를 사용합니다. 이중 등호는 참조에 상관 없이 값이 동일하면 true를 , 값이 다르면 false를 반환합니다. 삼중 등호는 값과 상관없이 참조가 동일하면 true를 반환합니다. 값이 동일하더라도 참조 주소가 다르면 false를 반환합니다.

val a: Int = 128
val b: Int = 128
println(a == b)    // true
println(a === b)   // true
// 이런 경우 코틀린 컴파일러가 참조형으로 선언된 a와 b는 기본형으로 변환하여 저장합니다.
// 반면
val a: Int = 128
val b: Int? = 128
println(a == b) // true
println(a === b) // false
// Int형으로 선언된 a는 기본형으로 변환되어 스택에 128이라는 값 자체를 저장합니다.
// 하지만 Int?형으로 선언된 b는 참조형으로 저장되므로 b에는 128이 저장된 힙의 참조 주소가 저장됩니다.
val a: Int = 128
val b = a
println(a === b) // true 자료형이 기본형인 int형이 되어 값이 동일

val c: Int? = a
val d: Int? = a
val e: Int? = c
println(c == d) // true 값의 내용만 비교하는 경우 값이 동일
println(c === d) // false 값이 동일하더라도 참조 주소를 비교해 다른 객체
println(c === e) // true 값과 참조 주소 모두 같으므로 동일
저장되는 값이 128보다 작으면 그 값은 캐시에 저장되어 참조됩니다
코틀린에서는 참조형으로 선언한 변수의 값이 -128 ~ 127 범위에 있으면 그 값을 저장하고 변수는 캐시의 주소를 가리킵니다. 이렇게 하면 더 좋은 성능의 프로그램을 만들 수 있기 때문입니다. 예를 들어 var a: Int = 28과 var b: Int = 28이라고 변수를 선언하면 28이라는 값은 스택이 아니라 캐시에 저장됩니다. 그리고 a와 b는 캐시의 주소를 참조하게 됩니다. 따라서 위 예제에서 a의 값을 128이 아니라 -128~127의 값으로 변경하면 c와 d의 참조 주소 값이 같아집니다. 그래서 a, b, c, d를 삼중 등호로 비교한 값은 모두 true가 됩니다.

 

스마트 캐스트

만약 어떤 값이 정수일 수도 있고 실수일 수도 있다면 어떻게 해야 할까요? 그때마다 자료형을 변환해도 되지만 컴파일러가 자동으로 형 변환을 하는 스마트 캐스트(Smart Cast)를 사용하는 것이 더 편리합니다. 스마트 캐스트가 적용되는 자료형은 Number형이 있습니다. Number형을 사용하면 숫자를 저장하기 위한 특수한 자료형 객체를 만듭니다. Number형으로 정의된 변수에는 저장되는 값에 따라 정수형이나 실수형 등으로 자료형이 변환됩니다. 

var num: Number = 12.2
println(num)

num = 12
println(num)

num = 123.4f
println(num)

num = 444L
println(num)

 

자료형 검사하기

변수의 자료형을 알아볼 때는 is키워드를 사용하면 됩니다. is는 왼쪽 항의 변수가 오른쪽 항의 자료형과 같으면 true를, 아니면 false를 반환합니다.

val num: Int = 123

println(num is Int) // true
println(num is String) // false

 

as에 의한 스마트 캐스트

as로 스마트 캐스트 할수도 있습니다. as는 형 변환이 가능하지 않으면 예외를 발생시킵니다. 

val x: String = y as String // y가 null이 아니면 String 형 변환되어 x에 할당, y가 null이면 예외 발생
val x: String? = y as String? // null가능성 까지 고려하여 예외 발생을 피하려면 nullable변수로 선언

 

묵시적 변환

Any형은 자료형이 특별히 정해지지 않은 경우에 사용합니다. Any클래스는 모든 클래스의 부모 클래스입니다.(사용자 정의 클래스 역시 Any클래스의 자식 클래스) Any형은 어떠한 자료형으로도 캐스팅 할 수 있습니다.

var a: Any = 1
a = 20L
println("a: $a type : ${a.javaClass}") //javaClass는 해당 변수가 어떤 기본형을 가지고 있는지 출력

 

 

비트 연산자

비트 연산을 위한 비트 메서드( 4 shl 1과 같이 멤버에 접근하는 점(.)연산자와 소괄호를 생략하는 표현식을 중위 표현식이라고 부릅니다.)
                                 

사용 예 설명
4.shl(bits) 4를 표현하는 비트를 bits만큼 왼쪽으로 이동(부호 O)
7.shr(bits) 7을 표현하는 비트를 bits만큼 오른쪽으로 이동(부호 O)
12.ushr(bits) 12를 표현하는 비트를 bits만큼 오른쪽으로 이동(부호 X)
9.and(bits) 9를 표현하는 비트와 bits를 표현하는 비트로 논리곱 연산
4.or(bits) 4를 표현하는 비트와 bits를 표현하는 비트로 논리합 연산
24.xor(bits) 24를 표현하는 비트와 bits를 표현하는 비트로 배타적 연산
78.inv() 78을 표현하는 비트를 모두 뒤집음

 

02장 마무리 문제

Q1. 불변(Immutable)의 변수를 선언할 때는 val, 가변(Mutable)의 변수를 선언할 때는 var를 사용합니다.

Q2. 불변 정수형 변수 abc에 20을 초기화해 선언하려고 합니다. 맞는 표현법은 무엇일까요?

  1. val abc: Int = 20
  2. var abc: Int = 20
  3. val abc: String = 20
  4. val abc = 20L

Q3. null이 허용되는 변수 선언은 어떤 기호와 함께 사용할까요?

  1. var b: String! = "abc"
  2. var b: Stirng? = "abc"
  3. val b: String@ = "abc"
  4. val b: String& = "abc"

 

이상 마치겠습니다 :)