첫 스마트 워치

나의 첫 스마트 워치는 Galaxy Gear3다. 회사에 있던 테스트 기기를 일주간 사용할 수 있었다. 스마트 워치에 대해 IT 뉴스 및 소식 글 등으로 출시 등은 알고 있었다.
그러나 가격이 제법 나가기 때문에 선뜻 구매 욕구는 일어나지 않았었고 사서 이걸로 무엇을 할까 생각이 들었다.

하지만 직접 사용해보니 생각이 180도 바뀌었다. 당시 사용했던 스마트폰은 갤럭시 S10+ 5G로 Gear S3를 사용하기 딱 좋았다.
스마트폰과 연동해두면 자동으로 내 활동이 모니터링 되어 내가 얼마나 움직였는지 (움직이지 않았는지)알 수 있었고, 삼성 조합의 최대 장점인 삼성 페이도 손목으로 사용할 수 있었다.

제법 편리한 기능 덕에 마음이 많이 이끌렸으나 착용감이 상당히 큰 걸림돌로 다가왔다. 바디가 생각 보다 두꺼워 손목을 타이트하게 조이지 않으면 상당히 많이 흔들렸고 스트랩도 그만큼 두꺼웠다.
기종이 오래전에 출시했던 모델인 탓임도 있지만 이렇게 무겁고 거슬리는것을 계속 차고다니기에는 무리가 있을 것 같다고 생각했다. 또한 하루를 겨우 버텨주는 배터리도 구매의욕을 꺽는 원인중 하나였다.
사용했던 기기는 테스트 기기여서 제대로 관리되지 않아 하루는 커녕 반나절도 가지 못했다. 정상적인 배터리 수명에 일반적인 사용 패턴이라면 하루 조금 안되게 버틴다고 한다.

구매 결정

그렇게 시간이 지나 스마트폰을 아이폰 11로 바꾸면서 애플 워치를 구매하게 되었다. 구매 목적은 활동 모니터링이 제일 컸다. 기어를 사용했을때 걷기나 뛰기 모니터링은 큰 감동으로 다가왔었다.
손목에서 지속적으로 울려주니 움직이는데 가벼운 동기부여도 되었고 내가 얼마나 움직였는지 폰에서 확인할 수 있던 것도 좋은 기억이었다.

그래서 주변 기기 생태계를 apple로 바꾸는 김에 구매를 결정할수 있었다. 본인은 애플워치 시리즈6 product red를 구매했다.

구성

패키징은 애플 답게 역시 깔끔하다.


포장에 사용된 소재는 견고한 코팅지인 것 같다. 손으로 집어들었을 때 견고하다는 느낌이 강하게 든다.


겉 포장을 풀면 본체, 각종 설명과 인증서와 스트랩이 위와 같이 별도로 묶여있다.


본체 사진이 인쇄된 상자를 열면 충전기, 각 인증서 설명서, 본체가 놓여있다. 애플 답게 포장도 신경써서 했다는 인상이다.
저 빨간 한지 같은 종이에 본체가 래핑되어있다.


본체와 스트랩을 조립하고, 충전기에 붙이면 자동으로 탁상 시계 모드로 들어간다.

사용기

착용감

본인은 앞서 서두에 언급한 내용을 보면 손목 시계를 즐겨 착용하는 편이 아님을 알수 있을것이다. 책상앞에 오래 앉아있는 생활 패턴도 손목의 자유를 찾게되는 이유중 하나이다.
이 덕에 손목 시계를 찾지 않게 되었고 스마트 워치는 당연히 눈여겨보지도 않았엇다.

애플워치는 이전의 기어와는 완전히 달랐다. 적당히 활동적일 수 있는 무게에 오히려 얇다는 느낌이 드는 두께는 전혀 부담을 주지 않았고 기본 스트랩은 너무 헐겁지도 너무 타이트하지도 않게 적당했다.
긴팔을 입고도 크게 걸리적 거리지 않았고 책상에서 타이핑할때도 불편감은 없었다.

활동시에도 워낙 가볍고 크지 않아 불편하지 않았다. 일반적인 시계정도 혹은 그보다 더 얇다는 느낌을 받았고 평소 시계를 자주 차고다닌다면 느끼지 못할것 같았다.
실제로도 익숙해지니 차고있는 동안에는 불편감은 없었다.

기능

애플 워치의 존재감을 나타내는 기능은 역시 헬스케어라 생각한다. 일상 속에 페이스 메이커와 함께 다니는 기분을 들기에 충분헸다.

심호흡 세션은 주기적으로 스트레스를 낮추게끔 도와주며 1시간 이상 움직이지 않게되면 자리에서 일어나 움직이기를 알려준다.
또한 5분 이상 걷게 되면 걷는 속도와 심박수를 기록해 내가 얼마나 이동했는지를 보여준다.

운동량 기록 또한 탁월했다. 운동 시작 전에 내가 할 운동을 미리 정해두면 심박수와 행동 모니터링을 통해 운동량을 기록해주며
칼로리 계산으로 내가 얼마나 움직였는지를 알려준다.
실제로 홈트레이닝을 강제로 하고있는 요즘엔 개인 PT를 받는 느낌을 받을 수 있었다. 운동 시간, 운동량을 정량적으로 알려준다는 것이 아주 마음에 들었다.

추가적으로 애플 워치를 착용하고있는 동안에는 맥북 잠금 해제를 자동으로 할 수 있다. Touch ID를 사용중이더라도 애플워치를 착용중이라면 알아서 잠금해제되어 빠르게 하던 일을 이어갈 수 있다.

구매 추천

애플워치는 애플 생태계에서만 위력을 발휘한다. 애플 생태계에 있거나 옮길 계획이라면 구매를 추천한다.
과장 조금 보탠다면 개인 비서를 둔것 같은 경험을 얻을 수 있다.

최근 업데이트를 통해 본인이 아이폰을 사용하지 않더라도 가까운 주변인이 사용중이라면 연동할 수 있게 되었다. 조금은 불편 하지만 가족 사용이라면 충분해 보인다.
내가 아이폰을 사용하지 않고 내 주변에 아이폰 사용자가 없다면 구매할 이유가 없다. 연동할 방법이 없기 때문이다.


쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.

Apple 2020년 애플워치 6, GPS, (PRODUCT)RED 알루미늄 케이스, (PRODUCT)RED 스포츠 밴드

오브젝트:코드로 이해하는 객체지향 설계


'리뷰' 카테고리의 다른 글

아이패드 프로 4세대 11인치 리뷰  (0) 2021.01.12
맥북 프로 2018 15인치 터치바 리뷰  (0) 2021.01.03

클래스는 객체지향 프로그래밍을 위한 도구로서 많은 개발자들이 객체지향에 대해 생각하면 떠오르는 키워드일 것이다. 그러나 클래스는 프로그래밍 도구일 뿐 설계를 클래스에 초점을 두어선 안된다.
정말 중요한 것은 객체들이 주고 받는 메시지이며 이 메시지가 객체의 퍼블릭 인터페이스를 결정하고, 클래스를 결정한다.

협력과 메시지

클라이언트 - 서버 모델

협력은 어떤 객체가 다른 객체에게 무언가를 요청할 때 시작된다. [위프스브록(Wirfs-Brock03)]

메시지를 매개로 하는 요청과 응답의 조합이 두 객체 사이의 협력을 결정한다. 두 객체 사이의 협력 관계를 설명하기 위한 전형적인 메타포는 클라이언트 - 서버(Client - Server) 모델 이다.
클라이언트가 서버의 서비스를 요청하는 단방향 상호작용을 협력으로써 활용한다. 객체지향 관점에서 클라이언트는 메시지를 보내는 주체가 되는 객체이며 서버는 메시지를 수신받은 협력 객체이다.

객체는 협력에 참여하는 동안 클라이언트와 서버의 역할을 동시에 수행하는 것이 일반적이다. 객체 외부로 전송하는 메시지의 집합과 외부의 객체로부터 수신하는 메시지의 집합의 두 가지의 메시지 집합으로 구성된다.

메시지와 메시지 전송

메시지 (Message)

  • 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다.
  • 다른 객체에게 도움을 요청하는 것을 메시지 전송(message sending) 또는 메시지 패싱(message passing)이라 한다.
  • 메시지를 전송하는 객체를 메시지 전송자(message sender), 수신하는 객체를 메시지 수신자(message receiver) 라 한다.
  • 클라이언트 - 서버 모델에선 메시지 전송자를 클라이언트, 메시지 수신자를 서버라 할 수 있다.
  • 오퍼레이션 명 (operation name) + 인자 (argument)로 구성.
  • 메시지 전송 = [메시지 수신자] + [오퍼레이션 명] + [인자].

메시지와 메서드

메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입이 무엇이가에 달려있다. 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라 한다.
메시지와 메서드의 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있게 한다. 전송자는 전송할 메시지만 알면 되고 수신자는 받은 메시지를 자율적으로 처리하면 된다.
이렇게 실행 시점에 메시지와 메서드를 바인등하는 메커니즘은 객체간의 결합도를 낮춰 확장 가능한 코드를 작성할 수 있게 한다.

퍼블릭 인터페이스와 오퍼레이션

