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

2025. 10. 3. 22:45·공부일기../Spring

설계 원칙 위반

1. 새 기능 추가 시 여러 곳을 수정해야 하는 경우

문제: if-else 분기 지옥
해결: 전략 패턴
효과: 새 타입 추가 시 기존 코드 수정 불필요

 

새로운 결제 수단 하나 추가하는데 여기저기 다 수정해야 하는 상황

이것이 바로 개방-폐쇄 원칙(OCP) 위반의 신호다.

 

문제 상황:

public class DiscountService {
    public int calculateDiscount(String memberType, int price) {
        if (memberType.equals("VIP")) {
            return price * 20 / 100;
        } else if (memberType.equals("GOLD")) {
            return price * 15 / 100;
        } else if (memberType.equals("SILVER")) {
            return price * 10 / 100;
        }
        return 0;
    }
}

"PLATINUM 등급을 추가해달라"는 요청이 오면 if문을 또 추가해야 한다.

10개 등급이 있다면 끔찍할 것이다...

 

사고 과정:

  1. 새 타입 추가 = 기존 코드 수정 → OCP 위반 발견
  2. 각 등급의 할인 로직은 "전략"으로 볼 수 있다
  3. 전략 패턴 적용하면 새 등급 추가 시 새 클래스만 만들면 된다

해결:

// 전략 인터페이스
interface DiscountPolicy {
    int calculateDiscount(int price);
}

// 각 등급별 전략 구현
class VipDiscountPolicy implements DiscountPolicy {
    public int calculateDiscount(int price) {
        return price * 20 / 100;
    }
}

class GoldDiscountPolicy implements DiscountPolicy {
    public int calculateDiscount(int price) {
        return price * 15 / 100;
    }
}

// 서비스 코드는 깔끔하게
public class DiscountService {
    public int calculateDiscount(DiscountPolicy policy, int price) {
        return policy.calculateDiscount(price);
    }
}

새 등급 추가 시 PlatinumDiscountPolicy 클래스만 만들면 된다. 기존 코드는 건드리지 않는다.

 

 


 

2. 메서드가 너무 긴 경우

문제: 100줄 이상의 긴 메서드
해결: 메서드 추출, 단일 책임 원칙
효과: 전체 흐름을 한눈에 파악 가능

 

메서드가 100줄, 200줄... 읽다가 정신이 혼미해지는 경험을 해봤을 것이다.

문제 상황:

public void processOrder(Order order) {
    // 재고 확인 (20줄)
    for (Item item : order.getItems()) {
        Product product = productRepository.findById(item.getProductId());
        if (product.getStock() < item.getQuantity()) {
            throw new OutOfStockException();
        }
    }
    
    // 가격 계산 (30줄)
    int totalPrice = 0;
    for (Item item : order.getItems()) {
        Product product = productRepository.findById(item.getProductId());
        totalPrice += product.getPrice() * item.getQuantity();
    }
    if (order.getCoupon() != null) {
        totalPrice -= order.getCoupon().getDiscount();
    }
    
    // 결제 처리 (25줄)
    PaymentGateway gateway = new PaymentGateway();
    gateway.connect();
    gateway.authorize(order.getPaymentInfo());
    gateway.charge(totalPrice);
    gateway.disconnect();
    
    // 재고 차감 (15줄)
    for (Item item : order.getItems()) {
        Product product = productRepository.findById(item.getProductId());
        product.decreaseStock(item.getQuantity());
        productRepository.save(product);
    }
    
    // 이메일 발송 (20줄)
    EmailSender sender = new EmailSender();
    sender.setRecipient(order.getCustomer().getEmail());
    sender.setSubject("주문 완료");
    // ... 이메일 본문 작성 ...
    sender.send();
}

110줄짜리 메서드... 주석 없으면 뭐 하는지 알 수 없다.

 

사고 과정:

  1. 한 메서드가 재고, 가격, 결제, 이메일을 다 한다 → SRP 위반
  2. 각 단계를 의미 있는 이름의 메서드로 추출한다
  3. 메인 메서드는 "오케스트레이터" 역할만 한다

해결:

public void processOrder(Order order) {
    validateStock(order);
    int totalPrice = calculateTotalPrice(order);
    processPayment(order, totalPrice);
    decreaseStock(order);
    sendOrderEmail(order);
}

private void validateStock(Order order) {
    for (Item item : order.getItems()) {
        Product product = productRepository.findById(item.getProductId());
        if (product.getStock() < item.getQuantity()) {
            throw new OutOfStockException();
        }
    }
}

private int calculateTotalPrice(Order order) {
    // 가격 계산 로직
}

private void processPayment(Order order, int amount) {
    // 결제 처리 로직
}

// ... 나머지 메서드들

이제 processOrder만 봐도 전체 흐름이 한눈에 들어온다.

 

 

 


 

3. 클래스가 너무 많은 일을 하는 경우

문제: God Object (만능 클래스)
해결: 책임별 클래스 분리
효과: 변경 영향 범위 최소화

 

문제 상황:

public class UserManager {
    public void createUser(String name, String email) { }
    public void deleteUser(Long id) { }
    public void sendWelcomeEmail(User user) { }
    public void validateEmail(String email) { }
    public byte[] generateUserReport() { }
    public void sendPasswordResetEmail(User user) { }
    public boolean checkLoginAttempts(User user) { }
    public void logUserActivity(User user, String action) { }
    public void calculateUserScore(User user) { }
    public void sendBirthdayEmail(User user) { }
}

