버전 비교

  • 이 줄이 추가되었습니다.
  • 이 줄이 삭제되었습니다.
  • 서식이 변경되었습니다.
정보

개발 가이드
X2BEE 개발 환경을 제공하기 위한 자료입니다.

목차
minLevel1
maxLevel1
stylesquare

1. 사용 라이브러리 정보

명칭

버전

용도

JRE System Library

1.8

웹 어플리케이션 구동에 필요한 자바 런타임 라이브러리

Spring Boot Framework

5.3.4

서버 로직 전반을 담당하는 프레임워크

Gradle

6.6

모듈의 의존성 관리를 위한 라이브러리

Mybatis-spring-boot-starter

2.1.3

데이터베이스 연결 및 쿼리 처리를 위한 라이브러리

Logback

로깅을 위한 라이브러리

2. 프로젝트 패키지 구조

 1) src/main/java

  • 프레임워크 공통 클래스 패키지: 예외처리, DB관련 속성, 보안, 유틸 클래스 포함

  • API 관련 비즈니스 로직 클래스 패키지: Controller, Service, Dao, Entity 클래스

  • 프레임워크 관련 로직 클래스 패키지: RootController, User, Role 관련 설정 클래스

  • 설정 관련 로직 클래스 패키지: 프로젝트 관련 설정 클래스

 2) src/main/resources

  • 프레임워크 설정 패캐지: dev/local 별 datasource 설정, 로깅 설정, 기타 설정 파일

  • API 관련 쿼리 패키지: 프로젝트 내에서 사용될 쿼리 파일

  • View 템플릿 패키지: Front-end html 파일과 error html 파일

  • 기타 설정 패키지: 프로젝트 전체 appliation.yml 파일 및 설정 파일

3. 데이터베이스 연결

 1) application.yml 파일 내 datasource 관련 설정 추가

  • 아래와 같이 application.yml 파일에서 데이터베이스 관련 설정 파일 위치 명시

코드 블럭
languageyaml
....
x2config:
  location:
    datasource: config/dev/properties/datasource.properties
    site: config/dev/properties/site.properties
....

 2) datasource.properties 파일 작성

  • src/main/resource/{}/properties 폴더 하위에 datasource.properties 파일 작성

  • prefix 값 설정 필요 (아래 예제에서는 x2base)

  • 파일에 추가될 데이터베이스 관련 설정 정보

    프로퍼티명

    설명

    url데이터베이스 접속 URLusername데이터베이스 사용자 아이디password데이터베이스 사용자 비밀번호driveClassName데이터베이스 드라이버 클래스 명

코드 블럭
languagehtml
x2base.url=jdbc:log4jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO
x2base.username=X2BASE
x2base.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR)
x2base.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy

 3) DefaultDatasourceProperties.java 파일 작성

  • 앞서 작성한 datasource.properties 파일 내 프로퍼티 값을 Java 파일에서 처리 가능하도록 다음과 같이 작업

  • @ConfigurationProperties 어노테이션을 이용하여 앞서 설정한 prefix 값으로 세팅하여 위의 설정 파일과 매핑

코드 블럭
languagejava
package com.plateer.base.config.properties;
 
import javax.validation.constraints.NotNull;
 
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.validation.annotation.Validated;
 
import lombok.Getter;
import lombok.ToString;
 
/* location : /config/${spring.active.profile}/datasource.properties */
 
@ConfigurationProperties(prefix = "x2base")
@ConstructorBinding
@Validated
@Getter
@ToString
public class DefaultDatasourceProperties {
 
    @NotNull
    private final String username;
    @NotNull
    private final String password;
    @NotNull
    private final String url;
    @NotNull
    private final String driverClassName;
     
    public DefaultDatasourceProperties(String username, String password, String url, String driverClassName) {
        this.username = username;
        this.password = password;
        this.url = url;
        this.driverClassName = driverClassName;
    }
}

 4) 데이터베이스 관련 빈 설정 및 등록

  • 앞서 설정한 파일 내 정보를 이용하여 SqlSessionFactory 및 SqlSessionTemplate 설정

  • @MapperScan 어노테이션으로 매퍼 파일 위치 및 SqlSessinoFactory 클래스 지정

  • 트랜잭션 처리를 위하여 @EnableTransactionManagement 어노테이션 추가

