저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

문서

개발자이야기 2016.08.03 21:47

난 개발과 관련해서 문서 작성을 선호하는 편이다. 그렇다고 검수 산출물과 같은 형식적인 문서 작성을 선호하는 것은 아니다. 주로 다음을 위한 문서 작성을 선호한다.

  • 대화 재료
  • 미래에 시스템을 운영할 사람

대화 재료로서의 문서는 회의에서 효과가 크다. 기능 스펙, 설계 초안, 구현 아이디어, 목차 등을 정리한 문서는 대화를 시작하기 위한 좋은 재료가 된다. 이때 문서는 소통이 가능한 수준의 내용만 담으면 된다. 간단한 클래스 다이어그램이나 상태 다이어그램이어도 되고, 자유 형식의 사용자 스토리여도 된다. 또는 종이에 끄적인 내용을 복사해서 함께 봐도 되고, 화이트보드에 그려 놓은 내용을 사진으로 찍어 출력한 문서여도 된다. 이 문서를 바탕으로 대화를 하면서 내용을 발전시켜 나갈 수만 있으면 된다.


대화 재료로 사용할 문서가 격식을 갖출 필요는 없지만, 그렇다고 내용을 대충 만들어도 된다는 건 아니다. 재료가 나쁘면 대화도 함께 나빠진다. 대화 과정에서 발전시켜야 할 내용이 문서에 없으면, 그 문서는 도움이 안 된다. 그래서, 대화 재료에 사용할 문서를 작성하는 사람은 논의할 내용의 핵심이 잘 담기도록 노력해야 한다.


잘 만든 문서 하나는 대화 참석자를 빠르게 핵심 주제로 안내하고 대화 결과물이 좋을 가능성을 높여준다. 몇 년 전에 스프링캠프 발표 주제를 정리하기 위해 운영진과 회의를 한 적이 있다. 그때 발표하고 싶은 주제와 개요 수준의 목차를 담은 마인드맵 문서를 대화 재료로 들고 나갔다. 그 문서를 기반으로 빠르게 컨텍스트를 맞춘 덕에 회의가 수월하게 끝난 기억이 난다.


미래에 시스템을 운영할 사람을 위한 문서는 중요하다. 퇴사 시점에 이런 문서를 작성해서 후임자에게 넘기는 경우가 많은데, 그 시점보다는 중요 마일스톤을 찍는 시점에 운영과 관련된 문서도 함께 정리하는 것이 좋은 것 같다. 한참 시간이 흘러 작성하려면 왜 배포 과정에서 우회 방법을 선택했는지, 특정 IP를 왜 열었는지와 같은 이유를 잊게 된다. 이는 중요한 정보를 사라지게 만든다.


운영을 위한 문서는 코드처럼 정리를 해야 한다. 더 이상 필요 없으면 삭제해서 혼란을 제거하고, 기존 문서의 설명과 다른 절차가 추가되었다면 반영해서 정보가 사라지지 않도록 해야 한다. 운영을 위한 문서가 현재를 반영하지 못하면 그 문서는 죽은 문서가 되고, 중요 정보를 구전으로 전수받아야할 상황이 발생한다.


이런 문서는 정확한 표현이 중요하다. 애매모호한 표현을 사용하면 문서를 읽는 사람을 혼란스럽게 할뿐이다. 이는 말로 하는 것과 차이가 없다. 말로 전달하는 것 이상의 효과를 얻으려면 공유할 내용을 적절한 방법으로 표현하는 연습을 해야 한다. 경우에 따라 문장을 쓰거나 코드를 보여주거나 다이어그램으로 표현할 수 있어야 한다. 


이런 표현력이 쌓이면 대화 중에도 즉석에서 도움이 되는 대화 재료를 만들 수 있다. 아래 그림은 몇 해 전에 회의 중간에 그린 다이어그램이다. 간단한 다이어그램이지만 이 그림이 나오기 전까지 회의에 참여한 사람들은 서로 다른 소리를 하고 있었다. 개발자, PM, 기획자, 또 다른 개발자가 같은 이야기를 듣고 서로 다르게 이해를 했다. 그런 상황이 계속될 기미가 보이기 시작해서, 자리에서 일어나 화이트보드에 대화를 정리하기 위한 다이어그램을 그렸다. 이 그림이 그려진 이후 같은 이해를 바탕으로 회의가 정리되기 시작했다.



개발자가 본능적으로 문서 작성을 싫어하는지는 모르겠지만, 좋은(?) 개발자가 되려면 의사소통 능력이 중요하며 그 능력을 향상시켜주는 것 중 하나가 문서 작성 역량이라고 생각한다. 문서는 말과 함께 의사소통을 위한 중요한 수단이기 때문이다. 코드의 표현력이 중요한 이유가 코드에 담긴 지식을 자신을 포함한 누군가에게 전달하기 위함인것처럼, 동일하게 문서 역시 누군가에게 뭔가를 전달할 때 사용할 수 있는 방법이다.


문서를 작성하는 것은 너무 너무 귀찮은 일이지만, 도움이 되는 문서를 제대로 작성하는 것은  매우 중요한 일이기도 하다. 이 중요한 일을 잘 하기 위한 연습을 게을리하는 개발자는 되지 말자.


Posted by 최범균 madvirus

댓글을 달아 주세요

* 이 글은 브런치(https://brunch.co.kr/@madvirus/28)에도 게시된 글입니다.


지인 통해 K사, C사, N사, P사, B사 등 면접 후기를 듣다 보면, 면접이 하나 같이 어렵다. 전화 면접, 알고리즘 풀이에 가까운 코딩 테스트, 간이 WAS 만들기, 최신 기술에 대한 질문, 최신 개발 트렌드에 대한 질문, OS나 네트워크에 대한 질문, 설계 역량에 대한 검증,  인간관계에 대한 간 보기, 면접관이 잘 아는 분야에 대한 질문, 떼로 들어오는 면접관, 간혹 영어 이력서 제출까지 쉬운 게 없다. 면접 보기가 두려울 정도다. 


난 서비스 회사에서 주로 웹 개발을 담당해서 그런지 몰라도 대단한 알고리즘이 필요한 적이 없었다. 애자일이니 스크럼을 제대로 알지  못할뿐더러 그걸 잘 한다는 팀이 존재한다는 소문을 아직 못 들었다. OS나 네트워크 지식은 깊이가 없고 최신 구현 기술이나 개발 언어에 무지한 편인데, 다행히 초특급 슈퍼 울트라 고성능 시스템을 만들어야 하는 경우는 없었다. 처세나 관계 형성을 잘 못해 애를 먹은 적도 많다. 그나마 관심 있는 건 도메인 모델 정도뿐이다. 상황이 이런데 이런 식의 어려운 면접 과정을 거치면, 1~2차 면접은 고사하고 전화 면접이나 코딩 테스트부터 난관에 봉착할 것 같다.


수학적 사고가 뛰어나고 코딩을 빠르게 작성하면서도 코드 품질이 높은데다가 학구열도 높아 최신 기술과 트렌드를 줄줄 꾀고,  인간관계가 좋고 리더십이 뛰어나서 모든 사람이 같이 일하고 싶어 하며, 여러 면접관이 좋아하는 분야를 통달한 그런 인재를 뽑고 싶은 마음이야 누군들 없겠는가?


그런데, 이런 사람이 어디 있으랴... 괜히 어려운 면접 말고 합리적인 채용 기준을 설정하고 면접을 진행하는 채용이 더 늘어나길 바랄 뿐이다.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 2016.01.22 10:57  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

  2. jason.moon 2016.01.22 18:20 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 말씀입니다~

  3. PuppyRush 2016.02.17 22:19 신고  댓글주소  수정/삭제  댓글쓰기

    컴공으로서 곧 졸업하는 4학년입니다.
    글을 읽어보니 조금은 걱정이 앞섭니다. 경험을 쌓으러 가는건데 이리도 많은걸 요구하니...
    대학교에서 노력한들 기업에 얼마만큼이나 적합한 인재가 나올지 참으로 의문입니다.
    물론 노력이야 하지만요.

    유병재의 어느 영상이 생각나네요

    • 최범균 madvirus 2016.02.21 21:41 신고  댓글주소  수정/삭제

      신입과 경력직은 기준을 다르게 잡긴 하는데, 아무래도 인기 많은 회사일수록 다양한 측면에서 높은 기준으로 요구하고 있습니다.
      걱정보다는 다양한 사회 선배들을 만나고 그들이 어떻게 원하는 회사에 들어갈 수 있었는지 얘기를 듣다 보면 길을 찾는데 도움이 되실 겁니다.

  4. PuppyRush 2016.02.22 21:41 신고  댓글주소  수정/삭제  댓글쓰기

    현실적인 조언 감사합니다 :)

  5. zxcvb4825 2017.02.05 05:55 신고  댓글주소  수정/삭제  댓글쓰기

    후,,

