잊지 않겠습니다.

매번 코드를 작성할 때마다 까먹어서 정리합니다. 기억력이 감퇴가 된것 같네요.

  1. MessageSource를 선언. refresh를 위해 ReloadableResourceBundleMessageSource를 사용하는 것이 정신건강상 좋다.
  2. LocaleChangeInterceptor를 선언
  3. addInterceptors method를 override 시켜, interceptor를 추가
  4. AcceptHeaderLocaleResolver, CookieLocaleResolver, SessionLocaleResolver 중 하나를 선택하여 LocaleResolver를 구현

AcceptHandlerLocaleResolver

기본적인 Http Header인 Accept-Language를 이용하는 방법입니다. 기본값이 AcceptHeaderLocaleResolver로 되어 있습니다. 가장 구현이 간단하나, parameter를 통해서 동적으로 Locale을 변경하지 못합니다. UnsupportedOperationException이 발생합니다. 이를 테스트 하기 위해서는 Browser의 설정중에 언어 셋을 변경시켜야지 됩니다.

CokieLocaleResolver

Cookie를 이용하는 방식으로, Cookie name, expired time 등을 설정 가능합니다.

SessionLocaleResolver

Session값을 이용하는 방법으로 서버의 부하측면이 적다면 가장 좋은 방법인것 같습니다.

