두 개의 물리적으로 다른 Database에 트랜잭션을 적용하기 위해서 JTA를 사용해야 한다는 것을 듣고 그 중에 Atomikos를 사용하면 된다는 이야기에 구현을 해 보았다.

구글링을 해 보아도 이런말 저런말들이 많고 예전 기준으로 구현된 것도 많고 소스들이 너무나도 달라 뭘 어떻게 따라 해야 할지 막막했는데...

그리고 주로 MyBatis 기준으로 작업이 되어 있어서 답답한 감이 있었는데 JPA 기준으로 작업을 할 수 있도록 4일 정도 삽질 끝에 완성하여 백업 차원에서 기록으로 남긴다.

 

먼저 Legacy Database와 새롭게 사용하는 Database가 있다는 가정하에 아래와 같이 작업을 해야 한다.

먼저 사용한 의존성은 아래와 같다.

 

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation('org.springframework.boot:spring-boot-starter-aop')
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation('org.springframework.data:spring-data-envers')
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }

    // providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')
    implementation('org.hibernate:hibernate-java8:5.1.0.Final')
    implementation('org.springframework.boot:spring-boot-starter-jta-atomikos')

    compileOnly('org.projectlombok:lombok:1.18.4')
    testCompileOnly('org.projectlombok:lombok:1.18.4')

    runtimeOnly 'mysql:mysql-connector-java'
}

 

그리고 사용하게 된 설정 파일은 아래와 같다.

 

application.yml

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: goodchoice-user
    description: "User internal api for goodchoice"
  jackson:
    time-zone: "Asia/Seoul"
  host: host주소값
  # datasource 이하 값들은 Legacy Hikari datasource를 위한 값들
  datasource:
    lw:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://legacy-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: write
      readOnly: false
    lr:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://legacy-read-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: read
      readOnly: true
  # userbenefit 이하 값들은 신규 Hikari datasource를 위한 값들
  userbenefit:
    ubw:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://new-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: write
      readOnly: false
    ubr:
      driverClassName: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://new-read-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
      username: id
      password: password
      maximumPoolSize: 30
      minimumIdle: 5
      poolName: read
      readOnly: true
  # jta 이하 값들은 jta atomikos에서 사용하기 위한 값들
  jta:
    enabled: true
    atomikos:
      datasource:
        lgyw:
          unique-resource-name: xaForLegacyDataSource
          xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource	# class로 사용되는 MysqlXADataSource 클래스는 mysql-connector-java의 버전에 따라 위치가 다르기 때문에 확인 필요
          xa-properties:
            user: id
            password: password
            url: jdbc:mysql://legacy-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
        ubgw:
          unique-resource-name: xaForUserbenefitDataSource
          xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
          xa-properties:
            user: id
            password: password
            url: jdbc:mysql://new-write-server:3306/databasename?autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql: fase
        use_sql_comments: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL55Dialect

 

먼저 Atomikos 사용을 위해서 필요한 UserTransation object와 TransactionManager를 담을 수 있는 클래스를 만들어 보자.

 

AtomikosJtaPlatform.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

public class AtomikosJtaPlatform extends AbstractJtaPlatform {
    private static final long serialVersionUID = 1L;

    static TransactionManager transactionManager;
    static UserTransaction transaction;

    @Override
    protected TransactionManager locateTransactionManager() {
        return transactionManager;
    }

    @Override
    protected UserTransaction locateUserTransaction() {
        return transaction;
    }
}

 

위와 같이 담을 그릇이 준비 되었다면 해당 Object에 값을 담을 수 있는 클래스를 작성하자.

 

XaDataManagerConfig.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

@Slf4j
@Configuration
@ComponentScan
@EnableTransactionManagement
public class XaDataManagerConfig {
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        hibernateJpaVendorAdapter.setGenerateDdl(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);

        return hibernateJpaVendorAdapter;
    }

    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable {
        log.info("========= userTransaction =========");
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);

        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
    public TransactionManager atomikosTransactionManager() throws Throwable {
        log.info("========= atomikosTransactionManager =========");
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);

        AtomikosJtaPlatform.transactionManager = userTransactionManager;

        return userTransactionManager;
    }

    @Bean(name = "multiTxManager")
    @DependsOn({"userTransaction", "atomikosTransactionManager"})
    public PlatformTransactionManager transactionManager() throws Throwable {
        log.info("========= transactionManager =========");
        UserTransaction userTransaction = userTransaction();

        AtomikosJtaPlatform.transaction = userTransaction;

        JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikosTransactionManager());

        return manager;
    }
}

 

위 파일에서 중요한 부분은 AtomikosJtaPlatform.transactionManager와 AtomikosJtaPlatform.transaction에 값을 담는 부분이다.

그리고 multiTxManager bean에서 두 개의 bean (userTransation, atomikosTransactionManager)을 묶어서 JtaTransactionManager로 묶는 것이다.

 

이렇게까지 작업을 한 이후 실제 물리적으로 떨어져 있는 각각의 데이터베이스를 이용한 DataSource를 만들어 보자

 

먼저 Atomikos에서 사용하기 위한 Legacy DataSource 클래스를 작성해 보자.

파일 이름은 XaForLegacyDataSourceConfig 로 작성했다.

사용된 XA 이름은 아래 참고 링크에서 볼 수 있듯이 분산트랜잭션 처리를 위한 표준 스팩 이름이기 때문에 사용하였다.

 

https://d2.naver.com/helloworld/5812258

불러오는 중입니다...

 

