Author: 김원철
Technologies: JTA, MyBatis, Spring MVC
Summary: Spring Framework와 MyBatis를 이용한 Rest Backend 웹서비스 어플리케이션.
Target Product: WAR
Product Versions: Java 1.8, Tomcat 9.0, Spring 4.3.14, MyBatis 3.4.5
- Java SDK 1.8 or later
- Maven 3.5 or later
- Jboss EAP or Wildfly
- GitLab에서 소스를 다운 받은 후 아래 순서로 진행
- 프로젝트 명에서 마우스 오른쪽 버튼 클릭한 후 Run As > Maven clean 실행한다. 빌드 전 target 폴더에 내용을 정리하기 위함이다.
- Maven clean 성공하면 프로젝트 명에서 마우스 오른쪽 버튼 클릭한 후 Run As > Maven install 실행한다.
pom.xml 파일에 설정한 library 파일들을 메이븐 repository 에 존재하지 않는 경우 메이븐 repository로 부터 내려받고 빌드하면 target 폴더에 war 파일이 생성된다. 또한 pom.xml 에 설정한 메이븐 repository 경로에 war 파일이 생성된다.
- 프로젝트 명에서 마우스 오른쪽 버튼 클릭한 후 Run As > Run on Server 실행하면 등록된 서버에서 선택한 어플리케이션이 실행된다.
- 프로젝트 명에서 마우스 오른쪽 버튼 클릭한 후 Debug As > Debug on server 실행한다.
- 디버깅 모드에서는 좀 더 자세한 로그와 정보가 콘솔창에 출력된다.
- 어플리케이션 전반에 걸쳐 적용할 Servlet과 Filter가 정의된 설정 파일의 경로를 지정한다.
- param-value: classpath:/config/applicationContext-*.xml
해당 경로에 applicationContext- 로 시작하면서 xml로 끝나는 모든 파일을 읽어온다.
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/config/applicationContext-*.xml</param-value>
</context-param>
- 스프링 컨테이너를 생성한 후 앞서 정의한 servles과 Filters를 컨테이너에 로딩한다.
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/dispatcher-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
- url-pattern
- Servlet이 실행되는 url 패턴을 설정한다.
/api/로 시작되는 url로 request가 들어올 경우 앞서 설정한 DispatcherServlet를 실행한다.
- Servlet이 실행되는 url 패턴을 설정한다.
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
-
src/main/resources/config 폴더에 applicationContext-datasource.xml 파일을 연다.
-
Jboss EAP 계열인 경우 WAS에 JTA Datasource가 정의되어 있으므로 jee:jndi-lookup 설정하는 부분에 jndi-name 설정을 동일하게 변경하면 된다.
- 만일 다중 Connection을 설정하고자 하면 기존 dataSource bean을 참조하여 DataSource bean을 추가한다. 이때 bean id와 JNDI 설정 시 기존 DataSource와 중복되지 않도록 적절하게 설정한다.
<!-- DataSource -->
<jee:jndi-lookup id="dataSource" jndi-name="java:/jdbc/sampleDS" />
- src/main/resources/config 폴더에 applicationContext-datasource.xml 파일을 연다.
- bean id="sqlSessionFactory"에 속성값을 필요 시 수정한다.
- dataSource: 앞서 설정한 DataSource bean을 입력한다.
- mapperLocations: SQL 구문이 존재하는 파일 경로를 설정한다. 경로는 프로젝트 명에 따라 폴더를 지정한다.
- configLocation: MyBatis 환결설정 파일경로를 설정한다.
<!-- myBatis -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/sql/sample/*.xml" />
<property name="configLocation" value="classpath:/sql/mybatis-config.xml" />
</bean>
- bean id="sqlSession" 에 속성값을 필요 시 수정한다.
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
- 다중 Connection 설정하고자 하면 각 dataSource 마다 sqlSessionFactory 및 sqlSession bean을 생성한다.
- resources/sql 폴더에 mybatis-config.xml 파일을 연다.
- settings: mybatis 초기 설정값을 지정한다.
- mapUnderscoreToCamelCase: 필드명에 언더바가 있는 경우 대문자로 전환하는 형태로 맴버변수를 지정하겠다는 뜻이다.
- logImpl: mybatis 로그를 LOG4J로 출력하겠다는 의미이다.
- typeAliases: 쿼리 결과 출력되는 resultType이 너무 긴 경우 type에 대한 alias를 설정할 수 있다.
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
<setting name="logImpl" value="LOG4J"/>
</settings>
<typeAliases>
<package name="com.rsm.sample.vo" />
<package name="java.lang" />
</typeAliases>
</configuration>
- Rest 서비스 호출 시 메시지 컨버터를 위한 클래스를 설정한다.
<!-- Enables the Spring MVC @Controller programming model -->
<mvc:annotation-driven>
<mvc:message-converters>
<beans:bean
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
</beans:bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- Handles HTTP GET requests for /resources/** by efficiently serving
up static resources in the ${webappRoot}/resources directory -->
<mvc:resources mapping="/resources/**" location="/resources/" />
- 기본적으로 모든 경로 및 origin에 대해 설정하였으나 필요 시 경로와 헤더를 제한할 수도 있다.
<!-- CORS -->
<mvc:cors>
<mvc:mapping path="/**/**" allowed-origins="*" />
</mvc:cors>
-
LanguageConfig
Header에 Language가 입력된 경우 Language코드를 읽어서 언어를 변환해 준다.
모든 경로에 대해 실행되도록 설정한다. -
LoginCheck
로그인 후 생성한 토큰의 유효성을 체크하여 유효하지 않은 경우 request 처리를 중단한다.
auth 경로를 제외한 경로에 대해 실행되도록 설정한다.
<!-- Interceptor -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**/**" />
<beans:bean class="com.rsm.common.interceptor.LanguageConfig"></beans:bean>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/emp/**" />
<mvc:exclude-mapping path="/auth/**" />
<beans:bean class="com.rsm.common.interceptor.LoginCheck"></beans:bean>
</mvc:interceptor>
</mvc:interceptors>
- JtaTransactionManager 빈을 설정
<!-- Configure the Spring framework to use JTA transactions -->
<bean id="JtaTransactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
</bean>
- 트랜젝션 Advice를 설정한다. get으로 시작하는 메서드는 read-only로 설정한다. (SELECT)
set으로 시작하는 메서드는 Exception 발생 시 Rollback 처리가 되도록 한다.
<!-- tx Advice -->
<tx:advice id="txAdvice" transaction-manager="JtaTransactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true" />
<tx:method name="set*" propagation="REQUIRED" rollback-for="Exception" />
</tx:attributes>
</tx:advice>
- 트랜젝션 AOP를 설정한다. service 클래스에 앞서 설정한 트랜젝션 Advice가 설정되도록 한다.
<!-- tx AOP -->
<aop:config>
<aop:pointcut id="txAdvisePointCut"
expression="execution(* com.rsm.sample.service.*Service.*(..))" />
<aop:advisor id="transactionAdvisor" pointcut-ref="txAdvisePointCut"
advice-ref="txAdvice" />
</aop:config>
- cache:annotation-driven: 어노테이션 방식으로 캐시를 설정한다.
- beans:set 뒤로 bean class="..." 구문을 복사붙이기 한 후 캐시하고자하는 메서드명을 name= 뒤에 입력한다.
<!-- Cache -->
<cache:annotation-driven cache-manager="cacheManager" />
<beans:bean id="cacheManager"
class="org.springframework.cache.support.SimpleCacheManager">
<beans:property name="caches">
<beans:set>
<beans:bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name="DepList" />
<beans:bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name="JobList" />
</beans:set>
</beans:property>
</beans:bean>
<context:component-scan base-package="com.rsm.sample" />
<context:component-scan base-package="com.rsm.common.util" />
<context:component-scan base-package="com.rsm.common.security" />
- task:annotation-driven: 어노테이션 방식으로 스캐줄러를 설정한다.
<!-- Scheduler -->
<task:annotation-driven />
- src/main/resources/config 폴더에 applicationContext-message.xml 파일을 연다.
- basenames: 언어 파일이 저장된 폴더 경로를 설정한다.
- defaultEncoding: 기본 인코딩 타입을 설정한다.
- cacheSeconds: 캐시 기간을 설정한다.
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:/i18n/message</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8" />
<property name="cacheSeconds" value="180" />
</bean>
- message.properties 파일을 설정한다. 설정 시 앞에 구분자를 설정하여 메시지 특성을 구분한다.
INFO_ : 정보성 메시지
ERR_ : 에러 메시지
WARN_ : 경고 메시지
- log4j.xml 어플리케이션 logger를 설정한다.
<!-- Application Loggers -->
<logger name="com.rsm.sample" additivity="false">
<level value="debug" />
<appender-ref ref="console" />
</logger>
- 패키지 이름은 모두 소문자로 작성한다.
com.rsm.sample.vo
- 클래스 이름은 대문자로 시작하는 카멜 표기법을 기반으로 한다.
EmpListVo
- 인스턴스 이름은 소문자로 시작하는 카멜 표기법을 기반으로 한다.
empListVo
- 변수 이름은 소문자로 시작하는 카멜 표기법을 기반으로 한다.
departmentId
- Package
- 패키지는 com.rsm.(시스템명).vo 로 설정한다.
- 시스템의 규모에 따라 상위 패키지를 시스템으로 하고 하위에 서브시스템을 서브 패키지로 작성한다.
- 패키지는 com.rsm.(시스템명).vo 로 설정한다.
com.rsm.sample.vo / com.rsm.sample.subsample.vo
- Class
- 클래스는 Serializable 인터페이스를 상속한다.
public class EmpListVo implements Serializable
- Property
- 맴버변수 접근제어자는 private으로 지정하고 DB 테이블 컬럼 Type에 따라 변수 Type을 지정한다.
- 맴버변수 이름은 소문자로 시작하는 카멜 표기법을 기반으로 하며, DB 테이블의 컬럼명에 언더바(_) 뒤에 있는 문자는 대문자로 표기한다.
- 맴버변수 접근제어자는 private으로 지정하고 DB 테이블 컬럼 Type에 따라 변수 Type을 지정한다.
Oracle DBMS Type과 맴버변수 Type 맵핑
- CHAR, VARCHAR2: String
- NUMBER: Long
- DATE: Date
private Long employeeId;
- Method
- 각 맴버변수에 대해 getter, setter를 작성한다. 작성 시 eclipse의 source 메뉴를 활용하면 쉽게 작성이 가능하다.
- toString 메서드를 오버라이딩한다. 작성 시 eclipse의 source 메뉴를 활용하면 쉽게 작성이 가능하다.
- 각 맴버변수에 대해 getter, setter를 작성한다. 작성 시 eclipse의 source 메뉴를 활용하면 쉽게 작성이 가능하다.
- Package
- 인터페이스: com.rsm.(시스템명).dao
- 실행: com.rsm.(시스템명).dao.impl 로 설정한다.
시스템의 규모에 따라 상위 패키지를 시스템으로 하고 하위에 서브시스템을 서브 패키지로 작성한다.
- 인터페이스: com.rsm.(시스템명).dao
com.rsm.sample.dao / com.rsm.sample.dao.impl
- Class
- Annotation: .impl 클래스 상단에 @Repository annotation을 반드시 기입한다.
@Repository
public class SampleDaoImpl implements SampleDao
- Name space
- sql 구문을 파라메터로 입력하기 위하여 네임스페이스를 선언한다.
private static String namespace = "com.rsm.sample.sampleDao";
- Dependency Injection
- sql 구문을 가져오기 위해 sqlSessionTemplate을 의존성 주입한다.
@Autowired
private SqlSessionTemplate sqlSession;
- Select Method
- DB에서 여러 행을 조회하는 메서드인 경우 List<> 클래스 타입으로 선언하고 get을 메서드 명 앞에 반드시 붙여준다.
- 앞서 의존성 주입한 sqlSessionTemplat의 selectList 메서드를 호출한다. 이때 실행하고자 하는 sql id를 파라메터로 입력한다.
- 메서드 실행 후 결과값을 리턴한다.
public List<EmpListVo> getEmpList(String departmentId) {
logger.debug("departmentId: " + departmentId);
return sqlSession.selectList(namespace + ".selectEmpList", departmentId);
}
- Insert, Update and Delete Method
- 데이터를 삽입, 수정, 삭제하는 메서드인 경우 int 타입으로 선언하고 반드시 Exception 클래스를 throws 하도록 한다.
- Exception 클래스를 throws 해야만 데이터 저장 중 DB에서 오류가 발생한 경우 Rollback이 된다.
- Insert: Ins / Update: set / Delete: del 을 메서드명 앞에 반드시 붙여준다.
- 앞서 의존성 주입한 sqlSessionTemplat의 insert/update/delete 메서드를 호출한다. 이때 실행하고자 하는 sql id를 파라메터로 입력한다.
- 메서드 실행 후 결과값을 리턴한다.
public int setEmp(EmpSaveVo empSaveDto) throws Exception {
logger.debug(empSaveDto.toString());
result = sqlSession.update(namespace + ".updateEmp", empSaveDto);
if (result < 1) {
throw new Exception();
}
return result;
}
- Package
- 인터페이스: com.rsm.(시스템명).service
- 실행: com.rsm.(시스템명).service.impl 로 설정한다.
시스템의 규모에 따라 상위 패키지를 시스템으로 하고 하위에 서브시스템을 서브 패키지로 작성한다.
com.rsm.sample.service / com.rsm.sample.service.impl
- Class
- Annotation: .impl 클래스 상단에 @Service annotation을 반드시 기입한다.
@Service
public class SampleServiceImpl implements SampleService
- Dependency Injection
- 해당 service와 관련된 dao 클래스를 의존성 주입한다.
@Autowired
private SampleDao dao;
- Select Method
- 데이터 조회하여 여러 행을 조회하는 메서드인 경우 List<> 클래스 타입으로 선언하고 get을 메서드 명 앞에 반드시 붙여준다.
- dao 인스턴스에서 select 관련 메서드를 실행하고 그 결과값을 리턴한다.
public List<EmpListVo> getEmpList(String departmentId) {
return dao.getEmpList(departmentId);
}
- Insert, Update, Delete Method
- 데이터를 삽입, 수정, 삭제하는 메서드인 경우 String 타입으로 선언한다.
- dao 인스턴스에서 데이터 저장 메서드 실행 시 try catch 구문을 사용한다.
- catch 구문에 Exception 발생 시 에러 메시지가 리턴되도록 한다.
- dao 인스턴스의 메서드 실행하여 정상적으로 데이터가 저장되면 저장 성공 메시지가 리턴되도록 한다.
public String setEmp(List<EmpSaveVo> vos) throws Exception {
try {
for (EmpSaveVo vo : vos) {
if (vo.get_status() == 2) {
dao.delEmp(vo.getEmployeeId());
} else if (vo.get_status() == 0) {
dao.insEmp(vo);
} else if (vo.get_status() == 1) {
dao.setEmp(vo);
}
}
} catch (Exception e) {
logger.error(e.getMessage());
throw new Exception(MessageProp.ERR_SAVE.getMsg());
}
return MessageProp.INFO_SAVE.getMsg();
}
- 빈번하게 수행되지만 변경이 자주 안되는 서비스인 경우 캐시를 설정한다.
@Cacheable 어노테이션 뒤에 dispatcher-servlet.xml 파일에 있는 cache name과 동일한 명을 value= 뒤에 기입한다.
@Override
@Cacheable(value="DepList")
public List<DepListVo> getDepList() {
return dao.getDepList();
}
- 캐시를 주기적으로 삭제해 줄 필요가 있는 경우 캐시를 리프레쉬해 주는 서비스를 등록한다.
@CacheEvict 어노테이션 뒤에 dispatcher-servlet.xml 파일에 있는 cache name과 동일한 명을 value= 뒤에 기입한다.
@Scheduled 어노테이션 뒤에 실행되는 시간 또는 주기를 설정한다.
/**
* Cache Refresh
* 매일 새벽 3시에 실행
*/
@Override
@CacheEvict(value = { "DepList", "JobList" })
@Scheduled(cron = "0 0 3 * * ?")
public void scheRefreshCache() {
}
- Package
- 인터페이스: com.rsm.(시스템명).controller
- 시스템의 규모에 따라 상위 패키지를 시스템으로 하고 하위에 서브시스템을 서브 패키지로 작성한다.
com.rsm.sample.controller / com.rsm.sample.subsample.controller
- Class
- 클래스 상단에 @RestController annotation을 반드시 기입한다.
- 해당 컨트롤러의 최상단 URL 경로를 지정한다.
- 클래스 이름 뒤에 반드시 Ctrl을 붙여준다.
@RestController
@RequestMapping("/emp")
public class SampleCtrl
- Dependency Injection
- 해당 controller와 관련된 service 클래스를 의존성 주입한다.
- 리턴 메시지 번역하기 위한 MessageTrans 클래스를 의존성 주입한다.
- 리턴 메시지를 가져오기 위한 MessageReturn 클래스를 의존성 주입한다.
@Autowired
private SampleService service;
@Autowired
private MessageTrans messageTrans;
@Autowired
private MessageReturn messageReturn;
- RequestMapping
- 각 Request 별 URL 경로를 설정하고 http method를 지정한다.
- [http method]
create: PUT
retrieve: GET
update: POST
delete: DELETE
(단, 여러 건을 한 번에 삭제해야 한다면 POST로 지정하도록 한다.) - @ResponseBody annotation을 기입하여 Response가 html body 부분에 할당됨을 표기한다.
@RequestMapping(value = "list", method = RequestMethod.GET)
@ResponseBody
- select Method
- 데이터를 조회하는 메서드인 경우 ResponseEntity<> 타입으로 선언한다.
parameter를 선언하려면 @RequestParam("변수명") 지정하고 그 뒤에 타입과 변수명을 작성한다.
언어설정을 위해 @RequestHeader에 Language 변수 값을 lang 이라는 String 타입의 변수에 대입한다.
언어설정 값이 없는 경우 디폴트 'ko'로 설정한다. - service 인스턴스의 데이터 조회관련 메서드 실행하여 그 결과값이 리턴되도록 한다.
- 데이터를 조회하는 메서드인 경우 ResponseEntity<> 타입으로 선언한다.
public ResponseEntity<List<EmpListVo>> getEmpList(@RequestParam("departmentId") String departmentId,
@RequestHeader(value = "Language", defaultValue = "ko") String lang) {
List<EmpListVo> empLists = service.getEmpList(departmentId);
return messageReturn.getRestRespList(empLists);
}
- Insert, Update, Delete
- 데이터를 삽입, 수정, 삭제하는 메서드인 경우 ResponseEntity<Map<String, Object>> 타입으로 선언한다.
parameter를 선언하려면 @RequestBody "변수타입" "변수명" 형태로 한다. - service 인스턴스의 데이터 저장관련 메서드 실행하여 그 결과값이 리턴되도록 한 후 다시 그 값을 언어설정에 맞게 변환한다.
- 결과값과 변환된 메시지 값을 리턴한다.
- 데이터를 삽입, 수정, 삭제하는 메서드인 경우 ResponseEntity<Map<String, Object>> 타입으로 선언한다.
public ResponseEntity<Map<String, Object>> setEmp(@RequestBody List<EmpSaveVo> vos) {
result.clear();
try {
msg = service.setEmp(vos);
} catch (Exception e) {
msg = e.getMessage();
}
result = messageTrans.getMapLang(msg);
return messageReturn.getRestResp(result, msg);
}