Whiteship's Note


조금 친절한 코드 생성기 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