잊지 않겠습니다.

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
,