2016년 10월 2일 일요일

JSON 에 대한 간략한 생각

제목이 좀 애매한데...그냥 개인적으로JSON 데이터 구조를 설계하는데 조심해야 할 한가지(어떻게 보면 프로그래밍에서의 설계 방식에 대한 기본 중 하나가 아닐까 하지만)에 대한 잡설입니다.


지금은 아주 약간 시간이 지났지만, JSON 에서 데이터를 만드는 부분에 있어 어떤 사람과 논쟁이 발생한 적이 있었습니다. 저는 중간에 그냥 대화가 힘들 것을 직감하고 대화를 끊었지만, 그 이후에도 다른 사람과 계속 논쟁을 이어 가더군요. 좋은 의미의 논쟁이었으면 좋았겠지만 좀 어거지 수준의 대화도 일부 있어서...그냥 눈팅하면서도 좀 짜증이 나더군요.

논쟁이 일어난 계기는 질문자가 아래와 같은 늬앙스의 질문을 했기 때문입니다.

JSON 으로 웹의 그리드에 표시할 데이터 목록을 보내려고 하는데, 생성할 때의 키 순서와 달리 실제 String 으로 변환되어 나온 JSON 에서는 생성한 순서처럼 내용이 생성되어 있지 않다. 방법이 없을까?

그리고 질문자가 올린 것을 보니, JSON Object 에 key 로 "A", "B", "C" 와 같이 순서를 위해 특정값을 넣고, 뒤에 해당 줄에 표시할 내용을 value 로 넣었더군요.

처음에 질문을 봤을 때 그냥 String 으로 직접 만들면 순서가 흐트러지지 않는다고 하려다가...이건 좀 아니다 싶어서 좀 더 원론적인 답변을 했습니다.

JSON Array 는 순서가 유지됩니다. 말씀하신 내용의 정보를 보낼 때에는 그냥 JSON Array 로 데이터를 보내시는게 맞습니다. 그렇게 내용을 변경해보세요. 받는 쪽에서도 순서가 뒤바뀌지 않기 때문에 이렇게 구조를 바꾸시는게 좋습니다.

그런데, 뒤에 어떤 사람이 Google 의 Gson 을 이용해서 입력된 순서...가 아닌, key 의 정렬을 유지한 채(key 정렬 순이 원 질문자의 의도가 맞긴 합니다)로 String 을 만들어내는 이야기 했습니다. 물론 받는 쪽은 Javascript 였으니 Gson 은 없겠지만 말입니다.

제가 이 사람의 댓글에 문제를 제기하면서 사태가 커졌습니다.

말씀하신 방법대로 정렬을 해서 보내는 방법도 좋지만, 그런 식으로 String 을 생성해도 받는 쪽에서 순서를 유지하는 방법을 쓰지 않으면 다시 순서가 흐트러지기 때문에 그런 방법은 좋지 않습니다.

이런 늬앙스였습니다.

그 사람은 질문 내용 그 자체에 대한 답변에만 충실하면 되지 않느냐, 이렇게 하면 정렬된 문자열이 나오는데 무슨 문제냐...등...반발을 심하게 했죠.

그래서, 여기에 대해서 이야기를 하려고 합니다.



먼저 JSON 의 특성 중 일부를 짚고 넘어가야 겠네요.

JSON 은 Obejct 라고 불리는 구조로 이루어져야 합니다. Object 는 key 와 value 로 이루어져 있는 단순한 집합이고, 각 key/value 쌍은 콤마로 구분되어 여러 개가 저장될 수 있습니다. 그리고 "key 는 순서가 유지되지 않습니다".
이 Object 는 String 으로 된 key 와 여러 형태의 value 을 가질 수 있는데, 숫자형(double), 문자형(String), 참/거짓(Boolean) 뿐만 아니라 null 이나 JSON Object, JSON Array 같은 구조도 가질 수 있습니다. 여기서 JSON Array 는 흔히 말하는 배열과 같은 형태 같지만, 사실 각 값들은 여러 자료형이 섞여서 들어갈 수 있기 때문에 Java 의 Object[] 나 List 에 가깝다고 이해하는게 맞습니다. List<Object> 라고 할까요.
어쨌든 표준 JSON 문장은 이런 특징 때문에 Java 라이브러리에서는 통상적으로 JSON Object 는 HashMap 으로, JSON Array 는 ArrayList 로 대응되어 구현되고 JSON String 을 Java Bean 개체와 마샬링/언마샬링 할 때에 자동으로 HashMap 과 ArrayList 로 변환되어 저장되는 경우가 대부분입니다(대부분이 이유가 JSON 을 다루는 라이브러리는 하나가 아니니까요).

그리고, JSON 은 Javascript 에서 이용되기 위해 처음 고안된 방법이다 보니 Javascript 에서 아무런 추가 라이브러리 없이 바로 처리가 가능합니다. 그리고 Javascript 에서는 key 순서를 애초에 보장하지 않기 때문에 String 으로 정렬된 형태로 받는다고 해도 결국 순서대로 Object 의 key 들을 가져오지 못합니다. 정렬의 개념도 내부적으로 없구요. 그렇기 때문에 JSON Object 의 키는 언제든 순서에 상관없이 빠르게 처리할 수 있는 형태대로 내부에서 저장하고 있다고 이해한 상태에서 개발해야지, 이를 순서대로 뽑아낼 것이라고 생각하고 프로그래밍 하는 것은 의미가 없습니다.

