목적


그동안 프로젝트 CI(Continuous Integration) 관리를 위하여 빌드서버를 따로 관리해야하는 번거로움이 있었는데, 
AWS에서 이러한 어려움을 덜어주기위해 AWS CodeBuild를 출시했다.
보편적으로 사용되고 있는 빌드툴인 젠킨스를 사용하기 위해서는 다소 복잡한 세팅과정과 빌드용 서버가 필요했다.
빌드를 위한 작업자와 서버에 들어가는 비용을 줄이기 위하여, AWS CodeBuild를 사용하려 한다.

빌드하고자하는 프로젝트의 환경은 Maven, Spring4, Github 이다.

AWS CodeBuild란?


CodeBuild는 빌드에 소요되는 시간(분당 $0.005)에 과금하며, 빌드용 서버 사양을 선택할 수 있다.
테스트 해본결과 보통 5분이면 빌드가 완료되었고, 가격으로 따지면 빌드당 $0.025이다. 왠만큼 큰 프로젝트가 아니면 저렴하게 사용할 수 있겠다.
소스 레파지토리는 AWS CodeCommit, GitHub, S3bucket를 사용할 수 있고 언어 대부분의 서버언어를 커버한다.
빌드 스펙은 YAML 양식을 사용하는데 상세 내용은 아래 세팅하기에서 다루겠다.
상세 스펙은 한글로 번역된 페이지를 첨부하여 대신한다.

 빌드 서버를 설치, 설정 및 확장 및 패치 등에 신경쓰지 않고, CodeBuild를 활용하여 개발 과정에서 유연상을 보장하고 여러 형태의 빌드 상태나 호환성의 불일치 문제를 해결할 수 있습니다. CodeBuild를 사용 하면, 사전에 빌드 서버를 프로비저닝 할 필요가 없으며, 대기중인 빌드를 쌓아 두는 대신에 빌드 볼륨을 활용할 수 있도록 자동으로 확장됩니다. 분당 $0.005부터 시작하는 가격으로 분당 기준으로 빌드 리소스에 비용을 지불하기 때문에 사용한 시간만 비용을 지불합니다.

세팅하기


빌드를 위한 서버세팅이 필요 없었기 때문에, 전체적인 빌드 세팅은 젠킨스보다 훨씬 간편했다. 
다만, 레퍼런스가 적고 ASIA쪽 리전은 서비스 이전이라 Oregon 리전을 사용해야하는 번거로움이 있었다.
  • CodeBuild 프로젝트 생성 (Configure your Project)
    • CodeBuild 서비스에서 Create Project를 클릭한다.
    • Project name은 아무거나 상관없다. 
  • Source: What to build
    • 소스코는 github에서 가져오려고한다. 간단하게 사용자 인증을 하면, 내 깃헙의 레파지토리를 볼 수 있다.
  • buildspec.yml 작성
    • 프로젝트 루트레벨에 buildspec.yml 이름의 파일을 생성한다.
    • 실직적으로 빌드를 위한 스크립트내용을 담고 있는 파일이다.

      version : 0.1 (빌드할 프로젝트의 버전)
      environment_variables : (환경 변수로 빌드 시 입력받을 수도 있지만 여기서도 받을 수 있다. 없어도 됨)
      phases : 빌드 단계별 설정으로 pre bulid 와 build만 작성했다.
      artifacts : 빌드 완료 후 결과물 경로를 위한 설정으로 AWS S3에 업로드하기 위한 필수조건이다.


  • Environment: How to build
    • 빌드 스크립트를 buildspec.yml를 통해 정의했다면 빌드 환경만 선택해 주면 된다.
    • OS는 현재 Ubuntu만 사용가능하다. (Java, jdk8 선택)


    • Artifacts: Where to put the artifacts from this build project
      • 빌드 후 생성되는 war 파일을 저장할 곳을 지정한다.
      • 같은 리전의 S3 저장소만 사용할 수 있음에 유의한다.
    • Service role & Advanced settings
      • 서비스 롤과 빌드 서버 성능, 환경변수값을 지정할 수 있다.
      • 자세한 설명은 생략한다.


빌드하기


이제 Source Version에 빌드할 브랜치명만 추가하면 프로젝트를 빌드할 수 있다.
빌드 과정과 로그가 친절하게 표시된다.


빌드과 완료되면 단계별 결과와 로그를 확인 할 수 있다.



