리팩터링

[ Programming > Refactoring ]

[리팩터링] 기본적인 리팩터링

 Carrot Yoon
 2025-08-12
 11

[리팩터링] 기본적인 리팩터링

최근 Nextjs로 만든 블로그를 리팩터링하면서 만족스럽지 않은 것 같아 다시 리팩터링을 공부해보고자 한다. 나는 "어떤 자세를 취해야 할까??" 혹은 "어느 정도까지 리팩터링 원칙을 적용해야할까?" 혹은 "책에서는 리팩터링이 성능을 낮추지만 리팩터링 후에는 오히려 성능 튜닝하기 편해진다는 식으로 말했었는데 과연 그런 여유를 가지며 리팩터링 할 수 있을까??"와 같은 생각을 자주 한다. 현업에서도 상황에 따라 적정선이 많이 달라지는 것 같다는 생각을 많이 했었고 결국 모든 것은 트레이드오프 관계로 이루어진다는 것이 현재 나의 생각이다... 이런 막연한 생각 말고 좀 더 구체적으로 상황에 따른 해법을 제시할 수 있으면 좋겠다.

그래서 내가 만든 컴포넌트 중 엉망인 부분을 리팩터링 책에 나온 기본적인 리팩터링을 적용하면 어떻게 느껴질지 실험해보고자 한다. 물론 시간이 많이 걸려서 책에 있는 예시도 많이 사용할 예정이다.

1. 함수 추출하기

함수 호출하기는 인라인 코드를 별도로 함수로 만들어 호출하는 것을 의미한다. 함수(function) 추출하기에서 함수는 객체 지향 언어의 메서드(method)나 절차형 언어의 프로시저(procedure)나 서브루틴(subroutine)이라고 보고 똑같이 적용하면 된다.

코드를 "언제 함수로 묶어야 할까?"라는 기준은 너도 나도 다르다. 그 기준은 길이가 될 수 있고, 화면을 기준으로 될 수 있고, 재사용성을 기준으로 둘 수 있다. 리팩터링 책 저자는 "목적과 구현을 분리"하는 방식이 가장 합리적인 기준으로 본다. 개인 적인 해석으로는 추상화라고 생각한다. 구현한 코드의 의미(목적)을 하나의 네이밍(함수)로 추상화하여 분리하는 것이라는 소리로 해석된다.(동의!!)

저자가 예를 든 것 중 하나가. highlight라는 함수가 있는데 이 함수는 reverse()를 호출하여 색상을 반전만 하는 코드 1개만 있었다고 한다. hilight(강조)가 목적이며 그 목적을 달성하기 위한 구현은 reverse(색상 반전)이 된다는 것이다.

또한 저자는 함수를 짧게 만들면 함수 호출이 많아져서 성능이 느려질까봐 걱정하는 사람이 있다고 하는데, 함수가 짧으면 캐싱하기가 더 쉽기 때문에 컴파일러가 최적화하는 데 유리할 때가 많다고 한다. 근데 개인적인 경험으로는 그렇지 않았던 경우가 있어서 나는 동의하기가 힘들었다. 대용량 리스트를 엑셀 형식으로 관리하는 컴포넌트를 만든적이 있었는데, 리스트 데이터를 처리할 때 for문을 사용하는 것과 Array.forEach를 사용하는 경우 무시하지 못할 정도의 속도가 차이나는 경험했었기 때문이다. 실제 사용했을때 미세한 렉??도 줄어들었었다. 그 원인은 자바스크립트 실행 컨텍스트 생성유무라고 생각했었긴 한다.

1.1 함수 추출 절차

다음은 저자가 말하는 함수 추출 절차이다

  • 절차

    1. 함수를 새로 만들고 목적을 잘 드러내는 이름 붙이기("어떻게"가 아니라 "무엇을" 하는지 잘 나타내야함)

    2. 추출할 코드를 복사하여 새 함수에 붙이기.

    3. 추출한 코드에서 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 매개변수(파라미터)로 전달.(단 함수 안에 중첩 함수로 선언 가능한 경우에는 지역 변수를 매개변수로 받지 않을 수 있음.)

    4. 함수 컴파일 해보기

    5. 원본 함수의 코드를 새로 만든 함수로 교체

    6. 테스트

    7. 다른 코드에 추출한 함수와 비슷한 코드가 있는지 확인한 후, 추출한 함수로 바꿀지 결정.

그리고 아래는 내가 만들어본 간단한 절차이다

  • 절차2 (내가 만들어본 간소화한 절차)

    1. 인라인 코드들에 주석으로 무슨 일을 하는지 묶어서 적어본다.

    2. 주석을 통해 목적에 맞는 함수명으로 함수를 추출한다.

    3. 추출한 함수로 인라인 코드를 교체한다.

    4. 다음 번에 비슷한 로직이 사용되는 것을 발견하면 함수를 공통으로 뺄지 고려해본다.

