주요글: 도커 시작하기

[3부]에 이어서

 

4월 16일 목요일. 미디어는 온통 415 총선 얘기다. 충격적인 결과 때문인지 다양한 기사가 쏟아져 나온다. 온라인 개학도 본격적으로 시작했다. 학생들이 온라인 수업에 참여해서 그런지 오전 트래픽이 줄었다. 별일 없이 하루가 지나가고 있다.

오후 4시 40분. 평온함을 깨는 전화가 온다. 발신자를 보니 감이 온다. M사에 문제가 생겼나 보다. 아니나 다를까, M사 서비스에 장애가 났으니 내일 가서 지원하라는 지시가 내려왔다. 학교 선생님과 학생이 온라인 수업 보조 도구로 M사 서비스를 사용하는데 트래픽이 몰리면서 M사 서비스가 다운된 것이다. 온라인 개학이 시작되면서 EBS를 비롯해 여러 서비스가 먹통 됐는데 M사 서비스도 그중 하나였던 것이다.

일시 정지

4월 17일 금요일. 바로 M사로 출근했다. A클라우드의 관리형 DB 성능이 저하되면서 장애를 일으켰다고 한다. 성능 문제를 일으킨 건 게시글 내용과 관련이 있다. 내용 칼럼은 MySQL MEDIUM TEXT 타입인데 어떤 이유인지 내용 칼럼을 조회하면 급격히 응답 속도가 느려진다. 기능 특성상 첫 화면부터 게시글 목록에 내용을 보여주기 때문에 게시글 내용 성능 저하가 바로 서비스 먹통으로 연결된 것이다.

M사 인프라 조직은 이대로는 서비스를 할 수 없다고 판단하고 클라우드 DB를 빼기로 결정했다. 대신 비슷한 성능의 VM을 다수 생성해서 직접 마스터/슬레이브 DB를 구성한다고 한다. DB 변경 일정은 오후 7시부터 시작해서 4월 20일 새벽까지로 예정되어 있다.

 

이번 작업의 핵심은 마스터/슬레이브를 구성해서 처리 용량을 높이는 것이므로 이를 위한 코드 작업을 먼저 진행했다. 스프링을 사용하고 있지만 @Transactional을 사용하기 어려운 구조다. 그래서 필터를 선택했다. URL 경로에 따라 마스터나 슬레이브를 선택하도록 필터를 구현했다. 세 개 정도 경로를 넣고 확인해보니 잘 동작한다. 다음 작업으로 넘어가야겠다.

 
이어서 내용 칼럼만 조회하는 코드를 분리하기 시작했다. 내용만 캐시를 적용할 수 있는 구조로 만들기 위함이다. 앞서 게시글 조회 API를 개선해 놓지 않았으면 할 수 없는 작업이었다.

 

오후 7시가 되었다. 앱에는 작업 공지가 올라왔다. J 개발자와 난 하던 작업을 멈추고 J 개발자는 모든 서버를 내렸다. DB 사용이 없어지자 인프라 조직에서 클라우드 DB 데이터 덤프를 시작한다. 우리는 하던 작업을 이어서 진행했다. 내용 조회 코드 분리를 마치니 9시가 넘어간다. 나머지는 내일 진행해야겠다.

맑은 하늘

4월 18일 토요일. 판교에 도착하니 9시다. 스타벅스에 들려 커피를 마시고 있는데 J 개발자가 들어온다. J 개발자가 커피를 들고 자리에 오더니 불안한 표정으로 아직 DB 구성 작업이 끝나지 않았다고 말한다. 당연히 끝나 있을 거라 생각했는데 실패했다는 것이다. 이유가 궁금했다.

 

J 개발자와 사무실로 들어갔다. 얼마 지나지 않아 인프라 조직장이 왔다. 얘기를 들어 보니 관리형 DB에서 덤프 받는 속도가 매우 느렸다고 한다. 시간당 수십 메가 수준이었다고 한다. 어쩌면 게시글 내용 조회 속도가 느렸던 것도 관리형 DB의 IO 성능 문제가 아니었을까 하는 의심이 들기 시작한다.

 

DB 문제를 해결하는 동안 J 개발자와 난 게시글 내용을 캐시에 담는 코드를 구현했다. 게시글 쓰기, 수정, 조회 기능에 레디스 연동 코드를 추가했다. 얼추 잘 된다. 이어서 추천 게시글 목록에도 캐시를 적용했다. 추천 게시글 목록은 양도 크지 않고 변경도 없어 카페인 캐시를 사용했다. 불필요한 카운트 쿼리 때문에 간헐적으로 응답이 느린 코드도 손을 봤다. 게시글 조회 트래픽이 전체 서버 쓰레드를 점유하는 것을 막기 위해 동시 처리 개수를 제한하는 코드도 추가했다.

 

어느덧 5시. 저녁을 먹는 동안에도 인프라 조직은 DB 관련 통화를 하느라 바쁘다. DB 덤프 속도가 오후부터 빨라져서 곧 DB 구성을 할 수 있을 것 같다고 한다. 다행이다.

 