빌드가 끝나면, 앞서 세팅해두었던 S3에 artifact가 저장되어있다. 완료~!

삽질내용,  앞으로 AWS CodeDeploy 연동 


buildspec.yml 을 정의하지 않고 커맨드라인으로 빌드 스크립트를 작성했더니, 빌드 완료 후 S3에 올릴 artifact를 찾지 못했다.
pom에 등록된 의존성 중에서 oracle 관련한 라이브러리를 가져오지 못했다. -> pom에 레파지토리 추가.

이제 CodeDeploy를 이용하여 무중단 자동 배포만 준비하면 된다. 
Jenkins를 알고 CodeBuild를 설정해서 그런지, 무척이나 쉽게 느껴졌다.
하루빨리 Seoul 리전에도 서비스 되길! 


목적

Spring + Mybatis로 구성된 서비스를 운영하던 중 사용자가 몰렸을 때 DB에 부담을 덜어주기 위한 방법을 고민했다.
Mybatis 의 ExcutorType 을 Batch로 설정하면 성능이 향상 되었는데 이유를 파악하기 위해 포스트 남긴다.

결론적으로 Batch 는 statement를 재사용 하며 Create, Insert, Update를 벌크로 모아서 처리하고, 
Select를 만나면 기존에 모아놓았던 쿼리들을 수행하기 때문에 C, I, U 작업은 연달아서 할 때 가장 효율적이었다.
트랜잭션과, select문이 끼어드는 위치를 잘 조정하여 사용하면  DB의 부담을 덜 수 있을것으로 보인다. 

Batch Processing? - wiki

일괄 처리(batch processing)란 컴퓨터 프로그램 흐름에 따라 순차적으로 자료를 처리하는 방식을 뜻한다.
일괄 처리 시스템(batch system)이란 일괄처리(batch processing) 방식이 적용된 시스템으로서, 하나의 작업이 끝나기 전까지는 다른 작업을 할 수 없다.

장점 
  • 많은 사용자 사이에서 컴퓨터 자원을 공유할 수 있다.
  • 작업 프로세스의 시간대를 컴퓨터 리소스가 덜 사용되는 시간대로 이동한다.
  • 분 단위의 사용자 응답 대기와 더불어 컴퓨터 리소스의 유휴 사용을 피한다.
  • 전반적인 이용률을 높임으로써 컴퓨터의 비용을 더 잘 상환하도록 도와 준다.

JDBC에서 Batch Processing 

JDBC 2.0는 batch proccessing 은 다음과 같이 제공한다.
1. Prepare statement.
2. Bind parameters.
3. Add to batch.
4. Repeat steps 2 and 3 until interest has been assigned for each account.
5. Execute.

preparedStatement를 사용한 Batch Processing

PreparedStatement stmt = conn.prepareStatement(
    "UPDATE account SET balance = ? WHERE acct_id = ?");"UPDATE account SET balance = ? WHERE acct_id = ?");
int[] rows;

for(int i=0; i<accts.length; i++) {
    accts[i].calculateInterest( );
    stmt.setDouble(1, accts[i].getBalance( ));
    stmt.setLong(2, accts[i].getID( ));
    stmt.addBatch( );
}
rows = stmt.executeBatch( );
  • addBatch() : create, insert, update, deleted 일괄 처리 결과를 관리 할 필요가 없기 때문에 batch에 statement를 추가하는 일만 한다.
  • excuteBatch()가 쿼리들을 수행하고 정수형 배열을 리턴하는데, 각 배열의 요소는 각 statement로 인해 update된 row의 숫자이다. 

batch모드로 수행할 때 auto-commit을 사용하면 각각의 statement 를 수행 후 commit이 발생한다.
그래서 batch 수행 중 에러가 발생하면 에러 발생 전까지 statement는 DB 에 반영되어있고 BatchUpdateException에서 getUpdateCounts()로 성공한 쿼리의 숫자를 얻어 올 수 있다.
실제 배치 과정에서, 각 statement 마다 commit을 하면 배치의 성능이 떨어지게 되는데 이것을 트랜잭션으로 묶어서 관리하려고 해도 성능에 악영향을 미친다. 가장 좋은 방법은 임의의 수 만큼 statement 를 묶어서, 명시적으로 commit 하는 것 같다. 


Mybatis Batch Mode

