본문 바로가기

(54)

테스트 주도 개발(켄트백) - 3부. 테스트 주도 개발의 패턴(1)

25장. 테스트 주도 개발 패턴 우선 기본적인 전략에 관한 질문에 답해야 한다. 테스트한다는 것을 무엇을 뜻하는가? 테스트를 언제 해야 하는가? 테스트할 로직을 어떻게 고를 것인가? 테스트할 데이터를 어떻게 고를 것인가? (위에 처럼 글로 정리를 안해서 그렇지 위와 같은 문제점에 혼자서 답을 얻기 힘들었기 때문에 이 책을 읽고있는게 아닌가 하다) | 테스트(명사) | 자동화된 테스트를 만들어라! 테스트하다(test)는 '평가하다'라는 뜻의 동사다. 그 어떤 소프트웨어 엔지니어도, 아무리 작은 변화라도 테스트하지 않고 릴리즈하지는 않는다. 변화를 테스트할 수 있다고 해도, 실제로 변화를 테스트하는 것은 '테스트를 갖고 있다'는 것과는 같지 않다. | 격리된 테스트 | 테스트를 실행하는 것이 어떤 식으로 영..

마이크로서비스 패턴 - 길벗 출판사

최근 면접본 회사는 MSA(Micro Service Architecture)식 개발을 한다고 하였고, 더 최근에 기술과제를 수행하고 그에 대한 피드백을 받았는데, 디자인 패턴에 대한 공부를 병행하는 것을 추천받았다. 때마침 길벗 출판사에서 "마이크로서비스 패턴"이라는 책을 출간했다. 또한 서평단을 모집한다는 소식에 망설이지않고 바로 지원했다. 결과는 .... (두둥) 집에 책이 도착했다. 신나는 마음으로 열어보았다. 처음에는 책 내용을 정리해서 같이 적을까 하다가. 적어둔 내용이 (절반정도 적었었는데... ㅠ) 싹 날아가버려서 ... 도저히 다시 그정도의 요약을 하기는 힘들 것 같다. 아뿔사... 대상 독자를 보니 CTO, 기술 부사장, 아키텍트, 개발자 등을 얘기하고있다. '개발자'라고 포괄적인 범위 ..

Chap 4, 5, 6 - 스트림 소개, 활용, 데이터 수집

스터디를 통해서 '모던 자바'에 대한 공부를 했었다. 하지만 배울 때는 분명 다 내것이 되었다고 생각했었는데, 막상 기억에 남는게 별로 없는 것 같다. 역시 복습은 선택이아닌 필수가 아닌가 한다. (= 인간은 망각의 동물이다) 당장 내일 응시해야하는 코딩테스트가 하나 있어서, 급하게 스트림의 활용에 대한 내용을 간략하게 정리해보자 한다. 이론적인 부분은 시간을 잡아서 제대로 정리하려고 한다(스트림 뿐만 아니라 모던 자바 인 액션 책 전체적으로 정리를 하려고 한다). 스트림(Stream)은 자바 8에서 추가된 기능이고, 스트림을 이용하면 선언형(SQL의 쿼리와 같이 질의로 표현할 수 있다)으로 컬렉션 데이터를 처리할 수 있다. 또한 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬..

고객이 보이는 구글 애널리틱스 - 한빛미디어

현재 한빛 미디어의 서평단으로 활동 중이다. 3월의 랜덤 서적으로 '고객이 보이는 구글 애널리틱스' 책을 받았다. 사실 선택한 3개 중에서 가장 안왔으면 했던 책이다. (어떻게... 귀신같이) 그래도 최근에 면접을 봤던 회사(결과가 궁금한 사람이 있을까봐 TMI하게 말하자면 떨어졌다...ㅠ)에서 '구글 애널리틱스'를 이용해서 더 많은 고객을 유치하기위한 노력을 하고 있다는 얘기를 들었었다. 이번 기회에 한번 읽으면서 구글 애널리틱스의 이점과 사용법에 대해서 알아보자는 긍정적인 생각으로 읽었고 서평을 작성한다. 먼저 각 챕터의 구성을 나열해본다. 먼저 실습을 하기 직전 기초적인 웹로그의 개념과 구글 애널리틱스를 사용함으로써의 장점, 그리고 각 포지션(기획자, 마케터 ..etc)이 웹로그를 분석해야하는 이유..

아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다. (클래스의 인스턴스를 반환하는 단순한 정적 메서드를 말하는게 맞다!) 다음 코드는 boolean 기본 타입의 박싱 클래스인 Boolean에서 발췌한 간단한 예다. public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; } 클래스는 클라이언트에 public 생성자 대신 정적 팩터리 메서드를 제공할 수 있다고 했다. (같이 제공할수도 있다) 이 방식에는 장점, 단점이 모두 존재한다. 먼저 장점 다섯 가지에 대해 알아본다. 장점 1. 이름을 가질 수 있다. 생성자에 넘기..

Do it! 웹 사이트 따라 만들기

