변화하는 소프트웨어와 리펙토링

gyomni·2022년 6월 6일
1

Week I Learned

목록 보기
12/20
post-thumbnail

소프트웨어의 가장 큰 특징 ?

변한다!

대부분의 소프트웨어는 현실의 문제를 해결하기 위한 솔루션으로 만들어진다.
여기서의 현실은? 매번 변하게 된다!
당연히 현실의 문제가 바뀌니까, 현실의 문제를 해결하는 소프트웨어도 바뀌어야 한다!
그래서! 소프트웨어는 굉장히 자주, 그리고 지속적으로 바뀌게 된다.


소프트웨어를 변경하는 사람은 ? 👩‍💻

당연하게 바로 개발자! (물론 나도... 힛)

개발자들은 소프트웨어를 이루고 있는 코드를 자주 바꿀 수 밖에 없다.
그럼, 소프트웨어가 자주 변화하는 환경에서 개발자들이 생각해야 할 점은 무엇일까?

현재 소프트웨어 차지 비율이 점점 늘고 있고, 소프트웨어에 의존성 또한 커지고 있다.
그만큼 소프트웨어에게 주어지는 책임감도 커져있는것이 사실이다.

  • 여기서의 책임감이란, 안전하게, 올바르게 작동되어야 한다는 것!
  • 개발자에게 다가오는 책임감의 무게가 무거워 질 수 밖에 없다.
  • 소프트웨어를 바꿨을 경우, 안전하게 잘 작동되어야 하는 것은 굉장히 중요한 일이 될수 밖에 없다.

이와 같은 환경을 전제로 깔 경우,
소프트웨어는 계속 바뀌어야 하고, 개발자들은 안전하게 잘 바꿔나가야 한다.


개발자는 무엇을 고려해야 할까? 🤔

  • 어떻게 코드를 안전하게 잘 바꿀 수 있을까?
  • (바꾸기 전에는 코드를 잘 파악해야 하므로) 어떤 코드 형태가 내용을 잘 파악할 수 있는 형태일까?

위에서 언급한 책임감을 가지고 개발하기 위해서는 이런 사항들을 충분히 고려해야 한다!

수 많은 소프트웨어 개발, 프로그래밍 패턴과 같은 것들이 대부분 변경 용이성에 초점이 맞춰져 있다.
(아주 다양한 프로그래밍 패턴들이 존재하고, 아직 나는 이 부분에 대한 지식이 없으므로 조만간 공부해야봐야 겠다!!! )

아무튼, 이 다양한 프로그래밍 패턴은 굉장히 다양하고 복잡하지만, 이의 근본은

  • 어떻게하면 잘, 안전하게, 빠르게 바꿀 수 있을까?
  • 바꿔도 올바르게 동작할 수 있을까?
  • 올바르게 동작하지 못하더라도 그 올바르지 못한 동작이 최소한의 영향 범위를 갖게 할 수 있을까?

이와 같은 사항들에 초점이 많이 맞춰져 있다.


위의 내용을 간단한 코드 리펙토링에 적용해서 이해해보자!

[ A ]

function createEl(type, props){
	switch(type){
    	case 'h1':
        	return [document.createElement('h1')]
        		.map(el => {
            		Object
              		.entries({...props, 'data-id':'title'})
              		.forEach(([name, value]) => el.setAttribute(name, value))
            		return el;
            	})[0];
        case 'div':
        	return [document.createElement('div')]
        		.map(el => {
            		Object
              		.entries({...props, 'data-id':'layout'})
              		.forEach(([name, value]) => el.setAttribute(name, value))
            		return el;
            	})[0];
    }
}

코드 설명
1) type에 들어오는 문자열에 따라 createElement를 이용하여DOM APIDOM node를 하나 만들고, Atrributedata-id 속성을 추가하여 내용을 넣는다.

2) 또 다른 인자 props 객체를 setAttribute를 통해 주입 하고, 만들어진 elementreturn한다.

위 함수의 경우 switch case가 여러가지 늘어날 수 있다. (확장 가능성 있는 함수)

소프트웨어는 변한다고 했으니, 그런 상황을 가정해보자!
어떤 변화가 생길 수 있을까?

  • createElcase 추가
  • case구문 안의 로직 변경

등등 이 있을 수 있다.

여기서,
h1 태그 생성 로직을 수정한다고 생각해본다면, div 태그 생성 로직 상관이 없는 변경이 된다.
즉, h1 생성 로직 범위만 변경 되었지만 안전하게 아무 이상없이 동작된다는 것을 테스트 해야 하는 범위는 createEl함수 전체가 된다. 현재는 바뀐 부분만 테스트 할 수 없는 구조로 되어있기 때문!

변경 이후 안정성을 어느 정도 확보해 줄 것인가? 라는 측면에서 본다면 위의 함수는 변경에 용이하지 않은 구조라는 것을 알 수 있다.

이런 부분을 고려하여 첫번째 리펙토링을 해보자!
case들은 개별 로직이기 때문에, 따로 분리해볼 수 있다.

[ B ]

