2016년 12월 19일 월요일

AES 암복호화 보완, (AES/CBC/PKCS5Padding 방식으로)

특정 DB컬럼의 값을 암호화하여 저장하라는 회사의 아키텍쳐 요건을 만족시키기 위해서 AES로 암호화하여 사용하고 있었다. 그런데 기존 방식이 AES/ECB/PKCS5Padding를 사용하고 있었는데 ECB가 문제가 되서 AES/CBC/PKCS5Padding로 변경하는 작업을 진행했다.
 

1.
암호화로 사용할 키는 KeyGeneratorSecureRandom으로 생성한다.

```
KeyGenerator generator = KeyGenerator.getInstance("AES");
SecureRandom random = new SecureRandom();
generator.init(128, random);
Key secureKey = generator.generateKey();

System.out.println(Base64.encodeBase64String(secureKey.getEncoded()));
```

2.
같은 문자열도 암호화할때마다 다른 문자열을 만들어내기 위해서 IV값을 랜덤하게 만들어준다.

```
/**
* 랜덤하게 IV 생성
*
* 같은 평문도 암호화할 때마다 암호문을 다르게 만들어 낸다.
* @return 랜덤하게 생성된 IvParameterSpec
*/
private static IvParameterSpec getRandomIvParameterSpec() {
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
return new IvParameterSpec(iv);
}
```

3.
IV값과 암호키로 암호화한 값을 합쳐야 하는데 java byte concat으로 검색해보니 무려 자바 System 클래스에 배열복사가 있었다!

```
// IV + 암호문으로 출력할 Byte를 붙인다.
byte[] ivcipherByte = new byte[16 + cipherByte.length];
System.arraycopy(iv.getIV(), 0, ivcipherByte, 0, 16);
System.arraycopy(cipherByte, 0, ivcipherByte, 16, cipherByte.length);
```

4.
다시 복호화할때는 입력된 byte를 IV와 암호값으로 다시 나눈다.

```
// 입력된 Byte를 IV + 암호문으로 나눈다.
byte[] originalIvByte = Arrays.copyOfRange(cipherByte, 0, 16);
byte[] originalCipherByte = Arrays.copyOfRange(cipherByte, 16, cipherByte.length);
```

5.
이렇게 만든 encrypt와 decrypt를 IN/OUT을 모두 String으로 만들어서 쓰기 좋게

```
/**
* 암호화하고, Base64로 인코딩
* @param plainText - 평문
* @return 암호문
*/
public static String encryptAndEncoding(String plainText) {
// 암호화하고 나서, 다시 문자열로 만들기위해 Base64 인코딩
return Base64.encodeBase64String(encrypt(plainText));
}

/**
* Base64로 디코딩하고, 복호화
* @param cipherText - 암호문
* @return 평문
*/
public static String decodeAndDecrypt(String cipherText) {
// 다시 Byte로 만들기위해 Base64 디코딩하고 복호화
return decrypt(Base64.decodeBase64(cipherText));
}
```


2016년 12월 18일 일요일

[예전글] 업무연락하기 노하우

2012년 8월에 후배들에게 보여주기 위해 썼던 내용을 옮겨와 본다. 당시 부서 스텝에서 신입사원에게 교육할 때 사용한다고 정리해 달라고 했던 내용이다. 지금 다시 보면 좀 다르게 정리할 수도 있는 내용이 섞여 있겠지만, 그 때의 내가 가진 기준이나 느낌을 그대로 살리기 위해 변경없이 블로그에 남긴다. 원본에는 예시로 실제로 내가 당시에 주고 받은 메일을 캡쳐하여 추가했었지만, 그건 이해를 돕기 위한 것이고 본문만으로도 충분히 의도가 전달되므로 블로그에는 생략한다.

# 업무연락하기 노하우

## 보고 + 기록 + 자료

보고 : 본인이 수행한 업무 결과를 타인에게 알려줌
>> 타인의 입장에서 필요한 정보만 기술한다.

기록 :업무관계자끼리 결정된 내용을 글로 남김
>> 결정된 내용과 그에 따른 할일을 표시한다.

자료 :업무수행을 위해 필요한 자료를 공유
>> 자료에 대한 설명 위주로 작성한다.

## 한번에 한가지 내용만

수신자가 같더라도 서로 다른 종류의 업무에 대한 내용이 하나의 메일/쪽지에 담기지 않도록 주의한다.

특히 답장을 보내면서 원문과 상관없는 내용을 추가하여 질문하는 것을 피한다.
이런 경우 별도의 메일/쪽지를 작성하여 보낸다.

## 제목은 구체적으로

ex) 회의결과 공유합니다.
어떤 주제에 대한 회의였는지 없음
회의결과에 대한 내용이 없음
수신인이 내용을 저장할때 난감함
나중에 검색할때 제목으로 검색이 불가

ex) 주민등록번호 입력은 숫자만 13자리로 결정함

## 수신자와 참조자를 구분

수신자
연락을 기다리는 사람
관련된 할일이 있는 사람

참조자
할일은 없지만 내용을 알고 있어야 하는 사람

수신/참조자 목록을 공유할 필요가 있을 경우
본문 시작에 수신자와 참조자를 구분하여 표시

## 표현은 간결하게

최대한 짧고 간단한 문장으로 표현한다.

자기소개를 장황하게 하지 않는다.

과도하게 높임을 사용하지 않는다.

## 번호로 내용 구분

여러 항목에 대한 내용이 포함된 경우
각각의 답변이 필요한 경우
쪽지/메일 안에서 각 항목을 지칭해야 하는 경우
ex) 1번과 2번이 결정된다면 3번은 생략하겠습니다.

한 번호에 내용이 너무 길지 않도록 주의. 그런 경우 번호를 나누면 된다.
번호마다 꼭 소제목을 달 필요는 없다.
번호마다 시공간상 흐름이 있다면 흐름대로 순서

## 중요한 내용은 색으로 표시

최종 결정된 내용
수신인이 해야 하는 일
이슈로 제기된 내용

붉은색, 파란색 글자색 활용
노란색, 연두색 바탕색 활용

## 첨부파일에 대한 설명필수

첨부파일에 대한 설명을 본문에 포함시킨다.
파일이 여러개일 경우, 파일명을 정확하게 명시하여 구분한다.

첨부파일의 특정부분이 중요한 경우, 위치를 명시한다.

본문에 첨부파일의 모든 내용이 포함된 경우, 본문에 동일함을 명시
수신자가 본문내용과 첨부파일 중에서 선택하여 읽을수 있도록

## 답장하는 내용은 색으로 구분

답장시에는 본인이 작성한 부분을 원문과 구분
특수기호를 넣거나 색을 다르게 표시

2016년 12월 16일 금요일

maven central에 없는 라이브러리 github에서 가져오기

개발셀에서 Gephi라는 프로그램의 Toolkit을 쓸 일이 생겨서 Toolkit으로 오픈된 라이브러리를 이용해서 커맨드라인으로 파일을 변환하는 자바 어플리케이션의 개발환경을 구축하게 되었다.

1.
라이브러리는 github에서 제공되고 있었는데, 다운로드를 받아보니 Fat Jar 형태였다. 즉, 라이브러리에서 직접 제공하는 자바 클래스와 실행을 위해 필요한 다른 라이브러리의 클래스까지 모두 하나의 jar로 압축되어 있었다.

2.
담당 개발자가 POC처럼 이것저것 해볼때는 fat jar를 다운받아서 로컬에서 이클립스 프로젝트를 구성하여 사용하였는데, 이걸 다시 github에 올리기 전에 빌드를 gradle로 구성하기로 했다.

3.
우선 Gephi Toolkit이 maven에 올라가져 있길래 gradle dependency에 추가했다.

```
dependencies {
// gephi toolkit
compile group: 'org.gephi', name: 'gephi-toolkit', version: '0.9.1'
}
```

4.
수많은 다른 라이브러리들을 하나씩 내려받다가 2개 라이브러리를 내려받지 못해서 에러가 발생했다. 찾아보니 정말로 maven central에 각각의 라이브러리는 있지만 찾고 있는 버전은 등록되어 있지 않았다. stax-utils-snapshot-20100402.jartrove-2.1.0.jar이 그것들이다.

