코딩을 지탱하는 기술

Prologue

  • 수많은 프로그래밍 언어가 존재한다. 하지만 시간은 제한되어 있다.

  • 비교를 통한 학습
    • 다수의 언어를 비교하는 것.
    • 무엇이 그 언어만의 특징이고, 무엇이 공통점인지 배울 수 있다.
  • 역사를 통한 학습
    • 언어의 발달 과정을 따라가는 것.
    • 탄생과 변화를 배움으로 왜 이런 식으로 동작하고 있는지에 대한 의문을 풀 수 있다.
  • 만드는 것을 통한 학습
    • 직접 언어를 만드는 것.
    • 나라면 어떻게 만들까? 라는 생각을 통해 설계자의 의도를 이해할 수 있다.

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

  • 언어에 의존하지 않는 보편적인 지식이 중요.

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

  • FORTRAN의 등장.
    • FORTRAN이 최초의 프로그래밍 언어는 아니다. 어떤 것이 최초의 프로그래밍 언어인지는 어려운 문제다.
    • 하지만 FORTRAN이 현재 프로그래밍 언어와 비슷하다고 생각한다.
    • Formula Translating System
    • 수식 변환 시스템이라는 뜻이 의미하듯, 수식을 기계어로 변환하는 것이 특징.
    • 효율은 매우 떨어지나, 코드량이 크게 줄어들었고 가독성이 크게 향상되었다.

      내가 이룬 성과의 대부분은 나태함에서 오고 있다. 나는 프로그램 짜는 것을 좋아하지 않았다. 그래서 프로그램을 쉽게 짤 수 있는 시스템을 만들었다. -John Backus

  • ※ 프로그래머의 삼대 미덕
    • Perl의 설계자인 Larry Wall의 저서에서 프로그래머가 가져야 할 3가지 자질을 소개했다.
    • 나태, 조바심, 자만심.

    • 나태 : 노동력을 줄이기 위해 만든 프로그램은 다른 사람들도 사용하게 되며, 그 프로그램에 관한 질문에 일일이 답하는 수고를 덜기 위해 문서를 만들게 된다.
  • 프로그래밍 언어의 목적은 편리함.
    • 다양한 언어가 존재하는 이유는 편하다는 의미가 사람마다 다르기 때문이다.
  • 언어는 도구다. 어떤 언어가 자신의 목적에 적합한지 판단.
    • 다른 사람들이 사용하고 있으니까 라는 이유는 상관없다.
    • 자신이 어느 정도 성과를 낼 수 있는지를 고려해서 결정해야 한다.

3장. 문법의 탄생

  • 3 + 3 * 2 = ?
    • 위 식의 답은 9이다.
    • 왜 12가 아닌가?
    • 곱셈을 먼저 수행한다라는 규칙이 있기 때문.
  • 문법은 언어 설계자가 정한 규칙이다.

스택머신과 FORTH 언어

  • FORTH라는 언어는 문법이 거의 없다.
  • 설계자 Charles H. Moore에 따르면
    • FORTH는 가장 간단한 컴퓨터 언어 이다.
    • 세상의 모든 언어가 가독성이 있다고 주장하고 있지만, 처음 그 언어를 다루는 사람은 항상 당황한다.
    • 이는 난해하고 변덕스러운 문법 때문이다.
    • FORTH는 구문을 최대한 제한함으로 문제를 해결한다.
  • java, python, ruby 등은 스택 머신 형의 VM을 사용하고 있다.
    • 내부적으로 FORTH와 같은 프로그램으로 컴파일 되어 동작한다.

구문 트리

  • 구문 트리
    • 1과 2를 더한 후 3을 곱하라.
    • SyntaxTree
    • Parser는 소스코드를 문자열로 읽어 들여 해석하고, 그것을 구문 트리로 변경하는 프로그램.
  • 언어간의 차이는 어떤 문자열을 쓰면 어떤 구문 트리가 생기는가 이다.
    • 즉, 이 규칙이 문법(Syntax) 이다.