Mybatis는 SqlSessionFactory에서 SqlSessionTemplate(SqlSession 구현체)을 생성할 때 ExcutorType을 지정해 줄 수 있다.

ExcutorTypeDescription
SIMPLEexecutor does nothing special 
REUSEexecutor reuses prepared statements 
BATCHexecutor reuses statements and batches updates


ExecutorType.BATCH : 이 실행자는 모든 update(C,I,U,D)구문을 배치처리하고 중간에 select 가 실행될 경우 필요하다면 경계를 표시한다.

여기서 이 경계는 아래 소스코드를 통해 확인 할 수 있다. 


SqlSessionTemplate에서 ExecutorType이 BATCH인 경우에 Create, Insert, Update, Delete 구문은 doUpdate 에서, Select 구문은 doQuery 에서 처리한다.

아래 코드의 59번 줄에서 statement를 재사용하는 로직을 확인 할 수 있고, 61번, 69번 줄에서 statement 재사용 여부에 따라 statementList에 statement를 추가하고 63번, 70번 줄에서 파라미터 추가, 다른 구문일 경우 BatchResult를 추가하는 것을 볼 수 있다.

그리고 doQuery 메소드의 81번줄에서 flushStatements()를 호출 하는데, 이는 그동안 statementList에 쌓아 왔던 statement를  executeBatch() 메소드로 수행한다. 수행 결과는 batchResult에 저장하여 return 하고, 그동안 쌓아왔던 statementList 와 batchResultList 를 초기화한다.

아래 코드처럼 Select문이 실행 될 경우 BATCH 처리를 멈추고 초기화 하기 때문에, BATCH 작업 도중 SELECT 문이 끼어들면 성능이 저하된다. 


org.apache.ibatis.executor.BatchExecutor.class - grepcode

40 public class More ...BatchExecutor extends BaseExecutor {
41
42   public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
43
44   private final List<Statement> statementList = new ArrayList<Statement>();
45   private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
46   private String currentSql;
47   private MappedStatement currentStatement;
48
49   public More ...BatchExecutor(Configuration configuration, Transaction transaction) {
50     super(configuration, transaction);
51   }
52
53   public int More ...doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
54     final Configuration configuration = ms.getConfiguration();
55     final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
56     final BoundSql boundSql = handler.getBoundSql();
57     final String sql = boundSql.getSql();
58     final Statement stmt;
59     if (sql.equals(currentSql) && ms.equals(currentStatement)) {
60       int last = statementList.size() - 1;
61       stmt = statementList.get(last);
62       BatchResult batchResult = batchResultList.get(last);
63       batchResult.addParameterObject(parameterObject);
64     } else {
65       Connection connection = getConnection(ms.getStatementLog());
66       stmt = handler.prepare(connection);
67       currentSql = sql;
68       currentStatement = ms;
69       statementList.add(stmt);
70       batchResultList.add(new BatchResult(ms, sql, parameterObject));
71     }
72     handler.parameterize(stmt);
73     handler.batch(stmt);
74     return BATCH_UPDATE_RETURN_VALUE;
75   }
76
77   public <E> List<E> More ...doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
78       throws SQLException {
79     Statement stmt = null;
80     try {
81       flushStatements();
82       Configuration configuration = ms.getConfiguration();
83       StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
84       Connection connection = getConnection(ms.getStatementLog());
85       stmt = handler.prepare(connection);
86       handler.parameterize(stmt);
87       return handler.<E>query(stmt, resultHandler);
88     } finally {
89       closeStatement(stmt);
90     }
91   }
92
93   public List<BatchResult> More ...doFlushStatements(boolean isRollback) throws SQLException {
94     try {
95       List<BatchResult> results = new ArrayList<BatchResult>();
96       if (isRollback) {
97         return Collections.emptyList();
98       } else {
99         for (int i = 0, n = statementList.size(); i < n; i++) {
100          Statement stmt = statementList.get(i);
101          BatchResult batchResult = batchResultList.get(i);
102          try {
103            batchResult.setUpdateCounts(stmt.executeBatch());
104            MappedStatement ms = batchResult.getMappedStatement();
105            List<Object> parameterObjects = batchResult.getParameterObjects();
106            KeyGenerator keyGenerator = ms.getKeyGenerator();
107            if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
108              Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
109              jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
110            } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
111              for (Object parameter : parameterObjects) {
112                keyGenerator.processAfter(this, ms, stmt, parameter);
113              }
114            }
115          } catch (BatchUpdateException e) {
116            StringBuffer message = new StringBuffer();
117            message.append(batchResult.getMappedStatement().getId())
118                .append(" (batch index #")
119                .append(i + 1)
120                .append(")")
121                .append(" failed.");
122            if (i > 0) {
123              message.append(" ")
124                  .append(i)
125                  .append(" prior sub executor(s) completed successfully, but will be rolled back.");
126            }
127            throw new BatchExecutorException(message.toString(), e, results, batchResult);
128          }
129          results.add(batchResult);
130        }
131        return results;
132      }
133    } finally {
134      for (Statement stmt : statementList) {
135        closeStatement(stmt);
136      }
137      currentSql = null;
138      statementList.clear();
139      batchResultList.clear();
140    }
141  }
142
143}

 기타, 참조


