서평

프로그래머로 롱런하는 비결

데브테드 2019. 6. 16. 01:29

코딩을 지탱하는 기술 서평

부제: 원리로 깨우치는 프로그래밍 기법

작가 : 니시오 히로카즈 지음 / 김완섭 옮김

 

간단 요약 : 실력 있는 프로그래머가 롱런한다. 실력을 키우려면 제대로 학습해야 한다. 그리고 이 책은 ‘제대로’ 학습하는 방법을 알려준다.

 

 나는 프로그래머라는 직업으로 일하고 있고 전문직으로서 기술직으로서 내 직업에 자부심을 갖고 있다. 하지만 일반적인 전문직과는 다르게 이 직업은 확실한 위험성을 갖고 있다. 보통 전문직들은 경력이 쌓이면 그 경력 자체가 실력으로 인정되는 경우가 많은데 프로그래머는 경력을 통해서 발전시킨 스킬이 기술의 빠른 발전과 함께 죽은 지식이 되버리는 경우가 많다. 그래서 뭔가 계속해서 배워야 할 것만 같고, 내 실력이 늘고 있지 않다고 생각되면 불안해진다. 근데 막연히 불안해하면서 이것저것 한다고 실력이 늘지는 않더라. 물론 이것저것 여러 가지 할 수 있는 사람이 될 수는 있다. 다만 프로그래머로서 이것저것 할 줄 안다고 해도 본인 일에 실제로 그 잡다한 지식들을 활용하는 경우는 매우 적고, 직장에서의 포지션이나 직장의 상황에 따라 활용하는 기술이 천차만별이다보니 결국 버려지는 지식들이 잔뜩 쌓이게 된다. 이런 악순환을 그대로 두게 되면 노력은 할만큼 했는데 실제 내 역량이 성장하지 않는 경우가 잦다.

 그럼 어떻게 해야 진짜 실력을 ‘쌓아’ 나갈 수 있을까? 내가 생각한 답은 프로그래머의 토대가 되는 기본에서부터 모든 지식을 쌓아나가는 것이다. 마치 건물을 만들 때, 기초공사로 뼈대부터 만들거나 땅부터 다지는 것처럼 기본이 되는 토대를 견고하게 세우는 것이다.(물론 실제 건물하고 다르게 지식의 기초는 건물을 만들고 나서도 다시 강화하기 쉽다) 그렇게 견고한 기초가 있으면 새로운 지식들이 아무리 잡다하게 들어오더라도 그 지식을 내 지식의 건물 어디에 쌓아둬야 할지 명확히 알고 있기 때문에 온전히 내 실력으로 쌓아둘 수 있다.

 근데 말이 쉽지 어떤 기본기를 쌓아야 하는지 그리고 새로 습득하는 지식은 어느 지식과 연결시켜야 할지 아는 것 자체가 어렵다. 그런 사람들에게 이 책 `코딩을 지탱하는 기술`을 추천한다. 저자인 니시오 히로카즈는 프로그래밍 언어라는 주제를 이용해서 기본기가 무엇이고, 새로 습득할 지식을 어디에 연결해야 할지 제대로 학습하는 방법을 가르쳐준다.

 


 

내용 요약 

(스크롤 주의)

