본문 바로가기

Dev.../플밍 관련 자료

[펌] EJB 기반 프로젝트 수행 가이드] ④

EJB 기반 프로젝트 수행 가이드] ④
김주현 (ERP 개발자)
2004/04/26
① 세션빈에서의 DB 접근전략 및 엔터티빈 사용시 주의사항
② 세션빈에서의 트랜잭션 관리
③ 비즈니스 프로세스 구현 최적화하기
④ 능률 높여주는 유틸리티를 사용하자

EJB를 사용하는 프로젝트에서 개발속도를 향상시킬 수 있는 유틸리티에 대해 알아보자. 이 유틸리티들은 개발자가 프로젝트를 시작할때 직접 만들어서 사용하는 것이다.

EJB 스펙을 지원하는 개발툴 중 비주얼 카페, 비주얼 에이지 등을 사용해본 개발자라면 이 툴들이 EJB 개발에 많은 도움을 주지만 소위 ‘무겁다’는 단점이 있음을 느꼈을 것이다.

PC 사양이 받쳐줘야 돌릴 수 있다는 것 외에, 이러한 툴들은 전체 개발을 놓고 볼 때 오히려 개발자의 집중에 저해요소로 작용하는 경우도 빈번하다.

따라서 개발자가 선택하게 되는 최종적인 툴은 바로 텍스트 에디터 프로그램이다. 실제로 개발자들을 보면 비주얼 카페나 비주얼 에이지를 기동시키고 코딩하는 것보다 "텍스트 에디터로 모든 것을 해결하기"를 선호하는 경우가 많다.

텍스트 에디터를 사용하면서 문제는 테이블 하나당 INSERT, DELETE, UPDATE, SELECT라는 DML 문을 DB 래퍼 클래스에 코딩할 때 남은 반복적인 작업을 어떻게 해결하느냐가 될 것이다. 이때 개발자 유틸리티가 필요하다.

DB 래퍼 클래스 자동 생성하기
DB 래퍼 클래스는 주로 테이블 하나당 하나씩 만들게 된다. EMP 테이블에 대한 INSERT, DELETE, UPDATE, SELECT 문을 실행시킬 EmpDb라는 클래스를 만든다고 생각해보자. 필요한 SQL문을 실행시키는 메소드가 나열될 것이다.

개발자는 다음 DEPT 테이블에 대해서도 똑같은 작업을 해야한다. 테이블 컬럼 명이 다르기 때문에 Copy & Paste 작업도 여의치 않다.

이쯤되면 해당 테이블의 컬럼명 정보 및 타입, PK 정보 등을 읽어서 자동으로 메소드들을 생성시켜 주는 유틸리티를 만들어야겠다는 생각이 들 것이다. PC에서 간단히 실행만 시키면 DB래퍼 자바 파일을 만들어주는 유틸리티가 있다면 작업 속도를 향상시키고 능률을 높일 수 있을 것이다.

이 클래스를 실행시키면 먼저 프로퍼티 파일을 읽어와야 한다. 클래스 안에 직접 테이블 명 혹은 DB URL 등을 타이핑해서 쓰는 것 보다, 프로퍼티 파일 안의 테이블 명, 접속 DB 정보 등을 읽어오도록 함으로써 보다 쉽게 활용할 수 있는 방향으로 만들어야 한다.

다음은 해당 DML 문이 적혀있는 파일을 생성시켜야 한다. 이 작업에는 파일을 생성시켜야 하므로 PrintWriter 클래스도 등장하고 프로퍼티 파일을 읽어올 때 Properties 클래스도 써야 할 것이다.

핵심적인 기능인 SELECT 및 DML문 생성 시에는 어떤 클래스가 사용될까.
DatabaseMetaData 인터페이스의 getColumns 메소드와 getPrimaryKeys 메소드를 이용하도록 하자.

아래는 컬럼 어레이를 리턴하는 메소드이다.

소스보기 1

이 메소드를 호출할 시에 catalog, schemaPattern, tableNamePattern, columnNamePattern을 넘기면(주로 catalog나 shema는 null로 넘겨진다) DatabaseMetaData는 받은 조건으로 검색한 후, 해당하는 컬럼이 있으면 그것의 모든 정보를 ResultSet 형태로 가지고 온다. 이를 컬럼 어레이로 받아오는 것이다.

