저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링4 입문

스프링 4

DDD Start

객체 지향과
디자인 패턴

JSP 2.3

JPA 입문

현재 참여하고 있는 프로젝트는 레거시 시스템과 관련이 있다. CHAR 타입 칼럼의 기본값으로 공백문자(' ')를 지정한 테이블이 많아서 기존 코드를 보면 TRIM 처리를 하는 쿼리가 많이 있다. 현재 프로젝트는 JPA를 사용하고 있는데, 조회 결과에서 불필요한 공백을 제거할 필요가 생겼다. 이를 위해 EntityListener와 커스텀 애노테이션을 사용했다.


@Trim 커스텀 애노테이션


DB에서 읽어온 값을 trim해야 하는 대상을 설정하기 위해 @Trim 애노테이션을 추가했다.


@Target({ ElementType.FIELD })

@Retention(RUNTIME)

public @interface Trim {

}


JPA 엔티티 리스너(entity listener) 메서드 구현


엔티티 단위로 @Trim 애노테이션이 붙은 필드에 대해 trim 처리를 수행하기 위해 JPA 엔티티 리스너 메서드를 구현했다. 엔티티 리스너 메서드는 크게 별도 클래스로 구현하거나 엔티티 메서드로 구현하는 두 가지 방법이 존재하는데 범용적으로 적용하기 위해 별도 클래스로 구현했다. 구현 클래스는 아래와 같다.


import javax.persistence.PostLoad;


public class TrimEntityListener {


    @PostLoad

    public void postLoad(Object entity) {

        Field[] fields = entity.getClass().getDeclaredFields();

        for (Field field : fields) {

            if (field.isAnnotationPresent(Trim.class)) {

                field.setAccessible(true);

                try {

                    Object value = field.get(entity);

                    if (value != null && value instanceof String) {

                        String s = (String) value;

                        field.set(entity, s.trim());

                    }

                } catch (Exception ex) {

                }

            }

        }

    }

}


@PostLoad 애노테이션은 엔티티를 로딩한 후에 호출할 메서드를 설정한다. @PostLoad 외에도 엔티티 라이프사이클에 맞춘 @PrePersist, @PostPersist, @PreUpdate, @PostUpdate 등의 애노테이션이 존재한다.


postLoad() 메서드의 entity 파라미터를 로딩한 엔티티 객체이다. postLoad() 메서드는 엔티티 객체의 필드 중에서 @Trim 애노테이션이 존재하는 필드를 찾아서 값에 대해 trim 처리를 한다.


엔티티 리스너 클래스 적용


이제 엔티티 리스너 클래스를 사용하도록 설정만 하면 된다. 전체 엔티티를 대상으로 엔티티 리스너 클래스를 적용하고 싶다면 JPA 설정 파일(스프링 부트를 사용한다면 META-INF/orm.xml 파일)의 <entity-listener> 태그에 엔티티 리스너 클래스를 설정하면 된다.


<?xml version="1.0" encoding="UTF-8"?>


<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm

                    http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"

  version="2.1">


  <persistence-unit-metadata>

    <persistence-unit-defaults>

      <entity-listeners>

        <entity-listener class="jpa.TrimEntityListener" />

      </entity-listeners>

    </persistence-unit-defaults>

  </persistence-unit-metadata>

  

</entity-mappings>


엔티티별로 엔티티 리스너 클래스를 적용할 수도 있다. @EntityListener 애노테이션을 사용해서 각 엔티티 클래스에 사용할 엔티티 리스너를 지정하면 된다.


import javax.persistence.EntityListeners;


@Entity

@EntityListeners({TrimEntityListener.class})

public class Division {


    @Trim

    private String location;

    ...

}


TrimEntityListener는 @PostLoad 메서드를 정의하고 있으므로, 엔티티를 로딩할 때마다 @PostLoad 메서드를 사용해서 @Trim이 붙은 필드 값을 trim 처리할 수 있다.

저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링부터 다중 데이터소스 설정 방법은 간단하다. 먼저 application.properties 파일에 "구분.datasource"로 시작하는 데이터소스 설정을 추가한다. 다음은 예이다.



# datasource 1

spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

spring.datasource.url=jdbc:oracle:thin:@10.xx.xx.xx:SID

spring.datasource.username=dbuser

spring.datasource.password=dbpw


spring.datasource.tomcat.initialSize=5

spring.datasource.tomcat.maxActive=40

spring.datasource.tomcat.maxIdle=40

spring.datasource.tomcat.minIdle=5

spring.datasource.tomcat.maxWait=5

spring.datasource.tomcat.validationQuery=select 1+1 from dual

spring.datasource.tomcat.testWhileIdle=true

spring.datasource.tomcat.timeBetweenEvictionRunsMillis=60000


# datasource 2

sms.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver

sms.datasource.url=jdbc:sqlserver://10.xx.xx.xx:1433;databaseName=SMS

sms.datasource.username=dbuser1

sms.datasource.password=dbpw1


sms.datasource.tomcat.initialSize=1

sms.datasource.tomcat.maxActive=20

sms.datasource.tomcat.maxIdle=20

sms.datasource.tomcat.minIdle=1

sms.datasource.tomcat.maxWait=1

sms.datasource.tomcat.validationQuery=select 1

sms.datasource.tomcat.testWhileIdle=true

sms.datasource.tomcat.timeBetweenEvictionRunsMillis=60000


이 코드는 spring.datasource로 시작하는 데이터소스와 sms.datasource로 시작하는 데이터소스를 설정했다. "구분.datasource.tomcat"으로 시작하는 설정은 톰캣 DBCP 설정이다.


스프링부트는 별도 설정을 하지 않으면 spring.datasource로 시작하는 설정만 데이터소스로 사용하므로, 다중 데이터소스를 사용하려면 별도 설정을 추가해야 한다. 다음은 다중 데이터소스를 위한 자바 설정 예이다.


@Configuration

public class DataSourceConfiguration {


    @Bean

    @ConfigurationProperties(prefix = "spring.datasource")

    public DataSourceProperties dataSourceProp() {

        return new DataSourceProperties();

    }


    @Primary

    @Bean(name = "dataSource")

    @ConfigurationProperties(prefix = "spring.datasource.tomcat")

    @Qualifier("primary")

    public DataSource dataSource() {

        return dataSourceProp().initializeDataSourceBuilder().build();

    }


    @Primary

    @Bean(name = "transactionManager")

    @Qualifier("primary")

    public PlatformTransactionManager transactionManager() {

        return new DataSourceTransactionManager(dataSource());

    }


    @Bean

    @ConfigurationProperties(prefix = "sms.datasource")

    public DataSourceProperties smsDataSourceProp() {

        return new DataSourceProperties();

    }


    @Bean(name = "smsDataSource")

    @ConfigurationProperties(prefix = "sms.datasource.tomcat")

    @Qualifier("sms")

    public DataSource smsDataSource() {

        return smsDataSourceProp().initializeDataSourceBuilder().build();

    }


    @Bean(name = "smsTransactionManager")

    @Qualifier("sms")

    public PlatformTransactionManager smsTransactionManager() {

        return new DataSourceTransactionManager(smsDataSource());

    }



각 데이터소스 설정별로 세 개의 빈을 설정했다.

