Whiteship's Note

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

  1. 2010.01.19 개발 용어 한글화 프로젝트 "DevTerms"
  2. 2009.12.19 [웹 사이트 속도 향상 베스트 프랙티스 10] 자바스크립트 CSS 크기 줄이기 (2)
  3. 2009.12.18 [스프링 테스트 확장] static member class를 빈으로 등록하는 테스트 로더 1 (6)
  4. 2009.12.16 ProxyFactoryBean을 이용한 초간단 AOP 구현 (2)
  5. 2009.12.16 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 4 - 일단 끝 (4)
  6. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 3
  7. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 2 (2)
  8. 2009.12.15 [스프링 3.0 테스트 확장] 애노테이션 설정 기반 테스트 러너 만들기 1
  9. 2009.12.09 스프링이 컴포넌트 스캔을 할 때 클래스로딩을 하지 않는다는 증거 (2)
  10. 2009.12.04 조금 친절한 코드 생성기 7(일단 완성) - DAO 생성기 코딩
  11. 2009.12.04 조금 친절한 코드 생성기 6 - 테스트 코드 리팩토링
  12. 2009.12.04 조금 친절한 코드 생성기 5 - 리팩토링
  13. 2009.12.04 조금 친절한 코드 생성기 4 - 설계 변경 적용(인터페이스)
  14. 2009.12.04 조금 친절한 코드 생성기 3 - 설계 변경 적용(Map)
  15. 2009.12.04 조금 친절한 코드 생성기 2 - 설계 변경
  16. 2009.12.04 조금 친절한 코드 생성기 1 - 구상
  17. 2009.12.03 불친절한 코드 생성기 5(일단 끝) - 프리마커 기반 코드 생성기 만들기 (2)
  18. 2009.12.03 불친절한 코드 생성기 4 - 템플릿 만들기
  19. 2009.12.03 불친절한 코드 생성기 3 - Freemarker 학습 테스트
  20. 2009.12.03 불친절한 코드 생성기 2 - 구상2
  21. 2009.12.03 불친절한 코드 생성기 1 - 구상 (2)
  22. 2009.12.01 [웹 사이트 속도 향상 베스트 프랙티스 9] DNS 룩업 줄이기
  23. 2009.11.30 [봄싹] 점진적인 개발
  24. 2009.11.30 [웹 사이트 속도 향상 베스트 프랙티스 8] 자바스크립트와 CSS 외부화하기 (3)
  25. 2009.11.27 봄싹 사이트 1.0 M1 오픈 임박!! (4)
  26. 2009.11.25 [웹 사이트 속도 향상 베스트 프랙티스 7] CSS expression 사용하지 않기
  27. 2009.11.24 애노테이션 VS XML, 상속, static method call, ... (4)
  28. 2009.11.24 [웹 사이트 속도 향상 베스트 프랙티스 6] 스크립트를 바닦에 두기
  29. 2009.11.23 [웹 사이트 속도 향상 베스트 프랙티스 5] 스타일시트를 HEAD에 넣기 (2)
  30. 2009.11.19 [웹 사이트 속도 향상 베스트 프랙티스 4] 컴포넌트 압축하기(Gzip Components)

개발 용어 한글화 프로젝트 "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


[봄싹] 점진적인 개발

모하니?/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


[웹 사이트 속도 향상 베스트 프랙티스 8] 자바스크립트와 CSS 외부화하기

모하니?/Coding : 2009.11.30 12:38


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

Make JavaScript and CSS External

tag: javascript, css

많은 성능관련 규칙들이 어떻게 관리중인 컴포넌트들을 외부화 하는지를 다루고 있다. 하지만, 그 전에 보다 기본적인 질문에대해 생각해봐야 한다. 자바스크립트와 CSS는 외부 파일에 들어있어야 하는가 아니면 페이지 자체에 포함되어 있어야 하는가?

실제로는 외부 파일을 사용함으로써, 브라우저가 자바스크립트와 CSS를 캐시하여 페이지 로딩 속도를 향상시킬 수 있다. HTML 문서에 들어있는 자바스크립트와 CSS는 매번 HTML 문서를 요청받을 때마다 다운로드 된다. 이렇게 하면 필요한 HTTP 요청을 줄일 수 있기는 하지만, HTML 문서 크기를 증가시킨다. 반면에, 만약 자바스크립트와 CSS를 외부 파일로 두고 브라우저가 캐시할 수 있게 하면 HTML 문서 사이즈도 줄이고 HTTP 요청 수의 증가도 없다.

