Whiteship's Note

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

  1. 2008.07.30 하이버네이트 Criteria 다루기 - 중복일까 아닐까 (2)
  2. 2008.07.23 log4j 설정 파일 위치를 명시적으로 설정하고 싶을 때.. (5)
  3. 2008.07.23 바코드를 출력 및 읽기 코딩할 때 주의할 것
  4. 2008.07.15 서비스 계층 다이어트 시키기
  5. 2008.07.08 상태 기반 테스트란?
  6. 2008.07.08 expect -> run -> verify 스타일(ex. Easymock) 바이바이
  7. 2008.07.08 JUnit 4.4에 추가된 Assumetion
  8. 2008.07.08 JUnit 4.4에 추가된 assertThat()
  9. 2008.07.01 엑셀 시트 복사하기(with POI) + 리팩터링
  10. 2008.06.26 서버에 위치한 파일 다운로드 링크 달기
  11. 2008.06.26 톰캣 인코딩 설정해야 하는 경우 (2)
  12. 2008.06.05 배치관리 및 실행 구현시 고려할 것.
  13. 2008.06.04 스프링을 사용한 트랜잭션 관리 방법 (2)
  14. 2008.06.02 윈도우 시간과 자바에 찍히는 시간이 안맞을 때
  15. 2008.05.14 Spring 2.5 @Controller 사용시 BindingResult 주의 할 것.
  16. 2008.05.11 OSIV 사용시 주의 할 것
  17. 2008.05.08 DWR이 제대로 자바스크립트 만들었는지 확인하기
  18. 2008.04.29 EasyMock 사용할 때 주의 할 것 (2)
  19. 2008.04.28 스프링의 getBean() 타입 캐스팅 없애는 방법 (2)
  20. 2008.04.26 이클립스의 기본 try-catch 탬플릿 비추
  21. 2008.04.25 테스트는 진짜로 잘 만들어야 함 (2)
  22. 2008.04.24 @Configurable 사용시 버그 피해가기
  23. 2008.04.22 GenericPropertyEditor 만들려면...
  24. 2008.04.18 TDD 연습하기 - RomanNumber (6)
  25. 2008.04.16 Spring 컨트롤러와 request scope bean (4)
  26. 2008.04.08 ControllerClassNameHandlerMapping 잘 되네~ (4)
  27. 2008.04.08 ControllerClassNameHandlerMapping가 찾아 준댔는데...
  28. 2008.04.02 스프링 하이버네이트 사용시 insert 문은 안 날아가고 select만 될 때.. (2)
  29. 2008.03.26 자바스크립트 줄바꿈 버그 해결과 원인 발견 (4)
  30. 2008.03.26 Hibernate의 Dynamic Instantiation 사용하기

하이버네이트 Criteria 다루기 - 중복일까 아닐까

모하니?/Coding : 2008.07.30 13:31


    @Override
    public List<T> search(P params, OrderPage orderPage) {
        // total rowcount
        orderPage.setRowcount((Integer) (addRestrictions(
                getSession().createCriteria(this.persistentClass), params)
                .setProjection(Projections.rowCount()).uniqueResult()));

        // pages list
        Criteria c = addRestrictions(getSession().createCriteria(
                this.persistentClass), params);
        orderPage.applyPage(c);
        orderPage.applyOrder(c);

        return c.list();
    }

    /**
     * template method for search
     *
     * @param c
     * @param params
     * @return
     */
    protected Criteria addRestrictions(Criteria c, P params) {
        return c;
    }

위 코드에서 중복이 보이시나요? 안 보이신 다구요?

    @Override
   public List<T> search(P params, OrderPage orderPage) {
       // total rowcount
       orderPage.setRowcount((Integer) (addRestrictions(
               getSession().createCriteria(this.persistentClass), params)
               .setProjection(Projections.rowCount()).uniqueResult()));

       // pages list
       Criteria c = addRestrictions(getSession().createCriteria(
               this.persistentClass), params);
       orderPage.applyPage(c);
       orderPage.applyOrder(c);

       return c.list();
   }

어떤가요. 중복 이죠? 그러나..

Criteria c = addRestrictions(getSession().createCriteria(
               this.persistentClass), params);
orderPage.setRowcount((Integer) (c.setProjection(Projections.rowCount()).uniqueResult()));
orderPage.applyPage(c);
orderPage.applyOrder(c);

대강 이런 식으로 리팩터링 해보면 하이버네이트는 요상한 쿼리와 함께 에러를 뱉어냅니다.

전체 Row 갯수를 반환하는 Criteria(쿼리는 select count(*).. )이런식으로 시작)를 다시 Order와 Page 처리를 할 때 사용하면 이상한 쿼리(select count(*).. order by ... 이게 이상한 이유는 order by에서 사용한 컬럼이 group by에 있어야 하는데 groupd by를 정의한 적이 없거니와, 사실 두 번째 쿼리는 count(*)가 없어야 하는데 앞에서 만들어둔 Criteria에 이어 붙인 꼴이 되어서 이상해졌습니다.)가 되버립니다.

중복처럼 보이지만 제거하면 코드가 깨지는... 요상한 경우. 이거 어떻게 처리하는게 좋을까요? 전 요리 조리 해보다가 그냥 뒀습니다.
top


log4j 설정 파일 위치를 명시적으로 설정하고 싶을 때..

모하니?/Coding : 2008.07.23 13:51


Junit4 기반 테스트시 Log4 설정파일 위치 지정 방법이 있을까요?

질문에 답이 있었습니다.

질문의 내용은 웹 애플리케이션이 돌아갈 때 사용할 log4j 설정파일은 web.xml에서 설정할 수 있었는데, 테스트 할 때 사용할 설정 파일의 위치는 어떻게 설정할 수 있느냐 입니다. 저도 잘 모르겠어서 어떻게 해야 되지? 라는 고민을 하다가 가장 먼저 떠오른 방법은 문제를 우회하는 방법이었습니다. "왜 위치를 명시적으로 설정해야 되지?" 싶어서 말이죠.

그래서 생각한 방법이 소스 폴더에 설정 파일을 두는 방법입니다. Maven이 아닐 경우 보통 src와 test 라는 소스 폴더를 만들어서 사용할텐데요. 그 경우 둘 중 아무곳에나 설정 파일을 두기만 하면 output 폴더(이클립스 자바 프로젝트 기본 outpu 폴더는 bin폴더)로 이동하게 됩니다. 그럼 그 파일을 log4j가 찾아서 사용하죠. Maven일 경우네는 src/main/resouces에 두거나 src/test/resource에 두면 됩니다.

그러나 Maven일 때 주의 할 것이 있는데, 바로 해당 폴더 루트에 위치 시켜야 한다는 겁니다. src/main/recouces에 두지 않고 그 안에 새로운 폴더 하나를 만들어서 두면 그건 소스 폴더로 인식하지 않습니다. 따라서 log4j가 설정파일을 못찾게 되죠.

결론적으로 명시적으로 log4j 설정파일 위치를 설정해주고 싶습니다. 굳이 소스폴더가 아니여도 참조할 수 있게 말이죠. 그래서 살펴본 것이 web.xml에 등록되어 있는 org.springframework.web.util.Log4jConfigListener 이 클래스 입니다. 분명 저 클래스가 설정 파일 위치 정보를 가져가니까 어디선가는 그 파일을 읽어서 설정하겠죠.

그래서 Log4jConfigListener 이 클래스를 찾아갔더니 별로 코드가 없습니다.

public class Log4jConfigListener implements ServletContextListener {

    public void contextInitialized(ServletContextEvent event) {
        Log4jWebConfigurer.initLogging(event.getServletContext());
    }

    public void contextDestroyed(ServletContextEvent event) {
        Log4jWebConfigurer.shutdownLogging(event.getServletContext());
    }

}

이 코드에서 사용하고 있는 Log4jWebConfigurer를 찾아갔습니다. 그리고 그 안에 있는 initLogging 메소드 안에서 다음의 코드를 발견했습니다.

Log4jConfigurer.initLogging(location, refreshInterval);

빙고!! 게임은 끝났습니다.

유겐 휄러가 2003년에 만든 클래스더군요. Log4jConfigurer 클래스를 감상하면서.. 참 멋지다는 생각을 했습니다.

