메타 데이터의 끝으로 건너뛰기
메타 데이터의 시작으로 이동

이 페이지의 이전 버전을 보고 있습니다. 현재 버전 보기.

현재와 비교 페이지 이력 보기

« 이전 버전 7 다음 »

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

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번호와 데이터베이스 관련 설정 명시

  • 단일연결

....
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
....
  • 다중연결

....
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 파일 작성

/**
 * 
 */
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 파일 연결

<?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파일 작성

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) {
    }
}

/**
 * 
 */
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

 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. 트랜잭션 처리 (추후 정책 수립 후 작성 예정)

  • 레이블 없음