리팩터링은 코드를 깔끔하게 정리하는 일이 아니다

vx_developer·2026년 10월 12일

개발하다가

목록 보기
49/49
post-thumbnail

프로그래밍을 처음 배울 때 리팩터링은 보통 “동작하는 코드를 더 깔끔하게 정리하는 일”이라고 배운다.

const n = appointment.patientName;

의미가 드러나는 이름으로 바꿀 수 있다.

const patientName = appointment.patientName;

두 코드의 결과는 같다. 변수 이름만 읽기 쉽게 바뀌었다.

리팩터링의 기본 개념을 이해하기에는 충분한 설명이다. 불분명한 이름을 바꾸고, 긴 함수를 나누고, 중복 코드를 줄이는 일은 여전히 중요하다.

하지만 실제 서비스를 개발하면 더 많은 판단이 필요하다.

  • 코드의 동작이 바뀌지 않았다는 것을 어떻게 확인하는가?
  • 어떤 코드를 먼저 개선해야 하는가?
  • 중복처럼 보이는 규칙을 정말 하나로 합쳐도 되는가?
  • 리팩터링과 기능 변경을 한 작업에 섞어도 되는가?
  • 데이터베이스, 외부 API와 상태 변경이 포함된 코드는 어떻게 안전하게 바꾸는가?
  • 코드가 더 짧아졌지만 다음 요구사항을 추가하기 어려워졌다면 개선이라고 할 수 있는가?

리팩터링은 코드를 깔끔하게 정리하는 일이 아니라 현재 동작을 유지하면서 코드의 책임과 경계를 다시 설계해 다음 변경의 비용과 위험을 줄이는 작업이다.


코드를 읽기 쉽게 만드는 이유는 다음 변경을 준비하기 위해서다

크리스가 병원 예약 서비스를 개발한다고 생각해 보자.

환자가 진료 예약을 취소할 때 다음 함수가 실행된다.

async function cancelAppointment(
  appointmentId: string,
  patientId: string
) {
  const appointment =
    await appointmentRepository.findById(
      appointmentId
    );

  if (!appointment) {
    throw new Error("Appointment not found");
  }

  if (appointment.patientId !== patientId) {
    throw new Error("Forbidden");
  }

  if (appointment.status !== "confirmed") {
    throw new Error("Cannot cancel appointment");
  }

  const hoursUntilAppointment =
    (appointment.startsAt.getTime() - Date.now()) /
    (1000 * 60 * 60);

  if (hoursUntilAppointment < 24) {
    throw new Error("Cancellation period expired");
  }

  await appointmentRepository.update(
    appointmentId,
    { status: "cancelled" }
  );

  await notificationService.sendCancellation(
    appointment
  );
}

이 코드는 동작할 수 있다. 그러나 한 함수가 여러 책임을 맡고 있다.

  • 예약을 조회한다.
  • 요청한 환자의 예약인지 확인한다.
  • 현재 상태를 검사한다.
  • 취소 가능 시간을 계산한다.
  • 예약 상태를 저장한다.
  • 취소 알림을 발송한다.

취소 수수료, 병원별 정책, 의사 알림과 환불 기능이 추가되면 이 함수는 더 많은 이유로 변경된다.

문제는 줄 수가 길다는 사실 자체가 아니다. 서로 다른 정책과 외부 작업이 한곳에 묶여 있어 하나를 바꿀 때 나머지 동작까지 영향을 받을 수 있다는 점이다.

리팩터링은 현재 코드를 보기 좋게 만드는 데서 끝나지 않는다. 다음 요구사항이 들어왔을 때 어디를 변경해야 하는지 명확하게 만드는 작업이다.


무엇을 보존할지 모르면 안전하게 구조를 바꿀 수 없다

리팩터링은 외부에서 관찰할 수 있는 동작을 보존한다.

병원 예약 취소에서 보존해야 할 동작은 단순한 반환값만이 아니다.

입력 또는 상황보존해야 할 동작
존재하지 않는 예약Appointment not found 오류
다른 환자의 예약권한 오류
이미 완료되거나 취소된 예약취소 거부
시작까지 24시간 미만취소 거부
정상적인 취소상태를 cancelled로 변경
저장 완료 후취소 알림 발송
저장 실패알림을 발송하지 않음

이 경계를 테스트로 고정할 수 있다.