@Configuration code는 다음과 같습니다.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {
        "me.xyzlast.web.controllers"
})
public class ControllerConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/lib/**").addResourceLocations("/lib/**");
        super.addResourceHandlers(registry);
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() throws IOException {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
        super.addInterceptors(registry);
    }

    @Bean
    public AcceptHeaderLocaleResolver acceptHeaderLocaleResolver() {
        return new AcceptHeaderLocaleResolver();
    }

    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setBasename("classpath:messages");
        messageSource.setFallbackToSystemLocale(false);
        return messageSource;
    }
}


Posted by Y2K
,

국내 SI 개발환경에서의 거의 100이면 99정도 myBatis를 이용한 DB Handling을 하고 있습니다. 개인적으로는 상당한 ORM 빠돌이라서… 왜 ORM을 사용하지 않지? 라는 의문을 항시 가지고 있다가 다른 여러 사람들 및 개인적인 생각을 정리해서 한번 올려봅니다.

ORM에 대한 learning curve

먼저, 개발자들에 대한 개인 책임이 어느정도 있다고 생각합니다. 근처의 여러 사람들을 만나보면, 개발자들중 3년이 지나고 나서 공부를 꾸준히하는 개발자들을 찾기 힘들다는 이야기를 자주 듣습니다. 현업에서 주로 사용되고 있는 myBatis 이외에 자신이 찾아서 공부를 하는 사람들의 수가 의외로 적은 것이 한 이유가 아닐까 생각됩니다.

이는 철저히 개발자들의 문제점이 아닐까 라는 생각을 가지고 있습니다. 한해한해 바뀌어가는 개발환경에 대해서 자기 자신을 꾸준히 업그레이드해두지 않으면 자연스럽게 도태가 되어버리는 것이 현 상황인데, 한가지 기술로 너무나 오랫동안 욹어먹는 것 역시 현 IT 환경에서의 죄악 이지 아닐까 싶습니다.

수주에 따른 SI 개발이기 때문에 초기 Reference가 없다.

공적인 분야에서의 대규모 SI를 토대로 성장한 국내 IT 개발환경은 초기 Reference가 매우 중요합니다. 대부분 SQL을 그대로 사용하는 개발환경이 지금까지 이어져왔고, 이러한 개발환경에 대한 reference를 요구하기 때문에, 어쩔수 없이 myBatis를 이용하게 되는 것이 당연하게 이어져오고 있습니다.

이건 어떻게 하면 깰수 있을까요? 솔찍히 이 부분이 개인적으로는 가장 답이 안나옵니다. 공공부분 SI의 경우, reference를 요구하는 것이 당연하다고 생각됩니다. 그렇지만, 이러한 reference 요구에 의해서 신기술의 발전 및 보다 나은 기술의 채택부분이 막힌다면 이거 역시 문제가 아닐까 싶습니다. 점진적으로 해결해 나간다면, 개발자들이 최대한 ORM을 이용해서 공공부분 SI 이외의 현장에서 reference를 쌓는 것이 필요하다고 생각합니다. 지금까지 시장에셔와는 다르게 외부에서의 reference를 이용해서 공공부분에 들어가보는 것 이외에는 조금 답이 안나오는 것 아닌가 싶습니다.

기존 BL이 SQL로 구성되어 있기 때문에

가장 막막하고, 답답한 부분입니다. 기존의 로직이 SQL로 구성되어 있기 때문에 그 SQL을 사용하기 위해서 myBatis를 이용해야지 된다. 라는 의견입니다. 이에 대해서는 전 조금 다른 관점을 보고 싶습니다.

국내는 유독 차세대라는 개발 Project들이 많습니다. 해외에는 차세대라는 말을 붙일 수 있을 정도로 모든 시스템을 갈아엎는 Project들이 별로 없습니다.

그 이유가 무엇일까요?
해외는 대부분 점진적인 개량을 통해서 시스템을 항상 최신으로 유지를 시키거나, 계속적인 성능 개선을 해오고 있기 때문이라고 합니다. 국내의 사정은 어떤가요? 국내는 대부분 SI/SM으로 개발자의 직군이 나뉘게 됩니다. 전자는 주로 개발을, 후자는 주로 운영업무를 하게 됩니다. 그런데, 이 SM 업무의 경우에 대부분 신규로 무언가 개발을 하는 일을 막아버립니다. 업무로 인해서요. 개발이 필요한 상황이 있으면 개발팀에. 라는 것이 일반적인 상황이지요. 실리콘밸리의 새로운 trend라고 불리우고 있는 devOps의 경우, 이러한 용어로 만들어진 것은 얼마 안되었지만 기존까지 이런 식의 조직운영은 꾸준히 계속되어가고 있던것으로 알고 있습니다. 운영팀에서 보다 나은 개발방향을 위해서 시스템을 점진적으로 업그레이드 하고 변경시키는 과정을 이미 행하고 있는겁니다. 그렇지만, 국내의 SM환경은 대부분 어떻습니까? 대부분이 업무처리에 대한 전화상담 및 그에 따른 DB rollback, sql query문 작성에 대부분의 시간을 보내게 되는 것이 사실입니다.

이 문제는 제 개인적으로는 다음 문제와도 연결이 되고 있다고 생각합니다.

개발에 대한 사회적인 인식 문제

현 상황에서 우리 사회는 개발을 한번에 큰 돈을 들여서 하고, 그 다음에는 돈을 안쓰는 것이라고 인식하고 있습니다. 물론 개발에는 초창기 큰 돈이 듭니다. 그렇지만, 유지보수에 더욱더 많은 돈과 시간이 들게 되는 것또한 사실이지만, 국내에는 전혀 이런 사실이 먹히지 않고 있습니다. 유지보수 비용측정만봐도 알 수 있지요.

위 인식때문에, 개발을 할때 대부분이 외주개발자 또는 프리랜서들을 사용해서 대규모 프로젝트를 수행합니다. 그리고, 개발이 마쳐지면 이 사람들이 모두 이 업무에서 손을 떼게됩니다. 개발에 대한 내부적인 역량자체가 거의 없어지는거지요. 그리고 큰 돈을 들인 Application이 정상적으로 돌아가기 위한 산소호흡기만 붙인 상태로 유지를 시켜나가게 되는거지요. 점진적인 개선같은 것은 꿈도 못 꾸는 경우가 많습니다.

거기에다 우리나라는 자산에 대한 선호도가 매우 강합니다. 매우 잘 정제되고 훌륭한 Application이 아닌 시스템과 DB, 즉 자산에 해당되는 곳에는 매우 큰 돈을 쓰지만, 무형자산이라고 할 수 있는 Application에 대한 홀대는 매우 심각한 편입니다. 다른 이야기를 한다면, 이런 이유로 인하여 국내 기업환경에서 내부 데이터에 대한 Cloud는 매우 험난한 길을 가야지 될 것같다는 예상을 합니다. 국내 SI 개발환경에서 개발자들에게 투자한다는 이야기는 정말로 들어본적이 없습니다. 대부분 서버나 DB를 얼마나 비싼것을 샀는지를 자랑하는 기사들을 많이 봤지요.

솔찍히 이 문제는 국내의 천박한 자본주의를 보여주는 것이 아닌가도 싶습니다. 개발을 하는 사람들과 그 사람들의 노력이 귀한줄을 모르고 있는 것이 아닌가 싶어요.

마치면서…

ORM이 아닌 myBatis에 대한 의존 문제 자체가 개인적으로는 국내 IT 발전상황을 가로막는 일중 하나가 되지 않을까 생각하고 있습니다. 당장 open source로 무언가 새로운 web framework가 나오게 되면, 그 다음에 바로 나오는 것이 ORM입니다. DDD를 이용한 ORM modeling을 잘 할 줄 아는 사람들은 결국은 객체에 대한 이해가 좀더 나은 사람들이였던것이 제 개인적인 경험들이였습니다.

이제 ORM은 보다 더 나은 개발자가 되기 위한 조건이 아니라, 필수가 되어가고 있는 것 같은데… 국내 환경은 앞으로도 어떻게 되어갈까요.

Posted by Y2K
,

Bower는 maven, gradle을 이용한 java에서의 open source library의 버젼관리를 javascript client library에 옮겨온 개념입니다.

bower 소개

Bower는 maven, gradle과 비슷하게 bower.json과 .bowerrc 두개의 파일로 설정이 관리가 됩니다.

  • .bowerrc : Bower에서 source와 dist를 다운받을 위치를 지정합니다.
  • bower.json : Bower에서 다운받을 외부 component의 버젼 및 Project의 name, version을 지정합니다. 이는 maven, gradle에서 project와 비슷한 값을 지정하게 됩니다.

.bowerrc

{
  "directory": "components"
}

bower.json

{
  "name": "my-project",
  "version": "0.0.1",
  "dependencies": {
    "modernizr": "~2.6.2",
    "bootstrap": "~2.2.2",
    "angular-strap": "~0.7.0",
    "angular-ui" : "0.4.0",
    "ngInfiniteScroll" : "1.0.0",
    "angular-underscore" : "",
    "underscore" : "",
    "angular-bootstrap" : "~0.2.0",
    "font-awesome" : "3.0.2",
    "emoji" : "0.1.2"
  }
}

bower의 설치

bower는 node.js를 이용해서 작성되어있습니다. 먼저 node.js를 설치하는 것이 필요합니다. node.js의 설치방법은 너무나 많은 곳들에서 소개가 되고 있기 때문에 넘어가기로 하겠습니다.
Bower는 전역으로 사용되는 application이기 때문에, npm install -g를 이용해서 설치를 해야지 됩니다. 최종적으로 사용하기 위해서는 bower-installer 역시 같이 설치해주는 것이 좋습니다.

sudo npm install -g bower
sudo npm install -g bower-installer

bower의 활용

기본적으로 mvnrepository와 같이 사용할 library를 http://bower.io/search/ 에서 검색해서 찾는 것이 가능합니다. bower를 이용해서 library를 설치하기 위해서는 다음 command를 실행하면 됩니다.

bower install jquery

위는 jquery를 지정해서 설치하게 됩니다. 버젼이 따로 적혀있지 않는 경우, 가장 최신의 버젼을 다운받아 사용하게 됩니다. 특정하게 버젼을 지정하기 위해서는 다음과 같이 작성할 수 있습니다.

bower install jquery#1.11.0

설치와 bower.json 파일 기록을 동시에 할 수 있습니다.

bower install jquery#1.11.0 --save
bower install jquery#1.11.0 -s

bower-installer

bower를 통해서 component들을 설치하게 되면 source 코드와 min file 모두가 다운받게 됩니다. 그렇지만, 사용하게 될 component 들은 대다수 min file들만이 사용되게 됩니다. 이를 위해 설치한 component들의 min file 들만을 사용할 위치로 copy 해주는 tool이 bower-installer 입니다. bower-install는 bower.json에서 설정하며, 다음과 같이 설정할 수 있습니다.

    "install" : {
        "path" : {
            //"파일 확장자" : "copy될 위치"
            "css" : "src/main/webapp/lib/css",
            "js" : "src/main/webapp/lib/js",
            "woff" : "src/main/webapp/lib/fonts"
        },
        "sources" : {
            "component 이름" : [
                "copy 할 file 이름"
            ]
        }
    }

bower.json을 수정한 후, bower-installer를 실행하면 위치에 맞게 file이 모두 copy되는 것을 볼 수 있습니다.

bower와 gradle java war project의 연결

gradle을 이용한 war project의 경우, src/main/webapp에 사용되는 모든 code들과 javascript가 위치하게 됩니다. 여기 위치에 bower_component를 모두 다 다운받아서 처리하게 되면 url이 지저분해지는 경향이 있습니다. 이를 개발 방향에 맞게 정리해주는 것이 필요합니다. 이 때, bower-installer를 사용하면 원하는 directory 구조를 만들 수 있습니다.

제가 개인적으로 생각하는 좋은 구조는 다음과 같습니다.

project
- src
    - main
        - java
        - resources
        - webapp
    - test
        - java
        - resources
 .bowerrc
 .bower.json
 build.gradle
 setting.gradle

.bowerrc

{
    "directory" : "bower_components"
}

bower.json (bootstrap과 jquery를 설치한 상태입니다.)

{
    "name" : "bookstore-web",
    "version" : "0.0.0.1",
    "dependencies" : {
        "jquery" : "1.11.0",
        "bootstrap" : "3.1.1"
    },
    "install" : {
        "path" : {
            "css" : "src/main/webapp/lib/css",
            "js" : "src/main/webapp/lib/js",
            "eot" : "src/main/webapp/lib/fonts",
            "svg" : "src/main/webapp/lib/fonts",
            "ttf" : "src/main/webapp/lib/fonts",
            "woff" : "src/main/webapp/lib/fonts"
        },
        "sources" : {
            "jquery" : [
                    "bower_components/jquery/dist/jquery.min.js"
                ],
            "bootstrap" : [
                    "bower_components/bootstrap/dist/css/bootstrap.min.css",
                    "bower_components/bootstrap/dist/css/bootstrap-theme.min.css",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff",
                    "bower_components/bootstrap/dist/js/bootstrap.min.js"
                ]
        }
    }
}

위와 같이 설정해주면 bower-installer를 실행하면 src/main/webapp/lib 안에 모든 javascript component를 위치할 수 있게 됩니다. 그런데, 이 과정 역시 gradle에 추가하는 것이 보다 더 사용하기 편합니다. 다음과 같이 gradle task를 추가합니다.

import org.apache.tools.ant.taskdefs.condition.Os
task bowerInstaller(type:Exec) {
    if(Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', 'bower-installer'
    } else {
        commandLine 'bower-installer'
    }
}

이제 gradle bowerInstaller commmand를 통해 bower-installer를 실행할 수 있습니다. bowerInstaller를 따로 실행시켜줘도 좋지만, war로 배포될 때 자동으로 포함될 수 있도록 war task에 dependency를 추가하도록 합니다.

war {
    dependsOn bowerInstaller
}

이제 gradle war를 통해서도 모든 component들을 같이 관리할 수 있습니다. gradle의 놀라운 확장성에 대해서 다시 한번 감탄하게 됩니다. ^^

마치면서

bower는 java에서의 maven, gradle과 같이 dependencies에 대한 관리 툴입니다. 따라서 기존 관행처럼 모든 javascript를 SCM에 올릴 필요가 더이상 없어집니다. 버젼 관리와 같이 개발되는 application에서 사용되고 있는 component에 대한 관리와 같이 maven, gradle을 사용할 때와 같은 매우 멋진 작업들이 가능하게 됩니다. 모두 bower를 한번 써봅시다. ^^

Posted by Y2K
,

Spring에서 xml이나 annotation을 이용한 component-scan에 의해서 등록되는 Bean중에서 @PersistenceContext의 동작원리는 다음과 같습니다.

ComponentScanBeanDefinitionParser

ComponentScanBeanDefinitionParser.registerComponents를 통해 Bean을 등록시켜준다. annotationConfig가 enable된 상태에서 context의 Bean들을 scan시켜줍니다.

protected void registerComponents(
        XmlReaderContext readerContext, Set<BeanDefinitionHolder> beanDefinitions, Element element) {

    Object source = readerContext.extractSource(element);
    CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source);

    for (BeanDefinitionHolder beanDefHolder : beanDefinitions) {
        compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder));
    }

    // Register annotation config processors, if necessary.
    boolean annotationConfig = true;
    if (element.hasAttribute(ANNOTATION_CONFIG_ATTRIBUTE)) {
        annotationConfig = Boolean.valueOf(element.getAttribute(ANNOTATION_CONFIG_ATTRIBUTE));
    }
    if (annotationConfig) {
        Set<BeanDefinitionHolder> processorDefinitions =
                AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source);
        for (BeanDefinitionHolder processorDefinition : processorDefinitions) {
            compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition));
        }
    }

    readerContext.fireComponentRegistered(compositeDef);
}

AnnotationConfigUtil

AnnotationConfigUtil을 통해 Annotation이 설정된 Bean들을 얻어오기 시작합니다. registerAnnotationConfigProcessors에 의해서 각각의 Type에 따라 다른 등록을 Process 처리하게 됩니다.

if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}

if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}

if (!registry.containsBeanDefinition(REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(RequiredAnnotationBeanPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}

// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}

// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition();
    try {
        ClassLoader cl = AnnotationConfigUtils.class.getClassLoader();
        def.setBeanClass(cl.loadClass(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME));
    }
    catch (ClassNotFoundException ex) {
        throw new IllegalStateException(
                "Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
    }
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
}

처리되는 Bean들은 다음과 같습니다.

  • CONFIGURATIONANNOTATIONPROCESSORBEANNAME : @Configuration이 적용된 객체들을 처리하는 Process 입니다.
  • AUTOWIREDANNOTATIONPROCESSORBEANNAME : Bean 객체들의 @Autowired가 적용된 객체들을 처리하는 Process 입니다.
  • REQUIREDANNOTATIONPROCESSORBEANNAME : @Required Annotation이 처리됩니다.
  • COMMONANNOTATIONPROCESSORBEANNAME : EJB, JNDI 지원 객체들의 Property들을 처리하는 Process 입니다.
  • PERSISTENCEANNOTATIONPROCESSORBEANNAME : JPA를 처리합니다. 실질적으로 JPA의 EntityManagerFactory 객체의 경우에는 PersistenceAnnotationBeanPostProcess 에서 처리하게 됩니다.
PersistenceAnnotationBeanPostProcess

이제 마지막 코드입니다. PersistenceAnnotationBeanPostProcess는 @PersistenceContext로 지정된 Property에 EntityManagerFactory에서 새로운 EntityManager를 생성하거나, Transaction에 의해 기존ㅇ에 생성된 EntityManager를 반환시켜서 사용하게 해줍니다. 이에 대한 코드는 다음과 같습니다.

먼저, EntityManagerFactory를 생성하고 주입하고 있습니다.

public PersistenceElement(Member member, PropertyDescriptor pd) {
    super(member, pd);
    AnnotatedElement ae = (AnnotatedElement) member;
    PersistenceContext pc = ae.getAnnotation(PersistenceContext.class);
    PersistenceUnit pu = ae.getAnnotation(PersistenceUnit.class);
    Class<?> resourceType = EntityManager.class;
    if (pc != null) {
        if (pu != null) {
            throw new IllegalStateException("Member may only be annotated with either " +
                    "@PersistenceContext or @PersistenceUnit, not both: " + member);
        }
        Properties properties = null;
        PersistenceProperty[] pps = pc.properties();
        if (!ObjectUtils.isEmpty(pps)) {
            properties = new Properties();
            for (PersistenceProperty pp : pps) {
                properties.setProperty(pp.name(), pp.value());
            }
        }
        this.unitName = pc.unitName();
        this.type = pc.type();
        this.synchronizedWithTransaction = (synchronizationTypeAttribute == null ||
                "SYNCHRONIZED".equals(ReflectionUtils.invokeMethod(synchronizationTypeAttribute, pc).toString()));
        this.properties = properties;
    }
    else {
        resourceType = EntityManagerFactory.class;
        this.unitName = pu.unitName();
    }
    checkResourceType(resourceType);
}

다음에, 각 Resource에 EntityManager를 주입합니다.

@Override
protected Object getResourceToInject(Object target, String requestingBeanName) {
    // Resolves to EntityManagerFactory or EntityManager.
    if (this.type != null) {
        return (this.type == PersistenceContextType.EXTENDED ?
                resolveExtendedEntityManager(target, requestingBeanName) :
                resolveEntityManager(requestingBeanName));
    }
    else {
        // OK, so we need an EntityManagerFactory...
        return resolveEntityManagerFactory(requestingBeanName);
    }
}

최종적으로 얻어지는 EntityManager는 resolveEntityManager method를 통해서 주입되게 됩니다. EntityManager의 코드는 다음과 같습니다.

private EntityManager resolveEntityManager(String requestingBeanName) {
    // Obtain EntityManager reference from JNDI?
    EntityManager em = getPersistenceContext(this.unitName, false);
    if (em == null) {
        // No pre-built EntityManager found -> build one based on factory.
        // Obtain EntityManagerFactory from JNDI?
        EntityManagerFactory emf = getPersistenceUnit(this.unitName);
        if (emf == null) {
            // Need to search for EntityManagerFactory beans.
            emf = findEntityManagerFactory(this.unitName, requestingBeanName);
        }
        // Inject a shared transactional EntityManager proxy.
        if (emf instanceof EntityManagerFactoryInfo &&
                ((EntityManagerFactoryInfo) emf).getEntityManagerInterface() != null) {
            // Create EntityManager based on the info's vendor-specific type
            // (which might be more specific than the field's type).
            em = SharedEntityManagerCreator.createSharedEntityManager(
                    emf, this.properties, this.synchronizedWithTransaction);
        }
        else {
            // Create EntityManager based on the field's type.
            em = SharedEntityManagerCreator.createSharedEntityManager(
                    emf, this.properties, this.synchronizedWithTransaction, getResourceType());
        }
    }
    return em;
}

항시 SharedEntityManager를 이용해서 @Transactional annotation과 같이 사용할 수 있도록 동작하고 있는 것을 볼 수 있습니다. 코드를 보시면 아시겠지만, JPA를 사용하실 때는 반드시 @PersistenceContext annotation을 이용해야지 됩니다.

Posted by Y2K
,

기본적인 CRUD를 지원하는 GenericDAO를 구성하는 코드의 비교를 해볼려고 합니다.

일단 기술 배경으로는 Spring을 사용하고, Spring내에서의 Hibernate, JPA, 그리고 Hibernate를 이용한 queryDSL 코드간의 GenericDao를 비교해보고자 합니다.

기본적으로 Entity는 BaseEntity를 상속하는 것을 원칙으로 갖습니다. DB에서 생성되는 단일키를 사용하는 것으로 기본 구상을 잡고 들어가면 GenericDao의 interface는 다음과 같이 구성될 수 있습니다.

public interface EntityDao<T extends BaseEntity> {
    List<T> getAll();
    void deleteAll();
    T getById(int id);
    void add(T entity);
    void update(T entity);
    int countAll();
}

이제 이 interface를 구현한 AbstractDao class를 각 3개의 기술로 따로 구현해보고자 합니다.

Hibernate

Hibernate는 SessionFactory를 통해 생성되는 Session을 기반으로 구성합니다. Spring을 이용하기 때문에 SessionFactory.getCurrentSession() method를 이용해서 Session을 구해와서 처리합니다.

약간의 특징이라고 할 수 있는 것이 Criteria를 이용하는 코드의 경우, Class<?>를 반드시 각 EntityDao 생성 객체에서 넘겨주는 것이 필요합니다.

public abstract class AbstractSessionFactoryDao<T extends BaseEntity> implements EntityDao<T> {

    @Autowired
    protected SessionFactory sessionFactory;
    private final Class<?> clazz;

    protected AbstractSessionFactoryDao(Class<?> clazz) {
        this.clazz = clazz;
    }


    @Override
    public List<T> getAll() {
        Session session = sessionFactory.getCurrentSession();
        return session.createCriteria(clazz).list();
    }

    @Override
    public void deleteAll() {
        List<T> items = getAll();
        Session session = sessionFactory.getCurrentSession();
        for (T item : items) {
            session.delete(item);
        }
    }

    @Override
    public T getById(final int id) {
        Session session = sessionFactory.getCurrentSession();
        return (T)session.get(clazz, id);
    }

    @Override
    public void add(final T entity) {
        Session session = sessionFactory.getCurrentSession();
        session.save(entity);
    }

    @Override
    public void update(final T entity) {
        Session session = sessionFactory.getCurrentSession();
        session.update(entity);
    }

    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        Long count = (Long) session.createCriteria(clazz)
                .setProjection(Projections.rowCount())
                .uniqueResult();

        if(count == null) {
            return 0;
        } else {
            return count.intValue();
        }
    }
}
JPA

jpa를 이용하는 경우, @PersistenceContext 를 이용해서 EntityManager를 등록해서 사용해주면 됩니다. Spring은 @PersistenceContext를 확인하면, 등록된 EntityManagerFactoryBean은 EntityManager를 사용할때마다 EntityManagerFactory에서 얻어와서 사용하게 됩니다. 이는 Spring에서 핸들링시켜줍니다. 이에 대한 코드 분석은 다음 Post에서 한번 소개해보도록 하겠습니다.

JPA를 이용한 GenericDao class의 코드는 다음과 같습니다.

public abstract class AbstractJpaDao<T extends BaseEntity> implements EntityDao<T> {

    protected AbstractJpaDao(String entityName) {
        this.entityName = entityName;
    }

    @PersistenceContext
    protected EntityManager em;

    @Autowired
    protected JpaExecutor executor;
    protected final String entityName;

    @Override
    public List<T> getAll() {
        return em.createQuery("from " + entityName).getResultList();
    }

    @Override
    public void deleteAll() {
        System.out.println(em);
        em.createQuery("delete from " + entityName).executeUpdate();
    }

    @Override
    public T getById(final int id) {
        Query query = em.createQuery("from " + entityName + " where id = :id");
        query.setParameter("id", id);
        return (T) query.getSingleResult();
    }

    @Override
    public void add(final T entity) {
        em.merge(entity);
    }

    @Override
    public void update(final T entity) {
        em.merge(entity);
    }

    @Override
    public int countAll() {
        return em.createQuery("from " + entityName).getResultList().size();
    }
}
Hibernate기반의 queryDSL

GenericDAO class를 만들기 위해서 QueryDsl에서 가장 문제가 되는 것은 QClass들의 존재입니다. Generator에서 생성되는 Q Class들은 EntityPathBase class에서 상속받는 객체들입니다. 따라서 다른 GenericDAO class 들과는 다르게 2개의 Generic Parameter를 받아야지 됩니다.

다음은 queryDSL을 이용한 GenericDAO code입니다.

public abstract class AbstractQueryDslDao<T extends BaseEntity, Q extends EntityPathBase<T>> implements EntityDao<T> {

    @Autowired
    protected SessionFactory sessionFactory;
    protected final Q q;
    protected final Class<?> clazz;

    protected AbstractQueryDslDao(Class<?> clazz, Q q) {
        this.clazz = clazz;
        this.q = q;
    }

    protected HibernateQuery getSelectQuery() {
        return new HibernateQuery(sessionFactory.getCurrentSession()).from(q);
    }

    protected HibernateDeleteClause getDeleteQuery() {
        return new HibernateDeleteClause(sessionFactory.getCurrentSession(), q);
    }

    protected HibernateUpdateClause getUpdateQuery() {
        return new HibernateUpdateClause(sessionFactory.getCurrentSession(), q);
    }


    @Override
    public List<T> getAll() {
        HibernateQuery query = getSelectQuery();
        return query.list(q);
    }

    @Override
    public void deleteAll() {
        getDeleteQuery().execute();
    }

    @Override
    public T getById(int id) {
        return (T)sessionFactory.getCurrentSession().get(clazz, id);
    }

    @Override
    public void add(T entity) {
        sessionFactory.getCurrentSession().saveOrUpdate(entity);
    }

    @Override
    public void update(T entity) {
        sessionFactory.getCurrentSession().saveOrUpdate(entity);
    }

    @Override
    public int countAll() {
        Long count = getSelectQuery().count();
        return count.intValue();
    }
}

개인적으로는 JPA에서 사용하는 JPQL의 문법이 조금 맘에 안듭니다. 거의 SQL query문을 객체를 이용해서 사용한다는 느낌만 들고 있기 때문에, 오타 등의 문제에서 자유롭지가 않습니다. 그런 면에서는 queryDSL을 이용하는 것이 가장 좋은 것 같지만 queryDSL의 경우에는 처음 초기 설정이 조금 까다롭습니다. 같이 일하는 팀원중 이런 설정을 잘 잡는 사람이 있으면 괜찮지만…

다음에는 글 내용중 언급한것처럼 @PersistenceContext Autowiring에 대해서 코드 분석을 해볼까합니다. ^^ 아무도 안오는 Blog지만요. ^^

Posted by Y2K
,

java에서 가장 대표적인 ORM인 Hibernate와 Hibernate 기반의 JPA연결 방법에 대해서 알아보도록 하겠습니다.

먼저, 모든 연결 방법은 Hibernate를 기반으로 구성되어 있습니다. 이는 Hibernate의 사상이 JPA의 기본이 되어버린 것이 큽니다. 따라서, Spring을 이용한 ORM의 접근방법이라고 하는 것이 대부분 Hibernate에 대한 연결이 되는 것이 일반적입니다.

Hibernate

Hibernate를 사용하기 위해서는 Configuration에 다음 2가지가 등록되어야지 됩니다.

  1. LocalSessionFactoryBean : SessionFactory에 대한 Factory Bean입니다. SessionFactory를 생성하는 객체를 등록시켜줍니다. 이는 Spring에서 사용할 DataSource와 Entity가 위치한 Package들에 대한 검색을 모두 포함하게 됩니다.
  2. HibernateTransactionManager : PlatformTransactionManager를 구현한 Hibernate용 TransactionManager를 등록해야지 됩니다. 이는 Spring에서 @EnableTransactionManager와 같이 사용되어 @Transactional annotation을 사용할 수 있게 됩니다.
LocalSessionFactory

SessionFactory를 생성하는 FactoryBean입니다. hibernate.cfg.xml 에서 설정되는 내용이 여기에 들어가게 됩니다.

  • Hibernate의 property를 등록합니다.
  • DataSource를 등록시킵니다.
  • Entity가 위치한 Package를 지정합니다.
    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
        properties.setProperty("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
        properties.setProperty("hibernate.format_sql", env.getProperty("hibernate.format_sql"));

        LocalSessionFactoryBean localSessionFactoryBean = new LocalSessionFactoryBean();
        localSessionFactoryBean.setDataSource(dataSource());
        localSessionFactoryBean.setHibernateProperties(properties);

        localSessionFactoryBean.setPackagesToScan(new String[] { "me.xyzlast.bh.entities" });
        return localSessionFactoryBean;
    }
HibernateTransactionManager

Transaction을 사용하기 위한 PlatformTransactionManager interface의 구현체를 등록합니다.

  • SessionFactory와 같이 사용될 DataSource를 등록합니다.
  • SessionFactoryBean에서 생성되는 SessionFactory를 등록시켜줍니다.
    @Bean
    public PlatformTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setSessionFactory(sessionFactory().getObject());
        transactionManager.setHibernateManagedSession(false);
        return transactionManager;
    }

JPA

Hibernate 기반의 JPA를 등록하기 위해서는 Hibernate-JPA support bean이 필요합니다. 이에 대한 maven repository의 gradle 설정은 다음과 같습니다.

    compile 'org.hibernate:hibernate-entitymanager:4.3.1.Final'

Spring에서 Hibernate기반의 JPA를 사용하기 위해서는 다음 Bean들이 필요합니다.

  1. LocalContainerEntityManagerFactoryBean : SessionFactoryBean과 동일한 역활을 맡습니다. SessionFactoryBean과 동일하게 DataSource와 Hibernate Property, Entity가 위치한 package를 지정합니다. 또한 Hibernate 기반으로 동작하는 것을 지정해하는 JpaVendor를 설정해줘야지 됩니다.
  2. HibernateJpaVendorAdapter : Hibernate vendor과 JPA간의 Adapter를 설정합니다. 간단히 showSql 정도의 Property만을 설정하면 됩니다. 매우 단순한 code입니다.
  3. JpaTransactionManager : DataSource와 EntityManagerFactoryBean에서 생성되는 EntityManagerFactory를 지정하는 Bean입니다.
LocalContainerEntityManagerFactoryBean

Hibernate에서 SessionFactoryBean과 동일한 역활을 담당하는 FactoryBean입니다. EntityManagerFactory를 생성하는 FactoryBean형태입니다. @Configuration 선언은 다음과 같습니다.

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setJpaVendorAdapter(hibernateJpaVendorAdapter());
        entityManagerFactoryBean.setDataSource(dataSource());
        entityManagerFactoryBean.setPackagesToScan("me.xyzlast.bh.entities");
        // NOTE : Properties를 이용해서 Hibernate property를 설정합니다.
        // entityManagerFactoryBean.setJpaProperties();
        return entityManagerFactoryBean;
    }
HibernateJpaVendorAdapter

JPA 규약과 Hibernate간의 Adapter 입니다. 특별히 설정할 내용은 존재하지 않고, showSQL 정도의 설정만이 존재합니다.

    @Bean
    public HibernateJpaVendorAdapter hibernateJpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        return hibernateJpaVendorAdapter;
    }
JpaTransactionManager

JPA를 지원하는 TransactionManager를 등록합니다. DataSource와 EntityManagerFactory를 등록시켜주면 됩니다.

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(entityManagerFactory().getObject());
        jpaTransactionManager.setDataSource(dataSource());
        return jpaTransactionManager;
    }


Posted by Y2K
,

aspectJ기술은 AOP를 보다 쉽고 강력하게 적용하기 위한 기술입니다. aspectJ는 다음과 같은 특징을 가지고 있습니다.

  • compile된 bytecode에 대한 re-compile을 통한 새로운 bytecode의 작성으로 인한 AOP가 갖는 성능 저하의 최소
  • pointcut, advisor의 직접적인 선언을 이용한 보기 쉬운 코드의 작성

먼저, AspectJ를 실행하기 위해서는 다음 3개의 maven repository의 등록이 필요합니다.

compile 'org.aspectj:aspectjrt:1.7.4'
compile 'org.aspectj:aspectjtools:1.7.4'
compile 'org.aspectj:aspectjweaver:1.7.4'

aspectJ는 중요한 3개의 단어 정의가 먼저 필요합니다.

  1. Pointcut : AOP가 적용되는 시점을 정의합니다. 정의 방법은 @annotation, expression, method, beanName 등의 방법이 있습니다.
  2. Advice : Pointcut과 연동되어 AOP가 적용될 method를 의미합니다. 전/후 처리 method를 의미합니다.
  3. Advisor : Point + Advice를 정의된 class를 의미합니다. Spring에서 aspectJ에 대한 지원을 하기 위한 방법으로 사용됩니다.

예시로 가장 간단히 사용할 수 있는 @annotation을 이용한 AOP에 대한 예제에 대해서 알아보도록 하겠습니다.

먼저 AOP target method를 지정할 @interface를 생성합니다.

public @interface ToLowerOutput {
}

생성된 interface를 지정하는 Pointcut과 Advice를 포함한 Advisor class를 생성합니다.

@Aspect
@Component
public class HelloAdvisor {
    @Pointcut(value = "@annotation(me.xyzlast.bookstore.aop.ToLowerOutput)")
    private void convertToLowercast() {

    }

    @Around(value = "convertToLowercast()")
    public Object convertResultStringToLower(ProceedingJoinPoint pjp) throws Throwable {
        return pjp.proceed().toString().toLowerCase();
    }
}

만들어진 @annotation을 class에 적용합니다.

@Component
public class HelloImpl implements Hello {
    @ToLowerOutput
    @Override
    public String sayHello(String name) {
        return "Hello " + name + "!";
    }

    @ToLowerOutput
    @Override
    public String sayHi(String name) {
        return "Hi! " + name + "!";
    }

    @ToLowerOutput
    @Override
    public String sayThankYou(String name) {
        return "Thank you, " + name + "!";
    }
}

Spring configuration을 다음과 같이 수정합니다. 주의할점은 반드시 annotation auto proxy 설정이 bean 정의보다 먼저 위치해야지 됩니다!

    <aop:aspectj-autoproxy/>
    <bean name="helloImpl" class="me.xyzlast.bookstore.aop.HelloImpl"/>
    <bean name="aspectJAOP" class="me.xyzlast.bookstore.aop.HelloAdvisor"/>

이제 test code를 이용하면 모든 값들이 다 소문자로 나오게 되는 것을 볼 수 있습니다.

Posted by Y2K
,

개발시, 외부 SOAP 서비스 또는 REST 서비스를 이용해서 인증을 처리해줘야지 되는 경우가 자주 발생하게 됩니다. 특히, 기존의 인증 시스템을 가지고 있는 상태에서 신규 개발은 기존의 인증시스템을 그대로 사용해야지 될 때, 이런 경우가 자주 생기게 됩니다.

일단. 이와 같은 상황에서는 가장 널리 사용되고 있는 DaoAuthentication을 사용하지 못합니다. Application의 Database에는 사용자의 정보를 전혀 가지지 못하게 되니까요. 이때, 인증 process만을 외부에서 사용하고, 인증된 Process를 거친 후, Spring Security FIlter Chain을 통과하게 된다면 가장 좋은 방법이 될 것 같습니다.

Spring Security 인증 구성

Spring Security에서는 여러가지 인증방법을 제공하는데, 이를 지원하기 위해서는 AuthenticationManagerBuilder에 AuthenticationProvider를 구현한 객체를 설정해줘야지 됩니다.

Spring Security에서 일반적으로 인증을 구성하는 방식은 다음과 같습니다.

  1. AuthenticationProvider 구성
  2. AuthenticationManager 구성 후, Provider에 Bean Setting
  3. AuthenticationManagerBuilder에 설정된 AuthenticationProvider 설정

가장 기본적이라고 할 수 있는 DaoAuthenticationProvider를 이용한 설정은 다음과 같습니다.

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(userDetailsServiceBean());
    daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    return daoAuthenticationProvider;
}

@Override
@Bean(name = BeanIds.USER_DETAILS_SERVICE)
public UserDetailsService userDetailsServiceBean() throws Exception {
    CommonAppUserDetailService userDetailsService = new CommonAppUserDetailService();
    userDetailsService.setUserService(context.getBean(UserService.class));
    return userDetailsService;
}

RemoteAuthenticationProvider 의 구성

DaoAuthenticationProvider의 구성과 동일하게 코드는 다음과 같이 구성될 수 있습니다.

@Bean
public RemoteAuthenticationProvider remoteAuthenticationProvider() throws Exception {
    RemoteAuthenticationProvider provider = new RemoteAuthenticationProvider();
    provider.setRemoteAuthenticationManager(remoteAuthenticationManager());
    return provider;
}

여기에서 중요한 것이, setRemoteAuthenticationManager method입니다. 이제 외부에서 인증을 하는 Client code가 구성되는 곳이 RemoteAuthenticationManager이기 때문입니다.

RemoteAuthenticationManager interface는 다음과 같이 구현되어 있습니다.

public interface RemoteAuthenticationManager {
    Collection<? extends GrantedAuthority> attemptAuthentication(String username, String password) throws RemoteAuthenticationException;
}

매우 단순한 Interface입니다. 여기서 주의할 것이, username과 password는 모두 평문으로 들어오게 됩니다. DaoAuthenticationManager의 경우, Password를 따로 encoding을 할 수 있는 기능을 제공하지만 RemoteAuthenticationManager의 경우에는 plan text로 username과 password를 다뤄야지 됩니다. 하긴 이게 당연한것인지도 모릅니다. username과 password를 단순히 전달하는 역활을 하게 되는 것이 일반적이니까요.

개인적으로 만들어본 SOAP을 이용한 외부 서비스를 호출하는 RemoteAuthenticationManager입니다. RemoteAuthenticationManager를 통해 인증을 처리하고, 외부 SOAP API에 있는 사용자 정보를 Application DB에 옮기는 작업을 하도록 구성하였습니다. 그 이유는 UserDetailsService 때문입니다. UserDetailsService를 통해 username, password 이외의 사용자 정보를 활용할 수 있는 여지를 남겨주고 싶었습니다.

public class SsoAuthServiceImpl implements SsoAuthService, RemoteAuthenticationManager {
    private static final QName SERVICE_NAME = new QName("http://ws.daesung.co.kr/", "SSOWebServiceForJava");
    public static final String USERNAME_PASSWORD_IS_NOT_MATCHED = "username/password is not matched!";
    public static final String ROLE_USER = "ROLE_USER";

    @Getter
    @Setter
    private UserService userService;
    @Getter
    @Setter
    private ApplicationContext context;

    @Override
    public SsoAuthResult login(String username, String password) throws IOException {
        URL wsdlURL = SSOWebServiceForJava.WSDL_LOCATION;
        SSOWebServiceForJava service = new SSOWebServiceForJava(wsdlURL, SERVICE_NAME);
        SSOWebServiceForJavaSoap request = service.getSSOWebServiceForJavaSoap12();
        ArrayOfString result = request.ssoLoginArray(username, password, "NS");
        List<String> items = result.getString();

        SsoAuthResult authResult = context.getBean(SsoAuthResult.class);
        authResult.initialize(items);

        return authResult;
    }

    @Override
    public Collection<? extends GrantedAuthority> attemptAuthentication(String username, String password) throws RemoteAuthenticationException {
       try {

            SsoAuthResult authResult = login(username, password);
            if (authResult.isLoginSuccess()) {
                User user = userService.findByUsername(username);
                if(user == null) {
                    user = userService.addNewUser(username, authResult.getName(), password, ROLE_USER);
                } else {
                    user.setName(authResult.getName());
                    userService.updateUser(user);
                }
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                for(String role : user.getRoles()) {
                    authorities.add(new SimpleGrantedAuthority(role));
                }
                return authorities;
            } else {
                throw new RemoteAuthenticationException(USERNAME_PASSWORD_IS_NOT_MATCHED);
            }

        } catch (Exception e) {
            throw new RemoteAuthenticationException(USERNAME_PASSWORD_IS_NOT_MATCHED);
        }
//        return null;
    }
}

RemoteAuthenticationManager의 경우에는 attemptAuthentication method만을 갖는 interface입니다. 사용자의 username, password가 전달되기 때문에, 이를 이용해서 외부 API call을 해서 인증을 처리하는 것이 가능하게 됩니다.

이를 모두 반영시킨 Spring Security의 Configuration은 다음과 같습니다.

@EnableWebSecurity
@Configuration
@Slf4j
@ComponentScan("co.kr.daesung.app.center.api.web.cors")
@ImportResource(value = "classpath:cxf-servlet.xml")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    public static final String CORS_SUPPORT_FILTER = "corsSupportFilter";
    public static final String ADMIN_ROLE = "ADMIN";

    @Autowired
    private UserService userService;
    @Autowired
    private ApplicationContext context;
    @Autowired
    private CorsSupportLoginUrlAuthenticationEntryPoint corsEntryPoint;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling().authenticationEntryPoint(corsEntryPoint)
             .and()
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/admin/**").hasRole(ADMIN_ROLE)
                .antMatchers("/api/apiKey/**").authenticated()
                .antMatchers("/api/auth/**").authenticated()
            .and()
                .addFilterAfter(digestAuthenticationFilter(), BasicAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(remoteAuthenticationProvider());
    }

    @Bean
    public RemoteAuthenticationProvider remoteAuthenticationProvider() throws Exception {
        RemoteAuthenticationProvider provider = new RemoteAuthenticationProvider();
        provider.setRemoteAuthenticationManager(remoteAuthenticationManager());
        return provider;
    }

    @Bean
    public RemoteAuthenticationManager remoteAuthenticationManager() throws Exception {
        SsoAuthServiceImpl ssoAuthService = new SsoAuthServiceImpl();
        ssoAuthService.setUserService(userService);
        ssoAuthService.setContext(context);
        return ssoAuthService;
    }

    /**
     * AuthenticationManager를 LoginController에서 사용하기 위해서는 반드시, @Bean으로 등록되어야지 된다.
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * UserDetailsService를 구성해준다. Bean Name은 반드시 BeanIds.USER_DETAILS_SERVICE로 등록되어야지 된다.
     * @return
     * @throws Exception
     */
    @Override
    @Bean(name = BeanIds.USER_DETAILS_SERVICE)
    public UserDetailsService userDetailsServiceBean() throws Exception {
        CommonAppUserDetailService userDetailsService = new CommonAppUserDetailService();
        userDetailsService.setUserService(context.getBean(UserService.class));
        return userDetailsService;
    }

    @Bean
    public SsoAuthService ssoAuthService() {
        return new SsoAuthServiceImpl();
    }

}