1장 효율적으로 언어 배우기

 프로그래밍 언어를 배울 때 새로운 것이라고 생각해서 무작정 암기를 하게 되면 쉽게 잊히거나 자신이 무엇을 알고 있는지 제대로 알지 못하는 경우가 많다. 그런 죽은 지식을 없애는 학습법으로 저자인 히로카즈는 비교를 통한 배움, 역사를 통한 배움을 제안한다.

 비교를 통한 배움은 서로 다른 언어들을 비교하면서 지식을 연결시키는 방법을 말한다. 예를 들어 if문의 동작 방식을 제대로 알고 싶다면 같은 요소를 갖고 있는 C, Java, C++, C#, Python, PHP 에서의 if문의 동작방식을 비교하면서 학습하는 것이다.(사실 현존하는 거의 모든 고급 언어가 if문을 활용한다.)

 역사를 통한 배움은 언어의 기능이 만들어졌을 때 이 기능이 왜 만들어졌는지 그 맥락을 추적해봄으로써 "왜 이 기능이 필요한가"를 깨닫게 해 준다. 프로그래밍 언어의 역사라고 한다면 언어를 만들 때 언어 설계자의 의도가 어떤 것이었나에서부터 특정 기술이 생겨나게 된 원인이 무엇인지 아는 것 등 다양하다.

 

 나는 이 두 가지 학습법은 한 분야를 깊게 아는 전문가가 되고싶다면 필수로 체화시켜야 한다고 생각한다. 그리고 더 나아가서 각 분야의 지식을 ‘단권화’ 시킬 수 있다면 (마치 사법고시를 준비하는 사법고시생들처럼) 그 분야의 끝판왕급 내공을 가질 수 있을 거라고 생각한다. 이건 내가 이 책을 읽으며 생각하게 된 아이디어이고, 앞으로 내 커리어와 함께할 목표가 되었다.

 

2장 프로그래밍 언어를 조감하다

 프로그래밍 언어라는 것이 왜 만들어 졌는지 컴퓨터 탄생의 시기부터 역사를 되돌아본다. 처음 이 장을 읽었을 때의 지적 희열은 아직도 강렬한 기억으로 남아있다. 기계어와 FORTRAN(사실상 최초의 고급 프로그래밍 언어로 평가받음) 통해 프로그래밍 언어가 만들어진 이유를 설명해준다. 프로그래밍 언어의 핵심은 편리하게 해주는 것, 책의 내용을 이해할 때 핵심 포인트도 "이 기술은 무엇을 편하게 해준 것인지"이다.

 

 현대에 있어서 다양한 언어가 있고, 개발자들끼리는 어떤 언어, 기술이 더 우월하다, 좋다 등의 주제를 갖고 싸우는 경우가 잦다. 그렇지만 본질적이고 중요한 문제는 이런 기술이 내가 원하는 편리함을 제공하는지에 맞춰야 한다는 점을 되새기는 시간이었다.

 

3장 문법의 탄생

 프로그래밍 언어에도 한글 같은 언어에서처럼 언어의 규칙 문법이 존재한다. 근데 이 문법은 왜 존재하는가? 언어 설계자가 편리해지길 바라는 문제가 있었을 것이고, 문제를 편리하게 만드는 해결책에 따라 필요한 규칙이 추가 된다. 그럼 프로그래밍에는 문법이 필요한 것인가? 책에서는 수학의 규칙이 프로그래밍 언어에 어떻게 적용되는지를 자세히 풀어서 설명해준다.

 간단한 사칙연산을 코딩할 때 당연히 수학에서 사용하던 수식을 적용한다. 하지만 이 당연해 보이는 것이 당연한 것이 아니라는 사실을 간단한 덧셈 등을 프로그래밍 언어가 처리하는 방식은 다르다는 것을 보여준다. 프로그래밍 언어식 처리 방식은 상자를 쌓아놓듯이 하나하나의 숫자와 수식(덧셈, 뺄셈)을 한 곳에 쌓아놓고 순서대로 처리하는 스택 머신(Stack Machine)의 개념에 따라 처리한다. 그렇지만 이런 방식은 수학의 방식과 달라서 코딩하는 사람이 이해해서 활용하려면 컴퓨터식 수식처리 방식을 별도로 학습해야 했다. 하지만 이에 불편함을 느꼈던 사람들은 컴퓨터식 처리방식의 높은 효율성(더 빠르다던가, 메모리를 아낄 수 있다던가) 을 버리고 수학의 규칙(문법)을 적용한 FORTRAN 언어를 만들었고, 심지어 그때 당시 기준으로 가장 선호되는 언어가 됐다.

 수식처리 이후에도 프로그래밍 언어에 적용되는 문법들은 점차 늘어났다. 왜냐하면 사람(프로그래머)이 더 편리하게 개발하고 싶었기 때문이다. 아무래도 컴퓨터가 수학을 기반으로 발명됐고 숫자로 돌아가는 기계이다 보니 수식을 처리하는 문법이 가장 먼저 만들어졌고, 그 이후의 규칙들은 수학의 틀에서 벗어나서 점차 다양한 방향으로 발전되기 시작한다.

 

