[spring] 언제! 리팩토링해야 할까? 실전 가이드-유지보수성 문제 (파트 3/3)

2025. 10. 5. 22:58·공부일기../Spring

유지보수성 문제

9. switch/if-else 분기가 계속 늘어나는 경우

문제: 타입별 분기 처리
해결: Factory 패턴 + Enum 활용
효과: 새 타입 추가 시 분기문 수정 불필요

 

문제 상황:

public void processPayment(String paymentType, int amount) {
    if (paymentType.equals("CARD")) {
        // 카드 결제 로직
        CardAPI cardApi = new CardAPI();
        cardApi.charge(amount);
    } else if (paymentType.equals("BANK")) {
        // 계좌이체 로직
        BankAPI bankApi = new BankAPI();
        bankApi.transfer(amount);
    } else if (paymentType.equals("KAKAO")) {
        // 카카오페이 로직
        KakaoAPI kakaoApi = new KakaoAPI();
        kakaoApi.pay(amount);
    } else if (paymentType.equals("NAVER")) {
        // 네이버페이 로직
        NaverAPI naverApi = new NaverAPI();
        naverApi.payment(amount);
    }
}

새 결제 수단이 추가될 때마다 if문이 늘어난다.

10개가 되면 감당이 안 된다.

 

사고 과정:

  1. 타입별 처리 로직이 분산되어 있다
  2. Enum과 Factory 패턴으로 관리한다
  3. 각 타입별 처리를 캡슐화한다

해결:

// Enum으로 타입 관리
public enum PaymentType {
    // 각 상수가 추상 메서드를 구현
    CARD {
        @Override
        public PaymentProcessor createProcessor() {
            return new CardPaymentProcessor();
        }
    },
    BANK {
        @Override
        public PaymentProcessor createProcessor() {
            return new BankPaymentProcessor();
        }
    },
    KAKAO {
        @Override
        public PaymentProcessor createProcessor() {
            return new KakaoPaymentProcessor();
        }
    };  // 세미콜론 주의!
    
    // Enum 본체: 추상 메서드 선언
    // 각 상수가 반드시 이 메서드를 구현해야 함
    public abstract PaymentProcessor createProcessor();
}

// 공통 인터페이스
interface PaymentProcessor {
    void process(int amount);
}

// 구현체들
class CardPaymentProcessor implements PaymentProcessor {
    public void process(int amount) {
        CardAPI cardApi = new CardAPI();
        cardApi.charge(amount);
    }
}

class BankPaymentProcessor implements PaymentProcessor {
    public void process(int amount) {
        BankAPI bankApi = new BankAPI();
        bankApi.transfer(amount);
    }
}

// 깔끔한 서비스 코드
public void processPayment(PaymentType paymentType, int amount) {
    PaymentProcessor processor = paymentType.createProcessor();
    processor.process(amount);
}

새 결제 수단 추가 시 Enum에 항목만 추가하면 된다.

 

자세한 설명은 참고 ! [java] Enum 추상 메서드 활용과 이해

Enum 추상 메서드 동작 원리:

  • 각 Enum 상수(CARD, BANK 등)는 익명 클래스처럼 동작한다
  • 상수들 선언 후 세미콜론(;)으로 구분
  • Enum 본체에 abstract 메서드를 선언하면 각 상수가 반드시 구현해야 한다
  • 새 결제 수단 추가 시 Enum에 상수만 추가하면 컴파일러가 메서드 구현을 강제한다

 

 

10. 테스트 코드 짜기가 너무 힘든 경우

문제: 외부 의존성 하드코딩
해결: 의존성 주입(DI), 인터페이스 추상화
효과: 단위 테스트 작성 가능

 

테스트하기 어렵다 = 설계가 잘못됐다는 신호다.

 

문제 상황:

public class OrderService {
    public void createOrder(Order order) {
        // DB에 직접 연결
        Connection conn = DriverManager.getConnection("jdbc:mysql://...");
        
        // 외부 API 직접 호출
        PaymentAPI api = new PaymentAPI();
        api.charge(order.getAmount());
        
        // 파일 시스템 직접 접근
        FileWriter writer = new FileWriter("/logs/order.log");
        writer.write("주문 생성: " + order.getId());
        
        // 현재 시간 직접 사용
        order.setCreatedAt(LocalDateTime.now());
    }
}

