MySQL 정보

  • 버전: 5.7
  • 트랜잭션 격리 수준: Default(repeatable read)

장애 현상

  • A 트랜잭션에서 T1 테이블에 delete 쿼리 진행할 때 B 트랜잭션에서 T1 테이블에 insert 쿼리가 lock wait 발생

원인-1(첫 번째 분석)

  • delete 쿼리의 실행 계획에서 풀 테이블 스캔 발생
  • Where 절이 pk로 구성되어 있어도 옵티마이저는 풀 테이블 스캔이 유리하다고 판단(풀 테이블 스캔은 멀티 블록 스캔이고 인덱스는 싱글 블록 스캔이니 일정 분기점 이상 부터는 인덱스 스캔보다는 풀테이블 스캔이 유리함)
  • MySQL에서 로우에 락이 잡히는 기준은 스캐닝된 인덱스 레코드[1]

위의 분석으로 알 수 있는 사항은 다음과 같다.

  • T1 테이블의 모든 로우는 락이 잡혔으며
  • 락이 잡혀있는 동안 다른 세션(트랜잭션)에서 T1 테이블의 모든 로우는 delete/update 불가능하다.

근데 insert는 왜 안돼??? 격리 수준을 read committed 했을 때는 insert가 잘 된다.

원인-2(두 번째 분석)

  • 현재 MySQL의 격리 수준은 repeatable read이다.
  • delete 쿼리를 진행할 때 Next-Key-Lock이 잡혔으며[2],
  • Repeatable read에서 Next-key-lock으로 인해 positive infinity 레코드 까지 락이 걸렸고,
  • 해당 락으로 인해 insert에 lock wait이 발생한다.
  • 위의 락으로 본의 아니게 팬텀 로우를 방지한다

기타 특이 사항

  • delete할 때 풀 테이블 스캔이 아닌 유니크 스캔을 할 때 insert에 lock wait이 발생하지 않는다.
  • ㄴ유니크 스캔을 하면 lock wait 발생하지 않는게 웃기지 않나?? Repeatable read에서 팬텀 로우를 막아준다는데 안 막아준다. ㅈㄴ 어불성설이다ㅋㅋㅋㅋㅋㅋㅋㅋ(아 근데 이 부분에서 lock wait 발생하면 동시성 ㅈ됨)
  • read committed 격리 수준일 때 되는 이유는 단순하다. gap lock, next-key-lock이 비활성화 되어 있기 때문이다.
  • 반대 상황도 Lock이 발생한다. 1) insert 2) delete 풀 테이블 스캔

기타 주의 사항

  • 인덱스 조건없이 쿼리하는 경우 테이블 락 발생
  • 2개 이상의 세션에서 동일한 인덱스 키를 사용하여 다른 행의 레코드에 엑세스할 때 락 충돌할 수 있다(동일한 스캔 범위가 있는 경우 lock wait 발생)
  • 인덱스 효율이 좋지 않으면 락 범위가 넓어진다.
  • 아무리 PK/유니크 인덱스를 사용하여도 옵티마이저 판단하에 풀 테이블 스캔으로 테이블 락이 발생할 수 있다(지금 상황).

재연

1. 테이블 생성

mysql> create table test_tt(t1 int primary key auto_increment, t2 int, t3 int); 
Query OK, 0 rows affected (0.09 sec)

2. 테스트 데이터 삽입/조회

mysql> insert into test_tt(t2, t3) values (1,2); Query OK, 1 row affected (0.00 sec) 
... 

mysql> select * from test_tt;
+----+------+------+
| t1 | t2   | t3   |
+----+------+------+
|  1 |    1 |    2 |
|  2 |    1 |    2 |
|  4 |    1 |    2 |
| 31 |    1 |    2 |
| 32 |    1 |    2 |
| 33 |    1 |    2 |
| 34 |    1 |    2 |
| 35 |    1 |    2 |
| 36 |    1 |    2 |
| 37 |    1 |    2 |
| 38 |    1 |    2 |
| 39 |    1 |    2 |
| 40 |    1 |    2 |
| 41 |    1 |    2 |
| 42 |    1 |    2 |
| 43 |    1 |    2 |
| 44 |    1 |    2 |
+----+------+------+
17 rows in set (0.00 sec)

3. 삭제 실행(세션 1, 트랜잭션 시작, 삭제실행)

mysql> explain delete from test_tt where t1 in(31,32,33,34);
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | DELETE      | test_tt | NULL       | ALL  | PRIMARY       | NULL | NULL    | NULL |    4 |   100.00 | Using where |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set (0.00 sec)

4. 삽입 실행(세션2, 트랜잭션 시작)

mysql> insert into test_tt(t2, t3) values (1,2);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

5. DB 락 현황 조회 결과

mysql> select * from information_schema.innodb_locks;
+--------------+-------------+-----------+-----------+-------------------+------------+------------+-----------+----------+------------------------+
| lock_id      | lock_trx_id | lock_mode | lock_type | lock_table        | lock_index | lock_space | lock_page | lock_rec | lock_data              |
+--------------+-------------+-----------+-----------+-------------------+------------+------------+-----------+----------+------------------------+
| 10586:43:3:1 | 10586       | X         | RECORD    | `mysql`.`test_tt` | PRIMARY    |         43 |         3 |        1 | supremum pseudo-record |
| 10581:43:3:1 | 10581       | X         | RECORD    | `mysql`.`test_tt` | PRIMARY    |         43 |         3 |        1 | supremum pseudo-record |
+--------------+-------------+-----------+-----------+-------------------+------------+------------+-----------+----------+------------------------+
2 rows in set, 1 warning (0.00 sec)
 
mysql> select * from information_schema.innodb_lock_waits;
+-------------------+-------------------+-----------------+------------------+
| requesting_trx_id | requested_lock_id | blocking_trx_id | blocking_lock_id |
+-------------------+-------------------+-----------------+------------------+
| 10586             | 10586:43:3:1      | 10581           | 10581:43:3:1     |
+-------------------+-------------------+-----------------+------------------+
1 row in set, 1 warning (0.00 sec)

 

참고문헌

Posted by 동팡

목차

  • 시리즈 소개
  • 개요
  • 라이브러리 소개
  • 데모 - 1
  • 데모 - 2
  • 데모 - 3
  • 데모 - 4
  • 마치며, 다음 시리즈

 

시리즈 소개

본 시리즈는 웹페이지 앞단에서 게시판 페이징을 하는 방법을 소개한다. 즉, JavaScript에서 어떻게 페이징 처리하는지 확인할 수 있다. 피곤하지 않으면 2장 라이브러리 최적화 작업을 진행할 예정이다. 요즘 바빠서 너무 피곤하다... 

  • Pure-JavaSciprt 게시판 페이징 - 1(기본/응용)
  • Pure-JavaSciprt 게시판 페이징 - 2(라이브러리 최적화)

 

개요

[들어가면서...]

웹 페이지에서 제일 많이 사용하는 기능은 단연 게시판이다. 그리고 신입 개발자가 처음에 직면하는 벽이 게시판 페이징 작업이다. 게시판 페이징 작업을 구글링 하여 구현하는 거는 진짜 쉽지 않을 것이다. 게시판 페이징 개발 스타일이 다양해서 이것저것 참고하다가 실패할 것이다. 차라리 책에서 제공하는 게시판 페이징 코드를 활용하는 게 정신 건강에 좋을 것이다. 

 

[게시판 페이징 개발 스타일]

그렇다면 어떻게 개발 스타일이 다를까? 다음과 같다.

  • Java(Spring) / JSP(JSTL) 백단 게시판 페이징 
  • JS 앞단 게시판 페이징

백단 게시판 페이징 스타일은 옛날 스타일이다. 백단에서 게시판 페이징에 필요한 데이터를 가공하여 앞단에 전달, JSP영역에서 JSTL 반복문을 사용하여 게시판 페이징을 구현한다. 단점은 1) 페이지 이동할 때마다 페이지 깜박임, 2) JSP에 종속적인 개발 방법이다. 앞단 게시판 페이징은 REST API가 많이 부각되면서 생기지 않았나 생각한다. 백단 기술 스택에 종속적이지 않고 페이지 깜박이는 현상도 없다. 필자 또한 후자를 소개하고자 한다. 

 

[게시판 페이징 라이브러리]

JS 게시판 페이징 라이브러리는 다양하다. 필자는 JS 개발하면서 2~3개 정도 사용했던 기억이 있다. 웹 페이지 특성상 가시적인 요소가 다분하기 때문에 요구사항이 많다. 이와 같은 요구사항을 모두 충족하는 게시판 페이징 라이브러리는 존재하지 않는다. 비약이 심했지만 진짜 찾다 포기했다. 또한 요구사항 충족을 위한 라이브러리 스터디와 분석이 필요하다. 해당 사항으로 인해 작업 속도가 더 느려질 수 있다. 그래서 결국 필자는 별도의 라이브러리를 개발하여 사용하고 있다. 이유는 단순하다. 외주작업을 혼자 하는데 지랄 같은 요구사항 이행하려면 기능 추가가 가능한 라이브러리가 필요하다. 그리고 실무에서 좀 있다 보면 게시판 페이징 라이브러리를 개발하는 것은 많이 어렵지 않을 것이다. 

 

 

[실무에서 게시판 페이징 디테일]

외주/회사 업무에서 겪은 게시판 페이징 요구사항은 다음과 같다.

  • 체크박스를 이용한 테이블 제어
  • 특정 로우에 이벤트 핸들링
  • 특정 열에 이벤트 핸들링
  • 특정 로우, 열에 이벤트 핸들링
  • 게시판 로우 제어
  • 퍼블리셔와의 템플릿 협업

주로 업무 시스템 개발하는 인원은 이것보다 좀 더 다양한 요구사항을 겪을 것이다. 필자는 다음의 요구사항 때문에 직접 만들기로 결심했다. 실제 개발 결과물이다.

특정 열에 이벤트 핸들링

있을 수 있지만, 각 열에 별개의 이벤트 핸들링을 지원하는 게시판 페이징 라이브러리를 찾지 못했다. 1년 전에 개발한 라이브러리이다.. 기억을 되살리고 테스트하여 시리즈를 연재한다.

 

라이브러리 소개

게시판 페이징을 하기 위한 복잡한 Pure-JS 라이브러리이다. 대단한 기능은 없다. 말마따나 복잡하다. 필자가 봐도 복잡하다. 라이브러리 최적화가 필요하기는 하다.  해당 라이브러리를 사용하기 위한 필수 값이 존재한다. 당연 필수 값은 게시판 페이징에 사용되는 데이터들이다. 데이터들은 백단 API를 이용해서 반환받을 수 있다.

  • total: 게시판 페이징 데이터의 총 로우 수
  • limit: 게시판에 뿌려질 데이터의 개수
  • offset: 게시판 페이지의 위치 
  • datas: 게시판에 뿌려질 데이터 

Oracle 외의 RDBMS를 이용하여 게시판 페이징을 했으면 limit, offset을 알 수 있다. RDBMS에서 limit, offset과 같은 의미고 같은 기능이다. 

 

아 참고로 바빠서 넣지 못한 기능이 페이징 넘버링에 대한 이벤트 바인딩이다. Prev, Next, 페이지 넘버에 대한 이벤트 바인딩은 사용자가 직접 추가해야 한다. 

(라이브러리 최적화 때 이벤트 바인딩 기능을 추가하면 괜찮을 것 같다.) 

 

소개는 짧게 하고 바로 데모를 진행한다. 

 

데모 1: 게시판 페이징 기본

백문이불여일견 JS 피들을 확인한다. 차근차근 진행하기 위해 이벤트 바인딩은 하지 않았다. JS, HTML 영역을 보면 쉽게 이해할 수 있다.

JS 영역

function renderTableList(tableDivEl, pageDivEl, tlbLibOpt) {
    var tblPageEl = new TblPager( tableDivEl, pageDivEl, tlbLibOpt).generateTablePage();
}

HTML 영역

<body>
  <!-- table -->
  <div id="tblArea"></div>
  <!-- //table -->
  <!-- pagination -->
  <div id="pageArea"></div>
  <!-- //pagination -->

</body>

 

설명

  • TEST DATAS는 백단 API에서 데이터를 받았다는 가정이다.
  • div의 id는 꼭 위와 같이 설정한다.
  • tlbLibOpt의 Object Literal은 라이브러리가 지정한 값만 사용해야 한다. 
  • headers를 이용하여 백단 API에서 받은 것을 사용자가 보기 좋게 TH 문구를 바꾼다. header는 tlbLibOpt에 꼭 추가해야 한다. 
  • tlbLibOpt의 변수명을 보면 대강 의미를 이해할 수 있다.

 

