예측 가능한 코드를 작성하라

w-beom·2023년 4월 9일
0
post-thumbnail

Chapter 6. 예측 가능한 코드를 작성하라

■ 코드가 어떻게 예측을 벗어나 작동할 수 있는지
■ 소프트웨어에서 예측을 벗어나는 코드가 어떻게 버그로 이어질 수 있는지
■ 코드가 예측을 벗어나지 않도록 보장하는 방법

6.1 매직값을 반환하지 말아야 한다.

매직 값은 함수의 정상적인 반환 유형에 적합하지만 특별한 의미를 가지고 있다. 매직값의 일반적인 예는 값이 없거나 오류가 발생했음을 나타내기 위해 -1을 반환하는 것이다.

6.1.1 매직값은 버그를 유발할 수 있다

매직값(Magic Value)이란, 특정 값을 사용하여 의미를 부여하는 것으로, 그 값이 특정한 의미를 가지는 것을 의미한다.

아래의 예시를 통해 알아보자.

class User {
	private Integer age;
    public Integer getAge(){
    	if(age == null){
        	return -1;
        }
    	return age;
    };
}

User클래스안에 사용자의 나이를 담는 변수 age가 존재하고 getAge 함수는 사용자의 나이가 입력되어있지 않을경우 null 대신 -1을 return 한다고 가정한다.
여기서 -1은 나이가 입력되어 있지 않다는 뜻을 가진 매직 값(Magic Value)이다.

다른 개발자가 모든 사용자들의 평균 나이를 구하기 위해 User클래스의 getAge함수를 사용한다고 가정해보자.

public double calculateAverageAge(List<User> users){
	int totalAge = 0;
	for(User user : users){
    	totalAge += user.getAge();
    }
    
    return (double) totalAge / (double) users.size();
}

개발자가 평균나이를 구하기 위해 getAge 함수를 호출해서 totalAge 변수에 더하는 과정에서 나이가 존재하지 않을경우 -1를 더하기 때문에 버그를 발생시킬 수 있다.

6.1.2 해결책: 널, 옵셔널 또는 오류를 반환하라

함수에서 매직값을 반환할 때의 문제점은 호출하는 쪽에서 함수의 구현내용에 대해 잘 알아야 한다는 점이다. 어떤 개발자들은 구현내용을 확인하지 않거나 확인하고나서 잊어버린다. 이런 경우에는 예측을 벗어나는 일이 생길 수 있다.

  1. null을 반환하라
class User {
	private Integer age;
    public Integer getAge(){
    	return age;
    };
}
  1. 옵셔널을 반환하라.
class User {
	private Integer age;
    public Optional<Integer> getAge(){
    	if(age == null){
        	return Optional.empty();
        }
    	return Optional.of(age);
    };
}
  1. 오류를 반환하라.
class User {
	private Integer age;
    public Integer getAge(){
    	if(age == null){
        	throw new CustomException("나이가 존재하지 않습니다.");
        }
    	return age;
    };
}

6.2 널 객체 패턴을 적절히 사용하라

값을 얻을 수 없을 때 널값이나 빈 옵셔널을 반환하는 대신 널 객체 패턴을 사용할 수 있다. 널 객체 패턴을 사용하는 이유는 널값을 반환하는 대신 유효한 값이 반환되어 그 이후에 실행되는 로직에서 널값으로 인해 시스템에 피해가 가지 않도록 하기 위함이다.
널 객체 패턴의 가장 간단한 형태는 빈 문자열이나 빈 리스트를 반환하는 것이다.

6.2.1 빈 컬렉션을 반환하면 코드가 개선될 수 있다.

HtmlElement에서 highlighted라는 class가 존재 여부를 체크하는 함수이다.

개선 전 코드

public Set<String> getClassNames(HtmlElement element) {
	String attirbute = element.getAttribute("class");
    if (attribute == null) {
    	return null;
    }
	return new Hashet<>(attribute.split(" "));
}