객체의 내부는 해당 객체만이 알고있다. 외부에선 장막으로 가린 듯이 명확하게 구분되어있다. 이러한 외부와 단절된 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 메인터페이스라 한다.
프로그래밍 언어의 관점에서 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation)이라고 부른다. 런타임에서 메시지 전송자가 메시지를 보내면 메시지 수신자는 실제 타입을 기반으로 적절한 메서드를 찾아 실행한다.
퍼블릭 인터페이스와 메시지의 관점에서 보면 '메서드 호출'보다는 '오퍼레이션 호출'이라는 용어가 더 적절한 표현이다.

시그니처

오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니처 (signature)라고 한다. 오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것이다.

  • 메서드 = 시그니처 + 구현
  • 시그니처 = 오퍼레이션 명 + 인자

인터페이스와 품질

퍼블릭 인터페이스의 품질에 영향을 미치는 4가지 원칙

  • 디미터 법칙
  • 묻지 말고 시켜라
  • 의도를 드러내는 인터페이스
  • 명령 - 쿼리 분리

디미터 법칙(Law of Demeter)

객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하는 원칙이다.

낮선 자에게 말하지 마라 [Larman04]
오직 인접한 이웃하고만 말하라 [Metz12]
오직 하나의 도트만 사용하라 [Metz12]

디미터 법칙을 따르기 위해서는 클래스가 특정한 조건을 만족하는 대상에게만 메시지를 전송하도록 제한해야 한다.

모든 클래스 C와 C에 구현된 모든 메서드 M에 대해

  • M의 인자로 전달된 클래스
  • C의 인스턴스 변수의 클래스

이를 좀더 구체화 해보면 아래와 같이 나열할 수 있다.

  • this 객체
  • 메서드의 매개변수
  • this의 속성
  • this의 속성인 컬렉션의 요소
  • 메서드 내에서 생성된 지역 객체

디미터의 법칙을 따르면 부끄럼타는 코드(shy code) 를 작성할 수 있다. 즉, 불필요한 어떤 것도 외부에 공개하지 않으며 다른 객체의 구현에 의존하지 않는 코드를 작성 할 수 있다.
이로 인해 클라이언트와 서버 사이에 낮은 결합도를 유지할 수 있다.

디미터 법칙을 위반하는 코드의 경우 아래와 같은 모습일 것이다.

a.getB().getC().doSometing();

각 메서드의 결과 반환 타입이 A -> B -> C로 변화하는 것을 알 수 있다. 이 로직을 사용하는 부분은 A, B, C 모두에게 결합되어 유연하지 못한 코드가 된다.

묻지말고 시켜라

훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원하는 것을 시키는 것이다. 메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안된다.
모든 구현되는 로직은 메시지 수자가 담당해야할 책임인 것이다.

묻지 말고 시켜라 원칙을 따르도록 메시지를 결정하면 자연스럽게 정보 전문가에게 책임을 할당하게 되고 높은 응집도를 가진 클래스를 얻을 확률이 높아진다.
객체 내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재한다면 해당 객체가 책임져야하는 어떤 행동이 객체 외부로 누수된 것이다.

의도를 드러내는 인터페이스

메서드의 이름을 짓는 방법에는 두 가지가 있다.

  • 메서드의 내부 구현 방법을 드러내는 이름 (어떻게 동작하는가?)
  • 메서드가 짊어진 책임이 무엇인지를 드러내는 이름 (무엇을 하는가?)

첫 번째의 경우는 나쁘지는 않지만 다형성과 같은 기법을 사용하기 어려워진다. 같은 역할의 다른 구현인 객체는 모두 다른 메서드 이름을 갖게 될 것이기 때문이다. 더 큰 문제는 메서드 수준에서 이미 캡슐화를 위반한다는 것이다.
이런 이름의 메서드들은 협력하는 객체의 종류를 알도록 강요한다.
두 번째의 경우를 따르게 되면 프로그래머는 메서드가 하는 일에 대한 충분한 정보를 보여주며 역할과 책임에 기반한 코드를 작성할 수 있다.
이 원칙에서 중요한 것은 구현과 관련된 모든 정보를 캡슐화하고 객체의 퍼블릭 인터페이스에는 협력과 관련된 의도만을 표현한다는 것이다.

원칙의 함정

디미터 원칙과 묻지말고 시켜라 스타일은 객체지향에서 좋은 원칙임에는 분명하다. 그러나 그 어떤 원칙도 절대적인 것은 아니다.
설계는 트레이드오프의 결과물이라는 사실을 잊지 말아야 한다. 적용하려는 원칙이 충돌하는 상황에서도 원칙에 너무 얽메여 억지로 끼워맞추는 상황이 발생하고 결과적으로 설계는 일관성을 잃고 코드는 무질서 속에 파묻히게 된다.

디미터 법칙은 하나의 도트(.)를 강제하는 규칙이 아니다.

앞서 말한 디미터 법칙에서 단 하나의 도트를 사용하라[Metz12]고 했었다. 이를 보고 대부분의 사람들이 IntStream이 디비터 법칙을 위반했다고 생각할 것이다.
그러나 IntStream의 메서드는 다른 어떤 것을 반환하지 않고 또다른 IntStream의 인스턴스를 반환한다. A 가 B 가 되는 것이 아닌 A 가 A'이 되는 것일 뿐이다.
디미터 법칙은 결합도와 관련된 것이며 결합도가 문제가 되는 것은 객체의 내부 구조가 외부로 노출되는 경우로 한정된다.

결합도와 응집도의 충돌

묻지말고 시켜라와 디미터 법칙을 준수하는 것이 항상 긍정적인 결과로만 귀결되는 것은 아니다. 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 된다. 결과적으로 객체는 필요없는 책임을 지게 되어 응집도가 낮아진다.

가끔씩은 객체의 상태를 묻는것 외에 다른 방법이 없는 경우도 있다. 컬렉션에 포함된 객체들을 처리하는 유일한 방법은 객체에게 물어보는 것 뿐이다. 객체가 정말로 데이터인 경우에는 묻지 않고서는 처리항 방법이 없다.
객체에게 시키는 것이 항상 가능한 것은 아니다. 늘 "경우에 따라 다르다"라는 사실을 명심해야한다.

명령 쿼리 분리 원칙

명령 - 쿼리 분리(Command - Operation Separation) 원칙은 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공한다.

  • 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴(routine) 이라 한다.
  • 루틴은 프로시저와 함수로 구분할 수 있다.
  • 프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
  • 함수는 값을 반환할 수 있지만 부수 효과를 발생시킬 수 없다.

명령(Command)쿼리(Query) 는 객체의 인터페이스 측면에서 프로지서와 함수를 부르는 또 다른 이름이다.

  • 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
  • 객체의 정보를 변환하는 쿼리는 상태를 변경할 수 없다.

어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안된다. 명령-쿼리 분리 원칙을 한 문장으로 표현하면 "질문이 답변을 수정해서는 안된다." 는 것이다.

명령-쿼리 분리와 참조 투명성

참조 투명성 (referential transparency) 이란 어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성을 의미한다.

f(1) + f(1) = 6
f(1) * 2 = 6
f(1) - 1 = 2

3 + 3 = 6
3 * 2 = 6
3 - 1 = 2

참조 투명성을 만족하는 식은 두 가지 장점을 제공한다.

  • 모든 함수를 이미 알고 잇는 하나의 결과값으로 대체할 수 있기 때문에 쉽게 계산할 수 있다.
  • 모든 곳에서 함수의 결과값이 동일하기 때문에 식의 순서를 변경하더라도 각 식의 결과는 달라지지 않는다.

책임에 초점을 맞춰라

앞서 소개된 원칙들의 장점을 정리하면 다음과 같다.

  • 디미터 법칙
    협력이라는 컨텍스트 안에서 객체보다 메시지를 먼저 결정하면 두 객체 사이의 구조적인 결합도를 낮출 수 있다. 수신할 객체를 알지 못한 상태에서 메시지를 먼저 선택하기 때문에 객체의 내부 구조에 대해 고민할 필요가 없어진다.
    따라서 메시지가 객체를 선택하게 함으로써 의도적으로 디미터 법칙을 위반할 위험을 최소화할 수 있다.
  • 묻지 말고 시켜라
    메시지 를 먼저 선택하면 묻지 말고 시켜라 스타일에 따라 협력을 구조화하게 된다. 클라이언트의 관점에서 메시지를 선택하기 때문에 필요한 정보를 물을 필요 없이 원하는 것을 표현한 메시지를 전송하면 된다.
  • 의도를 드러내는 인터페이스
    메시지를 먼저 선택한다는 것은 메시지를 전송하는 클라이언트의 관점에서 메시지의 이름을 정한다는 것이다. 당연히 그 이름에는 클라이언트가 무엇을 원하는지, 그 의도가 분명하게 드러날 수 밖에 없다.
  • 명령-쿼리 분리 원칙
    메시지를 먼저 선택한다는 것은 협력이라는 문맥 안에서 객체의 인터페이스에 관해 고민한다는 것을 의마한다. 객체가 단순히 어떤 일을 해야하는지 뿐만 아니라 협력 속에서 객체의 상태를 예측하고 이해하기 쉽게 만들기 위한 방법에 관해 고민하게 된다.
    따라서 예측 가능한 협력을 만들기 위해 명령과 쿼리를 분리하게 될 것이다.

훌륭한 메시지를 얻기 위해서는 책임 주도 설계 원칙을 따라야 한다. 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하는 것이기 때문에 적합한 메시지를 결정할 수 있을 확률이 높아진다.


쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.

오브젝트:코드로 이해하는 객체지향 설계