5.
검색을 좀 해보니 당연히 나만 이런 것이 아니었고, 해결책으로는 
들이 이미 덧글로 달려있었다. 2개만 별도로 다운로드 하라는 것은 그 2개의 라이브러리가 가지고 있을지도 모르는 depencency는 무시하게 될 것이다. 그래서 all 버전을 사용하기로 결정했다.

6.
그렇다고 all 버전을 git에 올릴 수는 없었다. 그래서 github에 제공된 링크를 통해서 gradle 빌드시점에 다운로드 받는 방법을 찾아보기로 했다. 조금 검색해보니 Gradle Download Task라는 plugin을 찾을 수 있었다. Java 컴파일 하기전에 다운로드 받도록 gradle 파일을 구성했다.

```
apply plugin: 'de.undercouch.download'

import de.undercouch.gradle.tasks.download.Download

task downloadGephiToolkit(type: Download) {
    src 'http://github.com/gephi/gephi-toolkit/releases/download/v0.9.1/gephi-toolkit-0.9.1-all.jar'
    dest 'lib/'
    overwrite false
}

compileJava.dependsOn downloadGephiToolkit
```

7.
그리고 gradle의 dependency도 아래처럼 변경하고, 마지막으로 gradle 파일이 이렇게 된 이유를 설명하는 주석을 달아두었다.

```
dependencies {
// gephi toolkit
compile name: 'gephi-toolkit-0.9.1-all'

//XXX 아래처럼 선언하면 stax-utils와 trove를 maven에서 찾다가 에러난다. 그래서 gephi-toolkit-0.9.1-all을 사용한다.
// compile group: 'org.gephi', name: 'gephi-toolkit', version: '0.9.1'
}
```




2016년 12월 13일 화요일

Underscore.js 스터디 계획

자주 사용하게 되는 Underscore.js에 대한 스터디를 시작하게 되어, 그 계획을 짧게 적어본다. 다양한 API에 대해서 인지하고, 사용법을 숙지하고, Functional Programming에 대해서 간단히 감을 잡는 시간이 될 것 같다. 그리고 사용법만 보지 않고 소스코드도 같이 보면 좋을 것 같다.

1. 첫째날
- Introduction : 같이 읽어 보고 어떤 느낌의 라이브러리인지 공감한다.
- Collections : 이때 filter, map, reduce 유형으로 크게 나누고, 각 함수마다 어느 유형에 속하는지 이야기해본다.


2. 둘쨰날
- Arrays
- Objects
- OOP Style
- Chaining

3. 셋째날
- Functions
- Utility

4. 넷째날
- Change Log
- Links : Underscore.string, Underscore.java
- 다른 라이브러리 : Lodash

2016년 12월 1일 목요일

git repo로 구성된 소스코드의 Tab과 Space 일괄 통일 작업 실행


예전에 작성한 git repo로 구성된 소스코드의 Tab과 Space 일괄 통일 작업 계획을 오늘 실제로 실행했기에 블로그를 적어 본다.

작업 전에 미리 생각해볼 문제들


Tab과 Space 중 Tab으로 결정
- 이유 1 : 개발팀원이 모두 Tab을 선호함
- 이유 2 : 이클립스의 기본 설정
- 이유 3 : 키보드의 Tab키를 눌러서 들여쓰기를 하니까 실제 행동과 결과를 맞추려고

Space 4개를 Tab 1개로 하고, 4개 미만의 Space는 버림
- 이유 1 : 단순한 계산이 가능해서
- 이유 2 : 이클립스의 기본 설정이 Space 4 = Tab 1

변경 후 혹시 틀어지게 되는 들여쓰기는 무시했다.
- 이유 1 : 나중에 그 부분을 수정할 때 고치겠지.
- 이유 2 : 어차피 지금도 일관된 규칙으로 들여쓰기 되지 않음
- 이유 3 : 일괄로 들여쓰기 정도를 고치면 들여쓰기가 틀어질 수 있음

일괄 작업 날짜는 특정 버전 릴리즈 직후로 했다.
- 이유 1 : 개별 개발자가 별도 branch를 만들 경우가 거의 없다.
- 이유 2 : 일괄 작업한 결과를 master와 development 모두 merge 가능

작업하는 김에 줄끝의 공백문자도 같이 지워주고, 줄바꿈문자도 UNIX(\n)로 같이 통일했다.


작업 방법


소스코드를 변경하는 java 프로그램을 작성해서 일괄로 작업했다. 이런 작업을 일괄로 하위 디렉토리까지 해주는 도구를 찾아볼까도 생각했는데, 프로그램을 직접 만드는게 간단할 것 같아서 그냥 구현했다.

파일을 한줄씩 읽어서 처리하고 다시 한줄씩 쓰는 형태인데, java7 nio 패키지의 Files를 이용해서 한번에 읽어서 처리하고 다시 한번에 쓰는 형태로 구현했다.

그리고 Files.write를 할 때 UNIX 줄바꿈 문자를 사용해야 하는데, 작업을 해보니 그대로 DOS형식이 유지되어 파일이 생성되고 있었다. javadoc을 보면 line.separator라는 시스템 프러퍼티에서 지정한 플랫폼별 줄바꿈문자를 사용한다고 한다. 그래서 시스템 프러퍼티를 강제로 \n으로 바꿔 주어야 한다.

```java
System.setProperty("line.separator", "\n");

List<String> readAllLines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);

for (int i = 0; i < readAllLines.size(); i++) {
String line = readAllLines.get(i);
String changedline = StringUtils.stripEnd(line, null);
changedline = changeToTab(changedline);
readAllLines.set(i, changedline);
}

Files.write(file.toPath(), readAllLines, StandardCharsets.UTF_8);
```

이 프로그램이 정말 소스코드의 앞뒤 공백문자와 줄바꿈문자만 변경했는지를 확인하고 싶었다. 그래서 생각해 낸 것이 git diff를 공백을 무시하여 확인하는 방법인데, command line에서 아래 명령을 실행해서 변경된 것이 아무것도 없는 것을 확인하여 예상대로 작업이 되었다고 확인할 수 있었다.

```
git diff --ignore-all-space
```


작업 후 처리


앞으로 변경되는 소스코드에서 일괄작업에서 적용한 규칙을 지켜서 git 형상에 들어올 수 있도록 개별 개발자의 개발편집환경을 맞출 필요가 생겼다. 우리 개발팀은 모두 이클립스를 사용하므로, 이클립스에서 각 프로젝트의 환경설정과 AnyEdit라는 플러그인의 옵션을 설정하는 것으로 이를 맞출 수 있었다.

.settings/org.eclipse.core.resources.prefs 파일 내용
eclipse.preferences.version=1
encoding/<project>=UTF-8

.settings/org.eclipse.core.runtime.prefs 파일 내용
eclipse.preferences.version=1
line.separator=\n

.settings/de.loskutov.anyedit.AnyEditTools.prefs 파일 내용
activeContentFilterList=
addNewLine=false
convertActionOnSaave=AnyEdit.CnvrtSpacesToTabs
eclipse.preferences.version=1
fixLineDelimiters=true
ignoreBlankLinesWhenTrimming=false
inActiveContentFilterList=
javaTabWidthForJava=false
org.eclipse.jdt.ui.editor.tab.width=4
projectPropsEnabled=true
removeTrailingSpaces=true
replaceAllSpaces=false
replaceAllTabs=false
saveAndAddLine=false
saveAndConvert=false
saveAndFixLineDelimiters=true
saveAndTrim=true
useModulo4Tabs=false

2016년 11월 23일 수요일

90년대 음악과 영화에서 찾아낸 상대방을 위로하는 말 3가지

예전부터 누군가를 위로한다는 것을 항상 어려워 했다. 상대방을 걱정하는 마음, 슬퍼하는 마음을 말이나 행동으로 전하는 것이 너무 어려웠다. 막연히 누군가가 위로하는 방법을 알려줬으면 했다.

나름의 나이를 먹고, 세월을 지내고 보니 나만의 방법이라는 것이 생겼다. 방법이 생겼다기 보다는 어떤 말로 시작해야 할지 알게 되었다. 이 말로 위로를 시작하면 좋을 것 같다는 생각을 하게 되었다. 이 3가지 말을 나에게 알려준 90년대 음악과 영화를 함께 적어 본다.

