Notice
Recent Posts
Recent Comments
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

SOLID (2) - 개방-폐쇄 원칙, OCP 본문

Design Pattern/References

SOLID (2) - 개방-폐쇄 원칙, OCP

defacto standard 2017. 10. 28. 18:57

개방-폐쇄 원칙 (OCP, Open-Closed Principle)

기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다.


단일책임 원칙에서 예를 든 성적표나 출석부에 학생의 성적이나 출석 기록을 출력하는 기능을 생각해보자.

다음 그림은 SomeClient 클래스에서 이 기능을 이용하는 경우를 모델링한 것.

 

성적표나 출석부에 학생을 출력하는 기능을 사용


만약 도서관 대여 명부와 같은 새로운 매체에 학생의 대여 기록을 출력하는 경우라면, 아주 간단한 방식으로 도서관 대여 명부 클래스를 만들어 SomeClient 클래스가 이 기능을 이용하도록 할 수 있다. 그러나 이 방식은 OCP를 위반한다.

 

새로운 기능(도서관 대여 명부에 학생의 대여 기록을 출력)을 추가하려고 SomeClient 클래스를 수정해야 하기 때문이다.

OCP를 위반하지 않은 설계를 할 때 가장 중요한 것은 무엇이 변하는 것인지, 무엇이 변하지 않는 것인지를 구분해야 한다.

 

변해야 하는 것은 쉽게 변할 수 있게 하고, 변하지 않아야 할 것은 변하는 것에 영향을 받지 않게 해야 한다.

이 경우에 변하는 것은 다음과 같은 학생의 대여 기록을 출력하는 매체(도서관 대여 명부)다.

OCP를 만족하는 설계

 

따라서 새로운 출력 매체를 표현하는 클래스를 추가하게 하고 이러한 변경이 있더라도 기존의 클래스(SomeClient 클래스)가 영향을 받지 않게 하려면 SomeClient 클래스가 개별적인 클래스를 처리하도록 하지 않고, 위 그림과 같이 인터페이스에서 구체적인 출력 매체를 캡슐화해 처리하도록 해야 한다.


OCP를 보는 또 하나의 관점은 클래스를 변경하지 않고도(Closed) 대상 클래스의 환경을 변경할 수 있는(Open) 설계가 되어야 한다.

이는 특히 단위 테스트를 수행할 때 매우 중요하다. 예를 들어 테스트 대상이 되는 기능이 네트워크를 통해 웹 서비스를 사용한다고 할때, 이런 경우 DB나 웹 서버를 설치해야 테스트할 수 있다.

 

그런데 단위 테스트의 가장 중요한 점은 빠른 시간에 자주 테스트해야 한다는 것이므로 시간이 많이 소요되는 DB나 웹서버 설치는 테스트를 회피하게 하는 요인이 된다. 따라서 테스트 대상 기능이 사용하는 실제 외부의 서비스를 흉내내는 가짜 객체를 만들어 테스트의 효울성을 높일 필요가 있다.


실제 서비스에서 사용할 객체를 그대로 테스트할 때 위험이 따르는 경우도 있다. 어떤 기능이 DB를 사용할 때 테스트를 위해 DB에 삭제를 포함한 여러 작업을 실행한다고 하자.

이 경우 실제 DB에 변경이 생기는데, 이는 원하지 않는 상황이다.

따라서 테스트를 위해 실제 DB기능을 대체하는 가짜 객체를 만들 필요가 있다.


또한 테스트 대상 기능이 특정 상태에서 의존해서 동작할 수 있다는 점도 고려해야 한다.

예를 들어 비행기 관제 기능 중 동시에 착륙하려고 하는 비행기 수가 1000대인 경우를 테스트한다면 실제로 할 수는 없으므로 테스트 대상 기능의 동작 상태를 강제로 1000대의 비행기가 착륙하려는 상태로 만들 필요가 있다.

이 경우에도 모의 객체를 이용하면 특정 상태를 가상으로 만들 수 있다.


*단위 테스트 = 빠른 테스트 / 모의 객체 = 테스트용 가짜 객체


*다음 FuelTankMonitoring 클래스는 로켓의 연료 탱크를 검사해 특정 조건에 맞지 않으면 관리자에게 경고 신호를 보내주는 기능이 있다. 연료 탱크를 검사하는 방식과 경고를 보내는 방식이 변경될 가능성이 큰 경우에 대비해 다음 코드를 수정.


public class FuelTankMonitoring {
...

public void checkAndWarn() {
...

if (checkFuelTank(...)) {
giveWarningSignal(...);
}
}

private boolean checkFuelTank(...) {...}
private void givwWarningSignal(...) {...}
}


우선 무엇이 변하는지를 파악할 필요가 있다. 이 경우 checkFuelTank 메서드와 giveWarningSignal 메서드의 구체적 행위 방식이 변한다.

따라서 새로운 행위 방식을 기존의 코드에 영향을 주지 않고 추가하려면 이 두 메서드를 개별 클래스에서 정의하도록 해야 한다.

이를 위해 checkFuelTank 메서드와 giveWarningSignal 메서드를 protected라는 접근 제어자를 사용해 변경하고 상속 관계를 이용해 자식 클래스에 새로운 방식의 메서드를 정의한다.


