티스토리 뷰

나의 공부방

[개발서적] 스트리트 코더(Street Coder) 핵심 내용 정리 및 요약

망나니개발자 2024. 8. 27. 10:00
반응형

 

 

1. 당신의 시장을 선택하라


[ 1.1 길거리에서 중요한 것 ]

한 가지 분명한 건 본인이 일을 얼마나 빨리 하느냐가 가장 중요하다는 점이다. 아무도 우아한 설계, 알고리즘 지식, 고품질의 코드를 신경 쓰지 않는다. 여러분을 고용한 사람들은 주어진 시간에 얼마나 많은 것을 해줄 수 있느냐에만 관심이 있다. 하지만 여러분의 직관과는 반대로 좋은 설계, 좋은 알고리즘, 좋은 품질의 코드는 처리량에 상당한 영향을 주며, 많은 프로그래머가 이를 놓치고 있다.

만약 여러분이 나쁜 코드를 쓰고 있다면 여러분은 동료들의 속도를 늦추고 있는 것이다.

 

 

[ 1.3 훌륭한 스트리트 코더 ]

스트리트 코더는 업계의 인정, 명예, 충성심 외에도 이상적으로 다음과 같은 자질을 가지고 있다.

  • 질문하기
  • 결과 중심적
  • 높은 처리량
  • 복잡성과 모호성 수용

 

1.3.2 결과 중심적

결과를 얻는 것은 코드 품질이나 우아함, 기술적 우수성의 희생을 의미할 수도 있다. 중요한 것은 이런 관점을 가지는 동시에 자신이 무엇을 하고 있으며 이는 누구를 위한 것인지를 계속해서 점검하는 것이다.

 

 

1.3.4 복잡성과 모호성 수용

복잡성은 무섭고 모호성은 더 무섭다. 그 이유는 얼마나 두려워해야 하는지조차 모르기 때문이다.

모호함을 다루는 것은 마이크로소프트 채용 담당자가 면접에서 질문하는 핵심 기술 중 하나이다. 그것은 보통 “서울에 바이올린 수리점이 몇 개 있을까요?”, “로스앤젤레스에는 주유소가 몇 개 있을까요?” 또는 “경호 요원 몇 명이 대통령을 경호하며, 교대 근무 일정은 어떻게 될까요? 경호 요원의 이름을 나열하고, 가급적이면 청와대의 청사진에서 이들의 동선을 보여주세요”와 같은 가상의 질문으로 나타난다.

이러한 문제를 해결하는 요령은 문제에 대해 알고 있는 것을 모두 명확히 수치화하고, 이러한 사실을 바탕으로 근사치에 도달하는 것이다.

 

 

 

[ 1.4 최근 소프트웨어 개발의 문제점 ]

1.4.1 너무 많은 기술

대부분의 기술은 생산성 향상에 따른 트레이드오프를 가진다. 생산성을 높이는 것은 어떤 기술을 사용하는지보다 해당 기술을 얼마나 능숙하게 다루느냐에 달려 있다.

 

 

1.4.3 기술의 블랙박스

프레임워크나 라이브러리는 일종의 패키지다. 소프트웨거 개발자는 이것을 설치하고, 설명서를 읽고 사용한다. 하지만 대부분 패키지가 어떻게 동작하는지는 모른다.

라이브러리, 프레임워크, 또는 컴퓨터가 어떻게 동작하는지에 대한 세부 사항은 그 위에 무엇이 구축되었는지 이해하는 데 엄청난 영향을 미칠 수 있다.

 

 

1.4.5 내 일이 아니다

특정 기술이 어떻게 동작하고, 라이브러리가 무슨 일을 하는지, 의존성이 어떻게 작동하고 연결되는지 배우는 것은 우리가 코드를 작성할 때 더 나은 결정을 내릴 수 있도록 해준다.

 

 

1.4.6 시시해 보이는 일도 도움이 될 수 있다.

복사-붙여넣기가 항상 나쁜 것은 아닌 것처럼 시시한 일이라고 모두 나쁜 것만은 아니다. 별로라는 선입견이 있지만, 여러분이 배운 모번 사례보다 더 효율적으로 만들 수 있는 방법이 있다.

 

 

 

 

2. 실용적인 이론


[ 2.2 내부 데이터 구조 ]

호출 스택(call stack)은 함수의 반환 주소를 저장해 호출된 함수의 실행이 완료되면 반환할 위치를 알려주는 데이터 구조이다. 쓰레드당 호출 스택 하나를 가진다.

모든 스레드에는 고정된 메모리로서 자체 호출 스택이 있다. 전통적으로 스택은 프로세스 메모리 공간에서 위에서 아래로 늘어나며, 맨 위는 메모리 공간의 끝을 의미하고 맨 아래는 누구나 아는 널 포인터(null pointer), 주소 0을 의미한다. 항목을 호출 스택에 푸시하는 것은 항목을 거기에 놓고 스택 포인터를 줄이는 것을 의미한다.

호출 스택은 반환 주소뿐만 아니라 함수의 매개변수와 로컬변수도 포함한다. 로컬 변수는 메모리를 거의 차지하지 않으므로 할당 및 할당 해제와 같은 추가적인 메모리 관리 단계가 필요하지 않다. 따라서 스택을 사용하는 것이 매우 효율적이다. 스택은 빠르지만 크기가 고정되고, 사용하는 함수와 그 수명이 같다. 함수를 반환할 때 스택 공간이 반환된다. 그렇기 때문에 소량의 데이터를 저장하는 것이 이상적이다. 따라서 C#이나 자바와 같은 관리되는 런타임은 클래스 데이터를 스택에 저장하지 않고 대신 참조만 저장한다.

이것은 특정 경우에 값 타입이 참조 타입보다 더 나은 성능을 가질 수 있는 또 다른 이유다. 값 타입은 복사로 전달되지만 로컬로 선언된 경우에만 스택에 존재한다.

 

 

[ 2.3 타입에 대한 과대 포장은 무엇인가? ]

2.3.2 유효성 증명

유효성 증명은 사전 정의된 타입을 사용하는, 잘 알려지지 않은 이점 중 하나이다.

