본문 바로가기
☕️ Java

"원시값 포장"에 대해 알아보자

by dev_writer 2023. 11. 1.

서론

원시값 포장은 객체지향 생활체조로 유명한 소트웍스 앤솔러지에도 기록되어 있는 내용입니다.

 

원시값 포장을 하면 더욱 객체지향적으로 설계할 수 있다는데, 왜 그런 것일까요? 먼저 원시값 포장을 하기 전의 코드를 보겠습니다.

public class Car {

    // 상수 등 기타 필드
    
    private final String name;
    
    private Car(final String name) {
        this.name = name;
    }
    
    // 정적 팩터리 메서드 등 기타 다른 메서드들
    
    public boolean isNameEqualTo(final String name) {
        return name.equals(this.name);
    }
    
    public String getName() {
        return this.name;
    }
}

예시를 봤을 때 어떤가요? 일반적인 자바 클래스와 별 다른 게 없어 보입니다.

 

이제 본격적으로 원시값 포장에 대해 알아보겠습니다!


원시값 포장이란

원시값 포장은 원시 타입의 값의미 있는 객체로 포장하는 것을 뜻합니다. 소트웍스 앤솔러지에서는 모든 원시값과 문자열을 포장(wrap)한다는 것이 언급되어 있습니다.

원시 타입

다소 생소할 수도 있는 원시 타입을 위키백과에서 찾아보면, 다음과 같이 기술되어 있습니다.

원시 자료형(primitive data type)은 컴퓨터 과학에서 프로그래밍 언어가 제공하는 자료형 중 하나다.
원시형은 또한 내장형이나 기본형으로도 불린다.
언어와 그 구현에 따라서, 원시형은 메모리 상에 일대일로 대응되는 개체(Object)를 가질 수도 있다.

일반적으로 원시형은 다음과 같다.
- 문자 (character, char);
- 다양한 산술 정밀도를 갖는 정수 (integer, int, short, long, byte);
- 부동소수점수 (float, double, real, double precision);
- 다양한 정밀도와 프로그래머가 지정 가능한 크기를 갖는 고정소수점수 (fixed).
- 불린 '참(true)'과 '거짓(false)' 값을 갖는다.
- 참조 (포인터 또는 핸들로 불림), 다른 개체(Object)의 메모리상 주소를 참조하는 값.

이해되셨나요? 즉, 위의 예시에서는 String 형태인 name이 원시값 포장이 가능한 예시가 될 수 있는 것입니다!


의미 있는 객체로 포장

그러면 이제 이 name을 의미 있는 객체로 포장해 봅시다.

클래스 이름 컨벤션

보통 자바 코드를 보면, int를 클래스로 만들 때 Integer, long을 클래스로 만들 때 Long으로 쓰는 등 첫 문자를 대문자로 표현합니다. 이 규칙에 맞추어, name을 Name이라는 클래스로 포장하겠습니다.

생성된 Name 클래스

public class Name {

    private final String name;
    
    private Name(final String name) {
        this.name = name;
    }
    
    public static Name from(final String name) {
        return new Name(name);
    }
    
    public boolean isYourName(final String name) {
        return name.equals(this.name);
    }
    
    public String getName() {
        return this.name;
    }
}

이 Name 클래스를 Car 클래스에게 연결하면 Car 코드는 다음과 같이 변경됩니다.

public class Car {

    // 상수 등 기타 필드
    
    private final Name name;
    
    private Car(final String name) {
        this.name = Name.from(name);
    }
    
    // 정적 팩터리 메서드 등 기타 다른 메서드들..
    
    public boolean isNameEqalTo(final String testName) {
        return this.name.isYourName(testName);
    }
    
    public String getName() {
        return name.getName();
    }
}

이렇게 원시값을 포장하면 어떤 점이 좋은 것일까요?


원시값 포장의 장점

객체에게 자율성을 제공할 수 있다.