XaForLegacyDataSourceConfig.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.Properties;

@Slf4j
@Configuration
@DependsOn("multiTxManager")
@EnableTransactionManagement
@EnableJpaAuditing
@EnableJpaRepositories(
        basePackages = {"kr.co.within.goodchoice.user.jta.domain.legacy"}
        , entityManagerFactoryRef = "xaEntityManagerFactory"
        , transactionManagerRef = "multiTxManager"
)
@EntityScan("kr.co.within.goodchoice.user.jta.domain.legacy")
public class XaForLegacyDataSourceConfig {
    @Value("${spring.jta.atomikos.datasource.lgyw.unique-resource-name}")
    private String uniqueResourceName;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-data-source-class-name}")
    private String dataSourceClassName;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-properties.user}")
    private String user;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-properties.password}")
    private String password;

    @Value("${spring.jta.atomikos.datasource.lgyw.xa-properties.url}")
    private String url;

    @Autowired
    private JpaVendorAdapter jpaVendorAdapter;

    @Bean(name = "xaForLegacyDataSource")
    public DataSource xaForLegacyDataSource() {
        log.info("==================== xaForLegacyDataSource");
        Properties properties = new Properties();
        properties.setProperty("url", url);
        properties.setProperty("user", user);
        properties.setProperty("password", password);

        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName(uniqueResourceName);
        dataSource.setXaDataSourceClassName(dataSourceClassName);
        dataSource.setXaProperties(properties);

        return dataSource;
    }

    @Bean(name = "xaEntityManagerFactory")
    @DependsOn("multiTxManager")
    public LocalContainerEntityManagerFactoryBean xaEntityManagerFactory() {
        log.info("==================== legacyEntityManagerFactory");
        Properties properties = new Properties();
        properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");

        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        entityManager.setDataSource(xaForLegacyDataSource());
        entityManager.setJpaVendorAdapter(jpaVendorAdapter);
        entityManager.setPackagesToScan("kr.co.within.goodchoice.user.jta.domain.legacy");
        entityManager.setPersistenceUnitName("legacy_write_unit");
        entityManager.setJpaProperties(properties);

        return entityManager;
    }
}

 

필요한 접속 정보를 셋팅하고 해당 값으로 Datasource를 만든 다음 entityManagerFactory를 생성 한다.

접속 정보를 @Value로 가지고 와도 되지만 아래처럼 해도 된다. (해보지는 않았지만 될 듯?)

DataSource를 만드는 메소드 위에 아래와 같은 어노테이션을 붙이면 된다.

 

@ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.lgyw.xa")

 

그리고 나서 중요한 것은 위에 XaDataManagerConfig.java에서 만들어 둔 multiTxManager bean에 대한 의존성을 적용해 줘야 한다.

클래스 상단과 entityManagerFactory 상단에 @DependsOn("multiTxManager") 를 넣으면 된다.

 

이 처럼 legacy쪽 Datasource 작업이 끝났다면 신규 Datasource쪽을 작업해 주면 된다. 소스는 비슷하다.

 

XaForUserbenefitDataSourceConfig.java

package kr.co.within.goodchoice.user.jta.infrastructure.config;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.Properties;

@Slf4j
@Primary
@Configuration
@DependsOn("multiTxManager")
@EnableTransactionManagement
@EnableJpaAuditing
@EnableJpaRepositories(
        basePackages = {"kr.co.within.goodchoice.user.jta.domain.userbenefit"}
        , entityManagerFactoryRef = "userbenefitEntityManagerFactory"
        , transactionManagerRef = "multiTxManager"
)
@EntityScan("kr.co.within.goodchoice.user.jta.domain.userbenefit")
public class XaForUserbenefitDataSourceConfig {
    @Value("${spring.jta.atomikos.datasource.ubgw.unique-resource-name}")
    private String uniqueResourceName;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-data-source-class-name}")
    private String dataSourceClassName;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-properties.user}")
    private String user;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-properties.password}")
    private String password;

    @Value("${spring.jta.atomikos.datasource.ubgw.xa-properties.url}")
    private String url;

    @Autowired
    private JpaVendorAdapter jpaVendorAdapter;

    @Primary
    @Bean(name = "xaForUserbenefitDataSource")
    public DataSource xaForUserbenefitDataSource() {
        log.info("==================== xaForUserbenefitDataSource");
        Properties properties = new Properties();
        properties.setProperty("url", url);
        properties.setProperty("user", user);
        properties.setProperty("password", password);

        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName(uniqueResourceName);
        dataSource.setXaDataSourceClassName(dataSourceClassName);
        dataSource.setXaProperties(properties);

        return dataSource;
    }

    @Primary
    @Bean(name = "userbenefitEntityManagerFactory")
    @DependsOn("multiTxManager")
    public LocalContainerEntityManagerFactoryBean userbenefitEntityManagerFactory() {
        log.info("==================== userbenefitEntityManagerFactory");
        Properties properties = new Properties();
        properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");

        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        entityManager.setDataSource(xaForUserbenefitDataSource());
        entityManager.setJpaVendorAdapter(jpaVendorAdapter);
        entityManager.setPackagesToScan("kr.co.within.goodchoice.user.jta.domain.userbenefit");
        entityManager.setPersistenceUnitName("write_unit");
        entityManager.setJpaProperties(properties);

        return entityManager;
    }
}

 

legacy쪽 소스와 다른 점은 클래스와 각 메소드 위에 @Primary 를 사용했다는 점이다.