용어

SqlSessionTemplate - http://www.mybatis.org/spring/ko/sqlsession.html
SqlSessionTemplate은 마이바티스 스프링 연동모듈의 핵심이다.
SqlSessionTemplate은 SqlSession을 구현하고 코드에서 SqlSession를 대체하는 역할을 한다.
SqlSessionTemplate 은 쓰레드에 안전하고 여러개의 DAO나 매퍼에서 공유할수 있다.
getMapper()에 의해 리턴된 매퍼가 가진 메서드를 포함해서 SQL을 처리하는 마이바티스 메서드를 호출할때 SqlSessionTemplate은 SqlSession이 현재의 스프링 트랜잭션에서 사용될수 있도록 보장한다.
추가적으로 SqlSessionTemplate은 필요한 시점에 세션을 닫고, 커밋하거나 롤백하는 것을 포함한 세션의 생명주기를 관리한다.
또한 마이바티스 예외를 스프링의 DataAccessException로 변환하는 작업또한 처리한다.
SqlSessionTemplate은 마이바티스의 디폴트 구현체인 DefaultSqlSession 대신 항상 사용된다.
왜냐하면 템플릿은 스프링 트랜잭션의 일부처럼 사용될 수 있고 여러개 주입된 매퍼 클래스에 의해 사용되도록 쓰레드에 안전하다.
동일한 애플리케이션에서 두개의 클래스간의 전환은 데이터 무결성 이슈를 야기할수 있다.

참조

Batch Processing - https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%84_%EC%B2%98%EB%A6%AC

Batch Processing(JDBC) - https://www.safaribooksonline.com/library/view/database-programming-with/1565926161/ch04s02.html

Batch Processing(JDBC) - http://www.tutorialspoint.com/jdbc/jdbc-batch-processing.htm

Mybatis3 - http://www.mybatis.org/mybatis-3/ko/index.html

Grep Code(org.apache.ibatis.executor.BatchExecutor.class) - http://grepcode.com/file/repo1.maven.org/maven2/org.mybatis/mybatis/3.2.7/org/apache/ibatis/executor/BatchExecutor.java#BatchExecutor

1. 개요


서비스에 대용량의 트래픽이 몰리면서, 특정 API 호출에 대한 응답이 지연되면 전체적 로직에 영향을 미치게 된다.

이러한 상황을 재현하기 위해 응답을 늦게 보내는 서버를 구현 하고자 했다.

단순하게 요청을 받고 몇초간의 sleep 후 응답을 돌려주면 간단하게 구현할 수 있지만, 

제한된 자원(톰캣 1대)에서 다수(초당 15000명)의 request 를 처리하기 위해서 일반적인 sleep 방법으로는 구현 할 수 없었다.

(성능 이슈가 없다면 이곳 을 참조하면 간단하게 응답 지연 api를 얻을 수 있다.)


2. 문제점


사실, response time은 늦으면서, 다수의 request를 처리한다는 건 역설적인 말이다. 

request 가 많을 수록, connection 의 수는 늘어 날 것이고, sleep 을 통해 connection을 놓아주지 않으면  더 이상의 request 를 처리할 수 없기 때문이다. 

request 를 위한 connection 비용이 적지 않을 뿐더러, Spring 에서 request 를 처리하기 위한 servlet container thread 자원은 application의 로직을

수행하기 위한 application thread 보다 적고 한정적이다. 


3. 해결 방법


Long polling 방식으로 request 에 대하여 늦은 response를 전달하는 방법으로 서버 성능 문제를 해결하고자 했다.