UserDetailsService는 반드시 구현하는 것이 좋습니다. 그리고 외부 서비스를 이용해서 로그인을 진행하더라도, 기본적인 사용자 정보을 반드시 Application DB안에 넣는 것이 좋을 것 같습니다.

Posted by Y2K
,

Gradle 2.0으로 업그레이드 된 버젼에 대해서 업데이트 된 글이 있습니다.


기존 gradle을 이용한 개발 환경 설정 업데이트입니다.


변경사항은 다음과 같습니다. 


1. jetty plugin의 제거 : 개발환경에서는 jetty / tomcat의 차이가 없을 것 같아, jetty를 없앴습니다.

2. spork를 이용한 테스트 환경 추가. : groovy base인 spork를 이용한 test 환경을 추가하였습니다.

3. tomcat plugin 사용시, tomcatRun 에러 수정 : tomcat plugin 사용시 tomcatRun 을 하면 servlet jar class not found 에러가 나오는 것을 수정했습니다. 

4. gradle 1.9 에서 동작하지 않던 버그를 수정했습니다.

5. build.gradle을 4개로 분리했습니다.

1) base.gradle : 기본 설정에 관련된 내용으로 구성됩니다.

2) domain.gradle : domain module에 관련된 내용으로 구성됩니다. (queryDsl QFile generate)