해당 어노테이션을 붙이지 않을 경우 오류로그에서 기대하는 datasource는 1개인데 그 이상이 들어왔다와 비슷한 오류를 보게 될 것이다.

 

이렇게까지 해 놓고 실제 사용을 해 보자.

필요한 것은 legacy에서 사용할 entity, repository, service 그리고 신규에서 사용할 entity, repository, service 이다.

또한 두 개의 서비스를 묶어 줄 또 하나의 상위 개념 service가 필요하다.

각각의 entity, repository, service는 간단하게 save만 하는 것이 필요하므로 생략하고 두 개를 묶어 줄 service의 소스 코드는 아래와 같다.

 

ApiService.java

package kr.co.within.goodchoice.user.jta.domain;

import kr.co.within.goodchoice.user.jta.domain.legacy.entity.LegacyUser;
import kr.co.within.goodchoice.user.jta.domain.legacy.service.LegacyUserService;
//import kr.co.within.goodchoice.user.jta.domain.userbenefit.entity.UserAddInfo;
//import kr.co.within.goodchoice.user.jta.domain.userbenefit.service.UserAddInfoService;
import kr.co.within.goodchoice.user.jta.domain.userbenefit.entity.UserAddInfo;
import kr.co.within.goodchoice.user.jta.domain.userbenefit.service.UserAddInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Slf4j
@Service
public class ApiService {
    @Autowired
    private LegacyUserService legacyUserService;

    @Autowired
    private UserAddInfoService userAddInfoService;

    @Transactional(propagation = Propagation.REQUIRED, transactionManager = "multiTxManager")
    public void addUser() {
        // legacy DB에서 사용되는 entity객체
        LegacyUser legacyUser = LegacyUser.builder()....build();

        // 신규에서 사용될 entity 객체
        UserAddInfo userAddInfo = UserAddInfo.builder()....build();

        legacyUserService.addUser(legacyUser);
        userAddInfoService.addUserInfo(userAddInfo);
    }
}

 

atomikos를 통해 만든 multiTxManager를 사용한다고 메소드 상단에 명시하고 각가 다른 두 개의 서비스를 호출해 보자.

userAddInfoService.addUserInfo 메소드에서는 다음과 같이 작업을 한다.

 

UserAddInfoService.java

package kr.co.within.goodchoice.user.jta.domain.userbenefit.service;

import kr.co.within.goodchoice.user.jta.domain.userbenefit.entity.UserAddInfo;
import kr.co.within.goodchoice.user.jta.domain.userbenefit.repository.UserAddInfoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserAddInfoService {
    @Autowired
    private UserAddInfoRepository userAddInfoRepository;

    public void addUserInfo(UserAddInfo userAddInfo) {
        userAddInfoRepository.save(userAddInfo);
        throw new RuntimeException("add UserInfo fail...");
    }
}

 

일부러 Exception을 일으켜 보면 legacy와 신규 DB 모두에 값이 안들어 가게 될 것이고 exception 부분을 지우고 테스트 하면 잘 들어가게 될 것이다.

개인적으로 테스트 시 이상 없이 동작함을 볼 수 있었다.

 

참고로 테스트를 unitTest를 사용하지 않은 이유는 unitTest를 사용하게 될 경우 자동으로 rollback이 되기 때문이다.

 

이와는 별도로 Hikari Pool을 이용하여 Datasource와 TransactionManager를 만들어 두고 ApiService.java 파일에서 해당 transactionManager를 명시하게 된다면 원하는대로 동작하지 않고 두 Database 모두에 커밋 되거나 하나만 커밋 되는 경우를 볼 수 있다.

 

Hikari Pool을 이용하여 다수의 Datasource를 사요하는 예제는 아래의 포스트를 참고하기 바란다.

2019/08/13 - [Java/Spring] - 멀티 DataSource 접속 방법 정리

 

이상으로 삽질을 마치며 아래는 구글링 하면서 알게 된 참조 사이트 들이다. 훨씬 더 많지만 기억이 안나기 때문에 아래 사이트만 남긴다.

 

https://d2.naver.com/helloworld/5812258

https://bigzero37.tistory.com/65

 

Atomikos를 이용한 이기종 DB 트랜잭션(Springboot + Mybatis) - 2. Application 환경구성 및 샘플코드

Atomikos를 이용한 이기종 DB 트랜잭션(Springboot + Mybatis) - 2. Application 환경구성 및 샘플코드 Springboot 및 Mybatis 를 위한 Config 설정 application.properties 정의 spring.jta.enabled=true # DATAS..

bigzero37.tistory.com

https://supawer0728.github.io/2018/03/22/spring-multi-transaction/

 

(Spring)다중 DataSource 처리

서론Spring Application을 만들면서 여러 DataSource와 transaction이 존재하고 하나의 transaction 내에 commit과 rollback이 잘 동작하도록 하려면 어떻게 설정해야 할까? 실제로 구현을 해본 적은 없지만 세 가지 방법이 머릿속에 떠올랐다. @Transactional의 propagation을 이용 spring-

supawer0728.github.io

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


아래와 같은 Enum 이 있다.

public enum InterestGoodsCategoryType {
    MOTEL("MOTEL"),
    HOTEL("HOTEL"),
    PENSION("PENSION"),
    GUESTHOUSE("GUESTHOUSE"),
    ACTIVITY("ACTIVITY");

    String typeCode;
    private static List<InterestGoodsCategoryType> allList;

    InterestGoodsCategoryType(String typeCode) {
        this.typeCode = typeCode;
    }