저녁을 먹고 옥상에 올라갔다. 3월엔 어둑어둑했는데 지금은 어둡지 않다. 하늘이 참 맑다. 토요일이 그렇게 지나갔다.

 

4월 19일 일요일. DB 구성이 잘 되었다고 한다. M사 직원들은 오전부터 운영 환경 테스트를 진행하고 있다. 난 이제 할 일이 없다. 부하 테스트를 진행해서 검증하고 싶지만 지금은 진행할 수 있는 사람이 없다. 불안하다. 그저 내일 트래픽을 잘 견뎠으면 하는 마음뿐이다.

태풍

4월 20일 월요일. 잠이 잘 안 와 일찍 깼다. 판교에 도착하니 7시 30분이다. 과연 잘 될까?

8시 15분. 서버가 출렁인다. 스카우터를 보니 레디스 연동에서 시간이 오래 걸린다. 젠장! 이것 때문에 게시글 목록 응답 시간이 느리다. 응답이 느려지자 동시 처리 개수 제한에 걸려 503 응답을 주기 시작했다. 아침부터 긴박하다.

 

잠시 후 J 개발자가 출근해서 급하게 레디스 연동을 제거하고 다시 배포했다. 레디스 설정을 쳐다볼 때가 아니다. 일단 살고 봐야 한다. 게시글 목록 조회는 돌아오기 시작했지만 여전히 느린 응답이 있다. 로그인하자마자 불리는 API에 풀스캔을 타는 쿼리가 있다. 뭐지? 쿼리를 보니까 조건이 빠져있다. 왜?

 

한참 만에 원인을 찾았다. 리플리케이션이 밀린 것이다. 그 망할 모듈 때문에 문제가 심해졌다. 리플리케이션이 밀리면 데이터가 안 나오면 그만인데 이 모듈은 where 절에서 조건이 빠진다. 그러면서 풀스캔이 발생한 것이다. 아 씨땡이다. 하지만 이 족보 없는 모듈을 붙이고 간 사람을 욕하고 있을 시간이 없다. 트래픽 태풍을 어떻게든 방어해야 한다. 풀스캔이 발생하는 기능을 찾아서 방어 코드를 넣었다. where 절에 조건이 빠지는 상황이면 아무 값이나 설정해서 조건이 빠지지 않도록 했다. 배포하니 서버가 나아진다.

 

여전히 느린 API가 있다. J 개발자에게 물어보니 아직 오픈하지 않은 기능이란다. 앱과 서버에 선반영 했는데 화면에는 노출되는 곳이 없단다. 이 API는 서버에서 다시 P 서비스를 호출하는데 이 P 서비스가 응답을 늦게 주는 것이다. P 서비스는 타 팀 서비스라 손을 댈 수 없다. 고민하다 일단 P 서비스에 대한 연동을 끊기로 했다. 앱에서 API를 호출하면 빈 응답을 주도록 코드를 바꾸고 배포했다. 드디어 느린 응답이 사라졌다!

 

벌써 오후다. 이번에는 업로드가 말썽이다. 사용자가 몰린 것이다. 그래도 전반적인 서비스는 안정적이다. 업로드만 조치하면 된다. 업로드 서버를 2대에서 4대로 늘려서 급한 불을 껐다.

 

안정

4월 21일 화요일. 8시 40분, 9시, 10시, 11시.... 오후 5시. 평온하다. 간헐적인 오류도 있긴 했지만 비교적 하루가 무사히 지나갔다. 고생한 J 개발자에게 수고했다고 슬랙 메시지를 보냈다. 모든 게 평화롭다. 긴장이 풀리면서 피로가 밀려온다.

 

다시 에러

4월 22일 수요일. 오전 10시. 갑자기 응답 시간이 느려진다. 9시도 잘 넘겼는데 10시에 왜? 원인을 모르겠다. J 개발자는 점진적으로 서버를 재시작했다. 문제가 사라졌다. 뭐지?

4월 23일 목요일. 오전 10시. 또 응답 시간이 느려진다. 원인을 모르겠다. 급하니까 일단 재시작이다. 잠시 후 신기하게 멀쩡해졌다. 어제와 증상이 동일하다. 증상만 보면 해당 시점에 마스터 DB로 연결을 구하지 못해 에러가 발생했다. J 개발자와 통화를 해서 몇 가지 조치를 취했다. 슬레이브를 사용하는 URL을 더 늘렸다. 설마 이 정도까지 했는데 마스터 연결이 안 될까?

 

4월 24일 금요일. 오전 10시가 다가온다. 젠장! 또 같은 증상이다. 평균 응답 시간, TPS를 계산했을 때 이 정도로 무너지면 안 된다. 게다가 피크 시간은 8시 30분에서 9시 사이다. 9시를 잘 넘겨 놓고 10시에 왜 이 모양이냐? 원인을 모르겠다. 이번에도 급하게 재시작이다.

