원시 타입 포장하기

이진호·2022년 10월 12일
0
post-thumbnail

규칙 3: 원시값과 문자열의 포장

원시 타입의 변수를 그대로 사용하기 보다는 객체로 포장하여 사용해야 한다. 원시 타입 값을 포장하게 되면, 그 변수가 의미하는 바를 명확히 나타낼 수 있고, 책임 관계가 보다 명확해지며, 코드의 유지, 보수에도 많은 도움이 된다.

원시 타입 포장

// 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;
    }
    ...
}

bonusNumberLottoNumber로 포장해 사용하도록 수정하면 로또 숫자의 범위를 검증하는 중복 코드 로직를 제거할 수 있으며, 향후 변경 사항에 유연하게 대처가 가능해집니다.

자료형에 구애받지 않으며 여러 타입의 지원 가능

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라는 멤버변수를 추가하고, 생성자 오버로딩을 통해 간단히 해결할 수 있습니다.

출처

0개의 댓글