개발 가이드
X2BEE 개발 환경을 제공하기 위한 자료입니다.
1. 사용 라이브러리 정보
명칭 | 버전 | 용도 |
---|---|---|
JRE System Library | 1.8 | 웹 어플리케이션 구동에 필요한 자바 런타임 라이브러리 |
Spring Boot Framework | 5.3.4 | 서버 로직 전반을 담당하는 프레임워크 |
Gradle | 6.6 | 모듈의 의존성 관리를 위한 라이브러리 |
Mybatis-spring-boot-starter | 2.1.3 | 데이터베이스 연결 및 쿼리 처리를 위한 라이브러리 |
Logback | 로깅을 위한 라이브러리 |
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 파일 내 datasource 관련 설정 추가
아래와 같이 application.yml 파일에서 데이터베이스 관련 설정 파일 위치 명시
.... x2config: location: datasource: config/dev/properties/datasource.properties site: config/dev/properties/site.properties ....
2) datasource.properties 파일 작성
src/main/resource/{}/properties 폴더 하위에 datasource.properties 파일 작성
prefix 값 설정 필요 (아래 예제에서는 x2base)
파일에 추가될 데이터베이스 관련 설정 정보
프로퍼티명
설명
url데이터베이스 접속 URLusername데이터베이스 사용자 아이디password데이터베이스 사용자 비밀번호driveClassName데이터베이스 드라이버 클래스 명
x2base.url=jdbc:log4jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO x2base.username=X2BASE x2base.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR) x2base.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
3) DefaultDatasourceProperties.java 파일 작성
앞서 작성한 datasource.properties 파일 내 프로퍼티 값을 Java 파일에서 처리 가능하도록 다음과 같이 작업
@ConfigurationProperties 어노테이션을 이용하여 앞서 설정한 prefix 값으로 세팅하여 위의 설정 파일과 매핑
package com.plateer.base.config.properties; import javax.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.validation.annotation.Validated; import lombok.Getter; import lombok.ToString; /* location : /config/${spring.active.profile}/datasource.properties */ @ConfigurationProperties(prefix = "x2base") @ConstructorBinding @Validated @Getter @ToString public class DefaultDatasourceProperties { @NotNull private final String username; @NotNull private final String password; @NotNull private final String url; @NotNull private final String driverClassName; public DefaultDatasourceProperties(String username, String password, String url, String driverClassName) { this.username = username; this.password = password; this.url = url; this.driverClassName = driverClassName; } }
4) 데이터베이스 관련 빈 설정 및 등록
앞서 설정한 파일 내 정보를 이용하여 SqlSessionFactory 및 SqlSessionTemplate 설정
@MapperScan 어노테이션으로 매퍼 파일 위치 및 SqlSessinoFactory 클래스 지정
트랜잭션 처리를 위하여 @EnableTransactionManagement 어노테이션 추가
package com.plateer.base.config.mybatis; 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.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.annotation.EnableTransactionManagement; import com.plateer.base.config.properties.DefaultDatasourceProperties; import com.zaxxer.hikari.HikariDataSource; @Configuration @MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "sqlSessionFactory") @EnableTransactionManagement public class DefaultDatasourceConfig { private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml"; private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml"; @Autowired private ApplicationContext applicationContext; @Primary @Bean(name = "defaultDatasource") public DataSource defaultDatasource(DefaultDatasourceProperties datasourceProperties) { return DataSourceBuilder.create() .type(HikariDataSource.class) .driverClassName(datasourceProperties.getDriverClassName()) .url(datasourceProperties.getUrl()) .username(datasourceProperties.getUsername()) .password(datasourceProperties.getPassword()) .build(); } @Primary @Bean(name = "sqlSessionFactory") public SqlSessionFactory sqlSessionFactory(@Qualifier("defaultDatasource") DataSource defaultDatasource, ApplicationContext context) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(defaultDatasource); sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG)); sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES)); return sqlSessionFactoryBean.getObject(); } @Primary @Bean(name = "sqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } @Primary @Bean(name="transactionManager") public DataSourceTransactionManager transactionManager(@Autowired @Qualifier("defaultDatasource") DataSource defaultDatasource) { return new DataSourceTransactionManager(defaultDatasource); } }
4. 다중 Datasource 연결
1) 데이터베이스 관련 정보 설정
datasource.properties 파일 내 DB 접속 정보 각각 설정
spring.primary.datasource.jdbc-url=jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO spring.primary.datasource.username=X2BASE spring.primary.datasource.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR) spring.secondary.datasource.jdbc-url=jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO spring.secondary.datasource.username=X2BASE spring.secondary.datasource.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR)
2) Datasource 설정
다중 Datasource 설정 시, 각각 Datasource 클래스 생성
@ConfigurationProperties 어노테이션으로 datasource.properties 내 DB 접속 정보 로드
title = primaryDatasourece.java package com.plateer.base.config.mybatis; 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.annotation.EnableTransactionManagement; @Configuration @MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "sqlSessionFactory") // Mapper 파일 스캔을 위한 패키지 기입 @EnableTransactionManagement public class PrimaryDatasource { private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml"; // Mapper 파일 스캔을 위한 경로 정보 기입 private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml"; // mybatis 관련 설정 파일 경로 정보 기입 @Primary @Bean(name="dataSource") @ConfigurationProperties(prefix="spring.primary.datasource") public DataSource dataSource() { return DataSourceBuilder.create().build(); } @Primary @Bean(name="sqlSessionFactory") public SqlSessionFactory sqlSessionFactoryBean(@Autowired @Qualifier("dataSource") DataSource dataSource, ApplicationContext applicationContext) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG)); sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES)); return sqlSessionFactoryBean.getObject(); } @Primary @Bean(name="sqlSession") public SqlSessionTemplate sqlSession(@Autowired @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } @Primary @Bean(name="transactionManager") public DataSourceTransactionManager transactionManager(@Autowired @Qualifier("dataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
package com.plateer.base.config.mybatis; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSession; 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.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "secondarySqlSessionFactory") @EnableTransactionManagement public class SecondaryDatasource { private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml"; private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml"; @Bean(name="secondaryDataSource") @ConfigurationProperties(prefix="spring.secondary.datasource") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name="secondarySqlSessionFactory") public SqlSessionFactory secondarySqlSessionFactoryBean(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource, ApplicationContext applicationContext) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(secondaryDataSource); sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG)); sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES)); return sqlSessionFactoryBean.getObject(); } @Bean(name="secondarySqlSession") public SqlSession secondarySqlSession(@Autowired @Qualifier("secondarySqlSessionFactory") SqlSessionFactory secondarySqlSessionFactory) { return new SqlSessionTemplate(secondarySqlSessionFactory); } @Bean(name="secondaryTransactionManager") public DataSourceTransactionManager secondaryTransactionManager(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource) { return new DataSourceTransactionManager(secondaryDataSource); } }
package com.plateer.base.config.mybatis; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSession; 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.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @MapperScan(basePackages = "com.plateer.x2co.**.dao.**", sqlSessionFactoryRef = "secondarySqlSessionFactory") @EnableTransactionManagement public class SecondaryDatasource { private final String CLASSPATH_RESOURCES="classpath:mybatis/mapper/**/*.xml"; private final String CLASSPATH_MYBATIS_CONFIG="classpath:config/mybatis-config.xml"; @Bean(name="secondaryDataSource") @ConfigurationProperties(prefix="spring.secondary.datasource") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name="secondarySqlSessionFactory") public SqlSessionFactory secondarySqlSessionFactoryBean(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource, ApplicationContext applicationContext) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(secondaryDataSource); sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource(CLASSPATH_MYBATIS_CONFIG)); sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(CLASSPATH_RESOURCES)); return sqlSessionFactoryBean.getObject(); } @Bean(name="secondarySqlSession") public SqlSession secondarySqlSession(@Autowired @Qualifier("secondarySqlSessionFactory") SqlSessionFactory secondarySqlSessionFactory) { return new SqlSessionTemplate(secondarySqlSessionFactory); } @Bean(name="secondaryTransactionManager") public DataSourceTransactionManager secondaryTransactionManager(@Autowired @Qualifier("secondaryDataSource") DataSource secondaryDataSource) { return new DataSourceTransactionManager(secondaryDataSource); } } |
3) 사용 예
.... public class SampleServiceImpl{ @Autowired @Qualifier("secondarySqlSession") // 사용하고자 하는 datasource 지정 (Primary 는 지정하지 않아도 됨) private SqlSession secondarySqlSession; .... } .... |
5. 로깅 설정
1) 로깅 관련 프로퍼티 설정
로깅 파일 저장 위치 설정
로깅 레벨 설정
라이트 로깅 여부 설정 (최소한의 정보만 로깅)
로그에 컨트롤러 파라미터를 포함할지 여부 설정
로그에 서비스 파라미터를 포함할지 여부 설정
logging.destination=C://log logging.level=INFO logging.lightLogging=false logging.includeControllerParameter=true logging.includeServiceParameter=false |
2) logback 관련 설정
src/main/resources/{} 폴더 하위 loback.xml 파일 설정
consoleAppender: 시스템 콘솔에 찍히는 로그 정보
fileAppender: 파일에 쓰여질 로그 정보
asyncConsoleAppender: 로깅 작업이 성능에 많은 영향을 주는 경우, 비동기로 설정하여 로깅 부하를 최소화 (콘솔 로깅 비동기 처리 설정)
asyncFileAppender: 로깅 작업이 성능에 많은 영향을 주는 경우, 비동기로 설정하여 로깅 부하를 최소화 (파일 로깅 비동기 처리 설정)
<?xml version="1.0" encoding="UTF-8" ?> <configuration scan="true" scanPeriod="30 seconds"> <springProperty scope="context" name="activeProfile" source="spring.profiles.active"/> <property resource="config/dev/properties/logging.properties"/> <property name="loggingPattern" value="%d{HH:mm:ss.SSS} %-5level [%thread] %replace([RID:%X{rid}]){'(\\[RID:\\])', ''} %replace([SID:%X{sid}]){'(\\[SID:\\])', ''} %replace([USER:%X{user}]){'(\\[USER:\\])', ''} %logger{36} - %msg[END]%n" /> <!-- console log appender --> <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>${loggingPattern}</Pattern> </layout> </appender> <!-- log file appender --> <appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${logging.destination}/debug.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- Daily Rollover --> <fileNamePattern>${logging.destination}/debug.%d{yyyy-MM-dd}_[%i].log</fileNamePattern> <!-- File Size 300Mb --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>300MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!-- Keep 30 days --> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>${loggingPattern}</pattern> </encoder> </appender> <!-- 비동기로 console에 추가 로깅 --> <appender name="asyncConsoleAppender" class="ch.qos.logback.classic.AsyncAppender"> <discardingThreshold>0</discardingThreshold> <queueSize>500</queueSize> <appender-ref ref="consoleAppender" /> </appender> <!-- 비동기로 file에 추가 로깅 --> <appender name="asyncFileAppender" class="ch.qos.logback.classic.AsyncAppender"> <discardingThreshold>0</discardingThreshold> <queueSize>500</queueSize> <appender-ref ref="fileAppender" /> </appender> <logger name="jdbc" level="OFF"/> <logger name="jdbc.sqlonly" level="OFF"/> <logger name="jdbc.sqltiming" level="DEBUG"/> <logger name="jdbc.audit" level="OFF"/> <logger name="jdbc.resultset" level="OFF"/> <logger name="jdbc.resultsettable" level="OFF"/> <logger name="jdbc.connection" level="OFF"/> <root level="${logging.level}"> <appender-ref ref="consoleAppender" /> <appender-ref ref="fileAppender" /> <!-- 비동기 logging --> <!-- <appender-ref ref="asyncConsoleAppender" /> <appender-ref ref="asyncFileAppender" /> --> </root> </configuration> |
3) 로깅 추가 정보 설정
로깅에 추가로 보여주고자 하는 정보 설정 (src/main/java/com/plateer/base/log 폴더 하위 ExecLoggingFilter.java 파일 수정)
아래 가이드는 RequestID 와 SessionID 를 설정하는 예제
package com.plateer.base.filter; import java.io.IOException; import java.util.UUID; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.slf4j.MDC; import org.slf4j.MDC.MDCCloseable; import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; @Slf4j @Component public class ExecLoggingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { try { HttpServletRequest request = (HttpServletRequest) req; // request id setting MDC.put("rid", getRequestId(request)); // session id setting MDC.put("sid", getSessionId(request)); chain.doFilter(req, res); } finally { // mdc remove MDC.clear(); } } private String getRequestId(HttpServletRequest request) { return request.getRequestURI() + "-" + UUID.randomUUID().toString(); } private String getSessionId(HttpServletRequest request) { String sessionId = request.getRequestedSessionId(); return sessionId != null ? request.getRequestedSessionId() : "?????"; } } |
Request 시 ReqeustID, SessionID 를 생성하고 제거하며, MDC 를 활용하여 로그 출력
6. 쿼리 로깅 설정
log4jdbc 를 이용한 쿼리 로깅 방법
1) log4jdbc 설정 파일 추가
resource 폴더 하위 아래 파일 추가
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator log4jdbc.dump.sql.maxlinelength=0 |
2) 데이터베이스 연결 정보 수정
driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy 추가
url 수정 (log4jdbc 추가)
x2base.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy x2base.url=jdbc:log4jdbc:oracle:thin:@218.38.15.94:1521:ORAX2CO x2base.username=X2BASE x2base.password=ENC(cDiQFk2pHBC7aep3NKNDl4QteXJLwa5XOHfkm4oR+vEdo9SBz52xelxEio3snTwR) |
3) logback.xml 파일 수정
아래 설정 내용 추가
<!-- log4jdbc 옵션 설정 --> <logger name="jdbc" level="OFF"/> <!-- 커넥션 open close 이벤트 로그로 남김 --> <logger name="jdbc.connection" level="OFF"/> <!-- SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여짐 --> <logger name="jdbc.sqlonly" level="OFF"/> <!-- SQL문과 해당 SQL을 실행시키는데 수행된 시간 정보(milliseconds)를 포함 --> <logger name="jdbc.sqltiming" level="DEBUG"/> <!-- ResultSet을 제외한 모든 JDBC 호출 정보를 로그로 남김. 방대한 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않음--> <logger name="jdbc.audit" level="OFF"/> <!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 방대한 양의 로그가 생성됨 --> <logger name="jdbc.resultset" level="OFF"/> <!-- SQL 결과 조회된 데이터의 table을 로그로 남김 --> <logger name="jdbc.resultsettable" level="DEBUG"/> |
7. 클라이언트, 서버 간 통신 데이터 형식
Map 형태가 아닌 VO(Value Object) 로 데이터 통신하는 것을 기본으로 함
아래 사용자 정보 처리 관련 VO 예제 참고
package com.plateer.x2co.common.entity; import java.util.Date; import lombok.Data; @Data public class User { private String usrId; private String usrNm; private String usrGrp; private String useYn; private String mobileNo; private String phoneNo; private String email; private String pw; private Date pwModDt; private String mailReceivedYn; private String smsReceivedYn; private String pwMissCnt; private String pwInitYn; private Date lastLoginDt; private String lastLoginIp; private String accExpireYn; private String accLockYn; private String pwModRequireYn; } |
8. 서버 측 개발 방식
Controller, Service, Mapper 구조
서버는 Spring MVC 패턴 기반이므로 데이터 처리를 위하여 다음과 같이 Controller, Service, Mapper 구조로 진행됨
http request → (mapping) → Controller → Service → ServiceImpl → Mapper → query 호출 순서로 진행됨
1) VO 클래스 작성
src/main/java 하위 해당 업무 폴더에 entity 폴더 생성 후 아래와 같이 작업 진행
데이터 전달을 위하여 사용될 객체 사전 정의
package com.plateer.x2co.backoffice.sample.entity; import lombok.Data; @Data public class SampleVO{ String itemNo; String salePrc; String mbrNo; .... } |
2) Controller 클래스 작성
src/main/java 하위 해당 업무 폴더에 Controller 폴더 생성 후 아래와 같이 작업 진행
@RestController 어노테이션으로 Controller 임을 명시
@RequestMapping 어노테이션으로 해당 Controller 전체에 대한 Http Request 매핑 정보 명시
@Autowired 어노테이션으로 service 인터페이스 주입
package com.plateer.x2co.backoffice.sample.controller; @RestController @RequestMapping("/rest") public class SampleController { @Autowired private SampleService sampleService ; @PostMapping(value="/categories", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ReponseEntity<List<SampleVO>> getPrDispCatList(@RequestBody String id) { List<SampleVO> listMap = prototypeServcie.getPrDispCatList(id); return sampleService.getPrDispCatList(id); } } |
3) Service 클래스 작성
src/main/java 하위 해당 업무 폴더에 Service 폴더 생성 후 아래와 같이 작업 진행
구현하고자 하는 업무 로직의 인터페이스 작성
package com.plateer.x2co.backoffice.sample.service; public interface SampleService { public ReponseEntity<List<SampleVO>> getPrDispCatList(String id); } |
위 인터페이스의 실제 구현체 작성
@Service 어노테이션으로 서비스 클래스 임을 명시
@Autowired 어노테이션으로 관련 Mapper 클래스 주입
package com.plateer.x2co.backoffice.sample.service; @Service public class SampleServiceImpl implements SampleService { @Autowired private SampleMapper sampleMapper ; @Override public ReponseEntity<List<SampleVO>> getPrDispCatList(String id) { return ResponseEntity.ok(sampleMapper.getPrDispCatList(id)); } } |
4) Mapper 클래스 및 Query 작성
src/main/java 하위 해당 업무 폴더에 dao 폴더 생성 후 아래와 같이 작업 진행
@Mapper 어노테이션을 이용하여 Mapper 임을 명시
package com.plateer.x2co.backoffice.sample.dao; @Mapper public interface SampleMapper { public List<SampleVO> getPrDispCatList(String id); } |
src/main/resource 하위에 해당 쿼리 작성
namespace 에 위 Mapper 클래스 명시
resultType 혹은 parameterType 에 데이터 객체 명시
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.plateer.x2co.api.backoffice.sample.dao.SampleMapper"> <!-- 카테고리 계층 목록 조회 --> <select id="getPrDispCatList" resultType="com.plateer.x2co.backoffice.sample.entity.SampleVO"> /* prDispCatBase.getPrDispCatList */ SELECT .... </select> |
위 작업 진행 시 최종 패키지 구조는 다음과 같음
9. 유효성 체크 방법
1) Bean Validation
데이터 통신을 VO 로 하므로, 다음과 같이 VO 객체에 javax.validation.constrains 패키지에서 제공하는 어노테이션을 이용
package com.plateer.x2co.api.prototype.entity; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import io.swagger.annotations.ApiParam; import lombok.Data; @Data public class Category { @NotEmpty private String catTpCd; @NotBlank private String siteNo; @NotEmpty private String dpmlNo; @NotEmpty private String dbLocaleLanguage; @NotEmpty private int maxLvl = 0; @NotEmpty private int minLvl = 0; } |
1-1) @Valid 를 이용한 @RequestBody 에 대한 유효성 검증
public class SampleController{ @PostMapping(value="categories") public ResponseEntity<?> getPrDispCatList(@Valid @RequestBody Category category) { .... } } |
1-2) @Validated 를 이용한 @PathVariable과 @RequestParam 에 대한 유효성 검증
@RestController @Validated public class SampleController{ @GetMapping("/users/{email}") public String getUserInfoByEmail(@PathVariable("email") @Email String email) { .... } } |
2) Custom Validator
javax.validation.constrains 패키지에서 제공되지 않는 validator 를 구현하고자 하는 경우, 본 방법으로 진행
2-1) Custom annotation 생성
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = LocaleValidator.class) @Documented public @interface LocaleConstraint { String message() default "Invalid Locale"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
2-2) Custom annotation 을 처리할 Custom Validator 생성
public class LocaleValidator implements ConstraintValidator<LocaleConstraint, String>{ public static final List<String> locales= Arrays.asList("ko_KR", "en_US"); @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && locales.contains(value.toLowerCase()) ; } } |
2-3) Custom annotation @LocaleConstraint을 적용하여 유효성 검증
package com.plateer.x2co.api.prototype.entity; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import io.swagger.annotations.ApiParam; import lombok.Data; @Data public class Category { @NotEmpty private String catTpCd; @NotBlank private String siteNo; @NotEmpty private String dpmlNo; @NotEmpty @LocaleConstraint // 다음과 같이 어노테이션으로 적용 private String dbLocaleLanguage; @NotEmpty private int maxLvl = 0; @NotEmpty private int minLvl = 0; } |
10. 에러메시지 및 예외 처리
1) 에러 메시지 처리
X2Commerce 4.0 에서 사용될 에러 메시지 포맷
package com.plateer.base.exception; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import org.springframework.http.HttpStatus; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; @Data public class X2ApiError { /* * status: the HTTP status code * LocalDateTime: the local date time * message: the error message associated with exception * error: List of constructed error messages */ private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss") private LocalDateTime timestamp; private String message; private List<String> errors; public X2ApiError(HttpStatus status, String message, List<String> errors) { super(); this.status = status; this.message = message; this.timestamp = LocalDateTime.now(); this.errors = errors; } public X2ApiError(HttpStatus status, String message, String error) { super(); this.status = status; this.message = message; this.timestamp = LocalDateTime.now(); errors = Arrays.asList(error); } } |
오류 발생 시 아래와 같이 응답 (HTTP Response code 는 헤더에 포함되어 있기 때문에 추가로 전송하지 않음)
{ "status": "METHOD_NOT_ALLOWED", "timestamp": "2020-09-17 11:20:42", "message": "Request method 'PUT' not supported", "errors": [ "PUT method is not supported for this request. Supported methods are POST " ] } |
2) 예외 처리 기능 (com.plateer.base.exception 패키지 하위 X2ExceptionController 클래스 구현)
ResponseEntityExceptionHandler 확장 구현
Spring 에서 기본적으로 제공해주는 ResponseEntityExceptionHandler 를 상속 받아 Exception Handler 구현
@ControllerAdvice 어노테이션을 이용하여 Exception 을 일괄 처리
package com.plateer.base.exception; import java.util.ArrayList; import java.util.List; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @ControllerAdvice public class X2ExceptionController extends ResponseEntityExceptionHandler{ // 400 @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { List<String> errors = new ArrayList<String>(); for (FieldError error : ex.getBindingResult().getFieldErrors()) { errors.add(error.getField() + ": " + error.getDefaultMessage()); } for (ObjectError error : ex.getBindingResult().getGlobalErrors()) { errors.add(error.getObjectName() + ": " + error.getDefaultMessage()); } X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors); return handleExceptionInternal(ex, x2ApiError, headers, x2ApiError.getStatus(), request); } // 400 @Override protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = ex.getParameterName() + " parameter is missing"; X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 400 @ExceptionHandler({ ConstraintViolationException.class }) public ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex, WebRequest request) { List<String> errors = new ArrayList<String>(); for (ConstraintViolation<?> violation : ex.getConstraintViolations()) { errors.add(violation.getRootBeanClass().getName() + " " + violation.getPropertyPath() + ": " + violation.getMessage()); } X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 400 @ExceptionHandler({ MethodArgumentTypeMismatchException.class }) public ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { String error = ex.getName() + " should be of type " + ex.getRequiredType().getName(); X2ApiError x2ApiError = new X2ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 401 @ExceptionHandler({ AuthenticationException.class }) public ResponseEntity<Object> authenticationException(AuthenticationException ex, WebRequest request) { X2ApiError x2ApiError = new X2ApiError(HttpStatus.UNAUTHORIZED, ex.getLocalizedMessage(), "error occurred"); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 403 @ExceptionHandler({ AccessDeniedException.class }) public ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException ex, WebRequest request) { X2ApiError x2ApiError = new X2ApiError(HttpStatus.FORBIDDEN, ex.getLocalizedMessage(), "error occurred"); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 404 @Override protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL(); X2ApiError x2ApiError = new X2ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 405 @Override protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { StringBuilder builder = new StringBuilder(); builder.append(ex.getMethod()); builder.append(" method is not supported for this request. Supported methods are "); ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " ")); X2ApiError x2ApiError = new X2ApiError(HttpStatus.METHOD_NOT_ALLOWED, ex.getLocalizedMessage(), builder.toString()); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 409 @ExceptionHandler({ DataIntegrityViolationException.class }) public ResponseEntity<Object> handleDataIntegrityViolationException(DataIntegrityViolationException ex, WebRequest request) { X2ApiError x2ApiError = new X2ApiError(HttpStatus.CONFLICT, ex.getLocalizedMessage(), "error occurred"); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 415 @Override protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { StringBuilder builder = new StringBuilder(); builder.append(ex.getContentType()); builder.append(" media type is not supported. Supported media types are "); ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", ")); X2ApiError x2ApiError = new X2ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2)); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } // 500, Default Handler @ExceptionHandler({ Exception.class }) public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) { X2ApiError x2ApiError = new X2ApiError(HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred"); return new ResponseEntity<Object>(x2ApiError, new HttpHeaders(), x2ApiError.getStatus()); } } |