책임에 초점을 맞춰 설계할 때 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기 쉽지 않다는 것이다.
모든 책임 활동은 트레이드오프 활동이며, 여러 방법중 최선의 할당 방법을 선택하는 것이다.

책임 주도 설계를 향해

책임 중심 설계를 위한 2가지 원칙

  • 데이터보다 행동을 먼저 결정하라
  • 협력이라는 문맥 안에서 책임을 결정하라.

위의 두 원칙의 핵심은 객체를 데이터가 아닌 협력을 중심으로 생각하는 것이다.

데이터보다 행동을 먼저 결정하라.

데이터는 객체가 책임을 수행하는 데 필요한 재료를 제공할 뿐이다. 책임 중심 설계는 "이 객체가 수행헤야할 책임은 무엇인가?"를 먼저 결정한 후에
"이 책임을 수행하는데 필요한 데이터는 무엇인가"를 결정한다. 책임을 먼저 결정한 후에 객체의 상태를 결정하는 것이다.

협력이라는 문맥 안에서 책임을 결정하라.

책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다. 협력은 메시지를 전송하는 객체로부터 시작되기 때문에 적합한 책임이란 메시지 전송자에게 적합한 책임을 의미한다.
즉, 메시지를 전송하는 클라이언트에게 적합한 책임을 할당해야 한다.

객체를 결정한 후에 메시지를 선택하는것이 아닌, 메시지를 결정한 후에 적합한 객체를 선택해야 한다. 메시지가 존재하기 때문에 객체가 존재하는 것이다.

객체를 갖고있기 때문에 메시지를 보내는것이 아니다. 메시지를 전송하기 때문에 객체를 갖게 된 것이다.[Metz12].

문맥 안에서 메시지에 집중하는 책임은 더 깔끔하고 수월하게 캡슐화의 원칙을 지켜낼 수 있다. 반면 데이터 중심의 설계는 내부의 상태를 밖에서 알아야하기 때문에 캡슐화가 약화된다.

책임 주도 설계

핵심은 책임을 결정한 후에 책임을 수행할 객체를 결정하는 것이다. 협력에 참여하는 객체들의 책임이 어느정도 정리될 때까지는 객체 내부의 상태에는 관심을 가지지 않는다.

책임 할당을 위한 GRASP 패턴

GRASP 패턴이란?

  • General Responsibility Assignment Software Pattern [Craig Larman]
  • 일반적 책임 할당을 위한 소프트웨어 패턴
  • 객체에게 책임을 할당할 때 지침으로 사용할 수 있는 원칙들
  • 총 9가지 원칙
    • Information Expert (정보 전문가)
    • Creator (창조자)
    • Controller (조율자)
    • Low coupling (낮은 결합도)
    • Polymorphism (다형성)
    • Pure fabrication (순수 제작?)
    • Indirection (간접 참조)
    • Protected variation (변경 보호)

도메인 개념에서 출발하기

어떤 책임을 할당할 때 가장 먼저 고려해야할 것은 도메인이다. 도메인 안에는 무수히 많은 개념이 존재하며 도메인의 개념들을 책임 할당의 대상을 생각하면 좀더 수월하다.
그러나 도메인 개념을 정리하는데 오랜 시간을 할애해서는 안된다. 완벽한 도메인 모델이란 존재하지 않고, 정말 중요한 것은 설계를 시작하는 것이기 때문이다.

정보 전문가에게 책임을 할당하라

앞서 2장에서 본 내용이다. 어떤 책임을 수행하기에 가장 적합한 객체는 그 책임에 대해 가장 잘 알고 가장 잘 할수 있는 객체이다.
가장 처음 객체에게 책임을 할당하기 위한 질문은 어떤 메시지가 필요한지에 대한 질문이다.

메시지를 전송할 객체는 무엇을 원하는가?

이는 해결해야할 문제를 파악하는 질문이기도 하다. 해결해야할 문제가 무엇이고 해결하기 위해서 필요한 메시지는 무엇인지를 찾는다.

메시지를 결정한 다음으로 생각해야할 질문을 이 메시지를 처리할 객체는 무엇인가에 대한 질문이다.

메시지를 수신할 적합한 객체는 누구인가?

이 질문에 답하기 위해서는 객체가 상태화 행동을 통합한 캡슐화의 단위라는 사실에 집중해야 한다. 객체의 책임과 책임을 수행하는데 필요한 상태는 동일한 객체 내에 존재해야 한다.
따라서 객체에게 책임을 할당하는 첫 번째 원칙은 정보 전문가에게 책임을 할당하는 것이다.

정보 전문가 패턴(Information Expert pattern)은 객체가 자신이 소유하고 있는 정보와 관련된 작업을 수행한다는 일반적인 직관을 표현한 것이다.
여기서 한가지 중요한 것은 정보 !== 데이터 라는 사실이다. 객체가 정보를 필요로 한다 해서 꼭 저장 하고있을 필요는 없다.

높은 응집도와 낮은 결합도

높은 응집도와 낮은 결합도는 객체에 책임을 할당할 때에 항상 고려해야 하는 기본 원리이다. 객체가 자신의 밖의 내용을 많이 알 수록 결합도는 높아지며 내부에만 집중할 수록 응집도는 높아진다.

위 그림은 높은 결합도를 간략히 그림으로 나타낸 것이다. D 객체에서 C 객체가 참조하고있는 내용에 변경이 생기면 C 객체도 변경해야 하며 이 변경은 다시 C 객체의 내부에 의존하고있는 다른 객체에게도 전파된다.

위 그림은 각 객체가 높은 응집도를 보일 경우를 그림으로 표현한 것이다. 각 객체의 관심은 내부에 집중되어있기 때문에 각 객체의 변경은 외부를 오염시키지 않고 객체 내부에서만 유지된다.

낮은 결합도와 높은 응집도는 개발 이후의 유지 보수성을 높여준다. 객체가 자기 자신에게 집중할 수록 변경은 외부로 흘러나가지 않고, 외부의 객체는 변경되지 않는 퍼블릭 인터페이스를 참조한다.
Low Coupling(낮은 결합도)와 High Cohesion(높은 응집도)는 설계를 진행하면서 책임과 협력의 품질을 검토하는데 사용할 수 있는 중요한 평가 기준이다.

창조자에게 객체 생성 책임을 할당하라.

Creator 패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맞기는 것이다.
이 둘은 이미 강한 결합을 갖고있기 때문에 이 객체에 생성 책임을 할당하는 설계의 전체적인 결합도에 영향을 미치지 않는다. 이미 존재하는 객체 사이의 관계를 사용하기 때문에 전체 설계는 낮는 결합도를 유지할 수 있다.

다형성을 통해 분리하기

객체의 타입에 따라 행동이 달라진다면, 우선 객체를 타입으로 분리하고 행동을 각 타입의 책임으로 할당하는 방법을 GRASP에서는 Polymorphism이라 한다.
로직의 분기를 if

else, switch

case 로만 분리한다면, 이후 유지보수시 수정을 어렵게하고 변경에 취약하게 만든다. 이때 다형성을 사용해 분리하게되면 확장성을 챙길 수 있게된다.

변경으로부터 보호하기

Protected Variation(변경 보호)는 쉽게 말해 변경할 이유에 따라 분리하고, 캡슐화 하는 것을 말한다. 하나의 객체는 하나의 변경이유로만 변경해야 한다.
하나의 변경을 위해 여러 객체를 변경해야 하는 것은 프로그램의 유연성을 떨어뜨리고 확장성을 떨어뜨린다.

이를 위해서는 변경 예상지점을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당해야 한다. 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화 하는 것은 변화의 전파를 최소화 한다.

변경과 유연성

개발자들이 변경에 대비할 수 있는 방법은 두 가지가 있다.

  1. 코드를 이해하고 수정하기 쉽도록 단순하게 작성
  2. 코드를 수정하지 않고도 변경을 수용할 수 있도록 유연하게 설계

대부분의 경우 1.이 좋은 방법이지만, 코드가 빈번하게 반복될 경우 복잡성을 감수하더라도 2.를 하는것이 더 좋은 방법이다.

책임 주도 설계의 대안

책임 주도 설계를 적용하는데 어려움이 있다면, 다음의 대안을 활용할 수 있다.
우선 목표로 하는 기능을 우선 구현한 후, 해당 기능을 유지한 채 리팩터링 하는 것이다.

메서드 응집도

복잡한 기능을 구현하다보면 하나의 메서드가 프로세스와 같이 굉장히 길어지고 파악하기 어려워지는 경우가 생긴다. 이런 메서드를 몬스터 메서드라고 부른다. [Michael Feathers]
대부분의 이런 메서드들은 응집도가 낮아 변경의 이유가 하나 이상이며 메서드 흐름을 파악하기 위한 주석이 필요하다.

응집도 높은 메서드는 변경의 이유가 단 하나여야 한다. 메서드의 흐름을 한 눈에 볼 수 있어야 하며 메서드의 흐름을 읽는 것 자체가 일련의 주석을 읽는 것과 같이 간결해야 한다.

객체를 자율적으로 만들자

메서드의 응집도를 높여 분리했다면 객체의 응집도를 높일 차례다. 응집도 높은 메서드 중 객체의 책임에 맞지 않는 메서드를 올바른 책임을 진 객체로 옮기는 것이다.
리팩터링 결과는 올바름 책임을 갖고, 하나의 관심사에 집중되어있는 설계를 얻게 된다. 높은 캡슐화, 높은 응집도, 낮은 결합도의 설계가 구현되는 것이다.


쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.

오브젝트:코드로 이해하는 객체지향 설계


객체지향 패러다임의 관점에서 핵심은 역할(role), 책임(responsibility), 협력(collaboration) 이다.
객체지향 설계의 핵심은 협력을 구성하기 위해 적쩔한 객체를 찾고 적절한 책임을 할당하는 과정에서 드러난다.

역할

  • 객체들이 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할을 구성.
    책임
  • 객체가 협력에 참여하기 위해 수행하는 로직.
    협력
  • 애플리케이션의 기능을 구현하기위해 수행하는 상호작용.

협력

객체들은 혼자서 존재할 수 없다. 다른 객체와의 협력을 통해 기능을 구성한다. 다른 객체와의 협력할 수 있는 유일한 수단은 메시지 전송(message sending) 뿐이다.
객체는 다른 객체의 상세한 내부 구현에 접근할 수 없기 때문에 오직 메시지 전송을 통해 자신의 요청을 전달한다.
메시지를 수신한 객체는 메시지에 응답하기 위해 알아서 실행할 메서드를 골라 수행한다.

협력은 객체를 설계하는데 필요한 일종의 문맥(context) 를 제공한다.

객체가 참여한ㄴ 협력이 객체를 구성하는 행동과 상태를 모두 결정한다.

책임

협력에 참여하기위해 객체가 수행하는 행동을 책임이라 한다. 책임은 객체에 의해 정의되는 응집도 있는 행위의 집합이다.
크게 하는 것(doing)아는 것(knowing) 의 두가지로 나누어 세분화 할 수 있다 [크레이그 라만]

하는 것

  • 객체를 생성하거나 계산을 수행 하는 등의 스스로 하는 것.
  • 다른 객체의 행동을 시작시키는 것.
  • 다른 객체의 활동을 제어하고 조절하는 것.

아는 것

  • 사적인 정보에 관해 아는 것.
  • 관련된 객체에 관해 아는 것.
  • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것.

일반적으로 책임과 메시지의 크기는 다르다. 책임은 객체가 수행해야 할 행동을 종합적이고 더 간략하게 서술하기 때문에 더 추상적이고 개념적으로도 더 크다.
책임의 관점에서 아는 것과 하는 것이 밀접하게 연관되어있다. 어떤 책임을 수행하기 위해서는 그 책임을 수행하는데 필요한 정보도 함께 알아야 하기 때문이다.

책임 할당

INFORMATION EXPERT PATTERN (정보 전문가 패턴)

책임을 수행하는 데 필요한 정보를 가장 잘 알고 있는 전문가에게 그 책임을 할당하는 방법이다. 이는 일상상활에서 어떤 문제를 해결하기 위해 그 분야의 전문가에게 도움을 구하는 것과 같다.
그 전문가 객체는 자율적으로 자신이 가장 잘 알고있는 정보와 방법을 이용해 문제를 해결하고 메시지에 응답한다.

책임 주도 설계 (RDD - Responsibility driven design)

어떤 책임을 선택하느냐가 전체적인 설계의 방향과 흐름을 결정한다. 먼저 책임을 먼저 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방식이다.

  • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
  • 시스템 책임을 더 작은 책임으로 분할한다.
  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.

책임을 할당할 때에 고려해야 하는 두가지

  • 메시지가 객체를 결정한다.
  • 행동이 상태를 결정한다.

메시지가 객체를 결정한다.

메시지가 객체를 선택헤야하는 두 가지 중요한 이유.

  • 객체가 최소한의 인터페이스를 가질 수 있게 된다.
    필요한 메시지가 식별될 때까지 퍼블릭 인터페이스에 어떤 것도 추가하지 않기 때문에 필요한 크기의 인터페이스를 갖게 된다.

  • 객체는 충분히 추상적인 인터페이스를 가질 수 있게 된다.
    메시지는 외부의 객체가 요청하는 무언가를 의미하기 때문에 메시지를 먼저 식별하면 무엇을 수행할지에 초점을 맞추는 인터페이스를 얻을 수 있다.

협력을 구성하는 객체들의 인터페이스는 충분히 추상적인 동시에 최소한의 크기를 유지할 수 있다.

행동이 상태를 결정한다.

객체의 행동은 협력에 참여할 수 있는 유일한 방법이다. 객체가 협력에 적합한지를 결정하는 것은 객체의 상태가 아닌 행동이다.

Data-Driven Design (데이터 주도 설계)

객체의 내부 구현에 초점을 맞춘 설계 방법으로 책임 주도 설계와는 반대로 객체 내부의 상태를 우선 생각하고 인터페이스를 구성하는 방법이다.
DDD의 문제는 내부 구현이 퍼블릭 인터페이스로 노출된다는 것에 있다. 객체 내부의 상태를 컨트롤하기 위해 getter, setter가 퍼블릭 인터페이스로 노출이되어 결국 캡슐화를 저해한다.

캡슐화를 위반하지 않도록 구현에 대한 결정을 뒤로 미루면서 객체의 행위를 고려하기 위해서는 항상 협력이라는 문맥 안에서 객체를 생성해야 한다.
상태는 단지 객체가 행동을 정상적으로 수행하기 위해 필요한 재료일 뿐이다.

역할

역할과 협력

객체가 어떤 특정한 협력 안에서 수행해야할 책임의 집합을 역할이라 한다. 협력을 모델링 할때는 특정한 객체가 아니라 역할에게 책임을 할당한다 생각하는게 좋다.

유연하고 재사용 가능한 협력

역할이 중요한 이유는 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있기 때문이다. 객체가 아닌 책임에 초점을 맞추면 만들어야할 객체의 추상화된 개념을 알 수 있다.
이를 통해 설계단계에서 효과적으로 중복을 제거할 수 있다. 역할은 동일한 역할을 하게 될 다른 객체를 포괄하는 추상화라는 점을 주목해야한다.

객체 대 역할

역할은 객체가 참여할 수 있는 일종의 슬롯이다. 협력에 오직 한 가지의 객체만 참여한다면 역할을 고려해야 할까?

협력에 참여하는 후보가 여러 종류의 객체에 의해 수행 될 필요가 있다면 그 후보는 역할이 되지만, 단지 한 종류의 객체만이 협력에 참여할 필요가 있다면 후보는 객체가 된다.
-- 레베카 워프스브록 --

즉 협력에 적합한 대상이 한 종류라면 간단히 개게로 간주하고, 대상이 여러 종류라면 역할로 간주하면 된다.

협력은 역할들의 상호작용으로 구성되고, 협력을 구성하기 위해 역할에 적합한 객체가 선택되며, 객체는 클래스를 이용해 구현되고 생성된다. -- 트리그비 린스카우
협력(Collaboration) -- referene --> 역할(Role) -- select from --> 객체(Object) -- instance of --> 클래스(Class)

역할과 추상화

추상화는 세부 사항에 억눌리지 않고도 상위 수준의 정책을 쉽고 간단하게 표현할 수 있다. 불필요한 세부 사항을 생략하고 핵심적인 개념을 강조할 수 있다.
협력이라는 관점에서는 세부적인 사항을 무시하고 추상화에 집중하는 것이 유리하다.

또한 추상화는 설계를 유연하게 만들수 있다. 역할을 다양한 종류의 객체가 놓일 수 있는 슬롯이라 생각할 수 있다. 컴파일 타임과 런타임의 의존성을 다르게 할 수 있다.
협력안에서 동일한 책임의 다른 객체는 서로 대체될수 있다. 역할은 다양한 환경에서 다양한 객체들을 수용할 수 있게 해주므로 협력을 유연하게 만든다.


쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.

오브젝트:코드로 이해하는 객체지향 설계


맥북 다음으로 나의 마음을 사로잡은, 나를 애플 생태계로 몰아넣은 기기를 리뷰하려한다.

iPad Pro 4세대 이전에 이미 Galaxy tab s7을 사용하고있었다. 미디어 소비용, 업무시 설계및 노트용을 잘 사용하고 있었으나, 맥과의 연동은 서드파티앱을 사용했어야 했고,
그 마저도 매끄럽지 못했다. 펜이 포함되어있어서 태블릿 자체만으로는 완성도가 높았으나, 맥북을 사용하는 입장에서는 걷돌고있다는 생각이 떠나질 않았다.
심지어는 이어폰 마저 에어팟 1세대를 사용하고 있어서 페이링이 자주 끊어지는 현상도 겪고 있었다. 그런 와중에 s7 플러스 모델의 출시가 확정되었고,
중고값이 더 떨어지기 전에 처분하고 갈아타게 되었다.구매는 쿠팡에서 로켓으로 구매했다. 당시 다음 세대 ipad 에어의 폼팩터 전환이 기정 사실화 되어있던 시기여서 기다릴까 생각했었지만, 이미 태블릿을 처분한 상태였기에 바로 iPad Pro 4세대와 Apple Pencil2 구매했다.

1. 디자인

애플 다운 마감을 보여준다. 만듦새는 보자마자 좋다는 것이 바로 느껴지고, 출시 당시에 논란이 있었던 밴드 게이트도 보이지 않았다.

정면에는 강화유리 필름을 붙여놓은 상태이다. 펜슬을 쓰면서 화면을 보호하기 위해 강화유리를 구매했다. 종이 질감은 평이 좋지 않아서 선택하지 않았다.

