JWT(JSON Web Token): 웹에서 안전한 개발을 하기 위한 도구

우리가 사용하는 웹은 공개된 세상이다. 프로토콜이 공개되어 있고, 자유롭게 접근할 수 있는 데이터들이 있다.  개발자들은 공개된 프로토콜과 데이터를 활용해 이를 공개된 정보를 사용자들에게 제공한다.  웹이 지향하는 이 개방성은 모두에게 방대한 정보를 제공하는 기회를 제공한다.  이것이 최초의 웹이 현재의 웹이 된 이유일 것이다.

웹 세상에서의 통신

하지만 모든 정보가 모두에게 공개될 수 있는 것은 아니다.  특정 정보는 개인의 사적 정보를 담고 있기 때문에 그 사람에게만 제공되어야 한다.  마찬가지로 개인의 사적 정보 역시 안전하게 입력받아야 한다.  특히 사용자가 입력하는 정보는 사용자 개인을 특징지을 수 있는 정보를 포함하기 때문에 무조건 이런 처리를 해야한다.

그럼 웹에서 통신은 어떻게 이뤄질까?  개인 관점에서 정의해보면 아래와 같은 큰 덩어리로 정리될 수 있다고 생각한다. (OSI 7 Layer를 참고하긴 했지만 정확하게 일치하지 않는다. 순전히 개인적인 관점이다. ^^)

CommunicationLayers

각 계층(Layer)가 가지는 의미를 정의해보면 아래와 같다.

  • Physical Layer – 실질적인 통신선을 나타낸다.  무선이든 유선이든 일단 선을 깔아야지 통신이 되겠지?
  • Transport Layer – 선이 깔린 상태에서 이제 보내는 쪽과 받는 쪽이 일련이 규격을 통해 정보를 주고 받는다.
  • Application Layer – 쌍방간에 데이터를 주고 받을 때 어떤 형식(Protocol)을 사용할지 어플리케이션 사이에 정의한다.

이런 환경에서 웹 서버와 클라이언트(웹 브라우저)가 통신을 하고 있다. 당신이 PC에서 이 글을 보고 있다면 아마도 유선 랜으로 연결되어 있을 것이고 스마트폰에서 보고 있다면 무선으로 연결되어 있다.(Physical Layer)  접속한 각 기기는 IP를 가지고 TCP 기반에서 패킷을 통해 이 글을 요청했고, 웹 서버는 이에 응답해서 글을 내려보낸 것이다.(Transport Layer)  마지막으로 서버에서 HTML이라는 프로토콜로 ASCII 데이터를 내려보내면 브라우저가 이를 해석해서 보기 좋은 형태의 텍스트로 표시했다.(Application Layer)  그렇기 때문에 당신은 이 글을 볼 수 있다.

개인적으로 이 글에 내 신상에 관련된 아주 특별한 정보가 있는 것도 아니고 원래 의도가 불특정 다수에게 읽혀지길 원하기 때문에 특별히 보안을 요구하지는 않는다.  다만 내 의지가 아닌 남의 의지에 의해 내가 작성한 글들이 싸그리 날라가는 일만 없길 바랄 뿐이다.

불안함에 대응할려면?

불안함에 대응하기 위한 어지간한 방법들은 이미 널리 알려져있다.

Physical Layer 차원에서의 대응

전용선을 쓰면 된다.  하지만 구리선을 깐다는건 돈이 억수로 든다는 것을 의미한다.  그리고 당신의 프로그램(혹은 시스템)은 특정 고객을 위한 것이어야만 한다는 전제가 깔여 있어야 한다.  전용선을 여기저기 이어준다는 것은 전용선이 전용선이 아니라는 것이니 말이다.

돈이 걱정이라면 VPN(Virtual Private Network)을 이용할 수도 있다.  하지만 이 역시 소프트웨어를 이용한 가상적인 망이라는 것. 그리고 엄청나게까지는 아니겠지만 상당히 느릴 것이라는 것을 감수해야 한다.

Transport Layer 차원의 대응

일반적인 OSI 7 레이어에서 Transport 계층의 상위 계층은 TCP/IP이다. 그냥 쓰면 여러분이 보내는 정보를 그냥 까볼 수 있다. 이게 안되게 할려면 오고 가는 정겨운 메시지들을 암호화하면 된다. 주로 SSL 등이 TCP/IP 계층 위에서 연결된 양 단말 사이에 오고 가는 데이터들을 암호화한다.

이 방식은 대표적인 예가 HTTPS 프로토콜이다.  일반적인 HTTP는 TCP/IP 스택 위에서 암호화를 적용한다.  따라서 만약 이 데이터를 누군가가 중간에 가로채서 데이터를 변경했다고 했을 때 변경됐는지 아닌지 알 도리가 없다.  HTTPS는 중간 경로를 통한 위변조를 막기 위해

  1. “중립 기관(Certificate Authority)”를 통해 서명된 인증서(Certification)를 이용해 데이터를 암호화하고
  2. 브라우저에 암호화된 데이터와 인증서를 전달한다.
  3. 브라우저는 인증서에 적용된 서명이 올바른지를 다시 중립 기관을 통해 확인하고
  4. 실제 데이터를 보낸 서버로부터 받은 경우에만 인증서를 통해 데이터를 복호화하여 실행(표시)한다.

 