데모 2: 게시판 페이징, 이벤트 핸들링 추가

백문이불여일견 JS 피들을 확인한다. 페이지 번호와 Prev, Next에 이벤트 바인딩하였다. 이벤트 바인딩을 사용자가 하기 때문에 중복 코드와 복잡성을 가중한다. JS 피들에서 번호를 누르면 이동되는 것을 확인할 수 있다.

데모 2의 코드 플로우는 다음과 같다(추상화). 

  1. 페이지 DOM 로드 완료 후, 백단 API을 호출한다.
  2. 백단 API에서 받은 데이터를 JS DOM 랜더링 함수에 전달한다.
  3. JS DOM 랜더링 함수는 DOM을 생성/수정한다. 
  4. DOM 랜더링 완료 후 특정 DOM에 이벤트를 추가한다.
  5. 특정 DOM의 이벤트가 발생하면 2번부터 반복한다.

데모 2의 코드 플로우는 다음과 같다(구체화).

  1. 페이지 DOM 로드 완료 후, 게시판 페이징 백단 API을 호출한다.
  2. 백단 API에서 받은 데이터를 게시판 페이징 랜더링 함수에 전달한다.
  3. 랜더링 함수는 게시판 페이징 함수를 호출하여 DOM을 생성/수정한다.
  4. 게시판 항목(DOM) 랜더링 완료 후 페이지 번호, Prev/Next에 이벤트를 추가한다.
  5. 페이지 번호를 누르면 2번부터 반복한다.

 

데모 3: 게시판 페이징, 응용 - 요구사항 진행1

백문이불여일견 JS 피들을 확인한다. 퍼블리셔가 작업한 템플릿에 CSS의 기능을 죽이지 않고 JS 코드를 삽입해야 한다. 또한 고객의 요구사항을 몇 개 이행해야 한다. 이번 데모는 다음과 같은 요구사항을 이행한다.

  • 퍼블리셔가 작업한 템플릿이 적용된 게시판 페이징
  • 특정 로우, 특정 열 데이터 핸들링
  • 특정 열 데이터 핸들링

tlbLibOpt변수에 다음의 사항을 추가하였다.

tlbLibOpt : {
  tableInfo: {
    options: {
      tableClassName: "tableClassName",
      customBeforeThTag: function() { return tableColGroup },
      customThTag: function(headerVal) {
        if ( headerVal === "aaa" ) 
          return { start: "<th><span class='no_arrow'>", end: "</span></th>" }
        else 
           return { start: "<th><span>", end: "</span></th>"}
      },
      customTdBodyTag: function( headerVal, index ) {
        if (index == 2 && headerVal === "bbb") {
          return { start: "<td><span><b>", end: "</td></span></b>" }
        } else {
          return { start: "<td><span>", end: "</td></span>" }
         }
	  },
      customTdBodyValue: function( headerVal, listValue ) {
        if ( headerVal === 'bbb' ) {
          if ( listValue === true) {
            return '성공'
          } else {
            return '실패'
          }
        }

        return listValue
      }
    }
  },
  pageInfo: {
    options: {
      customPrev: {
        start: "<a href='#none'",
        end: "> <<<< </a>",
      },          
      customNext: {
      	start: "<a href='#none'",
        end: "> >>>> </a>",
      },
      customPageNum: {
        totalStart:"<span>",
        start: "<a href='#none'>",
        end: "</a>",
        totalEnd:"</span>"
      }
    }
  }
}

나참 드럽게 복잡하다;;

  • tableClassName: 생성되는 테이블에 class 이름을 추가한다.
  • customBeforeThTag: 테이블의 colgroup을 추가하기 위한 공간이다. <table><colgroup><thead>
  • customThTag: 콜백 함수를 추가했다. headerVal을 활용하여 특정 열에 다른 th 태그를 사용할 수 있다.
  • customTdBodyTag: TD의 특정 열 특정 로우에 특정 태그를 사용할 수 있다.
  • customTdBodyValue: TD의 특정 열 특정 로우에 값을 제어할 수 있다. 
  • customPrev/Next: 뒤로 가기에 특정 태그를 사용할 수 있다(이미지 삽입할 수 있다.).
  • customPageNum: 페이지 넘버링에 특정 태그를 삽입할 수 있다. 

개발자 도구를 활용하여 HTML 태그 랜더링이 어떻게 변했는지 확인할 수 있다. 

 

데모 4: 게시판 페이징, 응용 - 요구사항 진행2

 

백문이불여일견 JS 피들을 확인한다. 데모 3을 베이스로 다음의 추가 요구사항을 이행한다. 

  • 체크 박스 처리
  • 특정 로우, 특정 열 이벤트 바인딩
  • 특정 로우 이벤트 바인딩

 

마치며, 다음 시리즈

인터넷에 돌아다니는 게시판 페이징 테마(CSS)를 사용하여 추가 데모를 시연할 것이다. 또한 데모에서 불편한 사항을 개선할 예정이다. 당장 눈에 보이는 불편한 사항은 다음과 같다.

  • 사용자는 페이징 1~10, Prev, Next DOM의 이벤트 바인딩을 해야한다. 
  • 사용자는 체크 박스의 이벤트 바인딩을 해야한다.
  • 사용자는 테이블 로우 클릭 이벤트 바인딩을 해야한다.
  • 라이브러리에서 제공하는 기타 유틸 함수가 부족하다.

다음 시리즈는 위의 열거한 사항을 기반으로 추가 분석을 실시하여, 라이브러리 최적화 작업을 진행한다. 진행한 결과물에 테마를 입혀 데모를 시연한다. 

 

Posted by 동팡

목차

  • 들어가며
  • Vault Docker 환경 구성
  • 참고문헌

 

들어가며

본 게시물은 Vault 공식 문서의 내용과 필자의 생각을 정리하였다. Vault는 무엇인지, 어디에 사용하는지, 왜 사용하는지, 어떻게 사용하는지 서술한다. Vault를 처음 접하는 인원은 Vault의 흐름과 골격을 이해할 수 있는 시간을 갖는다. 또한 Vault를 실질적으로 사용해보는 시간을 갖는다. Vault는 Dev 서버 모드를 지원한다. Dev 서버 모드는 사전 설정이 되어있는 데모 서버라고 생각하면 좋다. 사용자는 해당 데모 서버에서 Vault를 학습할 수 있다. 시간이 괜찮으면 Vault HA를 구성한다(시간이 있으면...). 

 

본 게시물의 시리즈는 다음과 같다.

 

Vault Docker 환경 구성

[개요]

yum 패키지 설치 기능을 사용하여 Vault 환경을 구성할 수 있다. 이번에는 Docker를 활용하여 Vault 환경을 구성한다. 해당 환경을 구성하기 위해 Docker는 당연히 사용하며, Docker-Compose까지 사용한다. 디렉토리의 구조는 아래와 같다. 환경 구성 후 가동 및 테스트 API만 호출한다.

vault
 |
 -- data  
 |
 -- config
 |    | 
 |    -- config.hcl
 |
 -- docker-compose.yml

 

[Docker 설치 및 기본 설정]

Docker는 별도의 설정을 하지 않을 경우 root 권한으로만 가동할 수 있다. 아래와 같이 설정하면 user 권한에서 Docker를 제어할 수 있다. 

$yum install docker
$yum install docker-compose

$groupadd docker
$usermod -aG docker $username
$sudo systemctl start docker
$sudo chmod 666 /var/run/docker.sock

Docker 명령어 확인 및 Vault 최신 이미지를 갖고온다. 

$docker ps -a

$docker pull vault

 

[Vault - Config]

 

#config.hcl

storage "raft" {
  path = "/vault/file"#vault dir in docker
  node_id = "node1"
}

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = "true"
}

api_addr="http://127.0.0.1:8200"
cluster_addr="https://127.0.0.1:8201"
ui = true

참고로 Listen address를 0.0.0.0을 설정해야 정상 가동할 수 있다.

 

[Vault - docker-comopse.yml]

#docker_compose.yml

version: "2"

services:
  vault:
	image: vault:latest
	container_name: vault
	volumes:
	  - ./vault/config:/vault/config
	  - ./vault/data:/vault/data
	ports:
	  - 8200:8200
	cap_add:
	  - IPC_LOCK
	command: vault server -config=/vault/config/config.hcl

 

[Vault - Docker 구동 및 테스트]

$docker-compose up -d

$curl http://127.0.0.1:8200/v1/sys/init | jq

//응답
{"initialized":false}

위의 응답은 정상적인 반환문이다. 

Vault 서버의 초기화 작업($vault operator init)을 하지 않았기 때문에 반환문의 값은 false이다. 

 

참고문헌

https://blog.exxeta.com/en/2019/12/20/setup-hashicorp-vault-on-docker/
https://stackoverflow.com/questions/50031086/why-does-vault-by-hashicorp-require-the-ipc-lock-capability-to-be-enabled
https://stackoverflow.com/questions/45171564/using-vault-with-docker-compose-file
https://hub.docker.com/_/vault
https://github.com/hashicorp/vault/issues/441

 

Posted by 동팡

목차

  • 들어가며
  • Vault Real 서버 튜토리얼
  • 참고문헌

 

들어가며

본 게시물은 Vault 공식 문서의 내용과 필자의 생각을 정리하였다. Vault는 무엇인지, 어디에 사용하는지, 왜 사용하는지, 어떻게 사용하는지 서술한다. Vault를 처음 접하는 인원은 Vault의 흐름과 골격을 이해할 수 있는 시간을 갖는다. 또한 Vault를 실질적으로 사용해보는 시간을 갖는다. Vault는 Dev 서버 모드를 지원한다. Dev 서버 모드는 사전 설정이 되어있는 데모 서버라고 생각하면 좋다. 사용자는 해당 데모 서버에서 Vault를 학습할 수 있다. 시간이 괜찮으면 Vault HA를 구성한다(시간이 있으면...). 

 

본 게시물의 시리즈는 다음과 같다.

 

Vault Real 서버 튜토리얼

[Vault 최종 디렉토리 구성]

Vault 디렉토리에 존재하는 파일

  • config.hcl: Vault 엔진 설정 정보
  • data: 데이터를 보관하는 장소
  • keyinfo.txt: SSS 알고리즘으로 분할한 키와 Root 토큰 정보
  • start.sh: Vault를 기동하기 위한 쉘 스크립트
  • stop.sh: Vault를 종료하기 위한 쉘 스크립트

굵게 표시한 것은 필수적으로 구성해야 하는 것이며, 기울여서 표시한 것은 편리를 위해 구성한 것이다. 그러나 프로덕트 환경에서는 keyinfo.txt 파일은 별도 디바이스에 보관해야 한다. 

 

[환경 구성]

CentOS 기준, yum 명령어를 사용하여 Vault를 설치한다. 설치방법은 Vault Tutorial 페이지에서 확인할 수 있다.

$mkdir vault

$cd vault

$vi config.hcl
	
    storage "raft" {
	  path = "./data"
	  node_id = "node1"
	}

	listener "tcp" {
	  address = "127.0.0.1:8200"
	  tls_disable = "true"
	}

	api_addr="http://127.0.0.1:8200"
	cluster_addr="https://127.0.0.1:8201"
	ui = true

$mkdir data

 

아래와 같이 VAULT_ADDR 환경변수 설정

$vi ./.bash_profile
	VAULT_ADDR='http://127.0.0.1:8200'
	export VAULT_ADDR
			
$source ./.bash_profile

// 또는

$ export VAULT_ADDR='http://127.0.0.1:8200'

// 확인 
$set  | grep VAULT_ADDR

 

[Vault 서버 가동 및 초기화]

Vault 서버 가동

"-config"에는 config.hcl의 경로를 지정한다.

$vault server -config=/home1/irteamsu/vault/config.hcl &

 

Vault 서버 초기화 및 Unsealing

Vault 서버 가동 후 최초 1회 초기화 작업을 진행한다. 해당 초기화 작업으로 Vault 설정정보에 따라 SSS 분할키와 Root 토큰을 생성한다. 별도의 설정 정보가 없을 경우 threshold는 (3, 5)이다. 즉 5개의 분할키를 부여하며, 원본을 만들기 위해서는 최소 3개의 분할키가 필요하다. 필자의 경우 프로덕트 환경이 아니기 때문에 아래의 분할키와 Root 토큰을 keyinfo.txt 파일에 보관하였다. 

