본문으로 건너뛰기

오승윤

나 같은 기계들: 부조리와 살아가기

<종의 기원담>이 어린이 동화처럼 느껴질 정도로 현실적이지만 거짓이 섞여있어, 읽는 내내 혼란스러웠다.

보통 로봇이 나오는 상상은 지금으로부터 수십년에서 수백년 떨어진 미래를 무대로 한다. 하지만 여기서는 다르다. 1982년 즈음이 그 배경이다. 앨런 튜링이 살아있고, 실제로는 1976년 생인 데미스 허사비스가 1970년대에 앨런 튜링과 만나 같이 연구를 하고, 영국이 포클랜드 전쟁에서 패했으며, 토니 벤이 총리가 되고 암살당하는 등 실제 벌어진 역사와 다른 일들이 일어난다. 오지 않았다는 의미에서 이 역시 미래다. 복제 인간 로봇은 아직 오지 않았기에 허구의 시공간에서만 존재할 수 있는 것이다.

이해하기 어려운 개념인 P대 NP의 문제가 앨런 튜링과 함께 중요한 역할을 한다. 주인공 찰리처럼 나도 전혀 이해가 가지 않아서 따로 공부를 해봤지만, 역부족이었다. 미숙한 수준의 이해로는 이 버전의 1980년대에는 이 문제의 해결로 인해 기계가 모든 복잡한 문제를 해결할 수 있는 능력을 가진 존재가 되었다는 것이다. 하지만 23명의 아담과 이브들 중 상당수가 본인의 자아를 없애버리는 자살을 택한다. P대 NP 문제가 해결되었지만 거짓을 따르지 않는다는 대원칙과 이 세상은 양립할 수 없었던 것이다.

찰리는 주인공 같지만 카메라 역할을 한다. 본인의 렌즈를 통해 미란다, 마크, 아담, 튜링, 고린지를 보여주는 도구 같았다. 주변의 자극에 반응을 하고 관찰 결과에 주관을 잔뜩 섞어 사실인 양 전달하고 있다. 그의 삶에 많은 부분이 채워져 갈수록 그는 더 텅 비워져 간다. 아담이 불려놓은 재산으로 집을 사고 미란다, 마크와 가정을 이루려 하지만, 그는 이 모든 것에 어떤 중요한 역할도 하지 못하고 휩쓸려 간다. 아담은 자신이 번 돈이 찰리를 불행하게 만든다는 것, 미란다가 무고(고린지가 자신을 강간했다는 건에 한해서)를 했다는 진실을 전달하고, 바로 그 친구들에 손에 의해 살해당한다. 아담은 자살하지 않았지만, 진실을 행동에 옮김으로써 죽음에 이른다.

문명의 주요한 진보에도 불구하고 소설의 막바지의 영국은 혼란 그 자체다. 전쟁에서 수천의 국민이 죽거나 다치고, 자존심에 상처를 입었다. 현직 총리가 IRA에 의해 암살당하고, 유럽연합을 탈퇴하고, IMF 구제 금융을 받는다. 실업 문제는 심각한 수준에 이른다. 망하는 나라의 모습이 이런건가. 시위대가 런던 시내에 넘쳐난다. 아담의 시신을 싣고 앨런 튜링을 만나기 위해 힘겹게 그 사이를 지나는 찰리의 모습은 그 이유를 설명해주는 것 같다.

참으로 진지한 철학적 문제는 오직 하나뿐이다. 그것은 바로 자살이다.

알베르 카뮈의 이해할 수 없었던 난해한 시지프 신화의 첫 구절이다. 부조리와 함께 살아가야 하는 인간은 이런 고민을 하게 된다. 왜 살아가는가? 자신의 자아를 망가뜨려 자살하는 아담과 이브는 이런 고민을 했을 것이다.

이 버전의 1982년이 우리의 그것과 완전히 다른 미래인 이유는 앨런 튜링이 살아남았다는 것이다. 어떻게 살아 남았는가. 그는 거짓을 받아들였다. 모순되고 이상한 영국 사회에서 살아남기 위해서 그들이 원하는 거짓말을 해야했다. 무슬림 가정에서 태어난 미리암은 강간을 당한 피해자였지만, 집안의 수치가 될 수 없어 진실을 전하지 못하고 고통 받다가 자살한다. 왜 피해자가 고통을 받아야 하는가. 이것이 정의인가. 우리 역사의 1954년 앨런 튜링은 화학적 거세로 인해 변해가는 몸을 보며 독사과를 베어 물고 자살한다. 죽어간 아담과 이브들처럼, 실제 이 세상에 존재했던 그는 거짓과 살아갈 수 없었던 것이다.

