실제 현업에서 자주 마주치는 코드들을 리팩토링하는 구체적인 방법을 알아보겠습니다.
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);
}
}
마치며 🎁
좋은 리팩토링의 핵심은:
- 작은 단위로 나누기
- 명확한 책임 분리
- 테스트 가능한 구조
- 비즈니스 규칙의 명확한 표현
이러한 원칙들을 지키면서 점진적으로 개선해 나가는 것이 중요합니다.
References:
- Refactoring: Improving the Design of Existing Code (2nd Edition) by Martin Fowler
- Clean Code by Robert C. Martin
- Working Effectively with Legacy Code by Michael Feathers
- Java Design Patterns by Vaskaran Sarcar
728x90
'800===Dev Docs and License > Design Pattern' 카테고리의 다른 글
Locality of Behavior (행위의 국소성) 😊 (0) | 2024.11.17 |
---|---|
더 나은 Java 코드 리팩토링 가이드 🛠️ (0) | 2024.11.13 |
Observer Pattern with Java (0) | 2024.05.30 |
Clean Architecture (0) | 2024.05.28 |
Singleton Pattern with Java (0) | 2024.05.28 |