1. You are not alone

우선 당신은 혼자가 아니에요. 당신이 느끼는 어려움은 다른 사람들도 다 느끼는 것이에요. 당신이 처한 상황에 당신만 있는 것이 아니에요. 그리고 당신이 혼자 그 어려움을 견디는 것이 아니에요. 내가 있어요.

Michael Jackson - You Are Not Alone (1995)

2. It's not your fault

그리고 당신 잘못이 아니에요. 당신이 이런 상황을 만든 것이 아니에요. 당신도 어쩔 수 없었어요. 당신이 바꿀 수 없었을 거에요. 당신의 잘못이 아니라는 것을 알고 있어요.

Good Will Hunting (1997)


3. Everything's gonna be alright

그리고 다 잘 될 거에요. 어려운 일을 해야 하나요? 당신은 할 수 있을 거에요. 걱정되는 일이 있나요? 잘 해결될 거에요. 잘 될 거라는 생각을 해보세요. 그렇게 될 거에요. 

Sweetbox - Everything's Gonna Be Alright (1997)



2016년 11월 1일 화요일

Nodejs 서버에 Let's Encrypt 설치해서 https 적용 후기

daag.pe.kr에 https를 적용해달라는 요청이 있어서 그동안 봐두었던 Let's Encrypt를 적용한 후기를 남긴다.

1.
우선 Let's Encrypt에 접속해서 Getting Started를 가보니 shell 접근이 가능하면 certbot을 사용하면 편할거라고 한다. certbot 페이지에 가보면 nodejs는 선택지에 없다. 그리고 난데없이 certonly를 하라고 하고, webroot를 하라고 하는 등... 난 nodejs인데.. 하면서 letsencrypt와 nodejs, express로 검색을 하다보면 letsenrypt-express가 검색이 되서 나오게 된다. 설명을 보면 등록부터 사용, 갱신까지 다 알아서 해주는 것 처럼 보인다. 이렇게 생각하면 여기서 헤매게 된다.

2.
우선 키발급을 먼저 하고, 그 다음에 서버에 적용할 생각을 해야 하는데, 키발급을 인터넷 사이트에서 하려고 생각하는 순간 역시 헤매게 된다. 그러다가 certbot의 문서 중 이곳을 천천히 읽다가 키발급의 원리를 깨닫고 나서야 진행이 쉽게 되었다.

This will carry out the steps needed to validate that you control the domain(s) you are requesting a cert for, obtain a cert for the specified domain(s), and place it in the /etc/letsencryptdirectory on your machine

키발급을 요청하는 사람이 그 도메인에 대한 진짜 주인인지를 판단을 해야 하는데, 그 방법을 해당 도메인의 루트에 특정 파일을 심어 놓고 외부에서 접속해서 그 파일을 확인하는 방법을 사용한다. (아마 그런 것 같다)

3.
그래서 certbot에게 certonly라는 명령으로 키발급만 한다고 알려주고, --standalone으로 직접 80/443 서버를 띄워서 도메인을 확인하거나, --webroot-path 옵션으로 기존에 떠있는 서버를 이용해서 도메인을 확인하는 과정을 거쳐야 /etc/letsencrypt 디렉토리에 pem 파일이 생성된다.

sudo ./certbot-auto certonly --standalone -d 도메인주소

또는

sudo ./certbot-auto certonly --webroot-path 도메인주소로접근시서비스되는디렉토리 -d 도메인주소

이렇게 하고, 이메일 주소를 더 넣거나 하는 등의 과정을 거치면 아래처럼 정상메시지를 볼 수 있다.

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/도메인주소/fullchain.pem. Your cert will
   expire on 2017-01-29. To obtain a new or tweaked version of this
   certificate in the future, simply run certbot-auto again. To
   non-interactively renew *all* of your certificates, run
   "certbot-auto renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate

   Donating to EFF:                    https://eff.org/donate-le

4.
이 때 standalone으로 하는데, 80이나 443포트를 다른 프로세스에서 사용중이면 안된다. 그리고 443포트가 외부에서 접근이 가능한 상태인지, AWS의 경우 Security Group을 확인해줘야 한다.

5.
이제 nodejs 서버의 express에 letsencrypt-express를 적용하면 된다. 예제코드처럼 express()를 한번 래핑하는 식으로 쉽게 구현이 가능하다. 물론 실제 코드에서는 config 파일을 이용해서 아래처럼 구현한다.

```
var express = require('./config/express');

var app = require('letsencrypt-express').create({
        server: config.https.server,
        email: config.https.email,
        agreeTos: true,
        approveDomains: config.https.domains,
        app: express()
}).listen(config.port, config.https.port);


6.
nodejs 서버는 ubuntu 계정에서 실행하기 때문에 포트포워딩도 해줘야 한다.

```
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3080
sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 3443

7.
서버를 띄우면 알아서 /etc/letsencrypt/ 에서 발급된 키파일을 이용해서 서버를 띄워주는 것 같다. 그리고 초반에 server를 'staging'으로 해서 한번 띄우고 나면 다시 운영모드로 server를 바꾸고 서버를 재시작해도 키발급자가 "Fake LE Intermediate X1"으로 뜨면서 인증되지 않은 인증서라고 나오는 경우가 있다. 이럴 때는 ~/letsencrypt 디렉토리를 한번 삭제해주면 된다.

8.
그래서 이 작은 초록색 자물쇠를 결국 얻어냈다. ㅎㅎㅎ


2016년 10월 18일 화요일

작은 윈도우 배치 파일 만들기 - 변수,부모디렉토리,아규먼트전달

오랜만에 작은 윈도우 배치 파일을 만들 일이 있어서 그 결과를 공유한다.

1.
@ECHO OFF 말고는 생각이 안나서 일단 검색을 해보니 한글 블로그도 있고, 위키피디아도 있지만 세번째 검색결과로 나온 외국 개발자의 블로그가 오버뷰로 제일 볼만한 것 같다.

2.
윈도우 배치 파일에서는 SET 변수명=값 으로 변수를 설정하고, %변수명%으로 변수를 사용한다. 주의해야할 점은 리눅스와 달리 값을 쌍따옴표로 묶으면 쌍따옴표가 그대로 값으로 사용된다.

```
SET JAVA_OPTIONS="-Xms128M -Xmx512M -Xmx1024M"
java %JAVA_OPTIONS%        ===> java "-Xms128M -Xmx512M -Xmx1024M"
SET JAVA_OPTIONS=-Xms128M -Xmx512M -Xmx1024M
java %JAVA_OPTIONS%        ===> java -Xms128M -Xmx512M -Xmx1024M
```

3.
배포중인 어플리케이션의 구조가 /bin, /conf, /lib 와 같은 구조라서 실행파일의 부모디렉토리까지의 경로가 필요한데, 이것도 검색을 통해 아래처럼 구현했다.

```
for %%i in ("%~dp0..") do set "parent=%%~fi"
SET APP_PATH=%parent%
```

4.
배치 파일에 입력된 아규먼트를 갯수와 상관없이 그대로 사용하고자 할 때는 %* 를 사용하면 된다.

2016년 10월 6일 목요일

Java에서 Exabyte 급의 File System의 Free Space 구하기

특정 디렉토리가 속한 파일 시스템의 전체 용량과 사용가능한 용량을 주기적으로 체크해서 데이터베이스에 기록하는 프로그램이 있다. 처음 개발한 이후로 2년여 동안 별다른 문제가 없었는데, 지난주에 AWS에 배포된 시스템에서 문제가 생겼다.

전체 용량이 -999999999999999 비슷한 수의 음수로 저장되고 있었다. 로그를 확인해보니 시스템이 계산한 전체 용량은 -9223372036854775808 이었다. 이 또한 무슨 일인지?

시스템에서 df로 확인해 보았더니, 전체 용량이 8 EB였다. 이건 자바의 long 데이터타입의 최대값을 벗어나는 값이다.

[ec2-user@hostname]$ df
Filesystem                                                  1K-blocks      Used        Available Use% Mounted on
xxx.amazonaws.com:/ 9007199254740992 238655488 9007199016085504   1% /efs

