본문 바로가기
Programing/Spring

Method 주석에 @SQL은 무엇일까? DocletSqlMapClientFactoryBean이란?

by Tomining 2017. 5. 29.
ibatis를 사용하면 보통 xml에 SQL을 작성하거나 아니면 JPA처럼 ORM을 사용한다.
그런데 코드 분석 중 메서드 주석에 @SQL 구문을 발견하였다.
/**
 * @SQL
 <![CDATA[
    SELECT *
    FROM USER
 ]]>
 */
public List<T> listUser(Map param) {
    //...
}

처음에는 단순 주석인 줄 알았다.(가끔은 DAO 메서드에서 실행되는 SQL을 찾아 가기가 귀찮을 때가 있기 때문에… 물론 IDE plugin들이 많아서 그 귀찮음이 많이 없어지긴 했지만 말이다.)

단순 주석은 아닌 것 같아서 SqlMap 설정을 살펴 보았다. 눈에 띄는 것이 DocletSqlMapClientFactoryBean 이다.
@Bean(name = "sqlMapClientFactoryBean")
public DocletSqlMapClientFactoryBean docletSqlMapClientFactoryBean(
   @Value("classpath:sqlmap/sqlmap-config.xml") Resource configLocation,
   @Value("classpath*:com/**/*DAO.java") Resource[] mappingLocations) {
   DocletSqlMapClientFactoryBean sqlMapClientFactoryBean = new DocletSqlMapClientFactoryBean();
   sqlMapClientFactoryBean.setDataSource(dataSource(null));
   sqlMapClientFactoryBean.setConfigLocation(configLocation);
   sqlMapClientFactoryBean.setMappingLocations(mappingLocations);

   return sqlMapClientFactoryBean;
}

