서두

  • CURRVAL을 잘못 사용할 경우, 끔찍한 상황이 도래될 수 있다.
  • 정말 기본적인 사항을 간과했습니다...

오류

WARN  [org.jboss.jca.adapters.jdbc.local.LocalManagedConnectionFactory] (default task-10) IJ030027: Destroying connection that is not valid, due to the following exception: …

 중략

java.sql.SQLException: JDBC-6003: Sequence "AUDIT_REQUEST_ID_SEQ" has not been accessed in this session: no CURRVAL available.

위의 오류 상황은 다음과 같다.

  • API 요청에 대해 감사 로깅을 진행한다.
  • API 요청은 3개의 작업을 1개 트랜잭션 으로 작업을 진행한다.
  • 감사로그는 3개의 작업에 대해 각각 감사로그를 진행한다. 즉, 총 3개의 감사로깅을 진행한다.
  • 첫 번째 감사로그는 NEXTVAL을 사용한다.
  • 두 번째, 세 번째 감사로그는 CURRVAL을 사용한다.

Tibero-JDBC-6003오류의 경우, 현재 접속 세션(Connection)SEQUENCE.NEXTVAL 없이 SEQUENCE.CURRVAL을 사용했을 때 발생한다.

 

암호키 등록에 대한 감사로그 입력은 총 3회 실시한다. 첫 번째 감사로그를 입력할 때 NEXTVAL을 호출한다. 두 번째부터는 CURRVAL을 사용한다. 두 번째 감사로그 입력 후, DB Connection Reset이 발생하였다. 해당 사항으로 인해 CURRVAL의 값을 보관했던 커넥션(Session)은 소멸되었다. 그 후, 세 번째 감사로그 입력할 때 CURRVAL을 찾지 못한다.

그러나 위의 상황 분석을 통해 다음과 같이 통찰할 수 있다(다음의 통찰을 통해 쪽팔림 1을 획득할 수 있다.).

 

  • CURRVALNEXTVAL을 호출한 세션에서 호출해야 한다.
  • CURRVAL의 값은 Thread-Safe하지 않다.

(은연중에 CURRVAL이 Thread-Safe하지 않은 것을 알고 있었지만, 연계해서 생각을 하지 못 했다......)

 

그림으로 표현하면 아래와 같다.

  • 1개 트랜잭션 내부에서의 NEXTVAL->CURRVAL->CURRVAL은 상관없다고 생각한다(트랜잭션 과정 중 DB 세션이 안죽는다는 가정 하에(트랜잭션 중 DB 세션이 죽으면 그건 또 다른 문제이니까 상관 없을 듯...)).
  • 그러나, 위와 같이 별개의 트랜잭션에서의 NEXTVAL->CURRVAL->CURRVAL은 문제가 상당하다. 심각하다. 자세한 사항은 아래 "위의 사항으로 인한 추가 위험"을 참고한다.

위의 사항으로 인한 추가 위험

위의 원인 분석을 토대로 추가 위험사항을 분석한다. 해당 분석은 시나리오 정의와 검증을 토대로 분석을 진행한다. 대표적인 시나리오는 총 두 개,아래와 같다.

  • Thread-Safe 하지 않는 값의 사용으로 인해 의도하지 않는 값을 테이블에 적재
  • CURRVAL의 잘못된 사용으로 인해 어플리케이션 오류 발생

Time-1) Session1

 

Time-2) Session2

Time-3) Sesseion1

  • 두 개의 SessionCURRVAL 값이 다르다.
  • 만약, Time-3) Session1의 CURRVAL 값은 2가 아닌, 1이다.

[시나리오2 검증 : 잘못된 사용으로 인한 오류 발생]

현재의 오류가 시나리오2와 같다. 위의 오류는 DBCP 커넥션(세션)의 소멸로 인해 발생하였다. 그러나 많은 동접이 쇄도할 때 아래 그림과 같이 오류가 발생할 수 있다. 모든 요청을 Session1에서 처리하는 것으로 예상하였지만 두 번째 CURRVAL 요청은 Session2에서 처리할 경우 오류가 발생할 수 있다.

 

부록

  • 현재 DBMS Tibero이다. 그러나, Oracle 또한 위와 같은 상황이 있을 수 있다 [1].
  • 오라클 오류는 "ORA-08002: sequence string. CURRVAL is not yet defined in this session"이다.

 

 


[1] http://nimishgarg.blogspot.com/2014/07/ora-08002-sequence-stringcurrval-is-not.html

Posted by 동팡

(2018년 ~ 2019년 초반 과거 메모, 논문 쓰려다 망해서 던진 것, LoRa 1.1에 있는 내용 ㅠㅠ)

 

Abstract

사물인터넷은 4차 산업을 주도하는 대표적인 기술 중 하나이다. 사물인터넷은 다양한 산업분야에 적용이 가능하다. 사물인터넷을 여러 분야, 환경, 상황에 적용하기 위해서는 저전력, 광대역, 저비용 등의 특징이 필요하다. 이러한 조건의 적합한 기술은 LPWAN(Low Power Wide Area Network)이 적합하며, 대표적인 기술은 LoRaWAN(LongRangeWAN)이 있다. 이와 같은 기술을 적용하여, 사물인터넷 기술은 농업, 제조업, 서비스업 등 다양하게 사용할 수 있다. 그러나 사물인터넷은 많은 보안 위협에 노출되어 있으며, 실로 외부로부터 많은 공격을 받았고, 지속적으로 받고 있다. 또한 LoRaWAN의 경우, 기본적인 암호키 관리 체계가 상당히 부족하며, 위와 같은 현상은 절대적인 보안 취약점의 요소이다. 본 논문은 LoRaWAN의 암호키의 사용, 관리 및 유지에 대한 취약점 분석을 실시하며, 개선 모델을 제안한다.

1. 서론

 4차 산업혁명을 바라보는 오늘날,, 사물인터넷(Internet-of-Things, IoT)4차 산업혁명을 주도하는 핵심기술 중 하나이다. 이에 부응하여 IoT와 관련된 다양한 기술과 서비스들이 등장하고 있다.

IoT 기기는 제한된 배터리와 전원이 공급되지 않는 곳에 장기간 설치하여 안정적으로 동작하며, 데이터 송/수신을 하는 경우가 많다. 대부분의 데이터 통신은 단순한 유선 통신이 아닌, 무선 환경을 이용한 무선통신이다. IoT에 적합한(광대역, 저전력, 저비용, 적절한 통신 속도, 소량의 데이터 전달) 통신 규격은 LPWAN이다. LPWAN 기술은 대표적으로 총 4가지가 있으며 다음과 같다. LoRaWAN, Sigfox와 같은 비면허 대역과 LTE-M, NB-IOT와 같은 면혀대역의 LPWAN이 있다.

각각의 LPWAN은 장단점이 존재하며, 특정 기술로 모든 IoT에 동일하게 적용할 수 없으며, 사용 환경과 서비스에 적절하게 각각의 LPWAN을 적용해야 한다. 예를 들어, 높은 데이터 통신 품질, 높은 데이터 통신 빈도 및 양을 우선시하는IoT 기기는 NB-IOT, LTE-M이 적합하며, 저렴하고 넓은 데이터 통신 커버리지와 데이터 통신 품질에 의존하지 않는 경우, LoRaWAN, Sigfox가 적합하다.[2]

그러나 LPWAN 기술에서의 보안은 부차적인 요소로 되어있다. 각각의 LPWAN의 보안 적용 방법도 가지각색이며, 보안에 취약한 사항이 존재한다. IoT의 서비스는 하나의 개인과 점점 밀접해지며, 이러한 데이터는 기밀성, 무결성은 보호되어야 한다.

 본 논문은 최근 IoT에서 많이 사용하는 비면허 대역 LoRaWAN[3]의 보안 취약점 중 암호키 관리 방안에 대한 취약점을 중점적으로 분석했다.

본 논문의 구성은 다음과 같다. 2장은 LoRaWAN의 암호키 배포 과정(Join 과정)과 취약점 분석 선행 연구 분석을 다루고 이를 토대로 3장에서 LoRaWAN 암호키 관리 방안 취약점 분석을 하여, 문제점을 열거한다. 그 후, 4장은 결론을 맺는다.

2. 관련 연구

 2장 관련 연구는 다음과 같다. LoRaWAN 암호키 라이프 사이클은 LoRaWAN의 암호키의 주입과 배포, 세션키 유도, payload의 암/복호화, payload의 무결성 검증, 디바이스 식별 및 인증 절차 등 암호키와 관련된 보안 구조를 분석한다. 그리고 위의 과정의 취약점 분석에 대한 선행 연구를 분석한다.

2.1 LoRaWAN 암호키 Join 과정

 LoRaWAN은 아래 [그림 1]과 같이 두 개의 계층 별 암호화를 제공한다. MAC 계층의 암호화는 디바이스의 식별 및 인증 부분이며, Application 계층의 암호화는 디바이스에서 실질적으로 송신하고자 하는 데이터 즉 payload의 암호화를 담당한다. 두 개의 계층은 모두 AES128 대칭키를 사용하며, 상이한 유도식을 통해 산출된 다른 암호키를 사용한다.

[그림 1] LoRaWAN Security Architecture

 암호키를 위의 [그림 1]과 사용하기 위해서는 기기 정보 및 암호키의 사전 주입과 활성화 절차가 필요하다. 활성화 절차는 OTAA(Over- The-Air Activation), ABP(Activation by Personalization) 두 가지의 방법이 있으며, 위의 두 가지 방법마다 상이한 사전 주입 절차를 갖고 있다. OTAA은 기기 출하 전, 사전에 DevEUI, AppEUI, AppKey(PSK, Pre-Shared Key)를 기기에 주입을 한다. 기기는 위의 세 가지 정보를 통해 NS(Network Server)와의 Join 과정을 거쳐, 두 개의 Session Key(NwkSKey, AppSKey)를 얻는다. ABP은 갱신되지 않는 두 개의 Session Key를 통해 NS와 송/수신을 한다. 그러나 더 나은 보안성을 제공하기 위해서는 OTAA 방식을 권고한다[4]. 또한 본 논문은 OTAA 방식에서의 암호키 관리 방안에 대한 취약점 분석을 진행한다.

 OTAADevEUI, AppEUI, AppKey를 기기에 주입 후, 기기 최초 개통 또는 초기화를 할 때 Join 과정을 진행한다. Join 과정은 다음 [그림 2]와 같다.

[그림 2] LoRaWAN Join procedure[5]

 첫 번째 LoRa MoteNSAppEUI, DevEUI, DevNonce 값과 MIC(Message Integrity Code) 값을 보낸다. MIC를 제외한 세 개의 값은 평문으로 전송된다. AppKey는 기기 출하 전, 사전에 공유된 키이다. DevNonce는 재전송 공격을 방지하기 위한 Mote에서 임의로 생성된 2옥텟 값이며, MIC는 기기의 인증을 위한 값이다. MIC 값 계산식은 다음과 같다.

 

cmac = aes128_cmac(Appkey, MHDR | AppEUI | DevEUI | DevNonce)

MIC = cmac[0..3]

 

 두 번째 NS는 먼저 DevNonce 값을 통해 재전송 공격을 방지 절차를 진행한다. DevNonce 값이 일전에 사용되었을 경우, 요청은 거절된다. 그 후, Join요청을 한 DevEUIAppKey, Mac Header 등 위의 MIC 계산 식을 통해 MIC 인증 절차를 진행한다. 인증을 성공했을 경우, 두 개의 Session keyNetwork Session Key(NwkSKey), Application Session Key (AppSKey)를 생성한다. NwkSKeyAppSKey의 유도석은 다음과 같다.

 

NwkSKey = aes128_encrypt(AppKey, 0x01 | AppNonce | NetID | DevNonce | pad )

AppSKey = aes128_encrypt(Appkey, 0x02 | AppNonce | NetID | DevNonce | pad )

 

 세 번째 위의 인증 및 두 개의 세션키 생성 완료 후, MoteJoin Accept Message를 송신하기 위해, 암호키 유도식에 사용된 AppNonce, NetID 등을 AppKey를 이용하여 다음과 같이 암호화한다.

 

cmac = aes128_cmac(AppKey, MHDR | AppNonce | NetID | DevAddr | DLSettings | RxDelay | CFList)

 

 네 번째 생성된 AppSKeyMote와의 E2E 통신을 위해 AS(Application Server)에 송신한다[6]. LoRaWAN 규격은 AppSKey의 송신 시점을 명확하게 정의하지 않았다[5].

다섯 번째 MoteJoin Accept Message를 수신하여, AppKey를 통해 복호화를 하여 Session Key(NwkSKey, AppSKey)두 개를 생성한다.

 결과적으로 MoteAppKey, NwkSKey, AppSkey를 보관, NSAppKey, NwkSKey를 보관, ASAppSKey를 보관한다.

위와 같은 Join 과정이 완료되면 Mote, NS는 양단간 UpLink 또는 DownLink 준비가 끝난다. Mote의 주기 보고(UpLink)가 있을 경우, [그림 3]과 같이 암호화되어NS에 송신한다. Mote에서 실질적으로 송신하고자 하는 데이터 payloadFRMPayload(MAC Frame Payload)AppSKey를 통해 AES128 CMAC, CTR 모드를 활용하여 암호화한다. 메시지 무결성을 위해 Mac Header(MHDR), Frame Header (FHDR), FPort, 암호화된 FRMPayloadMIC[그림 3]과 같이 계산한다[7]. MIC 계산식은 다음과 같다.

 

cmac = aes128_cmac(NwkSKey, B0| msg)

MIC = cmac[0..3]

 

위의 식 UpLink 또는 DownLink의 구분자 역할을 한다.

[그림 3] LoRaWAN PHY payload structure of MAC Message Format

2.2 LoRaWAN 암호키 관리 방안 취약점 분석 선행 연구

 

 현재 LoRaWAN의 암호키 사용, 관리 및 유지함에 있어 취약점이 존재한다. 이런 취약점은 보안 사고와 개인(민감) 정보의 유출로 이어진다. 본 논문의 선행 연구 분석은 LoRaWAN의 암호키 관리에 대해 취약점을 언급한 선행 연구만 분석한다.

 선행 연구에서 언급한 LoRaWAN의 암호키 관리 측면에서의 취약점은 [1]과 같다.

 첫째 암호키의 관리 주체는 NS 또는 AS이다. LoRaWAN의 암호키의 보관, 사용, 유지, 분배, 폐기 등 모든 프로세스는 NSAS에서 이루어진다. 이와 같이 관리를 할 경우, 인가되지 않은 내부자의 접근을 허용하여, 암호키의 누수와 오용을 범할 여지가 있다.

 두 번째 AppKey의 갱신 프로세스가 존재하지 않는다. AppKey를 통해, 두 개의 세션키를 유도하지만, 본질적으로 사용하는 AppKey의 수명은 Mote의 수명과 같다. , AppKey가 노출될 경우, 그간 사용했던 Session Key가 복구될 위험성이 있다.

 세 번째 MoteOTAA 활성화의 경우, Join 과정이 존재하는데, Join Request(Mote->NS)payload는 평문으로 전송된다. Mote의 식별 값과 암호키 유도 재료 중 하나의 노출로 인하여 기밀성을 위배할 수 있다.

[1]의 선행 연구 분석 결과와 같이 LoRaWAN은 암호키 관리 측면에서 많은 취약점이 존재한다. III장 취약점 분석은 선행 연구의 취약점 분석에서 언급하지 않은 부분과 추가적인 취약점 분석을 실시한다.

 

취약 사항

참고문헌

ECB 암호 모드를 사용한다.

[8]

암호키의 관리 주체는 NSAS이다.

[8][12]

MACPayloadFPort 0 일 경우, payloadNwkSKey를 통해 암호화를 한다.

[8]

AppKey(PSK)의 온라인 갱신 프로세스가 존재하지 않는다.

[5][8]

MoteAppKey의 생명주기가 같다.

[8]

AppKey의 합의 및 폐기 메커니즘이 존재하지 않는다.

[8]

낮은 ByteNonce 값으로 인하여, 재전송 공격에 취약하다.

[9][10]

[11]

NSAS, NS의 세션키를 만든다. , 계층간의 암호화를 하지 않는다.

[5]

OTAA Join Request의 본문은 암호화되지 않고, NS에 송신된다.

[5][10]

[11]

