( 일반 파일과 클래스 파일 )
코틀린 프로그램은 확장자가 .kt 인 파일
( 파일의 구성 요소 )
코틀린 파일 내에 작성되는 구성요소는 대부분 프로그래밍 언어와 비슷하다.
패키지(package)와 임포트(import), 클래스, 변수, 함수 선언과 주석이 파일에 포함될 수 있다.
위의 그림은 간단한 코틀린 파일의 구성요소를 보여준다.
하나의 파일에 패키지, 임포트, 클래스를 선언한 단순 구조이며, 다른 객체지향 언어와 큰 차이가 없다. 코틀린 파일에도 패키지를 선언할 수 있는데, 반드시 파일의 첫 줄에 선언해야 한다. 그리고 그 하위에는 여러 개의 import 구문을 작성할 수있다. 그리고 그 하위에 파일의 구체적인 내용을 담고 있는 구성요소를 작성한다. 위의 그림에서는 클래스를 선언한 예이다.
그런데 코틀린 파일은 클래스를 사용하지 않고 변수와 함수로만 구성할 수도 있다.
위의 그림도 하나의 코틀린 파일의 구성요소를 보여준다. 첫 줄에 패키지 선언이 있고 import 구문은 없다. 그런데 클래스를 선언하지 않았고 파일 내에 변수와 함수만 선언했다. 이처럼 코틀린에서는 모든 구성요소를 꼭 클래스로 묶지 않아도 되며, 변수나 함수를 클래스 외부에 선언할 수 있다.
이번에는 하나의 코틀린 파일에 패키지, 임포트, 변수, 함수, 클래스 등을 모두 선언한 구조이다. 결국, 코틀린은 파일의 구성요소에 대한 규칙이 없으며 개발자 편의에 따라서 변수, 함수, 클래스 등을 자유롭게 정의할 수 있다.
( 변수와 함수 )
코틀린에서 변수는 val이나 var 키워드를 이용해서 명시적으로 선언해야 한다.
이처럼 두 가지 키워드를 제공하는 이유는 코틀린의 변수가 Assign-once(Read-only)와 Mutable로 구분되기 때문. Assign-once 변수는 한 번 초기화하면 더는 변경할 수 없고, Mutable 변수는 언제든지 변경할 수 있다. 즉, val (value)은 Assign-once 변수로 선언하고, var (variable)는 Mutable 변수로 선언한다.
코틀린에서 변수를 선언할 때 특징 중 하나는 변수명을 먼저 입력하고, 그 뒤에 콜론(:)을 구분자로 타입을 입력한다는 점입니다. 과거의 프로그래밍 언어(Java, C 같은)들은 타입을 먼저 입력하고 변수명을 그 뒤에 입력하는데, 최신 언어들(코틀린, 스칼라, 스위프트 등)은 변수명을 타입보다 먼저 입력하는 특징이 있다.
val data1: Int = 10
val data2 = 20
var data3 = 30
fun main(args: Array<String>) {
data2 = 40 //에러
data3 = 40 //성공
}
data1 변수는 Int 타입으로 선언하면서 초깃값 10을 대입했다. 그런데 data2, data3 변수는 데이터 타입을 명시하지 않았다. 이처럼 변수를 선언할 때 데이터 타입을 명시하지 않으면 대입하는 초깃값에 따라 적절한 타입으로 알아서 적용된다. 따라서 data2, data3에 대입한 초깃값이 정수이므로 타입을 명시하지 않아도 자동으로 Int 타입 변수로 선언된다. 즉, 코틀린에서 알아서 값을 유추해 값에 맞는 타입을 적용한다. 이를' 타입 추론'이라고 한다.
또한, data2는 val로 선언했으므로 이후에 값을 변경할 수 없고, 06번 줄의 data3은 var로 선언했으므로 값을 변경할 수 있다. 따라서 09번 줄처럼 data2 변수값을 변경하는 구문은 에러가 발생하고, 10번 줄처럼 data3 변수값을 변경하는 시도는 정상적으로 실행된다.
( 데이터 타입 )
타입은 대부분 데이터와 관련 있다. String 타입은 문자열 데이터를 표현하기 위한 목적이며 Int 타입은 정수를 표현하기 위한 목적이다. 그런데 코틀린에서 제공하는 타입 중 데이터와 관계 없이 특수 상황을 표현하기 위한 Unit과 Nothing 타입이 있다.
Unit은 흔히 함수의 반환 구문이 없다는 것을 표현하기 위해 사용된다. 흔히 자바의 void에 해당한다고 보면 됩니다. (정확히 이야기하면 자바의 void와는 차이가 있지만 여기서는 이 정도로 이해하면 된다.)
fun myFun1() { }
fun myFun2(): Unit { }
위 코드에서 myFun1( ) 함수를 선언했다. 이 함수에는 return 구문이 없다. 딱히 반환할 데이터가 없다는 것.
함수는 함수 선언 부분에 콜론(:)으로 구분해서 함수의 반환 타입을 명시해야 하는데 선언하지 않았다. 그러면 기본으로 Unit으로 선언한 것과 같다. 그러므로 첫번째 줄의 함수 선언은 두번째 줄의 Unit으로 반환 타입을 명시한 선언과 같다. 이처럼 Unit을 반환 타입으로 사용하는 함수는 함수에서 의미 있는 반환값이 없다는 의미이므로 자바의 void와 비슷하다고 이야기할 수 있다.
Nothing은 의미 있는 데이터가 없다는 것을 명시적으로 선언하기 위해 사용하는 타입이다.
fun myFun(arg: Nothing?): Nothing {
throw Exception()
}
위의 소스를 보면 함수를 Nothing 타입으로 선언했다. 이 함수는 항상 예외(Exception)를 발생시킨다. 이 함수에서 함수를 호출한 곳에 의미 있는 데이터를 반환하지 못한다. Nothing 타입은 이처럼 의미 있는 데이터가 없다는 것을 명시적으로 표현하고자 사용한다.
( 흐름제어 구문과 연산자 )
for 반복문
프로그래밍 언어에서 제공하는 for 문의 작성 방법과 코틀린의 작성 방법에는 약간의 차이가 있다.
fun main(args: Array<String>) {
var sum: Int = 0
for(i in 1..10) {
sum += i
}
println(sum)
}
【 실행결과 】
55
위의 소스에서 for의 반복 조건에 in을 이용했다. in 뒤에 1..10이라고 작성했는데, 이는 1부터 10까지를 의미한다. 이처럼 몇부터 몇까지 특정 범위를 표현할 때 숫자 사이에 점 두 개 (..)를 이용한다.
"1부터 10까지 숫자를 1씩 증가하면서 변수 i 에 대입하고 for의 { } 부분을 10번 반복하라"
컬렉션 타입 이용
만약 특정 컬렉션 타입(배열 같은)의 데이터 개수만큼 for 문을 반복해야 한다면 다음처럼 작성할 수 있다.
val list = listOf("Hello", "World", "!")
val sb=StringBuffer()
for(str in list) {
sb.append(str)
}
println(sb)
【 실행결과 】
HelloWorld!
반복 조건에 in을 사용했는데, in 뒤에 컬렉션 타입의 객체를 지정했다. 이렇게 하면 컬렉션 타입의 객체에 포함된 데이터 개수만큼 for 문을 반복하며, 한 번 반복할 때마다 컬렉션 타입의 객체에 담긴 데이터를 순서대로 하나씩 추출하여 in 앞의 변수에 대입한다.
컬렉션 타입의 인덱스값 이용
그런데 for 문을 이용하다 보면 인덱스값(컬렉션 타입에서 데이터의 위치를 나타내는 값)을 이용할 때도 있다. 예를 들어 for 문을 수행하면서 list 객체의 실제 데이터인 "Hello", "World" 등 값이 아니라, 0, 1 등 데이터 위치를 나타내는 인덱스값을 이용하고 싶습니다. 이럴 때는 indices 프로퍼티를 사용한다.
val list = listOf("Hello", "World", "!")
for (i in list.indices) {
println(list[i])
}
【 실행결과 】
Hello
World
!
indices는 Collection, Array 클래스에 선언되어 있는 프로퍼티로 컬렉션 타입의 인덱스 범위를 반환한다. 예를 들어 list 객체에 데이터가 3개 들어있다면, list.indices는 0..2를 반환한다. 결국 for (i in list.indices)는 for (i in 0..2)로 선언한 것과 같다.
컬렉션 타입의 인덱스와 값 모두 이용
컬렉션 타입을 이용해 for 문을 작성할 때 인덱스값은 indices를 이용하면 되는데, 어떤 경우에는 인덱스값과 컬렉션 타입의 객체에 저장된 데이터를 함께 얻고 싶을 때가 있다. 이때는 withIndex( ) 함수를 이용하여 인덱스($index)와 값($value)을 얻어올 수 있다.
val list = listOf("Hello", "World", "!")
for ((index, value) in list.withIndex()) {
println("the element at $index is $value")
}
【 실행결과 】
the element at 0 is Hello
the element at 1 is World
the element at 2 is !
또한, for 문의 조건을 줄 때 다음처럼 다양하게 명시할 수도 있다.
- for(i in 1..100) { … } → 100까지 포함
- for(i in 1 until 100) { … } → 100은 포함하지 않음
- for(x in 2..10 step 2) { … } → 2씩 증가
- for(x in 10 downTo 1) { … } → 숫자 감소
( 클래스 )
primary constructor
클래스에는 여러 생성자를 정의할 수 있는데 이 중 대표 생성자가 주 생성자(Primary Constructor)이다. 주 생성자는 하나의 클래스에 하나만 정의할 수 있다. 나중에 설명하겠지만 보조 생성자는 하나의 클래스에 여러 개 정의할 수 있다.
- 클래스 선언 부분에 작성
- 하나의 클래스에 하나의 주 생성자만 정의 가능
- 꼭 작성해야 하는 건 아니며 보조 생성자가 있다면 작성하지 않을 수 있음
주 생성자는 클래스 몸체가 아닌 헤더에서 클래스 이름 뒤에 선언합니다
03 class MyClass1 { }
04
05 class MyClass2() { }
06
07 class MyClass3 constructor() { }
08
09 fun main(args: Array<String>) {
10 val obj1 = MyClass1()
11 val obj2 = MyClass2()
12 val obj3 = MyClass3()
13 }
03번 줄의 MyClass1 클래스에는 생성자를 정의하지 않았다. 클래스 헤더에 주 생성자도, 클래스 몸체에 보조 생성자도 없다. 하지만 객체지향 문법상 객체가 생성될 때 반드시 하나 이상의 생성자가 호출되어야 한다. 따라서 MyClass1처럼 개발자가 별도로 선언한 생성자가 없다면 컴파일러가 자동으로 매개변수가 없는 주 생성자를 추가한다.
반면에 05번과 07번 줄에 선언한 클래스는 개발자가 명시적으로 매개변수 없는 주 생성자를 정의한 예 이다. 원래 생성자는 constructor 예약어를 이용해 정의하지만, 만약 주 생성자에 별도의 수식 구문(어노테이션, 접근 제한자 등)이 없다면 05번 줄처럼 constructor 예약어는 생략할 수 있다. 위의 소스에서 3개의 클래스를 선언했는데 모두 매개변수가 없는 주 생성자를 선언한 예이며, 10~12번 줄처럼 주 생성자를 호출하면서 각 클래스의 객체를 생성할 수 있다.
매개변수가 있는 주 생성자
주 생성자를 정의할 때 매개변수를 선언하면 해당 생성자로 객체를 생성할 때 매개변수에 맞는 인수를 전달해야 한다.
01 class User1 constructor(name: String, age: Int){ }
02 class User2(name: String, age: Int){ }
03 //…
04 val user1 = User1() //에러
05 val user2 = User1("kkang", 33)
06 val user3 = User2("kim", 28)
위의 소스에서 두 개의 클래스를 선언하고 각 클래스를 선언할 때 주 생성자에 매개변수를 지정했다. 이처럼 매개변수가 있는 생성자를 호출해 객체를 생성하려면 05~06번 줄처럼 생성자의 매개변수개수와 타입에 맞추어 인수를 전달해야 한다. 그렇지 않고 04번 줄처럼 인수를 전달하지 않은 채 호출하면 컴파일 에러가 발생한다.
생성자 매개변수 기본값 명시
일반 함수처럼 생성자의 매개변수에도 기본값을 명시할 수 있다. 만약 생성자의 매개변수에 기본값을 명시하면 객체를 생성할 때 해당 매개변수 값은 대입하지 않아도 되며, 이때는 자동으로 기본값이 적용된다.
01 class User3(name: String, age: Int = 0) { }
02 //…
03 val user4 = User3("kkang", 33)
04 val user5 = User3("kkang")
위의 소스 01번 줄에 클래스를 선언하고 2개의 매개변수가 있는 생성자를 선언했다. 그런데 age 매개변수에는 기본값을 0으로 명시했다. 이렇게 하면 객체를 생성할 때 03번 줄처럼 인수 2개를 모두 전달할 수도 있고, 04번 줄처럼 기본값이 있는 두 번째 인수는 전달하지 않아도 된다.
생성자 초기화 블록 init
클래스의 생성자에 매개변수를 지정하는 것 이외에 실행문을 작성할 수도 있다. 객체 생성과 동시에 생성자가 호출되므로 클래스의 멤버 변수를 초기화하는 등 보통 초기에 수행해야 할 작업을 생성자에 작성한다. 그런데 주 생성자는 클래스 헤더에 명시하다 보니 실행문을 포함할 수 없다는 문제가 있다. 즉, 주 생성자는 { }를 가질 수 없다.
01 class User4(name: String, age: Int) { }{ //에러
02
03 }
위의 코드는 컴파일 에러이다. 그냥 봐도 이치에 맞지 않는 구문입니다. 주 생성자를 선언하면서 바로 옆에 주 생성자를 위한 실행 영역을 { }로 명시할 수는 없습니다. 그렇다면 주 생성자는 실행 영역을 가질 수 없는 걸까요? 그렇지는 않습니다. 주 생성자는 클래스 선언 영역에 작성하다 보니 실행 영역을 클래스 몸체에 init 예약어로 따로 명시하는 기법을 사용한다.
01 class User4(name: String, age: Int) {
02 init {
03 println("i am init...")
04 }
05 }
06 //…
07 val user6 = User4("kkang", 33)
07번 줄에서 클래스의 객체를 생성했다. 이때 01번 줄에 선언한 주 생성자가 호출된다. 그리고 객체가 생성될 때 클래스 내에 init 예약어로 선언한 02번 줄의 초기화 블록도 함께 실행된다. 이처럼 주생성자의 실행 구문은 클래스 내부에 init 예약어를 이용해 작성하면 된다.
생성자 매개변수 값 이용
객체를 생성할 때 전달한 값은 생성자의 매개변수로 받아서 대부분 클래스 내의 멤버(프로퍼티, 함수등)에서 이용한다. 그런데 생성자의 매개변수를 이용할 때 제약이 있다. 클래스의 초기화 블록이나 프로퍼티에서는 접근할 수 있지만, 클래스에 정의된 멤버 함수에서는 사용할 수 없다.
01 class User5(name: String, age: Int){
02 init {
03 println("i am init... constructor argument : $name .. $age")
04 }
05 val upperName = name.toUpperCase()
06
07 fun sayHello(){
08 println("hello $name") 에러
09 }
10 }
01번 줄에 선언한 주 생성자의 매개변수는 클래스 내부에서 이용할 수 있다. 그런데 02번 줄의 초기화 블록과 05번 줄의 클래스 프로퍼티에서는 이용하는 데 별문제가 없는데, 08번 줄의 클래스 내에 선언한 함수에서는 컴파일 에러가 발생한다.
그렇다면 생성자의 매개변수는 함수에서 사용할 수 없는 걸까요? 그렇지는 않다. 두 가지 방법이 있는데, 첫 번째는 생성자의 매개변수를 클래스 프로퍼티에 대입하고 함수에서는 프로퍼티로 이용하는 방법이다.
01 class User5(name: String, age: Int){
02 val name = name
03 init {
04 println("i am init... constructor argument : $name .. $age")
05 }
06 fun sayHello(){
07 println("hello $name")
08 }
09 }
10 //…
11 val user7 = User5("kkang", 33)
12 user7.sayHello()
【 실행결과 】
i am init... constructor argument : kkang .. 33
hello kkang
01번 줄에서 선언한 생성자의 매개변수를 02번 줄에서 프로퍼티에 대입하고, 이 프로퍼티를 07번 줄의 함수 내부에서 이용했다. 생성자의 매개변수를 함수에서 이용하는 두 번째 방법은 생성자 내에서 val, var를 이용해 매개변수를 선언하는 방법이다.
01 class User6(val name: String, val age: Int){
02 val myName = name
03 init {
04 println("i am init... constructor argument : $name .. ${age}")
05 }
06 fun sayHello() {
07 println("hello $name")
08 }
09 }
위의 소스를 보면 01번 줄의 생성자 매개변수를 val로 선언했다. 이처럼 생성자의 매개변수를 선언 할 때 val 또는 var를 추가하면 해당 매개변수는 클래스의 프로퍼티가 된다. 따라서 07번 줄처럼 클래스에 정의된 함수에 이용할 수 있다.
생성자의 매개변수와 클래스의 프로퍼티
생성자에는 매개변수를 선언하고 클래스 영역에는 프로퍼티를 선언할 수 있다. 그런데 여기서 발생하는 문제가 하나 있다. 만약 생성자의 매개변수명과 클래스의 프로퍼티명이 같으면 어떻게 될까? 아무 문제 없이 컴파일되고 잘 실행된다. 잘 실행되는데 무엇이 문제일까?
01 class User7(name: String, age: Int){
02 val name: String = "kim"
03 init {
04 println("i am init... constructor argument : $name")
05 }
06 fun sayHello() {
07 println("hello $name")
08 }
09 }
10 //…
11 val user9 = User7("kkang", 33)
12 user9.sayHello()
【 실행결과 】
i am init... constructor argument : kkang
hello kim
위의 소스에서 01번 줄에 선언한 생성자의 매개변수명과 02번 줄에 선언한 프로퍼티명이 name으로 같다. 컴파일 후 실행하는 데는 문제가 없다. 그런데 04번 줄의 초기화 블록에서 사용하는 name 변수는 어떤 name을 참조하는 걸까? 또한, 07번 줄의 함수에서 이용하는 name 변수는 어떤 name 을 참조하는 걸까?
초기화 블록에서 이용하는 변수는 생성자의 매개변수이다. 실행결과를 보면 객체를 생성할 때 전달한 "kkang"이라는 문자열이 출력되었다. 그런데 07번 줄의 함수에서 이용하는 변수는 클래스의 프로퍼티이다. 따라서 실행결과를 보면 02번 줄에서 선언한 프로퍼티의 변숫값 "kim"이라는 문자열이 출력 되었다.
정리하면 생성자의 매개변수명과 프로퍼티명을 같게 선언할 수 있지만, 적용되는 곳이 다르다는 것에 주의해야 한다. 따지고 보면 초기화 블록은 주 생성자의 실행 영역이므로 여기서는 생성자의 매개변수를 이용하고, 함수는 클래스의 프로퍼티를 제어하는 목적이므로 프로퍼티를 이용하는 게 당연하다.
그렇다면 생성자의 매개변수를 선언할 때 var, val을 추가하면 어떻게 될까요? 앞에서 설명한 대로 생성자의 매개변수를 var, val로 선언하면 클래스의 프로퍼티로 이용된다. 이때 클래스 영역에 매개변수와 같은 이름의 프로퍼티를 선언하면 어떻게 될까? 예상했겠지만 당연히 에러가 발생한다.
01 class User8(val name: String, age: Int){
02 val name: String = "kkang" //에러
03 val age = 10
04 }
위의 소스에서 생성자의 매개변수인 name을 val로 선언했다. 그리고 02번 줄에서 같은 이름으로 다시 클래스의 프로퍼티를 선언했다. 이렇게 하면 컴파일 에러가 발생한다.
( data 클래스 )
어떤 클래스는 내부에 특별한 로직의 함수 없이 데이터만 포함할 수 있다. 흔히 객체지향 프로그래밍에서는 이를 VO(Value-Object) 클래스라 부른다. 관련된 데이터 여러 개를 클래스로 묶어서 이용하려는 목적으로 VO 클래스를 만든다. 그런데 코틀린에서는 이런 클래스들을 조금 더 편하게 이용하라고 데이터 클래스를 제공한다. 데이터 클래스는 data라는 예약어로 선언하는 클래스이다.
data class User(val name: String, val age: Int)
일반 클래스 선언과 비교해 보면 클래스 선언 부분에 data라는 예약어가 추가된다는 점만 차이가 있다. 데이터 클래스는 다음의 조건에 맞게 선언해야 한다.
- 주 생성자를 선언해야 하며 주 생성자의 매개변수는 최소 하나 이상이어야 한다.
- 모든 주 생성자의 매개변수는 var 혹은 val로 선언해야 한다.
- 데이터 클래스는 abstract, open, sealed, inner 등의 예약어를 추가할 수 없다.
다음은 이러한 조건에 어긋나서 데이터 클래스 사용 시 에러가 발생하는 상황을 보여준다.
data class User1() //에러 : 주 생성자에 매개변수가 없다.
data class User2(name: String) //에러 : 매개변수는 있지만 var나 val로 선언하지 않았다.
data abstract class User3(val name: String) //에러 : data클래스를 abstract로 선언할 수 없다.
data class User4(val name: String, no: Int) //에러 : 주 생성자의 매개변수는 몇 개를 선언하든 상관없지만, data클래스는 모두 var나 val로 선언해야 한다.
데이터 클래스는 클래스에 정의된 데이터와 관련된 편리한 기능을 제공한다. 그런데 데이터 클래스에 선언된 모든 데이터가 아닌, 주 생성자에 매개변수로 선언된 데이터에 한해서만 제공한다. 따라서 주생성자를 꼭 정의해야 하고 주 생성자의 매개변수를 하나 이상 선언해야 하며 val이나 var 키워드로 선언해야 한다는 조건이 있다.
출처:
'⚙️Backend > Kotlin' 카테고리의 다른 글
[ Kotlin ] apply, with, let, also, run (0) | 2020.11.28 |
---|---|
[ Kotlin ] Intro (0) | 2020.09.13 |