Clean Code 3판을 읽고 정리한 글입니다
Ⅹ. 클래스
- 앞서 코드 행과 코드 블록을 올바로 작성하는 방법, 함수를 올바로 구현하는 방법과 함수가 서로 관련을 맺는 방법 공부
- 위의 내용과 더불어 더 높은 단계까지 신경을 써야만 깨끗한 코드를 얻을 수 있음
클래스 체계
표준 자바 관례
- 가장 먼저 변수 목록
- static, public 상수가 있을 경우 맨 처음에 나온다
- 그다음 정적 비공개, 이어서 비공개 인스턴스 변수
- (공개 변수가 필요한 경우는 거의 없음)
- 공개 함수
- 비공개 함수
- 왜 이렇게 ?
- 가독성 좋게
- 추상화 단계가 순차적으로 내려간다
- 프로그램은 신문 기사처럼 읽힌다
캡슐화
변수/유틸리티
- 변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 하는 것은 아니다.
- protected로 선언해서 접근을 허용하기도 한다.
- 예) 테스트
- 우리에게 테스트는 중요하다
- 같은 패키지안에서 테스트 코드가 함수 호출하거나 변수를 사용시
- 해당 함수나 변수를 protected로 선언하거나 패키지 전체로 공개
- 캡슐화를 풀어주는 결정은 언제나 최후의 수단 : 비공개 상태를 유지할 온갖 방법을 강구
클래스는 작아야 한다
- 함수장에서 했던 이야기의 되풀이가 아님 : 함수와 마찬가지로 “작게”가 기본 규칙이라는 의미
- 얼마나 작아야 하나?
너무 많은 책임 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75// 어마어마하게 큰 슈퍼 만능 클래스
public class SuperDashboard extends JFrame implements MetaDataUser {
public String getCustomizerLanguagePath()
public void setSystemConfigPath(String systemConfigPath)
public String getSystemConfigDocument()
public void setSystemConfigDocument(String systemConfigDocument)
public boolean getGuruState()
public boolean getNoviceState()
public boolean getOpenSourceState()
public void showObject(MetaObject object)
public void showProgress(String s)
public boolean isMetadataDirty()
public void setIsMetadataDirty(boolean isMetadataDirty)
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public void setMouseSelectState(boolean isMouseSelected)
public boolean isMouseSelected()
public LanguageManager getLanguageManager()
public Project getProject()
public Project getFirstProject()
public Project getLastProject()
public String getNewProjectName()
public void setComponentSizes(Dimension dim)
public String getCurrentDir()
public void setCurrentDir(String newDir)
public void updateStatus(int dotPos, int markPos)
public Class[] getDataBaseClasses()
public MetadataFeeder getMetadataFeeder()
public void addProject(Project project)
public boolean setCurrentProject(Project project)
public boolean removeProject(Project project)
public MetaProjectHeader getProgramMetadata()
public void resetDashboard()
public Project loadProject(String fileName, String projectName)
public void setCanSaveMetadata(boolean canSave)
public MetaObject getSelectedObject()
public void deselectObjects()
public void setProject(Project project)
public void editorAction(String actionName, ActionEvent event)
public void setMode(int mode)
public FileManager getFileManager()
public void setFileManager(FileManager fileManager)
public ConfigManager getConfigManager()
public void setConfigManager(ConfigManager configManager)
public ClassLoader getClassLoader()
public void setClassLoader(ClassLoader classLoader)
public Properties getProps()
public String getUserHome()
public String getBaseDir()
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
public MetaObject pasting(MetaObject target, MetaObject pasted, MetaProject project)
public void processMenuItems(MetaObject metaObject)
public void processMenuSeparators(MetaObject metaObject)
public void processTabPages(MetaObject metaObject)
public void processPlacement(MetaObject object)
public void processCreateLayout(MetaObject object)
public void updateDisplayLayer(MetaObject object, int layerIndex)
public void propertyEditedRepaint(MetaObject object)
public void processDeleteObject(MetaObject object)
public boolean getAttachedToDesigner()
public void processProjectChangedState(boolean hasProjectChanged)
public void processObjectNameChanged(MetaObject object)
public void runProject()
public void setAçowDragging(boolean allowDragging)
public boolean allowDragging()
public boolean isCustomizing()
public void setTitle(String title)
public IdeMenuBar getIdeMenuBar()
public void showHelper(MetaObject metaObject, String propertyName)
// ... 많은 비공개 메서드가 계속 이어짐...
}
1 | // 메서드 수가 작아도 책임이 너무 많다 |
클래스 이름은 해당 클래스 책임을 기술해야
- 작명은 클래스 크기를 줄이는 첫번째 관문
- 간결한 이름이 떠오르지 않는다면? 클래스 책임이 너무 많아서!
- 클래스 이름에 Manager, Processor, Super등 모호한 단어(ch2언급)가 있다면 여러 책임을 떠안긴 것
- 클래스 설명은 “if”, “and”, “or”, “but”을 사용하지 않고 25 단어 내외로 가능해야
- 위의 SuperDashboard를 설명하면?
- “SuperDashboard는 마지막으로 포커스를 얻었던 컴포넌트에 접근하는 방법을 제공하며, 버전과 빌드 번호를 추적하는 메커니즘을 제공한다.”
- 첫 번쨰 “~하며,”는 SuperDashboard에 책임이 너무 많다는 증거
단일 책임 원칙
자세한 것은 클린 소프트웨어 참조
SRP
- 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙
- “책임”이라는 개념 정의, 적절한 클래스 크기 제시
위 5가지 메서드의 SuperDashboard는?
- sw 버전 정보를 추적 : sw 출시 때마다 달라짐
- 자바 스윙 컴포넌트 관리
- 최상위 GUI 윈도우 스윙 표현인 JFrame에서 파생한 클래스이므로
- 스윙 코드를 변경할 떄마다 버전 번호가 달라짐
- 역은 false : 때로는 다른 코드를 바꾸고 나서도 버전 번호를 바꾼다
책임 = 변경할 이유
- 이를 파악하려 애쓰면 코드 추상화가 쉬어진다
- 위에서 버전 정보를 다루는 메서드들을 빼내서 독자적인 클래스를 만든다면
단일 책임 클래스 1
2
3
4
5
6//다른 어플리케이션에서 재사용하기 아주 쉬운 구조!
public class Version {
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
SRP = 중요하고 이해하고 지키기 쉽지만 설계자가 잘 어기는 규칙 중 하나.. 어째서?
sw를 돌아가게 만드는 활동(구현 개발)과 sw를 깨끗하게 만드는 활동(클린 코드)은 별개
두뇌 용량의 한계로 클린 코딩 보다는 구현 개발에 초점
- 올바른 태도
- 관심사를 분리하는 작업은 프로그램뿐 아닌 프로그래밍 활동에서도 마찬가지로 중요
문제는?
- 프로그램이 돌아가면 일이 끝났다고 여김
- 구현 개발의 관심사 종료시 ‘깨끗하고 체계적인 sw(클린 코드)’라는 다음 관심사로 전환하지 않음
- 구현된 만능 클래스를 단일 책임 클래스 여럿으로 분리하는 대신 다음 이슈처리해버림
- 클래스가 많아지면 큰 그림 이해 못한다고 우려
- 자잘한 SRP 클래스가 많아지면 큰 그림 이해를 위해 여러 클래스를 수없이 넘나들어야 한다며 걱정
양은 비슷하다
- 작은 클래스가 많은 시스템에든, 큰 클래스가 몇개 뿐인 시스템이든 들어가는 부품은 그 수가 비슷함
- 어느 시스템이든 익힐 내용은 그 양이 비슷
따라서 고민할 질문은 도구상자를 어떻게 관리하고 싶은가? 이다
- 작은 서랍을 많이 두고 기능과 이름이 명확한 컴포넌트를 나눠 넣고 싶은가?
- 큰 서러 몇개에 모두를 던져 넣고 싶은가?
기본 규모가 어느정도에 이르는 시스템은 논리가 많고도 복잡하다
복잡성을 다룰려면 체계적인 정리가 필수
그래야 개발자가 어디에 무엇이 있는지 쉽게 찾을 수 있음
그래야 (변경시)직접 영향이 미치는 컴포넌트만 이해해도 충분
그렇지 않은 다목적 클래스 몇 개로 구성된 시스템은 (변경시) 당장 알 필요가 없는 부분까지 보여 독자 방해
작은 클래스 여럿으로 이뤄진 시스템이 바람직함
작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.
응집도(Cohesion)
- 클래스는 인스턴스 변수 수가 작아야 한다.
- 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.
- 일반적으로 메서드가 변수를 더 많이 사용할 수록 메서드와 클래스는 응집도가 더 높다.
- 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높지만, 이런 클래스는 가능하지도, 바람직하지도 않다.
- 하지만 가능한한 응집도가 높은 클래스를 지향해야 한다.
- 응집도가 높다 = 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미
1 | //size를 제외한 두 메서드는 두 변수 모두 사용 |
함수를 작게, 매개변수 목록을 짧게라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아지는 케이스 발생
→ 이는 십중 팔구 새로운 클래스를 쪼개야 한다는 신호
→ 응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼갠다
응집도를 유지하면 작은 클래스 여럿이 나온다
큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다
예)변수가 아주 많은 큰 함수
- 큰 함수 일부를 작은 함수로 extract하려 함
- 빼내려는 코드가 큰 함수에 정의 된 변수를 많이 사용한다
- 그러면 변수들을 새 함수에 인수로 넘겨야 하나?
- 답은? 전혀 아니다
- 변수들을 클래스 인스턴스 변수로 승격 시키면 인수가 필요없음
- 불행히 응집력이 낮아짐 : 몇몇 함수만 사용하는 인스턴스 변수가 점점 더 늘어나므로
- 몇몇 함수가 몇몇 인스턴스 변수만 사용하게되면? 독자적인 클래스로 쪼개라!!
결론 : 큰 함수 작은 함수로 스플릿 → 종종 작은 클래스 여럿으로 쪼갤 기회 → 프로그램의 체계가 잡히고 구조가 투명해짐
예시) 커누스 교수가 쓴 문학적 프로그래밍(Literate Programming)에 나오는 유서 깊은 예제
- 커누스 교수의 PrintPrimes를 자바로 변환한 코드
- 그가 직접 짠 포르그램이 아닌 웹도구로 출력한 결과
1 | package literatePrimes; |
엉망징창 프로그램
- 심한 들여쓰기
- 많은 이상한 변수
- 빡빡히 결합된 구조
작은 함수와 클래스로 나누고 함수와 클래스와 변수에 좀더 의미있는 이름을 부여하며 리팩터링을 해보자
1 | package literatePrimes; |
1 | package literatePrimes; |
1 | package literatePrimes; |
변화
- 프로그램이 길어짐
- 리팩터링 → 더 길 서술적인 변수명 사용
- 리팩터링 → 코드 주석을 추가하는 수단으로 함수 선언과 클래스 선언 사용
→ 가독성을 높이고자 공백을 추가하고 포맷팅
- 책임에 따라 분류
- PrimePrinter : main 함수 하나만 포함하며 실행 환경 책임
- RowColumnPagePrinter : 숫자 목록을 주어진 행과 열에 맞춰 페이지에 출력하는 방법
- PrimeGenerator : 소수 목록 생성
- 객체로 인스턴스화하는 클래스가 아니다
- 변수를 선언하고 감추려고 사용하는 유용한 공간일 뿐
- 소수 계산 알고리즘 변경시 해당 클래스 변경
- 재구현이 아니다
- 실제 동작원리가 비슷하다
- 원래 프로그램의 정확한 동작을 검증하는 테스트 슈트를 먼저 작성
- 이후 테스트를 수행하며 확인하면서 조금씩 코드를 변경
변경하기 쉬운 클래스
- 시스템은 지속적인 변경 가해짐
- 변경이 있을 때 마다 의도대로 동작하지 않을 위험이 따른다.
- 깨끗한 시스템은 클래스를 체계적으로 관리해 변경에 따르는 위험을 최대한 낮춘다.(경험에 의하면) 클래스 일부에서만 사용되는 비공개 메서드는 코드를 개선할 잠재젹 여지 시사
변경이 필요해 손대야 하는 클래스 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 새로운 새로운 SQL문을 지원할 때, 기존 SQL문을 수정할 때 수정 필요
// 변경할 이유가 2가지 → SRP를 위반한다
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)
}
→ 실제로 개선에 뛰어드는 계기는 시스템이 변해서라야 한다
→ SQL클래스를 논리적으로 완성으로 여긴다면 책임 분리 시도 필요 없음
→ 변경이 필요하지 않는다면 내버려두는 편이 좋음
→ 변경을 위해 손을 대는 순간 설계 개선에 대한 고민/시도 필요특징 : 장점만 취한다닫힌 클래스 집합 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53// 공개 인터페이스를 전부 SQL 클래스에서 파생하는 클래스로 만듬
// valueList와 같은 비공개 메서드는 해당 파생 클래스로 옮김
// 모든 파생 클래스가 공통으로 사용하는 비공개 메서드는 Where, ColumnList 두 유틸 클래스에 넣었다
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)
public String generate()
}
public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns)
public String generate()
}
public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields)
public String generate()
private String valuesList(Object[] fields, final Column[] columns)
}
public class SelectWithCriteriaSql extends Sql {
public SelectWithCriteriaSql(
String table, Column[] columns, Criteria criteria)
public String generate()
}
public class SelectWithMatchSql extends Sql {
public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern)
public String generate()
}
public class FindByKeySql extends Sql public FindByKeySql(
String table, Column[] columns, String keyColumn, String keyValue)
public String generate()
}
public class PreparedInsertSql extends Sql {
public PreparedInsertSql(String table, Column[] columns)
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()
} - SRP 지원 : 클래스가 서로 분리됨, 함수 하나를 수정해도 다른 함수가 망가지지 않음
- OCP 지원
- 파생 클래스 생성하는 방식으로 새로운 기능 추가에 개방적
- update 문 추가시 UpdateSql 클래스를 끼워 넣으면 끝
- 다른 클래스를 닫아놓음 → 수정에 폐쇄적
- 파생 클래스 생성하는 방식으로 새로운 기능 추가에 개방적
변경으로부터 격리
- 요구사항은 변한다 → 코드도 변한다
- 의존객체의 상세 구현에 의존하는 클라이언트 → 구현이 바뀌면 위험 → 인터페이스, 추상 클래스로 구현이 미치는 영향을 격리
- 상세한 구현에 의존하는 코드는 테스트도 어렵다
- 추상화를 통해 테스트가 가능할 정도로 시스템의 결합도를 낮춤→ 유연성과 재사용성도 더욱 높아진다.
결함도가 낮다는 말은 각 시스템 요소가 다른 요소로부터 그리고 변경으로부터 잘 격리되어있다는 뜻이다.
예)Portfolio 클래스
- 외부 TokyoStockExchange API를 사용해 포트폴리오 값을 계산 → 외부 시세 변화 영향으로 값이 달라짐
1 | public interface StockExchange { |
1 | // TokyoStockExchange 클래스 : StockExchange 인터페이스를 구현 |
1 | // StockExchange 인터페이스로 상세 구현의 의존을 막았으므로 테스트용 클래스로 바꿀 수 있음 |
위의 테스트가 가능할 정도로 시스템 결합을 낮추면?
- 유연성과 재사용성이 더 높아짐
- 시스템 결합이 낮다 : 각 시스템 요소가 다른 요소, 그리고 변경으로부터 잘 격리가 되어있다는 의미 → 각 요소 이해도 쉬어짐
- 결합도 최소로 줄이면? 자연스럽게 DIP를 따르는 클래스가 나온다
- DIP : 클래스가 상세 구현이 아닌 추상화 의존해야 한다는 원칙
Related POST
- [Clean Code] Ⅰ. 깨끗한 코드
- [Clean Code] Ⅱ.의미 있는 이름
- [Clean Code] Ⅲ. 함수
- [Clean Code] Ⅳ. 주석
- [Clean Code] Ⅴ. 형식 맞추기
- [Clean Code] Ⅵ. 객체와 자료구조
- [Clean Code] Ⅶ. 오류 처리
- [Clean Code] Ⅷ. 경계
- [Clean Code] Ⅸ. 단위 테스트
- [Clean Code] Ⅹ. 클래스
- [Clean Code] Ⅺ. 시스템
- [Clean Code] Ⅻ. 창발성(創發性)
- [Clean Code] XIII. 동시성
- [Clean Code] XIV. 점진적 개선(SUCCESSIVE REFINEMENT)
- [Clean Code] XV. JUnit 들여다보기
- [Clean Code] XVI. SerialDate 리팩터링
- [Clean Code] XVII. 냄새와 휴리스틱
- [Clean Code] 다 읽었다~