X2BEE 개발을 위한 개발 환경을
제공하기 위한 자료입니다사전에 이해하고 안정적인 빌드를 위해 개발자를 위한 환경을 설명합니다.
목차 | ||||||
---|---|---|---|---|---|---|
|
사용 라이브러리 정보
명칭 | 버전 | 용도 |
---|---|---|
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 방지 필터 |
프로젝트 패키지 구조
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
language | java |
---|
작성자 | ||||||
---|---|---|---|---|---|---|
|