Whiteship's Note

'모하니?/Coding'에 해당되는 글 277건

  1. 2010/03/11 [구글 Visualization API] Fluent Interface 적용하기
  2. 2010/03/07 [Google Visualization API] 캬.. 멋지구나~ (6)
  3. 2010/03/05 톰캣 메일링 리스트 가입하기
  4. 2010/03/03 우편번호 검색&입력기 만들기
  5. 2010/02/17 [스프링 3.0 @MVC] 컨트롤러에 스프링 AOP가 적용되지 않는다는건 이제 거짓말
  6. 2010/02/11 [SVN] Spring Framework 3.0.0.RELEASE
  7. 2010/01/21 [봄싹 DevTerms] 활성화 맞춤형 개발
  8. 2010/01/20 2010 대한민국 매쉬업 참가했습니다. (10)
  9. 2010/01/19 개발 용어 한글화 프로젝트 "DevTerms"
  10. 2009/12/19 [웹 사이트 속도 향상 베스트 프랙티스 10] 자바스크립트 CSS 크기 줄이기 (2)
  11. 2009/12/18 [스프링 테스트 확장] static member class를 빈으로 등록하는 테스트 로더 1 (6)
  12. 2009/12/16 ProxyFactoryBean을 이용한 초간단 AOP 구현 (2)
  13. 2009/12/16 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 4 - 일단 끝 (4)
  14. 2009/12/15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 3
  15. 2009/12/15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 2 (2)
  16. 2009/12/15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 1
  17. 2009/12/09 스프링이 컴포넌트 스캔을 할 때 클래스로딩을 하지 않는다는 증거 (2)
  18. 2009/12/04 조금 친절한 코드 생성기 7(일단 완성) - DAO 생성기 코딩
  19. 2009/12/04 조금 친절한 코드 생성기 6 - 테스트 코드 리팩토링
  20. 2009/12/04 조금 친절한 코드 생성기 5 - 리팩토링
  21. 2009/12/04 조금 친절한 코드 생성기 4 - 설계 변경 적용(인터페이스)
  22. 2009/12/04 조금 친절한 코드 생성기 3 - 설계 변경 적용(Map)
  23. 2009/12/04 조금 친절한 코드 생성기 2 - 설계 변경
  24. 2009/12/04 조금 친절한 코드 생성기 1 - 구상
  25. 2009/12/03 불친절한 코드 생성기 5(일단 끝) - 프리마커 기반 코드 생성기 만들기 (2)
  26. 2009/12/03 불친절한 코드 생성기 4 - 템플릿 만들기
  27. 2009/12/03 불친절한 코드 생성기 3 - Freemarker 학습 테스트
  28. 2009/12/03 불친절한 코드 생성기 2 - 구상2
  29. 2009/12/03 불친절한 코드 생성기 1 - 구상 (2)
  30. 2009/12/01 [웹 사이트 속도 향상 베스트 프랙티스 9] DNS 룩업 줄이기

[구글 Visualization API] Fluent Interface 적용하기

모하니?/Coding : 2010/03/11 14:18


http://martinfowler.com/bliki/FluentInterface.html

이름은 좀 멋지지만 별거 아니라는...

        Statistics statistics = new Statistics(StatisticsChartType.MOTION, "스터디 참석자 통계 그래프");
        statistics.addColumn(StatisticsColumnType.STRING, "스터디 이름");
        statistics.addColumn(StatisticsColumnType.DATE, "모임 날짜");
        statistics.addColumn(StatisticsColumnType.NUMBER, "참석자 수");
        statistics.addColumn(StatisticsColumnType.NUMBER, "모임 횟수");
        statistics.addColumn(StatisticsColumnType.NUMBER, "참석율");
        statistics.addColumn(StatisticsColumnType.STRING, "스터디 상태");
        statistics.addColumn(StatisticsColumnType.STRING, "모임명 이름");

이 코드는 얼마전에 봄싹 사이트에 Statistics 메뉴를 추가하면서 코딩한 것인데 오늘 보다가 갑자기 Fluent Interface가 떠올랐습니다. 그래서 적용해봤지요.

        Statistics statistics = new Statistics(StatisticsChartType.MOTION, "스터디 참석자 통계 그래프")
            .withColumn(StatisticsColumnType.STRING, "스터디 이름")
            .withColumn(StatisticsColumnType.DATE, "모임 날짜")
            .withColumn(StatisticsColumnType.NUMBER, "참석자 수")
            .withColumn(StatisticsColumnType.NUMBER, "모임 횟수")
            .withColumn(StatisticsColumnType.NUMBER, "참석율")
            .withColumn(StatisticsColumnType.STRING, "스터디 상태")
            .withColumn(StatisticsColumnType.STRING, "모임명 이름");

조금 fluent 하게 보이나요??ㅋ 객체를 다 만든 다음에 추가하는게 아니라 객체 만들 때 메서드를 연달아 호출하는 모양이여서 메서드 이름을 add -> with로 변경했습니다. 메서드 리턴 타입도 당연히 바꿨지요.

장점은 저렇게 조금 API가 이쁘고 사용하기 편해진다는 것이고 단점은 디버깅할 때 조금 불편할 수 있다는데.. 머.. 글쎄요. 그다지...
저작자 표시
top


[Google Visualization API] 캬.. 멋지구나~

모하니?/Coding : 2010/03/07 23:12


http://code.google.com/intl/ko-KR/apis/visualization/interactive_charts.html
http://code.google.com/intl/ko-KR/apis/visualization/documentation/index.html
http://code.google.com/intl/ko-KR/apis/visualization/documentation/using_overview.html
http://code.google.com/intl/ko-KR/apis/visualization/documentation/reference.html
http://code.google.com/apis/ajax/playground/?type=visualization#motion_chart


09-10 시즌 보딩을 저번 주를 끝으로 접었더니 주말에 갑자기 시간이 좀 생겼네요.
심심해서 낮에는 본가에 다녀오고 저녁에는 와이프가 공부하느라 흑흑.. 저는 코딩이나...

http://springsprout.org/statistics/index.do
저작자 표시
top


톰캣 메일링 리스트 가입하기

모하니?/Coding : 2010/03/05 20:34


http://tomcat.apache.org/lists.html#tomcat-dev

1. 메일 보내기

위 페이지에서 원하는 메일링 리스트로 메일을 보낸다. 제목도 없고 본문도 없이 그냥 빈 메일..

2. 답장하기

그럼 조금 뒤에 메일이 한통오는데 잘 읽어보면 결국 확인할겸 해당 주소로 답장을 보내달라는 것이다.
그냥 답장 버튼을 눌러서 간단하게 적어서 전송하면 된다.

3. 확인하기

그럼 확인 메일이 또 한통 온다.

끝.
저작자 표시
top


우편번호 검색&입력기 만들기

모하니?/Coding : 2010/03/03 16:30


1. 우편번호 DB 다운받기

http://www.zipfinder.co.kr/zipcode/index.html

우정국에서 제공하는 zipcode 데이터를 받습니다.
여러가지 형태가 있는데 저는 가장 컬럼 갯수가 적은 걸 받았습니다.

2. 도메인 만들기

@Entity
@DomainInfo("우편번호")
public class Zipcode {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    int id;
    @Column(length=7)
    @DomainInfo("zipcode")
    String zipcode;
    @Column(length=4)
    String sido;
    @Column(length=15)
    String gugun;
    @Column(length=52)
    String dong;
    @Column(length=17)
    String bunji;

위에서 받은 필드에 맞게 정의해줍니다.
hbm2ddl을 auto 모드로 놓고 개발중이라 하이버네이트가 돌다가 테이블이 없어서 만들어 줍니다.
제품 단계에서 hbm2ddl을 auto로 설정해두는 것은 매우 위험하오니 꼭 개발중에만 사용하시길..(저는 배포 중에도 쓰지만 ㅋㅋㅋ)

3. zipcode 데이터 넣기

1번에서 받은 데이터를 엑셀로 변환하고 만개 단위로 나눠서 저장했습니다. DBUnit으로 넣었는데 5만개가 넘는걸 한 방에 넣으려고 했더니 POI 쪽에서 에러가 나더군요. 스크롤 기능이 있는지 찾아보려다 귀찮아서 그냥 만개 단위로 잘라서 넣었습니다.

        dbUnitSupport.insertXlsData("zipcode1.xls");
        dbUnitSupport.insertXlsData("zipcode2.xls");
        dbUnitSupport.insertXlsData("zipcode3.xls");
        dbUnitSupport.insertXlsData("zipcode4.xls");
        dbUnitSupport.insertXlsData("zipcode5.xls");

대충 요런식으로..

4. 사용자 추가 화면에 우편번호 자동완성 필드 추가

<k:fzipcode />

이 태그 파일을 만들고 저거 한줄을 사용자를 추가하는 jsp에 추가해줍니다. 저 태그 파일은 대충..

...
<c:set var="pathNS" value="${path != null ? path : 'zipcode'}" />
<c:set var="labelNS" value="${label != null ? label : '우편번호'}" />
<c:set var="popupUrlNS" value="${popupUrl != null ? popupUrl : '/system/zipcode/popupsearch.do'}" />

<o:fSearchPopup label="${labelNS}" url="/ajax/zipcode/dong.do" path="${pathNS}"
    model="zipcodes" size="20"  maxlength="20" />

<script type="text/javascript">
function formatItem(row, i, total){
    return row[1] + " " + row[2] + " " + row[3] + " " + row[4] + " " + row[5];
}
function formatResult(row){
    return row[1];
}
function result(event, row, formatted){
    if (row){
        $("#addr").attr("value", row[2] + " " + row[3] + " " + row[4]);
    }
}
$("#${pathNS}row img").click(function(e) {
    popup("<c:url value='${popupUrlNS}'/>", "itemPopup", 900, 900, "yes", "yes");
});
</script>
...

이런식으로 생겼습니다. 여기서 또 다른 태그 파일을 사용하고 있는데 그녀석이 예전에 만들어둔 Ajax 자동완성 태그 파일입니다.

5. Ajax 요청처리를 구현합니다.