it("does not send a notification when saving fails", async () => {
  appointmentRepository.update.mockRejectedValue(
    new Error("Database unavailable")
  );

  await expect(
    cancelAppointment(
      confirmedAppointment.id,
      confirmedAppointment.patientId
    )
  ).rejects.toThrow("Database unavailable");

  expect(
    notificationService.sendCancellation
  ).not.toHaveBeenCalled();
});

이 테스트는 데이터베이스 저장에 실패했을 때 취소 알림이 먼저 발송되지 않는다는 동작을 보호한다.

테스트가 모든 오류를 막아 주는 것은 아니다. 그러나 무엇을 유지해야 하는지 표현하지 않은 채 구조를 크게 바꾸는 것보다 훨씬 안전한 출발점을 제공한다.

테스트가 부족한 오래된 코드라면 현재 동작을 기록하는 테스트부터 추가할 수 있다. 현재 동작이 이상적으로 보이지 않더라도 우선 그대로 기록하고, 버그 수정이나 정책 변경은 별도의 작업으로 분리하는 편이 낫다.


리팩터링과 동작 변경을 구분해야 실패 원인을 찾을 수 있다

크리스가 취소 제한을 24시간에서 12시간으로 바꾸면서 함수 구조까지 전부 변경한다고 생각해 보자.

변경 후 테스트가 실패하면 원인을 구분하기 어렵다.

  • 새로운 12시간 정책이 잘못되었는가?
  • 구조를 옮기는 과정에서 조건이 빠졌는가?
  • 저장과 알림의 순서가 달라졌는가?
  • 기존 테스트가 오래된 정책을 기대하고 있는가?

따라서 두 종류의 변경을 나누는 편이 좋다.

1. 기존 동작을 테스트로 고정한다.
2. 동작을 유지한 채 구조를 개선한다.
3. 리팩터링된 코드에서 새 정책을 적용한다.
4. 새 정책에 맞춰 테스트를 수정하거나 추가한다.

이 순서는 구조 변경과 기능 변경을 각각 검증할 수 있게 한다.

실제 커밋도 같은 기준으로 나눌 수 있다.

refactor: extract appointment cancellation policy
feat: allow cancellation until 12 hours before appointment

첫 번째 커밋은 동작을 유지하며 책임을 이동한다. 두 번째 커밋은 사용자가 경험하는 정책을 변경한다.

두 작업을 반드시 서로 다른 배포로 나눠야 하는 것은 아니다. 다만 코드 검토와 실패 분석에서 어떤 변경이 동작을 바꾸는지 구분할 수 있어야 한다.


긴 함수를 나누는 것보다 책임이 어디에 있어야 하는지 결정해야 한다

기존 함수에서 취소 가능 여부를 별도 함수로 옮길 수 있다.

function canCancelAppointment(
  appointment: Appointment,
  now: Date
) {
  if (appointment.status !== "confirmed") {
    return false;
  }

  const hoursUntilAppointment =
    (appointment.startsAt.getTime() -
      now.getTime()) /
    (1000 * 60 * 60);

  return hoursUntilAppointment >= 24;
}

이 함수는 예약 상태와 남은 시간을 이용해 취소 가능 여부를 계산한다.

단순히 코드 조각을 다른 위치로 옮긴 것처럼 보이지만 중요한 변화가 생긴다. 취소 정책이 데이터 조회와 알림 전송에서 분리되었다.

정책이 병원마다 달라진다면 시간 기준을 외부에서 받을 수 있다.

type CancellationPolicy = {
  minimumNoticeHours: number;
};

function canCancelAppointment(
  appointment: Appointment,
  policy: CancellationPolicy,
  now: Date
) {
  if (appointment.status !== "confirmed") {
    return false;
  }

  const hoursUntilAppointment =
    (appointment.startsAt.getTime() -
      now.getTime()) /
    (1000 * 60 * 60);

  return (
    hoursUntilAppointment >=
    policy.minimumNoticeHours
  );
}

이제 취소 가능 여부는 병원별 정책을 명시적인 입력으로 받는다.

예약 조회는 저장소의 책임이고, 취소 가능 여부는 도메인 정책이며, 알림 발송은 외부 서비스와의 통신이다. 함수를 나누는 기준은 코드 길이가 아니라 서로 다른 책임과 변경 이유다.


좋은 이름은 코드 모양보다 비즈니스 판단을 설명한다

다음 조건은 실행되지만 의미를 바로 파악하기 어렵다.

if (
  appointment.status === "confirmed" &&
  hours >= 24
) {
  await cancel();
}

이 코드에서는 24가 무엇을 의미하는지, 조건 전체가 어떤 정책을 표현하는지 알기 어렵다.