(http://www.webstepbook.com/supplements-2ed/slides/lecture27-security.shtml#slide8)

이 과정에서 오류가 발생되면 브라우저는 이를 사용자에게 알리거나 아예 접근을 차단한다.  이것이 우리가 구버전의 IE를 사용하면 안되는 이유이다.  IE 구버전은 사용의 편이성(?)을 위해 HTTPS의 증명이 틀리더라도 내려받은 코드(특히 자바스크립트 혹은 ActiveX)를 실행하기 때문이다.

Application Layer 차원의 대응

이정도면 충분한 준비를 갖춘것 아닐까?  기본은 충족을 한 것 같지만 그래도 좀 부족하다.

HTTPS 방식에서 인증 기관을 해커가 사칭한다면 어떻게 될까?  이 상황에서 서버에서 내려오는 정보를 마찬가지로 동일 해커가 위변조시킨다면 영락없이 당할 소중한 정보를 빼앗기거나 사용자 컴퓨터의 이러저러한 정보들이 탈탈 털리는 상황이 나온다.  서버와 클라이언트 사이에 전달되는 정보를 누구나 다 볼 수 있는 쿠키에 암호화되지 않은 형태로 주고받는 환경이라면 더욱 심각한 상황이 되버린다.

이런 상황을 가급적 최소화하기 위해서는 개발자의 힘이 필요하다.

JWT(JSON Web Token)이란?

앞서 이야기한 것처럼 시스템을 통해 정보를 보호하는 건 한계가 있다.  일반적인 보안 시스템이 할 수 있는 수준은 정성들인 해커에게는 대부분 무용지물인 경우가 많다. 쿠키에 많은 정보를 주고받는 것이 그냥 손에 익어버린 경우라면 더욱 더 문제다.

이런 걸 하지 말라는 차원에서 등장한 개념이 암호화된 토큰을 이용하는 방법이다.  우리가 다루는 모든 정보를 암호화할 필요는 없다.  외부에 노출되면 곤란한 개인 정보 혹은 중요 정보들을 그 대상으로 한다.  그리고 이 정보들을 시스템 사이에 주고 받을 때 이놈이 그놈이 맞는지를 확인할 수 있으면 된다.

JWT는 토큰 방식으로 이를 처리하기위해 제시된 방식이다. 이름이 의미하는 것처럼 JSON String 형태이기 때문에 웹 환경에서 쓰기에 좋다.  특히 RFC로 정의된 표준이다. JSON의 특성상 다양한 형태의 정보를 정의할 수 있으며, 정보의 출처를 확인하기 위한 검증 방식을 제공한다.

source from https://jwt.io/introduction/

그림에서 보는 것처럼 JWT은 일련의 암호화된 문자열이며, 점(.)을 기준으로 3가지 영역으로 구분된 구조를 갖는다.

  • Header – 해당 토큰이 어떤 암호화 알고리즘을 통해 암호화되었는지를 정의한다.  alg 라는 세부 필드로 표현된 알고리즘이 암호화에 적용된 알고리즘을 나타낸다.
  • Payload – 실제 전달하고자 하는 메시지 Body를 나타낸다.
  • Signature – 전달된 메시지에 대한 Signature 값을 나타낸다.  말 그대로 signature 값이 포함되어 있으며, 해당 값을 인식하기 위해서는 보낸쪽과 받는쪽에서 합의된 키 값을 가지고 Header에서 정의된 암호화 알고리즘을 통해 검증할 수 있다.

구조에서 Header와 Payload에는 암호화가 적용된 것이 아니라 Base64 URL Encoding되었을 뿐이다. 실제 암호화는 Signature에만 적용되어 있으며, 전달된 메시지를 서로 신뢰할 수 있는지를 평가하기 위한 구조로 사용된다.  이 말이 의미하는 바를 풀어 설명하면 이렇다.

전체 데이터에 대한 외부 노출 차단은 HTTPS와 같은 시스템 수준의 암호화를 통해 담보하고, 전달받은 데이터의 신뢰성을 평가를 암호화 토큰을 통해 보장한다.  따라서 암호화 알고리즘과 이에 대한 암호화 키 정의는 어플리케이션 개발자가 시스템의 구조와 토큰의 역할에 의해 결정된다.

그렇기 때문에 개발자의 역할이 중요해진다.

Just do it with code

그럼 이걸 어케 개발자가 프로그램으로 작성할 수 있을까?  앞서 링크한 jwt.io 사이트에서 관련된 이를 지원하는 여러 언어별 라이브러리들이 있다.  Javascript를 이용한 예제는 여기를 참조하자.  소개할 예제는 Java를 활용한 예제 코드로 nimbus 라이브러리를 활용했다.  nimbus를 사용하기 위해서는 다음의 메이븐 dependency를 설정한다.

    <dependency>
      <groupId>com.nimbusds</groupId>
      <artifactId>nimbus-jose-jwt</artifactId>
      <version>4.12</version>
    </dependency>

JWT를 생성하는 방식은 아래 코드를 참조한다.

JWSSigner signer = new MACSigner(key);
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                .claim("cid", "this-is-key")
                .claim("name", "chidoo")
                .claim("admin", true)
                .build();
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
signedJWT.sign(signer);

String jwtString = signedJWT.serialize();
System.out.println(jwtString);

암호화 알고리즘으로 HS256을 사용하기 때문에 key값은 32바이트 이상의 문자열을 사용한다. 출력된 코드가 정상적인지는 간단하게는 jwt.io 사이트에서 Verification할 수 있다.  혹은 다음 코드와 같이 입력된 JWT 값을 동일한 키 값을 통해 확인해볼 수 있다.

JWT signedJWT = (SignedJWT)SignedJWT.parse(jwtString);

// check payload and applied algorithm in header
System.out.println(signedJWT.getPayload().toJSONObject());
System.out.println(signedJWT.getHeader().getAlgorithm());

// verification
JWSVerifier verifier = new MACVerifier(key);
assertThat(signedJWT.verify(verifier), is(true));

실제 코드를 통해 이걸 사용하는 방법은 위의 몇 줄에서 보는 바와 같이 쉽다. ^^;

이렇게 활용할 수 있다.

그럼 이걸 어떤 경우에 써먹어야지 제대로 써먹었다고 이야기를 할 수 있을까?  가장 대표적으로 특정 시스템에 접근하기 위한 권한을 이 사용자가 제대로 가지고 있는지를 확인하는 경우이다.  아래 그림의 예제를 통해 살펴보면 다음과 같은 절차로 처리가 이뤄진다.

  1. 사용자가 로그인을 하면 보통 세션을 생성한다.
  2. 생성된 세션을 위한 사용자의 Key를 발급하고, 이를 가지고 사용자 식별자(보통 User ID)를 포함한 JWT를 생성해서 이를 Response로 보낸다.
  3. 이 JWT는 세션이 유지되는 동안 사용자가 쿠키 혹은 내부 변수의 형태로 가지고 있는다.
  4. 사용자가 민감한 정보를 가지는 시스템에 접근하는 경우 JWT를 그 시스템에 전송한다.
  5. 해당 시스템은 전달된 JWT 정보를 Decode해서 사용자를 알아낸다.
  6. 사용자에게 발급된 키를 통해 전달된 JWT 값이 위변조된 값인지 아닌지를 확인한다.
  7. 정상적인 토큰으로 인식하면 사용자에게 부여된 권한 등등에 부합할 때 데이터에 대한 접근을 허용하면 된다.

 

JWTUsageExample

 

위의 경우처럼 일반적인 사용자 인증 체계의 신뢰성을 확보하기 위한 용도가 가장 대표적일 수 있다.  이 이외에도 마이크로서비스 아키텍처 환경에서 각 서비스 시스템간 Collaboration을 위한 권한 관리 용도로도 활용이 가능하다.  마이크로서비스 환경에서 각 서비스 시스템들은 본인들이 제공하는 정보의 규격을 정의한다.  하지만 누구나 정보를 임의대로 허용할 수는 없다.  자신과 협업할 수 있는 시스템들의 제한이 필요하며 이를 위한 신뢰성있는 토큰으로 JWT를 활용할 수 있다.

ServiceCollaboration

Service A에 접근하기 위해서는 사전에 시스템간 협의에 의해 key를 발급한다.  연동 서비스에서 Service A에 접근하기 위해서는 본인에게 발급된 키를 가지고 JWT 생성하고 이를 원하는 Operation과 함께 시스템에 전송한다. A 서비스는 전달된 값을 검증해서 허용된 시스템에 대해서만 요청된 기능을 실행한다.  만약 협업이 허용되지 않은 시스템으로부터의 요청에 대해서는 당연히 이를 거부한다.

JWT를 사용하는데 있어서 JWT 값의 Lifetime을 명확하게 관리해야한다.  생성된 JWT가 반복적으로 쓰여도 별 문제가 안된다면 이건 하나마나한 결과를 초래하기 때문이다. 따라서 이 값이 언제 생성된 값인지 그리고 언제까지 유통 가능한지를 JWT 안에 포함시켜 관리하는 것이 보다 안전한 시스템 구성을 유지하는 길이다.

마음만 먹으면 제대로 할 수 있다.

뭔가를 보호하는 일은 솔직히 많이 사람을 귀찮게 한다. 특히나 재미있는 기능을 만들고 싶은 마음이 굴뚝인데, 기능 그 자체와는 동떨어진 일에 얽매여 있다보면 짜증이 날 수도 있다.  하지만 귀중한 정보를 가지고 있다면 그 귀중함을 잘 지켜야 할 의무 또한 존재한다.

방법이 있다면 그 방법을 적용해야 한다.

매번 소잃고 외양간만 고치고 있을 수는 없지 않을까?

– 끝 –

 

Pair Programming을 위한 준비 사항

간만에 짝 프로그래밍(Pair Programming)을 해보고 있다. (이후부터는 그냥 페어 프로그래밍)

Pair Programming.jpg - Wikimedia Commons

이 방법을 에자일과 XP 책들을 읽으면서 “아, 이런 방법도 있구나~” 하고 배웠다.  그러고 보면 이 시절에 페어 프로그래밍과 TDD를 포함해 새로운 지식들이 넘쳐나던 시절이었다.  어줍잖은 자신감으로 진행했던 프로젝트의 실패와 더불어 회사의 자금 사정 악화로 경제적으로 빈궁한 시절이었다. 하지만 실패를 곱씹는 과정에서 챙긴 지적 호기심은 이후에 참 많은 도움이 됐다.

사설이 길었지만 페어 프로그래밍을 실제로 시작한 건 네이버에서 일하기 시작한 이후다.  그것도 시작은 하다가 아예 공중 분해된 프로젝트를 진행할 때!!! (우연일지 필연일지 모르겠지만 망한 프로젝트에서 개인적으로 건져지는게 많은 듯…)  당시의 “나”라는 사람은 에자일(Agile)과 TDD라는 선진 문물에 신기해하면서 제대로 개발자의 궁색을 갖추는 중이었다.  아무래도 역량 부족.  페어의 정당한 짝꿍이라기에는 부족한 관전자일 수 밖에 없었다.  (이런 찌질함이었음에도 불구하고  2009년에 이따구 글을 적었다라는게 참… )

그 시절 이후로 시간이 많이 지났다. 제대로 개발하기 위한 몇 년의 시간이 있었고 때아닌 질풍같은 시간이 휩쓸고 지난 이후 현재의 자리에 있게 됐다.  잘 하는건 아니지만 나름 백엔드 개발에 대해서는 나름 코딩을 할 수 있다는 생각도 있고.  ^^;

최근에 시작한 새로운 프로젝트가 있어서 함께 하는 친구와 페어로 진행하고 있다.  진행하면서 느낀 “제대로 하기 위해 이런 것들이 꼭 필요하겠구나?” 하는 생각이 들어 정리해본다.

1. 서로를 존중해야 한다.

한 컴퓨터를 가지고 두 사람이 붙어 작업하는 것을 페어의 원칙이다.  세상에는 동일한 인간이 존재할 수 없다.  일란성 쌍둥이라고 하더라도 인격은 다를 수 밖에 없다. 특히나 프로그래머들의 경우에는 자신만의 이고(Ego)를 가진 사람들이 흔하다. 그렇기 때문에 긱스(Geeks)들이 널리고 널린 분야다.

둘이 하는 작업이다. 서로 양보하지 않고 부디친다면 충돌은 피할 수 없다.  기술에 대한 논쟁의 충돌은 환영할만한 일이다. 그러나 말이 전도되어 감정적인 언어가 이야기 사이에 끼어들면 폭망한다.

특히나 대화를 매개로 한 두 사람의 공동 작업에 나이나 직위가 끼어들지 않도록 해야한다. 권위주의가 대화에 개입하면 제대로 된 말이 이뤄지지 못한다.

2. 적극적으로 대화를 한다.

같이 작업을 하는 친구가 부끄럼쟁이지만 술만 먹으면 이야기를 또 나름 하는 친구다.  물론 실력도 출중하다. 그래서 페어를 할 때 이야기를 많이 한다. 말빨에 관련해서는 나도 밀리지는 않는 사람이기 때문에 또한 이야기를 많이 한다.

코드가 생긴 모양새나 왜 이렇게 코딩을 해야하는지에 대해 이야기하는 그 시간은 개인적으로 행복한 시간이다. (되려 게임하는 것보다도 이 시간이 더 재미있는 것 같기도 하다.)  특히 어떤 방식의 코드의 구조가 좋은지, 잘 읽히는 코드를 작성하기 위한 서로의 생각을 이야기는 정말 좋다. 너가 옳다 내가 옳다 이야기를 많이 하지만 결론적으로 각자 방식에서 장점을 취한다.  애도 아닌데 뭐가 결국 올바른지 몇 마디 말을 주고 받다보면 자연스럽게 알게 된다.  이런식의 대화가 길어지다보면 결국 쌓이는 건 지식이다.

3. 대등한 기량이면 더욱 좋다.

개발 능력이 되도록이면 비슷한 편이 좀 더 페어를 하기에 이점이 있다는게 개인적인 생각이다. 이전에 주니어 친구와 페어를 해봤다. 하지만 페어가 아니라 한 사람은 훈계를 하고 있고, 한 사람은 그 이야기를 주눅든 상태로 듣고 있었다. 아무래도 성질이 드러웠던 모양이다. -_-;;  이 시간이 물론 의미가 없는 건 아니다.  그렇다고 “두 사람 모두에게 발전적인 시간이었냐?” 라는 질문에는 “글쎄???” 라는 답변이 나올 것이다.

페어 프로그래밍도 일하는 방법 가운데 하나이다. 일을 하는 누구라도 일이 생산적이길 희망한다.  앞서 이야기한 것처럼 페어 과정의 토론은 서로의 성장에 큰 도움이 된다.  하지만 이것이 어느 일방의 가르침이라면 어떨까?  가르침을 받는 입장에서는 1:1 과외를 받는 특전일 수 있다.  일방적인 강의를 대학때에도 많이 들었다. 하지만 금방 잊혀지더라는 것이 개인적인 진리라고 본다.

4. 코드는 돌아가면서 작성한다.

내가 작성하는 코드를 누가 옆에서 혹은 뒤에서 쳐다보는 경험은 해보지 않은 사람에게는 매우 낯선 광경이다.  하지만 관찰 대상의 입장과 관찰자의 입장에서 이 광경은 모두 흥미롭다.  관찰 대상의 관점에서 본다면 코드 작성 과정과 툴 사용 과정들을 보여줘야한다.  그만큼 숙달된 조교의 솜씨를 강제한다. 관찰자의 관점에서 역시 다른 사람의 코딩하는경쾌한 키보드 소리를 듣거나 겁내 빠른 툴 사용을 위한 단축키 신공을 눈으로 볼 수 있다.

서로가 보여주고 보는 과정은 코드를 작성하는 본인의 자세를 그대로 까발린다.  얼마나 직관적으로 코드를 작성해나가는지? 술술술 코드를 적어나가는지를 볼 수 있다.  물론 이 과정에서 본인의 코딩 습관도 방청객에게 생얼굴로 보여준다.

관찰자는 민낯에서 얻는 것이 많다.  내가 작성하는 코드 대비 어떤 방식으로 다른 친구는 코드를 작성하는지 혹은 코딩 스타일은 어떤지를 배울 수 있다.  따라서 누구 하나가 독점적으로 키보드를 독차지하는 건 불합리하다.  종종 의자에 붙히는 엉덩이의 주인을 바꾸자.

5. 표준을 정하고 지킨다.

개발자는 코드를 작성한다. 하지만 인간인 이상 개성이 있고, 그 개성이 코드에 묻어나기 마련이다.  지금 작업하는 친구와 의미있는 페어를 하지만 이후에는 다른 친구와도 이런 의미있는 시간을 가져야 한다.  다른 친구의 만남에서 어떻게 상호 절충을 해야할까?  그리고 이런 상호 절충을 계속해야 하는 건가?

이것보다는 “표준안“이라는게 있는게 좋다. 왜 공통의 표준안을 따라야 하는가?  배려심이다.  자신이 작성한 코드를 다른 사람이 편안하게 읽을 수 있도록 혹은 다른 친구가 편안하게 수정할 수 있도록 맞춰주면 정말 좋아하지 않을까?

다른 이야기지만 내부 공유 시간에 나온 JS Framework에 대한 코딩 컨벤션을 무조건 체크하기 위해 lint를 사용했고, 하자는 의견이 있었다.  좋은 움직임이고 자바에 대해서도 이런 툴을 적용해보기로 했다.  표준안이 없다면 정립된 Defacto를 따르고, 그걸 팀의 현실에 맞춰 조정해나가는게 좋지 않을까 싶다.

6. 시간을 맞춘다(혹은 정한다)

이번 페어를 하면서 같이 하는 친구에게 미안한 점은 함께 진행할 수 있는 시간이 길지 않다는 것이다.  상대적으로 참석해야할 미팅이 많다보니 하루에 길게 진행하더라도 2시간 이상은 진행이 어려웠던 것 같다.  서로 시간을 할애해주지 않는건 내가 저지른 실수이긴 하지만 민폐다.

제대로 한다면 하루에 2시간 정도의 시간을 정해두고 페어를 하는게 좋다고 생각한다. 페어를 통해 공통적으로 해야할 일들, 예를 들어 핵심적인 내부 구조를 잡는다던지 서로 알고 있어야 할 업무 로직을 작성해야하는 일을 함께 하는게 좋다. 그리고 이외의 시간에 각자 작업한 부분을 코드 작성하고 이후에 각자 리뷰하면 일도 나름 효과적으로 할 수 있지 않을까 생각한다.

codingconfidence

여기까지 정리해봤다. 아마도 가장 중요한 건 “서로에 대한 존중과 이해“일 것이다.  만약 이걸 갖추고 있지 않으면 절대로 페어를 하지 않는게 좋다.  무시하고 시기하고 결국에는 쌈난다.  본인이 다른 사람을 존중할 줄 안다면 그럼 바로 페어를 해봐라.  존중과 이해 다음에 본인이 갖춰야 할 점이 바로 “실행“이다.

오늘 하루도 좋은 일이 있으시길~~~~

코드 리뷰의 시작에 임하는 자세

코드 리뷰를 어떻게 시작할 때 뭘 생각해야 하는지에 대한 재미있는 글이 있어서 짧게 기록해둔다.

Building a better code review process

글의 제목은 더 좋은 코드 리뷰에 대해 이야기를 하지만 것보다는 리뷰를 할 때 코드보다 더 신경써야 할 것들이 뭔지에 대한 이야기다.  코드 리뷰에서 우리가 주로 신경쓰는 부분이 “코드의 품질”이다.  사실 나도 다른 사람의 코드를 리뷰할 때 주로 이 부분을 본다. 얼마나 읽기 편하게 작성했는지, 재사용이라는 관점을 얼마나 반영했는지, 적절한 테스트 코드로 보장이 되는지 등등을 확인할려고 한다.

하지만 이 글에서는 “리뷰의 시작은 코드가 아니다!” 라고 이야기한다.  먼저 확인해야 할 부분은 코드가 아니라 “사용자, 제품“라고 말한다.  리뷰 대상 코드가 실 환경에 배포되면 이로 인해 변경되는 기능이 있다. 변경된 기능이 사용자에게 미치는 영향은 어떤 것이고, 그리고 인한 사이드 이펙트가 없는지를 먼저 따져봐야 한다.

작성된 코드가 사용성의 어떤 부분을 개선하기 위해 목적을 가지고 있는지를 알아야 한다. 이걸 알아야 작성된 것이 제대로 작성된 것인지, 그게 최선인지를 따질 수 있기 때문이다.

이 글을 읽으면서 지금까지 해온 코드 리뷰가 기능적 혹은 형식적이지 않았나 하는 생각이 들었다.  리뷰를 위한 리뷰를 했을 뿐이라는 생각이다. 리뷰의 결과로 반영될 제품에 이 코드가 어떤 변화를 일으키고, 그 변화가 사용자에게는 어떤 도움을 줄지에 대해 생각이 모자랐다.

리뷰 요청 메시지를 작성할 때 “사용자 기능 XXX를 YYY로 변경해서 사용자가 ZZZ 라는 이점을 얻을 수 있다” 라는 형식이 되면 좋을 것 같다.  메시지를 작성하면서 사용자에게 도움이 되는 코드를 작성하고 있는지 한번 더 돌이켜 볼 수 있지 않을까?  “장인(Craftsman)”이라는 관점에서 작성하는 코드의 매력도 물론 있기 하지만 결국 우리가 작성하는 대부분의 코드는 사람을 지향한다. 사람에게 도움이 되는 코드를 작성하는게 이성적 관점에서 옳다.

글을 작성한 분이 코드 리뷰를 진행하기전에 스스로 반문해보면 좋을 질문 리스트가 있어서 옮겨 적어본다.

  • Does this change solve a real problem for the customer?
  • Is the solution robust, or merely adequate?
  • How do we know that this change is a good investment, relative to other things we could be doing instead?
  • What new problems might arise as a result of this change, and how will we mitigate them?
  • Can the change be easily reversed and/or gracefully degraded if something goes wrong?
  • Are there ongoing maintenance costs associated with this change? What are they?

리뷰 진행전에 이 질문들에 대한 명확한 답을 본인이 할 수 있다면 통과할 가능성이 높다.  그만큼 사용자, 제품에 한발짝 도움이 되는 코드를 본인이 작성하거나 리뷰를 해주고 있다는 의미이기 때문이다.

크게 동감한다.

 

도대체 뭐가 RESTful 이라는건가?

요즘에 웹에서 API 기능을 개발한다고 하면 RESTful 이라는 단어를 많이 듣게 된다. 특히 “Single Page WebApp(웹앱)”이 웹 기능을 개발하는 주류 방법이 되면서 더욱 자주 귀에 들린다.  여기에서 그럼 웹앱이라는 것이 뭔지 우선 짚어보고 가보자.  이걸 알아야지 왜 API가 필요한 것이며 그 중에서도 RESTful을 이야기하는지를 알 수 있을 것 같으니 말이다.

웹앱은 뭔가요?

위키페디아에서 이야기하는 웹앱은 다음과 같이 정의한다.

클라이언트/서버 프로그램을 이야기한다. 특이점이라면 모든 클라이언트 코드가 “웹 브라우저”에서 동작된다.

WebApp웹에서 동작되는 것이기 때문에 당연히 웹 브라우저에서 돌아가겠지만 약간 애매한 단어인 “클라이언트/서버”라는 단어가 보인다.  흠… VC++ 언어로 윈도우 클라이언트 프로그램을 만든 경우를 생각해볼까? 한번 설치한 다음에는 그냥 설치된 걸 실행해서 여러분이 필요한 작업을 한다.  실행할 때마다 매번 설치하고 실행하지는 않는다.

반면에 웹은 어떤가? 링크 하나를 누를 때만다 뭔가를 잔뜩 다운로드 받아야 화면이 넘어간다. 내려받는 것들 가운데에는 코드도 있고, 데이터도 있고, 화면도 있다. 뒤죽박죽이다.

클라이언트/서버 프로그래밍을 해본 경험이 이야기하는 바는 화면은 빤하다라는 것이다.   변하는 것은 오직 데이터일 뿐이다. 데이터에 따라 조합된 화면을 사용자에게 제공하면 된다.

웹앱은 이 개념을 웹의 세상에 도입했다.  Markup, CSS로 만들어진 클라이언트 화면과 자바스크립트 코드로 클라이언트를 만든다. 데이터는 Ajax를 활용해서 API 서버를 통해 얻어온다. 주소 조작(Address and history manipulation)을 통해 앱 내부 영역내의 화면 이동을 위해 웹 서버에 클라이언트 화면과 관련된 어떠한 것도 요구하지 않는다. 데이터가 필요한 경우에만 API 서버와 통신한다.

웹앱 개념의 개발을 통해 기존에 로직과 데이터가 범벅이 됐던 웹의 개발이 화면 + 로직 + 데이터로 깔끔하게 분할됐다. 특히 업무 처리와 관련된 부분이 API 서버 영역으로 명확하게 구분되었다.  이걸 유식한 말로 말하면 “Separate of Concerns” 라고 표현한다.  각 영역에서 해야할 일을 명확히 구분짓고, 그 영역에 집중할 수 있게 되었다. 아마도 FEE, SWE 라는 용어 구분 역시 이런 구분이 가능해졌기  때문에 구체화된 것이 아닐까 싶다.

어떻게 API를 만드세요?

그럼 API를 만드는 방법을 생각해보자. API는 좁은 의미로 보자면 클라이언트 프로그램과 서버 프로그램의 상호 규약(Interface)이다.   얼핏 생각해보면 문서에 주절이 주절이 규약을 정해놓고 그것대로 구현되어 있으면 되는거 아닌가?  당연하다.  하지만 우리는 모두가 공감하는 방법으로 개발하길 희망한다.

이러한 희망속에 90년대 말에 SOAP이라는 심플하면서도 절대로 심플하지 않은 개발 방식이 나왔다.  W3C에서 업체의 힘을 빌어 밀어부쳐볼려고 했던 이 규약은 그러나 “심플하지 않다” 라는 절대적 사실 하나만으로 폭망했다.  실제로 진행했던 프로젝트 가운데 하나에서 성능 테스트를 했을 때, RESTful 방식 대비 최소 10배 이상 느린 것을 확인했다.  이후에 다시는 SOAP을 사용하자는 이야기를 하지도 않았고 듣지도 못했다.

저는 RESTful 방식으로 개발합니다.

흠.

요즘 SWE 면접 질문에 대한 답으로 지원자가 흔하게 이야기하는 답이다. 정말?

RESTfulREpresentational State Transfer ful

제대로 파고 들어가볼까?

RESTful, 제대로 하시는거죠?

대부분은 잘 한다고 이야기한다. 하지만 이야기를 하다보면 그리 잘 하는지에 대해서는 정말 ??? 를 두고 싶다.  대부분 RESTful을 한다고 했을 때

  • SOAP과 같은 RPC 형태가 아닌 URI Template의 형태로 요청을 보내고 받는다.
  • 물론 응답은 항상 JSON으로 API 응답을 내려야겠죠?
  • GET을 조회용으로 사용하고, “추가/변경”은 POST/PUT을 사용할려고 한다. 하지만 실제로는 글쎄… 아마 addXYZ 혹은 updateXYZ 등으로 하지 않을까? 물론 POST/PUT을 사용할 것이다. 왜 사용해야만 하는지는 물론 아실테고…

여기에서 더 있을까? 사실 내가 이 수준이었다. 공부하기 전에는. 그래서 사람은 공부를 해야하는 모양이다.

자, 그럼 정말 RESTful이라는 것이 지향하는 것이 뭘까 좀 더 살펴보자.

URI(Resource)/HTTP(Method)/Hypermedia(Link)

사실 RESTful이라는 이야기는 많이 들어봤지만 이 이야기는 전에 들어보지 못했다. RESTful WebService/API 구성과 관련해서 Leonard Richardson이라는 분이 이야기한 개념으로 아래 그림을 통해 살펴보면 빠르게 이해할 수 있다.

RESTful-Stack

API를 통해 우리가 클라이언트와 서버는 대화한다. API를 좁은 범위로 생각해보자.  그럼 API는 특정 리소스 혹은 데이터를 다룬다.  따라서 RESTful 방식에서 API를 설계할 때 URI가 이 리소스를 나타내도록 해야한다. 리소스를 물리적인 것도 될 수 있고, 추상적인 대상도 될 수 있다. 따라서 우리가 비즈니스를 통해 다룰려고 하는 대상 업무를 이런 리소스를 가지고 정의하는게 일반적인 접근일 것이다.

리소스를 다루는 가장 대표적인 방법이 C/R/U/D이다.  RESTful에서는 이것을 HTTP Method를 가지고 정의하도록 권고한다.  CRUD에 일반적으로 대응되는 HTTP Method는 아래와 같다.

  • Create – POST
  • Read or list – GET
  • Update – PUT or POST
  • Delete – DELETE

그런데 왜 권고일까?  HTTP 프로토콜에서 메소드가 의미 자체가 이런 Operation의 의미를 이미 가지고 있고 이미 개발자들 사이에 널리 퍼진 공감대이다.  또한 URI 혹은 다른 것으로 정의한다는 것 자체가 우리가 싫어하는 “중복”일 수 있다. 굳이 다른 토큰이나 키워드를 이를 위해 정의할 필요가 없다.

여기까지가 앞서 언급한 대충 우리가 아는 RESTful 방식이다.  중요하게 기억해야할 사항은 URI는 반드시 리소스를 나타내도록 해야한다.  “리소스”가 중요한 이유는 따로 아래 마이크로서비스 아키텍처 모델에서 좀 더 설명하도록 한다.

마지막 단계에 있는 “멀티미디어”는 그럼 당췌 뭘까? 우리가 구현하는 앱은 리소스를 가지고 CRUD만 하는게 다인가? 그게 목적이라면 그냥 DB에서 SQL 문장 돌리면 되지 굳이 이런 앱을 만들 필요는 없지 않을까?  의미있는 특정 동작을 하기 위함이다.  이 동작은 “리소스의 상태 변경(Transition)“으로 정의된다.  그럼 Transition이랑 멀티미디어랑 어떤 연관이 있을까?

HTML 문서에서 흔히 볼 수 있는 것이 바로 a href 링크다.  문서를 정보의 한 형태라고 가정해보자.  우리가 정보를 얻기 위해 한 문서에서 다른 문서로 사람은 링크를 클릭하면서 자신의 목적을 취한다.  이걸 기계적 관점에서 역으로 해석하면 사람이 보고 있는 문서는 “비즈니스의 상태“를 의미하며 링크를 클릭해서 다른 문서로 이동하는 것이 Transition이다. 한 문서는 다른 문서로 이동할 수 있는 경로를 a href 링크를 통해 사전에 정의한다. 비즈니스의 상태 역시 Transition할 수 있는 상태가 정해져있다.

API 호출은 앱이 처리하는 비즈니스의 상태를 A라는 상태에서 B라는 상태로 변경시킨다. 그리고 B 상태에서 이동될 수 있는 다른 상태 정보를 응답을 통해 알려준다.  이 응답은 현재 상태를 통해 앱이 다음 단계에서 할 수 있는 기능들이 뭔지를 알려준다.  아래 코드 예제와 같이 응답에서 이동 가능한 URI 정보들이 포함된다고 보면 된다.

<dap:link rel="http://relations.restbucks.com/payment" 
          mediaType="application/vnd.restbucks+xml" 
          uri="http://restbucks.com/order/1" />

Richardson Maturity Model

위의 계층 구조에서 바탕으로 우리가 현재 구현하는 RESTful 이라는 수준을 점검해 볼 수 있다. Richardson님이 위 스택을 기준으로 아래와 같이 단계를 구분해놓았다.

  • Level 0 – 하나의 URI을 가지고, 하나의 http method(mainly POST)를 사용한다. 내용에 대한 구분은 XML을 Payload로 사용해서 요청을 구분하는 방식을 취한다.  모든걸 하나의 리소스를 가지고 처리한다.
  • Level 1 – 다양한 URI를 사용하지만 http method((mainly GET)는 하나만 사용한다.  그나마 URI를 통해 요청이나 파라미터를 명시한다.
  • Level 2 – URI도 다양하게 사용하고, http method 역시 용도에 맞게 사용한다.  우리가 웹서비스라고 하는걸 이야기해보세요 하면 다 이 수준에서 이야기를 한다.  대체적으로 CRUD를 하는 웹서비스들이 여기에 포함되며, 대표적인 예는 Amazon S3가 여기에 포함된다.
  • Level 3 – Hypermedia를 활용해서 어플리케이션 상태 변화를 관리하는 수준을 말한다.  어플리케이션의 한 상태에서 다른 상태로의 이동 혹은 이동 가능한 상태를 링크를 통해 제시한다.

이 글을 읽고 있는 분들도 자신이 개발하는 RESTful API가 JSON을 응답으로 제고하는 수준에 머무는 정도인지 아니면 제대로 RESTful을 지향하는지 다시 한번 점검해보는 기회가 됐으면 좋겠다.

마이크로서비스 아키텍쳐와 함께 생각해보자.

앞서 언급한 사항 가운데 중요하게 기억해야할 사항은 URI는 반드시 리소스를 나타내도록 해야한다.  시스템을 Monolithic 방식이 아닌 마이크로서비스 아키텍쳐를 지향한다면 반드시 이 규칙을 따라야 한다.  RESTful 이야기를 하다가 왠 갑자기 뜬금없는 마이크로서비스 아키텍쳐라고?

마이크로서비스 아키텍쳐의 개념을 곰곰히 다시 생각해보자.  이 구조에서는 단위 업무들을 독립화시키고, 이들 업무가 서로 연합(Federation)될 수 있도록 한다.  연합에서 우리는 상대방이 어떤 시스템인지는 굳이 알 필요없다.  단지 연동 시스템이 우리가 필요한 API 규격을 준수하고 이를 제공한다라는 것을 알면 그만이다.  이를 구현하는 최적의 프로토콜이 바로 RESTful이라는 것은 두말할 필요가 없다.

RESTful-toMicroservice

그렇기 때문에 마이크로서비스 아키텍쳐 세상에서는 “리소스”가 시스템의 핵심이다. 해당 리소스에 대한 요청이 증가하거나 업무 로직의 부하가 커진다면 “리소스”를 처리하는 단위만 별도로 분리하는 방식으로 시스템을 확장한다.

물론 이와 같이 마이크로서비스로의 확장에서 서비스 시스템이 늘어나게 됐을 때 이를 Discovery할 수 있는 방안이 필요하다.  이 부분은 나중에 추가적으로 정리해보도록 하겠다.

일단 정리는 여기에서 마무리. ^^;

그래서 RESTful이라는게 뭐라는 건데?

아닌 것부터 이야기하면 JSON으로 응답을 내려주는거… 이건 아니다.

RESTful 방식으로 본인이 개발한다면

  • WHAT – (비즈니스 관점에서) 다룰 대상을 명확하게 인지하고 있고,
  • HOW – 어떤 상호작용들이 표준적인 방법(HTTP Method)으로 정의하였으며,
  • STATE – 일련의 상호작용으로 시작에서 끝에 이르는 상태 변화 과정을 거친다.

라는 것을 “각성“하라는 것이다.

최초의 SOAP을 필두로 웹서비스라는 기술적 개념이 태동했을 때 우리는 말 그대로 기술적인 관점의 가능성을 봤다. 대두되는 기술을 다시 한번 들여다봤을 때 그 안에서 우리가 한단계 더 나아가기 위해서는 기술을 어떤 관점에서 활용하고 적용하는게 올바른지를 꾸준히 생각해봐야 할 것 같다.

– 끝 –

개발자의 missing commitments에 대해.

한다고 한것들(commitments)을 못했을 때(missing)의 것들을 어떻게 받아들여야 할지 애매해서 좀 찾아봤는데 재미있는 링크를 찾았다.

Agile team missing commitments regularly and complaining about no trust

어떻게 보면 개발자들이 똘똘 뭉쳐서 에자일이라는 것을 해석해버리면 이런 식도 될 수 있겠구나 하는 생각도 든다.

질문의 요지는 스프린트 4개를 하는 동안 개발자들이 40~60% 정도를 빵구를 내고 있다. 하지만 MVP를 만들어낼때까지 4개의 스프린트가 남았음에도 불구하고 개발자들은 현재 상황에 대해 안이하다.  개발자들은 스토리가 거의 완성됐고 좀만 더 하면 된다고 이야기를 한다. 하지만 스토리는 완성되지 않았기 때문에 결국 스토리 완성은 다음 스프린트로 연장됐다. 따라서 계속 MVP에서 보여줘야 할 내용들은 줄어들 수 밖에는 없다. 스펙을 짤라야 하기 때문에.

럼 가장 많은 Vote를 받은 답변의 요지를 정리해본다. 답변에서는 3개의 Noop을 이야기한다.

4개 스프린트에서 스토리는 어느 정도(40 ~ 60%) 진행이 된거다.

하다만 스토리는 완성되지 않은 스토리다. 따라서 진행율은 0%다.  

스토리를 임의대로 자르고 붙혀서 이만큼 했다라고 이야기하는 것 자체가 작위적이다.  스토리를 정할 때 우리는 완결성을 부여한다.  “주어진 조건 아레서 어떤어떤 방식으로 동작해야하고 그 기능은 이렇게 검증한다” 라고 스토리를 써내려간다. 그런데 조건을 맘대로 변경하고, 동작을 변경하고, 검증 방식을 임의대로 자르고 붙혀서 우리는 잘 하고 있어.. 라고 이야기하는건 옳지 않다.  특히 정직할 수 밖에 없는 기계를 대상으로 작업하는 개발자가 이런 기계적이지도 못한 자세를 보인다는건 극히 잘못된 거다.

일은 거의 다 되어간다.

완결되지 않은 일은 백로그에 있는 다른 일들과 동급이다.  다만 다음 스프린트에서 높은 우선 순위를 가질 뿐

에자일 개발에서 스프린트는 백로그에 있는 것들 가운데서 이번 스프린트에서 해야할 일들을 뽑아서 진행해야한다.  지난 스프린트에서 다 못한 일들을 스프린트에 스프린트를 걸치게 하면서 개발하는건 제대로 된 에자일 프로세스가 아니다.  따라서 완결하지 못했다면 해당 스프린트에서 한 일은 없는 것이다. 단정적인 표현이지만 이것이 맞는 이야기다.  만약 이 규칙을 지키지 않는다면 에자일을 하는게 아니다.

에자일 개발 프로세스에서 스프린트는 중요한 의미를 갖는다.  한 스프린트는 집중해서 처리할 일들을 집중해서 처리하자는 암묵적 규칙 아래서 운영된다.  따라서 이 스프린트에 진행할 일들을 선정했다면 개발팀은 온전히 그 일에 집중해야 한다.  스프린트를 진행중인데 다른 일들이 급하다고 치고 들어온다면?  원칙에 따르면 “이게 급해요!!” 라고 치고 들어온 일들은 당연히 하면 안된다. 온전히 백로그에 잘 모셔둬야 한다. 스프린트에 집중헤야 할 일에 집중하는 것.  그것이 에자일에서 강조하는 스프린트의 운영 규칙이다.

누구나 한번쯤 응급실에 가본 경험이 있을 것이다. 다른 사람의 아픔보다는 자신의 아픔을 의사가 간호사가 먼저 돌봐주어야 한다고 우긴다.  고함 소리도 들리고 왜 의사가 오지  않는지 깽판을 치기도 한다.  하지만 의사는 자신의 우선 순위에 따라 움직이고 또 그래야만 한다.  같은 공간에 있지만 이미 치료를 시작한 환자가 있다면 그 환자의 치료가 끝날때까지 기다려야 한다.  당신이 죽을 정도라면 아마 응급실에 들어오자마자 의사가 바로 당신에게 달려들 것이다.

스프린트의 운영 역시 이와 유사한 측면이 있다. 스프린트를 통해 진행하기로 결정된 사항들에 대해 개발팀(응급실의 의사)는 이해 관계자들의 이해를 바란다. 이해 관계자들 역시 자신의 고통을 백로그에 담아두면서 기다림의 시간을 갖는다.  따라서 스프린트를 진행하는 개발자들은 해야할 일을 스토리를 통해 명확하게 정의해야 한다. 그리고 그 일의 완성을 위해 최선을 노력을 해야한다. 시간을 가지고 장난질을 하면 안된다.

이유가 뭐냐고 물어보면 개발팀을 못믿는거냐고 되려 받아친다.

서로 니 잘못이니 내 잘못이니 따지면 폭망이다. 왜 이렇게 예측(Planning)이 개판인지 물어보는게 바람직하다.

서로 잘잘못을 따지는 전투 행위로 들어가면 남는 건 상처밖에 없다.  상호 신뢰는 모든 일의 근간이다. 맞던 틀리던 험단과 힐란이 난무하면 신뢰는 무너진다. 결국에는 폭망에 이른다. 일을 시작했다면 되게 해야한다. 비난보다는 건설적인 견지에서 현상을 살펴볼 필요가 있다.  이 문제의 건설적인 관점은 왜 스토리가 스프린트에 완성되지 않는지를 따져보는 것이다. 특히 한번이 아닌 4번이나 제대로 스토리를 완성시키지 못했다는 사실에 집중해야 한다.

반복적으로 스프린트의 스토리를 완결하지 못했다는 것은 백로그에 존재하는 스토리의 규모를 제대로 예상하지 못한다는 것이다. 전후 관계가 명확한 스토리가 이런 꼬라지를 보인다면 더욱 더 개발 기간의 예측이 올바르지 못하다는 것을 뜻한다.   이 사실에 개발팀 공감해야 한다. 이 공감을 바탕으로 제대로 된 플래닝을 하기 위해 필요한 것이 뭔지를 고민해야 한다. 예를 들어 새로운 데이터베이스를 사용하기 때문에 혹은 새로운 언어를 사용해야 하기 때문일 수도 있다.  그렇다면 해당 분야의 고수를 초빙해서 플래닝에 도움을 요청하는 것도 좋은 대안이 될 수 있다.  만약 조직의 프로세스 혹은 권한 등이 문제라면 이를 잘 아는 주요 인사를 플래닝에 함께 참석시켜 예측을 해보는 것도 다른 방편이 될 수 있다.

가장 중요한 점은 최선을 다한 결과임에도 불구하고 동일한 문제가 반복적으로 들어난다면 열린 마음으로 이를 직시해야 하는 것이다. “믿고 맡겨주세요”와 같은 허왕된 문구는 정치인들이나 하난 말장난이다.  프로페셔널의 세계에서 문제는 객곽적인 사실로 보고 판단해야 한다. 그리고 그 안에서 개선할 부분이 있다면 인정하고 고쳐나가면 된다.

누구도 실수를 할 수 있다. 하지만 두번 실수를 하지 않는 것이 바로 프로다.

.gitignore 파일을 깔끔하게~

협업하는 다른 친구에게 pull request를 보냈다가 알게된 팁인데 꿀팁인 것 같아서 정리해둔다.

보통 .gitignore 파일은 프로젝트 빌드 혹은 작업 과정에서 생기는 부산물들을 굳이 git에 포함시킬 필요가 없는 파일들 혹은 디렉토리들을 제외시키기 위해서 사용된다. 이런 파일들 가운데 내가 종종 포함시키는 패턴들이 IDE와 관련된 파일들이었다.  하지만 IDE라는건 개인적인 취향에 따라 다름이 있다. 사람마다 서로 다른 IDE에 대한 부분을 .gitignore 파일에 포함시키다보면 .gitignore 파일이 두서없이 변경되기 마련이다. 내가 쓰는 IDE를 저 친구가 안쓴다고 비난할 일은 아니지않은가?

그럼 이 부분을 다른 사람을 방해하지 않고 처리할 수 있을까?  정답은 global .gitignore 파일을 사용하는 방법이다. 사용 방법은 간단히 다음 링크를 참고하면 된다.

https://help.github.com/articles/ignoring-files/#create-a-global-gitignore

내용을 추려서 정리하면

  • 사용자 홈 디렉토리에 ~/.gitignore 파일을 만들어둔다. (파일 이름은 꼭 이게 아니어도 된다.)
  • 이 파일에 내가 작업하는 내 IDE 혹은 생성될 수 있는 불필요한 파일들의 패턴들을 기록해둔다.

gitignore

  • 아래 명령을 한번 돌려준다.  그럼 이후에 다른 프로젝트를 동일한 IDE를 가지고 작업하더라도 해당 파일들은 git 대상에 포함되지 않는다.
$ git config --global core.excludesfile ~/.gitignore

이 방식을 사용하면 프로젝트에 포함된 .gitignore 파일은 프로젝트에 관련되서 생기는 불필요한 파일들만 관리할 수 있기 때문에 프로젝트를 온전히 프로젝트 용도로만 관리할 수 있게 된다.  작지만 협업자들을 위한 배려 차원에서 하나씩은 설정해둘만하다.

Spring Data JPA를 활용한 DAO를 바꿔보자.

부트 이전에 스프링에서 데이터베이스를 그래도 다른 사람이 쓰는 만큼 쓸려면 MyBatis를 써줘야했다.

MyBatis를 한번이라도 써본 사람이라면 알겠지만 복잡하다. 스프링 XML 설정의 복잡도에 MyBatis의 복잡도를 더하면 상당히 헬 수준으로 올라간다. 단순 목록 하나만 가져오는데 MyBatis를 쓰는건 형식주의에 빠진 불합리의 최상급이었다. 되려 JDBC를 가져다가 prepareStatement에 Bind 변수만 사용하는 것이 오히려 손쉽게 직관적일 수 있다. 글을 읽는 분들중에 Bind 변수를 쓴다는 말을 이해하지 못하시는 분들은 이해의 수고를 덜기 위해 그냥 MyBatis를 쓰는게 정신 건강에 좋다. 하지만 적극 추천한다.

부트(Springboot)를 공부하면서 대부분 Annotation을 가지고 처리하는데 데이터베이스에 대한 접근도 비슷한 방법이 없을까 싶어서 잠깐 찾아봤었지만 역시… JPA 라는 걸 이미 많이 사용하고 있었다.  그리고 MyBatis처럼 설정 그까이게 거의 없다.  테이블 혹은 쿼리와 맵핑되는 설정 몇 가지만으로도 바로 이 기능으로 데이터베이스에서 자료를 읽어내거나 저장할 수 있다.

개인적으로 데이터베이스를 엑세스하는 쿼리를 복잡하게 가져가는건 별로 좋은 방법이 아니라고 굳게 믿는다. 폄하하자면 프로그래밍을 못짜는 개발자들이 논리의 부재를 쿼리로 입막음하려는 경향이 있다.  정말 잘못된 자세다. 정신 머리를 고쳐먹고 쿼리는 최대한 심플하게 작성하고 로직은 프로그램으로 대응하는 버릇을 들이도록 정신을 개조해라.

시작하기

개발을 할려면 먼저 이걸 사용하기 위한 준비부터 해야한다. 메이븐을 개발 환경으로 사용하기 때문에 아래 설정을 반영하면 된다. 데이터베이스 종류에 따라 해당 데이터베이스에 대한 추가적인 의존성을 가져가야한다는건 이미 알고 있으리라 생각한다. 그냥 귀찮으니까 MySQL에 대한 설정 부분만 Copy & Paste하기 좋게 추가해둔다.  그리고 앞으로 설명은 전부 MySQL을 기반으로 진행한다.

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jpa</artifactId>
  <version>1.10.1.RELEASE</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.6</version>
  <scrope>runtime</scope>
</dependency>

버전에 관련된 사항은 해당 프로젝트 페이지를 통해 확인해보는게 좋다.

라이브러리는 준비됐으니까 이제 데이터베이스를 셋업해봐야겠다. 데이터베이스 셋업은 여기에서 설명하는 개발 주제랑은 무관하니까 일단 스킵. 하지만 개발자라면 꼭 자신의 로컬에 데이터베이스 하나쯤은 실행시켜서 쿼리 도구로 실행해봐야겠다.  그럼 이게 준비됐다라는 전제면 접속을 위한 설정 정보를 코드에 반영해둬야 한다. 이건 보통 application.properties 파일에 다음과 같이 잡아둔다.

spring.datasource.url=jdbc:mysql://localhost:3307/db_name
spring.datasource.username=admin
spring.datasource.password=********
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

응, 근데 JPA가 뭐지?

시작을 하긴 했지만 JPA가 뭐의 약자인지부터 알고 가자. JPA는 Java Persistence API의 약자. 2000년대 초반에 나왔다가 별로 빛을 보지 못했던 것 같다. 스프링 진영에서 아마 iBatis or MyBatis 류의 약진으로 뜰 기회가 아예 없었을 것 같다. 더구나 하나만 고집하는 한국 환경에서 이미 대세가 된 iBatis를 넘어서는 것이 불가능하다.

내가 이해하는 JPA의 컨셉은 데이터베이스에 존재하는 한 모델을 자바의 한 객체로 mapping하는데 목적이 있다.  이 방향성은 단방향성이 아니라 양방향성이다. 즉 코드상에 존재하는 객체는 마찬가지로 데이터베이스에서도 존재해야 한다.  이를 매개하는 존재가 ID 필드이다.  ID 필드는 특정 클래스/테이블의 Uniqueness를 보장하기 위해 사용된다.

jpa-concept

이 ID 필드는 클래스(객체)에 정의되지만 반대 급부로 테이블에도 마찬가지로 해당 필드가 정의되어야 한다. 다만 테이블과 다른 점은 객체를 통해 관리할 정보는 필요한 정보들에 국한될 수 있다.  즉 불필요한 정보는 굳이 객체로 관리할 필요가 없고, 따라서 클래스의 필드로 정의할 필요가 없다. 하지만 테이블 구조에서 Not null 필드 혹은 Constraint에 보호되는 필드라면 적절한 장치가 필요하긴 할 것이다.

단일 테이블부터

테이블 하나에 대한 처리는 정말 쉽다.  하지만 몇가지 지켜야 할 규칙이 있다.  우리가 다뤄야 할 테이블의 이름을 SAMPLE_TABLE이라고 가정하자.

create table SAMPLE_TABLE (
  id int not null autoincrement,
  sample_name varchar(100) not null,
  code varchar(20) not null,
  create_datetime datetime not null,
  primary key(id)
);
  • 테이블의 이름과 동일한 모델 클래스를 만든다. 따라서 클래스는 헝가리안 표기법(Hungarian Notation)을 따라 SampleTable 이라는 이름이어야 한다. 테이블 혹은 필드의 이름이 Underbar(‘_’)로 구분되면 각 단어의 처음이 대문자로 표현되어야 한다. 클래스의 이름을 통해 실제 테이블을 mapping하게 된다.
  • 테이블의 모든 컬럼에 대응하는 모든 필드를 만들 필요는 없으며 ID 필드를 포함한 다뤄야 할 필드들을 정의하면 된다. 쓸데없는 것까지 다룰 필요는 없다.
  • 테이블의 컬럼에 대응하는 필드의 이름은 카멜 표기법(Carmel Notation)을 따라 표기한다.
  • Serialize/Deserialize할 수 있어야 하기 때문에 테이블의 각 필드들에 대해 Getter/Setter를 만들어둬야 한다.  일일히 하기에 귀찮다. lombok 라이브러리를 사용하면 각 필드들에 @Getter @Setter Annotation을 사용하거나 전체 클래스에 대해 적용할려면 @Data Annotation을 적용하면 된다.
  • 스프링에서 해당 클래스를 참조할 수 있도록 @Entity라는 Annotation을 적용한다.
@Entity
@Data
public class SampleTable {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private long id;

    private String sampleName;
    private String code;
}

한 두개 팁을 확인해보자.

  • Entity 클래스와 테이블의 이름이 다른 경우 – Entity 클래스와 테이블의 이름이 틀려지면 @Table(name=”SAMPLE_TABLE”)을 지정한다.
  • 컬럼의 이름과 다른 필드의 이름을 부여하고 싶은 경우 – @Column(name = “COLUMN_NAME”) 어노테이션을 통해 해결한다.
  • 크기가 큰 컬럼들 – 테이블의 컬럼 타입이 CLOB 혹은 BLOB 같은 타입의 경우 자동으로 큰 크기를 처리할 수 없다.  이 경우에 mapping되는 컬럼이  해당 타입인지 여부를 @Clob 혹은 @Blob 같은 어노테이션을 통해 따로 지정해줘야 한다. (생각해보면 당연하다. CLOB/BLOB을 JDBC 혹은 Pro*C로 처리할 때 단순 Variable Binding을 가지고 처리할 수 없다는걸 아는 사람이라면. 그리고 왜 그래야만 하는지도.)

 

CRUD

데이터베이스에 대한 @Entity와 @Data 어노테이션을 활용해 데이터베이스 테이블과 클래스의 맵핑이 완료됐다. 그럼 실제로 동작이 되도록 이 두개를 Repository를 통해 이어주면 된다. 간단하다.

public interface YourSampleTableRepository extends CrudRepository<SampleTable, Long> {
    Layout findOne(Long id);
}

JPA에서 기본 제공해주는 CrudRepository 인터페이스를 상속해서 새로운 당신의 Repository를 만들면 된다. 그럼 기본은 전부할 수 있다. CrudRepository가 뭐하는건지 궁금하지 않은가? 이 인터페이스는 아래와 같이 생겨먹었다.

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
    <S extends T> S save(S var1);
    <S extends T> Iterable<S> save(Iterable<S> var1);

    T findOne(ID var1);
    Iterable<T> findAll();
    Iterable<T> findAll(Iterable<ID> var1);

    boolean exists(ID var1);
    long count();

    void delete(ID var1);
    void delete(T var1);
    void delete(Iterable<? extends T> var1);
    void deleteAll();
}

인터페이스의 생겨먹은 모습에서 알 수 있듯이 대부분의 동작들을 지원한다. (보기 편이를 위해서 약간 순서를 조정하긴 했다.) 그런데 흔하게 보다보면 CrudRepository라는 것 이외에 JpaRepository도 흔하게 사용한다. 아마도 처음 접하는 예제의 종류가 틀려서 그런가 싶기도 하지만… CrudRepository와 JpaRepository의 차이점은 여기 링크에서 잘 설명하고 있다. 간단히 요약하자면

  • JpaRepository는 CrudRepository의 손자뻘 인터페이스이다.
  • JpaRepository는 Crud에 비해 게시판 만들기에 용이한 Paging개념과 배치 작업 모드를 지원한다.
  • 하지만 다 할 수 있다고 다 이걸로 쓰는건 아니다. 언제나 강조하지만 닭잡는데 소잡는 칼을 쓸 필요는 없지않은가?

다른 길로 빠지긴 했지만 일단 기본으로 쓸려면 그냥 CrudRepository를 기본 칼로 쓰는게 좋다. 상황에 따라 적절한 도구를 사용하고 있는가 혹은 사용할 수 있는 도구를 고를 수 있는 자질이 되는가를 스스로 질문해보자. 답변을 할 수 있는 역량이 본인에게 있다면 다행이다.

Queries

만들고, 변경하고, 지우고 있는지를 확인하고 등등 기본적인 동작이 되는건 대강 확인했다. 그럼 특정 상황에 맞는 조회 문장을 만드는 건 Repository 인터페이스에 조회용 메소드를 정의함으로써 이뤄진다. JPA 환경에서 특정 조건을 만족하는 쿼리를 수행하는 방법은 크게 아래와 같은 가지수로 나뉜다. (상세한 설정에 대한 도움말은 역시 공식 홈페이지에 상세하게 적혀있다.)

  • Query creation from method – 쿼리를 유추할 수 있는 메소드의 이름을 통해 쿼리를 정의한다.  일반적인 규칙은 findBy뒤에 조건이 되는 필드의 이름을 And 혹은 Or 조건을 섞어 작성한다. 실행에 필요한 값들은 언급된 매개 변수의 순서에 맞춰서 작성한다.
public interface UserRepository extends Repository<User, Long> {

  List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

이 쿼리가 대강 실행되면 대강 아래와 같은 쿼리의 형식으로 실행된다.

select u from User u where u.emailAddress = ?1 and u.lastname = ?2
  • NamedQuery annotation – 설정을 xml 파일에 두는 경우, 해당 파일의 이름은 orm.xml이어야 한다. 그게 아니면 테이블에 맵핑된 클래스(이걸 Domain Class라고 부르는군.)에 @NamedQuery라는 annotation을 사용해서 실행될 쿼리를 아래 예제처럼 적어주면 된다.
<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>
@Entity
@NamedQuery(name = "User.findByEmailAddress", query = "select u from User u where u.emailAddress = ?1")
public class User {
    ...
}

public interface UserRepository extends Repository<User, Long> {
  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}
  • Query annotation – 가장 흔하게 사용하는 방법이다. Repository의 조회 메소드에 직접 실행될 쿼리를 적어준다.
public interface UserRepository extends JpaRepository<User, Long> {
  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

JPA는 조건문에 들어가는 파라미터의 위치를 메소드의 파라미터 위치로부터 유추한다. 즉 ?1 이라는 표시된 곳에 첫번째 파라미터의 값이 반영된다. 쿼리가 복잡하다면 이름을 참조하는 좀 더 알아보기 쉬운 방식으로 사용하는게 좋다. 이름을 참조할려면 인터페이스 메소드의 각 파라미터 앞에 @Param 이라는 추가 annotation을 사용해서 참조 가능한 이름을 주고 쿼리에서 이 이름을 사용하면 된다.

public interface UserRepository extends JpaRepository<User, Long> {
  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);
}

예제에서 보면 쿼리에 firstname, lastname이라는 변수를 참조했고, 참조된 변수는 @Param annotation으로 메소드에 함께 선언된 것을 확인할 수 있다.  복잡하다면 이렇게 해주는게 읽는 사람을 위한 배려다.

그래도 테이블 두개는 조인할 수 있어야지

테이블 한개에 대한 처리는 지금까지의 설명으로 충분하다. 하지만 2개 이상의 테이블을 조인해서 뭔가를 추출하는 경우라면 어떻게 해야할까?  쿼리상으로는 간단히 조인을 걸어서 결과를 확인하는 아주 간단한 거다. JPA 방식에서 사용할려면 좀 더 터치가 필요하다.  간단히 다음과 같은 테이블 구조가 있다고 하자.

joined

여기에서 사용자쪽의 정보를 바탕으로 그룹에 대한 정보를 가져올려고 한다면


select user.userid, password, group.groupid, ... from user join group on user.group_id = group.group_id

와 같이 하면 된다.  이걸 JPA 기반에서 처리한다면


@Entity
@Data
public class User {
    ...
    @ManyToOne
    @JoinTable(name="USER_GROUP")
    Group group;
};

@Entity
@Data
public class Group {
};

흠.. 이렇게 하면 될까? 실제로 해보질 않아서 모르겠다.

JPQL이라는 걸 JPA 내부적으로 사용한다고 한다. 하지만 파보질 않아서 잘 모르겠네.

상세한 내용은 다음 링크에서 도움을 나중에 받을 수 있을 것 같다.

  • https://en.wikibooks.org/wiki/Java_Persistence/Querying
  • http://www.objectdb.com/java/jpa/query/jpql/from

일단 미완.

Tips

MySQL의 경우에 테이블 이름을 Underscore(_)로 구분하는게 아니라 대문자 형식(ex: MyTable)으로 작성하는 경우가 있다. 이 경우에는 Entity의 테이블 이름을 MyTable로 주면 Spring JPA에서 자동으로 my_table로 변경해버려서 테이블을 찾지 못한다고 이야기하는 경우가 있다.  이때는 Naming Strategy를 아래와 같이 변경해주면 된다.

spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

한참을 헤메긴 했지만 알고 있다면 좋을 것 같다.

5월에 읽은 책 – 김훈 작가의 “라면을 끓이며”

한달에 만화책과 무협지를 빼고 뭐라도 두 권은 읽자라고 시작해서 처음으로 읽기를 마친 책.  읽기를 시작한지는 한달쯤 되었던 것 같긴 하지만 최근들어서 텍스트를 읽는 것이 어렵게 다가온 적이 요즘만하게 없었던 것 같다. 오래 걸리고 또 의미를 삼키기 어렵다.

김훈 산문집 – 라면을 끓이며

 

굳이 독후 감상문을 적을 필요는 없겠다.

본래 스스로 그러한 것들을 향하여 나는 오랫동안 중언부언하였다. 나는 쓸 수 없는 것들을 쓸 수 있는 것으로 잘못 알고 헛된 것들을 지껄다. 간절해서 쓴 것들도 모두 시간에 쓸려서 바래고 말하고자 하는 것은 늘 말 밖에 있었다. 지극한 말은, 말의 굴레를 벗어난 곳에서 태어나는 것이다.

이제 함부로 내보낸 말과 글을 위추치는 일을 여생의 과업으로 삼되, 뉘우쳐도 돌이킬 수 없으니 슬프고 누추하다. 나는 사물과 직접 마주 대하려 한다.

비슷한 동경되는 마음이 있어서 옮겨봤다.

글에도 색이 있구나라는 것을 처음으로 알게해준 작가가 김훈 작가가 아닌가 생각든다.  이전에 남한 산성에서 글에도 맛이 있구나라는 걸 느꼈는데 기대했던 맛과는 다른 글의 맛을 이번 책에서는 느낄 수 있었다.  아마도 세월의 흐름과 산문의 특성이 있어서 그런게 아닐까 하는 어림없는 추측을 던진다.  집에 칼의 노래도 있었던 것 같은데 올해가 가기전에 이 책도 한번 경험해봐야겠다.

Amazing Lamda

Java 8에서 제대로 지원하는 stream과 람다(lamda)를 섞어서 쓰면 좋은데 람다가 코드를 어느 수준까지 줄여주는지를 한번 살펴보니 무시무시하다라는 생각이 든다.

public class OneLoginSimpleUserService {
    public UserAuthority processLogin(Authentication auth) {
        return new UserAuthority() {
            @Override
            public Collection<? extends Authority> getOwnedRoles() {
                return Arrays.asList(new Authority[]{ new Authority() {
                    @Override
                    public String toStringCode() {
                        return"ROLE_ADMIN";
                    }
                }});
            }
        };
    }
}

이걸 람다를 적용해서 고치면…

public class OneLoginSimpleUserService {
    public UserAuthority processLogin(Authentication auth) {
        return () -> Arrays.asList(new Authority[] { () -> "ROLE_ADMIN" });
    }
}

기존 자바의 인터페이스를 사용해서 불필요한 라인들이 딱 한줄의 return문으로 정리된다.

이걸 해석하는데 좀 시간이 필요하긴 한데, 얻을 수 있는게 참 많을 것 같다. 특히나 Interface를 활용해서 DI 기법으로 코드를 작성하는 방식이 더욱 더 각광받을 것 같다.

미국과 다르지 않은 한국 개발자 환경?

페북에 개발자의 한국 상황과 미국 상황을 비교한 슬라이드가 올라와서 또 열심히 까는구나 하는 생각이 들어서 봤더니, 왠걸?  이 친구는 많이 다르지 않다고 이야기를 하네. 헐…

살짝 이력을 살펴보니 LG좀 다니다가 라인좀 다니다가 MS로 이직한 친구네!!

좋은 회사(?)들을 잘 거쳐서 성공적으로 미쿡 회사에 안착을 한 모양이다.  근데 좋은 대기업만 다녀서 그런가 한국의 다른 개발자들에 대해서 잘 모르는 모양이다.(네이버나 카카오를 중견기업이라고 이야기하는 개발자는 근래에 처음보긴 했다.)

이런 친구들에게 현실을 이야기하면

하면 되는데 너는 왜 못해? 다 너 잘못임!!

ㅎㅎㅎ 간단히 그럼 현실을 살펴볼까?

누구나 다 공감하는 건 역시 돈이다. 일이라는게 일차적으로 생활을 영위하기 위함이니까. 과연 일반적인 한국의 개발자들의 벌이는 어떤지 함 살펴보자. 우리나라의 2015년 기준 소프트웨어 개발자 단가가 이러하다고 정부에서 이야기한다.

개발자단가

평균적인 개발자를 대강 경력 5, 6년차 정도라고 생각해보면, 기준이 “중급 기술자“인 것 같다.  표에서 확인해보니 한달 평균 임금이 470만원이 조금 안된다네. 흠… 470만원.  많이 준다고 치면 한달에 400정도 받고 이걸 연봉으로 환산해보면 대략 5,000만원쯤 되겠네.  잘쳐주는 회사의 신입으로 들어가면 3,000만원쯤으로 시작하니까 대략 1년에 400만원씩 올라야 하고 이정도면 연봉 인상률이 대략 10%가 되야지 말이 되는 이야기인 것 같군.

10% 연봉 인상율이면… 말이 안되게 능력자이면 모를까 대한민국을 살아가는 평균 개발자는 절대 이렇게 살 수 없다.

올라가는 연봉보다는 약값이 더 들어간다.  아니 약값보다는 어디가 하나 제대로 고장난다.

한국에서 개발자의 안정적인 처우를 보장하는 회사는 몇 개 회사정도밖에는 되지 않는다.  아마도 그 친구가 이야기했던 네이버나 카카오 혹은 최근에 뜨는 소셜커머스 업체 정도가 되어야 가능하지 않을까 싶다.  간단히 검색해보면 2015년 기준으로 대졸 초임이 3,500 이상이고 인센티브를 합치면 입사 첫해에 받는 금액이 4,000 만원 이상이다.  처음 차이가 1,000만원이겠지만 이 금액은 시간이 흐르면 흐를수록 더 벌어지게 된다.  당연이 이 정도의 금액이라면 생활하는데 거짐 불편함이 없다.

네이버, 카카오 직원이 아닌 대한민국의 일반적인 개발자들은 이 차이를 메우기 위해서 열심히 야근을 한다. 하지만 소위 말하는 이런 류의 회사들에 있는 일반적인 개발자들과의 차이는 시간이 가면 갈수록 벌어진다. 왜냐구?  PT 자료에서 미쿡 친구들과 사용하는 기술들이 크게 다르지 않다고 이야기를 했다.  그렇다. 이 회사들에서 사용하는 기술들에는 큰 차이가 없다. 이런 걸 챙기는 별도의 조직이 있을 뿐만 아니라 생각외로 이런 걸 할 시간이 좀 된다.  젠킨스,  CI, Git  그리고 docker와 같은 도구들을 활용하기 위한 환경들을 누군가가 앞서서 조사하고 셋업을 해놓으면 그 뒤를 따라가는 사람들은 큰 비용을 들이지 않더라도 써보고 느껴볼 수 있다.

하지만 SI 개발 현장에 있는 일반적인 개발자들에게 이런 기술들을 익혀서 사용할 수 없다. 턱없이 시간이 부족하다. 해야만 하는 코딩을 마무리하기에도 시간이 부족한데 이런 편이 기능이나 유틸들을 제대로 즐길 시간이 없다. 그리고 이런 기능들을 설치해서 활용해볼 만한 인프라 환경도 열악하다.  처음부터 끝까지 본인 혼자서 감당해야 한다.  물론 함께할 수 있는 동료가 있다면 이런 부담을 덜 수 있겠지만 그런 친구… 흔하지 않다.

최근에야 AWS와 같은 저렴한 클라우드 인프라가 있기 때문에 접근성이 좋아지긴 했지만 절대적 시간 부족은 어떻게 할 수 있는게 아니다.

이러다보면 닭이 먼저냐 달걀이 먼저냐에 대한 쓸데없는 논쟁으로 빠져들게 마련이다. 실력을 키울려면 본인이 알아서 해야하는거 아니냐와 시간이 없는데 어떻게 그런걸 하느냐와 같은… 일반 개발자들에게는 시간이 부족하고 삶이 있는 저녁은 그림의 떡이다.