가입 초대 링크만 필요하신 분은 initiativeq.com/invite/EdvNeMXW9

페이팔에서 만든 코인이라는 정보가 퍼지고 있는데, 정작 이니셔티브 Q 홈페이지에 들어가서 소개글만 봐도 아닌 걸 확인할 수 있다.

페이팔에 인수된 Fraud Sciences를 만든 Saar Wilf가 만들었다고 소개되어 있는데, 사이트의 느낌은 이 전에 ICO 붐(?)이 일었을 때 그럴싸한 모델 사진과 이력을 적어두는 스캠이 많았었는데, 스캠인지 아닌지 까봐야 알겠지만 느낌은 비슷하다.

신뢰 네트워크를 구성하기 위해 단순 가입이 아닌 네트워크(다단계..) 회원 구조로 되어 있고, 초대 링크를 통해 가입할 때 마다 이후에 에어드랍을 통해 받을 수 있는 Q를 보상으로 받을 수 있다. 가입 시기에 따라 보상으로 받는 Q는 다를 수 있는데, 글을 쓰는 시점을 기준으로 1명당 909Q를 받을 수 있다.

초대를 통해서만 가입할 수 있도록 해둔 이유는 안전한 결제 네트워크를 위해 계정이 실제 사람과 연결되어야 하기 때문이라고 한다. 초대를 통해 신규 회원이 가입하고, 다시 초대한 사용자는 Review 및 Accept를 통해 보안을 향상시키는데 기여할 수 있다. (고 하는데 이른바 보증 구조인걸까)

로드맵을 기준으로 2018년 중반에 시작되었고, 2021년 올 해 후반 결제 네트워크로써 릴리즈 하는 것을 목표로 하고 있다. 상장하면 1Q = 1USD라고는 하지만, 그들의 계획으로는 연간 거래액이 5조 달러에 도달해야 될 수 있는 수치인 것 같고, 정말로 목표를 달성할 수 있다면야 좋겠지만 초반에는 미미하지 않을까 싶다. 가입만 하면 되는 거니 밑져야 본전으로 생각하고 해보기로 했다.

가입을 위한 절차는 아래와 같다.

1. 초대 링크를 통해 페이지에 접속하면 보이는 Reserve your spot 버튼을 클릭한다.

2. 이메일 또는 Open ID(Google, Facebook, Twitter 계정)으로 가입

3. 초대 링크 배포자가 Review & Accept(가입 수락 과정)하면 완료

4. 사용하는 스마트폰 OS에 맞는 스토어에서 Intiative Q를 검색하여 설치하고 로그인하면 이 후 에어드랍을 받을 수 있다.

 

다른 읽어볼 만한 글

- medium.com/@omrockstars/the-future-the-blueprint-or-a-scam-what-is-initiative-q-11374235c7e2

HTML 태그 사전 - 하주석, 홍익미디어씨엔씨(CNC)

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=262128

 

중학생 1, 2학년 무렵이었나, 지금은 구글, 네이버, 다음으로 어느 정도 검색 사이트가 자리 잡은 듯하지만 야후, 라이코스, 국내에도 네이버, 미스다찾니, 코리아닷컴, 심마니 등 여러 검색 사이트/포털들이 있었던 것 같다.

그 때 쯤의 라이코스에서는 개인 계정별로 BBS라는 게시판 서비스를 제공하고 있었다. 싸이월드가 나오기 전이었는데 그 게시판을 마치 싸이월드처럼 썼다.

게시판은 HTML, CSS, 자바스크립트를 편집할 수 있는 기능을 제공했는데, 게시판을 좀 바꿔보고 싶어서 시내 서점에서 HTML 태그 사전을 사서 읽으며 내 BBS에 적용하곤 했다. 책 구성은 마치 요즘의 API Documentation처럼 태그 하나하나마다 설명을 해주는 식이었다.

이 책과 함께 윈도우 도움말 파일로 누군가가 만들었던 HTML 사전들도 있었고 그런 도움말 파일도 많이 봤던 것 같다.

 

요즘의 SNS처럼 친구를 맺는다거나 팔로우를 하는 기능은 당연히 없었는데, 그 때는 어떻게 다른 사람들의 BBS를 찾아다니고 글을 남겼는지 모르겠다. 다른 사람들의 BBS에 들어가서 디자인이 예쁘거나 신기한 자바스크립트 기능이 있으면 소스코드를 보고 내 BBS에도 적용하곤 했는데 뭔가가 마우스 커서를 따라다니는 걸 만드는 게 유행이었던 적이 있었다. 아날로그 시계도 마우스를 따라다니고, 반짝 반짝 가루나 별 같은 게 커서 뒤를 줄지어 따라다니는 그런 식이다.

자바스크립트는 그 때 처음 접했는데 프로그래밍 개념이 없던 그 때에는 "변수"같은 용어조차도 무슨 의미인지 어떻게 써야할지 몰라서 그저 자바스크립트스쿨넷인가 하는 사이트에서 코드를 그대로 베껴와서 BBS에 적용했다.

 

문득 생각이 나서 알라딘에서 검색했는데 처음으로 웹을 접하고 처음으로 본 IT 서적이라 내게는 추억이 깃든 책이다.

Node.js Slack SDK를 이용해 슬랙 API를 연동하고, Heroku에 배포해보는 포스트입니다.

 