    @RequestMapping("/ajax/zipcode/dong.do")
    public ModelAndView zipcodes(ModelMap map, AjaxParams ajaxParams) {
        map.addAttribute("zipcodes", zipcodeService.findZipcodesByDong(ajaxParams));
        return new ModelAndView("jsonView", map);
    }

Ajax 요청이 왔을 때 우편번호에 필요한 것들을 가져다 주도록 Service -> DAO로 전달 전달. DAO가 가져와서 Service -> Controller 전달 전달.. 뷰까지 ㄱㄱㄱ

6. 검색 버튼 구현

자동완성 오른쪽에는 항상 수동으로 검색할 수 있도록 검색 버튼이 달려있습니다. 팝업이 떠서 검색하고 팝업이 닫힐 때 사용자 추가 화면에 자동으로 값을 넣어 주도록 만드는게 관건.

버튼 눌렀을 때 요청할 URL은 4번 단계에서 태그 파일에 설정해 두었으니 이제 해당 요청을 처리할 컨트롤러와 뷰 만들고 서비스 구현하면 끝.

<o:searchpage title="우편번호 검색">
    <o:buttons>
        <o:searchbtn />
    </o:buttons>
    <o:searchform action="selectgrid.do">
        <o:searchrow>
            <o:text label="읍, 면, 동, 리, 건물명" id="dong" size="30" maxlength="30" />
        </o:searchrow>
    </o:searchform>
</o:searchpage>
<%@ include file="/include/searchfooter.jsp" %>

이게 검색화면 끝.

<o:gridpage>
    <o:zipcodeselectgrid list="${list}">
        <o:col header="id" path="id" hidden="true" type="integer" pattern="#########"/>
        <o:col header="우편번호" path="zipcode" width="80"/>
        <o:col header="시, 도" path="sido" width="50"/>
        <o:col header="구, 군" path="gugun" width="160" />
        <o:col header="동, 면, 건물" path="dong" width="250" />
        <o:col header="번지" path="bunji" width="180" />
    </o:zipcodeselectgrid>
</o:gridpage>

이게 그리드 화면 끝.

    @RequestMapping
    public void popupsearch() {
    }

    @RequestMapping
    public ModelAndView selectgrid(HttpServletRequest req, HttpServletResponse res,
            ModelMap model, ZipcodeParams params, OrderPage orderPage, String view) throws Exception {
        ModelAndView mav = super.grid(req, res, model, params, orderPage, view);
        mav.setViewName("system/zipcode/selectgird");
        return mav;
    }

이게 요청 처리 및 서비스 구현 끝.

7. 완성



팝업으로 검색해서 선택하면 우편번호와 주소에 팍팍 입력되고 팝업 닫히는 것도 되지만 스샷은 생략~
저작자 표시
top


[스프링 3.0 @MVC] 컨트롤러에 스프링 AOP가 적용되지 않는다는건 이제 거짓말

모하니?/Coding : 2010/02/17 14:49


@MVC를 사용하면 좋은 점 중 하나가 스프링 AOP 적용이 쉽다는 겁니다. 아직도 여러 이유로 이전의 Controller 계층 구조를 이용해서 개발하시는 분들이 많겠지만 @MVC를 사용하시는 분들은 이점을 꼭 알고 계셔야 합니다. 스프링 @MVC도 이제 스프링 AOP가 아주 잘 먹힙니다.

@Controller
public class TestController {

    @RequestMapping("/test")
    @Transactional
    public void test() {
        System.out.println("hi!!!!!!!!!!!!!!!!!!!!!!");
    }

}

하지만 그렇다고 해서 이런 코드를 작성해 달라는 것은 아닙니다. 절대로.. 네버..

어찌됐든 위와 같은 코드도 동작하게 되어있는데 경우에 따라서는 안 될 수도 있습니다. 그런 경우는 스프링 트랜잭션이나 스프링 시큐리티 애노테이션에 문제가 있는것이 아니라 빈설정과 관련이 있을 수가 있으니 빈 설정을 잘 보셔야 합니다.

만약 위와 같은 경우라면.. @Controller 빈이 만들어지는 ApplicationContext와 tx:annotation 머시기가 등록되는 ApplicationContext가 같은지 확인해봐야 합니다.

정말,, 굳이.. 컨트롤러에 AOP를 적용해야 한다면 http://toby.epril.com/?p=934 이 글따라서 DS가 만드는 WebAC 하나로 모두 통합하는 것도 좋겠습니다.
top


[SVN] Spring Framework 3.0.0.RELEASE

모하니?/Coding : 2010/02/11 16:07


스프링 프레임워크 SVN: https://src.springframework.org/svn/spring-framework
스프링 프레임워크 3.0.0.RELEASE: tags/spring-framework-3.0.0.RELEASE


좋아 좋아. 인텔리J구나 역시...
top


[봄싹 DevTerms] 활성화 맞춤형 개발

모하니?/Coding : 2010/01/21 10:15



현재 봄싹 DevTerms는 오픈을 한지 얼마 안되서 인지 사용자가 몇 안되고 쌓인 데이터가 별로 없지만 장차 사용자도 늘어나고 데이터가 많아지면 다음과 같은 기능을 추가할 계획입니다. (어제 오늘 오픈 하고 난 뒤 생간난 아이디어들입니다.)

봄싹 DevTerms 트위터가 활성화 되면..
- 개발 용어 및 한글 용어가 올라갈 떄마다 트위터에 등록되는데, 만약 봄싹 DevTerms 트위터가 활성화 되어 올라온 메시지에 누군가 reply나 retweet등을 달면 해당 메시지들을 읽어와서 봄싹 DevTerms에서 보여줄 수 있겠습니다.

개발 용어가 많이 등록되면..
- 오늘 용어, 이번주 용어 탭을 추가하여 오늘 하루에 몇 개나 등록됐으며 어떤 단어들이 등록됐는지 볼 수 있게 하겠습니다. 그러나.. 아직은 뭐..

등록된 개발 용어가 2000개가 넘으면...
- 아이폰과 안드로이드폰 용 애플리케이션을 개발하겠습니다.

개발 용어가 빈번히 등록 또는 수정 된다면..
- 코멧을 적용하여 트위터처럼 실시간으로 새로 등록된 용어가 있음을 알려주고 페이지 업데이트를 하라는 메시지를 전달하거나 자동으로 페이지를 업데이트 해주겠습니다. 리버스 Ajax를 적용해야되서 기술적으로도 재밌을 것 같네요.

등록자 외에도 개발 용어를 편집하고 싶은 분들이 있다면..
- 위키 형태로 히스토리를 저장하고 누가 언제 수정했는지 기록을 남기겠습니다. 현재는 관리자 권한을 가진 사용자는 모든 용어를 편집할 수 있지만 히스토리를 남기고 있지 않아서 위키라고 볼 수는 없는데 아직 이 기능이 필요한 시점은 아닌 것 같습니다. 활성화가 된다면 모를까..

뷰 카운트 및 추천 카운트가 1000이 넘는다면..
- 현재 봄싹 막내(스루)가 미래를 위한 대비 작업으로 천천히 진행 중이긴 한데, StackOverFlow 처럼 카운트 단위를 도입해서 1000건이 넘는 조회수에는 k를 붙여서 1000v -> 1kv로 카운트 단위를 적용하겠습니다.

The more you use it, the more you will get.



top


2010 대한민국 매쉬업 참가했습니다.

모하니?/Coding : 2010/01/20 16:30


"개발 용어 한글화 프로젝트 DevTerms"로 참가했습니다. 이 녀석을 어제 배포하고 밤새 나머지 주요 기능 중 하나였던 '관심 용어'까지 구현한 뒤 배포하고 참가 신청까지 완료했습니다. 2010 매쉬업 참가 목록에서 보실 수 있습니다.


위에 보시면 아시겠지만 작년 11월부터 이미 DevTerms를 만들고 싶다는 이야기를 봄싹 그룹스에서 했었습니다. 그러던 중 매쉬업 소식을 듣게 되었고 DevTerms에 오픈 API를 적용해서 내보내게 된 겁니다. 1석 2조가 됐지요. 어차피 봄싹에 새 기능으로 추가할 계획이었고 그 기능에 필요한 오픈 API를 적용해서 매쉬업까지 나갔으니 말이죠.

그래서 그런지 제가 제출한 제품은 매쉬업이 메인이 아니라 '개발 용어 한글화' 서비스가 메인입니다. (벌써 43개나 제출됐네요.. 캬오.)그걸 보조하는 수단으로 사전 API와 트위터 API 사용하고 있죠. 사전 API는 기대 했던 것 보다 효과가 좋다고 생각하고 있습니다. 개발 용어는 영어 사전으로 검색한 결과를 보여주고 한글 용어는 한글 사전으로 검색한 결과를 보여주고 있습니다. 트위터 API를 이용하여 등록되는 용어들을 트위터 메시지로 등록해주고 링크를 타고 봄싹 DevTerms로 올 수 있게 해뒀습니다. 트위터 서비스만 해두면 사용자들이 트위터 RSS 피드나 노티 애플리케이션을 이용해서 등록되는 용어들을 실시간으로 받아 볼 수도 있지요.

http://twitter.com/devterms

사실 중간에 매쉬업 참가를 포기할까도 고민했습니다. 혼자서 디자인까지 신경쓰면서 기능 구현을 하자니 중간에 탁탁 끊기는 느낌이고 누군가 제가 집중해서 기능을 구현하고 있으면 그 동안 만든 페이지들 디자인을 점검하고 개선해주면 좋겠다 싶어서 봄싹 팀원에게 도움을 요청했습니다. 다행히 그 친구가 흔쾌히 승낙하고 휴가까지 반납하고 우리 회사로 찾아와 개발을 해주었기 때문에 일정과 품질을 다 기대했던 것 만큼 맞출 수 있었다고 생각합니다. 회사나 계약 등으로 묶여서 같이 일하는 것이 아니라 순수한 열정으로 묶여서 '함께 일한다'는 것이 무엇인지 조금은 알 수 있을 것 같은 기분입니다.

개발에 투자한 일수는 얼마 되지 않습니다. 1월 초까지도 번역을 하고 있었고 JSF를 공부하던 중에 갑자기 매쉬업이 생각나서 저번주부터 오늘까지 한 10일 정도 달린 것 같습니다. 하지만 투자한 시간은 거의 한달 정도 회사일에 투자한는 시간이었던 것 같습니다. 아침 10시부터 새벽 5시까지 전철 타고 밥먹는 시간 뺴고 코딩만 해본건 처음이었습니다. @_@ (어제와 오늘은 출근 길에도 전철에서 코딩을 했습니다. 아흑.. '쟤 모야... '이런 사람들의 시선도 무시한 채 꾿꾿히..) 이렇게 빠져버린 날 이해해준 아내에게 그저 고맙고 미안할 뿐입니다.

원래 계획은 1월 20일. 오늘부터 다시 번역에 집중해서 2월 달 안에 번역을 마무리하는게 계획이었습니다. 그런데 왠걸.. 24일까지 수정할 수 있게 해준다는군요. 뭐 기존에 생각했던 기능들은 다 만들었지만 막상 써보니까 생기는 요구사항들을 무시할 순 없을 것 같습니다. 24일까지만 손을 대고 후딱 번역을 마무리 한 다음 3월부터 진정한 2010년을 맞이해야겠습니다. 후아...

결과가 어찌될지 몰겠지만 수고했다. 기선아. 조금만 더 하고 마무리하자.





top


개발 용어 한글화 프로젝트 "DevTerms"

모하니?/Coding : 2010/01/19 19:34


봄싹에서 진행하는 프로젝트 중 하나입니다. 저번 주부터 이녀석 개발하느라 번역도 안하고 회사일도 미루고 잠도 줄이면서 올인했습니다. 아직도 좀 더 만들어야 하는데 일단 사용할 수 있을 정도가 됐다는 판단하에 배포했습니다. (그런데 오판이었어요. 고치고 만들께 아직도 많네요. @_@)

이 프로젝트의 목적은 다음과 같습니다.

- 공동 역자들의 용어 통일화
- 개발 용어에 대한 이해
- 개발 용어 한글화에 대한 근거 확보
- 다수가 선호하는 한글 용어 식별
- 개발 용어 태그 정리

http://springsprout.org/term/index.do



사용해 보시고 의견 주세요~




top


[웹 사이트 속도 향상 베스트 프랙티스 10] 자바스크립트 CSS 크기 줄이기

모하니?/Coding : 2009/12/19 12:53


http://developer.yahoo.com/performance/rules.html#minify

Minify JavaScript and CSS

tag: javascript, css

크기줄이기는 로드 타임을 향상시키기 위해 코드에서 불필요한 문자를 제거하여 그 파일 크기를 줄이는 방법이다. 코드를 최소화하면 모든 주석이 제거되고 불필요한 공백(스페이스, 새줄, 탭)등이 제거된다. 그렇게 하면 자바스크립트의 경우 다운로드 할 파일 크기가 줄어들어 응답 시간 퍼포먼스를 향상시켜준다. 자바스크립트 코드를 최소화하는데 사용하는 유명한 도구 두개로 JSMin과 YUI 압축기(Compressor)가 있다. YUI 압축기는 CSS도 최소화해준다.

Obfuscation은 소스 코드에 적용할 수 있는 최적화 대체기술이다. 이것은 최소화보다 복잡하고 obfuscation 과정 자체에서 버그를 만들어 낼 가능성이 더 많다. 미국 웹 사이트 탑 10을 조사한 결과 최소화는 21% 정도 크기를 줄인 반면 obfuscation은 25%를 줄였다. 비록 obfuscation이 더 많으 크기를 줄였지만 자바스크립트 최소화를 사용하는 것이 덜 위험하다.

외부의 스크립트와 스타일시트를 최소화하는 것과 더불어 인라인 <script>와 <style> 블럭도 최소화 해야 하며 그렇게 할 수 있다.  gzip으로 스크립트와 스타일 시트를 압축하더라도, 그것들을 최소화하는 것이 5% 이상 크기를 줄일 수 있다. 자바스크립트와 CSS 사용과 크기가 증가하면서 코드 최소화로 얻을 수 있는 것도 많아지고 있다.

ps: 구글에서도 뭔가 압축기 나왔담서요?? 
top


[스프링 테스트 확장] static member class를 빈으로 등록하는 테스트 로더 1

모하니?/Coding : 2009/12/18 13:19


테스트를 편하게 하기위한 기발한 아이디어를 읽는 도중 조금 맘에 안 드는 부분이 있었는데.. 그건 바로 ApplicationContext를 직접 생성하는 부분이었습니다. 게다가 원래는 @Configuration이 붙어있는 클래스를 넘겨받기로 되어있는 API에 static membe class를 넣어 빈으로 만들었습니다. 굉장히 기발하지만 약간 해킹스러운 방식이고 불편한 점들이 보였습니다. 빈으로 등록할 static member class가 많아지기라도 하다면.. 흠..

그래서 static member class를 빈으로 등록하겠다는 명시적인 이름을 가지고 있는 테스트 컨텍트스 로더를 만들기로 했습니다. 몇일전에 만들었던 애노테이션 설정 컨텍스트 로더를 조금만 고치면 가능할 것 같아서 시도했습니다.

그 결과 다음과 같이 테스트를 작성할 수 있게됐습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = StaticMemberClassLoader.class)
public class StaticInnerConfigTest {

