devFancy BE Developer

[내 코드가 그렇게 이상한가요?] 4장. 불변 활용하기: 안정적으로 동작하게 만들기

2023-11-08
devFancy

이 글은 내 코드가 그렇게 이상한가요? 책을 읽고 정리한 내용을 바탕으로 작성하였습니다.

재할당

  • 재할당은 변수에 값을 다시 할당하는 것을 말하며, 파괴적 할당이라고도 말한다.

    재할당은 변수의 의미를 바꿔 추측하기 어렵게 만들고,

    언제 어떻게 변경되는지 추척하기 힘들게 한다.

  • 따라서 재할당을 막기 위해서는 변수final 수식자를 붙여준다.

    마찬가지로, 매개변수에도 final 수식자를 붙인다.

가변으로 인해 발생하는 의도하지 않는 영향

  • 인스턴스가 가변이면 다른 부분에 의도하지 않은 영향을 주기 쉽다.

부수 효과의 단점

  • 함수의 부수 효과는 함수가 매개변수를 전달받고, 값을 리턴하는 것 이외에 외부 상태(인스턴스 변수)를 변경하는 것을 가리킨다.

  • 조금 더 구체적으로 설명하면, 함수(메서드)에는 주요 작용과 부수 효과가 있다.

    • 주요 작용: 함수(메서드)가 매개변수를 전달받고, 값을 리턴하는 것

    • 부수 효과: 주요 작용 이외의 상태 변경을 일으키는 것

  • 여기서 상태 변경이란 함수 밖에 있는 상태를 변경하는 것을 의미한다. 예를 들어 다음과 같은 것이다.

    • 인스턴스 변수 변경

    • 전역 변수 변경

    • 매개변수 변경

    • 파일 읽고 쓰기 같은 I/O 조작

  • 작업 실행 순서에 의존하는 코드는 결과를 예측하기 힘들며, 유지 보수하기 힘들다.

함수의 영향 범위 한정하기

  • 부수 효과가 있는 함수는 영향 범위를 예측하기 힘들다.

  • 따라서 예상치 못한 동작을 막으려면, 함수가 영향을 주거나 받을 수 있는 범위를 한정하는 것이다.

  • 함수는 다음 동작을 만족하도록 설계하는 것이 좋다.

    • 데이터(상태)는 매개변수로 받는다.

    • 상태를 변경하지 않는다.

    • 값은 함수의 리턴 값으로 돌려준다.

  • 따라서 매개변수로 상태를 받고, 상태를 변경하지 않고, 값을 리턴하기만 함수가 이상적이다.

불변으로 만들어서 예기치 못한 동작 막기

  • 지금까지 설명한 방식에 따라, 예상하지 못한 동작을 막기 위해 불변을 기반으로 코드를 설계한다.

  • 부수 효과의 자체를 없애는 방법은 간단하다. 인스턴스 변수final 수식자를 붙여서 불변으로 만들면 된다.

원시(기본) 타입(primitive type): byte, char, short, int, long, float, double, boolean

참조 타입(reference type): 배열 타입, 열거 타입, 클래스, 인터페이스

참고로, int형과 같은 원시 타입(primitive)은 참조값이 존재하지 않기 때문에 외부에서도 그대로 불변 으로 존재하게 된다.

하지만, 참조 타입인 객체나 Array, List와 같은 컬렉션일 경우에는 불변을 보장하려면 setter를 포함하지 않아야 하며, getter 사용시 방어적 복사를 통해 값을 전달해야 한다. 또한, 참조 변수 객체 내부 또한 불변이어야 불변이 성립한다.

  • 불변 변수로 만들면 변경할 수 없기 때문에, 변경된 값을 사용하고 싶다면 새로운 값을 가진 새로운 인스턴스 변수를 만들어서 사용해야 한다.

  • 아래 코드의 reinforce 메서드와 disable 메서드처럼 AttackPower 인스턴스를 새로 생성하고 리턴하는 구조로 변경한다.

class AttackPower() {
    static final int MIN = 0;
    final int value;    // final로 불변으로 만들기

    AttackPower(final int value) {
        if(value < MIN) {
            throw new IllegalArgumentException();
        }
        
        this.value = value;
    }

    /**
     * 공격력 강화하기
     * @param increment 공격력 증가량
     * @return 증가된 공격력
     */
    AttackPower reinforce(final AttackPower increment) {
        return new AttackPower(this.value + increment.value); // 인스턴스를 새로 생성하고 리턴하는 구조로 변경
    }

    /**
     * 무력화하기
     * @return 무력화한 공격력
     */
    AttackPower disable() {
        return new AttackPower(MIN); // 인스턴스를 새로 생성하고 리턴하는 구조로 변경
    }
}

불변과 가변은 어떻게 다루어야 할까?

  • 지금까지 설명한 것처럼 변수를 불변으로 만들면 다음과 같은 장점이 있다.

    • 변수의 의미가 변하지 않으므로, 혼란을 줄일 수가 있음.

    • 동작을 안정적이게 되므로, 결과를 예측하지 쉬움

    • 코드의 영향 범위가 한정적이므로, 유지 보수가 편리해짐

  • 따라서 기본적으로는 불변으로 설계하는 것이 좋다. (이 책에서도 불변을 표준 스타일로 사용한다)

가변으로 설계해야 하는 경우

  • 기본적으로 불변으로 설계하는 것이 좋지만, 가변이 필요한 경우도 있다.

  • 바로 성능(performance)이 중요한 경우이다.

    • 대량의 데이터를 처리해야 하는 경우,

    • 이미지를 처리하는 경우

    • 리소스에 제약이 큰 임베디드 소프트웨어를 다루는 경우

  • 위와 같은 예시에서는 가변을 사용하는 것이 좋을 수 있다.

  • 불변이라면 값을 변경할 때 인스턴스를 새로 생성해야 한다.

  • 만약 크기가 큰 인스턴스를 새로 생성하면서 시간이 오래 걸려 성능에 문제가 생긴다면 불변 보다는 가변을 사용하는 것이 좋다.

상태를 변경하는 메서드 설계하기

  • 어떤 게임에서 히트포인터에 대한 기본적인 조건은 다음과 같다.

    • 히트포인트는 0이상

    • 히트포인트가 0이 되면, 사망 상태로 변경

  • 히트포인트가 0이 될때 뮤테이터(mutater)를 통해 사망 상태로 변경하자.

뮤테이터(mutater): 상태를 변화시키는 메서드

class Hitpoint {
    ...

    /**
     * 대미지 받는 처리
     * @param damageAmount 대미지 크기
     */
    void damage(final int damageAmount) {
        hitPoint.damage(damageAmount) {
            if(hitPoint.isZero) {
                states.add(StateType.dead); // 사망 상태로 변경
            }
        }
    }
}

Reference


Comments

Index