/*
 * Copyright 2002-2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.URL;

import org.apache.log4j.LogManager;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;

/**
 * Convenience class that features simple methods for custom log4j configuration.
 *
 * <p>Only needed for non-default log4j initialization, for example with a custom
 * config location or a refresh interval. By default, log4j will simply read its
 * configuration from a "log4j.properties" or "log4j.xml" file in the root of
 * the classpath.
 *
 * <p>For web environments, the analogous Log4jWebConfigurer class can be found
 * in the web package, reading in its configuration from context-params in
 * <code>web.xml</code>. In a J2EE web application, log4j is usually set up
 * via Log4jConfigListener or Log4jConfigServlet, delegating to
 * Log4jWebConfigurer underneath.
 *
 * @author Juergen Hoeller
 * @since 13.03.2003
 * @see org.springframework.web.util.Log4jWebConfigurer
 * @see org.springframework.web.util.Log4jConfigListener
 * @see org.springframework.web.util.Log4jConfigServlet
 */

public abstract class Log4jConfigurer {


    /** Pseudo URL prefix for loading from the class path: "classpath:" */
    public static final String CLASSPATH_URL_PREFIX = "classpath:";

    /** Extension that indicates a log4j XML config file: ".xml" */
    public static final String XML_FILE_EXTENSION = ".xml";


    /**
     * Initialize log4j from the given file location, with no config file refreshing.
     * Assumes an XML file in case of a ".xml" file extension, and a properties file
     * otherwise.
     * @param location the location of the config file: either a "classpath:" location
     * (e.g. "classpath:myLog4j.properties"), an absolute file URL
     * (e.g. "file:C:/log4j.properties), or a plain absolute path in the file system
     * (e.g. "C:/log4j.properties")
     * @throws FileNotFoundException if the location specifies an invalid file path
     */
    public static void initLogging(String location) throws FileNotFoundException {
        String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location);
        URL url = ResourceUtils.getURL(resolvedLocation);
        if (resolvedLocation.toLowerCase().endsWith(XML_FILE_EXTENSION)) {
            DOMConfigurator.configure(url);
        }
        else {
            PropertyConfigurator.configure(url);
        }
    }

    /**
     * Initialize log4j from the given location, with the given refresh interval
     * for the config file. Assumes an XML file in case of a ".xml" file extension,
     * and a properties file otherwise.
     * <p>Log4j's watchdog thread will asynchronously check whether the timestamp
     * of the config file has changed, using the given interval between checks.
     * A refresh interval of 1000 milliseconds (one second), which allows to
     * do on-demand log level changes with immediate effect, is not unfeasible.
     * <p><b>WARNING:</b> Log4j's watchdog thread does not terminate until VM shutdown;
     * in particular, it does not terminate on LogManager shutdown. Therefore, it is
     * recommended to <i>not</i> use config file refreshing in a production J2EE
     * environment; the watchdog thread would not stop on application shutdown there.
     * @param location the location of the config file: either a "classpath:" location
     * (e.g. "classpath:myLog4j.properties"), an absolute file URL
     * (e.g. "file:C:/log4j.properties), or a plain absolute path in the file system
     * (e.g. "C:/log4j.properties")
     * @param refreshInterval interval between config file refresh checks, in milliseconds
     * @throws FileNotFoundException if the location specifies an invalid file path
     */
    public static void initLogging(String location, long refreshInterval) throws FileNotFoundException {
        String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location);
        File file = ResourceUtils.getFile(resolvedLocation);
        if (!file.exists()) {
            throw new FileNotFoundException("Log4j config file [" + resolvedLocation + "] not found");
        }
        if (resolvedLocation.toLowerCase().endsWith(XML_FILE_EXTENSION)) {
            DOMConfigurator.configureAndWatch(file.getAbsolutePath(), refreshInterval);
        }
        else {
            PropertyConfigurator.configureAndWatch(file.getAbsolutePath(), refreshInterval);
        }
    }

    /**
     * Shut down log4j, properly releasing all file locks.
     * <p>This isn't strictly necessary, but recommended for shutting down
     * log4j in a scenario where the host VM stays alive (for example, when
     * shutting down an application in a J2EE environment).
     */
    public static void shutdownLogging() {
        LogManager.shutdown();
    }

    /**
     * Set the specified system property to the current working directory.
     * <p>This can be used e.g. for test environments, for applications that leverage
     * Log4jWebConfigurer's "webAppRootKey" support in a web environment.
     * @param key system property key to use, as expected in Log4j configuration
     * (for example: "demo.root", used as "${demo.root}/WEB-INF/demo.log")
     * @see org.springframework.web.util.Log4jWebConfigurer
     */
    public static void setWorkingDirSystemProperty(String key) {
        System.setProperty(key, new File("").getAbsolutePath());
    }

}


유틸 클래스인데, abstract로 해놨습니다. 인스턴스 만들지 말라는거죠. 멋지지 않나요. 캬캬. 유틸 클래스를 보통 무심하게 그냥 public class로 해놓기 일수인데, abstract라는 키워드 하나가 정말 멋지게 느껴지지 않나요. 그리고 저 충실한 JavaDoc 코멘트들.. 정말 유겐은 대단합니다.

그러니까... 저런 코드를 출처(스프링 프로젝트 홈피 링크 덜렁 남기는게 아니라, 저 클래스나 javadoc URL을 걸어놔야 출처를 명시했다고 생각합니다. 그게 보통 말하는 출처 아닌가요?? 장난이 아니고서야 누가 대체 출처를 www.google.com 이라고 달죠?)도 없이 원작자 이름도 빼고 베끼는 코드를 보면, 어떻게 흥분을 안 하겠습니까.. 에흄.. 유겐은 참 착하기도 하지.

얘기가 좀 샜는데, 어쨌거나 저 클래스를 잘 이용하면 이 글의 제목에 대한 답은 된 것 같습니다. 역시나 멋진 스프링은 아직도 공부할 것들이 무궁무진 합니다. 특히 전 소스코드는 자세히 들여본적이 거의 없는데, 저 코드를 보고나니까 좀 관심이 갑니다. 쉬운 클래스 부터 하나씩 갈펴봐야겠습니다. BeanFactory나 ApplicationContext는 넘 어려워서 원~
top


바코드를 출력 및 읽기 코딩할 때 주의할 것

모하니?/Coding : 2008.07.23 11:39


바코드 언어에서는 시작과 끝을 알리는 기호로 * 를 쓰겠다는 룰이 있답니다. 그래서 출력할 때

e.Graphics.DrawString("*" + barcode + "*", barcodefont, Brushes.Black, 20, 35);

이런식으로 *로 감싸줘야 하고..

반대로 읽을 때는 그냥 *는 알아서 때어내고 읽어줍니다. 따라서 읽을 때는 별도의 작업 필요 없이 그냥 읽으면 된다는거~

첨에 저 코드를 보고 '흠.. 앞뒤에 왜 저런걸 붙여 놓으셨을까..괜히 길어지게.. 지우자.' 해서 지웠더니 바코드를 못 읽습니다. 크허헐 이유를 모르니 역시 또 사부님께 물어봤는데, 뭐 대답이 그냥 바로바로 나옵니다.

643388035488643388035488643388035488643388035488643388035488643388035488

쿄쿄쿄쿄. 닷넷이 재밌는게 아니라, 새로운 걸 알게 되고 새로운 걸 해보는게 재미있는 것 같습니다.
top

TAG 바코드

서비스 계층 다이어트 시키기

모하니?/Coding : 2008.07.15 14:51


    private void makeNewMonthlySalesSum(Supp supp, Date date) {
        MonthlySalesSum monthlySalesSum = new MonthlySalesSum();
        monthlySalesSum.setDate(date);
        monthlySalesSum.setSupp(supp);
        this.dao.add(monthlySalesSum);

        MonthlySalesSumDetail detail = null;
        for (Branch branch : branchDao.getBranchBySupp(supp.getId())) {
            DTO dto = this.dao.makeMonthlySalesSumDTO(branch, date);
            detail = makeNewMonthlySalesSumDetail(dto, branch, date);
           monthlySalesSum.addSalesCount(detail.getSalesCount());
            monthlySalesSum.addSellingQty(detail.getSellingQty());
            monthlySalesSum.addProfit(detail.getTotalProfit());
            monthlySalesSum.addSuppUnitPrice(detail.getTotalSuppUnitPrice());
            monthlySalesSum.addSuppUnitPriceWithoutText(detail.getTotalSuppUnitPriceWithoutTex());
            if(detail.getSalesCount() > 0){
                detail.setMonthlySalesSum(monthlySalesSum);
                monthlySalesSumDetailDao.add(detail);
            }
        }
        
    }

    private MonthlySalesSumDetail makeNewMonthlySalesSumDetail(DTO dto, Branch branch, Date date) {
        MonthlySalesSumDetail detail = new MonthlySalesSumDetail();
        detail.setBranch(branch);
        detail.setDate(date);
        if (dto != null) {
            detail.setSalesCount(dto.getSalesCount());
            detail.setSellingQty(dto.getSellingQty());
            detail.setTotalProfit(dto.getTotalProfit());
            detail.setTotalSuppUnitPrice(dto.getTotalSuppUnitPrice());
            detail.setTotalSuppUnitPriceWithoutTex(dto.getTotalSuppUnitPriceWithoutTex());
        }
        return detail;
    }