그렇다면, 중요한 것은 요청되는 HTML 문서의 양에 비해 상대적으로 캐시 해야하는 자바스크립트와 CSS 컴포넌트 빈도이다. 수치화하기 어려울 수 있지만, 이 요소는 다양한 척도를 통해 계산할 수 있다. 만약 사이트의 사용자가 세션 당 여러 페이지를 요청하고 페이지의 대부분에서 동일한 스크립트와 스타일시트를 재사용한다면, 외부 파일 캐시를 통해 얻을 수 있는 잠재적인 장점이 더 크다.

대부분의 웹 사이트는 이러한 척도 중간에 놓여있다. 그러한 사이트에서 최선은 선택은 보통 자바스크립트와 CSS를 외부 파일로 배포하는 것이다. 페이지에 포함시키는 것이 더 좋을 수 있는 예외로 야후의 첫 페이지와 마이 야후 같은 홈 페이지가 있다. 홈 페이지는 세션당 페이지 뷰가 거의 없다. (대부분 하나다) 따라서 자바스크립트와 CSS를 페이지에 포함시키는 것이 최종 사용자 응답 시간을 줄일 수 있다.

여러 페이지 뷰로 연결되는 첫 페이지의 경우, 외부 파일을 통해 캐시 장점을 이용하는 것 말고도 HTTP 요청을 줄이는 기술들이 있다. 그런 기술 중 하나가 자바스크립트와 CSS를 첫 페이지에 두지만, 외부 파일을 페이지 로딩이 끝난 뒤에 동적으로 다운로드하는 것이다. 그 다음 연쇄적으로 호출되는 페이지들은 이미 브라우저의 캐시된 외부 파일을 참조할 것이다.

top


봄싹 사이트 1.0 M1 오픈 임박!!

모하니?/Coding : 2009.11.27 08:35


현재 봄싹 사이트(0.9)에서 보다 더 스터디 관리 기능을 보강했으며, 위키 디자인을 개선하여 편의성을 높였습니다. 트위터와 이메링을 비롯한 다양한 알림 서비스도 이용하실 수 있습니다. 특히 구글 토크를 사용하시는 분들은 스터디나 모임이 있을 때 메시지를 받을 실 수 있는것은 기본이고, 채팅창에서 study? meeting? 으로 현재 진행중인 스터디와 모임 정보를 확인하실 수도 있습니다. 홈 페이지에는 낙서장과 공지 사항을 추가했으며 스터디-모임 목록, 위키 목록으로 바로 이동할 수 도 있습니다. 또한 컨텐츠 작업으로, 스프링 3.0 레퍼런스 번역이 활발히 진행중입니다. 위키를 더 개선하고나면 저도 본격적으로 봄싹 컨텐츠 만들기 작업에 돌입할 생각입니다. 스프링소스 블로그 번역, 각종 웹 아티클 번역 자료를 봄싹 위키에 정리할 겁니다. 아무래도 제 블로그 보다는 위키가 찾아보기 편하니까요. (블로그에도 올리긴 할 겁니다.ㅋ)

내용적인 면이나 기능적인 면을 떠나서 이번 배포의 가장 큰 주안점은 바로 디자인입니다.

불과 두 달 사이에 획기적으로 달라진 봄싹 1.0 M1은 내일 낮에 오픈합니다.


Develop with passion! Evolve with SpringSprout!!




top

TAG 1.0 M1, 봄싹

[웹 사이트 속도 향상 베스트 프랙티스 7] CSS expression 사용하지 않기

모하니?/Coding : 2009.11.25 10:56


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

Avoid CSS Expressions

tag: css

CSS expression은 CSS 속성을 동적으로 설정할 수 있는 강력(하면서도 위험)한 방법이다. IE 버전5 부터 지원되기 시작했다. 예를 들어, CSS 표현식을 사용하여 배경 색을 매 시마다 다르게 설정할 수 있다.

      background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );

여기 보시다시피, expression 메서드는 자바스크립트 표현식을 받아들인다. CSS 속성은 자바스크립트 표현식을 계산한 결과로 설정된다.. expression 메서드는 다른 브라우저에서는 무시된다. 따라서 여러 브라우저에 걸쳐 일관적인 경험을 제공하고자 할 때 IE에 속성을 설정하는 방법으로 유용하다.

expression의 문제는 사람들이 예상한 것보다 훨씬 빈번히 계산된다는 것이다. 페이지가 렌더링 되고 크기 조절이 될 때에만 계산되는 것이 아니라, 페이지를 스크롤 하거나 사용자가 페이지 위에서 마우스를 움직일떄에도 계산한다. CSS 표현식에 카운터를 추가하여 CSS expression을 얼마나 계산했는지 추적할 수 있다. 마우스를 페이지에서 움직이면 거의 10,000 번이 넘게 계산을 수행한다.

