저자: 김대곤
필자는
지난 번 기사(GRASP 패턴)에서 Singleton 패턴이 Factory Method 패턴을 구현하고 있다고 했는데, 그것은 잘못된 표현이다. 실제로 Singleton 패턴은 Factory Method 패턴을 구현하고 있지 않다. 이런 착각은 필자가 패턴을 이해할 때, 그것을 지나치게 단순화(Abstraction)시켜서 이해하기 때문이다. 필자에겐 Object의 생성을 담당하는 메소드가 있으면 모두 Factory Method 패턴을 구현하고 있는 것처럼 보이고, 항상 그렇다는 착각에 빠진다.
본 기사는 앞에 언급되었던 몇가지 주제들을 설명함으로서 Factory Method 패턴이 직면하는 상황이 왜 발생하는지를 제시할 것이다. Factory Method 패턴 자체는 별로 어려워 보이지 않는다. 그런데 좀처럼 패턴을 사용해야 하는 상황을 만나지 못하는 것이 이 패턴을 이해하는데 가장 큰 걸림돌이 아닐까 생각한다. 그러므로 패턴 자체에 대한 설명은 독자의 몫으로 남겨두고자 한다.
Abstraction
필자는 패턴을 단순화(또는 추상화 Abstraction)시켜서 이해한다고 말했다. Gang of Four의 Design Patterns에는 "Consequences", "Implementation"라는 소제목들이 등장한다. 하지만 필자는 거의 읽어본 적이 없고, 관심도 없다. 그러므로 필자에게 패턴이라는 단어와 "Consequences"나 "Implementation"이라는 단어와 연관되지 않는다. 이렇듯이 관심사항이 아닌 것을 제거해버리는 것을 추상화(Abstraction)이라고 할 수 있다.
Factory Method 패턴이 Object가 생성되어야 하는 시점은 알고 있으나 어떤 Object가 생성되어야 하는지 알 수 없을 때, 객체의 생성을 Subclass에 위임하는 방식으로 문제를 해결할 수 있다고 말하고 있다. 첫 번째 질문은 왜 이런 상황에 직면하게 되었는가이다. 어떻게 이런 일이 발생할 수 있는가? 그것은 여러 종류의 객체를 추상화(Abstraction)시켜서 사용했기 때문이다.
게임의 예를 들어보자. 이 게임은 병사와 탱크를 가지고 전투를 하는 게임이고, 병사와 탱크를 만들기 위해서 병사를 만들어 내는 막사와 탱크를 만들어 내는 공장이 있어야 된다. 게임의 기능은 "생성", "공격", "이동"이 있다. 생성의 기능을 자세히 살펴보자. 게임상에 존재하는 어떤 물체를 선택하고 "생성" 버튼을 눌렀다고 가정하자. 먼저 선택한 물체가 생성이라는 기능을 가지고 있는 객체인지를 확인하고, 막사이면 병사를 공장이면 탱크를 만들어야 한다. 만약, 실제 객체(막사와 공장)을 사용하였다고 가정하자. 그런데 비행기를 만드는 새로운 공장이 생기면 생성 기능은 수정되어야 한다. 선택한 객체가 새로운 공장인지를 체크해야 하고, 새로운 공장이면 비행기를 만들어야 하기 때문이다. "생성"이라는 기능의 입장에서는 그게 막사인지, 공장인지, 아니면 새로운 공장인지는 관심의 대상이 아니다. "생성" 기능의 입장에선 오직 선택한 객체가 "생성"이라는 기능을 가지고 있는가만 알면 된다. 실제로는 존재하진 않지만 생성이라는 기능을 가진 것들을 Factory라는 개념으로 추상화하여 사용하면, 빈번한 수정과 High Coupling을 피할 수 있다. 객체의 종류에 따라 행동양식이 바뀌는 경우, 조건문을 사용하지 말고 다형성(Polymorphism)를 사용하라는 GRASP 패턴의 "Polymorphism" 원칙을 적용하여 Interface를 만들어서 생성 기능안에서는 Factory Interface만 사용하고 실제 객체들은 Factory Interface 타입의 변수 안에서 모두 숨겨진다. 새로운 공장이 만들 때 Factory Interface만 구현하면 "생성"버튼의 코드는 전혀 수정될 필요가 없다.
"생성" 기능 버튼을 사용자가 클릭하면, 객체를 생성해야 한다. 시점은 알지만 어떤 객체가 생성될 것인지는 선택되는 객체에 따라서 다르다. 그러면 당연히 Factory 객체는 객체의 생성을 담당하는 메소드(병사, 탱크, 비행기 등을 만들어야 함으로)를 가지고 있어야 한다. 이것이 Factory Method 패턴이 직면하는 상황인 것이다. 사실 이 문제는 객체의 생성 뿐 아니라 일반적인 행동(메소드)도 마찬가지이다. "이동" 기능에 대해서 생각해보라.
객체 생성을 (생성자가 아닌) 메소드를 통해서 하고자 하는 이유
실제 코드 예제를 보기 전에 객체를 생성자가 아닌 메소드를 통해서 생성하려고 하는 이유에 대해서 살펴보자. 메소드를 통해서 생성한다는 것은 Singleton 패턴에 나오는 Singleton 클래스 또는 Static 메소드를 통한 자기 자신 타입의 객체 생성이 아닌 경우에는 객체를 자기 자신의 메소드가 아닌 다른 객체의 메소드 안에서 생성한다는 의미이다. GRASP 패턴에 나오는 Creator 패턴에서 설명했듯이 이런 경우 컨텍스에서 제공받아야 하거나 제약사항들을 체크할 수 있다. 어떤 객체가 Interface를 구현한 객체의 경우, Interface를 쓴 이유는 실제 객체를 숨기기 위해서 한 작업인데, 생성자를 통해서 하면 실제 객체가 드러나 목표하는 효과를 볼 수 없게 되기 때문이다.
너무나 유명하고, 흔한 예제
필자가 아는 선배는 글을 쓰거나 프리젠테이션을 할 때는 항상 예제를 제공하는 것이 기본적인 예의이지 원칙이라고 했다. 설명을 하면 어려운 것도 예제를 보면 쉽게 이해가 되기 때문이겠지만 사실 이해하기 쉬운 예제를 제공하는 것은 쉬운 일이 아니다. Factory Method 패턴은 너무 유명하고 흔해서 검색엔진으로 검색하면 금방 찾을 수 있다. 솔직히 말하면, 필자도 이 예제를 그런 방식으로 구했다. 1분 전에.
import java.sql.*;
public class JDBCExample {
private static String url = "";
private static String user = "";
private static String passwd = "";
public static void main(String args[]) {
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection connection = DriverManager.getConnection(url,user,passwd);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("Select to_char(sysdate) from dual");
while (resultSet.next())
{
System.out.println("It is " + resultSet.getString(1));
}
resultSet.close();
statement.close();
connection.close();
} catch (Exception e) {
} finally {
}
}
}
몇 가지이 수정이 가해지지 않으면, 이 예제는 실행되지는 않을 것이다. 그러나 Factory Method 패턴을 사용한 클래스가 무려 2개나 된다. 그것도 연결해서 사용했다. Connection 객체는 createStatement() 메소드를 통해서 Statement 객체를 생성하고 Statement 객체는 executeQuery() 메소드를 통해서 ResultSet 객체를 생성하고 있다. Connection, Statement는 모두 Interface이다. 만약 실제 객체를 사용했다면 위의 코드는 다음과 비슷한 모습이 될 것이다.
public class OracleJDBC {
private static String url = "";
private static String user = "";
private static String passwd = "";
public static void main(String args[]) {
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
OracleConnection connection = new OracleConnection(url,user,passwd);
OracleStatement statement = new OracleStatement();
OracleResultSet resultSet = statement.executeQuery("Select to_char(sysdate) from dual");
while (resultSet.next())
{
System.out.println("It is " + resultSet.getString(1));
}
resultSet.close();
statement.close();
connection.close();
} catch (Exception e) {
} finally {
}
}
}
위 코드는 실행되거나 컴파일이 되거나 하지는 않는다. 실제 오라클을 사용할 경우 쓰이는 객체들이지만 이렇게 코딩하는 사람은 없다. 만약 오라클 안쓰면 다 고쳐야 되는데 누가 이렇게 쓰겠는가? 위의 작업을 하는 사람들에겐 실제 무슨 객체가 쓰이는가는 관심사항이 아니다. 단지 어느 곳에 저장되어 있는 데이터를 읽어서 가져오는 것이 주요 목표인 것이다. JDBC는 각 데이터베이스들을 추상화했지만 더 높은 추상화를 적용한 JDO(Java Data Object)도 있다. JDO는 Persistence에 대한 추상화를 시도하고 있다.
결어
그럼 추상화(Abstraction)를 어떻게 할 것인가? 이런 문제는 아무도 설명해 주지 않는다. 필자도 누구에게서 추상화의 원리나 관련된 강의를 받은 적이 없다. 필자가 항상 염두에 두는 생각이 있다면 "내가 필요로 하는 최소한의 것만 받아들이겠다"는 자세이다. 누가 나에게 무언가를 요청한다면 "네가 좀 알아서 해 주면 안돼?"라고 말하는 자세 말이다.(실제로 이렇게 살면 맞아 죽겠지만 말이다.) 그렇게 해서 남은 최소한 것을 Interface로 정의해서 사용하자.
필자가 쓰고 있는 패턴에 관한 기사도 Design Pattern를 설명한다기 보다는 Design Pattern이라는 주제에 대해 버리고 버려서 남은, 비슷해 보이지만 전혀 다른 내용이 아닐까 생각한다. 필자는 상속을 좋아하지 않는다. 그래서 SubClass, SuperClass와 같은 용어보다는 Interface라는 용어를 많이 사용한다. 그러나 상속에 대해서도 적용될 수 있음을 밝혀두는 바이다.