    public String getCode() {
        return this.typeCode;
    }

    @JsonValue
    public InterestGoodsCategoryType jsonValue() {
        return this.typeCode;
    }

    public static InterestGoodsCategoryType fromValue(String value) {
        switch (value) {
            case "MOTEL":
                return InterestGoodsCategoryType.MOTEL;
            case "HOTEL":
                return InterestGoodsCategoryType.HOTEL;
            case "PENSION":
                return InterestGoodsCategoryType.PENSION;
            case "GUESTHOUSE":
                return InterestGoodsCategoryType.GUESTHOUSE;
            case "ACTIVITY":
                return InterestGoodsCategoryType.ACTIVITY;
        }
        return null;
    }

    public static List<InterestGoodsCategoryType> getAll() {
        if (Objects.isNull(allList)) {
            allList = new ArrayList<>(
                    Arrays.asList(MOTEL, HOTEL, PENSION, GUESTHOUSE, ACTIVITY)
            );
        }

        return allList;
    }
}

이럴 경우 Response에서 해당 Enum을 반환하면 typeCode 즉 MOTEL, HOTEL 등등의 코드 값이 리턴된다.

클라이언트의 요구 사항이 각 코드 값에 맞는 한글 이름을 반환해 달라고 해서 구조를 살짝 바꾸었다.

 

public enum InterestGoodsCategoryType {
    MOTEL("MOTEL", "모텔"),
    HOTEL("HOTEL", "호텔"),
    PENSION("PENSION", "펜션"),
    GUESTHOUSE("GUESTHOUSE", "게스트\n하우스"),
    ACTIVITY("ACTIVITY", "엑티\n비티");

    String typeCode;
    String typeName;
    private static List<InterestGoodsCategoryType> allList;

    InterestGoodsCategoryType(String typeCode, String typeName) {
        this.typeCode = typeCode;
        this.typeName = typeName;
    }

    public String getCode() {
        return this.typeCode;
    }

    public String getName() {
        return this.typeName;
    }

    @JsonValue
    public InterestGoodsCategoryTypeDto jsonValue() {
        return InterestGoodsCategoryTypeDto.builder().category(this.typeCode).name(this.typeName).build();
    }

    public static InterestGoodsCategoryType fromValue(String value) {
        switch (value) {
            case "MOTEL":
                return InterestGoodsCategoryType.MOTEL;
            case "HOTEL":
                return InterestGoodsCategoryType.HOTEL;
            case "PENSION":
                return InterestGoodsCategoryType.PENSION;
            case "GUESTHOUSE":
                return InterestGoodsCategoryType.GUESTHOUSE;
            case "ACTIVITY":
                return InterestGoodsCategoryType.ACTIVITY;
        }
        return null;
    }

    public static List<InterestGoodsCategoryType> getAll() {
        if (Objects.isNull(allList)) {
            allList = new ArrayList<>(
                    Arrays.asList(MOTEL, HOTEL, PENSION, GUESTHOUSE, ACTIVITY)
            );
        }

        return allList;
    }
}
@Builder
@Getter
public class InterestGoodsCategoryTypeDto {
    private String category;
    private String name;
}

 

typeCode에 더불어 typeName 이란 것을 추가 하였고, 생성자에서 이름을 받아 들이도록 수정하였다.

그리고 @JsonValue 어노테이션이 붙은 메소드에서는 별도의 DTO 객체를 통해서 구조화 되어 내려가도록 만들었다.

이렇게 만들 경우 json response는 아래와 같은 모습으로 나오게 된다.

{
    "data": [
        {
            "category": "MOTEL",
            "name": "모텔"
        },
        {
            "category": "HOTEL",
            "name": "호텔"
        },
        {
            "category": "PENSION",
            "name": "펜션"
        },
        {
            "category": "GUESTHOUSE",
            "name": "게스트\n하우스"
        },
        {
            "category": "ACTIVITY",
            "name": "엑티\n비티"
        }
    ]
}

 

하지만 RequestBody로 값이 넘어오는 경우 해당 Enum에 값을 맵핑해 주지 못하는 경우가 발생한다.

이럴 경우 어떻게 해야 하는지 고민을 하고 검색도 했는데 들인 시간에 비해 해결 방법은 너무 간단하였다.

Setter를 변형 해야 할 거라고 생각 하였지만 단 1개의 어노테이션을 붙여주면 되었다.

 

public enum InterestGoodsCategoryType {
    MOTEL("MOTEL", "모텔"),
    HOTEL("HOTEL", "호텔"),
    PENSION("PENSION", "펜션"),
    GUESTHOUSE("GUESTHOUSE", "게스트\n하우스"),
    ACTIVITY("ACTIVITY", "엑티\n비티");

    String typeCode;
    String typeName;
    private static List<InterestGoodsCategoryType> allList;

    InterestGoodsCategoryType(String typeCode, String typeName) {
        this.typeCode = typeCode;
        this.typeName = typeName;
    }

    public String getCode() {
        return this.typeCode;
    }

    public String getName() {
        return this.typeName;
    }

    @JsonValue
    public InterestGoodsCategoryTypeDto jsonValue() {
        return InterestGoodsCategoryTypeDto.builder().category(this.typeCode).name(this.typeName).build();
    }

