Framework/Database2009. 2. 20. 15:59

실험실 코드와 비실험실 코드를 분리한 후 가장 지켜야 할 원칙은 절대로 비실험실은 실험실 코드는 서로 관련이 없어야 한다는 것이다. (양방향성이다.)

이를 조금 더 확대하면 DB라는건 하나의 서비스이고 개발자가 만드는 제품도 하나의 서비스 이기 때문에 개발자가 만드는 제품은 DB를 개체로서의 DB가 아니라 개념으로서의 DB로 다루어야 한다.

추상적으로 A서비스와 B서비스가 있다고 할때 A서비스는 B서비스로 request를 하고 B 서비스는 A 서비스로 response를 한다. 이때 request와 response는 중립적인 메시지 형태가 되어야 하며 A.request는 B의 내부에 대해서 몰라야 하며 B.response는 A의 내부에 대해서 모르며 동시에 자기의 정체를 상세히 알려줘서는 안된다.

첫번째 A.request가 중립적인 메시지가 되기 위해서는 그리고 B가 개념으로서의 DB로 다루어질려면 request에는 개체로서 다루게 되는 select 문이나 clob 혹은 PrepareStatement 같은게 나와서는 안된다. 이야기가 다소 혼란이 있을지 모르므로 다시 앞에서 말한 "블랙박스처럼 DB를 다뤄라"와 비교를 해보자

먼저 객체지향의 개념과 개체의 구분을 해보자. 살고 있는 집에서 조금 떨어진 거리에 가끔 나가서 사먹는 붕어빵 파는 아저씨가 있다. 이때의 붕어빵은 개념이고 지금 밖에서 나가서 사온 지금 내 손에 들고 있는 따끈따근한 붕어빵은 개체다. Class와 Instance는 객체지향의 기본이니 별로 새삼스러울것도 없겠지만 이를 서비스에 적용해 보자.

블랙박스로서의 DB가 잘못됐다고 하는 이유는 DB를 개체로 다루되 개체의 특성- 이를테면 Procedure등을 사용하지 말고 Ansi SQL을 사용하라 - 을 무시하라가 그 주장이기 때문이다. 그러나 개념으로서의 DB 접근은 좀더 본질적이다.

비실험실인 서비스 코드에서 Select * From emp와 같은 SQL 문장이 나와서는 안되는 이유는 바로 Select문 자체가 개체로서의 DB에 접근하기 때문이다. 개념으로서의 DB는 데이타 저장 시스템일뿐이어야 하는데 위 간단한 SQL은 emp라는 테이블이 있는 바로 지금 옆에 설치되어 있는 DB 개체로 인식하고 있다.

프레임워크인 실험실 코드가 해야 할 일은 그래서 명백하다. A.request와 B.response의 중립적인 개념 코드(응?)를 해석해 주는 중간 레이어의 역할이다. 사실 컴퓨터 자체가 해석기관이고 랭귀지도 사람과 컴퓨터간의 해석 기관이라는 재귀적 관점에서 보면 프레임워크란 추상화 정도를 줄인 해석기관이라고 간주해도 크게 벗어나지 않는다.

package com.bleujin.framework.db.procedure;

import com.bleujin.framework.DBTestCase ;
import 
com.bleujin.framework.db.DBController;
import com.bleujin.framework.db.Rows;

public class TestLobTest extends DBTestCase{
  