4장 처리 흐름 제어 

  제어 구문으로 분류되는 if문, while문, for문 등 처리의 흐름을 제어하는 구문이 왜 필요했는지, 어떤 문제들을 해결해줬는지를 역사를 돌이켜 설명해준다. 프로그래밍이라는 것 자체가 사실상 if문 같은 조건형 처리가 없다면 간단한 계산기 소프트웨어조차 만들지 못한다.(if문이 필수라는 의미가 아닌 ‘if문식 처리’ 임을 다시 한번 강조함) 그렇기 때문에 이런 제어 구문식 처리는 기계어로 프로그램을 만들던 때에도 존재했다. 지금은 기계어를 사용하는 코딩은 잘 하지 않지만, 2세대 프로그래밍 언어이자 실질적 저급언어로 분류되는 어셈블리어가 있다. 이 어셈블리어를 이용해 문법이 없었을 때 제어구문과 같은 역할을 하는 코드는 어떻게 구현됐었나를 먼저 보여주고, 다른 고급언어들을 이용하여 제어구문을 썼을때 어떤 문제가 해결되고 편해지는지 예시를 보여줌으로써 이해를 도와준다. 

 

5장 함수

 함수가 왜 생기게 됐고, 함수를 만들기 위해 어떤 것을 고려했어야 됐는지에 대해 자세히 설명해준다. 함수가 생기게 된 이유는 '몇 번이고 반복해서 사용하는 명령을 한 곳에 모아 두고 재 사용하고 싶다는 필요성'을 충족시키기 위해서였다. 그런데 기존 goto문 구조로는 돌아갈 목적지를 저장하지 못하는 한계성 때문에 이를 실현할 수 없었다. 이때 돌아갈 목적지를 저장하기 위한 저장소로서 스택(Stack)을 만들게 된다. 목적지를 쌓듯이 저장할 수 있게 되면서 함수의 이용이 가능하게 됐고, 함수의 내부에서 함수를 사용하는 내포 함수인 ‘재귀 함수’라는 개념을 사용할 수 있게 됐다.

 

 for문으로 함수를 반복하는 것과 재귀 함수의 속도 차이가 왜 생기는지 다시 한번 공부해보도록 하자. 예전에 한 번 공부했었는데 까먹었다.

 

6장 에러 처리

 예외 처리가 어떤 목적으로 태어났는지, 어떻게 발전해왔는지, 그리고 어떤 문제가 계속해서 존재하는지 설명하는 장. 프로그램은 결국 실패할 수밖에 없다. 그렇다고 프로그램이 꺼지거나 멈추게 놔둘 수는 없으므로 이 실패를 처리해줘야 한다. 그리고 그 방법에는 크게 두 가지 방법이 있다. 반환 값으로 실패를 전달하는 것, 그리고 예외를 사용하는 것. 첫 번째 방법인 반환 값을 이용하는 방법은 두 가지 단점이 있다.

하나는 실패를 전달하는걸 프로그래머가 까먹을 경우가 많다는 것. 둘은 에러 처리 때문에 코드를 해석하기 어려워진다는 것.

 첫째의 경우 프로그래머의 주의력을 끌어올릴 수 있는 방법이 없으므로 어쩔 수 없는 것이고, 둘째는 에러 처리 코드를 좀 더 보기 좋게 정리하기 위해 코드 점프를 이용해서 코드를 보기 좋게 정리하는 방법이 일반적이다. 이렇게 코드를 점프로 처리하다 보니 실패의 종류를 점점 추가하게 됐고, 결국 커스텀으로 정의한 실패를 쓰고 싶다는 아이디어까지 실현됐다.

 그러다 보니 자연스럽게 발전한 게 실패할 거 같은 처리를 묶는 예외 처리 구문이었다. 예외 처리 구문을 다양하게 적용하다 보니 문을 열면 꼭 닫아야 되는 것처럼 짝이 되는 동작을 잘 처리하기 위해 finally라는 구문이 생기기도 했다. 예외 처리를 하는 대부분의 언어는 예외를 호출 처로 전파해주는데, 이게 문제가 예외를 전파만 해주고 어떤 코드가 어떤 예외를 던질 가능성이 있는지 모르는 경우가 많았다. 그래서 Java의 경우 검사 예외라는 방식을 도입해서 혹여나 놓칠 가능성이 있는 예외마저도 잘 처리할 수 있도록 만들었다. 하지만 이는 코드 복잡도나 유지보수가 어려워지도록 만들기도 해서 다른 언어에서는 잘 사용되지 않는 개념이 됐다.

 결국 예외 처리는 좋은 개념이었지만 해결할 수 없는 문제를 여전히 가지고 있다. 바로 어떤 에러를 던지는지 알 수 없다는 것. 결국 프로그램의 실패를 처리하는 방법은 여전히 두 가지다. 반환 값으로 돌려주기, 예외처리. 둘 다 적절히 써야 한다.

 

