Map을 쓰지 말고, Domain Model객체를 사용하자!!!

VO(TO)를 만들어서 유지보수하는 것이 힘들어서 Map을 사용하는 웹어플리케이션이 꽤 눈보인다. 웹어플리케이션에서 편하게 사용이 가능한 객체이기때문이다.

용어정리를 하자.  Domain Model을 VO로 사용이 가능하지만, VO가 모델은 아니다.
Domain Model : 객체의 특성과 행위를 가지고 있는 모델 객체
VO(TO) : value object(transfer object) 데이터를 전달하는 객체


사상이나 개념이 틀린 주제로 글을 쓰기가 어렵지만, 한번쯤 정리를 해야 할 필요를 느꼈다. 여기 적는 내용은 빙산의 일각에 불과하다. 개인적인 생각을 아주 많이 다루었으므로, 한쪽으로 사고가 치우쳤을 수도 있다. 아마도 반대되는 의견이 무척 많을 것이라고 생각을 하지만, 감수하고 글을 적어본다.

어플리케이션을 개발할때 전체적으로 코드를 보고, 레이간에 결합도를 낮추고 클래스의 응집도를 높이기 위해서는 어떻게 설계를 해야 할지, 또 좀더 투명하고, OOP적인 프로그래밍은 어떻게 해야 하는가 각자 생각을 해보자.

왜 편할까? servlet API를 보면 HttpServletRequest란 클래스가 있다.
service(HttpServletRequest request, HttpServletResponse response) : (doGet, doPost)라는 메소드가 HttpServlet의 클래스로 정의가 되어 있는데, 그것을 상속을 받아서 구현을 하는 것중의 하나이다.

그중의 request가 그것은 편하게(?) 만드는 요소이다. request.getParameterMap()을 해버리면, 현재 request에 있는 name, value의 파라미터들을 모두 가지고 올수 있기때문이다. 왜 이런 API가 존재했을까? 웹페이지에서 name, value로 짝은 지어서 폼을 만들어도 웹페이지 자체는 value값이 모두 string타입이다. 형이 온전치 않다는 것이다.

예를 들어서, seq라는 name에 5라는 숫자를 넣고 싶다고 하면, String seq = "5 이렇게 값을 받아야 한다. 그러므로, 파라미터로 넘어오는 거의 모든 value는 string이라고 얘기를 할수가 있다. 이 값들을 locked Map으로 request에 name/value로 존재한다. 모든 타입이 string이기때문에 Stirng Object형태로 저장되어 있을것이다.

웹에서 받은 데이터를 서블릿에서 받아내 다음 비즈니스 계층으로 데이터를 옮기고 싶을때 Map형태의 자료구조가 있으니 그냥 넘겨버리면, 서블릿에서는 데이터에 대한 populate가 없어지는 것처럼 보인다. Map은 비즈니스 계층에서도 존재할수 있는 자료형이라 웹에 관련된 레이어하고 애매한 부분이 존재를 한다.

Map에 넣은 데이터는 전역변수처럼 레이어간을 cross해서 지나간다. 이것은 request와 response를 전역변수로 사용하는 것과 별반 다를거 없다. 좀더 과격하게 표현을 하자면, 웹의 ApplicationContext를 모든 레이어간에 공유하는 것과 같다. OOP에서는 전역변수를 지양을 하는 편이고, 각각의 클래스에 위임을 통해서 구현을 많이 한다.

레이간의 decoupling은 보통 웹을 잊어버리고 코딩을 할때 좀더 나은 구조와 코드를 가져 갈수 있다는 것은 모두가 느끼는 바일것이다. 웹과 비즈니스 레이어간에 통신 부분은 action부분 그 이상이 되면 안될 것이다. 심지어, webwork framewokr 사용을 하면, action에서 web을 거의 잊을수도 있다. 이것은 컨테이너 없이, POJO기반으로 코딩이 가능하다는 이야기와 일맥상통한다.

대부분의 경우 Map을 확장을 해서, ExtendedMap, DataMap을 통해서 데이터를 Map에서 가져올때 데이터를 매번 casting하는 작업을 숨겨준다. 뭐..나름대로 은닉화를 해서 사용을 한다. 여기에는 불필요한 캐스팅을 매번해야 한다는 단점도 존재를 하지만, 요즘에는 casting정도에서 성능을 논하는 것은 의미없는 일이기때문에 pass를 하자.
ex) DataMap dmap = ?
    dmap.getInt("seq");