public boolean isElementHighlighted(HtmlElement element){
	Set<String> classNames = getClassNames(element);
    if (classNames == null){ // getClassNames에서 null을 반환할 수 있으므로 null 처리를 해줘야함
    	return false;
    }
    return classNames.contains("highlighted");
}

개선 후 코드

public Set<String> getClassNames(HtmlElement element){
	String attirbute = element.getAttribute("class");
    if (attribute == null){
    	return Collections.emptySet();
    }
	return new HashSet<>(attribute.split(" "));
}

public boolean isElementHighlighted(HtmlElement element){
	Set<String> classNames = getClassNames(element);
    //getClassNames에서 빈 Set객체를 반환해주니 null 처리를 할 필요가 없음
    return classNames.contains("highlighted"); 
}

6.2.2 빈 문자열을 반환하는 것도 때로는 문제가 될 수 있다.

문자열이 문자들을 모아 놓은 것에 지나지 않으며, 이 경우 널 대신 빈 문자열을 반환하는 것이 적절할 수 있다. 문자열이 이것을 넘어서는 의미를 지닐 때, 널 대신 빈 문자열을 반환하는 것이 문제가 될 수 있다.

6.2.3 더 복잡한 널 객체는 예측을 벗어날 수 있다.

함수를 호출할 때 널 객체 패턴을 사용하는 것은 본질적으로 빈 상자를 파는 것과 같다. 호출하는 쪽에 서 빈 상자를 받고 놀라거나 황당해할 가능성이 있다면, 널 객체 패턴을 피하는 것이 가장 좋을 것이다.

6.3 예상치 못한 부수 효과를 피하라

부수 효과 (side effect)는 어떤 함수의 호출로 인해 함수 외부에 초래한 상태 변화를 의미한다. 함수가 반환하는 값 외에 다른 효과가 있다면 이는 부수 효과가 있는 것이다.

부수 효과를 일으키는 것을 피하는 방법

  • 클래스 및 함수를 불변(Immutable)으로 만든다.
  • 호출하는 쪽에서 부수효과를 인지할 수 있도록 한다.

6.3.1 분명하고 의도적인 부수 효과는 괜찮다.

부수 효과가 코드의 어떤 부분에서 필요할 때가 있다.

class UserDisplay {
  private final Canvas canvas;
  ...
  
  void displayErrorMessage(String message) {
    canvas.drawText(message, Color.RED);
  }
}

displayErrorMessage() 함수는 한가지 부수 효과를 가지고 있는데, 사용자에게 표시되는 캔버스를 업데이트하는 것이다. 그러나 클래스를 UserDisplay라고 명명하고 함수를 displayErrorMessage()라고 하는 경우 이러한 부수 효과가 발생할 것이라는 점이 명백하고 예상을 벗어나는 동작은 없다.

6.3.2 예기치 않은 부수 효과는 문제가 될 수 있다.

class UserDisplay {
  private final Canvas canvas;
  ...
  
  Color getPixel(Int x, Int y) {
    canvas.redraw();    // 다시 그리기 이벤트를 발생하는 것은 부수 효과다.
    PixelData data = canvas.getPixel(x, y);
    return new Color(
      data.getRed(),
      data.getGreen(),
      data.getBlue());
  }
}

만약 canvas.redraw() 함수의 비용이 많이 들경우 이 부수효과를 알지 못하고 getPixel() 함수를 호출한 쪽은 큰 버그로 이어질수도 있다.

6.3.3 해결책: 부수 효과를 피하거나 그 사실을 분명하게 하라

  1. 함수의 이름을 좀 더 명확하게 수정한다.
class UserDisplay {
  private final Canvas canvas;
  ...
  Color redrawAndGetPixel(Int x, Int y) { //함수명을 수정함으로써 부수효과가 있다는 것을 나타내고있다.
    canvas.redraw();
    PixelData data = canvas.getPixel(x, y);
    return new Color(
      data.getRed(),
      data.getGreen(),
      data.getBlue());
  }
}

6.4 입력 매개변수를 수정하는 것에 주의하라