Node.js Slack SDK는 Slack의 API를 Node.js에서 간편하게 연동할 수 있도록 만들어진 모듈입니다. Web API, Events API, Interactive Message, RTM API, Incoming Webhooks를 제공합니다.

 


1. Slack 앱 만들기 및 권한 설정

https://api.slack.com/apps?new_app=1에 접속하여 슬랙 앱을 생성해 줍니다.

<슬랙 앱 생성>

 

App Name에 슬랙 앱의 이름을 입력하고 Development Slack Workspace는 개발 시 연동할 워크스페이스를 선택해 준 후 [Create App] 버튼을 눌러 앱을 생성해 줍니다.

 

앱을 생성해 준 후 왼쪽의 OAuth & Permissions 페이지로 이동합니다.

<앱 생성 후 OAuth & Permissions 페이지로 이동하기>

 

OAuth & Permissions 페이지에서 스크롤을 아래로 내리면 Scopes 섹션이 있고, 이 중 Bot Token Scopes에서 [Add an OAuth Scope]를 클릭해서 chat:write와 channels:history 스코프를 추가해 줍니다.

<Bot Token Scopes에 스코프 추가>

chat:write는 Web API로 메시지를 보내기 위한 스코프이고, channels:history는 Events API로 채널의 메시지를 이벤트로 받기 위한 스코프입니다. 이 두 스코프를 추가해 준 후 스크롤을 위로 올려 [Install App to Workspace]  버튼을 클릭하면 아래와 같이 앱을 설치할 때 필요한 권한을 확인하는 페이지가 나타납니다. [Allow]를 클릭해서 앱을 워크스페이스에 설치해 줍니다.

<앱 설치 시 권한 확인>

 

[Allow]  버튼을 클릭하면 OAuth & Permissions 설정 페이지로 돌아오는데, Bot User OAuth Access Token이 생성되어 있는 것을 확인할 수 있습니다. 이 토큰은 봇을 이용해 슬랙 API를 사용할 때 사용하게 될 토큰입니다.

 

 


2. Heroku 가입, CLI 설치, Node.js 환경 세팅

Heroku는 PaaS(Platform as a Service) 중 하나로써 애플리케이션 빌드, 배포, 운영 환경을 제공해줍니다. 뒤에서 Node.js로 개발한 슬랙봇 애플리케이션을 Heroku에 배포하여 Slack API 서버와 통신하도록 합니다.

 

https://www.heroku.com에 접속해서 회원 가입을 해준 후 https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up 을 참고해서 사용하는 운영체제에 맞게 Heroku CLI를 설치해주고 heroku login 명령을 실행해서 가입한 계정으로 로그인 해 줍니다.

(heroku는 git push 명령을 통해 서버에 코드를 배포하므로 git도 함께 설치되어 있어야 합니다.)

 

설치가 완료되면 heroku에서 제공하는 node.js 템플릿을 클론해 옵니다.

git clone https://github.com/heroku/node-js-getting-started.git hello-slack
cd hello-slack

 

3. Node.js Slack SDK 모듈 추가

클론해 온 폴더에는 기본적으로 Heroku에서 실행할 수 있도록 Node.js 기본 코드 및 파일들이 세팅되어 있습니다.

├── Procfile
├── README.md
├── app.json
├── index.js
├── package.json
├── public
│   ├── lang-logo.png
│   ├── node.svg
│   └── stylesheets
│       └── main.css
├── test.js
└── views
    ├── pages
    │   ├── db.ejs
    │   └── index.ejs
    └── partials
        ├── header.ejs
        └── nav.ejs

여기에 Slack API 연동을 위해 web-api 모듈을 설치해 줍니다.

# package.json에 정의된 dependency 설치
npm i

npm i @slack/web-api

 

index.js 파일을 열어보면 express 모듈을 이용해 작성된 서버 코드가 있습니다. 선호하는 웹 프레임워크를 사용해도 되고, 여기에서는 템플릿의 코드를 그대로 활용하고 라우팅만 추가해 주도록 하겠습니다. index.js 파일을 아래와 같이 수정해 줍니다.

const express = require('express')
const path = require('path')
const PORT = process.env.PORT || 5000

const { WebClient } = require('@slack/web-api');

// OAuth & Permissions 설정 페이지에서 생성된 Bot User OAuth Access Token
const token = 'xoxb으로 시작하는 BOT_USER_OAUTH_ACCESS_TOKEN 값으로 이 부분을 바꿔주세요.';
const web = new WebClient(token);

express()
  .use(express.static(path.join(__dirname, 'public')))
  .use(express.json())
  .set('views', path.join(__dirname, 'views'))
  .set('view engine', 'ejs')
  .get('/', (req, res) => res.render('pages/index'))
  .post('/slack/events', (req, res) => {
    let body = req.body;
    let event = body.event;

    if(body.type === 'event_callback') {
      if(event.type === 'message') {
        // 메시지 이벤트인 경우, 메시지가 '안녕'이면 '안녕하세요' 메시지 전송
        if(event.text === '안녕') {
          console.log(`인사 메시지 수신 channel:${event.channel}, user:${event.user}`);
          web.chat.postMessage({
            channel: event.channel,
            text: '안녕하세요.'
          }).then(result => {
            console.log('Message sent: ' + result.ts)
          });

          res.sendStatus(200);
        }
      }
    } else if(body.type === 'url_verification') {
      // URL 검증을 위한 처리
      console.log('url verification')
      res.send(body.challenge);
    } else {
      res.sendStatus(200);
    }
  })
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))

 

