eclipes4j's 개발은 언제나 즐겁다.

Multi DatabaseSource에 이어서 더..

Programming!

지난번에 이어서...


막상 기존 운영 서비스의 소스를 보니 데이터베이스 스키마가 하나가 더 있다. 아....


즉,  물리적 Database Server하나에 기존 서비스에서 사용되는 schema는 총 3개였던 것이다. 


"계정용DB, 파트너용DB, 고객운영용DB" 이렇게 3개.


파트너용 백오피스에서 예약 내역 조회는 가장 기본적인 비즈니스인데 파트너용DB와 고객운영용DB가 궂이 분리했고.. 거기서 테이블을 7-8개씩 조인을 한다. 아....



여튼 그래서 3개 모두 datasources를 따로 가져갈까 하다가 계정DB는 따로하는게 맞을 듯 해보이고 두개는 하나의 커넥션 내에서 하는게 맞을 듯 해서 datasource는 하나로 통합하기로 했다.


이때 조건이나 방법을 적어보면 아래와 같다.


- 두개의 schema를 모두 connection 이 가능한 계정이 있어야 한다.

- Datasource설정에 해당 스키마 entity와 repository가 놓일 package를 스캔하도록 한다.

- Entity에 schema또는 catalog를 지정해준다.


다음과 같다.


Auth DataSource : HikariCpSellerDataSource.java

@Configuration

@EnableTransactionManagement

@EnableJpaRepositories(basePackages = {

"xxx.pelican.repository.selleroffice"

"xxx.pelican.entity.selleroffice",

"xxx.pelican.repository.livedb",

"xxx.pelican.entity.livedb"})

public class HikariCpSellerDataSource {


    @Autowired

    private HikariConfig pelicanhikariConfig;

    

    

    @Bean

    @Primary

    @ConfigurationProperties(prefix = "spring.datasource")

    public DataSource sellerofficeDataSource() throws SQLException {

        HikariDataSource hikariDataSource = new HikariDataSource();

        return hikariDataSource;

    }

    

    @Primary

    @Bean(name = "entityManagerFactory")

    public LocalContainerEntityManagerFactoryBean sellerofficeEntityManagerFactory(EntityManagerFactoryBuilder builder@Qualifier("sellerofficeDataSource") DataSource dataSource) {

        return builder.dataSource(dataSource).packages(

"xxx.pelican.repository.selleroffice"

"xxx.pelican.entity.selleroffice"

"xxx.pelican.repository.livedb"

"xxx.pelican.entity.livedb"

).build();

    }


    @Primary

    @Bean(name = "transactionManager")

    public PlatformTransactionManager sellerofficeTransactionManager(@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {

        return new JpaTransactionManager(entityManagerFactory);

    }

}



Entity : Reservation.java - 고객운영용DB

@Data

@AllArgsConstructor

@NoArgsConstructor

@Builder

@Entity

@Table(catalog = "livedb", name = "reservation")

public class Reservation implements Serializable {

    @Id

    @GeneratedValue(strategy = GenerationType.AUTO)

    @Column(name = "idx", nullable = false, unique = true)

    private Long idx;


Entity : PartnerReservation.java - 파트너용DB

@Data

@AllArgsConstructor

@NoArgsConstructor

@Builder

@Entity

@Table(catalog = "selleroffce", name = "partner_reservation")

public class PartnerReservation implements Serializable {

    @Id

    @GeneratedValue(strategy = GenerationType.AUTO)

    @Column(name = "idx", nullable = false, unique = true)

    private Long idx;


Spring Boot 에 HikariCp / JPA / Multiple DataSources 적용해보기

Programming!

기존 운영중인 서비스를 java 로 변경하면서 플젝을 세팅하다보니 서비스 운영 데이터 DB와 계정 정보 DB가 분리되어 있는 걸 확인했다. 아...


Spring Security를 적용해 로그인 처리를 하기 위해서는 계정 DB(backoffice)에 사용해야 하고 그외에는 서비스 운영 데이터 DB(selleroffice)를 바라보도록 하는 것으로 정하고 세팅을 진행함.

( 물론, 계정 API를 따로 뺄까 생각하다.. 내 지금 상황에 맞는 유산을 누군가에게 물려준다라는 심보로..ㅋㅋㅋ 농담. - !!!! )



여튼 두개의 Datasource를 만들자.


File : application.properties

backoffice.datasource.jdbc-url=jdbc:mysql://localhost:3306/backoffice?useSSL=false

backoffice.datasource.username=master

backoffice.datasource.password=my.local

backoffice.datasource.driver-class-name=com.mysql.jdbc.Driver

backoffice.datasource.type=com.zaxxer.hikari.HikariDataSource


spring.datasource.jdbc-url=jdbc:mysql://localhost:3306/selleroffice?useSSL=false

spring.datasource.username=master

spring.datasource.password=my.local

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.type=com.zaxxer.hikari.HikariDataSource


spring.jpa.show-sql = true

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

....



Backoffice DataSource : HikariCpBackofficeDataSource.java

@Configuration

@EnableTransactionManagement

@EnableJpaRepositories(basePackages = {"xxx.pelican.repository.backoffice", "xxx.pelican.entity.backoffice"},

        transactionManagerRef = "backofficeTransactionManager", entityManagerFactoryRef = "backofficeEntityManagerFactory")

public class HikariCpBackofficeDataSource {


