도대체 뭐가 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와 같은 저렴한 클라우드 인프라가 있기 때문에 접근성이 좋아지긴 했지만 절대적 시간 부족은 어떻게 할 수 있는게 아니다.

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

 

 

 

Maven을 이용해서 신규 프로젝트 만들기

한땀한땀 손으로 Maven 프로젝트를 만드는 것도 의미있는 일이지만 귀찮다.

mvn archetype:generate -DinteractiveMode=false -DarchetypeArtifactId=maven-archetype-quickstart \
-DgroupId={project-packaging} -DartifactId={project-name}

와 같은 형태로 잡아주면 된다.
최근 개발은 Spring Boot를 많이 이용하기 때문에 여기에서 주로 쓸만한 archetype들을 나열해보면

  • maven-archetype-quickstart
  • spring-boot-sample-simple-archetype
  • spring-boot-sample-data-jpa-archetype
  • spring-boot-sample-actuator-log4j-archetype

Spring에서 사용할 수 있는 전체 Archetype 목록은 여기에서 확인 가능하다.  다만 Spring 기반으로 프로젝트를 생성시킬려면 기본 archetypeArtifactId 이외에 archetypeGroupId=org.springframework.boot 값을 추가로 줘야한다.

mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=org.springframework.boot -DarchetypeArtifactId={spring-archetype} \
-DgroupId={project-packaging} -DartifactId={project-name}

가장 대표적인 API 개발용 명령을 이용하는게 가장 깔끔하다.

mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=org.springframework.boot -DarchetypeArtifactId=spring-boot-sample-simple-archetype \
-DgroupId={project-packaging} -DartifactId={project-name}

그 다음에 생성된 pom.xml 파일의 spring-boot-starter-parent 의 버전을 1.3.3.RELEASE로 변경한다. CORS 지원과 몇가지 기능을 사용할려면 이 이상 버전을 사용하는게 좋다. 추가적으로 다음의 Dependency들을 pom.xml에 반영하면 즐거운 코딩 생활에 도움을 얻을 수 있다.

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.8</version>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.4</version>
    </dependency>
    <!-- for db programming with mysql -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.6</version>
      <scope>runtime</scope>
    </dependency>

한가지를 더 추가하자면 람다등을 사용할려면 자바 컴파일 환경을 1.8 이상으로 설정하는게 편하다. 다음 빌드 플러그인을 설정해두면 대부분의 IDE에서 인식한다.

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>

즐 코딩~

 

JUnit Parameterized Test – 반복 테스트를 하는 뻔한 방법

JUnit에서 조건값을 바꿔가면서 테스트를 해야하는 경우에 이 방법을 사용하는게 테스트 비용을 아끼는데 좋다.

@RunWith(Parameterized.class)
public class PrimeNumberCheckerTest {
    private int inputNumber;
    private boolean expectedResult;
    private PrimeNumberChecker checker;

    @Before
    public void setup() {
        checker = new PrimeNumberChecker();
    }

    @Parameterized.Parameters
    public static Collection<PrimeNumberValidationInput> primeNumbers() {
        return Arrays.asList(new PrimeNumberValidationInput[] {
                new PrimeNumberValidationInput(2, true),
                new PrimeNumberValidationInput(6, false)
        });
    }

    public PrimeNumberCheckerTest(PrimeNumberValidationInput input) {
        this.inputNumber = input.number;
        this.expectedResult = input.expectation;
    }

    @Test
    public void testCheckerValidatePrimeNumber() {
        assertThat(checker.validate(inputNumber), is(expectedResult));
    }
}

예제에서는 테스트를 위한 입력으로 PrimeNumberValidationInput이라는 클래스를 사용했다. 다른 책에서 예제를 인용할 때는 보통 Object[][]을 이용하는 경우가 많던데, 물론 간단한 수치 입력을 하는 경우에는 이 방법을 사용해도 크게 나쁘지는 않은 것 같다. 하지만 Object 변수를 사용한다는게 뭔가 테스트 코드의 품질을 떨어트리는 것만 같은 후질근함을 내보하기 때문에 별로 좋아보이지는 않는다. 더구나 코드의 의미 부여적인 측면에서 각 변수의 이름을 살려주는게 테스트의 맥락에서 좀 더 좋아보이기 때문에.

    public static class PrimeNumberValidationInput {
        public int number;
        public boolean expectation;

        public PrimeNumberValidationInput(int number, boolean expectation) {
            this.number = number;
            this.expectation = expectation;
        }
    }

이런 클래스 하나쯤 테스트 코드에 심어주면 간지나지 않을까 싶다. 테스트를 위해 투자하는거 넘 인색하지 말자. 테스트도 관리해야할 코드 가운데 하나니까 말이다.

Thunderdome – 개발자의 재미, 개발하기

2주일간의 출장을 마치고 복귀했다.