코드 수정이 완료되면 아래의 명령을 실행하여 Heroku 앱을 생성하고 커밋, 푸시해줍니다.

heroku create # Heroku 앱 생성
# 배포된 앱에 접속할 수 있는 URL이 출력됩니다. 
# create 시 앱 이름을 지정해줄 수도 있습니다.
# Git Remote가 heroku라는 이름으로 추가됩니다.

git add .
git commit -m "슬랙 이벤트 연동 기능 추가"

git push heroku master # heroku origin으로 push하면 앱이 배포됩니다

 

Git push 명령을 통해 배포가 정상적으로 완료되면 아래와 같이 로그가 출력됩니다.

heroku create 명령을 실행했을 때도 출력 됐던 앱의 URL이 출력됩니다.

 


 

4. 슬랙 이벤트 구독 설정

브라우저의 슬랙 앱 페이지에서 OAuth & Permissions 아래의 Event Subscriptions 페이지로 이동합니다.

 

Enable Events 오른쪽의 스위치를 토글해서 On으로 바꿔준 후 Request URL 에는 heroku create 명령을 실행했을 때 출력된 URL을 붙여넣어주고 path는 express에서 설정해준 /slack/events 를 붙여줍니다.

제 경우에는 https://warm-sierra-40409.herokuapp.com/slack/events 를 설정해주었습니다.

 

올바른 URL을 입력했다면 Request URL 레이블 오른편에 Verified가 표시됩니다. 그리고 슬랙에서 발생하는 이벤트 중 사용자가 메시지를 포스트한 경우를 Node.js 앱으로 받기 위해 [Subscribe to events on behalf of users]에서 [Add Workspace Event]를 클릭해 준 후 message.channels를 찾아 등록해 줍니다.

<message.channels 이벤트 추가>

 

끝으로 페이지 하단의 [Save Changes]를 눌러 변경 사항을 저장해 줍니다. 앱의 permission scopes가 변경되었다고 앱을 재설치하라는 메시지가 나타납니다.

메시지의 reinstall your app 링크를 클릭해서 앱을 재설치 해줍니다.

 

 


 

5. 슬랙에서 확인

필요한 설정과 배포가 완료되어 슬랙과 슬랙봇 앱이 연동되어 있는 상태입니다. 이를 확인하기 위해 슬랙 앱이나 웹에서 연동한 워크스페이스로 접속한 후 채널에 앱을 추가하고 메시지를 보내보겠습니다.

<채널의 Add an app 클릭>

채널에 접속한 후 [Add an app]을 클릭해서 표시되는 앱 중 In your workspace에 표시되어 있는 실습으로 만든 앱을 추가해 줍니다.

채널에 <hello-slack> 앱 추가

 

해당 채널에서 '안녕' 메시지를 입력하면 봇이 '안녕하세요'라고 메시지를 보내는 것을 확인할 수 있습니다.

 


참고

- Slack Events API: https://api.slack.com/events-api 

- Slack Web API: https://api.slack.com/web

- Node.js Slack SDK: https://github.com/slackapi/node-slack-sdk

 

'밤을 지새다 > Javascript' 카테고리의 다른 글

node-schedule  (0) 2020.04.05

node-schedule은 Node.js에서 동작하는 스케줄링을 위한 모듈입니다. 원하는 시간에 작업을 수행할 수 있는 기능을 제공하고 유사한 모듈로는 node-cron이 있습니다. 자바스크립트의 setInterval 함수나 setTimeout 함수로도 동일한 기능을 구현할 수 있지만 그런 작업을 더 편하게 할 수 있도록 도와줍니다.

GitHub Repository: https://github.com/node-schedule/node-schedule

 

1. 모듈 추가

npm i node-schedule

 

2. Cron 스타일의 스케줄링
유닉스 계열의 Job 스케줄러인 cron의 표현식을 사용해서 스케줄링을 적용할 수 있습니다.

