[OOP] 원시 타입을 포장하라
원시 타입
원시 타입(Primitive Type)은 알다시피 실제 데이터 값을 저장하는 타입으로 평소 많이 사용하는 형태이다. 원시 타입에는 보통 boolean, char, int, double 등 여러가지가 있다.
원시 타입을 포장하라?
원시 타입의 값을 객체로 포장하면 객체지향적으로 얻을 수 있는 이점들이 많다.
그럼 한 번 무엇이 있는지 살펴보자.
자신의 상태를 객체 스스로 관리할 수 있다
Wallet이라는 클래스에서, 사용자의 돈(cash)을 가지고 있다고 가정해보자.
public class Wallet {
private int cash;
public Wallet(int money) {
this.cash = cash;
}
}
위 형태처럼 원시 타입인 int로 돈을 가지고 있으면 어떨까?
일단 다음과 같이 나이에 관한 유효성 검사를 Wallet 클래스에서 하게 된다.
public class Wallet {
private int cash;
public Wallet(String cashValue) {
int cash = Integer.parseInt(cashValue);
if (cash < 0) {
throw new RuntimeException("돈은 0원 미만일 수 없습니다.");
}
this.cash = cash;
}
}
멤버 변수가 적어서 아직 문제를 못 느낄 수 있다. 하지만 다른 인스턴스 변수가 추가적으로 들어가 관리하게 된다면 문제가 생길 수 밖에 없다.
이름을 추가한 다음 코드를 살펴보자.
public class Wallet {
private String idCardNumber;
private int cash;
public Wallet(String idCardNumberValue, String cashValue) {
int cash = Integer.parseInt(cashValue);
validateCash(cash);
validateIdCardNumber(idCardNumberValue);
this.idCardNumber = idCardNumberValue;
this.cash = cash;
}
private void validateIdCardNumber(String idCardNumber) {
if (idCardNumber.length() < 13) {
throw new RuntimeException("주민번호는 13글자 이상이어야 합니다.");
}
}
private void validateCash(int cash) {
if (cash < 0) {
throw new RuntimeException("돈은 0원 미만일 수 없습니다.");
}
}
}
멤버 변수를 두 개 가졌을 뿐인데 Wallet 클래스가 할 일이 많아졌다. 그럼 한 번 원시 타입 변수를 포장해보자.
class IdCard {
private String idCardNumber;
public IdCard(String idCardNumber) {
if (idCardNumber.length() < 13) {
throw new RuntimeException("주민번호는 13글자 이상이어야 합니다.");
}
this.idCardNumber = idCardNumber;
}
}
class Money {
private int cash;
public Money(String cashValue) {
int cash = Integer.parseInt(cashValue);
if (cash < 0) {
throw new RuntimeException("돈은 0원 미만일 수 없습니다.");
}
this.cash = cash;
}
}
public class Wallet {
private IdCard idCard;
private Money money;
public Wallet(String idCardNumber, String cash) {
this.idCard = new IdCard(name);
this.money = new Money(cash);
}
}
이렇게 하면 주민번호와 돈의 처리를 각각 IdCard, Money가 담당하도록 바뀌었다.
유효성 검증을 비롯한 주민번호, 돈에 대한 상태 값을 Wallet에게 넘기지 않고 스스로 관리할 수 있게 되었다. 즉, 책임이 명확해졌다.
유지보수에 도움이 된다
먼저 다음 예시를 한 번 살펴보자.
다음은 로또와 로또 번호, 당첨 번호를 구성한 클래스들이다.
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 WinningNumber {
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;
}
}
위 코드를 보면 로또 숫자를 LottoNumber로 래핑하여 사용하고 있는 것을 확인할 수 있다.
만약 이렇게 하지 않고 원시 타입(int)으로 사용했다면 각 로또 숫자에 대해 처리하는 행위가 추가되고 Lotto 클래스의 크기가 커질 것이다.
이렇게 되면 객체지향적으로 코드를 작성하기 힘들어진다.
또, 변경사항이 생긴다면 WinningNumber 클래스, Lotto 클래스 모두 수정해야 하는 상황이 생기게 된다.
때문에 저렇게 LottoNumber로 래핑해주면 변경과 확장에 열려있는 형태로 만들어줄 수 있다.
개별적으로 확장이 가능하다
위 Money 클래스에서 만약 cash이외의 값이 필요해졌다면 기존에 있는 money값을 수정할 필요 없이 다음과 같이 새로 추가하면 된다.
class Money {
private int cash;
private int giftCard;
public Money(String cashValue, String giftCardValue) {
int cash = Integer.parseInt(cashValue);
int giftCard = Integer.parseInt(giftCardValue);
if (cash < 0 || giftCard < 0) {
throw new RuntimeException("돈은 0원 미만일 수 없습니다.");
}
this.cash = cash;
this.giftCard = giftCard;
}
}
public class Score {
private int score;
private double percentScore;
public Score(int score) {
validateScore(score);
this.score = score;
}
public Score(double score) {
validateScore(score);
this.percentScore = score / 100.0;
}
}
이렇게 인스턴스 변수를 간단히 새로 추가하고 관리할 수 있게 된다.
'Software Engineering > OOP' 카테고리의 다른 글
[OOP] 무분별한 Getter/Setter를 지양하라 (0) | 2023.03.16 |
---|---|
[OOP] 상속보다는 조합을 사용하자 (0) | 2023.03.15 |
[OOP] instanceof의 사용을 지양하라 (0) | 2023.03.14 |
댓글
이 글 공유하기
다른 글
-
[OOP] 무분별한 Getter/Setter를 지양하라
[OOP] 무분별한 Getter/Setter를 지양하라
2023.03.16 -
[OOP] 상속보다는 조합을 사용하자
[OOP] 상속보다는 조합을 사용하자
2023.03.15 -
[OOP] instanceof의 사용을 지양하라
[OOP] instanceof의 사용을 지양하라
2023.03.14