서비스 클래스에 있는 배치 작업 용 메소드들 입니다. 종합 정보를 생성하는데 상세 정보와 함께 묶어서 작업을 하고 있는데 코드가... 참... 거시기 합니다. 클래스에 들어있는 코드의 절반을 이 두 개의 메소드가 잡아먹고 있었습니다.

자세히 보니까 어떤 객체에서 값들을 꺼내서 다른 객체에 전달하는 일들이 전부 입니다. 흠... 옮길 수 있겠다!! 라는 생각이 젤 먼저 들었습니다. 잠시 뒤.. 오.. 당연히 옮겨야 되겠는데? 사실 저렇게 세팅해주는 일이 원래 저 서비스 클래스가 할 일은 아니니까... 걍 자기가 알아서 값들 세팅하면 되지 왜 서비스 계층이 저렇게 일일히 세팅하게 했을까나;; ㅠ.ㅜ (제가 코딩한 겁니다.ㅋㅋ)

그래서 코드를 고쳤습니다.


    private void makeNewMonthlySalesSum(Supp supp, Date date) {
        MonthlySalesSum monthlySalesSum = new MonthlySalesSum();
        monthlySalesSum.setDate(date);
        monthlySalesSum.setSupp(supp);
        this.dao.add(monthlySalesSum);
       
        MonthlySalesSumDetail detail = null;
        for (Branch branch : branchDao.getBranchBySupp(supp.getId())) {
            DTO dto = this.dao.makeMonthlySalesSumDTO(branch, date);
            detail = makeNewMonthlySalesSumDetail(dto, branch, date);
            monthlySalesSum.applyDetail(detail);
            if(detail.getSalesCount() > 0){
                detail.setMonthlySalesSum(monthlySalesSum);
                monthlySalesSumDetailDao.add(detail);
            }
        }
    }

    private MonthlySalesSumDetail makeNewMonthlySalesSumDetail(DTO dto, Branch branch, Date date) {
        MonthlySalesSumDetail detail = new MonthlySalesSumDetail();
        detail.setBranch(branch);
        detail.setDate(date);
        if (dto != null)
            detail.setByDTO(dto);
        return detail;
    }

코드를 1/3 가량 줄일 수 있었습니다. 야호~

서비스 계층은 가볍게 도메인 계층은 두툼하게...
자기가 할 일은 자기가 하자.

top


상태 기반 테스트란?

모하니?/Coding : 2008.07.08 20:16


참조 : http://blog.jayfields.com/2008/02/state-based-testing.html

번역 및 요약 및 편역

테스트 대상이 되는 객체에 있는 여러 메소드들을 호출 한 뒤에 객체의 상태를 확인해보는 테스트 기법이다. 테스트 코드가 테스트 대상의 세부 구현 내역에 대해 보더 덜 상세하게 알아도 된다. 따라서, 테스트 대상이 되는 코드의 구현 내용이 바뀐다 하더라도 결과 상태만 같으면 해당 테스트 코드는 깨지지 않고 유지 된다.

사족

흠.. 확실히 EasyMock을 사용할 때는 테스트 대상이 참조하는 객체를 Mock으로 만들고 그 Mock이 어떻게 행동할지를 일일히 세부적으로 순서까지 맞춰가면서 결과값까지 예측, 혹은 녹화를 해줬어야 테스트가 동작을 했습니다. 그래서 테스트 대상이 되는 메소드가 혹시 해당 객체에서 다른 메소드를 호출하거나 호출 순서를 약간 조정하면 해당 테스트를 깨지는 사태가 종종 발생했습니다. 불편했죠. 테스트 만들기도 빡쌨고, 이런식의 코드로 어떻게 (순수) TDD를 하나 싶었습니다. '차라리 이럴바엔 바로 구현을 하지... 테스트 코드를 왜 만들어...' 라는 생각이 절로 나는거죠.

그런데 위의 얘기 처럼 상태 기반으로 테스트를 쉽게 작성할 수만 있다면, Mockito가 그런식 으로 테스트를 작성하기 쉽게 해준다면, 좀 더 시간을 들여서 익혀볼 만 한 것 같습니다. 흠.. 상태 기반 테스트라~ 어떻게 작성해야 하나.. Mockito로 작성된 테스트 코드를 보고 싶네요.
top


expect -> run -> verify 스타일(ex. Easymock) 바이바이

모하니?/Coding : 2008.07.08 19:50


참조 : http://monkeyisland.pl/2008/02/01/deathwish/

이지목 스타일은 녹화 -> 플레이 -> 확인(expect -> run -> verify) 순으로 mocking 또는 stubbing 하는 거였습니다. 그러나 이 스타일은 다음과 같은 단점들이 있습니다.

1. 테스트 메소드가 지져분해짐.
- 이것 저것 예측/녹화를 해줘야 하는데 그게 테스트를 위해서가 아니라 Mock을 위해서 해줘야 한다는게 좀..

2. 자연스러운 테스트 스타일로 느껴지지 않는다.
- 예측을 한 담에 실행하는게 아니라, 실행 한 다음에 예측되는 Mock의 행위를 나열해 주는게 더 자연스럽다.

3. 테스트가 깨지기 쉽다.
- 새로운 기능을 추가하면, Mock을 사용한 테스트가 왕창 깨지는 경우가 발생한다.

4. 보다 자세한 실패 메시지를 보여줄 수 있었을 텐데...

5. 보다 가독성 좋게 만들 수 있었을 텐데...

그래서 상태 기반 테스트를 제공하는 Mockito를 강추 한다는거...

Mockito 홈에서 다음을 인용합니다.

No expect-run-verify also means that Mockito mocks are often ready without expensive setup upfront. They aim to be transparent and let the developer to focus on testing selected behavior rather than absorb attention.

Mockito has very slim API, almost no time is needed to start mocking. There is only one kind of mock, there is only one way of creating mocks. Just remember that stubbing goes before execution, verifications of interactions go afterwards. You'll soon notice how natural is that kind of mocking when test-driving java code.

즉 stubbing -> execution -> verification 라고 할 수 있겠네요. 훔.. 그래도 왠지 expect -> run -> verify 형태와 비슷해 보이네요.

사용법은 여기에 잘 나와있습니다.
top


JUnit 4.4에 추가된 Assumetion

모하니?/Coding : 2008.07.08 16:32


테스트 코드를 실행하는 환경이 달라짐에 따라서 테스트가 동작하지 않는 경우가 있는데, 그럴 때는 그런 환경 값들을 테스트를 돌리기 전에 설정해주면 테스트가 다시 잘 돌아갑니다. 예를 들어, 위도우에서는 폴더 구분할 때 \를 쓰지만 리눅스에서는 / 를 쓰고, 라인브레이크나 뭐 기타 표시들이 다를 수 있죠. 그런 경우 파일을 읽어오는 테스트가 있다면 운이 안 좋을 땐 테스트가 실패할 수도 있습니다.

그래서 그러한 "가정"을 실제로 코드로 미리 해두면, 그 테스트가 여러 환경에서 테스트를 하더라도 실패하는 일이 발생하진 않겠죠.

import static org.junit.Assume.*

@Test public void filenameIncludesUsername() {
   assumeThat(File.separatorChar, is('/'));
   assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
}

@Test public void correctBehaviorWhenFilenameIsNull() {
   assumeTrue(bugFixed("13356"));  // bugFixed is not included in JUnit
   assertThat(parse(null), is(new NullDocument()));
}

이런 식으로 사용할 수 있군요. 흠.. Stub을 만들고 그걸 Injection 해주는 걸까요.

참조 : http://junit.sourceforge.net/doc/ReleaseNotes4.4.html

ps : assumetion을 @Theory와 @Datapoint라는 것도 있는데 이건 좀 복잡해 보이네요. 패스~!
top


JUnit 4.4에 추가된 assertThat()

모하니?/Coding : 2008.07.08 15:44


참조
http://junit.sourceforge.net/doc/ReleaseNotes4.4.html

흠.. 맨날 쓰는 메소드만 쓰다보니까, 새로운 기능들을 전혀 몰랐네요;
assertThat() 처럼 멋진 메소드를 이제야 알게 됐습니다.

보통 값을 비교할 때 assertEquals()를 사용해서

assertEquals(new Integer(2), game.getLeastTryCount());