3. 취약점 분석

 III장 취약점 분석은 선행 연구 분석 토대로 추가적으로 LoRaWAN의 암호키에 관련한 취약점을 분석한다. 취약점 분석 관점은 암호키의 관리와 사용 측면을 중점으로 분석한다.

 첫 번째 위의 선행 연구 “MoteAppKey의 생명주기가 같다.”와 같이 현재 LoRaWAN은 암호키의 생명주기, 상태별 프로세스, 정책이 존재하지 않는다. 암호키의 생명주기는 NIST 키 관리 권고사항의 활성화, 비활성화, 폐기, 손상 등 Mote마다 암호키의 최초 생성부터 유지 및 폐기까지의 암호키의 생명주기와 암호키 상태별 사용 용도를 차등적으로 적용해야 한다[13]. 또한 Mote의 서비스 수준, 처리하는 데이터의 민감도, 용례 등을 통해 위험 평가를 실시하여, 보안 모델을 산정해야 한다[14]. 이와 같은 보안 모델을 통해 암호키의 수명을 측정해야 한다. 암호키의 수명은 암호 메커니즘, 데이터의 트랜잭션의 양, 데이터의 민감도, 키의 노출/사용 정도 보안 모델 요소 등으로 암호키의 수명을 차등적으로 부여해야 한다[15].

 두 번째 위의 선행 연구 “ECB 암호 모드를 사용한다.”와 같이 현재 LoRaWAN은 암호키를 통해 암호화를 할 때, IV 값이 존재하지 않는다. 또한 규격에는 IV 값에 대해서 언급을 하지 않았다. IV의 공유 방법이나 어떤 페킷에 적재하는지에 대한 여부가 존재하지 않는다. IV는 같은 평문을 지속적으로 암호화를 하여도 다른 암호문을 출력하기 위한 암호화 재료이다. IV는 재사용되지 않으며, 암호화의 필요성이 없다. LoRaWAN16Byte의 암호키 길이를 사용하며, 이와 적합한 IV는 16Byte이므로[18], 이에 적합한 페킷을 적재할 수 있는 공간이 필요하다.

 세 번째 세션키 NwkSKey, AppSKey에 대해 갱신 권고 사항이 존재하지 않는다. 세션키는 MoteNS 간의 Join 과정에서 확립된다. Join 과정은 Mote의 최초 개통 호 처리 또는 초기화를 하였을 경우만 진행한다. , Mote의 최초 개통 호 작업 후 Mote단에서 초기화 절차를 진행하지 않을 경우, 세션키의 생명주기는 Mote와 같을 수 있다. 세션은 둘 이상의 통신 장비 간, 일정 시간 유효 연결을 확립하여, 요청 및 응답을 처리하는 절차이다[16]. 세션키 또한 한 세션에서 유효한 키를 의미한다[17]. LoRaWAN 환경에서의 지속적인 세션키 갱신은 사실상 어려움이 존재한다. 그러나 일정 주기마다 세션키를 갱신하는 절차를 확립해야 한다.

 네 번째 AppKey의 합의 메커니즘을 분석한다. 선행 연구[8]에서는 AppKey의 합의 메커니즘 부재에 대해 문제점으로 지적하였으며, ECDH(Elliptic Curve Diffie-Hellman) 알고리즘을 통해서 해결안을 제시하였다. 현재 LoRaW- ANAppKey는 세션키를 유도할 때와 Join Accept Message를 암호화할 때 사용한다. 또한 AppKey에 대해 갱신 권고사항과 규격이 존재하지 않아, AppKeyMote의 생명주기가 같다. 위와 같은 문제점은 AppKey의 갱신 절차를 통해 해결할 수 있다. 그러나 AppKey의 암호키 교환 프로토콜(합의 메커니즘)를 적용할 경우 다음의 우려사항이 존재한다.

 LoRaWAN에서의 Class AMote의 경우, MoteUpLink를 통해서 NSDownLink가 가능하다. NS는 주체적으로 DownLink를 하지 못하는 제약이 존재한다. 또한 Mote의 높은 SF(Spreading Factor)는 낮은 Bit Rate로 인하여, 무선 통신은 외부의 잡음에 쉽게 노출된다. 결국 위와 같은 현상에서의 송/수신은 무선 통신의 높은 실패 확률을 초래한다[19].

이러한 통신 환경은 암호키 교환 프로토콜의 적용보다는 PSK가 적합하다. 현재 LoRaWAN에서 사용하는 암호키의 길이 128bit는 적합한 암호 비도를 갖고 있으며, 또한 이미 PSK 기술을 적용한 WPA(Wi-Fi Protected Access)는 다양하게 활용하고 있다. PSK를 안전하게 사용하는 조건은 신뢰된 제3의 서버의 암호키 발급과 갱신이 필요하다.

 

여기서부터 막장ㅋㅋㅋ

4. LoRaWAN 암호키 보호 모델 제안

선행 연구 분석과 추가적인 취약점 분석을 통해 LoRaWAN은 제3의 신뢰 서버가 필요하며, 안전한 암호키 사용과 유지를 위해서는 암호키 관리 체계와 안전한 암호키 배포 및 접근을 위한 식별 및 인증/인가의 체계 적용이 필요하다. 신뢰 서버의 구성요소는 아래 [그림 4]와 같다.

신뢰 서버는 암호키의 관리, 유지, 배포, 갱신 및 폐기 등의 암호키 사이클 관리를 한다. NSAS는 신뢰 서버 API를 통해 값의 암/복호화를 진행하며, 암호키의 관리는 신뢰 서버에 위임을 한다.

[그림 4] Trust Server in LoRaWAN

암호키의 전체적인 관리와 유지 및 갱신 등의 운영을 한다. 용례는 [그림 5]와 같다.

[그림 5] Trust Server Flow in LoRaWAN

 HSM 난수 발생기를 통해 임의의 암호키 생성 규격에 맞게 암호키 PSK를 생성한다. 생성된 PSKHSMKEK(Key Encryp -tion Key)를 통해 포장(Wrapping)하여, 안전하게 보관한다.

PSKMote에 배포 및 주입되며 개통 호 작업을 실시한다.

Join 과정을 할 때 신뢰 서버는 중계 서버 역할을 한다. NSMote의 식별 및 인증을 하며, Join 과정에 필요한 데이터를 신뢰 서버에 송신한다. 신뢰 서버는 NS에서 받은 필요 데이터와 함께 NwkSKeyAppSKey를 생성한 후, Mote에 송신한다.

 Mote에서의 UpLink를 받은 NS는 무결성 검증에 필요한 데이터를 신뢰 서버에 송신한다. 신뢰 서버는 해당 데이터와 NwkSKey를 통해 무결성 검증을 실시한다.

NS는 암호화된 FRMPayloadAS에서 송신한다.

ASNS로부터 받은 암호화된 FRMPay -load를 신뢰 서버에 송신하여, 복호화된 FRMPayload를 수신한다.

이와 같이 구성할 경우, NSAS의 독립된 계층 관리를 할 수 있다. 데이터의 암/복호화는 신뢰 서버에서 진행하며, NSAS는 암호키의 값이 필요 없다. 신뢰 서버는 NSAS의 식별/인증/인가를 통해 적합한 사용자(NS 또는 AS)를 파악할 수 있다.

 신뢰 서버는 Mote의 서비스 보안 수준 진단을 통해 암호키 보호 수준과 암호키의 생명주기를 결정하여, 암호키의 안전성을 지속적으로 유지한다. 또한 해당하는 보호 수준에 상응하는 정책을 적용한다.

Mote의 보안 수준과 정책에 맞게 AppKey, AppSKey, NwkSKey의 지속적인 갱신 절차를 적용한다.

암호키의 생명주기는 NIST 암호키 권고 사항의 권고안을 적용한다. 암호키의 상태 및 상황별 암호키의 생성, 등록, 백업, 복구, 검색, 수정, 폐기, 말소, 검증, 인증 등의 운영체계를 확립해야 한다.

결론

결론 대외비

[참고문헌]

[2] K. Mekki, E. Bajic, F. Chaxel and F. Meyer, "A comparative study of LPWAN technologies for large-scale IoT deployment", ICT Express, Vol. O, no. O, pp.00~11, Sep 2017

[3] N. Sornin, M. Luis, T. Eirich and T. Kramp, "LoRaWAN Specification V1.0.1 Draft 3", LoRa Alliance, Oct 2015

[4] "LoRa Security FAQ in LoRaAlliance", LoRa Alliance, Jul 2016

[5] JH Kim and JS Song, "A Dual Key-Based Activation Scheme for Secure LoRaWAN", Wireless Communication and Mobile Computing, Vol. 2017, Article ID 6590713, Nov 2017

[6] "LoRaWAN Security", LoRa Alliance, Jul 2016

[7] A. Augustin, J. Yi, T. Clausen, and W. M. Townsley, “A Study of LoRa: Long Range & Low Power Networks for the Internet of Things,” Sensors, vol. 16, no. 9, p. 1466, Sep 2016.

[8] van Leent, Marcel. "An improved key distribution and updating mechanism for low power wide area networks (LPWAN).", Cyber Security Academy, Vol. O, no. O, pp.00~11, Jan 2017

[9] Aras, E., Ramachandran, G. S., Lawrence, P., and Hughes, D., "Exploring The Security Vulnerabilities of LoRa." Cybernetics (CYBCONF), 2017 3rd IEEE International Conference on. IEEE, 2017.

[10] Na, S., Hwang, D., Shin, W., and Kim, K. H., "Scenario and countermeasure for replay attack using join request messages in LoRaWAN." Information Networking (ICOIN), 2017 International Conference on. IEEE, 2017.

[11] Tomasin, S., Zulian, S., and Vangelista, L., "Security Analysis of LoRaWAN Join Procedure for Internet of Things Networks." Wireless Communications and Networking Conference Workshops (WCNCW), 2017 IEEE. IEEE, 2017.

[12] R. Miller, "LoRa Security: Building a Secure LoRa Solution", MWR LABS, https://labs.mwrinfosecurity.com/publications/lo/

[13] NIST SP 800-57, "Recommendation for Key Management Part 1:General", 2012

[14] GSMA, IoT Security Guidelines Overview Document Version 2.0, CLP11, 2017.

[15] NIST SP 800-130, "A Framework for Designing Cryptographic Key Manage- ment Systems", 2013

[16]https://en.wikipedia.org/wiki/Session_(co mputer_science)

[17] J. Callas, L. Donnerhacke, H. Finney, D. Shaw, and R. Thayer, “RFC 4880 Open- PGP Message Format,” 2007.

[18] NIST SP 800-38A, "Recommendation for block cipher modes of operation", 2001

[19] Adelantado, F., Vilajosana, X., Tuset-Peiro, P., Martinez, B., Melia-Segui, J., & Watteyne, T. (2017). Understanding the limits of LoRaWAN. IEEE Communications Magazine, 55(9), 34-40., 2017

 

 

Posted by 동팡

과거 메모 : 2018년 ~ 2019년 초 자료인듯

 

LoRaWAN 1.1 spec Part 6 번역

6. End-Device Activation

디바이스가 LoRa 네트웤에 붙기 위해서는 활성화 절차를 밟아야 한다.

디바이스의 활성화 절차는 두가지가 있다. OTAA(Over-The-air-Activation), ABP(Activation By Personalization)이다[1].

6.1 Data Stored in the End-device

6.1.1 Before Activation

6.1.1.1 JoinEUI

JoinEUI IEEE EUI64 주소체계에 맞는 글로벌한 어플리케이션 Identifier이다. 해당 Join 서버는 EUI를 유일하게 식별한다(Join 서버는 Join 과정, 세션키 운반 같은 것을 도와줌[2]).

OTAA디바이스의 경우, Join 과정을 하기 전에 JoinEUI는 디바이스에 반드시 저장이되어야 한다. ABP 모드의 경우 JoinEUI 필요 없다.

6.1.1.2 DevEUI

DevEUIIEEE EUI64 주소체계에 맞는 글로벌한 디바이스 Identifier이다.

DevEUI NS에서 권고되는 유니크 디바이스 Ientifier이다. 활성화 절차를 하거나 네트웤을 통해 로밍을 할 때 식별한다.

OTAA 디바이스의 경우, Join 과정이 되기전에 DevEUI가 디바이스에 반드시 저장되어야 한다. ABPDevEUI가 저장될 필요가 없다.

6.1.1.3 Device root keys( AppKey & NwkKey )

NwkKey, AppKey AES-128는 디바이스에 할당된 rootKey이다. 디바이스가 OTAA를 통해 네트웤에 Join을 할 때 마다, NwkKeyFNwkSIntKey, SNwkSIntKey, NwkSEncKey 세션키 3개를 유도한다. AppKey AppSKey 세션키 1개를 유도한다.

Secure provisioning, storage, and usage of root keys NwkKey and AppKey on the end- device and the backend are intrinsic to the overall security of the solution. These are left to implementation and out of scope of this document. However, elements of this solution may include SE (Secure Elements) and HSM (Hardware Security Modules). => 키 관리는 본문(로라스팩)에서 안다룸

LoRaWAN 1.0과의 호환성이 필요할 때는 NwkKey만 사용한다(1.0AppKey라는 한 개의 키만 사용하는데 그것을 NwkKey로 대체해줌).

The end-device in this case MUST

-      Use the NwkKey to derive both the AppSKey and the FNwkSIntKey session keys as in LoRaWAN1.0 specification. è FNwkSIntKey1.0NwkSKey가 된다는 얘기

-      Set the SNwkSIntKey & NwkSEncKey equal to FNwkSIntKey, the same network session key is effectively used for both uplink and downlink MIC calculation and encryption of MAC payloads according to the LoRaWAN1.0 specification. è NwkKey에서 유도한 3개의 세션키를 똑같이 만들고 uplink, downlink 때 사용해라~

NwkKey, AppKey OTAA 방식에서는 반드시 저장되지만, ABP 방식은 아니다.

NwkKeyAppKey는 재사용되지 않게 안전하게 보관해야 한다.

6.1.1.4 JSIntKey and JSEncKey derivation

OTA 디바이스는 NwkKey로 유도되는 키가 아래와 같이 있다.

-      JSIntKey: MIC Rejoin-Request type 1 메시지와 Join-Accept 응답에 사용[3]

-      JSEncKey: Rejoin-Request에 의해 트리거 된 Join-Accept를 암호화에 사용[4]

6.1.2 After Activation

활성화 후 다음의 정보들이 디바이스에 저장된다.

-      DevAddr: 디바이스 어드래스

-      NwkSEncKey, SNwkSIntKey, FNwkSIntKey

-      AppSKey

6.1.2.1 End-device address(DevAddr)

32bit의 값 DevAddr을 통해 현재 네트웤에서 디바이스를 식별한다.

 

The LoRaWAN protocol supports various network address types with different network address space sizes. The variable size AddrPrefix field is derived from the Network Server’s  unique identifier NetID (see 6.2.3) allocated by the LoRa Alliance with the exception of the  AddrPrefix values reserved for experimental/private network. The AddrPrefix field enables  the discovery of the Network Server currently managing the end-device during roaming.  Devices that do not respect this rule cannot roam between two networks because their home  Network Server cannot be found. =>여러 네트워크 주소 유형 지원, 네트워크 서버 로밍관련 내용임;

The least significant (32-N) bits, the network address (NwkAddr) of the end-device, can be arbitrarily assigned by the network manager. => NwkAddr은 네트워크 관리자가 임의로 지정

The following AddrPrefix values may be used by any private/experimental network and will not be allocated by the LoRa Aliance. => 주저리주저리

6.1.2.2 Forwarding Network session integrity key (FNwkSIntKey)

FnwkSIntKey는 세션키임, upLink MIC를 계산하는데 사용된다. 4.4절 참고

FNwkSIntKey는 안전하게 보관 필요

6.1.2.3 Serving Network session integrity key (SNwkSIntKey)

SNwkIntKey는 세션키임, downlink 메시지의 MIC를 계산하여 검증하는데 사용되고, upLink의 메시지 MIC 1/2를 계산하는데 사용함[5]

1.0 NS에 붙으면 6.1.2.2 6.1.2.3 키는 같은 키이다.

SNwkSIntKey는 안전하게 보관 필요

6.1.2.4 Network session encryption key (NwkSEncKey)

NwkSEncKey는 세션키임, up/down link MAC Command를 암/복호화하는데 사용된다. 1.0 NS에 붙으면 MAC payload 암호화와 MIC 계산에 사용된다. NwkSEncKeyFNwkSIntKey는 같은 값

이것도 안전하게 보관 필요

6.1.2.5 Application session key (AppSKey)

AppSKey는 어플리케이션 세션키임, 어플리케이션 계층에서만 볼 수 있게 payload를 암/복호화 한다. 어플리케이션 페이로드는 단말과의 E2E보안이 됨, .BUT => but they are integrity protected only in a hop-by-hop fashion: one hop between the end-device and the Network Server, and the other hop between the Network Server and the application server. That means, a malicious Network Server may be able to alter the content of the data messages in transit, which may even help the Network Server to infer some information about the data by observing the reaction of the application end-points to the altered data.

(위의 사항이 취약점이라는데 이해 필요함.)(컨텐츠 이해 못함)

종단간 기밀성 무결성 보호를 하기위해 추가 보안 솔루션을 사용할 수 있음 ..;

6.1.2.6 Session Context

세션 컨텍스트는 NS, AS의 세션을 보관함

NS 세션은 아래와 같이 구성됨

-      N/SNwkSIntKey

-      NwkSEncKey

-      FCntUp

-      FCntDwn(LW 1.0) or NFCntDwn(LW 1.1)

-      DevAddr

AS 세션은 아래와 같이 구성됨

-      AppSKey

-      FCntUp

-      FCntDown(LW 1.0) or AFCntDwn(LW 1.1)

네트웤 세션 상태는 NS와 디바이스 끼리 관리된다. 어플리케이션 세션 상태는 AS와 디바이스끼리 관리된다.

OTAA 또는 ABP 과정이 끝나면, 새로운 보안 세션이 NS, AS, 디바이스에 수립된다. 키와 디바이스 주소는 세션 기간 동안 고정된다 (FNwkSIntKey, SNwkSIntKey, AppSKey, DevAddr). 프레임 카운터는 프레임 트래픽이 세션 기간 동안 교환 할 때 증가함 (FCntUp, FCntDwn, NFCntDwn, AFCntDwn)(이해안감).

It is RECOMMENDED that session state be maintained across power cycling of an end-device. Failure to do so for OTAA devices means the activation procedure will need to be executed on each power cycling of a device.

 

6.2 Over-the-Air Activation

OTAA의 경우, NS 송신 전 Join을 해야한다. Session context 정보가 없을 경우 매번 Join을 해야한다.

조인을 하기 전에 DevEUI, JoinEUI, NwkKey, AppKey가 있어야한다.

6.2.1 Join procedure

디바이스 관점에서, 조인은 Join/Re-Join Request/accept 메시지로 구성 되어있다.

 

6.2.2 Join-request message

디바이스가 조인 요청을 하면 조인 과정은 시작한다.

 

위의 3개 값이 전송된다.

DevNonce0부터 시작한다. DevNonce는 조인을 할 때마다 1씩 증가한다(다시 사용하면 안됨). DevNonce는 영구적이다(리붓해도 카운팅은 안사라짐). JoinEUI를 변경하지 않고 DevNonce를 리셋해버리면 NS는 조인 요청을 거절한다(NS는 디바이스의 Nonce를 관리함).

Note: 디바이스와 NSSync 관련 설명……

조인 요청에 쓰이는 MIC 값은 아래와 같이 생성된다.

cmac = aes128_cmac(NwkKey, MHDR | JoinEUI | DevEUI | DevNonce)

MIC = cmac[0..3]

조인 요청 메시지는 암호화 안함. (데이터 송/수신 설명….) 디바이스는 JoinRequest 송신 후, DevNonce 값을 1 올림

 

6.2.3 Join-accept message

조인이 성공하면 Join accept message를 리턴한다. 네트워크 송/수신 관련 설명 ….