단순히 클라이언트 쪽에서 long polling 방식은 connection을 그대로 유지하고 있기 때문에 문제를 해결 할 수 없는 방식이었고,

서버쪽에서 long polling 을 적용하여 connection 위한 thread 관리가 필요했다.


사실 long polling 방식도 request - response 모델이어서, 서버에서 클라이언트에게 응답을 주기 전까지 connection을

유지하기 때문에 이 방식도 성능 이슈를 해결하기 어려울 것 처럼 보였다. 

그러던 중 이러한 long polling의 문제점을 보완하기 위해 Spring 3.2에서 추가된 Async Servlet 기능을 보게 되었다. 

Async Servlet는 request 처리를 위한 servlet container thread의 일을 application thread에 위임시키고 servlet container thread를 반납해서

새로운 request 를 처리하는 방식으로 작동한다.


Spring 의 Async Servlet 을 이용하면, 서버의 thread pool 과 acceptCount 값을 조정하여, 

제한된 자원에서 다수의 request 를 처리할 수 있을것이라 생각했다.


4. 적용


소스 코드는 Spring-mvc-showcase 를 참조하였다. 


(1) 소스

@Controller @RequestMapping("/async/callable") public class CallableController { @RequestMapping("/response-body-2") public @ResponseBody Callable<String> callable() { return new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(2000); return "Callable result"; } }; } }



(2) 결과

응답이 2초 후 온 것을 알 수 있다.


위 로직을 수행하는 동안의 로그는 다음과 같다.

15:23:20 [http-apr-80-exec-9] DispatcherServlet - DispatcherServlet with name 'appServlet' processing GET request for [/async/callable/response-body-2]

15:23:20 [http-apr-80-exec-9] RequestMappingHandlerMapping - Looking up handler method for path /async/callable/response-body-2

15:23:20 [http-apr-80-exec-9] RequestMappingHandlerMapping - Returning handler method [public java.util.concurrent.Callable<java.lang.String> org.springframework.samples.mvc.async.CallableController.callable()]

15:23:20 [http-apr-80-exec-9] DispatcherServlet - Last-Modified value for [/async/callable/response-body-2] is: -1

여기 까지 기존 로직을 따른다

15:23:20 [http-apr-80-exec-9] WebAsyncManager - Concurrent handling starting for GET [/async/callable/response-body-2]

WebAsyncManager(application thread) 에서 병행처리를 시작하고

15:23:20 [http-apr-80-exec-9] DispatcherServlet - Leaving response open for concurrent processing

Dispatcher(servlet container thred) 에서 작업을 끝낸다.

이렇게 작업 쓰레드간 switching 이 이루어지고

15:23:22 [MvcAsync5] WebAsyncManager - Concurrent result value [Callable result] - dispatching request to resume processing

작업을 넘겨받은 MvcAsyc 는 작업을 수행(2초간의 sleep)후 dispatching 을 요청한다.

15:23:22 [http-apr-80-exec-4] DispatcherServlet - DispatcherServlet with name 'appServlet' resumed processing GET request for [/async/callable/response-body-2]

15:23:22 [http-apr-80-exec-4] RequestMappingHandlerMapping - Looking up handler method for path /async/callable/response-body-2

15:23:22 [http-apr-80-exec-4] RequestMappingHandlerMapping - Returning handler method [public java.util.concurrent.Callable<java.lang.String> org.springframework.samples.mvc.async.CallableController.callable()]

15:23:22 [http-apr-80-exec-4] DispatcherServlet - Last-Modified value for [/async/callable/response-body-2] is: -1

15:23:22 [http-apr-80-exec-4] RequestMappingHandlerAdapter - Found concurrent result value [Callable result]

15:23:22 [http-apr-80-exec-4] RequestResponseBodyMethodProcessor - Written [Callable result] as "text/html" using [org.springframework.http.converter.StringHttpMessageConverter@7967f612]

Spring mvc가 callable 을 보고 병행작업임을 알고 나머지 로직을 수행한다.

15:23:22 [http-apr-80-exec-4] DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'appServlet': assuming HandlerAdapter completed request handling

15:23:22 [http-apr-80-exec-4] DispatcherServlet - Successfully completed request



5. Servlet container thread 와 application specific thread 의 차이


여기서 servlet container thread 와 application thread 의 차이점을 살펴보자.

