π OOPμ SOLIDμ κ΄κ³ Troubleshooting !
ν΅μ¬ μ§λ¬Έ: OOPλ©΄ μΆ©λΆνλ°, μ SOLIDκ° νμν κΉ?
π€ μμ£Ό νλ μ°©κ°
// β "κ°μ²΄μ§ν₯μΌλ‘ μ€κ³νμΌλ λ μλκ°?"
@Service
public class OrderService {
private ProductRepository productRepo;
private UserRepository userRepo;
private PaymentService paymentService;
private EmailService emailService;
private SmsService smsService;
public void processOrder(OrderRequest request) {
// μν μ‘°ν, κ²°μ , μ΄λ©μΌ, SMS λ± λͺ¨λ κ±Έ ν λ²μ...
// 200μ€μ 볡μ‘ν λ‘μ§
}
}
// β
"OOP + SOLID μμΉμ μ μ©νλ©΄?"
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderValidator orderValidator;
private final PaymentProcessor paymentProcessor;
private final NotificationSender notificationSender;
public OrderResult processOrder(OrderRequest request) {
orderValidator.validate(request);
PaymentResult payment = paymentProcessor.process(request);
notificationSender.sendConfirmation(request, payment);
return OrderResult.from(request, payment);
}
}
βOOPλ‘ ν΄λμ€λ₯Ό λλκ³ μμμ μ°λ©΄ λλ κ±° μλκ°μ? μ SOLIDκ° λ νμνμ£ ?β
μ΄λ λ§μ κ°λ°μλ€μ΄ κ°μ§λ μμ°μ€λ¬μ΄ μλ¬Έμ λλ€. νμ§λ§ OOPλ§μΌλ‘λ 건κ°ν μ€κ³λ₯Ό 보μ₯ν μ μμ΅λλ€.
ποΈ μ λ΅: OOPλ ν λ, SOLIDλ 건κ°ν μ€κ³ κ°μ΄λ
π’ 건물 κ±΄μΆ λΉμ λ‘ μ΄ν΄νκΈ°
κ°λ | κ±΄μΆ λΉμ | κ°λ° λΉμ | μν |
---|---|---|---|
OOP | ποΈ κ±΄μΆ κ΅¬μ‘° (κΈ°λ₯, 보, λ²½) | ν΄λμ€, κ°μ²΄, μμ, μΊ‘μν | κΈ°λ³Έ ν λ |
SOLID | π§ κ±΄μΆ κ·λ² (λ΄μ§μ€κ³, μμ κ·μ ) | λ¨μΌμ± μμμΉ(SRP), κ°λ°©-νμμμΉ(OCP), 리μ€μ½ν, μΈν°νμ΄μ€λΆλ¦¬, μμ‘΄μμ | νμ§ κ°μ΄λλΌμΈ |
μ μ΄ λΉμ κ° μ€μνκ°?
- κΈ°λ₯κ³Ό λ²½λ§μΌλ‘λ κ±΄λ¬Όμ΄ λ¬΄λμ§ μ μμ (OOPλ§μΌλ‘λ λΆμ‘±)
- κ±΄μΆ κ·λ²μ΄ μμ΄μΌ μμ νκ³ μ€λκ°λ κ±΄λ¬Όμ΄ λ¨ (SOLID νμ)
- λ λ€ νμνμ§λ§ μν μ΄ λ€λ¦
π‘ OOP vs OOP+SOLID μ€μ λΉκ΅
μλ리μ€: μ£Όλ¬Έ μ²λ¦¬ μμ€ν
π¨ OOPλ§ μ μ©ν κ²½μ°
@Service
public class OrderService {
// λͺ¨λ μμ‘΄μ±μ μ§μ μ°Έμ‘°
private ProductRepository productRepository;
private UserRepository userRepository;
private PaymentGateway creditCardGateway; // μ μ©μΉ΄λλ§ μ§μ
private EmailSender emailSender; // μ΄λ©μΌλ§ μ§μ
public void processOrder(Long userId, Long productId) {
// β ν λ©μλμμ λͺ¨λ κ²μ μ²λ¦¬
User user = userRepository.findById(userId);
Product product = productRepository.findById(productId);
// μ¬κ³ νμΈ
if (product.getStock() < 1) {
throw new OutOfStockException();
}
// μ μ©μΉ΄λ κ²°μ λ§ κ°λ₯
creditCardGateway.charge(user.getCreditCard(), product.getPrice());
// μ¬κ³ κ°μ
product.decreaseStock(1);
productRepository.save(product);
// μ΄λ©μΌ λ°μ‘λ§ κ°λ₯
emailSender.send(user.getEmail(), "μ£Όλ¬Έ μλ£", "μ£Όλ¬Έμ΄ μλ£λμμ΅λλ€.");
// μ£Όλ¬Έ μ μ₯
Order order = new Order(userId, productId, LocalDateTime.now());
// ... μ μ₯ λ‘μ§
}
}
π₯ λ¬Έμ μ λ€:
- κ²°μ λ°©μ μΆκ° μ β μ 체 μ½λ μμ νμ
- SMS μλ¦Ό μΆκ° μ β λ λ€μ μ 체 μμ
- ν μ€νΈνκΈ° μ΄λ €μ β μ€μ κ²°μ κΉμ§ ν μ€νΈ
β OOP + SOLID μ μ©ν κ²½μ°
// 1οΈβ£ SRP: κ°κ°μ μ±
μμ λΆλ¦¬
@Component
public class OrderValidator {
public void validate(OrderRequest request) {
// μ£Όλ¬Έ μ ν¨μ± κ²μ¦λ§ λ΄λΉ
}
}
@Component
public class StockManager {
public void reserveStock(String productId, int quantity) {
// μ¬κ³ κ΄λ¦¬λ§ λ΄λΉ
}
}
// 2οΈβ£ DIP: μΈν°νμ΄μ€μ μμ‘΄
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}
public interface NotificationSender {
void sendOrderConfirmation(OrderConfirmation confirmation);
}
// 3οΈβ£ OCP: νμ₯μ μ΄λ €μκ³ μμ μ λ«νμμ
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderValidator orderValidator;
private final StockManager stockManager;
private final PaymentProcessor paymentProcessor; // ꡬν체μ μμ‘΄νμ§ μμ
private final NotificationSender notificationSender; // νμ₯ κ°λ₯
public OrderResult processOrder(OrderRequest request) {
// κ° λ¨κ³λ³λ‘ λͺ
ννκ² λΆλ¦¬
orderValidator.validate(request);
stockManager.reserveStock(request.getProductId(), request.getQuantity());
PaymentResult payment = paymentProcessor.process(request.toPaymentRequest());
notificationSender.sendOrderConfirmation(OrderConfirmation.from(request, payment));
return OrderResult.success(request, payment);
}
}
π μ₯μ λ€:
- μΉ΄μΉ΄μ€νμ΄ μΆκ° β ꡬνμ²΄λ§ μΆκ°, κΈ°μ‘΄ μ½λ 무λ³κ²½
- SMS μλ¦Ό μΆκ° β NotificationSender ꡬνμ²΄λ§ μΆκ°
- ν μ€νΈ μ©μ΄ β Mock κ°μ²΄λ‘ λ 립μ ν μ€νΈ
π― SOLID κ° μμΉμ μ€μ ν¨κ³Ό
1. π― SRP (Single Responsibility Principle) - λ¨μΌ μ± μ μμΉ
βνλμ ν΄λμ€λ νλμ μ± μλ§ κ°μ ΈμΌ νλ€β
β SRP μλ° μ¬λ‘
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
@Autowired
private AuditLogger auditLogger;
// μ¬μ©μ μμ± (CRUD μ±
μ)
public User createUser(UserCreateRequest request) {
// λΉλ°λ²νΈ κ²μ¦ λ‘μ§ (κ²μ¦ μ±
μ)
if (request.getPassword().length() < 8) {
throw new WeakPasswordException();
}
if (!request.getPassword().matches(".*[A-Z].*")) {
throw new WeakPasswordException();
}
User user = User.builder()
.email(request.getEmail())
.password(encryptPassword(request.getPassword()))
.build();
User savedUser = userRepository.save(user);
// νμ μ΄λ©μΌ λ°μ‘ (μ΄λ©μΌ μ±
μ)
emailService.sendWelcomeEmail(savedUser.getEmail(), savedUser.getName());
// κ°μ¬ λ‘κ·Έ κΈ°λ‘ (λ‘κΉ
μ±
μ)
auditLogger.logUserCreation(savedUser.getId(), LocalDateTime.now());
return savedUser;
}
// λΉλ°λ²νΈ μνΈν (μνΈν μ±
μ)
private String encryptPassword(String password) {
// 볡μ‘ν μνΈν λ‘μ§...
return BCrypt.hashpw(password, BCrypt.gensalt());
}
// μ¬μ©μ μ‘°ν (CRUD μ±
μ)
public User findUser(Long id) { ... }
// λΉλ°λ²νΈ λ³κ²½ μ μ΄λ©μΌ μλ¦Ό (μ΄λ©μΌ + CRUD μ±
μ)
public void changePassword(Long userId, String newPassword) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException());
// λΉλ°λ²νΈ κ²μ¦ (κ²μ¦ μ±
μ)
validatePassword(newPassword);
user.setPassword(encryptPassword(newPassword));
userRepository.save(user);
// μ΄λ©μΌ μλ¦Ό (μ΄λ©μΌ μ±
μ)
emailService.sendPasswordChangeNotification(user.getEmail());
// κ°μ¬ λ‘κ·Έ (λ‘κΉ
μ±
μ)
auditLogger.logPasswordChange(userId, LocalDateTime.now());
}
}
π₯ λ¬Έμ μ λ€:
- μ¬λ¬ μ΄μ λ‘ λ³κ²½λ¨: λΉλ°λ²νΈ μ μ± λ³κ²½, μ΄λ©μΌ ν νλ¦Ώ λ³κ²½, λ‘κΉ λ°©μ λ³κ²½
- ν μ€νΈ μ΄λ €μ: νλμ λ©μλ ν μ€νΈλ₯Ό μν΄ λͺ¨λ μμ‘΄μ± νμ
- μ¬μ¬μ© λΆκ°: λ€λ₯Έ κ³³μμ λΉλ°λ²νΈ κ²μ¦ λ‘μ§μ μΈ μ μμ
β SRP μ μ© ν
// 1. λΉλ°λ²νΈ κ²μ¦ μ±
μ λΆλ¦¬
@Component
public class PasswordValidator {
private static final int MIN_LENGTH = 8;
private static final String UPPERCASE_PATTERN = ".*[A-Z].*";
private static final String LOWERCASE_PATTERN = ".*[a-z].*";
private static final String DIGIT_PATTERN = ".*[0-9].*";
private static final String SPECIAL_CHAR_PATTERN = ".*[!@#$%^&*].*";
public void validate(String password) {
if (password == null || password.length() < MIN_LENGTH) {
throw new WeakPasswordException("λΉλ°λ²νΈλ μ΅μ " + MIN_LENGTH + "μ μ΄μμ΄μ΄μΌ ν©λλ€");
}
if (!password.matches(UPPERCASE_PATTERN)) {
throw new WeakPasswordException("λλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€");
}
if (!password.matches(LOWERCASE_PATTERN)) {
throw new WeakPasswordException("μλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€");
}
if (!password.matches(DIGIT_PATTERN)) {
throw new WeakPasswordException("μ«μλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€");
}
if (!password.matches(SPECIAL_CHAR_PATTERN)) {
throw new WeakPasswordException("νΉμλ¬Έμλ₯Ό ν¬ν¨ν΄μΌ ν©λλ€");
}
}
}
// 2. λΉλ°λ²νΈ μνΈν μ±
μ λΆλ¦¬
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.hashpw(rawPassword, BCrypt.gensalt(12));
}
public boolean matches(String rawPassword, String encodedPassword) {
return BCrypt.checkpw(rawPassword, encodedPassword);
}
}
// 3. μ¬μ©μ μ΄λ²€νΈ λ°ν μ±
μ λΆλ¦¬
@Component
@RequiredArgsConstructor
public class UserEventPublisher {
private final ApplicationEventPublisher eventPublisher;
public void publishUserCreated(User user) {
eventPublisher.publishEvent(new UserCreatedEvent(user));
}
public void publishPasswordChanged(Long userId) {
eventPublisher.publishEvent(new PasswordChangedEvent(userId));
}
}
// 4. μ¬μ©μ λλ©μΈ μλΉμ€ - μ€μ§ μ¬μ©μ CRUDλ§ λ΄λΉ
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordValidator passwordValidator;
private final PasswordEncoder passwordEncoder;
private final UserEventPublisher eventPublisher;
@Transactional
public User createUser(UserCreateRequest request) {
passwordValidator.validate(request.getPassword());
User user = User.builder()
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.name(request.getName())
.build();
User savedUser = userRepository.save(user);
eventPublisher.publishUserCreated(savedUser);
return savedUser;
}
@Transactional
public void changePassword(Long userId, String newPassword) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException());
passwordValidator.validate(newPassword);
user.changePassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
eventPublisher.publishPasswordChanged(userId);
}
@Transactional(readOnly = true)
public User findUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
}
}
// 5. μ΄λ²€νΈ νΈλ€λ¬λ‘ λΆμ ν¨κ³Ό μ²λ¦¬
@Component
@RequiredArgsConstructor
@EventListener
public class UserEventHandler {
private final EmailService emailService;
private final AuditLogger auditLogger;
@EventListener
@Async
public void handleUserCreated(UserCreatedEvent event) {
User user = event.getUser();
// νμ μ΄λ©μΌ λ°μ‘
emailService.sendWelcomeEmail(user.getEmail(), user.getName());
// κ°μ¬ λ‘κ·Έ κΈ°λ‘
auditLogger.logUserCreation(user.getId(), LocalDateTime.now());
}
@EventListener
@Async
public void handlePasswordChanged(PasswordChangedEvent event) {
User user = userRepository.findById(event.getUserId())
.orElseThrow(() -> new UserNotFoundException());
// λΉλ°λ²νΈ λ³κ²½ μλ¦Ό
emailService.sendPasswordChangeNotification(user.getEmail());
// κ°μ¬ λ‘κ·Έ κΈ°λ‘
auditLogger.logPasswordChange(event.getUserId(), LocalDateTime.now());
}
}
π SRP μ μ© ν μ₯μ λ€:
- λͺ νν μ± μ: κ° ν΄λμ€μ μν μ΄ λͺ νν¨
-
μ¬μ¬μ© κ°λ₯:
PasswordValidator
λ₯Ό λ€λ₯Έ κ³³μμλ μ¬μ© κ°λ₯ - ν μ€νΈ μ©μ΄: κ° μ»΄ν¬λνΈλ₯Ό λ 립μ μΌλ‘ ν μ€νΈ
-
λ³κ²½ μν₯ μ΅μν: λΉλ°λ²νΈ μ μ±
λ³κ²½ μ
PasswordValidator
λ§ μμ
2. πͺ OCP (Open-Closed Principle) - κ°λ°©-νμ μμΉ
βνμ₯μλ μ΄λ €μκ³ , μμ μλ λ«νμμ΄μΌ νλ€β
β OCP μλ° μ¬λ‘
@Service
@RequiredArgsConstructor
public class PaymentService {
// κ²°μ λ°©μμ΄ μΆκ°λ λλ§λ€ μ΄ λ©μλλ₯Ό μμ ν΄μΌ ν¨
public PaymentResult processPayment(PaymentRequest request) {
switch (request.getPaymentType()) {
case CREDIT_CARD:
return processCreditCard(request);
case BANK_TRANSFER:
return processBankTransfer(request);
case PAYPAL:
return processPaypal(request);
// μΉ΄μΉ΄μ€νμ΄ μΆκ° μ -> μ΄ λ©μλλ₯Ό μμ ν΄μΌ ν¨!
case KAKAO_PAY:
return processKakaoPay(request);
// λ€μ΄λ²νμ΄ μΆκ° μ -> λ μ΄ λ©μλλ₯Ό μμ ν΄μΌ ν¨!
case NAVER_PAY:
return processNaverPay(request);
default:
throw new UnsupportedPaymentTypeException();
}
}
private PaymentResult processCreditCard(PaymentRequest request) {
// μ μ©μΉ΄λ κ²°μ λ‘μ§
CreditCardGateway gateway = new CreditCardGateway();
return gateway.charge(request.getAmount(), request.getCreditCardInfo());
}
private PaymentResult processBankTransfer(PaymentRequest request) {
// κ³μ’μ΄μ²΄ λ‘μ§
BankTransferGateway gateway = new BankTransferGateway();
return gateway.transfer(request.getAmount(), request.getBankInfo());
}
// μ κ²°μ λ°©μλ§λ€ λ©μλκ° κ³μ μΆκ°λ¨...
private PaymentResult processPaypal(PaymentRequest request) { ... }
private PaymentResult processKakaoPay(PaymentRequest request) { ... }
private PaymentResult processNaverPay(PaymentRequest request) { ... }
}
π₯ λ¬Έμ μ λ€:
-
κΈ°μ‘΄ μ½λ μμ νμ: μ κ²°μ λ°©μλ§λ€
processPayment
λ©μλ μμ - ν΄λμ€ ν¬κΈ° μ¦κ°: κ²°μ λ°©μμ΄ λμ΄λ μλ‘ ν΄λμ€κ° κ±°λν΄μ§
- ν μ€νΈ μν₯: μ κ²°μ λ°©μ μΆκ° μ κΈ°μ‘΄ ν μ€νΈλ μμ νμ
β OCP μ μ© ν
// 1. κ²°μ μ²λ¦¬κΈ° μΈν°νμ΄μ€ μ μ
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
boolean supports(PaymentType paymentType);
}
// 2. κ° κ²°μ λ°©μλ³ κ΅¬ν체
@Component
public class CreditCardProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
CreditCardGateway gateway = new CreditCardGateway();
try {
CreditCardPaymentResult result = gateway.charge(
request.getAmount(),
request.getCreditCardInfo()
);
return PaymentResult.builder()
.transactionId(result.getTransactionId())
.status(PaymentStatus.SUCCESS)
.amount(request.getAmount())
.paymentType(PaymentType.CREDIT_CARD)
.processedAt(LocalDateTime.now())
.build();
} catch (CreditCardException e) {
return PaymentResult.failure(e.getMessage());
}
}
@Override
public boolean supports(PaymentType paymentType) {
return PaymentType.CREDIT_CARD.equals(paymentType);
}
}
@Component
public class BankTransferProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
BankTransferGateway gateway = new BankTransferGateway();
try {
BankTransferResult result = gateway.transfer(
request.getAmount(),
request.getBankInfo()
);
return PaymentResult.builder()
.transactionId(result.getTransactionId())
.status(PaymentStatus.SUCCESS)
.amount(request.getAmount())
.paymentType(PaymentType.BANK_TRANSFER)
.processedAt(LocalDateTime.now())
.build();
} catch (BankTransferException e) {
return PaymentResult.failure(e.getMessage());
}
}
@Override
public boolean supports(PaymentType paymentType) {
return PaymentType.BANK_TRANSFER.equals(paymentType);
}
}
// 3. μλ‘μ΄ κ²°μ λ°©μ μΆκ° - κΈ°μ‘΄ μ½λ μμ μμ!
@Component
public class KakaoPayProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
KakaoPayGateway gateway = new KakaoPayGateway();
try {
KakaoPayResult result = gateway.pay(
request.getAmount(),
request.getKakaoPayInfo()
);
return PaymentResult.builder()
.transactionId(result.getTid())
.status(PaymentStatus.SUCCESS)
.amount(request.getAmount())
.paymentType(PaymentType.KAKAO_PAY)
.processedAt(LocalDateTime.now())
.build();
} catch (KakaoPayException e) {
return PaymentResult.failure(e.getMessage());
}
}
@Override
public boolean supports(PaymentType paymentType) {
return PaymentType.KAKAO_PAY.equals(paymentType);
}
}
// 4. κ²°μ μλΉμ€ - μμ μ λ«νμκ³ νμ₯μ μ΄λ €μμ
@Service
@RequiredArgsConstructor
public class PaymentService {
private final List<PaymentProcessor> paymentProcessors;
public PaymentResult processPayment(PaymentRequest request) {
PaymentProcessor processor = paymentProcessors.stream()
.filter(p -> p.supports(request.getPaymentType()))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentTypeException(
"μ§μνμ§ μλ κ²°μ λ°©μ: " + request.getPaymentType()
));
return processor.process(request);
}
}
π OCP μ μ© ν μ₯μ λ€:
- κΈ°μ‘΄ μ½λ 보νΈ: μ κ²°μ λ°©μ μΆκ° μ κΈ°μ‘΄ μ½λ μμ λΆνμ
-
νμ₯ μ©μ΄:
PaymentProcessor
ꡬνμ²΄λ§ μΆκ°νλ©΄ λ¨ - λ 립μ κ°λ°: κ° κ²°μ λ°©μμ λ 립μ μΌλ‘ κ°λ°/ν μ€νΈ κ°λ₯
3. π LSP (Liskov Substitution Principle) - 리μ€μ½ν μΉν μμΉ
βλΆλͺ¨ ν΄λμ€λ₯Ό μμ ν΄λμ€λ‘ μΉνν΄λ νλ‘κ·Έλ¨μ΄ μ¬λ°λ₯΄κ² λμν΄μΌ νλ€β
β LSP μλ° μ¬λ‘
// κΈ°λ³Έ ν μΈ μ μ±
public class DiscountPolicy {
public int calculateDiscount(int originalPrice) {
return (int) (originalPrice * 0.1); // 10% ν μΈ
}
}
// λ¬Έμ μλ μμ - LSP μλ°
public class VipDiscountPolicy extends DiscountPolicy {
@Override
public int calculateDiscount(int originalPrice) {
if (originalPrice < 50000) {
// λΆλͺ¨μ λ€λ₯Έ μ μ 쑰건 - LSP μλ°!
throw new IllegalArgumentException("VIP ν μΈμ 5λ§μ μ΄μλΆν° κ°λ₯ν©λλ€");
}
return (int) (originalPrice * 0.2); // 20% ν μΈ
}
}
// λ λ€λ₯Έ LSP μλ°
public class NoDiscountPolicy extends DiscountPolicy {
@Override
public int calculateDiscount(int originalPrice) {
// λΆλͺ¨μ λ€λ₯Έ νν쑰건 - νμ 0 λ°ν
return 0; // ν μΈ μμ
}
}
// ν΄λΌμ΄μΈνΈ μ½λ
@Service
public class OrderService {
public OrderResult calculateOrder(List<OrderItem> items, User user) {
int totalPrice = items.stream()
.mapToInt(item -> item.getPrice() * item.getQuantity())
.sum();
DiscountPolicy discountPolicy = getDiscountPolicy(user);
// LSP μλ°μΌλ‘ μΈν λ¬Έμ λ°μ!
int discount = discountPolicy.calculateDiscount(totalPrice); // μμΈ λ°μ κ°λ₯
return new OrderResult(totalPrice, discount, totalPrice - discount);
}
private DiscountPolicy getDiscountPolicy(User user) {
if (user.isVip()) {
return new VipDiscountPolicy(); // 5λ§μ λ―Έλ§ μ μμΈ λ°μ!
}
return new DiscountPolicy();
}
}
π₯ λ¬Έμ μ λ€:
- μμμΉ λͺ»ν μμΈ: VIP ν μΈ μ μ± μμ κ°μκΈ° μμΈ λ°μ
- μΌκ΄μ± μμ: κ°μ μΈν°νμ΄μ€μΈλ° λ€λ₯Έ λμ λ°©μ
- μΉν λΆκ°λ₯: λΆλͺ¨λ₯Ό μμμΌλ‘ λ°κΎΈλ©΄ νλ‘κ·Έλ¨μ΄ κΉ¨μ§
β LSP μ μ© ν
// ν μΈ μ μ±
μΈν°νμ΄μ€ - λͺ
νν κ³μ½ μ μ
public interface DiscountPolicy {
/**
* μ£Όμ΄μ§ κ°κ²©μ λν ν μΈ κΈμ‘μ κ³μ°ν©λλ€.
* @param originalPrice μλ κ°κ²© (0 μ΄μ)
* @param user μ¬μ©μ μ 보
* @return ν μΈ κΈμ‘ (0 μ΄μ)
* @throws IllegalArgumentException κ°κ²©μ΄ 0 λ―Έλ§μΈ κ²½μ°
*/
int calculateDiscount(int originalPrice, User user);
/**
* μ΄ ν μΈ μ μ±
μ΄ μ μ© κ°λ₯νμ§ νμΈν©λλ€.
* @param user μ¬μ©μ μ 보
* @param totalPrice μ΄ μ£Όλ¬Έ κΈμ‘
* @return μ μ© κ°λ₯ μ¬λΆ
*/
boolean isApplicable(User user, int totalPrice);
}
// κΈ°λ³Έ ν μΈ μ μ±
@Component
public class RegularDiscountPolicy implements DiscountPolicy {
private static final double DISCOUNT_RATE = 0.1;
@Override
public int calculateDiscount(int originalPrice, User user) {
if (originalPrice < 0) {
throw new IllegalArgumentException("κ°κ²©μ 0 μ΄μμ΄μ΄μΌ ν©λλ€");
}
if (!isApplicable(user, originalPrice)) {
return 0;
}
return (int) (originalPrice * DISCOUNT_RATE);
}
@Override
public boolean isApplicable(User user, int totalPrice) {
return !user.isVip() && totalPrice >= 10000; // 1λ§μ μ΄μ μΌλ° νμ
}
}
// VIP ν μΈ μ μ±
- LSP μ€μ
@Component
public class VipDiscountPolicy implements DiscountPolicy {
private static final double DISCOUNT_RATE = 0.2;
private static final int MIN_PRICE_FOR_VIP_DISCOUNT = 50000;
@Override
public int calculateDiscount(int originalPrice, User user) {
if (originalPrice < 0) {
throw new IllegalArgumentException("κ°κ²©μ 0 μ΄μμ΄μ΄μΌ ν©λλ€");
}
// μ μ© λΆκ°λ₯ν κ²½μ° μμΈκ° μλ 0 λ°ν - LSP μ€μ
if (!isApplicable(user, originalPrice)) {
return 0;
}
return (int) (originalPrice * DISCOUNT_RATE);
}
@Override
public boolean isApplicable(User user, int totalPrice) {
return user.isVip() && totalPrice >= MIN_PRICE_FOR_VIP_DISCOUNT;
}
}
// ν μΈ μμ μ μ±
@Component
public class NoDiscountPolicy implements DiscountPolicy {
@Override
public int calculateDiscount(int originalPrice, User user) {
if (originalPrice < 0) {
throw new IllegalArgumentException("κ°κ²©μ 0 μ΄μμ΄μ΄μΌ ν©λλ€");
}
return 0; // νμ 0 λ°ν - μΌκ΄μ± μ μ§
}
@Override
public boolean isApplicable(User user, int totalPrice) {
return true; // νμ μ μ© κ°λ₯ (ν μΈ μμμ΄λ―λ‘)
}
}
// ν μΈ μ μ±
κ²°μ μ
@Service
@RequiredArgsConstructor
public class DiscountPolicyDecider {
private final List<DiscountPolicy> discountPolicies;
public DiscountPolicy decide(User user, int totalPrice) {
return discountPolicies.stream()
.filter(policy -> policy.isApplicable(user, totalPrice))
.findFirst()
.orElse(new NoDiscountPolicy());
}
}
// ν΄λΌμ΄μΈνΈ μ½λ - LSP μ€μλ‘ μμ ν¨
@Service
@RequiredArgsConstructor
public class OrderService {
private final DiscountPolicyDecider discountPolicyDecider;
public OrderResult calculateOrder(List<OrderItem> items, User user) {
int totalPrice = items.stream()
.mapToInt(item -> item.getPrice() * item.getQuantity())
.sum();
DiscountPolicy discountPolicy = discountPolicyDecider.decide(user, totalPrice);
// μ΄λ€ ꡬν체λ μμ νκ² μΉν κ°λ₯
int discount = discountPolicy.calculateDiscount(totalPrice, user);
return OrderResult.builder()
.originalPrice(totalPrice)
.discountAmount(discount)
.finalPrice(totalPrice - discount)
.appliedPolicy(discountPolicy.getClass().getSimpleName())
.build();
}
}
π LSP μ μ© ν μ₯μ λ€:
- μμ ν μΉν: μ΄λ€ ꡬν체λ μμΈ μμ΄ λμ
- μΌκ΄λ κ³μ½: λͺ¨λ ꡬνμ²΄κ° λμΌν μ μ /νν 쑰건 μ€μ
- μμΈ‘ κ°λ₯ν λμ: ν΄λΌμ΄μΈνΈ μ½λκ° μμ ν¨
4. π§© ISP (Interface Segregation Principle) - μΈν°νμ΄μ€ λΆλ¦¬ μμΉ
βν΄λΌμ΄μΈνΈλ μ¬μ©νμ§ μλ μΈν°νμ΄μ€μ μμ‘΄νλ©΄ μ λλ€β
β ISP μλ° μ¬λ‘
// κ±°λν μΈν°νμ΄μ€ - ISP μλ°
public interface UserManager {
// μ¬μ©μ CRUD
User createUser(UserCreateRequest request);
User updateUser(Long id, UserUpdateRequest request);
void deleteUser(Long id);
User findUser(Long id);
List<User> findAllUsers();
// μΈμ¦ κ΄λ ¨
boolean authenticate(String email, String password);
String generateToken(User user);
void logout(String token);
void resetPassword(String email);
// μ΄λ©μΌ κ΄λ ¨
void sendWelcomeEmail(User user);
void sendPasswordResetEmail(String email);
void sendPromotionEmail(List<User> users, String content);
// ν΅κ³ κ΄λ ¨
int getTotalUserCount();
List<User> getActiveUsers();
Map<String, Integer> getUserStatsByRegion();
// κ΄λ¦¬μ μ μ©
void banUser(Long userId, String reason);
void unbanUser(Long userId);
List<User> getBannedUsers();
}
// λ¬Έμ : μ΄λ©μΌ λ°μ‘λ§ νμν ν΄λΌμ΄μΈνΈκ° λͺ¨λ λ©μλμ μμ‘΄
@Component
public class EmailNotificationService {
private final UserManager userManager; // κ±°λν μΈν°νμ΄μ€μ μμ‘΄
public void sendMonthlyNewsletter() {
List<User> users = userManager.findAllUsers();
// μ€μ λ‘λ sendPromotionEmailλ§ νμνμ§λ§
// κ±°λν μΈν°νμ΄μ€ μ 체μ μμ‘΄
userManager.sendPromotionEmail(users, "μκ° λ΄μ€λ ν°");
}
}
// λ¬Έμ : μ¬μ©μ μ‘°νλ§ νμν ν΄λΌμ΄μΈνΈκ° λΆνμν μμ‘΄μ±μ κ°μ§
@Controller
public class UserController {
private final UserManager userManager; // κ±°λν μΈν°νμ΄μ€μ μμ‘΄
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
// findUserλ§ νμνμ§λ§ μ 체 μΈν°νμ΄μ€μ μμ‘΄
User user = userManager.findUser(id);
return ResponseEntity.ok(user);
}
}
π₯ λ¬Έμ μ λ€:
- λΆνμν μμ‘΄μ±: μ¬μ©νμ§ μλ λ©μλκΉμ§ μμ‘΄
- λ³κ²½ μν₯ νμ°: μΈν°νμ΄μ€ λ³κ²½ μ λͺ¨λ ν΄λΌμ΄μΈνΈ μν₯
- ꡬν λΆλ΄: ꡬν체μμ λͺ¨λ λ©μλλ₯Ό ꡬνν΄μΌ ν¨
β ISP μ μ© ν
// 1. μ¬μ©μ CRUD μ μ© μΈν°νμ΄μ€
public interface UserRepository {
User save(User user);
User findById(Long id);
List<User> findAll();
void deleteById(Long id);
boolean existsById(Long id);
}
// 2. μΈμ¦ μ μ© μΈν°νμ΄μ€
public interface AuthenticationService {
boolean authenticate(String email, String password);
String generateToken(User user);
void logout(String token);
void resetPassword(String email);
}
// 3. μ΄λ©μΌ μ μ© μΈν°νμ΄μ€
public interface UserEmailService {
void sendWelcomeEmail(User user);
void sendPasswordResetEmail(String email);
void sendPromotionEmail(List<User> users, String content);
}
// 4. μ¬μ©μ ν΅κ³ μ μ© μΈν°νμ΄μ€
public interface UserStatisticsService {
int getTotalUserCount();
List<User> getActiveUsers();
Map<String, Integer> getUserStatsByRegion();
List<User> getUsersRegisteredBetween(LocalDate start, LocalDate end);
}
// 5. κ΄λ¦¬μ μ μ© μΈν°νμ΄μ€
public interface UserAdminService {
void banUser(Long userId, String reason);
void unbanUser(Long userId);
List<User> getBannedUsers();
void sendWarningToUser(Long userId, String message);
}
// 6. μ¬μ©μ λλ©μΈ μλΉμ€
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserEventPublisher eventPublisher;
@Transactional
public User createUser(UserCreateRequest request) {
User user = User.builder()
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.name(request.getName())
.build();
User savedUser = userRepository.save(user);
eventPublisher.publishUserCreated(savedUser);
return savedUser;
}
@Transactional(readOnly = true)
public User findUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
}
}
// κ° ν΄λΌμ΄μΈνΈλ νμν μΈν°νμ΄μ€λ§ μμ‘΄
@Component
@RequiredArgsConstructor
public class EmailNotificationService {
private final UserEmailService userEmailService; // μ΄λ©μΌ μΈν°νμ΄μ€λ§ μμ‘΄
private final UserRepository userRepository; // μ¬μ©μ μ‘°ν μΈν°νμ΄μ€λ§ μμ‘΄
public void sendMonthlyNewsletter() {
List<User> users = userRepository.findAll();
userEmailService.sendPromotionEmail(users, "μκ° λ΄μ€λ ν°");
}
}
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService; // νμν μλΉμ€λ§ μμ‘΄
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findUser(id);
return ResponseEntity.ok(user);
}
}
@Controller
@RequiredArgsConstructor
public class AdminController {
private final UserAdminService adminService; // κ΄λ¦¬μ μΈν°νμ΄μ€λ§ μμ‘΄
private final UserStatisticsService statsService; // ν΅κ³ μΈν°νμ΄μ€λ§ μμ‘΄
@PostMapping("/admin/users/{id}/ban")
public ResponseEntity<Void> banUser(
@PathVariable Long id,
@RequestBody BanRequest request) {
adminService.banUser(id, request.getReason());
return ResponseEntity.ok().build();
}
@GetMapping("/admin/users/stats")
public ResponseEntity<UserStats> getUserStats() {
UserStats stats = UserStats.builder()
.totalCount(statsService.getTotalUserCount())
.activeUsers(statsService.getActiveUsers().size())
.regionStats(statsService.getUserStatsByRegion())
.build();
return ResponseEntity.ok(stats);
}
}
π ISP μ μ© ν μ₯μ λ€:
- νμν μμ‘΄μ±λ§: κ° ν΄λΌμ΄μΈνΈκ° μ€μ μ¬μ©νλ μΈν°νμ΄μ€λ§ μμ‘΄
- λ³κ²½ μν₯ μ΅μν: μΈν°νμ΄μ€ λ³κ²½ μ ν΄λΉ ν΄λΌμ΄μΈνΈλ§ μν₯
- ꡬν λ¨μν: κ° μΈν°νμ΄μ€λ³λ‘ λ 립μ μΈ κ΅¬ν체 μμ± κ°λ₯
5. β¬οΈ DIP (Dependency Inversion Principle) - μμ‘΄κ΄κ³ μμ μμΉ
βκ³ μμ€ λͺ¨λμ μ μμ€ λͺ¨λμ μμ‘΄ν΄μλ μ λλ€. λ λ€ μΆμνμ μμ‘΄ν΄μΌ νλ€β
β DIP μλ° μ¬λ‘
// μ μμ€ λͺ¨λλ€ (ꡬ체μ μΈ κ΅¬ν)
public class MySqlUserRepository {
public User save(User user) {
// MySQL νΉν μ μ₯ λ‘μ§
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
// JDBC μ½λ...
return user;
}
public User findById(Long id) {
// MySQL νΉν μ‘°ν λ‘μ§
String sql = "SELECT * FROM users WHERE id = ?";
// JDBC μ½λ...
return user;
}
}
public class SmtpEmailService {
public void sendEmail(String to, String subject, String content) {
// SMTP νΉν μ΄λ©μΌ λ°μ‘ λ‘μ§
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com");
// JavaMail μ½λ...
}
}
// κ³ μμ€ λͺ¨λμ΄ μ μμ€ λͺ¨λμ μ§μ μμ‘΄ - DIP μλ°
@Service
public class UserService {
// ꡬ체μ μΈ κ΅¬ν체μ μ§μ μμ‘΄!
private final MySqlUserRepository userRepository;
private final SmtpEmailService emailService;
public UserService() {
// μμ±μμμ μ§μ μΈμ€ν΄μ€ μμ± - κ°ν κ²°ν©!
this.userRepository = new MySqlUserRepository();
this.emailService = new SmtpEmailService();
}
public User createUser(String name, String email) {
User user = new User(name, email);
// MySQLμ κ°νκ² κ²°ν©
User savedUser = userRepository.save(user);
// SMTPμ κ°νκ² κ²°ν©
emailService.sendEmail(
savedUser.getEmail(),
"νμν©λλ€",
"κ°μ
μ νμν©λλ€"
);
return savedUser;
}
}
// λ λ€λ₯Έ DIP μλ° - κ²°μ μλΉμ€
@Service
public class PaymentService {
// ꡬ체μ μΈ PGμ¬μ μ§μ μμ‘΄
private final IamportPaymentGateway iamportGateway;
public PaymentService() {
this.iamportGateway = new IamportPaymentGateway();
}
public PaymentResult processPayment(PaymentRequest request) {
// μμν¬νΈμλ§ μμ‘΄ - λ€λ₯Έ PGλ‘ λ³κ²½ μ μ 체 μμ νμ
return iamportGateway.charge(request.getAmount(), request.getCardInfo());
}
}
π₯ λ¬Έμ μ λ€:
- κ°ν κ²°ν©: ꡬ체 ꡬνμ μ§μ μμ‘΄νμ¬ λ³κ²½μ΄ μ΄λ €μ
- ν μ€νΈ μ΄λ €μ: μ€μ MySQL, SMTPλ₯Ό μ¬μ©ν΄μΌ ν μ€νΈ κ°λ₯
- νμ₯μ± λΆμ‘±: λ€λ₯Έ ꡬνμ²΄λ‘ λ³κ²½ μ μ½λ μ 체 μμ νμ
β DIP μ μ© ν
// 1. κ³ μμ€μμ μ μν μΆμν (μΈν°νμ΄μ€)
public interface UserRepository {
User save(User user);
User findById(Long id);
List<User> findAll();
boolean existsByEmail(String email);
}
public interface EmailService {
void sendEmail(EmailMessage message);
}
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
PaymentResult refund(String transactionId, int amount);
}
// 2. κ³ μμ€ λͺ¨λ - μΆμνμλ§ μμ‘΄
@Service
@RequiredArgsConstructor // μμ±μ μ£Όμ
public class UserService {
// μΆμν(μΈν°νμ΄μ€)μ μμ‘΄
private final UserRepository userRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
@Transactional
public User createUser(UserCreateRequest request) {
// μ΄λ©μΌ μ€λ³΅ νμΈ
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException();
}
User user = User.builder()
.name(request.getName())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.build();
// μΆμνλ₯Ό ν΅ν μ μ₯
User savedUser = userRepository.save(user);
// μΆμνλ₯Ό ν΅ν μ΄λ©μΌ λ°μ‘
EmailMessage welcomeMessage = EmailMessage.builder()
.to(savedUser.getEmail())
.subject("νμν©λλ€!")
.content("κ°μ
μ νμν©λλ€, " + savedUser.getName() + "λ!")
.build();
emailService.sendEmail(welcomeMessage);
return savedUser;
}
}
@Service
@RequiredArgsConstructor
public class PaymentService {
// μΆμνμ μμ‘΄
private final PaymentGateway paymentGateway;
@Transactional
public PaymentResult processPayment(PaymentRequest request) {
try {
// μ΄λ€ PGμ¬λ λμΌν μΈν°νμ΄μ€λ‘ μ²λ¦¬
return paymentGateway.charge(request);
} catch (PaymentException e) {
throw new PaymentProcessingException("κ²°μ μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€", e);
}
}
@Transactional
public PaymentResult processRefund(String transactionId, int amount) {
return paymentGateway.refund(transactionId, amount);
}
}
// 3. μ μμ€ λͺ¨λλ€ - μΆμνλ₯Ό ꡬν
@Repository
public class MySqlUserRepository implements UserRepository {
@Override
public User save(User user) {
// MySQL JPA ꡬν
return userJpaRepository.save(user);
}
@Override
public User findById(Long id) {
return userJpaRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException());
}
@Override
public boolean existsByEmail(String email) {
return userJpaRepository.existsByEmail(email);
}
}
// λ€λ₯Έ DB ꡬν체λ μ½κ² μΆκ° κ°λ₯
@Repository
@Profile("mongodb")
public class MongoUserRepository implements UserRepository {
@Override
public User save(User user) {
// MongoDB ꡬν
return mongoTemplate.save(user);
}
@Override
public User findById(Long id) {
return mongoTemplate.findById(id, User.class);
}
}
@Service
public class SmtpEmailService implements EmailService {
@Override
public void sendEmail(EmailMessage message) {
// SMTP ꡬν
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(message.getTo());
mailMessage.setSubject(message.getSubject());
mailMessage.setText(message.getContent());
mailSender.send(mailMessage);
}
}
// λ€λ₯Έ μ΄λ©μΌ μλΉμ€λ μ½κ² μΆκ°
@Service
@Profile("ses")
public class AwsSesEmailService implements EmailService {
@Override
public void sendEmail(EmailMessage message) {
// AWS SES ꡬν
SendEmailRequest request = SendEmailRequest.builder()
.destination(Destination.builder().toAddresses(message.getTo()).build())
.message(Message.builder()
.subject(Content.builder().data(message.getSubject()).build())
.body(Body.builder().text(Content.builder().data(message.getContent()).build()).build())
.build())
.source(fromEmail)
.build();
sesClient.sendEmail(request);
}
}
@Service
public class IamportPaymentGateway implements PaymentGateway {
@Override
public PaymentResult charge(PaymentRequest request) {
// μμν¬νΈ API νΈμΆ
IamportResponse<Payment> response = iamportClient.paymentByImpUid(request.getImpUid());
return PaymentResult.builder()
.transactionId(response.getResponse().getImpUid())
.status(convertStatus(response.getResponse().getStatus()))
.amount(response.getResponse().getAmount().intValue())
.build();
}
}
// λ€λ₯Έ PGμ¬λ μ½κ² μΆκ°
@Service
@Profile("toss")
public class TossPaymentGateway implements PaymentGateway {
@Override
public PaymentResult charge(PaymentRequest request) {
// ν μ€νμ΄λ¨ΌμΈ API νΈμΆ
// ...
}
}
π DIP μ μ© ν μ₯μ λ€:
- λμ¨ν κ²°ν©: ꡬν체 λ³κ²½ μ κ³ μμ€ λͺ¨λ μμ λΆνμ
- ν μ€νΈ μ©μ΄μ±: Mock κ°μ²΄λ‘ μ½κ² λ¨μ ν μ€νΈ κ°λ₯
- νμ₯μ±: μλ‘μ΄ κ΅¬ν체 μΆκ°κ° λ§€μ° μ¬μ
- μ€μ κΈ°λ° μ ν: Profileμ΄λ μ€μ μΌλ‘ ꡬν체 λ³κ²½ κ°λ₯
// ν
μ€νΈ μ½λ μμ
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
@Test
void createUser_μ±κ³΅() {
// given
UserCreateRequest request = new UserCreateRequest("ν
μ€νΈ", "test@test.com", "password");
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(passwordEncoder.encode(request.getPassword())).thenReturn("encodedPassword");
User savedUser = User.builder()
.id(1L)
.name(request.getName())
.email(request.getEmail())
.build();
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// when
User result = userService.createUser(request);
// then
assertThat(result.getName()).isEqualTo("ν
μ€νΈ");
verify(emailService).sendEmail(any(EmailMessage.class));
}
}
βοΈ μΈμ SOLIDλ₯Ό μ μ©ν΄μΌ ν κΉ?
π’ SOLID μ κ·Ή μ μ© μν©
// 볡μ‘ν λΉμ¦λμ€ λ‘μ§μ κ°μ§ μ£Όλ¬Έ μ²λ¦¬ μμ€ν
@Service
@RequiredArgsConstructor
public class OrderProcessingService {
// μ¬λ¬ μΈλΆ μμ€ν
κ³Ό μ°λ
private final PaymentGateway paymentGateway;
private final InventoryService inventoryService;
private final ShippingService shippingService;
private final NotificationService notificationService;
private final LoyaltyService loyaltyService;
@Transactional
public OrderResult processOrder(OrderRequest request) {
// 볡μ‘ν μ£Όλ¬Έ μ²λ¦¬ νλ‘μ°
// 1. μ¬κ³ νμΈ λ° μμ½
// 2. κ²°μ μ²λ¦¬
// 3. λ°°μ‘ μμ²
// 4. ν¬μΈνΈ μ 립
// 5. μλ¦Ό λ°μ‘
// κ° λ¨κ³λ§λ€ λ€μν μμΈ μν© μ²λ¦¬
}
}
μ μ© μ νΈλ€:
- λμ 볡μ‘λ: ν΄λμ€κ° 100μ€ μ΄μ, λ©μλκ° 20μ€ μ΄μ
- λ€μν λ³κ²½ μμΈ: λΉμ¦λμ€ κ·μΉ, μΈλΆ μμ€ν , UI μꡬμ¬ν λ³κ²½
- νμ₯ κ³ν: μλ‘μ΄ κ²°μ λ°©μ, λ°°μ‘μ 체, μλ¦Ό μ±λ μΆκ° μμ
- ν κ·λͺ¨: 3λͺ μ΄μμ κ°λ°μκ° λμμ μμ
π‘ SOLID μ νμ μ μ© μν©
// μ€κ° 볡μ‘λμ μ¬μ©μ κ΄λ¦¬ μμ€ν
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public User createUser(UserCreateRequest request) {
// μ λΉν 볡μ‘λμ λ‘μ§
validateUserRequest(request);
User user = buildUser(request);
return userRepository.save(user);
}
private void validateUserRequest(UserCreateRequest request) {
// κ²μ¦ λ‘μ§ (10μ€ λ΄μΈ)
}
private User buildUser(UserCreateRequest request) {
// μμ± λ‘μ§ (5μ€ λ΄μΈ)
}
}
μ μ© κ³ λ € μ¬ν:
- DIP μ°μ μ μ©: ν μ€νΈ μ©μ΄μ±μ μν΄
- SRP λΆλΆ μ μ©: λ무 ν° ν΄λμ€λ§ λΆλ¦¬
- OCPλ νμ₯ κ³νμ΄ μμ λλ§
π΄ SOLID μ΅μ μ μ© μν©
// λ¨μν CRUD μλΉμ€
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository repository;
public Product save(Product product) {
return repository.save(product);
}
public Product findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
public void deleteById(Long id) {
repository.deleteById(id);
}
}
μ΅μ μ μ© κΈ°μ€:
- DIPλ§ μ μ©: Springμ μμ‘΄μ± μ£Όμ νμ©
- λλ¨Έμ§ μμΉμ κ³Όλν μ€κ³: λ¨μν¨μ΄ λ λμ
π¨ μμ£Ό νλ μ€μλ€κ³Ό ν΄κ²°μ±
β μ€μ 1: κ³Όλν μΆμν
// λΆνμν μΆμνμ μ
public interface StringProcessor {
String process(String input);
}
@Component
public class StringTrimmer implements StringProcessor {
public String process(String input) {
return input.trim();
}
}
@Component
public class StringUpperCaser implements StringProcessor {
public String process(String input) {
return input.toUpperCase();
}
}
// μ΄λ° λ¨μν κΈ°λ₯κΉμ§ μΈν°νμ΄μ€λ‘ λ§λ€ νμ μμ
β ν΄κ²°μ± : μ μ ν μμ€μ μΆμν
// μ νΈλ¦¬ν°μ± κΈ°λ₯μ μ μ λ©μλλ κ°λ¨ν μ»΄ν¬λνΈλ‘
@Component
public class StringUtils {
public String cleanAndFormat(String input) {
if (input == null) return "";
return input.trim().toUpperCase();
}
}
β μ€μ 2: μΈν°νμ΄μ€ ꡬν체 1:1 λ§€μΉ
// μλ―Έμλ 1:1 μΈν°νμ΄μ€
public interface UserService {
User createUser(UserCreateRequest request);
}
@Service
public class UserServiceImpl implements UserService {
// ꡬνμ²΄κ° νλλΏμΈλ° κ΅³μ΄ μΈν°νμ΄μ€?
}
β ν΄κ²°μ± : νμμμλ§ μΈν°νμ΄μ€ μμ±
// ꡬνμ²΄κ° νλλΏμ΄κ³ νμ₯ κ³νμ΄ μλ€λ©΄ μΈν°νμ΄μ€ λΆνμ
@Service
public class UserService {
public User createUser(UserCreateRequest request) {
// ꡬν λ‘μ§
}
}
// νμ₯ κ³νμ΄ μκ±°λ ν
μ€νΈλ₯Ό μν΄ Mockμ΄ νμν κ²½μ°μλ§ μΈν°νμ΄μ€ μ¬μ©
β μ€μ 3: God Object λ°©μ§λ₯Ό μν κ³Όλν λΆλ¦¬
// λ무 μΈλΆνλ λΆλ¦¬
@Component
public class UserNameValidator { } // μ΄λ¦ κ²μ¦λ§
@Component
public class UserEmailValidator { } // μ΄λ©μΌ κ²μ¦λ§
@Component
public class UserPhoneValidator { } // μ νλ²νΈ κ²μ¦λ§
@Component
public class UserAgeValidator { } // λμ΄ κ²μ¦λ§
// μ€νλ € 볡μ‘λλ§ μ¦κ°
β ν΄κ²°μ± : κ΄λ ¨μλ μ± μμ ν¨κ» λ¬ΆκΈ°
@Component
public class UserValidator {
public void validate(User user) {
validateName(user.getName());
validateEmail(user.getEmail());
validatePhone(user.getPhone());
validateAge(user.getAge());
}
private void validateName(String name) { }
private void validateEmail(String email) { }
private void validatePhone(String phone) { }
private void validateAge(int age) { }
}
π μ€λ¬΄ μ μ© λ‘λλ§΅
πββοΈ 1λ¨κ³: κΈ°μ΄ λ€μ§κΈ° (1-2μ£Ό)
DIPλΆν° μμ
// Before: ꡬ체 ν΄λμ€μ μμ‘΄
@Service
public class OrderService {
private MySqlOrderRepository repository = new MySqlOrderRepository();
}
// After: μΆμνμ μμ‘΄
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository repository; // μΈν°νμ΄μ€μ μμ‘΄
}
πΆββοΈ 2λ¨κ³: μ± μ λΆλ¦¬ (2-3μ£Ό)
SRP μ μ©μΌλ‘ ν° ν΄λμ€ λΆν΄
// Before: νλμ μλΉμ€κ° λͺ¨λ μΌμ λ΄λΉ
@Service
public class OrderService {
// μ£Όλ¬Έ μμ± + κ²°μ + μ¬κ³ κ΄λ¦¬ + μ΄λ©μΌ λ°μ‘ + λ‘κΉ
(200μ€)
}
// After: μ±
μλ³λ‘ λΆλ¦¬
@Service
public class OrderService {
private final OrderValidator validator;
private final PaymentService paymentService;
private final InventoryService inventoryService;
// κ°κ° λͺ
νν μ±
μ
}
πββοΈ 3λ¨κ³: νμ₯μ± κ³ λ € (3-4μ£Ό)
OCP μ μ©μΌλ‘ νμ₯ κ°λ₯ν ꡬ쑰
// μλ‘μ΄ κ²°μ λ°©μ, μλ¦Ό λ°©μμ μ½κ² μΆκ°ν μ μλ ꡬ쑰
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}
@Service
public class PaymentService {
private final List<PaymentProcessor> processors;
public PaymentResult process(PaymentRequest request) {
// μ μ ν processor μ νν΄μ μ²λ¦¬
}
}
π 4λ¨κ³: μ§μμ κ°μ
μ½λ 리뷰 체ν¬λ¦¬μ€νΈ
- SRP: μ΄ ν΄λμ€κ° λ³κ²½λλ μ΄μ κ° 2κ° μ΄μμΈκ°?
- OCP: μ κΈ°λ₯ μΆκ° μ κΈ°μ‘΄ μ½λλ₯Ό μμ ν΄μΌ νλκ°?
- LSP: λΆλͺ¨λ₯Ό μμμΌλ‘ λ°κΏλ μ μ λμνλκ°?
- ISP: μ¬μ©νμ§ μλ λ©μλμ μμ‘΄νκ³ μλκ°?
- DIP: ꡬ체 ν΄λμ€μ μ§μ μμ‘΄νκ³ μλκ°?
π‘ ν λ¨μ μ μ© μ λ΅
π₯ μμ ν (2-3λͺ )
// ν΅μ¬ μλΉμ€μλ§ μ νμ μ μ©
@Service
@RequiredArgsConstructor
public class OrderService { // ν΅μ¬ λΉμ¦λμ€ λ‘μ§λ§ SOLID μ μ©
private final PaymentGateway paymentGateway; // DIP
private final NotificationSender notificationSender; // ISP + OCP
}
@Service
public class ProductService { // λ¨μ CRUDλ κΈ°λ³Έ ꡬ쑰 μ μ§
private final ProductRepository repository;
}
π₯ μ€κ° ν (4-6λͺ )
// λͺ¨λλ³ λ΄λΉμ μ§μ , μΈν°νμ΄μ€ μ€μ¬ μ€κ³
public interface PaymentModule {
PaymentResult process(PaymentRequest request);
}
public interface NotificationModule {
void send(NotificationRequest request);
}
// κ° λͺ¨λμ λ
립μ μΌλ‘ κ°λ° κ°λ₯
π₯ ν° ν (7λͺ μ΄μ)
// μμ ν SOLID μ μ© + λλ©μΈλ³ λΆλ¦¬
// μ£Όλ¬Έ λλ©μΈ
@DomainService
public class OrderDomainService {
// λͺ¨λ SOLID μμΉ μ μ©
}
// κ²°μ λλ©μΈ
@DomainService
public class PaymentDomainService {
// λͺ¨λ SOLID μμΉ μ μ©
}
// λλ©μΈκ° ν΅μ μ μ΄λ²€νΈλ API κ²μ΄νΈμ¨μ΄ μ¬μ©
π― μ±κ³΅ μ§νμ μΈ‘μ
π μ λμ μ§ν
// 1. ν΄λμ€ ν¬κΈ° μΈ‘μ
// Before SOLID: νκ· 200μ€/ν΄λμ€
// After SOLID: νκ· 50-80μ€/ν΄λμ€
// 2. μμ‘΄μ± κ°μ
// Before: νλμ μλΉμ€κ° 10κ° μ΄μμ ꡬ체 ν΄λμ€μ μμ‘΄
// After: μΈν°νμ΄μ€ μμ‘΄μΌλ‘ κ²°ν©λ κ°μ
// 3. ν
μ€νΈ 컀λ²λ¦¬μ§
// Before: ν΅ν©ν
μ€νΈ μμ£Όλ‘ λλ¦° ν
μ€νΈ
// After: λ¨μν
μ€νΈ μ¦κ°λ‘ λΉ λ₯Έ νΌλλ°±
π μ μ±μ μ§ν
- μ κΈ°λ₯ μΆκ° μκ°: κΈ°μ‘΄ μ½λ μμ μμ΄ μΆκ° κ°λ₯
- λ²κ·Έ μ κ°μ: μ± μμ΄ λͺ νν΄μ Έ λ²κ·Έ λ°μ μ§μ λͺ ν
- ν μμ°μ±: λͺ¨λλ³ λ 립 κ°λ°λ‘ μΆ©λ κ°μ
πͺ ν΅μ¬ μμΉ μ 리
π μ±κ³΅νλ κ°λ°μμ SOLID λ§μΈλμ
βμλ²½ν μ€κ³λ³΄λ€λ μ μ§μ κ°μ μ!β
5κ°μ§ μ€μ² μμΉ
- π― SRP: βμ΄ ν΄λμ€κ° λ³κ²½λλ μ΄μ κ° 2κ° μ΄μμ΄λ©΄ λΆλ¦¬λ₯Ό κ³ λ €νλ€β
- πͺ OCP: βμ κΈ°λ₯ μΆκ° μ κΈ°μ‘΄ μ½λλ₯Ό μμ νκ³ μλ€λ©΄ μ€κ³λ₯Ό μμ¬νλ€β
- π LSP: βλΆλͺ¨λ₯Ό μμμΌλ‘ λ°κΏ¨μ λ μμΈκ° λ°μνλ©΄ μ€κ³λ₯Ό λ€μ λ³Έλ€β
- π§© ISP: βμ¬μ©νμ§ μλ λ©μλλ₯Ό ꡬννκ³ μλ€λ©΄ μΈν°νμ΄μ€λ₯Ό λΆλ¦¬νλ€β
- β¬οΈ DIP: βν μ€νΈνκΈ° μ΄λ ΅λ€λ©΄ ꡬ체 ν΄λμ€μ μμ‘΄νκ³ μλ 건 μλμ§ νμΈνλ€β
π λ§λ¬΄λ¦¬: μ€λ¬΄μμ μ΄μλ¨λ SOLID
β‘ μ€λ¬΄ μ μ©μ ν©κΈλ₯
βSOLIDλ λͺ©μ μ΄ μλλΌ μλ¨μ΄λ€β
SOLID μμΉμ μ μ©νλ μ΄μ λ μμΉ μμ²΄κ° λͺ©μ μ΄ μλλΌ, λ€μμ μν΄μμ λλ€:
- π§ μ μ§λ³΄μμ±: 6κ°μ νμλ μ΄ν΄νκΈ° μ¬μ΄ μ½λ
- π νμ₯μ±: μλ‘μ΄ μꡬμ¬νμ λΉ λ₯΄κ² λμ
- π§ͺ ν μ€νΈ μ©μ΄μ±: μμ μ μΈ λ°°ν¬λ₯Ό μν κ²¬κ³ ν ν μ€νΈ
- π₯ νμ : νμλ€κ³Ό ν¨κ» μΌνκΈ° μ’μ μ½λ
π― μ€λ¬΄ μ μ© 3λ¨κ³ μμ½
1οΈβ£ μμ λ¨κ³: DIPλΆν°
// μμ‘΄μ± μ£Όμ
μΌλ‘ ν
μ€νΈ κ°λ₯ν μ½λ λ§λ€κΈ°
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository repository; // μΆμνμ μμ‘΄
}
2οΈβ£ λ°μ λ¨κ³: SRP + OCP
// μ±
μ λΆλ¦¬ + νμ₯ κ°λ₯ν ꡬ쑰
@Service
public class OrderService {
private final List<OrderValidator> validators; // SRP
private final List<PaymentProcessor> processors; // OCP
}
3οΈβ£ μμ± λ¨κ³: μ 체 μμΉ μ μ©
// λͺ¨λ SOLID μμΉμ΄ μμ°μ€λ½κ² λ
Ήμλ μ½λ
// κ° ν΄λμ€κ° λͺ
νν μ±
μμ κ°μ§κ³
// νμ₯μ μ΄λ €μμΌλ©°
// μμ νκ² μΉν κ°λ₯νκ³
// νμν μΈν°νμ΄μ€λ§ μμ‘΄νλ©°
// μΆμνμ μμ‘΄νλ ꡬ쑰
π λ§μ§λ§ μ‘°μΈ
SOLID μμΉμ λ§Ήλͺ©μ μΌλ‘ μ μ©νμ§ λ§μΈμ. νλ‘μ νΈμ κ·λͺ¨, νμ ν¬κΈ°, μꡬμ¬νμ 볡μ‘λλ₯Ό κ³ λ €ν΄μ μ μ ν μμ€μμ μ μ©νλ κ²μ΄ μ€μν©λλ€.
- μμ νλ‘μ νΈ: DIP μ λλ§μΌλ‘λ μΆ©λΆ
- μ€κ° νλ‘μ νΈ: SRP + DIP + OCP μ ν μ μ©
- ν° νλ‘μ νΈ: λͺ¨λ SOLID μμΉ μ κ·Ή νμ©
βμ€λμ μ νμ΄ 6κ°μ νμ λλ₯Ό λ§λ λ€β
μ§κΈ λΉμ₯μ 볡μ‘ν΄ λ³΄μΌ μ μμ§λ§, SOLID μμΉμ 체λν κ°λ°μλ λ λΉ λ₯΄κ³ , λ μμ νκ², λ μ¦κ²κ² κ°λ°ν μ μμ΅λλ€.
μ¬λ¬λΆμ κ°λ° μ¬μ μ SOLIDκ° λ λ ν λμΉ¨λ°μ΄ λκΈ°λ₯Ό λ°λλλ€! π§β¨