버전 비교

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

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

목차
minLevel1
maxLevel1
stylesquare

1. 사용 라이브러리 정보

명칭

버전

용도

JRE System Library

1.8

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

Spring Boot Framework

5.3.9

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

Mybatis-spring-boot-starter

2.5.3

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

Logback

로깅을 위한 라이브러리

thymeleaf-spring

3.0.12

컨트롤러가 전달하는 데이터를 VIEW에 표시해주는 라이브러리

lombok

1.18.20

반복되는 메소드를 Annotation을 사용해서 자동으로 작성해주는 라이브러리

lucy-xss-servlet

2.0.1

XSS 방지 필터

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 파일 내 port 및 DB 관련 설정 추가

  • 아래와 같이 application.yml 파일에서 서버 port번호와 데이터베이스 관련 설정 명시

  • 단일연결

코드 블럭
languageyaml
....
server:
  port: 8080

spring:
  config:
    activate:
      on-profile: local
  devtools:
    livereload:
      port: 3${server.port}
  datasource:
    url: jdbc:log4jdbc:postgresql://xxxxx.xxxxxx.xxx:55005/{DatabaseName}?currentSchema={SchemaName}
    driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy    
    username: {userName}
    password: ******************
    hikari:
      maximum-pool-size: 3
      minimum-idle: 3
      connection-timeout: 30000
      validation-timeout: 5000
      max-lifetime: 1800000
      idle-timeout: 300000

  session:
    store-type: none

  zipkin:
    enabled: false
....
  • 다중연결

코드 블럭
languageyaml
....
server:
  port: 8097
  servlet:
    context-path: /api/bo
    
spring:
  config:
    activate:
      on-profile: local
  zipkin:
    enabled: false
  devtools:
    livereload:
      port: 3${server.port}
  displayrodb:
    datasource:
      url: jdbc:log4jdbc:postgresql://xxxxx.xxxxxx.xxx:55005/{DatabaseName}?currentSchema={SchemaName}
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy    
      username: {userName}
      password: ******************
      hikari:
        maximum-pool-size: 5
        minimum-idle: 3
        connection-timeout: 30000
        validation-timeout: 5000
        max-lifetime: 1800000
        idle-timeout: 300000
  displayrwdb:
    datasource:
      url: jdbc:log4jdbc:postgresql://xxxxx.xxxxxx.xxx:55005/{DatabaseName}?currentSchema={SchemaName}
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy    
      username: {userName}
      password: ******************
      hikari:
        maximum-pool-size: 5
        minimum-idle: 3
        connection-timeout: 30000
        validation-timeout: 5000
        max-lifetime: 1800000
        idle-timeout: 300000
....

프로퍼티명

설명

port

tomcat 포트

url

데이터베이스 접속 URL

username

데이터베이스 사용자 아이디

password

데이터베이스 사용자 비밀번호

driveClassName

데이터베이스 드라이버 클래스 명

hikari

기타

session

스프링 session 설정

zipkin

MSA 환경에서 분산 트렌젝션의 추적

 2) *******DatabaseConfig.java 파일 작성

DisplayRodbDatabaseConfig.java

코드 블럭
languagejava
/**
 * 
 */
package com.x2bee.api.bo.base.config;

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.PlatformTransactionManager;

@Configuration
@MapperScan(value="com.x2bee.api.bo.app.repository.displayrodb", sqlSessionFactoryRef="displayRodbSqlSessionFactory")
public class DisplayRodbDatabaseConfig {
    @Bean(name = "displayRodbDataSource")
    @ConfigurationProperties(prefix = "spring.displayrodb.datasource")
    public DataSource displayRodbDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "displayRodbSqlSessionFactory")
    public SqlSessionFactory displayRodbSqlSessionFactory(@Qualifier("displayRodbDataSource") DataSource displayRodbDataSource, ApplicationContext applicationContext) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(displayRodbDataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.x2bee.api.bo.app");
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mapper/displayrodb/**/*.xml"));
        sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource("classpath:mapper/mybatis-config.xml"));
        return sqlSessionFactoryBean.getObject();
    }
 
    @Bean(name = "displayRodbSqlSessionTemplate")
    public SqlSessionTemplate displayRodbSqlSessionTemplate(SqlSessionFactory displayRodbSqlSessionFactory) throws Exception { 
        return new SqlSessionTemplate(displayRodbSqlSessionFactory);
    }

...

 3) mybatis-config.xml 파일 작성

  • 데이터베이스 데이터 마스킹, 암호화 관련 java 파일 연결

코드 블럭
languagexml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>		<setting name="mapUnderscoreToCamelCase" value="true" />
	</settings>
	<plugins>
	<plugin interceptor="com.x2bee.api.bo.base.masking.MybatisMaskingInterceptor"/>
        <plugin interceptor="com.x2bee.common.base.encrypt.MybatisEncryptInterceptor"/>
    </plugins>
</configuration>

 4) 데이터베이스 데이터 마스킹, 암호화 관련 java파일 작성

MybatisMaskingInterceptor.java

코드 블럭
languagejava
package com.x2bee.api.bo.base.masking;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Properties;

import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.springframework.beans.factory.annotation.Autowired;

import com.x2bee.api.bo.app.service.common.AdminCommonService;
import com.x2bee.common.base.context.ApplicationContextWrapper;
import com.x2bee.common.base.masking.MaskString;

/**
 * Result interceptor
*/
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class }))
public class MybatisMaskingInterceptor implements Interceptor {
	
