타입을 집합이라고 했을 때, C언어의 타입 캐스팅 연산자가 왜 위험한지 알 수 있습니다. 그리고 모던 프로그래밍 언어에는 타입 캐스팅 연산자가 왜 여러 개가 존재하는지 알 수 있습니다.
C언어의 타입 캐스팅은 아래와 같습니다.
int ch = (int)'a';
문자형인 char 타입에서 정수형인 int 타입으로 타입 캐스팅을 하였습니다. C언어의 타입 캐스팅은 가장 강력하면서도 무식한 연산자입니다. 강력하면 좋을 것 같은데 왜 위험한 것일까요? 다음 예제를 보겠습니다.
사람은 죽는다.
소크라테스는 사람이다.
고로 소크라테스는 죽는다.
위 삼단 논법은 논리가 명확합니다. 윗글이 어떤 프로그램이라고 할 때, C언어의 타입 캐스팅을 적용해 보겠습니다.
돌맹이 = (돌맹이)소크라테스
사람은 죽는다.
돌맹이는 사람이다.
고로 돌맹이는 죽는다.
어떤가요? 말도 안 된다는 것을 알 수 있습니다. 즉, C언어의 타입 캐스팅은 프로그래밍 언어의 근간인 논리를 파괴하는 연산자입니다. 타입 캐스팅을 잘 못 하면 프로그램이 크래시가 발생하는 것은 부차적입니다. 프로그래밍은 논리의 언어인데 이 논리를 파괴하는 것이 더 큰 문제입니다. C언어가 이렇게 만들어진 이유는 오직 성능 때문입니다. 모던 프로그래밍 언어는 다양한 타입 캐스팅 연산자를 제공해 개발자가 프로그램의 논리를 파괴하지 않도록 도와줍니다. Kotlin만 보아도 as
, is
그리고 toInt(), toString() 등 다양한 타입 캐스팅 연산자를 제공합니다. 그리고 각 타입 캐스팅 연산자마다 용도와 때가 정해져 있습니다. C언어처럼 A타입에서 B타입(예: Int와 Long) 간에 자동으로 캐스팅이 되는 경우는 없습니다.
$1 + 45^{\circ}$은 이항 연산이 아닙니다. 수학의 이항 연산은 같은 집합의 두 원소 간의 연산이기 때문입니다. 따라서 $1 + 45^{\circ}$을 계산하려면 각도 집합으로 통일하던가 아니면 실수 집합으로 통일해서 연산해야 합니다. 컴퓨터 프로그래밍의 연산도 이러한 성질을 따릅니다. 서로 다른 타입 간에 연산은 정의되지 않는 것이 원칙입니다. C언어에서 정수 int 타입과 실수 Double 타입 간에 + 연산이 수행되는 이유는 정수 int 타입이 자동으로 Double 타입으로 변환되어 연산 되기 때문입니다. 하지만 이런 자동 형 변환은 좋지 않은 것이기 때문에 모던 프로그래밍 언어에서는 모두 직접 명시적으로 형 변환을 수행토록 하는 추세입니다.
이 글에서 살펴본 데이터의 흐름은 익숙한 모양새입니다. C++를 배웠다면 iostream의 입출력 연산자를 아래처럼 사용하는 것이 익숙할 것입니다.
std::cout << 1 << "2" << 3.0 << std::endl
이렇게 쓸 수 있는 이유는 <<
연산자가 this 포인터를 반환하기 때문입니다. 그래서 this 포인터 체이닝을 구성할 수 있습니다. 이와 비슷한 패턴은 Builder 패턴입니다.
val builder = SomeType.Builder()
builder
.setSomeValue(x)
.set....
.set....
.set...
.build()
빌더 패턴도 마찬가지로 각 메서드가 this포인터를 반환하여 위처럼 흐름을 구성할 수 있습니다. 모나드와 차이점은 this 포인터가 흐르는 것은 언제나 사이드이펙트가 발생할 수 있음을 암시하는 것입니다. 사이드이펙트를 줄이는 모나드와 달리 this 포인터를 흐르게 하는 것은 사이드이펙트를 부풀리는 것입니다.
함수 합성식을 보면 모든 함수의 인자와 결과값을 제네릭 타입으로 설정하면 될 것처럼 느껴집니다.
fun <T> a(value: T): T
fun <T> b(value: T): T
fun <T> c(value: T): T
val result = c(b(a))
위처럼 함수를 작성할 수 있지만 내부 로직을 작성하는 일은 어렵습니다. 그 이유는 제네릭은 타입의 정보를 지우는 것에서 출발하기 때문입니다. 함수 a, b, c의 내용을 정의하려고 해도 타입 T가 클래스인지 원시타입인지 사칙 연산자는 지원하는지 등을 알 수 없기 때문에 함수의 로직을 작성하기가 어렵습니다. 그에 반해 모나드 Monad[T]는 감싸는 타입은 제네릭 타입이지만 자신의 타입은 그대로 유지합니다. 그리고 map과 flatMap을 실행할 때는 Monad[T]가 특정 타입으로 구체화 되어 있기 때문에 람다를 통해서 구체타입의 정보를 알 수 있어 원하는 연산을 수행할 수 있습니다.
지금까지 긴 글을 읽어 주셔서 감사합니다. 부족한 글이지만 모나드를 이해하는데 도움이 되었기를 바래봅니다. 수정할 내용 또는 피드백이 있으시다면 sungcheol@dable.io 또는 skyfe79@gmail.com으로 보내주세요. 감사합니다.
알림
이 글은 데이블 기술블로그에 올린 글을 제 블로그에 다시 올린 글임을 알려드립니다.