실용주의 프로그래머 Day7

HYl·2022년 4월 1일
0

DAY 7

오늘 읽은 범위 : 7장 코딩하는 동안


책에서 기억하고 싶은 내용을 써보세요.

파충류와 이야기하는 법

  • 여러분 내면의 파충류에게 귀 기울여라.
  • 일단, 하고 있는 일을 멈춰라. 여러분의 뇌가 정리를 좀 할 수 있도록 약간의 시간과 공간을 확보하라. 코드에 대해 생각하지 말고 키보드에서 떨어져서 잠깐 머리를 비운 채로 할 수 있는 일을 하라.
  • 언젠가는 다시 생각이 의식의 영역으로 올라와서 ‘아하!’하는 순간이 찾아올 것 이다.
  • 동료에게 설명해 보라. 프로그래머가 아닌 사람이면 더 좋고 사람이 없으면 고무 오리도 괜찮다. 여러분 뇌의 다른 부위에 문제를 노출하라.

의도적으로 프로그래밍하기

  • 자신도 잘 모르는 코드를 만들지 말라. 완전히 파악하지 못한 애플리케이 션을 빌드하거나, 이해하지 못한 기술을 사용하면 우연의 함정에 빠질 가 능성이 높다. 이것이 왜 동작하는지 잘 모른다면 왜 실패하는지도 알 리가 없다.
  • 가정을 기록으로 남겨라. 147쪽의 ‹항목 23. 계약에 의한 설계›를 따르면 자신의 마음속에서 가정을 명확하게 하는 데 도움이 될뿐더러, 다른 사람과 그에 대해 소통하는 데에도 도움이 된다.
  • 코드뿐 아니라 여러분이 세운 가정도 테스트해 보아야 한다. 어떤 일이든 추측만 하지 말고 실제로 시험해 보라. 여러분의 가정을 시험할 수 있는 단정문을 작성하라.

알고리즘을 추정한다는 말의 의미

  • 대문자 O 표기법이 유용하다. 대문자 O 표기법은 근삿값을 다루는 수학적 방법으로 O( )와 같이 표기

상식으로 추정하기

  • 단순 반복문
    • 단순 반복문 하나가 1부터 n까지 돌아간다면 이 알고리즘은 O(n)일 가능성이 크다.
    • 대표적인 예로는 소진 탐색(exhaustive search), 배열에서 최댓값 찾기, 체크섬 생성하기 등
  • 중첩 반복문
    • 반복문 안에 또 반복문이 들어 있다면, 알고리즘은 O(m×n)이 되며, 여기서 m과 n은 두 반복문의 반복 횟수다
    • 이런 상황은 보통 버블 정렬처럼 간단한 정렬 알고리즘에서 나타나는데, 여기서 바깥쪽 반복문은 배열의 각 원소를 차례대로 방문하고, 안쪽 반복문은 그 원소를 정렬된 결과 중 어디에 둘 것인지 찾는다. 이런 정렬 알고리즘은 O(n2)이 되기 쉽다.
  • 반씩 자르기
    • 반복문을 돌 때마다 작업 대상의 수를 반으로 줄여 나가는 알고리즘이라면 로그적 알고리즘, 즉 O(lgn)이 될 가능성이 크다. 정렬된 목록의 이진 검색이나 이진 트리의 탐색, 정수의 2진수 표현에서 첫 번째 1인 비트를 찾는 문제 등이 모두 O(lgn)이 될 수 있다.
  • 분할 정복
    • 입력 데이터를 둘로 나눠서 각각 독립적으로 작업한 다음, 결과를 합치는 알고리즘은 O(nlgn)일 수 있다.
    • 퀵 정렬(quicksort)이 전형적인 예로, 퀵 정렬은 데이터를 반으로 나누고 각 반쪽에서 재귀적으로 정렬을 수행한다. 이미 정렬된 입력값이 들어올 때는 성능이 떨어지기 때문에 엄밀하게는 O(n2)이지만, 퀵 정렬의 평균 수행 시간은 O(nlgn)이다.
  • 조합적
    • 알고리즘이 항목의 순열(permutation)을 다루기 시작하면 대부분의 경우 수행 시간은 걷잡을 수 없이 늘어난다. 순열에는 계승(factorial)이 따라오기 때문이다. (1부터 5까지의 숫자로 이루어진 순열은 5!=5×4×3×2×1=120가 지나 있다.) 원소 5개를 처리하는 조합적 알고리즘의 시간을 재보자. 원 소 6개를 처리하려면 시간이 6배나 더 걸릴 것이다. 7개를 처리하려면 시간이 42배 걸린다. ‘난해(hard)’하다고 분류되는 문제를 푸는 알고리즘이 대개 여기에 속한다.
    • 여행하는 외판원 문제, 상자에 물건을 최적으로 집어넣는 문제, 숫자 집합을 분할해서 각 부분 집합의 원소 합을 모두 같게 만드는 문제등 이다.