	@Override
    public Object intercept(Invocation invocation) throws Throwable {
		
		BoApiMaskingUtils maskingUtils = (BoApiMaskingUtils)ApplicationContextWrapper.getBean("boApiMaskingUtils");
		
        Object result = invocation.proceed();
        if (Objects.isNull(result)){
            return null;
        }

        if (result instanceof ArrayList) {
            ArrayList<?> resultList = (ArrayList<?>) result;

            for (int i = 0; i < resultList.size(); i++) {
            	Field[] fields = resultList.get(i).getClass().getDeclaredFields();
            	for (Field field : fields) {
            		MaskString annotation = field.getAnnotation(MaskString.class);

            		if(annotation!=null && field.getType() == String.class) {
            			field.setAccessible(true);
            			String val = maskingUtils.getValue(field.get(resultList.get(i))+"", annotation.type());
            			try {
            				field.set(resultList.get(i), val);
            			}
                        catch (IllegalAccessException e) {
                        	System.out.println(e.getMessage());
                        }
            		}
            	}
            }
        }else {
        	Field[] fields = result.getClass().getDeclaredFields();
        	for (Field field : fields) {
        		MaskString annotation = field.getAnnotation(MaskString.class);

        		if(annotation!=null && field.getType() == String.class) {
        			field.setAccessible(true);
        			String val = maskingUtils.getValue(field.get(result)+"", annotation.type());
        			try {
        				field.set(result, val);
        			}catch (IllegalAccessException e) {
                    	System.out.println(e.getMessage());
                    }
        		}
        	}

        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

MybatisEncryptInterceptor.java

코드 블럭
languagejava
/**
 * 
 */
package com.x2bee.common.base.encrypt;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Objects;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

import lombok.extern.slf4j.Slf4j;

/**
 * @author choiyh44
 * @version 1.0
 * @since 2021. 12. 6.
 *
 */
@Slf4j
@Intercepts({
    @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class })
})
public class MybatisEncryptInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String method = invocation.getMethod().getName();
        
        if ("update".equals(method)) {
            return processUpdate(invocation);
        }
        else if ("handleResultSets".equals(method)) {
            return processQuery(invocation);
        }
        else {
            return invocation.proceed();
        }
    }

    private Object processUpdate(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
        Object[] args = invocation.getArgs();
        Object param = args[1];
        if (param != null) {
            Field[] fields = param.getClass().getDeclaredFields();
            for (Field field : fields) {
                Encrypt annotation = field.getAnnotation(Encrypt.class);
                if(annotation!=null && field.getType() == String.class) {
                    field.setAccessible(true);
                    try {
                        String val = EncryptUtils.getEncryptValue(field.get(param)+"", annotation.type());
                        log.info("EncryptValue: {}: {}", val.length(), val);
                        field.set(param, val);
                    }
                    catch (Exception e) {
                        log.warn(e.getMessage(), e);
                    }
                }
            }
        }
        return invocation.proceed();
    }

    private Object processQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
        Object result = invocation.proceed();
        if (Objects.isNull(result)){
            return null;
        }

        if (result instanceof ArrayList) {
            ArrayList<?> resultList = (ArrayList<?>) result;

            for (int i = 0; i < resultList.size(); i++) {
                Field[] fields = resultList.get(i).getClass().getDeclaredFields();
                for (Field field : fields) {
                    Encrypt annotation = field.getAnnotation(Encrypt.class);

                    if(annotation!=null && field.getType() == String.class) {
                        field.setAccessible(true);
                        try {
                            String val = EncryptUtils.getDecryptValue(field.get(resultList.get(i))+"", annotation.type());
                            field.set(resultList.get(i), val);
                        }
                        catch (Exception e) {
                            System.out.println(e.getMessage());
                        }
                    }
                }
            }
        }else {
            Field[] fields = result.getClass().getDeclaredFields();
            for (Field field : fields) {
                Encrypt annotation = field.getAnnotation(Encrypt.class);

                if(annotation!=null && field.getType() == String.class) {
                    field.setAccessible(true);
                    try {
                        String val = EncryptUtils.getDecryptValue(field.get(result)+"", annotation.type());
                        field.set(result, val);
                    }catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                }
            }

        }
        return result;
    }

}

5. 로깅 설정

 1)

로깅 관련 프로퍼티 설정
  • 로깅 파일 저장 위치 설정

  • 로깅 레벨 설정

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

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

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

코드 블럭logging.destination=C://log logging.level=INFO logging.lightLogging=false logging.includeControllerParameter=true logging.includeServiceParameter=false

쿼리로깅 log4jdbc 설정 파일 추가

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

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

 2) logback 관련 설정

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

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

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

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

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

code
코드 블럭
languagexml
<?xml version="1.0" encoding="UTF-8"
?>
<configuration>
<configuration
	<include 
scan
resource="
true" scanPeriod="30 seconds">
org/springframework/boot/logging/logback/defaults.xml" />
	<include resource="org/springframework/boot/logging/logback/console-appender.xml" />

	<springProperty scope="context" name="
activeProfile
myappName" source="spring.
profiles
application.
active
name"/>

	<property 
resource
name="
config/dev/properties/logging.properties"/> <property name="loggingPattern"
MSG_FORMAT" value="%d{yyyy-MM-dd 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
} [${myappName}] [%-5p] [%t] [%X{traceId},%X{spanId}] [%F::%M\\(%L\\)] [%X{requestURL}] : %m%n"/>

	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			
<Pattern>$
<pattern>${
loggingPattern
MSG_FORMAT}</
Pattern>
pattern>
		</layout>
	</appender>

	
<!-- log file appender --> <appender
<property name="
fileAppender
COLOR_MSG_FORMAT" 
class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${logging.destination}/debug.log</file> <rollingPolicy class="
value="%clr(%d{yyyy-MM-dd HH:mm:ss}){faint} %clr([%-5p]) [%X{requestURL}] %clr([%X{traceId},%X{spanId}]){magenta} %clr([%30.-30F::%-20.20M\\(%4L\\)]){cyan} %clr(:){faint} %m%n"/>

	<appender name="COLOR_STDOUT" class="ch.qos.logback.core.
rolling.TimeBasedRollingPolicy
ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<pattern>${COLOR_MSG_FORMAT}</pattern>
		</layout>
	<
!--
/appender>
Daily

Rollover --
	<springProfile name="local, default">
		<!-- log4jdbc 옵션 
<fileNamePattern>${logging.destination}/debug.%d{yyyy-MM-dd}_[%i].log</fileNamePattern>
설정 -->
		<logger name="jdbc" 
level="OFF"/>
		<!--
File Size 300Mb
 커넥션 open close 이벤트 로그로 남김 -->
		<logger 
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>300MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy>
name="jdbc.connection" level="OFF"/>
		<!-- SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여짐 -->
		<logger name="jdbc.sqlonly" level="OFF"/>
		<!-- 
Keep
SQL문과 
30
해당 
days
SQL을 
--> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>${loggingPattern}</pattern> </encoder> </appender>
실행시키는데 수행된 시간 정보(milliseconds)를 포함 -->
		<logger name="jdbc.sqltiming" level="DEBUG"/>
		<!-- 