조인 요청이 실패하면 리턴을 안한다.

조인 억셉 메시지는 아래와 같다.

 

Downlink 파라미터는 JoinNonce, NetID, DevAddr, DLSettings이다. RxDelay, CFList 네트워크 송/수신 관련 값이다.

 JoinNonce FNwkSIntKey, SNwkSIntKey, NwkSEncKey, AppSKey를 생성하는데 사용된다. JoinNonce는 조인 억셉 메시지가 내려가는 순간 1 증가한다.

디바이스는 Join Nonce를 보관한다. 디바이스는 MIC 값 검증과 JoinNonce가 큰 것에 대해(자기 값보다)서만 처리해야 한다. Join accept message를 제대로 받으면 디바이스의 Join Nonce는 바뀐다.

LoRa 얼라이언스는 관리되지 않는 실험/사설망을 제외하고 24bite의 고유 네트워크 식별자(NetID)를 할당한다.

조인할 경우 OptNeg를 통해 버전을 식별하고 하위 호환성을 해소한다 라는 내용임

NwkKey를 통해 JoinAcceptMessgae를 복호호화한다. 해당 Join Nonce를 통해 아래

NwkKey를 통해 JoinAcceptMessgae를 복호화한다. 그 후, Join Nonce를 통해 아래 5가지 키를 생성하며, JSIntKey를 통해 MIC 검증을 한다.

AppSKey = aes128_encrypt(AppKey, 0x02 | JoinNonce | JoinEUI | DevNonce | pad16)

FNwkSIntKey = aes128_encrypt(NwkKey, 0x01 | JoinNonce | JoinEUI | DevNonce | pad16 )

SNwkSIntKey = aes128_encrypt(NwkKey, 0x03 | JoinNonce | JoinEUI | DevNonce | pad16)

NwkSEncKey = aes128_encrypt(NwkKey, 0x04 | JoinNonce | JoinEUI | DevNonce | pad16)

cmac = aes128_cmac(JSIntKey, JoinReqType | JoinEUI | DevNonce | MHDR | JoinNonce | NetID | DevAddr | 1610 DLSettings | RxDelay | CFList )

MIC = cmac[0..3]

 

ReJoin일 경우, 위의 그림과 같이 JSEncKey를 생성하여, 복호화 한다(JoinReqType은 헤더에?(MAC 은 검증용으로 포함하고)).

 

6.2.4 ReJoin-request message

디바이스가 Join을 하여 활성화가 되면, 주기적으로 Re-Join 할 수 있다. Re-Join 요청의 경우, 뒷단 서버와 디바이스끼리 새로운 session context를 확립할 수 있게 해준다. Re-Join은 디바이스 처리 주체를 다른 NS로 이관을 하거나, 주어진 네트워크의 디바이스 devAddr을 변경하거나 re-key를 한다.

RX1/RX2 관련 설명 중

Note: NS Re-join Request/Accept Messgae를 처리하는데 디바이스의 성공적인 re-join upLink가 올라오기 전까지 이전 보안 context(session context)를 유지해야한다. 모든 케이스의 re-join 요청 메시지는 Re-Join 요청은 표준 Jorin 요청 메시지와 유사하다.

RejoinRejoin Type이라는 필드로 3개의 명령을 아래와 같이 구분한다.

Rejoin Req Type

설명

0

NetIDDevEUI를 포함한다. 라디오 파라미터를 포함한 디바이스의 모든context를 리셋한다(devAddr, session keys, frame counters, radio parameters).

이 메시지는 장치의 JoinServer가 아닌 수신 네트워크 서버에 의해서만 장치의 홈 네트워크 서버로 라우팅 될 수 있다. MIC .

1

JoinEUIDevEUI를 포함한다. 초기 Join 요청과 똑같다.

2

NetIDDevEUI를 포함한다. Re-key DevAddr을 바꾼다.

 


[1] 해당사항에 대한 개요는 선행 연구 자료에서 확인할 것

[2] 1.0.1 버전과의 제일 큰차이점 중 하나 Join 서버 출현(그 외 여러가지 키 출현, Re-Join 출현)

[3] JSIntKey = aes128_encrypt(NwkKey, 0x06 | DevEUI | pad16)

[4] JSEncKey = aes128_encrypt(NwkKey, 0x05 | DevEUI | pad16)

[5] Uplink MIC FNwkSIntKeySNwkSIntKey 2개의 키로 완성된다. 왜냐하면: 로밍 설정에서 잔달 네트워크 서버가 MIC 필드의 절반 만 확인할 수 있기 위해서이다. >> 이해 안감

Posted by 동팡

목차

  • Enum 설명
  • Enum 기본
  • Enum 활용
  • Spring에서의 Enum활용(Message Converter)
  • Spring에서의 Enum활용(Validation)
  • MyBatis에서의 Enum활용(TypeHandler)
  • 해당 사항 테스트 코드

Enum 설명

고정된 상수 집합이 필요할 때 Enum을 제일 많이 사용한다. 

Java Enum은 Java 5부터 나왔다. Int Enum이나 String Enum을 대체하기 위해 나온 Java의 Enum기능은 강력하다.

보통 개발 할 때 아래와 같이 Int / String Enum을 많이 사용한다.

private static final int FRUIT_ORANGE = 1;
private static final int FRUIT_APPLE = 2;

private static final String OPERATION_MODULE_A = "op_a";
private static final String OPERATION_MODULE_B = "op_b";
private static final String OPERATION_MODULE_C = "op_c";

위와 같은 Enum들을 자주 봤을 것이다. 일단 얘기하자면 위와 같은 것은 Enum 안티 패턴이다.

아래와 같은 이유로 인해 Int/String Enum 패턴을 사용하면 프로그램이 쉽게 깨질 우려가 존재한다. 

  • 컴파일 시점의 상수 -> 상수의 유연한 핸들링이 힘들다.
  • 그룹핑 기능이 없기 때문에 복잡한 기능을 소화하지 못 한다.
  • Int Enum의 경우, 인쇄 가능한 문자열을 생성하기 피곤하다.
  • String Enum의 경우, 문자열 비교를 하기 때문에 느리다. 
  • 타입 안전성을 보장하지 못 한다.

즉, 정리하자면 타입 세이프하지 못 하다. 때문에, 컴파일 타임에서 오류를 잡을 수 없다. 런타임 테스트를 통해 오류를 확인할 수 있다(int 또는 String형 만 받으면 다 되기 때문에 문제점을 찾기 위한 뎁스가 추가된다). 또한 확장성이 많이 부족하기 때문에 활용하는데 있어, 한계점이 존재할 수 밖에 없다(다만, 컴파일 시점 상수이기 때문에, 어노테이션에 쉽게 적용할 수 있다ㅎㅎ, Enum의 값은 어노테이션에 적용하지 못해 불편하다ㅠㅠㅠ)

 

그러나 우리는(나 포함) 간단하기 때문에 쓴닼ㅋㅋㅋㅋㅋㅋㅋ. 다만 제품화가 되는 프로그램은 Java Enum 을 쓰는 것을 강력하게 권고 하고싶다(또는 완성도를 더 높이고 싶은 토이 프로젝트).

 

Enum은 다음의 특징이 존재한다.

  • Enum은 static final 하다.
  • 계승(상속)이 불가능하다. 
  • 컴파일 타임, 타입 세이프하다.
  • 그룹핑이 가능하다(Enum의 Enum을 하여 조합 가능).
  • Object를 계상받아 Object에서 제공하는 메소드를 활용할 수 있다. 또는 디폴트 메소드를 사용할 수 있다.
  • serializable, comparable이 가능하다.
  • 메소드를 사용하여 기능 확장이 무궁무진하다(상수 + 관련 데이터의 연계 및 연산을 사용할 수 있다.).
  • 비교 연산은 Int 상수와 성능이 비슷하다.

위와 같은 이유로 인해 코드 가독성이 좋으며, 형 안전성이 좋고, 기능이 강력하다.

Enum 기본

전형적으로 Enum을 사용하는 것은 다음과 같다.

public enum UserAuthority {
	GREAT_USER( "USR_001" ),
	GOOD_USER( "USR_002" ),
	COMMON_USER( "USR_003" ),
	BAD_USER( "USR_004" ),
	GET_OUT_USER( "USR_005" );
	
	private String authCode;
	
	UserAuthority( String authCode ) {
		this.authCode = authCode;
	}
    
	public String getCode() {
		return this.authCode;
	}		
}

1. Enum은 DB의 코드 값이랑 많이 활용할 수 있다. 추가적으로 코드를 추가하는데도 무리가 없다.

2. 실질적으로 사용할 때 UserAuthority.GREAT_USER 이런식으로 타입 세이프하게 사용할 수 있다.

3. 접근 사용자가 권한의 가능 여부를 판단하는 기능을 메소드 추가를 통해 해소할 수 있다(상수 관련 연계 및 연산 처리).

4. Enum의 Enum을 사용하여, 권한의 권한으로 GREAT_USER, GOOD_USER에 해당하는 권한만 존재하는 Enum을 생성할 수 있다.

 

Enum 활용

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS (4.869e+24, 6.052e6),
    EARTH (5.975e+24, 6.378e6),
    MARS (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);
    
    private final double mass; // In kilograms
    private final double radius; // In meters
    private final double surfaceGravity; // In m / s^2

    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;
   
   	// Constructor
    Planet(double mass, double radius) {
   		 this.mass = mass;
   		 this.radius = radius;
   		 surfaceGravity = G * mass / (radius * radius);
    }
    
    public double mass() { return mass; }
    public double radius() { return radius; }
    public double surfaceGravity() { return surfaceGravity; }
   
   public double surfaceWeight(double mass) {
		return mass * surfaceGravity; // F = ma
   }
}

해당 코드는 이펙티브 자바의 Enum 편에 제일 첫장에 나오는 소스 코드이다. 

위와 같이 Enum을 활용할 수 있다. 

  • Planet.VENUS.mass() 또는 Planet.VENUS.radius() 를 통해 쉽게 상수 값을 확인할 수 있다.
  • 코드를 보면 딱 이해할 수 있다(가독성 원탑).
  • 해당 상수와 연계된 연산(surfaceWeight, surfaceGravity)을 해당 Enum에서 충분히 소화할 수 있다(SRP)

Enum은 EnumMap, EnumSet 활용하여 더 활용할 수 있으며, 인터페이스를 통해 유연하게 개발할 수 있다.

자세한 사항은 이펙티브 자바 Enum편 또는 Enum 활용에 대한 구글링을 참고한다. 정말 다양하게 Enum을 활용한다. 

멋지다.

 

Spring/MyBatis에서 Enum을 사용할 때 골 때리는 구간이 좀 존재한다. 아래와 같이 해결한다.

Spring에서의 Enum활용(Message Converter)

스프링에서 Jackson Mapper를 통해 JSON 메시지에 대해 자동 Message Converting 기능을 사용할 때 Enum은 별도 처리가 필요하다. 아래와 같이 @JsonValue어노테이션을 지정하면 된다. 좀 아쉬운 점은 모든 Message Converter에 공통적으로 적용하기는 어려운 듯 하다.. 그래도 어노테이션 하나 지정을 통해 개선할 수 있어 다행이다.

@JsonValue
public String getCode() {
	return this.authCode;
}

Spring에서의 Enum활용(Validation)

스프링에서는 Message Converting 작업과 동시에 Validation을 할 수 있게해준다. 그러면, Enum의 경우, 해당 작업을 어떻게 처리할까? @NotEmpty를 적용했을 경우 아래와 같은 오류를 뱉어 낸다.

Jackson mapper @NotEmpty 적용(실패)

{"status":"fail","errMsg":"UnexpectedTypeException: No validator could be found for type: **.EnumTestVo$UserAuthority","errCode":1}

별도의 처리를 하지 않을 경우, @NotNull @Null 만 적용할 수 있다. @NotNull을 처리할 경우, 아래와 같이 성공적인 벨리데이션 체크를 할 수 있다. 

Jackson mapper @NotNull 적용(성공)

{"status":"fail","errMsg":"CustomIllegalArgumentException: Message Validation Error : [target: enumTestVo.userAuthority, errMsg: may not be null ] ","errCode":21013}

만약 NotEmpty와 그 이상의 Validation은 그냥 Custom Validation을 생성하면 된다. 

 

EnumNamePattern.class

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = EnumNamePatternValidator.class)
public @interface EnumNamePattern {
	String[] enumCodes();
	String message() default "Enum value is invalid.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

 

EnumNamePatternValidator.class

public class EnumNamePatternValidator implements ConstraintValidator<EnumNamePattern, Enum<?>> {

	private Pattern pattern;
	 
	@Override
	public void initialize(EnumNamePattern annotation) {
		try {
			StringBuilder sb = new StringBuilder();
    		 
			sb.append( annotation.enumCodes()[0] );
			for( int i=1, len = annotation.enumCodes().length; i<len; i++ ) {
				sb.append( "|" ).append( annotation.enumCodes()[i] );
			}
			pattern = Pattern.compile( sb.toString() );
             
		} catch ( PatternSyntaxException e ) {
			throw new IllegalArgumentException("Given regex is invalid", e);
		}
	}
 
	@Override
	public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
		if (value == null) {
			return false;
		}
 
		Matcher m = pattern.matcher(value.name());
		return m.matches();
	}
}

 

VO객체 일부 발췌.class

@EnumNamePattern(enumCodes = {"BAD_USER"})
private UserAuthority userAuthority;

간략하게 설명하자면 다음과 같다.

1. @EnumNamePattern(enumCodes={"BAD_USER"})의 경우, BAD_USER만 받는 VO 객체이다. 

2. 해당 사항은 Custom Validation과 어노테이션을 추가한 것이다.

3. EnumNamePatternValidator에서 정규 표현식을 통해 참 / 거짓을 판단한다.

4. EnumNamePatternValidator에서 null 값에 대해 는 거짓으로 판단한다. 

initialize 메소드에서 최대한 작업할 수 있는 것을 작업하자, isValid 에서 매 Request마다 작업을 진행하기 때문이다. 

 

MyBatis에서의 Enum활용(TypeHandler)

이제 Controller 생성(아래 참고) 및 MyBatis를 연동한다. 우리는 "BAD_USER"가 아닌 DB 코드 값 "USR_004"를 입력하고 싶다. Typehandler를 적용하지 않을 경우, BAD_USER가 입력되는 불상사가 생긴다. MyBatis TypeHandler는 JDBC 프로그래밍에서의 ResultSet, PreparedStatement를 원하는대로 핸들링할 수 있다.

 

TypeHandler 미적용 감사로그(BAD_USER가 insert 된다)

[***]-2020-07-20 22:35:21.741 main DEBUG: **.mapper.enum.insertEnumTest - ==>  Preparing: insert into enum_test( user_id, user_name, user_authority ) values ( ?, ?, ? ); 
[***]-2020-07-20 22:35:21.764 main DEBUG: **.insertEnumTest - ==> Parameters: ehdvudee(String), shin(String), BAD_USER(String)
[***]-2020-07-20 22:35:21.770 main DEBUG: **.enum.insertEnumTest - <==    Updates: 1
[***]-2020-07-20 22:35:21.773 main DEBUG: **.mapper.enum.selectEnumTest - ==>  Preparing: select user_id userId, user_name userName, user_authority userAuthority from enum_test 
[***]-2020-07-20 22:35:21.773 main DEBUG: **.mapper.enum.selectEnumTest - ==> Parameters: 
[***]-2020-07-20 22:35:21.800 main TRACE: **.mapper.enum.selectEnumTest - <==    Columns: userid, username, userauthority
[***]-2020-07-20 22:35:21.800 main TRACE: **.mapper.enum.selectEnumTest - <==        Row: ehdvudee, shin, BAD_USER
[***]-2020-07-20 22:35:21.803 main DEBUG: **.mapper.enum.selectEnumTest - <==      Total: 1

 

VO객체 일부 발췌.class

@MappedTypes(UserAuthority.class)
public static class TypeHandler extends CodeEnumTypeHandler<UserAuthority> {
	public TypeHandler() {
		super( UserAuthority.class );
	}
}

위의 코드와 같이 @MappedTypes를 지정한다. 솔직히 원리는 잘 모르겠다(원리에 대해 학습이 필요). 

아래와 같이 TypeHandler 생성 및 MyBatis 설정에 TypeHandler를 추가한다. 

 

CodeEnumTypeHandler.class

public abstract class CodeEnumTypeHandler<E extends Enum<E>> implements TypeHandler<EnumTestVo.CodeEnum> {
	private Class<E> type;

	public CodeEnumTypeHandler( Class <E> type ) {
		this.type = type;
	}

	@Override
	public void setParameter( PreparedStatement ps, int i, EnumTestVo.CodeEnum parameter, JdbcType jdbcType ) throws SQLException {
		ps.setString( i, parameter.getCode() );
	}

	@Override
	public EnumTestVo.CodeEnum getResult( ResultSet rs, String columnName ) throws SQLException {
		String code = rs.getString( columnName );
		return getCodeEnum( code );
	}

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

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

	private EnumTestVo.CodeEnum getCodeEnum( String code ) {
		try {
			EnumTestVo.CodeEnum[] enumConstants = (EnumTestVo.CodeEnum[]) type.getEnumConstants();
			for ( EnumTestVo.CodeEnum codeNum: enumConstants ) {           
				if ( codeNum.getCode().equals( code ) ) {
					return codeNum;
				}
			}
			
			return null;
		} catch (Exception e) {
			throw new TypeException("Can't make enum object '" + type + "'", e);
		}
	}
}

 

TypeHandler 적용(AppConfig.class)

private SqlSessionFactoryBean setCommonMybatisConfig( SqlSessionFactoryBean sessionFactory ) {
	sessionFactory.setTypeAliases( new Class<?>[] { 
		EnumTestVo.class
	});
	
	sessionFactory.setTypeHandlers( new TypeHandler[] {
		new EnumTestVo.UserAuthority.TypeHandler()
	});
	
	org.apache.ibatis.session.Configuration settings = new org.apache.ibatis.session.Configuration();
	
	settings.setCacheEnabled( false );
	settings.setJdbcTypeForNull( JdbcType.NULL );
	
	sessionFactory.setConfiguration( settings );
	
	return sessionFactory;
}

 