찾아보니 프로그램에서 사용한 자바의 File 클래스의 getTotalSpace 메서드의 리턴이 long이고, 이 메서드가 음수를 리턴하고 있었다. 조금더 검색해보니 이미 2016년 7월 26일에 버그로 리포팅된 내용이었다. 여기여기에서 확인이 가능하다. 내용을 보면 리눅스는 64-bit의 unsigned integer 2개로 블록사이즈와 블록갯수로 파일시스템의 용량을 표현하는데 자바는 이걸 곱한 값을 64-bit signed long으로 리턴한다는 것이다.

버그가 고쳐지길 기다릴 수는 없기에 검색을 해보니 apache common io 라이브러리가 만들어놓은 FileSystemUtils 클래스의 freeSpaceKB 메서드가 있었다. 설명을 읽어보니 내부적으로 리눅스의 df 명령의 결과를 파싱해서 리턴하는 것 같다. 그래도 KB로 리턴하니까 overflow는 발생하지 않을 것 같다.

그런데 사용가능한 용량외에도 전체 용량도 KB단위로 리턴하는 메서드가 필요한데 그것은 아무리 찾아봐도 나오지가 않는다.

결국 자체적으로 df를 파싱해서 전체 용량과 사용가능한 용량을 구해야 할 것 같다. 그리고 이 기능은 df가 가능한 리눅스에서만 정상적으로 동작할 것이다.



2016년 9월 19일 월요일

Apache Common IO의 BOMInputStream의 버그

Java는 BOM을 별도로 처리해주지 않는다고 한다.
http://bugs.java.com/view_bug.do?bug_id=4508058
http://bugs.java.com/view_bug.do?bug_id=6378911

유니코드에서는 UTF-8인 경우 BOM을 넣지 말도록 권장한다.
하지만 넣는다고해서 BOM규칙을 어기는 것도 아니다.
https://en.wikipedia.org/wiki/Byte_order_mark

즉, BOM은 있을 수도 있고, 없을 수도 있고, Java가 알아서 처리해주지도 않는다.
그래서 아파치에서 Common IO에 BOMInputStream을 만들어놨다.
https://commons.apache.org/proper/commons-io/javadocs/api-release/org/apache/commons/io/input/BOMInputStream.html

그런데 BOMInputStream을 사용하면서 뜻밖의 사소한 버그로 고생을 좀 했다.

첫번째 버그
BuffredReader로 감싸서 readLine()을 호출하면 파일의 마지막에서 null을 리턴하지 않고, IOException을 발생시킨다.
이건 2.0.1에서 고쳐진 버그이다.
https://issues.apache.org/jira/browse/IO-257
http://svn.apache.org/viewvc?view=revision&revision=1052095

두번째 버그
hasBOM(ByteOrderMark) 메서드가 올바른 값을 리턴하지 않는다. hasBOM()을 한번 호출하고 나면 올바른 값을 리턴한다. 이건 누군가가 아파치 JIRA에 리포팅해서 수정도 했지만, 반영되진 않은 상태이다.
https://issues.apache.org/jira/browse/IO-482


2016년 9월 7일 수요일

git repo로 구성된 소스코드의 Tab과 Space 일괄 통일 작업 계획

소스코드의 들여쓰기를 Tab과 Space 중에 무엇으로 하는가에 대한 이야기가 있다. 설문조사도 있고, github의 현황을 통계낸 자료도 있다. 업무로 개발중인 소스코드에서는 표준없이 마구 개발하다보니 통일되어 있지 않은데, 관련해서 생각을 정리해본다.

왜 통일하려고 하는가?

  • 머지 리퀘스트 diff 중에 공백문자로 인한 불필요한 diff를 보기 싫어서
  • 에디터 설정에 따라서 인덴트가 둘쑥날쑥하면 소스 보기 어려워서
  • 마음속 깊은 곳의 불편함을 없에고 싶어서
  • 그냥 해보고 싶어서

통일작업시 고려할 것은?

  • Tab과 Space중 무엇으로 할 것인가? (개발팀의 의견 중요)
  • Space로 하면 몇 칸으로 할 것인가? (2 or 4)
  • Space를 Tab으로 바꾸면 Space 몇 칸을 Tab 1개로 할 것인지?
  • Space 나머지는 어떻게 할 것인지? (ex: Space 4이 Tab1개인데, Space가 14칸일때 Tab은 몇개?)
  • 바꾸고 나면 인덴트가 틀어지는데, 파일 전체의 자동 인덴트를 할 수 있을지?

통일작업 이외에 고려할 것은?

  • 언제 BigBang 날짜를 잡을 것인가?
  • 작업중인 feature 들은 어떻게 할지? (머지 리퀘스트 할때 conflict 날텐데 ㅠㅠ)
  • 하는 김에 줄끝의 공백문자도 같이 지워주면 좋다.
  • 하는 김에 줄바꿈 문자도 통일하면 좋다.
  • 공백문자만 변경된 머지 리퀘스트는 공백문자만 바뀌었는지를 어떻게 보장할지?
  • development에서 작업하고나서, 다음 master 머지전까지 master에서 변경되는 hotfix는 develoment로 어떻게 (자동으로) 머지할지?

통일작업 완료되면 할일은?

  • 개발표준 문서/위키 변경 및 공지
  • 모든 개발자의 에디터의 Save Action 설정 통일
  • 커밋 훅 추가해서 안지켜진 소스는 reject
  • 머지 리퀘스트에서는 머 해줄거 없을까....
  • 작업 일지 남기기

2016년 8월 23일 화요일

gradle로 application 배포zip 파일 만들기

회사 업무 중에 linux에서 stand alone으로 실행되는 java 프로그램이 하나 있다. 이걸 개발하고 빌드하고 배포해서 실행하는 일련의 과정이 옛날의 방식으로 되어 있어서 최근 몇일동안 요즘의 방식을 적용하면서 자동화를 했다.

1. ant -> gradle
이전에는 ant로 빌드 task를 하나씩 정의해서 순서대로 실행되도록 depends를 걸고 실행해서 jar 파일을 만들었다.
이제는 gradle로 빌드환경을 바꿔서 gradle의 java 플러그인이 가진 convention을 활용하여 별도로 정의한 task 없이 jar task에 속성이나 메서드를 정의해서 jar를 만들게 변경했다. 이변경을 통해서 ant xml 파일의 수십줄의 보통의 빌드스크립트를 삭제할 수 있었다.

```
apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.7
targetCompatibility = 1.7

compileJava.options.encoding = 'UTF-8'

jar {
baseName 'sds-genomics-analysis'
    manifest {
        attributes 'Main-Class': 'aaa.bbb.ccc.DddMain'
    }
    exclude (['spring/**', 'properties/**', 'logback.xml'])
}
```

2. dependency
이전에는 라이브러리에 대한 dependency를 관리하지 않고 있었다. 필요한 라이브러리가 있을 때마다 jar 파일을 다운로드 받아서 lib 폴더에 넣고 classpath에 추가했었다. 물론 jar파일까지 git repo에 커밋했어야 했다.
이제는 gradle을 적용하면서 사내 nexus를 repository로 지정하고 dependency를 선언하여 빌드시점에 자동으로 다운로드 받도록 변경했다. 당연히 jar 파일들은 git repo에서 빠지게 되었다. 이제 라이브러리 버전업을 큰 걱정없이 시도해볼 수 있게 되었다.

3. distZip
jar 라이브러리파일, xml 설정파일, 프러퍼티파일, 시작/종료 쉘스크립트파일을 적절한 폴더구조로 만들어서 배포판을 만드는 것도 모두 ant에 xml로 정의된 스크립트로 존재했었다.
이제는 gradle의 distZip task를 활용해서 간단하게 배포판 zip파일을 만든다.