4장. 처리 흐름 제어

  • 규칙을 사용하여 코드 구조를 쉽게 만들자.
    • if, while 등 흐름을 제어하는 제어 구문의 탄생.
  • 어셈블러의 jump와 같이 goto를 사용하여 전부 해결 가능 하지만 가독성을 높이기 위해 if, while등의 제어구문이 고안되었다.
    • 즉 if, while등의 제어 구문은 조건(제한)이 붙은 goto와 같다.
  • goto -> while -> for -> foreach
    • 참고로 java에서는 foreach를 확장 for문 이라고 부르기도 한다.
    • while은 조건식으로 반복을 제어한다. for은 횟수로 반복을 제어한다. foreach는 처리대상으로 반복을 제어한다.
    • foreach 구문은 어떤 대상의 요소에 전부 어떤 처리를 한다. 의 개념.
  • 처리 흐름을 제어하는 구문들은 전부 알기 쉬운 코드를 구성하기 위한 규칙.

5장. 함수

  • 함수란, 코드의 일부를 한 덩어리로 잘라내어 그것에 이름을 붙이는 기능.
  • 이해를 돕고
    • 사람이 많은 조직의 부서를 나누는 것과 같은 원리.
  • 재사용성을 높이는 데 의의가 있다.
    • 모터, 타이어 등의 부품과 같다.
  • 함수의 탄생으로 인해 내포 구조를 다루는 방법, 재귀 호출 탄생
    • 내포 구조 : 어떤 물품이 그 물품 자신을 사용하여 만들어진 구조.

6장. 에러 처리

  • 프로그램도 실패를 한다.
  • 실패 한 경우에 어떠한 알림도 없다면, 사용자는 실패를 알아차리기 힘들다.
  • 빨리 알아차리지 못하면 큰 사고로 연결될 지도 모른다.
  • 때문에 실패를 알리는 구조가 필요하다.

  • 에러 처리를 어떻게 하면 좋을까?
    • 1) 반환 값으로 실패 플래그를 전달한다. 사용 시 반환 값을 체크하여 실패일 때 에러를 처리한다.
    • 2) 에러처리 코드를 등록해두고, 실패를 던지면 에러 처리 코드로 점프한다.

1) 반환 값을 체크하여 실패일 때 에러를 처리한다.

  • 1)의 문제점.
    • 실패를 놓친다.
      • 프로그래머가 함수를 사용한 후 반환값 체크를 잊는 경우.
      • 실패가 발생한 타이밍과 문제를 발견하는 타이밍이 다르다.
        • 반환값을 확인하는 로직이 바로 연속적으로 위치해 있지 않을 수도 있다.
        • 이를 계기로 다른 함수도 실패하는 사이드 이펙트가 발생할 수도 있다.
    • 에러 처리 때문에 코드를 해석하기 어렵다.
      • 아래는 func 라는 함수가 실패할 수도 있다고 가정했을 때.
    if(!func("A")){
        /*
        실패했을 때 처리
        */
    }
    else if(!func("B")){
        /*
        실패했을 때 처리
        */
    }
    else if(!func("C")){
        /*
        실패했을 때 처리
        */
    }

원래 하고 싶은 것은 3개의 처리를 실행한다 였지만 장황해졌다.

  • 에러 코드를 한 곳에 정리해놓는다면, 실패했을 때의 처리원래 하고 싶은 것을 기술한 코드를 분리할 수 있다.
    • 아래가 1)의 베스트 방법.
    if(!func("A")) goto ERROR;
    if(!func("B")) goto ERROR;
    if(!func("C")) goto ERROR;

    ERROR :
    /*
    실패했을 때의 처리
    */
  • 1)의 경우 C언어를 포함한 다양한 언어에서 사용하고 있는 예외처리 방식이다.

2) 실패를 던지면 에러 처리 코드로 점프한다.

  • 2)의 경우를 살펴본다.
    • 사람이 반환값을 체크하지 않고, 언어 처리 단계에서 실패를 체크한다.
GO: procedure;
    on error go to ERROR;
    call func(1);
    call func(2);
    call func(3);
    return;
    ERROR : < 에러시 처리 >
end;



자발적으로 실패할 것 같은 처리를 묶는 구문으로 발전

  • try / catch
  • why finally?
    • 예측하지 못한 종료가 발생했을 시에도 메모리 블록이나 파일 등의 리소스를 잘 닫을 수 있게 된다.
    • 짝이 되는 처리를 반드시 실행한다.
      • 예) lock을 했으면 unlock.
    • 출구는 하나다
    • C++은 finally가 없는 대신, 소멸자라는 방식을 택한다.
  • 예외적 상황에 정답은 없으나, 저자는 틀리면 바로 예외를 던지는 것을 추천한다.
    • Fail first
      • 이상하면 처리를 정지하고 빨리 보고해야 한다. 는 설계 이념