비동기로
ResultSet을 
console에
제외한 
추가
모든 
로깅
JDBC 
--> <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>
호출 정보를 로그로 남김. 방대한 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않음-->
		<logger name="jdbc.audit" level="OFF"/>
		<!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 방대한 양의 로그가 생성됨 -->
		<logger name="jdbc.resultset" level="OFF"/>
		<!-- SQL 결과 조회된 데이터의 table을 로그로 남김 -->
		<logger name="jdbc.resultsettable" level="
OFF
DEBUG"/>
		<logger name="
jdbc
com.
sqlonly
amazonaws" level="
OFF
error"/>
		<logger name="org.springframework.jdbc.datasource.
sqltiming
DataSourceTransactionManager" 
level
additivity="
DEBUG"/
false" level='off'>
			
<logger
<appender-ref 
name
ref="
jdbc.audit
COLOR_STDOUT" 
level="OFF"
/>
		</logger>

		<logger name="
jdbc
com.x2bee.
resultset
common" 
level
additivity="
OFF"/
false" level='debug'>
	
<logger name="jdbc.resultsettable" level="OFF"/>
		<appender-ref ref="COLOR_STDOUT" />
		</logger>
		<logger name="
jdbc
com.x2bee.
connection
api" 
level
additivity="
OFF"/> <root
false" level=
"${logging.level}"
'debug'>
			<appender-ref ref="
consoleAppender
COLOR_STDOUT" />
		</logger>
		
<appender-ref
<root 
ref
level="
fileAppender
info"
/
>

	
<!-- 비동기 logging -->
		
<!--
<appender-ref ref="
asyncConsoleAppender
COLOR_STDOUT" />
		</root>
	</springProfile>

	<springProfile name="dev, stg, qa, prd">
		<appender
-ref
 
ref
name="
asyncFileAppender
logbackTcp" 
/
class="net.logstash.logback.appender.LogstashTcpSocketAppender">
			<destination>fluentd-
->
svc.thm-mgmt:24220</destination>
	
</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>

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

Image Removed

9. 유효성 체크 방법

 1) Bean Validation

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

code
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
				<providers>
					<timestamp/>
					<mdc />
					<pattern>
						<pattern>
							{
							"project": "${myappName}"
							}
						</pattern>
					</pattern>
					<logLevel/>
					<context />
					<threadName/>
					<loggerName/>
					<callerData/>
					<message/>
					<stackTrace/>
				</providers>
			</encoder>
		</appender>
	</springProfile>


	<springProfile name="dev, stg, qa">
		<logger name="com.x2bee.common" additivity="false" level='debug'>
			<appender-ref ref="logbackTcp" />
		</logger>
		<logger name="com.x2bee.api" additivity="false" level='debug'>
			<appender-ref ref="logbackTcp" />
		</logger>
		<root level="info">
			<appender-ref ref="logbackTcp" />
		</root>
	</springProfile>

	<springProfile name="prd">
		<logger name="com.x2bee.common" additivity="false" level='warn'>
			<appender-ref ref="logbackTcp" />
		</logger>
		<logger name="com.x2bee.api" additivity="false" level='warn'>
			<appender-ref ref="logbackTcp" />
		</logger>
		<root level="info">
			<appender-ref ref="logbackTcp" />
		</root>
	</springProfile>

</configuration>

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

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

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

User.java

코드 블럭
languagejava
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;
}

7. 서버 측 개발 방식

 Controller, Service, (APIController, service), Mapper 구조

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

  • MAS 구조로 BO 에서 API서버를 호출하여 DB서버와 통신하는 형태

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

 1) DTO 클래스 작성

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

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

Sample.java

코드 블럭
languagejava
package com.x2bee.api.bo.app.entity;
import javax.validation.constraints.NotNull;
import org.apache.ibatis.type.Alias;
import com.x2bee.common.base.entity.BaseCommonEntity;

import lombok.Getter;
import lombok.Setter;

@Alias("Sample")
@Getter
@Setter
public class Sample extends BaseCommonEntity {
	private static final long serialVersionUID = -5756700830219562201L;
	private Long id;
	@NotNull
	private String name;
	private String description;
}

 2) Controller 클래스 작성

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

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

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

  • @RequiredArgsConstructor어노테이션으로 생성자 명시

SampleController.java

코드 블럭
languagejava
package com.x2bee.bo.app.controller.sample;

import java.util.List;

import javax.validation.Valid;

import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.x2bee.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.bo.app.entity.Sample;
import com.x2bee.bo.app.service.sample.SampleService;
import com.x2bee.common.base.rest.Response;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/samples")
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleController {
	private final SampleService sampleService;

//	@GetMapping("")
//	public Response<List<SampleResponse>> getAllSamples() {
//		return new Response<List<SampleResponse>>().setPayload(sampleService.getAllSamples());
//	}

	@GetMapping("/{id}")
	public Response<SampleResponse> getSample(@PathVariable Long id) {
		return new Response<SampleResponse>().setPayload(sampleService.getSample(id));
	}

	@GetMapping("/{id}/save")
	public Response<SampleResponse> saveSample(@PathVariable Long id) {
		return new Response<SampleResponse>().setPayload(sampleService.saveSample(id));
	}

//	@GetMapping("/search")
//	public Response<List<SampleResponse>> searchSamples(@RequestBody SampleRequest sampleRequest) {
//		log.info("sampleRequest: {}", sampleRequest);
//		return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest));
//	}
//
	@PostMapping("")
	public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException {
		log.info("sampleRequest: {}", sample);

		return new Response<String>();
	}

	@PutMapping("/{id}")
	public Response<String> saveSample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
		log.info("id: {}, sampleRequest: {}", id, sampleRequest);

		return new Response<String>();
	}

	@PatchMapping("/{id}")
	public Response<String> modifySample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
		log.info("id: {}, sampleRequest: {}", id, sampleRequest);

		return new Response<String>();
	}

	@DeleteMapping("/{id}")
	public Response<String> removeSample(@PathVariable Long id) {
		log.info("id: {}", id);

		return new Response<String>();
	}

