좋은 코드, 나쁜 코드 2장

2장 추상화 계층

널값 및 의사코드 규약

  • 널값에 대한 규약을 지정하지 않으면, NullPointerException, NullRefrenceException, Cannot read property of null과 같은 에러를 발생할 수 있다.
  • 이를 위해 널 안전성과 보이드 안전성에 대한 개념이 생겼다.
  • 자바나 c++에서는 옵셔널 타입을 통해서 널 안전성을 보장한다.

추상화 계층 및 코드 품질의 핵심 요소

  • 가독성
    • 모든 세부 사항을 이해하는 것은 불가능하지만, 높은 계층의 추상화를 이해하고 사용하기 쉽게 해야한다.
  • 모듈화
    • 구현 세부 사항이 외부로 노출되지 않도록 보장할 때, 다른 계층이나 코드의 일부에 영향을 미치지 않고 계층 내에서만 구현을 변경하기가 매우 쉬워진다.
  • 재사용성 및 일반화성
  • 테스트 용이성

코드의 계층

API 및 구현 세부 사항

  • 코드를 작성할 때 고려해야 할 측면이 두 가지 있다.
    • 코드를 호출할 때 볼 수 있는 내용
      • 퍼블릭 클래스, 인터페이스 및 함수(메서드)
      • 이름, 입력 매개변수 및 반환 유형이 표현하고자 하는 개념
      • 코드 호출 시 코드를 올바르게 사용하기 위해 알아야 하는 추가 정보
    • 코드를 호출할 때 볼 수 없는 내용
      • 구현 세부 사항
  • 서비스를 구축한 경험이 있다면 애플리케이션 프로그래밍 인터페이스라는 용어를 알게 될 것이다.
    • 서비스를 사용할 때 알아야 할 것들에 대한 개념을 형식화
    • 서비스의 모든 구현 세부 사항을 이 API 뒤에 감춤
    • 미니 API를 노출하는 것
    • 구현 세부 사항으로는 프라이빗 함수 & 변수와 클래스가 의존하는 라이브러리가 있다.
    • 함수가 퍼블릭이어도 함수 내의 코드는 구현 세부 사항에 포함된다.
    • 코드를 작성하거나 수정할 때, API에 수정 사항에 대한 구현 세부 정보가 새어 나간다면 추상화 계층이 명확하게 구분 된 것은 아니다. (입력 매개변수, 반환 유형, 퍼블릭 함수 등을 통해)

함수를 작성하기 위한 좋은 전략

  • 로직을 함수로 구현하는 것은 매우 유용하다.
  • 함수 명명 규칙에는 중요한 두 가지 사항이 있다.
    • 단일 업무 수행
    • 잘 명명된 다른 함수를 호출해서 더 복잡한 동작 구성
  • 코드의 가독성과 재사용성을 높여준다.
  • 함수로 표현하기 어렵게 구현되었다면, 잘 명명된 헬퍼 함수로 분리하는 것을 고려한다.
    • 헬퍼 함수 : 사용자 정의 함수이며, 코드의 여러 위치에서 사용할 수 있다.

클래스 분리하기

  • 클래스를 분리하기 위해서 아래와 같은 많은 이론과 경험 법칙을 제시할 수 있다.
    • 줄 수
      • 코드는 300줄을 넘어야하지 않는다 등의 number of lines 규칙
      • 실제로 사용하기는 상당히 제한적
    • 응집력
      • 순차적 응집력 : 한 요소의 출력이 다른 요소의 입력으로 필요할 때 사용 된다.
        • 원두를 간다 → 커피를 내린다
      • 기능적 응집력 : 몇 가지 요소들이 모여서 하나의 일을 성취하는데 기여
        • 주관적일 수도 있지만, 케이크를 만들기 위해 요소들을 요구하는 것을 뜻한다. (케이크 통, 장비, 그릇 등..)
    • 관심사 분리
      • 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다고 주장하는 설계 원칙
        • 게임 콘솔로 예를 들 수 있다.
          • TV와 게임 콘솔을 분리
          • 각 분리에 맞는 업그레이드 가능
          • 게임기를 TV를 새로 살 필요 없이 교체 가능
  • 클래스 분리는 아래와 같은 네 가지 핵심 요소를 달성할 수 있다.
    • 코드 가독성 향상
    • 코드 모듈화
    • 코드 재사용성 및 일반화
    • 테스트 용이성 및 적절한 테스트

