공부하는 블로그

#2 - 고객이 원하는 기능을 하게 하라 본문

design patterns

#2 - 고객이 원하는 기능을 하게 하라

devtimothy 2019. 3. 16. 13:44

고객이 원하는 기능을 하게 하라

본 블로깅은 Head first OOAD: 세상을 설계하는 객체지향 방법론 (한빛미디어) 책을 Typescript 문법으로 전환하며 공부하는 글입니다.

글을 읽기 전에, 광고 배너 한번씩만 클릭 부탁드립니다. 블로그 운영에 큰 보탬이 됩니다 :)

책에서 말하는 바는 우선 '돌아가게끔 만들라'는 것이다. 앞서 보았지만 찾는 기타가 있음에도 불구하고, 프로그램은 기타를 찾아주지 못하고 있다.

또한 fender로 검색하면 Fender로 등록된 것들은 찾아주지를 못한다. 여러가지 생각을 해 볼수 있을 것 같다. toLowerCase() 를 호출해서 해결하는 방법, enum 타입을 사용하는 방법 등...

그러나 하나 유의할 점은, 미리 앞서서 고민할 필요는 없다는 것이다. 고객님들은 언제든지 우리를 골탕먹일 엿먹일 준비를 하고 계시다는 점이다. 와꾸가 안 나와 있을때는 생각하지 못했던 부분이 와꾸가 생기고 나면 또 다른 아이디어가 떠올라서 요구할 수 있을테니 말이다.

그래서 설계에 대한 걱정은 나중에 해도 좋을 것 같다. (특히나 나의 경우에는 스타트업에서 일하면서 급격하게 변화하는 상황에 맞춰나가다보니 구조보다는 서비스 개발에만 집중하게 되어 구조적인 부분을 고민하지 못한 것이 많이 아쉬운 부분 중 하나이다.)

문자열 비교 없애기

책에서는 enum을 이용해 문자열 비교를 없애고자 한다. 소스코드는 아래와 같다.

// Builder.ts
export enum Builder {
 FENDER = "Fender",
 MARTIN = "Martin",
 GIBSON = "Gibson",
 COLLINGS = "Collings",
 OLSON = "Olson",
 RYAN = "Ryan",
 PRS = "PRS"
}

// Type.ts
export enum Type {
 ACOUSTIC = "acoustic",
 ELECTRIC = "electric"
}

// Wood.ts
export enum Wood {
 INDIAN_ROSEWOOD = "Indian Rosewood",
 BRAZILIAN_ROSEWOOD = "Brazilian Rosewood",
 MAHOGANY = "Mahogany",
 MAPLE = "Maple",
 COCOBOLO = "Cocobolo",
 CEDAR = "Cedar",
 ADIRONDACK = "Adirondack",
 ALDER = "Alder",
 SITKA = "Sitka"
}