//	@GetMapping("/error")
//	public Response<String> getError() {
//		if (true) {
//			AppException.exception(ApiError.UNKNOWN);
//		}
//
//		return new Response<String>();
//	}

	/**
	 * void 유형으로 응답하면 안됩니다. Response<T> 유형으로 응답 해야합니다.
	 * @param sampleParam
	 * @throws InterruptedException
	 * @deprecated
	 */
	@PostMapping("/void")
	public void registerVod(@RequestBody SampleRequest sampleRequest) throws InterruptedException {
		log.info("sampleRequest: {}", sampleRequest);
	}

//	@PostMapping("/crc-cds")
//	public Response<String> registerCrcCd() throws Exception {
//		sampleService.registerCrcCd();
//		return new Response<String>();
//	}

	@GetMapping("/call-goods")
	public Response<List<SampleResponse>> callChain(SampleRequest sampleRequest) throws Exception {
		log.info("sampleRequest: {}", sampleRequest);
		return new Response<List<SampleResponse>>().setPayload(sampleService.callChain(sampleRequest));
	}

	/**
	 * 동영상 업로드 샘플. 
	 * 상품컨텐츠정보 등록 시 동영상이 업로드 되는 경우의 샘플임.
	 * POST formData 로 업로드한다.
	 * @return
	 */
	@PostMapping("/mpic")
	public Response<String> registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
		sampleService.registerGoodsContInfoWithMpic(sampleMpicRequest);
		return new Response<String>();
	}

	@GetMapping("/publishAlarm")
	public Response<String> publishAlarm() {
		sampleService.publishAlarm();
		return new Response<String>();
	}

	@GetMapping("/publishPhoneAppointment")
	public Response<String> publishPhoneAppointment() {
		sampleService.publishPhoneAppointment();
		return new Response<String>();
	}

	
	
}

 3) Service 클래스 작성

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

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

SampleService.java

코드 블럭
languagejava
package com.x2bee.bo.app.service.sample;

import java.util.List;

import com.x2bee.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.bo.app.dto.response.sample.SampleResponse;

public interface SampleService {
//	public List<SampleResponse> getAllSamples();
	public SampleResponse getSample(Long id);
//	public List<SampleResponse> searchSamples(SampleRequest sampleRequest);
//	void registerCrcCd() throws Exception;
	public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception;
	public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest);
	public SampleResponse saveSample(Long id);
	public void publishPhoneAppointment();
	public void publishAlarm();
}

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

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

  • @Value 어노테이션으로 API 서버 URL 명시

  • RestApiUtil 의 get, post, put, delete 메서드를 이용하여 API 호출

SampleServiceImpl.java

코드 블럭
languagejava
package com.x2bee.bo.app.service.sample;

import java.util.List;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import com.x2bee.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.bo.app.dto.request.messaging.ReceiveMessage;
import com.x2bee.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.bo.app.dto.response.common.MpicResponse;
import com.x2bee.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.bo.app.entity.PrGoodsContInfo;
import com.x2bee.bo.app.entity.StMpicMappInfo;
import com.x2bee.bo.app.service.common.MpicService;
import com.x2bee.bo.base.redismessage.AlarmRedisMessagePublisher;
import com.x2bee.bo.base.redismessage.PhoneAppointmentMessage;
import com.x2bee.bo.base.redismessage.PhoneAppointmentRedisMessagePublisher;
import com.x2bee.common.base.rest.Response;
import com.x2bee.common.base.rest.RestApiUtil;
import com.x2bee.common.base.upload.AttacheFileKind;
import com.x2bee.common.base.util.JsonUtils;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {
	private final MpicService mpicService;
	
	private final RestApiUtil restApiUtil;

	@Value("${app.apiUrl.goods}")
	private String goodsApiUrl;

    private final PhoneAppointmentRedisMessagePublisher phoneAppointmentRedisMessagePublisher;
    private final AlarmRedisMessagePublisher alarmRedisMessagePublisher;

	@Override
	@Cacheable(cacheNames="SampleService:getSample", key="#id", cacheManager="defaultRedisCacheManager")
	public SampleResponse getSample(Long id) {
		SampleResponse sampleResponse = new SampleResponse();
		sampleResponse.setId(1L);
		sampleResponse.setName("테스트님");
		sampleResponse.setDescription("테스트입니다.");
		log.debug("@@@@@@@@@@@@@@ SampleResponse: {}", sampleResponse);
		return sampleResponse;
	}

	@Override
	@CacheEvict(cacheNames="SampleService:getSample", key="#id", cacheManager="defaultRedisCacheManager")
	public SampleResponse saveSample(Long id) {
		SampleResponse sampleResponse = new SampleResponse();
		sampleResponse.setId(1L);
		sampleResponse.setName("테스트님");
		sampleResponse.setDescription("테스트입니다.");
		log.debug("@@@@@@@@@@@@@@ SampleResponse: {}", sampleResponse);
		return sampleResponse;
	}

	@Override
	public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception {
        return restApiUtil.get(goodsApiUrl+ "/api/goods/samples/list", sampleRequest, new ParameterizedTypeReference<Response<List<SampleResponse>>>() {}).getPayload();
	}

	/**
	 * 동영상 업로드 샘플 서비스
	 */
	@Override
	public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
		// 파일 업로드
		MpicResponse mpicResponse = mpicService.uploadMpic(sampleMpicRequest.getFile1(), sampleMpicRequest.getDirection(), AttacheFileKind.GOODS);
		
		// 업무컨텐츠정보 등록
		PrGoodsContInfo prGoodsContInfo = insertPrGoodsContInfo(sampleMpicRequest, mpicResponse);
		
		// 동영상변환상태정보 등록
		registerMpicMappInfo(prGoodsContInfo, mpicResponse);
	}


	// 업무컨텐츠정보 등록 - 업무별 비지니스 로직 구현필요
	private PrGoodsContInfo insertPrGoodsContInfo(SampleMpicRequest sampleMpicRequest, MpicResponse mpicResponse) {
		PrGoodsContInfo prGoodsContInfo = new PrGoodsContInfo();
		prGoodsContInfo.setGoodsNo("TEST_001");
		prGoodsContInfo.setCmtTypCd("02");
		prGoodsContInfo.setCmtSerialNo("10"+RandomStringUtils.randomNumeric(6));
		prGoodsContInfo.setOptnCatNo("1000");
		prGoodsContInfo.setOptnNo("BLACK");
		prGoodsContInfo.setImgGbCd("T01");
		prGoodsContInfo.setBaseImgYn("N");
		prGoodsContInfo.setContFilePathNm(null);
		prGoodsContInfo.setContFileNm(mpicResponse.getFileNm());
		prGoodsContInfo.setTrnfTextCont("testTrnfTextCont");
		prGoodsContInfo.setSysRegId("FRONT");
		prGoodsContInfo.setSysModId("FRONT");
//		sampleTrxMapper.insertPrGoodsContInfo(prGoodsContInfo);
		
		return prGoodsContInfo;
	}

	// 동영상변환상태정보 등록
	private void registerMpicMappInfo(PrGoodsContInfo prGoodsContInfo, MpicResponse mpicResponse) {
		StMpicMappInfo stMpicMappInfo = new StMpicMappInfo();
		// 원본경로명이 "/" 로 시작하도록 처리함.
		String s3OrigPathNm = StringUtils.startsWith(mpicResponse.getS3OrigPathNm(), "/") ? mpicResponse.getS3OrigPathNm() : "/" + mpicResponse.getS3OrigPathNm();
		stMpicMappInfo.setOrgPathNm(s3OrigPathNm);
		stMpicMappInfo.setTblNm("pr_goods_cont_info");
		stMpicMappInfo.setRef1Val(prGoodsContInfo.getGoodsNo());
		stMpicMappInfo.setRef2Val(prGoodsContInfo.getCmtTypCd());
		stMpicMappInfo.setRef3Val(prGoodsContInfo.getCmtSerialNo());
		stMpicMappInfo.setSysRegId("SAMPLE");
		stMpicMappInfo.setSysModId("SAMPLE");
		mpicService.registerMpicMappInfo(stMpicMappInfo);
	}
	
	@Override
    public void publishAlarm() {
        log.info("redis pub/sub: publish");
        ReceiveMessage receiveMessage = new ReceiveMessage();
        receiveMessage.setType(ReceiveMessage.MessageType.ALARM);
        receiveMessage.setId("x2bee");
        receiveMessage.setCount("999");
        receiveMessage.setMessageTxt("test message text");
        alarmRedisMessagePublisher.publish(JsonUtils.string(receiveMessage));
    }

	@Override
    public void publishPhoneAppointment() {
        log.info("redis pub/sub: publish");
        PhoneAppointmentMessage phoneReservationResponse = new PhoneAppointmentMessage();
        phoneReservationResponse.setId("x2bee");
        phoneAppointmentRedisMessagePublisher.publish(JsonUtils.string(phoneReservationResponse));
    }

}

