TDD를 하신다구요?

사람들과 전화너머로 이야기를 하다보면 TDD를 자신있게 말하는 분들을 종종 만난다.  물론 이 분들의 이력서에도 “활용 가능한 기술”들 가운데 하나로 TDD라는 3글짜 알파벳이 강렬하게 적혀있다. 개인적으로 TDD 방식의 개발의 예찬론자이기도 하기 때문에 이런 분들을 만날 때마다 반가운 생각이 든다.

처음 이 단어를 들었던 때가 아마도 2010년도 쯤이었을 것 같다. 주변의 개발자들 가운데 아는 사람도 적고 해서 손에 익히기에 쉽지 않았다. 지금은 거의 대부분의 이력서에 언급될만큼 보편적인 것이 되버렸다고 생각했다.

하지만 이런 분들을 만나서 “TDD 방식으로 코드를 작성해봐주세요~” 하면 다른 양상이 펼쳐진다. 테스트가 개발을 주도하는 모습을 기대했지만 대부분 테스트는 장식이다.  이 모습을 보면서 아래와 같은 생각을 해본다.

왜 이런 의미없는 테스트를 작성할까?

테스트를 작성하는게 테스트 주도 개발인가?

뭔가 착각이 있는 것 같다. 원래 테스트와 테스트 주도 개발은 다른 건데 말이다.

시작이 문제다.

2차원상의 좌표 점들의 거리를 계산하는 프로그램을 작성해야 한다고 치자. 뭐부터 작성해야할까?

public class DistanceCalculator {
  public void addPoints(int x, int y) {
  }
  public float calc() {
    return -1;
  }
};

테스트를 먼저 한다고 하지만 그래도 이정도는 먼저 찍어놓고 해야겠지?  본능적인 촉에 의하면 2차원 좌표점이라고 이야기를 했으니까 그건 x, y로 표시해야하고, 점들이 많을테니까 이걸 관리해야하는 기능도 필요할테니 점들을 넣을 수 있는 addPoint(x, y)와 같은 메소드도 필요하다.  그리고 이것들을 가지고 계산을 해야하니까 당연히 calc() 라는 메소드도 있어야 한다.  TDD로 개발하는 개발자니까 구현은 테스트를 작성한 다음에 하는거지!!!

이렇게 하는게 과연 Test Driven일까?  하지만 이 코드는 이미 Developed 되어있다.  해야할 일은 채워넣는 일일뿐.  여기에서 테스트가 하는 일은 이미 구조가 잡힌 코드의 안정성을 보장하는데 사용된다.  물론 예외등을 테스트하다보면 코드의 발전을 이끌 수는 있겠지만, 완전한 Driven이라고 이야기는 어렵다.

테스트 먼저

TDD를 좋아하는 개발자라면 시작은 DistanceCalculator 클래스가 아니라 DistanceCalculatorTest 클래스에 대한 코드부터 적어야 한다.

public class DistanceCalculatorTest {
  @Test
  public void shouldItCalculateDistance {
  final Point start;
  final Point end;
  final float expectedDistance = 1.0f;

  GIVEN: {
    start = new Point(0, 0);
    end = new Point(0, 1);
  }

  WHEN: {
    calculator = new Calculator();
    actualDistance = calculator.distance(new Point[] { start, end });
  }

  THEN: {
    assertThat(actualDistance, is(expectedDistance));
  }
};

이 테스트 코드를 통해 우리가 많은 것들을 정리할 수 있다.

  • 계산을 수행할 객체의 이름을 정했다. DistanceCalculator 보다는 Calculator가 현재의 컨텍스트에서 좀 더 좋겠다는 생각이 들었다.  (물론 테스트의 이름도 변경하는게 맞지만 예제를 위해…)
  • 계산을 수행하기 위한 메소드는 2차원 좌표를 기술할 수 있는 Point 객체의 배열을 받는다. addPoint등을 생각할 수 있지만, 그렇게 하면 테스트 코드를 작성하는게 번거로워진다. 테스트 자체를 통해 메소드의 사용성을 평가하고 쉽게 사용할 수 있는 방향으로 메소드를 설계한다.

여기까지를 정리했다면 이제 메인 코드를 작성할 때이다.  이클립스나 IntelliJ에서 제공해주는 Code Complete 기능을 사용하면 순식간에 찍어낸다.  위 두가지 과정에서 볼 수 있는 건 우리가 작성할 코드의 방향과 형태를 테스트를 작성해봄으로써 끌어냈다라는 것이다.  이것이 Test Driven의 진정한 모습이다.

이제 실패하는 테스트를 통해 로직을 완성하면 된다.

나누고 끄집어내라

자 우리의 말썽많은 고객분께서 요구 사항을 바꿨다.  2D 세상에 만족을 못하고 3D 세상에서도 거리를 계산해달라고 한다.  테스트를 다시 한번 살펴보자.