DatabaseMetaData 인터페이스는 DB의 래퍼 클래스와 같은 역할을 하는 것이다. 이제 이 컬럼 정보로 만드는 INSERT문을 보자.

소스보기 2
  /********************************************************************
   INSERT 문 String ( insert_sql_2 는 값들을 받을 구절을 만들기 위해 미리 선언한다. )
  ********************************************************************/
  String insert_sql = "\t\t\t\"INSERT INTO " + tables_name + " (\" +\r\n";
  String insert_sql_2 = "";

  for (int j = 0; j < columns.length; j++) {

    if (j < (columns.length - 1)) {

      insert_sql += "\t\t\t\" " + columns[j].column_name.toUpperCase() + ", \" +\r\n";
      insert_sql_2 += "?, ";

    } else {

      insert_sql += "\t\t\t\" " + columns[j].column_name.toUpperCase() + " \" +\r\n";
      insert_sql_2 += "?";

    }
  }


  /********************************************************************
     INSERT 문 완성
  ********************************************************************/
  insert_sql += "\t\t\t\") VALUES (" + insert_sql_2 + ")\";";

  /********************************************************************
     out 객체는 PrintWriter 객체
     @param  info 는 , 한 테이블의 정보를 실어나르는 객체명
  ********************************************************************/

  out.println("  /**");
  out.println("   * insert.");
  out.println("   *");
  out.println("   * @param  info");
  out.println("   * @return  row count");
  out.println("   * @exception  java.sql.SQLException");
  out.println("   */");
  out.println("  public int " + method_insert + "(" + info_class_name + " info)");
  out.println("  throws java.sql.SQLException {");
  out.println();
  out.println("    int rowCount = 0;");
  out.println("    PreparedStatement pstmt = null;");
  out.println("    String sql = ");
  out.println(insert_sql);
  out.println();
  out.println("    try {");
  out.println("      pstmt = con.prepareStatement(sql);");
  out.println();
  for (int j = 0; j < columns.length; j++) {
    String methodName = columnName[j].substring(0, 1).toUpperCase() + columnName[j].substring(1);
    String rsMethodName = DBConnection.rsMethod(javaType[j]);
    out.println("      pstmt.set" + rsMethodName + "(" + (j+1) + ", info.get" + methodName + "());");
  }
  out.println();
  out.println("      rowCount = pstmt.executeUpdate();");
  out.println("    } finally {");
  out.println("      DBManager.closeStatement(pstmt);");
  out.println("    }");
  out.println();
  out.println("    return  rowCount;");
  out.println("  }");
  out.flush();

실행하면 EmpDb.java 파일의 INSERT 문이 아래와 같이 생성될 것이다.

소스보기 3
  /**
   * insert.
   *
   * @param  info
   * @return  row count
   * @exception  java.sql.SQLException
   */
  public int insert(EmpInfo info)
  throws java.sql.SQLException {

    int rowCount = 0;
    PreparedStatement pstmt = null;
    String sql =
      "INSERT INTO EMP (" +
      "  EMPNO, " +
      "  ENAME, " +
      "  JOB, " +
      "  MGR, " +
      "  HIREDATE, " +
      "  SAL, " +
      "  COMM, " +
      "  DEPTNO " +
      ") VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

    try {
      pstmt = con.prepareStatement(sql);

      pstmt.setLong(1, info.getEmpNo());
      pstmt.setString(2, info.getEName());
      pstmt.setString(3, info.getJob());
      pstmt.setLong(4, info.getMgr());
      pstmt.setTimestamp(5, info.getRegDate());
      pstmt.setLong(6, info.getSal());
      pstmt.setLong(7, info.getComm());
      pstmt.setLong(8, info.getDeptNo());

      rowCount = pstmt.executeUpdate();
    } finally {
      DBManager.closeStatement(pstmt);
    }

    return  rowCount;
  }

INSERT 문을 예로 들었는데, 마찬가지 방법으로 SELECT, DELETE, UPDATE 문도 만들 수 있을 것이다.

다만 SELECT 문의 경우 테이블의 Primary Key를 인자로 받을 때와 받지 않을 때, 만들어지게 되는 쿼리가 다르므로 인자로 받는 Primary Key 갯수마다 각각 SELECT 문을 만들어 볼 수도 있다.