프로그래머로서 헷갈릴 수 있는 예외 처리라는 기능이 언어에 왜 존재하는지 잘 설명해 준다. 꼭 읽어봐야 할 장.

 

7장 이름과 스코프

 프로그래밍에서 이름은 중요하다. 그래서 이름이 왜 필요한지는 간단한 예시만 봐도 알 수 있다. 예를 들어 36번째 책이라는 이름보다 코딩을 지탱하는 기술이라는 제목을 가진 책이 사람이 더 이해하기 좋다. 이런 명백한 이유 덕에 이름이라는 개념은 초기 프로그래밍에서 금방 적용됐었다. 하지만 이는 금방 ‘이름 충돌’이라는 문제를 발생시켰다. 같은 이름을 1번째 줄에서 썼다가 1000번째 줄을 작성할 때 또 사용하게 되면 첫 번째 이름과 두 번째 이름이 중복되면서 충돌 문제가 발생하는 것이다. 이를 해결하기 위한 방법으로 긴 이름 사용하기, 스코프(범위)를 사용하기가 사용됐다.

 첫 번째 방법인 긴 이름 사용하기는 한계가 명백했다. 이름이 무한정 길어지게 되면 오히려 더 불편했기 때문이다. 그래서 두 번째 방법인 스코프 사용하기를 더 발전시키게 됐다.

 처음에는 동적 스코프라는 개념이 도입됐다. 동적 스코프는 쉽게 설명하면 일정 범위에서는 무조건 이 이름이 사용되도록 범위를 좁혀주는 것이다. 다른 곳에서 x라는 이름을 사용했어도, 지금 코드가 굴러갈 새로운 스코프에서는 local x라는 변수명을 사용하면 기존 x를 무시하고 local x를 사용한 후 스코프가 끝날 때 원래 x값을 돌려주는 그런 개념이었다. 하지만 이는 함수 내 함수 호출 같은 경우 변수 값이 제대로 전달 안 되는 문제를 발생시켰다.

 그래서 만들어진것이 정적 스코프이다. 기존의 스코프는 이름들을 하나의 큰 판에 담아놓고 공유 했다고 한다면, 정적 스코프는 지정된 스코프마다 판을 하나씩 만들어서 따로 관리하는 것이다. 그래서 값이 전달 안되는 등의 문제를 해결할 수 있었다. 그러나 여전히 정적 스코프도 일부 언어에서 문제가 남아있는데 Python이나 Ruby 언어를 사용한다면 주의해야 한다.(자세한 내용은...책을..) 주로 함수 내부에서 내포된 함수를 정의하는 경우 발생한다.

 

프로그래밍에서 이름을 짓는 네이밍과 범위를 구분지어서 생각하는 스코프의 개념은 폭넓게 사용되는 중요한 기본기 중 하나다.

 

8장 형(Type)

 마치 찰스 펫졸드의 CODE를 연상시키는 장. 타입에 대해 설명해주기 위해 비트로 숫자를 표시하는 것에서부터 비트로 10진수, 2진수, 8진수, 16진수를 표시하는 법, 비트로 실수를 표시하는 법 등을 예시로 설명해준다. 결국 타입이라는 것이 왜 존재하는지는 형이 없는 경우 어떤 문제가 발생하는지를 보면 되는데, 서로 다른 형식의 숫자가 비트 구조 자체는 비슷한 경우를 구분하기 위해서라고 이해할 수 있게 된다.

 그 외 이 장에서는 숫자에서 시작한 타입이 어떤 식으로 다양하게 발전해가는지 소개하면서 마친다. 그리고 이 타입의 다양화로 인해 기본형 타입과 참조나 사용자 정의형 타입을 구분해서 이해해야 할 필요가 생겼다.

 

