개요
코틀린을 쌈싸먹자고 다짐한지 벌써 1달.. 놀랍게도 앞서 올렸던 포스트 이후로 단 한 번도 코틀린을 공부하지 않았다 😇
프레임워크는 공부하는게 마냥 재밌는데, 프로그래밍 언어 (코틀린)는 왜 손이 잘 안갈까 고민을 하다가, 공식 문서를 보고 목차 하나하나씩 따라가는 공부법이 좀 루즈해서 관심이 가지 않는다는 결론을 내렸다.
아예 내가 개발을 처음 해보고, 사용할 수 있는 프로그래밍 언어가 하나도 없다면 물론 공식 문서를 보며 차근차근 이해하는게 좋다. 하지만 내가 이미 몇 가지 프로그래밍 언어를 다룰 줄 안다면, 그리고 새로운 언어로 당장 개발을 시작해야 한다면 꼭 고급 문법들을 알아야 할 필요는 없을 것 같다.
(Ex. Java로 간단한 계산기 프로그램을 만든다고 가정했을 때, synchronized, volatile 같은 키워드를 사용하진 않을 거니까..! )
따라서 이번 포스팅은 공식 문서를 읽으며 따라하기 보다는, 직접 간단한 기능을 만들면서 그 기능을 코틀린으로 구현하는 방법은 무엇인지 찾아가는 식으로 진행해보려고 한다! 이렇게 코틀린과 좀 친해진 다음에 내부 동작 원리나 고급 문법 등을 공부하면 딱 재밌을 것 같다 😎
기능 설명
모든 프로그래밍 언어에 공통적으로 존재하는 변수, 대입문, 입출력, 연산자, 반복문, 조건문, 함수 개념을 모두 사용해 볼 수 있는, 간단한 계산기 프로그램을 만들 것이다. 플로우는 다음과 같다.
- 프로그램을 실행하면 계산기 메뉴가 출력된다. (= 출력)
- 원하는 연산 메뉴를 입력한다. (= 입력, 변수, 대입)
- 만약 -1을 입력했다면 프로그램을 종료한다. 그렇지 않다면 피연산자를 입력한다. (= 조건문, 입력)
- 입력받은 피연산자를 이용해 선택 메뉴의 연산을 수행한다. (= 조건문, 연산자, 함수)
- 결과를 출력한다. (= 출력)
- 다시 1부터 반복한다. (= 반복문)
일단 기능 구현 먼저 하고, 이후에 테스트 코드, 함수형 프로그래밍까지 천천히 붙여서 개선해보자.
Kotlin 버전 계산기 프로그램
List 선언과 초기화, 범위 연산자, 클래스 메서드 (메소드)
계산기 클래스부터 만들었다. 계산기 클래스의 메서드를 이용해 메뉴를 출력하고, 잘못된 입력에 대한 경고문을 리턴할 것이다.
class Calculator {
fun printMenu() {
println("=================")
println("1. 덧셈")
println("2. 뺄셈")
println("3. 곱셈")
println("4. 나눗셈")
println("-1. 종료")
println("=================")
}
fun printNotice(menu: Int) {
if (menu !in listOf(1, 2, 3, 4, -1)) {
println("입력한 값은 유효한 숫자가 아닙니다.")
}
}
}
와! 일단 굉장히 코드가 깔끔하다! print 구문이 굉장히 간략화 된 것은 이전 포스트에서 언급했으니, 이번엔 리스트의 요소에 해당하는지 확인하는 코드에 주목해보자. printNotice의 자바 버전은 아래와 같다.
public void printNotice(final int menu) {
List<Integer> validValues = Arrays.asList(1, 2, 3, 4, -1);
if (!validValues.contains(menu)) {
System.out.println("입력한 값은 유효한 숫자가 아닙니다.");
}
}
딱 보면 List의 생성과 초기화 방법에 차이가 있고, 특정 값이 List에 포함되는지 파악하는 방법에도 차이가 있다. 코틀린에서 사용할 수 있는 List의 종류와 각각의 선언, 초기화 방법은 다음과 같다.
종류 | 이름 | 선언 | 초기화 |
Immutable List | 불변 리스트 | List<T> | listOf(vararg elements:T) |
Mutable List | 가변 리스트 | MutableList<T> | mutableListOf(vararg elements: T) |
ArrayList | 배열 리스트 | ArrayList<T> | arrayListOf(varag elements: T) |
LinkedList | 연결 리스트 | LinkedList<T> | linkedListOf(vararg elements: T) |
동시에 코틀린은 범위 연산자 in을 이용해 보다 쉽게 원소의 포함 여부를 확인할 수 있다. 위 코드에서는 List를 이용해 입력 값을 체크했지만, .. 연산자를 활용하여 아래처럼 사용할 수도 있다.
fun printNotice(menu: Int) {
if (menu !in (1..4)) {
println("입력한 값은 유효한 숫자가 아닙니다.")
}
}
이번 예제에서는 1~4 뿐만 아니라 -1도 사용할 수 있기에 listOf를 사용했다. 연속된 값의 범위를 체크하고 싶다면 .. 를 사용하면 된다.
입력
사용자가 값을 입력하면 변수에 입력값을 할당하는 코드를 작성하자.
fun getInput(): Int {
println("숫자를 입력해주세요.")
val num = readln().toIntOrNull()
return num ?: 0
}
위 코드의 자바 버전은 아래와 같다.
public int getInput() {
System.out.println("숫자를 입력해주세요.");
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
int num;
try {
num = Integer.parseInt(input);
} catch (NumberFormatException e) {
num = 0;
}
return num;
}
자바에서 입력을 받기 위해선 Scanner이나 BufferedReader 등을 이용할 수 있다. 다만 두 친구 모두 Scanner 객체 또는 BufferedReader를 생성한 후에야 입력을 받을 수 있는데, 코틀린은 입력 객체 없이 readln() 메서드를 이용해 바로 값을 받을 수 있어 편리했다.
단, BufferedReader와 Scanner 등을 이용하면 좀 더 다양한 읽기 메서드를 사용할 수 있고, 버퍼를 이용해 읽기 성능을 향상시킬 수도 있다 (자바로 코테 문제 풀때 버퍼리더를 많이들 사용하는 이유). 즉 코드량이 많아지는 대신 좀 더 커스터마이징이나 유연한 처리를 수행할 수 있다는 점이 장점인 것 같다 🤔
그리고 또 주목할 사항은, 코틀린이 자바보다 Null을 처리하는 것이 훨씬 쉽다는 점이다.
자바는 Integer 등 Wrapper 클래스를 사용하면 변수에 Null을 할당할 수 있지만, primitive type을 사용하면 Null을 할당할 수 없다. 하지만 코틀린은 primitive type과 Wrapper 타입을 따로 구분하지 않는다.
또한 코틀린은 ?: (엘비스 연산자)를 지원해서, Null인지 아닌지에 대한 체크를 보다 간단하게 처리할 수 있다. try-catch로 Null로 인한 exception을 핸들링해주거나 조건문으로 따로 확인하는게 아니라 간단한 연산자만 이용하면 되기에 좀 더 명시적이라고 느껴진다 👀
연산
사칙연산 메서드를 만들었다. 이건 자바랑 다른게 없으니 패스!
fun add(a: Int, b: Int): Int {
return a + b
}
fun subtract(a: Int, b: Int): Int {
return a - b
}
fun multiply(a: Int, b: Int): Int {
return a * b
}
fun divide(a: Int, b: Int): Int? {
if (b == 0) {
println("0으로 나눌 수 없습니다.")
return null
}
return a / b
}
반복문
쏘이지.... 자바와 동일하게 for문, while문, forEach문 등을 활용할 수 있다.
while(true) {
// 내용물
}
while문은 완전 똑띠지만, for문을 사용할 때는 포맷이 조금 달라진다. 예를 들어보자.
// 인덱스 이용
for (int i = 0; i < 10; i++) {
// 내용
}
// 배열 이용
int[] numbers = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int number : numbers) {
// 내용
}
자바에서의 for 문 사용 예시이다. 인덱스를 이용하거나, 혹은 for each (enhanced for)를 이용해 위와 같이 구현할 수 있다.
// 1
for (i in 0 until 10) {
// 내용
}
// 2
for (i in (0..9)) {
// 내용
}
// 3
val numbers = intArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
for (number in numbers) {
// 내용
}
코틀린에서의 for문 사용 예시이다. 위에서 배운 in 키워드를 이용해 for문을 보다 간단하게 사용할 수 있다.
조건문
코틀린은 if문과 when 두개의 조건문을 제공한다. if문 자체는 일반적인 if문과 동일하지만, 특이한 점은 if 문에서 값을 리턴할 수 있다는 것이다.
// Java
int num;
if (a > b) {
System.print.out(a);
num = a;
} else {
System.print.out(a);
num = b;
}
// Java 삼항 연산자 버전
int num = (a > b) ? a : b;
// Kotlin
val num = if (a > b) {
println(a)
a
} else {
println(b)
b
}
삼항 연산자를 지원하지 않는게 좀 신기한데, 어쨌든 위와 같은 방법으로 조건문 안에서 값을 리턴할 수 있다. 블럭 내부의 코드 중 제일 마지막 줄의 값을 리턴한다고 한다.
그리고 when 문. 코틀린은 switch 문 대신 when 문을 지원한다.
var result: Int? = 0
when(menu) {
1 -> result = calculator.add(firstNum, secondNum)
2 -> result = calculator.subtract(firstNum, secondNum)
3 -> result = calculator.multiply(firstNum, secondNum)
4 -> result = calculator.divide(firstNum, secondNum)
}
case는 '->'로, default는 'else'로 표현할 수 있다. if문과 마찬가지로 값을 바로 리턴하는 것도 가능하다.
이렇게 해서 변수, 대입문, 입출력, 연산자, 반복문, 조건문, 함수 개념을 이용한 사칙연산 계산기 프로그램을 Kotlin을 이용해 작성해보았다.
좀 더 코틀린스럽게 고쳐보자!
위 코드는 동작하긴 하지만, 코드 자체가 예쁜 느낌은 없다. 진짜 와르르 우당탕탕 작성한 코드..@ 한번 코틀린만의 문법을 이용해서 계산기 프로그램을 리팩토링 해보자.
함수 구조 수정하기
코틀린은 함수 body 부분이 단일 표현식이라면 중괄호를 생략할 수 있다. 따라서 Calculator의 계산 함수들을 아래와 같이 수정할 수 있다.
fun add(a: Int, b: Int): Int = a + b
fun subtract(a: Int, b: Int): Int = a - b
fun multiply(a: Int, b: Int): Int = a * b
하지만 나눗셈 함수의 경우, 0으로 나눌 수 없다는 예외처리가 필요하기에 다른 함수들처럼 단순하게 쓸 수는 없다. 나눗셈 함수를 구현하기 위해 필요한 조건은 다음과 같다.
- b가 0이 아니라면 넘어가고, 0이라면 에러메세지를 출력하고 null을 리턴한다
- a와 b를 이용해 나눗셈 연산을 수행한다.
우리는 아까 코틀린의 if 문이 return 값을 가질 수 있다는 것을 배웠다. 이를 이용해 해당 조건을 충족하게 함수를 수정해보자.
fun divide(a: Int, b: Int): Int? =
if (b != 0) {
a / b
} else {
println("0으로 나눌 수 없습니다.")
null
}
이전과 동일한 조건문을 사용했지만, 코틀린의 if문 반환값을 이용해 단일 표현식으로 함수를 축약할 수 있게 된다.
Sequence
자바 개발자라면 스트림을 한 번쯤은 사용해봤을 것이다. 컬렉션을 처리하거나 컬렉션을 이용해 다양한 작업을 수행할 수 있는 기능인데, 연결된 여러 개의 메소드를 순차적으로 수행하여 최종 연산 결과를 리턴할 수 있다.
코틀린에는 스트림과 비슷한 Sequence가 있다. 코틀린 공식 문서는 Sequence에 대해 아래와 같이 언급했다.
시퀀스는 연산의 중간 결과를 반환하지 않아도 되기에 전체 컬렉션 처리 성능이 향상된다는 장점이 있지만, 최종 메서드가 실행 될 때에야 전체가 실행된다는 Lazy한 컨셉 때문에 작은 컬렉션을 처리할 때는 오히려 오버헤드가 증가할 수 있다는 단점이 있다.
우리가 만든 계산기 프로그램은 무한 반복을 돌면서 한 차례가 끝나면 결과를 리턴해야 하는 특징을 갖는다. 한번 시퀀스를 이용해 리팩토링 해보자.
fun main(args: Array<String>) {
// 계산기 객체 생성
val calculator = Calculator()
// 무한반복 돌면서 연산 수행 -> -1 입력하면 종료
generateSequence { calculator.printMenu(); readln().toInt() }
.takeWhile { it != -1 }
.forEach { menu ->
calculator.printNotice(menu)
val firstNum = calculator.getInput()
val secondNum = calculator.getInput()
val result = when (menu) {
1 -> calculator.add(firstNum, secondNum)
2 -> calculator.subtract(firstNum, secondNum)
3 -> calculator.multiply(firstNum, secondNum)
4 -> calculator.divide(firstNum, secondNum)
else -> null
}
result?.let { println("결과는 $it 입니다.") }
}
}
새로 나온 개념들을 하나씩 살펴보자.
- generateSequence : 시퀀스 생성 함수.
- 시퀀스의 각 요소가 printMenu, readln을 가지게 생성 -> 반복할 때마다 메뉴를 출력하고 값을 입력받게 한다.
- takeWhile : 시퀀스 요소를 조건에 따라 수행.
- 위에서 readln으로 입력받은 값, it이 -1이면 종료
- forEach : 시퀀스 각 요소에 대해 작업 수행
- when : 조건문 안에서 값을 리턴
- 결과 값이 null이 아닌 경우에만 값을 출력하게 처리
와! 이렇게 해서 계산기 프로그램의 리팩토링까지 끝마쳤다. 코틀린의 다양한 요소를 이용해 더 개선할 수도 있겠지만, 일단은 여기서 스탑 ^__^ 전체 코드는 아래 github에서 확인할 수 있다.
https://github.com/EunjiShin/kotlin-study
마무리
오늘은 프로그래밍 언어마다 존재하는 베이직한 요소들을 전체적으로 활용해서 계산기 프로그램을 만들고, 코틀린에만 존재하는 기능을 이용해 리팩토링까지 해봤다. 간단한 기능이지만 코틀린을 이용해 뭔가를 구현하는 과정을 엿볼 수 있어 재밌었다 😎
다음에는 오늘 짠 코드에 대한 테스트 코드를 작성하거나, 간단한 CRUD 기능을 코프링으로 구현해볼까 한다 👍
레퍼런스
- Kotlin sequence : https://kotlinlang.org/docs/sequences.html#construct
'Develop > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린을 쌈싸먹어 보자 (1) (1) | 2023.06.07 |
---|