[도서] '코딩을 지탱하는 기술' 정리

Junseo Kim·2021년 2월 11일
0

[도서]

목록 보기
1/5

효율적으로 언어 배우기

이 책은 특정 언어에 대한 것이 아니라 보편적인 지식을 쌓게 해주는 책이다. java가 지금처럼 10년 후에도 많이 쓰일 것이라는 보장은 없다. 따라서 쓰이는 언어가 바뀌더라도 기반을 단단히 쌓아 적응할 수 있어야한다.

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

프로그래밍 언어는 과거의 것을 점점 발전시켜 온 것이다. 좀 더 편리하게, 좀 더 효율적으로 변화되어 온 것이다. 현재 수 많은 언어가 존재하는 이유는 어떤 것을 편하게 발전시켰냐의 차이이다. 각 언어의 발전 목적이 다르기 때문에 언어끼리 '읽기 어렵다'나 '느리다'는 식의 비교는 유익하지 못하다. 자신의 목적에 맞는 언어를 선택하면 되는 것이다.

문법의 탄생

문법이란 설계자가 정한 규칙이다. 문법은 당연히 언어에 따라 다르며 연산자 종류도 언어에 따라 다르다. 1 + 2 * 3과 같은 식을 처리할 때 언어에 따라 표현 방법이 다르다. 하지만 구문트리로 표현해보면 거의 동일한 트리가 생긴다. 언어별 차이점은 '어떤 문자열을 쓰면 어떤 구문 트리가 생기는가'라는 규칙이다.

현재 프로그래밍 언어에 이해하기 어려운 작성법이 존재하는 이유는 모순 없이 해석할 수 있는 문법을 만드는 것 자체도 어렵고, 기존 존재하는 문법과 마찰되지 않게 만들다 보니 어쩔 수 없이 존재하는 것이다.

처리 흐름 제어

사람이 프로그램을 편하게 쓰고 읽을 수 있도록 규칙을 만든 것이 구조화 프로그래밍이다. if문이나 while문 등을 사용하여 코드 구조를 만드는 것이다. if문이나 while문, for문이 없어도 구문을 작성할 수는 있다. while문은 반복되는 if를 읽기 쉽게 표현하기 위해, for은 수치를 증가시키는 while을 읽기 쉽게 표현하기 위해 만들어진 것이다.

함수

함수는 코드의 일부를 분리해내는 것이다. 프로그램이 커지면 코드를 파악하기 어렵기 때문에 함수로 분리하는 것이다. 코드는 행수가 작으면 파악하기 쉬워진다. 또 함수로 분리하면 재사용할 수 있다. 같은 기능을 여러번 사용해야한다면 함수로 분리 후, 해당 함수를 호출만 하면 되는 것이다.

함수에 이름을 붙이는 것은 '처리가 시작되는 메모리상의 위치'를 수치 대신 알기 쉽게 문자열로 표현하는 것이다. 변수 또한 마찬가지다.

재귀함수는 함수 x안에서 함수 x를 호출하는 것이다. 재귀함수는 어떤 처리를 하고 있는 도중에 동일한 처리를 다른 인수에 대해 실행할 때 유용하다.

에러 처리

프로그램도 실패를 할 수 밖에 없다. 파일에 무엇인가를 기록하려할 때 이미 디스크가 가득찬 경우도 실패가 일어날 수 있는 경우 중 하나이다. 이때 실패를 알려주지 않는다면 사용자는 파일에 뭔가가 기록되었다고 착각할 수 있다. 이런 경우를 방지하기 위해 에러를 처리해줘야한다.

에러처리에는 2가지 방법이 있다.
1) 반환값으로 실패가 전달되어오면 함수를 호출한 곳에서 에러 처리를 하는 방법
2) 두 번째는 함수 호출 전 에러 코드를 등록해두고, 실패 시 에러 처리 코드로 점프하는 방법