UserManager라는 이름의 "만능 클래스". 뭐든지 다 한다. 이런 것을 God Object라고 부른다.

 

사고 과정:

  1. 한 클래스가 사용자 관리, 이메일, 검증, 리포트, 로깅 등 모든 것을 한다 → SRP 위반
  2. 책임별로 분리: 생성/삭제, 이메일, 검증, 리포트 등
  3. 각 클래스는 하나의 변경 이유만 가져야 한다

해결:

// 사용자 생명주기 관리
public class UserService {
    public void createUser(String name, String email) { }
    public void deleteUser(Long id) { }
}

// 이메일 전송
public class UserEmailService {
    public void sendWelcomeEmail(User user) { }
    public void sendPasswordResetEmail(User user) { }
    public void sendBirthdayEmail(User user) { }
}

// 검증
public class UserValidator {
    public void validateEmail(String email) { }
    public boolean checkLoginAttempts(User user) { }
}

// 리포트
public class UserReportService {
    public byte[] generateUserReport() { }
}

// 활동 로깅
public class UserActivityLogger {
    public void logUserActivity(User user, String action) { }
}

// 점수 계산
public class UserScoreCalculator {
    public void calculateUserScore(User user) { }
}

이제 각 클래스의 역할이 명확하다. 이메일 로직 변경 시 UserEmailService만 보면 된다.

 

 

 


 

4. 데이터와 로직이 분리되어 있는 경우

문제: 빈약한 도메인 모델
해결: 풍부한 도메인 모델
효과: 객체가 자신의 데이터를 스스로 관리

 

문제 상황:

// 데이터만 있는 클래스
class Order {
    private List<OrderItem> items;
    private int totalAmount;
    private OrderStatus status;
    
    // getter, setter만 있음
}

// 로직은 서비스에
class OrderService {
    public int calculateTotal(Order order) {
        int total = 0;
        for (OrderItem item : order.getItems()) {
            total += item.getPrice() * item.getQuantity();
        }
        return total;
    }
    
    public boolean canCancel(Order order) {
        return order.getStatus() == OrderStatus.PENDING
            || order.getStatus() == OrderStatus.PAYMENT_WAITING;
    }
    
    public void cancel(Order order) {
        if (!canCancel(order)) {
            throw new IllegalStateException("취소할 수 없는 주문입니다");
        }
        order.setStatus(OrderStatus.CANCELLED);
    }
}

 

Order 클래스는 데이터만 담고, 모든 비즈니스 로직은 Service에 있다.

Order에 대한 로직이 여기저기 흩어진다.

 

사고 과정:

  1. 데이터와 로직이 분리되어 있다
  2. 객체가 자신의 데이터를 직접 다뤄야 한다
  3. 캡슐화를 강화한다

해결:

// 풍부한 도메인 모델
class Order {
    private List<OrderItem> items;
    private int totalAmount;
    private OrderStatus status;
    
    // 자신의 데이터를 스스로 관리
    public int calculateTotal() {
        return items.stream()
            .mapToInt(item -> item.getPrice() * item.getQuantity())
            .sum();
    }
    
    public boolean canCancel() {
        return status == OrderStatus.PENDING
            || status == OrderStatus.PAYMENT_WAITING;
    }
    
    public void cancel() {
        if (!canCancel()) {
            throw new IllegalStateException("취소할 수 없는 주문입니다");
        }
        this.status = OrderStatus.CANCELLED;
    }
    
    // setter 대신 의미 있는 메서드
    public void complete() {
        this.status = OrderStatus.COMPLETED;
    }
}

// 서비스는 얇아진다
class OrderService {
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);
        order.cancel();  // Order가 알아서 처리
        orderRepository.save(order);
    }
}

이제 Order에 대한 모든 로직이 Order 클래스 안에 있다. (DDD)

 

 

 


 

리팩토링 체크리스트 (설계 원칙)

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

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

 

다음 편 : 코드 품질 문제 (null 체크, 중복 코드, 매개변수, 매직 넘버)

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

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

[spring] 언제! 리팩토링해야 할까? 실전 가이드-유지보수성 문제 (파트 3/3)  (0) 2025.10.05
[spring] 언제! 리팩토링해야 할까? 실전 가이드- 코드 품질 문제 (2/3)  (0) 2025.10.04
[Spring] Spring Boot MySQL 설정 (커넥션 풀, OSIV, SQL 로깅)  (0) 2025.09.18
[Spring Boot] application.yml 설정파일 마스타~~  (0) 2025.09.14
[Spring-legacy] 이거 @RequestBody Map으로 받아도 되나요?  (1) 2025.08.27
'공부일기../Spring' 카테고리의 다른 글
  • [spring] 언제! 리팩토링해야 할까? 실전 가이드-유지보수성 문제 (파트 3/3)
  • [spring] 언제! 리팩토링해야 할까? 실전 가이드- 코드 품질 문제 (2/3)
  • [Spring] Spring Boot MySQL 설정 (커넥션 풀, OSIV, SQL 로깅)
  • [Spring Boot] application.yml 설정파일 마스타~~
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)
  • 블로그 메뉴

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

    • 깃허브
  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
s0-0mzzang
[spring] 언제! 리팩토링해야 할까? 실전 가이드- 설계원칙 위반 (1/3)
상단으로

티스토리툴바