문제는, 질문자의 경우 "A", "B", "C" 와 같이 순서대로 key 을 정렬하여 String 을 저장한 뒤 받는 쪽에서 역시나 순서대로 하나씩 꺼낼 생각을 하고 있겠지만...이건 양 쪽 모두 한 사람이 프로그래밍을 하거나 문서를 잘 만들어서 이후에도 받는 쪽에서 변함없이 이렇게 처리하도록 프로그램을 유지해야 가능한 방법입니다. 예전 제 글(http://zepinos.blogspot.kr/2016/09/mybatisibatis-oralce-sequence.html)에서도 말한 적이 있지만 프로그램을 영원히 혼자서 만드는 경우가 아니라면 가급적 표준을 지키고, 문제가 발생할 가능성이 최대한 적은 쪽으로 프로그래밍을 하는 습관을 가지는게 중요합니다. 그런데 굳이 표준을 지키면서 개발할 수 있는걸 이렇게 엉뚱하게 프로그래밍을 하려고 하는 것도 그렇고(초보니까 이럴 수도 있죠), 그걸 알면서도 질문에 대한 답만 해야한다고 주장하는 것도...좀 질 나빠 보이는건 어쩔 수 없더군요.

그리고, 보통 이런 리스트를 DB 에서 가져올 때 List 개체에 담아서 가져오기 때문에 이걸 그냥 Map.put("키", List) 와 같이 담고 JSON String 으로만 변환하면 자동으로 "키" 에 JSON Array 형태로 JSON Object 들이 순서대로 담겨져 들어가있을 것입니다. 너무나 쉽고 for 을 써가면서 "A", "B", "C" 와 같이 키를 만들 필요도 없고(당연히 사용자가 매우 많아지면 서버 측의 속도 저하가 필연적으로 따라옵니다), List 의 개수가 가변적일 경우 대응하기도 쉽습니다(사실상 코드를 고칠 필요가 없습니다). 받는 쪽에서도 어떤 라이브러리를 이용하든 Array 형태의 데이터는 순서가 바뀌지도 않구요.

실제로 많은 Javascript grid 라이브러리들은 데이터를 JSON 으로 동적으로 가져올 때 JSON Array 형태로 값을 요구합니다. 그렇기 때문에, 이러한 방법은 매우 보편적인 방법이라고 할 수 있습니다. 만약, 어떤 라이브러리에서 리스트 형태의 데이터를 binding 할 때 Array 형태가 아닌 key 의 정렬 순서대로 값을 Object 에서 가져오도록 되어 있을 때...과연 이 라이브러리가 잘 짜여진 프로그램이라고 생각할 수 있을까요? 저라면...이런 라이브러리는 안쓸 것 같은데요...



특히 JSON 은 주석을 달 수 없기 때문에 데이터만 가지고 그 정보의 내용을 유추해야 하는 경우도 많고, 문서가 없을 경우 자신만의 독특한 구조로 데이터를 구성한다면 협업하는 입장에서 피해야할 개발자로 낙인 찍히기 쉽습니다. 이 점 유념했으면 좋겠습니다.
그런 의미에서, YAML 을 주목하고 있습니다. Spring Boot 의 기본 설정 파일 형식 중 하나로 이용되고 있는데, parsing 속도가 아직은 매우 느리다고 합니다만, 주석도 담을 수 있고, 계층 구조와 List 등도 모두 지원하기 때문에 JSON 을 대체하는 좋은 방법이 될 수 있을꺼란 기대가 있습니다.




마지막으로, 이 때의 일을 소회하자면...끝까지 이 내용 가지고 질질 끌면서 어거지 쓰면서 새 글까지 써대면서 질문자의 글에 대한 내용만 답변해야 하는거 아니냐고 동조자를 모으는 모습을 보이던데...중간에 빨리 발 빼길 잘했다는 생각이 들었습니다. 저와는 맞지 않는 사람이란 걸 느꼈습니다. 저와의 이 일이 있기 전에 더 큰 일이 있고, 상대방과 코드까지 공개하면서 논쟁을 벌였는데...상대방이 반말을 막 하는 모습 때문에 상대방을 안좋게 보았는데...그 때도 말꼬리 잡는 것 때문에 상대방이 좀 짜증은 났겠다는 생각이 들었습니다. 말꼬리라고 하는 것도...숲은 안보고 나무만 집요하게 들이밀면서 상대의 말실수를 가지고 끝까지 공격하는 모습은 좋지 않게 보였는데...저도 당하고 보니 다시는 상대하고 싶지 않은 사람이더군요. 그래서 내린 결론이..."절대 대응하지 않는다", "없는 사람으로 대응한다" 입니다. 이 글도 여기 쓰는 목적이고...

혹시 그 당사자가 이 글 봤다면 개인 블로그에 혼자 떠드시길 바랍니다. 해당 커뮤니티에선 제 이미지 관리(?) 차원 뿐만 아니라 공공성이 있는 곳에서 분탕치기 싫어서 조용히 있었던 거지, 당신의 내용에 동의하거나 납득하기에 그만하는게 아니라는거...알길 바랍니다. 벼로써 키는 충분히 자라기는 했는데 아직 고개를 숙이는 단계까지 가질 않은 것 같아서 이해는 하긴 합니다만, 나중에 이불킥 할 일 늘리지 마세요.

2016년 9월 17일 토요일

MyBatis(iBatis) 에서 Oralce 등의 Sequence 을 쓸 때 을 권장하는 이유

이 글을 쓰는 시점에서 약간 지난(2~3주 쯤 된 것 같다) 시점에서 한 커뮤니티에 대략 아래와 같은 글이 올라왔다.

Oracle, MyBatis 을 쓰는데, 아래와 같이 insert 을 할 때 문제가 발생합니다.
insert into 테이블 (컬럼1, 컬럼2, ...) values (sequence.nextval, 값2, ...)
insert into 테이블 (컬럼1, 컬럼2, ...) values (sequence.currval, 값2, ...)

위와 같이 작성했는데 문제가 된다면 보통은 트랜젝션을 선언해주지 않아서 다른 세션으로 insert 가 실행되었을 가능성이 크다.

그런데, 애들 때문에 시달리는 와중에 모바일로 내용을 보고(애들 때문에 컴을 켤 엄두도 안난다)...세션 생각은 덜 한 상태에서, 조금 틀린 내용을 포함한 답을 해줬다. 질문자가 이미 트랜젝션을 해주니 문제가 해결되었다고 댓글을 달았는데, 저렇게 하지 말라고 오지랍을 떤 것이다.

물론 밤 늦게 글이 달렸었고, 그 사이 다행이도 다른 댓글이 달리지 않았고, 출근길에 전철 안에서 세션 단위로 값을 제공하기 때문에 같은 세션에선 거의 발생하지 않는다는...댓글 수정이 아주 불편한 모바일을 감안해서 약간의 변명스러운 댓글을 달았다. 물론 세션 문제를 제외하고, 저런 식의 코딩이 위험하다는 건 변함이 없었지만...

그런데, 모든 댓글에 -1 이 달렸다. 그 커뮤니티에서는 질문/답변 게시판에서 댓글에 1 점 혹은 -1 점을 줄 수 있는데, -1 점이 달린 것이었다. 그 커뮤니티가 나름 프로그래밍 쪽에서는 많은 회원을 가진 곳이긴 하지만 답변 내용이 틀렸다고 저렇게 -1 점을 막 주는 사이트는 아닌데...하면서 의문을 가졌지만 곧 의문이 풀렸다.
다른 사람들끼리 약간의 논쟁(이라 쓰고 싸움이라 읽을 수도 있다)이 벌어지면서 누가 점수를 그렇게 줬는지 알아버렸기 때문이었다. 다만, 그 사람이...내가 다시는 그 사람의 글이나 댓글 등에 반응을 일체 하지 않겠다고 혼자서 결심한 사람이었기에 문제가 되었지만...그래서 이 글을 쓰고 있는 것이기도 하다.

심지어 그 댓글들 때문에 상대에게 별 의미도 없는 -1 점 주는 행동에, 나에게 -1 점을 막 주던 그 사람은 자기에게 -1 점 줬다고 다른 사람들을 비난하기 시작했고, 그 글에서 단 한 번도 나오지 않는 MS SQL Server 에서의 문제까지 가지고 나와서 시덥잖은 변명까지 하기 시작했다.



그래서 이 글을 적는다.

먼저, 글의 내용처럼 오라클에 한해서만 예제를 테스트했다. 그리고, 이런 경우는 극히 발생하지 않는다는 것도 덧붙인다. 다만, 난 겪었었던 일이었고...그게 경력이 가지는 무서움이라는 것이다. 일하던 산업군이 다양했던 것도 한 몫 했을 것이다. 특히 금융권 같이...한 번 일하기 시작하면 그 산업군을 벗어나는 경우가 드물고, 코드 자체를 매우 보수적으로 개발하고 운영 역시 그러한 곳에서는 이런 일은 발생하지 않을꺼라 본다. 하지만, 저 질문을 한 사람에게 저걸 쓰는게 뭐 어때서...라고 추천하는 순간 이런 황당한(하지만, 사실 예상할 수도 있다) 경우를 겪을 수 있는, 일종의 버그를 만들 수 있는 코드를 작성하는 개발자 하나를 탄생하게 만드는 것이다. 물론 공개된 곳의 글이기 때문에 한 명에서 그치지 않을 수도 있고(그래서 글 내용만 가지고 기계적인 답변을 달지 말라고 그러는 것이다)...

github 에 전체 코드를 올리면 좋겠지만, 뭐 좋은 코드라고 올리겠는가...그냥 대충만 적는다.

먼저 DB 를 만든다.


create table log (seq int PRIMARY KEY , group_seq int, message varchar2(1000));
create sequence seq_log_seq;
create sequence seq_log_group_seq;

아래와 같은 두 개의 쿼리를 만든다.


<mapper namespace="com.zepinos.mapper.Log1Mapper">
    <insert id="insertLog1" parameterType="Log">
        insert into log (seq, group_seq, message) values (seq_log_seq.nextval, seq_log_group_seq.nextval, #{message})
    </insert>
</mapper>

<mapper namespace="com.zepinos.mapper.Log2Mapper">
    <insert id="insertLog2" parameterType="Log">
        insert into log (seq, group_seq, message) values (seq_log_seq.nextval, seq_log_group_seq.currval, #{message})
    </insert>
</mapper>

그리고 한 개의 서비스를 만들어서 두 쿼리를 실행하도록 한다.


@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private Log1Mapper log1Mapper;
    @Autowired
    private Log2Mapper log2Mapper;

    @Transactional
    public void test() {

        Log log = new Log();
        log.setMessage("Test1");

        int result = log1Mapper.insertLog1(log);

        if (result > 0) {

            log.setMessage("Test2");

            log2Mapper.insertLog2(log);

        }

    }

}

큰 문제가 없다면, 아래와 같이 출력될 것이다.

1    1    Test1
2    1    Test2

너무 당연하다. 두번째 컬럼인 group_seq 는 일종의 레코드의 그룹을 알려주기 위해서 하나의 트랜젝션에 같은 값을 가지게 하기 위해서 저렇게 만드는 경우가 있다. 실제 내가 지금 만들고 있는 게임 서버의 로그도 저런 식으로 하나의 트랜젝션(사용자의 한 번의 작업)에 그룹을 구별하기 쉽게 하고, 순서도 별도 컬럼에서 넣기도 한다. 즉, 저런 코딩이 없는 경우가 아니란 것이다. 그리고 원 질문자의 말처럼 currval 은 잘 작동한다.

하지만, 내가 겪은 일은...아쉽게도 저런 식의 코드를 짠 사람이 뭔가 실수해서 발생한 것이 아니었다. 심지어, 문제를 일으킨 코드를 작성한 사람이 잘못한 것도 아니었다. 오히려 괜찮은 실력에, 개발자로써의 자질(게으름!)도 가지고 있었기에 발생했던 것이다.

이걸 쉽게 구현하기 위해서 다음과 같은 코드를 만들었다.

먼저, 그냥 쉽게 하기 위해서 위에서 만든 테이블에 값을 넣는 동일한 쿼리를 만들었다. 물론 내가 겪었던 사이트는 이것조차 만들지 않았다. 빨리 만드느라 이렇게 된 것이니 양해를...


<mapper namespace="com.zepinos.aspect.TimeMapper">
    <insert id="checkTime" parameterType="String">
        insert into log (seq, group_seq, message) values (seq_log_seq.nextval, seq_log_group_seq.nextval, #{message})
    </insert>
</mapper>

그리고 아래와 같은 클래스를 생성했다. 두둥...


@Aspect
public class InsertRunTimeChecker {

    @Autowired
    private TimeMapper timeMapper;

    @Around("execution(* com.zepinos.mapper.*Mapper.insert*(..))")
    public Object insertAround(final ProceedingJoinPoint joinPoint) throws Throwable {

        long time = System.nanoTime();

        Object proceed = joinPoint.proceed();

        timeMapper.checkTime("Spend Time : " + (System.nanoTime() - time));

        return proceed;

    }

}

뭐...금방 눈치챈 분들도 계시겠지만...

이 코드는 위에서 개발한 두 개의 insert 의 동작 시간을 로그에 추가적으로 남기기 위한 코드다. Aspect 을 이용해서 말이다. 다만, 그 때 당시 이걸 추가했던 사람이 insert 되었던 로그와 같은 그룹으로 넣지 않고 별도로 넣은건 나중에 일괄적으로 삭제를 하려고 했던건지 아니면 혼동을 줄이기 위해서인지는 모르겠다. 어쨌든 두 개의 시퀀스의 용도를 제대로 이해하고 있다면 위와 같은 코드를 만드는게 문제가 될 리 없다.
그리고, 보통은 insert 가 여러번 연달아서 발생하는 경우가 드문 사이트였고...여러 이유에서 저런 코드가 바로 문제시 되지 않았던 것도 서비스 후에 이 문제가 발견된 이유이기도 했다.

당연히 저렇게 해서 값이 입력되면 아래와 같은 결과 형태로 테이블에 저장된다.

1    1    Test1
2    2    Spend Time : 149358240
3    2    Test2
4    3    Spend Time : 1457061

자...이렇게 로그가 남게 되면 처음에 의도한 바와 달리 데이터가 이상하게 그룹화 되어서, 저 데이터를 봐야 하는 운영팀이나 사업팀 등은 맨붕에 빠지게 된다. 그나마 실제 상황에서 저런 일이 로그 테이블에서 발생했으니 망정이었지...

이게 왜 짜증나는 경우냐면, 그 때 당시 내가 가이드 코드들을 제공했고, 분명히 insert 시에 <selectKey> 을 이용하는 예제만 존재했기 때문이었다. 물론 나 역시 저런 식의 문제가 생길꺼라곤 생각을 못했지만...흔히 인터넷에 위 질문과 달리 <selectKey> 만 쓰는 이유도, 어떤 선각자(?)가 이런 문제를 예감하고 그랬을런지도 모른다.

왜냐하면, <selectKey> 을 쓸 경우 AOP 등이 끼어들 여지가 없고, 먼저 값을 가져온 뒤에 그 값을 가지고 insert 할 때마다 이용하기 때문에, 고의로 그 값을 변조하지 않는 이상(혹은 정말 실력이 떨어져서 thread 에서 그 값을 공유하는 코딩을 하면 망한다) 이런 문제가 발생하지 않는다. 실제 MyBatis 3 나 iBatis 2 에서는 사용법이 좀 다르긴 하나, MyBatis 3 에서는 <selectKey> 에 여러 값을 조회한 뒤, Bean 에 그 값을 넣어주기 때문에 정말 문제 발생 가능성이 떨어지고, 난 개인적으로 이런 코딩을 방어적인 코딩이라고 부른다.
그런데, 이렇게 코딩하는건...너무 예제도 많고...귀찮아서 여기엔 적지 않겠다.

중요한 건, 분명 Oracle 시퀀스에서 nextval 와 currval 을 insert 에 바로 적을 경우 문제가 발생할 부분이 존재한다는 것이다. 사실 위에서는 극적으로 보이기 위해 AOP 을 썼지만...사실 예전에 문제가 됐을 땐 Service 안에 테이블에 값을 넣는 Mapper 을 호출하도록 중간에 코드를 넣는 형태였다.

그리고, 이런 위험성을 알면서 알려주지 않는 것은...상대가 그걸 받아들일만큼의 실력이 되는지의 여부와는 상관이 없다고 본다. 오히려 문제가 있다고 하는데도 자기 주장 때문에 부득부득 우기는 모습은......그만하련다. 내 짧은 삶에 그런 걸로 시간 낭비하는 것도 짜증난다. 그 사람 입장에서도 평생 이런 문제 안겪을 확률이 높을텐데 내가 왜 나서서 이러고 있는지는 모르겠지만...

어쨌든, Oracle / MyBatis 에서 Sequence 쓸 때 그 값을 재사용한다면 습관적으로 <selectKey> 을 쓰는 걸 권한다. 물론 선실행이 필요한 Sequence 말고도 후실행되는 Auto increment 을 지원하는 다른 DBMS 에서도 그냥 생각없이 <selectKey> 쓰는게 나을 수 있겠다. 굳이 이경우 저경우 다 따질 필요도 없고, 하나의 XML Element 에서 두 번의 쿼리를 통해 이것저것 다 할 수 있어서 코드도 간결해 지니까 말이다.




꼬랑지 1. MS SQL Server 는 2012 버전부터 Sequence 을 지원한다. Auto increment 말고 Sequence 쓰는걸 추천한다. 물론 방어적인 이유 때문이다. 본인이 하는 사이트에서 문제 발생하지 않았다면 그냥 계속 써도 무방하다. 나처럼 insert 가 무지막지하게 발생하는 곳에서는 이것 때문에 고생할 수 있겠지만, 평생 이런 경우 안겪는 경우가 더 많을꺼다.

꼬랑지 2. MS SQL Server 에서는 <selectKey> 와 별개와 SELECT SCOPE_IDENTITY() 와 IDENT_CURRENT() 와 관련된 문제가 있다. 그런데, 이걸 위 주제와 연결시키는건 한마디로 지기 싫어서 논점을 흐리는 것에 지나지 않는다고 본다. 이걸 꺼내든 사람은 절대 인정하지 않겠지만...그냥 Sequence 쓰라고 한 마디 하고 싶었지만, 어쨌든 그 커뮤니티에서는 절대 응대하지 않는다...가 나의 개인방침으로 굳어졌기에...물론 여기를 찾아내서 댓글 달아도 응대할 생각 없음. (그 커뮤니티는 내 개인 사이트가 아니기 때문에 그곳이 시끄러워 지는게 절대 좋을게 없다는 판단 때문, 그냥 나만 참으면 되는 것이기에)

꼬랑지 3. 이 글에도 다른 분들의 어거지는 엥간하면 응대 안합니다. 이 문제로 싸워봐야 서로 니 생각이 옳네 내 생각이 옳네...수준이지 건설적이지 못하다고 생각합니다.

2016년 6월 17일 금요일

SATA 확장 카드

메인모드의 SATA 포트가 부족할 때 확장 카드를 설치해서 사용할 수 있습니다.
기왕 할 꺼 8포트짜리를 추가한다면...

AOC-SAS2LP-MV8
SD-PEX40104
SI-PEX40071

같은 모델이 있습니다. AOC-SAS2LP-MV8 은 PCI-e 8x 용입니다. 그 만큼 대역폭이 커야한다는 이야기겠죠. AOC-SAS2LP-MV8 가 그나마 브랜드(Supermicro)이고 안정성이 뛰어나다고 합니다. xpenology 에서도 작동확인이 되었다고 합니다.

2016년 6월 7일 화요일

윈도우 창 매니저 교체

기존에 Winsplit revolution + UltraMon 으로 창 위치 타일 위치로 크기 및 위치 변경 + 다른 모니터로 이동 + 전체 크기로 변경 등을 했었는데, 윈도우즈 10 으로 변경한 뒤 Winsplit revolution 이 문제가 발생하기 시작했습니다.

그래서 무엇으로 변경해야 하나...한참 찾았는데,  아쿠아 스냅(AquaSnap) 으로 한 방에 해결했습니다. 두 개를 섞어써야 했던 것을 한 프로그램 안에서 다 해결했네요.

어짜피 다른 기능은 안쓰고, UltraMon 의 작업표시줄 확장 기능은 Windows 10 에 포함되어 있어서 필요가 없어졌습니다. UltraMon 을 저렴한 가격으로 오랫동안 잘 썼지만 이젠 놔줘야 할 것 같네요. (안되면 아쿠아 스탭과 섞어 쓰죠 뭐...)

http://www.nurgo-software.com/?utm_source=AquaSnap&utm_medium=application

2016년 5월 25일 수요일

Vert.x 3.1.0 시작해보기...(6)

제가 요즘 요긴하게 써먹고 있는 Vert.x 을 소개하고자 합니다. 폴리글랏이긴 한데, 저는 Java 만을 사용해서 개발하고 있습니다. 그래서 Java 코드만 올리겠습니다.

장문의 글을 올리기 보다는, 바로 어떤 결과가 나오는 짧은 팁만 올리고, 댓글로 보충 내용을 올리는 쪽으로 해보겠습니다. 방송 형태가 적합하겠지만, 할 줄을 몰라서...(컴맹입니다)

------------------------------------------------------------------------------------------------------

역시나 오랫만에 글을 올립니다. 요즘 좀 많이 게을러지기도 해서...

제목이 살짝 바뀌었습니다. 눈치채신 분 계신가요? 그 사이 Vert.x 의 버전이 3.0.0 에서 3.1.0 으로 올라갔습니다. 몇몇 변화점들이 눈에 띄는데, 그 중에서도 제가 구글 그룹스에 글을 남겼던 부분도 개선이 되었네요. (https://groups.google.com/forum/#!topic/vertx/-ZofvUfbzj8 )

하지만, 일반적인 사용에 있어서는 큰 변화가 없으니...제 앞의 게시물들은 여전히 변경 없이 사용 가능할 것입니다.



이번에 알아볼 것은 config 파일와 LocalMap 입니다.



먼저, config 은 vertx run 을 통해 실행될 때 설정 파일을 읽어오는 기능입니다. Vert.x 는 JSON 을 이용해서 데이터들을 주고 받는 것을 기본으로 하고 있어서인지, 설정 파일 역시 JSON 파일로 읽어올 수 있게 개발되어 있습니다. 그냥 JSON 이 들어있는 파일을 실행 시 옵션으로 가리키면 그 파일을 읽어오는 것이죠.

가령 예를 들면 아래와 같습니다.

vertx run Test6_1.java -conf config.json

-conf 에 파일 경로와 이름을 적어주면 해당 파일을 읽어들입니다. 지금은 java 파일과 같은 위치에 존재하기 때문에 특별히 경로를 적지 않았습니다만, 개발 PC 와 서버마다 VCS 외부에 파일을 위치시켜두고 실행을 배치 파일로 하도록 만들어서 각각의 config 파일을 읽게 해주면 Spring 의 profile 와 같은 역할을 할 수 있습니다. JSON 형태를 읽기 때문에 좀 더 강력하다고 할까요...

위와 같이 실행될 경우 Test6_1.java 는 start() 안에서 context.config() 으로 읽어올 수 있습니다.

하지만, 주의할 것은 이 설정 파일은 Test6_1.java 파일에서만 읽을 수 있으며, Test6_1.java 파일에서 deploy 한 다른 파일에서는 context.config() 으로 받을 수 없다는 것입니다. 대신에, deployVerticle() 시 옵션을 통해 config JSON 을 전달할 수 있습니다(물론 중간에 가공한 뒤 보낼 수도 있겠죠). 아래와 같이 말이죠.

{
  "TestMessage": "Hello World"
}

public class Test6_1 extends AbstractVerticle {

 @Override
 public void start() throws Exception {

  super.start();

  EventBus eventBus = vertx.eventBus();
  JsonObject config = context.config();

  DeploymentOptions options = new DeploymentOptions().setConfig(config);

  vertx.deployVerticle("Test6_2.java", options);
  vertx.deployVerticle("Test6_3.java", options);

앞 글에서 살펴본 DeploymentOptions 을 통해서 여러 설정을 할 수 있는데, 그 중에 config 파일을 담아서 보내는 기능도 있습니다.

그럼, 다른 Verticle 에서 잘 받는지 구문을 만들어봐야겠죠? 아래와 같이 만들어봤습니다.

public class Test6_2 extends AbstractVerticle {

 @Override
 public void start() throws Exception {

  super.start();

  EventBus eventBus = vertx.eventBus();
  JsonObject config = context.config();

  eventBus.consumer("Test6_2", (Message<Object> message) -> {

   String testMessage = config.getString("TestMessage");

실제로 Test6_1~Test6_3 까지 받으신 후 실행하고서, http://localhost:8080/Test6_2  라고 호출하면 브라우저에 config 안에 있던 "Hello World" 가 출력됨을 아실 수 있을 겁니다.



이번에는 LocalMap 입니다. Vert.x 2.x 에서는 SharedMap 이라고만 있었는데, Vert.x 3 가 나오면서 LocalMap 와 ClusterWideMap 으로 나뉘어졌습니다. 예전의 SharedMap 은 LocalMap 와 동일한 것입니다. Embeded 모드에선 ClusterWideMap 이 의미가 없지만, 실행 모드에서 Cluster 구성을 한 뒤에는 차이가 존재합니다. Vert.x 는 Hazelcast 을 통해 Cluster 을 제공하는데, EventBus 을 여러 instance 의 것을 분산해서 사용할 수 있는 형태입니다. 문제는 다른 JVM 이다보니 일반적인 Object 로는 값의 동기화가 안되기 때문에 Hazelcast 의 공유 Object 을 이용하는데, Vert.x 는 이것을 ClusterWideMap 을 통해서 제공합니다. LocalMap 은 Instance 마다 개별적으로 분리되기 때문에 Cluster 환경에선 LocalMap 와 ClusterWideMap 의 사용에 신중을 기해야 합니다. 그리고, ClusterWideMap 은 반드시 비동기 형태로만 작성되는 것도 유의해야 하구요(Hazelcast 는 아닌데...).

LocalMap 은 다음과 같이 String 기반으로 개체를 구분해서 사용할 수 있습니다. 다음 예제는 다른 Verticle 에서 선언한 LocalMap 을 공유해서 쓰는 것을 보여줍니다.

public class Test6_2 extends AbstractVerticle {

 @Override
 public void start() throws Exception {

  super.start();

  EventBus eventBus = vertx.eventBus();
  JsonObject config = context.config();

  eventBus.consumer("Test6_2", (Message<Object> message) -> {

   String testMessage = config.getString("TestMessage");

   LocalMap<String, String> map = vertx.sharedData().getLocalMap("Test6.LocalMap");

   String localMapValue = map.get("LocalMapKey");

   System.out.println("LocalMapValue : " + localMapValue);

   String sCount = map.get("Count");

   if (sCount == null) {

    sCount = "0";

   }

   int count = Integer.parseInt(sCount) + 1;

   map.put("Count", Integer.toString(count));

   localMapValue = "Count" + count;

   map.put("LocalMapKey", localMapValue);

   message.reply(testMessage);

  });

 }

위의 config 와 같은 파일에 구현을 했는데, config 의 결과는 웹브라우져에 결과가 전송되도록 되어 있고, 이번 예제는 서버 측 콘솔에 결과가 출력되도록 되어 있습니다. 처음 Test6_2 을 호출하게 되면 콘솔에 LocalMapValue : null 가 출력됩니다. 하지만 다음 요청부터는 Test6_2 을 호출하든, Test6_3 을 호출하든 LocalMapValue : Count1 와 같이 개체에 값이 추가된 것을 확인할 수 있습니다. 이렇게 값을 공유할 수 있다는 것이죠.



오늘은 여기까지...



이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;

위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.

Vert.x 3.0.0 시작해보기...(5)

제가 요즘 요긴하게 써먹고 있는 Vert.x 을 소개하고자 합니다. 폴리글랏이긴 한데, 저는 Java 만을 사용해서 개발하고 있습니다. 그래서 Java 코드만 올리겠습니다.

장문의 글을 올리기 보다는, 바로 어떤 결과가 나오는 짧은 팁만 올리고, 댓글로 보충 내용을 올리는 쪽으로 해보겠습니다. 방송 형태가 적합하겠지만, 할 줄을 몰라서...(컴맹입니다)

------------------------------------------------------------------------------------------------------

오랫만에 글을 올리네요. 하루에 한 개씩 올릴 수 있을 줄 알았으나, 게으름을 이기지 못하네요.

이번에는 Vert.x 의 Verticle 이 가지는 아주 기초적인 특성 하나만 알아볼 것입니다. 바로 독립적으로 돌아가는 특성입니다.

먼저, 가장 기본적인 Java 에서의 싱글톤(Singleton)을 한 번 구성해봤습니다. 너무 쉬운 내용이지만, 다음 내용 설명을 위해서 기초적인 형태로 구성한 점, 양해 바랍니다.

public class Test5_1 {

 private Test5_1() {

 }

 private static Test5_1 instance;

 public static Test5_1 getInstance() {

  if (instance == null) {

   System.out.println("Create Instance Test5_1");

   instance = new Test5_1();

  }

  return instance;

 }

}

정말 단순한 형태의 싱글톤입니다. 단지, 클래스가 생성될 때 인스턴스를 생성하게 하지 않고, 아직 인스턴스가 null 인지 확인한 뒤 생성해줄 때 로그를 한 번 출력해주고(생성되는 횟수도 알겸) 개체를 생성한 다음 결과로 인스턴스를 넘겨주도록 구성했습니다.

그리고 이걸 불러서 호출하는 클래스를 2 개 만들어 봤습니다.

import java.util.Random;

public class Test5_2 {

 public static void main(String[] args) throws Exception {

  System.out.println("Start Test5_2");

  Test5_2 test5_2 = new Test5_2();
  Test5_3 test5_3 = new Test5_3();

  Random random = new Random(System.currentTimeMillis());

  for (int iCount = 0; iCount < 10; iCount++) {

   if (random.nextInt(2) == 0) {

    if (random.nextInt(2) == 0) {

     test5_2.test1();

    } else {

     test5_2.test2();

    }

   } else {

    if (random.nextInt(2) == 0) {

     test5_3.test3();

    } else {

     test5_3.test4();

    }

   }

  }

  System.out.println("End Test5_2");

 }

 public void test1() {

  Test5_1 test5_1 = Test5_1.getInstance();

  System.out.println("Get Instance Test5_1 : test1");

 }

 public void test2() {

  Test5_1 test5_1 = Test5_1.getInstance();

  System.out.println("Get Instance Test5_1 : test2");

 }

}

public class Test5_3 {

 public void test3() {

  Test5_1 test5_1 = Test5_1.getInstance();

  System.out.println("Get Instance Test5_1 : test3");

 }

 public void test4() {

  Test5_1 test5_1 = Test5_1.getInstance();

  System.out.println("Get Instance Test5_1 : test4");

 }

}

main 은 Test5_2 에 있고, main 이 호출되면 Random 을 이용해서 임의적으로 4 개의 매서드 중 하나를 호출해서 인스턴스를 가져오는 아주 간단한 예제입니다.

출력 결과는 다음과 같습니다.

Start Test5_2
Create Instance Test5_1
Get Instance Test5_1 : test3
Get Instance Test5_1 : test1
Get Instance Test5_1 : test2
Get Instance Test5_1 : test1
Get Instance Test5_1 : test2
Get Instance Test5_1 : test4
Get Instance Test5_1 : test1
Get Instance Test5_1 : test3
Get Instance Test5_1 : test2
Get Instance Test5_1 : test2
End Test5_2

물론 출력되는 Get Instance Test5_1 : 이후의 내용은 Random 하게 바뀌겠지만, Start 후 Create 가 한 번만 나오고 End 가 마지막에 나오는 것은 누구나 예측할 수 있을 것입니다. 이게 싱클톤이니까요. 보통 이런 싱글톤은 JDBC 을 이용해서 DB Connection 을 가져오는 문장에서 많이들 사용하셨을 겁니다.

그런데, Vert.x 에서는 이렇게 싱글톤을 이용할 경우 큰일이 납니다. 다음 예제를 한 번 보시죠.

import io.vertx.core.AbstractVerticle;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.eventbus.Message;

import java.util.Random;

public class Test5_4 extends AbstractVerticle {

 @Override
 public void start() throws Exception {

  super.start();

  vertx.deployVerticle("Test5_5.java");

  EventBus eventBus = vertx.eventBus();

  eventBus.consumer("Test5_4.test1", (Message<Object> message) -> {

   Test5_1 test5_1 = Test5_1.getInstance();

   System.out.println("Get Instance Test5_1 : test1");

   message.reply(null);

  });

  eventBus.consumer("Test5_4.test2", (Message<Object> message) -> {

   Test5_1 test5_1 = Test5_1.getInstance();

   System.out.println("Get Instance Test5_1 : test2");

   message.reply(null);

  });

  Random random = new Random(System.currentTimeMillis());

  for (int iCount = 0; iCount < 10; iCount++) {

   if (random.nextInt(2) == 0) {

    if (random.nextInt(2) == 0) {

     eventBus.send("Test5_4.test1", null);

    } else {

     eventBus.send("Test5_4.test2", null);

    }

   } else {

    if (random.nextInt(2) == 0) {

     eventBus.send("Test5_5.test3", null);

    } else {

     eventBus.send("Test5_5.test4", null);

    }

   }

  }

 }

 @Override
 public void stop() throws Exception {

 }

}

import io.vertx.core.AbstractVerticle;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.eventbus.Message;

public class Test5_5 extends AbstractVerticle {

 @Override
 public void start() throws Exception {

  super.start();

  EventBus eventBus = vertx.eventBus();

  eventBus.consumer("Test5_5.test3", (Message<Object> message) -> {

   Test5_1 test5_1 = Test5_1.getInstance();

   System.out.println("Get Instance Test5_1 : test3");

   message.reply(null);

  });

  eventBus.consumer("Test5_5.test4", (Message<Object> message) -> {

   Test5_1 test5_1 = Test5_1.getInstance();

   System.out.println("Get Instance Test5_1 : test4");

   message.reply(null);

  });

 }

 @Override
 public void stop() throws Exception {

 }

}

위 예제를 보시면 메서드를 EventBus 로 단순히 바꾼 형태라는 걸 눈치채실 겁니다. 이 코드를 이용해서 Test5_4.java 을 Vert.x 로 실행하면 어떤 결과가 나올까요? 대략 아래와 같은 결과가 나옵니다.

> vertx run Test5_4.java
Create Instance Test5_1
Get Instance Test5_1 : test3
Get Instance Test5_1 : test4
Get Instance Test5_1 : test4
Create Instance Test5_1
Get Instance Test5_1 : test1
Get Instance Test5_1 : test2
Get Instance Test5_1 : test1
Get Instance Test5_1 : test2
Get Instance Test5_1 : test1
Get Instance Test5_1 : test4
Get Instance Test5_1 : test3
Succeeded in deploying verticle

예상하신 분들은 예상하셨겠지만...Create 가 두 번 발생합니다.

만약 Verticle 이 10 개라면, 10 개의 Create 가 발생한다는 이야기입니다. 즉, 모든 Verticle 은 별개로 동작하고, 이걸 내부적으로 Hazelcast 로 통신하면서 값을 전달하기 때문에 기존의 Singleton 을 가지고 하나의 접속만을 유지하면서 사용한다는 개념으로 접근해버리면, 전혀 엉뚱한 결과에 멘탈이 붕괴되는 현상을 경험하실 겁니다.

그래서, DB 접속이든 쿼리를 실행하는 구문이든, Singleton 으로 Connection 개체를 받아와서 처리하는 형태로 구성해서는 안되고, 쿼리를 처리하는 하나의 버티클을 만들고 그 안에 쿼리를 처리하는 EventBus 을 만든 뒤, 모든 요청을 그 EventBus 로 보내서 처리하고 결과만 JsonObject 등으로 받아오는 형태로 프로그램을 작성해야 합니다. 그래서, 오히려 처리가 더 힘든 면이 있지 않나 싶네요.


오늘은 여기까지...


이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;

위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.