    @Autowired ApplicationContext ac;

    static class Hello {
        @Inject Printer printer;
    }

    static class Printer {
    }

    class Foo {}

    @Test
    public void inject() {
        for(String beanName : ac.getBeanDefinitionNames()){
            System.out.println(beanName);
        }

        Hello hello = ac.getBean(Hello.class);
        assertThat(hello.printer, is(notNullValue()));
    }
}

사부님이 작성한것과 비교하자면, 우선 ApplicationContext를 직접 만들지 않아도 알아서 static member class를 빈으로 만들어주며, static member class를 ContextConfigruation으로 쓰겠다고 로더 설정이 되어있으니 좀 더 명시적이라고 할 수 있겠습니다. 음헤헷;


콘솔에는 위와 같이 찍힙니다. StaticMemberClassLoader를 확장하거나 수정하여 빈을 등록할 때 빈 이름이나, 등록할 빈을 골라내는 작업을 얼마든지 변경할 수도 있습니다.

사부님 덕분에 아침부터 잼난 코딩을 했더니 기분이좋습니다. 참고로 이 로더 구현체에는 문제가 좀 많습니다. 무슨 문제가 있는지 지적해주시는 분이 계실지 모르겠지만 퀴즈 삼아 문제가 많은 코드를 공유하고 싶어졌습니다. 발코딩을 까고 싶으신 분들에게는 절호의 기회입니다. 마음껏 까주세요!

public class StaticMemberClassLoader extends AnnotationContextLoader {

    @Override
    protected void createCustomBean(DefaultListableBeanFactory context, String[] locations) throws ClassNotFoundException {
        for(String string : locations){
            Class beanClass = ClassUtils.forName(string, getClass().getClassLoader());
            context.registerBeanDefinition(beanClass.getSimpleName(), BeanDefinitionBuilder.rootBeanDefinition(beanClass).getBeanDefinition());
        }
    }

    @Override
    protected String[] generateDefaultLocations(Class<?> clazz) {
        Class[] classes = clazz.getDeclaredClasses();
        List<String> modifiedLocations = new ArrayList<String>();
        for (int i = 0; i < classes.length; i++) {
            Class klass = classes[i];
            if(klass.getModifiers() == Modifier.STATIC)
                modifiedLocations.add(klass.getName());
        }
        return modifiedLocations.toArray(new String[modifiedLocations.size()]);
    }

}


top


ProxyFactoryBean을 이용한 초간단 AOP 구현

모하니?/Coding : 2009/12/16 16:01


스프링 AOP가 어렵다고 생각하시는 분들은 저 클래스를 사용하는 방법부터 익히시면 도움이 될 것 같습니다.
아주 간략하게 ProxyFactoryBean을 사용해서 AOP를 적용해 보겠습니다.

AOP는 OOP 같은 프로그래밍 기법이지 무슨 기술이 아닙니다. 따라서 이 글에서 구현하는 내용은 AOP 적용 방법 중 하나라고 생각하시면 됩니다.

    interface Service {
        public void hi();
        public void hi2();
    }

이런 인터페이스가 있고

    class ServiceImpl implements Service {
        public void hi(){
            System.out.println("my business");
        }

        @Override
        public void hi2() {
            System.out.println("my2");
        }
    }

이런 구현체가 있을 때. hi() 호출 전 후에만 메시지를 출력하고 hi2()는 걍 저대로 출력하고 싶다면 ...  우선 해당 작업을 수행할 MethodIterceptor 인터페이스의 구현체를 만듭니다.

    class ServiceLoggingAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            System.out.println("before hi");
            invocation.proceed();
            System.out.println("after hi");
            return null; 
        }
    }

그리고 hi()와 hi2()를 선별해줄 NameMatchMethodPointcut을 만듭니다.

    class ServiceLoggingPointcur extends NameMatchMethodPointcut {

        ServiceLoggingPointcur() {
            setMappedName("hi");
        }
    }

이제 마지막으로 ProxyFactoryBean으로 위에서 만들었던 타겟(Service 구현체), 어드바이스(MethodInterceptor 구현체), 포인트컷(NameMatchMethodPointcut 구현체)을 ProxyFactoryBean에 설정 해주면 끝!

    Service service;
    ProxyFactoryBean proxyFactoryBean;

    @Before
    public void setUp(){
        proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(new ServiceImpl());
        proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(
                new ServiceLoggingPointcur(),
                new ServiceLoggingAdvice()));
    }

    @Test
    public void logging(){
        Service service = (Service) proxyFactoryBean.getObject();
        service.hi();
        service.hi2();
    }