10시 15분. 메시지가 하나 왔다. 차마 남들에겐 언급할 수 없는 이유로 10시부터 약 10~15분 정도 마스터 DB로의 연결이 잘 안 되었다고 한다. 아... J 개발자와 난 상상조차 하지 못했다.

 

이젠 괜찮겠지 했는데 오후에 메신저가 시끄럽다. 파일 업로드가 안 된다. CDN이 업로드 트래픽을 견디기 힘들어 M사 서비스에 대한 업로드를 차단했단다. 정말 다양한 일이 벌어지는구나! 속도를 늦추는 것도 아니고 업로드 일시 차단이라니. M사는 재발을 막기 위해 CDN 관련 작업을 4월 25일에 진행했다.

 

4월 27일 월요일 오후 6시. 오늘 하루가 고요하다. 이제 끝인가 보다. 마침 J 개발자가 다른 일로 서울 사무실에 왔다. 저녁 시간도 다 됐고 겸사겸사 술 한잔 기울이며 서로를 다독인다.

  1. MSX2+ 2020.04.29 16:08

    흥미진진하게 잘 읽었습니다! 남들에게 언급할 수 없는 이유가 너무 궁금하네요.

  2. 애송이 2020.05.13 18:08

    재밌게 잘 읽었습니다.

  3. msa 2020.06.03 23:48

    차마 남들에겐 언급할 수 없는 이유때문에 현기증 납니다...

  4. 2020.06.16 22:49

    비밀댓글입니다

  5. 루키로키 2020.06.23 15:36 신고

    너무 재밌게 잘 읽었습니다 ! 긴박함이 생생하게 느껴져서 더 몰입하면서 읽은 것 같네요.

  6. 2020.07.11 10:40

    비밀댓글입니다

[2부]에 이어

 

앱에는 아직 새로 만든 API가 반영되지 않고 있다. 웹도 극히 일부만 적용했다. 서버와 클라이언트 모두 성능 개선과 기능 개발을 병행하고 있다. 숨이 꼴딱 꼴딱 넘어가기 직전인데 기능 개발을 함께 하고 있다. 이러고 있을 처지가 아닌데 여유를 부리고 있다. 난 외부 인력이어서 여기에 대해 뭐라 말할 처지가 못 된다.

3월 20일. 결국 장애가 났다. 데이터가 늘어나면서 쿼리 시간이 조금씩 증가하더니 어느 순간에 급격히 무너졌다. 원인은 이번에도 풀스캔. 데이터가 적을 때는 느리지 않아 발견하지 못하고 놓친 쿼리가 문제를 일으켰다. 슬슬 느려지는 추이를 보이고 있었는데 기능 개발하느라 놓친 것이다. 3월 25일까지 비슷한 이유로 몇 차례 장애가 더 발생했다. 그때마다 M사는 정신없는 상황이 반복되었고 J 개발자는 하던 일을 멈추고 장애 처리에 시간을 보내야 했다.

 

드디어 배포

3월 26일. 드디어 수정한 API를 사용한 안드로이드 앱을 배포했다. 안을 제안한지 20일 만이다. 여전히 기존 API를 사용하는 곳이 많지만 전반적인 응답 시간은 확연히 줄었다. 다행이다.  게시글 목록 조회/상세 API를 앱의 일부 화면에 적용했을 뿐인데도 효과가 있다. 기세를 몰아서 아직 개선 못한 부분을 진행하면 좋을 것 같다.

 

J 개발자에게 연락이 왔다. 이제 기능 개선을 안 하고 기능 개발에 집중한단다. 아직 해야 할게 많이 남았는데 성능 개선을 멈추는 것이다. 성능 개선을 더 하길 원했지만 결정권자가 기능 개발을 지시했다고 한다. 결국 J 개발자는 더 이상 성능 개선을 못하고 기능 개발에 집중하기 시작했다. 안타깝다. 당하고 또 당했는데 이렇게 결정하다니. 아직 더 많이 개선해야 하는데 이 수준에 멈추다니. 걱정이다.

 

고요

4월 초. 앱을 배포하고 1주가 지났다. 아직까지 서버 상태는 고요하다. 현재 수준의 트래픽은 무난히 처리하고 있다.

 

뉴스는 온라인 개학 얘기로 시끄럽다. 코로나 여파로 개학을 미뤄왔는데 더 이상 미룰 수가 없다고 판단을 한 것 같다. 학생이 모이지 않게 온라인 수업을 검토하고 있다는 언론 보도도 나오기 시작했다.

 

M사는 추가 기능 개발에 한참이다. 나도 지원하느라 제때 처리하지 못한 일에 집중하고 있다. 서버 성능은 더 이상 관심 대상이 아니다. 그렇게 아무도 신경 쓰고 있지 않을 때 온라인 개학이 눈 앞으로 다가왔다.

 

[4부]에서 계속 ......

[1부]에 이어

 