코드 블럭
languagejava
package com.plateer.base.config.mybatis;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.plateer.base.config.properties.DefaultDatasourceProperties;
import com.zaxxer.hikari.HikariDataSource;

@Configuration
@MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "sqlSessionFactory")
@EnableTransactionManagement
public class DefaultDatasourceConfig {
	
	private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml";
	private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml";
	
	@Autowired
	private ApplicationContext applicationContext;
	
	@Primary
	@Bean(name = "defaultDatasource")
	public DataSource defaultDatasource(DefaultDatasourceProperties datasourceProperties) {
		return DataSourceBuilder.create()
				.type(HikariDataSource.class)
				.driverClassName(datasourceProperties.getDriverClassName())
				.url(datasourceProperties.getUrl())
				.username(datasourceProperties.getUsername())
				.password(datasourceProperties.getPassword())
				.build();
	}
	
	@Primary
	@Bean(name = "sqlSessionFactory")
	public SqlSessionFactory sqlSessionFactory(@Qualifier("defaultDatasource") DataSource defaultDatasource, ApplicationContext context) throws Exception {
		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(defaultDatasource);
		sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG));
		sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES));
		return sqlSessionFactoryBean.getObject();
	}
	
	@Primary
	@Bean(name = "sqlSessionTemplate")
	public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
	
	@Primary
    @Bean(name="transactionManager")
    public DataSourceTransactionManager transactionManager(@Autowired @Qualifier("defaultDatasource") DataSource defaultDatasource) {
        return new DataSourceTransactionManager(defaultDatasource);
    }
}

4. 다중 Datasource 연결

 1) 데이터베이스 관련 정보 설정

  • datasource.properties 파일 내 DB 접속 정보 각각 설정

코드 블럭
languageautoit
spring.primary.datasource.jdbc-url=jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO
spring.primary.datasource.username=X2BASE
spring.primary.datasource.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR)
 
spring.secondary.datasource.jdbc-url=jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO
spring.secondary.datasource.username=X2BASE
spring.secondary.datasource.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR)

 2) Datasource 설정

  • 다중 Datasource 설정 시, 각각 Datasource 클래스 생성

  • @ConfigurationProperties 어노테이션으로 datasource.properties 내 DB 접속 정보 로드

펼치기
코드 블럭
languagejava
title = primaryDatasourece.java
package com.plateer.base.config.mybatis;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
@Configuration
@MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "sqlSessionFactory") // Mapper 파일 스캔을 위한 패키지 기입
@EnableTransactionManagement
public class PrimaryDatasource {

    private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml"; // Mapper 파일 스캔을 위한 경로 정보 기입
    private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml"; // mybatis 관련 설정 파일 경로 정보 기입
 
    @Primary
    @Bean(name="dataSource")
    @ConfigurationProperties(prefix="spring.primary.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Primary
    @Bean(name="sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean(@Autowired @Qualifier("dataSource") DataSource dataSource, ApplicationContext applicationContext)
            throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG));
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES));
        return sqlSessionFactoryBean.getObject();
    }
 
    @Primary
    @Bean(name="sqlSession")
    public SqlSessionTemplate sqlSession(@Autowired @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
 
    @Primary
    @Bean(name="transactionManager")
    public DataSourceTransactionManager transactionManager(@Autowired @Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
 
}
코드 블럭
languagejava
package com.plateer.base.config.mybatis;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
@Configuration
@MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "sqlSessionFactory") // Mapper 파일 스캔을 위한 패키지 기입
@EnableTransactionManagement
public class PrimaryDatasource {

    private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml"; // Mapper 파일 스캔을 위한 경로 정보 기입
    private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml"; // mybatis 관련 설정 파일 경로 정보 기입
 
    @Primary
    @Bean(name="dataSource")
    @ConfigurationProperties(prefix="spring.primary.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Primary
    @Bean(name="sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean(@Autowired @Qualifier("dataSource") DataSource dataSource, ApplicationContext applicationContext)
            throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG));
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES));
        return sqlSessionFactoryBean.getObject();
    }
 
    @Primary
    @Bean(name="sqlSession")
    public SqlSessionTemplate sqlSession(@Autowired @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
 
    @Primary
    @Bean(name="transactionManager")
    public DataSourceTransactionManager transactionManager(@Autowired @Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
 
}


코드 블럭
languagejava
package com.plateer.base.config.mybatis;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
@Configuration
@MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "secondarySqlSessionFactory")
@EnableTransactionManagement
public class SecondaryDatasource {

