정보 |
---|
개발 가이드 |
목차 | ||||||
---|---|---|---|---|---|---|
|
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());
}
}
|
...
X2BEE 개발에 참여하신 여러분을 위한 개발 시작 가이드입니다. 개발 환경 설정을 시작으로 개발 업무에 필요한 중요한 정보들을 설명하고 있습니다. 이 문서는 프로젝트 개발을 진행하면서 일관성 있는 코드와 높은 품질을 유지하기 위해 다음과 같은 내용을 담고 있습니다.
...
이 가이드 문서는
개발 환경 개요
이 섹션에서는 프로젝트 개발에 필요한 개발 환경에 대한 개요를 제공합니다. 로컬 개발 환경 설정 방법 및 공통 기능 구현과 설정에 대한 정보를 찾을 수 있습니다.
공통 기능 구현 및 설정
이 부분에서는 개발 환경 설정 이후에 공통 기능 구현과 관련된 다양한 주제를 다룹니다. 파일 업로드, XSS 방지 처리, HTTP 인터페이스 구현, 데이터 마스킹 처리 및 API(Controller) 권한 설정과 같은 주요 주제들이 포함됩니다.
데이터베이스 활용 및 포맷
데이터베이스는 많은 프로젝트에서 중요한 부분입니다. 이 섹션에서는 데이터베이스 활용, 트랜잭션 처리, 데이터 유효성 검사 및 날짜 변환과 같은 주요 데이터베이스 주제에 대해 다룹니다.
메시지 처리 및 응답
섹션에서는 에러 메시지 및 예외 처리, 응답 값(Response)의 공통 처리, 그리고 프론트 엔드와 백 엔드 간의 메시지 처리에 관한 내용을 다룹니다.
보안 및 통신
보안과 통신은 모든 프로젝트에서 중요한 부분입니다. 이 섹션에서는 로깅 설정, 클라이언트 및 서버 간 통신 데이터 처리, 서버 측 개발 방법 및 데이터 암호화와 같은 주제를 다룹니다.
고객 메시지 전송
이 부분은 고객 메시지 전송과 관련된 내용을 다룹니다. 문자 메시지, 알림톡 메시지 및 이메일 메시지의 전송 방법과 관련 정보를 제공합니다.
/wiki/spaces/TG/pages/112132097
검색 및 인덱싱은 데이터 처리와 관련된 중요한 측면입니다. 이 섹션에서는 검색 엔진 가이드와 메시지 처리 및 응답에 대한 정보를 제공합니다.
검색 및 기타 환경 설정
검색 엔진 사용 방법 및 Swagger 설정에 대해서 설명합니다.
참고사항 |
---|
각 문서의 상세 내용은 X2BEE의 기술적인 세부 정보를 포함하고 있으며, 프로젝트 개발을 지원하기 위한 자세한 내용을 다루고 있습니다. 이 가이드의 전체 내용을 확인하기 위해서 로그인해야 하며, 해당 자료는 파트너 및 개발자를 위해 제공되고 있습니다. 로그인하지 않은 사용자에게는 콘텐츠 및 목록이 노출 되지 않습니다. 문의사항이나 로그인 지원이 필요한 경우 파트너가 되는 방법을 참고해주세요. |
가이드 목록
페이지 트리 | ||||
---|---|---|---|---|
|