Chapter 01. 우선 정리부터

Yeseong31·2023년 8월 19일
0

자바 코딩의 기술

목록 보기
1/8

읽기 쉬운 코드 작성은 가장 중요한 의사소통 능력 중 하나이다.


쓸모없는 비교 피하기

  • 불 표현식은 불 원시값과 비교하지 않아도 된다.
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 피하기

  • NullPointerExceptionnull을 참조하는 메서드를 호출하거나 속성에 접근할 때 발생한다.
  • 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 참조를 올바르게 확인하지 않는다.
    • locationnull이라면
      Files.isDirectory(location)NullPointerException과 함께 실패한다.
    • message.trim().equals("")에서도 messagenull이면
      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

StandardOpenOptionsFiles.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 참조를 확인한다.
  • 자바 APIObjects.requireNonNull()을 사용하면 입력이 null인 경우에 예외를 발생시킨다.
  • 하지만 authorizeUser()에는 스위치 실패(switch fallthrough)라는 고전적인 버그가 있다.

스위치 실패 버그 유형

  1. case 끝에 break 문이 없는 경우

    • switch 문은 break 문 또는 블록 끝에 다다라야 실행을 멈춘다.
    • C 스타일 언어 및 자바는 이와 같은 switch 실행 시멘틱(semantic)을 사용한다.
  2. 서로 다른 관심사가 같은 코드 블록에 있는 경우

    • 서로 다른 관심사는 서로 다른 코드 블록에 넣어야 한다.
    • 하지만 switch 문으로는 관심사를 분리하기 어렵다.
    • 따라서 if 문을 사용하는 방법이 더 선호된다.
  3. 코드 수정으로 인한 조건 실패

    • 코드 수정이 있다면 조건을 조정해야 하는데 잊어버리기 쉽다.
    • 따라서 코딩하지 않은 값을 명시적으로 처리하는 예비 분기(branch)가 항상 있어야 한다.
    • 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장 내용을 정리한 것입니다.

profile
역시 개발자는 알아야 할 게 많다.

0개의 댓글

관련 채용 정보