    private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml";
    private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml";
	
    @Bean(name="secondaryDataSource")
    @ConfigurationProperties(prefix="spring.secondary.datasource")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Bean(name="secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactoryBean(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource, ApplicationContext applicationContext)
            throws Exception {
    	SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(secondaryDataSource);
        sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG));
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES));
        return sqlSessionFactoryBean.getObject();
    }
    
    @Bean(name="secondarySqlSession")
    public SqlSession secondarySqlSession(@Autowired @Qualifier("secondarySqlSessionFactory") SqlSessionFactory secondarySqlSessionFactory) {
        return new SqlSessionTemplate(secondarySqlSessionFactory);
    }
     
    @Bean(name="secondaryTransactionManager")
    public DataSourceTransactionManager secondaryTransactionManager(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        return new DataSourceTransactionManager(secondaryDataSource);
    }
 
}

코드 블럭
package com.plateer.base.config.mybatis;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
@Configuration
@MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "secondarySqlSessionFactory")
@EnableTransactionManagement
public class SecondaryDatasource {

    private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml";
    private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml";
	
    @Bean(name="secondaryDataSource")
    @ConfigurationProperties(prefix="spring.secondary.datasource")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Bean(name="secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactoryBean(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource, ApplicationContext applicationContext)
            throws Exception {
    	SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(secondaryDataSource);
        sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG));
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES));
        return sqlSessionFactoryBean.getObject();
    }
    
    @Bean(name="secondarySqlSession")
    public SqlSession secondarySqlSession(@Autowired @Qualifier("secondarySqlSessionFactory") SqlSessionFactory secondarySqlSessionFactory) {
        return new SqlSessionTemplate(secondarySqlSessionFactory);
    }
     
    @Bean(name="secondaryTransactionManager")
    public DataSourceTransactionManager secondaryTransactionManager(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        return new DataSourceTransactionManager(secondaryDataSource);
    }
 
}

 3) 사용 예

코드 블럭
....
    public class SampleServiceImpl{
 
        @Autowired
        @Qualifier("secondarySqlSession") // 사용하고자 하는 datasource 지정 (Primary 는 지정하지 않아도 됨)
        private SqlSession secondarySqlSession;
    
        ....
    }
....

5. 로깅 설정

 1) 로깅 관련 프로퍼티 설정

  • 로깅 파일 저장 위치 설정

  • 로깅 레벨 설정

  • 라이트 로깅 여부 설정 (최소한의 정보만 로깅)

  • 로그에 컨트롤러 파라미터를 포함할지 여부 설정

  • 로그에 서비스 파라미터를 포함할지 여부 설정

코드 블럭
logging.destination=C://log
logging.level=INFO

logging.lightLogging=false
logging.includeControllerParameter=true
logging.includeServiceParameter=false

 2) logback 관련 설정

  • src/main/resources/{} 폴더 하위 loback.xml 파일 설정

  • consoleAppender: 시스템 콘솔에 찍히는 로그 정보

  • fileAppender: 파일에 쓰여질 로그 정보

  • asyncConsoleAppender: 로깅 작업이 성능에 많은 영향을 주는 경우, 비동기로 설정하여 로깅 부하를 최소화 (콘솔 로깅 비동기 처리 설정)

  • asyncFileAppender: 로깅 작업이 성능에 많은 영향을 주는 경우, 비동기로 설정하여 로깅 부하를 최소화 (파일 로깅 비동기 처리 설정)

