Whiteship's Note

'모하니?'에 해당되는 글 934건

  1. 2009.12.19 [웹 사이트 속도 향상 베스트 프랙티스 10] 자바스크립트 CSS 크기 줄이기 (2)
  2. 2009.12.18 [스프링 테스트 확장] static member class를 빈으로 등록하는 테스트 로더 1 (6)
  3. 2009.12.17 뱀이다~ 뱀이다~ 몸에 좋고 맛도 좋은 뱀이다~ (12)
  4. 2009.12.17 봄싹 Career Path (6)
  5. 2009.12.16 ProxyFactoryBean을 이용한 초간단 AOP 구현 (2)
  6. 2009.12.16 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 4 - 일단 끝 (4)
  7. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 3
  8. 2009.12.15 2010 매쉬업 경진대회 시작
  9. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 2 (2)
  10. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 1
  11. 2009.12.11 [빈둥 빈둥] 닌텐도Wii 4.2k V2 홈브류 설치 (30)
  12. 2009.12.09 스프링이 컴포넌트 스캔을 할 때 클래스로딩을 하지 않는다는 증거 (2)
  13. 2009.12.08 [봄싹] 하이버네이트와 자바 퍼시스턴스 베타리딩 합니다. (8)
  14. 2009.12.04 조금 친절한 코드 생성기 7(일단 완성) - DAO 생성기 코딩
  15. 2009.12.04 조금 친절한 코드 생성기 6 - 테스트 코드 리팩토링
  16. 2009.12.04 조금 친절한 코드 생성기 5 - 리팩토링
  17. 2009.12.04 조금 친절한 코드 생성기 4 - 설계 변경 적용(인터페이스)
  18. 2009.12.04 조금 친절한 코드 생성기 3 - 설계 변경 적용(Map)
  19. 2009.12.04 조금 친절한 코드 생성기 2 - 설계 변경
  20. 2009.12.04 조금 친절한 코드 생성기 1 - 구상
  21. 2009.12.04 와이프는 요리사 - 한방 삼계탕 (6)
  22. 2009.12.03 불친절한 코드 생성기 5(일단 끝) - 프리마커 기반 코드 생성기 만들기 (2)
  23. 2009.12.03 불친절한 코드 생성기 4 - 템플릿 만들기
  24. 2009.12.03 불친절한 코드 생성기 3 - Freemarker 학습 테스트
  25. 2009.12.03 불친절한 코드 생성기 2 - 구상2
  26. 2009.12.03 불친절한 코드 생성기 1 - 구상 (2)
  27. 2009.12.03 이번주 토요일 IBM dW "웹 개발 다반사" (2)
  28. 2009.12.01 [웹 사이트 속도 향상 베스트 프랙티스 9] DNS 룩업 줄이기
  29. 2009.12.01 난 월세가 좋은데; (6)
  30. 2009.11.30 [봄싹] 점진적인 개발

