아키텍처와 함께

블로그 이미지
by gregorio
  • Total hit
  • Today hit
  • Yesterday hit

'전체 글'에 해당되는 글 56건

  1. 2020.05.08
    Spring MVC 테스트 자동화 및 커버리지 분석
  2. 2020.03.03
    Spring Framwork의 RequetMapping 정보 추출하기
  3. 2020.03.03
    Mybatis TypeHandler를 이용한 Password 암호화
  4. 2020.03.03
    Spring Transaction Simulation 기능
  5. 2019.04.28
    Mybatis SQL Logging

Spring MVC 테스트 자동화 및 Jacoco를 이용한 커버리지 분석 방법에 대해 소개한다.

Spring MVC 테스트 자동화를 위해 MockMvc를 이용하여 Controller를 호출하고, 그 결과를 검증하는 테스트 자동화 방법이다.

 

프로젝트 빌드 시 테스트 케이스가 수행되고, 수행 로직에 따라 커버리지 결과가 자동으로 생성한다.

 

먼저 pom.xml에 Dependency를 설정한다.

 

더보기

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${org.springframework.version}</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit.version}</version>
    <scope>test</scope>
</dependency>


 <dependency>
     <groupId>org.hamcrest</groupId>
     <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

 

 <dependency> 
     <groupId>com.jayway.jsonpath</groupId> 
     <artifactId>json-path</artifactId> 
    <version>2.2.0</version> 
    <scope>test</scope> 
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test-mvc</artifactId>
    <version>1.0.0.M1</version>
    <scope>test</scope>
</dependency>

 

pom.xml 파일에 Dependency를 추가한 후 Maven build시 자동으로 테스트 케이스와 커버리지를 수행하기 위해 plugin을 다음과 같이 설정한다.

 

더보기
plugin 설정

 

설정이 완료된 후 테스트 케이스를 작성한다.

 

테스트 케이스를 작성하기 전에 공통적으로 설정이 필요한 클래스를 작성하여 모든 테스트 클래스에서 상속하여 사용하도록 한다.

 

다음은 테스트를 위한 공통 클래스이다.

 

더보기
공통 클래스

다음은 테스트를 위한 클래스를 생성한다.

 

더보기
테스트 클래스

 

모든 작업이 완료된 후 Eclipse에서 Maven install을 수행한다.

 

테스트 수행이 완료된 후 다음과 같은 결과를 볼수 있다.

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

 

테스트 케이스에 대한 결과 파일은 workspace/프로젝트명/target/surefile-reports 디렉토리에 생성된다.

 

더보기
테스트 케이스 결과 파일

 

테스트에 대한 커버리지는 workspace/프로젝트명/target/site/jacoco 디렉토리에 생성된다.

더보기
Jacoco 테스트 커버리지

 

index.html을 클리하면 브라우저에 다음과 같은 화면을 통해 테스트 커버리지를 확인할 수 있다.

더보기
Jacoco 테스트 커버리지 결과

 

AND

Spring Framwork의 Controller에 정의되어 있는 모든 RequestMapping의 정보를 추출이 필요할 때가 있다.

 

이 경우 간단한 Controller를 생성하여 정보를 추출할 수 있다.

 

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import com.lgcns.spring.base.BaseController;
import com.lgcns.spring.base.BaseMap;



@Controller
public class RequestController extends BaseController{

@Autowired
private RequestMappingHandlerMapping handlerMapping;


@RequestMapping(value = "/endPoint.do", method=RequestMethod.POST)
@ResponseBody
public List getEndPoints(Model model) throws Exception {
List result = new ArrayList();
for (RequestMappingInfo key : handlerMapping.getHandlerMethods().keySet()) {
BaseMap map = new BaseMap();
map.put("name", key.getName());
map.put("path", key.getPatternsCondition());
map.put("methods", key.getMethodsCondition());
map.put("consumes", key.getConsumesCondition());
map.put("produces", key.getProducesCondition());
map.put("params", key.getParamsCondition());
map.put("headers", key.getHeadersCondition());
map.put("custom", key.getCustomCondition());
result.add(map);
}

return result;
}

}

RequestMappingHandlerMapping를 이용하여 Mapping 정보를 추출한 후 Json형식으로 화면에 보여주고 있다.

