기타

[개발 서적] 좋은코드 나쁜코드 2장 - 추상화 계층

베르_최성훈 2023. 8. 12. 14:13

책 요약 및 리뷰

리뷰는 개인적인 생각 및 의견으로 파란색으로 남기겠습니다.

들어가기 전에

  코드 작성의 목적은 문제 해결이다. 우리는 보통 상위 수준의 문제를 하위 수준의 문제들로 나누어 해결한다. 하위 수준의 문제를 해결하는 방법도 중요하지만 그 코드를 어떻게 구성하는가도 중요하다.

 

  코드를 구성하는 방법은 코드 품질의 기본적인 측면 중 하나이다. 코드를 잘 구성한다는 것은 간결한 추상화 계층을 만드는 것으로 귀결된다. 문제를 추상화 계층으로 나누고 어떻게 코드를 구성하는지, 그 효과로 가독성, 모듈성, 재사용성, 일반화성, 테스트 용이성이 개선되는지 확인해보자.

 

-> 이 책에서는 계속해서 가독성, 모듈성, 재사용성, 일반화성, 테스트 용이성을 강조하고 있다. 좋은 코드를 만들기 위해서는 개발자가 유념해야하는 기본 요소로 판단된다.

2.1 널값 및 의사코드 규약

값이 없다를 표현하는 null 은 유용하면서도 문제가 많다.

  • 값이 제공되지 않거나 함수가 원하는 결과를 반환할 수 없을 경우 값이 없다, 부재한다의 개념이 유용하다.
  • 그러나 Nullable, NonNullable 이 항상 명백한 것이 아니라 문제가 발생한다.
  • 개발자들이 널 Check 하는 것을 자주 까먹는다.

다행히 최근 등장한 중요 언어들 대부분은 null safety 를 지원한다. 최신 C# 이나 Java 에서도 선택적으로 사용할 수 있다. (Java 의 Optional wrapper class)

 

-> Kotlin 은 Nullable type 을 지원해서 컴파일러가 컴파일 시점에 Nullable 한지 체크한다. Nullable 하다면 널체크 하지 않고 사용할 수 없도록 강제한다.

 

  Java 또한 Optional 이 존재하지만 이는 문법 자체를 수정한 것이 아니기 때문에 Optional 자체가 null 이 될 수 있다.

그럼 자바는 왜 코틀린 처럼 Nullable type 을 지원하지 않을까?

 

  자바가 Nullable type 을 지원하지 않는 이유는 문법을 바꾸는 것이 기존 자바 개발자들에게 혼란을 줄 수 있기 때문으로 컴파일러가 아닌 개발자 역량에 맡기는 선택을 했다.

2.2 왜 추상화 계층을 만드는가?

코드 작성은 큰 문제를 작은 문제로 세분화하는 과정

 

예시) 서버로 메세지 보내기
HttpConnection connection = HttpConnection.connect("http://example.com/server")
connection.send("Hello server")
connection.close();

 

 서버로 메세지 보내야 한다는 상위 수준의 문제를 해결하기 위해선 엄청나게 복잡한 과정이 있다. 하지만 하위 수준의 문제로 쪼개고 추상하하여 최상위에선 HTTP 프로토콜이 어떻게 구현되어 있는지 알 필요도 없게 만들었다.

 

 이렇게 추상적인 개념으로 만든 것을 추상화 계층(layers of abstraction)이라고 한다. 어떤 문제를 하위 문제로 계속 나누어 내려가면서 추상화 계층을 만든다면 개별 코드는 별로 복잡하지 않게 만들 수 있다. 즉, 문제가 엄청나게 복잡할지라도 하위 문제들을 식별하고 올바른 추상화 계층을 만듦으로써 복잡한 문제를 해결할 수 있다.

 

-> 복잡하고 구체적인 작업의 내용을 알지 못하더라도 함수 하나를 호출해서 실행 가능하다. 인터페이스, 추상클래스를 쓰지 않고 단순히 함수를 분리하는 것도 추상화이자 모듈화이다.

효과 요약

  • 가독성 : 코드베이스에 있는 코드의 모든 세부사항을 알긴 어렵지만 추상화 계층을 이해하고 사용하긴 쉽다. 한 번에 다룰 개념이 줄어들어 가독성이 향상된다.
  • 모듈화 : 하위 문제에대한 해결책을 깔끔하게 나누고 구현이 외부로 노출되지 않도록 보장할 때 다른 계층에 영향을 미치지 않고 구현을 변경하기 매우 쉬워진다.
  • 재사용성 및 일반화성 : 추상화 계층으로 제시된 하위 문제의 해결책은 재사용하기 쉬워진다.
  • 테스트 용이성 : 신뢰할 수 있는 코드를 작성하고자 한다면 각 하위 문제에 대한 해결책이 제대로 작동하는지 확인해야 한다. 하위 문제에대한 해결책을 완벽하게 테스트하는 것이 훨씬 쉬워진다.

 

코드의 계층

추상화 계층을 생성하기 위해선 의존 관계를 보여주는 의존성 그래프를 생성해야 한다. 다음과 같은 요소를 사용한다.

  • 함수
  • 클래스
  • 인터페이스
  • 패키지, 네임스페이스, 모듈

API 및 구현 세부사항

  API 는 서비스를 사용할 때 알아야하는 개념은 형식화하고, 서비스의 모든 구현 세부 사항은 이 API 뒤에 감춘다.

클래스, 인터페이스, 함수들을 API 노출이라고 볼 수 있다.

함수

  함수명을 짓기 어렵거나 어색하면 함수가 너무 길다는 것을 의미하며 작은 함수로 나누는 것이 유익하다. 작게 나누어진 함수는 재사용성과 가독성이 높아진다.