이런식으로 값을 비교합니다. 메소드에 넘겨주는 첫 번째 인자가 기대값이고 뒤에 오는 인자가 실제 값인데, 사실  인자들의 순서가 바껴도 테스트 목적에는 별 다른 지장을 주지 않습니다. 위에서 테스트한 것과 동일한 내용을 assertThat()을 사용하여 다음과 같이 작성할 수 있습니다.

assertThat(game.getLeastTryCount(), is(2));

코드를 한 번 읽어보시죠. 훨씬 좋치 않나요? 위에서 사용한 is() 라는 메소드는 JUnit이 처음로 의존성을 가지고 사용하는 제 3자의 클래스들 입니다. Hamcrest라는 프로젝트 인데, Matcher를 확장한 다양한 메소드들을 제공해주고 있습니다. 그 중 하나가 is() 입니다. 따라서 import 문이 필요한데, 위에서 is만 사용한거 보니까 static import 겠거니.. 하고 짐작을 하셨겠죠?

import static org.hamcrest.CoreMatchers.*;

이렇게 추가해주시면 됩니다.

문장이 읽기 좋다는 장점 외에도 다음과 같은 장점들이 있습니다.
- 콤비네이션 가능. is(not(3)) 이나 eather(2).or(3) 같은 조합을 이뤄서 사용할 수 있음.
- assertEquals() 보다 더 가독성 높은 에러 메시지.
- 커스텀 매처 사용하능.

JUnit 4.4 가 제공하는 매처들
- org.hamcrest.CoreMatchers
- org.junit.matchers.JUnitMatchers.


top


엑셀 시트 복사하기(with POI) + 리팩터링

모하니?/Coding : 2008.07.01 13:09


    private void copySheet(HSSFSheet from, HSSFSheet to) {
        HSSFRow firstRow = from.getRow(0);
        HSSFRow secondRow = from.getRow(1);
        HSSFRow thirdRow = from.getRow(2);
        
        HSSFRow firstRow2 = to.createRow(0);
        HSSFRow secondRow2 = to.createRow(1);
        HSSFRow thirdRow2 = to.createRow(2);
        
        Iterator<HSSFCell> iterator = firstRow.cellIterator();
        short col = 0;
        while(iterator.hasNext())
            addCell(firstRow2, col++, iterator.next().getStringCellValue());
        
        col = 0;
        iterator = secondRow.cellIterator();
        while(iterator.hasNext())
            addCell(secondRow2, col++, iterator.next().getStringCellValue());

        col = 0;
        iterator = thirdRow.cellIterator();
        while(iterator.hasNext())
            addCell(thirdRow2, col++, iterator.next().getStringCellValue());
    }

무려 세 번이나 중복되고 있지만, 일단 기능이 제대로 돌아가는지부터 보려고 허겁 지겁 코딩을 하고 결과를 확인해 보니 괜춘하네요. 여기서 그만 둘까도 생각했지만, 에이~ 뭐 시간도 많은데 저런걸 그냥 두긴 뭐하다 싶어서 리팩터링..

    private void copySheet(HSSFSheet from, HSSFSheet to, int fromRowCnt, int toRowCnt) {
        HSSFRow fromRow = null;
        HSSFRow toRow = null;
        for(int i = fromRowCnt ; i <= toRowCnt ; i++){
            fromRow = from.getRow(i);
            toRow = to.createRow(i);
            Iterator<HSSFCell> iterator = fromRow.cellIterator();
            short col = 0;
            while(iterator.hasNext())
                addCell(toRow, col++, iterator.next().getStringCellValue());
        }
    }

리팩터링 하는 김에 좀 더 Generic하게 만들어서 몇 번째 줄부터 몇 번째 줄까지 복사할지 인자로 넘겨주도록 수정 함. 이제 저 메소드는 ExcelUtils로 옮기면 ExelView쪽 코드는 약간 더 깔끔해지겠죠. 그건 뭐 간단하니 생략합니다.
top


서버에 위치한 파일 다운로드 링크 달기

모하니?/Coding : 2008.06.26 18:05


    var excelformdown = function(){
        location.href = "${actionURL}";
    }

일단 화면에서 어떤 이미지 버튼을 클릭하면 자바스크립트로 특정 URL을 호출하게 하도록 하고...

저 URL을 처리할 핸들러에서 파일을 응답으로 돌려주면 된다.

    @RequestMapping
    public void excelformdown(HttpServletResponse response){
        giveExcelForm("sales.xls", response);
    }

저 메소드 안 에서는 응답 유형을 multipart로 설정하고, 파일 이름을 설정해준다. 그리고 파일을 찾아서 output 스트림에 write() 해준다.
top


톰캣 인코딩 설정해야 하는 경우

모하니?/Coding : 2008.06.26 17:51


updated 090623

<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" useBodyEncodingForURI="true"/>

--------------------------------------------------------------------------
검색창에 한글만 입력하면 다음과 같은 에러가 발생한다.

org.postgresql.util.PSQLException: ERROR: character 0xc3a7 of encoding "UTF8" has no equivalent in "EUC_KR"

웹 페이지도 EUC-KR로 설정되어 있고 Postgres DB도 EUC-KR을 사용하고 있다. web.xml에 인코딩 필터를 확인해봤더니, 그것도 역시 EUC-KR로 설정되어 있다.

알턱이 없으니 사부에게 문의해서 알아냈다.

톰캣 5.5 이상 부터는 Post 방식에는 인코딩 필터를 적용할 수 있지만 GET 방식은 서버가 connector에 설정한  (server.xml) URIEncoding의 값으로 인코딩 함.

<Connector port="8209" protocol="AJP/1.3" redirectPort="8543" URIEncoding="euc-kr" />

위는 아파치랑 연동되어 있어서 아파치와 연동하는 부분에 인코딩 설정. 아마도 저 값의 기본값이 UTF-8이었거나 아파치쪽의 기본값이 UTF-8 이었나보다. 몰겠다. 자세히는;
top


배치관리 및 실행 구현시 고려할 것.

모하니?/Coding : 2008.06.05 00:00


1. 배치들 간의 의존성

배치들 사이에도 dependency가 존재할 수 있다. 먼저 특정 배치가 돌아야 그 다음 배치를 돌릴 수 있는 그런 관계의 배치 작업. 선행하는 배치가 먼저 실행되었는지, 그 결과는 어떤지에 따라 현재 배치 작업을 실행해야 하는지 패스해야 하는지 알 수 있다.

2. 배치 실행

배치 실행은 쿼츠를 사용하여 주기적으로 배치가 실행되어야 하는 시간을 확인하여 실행한다. Ant로 빌드 스크립트 짜고 리눅스의 클론좝인가로 돌릴 수도 있고, 독립적으로 쿼츠를 실행할 수도 있는데, 스프링을 사용하면  applicationContext.xml에 간단한 빈 설정을 통해 애플리케이션 구동시 쿼츠 스케쥴러를 가동할 수 있다.

3. 배치 결과

배치 결과, 배치 시작 시간, 종료 시간, 시도 횟수, 에러 로그 정보를 기록한다. 이 때 배치작업이 예외를 던지고 뻗어도 배치 결과는 DB에 저장이 되어야하기 때문에, 두 개의 작업은 별도의 트랜잭션으로 다뤄져야 한다. 스프링을 사용한다면, REQUIRED_NEW 를 사용하면 현재 트랜잭션은 잠깐 멈춰두고 새로운 트랜잭션을 만든다. 그리고 그걸 끝내든 롤백하든 마무리하고 기존에 대기해뒀던 트랜잭션을 다시 사용하게 됨으로 이런 경우에 매우 적절하다.

4. 로그 메시지

일관되어 보기 좋은 형태로 로그 메시지를 출력해야 하며, 필요한 지점에 효율적으로 로깅을 해야한다. 로깅의 레벨을 조정하여 info, debug, error 등을 잘 구분하여 메시지를 남기도록하자.

5. 테스트하기

어렵다.
top


스프링을 사용한 트랜잭션 관리 방법

모하니?/Coding : 2008.06.04 22:56


크게 두 가지로 나눌 수 있습니다. 프록시를 사용하는 방법과 그렇치 않은 방법. 좀 더 세밀하게 나누면, 여러 방법이 있지만 구현되어 있는 특징을 기준으로 나누면 AOP를 사용한 방법과 그렇치 않은 방법으로 나눌 수 있고 그 두 방법의 대표자로 각각 @Transactional과 PlatformTransactionManager을 꼽을 수 있습니다.

PlatformTransactionManager
1. 트랜잭션 범위를 세밀하게 조정할 수 있다.
2. 코드가 지져분해진다. 템플릿을 이용하면 어느 정도는 청소할 수 있지만 한계는 있다.
3. 메소드 접근 지시자를 신경쓰지 않아도 된다.
4. 롤백 시킬 예외를 명시적으로 catch 블럭에서 잡아줘야 한다.