  • 설정 프로퍼티를 담은 DataSourceProperties 빈
    • @ConfigurationProperties를 사용해서 접두어를 지정(예, smsDataSourceProp() 빈 설정은 @ConfigurationProperties의 prefix 값으로 sms.datasource 사용)
  • DataSourceProperties 빈을 이용해서 DataSource 빈 생성
    • DataSourceProperties의 initializeDataSourceBuilder() 메서드를 이용해서 생성해야 tomcat 설정이 적용
  • 각 DataSource 마다 트랜잭션관리자 생성
    • 글로벌 트랜잭션을 사용해야 하면 JTA 트랜잭션 설정


저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링의 @EnableCache 설정과 @Cacheable 설정을 사용하면 매우 쉽게 레디시를 캐시로 사용할 수 있어서 편리하다. 한 가지 불편한 점이 있다면 캐시키를 JDK 직렬화를 사용해서 저장한다는 것이다. 그러다보니 상상한 레디스 키가 cache:1:2:3" 문자열이어도(캐시 이름 cache, 키 "1:2:3") 실제 레디스에 들어간 키는 "cache:타입데이터1:2:3" 이런 모양이 된다. 이는 레디스에 직접 연결해서 데이터를 확인할 때 불편함을 준다.


JDK 직렬화 대신 문자열로 키를 생성하는 KeySerializer를 사용하면 이 단점을 보완할 수 있다. 이를 하려면 직접 RedisTemplate을 생성해주면 된다. 다음은 코드 구현 예이다.


import java.net.UnknownHostException;


import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Primary;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration

public class RedisTemplateConfiguration {


    @Bean

    @Primary

    public RedisTemplate<Object, Object> redisTemplate(

            RedisConnectionFactory redisConnectionFactory)

            throws UnknownHostException {

        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();

        template.setConnectionFactory(redisConnectionFactory);

        template.setKeySerializer(new StringRedisSerializer());

        return template;

    }

}


StringRedisSerializer는 키 값을 직렬화할 때 String.getBytes()를 사용하므로 JDK 직렬화를 사용할 때처럼 불필요한 타입정보가 붙지 않는다.

저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링은 캐시 구현으로 레디스를 지원한다. 스프링 부트를 사용하면 다음의 간단한 설정만 추가하면 된다.


<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-cache</artifactId>

</dependency>


이 설정을 추가하면 @EnableCaching 만으로 쉽게 @Cacheable과 관련 애노테이션을 사용해서 레디스를 캐시로 사용할 수 있다.

그런데, 스프링 부트가 제공하는 설정만으로 레디스를 캐시로 사용하면 캐시별로 유효 시간을 줄 수 없다. Ehcache를 캐시로 사용하면 설정 파일을 이용해서 유효 시간을 줄 수 있는데 레디스의 경우 설정 파일도 없다.(http://docs.spring.io/spring-boot/docs/1.4.3.RELEASE/reference/html/common-application-properties.html 참고)


진행중인 프로젝트에 캐시별 유효시간 설정 기능이 필요해서 스프링 부트로 다음과 같은 프로퍼티를 사용해서 캐시별로 유효 시간을 설정할 수 있도록 구현해봤다.


# application.properties

spring.cache.redis.defaultExpireTime=0

spring.cache.redis.expireTime.billDetailData=3600

spring.cache.redis.expireTime.billSummaryInfos=3600


이 설정에서 "spring.cache.redis"는 접두어이다. defaultExpireTime은 전체 캐시에 기본으로 적용할 유효시간을 설정한다. "expirTime.캐시이름"은 캐시 이름별로 캐시 시간을 설정한다. 유효 시간은 초 단위이다.


이 설정을 담기 위한 @ConfigurationProperties 클래스를 프로퍼티 클래스를 다음과 같이 작성했다.


import java.util.HashMap;

import java.util.Map;

import java.util.Map.Entry;


import org.springframework.boot.context.properties.ConfigurationProperties;


@ConfigurationProperties(prefix = "spring.cache.redis")

public class CacheRedisProperties {


    private long defaultExpireTime = 0L;

    private Map<String, Long> expireTime = new HashMap<>();


    private CacheTimeParser parser = new CacheTimeParser();


    public long getDefaultExpireTime() {

        return defaultExpireTime;

    }


    public void setDefaultExpireTime(long defaultExpireTime) {

        this.defaultExpireTime = defaultExpireTime;

    }


    public Map<String, Long> getExpireTime() {

        return expireTime;

    }


    public void setExpireTime(Map<String, Long> expireTime) {

        this.expireTime = expireTime;

    }

}


다음으로 RedisCacheManager에 유효 시간 설정하면 된다. 이를 위한 코드는 다음과 같다.


import java.util.Map.Entry;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Profile;

import org.springframework.data.redis.cache.RedisCacheManager;


/**

 * org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 클래스 참고

 */

@Configuration

@EnableCaching

@EnableConfigurationProperties(CacheRedisProperties.class)

public class CustomRedisCacheConfiguration {

    private Logger logger = LoggerFactory.getLogger(getClass());


    @Autowired

    private CacheRedisProperties cacheRedisProperties;


    @Bean

    public CacheManagerCustomizer<RedisCacheManager> cacheManagerCustomizer() {

        return new CacheManagerCustomizer<RedisCacheManager>() {

            @Override

            public void customize(RedisCacheManager cacheManager) {

                cacheManager.setDefaultExpiration(cacheRedisProperties.getDefaultExpireTime());

                cacheManager.setExpires(cacheRedisProperties.getExpireTime());

            }

        };

    }

}


스프링부트가 제공하는 CacheManagerCustomizer를 이용하면 부트가 생성한 CacheManager를 커스터마이징할 수 있다. 이 기능을 사용해서 RedisCacheManager에 캐시 유효 시간을 설정하면 된다.


저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. ㅇㅇ 2017.01.29 20:55 신고  댓글주소  수정/삭제  댓글쓰기

    최범균님이 집필하신 책으로 공부하고 있습니다.

    저에게 많은 도움이 되는 것 같아 감사의 의미로 댓글 남깁니다.

    새해 복 많이 받으시고 건강하세요. ^^

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

다음과 같은 간단한 이벤트 관련 코드를 만들 일이 생겼다.

  1. 도메인 객체가 트랜잭션 범위에서 이벤트를 발생하면 핸들러로 처리
  2. 트랜잭션이 커밋된 이후에 이벤트 핸들러에 이벤트 전달해야 함
  3. 이벤트가 유실되어 처리하지 못해도 됨(실패시 후처리)
  4. 이벤트 핸들러는 비동기로 실행

스프링 4.2나 그 이후 버전을 사용한다면 아주 간단하게 위 조건을 충족하는 코드를 만들 수 있다. 다음 조합을 사용하면 된다.

  • ApplicationEventPublisher.publishEvent(Object event) 사용
  • @TransactionEventListener
  • @Async로 비동기 처리


1. Events 클래스

다음은 도메인 객체에서 이벤트를 발생시킬 때 사용할 Events 클래스이다.


import org.springframework.context.ApplicationEventPublisher;


public class Events {

    private static ThreadLocal<ApplicationEventPublisher> publisherLocal = 

            new ThreadLocal<>();


    public static void raise(DomainEvent event) {

        if (event == null) return;


        if (publisherLocal.get() != null) {

            publisherLocal.get().publishEvent(event);

        }

    }


    static void setPublisher(ApplicationEventPublisher publisher) {

        publisherLocal.set(publisher);

    }


    static void reset() {

        publisherLocal.remove();

    }

}


Events 클래스의 raise() 메서드는 ApplicationEventPublisher를 이용해서 이벤트를 퍼블리싱한다. 참고로 raise() 메서드의 event 파라미터는 Event 타입인데 이 타입은 원하는 타입으로 알맞게 만들면 된다.


도메인 객체에서 Events.raise()로 발생한 이벤트를 ApplicationEventPublisher로 퍼블리싱하려면 도메인 객체를 실행하기 전에 Events.setPublisher()로 ApplicationEventPublisher를 설정해야 한다. 이를 위해 다음의 Aspect를 구현했다..


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.springframework.context.ApplicationEventPublisher;

import org.springframework.context.ApplicationEventPublisherAware;

import org.springframework.stereotype.Component;


@Aspect

@Component

public class EventPublisherAspect implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    private ThreadLocal<Boolean> appliedLocal = new ThreadLocal<>();


    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")

    public Object handleEvent(ProceedingJoinPoint joinPoint) throws Throwable {

        Boolean appliedValue = appliedLocal.get();

        boolean nested = false;

        if (appliedValue != null && appliedValue) {

            nested = true;

        } else {

            nested = false;

            appliedLocal.set(Boolean.TRUE);

        }

        if (!nested) Events.setPublisher(publisher);

        try {

            return joinPoint.proceed();

        } finally {

            if (!nested) {

                Events.reset();

                appliedLocal.remove();

            }

        }

    }


    @Override

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {

        this.publisher = eventPublisher;

    }

}


트랜잭션 범위에서 실행되는 경우에만 이벤트를 처리하기 위해 @Transactional을 적용한 경우에만 적용하도록 설정했다. 대상 메서드를 실행하기 전에 Events.setPublisher()를 이용해서 스프링의 ApplicationEventPublisher를 설정하고, 대상 메서드를 실행한 후에 Events.reset()으로 초기화하도록 했다.


이제 트랜잭션 범위에서 실행되는 도메인 객체는 다음과 같은 코드를 이용해서 이벤트를 발생시키면 된다.


public class Order {