잘못된 입력으로 인해 코드 어딘가에서 예상치 못한 예외가 발생하기 때문이다. 이렇게 프로그램이 갑자기 멈추는 것은 사용자 입장에서 최악의 경험이다.

해당 입력을 검사하지 않고 표시할 경우 보안 문제가 발생할 수도 있다.

 

 

꼭 필요한 경우가 아니면 연산자 오버로딩은 사용하지 마라.

이러한 의도를 알아채는 것은 거의 불가능하며, 설명서를 읽지 않는다면 절대 발견할 수 없을 것이다. 타입이 오버로드되는 연산자를 검색하는 IDE 기능도 없다.

 

 

 

2.3.5 nullable이 아니라 non-nullable이라 했어야 한다

널을 처음 고안한 것으로 잘 알려진 토니 호어는 애초에 이것을 처음 만든 것이 “10억 달러의 실수”라고 했다. 하지만 절망적이기만 한 것은 아니다.

nullability 검사는 여러분이 작성 중인 코드에 대한 의도를 파악하는 데 도움을 준다. 이를 통해 여러분은 그 값이 정말 선택적인지 아니면 선택적일 필요가 전혀 없는지에 대해 더 명확하게 생각할 것이다. 이를 통해 여러분은 버그를 줄이고 더 나은 개발자가 될 수 있을 것이다.

 

 

2.3.6 무료 성능 향상

기존의 타입은 더 효율적인 스토리지(저장 공간)를 무료로 사용할 수 있다.

 

 

2.3.7 참조 타입대 값 타입

값 타입의 실제 변수 값은 호출 스택에 저장되는 반면, 참조 타입은 힙에 저장되고, 실제 값에 대한 참조만 호출 스택에 저장된다.

참조는 관리되는 포인터와 유사하다. 포인터는 일종의 메모리 주소이다.

우리는 포인터라는 주소를 전달함으로써 함수 간에 기가바이트의 데이터를 전달할 수 있따. 그렇지 않으면 모든 함수 호출 시 기가바이트의 메모리를 복사해야 한다. 포인터를 사용하면 숫자 하나만 복사하면 된다.

 

 

 

3. 유용한 안티패턴


[ 3.1 깨지 않았다면 깨버려라 ]

3.1.2 빠르게 옮기고 깨버리자

A를 변경하려면 중속된 구성 요소도 변경해야 하며, 그중 하나라도 망가질 위험이 존재하기 때문에 더 어렵다. 프로그래머들은 코드를 더 많이 재사용할수록 시간을 더 많이 절약할 수 있다고 생각하는 경향이 있다. 하지만 이로 인해 어떤 대가를 치러야 하는지를 고민해봐야 한다.

 

 

[ 3.2 처음부터 다시 작성하라 ]

3.2.1 지우고 다시 써라

처음부터 다시 시작하라. 이것이 얼마나 새롭고 빠른 방식인지, 여러분은 상상할 수 없을 것이다. 처음부터 다시 작성하는 것은 매우 비효율적이며 두 배의 시간이 걸릴 것이라고 생각할 수도 있지만, 이미 한 번 해봤기 때문에 그렇지 않다. 이미 문제를 해결하는 방법을 알고 있다.

반복을 통해 게임 실력이 더 나아지듯이, 프로그래밍도 반복할수록 더 잘하게 된다.

 

 

[ 3.5 지금 새로운 것을 시도하라 ]

이미 존재하는 바퀴를 다시 발명하는 것은 문제가 있다. 심지어 컴퓨터 과학에는 이러한 현상에 대한 병리학적인 용어도 있다. NIH증후군이라는 용어인데, 이미 발명된 제품을 스스로 발명하지 않으면 밤에 잘 수 없는 유형의 사람을 일컫는다.

 

 

[ 3.8 불량 코드를 작성하라 ]

모범 사례는 잘못된 코드에서 얻어진다. 또한 모범 사례를 맹목적으로 적용하다 보면 코드를 망칠 수도 있다.

 

 

3.8.2 goto를 사용하라

에츠허르 데이크스트라가 “goto 구문은 해로운 것으로 간주된다”라는 제목의 논문을 발표한 이후 goto 사용은 권장되지 않는다. 데이크스트라의 논문에 대해서는 많은 오해가 있는데, 제목에 대한 오해가 대표적이다. 데이크스트라는 자신의 논문 제목을 “goto 구문에 반대하는 사례”로 정했지만, 파스칼 언어의 발명가이자 그의 편집자였던 니클라우스 비르트가 제목을 바꿨다.

 

 

 

4. 맛있는 테스트


[ 4.1 테스트 유형 ]

소프트웨어 테스트는 소프트웨어의 동작에 대한 자신감을 높이는 일이다. 테스트가 소프트웨어의 동작을 완전히 보장하는 것은 아니지만, 그 가능성을 상당히 높여준다. 테스트의 유형을 분류하는 방법에는 여러 가지가 있지만, 이를 구분하는 가장 중요한 것은 테스트를 실행하거나 구현하는 방식이다. 테스트가 개발 시간에 가장 큰 영향을 끼치기 때문이다.

 

 

4.1.1 수동 테스트

코드 리뷰는 효과가 보다 약하긴 하지만 테스트 방법 중의 하나로 간주된다.

 

 

코드 리뷰란?

이상적으로 코드 리뷰는 코드 스타일이나 형식에 대한 것이 아니다. 린터 혹은 분석 도구라고 불리는 자동화된 도구가 이런 문제를 확인해주기 때문이다. 대신 코드 리뷰는 주로 다른 개발자가 가져올 수 있는 버그나 기술적 부채에 대한 것이어야 한다. 코드 리뷰는 비동기식 페어 프로그래밍이라고 볼 수 있다.

 

 