3) web.gradle : war에 관련된 내용으로 구성됩니다. (tomcatRun, tomcatRunWar)


build.gradle

apply plugin: 'base'
apply plugin: 'sonar-runner'
apply plugin: 'maven'
version = '1.0.0'

ext {
    javaVersion = 1.7
    springVersion = '4.0.0.RELEASE'
}


buildscript {
    repositories {
        mavenCentral()
        jcenter()
        maven { url 'http://repo.springsource.org/plugins-release' }
    }
    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0'
        classpath (group: 'org.gradle.api.plugins', name: 'gradle-cargo-plugin', version: '0.6.1')
        classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.1'
    }
}

allprojects {
    apply plugin: 'propdeps'
    apply plugin: 'propdeps-maven'
    apply plugin: 'propdeps-idea'
    apply plugin: 'propdeps-eclipse'

    repositories {
        mavenLocal()
        mavenCentral()
        jcenter()
        maven { url 'http://download.java.net/maven/2' }
        maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url 'https://maven.java.net/content/repositories/releases'}
        maven { url 'http://repo.springsource.org/plugins-release' }
    }

    dependencies {
        provided 'org.projectlombok:lombok:0.12.0'
    }
}


apply from: 'base.gradle'
apply from: 'domain.gradle'
apply from: 'web.gradle'


