어떤 객체가 정상적으로 작동하기 위해 절때 허무러지지 않아야 하는 값, 식, 상태의 일관성을 보장하기 위해 항상 참이 되는 조건(condition)을 말한다.
자동차를 표현 한 Car라는 객체에는 현재 보유한 연료의 양과 최대로 넣을 수 있는 연료의 양이 있다.
class Car{
private static final int MAX_OIL_VALUE = 1000;
protected int oilValue;
Car(int oilValue){
this.oilValue = oilValue;
}
}
이때 oilValue의 값이 0 미만이 되거나, MAX_OIL_VALUE를 넘어서는 경우는
정상적인 자동차 객체의 상태(State)라고 할 수 없다.
이런 경우를 Car 객체(Object)의 불변식(Invariant)이 깨졌다. 라고 부를 수 있다.
이제 객체의 불변식을 유지하는 방법을 알아보자.
생성자의 최상단에 Assertion 코드를 삽입한다.
class Car{
private static final int MAX_OIL_VALUE = 1000;
protected int oilValue;
Car(int oilValue){
// 불변식(Invariant)를 유지하기 위한 방어적 아규먼트 체크
if(oilValue < 0 && oilValue > MAX_OIL_VALUE){
// 불변 클래스(Immutable Class)가 아니라면 예외를 던질 때
// 실패 원자성(Failure Atomicity)을 고려해야 한다.
throw new IllegalArgumentException();
}
this.oilValue = oilValue;
}
}
Car 객체의 불변식을 유지하기 위해서는 oilValue의 값이 처음 초기화 될 때, 방어적인 값 체크가 필요하다.
세터(setter) 메서드 최상단에 Assertion 코드를 삽입한다.
class Car{
private static final int MAX_OIL_VALUE = 1000;
protected int oilValue;
Car(int oilValue){
// 불변식(Invariant)를 유지하기 위한 방어적 아규먼트 체크
if(oilValue < 0 && oilValue > MAX_OIL_VALUE){
// 불변 클래스(Immutable Class)가 아니라면 예외를 던질 때
// 실패 원자성(Failure Atomicity)을 고려해야 한다.
throw new IllegalArgumentException();
}
this.oilValue = oilValue;
}
public setOilValue(int oilValue){
if(oilValue < 0 && oilValue > MAX_OIL_VALUE){
throw new IllegalArgumentException();
}
this.oilValue = oilValue;
}
}
Car 객체의 불변식을 유지하기 위해서는 oilValue의 값이 변경될 여지가 있는 모든 코드에서 방어적인 값 체크가 필요하다.
구체 클래스를 상속 받지 않는다.
class Car{
private static final int MAX_OIL_VALUE = 1000;
protected int oilValue;
//지붕을 접거나 펼 수 있는 자동차 외형인지의 여부
protected boolean convertible = false;
Car(int oilValue){
// 불변식(Invariant)를 유지하기 위한 방어적 아규먼트 체크
if(oilValue < 0 && oilValue > MAX_OIL_VALUE){
// 불변 클래스(Immutable Class)가 아니라면 예외를 던질 때
// 실패 원자성(Failure Atomicity)을 고려해야 한다.
throw new IllegalArgumentException();
}
this.oilValue = oilValue;
}
public void setOilValue(int oilValue){
if(oilValue < 0 && oilValue > MAX_OIL_VALUE){
throw new IllegalArgumentException();
}
this.oilValue = oilValue;
}
public void setConvertible(boolean convertible){
this.convertible = Objects.requireNonNull(convertible);
}
}
class OpenCar extends Car{
OpenCar(int oilValue){
super(oilValue);
this.convertible = true;
}
@Override
public void setOilValue(int oilValue){
this.oilValue = oilValue;
}
}
상위 클래스인 Car 클래스를 확장(extends)해, 오픈카를 표현 한 OpenCar 클래스를 구현 하였다.
하지만 두 클래스는 서로가 서로의 불변식을 훼손하는 메서드를 외부에 버젓히 공개(public)하고 있다.
Car 객체는 다음과 같은 식을 항상 참으로 유지해야 한다.
oilValue의 값이 0 미만이 되거나, MAX_OIL_VALUE를 넘어서는 안된다.
그러나 OpenCar 클래스의 개발자는 이 사실을 모른 채 setOilValue 메서드를 재정의(override) 하였고, 재정의된 setOilValue 메서드는 상위 클래스의 불변식을 지키지 못했다.
OpenCar 객체는 자동차의 지붕을 열고, 닫는 기능을 항상 지원해야 한다. 따라서 불변식은 다음과 같다.
OpenCar 객체의 convertible 값은 항상 true다.
그러나 구체 클래스인 Car 클래스를 설계한 개발자는 당연히 하위 클래스의 확장성을 고려하지 않았으므로, OpenCar 객체의 불변식은 상위 클래스의 setConvertible 메서드를 호출하는 것 만으로 불변식을 지키지 못하게 되었다.
class OpenCar extends Car{
OpenCar(int oilValue){
super(oilValue);
this.convertible = true;
}
@Override
public void setOilValue(int oilValue){
this.oilValue = oilValue;
}
@Override
public void setConvertible(boolean convertible){
throw new AssertionError();
}
}
만약 setConvertible 메서드를 OpenCar 객체에서 호출하지 못하도록, 절대 받을 수 없는 에러를 던진다면 어떨까? 이는 매우 위험한 발상이다.
OpenCar 클래스가 Car 클래스의 하위 타입이라면, 필요한 프로그램의 속성(정확성, 수행하는 업무 등)의 변경 없이 Car 클래스의 객체를 OpenCar 클래스의 객체로 교체(치환)할 수 있어야 한다.
(SOLID, Liskov Subsitution Principle : 리스코프 치환 원칙)
조금 쉽게 설명하면, *Car 객체가 제공하는 기능은 OpenCar 객체도 똑같이 지원 해야 함을 뜻한다. *
convertible 변수의 값을 이 메서드의 파라미터 값으로 변경 할 수 있다.
Car 객체에 있는 setConvertible 메서드의 위와 같은 기능을 OpenCar 객체가 수행하지 못하므로, 리스코프 치환 원칙을 완전히 어겨, *객체지향 언어의 장점을 스스로 버린 설계라고 할 수 있다. *
클래스를 불변 클래스(immutable class)로 설계한다.
불변 객체는 한번 초기화된 내부의 값, 식, 상태를 다시는 변경할 수 없다.
따라서 불변 클래스의 불변식은 항상 참이다.
Reference
이펙티브 자바 Effective Java 3/E
조슈아 블로크 저/개앞맵시 역 | 인사이트(insight) | 2018년 11월 01일
밀오의 프로그래밍과 데이터베이스
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=salinokl&logNo=221053934445
'객체지향 프로그래밍(Object-Oriented Programming)' 카테고리의 다른 글
불변 클래스(Immutable Class) (0) | 2023.05.10 |
---|
댓글