@Transactional을 사용할 때 주의
1. 코드가 깔끔해진다.
2. public 메소드만 트랜잭션 처리가 된다는 것을 알아둬야 한다.
3. 세밀하게 적용하려면 해당 부분을 별도의 public 메소드로 빼내야 한다.
4. RuntimeException은 기본으로 롤백해버리고 Catched Exception중에서도 롤백하고 싶은게 있으면 별도의 설정을 필요로 한다.

여기에 하나더, @Transactional을 사용할 때 만약 인터페이스가 아니라 클래스의 프록시 객체를 사용할 때, CGLib을 사용하게 되는데, CGLib 좀 별로임. 성능도 떨어지고 리플랙션 할 떄 문제도 있는 것 같고.. 따라서 프록시 사용할 때는 무조건 인터페이스 기반으로 만들어서 JDK의 프록시 사용하도록 할 것.




top


윈도우 시간과 자바에 찍히는 시간이 안맞을 때

모하니?/Coding : 2008.06.02 00:01


-Duser.timezone=Asia/Seoul

 Timezone.getDefault()를 호출하여 자바 창에 찍어보면 기준시간대가 Asia/Seoul이 아닐 것입니다. 이걸 프로그램 내부에서 Timezone 객체를 사용하여 세팅할 수도 있지만, 위의 인자를 자바 컨테이너 실행시에 옵션으로 줄 수도 있습니다. 웹 애플인 경우에는 후자로, 간단한 독립적인 애플일 경우에는 전자로...

top

TAG 자바,

Spring 2.5 @Controller 사용시 BindingResult 주의 할 것.

모하니?/Coding : 2008.05.14 18:13


스프링 레퍼런스에 BindingResult에 대한 언급은 단 한 줄.
org.springframework.validation.Errors / org.springframework.validation.BindingResult  validation results for a preceding command/form object (the immediate preceding argument).

이게 끝입니다. 한 줄이라고 무시하면 안 됩니다. 진짜 중요한 한 줄입니다.

public String update(@ModelAttribute("model") Foo model, BindingResult result, Bar bar)

public String update(@ModelAttribute("model") Foo model, Bar bar, BindingResult result)

이 두 줄의 코드는 어쩌면 아무런 차이가 없을 수도 있지만 Validator를 만들어 보시면 그 차이를 알 수 있습니다. 무슨 차이인지는 비밀입니다. ㅋㅋ 이미 스프링 레퍼런스에 다 설명이 나와있어서 비밀이랄 것도 없지만 말이죠.ㅋ

힌트 1.
validator.validate(model, result);

힌트 2.
public void validate(Object obj, Errors errors) {
    ...
}


top


OSIV 사용시 주의 할 것

모하니?/Coding : 2008.05.11 23:32


OSIV 기본 지식 - 참조 http://www.hibernate.org/43.html

먼저 OSIV는 Open Session In View 패턴의 약자로 보통 OSIV 필터나 인터셉터 중 하나를 사용합니다. 사용하는 이유는? 뷰 랜더링을 완료 할 때까지 세션을 유지하기 위함이다. 세션이 닫힌 상태에서 프록시로 읽어온 콜렉션이나 레퍼런스의 속성에 접근하면 LazyInitializationException이 발생하고, 이 해결책으로 뷰를 랜더링 하기 위한 세션을 새로 열수도 있겠지만, 이 방법은 그리 좋치 않다. 일단 이 작업이 이전 세션에 포함되어야 적당하지 개별적인 작업 단위로 보기는 뭐시기하기 때문이다. 요청이 오면 새로운 Session과 Transaction을 생성하고 응답을 클라이언트에 보내기 직전에 Transaction을 커밋하고 Session을 닫는게 가장 단순한 OSIV의 기능이 되겠다.

여기에 Conversation을 고려하고 서브 트랜잭션(서브 트랜잭션을 지원한다면, 두 개의 트랜잭션으로 나워서 읽고/쓰기 작업을 하는 트랜잭션과 뷰에 랜더링 하는 읽기 전용 트랜잭션으로 쪼개는 것이 좋다. 쓰기롹을 빨리 반환할 수 있으니까.)그리고 예외 처리까지 고려해서 인터셉터나 필터를 만들어야 한다.

하이버네이트의 update() 기본 지식 - 참조 http://whiteship.tistory.com/1616

Persistent Context에 이미 Persistent 상태로 로딩되어 있는 객체가 있을 때, 그와 같은 id를 가진 객체를 또 다시 Persistent Context에 붙이려는 시도가 있을 때 NonUnique뭐시기 에러가 발생합니다. 흔히 Detached 상태의 객체를 update() 메소드를 사용하여 Persistent Context에 Reattch를 시도할 때 이런 예외가 발생할 수 있는데, 그럴 때는 merge를 하여 기존의 Persistent Context에 있는 객체의 값을 새로운 객체 값으로 덮어쓸 수도 있지만 이 때 merge() 메소드로 넘겨준 객체의 상태가 Persistent 상태로 변하지 않고 그대로 유지 되되며, merge()가 반환하는 레퍼런스와 기존에 Persistent Context에 존재하는 레퍼런스 두 개가 동일한 데이터를 가리키게 됨으로 프로그래밍에 혼란을 줄 수 있다. 따라서 해당 객체를 evict()를 사용하여 Persistent Context에서 빼내고 update()의 인자로 넘겨준 객체를 Persistent 상태로 만드는 것이 적절한 해결책일 것이다.

자... 이제 OSIV 필터를 사용하고 있을 때 Validator에 다음과 같은 코드가 있습니다.

public class MemberValidator implements Validator {
...
  @Autowired
  MemberService memberService;

  ...
  Member memberCommand = (Member)command;
  Member existingMember = memberService.get(memberCommand.getEmail());
  if(existingMember != null)
    errors.reject("email", "duplicated", "해당 이메일은 이미 가입되어 있습니다.");
  ...
 
...
}

위의 코드는 일단 상당히 별로 입니다. memberSerivce.isExistingEmail(memberComman); 라는 메소드를 만들어서 그 반환값을 가지고 조건을 거는게 더 좋은 API로 생각됩니다. MemberService의 isExistingEmail() 에서 데이터베이스에 접근하는 코드는 memberDao를 사용해서 Member 객체를 가져오고 지지고 볶는게 훨씬 좋습니다. 그리고 사실 저런 경우 멤버 객체를 가져올 필요도 없고 레코드 갯수만 가져오면 되겠죠. 그런데 그냥.. 여기서는 그냥. 저렇게 코딩을 했다고 가정하겠습니다.

이런 상황에서 방금 말씀드린 코드의 책임 문제를 떠나 정말 중대한 문제가 있습니다.

그 문제가 뭔지는 알 갈쳐드립니다. 비밀이에요. (ㅋㅋ이미 문제의 원인과 해결책은 위의 기본 이론에 다 설명이 되어 있습니다.) 토비 사부님께 듣기로는 물개 선생님께서도 이와 같은 문제를 겪은 적이 있다는 얘기를 들었습니다. 이런 해프닝을 겪으면서 느낀 건 아무리 공부를 해도 역시.. 코딩을 해봐야... 알 수 있고.. 문제의 원인과 그 원인 해결책은 다시 공부를 해야 이해할 수 있다는 것입니다.

공부와 코딩을 떨어질래야 떨어질 수 없는 친구인거죠.
top


DWR이 제대로 자바스크립트 만들었는지 확인하기

모하니?/Coding : 2008.05.08 15:56


기본 웹 애플리케이션 홈 URL/dwr

로 접속한다.

사용자 삽입 이미지

그럼 DWR이 만들어주는 자바스크립트 들이 보임.

사용자 삽입 이미지

자바스크립트 사용하려면 추가해야 하는 코드까지 나옴. 귿.. 복사해서 붙여넣으면 됨.
execute를 클릭해서 서버에 다녀오는지 확인한 다음 계속해서 개발하러 ㄱㄱㅆ..
top


EasyMock 사용할 때 주의 할 것

모하니?/Coding : 2008.04.29 19:47


    public void foo(Bar bar) {
        ...
        
        bar.toby(whiteship);
        bar.whiteship(toby);
    }

위와 같은 메소드를 테스트 할 때 EasyMock을 사용해서 다음과 같은 테스트를 작성할 수 있습니다.

@Test
public void foo(){
   Bar mockBar = createMock(Bar.class);
   ...
   mockBar.toby(whiteship);
   mockBar.whiteship(toby);
   replay(mockBar);
   a.foo(mockBar);
   verify(mockBar);
}

테스트가 통과할 것만 같은 코드입니다. 그렇쵸? 대부분은 테스트가 통과 합니다. 그런데 통과하지 않는 경우도 있습니다.