    public void cancel() {

        ...

        Events.raise(new OrderCanceledEvent(this.id));

    }

}



2. @TransactionEventListener로 이벤트 핸들러 구현하기

스프링 4.2 이전까지는 트랜잭션과 동기화해서 뭘 실행하려면 TransactionSynchronizationManager를 사용해야 했는데, 스프링 4.2에 들어간 @TransactionEventListener를 사용하면 손쉽게 트랜잭션 커밋 이후에 이벤트 핸들러를 실행할 수 있다.


import org.springframework.stereotype.Component;

import org.springframework.transaction.event.TransactionalEventListener;


@Component

public class EventHandler {


    @TransactionalEventListener

    public void handle(OrderCanceledEvent event) {

        // ... 이벤트 처리

    }



@TransactionalEventListener의 phase 속성을 사용하면 트랜잭션 커밋 이후뿐만 아니라 커밋 전, 롤백 이후, 커밋이나 롤백 이후에 이벤트를 처리하도록 설정할 수 있다.


트랜잭션 여부에 상관없이 이벤트 발생 시점에 이벤트를 처리하고 싶다면 @EventListener를 사용하면 된다.


3. @EnableAsync와 @Async로 비동기로 핸들러 실행하기

이벤트 핸들러를 비동기로 처리하고 싶다면 @EnableAsync와 @Async를 사용하면 된다. 스프링 설정 클래스에 @EnableAsync를 추가했다면, @TransactionalEventListener와 @Async를 함께 사용해서 이벤트를 트랜잭션 커밋 이후에 비동기로 처리할 수 있다.



import org.springframework.scheduling.annotation.Async;

import org.springframework.stereotype.Component;

import org.springframework.transaction.event.TransactionalEventListener;


@Component

public class EventHandler {


    @Async

    @TransactionalEventListener

    public void handle(OrderCanceledEvent event) {

        // ... 이벤트 처리

    }




저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. coding8282 2017.03.19 09:43 신고  댓글주소  수정/삭제  댓글쓰기

    저는 Domain Event Handler를 따로따로 만들어서 사용했었는데, 이 방법을 적용하니 정말 좋네요. 비동기 처리도 간단하구요~... 응용 범위가 굉장히 넓을 것 같습니다~~~ 감사합니다

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

내장 톰캣을 사용해서 동작하는 스프링부트 기반 웹어플리케이션을 AJP 프로토콜을 이용해서 아파치 HTTPD 웹 서버와 연동할 일이 생겼다. 연동을 위해 할 내용은 생각보다 간단했다. 다음의 두 가지만 해 주면 된다.

  1. 스프링부트 어플리케이션: 내장 톰캣을 위한 AJP 커넥터 설정
  2. 아파치 웹 서버 : ProxyPass로 ajp 연동 설정

스프링부트 내장 톰캣 설정


내장 톰캣 설정에 AJP 커넥터를 추가한다.


@Configuration

public class ContainerConfig {

    @Bean

    public EmbeddedServletContainerCustomizer containerCustomizer() {

        return container -> {

            TomcatEmbeddedServletContainerFactory tomcat

                    (TomcatEmbeddedServletContainerFactory) container;


            Connector ajpConnector = new Connector("AJP/1.3");

            ajpConnector.setProtocol("AJP/1.3");

            ajpConnector.setPort(9090);

            ajpConnector.setSecure(false);

            ajpConnector.setAllowTrace(false);

            ajpConnector.setScheme("http");

            tomcat.addAdditionalTomcatConnectors(ajpConnector);

        };

    }

}


아파치 설정


아파치에 톰캣 관련 설정을 추가한다.


ProxyPass "/contextPath" "ajp://localhost:9090/contextPath"


그리고 아파치 서버를 재시작하면 끝이다.

저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 임예준 2016.06.14 10:29 신고  댓글주소  수정/삭제  댓글쓰기

    이걸 몰라서 boot 내장 톰캣을 못쓰고,
    static 과 java 리소스를 httpd 와 외부 톰캣에 각각 배포 해야 되나 싶었는데 말입니다.
    ProxyPass를 사용하면 JK를 안써도 되는거죠?

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링 부트는 application.properties 파일을 이용해서 설정을 제공한다. 이 파일에는 부트가 제공하는 프로퍼티뿐만 아니라 커스텀 프로퍼티를 추가할 수 있다. 커스텀 프로퍼티를 사용하는 몇 가지 방법이 있는데 그 중에서 설정 프로퍼티 클래스를 사용하면 관련 설정을 한 클래스에서 관리할 수 있어 편리하다.


설정 프로퍼티 클래스를 사용하는 방법은 간단하다. 먼저, 설정 프로퍼티 클래스로 사용할 클래스를 작성한다. 이 클래스는 다음과 같이 작성한다.

  • @ConfigurationProperties 애노테이션을 클래스에 적용한다. application.properties 파일에서 사용할 접두어를 지정한다.
  • application.properties 파일에 설정한 프로퍼티 값을 전달받을 setter를 추가한다.
  • 프로퍼티 값을 참조할 때 사용할 get 메서드를 추가한다.
다음은 설정 프로퍼티 클래스의 작성 예이다.


@ConfigurationProperties(prefix = "eval.security")

public class SecuritySetting {

    private String authcookie;

    private String authcookieSalt;


    public String getAuthcookie() {

        return authcookie;

    }


    public void setAuthcookie(String authcookie) {

        this.authcookie = authcookie;

    }


    public String getAuthcookieSalt() {

        return authcookieSalt;

    }


    public void setAuthcookieSalt(String authcookieSalt) {

        this.authcookieSalt = authcookieSalt;

    }


}


application.properties 파일의 프로퍼티의 설정 프로퍼티 클래스의 프로퍼티는 다음과 같이 매칭된다.


* application.properties 프로퍼티 이름 = prefix + "." + setter 프로퍼티 이름


예를 들어, 위 코드에서 prefix는 "eval.security"이므로 setAuthcookie()에 해당하는 프로퍼티는 "eval.security.authcookie"가 된다. 만약 setter의 프로퍼티 이름 중간에 대문자가 포함되어 있다면 다양한 매핑을 지원한다. 예를 들어, 위 코드에서 authcookieSalt가 중간에 대문자를 포함하고 있는데 이 경우 다음과 같은 프로퍼티로부터 값을 가져올 수 있다.