base.gradle

subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'
    apply plugin: 'jacoco'
    apply plugin: 'groovy'

    sonarRunner {
        sonarProperties {
            property "sonar.sourceEncoding", "UTF-8"
        }
    }

    jacoco {
        toolVersion = '0.6.3.201306030806'
    }

    test {
        jacoco {
            append = false
            destinationFile = file("target/jacoco.exec")
            classDumpFile = file("target/classpathdumps")
        }
    }

    if(project.hasProperty('target')) {
        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-${project.target}"]
        }
    } else {
        String hostname = InetAddress.getLocalHost().getHostName();
        if(hostname.endsWith('.local')) {   //맥의 경우, .local 이 모든 hostname에 추가됩니다.
            hostname.replace(".local", '')
        }

        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-" + hostname]
        }
    }

    dependencies {
        def slf4jVersion = "1.7.2"
        compile "org.slf4j:jcl-over-slf4j:$slf4jVersion"
        compile "org.slf4j:jul-to-slf4j:$slf4jVersion"
        compile "org.slf4j:slf4j-api:$slf4jVersion"
        compile 'ch.qos.logback:logback-classic:1.0.13'

        testCompile "junit:junit:4.11"

        groovy "org.codehaus.groovy:groovy-all:2.1.5"
        testCompile "org.spockframework:spock-core:1.0-groovy-2.0-SNAPSHOT"
        testCompile "org.spockframework:spock-spring:1.0-groovy-2.0-SNAPSHOT"
    }

    sourceCompatibility = rootProject.ext.javaVersion
    targetCompatibility = rootProject.ext.javaVersion

    tasks.withType(Compile) {
        options.encoding = 'UTF-8'
    }
}

