[Kotlin] 프로그램을 더 안전하게 만들기

chris·2024년 1월 13일
0

kotlin

목록 보기
1/1
post-thumbnail

"코틀린을 다루는 기술"을 읽으면서 개인적으로 중요하다고 생각한 내용을 기록한 글이다.

프로그램을 더 안전하게 만들기 위한 몇가지 방법

  • 가변 참조(변수) 사용을 피하고, 상태 변이를 피할 수 없는 경우에는 그 부분을 추상화하라
  • 제어 구조를 피하라
  • 효과(effect, 외부 세계와의 상호 작용을 말한다!)를 우리가 작성하는 프로그램의 일부 영역 안에서만 일어나도록 제한하라. 이 말은 프로그램을 작성할 때 일부 한정된 영역을 제외한 나머지 부분에서 콘솔 등의 장치에 출력하거나 파일, 데이터베이스, 네트워크 등의 장치에 데이터를 쓰는 등의 행위를 하지 말아야 한다는 뜻이다.
  • 예외를 던지지 말라. 예외를 던지는 것은 무조건 분기(GOTO)의 현재적인 변형이라 할 수 있다. 이로 인해 프로그램이 스파게티 코드(spaghetti code)가 될 수 있다. 스파게티 코드라는 말은 프로그램 흐름이 어디서 시작하는지 알 수 있지만, 어디로 흘러가는지 제대로 따라갈 수 없다는 뜻이다.

부수 효과

일부 함수는 값을 반환한다. 일부 함수는 세계의 상태를 변경한다. 일부 함수는 값을 반환하는 동시에 상태를 변경하기도 한다. 값을 반환하는 함수나 외부 상태를 변경하는 경우를 부수 효과(side effect)라고 한다.
부수 효과를 사용하는 프로그램은 잘못된 것이다. 프로그램에서 부수 효과는 프로그램이 반환하는 결괏값에 덧붙여 프로그램 밖에서 관찰할 수 있는 어떤 변화를 뜻한다.
안전한 프로그램은 인자를 받아서 값을 반환하는 여러 함수를 합성해 만들어진다. 그리고 그런 함수 함성이 프로그램을 이루는 전부다. 우리는 함수 안에서 어떤 일이 벌어지는지 신경 쓰지 않는다. 왜냐하면 이론적으로 함수 안에서는 아무런 일도 벌어지지 않기 때문이다.

참조 투명성으로 프로그램을 더 안전하게 만들기

외부 세계의 상태를 변경하지도 않고 외부 상태에 의존하지도 않는 코드를 일걸어 참조 투명(referentially transparent)하다고 한다.

참조 투명한 코드의 특성

  • 참조 투명한 코드는 자기 완결적이다. 어떤 문맥에서나 그 코드를 사용할 수 있다. 단지 올바른 인자를 그 코드에 제공하기만 하면 된다.
  • 참조 투명한 코드는 결정적이다. 참조 투명한 코드는 인자가 같으면 항상 결과도 같다. 하지만 참조 투명한 코드도 잘못된 결괏값을 돌려줄 수 있다. 그렇지만 적어도 같은 인자에 대해 항상 같은 결과를 보장한다.
  • 참조 투명한 코드는 절대 예외를 던지지 않는다. 다만 참조 투명한 코드도 메모리 부족 오류나 스택 오버플로 오류를 발생시킬 수는 있다. 하지만 이런 오류는 프로그램에 보그가 있음을 뜻한다. 메모리 부족이나 스택 오버플로 등은 프로그래머인 우리가 제공하는 API를 사용하는 쪽에서 처리할 수 있는 오류 상황이 아니다.
  • 참조 투명한 코드는 예기치 않게 다른 코드가 실패하는 상황을 만들지 않는다. 참조 투명한 코드는 인자를 변경하거나 다른 외부 데이터를 변경하지 않으며, 그에 따라 코드를 호출하는 쪽의 데이터가 오염되거나 동시 접근으로 인해 오류가 발생하는 경우가 없다.
  • 참조 투명한 코드는 자신이 제대로 작동하기 위해 외부 장치에 의존하지 않는다. 참조 투명한 코드는 외부 장치(예를 들어 데이터베이스, 파일 시스템, 네트워크)를 사용할 수 없거나, 외부 장치가 너무 느리거나 고장 나서 코드가 계속 대기 상태에 머무는 경우가 없다.

안전한 프로그래밍의 이점

  • 프로그램이 결정적이기 때문에 프로그램을 추론하기 더 쉽다. 입력이 같으면 항상 출력도 같다. 따라서 대대적으로 테스트해도 여전히 그 프로그램이 예기치 않은 어떤 조건에서 깨질 수 있는지 확신할 수 없는 비결정적인 프로그램과 달리 프로그램의 올바름을 증명할 수 있다.
  • 프로그램을 더 쉽게 테스트할 수 있다. 부수 효과가 없으므로 테스트하면서 프로그램 컴포넌트를 외부와 격리하기 위해 mock을 사용할 필요가 없다.
  • 프로그램을 훨씬 쉽게 합성하고 재조합할 수 있다. 프로그램을 작성하면서 먼저 필요한 여러 기반 함수를 만들고, 그들을 합성해 더 높은 수준의 함수를 만들며, 원하는 프로그램에 해당하는 함수가 만들어질 때까지 이 과정을 반복한다.
  • 공유 상태 변이를 피하므로 프로그램이 태생적으로 Thread-Self하다. 이 말은 모든 데이터가 불변이어야만 한다는 뜻은 아니다. 다만 공유하려는 데이터는 모두 불변이어야 한다.

프로그램을 추론하는 데 치환 모델 사용하기

관찰할 수 있는 효과가 없이 값만 반환하는 함수를 사용해서 얻는 주된 이점은 함수 호출이 반환값과 동등하다는 데 있다.
참조 투명한 함수는 아무 부수 효과도 일으키지 않고, 오직 인자에 따라 결정되는 결괏값만 지닌다. 그 결과, 참조 투명한 함수 호출이나 식은 항상 그 결괏값으로 치환할 수 있다.

fun main(args: Array<String>) {
	val x = add(mult(2, 3), mult(4, 5))
    println(x)
}

fun add(a: Int, b: Int): Int {
	log(String.format("Returning ${a + b} as the result of $a + $b"))
    return a + b
}

fun mult(a: Int, b: Int) = a * b

fun log(m: String) {
	println(m)
}

위 코드의 add(mult(2, 3), mult(4, 5))를 'add(6, 20)`으로 치환해도 프로그램 전체의 의미는 바뀌지 않는다.

요약

  • 결괏값을 반환하는 함수를 외부와 상호 작용하는 효과와 명확히 분리함으로써 프로그램을 더 안전하게 만들 수 있다.
  • 함수의 출력이 결정적이고 함수가 외부 상태에 의존하지 않는다면 함수를 더 쉽게 테스트하고 함수의 성질을 더 잘 추론할 수 있다.
  • 더 높은 수준까지 추상화를 추구한다면 안전성, 유지 보수 용이성, 테스트 용이성, 재사용성이 좋아진다.
  • 불변성이나 참조 투명성과 같은 안전성 원칙을 적용하면 프로그램이 상태 변이를 실수로 공유하는 경우를 방지할 수 있다. 다중 스레드 환경에서 발생하는 버그 중 아주 많은 경우가 상태 변이 공유로 인해 생겨난다.
profile
software engineer

0개의 댓글