java.lang.IllegalStateException: missing behavior definition for the preceeding method call toby(whiteship);

이런 메시지와 함께 테스트가 통과하지 않습니다.

그럴 때는 뭘 확인해 봐야 할까요? Bar 인터페이스에 있는 whiteship과 foo라는 메소드의 리턴타입이 있는지 확인해봐야 합니다. 리턴타입이 있으면 http://whiteship.tistory.com/1504
top

TAG EasyMock

스프링의 getBean() 타입 캐스팅 없애는 방법

모하니?/Coding : 2008.04.28 18:08


 Service myService = (Service) ctx.getBean("service");

대부분 위처럼 캐스팅을 해서 사용합니다. 저는 캐스팅이 정말 싫었습니다.

 Service myService = ctx.getBean(Service.class);

이렇게 캐스팅 없이 사용할 수 있는 방법이 있습니다.

정답은? JavaConfig, http://www.springframework.org/javaconfig, JavaConfigApplicationContext
top


이클립스의 기본 try-catch 탬플릿 비추

모하니?/Coding : 2008.04.26 09:59


Catched Exception이 하두 지저분해서 호출한 쪽에서 계속해서 던지거나 try-catch로 감싸는게 싫어서 던지는 쪽에서 그냥 퀵 픽스를 누르고 try-catch 블럭으로 감싸버리는 실수를 했습니다.

이클립스가 기본으로 제공하는 try-catch 블럭은 예외를 먹어버리는 악성코드입니다. 기능 구현에 집중하고 이런 세세한 부분을 신경 안 쓰다가는 디버깅하기 어려운 코드를 만들게 됩니다. 따라서 왠만하면 사용하지 맙시다. 아니면 template을 수정해서 throw new RuntimeExcepion(e)를 추가해줘도 되겠죠.

이미 알고 있는 내용이었는데도 이런 실수를 하다니...ㅠ.ㅠ

try {
// 위험한 코드
} catch(어떤 예외 e) {
    e.printStackTrace()
}

위의 코드가 이클립스가 제공해주는 코드인데 저러면 안 됩니다. 에러를 찍고 그냥 프로그램이 계속 돌아갑니다. 이게 뭐가 잘 못 된건데?? 라고 하신다면.. ㄷㄷㄷ.. 중요한 코드가 제대로 동작해야 하는데 그렇치 못한 상태에서 프로그램이 계속 돌면 분명 에러 찾기도 어렵고 문제가 있는지 조차 알 수가 없습니다.

그래서..

try {
// 위험한 코드
} catch(어떤 예외 e) {
throw new RuntimeException(e)
}

이렇게 하면 원래 에러를 가진 채 런타임 에러를 던지게 되고 만약 위험한 코드에서 정말 위험한 일이 발생하면 프로그램은 바로 에러를 뱉고 쓰러집니다. 에러를 바로 발견할 수 있고 위처럼 원래 예외 객체를 같이 넘겨줘야 nested excepton으로 원래 예외도 알 수 있습니다.


top


테스트는 진짜로 잘 만들어야 함

모하니?/Coding : 2008.04.25 22:51


오늘 테스트를 "제대로" 만들어야 함을 배웠습니다. 개발하고 배우고 느끼고 공부하고 혼나고 가끔 이렇게 공유도 하고 재밌습니다.

제가 만든 클래스는 제네릭 프로퍼티 에디터로, 특정 객체 타입이 오면 해당 객체가 가지고 있는 id 값을 화면에 보여주기 위한 클래스입니다. 즉 ${member} 라고 JSP에 적어두면 member 객체의 id를 사용합니다.

구현도 잘 했고, 테스트도 잘 했다고 생각해서 기분이 좋았었습니다. 그런데 이게 왠걸.. 실제로 돌아가는걸 보니까 잘 안 되는 것입니다. 제가 만든건 GenericPropertyEditor 입니다. 코드는 공개 안 하려고 했지만 어쩔 수 없네요.ㅋㅋ

아래가 구현한 코드입니다.
    public String getAsText() {
        T entity = (T) this.getValue();
        logger.info("entity = " + entity);
        if (entity == null)
            return "";
        else
            return String.valueOf(SimpleReflectionUtils.getValue(entityClass, entity, "id"));
    }

저걸 테스트 해보려고 다음과 같은 코드를 작성했습니다.
    @Test
    public void getAsText() throws Exception {
        Supp supp = new Supp();
        int id = 10;
        supp.setId(id);
       
        suppPropertyEditor.setValue(supp);
       
        assertEquals(String.valueOf(id), suppPropertyEditor.getAsText());
    }

잘 돌아갑니다.

그런데 분명 위의 코드에는 문제가 있습니다. 무슨 문제인지는 비밀입니다. 스프링, 하이버, @ManyToOne,Lazy Initialization, Proxy, id, getter, field 등을 조합해서 생각하면 문제를 발견하실 수도 있으실 테지만, 전 상상도 못했었습니다.

테스트를 기계적으로 만들지 말고 해당 클래스가 어떤 상황에서 어떤 역할을 하는지 고려해서 작성해야 한다는 걸 느꼈습니다.
top

TAG 테스트

@Configurable 사용시 버그 피해가기

모하니?/Coding : 2008.04.24 15:06


Caused by: java.lang.VerifyError: (class: 머시기/모시기/클래스$$EnhancerByCGLIB$$6dd4e683, method: unique signature: ()L저기기/거시기/클래스;) Inconsistent stack height 1 != 0


참조 : http://forum.springframework.org/showthread.php?t=51455

CGLib으로 만든어진 객체에 위빙을 시도해서 생기는 문제 같은데, 일종의 버그인가봅니다. 이전에 @Configurable 테스트 할 땐 못 본 에러인데 오랜만에 이걸 쓸 일이 있어서 사용하니까 발생하네요.

이 문제를 해결(?)하려면 일단 META-INF 폴더를 클래스 패스 안에 만들어 줍니다. 만든 다음에 클래스 패스에 추가해도 되구요.

다음은 aop.xml 파일을 정의하고 다음과 같이 적어줍니다.

<?xml version="1.0"?>

<!--
    AspectJ load-time weaving config file to install common Spring aspects.
-->
<aspectj>

    <weaver options="-showWeaveInfo" >
        <exclude within="*..*CGLIB*" />
    </weaver>

    <aspects>
        <aspect
            name="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect" />
        <aspect
            name="org.springframework.transaction.aspectj.AnnotationTransactionAspect" />
    </aspects>

</aspectj>

다른 내용은 spring-aspects.jar 파일에 기본으로 들어있는 aop.xml의 내용과 일치하구요. 거기에 <warver> 엘리먼트를 추가해서 CGLIB이 들어간 클래스에는 위빙하지 말라는 설정을 해줍니다.

아.. 근데 이렇게 하면 프록시에는 위빙하지 말라는건데... 이것참 난감하네요. 버그 보니까 해결됐다고 나오는데 저 문제는 왜 발생하는건지..에구구...

top


GenericPropertyEditor 만들려면...

모하니?/Coding : 2008.04.22 21:04


0. PropertyEditor가 뭔지 알아야 함.
1. 왜 PropertyEditor를 쓰는지 알아야 함.
2. PropertyEditor를 자주 사용하는 코드가 있어야 함.
3. 자바의 Generic을 알아야 함.
4. Reflection 알아야 함.
5. ApplicationContext 알아야 함.
6. 스프링 기반 테스트 코드 만들 수 있어야 함.
7. BeanLifeCycle 적절히 이용할 줄 알아야 함.

자바의 Generic이 erasure 방식이라 아쉽지만, 그래도 잘 사용하면 엄청나게 많은 코드가 줄어들고, 코드가 줄어드는 만큼 개발 생산성은 팍팍팍 증가합니다.

Java 5 이상을 못 or 안 사용하고 계신 분들이더라도 괜찮습니다. 굳이 Generic을 사용하지 않고도 비슷한 추상 클래스를 만들어서 사용하시면 됩니다. 대신에 매번 캐스팅 하는 수고는 감수하셔야겠지만 말이죠.

스프링으로 테스트 클래스 만들어가며 GenericPropertyEditor를 만들어 봤습니다. 정말 재미있어서 옆 모니터에 틀어둔 스타리그도 안 보고, 여친님 문자가 왔었는지도 몰랐네요. 소스 코드는 공개하지 않습니다. 헤헷.

ROO보다 멋진 OSAF가 공개되는 날을 기다리며~
top


TDD 연습하기 - RomanNumber

모하니?/Coding : 2008.04.18 14:21


1부터 1000사이의 숫자를 주면 로마숫자로 변환하는 프로그램을 TDD로 개발하시오.

