사설
-> 프로그래밍 언어는 도구에 불과해! 어떤 언어든 변수 만들고 조건문, 반복문, ... 공식 문서 보면서 코딩하면 금방해!
어느정도 개발을 해온 사람들 중에 어떤 사람들은 간혹 위와 같은 말을 한다.
필자는 프로그래밍 언어를 공부하는데 있어서 그렇게 생각하지 않는다.
스프링 개발할 때 공부안하고 무턱대고 문서만 보면서 개발하면 빠르고 정확하게 개발할 수 있을까?
자바 문법만 익히면 자바로된 프로그램을 다 짤 수 있는건가?
경험상 그렇지 않은게 대부분이다. 프로그래밍 언어를 정확하게 배우는 것은 큰 의미가 있다.
반대로 아래와 같은 말들도 들어봤다.
-> C#하는게 나아요? 자바하는게 나아요? 코틀린은 안드로이드에서 주로 쓴다는데? 자바스크립트 유망한가요?
프로그래밍 언어를 학습하는데에 엄청나게 큰 부담을 갖고 있는 것 같은 느낌이다.
막말로 프로젝트까지는 아니어도 하나의 프로그래밍 언어를 학습하는데 1달이 안 걸린다. 그냥 공부하면 된다.
특히나 필자 같은 경우는 이번에 코틀린을 공부하면서 아주 기분이 좋았다.
내가 실제로 코틀린을 현업에서 사용하지 않을 것 같지만, 옛날에 프로그래밍 언어를 공부하던 때의 느낌도 나고 괜히 쉬운 부분을 접하면서 refresh하는 기분까지 들었다. 이 글을 읽은 분들은 부담 없이 시작해보길 바란다.
(이제는 스프링으로 서버 개발을 해도 자바가 아닌 코틀린으로 하는게 추세가 되어버렸다...)
# 변수
IDE는 Intellij를 설치했다. openjdk 11버전도 설치했다. 일단 hello world부터 찍어보자
1. 코틀린 프로젝트 만들기
-> main 함수를 작성하는 방법 2가지
- fun main(){...}
- fun main(args: Array<String>){...}
코틀린 프로젝트를 생성하고 src디렉토리 밑에 HelloKotlin.kt 파일을 만든다.
에디터에 빈 화면이 나올텐데 거기다가 프로그램 진입점인 main 함수를 만들어야하는데 방식은 위의 두 가지다.
하나는 실행할 때 arguments를 받지 않는 것이고, 하나는 받는 것이다.
자바에서는 public static void main(String[] args) 이렇게 만드는데 똑같다고 보면된다.
fun main() {
println("hello kotlin!")
}
이렇게하고 실행시키면 "hello kotlin!"이 콘솔에 찍힌다.
위의 main 함수는 JVM에서 실행되면, main함수가 있는 파일 이름을 기준으로 자바 클래스가 자동생성된다.
* 참고로 코틀린 -> 자바코드 -> 바이트코드 이런 순서로 바이트코드를 JVM이 실행하는 것이 아니다.
(코틀린 -> 바이트코드)
- package
코틀린 프로젝트는 여러 개의 모듈로 구성된다. 보통 각각의 기능을 모듈 단위로 분리해서 관리하고, 그 안에 package를 둬서 클래스/파일들을 관리한다.
다른 언어와 달리 특별한 점은 import한 클래스의 이름을 as 키워드를 이용해서 별명으로 쓸 수 있는 점이다.
import com.example.jeongpro.Person as User
fun main(){
val user = User("jdk", 28);
}
2. 변수 만들기
변수 선언 키워드는 2가지가 있다. val 과 var다. (val = value, var = variable)
- val : immutable 불변, 자바스크립트의 const, 자바의 final 키워드 처럼 재할당이 불가능하다는 게 특징이다.
- var : mutable 가변, 일반적인 변수로 재할당이 가능하다. 자바스크립트의 let 같은 것이다.
코틀린에서 변수를 선언하려면 반드시 저 둘중에 하나의 키워드를 사용해야 한다.
* 웬만하면 val 위주로 적용해서 안전성을 높이는 코딩을 지향해야하고, 꼭 가변적으로 할당되어야만 하는 변수에만 var를 사용하도록 하자.
변수 선언 방법
val username:String = "jeongpro"
"변수선언키워드 변수명: 타입 = 값" 이렇게 쓰면 된다.
특징1) 코틀린 컴파일러가 타입을 추론할 수 있는 경우에는 타입을 생략해도 된다.
val username = "jeongpro"
이렇게 하면 값이 문자열인 것을 보고 코틀린 컴파일러가 username을 String타입으로 추론한다.
특징2) 변수 선언과 할당을 동시에 하지 않을 경우에는 타입을 생략할 수 없다.
val name //불가능
val name:String //가능
name = "hello"
위와 같이 선언만 먼저 하는 경우에는 반드시 타입을 적어줘야한다.
아래와 같이 연습해본다. * 변수 출력에 대해서는 설명이 없었는데 $(달러표시)를 붙이면 사용할 수 있다.
//val var 차이
val name:String = "immutable"
var age:Int = 20
name = "jeongpro" //val로 설정했으니까 컴파일에서 에러남
age = 30
println("age : $age") // $(달러표시)로 변수를 출력할 수 있다. 자바는 println("age : " + age);로 + 연산자를 사용했었다.
//선언과 할당
var number = 10
number = 20
var number2:Int //Int라고 타입을 줬으니까 선언만 해도 괜찮음
number2 = 30
val number4 // this variable must either have a type annotation or be initialized 라는 에러가 남
변수 타입 기본형(Primitive type)과 참조형(Reference type)
코틀린에서는 참조형을 사용해서 코딩해야한다.
참조형만 사용해도 괜찮은 이유는 컴파일러가 내부적으로 기본형으로 바꿔서 최적화된 바이트코드로 만들어주기 때문이다.
(int, long, double, ... 이런 기본형 대신 Int, Long, Double, ... 같은 참조형을 써야하고 이렇게해도 내부적으로 잘 바꿔서 쓴다)
# 자료형
1. 정수 자료형
Long, Int, Short, Byte가 있다.
변수를 선언할 때 타입을 별도로 적지 않으면 코틀린이 타입을 추론해서 지정해준다고 했다.
그렇다면 100은 무슨 타입으로 추론하고 99933322233344455는 무슨 타입으로 추론할까?
답은 100은 Int로 추론하고 99933322233344455는 Long타입으로 추론한다.
코틀린은 Int범위 내에 있으면 Int로하고 그 이상 Long범위에 있으면 Long으로 추론한다.
val ex1 = 100L 이렇게 접미사 L을 붙이면 Long타입으로 추론한다. (val ex1:Long = 100 이건 추론이 아니라 명시적이기 때문에 당연히 된다.)
정수 타입에서는 Int가 기본이다.
Short 범위내 정수라하더라도 Short으로 추론하지 않고 Int로 추론한다.
* 부호가 없는 정수 타입(ULong, UInt, UShort, UByte)도 있지만 embedded system에 코딩하는게 아니라면 잘 사용하지 않으므로 자세히 살펴보지 않는다.
-> underscore(_)를 값에 추가할 수 있다. 이렇게하면 코드내에서 자리수를 구분하는데 도움을 줄 수도 있다. (언더스코어가 결과에는 아무런 지장을 주지 않는다)
val myLong = 99_933_322_233_344_455 //Long으로 추론
val ex1 = 0x0F //16진수표기 15
val ex2 = 0b00001111 //2진수표기 15
2. 실수 자료형
기본적으로 double로 추론. 값에 F를 붙이면 Float타입으로 추론한다.
3. 논리 자료형
val isOpen = true 이렇게하면 자동으로 Boolean으로 추론한다.
4. 문자 자료형
''(single quote)으로 한 문자를 지정하면 Char로 추론한다.
5. 문자열 자료형
문자열 타입은 다른 것들과 다르게 기본형이 아니라 참조형 타입이다.
값에 문자를 배열하면 String타입으로 추론한다.
특징적인 것은 자바와 마찬가지로 힙에 StringPool을 공유한다는 것이다.
var string1 = "hello"
var string2 = "hello"
println(${string1 === string2}) //true
//string1과 string2가 참조하는 주소값이 같음
* $(달러표시)를 이용해서 변수값이나 표현식을 나타낼 수 있다.
var a = 2
var str1 = "a: $a" // a: 2
var str2 = "a: ${a+1}" // a: 3 중괄호로 묶으면 표현식이 된다.
문자열 내에 "(쌍따옴표)나 $(달러표시)를 쓰고 싶으면 백슬래시(\)를 앞에 붙여서 사용하면 된다.
val expression = "\"hello\" this is \$10"
백슬래시 대신 ${'"'} 이렇게 써도 쌍따옴표를 적용할 수 있다. (참고용)
- """ 문자열 사용법 (문자열 그 자체 그대로 표현하는 방법)
val num = 10
val formattedString = """
var a = 11
var b = "hello kotlin"
"""
println(formattedString)
결과 :
var a = 11
var b = "hello kotlin"
- 자료형에 별명 붙이기
typealias Username = String
val user:Username = "jeongpro"
typealias 키워드를 이용해서 String타입에 Username이라는 별명을 붙여서 사용했다. (추후에 많이 사용된다는데 아직은 잘 모르겠다)
# 자료형 검사
* 코틀린 null
코틀린은 변수를 사용할 때 반드시 값이 할당되어 있어야한다! 라는 원칙이 있다. (null을 싫어함)
지금까지 위에서 작성한 코드는 전부다 변수에 null을 허용하지 않는 방식이다.
만약 null을 허용하고 싶으면 물음표(?)기호를 타입 뒤에 붙여서 변수를 선언하면 된다.
//null문제
val str:String = "helloworld"
str = null//컴파일러에서 이미 에러
//타입 뒤에 ? 붙이면 null이 할당될 수 있는 변수라는 의미
var str2:String? = "helloworld"
str2 = null
println(str2) //null 출력
* non-null 단정 기호와 세이프 콜(safe call)
//non-null단정 기호와 세이프 콜
var nullableString:String? = "hello kotlin"
nullableString = null
//println("nullableString : $nullableString , length : ${nullableString.length}") // .에서 컴파일 에러남
//String? 타입에서는 ?.(세이프콜) 또는 !!.(non-null 단정기호)만 쓸 수 있다고 나온다.
//- safe call이란 말 그대로 안전하게 호출하도록 도와주는 기법이다.
println("nullableString : $nullableString , length : ${nullableString?.length}")
//출력 값 : nullableString : null , length : null
세이프 콜은 객체가 null이면 해당 객체의 메서드 또는 변수에 접근하지 않고 null을 리턴해주는 기능이다.
non-null 단정 기호를 쓰면 null이 아니라고 단정하는 의미로 null 체크없이 접근하고 NullPointerException을 발생시켜버리므로 쓰지 않는 것이 좋다.
* 엘비스 연산자(?:)
위에서 배운 세이프 콜과 엘비스 연산자를 같이 사용하면 아주 아름다운 null 처리가 가능하다.
var testString:String? = "hello kotlin"
testString = null
println("testString length : ${testString?.length ?: -1}")
testString이 세이프 콜로 length에 접근하지 않고 null을 리턴한다 그랬으니까 엘비스 연산자 앞의 값은 null이다.
엘비스 연산자는 값이 null이면, 뒤에 미리지정해놓은 값을 리턴하는 연산자다. 따라서 출력은 -1로 지정해놓았으니까 -1이 나올 것이다.
if 문으로 null검사를 일일이 할 필요가 없어지고 처리까지 한 줄에 되므로 가독성도 좋아진다.
* 자료형 변환
자바에서는 int값을 double 변수에 할당하면 자동으로 타입이 변환되었다.
int a = 3;
double d = a; //3.0으로 자동변환
코틀린은 자동으로 형변환을 시켜주지 않는다. 오히려 타입 미스매치 에러를 내면서 실수(?)를 철저하게 방지한다.
그래서 코틀린은 형변환 메서드를 제공한다.
val intValue:Int = 3
val doubleValue:Double = intValue.toDouble()
val result = 1L + 3 //Long으로 형변환 -> 표현범위가 더 큰 쪽으로 변환된다.
* 기본형과 참조형 타입 비교 원리
값 비교 : == , 값이 같으면 true 다르면 false
참조 주소 비교 : ===, 값과 상관없이 참조 자체가 동일하면 true 다르면 false (값과 상관없지만 참조 자체가 동일하면 값도 같다...)
val one: Int = 128
val two: Int = 128
println(one == two) //true
println(one === two) //true
//참조 비교
val three: Int = 128
val four: Int? = 128
println(three == four) //true
println(three === four) //false
//Int형으로 선언된 three는 128값 자체가 스택에 저장되지만, Int?형으로 선언된 four는 128이 힙에 저장되어있고 그것의 주소로 저장한다.
val data1:Int = 128
val data2 = data1;
println(data1 == data2)//기본형으로 int로 변환되어 값이 동일하므로 true
val data3:Int? = data1 //128이라는 값을 힙에 할당하고 그것을 가리킨다.
val data4:Int? = data1 //128이라는 값을 힙에 할당하고 그것을 가리킨다.
val data5:Int? = data3
println(data3 == data4)//true 값만 비교했는데 두 개의 값은 128로 같다
println(data3 === data4)//false 각각 값은 128로 동일하지만 힙에 생긴 128은 각각생기기 때문에 다른 주소를 가리키므로 주소비교는 false
println(data3 === data5)//true data3는 128을 힙에 할당하고 그것을 가리키고 있다. data5는 data3를 가리키는데 data3가 힙의 128의 주소를 가리키고 있으므로
//복사되었을 때 같은 힙에 있는 128을 가리킨다.
메모리에 힙에 생성되느냐 스택에 생성되느냐를 일단 알아야 하는데 간략하게 설명하면 null을 허용하지 않는 변수는 참조형을 사용하더라도 내부적으로 기본형으로 값을 갖게 되는데 그것이 스택에 직접할당된다.
그러나 null을 허용하는 변수는 기본형으로 변환되는 참조형일지라도 자기만의 값을 힙에 할당하고 그것을 가리키는 주소를 스택 공간에 할당한다.
* 기본적으로 null을 허용한다는 것 그 자체로 그 변수를 객체로 본다는 뜻이다. 기본형에는 null이란 것을 할당할 수 조차 없다.
* -128 ~ 127의 정수 값은 캐시에 저장되어 참조된다.
따라서 위의 예제에서 값을 만약 10으로 했다면 10이라는 값자체가 캐시에 저장되고 모든 변수들은 다 그 캐시의 주소값을 저장하고 있을 것이다.
그래서 10으로 테스트하면 data1 ~ data4까지 === 비교를 하면 다 true가 나올 것이므로 참고하면 된다.
* 스마트 캐스트
어떤 값이 정수일 때도 있고 실수일 때도 있으면 어떻게 해야할까?
Number 타입이라는 것이 있다. (특수한 객체)
Number 타입으로 정의된 변수에는 정수, 실수 둘다 들어갈 수 있다.
// * 스마트 캐스트
var sc:Number = 3.14
println(sc) //3.14 Double
sc = 3
println(sc) //3 Int
sc = 30L
println(sc) //30 Long
sc = 10.1F
println(sc) //10.1 Float
* 자료형 검사할 때 is 키워드
val cup = 3
if(cup is Int){
println("Int Type")
}
if(cup !is Int){
println("Not a Int Type")
}
if(!(cup is Int)){
println("Not a Int Type")
}
* 자료형 스마트 캐스트
val x:Any //Any는 자바의 Object와 유사하고(같지않음) 모든 타입의 부모 타입이다. Any를 이용하면 자료형을 결정하지 않은 채로 변수를 선언할 수 있다.
x = "jdk" //x에 문자열이 들어가면서 자동으로 String으로 캐스팅? 아니다. 아직 Any타입이다.
if(x is String){ //is 키워드로 검사될 때 자동으로 스마트캐스팅 되었으므로 해당 조건문을 탄다
println(x)
}
* as 키워드에 의한 스마트 캐스트
as는 형변환이 가능하지 않으면 예외를 발생시킨다.
var y:Int = 3
val t:String = y as String //y가 null이 아니면 String으로 형변환되어 t에 할당된다. 만약 null이라면 형변환할 수 없으므로 exception 발생
//null이 발생할 가능성이 있다면 예외를 피하기 위해 아래와 같이 물음표를 붙여서 해결가능
val tt:String? = y as? String //y가 null이면 as키워드에 접근하지 않고 null 리턴. 그러면 tt는 null이 가능하니까 null 저장
# 연산자
코틀린 연산자는 대부분의 다른 언어와 거의 똑같다.
+ - * / % (사칙연산, 나머지)
+= -= *= = %= (사칙연산 + 대입연산 축약형)
--a a-- ++a a++ (증감연산, 앞에 있냐 뒤에 있냐 차이 알아야함(타언어랑 동일))
> >= < <= (비교연산)
== != === !==
&& || !
-> 비트연산은 임베디드 시스템아니면 잘 안쓰므로 생략
* 잘못된 부분이 있을 수 있습니다. 지적해주시면 고쳐서 공유할 수 있도록 하겠습니다.
참고 도서
- Do it 코틀린 프로그래밍 (이지스퍼블리싱)