6.4.1 입력 매개변수를 수정하면 버그를 초래할 수 있다.

만약 친구에게 책을 빌려준 후에 돌려받았는데 몇 페이지나 찢어져 있고 여백에 메모가 휘갈겨져 있다면 아마 화가 많이 날 것이다. 어떤 객체를 다른 함수에 입력으로 넘기는 것은 이처럼 친구에게 책을 빌려주는 것과 유사하다.
입력으로 받은 객체에는 그 함수에 필요한 정보가 있지만, 이 함수가 호출된 뒤에도 해당 객체를 다른 용도로 사용할 가능성이 크다. 입력 매개변수를 수정하는 것은 함수가 외부의 무언가에 영향을 미치기 때문에 부수 효과의 또 다른 예다.

6.4.2 해결책 : 변경하기 전에 복사하라.

List<Invoice> getBillableInvoices(Map<User, Invoice> userInvoices, Set<User> usersWithFreeTrial) {
  return userInvoices.entries()
                      .filter(entry ->
                      !usersWithFreeTrial.contains(entry.getKey()))
                      .map(entry -> entry.getValue());
}

값을 복사하면 메모리나 CPU, 혹은 두 가지 모두와 관련해 성능에 영향을 미칠 수 있다. 하지만 입력 매개변수의 변경으로 인해 발생할 수 있는 예기치 못한 동작이나 버그와 비교하면 성능이 크게 문제되지 않는 경우가 많다.

6.5 오해를 일으키는 함수는 작성하지 말라

개발자가 어떤 함수를 호출하는 코드를 접하면 그들은 자신들이 보는 것에 기초하여 함수 내부에서 무슨 일이 일어나고 있는지에 대해 생각한다. 하지만 항상 그렇지만은 않다면, 이 함수로 인해 예상을 벗어나는 결과와 버그로 이어지기 쉽다.

6.5.1 중요한 입력이 누락되었을 때 아무것도 하지 않으면 놀랄 수 있다.

매개변수가 없더라도 호출할 수 있고 해당 매개변수가 없으면 아무 작업도 수행하지 않는 함수가 있다면, 이 함수가 수행하는 작업에 대해 오해의 소지가 있을 수 있다.
호출하는 쪽에서는 해당 매개변수의 값을 제공하지 않고 함수를 호출하는 것의 심각성을 모를 수 있으며, 코드를 읽는 사람은 함수 호출 시 항상 무언가 작업이 이루어진다고 잘못 생각할 수 있다.

6.5.2 해결책: 중요한 입력은 필수 항목으로 만들라

어떤 매개변수 없이는 함수가 수행하려는 작업을 못 하는 경우 그 매개변수는 해당 함수에 중요하다. 이러한 매개변수에 대해서는 값을 사용할 수 없는 경우 함수를 호출할 수 없도록 널을 허용하지 않는 것이 더 안전할 수 있다.

public class Calculator {
	public int add(Integer a, Integer b){ // 매개변수가 잘못 들어왔을 경우 exception을 발생시킨다.
    	if(a == null || b == null){
        	throw new IllegAlargumentexception();
        }
        return a + b;
    }
}

6.6 미래를 대비한 열거형 처리

열거형을 처리해야 하는 경우 나중에 열거형에 더 많은 값이 추가될 수 있다는 점을 기억하는 것이 중요하다. 이것을 무시하고 코드를 작성하면, 자기 자신 혹은 다른 개발자들의 예측을 벗어나는 좋지 않은 결과를 초래할 수 있다.

6.6.1 미래에 추가될 수 있는 열것값을 암묵적으로 처리하는 것은 문제가 될 수 있다.

enum PredictedOutcome {
    COMPANY_WILL_GO_BUST,
    COMPANY_WILL_MAKE_A_PROFIT,
  }
  
  Boolean isOutcomeSafe(PredictedOutcome prediction) {
    if (prediction == PredictedOutcome.COMPANY_WILL_GO_BUST) {
    return false;
  }
  return true;
}