public class FuelTankMonitoring {
...

public void checkAndWarn() {
...

if (checkFuelTank(...)) {
giveWarningSignal(...);
}
}

protected boolean checkFuelTank(...) {...} // default 방식
protected void givwWarningSignal(...) {...} // default 방식
}
public class FuelTankMonitoringWith extends FuelTankMonitoring { // X 방식
...
protected boolean checkFuelTank(...) {...} // X 방식
protected void giveWarningSignal(...) {...} // X 방식
}


*모의 객체는 테스트 더블(double**)의 한 종류로, '무엇'인가를 대신하는 가짜라는 뜻이다. 단위 테스트에서는 여러 가지 이유로 다음과 같은 테스트 더블을 사용한다.

 

 - 더미 객체(dummy object) : 테스트할 때 객체만 필요하고 해당 객체의 기능까지는 필요하지 않은 경우에 사용. 더미 객체의 메서드가 호출되는 경우에는 정상 동작을 실행하지 않고 예외가 발생.

 

 - 테스트 스텁(test stub) : 더미 객체에 단순한 기능을 추가. 객체의 특정 상태를 가정해서 작성하며 특정한 값을 반환하거나 특정한 메시지를 출력

 

 - 테스트 스파이(test spy) : 주로 테스트 대상 클래스가 의존하는 클래스로의 출력(간접 출력)을 검증하는 데 사용. 대상 클래스가 실행되는 동안 특정 의존 클래스로의 호출(또는 호출 결과)을 잡아내며 대상 클래스의 실행이 끝난 후에는 원하는 대로 호출되었는지 검사함.

 

 - 가짜 객체(fake object) : 실제 의존 클래스의 기능을 대체해야 할 경우에 사용되며 실제 의존 클래스의 기능 중 전체나 일부를 훨씬 단순하게 구현함. 실제 의존 클래스가 구현되지 않았거나, 너무 느리거나, 테스트 환경에서는 사용할 수 없을 때 가짜 객체를 사용.

 

 - 목 객체(mock object) : 미리 정의한 기대 값과 실제 호출을 단언문(assertion)으로 비교해 문제가 있으면 테스트 메서드를 대신해 모의 객체가 테스트를 실패하게 함. 목 객체는 테스트 더블의 모든 형태들을 포함하는 의미로 사용되기도 한다.


*다음 코드는 오후 10시가 되면 MP3를 작동시켜 음악을 연주한다. 이 코드가 제대로 작동하는지 테스트하려면 저녁 10시까지 기다려야 한다. OCP를 적용해 이 문제를 해결하는 코드를 작성.


public class TimeReminder {
private MP3 m;

public void reminder() {
Calendar cal = Calendar.getInstance();
m = new MP3();
int hour = cal.get(Calendar.HOUR_OF_DAY);

if (hour >= 22) {
m.playSong();
}
}
}


실제 시간을 사용해서 테스트 하는 방법은 번거롭고 시간도 오래 걸리므로 원하는 시간으로 설정해 이용할 수 있는 방법을 찾는다.

다음 클래스 다이어그램처럼 인터페이스를 만들고 이 인터페이스에서 파생한 2개의 클래스를 만든다.

한 클래스는 진짜 시간을 제공하는 클래스고, 다른 한 클래스는 테스트에 사용할 수 있게 임의의 시간을 원하는 대로 설정할 수 있는 클래스다.

이러한 설계는 TimeReminder 클래스를 전혀 수정하지 않고 주변의 환경을 바꿀 수 있다.

 

public interface TimeProvider { // 인터페이스 도입
public void setHours(int hours);
public int getTime();
}
public class FakeTimeProvider implements TimeProvider { // TimeProvider 테스트 스텁
private Calendar cal;

public FakeTimeProvider() {
cal = Calendar.getInstance();
}

public FakeTimeProvider(int hours) {
cal = Calendar.getInstance();
setHours(hours);
}

public void setHours(int hours) {
cal.set(Calendar.HOUR_OF_DAY, hours); // 주어진 시간으로 시간 설정
}

public int getTime() {
return cal.get(Calendar.HOUR_OF_DAY); // 현재 시간 반환
}
}
public class TimeReminder {
TimeProvider tProv;
MP3 m = new MP3();

public void setTimeProvider(TimeProvider tProv) {
this.tProv = tProv; // 테스트 스텁이나 실제 시간을 제공하는 인스턴스를 주입
}

public void reminder() {
int hour = tProv.getTime();
if (hour >= 22) {
m.playSong();
}
}
}

다음 코드를 사용하면 현재 시간이 오전 10시라도 오후 10시가 되게 시간을 설정해 MP3가 올바르게 음악을 연주하도록 호출하는지 테스트할 수 있다.

sut = new TimeReminder();
tProvStub = new FakeTimeProvider();
tProvStub.setHours(18);
sut.setTimeProvider(tProvStub);


그러나 여전히 문제가 있다. 실제 MP3 기기를 사용해 테스트를 실행하면 테스트할 때마다 원하지 않은 음악 소리가 난다.

그리고 테스트를 위해 실제 MP3 기기를 이용하는 것도 바람직하지 않을 뿐만 아니라 테스트의 실행 속도를 현저하게 느리게 하는 원인이 될 수도 있다.

 

이를 해결하는 방법도 시간 문제를 해결하는 방법과 유사하다.

 

Comments