예시로 이해하는 개방 폐쇄 원칙(OCP)
확장에는 열려 있어야 하고, 변경에는 닫혀야 한다.
말이 좀 어려운데, 풀어보면 다음과 같다.
기능을 변경하거나 확장할 수 있으면서, 그 기능을 사용하는 코드는 수정하지 않는다.
기능을 변경하면서 그 기능을 사용하는 코드는 바꾸면 안된다..? 이것이 가능한 얘기일까?
예시를 하나 들어보자.
public class FlowController {
public void process(){
FileDataReader reader = new FileDataReader();
byte[] data = reader.read();
Encryptor encryptor = new Encryptor();
byte[] encryptedData = encryptor.encrypt(data);
FileDataWriter writer = new FileDataWriter();
writer.write(encryptedData);
}
}
FlowController는 파일로부터 데이터를 읽고, 데이터를 암호화하고, 그것을 쓴다.
여기서 만약 데이터를 파일이 아닌, 소켓으로부터 읽어온다면 코드는 어떻게 바뀔까?
public class FlowController {
public void process(){
byte[] data = null;
if(useFile){
FileDataReader reader = new FileDataReader();
data = reader.read();
}
else{
SocketDataReader socketReader = new SocketDataReader();
data = socketReader.read();
}
Encryptor encryptor = new Encryptor();
byte[] encryptedData = encryptor.encrypt(data);
FileDataWriter writer = new FileDataWriter();
writer.write(encryptedData);
}
}
가장 쉽게 생각하면 file일 경우와 아닐 경우를 if-else 문으로 나누는 것이다. 쉽고 간편한 방법이지만 데이터를 읽어들이는 방식이 추가될 때마다 if-else 문이 늘어나고 코드는 점점 지저분하고 복잡해질 것이다. 결국 프로그램의 유연성은 떨어지고 유지 보수가 어려워진다.
데이터를 읽는 방식이 다양해지더라도 flowController의 코드는 그대로 유지할 수 있는 방법이 없을까?
public interface ByteSource{
public byte[] read();
}
public class FileDataReader implements ByteSource{
public byte read(){
...
}
}
public class SocketDataReader implements ByteSource{
...
}
먼저 데이터를 읽어들이는 것을 추상화하자. ByteSource 인터페이스를 만들고,FileDataReader와 SocketDataReader가 이를 구현하도록 했다.
public class ByteSourceFactory{
public ByteSource create(){
if(useFile())
return new FileDataReader();
else
return new SocketDataReader();
}
private boolean useFile(){
...
}
private static ByteSourceFactory instance = new ByteSourceFactory();
public static ByteSourceFactory getInstance(){
return instance;
}
...
}
그리고 적절한 구현체를 선택하는 객체를 따로 만들었다. ByteSourceFactory을 이용해 요구사항에 맞게 ByteSource의 type을 결정할 수 있다. 즉, File로 데이터를 읽을지, Socket으로 읽을지 변경하고 싶다면 이 Factory만 수정하면 된다.
(참고) 이 ByteSourceFactory를 대신할 수 있는 방법으로 생성자를 통해 적절한 ByteSource를 전달받는 방법이 있는데, 이것이 바로 Dependency Injection이다.
public class FlowController {
public void process(){
/*
byte[] data = null;
if(useFile){
FileDataReader reader = new FileDataReader();
data = reader.read();
}
else{
SocketDataReader socketReader = new SocketDataReader();
data = socketReader.read();
}
*/
ByteSource source = ByteSourceFactory.getInstance().create();
byte[] data = source.read();
Encryptor encryptor = new Encryptor();
byte[] encryptedData = encryptor.encrypt(data);
FileDataWriter writer = new FileDataWriter();
writer.write(encryptedData);
}
}
마지막으로 FlowController를 보자. if-else문이 사라지고 데이터를 읽어들이는 부분이 간결해졌다.
이제 데이터를 파일로 받든, 소켓으로 받든, FlowController의 코드는 손대지 않아도 된다.
이것이 바로 OCP이다. 여기서 데이터를 읽는 방식으로 HTTP를 추가한다고 가정해보자.(기능 추가) 그래도 이 기능을 사용하는 FlowController의 코드는 바뀌지 않는다. OCP의 정의대로, 기능을 변경하거나 확장하면서도 그 기능을 사용하는 코드는 수정하지 않는 것이다.
(참고) OCP가 위반되었을 때 나타나는 증상
- instanceof 와 같은 타입 확인 연산자로 분기 처리됨.
- 유사한 if-else 블록이 존재.
요컨대, 개방 폐쇄 원칙은 변화가 예상되는 곳을 추상화해서 변경의 유연함을 얻게 해준다. 만약 기능이 변해서 코드를 바꿔야한다면, 변화와 관련된 구현을 추상화해서 OCP 원칙에 맞게 수정할 수 있는지 확인하자.
코드 출처, 참고 도서 : 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴, 최범균 지음, 인투북스