페이스북 '생활코딩' 채널 커뮤니티 페이지에서 Do it! 웹 사이트 따라 만들기 서평단을 모집한다는 글을 보고(안 그래도 최근 프로젝트를 진행하며 프론트 엔드부분에서 막히는 부분이 많았었다, 물론 벡엔드도..) 바로 지원했다. 운이 좋게도 서평단에 발탁이 되었고, 얼마지나지 않아 책을 받아볼 수 있었다. 이전에 배운적이 있었지만, 머릿속에 그 지식이 일단 책의 시작 부분에 HTML, CSS, JS에 대한 기본 지식에 대해서 핵심만 간략하게 설명이 있다. 기본지식이 탄탄한 경우에는 과감히 건너뛰어도 될 것 같았다. (헷갈리거나 잊고 있었던 개념 확립에 많은 도움을 받았다) 최근 HTML의 정보로서의 가치가 계속해서 증가하는 추세라고 한다. (자신의 사이트가 더 많이 노출되기위해서 어떤 요소를 신경써야하는..

개발서적

테스트 주도 개발(켄트백) - 3부. 테스트 주도 개발의 패턴(1)

728x90

 

25장. 테스트 주도 개발 패턴

우선 기본적인 전략에 관한 질문에 답해야 한다.

  • 테스트한다는 것을 무엇을 뜻하는가?
  • 테스트를 언제 해야 하는가?
  • 테스트할 로직을 어떻게 고를 것인가?
  • 테스트할 데이터를 어떻게 고를 것인가?

(위에 처럼 글로 정리를 안해서 그렇지 위와 같은 문제점에 혼자서 답을 얻기 힘들었기 때문에 이 책을 읽고있는게 아닌가 하다) 

| 테스트(명사) |

자동화된 테스트를 만들어라! 테스트하다(test)는 '평가하다'라는 뜻의 동사다. 그 어떤 소프트웨어 엔지니어도, 아무리 작은 변화라도 테스트하지 않고 릴리즈하지는 않는다. 변화를 테스트할 수 있다고 해도, 실제로 변화를 테스트하는 것은 '테스트를 갖고 있다'는 것과는 같지 않다. 

| 격리된 테스트 |

테스트를 실행하는 것이 어떤 식으로 영향을 미쳐야 좋은가? 아무 영향이 없어야 한다. 각각의 테스트는 다른 테스트와 완전히 독립적이어야 한다. 즉 문제가 하나면 테스트도 하나만 실패해야 하고, 문제가 둘이면 테스트도 두 개만 실패해야 한다. 

격리된 테스트가 암묵적으로 내포하는 특징 중 하나는 테스트가 실행순서에 독립적이게 된다는 점이다. (테스트의 일부만 실행해보고 싶으면, 선행 테스트가 실행되지 않아서 내가 고른 테스트들이 실패하지 않을까 걱정할 필요 없이 그렇게 할 수 있어야 한다) 

성능 문제는 테스트가 데이터를 공유해야 하는 이유로 자주 언급된다. 격리된 테스트가 내포하는게 또 하나 있는데, 이는 주어진 문제를 작은 단위로 분리하기 위해 노력해서 각 테스트를 실행하기 위한 환경을 쉽고 빠르게 세팅할 수 있게 해야 한다는 것이다. 테스트를 격리하기 위한 작업은 결과적으로 시스템이 응집도는 높고, 결합도는 낮은 객체의 모음으로 구성되도록 한다

| 테스트 목록 |

뭘 테스트해야 하나? 시작하기 전에 작성해야 할 테스트 목록을 모두 적어둘 것. 우선 구현할 필요가 있는 모든 오퍼레이션의 사용예들을 적는다. 그 다음, 아직 존재하지 않는 오퍼레이션에 대해서는 해당 오퍼레이션의 널 버전(아무 일도 하지 않는 버전)을 리스트에 적는다. 마지막으로 깔끔한 코드를 얻기 위해 이번 작업을 끝내기 전에 반드시 해야할 리팩토링 목록을 적는다. 테스트의 윤곽만 잡는 대신, 한 걸음 더 나아가 테스트를 전부 구현할 수도 있다. (But 그렇게 추천하지는 않음, 실제로 초록 막대를 보는데 걸리는 시간이 상당히 길어지기 때문에 TDD와는 조금 거리가 멀어진다고 생각이 듦)

테스트를 통과하게 만드는 과정에서 우리가 작성한 코드들은 새로운 테스트가 필요함을 암시적으로 알려줄 것이다. 이 새 테스트를 리팩토링과 마찬가지로 할일 목록에 적어 놓아라. 세션이 끝났을 때 목록에 남아 있는 항목들은 따로 신경 쓸 필요가 있다. 어떤 기능을 하나 진행하는 중이라면 다음 번에도 똑같은 목록을 사용하라. 현재 작업 범위를 넘어서는 큰 리팩토링 거리를 발견한다면, '다음' 할일 목록으로 옮겨라. 

| 테스트 우선 |

테스트를 언제 작성하는 것이 좋을까? 테스트 대상이 되는 코드를 작성하기 직전에 작성하는 것이 좋다. 코드를 작성한 후에는 테스트를 만들지 않을 것이다. 프로그래머로서 우리의 목표는 기능이 실행되도록 만드는 것이다. 하지만 또 한편으로는 프로그램의 설계에 대해 생각해볼 시간도 필요하고 작업 범위를 조절할 방법도 필요할 것이다. 

| 단언 우선 | 

테스트를 작성할 때 단언(assert)를 언제쯤 쓸까? 단언을 제일 먼저 쓰고 시작하라

  • 시스템을 개발할 때 무슨 일부터 하는가? 완료된 시스템이 어떨 거라고 알려주는 이야기부터 작성한다.
  • 특정 기능을 개발할 때 무슨 일부터 하는가? 기능이 완료되면 통과할 수 있는 테스트부터 작성한다.
  • 테스트를 개발할 때 무슨 일부터 하는가? 완료될 때 통과해야 할 단언부터 작성한다. 

단언을 먼저 작성하면 작업을 단순하게 만드는 강력한 효과를 볼 수 있다. 구현에대해 전혀 고려하지 않고 테스트만 작성할 때도 사실 우리는 몇 가지 문제들을 한번에 해결하는 것이다. 

  • 테스트하고자 하는 기능이 어디에 속하는 걸까? 기존의 메서드를 수정해야 하나, 기존의 클래스에 새로운 메서드를 추가해야 하나, 아니면 이름이 같은 메서드를 새 장소에? 또는 새 클래스에?
  • 메서드 이름은 뭐라고 해야 하나?
  • 올바른 결과를 어떤 식으로 검사할 것인가?
  • 이 테스트가 제안하는 또 다른 테스트에는 뭐가 있을까? 

이 문제를 한번에 잘 해결하기에는 쉽지가 않다.  '올바른 결과는 무엇인가?', '어떤 식으로 검사할 것인가?;는 나머지 문제에서 쉽게 분리할 수 있다. 

 

예를 들어, 소켓을 통해 다른 시스템과 통신하려 한다고 가정할 때, 통신을 마친 후 소켓은 닫혀 있고, 소켓에서 문자열 'abc'를 읽어와야 한다고 치자. 

testCompleteTransaction() {
    ....
    assertTrue(reader.isClosed());
    assertEquals("abc", reply.contents());
}

reply는 어디에서 얻어오나? 물론 socket이다. 

testCompleteTransaction() {
    ...
    Buffer reply = reader.contents();
    assertTrue(reader.isClosed());
    assertEquals("abc", reply.contents());
}

그럼 socket은 어디에서 나오나? 서버에 접속할 때 생성된다. 

testCompleteTransaction() {
    ...
    Socket reader = Socket("localhost", defaultPort());
    Buffer reply = reader.contents();
    assertTrue(reader.isClosed());
    assertEquals("abc", reply.contents());
}

물론 이 작업을 하기 전에 서버를 먼저 열어야 한다. 

testCompleteTransaction() {
    Server writer = Server(defaultPort(), "abc");
    Socket reader = Socket("localhost", defaultPort());
    Buffer reply = reader.contents();
    assertTrue(reader.isClosed());
    assertEquals("abc", reply.contents());
}

아직 실제 용도에 맞게 이름을 수정하는 일이 남아 있긴 하지만 지금까지 아주 작은 단계로 빠른 피드백을 받으며 테스트의 아웃라인을 만들었다. 

| 테스트 데이터 |

테스트할 때 어떤 데이터를 사용해야 하는가? 테스트를 읽을 때 쉽고 따라가기 좋을 만한 데이터를 사용하라. 데이터 작성에도 청중이 존재한다. 단지 데이터 값을 산발하기 위해 데이터 값을 산발하지 마라. 데이터 간에 차이가 있다면 그 속에 어떤 의미가 있어야 한다. (1과 2사이에 어떠한 개념적 차이도 없다면 1을 사용하라) 

테스트 데이터 패턴이 완전한 확신을 얻지 않아도 되는 라이선스는 아니다. 만약 시스템이 여러 입력을 다루어야 한다면 테스트 역시 여러 입력을 반영해야 한다. 하지만 세 항목만으로 동일한 설계와 구현을 이끌어낼 수 있다면 굳이 항목을 열 개나 나열할 필요는 없다.

테스트 데이터 패턴의 한 가지 트릭은 여러 의미를 담는 동일한 상수를 쓰지 않는 것이다. 만약 plus() 메서드를 구현하려고 한다면 고전적 예제인 2+2 혹은 1+1을 쓰고 싶을 것이다. 만약 구현에서 인자의 순서가 뒤집힌다면 어떻게 될까? (plus()에서야 순서가 뒤집혀도 상관이 없겠지만 다른 메서드는 아닐수도 있다 )우리가 첫 번째 인자로 2를 썼다면 두 번째 인자는 3을 써야 한다. 테스트 데이터에 대한 대안은 실제 세상에서 얻어진 실제 데이터를 사용하는 것이다. 실제 데이터는 다음과 같은 경우에 유용하다. 

  • 실제 실행을 통해 수집한 외부 이벤트의 결과를 이용하여 실시간 시스템을 테스트하고자 하는 경우.
  • 예전 시스템의 출력과 현재 시스템의 출력을 비교하고자 하는 경우 (병렬 테스팅)
  • 시뮬레이션 시스템을 리팩토링한 후 기존과 정확히 동일한 결과가 나오는지 확인하고자 할 경우. 특히 부동소수점 값의 정확성이 문제가 될 수 있다.

| 명백한 데이터 |

데이터의 의도를 어떻게 표현할 것인가? 테스트 자체에 예상되는 값과 실제 값을 포함하고 이 둘 사이의 관계를 드러내기 위해 노력하라. 테스트를 작성할 때는 컴퓨터뿐 아니라 후에 코드를 읽을 다른 사람들도 생각해야한다. 

예를 들어서, 한 통화를 다른 통화로 환전하려고 하는데, 이 거래에는 수수료 1.5%가 붙는다. USD에서 GBP로 교환하는 환율이 2:1이라면 $100를 환전하려면 50GBP - 1.5% = 49.25GBP여야 한다. 이 테스트는 아래와 같이 쓸 수 있다. 

Bank bank = new Bank();
bank.addRate("USD", "GBP", STANDARD_RATE);
bank.commission(STANDARD_COMMISSION);
Money result = bank.convert(new Note(100, "USD"), "GBP);
assertEquals(new Note(49.25, "GBP"), result);

 또는 계산을 더 명확히 표현할 수도 있다 .

Bank bank = new Bank();
bank.addRate("USD", "GBP", 2);
bank.commission(0.015);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(100 / 2 * (1 - 0.015), "GBP"), result);

이 테스트에서는 입력으로 사용된 숫자와 예상되는 결과 사이의 관계를 읽어낼 수가 있다. 

명백한 데이터가 주는 또 다른 이점프로그래밍이 더 쉬워진다는 것이다. 단언 부분에 일단 수식을 써놓으면 다음으로 무엇을 해야 할지 쉽게알게 된다. 이런 경우 어떻게든 나눗셈과 곱셈을 수행할 프로그램을 만들어야 한다는 걸 알게 되는 것이다. 이 오퍼레이션이 어디에 속할지를 점진적으로 알아내기 위해 가짜 구현을 해볼 수도 있다.

명백한 데이터는 코드에 매직넘버를  쓰지 말라는 것에 대한 예외적인 규칙일 수도 있다. 단일 메서드의 범위에서라면 어떤 매직넘버 사이의 관계는 명백하다. 하지만 이미 정의된 기호 상수가 있다면 나는 그것을 사용할 것이다. 

 


26장. 빨간 막대 패턴 

이 패턴들은 테스트를 언제 어디에 작성할 것인지, 테스트 작성을 언제 멈출지에 대한 것이다. 

| 한 단계 테스트 |

목록에서 다음 테스트를 고를 때 무엇을 기준으로 할 것인가? 본인에게 새로운 무언가를 가르쳐줄 수 있으며, 구현할 수 있다는 확신이 드는 테스트를 고를 것.

각 테스트는 우리를 최종 목표로 한 단계 진전시켜 줄 수 있어야 한다. 다음 테스트 목록 중 무엇을 고르는게 좋을까?

  • 더하기
  • 빼기
  • 곱하기
  • 나누기
  • 비슷한 것 더하기
  • 동치성(equals)
  • 널과의 동치성(equals null)
  • 널 환전
  • 한 개의 통화를 환전하기
  • 두 개의 통화를 환전하기 
  • 환시세 

이중에서 어떤 것을 두고 해도 상관없다. (본인이 진짜로 구현이 가능하다고 확신하는 것을 고르자) 

전체 계산 중 간단한 하나의 사례를 나타내는 테스트에서 시작했다면, 이 테스트를 통해 자라는 프로그램은 하향식(top-down)으로 작성된 것으로 보일 수 있다. 반면 전체의 작은 한 조각을 나타내는 테스트에서 시작하여 조금씩 붙여나가는 식이었다면, 이 프로그램은 상향식(bottom-up)으로 작성된 것으로 보일 수 있다. 

사실은 상향식, 하향식 둘 다 TDD의 프로세스를 효과적으로 설명해 줄 수 없다. 첫째로 이와 같은 수직적 메타포는 프로그램이 시간에 따라 어떻게 변해 가는지에 대한 단순화된 시각일 뿐이다. 이보다 성장이란 단어를 보자. '성장'은 일종의 자기유사성을 가진 피드백 고리를 암시하는데, 이 피드백 고리에서는 환경이 프로그램에 영향을 주고 프로그램이 다시 환경에 영향을 준다. 둘째로, 만약 메타포가 어떤 방향성을 가질 필요가 있다면 (상향 혹은 하향보다는) '아는 것에서 모르는 것으로' 라는 방향이 유용할 것이다. '아는 것에서 모르는 것으로'는 우리가 어느 정도의 지식과 경험을 가지고 개발을 시작한다는 점, 개발하는 중에 새로운 것을 배우게 될 것임을 예상한다는 점 등을 암시한다. (우리는 아는 것에서 모르는 것으로 성장하는 프로그램을 갖게 된다)

| 시작 테스트 |

어떤 테스트부터 시작하는 게 좋을까? 오퍼레이션이 아무 일도 하지 않는 경우를 먼저 테스트할 것.

새 오퍼레이션에 대한 첫 질문은 다음과 같을 것이다. "이 오퍼레이션을 어디에 넣어야 하지?" 이 질문에 답하기 전까지는 테스트에 뭘 적어야 할지 알 수 없을 것이다. 한 번에 한 문제만 해결하자는 의미에서 다른 질문은 다 빼고 딱 이 질문만 생각할 방법은 무엇인가? 

첫 걸음으로 현실적인 테스트를 하나 작성한다면 상당히 많은 문제를 한번에 해결해야 하는 상황이 될 것이다.

  • 이 오퍼레이션을 어디에 두어야 하나?
  • 적절한 입력 값은 무엇인가?
  • 이 입력들이 주어졌을 때 적절한 출력은 무엇인가?

현실적인 테스트 하나로 시작하면 너무 오랫동안 피드백이 없을 것이다. 빨강/초록/리팩토링, 빨강/초록/리팩토링. 우리는 이 고리가 몇 분 이내로 반복되길 원할 것이다.

정말 발견하기 쉬운 입력과 출력을 사용하면 이 시간을 짧게 줄일 수 있다. (무슨말인지 이해가 잘 안된다. 걱정하지말자 책에서 설명하는 예제를 곧이 곧대로 적어보겠다.... 고..고갱뉨 당황하쎠서요)

예를 들어 XP 뉴스그룹에 누군가가 다각형 축소기를 테스트 우선으로 어떻게 작성할지 질문했다. 입력은 다각형 그물이고, 출력은 정확하게 똑같은 표면이면서 가능한 한 최소 개수의 다각형으로 구성된 다각형 그물이 된다. "테스트를 작동하도록 하는 데 박사 학위 논문을 읽어야 하는 경우, 이 문제를 어떻게 테스트 주도로 접근할 수 있을까요?" 시작 테스트 패턴이 이 문제에 대한 답을 준다. 

  • 출력이 입력과 같은 경우가 있다. 어떤 형상(configuration)의 다각형들은 이미 정규화되어 있고 더 축소할 수 없다.
  • 입력은 가능한 한 적어야 한다. 이를테면 다각형 하나 또는 아예 비어있는 다각형 목록일 수도 있다. 

나는 다음 시작 테스트를 작성했다.

Reducer reducer = new Reducer(new Polygon());
assertEquals(0, reducer.result().npoints);

첫 번째 테스트가 돌아간다. 이제 목록에 있는 나머지 테스트를 처리 할 차례다. 

한 단계 테스트는 시작 테스트에도 적용된다. 본인에게 뭔가를 가르쳐줄 수 있으면서도 빠르게 구현할 수 있는 테스트를 선택하라. 만약 본인이 어떤 애플리케이션을 n번째 구현하고 있다면, 오퍼레이션을 한두 개 필요로 하는 테스트를 하나 골라라. 본인 스스로 그걸 작동하게 할 수 있을 거라 자신할 것이다. 많은 경우 나의 시작 테스트는 그 이후의 테스트에 비해 좀더 높은 차원의 테스트로, 애플리케이션 테스트와 비슷하다.

 

| 설명 테스트 |

자동화된 테스트가 더 널리 쓰이게 하려면 어떻게 해야 할까? 테스트를 통해 설명을 요청하고 테스트를 통해 설명하라. TDD의 사용을 강요하는 것 만큼 TDD가 퍼지는 것을 빨리 막는 방법은 없다. 타인의 일하는 방식을 강제로 바꿀 수는 없다. 어떻게 해야 하나? 단순한 시작법은 테스트를 이용하여 묻고, 테스트를 이용하여 설명하는 것이다

| 학습 테스트 | 

외부에서 만든 소프트웨어에 대한 테스트를 작성해야 할 때도 있을까? 패키지의 새로운 기능을 처음으로 사용해보기 전에 작성할 수 있다. 예를들어 자바의 모바일 정보 기기 프로파일(MIDP) 라이브러리를 기반으로 뭔가를 만들어야 한다고 치자. RecordStore에 어떤 데이터를 저장하고 이를 받아오고자 한다. 그냥 코딩하고선 그게 잘 돌아가길 바라는 게 좋을까? 물론 그것도 하나의 방법이기는 하다. 한 가지 대안은 우리가 이제 막 새 클래스의 새 메서드를 하나 사용한다는 것을 알아채는 것이다. 그냥 바로 사용하는 대신 API가 우리 예상대로 실행된다는 것을 확인해줄 만한 작은 테스트를 만들어 보는 것이다. 즉 다음과 같다. 

public void setUp() {
    store = RecordStore.openRecordStore("testing", true);
}

public void tearDown() {
    RecordStore.deleteRecordStore("testing");
}

public void testStore() {
    int id = store.addRecord(new byte[] {5, 6}, 0, 2);
    assertEquals(2, store.getRecordSize(id);
    byte[] buffer = new byte[2];
    assertEquals(2, store.getRecord(id, buffer, 0));
    assertEquals(5, buffer[0]);
    assertEquals(6, buffer[1]);
}

만약 우리가 API를 제대로 이해했다면 이 테스트는 한번에 통과할 것이다. 아래는 학습 테스트를 작성하는 관례를 설명한다. 

  • 패키지의 새 버전이 도착하면 우선 테스트를 실행한다(그리고 필요하다면 수정한다).
  • 만약 테스트가 통과되지 않는다면 애플리케이션 역시 실행되지 않을 것이 뻔하기 때문에 애플리 케이션을 실행해볼 필요도 없다.
  • 일단 테스트가 통과한다면 애플리케이션은 항상 제대로 돌아갈 것이다

 

| 또 다른 테스트 |

어떻게 하면 주제에서 벗어나지 않고 기술적인 논의를 계속할 수 있을까? 주제와 무관한 아이디어가 떠오르면 이에 대한 테스트를 할일 목록에 적어놓고 다시 주제로 돌아와라.

대화를 엄격하게 한 주제로 묶는 것은 훌륭한 아이디어를 억압하는 최고의 방법이다. 하루 온종일 비생산적인 날들을 보낸 경험에서, 내가 가야 할 길을 놓치지 않는 것이 때로는 최선임을 배웠다. 새 아이디어가 떠오르면 존중하고 맞이하되 그것이 내 주의를 흩뜨리지 않게 한다. 그 아이디어를 리스트에 적어놓고는 하던 일로 다시 돌아간다. 

 

| 회귀 테스트 |

시스템 장애가 보고될 때 우리는 무슨 일을 제일 먼저 하는가? 그 장애로 인하여 실패하는 테스트, 그리고 통과할 경우엔 장애가 수정되었다고 볼 수 있는 테스트를 가장 간단하게 작성하라

회귀 테스트(regression test)란, 사실 여러분에게 완벽한 선견지명이 있다면, 처음 코딩할 때 작성했어야 하는 테스트다. 회귀 테스트를 작성할 때는 이 테스트를 작성해야 한다는 사실을 어떻게 하면 애초에 알 수 있었을지 항상 생각해보라.

전체 애플리케이션 차원에서 테스트를 수행하는 것에서도 가치를 얻을 수 있다. 애플리케이션 차원의 회귀 테스트는 시스템의 사용자들이 여러분에게 정확히 무엇을 기대했으며 무엇이 잘못되었는지 말할 기회를 준다. 좀더 작은 차원에서의 회귀 테스트는 당신의 테스트를 개선하는 방법이된다. 기괴할 정도로 큰 음수에 대한 결함보고서가 있을 수 있다. 여기에서는 테스트 목록을 작성할 때 정수 롤오버를 테스트할 필요가 있다는 것을 배울 수 있다. 

시스템 장애를 손쉽게 격리시킬 수 없다면 리팩토링해야 한다. 이러한 종류의 장애가 있다는 것은, 시스템의 설계가 아직 끝나지 않았다는 뜻이다.

 

| 휴식 | 

지치고 고난에 빠졌을 땐 뭘 해야 하나? 그럴 땐 좀 쉬는게 좋다.

당신이 결정한 것에 대한 감정적 책임과 당신이 타이핑해 넣은 문자들을 손에서 깨끗이 씻어 버리도록 하라. 종종 이 정도의 거리 두기를 통해 당신에게 부족했던 아이디어가 튀어나올 수 있다. 다음과 같은 생각이 들면 여러분은 벌떡 일어날 것이다 "매개 변수를 뒤집은 상태에서 시도한 적은 없었지!" 어찌됐건간에 좀 휴식을 취하라. 만약 그러한 아이디어를 얻지 못한다면, 현재 세션의 목적을 다시 검토해 보라. 여전히 현실적인가, 아니면 새로운 목적을 골라야 하는가? 당신이 이루려고 노력했던 것이 불가능한 건 아닌가? 만약 그렇다면 팀에는 어떤 의미가 있나? 

데이브 웅가(Dave Ungar)는 이걸 샤워 방법론이라고 부른다. 키보드로 뭘 쳐야 할지 알면, 그걸 치면 된다. 뭘 해야 할지 모르겠으면 샤워하러 가서 뭘 해야 할지 생각날 때까지 계속 샤워를 한다. 그의 방법론을 따른다면 많은 팀들이 더 행복해질 것이고 생산성도 향상될 것이며 냄새도 훨씬 덜 날것이다. TDD는 웅가의 샤워 방법론을 정제한 것이다. 키보드로 뭘 쳐야 할지 알면, 명백한 구현을 한다. 잘 모르겠다면 가짜 구현을 한다. 올바른 설계가 명확하지 않다면 삼각측량 기법을 사용한다. 그래도 모르겠다면 샤워나 하러 가는 거다. 

| 다시 하기 |

길을 잃은 느낌이 들 땐 어떻게 할까? 코드를 다 지워버리고 처음부터 다시 해보자

한 시간 전까지만 해도 잘 돌던 코드가 뒤죽박죽이 됐다. 다음 테스트를 어떻게 통과시켜야 할지도 모르겠고, 앞으로 20개나 되는 테스트를 다 구현해야 한다. 이런일은 자주 있다. 내 본능적인 반응은 꼬인 코드를 계속 진행할 수 있을만큼만 풀어놓자는 것이다. 숙고해보기 위해 잠깐 멈추어 생각해보면, 처음부터 다시 하는 게 항상 더 합리적이라는 결론이 났다. 

 

| 싸구려 책상, 좋은 의자 |

TDD를 할 때 어떤 물리적 환경이 적절한가? 나머지 시설은 싸구려를 쓸지라도 정말 좋은 의자를 구해라. 

허리가 아프면 프로그램을 잘 짤 수가 없다. 

 


27장. 테스팅 패턴

 

| 자식 테스트 | 

지나치게 큰 테스트 케이스를 어떻게 돌아가도록 할 수 있을까? 원래 테스트 케이스의 깨지는 부분에 해당하는 작은 테스트 케이스를 작성하고 그 작은 테스트 케이스가 실행되도록 하라. 그 후에 다시 원래의 큰 테스트 케이스를 추가하라

빨강/초록/리팩토링 리듬은 성공이 지속되는 데 너무나도 중요해서, 그 리듬을 잃어버릴 것 같은 위기 순간에 부가의 노력으로 리듬을 유지하는 것이 충분히 가치 있다. 나의 경우, 테스트를 만들어놓고 보니 막상 이걸 통과시키려면 몇 가지를 한번에 수정해야만 하는 때에 이런 위기의 순간이 생기는데, 단 10분간만 빨간색이 지속되어도 겁이 난다. 

나는 큰 테스트를 작성하고 나면 우선 교훈을 찾기 위해 노력한다. 왜 그렇게 테스트가 컸을까? 어떤 다른 방식을 취했더라면 좀더 작게 만들 수 있었을까? 

나 자신을 형이상학적으로 바라볼 수 있게 되면, 거슬리는 테스트를 삭제하고 다시 시작한다. 때로는 진짜로 테스트를 지워버리기도하고, 또는 메서드 이름 앞에 x를 추가해서 실행만되지 않게 하기도 한다. 두 가지 모두 시도해 보기 바란다. 테스트 두 개가 깨진 상황에서는 어떤 다른 느낌이 드는지, 스스로 어떤 다른 방식으로 코딩하는지 관찰해보고 적절한 방식을 선택하라. 

 

| 모의 객체 |

비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면 어떻게 해야 할까? 상수를 반환하게끔 만든 속임수 버전의 리소스를 만atabase들면 된다

모의 객체(Mock Object)에 대해서는 최소한 책 한 권 분량 정도의 자료가 있지만, 이 책에서는 간단한 소개 정도만 하고자 한다. 고전적인 예는 데이터베이스다. 데이터베이스는 시작 시간이 오래 걸리고, 깨끗한 상태로 유지하기가 어렵다. 그리고 만약 데이터베이스가 원격 서버에 있다면 이로 인해 테스트 성공 여부가 네트워크 상의 물리적 위치에 영향을 받게된다. 또한 데이터베이스는 개발 중 많은 오류의 원인이 된다.

해법은 대부분의 경우에 진짜 데이터베이스를 사용하지 않는 것이다. 대다수의 테스트는, 마치 데이터베이스인 것처럼 행동하지만 실제로는 메모리에만 존재하는 객체를 통해 작성될 수 있다.

public void testOrderLookup() {
    Database db = new MockDatabase();
    db.expectQuery("select order_no from Order where cust_no is 123");
    db.returnResult(new String[] {"Order 2", "Order 3"});
    ...
}

 

MockDatabase는 예상된 쿼리를 얻지 못하면 예외를 던질 것이다. 만약 쿼리가 올바르다면 MockDatabase는 상수 문자열에서 마치 결과 집합(result set)처럼 보이는 뭔가를 생성하여 반환한다. 

성능과 견고함 이외에 모의 객체의 또 다른 가치는 가독성에 있다. 당신은 앞서 나온 테스트를 끝에서 끝까지 읽을 수 있다. 만약 사실적인 데이터로 가득 찬 테스트 데이터베이스를 사용한다면, 어떤 쿼리가 결과 14개를 되돌려야 한다고 적은 테스트를 보더라도 도대체 왜 14개가 올바른 답인지 알기가 쉽지 않다. 

만약 모의 객체를 사용하길 원한다면, 값비싼 자원을 전역 변수에 손쉽게 저장해 버릴 수는 없다. 만약 그렇게 한다면, 전역 변수를 모의 객체로 설정하고, 테스트를 실행한 후 다시 전역 변수를 복구시켜 놓아야 한다.

모의 객체는 당신이 모든 객체의 가시성(visibility)에 대해 고민하도록 격려해서, 설계에서 커플링이 감소하도록 한다. 모의 객체를 사용하면 프로젝트에 위험 요소가 하나 추가된다. 모의 객체가 진짜 객체와 동일하게 동작하지 않으면 어떻게 될까? 모의 객체용 테스트 집합을 진짜 객체가 사용 가능해질 때 그대로 적용해서 이러한 위험을 줄일 수 있다. 

 

| 셀프 션트(self shunt) |

한 객체가 다른 객체와 올바르게 대화하는지 테스트하려면 어떻게 할까? 테스트 대상이 되는 객체가 원래의 대화 상대가 아니라 테스트 케이스와 대화하도록 만들면 된다.

테스팅 사용자 인터페이스의 초록 막대를 동적으로 업데이트하고자하는 상황을 가정해 보자. UI 객체를 TestResult와 연결할 수 있다면 테스트가 실행된 시점, 테스트가 실패한 시점, 전체 테스트 슈트가 시작되고 끝난 시점 등을 통보 받을 수 있을 것이다. 그리고 이를 위한 테스트는 다음과 같을 것이다. 

ResultListenerTest

def testNotification(self):
    result = TestResult()
    listener = ResultListener()
    result.addListener(listener)
    WasRun("testMethod").run(result)
    assert 1 == listener.count

이 테스트가 수행되려면 이벤트 통보 횟수를 셀 객체가 필요하다.

ResultListener

class ResultListener:
    def__init__(self):
        self.count= 0
    def startTest(self):
        self.count= self.count + 1

그런데 왜 이벤트 리스너를 위해 별도의 객체를 만들어야 하는 걸까? 그냥 테스트 케이스 자체를 리스너로 쓰면 될 텐데 말이다. 즉 테스트 케이스가 일종의 모의 객체 노릇을 하는 것이다. 

ResultListenerTest

def testNotification(self):
    self.count = 0
    result= TestResult()
    result.addListener(self)
    WasRun("testMethod").run(result)
    assert 1 == self.count
def startTest(self):
    self.count= self.count + 1

 

셀프 션트 패턴을 이용해 작성한 테스트가 그렇지 않은 테스트보다 읽기에 더 수월하다. 위의 테스트가 좋은 예다. 두 번째 버전의 테스트 메서드는 통보 횟수에 대한 두 값이 한곳에 모여 있는 반면, 첫 번째 버전의 테스트 메서드에서는 하나의 클래스에서 횟수를 0으로 설정하고 다른 클래스에서 1이 예상치임을 나타낸다. 

셀프 션트 패턴은 테스트 케이스가 구현할 인터페이스를 얻기 위해 인터페이스 추출(Extract Interface)을 해야 한다. (인터페이스를 추출하는 것이 더 쉬운지, 존재하는 클래스를 블랙 박스로 테스트하는 것이 더 쉬운지는 사용자의 몫이다) 셀프션트를 위해 추출해 낸 인터페이스는 여러 곳에서 쓰이는 경우가 많다. 

자바의 경우, 셀프 션트를 사용한 결과로 인터페이스 안의 온갖 기괴한 메서드들을 다 구현한 테스트들을 보게 될 것이다. 낙관적 타입 시스템을 가진 언어(동적인 타입 검사를 수행하는 언어들)에서는 테스트 케이스 클래스가 실제로 테스트를 수행하는데 꼭 필요한 오퍼레이션들만 구현하면 된다. 하지만 자바에서는 빈 메서드라도 인터페이스의 모든 오퍼레이션들을 구현해야 한다. 그러므로 가능한 한 인터페이스를 작게 만들길 원할 것이다. 인터페이스에 대한 구현은 또한 적절한 값을 되돌리거나 부적절한 오퍼레이션이 호출된 경우 예외를 던지게끔 만들어야 할 것이다. 

 

| 로그 문자열 | 

메시지의 호출 순서가 올바른지를 검사하려면 어떻게 해야 할까? 로그 문자열을 가지고 있다가 메시지가 호출될 때마다 그 문자열에 추가하도록 한다. 

xUnit에서 쓴 예제를 사용할 수 있다. setUp(), 테스트를 수행하는 메서드, tearDown() 순서로 호출되길 원하는 템플릿 메서드(Template Method)가 있다. 각 메서드들이 로그 문자열에 자기 이름을 추가하게 구현하면 쉽게 읽히는 테스트를 만들 수 있다. 

def testTemplateMethod(self):
    test= WasRun("testMethod")
    result= TestResult()
    test.run(result)
    assert("setUp testMethod tearDown " == test.log)

구현 또한 간단하다. 

WasRun

def setUp(self):
    self.log= "setUp "
def testMethod(self):
    self.log= self.log + "testMethod "
def tearDown(self):
    self.log= self.log + "tearDown "

로그 문자열은 특히 옵저버(Observer)를 구현하고, 이벤트 통보가 원하는 순서대로 발생하는지를 확인하고자 할 때 유용하다. 만약 어떤 이벤트 통보들이 일어나는지를 검사하기는 하지만 그 순서는 상관이 없다면 문자열 집합을 저장하고 있다가 단언(assertion)에서 집합 비교를 수행하면 된다. 

로그 문자열은 셀프 션트와도 잘 작동한다. 해당 테스트 케이스는 로그를 추가하고 적절한 값을 반환하는 식으로 셀프 션트한 인터페이스의 메서드를 구현한다. 

 

| 크래시 테스트 더미 | 

호출되지 않을 것 같은 에러 코드(발생하기 힘든 에러 상황)를 어떻게 테스트할 것인가? 실제 작업을 수행하는 대신 그냥 예외를 발생시키기만 하는 특수한 객체를 만들어서 이를 호출한다.

테스트되지 않은 코드는 작동하는 것이 아니다. 이것이 안전한 가정 같다. 그렇다면 수많은 에러 상황에 대해서는 어떻게 테스트할 것인가? --> 작동하길 원하는 부분에 대해서만 하면 된다. 

파일 시스템에 여유 공간이 없을 경우 발생할 문제에 대해 테스트하기를 원한다고 생각해보자. 1)실제로 큰 파일을 많이 만들어서 파일 시스템을 꽉 채울 수도 있고, 2)가짜 구현(fake it)을 사용할 수도 있다. 