다음에 비슷한 모임을 할 때 참고용으로 보고자 진행했던 과정을 일자 별로 남겨본다.

  • 1월 8일: 공개 모임을 진행할 것임을 그룹에 알림
  • 1월 9일: 미림여자정보과학고 함 선생님으로부터 지원할 수 있다는 답변 댓글 받음
  • 1월 13일: 발표자 모집 시작
  • 1월 18일: 3월 21일로 공개 모임 날짜 확정 (함 선생님과 일자 협의)
  • 1월 20일: 발표하고 싶은 분들과 주제 논의 시작
  • 1월 29일: 정재한, 류재섭, 오학섭 등 주제 확정 및 개요 정리 시작
  • 2월 9일: 장소 사전 확인 (최초 20명 내외 수용 가능한 강의실)
  • 3월 5일: 발표자들과 자료 리뷰 및 피드백 진행
  • 3월 6일: 온오프믹스 모임 참가 신청 접수 시작
  • 3월 18일: 모임 장소 최종 점검
  • 3월 20일: 장소 안내 목적 참가자에 이메일/SMS 발송
  • 3월 21일: 모임!
가장 어려웠을지 모를 장소 문제를 함 선생님 덕분에 깔끔하게 해소했다. 모임 장소에 대한 접근성도 우려했던 것 보다 나쁘지 않아서 56명의 신청자 분 중에서 50분 정도가 참여를 해 주셨다.


Posted by 최범균 madvirus

댓글을 달아 주세요

회사에서 입사해서 3년 반 정도 전에 그 당시 신입들 교육 목적으로 만들었던 평가 시스템이 있다. 한 번 쓰고 버릴 거라 생각해서 급한 불을 끄는 심정으로 만들었었다. 그런데 바로 버릴 것 같았던 이 시스템을 3년이나 쓰게 되었고 이번에도 쓰게 되어서, 기술도 익힐 겸 공부도 할 겸 새롭게 구현해 보았다. 몇 가지를 새로 적용해봤는데, 그 중 중요한 것은 다음과 같다.

  • 이벤트 소싱(Event Sourcing)과 CQRS
  • 스프링 부트(Spring boot)
  • 앵귤러JS(AngularJS)
  • 부트스트랩(Bootstrap)
이벤트 소싱(Event Sourcing) 적용