* * * * * *
│ │ │ │ │ │
│ │ │ │ │ └ 요일(0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── 월 (1 - 12)
│ │ │ └────────── 일 (1 - 31)
│ │ └─────────────── 시 (0 - 23)
│ └──────────────────── 분 (0 - 59)
└───────────────────────── 초 (0 - 59, OPTIONAL)
const schedule = require('node-schedule');

// 매일 10시 정각에 '스크럼 시간입니다.' 출력
let job = schedule.scheduleJob('0 10 * * *', () => {
  console.log('스크럼 시간입니다.')
});

 

3. 날짜 기반 스케줄링
Date 객체를 전달해서 스케줄링 할 수 있습니다. Date 객체에 설정된 시간에 1회 작업을 수행합니다.

const schedule = require('node-schedule');

// 2020년 1월 1일 0시 0분 0초 - 월은 0부터 시작
const date = new Date(2020, 0, 1, 0, 0, 0);
let job = schedule.scheduleJob(date, () => {
console.log('새해가 밝았습니다.');
});

 

4. Recurrence Rule 스케줄링(반복 작업)
cron 표현식으로도 동일하게 반복 스케줄링 작업을 할 수 있지만 Recurrence Rule로 명시적으로 표현할 수 있습니다.

const schedule = require('node-schedule');

let recurrenceRule = new schedule.RecurrenceRule();
// Range 객체로 범위 설정
recurrenceRule.second = new schedule.Range(0, 59);
// 숫자 리터럴로 지정된 값 설정
recurrenceRule.minute = 20;

// 매시 20분 매초에 메시지 출력
schedule.scheduleJob(recurrenceRule, () => {
    console.log('Hello!')
});

 

5. 객체 리터럴
Recurrence Rule 대신 객체 리터럴로 전달할 수도 있습니다. 객체 리터럴로 전달 시 프로퍼티로는 second, minute, hour, date, month, year, dayOfWeek, start, end, rule이 있습니다.

const schedule = require('node-schedule');

// 매 0초 마다 'hi' 출력
schedule.scheduleJob({
  second: 0
}, () => {
  console.log('hi')
});
const schedule = require('node-schedule');

// startTime, endTime, rule 조합
let start = new Date(Date.now() + 5000); // 5초 뒤
let end = new Date(startTiime.getTime() + 5000); // 10초 뒤

// start ~ end 동안 매초 마다 'hi' 출력
schedule.scheduleJob({
  start,
  end,
  rule: '*/1 * * * * *'
}, () => {
  console.log('hi');
});

'밤을 지새다 > Javascript' 카테고리의 다른 글

Node.js로 슬랙 API 연동  (0) 2020.04.05

프로젝트에서 Spring Cloud OpenFeign을 사용하고 있는데, 연동하고 있는 서비스에서 종종 오류가 나는 경우가 있어 급한 대로 예외 처리만 해두었으나, 장기적으로 안정적인 장애 예방을 위해 Resilience4j를 적용하려고 문서와 예제들을 찾아보고 있다.

Netflix Hystrix를 먼저 생각해두고 있었으나, 2018년 11월부로 개발이 중단되고, maintenance mode라고 하여, Resilience4j를 사용하기로 결정. (Google Trend 상으로는 아직 Hystrix를 더 많이 찾고 있는 듯)

Google Trends - Hystrix vs Resilience4j (파랑이 Hystrix)

 

Resilience4j는 공식 문서에서 "fault tolerance library for Java™"라고 소개하고 있고 Netflix Hystrix에서 영감을 받아 만들었으며, Java 8, 함수형 프로그래밍에 맞게 설계되었다고 한다.

Resilience4j의 코어 모듈인 CircuitBreaker, Bulkhead, RateLimiter, Retry, TimeLimiter, Cache와 라이브러리나 프레임워크와 연동할 수 있는 애드 온 모듈들이 있다.

 

당장은 서킷 브레이커만 사용하면 돼서 resilience4j-spring-boot2 디펜던시를 추가한 후 아래와 같이 작성하고 동작할 지 테스트를 해보았다.

@FeignClient(name = "api")
interface ApiClient {
    @CircuitBreaker(name = "api", fallbackMethod = "fallback")
    @GetMapping("/users")
    fun getUsers(): List<User>
    
    // Exception을 받을 파라미터가 필요.
    // getUsers와 동일한 시그니쳐로 할 경우 NoSuchMethodException 발생
    fun fallback(e: Exception): List<User> = emptyList()
}

 

우선 위 코드는 오류와 함께 애플리케이션이 실행되지 않는다. @FeignClient가 적용되어 프록시를 생성할 때 fallback 메서드에 매핑 정보가 없기 때문이다. 애너테이션이 없으면 그냥 무시할 줄 알았는데, Feign은 인터페이스에 정의된 메서드에 대해 모두 처리한다. Retrofit은 다른가 싶어 테스트 해봤으나 Retrofit도 이에 대해서는 동일하다. @Ignore 따위의 애너테이션이 있을까 싶어 찾아봤지만 없었고, GitHub Issue에 사용자들이 올린 글을 찾다보니 Feign, Retrofit 모두 default 메서드에 대한 처리에 어려움이 있어 아직은 지원하지 않는 기능이라고 한다.

fallback 메서드에 @GetMapping("/ignore")와 같이 붙여주면 애플리케이션은 실행되지만 getUsers()를 호출하여 오류가 발생했을 때 폴백 메서드를 호출하긴 하지만 프록시된 fallback을 호출하기 때문에 결과적으로는 GET /ignore를 요청하게 되어 원하는대로의 폴백으로 동작하진 않는다.

 

resilience4j-feign도 있고, resilience4-spring-boot2도 있지만 Spring Cloud Open Feign에 resilience를 위의 코드처럼 작성하는 건 괜히 더 복잡해져서 돌아가더라도 쓸 수 있는 형태로 적용해 보기로 했다. 아래의 두 가지 방법으로 시도했다.

1. Spring Cloud Open Feign으로 만들어진 빈의 래퍼 컴포넌트 작성(resilience4j-spring-boot2)

2. Spring Cloud Open Feign을 사용하지 않고 resilience4j-feign을 통해 Feign 빈 생성

 


환경

  • IntelliJ IDEA
  • Kotlin 1.3.71
  • Gradle 6.3
  • Gradle Dependencies(Spring Intializr로 프로젝트 생성)
    • org.springframework.cloud:spring-cloud-starter-openfeign
    • io.github.resilience4j:resilience4j-feign:1.1.0 (1.3.x 버전이 있으나 내부에서 버전 충돌)
    • io.github.resilience4j:resilience4j-spring-boot2:1.1.0
    • org.springframework.boot:spring-boot-starter-webflux
    • org.springframework.boot:spring-boot-starter-aop
    • org.springframework.boot:spring-boot-starter-actuator

 

 

공통 구현

- Feign으로 호출할 테스트 컨트롤러 작성 - 404를 응답하는 핸들러 메서드를 작성

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux

@RestController
class TestApiController {
    @GetMapping("/api/test")
    @ResponseStatus(HttpStatus.NOT_FOUND)
    fun test(): Flux<String> = Flux.just("NOT_FOUND")
}

- @EnableFeignClients 추가

 


 

1. Spring Cloud Open Feign Wrapper 컴포넌트로 Resilience4j 적용

Feign 클라이언트 인터페이스 작성 - TestApiOpenFeignClient

import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping

@FeignClient(name = "test", url = "http://localhost:8080/api")
interface TestApiOpenFeignClient {
    @GetMapping("/test")
    fun getTest(): String
}

 

Wrapper 작성 - TestApiOpenFeignClientWrapper

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import org.springframework.stereotype.Component

@Component
class TestApiOpenFeignClientWrapper(
        private val testApiOpenFeignClient: TestApiOpenFeignClient
) {
    @CircuitBreaker(name = "OPEN_FEIGN_API", fallbackMethod = "fallback")
    fun getTest(): String {
        return testApiOpenFeignClient.getTest();
    }

    /**
     * 서킷 브레이커에서 호출할 Fallback 메서드
     * @param e 발생한 예외
     */
    fun fallback(e: Exception): String {
        return "RESULT BY FALLBACK"
    }
}

 

테스트

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
internal class TestApiOpenFeignClientWrapperTest {
    @Autowired
    private lateinit var client: TestApiOpenFeignClientWrapper

    @Test
    fun getTest() {
        assertEquals("RESULT BY FALLBACK", client.getTest())
    }
}

 


 

2. Resilience4j-feign으로 Feign 인스턴스 생성

Feign 클라이언트 인터페이스 작성 - TestApiFeignClient

import feign.RequestLine

interface TestApiFeignClient {
    @RequestLine("GET /api/test")
    fun getTest(): String
}

 

Feign 빈 정의 - FeignConfiguration

import feign.FeignException
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
import io.github.resilience4j.feign.FeignDecorators
import io.github.resilience4j.feign.Resilience4jFeign
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FeignConfiguration {
    @Bean
    fun testApiClient(registry: CircuitBreakerRegistry): TestApiFeignClient {
        val circuitBreaker = registry.circuitBreaker("FEIGN_API")

        val decorators = FeignDecorators.builder()
                .withCircuitBreaker(circuitBreaker)
                .withFallback(object: TestApiFeignClient {
                    override fun getTest() = "RESULT BY FALLBACK"
                }, FeignException::class.java)
                .build()

        return Resilience4jFeign.builder(decorators)
                .target(TestApiFeignClient::class.java, "http://localhost:8080/api")
    }
}

 

테스트

import feign.RequestLine

interface TestApiFeignClient {
    @RequestLine("GET /api/test")
    fun getTest(): String
}

- hibernate 버전: 5.0.12-FINAL


스프링 배치 잡 작업 중 뜬금없이 하이버네이트의 EntityEntryContext 클래스의 reentrantSafeEntityEntries() 메서드에서 ArrayIndexOutOfBoundsException이 발생했다.


java.lang.ArrayIndexOutOfBoundsException: 6673

at org.hibernate.engine.internal.EntityEntryContext.reentrantSafeEntityEntries(EntityEntryContext.java:319)

at org.hibernate.engine.internal.StatefulPersistenceContext.reentrantSafeEntityEntries(StatefulPersistenceContext.java:1128)

at org.hibernate.engine.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:136)

