Java

상수 값 추출에 대하여

zangsu_ 2023. 12. 3. 19:53

클린 코딩에 관심이 있는 개발자라면 모두 다음의 문장을 읽어본 적이 있을 것 같습니다.

"매직 넘버, 매직 스트링을 상수로 선언하라."

 

상수 추출을 하는 이유

코드의 역할이 무엇인가?

그렇다면, 왜 매직 넘버, 매직 스트링을 지양하고 해당 값을 상수로 선언해야 하는 것일까요?

아래의 코드로 그 이유를 한번 살펴봅시다!

private void validateNumberRange(int number) {
    if (number < 1 || number > 45) {
        throw new IllegalArgumentException();
    }
}

 

해당 코드의 역할이 무엇인지 짐작이 가나요?

 

"숫자가 1 ~ 45 사이의 값인지를 검증해 주는 코드입니다." 라고 한다면, 그건 프로그램을 조금이라도 다뤄 봤던 사람이라면 모두 알 수 있을거에요.

우리가 관심을 가져야 하는 부분은 해당 코드가 "어떤 동작"을 하는지가 아닌, "어떤 역할"을 하는지 인데요.

 

그럼 다시 한번 물어볼게요. 해당 코드는 어떤 "역할"을 하고 있나요??

위의 코드만 보고는 그 정보를 전혀 알 수 없어요.

왜 수의 범위를 검증해야 하는지, 그리고 그 값은 왜 1 ~ 45 사이의 값인지, 아무런 정보도 얻을 수가 없습니다.

 

그럼 위의 코드를 정말 조금만 수정해 볼게요!

private final int LOTTO_RANGE_MIN = 1;
private final int LOTTO_RANGE_MAX = 45;

private void validateNumberRange(int number) {
    if (number < LOTTO_RANGE_MIN || number > LOTTO_RANGE_MAX) {
        throw new IllegalArgumentException();
    }
}

 

이제는 위의 코드가 어떤 역할을 하고 있는지 보이나요?

위의 코드는 입력받은 숫자가 "로또 숫자"의 범위에 해당하는지를 확인하는 코드네요!

우리가 확인하는 값의 범위 1~45는 각각 로또 번호가 가질 수 있는 최솟값과 최댓값이었어요.

 

방금 우리는 상수 추출이 가지는 첫 번째 장점을 알아보았습니다!

그것은 바로 "특정 값이 지니는 논리적인 의미를 설명해 준다." 라는 것입니다.

 

요구사항이 변경된다면?

이번엔 이런 가정을 해볼게요.

로또 회사가 로또 서비스를 제공하다 보니 로또 당첨률이 너무 높은 것 같았나봐요. 그래서 로또 회사는 로또 숫자의 범위를 더 늘리기로 했어요.

이젠 로또 숫자는 1~45가 아니라, 1~85의 범위를 가지게 될거에요.

 

이제 우리 개발자는 어플리케이션 내에서 사용되는 로또 값의 범위를 수정해 주어야 겠네요!

그럼 우리는 어떻게 해야 할까요?

프로그램을 구성하는 모든 코드를 찾아보면서 45라고 사용되고 있던 값을 모두 85로 변경해 주어야겠네요!

 

우리는 랜덤으로 숫자를 생성하는 부분에 사용되던 45도 85로 변경해 줄거고, 로또 숫자를 검증하는 부분에 사용되던 45도 85로 변경해 줄거에요.

@Override
protected List<Integer> pickLottoNumbers() {
    return Randoms.pickUniqueNumbersInRange(1, 45, 6); // 45 -> 85로 변경
}

//... 중간 생략 ...//

private void validateNumberRange(int number) {
    if (number < 1 || number > 45) { //45 -> 85로 변경
        throw new IllegalArgumentException();
    }
}

 

그런데, 정말 이 두 값만 변경하면 요구사항 변경에 대한 코드 수정이 끝난걸까요??

 

매직 넘버를 지양하고 상수를 선언해야 하는 두 번째 이유가 이런 이유인데요.

만약 우리가 로직에 사용할 값을 매직 넘버로 사용하고 있다면, 각각의 값은 모두 개별적인 코드로 존재하게 됩니다.

숫자를 생성하는 부분에 사용되던 45와 숫자를 검증하는 부분에 사용되던 45는 숫자만 같을 뿐이지 서로 아무런 연관성이 없는 별개의 코드일 뿐이에요.

 

하지만, 우리가 위와 같이 상수 추출을 했다고 가정해 볼게요.

