안녕하세요.
오늘은 클래스와 클린코드를 엮어서 학습해 볼 것입니다.
즉, 깨끗한 클래스를 다룰 겁니다.
개인적으로 클래스 분리는 객체지향의 꽃이라고 생각합니다.
✔️ 클래스는 작아야 한다!
클래스를 만들 때 첫 번째 규칙은 크기다. 클래스는 작아야 한다.
두 번째 규칙도 크기다. 더 작아야 한다.
❓ 그럼 도대체 얼마나 작아야 할까?
함수는 물리적인 행 수로 크기를 측정했다.
💡 클래스는 맡은 책임을 센다!
다음 코드는 SuperDashboard라는 클래스로, 공개 메서드 수가 대략 70개 정도다.
너무 많은 책임을 가진다.
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 showProgress(String s);
public void showObject(MetaObject object);
public boolean isMetadataDirty();
public void setIsMetadataDirty(boolean isMetadataDirty);
public void setMouseSelectState(boolean isMouseSelected);
...
}
아마 대다수의 개발자는 이 클래스가 엄청나게 크다는 사실에 동의할 것이다.
어쩌면 SuperDashboard 클래스를 '만능 클래스'라고 부를지도?
하지만 만약 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();
}
메서드 5개 정도면 괜찮을까?
여기서는 그렇지 않다.
SuperDashboard는 메서드 수가 작음에도 불구하고 책임이 너무 많다.
💡 클래스 이름은 해당 클래스 책임을 기술해야 한다.
실제로 작명은 클래스 크기를 줄이는 첫 번째 관문이다.
만약 간격한 이름이 떠오르지 않는다면,
필경 클래스 크기가 너무 커서 그렇다.
클래스 이름이 모호하다면,
필경 클래스 책임이 너무 많아서다.
예를 들어, 클래스 이름에
🚨 Processor, Manager, Super 등과 같이 모호한 단어가 많이 있다면,
클래스에다 여러 책임을 떠안겼다는 증거이다.
🚨 또한 클래스 설명은 if, and, or, but을 사용하지 않고서
25 단어 내외로 가능해야 한다.
✔️ 단일 책임 원칙
단일 책임 원칙(Single Responsibility Principle, SRP)은
클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.
SRP는 '책임'이라는 개념을 정의하며, 적절한 클래스 크기를 제시한다.
클래스는 책임, 즉 변경할 이유가 하나여야 한다는 의미이다.
위의 SuperDashboard는 겉보기엔 작아 보인다.
하지만 변경할 이유가 두 가지이다.
- SuperDashboard는 소프트웨어 버전 정보를 추적한다.
- 그런데 버전 정보는 소프트웨어를 출시할 때마다 달라진다.
- SuperDashboard는 자바 스윙 컴포넌트를 관리한다.
- 즉, 스윙 코드를 변경할 때마다 버전 번호가 달라진다.
💡 책임, 즉 변경할 이유를 파악하려 애쓰다 보면,
코드를 추상화하기도 쉬워진다!
더 좋은 추상화가 더 쉽게 떠오른다.
SuperDashboard에서 버전 정보를 다루는 메서드 세 개를 따로 빼내어 Version이라는 독자적인 클래스를 만든다.
다음 Version 클래스는 다른 애플리케이션에서 재사용하기 아주 쉬운 구조다!
public class Version {
public int getMajorVersionNumber();
public int getMinorVersionNumber();
public int getBuildNumber();
}
- '버전 정보'라는 단일 책임 클래스이다.
❗ SRP는 객체 지향 설계에서 더욱 중요한 개념이다.
또한 이해하고 지키기 수월한 개념이기도 하다.
✔️ 응집도를 유지하면 작은 클래스 여럿이 나온다.
일반적으로 응집도가 가장 높은 클래스는 가능하지도 바람직하지도 않다.
그렇지만 우리는 응집도가 높은 클래스를 선호한다.
💡 응집도가 높다는 말은, 클래스에 속한 메서드와 변수가
서로 의존하며 논리적인 단위로 묶인다는 의미이기 때문이다.
다음 코드는 Stack을 구현한 코드이다.
아래 클래스는 응집도가 아주 높은 클래스이다.
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedLIst<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0)
throw new PoppedWhenEmpty;
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
- size()를 제외한 다른 두 메서드는 두 변수를 모두 사용한다.
'함수를 작게, 매개변수 목록을 짧게'라는 전략을 따르다 보면,
때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다.
❗ 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다!
응집도가 높아지도록, 변수와 메서드를 적절히 분리해
새로운 클래스 두세 개로 쪼개준다.
🤔 그럼 어떻게 해야 할까?
💡 만약 클래스가 응집력을 잃는다면 쪼개라!
→ 몇몇 함수가 몇몇 변수만 사용한다면, 독자적인 클래스로 분리해도 되지 않는가? 당연하다.
큰 함수를 작은 함수 여럿으로 쪼개다 보면,
종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다.
그러면서 프로그램에 점점 더 체계가 잡히고, 구조가 투명해진다.
✔️ 큰 함수를 작은 함수/클래스 여럿으로 쪼개보자
[PrintPrimes.java]
public class PrintPrimes {
public static void main(String[] args) {
final int M = 1000;
final int RR = 50;
final int CC = 4;
final int WW = 10;
final int ORDMAX = 30;
int P[] = new int[M + 1];
int PAGENUMBER;
int PAGEOFFSET;
int ROWOFFSET;
int C;
int J;
int K;
boolean JPRIME;
int ORD;
int SQUARE;
int N;
int MULT[] = new int[ORDMAX + 1];
J = 1;
K = 1;
P[1] = 2;
ORD = 2;
SQUARE = 9;
while (K < M) {
do {
J = J + 2;
if (J == SQUARE) {
ORD = ORD + 1;
SQUARE = P[ORD] * P[ORD];
MULT[ORD - 1] = J;
}
N = 2;
JPRIME = true;
while (N < ORD && JPRIME) {
while (MULT[N] < J)
MULT[N] = MULT[N] + P[N] + P[N];
if (MULT[N] == J)
JPRIME = false;
N = N + 1;
}
} while (!JPRIME);
K = K + 1;
P[K] = J;
}
{
PAGENUMBER = 1;
PAGEOFFSET = 1;
while (PAGEOFFSET <= M) {
System.out.println("The First " + M +
" Prime Numbers --- Page " + PAGENUMBER);
System.out.println("");
for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++) {
for (C = 0; C < CC; C++)
if (ROWOFFSET + C * RR <= M)
System.out.format("%10d", P[ROWOFFSET + C * RR]);
System.out.println("");
}
System.out.println("\f");
PAGENUMBER = PAGENUMBER + 1;
PAGEOFFSET = PAGEOFFSET + RR * CC;
}
}
}
}
- 함수가 하나뿐인 위 프로그램은 엉망진창이다.
- 들여쓰기가 심하고
- 이상한 변수가 많고
- 구조가 빡빡하게 결합되었다.
- 최소한 여러 함수로 나눠야 마땅하다.
위 코드를 작은 함수와 클래스로 나눈 후
함수와 클래스와 변수에 좀 더 의미 있는 이름을 부여해 보자.
[PrimePrinter.java] - 리팩터링한 버전
public class PrintPrimes {
public static void main(String[] args) {
final int NUMBER_OF_PRIMES = 1000;
int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);
final int ROWS_PER_PAGE = 50;
final int COLUMNS_PER_PAGE = 4;
RowColumnPagePrinter tablePrinter =
new RowColumnPagePrinter(ROWS_PER_PAGE,
COLUMNS_PER_PAGE,
"The First " + NUMBER_OF_PRIMES +
" Prime Numbers");
tablePrinter.print(primes);
}
}
[RowColumnPagePrinter.java]
import java.io.PrintStream;
public class RowColumnPagePrinter {
private int rowsPerPage;
private int columnPerPage;
private int numbersPerPage;
private String pageHeader;
private PrintStream printStream;
public RowColumnPagePrinter(int rowsPerPage, int columnPerPage, String pageHeader) {
this.rowsPerPage = rowsPerPage;
this.columnPerPage = columnPerPage;
this.pageHeader = pageHeader;
numbersPerPage = rowsPerPage * columnPerPage;
printStream = System.out;
}
public void print(int data[]) {
int pageNumber = 1;
for (int firstIndexOnPage = 0;
firstIndexOnPage < data.length;
firstIndexOnPage += numbersPerPage) {
int lastIndexOnPage =
Math.min(firstIndexOnPage + numbersPerPage - 1,
data.length - 1);
printPageHeader(pageHeader, pageNumber);
printPage(firstIndexOnPage, lastIndexOnPage, data);
printStream.println("\f");
pageNumber++;
}
}
private void printPage(int firstIndexOnPage,
int lastIndexOnPage,
int[] data) {
int firstIndexOfLastOrwOnPage =
firstIndexOnPage + rowsPerPage - 1;
for (int firstIndexInRow = firstIndexOnPage;
firstIndexInRow <= firstIndexOfLastOrwOnPage;
firstIndexInRow++) {
printRow(firstIndexInRow, lastIndexOnPage, data);
printStream.println("");
}
}
private void printRow(int firstIndexOnRow,
int lastIndexOnPage,
int[] data) {
for (int column = 0; column < columnPerPage; column++) {
int index = firstIndexOnRow + column * rowsPerPage;
if (index <= lastIndexOnPage)
printStream.format("%10d", data[index]);
}
}
private void printPageHeader(String pageHeader,
int pageNumber) {
printStream.println(pageHeader + " --- Page " + pageNumber);
printStream.println("");
}
public void setOutput(PrintStream printStream) {
this.printStream = printStream;
}
}
[PrimeGenerator.java]
import java.util.ArrayList;
public class PrimeGenerator {
private static int[] primes;
private static ArrayList<Integer> multiplesOfPrimeFactors;
protected static int[] generate(int n) {
primes = new int[n];
multiplesOfPrimeFactors = new ArrayList<Integer>();
set2AsFirstPrime();
checkOddNumbersForSubsequentPrimes();
return primes;
}
private static void set2AsFirstPrime() {
primes[0] = 2;
multiplesOfPrimeFactors.add(2);
}
private static void checkOddNumbersForSubsequentPrimes() {
int primeIndex = 1;
for (int candidate = 3;
primeIndex < primes.length;
candidate += 2) {
if (isPrime(candidate))
primes[primeIndex++] = candidate;
}
}
private static boolean isPrime(int candidate) {
if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) {
multiplesOfPrimeFactors.add(candidate);
return false;
}
return isNotMultipleOfAnyPreviousPrimeFactor(candidate);
}
private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) {
int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()];
int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor;
return candidate == leastRelevantMultiple;
}
private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) {
for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) {
if (isMultipleOfNthPrimeFactor(candidate, n))
return false;
}
return true;
}
private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) {
return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n);
}
private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) {
int multiple = multiplesOfPrimeFactors.get(n);
while (multiple < candidate)
multiple += 2 * primes[n];
multiplesOfPrimeFactors.set(n, multiple);
return multiple;
}
}
💡 가장 눈에 띄는 변화는, 프로그램이 길어졌다는 것이다.
그럼 길이가 늘어난 이유가 뭘까?
- 리팩터링한 프로그램은 좀 더 길고 서술적인 변수 이름을 사용한다.
- 리팩터링한 프로그램은 코드에 주석을 추가하는 수단으로, 함수 선언과 클래스 선언을 활용한다.
- 가독성을 높이고자 공백을 추가하고 형식을 맞추었다.
😇 원래 프로그램은 세 가지 책임으로 나눠졌다.
- PrimePrinter 클래스
- main 함수 하나만 포함
- 실행 환경을 책임짐
- 호출방식이 달라지면 클래스도 바뀐다.
- RowColumnPagePrinter 클래스
- 숫자 목록을 주어진 행과 열에 맞춰 페이지에 출력하는 방법을 안다.
- 출력하는 모양새를 바꾸려면 RowColumnPagePrinter 클래스를 고쳐준다.
- PrimeGenerator 클래스
- 소수 목록을 생성하는 방법을 안다.
- 객체로 인스턴스화하는 클래스가 아니다.
- 단순히 변수를 선언하고 감추려고 사용하는 공간일 뿐이다.
- 소수를 계산하는 알고리즘이 바뀐다면, PrimeGenerator 클래스를 고쳐준다.
⭐ 이것들은 재구현이 아니다!
프로그램을 처음부터 다시 짜지 않았다.
실제로 두 프로그램을 비교해 보면, 알고리즘과 동작 원리가 동일하다.
가장 먼저,
- 원래 프로그램의 정확한 동작을 검증하는 테스트 슈트를 작성했다.
- 그런 다음, 한 번에 하나씩 수 차례에 걸쳐 조금씩 코드를 변경했다.
- 코드를 변경할 때마다 테스트를 수행해 원래 프로그램과 동일하게 동작하는지 확인했다.
- 조금씩 원래 프로그램을 정리한 결과 최종 프로그램이 얻어졌다.
✔️ 변경하기 쉬운 클래스
대다수의 시스템은 지속적인 변경이 가해진다.
그리고 뭔가 변경할 때마다 시스템이 의도대로 동작하지 않을 위험이 따른다.
💡 깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다.
객체 지향 설계에서 OCP(Open-Closed Principle)이란 무엇일까?
OCP란, 클래스는 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙이다.
예를 들어, 어떤 클래스는 파생 클래스를 생성하는 방식으로,
새 기능에 개방적인 동시에 다른 클래스를 닫아놓는 방식으로 수정에 폐쇄적인 경우이다.
새 기능을 수정하거나 기존 기능을 변경할 때 건들리 코드가 최소인 시스템 구조가 바람직하다.
💡 이상적인 시스템이라면 새 기능을 추가할 때, 시스템을 확장할 뿐 기존 코드를 변경하지는 않는다.
✔️ 결합도가 낮다는 것은?
결합도가 낮다는 소리는
각 시스템 요소가 다른 요소로부터 그리고 변경으로부터
잘 격리되어 있다는 의미다.
💡 시스템 요소가 서로 잘 격리되어 있으면, 각 요소를 이해하기도 더 쉬워진다.
❗ 이렇게 결합도를 최소로 줄이면, 자연스럽게
또 다른 클래스 설계 원칙인 DIP(Dependency Inversion Principle)를 따르는 클래스가 나온다.
→ 본질적으로 DIP는 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.
예를 들어,
상세한 구현 클래스가 아닌, 인터페이스에 의존한다.
그 인터페이스는 어떠한 기능에 대한 추상적인 개념을 표현한다.
→ 이와 같은 추상화로 실제 기능 같은 구체적인 사실을 모두 숨긴다.
참고: Clean Code 클린 코드 애자일 소프트웨어 장인 정신 / 로버트 C. 마틴 저
'객제지향, TDD, 클린코드 > 클린코드' 카테고리의 다른 글
객체와 자료 구조 (0) | 2023.04.19 |
---|---|
경계 & 일급 컬렉션이란? (0) | 2023.03.24 |
깨끗한 테스트 코드 유지하기 (0) | 2023.03.17 |
함수는 한 가지만 해라 (0) | 2023.03.16 |
오류 코드보다 예외를 사용하라 (0) | 2023.03.08 |