    @Bean(name = "backofficeDataSource")

    @ConfigurationProperties(prefix = "backoffice.datasource")

    public DataSource backofficeDataSource() throws SQLException {

        HikariDataSource hikariDataSource = new HikariDataSource();

        return hikariDataSource;

    }


    @Bean(name = "backofficeEntityManagerFactory")

    public LocalContainerEntityManagerFactoryBean backofficeEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("backofficeDataSource") DataSource dataSource) {

        return builder.dataSource(dataSource).packages("xxx.pelican.repository.backoffice", "xxx.pelican.entity.backoffice").build();

    }


    @Bean(name = "backofficeTransactionManager")

    public PlatformTransactionManager backofficeTransactionManager(@Qualifier("backofficeEntityManagerFactory") EntityManagerFactory entityManagerFactory) {

        return new JpaTransactionManager(entityManagerFactory);

    }


}


Auth DataSource : HikariCpSellerDataSource.java

@Configuration

@EnableTransactionManagement

@EnableJpaRepositories(basePackages = {"xxx.pelican.repository.selleroffice", "xxx.pelican.entity.selleroffice"})

public class HikariCpSellerDataSource {


    @Autowired

    private HikariConfig pelicanhikariConfig;

    

    

    @Bean

    @Primary

    @ConfigurationProperties(prefix = "spring.datasource")

    public DataSource sellerofficeDataSource() throws SQLException {

        HikariDataSource hikariDataSource = new HikariDataSource();

        return hikariDataSource;

    }

    

    @Primary

    @Bean(name = "entityManagerFactory")

    public LocalContainerEntityManagerFactoryBean sellerofficeEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("sellerofficeDataSource") DataSource dataSource) {

        return builder.dataSource(dataSource).packages("xxx.pelican.repository.selleroffice", "xxx.pelican.entity.selleroffice").build();

    }


    @Primary

    @Bean(name = "transactionManager")

    public PlatformTransactionManager sellerofficeTransactionManager(@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {

        return new JpaTransactionManager(entityManagerFactory);

    }

}


코드를 보면 알겠지만 기본적으로 분기처리는 package에 의해 처리된다. 

기본 서비스용 DB로 사용되는 DataSource에는 @Primary를 지정 하고, properties 의 prefix도  "spring.datasource"로 지정, 계정 관리로 사용되는 DataSource의 properties prefix는 "backoffice.datasource"로 지정했다.



이후 Repository나 Entity를 지정된 package에 맞춰 생성하면 해당 datasource를 사용하게 된다.

여기까지가 다중 DataSource를 사용하는 부분이고... 


막상 hikaricp를 쓰는데 pool에 대한 각 properties를 지정할 수가 없었다. 예로 "spring.datasource.hikari.idle-timeout" 와 같은..


DataSourceBuilder 사용으로도 처리가 안되고... 다른 문제가 있는건지  좀 더 파고 들어야 겠지만 우선 커넥션 관련 DataSource는 문제 없으니, 공통적인 부분은 HikariConfig로 따로 관리 하기로 했다.


우선 HikariConfig관련 Properties를 따로 빼고

@Configuration

public class HikariCpProperties {


    @Bean

    public HikariConfig pelicanhikariConfig() throws Exception {

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

        Resource resource = resolver.getResource("classpath:hikaricp.properties");

        Properties loadProperties = PropertiesLoaderUtils.loadProperties(resource);

        HikariConfig hikariConfig = new HikariConfig(loadProperties);

        return hikariConfig;

    }

}


hikaricp.properties에 각종 공통으로 사용되는 설정 옵션을 지정한다.

poolName=HikariConnectionPool

minimumIdle=20

maximumPoolSize=30

idleTimeout=250000

maxLifetime=290000

validationTimeout=10000



요로코럼 해서 DataSource생성시 설정을 각각 set 처리해주면 된다. HikariCpDataSource생성시 config를 parameter로 넘기면 jdbc-url이 null처리 되므로 속성을 개별적으로 지정해주자.


끄윽.






Json 속성중 Enum에 대한 Null 처리

Programming!

일반적인 null이나 알 수 없는 속성의 경우는


@JsonIgnoreProperties(ignoreUnknown = true)

@JsonInclude(JsonInclude.Include.NON_NULL)

public class RateInfo {

...


위 Annotation 정도면 무난하게 파싱이 가능하지만, Enum이 추가되고 해당 Enum이 Null 또는 Enum에 매칭되는 속성이 아닌 경우는 오류가 발생하게 된다.

가령 Json과 Object가 아래와 같은 경우 "Unknown"이 없으므로 오류(deserialization error)이다.


Json

},

"nonRefundable": false,

"rateType": "Unknown"

}

},