// FindGuitarTester.ts
import { Inventory } from "./Inventory";
import { Guitar } from "./Guitar";
import { Builder } from "./types/Builder";
import { Type } from "./types/Type";
import { Wood } from "./types/Wood";
const FindGuitarTester = class {
 public main(): void {
   const inventory: Inventory = new Inventory();
   this.initInventory(inventory);

   const whatErinLikes: Guitar = new Guitar(
     "",
     0,
     Builder.FENDER,
     "Stratocastor",
     Type.ELECTRIC,
     Wood.ALDER,
     Wood.ALDER
  );
   const guitar: Guitar = inventory.search(whatErinLikes);
   if (guitar) {
     const { builder, model, type, backWood, topWood, price } = guitar;
     console.log(`Erin, you might like this ${builder} ${model} ${type} guitar:
         ${backWood} back and sides,
         ${topWood} top.
         You can have it for only ${price}!`);
  } else {
     console.log("Sorry, Erin, we have noting for you.");
  }
}
 private initInventory(inventory: Inventory) {
   inventory.addGuitar(
     "V95693",
     1499.95,
     Builder.FENDER,
     "Stratocastor",
     Type.ELECTRIC,
     Wood.ALDER,
     Wood.ALDER
  );
   inventory.addGuitar(
     "a",
     1000,
     Builder.COLLINGS,
     "c",
     Type.ELECTRIC,
     Wood.ALDER,
     Wood.BRAZILIAN_ROSEWOOD
  );
   inventory.addGuitar(
     "a1",
     1000,
     Builder.FENDER,
     "c",
     Type.ACOUSTIC,
     Wood.ALDER,
     Wood.BRAZILIAN_ROSEWOOD
  );
   inventory.addGuitar(
     "a2",
     1000,
     Builder.GIBSON,
     "c",
     Type.ACOUSTIC,
     Wood.ALDER,
     Wood.BRAZILIAN_ROSEWOOD
  );
   inventory.addGuitar(
     "a3",
     1000,
     Builder.MARTIN,
     "c",
     Type.ACOUSTIC,
     Wood.ALDER,
     Wood.BRAZILIAN_ROSEWOOD
  );
   inventory.addGuitar(
     "a4",
     1000,
     Builder.OLSON,
     "c",
     Type.ELECTRIC,
     Wood.ALDER,
     Wood.BRAZILIAN_ROSEWOOD
  );
}
};

new FindGuitarTester().main();

검색 조건이 enum으로 수정됨에 따라서, 코드가 타입 안정성, 값 안정성을 갖게 되어 더 튼튼해진다. 이는 우리들에게는 유지보수의 필요성을 줄여준다.

연필을 깎으며.

나는 회사에서 프론트엔드와 백엔드를 왔다갔다하면서 개발을 하고 있다. 프론트엔드 작업을 했던 것중 대표적인 것은 음식점 대기관리 앱파트너 페이지 인데, 특히 파트너 페이지에서 알림톡 관리를 들 수 있겠다.

처음엔 음식점 앞에 대기장부를 앱으로 만들면 되지, 하는 마음으로 시작했는데 고객들의 요구사항이 점점 다양해졌다. (꼭 이름을 넣어야 해요?, 메뉴를 받고 싶어요, 고객들 호출할 때 알림톡을 보내고 싶어요, 번호표를 발급해줄 수 없나요?, 이름도, 전화번호도 필요없어요! 그냥 목록만 나오게 해주세요! 등등…)

예측불허의 다양한 요구사항들이 나오면서 때로는 기존 시스템을 뒤집어야 하는 상황들도 경험할 수 있었다.

며칠 전에 동료 개발자분께서 공유해주신 글인데, 한번 퍼날라본다.

해커뉴스에 "지금까지 작업해본 가장 거지같은 코드가 무엇인가"라는 글타래가 있는데 여기에 전직 오라클 개발자가 쓴 글이 화제가 된 적이 있습니다. 너무나 대단해서 한번 번역해 보았습니다.

--
오라클 데이터베이스 12.2
2500만줄 가량의 C 코드로 이루어져 있습니다.

정말이지 상상할 수 없을 만큼 끔찍합니다! 한 줄의 코드를 고치면 수천 개의 테스트가 깨져요. 여러 세월동안 수많은 프로그래머들이 빡빡한 데드라인에 맞춰서 온갖 개똥같은 코드로 채워놨어요.

엄청 복잡한 로직, 메모리 관리, 컨텍스트 스위칭 등등이 수천개의 플래그로 엮여 있습니다. 코드 전체가 손으로 일일히 하나씩 하나씩 펼쳐보지 않고서는 파악이 불가능한 매크로로 뒤덮여 있어요. 매크로 한개가 실제로 무슨 역할을 하는지 이해하기 위해서는 하루에서 이틀까지 걸릴 수 있습니다.