스카우터에 찍힌 쿼리와 코드를 계속해서 탐색했다. 몇 가지 개선해야 할 것이 눈에 보이기 시작했다. 코드가 변경이 쉬운 구조는 아니지만 J 개발자에게 코드의 구조와 동작 방식을 들으면서 변경 방법을 조금씩 찾기 시작했다.

 

이 와중에

J 개발자와 코드를 이리저리 보고 있는데 다급한 표정으로 누군가 다가온다. 푸시 발송에 문제가 있단다. 개선안 찾던 걸 멈추고 푸시 문제로 넘어갔다. 스카우터를 보니 푸시 서버 관련 XLog가 제일 위에 붙어 있다.

일단 푸시를 내렸다. 코드를 봤더니 푸시 목록을 DB에서 조회하고 푸시를 발송하고 발송 상태로 변경하는 작업이 한 트랜잭션으로 묶여 있다. 이를 주기적으로 실행해서 푸시를 발송하고 있는데 발송 대상이 늘어나면서 실행 주기 안에 푸시 발송을 끝내지 못했고 이로 인해 이슈가 발생하기 시작했다. 아직 푸시 처리를 하고 있는데 다시 푸시 처리를 요청하면서 락이 발생했고 그러면서 전체 푸시 발송이 먹통이 되었다.

당장 구조를 변경할 수 없어 중복 실행을 방지하는 쪽으로 방향을 잡았다. 푸시 발송 상태 필드를 추가하고 아직 발송 중이면 실행 요청을 무시하는 코드를 넣었다. 수정한 코드를 배포하고 푸시를 재개했다. 다행히 문제가 재발하지 않는다.

급한 불을 끄고 J 개발자와 옥상에 올라갔다. J 개발자는 담배 한 모금과 긴 한 숨을 내 쉬며 마음을 가라 앉힌다. 판교 하늘이 어두워지기 시작했다.

 

다시 안 도출

저녁을 먹고 자리로 돌아와 다시 안을 도출하기 시작했다. 한 번에 모두 바꿀 수 없으니 조회 성능을 높이는데 초점을 맞췄다. 크게 4개 정도 안을 도출했다.

  • 자주 불리는 기능에 대해 조회 전용 모델 개발. 현재는 하나의 모델을 그것도 복잡하게 연관을 맺은 모델을 명령과 조회에 함께 사용하고 있어 조회 시 불필요하게 많은 쿼리를 실행하고 이로 인해 응답 시간이 길어지고 있다.
  • 집계용 테이블 추가. 게시물의 댓글 개수를 댓글 테이블에 count 쿼리를 날려서 구하고 있다. 좋아요 수도 동일한 방식으로 구하고 있다. 이 또한 응답 시간을 증가시키고 있다.
  • 조회 기능 일부에 캐시 적용. 조회 전용 모델을 만들어내는데 성공하면 일부 기능에 캐시를 적용할 수 있게 된다. 아직은 바로 할 수 없다.
  • 채팅 용도 서버 분리. 지금은 API 서버가 모든 요청을 처리하고 있다. 채팅 트래픽이 많지는 않지만 응답 시간이 증가해 톰캣 쓰레드를 잡고 있으면 다른 기능도 영향을 받게 된다. 당장 DB 분리는 못 하더라도 채팅 전용 서버라도 따로 분리해 영향을 줄이고 싶다.

안을 정리하고 나니 벌써 금요일 저녁 10시다. 집에 가야겠다.

 

구현 시작

3월 9일. 주말을 쉬고 돌아온 월요일. M사에 해당 서비스를 책임지는 서버 개발자는 J 혼자다. M사의 다른 조직에도 서버 개발자가 있긴 한데 적극적으로 돕지는 않고 있다. 이대로 가만있으면 도출한 안을 실행하지 못할 것 같다. 일부라도 실행하려면 M사에 가서 밀어 붙여야 한다. 점심을 먹고 M사로 이동했다.

 

M사에 도착했다. 여전히 다들 정신이 없다. 한가지 다행이라면 J 개발자가 채팅 용도 서버는 분리했다는 것이다. 하나를 했으니 다른 하나를 진행하면 된다. 오늘의 목표는 게시글 조회 전용 모델을 만드는 것이다. 이걸 만들지 못하면 다음 단계로 넘어갈 수 없다.

 

기존 게시글 모델을 복사해서 새 모델을 만들었다. 조회에 불필요한 사용자 엔티티, 첨부 파일 엔티티 등 대부분 연관을 삭제했다. 첨부파일의 썸네일 이미지 경로는 코드에서 조합했다. 새 모델을 위한 조회 API는 별도 클래스로 직접 구현했다. 나중에 일부라도 캐시를 적용하고 싶어서다.

 