처음에는 gradle의 application 플러그인을 적용했었다. application 플러그인은 startScripts task와 distZip task를 가지고 있는데, 각각 시작 쉘스크립트 파일을 만들고, 배포 zip파일을 convention을 따라 만들어준다. 그런데 startScripts가 시작 쉘스크립트만 만들고 종료 쉘스크립트는 만들지 않고, 만들어지는 쉘스크립트 내용도 내맘대로 구성하는 것이 잘 안되서 나만의 distZip을 만들어서 사용했다. 그리고 쉘스크립트는 실행권한을 주기 위해서 zip으로 만들때 755권한을 준다.
```
task distZip(type: Zip){
def baseDir = { archiveName - ".zip" }
into("/") {
// shell 파일은 실행권한을 주기 위해 별도로 포함시킨다.
from(project.file("src/dist")) {
        exclude('**/*.sh')
    }
from(project.file("src/dist")) {
        include('**/*.sh')
        fileMode = 0755
    }
}
into("conf"){
from(project.file("src/main/resources")) {
        exclude('sqlmap')
    }
}
into("lib") {
    from(jar)
    from(project.configurations.runtime)
}
}
```

4. start.sh
이전에는 시작 쉘스크립트에 실행디렉토리 위치가 하드코딩되어 있었다.
이제는 쉘스크립트가 실행되는 위치를 인식해서 경로를 찾아내도록 변경하였다. $0 파라미터를 사용하는 것이 기본 아이디어인데, 심볼릭링크 제거를 위해 readlink를 사용하고, 부모 디렉토리를 얻기 위해 dirname을 2번 사용했다.
```
#!/bin/sh
APP_PATH=$(dirname `readlink -f $0`)
APP_PATH=$(dirname $APP_PATH)
echo $APP_PATH
```


2016년 8월 17일 수요일

study내용 jquery의 ajax

Ajax
  • Global Ajax Event Handlers
  • Helper Functions
  • Low-Level Interface
  • Shorthand Methods

1.
먼저 Ajax Event에서 ajax가 발생할 때마다 호출되는 이벤트 핸들러의 종류와 순서를 먼저 인지한다. 모든 ajax 호출에 대해서 같이 발생하는 ajaxStart와 ajaxStop 이벤트는 발생시점을 다른 것과 분리해서 생각하면 된다. 
그리고 나서 Global Ajax Event Handlers를 본다. 개별 ajax의 이벤트는 ajax()를 볼 때 본다.

2.
ajax()가 포함된 Low-Level Interface를 본다. 길고긴 ajax()를 먼저 본다. 특히 중간에 ajax()의 리턴 객체인 jqXHR를 주의깊게 본다.
ajax()를 보고나서, ajaxSetup()와 ajaxPrefilter()를 비교하면서 본다. ajaxSetup()은 option 객체를 받아서 디폴트를 변경하는 것, ajaxPrefilter()는 함수를 받아서 ajax실행될 때마다 전달된 option을 변경하는 것.

3.
그 다음 Showhand Method를 본다. 각각 무엇이 단축된 것인지 확인한다.

4.
마지막으로 Helper Functions을 본다. 3개 함수의 호출관계를 파악하면 좋다. serialize()의 소스코드를 보면 제일 간단하다. serializeArray()를 호출한다음 param()을 호출한다.



2016년 7월 2일 토요일

Mybatis Plugin으로 특정 DB컬럼값을 암호화/복호화 처리

Mybatis를 이용하여 DAO 클래스를 생성하여 DB 조회/처리를 담당하게 만들어서 사용하고 있다. DB의 특정 테이블의 특정 컬럼은 값을 암호화하여 저장하고 있다. DB에 넣기 전에 암호화하고, 읽고 나서는 복호화를 해야 어플리케이션에서 처리가 가능하다.

1. 현재 암복호화 방식
현재는 DAO 패턴을 사용하고 있고, DAO에 데이터를 항상 DVO를 이용해서 전달하고 전달받고 있다. 그리고 암호화 대상인 컬럼인 경우에는 DVO에 encrypt() 메서드와 decrypt() 메서드를 DAO에서 호출해서 암호화/복호화를 수행하고 있다.

2. 문제1
암호화 대상 컬럼이 변경되는 경우, 대상 컬럼이 포함된 기존의 DVO 소스를 모두 변경(또는 재생성)해야 한다. 암호화 대상은 자주 바뀌는 성질은 가지고 있진 않지만, 변경되는 경우 그 변경 영향도가 매우 크고, 테스트에 대한 부담감도 커지게 된다.

3. 문제2
DAO에 DVO이외의 자료구조를 이용해서 데이터를 주고 받고자 한다면, 암호화컬럼에 대한 처리 방식을 고민해야 한다.

3. 문제3
DVO마다 encrypt()와 decrypt() 메서드를 항상 구현해야 한다. 비록 그 메서드 내용이 아무것도 없다고 하더라도. 또는 모든 DVO가 특정 클래스를 상속받도록 하고, 상위 클래스에 아무것도 구현안된 encrypt()와 decrypt()를 구현해 놓아야 한다. 이러면 DVO가 일반 POJO가 아니게 된다.

4. 해결책
Mybatis의 plugin 기능으로 해결이 가능하다. 모든 쿼리에 대해서 실행 전에 Parameter 중 특정 컬럼에 해당하는 이름을 가진 필드값을 암호화하고, 실행 후에 ResultSet에서 같은 이름의 필드값에 대해서 복호화를 수행하는 것이다.

5. 여전히 문제점
아직 DAO의 Parameter와 ResultSet이 DVO로 구성된다는 가정이 존재한다. Primitive 타입이나 String, Map과 같은 Java 기본 클래스도 사용하게 된다면, 그런 자료구조에 대한 처리도 추가로 필요할 것이다.

6. 걱정
모든 쿼리의 입출력 DVO에 대해서 Reflection을 사용하고 있어서 성능상 문제가 되지는 않을지 걱정이다.

7. 한계
실제로 DB에서는 암호화 컬럼인데, SQL의 ALIAS를 이용해서 컬럼의 이름을 변경하면 복호화되지 않은 체로 ResultSet이 리턴될 수 있다.





2016년 5월 30일 월요일

git format-patch와 git am

출장때문에 집으로 회사 노트북을 가지고 왔더니, 집에서 git에 commit을 몇 번 했다. 이걸 회사 git에도 반영해야 하는데, 출장을 다녀와서 반영하기에는 일주일이라는 시간이 기다리는 사람 입장에서는 좀 길게 느껴질 것 같다.

언듯 이메일로 커밋 내용을 보내면 다른 사람이 대신 커밋해줄수 있다고도 들어서, 이걸 해보기로 했다.

1.
git email patch라고 검색하니까 세번째 검색결과에 원하는 git 명령어들이 모여있었다. 여러가지가 있는데, 내가 원하는 것은 git format-patch와 git am이었다.

2.
git format-patch명령어를 사용해서 내가 작업중인 branch에 커밋한 내용을 이메일 전송 형식의 파일로 만들어 낼 수 있었다. git format-patch -8 명령으로 최근 8개의 커밋의 패치파일을 한번에 만들었다.

3.
파일을 회사 메일로 보내고, VDI로 접속해서 패치파일을 git workspace에 복사하고 git am 0001.patch를 입력하니 에러가 발생했다. tailing-space와 관련된 에러였는데, --whitespace=fix 옵션을 추가해서 git이 알아서 고치도록 만들었다.

4.
git am --whitespace=fix 0002.patch 0003.patch 0004.patch ........ 0008.patch를 입력해서 한번에 공백문자 해결하면서 commit이 적용되도록 만들었다. remote에 push까지 하고 빌드도 수행했다.

5.
출장 중에 수정되는 소스를 로컬 git repo에 commit하고, 위와 같은 방식을 거쳐서 그날그날 회사 git 서버에도 적용이 가능할 것 같다.

2016년 4월 19일 화요일

Java의 Property와 ResourceBundle

개인프로젝트로 작은 java 라이브러리를 만들고 있다. 항상 라이브러리를 가져다 쓰는 입장이었는데, 만들려고 하니 평소에는 쉽게 지나치던 것들도 유심히 따져보게된다.

1. Properties
Java 1.0부터 있는 클래스다. Java 1.7기준으로 javadoctutorial을 볼 수 있다. 보통 properties 파일은 UTF-8이 아니어서 한글을 입력하면 \u313B처럼 인코딩되어 보이곤 하는데, load() 메서드에 인코딩을 UTF-8로 지정한 InputStreamReader을 던지면 UTF-8도 상관없다고 한다.