CSS expression 계산 횟수를 줄일 수 있는 방법은 딱 한 번만 expression를 수행하도록 하는 것이다. 처음 expressio을 계산할때 stype 속성을 CSS expression 대신 명시적인 값으로 교체한다. 만약 스타일 속성이 반드시 페이지가 살아있는 동안 동적으로 바뀌어야 한다면, CSS Css expression 대신 이벤트 핸들러를 사용하는것도 하나의 대안이다. 만약 반드시 Css expression을 사용해야 한다면,수 천번 계산될 거라는 것과 그로인해 페이지 성능에 영향을줄수 있다는 것을 염두하라.
top


애노테이션 VS XML, 상속, static method call, ...

모하니?/Coding : 2009.11.24 14:20


봄싹 그룹스에서 애노테이션과 XML에 대한 논쟁이 어제 낮부터 오늘 낮까지 24시간 동안 불이 날 정도로 이어지고 있습니다. 하지만 애노테이션은 XML 하고만 비교당할 수 있는게 아닙니다.

스프링 테스트는 어떤가요?  이건 "애노테이션 VS 상속"의 문제입니다.
스프링 컨트롤러는 어떤가요? 그것도 "애노테이션 VS 상속"의 문제입니다.
Mockito의 @Mock은 어떤가요? 이건 "애노테이션 VS static method call"의 문제 아닐까요?

이렇게 여러 기술(?)과 비교 당할 수 밖에 없는 애노테이션이 저는 불쌍하고 애처롭습니다.

애노테이션아. 넌 왜 그렇게 유능해서(?).. 여러명한테 동시에 비교를 당해야 하는거니? 불쌍해서 봐줄수가 없구나.. 흑흑..
top


[웹 사이트 속도 향상 베스트 프랙티스 6] 스크립트를 바닦에 두기

모하니?/Coding : 2009.11.24 11:24


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

Put Scripts at the Bottom

tag: javascript

스크립트가 야기할 수 있는 문제는 병렬적인 다운로드를 막는다는 것이다. HTTP/1.1 표준은 브라우저가 호스트 이름당 두 개 이상의 컴포넌트를 병렬적으로 다운로지 하지 않도록 제안하고 있다. 만약 이미지를 여러 호스트 이름에서 제공한다면, 병렬적으로 두 개 이상의 다운로드가 발생하게 할 수 있다. 하지만, 어떤 스크립트를 하나 다운로드 할 때, 브라우저는 심지어 다른 호스트 이름에 있는 것이라 하더라도, 어떠한 다운로드도 시작하지 않는다.

어떤 상황에서는 스크립트를 바닦으로 옮기는 것이 쉽지 않을 수 있다. 예를들어, 만약 스크립트가 document를 사용하여 페이지의 내용 중 일부를 채우는 것이라면 페이지 하단으로 옮길 수가 없다. 또한 스코프와 관련된 이슈도 있을 수 있다. 대부분의 경우, 이런 상황에 대한 차선책이 있기 마련이다.

보통 자주 사용하는 차선책으로 지연된(deffered) 스크립트를 사용하는 방법이 있다. DEFER 속성은 해당 스크립트가 document.write를 가지고 있지 않다고 알려주며 브라우저가 계속해서 랜더링을 할 수 있는 단서가 된다. 불행히도, 파이어폭스는 DEFER 속성을 지원하지 않는다. IE에서는 스크립트를 지연시킬 수 있지만, 원하는 만큼은 아니다. 만약 스크립트를 지연시킬 수 있다면, 페이지의 바닦으로 옮길 수도 있을 것이다. 그렇게 하면 웹 페이지의 로딩 속도가 더 빨라질 것이다.
top


[웹 사이트 속도 향상 베스트 프랙티스 5] 스타일시트를 HEAD에 넣기

모하니?/Coding : 2009.11.23 15:53


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

Put Stylesheets at the Top

tag: css

야후에서 성능 조사를 하는 동안, 스타일 시트를 문서의 HEAD로 옮기는 것이 페이지 로딩을 더 빠르게 해준다는 것을 발견했다. 스타일 시트를 HEAD에 놓음으로써, 페이지 랜더링이 더 빨라지기 때문이다.

성능을 고려하는 화면 엔지니어는 페이지 로딩이 빨라지길 원한다. 즉, 브라우저가 어떤 내용이던지 가능한 빨리 보여주길 원한다. 이것은 느린 인터넷 연결을 사용하는 사용자와 보여줄 내용이 많은 페이지에서 특히 중요하다. 진행 상태 표시와 같은 시각적인 피드백을 주는 것의 중요함에 대해서는 설문조사가 잘 되어 있고 잘 문서화 되어 있다. 우리의 경우 HTML 페이지가 곧 진행 상태 표시기이다! 브라우저는 페이지 헤더를 빠르게 로딩할 때 네비게이션 바, 화면 윗 부분의 로고 등 모든 시각적인 피드백을 해당 페이지를 기다리는 사용자에게 제공한다. 그런식으로 전반적인 사용자 경험을 향상시킬 수 있다.