    @JsonCreator
    public static InterestGoodsCategoryType fromValue(String value) {
        switch (value) {
            case "MOTEL":
                return InterestGoodsCategoryType.MOTEL;
            case "HOTEL":
                return InterestGoodsCategoryType.HOTEL;
            case "PENSION":
                return InterestGoodsCategoryType.PENSION;
            case "GUESTHOUSE":
                return InterestGoodsCategoryType.GUESTHOUSE;
            case "ACTIVITY":
                return InterestGoodsCategoryType.ACTIVITY;
        }
        return null;
    }

    public static List<InterestGoodsCategoryType> getAll() {
        if (Objects.isNull(allList)) {
            allList = new ArrayList<>(
                    Arrays.asList(MOTEL, HOTEL, PENSION, GUESTHOUSE, ACTIVITY)
            );
        }

        return allList;
    }
}

그렇다. @JsonCreator 어노테이션을 붙여 주면 된다.

넘어오는 RequestBody의 모습은 아래와 같다.

{
	"birthMonth" : "2019-11",
    "interestGoodsCategoryTypeList" : ["HOTEL", "PENSION"]
}

HOTEL, PENSION 과 같은 코드 값만 넘오 오더라도 해당 값을 @JsonCreator 어노테이션을 달은 메소드에서 맵핑해서 실제 Enum을 리턴해 주도록 만들면 된다.

 

오늘의 삽질은 여기까지...




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


JPA를 사용하면서 QueryDSL을 셋팅하고 사용하는 부분에 있어서 매번 헷깔려 정리한다.

 

QueryDSL을 사용하기 위해서 build.gradle 파일에 아래의 내용을 추가 해 준다.

plugins {
	...
    
    id 'idea'
    id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
    
    ...
}

dependencies {
	...
    
    implementation 'com.querydsl:querydsl-apt:4.1.4'
    implementation 'com.querydsl:querydsl-jpa:4.1.4'
    
    ...
}

ext {
    querydslSrcDir = 'src/main/generated'
    queryDslVersion = '4.1.4'
}

configurations {
    querydsl.extendsFrom compileClasspath
}

querydsl {
    library = "com.querydsl:querydsl-apt"
    querydslSourcesDir = 'src/main/generated'
    jpa = true
    querydslDefault = true
}

sourceSets {
    main {
        java {
            srcDirs += file(querydslSrcDir)
        }
    }
}

idea {
    module {
        generatedSourceDirs += file(querydslSrcDir)
    }
}

 

Entity 작성

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@EntityListeners(value = { AuditingEntityListener.class })
@Table(name = "user")
@Getter
@Setter
public class YeogiUser {
    @Id
    @Column(name="uno")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;
    
    ....
    
    @UpdateTimestamp
    @Column(name = "uedit")
    private LocalDateTime updatedAt;

    @CreationTimestamp
    @Column(name="ureg", updatable = false)
    private LocalDateTime createdAt;
    
    ....
    
    @QueryProjection
    public YeogiUser(long userId, LocalDateTime updatedAt, LocalDateTime createdAt) {
        this.userId = userId;
        this.updatedAt = updatedAt;
        this.createdAt = createdAt;
    }
}

 

기본 JPA Interface 작성

public interface YeogiUserQueryRepository extends JpaRepository<YeogiUser, Long>, YeogiUserQueryRepositoryCustom {
    ...
}

 

Custom으로 QueryDSL을 사용할 interface 작성

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface YeogiUserQueryRepositoryCustom {
    Page<YeogiUser> findByUserParamForIpcc(Pageable pageable, UserParamForIpcc userParamForIpcc);
}

 

Custom interface 구현 클래스 작성

import com.querydsl.core.QueryResults;
import com.querydsl.core.types.ConstructorExpression;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import kr.co.within.cms.user.api.application.params.UserParamForIpcc;
import kr.co.within.cms.user.api.domain.yeogi.user.entity.QYeogiUser;
import kr.co.within.cms.user.api.domain.yeogi.user.entity.YeogiUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

public class YeogiUserQueryRepositoryImpl extends QuerydslRepositorySupport implements YeogiUserQueryRepositoryCustom {
    public YeogiUserQueryRepositoryImpl() {
        super(YeogiUser.class);
    }

    @Override
    public Page<YeogiUser> findByUserParamForIpcc(Pageable pageable, UserParamForIpcc userParamForIpcc) {
        JPAQueryFactory query = new JPAQueryFactory(this.getEntityManager());
        QYeogiUser yegiUser = QYeogiUser.yeogiUser;

        JPAQuery<YeogiUser> jpaQuery = query
                .select(getYeogiUserProjection())
                .from(yegiUser);

        if (CollectionUtils.isEmpty(userParamForIpcc.getUserIdList()) == false) {
            jpaQuery = jpaQuery.where(yegiUser.userId.in(userParamForIpcc.getUserIdList()));
        }

        if (StringUtils.isNotEmpty(userParamForIpcc.getLoginId())) {
            jpaQuery = jpaQuery.where(yegiUser.loginId.eq(userParamForIpcc.getLoginId()));
        }

        if (StringUtils.isNotEmpty(userParamForIpcc.getNickname())) {
            jpaQuery = jpaQuery.where(yegiUser.nickname.eq(userParamForIpcc.getNickname()));
        }

        if (StringUtils.isNotEmpty(userParamForIpcc.getPhone())) {
            jpaQuery = jpaQuery.where(yegiUser.phone.eq(userParamForIpcc.getPhone()));
        }

        QueryResults<YeogiUser> list = jpaQuery
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(new OrderSpecifier(Order.ASC, yegiUser.userId))
                .fetchResults();

        return new PageImpl<>(list.getResults(), pageable, list.getTotal());
    }

