읽기 쉬운 코드 작성은 가장 중요한 의사소통 능력 중 하나이다.
class Laboratory {
Microscope microscope;
Result analyze(Sample sample) {
// if (microscope.isInorganic(sample) == true) {
if (microscope.isInorganic(sample)) {
return Result.INORGANIC;
} else {
return analyzeOrganic(sample);
}
}
private Result analyzeOrganic(Sample sample) {
// if (!microscope.isHumanoid(sample) == false)
if (!microscope.isHumanoid(sample)) {
return Result.ALIEN;
} else {
return Result.HUMANOID;
}
}
}
코드에서는 긍정 표현식이 부정 표현식보다 더 낫다.
class Laboratory {
Microscope microscope;
Result analyze(Sample sample) {
if (microscope.isOrganic(sample)) { // 긍정 표현식으로 변경
return analyzeOrganic(sample);
} else {
return Result.INORGANIC;
}
}
private Result analyzeOrganic(Sample sample) {
if (microscope.isHumanoid(sample)) { // 긍정 표현식으로 변경
return Result.HUMANOID;
} else {
return Result.ALIEN;
}
}
}
if
문으로 감쌀 필요 없이 값을 바로 반환할 수 있다.boolean isValid() {
return missions < 0 || name == null || name.trim().isEmpty();
}
boolean isValid() {
boolean isValidMissions = missions >= 0;
boolean isValidName = name != null && !name.trim().isEmpty();
return isValidMissions && isValidName;
}
조건문 덩어리를 다른 곳에서도 사용해야 한다면 아래 불 표현식 간소화를 참고하자.
훌륭한 그루핑(Grouping)이란 조건의 의미에 따라 좌우된다.
class SpaceShip {
Crew crew;
FuelTank fuelTank;
Hull hull;
Navigator navigator;
OxygenTank oxygenTank;
boolean willCrewSurvive() {
boolean hasEnoughResources = hasEnoughFuel() && hasEnoughOxygen();
return hull.isIntact() && hasEnoughResources;
}
private boolean hasEnoughOxygen() {
return oxygenTank.lastsFor(crew.size) > navigator.timeToEarth();
}
private boolean hasEnoughFuel() {
return fuelTank.fuel >= navigator.requiredFuelToEarth();
}
}
NullPointerException
은 null
을 참조하는 메서드를 호출하거나 속성에 접근할 때 발생한다.NullPointerException
을 막으려면 메서드 인수가 유효한지 검사해야 한다.class Logbook {
void writeMessage(String message, Path location) throws IOException {
if (Files.isDirectory(location)) {
throw new IllegalArgumentException("The path is invalid!");
}
if (message.trim().equals("") || message == null) {
throw new IllegalArgumentException("The message is invalid!");
}
String entry = LocalDate.now() + ": " + message;
// location 인수로 명시한 파일 시스템 내 특정 파일에 로그 메시지 정리
Files.write(location, Collections.singletonList(entry),
StandardCharsets.UTF_8, StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
}
Logbook
클래스에서는 null
참조를 올바르게 확인하지 않는다.location
이 null
이라면Files.isDirectory(location)
은 NullPointerException
과 함께 실패한다.message.trim().equals("")
에서도 message
가 null
이면NullPointerException
과 함께 실패한다.인수를 검증할 때는 반드시
null
을 먼저 확인하고, 도메인에 따라 “유효하지 않은 값”을 검사해야 한다.
class Logbook {
void writeMessage(String message, Path location) throws IOException {
if (message == null || message.trim().isEmpty()) {
throw new IllegalArgumentException("The message is invalid!");
}
if (location == null || Files.isDirectory(location)) {
throw new IllegalArgumentException("The path is invalid!");
}
String entry = LocalDate.now() + ": " + message;
Files.write(location, Collections.singletonList(entry),
StandardCharsets.UTF_8, StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
}
null
값 여부를 확인하고, 이어서 도메인에 따른 제한을 확인하도록 수정했다.isEmpty()
를 사용하여 빈 문자열을 확인했다.매개변수 검사는
public
,protected
,default
메서드에서만 하면 된다.
파일 오픈 옵션
StandardOpenOptions
StandardOpenOptions
로Files.write()
메서드의 동작을 명시할 수 있다.
switch
문은 지난 수년 간 수많은 버그의 원인으로 악명을 떨친 프로그래밍 언어 구조체 중 하나이다.
class BoardComputer {
CruiseControl cruiseControl;
void authorize(User user) {
Objects.requireNonNull(user);
switch (user.getRank()) {
case UNKNOWN:
cruiseControl.logUnauthorizedAccessAttempt();
case ASTRONAUT:
cruiseControl.grantAccess(user);
break;
case COMMANDER:
cruiseControl.grantAccess(user);
cruiseControl.grantAdminAccess(user);
break;
}
}
}
authorizeUser()
는 매개변수를 검증하고 null
참조를 확인한다.Objects.requireNonNull()
을 사용하면 입력이 null
인 경우에 예외를 발생시킨다.authorizeUser()
에는 스위치 실패(switch fallthrough)라는 고전적인 버그가 있다.case
끝에 break
문이 없는 경우
switch
문은 break
문 또는 블록 끝에 다다라야 실행을 멈춘다.switch
실행 시멘틱(semantic)을 사용한다.서로 다른 관심사가 같은 코드 블록에 있는 경우
switch
문으로는 관심사를 분리하기 어렵다.if
문을 사용하는 방법이 더 선호된다.코드 수정으로 인한 조건 실패
switch
문은 default
케이스로 이러한 기능을 제공한다.AssertionError
를 던져서 코드 동작을 확인하기도 한다.if
문을 사용할 때 들여쓰기를 올바르게 하더라도 버그가 생기면 알아차리기 어렵다.class BoardComputer {
CruiseControl cruiseControl;
void authorize(User user) {
Objects.requireNonNull(user);
if (user.isUnknown()) {
cruiseControl.logUnauthorizedAccessAttempt();
}
if (user.isAstronaut()) {
cruiseControl.grantAccess(user);
}
if (user.isCommander()) {
cruiseControl.grantAccess(user);
}
cruiseControl.grantAdminAccess(user); // 보안 위험!
}
}
더 나은 코드는 코드가 적은 것이 아니라 더 읽기 쉬운 코드이다.
코드 대칭성(code symmetry)
“거의 같은 것들은 똑같은 부분과 완전히 다른 부분으로 나눌 수 없다.” - 켄트 벡(Kent Beck)
void authorize(User user) {
Objects.requireNonNull(user);
if (user.isUnknown()) {
cruiseControl.logUnauthorizedAccessAttempt();
} else if (user.isAstronaut()) {
cruiseControl.grantAccess(user);
} else if (user.isCommander()) {
cruiseControl.grantAccess(user);
cruiseControl.grantAdminAccess(user);
}
}
authorize()
메서드를 보면 눈에 띄는 버그는 없지만. 조건과 명령문이 계속 연이어 나온다.
authorize()
메서드에 대해 다음의 질문들을 생각해 보자.
- 모든 분기가 비슷한 관심사를 나타내는가?
- 병렬 구조를 아루고 있나?
- 아니면 세 가지 분기 모두 대칭인가?
현재 코드는 “그렇지 않다.”
void authorize(User user) {
Objects.requireNonNull(user);
// 권한을 부여하는 코드
if (user.isUnknown()) {
cruiseControl.logUnauthorizedAccessAttempt();
return;
}
// 권한을 부여하지 않는 코드
if (user.isAstronaut()) {
cruiseControl.grantAccess(user);
} else if (user.isCommander()) {
cruiseControl.grantAccess(user);
cruiseControl.grantAdminAccess(user);
}
}
최적화할 여지는 아직 남아있다.
authorize()
메서드의 두 번째 조건의 양 분기에서grantAccess()
메서드를 동일하게 호출하고 있는데,
두 조건을 별개의 비공개(private) 메서드로 추출하면 코드의 목적이 더 명확해질 것이다.이 부분은 이후 Chapter.05의 “빠른 실패”에서 자세히 설명한다.
이 장의 내용은 [자바 코딩의 기술: 똑똑하게 코딩하는 법]의 1장 내용을 정리한 것입니다.