800===Dev Docs and License/Design Pattern

실전 Java 코드 리팩토링 상세 가이드 🔧

블로글러 2024. 11. 13. 23:54

실제 현업에서 자주 마주치는 코드들을 리팩토링하는 구체적인 방법을 알아보겠습니다.

1. 긴 메소드 리팩토링 📝

Before

public class OrderProcessor {
    public void processOrder(Order order) {
        // 주문 유효성 검증
        if (order == null) throw new IllegalArgumentException("Order cannot be null");
        if (order.getItems() == null || order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order must have items");
        }
        if (order.getCustomer() == null) {
            throw new IllegalArgumentException("Order must have customer");
        }

        // 가격 계산
        double totalPrice = 0;
        for (OrderItem item : order.getItems()) {
            double itemPrice = item.getPrice() * item.getQuantity();
            if (item.getQuantity() > 10) {
                itemPrice = itemPrice * 0.9; // 10개 이상 구매시 10% 할인
            }
            totalPrice += itemPrice;
        }

        // 배송비 계산
        double shippingCost = 0;
        if (totalPrice < 50000) {
            shippingCost = 3000;
        } else if (totalPrice < 100000) {
            shippingCost = 1500;
        }

        // DB 저장
        order.setTotalPrice(totalPrice + shippingCost);
        orderRepository.save(order);

        // 이메일 발송
        EmailService.sendEmail(
            order.getCustomer().getEmail(),
            "주문 확인",
            "주문금액: " + totalPrice + "원\n배송비: " + shippingCost + "원"
        );
    }
}

After

public class OrderProcessor {
    private static final int BULK_ORDER_THRESHOLD = 10;
    private static final double BULK_ORDER_DISCOUNT = 0.9;
    private static final double FREE_SHIPPING_THRESHOLD = 100000;
    private static final double REDUCED_SHIPPING_THRESHOLD = 50000;

    private final OrderValidator validator;
    private final PriceCalculator priceCalculator;
    private final ShippingCalculator shippingCalculator;
    private final OrderRepository orderRepository;
    private final EmailService emailService;

    public void processOrder(Order order) {
        validator.validateOrder(order);

        double totalPrice = priceCalculator.calculatePrice(order);
        double shippingCost = shippingCalculator.calculateShipping(totalPrice);

        saveOrder(order, totalPrice, shippingCost);
        sendOrderConfirmation(order, totalPrice, shippingCost);
    }
}

@Component
class OrderValidator {
    public void validateOrder(Order order) {
        Optional.ofNullable(order)
            .orElseThrow(() -> new IllegalArgumentException("Order cannot be null"));

        Optional.ofNullable(order.getItems())
            .filter(items -> !items.isEmpty())
            .orElseThrow(() -> new IllegalArgumentException("Order must have items"));

        Optional.ofNullable(order.getCustomer())
            .orElseThrow(() -> new IllegalArgumentException("Order must have customer"));
    }
}

@Component
class PriceCalculator {
    public double calculatePrice(Order order) {
        return order.getItems().stream()
            .map(this::calculateItemPrice)
            .reduce(0.0, Double::sum);
    }

    private double calculateItemPrice(OrderItem item) {
        double basePrice = item.getPrice() * item.getQuantity();
        return applyBulkDiscount(basePrice, item.getQuantity());
    }

    private double applyBulkDiscount(double price, int quantity) {
        return quantity > BULK_ORDER_THRESHOLD ? price * BULK_ORDER_DISCOUNT : price;
    }
}

2. 복잡한 조건문 리팩토링 🎯

Before

public class InsuranceCalculator {
    public double calculatePremium(Customer customer) {
        double premium = 0;

        if (customer.getAge() < 25) {
            if (customer.getAccidents() == 0) {
                if (customer.getYearsOfDriving() > 2) {
                    premium = 1000;
                } else {
                    premium = 1500;
                }
            } else {
                if (customer.getAccidents() < 3) {
                    premium = 2000;
                } else {
                    premium = 2500;
                }
            }
        } else {
            if (customer.getAccidents() == 0) {
                if (customer.getYearsOfDriving() > 5) {
                    premium = 500;
                } else {
                    premium = 750;
                }
            } else {
                if (customer.getAccidents() < 3) {
                    premium = 1000;
                } else {
                    premium = 1250;
                }
            }
        }
        return premium;
    }
}

After

public class InsuranceCalculator {
    public double calculatePremium(Customer customer) {
        return new PremiumRuleEngine()
            .withBaseRate(determineBaseRate(customer))
            .applyAccidentMultiplier(customer.getAccidents())
            .applyExperienceDiscount(customer.getYearsOfDriving())
            .calculate();
    }

    private double determineBaseRate(Customer customer) {
        return customer.getAge() < 25 ? 1000 : 500;
    }
}

@Component
class PremiumRuleEngine {
    private double premium;

    public PremiumRuleEngine withBaseRate(double baseRate) {
        this.premium = baseRate;
        return this;
    }