그냥 시뮬레이션한다고 가정하자. 파일을 위한 크래시 테스트 더미(Crash Test Dummy)는 다음과 같다. 

private class FullFile extends File {
    public FullFile(String path) {
        super(path);
    }
    
    public boolean createNewFile() throws IOException {
        throw new IOException();
    }
}

이제 다음과 같은 테스트를 할 수 있다. 

public void testFileSystemError() {
    File f = new FullFile("foo");
    try {
        saveAs(f);
        fail();
    } catch (IOException e) {
    }
}

 

객체 전체를 흉내낼 필요가 없다는 점을 제외하면 크래시 테스트 더미는 모의 객체와 유사하다. 자바의 익명 클래스(anonymous innerclass)는 우리가 테스트하기 원하는 적절한 메서드만이 오류를 발생시키게끔 하기 위해 유용하게 쓰인다. 테스트 케이스 안에서 원하는 메서드 하나만 재정의할 수 있다. 이렇게 하면 테스트 읽기가 수월해진다. 

public void testFileSystemError() {
    File f = new File("foo") {
        public boolean createNewFile() throws IOException {
            throw new IOException();
        }
    };
    try {
        saveAs(f);
        fail();
    } catch (IOException e) {
    }
}

 

| 깨진 테스트 |

혼자서 프로그래밍할 때 프로그래밍 세션을 어떤 상태로 끝마치는게 좋을까? 마지막 테스트가 깨진 상태로 끝마치는 것이 좋다.