    private ConstructorExpression<YeogiUser> getYeogiUserProjection() {
        QYeogiUser yegiUser = QYeogiUser.yeogiUser;

        return Projections.constructor(YeogiUser.class,
                yegiUser.userId, yegiUser.userStatus, yegiUser.userType, yegiUser.ano, yegiUser.loginId, yegiUser.loginPassword,
                yegiUser.nickname, yegiUser.name, yegiUser.facebookId, yegiUser.updatedAt, yegiUser.createdAt,
                yegiUser.deviceId, yegiUser.myRecommendationCode, yegiUser.friendRecommendationCode, yegiUser.recommededCount,
                yegiUser.snsAgree, yegiUser.uaname, yegiUser.group, yegiUser.alarmStatus, yegiUser.latestLodging,
                yegiUser.alarmAgreeDate, yegiUser.phone, yegiUser.lastLoginAt);
    }
}

 

사용

    @Autowired
    private YeogiUserQueryRepository yeogiUserQueryRepository;

    public Page<YeogiUser> getUserList(Pageable pageable, UserParamForIpcc userParamForIpcc) {
        return yeogiUserQueryRepository.findByUserParamForIpcc(pageable, userParamForIpcc);
    }

 

자세한 설명은 생략한다.

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


Java로 Entity를 만들고 멤버 변수로 1 ~ 31일을 만들어 둔 다음 넘오는 날짜에 따라 특정 날짜 변수에 값을 담는 작업을 하다 알게 된 내용이다.

역시 새롭게 알게 된 내용이라 정리 차원으로 올린다.

 

MontTimeTable Entity는 아래와 같다.

@Entity
@EntityListeners(value = {AuditingEntityListener.class})
@Data
@Table(name = "month_timetable")
public class MonthTimetable {
    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private Integer policy_instance_id;

    @Column
    private Integer year;

    @Column
    private Integer month;

    @Column
    private Integer d1;
    @Column
    private Integer d2;
    @Column
    private Integer d3;
    @Column
    private Integer d4;
    @Column
    private Integer d5;
    @Column
    private Integer d6;
    @Column
    private Integer d7;
    @Column
    private Integer d8;
    @Column
    private Integer d9;
    @Column
    private Integer d10;
    @Column
    private Integer d11;
    @Column
    private Integer d12;
    @Column
    private Integer d13;
    @Column
    private Integer d14;
    @Column
    private Integer d15;
    @Column
    private Integer d16;
    @Column
    private Integer d17;
    @Column
    private Integer d18;
    @Column
    private Integer d19;
    @Column
    private Integer d20;
    @Column
    private Integer d21;
    @Column
    private Integer d22;
    @Column
    private Integer d23;
    @Column
    private Integer d24;
    @Column
    private Integer d25;
    @Column
    private Integer d26;
    @Column
    private Integer d27;
    @Column
    private Integer d28;
    @Column
    private Integer d29;
    @Column
    private Integer d30;
    @Column
    private Integer d31;

    @CreatedDate
    @Column(updatable = false, name = "created_at")
    private Date createdAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    @Temporal(TemporalType.TIMESTAMP)
    private Date updatedAt;
}

 

그리고 넘어온 날짜 기반으로 각 필요한 날짜 변수에 값을 셋팅을 해야 하는데 아래와 같은 방법으로 Set 하게 된다.

@Service
@Slf4j
public class MonthTimetableService {
    public void createTimeTable(Long id, List<Date> timetableList) {
        Map<String, MonthTimetable> monthTimetableMap = new HashMap<>();

        for (Date date:timetableList) {
            LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            int year  = localDate.getYear();
            int month = localDate.getMonthValue();
            int day   = localDate.getDayOfMonth();

            MonthTimetable monthTimetable = monthTimetableMap.get(year+""+month);
            if(monthTimetable == null) {
                monthTimetable = new MonthTimetable();
                monthTimetable.setYear(year);
                monthTimetable.setMonth(month);

                monthTimetableMap.put(year+""+month, monthTimetable);
            }

            this.setDay(monthTimetable, day);
        }
    }

    private void setDay(MonthTimetable monthTimetable, int day) {
        try {
            String setterMethodName = "d" + day;
            Field field = MonthTimetable.class.getDeclaredField(setterMethodName);
            field.setAccessible(true);
            field.set(monthTimetable, 1);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new PolicyException(ErrorMessage.FAIL_POLICY_ADD);
        }
    }
}

 

 

이렇게 하게 될 경우 동적으로 필요한 변수를 찾아 값을 Set 할 수 있게 된다.

 

참고 : https://stackoverflow.com/questions/11652598/how-to-instantiate-and-call-methods-dynamically-of-a-class-member-in-java




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


JPA, QueryDsl 쓰기 참 힘들다.

그래도 하나 하나 어렵게 해 나가고 있는데 오늘은 또 Group By한 Count 개수를 반환하는 것을 만든 삽질 내용을 올린다.

 

리파지토리 단 소스는 아래와 같다.

@Override
public QueryResults<Tuple> getBusinessTotalCount() {
    QUser user = QUser.user;

    return from(user).where(user.userType.eq(UserType.B2B)).groupBy(user.platform).select(user.platform, user.platform.count()).fetchResults();
}

 

 

그리고 가지고 온 내용을 가지고 맵으로 이쁘게 정렬해서 반환해 주면 된다.