$vault operator init

2021-06-15T16:43:36.202+0900 [INFO]  core: pre-seal teardown complete
		Unseal Key 1: Iv9H20vmAUMkDaAfyuar2rSWyGnZgvBOhQPl7df4lrib
		Unseal Key 2: G7SnjSZPOaZwkMX9rvCyK8ssdoIMAdAYu+oDo1b9uZ+j
		Unseal Key 3: jNfpkii4QiJsuIUVwkuIm1jjIz+bzh5oalsfevXGs5Wc
		Unseal Key 4: NnruCilytFhgdQ4zCQmKeSrrV4e8Ijws663trVHAamyl
		Unseal Key 5: pqb+bP30/wAraxLITsnjza1ibDyqKIcAHTEZkDzf902W

		Initial Root Token: s.ZPBA2pFTJEtmlaUfIEHk1Lq6

		Vault initialized with 5 key shares and a key threshold of 3. Please securely
		distribute the key shares printed above. When the Vault is re-sealed,
		restarted, or stopped, you must supply at least 3 of these keys to unseal it
		before it can start servicing requests.

		Vault does not store the generated master key. Without at least 3 key to
		reconstruct the master key, Vault will remain permanently sealed!

		It is possible to generate new unseal keys, provided you have a quorum of
		existing unseal keys shares. See "vault operator rekey" for more information.

서버 가동, 초기화 작업을 완료하였다. 그러나 Vault는 아직 "Sealed" 상태이다. 위의 분할키는 Vault의 상태를 "Unsealed" 상태로 전이할 수 있다. 

$vault operator unseal G7SnjSZPOaZwkMX9rvCyK8ssdoIMAdAYu+oDo1b9uZ+j
$vault operator unseal jNfpkii4QiJsuIUVwkuIm1jjIz+bzh5oalsfevXGs5Wc
$vault operator unseal NnruCilytFhgdQ4zCQmKeSrrV4e8Ijws663trVHAamyl

위의 작업을 완료하면 Sealed false 값을 확인할 수 있다. Root 토큰으로 로그인한 사용자는 "vault operator seal" 명령을 사용하여 Vault 서버에 Sealing을 할 수 있다. root 사용자의 경우, vault operator seal을 통해 sealing을 다시 할 수 있다. 이제 Vault를 마음껏 사용할 수 있다. 

 

[Vault 가동 스크립트 작성]

Vault를 기동할 때 "Unsealing" 절차는 필수이다. 해당 절차의 편리를 위해 스크립트를 작성한다. 스크립트는 아래와 같다.

start.sh

#!/bin/bash

partOfKey1=$1
partOfKey2=$2
partOfKey3=$3

if [ "$#" != 3 ]
then
		echo "argument is not 3"
		exit
else
		vault server -config=/home1/irteamsu/vault/config.hcl &

		sleep 1
		echo -ne '\n'

		vault operator unseal $1
		vault operator unseal $2
		vault operator unseal $3
fi

 

stop.sh

#!/bin/bash

pid=`ps aux | grep 'vault server' | grep -v grep | awk '{print $2}'`

if [ -z "$pid" ]
then
	echo "server is not running"
else
	echo "shutdown..."
	kill -9 $pid
fi

추가 분기를 사용하여 kill -9, -15 옵션을 결정할 수 있다. 필자는 그냥 "-9"를 사용하였다. 프로덕트 환경의 경우, "-15" 옵션을 위한 분기 사용을 권한다. 

 

[Vault 사용 - REST API - 서버 상태 확인]

$curl http://127.0.0.1:8200/v1/sys/init | jq

 

[Vault 사용 - REST API - auth method 활성화]

(Root 토큰을 환경변수에 지정한다(VAULT_TOKEN).)

$curl -X POST -H "X-Vault-Request: true" -H "X-Vault-Token: $VAULT_TOKEN" -d '{"type":"approle","description":"","config":{"options":null,"default_lease_ttl":"0s","max_lease_ttl":"0s","force_no_cache":false},"local":false,"seal_wrap":false,"external_entropy_access":false,"options":null}' http://127.0.0.1:8200/v1/sys/auth/approle

 

[Vault 사용 - REST API - 정책 추가]

$curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--request PUT \
--data '{"policy":"# Dev servers have version 2 of KV secrets engine mounted by default, so will\n# need these paths to grant permissions:\npath \"secret/data/*\" {\n  capabilities = [\"create\", \"update\"]\n}\n\npath \"secret/data/foo\" {\n  capabilities = [\"read\"]\n}\n"}' \
-D - http://127.0.0.1:8200/v1/sys/policies/acl/my-policy

커맨드 명령어를 사용하여, 추가한 정책을 확인한다.

$vault policy list

 

[Vault 사용 - REST API - SecretPath 생성]

$curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--request POST \
--data '{ "type":"kv-v2" }' \
-D - http://127.0.0.1:8200/v1/sys/mounts/secret

 

[Vault 사용 -REST API - 정책 기반으로 Role 생성]

$curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--request POST \
--data '{"policies": ["my-policy"]}' \
-D - http://127.0.0.1:8200/v1/auth/approle/role/my-role

커맨드 명령어를 사용하여, 추가한 Role을 확인한다.

$vault list auth/approle/role
$vault read auth/approle/role/my-role
$vault read auth/approle/role/my-role/role-id

REST API를 사용하여, 추가한 Role을 확인한다.

$curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
http://127.0.0.1:8200/v1/auth/approle/role/my-role/role-id | jq -r ".data"

 

[Vault  사용 - REST API - Secret-ID 생성/확인]

$curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--request POST \
http://127.0.0.1:8200/v1/auth/approle/role/my-role/secret-id | jq -r ".data"
	
//응답
{
  "secret_id": "c7d3485f-403d-0c57-320d-3eb97697d85a",
  "secret_id_accessor": "b0e25931-9e7e-4a4d-80b6-11a0087671d5",
  "secret_id_ttl": 0
}

 

[Vault 사용 - REST API - AppRole 인증 및 클라이언트 토큰 반환]

ㄴㅇ

$curl --request POST \
--data '{"role_id": "93bfae74-3356-9b0d-08b3-8c2cdc76f55a", "secret_id": "c7d3485f-403d-0c57-320d-3eb97697d85a"}' \
http://127.0.0.1:8200/v1/auth/approle/login | jq -r ".auth"


// 응답
{
  "client_token": "s.DOqgLc4sIVKJjmgVIzCIJVeZ",
  "accessor": "eRxKIqBlT3eKsKRCfvt80sLO",
  "policies": [
	"default",
	"my-policy"
  ],
  "token_policies": [
	"default",
	"my-policy"
  ],
  "metadata": {
	"role_name": "my-role"
  },
  "lease_duration": 2764800,
  "renewable": true,
  "entity_id": "d624b3eb-0e52-fb88-439f-f456a8519fa2",
  "token_type": "service",
  "orphan": true
}

 

[Vault 사용 - REST API - 클라이언트 토큰으로 Secret 저장]

먼저 클라이언트 토큰을 환경변수에 저장한다. 

$export VAULT_TOKEN="s.DOqgLc4sIVKJjmgVIzCIJVeZ"

 

REST API를 사용하여 Secret을 저장한다.

$curl \
--header "X-Vault-Token: $VAULT_TOKEN" \
--request POST \
--data '{ "data": {"password": "my-long-password"} }' \
http://127.0.0.1:8200/v1/secret/data/creds | jq -r ".data"

이렇게 Dev 모드가 아닌 실제 서버를 설정하였다. 해당 서버는 vault 커맨드를 사용하여 제어할 수 있으며, REST API를 사용하여 제어할 수 있다. 본 게시글은 curl 명령과 함께 REST API를 사용하였다. 실질적으로 프로덕션 레벨에서는 클라이언트 토큰을 생성 후, Secret을 제어하는 것이다. 해당 사항은 Java/Spring환경을 구성한 후 테스트 예정이다.  

 

참고문헌

Vault 공식 튜토리얼 사이트

Posted by 동팡

목차

  • 들어가며
  • Vault Dev 서버 모드 튜토리얼
  • 참고문헌

 

들어가며

본 게시물은 Vault 공식 문서의 내용과 필자의 생각을 정리하였다. Vault는 무엇인지, 어디에 사용하는지, 왜 사용하는지, 어떻게 사용하는지 서술한다. Vault를 처음 접하는 인원은 Vault의 흐름과 골격을 이해할 수 있는 시간을 갖는다. 또한 Vault를 실질적으로 사용해보는 시간을 갖는다. Vault는 Dev 서버 모드를 지원한다. Dev 서버 모드는 사전 설정이 되어있는 데모 서버라고 생각하면 좋다. 사용자는 해당 데모 서버에서 Vault를 학습할 수 있다. 시간이 괜찮으면 Vault HA를 구성한다(시간이 있으면...). 

 

본 게시물의 시리즈는 다음과 같다.

 

Vault Dev 서버 모드 튜토리얼

CentOS 기준, yum 명령어를 사용하여 Vault를 설치한다. 설치방법은 Vault Tutorial 페이지에서 확인할 수 있다.

 

[Vault 서버 가동]

$vault server -dev &

$vault server -dev & 후의 vault 로그


[Vault 환경 변수 설정]

환경 변수 설정은 필수가 아니다. 그러나 CLI 환경에서 Vault 명령을 쉽게 사용하기 위해 환경변수를 설정한다. CentOS는 .bash_profile를 통해 환경변수를 영구 설정할 수 있지만, 그렇게 하지 않는다.

 

위의 로깅된 Root Token과 HOST에 맞게 환경변수를 설정한다.

$export VAULT_ADDR='http://127.0.0.1:8200'
$export VAULT_TOKEN="s.XmpNPoi9sRhYtdKHaQhkHP6x"

환경변수 조회 및 확인 

$ set | grep VAULT

[Vault 사용 - Secret 등록]

$vault kv put secret/hello foo=world 

설명

  • Secret, foo=world 등록
  • kv: key-value storage에 접근할 때 사용
  • put secret/hello: secret/hello라는 secret 생성(위치? 엔드포인트?)
  • foo=world: foo라는 파라메터와 함께 secret 생성
  • +) vault kv put 명령은 생성할 때 사용하지만 갱신/대체 작업에도 사용

[Vault 사용 - Secret 조회]

$vault kv get secret/hello

$vault kv get -field=foo secret/hello

$vault kv get -format=json secret/hello

설명

  • get secret/hello: secret/hello에 위치하는 Secret 조회
  • -field=foo: foo 필드의 값만 조회
  • -format=json: JSON 형식으로 조회(json, yaml, table 형식으로 조회 가능, 디폴트 값은 table)

[Vault 사용 - Secret 폐기]

$vault kv delete secret/hello

설명

  • delete: secret/hello에 위치하는 것을 폐기

[Vault 사용 - Secret Engine 생성]

Key/Value형식의 Secret Engine 생성 및 조회

$vault secrets enable -path=kv kv

$vault secrets list

Vault Secret List

[Vault 사용 - 방금 생성한 Secret Engine에 Secret 등록]

$vault kv put kv/my-secret value="s3c(eT"

$vault kv get kv/my-secret

$vault kv delete kv/my-secret

$vault kv list kv/

$vault secrets disable kv

$vault secrets enable -path=kv kv

[Vault 사용 - 토큰 생성]

토큰을 생성/로그인/폐기한다.

$vault token create

$vault login s.w5cdTSBCLaFbHAu8lHDfHpCW

$vault token revoke s.w5cdTSBCLaFbHAu8lHDfHpCW

[Vault 사용 - 토큰의 정책 설정]

$vault policy list 

$vault policy read default

$vault policy write my-policy - << EOF
# Dev servers have version 2 of KV secrets engine mounted by default, so will
# need these paths to grant permissions:
path "secret/data/*" {
  capabilities = ["create", "update"]
}

path "secret/data/foo" {
  capabilities = ["read"]
}
EOF

$vault token lookup

$vault token create -policy=my-policy

$export VAULT_TOKEN="$(vault token create -field token -policy=my-policy)"

$vault kv put secret/creds password="my-long-password"
$vault kv put secret/creds password="my-long-password-1"

$vault kv get secret/creds   //(안됨)

$export VAULT_TOKEN="s.kZw14yyEbEIkDSRRSLEsVM8M"    //(root 토큰)
$vault kv get secret/creds(됨)