프로그래밍 세션을 끝낼 때 테스트 케이스를 작성하고 이것이 실패하는 것을 확실히 확인하는 것이다. 나중에 다시 코딩하기위해 돌아왔을 때, 어느 작업부터 시작할 것인지 명백히 알 수 있다. 전에하고 있던 생각에 대한 명확하고 구체적인 책갈피를 가지게 되는 것이다. 깨진 테스트가 하나가 있다고 해서 프로그램 완성도가 더 떨어지는 것은 아니며, 단지 프로그램의 상태를 드러나게 해줄 뿐이다.

 

| 깨끗한 체크인 |

팀 프로그래밍을 할 때 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까? 모든 테스트가 성공한 상태로 끝마치는 것이 좋다. 

같은 팀원들과 함께 작업하는 경우라면 상황은 완전히 달라진다. 팀 프로젝트에서 프로그래밍 세션을 시작하는 경우라면 자신이 마지막으로 코딩한 다음부터 지금까지 무슨 일이 있었는지 세밀하게 알 수 없다. 안심이되고 확신이 서는 상태에서 시작할 필요가 있다. 따라서 코드를 체크인하기 전에 항상 모든 테스트가 돌아가는 상태로 만들어 두어야 한다

체크인하기 전에 실행하는 테스트 스위트(test suite)는 작업 중에 분 단위로 실행하는 테스트 슈트보다 더 클 것이다. 때론 통합 테스트 슈트에서 테스트가 실패하는 경우도 있을 것이다. 그럴 땐 어떻게 해야 할까? 

가장 단순한 규칙은 그동안 작업한 코드를 날려버리고 다시 하는 것이다. 실패한 테스트는 방금 만들어낸 프로그램을 완전히 이해하지 못했다는 강력한 증거이기 때문이다. 만약 전체 팀원이 규칙을 따른다면 체크인을 더 자주하려는 경향이 생길 것이다. 왜냐하면 제일 먼저 체크인하는 사람은 작업을 날릴 위험이 없을 테니까. 체크인을 자주하는 것은 아마 좋은 일일 것이다. 

이 방법보다 아주 약간 방탕해 보이는 접근(원문은 뭐라고 적혀있었을까..?)은, 문제를 수정하고 테스트를 다시 실행해보는 것이다. 통합 자원을 독차지하는 것을 피하기 위해, 아마도 몇 분 후에는 그냥 포기해버리고 다시 시작해야 할 것이다. 말할 필요도 없지만, 테스트 슈트를 통과시키기 위해 주석 처리 하는 것은 금지되는 것이다. 

 

728x90
개발서적

마이크로서비스 패턴 - 길벗 출판사

728x90

최근 면접본 회사는 MSA(Micro Service Architecture)식 개발을 한다고 하였고, 더 최근에 기술과제를 수행하고 그에 대한 피드백을 받았는데, 디자인 패턴에 대한 공부를 병행하는 것을 추천받았다. 때마침 길벗 출판사에서 "마이크로서비스 패턴"이라는 책을 출간했다. 또한 서평단을 모집한다는 소식에 망설이지않고 바로 지원했다. 결과는 .... (두둥)

 

집에 책이 도착했다. 신나는 마음으로 열어보았다. 처음에는 책 내용을 정리해서 같이 적을까 하다가. 적어둔 내용이 (절반정도 적었었는데... ㅠ) 싹 날아가버려서 ... 도저히 다시 그정도의 요약을 하기는 힘들 것 같다. 

아뿔사... 대상 독자를 보니 CTO, 기술 부사장, 아키텍트, 개발자 등을 얘기하고있다. '개발자'라고 포괄적인 범위 외에는 뭔가 다들 개발 년차가 최소 두자리수는 넘어 갈 것 같은 직책이다. (약간 의기소침 해지지만... 가볍게! 가볍게! 언젠가 나도 이런 포지션이 오지 않을까? 생각보다 금방 다가올지도 모른다. 적어도 처음 다가왔을 때, 그게 뭐지? 보다는 아...? 정확하게는 모르지만 그거? 정도는 되면 처음 보는 것 보다는 쉽게 와닿지 않을까? 하는 큰 그림을 그려본다) 


챕터의 순서와 간략한 소개를 진행해본다. 

1장. 모놀리식 지옥에서 벗어나라

미국의 FTGO(Food to Go)의 예를 들면서 내용을 진행하고있다. 미국 온라인 배달 업계를 선도하는 회사라고 한다. 우리나라의 '배달의 민족' 정도의 포지션이 아닐까 한다. FTGO가 사업초기와 같이 작은 규모일 때는 문제점이 없었지만, 규모가 엄청나게 커진 FTGO는 모놀리식 지옥에 빠져있는 상황이고 이를 극복하기 위해 마이크로 서비스 패턴을 적용하려고한다. 

먼저 모놀리식 아키텍처의 장점을 알아보자

모놀리식 아키텍처의 장점 

  • 개발이 간단하다
  • 애플리케이션을 쉽게 변경할 수 있다.
  • 테스트하기 쉽다.
  • 배포하기 쉽다.
  • 확장하기 쉽다.

(하지만 거대한 모놀리스는 이러한 장점들의 의미가 퇴색하기 시작한다) 

모놀리식 지옥에 빠지게 되는 이유

  • 너무 복잡해서 개발자가 주눅 든다.
  • 개발이 더디다.
  • 커밋부터 배포에 이르고 길고 험난한 여정
  • 확장하기 어렵다
  • 모놀리스는 확실하게 전달하기 어렵다
  • 갈수록 한물간 기술 스택에 발목이 붙잡힌다

모놀리스 지옥을 벗어나기 위한 해법으로 마이크로서비스 아키텍처를 제시한다.  

마이크로서비스 아키텍처의 특징

  1. 확장 큐브와 마이크로서비스 (확장 큐브라는 3차원 확장 모델)
  2. 마이크로서비스는 모듈성을 갖고 있다.
  3. 서비스마다 DB가 따로 있다
  4. FTGO 마이크로서비스 아키텍처
  5. 마이크로서비스 아키텍처와 SOA

 