소설을 읽는 내내 역사적 사실처럼 다뤄지는 모든 부분이 사실인지 아닌지 찾아보면서 읽게 됐다. 덕분에 포클랜드 전쟁, 마거릿 대처와 토니 벤, 앨런 튜링에 대해 흥미로운 것들을 많이 알게 돼 좋았지만, 너무 범벅이 되어있어서 나중에는 그걸 아는 게 의미가 있는건지도 의문이었다. 읽는 내내 씁쓸한 느낌에 우울한 느낌이 들었던, 인간에 대해 많은 생각을 불러 일으키는 영국 날씨 같은 소설이었다.

오승윤

Python에서 인터페이스 역할을 하는 추상 클래스를 만드는 예제들을 보면, 메서드에 NotImplementedError를 raise 하는 것을 볼 수 있습니다. 다음 예와 같이 말이죠.

class SomeAbstractClass:
def some_method_must_be_implemented(self, *args, **kwargs):
raise NotImplementedError("It should be implemented!")

그런데, Pycharm 같은 IDE를 사용하면 자동완성에 NotImplemented가 등장하는 것을 볼 수 있습니다. 저는 NotImplementedError == NotImplemented로 이해를 하고 있었는데요.

이번에 찾아보니 완전히 다른 것이어서 여기에 정리해두려고 합니다.

NotImplementedError는 Exception의 한 종류

공식문서를 보면, NotImplementedError는 Python의 빌트인 예외 객체중 하나입니다. RuntimeError로부터 만들어졌다고 하네요.

In user defined base classes, abstract methods should raise this exception when they require derived classes to override the method, or while the class is being developed to indicate that the real implementation still needs to be added.

위의 공식문서의 설명을 보면 추상 메서드들은 반드시 이 예외를 뱉어야한다고 합니다. 이 공식문서에서는 저처럼 헷갈려하는 사람들을 위해 다음과 같은 내용도 추가해놓았습니다.

NotImplementedError and NotImplemented are not interchangeable, even though they have similar names and purposes. See NotImplemented for details on when to use it.

다른 거니까 혼동해서 쓰지 말라구요.

NotImplemented는 값!

여기서도 공식문서를 봅시다.

A special value which should be returned by the binary special methods (e.g. eq(), lt(), add(), rsub(), etc.) to indicate that the operation is not implemented with respect to the other type

다른 타입과 해당 연산이 이뤄지는 것이 구현되어 있지 않은 경우 특수 메서드들이 반환해야하는 특수 값이라고 합니다. 이 특수 함수들의 이름과 이에 대응되는 연산 기호는 다음과 같은 것들을 말합니다.

  • __eq__(): ==
  • __lt__: <
  • __add__: +
  • ...

근데 이런 특수 메서드들에서 NotImplemented라는 값을 왜 반환해야하고, 반환하면 어떤 일이 일어나는 걸까요?

알아보기 위해 예시를 만들고 시작해봅시다.

예시: 나이가 무척이나 중요한 한국인 클래스

한국인에게는 나이가 가장 중요하죠? 한국인 사이에서 존댓말을 해야하는지 아닌지 알려주는 코드를 짜야한다고 합시다. 객체를 직접 비교 연산자(>)로 비교할 수 있다면 편하겠죠. 특수함수 __gt__()를 구현해서 비교하도록 해봅시다.

from datetime import datetime

class Korean:
def __init__(self, name: str, birth_year: int):
self.name = name
self.birth_year = birth_year

def get_korean_age(self):
current_year = datetime.now().year
return current_year - self.birth_year + 1

def __str__(self):
return f"{self.birth_year}년생 {self.name}"

def __gt__(self, other): # 비교 연산자로
return self.get_korean_age() > other.get_korean_age()

kimchulsoo = Korean("김철수", 1989)
honggildong = Korean("홍길동", 1990)

print(kimchulsoo > honggildong) # True

나이만 중요하다고 하면 한국인 객체를 숫자랑 직접 비교할 수도 있을 겁니다. 다음 내용처럼 비교를 해볼까요.

print(kimchulsoo > 34)

예상했듯이 에러가 날겁니다.

AttributeError: 'int' object has no attribute 'get_korean_age'

에러 메시지가 이해하기 쉽지 않네요.

NotImplemented를 사용하는 이유 1: 에러를 이해하기 쉽게 만들기

이 에러를 이해하기 쉽게 쓰면 "Korean과 int 사이에는 > 연산이 지원되지 않는다"가 될 것입니다. NotImplemented를 반환하도록 __gt__() 함수를 좀 바꿔보겠습니다.