...


검색하다가 hibernate 커뮤니티의 JIRA에서 동일한 이슈[각주:1]를 발견할 수 있었는데 해당 이슈 보고자도 멀티스레드 환경에서 엔티티를 저장하는 과정에서 가끔 발생했다고 한다.

커멘트의 마지막 부분에 관련 이슈[각주:2]와 함께 이슈를 클로즈한다고 하여 따라가보니 5.1 버전에 픽스되었다고 하여 일단은 버전업을 해보는 것으로 해결.

  1. https://hibernate.atlassian.net/browse/HHH-8880 [본문으로]
  2. https://hibernate.atlassian.net/browse/HHH-10795 [본문으로]

혼자서 어설픈 영어 실력으로 169페이지에 달하는 RFC 문서를 얼마나 번역할 수 있을지는 의문이지만(...) iCalendar의 개념과 프로젝트에서 사용할 Event 타입에 대해 파악하고자 한글로 정리해봅니다.



1. 소개

지난 10년간 캘린더와 일정의 사용이 상당히 증가했습니다. 엔터프라이즈 및 엔터프라이즈간 비즈니스는 이 정보 기술을 사용하여 이벤트 및 작업을 신속하게 스케줄링하는데 의존하게 되었습니다. 이 RFC는 서로 다른 일정 관리 애플리케이션과 일정 관리 애플리케이션 간에 가능한 상호 운용성 수준을 향상시키기 위한 것입니다. 이 RFC 문서는 전자 캘린더 및 일정 정보를 교환하기 위한 MIME 콘텐츠 유형을 정의합니다. 인터넷 일정 및 예약 핵심 객체 사양 또는 iCalendar를 사용하면 일정 및 일정 관리 애플리케이션에 일반적으로 저장된 정보를 캡처하고 교환할 수 있습니다. PIM(개인 정보 관리자, Personal Information Manager) 또는 그룹 일정 관리 제품과 같은 다른 시스템에서 사용할 수 있습니다.


