프로그래밍 책

추상화 계층을 나누는 방법 #좋은코드나쁜코드 #2장 #API #마이크로서비스

열심히 사는 우진 2023. 4. 4. 11:54
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 2장을 기반으로 작성된 글입니다.

 

 

 


추상화 계층을 만들면 복잡한 문제를 간단하게 해결할 수 있다.

다음은 HTTP 통신을 통해 서버에 메시지를 보내는 코드의 예시이다.

HttpConnection connection = HttpConnection.connect("http://example.com/server");
connection.send("Hello server");
connection.close();

높은 단계에서 바라보면 이것은 꽤 간단해 보이는데,

사실 이 과정에서는 문자열 직렬화, HTTP 프로토콜의 복잡한 동작, TCP 연결, 네트워크 연결 확인, 신호 변조, 데이터 전송 오류 수정 등의 복잡한 과정들이 일어난다.

하지만 HttpConnection 라이브러리(모듈)를 사용하는 입장에서는 추상화된 개념만 이해하면 될 뿐이다.

이를 통해 코드 품질의 4가지 핵심 요소를 달성할 수 있다.

  • 가독성 : 추상화된 개념만 이해하면 된다.
  • 모듈화 : 추상화된 모듈을 다르게 구현된 모듈로 쉽게 변경할 수 있다.
  • 재사용성 및 일반화성 : 간결하게 추상화된 계층은 재사용하기 쉽고, 다른 상황에서 일반화되기 쉽다.
  • 테스트 용이성 : 간결하게 나눠진 부분에 대해서만 완벽하게 테스트하면 된다.

추상화 계층은 다음의 단위로 코드를 분할하고 의존하게 함으로써 나눌 수 있다.

  • 함수(메서드)
  • 클래스(및 구조체나 믹스인과 같이 클래스와 비슷한 요소도 가능)
  • 인터페이스
  • 패키지, 네임스페이스, 모듈

API, 함수

API는 인자, 반환 타입, 메서드명에 대해서만 공개하고, 세부 구현은 숨긴다.

API로 노출함으로써 추상화 계층을 유지할 수 있다.

함수가 너무 많은 일을 한다면, 작은 함수로 나눌 수 있다.

클래스

클래스의 추상화 기준에 대한 다양한 이론과 경험 법칙이 있다.

  • 줄 수 : 코드 300줄 이하 같은 기준은 대체로 맞지만, 상황에 따라 다를 수 있어 정확하지 않다.
  • 응집력
    • 순차적 응집력 : 한 요소의 출력이 다른 요소에 대한 입력으로 필요하다면 순차적인 응집력이 있다고 볼 수 있다.
      • ex: 커피 만들기(원두를 갈아서 → 추출)
    • 기능적 응집력 : 하나의 일을 성취하는 데 기여하느닞
  • 관심사의 분리 : 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다
    • 게임 콘솔은 TV와 독립적이다.

응집력을 고려하고 관심사의 분리가 적절하게 이루어졌다면,

코드 가독성이 좋아지고,

코드가 모듈화 되고 상호작용을 몇 가지 퍼블릭 함수를 통해서만 이루어져 구현 클래스 교체가 쉬워지고,

재사용과 일반화과 쉬워지고,

테스트가 쉬워진다.

텍스트 요약 클래스 예시

TextSummarizer 라는 클래스는,

  • 텍스트를 단락으로 분할
  • 텍스트 문자열의 중요도 점수 계산
  • 중요도에 따라 텍스트 요약

위와 같은 책임을 가진다.

이는 코드 품질의 4가지 핵심 요소에 반한다.

단락을 분할하는 ParagraphFinder와, 문자열의 중요도를 계산하는 TextImportanceScorer를 분리하고,

TextSummarizer에 의존성을 주입시킨다.

class TextSummarizer {
    private final ParagraphFinder paragraphFinder;
    private final TextImportanceScorer importanceScorer;

...
}

이를 통해 가독성이 좋고, 모듈화 되고, 재사용 일반화가 쉽고, 테스트가 쉬운 코드가 될 수 있다.

인터페이스

위의 예시에서 TextImportanceScorer의 로직을 바꾸어, 기계학습 모델링을 적용하고 싶다고 하자.

이럴 때는, TextImportanceScorer 클래스를 인터페이스로 추출한 다음,

이 문제(문자열 중요도 점수 계산)를 해결하는 각각의 방식을 서로 다른 클래스로 구현하며 된다.

WordBasedScorer / ModelBasedScorer

코드를 작성하다 보면, 주어진 추상화 계층에 한 가지 구현만 있고, 향후에 다른 구현을 추가할 계획이 없는 경우가 있다.

그럼에도 인터페이스를 통해 추상화 계층을 표현하면 몇몇 이점이 있다. (이 경우 상위 계층은 인터페이스에 의존할 뿐, 구현 클래스에 직접 의존하지 않는다.)

  • 퍼블릭 API를 명확하게 알 수 있다 (인터페이스를 보고)
  • 나중에 구현이 추가될 가능성이 있다
  • 테스트가 쉽다 (목, 페이크로 대체 가능)
  • 같은 클래스로 두 가지 하위 문제를 해결할 수 있다.

물론 작업이 더 필요하고, 코드가 복잡해질 수 있다는 단점도 있다.

층이 너무 얇아질 때

무리하게 계층을 나누면, 계층을 나누기 위해서만 계층을 나누는 무의미한 일이 될 수 있다.

하지만, 나눌지 말지 헷갈리는 경우에는 대부분 나누는 것이 더 좋다.

안 나눴을 때의 단점이 더 크기 때문.

마이크로서비스에 대해서는 어떤가?

개별 문제에 대한 해결책을, 독립적으로 실행되는 서비스로 배포하는 구조를 MSA(MicroService Architecture)라고 한다.

예를 들어, 온라인 소매업체에서 재고를 확인하고 수정하는 마이크로서비스를 개발할 수 있겠다.

이 경우, MSA 자체가 시스템을 분리하고 모듈화하지만,

서비스 구현을 위해 해결하는 하위 문제들을 추상화할 필요성은 변하지 않는다!

반응형