객체 공동체에 속한 객체들은 공동의 목표를 달성하기 위해 협력에 참여하지만 스스로의 결정과 판단에 따라 행동하는 자율적인 존재다. - 객체지향의 사실과 오해, 32p

원시값 포장을 활용하면 해당 객체에게 자율성을 제공할 수 있습니다. 기존에는 name이 Car에 종속되어 이름이 주어진 이름과 같은지 비교할 때도 Car에서 이루어졌지만, Name 클래스를 만들고 isYourName 메서드를 만듦으로써 Name에게 자율성을 제공하였다고 할 수 있습니다.

적절한 책임 부여가 가능하며, 유지보수성을 높일 수 있다.

시스템의 기능은 더 작은 규모의 책임으로 분할되고 각 책임은 책임을 수행할 적절한 객체에게 할당된다. 객체가 책임을 수행하는 도중에 스스로 처리할 수 없는 정보나 기능이 필요할 경우 적절한 객체를 찾아 필요한 작업을 요청한다. - 객체지향의 사실과 오해, 132p

또한 Car 객체에 부여되어 있는 책임을 분할함으로써 더 완성도 있는 객체지향적 코드를 짤 수 있습니다. 예시로 자동차 경주 미션에서는 다음과 같은 조건이 있습니다.

자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.

여기에서 이름은 5자 이하만 가능하다에 주목합시다. 만약 원시값 포장을 만들지 않았다면 어떻게 될까요? 이렇게 검증과 같은 기능들이 생길 때마다 그것을 사용하는 Car 객체 등에게 직접 할당해줘야 할 것입니다. 이러면 적절한 객체에게 적절한 행위가 간 것이 아니라고 할 수 있겠죠? (반면 Name을 만들면 Name은 자율적인 객체가 되며, 이는 테스트를 할 때도 따로 빼내올 수 있는 등 유지보수성이 올라갑니다!)

 

더불어, 만약 name이 여러 클래스에서 사용된다면 name이 사용되는 모든 클래스들에 다 들어가서 일일이 기능들을 작성해야 할 것입니다.

 

따라서 정리하자면, 원시값 포장은 적절한 객체에게 적절한 책임을 부여하는 데 일조하며, 유지보수성 또한 높여줄 수 있다는 장점이 있습니다.


번외: 검증 로직은 어디에서 처리하는 게 좋을까?

원시값 포장을 잘 활용하던 도중, 검증 로직을 어디에서 처리해야 할지 고민이 된 적이 있습니다. 크게 생성자에서 호출할 것인지, 정적 팩토리 메서드에서 호출할 것인지였습니다. (이펙티브 자바를 읽은 이후부터 정적 팩토리 메서드로 클래스를 생성하고 있습니다.)

 

내린 결론은, 정적 팩터리 메서드에서 호출하도록 하는 것이 더 적합하다는 생각입니다.

 

예시 코드를 보겠습니다.

public class Age {

    private final int age;
    
    private Age(final int age) {
        this.age = age;
    }
    
    public static Age from(final String age) {
        validateIsPositiveNumber(age);
        return new Age(Integer.parseInt(age));
    }
    
    private static void validateIsPositiveNumber(final String age) {...}
}

만약 콘솔 환경에서, 이름을 입력한 뒤 Age 클래스를 생성한다고 가정하겠습니다. (검증 조건은 양수여야 한다는 것입니다.)

 

이름을 "입력" 한다는 것은, 처음에는 문자열 형태라는 것이겠죠? 그런데 생성자 단에서 직접 검증을 하려면, 결국 인자를 String으로 받아야 합니다. String 형태의 값을 검증한 다음, 문제가 없을 경우에야 int로 변환해서 저장할 테니까요.

 

우리가 보통 클래스의 생성자를 정의할 때는, 클래스에 존재하는 필드의 타입들이 생성자에 있는 인자들의 타입과 서로 일치하는 것을 볼 수 있었습니다. 이렇게 컨벤션을 맞추기 위해서라도 정적 팩토리 메서드에서 호출하도록 하는 게 맞을 것 같다는 결론을 내렸습니다.