TypeHandler 적용 후("BAD_USER"가 아닌 "USR_004"가 들어가는 것을 확인할 수 있다.)

[***]-2020-07-20 22:40:01.195 main DEBUG: **.mapper.enum.insertEnumTest - ==>  Preparing: insert into enum_test( user_id, user_name, user_authority ) values ( ?, ?, ? ); 
[***]-2020-07-20 22:40:01.236 main DEBUG: **.mapper.enum.insertEnumTest - ==> Parameters: ehdvudee(String), shin(String), USR_004(String)
[***]-2020-07-20 22:40:01.257 main DEBUG: **.mapper.enum.insertEnumTest - <==    Updates: 1
[***]-2020-07-20 22:40:01.260 main DEBUG: **.mapper.enum.selectEnumTest - ==>  Preparing: select user_id userId, user_name userName, user_authority userAuthority from enum_test 
[***]-2020-07-20 22:40:01.261 main DEBUG: **.mapper.enum.selectEnumTest - ==> Parameters: 
[***]-2020-07-20 22:40:01.291 main TRACE: **.mapper.enum.selectEnumTest - <==    Columns: userid, username, userauthority
[***]-2020-07-20 22:40:01.292 main TRACE: **.mapper.enum.selectEnumTest - <==        Row: ehdvudee, shin, USR_004
[***]-2020-07-20 22:40:01.294 main DEBUG: **.mapper.enum.selectEnumTest - <==      Total: 1

 

최종 VO 객체

public class EnumTestVo {

	@NotEmpty
	private String userId;
	@NotEmpty
	private String userName;
	@EnumNamePattern(enumCodes = {"BAD_USER"})
	private UserAuthority userAuthority;
	
	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public UserAuthority getUserAuthority() {
		return userAuthority;
	}

	public void setUserAuthority(UserAuthority userAuthority) {
		this.userAuthority = userAuthority;
	}

	@Override
	public String toString() {
		return "EnumTestVo [userId=" + userId + ", userName=" + userName + ", userAuthority=" + userAuthority + "]";
	}
	
	public interface CodeEnum {
		public String getCode();
	}

	public enum UserAuthority implements CodeEnum {
		GREAT_USER( "USR_001" ),
		GOOD_USER( "USR_002" ),
		COMMON_USER( "USR_003" ),
		BAD_USER( "USR_004" ),
		GET_OUT_USER( "USR_005" );
		
		private String authCode;
		
		UserAuthority( String authCode ) {
			this.authCode = authCode;
		}

		@Override
		@JsonValue
		public String getCode() {
			return this.authCode;
		}
		
		@MappedTypes(UserAuthority.class)
		public static class TypeHandler extends CodeEnumTypeHandler<UserAuthority> {
			public TypeHandler() {
				super( UserAuthority.class );
			}
		}
	}
}

 

 

Spring/MyBatis 환경에서 Enum을 활용하기 위해서는 아래의 여건들이 충족되어야 한다.

  • MessageConverter의 Enum 처리 
  • Validation 체크
  • MyBatis에서 처리하기 위해 TypeHandler 지정

해당 사항 테스트 코드

해당사항을 테스트 하기 위해, Controller 생성 및 Spring 테스트 코드를 작성하였다. 

 

HomeController.class

@RestController
@RequestMapping("/")
public class HomeControlller {
		
	private final CommonErrorHandler commonErrorHandler;
	private final SqlSession session;
	
	@Autowired
	public HomeControlller( CommonErrorHandler commonErrorHandler, SqlSession session ) {
		this.commonErrorHandler = commonErrorHandler;
		this.session = session;
	}
		
	@RequestMapping(value="user", method=RequestMethod.POST)
	public ResponseEntity<?> createUser( @RequestBody @Valid EnumTestVo enumTestVo, BindingResult bindingResult, WebRequest request ) {
		if ( bindingResult.hasErrors() ) {
			String errMsg = commonErrorHandler.getValidationErrMsg( bindingResult );
			throw new CustomIllegalArgumentException( ErrorCode.INVALID_HTTP_BODY, errMsg );
		}
		System.out.println( enumTestVo );
		
		// SERVICE & DAO AREA 
		
		session.insert("**.mapper.enum.insertEnumTest", enumTestVo );
		EnumTestVo returnVo = session.selectOne( "**.mapper.enum.selectEnumTest", enumTestVo );
		
		// SERVICE & DAO AREA
		
		return ResponseEntity.ok( returnVo );
	}
}

 

EnumTest.class

 

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes= {WebConfig.class, AppConfig.class})
@Transactional
public class EnumTest {

	@Autowired
	private WebApplicationContext context;
	
	private MockMvc mockMvc;
	private ObjectMapper objectMapper; 
	
	@Before
	public void init() {
		this.mockMvc = MockMvcBuilders.webAppContextSetup( context ).build();
		this.objectMapper = new ObjectMapper();
		
		objectMapper.setSerializationInclusion( JsonInclude.Include.NON_NULL );
        objectMapper.setDateFormat( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss") );
	}
	
	@Test
	public void run() throws Exception {
		// GIVEN
		
		EnumTestVo vo = new EnumTestVo();
		
		vo.setUserId( "ehdvudee" );
		vo.setUserName( "shin" );
		vo.setUserAuthority( EnumTestVo.UserAuthority.BAD_USER );
		
		String jsonData = objectMapper.writeValueAsString( vo );
		
		System.out.println( "[Request JSON Body : " + jsonData + " ]");
		
		// WHEN THEN
		MvcResult result = mockMvc.perform( post( "/user" )
				.header( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE )
				.header( HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE )
                .header( HttpHeaders.CONTENT_ENCODING, "UTF-8" )
                .content( jsonData )
                
		)
		.andDo( print() )
		.andExpect( status().isOk() )
		.andReturn();
		
		System.out.println( "[Response Body : " + result.getResponse().getContentAsString() + " ]"); 
	}

}

 

최종 로그

[Request JSON Body : {"userId":"ehdvudee","userName":"shin","userAuthority":"USR_004"} ]

[***]-2020-07-20 22:40:01.195 main DEBUG: **.mapper.enum.insertEnumTest - ==>  Preparing: insert into enum_test( user_id, user_name, user_authority ) values ( ?, ?, ? ); 
[***]-2020-07-20 22:40:01.236 main DEBUG: **.mapper.enum.insertEnumTest - ==> Parameters: ehdvudee(String), shin(String), USR_004(String)
[***]-2020-07-20 22:40:01.257 main DEBUG: **.mapper.enum.insertEnumTest - <==    Updates: 1
[***]-2020-07-20 22:40:01.260 main DEBUG: **.mapper.enum.selectEnumTest - ==>  Preparing: select user_id userId, user_name userName, user_authority userAuthority from enum_test 
[***]-2020-07-20 22:40:01.261 main DEBUG: **.mapper.enum.selectEnumTest - ==> Parameters: 
[***]-2020-07-20 22:40:01.291 main TRACE: **.mapper.enum.selectEnumTest - <==    Columns: userid, username, userauthority
[***]-2020-07-20 22:40:01.292 main TRACE: **.mapper.enum.selectEnumTest - <==        Row: ehdvudee, shin, USR_004
[***]-2020-07-20 22:40:01.294 main DEBUG: **.mapper.enum.selectEnumTest - <==      Total: 1

MockHttpServletRequest:
         HTTP Method = POST
         Request URI = /user
          Parameters = {}
             Headers = {Content-Type=[application/json], Accept=[application/json], Content-Encoding=[UTF-8]}

             Handler:
                Type = **.HomeControlller
              Method = public org.springframework.http.ResponseEntity<?> **.HomeControlller.createUser(**.EnumTestVo,org.springframework.validation.BindingResult,org.springframework.web.context.request.WebRequest)

  Resolved Exception:
                Type = null

        ModelAndView:
           View name = null
                View = null
               Model = null

            FlashMap:

MockHttpServletResponse:
              Status = 200
       Error message = null
             Headers = {Content-Type=[application/json;charset=UTF-8]}
        Content type = application/json;charset=UTF-8
                Body = {"userId":"ehdvudee","userName":"shin","userAuthority":"USR_004"}
       Forwarded URL = null
      Redirected URL = null
             Cookies = []

[Response Body : {"userId":"ehdvudee","userName":"shin","userAuthority":"USR_004"} ]

 

참고

https://github.com/HomoEfficio/dev-tips/wiki/SpringMVC%EC%97%90%EC%84%9C-Collection%EC%9D%98-Validation

https://www.holaxprogramming.com/2015/11/12/spring-boot-mybatis-typehandler/

https://www.baeldung.com/javax-validations-enums

https://woowabros.github.io/tools/2017/07/10/java-enum-uses.html

https://androphil.tistory.com/707

https://www.baeldung.com/javax-validations-enums

https://sejoung.github.io/2019/06/2019-06-05-spring_jackson-enum-serializing-and-deserializer/#%EC%8A%A4%ED%94%84%EB%A7%81-jackson-enum-deserializer

Posted by 동팡

목차

 개요

 클래스 및 메소드 소개(중요사항만)

 코드 분석(중요사항만)

 

개요

SSS의 구현체를 분석한다. 분석 대상은 오픈 소스이다. 분석 대상 프로젝트의 링크는 다음과 같다.

https://github.com/timtiemens/secretshare

프로젝트 선정 기준은 다음과 같다.

  • 개발 언어 유형 – Java

  • 높은 스타 지수

  • 샘플 파일 및 최소한의 문서(READEME.md)

 클래스 및 메소드 소개(중요사항만)

[SecretShare.class(중요사항만 기재)]

내부 클래스 메소드 설명 비고
PublicInfo -

Secret Share를 생성하기 위한 공개 정보를 보관한다. 공개 정보는 아래와 같다.

-       k: (t, n) 구성 요소 중 t에 해당하는 값이다. 비밀을 만들기 위한 비밀 조각들의 임계치 값(최소 개수 값)

-       n: (t, n) 구성 요소 중 n에 해당하는 값이다. 생성하고자 하는 비밀 조각들의 개수이다.

-       primeModulus: 비밀을 만들 때 사용하는 소수 값이다.

-       Description Info: 기타 정보

모든 메소들은 유틸성이다.
 
SplitSecretOutput - split 메스도를 통해 반환되는 객체이다. 사용자는 해당 객체를 이용해서 시크릿 조각들을 사용할 수 있다.  
ShareInfo - 시크릿 조각을 갖고 있는 객체이다(x, y ).  
CombineOutput - 시크릿 조각 조합 후의 객체  
- split 시크릿 조각들을 생성한다.  
- combine 시크릿 조각 조합(비밀 생성)  

[유틸 클래스(중요사항만 기재)]

내부 클래스 메소드 설명 비고
PolyEquationImpl

-

다항 방정식을 처리하는 유틸클래스이다.

 
calculateFofX

x 값을 정의하면, 해당 값으로 방정식의 계산을 실시한다. 예는 다음과 같다.

fx = 1234 + 166x + 94x2  

위의 값에 x1일 경우, 1494를 반환한다.

 
NumberSimplex -

선형대수 관련 연산을 할 때 사용.

시크릿 조각을 조립할 때 사용하는 핵심 클래스
 
BigRational - 큰 값의 유리수 처리  
Numbermatrix - Number를 확장한 객체에 대해 행렬을 형성해주는 클래스  

 

코드 분석(중요사항만)

[분석 대상]

- SecretShare.split 메소드

- SecretShare.combine 메소드

 

[SecretShare.split]

개요

: PublicInfo객체의 맴버 변수를 다음과 같이 지정한다.

-       n = 16

-       k = 3

-       prime = 1613

위를 해석하면 16개의 조각을 생성하며, 3개의 조각은 임계치를 뜻한다. , 3개의 조각이 존재할 경우 Secret을 생성할 수 있다. k3을 뜻하는 것은 이차방정식 그래프를 생성한 다는것이다. 예를 들면 아래와 같다(1234는 시크릿이다.).

 

 

코드 분석

        if (secret == null)
        {
            throw new SecretShareException("Secret cannot be null");
        }
        if (secret.signum() <= 0)
        {
            throw new SecretShareException("Secret cannot be negative");
        }
        if (publicInfo.getPrimeModulus() != null)
        {
            checkThatModulusIsAppropriate(publicInfo.getPrimeModulus(), secret);
        }

 - 유효성 검사를 실시한다.

 

BigInteger[] coeffs = new BigInteger[publicInfo.getK()];

 - k를 조회하여, k개의 계수를 생성한다 coeffscoefficient(계수)의 줄임말을 의미한다.

 - 계수 1개의 수준은 1개의 Array이다.

randomizeCoeffs(coeffs, random, publicInfo.getPrimeModulus(), secret);

private void randomizeCoeffs(final BigInteger[] coeffs, final Random random, final BigInteger modulus, final BigInteger secret)
    {
        for (int i = 1, n = coeffs.length; i < n; i++)
        {
            BigInteger big = null;

            //big = coeffGenOriginal(random); // see Issue#8
            big = coeffGenImproved(random, modulus);

            // FIX? TODO:? FIX?
            big = big.abs(); // make it positive

            coeffs[i] = big;

            // Book says "all coefficients are smaller than the modulus"
            if (modulus != null)
            {
                coeffs[i] = coeffs[i].mod(modulus);
            }

            // FIX? TODO: FIX? experiment says "all coefficients are smaller than the secret"
            coeffs[i] = coeffs[i].mod(secret);
        }
    }

 - Secret보다 작고, Prime보다 작은 Secret을 제외한 계수들을 생성한다.

 - 예를들면 S1234이고 k3일 때 S를 제외한 1차항, 2차항에 대해 랜덤 계수를 생성한다.

 - 0차항의 자리는 S 1234 값이 들어간다.

 

coeffs[0] = secret;

 - 0차 항은 S 값이다.

 

        SplitSecretOutput ret = new SplitSecretOutput(this.publicInfo, equation);

        for (int x = 1, n = publicInfo.getNforSplit() + 1; x < n; x++)
        {
            final BigInteger fofx = equation.calculateFofX(BigInteger.valueOf(x));
            BigInteger data = fofx;
            if (publicInfo.primeModulus != null)
            {
                data = data.mod(publicInfo.primeModulus);
            }
            final ShareInfo share = new ShareInfo(x, data, this.publicInfo);
            if (publicInfo.primeModulus != null)
            {
                if (data.compareTo(publicInfo.getPrimeModulus()) > 0)
                {
                    throw new RuntimeException("" + data + "\n" + publicInfo.getPrimeModulus() + "\n");
                }
            }
            ret.sharesInfo.add(share);
        }

        return ret;

 - x값은 1부터 시작하기 때문에 1부터 시작하여, n(16) 까지 반복을 실시한다.

 - calculateFofX를 통해 방정식의 셈을 실시한다.

 - 해당 결과 값에 대해 mod를 실시한다.

 - 생성한 Secret 조각들은 ShareInfo 객체에 적재한다.

 - ShareInfo 객체는 SplitSecretOutput에 적재, 사용자는 최종적으로 SplitSecretOutput을 반환 받는다.

 - 해당 과정은 다음의 그림을 만드는 과정과 같다.

 

[SecretShare.combine]

개요

ShareInfo는 다음과 같다.

-       (1, 1494)

-       (2, 329)

-       (3, 965)

위의 ShareInfo 객체는 비밀을 만들기 위해 사용되는 비밀 조각들이다. 해당 조각들을 이용하여 combine 메소드를 실행한다. 해당 메소드는 불필요한 부분이 많기 때문에 해당 부분은 전부 설명에서 제외하였다.

 

코드 분석

        CombineOutput ret = null;

        sanityCheckPublicInfos(publicInfo, usetheseshares);

        if (true)
        {
            println(" SOLVING USING THESE SHARES, mod=" + publicInfo.getPrimeModulus());
            for (ShareInfo si : usetheseshares)
            {
                println("   " + si.share);
            }
            println("end SOLVING USING THESE SHARES");
        }
        if (publicInfo.getK() > usetheseshares.size())
        {
            throw new SecretShareException("Must have " + publicInfo.getK() +
                                           " shares to solve.  Only provided " +
                                           usetheseshares.size());
        }

        checkForDuplicatesOrThrow(usetheseshares);

예외 처리

 

            BigInteger[][] matrix = ele.getMatrix();
            NumberMatrix.print("SS.java", matrix, out);
            println("CVT matrix.height=" + matrix.length + " width=" + matrix[0].length);
            BigRationalMatrix brm = BigRationalMatrix.create(matrix);
            NumberMatrix.print("SS.java brm", brm.getArray(), out);

            NumberSimplex<BigRational> simplex = new NumberSimplex<BigRational>(brm, 0);
            simplex.initForSolve(out);
            simplex.solve(out);

            BigRational answer = simplex.getAnswer(1);
            if (publicInfo.getPrimeModulus() != null)
            {
                solveSecret = answer.computeBigIntegerMod(publicInfo.getPrimeModulus());
            }
            else
            {
                solveSecret = answer.bigIntegerValue();
            }

 - 제일 중요한 부분이다.

 - 역행렬 연산을 통해 선형 방정식을 계산한다.

 

 - simplex.initForSolve(out);

  -> 해당 메소드에서 역행렬을 진행하기 위한 행렬을 생성한다.

 

 - simplex.solve(out);

  -> 역행렬 작업을 진행한다.

  -> 역행렬 작업은 NumberSimplex.createAbar(E[][] array, int I, int j) 메소드에서 진행한다.

 

 - 위와 같은 선형 방정식 행렬을 역행렬 작업을 진행한다.

 - 위와 같이 3x3 행렬이 있다.

 - 역행렬 공식은 위와 같다. 위의 작업을 simplex.solve(out)에서 진행한다.

 

 - 역행렬 작업이 끝나면 위와 같이 행렬이 구성되어 있다.

 

 - 위의 행렬을 토대로 NumberSimplex.solve 메소드에서 computeAnswers 메소드를 통해 행렬 곱을 진행한다.

 

            BigRational answer = simplex.getAnswer(0);
            if (publicInfo.getPrimeModulus() != null)
            {
                solveSecret = answer.computeBigIntegerMod(publicInfo.getPrimeModulus());
            }
            else
            {
                solveSecret = answer.bigIntegerValue();
            }

