훌륭한 코드는 짧고 단순하고 대칭을 이룬다. 문제는 어떻게 그렇게 하느냐다. - 숀 파렌트
매직 넘버는 표면상 의미 없는 숫자이지만 프로그램의 동작을 제어한다.
하지만 매직 넘버를 사용하면 코드를 이해하기 어렵고 오류가 발생하기 쉬워진다.
class CruiseControl {
private double targetSpeedKmh;
void setPreset(int speedPreset) {
if (speedPreset == 2) {
setTargetSpeedKmh(16944);
} else if (speedPreset == 1) {
setTargetSpeedKmh(7667);
} else if (speedPreset == 0) {
setTargetSpeedKmh(0);
}
}
void setTargetSpeedKmh(double speed) {
targetSpeedKmh = speed;
}
}
setPreset()
메서드를 정수와 함깨 호출하려면 메서드 내부에 대한 이해가 필수로 요구된다.speedPreset
과 대응하는 실제 targetSpeedKmh
도 임의적이다.class CruiseControl {
static final int STOP_PRESET = 0;
static final int PLANETARY_SPEED_PRESET = 1;
static final int CRUISE_SPEED_PRESET = 2;
static final double CRUISE_SPEED_KMH = 16944;
static final double PLANETARY_SPEED_KMH = 7667;
static final double STOP_SPEED_KMH = 0;
private double targetSpeedKmh;
void setPreset(int speedPreset) {
if (speedPreset == CRUISE_SPEED_PRESET) {
setTargetSpeedKmh(CRUISE_SPEED_KMH);
} else if (speedPreset == PLANETARY_SPEED_PRESET) {
setTargetSpeedKmh(PLANETARY_SPEED_KMH);
} else if (speedPreset == STOP_PRESET) {
setTargetSpeedKmh(STOP_SPEED_KMH);
}
}
void setTargetSpeedKmh(double speed) {
targetSpeedKmh = speed;
}
}
static final
로 정의한 상수를 사용하면 코드를 이해하기 쉬워진다.옵션을 모두 열거할 수 있다면 자바 타입 시스템이 제공하는 방법이 더 낫다.
class CruiseControl {
private double targetSpeedKmh;
void setPreset(SpeedPreset speedPreset) {
Objects.requireNonNull(speedPreset);
setTargetSpeedKmh(speedPreset.speedKmh);
}
void setTargetSpeedKmh(double speedKmh) {
targetSpeedKmh = speedKmh;
}
}
enum SpeedPreset {
STOP(0), PLANETARY_SPEED(7667), CRUISE_SPEED(16944);
final double speedKmh;
SpeedPreset(double speedKmh) {
this.speedKmh = speedKmh;
}
}
enum
타입은 가능한 옵션을 열거하는 데 사용한다.setPreset()
메서드로 넣을 수 없게 되었다. (알아도 컴파일러가 중지시킴)if
~else if
문을 제거할 수 있다.class LaunchChecklist {
List<String> checks = Arrays.asList("Cabin Pressure", "Communication", "Engine");
Status prepareForTakeoff(Commander commander) {
for (int i = 0; i < checks.size(); i++) {
boolean shouldAbortTakeoff = commander.isFailing(checks.get(i));
if (shouldAbortTakeoff) {
return Status.ABORT_TAKE_OFF;
}
}
return Status.READY_FOR_TAKE_OFF;
}
}
protected
가 아니기 때문에 언제든 덮어쓸 수 있는 문제가 있다.<
인지 <=
인지에 따라 IndexOutOfBoundsExceptions
가 일어날 수 있다.인덱스 변수가 제공하는 정보를 자세히 알아야 하는 경우는 드물다.
이럴 때에는 세부 순회 내용은 보호할 수 없어도 프로그래머에게는 숨기는 식으로 작성해야 한다.
class LaunchChecklist {
List<String> checks = Arrays.asList("Cabin Pressure", "Communication", "Engine");
Status prepareForTakeoff(Commander commander) {
for (String check : checks) { // 변경
boolean shouldAbortTakeoff = commander.isFailing(check);
if (shouldAbortTakeoff) {
return Status.ABORT_TAKE_OFF;
}
}
return Status.READY_FOR_TAKE_OFF;
}
}
for-each
문을 사용하면 반복 인덱스를 더 이상 다루지 않아도 된다.check
에 할당한다.Set
등과 같이 인덱싱되지 않은 컬렉션에서도 잘 동작한다.또 다른 순회 매커니즘은 순회하며 컬렉션 수정하지 않기처럼 반복자(iterator)를 사용하는 방법이 있다.
ConcurrentModificationException
예외가 발생한다.List
인터페이스의 표준 구현 Set
, Queue
와 같은 Collection
인터페이스의 구현 등단일 스레드 애플리케이션에서
ConcurrentModificationException
이라는 이름이 낯설게 느껴질 수 있다.
여기서의 동시(concurrency) 실행은Collection
을 순회하는 동안 그 컬렉션을 수정한다는 의미이다.
참고로ConcurrentModificationException
은 자바 컴파일 시점에 잡지 못한다.
class Inventory {
private List<Supply> supplies = new ArrayList<>();
void disposeContaminatedSupplies() {
// 이터레이터 사용
Iterator<Supply> iterator = supplies.iterator();
while (iterator.hasNext()) {
if (iterator.next().isContaminated()) {
iterator.remove();
}
}
}
}
잘 동작하지만 코드 몇 줄을 더 적어야 하고, 순회하는 동안 타겟이 되는 원소를 저장할 자료 구조가 필요하다.
supplies
컬렉션의 Iterator
를 활용하는 while
루프라는 새로운 순회 방식을 사용한다.hasNext()
로 남은 원소를 확인하고, next()
로 다음 원소를 얻고, 반환된 마지막 원소를 remove()
한다.List
를 직접 수정하지 않더라도 iterator
를 사용하면 순회 중에도 모든 작업을 올바르게 수행한다.
for-each
문도 내부적으로iterator
를 사용한다.
CopyOnWriteArrayList
와 같은 특수List
구현은 순회와 수정을 동시에 수행하기도 한다.
하지만 리스트에 원소를 추가하거나 삭제할 때마다 매번 전체 리스트를 복사해서 시간/공간적 문제가 될 수 있다.
자바 8부터는 람다를 통해
Collection.removeIf()
메서드를 사용할 수 있다.
자료 구조를 순회할 때는 수행할 연산 유형에 주의해야 한다.
class Inventory {
private List<Supply> supplies = new ArrayList<>();
List<Supply> find(String regex) {
List<Supply> result = new LinkedList<>();
for (Supply supply : supplies) {
if (Pattern.matches(regex, supply.toString())) {
result.add(supply);
}
}
return result;
}
}
regex
로 쿼리 문자열을 만든다.자바 API의
java.util.regex.Pattern
클래스는 정규식을 만들고 관련 메서드들을 제공한다.
Pattern
의 정적 메서드인 matches()
를 호출하여 정규식 검색하는 방식이 많이 사용된다.String
의 표현식인 regex
를 가져온 뒤, regex
로부터 특수한 목적의 오토마톤(automaton)을 만든다.오토마톤(automaton)은 패턴을 따르는 문자열만 허용하고, 나머지는 모두 거절한다.
Pattern.matches(regex, supply.toString())
는 오토마톤을 컴파일해서 supply.toString()
과 매칭시켜본다.class Inventory {
private List<Supply> supplies = new ArrayList<>();
List<Supply> find(String regex) {
List<Supply> result = new LinkedList<>();
// 정규식은 메서드 호출 전에 딱 한 번만 컴파일
Pattern pattern = Pattern.compile(regex);
for (Supply supply : supplies) {
if (pattern.matcher(supply.toString()).matches()) {
result.add(supply);
}
}
return result;
}
}
위 코드처럼 메서드를 호출할 때 정규식을 딱 한 번만 컴파일하면 성능 저하를 막을 수 있다.
이렇게 하려면 Pattern.matches()
호출을 두 번의 연산으로 분해해야 한다.
표현식 컴파일
Pattern pattern = Pattern.compile();
컴파일된 표현식 실행
pattern.matcher(supply.toString()).matches()
코드 블록이 서로 붙어 있다면 보통 한 덩어리로 간주한다.
각각의 블록을 새 줄로 분리하면 코드 이해도를 높일 수 있다.
enum DistanceUnit {
MILES, KILOMETERS;
static final int IDENTITY = 1;
static final double MILE_IN_KILOMETERS = 1.60934;
static final double KILOMETER_IN_MILES = 1 / MILE_IN_KILOMETERS;
double getConversionRate(DistanceUnit unit) {
if (this == unit) {
return IDENTITY;
}
if (this == MILES && unit == KILOMETERS) {
return MILE_IN_KILOMETERS;
} else {
return KILOMETER_IN_MILES;
}
}
}
연관된 코드와 개념은 함께 그루핑하고, 서로 다른 그룹은 빈 줄로 각각 분리해야 한다.
IDENTITY
필드를 다른 상수들과 분리했다.getConversionRate()
에서는 두 개의 if
블록을 서로 분리했다.로버트 C. 마틴은 <클린 코드>에서 수직 서식화를 신문에 비유했다.
- 훌륭한 기사는 제목(클래스명)으로 시작한다.
- 그다음으로 섹션 머릿말(공개 멤버/생성자/메서드)이 온다.
- 뒤이어 세부 내용(비공개 메서드)이 위치한다.
긴 문자열을 생성할 때 서식 문자열을 사용하면 더 읽기 쉽게 만들 수 있다.
class Mission {
Logbook logbook;
LocalDate start;
void update(String author, String message) {
final LocalDate today = LocalDate.now();
String entry = String.format(
"%S: [%tm-%<te-%<tY](Day %d)> %s%n",
author, today,ChronoUnit.DAYS.between(start, today) + 1, message);
logbook.write(entry);
}
}
String
레이아웃과 데이터를 분리하는 것이다.%S
, %tm
, %te
, %Y
, %d
, %s
, %n
등은 모두 위치 지정자이다.위치 지정자와 일반 데이터가 복잡하게 뒤섞여 있으면 무엇이 출력되는지 알기 힘들다.
하지만 이 방식은 문서화가 잘 된 표준이자 훌륭한 대안이다.만약 문자열이 길다면 강력한 템플릿 엔진인
StringTemplate
의 사용을 권장한다.
서식화된 문자열은 몇 가지 예제를 들면서 문서화를 해놓는 것이 좋다.
자바 API는 시간이 지나면서 거대해졌고, 빠르고 버그가 거의 없는 표준 라이브러리가 많이 만들어졌다.
class Inventory {
private List<Supply> supplies = new ArrayList<>();
int getQuantity(Supply supply) {
if (supply == null) {
throw new NullPointerException("supply must not be null");
}
int quantity = 0;
for (Supply supplyInStock : supplies) {
if (supply.equals(supplyInStock)) {
quantity++;
}
}
return quantity;
}
}
null
값이 없도록 입력 매개변수를 “직접” 검증하고 있다.for
루프 대신 for-each
루프로 자료 구조를 “직접” 순회하고 있다.class Inventory {
private List<Supply> supplies = new ArrayList<>();
int getQuantity(Supply supply) {
Objects.requireNonNull(supply, "supply must not be null");
return Collections.frequency(supplies, supply);
}
}
Objects
유틸리티 클래스의 requireNonNull()
메서드로 null
검증을 할 수 있다.Collections
는 Collection
내 객체 빈도수를 세는 frequency()
메서드를 제공한다.이 외에도 자바 API는 온갖 유용한 기능을 제공하는 수천 개 클래스를 포함하고 있다.
이 장의 내용은 [자바 코딩의 기술: 똑똑하게 코딩하는 법]의 2장 내용을 정리한 것입니다.