설계 원칙 위반
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개 등급이 있다면 끔찍할 것이다...
사고 과정:
- 새 타입 추가 = 기존 코드 수정 → OCP 위반 발견
- 각 등급의 할인 로직은 "전략"으로 볼 수 있다
- 전략 패턴 적용하면 새 등급 추가 시 새 클래스만 만들면 된다
해결:
// 전략 인터페이스
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줄짜리 메서드... 주석 없으면 뭐 하는지 알 수 없다.
사고 과정:
- 한 메서드가 재고, 가격, 결제, 이메일을 다 한다 → SRP 위반
- 각 단계를 의미 있는 이름의 메서드로 추출한다
- 메인 메서드는 "오케스트레이터" 역할만 한다
해결:
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라고 부른다.
사고 과정:
- 한 클래스가 사용자 관리, 이메일, 검증, 리포트, 로깅 등 모든 것을 한다 → SRP 위반
- 책임별로 분리: 생성/삭제, 이메일, 검증, 리포트 등
- 각 클래스는 하나의 변경 이유만 가져야 한다
해결:
// 사용자 생명주기 관리
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에 대한 로직이 여기저기 흩어진다.
사고 과정:
- 데이터와 로직이 분리되어 있다
- 객체가 자신의 데이터를 직접 다뤄야 한다
- 캡슐화를 강화한다
해결:
// 풍부한 도메인 모델
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' 카테고리의 다른 글
| [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 |