현재 게시글 목록 조회 API는 페이지 단위로 조회하고 있다. 여기에 like 검색도 섞여 있다. 페이지 개수를 구하기 위해 count 쿼리도 날린다. 이대로는 성능 개선이 어렵다. 마침 등록 시간에 인덱스가 걸려 있는 걸 확인했다. 다행이다. 이걸 쓰면 되겠다. 조회 전용 게시글 모델 목록 API는 등록 시간 기준 from - to 조건과 조회 개수 제한 limit을 사용해서 목록을 조회하게 구현했다. 이를 이용하면 일정 시간 내에 목록을 제공할 수 있을 것 같다.

 

새 게시글 목록 API를 다 만들고 개발에서 기능을 확인했다. 끝나고 나니 저녁 10시가 되었다. 월요일부터 힘들다. 못하겠다. 퇴근이다.

 

서버 다운

3월 10일 화요일. 오전부터 응답 시간이 폭증한다. 일부 서버가 풀GC를 미친듯이 하고 있다. 특정 게시글을 조회할 때 발생한 것으로 보인다. 거미줄처럼 엮어 있는 연관 관계 때문에 조회 결과에 연관된 엔티티 개수가 많은 게시글이 포함되어 응답 시간이 느려진 것이다. 0.01초 걸리는 쿼리를 1000번 돌리면 10초가 걸리는 것과 같은 효과다. 응답이 빨리 안 오자 새로고침을 계속하고 그러면서 메모리 사용이 증가했고 이것이 풀GC를 유발했으며 그러다 어느 순간 죽은 거다.

 

당장 살아야 하니 급한대로 톰캣이 사용하는 메모리를 늘려서 재시작하자고 했다. 조회 전용 모델을 빨리 운영에 반영해야 한다. 안 그러면 또 같은 일이 벌어질 것이다.

 

작은 실랑이

3월 11일. 수요일. 앱 개발자/앱 기획자와 만났다. API 변경에 대해 얘기하기 위함이다. 서버를 M사에서 진행했다면 앱은 I사에서 진행했다. I사는 내가 다니는 회사다. 그렇다. 같은 회사 직원을 만나 회의를 진행했다.

앱 쪽과 먼저 협의하지 않고 서버 API 변경을 진행하는 것이 서운했는지 반응이 좋지 않다. 당장 시작해도 늦었는데 이런 반응이라니. 순간 목소리가 높아졌다. 몇 차례 언성을 높였다가 정신을 차리고 다시 논의를 했다. 사실 앱 개발자도 처음부터 서버 API가 많이 이상해서 바꿔달라고 요구했지만 그 프리랜서 개발자가 고집을 피우며 바꾸지 않았다고 한다. 아...... 마음을 가라 앉히고 다시 API 변경에 대해 논의했다. 안을 정리했고 회의 말미에는 언성을 높인 것도 사과했다.

 

J 개발자에게 회의 내용을 공유하고 조회 전용 모델을 더 추가해 달라고 말했다.

일주일

3월 12일. 목요일이다. 처음 연락을 받은 뒤로 일주일이 지났다. 그 사이에 J 개발자가 인덱스 몇 개를 더 추가했다. 그 덕에 당장은 버티고 있다. 하지만 이걸로는 안 된다. 트래픽이 조금만 증가해도 무너질 것이다. 마음이 급하다. 오후에 다시 M사로 이동했다.

 

오늘은 게시글 관련 집계 정보를 별도 테이블로 구현하는 것이 목표다. 댓글 수, 좋아요 수를 담을 테이블을 구성하고 개발을 시작했다. 댓글 생성 기능에 이벤트 발생 코드를 추가했다. 이 이벤트를 수신할 핸들러를 만들고 핸들러에서 댓글 수 증가 기능을 호출하게 구현했다. 핸들러는 비동기로 실행했다. 카운트 증가 감소는 JdbcTrmplate을 이용해서 쿼리로 구현했다. JPA를 써야 할 이유가 없다. 게시글 조회수 증가도 변경했다. 기존에는 게시글 엔티티의 readCount 값을 1 증가하는 방식이었는데 이를 댓글 개수와 동일하게 이벤트+비동기 핸들러+쿼리로 바꿨다.

새로 만든 게시글 목록/상세 API에 카운트 테이블 연동을 추가했다. 목표를 달성했다. 끝내고 나니 10시가 넘었다. 퇴근이다.

 

초조

3월 13일. 금요일. J 개발자와 통화를 했다. 게시글 관련 집계 정보를 별도 테이블이 아니라 게시글 테이블에 칼럼을 추가한 방식으로 구현을 바꿨단다. 음... 분리한 이유가 있는데. 지금은 그 이유를 설명하고 있을 여유가 없다. 일단 하나를 했으니 다음을 진행해야 한다.

 

이후로 몇 일이 지났다. J 개발자의 노력으로 몇 개 기능에 대해 조회 전용 모델이 완성됐다. 하지만 웹과 앱에는 아직 반영되지 않고 있다. 초조하다. 빨리 적용해야 하는데...

[3부]에서 계속 ......