점심시간에 밥 빨리먹고 심심하신 분들 한 번 해보세요. 재밌네요. 캬캬..
토비형님은 자바코드 열댓줄 루비코드 대여섯줄이면 끝난다고 하네요... 워우...

자바코드로 짜실 분들을 위테 테스트코드를 올려드리겠습니다.

        assertEquals("I", maker.convert(1));
        assertEquals("II", maker.convert(2));
        assertEquals("III", maker.convert(3));
        assertEquals("IV", maker.convert(4));
        assertEquals("V", maker.convert(5));
        assertEquals("VI", maker.convert(6));
        assertEquals("VII", maker.convert(7));
        assertEquals("VIII", maker.convert(8));
        assertEquals("IX", maker.convert(9));
        assertEquals("X", maker.convert(10));
        assertEquals("XI", maker.convert(11));
        assertEquals("XII", maker.convert(12));
        assertEquals("XIII", maker.convert(13));
        assertEquals("XIV", maker.convert(14));
        assertEquals("XV", maker.convert(15));
        assertEquals("XIX", maker.convert(19));
        assertEquals("XX", maker.convert(20));
        assertEquals("XXX", maker.convert(30));
        assertEquals("XL", maker.convert(40));
        assertEquals("L", maker.convert(50));
        assertEquals("LX", maker.convert(60));
        assertEquals("LXX", maker.convert(70));
        assertEquals("LXXX", maker.convert(80));
        assertEquals("XC", maker.convert(90));
        assertEquals("C", maker.convert(100));
        assertEquals("CC", maker.convert(200));
        assertEquals("CD", maker.convert(400));
        assertEquals("D", maker.convert(500));
        assertEquals("DCLXVI", maker.convert(666));
        assertEquals("CM", maker.convert(900));
        assertEquals("M", maker.convert(1000));
        assertEquals("MCMXLV", maker.convert(1945));
        assertEquals("MCMXCIX", maker.convert(1999));
        assertEquals("MM", maker.convert(2000));
        assertEquals("MMM", maker.convert(3000));
        assertEquals("ↁ", maker.convert(5000));

간단하지요. 테스트 코드 짜는건 쉬운데.. (위키피디아에 있는 표를 전부 테스트 해봤습니다. 불필요한 것도 있겠지만.. 뭐 어때요. 캬캬캬) 전 구현해보니 60줄 정도 나왔습니다. 토비형님 코드의 4 배... 라인수는 Ctrl + Shift + F 기준입니다.

저녁값 내기로 가장 짧은 코드로 구현하기 뭐 이런거 해도 재밌겠네요. 전 .. 그러고 놀 수 사람이... 없어요. 흑흑.. 외로워... 흑흑흑... 이젠 어려운 자바습이랑 놀아야지.. @.@ 자바습아 이리온...


풀어보신 분들은 저처럼 소스 코드를 올려주세요. 부끄럽지만 제 파일도 올려둡니다. 재밌게 공유해 보아요.


top


Spring 컨트롤러와 request scope bean

모하니?/Coding : 2008.04.16 19:07


스프링 컨트롤러와 request scope관한 이메일이 왔습니다.

...

제가 느끼고 있는 궁금증은 Controller 사용시 bean scope에 관한 부분입니다.

몇번의 프로젝트에서  SpringMVC를 이용하여 프로젝트를 했었는데요.
하나의 Controller에서 요청을 처리를 하기 위해 MultiActionController를 사용했습니다.
 
BaseController를 정의해서 BaseController가 MultiActionController를 상속받도록 만들었고,
모든 Controller는 BaseController를 상속 받아 쓰는 형식으로 구조를 잡았습니다.
 
BaseController에는 handleRequestInternal() 메소드를 오버라이드 해서 모든 request 값을
파싱하여 Map에 담도록 해놓았구요. (map에서 값을 꺼내 요청을 처리하도록 말이죠.)
 
테스트를 위해 Controller 내에서 sleep() 을 준 뒤, 몇개의 요청을 날려보면
가장 나중에 요청된 정보로 앞의 정보들이 변경되더군요.
아마도 scope이 singleton이라 그런것 같더군요.
그래서 빈 설정시에 Controller에 대한 scope을 모두 request로 바꿔줬습니다.
 
<bean id="memberController" class="MemberController" scope="request">
 
이렇게 바꾸어주니 이전과 같은 현상은 발생하지 않더군요.

과연 이렇게 하는 것이 맞는 것인지, 아니면 BaseController를 scope="request"로 만들면 그걸
상속받는 다른 빈도 request가 되는 것인지 정확한 판단이 서질 않더군요.

제가 알기로는 Spring에서 Controller 이용시 threadsafe 한 설계는 개발자의 몫이라고 들었습니다.
어디를 봐도 정확한 가이드가 나와있지 않아서 혼자 헤매다 이렇게 메일을 드리게 되었습니다.

...

그리고 다음과 같이 답변해드렸습니다.


request scope 빈을 써본지가 까마득한데 이런 경우에 유용하게 쓸 수 있겠네요. 내용 공유를 허락해주셔서 감사합니다.
top


ControllerClassNameHandlerMapping 잘 되네~

모하니?/Coding : 2008.04.08 14:54


@Controller
public class MemberController {

    @Autowired
    private MemberService memberService;

    @RequestMapping
    public ModelAndView list(){
        return new ModelAndView("member/list")
            .addObject("members", memberService.getAll());
    }
}

요렇게만 해두면, /member/list.xxx 라는 요청이 오면 알아서(Conversion) 저 메소드가 처리하도록 합니다. ㄴXxx-servlet.xml에는 XML에는

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping" />

애만 등록해주면 됩니다. 저 한 줄의 XML로 URL 맵핑을 손수 적는 수고를 덜 수 있습니다.

계속해서 진화하는 Spring MVC 어디까지 갈텐가~~ 멋있어 멋있어 끝까지 가는거야~~

2008/04/08 - [모하니?/Coding] - ControllerClassNameHandlerMapping가 찾아 준댔는데...
ps : 새벽에 괜히 삽질만 하고... 잠이나 계속 잘껄..
top


ControllerClassNameHandlerMapping가 찾아 준댔는데...

모하니?/Coding : 2008.04.08 04:27


어젠가 오늘 배포된 Spring 2.5.3부터는 ControllerClassNameHandlerMapping의 기능이 확장되서, @Controller로 만든 컨트롤러들도 인식한다고 합니다.

ControllerClassNameHandlerMapping는 스프링의 막강한 CoC 기능 중에 하나로 매번 귀찮게 요청과 요청을 처리할 클래스 맵핑을 하지 않아도 되는 매우 편리한 클래스입니다.

@Controller를 사용하면, 요청을 처리할 클래스나 메소드 위에 @RequestMapping을 사용해서 요청 맵핑을 해야만 했는데, 이제는 저 클래스를 사용해여 Conversion을 적용하면 @RequestMapping을 일일히 안적어 주고, 메소드 이름을 요청의 이름이랑 맞춰주면 됩니다.

그러나.... 테스트 해봤더니 못 찾습니다. 에구구.. 다시 자야지.. 왜 안 돼지; 되면 정말 편한데..

2007/06/19 - [Spring/Chapter 13] - 13.11. Convention over configuration 1
2007/03/02 - [Hibernate/study] - Convention over Configuration




top


스프링 하이버네이트 사용시 insert 문은 안 날아가고 select만 될 때..

모하니?/Coding : 2008.04.02 08:29


Nontransactional 상태에서 SQL을 날리면 그런 일이 발생합니다.

하이버네이트는 Flush 시점에 꼭 필요한 SQL만 날립니다. 그 전에 프로그램에서 필요로 하는 데이터를 위한 SQL 역시 날아갑니다. 이것에 관한건 이전에 Flush에 관한 부분을 정리해두었으니 그 부분을 참조하시면 됩니다.

문제는, get() 류의 SQL들은 바로바로 날아가는데, insert와 update류의 SQL들은 바로 날아가지 않고 Persistence Context에 담겨 있다가 가장 나중에 Write behind 쓴다는 것입니다. 이 때, 만약 트랜잭션 경계를 지정해두지 않고 코딩을 했다면 문제가 복잡해 질 수 있습니다.

트랜잭션 범위를 명시적으로 정해주지 않으면, 사용하는 DB에 따라 동작하는 방식이 다른데, 오라클의 경우 트랜잭션 범위 없이 커넥션을 닫아버리면 그냥 커밋해버립니다. 다른 DB들은 롤백해버립니다. 오라클이 아닌 DB를 사용할 때는 결국 select문만 날아가고 insert 문이 안날아 가는 상황을 목격할 수 있습니다. 이것도 이렇게 단순한게 아닙니다. 주키 생성방식에 따라 insert문이 날아갈 수도 있고 안날아 갈 수 도 있습니다. 주키를 sequence를 사용해서 생성할 때에는 insert문이 날아가지 않고 sequence를 구해오는 SQL 함수만 호출합니다. 주키를 indentity 방식으로 생성할 때에는 insert문을 날려서 주키 값을 가져옵니다.