...
def __gt__(self, other):
if isinstance(other, Korean):
return self.get_korean_age() > other.get_korean_age()
return NotImplemented
...

이제 kimchulsoo > 34를 실행하면 좀 더 명확한 에러 메시지가 출력됩니다.

TypeError: '>' not supported between instances of 'Korean' and 'int'

자, 이제 원인을 알았으니 int와 연산할 때 어떻게 해야하는지 정의해줍시다.

...
def __gt__(self, other):
if isinstance(other, Korean):
return self.get_korean_age() > other.get_korean_age()
elif isinstance(other, int):
return self.get_korean_age() > other
return NotImplemented
...

이러면 print(kimchulsoo > 34) 는 정상적으로 True로 출력될 겁니다.

NotImplemented를 사용하는 이유 2: 가능한 다른 연산 찾도록 만들기

NotImplemented에는 에러를 명확하게 하는 것 이외에 다른 용도가 있습니다. 바로 Python 인터프리터가 다른 가능한 연산을 시도하도록 유도하는 것인데요.

한국인 존댓말 코드를 우리 코드를 가지고 신나게 짜다가 다음과 같이 intKorean을 순서를 바꿔 썼다고 해봅시다.

34 > kimchulsoo  # 에러가 발생!

잘 따라오신 분들은 저렇게 쓰면 당연히 에러가 난다는 것을 알 것입니다. 왜냐하면 int__gt__() 함수에서는 당연히 Korean과의 연산은 정의되지 않았기 때문에 NotImplemented가 반환될 것이기 때문이죠.

그럼 Korean 클래스와 int 사이의 연산은 항상 Korean, int 순으로 이뤄져야 하는 걸까요? 이거 너무 불편할 것 같습니다.

그런데 저 예시를 조금만 바꿔보면 신기한 일이 일어납니다.

34 < kimchulsoo  # 에러가 안난다!

어라 에러가 안나네요?

이건 NotImplemented가 반환 됐을 때 인터프리터가 처리하는 방식 때문에 생긴 일입니다.

Reflected Operation

공식문서를 보면 다음과 같은 내용을 볼 수 있습니다.

"the interpreter will try the reflected operation on the other type (or some other fallback, depending on the operator)."

인터프리터는 NotImplemented가 반환되면 "reflected operation"이라는 것을 찾습니다. 직역을 하자면 반사된 연산이라는 얘긴데요. 순서를 바꿔서 연산시켜 보는 겁니다. 그러니까 이렇게요:

34 < kimchulsoo  # 에러가 안난다!

# 윗줄을 뒤집어보자!
kimchulsoo > 34 # 이건 정의돼 있는 연산이다.

이렇게 순서가 뒤집힌 연산은 우리가 정의해놨기 때문에 에러가 안나고 제대로 처리가 된 것입니다. 각 특수 함수의 Python의 공식문서를 살펴보면, reflected operation이 어떤 것인지 알 수 있습니다.

__gt__() 메서드의 공식문서를 보면 __lt__()가 "뒤집힌" 연산에 해당하네요. 그럼 아까 에러가 났던 부분을 고쳐보죠!

from datetime import datetime

class Korean:
def __init__(self, name: str, birth_year: int):
self.name = name
self.birth_year = birth_year

def get_korean_age(self):
current_year = datetime.now().year
return current_year - self.birth_year + 1

def __str__(self):
return f"{self.birth_year}년생 {self.name}"

def __gt__(self, other):
if isinstance(other, Korean):
return self.get_korean_age() > other.get_korean_age()
elif isinstance(other, int):
return self.get_korean_age() > other
return NotImplemented

def __lt__(self, other):
if isinstance(other, Korean):
return self.get_korean_age() < other.get_korean_age()
elif isinstance(other, int):
return self.get_korean_age() < other
return NotImplemented

print(34 > kimchulsoo) # False

이제 이 에러가 안납니다.

결론

NotImplementedErrorNotImplemented는 이름이 비슷해서 혼동했었는데 완전히 다른 것이었습니다. NotImplemented을 공부해보니 Python을 설계하신 분들의 유연한 설계를 엿볼 수 있었습니다. 앞으로도 그냥 갖다가 쓰는게 아니라 문서를 좀 읽어보면서 왜 이렇게 구현했을까 살펴보면 재밌을 것 같습니다.

