개발 가이드
X2BEE 개발 환경을 제공하기 위한 자료입니다.
1. 사용 라이브러리 정보
명칭 | 버전 | 용도 |
---|---|---|
JRE System Library | 1.8 | 웹 어플리케이션 구동에 필요한 자바 런타임 라이브러리 |
Spring Boot Framework | 5.3.9 | 서버 로직 전반을 담당하는 프레임워크 |
Mybatis-spring-boot-starter | 2.5.3 | 데이터베이스 연결 및 쿼리 처리를 위한 라이브러리 |
Logback | 로깅을 위한 라이브러리 | |
thymeleaf-spring | 3.0.12 | 컨트롤러가 전달하는 데이터를 VIEW에 표시해주는 라이브러리 |
lombok | 1.18.20 | 반복되는 메소드를 Annotation을 사용해서 자동으로 작성해주는 라이브러리 |
lucy-xss-servlet | 2.0.1 | XSS 방지 필터 |
2. 프로젝트 패키지 구조
1) src/main/java
프레임워크 공통 클래스 패키지: 예외처리, DB관련 속성, 보안, 유틸 클래스 포함
API 관련 비즈니스 로직 클래스 패키지: Controller, Service, Dao, Entity 클래스
프레임워크 관련 로직 클래스 패키지: RootController, User, Role 관련 설정 클래스
설정 관련 로직 클래스 패키지: 프로젝트 관련 설정 클래스
2) src/main/resources
프레임워크 설정 패캐지: dev/local 별 datasource 설정, 로깅 설정, 기타 설정 파일
API 관련 쿼리 패키지: 프로젝트 내에서 사용될 쿼리 파일
View 템플릿 패키지: Front-end html 파일과 error html 파일
기타 설정 패키지: 프로젝트 전체 appliation.yml 파일 및 설정 파일
3. 데이터베이스 연결, 다중 데이터베이스 연결
1) application.yml 파일 내 port 및 DB 관련 설정 추가
아래와 같이 application.yml 파일에서 서버 port번호와 데이터베이스 관련 설정 명시
단일연결
.... server: port: 8080 spring: config: activate: on-profile: local devtools: livereload: port: 3${server.port} datasource: url: jdbc:log4jdbc:postgresql://xxxxx.xxxxxx.xxx:55005/{DatabaseName}?currentSchema={SchemaName} driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy username: {userName} password: ****************** hikari: maximum-pool-size: 3 minimum-idle: 3 connection-timeout: 30000 validation-timeout: 5000 max-lifetime: 1800000 idle-timeout: 300000 session: store-type: none zipkin: enabled: false .... |
다중연결
.... server: port: 8097 servlet: context-path: /api/bo spring: config: activate: on-profile: local zipkin: enabled: false devtools: livereload: port: 3${server.port} displayrodb: datasource: url: jdbc:log4jdbc:postgresql://xxxxx.xxxxxx.xxx:55005/{DatabaseName}?currentSchema={SchemaName} driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy username: {userName} password: ****************** hikari: maximum-pool-size: 5 minimum-idle: 3 connection-timeout: 30000 validation-timeout: 5000 max-lifetime: 1800000 idle-timeout: 300000 displayrwdb: datasource: url: jdbc:log4jdbc:postgresql://xxxxx.xxxxxx.xxx:55005/{DatabaseName}?currentSchema={SchemaName} driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy username: {userName} password: ****************** hikari: maximum-pool-size: 5 minimum-idle: 3 connection-timeout: 30000 validation-timeout: 5000 max-lifetime: 1800000 idle-timeout: 300000 .... |
프로퍼티명 | 설명 |
---|---|
port | tomcat 포트 |
url | 데이터베이스 접속 URL |
username | 데이터베이스 사용자 아이디 |
password | 데이터베이스 사용자 비밀번호 |
driveClassName | 데이터베이스 드라이버 클래스 명 |
hikari | 기타 |
session | 스프링 session 설정 |
zipkin | MSA 환경에서 분산 트렌젝션의 추적 |
2) *******DatabaseConfig.java 파일 작성
DisplayRodbDatabaseConfig.java
/** * */ package com.x2bee.api.bo.base.config; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; @Configuration @MapperScan(value="com.x2bee.api.bo.app.repository.displayrodb", sqlSessionFactoryRef="displayRodbSqlSessionFactory") public class DisplayRodbDatabaseConfig { @Bean(name = "displayRodbDataSource") @ConfigurationProperties(prefix = "spring.displayrodb.datasource") public DataSource displayRodbDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "displayRodbSqlSessionFactory") public SqlSessionFactory displayRodbSqlSessionFactory(@Qualifier("displayRodbDataSource") DataSource displayRodbDataSource, ApplicationContext applicationContext) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(displayRodbDataSource); sqlSessionFactoryBean.setTypeAliasesPackage("com.x2bee.api.bo.app"); sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mapper/displayrodb/**/*.xml")); sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource("classpath:mapper/mybatis-config.xml")); return sqlSessionFactoryBean.getObject(); } @Bean(name = "displayRodbSqlSessionTemplate") public SqlSessionTemplate displayRodbSqlSessionTemplate(SqlSessionFactory displayRodbSqlSessionFactory) throws Exception { return new SqlSessionTemplate(displayRodbSqlSessionFactory); } ... |
3) mybatis-config.xml 파일 작성
데이터베이스 데이터 마스킹, 암호화 관련 java 파일 연결
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> <plugins> <plugin interceptor="com.x2bee.api.bo.base.masking.MybatisMaskingInterceptor"/> <plugin interceptor="com.x2bee.common.base.encrypt.MybatisEncryptInterceptor"/> </plugins> </configuration> |
4) 데이터베이스 데이터 마스킹, 암호화 관련 java파일 작성
MybatisMaskingInterceptor.java
package com.x2bee.api.bo.base.masking; import java.lang.reflect.Field; import java.sql.Statement; import java.util.ArrayList; import java.util.Objects; import java.util.Properties; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.springframework.beans.factory.annotation.Autowired; import com.x2bee.api.bo.app.service.common.AdminCommonService; import com.x2bee.common.base.context.ApplicationContextWrapper; import com.x2bee.common.base.masking.MaskString; /** * Result interceptor */ @Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class })) public class MybatisMaskingInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { BoApiMaskingUtils maskingUtils = (BoApiMaskingUtils)ApplicationContextWrapper.getBean("boApiMaskingUtils"); Object result = invocation.proceed(); if (Objects.isNull(result)){ return null; } if (result instanceof ArrayList) { ArrayList<?> resultList = (ArrayList<?>) result; for (int i = 0; i < resultList.size(); i++) { Field[] fields = resultList.get(i).getClass().getDeclaredFields(); for (Field field : fields) { MaskString annotation = field.getAnnotation(MaskString.class); if(annotation!=null && field.getType() == String.class) { field.setAccessible(true); String val = maskingUtils.getValue(field.get(resultList.get(i))+"", annotation.type()); try { field.set(resultList.get(i), val); } catch (IllegalAccessException e) { System.out.println(e.getMessage()); } } } } }else { Field[] fields = result.getClass().getDeclaredFields(); for (Field field : fields) { MaskString annotation = field.getAnnotation(MaskString.class); if(annotation!=null && field.getType() == String.class) { field.setAccessible(true); String val = maskingUtils.getValue(field.get(result)+"", annotation.type()); try { field.set(result, val); }catch (IllegalAccessException e) { System.out.println(e.getMessage()); } } } } return result; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } } |
MybatisEncryptInterceptor.java
/** * */ package com.x2bee.common.base.encrypt; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.sql.Statement; import java.util.ArrayList; import java.util.Objects; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import lombok.extern.slf4j.Slf4j; /** * @author choiyh44 * @version 1.0 * @since 2021. 12. 6. * */ @Slf4j @Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }), @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class }) }) public class MybatisEncryptInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { String method = invocation.getMethod().getName(); if ("update".equals(method)) { return processUpdate(invocation); } else if ("handleResultSets".equals(method)) { return processQuery(invocation); } else { return invocation.proceed(); } } private Object processUpdate(Invocation invocation) throws InvocationTargetException, IllegalAccessException { Object[] args = invocation.getArgs(); Object param = args[1]; if (param != null) { Field[] fields = param.getClass().getDeclaredFields(); for (Field field : fields) { Encrypt annotation = field.getAnnotation(Encrypt.class); if(annotation!=null && field.getType() == String.class) { field.setAccessible(true); try { String val = EncryptUtils.getEncryptValue(field.get(param)+"", annotation.type()); log.info("EncryptValue: {}: {}", val.length(), val); field.set(param, val); } catch (Exception e) { log.warn(e.getMessage(), e); } } } } return invocation.proceed(); } private Object processQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException { Object result = invocation.proceed(); if (Objects.isNull(result)){ return null; } if (result instanceof ArrayList) { ArrayList<?> resultList = (ArrayList<?>) result; for (int i = 0; i < resultList.size(); i++) { Field[] fields = resultList.get(i).getClass().getDeclaredFields(); for (Field field : fields) { Encrypt annotation = field.getAnnotation(Encrypt.class); if(annotation!=null && field.getType() == String.class) { field.setAccessible(true); try { String val = EncryptUtils.getDecryptValue(field.get(resultList.get(i))+"", annotation.type()); field.set(resultList.get(i), val); } catch (Exception e) { System.out.println(e.getMessage()); } } } } }else { Field[] fields = result.getClass().getDeclaredFields(); for (Field field : fields) { Encrypt annotation = field.getAnnotation(Encrypt.class); if(annotation!=null && field.getType() == String.class) { field.setAccessible(true); try { String val = EncryptUtils.getDecryptValue(field.get(result)+"", annotation.type()); field.set(result, val); }catch (Exception e) { System.out.println(e.getMessage()); } } } } return result; } } |
5. 로깅 설정
1) 쿼리로깅 log4jdbc 설정 파일 추가
resource 폴더 하위 아래 파일 추가
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator log4jdbc.dump.sql.maxlinelength=0 |
2) logback 관련 설정
src/main/resources/{} 폴더 하위 loback-spring.xml 파일 설정
consoleAppender: 시스템 콘솔에 찍히는 로그 정보
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml" /> <include resource="org/springframework/boot/logging/logback/console-appender.xml" /> <springProperty scope="context" name="myappName" source="spring.application.name"/> <property name="MSG_FORMAT" value="%d{yyyy-MM-dd HH:mm:ss} [${myappName}] [%-5p] [%t] [%X{traceId},%X{spanId}] [%F::%M\\(%L\\)] [%X{requestURL}] : %m%n"/> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern>${MSG_FORMAT}</pattern> </layout> </appender> <property name="COLOR_MSG_FORMAT" value="%clr(%d{yyyy-MM-dd HH:mm:ss}){faint} %clr([%-5p]) [%X{requestURL}] %clr([%X{traceId},%X{spanId}]){magenta} %clr([%30.-30F::%-20.20M\\(%4L\\)]){cyan} %clr(:){faint} %m%n"/> <appender name="COLOR_STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern>${COLOR_MSG_FORMAT}</pattern> </layout> </appender> <springProfile name="local, default"> <!-- log4jdbc 옵션 설정 --> <logger name="jdbc" level="OFF"/> <!-- 커넥션 open close 이벤트 로그로 남김 --> <logger name="jdbc.connection" level="OFF"/> <!-- SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여짐 --> <logger name="jdbc.sqlonly" level="OFF"/> <!-- SQL문과 해당 SQL을 실행시키는데 수행된 시간 정보(milliseconds)를 포함 --> <logger name="jdbc.sqltiming" level="DEBUG"/> <!-- ResultSet을 제외한 모든 JDBC 호출 정보를 로그로 남김. 방대한 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않음--> <logger name="jdbc.audit" level="OFF"/> <!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 방대한 양의 로그가 생성됨 --> <logger name="jdbc.resultset" level="OFF"/> <!-- SQL 결과 조회된 데이터의 table을 로그로 남김 --> <logger name="jdbc.resultsettable" level="DEBUG"/> <logger name="com.amazonaws" level="error"/> <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" additivity="false" level='off'> <appender-ref ref="COLOR_STDOUT" /> </logger> <logger name="com.x2bee.common" additivity="false" level='debug'> <appender-ref ref="COLOR_STDOUT" /> </logger> <logger name="com.x2bee.api" additivity="false" level='debug'> <appender-ref ref="COLOR_STDOUT" /> </logger> <root level="info"> <appender-ref ref="COLOR_STDOUT" /> </root> </springProfile> <springProfile name="dev, stg, qa, prd"> <appender name="logbackTcp" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>fluentd-svc.thm-mgmt:24220</destination> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp/> <mdc /> <pattern> <pattern> { "project": "${myappName}" } </pattern> </pattern> <logLevel/> <context /> <threadName/> <loggerName/> <callerData/> <message/> <stackTrace/> </providers> </encoder> </appender> </springProfile> <springProfile name="dev, stg, qa"> <logger name="com.x2bee.common" additivity="false" level='debug'> <appender-ref ref="logbackTcp" /> </logger> <logger name="com.x2bee.api" additivity="false" level='debug'> <appender-ref ref="logbackTcp" /> </logger> <root level="info"> <appender-ref ref="logbackTcp" /> </root> </springProfile> <springProfile name="prd"> <logger name="com.x2bee.common" additivity="false" level='warn'> <appender-ref ref="logbackTcp" /> </logger> <logger name="com.x2bee.api" additivity="false" level='warn'> <appender-ref ref="logbackTcp" /> </logger> <root level="info"> <appender-ref ref="logbackTcp" /> </root> </springProfile> </configuration> |
6. 클라이언트, 서버 간 통신 데이터 형식
Map 형태가 아닌 VO(Value Object) 로 데이터 통신하는 것을 기본으로 함
아래 사용자 정보 처리 관련 VO 예제 참고
User.java
package com.plateer.x2co.common.entity; import java.util.Date; import lombok.Data; @Data public class User { private String usrId; private String usrNm; private String usrGrp; private String useYn; private String mobileNo; private String phoneNo; private String email; private String pw; private Date pwModDt; private String mailReceivedYn; private String smsReceivedYn; private String pwMissCnt; private String pwInitYn; private Date lastLoginDt; private String lastLoginIp; private String accExpireYn; private String accLockYn; private String pwModRequireYn; } |
7. 서버 측 개발 방식
Controller, Service, (APIController, service), Mapper 구조
서버는 Spring MVC 패턴 기반이므로 데이터 처리를 위하여 다음과 같이 Controller, Service, Mapper 구조로 진행됨
MAS 구조로 BO 에서 API서버를 호출하여 DB서버와 통신하는 형태
http request → (mapping) → Controller → Service → ServiceImpl → APIController → Service → ServiceImpl → Mapper → XML(query) 호출 순서로 진행됨
1) DTO 클래스 작성
src/main/java 하위 해당 업무 폴더에 entity 폴더 생성 후 아래와 같이 작업 진행
데이터 전달을 위하여 사용될 객체 사전 정의
Sample.java
package com.x2bee.api.bo.app.entity; import javax.validation.constraints.NotNull; import org.apache.ibatis.type.Alias; import com.x2bee.common.base.entity.BaseCommonEntity; import lombok.Getter; import lombok.Setter; @Alias("Sample") @Getter @Setter public class Sample extends BaseCommonEntity { private static final long serialVersionUID = -5756700830219562201L; private Long id; @NotNull private String name; private String description; } |
2) Controller 클래스 작성
src/main/java 하위 해당 업무 폴더에 Controller 폴더 생성 후 아래와 같이 작업 진행
@RestController 어노테이션으로 Controller 임을 명시
@RequestMapping 어노테이션으로 해당 Controller 전체에 대한 Http Request 매핑 정보 명시
@RequiredArgsConstructor어노테이션으로 생성자 명시
SampleController.java
package com.x2bee.bo.app.controller.sample; import java.util.List; import javax.validation.Valid; import org.springframework.context.annotation.Lazy; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.x2bee.bo.app.dto.request.common.SampleMpicRequest; import com.x2bee.bo.app.dto.request.sample.SampleRequest; import com.x2bee.bo.app.dto.response.sample.SampleResponse; import com.x2bee.bo.app.entity.Sample; import com.x2bee.bo.app.service.sample.SampleService; import com.x2bee.common.base.rest.Response; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @RestController @RequestMapping("/samples") @Lazy @Slf4j @RequiredArgsConstructor public class SampleController { private final SampleService sampleService; // @GetMapping("") // public Response<List<SampleResponse>> getAllSamples() { // return new Response<List<SampleResponse>>().setPayload(sampleService.getAllSamples()); // } @GetMapping("/{id}") public Response<SampleResponse> getSample(@PathVariable Long id) { return new Response<SampleResponse>().setPayload(sampleService.getSample(id)); } @GetMapping("/{id}/save") public Response<SampleResponse> saveSample(@PathVariable Long id) { return new Response<SampleResponse>().setPayload(sampleService.saveSample(id)); } // @GetMapping("/search") // public Response<List<SampleResponse>> searchSamples(@RequestBody SampleRequest sampleRequest) { // log.info("sampleRequest: {}", sampleRequest); // return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest)); // } // @PostMapping("") public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException { log.info("sampleRequest: {}", sample); return new Response<String>(); } @PutMapping("/{id}") public Response<String> saveSample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) { log.info("id: {}, sampleRequest: {}", id, sampleRequest); return new Response<String>(); } @PatchMapping("/{id}") public Response<String> modifySample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) { log.info("id: {}, sampleRequest: {}", id, sampleRequest); return new Response<String>(); } @DeleteMapping("/{id}") public Response<String> removeSample(@PathVariable Long id) { log.info("id: {}", id); return new Response<String>(); } // @GetMapping("/error") // public Response<String> getError() { // if (true) { // AppException.exception(ApiError.UNKNOWN); // } // // return new Response<String>(); // } /** * void 유형으로 응답하면 안됩니다. Response<T> 유형으로 응답 해야합니다. * @param sampleParam * @throws InterruptedException * @deprecated */ @PostMapping("/void") public void registerVod(@RequestBody SampleRequest sampleRequest) throws InterruptedException { log.info("sampleRequest: {}", sampleRequest); } // @PostMapping("/crc-cds") // public Response<String> registerCrcCd() throws Exception { // sampleService.registerCrcCd(); // return new Response<String>(); // } @GetMapping("/call-goods") public Response<List<SampleResponse>> callChain(SampleRequest sampleRequest) throws Exception { log.info("sampleRequest: {}", sampleRequest); return new Response<List<SampleResponse>>().setPayload(sampleService.callChain(sampleRequest)); } /** * 동영상 업로드 샘플. * 상품컨텐츠정보 등록 시 동영상이 업로드 되는 경우의 샘플임. * POST formData 로 업로드한다. * @return */ @PostMapping("/mpic") public Response<String> registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) { sampleService.registerGoodsContInfoWithMpic(sampleMpicRequest); return new Response<String>(); } @GetMapping("/publishAlarm") public Response<String> publishAlarm() { sampleService.publishAlarm(); return new Response<String>(); } @GetMapping("/publishPhoneAppointment") public Response<String> publishPhoneAppointment() { sampleService.publishPhoneAppointment(); return new Response<String>(); } } |
3) Service 클래스 작성
src/main/java 하위 해당 업무 폴더에 Service 폴더 생성 후 아래와 같이 작업 진행
구현하고자 하는 업무 로직의 인터페이스 작성
SampleService.java
package com.x2bee.bo.app.service.sample; import java.util.List; import com.x2bee.bo.app.dto.request.common.SampleMpicRequest; import com.x2bee.bo.app.dto.request.sample.SampleRequest; import com.x2bee.bo.app.dto.response.sample.SampleResponse; public interface SampleService { // public List<SampleResponse> getAllSamples(); public SampleResponse getSample(Long id); // public List<SampleResponse> searchSamples(SampleRequest sampleRequest); // void registerCrcCd() throws Exception; public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception; public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest); public SampleResponse saveSample(Long id); public void publishPhoneAppointment(); public void publishAlarm(); } |
위 인터페이스의 실제 구현체 작성
@Service 어노테이션으로 서비스 클래스 임을 명시
@Value 어노테이션으로 API 서버 URL 명시
RestApiUtil 의 get, post, put, delete 메서드를 이용하여 API 호출
SampleServiceImpl.java
package com.x2bee.bo.app.service.sample; import java.util.List; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; import com.x2bee.bo.app.dto.request.common.SampleMpicRequest; import com.x2bee.bo.app.dto.request.messaging.ReceiveMessage; import com.x2bee.bo.app.dto.request.sample.SampleRequest; import com.x2bee.bo.app.dto.response.common.MpicResponse; import com.x2bee.bo.app.dto.response.sample.SampleResponse; import com.x2bee.bo.app.entity.PrGoodsContInfo; import com.x2bee.bo.app.entity.StMpicMappInfo; import com.x2bee.bo.app.service.common.MpicService; import com.x2bee.bo.base.redismessage.AlarmRedisMessagePublisher; import com.x2bee.bo.base.redismessage.PhoneAppointmentMessage; import com.x2bee.bo.base.redismessage.PhoneAppointmentRedisMessagePublisher; import com.x2bee.common.base.rest.Response; import com.x2bee.common.base.rest.RestApiUtil; import com.x2bee.common.base.upload.AttacheFileKind; import com.x2bee.common.base.util.JsonUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Service @Lazy @Slf4j @RequiredArgsConstructor public class SampleServiceImpl implements SampleService { private final MpicService mpicService; private final RestApiUtil restApiUtil; @Value("${app.apiUrl.goods}") private String goodsApiUrl; private final PhoneAppointmentRedisMessagePublisher phoneAppointmentRedisMessagePublisher; private final AlarmRedisMessagePublisher alarmRedisMessagePublisher; @Override @Cacheable(cacheNames="SampleService:getSample", key="#id", cacheManager="defaultRedisCacheManager") public SampleResponse getSample(Long id) { SampleResponse sampleResponse = new SampleResponse(); sampleResponse.setId(1L); sampleResponse.setName("테스트님"); sampleResponse.setDescription("테스트입니다."); log.debug("@@@@@@@@@@@@@@ SampleResponse: {}", sampleResponse); return sampleResponse; } @Override @CacheEvict(cacheNames="SampleService:getSample", key="#id", cacheManager="defaultRedisCacheManager") public SampleResponse saveSample(Long id) { SampleResponse sampleResponse = new SampleResponse(); sampleResponse.setId(1L); sampleResponse.setName("테스트님"); sampleResponse.setDescription("테스트입니다."); log.debug("@@@@@@@@@@@@@@ SampleResponse: {}", sampleResponse); return sampleResponse; } @Override public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception { return restApiUtil.get(goodsApiUrl+ "/api/goods/samples/list", sampleRequest, new ParameterizedTypeReference<Response<List<SampleResponse>>>() {}).getPayload(); } /** * 동영상 업로드 샘플 서비스 */ @Override public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) { // 파일 업로드 MpicResponse mpicResponse = mpicService.uploadMpic(sampleMpicRequest.getFile1(), sampleMpicRequest.getDirection(), AttacheFileKind.GOODS); // 업무컨텐츠정보 등록 PrGoodsContInfo prGoodsContInfo = insertPrGoodsContInfo(sampleMpicRequest, mpicResponse); // 동영상변환상태정보 등록 registerMpicMappInfo(prGoodsContInfo, mpicResponse); } // 업무컨텐츠정보 등록 - 업무별 비지니스 로직 구현필요 private PrGoodsContInfo insertPrGoodsContInfo(SampleMpicRequest sampleMpicRequest, MpicResponse mpicResponse) { PrGoodsContInfo prGoodsContInfo = new PrGoodsContInfo(); prGoodsContInfo.setGoodsNo("TEST_001"); prGoodsContInfo.setCmtTypCd("02"); prGoodsContInfo.setCmtSerialNo("10"+RandomStringUtils.randomNumeric(6)); prGoodsContInfo.setOptnCatNo("1000"); prGoodsContInfo.setOptnNo("BLACK"); prGoodsContInfo.setImgGbCd("T01"); prGoodsContInfo.setBaseImgYn("N"); prGoodsContInfo.setContFilePathNm(null); prGoodsContInfo.setContFileNm(mpicResponse.getFileNm()); prGoodsContInfo.setTrnfTextCont("testTrnfTextCont"); prGoodsContInfo.setSysRegId("FRONT"); prGoodsContInfo.setSysModId("FRONT"); // sampleTrxMapper.insertPrGoodsContInfo(prGoodsContInfo); return prGoodsContInfo; } // 동영상변환상태정보 등록 private void registerMpicMappInfo(PrGoodsContInfo prGoodsContInfo, MpicResponse mpicResponse) { StMpicMappInfo stMpicMappInfo = new StMpicMappInfo(); // 원본경로명이 "/" 로 시작하도록 처리함. String s3OrigPathNm = StringUtils.startsWith(mpicResponse.getS3OrigPathNm(), "/") ? mpicResponse.getS3OrigPathNm() : "/" + mpicResponse.getS3OrigPathNm(); stMpicMappInfo.setOrgPathNm(s3OrigPathNm); stMpicMappInfo.setTblNm("pr_goods_cont_info"); stMpicMappInfo.setRef1Val(prGoodsContInfo.getGoodsNo()); stMpicMappInfo.setRef2Val(prGoodsContInfo.getCmtTypCd()); stMpicMappInfo.setRef3Val(prGoodsContInfo.getCmtSerialNo()); stMpicMappInfo.setSysRegId("SAMPLE"); stMpicMappInfo.setSysModId("SAMPLE"); mpicService.registerMpicMappInfo(stMpicMappInfo); } @Override public void publishAlarm() { log.info("redis pub/sub: publish"); ReceiveMessage receiveMessage = new ReceiveMessage(); receiveMessage.setType(ReceiveMessage.MessageType.ALARM); receiveMessage.setId("x2bee"); receiveMessage.setCount("999"); receiveMessage.setMessageTxt("test message text"); alarmRedisMessagePublisher.publish(JsonUtils.string(receiveMessage)); } @Override public void publishPhoneAppointment() { log.info("redis pub/sub: publish"); PhoneAppointmentMessage phoneReservationResponse = new PhoneAppointmentMessage(); phoneReservationResponse.setId("x2bee"); phoneAppointmentRedisMessagePublisher.publish(JsonUtils.string(phoneReservationResponse)); } } |
4) API Controller 클래스 작성
API 서버의 Controller 작성
Response 객체로 Return
SampleController.java
package com.x2bee.api.bo.app.controller.sample; import java.util.List; import javax.validation.Valid; import org.springframework.context.annotation.Lazy; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest; import com.x2bee.api.bo.app.dto.request.sample.SampleRequest; import com.x2bee.api.bo.app.dto.response.sample.SampleResponse; import com.x2bee.api.bo.app.entity.Sample; import com.x2bee.api.bo.app.service.sample.SampleService; import com.x2bee.api.bo.base.advice.ApiError; import com.x2bee.api.bo.base.annotation.IndInfoLog; import com.x2bee.common.base.exception.AppException; import com.x2bee.common.base.rest.Response; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @RestController @RequestMapping("/samples") @Lazy @Slf4j @RequiredArgsConstructor public class SampleController { private final SampleService sampleService; @GetMapping("") public Response<List<SampleResponse>> getAllSamples() { return new Response<List<SampleResponse>>().setPayload(sampleService.getAllSamples()); } @GetMapping("/{id}") public Response<SampleResponse> getSample(@PathVariable Long id, @RequestHeader(value="test-header-key1", required = false) String testHeader1) { log.info("id: {}, testHeader1: {}", id, testHeader1); return new Response<SampleResponse>().setPayload(sampleService.getSample(id)); } @GetMapping("/search") public Response<List<SampleResponse>> searchSamples(SampleRequest sampleRequest) { log.info("sampleRequest: {}", sampleRequest); return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest)); } @PostMapping("") public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException { log.info("sampleRequest: {}", sample); return new Response<String>(); } @PutMapping("/{id}") public Response<String> saveSample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) { log.info("id: {}, sampleRequest: {}", id, sampleRequest); return new Response<String>(); } @PatchMapping("/{id}") public Response<String> modifySample(@PathVariable Long id, @RequestBody SampleRequest sampleRequest) { log.info("id: {}, sampleRequest: {}", id, sampleRequest); return new Response<String>(); } @DeleteMapping("/{id}") public Response<String> removeSample(@PathVariable Long id) { log.info("id: {}", id); return new Response<String>(); } @GetMapping("/error") public Response<String> getError() { if (true) { AppException.exception(ApiError.UNKNOWN); } return new Response<String>(); } /** * void 유형으로 응답하면 안됩니다. Response<T> 유형으로 응답 해야합니다. * @param sampleParam * @throws InterruptedException * @deprecated */ @PostMapping("/void") public void registerVod(@RequestBody SampleRequest sampleRequest) throws InterruptedException { log.info("sampleRequest: {}", sampleRequest); } @PostMapping("/display-samples") public Response<String> registerDisplaySample(@RequestBody SampleRequest sampleRequest) throws Exception { log.info("sampleRequest: {}", sampleRequest); return new Response<String>().setPayload(sampleService.registerDisplaySample(sampleRequest)); } @GetMapping("/call-order") public Response<List<SampleResponse>> callOrder(SampleRequest sampleRequest) throws Exception { log.info("sampleRequest: {}", sampleRequest); return new Response<List<SampleResponse>>().setPayload(sampleService.callChain(sampleRequest)); } @GetMapping("/infInfoLog") @IndInfoLog public Response<List<SampleResponse>> infInfoLog(SampleRequest sampleRequest) { log.info("sampleRequest: {}", sampleRequest); return new Response<List<SampleResponse>>().setPayload(sampleService.searchSamples(sampleRequest)); } /** * 동영상 업로드 샘플. * 상품컨텐츠정보 등록 시 동영상이 업로드 되는 경우의 샘플임. * POST formData 로 업로드한다. * @return */ @PostMapping("/mpic") public Response<String> registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) { sampleService.registerGoodsContInfoWithMpic(sampleMpicRequest); return new Response<String>(); } } |
5) API Service 클래스 작성
SampleService.java
package com.x2bee.api.bo.app.service.sample; import java.util.List; import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest; import com.x2bee.api.bo.app.dto.request.sample.SampleRequest; import com.x2bee.api.bo.app.dto.response.sample.SampleResponse; public interface SampleService { public List<SampleResponse> getAllSamples(); public SampleResponse getSample(Long id); public List<SampleResponse> searchSamples(SampleRequest sampleRequest); String registerDisplaySample(SampleRequest sampleRequest) throws Exception; public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception; public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest); } |
위 인터페이스의 실제 구현체 작성
SampleServiceImpl.java
package com.x2bee.api.bo.app.service.sample; import java.util.List; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; import com.x2bee.api.bo.app.dto.request.common.SampleMpicRequest; import com.x2bee.api.bo.app.dto.request.sample.SampleRequest; import com.x2bee.api.bo.app.dto.response.common.MpicResponse; import com.x2bee.api.bo.app.dto.response.sample.SampleResponse; import com.x2bee.api.bo.app.entity.PrGoodsContInfo; import com.x2bee.api.bo.app.entity.StMpicMappInfo; import com.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper; import com.x2bee.api.bo.app.repository.displayrwdb.sample.SampleTrxMapper; import com.x2bee.api.bo.app.service.common.MpicService; import com.x2bee.api.bo.base.advice.ApiError; import com.x2bee.common.base.exception.AppException; import com.x2bee.common.base.rest.Response; import com.x2bee.common.base.rest.RestApiUtil; import com.x2bee.common.base.upload.AttacheFileKind; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Service @Lazy @Slf4j @RequiredArgsConstructor public class SampleServiceImpl implements SampleService { private final SampleMapper sampleMapper; private final SampleTrxMapper sampleTrxMapper; private final RestApiUtil restApiUtil; private final MpicService mpicService; @Value("${app.apiUrl.display}") private String displayApiUrl; @Value("${app.apiUrl.order}") private String orderApiUrl; @Value("${sample.prop}") private String sampleProp; @Override public List<SampleResponse> getAllSamples() { log.debug("sample prop: {}", sampleProp); return sampleMapper.selectAllSamples(); } @Override public SampleResponse getSample(Long id) { SampleResponse sampleResponse = sampleMapper.selectSampleById(id).orElse(null); if (sampleResponse == null) { AppException.exception(ApiError.DATA_NOT_FOUND); } return sampleResponse; } @Override public List<SampleResponse> searchSamples(SampleRequest sampleRequest) { return sampleMapper.selectSamples(sampleRequest); } @Override public String registerDisplaySample(SampleRequest sampleRequest) throws Exception { return restApiUtil.post(displayApiUrl+ "/api/display/samples", sampleRequest, new ParameterizedTypeReference<Response<String>>() {}).getPayload(); } @Override public List<SampleResponse> callChain(SampleRequest sampleRequest) throws Exception { return restApiUtil.get(orderApiUrl+ "/api/order/samples/call-display", sampleRequest, new ParameterizedTypeReference<Response<List<SampleResponse>>>() {}).getPayload(); } /** * 동영상 업로드 샘플 서비스 */ @Override public void registerGoodsContInfoWithMpic(SampleMpicRequest sampleMpicRequest) { // 파일 업로드 MpicResponse mpicResponse = mpicService.uploadMpic(sampleMpicRequest.getFile1(), sampleMpicRequest.getDirection(), AttacheFileKind.GOODS); // 업무컨텐츠정보 등록 PrGoodsContInfo prGoodsContInfo = insertPrGoodsContInfo(sampleMpicRequest, mpicResponse); // 동영상변환상태정보 등록 registerMpicMappInfo(prGoodsContInfo, mpicResponse); } // 업무컨텐츠정보 등록 - 업무별 비지니스 로직 구현필요 private PrGoodsContInfo insertPrGoodsContInfo(SampleMpicRequest sampleMpicRequest, MpicResponse mpicResponse) { PrGoodsContInfo prGoodsContInfo = new PrGoodsContInfo(); prGoodsContInfo.setGoodsNo("TEST_001"); prGoodsContInfo.setCmtTypCd("02"); prGoodsContInfo.setCmtSerialNo("10"+RandomStringUtils.randomNumeric(6)); prGoodsContInfo.setOptnCatNo("1000"); prGoodsContInfo.setOptnNo("BLACK"); prGoodsContInfo.setImgGbCd("T01"); prGoodsContInfo.setBaseImgYn("N"); prGoodsContInfo.setContFilePathNm(null); prGoodsContInfo.setContFileNm(mpicResponse.getFileNm()); prGoodsContInfo.setTrnfTextCont("testTrnfTextCont"); prGoodsContInfo.setSysRegId("FRONT"); prGoodsContInfo.setSysModId("FRONT"); sampleTrxMapper.insertPrGoodsContInfo(prGoodsContInfo); return prGoodsContInfo; } // 동영상변환상태정보 등록 private void registerMpicMappInfo(PrGoodsContInfo prGoodsContInfo, MpicResponse mpicResponse) { StMpicMappInfo stMpicMappInfo = new StMpicMappInfo(); // 원본경로명이 "/" 로 시작하도록 처리함. String s3OrigPathNm = StringUtils.startsWith(mpicResponse.getS3OrigPathNm(), "/") ? mpicResponse.getS3OrigPathNm() : "/" + mpicResponse.getS3OrigPathNm(); stMpicMappInfo.setOrgPathNm(s3OrigPathNm); stMpicMappInfo.setTblNm("pr_goods_cont_info"); stMpicMappInfo.setRef1Val(prGoodsContInfo.getGoodsNo()); stMpicMappInfo.setRef2Val(prGoodsContInfo.getCmtTypCd()); stMpicMappInfo.setRef3Val(prGoodsContInfo.getCmtSerialNo()); stMpicMappInfo.setSysRegId("SAMPLE"); stMpicMappInfo.setSysModId("SAMPLE"); mpicService.registerMpicMappInfo(stMpicMappInfo); } } |
6) Mapper 클래스 및 Query 작성
src/main/java 하위 해당 업무 폴더에 repository/{db연결명} 폴더 생성 후 아래와 같이 작업 진행
SampleMapper.java
package com.x2bee.api.bo.app.repository.displayrodb.sample; import java.util.List; import java.util.Optional; import com.x2bee.api.bo.app.dto.request.sample.SampleRequest; import com.x2bee.api.bo.app.dto.response.sample.SampleResponse; public interface SampleMapper { public List<SampleResponse> selectAllSamples(); public Optional<SampleResponse> selectSampleById(Long id); public List<SampleResponse> selectSamples(SampleRequest request); } |
src/main/resource 하위에 해당 쿼리 작성
namespace 에 위 Mapper 클래스 명시
resultType 혹은 parameterType 에 데이터 객체 명시
SampleMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.x2bee.api.bo.app.repository.displayrodb.sample.SampleMapper"> <sql id="sampleList"> SELECT 1 as id, 'name1' as name, 'desc1' as description union all SELECT 2 as id, 'name2' as name, 'desc2' as description union all SELECT 3 as id, 'name3' as name, 'desc3' as description </sql> <!-- 전체 샘플 조회 --> <select id="selectAllSamples" resultType="sampleResponse"> /* SampleMapper.selectAllSamples */ <include refid="sampleList" /> </select> <!-- 샘플 단건 조회 --> <select id="selectSampleById" parameterType="long" resultType="sampleResponse"> /* SampleMapper.selectSampleById */ select * from ( <include refid="sampleList" /> ) a where id = #{id} </select> <!-- 샘플 목록 조회 --> <select id="selectSamples" parameterType="sampleRequest" resultType="sampleResponse"> /* SampleMapper.selectSamples */ select * from ( <include refid="sampleList" /> ) a <where> <if test="id != null"> and id = #{id} </if> <if test="name != null and name != ''"> and name = #{name} </if> <if test="description != null and description != ''"> and description = #{description} </if> </where> </select> </mapper> |
위 작업 진행 시 최종 패키지 구조는 다음과 같음
8. 유효성 체크 방법
1) Bean Validation
Alias 어노테이션으로 별칭 지정
데이터 통신을 VO 로 하므로, 다음과 같이 VO 객체에 javax.validation.constrains 패키지에서 제공하는 어노테이션을 이용
Group.java
package com.x2bee.api.bo.app.entity; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import org.apache.ibatis.type.Alias; import com.x2bee.common.base.entity.AbstractEntity; import lombok.Getter; import lombok.Setter; @Alias("group") @Getter @Setter public class Group extends AbstractEntity { @NotNull String groupNo; @NotEmpty String groupName; } |
1-1) @Valid 를 이용한 @RequestBody 에 대한 유효성 검증
SampleController.java
public class SampleController{ ... @PostMapping("") public Response<String> registerSample(@RequestBody @Valid Sample sample) throws InterruptedException { ... } ... } |
1-2) @Validated 를 이용한 @PathVariable과 @RequestParam 에 대한 유효성 검증
SampleController.java
@RestController @Validated public class SampleController{ @GetMapping("/users/{email}") public String getUserInfoByEmail(@PathVariable("email") @Email String email) { .... } } |
2) Custom Validator
javax.validation.constrains 패키지에서 제공되지 않는 validator 를 구현하고자 하는 경우, 본 방법으로 진행
2-1) Custom annotation 생성
LocaleConstraint.java
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = LocaleValidator.class) @Documented public @interface LocaleConstraint { String message() default "Invalid Locale"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
2-2) Custom annotation 을 처리할 Custom Validator 생성
LocaleValidator.java
public class LocaleValidator implements ConstraintValidator<LocaleConstraint, String>{ public static final List<String> locales= Arrays.asList("ko_KR", "en_US"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && locales.contains(value.toLowerCase()) ; } } |
2-3) Custom annotation @LocaleConstraint을 적용하여 유효성 검증
Category.java
package com.plateer.x2co.api.prototype.entity; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import io.swagger.annotations.ApiParam; import lombok.Data; @Data public class Category { @NotEmpty private String catTpCd; @NotBlank private String siteNo; @NotEmpty private String dpmlNo; @NotEmpty @LocaleConstraint // 다음과 같이 어노테이션으로 적용 private String dbLocaleLanguage; @NotEmpty private int maxLvl = 0; @NotEmpty private int minLvl = 0; } |
9. 에러메시지 및 예외 처리
1) 에러 메시지 처리
X2BEE 1.0 에서 사용할 에러처리 포멧'
GlobalErrorController.java
package com.x2bee.bo.base.advice; import java.util.Map; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import com.x2bee.bo.base.exception.AuthException; import com.x2bee.common.base.exception.MessageResolver; import com.x2bee.common.base.exception.ValidationException; import com.x2bee.common.base.rest.ErrorResponse; import lombok.extern.slf4j.Slf4j; @Controller //@RequestMapping("${server.error.path:${error.path:/error}}") // 1) @RequestMapping("/error") // 1) @Slf4j public class GlobalErrorController extends AbstractErrorController { public static final String EXCEPTION_KEY = "_ExceptioN_KEY_"; public static final String COMMON_ERROR_PAGE = "common/error"; public GlobalErrorController(ErrorAttributes errorAttributes) { super(errorAttributes); } @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 2) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); Map<String, Object> model = getErrorAttributes(request, options); String errorPage = "error/error"; Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception"); if (exception != null) { Throwable throwable = exception.getCause(); if (throwable instanceof AuthException) { errorPage = "error/403"; } else if (throwable instanceof ValidationException) { errorPage = "error/404"; } else { errorPage = "error/500"; } } else { errorPage = "error/500"; } ModelAndView modelAndView = new ModelAndView(errorPage, model); modelAndView.addObject(EXCEPTION_KEY, exception.getCause()); return modelAndView; } protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) { ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); options = options.including(Include.MESSAGE); options = options.including(Include.BINDING_ERRORS); return options; } @RequestMapping public ResponseEntity<ErrorResponse> error(HttpServletRequest request) { Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); log.error("status_code: {}", request.getAttribute("javax.servlet.error.status_code")); log.error("exception_type: {}", request.getAttribute("javax.servlet.error.exception_type")); log.error("message: {}", request.getAttribute("javax.servlet.error.message")); log.error("request_uri: {}", request.getAttribute("javax.servlet.error.request_uri")); log.error("exception: {}", request.getAttribute("javax.servlet.error.exception")); Exception exception = (Exception)request.getAttribute("javax.servlet.error.exception"); if (exception != null) { Throwable throwable = exception.getCause(); if (throwable instanceof AuthException) { return new ResponseEntity<ErrorResponse>( new ErrorResponse("0403", ((AuthException)throwable).getMessage()), new HttpHeaders(), HttpStatus.FORBIDDEN); } else { return new ResponseEntity<ErrorResponse>( new ErrorResponse("9000", MessageResolver.getMessage("adminCommon.system.error")), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR); } } else { return new ResponseEntity<ErrorResponse>( new ErrorResponse("9000", MessageResolver.getMessage("adminCommon.system.error")), new HttpHeaders(), HttpStatus.valueOf(Integer.valueOf(String.valueOf(status)))); } } } |
API 오류 처리
Response 객체에 Error 내용을 담아 Return
ErrorResponse.java
package com.x2bee.common.base.rest; import java.time.LocalDateTime; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import lombok.experimental.Accessors; @Getter @Setter @ToString @Accessors(chain = true) @NoArgsConstructor public class ErrorResponse { @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime timestamp = LocalDateTime.now(); private String code; private String message; private final Object payload = null; public ErrorResponse(String code, String message) { this.code = code; this.message = message; } } |
Error Code 정의
package com.x2bee.api.bo.base.advice; import com.x2bee.common.base.exception.AppError; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum ApiError implements AppError { // success SUCCESS("0000", "common.message.success"), // app error EMPTY_PARAMETER("1001", "common.error.emptyParameter"), INVALID_PARAMETER("1002", "common.error.invalidParameter"), DATA_NOT_FOUND("1003", "common.error.dataNotFound"), DUPLICATE_DATA("1004", "common.error.duplicateData"), UPLOAD_FAIL("1100", "common.error.uploadFail"), //ERP ERP_ERROR_MESSAGE("2000", "common.erp.errorMessage"), ERP_INTRERFACE_ERROR("2001", "common.erp.interfaceError"), // 권한 없음. NOT_AUTHORIZED("7000", "common.error.notAuthorized"), // binding error BINDING_ERROR("8000", "common.error.bindingError"), BINDING_ERROR_NOT_NULL("8001", "common.error.bindingErrorNotNull"), // unknow error UNKNOWN("9000", "common.error.unknown"), // ValidatioException error VALIDATION_EXCEPTION("9100", "common.error.unknown"); private final String code; private final String messageKey; } |
2) 예외 처리 기능 (x2bee-common 패키지 하위 Exception 클래스 구현)
src/main/java 아래에 패키지 생성 후 Exception 코드 작성
예시1
CommonException.java
package com.x2bee.common.base.exception; import lombok.Getter; import lombok.Setter; @Getter @Setter public class CommonException extends UserDefinedException { private static final long serialVersionUID = 1L; private String errorCode; private String errorMessage; public CommonException(String message, Throwable cause) { super(message, cause); } public CommonException(String message) { super(message); } public CommonException(Throwable cause) { super(cause); } public CommonException(String errorCode, String errorMessage) { super(errorMessage); this.errorCode = errorCode; this.errorMessage = errorMessage; } } |
예시2
ValidationException.java
package com.x2bee.common.base.exception; @SuppressWarnings("serial") public class ValidationException extends UserDefinedException { public ValidationException() { super(); } public ValidationException(String message) { super(message); } public ValidationException(Throwable t) { super(t); } public ValidationException(String message, Throwable t) { super(message, t); } } |
10. 트랜잭션 처리
등록/수정/삭제 서비스 메소드에 자동으로 트랜잭션이 동작하도록 AOP 가 설정되어있음
register/modify/delete/save 를 메소드 이름에 포함시켜야 동작하므로 메소드 명칭에 오타가 없도록 유의
registerSampleMulti.java
... public void registerSampleMulti(Cateogry category, Cateogry subCategory) throws Exception { categoryMapper.insert(category); categoryMapper.insert(subCategory); } ... |
DisplayDbTranscationAspect.java
@Aspect @Configuration public class DisplayDbTranscationAspect { @Resource(name="displayRwdbTxManager") private TransactionManager displayRwdbTxManager; private static final String EXPRESSION = " execution(* com.x2bee.api.bo.app.service.code.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.common.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.dashboard.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.display.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.goods.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.inf.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.main.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.popup.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.sample.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.statistics.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.system.*ServiceImpl.*(..)) " + " || execution(* com.x2bee.api.bo.app.service.vendor.*ServiceImpl.*(..)) " ; @Bean public TransactionInterceptor displayDbTransactionAdvice() { List<RollbackRuleAttribute> rollbackRules = Collections.singletonList(new RollbackRuleAttribute(Exception.class)); RuleBasedTransactionAttribute transactionAttribute = new RuleBasedTransactionAttribute(); transactionAttribute.setRollbackRules(rollbackRules); transactionAttribute.setName("*"); MatchAlwaysTransactionAttributeSource attributeSource = new MatchAlwaysTransactionAttributeSource(); attributeSource.setTransactionAttribute(transactionAttribute); return new TransactionInterceptor(displayRwdbTxManager, attributeSource); } @Bean public Advisor displayDbTransactionAdvisor() { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(EXPRESSION); return new DefaultPointcutAdvisor(pointcut, displayDbTransactionAdvice()); } } |