 - simplex.getAnswer(0) // 리턴 씨크릿

 - modulo 1613 작업을 진행한다.

 

 

Posted by 동팡

목차

프로젝트명

개요

데이터셋의 구성 현황

학습 전략

전처리

데이터 셋의 시각화

가설 정의

학습 알고리즘

비용 함수

옵티마이져

학습

데이터 검증

요약

한계

GitHub

 

프로젝트 명

병무청 신체 측정 정보를 통한 체중 예측

개요

공공데이터 포털에서 병무청 신체 계측 정보를 제공 받는다. 해당 데이터는 군대를 가기 위한 20대 남성의 신체정보를 계측한 데이터셋이다. 해당 데이터 셋을 학습 시킨 후 다음과 같이 체중을 예측한다. 신장, 허리 둘레, 가슴 둘레, 머리 둘레, 발 길이 값을 통해 체중을 예측한다.  데이터 셋의 구성은 아래와 같다. 학습방법은 대표적인 기본 방법인 선형 학습법을 사용한다. 

 

데이터셋의 구성 현황

항목

설명

비고

성별

 

연령

20(대부분 20대 초반)

 

데이터 개수

135,670

 

데이터 구성

순번, 날짜, 가슴 둘레, 소매 길이, 신장, 허리 둘레, 샅 높이, 머리 둘레, 발 길이, 몸무게

 

데이터 단위

cm

 

데이터 계측 기간

2013 ~ 2016

 

항목

설명

비고

순번

순번 값

 

측정 일자

yyyyMMDD 또는 yyyy

 

가슴 둘레

가슴둘레(단위: cm)

X1

소매 길이

소매 길이(단위: cm)

 

신장

신장(단위: cm)

x2

허리 둘레

허리둘레(단위: cm)

X3

샅 높이

실제 다리 길이(단위: cm)

 

머리 둘레

머리둘레(단위: cm)

x4

발 길이

발길이(단위: cm)

x5

체중

체중(단위: cm)

y

학습 전략

해당 데이터셋을 통해 지도학습을 하기위해서는 아래와 같은 절차가 존재한다.

 

1.가설 정의

2.학습 알고리즘 선정

3.학습을 위한 코스트 함수 정의

4.코스트를 낮추기 위한 옵티마이저 정의

5.학습률 및 횟수 정의

6.학습 및 데이터 검증

 

보통, 키가 크면 몸무게가 높다.” 또는 보통, 허리 둘레가 크면 몸무게가 높다.” 와 같이 가설을 잡는다. 해당 가설을 입증하기 위해, 데이터 학습과 검증을 진행한다. 데이터 학습을 진행하기 위해서는 가설에 상응하는 학습 알고리즘 선정, 코스트 함수 정의, 옵티마이저 정의가 필요하다. 해당 가설은 선형 회귀(Linear Regression)알고리즘을 통해 학습을 진행하며, 해당 학습 Cost를 최소화 하기 위해, 경사하강법(Gradient Descent) 알고리즘을 적용하여, Cost의 저점을 찾는다. 경사하강법의 학습 폭은 학습률(Learning Rate)을 통해 조절할 수 있다.

전처리

해당 데이터셋은 CSV 형식으로 제공받을 수 있기 때문에 전처리 작업이 매우 수월하다.

데이터셋에서 학습에 사용할 데이터는 가슴 둘레, 신장, 허리 둘레, 머리 둘레, 발 길이, 체중이다. 여기서 체중을 제외한 5개의 값은 입력 값이며, 체중은 출력 값이다.

전처리 

데이터 셋의 시각화

- 해당 데이터셋에서의 신장, 허리 둘레, 체중을 시각화하였을 경우, 약한 선형 구조를 그릴 수 있다. , 상관관계가 존재한다.

- 허리 둘레와 체중 비교 그래프를 통해 허리 둘레가 크면, 체중도 크다는 사항에 대해 이해할 수 있다.

- 신장과 체중 비교 그래프는 규칙이 존재하지 않는다.  

- 허리 둘레와 체중 비교 그래프는 약한 선형 구조이다. 강한 선형구조를 도출하기 위해 신장”, “가슴 둘레등과 같은 값을 통해 강한 선형 구조를 그릴 수 있다. , 허리 둘레(x1) + 신장(x2) + x… = 체중(y)“ 이다.

가설 정의

현실 세계에서 납득할 수 있는 가설은 총 3개를 구할 수 있다.

-허리 둘레가 크면 몸무게가 높다.

-가슴 둘레가 크면 몸무게가 높다.

-신장이 크면 몸무게가 높다.

, 최종 가설은허리 둘레, 가슴 둘레, 신장이 크면 그만큼 몸무게 또한 크다.” 이다.

[학습 알고리즘] – 개요

보통 위의 가설과 같이 선형의 구조를 갖고 있는 경우, 학습 알고리즘은 선형 회귀를 선택한다. 선형 회귀는 선형 예측 함수를 사용해 회귀식을 모델링하며, 알려지지 않은 파라미터는 데이터로부터 예측한다. , 데이터[허리 둘레(x1), 가슴 둘레(x2), 신장(x3)]로 부터 알려지지 않은 파라미터[체중(y)]를 예측한다.

 

학습 방법 - [학습 알고리즘] – 설명

선형회귀 알고리즘을 시각화하면 다음과 같다.  

-선형 회귀는 일차방정식 처럼 Wb에 의해 가설 H 값이 달라진다.

-만약 허리 둘레, 가슴 둘레, 신장과 같이 값이 2개 이상일때는 선형 회귀(2)와 같다.

-우리는 H(X) = XW를 통해 다중 선형 회귀 또한 구성할 수 있다.

비용 함수

선형 회귀 알고리즘의 학습 정확도를 올리기 위해서는 cost 또는 loss를 줄여야한다. 가설과 실제 데이터 점의 거리가 짧아야 좋은 것이다. , 더 좋은 가설을 판단하기 위해서는 가설의 점과 실제 데이터의 점을 비교한다. 해당 점이 짧은 것이 좋은 가설이며, 해당 거리를 측정해주는 것을 cost/loss function이라 칭한다.

 

형 회귀의 Cost Function 식은 보통 (H(x)-y)^2 이다. 만약 2개 이상의 값이 있을 경우, Cost Function 식 그림과 같이 시그마 합 공식으로 표현할 수 있다. 해당 값을 작게 구하는 것이 선형 회귀의 학습이다.

옵티마이저

- Cost Function 최소값을 찾는 알고리즘을 옵티마이져라 한다. 해당 프로젝트는 경사 하강법(Gradient Descent) 옵티마이져를 통해 최소값을 구한다.

- 지속적인 반복 학습을 통해 Cost의 최소 값을 도출한다.

- 그러나, 데이터의 선형성이 존재하지 않으면 제대로된 Cost 감소를 실현하지 못한다.

학습

- 해당 가설을 입증하기 위해 선형 회귀(Linear Regression)알고리즘을 통해 학습을 진행

- Cost를 최소화 하기 위해, 경사하강법(Gradient Descent) 알고리즘을 적용

- 경사하강법의 학습 폭은 학습률(Learning Rate)을 통해 조절

- 학습률(Learning Rate)0.000015이다.

- 135,670개의 데이터를 5만번 학습 시켰으며, 학습률은 0.000015이다. 최종 Cost는 약 32.7382이다. 준수한 Cost를 얻었다.

데이터 검증

제일 중요한 부분이다. 우리가 가설을 정의하고 알고리즘을 선정하고 학습 시킨 모델이 제대로된 결과를 예측하는지 확인해야한다.

- 제일 좋은 방법은 데이터셋이 많을 경우, 데이터셋 중 1%만 검증에 사용하는 것이다.

- 또한 현실 세계에서 계측할 수 있으면 해당 값을 검증 데이터로 활용할 수 있다.

- 학습된 모델을 로컬 디스크에 저장하여, 해당 모델을 이용하여, 검증을 실시한다.

항목

순번

신장

허리둘레

가슴둘레

머리둘레

길이

실몸무게

예상치

(AI 계산)

오차(kg)

비고

검증 데이터

1

170.0

72.6

83.4

60.8

27.1

50.2

52.099712

+2

 

2

173.0

75.6

84.8

59.6

29.3

57.4

56.72512

-1

 

3

177.8

76.0

94.6

59.6

29.5

65.7

64.838196

-1

 

4

181.1

82.1

91.1

59.1

29.2

67.8

66.06932

-1

 

5

175.3

90.3

97.3

61.0

29.6

76.5

72.91974

-4

오차 벗어남

6

173.7

89.0

97.1

58.9

26.5

81.3

73.65506

-8

오차 벗어남

7

172.4

101.3

101.7

59.1

27.8

82.4

83.767784

+1

 

8

186.9

97.9

97.6

60.8

29.9

86.7

78.1594

-8

오차 벗어남

9

170.0

98.4

106.5

61.3

29.0

86.8

83.62165

-3

 

10

175.2

92.8

108.1

60.2

30.9

88.0

83.75437

-5

오차 벗어남

11

179.0

103.8

106.9

59.5

28.3

91

89.168076

-2

 

12

175.4

111.7

118.0

60.6

28.2

101.1

100.55304

-1

 

실세계

검증 데이터

13

183.2

98

102

61

28

87.5

80.801865

-7

오차 벗어남

14

181

115

121

60.5

28

107

105.02866

-2

 

15

173

85

89.5

56

26.5

68

68.778595

-

 

16

176

92

107

61

28

82

81.05168

-1

 

17

172

108

110

59

28

96

93.85968

-3

 

18

174.1

98

105

58

26.8

83

85.66101

-2

 

요약

- 사용자의 신장, 허리 둘레, 머리 둘레, 가슴 둘레, 발 길이를 통해 체중 예측

- 가설을 입증하기 위해 선형 회귀(Linear Regression)알고리즘을 통해 학습을 진행

- Cost를 최소화 하기 위해, 경사하강법(Gradient Descent) 알고리즘을 적용

- 경사하강법의 학습 폭은 학습률(Learning Rate)을 통해 조절

- 학습률(Learning Rate)0.000015이다.

- 135,670개의 데이터를 5만번 학습 시켰으며, 학습률은 0.000015이다. 최종 Cost는 약 32.7382이다. 준수한 Cost를 얻었다

- 데이터의 매칭률은 72.22%이다.

 

한계

- 체질량 지수(Body Mass Index, BMI)의 문제점은 순전히 키와 몸무게를 통해 비만을 판정한다.

- 해당 사항의 문제점은 체지방 비율, 근육량, 뼈밀도 등의 데이터가 배제되었다는 것이다.

- 현재 데이터 또한 해당 사항을 배제하였다.

- 결국 오차는 근육량, 체지방 비율, 뼈밀도 등의 값이 평균치 보다 많거나 적을 때 생길 수 있다.

- , 추가적으로 필요한 데이터셋은 나이, 근육량, 체지방량, 뼈밀도 이다. 해당 데이터셋을 통해 Cost5이하 최소화할 수 있을 것이다.

GitHub

https://github.com/ehdvudee/learn-tensor-project

 

ehdvudee/learn-tensor-project

Contribute to ehdvudee/learn-tensor-project development by creating an account on GitHub.

github.com

 

Posted by 동팡

해당 글은 이론/실무편으로 나누어져있다.

이론은 SSS의 이론에 대한 설명이며, 구현체 분석은 GitHub의 SSS 구현체 중 스타 지수가 높은 프로젝트의 소스코드를 분석한다.

 

목차

개요

이론 - 개념

이론 - 기본

이론 - 응용

장점

단점

보안

참고문헌

개요

비밀 공유(secret sharing)는 비밀키의 소유자가 1명이 아닌 다수의 인증된(authorized) 참가자(participant) 들이 되고, 이 때 임의의 참가자는 비밀키의 일부 조각을 소유하게 되는 것이다. 예를 들어, 임의의 그룹 내에 n명의 인증된 참가자가 존재할 경우 비밀키를 n개의 조각으로 분리하여 참가자들에게 각각 비밀 조각 1개씩 나눈다. 이 중 적어 도 n명 이상의 인증된 참가자들 모이면 이들의 비밀 조각들을 이용해 본래의 비밀키(또는 비밀데이터)를 알게 된다

즉, 비밀키의 조각을 나눠 갖은 3명이 존재한다. 해당 비밀키를 복구하기 위해서는 무조건 3명이 존재해야 한다.

이론 - 개념

선 1줄 요약: 선을 정의하는데 필요한 점의 수

 

SSS의 이해 – 1

 

일차 함수 그래프를 통한 비밀번호 공유

참석자: A, B, C

 

y = x + 3 일차 함수 그래프

  1. A는 위의 그림과 같은 공식(y = x + 3, 일차 함수 그래프)으로 비밀번호를 생성하였다.
  2. A는 비밀번호를 복구를 위해, BC에게 (-2, 1)(1, 4)를 각각 공유하였다
    1. B 또는 C는 혼자서 비밀번호를 구성(완성)하지 못한다.
    2. 점에서 선을 정의하기 위해서는 수많은(거의 무한의) 경우의 수가 존재하기 떄문이다.
  3. 만약 B, C가 합의를 한다면 비밀번호를 생성할 수 있다.
  4. 위와 같이 B, C의 부정행위를 방지할 수 없다.

 

SSS의 이해 – 2

 

이차 함수 그래프를 통한 비밀번호 공유

참석자: A, B, C, D

y= x^2 - 3x + 3

  1. A는 위의 그림과 같은 공식(y = x^2 -3x + 3, 이차 함수 그래프)으로 비밀번호를 생성하였다.

    1. A의 베프는 D이다. 이 친구는 절대 배신을 하지 않는다.

  2. A는 비밀번호를 복구를 위해, BCD에게 (1.5, 0.75), (0, 3), (3,3)의 값을 각각 공유하였다. 그리고, 자기 자신은 (2, 1)을 갖고 있는다.

    1. B, C와 공모를 하였지만 비밀번호를 추측하지 못한다.

    2. 해당 공식은 3개의 점이 필요하기 때문이다.

  3. 만약 A는 비밀번호를 깜박하여, 비밀번호의 재생성이 필요하다.

  4.  BCD를 소집하여, 비밀번호를 생성한다.

  5. 만약 C가 해당 값을 잃어버리거나 또는 해당 값이 compromised가 되면 ABD는 소집하여, 해당 값을 생성한다.

