티스토리 뷰

Java

[Java] 원시값을 포장해 사용하자

Jaehee Jeon 2023. 2. 26. 23:25
반응형

객체지향 생활체조 원칙에선 '모든 원시값과 문자열을 포장한다'라는 규칙이 존재한다. 객체지향적 설계를 위해 원시값을 포장해 얻는 이점이 무엇이며, 어떻게 사용하면 좋을까?


원시값을 포장하여 책임을 분산시키자

모의 로또 게임을 위한 값을 입력받는 상황을 가정하자. 원시값을 포장하면 아래와 같이 새로 입력받은 값에 대한 검증을

    private List<Integer> getLottoNumbers() throws Exception {
        List<Integer> lottoNumbers = new ArrayList<>();
        for (int i = 0; i < 6; i++) {
            int lottoNumber = readLottoNumber();
            validateLottoNumber(lottoNumber);
            lottoNumbers.add(lottoNumber)
        }
        return lottoNumbers;
    }

 

객체가 생성되며 알아서 검증되게 할 수 있다.

 

    class LottoNumber {
        private int lottoNumber;
        public LottoNumber(int lottoNumber) {
            validateLottoNumber();
            this.lottoNumber = lottoNumber;
        }
    }

 

현재는 간단하나 값에 대해 처리해야 할 행위들이 늘어나면 그 값을 처리하는 getLottoNumbers()의 책임이 증가하나, 값을 포장하여 검증 등의 책임을 분산시킬 수 있다.

또한 VO와 유사하게 값의 상태에 대한 행위를 내장시킬 수 있다.

블랙잭 게임을 설계한다 가정하자. 점수에 따라 블랙잭 여부나 Bust 여부 등을 판별하는 비즈니스 로직이 필요하다.

 

    public void logic(int score) {
        if (isBust(score)) {
            // 로직...
        }
    }

    private boolean isBust(int score) {
        if (score > 21) {
            return true;
        }
        return false;
    }

 

위와 같이 점수에 대한 비즈니스 로직을 필요할 때 각각 만들어 처리하기보다

 

class Score {
    int score;

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

    public boolean isBust() {
        return score > 21;
    }
}

 

윈시값을 포장한 Score클래스에 기능들을 내장시켜 상태에 대한 행위들을 일괄 처리, 관리하면 편리하다.

 

의도를 명확하게 하여 실수를 방지한다.

동일한 자료형을 파라미터로 받는 경우의 혼동을 방지한다.

 

    public void logic(int userId, int userScore) {
        // logic
    }

 

IDE가 많은 도움을 주긴 하지만, 같은 자료형 여러 개를 입력받는 경우 사용자의 입장에선 여전히 그 순서를 혼동할 수 있다.
아래와 같이 포장된 원시값을 처리하여 실수를 방지할 수 있다.

 

    public void logic(UserId userId, UserScore userScore) {
        // logic
    }

 

중요한 값에 대한 변경 가능성도 제한할 수 있다. 포장하지 않았다면 position이라는 상태는 변경에 매우 취약하나, 아래와 같이 구성하면 position라는 상태에 대해 1만큼만 변경하는 의도를 강제할 수 있다.

 

class Position {
    private int position;

    public void increase() {
        position++;
    }

    public Position decrease() {
        position--;
    }  
}

 


원시값을 포장하면 반필수적으로 아래의 내용들을 고려해야 한다.

 

equals(), hashCode() 오버라이딩

포장된 값들은 이제 객체이므로, 기존과 달리 상태만으로 비교될 수 없다.

 

    public void run() {
        Score score1 = new Score(0);
        Score score2 = new Score(0);
        System.out.println(score1 == score2); // false
    }

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

 

equals()를 오버라이딩하여 Score클래스 비교시에는 객체의 해시값을 비교하는 것이 아닌, 객체의 상태인 score를 비교하도록 하자.

더 나아가서 HashMap과 같이 해당 객체의 해시값을 사용하는 자료구조를 이용하기 위해서는 같은 상태를 가지는 값은 같은 객체를 의미하도록 hashCode()또한 오버라이딩하자.

 

public class Score {
    private int score;

    public Score(int score) {
        this.score = score;
    }
    // getter 등..

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Score)) {
            return false;
        }
        Score score = (Score)o;
        return Objects.equals(this.score, score.getScore());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(score);
    }
}

 

toString() 오버라이딩

위의 내용과 마찬가지로, 로깅을 해야한다면 거의 필수적으로 toString()까지 오버라이딩하여 포장된 상태를 나타내도록 하자.
toString()까지 재정의되면 객체로 변환되어 생기는 불편을 거의 상쇄할 수 있다.

 


 

 간단한 값을 다루기 위해 오버라이딩해야하는 것들도 생기고, 추상화 단계도 올라가며, 값을 포장하고 getter를 호출하여 꺼내는 불편함도 동시에 수반된다. 값을 포장하거나 풀어낼 책임이 있는 Class를 무엇으로 할 지도 매우 중요하다. 본인이 설계하는 시스템에 대해 이해하고, 1. 어떠한 상태가 분명한 의도에 따라서만 변화해야 하거나, 2. 상태에 대한 반복적인 행위가 필요한 경우에 해당 상태를 포장하는 것을 고려하자.

반응형