JAVA

자바 코드로 살펴보는 원시 값 포장에 대해

더즈 2022. 2. 17. 18:48

모든 원시 값을 포장하라라는 말이 있다. 예를 들어 보자.

  • 어떤 로직을 실행하는데 필요한 실행 횟수를 입력받아야 한다.
  • '실행 횟수'인만큼 1 이상인 정수여야 한다.

시도 횟수를 원시 값 그대로 사용할 경우 

int attemptNumber = 5;

 

public void excute(int attemptNumber) {
	if (attemptNumber < 1) {
		throw new IllegalArgumentException();
	}

	// attemptNumber 사용
}

시도 횟수를 사용하려면 사용하는 곳에서 검증 작업이 들어가야 한다.  만약 이 '시도 횟수'라는 개념이 여러 곳에서도 쓰인다면 이 검증 로직은 매번 들어가야 한다. (int attemptNumber는 신뢰할 수 없으므로 항상 검증하고 사용하여야 함!)


시도 횟수를 포장할 경우

public class AttemptNumber {

	private final int attemptNumber;

	private AttemptNumber(int attemptNumber) {
		validateAttemptNumberRange(attemptNumber);
		this.attemptNumber = attemptNumber;
	}

	public static AttemptNumber from(int attemptNumber) {
		return new AttemptNumber(attemptNumber);
	}

	private void validateAttemptNumberRange(int attemptNumber) {
		if (attemptNumber < 1) {
			throw new IllegalArgumentException();
		}
	}
}

 

이렇게 원시 값을 포장하면 다음과 같은 장점이 생긴다.

  • 시도 횟수에 관한 로직(1 이상이어야 한다는 검증)이 행해질 곳이 명확해진다.
  • 그저 int 값에 변수명으로 구분되었던 개념이 클래스화 됨으로써 이 객체가 무엇을 하는지 정확히 전달할 수 있다.
  • final 선언을 통해 불변을, equals와 hascode를 오버라이드 해서 동등성 또한 보장해 줄 수 있을 것이다.
  • 타입에서 자유로워진다.(int말고 long이나 double 등의 값을 받고 싶으면 추가할 수 있다)
  • 즉 AttemptNumber는 신뢰할 수 있다!

 

바뀐 코드를 살펴보면 밑에 코드처럼 이렇게 객체가 생성되는 시점에 이미 검증이 끝나 버리고 다른 유효성 검사는 더 이상 하지 않아도 된다.

AttemptNumber attemptNumber = AttemptNumber.from(5);
public void excute(AttemptNumber attemptNumber) {
	// attemptNumber 사용
}

만약 객체마다 다르게 받아들여져야 하는 원시값이라면?

이름, String name을 생각해보면 여러 이름을 가질 수 있는 객체들이 있다고 가정했을 때 어떤 객체는 이름이 5글자여야 하고, 어떤 객체는 이름이 영어여만하고... 성격이 다를 수도 있다.

우테코 자동차 경주 미션 리뷰에서 리뷰어님은 이렇게 코멘트를 남겨주셨다. 

public class Name {

	private static final String EMPTY_NAME_ERROR_MESSAGE = "이름은 1글자 이상이어야 합니다.";

	private final String name;

	private Name(String name) {
		validateEmptyName(name);
		this.name = name;
	}

	public static Name from(String name) {
		return new Name(name);
	}

	private void validateEmptyName(String name) {
		if (name.isEmpty()) {
			throw new IllegalArgumentException(EMPTY_NAME_ERROR_MESSAGE);
		}
	}
}

 

private Car(String name, int position) {
	validatePosition(position);
	validateNameOverLength(name);
	this.name = Name.from(name);
	this.position = position;
	}

private void validateNameOverLength(String carName) {
	if (carName.length() > NAME_LENGTH_LIMIT) {
		throw new IllegalArgumentException(INVALID_CAR_NAME_ERROR_MESSAGE);
	}
}

 

이렇게 구현하면 이름이 비어있으면 안된다는 공통 로직은 Name에서 바로 처리하고 각 객체가 가지는 특정 검증은 객체에서알아서 넣어주면 유효한 이름이 완성된다.  Name만이 가지는 공통적인 부분은 Name 객체 안에 표현하고 변할 수 있는 부분을 따로 빼주는게 포인트라고 생각한다.


결론

시도 횟수라는 매우 간단한 값이었지만 객체로 만들어주니 사용하는 곳에선 그 값에 대한 의미와 신뢰를 가지고 사용할 수 있게 되었다. 더 복잡한 유효성 검사와 개념이 들어간 정수나 문자열을 이렇게 포장해서 사용하면 코드가 많이 깔끔해질 것이다.