설명

  • 토큰 생성 시 정책을 적용할 수 있다.
  • 정책은 Secret에 대해 접근제어를 실시한다. 
  • 정책 "my-policy의 설명은 다음과 같다.
    • "secret/data 이하의 경로에 대해 create와 update 작업을 실시할 수 있다(udpate는 put을 통한 갱신 작업).
    • "secret/data/foo에 해당하는 경로는 read 작업을 실시할 수 있다.
  • 다음의 설명은 생략한다.

[Vault 사용 - AppRole]

위의 "토큰 생성" 부분에서 Vault는 사용자의 토큰을 인증 절차 없이 생성한다. 이유는 다음과 같다. 현재 적용된 토큰은 "루트 토큰"이다. 그래서 인증 절차 없이 사용자의 토큰을 생성할 수 있다. 그러나 일반적인 상황에서는 인증 절차가 필요하다. 대표적인 인증 절차는 ID/PW 방식을 사용할 수 있다. Vault의 AppRole은 식별값(ID)/인증값(PW)을 이용해서 토큰을 발급한다. 또한 정책을 적용할 수 있으며, 토큰에 적용 가능하다.

// approle 확인
$vault auth list | grep 'approle/'

// 리스트업이 되지 않을 경우, approle 활성화
$vault auth enable approle

// approle 확인
$vault auth list | grep 'aaprole/'

//approle에 my-role(사용자) 생성
$vault write auth/approle/role/my-role \
secret_id_ttl=10m \
token_num_uses=10 \
token_ttl=20m \
token_max_ttl=30m \
secret_id_num_uses=40 \
token_policies=my-policy

// approle 조회
$vault list auth/approle/role
$vault read auth/approle/role/my-role
$vault read auth/approle/role/my-role/role-id
$vault read -field=role_id auth/approle/role/my-role/role-id

// ROLE_ID(식별자) 환경 변수 적용 및 조회
$export ROLE_ID="$(vault read -field=role_id auth/approle/role/my-role/role-id)"
$set | grep -e VAULT -e ROLE_ID

// SECRET_ID 설정(AppRole 인증을 위한 비밀번호 개념과 비슷하다.)
// SECRET_ID(인증값) 환경 변수 적용 및 조회
// vault write를 별도로 진행해도 괜찮다.
$export SECRET_ID="$(vault write -f -field=secret_id auth/approle/role/my-role/secret-id)"
$set | grep -e SECRET_ID -e VAULT -e ROLE_ID

// 토큰 요청
// approle 인증 완료 후, 토큰을 반환받을 수 있다.
$vault write auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID"

 

참고문헌

Vault 공식 튜토리얼 사이트

Posted by 동팡

목차

  • 들어가며
  • 개요 
  • 용어 정의
  • 아키텍처 개요

 

들어가며

본 게시물은 Vault 공식 문서의 내용과 필자의 생각을 정리하였다. Vault는 무엇인지, 어디에 사용하는지, 왜 사용하는지, 어떻게 사용하는지 서술한다. Vault를 처음 접하는 인원은 Vault의 흐름과 골격을 이해할 수 있는 시간을 갖는다. 또한 Vault를 실질적으로 사용해보는 시간을 갖는다. Vault는 Dev 서버 모드를 지원한다. Dev 서버 모드는 사전 설정이 되어있는 데모 서버라고 생각하면 좋다. 사용자는 해당 데모 서버에서 Vault를 학습할 수 있다. 시간이 괜찮으면 Vault HA를 구성한다(시간이 있으면...). 

 

본 게시물의 시리즈는 다음과 같다.

 

개요

HashiCorp의 Vault는 민감한 데이터를 안전하게 저장하는 저장소이다. 민감한 데이터의 종류는 다음과 같다. 1)Secret 2)Credential 3)Password 4)Enctyption-Key 등이다. 이와 같이 기밀성이 요구되는 정보는 DBMS에 단순 저장하면 안 된다. Vault는 위의 개체들을 암호화하여 안전하게 저장한다. 

 

용어 정의

Storage Backend

  • Vault의 암호화된 Secret을 보관하는 곳이다. 

Barrier

  • Barrier는 Vault의 일부 구성요소를 감싸고 있다. 때문에 Vault와 Storage Backend 간의 통신은 Barrier를 거쳐야 하며, Barrier가 프록시 역할(대문 역할)을 한다. Barrier의 구성요소들은 서로 Trust 한 관계를 유지/성립한다.
  • Vault는 암호화된 데이터만 밖으로 나오게 한다.
  • Vault는 Barrier의 상태가 “unsealed”가 되어야 접근할 수 있다.

Secret Engine

  • Secret의 관리를 책임진다.
  • Secret 관련 작업은 Secret Engine으로 전달하고 Engine의 구현체마다 상이한 방식으로 저장한다. 
  • Secret Engine의 인터페이스를 활용하여 DB, File System 또는 유저가 정의한 방식으로 저장한다.

Audit Device

  • 모든 Vault의 Request/Respones는 Audit Device에 의해 감사 로깅이 된다. 

Auth Method 

  • Vault에 접근하는 클라이언트를 인증한다.
  • 인증된 클라이언트의 토큰을 반환한다.

Client Token

  • HTTP에서의 세션 ID와 같은 토큰을 반환한다.
  • Vault의 REST API를 사용하는 경우 HTTP 헤더에 토큰을 적재한다.

Secret

  • Vault에서 관리하는 비밀 객체이다.
  • Secret은 일정 주기를 가지며, 해당 주기가 만료되면 폐기해야 한다.

 

 

아키텍처 개요

High level overview of vault

[전체 흐름]

 

Vault는 기밀성이 요구되는 데이터(이하 Secret)를 안전하게 보관하는 저장소이다. Vault를 사용하기 위해서는 “unsealing” 초기화 작업이 필요하다. Vault 서버 초기화 시 Vault“sealed” 상태이다. “Unsealed-Key”는 “sealed” 상태를 “unsealed” 상태로 전이할 수 있다. “unsealed” 상태가 되면 Barrier안에 있는 Vault의 모든 기능 및 구성요소에 원활하게 접근할 수 있다.

 

Vault에서 실질적으로 Secret을 암/복호화하는 키는 "Encryption-Key"이다.

1) "Master-Key"는 “Encryption-Key”를 암호화한다.

2) "Unsealed-Key"는 “Master-Key”를 암호화한다.

3) “Unsealed-key”SSS(Shamir Secret Sharing) 알고리즘으로분할 보관한다.

 

키들의 관계는 아래 도식화 자료와 같다.

 

Vault Crypto-Key mechanism

“Encryption-Key”는 “Secret”을 안전하게 암/복호화한다. “Secret”들은 “Storage backend”에 보관하며, “Secret”에 접근하기 위해서는 REST API를 사용한다. REST API는 Create, Register, Rotate, Destroy 등의 명령을 제공하며, "Secret"을 제어할 수 있다. REST API를 사용하기 위해서는 "토큰 인증"과 "토큰에 상응하는 정책 인가" 작업이 필요하다. 

 

[고가용성]

Vault는 HA를 구성할 수 있다. HA의 구조는 Active-Standby 구조이다. 오픈 소스 버전의 경우 Standby는 Read-Only 기능을 제공하지 않는다. Standby에 요청이 들어오면 요청을 Active에 포워딩한다. Read-Only 기능은 엔터프라이즈에서 제공한다. 그래도 오픈 소스 버전에서 Hot-Standby를 제공하기 때문에 Failover는 문제없다. 제일 큰 단점은 Standby의 read-only의 미지원이다. Product 레벨에서 Vault를 사용하기 위해서는 해당 기능이 제일 필요할 것 같다.

 

Replication 방식은 PostgreSQL의 방식과 같이 Active-Standby 구성이며, Active의 변경/추가/삭제된 것에 대한 로그(WAL, Write Ahead Log) Standby에 전송한다. 데이터 처리에 있어 보통의 DBMS과 같이 WAL 기법을 사용하는 것 같다.

 

[보안 특성]

  • Vault와 클라이언트 상호 인증은 제공하지 않는다. 다만 서버에서 제공하는 네트워크 계층 보안, TLS를 제공한다.
  • 정상 토큰을 보유한 주체는 리소스("Secret")에 접근할 수 있다.
  • 토큰 인가 정책은 리소스에 접근하는 주체를 제어한다.
  • Secret에 접근하는 모든 행위를 감사 로깅한다.
  • "Secret"을 암호화하여 Storage Backend에 저장한다.
  • Vault의 Barrier를 통과하는 모든 요청과 반환은 AES-256(GCM)으로 암호화한다. IV의 경우, 자동으로 임의로 생성한다.
  • SSS(Shamir Secret Sharing) 알고리즘으로 MasterKey를 분리 보관한다.

 

[Key 회전(갱신)]

rekey 명령은 Unsealed-KeyMaster-Key를 갱신한다. 갱신된 Master-KeyEncryption-Key를 재암호화하며,갱신된 Unsealed-Key SSS에 의해 다시 분리하여 보관하여야 한다. 

rotate 명령은 Encryption-Key를 갱신한다. 기존 Encryption-Key는 별도의 keyring에 보관한다. 후의 요청들은 새로운 Encryption-Key로 암호화를 한다. Keyring에 있는 Encryption-Key는 복호화 용도에만 사용한다. 이와 같이 사용하면 re-encryption을 수행하지 않아도 괜찮다(keyring에 예전 Encryption-Key를 다 보관하고 있다. 갱신에 의미가 있나?).

 

[정책]

사용자는 Identifier/Authentication값을 요청한다. 요청 성공 시 토큰을 반환받는다. 해당 토큰은 다음과 같이 화이트 리스트 정책을 갖고 있다

# This section grants all access on "secret/*". Further restrictions can be
# applied to this broad policy, as shown below.
path "secret/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}
 
# Permit reading secret/foo/bar/teamb, secret/bar/foo/teamb, etc.
path "secret/+/+/teamb" {
  capabilities = ["read"]
}
 
# Policies can also specify allowed, disallowed, and required parameters. Here
# the key "secret/restricted" can only contain "foo" (any value) and "bar" (one
# of "zip" or "zap").
path "secret/restricted" {
  capabilities = ["create"]
  allowed_parameters = {
    "foo" = []
    "bar" = ["zip", "zap"]
  }
}
 

 

Posted by 동팡

목차

  • 개요
  • Spring Boot 마이그레이션 결과 조건
  • JSP 적용 문제
  • Multiple-DataSource 문제
  • Tomcat / JNDI DataSource 문제
  • External Library(JAR) 문제
  • Tomcat/ExecutableJAR 동시 배포 가능의 문제
  • 쉘 스크립트 작성
  • 완성본
  • 참고문헌

개요

드디어 회사 솔루션 최소 Java 버전이 6에서 7로 변경됐다(8 쓰고 싶은데...). 현재 관리하는 Spring 웹 프로젝트에 Boot를 적용한다. 최소 지원 Java 버전이 7로 올라감에 따라 기존 Spring Legacy 기반의 프로젝트를 Spring Boot 기반의 프로젝트로 마이그레이션을 진행한다. Spring Boot는 Boot의 다양한 기능을 활용하여 어플리케이션을 개발할 수 있는 장점이 존재하지만, 주요 기대효과는 배포 및 설치의 간결함이다. Spring Boot의 경우, 내장 Tomcat을 이용하여 WAS를 가동할 수 있다. 해당 기능을 통해 설치 간결함을 제공할 수 있다. 

 

Spring Boot 마이그레이션 결과 조건

당연한 조건이지만, 최종 조건은 프로그램의 API 기능의 변동없는 정상적인 가동이다. 해당 조건을 아래와 같이 명시할 수 있다. 

 

[JSP 템플릿의 사용]

Spring Boot는 타임리프, 프리마커, JSP를 지원한다. 그러나, JSP의 사용을 권고하지 않는다. 그런데 뷰 템플릿을 바꾸기에는 프로젝트의 위험도가 올라간다. 

어떻게 해결할 것인가?

 

[Multiple-DataSource]

Spring Boot의 경우, application.yaml(properties)를 통해 DataSource를 설정할 수 있다.

그런데 2개 이상의 DataSource를 지정할 때 어떻게 할까?

 

[외장 Tomcat에서의 정상 작동]

솔루션이다보니, 사이트에서 사용하는 WAS Container를 사용할 때가 빈번하다...

한 번의 빌드를 통해 동시에 내장/외장 Tomcat(WAS Container)를 지원하면 그야말로 베스트이다.

어떻게 할 수 있을까?

 

[외장 Tomcat에서의 JNDI DataSource 사용]

외장 Tomcat(WAS Container)를 사용할 때 해당 Container의 DBCP를 사용해야 하는 경우가 다반사이다. 다음과 같이 구성이 필요하다.