DocletSqlMapClientFactoryBean 이 클래스가 SQL을 파싱하여 DB에 쿼리를 수행할 수 있도록 해 주는 넘인 듯 하다.
어떻게 SQL을 파싱할 수 있을까? 궁금해서 무작정 클래스를 열어 보았다.
public String parseSource(JavaClass javaClass) throws NestedIOException {
   boolean isDAO = false;
   Annotation[] arr$ = javaClass.getAnnotations();
   int len$ = arr$.length;

   for(int i$ = 0; i$ < len$; ++i$) {
      Annotation annotation = arr$[i$];
      if(annotation.getType().getJavaClass().isA(SqlMap.class.getName())) {
         isDAO = true;
         break;
      }
   }

   if(!isDAO) {
      return null;
   } else {
      if(this.log.isDebugEnabled()) {
         this.log.debug(javaClass.getName() + " is a annotated SQLMAP DAO bean");
      }

      Sqlmap sqlMap = new Sqlmap();
      String namespace = javaClass.getFullyQualifiedName();
      sqlMap.setNamespace(namespace);
      JavaMethod[] arr$ = javaClass.getMethods();
      int len$ = arr$.length;

      for(int i$ = 0; i$ < len$; ++i$) {
         JavaMethod javaMethod = arr$[i$];
         DocletTag sqlTag = javaMethod.getTagByName("SQL");
         if(sqlTag != null) {
            if(this.log.isDebugEnabled()) {
               this.log.debug("parsing SQL : " + javaClass.getName() + "#" + javaMethod.getName());
            }

            String methodNm = javaMethod.getName();
            Statement stmt = null;
            if(!methodNm.startsWith("list") && !methodNm.startsWith("select")) {
               if(methodNm.startsWith("count")) {
                  stmt = new Select();
                  ((Statement)stmt).setResultClass("int");
               } else if(methodNm.startsWith("update")) {
                  stmt = new Update();
               } else if(methodNm.startsWith("delete")) {
                  stmt = new Delete();
               } else if(methodNm.startsWith("insert")) {
                  stmt = new Insert();
               } else if(methodNm.startsWith("sql")) {
                  stmt = new Sql();
               }
            } else {
               stmt = new Select();
               ((Statement)stmt).setResultClass(ColumnToJavaNameConversionMap.class.getName());
               if(methodNm.startsWith("select") && javaMethod.getReturns().isPrimitive()) {
                  ((Statement)stmt).setResultClass(javaMethod.getReturns().getValue());
               }
            }

            if(stmt == null) {
               throw new NestedIOException("invalid statement for " + namespace + "#" + methodNm);
            }

            String locInfo = "/* " + javaClass.getFullyQualifiedName() + "." + javaMethod.getName() + "(" + javaClass.getName() + ".java:" + javaMethod.getLineNumber() + ") */\n";
            String xml = "<statement>" + locInfo + sqlTag.getValue() + "\n/* END " + javaClass.getName() + "." + javaMethod.getName() + " */ </statement>";

            try {
               Statement tmpStmt = (Statement)this.unmar.unmarshal(new StringReader(xml));
               ((Statement)stmt).getContent().addAll(tmpStmt.getContent());
            } catch (JAXBException var21) {
               throw new NestedIOException("invalid sqlmap statement for " + namespace + "#" + methodNm + "\n" + SQLFORM.formatSQLAsString(sqlTag.getValue().replace("<![CDATA[", "/*CDATA*/").replace("]]>", "/*CDATA_END*/").replace("<", "/*STAG").replace(">", "ETAG*/").replaceAll("#([^#]*)#", "'_SHARP_$1_SHARP_END_'")).replace("/*CDATA*/", "<![CDATA[").replace("/*CDATA_END*/", "]]>").replace("/*STAG", "<").replace("ETAG*/", ">").replaceAll("'_SHARP_(.*)_SHARP_END_'", "#$1#") + "\n", var21);
            }

            ((Statement)stmt).setId(methodNm);
            ArrayList<String> args = new ArrayList();
            JavaParameter[] arr$ = javaMethod.getParameters();
            int len$ = arr$.length;

            for(int i$ = 0; i$ < len$; ++i$) {
               JavaParameter javaParameter = arr$[i$];
               args.add(javaParameter.getName());
            }

            String[] paramnames = new String[args.size()];
            this.sqlMapClientWrapper.addParameter(namespace + "." + methodNm, (String[])args.toArray(paramnames));
            if(args.size() > 0) {
               ((Statement)stmt).setParameterClass("java.util.Map");
            }

            if(stmt != null) {
               sqlMap.getStatements().add(stmt);
            }
         }
      }

      StringWriter writer = new StringWriter();
      this.transformer.setOutputProperty("indent", "yes");

      try {
         this.transformer.transform(new JAXBSource(this.context, sqlMap), new StreamResult(writer));
      } catch (Exception var20) {
         throw new NestedIOException("error from " + javaClass.getFullyQualifiedName() + ":" + var20.getMessage(), var20);
      }

      return writer.toString();
   }
}

뭔가 복잡해 보이긴 하지만 로직은 간단하다.
메서드 주석에 작성된 @SQL 구문을 파싱하여 Sql로 사용한다. 단 몇 가지 규약이 있다.(필히 지켜져야 하는 항목)
  • 맴버 속석 중 SqlMap 인터페이스를 구현한 클래스가 존재해야 한다.
    클래스 레벨에 @SqlMap 을 넣어 주어도 되고, SqlMapClientTemplate 같은 빈을 주입해 두어도 된다.
  • Select를 위해서는 메서드 이름이 list 또는 select로 시작해야 한다.
  • 그 외에는 update, delete, insert로 시작하고 나머지는 Sql로 프로시저 같은 단순 수행 sql을 의미한다.
  • <![CDATA[ ]]>로 꼭 묵어주자. 아니어도 상관없지만 비교 구문인 <, >에서 파싱 오류가 발생할 수 있다.

결론

XML로만 SQL을 지정할 수 있는 줄로만 알고 있었는데, @SQL을 이용하여 클래스 내에 주석을 활용할 수 있는 방법도 있다는 것을 처음 알게 되었다. 아직은 어떤 이점이 있는지 알지 못하겟지만 method만 봐도 어떤 SQL이 수행될 지 알 수 있는 편리성이 있는 것만 같다.
다만 메서드 명명 규칙에 규약이 있기 때문에 프로젝트 개발시 코딩 컨벤션은 잘 고려를 해야 할 것 같다.