인터페이스 활용하기

  • 계층 사이를 뚜렷이 구분하고 구현 세부 사항이 계층 사이에 유출되지 않도록 하기 위해 사용할 수 있는 접근법
  • 어떤 함수를 외부로 노출할 것인지를 인터페이스를 통해 결정
  • 상위 계층은 인터페이스에 의존할 뿐 로직을 구현하는 구체적인 클래스에 의존하지 않는다.
  • 주어진 하위 문제에 대한 둘 이상의 서로 다른 구체적인 구현이 가능하고, 이들 구현 클래스 사이에 전환이 필요할 때는 추상화 계층을 나타내는 인터페이스를 정의한다.
  • 하나의 인터페이스 및 단일 구현이 발생할 수 있지만, 이에 대한 몇 가지 장점이 있다.
    • 퍼블릭 API를 매우 명확하게 보여준다.
      • 사용할 기능과 사용하지 말아야 할 기능에 대해 혼동할 일이 없다.
    • 한 가지 구현만 필요하다고 잘못 추측한 경우도 있다.
      • 확신을 가지더라도, 구현할 기능이 증가하면 잘못 된 추측을 한 경우가 있다.
    • 테스트를 쉽게 할 수 있다.
      • 구현 클래스가 복잡하거나 네트워크 I/O에 의존하는 작업을 수행하면 테스트 중 목이나 페이크 객체로 대체할 수 있다.
    • 같은 클래스로 두 가지 하위 문제를 해결할 수 있다.
  • 아래와 같은 단점도 가진다.
    • 더 많은 작업이 필요하며, 코드가 복잡해질 가능성이 있다.

층이 너무 얇은 구조

  • 코드를 별개로 계층으로 세분화하면 추가 비용이 발생한다.
    • 클래스를 정의하거나 의존성을 새 파일로 임포트하려고 반복적으로 사용하는 코드량이 증가한다.
      • boilerplate code
    • 로직의 이해를 위해 파일이나 클래스를 따라갈 때 더 많은 노력이 필요하다.
    • 인터페이스 뒤에 계층을 숨기게 되면 어떤 상황에서 어떤 구현이 사용되는지 파악하는데 더 많은 시간이 필요하다.
    • 이로 인해 로직을 이해하거나 디버깅하는 것이 더 어려워질 수 있다.
  • 서로 다른 계층으로 분할해서 얻는 장점과 비교하면 비용은 상당히 낮지만, 분할을 위한 분할은 의미 없다는 것을 알아야 한다.
    • 즉 불필요하게 계층을 쪼개는 것 역시 큰 비용이 든다.

마이크로 서비스?

  • 마이크로 서비스 아키텍처는 개발 문제에 대한 해결책을 단일 프로그램(컴파일 되는 라이브러리 수준)이 아닌, 독립적으로 실행되는 서비스로 배포된다.
  • 시스템이 여러 개의 소규모 프로그램으로 분할되어 실행된다.
  • 마이크로 서비스는 시스템을 분리하여 모듈화하는 좋은 방법이지만, 서비스를 구현하기 위해 올바른 추상화 및 코드 계층을 만드는 것이 여전히 중요하다는 사실은 변함이 없다,

요약

  • 코드를 깨끗하고 뚜렷한 추상화 계층으로 세분화하면 가독성, 모듈화, 재사용, 일반화 및 테스트 용이성이 향상된다.
  • 특정 언어에 국한된 기능뿐만 아니라 함수, 클래스 및 인터페이스를 사용하여 코드를 추상화 계층으로 나눌 수 있다.
  • 코드를 추상화 계층으로 분류하는 방법을 결정하려면 해결 중인 문제에 대한 판단과 지식을 사용해야 한다.
  • 너무 비대한 계층 때문에 발생하는 문제는 너무 얇은 계층 때문에 발생하는 문제보다 더 심각하다.
  • 확실하지 않는 경우에는 남용의 위험에도 불구하고 계층을 얇게 만드는 것이 좋다.