Primitive Obsession
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 가 중요한 역할을 한다는 것은 알고가자
그러면 항상 원시값 포장을 적용해야 하는가??
원시값 포장을 하게 된다면 객체 생성 비용이 커질 수 있다
그러므로 항상 적용하는건 비효율적이라고 생각한다
그러나, 해당 필드가 어떤 로직을 이행해야 한다면 (검증이라던가..) 혹은 '원시타입 필드' 이상의 의미를 가지고 있다면
원시값 포장은 그 필드를 가지고 있는 객체의 책임을 덜어준다는 것에 의의가 있다고 생각한다