몇번 가지도 않았지만 뱅기타고 가는 출장에서는 풀어내야할 꺼리들이 한가득이었다.  그리고 시간도 부족했다.  일에 대한 압박감도 있고, 제대로 되지도 않는 영어로 이야기를 하자니… 출장가서 잠을 제대로 못자는건 단순히 시차 적응 실패가 가장 큰 원인이겠지만 이런 류의 스트레스도 한몫을 한다고 본다.

이번 출장은 좀 다른 목적을 가지고 있었다.  썬더돔(Thunderdome)이라는 회사 내부에서 하는 핵커톤 행사에 참가하고, Architecting Conference에 참가해서 배움을 넓히는 시간이었다. 한마디로 개발자스럽게 놀다가 오는 시간이랄까? 하지만 피곤한건 변하지 않는다.

thunderdome8-after

처음 시작하는 모습은 그럭저럭 괜찮긴 했지만, 해커톤 행사 하루만에 확연하게 거북목 증세를 보인다.  거북목을 피할려고 받침대랑 키보드랑 마우스도 바리바리 챙겨갔음에도 불구하고 이런 자세가 되는건 아무래도 병인 것 같다.

thunderdome8-before

이런 상태로 3일을 정말 열심히 코딩을 했던 것 같다.  물론 팀의 성과는 좋았다. 다만 내가 기여할려고 했던 부분은 제대로 돌리는데 실패했다. 너무 자신만만하게 문제를 대했던 것이다.   덕분에 내부 구조가 어떻게 돌아가고 뭐가 문제인지는 확실하게 알게 됐지만… 이걸 제대로 만드는건 3일동의 작업만으로는 택도 없다라는 진실을 깨우쳤다.

하지만 같이 간 다른 친구들이 정말 엄청나게 일을 잘 해줬다. 덕분에 원래 의도에 거의 부합하는 작품을 만들어냈다.  시연 시간에는 덕분에 사람들의 관심이 폭발해서 참가한 보람을 많이 찾을 수 있었다. 3일 동안 어떻게든 재미있게 개발을 했던 시간이 나름 유종의 미를 거두니 잠못잔 시간이 아깝게 느껴지지는 않았다.

thunderdome8_converted

나중에 제대로 만들어진다면 그때 제대로 기여할 수 있지 않을까 하는 생각이다.

LA에서 일정은 정말 피곤한 3일을 보내는 것으로 마무리했다. 한인 타운에서 한잔 술 털어넜고 같이 했던 친구들과 재미있는 이야기도 나누니 정말 좋았다.  이런 좋은 시간을 뒤로하고 뉴욕으로 컨퍼런스 들으러 이동했다.

혼자 LA에서 NYC로 이동하는 것도 또 다른 하나의 도전이다. 혼자 출장온 경험도 있긴 했지만 그때는 대한항공을 타고 오니 낯설음이라는게 적었다. 하지만 AA 항공타고 미국 국내선으로 이동하는 길에 한국인은 딱 나 혼자였다.  혼자서 이런 저런 일들을 다 처리해야한다고 생각하니 정말 깝깝스러웠다.

가장 큰 문제는 이야기할 사람이 없다는거.  회사에서는 그래도 업무적으로 이야기할 사람이라도 있었고, 지역 오피스를 배려하는 좋은 분위기가 있어서 이야기는 것도 훨씬 수월했다. 컨퍼런스에서 막다뜨린 사람들과 뭐라도 한마디 말을 섞어볼려고 해도 언어적인 장벽이 전혀 만만하지 않았다. 기술 언어를 하면 될거라고 생각했지만 사람들은 생활 영어를 한다는거.  이런게 또 다른 Pain Point!!

대강 알아들으면서 대강 이야기를 하면서 여기서도 4일간 고생을 했다.  2일은 Traning 세션이었고 2일은 Conference였는데 나름 의미는 있었다. 트레이닝 세션으로 들었던 Docker 관련 사항은 회사의 시스템 방향이기도 했기 때문에 짧게라도 Docker를 경험해볼 수 있는 좋은 시간이었다.   물론 따로 공부할려고 책도 읽는 중이었지만 단순히 눈으로 읽는 것과 실제로 손으로 타이핑을 쳐보는 건 완전히 다른 경험이니 말이다.

사진에 있는 스트라이브를 입으신 분이 주로 Docker를 이용한 Architecting에 대해 강의를 진행했는데 영국 억양이 상당히 인상적이었다.  삐쩍 말랐을 거라고 생각했는데 와중에 올챙이 배를 봤을 때는 웃음이 절로 나왔다.  역시 나이를 먹으면 나오는게 똥배구나…

다행이 맨하튼에 한인 타운이 있어서 밥은 정말 잘 먹었다. LA에서 먹는 버거나 피자 혹은 현지 음식들은 상당히 소금을 많이 쳐서 별로 땡기지 않았는데, 뉴욕은 음식이 짜지 않았다. 🙂 이것만으로도 정말 다행이었다.  물론 저녁은 항상 한식이었지만.

wp-1461337451094.jpeg

대도시의 백미는 역시 네온사인이었던 것 같다. 영화나 미드에서 종종 나오던 골목을 실제로 걸을 수 있었던 것만으로도 좋았던 경험이지 않았을까 싶다.