1) Multiple-DataSource, 2) 외장 Tomcat, 3) 외장 Tomcat JNDI DataSource를 사용한다.

 

[ExecutableJAR/WAR 외의 Classpath 지정을 통해 라이브러리 변경]

Spring Boot는 빌드된 JAR/WAR 안에 종속 라이브러리가 존재한다.

만약 사용하는 Library가 변경됐을 경우 재 빌드가 필요하다. 

라이브러리의 변경만 필요하는 경우가 종종 존재한다. 

즉 External Library를 사용할 수 있어야 한다.

어떻게 해결할 것인가?

 

[쉘 스크립트 작성]

개발자 입장에서는 프로그램의 가동/상태 확인/정지 등을 쉽게 할 수 있다.

그러나 설치 인원 또는 고객은 얘기가 다르다. 고객의 경우, 프로그램을 가동함에 있어 지식 습득이 필요하면 절대 안 된다.

가동/상태/정지를 쉽게 할 수 있도록 별도의 쉘 스크립트를 제공한다. 

 

[기타 사항]

본문에서는 다루지 않지만 다음의 사항을 고민하여 마이그레이션을 진행하였다.

  • web.xml 마이그레이션(web.xml -> ServletContextInitializer)
  • pom.xml 마이그레이션(Spring Boot 디펜던시 및 빌드 설정)
  • ClassPath를 통한 리소스 획득 -> Spring ResourceLoader를 통한 리소스 획득
  • Custom-CORSFilter -> Spring-CORSFIlter(Boot에서 정상적으로 CORS 필터링이 되지 않아 Spring-CORSFilter 변경)
  • Spring Legacy 테스트 -> Spring Boot 테스트
  • WebConfig.class -> application.yaml(리소스, 뷰 리졸버, AOP 등의 일부 Servlet Web ApplicationContext 계층의 설정들을 application.yaml로 이동)
  • AppConfig.class -> application.yaml(AOP, MyBatis 등의 서비스 계층의 "환경" 관련 설정들이 application.yaml로 이동)
  • (그러나 MyBatis의 경우, 복잡한 설정과 DBMS 별 설정 분기들이 존재하여 MyBatis Spring AutoConfiguration은 flase 설정을 하였다.
  • (서비스 계층에서 사용하는 즉, 서비스 계층 로직을 위한 빈 Config는 그대로 보존)
  • (web.xml이 사라지는 과정은 해당 링크를 확인한다.)

JSP 적용 문제

Spring Boot는 타임리프, 프리마커, JSP를 지원한다. 그러나, JSP의 사용을 권고하지 않는다. 왜 권고하지 않을까? 자세한 사항은 다음의 스프링 사이트에서 확인할 수 있다. 간략하게 정리하면  다음과 같다.

 

그래도 우리는 Boot에서 JSP를 사용해야 한다. 다음과 같이 진행한다.

아래와 같이 JSP 관련 디펜던시 설정이 필요하다.  

	<dependency>
		<groupId>org.apache.tomcat.embed</groupId>
		<artifactId>tomcat-embed-jasper</artifactId>
	</dependency>

	<!--JSP에서 JSTL를 사용할 경우 -->

	<dependency> 
		<groupId>javax.servlet</groupId> 
		<artifactId>jstl</artifactId> 
	</dependency>

jasper 설정은 무엇이고 왜 필요할까?

jasper는 톰켓의 JSP 엔진이다. jasper는 JSP 파일을 파싱하여 서블릿 코드로 변환하는 작업을 진행하며, JSP 파일의 변경을 감지하여 변환 작업을 수행한다. 

 

Spring boot에서 톰켓 관련 디펜던시를 알아서 설정해주는데 위의 항목을 제외하였다. 아래와 같이 타고 타고 올라가면 jasper 관련 디펜던시가 없는 것을 확인할 수 있다.

 

spring-boot-starter-web => spring-boot-starter-tomcat => tomcat-embed-core

 

Multiple-DataSource 문제

Spring Boot에서는 application.yaml(properties)를 이용하여 DataSource를 설정할 수 있다. 그런데.. 2개 이상은?

application.yaml의 spring.datasource. ...을 이용하여 설정하지 못한다. 즉, 우리는 별도의 DataSourceConfig.class 또는 xml 파일을 생성해야 한다. 그리고 Spring Boot의 auto-configuration을 제거해야 한다. 다음과 같다(Transaction 자동 설정도 제거해버렸다. 별도 설정을 진행해야 한다.).

 

[application.class]

@SpringBootApplication( exclude = {
	DataSourceAutoConfiguration.class,
	DataSourceTransactionManagerAutoConfiguration.class
})

[DataSourceConfig.class]

	@Primary
	@Bean(name="***DataSource")
	@ConfigurationProperties(prefix="com.example.product.***.datasource.hikari")
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();
	}

	@Bean(name="****DataSource")
	@ConfigurationProperties(prefix="com.example.product.****.datasource.hikari")
	public DataSource ****DataSource() {
		return DataSourceBuilder.create().build();
	}

[application.yaml]

com:
  example:
    product:
      ***:
        datasource:
          hikari:
            driver-class-name: com.tmax.tibero.jdbc.TbDriver
            jdbc-url: jdbc:tibero:thin:@10.10.10.174:8629:tibero
            username: user
            password: pass
            minimum-idle: 50
            maximum-pool-size: 200
            connection-test-query: select 1 from dual
            connection-timeout: 3000
            idle-timeout: 600000
            max-lifetime: 1800000
            
       
      ****:          
        datasource:
          hikari:
            driver-class-name: com.tmax.tibero.jdbc.TbDriver
            jdbc-url: jdbc:tibero:thin:@10.10.10.174:8629:tibero
            username: user
            password: pass
            minimum-idle: 50
            maximum-pool-size: 50
            connection-test-query: select 1 from dual
            connection-timeout: 3000
            idle-timeout: 600000
            max-lifetime: 1800000

Spring Boot는 DataSourceBuilder를 통해 편리하게 DataSource를 설정할 수 있다. 해당 클래스의 레퍼런스를 확인하면 다운 캐스팅 또는 @ConfigurationProperties를 통해 상세 설정을 할 수 있다. 캐스팅을 통해 값들을 설정하는 것보다는 properties 값들을 통해 해소하는 것이 좀 더 바람직하다. 그러면 2개의 DataSource Bean을 생성할 수 있으며 Primary을 통해 디폴트(우선순위) 빈을 설정한다. 

Tomcat / JNDI DataSource 문제

별도의 외장 WAS 컨테이너에 WAR를 배포하는 경우가 존재한다(매우). 필자가 관리하는 제품은 솔루션이다. 솔루션이다 보니 사이트에 적용/설치가 필요하기 때문에 사이트에서 사용하는 WAS 컨테이너를 사용해야 하는 경우가 많이 존재한다. 그래서 Spring Boot의 내장 톰켓을 사용하는 경우도 존재하지만 외장 WAS 컨테이너를 사용하는 경우도 존재한다.

 

아래와 같이 설정한다.

[pom.xml]

<project>
	...
    
    <packaging>war</packaging>
    
    ...
	
</project>

이렇게 설정하여도 다음과 같은 명령어를 실행할 수 있다.

 

java -jar application.war

 

그런데, 문제가 발생하였다. WAS 컨테이너의 DBCP를 사용해야 한다. 즉, JNDI를 이용하여 WAS에서 관리하는 DataSource를 호출해야 한다. 

다음과 같이 설정할 수 있으며, 이때, Java Config가 빛을 바란다. 그저 빛빛빛.

 

[DataSourceConfig.class]

	@Value("${com.example.product.datasource.using-jndi}")
	private boolean isJndiDataSource;
	
	@Primary
	@Bean(name="***DataSource")
	@ConfigurationProperties(prefix="com.example.product.***.datasource.hikari")
	public DataSource dataSource() {
    	if ( isJndiDataSource ) {
        	JndiDataSourceLookup lookup = new JndiDataSourceLookup();
			return lookup.getDataSource( "java:/comp/env/jdbc/***" );
        } else {
			return DataSourceBuilder.create().build();
        }
	}

	@Bean(name="****DataSource")
	@ConfigurationProperties(prefix="com.example.product.****.datasource.hikari")
	public DataSource ****DataSource() {
    	if ( isJndiDataSource ) {
			JndiDataSourceLookup lookup = new JndiDataSourceLookup();
			return lookup.getDataSource( "java:/comp/env/jdbc/****" );
        } else {
        	return DataSourceBuilder.create().build();
        }
	}
	

[application.yaml](위의 Multiple-DataSource에 아래 설정을 추가하였다.)

com:
  example:
    product:
      datasource:
        using-jndi: true

using-jndi 1개의 설정을 통해 WAS 컨테이너의 DBCP를 쓸지 내장 DBCP를 쓸지 결정할 수 있다. 이런 분기문으로 빈 설정을 할 수 있는 것이 JavaConfig의 제일 큰 장점 같다. 

External Library(JAR) 문제

솔직히, 사용하는 디펜던시(JAR)가 변경된다고 External Lib를 구성할 필요는 없다. 근데 우리는 다음과 같은 참 별로인 경우가 존재하였다.

  • 해당 JAR는 재 빌드 불가(코드 수정 후 다시 빌드 못함, 해당 JAR만 사용해야 함)
  • 해당 JAR는 File의 절대 경로를 파싱하여 같은 경로에 있는 HMAC 파일을 통해 무결성 검증을 실시한다.

JAR의 절대경로를 갖고 와 해당 경로에 있는 HMAC파일을 File 객체로 파싱 할 때 다음의 문제점이 생긴다. JAR 안에 있는 것에 대해 File 객체로 만들지 못한다. 당연하다... JAR안에 있는데 파일 시스템에서 어떻게 제대로 읽을 수 있겠나...

1번 조건 때문에 코드 수정은 못 한다. 결국 해당 라이브러리는 밖으로 빼야 한다. 

 

클래스 로더가 외부에 있는 디렉토리를 읽게 하기 위해 클래스 패스 설정 다음과 같이 해봤다.

 

1) ClassPath를 MANIFEST에 적용해보자.

현재 META-INF의 MANIFEST는 다음과 같다. 

 

 

위의 노란색 항목이 클래스패스로 설정된 것 같다. 그러면 내가 원하는 클래스 패스를 추가해야한다. 

 

메이븐 빌드 플러그인에서 클래스 패스를 설정할 수 있다. 

 

그런데... 설정에서 ClassPath 설정하는 것을 넣어봤자 제대로 적용이 되지 않는다. Spring Boot 빌드 플러그인이 모든 설정을 무시하는 것 같다.

 

(아래와 같이 MANIFEST의 Class-Path를 설정하고자 하였다.)

 

2) java 커맨드 -cp 

안 된다. ㅋㅋㅋㅋㅋㅋㅋㅋ 걍 안돼 

 

3) Spring의 PropertiesLauncher를 사용한다.

된다.

Spring Boot에서 WAR/JAR를 읽기 위해 JarLauncher/WarLauncher/PropertiesLauncher를 사용한다. 자세한 사항은 별도의 스터디가 필요할 것 같지만, 일단 사용자 클래스 패스를 설정하기 위해서는 PropertiesLauncher를 사용해야 한다. 그런데 WAR 빌드인데 PropertiesLauncher를 사용했는데 잘 되지 않았다. 몇몇 삽질을 해보니 PropertiesLauncher는 JAR 기준이라 클래스패스를 재설정해야 한다. 

 

다음과 같이 할 수 있다(리눅스의 경우, cp의 구분자를 ":"으로 지정한다.).

 

