일정 예상은 왜 실패할까?

모든 일에서 데드라인을 지키는 것은 중요하다. 마감을 지키는 것은 일의 기본이다. 특히 팀으로 작업하는 일에서는 더더욱 그렇다. 소프트웨어 개발의 많은 일은 파이프라인 형식으로 진행되기 때문에, 내가 약속한 일정을 못 맞추면, 내 일만 뒤로 밀리는 것이 아니라 협업 중인 다른 사람의 일정에까지 영향을 주는 경우가 많다. 하지만 일정을 맞추는 것은 어렵다. 그게 뭐가 어렵냐고 생각할 수 있는데 정말 어렵다. 언제나 이번만큼은 예외라고 생각하지만, 언제나 일정은 틀어진다. 심지어 예상보다 오래 걸릴 것이라는 경험을 바탕으로 계산보다 일정을 길게 잡아도 여전히 마감을 못 지킨다. 이런 현상을 호프스태터의 법칙 이라고 부른다. 그렇다면 호프스테터의 법칙은 왜 생길까? 일정 예측이 틀리는 이유를 알아보기 전에 우선 짚고 넘어갈 것이 있다. 어떤 프로젝트는 일정을 맞출 수 없다. 애초에 부족한 시간이 주어진 경우가 그렇다. 아쉽게도 많은 프로젝트에서 일정은 기술적 요인만으로 결정되지 않고, 다른 사업적 이유로 결정된다. 이런 경우는 답이 없다. 하지만 이런 것은 "프로젝트가 * 같았다"라고 말하지 예측이 실패했다고는 하지 않는다. 이 글에서 말하는 일정 예상이 틀렸다고 말하는 것은 기술적 요인을 충분히 고려하여 일정을 잡았어도 예상한 경우보다 많이 걸리는 경우를 말한다. 애초에 이루어질 수 없는 스케줄을 주고 일정 예측에 실패했다고 말하는 것은 그냥 양심 없는 짓이다. 일정이 틀린 이유를 물어보면 다양한 답이 나온다. 과거에 작업했던 코드에서 버그가 있어 수정하느라 늦었다고 하는 경우도 있고, 스펙이 완전치 않아 스펙을 보완하느라 늦어졌다고 하기도 한다. 레거시 코드를 다루는 경우 기존 코드를 완전히 이해하지 못한 채로 시작을 해 늦어졌다고 하기도 한다. 어떤 경우는 구현해야 할 기능에 대한 지식이 부족하여 일정을 잘못 예상했다고 대답하는 경우도 있다. 이 모든 것들은 일정 예측이 틀리는 타당한 이유이긴 하지만 근본적인 이유는 아니다. 만

[TypeScript] Promise.all에는 인자 개수 제한이 없다

트위터에서 이상한 글을 봤다. TypeScript 가 선언한 Promise.all 이 iterable 을 받지 않고 인자 개수 제한이 있다는 것이다. 근데 그럴 리가. TypeScript쯤 되는 프로젝트에서 무언가가 스펙과 다른 것으로 보인다면, 코드를 잘못 이해했을 확률이 높다. 코드를 다시 확인해보자. 당연히 iterable 을 인자로 받는 Promise.all 선언 도 있다. 다만, 이 선언이 es2015.promise.d.ts 가 아닌 es2015.iterable.d.ts 에 있을 뿐이다. 타입스크립트 컴파일러 옵션 중 --lib 으로 사용할 라이브러리를 지정할 수 있다. 대부분 es2015 , es2018 같은 식으로 버전을 지정하여 포함하는 것이 일반적이라 잘 알려지지 않았지만, es2015.symbol 로 Symbol 에 대한 선언만 포함하거나, es2015.promise 로 Promise 에 대한 선언만 포함하는 것이 가능하다. 각 라이브러리는 서로 간에 의존성이 없도록 작성돼 있으며, 특히 iterable 에 관련된 선언은 전부 es2015.iterable 에서 선언한다. 예를 들어 Map 이나 Set 은 es2015.collection 에 선언돼 있지만, iterable 을 반환하는 entries , values , keys 등의 선언은 전부 es2015.iterable 에 선언돼 있다. 마찬가지로 iterable 을 인자로 받는 Promise.all 함수도 es2015.iterable에 선언돼있다. iterable 을 받는 Promise.all 함수 선언은 es2015.iterable 에 선언돼 있는데, es2015.promise 에 10개가 더 선언돼있다. 이들은 무엇일까? 사실 이 함수들은 배열을 인자로 받는 함수가 아니라 tuple 을 인자로 받는 함수 선언이다. JavaScript 에는 임의 개수의 원소를 가지는 배열은 있지만, 고정된 개수의 원소를 가지는 tuple은 없다. 그래서 tuple이 필요할 때 배열을 사

[Rust] 타입 변환하기