이렇게 하면 콘솔에


요런식으로 출력됩니다.

자세한 내용은 언젠간 출간될 Toby님의 스프링 책을 참조하세요.




top


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 4 - 일단 끝

모하니?/Coding : 2009/12/16 13:53


/**
 * @author Keesun Baik(Whiteship)
 * @author Seongyoon Kim(Is윤군)
 */
public class AnnotationContextLoader extends AbstractContextLoader {

    private static final String JAVA_FILE_SUFFIX = ".java";
    private static final String APP_CONFIG_FILE_PREFIX = "AppConfig";

    @Override
    public String getResourceSuffix() {
        return APP_CONFIG_FILE_PREFIX + JAVA_FILE_SUFFIX;
    }

    @Override
    protected String[] generateDefaultLocations(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        String suffix = getResourceSuffix();
        Assert.hasText(suffix, "Resource suffix must not be empty");
        return new String[] { clazz.getName() + suffix };
    }

    @Override
    protected String[] modifyLocations(Class<?> clazz, String... locations) {
        String[] modifiedLocations = new String[locations.length];
        for (int i = 0; i < locations.length; i++) {
            String path = locations[i];

            if(path.endsWith("/"))
                path = path.substring(0, path.length() - 1);

            if (path.startsWith("/")) {
                modifiedLocations[i] = ClassUtils.convertResourcePathToClassName(path.substring(1));
            }
            else if (!ResourcePatternUtils.isUrl(path)) {
                modifiedLocations[i] = getClassName(clazz, path);
            }
            else {
                throw new UnsupportedOperationException();
            }
        }
        return modifiedLocations;
    }

    private String getClassName(Class clazz, String path) {
        return ClassUtils.convertResourcePathToClassName(
            StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(clazz) + "/" + path));
    }

    public final ConfigurableApplicationContext loadContext(String... locations) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        prepareContext(context);
        customizeBeanFactory(context.getDefaultListableBeanFactory());
        context.register(getAppConfigClasses(context.getClassLoader(), locations));
        context.scan(getAppConfigPackages(context.getClassLoader(), locations));
        AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
        context.refresh();
        context.registerShutdownHook();
        return context;
    }

    private String[] getAppConfigPackages(ClassLoader classLoader, String[] locations) {
        List<String> packages = new ArrayList<String>();
        for(String location : locations){
            if(!location.contains(JAVA_FILE_SUFFIX))
                packages.add(location);
        }
        return packages.toArray(new String[packages.size()]);
    }

    private Class<?>[] getAppConfigClasses(ClassLoader classLoader, String[] locations) throws ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<Class<?>>();
        for(String location : locations){
            if(location.contains(JAVA_FILE_SUFFIX))
                classes.add(ClassUtils.forName(location.replace(JAVA_FILE_SUFFIX, ""), classLoader));
        }
        return classes.toArray(new Class<?>[classes.size()]);
    }

    protected void prepareContext(GenericApplicationContext context) {
    }
    protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
    }
    protected void customizeContext(GenericApplicationContext context) {
    }

이걸 사용하시면 됩니다. 이제 스프링 이슈에 올려야지. 캬캬캬
스프링 레퍼런스 공부할 때 오타 찾아서 이슈 등록하던게 엊그제 같은데 이젠 코드도 기여할 수 있으려나..

코드를 보시면 아시겠지만, classpath:, file:, url: 등의 prefix 지원은 포기했습니다. 그래도..

1. 스프링 애노테이션 설정 파일만 가지고 쉽게 테스트 할 수 있으며
2. 스프링 애노테이션 설정 파일을 명시적으로 설정할 수 있으며
3. 임의의 패키지를 명시적으로 설정할 수 있습니다.

딱 제가 원하던 만큼이니 이정도면 저는 만족합니다. 이걸 스프링에서 고쳐서 넣어주던 말던~ 일단은 try.

ps: 같이 코딩해주고 상의해준 성윤군과, 스프링 러너와 로더 설정 방법 알려주신 사부님 썡큐!

top


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 3

모하니?/Coding : 2009/12/15 19:01


하악 하악.. 배가 너무 고파요. 언능 집에가서 집안일 도와줘야 하는데;; 오늘 너무 늦게 들어가서 와이프 고생할까봐 마음이 아프네요. ㅠ.ㅠ 미안해 내가 코딩을 못해서.. 너무 늦게 들어가게 생겼어;; 나머진 내일 해야지;; 후딱 갈께;; (이것만 쓰고;;)

public class AnnotationContextLoaderTest {

    AnnotationContextLoader acl = new AnnotationContextLoader();

    @Test
    public void generateDefaultLocations(){
        String[] result = acl.generateDefaultLocations(SpringAnnotationConfigTest.class);
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));

    }

    @Test
    public void modifyLocations(){
        // absolute location
        String[] result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample/SpringAnnotationConfigTestAppConfig.java");
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample/");
        assertThat(result[0], is("sandbox.springtest.sample"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "/sandbox/springtest/sample");
        assertThat(result[0], is("sandbox.springtest.sample"));
        // relative location
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "./SpringAnnotationConfigTestAppConfig.java");
        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "./");
        assertThat(result[0], is("sandbox.springtest.sample"));
        result = acl.modifyLocations(SpringAnnotationConfigTest.class, ".");
        assertThat(result[0], is("sandbox.springtest.sample"));
        // classpath: prefix
//        result = acl.modifyLocations(SpringAnnotationConfigTest.class, "classpath:./SpringAnnotationConfigTestAppConfig.java");
//        assertThat(result[0], is("sandbox.springtest.sample.SpringAnnotationConfigTestAppConfig.java"));
    }

}


경로 가져오는 부분이 복잡하므로, TDD로 테스트! 이런건 테스트 먼저 만들기도 좀 쉬운편이죠.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample/SpringAnnotationConfigTestAppConfig.java"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample/"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"/sandbox/springtest/sample"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"./SpringAnnotationConfigTestAppConfig.java"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"./"})
//@ContextConfiguration(loader = AnnotationContextLoader.class, locations = {"."})
//@ContextConfiguration(loader = AnnotationContextLoader.class)
public class SpringAnnotationConfigTest {

    @Autowired ApplicationContext ac;

    @Test
    public void di(){
        assertNotNull(ac);
        String name = ac.getBean("name", String.class);
        assertNotNull(name);
    }

}

현재 구현한 것으로 사용할 수 있는 방법을 나열해 봤습니다. 주석을 바꿔가면서 테스트 해볼 수 있지요;; 물론 이미 테스트에서 경로 확인은 했지만, 그 뒤에 벌어지는 일에 대해서는 저걸로 통합 테스트;;

구현체는 비공개!!

prefix 붙이는 부분만 처리되면 공개할지도?? 말지도??

top


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 2

모하니?/Coding : 2009/12/15 17:35


가장 먼저 떠올랐던 기본적인 시나리오는 다음과 같습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationContextLoader.class)
public class SpringAnnotationConfigTest {

    @Autowired ApplicationContext ac;

    @Test
    public void di(){
        assertNotNull(ac);
        String name = ac.getBean("name", String.class);
        assertNotNull(name);
    }

}

이런 테스트가 있고, @ContextConfigruation에 아무런 locations를 지정하지 않았을 때는 테스트 클래스 이름 + AppConfig.java 파일을 찾아서 해당 클래스를 애노테이션 설정 클래스로 인식하는 겁니다.

@Configuration
public class SpringAnnotationConfigTestAppConfig {

    @Bean
    public String name(){
        return "keesun";
    }

}

즉 위와같은 애노테이션 설정 클래스를 기본 설정으로 인식하는거죠. 물론 AppConfig라는 이름을 다른 이름으로 변경할 수 있도록 확장성을 고려해야겠습니다.

그 다음 시나리오는 조금 복잡한데, @ContextConfiguration에 AppConfig 같은 @Configuration을 사용한 자바 설정 파일과 springsprout.org.config 같은 패키지 명을 설정할 수 있게 하는 겁니다.

@ContextConfiguration(locations = {"AppConfig.java", "classpath:../"})

@ContextConfiguration(locations = {"classpath:./AppConfig.java", "../"})

.java로 끝나는 location 정보는 AnnotationConfigApplicationContext의 register를 이용하며, .java로 끝나지 않는 location 정보는 패키지로 인식하여 AnnotationConfigApplicationContext의 scan을 이용해주는 겁니다. 물론, 각각의 resource 정보는 스프링의 Resource prefix인 classpath:, file, url:을 이용할 수 있어야겠습니다.

이제 구현 ㄱㄱㅆ

top