  WHEN: {
    ...
    actualDistance = calculator.distance(new Point[] { start, end });
    ...
  }

distance 메소드가 수행하는 역할(Responsibility)를 정리해보자.

  • 인접한 두 Point 간의 거리를 계산하고,
  • 계산된 결과값의 합을 구하는 역할을 한다.

2D, 3D일지의 문제는 첫번째 “인접한 두 포인트간의 거리”에만 영향을 미친다. 나머지 기능은 원래 distance 메소드가 수행하는대로 하면 된다. 두 점 사이의 거리 계산 부분을 수행하도록 Point 객체에게 위임해버리면 된다.

한방에 끝낼려고 하지 말자

인접 점들 사이에 거리를 어떤 방식으로 distance 메소드내에서 수행되어야 할지를 결정해야 한다. 이걸 어떻게 할지를 테스트를 통해 적절한 구조와 로직을 찾아보자.

public void Space2DPointTest {
  @Test
  public void should2DPointCalculateDistnaceWithOhter() {
    final Point p1;
    final Point p2;

    GIVEN: {
      p1 = new Space2DPoint(0, 0);
      p2 = new Space2DPoint(0, 1);
    }

    final float actual;
    THEN: {
      actual = p1.distance(p2);
    }

    final float expected = 1.0f;
    THEN: {
      assertThat(expected, is(actual));
  }
}

테스트 코드를 통해 포인트에 대한 구조를 아래와 같이 잡으면 된다는걸 알 수 있다.

  • Point라는 인터페이스를 둔다.
  • 인터페이스는 Point 객체를 파라미터로 받는 distance라는 메소드를 정의한다.
  • Space2DPoint 클래스는 Point 인터페이스를 구현한다.

비슷한 맥락에서 Space3DPoint에 대해서도 테스트 클래스를 작성해보자. 두 테스트에서 동일한 구조로 동작이 가능함을 확인한다.

확인이 완료됐다면 다시 원래의 CalculatorTest 클래스로 돌아가 이를 수정한다. 현재까지 작성된 테스트들의 관점에서 우리는 GIVEN 영역을 아래와 같이 수정하면 된다.

  GIVEN: {
    start = new Space2DPoint(0, 0);
    end = new Space2DPoint(0, 1);
  }

그리고 원래의 Calculator.distance() 메소드를 아래와 같이 변경한다.

public float distance(Point[] points) {
  ...
  for (int i=1; i<points.length; i++) {
    distanceSum += points[i-1].distance(points[i]);
  }
  ...
 return distanceSum;
}

흠… 이 과정이 리팩토링이다. 알흠답다. ^^;

끝날때까지 끝난게 아니다.

여러분이 만드는 서비스/제품이 계속 사용자들을 만난다면 여러분의 개발은 끝난게 아니다.

변화는 필수적이며 변화에 대응하기 위한 테스트와 이에 상응하는 리팩토링 역시 지속되어야 한다.  이 과정을 반복함으로써 여러분의 코드의 날이 날카롭게 빛날 것이다.

 

ps. 조금 더 긴 프로그래머틱한 글을 이전 블로그에 올려둔 게 있다.  내용이 썩 맘에 들진 않지만 그래도 노력이 있으니까 한번 봐주길 바란다. 언젠가 기회가 된다면 이 글도 제대로 손을 봐야할 듯 싶긴 하다.

AWS를 활용한 블로그 시스템 만들기

그동안 네이버에서 제공하던 블로그를 써서 글을 써왔다.

많지 않은 글이긴 하지만 그래도 근 10년 가까이 글을 쓸 수 있도록 해준 고마운 친구였다. 하지만 변화하는 세월앞에서 그 친구도 새로운 옷을 점점 갈아입기 시작하더니 결국에는 내가 싫어하는 스타일로 가버렸다.

개발자의 이야기를 많이 하고 싶은 심정이긴 하지만 받아주는 친구가 맛집에 너무 친화적으로 바뀌다보니까 개발자스럽게 글을 쓸 수 있는 거랑은 거리가 멀어져버린 것 같다.   절이 싫으면 중이 떠나서 본인 입맛에 맞는 걸 찾는 수밖에 없겠지…  온라인 툴로 많이 사용하는 브런치도 꽤 좋은 선택지일 것 같지만 코드를 담아내기에는 최적은 아닌 것 같았다.

결론은 워드프레스(WordPress)를 이용해서 나만의 시스템을 만들자!  최근에 아마존 AWS 기반으로 일을 하고 있기 때문에 처음부터 끝까지를 해본다면 얼마나 할 수 있을지를 시험해보고 싶기도 했다. 하면 된다고 말을 했지만 실제로 처음부터 끝까지 내 손으로 해본 적은 또 없기 때문에 이번이 좋은 기회라는 생각도 들었다.

간단히 무료 블로그 시스템을 구성하는데 사용한 것들은 아래와 같다.

  • EC2 – 1 CPU, 1G Mem, 30G SSD Disk
  • Elastic IP – Public IP (고정 IP 할당용)
  • RDS – MySQL Instance, 20G 용량
  • Route53 – 도메인 등록 및 Sub domain 관리

 

Setting AWS Environment

EC2

EC2DiskUsage사용할 워드프레스는 PHP 기반이므로 아파치에서 동작된다. 이건 이걸 돌리는데 그리 큰 메모리를 필요로 하지 않는다는 것이다. 또한 특성상 CPU를 많이 먹을 이유도 없다.  그렇기 때문에 죽는걸 고려하지 않는다면 아주 간단한 웹서버만으로도 돌리는게 가능하다.

EC2에서 제공하는 Free Tier 장비로 1CPU, 1G를 제공한다. 자바 어플을 돌리기에는 부담스럽긴 하지만 워드프레스류를 돌리기에는 부담이 없다. 와중에 저장 공간 역시 30G를 제공한다. 당장 쓰는데 필요한 모든 소프트웨어를 다 깔아둔 상황임에도 불구하고 5% 수준을 사용하고 있다. 95%를 언제 채울 수 있을까 싶긴하다.

EC2를 통해 생성된 클라우드 장비는 변동 IP를 가진다. 고로 장비를 Restart되거나 혹은 리부팅되면 IP가 바뀔 가능성이 아주 농후하다.  물론 Private IP는 변경되지 않지만 Public IP는 소중한 존재이기 때문에 고정된 형태로 한 장비에 이를 허해주지는 않는다.

외부에서 그 이 장비를 어떻게 볼 수 있는거지?  그 방안을 Elastic IP를 통해 제공한다.

 

Elastic IP

AWS 환경에서 고정 Public IP를 사용하기 위해서는 Elastic IP를 사용해야 한다.  가난히 설명하면 고정 Public IP를 갖는 Domain Name을 생성하고, 그 IP에 다시 특정 EC2 Instance를 Binding 시키는 방법이다.

물론 익히 아는 바와 같이 이건 유료다. Public IP를 할당받았으면 아마존은 담달부터 당신에게 청구서를 보낼 것이다.

RDS

예전에 이런 서비스가 있다는 이야기를 듣긴 했지만 써보니 역시 좋았다. 각종 RDBMS를 내가 별도로 설치하는게 아니라 몇 개 설정만 꾹꾹 누르면 설정하기 힘든 데이터베이스 서버가 쭉쭉 생겨난다.

가장 흔하게 사용할 수 있는 MySQL 서버를 지정했다. 인스턴스 타입을 보면 Production용과 Dev/Test용 장비로 분명하게 고를 수 있다. Dev/Test 용도의 장비는 무료이기 때문에 당연히 이걸로 갔다. 와중에 용량도 최대 20G(!!)나 준다. 내가 이걸 다 채울려면 아마도 글을 정말 열심히 써야할 것 같다는 생각 뿐이다.

생성된 데이터베이스 인스턴스는 이름을 통해 접근 가능한 Entry Point를 제공한다. 물론 IP를 통해 접근도 가능하겠지만, 클라우드 환경에서 IP는 믿을 수 없는 존재이다. 어떻게든 이름을 가지고 접근할 수 있어야 한다.  설정에서 주의할 점은 MySQL 데이베이스에 대한 접근을 모든 IP 대역에서 접근하도록 허용하면 좀 곤란한다. 가급적 외부망에서의 접근은 차단하고 EC2에서 생성된 Instance만을 통해 접근이 되도록 제어를 해주는게 좋다.

Router53

도메인을 신청한다.  여기에서는 chidoo.me  물론 이것도 다 돈이다. 하지만 인터넷상에 자신의 집을 하나 가진다고 생각하면 머… 술한잔 값도 아니니 안심하구 지르시길 바란다.

도메인이 신청되면 Hosted Zone이라고 해당 도메인의 Sub domain을 관리할 수 있는 기능이 생긴다. CNAME 방식으로 Elastic IP를 통해 생성된 내부 도메인 이름을 www.chidoo.me가 되도록 설정한다.

Setting WordPress

워드프레스 설정 자체는 인터넷상에서 충분한 정보가 있기 때문에 굳이 따로 언급하지는 않는다.

다만 주의할 점은 한가지 있다. 워드프레스의 각종 기능들을 업데이트하기 위해 ftp 기능을 사용한다.  하지만 ftp는 매우 보안상 취약한 놈이기 때문에 쓰지 말아야 한다. 이 방식보다는 다음의 방법을 통해 각종 테마나 플러그인을 업데이트할 수 있다.

다음의 두 사이트에서 원하는 걸 찾는다.

  • 플러그인: https://wordpress.org/plugins/
  •  테마: https://wordpress.org/themes/browse/new/

다운로드를 원하는 것을 찾았다면 다음의 방법을 해당 모듈의 다운로드 링크를 찾는다.

GetDownloadAddress

복사를 했다면 터미널에 접속해서 이걸 플러그인 혹은 테마 디렉토리에 wget 명령을 이용해서 다운받는다.

$ cd $WPINSTALL/wp-content/

해당 디렉토리에 보면 plugins, themes 라는 디렉토리가 존재하는 것을 확인할 수 있다.


$ wget <download-url>

$ unzip <downloaded_file>

$ mv <downloaded_file>

확인 후 워드프레스의 Dashboard를 통해 활성화시키면 적용 완료!  약간의 수고를 더하면 불필요한 위험을 굳이 감내할 필요가 없다.

끝.

git: terminal에서 간지나게 써보자.

Git을 이클립스나 IntelliJ에서만 사용해야한다면 좀 쪽팔릴 것 같다.
GUI 없는 상황에서는 바보가 될 거기도 하고 뭔가 Cool하지 않다.

기억력은 역시나 3초말이라 항상 까먹는다. 흔하게 사용하는 것들 위주로 정리해놓는다.

Git Repository 처음 사용하기

$ git init
현재 디렉토리를 git 저장소로 등록한다.

$ git add –all
현재 디렉토리 및 그 하위에 있는 모든 파일들을 git의 변경 대상으로 등록한다. 하기전에 아래 .gitignore 파일에 쓸데없는 파일들을 먼저 정리하자. 안그러면 번거로운 일을 두번할 수 있다.

만약 이전에 .gitignore 파일이 없다면 아래 파일을 먼저 정의한 다음에 –all 옵션으로 실행한다. 이미 실행했다고? 그럼 rm -rf .git 명령으로 git repository 설정을 날려버리면 된다. 쫄지 말자!

.gitignore
이 파일에 만약 git을 통해 관리하기 싫은 파일이 있다면 기록해둔다. .gitignore 파일이 존재하는 디렉토리를 기준으로 상대 경로를 지정하면 해당 파일은 관리 대상에서 빠진다. 생각외로 이거 사용해보면 참 좋다. 이클립스나 IntelliJ를 포함해서 서버에 올릴 파일들을 빼고 설정을 잡는다면 대강 아래와 같다.

*.class

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.ear

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

# eclipse ignorings
.classpath
.project
.settings/

# intellij ignorings
*.iml
.idea/

 

변경 내용 커밋하기

$ git commit
git add 명령을 통해 등록된 파일들을 커밋한다. git add / rm 되지 않은 파일들은 커밋 대상에 포함되지 않는다. 단순히 변경했다고 하더라도. 이런 상태를 unstaging이라고 부르는데 add/rm을 통해 staged 상태로 만들어줘야 한다.

$ git commit -a
git add 명령을 통해 이전에 등록해둔 파일들의 변경(modified, deleted)된 내역들을 자동으로 커밋한다.

$ git commit file1 file2 file3
지정된 특정 파일들만 커밋한다.

$ git commit -m ‘commit message’
커밋 로그를 작성하지 않고 -m 명령에 붙은 메시지를 커밋 로그로 반영한다. 이렇게 하면 뭔가 쿨해보인다. *^^*

여기까지 했으면 로컬에 설치된 git을 통해 작업할 건 다 했다. 이제 원격에 있는 git repository에 작업된 코드를 반영할 시간이다.

$ git stash / git stash apply

작업을 하다보면 현재 작업 부분을 마무리하지 못하고 git repository에 반영을 해야만하는 경우가 있다.  작업한 부분이 좀 된다면 Rollback한 다음에 하기에는 좀 아쉬움이 크다.  그렇다고 진행중인 내용을 commit 해버릴 수도 없는 노릇이고…  이런 경우에 활용할 수 있는 명령이 stash 기능이다.  stash 기능을 사용하면 현재 작업중인 결과물을 임시로 별도의 공간에 저장하고, 소스 repository의 상태를 최종 commit한 버전의 상태로 돌려준다.

최종 커밋한 결과물까지를 서버에 push하거나 그 상태에서 추가적인 작업을 마무리하고 commit & push를 하면 된다.  그리고 stash apply 기능을 통해 이전에 작업중인 코드를 현재 branch로 불러들여와 작업을 이어가면 된다.

 

원격 서버와 친해지기

$ git remote add origin git-repo-address
원격 git repository를 현재 디렉토리에 바인딩한다. git repository를 지정하는 방식은

  • https://protocol
  • git:// or
  • user@server:path/to/repo.git

방식 가운데 하나를 사용해서 지정한다.

만약 origin 이라는 이름이 너무 상투적이라고 생각된다면 다른 이름을 사용해도 된다. 하지만 헷갈리지 말자. 지정해놓고 헷갈리면 .git 디렉토리에서 뒤지면 나온다.

$ git clone git-repo-address
만약 이미 원격에 있는 소스를 내가 받아와야 한다면, clone 명령을 사용한다. address 뒤에 별도의 디렉토리 이름을 입력하지 않으면 repository 이름이 그대로 디렉토리 이름으로 사용된다.

$ git pull origin master
원격 서버의 master branch의 변경 내역들을 땡겨온다.

$ git pull origin master
서버에 올린다. 만약 master라는 원격 branch의 이름을 까먹으면 로컬에 있는 모든 브랜치가 서버에 다 올라간다. 작업하다가 임시로 만들어놓은 것까지… 귀찮으니까 가능하면 branch의 이름을 적어주는 습관을 들이는게 좋고, 그게 아니라면 로컬에는 작업이 끝난 branch는 삭제해주는게 좋다.
평소에 보이지 않는 깔끔한 코드 작성할 때는 보여주자.

 

머지하기

다른 git repository와 머지하는 방법은 다음과 같은 방식을 사용한다.

1. 현재 작업 내용을 일단 commit 한다.

2. 현재 branch를 기준으로 머지할 브랜치를 따로 checkout 한다.
$ git checkout -b update_branch current_branch
만약 작업할 branch가 이미 있다면…
$ git checkout update_branch

3. 해당 branch로 다른 repository의 데이터를 땡겨온다.

$ git pull git-repo-address branch_name

이 과정에서 만약 쫑이나면 쫑이난 문제를 해결해야한다.
보통 쫑나면 해당 파일에 > < 와 같은 표식으로 표시가 되고, 그걸 vi 혹은 GUI 편집기를 가지고 편집하면 된다. 분량이 작을 경우에는 vi를 가지고 해도 되겠지만, 분량이 좀 된다면 그냥 이클립스나 IntelliJ를 사용하도록 하자.

4. 이제 현재 branch로 이동
$ git checkout current_branch

5. 머지하기
$ git merge update_branch

이미 앞단계에서 원격 서버와 다 합쳤기 때문에 여기서는 별달리 할게 없이 바로 되야한다. 안되면 뭔가 이상한거야!!

불필요해진 파일 삭제하기

$ git rm filename
그냥 용감하게 rm 명령으로 지운다고 git에서 제거되지 않는다. 꼭 이 명령으로 제거해줘야지 git entry에서도 빠지고, 이후에 작업하는데 번거롭지 않게된다.

채용의 방향

현재 회사의 채용 방향에 대해 가장 공감가는 글로 공유된 글이다.

혹시 외국계 소프트웨어 회사에서 프로그래머를 지망한다면 눈여겨서 볼만한 글이다.

How to pass a programming interview

굳이 요약이 필요없는 글이라 한번 일견해보는것도 좋을 것 같다.

처음 글입니다.

아직은 셋업을 하고 있습니다.

제대로 언제쯤 만들어질 수 있을지는 모르겠지만,  개발에 관련된 이야기들을 이어가고 싶습니다.

 

class First {
 public void main(String[] args) {
     system.out.println("hahahaha, I'm alive!");
 }
};

조만간에 좀 더 나아진 모습으로 여러분들과 만날 수 있지 않을까 생각중입니다.