  • eval.security.authcookieSalt
  • eval.security.authcookie-salt
  • eval.security.authcookie_salt
  • EVAL_SECURITY_AUTHCOOKIE_SALT

@ConfigurationProperties를 적용한 클래스를 만들었다면, 다음 할 일은 빈으로 등록하는 것이다. 스프링 빈으로 등록하는 방법은 간단하다. 다음의 두 가지 방식 중 하나를 사용하면 된다.

  • @EnableConfigurationProperties을 이용해서 지정하기
  • 설정 프로퍼티 클래스에 @Configuration 적용하기 (또는 설정 프로퍼티 클래스를 @Bean으로 등록하기)

먼저 @EnableConfigurationProperties을 사용하는 방법은 다음과 같다. @EnableConfigurationProperties을 설정 클래스에 추가하고 @EnableConfigurationProperties의 값으로 @ConfigurationProperties를 적용한 클래스를 지정하면 된다. 이 경우 @EnableConfigurationProperties는 해당 클래스를 빈으로 등록하고 프로퍼티 값을 할당한다.


@SpringBootApplication

@EnableConfigurationProperties(SecuritySetting.class)

public class Application { ... }


두 번째 방법은 설정 프로퍼티 클래스를 빈으로 등록하는 것이다. 스프링 부트는 컴포넌트 스캔을 하므로 설정 프로퍼티 클래스에 @Configuration을 붙이면 자동으로 빈으로 등록된다. 스프링 부트는 해당 빈이 @ConfigurationProperties를 적용한 경우 프로퍼티 값을 할당한다.


@ConfigurationProperties 적용 클래스를 빈으로 등록했다면 이제 설정 정보가 필요한 곳에서 해당 빈을 주입받아 사용하면 된다. 예를 들면 다음과 같이 자동 주입 받아 필요한 정보를 사용하면 된다.


@Configuration

public class SecurityConfig {

    @Autowired

    private SecuritySetting securitySetting;


    @Bean

    public Encryptor encryptor() {

        Encryptor encryptor = new Encryptor();

        encryptor.setSalt(securitySetting.getAuthcookieSalt());

        ...

    }


@ConfigurationProperties를 사용할 때의 장점은 다음과 같다.

  • 필요한 외부 설정을 접두어(prefix)로 묶을 수 있고 중첩 설정을 지원한다. (예, YAML을 사용하면 계층 구조로 묶을 수 있다.)
  • 기본 값을 쉽게 지정할 수 있다. 설정 프로퍼티 클래스의 필드에 기본 값을 주면 된다.
  • int, double 등 String 이외의 타입을 설정 프로퍼티 클래스의 프로퍼티에 사용할 수 있다. 설정 파일의 문자열을 설정 프로퍼티 클래스의 프로퍼티 타입으로 스프링이 알아서 변환해준다.



저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링을 이용해서 회사 서비스를 개발하다보면 은근히 비슷하게 작성하는 설정 코드가 존재하기 마련이다. 예를 들어, 여러 서비스에서 LDAP 연동을 위해 같은 스프링 시큐리티 설정을 사용하거나 내부 API 연동을 위해 동일한 설정을 사용할 수 있다. 또한, 데이터 저장소에 따라 사내에서 제안하는 설정 가이드가 존재할 수도 있다.


이런 류의 설정은 여러 곳에서 반복되기 때문에 http://javacan.tistory.com/entry/spring-at-enable-config 글에서 설명한 @Enable을 이용해서 커스텀 설정 편의 기능을 제공하면 편리하다. 스프링부트를 사용한다면, 부트의 자동 설정 기능까지 함께 지원함으로써 설정 편의 기능을 완성할 수 있다.


스프링캠프2016에서 이수홍님이 발표하신 자료(http://www.slideshare.net/sbcoba/2016-deep-dive-into-spring-boot-autoconfiguration-61584342)를 보면 스프링 부트의 자동 설정 기능에 대해 많은 내용을 알 수 있다. 이 자료와 함께 이수홍님 깃헙 프로젝트(https://github.com/sbcoba/spring-camp-2016-spring-boot-autoconfiguration)에서 관련 코드도 참고할 수 있다.


이수홍님 자료를 바탕으로 자동 설정 기능을 추가하는 방법을 정리하면 다음과 같다.

  • 자동 설정 기능 제공 모듈: 설정을 제공하는 @Configuration 적용 클래스 구현
  • 자동 설정 기능 제공 모듈: spring.factories 파일 작성
  • 적용: 자동 설정이 필요한 프로젝트에서 모듈에 대한 의존 추가

1. 자동으로 설정할 내용을 담을 @Configuration 클래스 작성


자동 설정 기능을 위한 코드를 담고 있는 모듈(jar 파일)은 @Configuration 애노테이션을 사용해서 설정 클래스를 작성한다.


@Configuration

@ConditionalOnMissingBean(AuthClient.class)

public class CompAutoConf {


    @Bean

    public AuthClient authClient() {

        return new HttpAuthClient();

    }



이 설정 클래스는 일반적인 스프링 설정 클래스와 차이가 없다. 보통 자동 설정 기능에서 사용하는 @Configuration 클래스는 조건에 따라 설정 사용 여부를 결정하기 위해 @Conditional 애노테이션을 포함한다. 위 코드의 경우 AuthClient 타입의 빈이 없는 경우에만 CompAutoConf를 설정 클래스로 사용하도록 했다.



2. META-INF/spring.factories 파일에 설정 클래스 지정하기


설정 클래스를 구현했다면, 그 다음으로 할 일은 spring.factories 파일에 설정 클래스를 지정하는 것이다. 클래스패스 위치에(메이븐 같으면 src/main/resources 폴더에) META-INF/spring.factories 파일을 만들고, org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티의 값으로 자동 설정으로 사용할 클래스를 값으로 준다.


# Auto Configure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=mycompany.CompAutoConf



3. 자동 설정 모듈 사용


앞서 언급한 이수홍님 자료를 보면, 스프링 부트는 클래스패스에 위치한 모든 META-INF/spring.factories 파일의 org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티 값을 읽어와 설정 클래스로 사용한다. 따라서, 스프링 부트 프로젝트에 자동 설정 모듈에 대한 의존을 추가해주기만 하면 된다.


@SpringBootApplication // META-INF/spring.factories에 지정한 설정 클래스 사용

public class Application {


    public static void main(String[] args) {

        ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);

        // CompAutoConf에 정의한 @Bean 설정에 따라 HttpAuthClient 객체를 빈으로 등록

        AuthClient client = ctx.getBean(AuthClient.class);

        ...

    }

}



조건에 따라 설정 적용하기: @Conditional


앞서 CompAutoConf 예를 보면 @ConditiionalOnMissingBean(AuthClient.class)가 있는데, 이는 AuthClient 타입의 빈이 없는 경우에만 해당 설정을 사용한다는 것을 의미한다. 만약 다음과 같이 설정에 AuthClient 타입 빈이 포함되어 있으면 @ConditiionalOnMissingBean(AuthClient.class)에 따라 CompAutoConf 설정 클래스를 사용하지 않는다.


@SpringBootApplication

public class Application {


    @Bean

    public AuthClient authClient() {

        return new ProtobuffAuthClient();

    }


    ...


@Conditional은 스프링 4.0부터 지원하는데, 스프링은 클래스 존재 여부, 프로퍼티 존재 여부, 빈 존재 여부 등 다양한 @Conditional을 제공하고 있다.


설정 순서 지정하기: @AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder


자동 설정 클래스를 적용하는 순서가 중요할 때가 있다. 예를 들어, 내가 만들 자동 설정이 DataSource를 설정한다고 해보자. 이 자동 설정은 스프링 부트가 제공하는 DataSource 자동 설정 클래스인 DataSourceAutoConfiguration보다 먼저 실행되어야 한다. 그렇지 않을 경우, DataSourceAutoConfiguration이 생성한 DataSource와 내가 만든 자동 설정 기능이 생성한 DataSource가 함께 만들어진다.


보통 이는 원하는 결과가 아니다. 스프링 부트의 DataSourceAutoConfiguration는 DataSource 타입 빈이 존재하지 않는 경우에만 DataSource를 만드므로, 스프링 부트의 DataSourceAutoConfiguration보다 내가 만든 자동 설정 클래스가 먼저 실행되면 여러 DataSource가 만들어지는 문제를 피할 수 있다. 이를 위한 간단한 방법은 내가 만든 자동 설정 클래스에 @AutoConfigureBefore를 붙이는 것이다.


@AutoConfigureBefore(DataSourceAutoConfiguration.class)

@ConditionalOnMissingBean(DataSource.class)

public class MyCompDataSourceAutoConfiguration {

    ...

}


@AutoConfigureAfter를 사용해서 다른 설정 뒤에 자동 설정을 적용하거나 @AutoConfigureOrder를 사용해서 설정의 우선순위를 지정할 수 있다.

저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링 3.0 버전까지만 해도 스프링 자바 설정이 상대적으로 인기가 없었는데, 주된 이유는 XML의 네임스페이스 기반 설정 편의 기능을 대체하지 못했기 때문이었다. 스프링 3.1 버전부터 자바 설정도 편의 기능을 제공하기 시작했는데 그것이 바로 @Enable로 시작하는 애노테이션 설정이다. @Enable 설정 편의 애노테이션은 사용자를 대신해서 많은 설정을 대신 해준다. 예를 들어, @EnableWebMvc의 경우 100여 줄에 가까운 설정 코드륻 대신 한다. 


스프링이 제공하는 @Enable 설정 편의 기능뿐만 아니라 직접 @Enable 설정 편의 애노테이션을 만들 수도 있는데, 크게 세 가지 방법이 존재한다.


1. 설정을 임포트하는 @Enable 애노테이션


첫 번째 방법은 @Configuration 설정을 임포트하는 @Enable 애노테이션을 작성하는 것이다. 예를 들어, 다음 @Configuration 설정을 보자.


@Configuration

public class MyCompanyConfig {

    

    @Bean

    public Authenticator authenticator() {

        Authenticator authenticator = new Authenticator();

        ...

        return authenticator;

    }


}


이 설정을 자동으로 처리하는 @Enable 애노테이션은 다음과 같이 구현할 수 있다.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Import(MyCompanyConfig.class)

@interface EnableMyCompany {}


@Import 속성은 설정으로 사용할 클래스를 지정한다.


이제 @EnableMyCompany 애노테이션을 설정에 추가하면 MyCompanyConfig에 포함된 설정을 자동으로 포함시킨다.


@Configuration

@EnableMyCompany

public class AppConfig {


    @Autowired

    private Authenticator authenticator;


    ...

}



2. ImportSelector 사용하기


두 번째 방법은 ImportSelector를 사용하는 것이다. 애노테이션 애트리뷰트 값에 따라 다른 설정 클래스를 사용하고 싶을 때 이 방식을 사용한다. 예를 들어, 다음 @Enable 애노테이션을 보자.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Import(CompanyConfigSelector.class)

public @interface EnableAuthConfig {

    ClientType type() default ClientType.HTTP;

}


이 애노테이션은 type 애트리뷰트를 갖고 있다.


@Import에 지정한 CompanyConfigSelector는 ImportSelector 인터페이스를 구현한 클래스로서 다음 코드처럼 @EnableAuthConfig 애노테이션의 type 애트리뷰트 값에 따라 사용할 설정 클래스 이름을 리턴하도록 구현한다. ImportSelector 자체는 설정 클래스가 아니므로 @Configuration이 붙지 않는다.


public class CompanyConfigSelector implements ImportSelector {

    @Override

    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        Map<String, Object> attributesMap = 

                 importingClassMetadata.getAnnotationAttributes(

                         EnableAuthConfig.class.getName(), false);

        AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributesMap);

        ClientType typeValue = attributes.<ClientType>getEnum("type");

        if (typeValue == ClientType.HTTP) {

            return new String[]{HttpAuthClientConfiig.class.getName()};

        } else if (typeValue == ClientType.PROTOBUFF) {

            return new String[]{ProtobuffAuthClientConfig.class.getName()};

        }

        return new String[0];

    }

}



ImportSelector의 selectImport() 메서드는 String 배열을 리턴하는데, 이 배열은 설정으로 사용할 클래스 이름을 값으로 갖는다. 위 코드의 경우 @EnableAuthConfig 애노테이션의 type 애트리뷰트 값에 따라 HttpAuthClientConfig나 ProtobuffAuthClient를 설정으로 사용하도록 구현하고 있다. 이 두 클래스는 다음과 같이 @Configuration을 이용한 설정 클래스이다.


@Configuration

public class HttpAuthClientConfiig {