[스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 1

모하니?/Coding : 2009/12/15 17:17


스프링 3.0 @Configuration에 대한 학습 테스트를 만들다 제일 먼저 찾게 된 것이 애노테이션 설정 기반 테스트 러너입니다. 스프링 XML 설정 파일 말고 애노테이션 설정 자바 파일만 주면 되는게.. 있을 줄 알았습니다.

하지만 없었습니다. 그래서 만들기로 마음 먹었습니다. 그리고 잘 만들어지면 스프링 이슈에 올려서 소스 코드를 기증할 생각도 가지고 있습니다.(받아 준다면 말이죠.ㅋ)

일단 처음 할 일은 적당한 확장 지점을 찾는 일인데, 이게 아주 재밌있습니다. 어제 사부님이 올려주신 주옥같은 글에서 스프링 내부에서의 DI 활용이 얼마나 멋진가를 보여주셨는데, 테스트 쪽에서도 그런 모습을 볼 수 있습니다.


스프링 최신 코드(3.0 RC3) 기준이기는 한데 2.5에 추가된 것들이고 그 후에는 변화가 없었습니다. 제가 만들려는 기능과 관련이 있는 클래스만 보고 있습니다. 소스 분석까지 쓰면 너무 길어지고 힘들기 때문에;; 그냥 바로 결론으로 넘어가겠습니다.

1. 처음에는 AGCL 화장을 시도했지만, AnnotationConfigApplicationContext가 내부에서 사용하고 있는 AnnotatedBeanDefinitionReader와 Scanner는 스프링의 BeanDefinitionReader 인터페이스를 따르고 있지 않아서 그럴 수가 없었습니다.

2. 한단계 올라가 AbstractContextLoader를 확장하기로 했습니다.

흠.. 다음은 시나리오를 좀 더 구체적으로 생각해봐야겠습니다.




top


스프링이 컴포넌트 스캔을 할 때 클래스로딩을 하지 않는다는 증거

모하니?/Coding : 2009/12/09 13:35


참조: InsideSpring (4) 빈 스캐너는 클래스를 로딩할까?

사부님의 주옥같은 글을 보다가.. 마지막 쯤에 클래스로딩이 되는지 안되는지 쉽게 확인한 방법이 안 떠오르신다고 해서;; 순간 생각난 원초적인 방법으로 실험해 봤습니다.

@Component
@Scope(value = "prototype")
public class Bean {

    static {
        System.out.println("I am loaded!");
    }
}

초간단 빈을 하나 만들고, 클래스가 로딩 될 때 실행되도록 static 블럭에 메시지를 출력합니다. 빈 스코프가 중요한데, 기본 스코프(singletone)이면 ApplicationContext를 만들 때 미리 인스턴스를 만들어 버리기 때문에, 만들다가 저 클래스를 로딩 하게되죠. 대충 로깅 메시지를 보고서 이미 컴포넌트 스캔이 끝난 뒤에 만들겠거니... 짐작할 수는 있지만 더 명시적으로 확인할 수 있게 애초에 prototype 스코프로 만듭니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class ClassLoadingTest {

    @Autowired ApplicationContext ac;
   
    @Test
    public void asm(){
        assertThat(ac, is(notNullValue()));
        for(String beanName : ac.getBeanDefinitionNames()){
            System.out.println(beanName);
        }
    }

}

그리고 초간단 테스트! 와 빈설정은.. 같은 패키지에 ClassLoadingTest-context.xml 이라는 이름으로

    <context:component-scan base-package="sandbox.asm" />

이렇게 달랑 한줄.

결과는..


반대로.. 리플렉션을 이용하면 어떻게 되는지 확인해봅니다.

역시;; 초간단 테스트 추가

    @Test
    public void reflection() throws ClassNotFoundException {
        Class bean = Class.forName("sandbox.asm.Bean");
        assertThat(bean, is(notNullValue()));
    }

메시지 확인


캬캬캬.. 스프링은 역시 좀 짱이구나;


top


조금 친절한 코드 생성기 7(일단 완성) - DAO 생성기 코딩

모하니?/Coding : 2009/12/04 16:15


이전 글에서 예고한데로, 테스트 코드 부터 작성했습니다.

    @Test
    public void generateDao(){
        RepositorySettings settings = new FreemarkerRepositorySettings("test", "repository.ftl", "repository_impl.ftl", "test/springsprout/modules", Study.class);
        service.generateRepository(settings);

        assertTrue(new File("test/springsprout/modules/test/StudyRepository.java").exists());
        assertTrue(new File("test/springsprout/modules/test/StudyRepositoryImpl.java").exists());
        service.deleteRepository();
        assertFalse(new File("test/springsprout/modules/test/StudyRepository.java").exists());
        assertFalse(new File("test/springsprout/modules/test/StudyRepositoryImpl.java").exists());
    }


그리고 컴파일 에러를 없애기 위해서 필요한 인터페이스, 클래스, 메서드들을 추가했습니다. 먼저, RepositorySettings를 만들었습니다.

public interface RepositorySettings {
   
}

이 구현체인 FreemarkerRepositorySettings는 다음과 같습니다.

public class FreemarkerRepositorySettings implements RepositorySettings {

    private String module;
    private String interfaceTemplateName;
    private String implTemplateName;
    private String destinationDirPath;
    private Class domainClass;

    private Map<String, String> modelMap;
    private List<File> destinationDirs;
    private File interfaceFile;
    private File implFile;

    public FreemarkerRepositorySettings(String module, String interfaceTemplateName, String implTemplateName, String destinationDirPath, Class domainClass) {
        this.module = module;
        this.interfaceTemplateName = interfaceTemplateName;
        this.implTemplateName = implTemplateName;
        this.destinationDirPath = destinationDirPath;
        this.domainClass = domainClass;

        String domainClassName = domainClass.getSimpleName();
        modelMap = new HashMap<String, String>();
        modelMap.put("module", module);
        modelMap.put("domainClass", domainClassName);

        interfaceFile = new File(destinationDirPath + "/" + module + "/" + domainClassName + "Repository.java");
        implFile = new File(destinationDirPath + "/" + module + "/" + domainClassName + "RepositoryImpl.java");

        destinationDirs = new ArrayList<File>();
        destinationDirs.add(new File(destinationDirPath));
        destinationDirs.add(new File(destinationDirPath + "/" + module));
    }

...

}

이전에 만들었던 FreemarkerControllerSettings와 비슷하지만, 살짝 다릅니다. 그래도 비슷한 코드가 많으니.. 어떻게 중복을 좀 제거할 방법을 생각해 봐야겠습니다. 일단은 계속 ㄱㄱ

이제는 CodeGenerationService 인터페이스에 메서드를 추가할 차례로군요.

public interface CodeGenerationService {

    void generateController(ControllerSettings settings) throws CodeGenerationException;

    void generateRepository(RepositorySettings settings) throws CodeGenerationException;

}

Settins를 만들 때 빼고는 여태까진 편했습니다. 아직까지는 테스트를 돌려도 분명히 에러가 날 것이기 때문에 별로 돌려보고 싶지도 않습니다.

이제 본격적으로 프리마커 코드 생성기에 위 인터페이스 구현체를 만들겠습니다.

    public void generateRepository(RepositorySettings settings) throws CodeGenerationException {
        generatedFiles = new Stack<File>();
        FreemarkerRepositorySettings frSettings = (FreemarkerRepositorySettings)settings;
        generateDirs(frSettings.getDestinationDirs());
        generateCode(frSettings.getInterfaceTemplateName(), frSettings.getModelMap(), frSettings.getInterfaceFile());
        generateCode(frSettings.getImplTemplateName(), frSettings.getModelMap(), frSettings.getImplFile());
    }

어머나.. 끝이로군요!! 이전 코드를 리팩토링한 효과가 있었습니다. 원래는 이 부분도 굉장히 장황해질뻔 했는데, 다행입니다. 이제 테스트를 돌려볼까요.


로깅은 귀찮아서 Sout으로 해결했습니다.

만들때
삭제 대상
삭제된 후

조금 친절한 코드 생성기는 그럼 여기서 끝!!








top


조금 친절한 코드 생성기 6 - 테스트 코드 리팩토링

모하니?/Coding : 2009/12/04 16:06


이번에는 테스트 코드부터 만들기로 마음 먹었습니다. 그랬더니, 테스트 코드에서도 리팩토링 할 부분이 있어서 수정했습니다.

일단 프리마커 Configuration을 여러 테스트에서 공유해야 하기 때문에, 인스턴스 변수로 선언했고, @Before를 이용해서 모든 테스트 마다 새로 만든 Configuration 객체를 사용하게 했습니다. 사실 그럴 필요까진 없는데 말이죠;

public class FreemarkerCodeGenerationServiceTest {

    FreemarkerCodeGenerationService service;

    @Before
    public void setUp() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        service = new FreemarkerCodeGenerationService(configuration);
    }

    @Test
    public void generateController() throws IOException {
        service.generateController(new FreemarkerControllerSetting("test", "controller.ftl", "test/springsprout/modules", Study.class));

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }

    //TODO template file loading fail test

    //TODO destination file make fail test

    //TODO template processing fail test

    //TODO destination folder mkdir test
}

자.. 이제는 정말로;;
top


조금 친절한 코드 생성기 5 - 리팩토링

모하니?/Coding : 2009/12/04 15:40


이번에도;; 새 기능을 추가하려다보니까 기존의 코드에서 분리 시켜야 새 메서드에서 재사용 할 수 있는 부분들이 보이길래, 기존의 코드를 쪼개서 리팩토링 했습니다. 그러면서 일부 기능은 프리마커 코드 생성기에서 컨트롤러 설정 부분으로 이동시켰습니다. 아무래도, 최종적으로 생성해야 할 장소와, 파일에 대한 정보는 설정 객체가 받은 기본 정보를 토대로 가공해서 코드 생성기 쪽으로 전달해주는게 나을 것 같아서요.

    public void generateController(ControllerSettings settings) throws CodeGenerationException {
        generatedFiles = new Stack<File>();
        FreemarkerControllerSetting fcSettings = (FreemarkerControllerSetting)settings;
        generateDirs(fcSettings.getDestinationDirs());
        generateCode(fcSettings.getTemplateFileName(), fcSettings.getModelMap(), fcSettings.getDestinationFile());
    }

이 부분이 이전 글에서 엄청나게 길었던, 코드 생성코드입니다.
1. 먼저 생성된 파일 정보를 저장해둘 컬렉션을 초기화 합니다.
2. 프리마커 컨트롤러 설정으로 타입을 변환하고
3. 최종 코드가 생성될 디렉토리를 만듭니다.
4. 최종 코드를 만듭니다.

3, 4 번이 핵심 기능인데, 이 기능들은 다른 코드 생성 메서드에서도 사용할 수 있어야 하기 때문에 기존 코드에서 분리했습니다.

그리고 기존 코드 생성기에 있던 코드 중에 일부를 컨트롤러 설정 쪽으로 옮겼습니다.

public class FreemarkerControllerSetting implements ControllerSettings {

    private String module;
    private String templateFileName;
    private String destinationDirPath;
    private Class domainClass;

    private Map<String, String> modelMap;
    private File destinationFile;
    private List<File> destinationDirs;