Rust에서 타입 변환은 특별한 것이 아니다. 그저 단순히 하나의 값을 소유권을 받아 다른 타입의 값을 반환하는 함수다. 따라서 자신이 원하는 이름으로 아무 함수나 만들면 된다. 하지만 가독성을 위해 as_ , to_ , into_ 를 prefix로 사용 하는 method를 만들거나, from_ 가 prefix로 붙는 생성자를 만들어 converting constructor처럼 만들어 사용한다. From 타입 변환을 보다 일반적으로 구현하고 싶으면 From 트레잇을 구현하면 된다. 예를 들어 A 라는 타입을 B 라는 타입으로 변환하고 싶을 때는, B 타입에 From<A> 를 구현하는 것이다. From 트레잇은 from 이라는 associated function 을 제공하기 때문에 A 타입의 변수 a 를 B 타입으로 변환시킬 경우 From::from(a) 이라는 식으로 사용한다. 만약, 컴파일러가 타입 추론을 못 해주는 경우 변환할 타입을 명시적으로 적어 B::from(a) 이라는 식으로 사용하면 된다. Into From 트레잇은 변환될 타입의 associated function을 제공한다. 덕분에 변환될 타입을 명시할 수 있다. 이는 From 트레잇의 장점이지만, 구체적인 타입을 많이 적을수록 코드의 범용성을 떨어뜨린다. 범용성을 떨어뜨리지 않기 위해 From::from(a) 와 같은 식으로 사용할 수도 있지만, from 은 associated function이기 때문에 a.from() 같은 식으로 호출할 수는 없다. 이 경우 Into 트레잇이 유용하게 사용된다. From 트레잇이 변환될 타입의 associated function을 제공하는 반면, Into 는 into 라는 method를 제공한다. 따라서 A 타입에서 B 로 가는 Into 트레잇을 구현하고 있으면, A 타입인 변수 a 가 있을 때, a.into() 같은 식으로 변환 함수를 호출할 수 있다. 물론, A::into(a) 나 Into::into(a)

Garbage collection과 memory leak

Garbage collection (a.k.a. GC)은 프로그램이 더 이상 접근할 수 없는 메모리를 자동으로 해제시켜 주는 기술을 의미한다. John McCarthy 가 Lisp에 처음 구현한 이후 많은 언어가 사용하여 현대 프로그래머 중에 모르는 사람이 없다고 해도 좋을 정도로 널리 알려진 개념이다. 근데 이 GC에 대해서 사람들이 자주 착각하는 것이 있다. GC를 사용하는 이유가 memory leak 을 잡아주기 위해서라고 생각하는 것이다. 만약 이렇게 생각했다면 GC에 대해 큰 착각을 하는 것이다. GC는 memory leak을 막지 못한다. 사실 튜링 완전 한 언어에서 memory leak을 막아주는 방법은 세상에 존재할 수 없다. 이것을 설명하기 위해서는 우선 memory leak이 무엇인지 알아야 한다. Wikipedia 는 memory leak을 다음과 같이 설명한다. a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released. 쉽게 설명해 memory leak이란, 사용하지 않을 메모리를 해제하지 않는 현상이다. 결국 memory leak을 잡기 위해서는 사용하지 않는 메모리를 찾아내는 것이 먼저다. 그리고 사용하지 않는 메모리를 완벽하게 찾아내는 건 불가능하다. 자세한 설명은 아래 코드를 통해서 하도록 하겠다. 위 코드에서 x 는 언제 해제해야 할까? 보통은 use(x) 가 끝난 뒤 해제해야 한다고 생각할 것이다. 하지만 some_function 이 종료되지 않는 함수라면 어떨까? some_function 이 내부에서 무한 루프를 돌고 절대 종료되지 않는 함수라면 use(x) 는 호출될 일이 없다. 따라서 x 는 앞으로 접근할 일이 없는 메모리고, some_function 이 실행되고 있는

managed language와 unmanaged language?

얼마 전 우연히 이런 글 을 보게 됐다. 프로그래밍 언어를 managed language와 unmanaged language로 구분한 것인데 그 기준을 garbage collection (a.k.a. GC)을 하는가 아닌가로 잡았다. 난생처음 들어보는 기준이었다. 인생이 힘들어서 노느라 바쁜 사이 뭔가 새로운 논문이 나왔나 하고 찾아봤다. 역시나 이런 경우 대부분 그렇듯이 다른 나라에서는 안 쓰이고 다른 나라에서는 안 쓰이고 우리나라에서 작성된 블로그만 보였다. 그 블로그들이 공통으로 언급하는 것으로 보아 어떤 사람이 유튜브에서 처음 사용한 것 같다. 사실 다른 나라에서는 안 쓰이는 기준이라는 건 별로 중요하지 않다. 그보다 중요한 건 이 managed language라는 것이 잘못 붙여진 이름이라는 것이다. 일단 managed/unmanaged라는 용어 자체가 없는 것은 아니다. 이건 MS 진영에서 만든 용어다. MS는 managed language 대신 managed code 라는 단어를 주로 쓰기는 했지만 말이다. MS에서 만든 managed code는 Common Language Runtime (a.k.a. CLR)에서 실행되는 코드를 의미한다. 반대로 CLR에 의존하지 않는 코드를 unmanaged code라고 부른다. 물론 CLR이 제공하는 기능에 GC가 포함되기 때문에 managed code는 GC을 사용한다. 하지만 GC에 초점을 맞추면 안 된다. 여기서 중요한 것은 virtual machine 위에서 코드가 실행된다는 것이다. virtual machine 위에서 돌기 때문에 컴퓨터 아키텍쳐 에 따라 새로 컴파일할 필요 없다거나, 다른 머신에서도 같은 동작을 기대할 수 있다는 등 CLR의 장점에 주목해야 한다. 한때는 managed code를 생성할 것을 목표로 설계된 C# , Visual Basic 같은 언어를 managed language라고 부르기도 했지만, 요새는 잘 안 보인다. 안 쓰이게 된 이유는 모르겠다. 그저 C