클래스

이상적인 클래스 나누기

  • 줄 수 : 한 클래스는 300줄을 넘지 않아야한다. 너무 많은 개념을 다루지 않게 하기위한 경험칙이다.
  • 응집력 : 좋은 클래스는 매우 응집력이 강하다. 요소들이 얼마나 잘 속해있는가
    • 순차적 응집력(일의 수행 단계가 모여있음), 기능적 응집력(일의 필요 요소가 모여있음)
  • 관심사의 분리: 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다는 설계 원칙

응집력과 관심사 분리는 주관적이기 때문에 경험으로 결정해야한다.

 

-> 줄 수는 어떤 개발자냐에 따라서 달라지는 말 그대로 경험칙이다. 따라서 응집력, 관심사의 분리에 따라서 나누는 것이 중요하다. 관심사 분리를 잘하게 된다면 다른 것은 신경쓰지 않아도 어느정도 잘 나눠진다고 생각한다.

 

  • 가독성 : 단일 클래스에 많은 내용이 담겨있을 수록 가독성이 저하된다.
  • 모듈화 : 다른 클래스와의 상호작용이 퍼블릭 함수를 통해서만 이뤄진다면 구현을 다른 클래스로 교체할 필요가 있을 때 쉬워진다.
  • 재사용성 및 일반화 : 다른 누군가가 나중에 같은 하위 문제를 해결해야할 가능성이 높다.
  • 테스트 용이성 및 적절한 테스트 : 여러 클래스로 나누면 각 부분을 적절하게 테스트하기 쉬워진다.

잘 분리되지 않은 클래스는 어떻게 개선할 수 있을까? 여러 단락으로 나누어 의존성 주입 해주면 된다. 8장에서 다룰 예정

코드를 적절한 크기로 세분화 하는 것은 추상화 계층을 잘 만들기 위한 가장 효과적인 도구이므로 시간과 노력을 들일 가치가 있다.

인터페이스

  구현 세부 사항이 계층 사이에 유출되지 않도록 하기 위해 외부로 노출할지 결정하고 사용할 수 있다. 위에 있는 계층이 인터페이스에 의존할 뿐 클래스에 의존하지 않도록 만들 수 있다.

 

  하나의 추상화 계층에 대해 두 가지 이상의 다른 방식으로 구현하거나 향후 변경 가능성이 있다면 인터페이스를 정의하는 것이 좋다.

주어진 추상화 계층에 대해 한 가지 구현만 있고 향후에 다른 구현을 추가할 계획이 없더라도 여전히 인터페이스를 통해 추상화 계층을 표현해야 하는가는 여러분과 여러분의 팀이 결정할 사안이다. 구현이 단 하나만 있고 향후 다르게 구현할 필요가 있을지 현재는 알 수 없어도 몇가지 장점을 가진다.

 

장점

  • 퍼블릭 API 를 매우 명확히 보여준다.
  • 한 가지 구현만 필요하다고 잘못 추측한 것일 수 있다.
  • 테스트를 쉽게 할 수 있다.
  • 같은 클래스로 두 가지 하위 문제를 해결할 수 있다. 한개의 구현 클래스가 두개의 인터페이스 구현체로 사용될 때

단점

  • 더 많은 작업
  • 복잡한 코드

-> 현재 Repository 구현체나 DataSource 등 하나의 구현체만 있음에도 인터페이스로 추상화하고 의존성을 외부에서 주입하고 있다. 이는 변화에 유연하게 하며 또한 Fake 객체나 Mockking 으로 테스트하기도 쉽게 만든다. 

 

지만 마찬가지로 단점이 명확하다. 작업이 많음과 동시에 너무 많은 파일이 혼란스럽게 만든다.

 

  극단적으로 모든 클래스에 인터페이스를 붙이는 것은 통제가 불가능하고 복잡해지며 이해와 수정이 어렵다. 그 장점이 확실한 경우에 인터페이스를 사용하라. 또한 일반적으로 클래스를 작성하거나 수정할 때 추후 인터페이스를 붙이는 것이 어렵지않도록 코드를 작성해야 한다.

층이 너무 얇아질 때

  코드 계층의 규모를 올바르게 결정하는 것이 중요하다. 코드를 서로 다른 계층으로 분할해서 얻는 장접과 비교하면 이 비용이 상당히 낮은 것이지만 분할을 위한 분할은 의미가 없다. 비용이 이익보다 더 큰 시점이 올 수 있으므로 상식에 맞게 적용하라.

 

그럼에도 너무 많은 계층을 남용하는 것이 한 계층에 모든 코드를 집어넣는 것보다는 낫다.

 

자신이 만든 계층이 코드의 가독성, 재사용성, 일반화, 테스트 용이성을 높이는지 스스로 판단하는게 최선의 조언이다.

 

마치며

  페어 프로그래밍, 팀 프로젝트를 하면서 나와 다른 의견을 가진 사람들을 많이 만났다. 각자 추상화 계층을 어디까지 나눌 것인가 허상에 불과하다라는 사람도 많고 TDD 가 아닌 코드를 다 짠 뒤에 하는 테스트는 의미가 없다라고 얘기하는 사람도 있다. 

 

  물론 개발은 정답은 없기에 누가 맞다고 할 수 없다. 다만 코드를 짤 때 가독성, 재사용성, 일반화, 테스트 용이성을 높이는지를 생각해야 한다. 각각의 요소는 주관적인 판단일지라도  주관적인 판단도 하지 않는 사람과는 달라질 것이다.

 

코드 한 줄에 이유가 있는 개발자가 되자.