코딩을 지탱하는 기술
by Yoonkh
코딩을 지탱하는 기술
프로그래밍 학습에 중요한 포인트 3가지
- 비교를 통한 학습
- 역사를 통한 학습
- 만드는 것을 통한 학습
규칙은 언어마다 다르다
ex)
- C언어에서는 0이 참이지만, ruby에서는 0이 거짓이다. 자바에서는 0이 단순한 정수형을 의미하기 때문에 조건식에 0을 사용하게 되면 컴파일 에러가 발생한다.
잘못된 고정관념을 버려야 한다.
특정언어에 국한된 지식을 습득할 것이 아니라 모든 언어에 통용 될 수 있는 이해력을 기를 필요가 있다.
프로그래머의 삼대 미덕
- 나태함, 조바심, 자만심
프로그래밍 언어는 사람을 편하게 하기 위해서 만들어졌다.
- 언어는 도구일 뿐이다
가상소프트웨어의 선결과제 = 빠른 실행 속도
ex)
- C++ 은 빠른 실행 속도를 자랑하지만 높은 언어사양을 요구한다.
–
프로그래밍 언어에 대한 이해력을 확인하기 위해서는 아웃풋(결과물)을 확인해야한다.
에러메세지가 발생하면 당황하거나 무서워하지말아야 한다. 내가 만든 것을 언어 처리계가 열심히 읽어서 여기를 모르겠다고 대답해주고 있는 것이다 - 언어 처리계와 제대로된 커뮤니케이션을 하는 방법
-
아직도 스택 머신(FORTH)은 살아있다.
- Python이나 Ruby, Java로 프로그램을 짜면 그 프로그램은 내부적으로 FORTH와 같은 프로그램으로 변환(컴파일)되어 동작하고 있는 것이다.
–
언어마다 차이가 생긴 것은…
표현 방법이 틀릴뿐. 구문트리는 동일하다!
- ‘어떤 문자열을 쓰면 어떤 구문 트리가 생기는가’ = 이것이 규칙이자 문법이다.
현재 대부분의 프로그래밍 언어들은 다가가기 쉬운 언어 작성법을 목표로 하고 있다. 하지만 기존의 문법과 마찰되지 않도록하면서 새로운 문법을 추가하는 것은 굉장히 어려운 일이다!
–
if문
if-else 구문의 이점
- ‘조건이 참인 경우와 거짓인 경우의 처리 흐름을 분배한다’ 이것을 간단하고 읽기 쉬운 형태로 볼 수 있게 해준다!
else가 반드시 필요한 것은 아니다. 하지만 가독성 측면에서 사용하는 것이 좋다.
–
###반복문
for문으로 가능한 것은 while문으로도 가능하긴 하다.
while문 에서는 분산되어 있던 반복 조건문들이 for문에서는 한곳에 모여있기 때문에 루프문 작성자의 의도를 쉽게 파악 할 수 있다.
foreach구문은 ‘어떤 대상의 요소 전부에 어떤 처리를 한다’는 코드를 쉽게 쓰기 위해 만들어졌다.
Python의 for문은 foreach 구문이다. 역으로, C 언어의 for 문에 대응하는 구문은 없다!
for문에서도 조건식으로 제어하고 있지만, 주요 사용 목적은 횟수를 의식한 제어다!
ex)
- for문은 정해진 범위 내에서 루프
- while문은 범위가 없이 루프
바꿔 말하자면, for문은 반복수를 알 때 사용한다. while문은 반복수를 모를 때 사용한다.
–
함수
코드가 함수로 나누어져 있는 것은 큰 조직이 부서로 나눠져 있는 것과 같다!!!
함수를 만드는 것은 작은 부품을 조립해서 큰 부품을 만드는 것과 같다!
- 예를 들어 무선 조정 자동차 속에는 모터가 있고 모터 속에는 코일이나 정류자가 들어있다!! = 함수의 개념이다
-
무선 조정 자동차가 느려지면 알칼리 전지가 약해졌다고 생각 할 수 있다
-
근처 편의점에 가서 전지를 구해야한다 = 함수의 이해와 같다
함수도 동일하다. 수십 행 수백 행의 코드가 함수로 정리되어 제공되고 있어서 그 함수를 호출해서 사용이 가능하다.
프로그램을 만드는 것과 물리적인 것을 만드는 것의 차이 ( 비용의 차이 )
-
예를 들어 아파트 100세대에 급수기를 설치해야 한다면 100개의 급수기가 필요하다!
-
프로그램은 100개의 특정 데이터가 있으면 함수만 100회 호출하면 된다!
한계가 없다
ex)
- 1: 110의 점프 명령의 점프 목적지를 3으로 바꾼다
- 2: 함수를 호출한다 ( 100으로 점프 )
- 3: 다음 명령.
- …
- 51: 110의 점프 명령의 점프 목적지를 53으로 바꾼다.
- 52: 함수 호출 ( 100으로 점프 )
- 53: 다음 명령.
- …
- 100: 함수 처리
- …
- 110: 돌아간다 ( 0으로 점프 )
이렇게 해서 함수가 탄생했다
함수의 이름
메모리상에서의 위치
-
함수를 사용한 처리에 이름을 붙이는 행위는 ‘처리가 시작되는 메모리상의 위치’를 수치로 표현하는 대신, 알기 쉬운 문자열로 표현하는 행위다.
-
변수도 마찬가지다. ‘값이 기록된 메모리 위치’를 수치가 아닌 문자열로 표현하기 위해 만들어졌다.
스택 ( Stack ) 영역
- 복수의 데이터 값을 저장해두는곳
- 마지막에 넣은 것을 가장 먼저 꺼내는 것에 적합
- 할당된 메모리 구조는 위에서부터 데이터 - 힙 - 스택 순으로 배치된다. (스택은 가장 아래부분)
재귀 함수
재귀함수는 함수 X안에서 함수 X 자신을 호출 하는 것이다!
- 내포 구조를 다루는데 적합하다!
–
재귀함수의 구조를 이해하기위한 좋은예
ex)
xs = [1,2,[3,4],5]
def total(xs):
result = 0
for x in xs:
if is_integer(x):
result += x
else:
result += total(x)
return result
x는 내포 리스트이기 때문에 total로 안에 든 값을 합산한다!
–
에러 처리
프로그램 실행 시 실패가 발생하는 경우가 많다. 실패 시에 어떻게 하는가(에러처리)는 매우 중요하다!!
크게 방법은 두가지이다
- 반환값을 사용하는 방법
- 예외를 사용하는 방법
반환값으로 실패를 전달하는 방법에 대해서…
이 방법은 C 언어를 비롯해서 많은 언어가 사용하고 있는 중이다.
그러나
2가지 문제점이 있다.
- 실패를 놓친다.
- 에러 처리 때문에 코드를 해석하기 어렵다.
때문에 점프로 에러를 처리하며 처리에는 goto를 사용한다.
C 언어에는 예외기능이 없기 때문이다
에러가 발생했을 때 점프할 장소를 사전에 등록해두는 방법이 있다. 이방법이 발전해서 오늘날의 ‘예외 처리’가 된 것이다
UNIVAC I의 경우
- ‘계산 시 오버플로우가 발생하면 xxx번지에 있는 명령을 실행한다’는 기능이 있었다. 이런 기능을 ‘인터럽트(interrupt)’라고 불렀다. (예로들면 키보드 입력 처리)
COBOL의 경우
1959년에 두가지 에러처리를 탑재하고 나타났다!
-
READ로 파일을 읽을 때 ‘AT END’라는 구문으로 ‘더 이상 데이터가 없다’ 등의 에러가 발생했을 때
-
ADD등으로 수치 계산을 할 때 오버플로우가 발생했을 경우 ‘ON SIZE ERROR’ 구문으로 처리를 표현
PL/I의 경우
1964년 FORTRAN, COBOL, ANGOL을 집성해서 설계되었다.
새롭게 정의한 실패를 프로그래머가 발생 시킬 수 있었다.
프로그래밍은 짝이 되는 처리가 중요하다
미술관을 예로들면, 입구에서 빌린 음성 안내기를 출구가 하나라면 제대로 회수 할 수 있지만, 출구가 여러개라면 회수가 어려워 진다.
여기서 출구란 ( return )
이문제는 finally를 사용해서 짝을 맞춰 줄 수 있으며 해결 할 수 있다.
- finally 블록은 처리가 try 블록에서 벗어날 때 반드시 실행된다.
Microsoft사에서 처음 도입했으며, 1995년에는 Java도 도입했다. 현재는 Python이나 Ruby에서도 동일한 구문을 지원하고 있다.
C++은 finally를 갖고 있지 않다.
함수를 벗어날때 함수의 지역 변수에 대해서 자동적으로 소멸자가 호출되어 finally의 역할을 대신한다.
어떤 경우에 예외를 던질까?
Python과 Ruby는 함수 호출 시점에서 예외를 던진다. 그러나, JavaScript는 인수에 ‘미정의를 의미하는 특수한 값’을 사용해서 처리를 계속한다.
예외는 어떤 경우에 사용해야하는가?
예외적 상황이란 무엇인지에 대한 정답은 없다
프로그래머도 사람인 이상, 실수를 피할 수는 없다.
무언가가 이상하다면 빨리 문제를 발견하는 것이 중요하다. 학습이나 개발 단계에서는 틀리면 바로 틀린 것을 발견하는 것이 오히려 이점이 많다.
이름과 스코프
스코프란 변수나 함수의 이름이 유효한 ‘범위’를 말한다.
왜 이름이 필요한가?
지금은 있는 것이 당연하다고 느끼는 변수나 함수의 이름은 초기에는 메모리에 번호를 부여해서 해당 값을 찾는 방식이었으나, 이름으로 대상을 지정 할 수 있도록 꾸준히 발전해왔다.
이름의 충돌
초기 프로그래밍 언어에서는 대응표를 프로그램 전체에서 공유하고 있었다. 회사로 말하면 큰 화이트 보드가 있어서 모두가 메모를 하고 있는 상황과 같았다.
- 이것은 같은 이름을 지닌 값이 있다면 데이터를 덮어써버려서 충돌을 일으키는 것이 문제였다.
충돌 피하기
- 긴 변수명을 사용하는 방법
- 스코프를 이용하는 방법
스코프란??
이름의 유효 범위다.
이것은 동적 스코프와 정적 스코프로 나뉜다.
-
동적 스코프 : 함수 입구에 원래의 값을 기록해두고 함수 출구에서 리턴값을 원래의 값으로 되돌리고 내보내는 것이다.
- 문제점 : 동적 스코프의 문제점으로는 변수를 변경한 후에 다른 함수를 호출한 경우 호출된 함수에 영향을 미치게 되기 때문에 다루기 힘들다는 점이 있다.
-
정적 스코프 : 함수가 호출될 때마다 새로운 대응표를 만드는 것과 같다고 할 수 있다. 회사 전체가 공유하고 있는 커다란 화이트 보드에 작성하는 대신, 한명 한명이 자신의 책상에 메모 용지를 갖고 있는 것으로 비유 할 수 있다.
하지만 동적 스코프로 만들어진 대응표는 소스 코드 전역에서 읽고 쓸 수 있다. 정적 스코프와의 가장 큰 차이점이다!
- 동적 스코프가 만약 참조한 경우에는 가까운 곳부터 순서대로 읽는다
정적 스코프는 함수별로 대응표를 나눈다
-
특정 함수내에서의 이름변경이 함수 밖까지 영향을 주지 못한다. (유효 범위 분할이 가능)
- 현재는 많은 언어가 정적 스코프를 도입중이다!
그렇다면 정적 스코프는 완성체인가?
정적 스코프의 특징이 편리해 보일 수도 있지만 2가지의 문제점을 지니고 있다.
-
내포 함수의 문제점 : 내포된 것처럼 보이는 함수가 실제로는 내포되지 않은 것이다.
-
Python 2.0 을 예로들면, 특정 함수의 지역 스코프에서 찾고자하는 이름을 발견하지 못할 경우, 전역 스코프를 보게 된다.
- 이는 소스 코드 상에서 가까이에 있는 정의가 사용되지 않는 예이다. 지금은 가까이에 있는 정의를 사용하도록 수정되었다.
-
-
외부 스코프에 재귀속되는 문제
내포한 스코프의 외부 변수를 변경할 수 없다
- 함수안에서 대입하면 그 함수의 지역 변수가 되는데, 해당 스코프에 이미 같은 이름의 변수가 있으면 해당 변수를 재귀속 시키고, 없으면 해당 스코프에서 새로운 지역 변수를 작성한다.
어느쪽이든 외부 스코프에 영향을 미치지 않게 되므로 이것은 외부 스코프에 있는 변수를 변경할 수 없다.
형
형이란 수치를 On과 Off로 표현하는 방법이다
- 컴퓨터 안에서는 On과 Off, 0과 1의 집합으로 모든 값이 표현된다.
자릿수의 발명
컴퓨터가 탄생하기 1000년 이상 전에 인류는 자릿수 기수법을 발명했다.
- 하나의 자리에 0에서 9까지 10가지 기호를 사용해서 수를 표현하는 방법이다!
7 세그먼트 디스플레이
계산기 등에서 수치를 표현하기 위해서 만들어졌다.
- 7 세그먼트 디스플레이를 사용하면 7개의 램프로 한 자릿수를 표현할 수 있어서, 3자릿수를 표현하기 위해 필요한 램프의 개수는 21개가 되고, 이는 기존의 것 보다 6개를 줄일 수 있었다.
주판
하지만 더 효율적인 방법이 나타났다. 바로 주판이다. 5개로 하나의 자리를 표현하는 방법이다.
- 3 자릿수를 표현하려면 15개의 램프가 필요하다. 이것으로 6개를 더 줄일 수 있었다.
램프 4개로 16가지를 표현할 수 있음에도 불구하고 10가지만 사용한다면 아까운 느낌이 든다. 좀 더 간격을 채워 넣을 방법이 없을까?
1의 자리, 10의 자리, 100의 자리(10 x 10), 1000의 자리(10 x 10 x 10)로 자릿수를 맞추는 대신, 1의 자리, 2의 자리, 4의 자리(2 x 2), 8의 자리(2 x 2 x 2)로 맞추자는 발상이다! 이것이 바로 2진수다!
2진수를 사용하면 10개의 램프로 0부터 1,023까지 표현이 가능하다
8진수
8가지 부호를 사용해서 자릿수를 맞추고 있어서 8진수라고 불린다.
- 한 묶음에 2 x 2 x 2로 8가지 패턴이 존재한다.
16진수
16가지 부호를 사용해서 자리를 맞추고 있기 때문에 16진수라고 불린다.
- 8진수 수치의 앞자리에는 0이나 0o를 붙이고, 16진수 수치에는 0x를 붙이는 경우가 많다.
실수는 어떻게 표현할까?
1.5나 0.001 등 소수점이 있는 ‘실수’는 어떻게 표현할까?
두가지 방법이 있다.
- 고정 소수점
-
부동 소수점
- 고정 소수점 ( 소수점을 어디에 붙일지 정한다 )
- 예를 들면 ‘이 정수는 소수점을 4자리 이동시켜서 소수점 이하 4자리를 소수부’라고 정하면 1이 0.0001이 되고, 100이 0.0100, 즉, 0.01이 된다. 하지만 0.0001보다 작은 값을 표현 할 수가 없고, 각각의 값에 대해 ‘어디까지 소수부로 정했는지’를 사람이 기억해야 한다.
- 부동 소수점 ( 어디부터 소수부인지의 정보 자체를 값에 포함시킨다 )
- 소수점을 이동시키는 방법으로 이 방법을 사용하면 1,023 이후에 0이 30개 계속되는 큰 숫자를 표현 할 수 있다.
- 고정 소수점 ( 소수점을 어디에 붙일지 정한다 )
현재는 표준화 되어 IEEE 754가 적용되고 있다
IEEE 754에서는 ‘지수부’가 소수점 위치에 해당하고, ‘가수부’가 유효 숫자의 소수점 이하 부분에 해당한다.
문제점
대부분의 경우에는 별 문제가 없지만, ‘3 나누기 10’의 결과를 표현하려고 하면, 10 진수로는 0.3을 사용해 표현 할 수 있지만, 2 진수의 경우 무한 소수가 되어 버린다.
–
컴퓨터는 비트열만으로 그것을 정수로 해석해야 할 지 부동 소수점으로 해석해야 할 지 알 수가 없다. 따라서, ‘이 값이 어떤 종류인지’ 정보가 별도로 필요한데 이것이 바로 ‘형’이다.
초기 FORTRAN의 형
변수명에 안에 든 것이 무엇인지 표현하기 위한 규칙을 만들게 되었다. 초기 FORTRAN에서는 변수명 선두가 I ~ N 이면 정수, 그 이외이면 부동 소수점이 들어 있다는 규칙을 사용했다.
- 다른 한가지 방법은 언어 처리계에 ‘이 변수는 정수다’라고 알려서 사람이 아닌 컴퓨터가 기억해두는 것이다.
정수끼리, 부동 소수점끼리의 연산
컴퓨터는 형 정보를 참고해서 무엇을 할 지 정한다. x와 y 둘 다 정수인 경우는 정수 덧셈을 하고, x와 y 둘 다 부동 소수점인 경우에는 부동 소수점 덧셈을 한다.
한쪽이 정수고 다른 한쪽이 부동 소수점이라면?
초기 FORTRAN의 경우 에러 처리를 했다. 하지만 C언어에서는 부동 소수점으로 암묵적 변환을 시킨다.
하지만 C언어의 경우, 이것은 프로그래머가 어떤 형인지 기억해두지 않으면, 특정 코드를 보고도 소수점 이하를 버리는지 아닌지를 알 수가 없는 단점이 있다.
이러한 설계는 많은 언어가 오랜 시간동안 사용해 왔던 것이기 때문에 ‘원래 그런 것이다’라고 생각하는 프로그래머가 많겠지만 이것은 불변의 물리 법칙이 아닌, 사람이 만든 설계일 뿐이다.
형의 다양한 전개
원래는 값의 종류를 저장하기 위해 사용되었던 형은 다양하게 응용되고 있다.
사용자 정의형과 객체 지향
-
언어가 가지고 있는 기본적인 형을 조합해서 새로운 형을 만드는 기능
-
정수 등 ‘데이터’뿐만 아니라 함수 등 ‘데이터’를 처리하는 기능도 형으로 정리
- 이런 형에 클래스(class)라는 이름을 붙였다. 이것이 제2의 객체 지향의 발명이다.
사양으로서 형
공개와 비공개를 나누다
- ‘형은 사양이다’라는 개념이 등장했다. 구조체나 클래스를 구성하는 형을 전부 공개하지 않고 최소한만을 공개한다는 것이다. 외부와 작업하는 부분만을 형으로 공개하고, 상세 구현 방법은 숨긴다는 발상이다.
인터페이스로 발전
- ‘형은 사양이다’라는 개념은 더욱 진화해서 구체적인 구현 방법을 가지고 있지 않는 형이 탄생했다.
총칭형, 제네릭스, 템플릿
‘일부만 바꾸고 싶은데 전부 다시 정의해야 하는 것은 이상하다. 재사용하고 싶다’라는 필요가 생겨났고, ‘구성 요소의 형을 일부만 바꾸는 형’, 즉 총칭형이 탄생했다. ‘형이 인수를 가지고 형을 만드는 함수’가 탄생한 것이다.
ex)
- C++의 템플릿(template), Java(generic)등이 그러한 구조이다.
동적 형결정
‘종류 정보’를 값과 함께 세트로 가지고 있는 것을 ‘동적 형결정’이라고 한다.
어떻게 표현하고 있을까?
동적 형결정 언어에서 형 선언이 필요 없는 것은 메모리 상에서 동일한 형으로 취급되도록 설계되어 있기 때문이다.
- Python에서 값은 정수든 부동 소수점이든 문자열이든 모두 PyObject 형으로 취급되도록 머리 부분이 모두 같은 형태로 되어 있다. 그리고 PyObject 형의 구조 안에는 값의 종류 정보를 저장할 수 있는 공간이 마련되어 있다.
이러한 방법으로 값의 종류를 관리하면 종래의 정적 형결정 언어에서는 할 수 없었던 유연한 처리가 가능해지지만, 동적 형결정 언어에서는 형 체크를 할 수 없기 때문에 일부 버그는 실행 전에 찾아낼 수가 없다.
형 추론
형 추론은 원래 OCaml이나 Haskell 등의 ML계 언어가 잘하는 분야였지만, 최근에는 Java VM 상에서 동작하는 언어인 Scala등 형 추론을 장점으로 부각시키는 언어가 늘고 있다.
- 현재는 정적 형결정과 동적 형결정과 같이 정보 저장 장소나 사용하는 타이밍이 다른 것까지 포함해서 ‘형’이라 부른다. 이 때문에 형이 무엇인지 더욱 이해하기 어렵게 되었다. 어떤 정보가 어디에 있고 어떤 타이밍에 사용되는지의 관점에서 보아야 이해하기 쉽다.
–
컨테이너와 문자열
무언가를 넣는 상자를 컨테이너라고 부른다고 하자.
- 언어마다 다양한 컨테이너가 있다.
- 각각의 컨테이너에는 장단점이있다.
컨테이너에 넣은 데이터는 메모리에 저장된다. 메모리에는 정해진 크기의 상자가 정렬되어 있으며, 각 상자는 번호가 부여되어 있는 물품 보관함 같은 것이다
- 컨테이너 종류에 따라서 메모리 저장 방법이 다르다
메모리 : 정해진 크기의 상자가 정렬되어 있고 각 상자에 번호가 부여되어 있다.
배열과 연결 리스트
두 종류의 컨테이너가 있다.
- 배열
- 연결 리스트
배열에 값을 삽입하는 경우
배열에서는 ‘값을 순서대로 넣는’방법으로 저장한다. 배열에 요소를 삽입 할 때는 삽입된 위치보다 뒤에 있는 요소를 전부 다른 위치로 옮겨야 한다.
연결 리스트에 값을 삽입하는 경우
연결 리스트에서는 메모리에 순서대로 정렬해있을 필요가 없다. ‘다음 요소가 들어있는 위치’를 메모리 상에 넣어두기 때문이다.
ex)
- 연결 리스트에 삽입 할 때는 상자 두개를 추가하고, 상자 하나만 변경하면 된다.
장단점
배열에서는 삭제된 요소보다 뒤에 있는 것을 모두 움직여야 하기에 O(n)이 돼버리지만, 연결 리스트에서는 ‘다음 요소는 어딘지’의 정보를 바꾸기만 하면 되기에 O(1)이다.
배열에서는 순서대로 나열하고 있기 때문에 금방 n번째 요소를 알 수 있지만, 연결 리스트에서는 각 요소를 자신이 원하는 곳에 넣을 수 있기 때문에 이 방법을 사용 할 수 없다.
언어에 따른 차이
Java나 Python, Ruby 등 대부분의 언어에서는 배열이 가장 기본적인 컨테이너로 제공되고 있다.
사전, 해쉬, 연상 배열
‘값을 복수 개 넣는다’는 목적을 위해 배열과 연결 리스트 등 여러 가지 방법이 있었듯이, ‘문자열과 값의 대응을 넣는다’는 목적을 위해서 몇 가지 구현 방법이 있다. 자주 사용되는 것이 해쉬테이블과 트리다.
해쉬 테이블
문자열을 인수로 받아서 정수를 반환하는 ‘해쉬 함수’를 사용해서 문자열과 값의 대응 관계를 표현하는 방법이다.
트리
트리는 데이터의 구조체다. ‘왼쪽 자식’, ‘오른쪽 자식’에 두 개의 화살표가 연결된다. 습관적으로 아래를 향해서 뻗어나가기 때문에 나무라기 보단 나무 뿌리처럼 보일 수도 있다.
요소를 꺼내는 시간
배열에 키와 값을 넣고 어떤 키에 대응하는 값을 찾고자 한다고 하자. 어떤 키가 어디에 있는지 모르기 때문에 배열의 맨 처음부터 순서대로 읽어나간다. 첫 부분에서 찾을 수도 있고, 맨 마지막에서 찾을 수도 있다. 평균적으로 n/2회 체크가 필요하다. 즉, O(n)이 된다.
트리의 경우
트리의 경우 높이가 하나씩 증가하면 요소 수는 약 2배가 된다. 반대로 말하면, 데이터량이 2배가 될 때마다 필요한 비교 횟수가 1회 증가한다. 즉, 트리에서 꺼내는 처리의 오더는 O(log n)이다.
해쉬 테이블의 경우
키에 대응하는 값을 꺼내기 위해서는 ‘키를 해쉬 함수로 변환’, ‘배열에 해당 장소에 있는 값을 읽는’ 작업이 필요하다. 이 작업은 데이터량과 관계가 없다. 즉 O(1)이다.
- 해쉬 테이블의 오더가 가장 작다.
- 해쉬 테이블은 값을 넣기 위해 큰 배열을 사용하고 있기 때문에 메모리 소모량이 매우 크다.
ex)
- 메모리 절약 » 배열
- 계산 시간 » 해쉬 테이블
만능 컨테이너란 없다
절대 정답은 존재하지 않는다. 자신의 상황에 맞게 적합한 균형을 찾는 게 중요하다.
–
문자란?
문자 집합과 문자 부호화 방식
부호화 방법을 공유하고 있지 않은 사람과는 작업을 할 수가 없다.
컴퓨터 이전 시대의 부호화
- 모스부호
- 보 코드
-
모스부호 : 모스부호는 사람이 스위치를 눌러서 송신하고, 사람이 귀로 들어 수신한다는 규칙으로 만들어진 부호화 방법이었다. 이 방법은 1초 동안 송신할 수 있는 양과 수신할 수 있는 양에 한계가 있다.
-
보 코드 : 하나의 문자를 On/Off 5개의 조합(5비트)으로 표현하는 것이다. 즉 SOS는 15비트로 표현 할 수 있었다. 모스부호의 절반으로 효율이 좋다.
컴퓨터 시대의 부호화
- EDSAC 문자 코드
- ASCII와 EBCDIC 시대
-
EDSAC : 텔렉스와 같이 한 문자에 5비트를 사용했고, 숫자, 문자, 시프트를 사용해서 출력했다. 보 코드와 같은 발상이다. 단지 어떤 문자를 어떤 비트열에 할당할지가 달랐다.
-
ASCII : ASCII는 한 문자당 7비트를 사용하는 부호화 방식이다. 7비트로 128 종류의 문자를 표현 할 수 있기 때문에 시프트로 변환할 필요가 없어졌다. ASCII가 부호화하는 문자 세트에는 EDSAC보다 많은 기호와 제어 코드, 그리고 소문자 알파벳도 포함되어 있다.
ASCII와 EBDCIC은 알파벳 대소문자나 기호를 부호화하는 기법
Unicode에 의한 통일
인터넷이 발전하고 다양한 나라에서 만들어낸 데이터가 교환되기 시작하면서 이 문제가 표면 위로 떠올랐다. 여러 나라가 정한 ‘규칙’을 이해하지 않으면 안 되는 상황이 되었다.
전 세계의 문자를 부호화하는 방법을 만들자는 움직임이 시작됐고, 1984년에 국제 표준화 기구(ISO)가 작업을 시작하였다. 이렇게 해서 전 세계 문자를 포함한 문자 집합 Unicode가 탄생했다.
문자열이란?
문자열이란 문자가 정렬해있는 것이다. 하지만 문자에 따라선 문자열을 표현하는 방법은 제 각각이다.
길이 정보를 가지고 있는 Pascal 문자열, 가지고 있지 않은 C 문자열
C언어와 Pascal은 둘 다 ‘하나의 문자를 8비트’로 정의하고 있다. Pascal 문자열은 제일 앞 부분에 문자열 길이를 기록해둔다는 규칙을 채용하고 있다. 그러나 C언어 문자열은 ‘문자열이 시작되는 메모리 상의 위치’만을 가지고 있다. 길이 정보를 가지고 있지 않아서, 해당 위치에서 어디까지가 문자열인지 알 수 없다.
NUL 문자로 문자열의 끝을 표현한다
- NUL 문자는 0에 대응하는 문자로, C언어 코드에서는 \0으로 표현한다.
Python은 Java와 같이 16비트 문자열인 ‘Unicode 문자열’과 Pascal과 같은 8비트 문자열 모두를 지원하는 언어다
-
Python 2.7에서는 Unicode 문자열에 바이트열을 결합할 경우 바이트열이 ASCII로만 이루어진 경우에 한해 성공한다
-
Python 3.0에서는 Unicode 문자열에 바이트열을 결합하면 항상 실패한다. 명시적으로 Unicode 문자열로 변환하고 나서 결합한다.
Ruby 1.9의 도전
Ruby는 독자 노선을 가고 있었다. Ruby 1.9부터 문자열은 8비트로 하고 ‘부호화 방식 정보’를 추가로 보유하도록 했다. 이 방법은 Unicode 문자 집합에 포함되지 않는 문자를 손쉽게 쓸 수 있다는 점이 장점이다.
병행처리
병행 처리란?
복수의 처리를 시간축 상에 오버랩에서 실행하는 것을 병행 처리라고 한다.
잘게 분할해서 실행한다
사람의 눈으로 보면 프로그램이 계속 동작하고 있는 것처럼 보이지만, 실제로는 잘게 분할해서 실행되고 있는 것이다.
처리를 변경하는 2가지 방법
‘언제 교대할 것인가?’를 정하는 방법은 크게 두가지로 나눌 수 있다.
- 협력적 멀티태스크
- 선점적 멀티태스크
-
협력적 멀티태스크 : ‘타이밍이 좋은 시점에서 교대하는’방법이다. 처리가 일단락되는 시점에 자발적으로 처리 교대를 하는 방법이다. 이 방법으로 구현된 멀티태스크(multi-task, 병행 처리)를 협력적 멀티태스크라고 한다.
-
선점적 멀티태스크 ( 일정 시간에 교대한다 ) : 개별 프로그램과 입장이 다른 프로그램(태스크 스케줄러)이 존재한다. 이 프로그램이 일정 시간마다 지금 실행되고 있는 처리를 강제적으로 중단시켜서 다른 프로그램이 실행될 수 있도록 한다.
경합 상태 방지법
경합 상태의 3가지 조건
평행해서 동작하고 있는 2가지 처리 간에 경합 상태가 발생하기 위해서는 다음 3가지 조건을 모두 만족해야 한다.
- 2가지 처리가 변수를 공유하고 있다
- 적어도 하나의 처리가 그 변수를 변경한다
- 한쪽 처리가 한 단락 마무리 되기 전에, 다른 한쪽의 처리가 끼어들 가능성이 있다.
이 3가지 조건 중 하나라도 제거 할 수 있다면 병행 실행 시에도 안정된 프로그램을 만들 수 있다.
공유하지 않는다 ( 프로세스와 액터 모델 )
- 처음부터 아무것도 공유하지 않으면 1.은 발생하지 않기 때문에 경합 상태를 신경 쓸 필요가 없다.
프로세스에서는 메모리를 공유하지 않는다
UNIX에서는 실행 중의 프로그램을 ‘프로세스’라고 부른다. 서로 다른 프로세스는 메모리를 공유하지 않는다. 때문에 복수의 프로그램이 메모리 상에서 경합 상태를 일으킬 일은 없다. 데이터베이스 접속, 파일 읽고 쓰기 등 무엇인가를 공유했을때만 주의하면 된다.
공유하지 않는 접근법은 성공했을까?
현재까지도 스레드를 사용해서 공유 메모리를 어떻게 다뤄야 할지 고심해가면서 프로그램이 만들어지고 있다.
- 액터 모델 : ‘메모리를 공유하지 않는다’는 설계 방침에서의 또 다른 흐름이 바로 액터 모델이다. 1973년에 발표된 병행 처리를 위한 모델이다. ( 병행해서 동작하는 복수의 처리가 정보를 교환하는 방법으로, ‘메모리를 공유한다’가 아닌 ‘메시지를 보낸다’를 제안했다 )
변경하지 않는다 - const, val, immutable
‘메모리를 공유해도 변경하지 않으면 문제가 없다’는 2.에 대한 대응책도 있다.
이 방식을 강하게 어필하고 있는 언어로써, 모든 값이 변경 불가능한 Haskell 등을 들 수 있다.
끼어들지 않는다
경합 상태가 발생하는 조건 3.인 ‘한 쪽의 처리가 한 단락 마무리 되기 전에 다른 한 쪽의 처리가 끼어들 가능성이 있다’를 방지하기 위해서는 어떻게 하면 좋을까?
협력적 스레드의 사용 - 파이버, 코루틴, 그린 스레드
파이버나 코루틴, 그린 스레드 등으로 불리는 기법을 사용하는것이 그 방법이다!
-
스레드가 선점성을 가지고 끼어드는 원인이 되기 때문에 협력적 스레드를 만들면 된다는 생각이다.
-
협력적 멀티태스크이기 때문에 어떤 스레드가 CPU를 독점하면 다른 스레드의 처리가 멈춘다. 어디까지나 각 스레드가 협력적으로 최적의 순간을 맞춘다는 사실을 전제로 한다.
끼어들면 곤란해지는 처리에 표식을 붙인다 - 락, 뮤텍스, 세마포어
‘지금 끼어들면 곤란해’라는 표식을 공유하는 방법이다. 예를 들어, 어떤 메모리 값이 0이 아니면 이것은 ‘다른 스레드가 끼어들면 곤란한 처리를 하고 있어’라고 정해 두는 것이다.
락의 문제점과 해결책
락의 문제점
- 교착 상태가 발생한다
- 합성할 수 없다
트랜잭션 메모리
이 문제를 해결하려고 하는 것이 트랜잭션 메모리라는 접근법이다. 데이터베이스의 트랜잭션 기법을 메모리에 적용한 것이다. 개념은 ‘실험적으로 해보고, 실패하면 처음부터 다시 고쳐서 하고, 성공하면 변경을 공유한다’이다.
트랜잭션 메모리의 역사
-
하드웨어로 구현
- 1986년에 Symbolics라는 회사가 트랜잭션을 하드웨어로 구현하려는 아이디어를 제안했다. 상업적으로는 성공하지 못했다.
-
소프트웨어로 구현
- 그 후 10년이 지난 1995년에 소프트웨어로 트랜잭션 메모리를 구현하려는 논문이 발표되었다. 그리고 10년 더 지난 후 2005년에 Microsoft가 Concurrent Haskell을 사용해서 소프트웨어적으로 트랜잭션 메모리를 실현하는 논문을 발표했다.
그 전후로 여러 언어에 소프트웨어 트랜잭션 메모리 기능이 탑재되었다.
객체와 클래스
객체 지향이란?
-
C++ 설계자인 Bjarne Stroustrup의 경우
-
객체 지향 프로그래밍이란 사용자 정의형과 상속을 사용한 프로그래밍이다!
-
또한 상태를 가진 객체가 메세지를 주고 받아서 커뮤니케이션하는 프로그램이다!
-
객체는 현실세계의 모형
현실 세계에 있는 ‘사물’의 ‘모형’을 컴퓨터 안에 만들려면 어떻게 하면 될까? 라는 의문을 해결하기 위해 ‘객체 지향’이라는 개념이 탄생했다.
클래스란?
C++에서는 ‘클래스는 사용자가 정의할 수 있는 형’이다.
C++은 정적 형결정 언어이고 Ruby나 Python은 동적 형결정 언어다.
단, Java는 예외다. ‘클래스라는 부품을 정의하고, 그것을 조립해나가는 것이 프로그래밍이다’라고 정의하고 있다. 따라서 Java에서는 클래스가 반드시 필요한 조건이다.
변수와 함수를 합쳐서 모형을 만드는 법
- 모듈(Module)
- 함수도 변수도 동일하게 해쉬에 넣는 방법
- 클로저(Closure)
-
모듈 : 함수를 합쳐두기 위한 패키지와 변수를 합쳐두기 위한 해쉬를 연결하는 방식
-
함수도 변수도 동일하게 해쉬에 넣는 방법 : JavaScript등의 언어가 채용하고 있다.
-
클로저 : 함수 실행 시의 이름 공간의 변수를 하나로 묶기 위해 사용하는 방법이다.
모듈, 패키지
모듈, 패키지란 무엇인가?
‘관련성이 높은 함수나 변수의 묶음’이다.
모듈은 ‘하나로 모으는 기법’이다.
Python이나 Ruby는 ‘모듈’이라 하고, Java나 Perl에서는 ‘패키지’라는 이름으로 부른다
Perl 패키지로 객체를 만든다
Perl 패키지는 함수나 변수를 하나로 묶어서 그것에 이름을 붙일 수 있는 기능이다.
하지만
모듈만으로는 부족하다
인수로 개별 해쉬를 전달한다
Perl에는 사전을 만드는 기능이 언어 처리계 자체에 탑재되어 있다. Perl은 이것은 ‘해쉬’라고 부른다.
- 해쉬는 함수를 호출 할 때 해쉬를 인수로 전달한다.
초기화 처리도 패키지에 넣는다
새로운 카운터를 만들 때는 프로그래머가 직접 명시를 해줘야만 한다. 값을 어떻게 초기화할지를사람이 기억하지 않으면 안되는 것이다. 이것은 좋지 않은 설계다. 이런 경우는 ‘초기화 방법’을 함수로 만들어서 패키지에 넣으면 된다.
해쉬와 패키지를 연결한다
데이터 저장소와 해당 데이터에 대응하는 동작의 집합(모듈)을 연결시킴으로 하나의 묶음으로 정리된 이해하기 쉬운 코드를 만들 수 있다.
함수도 해쉬에 넣을 수 있다!
퍼스트 클래스
JavaScript가 도입하고 있는 방법이다.
-
‘변수에 대입한다’, ‘함수의 인수로 전달한다’, ‘함수의 반환값으로 전달한다’ 등이 가능한 값을 ‘퍼스트 클래스값’이라고 부른다.
-
차별 대상이 아닌 일급 시민을 의미한 표현이다. 최근의 언어 Java나 Perl, Python 등에서 문자열은 퍼스트 클래스 값이다.
JavaScript에서는 함수도 퍼스트 클래스 값이다. 함수를 변수에 대입하거나 함수의 반환값으로 전달하는 것도 가능하다
복수 개 카운터를 만든다
카운터를 손쉽게 복수 개 만들기 위해서 해쉬로 초기화하기 위한 함수를 만들면 아주 간단하게 만들 수 있다. 단순히 해쉬를 makeCounter 안으로 이동하기만 하면 된다.
Perl 패키지와 동일하게 다음 이점들을 가지게 된다.
-
복수 객체를 만들 수 있다.
-
하나의 묶음으로 보인다.
-
초기화 방법을 사람이 기억하지 않아도 된다.
클로저
클로저란?
이것은 객체적인 것을 만들기 위한 기술이다.
- 클로저라는 특수한 구문이 있는 것은 아니다. 함수를 함수 안에 정의하고, 내포할 수 있는 정적 스코프가 있어서 함수를 반환값으로 사용하거나 변수에 대입하여 사용한다는 개념이다. 즉, 간단한 내포 구조를 사용함으로 상태 정보를 가진 함수를 만들 수 있다.
왜 클로저라고 부를까?
왜 이것을 클로저라고 부를까? 그것은 자유 변수를 포함한 식을 ‘열린 식’이라고 부르고, 그 자유 변수의 바인딩을 조합함으로 해당 식을 닫고 있기 때문이다.
클래스
언어 설계자가 ‘이 언어에서는 이것을 클래스라고 부른다’고 정한 것으로, 다양한 정의가 존재한다.
Hoare가 생각한 클래스
‘현실 세계의 사물(object)은 편의상 상호 배타적 종류(classes)로 분류될 수 있다’고 기술하고 있다. 그리고 ‘어떤 종류의 사물을 더욱 세분화된 종류(subclasses)로 분류할 수 있으면 편리하다’고 주장하고 있다.
-
처음 시작은 ‘분류’였던 것이다
- 현재 사용되고 있는 ‘클래스를 상속한다’, ‘클래스는 인스턴스다’라는 개념이 등장한것은 보다 나중의 이야기다.
C++ 클래스
클래스는 타입(type)이다. 이것이 C++을 이루는 아주 중요한 원리다. C++에서 class가 사용자 정의 타입을 의미한다면 왜 그것을 type이라고 부르지 않는가? 내가 class를 선택한 것은 계속 나오는 새로운 용어를 발명하는 것이 귀찮았기 때문이고, Simula의 class라면 아무도 당황하지 않을 것이라 판단했기 때문이다. - « C++로 배우는 프로그래밍의 원리와 설계 »
사양으로서 역할
C++에게 있어서 클래스(=형)란 사양을 표명한 것이기도 했다. 즉, 클래스는 ‘객체가 어떤 메소드를 갖고 있고, 어떤 메소드를 갖고 있지 않은가’라는 사양을 선언하는 역할도 했다.
- 그 메세지를 받은 객체가 어떤 동작(무언가를 실행할지, 에러처리할지 아니면 무시할 것인지)을 할지는 수신 객체가 자유롭게 결정 할 수 있다.
클래스의 3가지 역할
-
결합체를 만드는 생성기
-
어떤 조작이 가능한지에 대한 사양
-
코드를 재사용하는 단위
객체 지향은 현실세계의 사물 모형을 만들기 위해 만들어졌고, 언어마다 구현 방법이나 ‘객체 지향’이 의미하느 바가 다르다!
상속을 통한 재사용
상속이란?
어떤 클래스에서 선언된 것은 그것을 세분화한 ‘자식 클래스’에게도 자동으로 이어지는 게 좋다. 이것이 ‘상속’이다!!!
상속에 관한 다양한 접근법
상속은 크게 3가지 측면으로 접근할 수 있다.
-
일반화/특수화
-
공통 부분을 추출
-
차분 구현
-
일반화/특수화 : ‘부모 클래스로 일반적인 기능을 구현하고, 자식 클래스로 목적에 특화된 기능을 구현한다’는 접근이다.
-
공통 부분을 추출 : ‘복수 클래스의 공통 부분을 부모 클래스로서 추출하면 좋다’는 접근법이다
-
차분 구현 : ‘상속 후 변경된 부분만 구현하면 효율이 좋다’는 접근법이다
상속은 양날의 칼
‘상속을 많이 사용하면 코드가 복잡해진다. 제어를 추가해야 한다’는 의견이 나오고 있다.
- 상속을 반복하면, 코드 영향 범위가 넓어져서 이해하기 어렵게 된다. 이해하기 쉽게 하기 위해서는 상속 트리의 깊이를 낮추는 것이 중요하다
리스코프의 치환 원칙
CLU라는 언어의 설계자인 Liskov가 1987년에 제창한 것으로, 현재는 자식 클래스를 만들 때 주의해야 할 사항으로 자주 언급되는 것이 ‘리스코프의 치환 원칙’이다.
다중 상속
하나의 사물을 복수로 분류
현실세계에서 하나의 사물이 복수의 분류에 해당하는 경우가 있기 때문에 그것을 모델화하는 프로그래밍 언어가 복수의 클래스를 상속할 수 있어야 한다. 이것이 다중 상속이다.
코드 재사용에 편리한 다중 상속
다중 상속은 코드 재사용 방법으로도 매우 좋은 도구다!
- 다중 상속이 가능한 언어에서는 단순히 양쪽 클래스를 상속하기만 하면 된다.
다중 상속의 문제점 - 거듭되는 충돌
다중 상속은 편리한 기능이다. 그런데 다중 상속은 어떻게 메소드의 이름을 해결하고 있는 걸까? 클래스에게 ‘x의 값은 무엇인가?’라고 물으면 어떻게 대답할까? 이름해결의 문제가 발생한다.
해결책 1: 다중 상속을 금지한다
Java는 클래스 다중 상속을 금지하기로 했다. 클래스 다중 상속을 인정하지 않으면 앞의 경우와 같은 문제가 발생하지 않는다. 다중 상속을 버림으로 문제를 해결했지만, 대신 다중 상속의 편리함 또한 버리게 된다.
-
위임 : 사용하고 싶은 코드를 가지고 있는 클래스 객체를 만들고, 필요에 따라 해당 클래스에 처리를 맡기는 방법이다. 상속을 사용해서 형이나 이름 공간까지 함께 계승하는 것이 문제의 원인이기 때문에, 단순히 객체를 보유하기만 하면 문제를 막을 수 있다.
-
인터페이스 : Java는 다중 상속을 금지하고 있다고 설명했지만, Java에도 다중 상속이 가능한 것이 있다. 바로 인터페이스 이다. 인터페이스는 ‘코드를 가지고 있지 않는 클래스’다. ‘인터페이스를 상속한 클래스는 반드시 xx라는 이름의 메소드를 가지고 있다’라는 ‘사양’만 가지고 있다.
해결책 2: 메소드 해결 순서를 고민한다
‘자신이 모른다면 앞에 쓰여있는 부모부터 순서대로 확인한다’는 규칙(깊이 우선 탐색)으로 해결될까?
-
깊이 우선 탐색의 문제점 : 어떤 클래스 Base를 복수의 클래스 derived1, derived2가 상속하고 있다고 하자. 그리고 이 두 클래스를 상속하는 Multi가 있다고 하자. 이 같은 형태의 다중 상속을 ‘마름모 상속’이라고 부른다. 메소드가 재정의(오버라이드)되는 경우가 있다.
-
C3 선형화로 순서를 정한다
-
C3 선형화는 1996년에 제안된 알고리즘으로, 2가지 제약 조건을 만족하도록 클래스에 순서를 매겨서 정렬한다. 그 제약 조건은 다음과 같다.
- 부모 클래스는 자식 클래스보다 먼저 탐색되지 않는다.
- 어떤 클래스가 복수의 부모 클래스를 상속하고 있으면 먼저 만들어진 것이 우선된다.
-
해결책 3: 처리를 섞는다
어떤 클래스는 선조 클래스까지 도달하는 경로가 여러 개 있는 것이 문제다. 그렇다면 재사용하고 싶은 기능만을 모은 작은 클래스를 만들어서 해당 기능을 추가하고 싶은 클래스에 섞어 넣으면 된다. 이런 설계 방침이나 섞어 넣는 것, 그리고 섞기 위한 작은 클래스를 믹스-인(Mix-in)이라고 한다.
해결책 4: 트레이트
클래스가 ‘인스턴스를 만들기 위한 것’으로 사용될 때는 재사용 단위로 너무 크다. 그러면 재사용 단위라는 역할에 특화된 보다 작은 구조(트레이트 = 메소드 묶음)를 만드는 것이 좋다. 이것이 트레이트 개념이다.
Subscribe via RSS