2020년 4월 21일 화요일 오전 8시 20분. 어제 이 시간에는 이미 서버 응답 시간이 증가하기 시작했다. 간헐적으로 됐다 안 됐다를 반복하다 8시 40분 정도부터 서비스를 사용하기 힘든 수준으로 서버 상태가 나빠졌다. 온라인 개학이 시작한 4월 17일부터 여러 온라인 서비스가 먹통 증상을 보이고 있다지만 오늘도 서비스에 문제가 발생하면 안 된다. 고객 이탈이 심해질 것이다.

 

8시 40분, 9시, 10시, 11시 .... 오후 5시. 평온하다. 간헐적인 오류도 있긴 했지만 비교적 하루가 무사히 지나갔다. 고생한 J 개발자에게 수고했다고 슬랙 메시지를 보냈다. 긴장이 풀리면서 피로가 밀려온다.

시작

2020년 3월 5일 오후. 관계사인 M사의 서비스에 성능 문제가 있으니 같이 보라는 지시가 내려왔다. JPA를 이용해서 서버를 개발했는데 내가 JPA 경험이 있다는 이유로 나한테까지 연락이 왔다. 대충 들어보니 사용자가 증가하면서 서비스가 먹통이 되었다고 한다.

 

M사 서버 개발자 J에게 연락해서 VPN, 스카우터, DB 정보, 소스 리포지토리 주소를 받았다. 스카우터로 확인해보니 XLog의 많은 점이 폭포처럼 찍혀 있다. 나이아가라 폭포 같다. XLog에서 느린 쿼리를 몇 개 찾아 실행 계획을 확인했다. 풀스캔이다. 이런 쿼리 몇 개를 찾아 인덱스 추가를 요청했다.

 

다음날 점심을 먹고 있는데 전화가 왔다. M사에 직접 가서 지원하라는 지시였다. 식사를 마치고 바로 M사로 이동했다. 사무실에 도착해서 현 상태를 들었다. 클라우드 DB의 CPU와 IO 성능을 높였는데 일시적인 효과는 있었지만 여전히 응답 시간이 느린 상태라고 한다. 상황이 나빠서인지 서버 개발자 근처에 이사람 저사람 우루루 몰려와 각자 떠들고 있다. 개중에는 윗사람에 잘 보이려고 온 인간도 보인다. 별 도움 안 되는 소리만 하면서 시간을 뺏는다.

 

한바탕 소란이 지나고 다들 돌아갔다. 중간 중간 서비스 상태를 물어보는 사람만 있을 뿐 집중할 수 있는 환경이 되었다. 서버 개발자 J와 해결안을 찾기 시작했다. 응답 시간이 느린 요청부터 확인하기 시작했다. 두 가지 정도가 눈에 들어왔다. 첫 번째는 느린 쿼리다. 인덱스 추가로 일부 해결할 수 있을 것 같다. 어떤 기능은 like 검색을 하는데 페이징 처리를 위해 count 쿼리도 날린다. 이건 다른 해결안을 찾아야 할 것 같다. 두 번째는 불필요한 쿼리를 너무 많이 실행하는 기능이다. JPA를 잘못 사용한 것으로 보인다.

코드

성능 개선안을 도출하기 위해 코드를 보다 자세히 보기 시작했다. 에휴..... 한숨이 절로 나온다. 코드가 기능 추가/변경, 성능 개선 등은 내 알 바 아니라고 말하고 있다. JPA를 사용했지만 JPA가 제공하는 매퍼 기능을 제외하면 맵을 사용하는 것과 별 차이가 없을 정도다. 족보를 알 수 없는 자신만의 모듈을 만들어 사용하고 있어 성능 향상을 위한 운신의 폭도 좁다.

왜 이렇게 했을까 사정을 들어보니 오픈전까지 같이 개발한 프리랜서가 구현 기술을 선택하는 등 일종의 표준을 잡았다고 한다. 그런 개발자에게 중요 결정을 맡겼으니 지금 M사의 고생은 어찌보면 자업자득이다. 단지 나도 같이 빨려서 고생을 하고 있으니 그게 싫을 뿐이다.

코드에서 발견한 근본적인 문제는 엔티티 연관을 아주 마음껏 사용했다는 거다. 일대일, 일대다, 다대일 가릴 것 없이 엔티티 간 연관을 거미줄처럼 설정했다. 이로 인해 목록 조회시 정말 많은 쿼리를 불필요하게 실행하고 있다.

API도 문제다. 스프링을 사용했지만 전형적인 구조가 아니다. 스프링 MVC를 확장해서 JPA 리포지토리를 바로 API에 연결하도록 구현했는데 조회한 엔티티를 그대로 API 응답으로 보낸다. 이 자체도 문제지만 거미줄처럼 연관된 모든 데이터를 함께 전송하고 있는 것도 문제다. 수정은 조회한 엔티티 데이터에서 일부만 변경해서 그대로 다시 보내는 방식을 사용했다. 연관된 데이터도 모두 포함해서 말이다.

엔티티 처리 로직도 흩어져 있다. 생성 전, 생성 후에 불리는 메서드가 있고 이 메서드에 생성 관련 로직이 흩어져 있다. 조회도 비슷하다.