4) API Controller 클래스 작성

  • API 서버의 Controller 작성

  • Response 객체로 Return

SampleController.java

코드 블럭
languagejava
package com.x2bee.api.bo.app.controller.sample;

import java.util.List;

import javax.validation.Valid;

import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.api.bo.app.entity.Sample;
import com.x2bee.api.bo.app.service.sample.SampleService;
import com.x2bee.api.bo.base.advice.ApiError;
import com.x2bee.api.bo.base.annotation.IndInfoLog;
import com.x2bee.common.base.exception.AppException;
import com.x2bee.common.base.rest.Response;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@RestController
@RequestMapping("/samples")
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleController {
	private final SampleService sampleService;

	@GetMapping("")
	public Response<List<SampleResponse>> getAllSamples() {
		return new Response<List<SampleResponse>>().setPayload(sampleService.getAllSamples());
	}

	@GetMapping("/{id}")
	public Response<SampleResponse> getSample(@PathVariable Long id,
			@RequestHeader(value="test-header-key1", required = false) String testHeader1) {
		log.info("id: {}, testHeader1: {}", id, testHeader1);
		return new Response<SampleResponse>().setPayload(sampleService.getSample(id));
	}

	@GetMapping("/search")
	public Response<List<SampleResponse>> searchSamples(SampleRequest sampleRequest) {
		log.info("sampleRequest: {}", sampleRequest);
		return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest));
	}

	@PostMapping("")
	public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException {
		log.info("sampleRequest: {}", sample);

		return new Response<String>();
	}

	@PutMapping("/{id}")
	public Response<String> saveSample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
		log.info("id: {}, sampleRequest: {}", id, sampleRequest);

		return new Response<String>();
	}

	@PatchMapping("/{id}")
	public Response<String> modifySample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) {
		log.info("id: {}, sampleRequest: {}", id, sampleRequest);

		return new Response<String>();
	}

	@DeleteMapping("/{id}")
	public Response<String> removeSample(@PathVariable Long id) {
		log.info("id: {}", id);

		return new Response<String>();
	}

	@GetMapping("/error")
	public Response<String> getError() {
		if (true) {
			AppException.exception(ApiError.UNKNOWN);
		}

		return new Response<String>();
	}

	/**
	 * void 유형으로 응답하면 안됩니다. Response<T> 유형으로 응답 해야합니다.
	 * @param sampleParam
	 * @throws InterruptedException
	 * @deprecated
	 */
	@PostMapping("/void")
	public void registerVod(@RequestBody SampleRequest sampleRequest) throws InterruptedException {
		log.info("sampleRequest: {}", sampleRequest);
	}

	@PostMapping("/display-samples")
	public Response<String> registerDisplaySample(@RequestBody SampleRequest sampleRequest) throws Exception {
		log.info("sampleRequest: {}", sampleRequest);
		return new Response<String>().setPayload(sampleService.registerDisplaySample(sampleRequest));
	}

	@GetMapping("/call-order")
	public Response<List<SampleResponse>> callOrder(SampleRequest sampleRequest) throws Exception {
		log.info("sampleRequest: {}", sampleRequest);
		return new Response<List<SampleResponse>>().setPayload(sampleService.callChain(sampleRequest));
	}

	@GetMapping("/infInfoLog")
	@IndInfoLog
	public Response<List<SampleResponse>> infInfoLog(SampleRequest sampleRequest) {
		log.info("sampleRequest: {}", sampleRequest);
		return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest));
	}

	/**
	 * 동영상 업로드 샘플. 
	 * 상품컨텐츠정보 등록 시 동영상이 업로드 되는 경우의 샘플임.
	 * POST formData 로 업로드한다.
	 * @return
	 */
	@PostMapping("/mpic")
	public Response<String> registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
		sampleService.registerGoodsContInfoWithMpic(sampleMpicRequest);
		return new Response<String>();
	}


}