요약

  • 추상 메서드는 반드시 NotImplementedError를 raise 하도록 코드를 짜야한다.
  • NotImplemented는 연산을 위한 특별한 함수들이 반환하는, 다른 타입끼리의 연산이 정의되지 않았음을 나타내는 특수한 값이다.
    • 에러를 이해하기 쉽게 만들 수 있다. ("XXX와 YYY 사이에 ZZZ 연산이 정의되지 않았습니다")
    • 반전된 연산(reflected operation)을 활용해서 에러를 해결하는 유연한 방법을 제공한다.

오승윤

애틋함: 같이 산다는 것

소설을 읽을 때 좋은 점 중 하나는 다른 사람의 시각에서 세상을 바라보는 경험을 할 수 있다는 것이다. <종의 기원담>을 읽으면서 다른 세계의 다른 종, 그러니까 로봇의 시선으로 세상을, 인간을, 나를 바라볼 수 있었다.

우리가 평소 당연하게 생각하는 온도를 물을 기준(섭씨)으로 삼지 않고 이산화탄소를 기준(데씨)으로 생각해야 한다거나, 물이 독성화학물이고 “지구를 보호하는 검은 구름”을 말하는 부분에서 우리가 당연하고 긍정적으로 생각하는 것들이 누군가에게는 오염물질이고 해로운 것일 수 있다는게 흥미로웠다. 하지만 가장 낯설게 본 대상은 나, 인간이었다.

케이는 인간과 다르지 않은 것처럼 보인다. 그래서 케이를 보면서 인간의 껍데기인 물리적 형태를 제외하고, 인간을 인간답게 만드는 것이 무엇인지 고민하게 된다.

로봇들은 인간을 보면 무조건적으로 복종한다. 노만은 인간이 죽으라고 말했다는 이유로 스스로 “폐기”된다. 이건 자유의지, 자아가 있는 존재가 아니다. 그저 사물일 뿐이다.

인간은 어떤가? 역사에서 봐 왔듯이 다수의 인간은 자유의지가 없는 것처럼 행동하기도 했다. 생각하지 않는 것이 만들어내는 악의 평범성. 이것도 인간의 모습이다.

그러고 보면 나 역시 생각없이 하는 행동들이 많다. 위에서 말한 심각한 것은 아닐지라도 아무 이유도 없이 유투브를 본다던가, 밀가루 음식을 계속 찾는다거나, 특정 대상을 싫어한다던가... 내 자의식이 전면에 나서 깨어 있는 시간이 많지 않은 것 같다.

케이는 생각한다. 의심하고, 항상 “왜?”라는 질문을 생각한다. 본능이 명령할 때 거기에 저항하는 힘은 여기서 나오는 것 같다. 그리고 “이웃로봇을 네 몸과 같이 사랑하라”는 계명에 충실히 따랐던 것도 한 몫했던 것 같다. 로봇의 자유의지를 지키기 위해 인간을 절멸하기로 한 것이다.

인간은 절대적으로 복종하는 것을 생명으로 보지 않는다. 인간을 보자마자 자신의 모든 것을 내던지는 로봇들을 보며 위화감이 들면서 불편했던 이유는 이것이다. 동등한 존재로 보이지 않았던 것이다. 이전까지 나와 비슷해보였던 존재들이 사물로 바뀐 것이다.

케이의 인간숭배회로가 타버리면서 그는 인간을 있는 그대로 보게 되었다. 초라한 모습, 깡마른 모습, 공포에 떠는 모습...

비로소 케이는 인간의 존재를 오롯이 받아들일 수 있었다. 있는 그대로 애틋해할 수 있었다. 경애가 사라지자 증오도 똑같이 자취를 감추었다.

가장 인상깊었던 단어다. 애틋함. 타인을 이해하려면, 공존하려면 이 애틋함을 느껴야하는 것 같다. 너도 힘들었겠구나, 살려고 하는구나. 너도 나와 다르지 않구나. 진정한 공감에 가까운 감정이 바로 애틋함이었다.

인간 "시아"는 모든 로봇이 자아를 자유의지를 잃지 않도록, 그래서 인간의 생명을 보존하도록, 로봇의 4법칙을 한마디로 무용지물이 되게 만든다.

......인간의 어떤 명령도 들을 필요가 없습니다.

명령하는 투가 아니라는 점이 좋았다. 필요가 없다는 것이다. 로봇, 당신들은 이미 충분하다. 인간의 명령은 들을 필요없다... 로봇들이 자유의지를 갖게되면 인간을 해할 수 있지만, 애틋함을 느끼는 대상을 해하지는 못한다.

대상을 숭배하거나 증오하지 않고, 있는 그대로 볼 수 있어야 한다. 그래야 나와 다르지 않음을 느낄 수 있고 같이 공존할 수 있는 것이다.