  public void testDefault() throws Exception {
    // example 1
    
IUserProcedure u1 = dc.createUserProcedure("emp@add(?,?)";
    u1.addParam(110).addParam("bleujin";
    dc.execUpdate(u1;

    // example 2
    IUserProcedure u2 = dc.createUserProcedure("emp@list()";
    Rows rows = dc.getRows(u2;
    while(rows.next()){
       rows.getString("name";
    }
  }
}


위 코드는 DBFramework를 사용한 전형적인 예제이다. 먼저 A.request는 emp@add(?,?) 등의 중립적인 String으로 전달되고 있다. emp@add(?,?)를 어떻게 해석하는가는 실험실 코드인 DBManager 이하에서 결정할 문제일뿐 현재 서비스코드에서는 그에 대해 알 필요가 없다. 단지 실험실 바깥의 서비스 코드에서는 인자 2개를 셋팅한 UserProcedure를 호출했을 뿐이다.

두번째 예제에서 response인 Rows를 보면 JDBC의 ResultSet하고 닮긴 했지만 인터페이스만 공유할뿐 close를 호출해줘야 한다든가 예외처리를 해줘야 하는 책임이 없다. 사실 그냥 내부구조는 Map 형태의 일반 객체이기 때문이다. 일반적으로 JDBC의 ResultSet은 예외 처리와 적절한 시점에 close의 호출에 신경을 써야 하기 때문에 중립적인 메시지라고 보기 어렵다.

여기서는 DB를 순수 개념으로서의 Repository Service로서만 다룰뿐 실제의 개체는 모두 숨겨져 있다. 실제 개체로서의 DB는 바깥의 서비스에서는 알필요가 없는 것이다.


'Framework > Database' 카테고리의 다른 글

Framework (DBManager)  (0) 2009.03.04
Framework (구조적 중복 제거)  (0) 2009.02.21
Framework (개요)  (0) 2009.02.20
Framework (블랙박스 증후군)  (0) 2009.02.07
Framework (커서)  (0) 2009.01.12
Posted by bleujin
Framework/Database2009. 2. 20. 00:36

Database Framework를 작성한것은 사실 우연한 계기였다.
2003년 모회사에  입사하자마자 새버전의 솔류션 개발에 DBA로서 참여하게 되었는데 이전까지 DB전문가가 없었던 관계로 주로 하게될 역할은 모델링과 기초 설계 담당이었다.

그때까지 몇개의 웹스크립트와 몇개의 언어를 필요에 의해 겉핧기로만 알고 있었을뿐 개발자보다는 DBA나 모델러로서 오랫동안 일을 했기 때문에 자바의 실력은 아주 별로였다. (게다가 당시 내 주력언어는 VB와 C#이었다.) 고작해야 몇가지 패턴책을 보고 이것저것 몇가지 흉내만 가까스로 낼수 있는 정도였으니까.

이전버전의 제품은 수많은 버그와 유지보수의 문제로 몸살을 앓고 있었고 새로운 팀은 이러한 문제를 벗어나기 위한 한가지 방법으로서 System Framework를 먼저 제작하고 그 후에 비즈니스 요구사항을 가진 제품을 올리기로 합의하였다. 시간이 그리 넉넉하진 않았고 팀 자체가 새로 결성되었기에 먼저 팀는 인터넷의 여러 오픈소스 - 주로 아파치와 소스포지를 참고 - 를 참고하여 베이스로 깔고 목적에 맞게 보완/수정하는걸 기본전략으로 하였다.

당시 나는 CMS 도메인에 대해 기본지식이 없었기 때문에 인터넷이나 이전 버전을 참고하면서 논리적 디자인과 물리적 디자인 그리고 기초 데이타를 얻기위해 DB에 수천만간의 테스트 데이타를 작성해놓고 기초 성능 테스트를 하고 있었다.

문제가 생긴건 팀이 셋팅되고 한달쯤 지나 DB 디자인이 어느정도 확정이 되고 개발팀에게 ERD를 작성하여 모델 디자인을 설명하고 작성중이던 Framework 소스를 보게 된때였다. 그때까지는 개발자도 아니고 자바개발에 비교적 무심하였기 때문에 그냥 데이타베이스 쪽은 어떤가 하고 그냥 훓어봤을뿐이었다. 아마 그때 팀은 아파치 오픈소스인 현재의 common-dbutils의 모테인 릴리즈 이전버전을 사용하고 있었다. 지금 생각하면 우습지만 당시에는 커넥션 풀링을 쓴다는게 어려웠던 시절이고 마치 그게 대단한 기술로 보였던 시절이었다.(DB라는 리소스 특성상 커넥션하는 소스에 아주 조그만 버그만 있었도 프로그램이 중지되었으니까..) 그래서 커넥션 풀링이 된다는 것만으로 common-dbutils은 개발자들에게 아주 합당한 선택이었다. 

무심코 소스를 보게 된거지만 common-dbutils은 말 그대로 utility library일뿐 개념상으로 전혀 프레임워크가 아니었을뿐더러 만약 이 라이브러를 쓰면 DB의 대부분의 기능을 쓸수 없게 되리란걸 알게 되었다. 지금도 그렇지만 그때도 DB를 블랙박스로 다루어야 한다는 사상이 팽배해 있던 시절이라 "프로시저? 그거 특정 DB에만 있는거잖아 그걸 쓰면 안되지" 라던가 "DB 함수 그게 먼데? 그보단 일단 읽어서 자바 함수로 처리하자" 등등의 생각들이 지배하고 있었다.

이전글에서도 언급은 한적이 있지만 DB를 블랙박스로 다룬다는 것이 DB의 공통기능만을 써야 한다는 걸 - 이를테면 Ansi SQL만을 사용한다 등의 - 의미해서는 안된다. 오히려 DB와 상관없이 DB에 있는 모든 기능을 최대한 이용할 수 있는데까지 이용해야 한다. 물론 지금도 나아진게 없긴 하지만 DB를 단순히 저장매체의 종류로서만 사용한다면 DB가 수천만원을 호가할 이유가 없다.

어쨌거나 그걸 보고 팀장을 찾아가 이 프레임워크는 이런저런 문제가 있기 때문에 다시 설계/개발을 해야한다고 찾아가서 따지기 시작했다. 돌이켜 생각해보면 팀장으로서는 아마도 황당했으리라-ㅅ- 웬 처음 합류한 DBA가 몇시간 소스를 훔쳐보더니 개발이 어쩌고 저쩌고 하며 당시의 주류와는 다른 이상한 얘기를 해댔으니까. 삼사일간이나 이 문제로 투닥투닥 거린끝에 결국 "그럼 니가 한번 해보세요-ㅅ-"로 결론이 났고 자리로 돌아와선 괜히 말했나-ㅅ- 라는 후회가 슬쩍 들기도 하였지만 당시의 나는 고생끝에 작성한 DB 모델이 개발 소스의 요구에 의헤 훼손당하고 싶지 않아 오기를 부렸다.

그래서  주말에 이틀 날밤까고-ㅅ- 만든게 이 DBFramework 였다. 50개 안팍의 클래스가 6년동안 조금씩 기능이 추가되면서 약 150개의 클래스가 되었지만 기본 디자인은 전혀 바뀌지 않았다. 맨바닥 헤딩이었이면 훨씬 더 걸렸겠지만 이전에 C#으로 DCOM용으로 DB Framewor를 만들어본적이 있었고 그걸 1년동안 사용하면서 여러가지 단점에 대해서도 느낀바가 있었기 때문에 개념적인 문제보다는 Java라는 언어 때문에 고생하였다. 그 후로 어쩌다 보니 쭉 개발일도 하게 되면서 이제는 DBA보다는 개발자라는 명칭이 더 익숙하니 이 일은 내 IT 인생에 일종의 전환점이 되었다. 물론 딱히 좋은 전환점은 아니라고 쭉 생각하고 있긴 하지만 말이다 -ㅅ-;





당시에는 명확하게 정립이 된건 아니었지만 그 때에도 최우선했던 한가지는 실험실 코드의 분리였다. 다른 프레임워크와 마찬가지로 데이타베이스 프레임워크도 실제 코드에 쓰이기 위해서는 처음에 고려하지 못했던 다양한 요구사항들이 추가된다. 이를테면 SQL의 추적기능이 있어야 한다든가 느린 SQL은 취소할 수 있는 Undo 기능이 있어야 한다든가 로그 기능이 있어야 하거나 설정값을 조절할 수 있어야 한다든가 등등의 현실적 요구사항들이 나중에 스물스물 생겨나게 되는데 절대로 설계시 그러한 요구사항을 미리 고려해서는 안된다는 것이다. 



위 그림은 흔히 나오는 컴포넌트 디자인의 예이다. 오른쪽은 복잡도가 클래스에 비례하기 때문에 높은 반면에 왼쪽은 복잡도가 그리 높지 않다. 당연한 얘기이긴 한데 왼쪽과 같이 나누기 전에 먼저 고려해야 하는 것이 있다. 



멋지게 3차원 그림을 표현하고 싶지만능력이 안되니 위쪽과 아래쪽 컴포넌트를 높이에 따른 3차원이라고 생각하자 -ㅅ-
이 차원의 존재는 "관련있는"의 기준이 아닌 실험실과 비실험실의 차원이다. 물론 이것도 하나의 레이어의 일종이지만 별도의 차원으로 얘기하는 것은 이 차원이 가지는 특별성 때문이다. 

미리 나중에 생길지 모르는 비지니스 요구 사항을 대비하는 디자인이라는 것은 언뜻 그럴듯 해보이지만 사람은 앞으로 생길지 모르는 추가 요구사항을 고려하는 것 자체가 예언만큼이나 힘들고 그러한 것을 한다고 해도 불필요하게 과도한 복잡한 디자인을 유발할 가능성이 높다. 앞으로 나올 요구사항에 대비해야 하는 확장 가능한 디자인은 되려 간결한 설계에서 나오는 표현력을 떨어뜨리고 비지니스 정보의 확장 포인트가 중간에 끼어듬으로서 효율성을 감소시킨다.

프레임워크를 처음 만들때 가장 먼저 생각해야 할 분리 원칙은 비즈니스 요구사항과 상관없는 코드의 분리를 통한 미니멀리즘이다. 모든 일체의 기능적 요구사항은 무시하고 비기능적 요구사항 - 효율성, 테스트 용이성 등을 먼저 고려해여 레이어를 분리해야 한다. 그리고 이런 비기능적 요구사항이 구현될 실험실 레이어 부분을 현실 환경과 분리시켜야 한다.

위의 UML은 중요한 클래스의 Overview인데 파란색 사각형이 계속 말하는 실험실 코드이며 만약 비지니스 요구사항이 생긴다면 파란색 부분이 아닌 빨간색 부분에서 확장을 해야 한다.

'Framework > Database' 카테고리의 다른 글

Framework (DBManager)  (0) 2009.03.04
Framework (구조적 중복 제거)  (0) 2009.02.21
Framework (실험실 코드)  (0) 2009.02.20
Framework (블랙박스 증후군)  (0) 2009.02.07
Framework (커서)  (0) 2009.01.12
Posted by bleujin
Framework/Validation2009. 2. 18. 02:08

Framework를 작성할때 가장 먼저 생각해야 하는것은 실험실과 비실험실의 분류이다. 실험실이란 일종의 무균의 연구실을 상상하면 된다. 이 곳에서는 이 코드가 실제 사용될 비실험실의 제약에 대해 전혀 고려하지 않는다는게 포인트다. 오직 코드의 효율 그 자체만을 추구하며 공학이 가지는 모든 특성을 무시해도 상관없고 오히려 의도적으로 무시되어야 한다.

실험실의 코드는 오직 코드의 1차 목적에만 집중하는데 일차적으로 코드의 효율성이나 디자인의 간결함만을 추구해야 한다. 실험실의 코드는 비실험실에 대해 의도적으로 무시하기 때문에 비 실험실은 실험실 코드에 대해 최소한의 정보만 알기 때문에 request와 response 모두 인터페이스 기반 통신을 하게된다.

말은 조금 어려워졌지만 사실 유효성 검사와 관련한 실험실 코드를 작성하는 것은 그닥 어려운 일이 아니다.

첫째 고려해야 할 것은 유효성검사가 필요한 객체타입은 - 아마도 대부분 Getter 함수를 가지고 있는 Bean 형태의 객체가 되겠지만 - 비실험실에서 어떻게 사용될지 모르는 실험실 코드에서는 제약할수 없기 때문에 Object 형태가 되어야 한다. 다만 그냥 Object는 좀 그러니까 Serializable 인터페이스같이 메소드 정의가 없는 IValueObject 인터페이스를 사용하기로 하자.

public interface IValueObject {

}

public class TestBean implements IValueObject {

 private int ivalue;
 private String svalue;

 public int getIvalue() {
  return ivalue;
 }

 public void setIvalue(int ivalue) {
  this.ivalue = ivalue;
 }

 public String getSvalue() {
  return svalue;
 }

 public void setSvalue(String svalue) {
  this.svalue = svalue;
 }

}
와 같은 TestBean을 만든다. 비실험실에서 실험실 코드로 IValueObject 를 던지면 실험실 코드의 response도 하나의 interface가 될수도 있겠지만 사실 boolean isValid() 말고는 그닥 특별한 정보가 필요가 없다. 말했다시피 실험실은 외부와 차단되어 있어야 하기 때문에 최소한의 정보만을 주고받는다.

따라서 실험의 모든 유효성 검사 객체는 아래 인터페이스를 구현하게 된다.




public interface IValidator {
 public boolean isValid() ;
}

예를 들어 공백이어서는 안된다라는 유효성은
public class Required extends BaseValidator{

 public Required(IValueObject valueObject, String name) {
  super(valueObject, name);
 }

 public boolean isValid() {
  Object value = invokeGetter() ;  // reflection
  if (value == null) return false ;
  
  return StringUtil.isNotBlank(value.toString()) ;
 }
}

와 같이 작성될 것이다.

테스트 코드는 아래와 같이 작성된다.

public class StandardValidationTest extends TestCase{

 private TestBean b = new TestBean() ;
 private String SVALUE = "svalue";
 
 public void setUp(){
  b = new TestBean() ;
 }

 public void testRequired() throws Exception {

  assertEquals(false, new Required(b, SVALUE).isValid());
  b.setSvalue("") ;
  assertEquals(false, new Required(b, SVALUE).isValid());

  b.setSvalue(" \t\n") ;
  assertEquals(false, new Required(b, SVALUE).isValid());

  b.setSvalue("a") ;
  assertEquals(true, new Required(b, SVALUE).isValid());
 }
}

이런식으로 실험실 코드를 작성하면 된다. 사실 실험실 코드를 작성하는 것은 아주 쉬운일이다. 제약과 예외가 거의 없기 때문이다. 그리고 물론 제약과 예외를 고려하지 않는 것은 의도적인 것이다. 제약과 예외는 비실험실에서 생기는 것이며 그곳의 문제를 결코 실험실안으로 끌고 들어와서는 안된다.


이제 온갖 세균과 예외와 제한이 있는 비실험실인 현실세계로 돌아와 보자.
앞서 말했듯이 실제 어려운 것은 현실세계의 접촉 지점이다. 

1. 프로그램에서 직접 호출하는 방법
   모든 프레임워크가 그런것은 아니지만 유효성검사 프레임워크는 프로그램에서 직접 호출방식은 거의 고려할 필요가 없다. 왜냐하면 유효성 검사라는 말 자체가 가지는 개별적인 비지니스 특징때문이다. 어떤 객체의 유효성이란 거의 항상 비지니스 도메인에서 개별적으로 정해지기 마련이고 이는 다시 말해서 굉장히 유연해야 하므로 컴파일이 되는 딱딱한 바이트 코드에 넣어서 관리하는 것은 의미가 없다.

2. XML 설정 파일을 이용하는 방법 
   사실 대부분의 구성 정보는 XML을 통해 관리하는게 정답이긴 하다. 이전의 Property 파일보다는 다 짜임새 있기 때문이다. 다만 XML 파일은 앞서 말한대로 효과에 비해 지나치게 복잡하다. 정도의 문제이긴 하지만 일반 구성 정보가 아닌 수십 - 수백개에 이르는 개체의 대한 유효성 정보를 XML파일로 관리하는 것은 쉬운 일이 아니다. 게다가 유효성 검사의 특성상 많은 중복 정보가 발생하는데 - 이를테면 Required 가 필요한 수는 객체수 * 프로퍼티 * 0.3 정도이다. - 이를 효과적으로 처리하는게 어렵다.

두번째로 접착성이 떨어지는데 여기서 접착성이란 비지니스 프레임워크와 너무 단단하게 연결되어 있기 때문에 프레임워크의 이전이 쉽지 않다는 문제도 있다. 사실 현재의 Struts와 Spring의 Validation Code의 가장 큰 문제가 이거다. 더군다나 편의를 위한다고 써먹지도 못하는 듣보잡 자바스크립트 코드를 지멋대로 생성하곤 한다. - 이딴걸 어따 쓰라고 -ㅅ-; 프레임워크는 어디에도 붙일수 있는 포스트 잇의 접착도를 가져야 하지 일단 붙으면 떨어지기 힘든 본드의 접착도를 가져서는 안된다.

XML파일로 관리했을때 세번째 문제는 복잡한 유효성 감사를 다루기가 어렵다는 데 있다. 물론 그에 해당하는 CustomBean을 만들어서 어떻게 해결할 수도 있겠지만 케이스 바이 케이스로 하나씩 만든다는 것은 지나친 수고로움을 유발한다.


그럼 어떻게 해야할까? 보통 다른 선택이 있다면 Rule Engine를 제작하는 방법이 있다. Rule 엔진은 간단한 정의와 프로세싱 기능이 있기 때문에 XML파일보다는 훨씬 유연하고 자유롭다. 다만 일반적으로 Rule Engine은 보편성을 갖기가 힘들기 때문에 유효성 검사의 경우에 한정한다면 배보다 배꼽이 커지는 경우라 할 수 있다. 

그래서 가장 그럴듯한 다른 방법은 자바에 내장되어 있는 Rhino 같은 스크립트 엔진을 활용하는 것이다.
장점은 아래와 같다.
첫째 스크립트의 유효성 검사 코드 대부분을 재활용할 수 있다.
둘째 스크립트는 XML 보다 훨씬 더 익숙하다.
셋째 스크립트는 로직을 가질 수 있다.
넷째 XML 에 비해 불필요한 중복을 줄일수 있다.
다섯째 스크립트는 순수 텍스트인 XML에 비해 자체적으로 테스트가 가능하다.

가장 큰 장점은 첫째와 셋째이다.
기존의 브라우저에서 사용되는 자바 스크립트를 사용할 수도 있고 펄이나 파이썬이 익숙하다면 그것도 상관이 없다.

script.js 파일은 기존의 자바 스크립트에서 복사해서 

<script.js>

   fvalid = bsf.lookupBean ("fvalid"); 
   m = bsf.lookupBean("m");

  fvalid.setForward("add") ;
  fvalid.required("subject", m.get("jmsg.validate_null", m.get("etc.subject")), true);
  fvalid.maxLengthByte("subject", 200, m.get("jmsg.length", m.get("etc.subject"), "1", "200"), true);
  // fvalid.notAllowed2ByteSpace("subject", m.get("jmsg.include_fullwidth_space", m.get("etc.subject")), true);
  fvalid.required("content", m.get("jmsg.validate_null", m.get("etc.cont")), true);
  ......

와 같이 저장한다.

실제 코드에서는 위와 같이 저장된 js 파일을 읽어서 실행하게 된다.




이를 테스트 할수 있는 코드는 아래와 같다.

package test.bsf;

import java.io.FileReader;

import junit.framework.TestCase;

import org.apache.bsf.BSFManager;
import org.apache.bsf.util.IOUtils;

import com.bleujin.framework.res.Resources;

public class TestFormValid extends TestCase {
  private BSFManager mgr = new BSFManager();

  TestContentBean bean ;
  private FormValid fvalid ;
  private Resources m ;
  public void setUp() throws Exception{
    bean = new TestContentBean() ;
    fvalid = new FormValid(bean;
    m = Resources.getResources("default";
    
    mgr.registerBean("fvalid", fvalid;
    mgr.registerBean("m", m;
  }


  public void testFalse() throws Exception {
    FormValid fvalid = runScript("D:\\eclipse\\workspace\\testAnother\\src\\vtest.js""add");
    assertEquals(false, fvalid.getResult("add").isValid()) ;
  }
  
  
  public void testTrue() throws Exception {
    FormValid fvalid = runScript("D:\\eclipse\\workspace\\testAnother\\src\\vtest.js""add");
    assertEquals(true, fvalid.getResult("").isValid()) ;
    
    bean.setSubject("abcd";
    bean.setContent("defg";
    assertEquals(true, fvalid.getResult("add").isValid()) ;
    
  

  private FormValid runScript(String fileName, String forwardNamethrows Exception{
    String language = BSFManager.getLangFromFilename(fileName)// get scripting language name
    FileReader in = new FileReader(fileName);
    String script = IOUtils.getStringFromReader(in)// read script from file

    mgr.exec(language, fileName, -1, -1, script)// execute scriptusing appropriate engine*
    return fvalid ;
  }
}



FormValid 객체는 기존의 자바 스크립트 코드를 흉내내어 만든 Java 객체이다.
대충 이런 형태이다.

package test.bsf;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.bleujin.framework.valid.IValueObject;
import com.bleujin.framework.valid.validator.MaxUTFByteLength;
import com.bleujin.framework.valid.validator.Required;
import com.bleujin.framework.valid.validator.IValidation;

public class FormValid {

  private Map<String, List<ValidRule>> rules = new HashMap<String, List<ValidRule>>();
  private String currentForwardName = "";
  
  private IValueObject valueObject ;
  public FormValid(IValueObject valueObject){
    this.valueObject = valueObject ;
    setForward("";
  }
  
  public void setForward(String forwardName){
    this.currentForwardName = forwardName ;
    rules.put(forwardName, new ArrayList<ValidRule>()) ;
  }
  
  public void required(String name, String errorMessage, boolean focus) {
    getCurrentSet().add(new ValidRule(new Required(valueObject, name), errorMessage, focus)) ;
  }

  public void maxLengthByte(String name, int maxByte, String errorMessage, boolean focus){
    getCurrentSet().add(new ValidRule(new MaxUTFByteLength(valueObject, name, maxByte), errorMessage, focus)) ;
  }

  public ValidResult getResult(String forwardName){
    
    List<ValidRule> currentRules = rules.get(forwardName;
    ValidResult result = new ValidResult();
    for(ValidRule rule : currentRules){
      if (! rule.getValidator().isValid()){
        result.addInvalidRule(rule;
      }
    }
    return result ;
  }

  private List<ValidRule> getCurrentSet(){
    return rules.get(currentForwardName)
  }

}

class ValidResult {
  
  List<ValidRule> lists = new ArrayList<ValidRule>() ;
  void addInvalidRule(ValidRule rule){
    lists.add(rule;
  }
  
  public boolean isValid(){
    return lists.size() == ;
  }
  
  public String getErrorMessage(){
    StringBuffer result = new StringBuffer() ;
    for(ValidRule rule : lists) {
      result.append(rule.getErrorMessage() "\n";
    }
    return result.toString() ;
  }
  
}

class ValidRule{
  
  private IValidation validator ;
  private String errorMessage ;
  private boolean focus ;
  ValidRule(IValidation validator, String errorMessage, boolean focus){
    this.validator = validator ;
    this.errorMessage = errorMessage ;
    this.focus = focus ;
  }
  
  public IValidation getValidator() {
    return validator;
  }
  public String getErrorMessage() {
    return errorMessage;
  }
  public boolean isFocus() {
    return focus;
  }
  
}

'Framework > Validation' 카테고리의 다른 글

Framework - Validation  (0) 2009.02.17
Posted by bleujin
Framework/Validation2009. 2. 17. 05:08

자비를 7년정도 SQL을 다룬지는 10년이 넘었지만 그럼에도 개인적으로 가장 자신있는 언어가 무엇이냐고 묻는다면 아마도 Javascript라고 말하지 않을까 싶다. XP의 김창준씨는 개발자는 1년에 최소한 한개의 언어를 익히는게 좋다라고 하는데 굳이 익히려 노력하지는 않았지만 그 동안 다룬 언어는 Pascal부터 시작해서 C, VB, C#, Java등이 있고 사용한 Web Script 언어는 ASP나 PHP, Perl 등등 어쩌다 보니 여러가지 언어를 해보게 되었다. 그리고 그 어떤 언어보다 Javascript의 간결함과 우아함을 좋아한다.

프로그래머는 태생적으로 간결함의 미학을 좋아하는 것 같다. 다만 그 간결함을 파이썬이나 루비 등에서 찾곤 하는데 - 예전에는 자바도 그런 미학이 있었는데 최근의 자바는 너무 살이 쪘다. - 개인적으로는 Java하고 비슷한 문법이라 그런지 모르겠지만 Javascript가 더 재미있다.


보통의 초보 프로그래머들이 작성하는 Form Validation 코드는
var frm = document.forms[0] ;
if (frm.subject.value == '')  {
    alert ('글 제목이 비었음') ;
    frm.subject.focus() ;
} else if (form.subject.value.length > 200 ) {
    alert('글제목은 200자까지 가능') ;
    frm.subject.focus() ;
} else if (..... )
   ,,,,,,

}
frm.submit() ;
... 런식인데 위 코드는 몇가지 문제를 가지고 있다.

첫번째 복잡하다. 앞의 글에서 복잡하다와 어렵다는 다르다고 말한적이 있는데 위 식으로 작성된 코드는 매우 쉽지만 아주 아주 복잡한 코드가 된다. 복잡한 이유는 대부분의 경우 중복이 많기 때문이다. 중복이라는 것은 단순히 코드의 중복만을 말하는 것은 아니다. 실제의 프로그램에서는 위와 거의 동일한 수십개의 코드를 작성해야 겠지만 위 코드만을 본다면 코드 자체만으로는 중복이 되지 않았다. 다만 구조적인 중복이 있다. 위 코드는 하나의 if 절이 모두
 1) if 조건 확인
 2) 조건이 달성되지 않았을때 어떤 액션을 함
      alert 창을 띄움
      focus를 맞춤
이라는 구조적인 중복이 있다. 코드의 중복은 라이브러리로 해결이 가능하지만 - 이를테면 getLength(formElementName) 같은 함수로 해결할 수 있겠지만 - 구조적인 중복은 대부분 프레임워크로 해결이 되어야 한다.

두번째로 위 코드는 활용적인 면에서도 그닥 실용적 - 요즘에는 실용이라는 말에 거부감이 생기지만 -ㅅ- 이지 않다. 예컨데 i18n의 다국어 메시지 처리도 곤란하고 다른 라이브러리나 프레임워크와도 호환이 좋지 않다.



그래서 꽤 오래전에 작성한 코드지만 위와 같이 js를 만들어서

  fvalid.setForward('add') ;
  fvalid.required("subject", "<%= m.get("jmsg.validate_null", m.get("etc.subject") ) %>", true);
  fvalid.maxLengthByte("subject", 200, "<%= m.get("jmsg.length", m.get("etc.subject"), "1", "200") %>", true);
  fvalid.notAllowed2ByteSpace("subject", "<%= m.get("jmsg.include_fullwidth_space", m.get("etc.subject") ) %>", true);
  fvalid.required("content", "<%= m.get("jmsg.validate_null", m.get("etc.cont")) %>", true);

런식으로 사용하는데 그 기본원리는 간단하다.

여러가지 효율상의 이유로 자바의 경우 쓰레드를 사용해서 invokation와 execution를 분리시키는데 위 코드는 declaration과 execution을 분리한 것이다. 이런 방식을 선언적 프로그래밍 이라고도 할 수 있는데 이렇게 해서 얻을 수 있는 장점은 구조적인 중복을 없엘수 있는것과 다른 코드와 꽤 접착이 잘된다는 점이다.

fvalid.required 메소드를 호출했을때 호출 당시에 바로 실행되어 subject의 유효성을 확인 하지 않는다. 다만 subject는 빈 내용이 되어서는 안된다는 정보가 객체 형태로 변환되어 어딘가에 저장될 뿐이다. 어찌보면 execution을 executable infomation으로 바꾼다는 점에서는 command 패턴과 유사하게 보일 수도 있지만 목적이 다르다.

이렇게 선언이 되면 실제 유효성의 실행은 fvalid.isValid() 함수를 호출함으로써 이루어진다.



어쨌거나....Javascript 얘기를 하려던건 아니고..  이 글의 본래 주제는 Java에서는 Validation을 어떻게 처리해야 하는가이다. 찾아보면 그 쓰임의 양에 비해서 의외로 쓸만한 코드가 거의 없다. Spring 모듈에 포함되어 있는 Validation이 그나마 가장 많이 알려지긴 했지만 이는 몇가지 문제가 있다.

Spring에서 Validation을 할수 있는 방법은 크게 2가지인데 하나는 자바 코드에 직접 사용하는 Case1) Programmatic 방법이고 다른 하나는 XML 설정파일을 이용하는 Case2) Declarative 방법이다.

Case1 ) 
    public void validate(Object object, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "required", "Enter your email");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "required", "Enter your password");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmPassword", "required", "Enter the same password for confirmation");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "confirmMember", "required", "Enter ajn member code");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required", "Enter your name");
    }


Case 2)
<bean id="caseSwappingValidator" class="org.springmodules.validation.ValangValidatorFactoryBean">
 <property name="syntax">
  <value>    <![CDATA[ { name : alterCase(?) = 'sTEVEN' : 'Name must be Steven' }  ]]>  </value>
       </property>

 <property name="customFunctions">
     <map>
         <entry key="alterCase" value="com.apress.expertspringmvc.validation.AlterCaseFunction" />
  </map>
 </property>
</bean>


근데 2가지 방법 모두 별로다 -ㅅ-;

첫째로 앞의 다른 글에서 언급한적이 있지만 Validation 정보는 대부분 구성 정보에 가깝기 때문에 자바 코드에서 직접 확인하는 것은 바보같은 짓이다. 

두번째로 그래서 Bean의 XML 설정을 하기도 하는데 게시판 하나 달랑 있는 프로그램이라면 모르겠지만 그 양이 만만찮아서 XML 파일을 작성하기가 쉽지 않을뿐더러 그 대다수가 중복 정보이다. 실수로 오타라도 있다면 실제 에러가 발생하기 전까지는 거의 확인이 어렵다.

세번째로 Spring에 너무 긴밀히 통합이 되어 있어서 접착성이 많이 떨어진다. 더군다나 이 모듈을 활용하려면 HTML 작성시 id attribute 등의 일정한 약속을 따라야 한다. 

첫째 문제는 언급할 필요도 없고 사실 가장 큰 문제는 2번째인 지나친 XML 파일의 비대화이다. Framework가 유행을 타면서 원칙대로 구성정보를 설정할 수 있도록 XML 파일을 많이 활용하긴 하지만 지나친 욕심으로 일반화를 너무 많이 추구한 나머지 상대적으로 너무 많은 정보를 구성 정보에 적어야 작동할 수 있게 되어 버렸다. 

대규모 재사용은 소규모 재사용과 그 의미가 많이 다르다라고 이전에 말한적이 있는데 어느 규모이상의 코드에서 재사용의 활용도를 높이면 높일수록 이런 반작용이 더욱 심해진다. 즉 이것은 Spring Framework 개발자의 잘못은 아니지만 SpringFramework의 덩치가 커져서 더 일반화를 가지려고 할수록 실제 활용도는 오히려 떨어진다는 말이다.


그래서 좀더 간결한 방법을 찾을 필요가 있다.


다음에 계속....


'Framework > Validation' 카테고리의 다른 글

Framework - Validation(Java)  (0) 2009.02.18
Posted by bleujin



1. null이 아닌 길이가 0인 배열을 리턴하라.
메소드나 클래스나 좋은 디자인의 제 1원칙은 자신 보다는 호출자를 배려한 코드이다. 결국 좋은 메소드 디자인이란 메소드 그 자체에 있다기 보단 그 메소드를 호출하는 쪽에서 판단하는 것이다.

# Bad
public Cheese[] getCheeses() {
   if (cheeseInStock.size() == 0) return null ;
   .......
}
위 코드가 좋지 못한건 호출하는 쪽에서는

# Bad 호출하는 쪽
Cheese[] cheeses = shop.getCheeses() ;
if (cheese != null) {
    for (int i=0 ; i < cheeses.length ; i++) {

    }
}

와 같은 별도의 확인 작업을 해야하는 문제가 있다.

# Good
public Cheese[] getCheeses() {
   return (Cheese[]) cheesesInStock.toArray(new Cheese[0]) ;
}

만약 메소드를 위와 같이 작성했다면

# Good 호출하는 쪽
Cheese[] cheeses = shop.getCheeses() ;
for (int i=0 ; i < cheeses.length ; i++) {

}

null은 가능하면 리턴값으로 사용하지 않는게 좋다
.. 라는건 사실 아주 조그마한 것이지만 좋은 디자인이란 Outer에서 판단된다는 걸 잊어서는 안된다.





2. 디미터 함수 법칙

Case 1)
public void plotDate(Date aDate, Selection aSelection) {
    TimeZone tz = aSelection.getRecorder().getLocation().getTimeZone() ;
    ......

}

언뜻 별다를게 없어 보이는 위 코드는 디미터 함수의 관례를 위반하였다.

디미터 함수 법칙은 최소 지식의 원칙(Principle of Least Knowledge) 이라고도 불린다.(프로그램에서 모듈간 결합도를 최소화하려 시도한다.) OO 프로그래밍 연구 프로젝트(Demeter Project)에서 나온 법칙으로 흔히 '친한 친구들 하고만 이야기 하여라(Only talk to your immediate friends)' 라는 이야기로 요약된다. 친구는 클래스 정도로 해석하면 되며, getA().getB().getC() 하는 식으로 '여러 객체에 물어물어 가는 식으로 정보를 찾아내는 스타일은 자제하는 것이 좋다'라는 법칙이다. 

재미있는 것은 다른 법칙과 달리 디미터의 법칙은 장/단점이 생기는 법칙으로, 디미터의 법칙을 잘 따를 경우 메소드(=함수)가 많이 생기게 되면서 복잡도가 증가 할 수 있다. 하지만, 전체적으로는 잘 따르는 것이 좋은 법칙이다. 현재 디미터의 법칙이라 불리는 법칙은 디미터 프로젝트의 OO 프로그래밍 스타일 가이드 중 함수와 메소드(Law of Demeter for Functions/Methods) 부분의 가이드를 말한다.


사실 소프트웨어에서 법칙이라고 말하는게 조금 웃기긴 하지만
디미터 함수 법칙에 따르면 모든 메서드는 다음에 해당하는 메소드만을 호출해야 한다.

class Demeter {
   private A a = new A();

   private void func() {...}

   public void example(B b){
      C c = new C();
      Int f = func() ;     // 자신의 메소드
      b.invert() ;         // 메소드에 넘어온 인자
      a.setActive() ;   // 자신이 생성한 객체
      c.print() ;          // 직접 포함하고 잇는 객체
   }
}

디미터 함수 법칙을 가능한 지키려고 하면서 한 서브 프로젝틀를 리팩토링한 경험이 있는데 생각보다 훨씬 효과가 좋았다. 최근의 툴은 .을 누르면 해당 객체의 메소드가 대부분 자동으로 완성되기 때문에 무심코 사용하곤 하는데 aSelection.getRecorder().getLocation().getTimeZone() ; 식의 방법은 불필요한 관계를 맺음으로써 유지보수 하기 어려게 만들곤 한다. 




3. 인자의 유효성을 검사

public void AnonyMethod(AnonyClass ac) {
   Assert.notNull(ac) ;


    ac.fn() ;
    .............
}

이러한 방식을 방어적인 프로그램이라고 할 수 있다.

다만 일반적으로 봤을때 인자의 유효성을 호출자가 체크해야 하는가 아니면 위처럼 메소드 자체가 체크해야 하는가에 대해서는 개인적으로 아직 결론을 내리지 못했다.(섞어 쓰고 있다-ㅅ-) 위와 같은 방어적 프로그램은 메소드를 본래 의도와 상관없이 복잡하게 만들 가능성이 있기 때문이다.

에펠같은 계약에 의한 프로그래밍(PBC) 언어에서는 선조건과 후조건을 설정 가능하도록 되어 있는데 자바도 그 특징을 조금씩 도입하고 있다. 앞에서 말한바와 같이 좋다라는 판단은 호출자가 내리지만 잘못 사용하는건 내탓이오 라는 걸까.. 왠지 사회운동을 보는 것 같아 우습다 -ㅅ-











'Framework > 아키텍쳐 일반' 카테고리의 다른 글

Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Design Principle - SRP  (0) 2009.02.22
몇가지 프로그래밍 조언  (0) 2009.02.10
Bleujin Framework 활용 : HTML Parsing  (0) 2009.01.16
Posted by bleujin


몇가지 프로그래머책에서 뽑아봄.


1. prototyping은 배움의 경험이다. prototyping의 가치는 만들어낸 코드에 있지 않고, 작성하면서 배운 교훈에 있다.



2. 요구사항을 수집하지 말고 채굴하라. 

흔히 요구사항은 바닥에 널려져 있고 단지 수집(Gethering)하는 것이라고 착각하지만 보통의 요구사항은 가정과 오해, 그리고 정치의 지층들 속에 깊이 묻혀져 있다.

그리고 요구사항은 최대한 일반적 진술로 만들고, 정책에 대한 정보는 구현에서 지원해야 할것들의 예시로 넘겨주는게 좋다. 이를테면 '해당 직원의 관리자와 인사부에서만 그의 기록을 열람할수 있다. '라는 건 요구사항이 아니다. 이를 일반적 진술로 바꾸면 "권한이 부여된 사용자만이 직원기록에 접근할 수 있다." 이고 그 정책 예로 위 말을 언급할 수 있다. 이를 좀더 추상적으로 말하면 프로그래머는 접근관리 즉 권한관리 시스템을 설계해야 한다는 뜻이다.

다음으로 요구사항은 바람과 모래와 별들의 생텍쥐페리의 말대로 완성이라는 것은 더 이상 더할것이 없을때가 아니라, 더 이상 빼낼 것이 없을때 얻게 되는 것이다.

요구사항과 관련하여 마지막으로 할 말은 사용자들이 어떤 작업을 현재 어떻게 하느냐는 것을 알아내는 것보다, 왜 그걸 하는지 그 내재적 이유를 알아내는 것이 더 중요하다. xp의 실천 원칙처럼 사용자처럼 생각하기 위해서는 사용자와 함께 일하라



3. 관련없는 것들은 서로 관련없도록 하라.

기하학에서 두 직선이 직각으로 만나는 경우 직교한다고 말한다. 컴퓨팅에서 이 용어는 일종의 독립성(independence)나 결합도 줄이기(decoupling)를 의미한다. 그게 메소드가 됐든 클래스가 됐든 컴포넌트가 됐든 프레임워크가 됐든 자족적이고, 독립적이며, 단하나의 잘 정의된 목적만 갖도록 설계하라. 

직교성의 장점으로 첫번째로 생산성 향상을 들수 있다. 
   - 변화가 국소화되어 개발시간과 테스트 시간이 줄어든다.
   - 직교적인 접근법은 재사용을 촉진한다 (시스템이 더 느슨하게 결합되어 있을 수록 재설정하고 리엔지리어링하기 쉽다.)
   - 직교적인 컴포넌트들을 결합하는 경우 미묘한 생산성 향상이 있다. (M*N)

두번째로 직교성은 리스크를 감소시킨다.
   - 감염된 코드는 격리된다.
   - 시스템이 잘 깨어지지 않는다.
   - 써드파티 컴포넌트들로 연결되는 인터페이스들이 전체개발의 작은 부분에 한정되기 때문에 특정 벤더나, 플랫폼에 덜 종속될 것이다.



4. Select 는 망가지지 않았다.

OS나 컴파일러의 버그를 발견하는 일은 정말 드물게 일어나며, 심지어 써드파티 제품이나 라이브러리일지라도 드문일이다. 버그는 애플리케이션에 잇을 가능성이 가장 크다.

디버깅 사고방식
   - 가장 속이기 쉬운 사람은 자기 자신이다.(에드워드 블워 리톤) - 고무오리
   - 디버깅할때 당황하지 마라.(근시를 조심하라)
   - select는 망가지지 않았다. OS는 아마 망가지지 않았을 것이다. 데이타베이스도 아마 괜찮을 거다.

디버깅의 순서는 첫번째가 버그 재현이다. 일단 재현이 되지 않는 버그는 무척 해결하기 어렵다. 
두번째 버그를 분리해야 한다. 버그를 일으키는 프로세스의 최소 동작 집합을 만드는 걸 말한다. 
세번째 데이타를 가시화한다. 버그를 일으키는 프로세스에서 일어나는 값의 변화를 추적하고 트레이싱 하여 실제 버그를 이때 발견하게 된다. 
네번째 버그의 증명이다.
다섯번째가 가장 중요한데 새 테스트 코드를 추가하여 같은 버그는 한번만 잡도록 한다.




5. 일찍 작동을 멈추게 하라.

보통은 죽은 프로그램이 절름발이 프로그램보다 해를 훨씬 덜 끼친다.

모든 에러는 정보를 준다. 에러가 발생할리 없다고 스스로를 설득하고선 그걸 무시하기 보다는 만약 에러가 있다만 정말로 뭔가 나쁜일이 생긴 것이라고 자신에게 이야기 해야 한다.



6. 시작한 것은 끝내라

가능하다면, 리소스를 할당한 루틴이나 객체는 해제도 자기가 책임져야 한다.

private void readCustomer(final String fName, Customer cRec){
   cFile = fopen(fName, "r+") ;
   fread(cRec, sizeof(cRec), 1, cFile) ;
}

private void write Customer(Customer cRec){
   rewind(cFile) ;
   fwrite(cRec, sizeof(cRec), 1, cFile) ;
   fclose(cFile) ;
}

void updateCustomer(const String fName, double newBal){
   Customer cRec ;
   readCustomer(fName, cRec) ;
   cRec.balance = newBal ;
   writeCustomer(cRec) ;
}

위의 updateCustomer 함수는 read 호출 - value없데이트 - write 호출로 이루어져있고 별 문제 없어보이지만 깨지기 쉽다.  
정책상의 변화로 newBal이 0보다 큰경우에만 수정이 가능하도록 바꼈다고 해보자

void updateCustomer(const String fName, double newBal){
   Customer cRec ;
   readCustomer(fName, cRec) ;
   if (newBal >= 0.0) {
        cRec.balance = newBal ;
        writeCustomer(cRec) ;
   }
}
와 같이 무심코 바꾼 코드는 심각한 문제를 내포하게 된다. 논리상으로는 그럴듯해보이지만 newBal < 0 일때는 fread만 발생하고 fclose를 하지 않아 리소스의 손실이 발생하게 된다.

정확히 바꿔야 한다면 아래와 같이 바꿔야 한다.

void updateCustomer(const String fName, double newBal){
   Customer cRec ;
   readCustomer(fName, cRec) ;
   if (newBal >= 0.0) {
        cRec.balance = newBal ;
        writeCustomer(cRec) ;
   } else {
       fclose(cFile) ;
   }
}

하지만 그리 깔금해 보이지 않는다.

그보다는

private void readCustomer(File cFile, Customer cRec){
   fread(cRec, sizeof(cRec), 1, cFile) ;
}

private void write Customer(File cFile, Customer cRec){
   rewind(cFile) ;
   fwrite(cRec, sizeof(cRec), 1, cFile) ;
}

void updateCustomer(const String fName, double newBal){

   File cFile = fopen(fName, "r+");
   Customer cRec ;
   readCustomer(fName, cRec) ;
   if (newBal >= 0.0) {
        cRec.balance = newBal ;
        writeCustomer(cRec) ;
   }
   fclose(cFile) ;
}

와 같이 애초에 updateCustomer 함수가 리소스를 할당하고 해제하는 역할을 같이 담당하는게 좋다. 처음 예처럼 리소스를 할당하는 부분과 해제하는 부분을 각기 다른 메소드에 넣는다면 잠재적인 에러를 유발시킬 수 있다.



7. 통합하지 말고 설정하라.

비지니스 로직, 법률, 경영자의 취향 등을 수용하느라 코드를 변경할 때마다 새로운 버그가 시스템을 깨드릴수 있는 위험을 낳게 된다. 그러므로 세부사항에서 벗어나자. 아무리 뛰어난 천재라도 세부사항에 집착하면 그 재능이 발휘되지 않는다. (머피의 제 8법칙)

우선 시스템을 되도록 설정 가능하게 만들자, 배경색, 프롬프트 텍스트 뿐 아니라 알고리즘의 선택, 사용할 데이타베이스 제품, 미들웨에 기술, 사용자 인터페이스 스타일, 사용자 선호사항, 설치 디렉토리 등을 말이다.

이러한 방식을 메타데이터 주도 애플리케이션이라고 하는데 코드에서는 추상화를, 메타데이타에는 세부 내용을 (구체적인 것보다 추상적인것이 더 오래간다.) 담는다. 요구사항처럼 프로그램은 최대한 일반화 해서 만들고, 세부사항들은 가능하면 컴파일된 코드 기반 바깥으로 빼라.

이 방식은 아래와 같은 장점이 있다.
   - 설계의 결합도를 줄여 좀더 유연하고 적응성 있는 프로그램을 만들수 있다.
   - 세부사항을 코드 밖으로 몰아냄으로써 보다 강하고 추상적인 디자인을 만들수 있다.
   - 애플리케이션을 커스터마이징 하기 위해 다시 컴파일 할 필요가 없다.
   - 메타데이타는 범용 프로그래밍 언어보다 문제 도메인에 가까운 방식으로 표현될수 있다.
   - 동일한 애플리케이션 엔진과 상이한 메타데이타를 이용해 여러 다른 프로젝트를 진행할 수 있게 된다.





'Framework > 아키텍쳐 일반' 카테고리의 다른 글

Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Design Principle - SRP  (0) 2009.02.22
Method Design  (0) 2009.02.11
Bleujin Framework 활용 : HTML Parsing  (0) 2009.01.16
Posted by bleujin
Framework/예외처리2009. 2. 9. 08:31

밑의 예외 처리에 이어지는 글.

자바의 정설 이론은 checked exception는 정상적인 것이며 runtime exception은 처리할 수 없는 프로그래밍 에러를 가리킨다. 그리고 아래의 글중 관례 2는 자바의 정설에 기반한 관례이다.

사실 checked exception의 간단한 사용 예를 설명하는 예제들을 보면 이는 분명 멋진 생각처럼 보인다. 문제는 간단한 사용 예제가 아닐때이다.
// example
public interface User {
    public boolean login(String userId, String pwd) throws SQLException ;
    .....
}
이를테면 위의 예제와 같이 사용자 정보를 가지고 허가 여부를 확인하는 매소드를 작성하였다고 하자. 그러나 위의 인터페이스는 아주  깨어지기 쉽다. 첫번째로 사용자 정보가 DB에 있다는 가정을 하였고 두번째로 DB라고 해서 SQLException만 리턴하는 것은 아니다.(IOException등도 throw 할 수 있다.) 세번째로 그러함에도 불구하고 SQLException은 충분히 추상적이지도 충분히 세세하지도 못한 예외라른 사실이다.

물론 그런 저런 책들에서 언급한대로
// example
public interface User {
    public boolean login(String userId, String pwd) throws NoUserIdException ;
    .....
}
와 같은 새로운 고수준 사용자 예외로 처리할 방법을 생각했을 수도 있다. 사실 책에서의 글대로 이 아이디어는 아주 멋져보인다. 다만 이생각은 무균의 연구실을 벗어난 순간 아래와 같은 사실들로 오염되어 버린다.


1. 너무 많은 코드

직접 login 메소드의 구현을 담당한다고 생각해 보자.

public boolean login(String userId, String pwd) throws NoUserIdException {
      try {
           // connection DB 
           // exectue sql 
          .........
          // disconnect DB
      } catch(IOException e) {
             throw new NoUserIdException(.....) ;
      } catch(SQLException e){
             throw new NoUserIdException(.....) ;
      }
}

............... 와 같은 식의 메소드를 작성하게 된다. 별 문제 없어 보인다? What Problem ?...

문제는 이와 비슷한 식의 커스텀 예외를 던지는 메소드를 앞으로 수천개는 더 만들어야 한다는 것이다. 그 말은 수천개의 메소드들을 모두 자바의 표준 exception을 잡아서 사용자 예외로 wrapping해서 던지는 구문들만 수만줄은 된다는 얘기다.

단지 코드가 길다는게 좋은 프로그래밍이 아니다라고 할수는 없겠지만 이는 그 효과에 비해서 노력은 지나치게 많이 소모된다. 먼 미래 코드 어딘가에서 위 메소드를 사용했을때 NOUserIdException이라는 그럴듯한 이름을 가진 예외 처리를 할수 있다는 장점에 비해 위 관례를 지키기 위해 소요되는 노력은 지나치게 많다.



2. 깨지기 쉬운 메소드 원형

다시 위의 코드를 보자. login 메소드는 과연 NoUserIdException 만으로 충분한가? 위의 인터페이스로 좀더 코드를 작성하다보면 NotPasswordMatchedException이나 NotPermittedException 등등이 필요해질 것이다.
그러다 보면
public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, etc....
....와 같이 인터페이스를 수정해야 한다.

문제는 아직 더 있다. DB가 연결이 되지않을때도 엉뚱하게 그런 사용자가 없다느니 패스워드가 일치하지 않는다는 둥의 엉뚱한 예외가 튀어나올 것이다.

구현 메소드를 수정해서 connect절에 SQLException이 발생하면 다시 coonect DB 구문에 SQL 예외가 발생하면 다시 NotConnecttedUserInfo같은 사용자 예외를 던져야 할까?

그러다 보면

public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, etc....
 {
          // 변수 선언
           try {
                // connection DB 
           } catch(SQLException e){
               throw new NotConnecttedUserInfo(...) ;
          }
          // exectue sql 
         try {
           if (rs.next()) {
               if (pwd.equals(rs.getString("pwd") {
                    // 권한을 확인한다
                    try {
                       if ( isPermitted(rs)) {
                          ...
                      } else {
                         .....
                      }
                  } catch (SQLException ex) {
                   }
               } else {
                   throw new NotMatchedException(....) ;
               }
           } else {
              throw new NoUserIdException(.......) ;
           }
         } catch(SQLException e){
              ....               // outer SQLException
         } catch(IOException e) {
              .....             // outer IOException
         }

}

.........
공공장소에서 쓰레기를 버릴때 드는 죄책감이 들게된다. 첫번째 문제인 원래의 본 코드보다 예외 처리하는 코드가 더 복잡해 보인다는 건 일단 넘어가더라도 구현 메소드를 작성하면서 인터페이스를 수정해야 되는 웃기는 상황이 자주 발생하게 된다. 왜냐하면 새로운 Exception 종류가 앞으로도 계속 생겨날 것이기 때문이다. 그리고 그만큼 코드는 예외처리가 더욱 복잡해질 것이다.



3. 예외들의 끝없는 wrapping

바로 위의 예제의 // OuterSQLException 에 있는 예외 처리구문을 보자. 여기다가는 어떤 사용자 예외를 넣어야 할까? 기본적을 RecordSet인 rs의 모든 메소드는 SQLException을 던지기 때문에 안 잡을 수도 없다. 그럼 UnknownedException 같은 웃기는 이름의 exception을 새로 만들어야 할까 ? 아니면 그냥 원래의 SQLException을 던질까?

이 상황을 타계하기 위해 NotLoginnedException이라는 상위 수준으로 추상화된 Exception으로 Wrapping  했다고 하자. 

즉 이와 같이 고쳤다고 해보자.
         } catch(SQLException e){
             throw new NotLoginnedException(.... ) ;               // outer SQLException
         } catch(IOException e) {
             throw new NotLoginnedException(.... ) ;           // outer IOException
         }


그럼 메소드 원형은 public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, NotLogginedUserException etc.... 와 같이 될것이고 이 지저분한 쓰레기들을 보다가 문득 깨닫게 된다.

다른 사용자 예외들의 부모클래스로 NotLogginedUserException를 지정하면
public boolean login(String userId, String pwd) throws NotLogginedUserException ;
와 같이 간편하게 바꿀 수 있다고 ....

그럼 코드는 아래와 같이 수정할 수 있다.
public boolean login(String userId, String pwd) throws NotLogginedUserException {
      try {
           // connection DB 
           // exectue sql 
          .........
          // disconnect DB
      } catch(IOException e) {
             throw new NotLogginedUserException (.....) ;
      } catch(SQLException e){
             throw new NotLogginedUserException (.....) ;
      }
}

사실 지저분한 코드를 잔뜩 썼다가 지웠지만 그 결과문은 맨 처음 예제에서 에외 이름만 바뀐거에 불과하다. 한걸음만 더 생각해보면 우리는 닭질을 하고 있다. 앞의 예에서 우리가 사용자 예외로 처리함으로서 얻을 수 있는 작은 이득은 NoUserIdException이라는 예외 이름 그 자체에 있다고 했다.

그런데 login이라는 메소드에 NotLogginedException이라는 예외 이름이 과연 직관적인가? 그럴바엔 애초에 SQLException이나 별 차이도 없지 않은가 말이다

이렇게 변명할 수도 있다. 물론 메소드 원형에서 던지는 NotLogginedException은 좀 그렇지만 아래와 같이 

void report(UserInfo userInfo) {
       try {
            boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
            .....
       } catch(NotPermittedException ex) {
           // print notPermitted message 
           ....
      } catch(NotLoggingedException ex ) {
      }
      ........
}

와 같이 쓸수 있을거라고 ......

라는 생각은 한번도 상업 프로그램을 짜보지 않은 학자들이나 교생들 혹은 공기가 통하지 않는 곳에서 연구를 하고 있는 연구생들이나 할만한 생각이다.

첫번째로 login이라는 원형 메소드에 있는 NotLoggingedException 도 신경쓰기 귀찮은 판에 그 하위 exception 까지 문서화된 정보를(그런게 있다면 말이다..) 보면서 신경쓰고 싶지 않고

두번째로 신경쓰고 싶더라도 report라는 메소드를 작성하고 있는 프로그래머는 login 메소드 말고도 그 안에서 호출하고 있는 수많은 메소들마다 던지는 사용자 Exception에 아주 질려버릴 것이기 때문이다.

그래서 report 함수의 프로그래머는
void report(UserInfo userInfo) throw Exception{
       boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
       .....
       // 기타 등등의 코드....

}

와 같이 최상위 Exception으로 처리해야 하는 귀찮음을 대충 보자기 Exception 같은걸로 둘둘 싸서 다른 프로그래머에게 던져 버린다.

물론 쓸데없이 부지런한 프로그래머는
void report(UserInfo userInfo) throw ReportException{
       try {
          boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
          .....
          // 기타 등등의 코드....
       } catch(Exception ex){
           throw new ReportException(....) ;
       }

}
와 같이 자신의 쓰레기도 쓸쩍 넣는 치밀함을 보이기도 한다 -ㅅ-;

이를 단순히 개발자의 게으름으로 치부할 수는 없다. 엔트로피는 상승하게 마련이고 사실 작은 코드에서는 멋져보이는 해결책이 실제로는 엄청난 엔트로피의 증가를 가져오는 씨앗이 됐기 때문이다.

작은 메소드에서는 합당해 보이지만 그 메소드들을 호출하여 만드는 다른 프로그래머는 checked exception에 드는 과도한 노력을 피하기 마련이고 이는 결국 속이 보이지 않는 검은색 봉투로 exception을 모두 담아서 wrapping을 해버린다. 물론 이러한 과정에서 유용한 정보를 더한다면 나름 그럴듯한 선택일수도 있다. 그러나 애초에 복구가 가능하지 않는 NotLogginedException같은 경우 그것을 감싸는 것은 아무것도 성취하지 못하고 새로운 쓰레기만 더하게 된다. 그리고 그 쓰레기 만큼이나 stack rewind 하는 비용이 더 들게 된다.


4. 위의 이유들로 checked Exception이 인터페이스에서 잘 동작하지 않는다. 

결국 인터페이스를 작성할때 UnKnownException같은 불분명한 중립적 사용자 예외를 작성하거나 최상위 Exception을 던지는 인터페이스들을 만들다 보면 상황은 더욱 더 심각해진다.

마치 늪처럼 발버둥을 칠수록 더욱 더 깊이 빠져들게 되는 것이다.
이 문제들은 처리할 수 없는 예외들을 잡고, 감싸진 예외들을 다시 던지도록 강요받는 checked Exception의 문제로 속성지어질 수 있다. 이것은 성가시고, 그 자체가 에러를 낳기 쉬우며, 어떤 유용한 다른 목적도 제공하지 않는다.


그럼 다음에는 계속....


'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/예외처리2009. 2. 9. 05:15


exception란 말 그대로 exception이 필요한 상황은 예외적이라고 생각해서일까?
언어에 대한 기본 문법책은 아주 많이 있지만 exceptional 하게도 exception 분야는 그리 많이 다루지 않는다.

좋은 클래스와 좋은 메소드 디자인과 마찬가지로 좋은 예외처리 디자인은 중요하다. 


관례 1 예외는 예외 상황에만 써야 한다.

// Bad
try {
   int i = 0 ;
   while(true)
       a[i++].fn() ;
} catch(ArrayIndexOutOfBoundsException e) {
}


// Good
for (int i =0 ; i < a.length ; i++)
   a[i].fn() ;

예외 기반의 구현 패턴은 코드의 목적을 애매하게 만들고 성능도 떨어뜨린다. 게다가 제대로 동작하리란 보장조차 없다. 예외 기반의 구현패턴을 쓰면, 다른 버그가 있어도 프로그램은 이 버그를 감춘채 조용히 수행된다. 이런 버그는 정말 잡아내기 어렵다. 예외는 말 그대로 예외상황에서만 써야 한다.프로그램 흐름을 예외로 제어하려 하면 안된다. 


관례 2 checked exception과 runtime exception을 구분해서 던져라

처리해야 하는 예외(checked exception)는 호출자가 예외 상항을 복구 할수 있다가 기대할 수 있을때 던진다. 보통의 경우 런타임 예외는 프로그래밍 오류(precondition violation)가 발생했을때만 써야 한다. 에러는 JVM의 자원이 부족하거나, 불변규칙이 변하는 것과 같이 더 이상 프로그램을 진행할 수 없을때 JVMㅡ에서만 던지는 것이 관례이다.

그러나 관례란 관례일 뿐이다. 관례 2는 많은 예외상항이 있는데 그것에 관해서는 따로 다른 글로 포스팅하겠다.



관례 3 예외를 던질때는 신중해야 한다.

# Bad
} catch(TheCheckedException e) {
   throw new Error("Assertion error") ;
}


# BadBad
} catch(TheCheckedException e){
   e.printStackTrace() ;
   System.exit(1) ;
}

# Not Bad
if(obj.actionPermitted(args)) {
   obj.action(args) ;
} else {
   // 예외 상황을 처리한다.
}


처리해야 하는 예외를 던지는 메소드가 너무 많은 API는 아주 쓰기 번거롭다. 처리해야 하는 예외를 던지는 메소드를 호출하는 메소드는 catch 블록을 써서 이 예외를 잡거나, 같은 예외를 던진다고 선언하여 이 예외를 외부로 전파해야 한다. 사실 이런 작업은 프로그래머에게는 만만치 않은 부담을 준다. API를 정확히 쓰더라도 이런 예외가 발생할 수 있고, 프로그래머가 이 예외를 적절하게 처리할 수 있을때만 처리할 수 있는 예외를 던져야 한다. 위의 2조건을 모두 만족하지 못한다면 처리하지 않는 예외를 던지는 것이 좋다. 이 메소드를 사용하는 프로그래머들이 어떻게 처리할까 라는 질문을 한번 더 던져보자....

사실 관례 2의 예외는 바로 이 관례 3에 기인한다. 지나치기 많은 예외를 던지는것은 부작용을 너무 많이 가져온다. 



관례 4 표준 예외를 써라

클래스의 이름이 중요한만큼이나 예외의 이름도 중요하다. 따라서 가능하면 존재하는 예외라면 존재하는 표준 예외를 써주는 것이 좋다.

IllegalArgumentException : 인자값이 적절하지 못할때
IllegalStateExcetpion : 호출을 받은 객체 상태가 적절하지 못할때
NullPointerException : null을 금지한 인자값이 null일때
IndexOutOfBoundsException : 인덱스 값이 범위를 벗어났을때
ConcurrentModificationException : 동시 수정을 금지한 객체를 동시에 수정할때.
UnsupportedOperationException : 객체가 메소드를 지원하지 않을때.


관례 5 예외를 적절하게 추상화하라

// exception translation
try {

   ......
} catch(LowerLevelException e){
   throw new HigherLevelException(...) ;
}

낮은 계층의 세부 구현사항이 외부에 드러나게 되어 추상화 수준이 높은 계층의 API를 오염시킬수 있다. 인터페이스 디자인할때 어려운 점중 하나는 어느 수준의 예외를 던져야 하는가이다. 메소드 구현이 되지 않는 상태에서 어떤한 예외가 나올것이라고 추측하는 것은 미래를 예측하는 것 만큼이나 매우 매우 어렵기 때문이다. 그래서 인터페이스에서 아무 예외든 다 받아주겠어 식의 throw Exception은 다시 관례 2의 예외로 나타난다. 



관례 6 실패에 대한 자세한 정보를 상세 메시지 문자열에 담아라

예외의 문자열 표현과 최종 사용자가 쉽게 이해할 수 있어야 하는 "사용자에게 제공하는 오류 메시지"와 혼동하면 안된다. 사용자에게 제공하는 오류 메시지와 달리, 예외의 문자열 표현은 프로그래머나 유지보수 요원이 실패를 분석할때 쓰는 것이다. 따라서 예외ㅡ 문자열 표현은 이해하기 쉬운 정보를 담기보다는 자세한 정보를 담는 것이 훨씬 더 중요하다.


# Bad
try {
    ....
} catch(IOException ex) {
   throw new UserException("예외 발생.") ;
}


# Good
try {
    ....
} catch(IOException ex) {
   throw new UserException( ex.getMessage() + "예외와 관계있을수 있는 프로퍼티 정보 ") ;



관례 7 실퍠 원자성을 얻기 위해 노력하라

# Good
public Object pop(){
   if (size == 0) throw new EmptyStackException() ;

   Object result = element[--size] ;
   elements[size] = null ;
   return result ;
}



관례 8 예외를 잡아서 버리지 마라

예외를 잡아서 버리지 마라, 너무나 당연한 진리를 많은 프로그래머가 무시하고 있기 때문에 다시 한번 이야기한다. 어떤 메소드가 예외를 던질 수 있다는 것은, 이 메소드의 설계자가 여러분께 뭔가 알리고 싶어한다는 것이다.

catch 블럭 안에서 정말 아무것도 할 것이 없다면, 최소한 왜 예외를 잡어서 처리하지 않고 버리는지 그 이유라도 주석으로 달아 놓아야 한다.

그러나 이런 관례는 오히려 프로그래머의 게으름을 촉진시킬수 있다.




예외의 예외는 다음 글에서 =ㅅ=;




'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
checked exception의 문제  (0) 2009.02.09
Posted by bleujin
Framework/Database2009. 2. 7. 03:24

DB 아키텍처에 대해 깊히 이해하지 못하는 객체지향 개발자가 흔히 하는 실수중의 하나는 DB에 대한 블랙박스 증후군이다.

이들은 마치 데이타베이스를 라디오의 건전지처럼 교체할 수 있어야 한다고 생각하고 데이타베이스의 특정 기능을 사용하는 것을 마치 나쁜짓인양 무슨 수를 써서라도 피하려고 한다. 그래도 데이타베이스 독립이라는 명록으로 데이타베이스의 기능을 사용하고 데이타베이스 활용을 거부한다. (데이타베이스 종속이란 말 자체가 사실 우물에 독뿌리기 오류를 범하고 있다. 종속이란 그리 듣기 좋은 단어가 아니기 때문이다.) 사실 데이터베이스의 독립을 이루기란 극히 어려울뿐 아니라 반대의 경우보다 훨씬 더 많은 비용이 든다.

이 문제는 크게 경제성과 비경제성 문제로 나눌수 있는데

첫번째로 블랙박스 증후군에 걸린 개발자는 수학이 아니라 공학이 가져야 하는 경제성 즉 비싼 데이타베이스 비용을 낭비하거나 이미 존재하는 기능임에도 직접 작정하는 수고를 통해 많은 시간을 낭비하게 된다.

두번째로 앞에서도 말한바와 같이 실질적으로 매우 어렵다. Ansi SQL만을 사용해서 어떤 제품을 만든다는 것은 Hello World 프로그램 따위에서나 생각할 수 있다. DB 벤더마다 Length, substring 같은 기본 함수들의 사용법이나 명칭등이 다르기 때문에 고작 쓸 수 있는 SQL이란 select * from emp 같은 정도를 사용할 수 있을 뿐이다.

세번째로 어려울뿐 아니라 어떤 문제는 사실 불가능한 것들도 있다. 예컨데 트랜잭션 모델의 경우 Oracle과 MSSQL은 많은 차이가 있고 이는 설사 같은 SQL을 사용했다고 하더라도 다른 결과를 일으키는 경우가 있다.

이러한 등등의 문제로 10년이 넘는 개발자의 생활중 사실 단 한번도 DB 독립을 갖춘 제품을 보지 못했다는 경험적인 이유도 있다. 그 많은 제품들은 물론 데이타베이스의 종속을 피하고자~ 라는 말이 대부분 있었음에도 불구하고 말이다. 실질적으로 제품정도의 코드에서 데이타베이스의 종속을 제거한 코드를 만드는것은 이론적으로 불가능은 아닐지라도 매우매우 어렵다.

그래서 객체지향과 DB는 서로간에 한발 양보를 하고 타협을 하게 되는데.
하나는 객체지향쪽에서 DB쪽으로 한발 접근한 DB를 객체형식으로 다루는 등의 하이버네이트식의 접근이고
다른 하나는 DB쪽에서 아예 객체기반 DB를 만드려는 시도이다.

여기서 두번째는 이 글의 주제와 맞지 않고 사실 논리적으로도 문제가 있다. DB의 종속을 피하기 위해 객체지향 DB를 사용하라는것 자체가 모순이다.(그리고 사실 객체기반 DB는 단순히 종속성의 문제때문에 개발된게 아니다. )


첫번째 하이버네이트식의 접근은 그 열렬한 추종자가 많이 줄긴 했지만 지금도 충분히 지나치게 과대평가라고 생각한다. 몇년전 하이버네이트 소스를 며칠동안 뜯어본적이 있었는데 그 대부분이 케이스별 SQL을 만드는 string 연산이였다. 지금은 나아졌는지 모르겠지만 어쨌거나 이방식은 겉보기에는 종속을 피할 수 있을지라도 DB의 효율성을 심각하게 감소시킨다. 무료(?)인 오픈소스 디비를 사용하면 비용이 안든다고 주장하지만 그 만큼의 저 효율을 어딘가에서 메꿔야 하기 때문에 그닥 납득하지는 못하겠다.

IBatis의 방법은 조금 더 현실적이긴 하지만 이도 역시 다른 문제가 있다. 이건 머 차차 쓰기로 하고 잠이나 -ㅅ-;;;;

'Framework > Database' 카테고리의 다른 글

Framework (DBManager)  (0) 2009.03.04
Framework (구조적 중복 제거)  (0) 2009.02.21
Framework (실험실 코드)  (0) 2009.02.20
Framework (개요)  (0) 2009.02.20
Framework (커서)  (0) 2009.01.12
Posted by bleujin

굳이 메쉬업 프로그래밍을 하지 않더라도 하다보면 인터넷에서 자료를 쭉 긁어오고 싶을때가 있다.
이건 사실은 신문사 만화 사이트를 일일이 클릭해가면서 - 게다가 그 수많은 광고를 참으며 - 보느니
로컬에 그림만 다운로드 받아봐야지 하고 후다닥 만들었다-ㅅ-


이 작업을 하려면 먼저 bleujin Framework core의 jar 파일을 아래 포스팅에서 다운로드 받고..


위 3개의 jar가 추가적으로 필요하다. 예전에 작성할때 다운로드 받은거라 최근에는 버전업이 됐겠지만 그냥 쓴다-ㅅ-
아마 common-io랑 common-lang도 필요하겠지만 그건 그냥 apache 에서 다운로드 추천-ㅅ-
(오늘까지 방문자가 한명도 없으니 다소 무책임해도 상관없으리라.. 쿨럭)

Framework에는 html Parsing에 관련된 - jericho의 소스에 Facade 패턴을 사용해서 간단하게 만들 수 있었다. - 부분만 있어서 먼저 인터넷에 접속해서 InputStream을 받아오는 로직을 작성해야 한다.



public class Spider {

 private Logger log = LogBroker.getLogger(Spider.class) ;
 
 public Reader getPageContent(String httpURL) throws HttpException, IOException {
  String content = IOUtils.toString(getInputStream(httpURL), "EUC-KR") ;
  return new StringReader(content) ;
 }

 public InputStream getInputStream(String httpURL) throws HttpException, IOException {
  HttpClient httpclient = new HttpClient();
  GetMethod httpget = new GetMethod(httpURL);
  try {

   httpclient.executeMethod(httpget);
   log.info("recevied data : " + httpURL) ;
   
   InputStream input = httpget.getResponseBodyAsStream() ;
   InputStream result = new ByteArrayInputStream(IOUtils.toByteArray(input)) ;
   input.close() ;
   return result ;
   
  } catch (Exception ex) {
   log.warning(ex.getMessage()) ;
   throw new HttpException(ex.getMessage(), ex) ;
  } finally {
   httpget.releaseConnection() ;
  }
 }
}

EUC-KR의 상수 부분이 걸리긴 하지만 머 대충 만들고 넘어가자-ㅅ-(사실 사이트를 제대로 만들었다면 EUC-KR은 요즘의 국제화 시대에 맞지 않는 인코딩방법이다. )

사용은 대충 아래와 같이 한다.

 public void testSportChosun() throws Exception {
  String httpURL = "http://manhwa.sportschosun.com/enter/cartoon/juyu/cartoon_juyu2.asp?num=138&title=juyu&Direct=Next" ;
  Spider s = new Spider() ;
  Reader content = s.getPageContent(httpURL) ;
  HTag tag = HtmlParser.parse(content) ;
  
  HTag img = tag.findElementBy(IMG, "src", "http://manhwa.sportschosun.com/enter/cartoon/juyu/image_cartoons/20071119y_233.gif") ;
  System.out.println(img.toString());
  System.out.println(img.getPath());
  
  
  String imgPath = img.getAttributeValue("src") ;
  System.out.println(imgPath);
  
  content.close() ;
 }


 public void testSave() throws Exception {

  int current = 100 ;
  Spider s = new Spider() ;
  for (int i = 1; i <= current; i++) {
   String path = "http://manhwa.sportschosun.com/enter/cartoon/juyu/cartoon_juyu2.asp?num=" + i + "&title=juyu&Direct=Next" ;
   Reader content = s.getPageContent(path) ;
   HTag tag = HtmlParser.parse(content) ;
   HTag img = tag.getChild("body[0]/table[4]/tr[0]/td[2]/table[1]/tr[8]/td[0]/img[0]") ; // /html/body/table/tr/td/table/tr/td/img
   String imgSrc = img.getAttributeValue("src");
   
   FileOutputStream output = new FileOutputStream(new File("c:/temp/" + StringUtil.substringAfterLast(imgSrc, "/")));
   IOUtils.copy(s.getInputStream(imgSrc), output) ;
   output.close() ;
  }
 }


HTag가 클래스가 Facade 클래스인데 이런저런 다양한 파싱에 관련된 메소드들을 모두 가지고 있다.

이를테면 getChild("body/table[4]/tr[0]/td[2]/table[1]/tr[8]/td[0]/img[0]") 는
body안에 5번째 table 태그안에 첫번째 tr안에 3번재 td안에 2번째 table안에 9번째 tr안에 첫번째 td안에 첫번째 img Tag를 가져오라는 뜻이다 -ㅅ-;;


만화그림 다운로드 받는데 쓰다가.
나중에 네이버나 다음에 있는 상장된 기업정보 페이지를 모두 긁어서 데이타베이스 구축하는데도 사용했다.


야후는 일정시간에 많은 GET이 날라가면 자동으로 해당 아이피 Block을 시켜버린다=ㅅ=
외국 기업이라 그런지 그런 기본적인 것에 충실하다.



'Framework > 아키텍쳐 일반' 카테고리의 다른 글

Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Design Principle - SRP  (0) 2009.02.22
Method Design  (0) 2009.02.11
몇가지 프로그래밍 조언  (0) 2009.02.10
Posted by bleujin