구체적인 지식과 추상적인 지식
언어 X로 Y를 하는 방법과 같은 구체적인 지식은 당신의 생산성을 바로 높일 수 있다. 그러나 언어가 바뀌면 무용지물이 된다. 세상은 계속 바뀌고 있다. 즉, 응용 범위가 제한된 구체적인 지식은 점점 그 가치를 잃어간다.
반면, 추상적인 지식을 배워도 여러분이 가지고 있는 경험과 연관시키지 못하면 응용할 수 없다. 벚꽃이 가지고 싶다고 꽃이 피어있는 가지를 잘라와도 꽃이 죽어버리면 그만이다. 매년 꽃을 피게 하기 위해선 뿌리가 필요하다.
뿌리가 없는 지식은 응용에 도달하지 못하고, 들어온 지식을 다시 토해낼 뿐이다. 상황에 맞게 지식을 활용할 수가 없는 것이다.

7장. 이름과 스코프

이름의 탄생.

  • 초기에는 12345번지에 있는 것을 98765번지로 이동해 라는 식의 instruction.
    • 알기 쉬운 이름을 붙여 그 이름을 사용하는 것이 보다 편리하다.
    • 12345번지를 책이라 하고, 98765번지를 책꽂이라 할때
    • 책을 책꽂이로 이동해. 라는 instruction이 가능해진다.
    • 컴퓨터가 이름과 번지를 대응시키느 대응표를 가지고 있다면 이렇게 가능하다.

스코프의 탄생.

  • 초기에는 하나의 대응표를 프로그램 전체에서 공유하고 있었다.
for(int i = 0; i < 10; i++){
    func1();
}
  • 위와 같은 상황에서 func1 이라는 함수가 i 라는 변수 이름을 중복하여 사용한다면,
    • 10번 반복 수행하려 작성했던 코드는 원하지 않는 동작을 수행할 수도 있다.
    • 이름이 충돌되었기 때문이다.
    • 이러한 충돌을 피하기 위하여 스코프 라는 개념이 탄생한다.

스코프.

  • 스코프란 이름의 유효 범위를 의미한다.
동적 스코프.
  • 원래의 값을 다른 곳에 피신시켜두고 나중에 되돌린다.
    • 즉, 함수 입구에서 원래의 값을 기록해두고 출구에서 원래의 값으로 되돌리는 것.
  • 문제점
    • 변수를 변경한 후 다른 함수를 호출한 경우 호출된 함수에 영향을 미친다.
String x = "global"

void method1(){
    x = "method1" // local
    method2();
}

void method2(){
    System.out.print(x);
}
  • 지금의 java에서는 global 이라고 출력하겠지만, 동적 스코프를 사용할 경우 method1이 출력되게 된다.
  • 동적 스코프에서는 변경된 값이 호출된 곳에 파급된다.

정적 스코프

  • 함수에 들어갔을 때 새로운 대응표를 준비한다
  • 함수를 벗어날 때 그 대응표를 제거한다
  • 참조한 경우는 가까운 순서대로 읽는다

  • 가장 큰 차이점은 함수 안에서 변경이 함수 밖에까지 영향을 주지 않는다

8장. 형 (Type)

IEEE 754 부동 소수점 구조

  • 32bit로 실수를 표현한다
  • 1.xxx * 2^y 로 표현한 후
  • y를 지수부, xxx를 가수부에 표현한다.

부동소수점

  • 첫 번째 비트는 부호
    • 0 : 양수
    • 1 : 음수
  • 다음 8개의 비트는 지수부
    • 0 ~ 255 를 나타낼 수 있고, 여기서 127을 뺀 값을 사용한다
    • -127 ~ 128
      • 즉 127 은 0
      • 0은 -127, 255는 128
  • 나머지 23개의 비트는 가수부
    • 첫째 비트는 1/2
      • 다음 비트는 1/4, 1/8, 1/16 …
  • 예를 들어 1.75를 나타내보자
    • 이진수로 표현하면 1.11 * 2^0 이 된다
    • 부호부는 양수이므로 0
    • 지수부는 0이므로 127, 01111111
    • 가수부는 11이므로 110000 … 0
  • 3.5를 나타내보자.
    • 이진수로 표현하면 11.1 이므로, 1.11 * 2^1
    • 부호부는 양수이므로 0
    • 지수부는 1이므로 128, 10000000
    • 가수부는 11이므로 11000 … 0
  • 아래는 1.75와 3.5를 부동소수점으로 1을 회색, 0을 흰색으로 표현한 이미지이다