만약 enum에 새로운 값이 추가될 경우 isOutcomeSafe의 반환값은 항상 true일 것 이다.
isOutcomeSafe() 함수 정의는 열거형 정의에서 수백 줄 떨어진 코드이거나, 완전히 다른 파일 혹은 다른 패키지에 있을 수 있다. 이 함수는 완전히 다른 팀이 유지보수할 수도 있다. 따라서 어떤 개발자가 PredictedOutcome에 값을 추가하면 그에 따라 isOutcomeSafe() 함수도 당연히 수정할 것이라고 가정하는 것은 위험하다.

6.6.2 해결책: 모든 경우를 처리하는 스위치 문을 사용하라.

enum PredictedOutcome {
  COMPANY_WILL_GO_BUST,
  COMPANY_WILL_MAKE_A_PROFIT,
}

Boolean isOutcomeSafe(PredictedOutcome prediction) {
  switch (prediction) {
    case COMPANY_WILL_GO_BUST:
    return false;
    case COMPANY_WILL_MAKE_A_PROFIT:
    return true;
  }
  throw new UncheckedException("Unhandled prediction: " + prediction);
}

이제 enum의 새로운 값이 추가된 후 isOutcomeSafe를 호출해도 예외가 발생해 개발자에게 알려줄 수 있다.

6.6.3 기본 케이스를 주의하라

스위치 문은 일반적으로 처리되지 않은 모든 값에 대해 적용할 수 있는 기본 default 케이스를 지원한다. 열거형을 처리하는 스위치 문에 기본 케이스를 추가하면 향후 열거형 값이 암시적으로 처리될 수 있으며 잠재적으로 예기치 않은 문제와 버그가 발생할 수 있다.

Boolean isOutcomeSafe(PredictedOutcome prediction) {
  switch (prediction) {
    case COMPANY_WILL_GO_BUST:
    return false;
    case COMPANY_WILL_MAKE_A_PROFIT:
    return true;
    default:
    return false;
  }
}

6.7 이 모든 것을 테스트로 해결할 수는 없는가?

  • 어떤 개발자들은 테스트에 대해 그다지 부지런하지 않을 수도 있다. 즉, 여러분의 코드에 대해 자신이 가정한 것이 틀렸다는 것을 드러내기 위해 충분한 시나리오나 코너 케이스를 테스트하지 않는다. 특정 시나리오 혹은 매우 큰 입력에서만 문제가 드러나는 경우 이런 상황이 가능하다.

  • 테스트가 항상 실제 상황을 정확하게 시뮬레이션하는 것은 아니다. 코드를 테스트하는 개발자는 의존 라이브러리를 mock 객체를 통해 테스트해야 할 수도 있다. 이 경우 mock 객체가 어떻게 행동할 것인지 자신이 생각하는 바대로 프로그래밍한다. 실제 코드가 개발자의 가정과 예측을 벗어나는 방식으로 동작하지만 개발자가 이를 깨닫지 못한다면, mock 객체 자체를 올바르게 프로그래밍하지 못한다. 이런 일이 일어난다면 예상을 벗어나는 동작 때문에 발생하는 버그는 테스트를 통해 찾아내기가 어렵다.

  • 어떤 것들은 테스트하기가 매우 어렵다. 멀티스레딩 문제와 관련된 버그는 종종 낮은 확률로 발생하고, 어느 정도 큰 규모에서 실행될 때만 나타날 수 있어서 테스트하기 어렵기로 악명 높다.

테스트는 매우 중요하다. 아무리 많은 코드 구조화나 코드 계약에 대한 걱정도 고품질의 철저한 테스트를 대체할 수 없다. 그러나 그 반대 역시 사실이다. 직관적이지 않거나 예상을 벗어나는 코드에 숨어 있는 오류를 테스트만으로는 방지하기 어렵다.

profile
습득한 지식과 경험을 나누며 다른 사람들과 문제를 함께 해결해 나가는 과정에서 서로가 성장할 수 있는 기회를 만들고자 노력합니다.

0개의 댓글