이렇게 복잡한 상황을 면하려면, 트랜잭션 경계를 잘 설정해 주어야 합니다. 스프링을 사용해서 가장 간단하게 트랜잭션 경계를 설정하는 방법은 다음과 같습니다.

1. sessionFactory 빈 등록
2. <context:annotation-config /> 추가.
3. 트랜잭션 경계가 되는 클래스 위에 @Transactional 애노테이션 붙이기

1, 2번은 간단합니다. 아주 쉽죠. 3번도 간단해보이지만 그렇게 간단하지 않습니다. 아니, 단일 클래스를 사용했을 때는 간단합니다. 그런데 최소한 인터페이스는 사용하겠죠. 그래서 다음과 같은 구조가 있다고 생각해 봅시다.
사용자 삽입 이미지

MemberService는 인터페이스고 그 아래는 그것을 구현한 클래스입니다. 둘 중 어디에 @Transactional을 붙여야 할까요? 스프링 팀은 Concrete 클래스에 @Transactional을 붙이는 것을 권장합니다. 그 이유는 스프링 AOP와 Proxy 그리고 애노테이션과 관련된 것인데, 생략하겠습니다. 암튼 Concrete 클래스에 붙이면 만사 OK 끝입니다.

그럼 인터페이스를 구현한게 아니라, 클래스 상속 구조에서는 @Transactional을 어디에 붙여야 할까요?

사용자 삽입 이미지

고민입니다. 어디에 붙여야 할지...
1. 일단 MemberServiceImpl에만 붙이면 안 됩니다. 그럼 AbstractMemberService에 있는 메소드들은 트랜잭션 경계를 설정하지 않은 꼴이 됩니다.

2. 그렇다고 AbstractMemberService에만 붙이자니 그것도 안 됩니다. MemberServiceImpl에만 있는 메소드들에는 마찬가지로 트랜잭션 경계를 설정해주지 않을 꼴이 되니까요.

3. 그럼.. AbstractmemberService와 MemberServiceImpl에 있는 모든 public 메소드에 @Transactional을 붙여줄까요? 이 애노테이션은 public 메소드 위에도 붙일 수 있습니다.(사실 모든 메소드 위에 붙일 수 있지만, public 메소드에만 트랜잭션 경계를 적용시켜주고 다른건 무시합니다.) 귀찮습니다. 메소드 추가할 때마다 애노테이션 한 줄씩 넣기가 귀찮습니다.

4. 그럼 두 개의 클래스 위에 다가 @Transactional을 붙여버리자!! 빙고.. 그런데 프록시 많이 사용하는건 그다지 성능에 안 좋지 않나. 게다가 프록시는 self-invocation문제도 있는데..

5. 그럼 @Transactional을 두 개의 클래스 위에 붙이고 Spring AOP말고 AspectJ를 써야겠다. 근데 AspectJ 사용이 2.5에서 편해지긴 했는데 여전히 뭔가 귀찮단 말이지..

여기까지가 고민의 끝입니다.
top


자바스크립트 줄바꿈 버그 해결과 원인 발견

모하니?/Coding : 2008.03.26 23:42


form에서 String 타입의 변수에 입력할 때 엔터키를 눌러서 줄바꿈을 했다면 DB에 들어갈 때 캐리지 리턴이 포함되어 들어갑니다. 그런데 그 데이터를 화면에 뿌릴 때 자바스크립트에서 그걸 그대로 화면에서 줄바꿈을 해버려서 배열이 이상하게 되서 화면에 그리드가 나와야 하는데 undefined 라는 글자가 찍히고 "화면에 에러가 있습니다."라고 친절하게 알려줍니다.

해결하는 방법은 간단합니다. 자바 코드에서 해당 데이터를 가져오는 게터에서 다음과 같은 코드를 추가합니다.
캐리지 리턴을 모두 찾아서 한 칸짜리 공백으로 바꿔줍니다.

replaceAll("\r\n", " ");

진짜 어려운건 대체 저 버그가 발생했을 때 저런 이유로 인해 그런 결과가 나왔다는 것을 어떻게 금방 알아내느냐 인데.. 전 그걸 못해서 거의 일주일간 이 버그를 방치해두고 있었습니다. 그런데 제 사수님께서 거의 5분만에 원인을 알아내고 해결책까지 제시하는 걸 보고(한 두 번도 아니지만..) 정말 놀랐습니다. 그래서 어떻게 찾아내는 건지 물어봤더니 에러난 화면의 HTML 소스보기로 보니까 보인다고 해서 봤더니 정말 해당 데이터가 HTML 코드에서 갑자기 줄바꿈을 해버려서 눈에 튀더군요;;;

음.. 버그를 만났을 때 대처하는 방법과 자세에서 배울 것들이 정말 많습니다. 물론 그 밖에도 배울 것들이 많지요. 장인을 보고 있는 기분이랄까요... 장인어른 말구요.
top


Hibernate의 Dynamic Instantiation 사용하기

모하니?/Coding : 2008.03.26 23:15


s.createQuery(
"select item.itemid, count(foo), sum(bar) " +
"form Item item " +
...

뭐 이런 HQL이 있을 때 s.list() 의 결과는 Object[] 의 리스트입니다.
따라서 저기서 값을 꺼내서 새로운 객체 넣어줄 때 다음과 같은 코드를 작성하게 됩니다.

List list = s.list();
for(int i = 0 ; i < list.size() ; i++) {
    Object[] result = list.get(i)
    Baz baz = new Baz();
    baz.setItemId((Integer)result[0]);
    baz.setFoo(((Long)result[1]).intValue());
    baz.setBar(((Double)result[2]).intValue());
    ...
}

배열에서 일일히 꺼내는 코드가 짜증날 뿐만 아니라, 꺼내는 데이터 타입이 count일 경우에는 Long 타입이 나오고 sum의 경우에는 Double 타입의 값이 나오기 때문에, 가장 자주 사용하는 Integer로 세팅해야 하는 경우에는 위와 같이 어지러운 괄호를 사용해가며 파싱을 하고 다시 int값으로 세팅해줘야 합니다. 번거로운건 둘째치고 코드가 지져분해졌습니다.

좀.. 짜증나는 코드입니다. 마치 JDBC의 ResultSet 쓰듯이 사용한건데, 저는 JDBC 라이브러리를 쓰지 않고 엘레강트한 하이버네이트를 쓰고 싶었는데, 하이버네이트에서도 이건 어쩔 수 없는건가 하면서 무심코 그냥 저렇게 코딩을 했었습니다.

그런데.. 정말 엘레강트한 방법이 있더군요...

하이버네이트의 "다이내믹 인스턴세이션" Dynamic Instantiation 이라는 기능을 사용하면 정말 깔끔하게 코딩할 수 있습니다.

먼저 DTO를 만듭니다.

class DTO {

private Integer itemId;

private Integer foo;

private Integer bar;

public DTO(Integer itemId, Long foo, Double bar) {
    // parsing
}

// setter and getter

}

그리고 HQL을 다음과 같이 수정합니다.

select new whiteship.baz.DTO(item.itemId, count(foo), sum(bar))
...//나머지 동일

그러면 이제 저 쿼리의 결과인 s.list()를 호출하면 DTO 객체들의 컬렉션을 받을 수 있습니다. 즉 List<DTO> 타입으로 받을 수 있죠. 그러면 파싱하는 쪽 코드가 깔끔해집니다.

1. ForEach 문 사용가능.(한 줄이 줄어 들고,  for문이 짧아짐, i라는 변수 사라짐)
2. 형변환 하지 않아도 됨.(DTO의 생성자에서 하기 때문에 지져분한 코드 사라짐)

음.. 하이버네이트는 정말 엘레강트 한 것 같습니다. 
Hibernate is ROCK!!!

ps : 14장에 나오는 내용인데, 현재 10장을 보고 있습니다. 10장의 트랜잭션과 동기화, 11장 컨버ㄹ세이션보다 14, 15장의 쿼리에 관한 내용이 먼저 나왔으면 좋치 않았을까 싶습니다. 책을 끝까지 보기 전까지는 하이버네이트 쓸 생각을 하지 말라는 걸지도.. ㅋㅋㅋ
top




: 1 : ··· : 4 : 5 : 6 : 7 : 8 : 9 : 10 :