  6. 해당 값으로 다시 그래프를 생성한 후, ABCD는 새로운 인자를 나눠 갖는다

해당 공식은 3개의 점만 필요하지만, 보안을 위해 공식을 재구성할 수 있다. 예를 들어, 5, 10, 15개 그 이상의 점을 필요로 하는 것이다. 위의 예는 2차 함수 그래프이며, 구성을 위해 최소 3개의 점이 필요했다. 해당 사항을 4,5,6차 함수 그래프를 사용한다. 그러면, n차 함수 그래프는 n+1의 점으로 그래프를 형성할 수 있다. 아래와 같이 방정식으로 표현할 수 있다.

이것을 (t, n) threshold secret sharing이라 칭한다. n개의 비밀 조각들을 제공하고 비밀을 재구성하려면 최소 t조각의 임계 값이 필요하다. 위의 사항은(3, 4)이다. 이게 바로 SSS의 기본 이론이다. t-1차 다항식이 후보로 존재하여 원래의 다항식을 추측할 수 없다. , t개보다 적은 비밀 조각이 모이더라도 원래의 비밀 값은 알 수 없다.

 

이론 - 기본

Preparation

우리의 Secret1234이다(S = 1234).

해당 Secret6조각으로 분리한다. 6조각(n = 6) 3조각(k = 3)이 모이면 Secret을 생성할 수 있다. k – 1 값은 166, 94이다.

Secret이다.

 

여기서 마지막 k 값 즉, 마지막 공유 조각을 만드는 식은 아래와 같다.

 

 

Reconstruction

Secret 3개의 조각만 있으면 만들 수 있다. 2, 4, 5를 통해 Secret을 생성하면 다음의 식과 같다.

해당 식은 라그랑주 다항식[1]을 통해 아래와 같이 연산할 수 있다

 

해당 식을 시그마 공식으로 유도 후 계산하면 아래와 같다.

Secert S 값을 구할 수 있다.

해당 값은 아래와 같은 수식으로 표현(간소화)할 수 있다.

[표현 - 1(방정식 나열)]

[표현 - 2(방데르몽드 행렬)]

[표현 - 3(시그마 합공식)]

이론 - 응용

 

위의 식은 SSS의 기본 식이다. 위의 다항식은 정수 산술을 사용하기 때문에 안전하지가 않다. 공격자는 그래프의 포인트를 얻을 때마다, 포인트 경우의 수가 줄어든다. 예를 들어, 5차 함수 그래프를 그릴 때, SSSk 6이 된다. 즉 임계치는 6이 되며 6조각이 모이면 S를 유도할 수 있다. 그런데 공격자가 k-1까지의 값을 갖고 있으며, 해당 값으로 그래프를 표현해본다. 그러면 얼추 다음의 지점의 경우의 수가 많이 감소한다. 해당사항을 시각적으로 확인할 수 있다(상상해보라).

해당 문제는 다음과 같이 해결할 수 있다.

소수의 모듈러 연산을 통해 그래프를 꼬아 버린다.

모듈러 연산에 사용하는 소수 값의 조건은 아래와 같다. 아래의 조건을 설명하면 다음과 같다. P , prime(소수) pn(공유 조각들)보다 크고, S(비밀값)보다 크다. prime의 집합이다. p는 그냥 소수라는 의미이다.

예는 아래와 같다.

k or t = 3

n = 6

k-1 = 166, 94

S = 1234

P = 1613

 

공유 비밀의 값이 1234일 때 우리는 아래와 같은 SSS 공식을 사용하여, 비밀 조각을 생성할 수 있다.

[공유 비밀 생성]

[비밀 재구성 – 1(basic)]

 

설명

1. 유리수의 modulo 연산의 경우, 정수가 될 때까지 + 또는 연산을 통해 정수로 변경한다. 그 후, modulo 연산을 진행한다.

2. 음수의 modulo 연산의 경우, 일반 양수 mod 연산 후, 음수로 변경한 후, m을 더한다(: -22 mod 5, 22 mod 52이다. -2 + 5 = 3. , -22 mod 53이다.

[비밀 재구성 – 2(optimized)]

공유를 재구성할 때 아래의 식을 적용한다(j, m index0부터 시작할 때).

식의 풀이는 다음과 같다(i, jindex1부터 시작할 때)

위의 식이 최적화 식인 이유는 아래와 같이 필요한 지수에 대해서만 연산을 하기 때문이다(우리가 알고 싶은 것은 0차항 즉, Secret 값이다.). , 1234 + 166x + 94x^2의 값 중에 필요없는 16694는 연산에서 제외하고, 비밀 값 1234만 연산한다.

[대입]

위의 식이 최적화 식인 이유는 아래와 같이 필요한 지수에 대해서만 연산을 하기 때문이다(우리가 알고 싶은 것은 0차항 즉, Secret 값이다.). , 1234 + 166x + 94x^2의 값 중에 필요없는 16694는 연산에서 제외하고, 비밀 값 1234만 연산한다.

 

장점

Shamir’s secret sharing의 특징은 모든 구성원이 나눠갖는 비밀이 서로 유일(unique)하며 평등하다는 것이다. 또한 threshold 이상의 조각만 있으면 secret를 복구할 수 있기 때문에 일부 비밀 조각이 유실되더라도 안전하고, 소수의 배신자가 있더라도 secret을 복구할 수 없다.

단점

이런 특징들은 한계점이 되기도 한다. 모든 shared secret이 동등하기 때문에 비밀을 나눌 때 보안 등급에 따라 나누기가 힘들고, 비밀 조각을 제공할 때 일부러 틀린 비밀 값을 주는 것을 방지할 수 없으며 비밀을 복구할 때도 누군가 오염된 값을 줘서 비밀이 제대로 복구되었는지 알 수 없다.

그러나, 이와 같은 단점들은 타계층(: 어플리케이션 계층)에서 보완하면 되는 부분이라 생각한다. , 단점이라기보다는 필요 보완사항이 되겠다.

보안

SSS의 보안 내성은 정보 이론적 보안(Information-theoretic security)이다[1]. 정보 이론으로부터 순수하게 파생된 암호시스템이다. 공격자가 암호를 깨기 위한 충분한 정보가 있지 않은 이상, 공격자의 전사 공격(brute force attack)으로부터 안전하다. , 공격자가 아무리 (t, n) 구성 요소 중 t-1의 값을 갖고 있어도 안전하다. 쉽게 생각해서 1개를 이용하여, 선을 예측하라이다. 가능한가?

그러나, 2차 방정식 이상의 그래프는 그래프의 기울기를 얼추 예상할 수 있다. 때문에, 해당 기울기조차 쪼개어버리는 것이다. 그림 1의 그래프를 modulo 연산을 통해 그림 2처럼 쪼개어 버린다.

 

Modulo 연산을 통한 취약점 보완

 

위의 그림 1,2를 통해 맥락을 이해한 다음, 그림 3을 통해 이해를 마무리할 수 있다.

 

참고문헌

 

https://medium.com/wanchain-foundation/secure-multiparty-computation-and-shamirs-secret-sharing-on-wanchain-e502012b80ef

https://crypto.stackexchange.com/questions/31150/how-secure-is-shamirs-secret-sharing-for-password-sharing-when-attacker-has-t-1

https://phy64ev1.tistory.com/13

https://en.wikipedia.org/wiki/Information-theoretic_security

https://ericrafaloff.com/shamirs-secret-sharing-scheme/

https://www.youtube.com/watch?v=j7Ngtl1cCpY

https://web.engr.oregonstate.edu/~rosulekm/crypto/chap3.pdf

https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing

https://ko.wikipedia.org/wiki/%EB%B0%A9%EB%8D%B0%EB%A5%B4%EB%AA%BD%EB%93%9C_%ED%96%89%EB%A0%AC

https://www.impan.pl/wydawnictwa/preprints_impan/p708.pdf

https://ncalculators.com/matrix/inverse-matrix.htm

https://mathkorea.wordpress.com/2012/06/08/%EA%B0%80%EC%9A%B0%EC%8A%A4-%EC%A1%B0%EB%A5%B4%EB%8B%A8-%EC%86%8C%EA%B1%B0%EB%B2%95%EC%9C%BC%EB%A1%9C-%EC%97%AD%ED%96%89%EB%A0%AC-%EA%B5%AC%ED%95%98%EA%B8%B0/

http://blog.naver.com/PostView.nhn?blogId=agnes0129&logNo=203468722&redirect=Dlog&widgetTypeCall=true&directAccess=false

https://matrixcalc.org/ko/

https://ncalculators.com/matrix/inverse-matrix.htm

https://github.com/timtiemens/secretshare/issues/9

 

 

(해당 문서는 워드에서 작성한 문서이다. 수식은 다 이미지로 복붙했다. 별도 해당 문서 원본 PDF 첨부 참고) 

SSS(Shamir_Secret_Sharing).pdf
0.56MB

(티스토리 워드 원격 글 등록 API가 잘 되어있지 않다. 빙신이다ㅡㅡ)

 

Posted by 동팡

목차

  • 상황
  • REST API 속도 개선 기술
  • 스프링 캐시
  • 용례1 - 1억건 통계 데이터 처리 API 캐싱
  • 용례2 - KMS 캐싱 (EHCache를 통한 기능 추가)

상황

  • 우연히 1억건의 DB 데이터를 처리하는 API 서버를 개발하게 되었다. 그러나 해당 API 서버에서의 요청 처리 시간은 5~ 15, 끔직한 상황이다. REST API의 속도 개선이 절실히 필요하다.

REST API 속도 개선 기술

 

REST API(HTTP 통신)는 일단 느리다. 그러나, 우리는 웹어플리케이션(REST API)의 속도 개선을 위해 다음과 같은 기술들을 사용할 수 있다.

  • HTTP 프로토콜의 Cache-Control, ETag를 통한 네트워크 레이턴시 감소

  • DBMS의 딕셔너리 캐시/라이브러리 캐시/ 버퍼 캐시의 사용으로 인한 디스크 I/O 최소화

  • DBMS SQL 쿼리 튜닝

  • 어플리케이션의 소스 코드 개선

HTTP 프로토콜의 Cache-ControlETag를 동적 컨텐츠 즉, REST API에 적용하고 사용하기가 까다로우며(클라이언트의 수정이 요함), 효과가 미비하다.

 

DBMS의 엔진 튜닝 및 SQL 쿼리 튜닝도 한계가 있다(나는 전문가가 아니다.) SQL 튜닝 해봤자 실행계획 보고 쿼리 변경 및 인덱스 추가이다. 이번에는 쿼리 소트 부하가 상당하며, 해당사항을 해결하기 위한 SQL 튜닝 지식이 부족했다.

 

SQL 결과 값을 재가공하기 때문에 최소 O(n2)의 시간복잡도를 갖고 있다. 이런 시간복잡도는 어쩔 수 없다. 결론은 어플리케이션의 소스 코드 개선은 가성비가 좋지 못하다.

 

결론은 어플리케이션의 캐시 기술을 사용한다. , REST API 결과를 캐싱하는 것이다. 우리는 주변에서 많은 캐싱 기술과 용례를 보고 있었는데 왜 이것을 여지껏 몰랐을까? 정말 의문이다.

 

 

스프링 캐시

스프링은 3.1 이상부터 캐시 추상화 기술을 제공한다. 설정과 어노테이션 추가를 통해 바로 쉽게 캐시를 적용할 수 있다(4.1에 확장을 했지만 기능은 잘 모르겠다.). 트랜잭션과 마찬가지로 AOP를 이용해 메소드 실행 과정에 투명하게 적용된다. 캐시 관련 코드를 어플리케이션 로직에 적용하지 않아도 된다. 또한 캐시 서비스의 종류가 달라져도 괜찮다(캐시 impl/provider 변경). 캐시 기능과 어플리케이션의 기능이 이렇게 분리 되어있다. 해당 사항은 스프링에 대표적인 기술인 서비스 추상화 기술이다.

 

스프링 캐시를 사용하기 위해서는 CacheManager 설정과 해당 설정을 적용할 Service 메소드를 지정해야한다. Service 메소드는 트랜잭션의 설정과 같이 어노테이션 지정만으로 깔끔하게 캐시 기능을 구현할 수 있다.

 

CacheManager 설정은 용례1,2와 같이 설정한다. 간단한 설정은 용례 1과 같이하면된다.

 

Service 메소드에 적용하는 어노테이션은 다음과 같다.

@Cacheable

  • 해당 어노테이션이 지정된 메소드는 캐시기능을 제공할 수 있다. 해당 어노테이션에서 자주 사용하는 값은 key, value, condition 정도이다.

  • value 값을 통해 캐시 분류(: 사용자 조회 캐시/게시글 조회 캐시 분리)

  • key 값을 통해 인자별 캐시 분류(: 1(key) 사용자에 대한 캐싱) SpEL을 활용할 수 있다.

  • condition을 통해 일정 조건에 대해 충족하는 경우에만 캐싱

  • syncSpring 4.3 이상 지원되는 기능이며, 해당 기능을 통해 쓰레드 세이프하게 캐시를 생성한다(여러 스레드가 한번에 같은 캐시를 생성하는 것을 막는다.).

@CacheEvicit

  • 캐시된 데이터는 갱신되어야 한다. 해당 어노테이션이 지정된 메소드는 캐시 삭제 기능을 제공할 수 있다.

  • value/key/condition@Cacheable 설명과 같다. 다만 캐시 삭제에 초점이 맞춰져있다.

  • allEntiries 값을 통해 key 별 캐시 삭제가 아닌, 모든 key의 캐시 삭제가 가능하다.

  • beforeInvocation을 통해 메소드 실행 전 캐시를 삭제할 수 있다.

@CachePut

  • 해당 어노테이션이 지정된 메소드는 캐시하고자 하는 데이터를 저장’ 한다. 용례는 다음과 같다. 많은 데이터의 효율적인 캐시 기능 제공을 위해, 미리 저장만 하고 추후에 조회 API는 캐시된 데이터만 반환한다.

  • 아래의 코드 블럭으로 통해 설명이 가능하다.
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

CacheManager

AOP를 이용해 캐시 부가기능을 적용할 수 있게 한다. 동시에 캐시 기술(프로바이더) 상관없이 추상화된 스프링 캐시 API를 이용할 수 잇게 서비스 추상화를 제공한다. 캐시 매니저는 CacheManager 인터페이스를 사용하며, 5가지의 concrete class를 제공한다. Concrete class는 다음과 같다.

ConcurrentMapCacheManager

ConcurrentHashMap을 이용하여 간단한 캐시 기능을 구현한 CacheManager이다. 해당 CahceManager의 경우, 캐시 용량 제한, 캐싱 알고리즘, TTL 등의 부가 기능은 없기 떄문에, 테스트 용도로만 사용을 권고한다(용례 1에 해당한다.).

 

SimpleCacheManager

기본적으로 제공하는 캐시가 없으며, 직접 등록해줘야 한다. 스프링 Cache 인터페이스를 직접 개발했을 때 테스트할 때 사용할 수 있다(용례 1에 해당한다.).

 

EhCacheCacheManager

EhCahce는 캐시 용량 제한, 캐싱 알고리즘, TTL, TTI, 파일 캐싱, 분산 서버 캐싱 등의 고급 기능을 제공하는 Java에서 제일 인기 있는 캐시 프레임워크이다. 해당 설정들은 XML 또는 JavaConfig를 통해 설정할 수 있다. 용례 2JavaConfig를 통해 설정하였다(XML 설정은 정보가 많으나, JavaConfig는 정보가 정말 부족하다.)(용례 2에 해당한다.).

 

CompositeCacheManager

CompositeCacheManager는 하나 이상의 캐시 매니저를 사용하게 지원해주는 혼합 캐시 매니저이다.

 

NoOpCacheManager

패스ㅋ

 

용례1 - 1억건 통계 데이터 처리 API 캐싱

상황

1. DB 로우는 1억건

2. SQL을 통해 조회 하는 값 백~만 건

3. 해당 사항을 어플리케이션의 데이터 재가공

 

pom.xml

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${org.springframework-version}</version>
</dependency>

스프링 캐시 추상화를 사용하기 위해서는 spring-context-support JAR가 필요하다.

 

AppConfig.class( Cache 부분 발췌)

@EnableCaching
public class AppConfig {

   @Bean
   public CacheManager cacheManager() {
      SimpleCacheManager cacheManager = new SimpleCacheManager();

      cacheManager.setCaches( Arrays.asList(
            new ConcurrentMapCache( "default-keyword" ),
            new ConcurrentMapCache( "frequent-keyword" ),
            new ConcurrentMapCache( "trend-keyword" ),
            new ConcurrentMapCache( "relation-keyword" )
      ));

      return cacheManager;
   }

스프링에서 기본으로 제공하는 SimpleCacheManager를 사용한다.

 

Service.class

@Cacheable(cacheNames = "frequent-keyword", key="{#request.getParameter('returnCnt'), #request.getParameter('ageCode')," +
        " #request.getParameter('sex'),#request.getParameter('city'),#request.getParameter('county') }")
public List<Map<String, Object>> selectFrequentKeyword( HttpServletRequest request ){ … }

@Cacheable 어노테이션을 통해 key별 캐시 데이터를 생성할 수 있다.

 

 

정말 간단하게 5~15초 걸리던 API는 캐시를 통해 0.2~0.1초로 바꿀 수 있었다. 그러나, 우리는 여기서 간과한 사항이 있다. 먼저, 캐시의 주의사항을 파악한 후, 생각해 본다. 캐시 사용 주의사항은 다음과 같다.

  • 연산의 결과가 달라졌을 때의 데이터의 갱신 전략

  • 캐시의 TTL(Time To Live), 캐시 만료일

  • 캐시 메모리 교체 알고리즘의 선택(LRU, LFU )

  • 캐시 저장소의 선택(파일, 메모리, DBMS, 네트워크 등)

제일 중요한 데이터 갱신 전략이 적용되지 않았다. 그러나, 다행이 이 프로젝트의 DB 값을 변경될 예정이 없고 항상 같은 값을 반환하는 API이기 때문에 데이터의 갱신 전략은 생략해도 괜찮았다. 또한 부가 기능도 필요없었다.

 

하지만 너무 아쉽다. 삽이 필요하다.

결국 EhCache 삽질을 한다. 용례2와 같다.

 

용례2 - 조회 API 캐싱 (EHCache를 통한 기능 추가)

조회 부분에 적용한다. 설정 내역은 아래와 같다.

 

pom.xml

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${org.springframework-version}</version>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.9.0</version>
</dependency>

EhCacheCacheManager를 사용하기 위해서는 EhCache 라이브러리가 필요하다.

 

AppConfig(KeyCacheConfig)

@Configuration
@EnableCaching(order=400)
public class KeyCacheConfig implements CachingConfigurer {

   private final CacheManager cacheManager;
   private final net.sf.ehcache.CacheManager ehCacheManager;


   public KeyCacheConfig() {

      CacheConfiguration cacheConfiguration = new CacheConfiguration()
            .name( "default-cache" )
            .memoryStoreEvictionPolicy( "LRU" )
            .maxEntriesLocalHeap( 1000 )
            .timeToLiveSeconds( 86400 /*60 * 60 * 24 */ );

      net.sf.ehcache.config.Configuration config = new net.sf.ehcache.config.Configuration();

      // 디스크 캐시 사용할 경우 적용
//    setDiskCache( config, cacheConfiguration );
      config.addCache( cacheConfiguration );

      this.ehCacheManager = net.sf.ehcache.CacheManager.newInstance( config );
      this.cacheManager = new EhCacheCacheManager( ehCacheManager );
   }

   private void setDiskCache( net.sf.ehcache.config.Configuration config, CacheConfiguration cacheConfiguration ) {
      PersistenceConfiguration persistanceConf = new PersistenceConfiguration();
      persistanceConf.strategy( Strategy.LOCALTEMPSWAP );

      cacheConfiguration.addPersistence( persistanceConf );

      DiskStoreConfiguration diskCacheConfig = new DiskStoreConfiguration();
      String path = this.getClass().getClassLoader().getResource( "./cache" ).getPath();

      diskCacheConfig.path( path );
      config.addDiskStore( diskCacheConfig );
   }

   @Bean(destroyMethod="shutdown")
   public net.sf.ehcache.CacheManager ehCacheManager() {
      return ehCacheManager;
   }

   @Bean
   @Override
   public CacheManager cacheManager() {
      return cacheManager;
   }

   @Bean
   @Override
   public CacheResolver cacheResolver() {
      return new SimpleCacheResolver( cacheManager );
   }

   @Bean
   @Override
   public KeyGenerator keyGenerator() {
      return new SimpleKeyGenerator();
   }

   @Bean
   @Override
   public CacheErrorHandler errorHandler() {
      return new SimpleCacheErrorHandler();
   }
}

해당 설정은 아래의 URL을 참고하였다.

https://stackoverflow.com/questions/21944202/using-ehcache-in-spring-4-without-xml

CacheConfiguration 설정을 통해 다음의 설정을 할 수 있다.

  • 캐싱 알고리즘

  • 캐시 생성 최대 개수

  • 캐시 생성 최대 메모리

  • TTL(캐시 생성 후, 자동으로 캐시 데이터 삭제 일)

  • TTI(캐시 생성 후, 자동으로 캐시 데이터 삭제 일, 다만 사용할 때 마다 초기화)

  • 기타 등등

또한 setDiskCache 메소드를 통해 디스크 캐시를 사용할 수 있다. 예를들어 캐시 데이터가 20MB 이며, 해당 데이터가 많을 경우, 메모리를 사용하면 Memory leak이 발생할 것이다. 해당 경우를 상쇄하기 위해 key 값은 메모리가 갖고 있고 value 값은 디스크가 갖고있는다.

 

또한 암호키 조회의 경우, 암호키 값/기간 무결성 검증 로직이 있기 때문에 TTL은 하루만 설정하였다.

 

여기서 나오는 아쉬움은 암호키의 값/기간 무결성 검증 로직 또한 AOP로 분류 했으면 해당 암호키 캐싱은 암호키가 폐기할 때 까지 캐시할 수 있을 것이다. 정말 아쉽다.

 

Service

@Override
@AsyncUriAnnotation
@Transactional
@Cacheable( value="default-cache", key="{#identifier}" )
public Map<String, List<Map<String, Object>>> getKey(String identifier, HttpServletRequest request )
{ ... }

여기서 집중해야할 어노테이션은 Cacheable이다.

 

참고문헌

토비의 스프링 vol 2

https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache

https://www.baeldung.com/spring-cache-tutorial#use-caching-with-annotations

https://www.baeldung.com/spring-cache-tutorial

https://www.ehcache.org/documentation/2.7/configuration/fast-restart.html

https://www.ehcache.org/documentation/2.8/get-started/storage-options.html#diskstore-

https://minwan1.github.io/2018/03/18/2018-03-18-Spring-Cache/

https://javacan.tistory.com/entry/133

https://syaku.tistory.com/322

https://stackoverflow.com/questions/13381731/caching-with-multiple-keys

https://stackoverflow.com/questions/8181768/can-i-set-a-ttl-for-cacheable

https://mkyong.com/spring/spring-caching-and-ehcache-example/

https://stackoverflow.com/questions/21944202/using-ehcache-in-spring-4-without-xml

Posted by 동팡

목차

들어가면서

SQLite 특징

SQLite 이럴 때 사용 O

SQLite 이럴 때 사용 X

SQLite 아키텍쳐

SQLite Lock 개념

SQLite 트랜잭션 개념

부록) Oracle WAL 처리 방식

포팅할 때 겪었던 명령어들

SQLite 용례(실사용기)

참고문헌

들어가면서

해당 문서는 실제 프로젝트에 적용하면서 정리한 내용, 보통의 이론적인 내용 SQLite DOC 및 괜찮은 블로그들을 참고 했다.

SQLite 특징

SQLite C언어로 개발된 embeded 데이터베이스 엔진이다. SQLite의 특징은 다음과 같다.

 

  • 경량화: SQLite Library 1MB ~ 0.6MB 이하이다. 또한 보통의 파일시스템 컨트롤 보다 약 30% 빠르다.
  • 독립적: 표준 C 라이브러리를 통해 개발되었으며, 외부 라이브러리 또는 인터페이스에 종속적인 부분은 없으며, 단순 운영체제 위에서 독립적으로 혼자 잘 돌아간다.
  • 신뢰성: SQLite는 많은 스마트폰, IoT장치, 데스트콥 어플리케이션에서 오래 사용되었다. 수치로 했을 경우, 몇억개의 사용처가 10년 넘게 유지되었으며, 아직까지 업데이트하며 유지보수(지속적인 오픈소스 개발 기여)가 되고 있다.
  • 무서버: 데몬을 띄우는 보통의 RDBMS와 달리 서버없이 디스크 I/O를 통해 DB에 접근한다(Neo-Serverless, Application과의 같은 프로세스가 아닌 별도의 프로세스 지만, 그 별도의 프로세스는 Application이 관리하에 존재한다. 결국 Serverless이다. ).
  • 무설정: 편리하다. 무서버의 특징으로 인해, 서버 프로세스의 설치, 설정, 초기화(가동), 관리, 트러블슈팅 등의 작업이 존재하지 않기 때문에 편리하다.
  • 오픈소스: 오픈소스 라이센스는 public Domain으로 라이센스가 존재하지는 않지만, 지속적인 개발과 매 릴리즈마다 엄격한 테스트 과정이 존재한다. 최신 릴리즈 버전은 2019 12월이다 ㄷㄷ.., 20주년 된 오픈소스 프로젝트이다.
  • 1개의 데이터 파일: 말마따나 모든 데이터를 1개의 디스크 파일로 표현할 수 있다.
  • 크로스 플랫폼 데이터 파일: 32bit / 64Bit, Big / Little Endian, 운영체제 상관없이 데이터 파일을 사용할 수 있다(VFM을 사용하기 때문에).

SQLite 이럴 때 사용 O

 SQLite의 경우, PostgreSQL, MariaDB와 같은 clinet/server SQL DB 엔진과의 적절한 비교대상이 아니다. 해당 RDBMS는 엔터프라이즈 데이터의 공유 저장소 구현이기 때문에 확장성, 동시성, 중앙집중화 및 제어 등이 중요 초점 사항이다.

 

 SQLite 엔진은 개별 프로그램의 로컬 데이터 저장소 구현이기 때문에, 경제성, 효율성, 신뢰성, 독립 및 단순성이 중요 초점 사항이다(보통 스마트폰의 어플리케이션의 DB 사용을 생각하면 상상이 좀 더 쉽다.).

 

각각 Serverless SQLite client/server RDBMS와의 장단점이 존재한다.

 

SQLite의 사용처는 다음과 같다.

  • Embedded IoT 디바이스
  • SQLite DB는 관리자가 필요 없으며, 디바이스에서 혼자 잘 돌아갈 수 있다. 예는 다음과 같다. , TV, 게임 콘솔, 카메라, 시계, 부엌 장비, 자동차 등 모든 IoT 같은 장비
  • Mini/Toy 프로젝트 또는 소/중규모 웹사이트, 트래픽이 적은 웹사이트에서 무리없이 잘 작동한다.
  • 어플리케이션에서 엔터프라이즈 RDBMS의 데이터를 캐쉬하기 위해 사용될 수 있다.
  • 프로토타입/데모 프로젝트
    • 배보다 배꼽이 더큰 상황을 저지해줄 수 있다. 어플리케이션의 데모 또는 프로토타입을 할 때 임시적으로 사용도 괜찮다.

SQLite 이럴 때 사용 X

  • 다중 클라이언트, Database에 접근하는 많은 클라이언트가 존재할 경우(즉, 많은 동시성을 요구할 때)
  • 엔터프라이즈 환경, 대규모 프로젝트
  • 데이터웨어하우스, 데이터 대용량 처리의 제한, 해쉬조인을 지원하지 않는다. NL-Join(OLTP 적합)밖에 되지 않는다
  • 빅데이터, SQLite는 사이즈 제한은 140테라바이트 이다.
  • 빈도 높은 동시성, read에 대해 제한은 없으나 write은 딱 1개만 허용하기 때문에 동시성 보장을 못한다(WAL 모드를 통해 Read/Write를 동시에 수용할 수 있지만 결국 Write를 할 수 있는 프로세스는 1개이다.).
  • SQLite FAQ (5) Can multiple applications or multiple instances of the same application access a single database file at the same time?
    1. 여러 프로세스가 동시에 DB를 접근하여 SELECT를 할 수 있다, 그러나, 한개의 프로세스만이 Write(INSERT/UPDATE/DELTE)를 할 수 있다.
    2. SQLite reader/write Lock을 사용하여 DB 접근을 제어한다.
    3. 만약 특정 프로세스가 DB Write를 원하면, 파일 Write 하는 동안, Database 파일은 Lock이 걸려있으며, 이 때 Lock은 조회도 할 수 없다.
    4. , Write, DB 접속을 원하는 프로세스/쓰레드는 휴지기를 갖는다(아무것도 못한다.).         

SQLite 아키텍쳐

SQLite CoreBackend로 구성이 되어있다. CoreInterface, Tokenizer, Parser, Code generator, VM을 포함하고 있으며, BackendB-Tree, Pager, 파일 시스템에 접근하는 OS Interface를 포함하고 있다. Tokenizer, Parser, Code generatorVM에 코드를 돌리기 위한 Compiler로 칭하고 있다.

 

(아키택쳐는 Compiler의 Parser/Code Generator 부분이 많이 어려웠다.)

(해당 내요은 공식홈페이지와 참고문헌을 참고하는게 좀더 좋다)

SQLite DB 사용 흐름도

  • Interface: 해당 인터페이스를 통해 SQL 쿼리문을 보낸다.
  • Tokenzer: 요청된 SQL 쿼리를 스캔하여 각각 알맞은 토큰으로 변경한 후, 해당 토큰을 Parser에 전달한다.
  • Parser: 토큰 스트림을 읽고, 해당 토큰들을 기반으로 하여 Parse Tree를 생성한다(C코드를 생성한다. 이 방식은 YACC/BISON과 같은 맥락이다.).
  • Code Generator: Parse Tree 분석 후, SQL 작업을 수행할 수 있는 바이트코드를 생성한다.  
  • VDBE(Virtual DataBase Engine): Code Generator가 생성한 바이트코드를 해당 VM에서 실행한다.
  • BTree: 보통의 RDBMS의 경우, Index를 처리할 때 BTree를 사용한다. SQLite의 경우, 인덱스 뿐만 아니라, 데이터들 또한 BTree구조로 저장을 한다. SQLite에서 데이터를 조회하는 방식은 보통의 RDBMS에서 BTree를 통해 스캔하는 방식과 유사하다.
  • Pager: SQLite에서의 데이터 처리의 최소 단위를 칭하는 것 같다(Oracle의 데이터 블록과 유사하다 생각한다.) 아래와 같이 Page안에 Data BTree, Index BTree가 존재하는 방식인 것 같다. 트리에 있는 값은 진짜 값이 있는 것 같다(오라클 index BTree의 경우, 데이터 블록이 있고 그 블록안에 여러 값이 존재함). 해당 페이지 단위로 Locking과 Transaction 작업을 진행한다(commit, rollback) 

 

  • VFS(Virtual File System): 해당 Page의 실제 값을 조회할 때, Diskdatafile를 조회할 때 VFS를 사용한다. 해당 VM을 사용하기 때문에 플랫폼에 상관없이 똑 같은 API의 인터페이스를 통해 파일을 조회할 수 있다.

 

SQLite Lock 개념

현재 설명하고자 하는 Lock 메커니즘은 SQLite3 버전의 메커니즘이다. Lock은 데이터의 무결성을 유지하기 위해 존재하는 개념이다. 1개의 프로세스가 DB에 접근할 때 Lock을 얻는다.

 

Lock의 종류는 아래와 같다.

 

  • UNLOCKED: 파일을 열기만한 상태, DBLock이 잡혀있지 않는 상태이다. 해당 상태에서는 읽기/쓰기가 불가능하다. 아무 프로세스가 접근하여, Locking을 하여, 데이터를 쓰거나 읽을 수 있다. 해당 상태는 기본 상태이다.
  • SHARED: 파일을 읽고 있는 상태, 동시에 여러 프로세스가 Shared Lock을 설정하여, 동시에 DB 파일을 읽을 수 있다. 그러나, Shared Lock1개라도 잡혀있으면 DB 파일은 쓸수 없다.
  • RESERVED: 파일을 쓸 예정인 상태, 프로세스가 Shared Lock을 잡았으며, DB 파일을 쓸 예정인 상태이다. Reserved Lock1개의 DB1개의 프로세스만 얻을 수 있다. Reserved Lock이 잡혔다고, Shared Lock을 못 잡는 것은 아니다. , Reserved Lock이 잡혀도 DB 파일 읽기는 가능하다.
  • PENDING: Exclusive Lock을 잡기 전의 상태, 즉 데이터를 쓰기 전의 상태이다. 해당 LockDB 파일에 데이터를 쓰기 위해 기다리는 상태이다. 해당 Lock1개의 DB1개의 프로세스만 얻을 수 있으며, Shared Lock이 모두 풀릴 때까지 기다린다(다른 프로세스에서 읽기 완료될 때까지 대기). 해당 상태에 돌입하면, 누구도 해당 DB 파일에 접근하지 못한다. 이 때 접근하면, Database is locked라는 상태메시지가 출력된다. 만약 Shared Lock이 모두 사라지면 바로 Exclusive Lock으로 전이를 한다.
  • EXCLUSIVE: 프로세스가 파일을 쓰는 상태, Exclusive Lock에 존재는 DB 파일에 데이터를 입력하기 위해서이다. 데이터의 입력은 Insert, Update, Delete가 해당한다. 동시성을 제고하기 위해서는 해당 상태가 최대한 짧아야 한다. 해당 상태 또한 누구도 DB 파일에 접근하지 못하며, 접근할 경우, Database is locked라는 상태메시지가 출력된다.

SQLite 트랜잭션 개념

SqliteRollback Journal이라는 방식을 통해 트랜잭션을 구현한다.

 

1개의 프로세스가 Exclusive Lock을 획득하여, 1개의 DB 파일에 데이터를 입력할 때 Journaling 기술을 사용한다. 변경되기 전의 데이터를 rollback journal에 저장한다. 저장 후, rollback journalDB 파일의 초기 크기 값도 저장되기 때문에, insert된 데이터도 rollback이 가능하다. , 트랜잭션이 시작할 때 바구니 하나를 만들고, 변경되기 전의 값들을 바구니에 담고, 변경된 값들은 데이터 파일에 기록한다. Rollback상황에 마주치게 되면, 바구니에 값들을 갖고 Rollback을 실현한다. 트랜잭션 작업 중 시스템 크러쉬가 발생될 경우, 해당 journal“hot”이 되며, DB 데이터 무결성 복구를 위해 바로 사용된다.

 

Journal Mode는 총 6가지가 있다.

 

  • DELETE: 기본 모드이며, 트랜잭션 종료시 파일을 삭제한다.
  • TRUNCATE: 트랜잭션 종료시 파일을 0 상태로 만든다. DELETE 보다 더 빠르다.
  • PERSIST: 매 트랜잭션 마다 파일을 덮어쓰기 한다. TRUNCATE보다 빠르다.
  • MEMORY: Journal 데이터를 메모리에 기록한다. PERSIST 보다 빠르지만, DB Corrupt에 빠질 수 있다.
  • WAL: Write-Ahead Logging 방식을 사용한다.
  • NONE: Journaling을 하지 않음, 트랜잭션을 제어하지 못한다.

Journal WAL mode는 기존 전통적인 Journaling과 반대되는 방식이다. 일반 RDBMS에서 사용하는 WAL 방식을 차용하였다. OracleDBMSWAL 방식 설명은 아래를 참고한다. 해당 설명을 참고하고 읽으면 이해에 훨씬 도움이 될 것이다. 해당 WAL 방식을 통해 Read/Write를 동시에 할 수 있게 하여, 동시성을 증대시킨다.

 

SQLite에 일반 Journaling을 사용하면 파일에 대해 write/reading 상태 밖에 존재하지 않은데, WAL 방식을 사용하면, write/reading/checkpoing 상태가 존재한다. 또한 DB 파일 경로에 journal 임시 파일은 생기지 않고, wal 파일과 shm 파일 생성된다.

 

wal 파일에는 변경된, 변경하고 있는 데이터들을 저장한다. Commit을 할 경우에는 바로 db 파일에 적용하는 것이 아니라, wal 파일에 commit 구분자를 추가한다. 해당 wal 파일이 일정 크기만큼 커질 경우(default page size=1000) checkpoint가 발동되면서 wal 파일을 data file에 집어넣는 작업이 발생한다. shm 파일은 wal 파일에 접근할 때 공유 메모리 역할 즉, 캐쉬 역할을 한다.

 

이 때 궁금한 사항은 결국 WAL에 접근해야하면 기존 journal 방식과 똑같이 WAL 파일에 Lock이 걸리지 않냐 그럴 수 있다. 그러나, 다음과 같이 해결한다. 먼저, WAL의 유효한 커밋의 위치 파악을 한다. 해당 위치는 end_mark라 칭한다. WAL 모드에서는 CRUD 작업을 할 때 해당 end_mark를 통해 WAL 파일 복사 여부를 판단한다. 만약, 조회하고자 하는 데이터가 데이터 파일에만 있으면 해당 데이터 파일만 조회하고, 조회하고자 하는 데이터가 데이터 파일 + WAL 파일에도 존재하면 end_mark를 통해 필요 위치까지만 WAL 파일을 복사하여 조회한다. 이렇게 복사해서 사용하기 때문에 1Writer, N reader의 공존, writer/reader 가 같이 공존할 수 있는 동시성을 제공할 수 있다. 그러나, WAL 파일을 DB 파일에 저장하는 작업이 발생할 때는 Exclusive Lock이 발생한다. 해당 작업을 checkpointing라 한다.

부록) Oracle WAL 처리 방식

위의 사진은 오라클 공홈 DB 구조 사진을 발췌한 것이다. OracleWAL 작업 아래와 같이 간단하게 설명한다.

 