2. ResourceBundle
Java 1.1부터 있는 클래스다. Java 1.7기준의 javadoctutorial은 각각의 링크에서...

3. 차이점
두 클래스의 차이점은 Locale에 있는 듯 하다. Locale마다 다른 properties 파일을 읽어서 처리하고자 한다면 ResouceBundle은 이를 Locale 클래스 기반으로 알아서 처리해준다. 클래스이름에 "Bundle"이 있는 이유도 각 언어마다 properties파일을 만들기 때문에 "묶음"이 될 텐데 이 떄문인 듯 하다. 관련 링크1, 링크2

나는 지금 라이브러리를 사용하는 곳의 클래스패스에 존재하는 properties 파일을 읽어서 DB접속이나 라이브러리 기능을 제어하고자 하므로 Properties를 사용하는 것이 맞을 것 같다.

소프트웨어 개발의 3가지 원칙

이전에 블로그로 내가 믿지 않는 3가지를 정리했었다. 반대 개념으로, 내가 개발할 때 원칙으로 삼는 3가지도 있어서 정리해본다.

1. Don't Repeat Yourself
비슷한 우리말로는 "삽질금지"정도 될 것 같다. 같은 작업을 반복하지 말라는 뜻이다. 
리팩토링에서도 3번째 반복할때 리팩토링 하라고 나온다. 좀 더 엄격하게 적용한다면 소스코드를 복사/붙여넣기를 하려고 할때 그러지 말고 Extract스러운 리팩토링을 진행한다.
프로그래밍의 영역뿐만 아니라, 문서 작성을 할 때도 같은 파일을 조금씩 고치면서 "_최종", "_진짜최종", "_진짜진짜마지막" 파일들을 만들고 있다면, 그만두고 문서파일의 버전 관리를 해야한다.
반복된 작업은 내가 아니고 컴퓨터가 하게끔 해야한다. 프로그래밍을 할 때는 공통로직은 공통모듈을 만들어서 호출하게 한다. 타이핑을 할 때는 멀티블록이나 컬럼블록 기능이 있는 에디터를 사용해서 한번의 타이핑으로 끝낸다. 내가 사용하는 키보드에서는 하드웨어 매크로 키가 있어서 자주 쓰는 문구를 M1,M2,M3,M4로 지정해서 사용한다. 

2. Make it work, Make it right, Make it fast
켄트벡이 유닉스웨이라는 글에서 한 이라고 한다. 직역하면 일단 돌아가게 만들고, 올바르게 만들고, 빠르게 동작하도록 만들라는 말이다. 받아들이는 사람에 따라서 "work", "right", "fast"에 대해서 해석의 차이가 있는 듯 하다. 그리고 3단계를 적용하는 시점도 단기적으로 볼 것인지, 장기적으로 볼 것인지에 따라서도 해석이 다르다.
나는 "work"를 "기능이 동작하는"으로 해석한다. 내부 구현 로직이나 소스코드의 아름다움따위는 생각하지 않고 사용자 관점에서 기능이 동작하게 만들면 "Make it work"한 것이다.
"right"는 유연한 아키텍쳐와 깨끗한 소스코드, 풍부한 테스트코드로 해석한다. 당장의 기능 동작을 물론이고, 앞으로의 기능동작을 위한 사전 작업이랄까. 앞으로 기능 변경시에 적은 수고로움으로 빠르게 고칠 수 있는 상태로 만든다면 "Make it right"한 것이다.
"fast"는 성능 튜닝으로 해석한다. 기능이 사용자가 원하는 수준의 성능으로 동작하지 못 할 때, 동작하도록 만드는 것이 "Make it fast"이다. 그래서 사용자가 원하는 수준이 낮다면 이 단계까지는 이르지 못 할 수도 있다.
이 3단계를 반복하면서 경험이 쌓이게 되면 "work"하게 만들면서 "right"을 생각하게 된다. "Make it work and right"이 되는 것이다. 예측 가능한 변경점을 고려하여 처음 부터 설계하는 것이다. "fast"를 미리 생각하면서 프로그래밍할 경우는 많지 않았지만, "not slow"하게 만드는 것은 미리 생각하는 정도이다.

3. Do not reinvent the wheel
바퀴는 이미 만들어져 있는 것이지 또 만들지 말라는 말이다. 수많은 라이브러리의 같은 기능을 스스로 다시 구현하지 말고 가져다 쓰자는 말이다. 나는 이를 조금은 철저히 지키는 편이다. 내가 구현한 소스보다 누군가가 구현하고 테스트한 소스가 더 견고하다는 믿음이 그 기반이다. 좋은 라이브러리는 좋은 구조로 설계되어 있고, 자연스럽게 SRP 원칙이 잘 지켜져 있는 편이므로 결과적으로 나의 소스코드도 좋은 설계로 이끄는 경향이 있다.
얼마전에 nodejs쪽에서 발생한 사태는 오히려 이 원칙의 부작용을 설명하는 듯 하지만, 사실 그 사건은 "바퀴"가 문제가 아니라 바퀴만드는 "사람"이 문제였던 사건이므로 부작용이라고 할 수는 없을 것 같다.

회사와 개인의 프로젝트를 진행하면서 프로그래밍과 관련된 판단을 내릴 때 위의 3가지 원칙을 기준으로 삼고 결정을 하도록 노력하는 편이다.

2016년 3월 5일 토요일

서버에서 동적으로 차트 이미지를 만들자

차트의 썸네일을 클릭해서 선택한 차트를 크게 보여주는 페이지가 있다. 차트의 유형이 고정적으로 정해져 있어서 썸네일 이미지를 특정한 경우의 차트 모양을 캡쳐하여 이미지로 소스저장소에 저장해서 사용하고 있었다. 이 썸네일 이미지를 실제 차트로 보여질 데이터의 모양으로 보여달라는 기능을 구현해야 해서 찾아본 내용을 공유한다.

1.
썸네일이미지를 만들지 말고 실제 차트를 썸네일 크기로 만드는 방법도 검토했다. 그런데 작은 공간에 수많은 div들과 svg들이 들어가게 되는 것이 좀 꺼림칙했다. 그리고 페이지가 스크롤되거나 윈도우 사이즈가 변경될 때 썸네일차트들이 잘 렌더링될지도 걱정이 되었다.

2.
썸네일이미지를 동적으로 만드는 방법도 검토했다. "generate chart image response java"와 같은 검색어로 검색도 해보고, java chart library를 찾아보기도 했다. 이 방면에서 가장 유명한 라이브러리는 jfreechart인 것 같다. 흥미로운 것은 FAQ에서 다른 차트 라이브러리를 소개하고 있다.

3.
xchart가 적당한 수준의  기능이 구현된 것 같아서선택하여 구현했다. 기본적으로 swing을 이용해서 차트를 띄울 수 있어서 테스트해가면서 차트의 모양을 확인하기가 편했다. 그리고 완성차트의 이미지 바이트배열을 제공하므로 http response로 보낼수도 있었다.

double[] yData = new double[] { 2.0, 1.0, 1.7, 1.0, 1.7, 1.0, 1.7, 1.0, 1.7, 1.0, 1.7, 1.0, 1.7, 1.0, 1.7, 1.0, 1.7, 1.0, 1.7 };

// Create Chart
Chart_XY chart = new Chart_XY(85 * 2, 44 * 2);
chart.getStyler().setChartBackgroundColor(Color.WHITE);
chart.getStyler().setChartTitleVisible(false);
chart.getStyler().setChartTitleBoxVisible(false);
chart.getStyler().setPlotGridLinesVisible(false);
chart.getStyler().setLegendVisible(false);
chart.getStyler().setAxisTicksVisible(false);
chart.getStyler().setAxisTitlesVisible(false);

Series_XY series = chart.addSeries(" ", null, yData);
series.setLineColor(Color.BLUE);
series.setMarker(SeriesMarkers.NONE);
series.setLineStyle(SeriesLines.SOLID);

byte[] img = BitmapEncoder.getBitmapBytes(chart, BitmapFormat.PNG);