시스템 개편을 한 주된 이유는 이벤트 소싱을 작게나마 적용해보고 싶기 때문이었다. DDD와 CQRS 등을 학습하는 과정에서 이벤트 소싱을 만나게 되었는데, 평소 SQL을 사랑하지 않았던 나로서는 이벤트 소싱과 도메인의 만남이 매우 환상적으로 느껴졌다. (이벤트소싱 자체에 대한 내용이 궁금하다면 http://docs.geteventstore.com/introduction/event-sourcing-basics/ 글을 읽어보자.)

이벤트 소싱은 모든 상태 변경을 이벤트로 보관하고, 모든 이벤트를 차례대로 적용함으로써 최신 상태를 구할 수 있도록 만들어준다. 이런 이벤트 소싱을 도메인에 적용해보니, 보다 객체 지향적인 방법으로 도메인 코드를 구현할 수 있음을 알게 되었다.

그간 JPA와 같은 ORM 프레임워크를 사용해서 도메인 객체의 내부 상태와 DB 테이블 간의 매핑을 처리했었는데, 이는 자유롭게 도메인 코드를 만들지 못하고 테이블 구조에 억지로 맞춰 도메인 코드를 만들도록 유도했다. 예를 들어, 필자의 경우 Map, List, Set, Immutable 객체 등을 자유롭게 쓰고 싶어도, 테이블 구조/쿼리 성능/ORM 한계로 인해 사실상 도메인 코드가 테이블 설계에 영향을 받았다.

이벤트 소싱을 이용해서 도메인 코드를 구현하면, 도메인의 상태 변경 정보를 담은 이벤트 정보만 저장소에 보관하게 된다. 즉, 이벤트와 저장소 사이의 매핑만 처리하면 되기 때문에(예, 이벤트를 HBase에 보관하거나, 이벤트를 RDBMS 테이블에 보관하는 등), 도메인 자체의 코드는 ORM이나 SQL과 같은 데이터 매핑으로부터 자유로워진다. 원하는 클래스를 이용해서 객체 군(Aggregate)을 마음대로 구성할 수 있고, 기능 위주로 객체를 구현하는데 더 집중할 수 있도록 만들어준다. 이는 나에게 굉장한 해방감을 주었다.

이벤트 소싱을 적용하기 위해 Axon Framework를 사용했기 때문에 Axon Framework에 제한된 방식으로 도메인 코드를 구현해야 했지만, JPA나 테이블 구조에 따른 제한에 비하면 아무것도 아니다.

CQRS

이벤트 소싱을 이용해서 도메인을 구현하면, 객체 군 단위로 조회를 하게 된다. 이는 여러 객체 군을 동시에 조회하는 기능을 구현할 때 RDBMS 만큼의 조회 속도를 제공하지 못한다. 예를 들어, 테이블을 사용하면 "select * from PersonalEval e, PerfItem pi, PerfEval ev where ..."와 같이 필요한 데이터만 선택적으로 조회할 수 있는데 반해 이벤트 소싱의 경우는 한 타입의 도메인 객체 군에 해당하는 모든 이벤트 데이터를 로딩해야 하기 때문이다.

이런 문제를 해결하기 위해 CQRS(Command Query Responsiblity Segregation)는 필수다. 상태 변경과 관련된 기능을 제공하는 도메인 모델과 상태 조회를 위한 전용 모델을 구분해서 조회 성능을 높여주어야 한다. 평가 시스템의 경우 '평가 시즌' 도메인 객체는 '평가자 매핑'을 관리하는데, 이 평가자 매핑은 '피평가자-1차 평가자-2차평가자-동료평가자들'로 구성되어 있다.그런데, 1차 평가자 입장에서는 내가 평가해야 할 피평가자 목록이 필요하므로, 조회 시점에서는 '1차 평가자-피평가자목록'이 필요하다. 비슷하게 '2차 평가자-피평가자목록', '동료평가자-피평가자목록'도 조회 시점에 필요하다. 이렇게 조회 시점에 필요한 조회 모델을 별도로 만들어서 조회 성능 문제를 없앴다.

'평가자-피평가자' 매핑 조회 전용 데이터 모델은 도메인 객체가 생성한 이벤트를 이용해서 생성했다. 즉, 도메인 기능 실행 과정에서 발생한 이벤트를 수신해서 그 이벤트로부터 조회 모델의 데이터를 변경하는 방식을 사용했다. 조회 전용 모델을 보관하기 위해 DB를 사용할 수도 있지만, 이 프로젝트의 경우에는 데이터 양이 크지 않아 그냥 메모리에 상태 정보를 보관했다. 어플리케이션이 처음 구동될 때 관련 이벤트를 읽어와 메모리에 최종 상태의 조회 전용 데이터를 생성한 뒤, 이벤트가 발생할 때 마다 메모리의 상태를 수정하는 방식을 사용했다.

낯설고 복잡하게 느껴지지만, 일단 감 잡으면 장점

처음 이벤트 소싱을 공부하고 적용하는 과정은 낯설었다. 만약 SQL만 고집하는 개발자였다면, 심리적으로 저항항하게 될 뿐만 아니라 객체 중심 코드를 만드는데 힘겨워할 것 같다. 또한, CQRS를 처음 만나면 복잡하게 느껴진다. 물론, 최근에는 조회를 위해 데이터를 캐시에 담아두는 곳이 많기 때문에 뷰 전용 모델을 만드는 것에 대한 거부감이 이번보다 덜 하지만, 상태를 변경할 때 사용하는 모델과 조회할 때 사용하는 모델이 다르기 때문에 복잡하게 느껴진다.

하지만, 도메인 모델이 조금만 커져도, 상태 변경을 위한 SQL은 복잡해지는 반면에 객체 기반 코드는 상대적으로 단순해졌다. 새로운 기능을 추가한다거나 로직이 변경되더라도 ERD를 변경할 필요 없이 도메인 코드만 수정하면 되므로 변경에 유연하게 대처할 수 있게 된다. 또한, 조회 전용 모델도 얼마든지 원하는 방식으로 만들어낼 수 있기 때문에 조회를 위한 모델의 자유도도 높아졌다.

스프링 부트(Spring Boot)


작년부터 뜨고 있는 스프링 부트를 적용했다. 사용한 이유는 다음과 같다.

  • 비교적 단순해진 의존 설정
  • 임베디드 톰캣을 이용해서 실행 가능한 war를 배포해서 실행 과정을 단순화
위 두 가지는 확실히 장점이었다. 기본적인 설정을 모두 스프링 부트가 알아서 먹어주기 때문에, 필요한 설정들만 추가해 주면 되었다. 실행 가능한 war는 배포에서의 장점이었다. war를 배포하고 java 명령어를 이용해서 실행해주기만 하면 끝났다. 톰캣의 catalina.sh과 같은 쉘 파일을 만들어주긴 했지만 war를 바로 실행할 수 있기 때문에 배포와 실행이 매우 간단해졌다.

물론, 단점도 있다. 스프링 부트가 너무나 많은 걸 내부적으로 설정해버리기 때문에, 기본 설정에서 벗어나기 위해 부트의 내부를 이해해야 하는 경우도 있었다. 부트에 대한 이해도가 떨어지면, 처음에 설정과 관련해서 삽질할 가능성이 높아진다.

뷰 구현 기술로는 JSP를 사용했는데, 이유는 다른 구현 뷰 구현 기술이 익숙하지 않았기 때문이다. 다음에 임베디드 서버를 이용해서 실행 가능한 war를 만들 때에는 타임리프(thymeleaf)를 사용해보는 것도 고려하고 있다.


앵귤러JS와 부트스트랩


부트스트랩은 "사랑"이다. 디자인 감각이 없는 나에게 부트스트랩은 없어서는 안 될 존재이다. 이번 시스템의 경우도 부트스트랩을 사용한 덕에 디자인이 깔끔해졌다는 평을 들을 수 있었다. 직접 CSS를 만지작거렸다면 아마 무엇을 상상해도 그 이하의 디자인이 나왔을것이다.


프론트 기술이 약한 나에게 앵귤러JS도 '사랑'이다. 앵귤러JS 2 버전이 올해 나올 예정이지만 아직 알파 버전이기 때문에 앵귤러 JS 1을 사용했다. 앵귤러 JS가 없었다면 jQuery와 자바 복잡한 스크립트 코드를 만드느라 멘붕을 수도 없이 겪었을지 모른다. 앵귤러 JS 덕에 폼에 입력한 값을 쉽게 처리할 수 있었고, 서버와의 연동도 간단하게 할 할 수 있었다.


REST API를 연동하는 부분에서는 $q를 사용하면서 Promise에 대해 약간의 감을 잡을 수 있었다.


이 외에 앵귤러 JS의 다음 기능들을 사용했다.

  • 폼 값 검증
  • angular-elastic : textarea의 높이를 입력 값에 따라 자동으로 조절해주는 모듈듈
  • angular-ui-bootstrap : 부트스트랩의 모달 다이얼로그를 앵귤러 컨트롤러로 처리하기 위해 사용
예제 코드

딱히 코드가 아름답진 않지만, 학습 과정에서 작성한 코드를 https://github.com/madvirus/evaluation 에서 확인할 수 있다.


Posted by 최범균 madvirus

댓글을 달아 주세요

최근 몇 년 동안 여러 분들(회사 후배, 사회 후배 등)과 만나면서 이런 저런 다양한 얘기를 나누었는데, 몇 가지 생각나는 걸 끄젹여본다.


기본적인 몇 가지:

  • 꿈은?
  • (내 꿈이 뭐냐고 거꾸로 물어볼 때) 한국의 개발자들의 평균 수준을 끌어올리는 것 (아, 거창하다)
  • 프로그래밍이 직장인지 직업인지 고민해라. 모든 사람이 개발을 잘 할 필요는 없다.
  • 회사를 따지지 말고, 본인을 따져라.
  • 직업에서의 멘토/선배/동료를 만들어라.
  • 1~5년의 목표는?
  • (나에게 거꾸로 목표를 물어볼 때) 건물주가 되어 월세를 받고 싶다. ^^;; 영어로 책을 쓰고 싶다.
개발을 '잘' 하고 싶다는 뜻을 내 비치면,
  • 진짜?
  • 공부 좀 해라. ^^;;
  • 인터넷에서 짜집기한 것만 읽지 말고, 책을 읽어라.
  • 같은 책을 여러 번 읽어라.
  • 꾸준히 해라.
  • 하고 싶은 게 있다고 말로만 하지 말고, 좀 해라. 회사에서 할 수 없다면 개인 프로젝트로라도 해라.
  • '구글의 OOO'가 아니라 'OO의 홍길동'이 되어라.
코딩 할 때,
  • (코드 볼 때) 이름이 좀 이상한데? 이 코드 의미가 뭐야?
  • (짝 코딩할 때) 이 클래스/메서드/변수/필드 이름을 뭐라고 하지?
  • (코딩하는 거 볼 때) 컨트롤+1
  • (코딩하는 거 볼 때) 컨트롤+스페이스
  • (짝 코딩에서 내가 테스트 코드 만들 때) 이 클래스는 이렇게 동작해야 하고~
  • (한 번 짝 코딩 한 뒤에, 테스트 코드 안 만들고 있을 때, 지나가는 말로) 테스트 코드가 없네.....


Posted by 최범균 madvirus
TAG 대화

댓글을 달아 주세요

앞 이야기:

작은 요구 사항 변경이 발생했다. 최초의 계획은 '전날 6시 이후의 변경 내역'을 읽어와 리포지토리에 반영하는 것이었는데, 대화를 진행하면서, 아래와 같이 변경되었다.

  • 나: 팀장님, 말씀하신 것 개발이 완료되어 가는데요, '전날 6시 이후로 변경된 내역을 보관하도록 했어요. 매일 오전 6시에 배치로 돌리면 될 것 같아요.
  • A: 최팀장, 그거 업무 시간 중에 변경되는 증분을 보고 싶은 그런 것도 있어서 24시간은 너무 긴 거 같아. 좀 더 짧았으면 좋겠는데....
  • 나: 그럼 얼마나요?
  • A: 음.. 10~20분 주기로?
  • 나: 아,, 네. 단순히 일단위 백업이 아니라 소스 버전 관리 느낌을 좀 더 보태고 싶다는 말씀이시죠?
  • A: 네 그래요~
요구사항의 변화를 수용하기 위한 첫 작업: 테스트로부터 개발


이 요구사항을 충족하려면 전날 6시가 아니라 마지막으로 백업 받은 이전 시간을 어딘가에 기록했다가 다음에 실행할 때 그 시간을 찾아야 한다. 기록할 장소로 파일을 선택했고, 이를 반영하기 위해 테스트를 수정했다.


public class BackupToolTest {


    private static final String TIMEFILE_PATH = "target/timefile.txt";

    private DbCodeFinder mockDbCodeFinder = mock(DbCodeFinder.class);

    private BackupTool tool = new BackupTool();

    private SvnClient mockSvnClient = mock(SvnClient.class);

    private ArgumentCaptor<Date> captor;

    private List<DbCode> dbCodeList;


    @Before

    public void setUp() {

        tool.setDbCodeFinder(mockDbCodeFinder);

        tool.setSvnClient(mockSvnClient);


        captor = ArgumentCaptor.forClass(Date.class);


        createDbCodeList();

        when(mockDbCodeFinder.findUpdatedDbCodesBetween(any(Date.class), any(Date.class)))

                .thenReturn(dbCodeList);


        deleteTimeFileIfExists();

    }


    private void deleteTimeFileIfExists() {

        File file = new File(TIMEFILE_PATH);

        file.delete();

    }


    @Test

    public void givenTimeFileNotExists_run() throws IOException {

        tool.backup();


        verify(mockDbCodeFinder).findUpdatedDbCodesBetween(captor.capture(), captor.capture());

        verify(mockSvnClient).commit(dbCodeList);


        List<Date> dateValues = captor.getAllValues();

        Date fromDate = dateValues.get(0);

        // 파일이 없는 경우 하루 전달 이후 변경 분을 읽어오는 지 검사

        assertThat(getTimeByMinutes(fromDate), equalTo(getOneDayBefore()));

        // tool.backup() 실행 후, 지정한 파일에 마지막 실행 시간 기록하는 지 검사

        assertTimeFileWritten(dateValues.get(1));

    }


    @Test

    public void givenTimeFileExists_run() throws Exception {

        givenTimeFile();


        tool.backup();


        verify(mockDbCodeFinder).findUpdatedDbCodesBetween(captor.capture(), captor.capture());

        verify(mockSvnClient).commit(dbCodeList);


        List<Date> dateValues = captor.getAllValues();

        // 변경된 쿼리 구할 때 사용할 시작 시간 값을 파일에서 읽어오는 지 검증

        assertThat(DateUtil.formatDate(dateValues.get(0)), equalTo("20131018120130"));

        // tool.backup() 실행 후, 지정한 파일에 마지막 실행 시간 기록하는 지 검사

        assertTimeFileWritten(dateValues.get(1));

    }


    private void givenTimeFile() throws Exception {

        File file = new File(TIMEFILE_PATH);

        FileCopyUtils.copy("20131018120130", new FileWriter(file));

    }


    private void assertTimeFileWritten(Date toTime) throws IOException {

        String fileContents = FileCopyUtils.copyToString(new FileReader(TIMEFILE_PATH));

        assertThat(fileContents, equalTo(DateUtil.formatDate(toTime)));

    }


    ... // 나머지 다른 메서드들


처음부터 위 테스트 코드가 완성된 것은 아니고, 아래의 순서로 위 코드가 만들어졌다.

  • givenTimeFileExists_run() 메서드를 작성
    • 마지막 실행 시간을 기록한 파일이 존재할 경우, DB에서 조회할 때 사용되는 시간 값으로 파일에 기록된 값을 사용하는지 검사
    • 실행 종료 후 마지막 실행 시간을 지정한 파일에 보관하는지 검사
  • givenTimeFileExists_run() 테스트를 통과시키기 위해 BackupTool의 backup() 메서드 구현
  • 기존 테스트 메서드의 이름을 givenTimeFileNotExists_run() 으로 변경
    • 기존 파일이 없는 경우이므로, setUp() 메서드에서 파일 존재시 삭제 처리
    • 파일이 없는 경우 24시간 이전 시간을 조회 시작 시간으로 사용하는 지 검사
    • 실행 종료 후 마지막 실행 시간을 지정한 파일에 보관하는지 검사
  • givenTimeFileNotExists_run() 테스트를 통과시키기 위해 BackupTool의 backup() 메서드 구현
아직 테스트 코드에서 발생하고 있는 중복을 제거하진 않았지만, 이 과정에서 BackupTool의 backup() 메서드가 완성되었다. 아래 코드는 테스트 코드가 통과된 뒤 약간의 리팩토링을 한 BackupTool 클래스의 일부이다.

public class BackupTool {
    private String timefilePath;
    private DbCodeFinder dbCodeFinder;
    private SvnClient svnClient;

    public void backup() {
        Date fromTime = getPreviousLastTime();
        Date toTime = getToTime();
        List<DbCode> dbCodeList = findUpdatedDbCodesAfter(fromTime, toTime);
        commit(dbCodeList);
        writeLastTimeToFile(toTime);
    }

    private Date getPreviousLastTime() {
        File file = new File(timefilePath);
        if (file.exists())
            return getPreviousTimeFromFile(file);
        else
            return getOneDayBeforeTime();
    }

    private Date getPreviousTimeFromFile(File file) {
        try {
            String fileContents = FileCopyUtils.copyToString(new FileReader(file));
            SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
            return format.parse(fileContents);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Date getOneDayBeforeTime() {
        Calendar time = Calendar.getInstance();
        time.add(Calendar.DATE, -1);
        return time.getTime();
    }

    private Date getToTime() {
        return new Date();
    }

    private List<DbCode> findUpdatedDbCodesAfter(Date fromDate, Date toTime) {
        return dbCodeFinder.findUpdatedDbCodesBetween(fromDate, toTime);
    }

    private void commit(List<DbCode> dbCodeList) {
        svnClient.commit(dbCodeList);
    }

    private void writeLastTimeToFile(Date toTime) {
        try {
            File file = new File(timefilePath);
            FileCopyUtils.copy(DateUtil.formatDate(toTime), new FileWriter(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void setDbCodeFinder(DbCodeFinder dbCodeFinder) {
        this.dbCodeFinder = dbCodeFinder;
    }

    public void setSvnClient(SvnClient svnClient) {
        this.svnClient = svnClient;
    }

    public void setTimefilePath(String timefilePath) {
        this.timefilePath = timefilePath;
    }

}


이제 남은 작업은 정리하지 못한 코드들을 마저 정리하는 것이다. 이건 어린개발자1에게 숙제로 남겨둘 예정이다.


Posted by 최범균 madvirus

댓글을 달아 주세요

이전글:


드디어 DB에서 변경된 DB 코드(프로시저, 함수, 패키지) 목록을 뽑아주는 쿼리를 전달받았다. 애초에 상상한 쿼리 실행 결과는 아래와 같았다.


[상상속에서 기대했던 쿼리 실행 결과]

OWNER

TYPE 

NAME 

QUERY 

O1

PROCEDURE 

findXXX 

프로시저 코드 

O2

PACKAGE 

somePkg 

패키지 코드 

...

... 

... 

... 


그런데 이게 왠 걸? 실제 쿼리를 실행하지 아래와 같은 결과가 나왔다.


[실제 쿼리 실행 결과]

OWNER

TYPE 

NAME 

QUERY 

O1

PROCEDURE 

findXXX 

프로시저 코드 1번줄

O1

PROCEDURE 

findXXX 

프로시저 코드 2번줄

...

... 

... 

... 

O1

PROCEDURE 

findXXX 

프로시저 코드 n번줄 

O2 

PACKAGE 

somePkg 

패키지 코드 1번줄 


오 마이! 이게 뭐야! 각 PL SQL의 각 코드가 1줄씩 결과로 리턴된다. 


쿼리를 갖고 몇 가지 방법을 쪼물딱 거려 봤으나, 줄 수가 좀(많이) 긴 패키지의 경우 오라클 장비가 먹어주지 못하는 상황이 발생했다. 이런 짜증 날 때를 봤나. 게다가 몇 만 줄이나 되는 패키지가 있다. (실제로 2만 줄이 넘는 패키지가 있다.)


한 번에 모든 쿼리 데이터를 다 읽어올까? 아님 필요할 때 읽어올까? 잠시 고민을 해 봤는데, 최초에 모든 쿼리를 받아서 SVN 리포지토리에 업로드해 주어야 하는 상황이 필요했고. (실DB에 수만 줄에 해당하는 프로시저 덩어리가 얼마나 있는 지 몰라 한방에 모든 DB 코드를 다 읽어오지 않고) DB 코드가 필요한 순간에 코드를 읽어오도록 DbCode 클래스 및 DbCodeFinder 의 코드를 수정했다. 먼저 다음과 같이 getDdl() 메서드를 호출하는 순간에 실제 쿼리 내용을 읽어오는 LazyDbCode 클래스를 추가했다.


public class LazyDbCode extends DbCode {


    private JdbcTemplate jdbcTemplate;


    public LazyDbCode(JdbcTemplate jdbcTemplate) {

        this.jdbcTemplate = jdbcTemplate;

    }


    @Override

    public String getDdl() {

        String query = "SELECT B.TEXT FROM ALL_OBJECTS A, ALL_SOURCE B "

                + "WHERE  A.OWNER = ?  AND A.OBJECT_NAME = ?  "

                + "AND A.OBJECT_TYPE = ?  AND A.OBJECT_NAME = B.NAME  "

                + "AND A.OBJECT_TYPE = B.TYPE  ";


        List<String> queryLines = jdbcTemplate.query(query,

                new PreparedStatementSetter() {

                    public void setValues(PreparedStatement ps) throws SQLException {

                        ps.setString(1, getSchema());

                        ps.setString(2, getName());

                        ps.setString(3, getType().name());

                    }

                }, new RowMapper<String>() {

                    public String mapRow(ResultSet rs, int rowNum) throws SQLException {

                        return rs.getString(1);

                    }

                });

        return queryLinesToString(queryLines);

    }

    

    public String queryLinesToString(List<String> queryLines) {

        ...

    }


}


DbCodeFinder의 구현 클래스인 JdbcDbCodeFinder는 DbCode 대신에 LazyDbCode 객체를 생성해서 리턴하도록 구현했다.


public class JdbcDbCodeFinder implements DbCodeFinder {


    private JdbcTemplate jdbcTemplate;


    public List<DbCode> findUpdatedDbCodesAfter(final Date fromTime, final Date toTime) {

        String query = "SELECT * FROM   ALL_OBJECTS A "

                + "WHERE  SUBSTR(A.OWNER,1,1) IN ('A','B','C','D','E','H')  "

                + "AND    A.OBJECT_TYPE IN ('PACKAGE', 'PACKAGE BODY', 'FUNCTION', 'PROCEDURE', 'TRIGGER')  "

                + "AND    TO_CHAR(A.LAST_DDL_TIME, 'YYYYMMDDHH24MISS')  BETWEEN ? AND ?  "

                + "ORDER BY A.OBJECT_NAME, A.OBJECT_TYPE";

        

        List<DbCode> dbCodeList = jdbcTemplate.query(query,

                new PreparedStatementSetter() {

                    public void setValues(PreparedStatement ps) throws SQLException {

                        ps.setString(1, formatDate(fromTime));

                        ps.setString(2, formatDate(toTime));

                    }

                }, new RowMapper<DbCode>() {

                    public DbCode mapRow(ResultSet rs, int rowNum) throws SQLException {

                        DbCode code = new LazyDbCode(jdbcTemplate);

                        code.setSchema(rs.getString("OWNER"));

                        code.setType(DbCode.Type.valueOf(rs.getString("OBJECT_TYPE")));

                        code.setName(rs.getString("OBJECT_NAME"));

                        return code;

                    }

                });

        return dbCodeList;

    }


SvnClient와 DbCodeFinder의 콘크리트 클래스를 구현이 완료된 시점의 설계는 아래와 같다.



[다음 이야기에서 계속 ...]


Posted by 최범균 madvirus

댓글을 달아 주세요

지난 이야기:

svn 프로그램을 이용해서 SvnClient를 구현하기로 결정했다. 그리고, 이런 저런 코드를 구현하고 있었다. 그러던 중 SvnKit이라는 자바 모듈을 찾게 되었고, 이 모듈을 이용해서 SvnKit을 실험해 보기로 했다. 실험 결과 SvnKit을 사용하면 로컬에 파일을 따로 보관할 필요 없이 곧 바로 원격 리포지토리에 변경 내역을 커밋할 수 있음을 알게 되었다.

  • 어린개발자1: 팀장님, SvnKit을 실험해 봤는데요, 원격 리포지토리에 바로 추가/수정/커밋이 되는데요.
  • 나: 그래? 아, 그럼 LocalCodeUpdater는 필요 없군!
SvnKit 사용에 따른 설계 변경 (LocalCodeUpdater의 제거)

LocalCodeUpdater가 필요 없어졌다. 그래서 기존의 테스트 코드를 아래와 같이 변경했다.


public class BackupToolTest {

    private BackupTool tool;

    private DbCodeFinder mockDbCodeFinder;

    private LocalCodeUpdater mockLocalCodeUpdater;

    private SvnClient mockSvnClient;

    private List<DbCode> dbCodeList;


    @Before

    public void setUp() {

        tool = new BackupTool();

        mockDbCodeFinder = mock(DbCodeFinder.class);

        mockLocalCodeUpdater = mock(LocalCodeUpdater.class);

        mockSvnClient = mock(MockSvnClient.class);


        dbCodeList = createDbCodeList();

        when(dbCodeFinder.findUpdatedDbCodeAfter(any(Date.class)).thenReturn(dbCodeList);


        tool.setDbCodeFinder(mockDbCodeFinder);
        tool.setLocalCodeUpdater(mockLocalCodeUpdater);
        tool.setSvnClient(mockSvnClient);

    }


    @Test

    public void runBackup() {

        tool.backup();


        verify(mockDbCodeFinder).findUpdatedDbCodeAfter(any(Date.class));

        verify(mockLocalCodeUpdater).update(dbCodeList);

        verify(mockSvnClient).commit(dbCodeList);

    }


LocalCodeUpdater가 필요 없어지면서, SvnClient의 commit() 메서드에 바로 DbCode 목록을 전달하도록 테스트 코드를 수정했다. 컴파일 에러를 잡아주기 위해 아래와 같이 SvnClient를 수정해주고,


public interface SvnClient {

    public void commit(List<DbCode> dbCodeList);

}


이 후, 테스트를 통과시키면서 BackupTool에서 LocalCodeUpdater가 사용되는 부분을 제거하였다.


public class BackupTool {

    private DbCodeFinder dbCodeFiner;

    private LocalCodeUpdater localCodeUpdater;

    private SvnClient svnClient;


    public void backup() {

        List<DbCode> dbCodeList = 

                dbCodeFinder.findUpdatedDbCodeAfter(getPreviousUpdatedTime());

        localCodeUpdater.update(dbCodeList);

        svnClient.commit(dbCodeList);

    }


    private Date getPreviousUpdatedTime() {

        // 어제 날짜 6시 값 리턴

        return ...;

    }


    public void setDbCodeFinder(DbCodeFinder dbCodeFinder) {

        this.dbCodeFinder = dbCodeFinder;

    }

    public void setLocalCodeUpdater(LocalCodeUpdater localCodeUpdater) {

        this.localCodeUpdater = localCodeUpdater;

    }

    public void setSvnClient(SvnClient svnClient) {

        this.svnClient = svnClient;

    }

}


이후, LocalCodeUpdater 인터페이스를 제거하였다.


이후 작업은 SvnKit을 이용한 SvnClient의 구현을 진행했다.

  • 나: 이제 SvnKit을 이용해서 SvnClient 구현 클래스를 만들도록,
  • 어린개발자1: 네!
[다음회에 계속.....]


Posted by 최범균 madvirus

댓글을 달아 주세요

요즘 간단한 DB PL SQL 백업 도구를 팀의 막내와 같이 만들고 있다. 오라클 DB의 프로시저 소스를 관리해주는 상용도구가 있겠지만, 비용상의 문제로 그 도구를 사용하지 않기로 결정했다. (음.. 내가 결정한게 아니고 이 기능을 필요로 하는 곳에서 큰 돈을 쓰지 않기로 결정했다.) 그래서, 부득이 오라클 DB의 코드-패키지, 프로시저, 함수-를 주기적으로 백업하는 간단한 어플리케이션을 개발하기로 했다. (음.. 정확히는 타팀의 개발 요청을 받았다.)


요구사항


요구사항은 간단하다.

  • 나: A님, 우리가 만들어야 하는 게 뭐에요?
  • A: DB에 있는 프로시저 소스를 백업해주는 프로그램이요.
  • 나: 백업은 어디다 해요?
  • A: SVN 리포지토리요.
  • 나: 버전 관리를 해야 하는 이유라도?
  • A: 그게 DB 프로시저가 전혀 관리가 안 되기 때문에, 주기적으로 백업해서 변경 이력을 남기고 싶어요.
  • 나: 아, 그럼 매번 전체 프로시저를 백업할 필요는 없겠네요.
  • A: 네, 변경된 것만 커밋하는 식으로 하면 좋겠어요. 변경된 프로시저를 구하는 SQL은 여기(저쪽 팀)에서 제공해줄께요.
오호라, SQL은 제공해 준단다. 우리가 해야 할 건 제공받은 SQL을 이용해서 변경된 PL SQL 코드를 가져오고, 그 코드를 리포지토리에 보관만 하면 된다.
  • 나: 그럼, 지금 바로 시작할 수 있겠는데요. 그런데, 그 쿼리는 언제 주세요?
  • A: 글쎄요. 그거 할 사람이 지금 다른 거 하고 있는데 그거 끝나고 줄께요.
  • 나: (엄..... ) 언제 끝나는데요?
  • A: 아마도 다음주?
  • 나: (엄.................) 네. 일단 할 수 있는 것 먼저 시작하고 있을께요.
구현의 시작

이런, 쿼리를 나중에 준단다. 그렇다고 멍 때리고 있을 이유는 없다. 만들 코드가 많기 때문이다. 변경된 프로시저를 읽어오는 부분은 여기서의 핵심 로직이 아니다. 읽어오는 기능이 있다 치고 핵심 로직과 그 외에 나머지 부분을 모두 만들어 낼 수 있다. (이에 대한 내용은 "저수준의 상세한 내용 없이 구현하기:http://javacan.tistory.com/297" 글을 읽어보기 바란다.)

우리(나+어린개발자1)는 바로 코드를 만들기 시작했다. 가장 먼저 작성한 것은 아래와 비슷한 테스트 코드이다.

public class BackupToolTest {
    @Test
    public void canCreate() {
        BackupTool tool = new BackupTool();
    }

    public class BackupTool {
    }
}

테스트 코드를 먼저 작성한 뒤에, 한 작업은 BackupTool의 행위를 테스트하는 메서드를 작성하는 것이었다.
  • 나: 자 tool의 backup() 메서드를 만들어보자고, 일단 테스트 코드에서 tool을 호출하도록 만들고,
  • 어린개발자1: (열심히 들음)
  • 나: tool 안에서 변경된 코드 목록을 불러와 그걸 로컬 디렉토리에 보관하고, 이를 커밋하도록 만들어야 할 것 같아. tool은 흐름만 관리하게 하고 나머지는 별도 객체가 처리하도록 하자.
  • 어린개발자1: (열심히 들음)
  • 나: 별도 객체가 처리하도록 할 거니까, backup() 메서드를 실행한 뒤에, 그 객체들이 호출되었는지 확인해보면 될 것 같아.
// 빨간색 코드는 컴파일 에러 발생하는 부분
public class BackupToolTest {
    ...
    @Test
    public void runBackup() {
        BackupTool tool = new BackupTool();
        tool.backup();
        verify(dbCodeFinder).findUpdatedDbCodeAfter(any(Date.class));
        verify(localCodeUpdater).update(dbCodeList);
        verify(svnClient).commit();
    }
}

  • 나: 검증 코드를 만들었으니까, 이제 컴파일 에러를 없애 볼까?
  • 어린개발자1: (끄덕임)
  • 나: (아래와 같은 코드를 입력하면서) DbCodeFinder 인터페이스 만들고, Mock 만들고, 나머지도 동일하게 인터페이스와 Mock 만들어주고, Mockito 이용해서 dbCodeFinder의 findUpdatedDbCodeAfter() 메서드가 호출되면 DbCode 목록을 리턴하게 해 주자. 이것들은 아직 구현이 필요 없으니까, 이따가 구현이 필요할 때 실제로 구현하면 될 것 같아.
// 파란색 코드는 새로 추가된 코드
public class BackupToolTest {
    ...
    @Test
    public void runBackup() {
        DbCodeFinder dbCodeFinder = mock(DbCodeFinder.class);
        LocalCodeUpdater localCodeUpdater = mock(LocalCodeUpdater.class);
        SvnClient svnClient = mock(SvnClient.class);

        List<DbCode> dbCodeList = createDbCodeList();
        when(dbCodeFinder.findUpdatedDbCodeAfter(any(Date.class)).thenReturn(dbCodeList);

        BackupTool tool = new BackupTool();
        tool.backup();

        verify(dbCodeFinder).findUpdatedDbCodeAfter(any(Date.class));
        verify(localCodeUpdater).update(dbCodeList);
        verify(svnClient).commit();
    }

    public class BackupTool {
        public void backup() {
        }
    }
    public interface DbCodeFinder {
        public List<DbCode> findUpdatedDbCodeAfter(Date date);
    }
    public interface LocalCodeUpdater {
        public void update(List<DbCode> dbCodeList);
    }
    public interface SvnClient {
        public void commit();
    }
}
  • 나: DbCode에 뭐가 들어올지 아직 모르니까, 일단 클래스만 만들어두자.
  • 어린개발자1: 네~
public class BackupToolTest {
    ...
    ...
    public class DbCode {
    }
}

이후 작업은 테스트를 통과시키기 위한 코드 작업을 진행했고, 몇 차례의 테스트 실패-테스트 통과-코드 정리 과정을 거쳐 아래의 코드가 완성되었다.

public class BackupToolTest {
    @Test
    public void runBackup() {
        DbCodeFinder dbCodeFinder = mock(DbCodeFinder.class);
        LocalCodeUpdater localCodeUpdater = mock(LocalCodeUpdater.class);
        SvnClient svnClient = mock(SvnClient.class);

        List<DbCode> dbCodeList = createDbCodeList();
        when(dbCodeFinder.findUpdatedDbCodeAfter(any(Date.class)).thenReturn(dbCodeList);

        BackupTool tool = new BackupTool();
        tool.backup();

        verify(dbCodeFinder).findUpdatedDbCodeAfter(any(Date.class));
        verify(localCodeUpdater).update(dbCodeList);
        verify(svnClient).commit();
    }

    public class BackupTool {
        private DbCodeFinder dbCodeFiner;
        private LocalCodeUpdater localCodeUpdater;
        private SvnClient svnClient;

        public void backup() {
            List<DbCode> dbCodeList = 
                    dbCodeFinder.findUpdatedDbCodeAfter(getPreviousUpdatedTime());
            localCodeUpdater.update(dbCodeList);
            svnClient.commit();
        }

        private Date getPreviousUpdatedTime() {
            // 어제 날짜 6시 값 리턴
            return ...;
        }

        public void setDbCodeFinder(DbCodeFinder dbCodeFinder) {
            this.dbCodeFinder = dbCodeFinder;
        }
        public void setLocalCodeUpdater(LocalCodeUpdater localCodeUpdater) {
            this.localCodeUpdater = localCodeUpdater;
        }
        public void setSvnClient(SvnClient svnClient) {
            this.svnClient = svnClient;
        }
    }
    public interface DbCodeFinder {
        public List<DbCode> findUpdatedDbCodeAfter(Date date);
    }
    public interface LocalCodeUpdater {
        public void update(List<DbCode> dbCodeList);
    }
    public interface SvnClient {
        public void commit();
    }
    public class DbCode {
    }
}

뭔가 정상적으로 동작하는 코드가 만들어졌다. 이제 테스트 클래스에 중첩 클래스로 구현한 클래스와 인터페이스들을 모두 별도 파일로 분리해냈다. 별 것 없는 테스트 코드를 만들어낸 것 같지만, 우리는 동작하는 코드로 설계를 진행하였다. 초기에 만들어진 클래스와 인터페이스의 설계는 아래와 같다.



DbCode 데이터 도출

주요 인터페이스가 도출되었다. 이제 할 일은 각 인터페이스를 구현하는 것이다. 가장 먼저 진행한 작업은 디렉토리에 읽어온 DbCode를 로컬에 파일로 기록하는 LocalCodeUpdater의 구현이었다. 이를 위한 테스트 코드를 작성했다. 최초에 대화를 주고 받으면 점진적으로 만들어진 테스트 코드는 아래와 같았다.

public class LocalFileCodeUpdaterTest {
    private LocalFileCodeUpdater codeUpdater;

    @Before
    public void setUp() {
        codeUpdater = new LocalFileCodeUpdater();
        codeUpdater.setRepositoryRoot("target/repo");
    }

    @Test
    public void whenNewDbCode_createFile() {
        deleteIfExists("target/repo/S1/PACKAGE/code1.sql");
        DbCode dbCode = dbCode();
        dbCode.setSchema("S1");
        dbCode.setType(Type.PACKAGE);
        dbCode.setName("code1");
        dbCode.setDdl("test ddl");
        List<DbCode> dbCodeList = new ArrayList<DbCode>();
        dbCodeList.add(dbCode);
        codeUpdater.update(dbCodeList);
        assertFileData("target/repo/S1/PACKAGE/code1.sql", dbCode);
    }

    ... // 파일이 존재할 때 수정되는 지 확인하는 테스트 코드
}

 위 테스트 코드를 만드는 과정에서 DbCode가 가져야 할 데이터가 정리가 되었다. 이후 과정은 테스트를 통과시키는 과정에서 LocalFileCodeUpdater의 구현을 완성해 나갔다. 그리고, SvnClient를 구현하는 단계로 넘어갔다.

[이어지는 이야기는 다음에....]



Posted by 최범균 madvirus

댓글을 달아 주세요

탁월함은 어디서부터 올까? 천부적인 기질? 생각? 행동? 3년 전에 받은 리더십 교육에서 강사가 탁월함을 이끄는 것에 대해 대해 언급을 했었는데, 그 중에 중요한 것으로 꼽았던 것이 바로 '습관'이다. 물론, 좋은 습관을 가졌다고 해서 탁월할 수 있는 것은 아니겠지만, 좋은 습관만으로도 이전보다 더 좋은 결과물을 만들어낼 수 있다고 생각한다. 그리고, 프로그래머에게도 이것이 적용된다고 생각한다.


프로그래머가 가져야 할 좋은 습관 중의 하나로 (요즘 필자가 초식 수련중인) "TDD"를 들 수 있다. TDD는 테스트 코드를 먼저 작성하고, 테스트를 통과 시키고, 그 다음에 리팩토링을 하는 간단한 흐름으로 구성된다. 이 간단한 흐름을 지키는 작은(?) 습관만으로도 다음의 결과물을 얻어낼 수 있다.

  • 테스트 시간을 줄여준다. 손과 눈으로 하는 테스트보다 컴퓨터가 실행하는 테스트가 훨씬!! 빠르다.
  • 테스트를 할 수 있도록 노력하는 덕분에 나름의 좋은 설계가 유도될 가능성이 높아진다.
  • 회귀 테스트를 만들어주기 때문에, 코드 수정에 대한 자신감을 갖게 된다.
  • 반복적인 리팩토링을 함으로써 더러워진 코드를 일정 부분 청소해준다.
  • 결과적으로 전반적인 코드의 가독성이 나빠지는 것을 방지해주거나 가독성을 향상시켜준다.

처음부터 TDD를 잘 하기는 어렵겠지만, TDD를 지속적으로 시도하는 것만으로도 위의 장점 중 몇 가지를 얻어낼 수 있으며, 이는 곧 개발 생산성의 향상으로 연결될 수 있다. TDD라는 (어떻게 보면 작은) 습관이 (TDD를 하지 않는 경우와 비교해서) 생산성을 올려주는 것이다!


작은 습관이 더 나은 결과물로 연결되는 경우의 또 다른 예로 단축키를 들 수 있다. 단축키가 별 것 아닌 것 같지만, 단축키를 잘 활용하는 개발자와 그렇지 못한 개발자의 코드 입력 속도는 많은 차이를 발생시킨다. 마우스를 잡고 이동하고 클릭하고 선택하는 시간보다 단축키를 눌러서 처리하는 시간이 빠르다. 이 시간이 쌓이면 쌓일수록 같은 기간 동안 더 많은 코드를 만들어내고 더 많은 생각을 할 수 있게 되고, 이는 곧 더 나은 품질의 결과물을 더 빠르게 만들어낼 수 있도록 만들어준다.


이 외에 좋은 습관으로 꾸준한 리팩토링, 일을 작게 나누기, 체크 리스트 만들기 등이 있을 것 같다.


좋은 습관. 좋은 습관이 우리를 탁월하게 만들어주진 않겠지만, 좋은 습관을 가진 것만으로도 탁월해 질 수 있는 가능성을 열어준다. 우리 모두 좋은 습관을 가져보자.



Posted by 최범균 madvirus
TAG 습관

댓글을 달아 주세요

  1. 홍성민 2013.12.31 14:35 신고  댓글주소  수정/삭제  댓글쓰기

    늘 좋은글 감사해요

오래된 휴대폰에서 사진을 정리하다가 아래 그림이 나왔다. 2년 반 정도 전에 다니던 회사에서 모바일 게임 개발팀과 개발 범위에 대해 논의하는 회의를 하면서 그린 그림이다.



그 당시 회의를 진행하면서 서로들 다른 소리를 하길래 (실은 몇 명이 잘 못 알아듣길래) 회의 진척을 위해 화이트보드에 그림을 그렸다. 이 그림을 그린 뒤로 서로 오해 없이 대화를 진행한 기억이 난다.


이 그림은 별 것 아니지만, 이 그림이 그려지기 전까지 서로 다른 모습을 상상하며 대화를 했었다. 통신하는 방식도 주요 컴포넌트의 구성도 비슷한 듯 다르게 상상했기 때문에, 대화도 "아,, 그게 아니라....." 이런 식으로 진행되곤 했다. 지식을 시각적으로 표현하고 나서야 비로서 상호 간의 차이를 맞추고 정확하게 의사 소통 할 수 있었다.


올바르게 동작하는 소프트웨어를 만들어야 하는 프로그래머라면, 다른 프로그래머에게 소프트웨어에 대한 지식을 공유할 수 있는 역량이 필수적이다. 이는 아키텍처, 상위 수준 설계, 심지어 코드 수준까지 모두 해당된다. 지식을 서로 제대로 공유하지 못한다면, 해메는 시간이 그 만큼 길어진다.


소프트웨어에 대한 지식을 공유하는 역량을 키울 수 있는 가장 효과적이면서 가장 쉬운 방법은 그림으로 지식을 표현하는 것이라고 생각한다. 게다가 소프트웨어를 다어그램으로 표현하는 표준인 UML도 있다. 아직 다이어그램으로 소프트웨어를 표현하는데 익숙하지 않다면, 기존에 자신이 만든 코드/소프트웨어/시스템 등을 모두 다이어그램으로 표현하는 연습을 해 보자. 그러다보면 소프트웨어를 정확하게 표현하는 역량이 향상될 것이다.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 대관령 2014.04.08 22:31 신고  댓글주소  수정/삭제  댓글쓰기

    바로 이거야~!
    저도 모르게 무릅치고 박수치고 갑니다.

  2. 붉은세상 2014.08.27 16:22 신고  댓글주소  수정/삭제  댓글쓰기

    회의때를 생각하면 늘 발생하는 상황이네요.. 엄청 공감합니다.

좀 거창한 목표가 하나 있는데, 그것은 "국내 프로그래머들의 전체적인 역량을 한 단계 끌어올리는 것"이다. 정말 거창하다. 나 스스로도 고수가 아닌 상황에서 이런 거창한 목표를 잡은 건, 많은 프로그래머들을 중수로만 끌어올릴 수 있어도 한국의 전반적인 소프트웨어 개발 역량이 높아지지 않을까라는 막연한 기대감을 갖고 있기 때문이다. 내 주제에 이런 걸 할 수나 있을까 하는 의구심이 아주 강하게 든다. 하지만, 학원/학교/회사들은 프로그래머를 성장시키기 위한 노력을 하기 보단 당장 써 먹어야 하는 기술을 가르치는데 급급하고, 프로그래머 스스로 자신을 끌어올리기에는 많은 한계가 있다고 생각한다.


주변을 보면 10년 가까이 지나도록 그 수준에 머물러 있는 프로그래머들이 가득하고, 이 바닥을 발전시켜줄 수 있는 고급 인력은 항상 모자라다. 프로그래머가 만드는 최종 결과물인 코드의 품질은 높아질 줄 모르고 이로 인해 소프트웨어를 만들고 개선하는 비용은 점점 증가하기만 한다. 프로그래머라는 직업이 고난이도의 지식과 사고력을 요구하는 그런 것이 아닌 몸으로 떼우고 시간으로 떼우는 노동 집약적인 것으로 인식되고 있기도 하다. 지식 집약이 아닌 노동 집약이라니... 이러다 보니 미국에서는 선망의 직업인 프로그래머가 한국에서는 기피 대상이 되어가고 있다.


프로그래머를 고용하는 사람들의 인식 구조가 조금씩 바뀌고는 있지만 (예를 들면, SKP나 쿠팡 같은 곳에서 프로그래머를 높은 연봉으로 빨아가는 것) 여전히 많은 고용주는 프로그래머를 벽돌 쌓는데 필요한 일용직 잡부 정도로 생각하는 곳이 많다고 느껴진다. 이런 인식 하에서 일을 빨리 끝낼 수 있는 방법이라곤 시간 투입을 늘려 노동 강도를 높이는 것 뿐이다. 이러다보니 피고용인인 프로그래머들도 더 적은 시간으로 더 나은 결과물을 만드는 방법을 찾기 보다는 (즉, 본인을 성장시키기 보다는), 벽돌 쌓는 기술을 연마하고 시간을 늘려서 일하거나(즉, 잡소리 듣기 싫어서 적당히 눈치봐가며 일하거나) 피똥싸가며 일을 하기에 바쁘다. 이런 상황은 고용인도 피고용인도 서로를 망치는 결과(프로젝트의 오픈 일정이 끝도 없이 뒤로 밀리고, 소프트웨어의 품질은 엉망이고, 오픈 후에도 유지보수에 높은 비용이 발생하는)만 초래할 뿐 윈-윈의 결과를 만들어내지 못한다.


이런 현실을 바꾸려면 회사-프로그래머-사회에 걸쳐 여러 가지 변화가 필요하겠지만, 한 가지 반드시 필요한 변화는 프로그래머들 스스로가 노동 집약이 아닌 지식 집약적인 전문가로 거듭나는 것이라고 생각한다. 프로그래머 스스로가 전문가로서 성장하지 않고서는 프로그래머를 일용직 잡부 정도로 생각하는 인식은 앞으로도 크게 바뀌지 않을 거고, 프로그래머를 이곳 저곳에 팔아먹는 인력 사무소만 활개칠 것이다. 지금도 내 폰엔 개발자 필요 없냐는 인력 사무소의 문자가 왔다.


변화를 만들어보고 싶다. 그래서 거창한 목표를 세우게 되었다. "그래, 프로그래머들의 역량을 한 단계 올려 보자!" 이 목표를 이룰 수 있는 지 여부는 생각하지 않는다. 무엇이 되었든 간에 프로그래머들의 역량을 올려줄 수 있는 것이라면 일단 시작해보자. 하다보면 결론이 나겠지.




Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 김과객 2013.06.11 17:24 신고  댓글주소  수정/삭제  댓글쓰기

    내 실력도 아니고 내 팀 실력도 아니고 내 회사 실력도 아닌 대한민국 전체 개발자의 실력을 번쩍 들어 올리겠다는 야심에 박수 짝짝짝... 모름지기 목표는 그정도 거대해야 맛이죠.

    대한민국 전체의 개발자 실력 향상의 첫 단추로 제 생각은 개발자들 스스로의 자성을 하고실력연마를 해야겠다는 의식을 일깨워야한다고 생각합니다.

    그냥 단순한 당위가 아니라 진짜로 뼛속까지 느끼는 그런 게 필요합니다.

  2. 남학생 2013.09.30 18:09 신고  댓글주소  수정/삭제  댓글쓰기

    화학공학의 전공을 바꾸고 개발자로 전향하는 26살의 학생입니다.
    이렇게 좋은 목표를 가지고 노력하시는분이 있다는 것에 참 감사합니다 ㅎ
    많이 배우고 좋은 프로그래머가 되도록 노력하겠습니다 ^^

  3. Reality 2013.11.29 22:02 신고  댓글주소  수정/삭제  댓글쓰기

    안타까운 현실은,

    구인구직 사이트에 대기업을 제외하고 어떤 기업도 프로그래머를 직원으로 고용치 않고 온통 인력사무소 구직정보만 널려 있다는게 안타깝네요..

    프로그래머 접은 1人

  4. 오치문 2014.02.26 09:46 신고  댓글주소  수정/삭제  댓글쓰기

    목표가 훌륭하시군요.
    경험을 공유해주시는 것만으로도 큰 영향을 미치고 있다고 생각합니다. :)

요즘 책을 쓰고 있다. 그 동안 써 오던 책들이 사용법 위주 책이었다면 이번 책은 이론에 가까운 책이다. 


책에 대한 반응이 싸늘하면 어쩌지? 너무 안 팔려서 출판사가 손해가 난다면?


오늘도 이런 걱정이 날 사로잡고 있다. 여러 종류의 책을 썼고, 운 좋게 팔린 책도 있지만, 그렇지 않은 책이 더 많다. 2년 전에 출판된 자바 기초 책은 출판사에 민망할 정도로 속된 말로 망했다.


그래도 시도는 해 봐야 한다. 10년 전에 MVC 프레임워크를 주제로 책을 썼던 때처럼, 6년 전에 한글로 된 레퍼런스가 필요할지 모른다고 생각하며 썼던 스프링 책처럼, 시장에 압도적 1위가 있지만 그래도 나만의 방식으로 자바 안내서를 만들고 싶었던 2년 전처럼, 시장에서 ORM을 의심하던 시절(지금도 그런 듯 하지만)에 썼던 하이버네이트 책 처럼.


시장에서의 반응은 알 수 없지만, 걱정을 뒤로 하고 담고 싶은 내용을 잘 담아내는데 집중하자.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 나그네 2014.03.10 09:39 신고  댓글주소  수정/삭제  댓글쓰기

    개발자가 정복해야 할 객체 지향과 디자인패턴 정말 잘보고 있습니다.
    그러고 보니 책장에 범균님 책이 4개나 있네요 ㅋㅋ
    잘보구 있으니 힘내세요!!!

    범균님 책으로 누군가는 지식과 행복을 느끼고 있는 구독자가 있다는걸 명심하세요
    앞으로도 좋은책 많이 써주세요!! 화이팅

"올 겨울 아우터 스타일링은 블랙컬러로 시크하게 연출하거나 크레이지한 컬러패딩으로 유니크하거나 펑키하게 연출하는 ... " 


이 문장을 보면 무슨 생각이 드는가? 전문가임을 티내려는 걸까? 아니면, 영어를 안다고 잘난 척 하려는 걸까? 그런데, 이런 문장은 의상 디자인에서만 사용되는 게 아니다. 몇 년전 국내 개발 관련 컨퍼런스에서 이런 저런 발표를 듣고 있는데, 함께 듣던 후배가 이런 말을 했다.


"무슨 발표 자료를 다 영어로 만들었어요. 영어 잘 한다고 잘난체하려는 건지.... 말 중간 중간에도 어찌나 영어 단어를 많이 쓰는지 짜증나더라구요."


정말이지 발표자의 자료는 온통 영어였고 (영어 단어, 영어 문장, 영어 이미지), 한글로 된 건 발표자의 이름 뿐이었다. 물론, 발표하는 내내 영어 단어를 많이 섞었음은 두말할 필요도 없다. 그런데, 얼마전에 다른 컨퍼런스에 사용된 발표자료를 봤는데, 뜨아... 이것도 역시나 온통 영어였다. 물론, 영어를 사용하는 사람들을 위한 컨퍼런스는 아니었고 국내에서 한국 개발자들을 위해 개최된 그런 컨퍼런스였다.


청자는 어디로?


후배는 다음과 같이 심정이었을 것이다.

발표자가 발표를 시작했는데 영어가 펼쳐진다. 발표자는 그 화면과 관련된 내용을 한글과 영어를 섞어가며 설명을 한다. 젠장, 난 집중해서 재빠르게 영어 문장을 읽어서 그 내용을 이해한 다음에 발표자가 하는 말과 내 눈에 비치는 문장을 함께 이해해야 한다. 영어를 재빠르게 읽는 능력이 없을 뿐더러 영어 자료와 한국어(물론, 영어 단어가 많이 섞인) 발표가 잘 섞이지도 않는다. 안 그래도 이해 안 되는 내용이 더 이해가 안 되기 시작한다. 이번 시간은 포기다.

그래, 결국은 내용을 제대로 이해하지 못하는 상황이 발생한다. 발표자는 단지 자신의 지식을 자랑하러 나온 건 아닐 것이다. 그 지식을 함께 나누기 위해 나왔을 것이다. 그런데, 영어로 범벅이 된 자료라니, 거기에 영어로 된 단어 남발이라니.. 


물론, 소프트웨어 개발의 특성상 영어 단어를 많이 사용할 수 밖에 없다는 점을 이해한다. 게다가, 특정 단어는  영어 단어를 사용할 때 그 의미가 명확하게 와 닿는 경우도 많다. 하지만, 그렇다고 하더라도 온통 영어 문장에 영어 단어인 발표자료를 모든 청자들이 이해할 수 있는 것은 아니다.


그렇다고 하더라도 99% 영어로 된 자료는 문제가 있는 것 아닌가? 이건 한국어를 사용하는 사람들을 위해 한국에서 개최되는 컨퍼런스에서 발표자가 취할 자세는 아닌 것 같다. 차라리 깨알같은 글씨로 만든 한글 발표 자료는 읽을수라도 있다. 그런데, 영어로 된 자료는 지식을 얻어갈 가능성을 박탈해버린다. 이럴거면 시간 내서 컨퍼런스에 참여한 이유가 없어진다.


발표자가 참석한 모든 사람들에게 지식을 공유하는 것은 불가능하겠지만, 적어도 그 가능성은 높여야 한다고 생각한다. 앞으로 있을 컨퍼런스들은 영어 자랑보다는 지식 공유에 좀 더 초점을 맞춘 그런 발표자분들이 많았으면 좋겠다.

Posted by 최범균 madvirus

댓글을 달아 주세요

흔히들 실무와 관련된 학습을 한다는 분들의 말을 듣다보면 그 실무라는 것이 사용 기술에 특화된 경우를 많이 본다. 스프링 프레임워크의 사용법이라든지, jQuery 사용법, 하둡 설치, Redis 연동, Node.js 이용한 메시지 처리 등 뭔가 기술 사용에 대한 것들이 많다. 그런데, 실무라는 게 진짜 이것 뿐인가? 그래서, 개발자에게 있어 실무가 뭔지 고민을 좀 해 보려고 한다. 


실무의 사전적 정의는 '실제적이고 구체적인 업무'이다. daum 사전을 검색해 보면 '실제'란 '있는 사실이나 현실 그대로의 상태나 형편'이고 '구체'란 '사물이나 현상이 일정한 모습을 갖추고 있는 것'이다. 이런 의미를 종합해보면 개발에 있어 실무란 '소프트웨어를 만들기 위해 수행하는 모든 업무'라고 볼 수 있을 것 같다.


소프트웨어를 만들기 위해 수행하는 모든 업무를 이곳에 다 나열할 수 없지만 (나 스스로 이걸 다 알지 못한다), 잠깐만 생각해봐도 다음과 같은 것들이 떠오른다.

  • 코딩
  • 요구사항 분석(요구사항 이해)
  • 설계 / 모델링
  • 테스트/QA (기능, 성능)
  • 소스 관리 / 버전 관리
  • 배포 / 인프라 관리
  • 운영
  • 일정 관리 / 비용 관리 (최소한 일정과 비용에 대한 개념)

하나의 소프트웨어가 만들어지려면, 위의 모든 작업들이 유기적으로 연결되어 실행되어야 한다. 요구사항이 제대로 분석되지 않은 상황에서 코드를 작성하면 의미없는 소프트웨어가 만들어질 것이다. 설계가 유연하지 않으면 요구사항의 변화를 제대로 수용할 수 없을 것이다. 소스 관리와 배포 관리가 되지 않으면, 이전 버전이 배포될 지도 모른다. 일정 관리가 되지 않으면 제때에 소프트웨어를 내놓지 못할 수도 있다.


이 모든 것들이 실무다. 단지, 스프링을 익히고, jQuery를 익히는 것만이 실무는 아닌 것이다. 스프링이나 jquery를 익히는 건 코드를 만드는 역할로서의 실무를 하기 위한 활동일 뿐이다. 개발자는 코더일 뿐만 아니라 코드를 만들기 위한 설계자로서 역할도 수행하며, 이를 잘 수행하기 위해서 요구사항을 분석하는 역할도 수행해야 한다. 또한, 본인이 만든 코드에 결함이 없도록 하기 위해 테스트도 어느 정도 수행해야 한다. 게다가 작은 조직이라면 직접 배포도 해야 하고, 일정도 어느 정도는 스스로 관리해야 한다. 소프트웨어에서 실무란 이런 작업들을 모두 포함하고 있는데, 단지 실무의 범위를 코드를 만드는데 사용되는 기술만으로 한정짓는 것은 건축 현장에서 벽돌을 쌓는 걸로 본인의 작업을 한정하는 것과 크게 다르지 않다.


개발자로서 실무를 잘 하려면 단지 특정 기술을 익혀 코딩하는 것만으로는 안 된다. 이건 소프트웨어를 만드는 데 있어서 가장 기본일 뿐이다. 코딩을 하지 않으면 소프트웨어는 만들어지지 않으므로 구현 기술을 익히는 것은 가장 기본이면서 가장 중요하지만, 이것만으로는 '좋은' 소프트웨어를 만들 수 없다.


사용자가 원하는 '좋은' 소프트웨어를 만들려면, 구현 기술뿐만 아니라 요구사항 분석 실무를 익혀야 한다. 또한, 요구사항의 변화를 적은 비용으로 대처할 수 있도록 유연한 설계를 만들 수 있는 방법을 익혀야 한다. 소프트웨어 결함을 낮추는 데 도움이 되는 테스트를 만드는 방법도 익혀야 한다. 이 뿐인가,, 서비스 중단을 최소화하면서 기존 소프트웨어를 업그레이드하는 방법도 익혀야 한다. 또한, 역할에 따라서 프로젝트 일정을 관리하고, 투입 예산을 관리하는 방법도 익혀야 한다.


이런게 다 실무다. 코딩만이 실무가 아닌 것이다. 이런 실무들을 잘 하려면 구현 기술을 익히기 위해 책/온라인자료/오프라인 강좌 등을 통해서 학습하는 것 처럼, 설계를 학습하고, 좋은 코드를 만드는 방법을 학습하고, 프로젝트 관리를 학습하고, 테스트를 학습해야 한다. 벽돌 쌓는 기술로 1층짜리 벽돌집은 간신히 만들 수 있을지는 모르겠으나, 고층빌딩은 고사하고 3-4층되는 집도 만들 수 없을 것이다. 비슷하게 소프트웨어 개발자도 간단한 코드는 만들 수 있을지 모르겠지만, 규모가 조금만 커져도 소프트웨어는 점점 엉망이 되서 더 이상 발전할 수 없는 그런 결과물이 만들어질 것이다.


이제 갓 이 바닥에 발을 들여 놓은 개발자들이 실무를 단지 벽돌쌓기 기술을 향상하는 것으로만 생각하지 않았으면 좋겠고, (나름 포함한) 많은 개발자들이 다양한 영역의 실무를 익혀서 좋은 소프트웨어를 만들 수 있는 개발자로 성장할 수 있기를 바란다.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 이루리 2013.04.05 16:57 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글 감사합니다.
    비록 제 현실과는 맞지 않지만 저렇게 업무를 할 수 있는 회사로 이직하는 날을 꿈꾸며

  2. 큐라 2013.04.13 15:27 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글이군요..
    역시 실무에 핵심은 지루한 회의가 아닐까요?..

  3. bluepoet 2013.04.29 15:19 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 포스팅 잘 읽었습니다.

    아래부분이 핵심 요지가 아닌가 싶네요^^

    ==============================================================================

    사용자가 원하는 '좋은' 소프트웨어를 만들려면, 구현 기술뿐만 아니라 요구사항 분석 실무를 익혀야 한다. 또한, 요구사항의 변화를 적은 비용으로 대처할 수 있도록 유연한 설계를 만들 수 있는 방법을 익혀야 한다. 소프트웨어 결함을 낮추는 데 도움이 되는 테스트를 만드는 방법도 익혀야 한다. 이 뿐인가,, 서비스 중단을 최소화하면서 기존 소프트웨어를 업그레이드하는 방법도 익혀야 한다. 또한, 역할에 따라서 프로젝트 일정을 관리하고, 투입 예산을 관리하는 방법도 익혀야 한다.