    @Bean

    public HttpAuthClient authClient() {

        return new HttpAuthClient();

    }

}


이제 @EnableAuthConfig 애노테이션을 사용하면 type 애트리뷰트 값에 따라 알맞은 설정을 사용하게 된다. 예를 들어, 아래 코드는 ProtobuffAuthClientConfig를 설정으로 사용하게 된다.


@Configuration

@EnableAuthConfig(type = ClientType.PROTOBUFF)

public class AppConfig {}



3. ImportBeanDefinitionRegistrar 사용하기


세 번째 방법은 ImportBeanDefinitionRegistrar을 사용하는 것이다. ImportSelector가 설정으로 사용할 클래스 이름을 리턴하는 방식이라면 ImportBeanDefinitionRegistrar는 빈 설정을 직접 등록하는 방식이다. 이 방식은 기존에 XML 스키마 확장 방식을 자바 설정으로 마이그레이션하고자 할 때 사용할 때 좋다.


Enable 애노테이션은 동일하다. 다음과 같이 @Import로 ImportBeanDefinitionRegistrar 인터페이스를 구현한 클래스를 지정하면 된다.


@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.TYPE)

@Import(CompanyConfigRegistrar.class)

public @interface EnableCompSecConfig {

    ClientType type() default ClientType.HTTP;

}


ImportBeanDefinitionRegistrar 인터페이스를 구현한 클래스는 애노테이션 애트리뷰트 정보를 이용해서 알맞은 빈 설정을 등록하면 된다. 다음은 간단한 구현 예이다.


public class CompanyConfigRegistrar implements ImportBeanDefinitionRegistrar {

    @Override

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,

            BeanDefinitionRegistry registry) {

        Map<String, Object> attributesMap = 

                importingClassMetadata.getAnnotationAttributes(

                        EnableCompSecConfig.class.getName(), false);

        AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributesMap);

        ClientType typeValue = attributes.<ClientType>getEnum("type");

