DRY, KISS, YAGNI

아직 주니어에 '주'도 못 뗀 개발자로써, 더 좋은 코드란 무엇인가 라는 생각을 다분히 하는 편이다.
어떤 코드가 깔끔한 코드이고, 어떤 코드가 나쁜 코드인지에 대한 감이 오지 않고, 그저 최대한 내 기준에서 Nice 하게 잘 분리하고, 재사용성이 높게 코드를 짜려 노력만 한다.

그러던 와중 유튜브 알고리즘에 드림코딩 엘리'코딩 잘하는 팁 세가지'가 뜨게 됐는데, 내용이 너무 좋아 포스팅 하고자 한다.

DRY(Don't Repeat Yourself)

"반복하지 마라"
DRY → 특정한 지식, 의도, 로직 등이 다양한 곳에서 다양한 형태로 계속 반복 되어지는 것을 피하자!
시스템내에서 특정한 지식과 로직은 한 곳에서 명확하고 신뢰할 수 있도록 존재해야한다.

동일한 로직을 반복적으로 작성해 둔다면, 로직의 변경사항이 필요한 경우 반복적으로 업데이트 해야할 뿐만 아니라, 실수해서 한 군데를 빠트린다면 유지보수가 어려워진다. 반대로 로직을 한군데에서만 작성한다면 유지보수성이 높아질 것이다.

상반된 개념으로 WET(Write Every Time / Waste Everyone's Time)이 있다.

DRY 예제코드

💩 dry1-bad.js

function greetings(user) {
  return `HI ${user.firstName} ${user.lastName}`;
}

function goodbye(user){
  return `See you next time ${user.firstName} ${user.lastName}`;
}

이렇게 하면 문제점이 무엇일까??

같은 로직(사용자의 이름출력)이 두번 쓰이므로, 이 부분을 따로 처리 할 수 있는 곳이 있으면 좋겠다.

이유 ::
사용자의 이름을 다르게 출력한다고 했을 때 사용자의 이름 출력 부분을 모두 수정해야 한다.

💡 dry1-good.js

function greetings(user) {
  return `HI ${user.fullName()}`;
}

function goodbye(user){
  return `See you next time ${user.fullName()}`;
}

class User {
  ...
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

이렇게 수정하면, 사용자의 fullName 을 출력 하는 곳을 한 군데에 정의를 해두면 이름에 대한 코드를 수정 할 때 한군데에서만 수정 하면 된다.

DRY 는 특정한 코드의 중복을 이야기하는 좁은 의미가 아니라, 지식, 의도, 로직 이 모든 것들이 중복되지 않도록 조금 더 광범위한 범위를 가리키고 있다.

KISS(Keep It Simple, Stupid)

"심플하고, 멍청하게 유지하자"
KISS → 대부분의 시스템은 복잡하게 보다는 심플하게 만들어졌을 때, 최고로 잘 동작한다. 그러므로, 시스템을 디자인할 때 불필요한 복잡성은 피해야한다.


열 줄짜리 코드를 한 줄로 바꾸기 위해서 화려한 테크닉을 이용해 가독성을 떨어트리기 보다는 심플하고 간결하게 작성하는 것이 좋다.

  • 1 function 1 기능
    별도의 주석 없이 함수의 이름, 매개변수, 구현 코드를 읽었을 때 한번에 이해 할 수 있도록 한 가지의 기능을 수행하는 함수를 심플하게 작성하는 것이 좋다.
  • 1 class 1 책임
  • UI 컴포넌트에는 별도의 비지니스 로직을 넣지 말 것
  • 서비스는 단 하나의 기능을 담당하는 개별적인 심플한 서비스!

KISS 예제코드

💩 kiss1-bad.js

function getFirst(array, isEven) {
  return array.find((x) => (isEven ? x % 2 === 0 : x % 2 !== 0));
}

이렇게 하면 문제점이 무엇일까??

이 함수는 배열을 받아서
isEven이 참이면, 처음으로 나오는 짝수의 값을
isEven이 거짓이면, 처음으로 나오는 홀수의 값을 return 한다.
그러기에는 함수를 바로 봤을 때 한눈에 이해 되지 않을 수 있다.
가독성 문제

그럼 이렇게 바꿔볼까??

function getFirst(array, isEven) {
  if (isEven) {
    return array.find((x) => x % 2 === 0);
  } else {
    return array.find((x) => x % 2 !== 0);
  }
}

하지만, 함수에서 전달되는 인자의 값이 true 인지 false 인지에 따라 다른 동작을 하게 만드는 것은 심플 하지 않다. (한 함수 에서 두가지 일을 수행)

💡 kiss1-good.js

function getFirstOdd(array) {
  return array.find((x) => x % 2 !== 0);
}

function getFirstEven(array) {
  return array.find((x) => x % 2 === 0);
}

// 굳이, 분기를 둬야 한다면
function getFirstByIsEven(array, isEven) {
  if (isEven) {
    return getFirstEven(array);
  } else {
    return getFirstOdd(array);
  }
}

훨씬 함수의 가독성이 좋아졌다!! 🙌

💩 kiss2-bad.js

function updateAndPrint(rawData) {
  // prep data...
  // more code...
  db.update(rawData);
  // get printer...
  // moer code...
  printer.print(data);
}

한 함수에 두가지 기능이 들어있다!

💡 kiss2-good.js

function update(rawData) {
  // prep data...
  db.update(rawData);
  // more code...
  return data;
}

function print(data) {
  // get printer...
  // more code...
  printer.print(data);
}

함수를 각각 개별적인 함수를 만들어 둠으로써, 이전에는 업데이트하고 프린트를 꼭 해야 했다면, 이제는 개별적으로 사용 할 수 있어졌다. 물론 같이 쓸 수도 있다.

💩 kiss3-bad.js

class LoginView {
  display() {
    // display view..
  }

  onLoginButtonClick() {
    fetch('https://server.com')
      .then((data) => data.json())
      .then((data) => {
        if (data.token) {
          localStorage.setItem('TOKEN', data.token);
          // Update UI Elements
        } else {
          // ...
        }
      });
  }
}

UI를 담당하는 class에서 데이터를 가져오고.. 데이터를 localStorage에 쌓고.. 로직이 너무 많다
로직들은 별도의 클래스를 만들어 관리하자

💡 kiss3-good.js

class LoginView {
  constructor(userPresenter) {
    this.userPresenter = userPresenter;
  }

  display() {
    // display view..
  }

  onLoginButtonClick() {
    this.userPresenter
      .login() //
      .then((result) => {
        // update text UI element with result.displayMessage
        // update button UI element with result.buttonText
      });
  }
}

class UserPresenter {
  userService;
  login() {
    this.userService
      .login() //
      .then((result) => {
        if (result.success) {
          localStorage.setItem('TOKEN', result.token);
          return {
            displayMessage: result.message,
            buttonText: 'Go Home',
          };
        } else {
          return {
            displayMessage: 'Unable to login',
            buttonText: 'Ok',
          };
        }
      });
  }
}

userPresenter 라는 별도의 클래스를 이용해 비즈니스 로직을 처리 한 다음에 처리된 데이터를 사용자에게 보여지게끔 하게 수정했다.

프리젠터는 백엔드와 어떻게 통신하면 되는지에 대한 로직을 담고있다.

💋 KISS → 개별적으로 분리하라 !!

YAGNI(You Ain't Gonna Need it)

"야! 너 그거 필요 없어"

  • 필요하지 않는 기능
  • 사용하지 않는 기능
  • 지나치게 미래지향적

⭕️

  • 깨끗하게
  • 변경이 쉽게
  • 유지보수 용이

YAGNI 예제코드

💩 yagni1-bad.js

function deleteUser(id, softDelete = false) {
  if (softDelete){
    // don't delet from db but onlt  mark as deleted.
    return this._softDelete(id);
  }
  return db.removeById(id);
}

이렇게 하면 문제점이 무엇일까??

미래에 필요하지도 않을 기능때문에 이렇게 복잡성을 추가하는 것은 좋지 않다.

이유 ::
미래에 필요하지 않을 기능 때문에 이 기능을 넣게 된다면, 우리가 기존에 작성해 두었던 데이터베이스를 다루는 모든 곳에서 업데이트를 해줘야 한다.

💡 yagni-good.js

function deleteUser(id) {
  return db.removeById(id);
}

마치며

이런 원칙들을 알고 조금만 신경써서 코드를 작성하다보면 왜 함수는 가능한 하나의 작업만 수행해야하게 좋은지, 왜 가독성이 좋은 코드가 필요한지 또한 왜 인터페이스 혹은 추상클래스가 필요한지, 그리고 왜 패턴들이 필요한지 알게 되는 것 같다.

다음에는 ealry return 같이 함수에서 헛짓 안하고 빠르게 원하는 output을 퉤! 하고 뱉어줄 수 있는 여러 방법론,
또 한 번은 그냥 내가 감각적으로 아 대충 이래서 이렇지 라고 생각되는 함수의 return문 제대로 이해하기를 TIL에 남겨볼까 한다.

좋은 소설가가 되기 위해 좋은 문학작품을 접하듯이, 좋은 개발자가 되기 위해 맛있는 코드들을 자주 접하는 것도 필요해보인다.

[ 참고 ]

좋은 웹페이지 즐겨찾기