코드 블럭
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="30 seconds">
	
	<springProperty scope="context" name="activeProfile" source="spring.profiles.active"/>

	<property resource="config/dev/properties/logging.properties"/>

	<property name="loggingPattern" 
		value="%d{HH:mm:ss.SSS} %-5level [%thread] %replace([RID:%X{rid}]){'(\\[RID:\\])', ''} %replace([SID:%X{sid}]){'(\\[SID:\\])', ''} %replace([USER:%X{user}]){'(\\[USER:\\])', ''} %logger{36} - %msg[END]%n" />
	
	<!-- console log appender -->
	<appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<Pattern>${loggingPattern}</Pattern>
		</layout>
	</appender>
	
	<!-- log file appender -->
	<appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<file>${logging.destination}/debug.log</file>
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
		    <!-- Daily Rollover -->
		    <fileNamePattern>${logging.destination}/debug.%d{yyyy-MM-dd}_[%i].log</fileNamePattern>
		    <!-- File Size 300Mb -->
		    <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">	      		
	      		<maxFileSize>300MB</maxFileSize>
	    	</timeBasedFileNamingAndTriggeringPolicy>
	    	<!-- Keep 30 days -->
			<maxHistory>30</maxHistory>
	  	</rollingPolicy>
	  	<encoder>
			<pattern>${loggingPattern}</pattern>
		</encoder>
	</appender>
	
	 <!-- 비동기로 console에 추가 로깅 -->
	<appender name="asyncConsoleAppender" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>500</queueSize>
        <appender-ref ref="consoleAppender" />
    </appender>
    
    <!-- 비동기로 file에 추가 로깅 -->
    <appender name="asyncFileAppender" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>500</queueSize>
        <appender-ref ref="fileAppender" />
    </appender>
    
	<logger name="jdbc" level="OFF"/>
	<logger name="jdbc.sqlonly" level="OFF"/>
	<logger name="jdbc.sqltiming" level="DEBUG"/>
	<logger name="jdbc.audit" level="OFF"/>
	<logger name="jdbc.resultset" level="OFF"/>
	<logger name="jdbc.resultsettable" level="OFF"/>
	<logger name="jdbc.connection" level="OFF"/>
	
	<root level="${logging.level}">
		<appender-ref ref="consoleAppender" />
		<appender-ref ref="fileAppender" />
		
		<!-- 비동기 logging -->
		<!-- <appender-ref ref="asyncConsoleAppender" />
        <appender-ref ref="asyncFileAppender" /> -->
	</root>
	
	
</configuration>

 3) 로깅 추가 정보 설정

  • 로깅에 추가로 보여주고자 하는 정보 설정 (src/main/java/com/plateer/base/log 폴더 하위 ExecLoggingFilter.java 파일 수정)

  • 아래 가이드는 RequestID 와 SessionID 를 설정하는 예제

코드 블럭
package com.plateer.base.filter; import java.io.IOException; import java.util.UUID; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.slf4j.MDC; import org.slf4j.MDC.MDCCloseable; import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; @Slf4j @Component public class ExecLoggingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { try { HttpServletRequest request = (HttpServletRequest) req; // request id setting MDC.put("rid", getRequestId(request)); // session id setting MDC.put("sid", getSessionId(request)); chain.doFilter(req, res); } finally { // mdc remove MDC.clear(); } } private String getRequestId(HttpServletRequest request) { return request.getRequestURI() + "-" + UUID.randomUUID().toString(); } private String getSessionId(HttpServletRequest request) { String sessionId = request.getRequestedSessionId(); return sessionId != null ? request.getRequestedSessionId() : "?????"; } }
  • Request 시 ReqeustID, SessionID 를 생성하고 제거하며, MDC 를 활용하여 로그 출력

6. 쿼리 로깅 설정

  • log4jdbc 를 이용한 쿼리 로깅 방법

 1)  log4jdbc 설정 파일 추가

  • resource 폴더 하위 아래 파일 추가

코드 블럭
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

 2) 데이터베이스 연결 정보 수정

  • driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy 추가

  • url 수정 (log4jdbc 추가)

코드 블럭
x2base.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
x2base.url=jdbc:log4jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO
x2base.username=X2BASE
x2base.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR)

 3) logback.xml 파일 수정

  • 아래 설정 내용 추가

코드 블럭
	<!-- log4jdbc 옵션 설정 -->
	<logger name="jdbc" level="OFF"/>
	<!-- 커넥션 open close 이벤트 로그로 남김 -->
	<logger name="jdbc.connection" level="OFF"/>
	<!-- SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여짐 -->
	<logger name="jdbc.sqlonly" level="OFF"/>
	<!-- SQL문과 해당 SQL을 실행시키는데 수행된 시간 정보(milliseconds)를 포함 -->
	<logger name="jdbc.sqltiming" level="DEBUG"/>
	<!-- ResultSet을 제외한 모든 JDBC 호출 정보를 로그로 남김. 방대한 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않음-->
	<logger name="jdbc.audit" level="OFF"/>
	<!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 방대한 양의 로그가 생성됨 -->
	<logger name="jdbc.resultset" level="OFF"/>
	<!-- SQL 결과 조회된 데이터의 table을 로그로 남김 -->
	<logger name="jdbc.resultsettable" level="DEBUG"/>