마이크로서비스 아키텍처의 장점

  • 크고 복잡한 애플리케이션을 지속적으로 전달/배포할 수 있다.
  • 서비스 규모가 작아 관리하기 쉽다.
  • 서비스를 독립적으로 배포/확장할 수 있다.
  • 마이크로서비스 아키텍처 덕분에 팀이 자율적으로 움직인다.
  • 결함 격리가 잘된다.
  • 새로운 기술을 실험하고 도입하기 쉽다.

 

마이크로서비스가 만병 통치약은 아니다. 마냥 장점만 있는게 아니다. 

마이크로서비스 아키텍처의 단점

  • 딱 맞는 서비스를 찾기가 쉽지 않다.
  • 분산 시스템은 너무 복잡해서 개발, 테스트, 배포가 어렵다.
  • 여러 서비스에 걸친 기능을 배포할 때에는 잘 조정해야 한다.
  • 마이크로서비스 아키텍처 도입 시점을 결정하기가 어렵다.

또한 책에서는 패턴과 패턴 언어를 소개하면서 마이크로서비스 아키텍처 패턴 언어를 설명하고 있다.

패턴은 아래와 같이 총 세 계층으로 분리되고 각 계층 중 주요 패턴 그룹을 하나씩 설명한다.

  • 인프라 패턴(infrastructure pattern)
  • 애플리케이션 인프라(application infrastructure)
  • 애플리케이션 패턴(application pattern)

 

2장. 분해 전략 

마이크로서비스 아키텍처란 무엇인가?로 시작하여서 소프트웨어 아키텍처의 정의, 아키텍처 스타일 그리고 마이크로서비스 아키텍처는 또 무엇인지에 대한 설명을 곁들이고있다. (이번 장의 구성이 나에게는 생소한 내용들임에도 불구하고 딱히 구선생님(Google)의 도움없이 잘 헤쳐나갔다) 

또한 마이크로서비스 아키텍쳐의 정의와 예시 모델들을 통해서 이해하기 쉽게 설명하고있다. 

3장. 프로세스 간 통신 

요즘에는 서비스에 적용 가능한 IPC(Inter-Process Communication) 기술이 정말 많다. HTTP, REST, gRPC... 동기 요청/응답 기반의 통신 메커니즘도 있고, AMQP, STOMP 등 비동기 메시지 기반의 통신 메커니즘도 있다. 메시지 포맷 역시 JSON, XML처럼 인간이 읽을 수 있는 포맷부터 아브로나 프로토콜 버퍼처럼 효율적인 이진 포맷등 매우 다양하다. 

서비스 API에 알맞은 IPC를 선택하기 전에 클라이언트/서비스 간 상호 작용 스타일을 잘 살펴보면, 요건에서 벗어나는 일 없이 특정 IPC 기술의 세부 내용에 빠져 헤매는 일을 방지할 수 있고, 상호 작용 스타일의 선택의 중요성을 설명하고 있다. 

첫째, 클라이언트/서비스 상호 작용 스타일은 두 가지 기준으로 분류할 수 있다.

  • 일대일
    1) 요청/응답  2) 비동기 요청/응답  3) 단방향 알림 
  • 일대다
    1) 발행/구독  2) 발행/비동기 응답

둘째, 동기/비동기 여부이다. 

  • 동기
  • 비동기

또한 마이크로서비스 API를 정의에 대한 설명을 하고있다. 

API는 어떤 IPC를 사용하느냐에 따라 그 내용이 결정된다. 메시징으로 통신하는 API는 메시지 채널, 메시지 타입, 메시지 포맷으로 정의한다. HTTP로 통신하는 API는 URL, HTTP 동사, 요청/응답 포맷으로 구성된다.

서비스 API는 시간에 따라 조금씩 발전한다. 걱정하지마라 이 책에서는 API를 발전시키기 위한 방법도 제시한다. (정말 이정도면 A to Z가 아닌가? 많이 친절한 서적인 것 같다.   추가적으로 지금까지 작성한 글을 보니 13장중에서 이제 3장을 적고있는데 이미 스압(스크린 압박)이 느껴진다. 서평이라는 것이 책의 내용을 요약하는 것은 아니고 책이 어떤지를 평하는 것이 목적이니 이후 내용은 간략하게 챕터를 언급하고 넘어가겠다.)

 

이후의 Chapter 

4장. 트랜잭션 관리: 사가

5장. 비즈니스 로직 설계 

6장. 비즈니스 로직 개발: 이벤트 소싱

7장. 마이크로서비스 쿼리 구현

8장. 외부 API 패턴 

9장. 마이크로서비스 테스트 1부

10장. 마이크로서비스 테스트 2부

11장. 프로덕션 레디 서비스 개발

12장. 마이크로서비스 배포

13장. 마이크로서비스로 리팩터링

부록. 실습 환경 구성 


Q. 이 책 어때요?

전체적인 책의 구성이 잘되어있다. 앞서 설명했듯이 디자인 패턴에 대한 공부를 따로 한적이 없는 신입 개발자(정확히는 신입을 지망하는...)도 이해하는데 그렇게 어렵지 않았다. 책을 처음부터 꼼꼼히만 읽는다면 궁금증이 생기는 부분에 대해서는 미리 전제를 깔고 간다거나 혹은 (친절하게도) 뒷장에서 설명을 해준다는 언급을 미리 해주고 뒷장에서 자세한 설명을 해준다. 내 성격상 글을 읽다가 이해가 안되면 (구gle선생님)바로바로 찾아보는 편인데 가끔 배경지식이 거의 없는 글을 볼때는 책보다 구글을 더 많이 봐서 책을 읽는 속도가 많이 느려지기도 했다. 그렇게 이해를 위해서 긴 시간을 투자해서 앞장들을 보면서 왔는데 알고보니 뒷장에서 설명이 자세하게 되어있는 경우... 그 허탈함은 이루 말할 수가 없다. 이런 독자의 수고를 덜어주는 구성도 좋았다. 

아직까지 다른 디자인 패턴 책을 본적이 없어서 다른 책들도 이렇게 친절하게 A to Z 식의 설명을 해주는지는 모르겠지만, 디자인 패턴이라는게 그렇게 쉬운 주제가 아니라고 생각한다(아마 나만 그렇게 생각하는게 아니지 않을까?). 그러한 주제다보니 사실 읽기전부터 걱정도 많이 됐었는데 막상 책을 읽으면서 우려했던 일(= 이게 무슨말이지...? 의 늪)은 벌어지지 않았다. 오히려 이번 계기로 디자인 패턴에 대한 기본적인 지식뿐만 아니라 마이크로 서비스 아키텍쳐, 모놀리식 아키텍처에 대해서도 알게 되었고(난 지금까지 모놀리식 아키텍처를 따르고 있었구나...? 라는 걸 알게됐다), 마이크로서비스 패턴을 따르는데 발생하는 문제점을 해결하기 위한 전략 및 방법론에 대해서도 이런게 있구나!? 정도의 지식을 얻을 수 있었다. '마이크로서비스 패턴'이 지금 애초당시에 한번 읽은 것 가지고는 온전히 내 것이 되기에는 쉽지 않은 주제이기에 2~3번은 더 읽어봐야 어디가서 좀 안다고 말 하지 않을까 싶다. 

728x90
개발서적/모던 자바 인 액션

Chap 4, 5, 6 - 스트림 소개, 활용, 데이터 수집

728x90

스터디를 통해서 '모던 자바'에 대한 공부를 했었다. 하지만 배울 때는 분명 다 내것이 되었다고 생각했었는데, 막상 기억에 남는게 별로 없는 것 같다. 역시 복습은 선택이아닌 필수가 아닌가 한다. (= 인간은 망각의 동물이다) 당장 내일 응시해야하는 코딩테스트가 하나 있어서, 급하게 스트림의 활용에 대한 내용을 간략하게 정리해보자 한다. 이론적인 부분은 시간을 잡아서 제대로 정리하려고 한다(스트림 뿐만 아니라 모던 자바 인 액션 책 전체적으로 정리를 하려고 한다). 


스트림(Stream)은 자바 8에서 추가된 기능이고, 스트림을 이용하면 선언형(SQL의 쿼리와 같이 질의로 표현할 수 있다)으로 컬렉션 데이터를 처리할 수 있다. 또한 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다(단순하게 stream() --> parallelStream()으로 바꾸기만 하면 사용이 가능하다. 물론 고려해야 할 요소가 있다... 일단은 넘어가자) 

자바 8의 스트림 API의 특징을 다음처럼 요약할 수 있다.

  • 선언형 : 더 간결하고 가독성이 좋아진다.
  • 조립할 수 있음 : 유연성이 좋아진다.
  • 병렬화 : 성능이 좋아진다.

스트림에는 다음과 같은 두 가지 중요 특징이 있다. 

  • 파이프라이닝(pipelining) : 대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. 그 덕분에 게으름(laziness), 쇼트서킷(short-circuiting) 같은 최적화도 얻을 수 있다. 연산 파이프라인은 데이터 소스에 적용하는 데이터베이스 질의와 비슷하다.
  • 내부 반복 : 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다.

 

스트림에 filter, map, limit, collect로 이어지는 일련의 데이터 처리 연산을 적용한다. collect를 제외한 모든 연산은 서로 파이프라인을 형성할 수 있도록 스트림을 반환한다. 파이프라인은 소스에 적용하는 질의 같은 존재다. 마지막으로 collect 연산으로 파이프라인을 처리해서 결과를 반환한다. 마지막에 collect를 호출하기 전까지는 무엇도 선택되지 않으며 출력 결과도 없다. 즉, collect가 호출되기 전까지 메서드 호출이 저장되는 효과가 있다.

filter, map, limit, collect는 각각 다음 작업을 수행한다.

  • filter : 람다를 인수로 받아 스트림에서 특정 요소를 제외시킨다. 
  • map : 람다를 이용해서 한 요소를 다른 요소로 변환하거나 정보를 추출한다. 
  • limit : 정해진 개수 이상의 요소가 스트림에 저장되지 못하게 스트림 크기를 축소 truncate한다. 
  • collect : 스트림을 다른 형식으로 변환한다. (일단은 collect가 다양한 변환 방법을 인수로 받아 스트림에 누적된 요소를 특정 결과로 변환시키는 기능을 수행하는 정도...로 이해하자) 

복잡한 데이터 처리 질의를 표현하는 스트림 기능을 자세히 살펴본다. 필터링, 슬라이싱, 검색, 매칭, 매핑, 리듀싱 등의 많은 패턴을 다룰 것이다. 

스트림은 중간연산자와 최종연산자로 나뉘는데.. (오늘은 좀 급하니까 자세한 설명은 생략하겠다. 중간연산자는 말그대로 파이프 라인의 중간 연산자 (리턴 타입이 스트림), 최종연산자는 파이프라인의 마지막이다.)

중간 연산

연산 형식 반환 형식 연산의 인수 함수 디스크립터
filter 중간 연산 Stream<T> Predicate<T> T -> boolean
map 중간 연산 Stream<R> Function<T, R> T -> R
limit 중간 연산 Stream<T>    
sorted 중간 연산 Stream<T> Comparator<T> (T, T) -> int
distinct 중간 연산 Stream<T>    

 

최종 연산

연산 형식 반환 형식 목적
forEach 최종 연산 void 스트림의 각 요소를 소비하면서 람다를 적용한다.
count 최종 연산 long(generic) 스트림의 요소 개수를 반환한다.
collect 최종 연산   스트림을 리듀스해서 리스트, 맵 정수 형식의 컬렉션을 만든다.

 

예제 

필터링

1) 프리디케이트 필터링 방법

List<Dish> vegetarianMenu = menu.stream()
                                .filter(Dish::isVegitarian)
                                .collect(toList()); 

2) 고유 요소 필터링 방법 

List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream()
       .filter(i -> i % 2 == 0)
       .distinct()
       .forEach(System.out::println);
       

 

스트림 슬라이싱

1) 프레디케이트를 이용한 슬라이싱 
1-1) takeWhile 활용 