iCalendar 포맷은 애플리케이션이나 시스템간의 교환 형식으로 적합합니다. 형식은 MIME 컨텐츠 타입으로 정의됩니다. 이렇게 하면 SMTP, HTTP, 파일시스템, 메모리 기반 클립보드 사용 또는 드래그/드랍, 점대점 비동기 통신과 같은 데스크탑 대화형 프로토콜을 비롯한 여러 전송 매체를 사용하여 개체를 교환할 수 있습니다. 통신, 유선 네트워크 전송 또는 적외선과 같은 유선 전송의 일부 형태로 제공됩니다.


이 RFC 문서는 모임이나 약속, 할 일, 업무 일지 항목 요청, 회신, 수정 및 취소와 같은 일정 및 일적 작업을 지원하기 위해 이 컨텐츠 타입을 메시지 집합에 매핑하는 iCalendar 개체 메서드의 정의도 제공합니다. iCalendar 개체 메서드를 사용하여 약속 있음 / 없음 시간 데이터 요청 및 응답과 같은 다른 일정 및 예약 작업을 정의할 수 있습니다. 이러한 스케줄링 프로토콜은 [2446bis]에 정의된 iCalendar iTIP(iCalendar Transport-Independent Interoperability) 에서 정의됩니다.


이 RFC 문서에는 [RFC5234]에 정의된 인터넷 ABNF를 기반으로 하는 컨텐트 타입에 대한 공식 문법도 포함됩니다. 이 ABNF는 파서를 구현 시 필요하며 메모의 서술적인 구문 정의를 해석할 때 모호함이나 질문이 생길 때 최종 참고 자료로 사용됩니다. ABNF 구문으로 쉽게 표현할 수 없는 추가 제한 사항은 ABNF에서 주석으로 지정됩니다. 표준 구문에 대한 주석은 그렇게 취급됩니다.



2. 기본 문법과 컨벤션

이 문서에서 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY" 그리고 "OPTIONAL" 키워드는 [RFC2119]에서 설명하고 있는대로 해석되어야 합니다.


이 RFC 문서는 캘린더 및 일정 포맷을 정의하기 위해 설명적인 산문과 공식적인 표기법 모두 사용합니다.


이 RFC 문서에 사용된 모든 숫자 값은 십진수 표기법으로 제공됩니다.


모든 속성 이름, 속성 매개변수, 열거 속성 값 및 속성 매개 변수 값은 대/소문자를 구분하지 않습니다. 그러나 달리 명시하지 않는 한 다른 모든 속성 값은 대/소문자를 구분합니다.


참고: 모든 들여쓰기 된 편집 노트는 독자에게 추가적인 정보를 제공하기 위한 것입니다. 이 정보는 이 RFC를 준수하는 구현을 구축하는데 필수적이지 않습니다. 이 정보는 RFC의 특정 특징이나 특성을 강조하기 위해 제공됩니다.


 iCalendar 객체의 형식은 [RFC2425] text/directory 미디어 타입의 구문을 기반으로 합니다. iCalendar 객체는 text/directory 미디어 타입 [RFC2425]의 프로파일은 아니지만 [RFC2425] 명세의 여러 요소를 재사용합니다.




2.1. 포맷팅 컨벤션

이 문서에 정의된 요소는 설명에 정의되어 있습니다. 이들을 설명하는데 사용된 많은 용오는 이 문서의 표준 사용법과 다른 일반적인 사용법을 가집니다. 이 문서에서 일정 및 일정 모델, 핵심 개체(Core object) 또는 상호 운용성 프로토콜 [2446bis]의 요소를 참조하기 위해 일부 포맷팅 컨벤션이 사용되었습니다. 캘린더 및 스케줄 롤은 각 단어의 첫문자가 대문자인 텍스트의 인용 문자열로 참조됩니다. 예를 들어 "Organizer"는 [2446bis]에 의해 정의된 스케줄링 프로토콜 내에서 "Calendar User"의 롤을 나타냅니다. 이 문서에 정의된 캘린더 구성 요소는 대문자로 된 따옴표로 묶은 텍스트 문자열로 참조됩니다. 모든 캘린더 구성 요소는 문자 "V"로 시작합니다. 예를 들어, "VEVENT"는 이벤트 일정 구성 요소를 나타내고 "VTODO"는 수행할 일정(TO DO) 구성 요소를 나타내고 "VJOURNAL"은 일일 업무 캘린더 구성요소를 나타냅니다. iTIP [2446bis]에 의해 정의된 스케줄링 방법은 대문자로 된 따옴표로 감싸진 텍스트 문자열로 참조됩니다. 예를 들어 "REQUEST"는 스케줄링 캘린더 구성 요소를 생성 또는 수정하도록 요청하는 방법을 나타내며 "REPLY"는 요청 수신자가 일정 구성 요소의 "Organizer"로 상태를 업데이트 하는데 사용하는 방법을 나타냅니다.


이 문서에 정의된 속성은 대문자로 된 따옴표로 감싼 텍스트 문자열과 "property"라는 단어로 참조됩니다. 예를 들어, "ATTENDEE" 속성은 캘린더 사용자의 주소를 전달하는데 사용되는 iCalendar 속성을 참조합니다. 이 문서에 정의된 속성 파라미터는 소문자로 묶인 텍스트 문자열과 "parameter"라는 단어로 참조됩니다. 예를 들어, "value"  특성 값의 기본값 유형을 대체하는데 사용되는 iCalendar 특성 파라미터를 나타냅니다. 이 노트에 정의된 열거형 값은 대문자로 된 텍스트를 단독으로 사용하거나, "value"라는 단어를 사용하여 참조됩니다. 예를 들어 "MINUTELY" 값은 "RECUR" 값 유형의 "FREQ" 구성 요소와 함께 사용되어 1분 이상의 간격을 기반으로 반복 구성 요소를 지정합니다.