4.1.4 적합한 테스트 방법 선택하기

  • 비용
    • 특정 테스트를 구현/실행하는 데 얼마나 많은 시간이 필요한가?
    • 몇 번 정도 반복해야 할까?
    • 테스트 코드를 변경하면 테스트할 줄 아는 사람이 있는가?
    • 이 테스트를 신뢰할 수 있게 유지하는 것이 얼마나 어려운가?
  • 위험
    • 이 시나리오가 깨질 가능성은 얼마나 될까?
    • 시나리오가 깨지면 사업에 얼마나 큰 영향을 줄까? 손해가 클까? 혹시 이걸 망치면 해고될까?
    • 시나리오가 깨지면 얼마나 많은 시나리오가 함께 깨지게 될까? 예를 들어 우편물 발송과 관련된 기능이 중지되면 이 기능에 의존하는 다른 여러 기능도 역시 깨질 것이다.
    • 이 코드는 얼마나 자주 변경될까? 앞으로는 얼마나 바뀔 것으로 예상하는가? 모든 변화는 새로운 위험을 가져온다.

 

비용이 가장 적게 들면서 위험도 가장 적은 최적의 방법을 찾아야 한다. 모든 위험은 더 많은 비용을 초래한다.

 

 

 

[ 4.3 TDD와 같이 약어로 된 용어를 사용하지 마라 ]

완벽한 테스트 스위트를 얻었다고 치자. 그러고 나면 코드 설계를 변경하는 것이 내키지 않을 것이다. 이 말은 테스트도 함께 변경해야 함을 의미하기 때문이다.

 

 

[ 4.4 자신의 이득을 위해 테스트를 작성하라 ]

테스트는 소프트웨어와 개발자 모두를 더 나아지게 한다. 좀 더 효율적인 개발자가 되기 위해 테스트를 작성하라.

 

 

[ 4.6 테스트를 작성하지 마라 ]

4.6.1 테스트를 작성하지 마라

코드를 작성할 때 이 부분을 생각해보라. 테스트할 가치가 있을까?

 

 

4.6.2 모든 테스트를 작성하려고 하지 마라

테스트를 현명하게 선택하면 20%의 테스트 커버리지로 80%의 신뢰성을 얻을 수 있다.

버그는 균일하게 발생하지 않는다. 모든 코드 라인이 버그가 발생할 확률을 동일하게 가지는 것은 아니다. 보통 더 자주 사용되거나 변경되는 코드에서 버그를 발견할 가능성이 더 높다. 이처럼 문제가 발새앟ㄹ 가능성이 높은 코드 영역을 핫 패스(hot path)라고 부른다.

마찬가지로 중요한 공유 구성 요소에 좋은 테스트 적용 범위를 갖는 것이 코드 커버리지 100%를 갖는 것보다 더 중요하다. 큰 차이가 없다면 테스트로 커버되지 않는, 기본 생성자에 있는 코드 한 줄에 대한 테스트 커버리지를 추가하기 위해 몇 시간을 소비하지 마라. 이미 우리는 코드 커버리지가 전부가 아니라는 것을 알고 있다.

 

 

 

5. 보람 있는 리팩터링


훌륭한 코드를 작성하는 것은 보통 효율적인 개발자가 뒤기 위한 조건의 절반 정도에 불과하다. 나머지 절반은 코드 변화에 기민하게 대응하는 것이다.

 

[ 5.1 우리는 왜 리팩터링을 하는가? ]

리팩터링은 단순히 코드를 변경하는 것 외의 목적을 수행한다. 리팩터링으로 할 수 있는 것은 다음과 같다.

  • 반복을 줄이고 코드 재사용을 증가시킨다
  • 여러분의 정신 모델과 코드를 더 가깝게 한다
  • 코드를 더 이해하기 쉽고 유지관리하기 쉽도록 만든다
  • 특정 클래스에 버그가 발생하지 않도록 한다
  • 중요한 아키테처 변화를 준비할 수 있다
  • 코드의 경직된 부분을 없앨 수 있다

 

 

[ 5.4 리팩터링을 하지 않는 경우 ]

리팩터링의 좋은 점은 코드를 개선할 방법을 생각하게 한다는 것이다. 리팩터링의 단점은 어느 순간에, 그것이EMACS 처럼 수단이 아닌 목적이 될 수도 있다는 것이다.

모든 코드 조각을 잠재적인 개선의 여지가 있는 것으로 보기 시작한다. 여기에 너무 중독되어 변경을 위한 변경을 할 뿐, 그것을 통해 얻는 이득에 대해서는 고려하지 않는다. 이것은 여러분의 시간을 낭비할 뿐만 아니라 여러분이 가져온 모든 변화에 적응해야 하는 팀원의 시간까지도 낭비하는 것이다.

 

 

 

6. 조사를 통한 보안


트로이 목마 이야기처럼 보안은 인간의 심리에 대한 넓고 깊은 용어이다. 이것이 바로 여러분이 받아야들여야 할 첫 번째 관점이다. 보안은 소프트웨어나 정보에만 국한된 것이 아니라 사람과 환경에 대한 것이다.

 

 

[ 6.4 첫 번째 플러드 그리기 ]

6.4.1 캡차를 사용하지 마라

캡차의 기본 원리는 “점심으로 무엇을 먹을까?”와 같이 사람이라면 쉽게 풀 수 있지만 공격에 사용되는 자동화된 소프트웨어는 해결하기 어려운 수학적으로 복잡한 문제를 풀어보는 과정을 통해 사람을 가려내는 것이다.

캡차는 유용하지만 동시에 서비스 거부 공격만큼이나 유해하다. 애플리케이션이 성장하는 단계에서는 UX에 마찰이 발생하는 것을 원하지 않을 것이다.

충분히 인기를 얻을 때가지 사용자를 힘들게 하지 마라.

 

 

[ 6.5 암호 저장하기 ]

6.5.1 소스 코드에 암호를 유지하는 것

필요 없는 데이터

처음부터 데이터가 없다면 유출될 일도 없다. 서비스 기능에 영향을 준다고 생각되는 데이터가 아니라면, 다른 데이터를 수집하는 것에 적극적으로 반대하라. 이를 통해 스토리지 요구 사항 감소, 성능 향상, 데이터 관리 작업 감소, 사용자의 마찰 감소와 같은 부수적인 이점을 얻을 수 있다.

 

 

올바른 비밀번호 해싱 방법

최신의 모든 무차별 공격에 내성이 있는 해시 알고리즘은 난이도 계수를 매개변수나 반복 횟수로 조절한다.