7. 클라이언트, 서버 간 통신 데이터 형식

  • Map 형태가 아닌 VO(Value Object) 로 데이터 통신하는 것을 기본으로 함

  • 아래 사용자 정보 처리 관련 VO 예제 참고

코드 블럭
package com.plateer.x2co.common.entity;

import java.util.Date;

import lombok.Data;

@Data
public class User {

	private String usrId;
	private String usrNm;
	private String usrGrp;
	private String useYn;
	private String mobileNo;
	private String phoneNo;
	private String email;
	private String pw;
	private Date pwModDt;
	private String mailReceivedYn;
	private String smsReceivedYn;
	private String pwMissCnt;
	private String pwInitYn;
	private Date lastLoginDt;
	private String lastLoginIp;
	private String accExpireYn;
	private String accLockYn;
	private String pwModRequireYn;
}

8. 서버 측 개발 방식

 Controller, Service, Mapper 구조

  • 서버는 Spring MVC 패턴 기반이므로 데이터 처리를 위하여 다음과 같이 Controller, Service, Mapper 구조로 진행됨

  • http request → (mapping) → Controller → Service → ServiceImpl → Mapper → query 호출 순서로 진행됨

 1) VO 클래스 작성

  • src/main/java 하위 해당 업무 폴더에 entity 폴더 생성 후 아래와 같이 작업 진행

  • 데이터 전달을 위하여 사용될 객체 사전 정의

코드 블럭
package com.plateer.x2co.backoffice.sample.entity;

import lombok.Data;

@Data
public class SampleVO{
 
	String itemNo;
	
	String salePrc;
	
	String mbrNo;

	....
	
}

 2) Controller 클래스 작성

  • src/main/java 하위 해당 업무 폴더에 Controller 폴더 생성 후 아래와 같이 작업 진행

  • @RestController 어노테이션으로 Controller 임을 명시

  • @RequestMapping 어노테이션으로 해당 Controller 전체에 대한 Http Request 매핑 정보 명시

  • @Autowired 어노테이션으로 service 인터페이스 주입

코드 블럭
package com.plateer.x2co.backoffice.sample.controller;

@RestController
@RequestMapping("/rest")
public class SampleController {
	
	@Autowired
	private SampleService sampleService ;
	
	@PostMapping(value="/categories", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public ReponseEntity<List<SampleVO>> getPrDispCatList(@RequestBody String id) { 

		List<SampleVO> listMap = prototypeServcie.getPrDispCatList(id);
		return sampleService.getPrDispCatList(id);
	}
}

 3) Service 클래스 작성

  • src/main/java 하위 해당 업무 폴더에 Service 폴더 생성 후 아래와 같이 작업 진행

  • 구현하고자 하는 업무 로직의 인터페이스 작성

코드 블럭
package com.plateer.x2co.backoffice.sample.service;

public interface SampleService {
	
	public ReponseEntity<List<SampleVO>> getPrDispCatList(String id);

}

  • 위 인터페이스의 실제 구현체 작성

  • @Service 어노테이션으로 서비스 클래스 임을 명시

  • @Autowired 어노테이션으로 관련 Mapper 클래스 주입

코드 블럭
package com.plateer.x2co.backoffice.sample.service;

@Service
public class SampleServiceImpl implements SampleService {
	
	@Autowired
	private SampleMapper sampleMapper ;

	@Override
	public ReponseEntity<List<SampleVO>> getPrDispCatList(String id) {
		return ResponseEntity.ok(sampleMapper.getPrDispCatList(id));
	}
}

 4) Mapper 클래스 및 Query 작성

  • src/main/java 하위 해당 업무 폴더에 dao 폴더 생성 후 아래와 같이 작업 진행

  • @Mapper 어노테이션을 이용하여 Mapper 임을 명시

코드 블럭
package com.plateer.x2co.backoffice.sample.dao;

@Mapper
public interface SampleMapper {

	public List<SampleVO> getPrDispCatList(String id);

}

  • src/main/resource 하위에 해당 쿼리 작성

  • namespace 에 위 Mapper 클래스 명시

  • resultType 혹은 parameterType 에 데이터 객체 명시