AND

사용자에 대한 password는 Hash를 이용하여 데이터베이스에 저장한다.

일반적으로 Spring framework를 사용하는 경우 BCryptPasswordEncoder를 이용하여 서비스에서 Hash값을 생성하여

데이터베이스에 저장한다.

 

입력한 암호를 Mybatis에 제공하고 있는 TypeHandler를 구현하여 Hash를 생성하여 데이터베이스에 저장하는 

방법이 있다.

 

Mybatis의 TypeHandler의 구현체를 생성한다.

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import com.lgcns.spring.crypt.PasswordEncoder;

public class PasswordHandler implements TypeHandler{


@Override
public void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
/** Password Encoding **/
String encodePassword = null;
try {
encodePassword = PasswordEncoder.encode(parameter);
} catch (Exception e) {
String errmsg = String.format("Fail to encrypt passwod :: [%s]", parameter);
throw new SQLException(errmsg);
}

ps.setString(i, encodePassword);
}

@Override
public String getResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}

@Override
public String getResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}

@Override
public String getResult(CallableStatement cs, int columnIndex) throws SQLException {
return  cs.getString(columnIndex);
}
}

본 예제에서는 setParamter 메소드에서 Hash를 생성하여 PreparedStatement에 Hash 값을 저장한다.

INSERT INTO USERS 
( USERNAME
  ,USER_ROLE_ID
  ,PASSWORD
  ,ENABLED
  ,CREATE_TIME
  ,CREATE_BY
  ,PAGE_ALLOWED )
VALUES ( 
#{username}
  , #{userRoleId}
  , #{password, typeHandler=com.lgcns.spring.mybatis.handler.PasswordHandler}
  , #{enabled}
  , #{createTime}
  , #{createTime}
  , #{pageAllowed} )

SQL에서 password 컬럼에 TypeHandler를 정의하면 TypeHandler에 정의되어 있는  setParameter에서 Return한 값을 데이터베이스에 저장한다.

 

Mybatis의 TypeHanlder를 이용하여 다야한 Masking 처리도 가능하다. 특히 카드번호, 주민번호 등 Masking 처리가 필요한 경우 적용하면 개발 시 편의성과 생산성이 향상될 수 있다.

AND

Spring Framework에서 CUD에 Transaction에 대해 DB에 데이터를 저장하지 않고 기능이 

정상적으로 동작하는지 확인이 필요한 경우가 있을 수 있다.

이 때 새로운 User Transaction을 정의한 후 모든 Transaction을 Rollback처리하도록 설정하면 가능하다.

 

본 기능에 대해서는 많은 테스트를 수행하지 않았지만, 하나의 테이블에 대한 Insert에 대해서는 정상적으로 

동작하는것을 테스트하였다.

 

Transaction은 일반적으로 서비스 단위에서 시작하지만,  모든 Trasnsaction을 Rollback 처리하기 위한 User

Transaction을 생성을 어디에서 해야할지 정의해야한다.

 

본 예제에서는 User Transaction을 Interceptor에서 정의한 후 해당 Transaction을 Rollback only로 설정하였다.

 

예제 프로그램은 다음과 같다.

 

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class SimulatorInterceptor extends HandlerInterceptorAdapter implements InitializingBean {

private static final Logger LOGGER = LoggerFactory.getLogger(SimulatorInterceptor.class);
private boolean enabled;
private boolean keyEnabled;
private String key;

private PlatformTransactionManager txManager;


@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String requestUri = ((HttpServletRequest)request).getRequestURI();

String keyValue = (String)request.getParameter(key);
keyValue = keyValue != null ? keyValue : "N";
if (enabled && (keyEnabled && "Y".equalsIgnoreCase(keyValue))) {
LOGGER.info("This [{}] request is for simulation", requestUri);

DefaultTransactionDefinition transDef = new DefaultTransactionDefinition();
transDef.setName("SimulationTx");
transDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transDef.setReadOnly(false);
transDef.setTimeout(30);
try {
/** New transaction **/
TransactionStatus status = txManager.getTransaction(transDef);
if (status.isNewTransaction()) {
LOGGER.warn("This [{}] transaction is created", requestUri);
}

/** Set roll back only **/
status.setRollbackOnly();
LOGGER.warn("This [{}] request is  roll back only", requestUri);
}
catch(Exception ex) {
LOGGER.error(ex.getMessage());
}

}

return super.preHandle(request, response, handler);
}


@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mav) throws Exception {
SimulatorContextHolder.rollback();
/** Do nothing **/
}