최근에 본 코드 중에 가장 나쁘다.

그래도

 

코드가 이 모양이어도 문제를 해결해야 한다. 서비스는 해야 하니까. 해결을 시도해 보자.

 

[2부]에서 계속...

 

  1. 호돌맨 2020.04.23 09:29 신고

    ㅎㅎ재미있게 읽었습니다. 빨리 2부 올려주세요 현기증납니다~~~~

  2. ㅎㅎㅎ 2020.04.23 14:22

    너무 재미있고 흥미진진합니다 ㅋㅋ 다음 내용이 너무 궁금하네요

팀장 역할을 할 때 가장 힘든 시기 중 하나가 요즘과 같은 평가 기간이다. 흔히 성과와 역량을 평가하는데 현재 조직에서는 평가해야 할 역량 항목이 적지 않다. 

 

누구나 자신의 역량을 높게 평가하고 싶겠지만 모든 항목을 높게 받을 수는 없다. 전 역량 항목이 높다는 것은 마치 국영수 평균이 90점 이상인데 100미터를 12초에 뛰고 노래방 가수 수준에 그림도 잘 그리며 음식 또한 맛있게 하고 패션 감각이 뛰어난 데 거기에 성격 좋고 이타적인 것도 모잘라 매력적인 외모를 가진 것과 같다. 

 

역량 평가를 가능한 객관적으로 하기 위해 기대하는 수준을 기준으로 평가한다. 기대하는 수준은 직급/연차/연봉 등을 고려한 기대하는 수준을 의미한다. 즉 기대하는 수준은 일반적인 평가에서 중간 값인 B에 해당한다.

 

동일 직급/연차 대비 상대적으로 잘하는(또는 그렇게 느껴지는) 항목이 있다면 중간 값보다 한 칸 위인 A로 평가한다. 다시 말하지만 동일 직급/연차 대비 잘해야 A다. 과장한테 기대하는 바가 있는데 과장이 사원보다 잘한다 해서 해당 역량을 A로 평가할 수는 없다.

 

S는 정말 뛰어나야 줄 수 있다. 정말 뛰어나다는 건 회사에서 그 역량 하나 만큼은 최고라는 뜻이다. 그냥 좀 하네 정도가 아니다.


어떤 항목은 A로 평가하기도 C로 평가하기도 애매하다. '책임감' 같은 항목이 그렇다. 매사에 일을 대충하고 기대하는 만큼 하지 않으면 '책임감'을 C로 평가하겠지만 단순히 일을 열심히 했다고 '책임감'을 A로 평가할 수는 없다. '열정', '소통', '윤리'와 같은 항목도 비슷하다. 특별히 못하면 티가 나지만 이 역시 남보다 내가 특별히 더 잘한다고 말하기 힘든 항목이다. 내가 옆 동료보다 더 윤리적이라고 말할 수 있으려면 얼마나 윤리적이어야 하나?

 

이렇다 보니 결국 역량 평가 결과는 '기대하는 수준'에서 크게 벗어나지 않는다. A를 절반 이상 받고 나머지는 부족한 점이 없어야 A와 B 사이인 B+를 받을 수 있는데 절반 이상의 역량을 '기대하는 수준' 이상 받기란 쉬운 게 아니다. 주변 동료보다 몇몇 항목은 뛰어날 수 있지만 절반이 넘는 역량 항목에서 뛰어나기란 쉽지 않다.

 

근데 이게 문제다. 직군에 따라 항목별로 가중치라도 있어야 역량 평가에 차이가 날텐데 다수가 비슷한 점수를 받는다. 이럴거면 뭐하러 역량 평가를 하나. 평가를 하는 사람도 평가를 받는 사람도 만족할 수 없는 방식이다. 1년 농사를 망치는 기분마저 든다.

예전에 '아이들이 열중하는 수업에는 법칙이 있다'는 책을 읽다가 너무 와 닿는 글귀가 있어 사진으로 남겨둔 적이 있다. 아래가 그 사진이다.

출처: 아이들이 열중하는 수업에는 법칙이 있다

책에서도 언급하고 있지만 이 글은 당연히 교사뿐만 아니라 개발자에게도 적용된다. 종종 경력이 쌓이고 프로젝트를 경험하면 실력이 는다고 말하는 이가 있지만 이는 어디까지나 신입일 때 얘기다. 이런 식으로는 실력 향상에 한계가 있다. 조금만 시간이 지나도 더 이상 실력이 늘지 않고 정체된다.

이 업계에서 정체는 곧 후퇴다. 기량 향상을 위해 노력하지 않는 것은 본인의 선택이지만 정체는 곧 후퇴라는 것은 잊지 말자.

