리팩토링 23. 참조를 값으로 바꾸기

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다. 


    리팩토링 23. 참조를 값으로 바꾸기 (Change Reference to Value)

    • 레퍼런스(Reference) 객체 vs 값(Value) 객체
      • https://martinfowler.com/bliki/ValueObject.html 
      • "Objects that are equal due to the value of their properties, in this case their x and y coordinates, are called value objects."
      • 값 객체는 객체가 가진 필드의 값으로 동일성을 확인한다.
      • 값 객체는 불변 객체다. 따라서 내부적으로 사용되는 변경 포인트를 줄이는 작업이 될 수 있음. 
    • 값 객체는 언제 사용할까?
      • 변경점을 전파하고 싶다면 레퍼런스 타입을 사용한다.
      • 변경을 전파하고 싶지 않다면 값 객체를 사용한다. 
    • 값 객체를 구현할 때는 반드시 equals, HashCode를 재정의 해야함. 
      • 기본 equals, Hashcode는 주소만 보고 같은 객체인지 확인함.
      • 값 객체는 가지고 있는 필드가 같을 때 같은 값이라 판단해야 하기 때문에 equals, HashCode를 재정의 해야함.

    이번 냄새의 요지는 가변 데이터가 많이 존재한다면 사이드 이펙트가 발생할 여지가 있기 때문에 가능하면 가변 데이터를 제거하자는 것이다. 이 리팩토링에서는 만약 변경점을 전파하고 싶지 않은 변수라면, 이것을 값 객체(Value Object)로 만들어서 가변 객체 → 불변 객체로 바꾸어 가변 객체를 제거하자는 것이다. 


    Reference 객체, Value 객체는 언제 사용할까?

    • 객체의 변경 사항을 전파하고 싶음 → 레퍼런스 객체를 사용. (일반적인 클래스)
    • 객체의 변경 사항을 전파하고 싶지 않음. → 값 객체, Record 사용. 

    코드 살펴보기

    아래 코드에서 TelephoneNumber는 레퍼런스 객체로 사용되고 있다. 왜냐하면 Setter가 공개적으로 열려있어서 언제든지 값이 변경될 수 있기 때문이다. 이런 가변 객체는 요구 사항에 맞다면 불변 객체(Value Object)로 변경하는 것이 좋다. 그렇다면 Value Object로 변경하려면 어떻게 해야할까?

    1. 생성자를 통해서만 값을 받고, Setter를 제거한다.
    2. 모든 필드를 final로 설정한다.
    3. Hashcode + Equals를 재정의한다. 
    // 현재 TelephonNumber는 Value 객체가 아니다. Setter가 존재하기 때문에 언제든지 값이 변할 수 있기 때문이다.
    public class TelephoneNumber {
    
        private String areaCode;
        private String number;
    
        public String areaCode() {
            return areaCode;
        }
    
        public void areaCode(String areaCode) {
            this.areaCode = areaCode;
        }
    
        public String number() {
            return number;
        }
    
        public void number(String number) {
            this.number = number;
        }
    }

    위의 고려 사항을 반영해서 레퍼런스 객체를 값 객체로 수정한 결과는 아래와 같다. 이렇게 작성하면, TelephoneNumber는 이제 Value Object로 불변 형태로 사용할 수 있다. 

    // 현재 TelephonNumber는 Value 객체가 아니다. Setter가 존재하기 때문에 언제든지 값이 변할 수 있기 때문이다.
    // 1. final 필드로 설정 / 2. Setter 제거. / 3. equals + hashcode() 오버라이딩.
    public class TelephoneNumber {
    
        private final String areaCode;
    
        private final String number;
    
        public TelephoneNumber(String areaCode, String number) {
            this.areaCode = areaCode;
            this.number = number;
        }
    
        public String areaCode() {
            return areaCode;
        }
    
        public String number() {
            return number;
        }
    
        // 값 객체를 위한 equals, hashCode 정의
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            TelephoneNumber that = (TelephoneNumber) o;
            return Objects.equals(areaCode, that.areaCode) && Objects.equals(number, that.number);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(areaCode, number);
        }
    }

    그런데 불변 객체이기 때문에 기존의 클라이언트의 코드도 변경이 되어야 한다. 

    • 레퍼런스 객체를 사용할 때는 값이 변경될 수 있었다. 따라서 클라이언트쪽에서 Setter를 호출해서 객체의 상태를 변경해서 사용할 수 있었다. 
    • 불변 객체를 사용할 때는 값이 변경될 수 없다. 따라서 기존 방식대로는 사용할 수 없고, 새로운 객체를 생성해서 참조하는 형식으로 사용해야만 한다. 
    public class Person {
    
        private TelephoneNumber officeTelephoneNumber;
    
        public String officeAreaCode() {
            return this.officeTelephoneNumber.areaCode();
        }
    
        public void officeAreaCode(String areaCode) {
            //this.officeTelephoneNumber.areaCode(areaCode);
            // 사용하는 쪽에서는 세터가 없으니, 새로운 객체를 만들어서 사용해야 한다.
            this.officeTelephoneNumber = new TelephoneNumber(areaCode, this.officeNumber());
        }
    
        public String officeNumber() {
            return this.officeTelephoneNumber.number();
        }
    
        public void officeNumber(String number) {
            // this.officeTelephoneNumber.number(number);
            // 사용하는 쪽에서는 세터가 없으니, 새로운 객체를 만들어서 사용해야 한다.
            this.officeTelephoneNumber = new TelephoneNumber(this.officeAreaCode(), number);
        }
    }

    자바 14 이후의 Value 객체는? → Record 사용

    자바 14 이후의 Value Object는 Record를 이용하면 아주 손쉽게 구현할 수 있다. 위에 우리가 하나씩 구현했던 모든 것이 Record를 사용하면 자동으로 구현된다. 

    public record TelephoneNumberRecord(String areaCode, String number) {
    }

    Equals와 HashCode를 같이 구현해줘야 하는 이유는?

    자바가 기본적으로 제공하는 Equals, Hashcode는 '같은 주소를 가지면 같은 객체로 판단'한다. 하지만 값 객체는 '가지고 있는 필드의 값이 같으면 같은 객체로 판단'해야한다. 따라서 Equals + HashCode를 재정의 해줘야한다. 

    또한, 값 객체가 Collection 객체에 들어갈 때를 생각해보자. 이 때, Collection은 Hashcode를 이용해서 객체가 같은지를 구별하는데, 값이 같으면 해쉬 값이 같도록 작성해야한다.  따라서 반드시 해쉬코드도 같이 구현해줘야 한다. 

    댓글

    Designed by JB FACTORY