servlet thread의 일을 application thread 로 넘겨줌으로 해서 long polling 의 단점을 보완한다고 하는데, 결국 application thread 역시

서버의 자원을 사용하지 않는가? 라는 의문을 가질 수 있다.


Servlet container 란 HTTP Request 와 response 객체를 서블릿에 넘겨주고, 서블릿의 생명주기를 관리한다.

따라서 servlet container thread 는 사용자의 request 를 처리하는 작업을 수행한다. 

반면 application는 서비스 자체를 의미하고, application thread는 Spring framework 에서 발생하는 작업들을 수행한다.


이러한 관점에서 보았을때,  serblet thread 의 일을 application thread 에 위임하는 것이 

많은 request 를 수신하기 위한 효율 적인 방법임을 알 수 있다.

하지만, 결국 한정된 서버의 자원을 공유하여 사용한다는것은 변함이 없다.

spring mvc 내에서 servlet container에 할당 할 thread 수를 조정할 수 있는지 알아보고 최적화가 필요할 것 같다.


6. 결론


서버의 한정된 자원에서 다수의 request 를 처리하는데 한계가 있지만, 

서버는 무엇으로 구성할지 (톰캣, 네티, 직접구현)에 따라서, 그리고 서버 환경 설정을 어떻게 하는냐에 따라 동시에 수용할 수 있는 request의 수를 증가 시킬 수 있다. 

서버의 설정 이후에는, 어플리케이션의 구현 부분에서 최적화가 필요할 것이다.

고민의 시작은 전자에서 시작했지만, 작업은 후자에서 진행되었다.

다시 서버단으로 돌아가서 고민을 해봐야겠지만, 넉넉한 자원이 주어졌을때, 해당 자원을 제대로 활용하지 못하는 경우

application의 구현부분에서의 최적화 역시 필요할것으로 생각한다.


결과적으로  Async Servlet를 이용한 long polling 방식으로 서버에 요청할 수 있는 최대 request 를 늘리는 것은 실패하였다.

추가적으로 서버의 최적화 또는, tomcat 에 comet 을 적용, 심플한 구조의 서버 구축과 같은 작업이 병행되어야 할 것으로 보인다.

 


Flyway?

데이터베이스 형상관리 툴.

로컬에서 변경한 데이터베이스의 스키마나 데이터를 운영 데이터베이스에 반영하는 것을 누락하는것을 막기 위해 사용한다.

또한 개발 DB와 운영 DB의 스키마를 비교하거나, 운영 DB에 수작업을 가하는 노가다와 위험성을 줄이기 위해 사용한다.

생성한 형상을 새로운 DB에 적용하면 그게 마이그레이션이다.



 

동작방식

Flyway 가 연결된 데이터베이스에 자동으로 SCHEMA_VERSION 이라는 메타데이터 테이블을 생성한다.

Flyway 는 사용자가 정의한 sql의 파일명을 자동으로 스캔하여, SCHEMA_VERSION 에 버전 정보를 남기는 동시에,

데이터베이스에 변경내용을 적용한다.

SCHEMA_VERSION 테이블에는 다음과 같은 정보를 담고 있다.

 


명령어 설명

init - SCHEMA_VERSION 을 baseline 과 함께 생성한다. 테이블이 이미 생성되어 있으면 수행되지 않는다.

migrate - 스키마정보를 리얼DB에 마이그레이션한다.

clean - flyway로 생성한 스키마를 모두 삭제한다고 하지만, 해당 데이터 베이스의 모든 테이블을 삭제한다. 

info - DB에 적용된 스키마 정보와, 로컬에 pending 되어있는 변경 정보를 보여준다.

validate - DB에 적용된 스키마 정보와, 로컬의 변경점을 비교하여 보여준다.

repair - 마이그레이션 실패한 내역을 수정한다 (삭제, 교체)

baseline - flyway로 형상 버전관리를 시작 할 baseline 을 설정한다.

Command-line tool 로 예제 실행

(1) 설치

http://flywaydb.org/getstarted/firststeps/commandline.html

.

(2) 설정 

설치 폴더 -> conf -> flyway.con 파일 수정 (C:\flyway\conf\flyway.conf)

flyway.url=jdbc:mysql://localhost:3306/테이블명
flyway.user=아이디
flyway.password=비번

.

(3) 마이그레이션 sql문 생성 