이름을 통해 서비스의 언어를 드러낼 수 있다.

const cancellationPolicy = {
  minimumNoticeHours: 24,
};

const isWithinCancellationPeriod =
  canCancelAppointment(
    appointment,
    cancellationPolicy,
    currentTime
  );

if (isWithinCancellationPeriod) {
  await cancelAppointment();
}

이 코드는 예약이 취소 가능 기간 안에 있는지를 판단한 뒤 취소 작업을 실행한다.

좋은 이름은 코드를 영어 문장처럼 보이게 만드는 장식이 아니다. 현실의 규칙이 코드에서 어떤 개념으로 표현되는지 보여주는 설계 도구다.


비슷하게 생긴 코드를 무조건 합치면 서로 다른 규칙이 묶인다

예약 취소와 일정 변경은 현재 같은 조건을 사용할 수 있다.

function canCancel(
  appointment: Appointment,
  now: Date
) {
  return (
    appointment.status === "confirmed" &&
    hoursUntil(appointment.startsAt, now) >= 24
  );
}

function canReschedule(
  appointment: Appointment,
  now: Date
) {
  return (
    appointment.status === "confirmed" &&
    hoursUntil(appointment.startsAt, now) >= 24
  );
}

두 함수의 구현은 현재 동일하다.

중복을 제거하기 위해 하나의 함수로 합치고 싶을 수 있다.

function canModifyAppointment(
  appointment: Appointment,
  now: Date
) {
  return (
    appointment.status === "confirmed" &&
    hoursUntil(appointment.startsAt, now) >= 24
  );
}

하지만 병원이 다음 달부터 취소는 24시간 전까지, 일정 변경은 12시간 전까지 허용할 수 있다.

현재의 코드 모양은 같지만 변경 이유는 다르다. 이런 중복을 성급하게 합치면 하나의 규칙을 수정할 때 관련 없는 정책까지 영향을 받는다.

리팩터링에서 중요한 질문은 “코드가 반복되는가?”만이 아니다.

이 코드들은 같은 이유로 함께 변경되는가?

변경 이유가 다르다면 약간의 중복이 잘못된 추상화보다 안전할 수 있다.


외부 입력의 검증 경계는 구조를 바꿔도 유지해야 한다

환자가 취소 요청을 보낼 때 예약 ID와 취소 이유가 외부에서 들어온다.

외부 입력은 검증 전까지 신뢰할 수 없다.

import { z } from "zod";

const CancelAppointmentSchema = z.object({
  appointmentId: z.string().uuid(),
  reason: z
    .enum([
      "schedule_conflict",
      "feeling_better",
      "other",
    ])
    .optional(),
});

const command =
  CancelAppointmentSchema.parse(request.body);

이 코드는 예약 ID의 형식과 허용된 취소 이유를 애플리케이션 경계에서 검증한다.

리팩터링 과정에서 타입이 이미 존재한다는 이유로 검증을 제거해서는 안 된다. TypeScript 타입은 실행 중 들어오는 JSON이 올바르다는 사실을 보장하지 않는다.

검증된 명령만 내부 로직으로 전달할 수 있다.

await appointmentCancellationService.cancel({
  appointmentId: command.appointmentId,
  patientId: currentUser.id,
  reason: command.reason,
  requestedAt: new Date(),
});

이 코드는 외부 입력 검증과 인증된 사용자 정보를 결합해 취소 서비스에 전달한다.

리팩터링은 경계를 없애는 일이 아니다. 어떤 데이터가 외부에서 들어오고, 어느 지점부터 내부에서 신뢰할 수 있는지를 더 명확하게 만드는 작업이다.


데이터 구조를 바꿀 때는 Source of Truth를 먼저 확인해야 한다

예약 가능 시간을 빠르게 표시하기 위해 availableSlots를 별도 배열로 저장하고 있다고 생각해 보자.

type DoctorSchedule = {
  appointments: Appointment[];
  availableSlots: Date[];
};

appointments와 availableSlots를 모두 수정해야 한다면 두 값이 서로 달라질 수 있다.

실제 예약이 Source of Truth라면 가능한 시간은 예약과 근무 시간에서 계산할 수 있다.

function calculateAvailableSlots(
  workingHours: TimeRange[],
  appointments: Appointment[]
) {
  return workingHours.filter((slot) =>
    appointments.every(
      (appointment) =>
        !overlaps(slot, appointment)
    )
  );
}