1.2 함수 추출 예시

아래는 함수 추출 적용 전과 후 예시입니다. 왜 별도 hook으로 분리 안하냐고 물으신다면.. 하겠습니다 ㅜㅜ

함수 추출 전
const SidebarProfile = () => {  const {name, role, profileImg, isLogined, changeName, logout} = useAuth();  // name edit 관련 변수들  const [isNameValid, setIsNameValid] = useState<boolean>(true);  const [isEdit, setIsEdit] = useState<boolean>(false);  const nameInputRef = useRef<HTMLInputElement>(null);  // 비로그인 시 출력 X;  if (!isLogined) return;  // 어드민 여부  const isAdmin = role === "ADMIN";  return (    <>      <div        style={{          display: "flex",          flexDirection: "row",          flexGrow: 1,          alignItems: "center",          gap: "0.75rem",          paddingInline: "1.25rem",        }}>        <div          className="border-1 h-8 overflow-hidden rounded-full"          style={{position: "relative"}}>          <Image            src={`${FILE_SERVER_URL}/${profileImg}`}            alt="profile image"            width={32}            height={32}            unoptimized={true}          />        </div>        <div          style={{            display: "flex",            flexDirection: "row",            justifyContent: "space-between",            alignItems: "center",            gap: "0.375rem",            flexGrow: 1,          }}>          {isEdit ? (            <TextField              id="name-change"              className={`max-w-23`}              size="sm"              hasError={!isNameValid}              defaultValue={name || "익명"}              placeholder="2-20 글자"              // 이 이름 수정 input 출력시 focus 해주기기              ref={node => {                if (node) {                  nameInputRef.current = node;                  node.focus();                }              }}              // 이름 유효성 여부부              onChange={e => {                const newName = e.currentTarget.value;                if (newName.length < 2 || newName.length>=10) {                  setIsNameValid(false);                } else {                  setIsNameValid(true);                }              }}            />          ) : (            <span>{name}</span>          )}          <div            style={{              display: "flex",              flexDirection: "row",              gap: "0.375rem",            }}>            {isEdit ? (              <IconButton                icon="check"                size="md"                onClick={() => {                  // 이름 수정 시도도                 const newName = (nameInputRef.current?.value)?.trim() || "";                                    if (newName && name) {                    if (newName === name) {                      setIsNameValid(false);                      return;                    }                    patchUser({                      originName: name,                      willChangeName: newName,                    }).then(newName => {                      changeName(newName);                      setIsEdit(false);                    });                  } else {                    setIsNameValid(false);                  }                }}              />            ) : null}            <IconButton              aria-label="name-edit"              icon={isEdit ? "x" : "edit"}              size="md"              tooltip={isEdit ? "수정 취소" : "닉네임 수정"}              // 수정을 취소하거 수정 모드로 진입 토글글              onClick={() => {                setIsNameValid(true);                setIsEdit(prev => !prev);              }}            />          </div>        </div>      </div>      <Card className="mx-5 flex flex-row justify-between px-2 py-1">        <IconButton          as={Link}          prefetch={false}          href={"/article/create"}          variant="secondary"          disabled={!isAdmin}          aria-label="go to write article"          tooltip="글쓰기"          icon="writing"          size="lg"        />        <IconButton          as={Link}          prefetch={false}          variant="secondary"          tooltip="카테고리 편집"          aria-label="go to edit category"          disabled={!isAdmin}          href={"/category/edit"}          icon="category"          size="lg"        />        <form method="POST" action={`${BACK_SERVER_URL}/auth/logout`}>          <IconButton            icon="logout"            type="submit"            name="logout"            id="logout"            variant="secondary"            size="lg"            tooltip="로그아웃"            aria-label="logout"            // 로그아웃 및, app을 위한 이벤트 전달.            onClick={() => {              logout();              if (window.ReactNativeWebView)                window.ReactNativeWebView.postMessage("googleSignOut");            }}          />        </form>      </Card>    </>  );};export default SidebarProfile;

함수 추출 적용 후
const SidebarProfile = () => {  const {name, role, profileImg, isLogined, changeName, logout} = useAuth();  const [isNameValid, setIsNameValid] = useState<boolean>(true);  const [isEdit, setIsEdit] = useState<boolean>(false);  const nameInputRef = useRef<HTMLInputElement>(null);  const validateName = useCallback(    (newName: string | null): newName is string => {      const isValidName =        !!newName &&        2 <= newName.length &&        newName.length < 10 &&        newName !== name;      return isValidName;    },    [name],  );  const toggleDisplayNameInput = useCallback(() => {    setIsNameValid(true);    setIsEdit(prev => !prev);  }, []);  const fetchChangeName = useCallback(    (newName: string) => {      const isValidName = validateName(newName);      if (!isValidName) return setIsNameValid(false);      function handleNameChangeSuccess(name: string) {        changeName(name);        toggleDisplayNameInput();      }      patchUser({        originName: name!,        willChangeName: newName,      }).then(newName => {        handleNameChangeSuccess(newName);      });    },    [name, changeName, toggleDisplayNameInput, validateName],  );  // 비로그인 시 출력 X;  if (!isLogined) return;  // 어드민 여부  const isAdmin = role === "ADMIN";  return (    <>      <div        style={{          display: "flex",          flexDirection: "row",          flexGrow: 1,          alignItems: "center",          gap: "0.75rem",          paddingInline: "1.25rem",        }}>        <div          className="border-1 h-8 overflow-hidden rounded-full"          style={{position: "relative"}}>          <Image            src={`${FILE_SERVER_URL}/${profileImg}`}            alt="profile image"            width={32}            height={32}            unoptimized={true}          />        </div>        <div          style={{            display: "flex",            flexDirection: "row",            justifyContent: "space-between",            alignItems: "center",            gap: "0.375rem",            flexGrow: 1,          }}>          {isEdit ? (            <TextField              id="name-change"              className={`max-w-23`}              size="sm"              hasError={!isNameValid}              defaultValue={name || "익명"}              placeholder="2-20 글자"              // 이 이름 수정 input 출력시 focus 해주기기              ref={node => {                nameInputRef.current = node;                node?.focus();              }}              // 이름 유효성 여부부              onChange={e => {                const newName = e.currentTarget.value;                const isValidName = validateName(newName);                setIsNameValid(isValidName);              }}            />          ) : (            <span>{name}</span>          )}          <div            style={{              display: "flex",              flexDirection: "row",              gap: "0.375rem",            }}>            {isEdit && (              <IconButton                icon="check"                size="md"                disabled={!isNameValid}                onClick={() => {                  // 이름 수정 시도도                  const newName = getTrimmedName(nameInputRef.current?.value);                  fetchChangeName(newName);                }}              />            )}            <IconButton              aria-label="name-edit"              icon={isEdit ? "x" : "edit"}              size="md"              tooltip={isEdit ? "수정 취소" : "닉네임 수정"}              // 수정을 취소하거 수정 모드로 진입 토글글              onClick={toggleDisplayNameInput}            />          </div>        </div>      </div>      <Card className="mx-5 flex flex-row justify-between px-2 py-1">        <IconButton          as={Link}          prefetch={false}          href={"/article/create"}          variant="secondary"          disabled={!isAdmin}          aria-label="go to write article"          tooltip="글쓰기"          icon="writing"          size="lg"        />        <IconButton          as={Link}          prefetch={false}          variant="secondary"          tooltip="카테고리 편집"          aria-label="go to edit category"          disabled={!isAdmin}          href={"/category/edit"}          icon="category"          size="lg"        />        <form method="POST" action={`${BACK_SERVER_URL}/auth/logout`}>          <IconButton            icon="logout"            type="submit"            name="logout"            id="logout"            variant="secondary"            size="lg"            tooltip="로그아웃"            aria-label="logout"            // 로그아웃 및, app을 위한 이벤트 전달.            onClick={logout}          />        </form>      </Card>    </>  );};export default SidebarProfile;function getTrimmedName(name?: string | null) {  return name?.trim() || "";}

2. 함수 인라인하기

함수 인라인하기는 함수 호출하기의 반대라고 생각하면 된다. 함수로 따로 뺀 코드를 함수를 제거하고 코드를 직접 사용하는 것이다.

함수 본문(인라인 코드)가 "굳이 따로 함수로 추출하지 않아도 될 만큼 명확한 경우" 혹은 "함수 이름만큼 깔끔하게 리팩터링한 코드인 경우"에 함수를 제거하고 코드 인라인을 그대로 사용한다. 즉 굳이 분리하지 않아도 될 만큼 명확하거나, 리팩터링을 잘못하여 잘못 함수를 추출을 한 경우에 함수 인라인을 적용한다고 보면 된다. 또한 간접 호출을 너무 과하게 쓰는 코드도 인라인 대상이라고 저자가 말한다. 단순히 위임하기 위한 함수가 많아서 위임 관계가 복잡하게 얽혀 있으면 인라인해버린다.

말로는 간단해 보여도 실제로는 그렇지 않는 경우가 많다. 예를 들면 재귀 호출, 반환문이 여러개인 함수, 접근자가 없는 다른 객체에 메서드를 인라인하는 방법 등등 실제로는 인라인하기 어려운 경우가 많다고 한다. 그런데 보통 이런 경우에는 인라인을 적용하면 안되는 경우일 것이라고 저자가 설명한다.

2.1 함수 인라인 절차

다음은 저자가 말하는 절차다

  • 절차

    1. 다형 메서드인지 확인한다. (서브클래스에서 오버라이드하는 메서드는 인라인하면 안 된다.)

    2. 인라인할 함수를 호출하는 곳을 모두 찾는다.

    3. 각 호출문을 함수 본문으로 교체한다.

    4. 하나씩 교체할 때마다 테스트한다. (굳이 한번에 처리할 필요 없고 까다로운 부분은 일단 남기고 여유가 생기면 틈틈이 한다.)

    5. 함수 정의를 삭제한다.

2.2 함수 인라인 예시

아래 예시는 책에 나온 간단한 예시다.

함수 인라인 적용 전
function rating(aDriver) {    return moreThanFivelateDeliveries(aDriver) ? 2 : 1;}function moreThanFiveLateDeliveries(dvr) {    return dvr.numberOfLateDeliveries > 5;}
함수 인라인 적용 후
function rating(aDriver) {    return aDriver.numberOfLateDeliveries > 5;}

3. 변수 추출하기

표현식이 너무 복잡해서 이해하기 어려울 때가 있다. 이럴 때 지역변수를 활용하면 표현식을 쪼개 관리하기 더 쉽게 만들 수 있다.(가독성은 덤)

그러면 복잡한 로직을 구성하는 단계마다 이름을 붙일 수 있어 코드의 목적을 훨씬 명확하게 드러낼 수 있다.

또한 추가한 변수는 디버깅에도 도움된다. 디버거에 중단점을 지정한거나 상태를 출력하게할 수 있기 때문이다.

변수로 추출할 때에는 이름이 들어갈 문맥을 고려해야한다. 현재 함수 안에서만 의미가 있는 이름으로 들어갈 수도 있지만, 더 넓은 문맥에서 사용한다면 그에 알맞는 이름의 변수로 추출해서 중복이 적은 코드를 작성해야한다. 그런데 넓은 문맥까지 넓힌다면 주로 함수로 추출된다.(ex. getBasePrice())

3.1 변수 추출하기 절차

아래는 리팩터링 저자의 절차이다.

  • 절차

    1. 추출하려는 표현식에 부작용은 없는지 확인한다.

    2. 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입한다.

    3. 원본 표현식을 새로 만든 변수로 교체한다.

    4. 테스트한다.

    5. 표현식을 여러곳에서 사용한다면 각각을 새로 만든 변수로 교체한다. 하나 교체할 때마다 테스트한다.

3.2 변수 추출하기 예시

여기서는 저자의 예시와 저의 예시를 함께 공유하겠습니다.

저자의 변수 추출 전/후
function price(order) {    // 가격(price) = 기본 가격 - 수량 할인 + 배송비    return order.quantity * order.itemPrice -             Math.max(0, order.quantity - 500) * order.itemPrice*0.05 +             Math.min(order.quantity * order.itemPrice*0.1,100);}function price(order) {    const basePrice = order.quantity * order.itemPrice;    const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice*0.05;    const shipping = Math.min(order.quantity * order.itemPrice*0.1,100);    return basePrice - quantityDiscount + shipping;}class Order {    get quantity() {return this._data.quantity;}    get itemPrice() {return this._data.itemPrice;}    get basePrice() {return this.quantity + this.itemPrice;}    get quantityDiscount() {return Math.max(0, this.quantity - 500) * this.itemPrice*0.05;}    get shipping() {return Math.min(this.quantity * this.itemPrice*0.1,100);}    get price() {        return this.basePrice - this.quantityDiscount + this.shipping;    }} 
내 코드에서 변수 추출 후 (표현식이 아닌 변수만 따로 뺌... 변수 추출하기 보다는 변수 네이밍?? 클린코드에 있음 ㅜㅜ)
const NAME_MAX_LENGTH = 10;const NAME_MIN_LENGTH = 2;const validateName = useCallback(    (newName: string | null): newName is string => {      const isValidName =        !!newName &&        NAME_MIN_LENGTH  <= newName.length &&        newName.length < NAME_MAX_LENGTH &&        newName !== name;      return isValidName;    },    [name]);

저자의 예시를 보면. class 형태에서 get 으로 연산해서 값을 가져온다. 이게 중요한 점은 side effect가 없다는 점이다(절차의 1번을 준수함). 왜냐하면 별도의 set을 하지 않고 get으로 계산하여 가져오기 때문이다.

4. 변수 인라인하기

변수 인라인하기는 너무 간단해서 예시로 설명하겠다. 한줄 요약 하면 원래 표현식과 다를 바 없을 정도로 의미가 없거나 리팩터링 하는데 오히려 방해가 될때 인라인 한다.

// ------------- 인라인 전 ------------- //let basePrice = anOrder.basePrice;return (basePrice > 1000);// ------------- 인라인 후 ------------- //return anOrder.basePrice > 1000;

그래도 주의할 점은 대입문의 우변(표현식)에서 부작용이 생기지 않는지 확인해야 한다.

5. 함수 선언(이름) 바꾸기

저자는 함수는 프로그램을 나누는 주된 수단이라고 한다. 함수 선언은 각 부분이 서로 맞물리는 방식을 표현하며 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 한다. 그래서 연결부를 잘 정의하면 시스템에 새로운 부분을 추가하기 쉬워지지만, 잘못 정의하면 지속적인 방해 요인이 된다.

이러한 연결부에서 가장 중요한 요소는 함수의 이름이다. 이름이 좋으면 함수의 구현 코드를 볼 필요 없이 호출문만 보고도 무슨 일을 하는지 파악할 수 있다. 하지만 좋은 이름을 떠올리기 힘들며, 언제나 안좋은 이름을 그대로 놔두고 싶은 유혹에 빠진다!!!! 좋은 이름이 생각나면 바로바로 반영하는 버릇을 가지자.

그러면 좋은 이름을 어떻게 잘 떠올릴까?? 바로 함수의 목적을 설명하는 것이다. 주석으로 목적을 정의해보고 주석을 지워 좋은 이름으로 바꿔 껴보자.(주석이 필요없는 네이밍을 해보자)

함수의 매개변수도 마찬가지다. 매개변수는 함수가 외부 세계와 어우러지는 방식을 정의하는 것이다. 매개변수는 함수를 사용하는 문맥을 결정한다. 매개변수를 올바르게 선택하기는 단순한 규칙으로 표현할 수 없고 정답이 없다. 예를 들면 어떤 객체의 대여 기간이 30일 지났는지 기준으로 지불 기한이 넘었는지 판단하는 간단한 함수가 있다고 할 때, 객체를 매개변수로 할 지, 지불 기한을 매개변수로 할 지 고민하게 된다. 객체를 매개변수로 하면 해당 객체의 인터페이스와 결합해버리지만 객체의 여러 속성에 접근할 수 있어 내부 로직이 복잡해져도 함수를 일일이 찾아서 변경할 필요가 없다. 실질적으로 함수의 캡슐화 수준이 높아지는 것이다. 중요한 것은 정답이 없다는 것이며, 어떻게 연결하는 것이 더 나은지 이해가 된다면 그때 코드를 개선하면 된다.

5.1 함수 선언 바꾸기 절차

아래는 리팩터링 저자의 함수 선언 바꾸기 절차다.

  • 간단한 절차

    1. 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.

    2. 메서드 선언을 원하는 형태로 바꾼다.

    3. 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.

    4. 테스트한다.

  • 마이그레이션 절차

    1. 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.

    2. 함수 본문을 새로운 함수로 추출한다.

    3. 추출한 함수에 매개변수를 추가해야 한다면 '간단한 절차'를 따라 추가한다.

    4. 테스트한다

    5. 기존 함수를 인라인한다.

    6. 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름으로 되돌린다.

    7. 테스트한다.

5.2 함수 선언 바꾸기 예시

함수 이름 바꾸기 (마이그레이션 절차)
/* --------- 원본 --------- */function circum(radius) {    return 2 * Math.PI * radius;}/* --------- 중간 --------- */function circum(radius) {    return circumferencce(radius);}function circumference(radius) {    return 2 * Math.PI * radius;}/* --------- 완료 --------- */function circumference(radius) {    return 2 * Math.PI * radius;}

매개변수 추가하기
/* --------- 원본 --------- */function addReservation(customer) {    this._reservations.push(customer);}/* --------- 중간 1 --------- */function addReservation(customer) {    this.zz_addReservation(customer);}function zz_addReservation(customer) {    this._reservations.push(customer);}/* --------- 중간 2 --------- */function addReservation(customer) {    this.zz_addReservation(customer, false);}function zz_addReservation(customer, isPriority) {    this._reservations.push(customer);}/* --------- 중간 3 --------- */function zz_addReservation(customer, isPriority) {    assert(isPrioirty === true || isPriority === false); // 사용하는 함수들 중에 빠진 부분들 확인(타입스크립트면 알아서)    this._reservations.push(customer);}/* --------- 마지막 --------- */function addReservation(customer, isPriority) {     ...}

6. 변수 캡슐화하기

변수 캡슐화는 변수를 직접 접근하지 않고, 접근 제어자를 통해 접근하거나 수정하도록 변경하는 것을 의미한다.

저자에 따르면 함수는 데이터보다 다루기 수월하다. 함수를 사용한다는 건 대체로 호출한다는 뜻이고, 함수의 이름을 바꾸거나 다른 모듈로 옮기기는 어렵지 않다. 여차하면 기존 함수는 그대로 두고 전달 함수로 활용할 수도 있다.

반대로 데이터는 함수보다 다루기 까다롭다. 왜냐하면 위와 같이 처리할 수 없기 때문이다. 데이터는 참조하는 모든 부분을 한번에 바꿔야 코드가 제대로 동작한다. 그리고 임시 변숯처럼 유효범위가 아주 좁은 데이터는 어렵지 않지만, 넓은 데이터는 다루기 어려워진다.

그래서 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독접하는 함수를 만드는 식으로 캡슐화하는 것이 가장 좋은 방법일 때가 많다. 데이터 재구성이라는 어려운 작업을 함수 재구성이라는 더 단순한 작업으로 변환하는 것이다.

저자에 따르면 데이터 캡슐화는 다른 경우에도 도움을 준다. 데이터를 변경하고 사용하는 코드를 감시할 수 있는 확실한 통로가 되어주기 때문에 데이터 변경 전 검증이나 변경 후 추가 로직을 쉽게 끼어넣을 수 있다. 그래서 저자는 유효범위가 함수 하나보다 넓은 가변 데이터는 모두 이런식으로 캡슐화해서 그 함수를 통해서만 접근하게 만드는 습관이 있다고 한다. (데이터에 대한 결합도도 높아지지 않을 수 있다.)

객체 지향에서 객체의 데이터를 항상 private으로 유지하라고 강조하는 이유가 바로 여기에 있다고 한다. 그리고 자가 캡슐화를 주장하는 사람도 있다고 한다(클래스 내에서도 필드 참조할때 접근자를 통해서 접근해야 한다고 주장하는 사람). 그런데 저자는 이건 지나치지 않냐라는 의견을 냈고, 그 정도로 캡슐화해야할 정도로 클래스가 크다면 잘게 쪼개야한다고 주장한다.

그리고 불변 데이터는 가변데이터보다 캡슐화할 이유가 적다. 데이터가 변경될 일이 없어서 갱신 전 검증 같은 추가 로직이 자리할 공간을 마련할 필요가 없기 때문이다. 그리고 불변 데이터는 옮길 필요 없이 복제하면 된다.

6.1 변수 캡슐화 절차

아래는 리팩터링 저자가 설명하는 절차다.

  • 절차

    1. 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.

    2. 정적 검사를 수행한다.

    3. 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꾼다. 하나씩 바꿀 때마다 테스트한다.

    4. 변수의 접근 범위를 제한한다.

    5. 테스트한다.

    6. 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려해본다.

6.2 변수 캡슐화 예시

// defaultOwner.jslet defaultOwner = {firstName:"마틴", lastName:"파울러"};export function defaultOwner() {return defaultOwnerData};export function setDefaultOwner(arg) {defaultOwnerData = arg};// 근데 위 방식은 만약 여러 파일에서 가져다 사용하고, 수정한다면 하나의 파일 수정할 때 다른 파일에도 영향을 줄 수 있다.//defaultOwner.js 그래서 아래와 같이 Object.assign으로 얕은 복사를 해서 주기도 한다. Setter에도 복사해서 주기도 한다.let defaultOwner = {firstName:"마틴", lastName:"파울러"};export function defaultOwner() {return Object.assign({},defaultOwnerData)};export function setDefaultOwner(arg) {defaultOwnerData = arg};// 하지만 위 방법들은 개별 필드에 대한 접근을 조절하기 힘들다. 그래서 Class로 레코드 캡슐화를 하기도한다.

7. 변수 이름 바꾸기

변수 이름 바꾸기는 간단히 예시만 짚고 넘어가겠다.

/* 전 */let a = height * width;const cpyNm = "구글";/* 후 */let area = height * width;const companyName = "구글";

DDD 환경에서는 축약어가 약속된 언어라면 그대로 쓰는 것이 맞을 것이다. 상황에 맞게 쓰자.

8. 매개변수 객체 만들기

저자는 데이터 항목 여러 개가 이 함수에서 저 함수로 함께 몰려다니는 경우를 자주 본다고 한다. 그리고 이런 데이터 무리를 발견하면 하나의 구조로 묶어준다고 한다.

데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해지며, 함수는 매개변수 수가 줄어든다. 그리고 같은 데이터 구조를 사용하는 모든 함수가 원소를 참조할 때 항상 똑같은 이름을 사용해서 일관성도 높아진다.

하지만 이 리팩터링의 진정한 힘은 코드를 더 근본적으로 바꿔준다는 것이다. 데이터 구조에 담길 데이터에 공통으로 적용되는 동작을 추출해서 함수로 만들 수 있다. 이 과정에서 새로 만든 데이터 구조가 문제 영역을 훨씬 간결하게 표현하는 새로운 추상 개념으로 격상되면서, 코드의 개념적인 그림을 다시 그릴 수도 있다.

8.1 매개변수 객체 만들기 절차

아래는 저자가 소개한 절차이다.

  • 절차

    1. 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다.

    2. 테스트한다.

    3. 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.

    4. 테스트한다.

    5. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트한다.

    6. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소로 사용하도록 바꾼다.

    7. 다 바꿨다면 기존 매개변수를 제거하고 테스트한다.

8.2 매개변수 객체 만들기 예시

const station = {    name:"123",    readings: [        {temp:47, time:100},        {temp:35, time:101},        {temp:30, time:102},    ]}/* 처음 */function readingOutsideRange(station, min, max) {    return station.readings.filter(r => r.temp < min || r.temp > max);}const alerts = readingOutsideRange(station, operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);/* 개선 후 + 매개변수 객체 활용 메서드 추가 */class NumberRange {    constructor(min, max) {        this._data = {min,max};    }    contains(value) {        return this._data.min <= value && value <= this._data.max ;    }}function readingOutsideRange(station, range) {    return station.readings.filter(r => !range.contains(r.temp));}const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);const alerts = readingOutsideRange(station, range);

9. 여러 함수를 클래스로 묶기

저자에 따르면 클래스는 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그중 일부를 외부에 제공하는 것이다. 저자는 함수 호출 시 인수로 전달되는 공통 데이터를 중심으로 긴밀하게 엮어 작동하는 함수 무리를 발견하면 클래스 하나로 묶고자 시도한다고 한다. 클래스로 묶으면 공유하는 공통 환경을 더 명확히 표현할 수 있고, 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있기 때문이다. 또한 이 객체는 다른 시스템에 전달하는 참조가 될수 도 있다.

물론 클래스 말고도 함수로 묶기도 가능하다. 클래스로 묶을 때는 클라이언트가 객체의 핵심 데이터를 변경할 수 있고, 파생 객체들을 일관되게 관리할 수 있다. 물론 중첩 함수 형태로 묶어도 되지만, 테스트할 때 까다로울 수 있다고 한다. 왜냐하면 외부에 공개할 함수가 여러 개일 때는 클래스를 사용할 수 밖에 없다고 저자가 언급했다. (요즘은 hooks 형태로 다 잘 묶는 것 같다.);

9.1 여러 함수를 클래스로 묶기 절차

아래는 저자의 절차이다.

  • 절차

    1. 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.

    2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다.(캡슐화한 레코드로)

    3. 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

9.2 여러 함수를 클래스로 묶기 예시

/* 적용 전 */const reading = {customer:"ivan", quantity: 10, month:5, year:2025};const baseCharge = baseRate(reading.month, reading.year) * reading.quantity;const texableCahrge = Math.max(0, baseCharge-taxThreshold(reading.year));/* 적용 후 */class Reading {    constructor(data) {        this._customer = data.customer;        this._quantity = data.quantity;        ...    }    get customer() {return this._customer}    ...    get baseCharge() {        return baseRate(this.month, this.yaer) * this.quantity);    }    get taxableChargeFn() {        return Math.max(0, this.baseCharge - taxThreshold(this.year);    }}const aReading = new Reading(reading);const taxableCharge = aReading.texableCharge;

10. 여러 함수를 변환 함수로 묶기.

이 리팩터링 방법은 클래스 묶기와 유사하다. 다만 어떠한 정보가 사용되는 곳마다 같은 도출 로직이 반복되는데, 이러한 도출 작업들을 하나로 모으는데 사용된다. 예를 들면 검색과 갱신을 일관된 장소에서 처리하며 로직 중복을 막을 수 있다.

원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤 각각을 출력 데이터의 필드에 넣어 반환하는 방식이다. 저자는 원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶는 것을 선호한다고 한다. (요즘은 리액트를 써서 커스텀 훅으로 잘 쓰는 것 같다.)

클래스로 묶는 것과 비슷해서 예시만 설명하고 넘어가겠다.

10.1 여러 함수를 변환 함수로 묶기 예시

/* 적용 전 */const reading = {customer:"ivan", quantity: 10, month:5, year:2025};const baseCharge = baseRate(reading.month, reading.year) * reading.quantity;const texableCharge = Math.max(0, baseCharge-taxThreshold(reading.year));/* 적용 후 */function enrichReading(original) {    const result = _.cloneDeep(original);    result.baseCharge = baseRate(reading.month, reading.year) * reading.quantity;    result.texableCharge = Math.max(0, baseCharge-taxThreshold(reading.year));    return result;}const aReading = enrichReading(reading);const texableCharge = aReading.texableCharge;

11. 단계 쪼개기

저자는 서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 개별 모듈로 나누는 방법을 모색한다고 한다. 코드를 수정할 때 두 대상을 동시에 생각할 필요 없이 하나에만 집중하기 위해서다. 이렇게 분리하는 가장 쉬운 방법은 두 단계로 나누는 것이다. 예를 들면 어떤 입력이 처리 로직에 적합하지 않다면 이를 단계를 나눠 다루기 편한 형태로 가공하는 단계로 분리하는 것이다.

11.1 단계 쪼개기 절차

아래는 저자의 단계 쪼개기 절차이다.

  • 절차

    1. 두 번째 단계에 해당하는 코드를 독립 함수로 추출한다.

    2. 테스트한다.

    3. 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수로 추가한다.

    4. 테스트한다.

    5. 추출한 두 번째 단계 함수의 매개변수를 하나씩 검토한다. 그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮긴다. 하나씩 옮길 때마다 테스트한다.

    6. 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만든다.

11.2 단계 쪼개기 예시

/* 쪼개기 전 */function priceOrder(product, quantity, shippingMethod) {    const basePrice = product.basePrice * quantity;    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;    const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ?                             shippingMethod.discountedFee : shippingMethod.feePerCase;    const shippingCost = quantity * shippingPerCase;    const price = basePrice - discount + shippingCost;    return price;}/* 쪼개기 중간 1 (독립 함수 추출) */function priceOrder(product, quantity, shippingMethod) {    const basePrice = product.basePrice * quantity;    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;    const price = applyShipping(basePrice, shippingMethod, quantity, discount);    return price;}function applyShipping(basePrice, shippingMethod, quantity, discount) {   const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ?                             shippingMethod.discountedFee : shippingMethod.feePerCase;    const shippingCost = quantity * shippingPerCase;    const price = basePrice - discount + shippingCost;    return price;}/* 쪼개기 중간 2 (중간 데이터 구조 추가) */function priceOrder(product, quantity, shippingMethod) {    const basePrice = product.basePrice * quantity;    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;    const priceData = {basePrice, quantity, discount};    const price = applyShipping(priceData, shippingMethod);    return price;}function applyShipping(priceData, shippingMethod) {   const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ?                             shippingMethod.discountedFee : shippingMethod.feePerCase;    const shippingCost = priceData.quantity * shippingPerCase;    const price = priceData.basePrice - priceData.discount + shippingCost;    return price;}/* 쪼개기 중간 3 (중간 데이터 구조 반환 추가) */function priceOrder(product, quantity, shippingMethod) {    const priceData = calculatePricingData(product, quantity);    const price = applyShipping(priceData, shippingMethod);    return price;}function calculatePricingData(product, quantity) {    const basePrice = product.basePrice * quantity;    const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;    return {basePrice, quantity, discount};}function applyShipping(priceData, shippingMethod) {   const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ?                             shippingMethod.discountedFee : shippingMethod.feePerCase;    const shippingCost = priceData.quantity * shippingPerCase;    const price = priceData.basePrice - priceData.discount + shippingCost;    return price;}/* 쪼개기 적용 후 기타 함수 생략. */function priceOrder(product, quantity, shippingMethod) {    const priceData = calculatePricingData(product, quantity);    return applyShipping(priceData, shippingMethod);}

12. 마무리

자바스크립트로 설명했지만 사실 자바를 사용하는 사람들에게 더 도움이 됐을 것 같습니다. 왜냐하면 현재 프론트엔드의 자바스크립트에서는 Class를 사용하여 인스턴스로 상태관리를 하기에는 isObject로 같은 객체인지 검사하여 상태 변화를 체크하는 React같은 프레임워크에서는 좀 힘들기 때문입니다. 변수 1개를 바꾸면 인스턴스를 다시 생성해서 값들을 넣어줘야 화면까지 반영이 되기 때문입니다..(직접 해보고 고생해봐서 알고 있습니다!! 아니면 MobX를 쓰던가...)

그래도 중요한건 어떻게 분리를 할까.. 어떻게 리팩토링을 할까 입니다. 말이 리팩터링이지 사실 클린코드나, 디자인 패턴 책을 보더라도 큰 줄기는 같은 것 같습니다... 그냥 리팩터링을 할때 단계별로 어떻게 적용할지를 잘 알려주는 책이 리팩터링 책인것 같습니다. 그대로 할 자신은 없습니다만.. 여러분들 함께 보면서 많은 공부 되셨길 빕니다!!