9장 컨테이너와 문자열 

컨테이너라는 게 엄청 새로운 걸 설명하는 건 아니고, 결국 자료구조에 대해서 주로 설명해준다. 중요한 포인트는 자료구조마다 장단점이 있으니 쓰임새에 맞게 써야 한다는 것, 그리고 같은 자료 구조라도 언어마다 세부 기능, 로직은 다를 수 있다는 점이다.

문자란 무엇인가, 문자를 컴퓨터에서는 어떻게 다뤘었나 역사를 통해서 소개해준다.  사람이 다루는 문자를 만국 공통으로 표준화해서 다루기 위해 어떤 노력이 있었는지도 알 수 있다.

문자의 집합을 담아둔 문자열을 각 언어들이 어떤 곳에 어떻게 담아뒀는지 등을 차이점의 예시를 보면서 이해를 돕는다.

 

 이쯤 돼서 느껴지는 감상. 이 책 세 번째 읽는데 내용이 기억 안 나는 부분이 너무 많다. 새로운 접근법으로 의식적 노력을 해야 할 필요성을 느낀다. 지금 내게 생각나는 전략은 단권화가 있다.

 

10장 병행 처리

 컴퓨터 내에서 멀티 태스킹이 어떻게 처리되고 있는지 이해하기 위한 핵심 개념인 프로세스, 스레드를 이해하기 위한 장. 일단 병행 처리를 하기 위해선 어떤 규칙을 통해서 동시에 일을 진행할지 정해야 한다. 크게 두 가지 규칙이 있는데, 첫째는 협력적으로 교대하기 좋아진 시점에 교대하며 처리하는 것, 둘째는 일정 시간을 정해서 시점 위주로 교대하며 처리하는 것. 컴퓨터에서는 먼저 첫 번째 규칙을 적용했었으나 프로그램 간의 높은 신뢰성을 기반으로 한 규칙이기 때문에 잘 사용하지 않게 됐다. 두 번째 규칙인 선점적 멀티태스크는 경합 상태(Race Condition) 문제가 발생했다. 이 경합 상태가 발생하는 조건은 세 가지다.

 

첫째, 2가지 이상의 처리가 하나의 변수를 공유.

둘째, 그중 적어도 하나의 처리가 공유한 변수 값을 변경.

셋째는 하나의 처리가 마무리되기 전에 다른 처리가 끼어들 가능성이 있는가이다.

 

이 세 가지의 조건에 따라서 각각 해결책으로 제시된 방법들이 있었다.

 

첫째, 공유하지 않기 위해서 프로세스라는 단위를 만들고, 서로 다른 프로세스는 메모리를 공유하지 않는다는 규칙을 만들었다. 하지만 이것도 결국 한계가 있었고, 프로세스 간 메모리 공유를 돕는 스레드라는 단위가 만들어졌다. 이 스레드를 더 효율적으로 다루기 위한 액터(Actor) 모델이 만들어져서 비동기 처리라는 개념도 적용되게 됐다.

둘째, 변경하지 않는다. 변수에 값을 한번 선언하면 그 값을 나중에 변경하지 않는 규칙이 다양한 언어에서 생겨났다. const, val, Immutable  패턴 등이 그 결과물이다.

셋째, 끼어들지 않는다. 협력적 스레드를 사용하는 방법.(병행처리를 위한 큰 규칙 둘 중 하나가 부분적으로 적용됐다), 스레드로 처리되는 태스크 중에는 아예 끼어들면 안 되는 처리가 존재하는데 이를 위해 락을 거는 처리가 발전하게 됐다. 즉, 스레드끼리 서로를 신뢰하면서 처리가 되는데 그중에 특히 중요한 처리는 락을 거는 방식이다.

 