프로그래머는 함수가 예외를 던질 가능성이 있다는 것을 잊어버릴 수 있다. 따라서 현재 예외 처리라고 불리는 것은 두 번째 방법이다. 명령이 어떤 예외를 던질 가능성이 있는지 선언하고, 실패할 것 같은 처리를 묶어둔다. 여기서 발전한 것이 현재 언어들이 사용하는 '실패할 것 같은 처리를 묶은 후 에러 처리를 나중에 기술'하는 것이다. 언어마다 어떤 경우에 예외를 던지는지는 모두 다르다. 정해진 정답이 없기 때문이다.

이름과 스코프

함수나 변수의 이름이 사용되기 전에는 메모리 번지를 사용했다. 하지만 숫자를 사용해서 부르는 것은 알아듣기 힘들었고 알기 쉬운 이름을 대신 붙여 사용하기 시작했다.

이름을 붙이기 위해 이름과 메모리 번지를 대응시키는 표를 사용한다. 초기에는 하나의 대응표를 프로그램 전체에서 공유했다. 이런 경우 함수 내부에 동일한 변수명을 사용한다던지 하면 충돌이 발생한다. 이를 해결하기 위해 스코프라는 개념이 등장했다.

스코프

스코프는 이름의 유효 범위를 말한다. 프로그램 전체에서 이름이 충돌하지 않게 관리하는 것은 사실상 어렵기 때문에 이름의 유효 범위를 좁게 설정해 관리한다.

스코프는 동적 스코프와 정적 스코프가 존재한다. 동적 스코프는 함수 호출 시 처음에 원래의 값을 기록해두고, 함수를 나갈 때 원래의 값으로 되돌리는 방법을 사용한다. 하지만 변수 변경 후, 함수 내부에서 다른 함수를 호출하는 경우 새로 호출된 곳에 영향을 끼칠 수 있다는 문제점이 있다.

정적 스코프는 함수 호출 시 마다 새로운 대응표를 만들어서 사용하고 함수를 벗어나게 되면 해당 대응표를 삭제한다. 함수마다 대응표를 만드는 것이 아니라, 함수 호출 마다 대응표를 만드는 것이다. 따라서 함수(A) 내부에서 또 다른 함수(B)를 호출하면 B에 해당하는 새로운 대응표가 만들어지고, 이 대응표에는 A에서 변경한 값이 들어있지 않으므로 전역 대응표를 살펴보게 되고 전역 대응표에 존재하는 값을 사용하게 된다. 따라서 A안에서의 변경이 A밖까지 영향을 주지 않게되었다. 일반적으로 현재 언어들은 이런 정적스코프를 사용하고있다.

컴퓨터에 저장된 데이처는 0과 1의 집합으로 이루어져있다. 이런 비트열은 어떻게 해석하냐에 따라 다른 값이 되어버리기 때문에 '어떤 종류의 값'인지를 나타내는 정보를 추가한 것이 형의 시작이다.

같은 값이라도 정수를 표시하는 법과, 소수를 표시하는 법은 매우 다르다. 예를 들어 정수 7은 00000000000000000000000000000111 이지만 부동 소수점 7.0은 01000000111000000000000000000000 이다. 컴퓨터에게는 정수의 연산과 부동소수점의 연산은 엄청나게 다른 연산이다.

이를 해결하기 위해 이 변수가 정수인지 소수인지를 컴퓨터에게 알려서 컴퓨터가 기억할 수 있게 했다. 이것이 변수형 선언이다. 이런 형 정보를 이용해서 어떤 연산을 처리하는지를 컴퓨터가 판단한다.

기본적인 형 뿐만 아니라 구조체나, 클래스와 같은 사용자 정의형이 나왔고 '형은 사양이다'라는 개념이 등장하면서 접근제어자, 인터페이스 등이 나왔다. 또 '구성 요소의 형을 일부만 바꿀 수는 없을까'라는 생각에서 제네릭, 템플릿 등의 총칭형이 나오게되었다.

컨테이너와 문자열

컨테이너

컨테이너란 '무언가를 넣기 위한 상자'이다. 하지만 같은 컨테이너라도 언어에 따라 의미가 다르게 사용될 수 있다.

컨테이너에 넣는 데이터는 메모리에 저장된다. 메모리에는 정해진 크기의 상자가 정렬되어 있고, 각 상자는 번호가 부여되어 있다. 컨테이너 종류에 따라 메모리 저장 방법이 다르고, 그 차이에 따라 장단점이 발생한다. 배열, 연결리스트, 해쉬테이블, 트리 등이 컨테이너의 예시이다.

