1장
주제 : 고품질의 코드란 무엇인가
디자인 패턴은 언제 적용해야 할까?
p. 2
속도와 확장성을 맹목적으로 추구하는 현재의 개발 환경에서 안타깝게도 대다수의 소프트웨어 엔지니어는 고품질의 코드를 작성하는 방법에 대해 생각할 시간이 많지 않은 것도 사실이다.
p. 16
개발 초기 단계에서 필요한 경우를 제외하고는 복잡한 디자인 패턴을 과도하게 설계하여 적용하면 안 되지만, ...
p. 20
따라서 잘못된 요구 사항 예측으로 인한 과도한 설계를 피하기 위해 지속적인 리팩터링 개발 방법을 권장한다.
항상 고민되는 부분이다.
좋은 코드를 작성하기 위해 설계에 신경 쓰고 싶지만, 현실적으로는 단순한 코드로도 충분히 해결할 수 있는 비즈니스 문제를 다루는 경우가 많다. 따라서 무조건 복잡한 설계를 적용하는 것은 오히려 불필요한 부담이 될 수 있다.
그러나 이런 방식으로 개발을 이어가다 보면 어느 순간 예상치 못하게 코드가 복잡해지고, 유지보수가 어려워지는 상황에 직면하게 된다. 여기서 중요한 것은 단순한 코드를 작성하는 것과 단순한 사고방식으로 개발하는 것은 전혀 다른 결과를 초래한다는 점이다. 단순함을 추구한다고 해서 설계 원칙까지 단순하게 가져가서는 안 된다.
아무리 간결하게 개발하더라도 시간이 지나면서 코드 중복이 발생하는 것은 피할 수 없다. 그렇기에 처음부터 과도한 설계를 적용하기보다는, 실제로 코드 중복이 늘어나고 유지보수가 어려워지는 순간을 포착하여 디자인 패턴을 적용하는 것이 더 현실적인 접근일 것이다. 즉, 처음부터 복잡한 구조를 강요하는 것이 아니라, 코드의 진화 과정에서 필요할 때 적절한 리팩터링을 수행하는 것이 바람직한 방향이라고 생각한다.
좋은 코드의 기본 원
p. 9
코드가 명확하게 계층화되어 있으며, 높은 모듈성, 높은 응집도와 낮은 결합도를 가지고 구현보다 인터페이스 기반의 설계 원칙을 고수한다면 코드의 유지 보수가 쉽다는 의미일 수 있다.
p. 10
우리는 코드의 명명이 의미가 있는지, 주석이 자세히 기술되어 있는지, 함수 길이는 적절한지, 모듈 구분이 명확한지, 코드가 높은 응집도와 낮은 결합도를 가지는지 등을 모두 확인해야 한다.
p. 10
코드의 확장성은 코드를 작성할 때 새로운 기능을 추가할 수 있는 여지가 설계 당시부터 고려되어 있어 확장용 인터페이스가 이미 존재함을 의미하며, ...
p. 19
디자인 패턴을 적용하는 목적은 디커플링, 즉 더 나은 코드 구조를 사용하여 단일 책임을 위해 큰 코드 조각을 작은 클래스로 분할하여 코드가 높은 응집도와 낮은 결합도의 특성을 충족하도록 하는 것이다.
1. 좋은 코드의 기본 원칙: 단일 책임 원칙(SRP)
좋은 코드에서는 하나의 클래스가 단일 책임을 가져야 하며, 해당 책임은 그 클래스에서만 관리되어야 한다. 즉, 하나의 클래스가 여러 역할을 수행하는 것이 아니라, 특정한 역할만 담당하도록 설계하는 것이 중요하다.
2. 비즈니스 로직 중심의 계층화된 설계
코드에서 가장 중요한 요소는 비즈니스 로직이므로, 이를 명확하게 관리하기 위해 계층화된 소프트웨어 구조를 적용한다. 이를 통해 비즈니스 로직과 UI, 데이터 접근 등을 분리하고, 유지보수성과 확장성을 높일 수 있다.
3. 모듈 간 의존성을 줄이는 방식: 위임과 인터페이스
하나의 모듈에서 필요한 데이터가 있다면, 직접 다른 계층으로 접근하여 계산하는 것이 아니라, 해당 데이터를 책임지는 모듈에 요청하여 제공받는 방식이 더 적절하다. 이를 통해 결합도를 낮추고 응집도를 높일 수 있으며, 변경이 필요한 경우에도 영향 범위를 최소화할 수 있다.
4. 인터페이스의 장점: 테스트 용이성과 확장성
모듈 간에는 인터페이스를 두어 결합도를 낮출 수 있다. 인터페이스를 사용하면 시스템의 복잡성이 다소 증가할 수 있지만, 테스트의 용이성을 높이고, 향후 모듈을 확장할 때 유연성을 제공하는 장점이 있다. 즉, 인터페이스를 활용하면 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있는 구조를 만들 수 있다.
5. 명확한 코드 작성을 위한 네이밍과 컨벤션
좋은 코드에는 코드 원칙(컨벤션)의 개념도 필요하다. 코드가 직관적으로 이해될 수 있도록 하기 위해서는 클래스명, 메서드명, 변수명 등을 명확하게 작성하는 것이 중요하다. 의미 있는 명명을 통해 주석 없이도 코드의 의도를 파악할 수 있어야 하며, 이를 위해 코드 스타일 가이드를 준수하는 것이 바람직하다.
2.1 ~ 2.3장
주제 : 객체지향과 객체지향 분석 및 설계
Service Interface 사용 여부 결정에 대한 접근 방식
p. 49
저장 방법을 캡슐화하기 위해 CredentialStorage를 인터페이스로 설계하고 프로그래밍한다.
Service Interface를 사용하는 것에 대한 찬반이 크게 나뉘는 것 같다.
일부는 대부분의 경우 구현체가 하나뿐이고 가까운 미래에 변경될 가능성이 낮으며, 코드 가독성이 저하되는 등의 이유로 이를 반대한다.
일부는 시스템의 결합도를 낮추는 데 도움이 되고 테스트 용이성이 향상되며, 추후 확장성과 유연성을 가지게 될 수 있으므로 이를 찬성한다.
아래와 같은 균형 잡힌 접근 방식으로 접근해도 좋을 것 같다.
실용적인 인터페이스 사용 기준
모든 서비스에 무조건 인터페이스를 적용하기보다, 다음과 같은 경우에 인터페이스 도입을 고려한다.
- 서비스가 여러 구현체를 가질 가능성이 있는 경우
- 테스트하기 어려운 외부 의존성(데이터베이스, 외부 API 등)과 상호작용하는 경우
- 프레임워크에서 인터페이스를 필요로 하는 경우
- 공개 API나 플러그인 아키텍처를 설계하는 경우
Deep Dive (Python Duck-Typing)
기존 추상 베이스 클래스(ABCs)의 한계
PEP 484와 typing 모듈은 Iterable이나 Sized와 같은 여러 파이썬 프로토콜에 대한 추상 베이스 클래스를 정의하고 있습니다. 그러나 이러한 ABC를 사용하려면 클래스가 명시적으로 이를 상속하거나 등록해야 합니다. 이는 동적 타이핑을 지향하는 파이썬의 관습에 어긋나며, 일반적인 파이썬 코드 스타일과도 맞지 않습니다. 예를 들어, 아래 코드는 PEP 484를 준수합니다:
from typing import Sized, Iterable, Iterator
class Bucket(Sized, Iterable[int]):
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...
구조적 서브타이핑을 통한 문제 해결
이 PEP의 목적은 클래스 정의에서 명시적인 베이스 클래스를 사용하지 않고도 위의 코드를 작성할 수 있도록 하는 것입니다. 이를 통해 Bucket 클래스는 구조적 서브타이핑을 사용하는 정적 타입 검사기에서 Sized와 Iterable[int]의 암묵적 서브타입으로 간주될 수 있습니다:
from typing import Iterator, Iterable
class Bucket:
...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[int]: ...
def collect(items: Iterable[int]) -> int: ...
result: int = collect(Bucket()) # 타입 검사 통과
명명적 서브타이핑(Nominal Subtyping) vs. 구조적 서브타이핑(Structural Subtyping)
구조적 서브타이핑은 파이썬 개발자들에게 자연스러운 개념인데, 이는 런타임에서 덕 타이핑(duck typing)의 동작 방식과 일치하기 때문이다. 즉, 객체가 특정한 속성을 가지고 있으면, 해당 객체의 실제 런타임 클래스와 관계없이 특정한 방식으로 다뤄질 수 있다. 그러나 PEP 483에서 논의된 것처럼, 명명적 서브타이핑(nominal subtyping)과 구조적 서브타이핑(structural subtyping)에는 각각 장점과 단점이 존재한다.
따라서, 이 PEP에서는 PEP 484에서 설명된 명명적 서브타이핑을 구조적 서브타이핑으로 완전히 대체할 것을 제안하지 않는다. 대신, 이 PEP에서 정의하는 프로토콜 클래스는 기존의 일반 클래스와 상호 보완적인 역할을 하며, 개발자가 특정한 상황에서 어떤 해결책을 적용할지 선택할 수 있도록 한다. PEP의 마지막 부분에 있는 "거부된 아이디어(Rejected Ideas)" 섹션에서 이에 대한 추가적인 설명을 확인할 수 있다.
비목표(Non-goals)
런타임에서는 프로토콜 클래스가 단순한 ABC(추상 베이스 클래스, Abstract Base Classes)로 동작한다. 프로토콜 클래스에 대해 복잡한 런타임 인스턴스 및 클래스 검사를 제공할 의도는 없다. 이러한 검사는 구현이 어렵고 오류를 유발할 가능성이 크며, PEP 484의 설계 원칙과도 충돌하기 때문이다.
또한, PEP 484 및 PEP 526의 원칙을 따라, 프로토콜은 완전히 선택적인(optional) 기능으로 제공된다.
- 프로토콜 클래스로 주석(annotations)된 변수나 매개변수에 대해 런타임에서 어떠한 의미도 부여하지 않는다.
- 검사는 서드파티 타입 검사기(third-party type checkers)나 기타 도구를 통해서만 수행된다.
- 타입 어노테이션을 사용하는 개발자라도 반드시 프로토콜을 사용할 필요는 없다.
- 미래에도 프로토콜을 필수적으로 사용하도록 강제할 계획은 없다.
즉, 이 PEP의 목표는 프로토콜 클래스에 대한 복잡한 런타임 동작을 제공하는 것이 아니라, 정적 구조적 서브타이핑(static structural subtyping)에 대한 지원과 표준을 마련하는 것이다. 프로토콜을 ABC로 사용할 수 있는 가능성은 부차적인 이점일 뿐이며, 이는 기존에 ABC를 사용하고 있던 프로젝트가 원활하게 전환할 수 있도록 하기 위한 것이다.
PEP 544 – Protocols: Structural subtyping (static duck typing) | peps.python.org
Type hints introduced in PEP 484 can be used to specify type metadata for static type checkers and other third party tools. However, PEP 484 only specifies the semantics of nominal subtyping. In this PEP we specify static and runtime semantics of protoc...
peps.python.org
얘기 나누고 싶은 내용
기획서의 구체성은 도메인과 규모에 따라 달라진다
p. 41
앞에서 프로젝트 관리자가 제시한 요구 사항은 다소 모호하고 너무 일반적인 내용으로만 채워져있어, 설계 및 구현을 할 수 있을 정도로 상세하지 않다.
프로젝트의 기획서가 얼마나 구체적인지는 도메인의 특성에 따라 다를 수 있다. 특히, 시스템을 사용하는 집단이 소규모인지, 대규모인지에 따라 요구 사항의 명확성이 달라질 가능성이 크다.
소규모 집단을 위한 시스템
소규모 집단을 위한 시스템에서는 사용자들이 높은 자유도를 원할 가능성이 크다. 이들은 시스템을 본인들만 사용할 것이므로, 명확한 요구 사항을 정해두기보다는 필요에 따라 변화를 원할 수 있다.
하지만 이런 경우, 기획서 자체가 모호하거나, 사용자들도 자신들의 요구 사항을 정확히 정의하지 못하는 경우가 많다. 결과적으로, 개발이 완료된 후 테스트 과정에서 전면적인 수정 요청이 발생할 가능성이 높아진다.
그렇다면 이런 상황에서 개발자의 역할은 어디까지인가? 단순히 요구 사항을 구현하는 역할을 넘어, 사용자의 요구를 구체화하는 과정에 적극적으로 참여해야 하는 것인가?
'독서' 카테고리의 다른 글
[이펙티브 코틀린] 8장. 효율적인 컬렉션 처리 (0) | 2024.08.11 |
---|---|
[이펙티브 코틀린] 7장. 비용 줄이기 (0) | 2024.08.04 |
[이펙티브 코틀린] 6장. 클래스 설계 (0) | 2024.07.28 |
[이펙티브 코틀린] 5장. 객체 생성 (1) | 2024.07.21 |
[이펙티브 코틀린] 4장. 추상화 설계 (5) | 2024.07.15 |