유지보수성 문제
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개가 되면 감당이 안 된다.
사고 과정:
- 타입별 처리 로직이 분산되어 있다
- Enum과 Factory 패턴으로 관리한다
- 각 타입별 처리를 캡슐화한다
해결:
// 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가 호출되고, 파일이 생성된다. 단위 테스트가 불가능하다.
사고 과정:
- 외부 의존성이 하드코딩됨 → 의존성 주입(DI) 필요
- 구체 클래스 직접 사용 → 인터페이스로 추상화
- 테스트 시 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: OCP, SRP 같은 설계 원칙을 지키는 것이 우선
- 파트 2: null, 중복, 매직 넘버 같은 기본적인 품질부터 개선
- 파트 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 |