5) API Service 클래스 작성

SampleService.java

코드 블럭
languagejava
package com.x2bee.api.bo.app.service.sample;

import java.util.List;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;

public interface SampleService {
	public List<SampleResponse> getAllSamples();
	public SampleResponse getSample(Long id);
	public List<SampleResponse> searchSamples(SampleRequest sampleRequest);
	String registerDisplaySample(SampleRequest sampleRequest) throws Exception;
	public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception;
	public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest);
}
  • 위 인터페이스의 실제 구현체 작성

SampleServiceImpl.java

코드 블럭
languagejava
package com.x2bee.api.bo.app.service.sample;

import java.util.List;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;

import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest;
import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.common.MpicResponse;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;
import com.x2bee.api.bo.app.entity.PrGoodsContInfo;
import com.x2bee.api.bo.app.entity.StMpicMappInfo;
import com.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper;
import com.x2bee.api.bo.app.repository.displayrwdb.sample.SampleTrxMapper;
import com.x2bee.api.bo.app.service.common.MpicService;
import com.x2bee.api.bo.base.advice.ApiError;
import com.x2bee.common.base.exception.AppException;
import com.x2bee.common.base.rest.Response;
import com.x2bee.common.base.rest.RestApiUtil;
import com.x2bee.common.base.upload.AttacheFileKind;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@Lazy
@Slf4j
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {
	private final SampleMapper sampleMapper;
	private final SampleTrxMapper sampleTrxMapper;
	private final RestApiUtil restApiUtil;
	private final MpicService mpicService;

	@Value("${app.apiUrl.display}")
	private String displayApiUrl;
	
	@Value("${app.apiUrl.order}")
	private String orderApiUrl;

	@Value("${sample.prop}")
	private String sampleProp;
	
	@Override
	public List<SampleResponse> getAllSamples() {
		log.debug("sample prop: {}", sampleProp);
		return sampleMapper.selectAllSamples();
	}

	@Override
	public SampleResponse getSample(Long id) {
		SampleResponse sampleResponse = sampleMapper.selectSampleById(id).orElse(null);
		if (sampleResponse == null) {
			AppException.exception(ApiError.DATA_NOT_FOUND);
		}
		return sampleResponse;
	}

	@Override
	public List<SampleResponse> searchSamples(SampleRequest sampleRequest) {
		return sampleMapper.selectSamples(sampleRequest);
	}

    @Override
    public String registerDisplaySample(SampleRequest sampleRequest) throws Exception {
        return restApiUtil.post(displayApiUrl+ "/api/display/samples", sampleRequest, new ParameterizedTypeReference<Response<String>>() {}).getPayload();
    }

	@Override
	public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception {
        return restApiUtil.get(orderApiUrl+ "/api/order/samples/call-display", sampleRequest, new ParameterizedTypeReference<Response<List<SampleResponse>>>() {}).getPayload();
	}

	/**
	 * 동영상 업로드 샘플 서비스
	 */
	@Override
	public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) {
		// 파일 업로드
		MpicResponse mpicResponse = mpicService.uploadMpic(sampleMpicRequest.getFile1(), sampleMpicRequest.getDirection(), AttacheFileKind.GOODS);
		
		// 업무컨텐츠정보 등록
		PrGoodsContInfo prGoodsContInfo = insertPrGoodsContInfo(sampleMpicRequest, mpicResponse);
		
		// 동영상변환상태정보 등록
		registerMpicMappInfo(prGoodsContInfo, mpicResponse);
	}


	// 업무컨텐츠정보 등록 - 업무별 비지니스 로직 구현필요
	private PrGoodsContInfo insertPrGoodsContInfo(SampleMpicRequest sampleMpicRequest, MpicResponse mpicResponse) {
		PrGoodsContInfo prGoodsContInfo = new PrGoodsContInfo();
		prGoodsContInfo.setGoodsNo("TEST_001");
		prGoodsContInfo.setCmtTypCd("02");
		prGoodsContInfo.setCmtSerialNo("10"+RandomStringUtils.randomNumeric(6));
		prGoodsContInfo.setOptnCatNo("1000");
		prGoodsContInfo.setOptnNo("BLACK");
		prGoodsContInfo.setImgGbCd("T01");
		prGoodsContInfo.setBaseImgYn("N");
		prGoodsContInfo.setContFilePathNm(null);
		prGoodsContInfo.setContFileNm(mpicResponse.getFileNm());
		prGoodsContInfo.setTrnfTextCont("testTrnfTextCont");
		prGoodsContInfo.setSysRegId("FRONT");
		prGoodsContInfo.setSysModId("FRONT");
		sampleTrxMapper.insertPrGoodsContInfo(prGoodsContInfo);
		
		return prGoodsContInfo;
	}

	// 동영상변환상태정보 등록
	private void registerMpicMappInfo(PrGoodsContInfo prGoodsContInfo, MpicResponse mpicResponse) {
		StMpicMappInfo stMpicMappInfo = new StMpicMappInfo();
		// 원본경로명이 "/" 로 시작하도록 처리함.
		String s3OrigPathNm = StringUtils.startsWith(mpicResponse.getS3OrigPathNm(), "/") ? mpicResponse.getS3OrigPathNm() : "/" + mpicResponse.getS3OrigPathNm();
		stMpicMappInfo.setOrgPathNm(s3OrigPathNm);
		stMpicMappInfo.setTblNm("pr_goods_cont_info");
		stMpicMappInfo.setRef1Val(prGoodsContInfo.getGoodsNo());
		stMpicMappInfo.setRef2Val(prGoodsContInfo.getCmtTypCd());
		stMpicMappInfo.setRef3Val(prGoodsContInfo.getCmtSerialNo());
		stMpicMappInfo.setSysRegId("SAMPLE");
		stMpicMappInfo.setSysModId("SAMPLE");
		mpicService.registerMpicMappInfo(stMpicMappInfo);
	}

}

6) Mapper 클래스 및 Query 작성

  • src/main/java 하위 해당 업무 폴더에 repository/{db연결명} 폴더 생성 후 아래와 같이 작업 진행

SampleMapper.java

코드 블럭
package com.x2bee.api.bo.app.repository.displayrodb.sample;

