Post

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을 사용하는 것은 부실한 접근 방법이다. 매번 출력을 자세히 살펴보고 올바른 출력인지 확인해야 하기 때문.
     

테스트 시스템의 종류

  1. JUnit : 자바에서 가장 널리 쓰이는 테스트 프레임워크. 코틀린도 유용하게 사용가능하다.
  2. 코테스트 (Kotest) : 코틀린 전용으로 설계됨. 코틀린 언어의 여러 기능을 살려서 작성됨.
  3. 스펙 프레임워크 : 명세 테스트 (specification test) 라는 다른 형태의 테스트 제공.

이 책에서는 자체 개발한 atomictest 프레임워크를 사용한다. 이 시스템은 식의 예상 결괏값을 보여주고, 실행될 수 있음을 알 수 있도록 출력을 제공한다.
 

테스트에서 기본이 되는 요소는 eqneq 이다.

이는 식 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"
}

eqneq는 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으로 정의하는 방식에 의존한다. 코틀린은 프로퍼티 접근자를 사용해 쉽게 접근을 제어할 수 있다.