이 함수는 의사의 근무 시간과 현재 예약을 기준으로 가능한 시간을 계산한다.

계산 비용 때문에 결과를 캐시할 수는 있다. 그러나 캐시는 원본이 아니라 다시 만들 수 있는 파생 데이터라는 책임을 유지해야 한다.

리팩터링 중 데이터 필드를 제거하거나 계산 방식으로 바꾸기 전에는 다음 내용을 확인해야 한다.

  • 어떤 데이터가 비즈니스 사실의 원본인가?
  • 파생 값은 언제 다시 계산되는가?
  • 기존 API가 해당 필드에 의존하는가?
  • 데이터 마이그레이션이 필요한가?
  • 이전 버전과 새 버전이 동시에 실행될 수 있는가?

코드 구조만 바꾸는 작업이 데이터 의미까지 바꾸기 시작하면 단순한 리팩터링의 범위를 넘어선다. 이때는 마이그레이션과 호환성을 별도로 설계해야 한다.


부수 효과를 경계로 밀어내면 핵심 규칙을 검증하기 쉬워진다

데이터베이스 저장, 현재 시간 확인, 이메일 발송처럼 외부 상태와 연결된 작업은 테스트와 변경을 어렵게 만들 수 있다.

기존 함수는 Date.now()를 직접 사용한다.

function canCancelAppointment(
  appointment: Appointment
) {
  const hoursUntilAppointment =
    (appointment.startsAt.getTime() -
      Date.now()) /
    (1000 * 60 * 60);

  return hoursUntilAppointment >= 24;
}

실행 시점마다 결과가 달라지므로 경계 시간의 테스트가 불안정해질 수 있다.

현재 시간을 입력으로 전달할 수 있다.

function canCancelAppointment(
  appointment: Appointment,
  now: Date,
  minimumNoticeHours: number
) {
  const hoursUntilAppointment =
    (appointment.startsAt.getTime() -
      now.getTime()) /
    (1000 * 60 * 60);

  return (
    appointment.status === "confirmed" &&
    hoursUntilAppointment >=
      minimumNoticeHours
  );
}

이제 같은 입력에는 같은 결과가 나온다. 24시간 바로 전과 직후 같은 경계 조건도 정확히 테스트할 수 있다.

핵심 정책은 가능한 한 입력을 받아 결과를 계산하도록 만들고, 데이터베이스와 알림 같은 부수 효과는 작업의 바깥 경계에서 조정하는 편이 이해하기 쉽다.


리팩터링 이후의 코드는 다음 요구사항에서 평가된다

크리스가 취소 정책을 별도 객체로 분리했다고 생각해 보자.

class AppointmentCancellationPolicy {
  constructor(
    private readonly minimumNoticeHours: number
  ) {}

  evaluate(
    appointment: Appointment,
    requestedAt: Date
  ): CancellationDecision {
    if (appointment.status !== "confirmed") {
      return {
        allowed: false,
        reason: "invalid_status",
      };
    }

    if (
      hoursUntil(
        appointment.startsAt,
        requestedAt
      ) < this.minimumNoticeHours
    ) {
      return {
        allowed: false,
        reason: "notice_period_expired",
      };
    }

    return { allowed: true };
  }
}

이 객체는 취소 가능 여부뿐 아니라 거부 이유까지 비즈니스 의미로 반환한다.

그 결과 API는 정책을 다시 해석하지 않고 사용자에게 적절한 오류를 전달할 수 있다. 병원별 제한 시간도 생성 시점에 주입할 수 있다.

그러나 클래스를 만들었다는 사실만으로 설계가 좋아지는 것은 아니다. 다음 변경에서 실제로 확인해야 한다.

  • 새 정책을 한곳에서 추가할 수 있는가?
  • 기존 정책을 실수로 우회하기 어려운가?
  • 테스트가 필요한 경계가 명확한가?
  • 호출자가 내부 구현을 지나치게 알아야 하지 않는가?
  • 사용하지 않는 추상화가 추가되지 않았는가?

리팩터링의 결과는 변경 직후의 코드 모양보다 다음 변경을 얼마나 국소적으로 처리할 수 있는지에서 드러난다.


작은 단계는 실수를 발견할 수 있는 지점을 늘린다

전체 예약 모듈을 한 번에 다시 작성하면 중간에 동작을 확인하기 어렵다.

대신 다음처럼 작은 단계로 진행할 수 있다.