import java.util.List;
import java.util.Optional;

import com.x2bee.api.bo.app.dto.request.sample.SampleRequest;
import com.x2bee.api.bo.app.dto.response.sample.SampleResponse;

public interface SampleMapper {
	public List<SampleResponse> selectAllSamples();
	public Optional<SampleResponse> selectSampleById(Long id);
	public List<SampleResponse> selectSamples(SampleRequest request);
}
  • src/main/resource 하위에 해당 쿼리 작성

  • namespace 에 위 Mapper 클래스 명시

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

SampleMapper.xml

코드 블럭
languagexml
<?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.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper">
	<sql id="sampleList">
        SELECT 
	        1 as id,
	        'name1' as name,
	        'desc1' as description
	    union all
        SELECT 
	        2 as id,
	        'name2' as name,
	        'desc2' as description
	    union all
        SELECT 
	        3 as id,
	        'name3' as name,
	        'desc3' as description
	</sql>

	
    <!--
    	전체 샘플 조회
    -->
    <select id="selectAllSamples" resultType="sampleResponse">
    	/* SampleMapper.selectAllSamples */
		<include refid="sampleList" />
    </select>
    
    <!--
    	샘플 단건 조회
    -->
    <select id="selectSampleById" parameterType="long" resultType="sampleResponse">
    	/* SampleMapper.selectSampleById */
    	select *
    	from (
        <include refid="sampleList" />
        ) a
        where id = #{id}
    </select>
    
    <!--
    	샘플 목록 조회
    -->
    <select id="selectSamples" parameterType="sampleRequest" resultType="sampleResponse">
    	/* SampleMapper.selectSamples */
    	select *
    	from (
        <include refid="sampleList" />
        ) a
        <where>
        	<if test="id != null">
        		and id = #{id}
        	</if>
        	<if test="name != null and name != ''">
        		and name = #{name}
        	</if>
        	<if test="description != null and description != ''">
        		and description = #{description}
        	</if>
        </where>
    </select>
    
</mapper>

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

Image Added

8. 유효성 체크 방법

 1) Bean Validation

  • Alias 어노테이션으로 별칭 지정

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

Group.java

코드 블럭
languagejava
package com.x2bee.api.bo.app.entity;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import org.apache.ibatis.type.Alias;

import com.x2bee.common.base.entity.AbstractEntity;

import lombok.Getter;
import lombok.Setter;

@Alias("group")
@Getter
@Setter
public class Group extends AbstractEntity {
	@NotNull
	String groupNo;
	
	@NotEmpty
	String groupName;
}

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

SampleController.java

코드 블럭
languagejava
public class SampleController{ 
		...
    @PostMapping("")
	public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException {
		...
	}
		...
}

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

SampleController.java

코드 블럭
languagejava
@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 생성

LocaleConstraint.java

코드 블럭
languagejava
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 생성

LocaleValidator.java

코드 블럭
languagejava
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을 적용하여 유효성 검증

Category.java

코드 블럭
languagejava
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
@LocaleConstraint 
String
// 
dbLocaleLanguage;
다음과 같이 어노테이션으로 
적용
 
@NotEmpty
   
private
int
 
maxLvl =
String 
0
dbLocaleLanguage;
    
    @NotEmpty
    private int 
minLvl
maxLvl = 0;
}

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

코드 블럭public
 
class
 
SampleController{
  
    
@PostMapping(value="categories")
@NotEmpty
    
public
private 
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
int minLvl = 0;
}

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

 1) 에러 메시지 처리

  • X2BEE 1.0 에서 사용할 에러처리 포멧'

GlobalErrorController.java

코드 블럭
languagejava
package com.x2bee.bo.base.advice;

import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.x2bee.bo.base.exception.AuthException;
import com.x2bee.common.base.exception.MessageResolver;
import com.x2bee.common.base.exception.ValidationException;
import com.x2bee.common.base.rest.ErrorResponse;

import lombok.extern.slf4j.Slf4j;

@Controller
//@RequestMapping("${server.error.path:${error.path:/error}}") // 1)
@RequestMapping("/error") // 1)
@Slf4j
public class GlobalErrorController extends AbstractErrorController {
	public static final String EXCEPTION_KEY = "_ExceptioN_KEY_";
    public static final String COMMON_ERROR_PAGE = "common/error";

	public GlobalErrorController(ErrorAttributes errorAttributes) {
		super(errorAttributes);
	}
	
	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 2)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
		Map<String, Object> model = getErrorAttributes(request, options);
		String errorPage = "error/error";
		Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
		
		if (exception != null) {
			Throwable throwable = exception.getCause();
        	if (throwable instanceof AuthException) {
        		errorPage = "error/403";
            } else if (throwable instanceof ValidationException) {
         
return
 
value
 
!=
 
null
	errorPage 
&& locales.contains(value.toLowerCase())
= "error/404";
     
}
  
}

  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
 } else {
            	errorPage = "error/500";
   
private
 
String
 
catTpCd;
      
@NotBlank
 }
		} else {
private String siteNo;
			errorPage = "error/500";
		}

   
@NotEmpty
 	ModelAndView modelAndView = new 
private String dpmlNo
ModelAndView(errorPage, model);
    	modelAndView.addObject(EXCEPTION_KEY, exception.getCause());
   
@NotEmpty
 	return modelAndView;
	}

@LocaleConstraint
	protected 