이 코드를 테스트하려면 DB를 띄워야 하고, 외부 API가 호출되고, 파일이 생성된다. 단위 테스트가 불가능하다.

 

사고 과정:

  1. 외부 의존성이 하드코딩됨 → 의존성 주입(DI) 필요
  2. 구체 클래스 직접 사용 → 인터페이스로 추상화
  3. 테스트 시 Mock 객체로 대체 가능하게 만든다

해결:

public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final OrderLogger orderLogger;
    private final TimeProvider timeProvider;
    
    // 생성자 주입
    public OrderService(
        OrderRepository orderRepository,
        PaymentService paymentService,
        OrderLogger orderLogger,
        TimeProvider timeProvider
    ) {
        this.orderRepository = orderRepository;
        this.paymentService = paymentService;
        this.orderLogger = orderLogger;
        this.timeProvider = timeProvider;
    }
    
    public void createOrder(Order order) {
        order.setCreatedAt(timeProvider.now());
        paymentService.charge(order.getAmount());
        orderRepository.save(order);
        orderLogger.log("주문 생성: " + order.getId());
    }
}

// 테스트 코드
@Test
void createOrderTest() {
    // Mock 객체 생성
    OrderRepository mockRepo = mock(OrderRepository.class);
    PaymentService mockPayment = mock(PaymentService.class);
    OrderLogger mockLogger = mock(OrderLogger.class);
    TimeProvider mockTime = mock(TimeProvider.class);
    
    // 시간 고정
    when(mockTime.now()).thenReturn(LocalDateTime.of(2024, 1, 1, 0, 0));
    
    OrderService service = new OrderService(
        mockRepo, mockPayment, mockLogger, mockTime
    );
    
    // 테스트 실행
    service.createOrder(order);
    
    // 검증
    verify(mockPayment).charge(order.getAmount());
    verify(mockRepo).save(order);
}

이제 DB 없이도, 외부 API 없이도 테스트가 가능하다.

 

 

 


 

 

리팩토링 시 주의사항

1. 한 번에 하나씩

❌ 나쁜 예: 인터페이스 추가 + 클래스 분리 + 네이밍 변경 + 패턴 적용
✅ 좋은 예: 먼저 인터페이스만 추가 → 테스트 → 다음 단계

2. 테스트 코드 먼저 확인

// 리팩토링 전에 테스트 작성
@Test
void paymentTest() {
    PayService service = new PayService();
    boolean result = service.processPay("kakao", 5000);
    assertTrue(result);
}

// 리팩토링 후에도 테스트 통과해야 함

3. 커밋 자주 하기

git commit -m "리팩토링: PaymentType Enum 추가"
git commit -m "리팩토링: 의존성 주입 적용"
git commit -m "리팩토링: Mock 테스트 추가"

4. 과도한 추상화 주의

// ❌ 과함
interface PaymentProcessorFactory {
    PaymentProcessor create(PaymentType type);
}
interface PaymentValidator {
    ValidationResult validate(Payment payment);
}
interface PaymentLogger {
    void log(PaymentEvent event);
}
// 간단한 결제 처리에 인터페이스가 너무 많다

// ✅ 적당함
public enum PaymentType {
    CARD, BANK, KAKAO;
    
    public PaymentProcessor createProcessor() {
        // ...
    }
}

 

 

 


 

전체 리팩토링 체크리스트

리펙토링 시작하기전에 체크할것!!! 

 

설계 원칙 (파트 1)

  • [ ] 이 코드가 변경되는 이유가 2개 이상인가? (SRP)
  • [ ] 새 기능 추가 시 기존 코드를 수정해야 하나? (OCP)
  • [ ] 구체적인 클래스에 의존하고 있나? (DIP)
  • [ ] 메서드가 30줄 이상인가?
  • [ ] 클래스가 10개 이상의 메서드를 가지고 있나?
  • [ ] 데이터 클래스와 로직이 분리되어 있나?

코드 품질 (파트 2)

  • [ ] null 체크가 3단계 이상 중첩되었나?
  • [ ] 비슷한 코드가 여러 곳에 있나? (DRY)
  • [ ] 매개변수가 4개 이상인가?
  • [ ] 매직 넘버나 매직 스트링이 있나?