read-writers lock - 공유 자원 접근하기

Multithreaded programming에서 공유 자원에 접근할 때는 동시에 두 개 이상의 스레드가 자원을 변경시키지 않기 위해서 mutex 를 사용한다. Mutex를 사용하면 공유자원에 접근하는 스레드를 한 개로 제한하기 때문에 안전하지만, 어떤 경우는 비효율적이다. 예를 들어 여러 스레드가 공유 자원에 동시에 접근해야 하지만 그중 일부 스레드만 값을 변경하는 경우는 어떨까? 이런 경우 값을 읽기만 하는 스레드는 동시에 접근해도 상관없다. 하지만 어떤 스레드가 값을 변경하고 있으면, 다른 스레드는 공유 자원에 접근해서는 안 된다. 반대로 다른 스레드가 공유 자원에 접근하고 있는 중에는 값을 변경하는 스레드는 접근해서는 안 된다. 이때 사용하는 것이 shared-exclusive lock이라고도 하는 readers-writer lock 이다. Readers-writer lock은 여러 개의 reader와 한 개의 writer를 허용한다. 그래서 multiple-readers/single-writer lock(MRSW lock)이라고도 불린다. 즉, 이미 read lock이 잡혀있는 readers-writer lock에 read lock을 잡으면 바로 lock이 잡히고 다음 코드를 실행할 수 있지만, write lock을 잡으면, lock을 잡지 못하고 read lock이 풀릴 때까지 기다린다. 그렇다면 A thread 가 read lock을 잡고, B thread 가 write lock을 잡은 뒤 C thread 가 read lock을 잡으면 어떻게 될까? 단순하게 생각해보면 A thread 가 read lock을 잡고 있으니 B thread 의 write lock은 잡히지 못하고 기다리고, C thread 의 read lock은 잡힐 수 있기 때문에, A thread 와 C thread 의 코드가 실행될 것이다. 하지만 이런 구현의 경우 read lock이 빈번하게 잡히는 코드라면, write lock이 영원히 실행되지 못할 수도 있다. 이

Rust 읽을거리

아무 글도 안 쓴지 벌써 삼 개월이 지났다. 아무래도 너무 오래 논거 같아서 뭐라도 써야 할 것 같다. 사실 쓰고 싶은 주제는 많다. 하지만 대부분 주제가 쓰는 데 시간이 걸릴 주제라서 귀찮고, 이번에는 간단하게 Rust 를 배우는데 읽기 좋은 자료들을 정리해보도록 하겠다. Rust는 배우기 어려운 언어로 소문나 있지만, 그런 만큼 Rust 개발 커뮤니티에서 공식적으로 제공하는 문서가 많이 있다. 그중에서 입문자가 읽기 가장 좋은 문서는 RBE라고 불리는 Rust by Example 다. RBE는 자세한 설명보다는 실제 코드를 어떻게 짜야 하는지를 보여준다. RBE를 읽었으면 Rust를 배우기 위해서 Rust Book이라고 불리는 Rust Programming Language 를 반드시 읽어야 한다. Rust Book은 Second edition 이후로 Live edition으로 관리되고 있는데, 기초적인 내용부터 Rust를 사용하는 데 꼭 필요한 내용을 담고 있다. 사실 일반적으로는 RBE보다 Rust Book을 먼저 읽는 것을 추천하는데 나는 Rust Book과 RBE 중 어느 쪽을 먼저 읽을지는 취향의 문제라고 생각한다. 그다음은 Rust Standard Library 다. Rust 표준 라이브러리를 설명한 문서인데, Rust를 no_std 옵션으로 사용할 것이 아니라면, 반드시 읽어야 하는 문서다. 하지만 라이브러리 매뉴얼이라는 특성상 처음부터 순서대로 읽을 일은 거의 없고 그때그때 필요한 것이 있으면 찾아보는 경우가 대부분이다. 하지만 한 번 처음부터 제대로 읽어보는 것을 추천한다. 특히 표준 라이브러리의 소스로 가는 링크를 제공하는데, 표준 라이브러리는 사실상 Rust를 가장 잘 쓰는 사람들이 짠 코드이기 때문에, 소스코드를 읽으며 배우는 점도 많다. Rust idiom을 배우기 위해서라도 한 번 읽어보는 것이 좋다. 여기까지 읽었으면 Rust에 대한 기본적인 지식은 생겼을 것이다. 여기까지만 해도 Rust로 프로그래밍하는데 별 문제

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API