다음 표에는 이 문서에서 참조하는 [US-ASCII] 문자 집합의 다른 문자가 나열되어 있습니다. 각 문제에 대해 이 표는 US-ASCII 10 진수 코드포인트와 함께 이 문서에서 사용된 문자 이름을 지정합니다.


+------------------------+-------------------+

| Character name         | Decimal codepoint |

+------------------------+-------------------+

| HTAB                   | 9                 |

| LF                     | 10                |

| CR                     | 13                |

| DQUOTE                 | 22                |

| SPACE                  | 32                |

| PLUS SIGN              | 43                |

| COMMA                  | 44                |

| HYPHEN-MINUS           | 45                |

| PERIOD                 | 46                |

| SOLIDUS                | 47                |

| COLON                  | 58                |

| SEMICOLON              | 59                |

| LATIN CAPITAL LETTER N | 78                |

| LATIN CAPITAL LETTER T | 84                |

| LATIN CAPITAL LETTER X | 88                |

| LATIN CAPITAL LETTER Z | 90                |

| BACKSLASH              | 92                |

| LATIN SMALL LETTER N   | 110               |

+------------------------+-------------------+ 



2.2. 관련 메모

이 RFC를 구현하는 사람은 이 문서와 함께 인터넷 캘린더 및 스케줄링 표준을 위한 프레임워크는 구성하는 몇 가지 다른 문서에 대해 잘 알고 있어야 합니다. 이 문서는 객체, 값의 타입, 속성과 속성 매개변수의 핵심 명세를 지정합니다.


- iTIP [2446bis]는 서로 다른 구현 간의 스케줄링을 위한 상호 운용성 프로토콜을 지정합니다.

- iCalendar 메시지 기반 상호 운용성 프로토콜(iMIP) [2447bis]는 [2446bis]에 대한 인터넷 전자 메일 바인딩을 지정합니다.


이 문서는 다른 문서에서 개념이나 정의의 명세를 반복하려고 시도하지 않습니다. 가능한 경우 이러한 개념이나 정의의 명세를 제공하는 문서를 참조하십시오.


3. iCalendar 객체 명세


다음 섹션에서는 캘린더 및 스케줄링 코어 객체 명세에 대한 세부 정보를 정의합니다. Calendaring, Scheduling Core Object 는 캘린더링과 스케줄링 정보의 컬렉션입니다. 일반적으로 이 정보는 하나 이상의 iCalendar 객체가 있는 iCalendar 스트림으로 구성됩니다. iCalendar 객체의 본문은 일련의 캘린더 속성과 하나 이상의 캘린더 구성 요소로 구성됩니다.


3.1. 절에서는 content 라인의 포맷을 정의합니다. 

3.2. 절에서는 속성 파라미터 형식을 정의합니다.

3.3. 절에서는 속성 값의 데이터 타입을 정의합니다.

3.4. 절에서는 iCalendar 객체 포맷을 정의합니다.

3.5. 절에서는 iCalendar 속성 타입을 정의합니다.

3.6. 절에서는 캘린더 구성 요소 타입을 정의합니다.

3.7. 절에서는 캘린더 속성을 정의합니다.

3.8. 절에서는 캘린더 구성 요소 등록 정보를 정의합니다.


이 정보는 MIME 컨텐츠 타입 등록을 위한 필수 요소입니다. 또한 이 정보는 그러한 컨텐츠 등록과 독립적으로 사용될 수 있습니다. 특히 이 문서는 파일, 메모리 또는 네트워크 기반 전송 메커니즘에서 캘린더 및 스케줄 교환 포맷을 위해 직접 적용할 수 있습니다.





Java 5부터 등장한 제너릭을 이제서야 정리해보려고 한다. 사실 만들어져 있는 제너릭 API들은 많이 사용해왔지만 직접 제너릭 클래스를 작성하는 일은 아무래도 일반 클래스를 작성하는 것보다 빈도가 낮다보니, 제너릭에 관해 최근에서야 알게 된 것들이 있기도 하다.

나는 이미 자바의 버전이 6일 때 배우기 시작해서 제너릭 전과 후의 차이를 극적으로 느끼지는 못했지만 가끔 레거시 코드 중에 제너릭을 쓰지 않고 컬렉션을 사용하는 코드를 보면 조금 힘들긴 하다(물론 제너릭이 늘 옳은 것만은 아니라고 생각한다).

1. 제너릭을 왜 쓰는 걸까요?

2. 제너릭 타입

3. Parameterized Type

4. 제너릭 메서드

5. Bounded Type Parameter

- Class & Interfaces.

6. 제너릭 타입 추론

7. 제너릭으론 할 수 없는 것들

8. 그렇게 중요하진 않지만 흥미로운 사실 - Multiple Extended(Implemented) Generic Interface.

9. assignable - Foo<T>, Bar extends Foo<T>, Foo assignable Bar. @Autowired

10. 제너릭 리플렉션