가끔씩 코드가 서로 다른 상황에서 어떻게 동작하는지 이해하려면 20개의 서로 다른 플래그와 관련된 값들을 이해해야 해요. 가끔씩은 수백개까지 가기도 해요! 절대로 과장하는게 아닙니다.

이 제품이 아직까지 살아남아서 굴러가는 유일한 이유는 말 그대로 수백만 개의 테스트 때문이에요!

오라클 데이터베이스 개발자의 하루는 대략 이렇습니다.

- 새로 발견된 버그를 수정하기 시작합니다.

- 20개의 서로 다른 플래그들이 어떻게 미스테리하게 버그를 유발하는지 조사하느라 2주를 보냅니다.

- 특수한 시나리오에 대응하기 위해 하나의 플래그를 더 만듭니다. 문제가 되는 상황을 비켜나가서 버그를 피하도록 플래그를 검사하는 코드를 몇줄 더 짭니다.

- 수정사항을 테스트 팜에 보냅니다. 테스트 팜은 100~200개의 서버로 이루어져 있는데 코드를 컴파일하고 새 오라클 DB를 빌드한 다음에 수백만개의 테스트를 분산시켜서 돌립니다.

- 집에 갑니다. 다음 날 다른 걸 합니다. 테스트는 완료되는데 20~30시간 가까이 걸리기 때문입니다.

- 집에 갑니다. 다음 날 테스트 결과를 확인합니다. 운이 좋은 날에는 테스트가 100개 정도 실패합니다. 나쁜 날에는 1000개 정도가 실패합니다. 이중 몇 개를 골라서 내가 무엇을 잘못했을지 고찰합니다. 아마 이 버그의 실체를 이해하려면 10개의 플래그가 더 필요할 수도 있겠네요.

- 이 이슈를 해결하기 위해 플래그를 몇개 더 만듭니다. 수정사항을 제출하고 테스트를 돌립니다. 20~30 시간을 더 기다립니다.

- 이 플래그들이 맞아떨어지는 마법의 주문을 찾을 때까지 계속 반복합니다.

- 마침내 어느날 테스트가 모두 성공합니다.

- 다른 개발자들이 이 코드를 건드려서 당신이 고친 것을 다시 망가뜨리지 않도록 수백개의 테스트를 더 작성합니다.

- 당신이 작업한 코드에 최종 테스트를 한번 더 돌립니다. 그리고 코드 리뷰를 맡깁니다. 리뷰하는데는 2주가 걸릴 수도 있고 2달이 걸릴 수도 있습니다. 그러니까 그 동안에는 새로운 버그를 잡으러 가세요.

- 2주 또는 2달이 지나고 모든 것이 끝나면 마침내 코드는 메인 브랜치로 머지됩니다.

여기까지 오라클에서 프로그래머가 버그를 잡는 과정을 절대 과장없이 말씀드렸습니다. 새로운 기능을 추가하는건 어떨지 상상해보세요. 작은 기능 하나 개발하는데 6개월에서 1년까지 (때론 2년까지) 걸립니다. (예를 들면 AD 인증 기능을 추가한다거나)

이 물건이 돌아간다는 것은 그야말로 기적이라는 말이라고밖에 설명할 수가 없을 것 같네요.

저는 더 이상 오라클에서 일하지 않습니다. 앞으로도 절대로 일하지 않을거에요!

눈물 없이 볼수 없는 내용들 아닌가? 😭

대기관리 앱을 처음 작업할 때는 가볍게 시작을 했다. 그래서 이번 챕터의 내용이 공감이 많이 간다. 최소한의 조건을 만족시키고, 이를 원하는 매장들을 소수 섭외해서 일종의 베타테스트를 진행했던 것이다. 뭐, 린 스타트업에서 주로 하는 start small, 작게 시작하라의 일종이라고 봐도 될 것 같다.

