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

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

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

버전 1 다음 »

개발 가이드
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 파일 작성

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

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

MybatisEncryptInterceptor.java

/**
 * 
 */
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) 쿼리로깅 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: 시스템 콘솔에 찍히는 로그 정보

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<include resource="org/springframework/boot/logging/logback/defaults.xml" />
	<include resource="org/springframework/boot/logging/logback/console-appender.xml" />

	<springProperty scope="context" name="myappName" source="spring.application.name"/>
	<property name="MSG_FORMAT" value="%d{yyyy-MM-dd HH:mm:ss} [${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>${MSG_FORMAT}</pattern>
		</layout>
	</appender>

	<property name="COLOR_MSG_FORMAT" 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.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<pattern>${COLOR_MSG_FORMAT}</pattern>
		</layout>
	</appender>

	<springProfile name="local, default">
		<!-- 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"/>
		<logger name="com.amazonaws" level="error"/>
		<logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" additivity="false" level='off'>
			<appender-ref ref="COLOR_STDOUT" />
		</logger>

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

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

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

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

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

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

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

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

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

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

<?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>

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

8. 유효성 체크 방법

 1) Bean Validation

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

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

Group.java

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

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

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

SampleController.java

@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

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

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

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;
}

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

 1) 에러 메시지 처리

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

GlobalErrorController.java

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) {
            	errorPage = "error/404";
            } else {
            	errorPage = "error/500";
            }
		} else {
			errorPage = "error/500";
		}

    	ModelAndView modelAndView = new ModelAndView(errorPage, model);
    	modelAndView.addObject(EXCEPTION_KEY, exception.getCause());
    	return modelAndView;
	}

	protected 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 = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);

        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>(
        				new ErrorResponse("0403", ((AuthException)throwable).getMessage()),
        				new HttpHeaders(), HttpStatus.FORBIDDEN);
            } else {
            	return new ResponseEntity<ErrorResponse>(
        				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 정의

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

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

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. 트랜잭션 처리

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

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

registerSampleMulti.java

...
public void registerSampleMulti(Cateogry category, Cateogry subCategory) throws Exception {
  categoryMapper.insert(category);
  categoryMapper.insert(subCategory);
}
...

DisplayDbTranscationAspect.java

@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());
}

}

  • 레이블 없음