  1. 클라이언트 insert 쿼리 요청
  2. 해당 insert문과 관련된 데이터의 undo 세그먼트 생성(rollback을 위한)
  3. 해당 insert문과 관련된 데이터의 redo log 생성(트랜잭션 작업 복구를 위한) undo 세그먼트 적재
  4. 해당 redo logredo log buffer에 저장
  5. 변경된 데이터를 buffer cache에 저장(해당 블록은 더티 블록으로 전이)
  6. 클라이언트에서 Commit 요청을 할 경우, LGWR 프로세스는 rede log filelog 파일을 저장한다. 만약 rollback을 하는 경우, redo log에 보관된 undo 세그먼트를 통해 rollback을 한다.
  7. 변경된 buffer cache의 더티 블록은 CKPT 프로세스에 의해 때가 되면 데이터 파일과 동기화 작업 실행 명령을 내린다. 데이터 파일 동기화 작업은 DBWR이 진행하며, 더티 블록 데이터를 데이터 파일에 등록한다.
  8. 위의 과정에서 데이터 저장 보다 로그 파일을 먼저 적재하는 것을 WAL 원칙 및 작업이라 한다. 이렇게 할 경우, DB corrupt으로부터 데이터를 무결하게 유지할 수 있다.

포팅할 때 겪었던 명령어들