sonarRunner {
    sonarProperties {
        property "sonar.host.url", "http://192.168.13.209:9000"
        property "sonar.jdbc.url", "jdbc:mysql://192.168.13.209:3306/sonar"
        property "sonar.jdbc.driverClassName", "com.mysql.jdbc.Driver"
        property "sonar.jdbc.username", "root"
        property "sonar.jdbc.password", 'qwer12#$'
    }
}


domain.gradle

configure(subprojects.findAll { it.name.endsWith('domain') }) {
    dependencies {
        compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.22'
        compile("com.jolbox:bonecp:0.8.0.RELEASE") {
            exclude group: 'com.google.guava'
        }
        compile group: 'com.google.guava', name: 'guava', version: '15.0'
        compile group: 'org.springframework', name: 'spring-core', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-orm', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-tx', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-context', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-context-support', version: "${rootProject.ext.springVersion}"

        compile group: 'org.hibernate', name: 'hibernate-core', version: '4.1.10.Final'
        compile group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-2.0-api', version: '1.0.1. Final'
        compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '4.1.10.Final'
        compile group: 'org.hibernate', name: 'hibernate-validator', version: '4.3.1.Final'

        compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.4.2.RELEASE'

        def queryDSL = '3.2.4'
        compile("com.mysema.querydsl:querydsl-core:$queryDSL")
        compile("com.mysema.querydsl:querydsl-jpa:$queryDSL")
        compile("com.mysema.querydsl:querydsl-sql:$queryDSL")
        provided("com.mysema.querydsl:querydsl-apt:$queryDSL") {
            exclude group: 'com.google.guava'
        }

    }

    sourceSets {
        generated {
            java {
                srcDirs = ['src/main/generated']
            }
        }
    }

    task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
        source = sourceSets.main.java
        classpath = configurations.compile + configurations.provided
        options.compilerArgs = [
                "-proc:only",
                "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
        ]
        destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
    }

    compileJava {
        dependsOn generateQueryDSL
        source generateQueryDSL.destinationDir
    }

    compileGeneratedJava {
        dependsOn generateQueryDSL
        options.warnings = false
        classpath += sourceSets.main.runtimeClasspath
    }

    clean {
        delete sourceSets.generated.java.srcDirs
    }

    idea {
        module {
            sourceDirs += file('src/main/generated')
        }
    }
}