설치 폴더 -> sql (C:\flyway\sql) 에 파일 생성.

init을 이용하여 SCHEMA_VERSION 테이블을 생성하면 V1 로 생성되기 때문에 파일명을 V2 로 생성함.

V2__Create_person_table.sql 

create table PERSON ( 
    ID int not null, 
    NAME varchar(100) not null 
);create table PERSON ( 
    ID int not null, 
    NAME varchar(100) not null 
);                                                                            

V2.1__Insert_person.sql

insert into PERSON (ID, NAME) values (1, 'Axel'); 
insert into PERSON (ID, NAME) values (2, 'Mr. Foo'); 
insert into PERSON (ID, NAME) values (3, 'Ms. Bar');

파일명은 V 와 숫자로 버전명을 지정하고 under_bar 두개로 시작되어야한다. 이미지참조.


  

.

(4) flyway init 명령문 수행

flyway init 없이 migrate를 수행하면, 자동으로 schema_version이 생성된다.

flyway init 을 수행하면 결과 화면과 같이 baseline 이 V1 로 입력되어있는것을 확인 할 수 있다.

명령문 수행화면

데이터베이스 적용 결과

(5) flyway migrate 명령문 수행

flyway가 sql 파일들을 스캐닝하여 수행한다.

V2 와 V2.1 의 sql 파일이 버전 순서대로 수행된 것을 확인 할 수 있다.

명령문 수행화면

데이터베이스 적용결과

.

(6) 마이그레이션 sql문 추가 생성

V2.2__Insert_another_person.sql 

insert into PERSON (ID, NAME) values (5, 'CLEAN!!!!');

.

(7) flyway validate 명령문 수행

migrate 없이, flyway validate 를 수행하면 새로 추가한 쿼리문에 실제 DB에 적용되어 있지않기 때문에,

fail이 떨어진다.

명령문 수행화면

.

(8) flyway info 명령문 수행

flyway info 로  내용을 확인해 보면 2.2 버전의 파일이 pending 상태인것을 알 수 있다.

명령문 수행화면

.

(9) flyway clean 명령문 수행

flyway clean 을 수행하면 y/n 질문도 없이 데이터베이스의 모든 테이블들이 드랍된다.

flyway로 작업된 테이블만 드랍된다는 말이 있는데, 아니다 모두 드랍된다.

명령문 수행화면

생각

전체적으로 사용하기 쉽고 대부분의 툴과 연동이 가능한 것이 장점.

데이터 베이스의 스키마 변경 이력을 확인 할 수 있다는 점이 가장 좋았음.

각자 로컬 데이터 베이스를 이용하여 개발하지 않으면 형상관리와 마이그레이션의 의미가 약해짐.

개발 DB 를 따로 운영함으로서, 작업중 스키마 변경에 따른 이슈가 발생 할 수 있지만,

스키마 이력관리를 위해, Flyway 를 적용하는 것은 오버스팩이라고 생각함. 

데이터베이스의 스키마를 변경하면 이력을 남기는 워크벤치쪽 플러그인이 있으면 좋겠다고 생각함.

참조

http://flywaydb.org/

1.목적

FUSE 를 이용하여 Native Camera에 접근하여 사진을 찍고, 보여주고, 저장하는 앱을 만든다.


2. 알아야 할 것

현재 FUSE 는 배타 환경으로 오픈하였기 때문에 NATIVE API가 빈약한 상태이다.

구현에 들어가기 전 FUSE 에서 제공하는 API의 스펙을 확인하는 것을 추천한다.


사용하려고 했던 API스펙


(1) Camera - https://www.fusetools.com/learn/fusejs#camera


Native Camera를 호출할 수 있는 API 다.

제공하는 함수는 takePicture 가 유일한데, 전달 값으로 너비와 높이, 방향이 있다.

그러나 직접 구현해본 결과 전달 값과 상관없이 기본 카메라의 앵글로 촬영되었으며,

이미지 생성도 기본 카메라와 동일한 크기로 작동하였다.


(2) Storage - https://www.fusetools.com/learn/fusejs#storage


Camera API 로 생성한 이미지를, 안드로이드 이미지 폴더에 복사(이동) 하기 위해 사용하려 했었던 API다.

하지만 상세 스펙을 보면 

'The storage API allows you to save text to files in the application directory.'

라고 한다. 이미지를 저장해보려 테스트를 해보니, 에러를 뱉진 않았지만, 결과값이 에러 값을 저장하고 있었다.