        if (typeValue == ClientType.HTTP) {

            BeanDefinition beanDef = new RootBeanDefinition("comp.config.HttpAuthClient");

            registry.registerBeanDefinition("authClient", beanDef);

        } else if (typeValue == ClientType.PROTOBUFF) {

            BeanDefinition beanDef = 

                    new RootBeanDefinition("comp.config.ProtobuffAuthClientConfiig");

            registry.registerBeanDefinition("authClient", beanDef);

        }

    }

}



이제 @EnableCompSecConfig를 사용하면 CompanyConfigRegistrar에서 등록한 빈 설정이 자등으로 추가된다.


@Configuration

@EnableCompSecConfig(type = ClientType.HTTP)

public class AppConfig {



저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

최근에 특정 개발 방식을 시도할 겸, 몇 가지 구현 기술도 익힐 겸해서 회사에서 필요한 어플리케이션을 만들어보고 있다. 스프링 부트와 자바, 스칼라, JPA, AngularJS 등을 사용하고 있고, 뷰 템플릿으로는 JSP를 이용했다.


배포의 단순함을 위해 스프링 부트의 실행가능한 war를 만든 뒤, 로컬PC와 운영될 서버에 올려서 기능 테스트를 진행했다. 테스트를 진행하는 과정에서 다음과 같은 증상을 발견했다.

  • 리눅스 서버에서 war를 실행: 동일한 JSP 뷰를 실행할 때, 약 4초 간격으로 응답 속도가 느려짐. 예를 들어, 새로고침을 짧은 간격으로 실행하면, 30~40ms 걸리던 응답 시간이 약 4초마다 300~400ms가 걸림
  • 윈도우 개발 PC에서 war를 실행: 증상 없음
  • 맥 개발 PC에서 war를 실행: 리눅스 서버와 동일 증상 발생
  • JSP를 사용하지 않는 JSON 응답의 경우 비슷한 증상 없음
  • mvn spring-boot:run으로 실행할 때에는 맥 개발 PC에서 이런 증상이 발생하지 않음
한 JSP 코드에서 <jsp:include>를 이용해서 다른 JSP를 include 할 경우, 응답 속도가 더 느려졌다. 몇 가지 의심되는 것이 있어서 차례대로 확인해 나가다보니 결과적으로 응답 속도를 느리게 만드는 용의자를 찾았다. 그 용의자는 톰캣 JSP 엔진 Jasper의 JSP 변경 여부 검사 옵션이었다.

톰캣의 JSP 엔진인 Jasper는 기본적으로 다음과 같이 설정되어 있다.
  • 개발모드: JSP의 변경 여부를 확인할지 여부 지정. 기본 값은 true.
  • 변경확인간격: 개발모드가 true인 경우, 지정한 간격이 지날 때 마다 실행하는 JSP의 변경을 확인. 기본 값은 4초.
4초! 그렇다. 4초다. 앞서 이상 증상에서 발견된 그 약 4초라는 시간이 설정에 있는 것이다. 원인은 잘 모르지만, 의심되는 것은 다음과 같다.
  • 실행 가능한 war로 묶인 경우, 리눅스 기반 OS의 JDK 사용시 war 파일에 포함된 파일의 변경 여부를 확인하는 과정에서 실행 속도가 느려짐
자바8로만 해봤기 때문에 자바7이나 자바6에서 동일 증상이 발생하는지 여부는 모르지만, 어쨋든 war 파일에 포함된 파일을 탐색하는 과정에서 속도가 느려지는 것 같다. 왜 그런지 알고 싶지만, 여기까지 파고 있을 시간이 없어서 다음 과정을 진행했다.

실행 가능 war의 경우 JSP 변경 여부를 확인하는 과정은 별 의미가 없기 때문에, 임베디드 톰캣이 JSP 변경 여부를 확인하지 않도록 스프링 설정을 변경했다. 이때 사용한 것이 TomcatContextCustomizer이다. 아래 코드는 Jasper의 개발모드를 false로 설정하기 위해 사용한 스프링 설정의 일부이다.

import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;

@Configuration
public class ContainerConfig {

    @Bean
    public ServerProperties serverProperties() {
        return new ServerProperties();
    }

    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer(){
        return new TomcatContainerCustomizer(tomcatJspServletConfig());
    }

    @Bean
    public TomcatJspServletConfig tomcatJspServletConfig() {
        return new TomcatJspServletConfig();
    }

    static class TomcatContainerCustomizer implements EmbeddedServletContainerCustomizer {

        private TomcatJspServletConfig tomcatJspServletConfig;

        public TomcatContainerCustomizer(TomcatJspServletConfig tomcatJspServletConfig) {
            this.tomcatJspServletConfig = tomcatJspServletConfig;
        }

        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            ...
            if (container instanceof TomcatEmbeddedServletContainerFactory) {
                TomcatEmbeddedServletContainerFactory tomcatContainer = 
                      (TomcatEmbeddedServletContainerFactory) container;
                tomcatContainer.addContextCustomizers(tomcatJspServletConfig);
            }
        }

    }

    public static class TomcatJspServletConfig implements TomcatContextCustomizer {

        private ContainerJasperSetting jasperSetting;

        public TomcatJspServletConfig(ContainerJasperSetting jasperSetting) {
            this.jasperSetting = jasperSetting;
        }

        @Override
        public void customize(Context context) {
            // TomcatEmbeddedServletContainerFactory가 설정한 JspServlet의 이름이 "jsp"
            Wrapper jsp = (Wrapper)context.findChild("jsp");
            // JspServlet의 개발모드를 비활성화
            // 실제로는 application.properties 파일에 설정할 수 있는 프로퍼티 추가해서 구현
            jsp.addInitParameter("development", "false");
        }
    }

}

위 코드에서는 개발모드를 비활성화했지만, 실제 코드에서는 application.properties 파일에 "jsp.jasper.development" 프로퍼티의 값에 따라서 개발모드를 활성화/비활성화하도록 구현했다.

위와 같이 Jasper 엔진의 개발모드를 비활성화한 뒤에 실행가능한 war를 다시 실행해보니, 4초 간격으로 응답 시간이 느려지는 증상이 사라졌다.만세다!


저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.




저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

신림프로그래머 모임 후 몇몇 분과 차 마시면서 대화를 하다가, 한 분으로부터 다음과 같은 상황을 처리하기 위해 시도했던 방법을 듣게 되었다.

  • DB 구조의 변경 (클러스터에서 MySQL 리플리케이션으로)
    • 변경 기능은 마스터 DB 사용
    • 조회 기능은 슬레이브 DB 사용
  • 기존 코드를 최대한 바꾸지 않길 원함
    • DB 구조로 바뀌기 전에 이미 <mybatis:scan> 태그를 사용해서 DAO 인터페이스만 정의
    • 기존 서비스 클래스는 모두 DAO 사용
    • 같은 DAO를 사용하더라도 변경 기능 서비스 클래스에서 사용하는 DAO는 마스터 DB에 붙어야 하고, 읽기 기능 서비스 클래스에서 사용하는 DAO는 슬레이브 DB에 붙어야 함 

시도한 방법을 듣다보니 트랜잭션 처리나 동시성 접근 등에서 문제의 소지가 있을 것 같아, 다른 방법을 찾아봤는데 약간의 노력으로 위 상황을 해소할 수 있을 것 같아 정리해본다.


먼저 마스터DB와 슬레이브DB에 대한 트랜잭션 관리자, DataSource, SqlSessionFactoryBean을 설정한다.


<!-- master db -->

<bean id="masterTx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="masterDS" />

</bean>


<bean id="masterDS" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">

    ...

</bean>


<bean id="masterSqlSF" class="org.mybatis.spring.SqlSessionFactoryBean">

    <property name="dataSource" ref="masterDS" />

    ...

</bean>


<!-- slave db -->

<bean id="slaveTx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

    <property name="dataSource" ref="slaveDS" />

</bean>


<bean id="slaveDS" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">

    ...

</bean>


<bean id="slaveSqlSF" class="org.mybatis.spring.SqlSessionFactoryBean">