web.gradle

configure(subprojects.findAll { it.name.endsWith('Web') }) {
    apply plugin: 'war'
    apply plugin: 'tomcat'
    apply plugin: 'cargo'

    dependencies {
        String tomcatVersion = '7.0.47'
        tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
        tomcat "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
        tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
            exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
        }

        def cargoVersion = '1.3.3'
        cargo "org.codehaus.cargo:cargo-core-uberjar:$cargoVersion",
                "org.codehaus.cargo:cargo-ant:$cargoVersion"

        providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
        providedCompile 'javax.websocket:javax.websocket-api:1.0'
        providedCompile 'javax.servlet:jsp-api:2.0'
        providedCompile "org.apache.tomcat:tomcat-servlet-api:${tomcatVersion}"
    }

    tomcatRun {
        dependsOn war
    }

    tomcatRunWar {
        dependsOn war
    }

    cargoRedeployRemote {
        dependsOn war
    }

    cargoDeployRemote {
        dependsOn war
    }

    cargo {
        containerId = 'tomcat7x'
        port = 8080

        deployable {
            context = "${project.name}"
        }

        remote {
            hostname = '192.168.13.209'
            username = 'ykyoon'
            password = 'qwer12#$'
        }
    }
}





Posted by Y2K
,

spock를 이용한 spring test

