일상/레벨업 독서

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

Gamii 2024. 7. 4. 10:00
728x90

1. final 수식자를 붙여 재할당을 피하자.

변수 하나를 재활용하지 않고, 계속해서 새로운 변수를 만들어 사용하면 재할당을 피할 수 있습니다.

 

 

1) 불변 변수로 만들어 재할당 막기

변수에 final 수식자를 붙이면 됩니다. 

void doSomething(){
    final int value = 100;
    value = 200;    //컴파일 오류
}

 

 

 

2) 매개변수도 불변으로 만들기

매개변수를 변경하면 의미가 바뀔 수 있습니다. 이렇게 의미가 바뀌면 코드를 읽는 사람이 헷갈리므로, 버그의 원인이 될 수 있습니다.

 

 

[매개변수 productPrice에 재할당하는 코드]

void addPrice(int productPrice){
    productPrice = totalPrice + productPrice;
    if(MAX_TOTAL_PRICE < productPrice){
    	throw new IllegalArgumentException("구매 상한 금액을 넘었습니다.");
    }
}

 

 

[매개변수에 final을 붙여 불변으로 만들기]

void addPrice(final int productPrice){
    final int increasedTotalPrice = totalPrice + productPrice;
    if(MAX_TOTAL_PRICE < increasedTotalPrice){
    	throw new IllegalArgumentException("구매 상한 금액을 넘었습니다.");
    }
}

 

 

 

 

 

 

 

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

 

 

1) 사례 : 가변 인스턴스 재사용하는 경우

//공격력 클래스
AttrackPower attackPower = new AttackPower(20);

//무기 클래스
Weapon weaponA = new Weapon(attackPower);
Weapon weaponB = new Weapon(attackPower);  //AttackPower 인스턴스 재사용

 

 

 

 

만약, weaponA의 공격력을 향상시기기 위해 20에서 25로 변경했을 때, 아래와 같은 현상이 발생합니다.

//공격력 클래스
AttackPower attackPower = new AttackPower(20);

//무기 클래스
Weapon weaponA = new Weapon(attackPower);
Weapon weaponB = new Weapon(attackPower);  //AttackPower 인스턴스 재사용


weaponA.attackPower.value = 25;

//결과
System.out.println("Weapon A attack power : " + weaponA.attackPower.value);
System.out.println("Weapon B attack power : " + weaponB.attackPower.value);

// 출력
// Weapon A attack power : 25
// Weapon B attack power : 25

 

 

 

WeaponA만 변경을 했지만 WeaponA와 WeaponB에서 같은 참조 값을 가진 AttackPower를 사용하고 있기 때문에, 하나만 변경해도 해당 주소의 값이 변경되어 WeaponB도 변경됩니다. 이러한 상황을 예방하려면, 인스턴스를 재사용하지 못하게 만들어야 합니다.

 

 

 

 

[인스턴스 재할당 해결 코드]

//공격력 클래스
AttackPower attackPowerA = new AttackPower(20);
AttackPower attackPowerB = new AttackPower(20);

//무기 클래스
Weapon weaponA = new Weapon(attackPowerA);
Weapon weaponB = new Weapon(attackPowerB);


weaponA.attackPower.value = 25;

//결과
System.out.println("Weapon A attack power : " + weaponA.attackPower.value);
System.out.println("Weapon B attack power : " + weaponB.attackPower.value);

// 출력
// Weapon A attack power : 25
// Weapon B attack power : 20

 

 

 

 

 

 

 

2) 사례 : 함수로 가변 인스턴스 조작하기

class AttackPower {
    static final int MIN = 0;
    int value;
    
    AttackPower(int value){
    	if(value < MIN){
        	throw new IllegalArgumentException();
        }
        
        this.value = value;
    }
    
    /**
    * 공격력 강화하기
    * @param increment 공격력 증가량
    */
    void reinforce(int increment){
    	value += increment;
    }
    
    /**
    * 무력화하기
    */
    void disable(){
    	value = MIN;
    }
}

 

reinforce를 이용해 공격력을 강화해서 잘 사용하고 있었는데, 어느날 공격력이 0이 되는 현상이 발생했습니다. 확인 결과 다른 스레드에서 attackPower.disable() 메서드를 호출해 공격력이 0이 된 것입니다. 

 

이처럼 AttackPower의 disable 메서드와 reinforce 메서드는 구조적인 문제를 갖고 있습니다. 이를 부수 효과라고 합니다.

 

 

 

 

 

부수 효과의 단점

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

  • 주요 작용 : 함수(메서드)가 매개변수를 전달받고, 값을 리던하는 것
  • 부수 효과 : 주요 작용 이외의 상태 변경을 일으키는 것
    • 상태 변경은 함수 밖에 있는 상태를 변경하는 것
      • 인스턴스 변수 변경
      • 전역 변수 변경
      • 매개변수 변경
      • 파일 읽고 쓰기 같은 I/O 조작

 

 

 

 

다른 스레드에서 호출했던 AttackPower.disable과 AttackPower.reinforce를 실행할 때마다 인스턴스 변수 AttackPower.value의 값이 계속해서 바뀝니다.

 

 

 

따라서 동일한 결과를 내기 위해서는 동일한 순서로 실행해야 합니다. 즉, 작업 실행 순서에 의존하게 되는 것입니다. 이런 코드는 결과를 예측하기 힘들며, 유지 보수하기 힘듭니다.

 

 

 

 

 

[해결 코드]

 

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

  • 데이터(상태)는 매개변수로 받습니다.
  • 상태를 변경하지 않습니다.
  • 값은 함수의 리턴 값으로 돌려줍니다.

 

class AttackPower {
    static final int MIN = 0;
    final int value;		//final로 불변으로 만들었습니다.
    
    AttackPower(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);
    }
    
    /**
    * 무력화하기
    */
    AttackPower disable(){
    	return new AttackPower(MIN);
    }
}

 

 

위 코드처럼 서로 다른 인스턴스를 생성을했으므로 영향을 주지 않습니다.

 

 

 

 

정리

 

기본적으로 불변으로 설계를 해야합니다.

 

 

하지만 성능이 중요한 아래와 같은 경우는 가변으로 설계해야할 수 있습니다.

 

  • 대량의 데이터를 빠르게 처리해야하는 경우
  • 이미지를 처리하는 경우
  • 리소스에 제약이 큰 임베디드 소프트웨어를 다루는 경우
  • 크키가 큰 인스턴스를 새로 생성할 경우
    • 불변일 경우는 인스턴스를 새로 생성해야하기 때문에, 시간이 오래 걸려 성능에 문제가 될 수 있는 경우
  • 스코프가 국소적인 경우
    • 반복문 카운터 등 반복 처리 스코프에서만 사용되는 지역변수는 가변으로 사용