근데 이 락방식도 주요한 단점 두 가지 있다.

첫째는 교착상태에 빠지는 데드락(DeadLock) 상태다. 쉽게 말하면 락을 건 주요 태스크들끼리 서로를 기다리느라 완전 멈춰버린 상태로 대기하게 되는 거다. 락이 죽어버렸으니 데드락. 이런 데드락들은 처리도 안되고 컴퓨터 자원만 갉아먹고 있게 된다.

둘째는 락 태스크 1과 락 태스크2 사이에 어중간한 중간상태가 필요한 경우가 있는데 이때 끼어들면 문제가 발생한다. 이런경우 락 태스크 1과 락태크 2를 감싸는 락을 하나 더 만들어야 되는 불편함이 생긴다. 결국 락을 쓰기위해서 락을 더 쓰는 불편한 상태가 되는 것. 이 문제를 해결하기 위해 트랜젝션 메모리(Transaction memory)라는 기법이 적용됐다. DB에서의 Transaction 처리 개념을 메모리를 처리할 때 적용한 것으로, 락 태스크1 -> 어중간 처리 -> 락 태스크 2의 처리 과정을 원본 메모리 데이터와는 별도로 뽑아서 복사본에서 트랜잭션처럼 하나로 블록화 한 후 모든 처리를 마쳐야 원본에 최종 결과에 적용되도록 하는 것. 근데 이 방법은 컴퓨터 자원을 많이 잡아먹고, 처리가 많아졌을 때 버려야 되는 몇몇 처리는 버려야 되는 등 부작용이 있어서 아직까지 적용을 시험하는 단계라고 한다. 내가 들어본 기술은 자바의 Clojure가 있다.(2013년 기준이다. 지금은 어떨까?)

 

 참고로 멀티 프로세스, 멀티 스레드를 말할 때 ‘병렬 처리’ 같은 단어를 쓰지 않도록 주의하자. 컴퓨터 용어적으로 병렬과 병행은 완전 다른 개념이다.

 

11장 객체와 클래스

 현대에서 일하는 프로그래머라면 객체가 무엇인가에 대해서는 꼭 이해하고 있어야 될 것, 그리고 이 객체를 이해하려면 객체 지향이라는 개념이 무엇인지를 이해해야 한다. 객체 지향은 SmallTalk 언어의 창시자인 Alan Kay가 최초로 발명한 용어로 현실 세계의 사물(object)의 모형(model)을 컴퓨터 세계 안에 만들려는 방향성이다. 이런 방향성을 충족시키기 위해서 발전한 게 객체 지향의 개념이다.

히로카즈는 클래스가 왜 필요한지를 이해시키기 위해서 클래스처럼 사물의 모형 역할을 하는 방식 세 가지를 먼저 설명해준다.

 

첫째, 모듈, 패키지

둘째, 함수까지 해쉬에 담기

셋째, 클로저(Closure)

 

 나는 자바 기반으로 프로그래밍을 배워서 그런지 클래스와 같은 기능을 하는 객체 지향 프로그래밍이 어떻게 돼야 하는지 명확하게 알 수 없었는데 이 세 가지 방식을 통해서 좀 더 시야를 넓힐 수 있었다.

 첫 번째 방법은 모듈이나 패키지에 함수들만 엮어서 보관하고, 나머지 함수에서 쓸 변수는 해쉬(이름과 값 대응 표)에 담아주고 패키지랑 연결해준다, 변수를 초기화하는 함수도 패키지 안에 만들어 두는 등 조치를 해준다. 이런 방식으로 구현하는 언어는 Perl이 대표적이다. 이렇게 잡다하게 신경을 쓰니 클래스처럼 쓸 수 있겠더라.

 두 번째 방법은 모듈에 넣던 함수도 그냥 해쉬에 보관해버리는 것이다. 이 방식의 대표적인 언어가 Javascript인데 함수를 int나 byte 변수에 대입할 수 있는 퍼스트 클래스로 만들어 버렸다. 이렇게 하면 아무튼 클래스처럼 쓸 수 있다. 다만 이 방법을 그대로 사용하면  메모리 낭비가 심해질 수 있으므로(공통된 부분마저 다 각자 메모리를 할당해야 함), 자바스크립트에서는 프로토타입이라는 개념을 만들어서, 공통되는 부분은 하나의 메모리만 사용할 수 있도록 내부적으로 설계해놨다.

 세 번째 방법은 클로저. 클로저는 함수를 함수 안에서 정의해서 닫힌 함수를 반환 값이나 변수에 대입하여 사용할 수 있게 만들어둔 것이다. 이 클로저가 왜 사물의 대체용인지는 개인적으로 의문이다. 하나의 동작밖에 못하는 사물이라니..? 클로저 내부에 다른 기능의 함수 여러 개를 구현해놓고 다양하게 쓸 수 없는 건가? 그게 현실의 사물인가 싶다. 설마 이렇게 제한되지는 않을 테고, 나중에 따로 공부해보도록 하자.

 