4. 
리턴이 이미지파일이므로 response에 적절한 헤더와 함께 담아야 한다. 그런데 Spring의 RequestMapping의 produces 속성에 이미지파일형식만 선언해서, 간단히 byte[]만 리턴하면 되게 할 수 도 있다.

@Controller
public class ImageController {
@RequestMapping(value="/", method = RequestMethod.GET, produces = MediaType.IMAGE_PNG_VALUE)
public @ResponseBody byte[] index() throws IOException {
return BitmapEncoder.getBitmapBytes(chart, BitmapFormat.PNG);
}
}

5.
이렇게 만들면 간단하게 <img> 태그의 주소만 변경해서 썸네일스러운 이미지를 동적으로 만들 수 있다. 실제 파일이 생성되는 것이 아니므로 스토리지에 대한 부담도 없다. 대략 아래와 같이 썸네일 스러운 이미지를 만들 수 있다.


2016년 2월 15일 월요일

Name for argument type [java.lang.String] not available 해결하기

Spring의 Controller를 사용할 때 아래와 같은 IllegalArgumentException이 발생하여 해결한 내용을 남긴다.


1.
어느날 부터 로컬 tomcat 서버에서는 에러가 안나는데 개발서버에서는 아래와 같은 에러가 발생하기 시작했다.

java.lang.IllegalArgumentException: Name for argument type [java.lang.String] not available, and parameter name information not found in class file either.
        at org.springframework.util.Assert.notNull(Assert.java:112) ~[spring-core-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.updateNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:141) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.getNamedValueInfo(AbstractNamedValueMethodArgumentResolver.java:120) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:87) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
        at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:79) ~[spring-web-4.0.3.RELEASE.jar:4.0.3.RELEASE]
... 이하 생략

2.
"Name for argument type [java.lang.String] not available"으로 검색해보니 3번째로 검색되는 블로그에 문제의 원인과 해결책이 제시되어 있었다. Controller 메서드의 파라메터를 HttpRequest의 파라메터로 연결해서 사용하려면 @ReqParam 어노테이션을 주고 value도 꼭 주어야 한다는 내용이다. 그래야 javac가 -g:none 옵션으로 컴파일이 되어도 파라메터 매핑이 에러없이 되다는 것이다.