실전에서의 알고리즘 속도

  • 사용하는 알고리즘의 차수를 추정하라.
  • 잠재적인 문제점을 해결하기 위해 생각해 볼 수 있는 방법이 몇 가지 있다. O(n2) 알고리즘이 있다면 분할 정복을 사용하여 O(nlgn)으로 줄일 수 없는 지 시도해 보라.
  • 결과를 그래프로 그려보라.
    • 정확하게 시간을 재는 일이 어렵다면 ‘코드 프로파일러code profiler’를 사용하여 알고리즘이 돌아갈 때 각 단계의 실행 횟수를 센 다음 입력값 크기별 실행 횟수를 그래프로 그려 보라.
  • 어떤 일을 하는 코드인지 코드 자체에 대해서도 생각해 보라. 입력값 n이 작을 경우, 단순한 O(n2) 코드가 복잡한 O(nlgn) 코드보다 더 좋은 성능을 내기도 한다. O(nlgn) 알고리즘의 반복문 안에 무거운 작업이 들어 있는 경 우라면 특히 더 그렇다.
  • 입력 데이터 집합이 작을 때는 수행 시간이 선형적으로 늘어나다가도, 수백만 개의 레코드를 입력하면 스래싱(thrashing)이 발생하면서 갑자기 수행 시간이 폭증하기도 한다.
    • 스래싱 : 과도한 메모리 사용으로 인하여 지속해서 페이지 폴트가 발생하는 상황을 말한다. 그 결과 시스템의 성능이 급격하게 떨어진다.

최고라고 언제나 최고는 아니다

  • 적당한 알고리즘을 선택할 때도 실용적이어야 할 필요가 있다. 가장 빠른 알 고리즘이 언제나 가장 좋은 알고리즘은 아니다. 입력값의 규모가 작다면 단순한 삽입 정렬도 퀵 정렬과 비슷한 성능을 낸다. 그러나 삽입 정렬을 작성하 고 디버깅하는 데 걸리는 시간은 퀵 정렬보다 적다.

리팩터링

밖으로 드러나는 동작은 그대로 유지한 채 내부 구조를 변경함으로써 이미
존재하는 코드를 재구성하는 체계적 기법.

  1. 이 활동은 체계적이다. 아무렇게나 하는 것이 아니다.
  2. 밖으로 드러나는 동작은 바뀌지 않는다.기능을 추가하는 작업이 아니다.

리팩터링은 언제 하는가?

  • 중복
    • DRY 원칙 위반을 발견했다.
  • 직교적이지 않은 설계
    • 더 직교적으로 바꿀 수 있는 무언가를 발견했다.
  • 더 이상 유효하지 않은 지식
    • 사물은 변하고, 요구 사항은 변경되며, 지금 처리하고 있는 문제에 대한 여러분의 지식은 점점 늘어난다. 코드는 지금 상황에 뒤떨어지지 않아야 한다.
  • 사용 사례
    • 진짜 사람들이 실제 상황에서 시스템을 사용하게 되면, 여러분은 어떤 기능은 예전에 생각했던 것보다 더 중요하고, “꼭 필요하다”고 생각했던 기능은 그렇지 않은 경우도 있다는 것을 깨닫게 될 것이다.
  • 성능
    • 성능을 개선하려면 시스템의 한 영역에서 다른 영역으로 기능을 옮겨야 한다.
  • 테스트 통과
    • 앞에서 설명했듯이 리팩터링은 작은 규모의 활동이고, 좋은 테스트가 뒷받침되어야 한다. 그러니 여러분이 코드를 조금 추가한 후 추가한 테스트가 통과했을 때가, 방금 추가한 코드로 다시 뛰어들어 깔끔하게 정리하기에 최고의 타이밍이다.

리팩터링은 어떻게 하는가?

  1. 리팩터링과 기능 추가를 동시에 하지 말라.
  2. 리팩터링을 시작하기 전 든든한 테스트가 있는지 먼저 확인하라. 할 수 있는 한 자주 테스트를 돌려 보라. 이렇게 하면 여러분이 바꾼 것 때문에 무언가 망가졌을 경우 그 사실을 재빨리 알 수 있다.
  3. 단계를 작게 나누어서 신중하게 작업하라. 클래스의 필드 하나를 다른 클래스로 옮기기, 메서드 하나 쪼개기, 변수명 하나 바꾸기 같이 작은 단위로 작업해야 한다. 리팩터링에서는 국지적인 변경들이 많이 모여서 커다란 규모의 변화를 낳는 일이 자주 발생한다. 단계를 작게 나누고, 한 단계가 끝날 때마다 테스트를 돌린다면 기나긴 디버깅 작업을 피할 수 있다.

깨진 창문을 그대로 놓아두지 말자.

테스트로 코딩하기

  • 테스트는 버그를 찾기 위한 것이 아니다.
    • 우리는 테스트의 주요한 이득이 테스트를 실행할 때가 아니라 테스트에 대해 생각하고, 테스트를 작성할 때 생긴다고 믿는다.

테스트에 대해 생각하기

테스트에 대해 생각하는 것으로 시작했는데 코드 한 줄 쓰지 않고도 두 가지
발견을 했다. 그리고 이를 바탕으로 우리 메서드의 API를 변경했다.

테스트 주도 개발 (test driven development, TDD)

  1. 추가하고 싶은 작은 기능 하나를 결정한다.
  2. 그 기능이 구현되었을 때 통과하게 될 테스트를 하나 작성한다.
  3. 테스트를 실행한다. 다른 테스트는 통과하고 방금 추가한 테스트 딱 하나만 실패해야 한다.
  4. 실패하는 테스트를 통과시킬 수 있는 최소한의 코드만 작성한다. 그리고 이제는 모든 테스트가 통과하는지 확인한다.
  5. 코드를 리팩터링한다. 방금 작성한 테스트나 함수를 개선할 수 있는 부분이 없는지 살펴본다. 개선한 후에도 테스트가 계속 통과하는지 확인한다.
  • 여러분의 소프트웨어를 테스트하라. 그러지 않으면 사용자가 테스트하게 된다.
profile
꾸준히 새로운 것을 알아가는 것을 좋아합니다.

0개의 댓글