public void setEnabled(boolean enabled) {
this.enabled = enabled;
}


public void setKey(String key) {
this.key = key;
}


public void setKeyEnabled(boolean keyEnabled) {
this.keyEnabled = keyEnabled;
}


public void setTxManager(PlatformTransactionManager txManager) {
this.txManager = txManager;
}


@Override
public void afterPropertiesSet() throws Exception {
if (keyEnabled && key == null) {
this.key = "simulationYn";
}
}
}

1. DefafultTransactionDefinition을 통해 Transaction 이름, Prapagatio, ReadOnly 및 Timeout을 정의한다.

2. TransactionManager를 통해 TransactionStatus를 생성한다.

3. TransactionStatus.setRollbackOnly를 통해 모든 Transaction을 Rollback처리한다.

 

본 예제에서는 Interceptor에서 User Transaction을 REQUIRED로 생성한 후 이후 모든 Transaction이 REQUIRED로

정의되어 있는 경우에 사용이 가능한다.

 

User Transaction 생성 이후 다음 Transaction이 REQUIRED_NEW로 정의되면 정상적으로 모든 Transaction이 Rollback 되지 않는다.

 

 

AND

Mybatis 사용 시 SQL LOG를 Log 파일에 추가하는 Source Code이다.

BoundSql을 사용하면 파라미터를 SQL에 매핑하지 못하는데 이를 해결하기 위해

SQL 호출 시 사용하는 Value Object를 파라미터를 받아 Field Name과 Value를 Map으로

변환하여 SQL의 파라미터와 매핑한다.

 

public class SqlLogger {

private static final Logger SQL = LoggerFactory.getLogger("sqllog");

/**
 * Logging SQL with parameter
 * @param sqlId
 * @param params
 * @throws Exception
 */
public static void logging(String sqlId, Object params) throws Exception {

SqlSessionFactory sqlSessionFactory = BaseContext.getBean("sqlSessionFactory");

SqlSession session = sqlSessionFactory.openSession();
MappedStatement ms = session.getConfiguration().getMappedStatement(sqlId);
BoundSql boundSql = ms.getBoundSql(params);
List mappedParams = boundSql.getParameterMappings();
String  finalSql = boundSql.getSql();
    finalSql = finalSql.trim();
    finalSql = finalSql.replaceAll("\\s+", " ");
        if (params instanceof String || params instanceof Integer || params instanceof Long) {
     finalSql = finalSql.replaceFirst("\\?", String.valueOf(params));
        }
        else {
     Map<String, Object > fieldInfo = fieldInfo(params);
         for (ParameterMapping pm : mappedParams) {
          if (fieldInfo.containsKey(pm.getProperty())) {
          String value = null;
          if (fieldInfo.get(pm.getProperty()) instanceof String) {
          value = String.format("'%s'", String.valueOf(fieldInfo.get(pm.getProperty())));
          }
          else {
          value = String.valueOf(fieldInfo.get(pm.getProperty()));
          }
                 finalSql = finalSql.replaceFirst("\\?", value);
             }
         }        
        }
SQL.info("{}", finalSql);
}

/**
 * Extract object field value
 * @param object
 * @return
 * @throws Exception
 */
public static Map<String, Object> fieldInfo(Object object) throws Exception {
Map<String, Object> fieldInfo = new HashMap<String, Object>();

List fields = FieldUtils.getAllFieldsList(object.getClass());
for (Field field : fields) {
field.setAccessible(true);
Object value = field.get(object) != null ? field.get(object) : "";
fieldInfo.put(field.getName(), value);
}
return fieldInfo;
}
}

AND

ARTICLE CATEGORY

분류 전체보기 (56)
Spring Framrwork (33)
Linux (1)
APM (1)
Java (8)
python (0)
ant (1)
chart (1)
OS (1)
tomcat (1)
apache (1)
database (0)

RECENT ARTICLE

RECENT COMMENT

CALENDAR

«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31

ARCHIVE

LINK