public Map<String,Integer> getBusinessTotalCount() {
	QueryResults<Tuple> queryResults = userQueryRepository.getBusinessTotalCount();

	Map<String,Integer> returnMap = new HashMap<>();
	List<Tuple> list = queryResults.getResults();
	for (Tuple tuple : list) {
		returnMap.put(tuple.get(0, String.class), tuple.get(1, Integer.class));
	}

	return returnMap;
}

 

 

코드만 놓고 보자면 별거 아닌데, 모르는 상태에서 찾아서 할려니 진도가 더디다. ㅠㅠ

 

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


WebSocket을 이용하여 특정 내용을 처리하는 로직을 개발 중에 삽질한 내용을 정리 차원에서 올린다.

클라이언트는 SockJS와 stompClient를 이용하였고, 서버는 Spring에서 기본적으로 정리한 내용을 크게 바꾸지 않은 상태로 코딩 하였다.

전반적으로 코드의 내용은 https://spring.io/guides/gs/messaging-stomp-websocket/ 에서 나오는 내용과 거의 다르지 않다.

다만 해당 내용을 코딩 중에 sockjs에서 websocket 서버의 "endpoint/info?timestamp" 와 같은 주소를 찾지 못하는 경우가 발생 했다.

이로 인해 서버의 특정 모듈 Dependency가 잘못 되던가, 알지 못하는 문제로 인해 발생 하는 것인줄 알고 프로젝트를 Spring Boot로 싹 다 새로 만들게 되었다.

 

스프링 부트로 만든 상태에서 테스트 해 보니 이상 없이 돌아 갔는데, 해당 내용을 실제 개발 플랫폼에 적용하려고 보니 다시 info 페이지가 404 에러를 내 뿜는게 아닌가 ㅠㅠ

https와 http 문제라고 생각해서 프로토콜도 맞추었지만 현상은 해결 되지 않았다.

 

그런데 크롬의 개발자 도구를 자세히 살펴 보니 info 호출 시 response에 "Invalid CORS request" 라고 response가 뜨는 것을 발견 ㅠㅠ

도메인 문제인 것 같은데...

그렇다면 서버에서 어떻게 설정을 해 주어야 다른 도메인에서도 접근 가능하도록 해 줄 수 있는것일까? 하고 다시 구글링 돌입...

 

결국 https://spring.io/guides/gs/messaging-stomp-websocket/#_configure_spring_for_stomp_messaging 부분에서 보이는 것 처럼 endpoint 설정 시 접근할 수 있는 도메인을 추가로 정해 주면 되는 것이었다.

내가 찾은 stackoverflow의 좌표는 https://stackoverflow.com/questions/30502943/spring-websocket-connecting-with-sockjs-to-a-different-domain

 

이전 코드

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
	// WebSocketMessageBrokerConfigurer.super.registerStompEndpoints(registry);
	registry.addEndpoint("/stork-websocket").withSockJS();
}

 

수정 된 코드

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
	// WebSocketMessageBrokerConfigurer.super.registerStompEndpoints(registry);
	registry.addEndpoint("/stork-websocket").setAllowedOrigins("*").withSockJS();
}

 

 

결국 setAllowedOrigins("*") 하나를 위해서 2주 정도 삽질을 하게 된 ㅠㅠ

물론 이것 뿐 아니라... 현재 웹 소켓에 동시 접속된 유저 수를 구하기 위해 프로젝트를 새롭게 구성할 필요가 있긴 했다.

 

동접자 구하는 소스는 참고로 아래와 같다.

다만... 공식적인 소스가 아니라 그냥 디버깅 해 가면서 만든 소스라서 정확하게 동작 안 할 수 도 있다.

 

Websocket Config 정의하는 Class 내에 아래와 같이 inbound interceptor를 정의 한다.

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
	registration.interceptors(new WebSocketInterceptor());
}

 

 

그리고 나서 인터셉터에서 세션을 Set에 담아 size를 구해와서 사용하면 된다.

package com.auctionblue.bluemessenger.interceptor;

import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptorAdapter;

public class WebSocketInterceptor extends ChannelInterceptorAdapter {
	Set<String> sessionSet = new HashSet<>();

	@Override
	public Message<?> preSend(Message<?> message, MessageChannel channel) {
		String simpMessageType = String.valueOf(message.getHeaders().get("simpMessageType"));
		
		if(StringUtils.equals(simpMessageType, "CONNECT")) {
			String simpSessionId = String.valueOf(message.getHeaders().get("simpSessionId"));
			sessionSet.add(simpSessionId);
		} else if(StringUtils.equals(simpMessageType, "DISCONNECT")) {
			String simpSessionId = String.valueOf(message.getHeaders().get("simpSessionId"));
			sessionSet.remove(simpSessionId);
		}
		
		int uniqueJoinSessionCount = sessionSet.size();
		System.out.println(uniqueJoinSessionCount);
		
		return super.preSend(message, channel);
	}
}

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  4개가 달렸습니다.
  1. 비밀댓글입니다
    • boot랑 spring이랑 차이 없지 않나요?
      maven pom.xml에서 의존성만 설정하고 필요한 라이브러리만 가지고 오면 상관 없을 것으로 보입니다.

      다만 주의 하실 것은 위 내용처럼 도메인 크로스 체크 부분인 것으로 보입니다.

      설정 역시 예제 스프링 소스가 있는 링크에 다 있는 것으로 압니다. ^^
  2. 감사감사 2018.11.28 13:45
    내가 찾던 내용
secret


구매후기 이미지 업로드에서 오류가 나고 있어서 해당 내용을 검토 중에 알게 된 내용을 정리합니다.