상단에는 다른 것은 없이 전원 버튼만 위치해있다. 스피커는 상 하단에 4개의 스테레오 구성으로 위치해있다.

하단에는 마찬가지로 스피커가 위치해있고 USB Type-C 포트가 위치해있다. Type-C 포트 덕에 확장성과 호환성에 무리가 없었다.

좌측에는 별다른 버튼은 없고 마이크만 하나 위치해있다.

우측에는 볼륨 버튼과 마그네틱 커넥터가 위치해 있다. 이 마그네틱 커넥터는 예상할 수 있듯이 펜슬을 붙여 충전하는 곳이다.

뒷면은 애플 다운 디자인으로, 군더더기 없이 깔끔하다. 그리고 고성능의 카메라와 플래시, 라이다(LiDAR)센서가 위치해있다. 하단에는 매직 키보드를 위한 스마트 커넥터가 위치해있다.

박스 구성은 본체, C-to-C Cable, 28W 충전기가 포함되어있다. 충전기는 사무실에 갖다논 상태라 사진에 포함되지 않았다.

추가로 구매한 Apple Pencil 2세대. 1세대는 호환되지 않는다. 충전할 수 없고, 꽂을 수 없기 때문에 페어링이 어렵다.

위와 같이 펜슬을 붙여 충전한다.

전반적으로 높은 수준의 마감을 보여준다. 애플 답게 케이스와 필름 없이 사용하고 싶은 완성도와 견도함이었다. 실제로 집에서, 침대 위에서는 맘편히 케이스 없이 사용는데 상당히 만족스럽다.

2. 성능

A12Z Bionic 프로세서는 어떤 작업에서도 버벅임을 찾아 볼 수 없었다. 주로 사용하는 앱은 자체 앱인 memo 와 Autodesk의 Sketch인데, 그 둘을 번갈아 가며 사용함에 그 어떠한 지연도 볼 수 없었다.
카메라 또한 12MP의 광각, 10MP의 초 광각 카메라 2개로 여타 태블릿에서 찾아 볼 수 없는 높은 수준의 사진을 찍을 수 있었다.

전면 디스플레이는 12.9인치의 아주 시원한 크기로, 해상도 또한 264ppi로 아주 선명하다. 또한 애플의 유명한 True Tone 디스플레이가 적용되어 자연스러운 색감을 보여준다.
스펙상에는 반사방지 코팅이 되어있으나, 펜슬을 사용한다면 필름 사용을 추천한다. 코팅은 언젠간 닳아 없어질 것이기 때문이다.
화면 주사율은 120Hz를 지원하기는 하나 가변형으로 필요한 시점에 자동으로 활성화 된다. 해당 옵션을 활성화 한 후 홈화면에서 몇번 넘겨보기만 해도 체감된다.

배터리는 사용 경험 상 대기 시간으로 펜슬을 붙여놓은 상태로 1주일 조금 안되는 것 같다. 연속 사용시 WIFI 환경, 발기 50%에서 하루 조금 안되게 버텨주는것 같다.
편차가 커보이는데 이는 가변형 120Hz 주사율 때문인 것으로 보인다.

아이패드 프로의 진가는 펜슬과 함께 할때 나타난다. 120hz와 A12Z bionic 덕에 정말 종이에 펜으로 적는 것 같은 경험을 할 수 있다.
펜으로 긋는 모든 획이 바로 반응을 보이며, 내장된 뉴럴 엔진의 보정 덕에 자연스러운 필기를 경험할 수 있다.

3. 구매 추천

이미 애플 생태계애 있고, 아이패드가 없다면 강력히 추천한다. 폰의 화면이 아주 커져 컨텐츠 소비에 무리가 없다 하지만, 실내에서의 태블릿 사용을 따라잡을 순 없을 것이다.
맥의 사이드카 기능으로 무선 확장 모니터로도 활용할 수 있으며, 펜슬이 있다면 무선 액정 태블릿으로 활용할 수 있다. 개발자인 필자는 simulator 를 sidecar로 ipad에 띄워 펜슬로 컨트롤하거나
터미널 화면을 넘겨 사용해 편리했다.

여유가 된다면 학생분들도 펜슬과 함께 구매하기를 추천한다. 대학생 필기에 아주을 것으로 생각된다. 자동으로 백업되는 자체 앱으로 필기 할 수 있고, 유명한 노트 앱을 활용해 체계적인 노트를 만들 수도 있다.
단순히 컨텐츠 소비용이라면, 추천하지 않는다. 가격이 가격인 만큼 생산성을 챙겨야 돈값을 한다 느낄 수 있다.

4. 총평

기존에 태블릿 사용에 익숙했던 필자는 흔히 가성비 태블릿들을 전전했었다. 업무용으로 쓰다 필요없으면 처분하면서 여러 태블릿을 전전했으나 이제 ipad로 정착하게 되었다.
작업 사이에 작은 지연이 생각보다 업무의 흐름을 방해하는 경우가 많았었는데 ipad에서는 전혀 찾아볼 수 없었다.


쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.

Apple 2020년 iPad Pro 12.9 4세대, Wi-Fi, 128GB, Space Gray
Apple 정품 애플펜슬 2세대 MU8F2KH/A, 단일 색상, 1개


'리뷰' 카테고리의 다른 글

애플 워치 시리즈 6 리뷰  (0) 2021.01.30
맥북 프로 2018 15인치 터치바 리뷰  (0) 2021.01.03

요구사항 확인

모든 설계가 그러하듯 요구사항을 확인하는 것이 우선이다. 요구사항 분석을 통해 필요한 객체를 정할 수 있고, 각 객체의 책임을 어떻게 분배할지 방향을 잡을 수 있다.

협력, 객체, 클래스

보통 프로그래밍을 어떻게 하는지 생각해보자. 객체지향으로 프로그래밍을 한다고 했을때 가장 먼저 하는것이 무엇인가? 대부분 어떤 class 가 필요할 지 고민한다.
클래스를 먼저 고민하고, 각 클래스는 어떤 속성과 메서드를 가지고 있어야 할 지를 생각한다.

객체지향 프로그래밍을 위해서는 우선 클래스가 아닌 객체 에 초점을 맞춰야 한다.

  1. 어떤 클래스가 필요한 지가 아닌, 어떤 객체가 필요한지를 생각한다.
    클래스는 공통적인 상태와 행동을 공유하는 객체를 추상화 한것이다. 추상화를 하기 위한 개념을 먼저 정리해야한다.

  2. 객체를 독립적인 존재가 아니라 기능을 구현하기위해 협력하는 공동체의 구성원으로 봐야한다.
    객체는 혼자서 할 수 있는 일은 거의 없다. 다른 객체와 상호작용하며 서로 협력하는 존재이며 살아 움직이는 생명체로서 봐야한다.

도메인의 구조를 따르는 프로그램 구조

도메인?

도메인이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야라 할수 있다. 영화 예매 시스템을 예로 들면, 영화를 좀 더 쉽고 빠르게 예매하려는 사용자의 문제를 해결 하는 것이다.
여기서 도메인은 영화 예매라 할 수 있다. 도메인은 이처럼 문제를 다루는 범위로 볼 수 있으며 더 작은 단위로도 생각해 볼 수 있다.

자바와 같은 객체지향 언어를 사용하면 이런 도메인이 class로 이어진다는 것을 쉽게 볼 수 있다. 클래스의 이름은 도메인을 쉽게 유추할 수 있는 이름으로 해야 하고 클래스 간의 관계 또한 도메인의 관계가 반영되어야 한다.

클래스 구현

도메인의 정의 다음으로 해야할 일은 도메인을 반영하는 클래스를 구현하는 것이다. 이때, 클래스 외부와 내부의 경계를 구분지어 명확한 경계를 세워야한다. 클래스를 구현하거나 다른 개발자에 의해 개발된 클래슬ㄹ 사용할 때 가장 중요한 것은 클래스의 경계를 구분짓는 것이다.
이 경계가 명확할 수록 객체의 자율성이 보장된다 (chapter 01 참고). 또한 프로그래머에게 구현의 자유를 제공하기 때문에 경계를 명확히 해야 한다.

자율적인 객체

객체에 대해 명심해야할 두 가지 사실이 있다.

  1. 객체는 상태 (state)와 행동 (behavior)ㅇㅇ 함께 가지는 복합적인 존재이다.
  2. 객체는 스스로 판단하고 행동하는 자율적인 존재이다.

객체 지향 개념은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로서 문제 영역의 아이디어를 적절하게 표현할 수 있게한다 (캡슐화).
객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서이다. 객체 외부의 그 무엇도 객체 내부의 상태에 직접 간섭해서는 안되며 객체가 어떤 생각을 하는지도 알아선 안된다.

객체의 캡슐화와 접근제어는 2가지로 나뉜다.

  1. public interface (퍼블릭 인터페이스)
    외부에서 접근 가능한 부분이며 메시지 교환을 위해 외부에 노출하는 부분이다.

  2. implementation (구현)
    외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분이다.

이는 인터페이스와 구현의 분리 원칙으로 훌륭한 객체지향 프로그램의 원칙 중 하나이다. 일반적으로 객체의 상탠ㄴ 숨기고 행동만 외부에 공개해야 한다. 속성은 private로 감추고 메서드는 public으로 공개하는 식이다.

프로그래머의 자유

