10장 - 클래스

  1. 클래스의 체계
  2. 클래스는 작아야 한다. (높은 응집도)
  3. 변경으로부터 격리 (낮은 결합도)

클래스 체계

클래스 정의하는 방식

class Person {
    // public 변수
    job: number;

    // private 변수
    #name: string;

    // constructor, 생성자
    constructor() {}

    // public 메서드
    introduce() {
        console.log(getIntroString());
    }

    // public 메서드에서 사용하는 비공개 메서드
    private getIntroString() {
        return `My name is ${this.#name}. I'm ${job}`;
    }
}
  1. 변수 - 메서드 순으로 작성한다.
  2. public - private 순으로 작성한다.
  3. private 메서드는 사용되는 public 메서드 바로 다음에 작성한다. (하나의 아티클과 같이 읽을 수 있도록)

캡슐화: 캡슐화는 객체 지향 프로그래밍에서 2가지 측면이 있다. 객체의 속성과 행위를 하나로 묶고, 실제 구현 내용 일부를 내부에 감추어 은닉한다.

클래스는 작아야 한다.

클래스는 작아야 한다.

클래스 크기의 척도는 클래스에게 주어진 책임의 정도.
메서드의 개수가 적다고 책임이 적은 것이 아니라, 실제 책임의 정도가 작아야 한다.
책임의 정도가 적다면 클래스의 이름을 간결하게 지을 수 있어야 한다.

단일 책임 원칙Single Responsibility Principle, SRP은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙.

단일 책임 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려하기도 한다.
큰 그림을 이해하기 위해 많은 클래스를 수없이 넘나들어야 한다고 걱정한다.
하지만, 작은 클래스가 많은 시스템이든 큰 클래스가 몇 개뿐인 시스템이든 돌아가는 부품은 그 수가 비슷하다.
규모가 커지면 논리가 많고 복잡하다. 이런 복잡성을 다루려면 체계적인 정리가 필수다.
그래야 책임을 찾기 쉽고, 문제 해결을 위해 이해해야 하는 분량이 적다.

응집도

클래스는 인스턴스 변수가 적어야 한다. 일반적으로 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 높다. 응집도가 높다는 것은 클래스 내의 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미다.

응집도를 유지하면 작은 클래스 여럿이 나온다.

클래스 내에 변수가 아주 많은 큰 함수 하나가 있다. 일부를 작은 함수 하나로 빼고 싶은데, 큰 함수 내에서 정의된 변수 4개를 사용한다. 그때, 4개를 함수의 인자로 전달할 것인가?

4개의 변수를 인스턴스 변수로 정의한다면 인자로 전달하지 않아도 된다. 함수를 나누기 쉬워진다.
다만, 클래스의 응집력이 떨어진다. (몇몇 함수만 사용하는 인스턴스 변수가 늘어나기 때문)
그땐, 클래스를 나누면 된다.

함수를 나누고, 클래스를 나누다 보면 점점 프로그램에 체계가 잡히고 구조가 투명해진다.

변경하기 쉬운 클래스

대다수 시스템은 지속적인 변경이 가해진다. 변경할 때마다 의도치 않은 동작이 발생할 위험이 따른다.
클래스를 체계적으로 정리해 변경에 따른 위험을 낮춘다.

class Sql {
    create(): string;
    insert(fields: Object[]): string;
    selectAll(): string;
    // ...
}

위 코드는 적절한 SQL 문자열을 만드는 클래스이다.
새로운 SQL 문을 지원하거나 새로운 기능을 추가, 기존 SQL 문을 변경해야 할 때,
모두 Sql 문을 고쳐야 한다. 변경해야 할 이유가 여러 개이므로 Sql 클래스는 SRP를 위반한다.

abstract class Sql {
    constructor(table: string, columns: Column[]) {}
    abstract generate(): string;
}

class CreateSql extends Sql {
    constructor(table: string, columns: Column[]) {}
    generate(): string {}
}

class InsertSql extends Sql {
    constructor(table: string, columns: Column[]) {}
    generate(): string {}
}

// ...

각 클래스는 단순하다. 수정에 대한 위험도 사라졌다.
update 문을 추가할 때도 기존 클래스를 변경할 필요 없이 새 클래스를 생성하면 된다.

이는 OCPOpen-Closed Principle을 지원한다. 확장에는 개방적이고 수정에 폐쇄적이어야 한다는 원칙.
파생 클래스를 생성하는 방식으로 새 기능에 개방적인 동시에, 다른 클래스를 닫아놓는 방식으로 수정에는 폐쇄적이다.

변경으로부터 격리

구체적인 클래스는 상세한 구현(코드)을 포함하며, 추상 클래스는 개념만 포함한다.
상세한 구현에 의존하는 클래스는 구현이 바뀌면 위험에 빠진다.
그래서 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.

시스템의 결합도를 낮추면 유연성과 재사용성이 높아진다.
이는 변경으로부터 잘 격리되어 있다는 의미.

결합도를 최소로 줄이면 DIPDependency Inversion Principle를 따른다.

참고 자료