아무튼 클래스는 크게 세 가지 역할을 한다.

첫째, 결합체를 만드는 생성기 (결합체는 함수랑 변수를 묶어서 메모리에 할당한 개체를 말함)

둘째, 어떤 조작이 가능한지에 대한 사양(이건 정적 형 결정 언어만 해당)

셋째, 코드를 재사용하는 단위

 

첫째에 집중한 게 javascript와 perl.

둘째는 C++이나 Java 등의 언어가 좋아한다. Java나 C#의 인터페이스 개념을 보면 이해하기 쉬움.

셋째는 상속 개념과 밀접하게 연관된다.

 

12장 상속을 통한 재사용

 상속이 어떠한 개념인지 설명한 후, 상속의 높은 자유도로 인해 발생할 수 있는 문제를 크게 세 가지 방식으로 해결할 수 있다는 것을 소개해주는 내용.

 

 상속은 클래스로 분류된 것들 중 공통된 속성을 지닌 클래스들을 상속을 통해 재사용하기 편하게 해 준다. 상속을 어떻게 할 것인지는 크게 세 가지 접근법이 있다.

 

첫째, 일반화 / 특수화. 공통되고 일반적인 속성을 가진 클래스가 부모 클래스, 특수한 속성이 추가되면 그건 자식 클래스에 구현하는 방식이다.

둘째, 공통부분을 추출하기. 이 때는 그냥 여러 클래스의 공통부분 만을 따서 부모 클래스로 구현하는 것이다.

셋째, 차분 구현. 새로 구현되는 기능을 새로운 자식 클래스에 넣어주는 개념이다.

 

 이런 식으로 생각하기에 따라서 다양하게 구현될 수 있는 것이 상속이기 때문에 주의해서 사용하지 않으면 코드가 매우 복잡해지는 부작용이 있다. 깊은 상속 트리를 가지게 되거나, 상속관계에 있는 클래스가 너무 많아서 부모 클래스에서 하나의 속성을 고친 것이 어떤 영향을 미칠지 모르게 된다거나 등 재사용하기 편해진 대신 코드 자체를 이해하기 어려워져 버렸다. 그래서 업계에서는 상속의 깊이를 얕게 유지해서만 사용하거나, 리스코프 치환 원칙을 적용해서 상속을 함부로 못하도록 엄격한 규칙을 적용하기도 한다.

 

 하지만 이런 엄격한 규칙이 적용된 상속은 현실세계의 사물의 분류와 맞지 않는다. 당장 나라는 사람을 클래스로 분류한다고 했을 때, 나는 남편이고, 아빠이고, 프로그래머고, 졸꾸러기 지망생이고 등등 하나의 분류가 적용되지 않는 경우가 흔하다. 그래서 이런 다양한 분류를 가진 클래스를 만들기 위해 ‘다중 상속’이라는 개념이 적용됐다.

 이 다중 상속은 코드의 재사용을 매우 편하게 만들어준다는 장점이 있지만, 복잡도가 늘어난 만큼 문제점도 크다. 단순히 네이밍 이슈를 봐도,  A라는 변수명을 지닌 속성을 하나의 부모 클래스가 갖고 있을 때, 다른 부모의 속성 중에도 A라는 변수명이 있다면 어떤 속성을 호출하는 것이 될까? 이에 대한 해결책이 핵심이 되어 크게 네 가지 해결책이 있다.