function createH1(props) {
  return [document.createElement("h1")].map((el) => {
    Object.entries({ ...props, "data-id": "title" }).forEach(([name, value]) =>
      el.setAttribute(name, value)
    );
    return el;
  })[0];
}

function createDiv(props) {
  return [document.createElement("h1")].map((el) => {
    Object.entries({ ...props, "data-id": "title" }).forEach(([name, value]) =>
      el.setAttribute(name, value)
    );
    return el;
  })[0];
}

function createEl(type, props) {
  switch (type) {
    case "h1":
      return createH1(props);
    case "div":
      return createDiv(props);
  }
}

코드 설명
바깥 쪽으로 createH1함수와 createDiv함수를 분리

리펙토링된 함수를 살펴 보면,
h1태그를 생성하는 로직에 변경사항이 생기면 createH1만 온전히 테스트하면 된다.

리펙토링 전 코드( [A] )는 모든 코드가 createEl함수 안에 다 들어가 있었기 때문에
createEl함수 전체를 테스트 했어야 했다. (실제 변경 범위와 테스트 범위가 달라지는 문제가 발생)
이런 측면에서 본다면, [A]코드보다는 [B]코드가 개선 된 것을 알 수 있다.

하지만, 아직은 추가에 취약하다는 문제가 남아있다.

만약 태그를 추가하는 함수가 생긴다면, (createInput, createH2, createH3....)
createEl함수의 case문이 추가 되어야 한다.
이런 경우는 createEl함수의 다른 case은 아무런 상관이 없음에도 불구하고 createEl함수를 전체 테스트 해봐야한다는 문제가 생긴다.
결국 [A]처럼 변경 범위와 테스트 범위가 달라지는 문제가 다시 발생하게 된 것이다.

그럼 추가 했을 경우에도 createEl함수의 영향 범위 안에 들어가지 않도록 switch구문을 없애보면 될까? 다시 리펙토링 해보자!

createEl함수의switch문을 보면,
문자열과 함수가 연결 되어있는 구조이고, type은 어떤 함수를 호출 할 것인가에 대한 트리거 역할만 하는 값이다.
이를 이용해서 맵핑 정보를 만들어두면, 맵핑 정보만 전달하고, 그 정보에 따라 호출만 해주는 식으로 바꾸면 되겠다!

[ C ]

function createH1(props) {
  return [document.createElement('h1')]
  .map(element => {
    Object
      .entries({ ...props, 'data-id': 'subject' })
      .forEach(([name, value]) => element.setAttribute(name, value))
    return element;
  })[0];
}

function createDiv(props) {
  return [document.createElement('div')]
  .map(element => {
    Object
      .entries({ ...props, 'data-id': 'layout' })
      .forEach(([name, value]) => element.setAttribute(name, value))
    return element;
  })[0];
}

const creatorMap = {
  h1: createH1,
  div: createDiv,
};

function createEl(type, props) {
  return creatorMap[type][props]
}

createEl가 훨씬 안정적인 모양을 취하게 되었다.
태그 생성 함수가 추가 된다면 creatorMap에 추가만 해주면 된다.
이렇게 추가 시에도 createEl함수는 전혀 변경이 없게 된다.

변화 시에 그 변화에 대한 영향 범위를 특정 범위로 굉장히 축소할 수 있는 코드로 만들어 진다면, 훨씬 더 안전, 단단, 견고한 코드가 된다는 것을 알 수 있게 되었다~!

한번 더 코드를 확인해보면 아직 변경사항이 남아있다!
createEl함수가 creatorMap이라는 외부 변수에 의존하고 있다.
그래서, 이 외부 변수인 creatorMap가 바뀌게 되었을 경우 createEl함수가 동작이 잘 안될 가능성이 존재한다.

마지막으로 이 부분을 제거해보자!

[ D ]

function createH1(props) {
  return [document.createElement('h1')]
  .map(element => {
    Object
      .entries({ ...props, 'data-id': 'subject' })
      .forEach(([name, value]) => element.setAttribute(name, value))
    return element;
  })[0];
}

function createDiv(props) {
  return [document.createElement('div')]
  .map(element => {
    Object
      .entries({ ...props, 'data-id': 'layout' })
      .forEach(([name, value]) => element.setAttribute(name, value))
    return element;
  })[0];
}

const creatorMap = {
  h1: createH1,
  div: createDiv,
};

const coupler = map => (type, props) => map[type](props);
const createEl = coupler(creatorMap);

createEl는 외부 변수인 creatorMap 이라고 하는 객체의 종속성을 끊어 냈고, coupler함수를 이용해서 안전하게 동작할 수 있는 구조가 되었다!

바깥쪽에서는 createEl인터페이스를 계속 유지하고 있으니 사용하는 방법은 [A], [B], [C], [D] 모두 같다!


✍ 코드의 내용은 간단할 수 있지만, 위와 같은 리렉토링을 해보면서 소프트웨어의 변화에 어떤식으로 안전하게 코드로 대응할 수 있을지를 한번 더 생각하고 고민해보는 계기가 되었다

profile
Front-end developer 👩‍💻✍

0개의 댓글