코드 블럭
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.plateer.x2co.api.backoffice.sample.dao.SampleMapper">
	
	<!-- 카테고리 계층 목록 조회 -->
	<select id="getPrDispCatList" resultType="com.plateer.x2co.backoffice.sample.entity.SampleVO">
	/* prDispCatBase.getPrDispCatList */
		SELECT
	....

	</select>

 위 작업 진행 시 최종 패키지 구조는 다음과 같음

9. 유효성 체크 방법

 1) Bean Validation

  • 데이터 통신을 VO 로 하므로, 다음과 같이 VO 객체에 javax.validation.constrains 패키지에서 제공하는 어노테이션을 이용

코드 블럭
package com.plateer.x2co.api.prototype.entity;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

import io.swagger.annotations.ApiParam;
import lombok.Data;

@Data
public class Category {
	
    @NotEmpty
    private String catTpCd;

    @NotBlank
    private String siteNo;

    @NotEmpty
    private String dpmlNo;
    
    @NotEmpty
    private String dbLocaleLanguage;
    
    @NotEmpty
    private int maxLvl = 0;
    
    @NotEmpty
    private int minLvl = 0;
}

  1-1) @Valid 를 이용한 @RequestBody 에 대한 유효성 검증

코드 블럭
public class SampleController{ 

    @PostMapping(value="categories")
    public ResponseEntity<?> getPrDispCatList(@Valid @RequestBody Category category) { 
        ....
    }
}

  1-2) @Validated 를 이용한 @PathVariable과 @RequestParam 에 대한 유효성 검증

코드 블럭
@RestController 
@Validated 
public class SampleController{ 

    @GetMapping("/users/{email}") 
    public String getUserInfoByEmail(@PathVariable("email") @Email String email) { 
        ....
    }
} 

 2) Custom Validator

  • javax.validation.constrains 패키지에서 제공되지 않는 validator 를 구현하고자 하는 경우, 본 방법으로 진행

  2-1) Custom annotation 생성

코드 블럭
import java.lang.annotation.Documented; 
import java.lang.annotation.ElementType; 
import java.lang.annotation.Retention; 
import java.lang.annotation.RetentionPolicy; 
import java.lang.annotation.Target; 
import javax.validation.Constraint; 
import javax.validation.Payload; 

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = LocaleValidator.class)
@Documented
public @interface LocaleConstraint { 
    String message() default "Invalid Locale"; 
    Class<?>[] groups() default {}; 
    Class<? extends Payload>[] payload() default {}; 
}

  2-2) Custom annotation 을 처리할 Custom Validator 생성

코드 블럭
public class LocaleValidator implements ConstraintValidator<LocaleConstraint, String>{ 
    public static final List<String> locales= Arrays.asList("ko_KR", "en_US"); 

    @Override 
    public boolean isValid(String value, ConstraintValidatorContext context) { 
        return value != null && locales.contains(value.toLowerCase()) ; 
    } 
}

  2-3) Custom annotation @LocaleConstraint을 적용하여 유효성 검증

코드 블럭
package com.plateer.x2co.api.prototype.entity;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

import io.swagger.annotations.ApiParam;
import lombok.Data;

@Data
public class Category {
	
    @NotEmpty
    private String catTpCd;

    @NotBlank
    private String siteNo;

    @NotEmpty
    private String dpmlNo;
    
    @NotEmpty
    @LocaleConstraint // 다음과 같이 어노테이션으로 적용
    private String dbLocaleLanguage;
    
    @NotEmpty
    private int maxLvl = 0;
    
    @NotEmpty
    private int minLvl = 0;
}

10. 에러메시지 및 예외 처리

 1) 에러 메시지 처리

  • X2Commerce 4.0 에서 사용될 에러 메시지 포맷

코드 블럭
package com.plateer.base.exception;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

import org.springframework.http.HttpStatus;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.Data;

@Data
public class X2ApiError {

	/*
	 * status: the HTTP status code
	 * LocalDateTime: the local date time
	 * message: the error message associated with exception
	 * error: List of constructed error messages
	 */
	private HttpStatus status;
	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
	private LocalDateTime timestamp;
	private String message;
	private List<String> errors;

	public X2ApiError(HttpStatus status, String message, List<String> errors) {
		super();	
		this.status = status;
		this.message = message;
		this.timestamp = LocalDateTime.now();
		this.errors = errors;
	}