알고리즘을 선택할 때는 사용 중인 프레임워크에서 지원하는 알고리즘을 선호하는 것이 좋다. 만약 사용할 수 없다면 가장 널리 검증된 알고리즘을 선택해야 한다. 보통 새로운 알고리즘은 이전에 나온 알고리즘보다 테스트나 검증 면에서 부족하다.

 

 

고정된 솔트를 사용하지 마라

솔트는 원래 동일한 해시 값을 갖는 비밀번호이지만 값에 차이를 주기 위해 암호 해싱 갈고리즘에 도입된 추가적인 값을 말한다. 이유는 공격자가 해시 값 하나만을 추측하는 것으로 동일한 비밀번호를 모두 알아내는 것을 원하지 않기 때문이다. 이렇게 하면 모든 사용자의 비밀번호가 hunter2라고 해도 모든 사용자가 다른 해시 값을 갖게 되며, 이는 공격자의 삶을 더 힘들게 만든다.

 

 

UUID는 랜덤이 아니다

보안에 민감한 토큰을 생성하려면 항상 CSPRNG를 사용해야 한다.

 

 

 

7. 자기 주장이 뚜렷한 최적화


최적화에 대한 프로그래밍 참고 문헌은 항상 저명한 컴퓨터 과학자 도널드 커누스의 말을 인용하는 것으로 시작한다. “섣부른 최적화는 모든 악의 근원이다.” 이 말은 잘못 알려졌을 뿐만 아니라 항상 잘못 인용되고 있다. 첫째, 모든 악의 근원은 객체 지향 프로그래밍이라는 것을 모두가 알고 있으므로 이 말은 잘못된 표현이다. 객체 지향 프로그래밍은 나쁜 클래스 구조로 여러 가지 문제를 불러온다. 둘째, 실제 인용문은 더 미묘하기 때문에 잘못됐다. 다른 의미가 있는 라틴어 텍스트의 중간에서 가져온 말이기 때문에 그 뜻을 정확히 알 수 없다. 커누스가 실제로 한 말은 다음과 같다. “우리는 작은 효율성을 잊어야 한다. 97%의 경우에 말이다. 섣부른 최적화는 모든 악의 근원이다. 그러나 중요한 3%의 기회를 놓쳐서는 안 된다.”

나는 오히려 섣부른 최적화가 모든 학습의 근원이라고 주장하고 싶다. 열정적으로 임하는 일에 주저하지 마라.

섣부른 최적화는 존재하지 않는 가상의 문제를 만들어 푸는 것과 같으며 좋은 연습이 되어 준다.

하지만 섣부른 최적화를 권하지 않는 데는 다 이유가 있다. 최적화는 코드에 경직성을 가져와 유지 관리를 더 어렵게 만들 수 있다.

더 중요한 것은 애초에 존재하지 않는 문제를 위해 최적화하려고 했을 수 있고, 이는 코드의 신뢰성을 떨어뜨릴 수 있다는 점이다.

 

 

[ 7.1 올바른 문제를 해결하라 ]

7.1.2 성능 대 응답성

사용자 관점에서 느리다는 것에 대한 일반적인 원칙은 무엇일까? 보통 100밀리초 이상 걸리는 동작은 지연된 것으로 느껴지며, 300밀리초 이상 걸리는 동작은 느린 것으로 간주된다. 1초 이상 걸린다는 것은 생각할 수 없다. 3초 이상 기다려야 한다면 대부분의 사용자는 웹 페이지나 앱을 떠날 것이다. 사용자의 행동에 응답이 5초 이상 걸린다면, 그 시점에서는 이미 우주의 나이만큼 걸리는 것과 차이가 없다.

 

 

[ 7.2 완만함의 분석 ]

CPU는 RAM에서 읽은 명령을 처리하고 끝없는 루푸에서 명령을 반복적으로 수행하는 칩을 말한다. 일부 작업은 회전이 여러 번 필요할 수 있지만, 기본 단위는 한 바퀴이며 보통 클럭 사이클 또는 줄여서 사이클이라고 한다.

일반적으로 헤르츠(Hz)로 표시되는 CPU 속도는 초당 처리할 수 있는 클럭 사이클 수를 의미한다. 최초의 전자 컴퓨터인 에니악은 초당 10만 사이클을 처리할 수 있었고, 즉 속도가 100KHz였다.

최근에 출시된 3.4GHz AMD 라이젠 5950X CPU는 각 코어에서 초당 34억 사이클을 처리할 수 있다. 하지만 이것은 CPU가 처리할 수 있는 명령어 수를 의미하지는 않는다. 먼저, 일부 명령을 처리하는 데 클럭 사이클이 두 개 이상 필요할 수 있으며, 최신 CPU는 코어 하나에서 여러 명령을 병렬로 처리할 수 있기 때문이다. 따라서 때때로 CPU는 클럭 속도가 허용하는 것보다 더 많은 명령을 실행할 수 있다.

블록 메모리 복사 명령과 같이 일부 CPU 명령은 인수에 따라 임의의 시간이 걸릴 수도 있다. 블록 크기에 따라 O(N)의 시간이 걸린다.

기본적으로 코드 속도와 관련된 모든 성능 문제는 명령어 몇 개가 몇 번 실행되는지에 따라 달라진다. 코드를 최적화할 때 실행하는 명령어 수를 줄이거나 더 빠른 버전의 명령어를 사용하려고 시도하라.

 

 

[ 7.3 최고부터 시작하라 ]

실행하는 명령어 수를 줄이는 두 번째로 좋은 방법은 더 빠른 알고리즘을 선택하는 것이다. 첫 번째로 좋은 방법은 코드를 완전히 삭제하는 것이다. 농담이 아니다. 필요 없는 코드는 삭제하라. 코드 성능을 당장 저하시키지는 않더라도 개발자의 능률을 저하시켜 결국 코드 성능까지 저하시킬 수 있다. 주석 처리한 코드를 남겨지지 마라.

 

 

7.3.3 2b || !2b

이 경우 첫 번째 식에서 true를 반환하면 나머지 표현식은 계산하지 않는다.