스타일시트를 문서의 바닥으로 넣는 것의 문제는 IE를 포함한 대부분의 브라우저에서 빠른 랜더링을 방해하기 때문이다. 이들 브라우저는 문서의 스타일이 바뀌면 페이지의 요소들을 다시 그려야 하는 것을 방지하기 위해 랜더링을 막아둔다. 그 때 사용자는 하얀색의 빈 화면에 머물러 있게 된다.

HTML 표준은 분명히 스타일시트를 페이지의 HEAD에 포함시킬 것을 기술하고 있다. "[LINK]는 얼마나 많이 있던 문서의 HEAD 부분에만 있어야 한다." 그 어떠한 대안도 비어있는 하얀 화면이나 스타일이 없는 내용으로 채워진 플래시 등의 위험을 감당할 만한 가치가 없다. 최적의 해결책은 HTML 표준을 따라 스타일시트를 문서 HEAD 안에서 로딩하는 것이다.
top


[웹 사이트 속도 향상 베스트 프랙티스 4] 컴포넌트 압축하기(Gzip Components)

모하니?/Coding : 2009.11.19 13:02


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

Gzip Components

tag: server

HTTP 요청과 응답을 네트워크로 전송하는데 걸리는 시간을 화면 개발자들과의 논의를 통해 극적으로 줄일 수 있다. 물론 최종 사용자의 인터넷 속도는 개발 팀과 관계없는 인터넷 서비스 제공처와 접근하려는 곳과의 거리에 따라 달라지기는 한다. 하지만 그 외밖에도 응답 시간에 영향을 주는 변수들이 있다. 압축을 기용하면 HTTP 응답의 크기를 줄여서 응답 시간을 빠르게 할 수 있다.

HTTP/1.1 부터, 웹 클라이언트는 HTTP 요청의 Accept-Encoding 헤더를 사용하여 압축 형태를 지원한다는 것을 알려줄 수 있게 되었다.

      Accept-Encoding: gzip, deflate

만약에 웹서버가 요청에 들어있는 이 헤더를 본다면, 응답을 클라이언트가 나열한 방법으로 압축할 수가 있다. 웹 서버는 웹 클라이언트에게 응답의 Content-Encoding 헤더를 사용하여 사용한 방법을 알려준다.

      Content-Encoding: gzip

Gzip은 근래 가장 유명하고 효율적인 압축 기술로, GNU 프로젝트에 의해 만들어졌으며 RFC 1952로 표준화 되었다. 다른 압축 기술중 여러분이 보고 싶어하는 것으로 deflate가 있지만 Gzip에 비해서 덜 효율적이며 덜 알려져있다.

Gzip 압축은 응답의 크기를 약 70% 가량 쥴여준다. 아마도 근래의 브라우저를 떠돌고 있는 인터넷 트래픽 중 90%는 gzip을 지원한다고 선언해 두었을 것이다. 만약 아파치를 사용하고 있다면, gzip 모듈 설정 방법은 버전에 따라 다르다. 아차피 1.3은 mod_gzip을 사용하고 아파치 2는 mod_deflate를 사용한다.

브라우저와 프록시를 사용할 때 브라우저가 예상한 것과 실제로 압축하여 받은 내용이 일치하지 않는 이슈가 있다. 다행히도, 구 버전 브라우저 사용이 점차 줄어듬에 따라 그런 이슈도 줄어들고 있다. 아파치 모듈은 Vary 응답 헤더를 사용하여 자동으로 적절한 타입을 찾도록 도와준다.

서버는 파일 타입에 따라 무엇을 gzip으로 압축할지 선택하지만, 실제로 무엇을 압축할지 결정한 것을 보면 매우 제한적이다. 대부분의 웹 사이트는 그들의 HTML 문서를 gzip으로 압축한다. 스크립트와 스타일시트도 압축할 만한 가치가 있는데 대부분의 사이트는 이것을 간과한다. 사실, 응답에 들어가는 모든 XML, JSON, 이미지, PDF 파일 등은 압축하지 말아야 한다. 이미 압축되어있는 형태기 때문이다. 그것들을 압축하는 것은 CPU를 낭비하는 것일 뿐 아니라 잠재적으로 파일 사이즈를 늘리는거나 마찬가지다.

Gzip으로 가능한 많은 타입을 압축하는 것은 페이지의 무게를 줄이고 사용자 경험을 가속화하는 쉬운 방법이다.


top




: 1 : 2 : 3 : 4 : 5 : ··· : 10 :