   	public X2ApiError(HttpStatus status, String message, String error) {
        	super();
        	this.status = status;
        	this.message = message;
		this.timestamp = LocalDateTime.now();
        	errors = Arrays.asList(error);
    	}
}

  • 오류 발생 시 아래와 같이 응답 (HTTP Response code 는 헤더에 포함되어 있기 때문에 추가로 전송하지 않음)

코드 블럭
{
    "status": "METHOD_NOT_ALLOWED",
    "timestamp": "2020-09-17 11:20:42",
    "message": "Request method 'PUT' not supported",
    "errors": [
        "PUT method is not supported for this request. Supported methods are POST "
    ]
}

 2) 예외 처리 기능 (com.plateer.base.exception 패키지 하위 X2ExceptionController 클래스 구현)

  • ResponseEntityExceptionHandler 확장 구현

  • Spring 에서 기본적으로 제공해주는 ResponseEntityExceptionHandler 를 상속 받아 Exception Handler 구현

  • @ControllerAdvice 어노테이션을 이용하여 Exception 을 일괄 처리

코드 블럭
package com.plateer.base.exception;

import java.util.ArrayList;
import java.util.List;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class X2ExceptionController extends ResponseEntityExceptionHandler{
	
	// 400
	@Override
	protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
	    List<String> errors = new ArrayList<String>();
	    for (FieldError error : ex.getBindingResult().getFieldErrors()) {
	        errors.add(error.getField() + ": " + error.getDefaultMessage());
	    }
	    for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
	        errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
	    }
	    
	    X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
	    return handleExceptionInternal(ex, x2ApiError, headers, x2ApiError.getStatus(), request);
	}

	// 400
	@Override
	protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
		String error = ex.getParameterName() + " parameter is missing";
		
		X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
		return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}

	// 400
	@ExceptionHandler({ ConstraintViolationException.class })
	public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) {
	    List<String> errors = new ArrayList<String>();
	    for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
	        errors.add(violation.getRootBeanClass().getName() + " " + 
	          violation.getPropertyPath() + ": " + violation.getMessage());
	    }
	 
	    X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
	    return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
	
	// 400
	@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
	public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) {
	    String error = ex.getName() + " should be of type " + ex.getRequiredType().getName();
	    
	    X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
	    return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
	
	// 401
	@ExceptionHandler({ AuthenticationException.class })
    public ResponseEntity<Object> authenticationException(AuthenticationException ex, WebRequest request) {
		X2ApiError x2ApiError = new X2ApiError(HttpStatus.UNAUTHORIZED, ex.getLocalizedMessage(), "error occurred");
		return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
    }
	
	// 403
	@ExceptionHandler({ AccessDeniedException.class })
	public ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException ex, WebRequest request) {
		X2ApiError x2ApiError = new X2ApiError(HttpStatus.FORBIDDEN, ex.getLocalizedMessage(), "error occurred");
		return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
	
	// 404
	@Override
	protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
	    String error = "No handler found for  " + ex.getHttpMethod() + " " + ex.getRequestURL();
	 
	    X2ApiError x2ApiError = new X2ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
	    return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
	
	// 405
	@Override
	protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
	    StringBuilder builder = new StringBuilder();
	    builder.append(ex.getMethod());
	    builder.append(" method is not supported for this request. Supported methods are ");
	    ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));
	    
	    X2ApiError x2ApiError = new X2ApiError(HttpStatus.METHOD_NOT_ALLOWED, ex.getLocalizedMessage(), builder.toString());
	    return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
	
	// 409
	@ExceptionHandler({ DataIntegrityViolationException.class })
	public ResponseEntity<Object> handleDataIntegrityViolationException(DataIntegrityViolationException ex, WebRequest request) {
		X2ApiError x2ApiError = new X2ApiError(HttpStatus.CONFLICT, ex.getLocalizedMessage(), "error occurred");
		return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
	
	// 415
	@Override
	protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
	    StringBuilder builder = new StringBuilder();
	    builder.append(ex.getContentType());
	    builder.append(" media type is not supported. Supported media types are ");
	    ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));
	    
	    X2ApiError x2ApiError = new X2ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
	    return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
	
	// 500, Default Handler
	@ExceptionHandler({ Exception.class })
	public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) {
		X2ApiError x2ApiError = new X2ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
	    return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus());
	}
}




11. 트랜잭션 처리 (추후 정책 수립 후 작성 예정)