이런 코드가
public String viewCliRrpt(HttpServletRequest request, String prjtId, String samplId) {

이렇게 바뀌어야 한다.
public String viewCliRrpt(HttpServletRequest request
, @RequestParam(value = "prjtId") String prjtId
, @RequestParam(value = "samplId") String samplId) {

3.
어느날 갑자기 이런 에러가 발생한 이유는, 바로 그 어느날에 빌드스크립트를 정리하면서 ant의 javac 태스크의 debug="true" 옵션을 제거했기 때문이었다. 그날 부터 서버에 배포되는 war는 -g:none 옵션으로 컴파일된 class파일들이 포함되었고, RequestParam 어노테이션이 없었기 때문에 Exception이 발생했었다.

4.
그런데 개발서버에서만 에러가 발생하고 로컬 서버에서는 정상이었던 이유는 이클립스가 기본으로 debug 옵션을 포함하여 자바를 컴파일하기 때문이다. 여기에서 그 옵션이 무엇인지를 찾았고, 해당 옵션의 설명은 이클립스 헬프페이지에서 확인이 가능하다. 하지만 -g:none 옵션과 연결되어 설명되어 있지는 않다.

5.
결론은 빌드스크립트에서 debug="true" 옵션을 추가하는 것으로 마무리하였다. 개발서버에서도 debug log를 보는 경우가 허다하고, 실제 서비스되는 war라 하더라도 유지보수차원에서 로그를 확인해야 하는 경우가 생길 것이라고 판단했기 때문이다.


2016년 2월 12일 금요일

Spring의 SimpleMappingExceptionResolver에서 로그 남기기

회사에서 개발중인 시스템에서 Spring의 SimpleMappingExceptionResolver와 관련된 부분을 수정했기에 블로그로 남긴다.

1.
Web프레임웍으로 Anyframe을 사용중인데, *.do 요청을 AJAX로 보내면 Exception 발생시에도 stacktrace를 로그에 남기지 않아서 불편한 점이 있었다. 그동안은 *.do 요청이 많지 않아서 불편함이 드러나지 않았는데, 최근에 *.do 요청으로 중요 로직을 연결하여 사용하다보니 불편함이 느껴져서 고치게 되었다.

2.
Exception유형에 따른 View를 결정할때 Anyframe의 AjaxServiceExceptionHandler를 사용하고 있다. AjaxServiceExceptionHandler는 Spring의 SimpleMappingExceptionResolver를 상속받은 클래스이고, HttpRequestHeader에 X-Requested-With 헤더값이 XMLHttpRequest일때는 super.resolveException를 호출하지 않도록 구현되어 있다.

3.
좀 더 찾아보니 SimpleMappingExceptionResolver는 AbstractHandlerExceptionResolver를 상속받았기 때문에 warnLogCategory를 사용하는 방법이 있었다. WarnLogCategory에 logger의 이름을 넣어주면 해당 logger로 warn 레벨의 로그를 남겨주게 되어 있다.

spring-webmvc-4.0.3.RELEASE.jar:AbstractHandlerExceptionResolver.class의 일부

            public void setWarnLogCategory(String loggerName)
            {
/* 108*/        warnLogger = LogFactory.getLog(loggerName);
            }

            protected void logException(Exception ex, HttpServletRequest request)
            {
/* 186*/        if(warnLogger != null && warnLogger.isWarnEnabled())
/* 187*/            warnLogger.warn(buildLogMessage(ex, request), ex);
            }

그런데 2번에서 언급한 것 처럼 super를 호출하지 않기 때문에 빈생성시 warnLogCategory 프러퍼티를 넣어주어도 로그가 남지 않고 있었다.

4.
AjaxServiceExceptionHandler가 super.resolveException를 항상 호출하도록 수정할 수 없으니, AbstractHandlerExceptionResolver의 warnLogCategory를 활용하는 방법으로 해결하는 것은 포기하고, 검색하다 발견한 블로그처럼 AjaxServiceExceptionHandler를 상속받은 LogAjaxServiceExceptionHandler를 새로 만들어서 사용하기로 했다.

public class LogAjaxServiceExceptionHandler extends AjaxServiceExceptionHandler {

private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
logger.error(ex.getMessage(), ex);
return super.resolveException(request, response, handler, ex);
}

}

5.
logger를 새로 생성하지 않고 AbstractHandlerExceptionResolver가 가지고 있는 logger를 사용해도 무방하다. 다만, 나중에 logger의 이름을 변경하게 될 것 같아서 별도로 생성해 놨다.

6.
검색하다 보니 stacktrace를 로그로 남기는 방법을 직접 구현한 블로그도 있었는데, logger에 2번째 파라미터로 Exception을 넘겨주면 알아서 stacktrace도 찍어주기 때문에 불필요했다.


2016년 1월 31일 일요일

소프트웨어 개발에 있어서 믿지 못할 3가지 표현

소프트웨어 개발을 하다보니 자연스럽게 절대로 믿지 않게된 3가지

1. 절대 안바껴요
모든 것은 바뀔 수 있다. 왜냐하면 오늘날의 현실 세계가 그만큼 빠르게 변하고 있고, 그에 따른 요구사항도 빠르게 변하기 때문이다. 특히 소프트웨어 세계에서는 다른 영역에 비해서 그 빈도가 더 잦고, 그 범위가 더 넓다. 왜냐하면 흔히 사람들은 소프트웨어는 하드웨어와는 다르게 간단한 작업만으로도 쉽게 바꿀 수 있다고 생각하기 때문이다. 
프로그램을 설계하거나 개발할 때, 특정 요건이 절대 바뀌지 않는다는 가정은 하지 말자. 머지 않아 그 가정은 틀어질 수 있고, 그 가정에 기반한 설계와 소스코드는 수정하기 어려운 지경에 이르렀을 가능성도 높다. 변경될 부분을 고려하여 OCP스럽게 설계해야 할 것이다.

2. 한방에 다 되요
현실 세계가 매우 복잡해졌기 때문에, 그런 현실세계를 투영한 소프트웨어도 매우 복잡한 구성을 가지고 있는 경우가 대부분이다. 구성뿐만 아니라 절차마저도 복잡해졌다. 의미있는 결과물을 만들어 내기 위해서는 여러 단계의 작업을 거쳐야 한다. 일반적으로 데이터를 추출하고, 추출한 데이터들을 연결하고, 의미있는 계산을 수행한 다음, 결과를 보기 좋게 표시하는 작업이 이어지게 된다.
그런데 이런 일련의 작업을 한방에 다 되게끔 만드려는 시도가 있다. "한방쿼리"와 같은 단어가 대표적이다. 이는 중간 단계의 한 작업의 일부요소가 변경되는 경우에도 그 여파가 전체로 이어질 수 있는 위험한 설계이다. SRP를 유념하여, 프로그램을 나누어 가면서 작업할 필요가 있다.

3. 테스트 다했어요
사람은 불완전하기 때문에 모든 경우를 다 고려할 수 없다. 그런데 테스트를 다 했다고 표현하는 것은 상당히 오만한 자만심을 가진 경우이다. 1+1=2 수준의 프로그램을 만드는 경우조차 불완전한입력, 문자입력, null입력, overflow 등의 다양한 경우에 대해 신경을 써야 하고, 이보다 복잡한 프로그램은 그 경우가 더 많아질 것이다. 그만큼 요즘의 소프트웨어는 단순하게 테스트하기에는 복잡도와 난이도가 너무 높다.
아무리 테스트케이스를 다양하게 만든다고 하여도, 테스트케이스가 부족할 수 있다는 것을 언제나 잊지 말아야 한다. 또한 아직 발견되지 않은 결함이 존재한다는 것을 인정하고 부끄러워하지 말아야 할 것이다. 대신 그 결함을 발견할 수 있는 테스트케이스를 어떻게 만들 것인지를 고민해야 할 것이다.

누군가 위의 3가지 중에 한가지라도 말한다면, 그의 말을 믿지 말아라. 대신 언제든지 바뀔수 있는 요구사항과, 생각보다 복잡하고 어려운 현실세계와, 생각하지 못한 경우의 테스트가 있음을 잊지 말고 준비해라.

2016년 1월 29일 금요일

파일이름 변경 끝판왕 DarkNamer

자주 쓰는 작은 프로그램에서 두번째는 DarkNamer이다. 파일이름을 일괄로 변경하는 프로그램인데, 작지만 강력한 기능들을 가지고 있어서 자주 사용한다.

1. 무설치
별다른 설치 과정없이 exe 실행파일 하나로 바로 실행이 가능하다.

2. 일괄 파일 변경
문자열바꾸기, 앞이름붙이기, 뒷이름붙이기, 이름지우기, 위치지우기, 묶인곳지우기, 숫자만남기기, 자리수맞추기, 번호붙이기, 경로명앞에, 경로명뒤에, 경로통일, 확장자삭제, 확장자추가, 확장자변경이 가능하다.

3. 일괄 파일 추가
폴더를 드래그앤드롭하면 폴더를 프로그램에 넣고 이름을 바꿀 수도 있고, 폴더안의 파일들을 프로그램에 넣을 수 도 있다.

4. 이름이나 경로 복사
프로그램에 넣은 파일의 이름이나 경로를 복사해서 쓸 수 있다. 탐색기에서 여러가지 파일 이름을 텍스트로 뽑아내는게 의외로 간단한 일이 아닌데, 이 프로그램으로 간단하게 할 수 있다.

파일명의 형식을 맞추거나 할 때 이보다 편한 프로그램은 없었다.

2016년 1월 25일 월요일

spring file uplode size 다양하게설정하기

2011년 10월에 네이버블로그에 올렸던 내용을 이곳으로 모아서 게시한다. 최근에 업무에서도 요긴하게 사용했다.

1. 요구사항
첨부파일의 크기를 업무별로 다르게 설정해야 한다. 예를 들면 일반게시판에는 20메가연계게시판에는 10메가

2. 제약사항
- Spring에서 일반적으로 사용하는 CommonsMultipartResolver에서는 maxUploadSize를 한가지만 지정할 수 있다.
제한하고자 하는 크기는 자바 클래스에 상수로 선언하여 사용해야 한다.
제한된 크기를 초과하는 경우컨트롤러 레이어에서 에러메시지나 에러페이지 등을 제어해야한다. (, Spring에서 제공하는 ExceptionResolver 등을 사용하지 않는다)

3. 해결방법
CommonsMultipartResolver를 상속하여 resolveMultipart() 메서드를 오버라이딩하여 요청 URL에 따라서 각각의 업무클래스에 정의된 maxFileSize 상수로 maxUploadSize를 설정한다.
- super.resolveMultipart(request) 메서드 처리중에 발생한 MaxUploadSizeExceededException을 잡아서 Exception 객체를 attribute로 추가한 request를 리턴하여 컨트롤러 레이어까지 처리흐름이 이어지도록 한다.
public class CustomMultipartResolver extends CommonsMultipartResolver {

        @Override
        public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {

               // 요청URL path에 따라서 결정된 세부업무별로 maxUploadSize 설정
               String url = request.getRequestURI();
               int maxFileSizeMB = 0;
              
               // 게시판
               if( url.startsWith("/au/signbbs/") ) {
                       maxFileSizeMB = AuBoardFileConstant.getMaxFileSize();
               }
               // 연계게시판
               else if( url.startsWith("/au/connect/") ) {
                       maxFileSizeMB = AuBoardFileConstant.getMaxFileSize();
               }
               else {
                       throw new MultipartException("예상치 못한 url에서 파일업로드 시도함. AuMultipartResolver.resolveMultipart()에 추가 필요");
               }
              
               setMaxUploadSize(maxFileSizeMB * 1024 * 1024);
               setMaxInMemorySize(maxFileSizeMB * 1024 * 1024);
              
               try {
                       return super.resolveMultipart(request);
               catch (MaxUploadSizeExceededException e) {
                       // 파일사이즈초과되어도 에러메시지를 위해서 request 리턴하여 컨트롤러 메서드 호출되도록 한다.
                       request.setAttribute("exception", e);
                       return new DefaultMultipartHttpServletRequest(request, new HashMap(), new HashMap());
               }
        }
}

새로 만든 MultipartResolver 클래스를 Spring 설정 xml에 CommonsMultipartResolver 대신 등록한다.
<!-- 세부업무별로 파일업로드 사이즈 제한 -->
<bean id="multipartResolver" class="common.CustomMultipartResolver" />

컨트롤러 레이어에서는 request에서 가져온 MultipartFile 객체가 null인 경우를 확인하여 예외처리를 해준다. reqeuest에 MaxUploadSizeExceededException 객체가 있다면 적당한 메시지를 생성하여 UI로 에러메시지를 보내도록 한다.
// request에서 파일을 찾아서 MultipartFile 객체로 생성한다.
MultipartFile file = multipartRequest.getFile("attachfile");

// 최대업로드사이즈 초과 체크
if( file == null ) {
    Exception e = (Exception)multipartRequest.getAttribute("exception");
    if( e instanceof MaxUploadSizeExceededException ) {
       long maxFileSizeMB = (((MaxUploadSizeExceededException)e).getMaxUploadSize() / 1024 / 1024);
       errorMsg = "첨부파일 사이즈는 " + maxFileSizeMB + "M 를 초과할 수 없습니다.";
    }
    Log.error(e, "파일생성 중 예외발생");
    return "/error";
}

4. 활용방안
로그인한 회원의 권한에 따라서 크기를 다르게 설정해야 하는 요건의 경우에도 활용가능하다이를 테면 관리자 권한이나 수퍼유저의 경우 일반유저보다 큰 파일을 업로드 할 수 있다.

5. 개선할 점
요청 URL에 따라서 다른 상수클래스를 사용하는 부분에서 요청 URL이 하드코딩 되어있다그런데 명명규칙이 표준화되어 있어서 요청 URL과 상수클래스간에 일정한 규칙이 존재한다면 하드코딩된 부분을 제거할 수 있고이후 추가되는 업무에 따른 URL에 대해서도 소스코드 수정없이 적용이 가능할 것이다.

6. 참고