사용하는 목적, 어떤 사용법을 적용할지, 어떤 조작이 많은지에 따라 최적의 컨테이너가 달라진다. 즉 정답은 존재하지 않기 때문에, 상황에 맞는 컨테이너를 사용하면 된다.

문자열

문자란 인간이 '이것은 문자라고 부르자'고 정한 기호 집합이다. 나라나 문화에 따라 다르다. 따라서 맘대로 정해버리면 되지만, 문자를 부호화하는 사람과 부호를 문자로 되돌리는 사람은 같은 규칙을 공유해야한다.

예전에는 문자 부호화 방식이 제각각이었고 컴퓨터에 다라 문자열을 읽기 위해 일일이 변환을 해주어야했다. 이를 위해 문자 부호화 방법을 표준화하자는 움직임이 있었고 그것이 ASCII(아스키)이다. 그 후, 나라마다 '독자 문자 세트'를 표준화한 Unicode가 나오게 되었다.

문자열은 문자들이 들어있는 것이다. 문자열도 여러 차이가 있으며 각 언어가 지원하는 문자열이 동일하다고 볼 수 없다.
1) 문자 집합의 차이: 무엇이 문자인지
2) 문자 부호화 방식의 차이: 어떻게 문자를 비트열로 표현하는지
3) 문자열 구현: 어떤 정보를 어떤 메모리에 저장하는지

병행 처리

컴퓨터를 사용할 때, 음악을 들으면서 브라우저를 사용하는 등 동시에 여러가지 프로그램을 실행할 수 있다. 이런 것을 병행 처리라고 한다. 편리한 병행 처리를 위해 프로세스나 스레드가 생겼고 이로 인한 문제점을 해결하기 위해 락이나 파이버의 개념이 생겼다.

최근 PC는 CPU안에 여개 회로가 장착되어 있지만(멀티 코어) 싱글 코어인 경우에도 복수의 처리를 동시에 할 수 있다. 그 이유는 사람이 눈치챌 수 없는 짧은 순간에 복수의 처리를 변경해가면서 실행하기 때문이다. 즉, 한 순간에는 하나의 처리만 실행되지만, 사람이 느끼기에는 동시에 여러 처리가 일어나는 것 처럼 느껴지는 것이다.

이렇게 하기 위해서 각 프로그램의 처리를 '언제 교대할 것인가'를 정해줘야 한다. 이 방법에는 2가지가 있다.

1) 협력적 멀티태스크: 타이밍이 좋은 시점에서 교대하는 것. 즉, 처리가 끝나면 자발적으로 처리 교대. 하지만 어떤 처리가 교대해도 좋다고 말하기 전까지 다른 처리들은 계속 기다려야하는 문제점이 있다.

2) 선점적 멀티태스크: 일정 시간에 교대하는 것. 태스크 스케줄러를 이용해 각 처리마다 시간을 배분해주고 강제로 중단시켜 다른 처리가 실행될 수 있게한다.

현재 OS들은 선점적 멀티태스크를 채택하고 있지만, 경합 상태라는 문제점이 존재한다.

  • 2가지 처리가 변수를 공유 -> 프로세스, 스레드, 액터
  • 적어도 하나의 처리가 그 변수를 변경 -> const, val, Immutable
  • 한쪽 처리가 마무리 되기 전에 다른 처리가 끼어들 가능성이 존재 -> 협력적 스레드, 락, 뮤텍스, 세마포어

이 3가지 조건 중 하나라도 제거할 수 있다면 병행 실행 시 안전해진다.

'한쪽 처리가 마무리 되기 전에 다른 처리가 끼어들 가능성이 존재' 조건을 막기 위해 락(사용중을 나타내는 것)이 사용되지만 락도 문제점이 존재한다.

  • 교착상태(공유된 자원을 변경하는 처리들이 서로 상대방의 락이 풀리기를 기다리고있는 상태)
  • 합성할 수 없다. -> 트랜잭션 메모리

객체와 클래스