부동소수점예제

  • 문제점
    • 대부분의 경우에는문제가 없으나, 3 / 10 과 같은 수 일 때 문제가 발생
      • 10진수 : 0.3
      • 2진수 : 0.0100110011001……
      • 무한 소수가 되어버려서 0.3 을 10번 더한 후 소수자리를 버려버리면 2가 됨

형은 무엇을 위해 존재할까?

  • 형이 없을 때 발생하는 문제
    • 3.0 + 7.0
    • 부동소수점 3.0 + 7.0 계산 = 10.0
    • 비트패턴으로 정수 3.0 + 7.0 = 매우 큰 값
    • 전혀 다른 값
  • 사용자 정의 형
    • C 구조체, java class
  • 구성 요소의 형을 일부만 바꾸는 형
    • C++ 템플릿(template), java 제너릭(generics)

9장. 컨테이너와 문자열

컨테이너

  • 만능 컨테이너란 없다
    • 컨테이너의 사용 목적을 상기하라
      • 어떤 조작이 많은가
      • 계산 시간을 줄일 필요가 있는가
      • 메모리를 절약할 필요가 있는가
    • 자신의 상황에 맞게 적합한 컨테이너를 선택해야 한다

문자열

  • C의 character 는 8비트(1 Byte)
    • for ASCII
  • Java의 character 는 16비트(2 Byte)
    • for Unicode

11장. 객체와 클래스

  • 원시형태의 클래스는 분류 였다
    • 이코노믹 클래스, 클래스(반) 과 같은 의미

12장. 상속을 통한 재사용

상속에 관한 다양한 접근법

  1. 일반화 / 특수화
    • 부모 클래스로 일반적인 기능을 구현하고 자식 클래스로 목적에 특화된 기능을 구현한다
  2. 공통 부분을 추출

  3. 차분 구현
    • 상속 후 변경된 부분만을 구현
    • 상속을 재사용을 위해 사용함으로 구현이 편해질 수 있다는 발상
  • 1번을 제외한 2,3 번은 리스코프 치환 원칙을 위배한다

다중 상속

  • 다중 상속은 코드 재사용 방법으로 매우 좋은 도구다
    • 사원 클래스를 나눠, 프로그래머 클래스와 영업 사원 클래스로 나누었다고 가정할 때
    • 한 사람이 프로그래머로서의 역할과 영업 사원으로서의 역할을 모두 가지는 경우는 충분히 있을 수 있는 일이다
    • 이 경우 클래스가 복수의 클래스를 상속하는 것이 자연스럽다
  • 다중 상속의 문제점 : 충돌
    • 다중 상속을 받은 클래스에게 x가 무엇인지 질의
      • 본인이 x를 알고 있다면 대답한다
      • 모른다면 부모 클래스에게 묻는다
      • 이 때, 부모 클래스 모두 같은 이름의 x를 가지고 있다면 어떻게 될까?
  • 해결법 : 위임
    • 상속받지 않고, 객체를 인스턴스화 하여 갖고 있고
    • 해당 객체의 메소드를 사용한다.
    • 아래의 UseDelegate 클래스는 Hello 클래스를 상속받지 않고 인스턴스로 보유하고 있으면서, 같은 기능을 제공하는 메소드의 이름을 달리하여 호출 시 실제 수행은 Hello 클래스가 하게 위임한다
    • 복수 클래스를 상속 사용하여 결합하는 것이 아니라, 위임을 사용해 조합하는 것이 낫다
    • 위임의 참조도 소스에 하드코딩 하는 것이 아니라 실행 시에 주입하는 것이 좋다 (Dependency Injection 탄생)
class UseDelegate {
    Hello h = new Hello();
    public void useHello(){
        h.hello();
    }
}