Atomic Kotlin (2)
아토믹 코틀린을 읽으면서 새로 알게 된 내용, 애매한 부분을 정리함
2부: 객체 소개
코틀린은 객체 지향과 함수형을 혼합한 하이브리드 언어이다.
16. 객체는 모든 곳에 존재한다.
객체는 프로퍼티(val, var)를 사용해 데이터를 저장하고, 함수를 사용해 이 데이터에 대한 연산을 수행한다.
- 용어 정리
- 클래스 class 새로운 데이터 타입의 기초가 될 프로퍼티와 함수를 정의. == 사용자 정의 타입 이라고도 함.
- 멤버 member 클래스에 속한 프로퍼티 or 함수
- 멤버 함수 함수 안에 정의됨. 특정 클래스에 속한 객체가 있어야만 사용 가능
- 객체 생성 클래스에 해당하는 val, var 값을 만드는 과정. “클래스의 인스턴스를 생성한다” 라고도 함.
- 객체에 대해 멤버 함수를 호출하는 과정을 ‘메시지를 보낸다’ 라는 말로 설명 가능
- 클래스를 잘 정의하면 이해하기 쉬운 코드를 작성 가능하다.
17. 클래스 만들기
- 코틀린은 메서드라는 용어를 채택하지 않고, 대신 함수라는 표현을 언어 전반에서 계속 사용한다.
- 코틀린의 함수적인 특성 강조 및 메서드/함수의 구분이 혼란을 야기할 수 있기 때문.
꼭 구분해야 하는 경우 다음과 같은 용어를 사용한다.
- 멤버 함수: 클래스에 속한 함수
- 최상위 함수: 클래스에 속하지 않은 함수
- this
- 멤버 함수 호출 시, 코틀린은 객체를 가리키는 reference를 함수에 전달해 대상 객체를 추적한다. 멤버 함수 내부에서
this
라는 이름으로 이 참조에 접근 가능하다.
코드에서 불필요한
this
의 명시는 다른 프로그래머에게 혼란을 줄 수 있다.
18. 프로퍼티
프로퍼티는 클래스에 속한 var 나 val 이다.
프로퍼티를 정의함으로써 클래스 안에서 상태를 유지함. 함수를 한두 개 별도로 작성하는 대신 클래스를 작성하는 주된 이유.
각 객체는 프로퍼티를 저장할 자신만의 공간을 따로 할당받음
- 최상위 프로퍼티
- 클래스 밖에서는 멤버 함수와 프로퍼티를 모두 한정시켜야 한다. val은 변경할 수 없으므로 정의해도 안전하지만, var인 최상위 프로퍼티를 선언하는 일은 안티패턴으로 간주된다.
-> 프로그램이 복잡해질 수록 가변 상태에 대해 추론이 어려움. -> 누구나 접근 가능해져서 제대로 변경된다고 보장 불가능.
var과 val은 객체가 아닌, 참조를 제한한다. var을 사용하면 참조가 가리키는 대상을 다른 대상으로 다시 엮을 수 있지만, val은 그럴 수 없다.
19. 생성자
constructor에 정보를 전달해 새 객체를 초기화 가능하다.
다음과 같은 코드에서, name 프로퍼티에 val, var을 지정하지 않았기 때문에 name은 Alien의 생성자에서만 접근 가능하다.
1
2
3
class Alien(name: String){
val greeting = "Poor $name!"
}
20. 가시성 제한하기
작성한 코드를 며칠, 몇 주 동안 보지 않았다가 다시 살펴보면 그 코드를 작성하는 더 좋은 방법이 보일 수도 있다.
: 리팩터링을 하는 주된 이유이다. 라이브러리 생산자는 자신이 변경한 내용이 클라이언트의 코드에 영향을 끼치지 않는다는 확신을 바탕으로 라이브러리를 자유롭게 수정하고 개선할 수 있어야 한다.
따라서 소프트웨어를 설계할 때 일차적으로 고려해야 할 내용은 다음과 같다.
변화해야 하는 요소와 동일하게 유지되어야 하는 요소를 분리하라.
코틀린은 가시성을 제어하기 위해 public, private, protected, internal
등의 변경자를 제공.
- 코틀린의 default는 public 이지만, 의도를 명확히 드러내기 위해 public을 써야 하는 경우가 가끔 있음.
- private: 정의를 변경/삭제하더라도 클라이언트 프로그래머에게는 직접적인 영향이 없어야함. 최상위 프로퍼티일 시 같은 파일, 클래스 멤버일 시에 같은 클래스에 속한 멤버 외에 접근 불가.
내부 구현을 노출시켜야 하는 경우를 제외하고 프로퍼티를 private으로 만들자.
- internal: 정의가 포함된 모듈 내부에서만 접근 가능. 멀티 모듈, 라이브러리 사용 시 유용하게 사용 가능하다.
21. 패키지
프로그래밍에서 근본적인 원칙은 DRY. 즉, 반복하지 말라는 의미를 지닌 약자로 나타낼 수 있다.
import
키워드로 다른 파일에 정의된 코드를 재사용할 수 있다.as
키워드로 임포트 시에 이름을 변경 가능하다. 라이브러리에서 이름을 잘못 선택했거나 이름이 너무 길 때 유용하다.
1
2
3
4
5
import kotlin.math.PI as circleRatio
fun main() {
println(circleRatio)
}
- 코드 안에서 임포트한 이름의 패키지 경로를 전부 다 쓸 수도 있음
파일 이름이 클래스 이름과 같아야 하는 자바와 달리, 코틀린에서는 소스 코드 파일 이름으로 아무 이름이나 붙여도 좋다. 패키지도 아무 이름이나 선택할 수 있지만, 경로와 같이 하는 게 좋음.
22. 테스트
프로그램을 빠르게 개발하기 위해 지속적인 테스트는 필수
- 코드의 올바름을 확인하기 위해 println을 사용하는 것은 부실한 접근 방법이다. 매번 출력을 자세히 살펴보고 올바른 출력인지 확인해야 하기 때문.
테스트 시스템의 종류
- JUnit : 자바에서 가장 널리 쓰이는 테스트 프레임워크. 코틀린도 유용하게 사용가능하다.
- 코테스트 (Kotest) : 코틀린 전용으로 설계됨. 코틀린 언어의 여러 기능을 살려서 작성됨.
- 스펙 프레임워크 : 명세 테스트 (specification test) 라는 다른 형태의 테스트 제공.
이 책에서는 자체 개발한 atomictest 프레임워크를 사용한다. 이 시스템은 식의 예상 결괏값을 보여주고, 실행될 수 있음을 알 수 있도록 출력을 제공한다.
테스트에서 기본이 되는 요소는 eq
와 neq
이다.
이는 식 eq 예상값
과 같이 사용하여, 식과 예상값이 동등하지 않다면 오류를 보여준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import atomictest.*
fun main() {
val v1 = 11
val v2 = "Ontology"
// 'eq' means "equals":
v1 eq 11 //11 출력
v2 eq "Ontology" //Ontology 출력
// 'neq' means "not equal"
v2 neq "Epistemology" // Ontology 출력
// [Error] Epistemology != Ontology 출력
v2 eq "Epistemology"
}
eq
와 neq
는 AtomicTest에 정의된 기본 중위 함수다.
trace
객체는 출력을 저장해 나중에 사용할 수 있도록 한다.
1
2
3
4
5
6
7
8
9
10
fun main() {
trace("line 1")
trace(47)
trace("line 2")
trace eq """
line 1
47
line 2
"""
}
결과
1
2
3
line 1
47
line 2
프로그램의 일부분인 테스트
코드 작성 전 테스트를 작성해 실패시킨 후, 나중에 테스트를 통과하도록 코드를 작성하는 방법을 테스트 주도 개발(Test Driven Development, TDD)이라고 한다. TDD는 자신이 생각하는 대상을 정말 테스트하고 있는 지 확실히 알 수 있다.
테스트 가능하도록 코드를 작성하면, 코드를 작성하는 방식 또한 달라진다. 테스트를 위해 함수가 무언가를 반환하도록 하고, 파라미터를 받아 출력만 만들어내고 다른 일은 하지 않는 함수를 사용하면 설계가 더 나아지는 경향이 있다.
23. 예외
- 예외적인 상황과 일반적인 문제를 구분하는 것이 중요.
- 문제를 처리하기 위한 충분한 정보가 현재 맥락에 존재 » 일반적인 문제
- 현재 맥락에서 계속 처리 불가 » 예외
예외가 던져지면 실행 경로 (더 이상 진행 불가)가 중단되고, 예외 객체는 현재 문맥을 벗어남.
예외를 고의적으로 throw 할 경우, 디폴트 ArithmeticException
을 던지는 것 보다 IllegalArgumentException("Months can't be zero")
와 같이 구체적인 예외 메세지를 추가하는 것이 유지 보수에 좋다.
24. 리스트
List는 다른 객체를 담는 컨테이너 객체다.
List는 표준 코틀린 패키지에 들어 있기 때문에 import할 필요 없음
List.sorted()
: 원본 원소들을 정렬한 새로운 리스트 반환 List.sort()
: 원본 리스트를 직접 정렬함
sorted, reversed와 같이 원본 객체를 바꾸지 않는 접근 방식을 코틀린 라이브러리에서 일관성 있게 볼 수 있다. 코드 작성 시에도 이런 패턴을 따르기 위해 노력하자!
파라미터화한 타입
- 타입 추론은 코드를 깔끔하게 해주지만, 이해하기 쉽게 코드를 작성하기 위해서는 직접 타입을 명시해야 한다.
val list = listOf()
보다val list : List<String> = listOf()
이 이해하기 쉬움- 타입 파라미터 <> : ‘이 컨테이너는 파라미터 타입의 객체를 담는다’고 말하는 방법
읽기 전용과 가변 List
- 가변 List (mutableList) 는 명시적으로 표시해야함. listOf()는 읽기 전용 리스트
+=
연산자mutableList += 'A'
==mutableList.plusAssign('A')
- mutableList는 컴파일러가 += 연산자를 plusAssign()으로 번역해주므로, 변수에 다른 리스트가 재대입 되는 일이 없다.
val list += 'B'
: list에 ‘B’를 합친 새로운 리스트를 생성하려고 시도하므로 컴파일 오류var list += 'C'
: 새로운 리스트 생성 후 list에 재대입
25. 가변 인자 목록
vararg 키워드는 길이가 변할 수 있는 인자 목록을 만든다
1
2
3
4
5
6
7
8
9
package varargs
fun v(s: String, vararg d: Double) {}
fun main() {
v("abc", 1.0, 2.0)
v("def", 1.0, 2.0, 3.0, 4.0)
v("ghi", 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
}
- vararg 키워드는 listOf처럼 임의의 길이로 인자를 받을 수 있는 함수를 정의할 수 있다.
- 함수 정의에는 vararg로 선언된 인자가 최대 하나만 있어야 한다. (일반적으로 마지막에 위치)
- 모든 인자는 지정한 타입에 속하고, d는 Array로 취급됨
일상적인 프로그래밍에서 간단한 시퀀스는 List를 사용하고, 서드파티 API가 Array를 요구하거나 vararg를 다뤄야 하는 경우에만 Array를 쓰자.
- vararg가 필요한 위치에 스프레드 연산자
*
를 사용해 임의의 타입 Array를 넘길 수 있다.1 2 3 4 5 6 7
fun main() { val array = intArrayOf(4, 5) sum(1, 2, 3, *array, 6) eq 21 val list = listOf(9, 10, 11) sum(*list.toIntArray()) eq 30 }
- 스프레드 연산자는 vararg로 받은 파라미터를 다시 다른 vararg를 요구하는 함수에 전달할 때 특히 유용하다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun first(vararg numbers: Int): String {
var result = ""
for (i in numbers) {
result += "[$i]"
}
return result
}
fun second(vararg numbers: Int) =
first(*numbers)
fun main() {
second(7, 9, 32) eq "[7][9][32]"
}
명령줄 인자
프로그램을 시작할 때, cmd에서 원하는 만큼 인자를 전달할 수 있다.
main(args: Array<String>)
으로, 인텔리제이에서 Run Configuration을 변경하거나 kotlinc 컴파일러를 사용해 인자를 전달 가능하다.
26. 집합 Set
Set은 각각의 값이 오직 하나만 존재할 수 있는 컬렉션이다.
가장 일반적인 Set 연산은 in, contains()
를 사용해 원소인지 검사하는 것
set.containsAll(anotherSet: Set)
: 이 집합이 다른 집합을 포함하는가?set.union(anotherSet: Set)
: 합집합set intersect anotherSet
: 교집합set subtract anotherSet
==set - anotherSet
: 차집합
union, intersect, substract
는 점 표기법과 중위 표기법 모두 사용 가능하다
27. 맵 Map
Map은 키, 값을 연결하고, 키가 주어지면 연결된 값을 찾아준다.
((key, value) in map)
: 이테러이션을 하면서 키, 값 분리 가능mapOf()
와mutableMapOf()
는 원소가 Map에 전달된 순서를 유지해준다.map['b']
에서, 키 값 ‘b’가 포함되지 않으면 null을 반환.map.valueOf('b')
는NoSuchElementException
발생
28. 프로퍼티 접근자 get, set
프로퍼티 이름을 사용해 프로퍼티를 읽는다. 대입 연산자 ‘=’을 사용해 가변 프로퍼티에 값을 대입한다.
print(data.i), data.i = 20
과 같이, i라는 이름이 가리키는 저장소 위치에 접근할 때, 코틀린은 함수를 호출해 읽기와 쓰기 연산을 수행한다.프로퍼티 값을 얻기 위해 사용하는 접근자를 getter 라고 한다. 프로퍼티 정의 바로 다음에
get()
을 정의하면 getter를 정의 가능하다. 갱신은 setter,set(value)
정의 시 커스텀화 가능.getter, setter 안에서 기본적으로
field
라는 이름을 사용해 저장된 값에 직접 접근 가능하다.- 프로퍼티를 private으로 정의하면 두 접근자 모두 private이 된다.
- setter만 private으로 하면 같은 클래스 내부에서만 프로퍼티의 값을 변경 가능하다.
코틀린 스타일 가이드에서는 계산 비용이 많이 들지 않고 객체 상태가 바뀌지 않는 한 같은 결과를 내놓는 함수의 경우 프로퍼티를 사용하는 편이 낫다고 안내한다
많은 객체 지향 언어가 프로퍼티에 대한 접근을 제어하기 위해 필드를 private으로 정의하는 방식에 의존한다. 코틀린은 프로퍼티 접근자를 사용해 쉽게 접근을 제어할 수 있다.