[웹 사이트 속도 향상 베스트 프랙티스 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


뱀이다~ 뱀이다~ 몸에 좋고 맛도 좋은 뱀이다~




보아뱀을 두르고도 자연스러운 저 표정. 참 여유로와 보이지 않나요? 후훗..

뱀이 목을 졸라주면 (뱀 기분이) 좋은거고, 입을 벌리면,, 별로 안 좋은거임; 혀를 낼름 거리는 건 냄새를 맡기 위해서 라네요. 저 뱀은 저를 별로 좋아하지 않았어요.ㅋㅋ
top

TAG , 헤이리

봄싹 Career Path

모하니?/Planning : 2009.12.17 15:19


저는 봄싹 대표가 아니라 운영진 중 한명으로써 봄싹 스터디를 어떻게 키워나갈지 항상 고민하고 있습니다. 사실 대표가 왜 필요한지도 잘 모르겠어요. 규모가 작아서 그런가.. 아무튼 성윤군과 정우형 등 팀원들이 잘 도와주고 있어서 별다른 어려움은 못 느끼고 있습니다.

어쨋거나, 그건 그렇고 봄싹 커리어 패스를 생각해봤습니다.


현재 스터디 모임을 벗어나 개발자 모임으로 성장했다고 생각합니다. 이 단계에서 아직 못 이룬 것이 있다면 "세미나"입니다. 요즘 날이 추워서;; 내년 봄에 지금까지 봄싹에서 진행한 스터디와 프로젝트 내용을 종합하여 세미나를 가질 생각입니다. 아직 구체적으로는 계획하진 않았지만 꼭 할 겁니다.

2009년은 개발자 모임으로 진화했다면 2010년에는 좀 더 새로운 모습으로 진화하는 한 해가 되었으면 하는 바램입니다. 현재 스프링 3.0 레퍼런스 번역과 스프링소스 블로그 번역은 자체적으로 진행 중이며, 프로 스프링 2.5 베타리딩과 하이버네이트 베타리딩을 맡아 진행 중입니다. 그 외에도 저술과 원서 번역 계획을 가지고는 있지만 역시 이것도 아직 구체적으로 계획된 것은 없습니다. 하지만 아마도.. 할 것 같습니다.

물론, 진화를 한다고 해서 이전에 하던 것들을 모두 중단하지는 않습니다. 개발자 모임으로 바뀌었지만, 최근까지 오리지널 스프링 스터디를 진행했으며, 계속해서 새로운 스터디 주제거리(하이버네이트, 자바스크립트, JSF 등)가 생겨나고 있고, 새로운 스터디 방식을 모색 중입니다.

2010년에는 어떤 모습으로 변할지 기대 됩니다.
하지만 아직은 그 단계를 뭐라고 불러야 할지 떠오르질 않네요.

머라고 해야하지? @_@ "수퍼 울트라 캡숑 모임?"
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


2010 매쉬업 경진대회 시작



http://mashupkorea.org/106

우왕 커피 마시자~

매쉬업도 나가볼까나~
top

TAG 매쉬업

[스프링 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


[빈둥 빈둥] 닌텐도Wii 4.2k V2 홈브류 설치



감기 때문에 집에서 쉬는데 공부도 안 되고(코가 맹맹 거리면 머리가 웅웅 거리면서 무언가에 집중하기가 힘들어집니다.) 게임기나 만지작 거려야지 결심했습니다. (-- );;;

그런데 왜들 그렇게 정보를 꼭꼭 숨겨놓고 사시는지... 닌텐도 Wii 4.2k V2로 아무리 구글링을 해도 공개된 정보를 찾을 수가 없었습니다. 하지만 아주 없는 건 아니었습니다. 결국은 찾았죠. 영문자료 한 개와 한글 자료 두 개. 한글 자료 두 개는 그나마 이미 지워지거나 옮겨진 것이고 검색엔진의 '저장된 페이지 보기' 기능을 이용해서 볼 수 있었습니다.

기본라인: http://sites.google.com/site/completesg/hacking-guide/korea-4_2
파판: http://74.125.155.132/search?q=cache:kwRwCgmmwvoJ:www.finalfantasia.com/fantasybbs/board.php%3Fbo_table%3Dntdbbs%26wr_id%3D45713+Wii+4.2k+v2+%ED%99%88%EB%B8%8C%EB%A5%98+%EB%B0%8F+%ED%95%98%EB%93%9C%EB%A1%9C%EB%8D%94+%EC%84%A4%EC%B9%98&cd=1&hl=ko&ct=clnk&gl=kr&client=firefox-a

이 두 개를 섞어서 설치했더니 동작했습니다.

1. 파판 링크의 첫 번째 단계인 "홈 브류 설치"를 그대로 따라합니다. 별 문제 없이 잘 됩니다.

2. 파판 링크의 두 번째 단계 "기타 설치"는 그대로 따라하면 Trucha Bug Restorer에서 문제가 생깁니다.

3. 거기서 부터는 기본 라인과 파판을 짬뽕하여 제 맘대로 설치했습니다.
(맨 마지막 Wad Manager로 usb loader gx를 설치하기 전까지 모든 설치는 홈 브류 채널에 들어가서 합니다. 즉, SD 카드로 부팅하지 않았습니다.)

3-1. 기본라인 1~7 단계를 진행합니다.
3-2. 기본라인 8~9 단계를 진행합니다.
3-3. 파판링크의 '기타 설치' 11 단계를 진행합니다.
3-4. 기본라인의 11-12 단계를 진행합니다.
3-5. 파판링크의 '기타 설치' 14~16 단계를 진행합니다. (이 때만 SD 카드로 들어갔는데;; 이것 마저도 그냥 BHC에서 해도 될 것 같기도;;;)

끝난것 같지만 이제 시작입니다;;

Usb loader 설정해줘야 게임이 돌아가지 그냥 두면 게임이 안 돌아가고 검은 화면만 나오더군요. 마치 해킹한 PSP에서 커펌 버전이 낮다고 안 돌아가는 게임들처럼 말이죠; 게다가 게임 구해야지; 외장하드는 포맷해야지 덩덩덩;;

http://kws1070.tistory.com/46

위 링크를 기본으로 참조하되, 애플리케이션은 위 링크에 있는 걸 받지 마시고, 검색해서 최신 버전을 받으셔야 될 겁니다.

ps:  구글이 짱일세... 저장된 페이지 보기라... 캬..
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


[봄싹] 하이버네이트와 자바 퍼시스턴스 베타리딩 합니다.



Java Persistence With Hibernate 번역서 입니다. 이 책이 드디어 한국어로 나올 날이 가까워 오는군요. 물개선생님한테서 이 작업을 인수 받고 상당히 버거웠었는데, 다행히도 번역계의 마이다스이신 대엽님께서 번역 팀장을 맡고 계십니다.

베타리딩 방법은 이전의 프로 스프링 2.5 베타리딩 방식과 동일합니다. 프리스타일 이지만 어느 정도 중심적인 관점(Aspect or Concern)을 가지고 봐주셨으면 합니다.

"문법 위주로 보고 싶으신 분은 문법 위주로.." 예) 맞춤법, 올바른 한글 표현인지 아닌지..
"코드 위주로 보고 싶으신 분들은 코드 위주로.." 예) 코드를 다운 받을 수 있는지, 실행하는 방법은 어떤지, 코드가 틀리진 않았는지..
"내용 위주로 보고 싶으신 분들은 내용 위주를.." 예) n+1 select 문제에 대한 설명이 올바른지..