기본적으로 아래와 같은 방식으로 업로드가 되고 있습니다.

 

브라우저 -> php -> Java

 

그런데 문제는 어느 한쪽의 문제가 아니라 php, Java 둘다 문제가 발생...

그리고 문제의 원인은 둘다 Version Up이였습니다.

 

1. 먼저 Java

Error Message가 "Required MultipartFile parameter 'file' is not present" 라고 발생.

구글링 하니 http://stackoverflow.com/questions/25830503/spring-mvc-http-status-400-required-multipartfile-parameter-file-is-not-pre 가 검색 됨.

 

아래와 같은 방식으로 수정

 

AS-IS

<bean id="multipartResolver" class="**org.springframework.web.multipart.commons.CommonsMultipartResolver**" />

TO-BE

<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />

 

2. 다음으로 PHP

 

PHP에서 Java 쪽으로 파일을 올릴 때는 CURL을 사용했는데 기존에 /tmp에 올라온 임시 파일을 읽어 들일 때는 @를 사용해서 리소스를 읽어 들였음.

php 5.5.0 기준 Deprecated 된 기능이라서 다른 방법으로 수정

if (!function_exists('curl_file_create')) {
	$data = array(
		'Filedata' => '@'.$file['tmp_name'].';filename='.$file['name'].';type='.$file['type']
	);
	$ch = curl_init();
} else {
	$ch = curl_init($api_url.$uri);
	$cfile = curl_file_create($file['tmp_name'], $file['type'], $file['name']);

	$data = array(
		'Filedata' => $cfile
	);
}

 

 




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


Spring Batch를 그닦 많이 써본 경험이 없지만, 특정 요구 사항이 있어서 개발하게 되었습니다.

작업을 하면서 겪은 오류를 어떻게 해결 했는지 History 및 Backup을 위해 적어 둡니다.


보통 배치는 Reader와 Writer로 구분합니다.

특정 정보를 Reader에서 읽어 들여서 필요한 내용을 가공하고 Writer에서 필요한 행동을 합니다.


그런데 Reader에서 정보를 읽어 들이고 가공하는 시간이 오래 걸리면서 Writer에서 DB Connection을 못 찾게 되고, Writer 작업을 수행하지 못하게 되는 경우가 발생했습니다.


구글링을 한 결과 "autoReconnect 값을 true로 해라", "validationQuery를 날려라" 등등이 나왔지만 유효하지 않은 해결책이였습니다.


결국 찾아낸 해결 방법은 applicationContext.xml에서 database 관련 다음의 옵션을 수정하는 거였습니다.


testOnBorrow : true


이 옵션은 커넥션 풀에서 커넥션을 가져올 경우 유효한지 검사하는 것인데

일반적인 웹서비스에서는 커넥션이 자주 맺고 끊어져서 이 옵션과 상관없이 잘 동작 하는데

배치와 같이 특별한 경우에는 기존 커넥션을 사용하려다가 연결 해제된 커넥션을 검증 안하고 쓰면서 발생한 문제였습니다.

즉 testOnBorrow가 false인 경우에는 유효하지 않은 커넥션을 사용할 수 있으므로, Writer에서 에러가 종종 발생하게 된 것입니다.





WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret


다국어 사용을 위해서 그동안 Struts2 기반 MessageUtil만 사용하다가 Spring 기반으로 변경하기 위해 구글링, 작업한 내용을 정리 차원에서 올립니다.

 

1. applicationContext.xml에 다음 내용을 추가

 

<!-- for MessageUtil -->
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
	<property name="basenames">
		<list>
			<value>/WEB-INF/messages/messages</value>
		</list>
	</property>
	<property name="cacheSeconds" value="100000" />
	<property name="defaultEncoding" value="UTF-8" />
</bean>
<bean id="messageSourceAccessor" class="org.springframework.context.support.MessageSourceAccessor">
	<constructor-arg ref="messageSource"/>
</bean>
<bean id="messageUtil" class="package.path.MessageUtil">
	<property name="messageSourceAccessor" ref="messageSourceAccessor"/>
</bean>

 

2. MessageUtil

import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class MessageUtil {
	private static MessageSourceAccessor messageSourceAccessor = null;
     
    public void setMessageSourceAccessor(MessageSourceAccessor msAcc) {
		this.messageSourceAccessor = msAcc;
	}

	public static Locale getLocale() {

		Locale defaultLocale = Locale.ENGLISH;

		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
		String headerLocale = request.getHeader("Accept-Language");

		if (StringUtils.isNotBlank(headerLocale)) {
			if (headerLocale.indexOf(Locale.JAPAN.getLanguage()) != -1) {
				defaultLocale = Locale.JAPAN;
			} else if (headerLocale.indexOf(Locale.KOREA.getLanguage()) != -1) {
				defaultLocale = Locale.KOREA;
			}
		}

		return defaultLocale;
	}

	public static String getMessage(String key) {
		return messageSourceAccessor.getMessage(key, getLocale());
	}

	public static String getMessage(String key, Object... args) {
		return messageSourceAccessor.getMessage(key, args, getLocale());
	}

	public static String getMessage(Locale locale, String key, Object... args) {
		return messageSourceAccessor.getMessage(key, args, locale);
	}
}

 

 

참고 사이트

http://springsource.tistory.com/113

http://blog.naver.com/pureb612b/10120505318

http://zinlee.tistory.com/204




WRITTEN BY
체리필터
프로그램 그리고 인생...

트랙백  0 , 댓글  0개가 달렸습니다.
secret