    public FreemarkerControllerSetting(String module, String templateFileName, String destinationDirPath, Class domainClass) {
        this.module = module;
        this.templateFileName = templateFileName;
        this.destinationDirPath = destinationDirPath;
        this.domainClass = domainClass;

        modelMap = new HashMap<String,  String>();
        String className = domainClass.getSimpleName();
        modelMap.put("module", module);
        modelMap.put("domainClass", className);
        modelMap.put("domainName", ClassUtils.getShortNameAsProperty(domainClass));

        destinationFile = new File(destinationDirPath + "/" + module + "/" + domainClass.getSimpleName() + "Controller.java");

        destinationDirs = new ArrayList<File>();
        destinationDirs.add(new File(destinationDirPath));
        destinationDirs.add(new File(destinationDirPath + "/" + module));
    }

    public String getModule() {
        return module;
    }
    public String getTemplateFileName() {
        return templateFileName;
    }
    public String getDestinationDirPath() {
        return destinationDirPath;
    }
    public Class getDomainClass() {
        return domainClass;
    }

    public Map<String, String> getModelMap() {
        return modelMap;
    }
    public File getDestinationFile() {
        return destinationFile;
    }
    public List<File> getDestinationDirs() {
        return destinationDirs;
    }
}

1. 프리마커 Template을 가공할 때 사용할 modelMap.
2. 코드가 될 파일.
3. 코드가 담길 폴더들(기본 목적 폴더 + 모듈 폴더)

아 그리고 은근슬쩍 수정한 것이 있는데, domainClass를 generateContoller() 호출시에 넘겨주지 않고, ControllerSettings에 포함시켰습니다. 이녀석도 생성시에 필요한 설정 정보에 해당하는 건데 왜 이전 설계 변경할 때 빼먹었는지 모르겠네요. @_@;;

자 이번엔 진짜로 새 기능을 만들어 볼까요;;

아차차.. 테스트는 잘 돌아갑니다. 리팩토링 중에 중간 중간 돌려서 제대로 돌아가는지 확인할 수 있어서 좋았습니다. 코드를 엄청 많이 바꿨는데도 맘이 놓이네요. 안심이에요.
top


조금 친절한 코드 생성기 4 - 설계 변경 적용(인터페이스)

모하니?/Coding : 2009/12/04 14:02


이번에는 ControllerSettings라는 마커 인터페이스를 도입하여 설계를 변경해보겠습니다. 이번에도 저는 TDD 프로가 아니라서;; 테스트 코드 부터 수정하진 못했습니다.. (흑흑.. 다음 부턴;; 테스트부터;???)

public interface CodeGenerationService {

    void generateController(ControllerSettings settings, Class domainClass) throws CodeGenerationException;

}

ControllerSettings 라는 인터페이스에는 아무것도 없습니다. 단순히 타입만 맞추가 위한 거죠.

그리고 FreemarkerControllerSettings 구현체를 만듭니다. 여기에 실제 프리마커 코드 생성기가 필요로 하는 인자값들이 들어갑니다. 이전 글에서 Map이 하던 역할을 이녀석이 하는거죠.

public class FreemarkerControllerSetting implements ControllerSettings {

    private String module;

    private String templateFileName;

    private String destinationDirPath;

    public FreemarkerControllerSetting(String module, String templateFileName, String destinationDirPath) {
        this.module = module;
        this.templateFileName = templateFileName;
        this.destinationDirPath = destinationDirPath;
    }

    public String getModule() {
        return module;
    }

    public String getTemplateFileName() {
        return templateFileName;
    }

    public String getDestinationDirPath() {
        return destinationDirPath;
    }
}

별도의 세터는 없어서 변경을 막고, 생성자를 이용해서 강제적으로 세 값 모두 받도록 했습니다. (이렇게 해도 널 체크는 하긴 해야겠지만... 아 이 귀찮은 널 체크... 하긴.. 어차피 뭔가가 null이면 어디선가 에러가 날테니 굳이 안해도 되겠네요. 캬캬캬)

그리고 이제 프리마커 코드 생성기 코드를 수정합니다.

public class FreemarkerCodeGenerationService implements CodeGenerationService {

    private Configuration configuration;
    private Stack<File> createdFilesWhileGenerateController;
    private Stack<File> createdFilesWhileGenerateDao;

    public FreemarkerCodeGenerationService(Configuration configuration){
        this.configuration = configuration;
    }

    public void generateController(ControllerSettings settings, Class domainClass) throws CodeGenerationException {
        FreemarkerControllerSetting fcSettings = (FreemarkerControllerSetting)settings;
        String module = fcSettings.getModule();
        String templateFileName = fcSettings.getTemplateFileName();
        String destinationDirName = fcSettings.getDestinationDirPath();

        Map<String, String> map = new HashMap<String,  String>();
        String className = domainClass.getSimpleName();
        map.put("module", module);
        map.put("domainClass", className);
        map.put("domainName", ClassUtils.getShortNameAsProperty(domainClass));

        createdFilesWhileGenerateController = new Stack<File>();

        Template controllerTemplate = null;
        try {
            controllerTemplate = configuration.getTemplate(templateFileName);
        } catch (IOException e) {
            throw new CodeGenerationException("template file loading fail with [" + templateFileName + "]", e);
        }

        File desticationFolder = new File(destinationDirName);
        boolean created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);
       
        desticationFolder = new File(destinationDirName + "/" + module);
        created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);

        File destinationFile = new File(destinationDirName + "/" + module + "/" + className + "Controller.java");
        FileWriter writer = null;

        try {
            writer = new FileWriter(destinationFile);
            controllerTemplate.process(map, writer);
            writer.flush();
            writer.close();
            System.out.println(destinationFile.getAbsolutePath()  + " created");
             createdFilesWhileGenerateController.push(destinationFile);
        } catch (IOException e) {
            throw new CodeGenerationException("destincation file creation fail", e);
        } catch (TemplateException e) {
            throw new CodeGenerationException("template processing fail", e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
            }
        }
    }

...

}

음.. 확실히 Map에서 꺼내올 때에 비해서 뭔가 편합니다. 대체 Map에 어떤 키 값으로 데이터들이 들어있는지 애매한데다, 자쳇해서 스펠링이라도 틀리면;; 아무래도 그냥 이 방법으로 가야겠습니다.

마지막으로 테스트를 해봅니다. 물론 약간 고쳐야 하죠.

public class FreemarkerCodeGenerationServiceTest {

    @Test
    public void generationTst() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        FreemarkerCodeGenerationService service = new FreemarkerCodeGenerationService(configuration);

        service.generateController(new FreemarkerControllerSetting("test", "controller.ftl", "test/springsprout/modules"), Study.class);

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }
}

잘 돌아갑니다. 그다지 여러 경우를 테스트하고 있지는 않지만 나름대로 최소한의 기능 보장은 해주기 때문에 안심하고 코딩을 계속할 수 있겠습니다.



현재 상태에서 커버리지를 색으로 표시해봤습니다. 몇가지 특수한 상황에 대한 테스트가 안 된 부분이 있는데 저 부분에 대한 테스트는 나중에 만들기로 하고 테스트 코드에 요약해 둡니다.

public class FreemarkerCodeGenerationServiceTest {

    @Test
    public void generationTst() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        FreemarkerCodeGenerationService service = new FreemarkerCodeGenerationService(configuration);

        service.generateController(new FreemarkerControllerSetting("test", "controller.ftl", "test/springsprout/modules"), Study.class);

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }
   
    //TODO template file loading fail test
   
    //TODO destination file make fail test
   
    //TODO template processing fail test
   
    //TODO destination folder mkdir test
}

자 이제서야;; 본격적으로 Dao 코드 생성 작업에 들어갈 수 있겠군요.

top


조금 친절한 코드 생성기 3 - 설계 변경 적용(Map)

모하니?/Coding : 2009/12/04 13:40


먼저 인터페이스를 변경합니다. (TDD 프로라면 테스트 코드부터 바꾸셨겠지만, 저는 TDD 아마추어라;;)

public interface CodeGenerationService {

    void generateController(Map<String, String> settings, Class domainClass) throws CodeGenerationException;

}


Map을 사용하도록 바꿨습니다. 그리고 구현체에서 에러가 날테니 에러를 따라가서(인텔리J에서는 에러나는 코드로 알아서 자동으로 이동해 줍니다. 저는 걍 눈뜨고 손들고 있으면 알아서 코딩할 곳으로 데려다주죠.ㅋ)

public class FreemarkerCodeGenerationService implements CodeGenerationService {

    private Configuration configuration;
    private Stack<File> createdFilesWhileGenerateController;
    private Stack<File> createdFilesWhileGenerateDao;

    public FreemarkerCodeGenerationService(Configuration configuration){
        this.configuration = configuration;
    }

    public void generateController(Map<String, String> settings, Class domainClass) throws CodeGenerationException {
        createdFilesWhileGenerateController = new Stack<File>();

        String module = settings.get("module");
        String templateFileName = settings.get("templateFileName");
        String destinationDirName = settings.get("destinationDirName");
       
        //TODO check not null

        Map<String, String> map = new HashMap<String,  String>();
        String className = domainClass.getSimpleName();
        map.put("module", module);
        map.put("domainClass", className);
        map.put("domainName", ClassUtils.getShortNameAsProperty(domainClass));

        Template controllerTemplate = null;
        try {
            controllerTemplate = configuration.getTemplate(templateFileName);
        } catch (IOException e) {
            throw new CodeGenerationException("template file loading fail with [" + templateFileName + "]", e);
        }

        File desticationFolder = new File(destinationDirName);
        boolean created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);
       
        desticationFolder = new File(destinationDirName + "/" + module);
        created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);

        File destinationFile = new File(destinationDirName + "/" + module + "/" + className + "Controller.java");
        FileWriter writer = null;

        try {
            writer = new FileWriter(destinationFile);
            controllerTemplate.process(map, writer);
            writer.flush();
            writer.close();
            System.out.println(destinationFile.getAbsolutePath()  + " created");
             createdFilesWhileGenerateController.push(destinationFile);
        } catch (IOException e) {
            throw new CodeGenerationException("destincation file creation fail", e);
        } catch (TemplateException e) {
            throw new CodeGenerationException("template processing fail", e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
            }
        }
    }