프로그래머의 역할을 클래스 작성자와 클라이언트 프로그래머로 구분하는 것이 유용하다 [Eckel06].

클래스 작성자는 새로운 데이터 타입을 추가하고, 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.
클래스 작성자는 클라이언트 프로그래머가 접근하면 안되는 곳을 접근 제한을 통해 구현 은닉(implementation hiding)을 통해 제한한다. 이를 통해 클래스 작성자는 클라이언트 프로그래머에게 미칠 영향으로부터 자유롭게 내부 구현을 변경할 수 있다.
이 구현 은닉은 양쪽에게 이로운 개념이다. 클라이언트 프그래머는 내부 구현은 무시한 채 공개 인터페이스만 알고 있으면 된다. 클래스 작성자는 외부에 미칠 영향을 걱정하지 않고 변경을 할 수 있다.

협력하는 객체들의 공동체

앞서 말했듯 객체들은 서로 협력하며 존재한다. 객체는 다른 객체의 인터페잇에 공개된 행동을 수행하도록 요청할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답한다.
객체가 받은 요청을 처리하기 위한 방법을 메서드라고 한다.

추상화 - 상속과 다형성

컴파일 시간 의존성과 실행시간 의존성

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다. 유연하고 쉽게 재사용 할 수 있으며 확장 가능한 설계가 된다.
그러나 이렇게 유연해 질 수록 복잡도는 높아진다. 실행시점과 컴파일 시점의 의존성이 달라지면 코드를 이해하기 어려워진다. (Trade off :: 유연성, 재사용성 vs 유지보수)

차이에 의한 프로그래밍

상속은 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 부모의 코드를 그대로 자식에게 물려줄 수 있으며 자식은 부모와 다른 부분만을 구현하면 쉽게 확장할 수 있다. 이런 방식을 차이에 의한 프로그래밍이라 한다.

상속이 가치있는 이유는 부모의 변수를 재활용하기 때문이라기 보다는 부모가 가진 모든 인터페이스를 자식 클래스가 물려 받을 수 있기 때문이다. 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의하는 것임을 생각하면 알수 있다.
실제로 객체간의 관계에서 중요한 것은 어떤 인스턴스인지가 아니라 어떤 메시지를 수신할 수 있느냐가 중요한 것이다.

런타임에서 자식은 부모의 모든 메시지를 수신 할 수 있기에 부모 클래스와 같은 타입으로 취급하기도 한다. 이를 업캐스팅이라 하는데 자식 클래스가 부모의 타입으로 자동 형변환이 되는 것을 말한다.

다형성

메서드와 메시지는 다른 개념임을 상기하자. 객체가 메시지를 수신하면 어떤 메서드를 실행할 지는 그 객체와 연결된 클래스가 무엇인가에 따라 달라진다.
다시 말해 동일한 메시지에 대해 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라 부른다.

학부 과정을 이수했다면 들어봤을 동적바인딩, 정적바인딩이 여기서 사용된다. 다형성을 구현하는 방식은 다양하지만 메시지와 메서드를 실행 시점에 연결한다는 것은 동일하다.
이때 컴파일 타임에 연결하게되면 정적 바인딩, 실행 시점에 연결하는 것을 동적 바인딩이라 한다.

인터페이스와 다형성

자바 프로그래머에게 친숙한 인터페이스는 이러한 다형성을 위해 제공된 프로그래밍 요소이다. 클래스의 구현은 필요없고 순수하게 인터페이스 만을 공유하고 싶을때 사용한다.
기본적으로는 Abstract Base Class로 인터페이스를 정의할 수 있다.

추상화와 유연성

추상화의 힘

추상화는 크게 2가지의 장점을 갖고있다.

  1. 추상화 계층만 보면 요구 사항에 대해 높은 수준에서 서술할 수 있다.
    세부적인 정책을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다. 이는 다른 요소를 배제하고 상위 개념만으로 도메인의 중요 개념을 설명할 수 있게 해준다.

  2. 추상화를 이용하면 설계가 유연해진다.
    상위 정책을 추상화를 통해 표현하면 구조를 수정핮지 않고 새로운 기능을 쉽게 추가하고 확장할 수 있다.

유연한 설계

추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다. 세부 정책을 구현한 클래스가 추상화된 상위 정책을 상속받고 있다면 어떤 클래스와도 협력 가능하다.

추상 클래스와 인터페이스 트레이드 오프

구현과 관련된 모든 것들은 트레이드 오프의 대상이 될 수 있다. 코드의 작성에 있어 이유가 없는 코드는 없어야 한다. 사소한 결정이라도 심사숙고한 끝에 나온 코드와 그렇지 않은 코드는 품질에서 큰 차이가 있다.

코드 재사용

상속은 일반적으로 코드를 재사용하는데 널리 쓰이는 방법이다. 그러나 널리 사용된다고 해서 베스트는 아니다. 합성(composition)도 들어 보았을 것이다. 사용하고자 하는 코드를 담고있는 인스턴스를 변수로 포함해 사용하는 방식이다.
많은 컨텍스트나 프레임워크들이 합성을 선호하는 모습을 보인다., Spring framework애서도 대부분의 의존성을 합성 관계로 사용하는 것을 볼 수 있다.

상속

왜 상속 대신 합성을 선호할까? 상속의 문제점은 캡슐화를 위반한다는 것과 설계를 유현하지 않게 한다는 것이다.
가장 큰 문제는 캡슐화를 위반한다는 것이다. 상속을 하기 위한 기본 전제는 부모 클래스를 잘 알고있어야 한다는 것이다. 부모 내부의 다른 메서드가 내부의 어떤 추상 메서드를 호출하고 있다는 것을 알고 이를 구현해야 한다.
결과적으로 자식에게 부모의 구현이 고스란히 노출되기 때문에 캡슐화가 약화된다. 부모가 변경된 경우 자식도 변경될 확률을 높인다.

설계가 유연하지 않게 되는 것은 부모와 자식의 관계가 강하게 결합되기 때문이다. 부모와 자식의 관계는 컴파일 시점에 결정되기 때문에 실행 시점에 객체의 종류를 변경할 수 없게된다.

합성

앞서 말했듯 합성은 인스턴스를 내부에 포함하는 방법으로, 객체의 인스턴스를 통해서만 코드를 재사용할 수 있다. 합성은 상속이 가지는 두가지 문제점을 모두 해결한다.
인터페이스에 정의된 메시지만을 사용하기 때문에 구현을 효과적으로 캡슐화할 수 있다. 또한 의존하는 클래스를 교체하기 쉽기 때문에 설계를 유연하게 만든다.
따라서 코드 재사용을 위해서는 상속 보다는 합성을 선호하는 것이 더 좋은 방법이다.

그러나 합성만을 사용하는 것은 좋은 방법이 아니다. 대부분의 설계는 상속과 합성을 함께 사용해야 한다. 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수밖에 없다.


쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.