1. 기존 취소 동작을 테스트로 기록한다.
2. 시간 계산을 순수 함수로 추출한다.
3. 취소 가능 조건을 정책 함수로 추출한다.
4. 오류 이유를 명시적인 타입으로 표현한다.
5. 저장과 알림 순서를 서비스에 남긴다.
6. 각 단계마다 테스트와 타입 검사를 실행한다.
7. 동작이 같음을 확인한 뒤 다음 단계로 이동한다.

각 단계는 작지만 함께 적용하면 책임 구조가 크게 달라진다.

작은 변경은 코드 검토도 쉽게 한다. 검토자는 수백 줄의 이전 코드와 새 코드를 추측으로 비교하는 대신 한 번에 하나의 의도를 확인할 수 있다.

리팩터링 도구가 이름 변경과 함수 이동을 자동화해도 결과 검증까지 대신해 주지는 않는다. 자동화된 변환 이후에도 테스트, 타입 검사와 실제 호출 경계를 확인해야 한다.


모든 불편한 코드를 지금 리팩터링할 필요는 없다

읽기 어려운 코드를 발견했다고 해서 즉시 전체를 개선해야 하는 것은 아니다.

리팩터링에는 시간과 위험이 따른다. 우선순위는 코드의 모양보다 변경 가능성과 영향 범위를 기준으로 정할 수 있다.

코드 상황판단
자주 변경되고 장애가 반복됨우선적으로 개선할 가치가 큼
다음 기능이 같은 영역을 수정함기능 작업 전에 필요한 부분을 개선
여러 팀이 공통으로 사용함계약과 영향 범위를 먼저 확인
곧 제거될 코드큰 구조 개선보다 안전한 제거 준비
거의 변경되지 않는 안정된 코드불편하다는 이유만으로 재작성하지 않음
장애 대응 중인 코드우선 복구하고 별도 작업으로 개선

기능을 추가하다가 필요한 만큼 주변 코드를 개선하는 방식도 유용하다. 흔히 지나가는 곳을 조금 더 나은 상태로 남기는 접근이다.

다만 기능 변경과 관계없는 대규모 정리를 같은 작업에 포함하면 검토 범위가 커진다. 현재 요구사항을 안전하게 구현하는 데 필요한 범위를 중심으로 판단해야 한다.


리팩터링 전후에는 코드가 아니라 서비스의 약속을 확인해야 한다

테스트가 성공해도 실제 서비스의 모든 동작이 보존되었다고 단정할 수는 없다.

예약 취소 흐름에는 다음 검증이 함께 필요할 수 있다.

  • 정책 함수의 단위 테스트
  • 저장소와 트랜잭션의 통합 테스트
  • 기존 API 응답 형식을 확인하는 계약 테스트
  • 환자가 예약을 취소하는 E2E 테스트
  • 배포 후 취소 성공률과 오류 유형 확인
  • 알림 중복과 누락 모니터링

코드 커버리지 숫자가 높다는 사실보다 중요한 것은 중요한 동작과 실패 조건이 테스트되는지다.

리팩터링 후 오류율이나 처리 시간이 달라졌다면 외부 동작이 완전히 보존되지 않았을 수 있다. 이전 글에서 다룬 배포와 모니터링이 리팩터링의 안전장치로 다시 연결되는 이유다.


리팩터링을 시작하기 전에 물어봐야 할 질문

무엇을 보존해야 하는가

  1. 사용자가 현재 의존하는 동작은 무엇인가?
  2. 반환값 외에 저장, 알림과 외부 호출도 포함되는가?
  3. 오류 유형과 API 응답 형식도 계약의 일부인가?
  4. 현재 동작을 보여주는 테스트가 있는가?
  5. 의도하지 않았지만 호환성 때문에 유지해야 하는 동작이 있는가?

왜 지금 바꾸는가

  1. 다음 기능이 이 영역을 변경해야 하는가?
  2. 반복되는 장애나 수정 실수가 발생하는가?
  3. 여러 책임이 섞여 변경 범위가 커지는가?
  4. 리팩터링 비용이 앞으로 줄어들 변경 비용보다 작은가?
  5. 곧 제거될 코드를 불필요하게 다시 작성하고 있지 않은가?

책임과 경계가 명확한가

  1. 비즈니스 규칙은 어디에 있는가?
  2. 입력 검증은 어느 경계에서 수행하는가?
  3. 데이터 조회와 저장은 어느 계층이 담당하는가?
  4. 이메일과 외부 API 같은 부수 효과는 어디에서 실행하는가?
  5. 현재 시간과 환경 설정 같은 외부 값이 숨겨져 있지 않은가?