    public PremiumRuleEngine applyAccidentMultiplier(int accidents) {
        double multiplier = switch(accidents) {
            case 0 -> 1.0;
            case 1, 2 -> 2.0;
            default -> 2.5;
        };
        this.premium *= multiplier;
        return this;
    }

    public PremiumRuleEngine applyExperienceDiscount(int years) {
        double discount = years > 5 ? 0.8 : 
                         years > 2 ? 0.9 : 1.0;
        this.premium *= discount;
        return this;
    }

    public double calculate() {
        return premium;
    }
}

3. 데이터 클래스 리팩토링 📊

Before

public class User {
    private String firstName;
    private String lastName;
    private String email;
    private String phone;
    private String address;
    private String city;
    private String country;
    private String postalCode;

    // Getters and Setters
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }
    // ... 20+ more getters and setters
}

After

@Value
public class User {
    String firstName;
    String lastName;
    Email email;
    Phone phone;
    Address address;

    @Value
    public static class Email {
        String value;

        public Email(String value) {
            validateEmail(value);
            this.value = value;
        }

        private void validateEmail(String email) {
            if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
                throw new IllegalArgumentException("Invalid email format");
            }
        }
    }

    @Value
    public static class Address {
        String street;
        String city;
        String country;
        String postalCode;

        public static AddressBuilder builder() {
            return new AddressBuilder();
        }
    }
}

4. 상태 패턴을 활용한 리팩토링 🔄

Before

public class Order {
    private String status;

    public void process() {
        if ("NEW".equals(status)) {
            // 새 주문 처리
            status = "PROCESSING";
        } else if ("PROCESSING".equals(status)) {
            // 처리 중 로직
            status = "SHIPPING";
        } else if ("SHIPPING".equals(status)) {
            // 배송 중 로직
            status = "DELIVERED";
        } else if ("DELIVERED".equals(status)) {
            throw new IllegalStateException("이미 배송완료된 주문입니다.");
        }
    }
}

After

public interface OrderState {
    OrderState process();
    String getStatus();
}

@Component
public class Order {
    private OrderState state;

    public Order() {
        this.state = new NewOrder();
    }

    public void process() {
        state = state.process();
    }

    public String getStatus() {
        return state.getStatus();
    }
}

class NewOrder implements OrderState {
    @Override
    public OrderState process() {
        // 새 주문 처리 로직
        return new ProcessingOrder();
    }

    @Override
    public String getStatus() {
        return "NEW";
    }
}

class ProcessingOrder implements OrderState {
    @Override
    public OrderState process() {
        // 처리 중 로직
        return new ShippingOrder();
    }

    @Override
    public String getStatus() {
        return "PROCESSING";
    }
}

5. 테스트하기 쉬운 코드로 리팩토링 🧪

Before

public class PaymentService {
    public boolean processPayment(Payment payment) {
        // 직접 외부 API 호출
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://payment-api.com/process"))
            .POST(HttpRequest.BodyPublishers.ofString(payment.toString()))
            .build();

        try {
            HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
            return response.statusCode() == 200;
        } catch (Exception e) {
            return false;
        }
    }
}

After

public class PaymentService {
    private final PaymentGateway paymentGateway;
    private final PaymentValidator validator;
    private final PaymentRepository repository;

    public PaymentResult processPayment(Payment payment) {
        return new PaymentProcessor(payment)
            .validate(validator)
            .process(paymentGateway)
            .save(repository)
            .getResult();
    }
}

@Component
class PaymentProcessor {
    private final Payment payment;
    private PaymentResult result;

    public PaymentProcessor validate(PaymentValidator validator) {
        validator.validate(payment);
        return this;
    }

    public PaymentProcessor process(PaymentGateway gateway) {
        result = gateway.processPayment(payment);
        return this;
    }

    public PaymentProcessor save(PaymentRepository repository) {
        if (result.isSuccessful()) {
            repository.save(payment);
        }
        return this;
    }
}

@Test
class PaymentServiceTest {
    @Mock
    private PaymentGateway gateway;
    @Mock
    private PaymentValidator validator;
    @Mock
    private PaymentRepository repository;

    @Test
    void successfulPayment() {
        // Given
        Payment payment = new Payment(100, "USD");
        when(gateway.processPayment(payment))
            .thenReturn(PaymentResult.success());

        // When
        PaymentResult result = service.processPayment(payment);

        // Then
        assertThat(result.isSuccessful()).isTrue();
        verify(repository).save(payment);
    }
}

마치며 🎁

좋은 리팩토링의 핵심은:

  1. 작은 단위로 나누기
  2. 명확한 책임 분리
  3. 테스트 가능한 구조
  4. 비즈니스 규칙의 명확한 표현

이러한 원칙들을 지키면서 점진적으로 개선해 나가는 것이 중요합니다.


References:

  1. Refactoring: Improving the Design of Existing Code (2nd Edition) by Martin Fowler
  2. Clean Code by Robert C. Martin
  3. Working Effectively with Legacy Code by Michael Feathers
  4. Java Design Patterns by Vaskaran Sarcar
728x90