졸업 전만 해도 굉장한 개발자가 되고 싶었다. 뛰어난 설계 능력과 코딩 속도를 자랑하는 그런 실력자 말이다. 이런 막연한 목표는 오래가지 않아 사라졌다. 3-4년 정도 경력을 쌓는 동안 '적당히 잘하는 개발자'로 원하는 수준이 바뀌었다. 언제인지도 모르게 '굉장한' 개발자가 되기 어렵다는 걸 깨닫고 나름 노력하면 될 수 있는 '적당히 잘하는'으로 목표를 낮춘 것이다. 회사 생활을 하면서 뭔가 대단한 걸 만들 재주가 없다는 것을 알게 되었고 남이 만든 거라도 잘 쓰면 다행이란 생각을 하시 시작했다.

사회 초년기에 또 하나 깨달은 건 '기술'만으로는 일이 되지 않으며 기술은 일이 되게 하는 여러 요소 중 하나라는 사실이었다. 기술력이 없으면 안 되는 경우도 있겠지만 꽤 많은 프로젝트가 기술 난이도가 아닌 다른 이유로 실패하는 것을 경험했다. 기술에 대한 욕심이 줄고 다가올 일을 수행하는데 필요한 역량에 초점을 맞추기 시작한 것도 이 시기이다.

다다르고 싶은 수준이 내려가고 기술 외에 다른 것도 있다는 걸 알게 되면서 접하는 책의 주제도 다양해졌다. '피플 웨어', '테크니컬 리더(BTL)', '프로젝트 생존 전략', '스크럼'과 같이 구현 기술은 아니지만 개발과 연관된 책을 읽기 시작했다. '썩은사과'나 '인간력'과 같은 사람에 대한 책도 읽기 시작했다. 이런 책은 개발에 대한 시야를 넓히는데 도움이 되었다.

적당히 잘하기 위해 생산성을 높여야 했고 이를 위해 테스트 코드처럼 효율을 높이는 수단을 찾아 학습했다. 남들이 좋다고 하는 지식도 일부 학습했다. 당장 이해할 수 없는 주제가 많았지만 여러 번 책을 읽고 실제로 적용해 보면서 체득하려고 노력했다. 이런 지식은 개발하는 사고의 틀을 제공해 주었고 생산성을 높여주는 밑거름이 되었다.

많은 뛰어난 개발자가 좋다고 알려준 것도 다 못하고 있고, 배틀을 해서 이길 만큼 개발 지식이 넓지도 깊지도 않으며, 개발 리더로서의 자질도 부족해 팀장 역할이 힘겨울 때가 많다. 애초에 높은 경지가 목표가 아니었기에 당연한 모습이다. 그래도 위안을 삼자면 적당히 잘하는 수준은 되었다는 것이다. 최고의 결과를 만들어내는 고수는 아니나 그래도 중간 이상의 결과는 만들 수 있는 개발자는 되었다.

꽤 긴 경력에 이 정도 밖에 도달하지 못했지만 그래도 이게 어딘가! 20대 초반에 상상한 그런 초고수는 아니지만 지금의 모습에 아쉬움은 없다. 부족한 게 많지만 조금 더 갈고닦아 지금보다 조금이라도 나아질 수 있다면 그걸로 족하다.

  1. 빡빡이발레리나 2019.06.25 11:32

    저도 개발을 오래 하면서 생각이 많이 바뀌었습니다.
    저 역시 모든것을 다 알아야 하고 막힘없이 해결하고 다른 사람들에게 기술적으로 인정을 벋는 그런 사람이 되려 했지만 그런점들이 프로젝트를 하면서 보여지든 보여지지 않든 프로젝트의 결과를 향해 가는 항해에 방해가 되는 점이라는 생각이 어느날 들더군요.

    글을 잘 안남기지만 생각이 비슷하고 글을 읽고 머리가 환기되기에 적어 봅니다.

    좋은글 감사합니다.

  2. 공감합니다 2020.08.11 14:46

    저도 3년차를 넘어가면서 생각이 바뀌었습니다.
    정말 열심히 모든 것을 다 배워주마 하고 공부했던 시절이 있었지만,
    인생에 시간도 충분치 않고 매우 훌륭하다고 생각하지도 않기 때문에...
    조금씩만 나아지면 좋겠다고 생각합니다.

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

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

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


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


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


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


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


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


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



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


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


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


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


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


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


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

  1. 2016.01.22 10:57

    비밀댓글입니다

    • 최범균 madvirus 2016.01.22 14:56 신고

      대략 모바일 플랫폼, 쇼핑, 포탈, 게임, 모바일 주문 등의 회사를 떠올리시면 될 것 같아요.

  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분 정도가 참여를 해 주셨다.


회사에서 입사해서 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 에서 확인할 수 있다.


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


기본적인 몇 가지:

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


앞 이야기:

작은 요구 사항 변경이 발생했다. 최초의 계획은 '전날 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에게 숙제로 남겨둘 예정이다.


이전글:


드디어 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의 콘크리트 클래스를 구현이 완료된 시점의 설계는 아래와 같다.



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


지난 이야기:

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: 네!
[다음회에 계속.....]


+ Recent posts