...

}

코드는 이런식으로 바꼈습니다. 생성자가 한결 깔끔해 졌으며 generateCotroller에서는 settings 맵에서 필요한 값들을 꺼내서 작업하고 있습니다.

문제는
1. Map에 필요한 데이터가 없으면 어쩐댜?
2. 클라이언트 입장에서 필요한 속성이 뭔지 모르겠다.. @_@

일단 계속해서 테스트 코드를 수정합니다.

public class FreemarkerCodeGenerationServiceTest {

    @Test
    public void generationTst() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        FreemarkerCodeGenerationService service = new FreemarkerCodeGenerationService(configuration);

        Map<String, String> settings = new HashMap<String, String>();
        settings.put("module", "test");
        settings.put("templateFileName", "controller.ftl");
        settings.put("destinationDirName", "test/springsprout/modules");

        service.generateController(settings, Study.class);

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }
}

맵을 만들어서 넘겨줘야 했기 때문에 약간 길어졌지만, 테스트는 무사히 통과했습니다. 그리 큰 설계 변경은 아니지만 이 테스트 덕분에 맘이 편히 할 수 있었네요. 이제는 3번째 방안으로 구현해 보겠습니다.
top


조금 친절한 코드 생성기 2 - 설계 변경

모하니?/Coding : 2009/12/04 13:30


흠.. 그렇다. 어제 코딩하고 나서부터 계속 뭔가가 찜찜했는데, 그 원인을 (다른 기능을 추가하려다 보니까) 이제서야 알 것 같다. 컨트롤러만 생성할 것이었다면, 이런 발견은 못하고 찜찜한채로 넘어갔곘지만, DAO를 생성하려다 보니 발견한 것 같다.

문제의 원인은 컨트롤러 코드 생성에 필요한 정보를 생성자에서 받았는다는 것이었다.

    public FreemarkerCodeGenerationService(Configuration configuration, String controllerTemplateName, String destinationDir){
        this.destinationDir = destinationDir;
        this.configuration = configuration;
        try {
            this.controllerTemplate = configuration.getTemplate(controllerTemplateName);
        } catch (IOException e) {
            throw new CodeGenerationException("template file loading fail with [" + controllerTemplateName + "]", e);
        }
    }

이것은 CodeGenerationService 인터페이스 구현체 중에 하나인 프리마커 구현체로, 그 생성자에서 컨트롤러 코드 생성에 필요한 정보(코드를 생성할 위치와, 템플릿 파일이름)를 받고 있었다. 이런식이라면 DAO 코드 생성에 필요한 정보도 이 생성자에서 받아와야 할텐데;;; 그건 좀 아닌 것 같다. 그리고 저 정보를 굳이 인스턴스 레벨로 들고 있을 이유가 없지 않은가;

또한 컨트롤러를 어떤 모듈 밑에 생성할지에 대한 정보는 컨트롤러를 생성하는 메서드 안에 들어있다. 이건 대체 무슨 일인가. 왜 컨트롤러 생성에 필요한 정보가 분산 됐을까? 왜 이렇게 코딩했는지 나도 모르겠다. 이런걸 보고 응집도가 떨어졌다고 하는 것은 아닐까? 아님 말고.

"컨트롤러 생성에 필요한 정보는 컨트롤러를 생성할 때 주자."

설계가 바뀌었다 하지만 걱정은 없다. 나에겐 테스트가 있으니까. 변경해보자. 그런데, 변경 하자고 마음 먹은 순간부터 고민 거리가 떠올랐다. 어떤 타입으로 정보를 전달 할까.

1. 전부 나열

generateController(String module, String destinationDirPath, String templateFileName, Class domainClass)

2. 맵으로 압축

generateController(Map settings, Class domainClass)

3. 여러 매개변수를 하나의 타입으로 압축

generateController(ControllerSettings settings, Class domainClass)

어떤 방법이 더 개발하기 편하고 확장에 용이하며 직관적일까? 쉽지 않은 고민이다. 직관적인걸로 따지자면 1번과 3번이다. 2번은 대체 무슨 설정 값들을 맵에 넣어줘야 하는지;; 나만 알고 있다.

그러나 확장하자니 일단 1번은 아니다. 프리마커 코드 생성이 아니라 다른 방법으로 코드 생성을 할 떄는 위의 설정 내용이 필요없거나 다른 설정 값이 필요할 수도 있겠다. 그런 경우에는 2번이 제일 유리하고, 3번의 ControllerSettings를 단순 마커 인터페이스로 사용한다면 역시 유연하다고 볼 수 있겠다.

그럼 3번이 일단은 가장 유력한 후보로 보인다. 그러나 매번 이 마커 인터페이스의 구현체를 만들어 CodeGeneration 구현체 마다 하나씩은 만들어 줘야 할 것이며, DaoSettings, ServiceSettings 처럼 인터페이스도 늘어날 것이다. 클래스와 인터페이스 파일을 만들기가 귀찮을 수도 있겠다.

2번과 3번 사이에서 많이 고민이 된다. 둘 다 해볼까?
top


조금 친절한 코드 생성기 1 - 구상

모하니?/Coding : 2009/12/04 12:23


어제는 컨트롤러 코드를 약간 불친절하게 생성해주는 코드 생성기를 만들었는데, 오늘은 DAO 코드를 조금 친절하게 생성해주는 기능을 추가해보겠습니다. CodeGeneration 인터페이스에 generateDao 메서드를 추가하고, 템플릿도 추가하면 될 것 같네요. 흠..

DAO 코드는 컨트롤러랑 다르게 CRUD 코드가 거~의 변할 일이 없고 추가될 일만 있죠. 게다가 필요로 하는 인터페이스도 하이버네이트 DAO의 경우에는 SessionFactory 정도가 다입니다. DAO에서 서비스를 참조할 일도 없으니 컨트롤러 보다는 조금 친절하게 코드 생성을 해줄 수 있겠군요.

자 그럼~ 후딱 해볼까요.
top


불친절한 코드 생성기 5(일단 끝) - 프리마커 기반 코드 생성기 만들기

모하니?/Coding : 2009/12/03 18:02


서비스 인터페이스를 만듭니다.
public interface CodeGenerationService {

    void generateController(String module, Class domainClass) throws CodeGenerationException;

}


일단은 컨트롤러만 생성할테니, 컨틀로러 생성 메서드만 만듭니다. 이때 만들어질 컨트롤러가 속할 module의 이름과 어떤 도메인 클래스에 대한 컨트롤러인지 알려줍니다.

문제가 생기면 RuntimeException을 던집니다. 자, 이제 이 인터페이스를 프리마커 기반으로 구현할 시간이군요.

public class FreemarkerCodeGenerationService implements CodeGenerationService {

    private Configuration configuration;
    private Template controllerTemplate;
    private String destinationDir;
    private Stack<File> createdFilesWhileGenerateController;

    public FreemarkerCodeGenerationService(Configuration configuration, String controllerTemplateName, String destinationDir){
        this.destinationDir = destinationDir;
        this.configuration = configuration;
        try {
            this.controllerTemplate = configuration.getTemplate(controllerTemplateName);
        } catch (IOException e) {
            throw new CodeGenerationException("template file loading fail with [" + controllerTemplateName + "]", e);
        }
    }

    public void generateController(String module, Class domainClass) throws CodeGenerationException {
        createdFilesWhileGenerateController = new Stack<File>();
       
        Map<String, String> map = new HashMap<String,  String>();
        String className = domainClass.getSimpleName();
        map.put("module", module);
        map.put("domainClass", className);
        map.put("domainName", ClassUtils.getShortNameAsProperty(domainClass));

        File desticationFolder = new File(destinationDir);
        boolean created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);
       
        desticationFolder = new File(destinationDir + "/" + module);
        created = desticationFolder.mkdir();
        if(created)
            createdFilesWhileGenerateController.push(desticationFolder);

        File destinationFile = new File(destinationDir + "/" + module + "/" + className + "Controller.java");
        FileWriter writer = null;

        try {
            writer = new FileWriter(destinationFile);
            controllerTemplate.process(map, writer);
            writer.flush();
            writer.close();
            System.out.println(destinationFile.getAbsolutePath()  + " created");
             createdFilesWhileGenerateController.push(destinationFile);
        } catch (IOException e) {
            throw new CodeGenerationException("destincation file creation fail", e);
        } catch (TemplateException e) {
            throw new CodeGenerationException("template processing fail", e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
            }
        }
    }

...

    public void deleteController() {
        while(!createdFilesWhileGenerateController.empty()){
            File file = createdFilesWhileGenerateController.pop();
            System.out.println(file.getAbsolutePath());
            boolean deleted = file.delete();
            if(deleted)
                System.out.println(file.getAbsolutePath() + " deleted");
            else
                System.out.println(file.getAbsolutePath() + " not deleted");
        }
    }
}

왠지 좀.. 부끄럽네요. 으흑;;

저 클래스에 필요한 속성(configuration 타입 객체, 템플릿 파일들이 들어있는 위치, 컨틀롤러 템플릿 파일)을 봄싹 프로젝트에 맞게 기본으로 가지고 있는 클래스를 하나 만듭니다.

public class SpringSproutCodeGenerationService extends FreemarkerCodeGenerationService {

    private static final String DESTINATION_DIR = "src/springsprout/modules";
    private static final String CONTROLLER_TEMPLATE_NAME = "controller.ftl";

    public SpringSproutCodeGenerationService(Configuration configuration) {
        super(configuration, CONTROLLER_TEMPLATE_NAME, DESTINATION_DIR);
    }

}

이제 테스트를 해봅니다.

public class SpringSproutCodeGenerationServiceTest {

