모든 원시 값을 포장하라라는 말이 있다. 예를 들어 보자.
- 어떤 로직을 실행하는데 필요한 실행 횟수를 입력받아야 한다.
- '실행 횟수'인만큼 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 객체 안에 표현하고 변할 수 있는 부분을 따로 빼주는게 포인트라고 생각한다.
결론
시도 횟수라는 매우 간단한 값이었지만 객체로 만들어주니 사용하는 곳에선 그 값에 대한 의미와 신뢰를 가지고 사용할 수 있게 되었다. 더 복잡한 유효성 검사와 개념이 들어간 정수나 문자열을 이렇게 포장해서 사용하면 코드가 많이 깔끔해질 것이다.
'JAVA' 카테고리의 다른 글
옵저버 패턴(Observer Pattern) 살펴보기 (0) | 2022.03.05 |
---|---|
[AssertJ] Iterable and array assertions 활용 (컬렉션 테스트) (0) | 2022.03.03 |
[자바] 객체 내부 컬렉션 데이터 보호하기 (0) | 2022.02.26 |
자바 JDK, JRE, JVM 간단 정리 (0) | 2022.02.21 |
자바 스트림 사용 정리 (0) | 2022.02.20 |