시간의 틀에 갇혀 지내는 느낌이다. 뭔가 숨쉬기에도 벅찬.. 공간에 비유해보자면 박스 안에 갇혀있단 느낌을 받는 것 같다.
이직을 준비하는 동안에는 시간의 여유는 있었어도 마음과 경제적 여유가 없었고, 지금은 경제적인 속박에선 조금 헤어나왔어도 시간의 여유가 없어져버렸다.
거기에 더해 스트레스를 풀 수 있는 방법이 전혀 없다는 것도 이 답답함에 한 몫을 하고 있는 것 같다. 학교를 다닐 땐 점심 때마다 코인 노래방에가서 시원하게 소릴 지르다오거나, PC방에 가서 서든을 하거나, 혹은 겨울엔 사람들을 모아서 스노우보드를 타러 가는 개인적인 즐길거리가 있었는데 지금은 너무나 척박하다.
하다못해 집 근처나 회사 근처에 코인 노래방이라도 있었음 좋겠다 ㅜㅜ

'마음이 뛰다' 카테고리의 다른 글

휴..  (0) 2015.12.18
레거시 코드를 건들면 아주 큰일나는 거죠..  (0) 2015.08.21
드디어 몸무게 앞자리가 6을 찍었다!  (2) 2015.07.29
도돌이표  (0) 2015.05.15
정작 나는 준비되어 있는가?  (0) 2015.03.27

면접 질문 중에 가장 당황했던 질문이 아니었나 싶다.


"StackOverflow가 발생했을 때 어떻게 해결하실건가요?"


너무 OutOfMemoryError에 대해서만 대비를 했던 탓인지 카운터를 맞았다. 답변도 OutOfMemoryError에 대한 해결 방법으로 해버렸다. 그러자 면접관께서 "그건 Heap 메모리에서구요, StackOverflow는 어떻게 될까요?"하고 내가 잘못 이해했다고 생각을 하셨는지 다시금 바로 잡아주셨지만.. 머리가 하얘졌더랬다.


면접이 끝나고, 정신이 좀 돌아오니 그제야 StackOverflowError는 호출 스택의 깊이가 너무 깊어질 때 발생한다는 게 생각났다. 웬만해선 질문을 받아도 알면 대답하고, 모르면 모른다고 대답을 했을텐데, 말그대로 정말 당황해서 어버버 했었던 것 같다.


먼저 Java API 문서의 StackOverflowError 클래스의 설명에는 "Thrown when a stack overflow occurs because an application recurses too deeply"로 되어있다.  그럼 StackOverflowError가 발생했을 때 어떻게 해야할까? Heap 영역과 마찬가지로 Stack 영역의 메모리를 늘려주면 되는걸까? 복구는 가능할까?


일단 에러 상황을 만들기 위해 무한 재귀호출을 하도록 아래와 같이 작성했다.



그리고 JVM Option에서 -Xss10K를 추가해서 실행해보았다. 근데 아래와 같은 메시지를 출력하면서 실행되지 않는다.


Error: Could not create the Java Virtual Machine.

Error: A fatal exception has occurred. Program will exit.


The stack size specified is too small, Specify at least 160k


160K의 기준이 있나 싶어 1.8 버전의 JVM 명세를 찾아봤으나 딱히 160K에 대한 기준은 없은 것으로 봐서는 JVM 구현에서 결정하는 사항인 듯 하다. 검색을 하다보니 CASSANDRA 이슈에서 Stack 사이즈와 관련된 내용이 있었는데 Java 6의 경우는 -Xss128K도 되는데 Java 7부터는 160K의 하한이 생긴 모양이다. (https://issues.apache.org/jira/browse/CASSANDRA-4275)


다시 스택 크기를 일단 최소로 잡아서 160K로 조정해서 실행해보았다. 751까지 출력된 후 StackOverflowError를 발생시킨다. 일부러 재귀구조로 프로그램을 작성하지 않는 이상 이 정도의 깊이까지 호출하는 경우가 있을지는 의문이긴하다.


스택 크기를 320K로 조정했을 때는 2615까지, 480K일때는 4476, 640K일 때는 6345로 출력되었다. 메서드 구현에 따라 스택 메모리를 소모하는 정도는 달라질 수 있을 것이고, 또한 스택 크기를 지정할 때 스레드 스택 프레임을 위해 기본으로 할당되는 영역까지 고려되어야 할 것 같다. Stack 메모리는 스레드별로 할당되므로 요청을 스레드별로 처리하는 웹 애플리케이션의 경우에는 Heap과 Stack의 크기가 세심하게 설정되어야 할 것 같지만 아직은 경험이 없으니 이에 대한 방법은 더 공부를 해봐야겠다.



그럼 StackOverflowError가 발생했을 때 일반적인 Exception과 마찬가지로 예외 처리가 가능할까? 일단 Error도 try-catch로 Error를 잡을 수는 있지만, 이미 비정상적인 조건에서 발생하게 되므로, 에러가 발생하기 전까지, 혹은 그 후에 처리가 정상적으로 되었는지 확신하기 어렵다. 이 경우에는 Error를 잡는 것보다, Error가 발생하는 원인을 찾아 해결하는 쪽이 더 바람직하다고 본다.



※ 참고 자료

- https://docs.oracle.com/javase/7/docs/api/java/lang/StackOverflowError.html

- https://docs.oracle.com/javase/7/docs/api/java/lang/Error.html

- https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

https://issues.apache.org/jira/browse/CASSANDRA-4275

- http://stackoverflow.com/questions/20658264/when-does-stackoverflowerror-occur

+ Recent posts