01. 깨끗한 코드
우리는 온갖 이유를 들이댄다.
원래 설계를 뒤집는 방향으로 요구사항이 변했다고 불평한다.
일정이 촉박해 제대로 할 시간이 없었다고 한탄한다.
멍청한 관리자와 조급한 고객과 쓸모없는 마케팅 부서와
전화기 살균제 탓이라며 떠벌인다.
하지만 딜버트씨, 잘못은 전적으로 우리 프로그래머에게 있답니다.
우리가 전문가답지 못했기 때문입니다. _P.6
비유를 하나 들겠다. 자신이 의사라 가정하자.
어느 환자가 수술 전에 손을 씻지 말라고 요구한다.
시간이 너무 걸리니까. 확실히 환자는 상사다.
하지만 의사는 단호하게 거부한다.
왜? 질병과 감염의 위험은 환자보다 의사가 더 잘 아니까.
환자 말을 그대로 따르는 행동은 (범죄일 뿐만 아니라) 전문가 답지 못하니까.
프로그래머도 마찬가지다.
나쁜 코드의 위험을 이해하지 못하는 관리자 말을
그대로 따르는 행동은 전문가답지 못하다. _P.7
비야네 스트롭스트룹
나는 우아하고 효율적인 코드를 좋아한다.
논리가 간단해야 버그가 숨어들지 못한다.
의존성을 최대한 줄여야 유지보수가 쉬워진다.
오류는 명백한 전략에 의거해 철저히 처리한다.
성능을 최적으로 유지해야 사람들이 원칙 없는
최적화로 코드를 망치려는 유혹에 빠지지 않는다.
깨끗한 코드는 한 가지를 제대로 한다. _P.9
나쁜 코드
- 성능이 나쁜 코드
- 불필요한 연산이 들어가서 개선의 여지가 있는 코드
- 의미가 모호한 코드
- 네이밍과 그 내용이 다른 코드
- 중복된 코드
- 비슷한 내용인데 중복되는 코드들은 버그를 낳는다.
매번 얽히고설킨 코드를 해독해서 얽히고설킨 코드를 더한다.
나쁜 코드가 쌓일수록 팀 생산성은 떨어진다.
02. 의미 있는 이름
의도를 분명히 밝혀라
주석이 필요하다면 의도를 분명히 드러내지 못했다는 말이다.
int d; //경과 시간 (단위: 날짜)
이름 d는 아무 의미도 드러나지 않는다.
경과 시간이나 날짜라는 느낌이 안 든다. 측정하려는 값과 단위를 표현하는 이름이 필요하다.
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
의도가 드러나는 이름을 사용하면 코드 이해와 변경이 쉬워진다.
코드 짐작하기 어려운 코드
Public List<int []> getThem() {
List<int []> list1 = new ArrayList<int []>();
for(int[] x: theList)
if (x[0] === 4)
list.add(x);
return list1;
}
- theList에 무엇이 들었는가?
- theList에서 0번째 값이 어째서 중요한가?
- 값 4는 무슨 의미인가?
- 함수가 반환하는 list1을 어떻게 사용하는가?
개선 방안
- 정보를 드러내자.
- 지뢰 찾기 게임을 만든다고 가정 theList 가 게임판이라는 사실을 안다.
theList → gameBoard로 바꾸자. - 게임판에서 각 칸은 단순 배열로 표현
- 배열에서 0번째 값은 칸 상태를 뜻한다.
- 값 4는 깃발이 꽂힌 상태를 가리킨다.
- 각 개념에 이름만 붙여도 코드가 상당이 나아진다.
개선 코드
Public List<int []> getFlaggedCells() {
List<int []> flaggedCells= new ArrayList<int []>();
for(int[] x: gameBoard)
if (cell[STATUS_VALUE === FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
개선 코드 - int 배열을 사용하는 대신, 칸을 간단한 클래스로 만들어도 되겠다.
isFlagged라는 좀 더 명시적인 함수를 사용해 FLAGGED라는 상수를 감춰도 좋다.
Public List<int []> getFlaggedCells() {
List<Cell> flaggedCells= new ArrayList<Cell>();
for(int[] x: gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
단순히 이름만 고쳤는데 함수가 하는 일을 이해하기 쉬워졌다.
바로 이것이 좋은 이름이 주는 위력이다.
발음하기 쉬운 이름을 사용하라
- 나쁜 예 ) DtaRcrd102
- 좋은 예) Customer
검색하기 쉬운 이름을 사용하라
- 숫자 7은 은근히 까다롭다
- e라는 문자도 변수 이름으로 적합하지 못하다.
이름 길이는 범위 크기에 비례해야 한다.
자신의 기억력을 자랑하지 마라
문자 하나만 사용하는 변수 이름은 문제가 있다.
루프에서 반복 횟수를 세는 변수 i, j, k는 괜찮다. (l은 절대 안 된다.)
단, 루프 범위가 아주 작고 다른 이름과 충돌하지 않을 때만 괜찮다.
루프에서 반복 횟수 변수는 전통적으로 한 글자를 사용하기 때문이다.
그 외에는 대부분 적절하지 못하다.
클래스 이름
클래스 이름과 객체 이름은 명사나 명사구가 적합하다.
메서드 이름
메서드 이름은 동사나 동사구가 적합하다.
변수명에 타입 넣지 않기
String nameString(👎) -> name
int itemPriceAmount(👎) -> itemPrice
Account[] accountArray(👎) -> accounts
List<Account> accountList(👌) -> accounts, accountList
Map<Account> accountMap(👌)
public interface IShapeFactory(👎) -> ShapeFactory
// 옛날 코드에서 많이 사용 -> 접두어 I는 주의를 흐트리고 (나쁘게는)과도한 정보를 제공한다.
public class ShapeFactoryImpl(🔺) -> CircleFactory
// Impl을 붙여서 구현클레스라고 명시해주는 방법을 사용, 구현클레스의 정확한 이름으로 명시해주는게 좋을 수도 있다. (팀에서 정한 룰을 따르는게 제일 좋다.)
한 개념에 한 단어를 사용하라
똑같은 메서드를 클래스마다 fetch, retrieve, get으로 제각가 부르면 혼란스럽다.
03. 함수
작게 만들어라
함수를 만드는 첫째 규칙은 작게다.
함수를 만드는 둘째 규칙은 더 작게! 다.
한 가지만 해라.
함수는 한 가지를 해야 한다. 그 한 가지를 잘해야 한다. 그 한 가지만을 해야 한다.
위에서 아래로 코드 읽기: 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
한 함수 다음에는 추상화 수준이 한 단계 낮은 한수가 온다.
즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계식 낮아진다.
나는 이것을 내려가기 규칙이라 부른다.
다르기 표현하면 일련의 TO 문단을 읽듯이 프로그램이 읽혀야 한다는 의미다.
서술적인 이름을 사용하라
testableHtml → setupTeardownIncluder
한수가 하는 일을 좀 더 잘 표현하므로 오른쪽 이름이 훨씬 좋은 이름이다.
함수이름에 키워드를 추가하는 형식 쓰자
함수 이름에 인수 이름을 넣는다.
예를 들어, asertEquals 보다 assertExpectedEqualsActual(expeted, actual)이 더 좋다.
try/Catch 블록 뽑아내기
try/Catch 블록을 별도 함수로 뽑아내는 편이 좋다.
public void delete (Page page) {
try {
deletePageAndAllReferences(page);
}
catch {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey (page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
반복하지 마라
어쩌면 중복은 소프트웨어에서 모든 악의 근원이다.
04. 주석
나쁜 코드에 주석을 달지 마라. 새로 짜라
- 브라이언 w. 커니핸, P.J. 플라우거
- 불행하게도 주석이 언제나 코드를 따라가지 않는다.
- 주석은 언제나 실패를 의미한다.
- 주석은 나쁜 코드를 보완하지 못한다.
조각이 나뉘고 갈라지고 합쳐지면서 괴물로 변한다. 코드를 깔끔하게 정리하고 표현력을 강화하는 방향으로, 그래서 애초에 주석이 필요 없는 방향으로 에너지를 쏟겠다.
코드로 의도를 표현하라
나쁜 예
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if((employee.flags & HOURLY_FLAG) && employee.age > 65)
- 주석은 나쁜 코드를 보완하지 못한다.
- 주석을 추가하는 일반적인 이유는 코드 품질이 나쁘기 때문이다.
- 표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가, 복작하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋다.
- 자신이 저지른 난장판을 주석으로 설명하려 애쓰는 대신에 그 난장판을 깨끗이 치우는데 시간을 보내라.
좋은 예
// 의미있는 이름을 지으면 해결이 된다.
if(employee.isEligibleForFullBenefits())
좋은 주석
정규표현식이 시각과 날짜를 뜻한다고 설명한다.
// kk:mm:ss EEE, MMM dd, yyyy 형식
Pattern timeFormat =
Pattern.compile("\\\\d*:\\\\d:\\\\d* \\\\w*, \\\\w* \\\\d \\\\d*");
구체적으로 주어진 형식 문자열을 사용해 SimpleDateFormat.format 함수가 반환하는 시각과 날짜를 뜻한다. 이왕이면 시각과 날짜를 변환하는 클래스를 만들어 코드를 옮겨주면 더 좋고 더 깔끔하겠다. 그럼 주석이 필요 없어진다.
의미를 명료하게 밝히는 주석
인수나 반환값이 표준 라이브러리나 변경하지 못하는 코드에 속한다면 의미를 명료하게 밝히는 주석이 유용하다.
결과를 경고하는 주석
다른 프로그래머에게 결과를 경고할 목적으로 주석을 사용한다.
함수나 변수로 표현할 수 있다면 주석을 달지 마라
나쁜 예
// 전역 목록 <smodule>에 속하는 모듈이 우리가 속한 하위 시스템에 의존하는가?
if (smodule.getDependSubsystmes().contains(subSysMod.getSubSystem()))
좋은 예
ArrayList moduleDependees = ssmodule.getDependSubsystmes();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees .contains(ourSubSystem))
주석이 필요하지 않도록 코드를 개선하는 편이 좋다.
닫는 괄호에 다는 주석
닫는 괄호에 주석을 달아야겠다는 생각이 든다면 대신에 함수를 줄이려 시도하자.
전역 정보
시스템의 전반적인 정보를 기술하지 마라.
05. 형식 맞추기
수직거리
서로 밀접한 개념은 세로로 가까이 둬야 한다.
변수 선언
변수는 사용하는 위치에 최대한 가까이 선언한다.
인스턴스 변수
- 인스턴스 변수는 클래스 맨 처음에 선언한다.
- 변수 간에 세로로 거리를 두지 않는다.
- 인스턴스 변수를 모아둔다.
- 클래스는 많은 (혹은 대다수) 클래스 메서드가 인스턴스 변수를 사용하기 때문이다.
- 일반적으로 C++에서는 모든 인스턴스 변수를 클래스 마지막에 선언하는 소위 가위규칙을 적용한다.
- 클래스 맨 처음에 선언한다. (Java)
- 클래스 마지막에 선언한다. = 가위 규칙 (C++)
종속 함수
- 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다.
- 가능한 호출하는 함수를 호출되는 함수보다 먼저 배치한다.
- 방금 호출한 함수가 잠시 후 정의되리라는 사실을 예측할 수 있다.
개념적 유사성
- 개념적인 친화도가 높을수록 코드를 가까이 배치한다.
- 친화도가 높은 요인
- 한 함수가 다른 함수를 호출해 생기는 직접적인 종속성
- 변수와 그 변수를 사용하는 함수
- 비슷한 동작을 수행하는 일군의 함수
세로 순서
- 함수 호출 종속성은 아래 방향으로 유지한다.
- 호출되는 함수를 호출하는 함수보다 나중에 배치한다. (파스칼, c, c++와 정확히 반대다.)
- 소스 코드 모듈이 고차원 -> 저차원으로 자연스럽게 내려간다.
- 세세한 사항은 가장 마지막에 표현한다.
가로 공백과 밀집도
private void veasureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
line.addLine(lineSize, lineCount);
recordLine(lineSize);
}
---
public double root(int a, int b, int c) {
double determinant = determinant(a, b, c);
return (-b - Math.sqrt(determinant)) / (2*a);
}
- 함수 이름과 이어지는 괄호 사이에는 공백을 넣지 않는다.
- 함수와 인수는 서로 밀접하기 때문이다.
- 공백을 넣으면 한 개념이 아니라 별개로 보인다.
- 함수를 호출하는 코드에서 괄호 안 인수는 공백으로 분리했다.
- 쉼표를 강조해 인수가 별개라는 사실을 보여주기 위해서이다.
수식 정렬
- 승수 사이는 공백이 없다.
- (-b + Math.sqrt(determinant)) / (2*a); b*b - 4*a*c;
- 곱셈은 우선순위가 가장 높다.
- 우선순위 곱셈 > 덧셈과 뺄셈
밥 아저씨의 형식 규칙
끝으로 이 책의 저자가 사용하는 규칙이 여실히 드러나는 코드를 첨부하며 턴을 종료한다.
public class CodeAnalyzer implements JavaFileAnalysis {
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;
public CodeAnalyzer() {
lineWidthHistogram = new LineWidthHistogram();
}
public static List<File> findJavaFiles(File parentDirectory) {
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}
private static void findJavaFiles(File parentDirectory, List<File> files) {
for (File file : parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}
public void analyzeFile(File javaFile) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(javaFile));
String line;
while ((line = br.readLine()) != null)
measureLine(line);
}
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
private void recordWidestLine(int lineSize) {
if (lineSize > maxLineWidth) {
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}
public int getLineCount() {
return lineCount;
}
public int getMaxLineWidth() {
return maxLineWidth;
}
public int getWidestLineNumber() {
return widestLineNumber;
}
public LineWidthHistogram getLineWidthHistogram() {
return lineWidthHistogram;
}
public double getMeanLineWidth() {
return (double)totalChars/lineCount;
}
public int getMedianLineWidth() {
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount/2)
return width;
}
throw new Error("Cannot get here");
}
private int lineCountForWidth(int width) {
return lineWidthHistogram.getLinesforWidth(width).size();
}
private Integer[] getSortedWidths() {
Set<Integer> widths = lineWidthHistogram.getWidths();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}
06. 객체와 자료 구조
자료구조 vs 객체
자료구조(Data Structure) | 객체(Object) |
데이터 그 자체 | 비즈니스 로직 |
자료를 공개한다. | 자료를 숨기고, 추상화 한다. |
자료를 다루는 함수만 공개한다. | - |
변수 사이에조회 함수와 설정 함수로 변수를 다룬다고 객제가 되지 않는다. (getter, setter) | 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있다. |
객체 지향 vs 절차지향
객체지향에서 어려운 변경 코드는 절차 지향 코드에서 쉬우며
절차 지향에서 어려운 변경 코드는 객체 지향에서 쉽다.
객체 지향
- 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다.
- 객체지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
- 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.
절차지향
- 새로운 동작을 추가하는 유연성이 필요하면 자료 구조, 절차적인 코드가 더 적합하다.
- 절차적인 코드는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다.
- 자료구조를 사용하는 절차적인 코드는 기본 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
상황에 맞게 사용
// 이것보다
public class Square implements Shape {
public double area() {
//
}
}
public class Circle implements Shape {
public double area() {
//
}
}
// 이게 더 나을수도 있다
public class Geometry {
public double area(Object shape) {
if (shape instanceof Square) {
//
} else if (shape instanceof Circle) {
//
}
}
}
절차 지향 언어라고 해서 절차 지향적인 방법을 사용하지 않고 상황에 따라 선택하여 절차 지향, 객체 지향적인 방법을 사용한다.
디미터 법칙 - 객체
📖 디미터 법칙 관련 책
“Object-Oriented Programming: An Objective Sense of Style”
- Demeter라는 프로젝트를 진행하던 개발자들은 어떤 객체가 다른 객체에 대해 지나치게 많이 알다 보니, 결합도가 높아지고 좋지 못한 설계를 야기한다는 것을 발견했다.
- 개선하고자 객체에게 자료를 숨기는 대신 함수를 공개하도록 하였는데, 이것이 바로 디미터의 법칙이다.
- 디미터의 법칙은 다른 객체가 어떠한 자료를 갖고 있는지 속사정을 몰라야 한다는 것을 의미이다.
디미터의 법칙 예시 [ 디미터의 법칙을 위반하는 코드 ]
서울에 살고 있는 어떤 사용자에게 알림을 보내주는 함수를 구현
@Getter
public class User {
private String email;
private String name;
private Address address;
}
@Getter public class Address {
private String region;
private String details;
}
어떤 사용자가 서울에 살고 있으면 알림을 보내주는 함수
@Service public class NotificationService {
public void sendMessageForSeoulUser(final User user) {
if("서울".equals(user.getAddress().getRegion())) {
sendNotification(user);
}
}
}
위 코드는 디마터의 법칙을 위반
- 객체 지향적인 방법이 아니다.
- 객체에게 메시지를 보내는 것이 아니라 객체가 가지는 자료를 확인하고 있으며, 다른 객체가 어떠한 자료를 갖고 있는지 지나치게 잘 알고 있다.
- Getter 메서드를 통해 user 객체가 email, name, address를 가지고 있음을 파악할 수 있다
[ 디미터의 법칙은 준수하는 코드 ]
public class Address {
private String region;
private String details;
public boolean isSeoulRegion() {
return "서울".equals(region);
}
}
public class User {
private String email;
private String name;
private Address address;
public boolean isSeoulUser() {
return address.isSeoulRegion();
}
}
- Address 객체의 데이터를 통해 사용자의 지역을 파악하지 않는다.
- Address의 객체에 메시지를 보내서 서울 지역에 사는지 파악하도록 구현한다.
- 위 코드와 같이 객체에게 보내는 메시지를 구현하면 불필요한 @Getter 들을 지울 수 있다.
- User 객체와 Adress 객체가 어떠한 데이터들을 지니고 있는지 모른 채 메시지를 보낼 수 있다.
기존의 알림을 보내는 로직을 다음과 수정할 수 있다.
@Service public class NotificationService {
public void sendMessageForSeoulUser(final User user) {
if (user.isSeoulUser()) {
sendNotification(user);
}
}
}
- 기존의 user.getAddress().getRegion()처럼 여러 개의 .(도트)을 사용하여 참조하지 않기 때문에 디미터의 법칙을 잘 준수하고 있다.
.(도트)를 사용하도록 강제하는 것이 아니라는 점
예를 들어 Stream과 같은 API를 사용하는 경우 여러 개의 도트가 사용된다.
public List < User > getSeoulUserList() {
final List < User > userList = userRepository.findAll();
return userList.stream()
.filter(this:: isSeoulUser)
.collect(toList();
}
- 디미터의 법칙은 결합도와 관련된 것이며, 객체의 내부 구조가 외부로 노출되는지에 대한 것이다.
- Stream API 같은 경우에는 동일한 Stream으로 변환하여 반환할 뿐, 캡슐화는 그대로 유지되므로 문제가 없다.
- 만약 여러 .(도트)가 사용되더라도 객체의 내부 구현이 노출되지 않는다면 그것은 디미터의 법칙을 준수하는 코드이다.
- 또한 DTO 나 컬렉션 객체와 같은 자료 구조의 경우에는 물을 수밖에 없다. 만약 묻는 대상이 객체가 아닌 자료구조라면 당연히 내부를 노출해야 하므로 디미터의 법칙을 적용할 필요가 없다.
기차 충돌 - 디미터의 법칙에 어긋나는 상황
getter가 줄줄이 이어진 모습이 기차와 닮아서 열차 전복, 기차 충돌(train wreck)이라는 단어로 표현하기도 한다.
object.getChild().getContent().getItem().getTitle()
프로그램 순회 경로가 길수록 불안정하기 때문에 위의 방식은 피하도록 한다.
07. 오류 처리
- 오류 코드보다 예외를 사용하라
- 미확인 예외를 사용하라
- 확인된 예외는 상위 선언부에서 모두 선언해주어야 하기 때문이다 → OCP 위반
- OCP(개방 폐쇄 원칙) 위배 상위 레벨 메소드에서 하위 레벨 메소드의 디테일에 대해 알아야 하기 때문에 OCP 원칙에 위배된다.
- 예외에 의미를 제공하라
- 호출자를 고려해 예외 클래스를 정의하라
- 외부 라이브러리를 사용할 때에는 라이브러리용 클래스를 고려해보는 것이 좋다.
- null을 반환하지 마라
- null 체크 지옥이 시작된다.
08. 경계
코드 경계
Sensor라는 값을 관리해야 하는 상황(Sensor는 외부에서 사용된다.)
- Sensor Id와 Sensor 객체로 저장하고 싶어서, Map을 사용한다.
- 하지만 Map을 그대로 사용하면 Map이 가진 clear() 및 다른 불필요한 메서드가 외부로 노출되어서 누군가 사용할 수 있다.
- Sensor의 ‘외부’ 코드의 관점에서 Sensor 객체의 값들만 가져오도록 하기 위해서 캡슐화를 진행한다.
안 좋은 예
Map<Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId);
- 위처럼 Map을 직접 사용하게 되면 Map 인터페이스가 제공하는 clear 등 불필요한 기능이 노출된다.
- 외부 코드가 함부로 호출하면 sensor 데이터가 손상될 수 있고, 이는 우리 의도와 벗어난다.
좋은 예
public class Sensor {
private Map<Sensor> sensors = new HashMap<Sensor>();
public Sensor getById(String sensorId) {
return sensors.get(sensorId);
}
}
- 캡슐화를 통해서 Map을 감춘다.
- 원하는 기능만 제공하도록 한다.
- 적절한 경계로 우리 코드를 보호할 수 있다.
09. 단위 테스트
- 단위테스트 : 테스트는 유연성, 유지보수성, 재사용성을 제공한다.
- 가독성은 실제 코드보다 테스트 코드에 더더욱 중요하다.
- 명료성, 단순성, 풍부한 표현력이 필요하다.
- 테스트 코드는 본론에 돌입해 진짜 필요한 자료 유형과 함수만 사용한다.
- 테스트 당 assert 하나
- JUnit으로 테스트 코드를 짤때는 함수마다 assert 문을 단 하나만 사용한다.
- assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기가 쉽고 빠르다.
- 단, assert 문을 여럿 사용하는 편이 좋을 경우 사용하기.
- 테스트당 개념 하나
- 테스트 함수마다 한 개념만 테스트하기
F.I.R.S.T
- 빠르게 (Fast)
- 테스트는 빨라야 한다.
- 느리면 자주 돌릴 엄두를 못낸다.
- 자주 돌리지 않으면 초반에 문제를 찾기 힘들다.
- 독립적으로 (Independent)
- 테스트는 서로 의존하면 안 된다.
- 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다.
- 각 테스트는 독립적이어야 한다.
- 반복가능하게 (Repeatable)
- 어떤 환경에서도 반복 가능해야 한다.
- 테스트를 어느 환경에서든 돌릴 환경이어야 한다.
- 자가검증하는 (Self-Validating)
- 부울값으로 결과를 내야 한다.
- 수작업으로 비교해서 안된다.
- 적시에 (Timely)
- 실제 코드를 구현하기 직전에 구현한다.
- 실제 코드보다 먼저 테스트 코드를 작성해야 실제 코드가 테스트하기 어렵다는 사실을 알게 될 수 있다.
결론
사실상 테스트 코드 주제는 책 한 권을 할애해도 모자라다.
테스트 코드는 실제 코드만큼이나 중요하다.
10. 클래스
클래스 체계
클래스를 정의하는 표준 자바 관례에 따르면, 변수 목록 다음에는 공개 함수가 나온다. 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣는다. 즉, 추상화 단계가 순차적으로 내려간다. 그래서 프로그램은 신문 기사처럼 읽힌다.
캡슐화
변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다. 때로는 변수나 유틸리티 함수를 protected로 선언해 테스트 코드에 접근을 허용하기도 한다. 그전에 비공개 상태를 유지할 온갖 밥법을 강구한다. 캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.
클래스는 작아야 한다.
- 작게 가 기본 규칙
- 메서드 다섯 개면 작은 것인가? 메서드 수가 작음에도 불구하고 책임이 너무 많으면 안 된다.
- 클래스 이름은 해당 클래스 책임을 기술해야 한다.
단일 책임 원칙
- 클래스는 책임, 즉 변경할 이유가 하나여야 한다는 의미이다.
- 복잡성을 다루려면 체계적인 정리가 필수다.
- 큰 클래스 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.
응집도
- 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다.
- 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다.
- 응집도가 높다는 말은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미이기 때문이다.
- 응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개준다.
응집도를 유지하면 작은 클래스 여럿이 나온다.
클래스가 응집력을 잃는다면 쪼개라!
- 리팩터링한 프로그램은 좀 더 길고 서술적인 변수 이름을 사용한다.
- 리팩터링한 프로그램은 코드에 주석을 추가하는 수단으로 함수 선언과 클래스 선언을 활용한다.
- 가독성을 높이고자 공백을 추가하는 형식을 맞추었다.
변경하기 쉬은 클래스
어떤 변경이든 클래스에 손대면 다른 코드를 망가뜨릴 잠정적인 위험이 존재한다. 그래서 테스트도 완전히 다시 해야 한다.
OCP란 클래스는 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙이다.
새 기능을 추가할 때 시스템을 확장할 뿐 기존 코드를 변경하지 않는다.
변경으로부터 격리
테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다.
결합도가 낮다는 소리는 각 시스템 요소가 다른 요소로부터 그리고 변경으로부터 잘 격리되어 있다는 의미다.
DIP는 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.
'IT Book' 카테고리의 다른 글
IT 좀 아는 사람 : 비전공자도 IT 전문가처럼 생각하는 법 (0) | 2023.02.03 |
---|