이제 회사로 들어오는 많은 피드백들은 줄어들기 시작하고, 이제 내부적으로는 유지보수하며 코드를 견고화 하는 단계에 접어들기 시작했다. 고객 중심을 초점에 맞추어 빠르게 개발과 수정이 진행하던 방식에서 이제는 객체지향적으로, 그리고 유지보수가 쉬운 코드를 위해 힘을 써야하는 시기가 오는 것 같아서 한편으로는 감개가 무량하다.

아직 끝나지 않았다.

1단계가 끝난 건 아니다. 왜냐하면 기타 검색을 하면 하나만 나오고 있기 때문이다. 고객들은 자기가 찾는 기타가 여러개 나오기를 바란다.

inventory.addGuitar("V95693",1499.95,Builder.FENDER,"Stratocastor",Type.ELECTRIC,Wood.ALDER,Wood.ALDER);
inventory.addGuitar("V9512",1599.95,Builder.FENDER,"Stratocastor",Type.ELECTRIC,Wood.ALDER,Wood.ALDER);

(보다시피 두갠데. 하나만 나오면 섭섭하잖아요.)

두개의 메소드를 수정하도록 한다.

// Inventory.ts
...
search(searchGuitar: Guitar): Guitar[] {
   const {
     builder: tBuilder,
     model: tModel,
     type: tType,
     backWood: tBackWood,
     topWood: tTopWood
  } = searchGuitar;
   return this._guitars.filter(
    (item: Guitar): boolean => {
       const { builder, model, type, backWood, topWood } = item;
       return (
         builder &&
         builder == tBuilder &&
        (model && model.toLowerCase() == tModel.toLowerCase()) &&
        (type && type == tType) &&
        (backWood && backWood == tBackWood) &&
        (topWood && topWood == tTopWood)
      );
    }
  );
}


// FindGuitarTester.ts
...
const guitars: Guitar[] = inventory.search(whatErinLikes);
   if (guitars.length > 0) {
     guitars.forEach((guitar: Guitar) => {
       const { builder, model, type, backWood, topWood, price } = guitar;
       console.log(`Erin, you might like this ${builder} ${model} ${type} guitar:
               ${backWood} back and sides,
               ${topWood} top.
               You can have it for only ${price}!`);
    });
  } else {
     console.log("Sorry, Erin, we have noting for you.");
  }

위와 같이 두 부분을 수정함으로서 고객이 원하는 목록을 한가지가 아닌 여러가지를 보여줄 수 있게 되었다. 이로서 첫번째 단계, 고객이 원하는 기능을 하게 하라가 완료가 된 것이다.

프로그램이 제대로 동작하지 않는다면 프로그램은 여러 번 수정하게 될 것이다. 설계에 너무 신경쓰다보면 쓸데없는 시간 낭비를 하게 될 수 있다. 우리가 만든 클래스, 메소드들에 기능을 추가할 때마다 설계의 많은 부분이 바뀌기 때문이다.

꼭 이런 순서를 거쳐야한다는 것은 아니다. 개인적으로는 알림톡/문자 전송 서비스를 구축한 적이 있는데 선배 개발자분께서 아키텍쳐를 잘 구축해주셔서 수정/변경사항이 일어날 때 일부 소스만 변경하여 올리면 간단하게 해결되도록 할 수 있었다.

클라이언트 -> 서버 -> 메시징 에이전트 -> 큐(SQS) -> 컨슈머(람다) -> 워커(람다) -> 문자 발송

아마 이때 설계를 잘 해놓고 개발하지 않았더라면 계속되는 요구사항에 고통받았을지도 모르겠다는 생각이 든다. 그러나 구조적으로 짜는 프로그래밍은 1. 잘 돌아가게한다. 2. 잘 설계한다. 3. 재사용 가능하게 한다는 장점이 있다.

다음 단계는...

이제 객체지향의 기본 원리를 이용해 소프트웨어를 유연하게 만들어 볼 예정이다. 잘 돌아가는 프로그램에서 내부 구조가 잘 구성되어 있는지를 확인해보자.

Comments