첫째, 다중 상속을 금지하기

둘째, 메서드 해결 순서를 고민하기

셋째, 처리를 섞기(Mix)

넷째, 트레이트(trait) 사용하기

 

 첫 번째 방법은 Java나 C#에서 사용하는 방식이다. 다중 상속을 금지하고 대체하는 기술로 위임(delegation) 개념, 인터페이스 등을 적용해서 다중 상속을 통해서 해결하고자 하는 재사용의 편의성 이슈를 해결하려고 시도했다.

 두 번째 방법은 Python이나 Perl에서 채택한 방식으로, 다중 상속됐을 경우 호출이 중첩되면 탐색돼야 할 순서를 정해줬다. 무조건 부모를 호출하는 것은 아니고 C3 선형화라는 방식으로 자식을 먼저 탐색하고, 부모를 탐색하는데 부모도 더 먼저 만들어진 것을 먼저 탐색하는 규칙이다.

 세 번째 방법은 Python, Ruby에서 적용한 방식으로 다양한 클래스의 속성을 섞을 수 있는 믹스인(Min-in)이라는 클래스를 만들었다.

 네 번째 방법은 비교적 최근에 다양한 언어에 적용되고 있는 트레이트(trait)라는 개념으로, 다중 상속의 문제점을 근본적으로 해결하기 위해 개념부터 재정립한 것이다. 클래스에는 2가지 상반된 역할이 있는데 첫째는 인스턴스 생성기, 둘째는 재사용 단위이다. 첫 째 역할을 만족시키기 위해서는 필요한 모든 것을 가진 큰 클래스가 돼야하고, 둘째 역할을 만족시키기 위해서는 필요없는 기능을 가지지 않은 작은 클래스가 되야 한다. 이게 무슨..

 그럼 차라리 클래스를 인스턴스 생성기로서의 역할로 제한하고, 재사용 단위라는 역할에 특화된 작은 구조를 새로 만들 자라는 게 트레이트의 개념이다. 트레이트를 사용하면 믹스인으로 구현했을 때의 문제들을 해결할 수 있다. 그래서 사실상 믹스인의 상위 호환 방식이라고 봐도 된다.

 

 결국 상속의 코드 재사용성을 극대화하면서도 실수를 줄이는 방법은 클래스의 상반된 두 역할을 분리해내는 데 있다고 본다. 나는 첫 번째 방식과 네 번째 방식이 주요 해결책으로서 경쟁할 것이라고 생각하는데, 첫 번째 방식의 장점은 사양으로서의 클래스의 역할을 수행할 수 있다는 것이다. 다만 재사용성이 좀 떨어지는 단점이 있다. 네 번째 방식의 장점은 재사용성이 극대화된다는 것이지만, 클래스의 사양으로서의 역할이 약해져 실수할 가능이 더 높아진다.

 결국 이미 자신들만의 영역을 확고히 구축해 둔 이 두 가지 방법이 각자 어떻게 발전할지에 따라 대세가 정해질 거라고 생각한다. 일단 나는 trait는 써본 적이 없고 인터페이스만 써 봐서.. 실질적인 차이점이 어떨지는 Scala 등의 언어를 겪어봐야 알 수 있지 않을까 싶다. 그래도 그냥 넘어가는건 찜찜하니까 관련 내용을 정리해서 기술 블로그 글을 써놓아야겠다. 제목은 '인터페이스 vs 트레이트' 라고 지어야지.

 

마치며.

 하루에 한 문단씩 쓰기로 하고 서평을 쓰기 시작한지 10일여가 지나고, 더해서 서평을 더 다듬기까지 거진 14일 정도에 걸쳐서 이 서평을 마칠 수 있었다. 다 쓰고 보니 글자 수가 꽤 많은 서평이 되버렸는데, 하루하루 쪼개서 작업하다보니 부담도 크지 않았고 결과물도 나름 만족스럽게 나와서 보람차다. 앞으로도 이런 식으로 내 역량에 맞게 작은 단위로 매일 실천해보도록 하자.