1. 때가 되면 제가 워드 문서를 그룹스로 보내드릴겁니다. 예) [하이버네이트 베타리딩] n장
2. 그럼 워드의 메모 기능을 이용하여 검토해주세요.
3. 파일 이름은 "1장_베타리딩_누가_언제.doc" 규약을 따라주세요.
4. 해당 파일을 gmail에서 그룹스에 답멜로 첨부해 주시거나, 저에게(이 블로그 맨 밑에 주소가 있어요) 이메일로 보내주세요.

참 쉽죠?

아참. 베타리딩은 봄싹 스터디가 전담하기 때문에, 봄싹 그룹스에 가입하셔야 합니다. 설마... 베타리딩을 하고 싶으셔서 가입하시는 분들은...;; 있으시려나;; 환영합니다. ;-)
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


와이프는 요리사 - 한방 삼계탕




진짜로 요리사는 아니구요. 닭고기는 푹 삶아서 뼈에서 스스륵 분리가 되고, 국물은 온갖 한약 재료로 소금 간을 안해도 맛있게 마실 수 있더라구요. 캬~~ 와이프가 해주는 요리 중에 최고에요.

치킨 사 먹는 것보다 시간은 좀 오래 걸리지만, 싸고 맛있고 건강 음식이니까 좋은 것 같더라구요.

ps: 요즘 닭 한마리에 8천원 정도더군요.
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


이번주 토요일 IBM dW "웹 개발 다반사"

모하니?/Planning : 2009.12.03 10:16


참조: http://www.ibm.com//developerworks/kr/event/seminar/dwlive_1205/index.html

지난 번 KSUG 모임에서는 봄싹 소스 코드를 보면서 이런 저런 스프링 기능을 보여드리려고 했으나, 불발했습니다. 개인적으로 많이 아쉽고 속상했지만, 아직은 그런 모임 운영이 전문적이지 못한 신생 운영진이었는데다가, 이전 발표 하신 분들의 내용이 워낙 참신하고 재미있었기에 제 발표 시간이 없어진 것에 대해 머리로는 이해할 수 있었습니다. 그래도 아쉬운 속내는 어쩔 수가 없었죠.

그런데 이번에는 조금 다른 형식으로 봄싹을 소개할 자리가 마련됐습니다. 당일에도 스터디가 있어서 시작 시간에 맞춰서 갈 수 있을지는 의문이지만.. 발표 시간에는 늦지 않게 참석할 생각입니다.