유지보수성 (파트 3)

  • [ ] switch/if-else 분기가 5개 이상인가?
  • [ ] 테스트 코드 작성이 어려운가?
  • [ ] 외부 의존성이 하드코딩되어 있나?

하나라도 해당되면 리팩토링을 고려해볼 시점~~ 

 

 

마무리

리팩토링은 "완벽한 코드"를 만드는 것이 아니라 변화에 유연하고 이해하기 쉬운 코드를 만드는 과정이라고 한다.

처음부터 너무 완벽하게 설계할 필요 없다. 일단..구현하고.. 그때그때 개선하면 된다.

(엉망으로 하라는소리아님)

 

리팩토링 3원칙

  1. 작은 단위로 자주 리팩토링한다
    • 한 번에 하나의 문제만 해결한다
    • 매번 테스트를 통과시킨다
    • 자주 커밋한다
  2. 테스트로 안전망을 확보한다
    • 리팩토링 전에 테스트 작성
    • 리팩토링 후에도 모든 테스트 통과
    • 테스트가 없으면 리팩토링하지 않는다
  3. 팀원과 코드 리뷰한다
    • 혼자 판단하지 않는다
    • 과도한 추상화를 경계한다
    • 팀의 코드 스타일을 따른다

 

파트별 복습

  • 파트 1: OCP, SRP 같은 설계 원칙을 지키는 것이 우선
  • 파트 2: null, 중복, 매직 넘버 같은 기본적인 품질부터 개선
  • 파트 3: 테스트 가능한 구조로 만드는 것이 장기적으로 중요

 

이전 편:

[spring] 언제! 리팩토링해야 할까? 실전 가이드- 코드 품질 문제 (2/3)

[spring] 언제! 리팩토링해야 할까? 실전 가이드- 설계원칙 위반 (1/3)

'공부일기.. > Spring' 카테고리의 다른 글

[spring] final 키워드와 @RequiredArgsConstructor의 원리  (0) 2025.10.22
[spring] 스프링 컨테이너와 스프링 빈  (1) 2025.10.15
[spring] 언제! 리팩토링해야 할까? 실전 가이드- 코드 품질 문제 (2/3)  (0) 2025.10.04
[spring] 언제! 리팩토링해야 할까? 실전 가이드- 설계원칙 위반 (1/3)  (0) 2025.10.03
[Spring] Spring Boot MySQL 설정 (커넥션 풀, OSIV, SQL 로깅)  (0) 2025.09.18
'공부일기../Spring' 카테고리의 다른 글
  • [spring] final 키워드와 @RequiredArgsConstructor의 원리
  • [spring] 스프링 컨테이너와 스프링 빈
  • [spring] 언제! 리팩토링해야 할까? 실전 가이드- 코드 품질 문제 (2/3)
  • [spring] 언제! 리팩토링해야 할까? 실전 가이드- 설계원칙 위반 (1/3)
s0-0mzzang
s0-0mzzang
공부한것을 기록합니다...
  • s0-0mzzang
    승민이의..개발일기..🐰
    s0-0mzzang
  • 전체
    오늘
    어제
    • 전체~ (108)
      • 마음가짐..! (10)
      • 공부일기.. (76)
        • weekly-log (6)
        • Spring (19)
        • Java (18)
        • DataBase (10)
        • git (2)
        • JPA (6)
        • kafka (1)
        • Backend Architecture (3)
        • Troubleshooting (삽질..ㅋ) (2)
        • Cloud (1)
        • Docker (2)
        • 알고리즘 (1)
        • 리액트 (2)
        • Infra (3)
      • 하루일기.. (22)
        • 그림일기 (8)
        • 생각일기 (14)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

    Paging
    StringTokenizer
    항해99
    다짐
    항해플러스
    스프링부트
    MySQL
    spring boot
    SpringBoot
    swagger
    인프라 기초
    ERD
    TDD
    BufferedReader
    spring
    ADC 환경
    자바
    React
    JPA
    리팩토링
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
s0-0mzzang
[spring] 언제! 리팩토링해야 할까? 실전 가이드-유지보수성 문제 (파트 3/3)
상단으로

티스토리툴바