[[위키북스] 오브젝트))](https://coupa.ng/bPjrmA)


객체

응집도와 캡슐화 (Encapsulation)

객체 내부의 상태를 캡슐화하고 객체 간에는 오직 메시지를 통해서만 상호작용하도록 만드는 것을 캡슐화(Encapsulation)이라 한다.
자신 이외의 객체는 내부의 상태, 구현등을 알 수 없어야 한다.

어느 한 객체가 다른 객체의 내부를 속속들이 알 수 있을 때, 결함도가 높다 할 수 있다. 결합도가 높을 수록 변경이 발생할 때 그 범위가 커진다.
알고있는 것이 많을 수록 건드려야 할 부분이 많아진다.

반대로 객체가 적절히 캡슐화되어 자신이 해야할 일만 하게 되어 자율적인 객체가 될 수록 응집도가 높아지고, 결합도가 낮아진다 할 수 있다.
결합도가 낮을 수록, 응집도가 높을 수록 변경에 유연해진다. 유지 보수에 필요한 수고가 줄어들게 된다.

객체는 자신의 데이터를 스스로 처리하는 자율적인 존재여야한다.외부의 간섭을 ㅊ최대한 배체하고 메시지를 통해서만 협력하는 자율적인 객체들의 공동체를 만드는 것이 객체지향 설계이다.

절차지향과 객체지향

절차적 프로그래밍?

하나의 기능에 여러 다른 데이터를 알고, 사용하는 방식의 프로그래밍을 절차적이라 할 수 있다.
프로세스와 데이터를 각기 다른 모듈로 위치하고, 프로세스에서 데이터를 다루는 방식을 절차적 프로그래밍이라 할 수 있다.
이 경우, 어느 한 곳에서 변경이 발생하면, 해당 프로세스에 참여한 다른 구성원에도 변경이 필요할 수 있다. 즉, 변경에 유연하지 않다.

이 경우, 절차를 말로 서술해보면 직관에 맞지 않게 된다.
저자는 이에 소극장을 예로 들며 설명한다.

소극장은 관람객의 가방을 열어 그 안에 초대장이 들어있는지 살펴본다. 가방 안에 초대장이 들어 있으면 판매원은 매표소에 보관 돼있는 티켓을 관람객의 가방 안으로 옮긴다.
가방 안에 초대장이 들어있지 않다면 관람객의 가방에서 티켓 금액만큼의 현금을 꺼내 매표소에 적립한 후에 매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다.

절차지향 프로그래밍은 우리의 일반적인 관념과 일치하지 않는 모습을 보인다. 이는 데이터에 해당하는 구성원들은 모두 수동적인 존재로 취금되었기 때문이다.
프로세스가 모든 데이터에 접근해 변화를 일으키고, 모든 변화를 관리하기 때문에 직관과 다른 작업 흐름이 발생한 것이다.
또한 위 예로도 결합도가 아주 높다는 것을 알 수 있을 것이다. 소극장은 관람객, 가방, 티켓, 판매원 모두에게 의존하고있다.

객체지향 프로그래밍?

위의 절차적 프로그래밍의 단점을 해결하기 위해선, 각 구성원이 자신의 데이터를 스스로 처리하도록 프로세스의 적절한 단계를 각 구성원의 내부로 옮기는 것이다.
위 서술에서 첫 부분을 바꿔보면 아래와 같을 것이다.

소극장은 관람객의 가방을 열어 그 안에 초대장이 들어있는지 살펴본다. -> 판매원은 관람객에게 초대장이 있는지 확인을 요청한다. 관람객은 자신의 가방을 열어 초대장이 있는지 확인해 판매원에게 알려준다.

위 변화는 우리가 일반적으로 생각할 수 있는 구성원의 행동이다. 객체지향 프로그래밍을 생각할 때 이렇게 우리가 생각할 수있는 행동을 서술해보면 알기 쉽게 어디가 문제인지 알수 있다.
만약 관람객이 초대장을 확인하기위해 지갑을 확인해야 한다 가정하면 절차적 프로그래밍의 예에서는 관람객은 물론 소극장에도 변화가 필요하게 된다.

책임의 이동

절차적 프로그래밍과 객체지향 프로그래밍의 근본적인 차이를 만드는 것은 "책임의 이동" 이다. 여기서 책임은 기능이라 할수 있다.
위 절차적 프로그래밍의 예시는 소극장에게 책임이 집중되어있다. 반면 객체지향 프로그래밍의 예시 서술은 한소절 뿐이지만 이미 소극장은 보이지 않고, 같은 일을 위한 단계가 적절히 나눠져있음을 알수 있다.

객체 지향에서는 독재자는 존재하지 않는다. 구성원 모두에게 적절한 책임이 있고, 각 구성원은 각자의 책임을 벗어나는 행위를 하지 않는다.

설계가 왜 필요한가?

책에서는 설계를 다음과같이 인용하고있다

설계란 코드를 배치하는 것이다 [Metz12]

한창 학과 공부를 할때는 설계가 굉장히 귀찮고, 힘든 일이며 구현하는 것보다 더 어려운 것으로 여겼다.
그러나 실무를 해보며 느낀 바로는 전혀 그렇지 않았다. 설계와 구현은 함께 가야하는 것이며, 코드를 작성하는 매 순간이 설계이며 설계는 코드 없이는 검증할 수 없다.

좋은 설계란 무엇인가? 오늘 완성해야 하는 기능을 구현하는 코드를 작성하는 동시에 내일 쉽게 변경할 수 있는 코드를 작성할수있는 설계이다.
변경을 수용할수 없는 설계는 이후에 비싼 대가를 치루며 더 어려운 코드를 만들거나, 새로만들게 된다. 최소한의 수정으로 원하는 변경을 충족해야한다.

객체지향 설계

객체지향 설계는 우리가 세상을 보는 관념대로 코드를 작성할 수 있게 돕는다. 객체는 서로를 의존하며 메시지를 주고받으며 기능을 구성한다.
훌륭한 객체지향 설계는 객체 사이의 의존성을 적절하게 관리하는 설계이다. 객체간의 의존성은 프로그램의 변경을 어렵게하는 주범이다.


쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.

[[위키북스] 오브젝트)](https://coupa.ng/bPjrmA)


AutoMonitor 중간 정리

  1. 개요

    • Spring Boot 스터디를 위한 프로젝트로 AutoMonitor를 개발.

    • Mockito를 Springboot 환경에서 TDD방법론으로 접근해 사용해보는 것이 학습 목표.

    • 궁극적으로, CI/CD Tool (e.g. jenkins) 를 활용하여 자동화 하는 것이 개발 목표.

      AutoMonitor란?

    • Api Testing Application

    • 주어진 API가 주어진 payload로 정상 작동하는가를 확인

    • API Health checking

      Stack

    • Kotlin

    • Springboot

    • JPA

    • Junit

    • Mockito

    • Hibernate

    • JPA

    • Vue

      학습 전략

    • TDD (Test Driven Development)

    • 레이어 별로 Mock-up

    • 항상 Test Case를 먼저 작성할 것

      환경 설정에 대한 설명은 생략.

  2. Test Driven Development

    TDD란?

    테스트 주도 개발(Test-driven development TDD)은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다.
    개발자는 먼저 요구사항을 검증하는 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 테스트 케이스를 통과하기 위한 최소한의 코드를 생성한다.
    마지막으로 작성한 코드를 표준에 맞도록 리팩토링한다.
    이 기법을 개발했거나 '재발견' 한 것으로 인정되는 Kent Beck은 2003년에 TDD가 단순한 설계를 장려하고 자신감을 불어넣어준다고 말하였다.
    -- Wiki에서 발췌 링크

    주목할 것은 '먼저 요구사항을 검증하는 자동화된 테스트 케이스를 작성한다.' 이다.
    테스트 케이스 자체가 기능 명세가 되고, 프로그램의 청사진이 된다.

    1. JPA Repository Mocking

      테스트의 목적은 작성한 Entity를 사용한 JpaRepository를 Mock-up 해보기 위함이다.

       /* Test Case */
       @RunWith(MockitoJUnitRunner::class) // Mockito runner
       class ApiSpecJpaRepositoryTest {
           @Mock
           lateinit var apiSpecJpaRepository: ApiSpecJpaRepository; // jpaRepository를 Mockito로 Mock-up
      
           @InjectMocks
           lateinit var apiSpecRepository: ApiSpecRepository; // jpaRepository를 이용한 Respository. apiSpecJpaRepository를 의존 객체로 갖는다.
      
           @BeforeAll
           fun setUp() { // Mockito의 어노테이션을 실행.
               MockitoAnnotations.initMocks(this);
           }
       }
       /* JPA Repository */
       @Repository
       interface ApiSpecJpaRepository: JpaRepository<ApiSpec, Long>
       /* Repository */
       @Repository
       class ApiSpecRepository(
               val jpaApiSpecRepository: ApiSpecJpaRepository
       )

      MockitoRunner가 의존성을 주입해주며 Mock-up을 만들어 준다. 위 테스트 클래스에서는 apiSpecJpaRepository가 Mock-up 객체가 되며 이를 apiSpecRepository가 사용한다.

      등록된 API를 조회하는 기능을 구현하고자 한다. 이 때, Respository에 해당 기능을 바로 구현하지 않고, 인터페이스만 놓아둔다.

       /* Repository */
       @Repository
       class ApiSpecRepository(
               val jpaApiSpecRepository: ApiSpecJpaRepository
       ) {
           /**
             * 등록된 api 목록 가져오기
             */
           fun getApis(page: Int): List<ApiSpec> {
               return listOf();
           }
       }

      그리고 바로 테스트 케이스를 작성한다.

       /* Test Case */
       class ApiSpecJpaRepositoryTest {
           ...
      
           private fun mockResultList(size: Long): List<ApiSpec> {
               return LongStream
                       .range(0, size)
                       .mapToObj {
                           val mocked = mock<ApiSpec>(); // Mock-up
                           // Data mocking
                           whenever(mocked.id).thenReturn(it + 1);
                           whenever(mocked.alias).thenReturn("Test Api - ${it + 1}");
                           whenever(mocked.url).thenReturn("/api/v1/test/${it + 1}");
                           whenever(mocked.payload).thenReturn("{ \"data\": { \"number\": ${it + 1} } }");
                           mocked;
                       }
                       .toList();
           }
      
           /**
           * 첫 페이지에 해당하는 API 목록을 불러온다.
           */
           @Test
           fun getApis_페이지_번호로_조회하기() {
               // when
               val page: Int = 0;
               val recordSize: Int = 12;
               val paging = PageRequest.of(page, recordSize);
      
               // mocking
               val mockedResults = mockResultList(12)
               val mockedPage = PageImpl<ApiSpec>(mockedResults)
               whenever(apiSpecJpaRepository.findAll(paging))
                       .thenReturn(mockedPage);
      
               // then
               assertEquals(12, apiSpecRepository.getApis(page).size);
           }
       }

      여기서 주목할 것은 mockResultList와 테스트 케이스의 mocking방법이다.

      mockResultList는 integer를 인자로 받아 그 값을 크기로 갖는 ApiSpec의 Mock-up 객체의 List를 반환한다.

      데이터들은 실제 값을 직접 바인드 하지 않고, Mockito의 whenever로 Mock-up 데이터를 생성한다.

      테스트 케이스에서는 apiSpecJpaRepository.findAll 의 결과를 mock-up 한다.

      apiSpecJpaRepository의 findAll 함수에 PageRequest 인자를 넘겨주어 실행하면 12개의 원소를 가진 List객체가 반환된다는 의미이다.

      물론 데이터는 모두 Mock-up된 상태이다.

      테스트를 수행하면 당연히 통과하지 못한다. apiSpecRepository에서 테스트하려는 getApis가 의미없는 반환을 하고있기 때문이다.

      이제 이 테스트 케이스를 통과하기 위해 ApiSpecRepository의 getApis함수를 마저 구현한다.

       @Repository
       class ApiSpecRepository(
               val jpaApiSpecRepository: ApiSpecJpaRepository
       ) {
           val recordSize: Int = 12;
           /**
            * 등록된 api 목록 가져오기
            */
           fun getApis(page: Int): List<ApiSpec> {
               val paging = PageRequest.of(page, recordSize);
               val result: Page<ApiSpec>? = jpaApiSpecRepository.findAll(paging);
      
               if (result != null) {
                   return result.content;
               }
      
               return listOf();
           }
       }

      PageRequest를 이용해 쿼리 결과를 제한하며, 반환할 내용은 모두 List로 변환한다.

      이제 테스트를 다시 수행하면 통과할 것이다. 테스트가 통과할 수 있는 이유는 바로 apiSpecJpaRepository가 Mock-up 되었기 때문이다.

      Mockito Test Runner가 @Mock으로 지정된 필드를 Mock-up해두고, Mock-up된 apiSpecJpaRepository를 @InjectMocks로 지정된 apiSpecRepository에 의존성 주입을 실행한다. 따라서 apiSpecRepository 내부에는 Mock-up된 jpaRepository가 위치하게 되며 내부에서 apiSpecJpaRepository를 통해 메서드를 호출할 수 있게 되었다.

      또한, 내부의 함수를 whenever로 Mock-up했기 때문에 실제 Database에 쿼리를 질의하지 않고 기능이 동작 할 수 있었다.

      이런 방법으로 테스트 케이스를 먼저 작성하고, 테스트를 수행하고, 실패한 테스트에 대해 리팩터링을 진행하는 것이 TDD의 기본이다.

    2. Repository Test

      Repository Test에서는 데이터의 저장, 조회가 내가 의도한 대로 이뤄지는지를 확인해야 하기 때문에 별도의 Mock-up을 만들지 않는다.
      간단하게 Save-Load Test를 수행한다.

       @RunWith(SpringRunner::class)
       @SpringBootTest
       @TestInstance(TestInstance.Lifecycle.PER_CLASS)
       class ApiSpecRepositoryTest {
      
           @Autowired
           lateinit var apiSpecRepository: ApiSpecRepository
      
           @BeforeAll
           fun setUp() {
               for (i: Int in 1..60) {
                   val data = ApiForm(
                           url = "http://localhost/api/v1/test/$i",
                           alias = "TestApi - $i",
                           payload = "{ data: [] }"
                   );
      
                   apiSpecRepository.saveApi(data);
               }
           }
      
           @AfterAll
           fun cleanUp() {
               apiSpecRepository
                       .jpaApiSpecRepository
                       .deleteAll();
           }
       }

      우선 setup과 cleanUp을 구성한다. 테스트 시작 전에 데이터를 미리 구성해 두고, 이후엔 테스트에 사용한 모든 데이터를 모두 제거한다.

       @RunWith(SpringRunner::class)
       @SpringBootTest
       @TestInstance(TestInstance.Lifecycle.PER_CLASS)
       class ApiSpecRepositoryTest {
           ...
           @Test
           fun `saveApi - 새로운 API 등록`() {
               // mocking
               val inputData = ApiForm(
                       url = "http://test/api/v1/mocked",
                       alias = "test",
                       payload = "{ \"data\" : \"test\" }"
               );
      
               val inserted = apiSpecRepository.saveApi(inputData);
      
               val result = apiSpecRepository.jpaApiSpecRepository.findById(inserted.id!!).get();
      
               Assertions.assertEquals(inputData.url, result.url);
               Assertions.assertEquals(inputData.alias, result.alias);
               Assertions.assertEquals(inputData.payload, result.payload);
           }
       }

      Save 테스트에서는 데이터를 넣어보고, 이를 다시 꺼내어 확인하는 방식으로 테스트를 진행했다.

       @RunWith(SpringRunner::class)
       @SpringBootTest
       @TestInstance(TestInstance.Lifecycle.PER_CLASS)
       class ApiSpecRepositoryTest {
           ...
           @Test
           fun `getApi - 단건 조회`() {
               // when
               val id : Long = 1;
      
               // then
               val result = apiSpecRepository.getApi(id);
               Assertions.assertNotNull(result);
               Assertions.assertEquals(result!!.id!!, id);
           }
       }

      Load Test에선 기존 준비한 데이터를 꺼내보는 방향으로 진행했다.

    3. Controller Test
      Controller Test는 앞서서 만든 Repository를 Mock-Up해서 진행한다. Controller Test의 목적은 매핑된 함수들이 제대로 동작 하는지를 보기 위함이지 데이터를 꺼내오는 것이 제대로 동작하는 것을 보는 것이 아니기 때문이다. 관심사를 생각해서 테스트 케이스와 Mock-Up 범위를 결정한다.

       @RunWith(MockitoJUnitRunner::class)
       @SpringBootTest
       @AutoConfigureMockMvc
       @TestInstance(TestInstance.Lifecycle.PER_CLASS)
       class DashboardControllerTest {
           @Mock
           lateinit var apiSpecRepository: ApiSpecRepository;
      
           @InjectMocks
           lateinit var dashboardController: DashboardController;
      
           @Before
           fun setUp() {
               MockitoAnnotations.initMocks(this);
           }
      
           private fun mockResultList(size: Long): List<ApiSpec> {
               return LongStream
                       .range(0, size)
                       .mapToObj {
                           ApiSpec(
                                   id = it + 1,
                                   alias = "Test Api - ${it + 1}",
                                   url = "/api/v1/test/${it + 1}",
                                   payload = "{ \"data\": { \"number\": ${it + 1} } }"
                           );
                       }
                       .toList();
           }
       }

      우선 테스트 준비를 위와 같이 해둔다. 실제 Request를 주고 받는 테스트를 할 것이기 때문에 MockMVC를 사용한다. 앞서서 언급한 대로 apiSpecRepository는 Mock-UP해 두고 테스트 케이스 마다 예상 값을 정한다. 테스트 데이터는 엔티티를 사용하였다. JpaRepository테스트와 다른 점은 데이터 자체를 다시 Mockito로 Mock-Up하지 않은 것이다. Object De/Serialize에서 Mock-up객체의 다른 메서드, 프로퍼티들이 함께 결과에 포함 될 수 있기 때문에 데이터를 직접 구성하게 했다.

       @RunWith(MockitoJUnitRunner::class)
       @SpringBootTest
       @AutoConfigureMockMvc
       @TestInstance(TestInstance.Lifecycle.PER_CLASS)
       class DashboardControllerTest {
           ...
           @Test
           fun `getRegisteredApis - API Calling 테스트 - 데이터 조회`() {
               val mockedList = mockResultList(12)
               whenever(apiSpecRepository.getApis(1))
                       .thenReturn(mockedList);
      
               val request = get("/api/v1/apis/1")
                       .accept(MediaType.APPLICATION_JSON);
      
               MockMvcBuilders
                       .standaloneSetup(dashboardController)
                       .build()
                       .perform(request)
                       .andExpect(status().isOk)
                       .andDo(ResultHandler {
                           println(it.response.contentAsString);
                       })
                       .andExpect(jsonPath("$").isArray)
                       .andExpect(jsonPath("$.*", Matchers.hasSize<ApiSpec>(12)))
      
           }
       }

      조회 테스트이다. Mock-Up 데이터를 12개 (1 page 분량)을 준비해두고 페이지 번호를 주어 요청하는 테스트 시나리오이다.
      이제 테스트를 수행하면 실패할 것이다. 컨트롤러에 가서 해당 기능을 구현한다.

       @RestController
       class DashboardController(
               val apiSpecRepository: ApiSpecRepository
       ) {
           @GetMapping("/api/v1/apis/{page}")
           fun getRegisteredApis(@PathVariable("page") page: Int): List<ApiSpec> = apiSpecRepository.getApis(page)
       }

      Get요청 테스트를 작성했으니 GetMapping을 구성한다. Restful 디자인을 사용했다.
      다시 테스트를 수행하면 통과한다.

    4. 반복...

      이제 기능을 개발할 때 마다 위 흐름을 반복하면된다.

      1. Test Case 작성
        • JpaRepository
          • 어떤 메서드를 사용해 데이터를 가져올 지 정한다.
          • Repository에 해당 메서드를 사용하는 기능의 껍데기를 만든다.
          • Repository에 JpaRepository의 Mock-Up을 의존성으로 주입한다.
        • Repository
          • mock-up없이, 실제 데이터의 CRUD가 이뤄지는지를 확인하는 테스트 케이스를 작성한다.
        • Controller
          • Repository로 꺼내온 데이터를 어떻게 핸들링 할 지에 대해 테스트 케이스를 작성한다.
          • 실제 Request와 유사하게 테스트해야 하므로 MockMVC를 사용하는 것을 권장한다.
          • Repository의 Mock-Up을 Controller의 의존성으로 주입한다.
      2. Test 수행
      3. Refactoring
  3. 앞으로...

    1. Frontend와 Api연동.
      • 현재까지 작성된 Api들을 Frontend와 연동해 기능의 동작을 확인한다.
    2. CI/CD툴과 연동.
      • Jenkins로 배치 프로세스를 구성해 테스트를 주기적으로, 자동화 한다.

쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다.


'스터디 > Spring Boot' 카테고리의 다른 글

SpringApplication (2)  (0) 2021.01.08
SpringApplication (1)  (0) 2021.01.07

+ Recent posts