JAVA 원시값 포장

sameul__choi·2022년 3월 5일
4

새싹스터디 🌱

목록 보기
3/5

원시값 포장이란 무엇인가 ? 왜 해야하며, 원시값 포장을 했을 때 어떤 이점이 있는가 ?

원시값 포장이라는 말을 들었다. 모든 원시값과 문자열을 포장하라 객체지향 체조 원칙에서 나온 말이다.

  1. Wrap All Primitives And Strings

원시값 포장이 무엇인가 ?

원시값 포장은 원시유형의 값을 이용해 의미를 나타내지 않고, 의미 있는 객체로 포장한다는 개념이라고 볼 수 있다. (Collection으로 선언한 변수도 포장한다. 이를 일급 컬렉션이라고 하며 추후에 블로그 포스팅을 진행하려한다.)

변수를 선언하는 방법에는 두 가지가 있다.

int age = 20;
Age age = new Age(20);

바로 원시타입의 변수를 선언하는 방법과, 원시 타입의 변수를 객체로 포장한 변수를 선언하는 방법이 있다.

그렇다면 원시값 포장을 하는 이유는 무엇인가 ?

원시값 포장은 Primitive Obsession Anti Pattern(도메인의 객체를 나타내기 위해 primitive 타입을 사용하는 나쁜 습관)을 피하기 위함이다.

원시값 포장을 함으로써 안티 패턴을 피함으로써 얻을 수 이점들이 있기 때문이다.

00 자신의 상태를 객체 스스로 관리한다.

public class User {
    private int age;

    public User(int age) {
        this.age = age;
    }
}

위의 예제 코드처럼 원시 타입인 int로 나이를 가지고 있다면 어떻게 될까 ? 가장 먼저 생각 나는 것은 나이에 관한 유효성 검사를 User 클래스에서 하게 된다는 점이다.

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

따라서 위와 같은 코드를 작성하게 된다. 지금은 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 클래스가 할일이 늘어났다. 이름에 대한 상태관리, 나이에 대한 상태관리를 모두 해야한다. 이는 그렇게 좋은 코드가 아니다. 한 클래스는 한가지 역할만 하는 것이 좋다.

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살부터 시작합니다.");
        }
    }
}

이름과 나이 값을 포장하여 예외처리라는 동작을 Name, Age가 담당하도록 위임하였다.
즉, 책임이 명확해졌다 ! !

01 유지보수에 도움이 된다.

원시값 포장을 하게 되면 명시적으로 값의 의미를 부여할 수 있게 된다. 때문에 개발자가 달라지더라도 일관된 타입을 전달할 수 있다.

더 나아가서 예제 코드와 함께 살펴 보자.

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

로또 숫자의 범위를 1~45가 아닌 1-10으로 바꾼다고 가정해보자. 그렇다면 WinningLotto의 예시처럼 로또 숫자가ㅏ 원시값이라면 같은 조건의 로또 숫자가 사용되는 WinningLotto 클래스와 Lotto 클래스를 모두 고칠 수 밖에 없어진다.

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

원시값인 개별 로또 숫자를 위처럼 LottoNumber로 포장만 해준다면, 로또 숫자의 확장, 변경에 대해 유연해지게된다 Lotto와 WinningLotto는 변경할 필요 없이 로또 숫자를 포장한 LottoNumber만 수정해주면 되기 때문이다.

02 자료형에 구애 받지 않음 (여러 타입 지원 가능)

점수라는 값을 포장한 Score 클래스가 있다. 현재 점수는 int 값이다.

public class Score {
    private int score;

    public Score(int score) {
        validateScore(score);
        this.score = score;
    }
    ...
}

점수를 보여주는 역할만 했던 Score객체에 연산 등의 기능이 추가되어 새로운 자료형의 지원이 필요해졌다면? 기존의 Score변수를 없앨 필요가 없다.

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 클래스를 활용하면 된다. doubleScore라는 멤버변수를 추가하고, 생성자 오버로딩을 통해 간단히 해결할 수 있다. String 값이 필요하다 해도 마찬가지로 해결이 가능하다.

마치며

우테코 프리코스에 참여하면서 원시값을 포장하면 좋다는 말을 들었었다. 그때 요구사항에는 원시값을 포장하라는 말이 없어서, 개의치 않고 포장하지 않았었는데포장했다면 더욱 객체지향의 향기가 나는 코드를 작성할 수 있었지 않았나라는 생각을 한다. 페어프로그래밍 도중 원시값 포장이라는 키워드를 또 접하게되어 정리해보았다. 원시값 포장을 통해 책임관계가 보다 명확해지고 유지보수가 용이한 코드를 작성할 수 있다는 점을 배울 수 있었다.

0개의 댓글