List<Dish> filterdMenu = specialMenu.stream()
                                    .takeWhile(dish -> dish.getCalories() < 320)
                                    .collect(toList());

(filterdMenu가 이미 칼로리의 오름차순으로 정렬되어 있다는 가정에서는 takeWhile을 이용하면 그냥 filter를 이용한 것보다 더 효율적인 처리가 가능하다) takeWhile : 프리디케이트가 거짓이라면 반복 작업을 중단

1-2) dropWhile 활용

List<Dish> slicedMenu2 = specialMenu.stream()
                                    .dropWhile(dish -> dish.getCalories() < 320)
                                    .collect(toList());

dropWhile은 takeWhile과 정 반대의 작업을 수행한다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다. (프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환단다) 

2) 스트림 축소 

2-1) limit 활용

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환는 limit(n) 메서드를 지원한다. 스트림이 정렬되어 있으면 최대 n개를 반환할 수 있다. 

List<Dish> dishes = specialMenu.stream()
                               .filter(dish -> dish.getCalories() > 300)
                               .limit(3)
                               .collect(toList());
                               

소스가 정렬되어 있지 않았다면 limit의 결과도 정렬되지 않은 상태를 반환된다. 

2-2) skip 활용 

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다. 

List<Dish> dishes = menu.stream()
                        .filter(d -> d.getCalories() > 300)
                        .skip(2)
                        .collect(toList());
                        

 

3) 매핑

3-1) 스트림의 각 요소에 함수 적용하기 

List<String> dishNames = menu.stream()
                             .map(Dish::getName)
                             .collect(toList());
List<Integer> wordLengths = words.stream()
                                 .map(String::length)
                                 .collect(toList());
                                 

 

3-2) 스트림 평면화

(이부분은 볼때마다 헷갈리는 부분이라서 조금 더 자세히 적는다)
메서드 map을 이용해서 리스트의 각 단어의 길이를 반환하는 방법을 확인했다. 이를 응용해서 리스트에서 고유 문자로 이루어진 리스트를 반환해보자. 예를 들어 ["Hello", "World"] 리스트가 있다면 결과로 ["H", "e", "l", "o", "W", "r", "d"]를 포함하는 리스트가 반환되어야 한다. 

아래와 같이 리스트에 있는 각 단어를 문자로 매핑한 다음에 distinct로 중복된 문자를 필터링해서 쉽게 문제를 해결할 수 있다고 생각할 수 있지만.... 

words.stream()
     .map(word -> word.split(""))
     .distinct()
     .collect(toList());

위 코드에서 map으로 전달한 람다의 각 단어의 String[](배열) 을 반환한다는 점이 문제다. 따라서 map 메서드가 반환한 스트림의 형식을 Stream<String[]> 이다. 우리가 원하는 것은 문자열의 스트림을 표현할 Stream<String> 이다. 다행히 flatMap이라는 메서드를 이용해서 이 문제를 해결할 수 있다.

words.stream()
     .map(word -> word.split(""))
     .map(Arrays::stream)
     .distinct()
     .collect(toList());

결국 스트림 리스트가 만들어지면서 문제가 해결되지 않았다. 문제를 해결하려면 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 다음에 각 배열을 별도의 스트림으로 만들어야 한다. 

flatMap 의 사용 

flatMap을 사용하면 다음처럼 문제를 해결할 수 있다. 

List<String> uniqueCharacters = words.stream()
                                     .map(word -> word.split("")
                                     .flatMap(Arrays::stream)
                                     .distinct()
                                     .collect(toList());

flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, map(Arrays::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다. (요약하자면.. flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 나의 스트림으로 연결하는 기능을 수행한다) 

4. 검색과 매칭

4-1) 프레디케이트가 적어도 한 요소와 일치하는지 확인 

if(menu.stream().anyMatch(Dish::isVegetarian)) {
    ....
} 

anyMatch 메서드를 이용한다. anyMatch는 불리언을 반환하므로 최종 연산이다. 

4-2) 프레디케이트가 모든 요소와 일치하는지 검사

allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다. 

boolean isHealthy = menu.stream()
                        .allMatch(dish -> dish.getCalories() < 1000);

 

NONEMATCH 

noneMatch는 allMatch와 반대 연산을 수행한다. 즉, noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다. 예를 들어 이전 이전 예제를 다음처럼 noneMatch로 다시 구현할 수 있다. 

boolean isHealthy = menu.stream()
                        .noneMatch(d -> d.getCalories() >= 1000);

anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다. 

4-3) 요소 검색 

findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny 메서드를 다른 스트림연산과 연결해서 사용할 수 있다. 예를 들어 다음 코드처럼 filter와 findAny를 이용해서 채식요리를 선택할 수 있다. 

Optional<Dish> dish = menu.stream()
                          .filter(Dish::isVegetarian)
                          .findAny(); 

스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다. 즉, 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다. (Optional 또한 중요한 클래스인데.. 실제로 모던 자바 인 액션에서도 한 비중을 차지하고 있다. 지금은 Stream의 정리가 급한 상황이니 따로 깊게 들어가지는 않겠다. null 처리를 위한 요소라고 생각하면 좋을 것 같다. 어떤 필드가 Optional 타입이라면 이 값은 존재하지 않을 수도 있다는 전제가 깔려있다고 생각해도 좋다.  어떤 의미로는 가독성도 늘어난다.(물론 코드를 보는 사람도 Optional에 대한 이해를 하고 있다는 가정에서 ) )

 

4-4) 첫 번째 요소 찾기 

리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야 할까? 예를 들어 숫자 리스트에서 3으로 나누어떨어지는 첫 번째 제곱값을 반환하는 다음 코드를 살펴본다. 

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
                                                           .map(n -> n % 3 == 0)
                                                           .filter(n -> n % 3 == 0)
                                                           .findFirst(); //9

 

더보기

findFirst와 findAny의 사용 시기? 

왜 비슷해보이는 두 메서드가 모두 존재할까?  바로 병렬성 때문이다. 병렬 실행에서는 첫 번째 요소를 찾기가 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다. 

 

5) 리듀싱

지금까지 살펴본 최종 연산은 불리언, void, 또는 Optional 객체를 반환했다. 또한 collect로 모든 스트림의 요소를 리스트로 모으는 방법도 살펴봤다. 

리듀스(reduce) 연산을 이용해서 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 방법을 설명한다. 

5-1) 요소의 합 

리스트의 숫자 요소를 더하는 코드를 확인한다. numbers의 각 요소는 결과에 반복적으로 더해진다. 리스트에서 하나의 숫자가 남을 때까지 reduce 과정을 반복한다. 코드에는 파라미터를 두 개 사용했다. 

  • sum 변수의 초깃값0
  • 리스트의 모든 요소를 조합하는 연산(+)

이런 상황에서 reduce를 이용하면 애플리케이션의 반복된 패턴을 추상화할 수 있다. reduce를 이용해서 다음처럼 스트림의 모든 요소를 더할 수 있다. 

int sum = numbers.stream().reduce(0, (a,b) -> a + b);

reduce는 두 개의 인수를 갖는다. 

1) 초깃값 0 , 2) 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>. 예제에서는 람다 표현식 (a,b) -> a + b를 사용했다. 

reduce로 다른 람다, 즉 (a,b) -> a * b를 넘겨주면 모든 요소에 곱셈을 적용할 수 있다. 

int product = numbers.stream().reduce(1, (a, b) -> a * b);

 

(초깃값을 받지 않도록 오버로드된 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환 받는다. ) 

5-2) 최댓값과 최솟값 

최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다. reduce를 이용해서 스트림에서 최댓값과 최솟값을 찾는 방법을 살펴보자. reduce는 두 인수를 받는다. 

  • 초깃값
  • 스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다
Optional<Intger> max = numbers.stream().reduce(Integer::max);

Integer.max 대신 Integer.min 대신 람다 표현식 (x, y) -> x < y ? x : y를 사용해도 되지만, 메서드 참조 표현이 더 읽기 쉽다. 

 

5-3) 숫자형 스트림 

reduce 메서드로 스트림 요소의 합을 구하는 예제를 살펴봤다. 예를 들어 다음처럼 메뉴의 칼로리 합계를 계산할 수 있다. 

int calories = menu.stream()
                   .map(Dish::getCalories)
                   .reduce(0, Integer::sum);

 

사실 위 코드에는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다. 다음 코드처럼 직접 sum메서드를 호출할 수 있다면 더 좋지 않을까? 이런 취지에서 바로 최종 연산자를 sum() 으로 호출하면 좋을텐데.. 직접 sum 메서드를 호출할 수는 없다. map 메서드가 Stream<T>를 생성하기 때문이다. 스트림의 요소 형식은 Integer지만 인터페이스에는 sum 메서드가 없다. 이를 해결하기 위해서는 기본 특화 스트림을 사용해야 한다. 

5-3-1) 기본형 특화 스트림 

int calories = menu.stream()
                   .mapToInt(Dish::getCalories)
                   .sum();
                   

 

mapToInt 메서드는 각 요리에서 모든 칼로리(Integer 형식)를 추출한 다음에 IntStream을 반환한다. 따라서 IntStream 인터페이스에서 제공하는 sum 메서드를 이용해서 칼로리 합계를 계산할 수 있다. 스트림이 비어있으면 sum은 기본값 0을 반환한다. IntStream은 max, min, averager 등 다양한 유틸리티 메서드도 지원한다. 

객체 스트림으로 복원하기

숫자 스트림을 만든 다음에, 원상태인 특화되지 않은 스트림으로 복원할 수 있을까? 간단하게 말하자면 boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다. 

Stream<Integer> stream = menu.stream()
                          .mapToInt(Dish::getCalories)
                          .boxed();

 

숫자 범위 

자바 8의 IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다. 두 메서드 모두 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을 갖는다. range 메서드는 시작값과 종료값이 결과에 포함되지 않는 반면 rangeClosed는 시작값과 종료값이 결과에 포함된다는 점이 다르다. 

IntStream evenNumbers = IntStream.rangeClosed(1, 100)
                                 .filter(n -> n % 2 == 0);

 

스트림 만들기

이 절에서는 일련의 값, 배열, 파일, 심지어 함수를 이용한 무한 스트림 만들기 등 다양한 방식으로 스트림을 만드는 방법을 설명한다. 

 

값으로 스트림 만들기

임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다. 예를 들어 다음 코드는 Stream.of로 문자열 스트림을 만드는 예제다. 스트림의 모든 문자열을 대문자로 변환한 후 문자열을 하나씩 출력한다. 

Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out.println); 

 

null이 될 수 있는 객체로 스트림 만들기 

자바 9에서는 null이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 메서드가 추가되었다. 때로는 null이 될 수 있는 객체를 스트림으로 만들어야 할 수 있다. 예를 들어 System.getProperty는 제공된 키에 대응하는 속성이 없으면 null을 반환한다. 이런 메서드를 스트림에 활용하려면 다음처럼 null을 명시적으로 확인해야 했다. 
(이부분은 잘 이해가 안되는 것 같다...) 

 

배열로 스트림 만들기 

배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림을 만들 수 있다. 예를 들어 다음처럼 기본형 int로 이루어진 배열을 IntStream으로 변환할 수 있다. 

int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum(); 

 

함수로 무한 스트림 만들기

스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용해서 무한 스트림, 즉 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과는 달리 크기가 고정되지 않은 스트림을 만들 수 있다. iterate와 generater에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다. 따라서 무제한으로 값을 계산할 수 있다. 하지만 보통 무한한 값을 출력하지 않도록 limit(n) 함수를 연결해서 사용한다. 

iterater 메서드

Stream.iterate(0, n -> n + 2)
      .limit(10)
      .forEach(System.out::println);

iterate 메서드는 초깃값과 람다를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다. iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 무한 스트림을 만든다. 

generate 메서드

Stream.generate(Match::random)
      .limit(5)
      .forEach(System.out::println);

iterate와 비슷하게 generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다. 하지만 iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않는다. generate는 Supplier<T>를 인수로 받아서 새로운 값을 생산한다. 

 


 

