200줄 메서드를 파사드 패턴으로 정복하기
TL;DR
- 문제: 200줄짜리 메서드, 스크롤 끝이 안 보임, 수정할 때마다 겁남, 6개월 후 이해하는 데 30분 소요
- 원인: 외부 API 호출 + 검증 + 변환 + 저장 + 예외 처리가 한 메서드에 몰려 있음
- 해결: 파사드 패턴 - 큰 메서드를 작은 서비스로 쪼개고, Facade가 조합 담당
- 효과: 200줄 → 30줄, 읽는 시간 30분 → 5분, 테스트 쉬움 (각 서비스 독립)
- 한계: 초기 분리 시간 (1주), 클래스 수 증가, 팀 학습 곡선, 간단한 로직엔 오버
글 머리말
코드 리뷰 때 팀원이 제 화면을 보더니 이렇게 말했습니다.
“이 메서드… 스크롤이 언제 끝나요?”
200줄짜리 메서드였습니다. 외부 시스템에서 데이터를 받아와서 우리 DB에 저장하는 로직인데, 비즈니스 규칙이 복잡하다 보니 if문과 예외 처리가 중첩되면서 점점 비대해졌죠.
“이거 수정할 때마다 겁나요. 뭘 건드리면 어디가 터질지 모르겠어요.”
팀원의 말이 뼈아프게 다가왔습니다. 저도 이 코드를 6개월 만에 다시 보면 30분은 들여다봐야 이해가 되거든요.
그날 저녁, 구글에 “복잡한 메서드 리팩토링”을 검색했습니다. 스택오버플로우, 기술 블로그를 뒤지다가 파사드 패턴(Facade Pattern)이라는 키워드를 발견했습니다.
“복잡한 서브시스템을 간단한 인터페이스로 감싸는 패턴”
딱 우리 상황에 맞는 것 같았습니다.
이번 포스팅에서는 실무 코드에 파사드 패턴을 적용해서 200줄 메서드를 정복한 경험을 공유합니다.
문제: 200줄 메서드의 고통
실제 코드
실제 운영 중인 시 스템의 코드입니다 (도메인은 일반화했습니다).
외부 자산관리 시스템에서 주문 데이터를 받아와 우리 DB에 동기화하는 메서드였습니다.
@Synchronized
public void syncExternalOrder(OrderSyncRequest req) throws SystemException {
try {
// 1. 외부 API 호출
List<Map<String, Object>> res = callExternalApi(API_PATH, req);
List<ExternalOrder> orders = convertToOrders(res);
if (!CollectionUtils.isEmpty(orders)) {
// 2. 연관 데이터 조회
req.setResourceId(orders.get(0).getResourceId());
Optional<Resource> resourceOpt = resourceRepository.findByExternalId(req.getResourceId());
if (resourceOpt.isEmpty() || resourceOpt.get().getStatus().equals("INACTIVE")) {
throw new SystemException(ErrorCode.RESOURCE_NOT_FOUND);
}
// 3. 사이트 조회
Optional<Site> siteOpt = siteRepository.findByExternalId(orders.get(0).getSiteCode());
Site site = siteOpt.orElseThrow(() -> new SystemException(ErrorCode.SITE_NOT_FOUND));
// 4. 민감정보 복호화 (6개 필드)
orders.get(0).setAccountNo(decrypt(orders.get(0).getAccountNo()));
orders.get(0).setMonthlyAccountNo(decrypt(orders.get(0).getMonthlyAccountNo()));
orders.get(0).setAccountName(decrypt(orders.get(0).getAccountName()));
orders.get(0).setBankName(decrypt(orders.get(0).getBankName()));
orders.get(0).setMonthlyBankName(decrypt(orders.get(0).getMonthlyBankName()));
orders.get(0).setMonthlyAccountName(decrypt(orders.get(0).getMonthlyAccountName()));
// 5. Order 엔티티 생성
Order order = new Order(orders.get(0), site);
// 6. 레거시 시스템 매핑 처리 (중첩 if 3단계)
if (StringUtils.isNotEmpty(orders.get(0).getLegacyOrderId()) &&
!orders.get(0).getLegacyOrderId().equalsIgnoreCase("0")) {
Optional<Order> existOrder = orderRepository.findByGroupId(orders.get(0).getGroupId());
if (existOrder.isPresent()) {
order.setId(existOrder.get().getId());
order.setMigrated(true);
} else {
Optional<Order> legacyOrder = orderRepository.findByLegacyId(orders.get(0).getLegacyOrderId());
if (legacyOrder.isPresent()) {
order.setId(legacyOrder.get().getId());
order.setMigrated(true);
} else {
throw new SystemException(ErrorCode.ORDER_NOT_FOUND);
}
}
}
// 7~16. 계속 이어짐... (총 200줄)
// - 보증금 계산 (for문)
// - 주문 상태 업데이트 (복잡한 if)
// - 버전 비교 및 데이터 보정
// - 특수 계약 타입 처리
// - DB 저장
// - 프리 기간 처리
// - 중개 정보 처리
}
} catch (SystemException e) {
log.error("System error: {}", e);
throw new SystemException(e.getErrorCode());
} catch (Exception e) {
log.error("Unexpected error: {}", e);
throw new SystemException(ErrorCode.BAD_REQUEST);
}
}이 코드의 문제점
1. 인지 부하가 너무 높습니다
- 한 메서드가 16가지 일을 처리합니다
- 코드를 읽다 보면 “지금 몇 번째 단계였지?” 헷갈립니다
2. 수정할 때마다 두렵습니다
- 보증금 계산 로직만 수정하려고 해도, 앞뒤 200줄을 다 읽어야 합니다
- 다른 곳에 영향을 줄까 봐 조심스럽습니다
3. 테스트가 사실상 불가능합니다
- 이 메서드를 테스트하려면 외부 API부터 DB까지 모든 게 준비돼야 합니다
- 보증금 계산 로직만 따로 테스트할 수가 없습니다
4. 재사용이 안 됩니다
- 민감정보 복호화 로직이 필요한 다른 곳에서는요?
- 200줄을 복붙할 순 없으니 결국 중복 코드가 생깁니다
배경: 파사드 패턴이란
핵심 개념
파사드(Facade)는 “건물의 정면”이라는 뜻입니다.
건물 내부는 복잡한 배관, 전선, 구조물이 얽혀 있지만, 밖에서 보면 깔끔한 외관만 보이죠.
복잡한 서브시스템들을 하나의 단순한 인터페이스로 감싸는 패턴입니다.
클라이언트 → 파사드 (간단) → 복잡한 서브시스템들건물 외관(Facade)이 내부의 복잡한 배관과 구조를 감추는 것과 같습니다.
전통적 파사드 vs 실무 파사드
GoF 디자인 패턴 책에 나오는 전통적인 구조입니다.
// 복잡한 서브시스템들
class SecurityService { void decrypt(String data) { ... } }
class ValidationService { void validate(Order order) { ... } }
class NotificationService { void sendNotification() { ... } }
class DatabaseService { void save(Order order) { ... } }
// 전통적 파사드 - 별도 클래스
class OrderFacade {
private SecurityService security;
private ValidationService validator;
private NotificationService notifier;
private DatabaseService database;
public void processOrder(Order order) {
security.decrypt(order);
validator.validate(order);
database.save(order);
notifier.sendNotification();
}
}별도 클래스를 만들지 않고, 같은 클래스 내에서 메서드로 분리해도 파사드 패턴의 효과를 얻을 수 있습니다.
// 인라인 파사드 - 메서드 분리만으로 충분
public void syncExternalOrder() {
decryptSensitiveFields(); // 서브시스템 1
validateData(); // 서브시스템 2
computeAmounts(); // 서브시스템 3
updateState(); // 서브시스템 4
saveToDatabase(); // 서브시스템 5
}
private void decryptSensitiveFields() { /* 복잡한 로직 */ }장점:
- 별도 클래스 불필요
- 의존성 주입 불필요
- 빠른 리팩토링 가능
핵심: 추상화 수준의 통일
파사드 패턴의 핵심은 “추상화 수준의 통일”입니다.
// Bad - 추상화 수준이 섞여 있음 (저수준 + 고수준)
public void processOrder() {
String decrypted = aesEncrypt.aesDecode(data, key); // 저수준
validateOrder(order); // 고수준
order.setStatus("COMPLETED"); // 저수준
orderRepository.save(order); // 고수준
}
// Good - 파사드 패턴 (모두 고수준)
public void processOrder() {
decryptSensitiveData(); // 고수준
validateOrder(); // 고수준
updateOrderStatus(); // 고수준
saveOrder(); // 고수준
}이렇게 하면 메인 메서드는 “무엇을 하는가”만 보여주고, “어떻게 하는가”는 각 서브시스템에 위임하게 됩니다. 코드를 읽는 사람은 메인 메서드만 봐도 전체 흐름을 파악할 수 있죠.
리팩토링: 단계별 적용
2단계: 조회 로직 추출
배경
예외 처리가 포함된 조회 로직을 메서드로 추출합니다. “유효한 리소스를 찾는다”는 하나의 책임으로 캡슐화합니다.
코드
private Resource findValidResource(String externalId) {
Resource resource = resourceRepository.findByExternalId(externalId)
.orElseThrow(() -> new SystemException(ErrorCode.RESOURCE_NOT_FOUND));
if (resource.getStatus().equals("INACTIVE")) {
throw new SystemException(ErrorCode.RESOURCE_INACTIVE);
}
return resource;
}
private Site findSite(String siteCode) {
return siteRepository.findByExternalId(siteCode)
.orElseThrow(() -> new SystemException(ErrorCode.SITE_NOT_FOUND));
}3단계: 반복 로직 메서드로 감싸기
배경
민감정보 복호화처럼 반복되는 패턴을 메서드로 감쌉니다.
코드
private void decryptSensitiveFields(ExternalOrder order) {
order.setAccountNo(decrypt(order.getAccountNo()));
order.setMonthlyAccountNo(decrypt(order.getMonthlyAccountNo()));
order.setAccountName(decrypt(order.getAccountName()));
order.setBankName(decrypt(order.getBankName()));
order.setMonthlyBankName(decrypt(order.getMonthlyBankName()));
order.setMonthlyAccountName(decrypt(order.getMonthlyAccountName()));
}효과
나중에 복호화 필드가 추가되면 이 메서드만 수정하면 됩니다.
4단계: 복잡한 조건문 분리
배경
중첩된 if문 3단계를 독립된 메서드로 분리합니다.
코드
private void mapLegacyOrder(ExternalOrder external, Order order) {
if (StringUtils.isEmpty(external.getLegacyOrderId()) ||
external.getLegacyOrderId().equalsIgnoreCase("0")) {
return;
}
Optional<Order> existOrder = orderRepository.findByGroupId(external.getGroupId());
if (existOrder.isPresent()) {
order.setId(existOrder.get().getId());
order.setMigrated(true);
return;
}
Order legacyOrder = orderRepository.findByLegacyId(external.getLegacyOrderId())
.orElseThrow(() -> new SystemException(ErrorCode.ORDER_NOT_FOUND));
order.setId(legacyOrder.getId());
order.setMigrated(true);
}효과
“레거시 주문을 매핑한다”는 명확한 책임. 이 로직이 실패하면 문제가 어디인지 바로 알 수 있습니다.
5단계: 비즈니스 로직 분리
배경
보증금 계산처럼 핵심 비즈니스 로직을 분리합니다. 이제 이 로직만 따로 테스트할 수 있습니다.
코드
private void computeDepositAmounts(ExternalOrder external, Order order) {
order.setContractAmount(0L);
order.setRemainingAmount(0L);
for (ExternalOrder.DepositItem deposit : external.getDeposits()) {
String type = deposit.getType().replaceAll(" ", "");
long amount = Long.parseLong(deposit.getAmount());
if (type.equalsIgnoreCase("계약금")) {
order.setContractAmount(order.getContractAmount() + amount);
}
if (type.equalsIgnoreCase("잔금")) {
order.setRemainingAmount(order.getRemainingAmount() + amount);
}
}
}테스트
@Test
void 보증금_계산_테스트() {
// Given
ExternalOrder external = createTestData();
Order order = new Order();
// When
computeDepositAmounts(external, order);
// Then
assertThat(order.getContractAmount()).isEqualTo(10_000_000L);
assertThat(order.getRemainingAmount()).isEqualTo(5_000_000L);
}6단계: 상태 관리 로직 분리
배경
주문 상태 전이 로직은 비즈니스 규칙이 복잡하게 얽혀 있습니다. 이런 로직을 메인 메서드에 두면 전체 흐름을 파악하기 어려워집니다.
코드
private void updateOrderState(Order order, Optional<Order> existingOrder) {
if (existingOrder.isEmpty()) {
return;
}
Order existing = existingOrder.get();
// 취소/종료 상태는 유지
if (StringUtils.equalsAnyIgnoreCase(existing.getState(),
OrderState.CANCELLED.getCode(),
OrderState.TERMINATED.getCode())) {
order.setState(existing.getState());
return;
}
// 완료 상태인데 문서가 없으면 대기 상태로
if (order.getState().equalsIgnoreCase(OrderState.COMPLETED.getCode())
&& !order.hasDocument()
&& StringUtils.equalsAnyIgnoreCase(existing.getState(),
OrderState.PENDING.getCode(),
OrderState.ON_HOLD.getCode())) {
order.setState(OrderState.PENDING.getCode());
}
}효과
복잡한 상태 전이 로직을 한곳에 모았습니다. 주석 없이도 비즈니스 규칙이 보입니다.
결과: 깔끔한 파사드 메서드
@Synchronized
public void syncExternalOrder(OrderSyncRequest req) throws SystemException {
try {
List<Map<String, Object>> res = callExternalApi(API_PATH, req);
List<ExternalOrder> orders = convertToOrders(res);
if (CollectionUtils.isEmpty(orders))
return;
ExternalOrder externalOrder = orders.get(0);
req.setResourceId(externalOrder.getResourceId());
Resource resource = findValidResource(req.getResourceId());
Site site = findSite(externalOrder.getSiteCode());
// 민감정보 복호화
decryptSensitiveFields(externalOrder);
Order order = new Order(externalOrder, site, resource);
// 레거시 매핑
mapLegacyOrder(externalOrder, order);
// 리소스 ID 설정
order.setResourceId(resource.getId());
// 보증금 계산
computeDepositAmounts(externalOrder, order);
Optional<Order> existingOrder = orderRepository.findById(order.getId());
// 주문 상태 업데이트
updateOrderState(order, existingOrder);
// 시작일 보정
adjustStartDate(order, existingOrder);
// 버전 비교 및 데이터 조정
existingOrder.ifPresent(existing -> adjustOrderDetails(order, existing));
// 특수 계약 처리
handleSpecialLeaseType(order, externalOrder);
// 주문 저장
saveOrder(order, externalOrder);
// 연관 데이터 동기화
syncRelatedData(req);
// 프리 기간 처리
processFreeSchedules(externalOrder, order);
// 중개 정보 처리
processBrokerage(req, order.getId(), externalOrder);
} catch (SystemException e) {
log.error("System error: {}", e);
throw new SystemException(e.getErrorCode());
} catch (Exception e) {
log.error("Unexpected error: {}", e);
throw new SystemException(ErrorCode.BAD_REQUEST);
}
}파사드 구조
graph TD
A[syncExternalOrder<br/>파사드] --> B[decryptSensitiveFields<br/>보안]
A --> C[computeDepositAmounts<br/>비즈니스 로직]
A --> D[updateOrderState<br/>상태 관리]
A --> E[adjustOrderDetails<br/>데이터 조정]
A --> F[processFreeSchedules<br/>스케줄 처리]
A --> G[processBrokerage<br/>중개 정보]
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#fff3e0
style D fill:#fff3e0
style E fill:#fff3e0
style F fill:#fff3e0
style G fill:#fff3e0클라이언트는 syncExternalOrder() 하나만 호출하면 됩니다. 내부에서 복잡한 서브시스템들이 돌아가는지 알 필요가 없습니다. 이게 파사드 패턴의 핵심이죠.
효과: Before vs After
메트릭 비교
리팩토링 후 체감한 변화를 정리해봤습니다. 정확한 측정치라기보다는 실제로 작업하면서 느낀 수준입니다.
| 항목 | Before | After | 체감 |
|---|---|---|---|
| 메서드 길이 | 200줄 | 약 50줄 | 4분의 1로 ↓ |
| 중첩 깊이 | 4단계 | 1단계 | 거의 제거 |
| 이해 시간 | 30분 | 3분 정도 | 10분의 1 ↓ |
| 수정 시간 | 30분 | 5분 내외 | 6배 ↓ |
| 테스트 가능 단위 | 1개 | 10개 | 10배 ↑ |
5가지 핵심 효과
1. 복잡도가 감춰졌다
- 메인 메서드만 보고 전체 흐름을 파악할 수 있습니다
- “어떻게”는 몰라도 “무엇을” 하는지 명확합니다
2. 변경 지점이 명확해졌다
- “보증금 계산이 이상해요” →
computeDepositAmounts()찾고 수정 - 더 이상 200줄을 뒤질 필요가 없습니다
3. 독립적 테스트가 가능해졌다
- 각 서브시스템을 DB나 외부 API 없이 테스트할 수 있습니다
- 단위 테스트 작성이 훨씬 쉬워졌습니다
4. 수정이 안전해졌다
- 프리 기간 로직 수정 시 다른 서브시스템에 영향을 주지 않습니다
- 느슨한 결합으로 변경 리스크가 줄었습니다
5. 재사용 가능성이 늘었다
- 민감정보 복호화 로직을 다른 곳에서도 사용할 수 있습니다
- 필요하면 별도 클래스로 쉽게 추출할 수 있습니다
내 프로젝트에 바로 적용하기
체크리스트
- 50줄 이상의 메서드가 있는가?
- 메서드가 3가지 이상의 일을 하는가?
- 주석이 필요한 블록이 2개 이상인가?
- 중첩 if문이 3단계 이상인가?
- 수정할 때 두려움이 느껴지는가?
하나라도 Yes라면 파사드 패턴 적용을 고려해볼 만합니다.
리팩토링 순서
1단계: 테스트 코드 작성
리팩토링 전에 기존 로직을 검증하는 테스트를 작성합니다. 리팩토링 후에도 같은 테스트가 통과하면 안전하다는 확신을 가질 수 있습니다.
2단계: 큰 덩어리로 분리
한 번에 다 하려고 하면 오히려 더 복잡해집니다. 큰 덩어리 3~4개로 먼저 분리합니다.
- 조회 로직
- 비즈니스 로직
- 저장 로직
3단계: 세부 서브시스템 분리
각 덩어리를 다시 2~3개씩 분리합니다.
- 민감정보 복호화
- 보증금 계산
- 상태 업데이트
4단계: 메서드 이름 다듬기
팀원에게 물어보는 것도 좋은 방법입니다. “이 메서드 이름 이해 되세요?” 혼자서는 당연해 보이는 이름도 다른 사람한테는 애매할 수 있거든요.
주의사항
너무 잘게 쪼개면 안 됩니다
// Bad - 과도하게 분해
validateStep1();
validateStep2();
validateStep3();
processStep1();
processStep2();
processStep3();의미 있는 단위로 묶어야 합니다
// Good - 적절한 분리
validateBusinessRules();
processOrderData();추천 설정
즉시 분리하는 경우:
- 중복 코드가 2번 이상 나타날 때
- 중첩이 3단계 이상일 때
분리를 고려하는 경우:
- 한 메서드가 50줄 이상일 때
- 주석이 필요한 블록이 있을 때
분리하지 않는 경우:
- 관련 있는 로직이 10줄 이하일 때
- 한 번만 사용되는 간단한 로직일 때
트러블슈팅
Q. “메서드가 너무 많아진 것 같아요”
- 저도 처음엔 그렇게 느꼈습니다. 하지만 메인 메서드만 보면 전체 흐름이 한눈에 들어옵니다
- 세부 구현이 궁금하면 그때 해당 메서드로 들어가면 되니까요
Q. “성능이 느려지지 않나요?”
- 실제로 측정해 보면 무시할 수준입니다
- JVM이 인라인 최적화를 잘 해주거든요
- 경험상 성능보다 가독성이 더 중요한 경우가 많았습니다
Q. “언제 별도 클래스로 분리하나요?”
- 서브시스템이 여러 곳에서 재사용되는 경우
- 독립적인 단위 테스트가 필요한 경우
- 저는 일단 인라인으로 시작해서, 필요하면 추출하는 편입니다
이 접근의 아쉬운 점
1. 초기 리팩토링 시간 (1주)
200줄 메서드를 20개 메서드로 분리하는 데 일주일이 걸렸습니다.
문제:
- 기존 기능 유지하면서 분리 (회귀 테스트 필수)
- 적절한 메서드명 고민
- 팀 리뷰 시간
만약 급한 마감이 있었다면?
리팩토링은 나중으로 미뤘을 겁니다. 기술 부채로 남았겠죠.
2. 메서드 수 폭발
Before: 1개 메서드 (200줄)
After: 10~20개 메서드 (각 10~20줄)
팀원: “메서드가 너무 많은 거 아닌가요?”
딜레마:
- 메서드 많음 = 처음 보는 사람은 혼란
- vs 긴 메서드 = 읽기 힘듦
우리 선택:
“메인 메서드만 보면 전체 흐름 파악” 방향으로 설득
3. 파사드 vs 단순 분리
사실 이 리팩토링은 “파사드 패턴”이라기보다 “메서드 추출”에 가깝습니다.
진짜 파사드 패턴:
// 별도 Facade 클래스
public class OrderSyncFacade {
private final ExternalApiService api;
private final ValidationService validation;
private final TransformService transform;
// ...
}우리 방식:
// 같은 클래스 내 private 메서드 분리
public class OrderService {
private void fetchExternalData() {}
private void validateOrder() {}
// ...
}왜 Facade 클래스를 안 만들었나?
- 이 로직이
OrderService의 핵심 책임임 - 별도 클래스는 오버엔지니어링
- 팀 규모 작음 (백엔드 3명)
4. 간단한 로직엔 오버
50줄 미만 메서드는 굳이 분리할 필요 없습니다.
예를 들어:
// 이 정도는 분리 안 해도 됨
public void simpleMethod() {
validate();
process();
save();
}분리 기준:
- 100줄 이상: 거의 필수
- 50~100줄: 상황 봐서
- 50줄 미만: 굳이 불필요
5. 결국 “적재적소”
파사드 패턴(메서드 분리)은:
- ✅ 긴 메서드 (100줄 이상)
- ✅ 복잡한 로직 (if 중첩, 예외 처리 많음)
- ✅ 자주 수정되는 코드
- ❌ 짧고 간단한 메서드
- ❌ 일회용 스크립트
만약 코드를 다시 안 볼 거라면?
리팩토링 안 해도 됩니다. 하지만 6개월 후에 다시 보게 됩니다.
지금은 이 정도로 충분합니다.
시스템 점검 체크리스트
저도 리팩토링할 때 이 항목들을 확인합니다. 파사드 패턴으로 메서드 분리를 고려한다면 참고하시면 좋을 것 같습니다.
- 메서드 길이: 100줄 이상의 메서드가 있는가? (분리 우선 대상)
- 의미 있는 이름: 추출한 메서드명이 “무엇을 하는지” 명확하게 표현하는가?
- 단일 책임: 각 메서드가 하나의 책임만 가지는가? (조회, 검증, 변환, 저장 등 분리)
- 테스트 가능성: 메인 메서드 없이도 개별 서브 메서드를 테스트할 수 있는가?
- 전체 흐름 파악: 파사드 메서드만 읽어도 전체 흐름이 이해되는가?
마무리
두 달 후, 프리 기간 계산 로직에 새로운 규칙을 추가해야 했습니다.
과거의 저라면 200줄 코드를 30분 동안 읽고, 어디를 고쳐야 할지 찾고, 조심스럽게 수정했을 겁니다. 하지만 이번엔 달랐습니다.
파사드 메서드 syncExternalOrder()를 열고, “아, processFreeSchedules() 여기구나” 3초 만에 찾았습니다. 해당 서브시스템 메서드만 수정하고, 5분 만에 끝냈죠.
파사드 패턴의 진짜 가치는 “복잡도를 숨기는 것”입니다.
디자인 패턴은 교과서 속에만 있지 않다
솔직히 처음엔 이런 생각이 들었습니다. “디자인 패턴? 그거 면접 때나 나오는 거 아냐?”
유튜브에서 본 클린 코드 영상에서도 항상 별도 Facade 클래스를 만들더군요. 근데 실무는 달랐습니다. 매번 새 클래스를 만들 필요가 없었습니다. 같은 클래스 내에서 메서드로 분리하는 것만으로도 파사드 패턴의 효과를 충분히 얻을 수 있었거든요.
디자인 패턴은 “정답”이 아니라 “가이드라인 ”입니다. 상황에 맞게 변형하고 적용하는 게 진짜 실력이라고 생각합니다.
한 달 후, 팀원의 반응
리팩토링 PR을 올렸을 때 팀원이 이렇게 댓글을 달았습니다.
“메서드가 너무 많은 거 아닌가요? 오히려 복잡해 보이는데요.”
맞는 말이었습니다. 메서드가 10개에서 20개로 늘어났으니까요. 하지만 저는 이렇게 답했습니다.
“메인 메서드만 보세요. 전체 흐름이 한눈에 들어오지 않나요? 세부 구현이 궁금하면 그때 해당 메서드로 들어가면 됩니다.”
그리고 한 달 후, 그 팀원이 이 코드를 수정하게 됐습니다.
“아… 이래서 나눈 거였구나. 수정할 곳이 바로 보이네요. 다른 로직 안 건드려도 되니까 안전하고요.”
좋은 코드는 시간이 지나면 증명됩니다.
작은 프로젝트에도 필요할까?
“우리 프로젝트는 작은데, 이렇게까지 해야 하나요?” 이런 질문을 받은 적이 있습니다.
저는 “작은 프로젝트일수록 더 필요하다”고 생각합니다.
작은 프로젝트는 빠르게 커집니다. 그리고 기술 부채는 복리로 쌓이죠. 처음부터 깔끔하게 유지하는 게 나중에 대대적 리팩토링하는 것보다 훨씬 쉽습니다.
저희 서비스도 작은 규모의 서비스입니다. 하지만 “작으니까 대충 짜도 돼”가 아니라, “작으니까 더 견고하게 짜야 해”라고 생각합니다. 확장할 때 코드 품질이 발목을 잡으면 안 되니까요.
결국 작은 개선의 힘
200줄짜리 “괴물 메서드”를 처음 마주했을 때는 막막했습니다. 어디서부터 손을 대야 할지 감이 안 왔거든요.
하지만 결국은 간단했습니다.
- 메인 로직을 3~4개 덩어리로 분리
- 각 덩어리에 의미 있는 이름 붙이기
- private 메서드로 추출
저도 처음엔 “이 정도면 됐나?”라고 불안했습니다. 하지만 시간이 지나면서 깨달았습니다.
작은 개선도 쌓이면 큰 차이를 만듭니다. 파사드 패턴은 어렵지 않습니다. 복잡한 것을 감추는 간단한 인터페이스를 만드는 것, 그게 전부입니다.
참고 :
https://refactoring.guru/design-patterns/facade
https://stackoverflow.com/questions/tagged/facade-pattern
리팩토링 2판 (마틴 파울러)
클린 코드 (로버트 C. 마틴)
