Java/클린코드

Primitive Obsession

김관현 2024. 7. 30. 21:52

Primitive Obsession

-> 원시타입 강박

코드가 Primitive Type(원시 타입)에 너무 많이 의존할 때를 말한다

도메인의 객체를 나타내기 위해 primitive type 을 사용하는 것을 피해야 한다

 

원시타입 강박을 해결하는 방법들과 그로인해 얻게 되는 이점은 무엇이 있는지 알아보자

 

 

원시값 포장

코드를 먼저 보고 이해해보자

 

원시값 포장 적용 전

public class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        if (score < 0 || score > 100) {   // name 에도 조건을 걸어버리는 경우 더욱 복잡해진다
            throw new IllegalArgumentException();     
        }
        
        this.name = name;
        this.score = score;
    }
}

 

 

score 에 원시값 포장 적용

public class Student {
    private String name;
    private Score score;    // int score -> Score score

    public Student(String name, Score score) {
        this.name = name;
        this.score = score;
    }
}

 

public class Score {
    private int score;

    public Score(int score) {
        if (score < 0 || score > 100) {  // score 에 대한 검증은 Score 객체가 실행함
            throw new IllegalArgumentException();
        }
        this.score = score;
    }
}

 

 

원시값 포장을 적용하지 않은 코드는 Student 가 직접 score 에 대한 검증을 진행하였다

Student 의 필드가 name 과 score 밖에 없고 검증을 score 에서만 진행하였음에도 불구하고 가독성이 그렇게 좋지 않다

물론 메서드를 따로 추출해버리는 방법도 있긴하지만 Student 가 너무나도 많은 일을 하고 있음은 바뀌지 않는다

 

또한 score 를 여러 곳에서 사용하는 경우 중복이 생길 확률이 높고

실수로 값이 변경될지도 모르고

새로운 개발자가 부적절하게 score 를 사용해버릴지도 모르고

수정해야 하는 경우 수많은 클래스들을 뒤져보아야 할 수 있으며

score 값이 여러 클래스에 뿌려져 있는 경우 로직을 찾는데 한참 걸릴 수 있다

 

 

하지만 밑에 score 에 원시값 포장을 적용한 코드를 보자

위에 서술한 원시타입 사용으로 인한 단점들을 모두 개선하였다

 

Score 는 생성자를 통해서만 만들어지므로 부적절하게 사용하기 어렵다

 

Score 클래스를 따로 만들어서 score 에 대한 검증을 Score 클래스에서 진행하였다

 -> 자신의 상태를 객체 스스로 관리한다

 

유지보수가 용이하다

만약 Score 에 관해서 추가적인 요구사항이 생기는 경우 원시타입을 썼다면 전부 다 뜯어고쳐야 할 확률이 높다

 (갑자기 소수점까지 확장된다던지..)

 

하지만 원시값 포장으로 인해 Score 클래스에서 조금만 수정하거나 멤버 변수를 추가, 오버로딩 등을 통해 간단히 해결해버릴 수 있다

 

또한 원하는 목적과 의도를 이름만으로도 알 수 있게 해준다

(반환 타입에 int 보다 Score 가 있는게 직관적이지 않을까)

 

더불어 개념을 문서화하기도 용이하니 얼마나 유용한가

 

그런데 이러한 원시값 포장에서 한단계 더 나아간 값 객체(Value Object) 라는 것이 있다

 

 

값 객체(Value Object)

데이터를 나타내고 표현하는 객체

기본 자료형으로 표현할 수 있더라도 클래스로 캡슐화하여 의미를 명확하게 하고 값도 표현할 수 있다.

(대표적으로 금액, 날짜, 좌표 등등)

 

엔티티와는 다르다 + 물론 원시값 포장과도 다르다(더 좁은 개념)

 

값 객체가 가지고 있는 기본적인 장점들은 원시값 포장이 가지고 있는 장점과 비슷하다

 

특성

1. 불변성 (immutable)

public final class Score {  // final
    private final int score;   // private & final

    public Score(int score) {  // 생성자를 통해서만 생성
        if (score < 0 || score > 100) {
            throw new IllegalArgumentException();
        }
        this.score = score;
    }
}

 

값 객체는 절대 불변해야한다 (불변 객체여야 한다)

값을 바꿀 수 있는 로직이 절대 존재해서는 안된다!!

 

멤버 변수에 final 을 줬다고 안심해서는 안된다

final 은 재할당을 막는 것이지 변경을 막지 않는다

 

불변성은 멀티스레드 상황에서 매우 유용하게 사용한다

 

2. 동등성 (Value Equality)

 

값이 같은 두 객체는 서로 동일한 것으로 본다

관현.score = new Score(80);
철수.score = new Score(80);

 

여기서 관현과 철수는 '동일한' score 를 가진다

똑같은 80점을 받은 것이다

 

VO 가 이런 동등성을 지키기 위해서는 equals 와 hashCode 를 재정의 해야한다

 

equals 와 hashCode 를 재정의 할때는 각각 규약을 지켜서 재정의하자

-> 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다!!

 

 

3. 자가 유효성 검사 (Self-Validation)

 

원시값 포장에서 들었던 예시이다

score 의 검증을 Score 에서 처리한다

 

따라서 score 를 사용하는 쪽에서 안전하게 사용할 수 있다

 

 

원시값 포장과 VO 는 결국 같은 것인가?

다르다!

 

VO는 Thread Safe 하지만 원시값 포장은 절대 그렇지 않다

 

원시값 포장에서 더 좁은 조건을 달아둔 것이 VO 이다

 

둘의 차이를 인지하는 것이 중요한 요소는 아니지만 둘 다 활용하려고 노력하고 멀티스레드 상황에서 VO 가 중요한 역할을 한다는 것은 알고가자

 

 

그러면 항상 원시값 포장을 적용해야 하는가??

원시값 포장을 하게 된다면 객체 생성 비용이 커질 수 있다

그러므로 항상 적용하는건 비효율적이라고 생각한다

 

그러나, 해당 필드가 어떤 로직을 이행해야 한다면 (검증이라던가..) 혹은 '원시타입 필드' 이상의 의미를 가지고 있다면

원시값 포장은 그 필드를 가지고 있는 객체의 책임을 덜어준다는 것에 의의가 있다고 생각한다