private static final int LOTTO_RANGE_MIN = 1;
private static final int LOTTO_RANGE_MAX = 45;

@Override
protected List<Integer> pickLottoNumbers() {
    return Randoms.pickUniqueNumbersInRange(LOTTO_RANGE_MIN, LOTTO_RANGE_MAX, 6);
}

//... 중간 생략 ...//

private void validateNumberRange(int number) {
    if (number < LOTTO_RANGE_MIN || number > LOTTO_RANGE_MAX) {
        throw new IllegalArgumentException();
    }
}

 

그럼 우리는 요구사항에 대처하기 위해 상수로 관리하고 있던 LOTTO_RANGE_MAX의 값만 45 -> 85로 변경해 주면 되는거에요!

코드 전체에 퍼져 있던 로또 숫자의 최댓값을 하나의 상수에서 값을 가져와 사용하도록 변경시켜 주었기 때문에 모든 사용부분이 해당 상수에 의존하고 있어요. 즉, 한 곳에서 "로또 숫자의 최댓값"이라는 논리적인 값이 관리되고 있는거죠.

 

정리하자면, 매직 넘버의 상수 선언은 다음과 같은 장점을 가져요.

  1. 값이 가지는 논리적인 의미를 나타내어 코드의 가독성을 높인다.
  2. 동일한 역할을 하는 값을 한 곳에서 관리하여 유지보수의 편의성을 높인다.

 

상수 추출 잘하기!

그렇다면, 이제 우리는 우리 코드에 존재하는 모든 리터럴값을 상수로 추출해 주면 되는걸까요??

 

이번에도 코드를 보면서 생각해 봅시다.

public boolean isEven(int number){
	return number % 2 == 0;
}

 

위의 코드는 주어진 숫자가 짝수인지를 확인해 주는 메서드네요.

그런데, 코드에 리터럴 상수 2와 0이 사용되고 있어요.

이런, 이 값들을 어서 상수로 추출해 줍시다!

 

public static final int ZERO = 0;
public static final int TWO = 2;

public boolean isEven(int number){
	return number % TWO == ZERO;
}

 

그런데, 상수로 추출하고 나니 뭔가 이상하지 않나요?

과연 0을 ZERO, 2를 TWO로 추출하는 것에 의미가 있을까요?

괜한 작업을 한번 더 하는 것 같지 않나요?

 

여기서 다시 한번 우리가 상수를 추출하는 이유를 복습해 봅시다.

우리가 상수를 추출하는 이유는 다음과 같다고 했어요.

 

  1. 값이 가지는 논리적인 의미를 나타내어 코드의 가독성을 높인다.
  2. 동일한 역할을 하는 값을 한 곳에서 관리하여 유지보수의 편의성을 높인다.

 

그렇다면, 이렇게 상수로 추출할 수 있는 부분을 상수로 추출하지 않는다면 아래와 같은 문제가 존재하겠네요.

  1. 해당 값이 가지는 논리적인 의미를 파악하지 못해 전체 코드를 읽어보며 해당 값의 역할을 추론해야 한다.
  2. 특정 값이 변경되어야 하는 경우 해당 값의 모든 사용처를 찾아 다니며 값을 변경해 주어야 한다.

그런데, 첫 번째 isEven() 메서드를 살펴봅시다.

방금 적어 둔 문제가 존재하나요?

 

"2로 나누었을때 나머지가 0이다." 에 해당하는 2와 0은 어떠한 논리적 값을 의미하는 것이 아니라 정말 숫자 0, 숫자 2 그 자체로 의미를 가지고 있어요.

게다가, 짝수를 판별하는 로직에 사용되는 2와 0이 다른 값으로 변경되어야 하는 상황도 절대 존재하지 않을거고요!

 

이 처럼 코드에 사용되는 값이 숫자 자체로 의미를 가져 이미 충분히 가독성이 좋고, 값의 변경 가능성 또한 존재하지 않는다면 이 곳에 사용되는 숫자 값은 상수로 선언해 주는 의미가 없겠네요.

 


 

우리는 왜 매직 넘버 대신 상수를 사용해야 하는지를 알아봤어요.

 

적절한 상수 추출은 코드의 가독성도 높여주며 특정 값을 한 곳에서 관리하기에 유지보수의 편의성도 높여주니 코드에 존재하는 "논리적인 의미를 가지는 값" 들은 지금 당장에는 한 곳에서만 사용되더라도 적극적으로 상수로 추출해 보면 좋을 것 같습니다!