아래와 같은 것을 보자. 이것도 가능하다. 만능VO인 Map답게 리턴값도 마음대로 가지고 올수도 있다. 이것은 장점이 될수도 있지만, 대부분의 경우 디버깅을 어렵게 하고, 버그를 만들기 쉽다. 컴파일 타임에서는 개발한 사람의 의도를 알수가 없다는 것이다. 물론 error 0이라는 기분좋은 메세지가 나오지만, 런타임시에는 어느 순간에 에러가 나고, 또는 에러없이 잘못된 방향으로 로직이 흐를 수도 있다. 전자보다 후자가 더 심각한 경우이다.
dmap.getString("seq");

그래서 이러한 코드가 투명성을 보장하지 못하는 것이다. OOP의 중요한 기본중의 하나인 투명성을 깨뜨리게 되는 것이다. 개발자의 의도를 전혀 알수가 없다. 웹에서 받아온 데이터를 map에 담아서 insert에 전달을 할 경우를 생각해보자. 프레임워크 차원에서 다음과 같이 service메소드에 확장된 map을 넣어준다고 하면(사실 Map도 거져먹는게 아니라 이것저것 해줘야 할게 많이 존재를 한다.) 아래와 같이 action을 하면 끝이다.

이것이 Map을 자주 사용하는 사람이 주장하는 단순함과 편리함이다. 비즈니스 객체에 인터페이스도 단순해진다. 하지만, 반대로 생각을 해보자. Map....도대체 무엇을 하는 놈일까? 도저히 알수가 없다. 이클립스에서 map.을 하고 CTRL+SPACE를 눌러봤다. 중요 메소드로 PUT/GET이 나온다. 또, 한번 궁금하다. 도대체 무엇을 넣고 빼라는 말인가? 이럴때 필요한게 무엇일까? 문서화다. 아니면, 코드에 주석으로 모든 프러퍼티의 이름과 타입을 적어주어야 한다. 자바코드는 단순해진거 같지만, 그 보답으로 그에 상응하는 또 다른 코딩(?)이 존재를 하는 것이다.
String execute(DataMap dmap ){
      bo.insert(Map map);
}

물론 validate를 해야 한다. validate하는 코드를 추가를 해보자.

2가지를 알아야 한다. 특정 소스에 기록이 되어 있을것이다.
1. 사용할때마다 데이터 타입을 확인해야 한다. 혼자 개발하는 머리 좋은 개발자는 암기할지 모르지만, 혼자 개발한 어플리케이션이 얼마나 되겠는가?
2. 사용할때마다 사용할 프러퍼티의 이름을 알아야 한다.

지금은 데이터를 처음받아서 비즈니스 레이어로 전달하기 전에 체크를 하는 것이지만, 레이어간에 의존성을 제거하기 위해서는 이 루틴은 레이어마다 들어가야 할 것이다.
String execute(DataMap dmap ){
      bo.insert(dmap);
}
void validate(DataMap dmap){
     dmap.getInt("seq");  // 데이터타입을 확인해야 한다. name을 확인해야 한다.
}

Map을 제대로 사용을 해서 구현을 할려면 이렇게 코딩을 해야 한다. 주석이나 문서화를 통해서 그 객체를 나타내는 방법도 있지만, 코드상에서도 확실히 해야 더 좋은 방법이다.

파라미터는 map인터페이스로 받았다.
public class InsertMap extends Map{
    public static final String SEQ = "seq";
}

insert(Map map){
  if( map instanceof InsertMap){
         DataMap dmap = new DataMap(map);
         dmap.get(InsertMap.SEQ);
  }
}

아니면 DataMap의 존재를 알려야 한다. 별로 다른 점은 없다. 이렇게 사용을 해야한다. 인터페이스는 단순화했을지 몰라도, 거기에 해당하는 비싼 댓가들을 치뤄야 한다.
public class InsertMap extends Map{
    public static final String SEQ = "seq";
    public static final String SEQ_TYPE = "int";
}

insert(DataMap dmap){
  if( dmap instanceof InsertMap && SEQ_TYPE.equals("int")){
       int localSeq = dmap.getInt(InsertMap.SEQ);
  }
}


웹에서 데이터를 가지고 왔지만, 도메인 영역에서는 사용하기 힘든 것도 사실이다. 형타입이 불분명해서 얻은 고통은 많은 사람들이 알고 있을 것이다. 그래서 JDK5.0에서는 generic이라는 새로운 syntax도 추가가 되었다. 이 부분은 C++의 템플릿과는 엄연히 다르며, 형타입의 안정성을 위해서 사용하면 nice하고 happy하다.