  1. 리눅스에서 SQLite 사용할 때 
    •  보기 편하게 설정(sqlite db 파일과 같은 경로에 .sqliterc 파일 생성)
    • vim .sqliterc( 아래 추가, PRAGMA 설정도 될 것 같음 확인은 안해봄 ㅋㅋ ) 
      • .header on
      • .mode colum
  2. SQLite 터미널 안에서
    • .open $db_name : 데이터베이스 생성(파일이 경우) 그리고 open
    • .exit, quit : 데이터베이스 종료
    • .tables: 테이블 목록 조회
    • .schema $tbl_name : DDL 조회
    • PRAGMA foreign_keys =ON: 참조 무결설 설정 ON(SQLite 하위 호환 때문에 디폴트 설정은 OFF이다.)
  3. 포팅할 때
    • Right/Full Outer Join을 지원하지 않는다. 그러므로 Right Outer Join의 경우, Left Outer Join으로 변경한다. Full Outer Join 쿼리를 잘 변경하자
    • now() 함수
      • datetime( 'now', 'localtime'): yyyy-MM-dd HH:mm:ss 포멧으로 저장
      • strftime('%s', datetime('now', 'localtime'): 유닉스 타임스탬프로 저장
      • strftime('%Y-%m-%d', datetime('now', 'localtime')): yyyy-MM-dd 포멧으로 저장
      • 주의사항: 맨위의 처럼 DB insert를 할 경우, 데이터 조회할 때 dateformat 오류가 생길 수 있다. 해당 오류는 다음과 같이 해결할 수 있다. 1)유닉스 타임스탬프로 저장해서 해결 2) PRAGMA date_string_format=yyyy-MM-dd HH:mm:ss  설정
    • 타입
      • 문자형 => varchar(n)
      • 숫자형 => integer
      • text => text
      • date => datetime
      • blob/bytea/varbinary => blob
    • auto_increment 적용 => auto_increment (DDL에 default를 붙이자)
    • date add 1 day
      • strftime('%s', datetime('now', 'localtime', '+1 day') );
    • MyBatis를 사용할 경우, sqlite JDBC ResultSet.getBlob() impelements가 되지 않았음 때문에, BlobTypeHandler를 확장하거나, TypeHandler impelements를 해서 ResultSet.getBytes로 변경해서 한다. 퍼시스턴스 프레임워크(SQL Mapper 또는 ORM)를 쓰지 않는경우 ResultSet에서 getBlob이 아닌 getBytes를 사용한다. 타입 핸들러 코드는 아래와 같다.
public class SqliteBlobTypeHandler extends BlobTypeHandler {

	@Override
	public byte[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
		return cs.getBytes( columnIndex );
	}
	
	@Override
	public byte[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
		return rs.getBytes( columnName );
	}
	
	@Override
	public byte[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
		return rs.getBytes( columnIndex );
	}
	
}	

 

  • primary key를 제외한 필드에 대해서 auto_incremenet가 적용되지 않는다. 다음과 같이 처리 ((SELECT IFNULL(MAX(id), 0) + 1 FROM Log)
  • JDBC URL 관련

SQLite 용례(실사용기)

1) SQlite 멀티 쓰레드 사용

  • 맨처음은 파일 DB를 잘 몰라, Database Lock이 걸릴까봐, SqlSession 객체 인스턴스를 한개만 만들었다. 
  • 그래서, 멀티 쓰레드라도, 1개의 객체 인스턴스를 쓰면 괜찮은 줄 알았다. 
  • SqlSession 여러개 만드나, 1개 만드나 큰차이가 없는 것 같았다. 

 

2) SQlite 멀티 프로세서 사용(2개의 데몬에서 1개의 DB 파일 사용) - 첫 번째 문제

  • 2개의 서버에서 1개의 DB 파일을 사용할 때 슬슬 문제가 올라왔다.
  • Database is locked가 떴다...

  • 처음에는 SQLiteException을 통해 catch 슬롯에서 다시 자기 자신을 호출하는 재귀 함수를 통해 개선했음, 근데 문제는 어플리케이션 코드의 수정과 재귀 함수에 스택오버 플로우의 문제가 발생함, 분명, Connection Timeout 같은것이 있을텐데 찾고 계속 찾다 보니 있었다.
  • busy_timeout=60000
  • 데이터소스 설정할 떄 해당 설정을 하면 다중 프로세스라도 기다렸다가 진행한다. 

 

3) SQlite 멀티 프로세서 사용(2개의 데몬에서 1개의 DB 파일 사용) - 두 번째 문제

  • busy_timeout 설정을 통해 개선 그러나 특정 API 처리할 때 똑같은 현상 발발
  • 해당 API 처리는 2개의 데몬이 동시에 처리하는 작업임.

  • 문제는 아래와 같다.

  • B 프로세스가 노란색 지점에서 BEGIN TRANSACTION을 시도하는 순간 A 프로세스의 EXCLUSIVE LOCK에 의해 LOCK 선점을 하지 못해, Database is locked가 생겼다. 

 

4-1) SQlite 멀티 프로세서 사용(2개의 데몬에서 2개의 DB 파일 사용) - 문제 파악

 

  • 결국 DB 파일을 분리했다ㅡㅡ.
  • 그러나, A 프로세스는 B 프로세스의 DB 파일 참조가 필요하다.
  • 즉, A 프로세스는 1) A DB파일 2) B DB 파일을 사용해야한다.
  • 어플리케이션 코드 수정은 불가피하다. 
  • 그러나, 한번의 어플리케이션 수정과 설정 값 제어를 통해 충분히 유연하게 대처가능하다.

 

4-2) SQlite 멀티 프로세서 사용(2개의 데몬에서 2개의 DB 파일 사용) - Spring 프로젝트 적용

  • 일단 간단하다. 
  • Spring에 2개의 데이터 소스 빈 생성 및 등록을 한다.
  • 그리고 DAO 객체는 해당 빈을 선택적으로 사용한다.
  • 쉽게 얘기해서, 선택적인 빈 주입과 선택적인 빈의 사용으로 어플리케이션 코드의 수정을 최소화 하고 유연하게 대응할 수 있다. 

참고문헌

https://gywn.net/2013/08/let-me-intorduce-sqlite/

https://lhcsoft.blogspot.com/2019/04/sqlite-multi-thread.html

https://thinking-jmini.tistory.com/23

https://www.sqlite.org/docs.html

https://dzone.com/articles/how-sqlite-database-works

https://medium.com/technology-in-essence/how-sqlite-database-works-b10ac80e4f07

http://forensicinsight.org/wp-content/uploads/2013/07/INSIGHT-SQLite-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B5%AC%EC%A1%B0.pdf

https://stackoverflow.com/questions/22673833/sqlite-wal-mode-checkpoint-on-background-thread-wal-journal-never-shrinks

Posted by 동팡

목차

AOP 개념

AOP 종류

AOP 용어

AOP Weaving

Spring AOP vs AspectJ

Do it AspectJ

버전 이슈사항

참고문헌

AOP 개념

AOP는 Aspect Oriented Programing의 약자로, 관점 지향 프로그래밍을 뜻한다. 공통 관심사(Aspect)의 분리(모듈화)를 통해 소스코드의 중복의 해소 및 어플리케이션의 책임 원칙을 좀덕 획일화한다. 하나의 예는 트랜잭션 잡업과 인증 절차를 예를들 수 있다. 즉, 기능 중에 주 기능에만 집중하고, 부가 기능은 Aspect로 다 분리 시킨다. 

 

AOP 전체 설명도

용어는 아래 용어 항목에서 참고한다.

Cross-Cutting Concern은 위 개념에서 얘기한 부가 기능을 뜻하며, Primary Concern은 주기능을 뜻한다. Existing App과 같이 구성된 것을 NewApp으로 변화 시키고, 개발자는 주기능과 부가기능을 모듈화 개발할 수 있는 행위를 AOP이다. 

대략적인 설명은 다음과 같다.

Cross-Cutting Concern, 즉 Aspect들을 Point-Cut을 통해 대상을 적용할 대상(Target)을 지정한 후, Weaving을 시켜 New App을 생성한다(이 문구는 아래의 글을 다 읽은 후, 다시 읽어보자).

AOP 종류

일단 Java 기준, AOP는 대표적으로 SpringAOP와 AspectJ 가 대표적이다. 

Spring AOP

IoC/DI/프록시 패턴/자동 프록시 생성 기법/빈 오브젝트의 후처리 조작기법 등을 통해 AOP를 지원하며, 이와 같은 AOP는 스프링과 기본 JDK를 통해 구현 가능하다. 즉 간접적인 방식을 통해 AOP를 지원한다. 해당 Spring AOP는 프록시 방식의 AOP이다. 즉 프록시 디자인 패턴을 사용한다. 

스프링 AOP의 프록시 빈 생성 절차는 다음과 같다. 

 1. 빈 설정파일을 통한 빈 오브젝트 생성

 2. (생성된 빈을 이용하여) 빈 후처리기를 통해 컨테이너 초기화 시점에서 자동으로 프록시 생성(DefaultAdvisorAutoProxyCreator.class/BeanPostProcessor.class), 

 3. 포인트컷을 활용하여, 프록시를 적용할 대상을 지정한다.

 4. 프록시 빈 오브젝트 생성 및 IoC 컨테이너에 등록

AspectJ

바이트코드 생성과 조작을 통해 AOP를 실현한다. Target 오브젝트를 뜯어 고쳐서 부가기능을 직접 넣어주는 직접적인 방식이다. 

AOP 용어

 

Target: 부가기능을 부여할 대상을 뜻한다. 즉, 주기능을 얘기한다.

Aspect: AOP의 기본 모듈이다. 한개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어진다.

Advice: 실질적으로 부가 기능의 구현체이다. Advice의 유형은 다음과 같다. Before, After, Around, AfterReturning, AfterThrowing이 있다. 

 - Before: Target을 실행하기전에 부가 기능 실행

 - After: Target실행 후 (해당 Target Exception 또는 정상리턴 여부 상관없이) 실행

 - Around: Before + AfterReturning

 - AfterReturning: Target 실행 후 성공적인 리턴할 때

 - AfterThrowing: Target 실행하다, Exception 던질 때

Join-Point: Advice가 적용될 위치를 표시한다(ex:메소드 실행 단계).

Point-Cut: Advice를 적용할 Target를 선별하는 역할을 한다. Annotation 또는 메소드의 정규식을 통해 표현한다.

Cross-Cutting Concertn: 횡단 공통 관심사이다. 이것은 이미지를 통해 연상하는 것이 더 빠르다. 

 

Cross-Cutting Concerns 설명

AOP Weaving

Aspect(부가기능)와 Application(핵심기능)의 Linking을 하는 과정이다. 해당 객체들을 묶어 새로운 객체를 생성한다.

Spring AOP

이미, 위에서 Weaving 절차를 설명하였다. Spring AOP의 Weaving 절차는 RunTime이다(프록시 빈 생성하는 것, IoC 컨테이너 초기화 작업할 때, 그러니까 WAS 가동할 때). 

인터페이스 기준으로 하는 JDK Dynamic Proxy와 Class 기준으로 하는 CGLib Proxy가 존재한다. CGLib Proxy의 경우, AspectJ의 Weaving 처럼 바이트 코드를 조작한다. SpringAOP는 JDK Dynamic Proxy 패턴을 선호한다. 또한 Proxy 패턴 자체가 인터페이스를 끼고하는 페턴이다. 

AspectJ

실제, AJC(Apsect Compiler)를 이용하여 Woven System 생성한다. 즉 부가기능과 핵심기능이 합쳐진 클래스 파일을 생성한다. AspectJ의 Weaving 타입은 아래와 같다.

Compile-Time Weaving: Aspect의 클래스와 Aspect를 사용하는 class들을 AJC를 통해 컴파일을 한다. JAR를 이용하여 Weaving을 하는 경우, Post-Compile Weaving(Binary Weaving)을 사용하며, 일반 소스 코드의 경우, 일반 Compile-Time Weaving을 사용한다.

Load-Time Weaving: 클래스로더를 통해 클래스가 JVM에 로딩되는 시점에 클래스의 바이트 코드를 조작한다. 즉, 객체를 메모리에 적재할 때 Weaving을 실현한다. 때문에, 다른 Weaving보다 속도 측면에서는 느리다.

AspectJ Weaving

Spring AOP vs AspectJ

일단 AspectJ 승리

1. 속도: 일단 AspectJ가 8~35배 빠르다. SpringAOP는 Aspect당 기타 AOP 관련 메소드를 추가적으로 호출하기 때문에 바이트코드를 직접적으로 건드린 AspectJ에 비하면 느릴 수 밖에 없다.

2. AspectJ는 강력한 기능을 제공한다. 

SpringAOP vs AspectJ for JointPoint

3. 기능 차이점

SpringAOP vs AspectJ

1. Spring AOP는 퓨어 자바를 통해 구현 가능

2. AsepctJ는 Load-Time Weaving을 하지 않는한, AJC를 통한 컴파일 절차가 필요하다. 

3. Spring AOP는 method level weaving만 가능(근데 보편적으로 method level weaving만 사용....)

4. Spring AOP는 Spring 프레임워크에서 한정적이지만 AspectJ는 모든 JVM에서 가능하다.

5. Spring AOP는 execution 포인트컷만(위에서 설명한 메소드 정규식) 사용가능

6. AspectJ가 빠름

7. Spring AOP 복잡도 승리(프록시 패턴을 활용하여 복잡도가 AspectJ보다 훨씬 낮다.)

Do it AspectJ

AspectJ를 실제 구현해보자

pom.xml

	<dependency>
		<groupId>org.aspectj</groupId>
		<artifactId>aspectjrt</artifactId>
		<version>${org.aspectj-version}</version>
	</dependency>
	<dependency>
   		<groupId>org.aspectj</groupId>
   		<artifactId>aspectjweaver</artifactId>
   		<version>${org.aspectj-version}</version>
	</dependency>

NoNameAop.class 

설명

 - order를 통해 Transacional 보다 늦게 실행하게 된다, 해당 AOP가 실패하면 Transactional에서 rollback처리한다.

 - AsyncUriAnnotation Annotaion만 해당 Aspect를 구동한다.

 - args를 통해 Target의(메소드의) 인자 값을 파싱한다.

 - joinPoint.proceed()를 통해 그냥 Target을 실행할 수 있지만,

 - 부가기능 로직에서 파라메터를 수정했기 때문에 아래와 같이 new Object[]를 하여 전송한다.

 - 리턴 값이 없을경우 메소드를 void로 만들 수 있다. 또는 리턴 값이 있으면 그냥 리턴을 해주면 된다.

 - 그러나, 난 리턴값도 수정할 것이다.ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 변경된 ret 값을 리턴한다. 

@Aspect
@Component
@Order(300)//tx:order:200
public class NoNameAop {
	
	private final AuthAsyncService authAsyncService;
	private final AuthCombiner authCombiner;
	private final AuthService authService;
	private static final Logger logger = Logger.getLogger( NoNameAop.class );
	
	@Autowired
	public UriAuthSupportAop( AuthAsyncService authAsyncService, AuthCombiner authCombiner, AuthService authService ) {
		this.authAsyncService = authAsyncService;
		this.authCombiner = authCombiner;
		this.authService = authService;
	}
	
	@Pointcut("@annotation(net.glaso.jwt.business.common.annotation.AsyncUriAnnotation) && " +
			"args(identifier, request)")
	private void asyncAuthPointCut( String identifier, HttpServletRequest request ){}

	@Around("asyncAuthPointCut(identifier, request)")
	public Object procAsyncToAuthUri( ProceedingJoinPoint joinPoint, String identifier, HttpServletRequest request ) throws Throwable {
		
        // .. Aspect 부가기능 로직
        
		// MAIN PROCESS
		Object ret = joinPoint.proceed( new Object[] { authCombiner.getUidIfIsNotUid( request, identifier ), request } );
		// MAIN PROCESS
		
		// .. Aspect 부가기능 로직
	
		return ret;
	}
}

PointCut Sample

1. net.glaso.jwt.business 하위 패키지 중 service 패키지의 post로 시작하는 메소드를 지정(전달인자는 아무거나 상관없고, 메소드의 리턴타입도 아무거나 상관없다.)

2. UserService클래스의 generate로 시작하는 메소드 지정

@Pointcut("execution(* net.glaso.jwt.business..service..post*(..))")
@Pointcut("execution(* net.glaso.jwt.business.user.service.UserService.generate*(..))")

버전 이슈사항

성공
Java 6 / Spring 4.1.7.RELEASE / AspectJ-1.6.10 / Tomcat 6.0.48 (테스트 완료)
Java 7 / Spring 4.1.7.RELEASE / AspectJ-1.6.10 / Tomcat 6.0.48 (테스트 완료)
Java 7 / Spring 4.1.7.RELEASE / AspectJ-1.7.4 / Tomcat 6.0.48 (테스트 완료)
Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.7.4 / Tomcat 6.0.48 (테스트 완료)
Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.8.9 / Tomcat 6.0.48 (테스트 완료)

 

실패
Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.6.6 / Tomcat 6.0.48
- Exception Message: java.lang.CharSequence': Invalid byte tag in constant pool: 15
- 관련 자료
https://www.eclipse.org/aspectj/doc/released/README-180.html
https://stackoverflow.com/questions/23801950/spring-4-and-java-8-invalid-byte-tag-exception
- 원인: Java8을 지원하기 위해서는 AspectJ 1.8 이상을 사용해야한다(근데 위의 1.7.4 버전은 테스트 성공...).

Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.8.10 / Tomcat 6.0.48
- Exception Message: java.lang.IllegalStateException: Expected raw type form of org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$Match
- 관련 자료
https://jira.spring.io/si/jira.issueviews:issue-html/SPR-15019/SPR-15019.html
https://github.com/spring-projects/spring-framework/issues/19586
- 원인: AspectJ-1.8.9에서 1.8.10으로 업데이트를 진행 하면서, IllegalStateException을 회귀 하면서 생긴 Spring의 버그 해당 사항은 Spring 4.2.9, Spring 4.3.5, Spring 5.0M4에서 패치되었다. 1.8.9를 사용하거나 Spring을 패치한다.

참고문헌

https://docs.spring.io/spring/docs/2.5.x/reference/aop.html 
https://www.baeldung.com/spring-aop-pointcut-tutorial 
https://www.baeldung.com/aspectj 
https://www.baeldung.com/spring-aop 
https://www.baeldung.com/spring-aop-vs-aspectj 
https://howtodoinjava.com/spring-aop-tutorial/ 
https://web.archive.org/web/20150520175004/https://docs.codehaus.org/display/AW/AOP+Benchmark 
https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html

 

Posted by 동팡