    ...

</bean>


그 다음 <mybatis:scan>에서 사용할 BeanNameGenerator 구현 클래스를 마스터용과 슬레이브용으로 작성한다.


import org.springframework.beans.factory.config.BeanDefinition;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;

import org.springframework.beans.factory.support.BeanNameGenerator;

import org.springframework.util.StringUtils;


public class MasterBeanNameGenerator implements BeanNameGenerator {

    @Override

    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        String fullClassName = definition.getBeanClassName();

        String simpleClassName = StringUtils.unqualify(fullClassName);

        return StringUtils.uncapitalize(simpleClassName) + "Master";

    }

}


public class SlaveBeanNameGenerator implements BeanNameGenerator {

    @Override

    public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

        String fullClassName = definition.getBeanClassName();

        String simpleClassName = StringUtils.unqualify(fullClassName);

        return StringUtils.uncapitalize(simpleClassName) + "Slave";

    }

}


MasterBeanNameGenerator는 검색된 DAO 인터페이스가 "dao.MemberDao"일 경우, 자동 생성되는 빈 객체의 이름을 "memberDaoMaster"로 생성한다. 비슷하게 SlaveBeanNameGenerator는 클래스 이름 뒤에 'Slave'를 붙인다.


이제 마스터용 설정과 슬레이브용 빈과 클래스를 사용해서 <mybatis:scan> 태그를 설정한다.


<mybatis:scan base-package="dao" factory-ref="masterSqlSF" 

                   name-generator="mybatis.MasterBeanNameGenerator" />


<mybatis:scan base-package="dao" factory-ref="slaveSqlSF" 

                   name-generator="mybatis.SlaveBeanNameGenerator" />


위와 같이 설정하면 한 DAO 인터페이스에 대해 두 개의 빈 객체를 생성하게 된다. 예를 들어, 자동 검색된 인터페이스가 MemberDao일 경우, 마스터용 MyBatis SqlSession을 사용하는 DAO 객체는 'memberDaoMaster' 빈으로 생성되고 슬레이브용 MyBatis SqlSession을 사용하는 DAO 객체는 'memberDalSlave' 빈으로 생성된다.


이제 마스터를 사용해야 하는 서비스 객체와 슬레이브를 사용해야 하는 서비스 객체에 알맞은(즉, 타입은 같지만 사용하는 DB가 서로 다른) DAO 객체를 주입해준다.


<!-- master 사용 서비스 -->

<bean id="placeOrderService" class="service.PlaceOrderServiceImpl">

    <property name="itemDao" ref="itemDaoMaster" />

    <property name="paymentInfoDao" ref="paymentInfoDaoMaster" />

    <property name="purchaseOrderDao" ref="purchaseOrderDaoMaster" />

</bean>


<!-- slave 사용 서비스 -->

<bean id="itemListService" class="service.ItemListService">

    <property name="itemDao" ref="itemDaoSlave" />

</bean>


이제 거의 다 왔다. 남은 작업은 서비스 별로 다른 트랜잭션 관리자를 사용하도록 설정해주는 것이다.


public class PlaceOrderServiceImpl implements PlaceOrderService {

    // 마스터 용 DAO 주입

    private ItemDao itemDao;

    private PaymentInfoDao paymentInfoDao;

    private PurchaseOrderDao purchaseOrderDao;


    @Transactional("masterTx") // 마스터용 트랜잭션 관리자 사용

    @Override

    public PurchaseOrderResult order(PurchaseOrderRequest orderRequest) {

        ...

    }

    // set 메서드

}


public class ItemListService {

    private ItemDao itemDao; // 슬레이브용 DAO 주입


    @Transactional("slaveTx") // 슬레이브용 트랜잭션 관리자 사용

    public List<Item> getAll() {

        ...

    }

    ...

}


이제 끝났다. 위 설정을 통해 order() 메서드는 마스터용 트랜잭션 관리자를 통해서 트랜잭션을 생성하고, 마스터용 DAO를 사용해서 마스터 DB에 붙게된다. 비슷하게 getAll() 메서드는 슬레이브용 트랜잭션 관리자를 이용해서 트랜잭션을 생성하고 슬레이브용 DAO를 사용해서 슬레이브 DB에 붙게된다.


저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

JPA를 이용해서 코딩 장난을 하던 중 도메인 모델상의 InetAddress를 DB 테이블의 VARCHAR 타입 컬럼에 매핑해야 할 요구가 생겼다. JPA 2.1 버전을 사용하고 있었는데, JPA 2.1에서 제공하는 AttributeConverter라는 걸 사용하면 매우 간단하게 InetAddress 프로퍼티 타입과 DB VARCHAR 타입 사이의 매핑을 처리할 수 있는 걸 알 수 있었다.


사용법은 간단하다. 먼저 AttributeConverter 인터페이스를 구현한 컨버터 클래스를 구현현다.


import javax.persistence.AttributeConverter;

import javax.persistence.Converter;

import java.net.InetAddress;

import java.net.UnknownHostException;


@Converter

public class InetAddressConverter implements AttributeConverter<InetAddress, String> {

    @Override

    public String convertToDatabaseColumn(InetAddress attribute) {

        if (attribute == null)

            return null;

        else

            return attribute.getHostAddress();

    }


    @Override

    public InetAddress convertToEntityAttribute(String dbData) {

        if (dbData == null || dbData.isEmpty()) return null;

        try {

            return InetAddress.getByName(dbData);

        } catch (UnknownHostException e) {

            throw new RuntimeException(String.format(

                "InetAddressConverter fail to convert %s to InetAddress: %s", dbData, e.getMessage()), 

            e);

        }

    }

}


AttributeConverter의 타입 파라미터에서 첫 번째 타입 파라미터는 자바쪽의 프로퍼티 타입을, 두 번째 타입 파라미터는 DB의 컬럼 타입을 의미한다. 위 코드의 경우 자바 모델에서 사용할 InetAddress 타입 프로퍼티와 DB 테이블에서 사용할 문자열 타입 간의 변환을 처리하는 컨버터를 구현한 것이다.


컨버터를 구현했으면, JPA의 매핑 클래스에서 @Convert 애노테이션을 사용해서 값을 변환대상 프로퍼티에 컨버터를 지정해주면 된다. 


import javax.persistence.Convert;


@Entity

public class IpRange {


    @Column(name = "RANGE_FROM")

    @Convert(converter = InetAddressConverter.class)

    private InetAddress from;



이제 JPA는 InetAddress 타입의 from 프로퍼티 값을 DB에 저장할 때 InetAddressConverter를 이용해서 문자열로 변환한 뒤에 저장한다. 비슷하게 DB에서 값을 읽어올 때 InetAddressConverter를 이용해서 문자열을 InetAddress로 변환해서 from 프로퍼티에 할당한다.

저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 궁금 2015.04.22 18:06 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요. 가끔 좋은 정보 많이 가져가는 개발자입니다.

    여기다 질문 올려도 되는건지 모르겠지만 JPA에 관련 내용이라서 여기다 적었습니다.
    @elementCollection 설정 시 Collection Table이 만들어지는데 Set타입이기 때문에
    Collection Table은 fk 컬럼을 포함하여 Embeddable 클래스의 모든 속성이 PK가 생성되는
    걸로 알고 있는데 PK가 생성되지 않습니다. embedded 하는 쪽의 PK만 FK로 생성되고
    PK는 만들어지지 않는데 그 이유가 무엇일까요? JPA 2.1에 Hibernate 4.x 를 영속 엔진으로
    테스트를 하고 있습니다. stackoverflow나 eclipse 커뮤니티에 이런 비슷한 질문이 올라와
    있는 것을 본적이 있는데 시원한 답변이 없더군요. 미리 감사드립니다~

