버전 비교

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

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

코드 블럭
languagexml
<?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

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

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

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
    @LocaleConstraint // 다음과 같이 어노테이션으로 적용
    private String dbLocaleLanguage;
    
    @NotEmpty
    private int maxLvl = 0;
    
    @NotEmpty
    private 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) {
            	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

코드 블럭
languagejava
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. 트랜잭션 처리

  • 등록/수정/삭제 서비스 메소드에 자동으로 트랜잭션이 동작하도록 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()); } }
작성자
showCounttrue
showAnonymoustrue
orderupdate