java -cp extlib/***.jar;$spring-boot-app.war -Dloader.path=WEB-INF/classes,WEB-INF org.springframework.boot.loader.PropertiesLauncher

 

된다...........ㅠㅠ 내 하루...

extlib/***.jar 때문에...

 

Tomcat/ExecutableJAR 동시 배포 가능의 문제

Spring Boot Maven 빌드에서 WAR 빌드하는 경우, 둘 다 배포 가능하다. 

Maven에서 Spring Boot 플러그인으로 빌드된 WAR는 WAS 컨테이너에 잘 배포된다.

Maven에서 Spring Boot 플러그인으로 빌드된 WAR는 java -jar app.war를 통해 잘 실행된다.

 

그리고 extlib 의존성 문제는 신경 안 써도 된다.

클래스 로더의 클래스 패스 룩업 때문에 위의 -cp 부분에 extlib/***.jar를 $spring-boot-app.war보다 먼저 지정하였다. 그래서 extlib도 걍 lib에 집어넣어 같이 빌드해버렸다. 

 

한번 빌드로 외장/내장 톰캣 가동이 가능하다.

 

 

쉘 스크립트 작성

음... 고객한테 위의 스크립트를 입력하여 어플리케이션 가동하라고는 못 하겠닼ㅋㅋㅋㅋㅋㅋ

각설하고 start/stat/stop은 아래와 같다.

 

아 맞다.

stat/stop을 정상적으로 진행하기 위해 application.pid가 필요하다. 

다음과 같이 application.class를 변경하면 spring boot application이 가동할 때 application.pid 파일을 생성한다.

 

[application.class]

	public static void main(String[] args) {
		SpringApplication springApplication = new SpringApplication( Application.class );
		springApplication.addListeners( new ApplicationPidFileWriter() );
		springApplication.run( args );
	}

 

[start.sh]

#!/bin/sh

app_pid=`cat ./application.pid`

if [ -z "$app_pid" ]
then
	java -cp extlib/***.jar:spring-boot-app.war -Dloader.path=WEB-INF/classes,WEB-INF org.springframework.boot.loader.PropertiesLauncher > /dev/null & 
	echo "Start ***App Server..."

else
        echo "Application(***App) is already running."
fi

 

[stat.sh]

#!/bin/sh
app_pid=`cat ./application.pid`

if [ -z "$app_pid" ]
then
	echo "Application(***App) is stopped"
else
	pstree -aAp $app_pid
	ps -ef | grep $app_pid | grep -v grep
	ss -anlp | grep $app_pid
	echo "Application(***App) is running"
fi

 

[stop.sh]

#!/bin/sh
app_pid=`cat ./application.pid`
opt=$1

if [ -z "$app_pid" ]
then
	echo "Application(***App) is stopped or pid is invalid."
else
	if [[ $opt = 'immediately' ]]
	then
		kill -9 $app_pid
		echo "Shutdown Application(***App) immediately"
	else
		kill -15 $app_pid
		echo "Shutdown Application(***App)"
	fi
fi

완성본

  • 위의 모든 디렉토리는 변경이 필요한 설정 값 및 리소스 파일들이다(메이븐 기준, src/main/resources 이하 변경이 되는 파일들이다.).
  • 위의 구성을 통해 Spring-Boot 어플리케이션을 배포한다.

 

음... Spring-Boot의 장점이 배포 및 설치의 간결함이었는데... 그랬는데... 간결한가;;? 음... 흠터레스팅.....ㅋㅋㅋㅋㅋㅋㅋ 

 

참고문헌

docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-jsp-limitations

www.baeldung.com/spring-xml-vs-java-config

www.baeldung.com/spring-boot-dispatcherservlet-web-xml

www.baeldung.com/java-web-app-without-web-xml

lelecoder.com/81

stackoverflow.com/questions/42154614/springboot-embedded-tomcat-and-tomcat-embed-jasper

www.baeldung.com/spring-boot-main-class

stackoverflow.com/questions/40499548/how-to-configure-additional-classpath-in-springboot

stackoverflow.com/questions/33956179/can-i-start-a-spring-boot-war-with-propertieslauncher

medium.com/saas-startup-factory/spring-boot-2-and-external-libs-with-the-propertieslauncher-fc49d2d93636

 

Posted by 동팡

목차

  • Servlet에서의 web.xml 대체
  • Spring-Legacy에서의 web.xml 대체
  • Spring-Boot에서의 web.xml 대체
  • 결론

Servlet에서의 web.xml 대체

Java6(Servlet 3.0 API)에서 XML 설정을 Java Config와 Annoation으로 대체하고자 하는 시도가 보인다. 

Servlet 3.0에서는 @WebServlet, @WebFilter, @WebListener를 통해 설정의 간소화를 도모하고 있다. 그러나 해당 어노테이션은 한계가 분명하게 보인다.

 

예를 들어, filter의 ordering은 어떻게 할 것인가?

session timeout은 어떻게 처리할 것인가?

security-role은 어떻게 처리할 것인가?

welcome-file-list는 어떻게 할 것인가?

 

annoation을 통해 해소하는데 한계가 있다. 

위와 같은 문제점을 해결하기 위해 ServletContainerInitializer 인터페이스의 ServletContext를 통해 web.xml 설정들을 대체할 수 있다. 쉽게 얘기해서 Spring의 xml 설정을 Java Config로 대체하는 것과 같은 맥락이다. 

 

ServletContainerInitilizer#onStartup은 서블릿 컨테이너에 의해 호출되어야 한다. 그러면 서블릿 컨테이너는 해당 메소드를 호출할 수 있을까? 그러니까 Tomcat은 ServletContainerInitilizer를 어떻게 호출할 수 있을까? ServletContainerInitilizer는 Service Provider Interface(SPI) 개념을 근간한다. 

SPI(Service Provider Interface)는 플러그인/모듈 등의 기능을 확장할 때 사용하는 인터페이스이다. 예는 다음과 같다.
Framework "F"는 "A"라는 API를 제공한다. Framework "F"는 여러 모듈 또는 플러그인이 존재한다. "A" API의 결과는 플러그인 또는 모듈에 따라 결과 값이 다르다. Framework "F"는 해당 플러그인 또는 모듈을 상황에 따라 선택하여 배포할 수 있다. 

SPI를 쓰기위해서는 META-INF/services에 정확한 패키지명과 클래스명을 다음과 같이 기재하며 호출할 수 있다. 

SP 지정: META-INF/services/com.service.MyServiceImpl
SP 호출: ServiceLoader<MyService> services = ServiceLoader.load(MyService.class);

 

ServletContainerInitilizer인터페이스 및 사용방법은 아래와 같다.

package javax.servlet;

...

public interface ServletContainerInitializer {

    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException;
}
@HandlesTypes({MyType.class})
  public class AppInitializer implements ServletContainerInitializer{
..
} 

Set<Class<?>>객체를 통해  javax.servlet.annotation.HandlesTypes에 지정한 클래스 객체를 갖고 올 수 있다. 만약에 해당 어노테이션에 인터페이스를 지정하면 해당 인터페이스를 구현한 모든 클래스를 조회하여 Set에 보관한다. 해당 클래스 객체들은 newInstance()를 통해 활용할 수 있다. 

위와 같은 ServletContainerInitlizer를 통해 프레임워크에 IoC 패턴을 삽입할 수 있다.

Spring-Legacy에서의 web.xml 대체

Spring에서의 web.xml은 WebApplicationInitializer를 통해 해결할 수 있다. 사용자는 WebApplicationInitializer을 구현하여 web.xml을 사용할 수 있다. 또는 AbstractAnnotationConfigDispatcherServletInitializer 추상 클래스를 사용한다. 단순하게 WebApplicationInitializer를 확장하여 사용할 경우, 모든 설정을 일일 설정해야 한다. 그러나, 스프링에서 제공하는 기본 구현 셋(AbstractAnnotationConfigDispatcherServletInitializer) 사용을 통해 간소화할 수 있다. 해당 추상 클래스를 을 통해 Servlet 계층의 스프링 설정, Servivce 계층의 스프링 설정, DispatcherServlet의 경로 지정, 필터 등록을 간결하게 할 수 있다.  

( WebApplicationInitializer, AbstractAnnotationConfigDispatcherServletInitializer의 사용 방법은 별도의 구글링을 통해 쉽게 확인할 수 있다.)

 

여기서 중요한 포인트는 SpringServletContainerInitilizer 클래스이다. 해당 클래스는 javax.servlet.ServletContainerInitializer 인터페이스를 구현한 클래스이다. 코드는 아래와 같다. 

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer) waiClass.newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}
}
  • HandlesTypes어노테이션에 WebApplicationInitializer 클래스를 지정하였다. 
  • Set에 있는 WebApplicationInitilizer을 구현한 모든 클래스에 대해 newInstance()를 하여 객체를 생성한다.
  • 생성한 객체에 onStartup 메소드를 실행한다. 

SpringServletContainerInitilizer 클래스에서 위와 같은 작업을 해주기 때문에 우리는 Spring에서 AbstractAnnotationConfigDispatcherServletInitializer 또는 WebApplicationInitializer를 구현해서 Servlet Container설정을 간결하게 할 수 있다.

Spring-Boot 에서의 web.xml 대체

Spring-Boot에서는 설정들이 아래와 같이 퍼졌다.

  • DispatcherServletAutoConfiguration을 통해 DispatcherServlet을 자동으로 설정한다.
  • application.yaml을 통한 DispatcherServlet 경로 설정(default는 "/"이다.)
  • FilterRegistrationBean, ServletListenerRegistrationBean을 통해 Servlet Filter와 Listener를 설정한다. 
  • welcome-file-list의 경우, Spring Container 설정으로 해소할 수 있다(ViewControllerRegister).
  • error의 경우, Spring-Boot만의 error 핸들링 스트럭쳐가 존재한다.

Servlet Listener의 경우, Spring ApplicationListener<ContextRefreshedEvent>으로 변경해도 무방할 경우, 해당 클래스로 마이그레이션 하는 것을 권고한다. 어플리케이션 초기화 설정은 Spring Bean을 사용하는 경우 다반사이기 때문이다. 즉 Spring Container 내에서의 초기화 설정을 진행하는 것이 편리하다. 

 

정리하면 Spring-Boot에서의 Servlet Container 설정은 이곳 저곳 퍼진 감이 있다. 그래도 Spring-Legacy와 비교하면 더 간편해졌다고 봐도 무방하다. 

 

결론

본문을 통해 web.xml이 어떻게 사라졌는지 확인할 수 있다. 설정 방법을 보러온 독자의 경우 많은 실망을 했을 수 있다. 다른 많은 블로그에서 다루니까 잘 찾아보길 바란다. 

  • Servlet에서는 web.xml은 SPI 기술을 통해 xml을 없앨 수 있었고,
  • Spring-Legacy에서는 HandlesTypes 어노테이션에 WebApplicationInitilizer을 지정하여 효율적으로 사용할 수 있었고,
  • Spring-Boot에서는 Auto-Configuration, Spring Container 대체, 빈 설정 등으로 간편하게 사용할 수 있었다.

여기서도 느낄 수 있지만 Spring(Boot)의 이해에는 먼저 Servlet의 이해가 정말 필요하다. Servlet 책 1권 정도는 보면 좋을 것 같다(엄진영의 Java 웹 개발 워크북을 추천한다. 시대별 Servlet 프로그래밍을 맛볼 수 있다.).

참고문헌

www.logicbig.com/tutorials/core-java-tutorial/java-se-api/service-loader.html

docs.oracle.com/javaee/6/api/javax/servlet/annotation/HandlesTypes.html

www.logicbig.com/tutorials/java-ee-tutorial/java-servlet/servlet-container-initializer-example.html

offbyone.tistory.com/215

www.baeldung.com/spring-xml-vs-java-config

www.baeldung.com/spring-boot-dispatcherservlet-web-xml

www.baeldung.com/java-web-app-without-web-xml

docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/SpringServletContainerInitializer.html

docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/WebApplicationInitializer.html

docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/support/AbstractAnnotationConfigDispatcherServletInitializer.html

 

Posted by 동팡

(4.5 부터 작성중~~~~...)

 

우리가 자주 사용하는 DBCP를 분석한다. 

 

Apache Commons-DBCP

defaultAutoCommit driver default 
오토커밋 여부를 결정한다. 
defaultTransactionIsolation driver default
READ UNCOMMITTED
READ_COMMITED
REPETABLE_READ
SERIALIZABLE
보통 DBMS의 경우, READ_COMMITED ~ REPETABLE_READ
cacheState 모르겠음
defaultQueryTimeout default null 
쿼리문의 타임아웃을 설정한다. 
그러나, DBMS의 쿼리 타임아웃 설정을 우선시한다.
enableAutoCommitOnReturn 모르겠음
initialSize default 0
커넥션 풀을 최초 초기화할 때 생기는 커넥션의 개수
maxTotal default 8
마이너스 값의 경우 무제한
커넥션 풀의 최대 생성 가능 커넥션 개수
maxIdle default 8
마이너스 값의 경우 무제한
커넥션 풀에 반납할 때 최대한 남을 수 있는 커넥션 개수
minIdle default 0
최소한 남을 수 있는 커넥션 개수
maxWaitMillis default 무제한
커넥션 풀에서 커넥션을 얻기 위해 기다리는 시간이다.
Tomcat의 경우, Tomcat 서버 쓰레드가 무한정 대기하는 불상사가 생길 수 있다.
validationQuery default -
커넥션의 헬스 체크할 때 어떤 쿼리를 써야하는지 결정한다. 
1row만 반환하는 SELECT 쿼리만 써야한다. 
validationQueryTimeout default 무제한
validation 쿼리의 타임아웃을 지정한다. 
testOnCreate default false
커넥션의 생성과 동시에 유효성 검사를 실시한다. 
testOnBorrow default true
커넥션 풀에서 커넥션을 갖고 올 때마다 유효성 검사를 실시한다. 
만약 유효성 검사를 실패하면 해당 커넥션을 폐기하여 다른 커넥션을 요청한다. 
testOnReturn default false
커넥션을 반납하기 전에 유효성 검사를 실시한다.
testWhileIdle default false
Evictor에 의해 주기적으로 커넥션 검사를 실시한다.
커넥션이 유효하지 않을 경우, 폐기한다.
timeBetweenEvictionRunsMillis default -1 
Evictor의 가동 주기를 설정한다(ms 단위).
음수 값을 경우, 실행하지 않는다. 
numTestsPerEvictionRun default 3 
Evictor를 가동할 때 유효성 검사를 할 커넥션의 개수
minEvictableIdleTimeMillis default 30분
Evictor를 사용하여 커넥션 유휴시간을 확인해 설정 값 이상일 경우 커넥션을 제거한다. 
softMinEvictableIdleTimeMillis default -1
모르겠음
maxConnLifetimeMillis default -1
커넥션의 최대 유지 시간이다. 해당 값이 넘을 경우, 사용 또는 유효성 검사 때 폐기된다. 
음수 값은 무한을 의미한다. 
logExpiredConnections default true
maxConnLifetimeMillis 설정에 의해 커넥션이 폐기될 경우 로깅을 한다. 
connectionInitSqls default null
모르겠음
lifo default true
LIFO, 최근에 사용한 커넥션(Last-In)을 반환받는다(First-Out).
false를 설정할 경우, FIFO 형태로 커넥션을 반환한다. 
removeAbandoned default false
활성화된 커넥션이 Fetch 시간이 removeAbandonedTimeout 값을 넘어서면 커넥션 강제 Close의 대상이된다. 
대량 SQL의 경우, 해다 옵션에 의해 커넥션이 소멸될 수 있다. 
해당 값의 설정보다 SqlStatement Timeout을 통해 제어하는게 더 좋지 않을까 생각이든다. 
poolPreparedStatements defualt false
현재 커넥션에서의 PreparedStatement의 풀링을 실시한다. 
maxOpenPreparedStatements
default 제한 없음

풀링하고자하는 preparedStatement의 개수를 지정한다. 
만약 5를 설정할 경우, 10개의 커넥션이 풀에 존재하면 50개의 PreparedStatement가 캐시에 저장된다. 
크기에 유의하며, 반복적인 Select가 잦은 서비스에 poolPreparedStatements/maxOpenPreparedStatements 옵션을 설정한다.

번외) PreparedStatement

PreparedStatements Pool 관련
PreparedStatement를 통해 바인드 변수를 사용할 수 있다.
바인드변수를 통해 DBMS 라이브러리 캐시(shared-pool)의 공유할 수 있는 커서를 생성한다. 
이렇게 생성한 커서들을 통해 SQL 실행계획을 공유할 수 있다. 
SQL 실행계획을 공유할 경우의 이점은 SQL 파싱을 하드 파싱이 아닌 소프트 파싱으로 진행할 수 있다.
소프트 파싱을 할 경우, 하드 파싱(SQL 최적화 과정)을 피할 수 있다.
하드 파싱(SQL 최적화 과정)은 다음의 사항들이 존재한다. 
 - 후보 실행계획 생성 및 각각의 비용 계산, SQL 엔진이 실행할 수 있도록 코드 생성

 위와 같이 PreparedStatement 사용을 통해 DBMS에서 SQL 소프트 파싱을 진행할 수 있다. 

(내 기준) Common-DBCP Best Practice

DBCP의 경우, 비지니스 서버에 따라 설정이 매우 상이하다. 

defaultAutoCommit = true
defaultQueryTimeout = 10
lifo = true(default: true)
maxTotal = 300
initialSize = 40
minIdle = 40
maxIdle = 60
maxWait = 5000
testOnBorrow = false
testWhileIdle = true
numTestsPerEvictionRun = 7
TimeBetweenEvictionRunsMilli=300000
minEvictableIdleTimeMillis = -1
poolPreparedStatements = true
maxOpenPreparedStatements = 5
validationQuery = select 1 (PostgreSQL 기준)
validationQueryTimeout = 10000
[조건]
 - 리눅스 서버의 TCP/IP 소켓의 지속시간은 30분
 - DBMS의 커넥션 타임아웃은 30분
 - DBMS의 쿼리 타임아웃은 10초이다. 
 - DBMS는 커넥션을 300개 이상 맺을 수 있다. 
 - DBMS는 라이브러리 캐시(Shared-Pool)에 커서를 2000개 이상을 담을 수 있다(크기로 파악 못해 ㅈㅅ).
 - OLTP 환경

[설명]

 - (가정) : 현재 OLTP 환경에서 10초 이상 되는 쿼리는 없고 간략한 쿼리를 진행한다. 그렇기 때문에 Timeout이 발생하면 문제가 생기는 것이다.
 - 많은 과부하를 예상 할 수 있기 때문에 최소한의 커넥션은 40개를 들 고 있는다(상황에 맞게 변경 필요).
 - 어차피 40개 들고 있어야하기 때문에 최초 생성할 때 40개를 생성한다.
 - testOnBorrow의 기능은 비활성화 하였다. 
   : testOn* 옵션은 커넥션 풀에 많은 부하를 줄 수도 안 줄 수도 있다.
   : 매번 커넥션을 획득할 때 유효성 검증은 할 필요 없다. 성능에 해를 끼칠 수 있다.
 - 주기적으로 Idle 커넥션의 유효성 검증을 실시한다.
   : DBMS의 커넥션 타임아웃에 의해 TimeBetweenEvictionRunsMilli 값이 변경될 수 있다.
   : 5분 마다 7개의 Idle 커넥션에 대해 유효성 검증을 실시한다.  
   : 30분이 될 때 42개(6 * 7 = 42)의 Idle 커넥션의 유효성 검증을 실시한다.  
   : 잦은 유효성 검증은 성능에 해를 끼칠 수 있다.
 - 유효성 검증을 위한 쿼리와 시간은 임의로 해도 괜찮을 것 같다.
 - PrepareStatement Pooling은 아직 분석이 되지 않았지만 커서를 적극적으로 활용하기 위해서는 필요한 옵션이라 생각한다. 
 - PrepareStatement Pooling 개수는 각 커넥션 당 5개까지의 쿼리문을 캐싱할 수 있다( maxActive(300) * 5 = 1500).

※ maxActive, maxIdle의 값을 똑같이 할 수 있다. 그러면 커넥션을 소멸과 생성 없이 영속적으로 사용할 수 있다.
※ 사용하는 JDBC가 Connection.isValid()를 구현하였으면 validationQuery를 생략하는 것이 이득이 될 수 있다.

※ PrepareStatement Pooling: 보통의 DBMS는 라이브러리 캐시(Shared-Pool)에서 SQL 실행 계획을 캐싱한다... 근데 DBCP에서의 PrepareStatement Pooling을 통해 또 캐시를한다?? 좀 뭔가 이상하다.. 추가 분석이 필요하다.

(생각이 다를 경우, 꼭 말씀해주세요!! 서로 배울 수 있는 기회를 주세요!)


 

 

Hikari-DBCP

... ...
   
   
   
   

 

아래 사항 정리해야함 21. 5. 12 - ehdvudee

HikariDBCP는 test-while-idle이 존재하지 않는다. 그러면 Idle 커넥션에 대해 유효성 검증을 어떻게 하는 것인가? 
일단 결론적으로 Idle 커넥션을 계속 유지하는, 소켓을 계속 유지하는 Commons-DBCP와는 다른 철학을 갖고있다.
HikariDBCP는 Idle 커넥션 갱신 방식을 지양한다. 
test-while-idle을 지속적으로 할 경우, 네트워크 인프라 설정과 DB의 WAIT_TIMEOUT을 존중하지 않으며, DB에 불필요한 SQL 요청을 한다는 입장이다.
test-while-idle을 통해 test-on-borrow를 개선 못한다는데... 이부분은 필자는 다르게 생각한다(필자가 잘못 이해했을 수도 있다.). 

 

즉, HikcariDBCP의 입장을 정리하면 다음과 같다github.com/brettwooldridge/HikariCP/issues/766).

- test-while-idle은 네트워크 인프라, DB의 설정을 존중하지 않고 Idle 커넥션 유지한다.
- test-while-idle 또는 test-on-borrow를 통해 지속적인 유효성 검증은 불필요한 부하를 야기한다(네트워크, 방화벽, DB 등). 
- 때문에, HikariDBCP는 maxLifeTime까지만 사용한 후, 커넥션을 폐기한다. 
- 폐기된 커넥션의 교체는 두자리의 ms 단위이며, negative attenuation을 통해 한번에 폐기가 발생되지 않는다.
- 즉, maxLifeTime 사용 후 폐기 그리고 생성이 위의 test-while-idle, test-on-borrow 비용이 전체적으로 판단했을 때 더 좋다고 판단하는 것 같다.

 



autoCommit
default true
오토커밋 여부를 결정한다.

connectionTimeout
default 30000(ms)
커넥션 풀에서 커넥션을 얻기 위해 기다릴 수 있는 최대 시간을 의미한다.
해당 값이 초과되면 SQLException을 던진다. 
최소 250 이상의 값부터 지정할 수 있다.

idleTimeout
minimumIdle 개수가 될 때 까지 작동을한다.
minimumIdle값이 maximumPoolsize 값보다 낮아야 작동한다(이하).
해당 시간이 지나면 idle 커넥션 폐기를 진행한다. 

maxLifetime
default 1800000(ms, 30분)
커넥션 풀에 유지할 수 있는 최대시간을 의미한다. 
HikariDBCP는 강력하게 권고하는 사항은 다음과 같다.
네트워크 또는 DB에 맞게 커넥션(소켓) 타임아웃과 맞춘다.
만약 네트워크 또는 DB에 커넥션 유지 타임 아웃이 30분일 경우, 해당 값은 29분 55~58초 정도 설정한다. 
최소 값은 30초이다.

minimumIdle
default maximumPoolSize와 같다


maximumPoolSize
최대 생성 커넥션 개수

 

pkgonan.github.io/2018/04/HikariCP-test-while-idle

 

 

참고 문헌

zzikjh.tistory.com/entry/DBCP-%EC%82%AC%EC%9A%A9%EC%8B%9C-poolPreparedStatements-%EC%86%8D%EC%84%B1%EC%9D%B4-%EC%84%B1%EB%8A%A5%EC%97%90-%EB%AF%B8%EC%B9%98%EB%8A%94-%EC%98%81%ED%96%A5zzikjh.tistory.com/entry/DBCP-%EC%82%AC%EC%9A%A9%EC%8B%9C-poolPreparedStatements-%EC%86%8D%EC%84%B1%EC%9D%B4-%EC%84%B1%EB%8A%A5%EC%97%90-%EB%AF%B8%EC%B9%98%EB%8A%94-%EC%98%81%ED%96%A5

12bme.tistory.com/313

www.tutorialspoint.com/what-are-bind-variables-how-to-execute-a-query-with-bind-variables-using-jdbc

aboutdb.tistory.com/232

engineering-skcc.github.io/cloud/tomcat/apache/DB-Pool-For-Event/

commons.apache.org/proper/commons-dbcp/configuration.html

d2.naver.com/helloworld/5102792

uncle-bae.blogspot.com/2016/04/common-dbcp.html

 

 

Posted by 동팡

목차

1부. 안전한 비동기 처리 설계 경험

  • 개요 
  • 비동기 
  • 저널링
  • 비동기 설계 

2부. Spring을 통한 비동기 처리 개발

  • 개요
  • ThreadPoolTaskExecutor
  • ThreadPoolTaskScheduler

1부. 안전한 비동기 처리 설계 경험

개요

TiberoDBMS를 사용하는 상태, 이 DBMS는 너무 느리다. DBA 또는 DB 엔지니어가 설정을 어떻게 했는지 모르는데 진짜 느리다. 아무리 DBMS 동접자가 많다한들 이런식으로 느린거는 진짜 선을 넘은 것이다. 

각설하고.. 고객님께서 삭제 API가 느리다고 콜이 왔다. 이래저래 DB 쪽에 문제가 많았는데.. 많은 과정을 해결하고 비동기 처리 적용을 하는 방향이 나왔음. 

 

CRU(Create, Read, Update) 부분의 경우, 해당 작업 후, 사용의 여지가 있기 때문에 비동기 처리하기에 적합하지 않다. Delete의 경우, 비동기 처리를 해도 무방하였다. 그러면 비동기를 처리해보자.

비동기

비동기를 잘 설명할 수 있는 생활의 예는 우리는 커피 주문 후 주문대에서 계속 기다리지 않고 벨을 받고 다른 곳에서 핸드폰을 하거나 일행과 대화를 하며 "다른 행위"를 한다. 이게 바로 비동기닼ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

동기의 경우, 커피 주문 후 주문대에서 아무것도 안하고 커피만 나올 때 까지 기다리는 것이다. 

 

필자의 몇몇 프로젝트는 비동기를 넣은 용례(아래 그림의 작업을 비동기로 처리함)가 존재한다. 아래 그림의 설명은 다음과 같다.

  • A  프로세스 시작
  • B 프로세스 시작 요청
  • B 프로세스 시작 요청 후 대기하는 것이 아닌 A 프로세스 재개
  • A 프로세스 완료 후, B 프로세스 완료 상태 확인 후, 반환

비동기 처리할 때 조심해야할 사항은 무엇일까? 제일 주의해야할 사항은 작업의 무결한 처리이다. 동기 처리의 경우 눈으로 확인이 가능하지만, 비동기 처리의 경우, 눈으로 확인하기 힘들다. 그래서 우리는 "비동기는 실패할 수 있다." 라는 가정하에 개발을 하였다.

저널링

비동기 처리 기술은 아니지만, 파일 시스템의 변경사항을 반영하기 전에, 저널안에 변경 로그(추적할 수 있는 기록)들을 저널에 기입한다. 파일 시스템 변경사항을 복구할 때 해당 저널들을 참고하여 복구한다.

저널링을 잘 이해하기 좋은 것은 Sqlite 트랜잭션 개념 중, "저널 모드"를 이해하면 좋다. Sqlite는 1개 프로세스만 Write 작업(CRU)을 할 수 있다. Sqlite의 트랜잭션을 진행할 때 1개 프로세스가 DB 파일에 Write 작업을 실시한다. 이 때 생성되는 임시 파일이 JOURNAL 파일이다. 반영사항은 DB파일에 기록하고 복구할 파일? 기록들은 JOURNAL에 기록하는 것이다. 만약 Commit이 아닌 Rollback을 진행할 때 JOURNAL 파일을 통해 Rollback을 실시한다. 이런 저널링은 리눅스 파일 시스템 뿐만아니라 많은 곳에서 활용한다.

 

저널링을 하는 이유는 다음과 같다. 작업 중, 얘기치 못한 이유로 작업을 실패했을 때 해당 지점을 찾아서 복구 또는 작업의 완료를 진행하기 위해서이지 않을까 생각이든다. 또는 시점 복구 기법을 사용할 때 이것을 쓰지 않을까? 생각한다.

 

(리눅스 파일 시스템에서 사용된다.) <<- 이 부분 분석/공부 후 추가 기재 필요

 

(번외로 면접에서 비동기 처리 관련 답변을 했었는데 내가 비동기 개발할 때 저널링 기법을 사용했다. 근데 그 때 당시 저널링 방식이 뭔지 몰랐다. 면접관이 묻더라.. "그런 방식을 뭐라하는지 아세요~?" 생각하다 잘 모르겠습니다. 했는데, 저널링이라고 한번 알아보라고 말씀해주셨다. 정말 고마웠다. 이런 피드백... 근데 Sqlite 분석할 때 분석했던 내용이더라... 에휴... 근데 몰랐었음... 그리고 이 글을 작성하는 동기도 주셨지... )

 

저널링의 발전이 DB redo log가 아닐까 생각이 든다... 

비동기 설계 

위에서의 설명과 같이 우리는 완벽한 저널링은 아니더라도 저널링 기법을 본 받아서 비동기 처리 프로세스를 설계해야한다. 

(아래 그림은 기존 설계 사항에서 몇개 삭제하였다.)

순서는 다음과 같다.

  • 클라이언트의 삭제 요청이 들어온다.
  • Server A는 삭제하고자하는 정보들의 식별정보와 삭제 요청한 주체에 대한 정보들을 특정 디렉토리에 기재한다(저널링, 간단한 저널링이다.).
  • 파일 입력 완료 후, 비동기 쓰레드 풀에 notify를 진행한다.
  • Cleint에 삭제 성공을 반환한다(202 Accepted).
  • 비동기 쓰레드 풀은 특정 쓰레드를 지정하여 로직을 진행한다.
  • 삭제하고자하는 식별정보는 파일에 입력했지만 메모리에 보관하고 있기 때문에 조회를 실시하지 않아도 된다.
  • 삭제를 진행한다. 
  • 삭제 트랜잭션은 Server A, B에서 일어나기 때문에 DB A, B 를 접근하여 삭제를 실시한다(이것도 비동기 로직을 태운다 ㅋㅋㅋ).
  • 삭제 완료 후, 결과 감사로깅을 진행한다. 
  • 삭제 트랜잭션을 성공적으로 완료하였다. 
  • 파일에 입력한 것(JOURNAL 파일)을 삭제한다.

위의 사항이 기본 순서이다. 그러나 위의 작업 중 얘기치 못한 시스템 오류로 서버가 종료 되었을 경우, 다음과 같이 진행한다.

  • Destroy 스케줄러는 주기적으로 디렉토리에 있는 파일들을 검사한다. 
  • 파일이 있는 경우, 정상적으로 삭제를 하지 못한 것이다. 
  • 또한 이 때 주의할 사항이 Thread-Safe이다.
  • 비동기 쓰레드가 삭제 하고 있을 때 스케줄러가 접근할 수 있다. 
  • Lock 방식으로 Thread-Safe하게 개발한다. 먼저 파일을 획득하고 선점하면 아무도 접근하지 못한다. 
  • 스케줄러는 Lock을 획득하여 삭제를 시도한다. 
  • 스케줄러는 삭제 트랜잭션을 성공적으로 완료한다.
  • 스케줄러에 대한 결과 값을 감사로깅을 한다.

위의 사항에서 "API 감사로그", "스케줄러 감사로그"를 통해 작업의 완료 여부를 확인할 수 있다.

더 나아가 발전하면 Admin 툴을 이용하여 비동기 실패 여부를 모니터링 기능을 제공할 수 있다.

2부. Spring을 통한 비동기 처리 개발 

개요

Spring을 이용하여 비동기 처리 개발할 때 ThreadPoolTaskExecutorThreadPoolTaskScheduler를 사용하였다. 쉬운 비동기 처리를 위해 ThreadPooltaskExecutor를 사용했으며, 비동기 처리의 실패를 보완하기 위해 ThreadPoolTaskScheduler를 통해 실패한 요청에 대해 재시도하는 로직을 개발하였다. 간략하게 위의 두 가지 사항에 대해 알아보자.

ThreadPoolTaskExecutor

 

Spring에서 설정과 어노테이션의 설정으로 간략하게 비동기 처리를 개발할 수 있다. 스프링의 AOP 전략으로 비동기 처리를 깔끔하게 한다 생각해도 좋다(트랜잭션과 비슷한 원리). 트랜잭션 또한 @Transactional 어노테이션을 통해 트랜잭션 관련 코드가 내 비지니스 로직을 침투를하지 않는다. 이것을 가능하게 하는 것이 SpringAOP이며 Spring 비동기도 마찬가지이다. @Async 어노테이션을 통해 비동기 처리 관련 코드가 내 비지니스 로직을 침투하지 않는다. 

 

@Async 어노테이션은 다음과 같이 지정해야한다.

  • 메소드 레벨 
  • public 메소드
  • Async는 또다른 Async를 호출하면 안된다.

비동기 반환 타입은 다음과 같이 진행할 수 있다.

  • void
  • Future<$너의타입>

java.utils.concurrent.Future을 통해 비동기 반환을 받을 수 있다. 그러나 비동기의 반환의 경우 아래와 같이 기다림이 필요하다.

	Future<String> future = asyncAnnotationExample.asyncMethodWithReturnType();

    while (true) {
        if (future.isDone()) {
            System.out.println("Result from asynchronous process - " + future.get());
            break;
        }
        System.out.println("Continue doing something else. ");
        Thread.sleep(1000);
    }

 

Future를 사용할 경우, 다음의 이점이 존재한다.

  • Future<>.get()을 통해 Exception을 메인 쓰레드(비동기를 호출한 부모 쓰레드)로 전파시켜 Exception 핸들링을 할 수 있다. 
  • 그러나 AsyncUncaughtExceptionHandler를 통해 해결할 수 있다.

SimpleAsyncTaskExecutor를 통해 바로 설정을 할 수 있지만 이런 방법을 통해 설정을 할 수 있다.

 

ThreadPoolTaskScheduler

스케줄러는 말마따나 별도의 쓰레드가 백그라운드에서 주기적으로 작업을 해주는 것이다. 그 이상.. 그 이하.. 설명할 것이 없다.. ThreadPoolTaskSchedulerThreadPoolTaskExecutor와 같이 TaskExecutor 인터페이스를 구현하였다. 즉, 비동기 쓰레들을 별도 관리 해준다. 

 

@Scheduled 어노테이션을 통해 간단하게 구현할 수 있지만 좀 복잡하게 설정이 필요한 경우가 존재하여 해당 어노테이션으로 구현하지 않고 일부는 수동으로 설정하였다. 

 

예제는 다음과 같다. 

[SchedulerConfig.class]

	@Bean(name="scheduler")
	public ThreadPoolTaskScheduler threadPoolTaskScheduler() throws IOException {
		ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
		threadPoolTaskScheduler.setPoolSize( 2 );
		threadPoolTaskScheduler.setThreadNamePrefix( "ThreadPoolTaskScheduler" );

		return threadPoolTaskScheduler;
	}
    
    	@Bean
	public ISchedulerOperation intValidOperation( SchedulerAuditLogService schedulerAuditLogService, Invoker invoker, SchedulerDao dao ) {
		if ( settings.getSchedulerAuditMode() ) {
			return new AuditSchedulerOperation( new IntValidSchedulerOperation( dao, invoker ), schedulerAuditLogService );
		} else {
			return new  IntValidSchedulerOperation( dao, invoker );
		}
	}

첫 번째 Bean은 ThreadPoolTaskScheduler이다. 가볍게 보고 넘어가도 괜찮다. 

두 번째 Bean은 프록시, 데코레이터 방식을 통해 빈을 상황에 따라 선택적 주입을 실시한다. 위의 코드를 말로 풀면 스케줄러에 대해 감사로그를 실시할 것 인가 안할 것인가 이다.

 

[Scheduler.class]

public abstract class Scheduler {
	
	boolean state = false;
	
	@Autowired
	private SchedulerDao dao;
	
	@Autowired
	private ThreadPoolTaskScheduler threadPoolTaskScheduler;

	@PostConstruct
	public void scheduleRunnableWithCronTrigger() throws IOException {
		if ( isEnabled() ) {
			threadPoolTaskScheduler.schedule( runner(), getTrigger() );
		}
	}
	
	public SchedulerDao getDao() { return this.dao; }
	
	public abstract Runnable runner();
	
	public abstract Trigger getTrigger();

	public abstract boolean isEnabled();
}

Scheduler를 확장한 구현체의 runner메소드에서 ISchedulerOperation인터페이스의 run메소드를 실행한다. 구현체는 별도 첨부를 하지 않았다.

위의 코드와 설정 파일을 통해 구현체를 아래와 같이 구현할 수 있다.

  • 설정파일에 값을 파싱하여 CronTrigger를 통해 스케줄링 시간을 설정할 수 있다.
  • 설정파일 값을 파싱하여 스케줄링 가동 여부를 설정할 수 있다.
  • 위의 인터페이스와 추상 클래스를 통해 통일된 스케줄러 클래스를 개발할 수 있다.

참고문헌

sqlite.org/atomiccommit.html

www.quora.com/What-is-the-difference-between-a-journaling-vs-a-log-structured-file-system

www.baeldung.com/spring-async

www.baeldung.com/spring-task-scheduler

 

Posted by 동팡