최종 선정된 발표 주제는 다음과 같습니다.
* 괜찮은 오픈 API 제공하기 + VLAAH API 소개 - 홍민희
* 봄싹 싸이트(http://springsprout.org) 개발 협업 방법 및 사용 기술 - 백기선
* 코드 품질 포탈 SONAR 적용기 - 고경철
* 흑백무성영화한편! (HTTP) - 이동욱
* 자바스크립트 삽질(실수?) 베스트 10 - 장동수
* (Startup기업 CEO의 관점에서 본) 기술의 경제학 - 정지웅
* Realtime Web 간보기 - 김석준
* Spring Framework with JavaFX - 이승철
* 추상 계층의 딜레마 - 황대산
* timelog 업무 적용 실험기 - 송승렬

흠.. 개인적으로는 자바스크립트 삽질 베스트, Realtime Web, 추상 계층의 딜레마 JavaFX with Spring(설마 JavaFX를 이용해서 스프링 프로그래밍을 한 건 한 건 아니겠죠? 제목이 좀...)이 기대됩니다. 다른건 저에게 생소한 것들(VLAAH, timelog, SONAR)이라;; 뭔지 잘 몰라서..;

'모하니? > Planning' 카테고리의 다른 글

블로깅 계획  (0) 2010.05.06
백기선's 2010 Weekly Report - 2/23  (2) 2010.02.23
목표 실천은 진행 중  (12) 2010.02.17
백기선 2010 목표  (12) 2010.02.15
봄싹 Career Path  (6) 2009.12.17
이번주 토요일 IBM dW "웹 개발 다반사"  (2) 2009.12.03
봄싹을 알리러 갑니다.  (6) 2009.11.24
[Atlassian] 이직 계획  (2) 2009.11.02
[ToDo] 20091016  (0) 2009.10.16
[ToDo] 오늘 할 일 - 할일(예상 소요 시간)(실제 소요 시간)  (4) 2009.10.15
2009년 마무리로 할 일  (4) 2009.10.08
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


난 월세가 좋은데;

모하니?/Thinking : 2009.12.01 12:41


그렇다. 난 돈이 없다. 그렇다고 끼니를 거르거나 길에서 자야할 정도는 아니지만 람보르기니를 산다거나, 마당에 수영장과 잔디밭과 호수가 있는 저택을 살 돈은 없다. 한마디로 그냥 서민이다. 우리 부모님은 자꾸 전세를 어떻게든 얻어라, 집을 빨리 사야 안정적이다 하시는데, 난 잘 모르겠다.

왜 집을 사야 하는걸까?? 아니면 왜 그 힘들게 모은 목돈으로 전세를 얻어 들어가야 하는걸까? 둘 다 어차피 월세 내는 만큼의 유지비(세금, 아파트 관리비 등)는 발생하기 마련 아닌가? 그렇다면 굳이.. '아~~ 내가 샀다!!' 이런 만족감을 얻기 위해서 그렇게까지 해야 할 필요가 있을까? 부동산 투자? 집 사서 나중에 오를테니까 그때 판다고? 과연 그렇게 집값이 오를만한 집을 나같이 평범한 시민이 살 수나 있을까? 부르마블처럼 언제든 팔려면 팔 수 있는 집을 살 수 있다면 나도 사고 싶기는 한데... 한국의 천민자본주의에서 그게 어디 가능키나 한 발상이냔 말이다.

지하철이나 TV 광고중에 가끔 20년 장기로 자기집을 마련하라는 광고를 접할때면 난 그런 생각이 든다. 왜 집을 담보로 반 평생을 국가의 노예로 살아야 하는거지? 저런 집을 사람들은 정말로 산단 말이야?? 헐...  사람이 살다 보면 일을 못하게 될 일이 생길 수도 있고, 그냥 일이 하기 싫어서 쉬고 싶을 수도 있는데, 그런 이유로 빚을 못갚아서 집을 국가가 다시 가져가서 경매로 내다 팔면? 난 거지가 되는거 아냐?? 그럼 아파도 참고 미친듯이 일을 해야 한다느건데.. 그렇게 일하는게.. 사람인가? 노예지.. 전국민을 노예로 만들려는 심산인가... 하긴 MB를 뽑을 정도의 개념이라면 자신이 돈의 노예가 되었다는 사실을 알지도 못한채 살아갈 것 같다.

그런 사람들이 많아지니까 천민자본주의가 되는거고, 실력보단 허세와 룸싸롱 로비가 먹히는거고, 순수한 열정을 가진 사람들은 질력이 나서 떠나거나 죽거나 물들어가며 사는거고, 그런걸 사회화라고, 성장하는거라며 자위나 하는 구더기로 넘쳐나는 쓰레기더미가 되가는건 아닐까?

그렇다. 나는 다소 과대망상하는 경향이 있다. 진정하고 공부나 하자. 그게 살 길이다.

top


[봄싹] 점진적인 개발

모하니?/Coding : 2009.11.30 13:52


봄싹에서 Agile 방법론을 공부한 적은 없지만, 자연스럽게 그와 비슷한 형태의 개발이 진행되는걸 보는 재미가 쏠쏠 합니다. 그 중 하나로 점진적인 개발을 들 수 있습니다. 다들 바쁜 생업을 하는 중에 짬을 내서 개발을 하는 중인데다가, 배포 주기를 될 수 있으면 짧게 가져가고 있기 때문에 '설계'에 많은 시간을 투자할 여유도 없거니와 전부 개발자이자 고객이기 때문에 '요구사항 정리'나 '문서화'에 시간을 들여야 할 이유가 별로 없습니다. 그 대신 구글 그룹스에서 활발한 토론을 통해서 필요한 기능에 대한 대충의 윤곽을 잡은 다음, 해당 기능을 만들고 싶은 개발자가 스스로 원하는데로 만듭니다. 그 뒤에 누군가 피드백을 주면, 다시 개발자는 그 피드백을 고객의 요구사항이라고 생각하고 되도록이면 반영해줍니다. 또는 피드백을 준 개발자가 직접 해당 기능을 손보기도 합니다. 그 과정 중에 자연스럽게 점진적인 개발을 실천하게 됩니다.

봄싹 사이트 0.9

지금 보시는 화면은 봄싹 0.9 버전의 메인 화면입니다. 전부가 정적인 구성요소로 HTML 파일로 만든것과 다를바 없는 JSP 파일이었습니다.

현재는 아래처럼 바뀌었습니다.

1. 제일 처음 디자인이 바뀌었습니다. 페이지는 여전히 HTML과 다를바 없었습니다. 낙서장, 공지사항, 스터디, 위키, 세미나, 커버플로우, 협찬 로고 등 아무것도 동적인 데이터는 없었습니다.

2. 그러다 낙서장 기능이 추가됩니다. 낙서장도 처음에는 정적인 디자인부터 시작했고, 동적으로 li를 추가하게 바뀌었습니다. 여기서 사용한 기술이 조금 재미있습니다. 페이지 릴로딩을 하지 않고 Ajax로 요청을 주기적으로 보내서 HTML을 업데이트 하는 방식입니다. 사용자는 가만히 첫 페이지를 열어두면 다른 사람들의 낙서가 업데이트 되는걸 볼 수 있습니다.

3. 다음은 공지 사항이 추가되었습니다. 그러면서 낙서장은 계속해서 디자인이 바뀌고 스크롤 방식이 바뀌어 나갑니다. 공지는 제이쿼리 플러긴을 이용해서 이쁜 팝업을 띄우게 되었고, 공지 목록이 길어지면 스크롤링을 하기 위해 처음부터 모든 데이터를 가져오지 않고 애초에 Ajax로 필요한 만큼의 데이터만 가져옵니다. 하지만 아직 스크롤링이 완전히 구현되어 있지는 않습니다. 아직도 개발 중인거죠.

4. 다음은 스터디 목록을 추가했습니다. 이 부부는 Ajax를 사용하지 않았었습니다. 처음에는 스터디 목록만 전부 읽어와서 랜더링 될 때 보여주고 끝이었습니다. 그러나 조금 뒤에 바뀌게 됩니다. 공간이 넉넉하고 스터디에 속한 모임을 주로 참조하는 관계로, 모임 목록까지 보여줘야할 필요가 생겼습니다. 하지만 사용자가 어떤 스터디의 모임목록을 원하는지도 모르면서 모든 모임 목록을 가져오고 싶지는 않았습니다. 그래서 Ajax를 적용했고, 하이버네이트 쿼리 캐쉬를 적용해서, 일단 한번 DB에서 가져온 모임 목록은 일정 시간동안 다시 쿼리를 날리지 않고도 보여주게 했습니다. Ajax 요청 캐시까지 적용하면 더욱 좋겠지만, 그건 아직 다음 과제로 남겨뒀습니다.

5. 마지막은 위키였습니다. 위키 목록은 스터디 목록을 뿌리는 것과 동일했습니다. 다만 위키 제목이 길어질 여지가 있어서 길이를 어느정도로 짜른다음 ...을 붙이도록 개선했습니다.

이와 같이 메인 페이지를 구성하는 것들 중에 어느 하나도 한 번에 개발이 끝난것이 없으며, 지금도 사실 완전히 끝났다고 볼 수 없는 기능들이 많습니다. 메인 페이지의 구성요소 하나 하나도 자세히 히스트로리를 들여다보면 점진적으로 개발되고 있는 모습을 볼 수 있습니다. 낙서장이 대표적입니다. 처음에는 정적인 리스트였다가. Ajax로 추가할 수 있게 바뀌고, 스크롤바를 추가했다가, 지금은 마우스 드래그로 바뀌었고, 무한 드래그에서 끝이 막히는 드래그로 바뀌었습니다. 공지, 스터디, 위키도 그렇게 매우 자연스럽게 진화를 거듭하고 있습니다.

조금씩.. 쓸 수 있는 상태를 유지하면서.. 발전하기.

바로 이런게 점진적인 개발 아닐까요?

참조: 롤러스케이트 구현
top




: 1 : 2 : 3 : 4 : 5 : 6 : 7 : ··· : 32 :