O-R맵핑툴을 사용하지 않으면, 관계형 데이터베이스와 연동을 하는 부분은 어쩔수 없이, OOP적이지 않은 요소들이 코드에 들어갈수 밖에 없는 현실이다. 특히, 웹과 같이 stateless한 비즈니스 구조에서는 더더욱 DB의 역할이 중요해서 DB에 의존적으로 개발하기 쉽다. 이것때문에 유지보수하기가 어려운 상황이 자주 발생을 한다. 이 부분은 어떤쪽을 선택해도 trade off가 존재를 한다. 하지만, 비즈니스 도메인 영역에서는 Domain Model이 좀더 명확하고, 의도를 알기 싶고, 버그가 적은 어플리케이션을 만들수 있다고 얘기를 하고 싶다. (개인적 생각으로는 hibernate가 EJB3.0에 JSR에 들어갔지만, 아예 JDK의 JSR로 채택이 되었으면 한다. VM에서 퍼포먼스 부분을 해결할수 있다면 좋을거 같다.)

필자같은 경우는 2가지 경우로 다 개발을 한 경험이 있다. Map을 사용해서 개발한 결과, 여러가지 면에서 헛점이 노출이 되었다. 특히, 의도하지 않은 값이 DB에 들어간 것은 엄청난 충격이다. 잘못된 데이터가 DB에 들어가도 예외도 일어나지 않고, 정상적으로 작동했던 것이다. Map과 SP(Store Procedure)의 합작품이다. 테스트도 중요하지만, 디버깅이 어려운 어플리케이션도 난감하다. 개발을 할때 가장 중요한 것은 테스트와 디버깅이다!!


매도 빨리 맞는게 낫다. 대부분의 코드를 작성할때 잘못된 코드는 컴파일러에서 잡힌다. 예전에   term에서 개발을 할때와 많이 틀려진 것중의 하나가 이클립스를 통해서 개발을 하면 실시간으로 컴파일이 되기때문에, 컴파일 에러를 빠르게 파악하고 고칠수가 있다.

과거에 대부분의 경우는 클래스를 만들고 컴파일을 하면서 좌절하신 분 꽤나 많았을 거라고 생각한다. 이클립스를 통해서 실시간 컴파일 에러는 대부분 잡힌다. 문제는 런타임 예외이다. 런타입 예외는 어디서 발생을 할 것인가? 최대한 빨리 런타임 에러는 잡는 것은 중요하다.

웹어플리케이션의 예를 들자면, 웹에서 데이터를 받는 순간 클라이언트 사이드 스크립트(javascript)에서 1차적으로 검사를 하고, 데이터는 action에 도달하게 된다. 거기서 비즈니스 레이어로 전달을 할때 형타입과 값을 검사를 하고 비즈니스 레이어로 전달을 해야 한다. 그러면, 여기서도 안되다면, 비즈니스 레이어에서도 같은 검사를 또 해야 한다. 대부분 여기서는 간단하게 null검사정도만 하는 편이다. 그 다음 비즈니스 로직을 수행한후 DAO부분을 도달했을때는 완벽하게 안전한 데이터만 통과가 되어야 할 것이다.  만약 그렇지 않다면 심각한 버그를 만들수도 있다. 레이어간에 검사할때마다 아래와 같은 코드로 가져올 것인가?
public class InsertMap extends Map{
    public static final String SEQ = "seq";
    public static final String SEQ_TYPE = "int";
}

insert(DataMap dmap){
  if( dmap instanceof InsertMap && SEQ_TYPE.equals("int")){
       int localSeq = dmap.getInt(InsertMap.SEQ);
  }
}

형타입과 프로퍼티가 entry point 에서 정해져 버리면, 그 다음은 사용하기가 편하다.
인터페이스에 model을 통해서 데이터를 넣고자 하는 의도를 알수가 있다. 데이터만을 넘기지 말고, 도메인 모델 자체를 넘기자.
public class Model(){
  private int seq;
   public void setSeq(int seq);
  public int getSeq();
}

insert(Model model){
   int localSeq = model.getSeq();
}