    @Test
    public void generationTst() throws IOException {
        Configuration configuration = new Configuration();
        configuration.setObjectWrapper(new DefaultObjectWrapper());
        configuration.setDirectoryForTemplateLoading(new FileSystemResource("doc/template").getFile());

        assertNotNull(configuration);

        FreemarkerCodeGenerationService service = new SpringSproutCodeGenerationService(configuration);
        service.setDestinationDir("test/springsprout/modules");
        service.generateController("test", Study.class);

        assertTrue(new File("test/springsprout/modules/test/StudyController.java").exists());
        service.deleteController();
        assertFalse(new File("test/springsprout/modules/test/StudyController.java").exists());
    }
}

끝...




top


불친절한 코드 생성기 4 - 템플릿 만들기

모하니?/Coding : 2009/12/03 17:48


가장 단순한 컨트롤러 코드를 가져다가 군대 군대 코드를 끼워 넣을 지점에 프리마커 태그(?)로 표시를 합니다.

...
@Controller
@SessionAttributes("${domainName}")
public class ${domainClass}Controller {

    @Autowired ${domainClass}Service service;
    @Autowired ${domainClass}Validator validator;

    @RequestMapping(value="/${domainName}/list.do")
    public void list(Model model) throws ServletRequestBindingException {
        model.addAttribute("list",service.getAll());
    }

    @RequestMapping(value="/${domainName}/{id}.do")
    public String view(Model model, @PathVariable int id) {
        model.addAttribute(service.get(id));
        return "${domainName}/view";
    }

    @RequestMapping(value="/${domainName}/add.do", method=RequestMethod.GET)
    public void add(Model model) {
        model.addAttribute(new ${domainClass}());
    }
...


이런식입니다. 참 쉽죠?
- 자바 코드에서 최대한 제네릭하게 편집한 다음 프리마커 편집기로 가져오는 것이 좋겠습니다.
- 태그로 교체할 때는 replace 툴을 이용합시다.
- 친절한 코드 생성기를 만들 때는 템플릿을 어떻게 만드냐에 따라 코드 생성기와 모델의 복잡도가 달라질 겁니다.
- 저는 불친절한 코드 생성기를 만들고 있기 때문에 맘편히 쉽게 만들었습니다.(생성뒤 필요한 import는 알아서 하도록..ㅋ)


top


불친절한 코드 생성기 3 - Freemarker 학습 테스트

모하니?/Coding : 2009/12/03 15:19


참조: http://freemarker.org/docs/pgui_quickstart_all.html

프로젝트의 sandbox에 패키지를 하나 만들고, main 메서드로 학습 테스트를 작성할 클래스 하나와 프리마커 템플릿 하나를 만듭니다. 그리고 위 참조 링크에서 코드를 가져다가 살짝 바꿔서 테스트 해봅니다.

템플릿 파일은 매우 간단하게;;

${message}

main 메서드에 들어갈 코드는 위 링크에서 복사해서 가져온 다음에 File은 스프링의 ClassPathResource를 이용해서 바꾸고, 템플릿 파일 이름은 위에서 만든 템플릿 파일이름으로, Map에는 message에 들어가야 할 것만 넣어 봅니다.

public class SimpleExample {

    public static void main(String[] args) throws Exception {
        /* ------------------------------------------------------------------- */
        /* You should do this ONLY ONCE in the whole application life-cycle:   */

        /* Create and adjust the configuration */
        Configuration cfg = new Configuration();
        cfg.setObjectWrapper(new DefaultObjectWrapper());
        cfg.setDirectoryForTemplateLoading(new ClassPathResource("/sandbox/freemarker").getFile());

        /* ------------------------------------------------------------------- */
        /* You usually do these for many times in the application life-cycle:  */

        /* Get or create a template */
        Template temp = cfg.getTemplate("testTemplate.ftl");

        /* Create a data-model */
        Map root = new HashMap();
        root.put("message", "Hello Freemarker");

        /* Merge data-model with template */
        Writer out = new OutputStreamWriter(System.out);
        temp.process(root, out);
        out.flush();
    }
}

결과는 그냥 눈으로 확인합니다.

Hello Freemarker
Process finished with exit code 0

끝!

이제 Configuration, Template, Map의 관계를 알았으니 본격적으로 코드 생성기를 작성해 보겠습니다.

top


불친절한 코드 생성기 2 - 구상2

모하니?/Coding : 2009/12/03 14:38


아랫 글에 댓글이 달렸지만, 이 기능에 대해 성윤군과 논의를 하다가 IDE가 제공하는 코드 템플릿 기능에 대해 들었습니다. 아차.. 싶더군요. 그래서 생각을 해봤습니다.

IDE 템플릿 기능을 이용할 경우

장점
- 매우 간편하게 코드 생성을 할 수 있습니다. 단축키를 입력하면 코드가 좌르륵.. 생겨나겠죠.

단점
- IDE 별로 템플릿을 작성해 줘야 합니다. 현재 봄싹 개발자 중 20%는 인텔리J IDE를 사용하고 있습니다. 따라서 이클립스용과 인텔리J용 템플릿을 만들어둬야 합니다.
- 개발자가 사용하는 IDE 마다 템플릿을 등록해줘야 합니다. 템플릿 파일도 버전 관리에 포함시켜서 들고다니면 배포하는 방법은 간편하지만 등록은 수동으로 해줘야 합니다. 그건 이클립스 자체를 패키징
- 자동 생성할 파일이 여러개면 매번 파일 만들고 그 파일 돌아다니면서 코드 템플릿 생성 해야함

템플릿 생성 프레임워크를 이용하여 구현할 경우

장점
- 배포 방법 고려할 필요 없음. 소스 코드에 들어있으니 그냥 실행.
- 나중에 여러 파일을 한 방에 생성하는 것도 가능

단점
- 코딩 쬐끔 해야 함.
- 라이브러리 추가 해야 함.

결국은 그냥 코딩 쬐끔 하는 편으로 기울었습니다.
top


불친절한 코드 생성기 1 - 구상

모하니?/Coding : 2009/12/03 12:44




사실 좀 고민입니다. OSAF처럼 GenericController를 만들까. 그냥 저 코드를 찍어내주는 코드 생성기를 만들까. 어떤게 더 사용하기 편하고 확장하기 편할까? GenericControlle를 확장하기 좋게 잘 만들면 되겠지만, 클래스 만들 때 프레임워크 코드를 상속해야 한다는게 귀찮기도 하고, 그래서 어차피 그 부분을 찍어내는 코드 생성기가 필요해지는 상황입니다. 게다가 GenericController를 만들려면 GenericService 인터페이스까지도 있어야 할테니, 일은 더 커지겠죠; 이러다 배보다 배꼽이 커지겠다 싶어서...

일단은 코드 생성기부터 만들기로 마음 먹었습니다. 간단하게;; 위와 비슷한 코드를 그냥 찍어내 주고, 컴파일 에러가 나던 말던 알아서 고쳐서 쓰도록!!!

이름하야.. 불친절한 코드 생성기!


top


[웹 사이트 속도 향상 베스트 프랙티스 9] DNS 룩업 줄이기

모하니?/Coding : 2009/12/01 13:50


http://developer.yahoo.com/performance/rules.html#dns_lookups

Reduce DNS Lookups

tag: content

도메인 네임 시스템(DNS)는 호스트네임을 IP 주소로 맵핑한다. 마치 전화부에서 사람의 이름을 그 사람의 전화번호로 맵핑하는 것처럼. www.yahoo.com을 브라우저에 입력하면, 브라우저에 연결된 DNS 리졸버(resolver) 해당 서버의 IP 주소를 룩업한다. DNS는 비용이 따른다. DNS가 해당 호스트네임으로 IP 주소를 찾는데 보통 20~120 밀리초가 소요된다. 브라우저는 DNS가 해당하는 것을 찾을 때까지 아무것도 다운로드 할 수가 없다.

DNS 룩업을 캐시해서 성능을 높인다. 이 캐싱은 사용자의 ISP 또는 LAN(로컬 지역 네트워크)에서 관리하는 별도의 캐싱 서버에서 할 수 있지만 사용자의 개인 컴퓨터에서도 캐싱할 수 있다. DNS 정보는 운영체제(OS)의 DNS 캐시에 남게된다. (마이크로소프트 윈도우는 "DNS Client service가 있다.) 대부분의 브라우저는 OS와는 별도로 그들만의 캐시를 가지고 있다. 브라우저가 DNS 기록은 자신의 캐시에 유지하는 이상, 기록을 뒤지기 위해 OS를 귀찮게 하지는 않는다.

인터넷 익스플로러(IE)는 DNS 룩업을 기본적으로 DnsCacaheTimeout 레지스트리 설정에 명시한데로, 30분간 캐시한다. 파이어폭스는 network.dnsCacheExpiration 설정에 의해 1분간 DNS 룩업을 캐시한다.(Fasterfox는 이것을 1시간으로 변경한다.)

클라이언트의 DNS 캐시가 (브라우저와 OS에서 모두) 비게되면, DNS 룩업 수가 웹 페이지에 있는 유일한 호스트네임의(unique hostname) 갯수와 동일하게 된다. 페이지의 URL, 이미지, 스크립트 파일, 스타일시트, 플래스 객체 등등에 포함되어 있는 모든걸 말한다. 따라서 호스트 네임 수를 줄이면 DNS 룩업의 수도 줄어든다.

호스트네임 수를 줄이면 잠재적으로 한 페이지에 있는 구성요소를 동시에 다운로드 할 수 있는 양이 줄어들게 된다. DNS 룩업 수는 줄어들지만, 동시 다운로드가 줄어들어 응답 시간이 증가할 수 있다. 개인적인 견해로는 2개 이상으로 컴포넌트를 나누되, 4개 이상의 호스트네임을 사용하지 말자는 것이다. 그런식으로 DNS 룩업 줄이기와 병렬 다운로드 사이의 균형을 맞출 수 있다.
top




: 1 : 2 : 3 : 4 : 5 : ... 10 :