Java 2013. 12. 11. 03:17

드디어 JUnit의 시대가 끝이 나는것인가.... 라는 생각이 드는 멋진 테스트 Framework인 Spock를 소개합니다.


groovy 언어로 구성을 해야지 되는 언어적 제약사항이 있지만, 기존의 JUnit Test에 비해서 매우 간결한 문법과 TDD를 위한 구성을 제공하고 있습니다. 


1. 설정


gradle 기준으로 spock는 다음과 같이 설정됩니다. 


    repositories {
        mavenCentral()
        maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
     }
    dependencies {
        groovy "org.codehaus.groovy:groovy-all:2.1.5"
        testCompile "org.spockframework:spock-core:1.0-groovy-2.0-SNAPSHOT"
    }
   


기본적으로 JUnit을 사용하고 있는 test project입니다. 


2. 구성


기본적으로 JUnit과 거의 유사한 기본 설정을 갖습니다.

SpockJUnit
SpecificationTest class
setup()@Before
cleanup()@After
setupSpec()@BeforeClass
cleanupSpec()@AfterClass
FeatureTest
Parameterized featureTheory
ConditionAssertion
Exception condition@Test(expected=...)
@FailsWith@Test(expected=...)
InteractionMock expectation (EasyMock, JMock, ...)



3. 테스트 코드의 종류에 따른 구현


우리가 일반적으로 테스트 코드를 짤때, 다음과 테스트 코드가 주로 작성되게 됩니다. 


1) 특정한 method를 실행후, 객체의 상태가 예측대로인지 확인

2) 특정한 method의 input을 선정하고, output값이 예측대로 나왔는지 확인


이 두가지 case는 약간은 미묘하게 다릅니다. 1)의 경우는 response를 확인을 하는 것이고, 2)의 결과는 예측하는 것으로 약간의 차이를 가지고 오게 됩니다. 

이를 spock에서는 여러 block으로 지정하여, case에 대한 내용을 명확하게 하고 있습니다. 


spock에서 지원하는 block은 다음과 같습니다.  또한 이 block뒤에는 자유롭게 문자열을 넣어서, block안의 내용을 서술할 수 있습니다.


setup : 객체에 대한 설정

when / then : when block이 있으면 반드시 then block이 존재합니다. when에서 객체의 response를 얻어내고, then을 통해 response를 확인합니다. condition을 설정한다고 생각하시면 편합니다.

expect / where : 예측되는 값과 예측값 list를 정의합니다. spock에서 가장 놀라운 기능중 하나입니다. 


위 block을 적용한 code example은 다음과 같습니다. 


    def "건물이름 길이가 작아 exception이 발생"() {
        def buildingName = "가"
        when:
        service.searchByBuilding(buildingName)
        then:
        def e = thrown(IllegalArgumentException)
        e.message == AddressSearchServiceImpl.BUILDING_NAME_IS_TOO_SHORT
    }

    def "서울내의 레미안만 얻어오기"() {
        def buildingName = "레미안"
        when:
        def searchResults = service.searchByBuilding(SidoEnum.SEOUL.getStringValue(), buildingName, 0, 10)
        then:
        searchResults.every { r ->
            r.buildingName.contains(buildingName)
            r.siGunGu.sido.sidoNumber.equals(SidoEnum.SEOUL.getStringValue()) == true
        }
    }

    def "검색문자열 분리 테스트"() {
        expect:
        def matcher = SearchTextRegex.extractSearchText(input)
        matcher.find() == true
        matcher.group("mainText").equals(mainText) == true
        matcher.group("mainNumber").equals(mainNumber) == true

        where:
        input | mainText | mainNumber
        "구로동 245 번지" | "구로동" | "245"
        "서초1동" | "서초1동" | ""
        "잠실동 624" | "잠실동" | "624"
        "지봉로 1" | "지봉로" | "1"
    }

너무나 멋지지 않나요? 지금까지 구성하던 test code의 길이가 절반 이하로 줄어들뿐 아니라, 매우 직관적이고, TDD에 적합한 테스트 코드 형태로 구성이 되고 있는 것을 알 수 있습니다. 


개인적으로 하고 있는 project들을 모두 spock로 변경하는 작업을 한번 해봐야지 될 것 같습니다. 이런 것을 보면 왠지 희열이 느껴지지 않나요? ^^;;

모두들 Happy Coding~ 



Posted by Y2K
,