개발
home
🏭

[클린코드] 10장 클래스

Created
2022/04/27
Tags
CleanCode
Java
2022-04-27 @이영훈
코드가 모여 함수가 되고 함수가 모여 클래스가 됩니다
높은 차원 단계인 클래스까지 신경써야 깨끗한 코드를 얻을 수 있습니다

클래스 체계

클래스를 정의하는 포준 자바 관례에 따르면,
static public 상수 → static private 상수 → private 인스턴스 변수
→ public 함수 → private 함수 순으로 나옵니다
(public 인스턴스 변수가 필요한 경우는 거의 없습니다)
추상화 단계가 순차적으로 내려가서 코드가 신문 기사처럼 읽힙니다.

캡슐화

변수와 유틸리티 함수는 가능한 공개하지 않는 편이 좋습니다 (private)
테스트를 위해서 protected로 풀어주는 경우가 있습니다
현재에는 reflection을 이용하여 테스트하는 방법이 있기 때문에 private으로 유지하는 것이 좋습니다
캡슐화를 풀어주는 결정은 언제나 최후의 수단입니다.

클래스는 작아야 한다

클래스를 만들 때 반드시 지켜하는 규칙은 클래스는 작아야 한다 입니다
클래스가 작다라는 것은 클래스가 맡은 책임이 적다라는 것입니다
하나의 클래스는 하나의 책임을 가지고 있는 것이 좋습니다
SuperDashBoard 클래스는 두 개의 책임을 가지고 있습니다
마지막 포커스 위치를 컨트롤하고 버전 정보를 가지고 있습니다
public class SuperDashBoard extends JFrame implements MetaDataUser { public Component getLastFocusedComponent(); public void setLastFocused(Component lastFocused); public int getMajorVersionNumber(); public int getMinorVersionNumber(); public int getBuildNumber(); }
Java
복사
클래스 이름은 해당 클래스 책임을 기술해야 합니다.
그리고 클래스 설명은 if, and, or, but 을 사용하지 않고 25단어 내외로 가능해야 합니다.
SuperDashBoard 클래스로 보면
1.
클래스 명은 SuperDashBoardAndVersion 이 더 적절합니다
2.
SuperDashBoardAndVersion 에 and가 있기 때문에 2개 이상의 역할을 가지고 있다는 것을 알 수 있습니다

단일 책임 원칙 SRP

단일 책임 원칙 (Single Responsibility Principle)은 클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙입니다.
SRP는 객체 지향 설계에서 중요한 개념입니다. 이해하고 지키기 수월한 개념이기도 합니다.
하지만 잘 지켜지지 않는 이유는
1.
‘돌아가는 소프트웨어'를 작성한 뒤에 ‘깨끗하고 체계적인 소프트웨어'로 전환하지 않습니다.
2.
자잘한 단일 책임 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려합니다.
큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직합니다.
규모가 큰 시스템은 논리가 많고 복잡합니다. 복잡한 시스템을 다루려면 체계적인 정리가 필수입니다.

응집도 Cohension

클래스는 인스턴스 변수 수가 적어야 합니다.
각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 합니다.
일반적으로 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높습니다.
응집도가 높다는 말은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미입니다.
‘함수를 작게, 매개변수 목록을 짧게'라는 전략을 따르다 보면 몇몇 메서드만 사용하는 인스턴스 변수가 아주 많아집니다. 이는 새로운 클래스로 쪼개야 하는 신호입니다.

큰 함수를 작은 함수로 쪼개면 작은 클래스 여럿이 나온다

큰 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 나눌 기회가 생깁니다.
그러면서 프로그램에 점점 더 체계가 잡히고 구조가 투명해집니다.

변경하기 쉬운 클래스

깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반되는 위험을 낮춥니다.
다음의 SQL 클래스는 update 문을 지원할 계획입니다.
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); }
Java
복사
 SQL 클래스는 많은 역할을 합니다.
select, select with criteria, ...
insert
where
column list
클래스가 하나의 역할만 하도록 다음과 같이 분리할 수 있습니다
하나의 역할을 할 자식 클래스들에서 SQL 클래스를 상속 받아 구현합니다.
abstract public Class SQL { public SQL(String table, Column[] columns); abstract public String generate(); }
Java
복사
SQL 쿼리 문자열을 만들어주는 역할을 하는 CreateSQL 클래스
public class CreateSQL extends SQL { public CreateSQL(String table, Column[] columns); @Override public String generate() { ... } }
Java
복사
Select 역할을 하는 SelectSQL 클래스
public class SelectSQL extends SQL { public SelectSQL(String table, Column[] columns); @Override public String generate() { ... } }
Java
복사
조건(where)에 따라 Select 역할을 하는 SelectWithCriteriaSQL 클래스
public class SelectWithCriteriaSQL extends SQL { public SelectWithCriteriaSQL(String table, Column[] columns, Criteria criteria); @Override public String generate() { ... } }
Java
복사
Insert 역할을 하는 InsertSQL 클래스
public class InsertSQL extends SQL { public InsertSQL(String table, Column[] columns, Object[] fields); @Override public String generate() { ... } private String valuesList(Object[] fields, final Column[] columns); }
Java
복사
각 클래스는 극도로 단순하고 코드는 순식간에 이해됩니다.
함수 하나를 수정했다고 다른 함수가 망가질 위험도 사라졌습니다. 테스트코드에서 테스트해야할 것들도 분명해지고 쉬워졌습니다.
클래스 일부에서만 사용되는 비공개 메서드는 코드를 개선할 잠재적인 여지를 시사합니다.
하지만 실제로 개선에 뛰어드는 시기는 시스템이 변해서라야 합니다.
하지만 클래스에 손대는 순간 설계를 개선하려는 고민과 시도가 필요합니다.
리팩토링 제1원칙: 돌아가는 코드는 수정하지 않습니다.

변경으로부터 격리

요구사항은 항상 변하고, 이에따라 코드도 변합니다.
추상 클래스를 사용해 구체 클래스에 구현이 미치는 영향을 격리해야 합니다.
역할과 구현. 추상 클래스와 구체 클래스.
할인 로직을 구현한다고 했을 때, 할인 역할(interface)을 만들고 실제 할인을 하는 구현을 만드는 게 좋습니다. (전체 예시 코드)
상세한 구현에 의존하는 것이 아니라 추상화에 의존하기 때문에 DIP원칙을 지킬 수 있습니다.
할인 역할
public interface DiscountPolicy { public int discount(int itemPrice); }
Java
복사
구현1: 퍼센트 할인
public class PercentDiscountPolicy implements DiscountPolicy { private final int discountPercent; public PercentDiscountPolicy(int discountPercent) { this.discountPercent = discountPercent; } @Override public int discount(int itemPrice) { // 퍼센트 할인시 생기는 소수점 자리는 버림 처리 return (int) (itemPrice * discountPercent * 0.01); } }
Java
복사
구현2: 고정 할인
public class FixAmountDiscountPolicy implements DiscountPolicy{ private final int discountAmount; public FixAmountDiscountPolicy(int discountAmount) { this.discountAmount = discountAmount; } @Override public int discount(int itemPrice) { return discountAmount; } }
Java
복사