    • 최범균 madvirus 2015.04.26 22:42 신고  댓글주소  수정/삭제

      음... 테이블 자동 생성이나 DDL 생성 기능을 말씀하시는 것 같은데, @ElementCollection이나 @Embeddable 등 애노테이션을 설정한 클래스를 같이 올려주시면 같이 테스트 해 보는데 도움이 되지 싶네요.

  2. 궁금 2015.05.04 23:05 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요. 답변을 이제야 확인했네요. 이 건과 관련해서 stackoverflow에 올린 글의 링크를 적고 갑니다. 제가 받은 답변은 JPA 스펙에서는 원래 그런 PK는 만들지 않는다 였습니다. http://stackoverflow.com/questions/29797352/jpa-with-hibernate-4-x-elementcollection-primary-key

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링 4.1 버전에 새롭게 추가된 테스트 기능 중에서 @Sql 애노테이션이 있는데, @Sql 애노테이션은 지정한 스크립트를 실행해주는 애노테이션이다.  스프링 3 버전에 추가된 ResourceDatabasePopulator를 사용해도 되지만, @Sql 애노테이션을 이용하면 매우 편리하게 테스트에서 데이터를 초기화할 수 있다.


사용방법은 매우 간단한다.

  1. 테스트를 실행하기 전에 사용할 쿼리 목록을 담은 파일을 작성한다. (각 쿼리는 ';'로 구분한다.)
  2. @Sql 애노테이션을 테스트 클래스나 테스트 메서드에 적용한다.

예를 들어, 아래 코드는 각 테스트 메서드를 실행하기 전에 member.init1.sql 파일과 memberinit2.sql 파일에 입력한 쿼리 목록을 실행한다.


@ContextConfiguration("classpath:spring-*.xml")

@RunWith(SpringJUnit4ClassRunner.class)

@Sql({"classpath:member.init1.sql", "classpath:member.init2.sql"})

public class MemberIntTest {

    @Autowired private MemberService memberService;


    @Test

    public void list() {

        …

    }


    @Test

    public void regist() {

        …

    }

}


@Sql에 명시한 파일은 테스트를 실행하는데 적합한 상태로 DB를 초기화하기 위해 DELETE, TRUNCATE, INSERT, CREATE와 같은 쿼리를 포함하게 된다.


클래스에 @Sql 애노테이션을 적용하면 각 테스트 메서드마다 적용되며, 테스트 메서드에 적용하면 해당 테스트를 실행할 때에만 사용된다. 예를 들어, 아래 코드와 같이 테스트 클래스와 메서드에 각각 @Sql 애노테이션을 적용하면, regist() 테스트 메서드를 실행할 때에는 "member.init3.sql" 만을 사용해서 쿼리를 실행하고, list() 테스트 메서드를 실행할 때에는 "member.init1.sql"과 "member.init2.sql"을 사용해서 쿼리를 실행한다.


@ContextConfiguration("classpath:spring-*.xml")

@RunWith(SpringJUnit4ClassRunner.class)

@Sql({"classpath:member.init1.sql", "classpath:member.init2.sql"})

public class ShopIntTest {

    @Autowired private MemberService memberService;


    @Test

    public void list() {

        …

    }


    @Sql("classpath:member.init3.sql")

    @Test

    public void regist() {

        …

    }

}


@Sql은 테스트 메서드 실행 전과 실행 후 중에서 언제 쿼리를 실행할지 여부를 지정할 수 있다. @Sql의 executionPhase 속성의 값으로 ExecutionPhase 열거 타입에 정의된 BEFORE_TEST_METHOD나 AFTER_TEST_METHOD를 설정하면 된다. 기본 값은 BEFORE_TEST_METHOD 이다. 다음 코드는 executionPhase 속성의 설정 예를 보여주고 있다.


@Sql("init.sql")

@Sql(scripts="remove.sql", executionPhase=ExecutionPhase.AFTER_TEST_METHOD)

@Test public void someTest() { … }


위 코드에서 보듯이 자바8을 사용하면 @Sql 애노테이션을 여러 개 사용해서 실행할 쿼리를 지정할 수 있다. 자바 7 이하 버전을 사용한다면, 아래 코드처럼 @SqlGroup 애노테이션을 이용하면 여러 개의 @Sql을 한 테스트 클래스나 메서드에 적용할 수 있다.


@SqlGroup( {

    @Sql("init.sql"), @Sql(scripts="clear.sql", executionPhase=ExecutionPhase.AFTER_TEST_METHOD)} )

@Test public void someTest() { … }


@Sql 애노테이션은 별도 설정을 하지 않으면 @ContextConfiguration에 지정한 설정 정보에 있는 DataSource 빈을 사용해서 스크립트를 실행하고, 트랜잭션 관리자가 존재할 경우 해당 트랜잭션 관리자를 이용해서 트랜잭션 범위 내에서 스크립트를 실행한다.

저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

스프링 MockMv의 단독 모드를 사용해서 한 개 컨트롤러에 대한 MockMvc를 생성하고, 테스트 코드를 작성했다.


mockMvc = MockMvcBuilders.standaloneSetup(SomeController).build();


여기서 문제는 익셉션 상황을 테스트하는 코드에서 발생했다. 이 프로젝트에서는 컨트롤러에서 발생한 익셉션의 처리를 표준화하기 위해 @ControllerAdvice를 사용했는데, 위와 같이 컨트롤러 단독 모드로 생성한 MockMvc의 경우 @ControllerAdvice를 이용한 익셉션 처리가 되지 않는 것이었다.


다행히 단독 모드로 생성한 StandaloneMockMvcBuilder는 다음의 메서드를 제공하고 있었다.


setHandlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers)


그래서, @ControllerAdvice를 지정한 클래스를 익셉션 핸들러로 사용하는 ExceptionHandlerExceptionResolver의 구현 객체를 만들어서 사용하기로 했다. 그 구현 객체를 생성하는 코드는 다음과 같다.


public static ExceptionHandlerExceptionResolver createExceptionResolver() {

    ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {

        @Override

        protected ServletInvocableHandlerMethod getExceptionHandlerMethod(

                HandlerMethod handlerMethod, Exception exception) {

            // 익셉션을 CommonExceptionHandler가 처리하도록 설정

            Method method = new ExceptionHandlerMethodResolver(

                    CommonExceptionHandler.class).resolveMethod(exception);

            return new ServletInvocableHandlerMethod(new CommonExceptionHandler(), method);

        }

    };

    exceptionResolver.getMessageConverters().add(new MappingJackson2HttpMessageConverter());

    exceptionResolver.afterPropertiesSet();

    return exceptionResolver;

}


위 코드에서 눈여겨 볼 점은 생성한 exceptionResolver에 알맞은 MessageConverter를 등록했다는 점이다. 새로 등록한 ExceptionResolver는 독립 모드로 생성한 MokcMvc가 사용하는 MessageConverter를 사용하지 않기 때문에, 위 코드처럼 필요한 MessageConverter를 ExceptionResolver에 직접 등록해 주었다.


@ControllerAdvice 클래스를 이용해서 익셉션을 처리하는 ExceptionResolver를 생성하는 기능을 만들었으니 이제 이 기능을 사용해서 MockMvc를 생성하면 된다.


mockMvc = MockMvcBuilders.standaloneSetup(someController)

        .setHandlerExceptionResolvers(createExceptionResolver()).build();


이제 someController에서 익셉션이 발생하면, @ControllerAdvice를 적용한 클래스를 이용해서 익셉션을 처리할 수 있게 된다.

저작자 표시 비영리 변경 금지
신고
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

티스토리 툴바