Camera로 생성한 이미지를 따로 저장하기 위해서는 다른 방법을 찾아봐야 할 것 같다.


3. 구현


(1) 화면 구성 

ui구현이 목적이 아니기 때문에, FUSE 에서 제공한 Slides using PageControl 예제를 수정하여 화면을 구성하였다.

Style 과 assets 들은 예제와 같기 때문에 MainView.ux 코드만 첨부한다.


MainView.ux 

    1. <App Theme="Basic" Background="#fff">
    2.     <DockPanel>
    3.             <MainStyle />
    4.                 <Page>
    5.                     <Info>
    6.                         <Header>
    7.                             <Text TextColor="#a94442" Value="{myfilePath}" Alignment="Center" />
    8.                             <Text TextColor="#a94442" Value="{errorMessage}" Alignment="Center" />
    9.                         </Header>
    10.                             <Button Text="Take a photo!!" ux:Name="button1" Clicked="{myTakePicture}" />
    11.                             <Button Text="Save a photo!!" ux:Name="button2" Clicked="{mySavePicture}"/>
    12.                     </Info>
    13.                     <BackgroundImage File="{myfilePath}" />
    14.                 </Page>
    15.     </DockPanel>
    16. </App>


(2) Camera 연동

카메라 촬영 후, 화면에 표시하기 위해 Observable API와 함께 작성하였다.


Camera.takePicture 의 너비와 높이 값은 결과물에 영향을 주지 못했고, takePicture 가 리턴해주는

file 객체에서 path 를 꺼내 myFilePath 에 연결하였다.

(file 객체를 이용하여 storage 에 저장하려 했으나, 실패하였다.)


2,3번 줄에서 사용할 객체 선언.

8번째 줄에서 takePicture 함수 선언.


16,19번 줄에서 모듈에 등록.


MainView.ux

    1. <App Theme="Basic" Background="#fff">
    2.     <JavaScript>
    3.         var Observable = require("FuseJS/Observable");
    4.         var Camera = require('FuseJS/Camera');
    5.  
    6.         var myfilePath = Observable();
    7.  
    8.         var myTakePicture = function() {
    9.             Camera.takePicture({ targetWidth: 640, targetHeight: 360}).then(function(file) {
    10.                 myfilePath.value = file.path;
    11.             }).catch(function(e) {
    12.                 console.log(e);
    13.             });
    14.         }
    15.  
    16.         module.exports = {
    17.             myfilePath: myfilePath,
    18.             myTakePicture: myTakePicture
    19.         };
    20.     </JavaScript>
    21.     <DockPanel>
    22.             <MainStyle />
    23.                 <Page>
    24.                     <Info>
    25.                         <Header>
    26.                             <Text TextColor="#a94442" Value="{myfilePath}" Alignment="Center" />
    27.                         </Header>
    28.                             <Button Text="Take a photo!!" ux:Name="button1" Clicked="{myTakePicture}" />
    29.                     </Info>
    30.                     <BackgroundImage File="{myfilePath}" />
    31.                 </Page>
    32.     </DockPanel>
    33. </App>


(3) 테스트


초기화면

촬영 후 화면

사진 아래에 파일 경로가 출력된 것을 볼 수 있다.


파일 저장 경로 확인

Android > data > 앱이름 > files 에 이미지가 저장 된 것을 확인할 수 있다.

4. 정리


카메라 호출 속도가 기존의 하이브리드 앱 툴에 비해 눈에 띄게 빨라서 놀랐다.

베타 환경이라는 점을 고려하면, 앞으로 하이브리드 앱을 개발하는데 있어서 점유율이 올라갈 것 같다.

아직 Native filesystem 을 이용할 수 있는 API 가 부족하여 촬영한 이미지를 갤러리 폴더로 옮기지 못했지만

기본이 되는 기능이므로 Fuse tools 에서 빠른 시일 내에, 업데이트할 것으로 예상한다.

이미지를 활용한 앱을 개발하기 위해서는 당분간 촬영한 이미지를 서버에 저장 가공 후, 제공해주는 형태로

개발을 진행하는 것이 좋겠다.


작성 코드 중 개선할 부분이나, 틀린 부분 지적해주시면 감사히 반영하도록 하겠습니다.


5. 참조

https://www.fusetools.com/


+ Recent posts