유지보수를 위한 refactoring의 경우를 보자 3-tier를 통과하는 map은 다음과 같이 사용이 될것이다.
action
int value1 = dmap.getInt(InsertMap.Value1);
int value2 = dmap.getInt(InsertMap.Value2);
BO
int value1 = dmap.getInt(InsertMap.Value1);
int value2 = dmap.getInt(InsertMap.Value2);
DAO
int value1 = dmap.getInt(InsertMap.Value1);
int value2 = dmap.getInt(InsertMap.Value2);
value2가 필요가 없어서 지워야 하는 상황이 생겼다. 실수로 DAO에서는 삭제를 안했다. 정상적인 컴파일이 된다. 컴파일 에러가 안난다..-.-
action
int value1 = dmap.getInt(InsertMap.Value1);
BO
int value1 = dmap.getInt(InsertMap.Value1);
DAO
int value1 = dmap.getInt(InsertMap.Value1);
int value2 = dmap.getInt(InsertMap.Value2);
모델의 경우를 생각해보자. 사실 모델을 사용할때는 모델의 특성을 바로 체크하는게 로컬변수화하는 것보다 좋을듯하다. 그 값에 대해서 액세스를 통해서 값을 변경할 경우만 local 변수를 사용하자.
class Model{
  getValue1();
  getValue2();
}
action
int value1 = model.getValue1();
int value2 = model.getValue2();
BO
int value1 = model.getValue1();
int value2 = model.getValue2();
DAO
int value1 = model.getValue1();
int value2 = model.getValue2();


주의 : 가능한 setter/getter의 노출을 줄이고, 객체에 위임이 바람직한 코딩이지만, 어쩔수 없는 상황이 있다. 좀더 자세히 공부를 하고 싶으면 다음 책을 보자....^^












도메인 모델에서 더 이상 쓰지 않는 것을 삭제해 버리면, 그곳을 사용하는 곳에서 모두 에러가 나기 때문에 수정이 가능하다. 컴파일 타임에 에러가 체크가 가능하다.


class Model{
  getValue1();
}
action
int value1 = model.getValue1();
int value2 = model.getValue2(); // 에러
BO
int value1 = model.getValue1();
int value2 = model.getValue2(); // 에러
DAO
int value1 = model.getValue1();
int value2 = model.getValue2(); // 에러




정리를 하자면,

Map의 장점
  • 인터페이스를 단순화할 수 있다.
  • 만능VO의 사용으로 클래스 수를 줄일수가 있다.
  • 같은 자료형으로 3tier를 통과할수가 있다.(과연 장점인가?)

Map의 단점
  • 타입체킹이 어렵다.
    (어쩔수 없는 경우를 제외하고는 가능한 형타입을 안전하게 가자.)
  • 디버깅이 어렵다.
  • 투명하지 않다. 의도가 불명확하다.
  • 의미가 명확하지 않은 코드를 자주 만들게 된다.
  • 버그가 생길 가능성이 비교적 높다.
  • 리팩토링이나 유지보수가 어렵다.
  • 확장Map을 사용하지 않으면 사용할때마다 casting을 해야 한다.
  • 자신의 특성을 데코레이션을 하기 어렵다.
  • 복잡한 구조의 자료형을 표현하기가 쉽지가 않다. Map in Map!!


Domain  Model에 대한 나의 사랑을 고백해보자.

1. 난 확실하고, 투명한 것이 좋다.
비록, 불필요한 setter/getter를 통해서, 많이 망가진 감도 없지않아 있지만, 최근 POJO기반의 어플리케이션이 많아지면서 Domain Model 본질의 것을 찾고자 하는 노력이 여러군데서 catch할수 있다. 툴의 도움을 받으면 만드는데도 시간이 얼마 걸리지 않는다. 경우에 따라서는 VO(TO)와 비슷하게 보일지 모르지만, 의도가 전혀 틀리다.

2. anemic domain model에서 rich model로 발전을 시키자.
현재는 대부분의 anemic한 도메인 모델이 많이 존재를 한다. 좀더 고민을 해서 rich한 domain을 만들어보자. Domain Driven Model(Eric Evan)이라는 책이 있다. 나도 현재 읽고 있는데, 참고해서 보길 바란다. 진정한 OOP를 해보자.












3. 아파치 자카르타의BeanUtils를 적극 활용하자.
무의미한 코드를 없앨수가 있다.
populate와 정말 필요한 곳에 map의 형태로 리턴을 하고 싶다면, 해결방법이 있다. http://jakarta.apache.org/commons/beanutils/


간만에 장문의 글을 두서없이 적었지만...
Domain Model클래스를 추천하고 싶다.
Domain model클래스를 사용하자!!


전에 썼던 글인데 아래글도 읽어보자.
Resultset vs (VO or DTO) vs Map
http://www.ologist.co.kr/204

PS. 물론 Map사용은 필요한 곳이 있다. 위에 사항을 다 고려한다음 반드시 필요한 곳에만 사용을 하고, 가급적 과다하게 사용을 하지 말자.
 
Posted by ologist
 TAG ,

블로그 이미지
ologist

공지사항

Yesterday221
Today25
Total34,406

달력

 « |  » 2012.02
      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      

최근에 받은 트랙백

글 보관함