2장 추상화 계층
널값 및 의사코드 규약
- 널값에 대한 규약을 지정하지 않으면, NullPointerException, NullRefrenceException, Cannot read property of null과 같은 에러를 발생할 수 있다.
- 이를 위해 널 안전성과 보이드 안전성에 대한 개념이 생겼다.
- 자바나 c++에서는 옵셔널 타입을 통해서 널 안전성을 보장한다.
추상화 계층 및 코드 품질의 핵심 요소
- 가독성
- 모든 세부 사항을 이해하는 것은 불가능하지만, 높은 계층의 추상화를 이해하고 사용하기 쉽게 해야한다.
- 모듈화
- 구현 세부 사항이 외부로 노출되지 않도록 보장할 때, 다른 계층이나 코드의 일부에 영향을 미치지 않고 계층 내에서만 구현을 변경하기가 매우 쉬워진다.
- 재사용성 및 일반화성
- 테스트 용이성
코드의 계층
- 추상화 계층을 생성하는 방법은 코드를 서로 다른 단위로 분할하여 단위 간 의존 관계를 보여주는 의존성 그래프를 생성하는 것이다
- 아래와 같은 언어 요소를 사용한다
- 함수
- 클래스 (및 구조체나 믹스인과 같이 클래스와 비슷한 요소도 가능)
- 인터페이스(또는 이와 동일한 요소)
- 패키지, 네임스페이스, 모듈
API 및 구현 세부 사항
- 코드를 작성할 때 고려해야 할 측면이 두 가지 있다.
- 코드를 호출할 때 볼 수 있는 내용
- 퍼블릭 클래스, 인터페이스 및 함수(메서드)
- 이름, 입력 매개변수 및 반환 유형이 표현하고자 하는 개념
- 코드 호출 시 코드를 올바르게 사용하기 위해 알아야 하는 추가 정보
- 코드를 호출할 때 볼 수 없는 내용
- 구현 세부 사항
- 코드를 호출할 때 볼 수 있는 내용
- 서비스를 구축한 경험이 있다면 애플리케이션 프로그래밍 인터페이스라는 용어를 알게 될 것이다.
- 서비스를 사용할 때 알아야 할 것들에 대한 개념을 형식화
- 서비스의 모든 구현 세부 사항을 이 API 뒤에 감춤
- 미니 API를 노출하는 것
- 구현 세부 사항으로는 프라이빗 함수 & 변수와 클래스가 의존하는 라이브러리가 있다.
- 함수가 퍼블릭이어도 함수 내의 코드는 구현 세부 사항에 포함된다.
- 코드를 작성하거나 수정할 때, API에 수정 사항에 대한 구현 세부 정보가 새어 나간다면 추상화 계층이 명확하게 구분 된 것은 아니다. (입력 매개변수, 반환 유형, 퍼블릭 함수 등을 통해)
함수를 작성하기 위한 좋은 전략
- 로직을 함수로 구현하는 것은 매우 유용하다.
- 함수 명명 규칙에는 중요한 두 가지 사항이 있다.
- 단일 업무 수행
- 잘 명명된 다른 함수를 호출해서 더 복잡한 동작 구성
- 코드의 가독성과 재사용성을 높여준다.
- 함수로 표현하기 어렵게 구현되었다면, 잘 명명된 헬퍼 함수로 분리하는 것을 고려한다.
- 헬퍼 함수 : 사용자 정의 함수이며, 코드의 여러 위치에서 사용할 수 있다.
클래스 분리하기
- 클래스를 분리하기 위해서 아래와 같은 많은 이론과 경험 법칙을 제시할 수 있다.
- 줄 수
- 코드는 300줄을 넘어야하지 않는다 등의 number of lines 규칙
- 실제로 사용하기는 상당히 제한적
- 응집력
- 순차적 응집력 : 한 요소의 출력이 다른 요소의 입력으로 필요할 때 사용 된다.
- 원두를 간다 → 커피를 내린다
- 기능적 응집력 : 몇 가지 요소들이 모여서 하나의 일을 성취하는데 기여
- 주관적일 수도 있지만, 케이크를 만들기 위해 요소들을 요구하는 것을 뜻한다. (케이크 통, 장비, 그릇 등..)
- 순차적 응집력 : 한 요소의 출력이 다른 요소의 입력으로 필요할 때 사용 된다.
- 관심사 분리
- 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다고 주장하는 설계 원칙
- 게임 콘솔로 예를 들 수 있다.
- TV와 게임 콘솔을 분리
- 각 분리에 맞는 업그레이드 가능
- 게임기를 TV를 새로 살 필요 없이 교체 가능
- 게임 콘솔로 예를 들 수 있다.
- 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다고 주장하는 설계 원칙
- 줄 수
- 클래스 분리는 아래와 같은 네 가지 핵심 요소를 달성할 수 있다.
- 코드 가독성 향상
- 코드 모듈화
- 코드 재사용성 및 일반화
- 테스트 용이성 및 적절한 테스트
인터페이스 활용하기
- 계층 사이를 뚜렷이 구분하고 구현 세부 사항이 계층 사이에 유출되지 않도록 하기 위해 사용할 수 있는 접근법
- 어떤 함수를 외부로 노출할 것인지를 인터페이스를 통해 결정
- 상위 계층은 인터페이스에 의존할 뿐 로직을 구현하는 구체적인 클래스에 의존하지 않는다.
- 주어진 하위 문제에 대한 둘 이상의 서로 다른 구체적인 구현이 가능하고, 이들 구현 클래스 사이에 전환이 필요할 때는 추상화 계층을 나타내는 인터페이스를 정의한다.
- 하나의 인터페이스 및 단일 구현이 발생할 수 있지만, 이에 대한 몇 가지 장점이 있다.
- 퍼블릭 API를 매우 명확하게 보여준다.
- 사용할 기능과 사용하지 말아야 할 기능에 대해 혼동할 일이 없다.
- 한 가지 구현만 필요하다고 잘못 추측한 경우도 있다.
- 확신을 가지더라도, 구현할 기능이 증가하면 잘못 된 추측을 한 경우가 있다.
- 테스트를 쉽게 할 수 있다.
- 구현 클래스가 복잡하거나 네트워크 I/O에 의존하는 작업을 수행하면 테스트 중 목이나 페이크 객체로 대체할 수 있다.
- 같은 클래스로 두 가지 하위 문제를 해결할 수 있다.
- 퍼블릭 API를 매우 명확하게 보여준다.
- 아래와 같은 단점도 가진다.
- 더 많은 작업이 필요하며, 코드가 복잡해질 가능성이 있다.
층이 너무 얇은 구조
- 코드를 별개로 계층으로 세분화하면 추가 비용이 발생한다.
- 클래스를 정의하거나 의존성을 새 파일로 임포트하려고 반복적으로 사용하는 코드량이 증가한다.
- boilerplate code
- 로직의 이해를 위해 파일이나 클래스를 따라갈 때 더 많은 노력이 필요하다.
- 인터페이스 뒤에 계층을 숨기게 되면 어떤 상황에서 어떤 구현이 사용되는지 파악하는데 더 많은 시간이 필요하다.
- 이로 인해 로직을 이해하거나 디버깅하는 것이 더 어려워질 수 있다.
- 클래스를 정의하거나 의존성을 새 파일로 임포트하려고 반복적으로 사용하는 코드량이 증가한다.
- 서로 다른 계층으로 분할해서 얻는 장점과 비교하면 비용은 상당히 낮지만, 분할을 위한 분할은 의미 없다는 것을 알아야 한다.
- 즉 불필요하게 계층을 쪼개는 것 역시 큰 비용이 든다.
마이크로 서비스?
- 마이크로 서비스 아키텍처는 개발 문제에 대한 해결책을 단일 프로그램(컴파일 되는 라이브러리 수준)이 아닌, 독립적으로 실행되는 서비스로 배포된다.
- 시스템이 여러 개의 소규모 프로그램으로 분할되어 실행된다.
- 마이크로 서비스는 시스템을 분리하여 모듈화하는 좋은 방법이지만, 서비스를 구현하기 위해 올바른 추상화 및 코드 계층을 만드는 것이 여전히 중요하다는 사실은 변함이 없다,
요약
- 코드를 깨끗하고 뚜렷한 추상화 계층으로 세분화하면 가독성, 모듈화, 재사용, 일반화 및 테스트 용이성이 향상된다.
- 특정 언어에 국한된 기능뿐만 아니라 함수, 클래스 및 인터페이스를 사용하여 코드를 추상화 계층으로 나눌 수 있다.
- 코드를 추상화 계층으로 분류하는 방법을 결정하려면 해결 중인 문제에 대한 판단과 지식을 사용해야 한다.
- 너무 비대한 계층 때문에 발생하는 문제는 너무 얇은 계층 때문에 발생하는 문제보다 더 심각하다.
- 확실하지 않는 경우에는 남용의 위험에도 불구하고 계층을 얇게 만드는 것이 좋다.