객체 지향이라는 개념이 생긴 이유는 '현실 세계에 있는 사물의 모형을 컴퓨터 안에 만들려면 어떻게 해야 할까?', '어떻게 하면 보다 편해질 수 있을까?' 와 같은 고민에서 생겨난 것이다. 하지만 언어에 따라 구현 방법과 객체 지향의 의미는 다르다.

프로그래머는 '하나로 모아서 모형을 만들고 싶다'는 목적을 가지고 있었다. 이런 생각에서 발전된 방법으로는 모듈, 패키지, 함수를 해쉬에 넣기, 클로저, 클래스 등의 방법이 있다.

그 중 클래스에 대해 알아보자. 클래스는 처음에 분류를 하기위해 만들어졌다. c++에서의 클래스는 형(type)이었다. 새로운 형을 정의해서 사용하기 위한 것이었다. 객체가 어떤 메소드를 가지고 있고, 가지고 있지 않은지에 대한 사양이라고 볼 수도 있다.

클래스는 3가지의 역할을 합쳐놓은 것이다.
1) 결합체를 만드는 생성기(설계도, 붕어빵 틀)
2) 어떤 조작이 가능한지에 대한 사양 -> 이 개념이 발전한 것이 인터페이스
3) 코드를 재사용하는 단위 -> 상속

상속

어떤 클래스에서 선언된 것은 그것을 세분화한 자식 클래스에게도 자동으로 이어져야하는데 이 개념이 상속이다.

상속은 크게 3가지 측면으로 접근할 수 있다.
1) 일반화/특수화: 부모 클래스는 일반적인 기능을 구현하고, 자식 클래스로 목적에 맞는 특화된 기능을 구현
2) 공통 부분을 추출: 복수 클래스의 공통 부분을 부모 클래스에서 추출
3) 차분 구현: 상속 후 변경된 부분만을 구현하면 효율이 좋다.

상속은 자유도가 높은 만큼 많이 사용하면 복잡해질 수 있다. 어떤 객체가 메소드X를 가지고 있을 때, 이 X가 어디서 정의된 건지 알기 힘들고, 메소드의 변경이 모든 자식 클래스에 영향을 미치게된다.

리스코프의 치환 원칙

T형의 객체 x에 관해 어떤 속성 q(x)가 항상 참이라고 한다. S가 T의 파생형이면, S형의 객체 y의 속성 q(y)가 항상 참이어야 한다

이 말은 '어떤 클래스 T의 객체에 대해 항상 성립하는 조건이 있다면, 그 조건을 자식 클래스 S의 객체에서도 항상 성립해야만 한다'는 것이다. 이 원칙을 '상속은 is-a 관계가 아니면 안 된다'고 표현하기도 한다.

다중 상속

현실 세계에서는 하나의 사물이 복수의 분류에 해당되는 경우가 존재한다. 이를 위해서 다중 상속이 나왔다. 하지만 다중 상속은 충돌이 발생할 수 있다는 단점이있다. 자식 클래스에 x라는 메소드가 없고, 2개의 부모 클래스에서 각각 x라는 메소드를 가지고 있는 경우이다.

이런 문제점 때문에 Java에서는 다중 상속을 금지시켰다. 즉 Java에서는 '코딩 시 재사용을 목적으로 한 상속'을 피하려고 한다. 대신 위임(객체를 가지고 있게하고 필요한 경우 가지고 있는 객체에서 호출)이라는 개념을 사용한다. 또 다른 방법으로 인터페이스를 사용한다. Java에서 다중 상속은 불가능하지만 인터페이스는 여러개 구현할 수 있다.

인터페이스는 코드를 가지고 있지 않는 클래스이다. 인터페이스를 구현한 클래스는 인터페이스가 가지고 있는 메소드를 반드시 구현해줘야한다. 인터페이스는 어떤 이름을 가진 메소드가 존재한다는 사양만 알려주는 것이기 때문에, 메소드 이름이 충돌해도 아무 문제가 발생하지 않는다. 실제 구현은 인터페이스를 구현한 클래스에서 해준다.(이때 앞에 적어준 인터페이스의 메소드를 override한다)

0개의 댓글