if (credits > 150_000 & badge.IsVisible) {

 

 

[ 7.4 병목 현상 깨뜨리기 ]

7.4.1 데이터를 패킹하지 마라

CPU가 정렬되지 않은 메모리 주소에서 데이터를 읽는 경우 패널티가 발생할 수 있기 때문에, 어떤 메모리 주소(예를 들어 1023)에서 읽는 것이 다른 메모리 주소(예를 들어 1024)에서 읽는 것보다 더 많은 시간이 걸릴 수 있다. 이런 의미에서 메모리 정렬은 그림 7-4에서 보는 바와 같이 최소 CPU의 워드 크기인 4, 8, 16 등의 배수에 메모리 위치가 오도록 하는 것을 의미한다. 일부 오래된 프로세서에서는 정렬되지 않은 메모리에 액세스할 경우, 사망에 이를 수 있다. 수천 번의 작은 전기 충격 때문이다.

다행히 컴파일러가 보통 이러한 정렬 작업을 처리한다. 그러나 컴파일러의 동작을 오버라이드할 수도 있고, 여전히 무언가 잘못되었다고 느끼지 못할 수도 있다. 작은 공간에 더 많은 것을 저장하면 읽어야 하는 메모리의 크기가 작기 때문에 읽기 작업이 더 빨라져야 한다.

어쩌면 값을 바이트 단위로 유지하여 작은 패킷으로 전달하고 싶어질 수도 있다.

하지만 졍렬되지 않은 경계에 대한 메모리 액세스 속도는 느려지기 때문에 스토리지 절감 효과는 구조체의 각 멤버에 대한 접근 패널티로 무마될 것이다. 만약 구조체의 데이터 타입을 byte에서 int로 변경하고 그 차이를 테스트하기 위해 벤치마크를 생성한다면 표 7-2에서 메모리 사용량이 4분의 1로 줄어들지만, 바이트 액세스 속도는 거의 두 배 느려지는 것을 볼 수 있다.

이 이야기의 교훈은 불필요하게 메모리 공간을 최적화하는 것을 피하라는 것이다. 예를 들어 숫자 10억 개를 배열로 만들고 싶을 때 byte와 int의 차이는 3GB가 될 수 있다. I/O 속도를 위해서 더 작은 크기를 사용하는 것이 바람직할 수도 있지만, 그게 아니라면 메모리 정렬을 신뢰하라.

 

 

CPU 워드 크기

워드 크기는 일반적으로 CPU가 한 번에 처리할 수 있는 데이터의 양으로 정의된다. 이 개념은 CPU가 32비트 또는 64비트라고 불리는 것과 밀접한 관련이 있다. 워드 크기는 대부분 CPU의 어큐뮬레이터 레지스터의 크기를 반영한다. 레지스터는 CPU 수준의 변수와 같으며, 어큐뮬레이터는 가장 일반적으로 사용되는 레지스터이다. Z80 CPU를 예로 들어 보자. 16비트 레지스터가 있고, 16비트 메모리 주소를 지정할 수 있지만 8비트 어큐뮬레이터 레지스터가 있기 때문에 8비트 프로세서로 간주된다.

 

 

7.4.2 근접성을 활용하라

캐싱이란 자주 사용하는 데이터를 일반적인 메모리 위치보다 더 빠르게 접근 가능한 위치에 보관하는 것을 말한다. CPU는 서로 다른 속도의 자체 캐시 메모리를 가지고 있으며, 모두 RAM보다 빠르다.

예를 들어 순차적으로 읽는 것이 메모리 주위를 랜덤으로 읽는 것보다 빠르다는 것을 의미한다.

 

 

7.4.3 종속 작업을 세분화하라

단일 CPU 명령어는 프로세서의 개별 유닛에 의해 처리된다. 예를 들어 한 유닛이 명령어의 디코딩을 담당하는 반면, 다른 유닛은 메모리 액세스를 담당한다. 그러나 디코더 유닛은 명령어가 완료될 때까지 기다리지 않고 메모리 액세스가 실행되는 동안 다음 명령어에 대한 다른 디코딩 작업을 실행할 수 있다. 이러한 기술을 파이프라이닝이라고 하며, 다음 명령어가 이전 명령어의 결과에 의존적이지 않는 한 CPU가 단일 코어에서 여러 명령어를 병렬로 실행할 수 있다는 것을 의미한다.

종속성을 줄이거나 적어도 명령어 흐름을 차단할 수 있는 영향을 줄이는 방법이 있다. 하나는 종속 코드 사이의 간격을 늘리기 위해 명령어의 순서를 변경하는 것이다. 이는 어떤 명령어가 첫 번째 연산 결과에 대한 의존성 때문에 파이프라인에서 다음 명령어를 차단하지 않도록 한다.

 

 

7.4.4 예측할 수 있도록 하라

스택 오버플로 역사상 가장 유명한 질문은 “왜 정렬되지 않은 배열을 처리하는 것보다 정렬된 배열을 처리하는 것이 더 빠를까?”이다. 실행 시간을 최적화하기 위해 CPU는 실행 코드보다 선제적으로 움직여 필요하기 전에 미리 준비한다. 이럴 때 CPU가 사용하는 기술을 분기 예측(branch prediction)이라고 한다.

 

 

7.4.5 SIMD

CPU는 또한 단일 명령어로 여러 데이터에 대한 연산을 동시에 실행할 수 있는 특수한 명령어를 지원한다. 이 기술을 단일 명령어 다중 데이터(Single Instruction Multiple Data, SIMD)라고 한다. 여러 변수에 동일한 연산을 하려는 경우 SIMD는 이를 지원하는 아키텍처에서 성능을 크게 향상시킬 수 있다.

 

 

[ 7.5 1초와 0초의 I/O(입출력) ]

I/O는 CPU가 디스크, 네트워크 어댑터, 심지어 GPU와 같은 주변 하드웨어와 통신하는 모든 것을 포함한다. I/O는 보통 성능 체인에서 가장 느린 링크이다. 생각해보라. 하드 드라이브는 실제로 데이터를 탐색하는 스핀들이 있는 회전식 디스크이다. 이것은 끊임없이 움직이는 로봇 팔과 같다. 네트워크 패킷은 빛의 속도로 이동할 수 있지만, 지구를 한 바퀴 도는데 여전히 100밀리초 이상이 걸릴 것이다. 프린터는 특히 느리고 비효율적이며 분노 유발을 일으키도록 설계되었다.

대부분의 경우 I/O 자체의 속도를 빠르게 만들 수는 없다. 물리적인 한계 때문이다. 하지만 하드웨어는 CPU와 독립적으로 실행될 수 있으며 CPU가 다른 작업을 수행하는 동안에도 여전히 작동할 수 있다. 즉, CPU와 I/O 작업을 겹치면 더 짧은 시간 내에 전체 작업을 완료할 수 있다.

 

 

7.5.1 I/O 속도 향상

물론, 하드웨어의 고유한 한계로 인해 I/O 속도는 느리지만 이를 더 빠르게 만들 수 있다.

여기서 ReadByte() 함수는 운영 체제의 읽기 함수를 호출한다. 운영 체제는 커널 모드를 호출하는데, 이는 CPU가 실행 모드를 변경하는 것을 의미한다. 운영 체제 루틴은 파일 핸들과 필수적인 데이터 구조를 검색하는 것이다. I/O 결과가 이미 캐시에 있는지 확인하고, 캐시에 없으면 관련 장치 드라이브럴 호출하여 디스크에서 실제 I/O 작업을 실행한다. 메모리의 읽기 부분은 프로세스의 주소 공간에서 버퍼로 복사된다. 이러한 작업은 순식간에 발생하며 1바이트만 읽어도 중요해질 수 있다.

많은 I/O 장치는 블록 단위로 읽고 쓰며 이를 블록 장치라고 부른다. 네트워크나 저장 장치는 일반적으로 블록 장치이다. 키보드는 한 번에 문자 하나를 보내기 때문에 문자 장치이다. 블록 장치는 블록 크기보다 작게 읽을 수 없으므로 일반적인 블록 크기보다 작은 것을 읽는 것은 의미가 없다. 하드 드라이브의 섹터 크기가 512바이트이므로 이는 디스크를 위한 일반적인 블록 크기가 된다. 최신 디스크는 더 큰 블록 크기를 가질 수 있지만, 512바이트를 읽기만 해도 성능이 얼마나 향상되는지 살펴보자.

 

 

7.5.2 I/O를 논-블로킹(non-blocking)으로 만들라

비동기 I/O(Async I/O)는 I/O 부하가 높은 작업만을 위한 병렬화 모델이며 단일 코어에서 작동할 수 있다. 멀티 스레딩과 비동기 I/O는 서로 다른 목적을 위해 활용되며 함께 사용할 수도 있다.

I/O는 자연스럽게 비동기적이다. 외부 하드웨어는 거의 항상 CPU보다 느리기 때문이다. CPU는 기다리면서 아무것도 하지 않는 것을 좋아하지 않는다. 인터럽트나 직접 메모리 접근(Direct Memory Access, DMA)과 같은 메커니즘은 I/O 동작이 완료되었을 때 하드웨어가 CPU에 신호를 보내고 CPU가 결과를 전송하도록 개발되었다. 즉, 하드웨어에 I/O 작업이 할당되면 하드웨어가 그 작업을 실행하는 동안 CPU는 다른 작업을 계속 처리할 수 있으며, I/O 작업이 완료될 때 다시 그 결과를 확인할 수 있다. 이러한 메커니즘이 비동기 I/O의 기초가 된다.

비동기 I/O의 성능 이점은 추가적인 작업 없이 코드에 자연스러운 병렬화를 제공하는 데 있다. 스레드를 추가로 만들 필요도 없다. 여러 I/O 작업을 병렬로 실행하고 경쟁 조건과 같은 멀티스레딩이 초래하는 문제를 겪지 않고도 결과를 수집할 수 있다. 실용적이며 확장 가능하다.

비동기 코드는 또한 스레드 소모 없이 이벤트 기반 메커니즘, 특히 사용자 인터페이스에서 응답성을 개선할 수 있다. UI는 I/O와 아무 관련이 없는 것처럼 보일 수 있지만, 사용자 입력은 터치스크린, 키보드, 마우스와 같은 I/O 장치에서 발생하며 사용자 인터페이스는 이러한 이벤트에 의해 트리거된다. 이들은 비동기 I/O와 비동기 프로그래밍을 위한 완벽한 후보이다. 타이머 기반의 애니메이션도 장치에서 타이머가 작동하는 방식 때문에 하드웨어로 구동되므로 비동기 I/O에 이상적인 후보다.

 

 

7.5.3 오래된 방법

2010년대 초반까지는 콜백 함수로 비동기 I/O를 관리했다. 비동기 운영 체제의 함수들은 콜백 함수를 전달해야 하며, I/O 작업이 끝나면 OS가 콜백 함수를 실행한다. 그동안 다른 작업을 수행할 수 있다.

이러한 상황은 Node.js 개발자들이 만든 용어처럼 콜백 지옥으로 바뀔 수 있다.

 

 

7.5.4 최신 비동기/대기

함수 내에서 await를 사용할 수 있도록 async 키워드로 함수를 선언한다. await 구문은 일종의 앵커를 정의하며, 그 뒤에 오는 표현식을 실행할 때까지 기다리지 않는다. 대기 중인 I/O 작업이 끝났을 때 돌아갈 지점을 표시하는 것뿐이며 다음 진행을 위해 새로운 콜백을 정의하지 않아도 된다. 일반적인 동기 코드처럼 코드를 작성할 수 있다.

async/await 키워드를 사용하여 코드를 작성할 때 뒤에 있는 코드는 컴파일 도중에 코드 7-11처럼 콜백 함수와 유사한 것으로 변환된다. async/await 사용으로 엄청난 수고를 줄일 수 있다.

 

 

7.5.5 비동기 I/O의 잠재적 문제

프로그래밍 언어에서 I/O에만 비동기 메커니즘을 사용할 필요는 없다. I/O와 관련된 작업의 호출 없이 async 함수를 선언하고 CPU 작업만 수행할 수 있다. 그런 경우에는 아무런 이점도 없이 불필요하고 복잡하게 만들 뿐이다. 컴파일러는 보통 이런 상황에 대해 경고하지만, 기업 환경에서 컴파일러 경고를 무시하는 경우가 많다.

async/await을 사용할 때 기억해야 하는 원칙 중 하나는 await는 기다리지 않는다는 것이다. 물론 await는 분명히 실행이 완료된 후 다음 줄을 실행한다. 하지만 내부의 비동기식 콜백 함수로 기다리거나 차단 없이 바로 실행한다. 만약 비동기 코드에서 무언가 완료될 때까지 계속 기다린다면 그건 잘못된 것이다.

 

 

[ 7.6 다른 모든 것이 실패할 경우 캐시를 이용하라 ]

캐싱은 성능을 즉시 향상시킬 수 있는 가장 확실한 방법 중 하나이다. 캐시 무효화는 어려운 문제일 수 있지만 무효화에 대해 걱정하지 않는 것만 캐싱하면 문제가 되지 않는다.

캐싱을 위해 설계되지 않은 데이터 구조는 사용하지 마라. 보통 오래된 데이터를 제거하거나 만료하는 메커니즘이 없기 때문에 메모리 누수의 원인이 되고, 결국에는 충돌하게 된다. 캐싱을 위해 설계된 것을 사용하라. 데이터베이스는 훌륭하고 영구적인 캐시가 될 수도 있다.

캐시 만료 시간이 무한대인 것을 두려워하지 마라. 이 우주가 끝나기 전에 캐시가 제거되거나 애플리케이션 재시작이 이뤄질 것이다.

 

 

 

8. 기분 좋은 확장성


[ 8.2 불일치를 수용하라 ]

신뢰성은 흑백의 개념이 아니다. 성능과 확장성이 크게 향상된다면 견딜 수 있는 수준의 비신뢰성이 있다.

 

 

8.2.1 무서운 NOLOCK

결과적 일관성이란 확실히 일관성이 보장되기는 하지만 지연 시간 후에만 가능한 것을 의미한다.

결과를 제대로 알고 있다면 이러한 불일치를 두려워하지 않아도 된다. 트레이드오프의 영향을 제대로 알고 있다면 확장성을 더욱 높이기 위해 의도적으로 불일치를 선호할 수도 있다.

 

 

[ 8.3 데이터베이스 연결을 캐시하지 마라 ]

이 비밀 해결책은 쿼리 수명 동안에만 연결을 유지하는 것이다. 이렇게 하면 가능한 한 빨리 풀에 연결을 반환하여 다른 요청이 사용 가능한 연결을 가져올 수 있도록 하며, 가능한 한 큰 확장성을 얻을 수 있다.

 

 

[ 8.4 스레드를 사용하지 마라 ]

스레드 풀에는 대개 시스템의 CPU 코어 수보다 더 많은 스레드가 있는데, 이는 스레드가 종종 I/O와 같은 다른 작업이 끝날 때까지 대기해야 하기 때문이다. 이렇게 하면 특정 스레드가 I/O가 완료되기를 기다리는 동안 같은 CPU 코어에 다른 스레드를 스케줄링할 수 있다.

CPU는 사용 가능한 CPU 코어 수보다 더 많은 스레드를 제공하여 한 스레드가 완료되기를 기다리는 동안 동일한 코어에서 실행을 위해 다른 스레드를 사용할 수 있다.

이는 CPU 코어 수만큼의 스레드를 갖는 것보다는 낫지만, 소중한 CPU 시간을 최대한 활용할 수 있을 정도로 정확하지는 않다. 운영 체제는 스레드에 짧은 실행 시간을 제공한 다음 CPU 코어를 다른 스레드에 양보하여 모든 스레드가 적당한 시간 안에 실행될 수 있도록 한다. 이러한 스케줄링 기술을 선점형(preemption)이라고 부르며, 이는 단일 코어 CPU에서 멀티태스킹을 다루던 방식이다. 운영 체제는 같은 코어에서 모든 스레드를 저글링하며 마치 멀티태스킹이라는 착각을 불러 일으킨다. 다행히 대부분의 스레드는 I/O를 기다리기 때문에 CPU 집약적인 애플리케이션을 실행하지 않는 한 사용자는 스레드가 단일 CPU에서 번갈아 실행된다는 사실을 눈치채지 못한다. CPU 연산이 많이 필요한 작업을 실행하면 아마 그 효과를 느끼게 될 것이다.

운영 체제가 스레드를 스케줄링하는 방법 때문에 스레드 풀에 CPU 코어 수보다 더 많은 스레드가 있는 것은 활용률을 높이기 위한 단순한 방법이지만, 실제로는 확장성을 해칠 수도 있다. 스레드가 너무 많으면 모든 스레드가 CPU 시간보다 작은 시간 조각을 얻게 되고 결국 실행 시간이 더 오래 걸리고 웹 사이트나 API 반응이 엄청나게 느려질 수 있다.

I/O 대기 시간을 활용하는 더 정확한 방법은 7장에서 설명한 대로 비동기식 I/O를 사용하는 것이다. 비동기 I/O는 명시적이다. await 키워드가 있으면 스레드가 콜백 결과를 기다린다는 것을 의미하며 하드웨어가 I/O 요청을 위해 작업하는 동안 다른 요청이 동일한 스레드를 사용할 수 있다.

 

 

8.4.1 비동기 코드의 주의사항

I/O 작업이 없다면 비동기도 필요 없음을 의미한다

어떤 함수가 비동기 함수를 호출하지 않는다면 그 함수도 비동기일 필요는 없다. 비동기식 프로그래밍은 I/O 바운드 작업과 함께 사용할 때만 확장성에 도움을 줄 수 있다. CPU 바운드 작업에서 비동기를 사용하면 단일 스레드에서 병렬로 실행되는 I/O 작업과 달리 작업을 위한 별도의 스레드가 필요하기 때문에 확장성에 도움이 되지 않는다.

 

 

동기화와 비동기화를 섞지 마라

동기화 코드에서 비동기 함수를 기다리는 것의 가장 큰 문제는 호출자 코드에 의존하는 비동기 함수에 잇는 다른 함수로 데드락이 발생할 수 있다는 것이다.

 

 

8.4.2 비동기를 이용한 멀티스레딩

비동기 I/O는 리소스를 덜 소모하기 때문에 I/O가 많이 필요한 코드에서 멀티스레딩보다 더 나은 확장성을 제공한다. 그러나 멀티스레딩과 비동기는 서로 베타적이지 않다. 두 가지를 동시에 가질 수 있고, 멀티스레드 코드를 작성하기 위해 비동기 프로그래밍 구문을 사용할 수도 있다.

 

 

9. 버그와의 동거


[ 9.1 버그를 수정하지 마라 ]

제법 큰 프로젝트를 맡고 있는 개발 팀이라면, 어떤 버그를 수정할 것인지를 결정하기 위한 트리아지 프로세스를 가지고 있어야 한다. 트리아지라는 용어는 1차 세계대전 중에 유래되었는데, 당시 의료진이 아직 생존 가능성이 있는 사람에게 제한된 자원을 할당하기 위해 어떤 환자는 먼저 치료하고 어떤 환자는 방치할 수밖에 없었던 결정에서 유래되었다. 이는 제한된 자원을 효과적으로 활용할 수 있는 유일한 방법이었다. 트리아지는 여러분이 무엇을 고쳐야 하며, 혹은 그것을 정말 고쳐야 하는지 결정하는 것을 도와준다.

우선순위와 심각성에 대한 임계 값을 설정하여 순위가 그 아래인 버그들은 고치지 않도록 해야 한다.

 

 

[ 9.2 오류에 대한 두려움 ]

9.2.2 예외를 잡아내지 마라

예외는 충돌의 원인이 되므로 잡아내지 마라. 이 예외가 잘못된 동작 때문에 발생한 것이라면 원인이 되는 버그를 수정하라. 이미 알고 있는 이유 때문에 발생하는 경우라면 해당하는 특정 사례를 위해 코드에 명시적으로 예외 처리 문을 입력하라.

 

 

9.2.3 예외 복원성

코드에 충돌이 발생한 경우에도 예외 처리 없이 올바르게 동작해야 한다. 예외가 계속 발생하더라도 잘 작동하는 흐름을 설계해야 하며, 오염된 상태에 빠지지 않도록 만들어야 한다.

예외 복원성이 뛰어난 설계는 멱등성(idempotency)에서 시작한다. 멱등성이란 함수나 URL이 호출 횟수에 관계없이 동일한 결과를 반환하는 성질을 의미한다.

 

 

[ 9.3 디버깅하지 마라 ]

디버거는 매우 편리하지만 항상 최고의 도구는 아니다. 문제의 근본적인 원인을 파악하는 데 시간이 더 많이 걸릴 수 있다. 항상 모든 상황에서 프로그램을 디버깅할 수 있는 것은 아니다. 코드가 실행 중인 환경에 접근하지 못할 수도 있다.

 

 

9.3.3 예외와 오류

이 책 앞부분에서 간단히 논의했듯이, 고무 오리 디버깅(rubber duck debugging)은 책상 위에 앉아있는 고무 오리에게 문제를 말해 해결하는 방법이다. 문제를 말로 표현하면서 여러분은 문제를 더 확실하게 재구성하게 되고 마법처럼 문제에 대한 해결책을 찾을 수 있게 된다.

나는 이를 위해 스택 오버플로 웹 사이트의 임시 작성글을 사용한다.

이 플랫폼에서 다른 사람에게 받는 압력을 의식한다면 질문을 구성할 때 중요한 한 가지 측면에 대해 재차 생각하게 되기 때문이다. “여러분은 무슨 시도를 했는가?”

스슬에게 이 질문을 하는 것은 여러 가지 이점이 있지만, 가장 중요한 것은 여러분이 아직 가능한 모든 해결책을 시도하지 않았따는 것을 깨닫게 해준다는 것이다. 질문에 대해 생각하다 보면 내가 고려하지 못한 수많은 다른 가능성을 함께 떠올리게 된다.

 

 

 

 

 

 

 

 

 

0. 스트리트 코더(Street Coder) 독서 후기


[ 스트리트 코더(Street Coder) 독서 후기 ]

이번에 읽은 책은 스트리트 코더(Street Coder)라는 책으로, 출판사로부터 받고 읽게 된 책이다. 본래는 실용적인 학습을 위해 다른 사람이 먼저 읽고 좋다고 평가한 책들 중에서 선정하여 읽는다. 하지만 이번 책은 예외적으로 읽어보았는데, 내용이 너무 좋았다.

출판사 측에서도 먼저 도서 제안을 주신 터라 내용이 좋지 않았다면 리뷰나 포스팅을 하지 않아도 괜찮다고 했는데, 개인적으로 다른 분들도 읽으면 좋을 것 같다는 생각이 많이 들어서 짧은 후기를 작성해본다.

개발을 할 때에는 의미론적 수준과 구현적 수준을 고려해야 하는데, 많은 책들에서 이를 고려하지 않으며 의미론적 부분을 강조하며 구현 수준을 의미론적 수준까지 끌어올리기를 주장한다. 하지만 실무자의 입장에서 이러한 부분의 유용성 또는 실용성을 고려하면 공감이 가지 않은 부분이 많았다. 때로는 저자가 해당 부분에 대하여 실무자의 입장에서도 충분한 관점이 있는지 의구심을 갖기도 했다.

하지만 이 책은 실용적으로 프로그래밍하기를 선호하는 나의 관점과 비슷한 부분이 많아서 좋았다. 예를 들어 "테스트할 가치가 있는지 판단하는 부분"과 "단일 구현에서는 인터페이스를 도입하지 말라" 라는 부분 등 확실한 실무자가 작성한 책이라는 생각이 많이 들었다. 그 외에도 많은 책에서 "하라"는 부분을 "하지 말라"고 주장하는 부분도 흥미로웠고, 성능을 위해 하드웨어 수준까지 내려가는 부분 등은 나의 학습 깊이를 더욱 낮춰갈 필요가 있음도 느꼈다. 책의 내용도 두껍지 않아서 가볍게 읽기 좋았던 것 같다.

개인적으로 도서 및 영상 내지는 컨텐츠 등의 수준에 꽤나 엄격하여 추천하기를 조심스러워 하는 편인데, 해당 책은 한번쯤 읽어봐도 좋을 것 같다는 생각이 많이 들었다.  아직 본인 만의 코딩 가치관이나 자아를 형성하지 못했다면 특히 좋은 책이 될 것 같다.

 

 

 

 

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함