Clean Code_10장. 클래스
#노마드코더 #북클럽 #노개북
TIL (Today I Learned)
클래스는 단일 책임만 가져야 한다는 사실을 배웠다.
오늘 읽은 범위
10장. 클래스
책에서 기억하고 싶은 내용
클래스 체계
표준 자바 관례에 따르면
- 변수 등장
- public static 상수가 있으면 맨 처음에 등장
- private static 변수
- private 인스턴스 변수
- 공개 변수가 필요한 경우는 거의 없다
- public 함수
- 자신을 호출하는 public 함수
- private 함수
→ 추상화 단계가 순차적으로 내려간다.
캡슐화
변수와 유틸리티 함수는 가능한 공개하지 않는 것이 좋지만 반드시 숨겨야 하는 것 아니다.
때로는 변수나 유틸리티 함수를 protected
로 선언해 테스트 코드에 접근을 허용하기도 한다.
캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.
클래스는 작아야 한다!
클래스를 설계할 때도 함수와 마찬가지로 ‘작게‘가 기본 규칙이다.
얼마나 작아야 할까?
함수는 물리적인 행 수로 크기를 측정했다.
클래스는 맡은 책임을 센다.
단일 책임 원칙
단일 책임 원칙(Single Responsibility Principle, SRP)은 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 하는 원칙.
큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.
작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.
응집도(Cohension)
클래스는 인스턴스 변수 수가 작아야 한다. 일반적으로 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다. 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다.
응집도가 높다는 말은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미다.
‘함수를 작게, 매개변수 목록을 짧게’라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다. 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다.
응집도를 유지하면 작은 클래스 여럿이 나온다
큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다.
변경하기 쉬운 클래스
깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다.
public class Sql {
public Sql(String table, Column[] columns)
public String create()
public string insert(Object[] fields)
public String selectAll()
public String findByKey(String keyColumn, String keyValue)
public String select(Column column, String pattern)
public String select(Criteria criteria)
public String preparedInsert()
private String columnList(Column[] columns)
private String valuesList(Object[] fields, final Column[] columns)
private String selectWithCriteria(String criteria)
private String placeholderList(Column[] columns)
}
위의 클래스는 수정하기 어렵다. 수정하면 테스트도 다시 해야한다.
abstract public class Sql {
public Sql(String table, Column[] columns)
abstract public String generate();
}
public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns)
@Override public String generate()
}
public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns)
@Override public String generate()
}
public class InsertSql extneds Sql {
public InsertSql(String table, Column[] columns, Object[] fileds)
@Override public String generate()
private String valuesList(Object[] fields, final Column[] columns)
}
public class SelectWithCriteriaSql extends Sql {
public SelectWithCriteriaSql(String table, Column[] columns, Criteria criteria)
@Override public String generate()
}
public class SelectWithMatchSql extends Sql {
public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern)
@Override public String generate()
}
public class FindByKeySql extends Sql {
public FindByKeySql(String table, Column[] columns, String keyColumn, String keyValue)
@Override public String generate()
}
public class PreparedInsertSql extends Sql {
public PreparedInsertSql(String table, Column[] columns)
@Override public String generate()
private String placeholderList(Column[] columns)
}
public class Where {
public Where(STring criteria)
public String generate()
}
public class columnList {
public columnList(Column[] columns)
public String generate()
}
이런식으로 추상 클래스 Sql을 만들고, 이를 상속하는 다양한 클래스들을 만든다.
모든 파생 클래스가 공통으로 사용하는 private
메서드는 Where과 ColumnList라는 두 유틸리티 클래스로 쪼갠다.
이렇게 바꾸면
- SRP를 지원한다
- OCP도 지원한다
- 확장에 개방적이고, 수정에 폐쇄적
- 새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소다
변경으로부터 격리
객체 지향 프로그래밍 입문에서 우리는 구체적인(concrete) 클래스와 추상(abstract)클래스가 있다고 배웠다.
구체적인 클래스는 상세한 구현(코드)을 포함하며 추상 클래스는 개념만 포함한다.
상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 그래서 우리는 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.
결합도를 낮추면 유연성과 재사용성이 높아지고 자연스럽게 DIP(Dependency Inversion Principle)를 따르는 클래스가 나온다.
본질적으로 DIP는 클래스가 상세한 구현이 아닌 추상화에 의존해야 한다는 원칙이다.
오늘 읽은 소감
함수와 마찬가지로 클래스도 크기가 작은 것이 좋다고 하지만 역시나 잘 쪼개는 방법은 쉽지 않은 것 같다. 어느 정도로 분리해야 하며 결합도가 높은 코드를 어떻게 풀어낼 것인지는 여러 번의 설계 고민과 리팩토링 연습을 통해 익혀야 할 것 같다.
궁금한 내용, 잘 이해되지 않는 내용
요구사항이 바뀌어 추상 클래스도 작성한 클래스도 바뀌어야 할 때는 그 클래스를 상속하는 많은 클래스들에 변화가 필요할텐데 이처럼 추상 클래스에 변화가 생겨야 하는 불가피한 경우 어떻게 리팩토링 하는 것이 효율적일지 궁금하다.