스트림으로 데이터 수집 

reduce가 그랬던 것처럼 collect 역시 다양한 요소 누적 방식을 인수로 받아서 스트림을 최종 결과로 도출하는 리듀싱 연산을 수행할 수 있음을 설명한다. 다양한 요소 누적 방식은 Collector 인터페이스에 정의되어 있다. (지금부터 컬렉션(Collection), 컬렉터(Collector), collect를 헷갈리지 않도록 주의하자) 

 

컬렉터란 무엇인가?

collect 메서드로 Collector 인터페이스 구현을 전달했다. Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다. 이전에는 toList를 Collectior 인터페이스의 구현으로 사용했다. 여기서는 groupingBy를 이용해서 맵(Map)을 만든다. 

고급 리듀싱 기능을 수행하는 컬렉터

훌륭하게 설계된 함수형 API의 또 다른 장점으로 높은 수준의 조합성과 재사용성을 꼽을 수 있다. collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이 컬렉터의 최대 강점이다. 보통 함수를 요소로 변환할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적한다. Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다. 

 

미리 정의된 컬렉터 

Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분할 수 있다. 

  • 스트림 요소를 하나의 값으로 리듀하고 요약
  • 요소 그룹화
  • 요소 분할

먼저 리듀싱과 요약관련 기능을 하는 컬렉터부터 살펴본다. 

리듀싱과 요약 

Collector 팩토리 클래스로 만든 컬렉터 인스턴스로 어떤 일을 할 수 있는지 살펴보자

1) counting() 

long howManyDishes = menu.stream()
                         .collect(Collectors.counting());

다음처럼 불필요한 과정을 생략할 수도 있다. 

long howManyDishes = menu.stream().count(); 

counting 컬렉터는 다른 컬렉터와 함께 사용할 때 위력을 발휘한다. 

스트림값에서 최댓값과 최솟값 검색 

Collectors.maxBy, Collectors.minBy 두 개의 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다. 

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream()
                                     .collect(maxBy(dishCaloriesComparator));

 

요약연산

Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드를 제공한다. summingInt는 객체를 int로 매핑하는 함수를 인수로 받는다. summingInt의 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 

int totalCalories = menu.stream()
                        .collect(summingInt(Dish::getCalories));

총합

double avgCalories = menu.stream()
                         .collect(averagingInt(Dish::getCalories));

평균

IntSummaryStatistics menuStatistics = menu.stream()
                                          .collect(summarizingInt(Dish::getCalories));

종합 --> IntSummaryStatistics 클래스로 모든 정보가 수집된다. menuStatistics 객체를 출력하면 다음과 같은 정보를 확인할 수 있다.
IntSummaryStatistics{count=9, sum=4300, min=120, average=477.7777778, max=800}  

마찬가지로 int뿐 아니라 long이나 double에 대응하는 summarizingLong, summarizingDouble 메서드와 관련된 LongSummaryStatistics, DoubleSummaryStatistics 클래스도 있다. 

 

문자열 연결 

컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다. 

String shoryMenu = menu.stream()
                       .map(Dish::getName)
                       .collect(joining());

 

String shortMenu = menu.stream()
                       .map(Dish::getName)
                       .collect(joining(", ")); 

(구분자 ","를 넣어서 가독성을 높인다) 

범용 리듀싱 요약 연산

지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있다. 즉, 범용 Collectors.reducing으로도 구현할 수 있다. (그럼에도 범용 팩토리 메서드 대신 특화된 컬렉터를 사용한 이유는 프로그래밍적 편의성 때문이다) 

int totalCalories = menu.stream()
                        .collect(reducing(0, Dish::getCalories, (i,j) -> i + j));

reducing은 인수 세 개를 받는다. 

  • 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다(숫자 합계에서는 인수가 없을 때 반환값으로 0이 적합하다).
  • 두 번째 인수는 변환 함수다.
  • 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다.

다음처럼 한 개의 인수를 가진 reducing 버전을 이용해서 가장 칼로리가 높은 요리를 찾는 방법도 있다. 

Optional<Dish> mostCalorieDish = menu.stream()
                                     .collect(reducing((d1, d2)
                                     -> d1.getCalories() > d2.getCalories() ? d1 : d2);

 

컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.

reducing 컬렉터를 사용한 이전 예제에서 람다 표현식 대신 Integer 클래스의 sum 메서드 참조를 이용하면 코드를 좀 더 단순화할 수 있다. 

int totalCalories = menu.stream()
                        .collect(reducing(0, Dish::getCalories, Integer::sum));

 

그룹화 

데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다. 다음처럼 팩토리 메서드 Collectors.groupingBy를 이용해서 쉽게 메뉴를 그룹화할 수 있다. 

Map<Dish.Type, List<Dish>> dishesByType = menu.stream()
                                              .collect(groupingBy(Dish::getType));

이 함수를 기준으로 스트림이 그룹화되므로 이를 분류함수라고 부른다. 

단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다. 따라서 다음 예제처럼 람다 표현식으로 필요한 로직을 구현할 수 있다. 

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream()
                                                         .collect(groupingBy(dish -> {
                                                             if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                                                             else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                                                             else return CaloricLevel.FAT;
                                                         }));

 

그룹화된 요소 조작 

요소가 없어도 키 값은 남아있게 하려면 아래의 두 예제를 잘 살펴보자 

Map<Dish.Type, List<Dish>> caloricDishesByType = menu.stream()
                                                     .filter(dish -> dish.getCalories() > 500)
                                                     .collect(groupingBy(Dish::getType));

 

Map<Dish.Type, List<Dish>> caloricDishesByType = menu.stream()
                                                     .collect(groupingBy(Dish::getType,
                                                         filtering(dish -> dish.getCalories() > 500, toList())));

 

위의 요소는 필터에 해당하는 값이 하나도 없는 key경우 아예 Map 컬렉션에 저장하지 않는다. 하지만 groupingBy 팩토리 메서드를 오버로드해 이 문제를 해결할 수 있다. 두 번째 Collector 안으로 필터 프리디케이트를 이동함으로써 요소가 하나도 없더라도 key 값이 Map 컬렉션에 저장된다. 

또한 그룹화된 항목을 조작하는 다른 유용한 기능 중 또 다른 하나로 맵핑 함수를 이용해 요소를 변환하는 작업이 있다. filtering 컬렉터와 같은 이유로 Collectors 클래스는 매핑 함수와 각 항목에 적용한 함수를 모으는 데 사용하는 또 다른 컬렉터를 인수로 받는 mapping 메서드를 제공한다. (예를 들어 이 함수를 이용해 그룹의 각 요리를 관련 이름 목록으로 변환할 수 있다) 

Map<Dish.Type, List<String>> dishNamesByType = menu.stream()
                                                   .collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));

flatMapping 또한 가능하지만... 생략하겠다. 

이외에 서브 그룹으로 나누는 내용도 있고 하지만... 코딩 테스트에서 그정도의 문제를 풀 일은 없을 것 같아 이정도로만 정리한다. 어짜피 모던 자바 인 액션 책은 1장부터 (1,2장은 생략하지 않을까?) 정리할 예정이다.(시간이 주어진다면..) 

 

728x90
개발서적

고객이 보이는 구글 애널리틱스 - 한빛미디어

728x90

현재 한빛 미디어의 서평단으로 활동 중이다. 3월의 랜덤 서적으로 '고객이 보이는 구글 애널리틱스' 책을 받았다. 사실 선택한 3개 중에서 가장 안왔으면 했던 책이다. (어떻게... 귀신같이) 그래도 최근에 면접을 봤던 회사(결과가 궁금한 사람이 있을까봐 TMI하게 말하자면 떨어졌다...ㅠ)에서 '구글 애널리틱스'를 이용해서 더 많은 고객을 유치하기위한 노력을 하고 있다는 얘기를 들었었다. 이번 기회에 한번 읽으면서 구글 애널리틱스의 이점과 사용법에 대해서 알아보자는 긍정적인 생각으로 읽었고 서평을 작성한다.

한빛미디어 - 고객이 보이는 구글 애널리틱스 서적

 

먼저 각 챕터의 구성을 나열해본다.

먼저 실습을 하기 직전 기초적인 웹로그의 개념과 구글 애널리틱스를 사용함으로써의 장점, 그리고 각 포지션(기획자, 마케터 ..etc)이 웹로그를 분석해야하는 이유 같은 내용도 알 수 있었다. 뒤에 실습을 위해서는 꼭 숙지하고 지나가야 할 내용들이다. 

1장. 웹로그 분석이 무엇인지 알아보고 구글 애널리틱스가 웹 로그 분석을 어떻게 쉽게 만들어주는지 또 어떻게 구글 애널리틱스를 사용하는지 알아본다. 

2장. 구글 애널리틱스 시작하기 
구글 애널리틱스 계정생성, 공부하기 위한 실습사이트 소개

3장. 보고서 사용 방법 익히기 
 


기초분석 따라 배우기(4 ~ 17장) 

실습 사이트를 이용해서 양질의 데이터(=실제 google merchandise 의 웹로그)를 이용하여 책의 내용을 그대로 따라하며 연습할 수 있다. 각 웹로그의 기초분석을 (세부적인)필요 상황에 따라서 정리해놨고, 어떤 상황에서 이런 분석법이 필요한지에 대한 설명을 읽으면서 진행하면 마케팅이나 기획자가 이런 고민을 하는구나? 같은 생각도 많이 들었던 것 같다. 

4장. 잠재고객 보고서 분석하기

5장. 표 사용 방법 익히기 

6장. 사이트 콘텐츠 보고서 분석하기

7장. 세그먼트 사용 방법 익히기

8장. 사용자 흐름 보고서 분석하기

9장. 이벤트 보고서 분석하기

10장. 이벤트 활용하기

11장. 목표 만들고 분석하기

12장. 유입경로 목표 만들고 분석하기

13장. 가상 페이지뷰 분석하기

14장. 채널분석하기

15장. 접속 검색어 분석하기 

16장. 리퍼러 분석하기

17장. 캠페인 분석하기


Part3. 고급 분석 따라 배우기(18장 ~ 22장)

기초분석 따라 배우기와 유사하게 직접 따라하면서 배울 수 있어서 이론만으로는 잘 와닿지 않는 내용을 더 수월하게 이해할 수 있었다. (아쉽게도 구글 머천다이즈 스토어 계정의 정책 변경으로 몇몇 예제는 따라할 수 없다. 그래도 대다수의 예제를 똑같이 따라할 수 있으니 걱정하지 않아도 될 것같다!) 말그대로 기초분석에서 다뤘던 예제들 보다는 더 복잡한 구성(그에 따라 조건도 세부적이다)의 예제들을 설명하고 있다. 그렇다고 너무 걱정하지는 말자 설명을 보면 충분히 납득할 수 있고, 또한 실습을 할 수 있어서 금방 이해할 수 있다.

18장. 사용자 탐색기 사용하기

19장. 동질 집단 분석하기
특히 이 장의 내용에서 '최적의 이메일 발송 시점'을 찾는 내용이 있는데, 보지도 않는 광고 메일이 한 사이트에서만 하루에 10개씩 오는 경우가 잦던 터라, 이런 기업들에게 구글 애널리틱스좀 써서 적절할 때, 메일좀 보냈으면 하는 생각이 들었다. (그냥 그런 생각이 들었다) 

20장. 측정 프로토콜 살펴보기
이 또한 프로모션 메일을 보내는 것에 그칠 것이 아니라, 이 '측정 프로토콜'의 기능을 이용해서 이메일 마케팅 효율을 측정했다면, 애초당시에 잠정적인 고객(=나와 같은?)에게 귀찮고 불쾌한 감정을 주지 않을 수 있었을텐데..... 이래서 공부가 필요한가보다. 아는 것이 힘이다. 

21장. 알림과 맞춤

22장. 필터와 보기


Part4. 전자상거래 추적하기