// 다음과 같이 어노테이션으로 적용 private String dbLocaleLanguage; @NotEmpty private int maxLvl = 0;
ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
		ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
		options = options.including(Include.MESSAGE);
		options = options.including(Include.BINDING_ERRORS);
		return options;
	}

	@RequestMapping
    public ResponseEntity<ErrorResponse> error(HttpServletRequest request) {
        Object status = 
@NotEmpty
request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);

    
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) {
 log.error("status_code: {}", request.getAttribute("javax.servlet.error.status_code"));
        log.error("exception_type: {}", request.getAttribute("javax.servlet.error.exception_type"));
        log.error("message: {}", request.getAttribute("javax.servlet.error.message"));
        log.error("request_uri: {}", request.getAttribute("javax.servlet.error.request_uri"));
        log.error("exception: {}", request.getAttribute("javax.servlet.error.exception"));

        Exception exception = (Exception)request.getAttribute("javax.servlet.error.exception");
        
        if (exception != null) {
        	Throwable throwable = exception.getCause();
        	if (throwable instanceof AuthException) {
        		return new ResponseEntity<ErrorResponse>(
        
super();
				new ErrorResponse("0403", ((AuthException)throwable).getMessage()),
        
this.status = status;
				new HttpHeaders(), HttpStatus.FORBIDDEN);
            } 
this.message = message; this.timestamp = LocalDateTime.now();
else {
            	
errors
return 
=
new 
Arrays.asList(error);
ResponseEntity<ErrorResponse>(
    
}
 
}
  • 오류 발생 시 아래와 같이 응답 (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()); } }
 				new ErrorResponse("9000", MessageResolver.getMessage("adminCommon.system.error")),
        				new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
            }
        } else {
    		return new ResponseEntity<ErrorResponse>(
    				new ErrorResponse("9000", MessageResolver.getMessage("adminCommon.system.error")),
    				new HttpHeaders(), HttpStatus.valueOf(Integer.valueOf(String.valueOf(status))));
        }
    }

}

  • API 오류 처리

  • Response 객체에 Error 내용을 담아 Return

ErrorResponse.java

코드 블럭
package com.x2bee.common.base.rest;

import java.time.LocalDateTime;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;

@Getter
@Setter
@ToString
@Accessors(chain = true)
@NoArgsConstructor
public class ErrorResponse {
	@JsonSerialize(using = LocalDateTimeSerializer.class)
	@JsonDeserialize(using = LocalDateTimeDeserializer.class)
	private LocalDateTime timestamp = LocalDateTime.now();
	private String code;
	private String message;
	private final Object payload = null;
	
	public ErrorResponse(String code, String message) {
		this.code = code;
		this.message = message;
	}
	
}

  • Error Code 정의

코드 블럭
languagejava
package com.x2bee.api.bo.base.advice;

import com.x2bee.common.base.exception.AppError;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ApiError implements AppError {
	// success
	SUCCESS("0000", "common.message.success"),
	// app error
	EMPTY_PARAMETER("1001", "common.error.emptyParameter"),
	INVALID_PARAMETER("1002", "common.error.invalidParameter"),
	DATA_NOT_FOUND("1003", "common.error.dataNotFound"),
	DUPLICATE_DATA("1004", "common.error.duplicateData"),
	UPLOAD_FAIL("1100", "common.error.uploadFail"),
	
	//ERP
	ERP_ERROR_MESSAGE("2000", "common.erp.errorMessage"),
	ERP_INTRERFACE_ERROR("2001", "common.erp.interfaceError"),
	// 권한 없음.
	NOT_AUTHORIZED("7000", "common.error.notAuthorized"),
	// binding error
	BINDING_ERROR("8000", "common.error.bindingError"),
	BINDING_ERROR_NOT_NULL("8001", "common.error.bindingErrorNotNull"),
	// unknow error
	UNKNOWN("9000", "common.error.unknown"),
	// ValidatioException error
	VALIDATION_EXCEPTION("9100", "common.error.unknown");

	private final String code;
	private final String messageKey;

}

 2) 예외 처리 기능 (x2bee-common 패키지 하위 Exception 클래스 구현)

  • src/main/java 아래에 패키지 생성 후 Exception 코드 작성

  • 예시1

CommonException.java

코드 블럭
languagejava
package com.x2bee.common.base.exception;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommonException extends UserDefinedException {
	private static final long serialVersionUID = 1L;

    private String errorCode;
    private String errorMessage;

	public CommonException(String message, Throwable cause) {
		super(message, cause);
		
	}

	public CommonException(String message) {
		super(message);
		
	}

	public CommonException(Throwable cause) {
		super(cause);
		
	}
	
	public CommonException(String errorCode, String errorMessage) {
		super(errorMessage);
		
		this.errorCode = errorCode;
		this.errorMessage = errorMessage;
	}
	
}
  • 예시2

ValidationException.java

코드 블럭
languagejava
package com.x2bee.common.base.exception;

@SuppressWarnings("serial")
public class ValidationException extends UserDefinedException {

    public ValidationException() {
        super();
    }

    public ValidationException(String message) {
        super(message);
    }

    public ValidationException(Throwable t) {
        super(t);
    }

    public ValidationException(String message, Throwable t) {
        super(message, t);
    }
}

10. Transaction 처리

  • 등록/수정/삭제 서비스 메소드에 자동으로 트랜잭션이 동작하도록 AOP 가 설정되어있음

  • register/modify/delete/save 를 메소드 이름에 포함시켜야 동작하므로 메소드 명칭에 오타가 없도록 유의

registerSampleMulti.java

코드 블럭
languagejava
...
public void registerSampleMulti(Cateogry category, Cateogry subCategory) throws Exception {
  categoryMapper.insert(category);
  categoryMapper.insert(subCategory);
}
...

DisplayDbTranscationAspect.java

코드 블럭
languagejava
@Aspect
@Configuration
public class DisplayDbTranscationAspect {

@Resource(name="displayRwdbTxManager")
private TransactionManager displayRwdbTxManager;

private static final String EXPRESSION 
= " execution(* com.x2bee.api.bo.app.service.code.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.common.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.dashboard.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.display.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.goods.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.inf.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.main.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.popup.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.sample.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.statistics.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.system.*ServiceImpl.*(..)) "
+ " || execution(* com.x2bee.api.bo.app.service.vendor.*ServiceImpl.*(..)) "
;

@Bean
public TransactionInterceptor displayDbTransactionAdvice() {

List<RollbackRuleAttribute> rollbackRules = Collections.singletonList(new RollbackRuleAttribute(Exception.class));

RuleBasedTransactionAttribute transactionAttribute = new RuleBasedTransactionAttribute();
transactionAttribute.setRollbackRules(rollbackRules);
transactionAttribute.setName("*");

MatchAlwaysTransactionAttributeSource attributeSource = new MatchAlwaysTransactionAttributeSource();
attributeSource.setTransactionAttribute(transactionAttribute);

return new TransactionInterceptor(displayRwdbTxManager, attributeSource);
}

@Bean
public Advisor displayDbTransactionAdvisor() {

AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(EXPRESSION);

return new DefaultPointcutAdvisor(pointcut, displayDbTransactionAdvice());
}

}

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