원시 타입의 변수를 그대로 사용하기 보다는 객체로 포장하여 사용해야 한다. 원시 타입 값을 포장하게 되면, 그 변수가 의미하는 바를 명확히 나타낼 수 있고, 책임 관계가 보다 명확해지며, 코드의 유지, 보수에도 많은 도움이 된다.
// AS-IS
int age = 20;
// TO-BE
Age age = new Age(20);
위와 같이 원시 타입을 그대로 사용하는 대신, 원시 타입의 변수를 객체로 포장한 변수를 선언하여 사용한다.
public class User {
private int age;
public User(String input) {
int age = Integer.parseInt(input);
if (age < 0) {
throw new RuntimeException("나이는 0살부터 시작합니다.");
}
this.age = age;
}
}
위의 형태처럼 원시 타입인 int 로 나이를 가지고 있으면, 나이에 관한 유효성 검사를 User 클래스에서 하게 됩니다. 멤버변수가 나이 하나만 있을때는 괜찮지만 사용자의 이름, 이메일 등 추가적인 값들이 추가되면 추가될수록 해당 객체의 복잡도가 증가하게 됩니다.
public class User {
private String name;
private int age;
public User(String nameValue, String ageValue) {
int age = Integer.parseInt(ageValue);
validateAge(age);
validateName(nameValue);
this.name = nameValue;
this.age = age;
}
private void validateName(String name) {
if (name.length() < 2) {
throw new RuntimeException("이름은 두 글자 이상이어야 합니다.");
}
}
private void validateAge(int age) {
if (age < 0) {
throw new RuntimeException("나이는 0살부터 시작합니다.");
}
}
}
위와 같이 이름 멤버 변수 하나가 추가되었을 뿐인데 복잡도가 확연히 증가되었습니다. 또한 User 클래스에서 이름 값에 대한 상태관리, 나이 값에 대한 상태관리를 모두 하게 됩니다.
public class User {
private Name name;
private Age age;
public User(String name, String age) {
this.name = new Name(name);
this.age = new Age(age);
}
}
public class Name {
private String name;
public Name(String name) {
if (name.length() < 2) {
throw new RuntimeException("이름은 두 글자 이상이어야 합니다.");
}
this.name = name;
}
}
public class Age() {
private int age;
public Age(String input) {
int age = Integer.parseInt(input);
if(age < 0) {
throw new RuntimeException("나이는 0살부터 시작합니다.");
}
}
}
위와 같이 원시 타입을 포장하면 이름과 나이에 대한 상태관리를 각각의 포장 객체가 담당하게 하여 책임이 명확해지게 됩니다.
public class LottoNumber {
private final static int MIN_LOTTO_NUMBER = 1;
private final static int MAX_LOTTO_NUMBER = 45;
private final static String OUT_OF_RANGE = "로또번호는 1~45의 범위입니다.";
private final static Map<Integer, LottoNumber> NUMBERS = new HashMap<>();
private int lottoNumber;
static {
for (int i = MIN_LOTTO_NUMBER; i < MAX_LOTTO_NUMBER + 1; i++) {
NUMBERS.put(i, new LottoNumber(i));
}
}
public LottoNumber(int number) {
this.lottoNumber = number;
}
public static LottoNumber of(int number) {
LottoNumber lottoNumber = NUMBERS.get(number);
if (lottoNumber == null) {
throw new IllegalArgumentException(OUT_OF_RANGE);
}
return lottoNumber;
}
...
}
public class Lotto {
...
private List<LottoNumber> lottoNumbers;
public Lotto(List<LottoNumber> lottoNumbers) {
validateDuplication(lottoNumbers);
validateAmountOfNumbers(lottoNumbers);
this.lottoNumbers = lottoNumbers;
}
...
}
public class WinningLotto {
...
private Lotto winningLottoNumbers;
private int bonusNumber;
public WinningNumber(Lotto winningLottoNumbers, int bonusNumber) {
this.winningLottoNumbers = winningLottoNumbers;
if (isBonusNumberDuplicatedWithWinningNumber(winningLottoNumbers, bonusNumber)) {
throw new IllegalArgumentException(
BONUS_CANNOT_BE_DUPLICATE_WITH_WINNING_NUMBER);
}
if (bonusNumber < 1 | bonusNumber > 45) {
throw new RuntimeException();
}
this.bonusNumber = bonusNumber;
}
...
}
위의 코드를 살펴보면 Lotto 클래스에서는 int 값인 로또 숫자 하나하나를 LottoNumber
로 포장해 사용하고 있는것에 반해, WinningLotto 클래스에서는 int 원시 타입의 bonusNumber
를 사용하고 있습니다. 이에 따라 불필요하게 로또 숫자의 범위를 검증하는 코드가 중복되게 됩니다.
public class WinningLotto {
...
private Lotto winningLottoNumbers;
private LottoNumber bonusNumber;
public WinningNumber(Lotto winningLottoNumbers, LottoNumber bonusNumber) {
this.winningLottoNumbers = winningLottoNumbers;
if (isBonusNumberDuplicatedWithWinningNumber(winningLottoNumbers, bonusNumber)) {
throw new IllegalArgumentException(
BONUS_CANNOT_BE_DUPLICATE_WITH_WINNING_NUMBER);
}
this.bonusNumber = bonusNumber;
}
...
}
bonusNumber
를 LottoNumber
로 포장해 사용하도록 수정하면 로또 숫자의 범위를 검증하는 중복 코드 로직를 제거할 수 있으며, 향후 변경 사항에 유연하게 대처가 가능해집니다.
public class Score {
private int score;
public Score(int score) {
validateScore(score);
this.score = score;
}
...
}
점수라는 값을 포장한 Score 클래스가 있으며 현재는 int 값만 사용함.
public class Score {
private int score;
private double doubleScore;
public Score(int score) {
validateScore(score);
this.score = score;
}
public Score(double score) {
validateScore(score);
this.doubleScore = score;
}
...
}
Score객체에 연산 등의 기능이 추가되어 새로운 자료형의 지원이 필요해지는 경우 위와 같이 기존 Score 클래스를 활용하여 doubleScore라는 멤버변수를 추가하고, 생성자 오버로딩을 통해 간단히 해결할 수 있습니다.
출처