구글 애널리틱스에는 '전자상거래 분석 기능'이 있는데 이 기능은 쇼핑몰의 매출 현황은 물론 사용자의 쇼핑 행동과 결제 행동까지 쉽게 분석할 수 있다. 이 장에서는 '기본 전자상거래 추적'과 '향상된 전자상거래 추적'의 사용 방법을 알아본다. 쇼핑몰 서비스를 하는 회사라면 다른 기능도 중요하지만 이 기능을 잘 활용하기만 하더라도 많은 이점을 얻을 수 있을 것이다. 이또한 실습이 가능하므로 너무 걱정하지는 말자!(애초당시에 이론적인 내용도 어렵지는 않다) 아무래도 복잡한 부분들에 대해서는 개발자와의 협업이 불가피한 경우가 많은 것 같다. 

23장. 기본 전자상거래 추적하기

24장. 향상된 전자상거래 추적하기
기본 전자상거래 추적의 한계점(= 구매 완료 시점의 데이터만 수집)을 넘어서 쇼핑의 시작부터 끝까지 모든 시점의 데이터를 수집할 수 있다. 

25장. 전자상거래 추적 보고서 분석하기 

 


Part5. 구글 마케팅 플랫폼 활용하기 

'구글 마케팅 플랫폼'은 구글 애널리틱스, 구글 옵티마이즈, 구글 태크매니저 등 마케팅에 도움되는 여러 도구를 제공한다고 한다. 이 파트에서는 구글 옵티마이즈, 구글 태크매니저를 사용한 예를 보여준다. 

26장. 구글 옵티마이즈 사용하기 
옵티마이즈의 실험 기능에 대한 설명, 예시를 보여준다. 실험을 진행하고 그 결과의 보고서를 확인하고 설정한 목표를 달성하기 위한 여러 시도들을 할 수 있다. (A/B 테스트에 대한 얘기도 나왔었는데 이전에 네이버 모바일 앱의 UI가 새로 개편되면서 어떨때는 이전 UI가 나오고 어떤때는 개편된 UI가 나오고 사용자들로부터 새로운 UI에 대한 테스트를 하고있다라는 얘기를 들은적이 있다. 그 내용을 떠올리면서 읽으니 A/B 테스트에 대한 설명이 더 재밌게 느껴진 것 같다)  

27장. 구글 태크매니저 사용하기 
구글 태크매니저를 사용하면 기획자, 마케터도 개발자의 도움 없이 간단한 데이터 수집을 구현할 수 있다. 이 장에서는 구글 태크 매니저를 사용해 구글 애널리틱스를 작동시키는 방법을 알아본다. 


이렇게 총 576 페이지에 걸쳐서 '구글 애널리틱스'를 이용할 수 있는 다양한 상황과 예시를 들었고 실습환경까지 구성되어있어 (심지어 구글이 제공하는 양질의 데이터를 이용할 수 있는...) 실제 본인이 운영하는 웹 사이트가 없더라도 '구글 애널리틱스'를 제.대.로 활용해볼 수 있었다. '구글 애널리틱스'에 큰 관심이 없었는데, 왜 구글 왜 서비스 업체들이 구글 애널리틱스를 도입해서 마케팅이나 기획을 하는지에 대해서도 알게됐고, 개발자의 입장에서 UI나 UX의 중요성도 마케팅, 기획에 생각이상의 큰 영향을 미친다는 것(어렴풋이만 알고있었다)을 알게되었다. 나중에 마케터나 기획자와 협업을 하게 된다면, 이번 서평에서 정리한 내용이 많은 도움이 될 것이라고 생각한다.  

728x90
개발서적/이펙티브 자바

아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라

728x90

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다. (클래스의 인스턴스를 반환하는 단순한 정적 메서드를 말하는게 맞다!)

다음 코드는 boolean 기본 타입의 박싱 클래스인 Boolean에서 발췌한 간단한 예다.

public static Boolean valueOf(boolean b) {
	return b ? Boolean.TRUE : Boolean.FALSE;
}

 

클래스는 클라이언트에 public 생성자 대신 정적 팩터리 메서드를 제공할 수 있다고 했다. (같이 제공할수도 있다)
이 방식에는 장점, 단점이 모두 존재한다. 먼저 장점 다섯 가지에 대해 알아본다.

장점


1. 이름을 가질 수 있다.

생성자에 넘기는 매개변수와 생성자 자체만으로 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 이름만 잘 지으며 반환될 객체의 특성을 쉽게 묘사할 수 있다.

2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

이 덕분에 불편클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 따라서 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려준다. 플라이웨이트 패턴도 이와 같은 기법이라 할 수 있다.
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다. 이런 클래스를 인스턴스 통제(instance-controlled) 클래스라 한다. 

더보기

인스턴스를 통제하는 이유는 무엇일까? 인스턴스를 통제하면 클래스를 싱글턴으로 만들 수도, 인스턴스화 불가로 만들 수도 있다. 또한 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.

 

3. 반환 타입의 하위 객체를 반환할 수 있는 능력이 있다.

이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 선물한다. API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 유지할 수 있다. 그리고 결과적으로 API가 작아진 것은 물론 개념적인 무게, 즉 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮춰준다.

 

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다. 클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수 도 없고 알 필요도 없다.

 

5. 정적 팩터리 메서드를 작성하는 시점에서 반환할 객체의 클래스가 존재하지 않아도 된다.

이런 유연함은 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다. (ex. JDBC )
서비스 제공 프레임워크에서 제공자는 서비스의 구현체다. 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다

  1. 서비스 인터페이스(service interface) : 구현체의 동작을 정의
  2. 제공자 등록 API (provider registration API) : 제공자가 구현체를 등록할 때 사용함
  3. 서비스 접근 API (service access API) : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
  4. 서비스 제공자 인터페이스(service provider interface) : 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명한다.
    (3개의 핵심 컴포넌트와 더불어 종종 서비스 제공자 인터페이스가 같이 쓰인다)

 

단점


1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

컬렉션 프레임워크의 유틸리티 구현 클래스들을 상속할 수 없다는 이야기다. (어찌 보면 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있다.)

2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다

생성자처럼 API설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.

아래는 정적 팩터리 메서드에 흔히 사용하는 명명 방식들을 소개한다

 

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
  • valueOf : from과 of의 더 자세한 버전
  • instance or getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
  • create or newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
  • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다.
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다.
  • type : getType과 newType의 간결한 버전

 

정리

정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 무작성 public 생성자를 제공하던 습관이 있다면 고치자.

 

728x90
개발서적

Do it! 웹 사이트 따라 만들기

728x90

페이스북 '생활코딩' 채널 커뮤니티 페이지에서 Do it! 웹 사이트 따라 만들기 서평단을 모집한다는 글을 보고(안 그래도 최근 프로젝트를 진행하며 프론트 엔드부분에서 막히는 부분이 많았었다, 물론 벡엔드도..) 바로 지원했다. 운이 좋게도 서평단에 발탁이 되었고, 얼마지나지 않아 책을 받아볼 수 있었다. 

이전에 배운적이 있었지만, 머릿속에 그 지식이  일단 책의 시작 부분에 HTML, CSS, JS에 대한 기본 지식에 대해서 핵심만 간략하게 설명이 있다. 기본지식이 탄탄한 경우에는 과감히 건너뛰어도 될 것 같았다. (헷갈리거나 잊고 있었던 개념 확립에 많은 도움을 받았다) 최근 HTML의 정보로서의 가치가 계속해서 증가하는 추세라고 한다. (자신의 사이트가 더 많이 노출되기위해서 어떤 요소를 신경써야하는지도 간략하게 쓰여있었는데 이론만 배웠을 때에 비해서 더 흥미롭게 봤던 부분이다!) 

meta 태그의 내용은 검색 엔진이 웹 사이트를 파악할 때 기본으로 사용하고 있다고 한다.

CSS에 대한 설명은 다소 헷갈릴 수 있는 요소들을 그림으로 첨부하여서 그 차이점을 확실하게 명시해줘서 그 차이를 '제대로' 인지할 수 있었다. 그리도 용어도 제대로 모르는 것들이 많았는데 각 용어의 명칭과 (비교적 쉬운) 설명을 겸해서 쉽게 읽어나갈 수 있었다. (가상 요소라는 잘 모르는 부분에 대해서도 알 수 있는 기회였다) 또, CSS로 애니메이션을 구현하는 방법에 대해서도 자세히 설명이 되어있다.

자바스크립트와 제이쿼리에 대한 설명 그리고 제이쿼리의 활용적인 면(난 지금까지.... 뭘 쓴 거지?)에서도 사용법이 잘 설명되어있다.

Ajax의 경우 이전부터 공부하겠다고 마음만 먹고 계속 미루고 있었던 부분인데 간략하게 설명되어있다. 

개발도구의 경우 '서브라임 텍스트'라는 무료 편집 프로그램을 추천하고 있다. (그 외에도 웹스톰(WebStorm), 에디트 플러스(EditPlus), 드림위버(Dreamweaver), 인텔리 제이 아이디어(IntelliJ IDEA)) 이전부터 인텔리제이 IDE를 사용하고 있었기에, 서브라임 텍스트를 별도로 다운로드하지 않고, 인텔리제이를 사용했다. 브라우저를 개발할 때는 개발자 도구(F12 , 크롬의 경우 오른쪽 마우스 클릭 -> 페이지 소스보기)의 용도, 크로스 브라우징에 대한 설명과 대처방안 등을 소개한다. 

그리고 현집 웹 퍼블리셔이신 분이 쓰신 책답게(?) '웹 퍼블리셔'가 무엇인지에 대해서도 설명되어있다. 나의 경우에도 웹 퍼블리셔라는 개념을 한 달 전쯤에 알았다. (디자이너 친구가 웹 퍼블리셔를 꿈꾼다는 얘기 덕에 알게 되었다) 간단하게 생각하면 웹 개발자와 웹 디자이너의 중간쯤이라고 생각하면 된다. 하지만 없어서는 안 될 역할이라는 것은 책을 보면 자연스레 알게 될 것이다.

실질적인 1장을 넘어가면 2장부터는 직접 웹 사이트를 처음부터 끝까지 만들어보는 실습을 진행한다. 각 장을 소개하자면

3. 전체 레이아웃 만들기

4. 페이지 이동 효과 만들기

5. 회사 소개 페이지 만들기

6. 도서 소개 페이지 만들기

7. 도서소개 페이지 추가하기

8. FAQ 페이지 만들기

9. Contact Us 페이지 만들기

10. 구글 API로 Contact Us 폼 처리하기 

총 8개의 실습으로 이루어져 있다. 

이전에 웹 페이지를 만들 때, 생각 없이 어떤 기능을 대충 어떻게 넣어야겠다! 식으로 레이아웃을 대충 구성하고 만들다가 나중에 문제가 생겨서 오히려 시간이 더 오래 걸린 경우가 잦았다. 하지만 이 책은 레이아웃을 구성하는 것에 대한 중요성을 강조하고 구조화시켜서 웹 사이트 제작을 더 체계적으로(= 문제가 안 생기도록!!) 만들어가도록 도와준다. 

웹 페이지 제작면에서도 분명 좋은 도서이지만, 13년 차 웹 퍼블리셔의 Know-How가 많이 들어있었고 그런 부분들은 현직자가 아니고서야(감히 예상컨대... 현직자들조차도!!) 알기 힘든 일이고, 웹 퍼블리셔 혹은 프론트 엔드 개발자에게는 꼭 읽는 것을 추천하고 싶었다.(저 같은 벡앤드 개발을 지향하는 개발자에게도 현업상에서 앞에서(=프론트) 어떤 고민과 고충이 있는지 간접적으로나마 체험할 수 있지 않을까(참된 개발자는 개발 능력뿐만 아니라 소통까지도 잘하는 개발자라고 생각하는 1인)하고 생각한다. 적절한 타이밍에 얻게 된 서평단의 기회 그리고 이 책을 볼 수 있게 된 좋은 기회였다고 생각한다. 이지스 퍼블리싱의 Do it! 시리즈는 이것 말고도 'Do it! 자료구조와 함께 배우는 알고리즘 입문(자바 편)'도 공부하고 있는데, 대체적으로 Do it! 시리즈 책들이 잘 만들어진 것 같다. 

728x90