이때는 역시 DatabaseMetaData의 getPrimaryKeys 메소드를 이용해 해당 컬럼의 Primary Key 컬럼을 컬럼 어레이로 받아서 작업한다.

SQL문에 더블 쿼테이션을 붙이는 작업
실제 INSERT, DELETE , UPDATE 문은 위 유틸리티의 활용도가 상당히 높다. 하지만 SELECT 문은 역시 개발자의 손이 필요하다. 조회조건이 다양하고, 사용하게 될 표의 형식이 복잡하기 때문에 하나의 테이블에 대해 Primary Key로만 검색하는 SELECT문은 실제로 잘 쓰여지지 않으며 개발자가 쿼리를 새로 만드는 일이 빈번한 것이다.

개발자들은 주로 자신이 만든 쿼리를 SQL 네비게이터나 골든 뷰 등의 툴을 이용해 실행시켜 정상적으로 동작하고 데이터들이 올바로 표시되는 쿼리라는 것을 확인 한 후에 자바클래스의 메소드에 붙이게 된다.

쿼리를 문자열로 바꾸기 위해 더블 쿼테이션 마크를 붙여야 하는데 이 작업을 일일이 하고 있는 것도 체력 및 시간 낭비이므로 아래와 같은 간단한 유틸리티를 사용해 보자.

quotationSample.html

더블 쿼테이션 마크를 붙이거나 제거하는 이 유틸리티는 StringBuffer가 아닌 String을 바로 사용한다. 이를 보고 String은 Immutable이므로 String에 대해 가변적인 작업을 할 때는 StringBuffer를 쓰되, 최종적으로 StringBuffer.toString() 메소드를 쓰는 것이 자원활용 면에서 더 나은 것이 아닌가 하는 생각이 들 수도 있다.

즉 아래의 형태가 아니라,

예제(1)
String sql = "SELECT " +
      "  EMPNO, " +
      "  ENAME, " +
      "  JOB, " +
      "  MGR " +
      " FROM EMP   WHERE 1=1 " ;

      if( empNo != null && "".equals( empNo ) == false ){
        sql += " AND EMPNO = ? ";
      }
      if( job != null && "".equals( job) == false ){
        sql += " AND JOB = ? ";
      }
    try {
      pstmt = con.prepareStatement(sql);

      int idx = 1 ;

      if( empNo != null && "".equals( empNo ) == false ){
        pstmt.setString( idx++ , empNo );
      }
      if( job != null && "".equals( job ) == false ){
        pstmt.setString( idx++ , job );
      }

      rs = pstmt.executeQuery();

      // 이하 생략

아래와 같은 형태로 쓴다는 것인데

예제(2)
StringBuffer sql = new StringBuffer();
sql.append( " SELECT EMPNO " )
      .append( " ENAME,")
      .append( " ENAME,")
      .append( " JOB," )
      .append( " MGR " )
      .append( " from EMP WHERE 1=1 ");

      //...

      if( empNo != null && "".equals( empNo ) == false ){
  sql.append( " AND EMPNO = ? " );
  }
  if( job != null && "".equals( job ) == false ){
  sql.append(" AND JOB = ? " );
  }

      pstmt = con.prepareStatement(sql.toString());

결론부터 말하자면 예제(1)의 형태를 추천한다. 코드 가독성 면에서 예제(1)이 훨씬 뛰어나고, StringBuffer와 String간의 퍼포먼스는 크리티컬한 정도로 차이가 나지 않는다. 쿼리 실행시 가장 신경 써야할 부분은 쿼리가 최적화된 경로로 실행돼야 한다는 점이다. 따라서 굳이 StringBuffer를 써서 개발 시 손이 더 많이 가고, 나중에 유지보수 할 때 쿼리 읽기가 힘들어지는 단점을 감수하지 않도록 하자.

마치며
2004년 5월이라는 이 시점에 EJB도 그다지 신기술은 아니다. 하지만 EJB 기술을 쓰는 개발자가 많음에도 실제 프로그램 개발 시의 최적화에 대한 가이드로써 공유된 부분은 많지 않다.

“하루에 1000라인 이상 개발할 수 있다”는 능력도 중요하지만, 짧다면 짧은 4회분의 이번 강의를 통해 필자가 전달하려 노력한 것은 실제 프로젝트를 진행하면서 “노동력을 낭비하지 않게 하는 방법들”이다. 작으나마 도움이 됐으면 하는 바램이다. @