Object

@JsonIgnoreProperties(ignoreUnknown = true)

@JsonInclude(JsonInclude.Include.NON_NULL)

public class RateInfo {

...

    @JsonProperty(value="rateType")

    private MerchantRateType rateType;


MerchantRateType

public enum MerchantRateType {

    MerchantPackage("MerchantPackage"), MerchantStandard("MerchantStandard");

}


이경우는 Enum에 Json에 대한 처리를 직접 해주는게 좋다. ( @JsonCreator ) 

public enum MerchantRateType {

....

    @JsonCreator

    public static MerchantRateType forValue(String value) {

        return Arrays.stream(MerchantRateType.values()).filter(o -> o.getRateTypeName().equals(value)).findFirst().orElse(null);

    }


    @JsonValue

    public String toValue() {

        return rateTypeName;

    }

....


Spring RestTemplate 에서 로그 처리

Programming!

우선 RestTemplate의 setInterceptors 를 보자.

 
/**
 * Sets the request interceptors that this accessor should use.
 */
public void setInterceptors(List<clienthttprequestinterceptor> interceptors) {
	this.interceptors = interceptors;
}

음.. ClientHttpRequestInterceptor 를 List 처리해서 넣어주면 Interceptor의 기능을 할 수 있구나!!!


그래서 만들어 보자. (Bean으로 만들어도 된다.)

@Slf4j

public class RestTemplateLoggingInterceptor implements ClientHttpRequestInterceptor {


    private Marker marker;


    public RestTemplateLoggingInterceptor(Marker marker) {

        this.marker = marker;

    }


    @Override

    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

        loggingRequest(request, body);

        ClientHttpResponse response = execution.execute(request, body);

        loggingResponse(request, response);

        return response;

    }


    private void loggingRequest(HttpRequest request, byte[] body) throws IOException {

        log.debug(marker, "URI         : {}", request.getURI());

        log.debug(marker, "Method      : {}", request.getMethod());

        log.debug(marker, "Headers     : {}", request.getHeaders());

        log.debug(marker, "Request body: {}", IOUtils.toString(body, StandardCharsets.UTF_8.name()));

    }


    private void loggingResponse(HttpRequest request, ClientHttpResponse response) throws IOException {

        log.debug(marker, "URI / Method : {} / {}", request.getURI(), request.getMethod());

        log.debug(marker, "Status code  : {}", response.getStatusCode());

        log.debug(marker, "Status text  : {}", response.getStatusText());

        log.debug(marker, "Headers      : {}", response.getHeaders());

        log.debug(marker, "Response body: {}", IOUtils.toString(response.getBody(), StandardCharsets.UTF_8.name()));

    }

}


ClientHttpRequestInterceptor를 구현해주면 되고 execute해서 떨어지는 response를 반환해주면 되고..

근데, 이렇게만 해서 setInterceptors에 넣어주면 response 로그만 찍히고 resource가 해제된체로 반환된다.(response body 가 null)


해서 restTemplate 의 requestFactory 지정을 Response Body를 카피해서 돌려주는 BufferingClientHttpRequestFactory로 넣어주어야 한다.


BufferingClientHttpRequestFactory를 타고 들어가면 아래의 소스를 볼 수 있다.

 
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
	this.request.getHeaders().putAll(headers);
	StreamUtils.copy(bufferedOutput, this.request.getBody());
	ClientHttpResponse response = this.request.execute();
	return new BufferingClientHttpResponseWrapper(response);
}


여튼 해당 factory를 설정에 넣어주고, interceptor도 넣어주자.


@EnableRetry

@Configuration

public class RestTemplateConfiguration {

@Bean(name="apiRestTemplate")

public RestTemplate apiRestTemplate() {

RestTemplate restTemplate = new RestTemplate();

restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));

restTemplate.setInterceptors(Lists.newArrayList(new RestTemplateLoggingInterceptor(LogMarker.XXX)));

....


만약 별도의 requestFactory 를 사용하고 있었다면 (ex.. HttpComponentsClientHttpRequestFactory) SimpleClientHttpRequestFactory 대신 사용중인 factory를 지정하면 된다.