추상화가 변경 이유를 따르는가

  1. 단지 코드 모양이 같다는 이유로 합치고 있지 않은가?
  2. 함께 묶인 코드는 같은 이유로 변경되는가?
  3. 이름이 기술 구현보다 서비스 개념을 설명하는가?
  4. 호출자가 추상화의 내부를 지나치게 알아야 하지 않는가?
  5. 미래 요구사항을 추측해 사용하지 않는 구조를 만들고 있지 않은가?

데이터 의미를 유지하는가

  1. 비즈니스 데이터의 Source of Truth는 무엇인가?
  2. 계산하거나 캐시할 수 있는 파생 값은 무엇인가?
  3. 데이터 구조 변경이나 마이그레이션이 포함되는가?
  4. 이전 버전과 새 버전이 같은 데이터를 사용할 수 있는가?
  5. 동시 요청에서도 상태 규칙이 유지되는가?

변경을 검증하고 되돌릴 수 있는가

  1. 한 번에 하나의 작은 변환으로 나눌 수 있는가?
  2. 각 단계에서 테스트와 타입 검사를 실행하는가?
  3. 구조 변경과 기능 변경이 구분되는가?
  4. 배포 후 확인할 사용자 지표가 있는가?
  5. 문제가 생기면 이전 구현으로 안전하게 돌아갈 수 있는가?

흔한 실수는 더 짧은 코드를 더 좋은 코드라고 생각하는 것이다

테스트 없이 구조부터 크게 바꾼다

보존해야 할 동작이 명확하지 않으면 변경 후 무엇이 깨졌는지 알기 어렵다.

먼저 핵심 동작과 실패 조건을 테스트로 기록해야 한다.

기능 변경과 리팩터링을 한 번에 섞는다

테스트 실패와 운영 문제의 원인을 구분하기 어려워진다.

동작을 보존하는 단계와 동작을 변경하는 단계를 구분하는 편이 낫다.

모든 중복을 하나의 함수로 합친다

현재 모양이 같아도 서로 다른 정책은 다른 시점에 변경될 수 있다.

중복 여부보다 변경 이유가 같은지 확인해야 한다.

클래스를 늘리면 설계가 좋아진다고 생각한다

작은 규칙마다 인터페이스와 클래스를 만들면 이동해야 할 파일만 늘어날 수 있다.

실제 책임과 변형 지점이 있을 때 추상화를 추가해야 한다.

전체 모듈을 새로 작성한다

기존 코드에 숨어 있던 예외 처리와 호환 동작을 놓치기 쉽다.

작은 변환을 반복하며 각 단계에서 동작을 확인하는 편이 안전하다.

타입이 있으므로 검증을 제거한다

TypeScript 타입은 외부 JSON이나 데이터베이스의 오래된 값까지 검증하지 않는다.

외부 입력과 신뢰할 수 없는 데이터의 런타임 검증은 유지해야 한다.

코드만 보고 성공을 판단한다

테스트는 통과하지만 API 계약, 데이터 저장 순서나 운영 지표가 달라질 수 있다.

서비스 수준의 테스트와 배포 후 관찰이 함께 필요하다.


리팩터링의 목적은 다음 변경을 안전하게 만드는 데 있다

입문 단계에서는 리팩터링을 변수 이름을 바꾸고 긴 함수를 나누며 중복을 제거하는 작업으로 이해해도 충분하다.

const patientName =
  appointment.patientName;

이 코드는 n보다 값의 의미를 분명하게 전달한다.

그러나 실제 서비스에서는 코드가 보기 좋아졌다는 사실만으로 리팩터링의 성공을 판단할 수 없다.

  • 보존해야 할 외부 동작을 알고 있는가?
  • 현재 동작을 테스트로 확인할 수 있는가?
  • 기능 변경과 구조 변경을 구분했는가?
  • 비즈니스 규칙과 외부 작업의 책임이 나뉘어 있는가?
  • 추상화가 실제 변경 이유를 따르는가?
  • 입력 검증과 Source of Truth가 유지되는가?
  • 작은 단계마다 동작을 검증했는가?
  • 다음 요구사항을 더 작은 범위에서 구현할 수 있는가?

리팩터링은 코드를 깔끔하게 정리하는 일이 아니다.

리팩터링은 현재 동작을 유지하면서 코드의 책임과 경계를 다시 설계해 다음 변경의 비용과 위험을 줄이는 작업이다.


어떤 자료를 참고했는가

profile
Vision eXperience Developer

0개의 댓글