Home > Backend Development > πŸ“š[Backend Development] πŸš€ λ°±μ—”λ“œ 개발자 핡심 μ°Έκ³  λ¬Έμ„œ κ°€μ΄λ“œ

πŸ“š[Backend Development] πŸš€ λ°±μ—”λ“œ 개발자 핡심 μ°Έκ³  λ¬Έμ„œ κ°€μ΄λ“œ
Backend Development Spring Boot API Documentation Blueprint Guide

πŸš€ λ°±μ—”λ“œ 개발자 핡심 μ°Έκ³  λ¬Έμ„œ κ°€μ΄λ“œ

β€œAPI λͺ…μ„Έμ„œλŠ” β€˜λ¬΄μ—‡μ„β€™ 주고받을지에 λŒ€ν•œ 약속이라면, λ°±μ—”λ“œ κ°œλ°œμžλŠ” κ·Έ 약속을 μ§€ν‚€κΈ° μœ„ν•΄ β€˜μ–΄λ–»κ²Œβ€™ λ™μž‘ν•΄μ•Ό ν•˜λŠ”μ§€μ— λŒ€ν•œ ꡬ체적인 섀계도와 μ§€μΉ¨μ„œλ₯Ό μ°Έκ³ ν•˜μ—¬ λ‚΄λΆ€ λ‘œμ§μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.”

λ°±μ—”λ“œ κ°œλ°œμžκ°€ ν•΅μ‹¬μ μœΌλ‘œ μ°Έκ³ ν•˜λŠ” 것은 크게 μ„Έ κ°€μ§€μž…λ‹ˆλ‹€.

1. μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ (κΈ°νšμ„œ, User Story)
2. λ°μ΄ν„°λ² μ΄μŠ€ μ„€κ³„μ„œ (ERD, μŠ€ν‚€λ§ˆ μ •μ˜)
3. μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ


🎯 κ°œμš”: λ°±μ—”λ“œ 개발의 μ‚Όκ°ν˜•

πŸ“ λ°±μ—”λ“œ 개발 μ°Έκ³  λ¬Έμ„œμ˜ 관계도

// API λͺ…μ„Έμ„œ (μ™ΈλΆ€ 계약)
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping
    public ResponseEntity<UserResponse> createUser(@RequestBody UserCreateRequest request) {
        // πŸ“ μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ β†’ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 κ΅¬ν˜„
        // πŸ’Ύ λ°μ΄ν„°λ² μ΄μŠ€ μ„€κ³„μ„œ β†’ 데이터 μ €μž₯/쑰회 ꡬ쑰
        // πŸ›οΈ μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ β†’ μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동 방식
        
        UserResponse response = userService.createUser(request);
        return ResponseEntity.ok(response);
    }
}

핡심 이해: API λͺ…μ„Έμ„œλŠ” 겉λͺ¨μŠ΅(Interface) 이고, λ‚˜λ¨Έμ§€ μ„Έ λ¬Έμ„œλŠ” λ‚΄λΆ€ κ΅¬ν˜„(Implementation) 을 μœ„ν•œ μ„€κ³„λ„μž…λ‹ˆλ‹€.


πŸ“ 1. μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ (κΈ°νšμ„œ, User Story)

β€œκ°€μž₯ μ€‘μš”ν•˜κ³  근본적인 μ°Έκ³  자료 - λΉ„μ¦ˆλ‹ˆμŠ€ 둜직의 λͺ¨λ“  것이 λ‹΄κΈ΄ 보물지도”

❌ μš”κ΅¬μ‚¬ν•­ λ¬΄μ‹œν•œ 잘λͺ»λœ κ΅¬ν˜„

// μš”κ΅¬μ‚¬ν•­μ„ μ œλŒ€λ‘œ νŒŒμ•…ν•˜μ§€ μ•Šκ³  λ‹¨μˆœν•˜κ²Œ κ΅¬ν˜„ν•œ 예
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    
    // λ„ˆλ¬΄ λ‹¨μˆœν•œ κ΅¬ν˜„ - λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λˆ„λ½
    @Transactional
    public Order createOrder(OrderCreateRequest request) {
        Order order = Order.builder()
                .userId(request.getUserId())
                .productId(request.getProductId())
                .quantity(request.getQuantity())
                .status(OrderStatus.CONFIRMED) // 무쑰건 ν™•μ •? 문제!
                .build();
                
        return orderRepository.save(order);
    }
}

πŸ”₯ λ¬Έμ œμ λ“€:

  • λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ λˆ„λ½: 재고 확인, 결제 처리, 배솑비 계산 λ“±
  • μ˜ˆμ™Έ 상황 미처리: ν’ˆμ ˆ, λ―Έμ„±λ…„μž μ£Όλ¬Έ, μ‹œκ°„ μ œν•œ λ“±
  • 데이터 μ •ν•©μ„± λΆ€μ‘±: μ£Όλ¬Έ μƒνƒœ 관리, 이λ ₯ 좔적 λ“±

βœ… μš”κ΅¬μ‚¬ν•­ 기반 μ˜¬λ°”λ₯Έ κ΅¬ν˜„

// μ‹€μ œ μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ μ˜ˆμ‹œ
/**
 * [μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ 발췌]
 * 
 * UC-001: μƒν’ˆ μ£Όλ¬Έ 처리
 * 
 * 1. κΈ°λ³Έ κ·œμΉ™:
 *    - μ˜€ν›„ 10μ‹œ 이후 주문은 λ‹€μŒ λ‚  μ•„μΉ¨ λ°°μ†‘μœΌλ‘œ 처리
 *    - VIP λ“±κΈ‰ νšŒμ›μ€ μƒν’ˆ κ°€κ²©μ˜ 5% μΆ”κ°€ 할인
 *    - μž¬κ³ κ°€ λΆ€μ‘±ν•  경우 주문을 λŒ€κΈ° μƒνƒœλ‘œ λ³€κ²½
 * 
 * 2. μ˜ˆμ™Έ 처리:
 *    - λ―Έμ„±λ…„μžλŠ” μ£Όλ₯˜ μ£Όλ¬Έ λΆˆκ°€ (μ£Όλ¬Έ 반렀 + μ•ˆλ‚΄ λ©”μ‹œμ§€)
 *    - 1회 μ£Όλ¬Έ μ΅œλŒ€ μˆ˜λŸ‰: 10개
 *    - ν’ˆμ ˆ μƒν’ˆμ€ μ£Όλ¬Έ λΆˆκ°€
 * 
 * 3. 데이터 μ •μ±…:
 *    - μ£Όλ¬Έ μ·¨μ†Œ μ‹œμ—λ„ 이λ ₯은 7λ…„κ°„ 보관
 *    - κ°œμΈμ •λ³΄λŠ” νšŒμ› νƒˆν‡΄ μ‹œ μ¦‰μ‹œ 파기, μ£Όλ¬Έ 내역은 5λ…„ 보관
 */

@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final ProductService productService;
    private final UserService userService;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final DiscountCalculator discountCalculator;
    private final DeliveryScheduler deliveryScheduler;
    private final NotificationService notificationService;
    
    @Transactional
    public OrderResult createOrder(OrderCreateRequest request) {
        // 1. μ‚¬μš©μž 검증
        User user = userService.findById(request.getUserId());
        validateUserOrderPermission(user, request);
        
        // 2. μƒν’ˆ 검증
        Product product = productService.findById(request.getProductId());
        validateProductOrderable(product, request);
        
        // 3. 재고 확인 및 μ˜ˆμ•½
        InventoryReservation reservation = inventoryService.reserveStock(
            request.getProductId(), 
            request.getQuantity()
        );
        
        if (!reservation.isSuccess()) {
            // 재고 λΆ€μ‘± μ‹œ λŒ€κΈ° μƒνƒœλ‘œ 처리
            return handleOutOfStock(request, user);
        }
        
        try {
            // 4. 할인 계산 (VIP 5% μΆ”κ°€ 할인 λ“±)
            DiscountResult discount = discountCalculator.calculate(product, user, request.getQuantity());
            
            // 5. 배솑 일정 κ²°μ • (μ˜€ν›„ 10μ‹œ μ΄ν›„λŠ” λ‹€μŒλ‚  배솑)
            DeliverySchedule schedule = deliveryScheduler.scheduleDelivery(LocalDateTime.now());
            
            // 6. μ£Όλ¬Έ 생성
            Order order = Order.builder()
                    .userId(user.getId())
                    .productId(product.getId())
                    .quantity(request.getQuantity())
                    .originalPrice(product.getPrice() * request.getQuantity())
                    .discountAmount(discount.getAmount())
                    .finalPrice(discount.getFinalPrice())
                    .status(OrderStatus.PENDING_PAYMENT)
                    .expectedDeliveryDate(schedule.getDeliveryDate())
                    .reservationId(reservation.getId())
                    .build();
            
            Order savedOrder = orderRepository.save(order);
            
            // 7. 결제 처리
            PaymentResult payment = paymentService.processPayment(
                PaymentRequest.builder()
                    .orderId(savedOrder.getId())
                    .amount(discount.getFinalPrice())
                    .paymentMethod(request.getPaymentMethod())
                    .build()
            );
            
            if (payment.isSuccess()) {
                savedOrder.confirmPayment(payment.getTransactionId());
                orderRepository.save(savedOrder);
                
                // 8. 후속 처리
                inventoryService.confirmReservation(reservation.getId());
                notificationService.sendOrderConfirmation(user, savedOrder);
                
                return OrderResult.success(savedOrder);
            } else {
                // 결제 μ‹€νŒ¨ μ‹œ 재고 μ˜ˆμ•½ ν•΄μ œ
                inventoryService.releaseReservation(reservation.getId());
                return OrderResult.paymentFailed(payment.getErrorMessage());
            }
            
        } catch (Exception e) {
            // μ˜ˆμ™Έ λ°œμƒ μ‹œ 재고 μ˜ˆμ•½ ν•΄μ œ
            inventoryService.releaseReservation(reservation.getId());
            throw new OrderProcessingException("μ£Όλ¬Έ 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€", e);
        }
    }
    
    // μš”κ΅¬μ‚¬ν•­: λ―Έμ„±λ…„μž μ£Όλ₯˜ μ£Όλ¬Έ 검증
    private void validateUserOrderPermission(User user, OrderCreateRequest request) {
        Product product = productService.findById(request.getProductId());
        
        if (product.isAlcohol() && user.isMinor()) {
            throw new OrderNotAllowedException(
                "λ―Έμ„±λ…„μžλŠ” μ£Όλ₯˜λ₯Ό μ£Όλ¬Έν•  수 μ—†μŠ΅λ‹ˆλ‹€",
                OrderErrorCode.MINOR_ALCOHOL_ORDER
            );
        }
        
        if (request.getQuantity() > 10) {
            throw new OrderNotAllowedException(
                "1회 μ£Όλ¬Έ μ΅œλŒ€ μˆ˜λŸ‰μ€ 10κ°œμž…λ‹ˆλ‹€",
                OrderErrorCode.EXCEED_MAX_QUANTITY
            );
        }
    }
    
    // μš”κ΅¬μ‚¬ν•­: ν’ˆμ ˆ μƒν’ˆ μ£Όλ¬Έ λΆˆκ°€
    private void validateProductOrderable(Product product, OrderCreateRequest request) {
        if (product.isOutOfStock()) {
            throw new ProductNotOrderableException(
                "ν’ˆμ ˆλœ μƒν’ˆμž…λ‹ˆλ‹€",
                ProductErrorCode.OUT_OF_STOCK
            );
        }
        
        if (!product.isActive()) {
            throw new ProductNotOrderableException(
                "νŒλ§€κ°€ μ€‘λ‹¨λœ μƒν’ˆμž…λ‹ˆλ‹€",
                ProductErrorCode.INACTIVE_PRODUCT
            );
        }
    }
    
    // μš”κ΅¬μ‚¬ν•­: 재고 λΆ€μ‘± μ‹œ λŒ€κΈ° μƒνƒœ 처리
    private OrderResult handleOutOfStock(OrderCreateRequest request, User user) {
        WaitingOrder waitingOrder = WaitingOrder.builder()
                .userId(request.getUserId())
                .productId(request.getProductId())
                .quantity(request.getQuantity())
                .status(WaitingStatus.WAITING_FOR_STOCK)
                .createdAt(LocalDateTime.now())
                .build();
                
        waitingOrderRepository.save(waitingOrder);
        
        // 재고 μ•Œλ¦Ό μ‹ μ²­
        notificationService.subscribeStockNotification(user.getEmail(), request.getProductId());
        
        return OrderResult.waitingForStock(waitingOrder);
    }
}

// VIP 5% μΆ”κ°€ 할인 μ •μ±… κ΅¬ν˜„
@Component
public class DiscountCalculator {
    
    public DiscountResult calculate(Product product, User user, int quantity) {
        int originalPrice = product.getPrice() * quantity;
        int discountAmount = 0;
        
        // κΈ°λ³Έ 할인
        if (originalPrice >= 50000) {
            discountAmount += (int) (originalPrice * 0.1); // 10% κΈ°λ³Έ 할인
        }
        
        // VIP μΆ”κ°€ 할인 (μš”κ΅¬μ‚¬ν•­ 반영)
        if (user.isVip()) {
            discountAmount += (int) (originalPrice * 0.05); // 5% μΆ”κ°€ 할인
        }
        
        // μˆ˜λŸ‰ 할인
        if (quantity >= 5) {
            discountAmount += (int) (originalPrice * 0.03); // 3% μˆ˜λŸ‰ 할인
        }
        
        int finalPrice = originalPrice - discountAmount;
        
        return DiscountResult.builder()
                .originalPrice(originalPrice)
                .discountAmount(discountAmount)
                .finalPrice(finalPrice)
                .appliedRules(buildAppliedRules(user, quantity, originalPrice))
                .build();
    }
}

// μ˜€ν›„ 10μ‹œ 이후 λ‹€μŒλ‚  배솑 κ·œμΉ™ κ΅¬ν˜„
@Component
public class DeliveryScheduler {
    private static final int CUTOFF_HOUR = 22; // μ˜€ν›„ 10μ‹œ
    
    public DeliverySchedule scheduleDelivery(LocalDateTime orderTime) {
        LocalDate deliveryDate;
        
        // μš”κ΅¬μ‚¬ν•­: μ˜€ν›„ 10μ‹œ μ΄ν›„λŠ” λ‹€μŒλ‚  배솑
        if (orderTime.getHour() >= CUTOFF_HOUR) {
            deliveryDate = orderTime.toLocalDate().plusDays(2); // λ‹€μŒλ‚  배솑
        } else {
            deliveryDate = orderTime.toLocalDate().plusDays(1); // 당일 배솑
        }
        
        // 주말/곡휴일 처리
        while (isWeekendOrHoliday(deliveryDate)) {
            deliveryDate = deliveryDate.plusDays(1);
        }
        
        return DeliverySchedule.builder()
                .deliveryDate(deliveryDate)
                .cutoffTime(orderTime.toLocalDate().atTime(CUTOFF_HOUR, 0))
                .isNextDayDelivery(orderTime.getHour() < CUTOFF_HOUR)
                .build();
    }
}

πŸŽ‰ μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ ν™œμš© 효과:

  • μ •ν™•ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직: λͺ¨λ“  업무 κ·œμΉ™μ΄ μ½”λ“œμ— 반영
  • μ˜ˆμ™Έ 상황 μ™„λ²½ λŒ€μ‘: λ―Έμ„±λ…„μž μ£Όλ¬Έ, 재고 λΆ€μ‘± λ“± λͺ¨λ“  μΌ€μ΄μŠ€ 처리
  • 데이터 μ •ν•©μ„± 보μž₯: μ£Όλ¬Έ μƒνƒœ 관리, 이λ ₯ 좔적 μ™„λ²½ κ΅¬ν˜„
  • μ‚¬μš©μž κ²½ν—˜ ν–₯상: λͺ…ν™•ν•œ μ—λŸ¬ λ©”μ‹œμ§€μ™€ λŒ€μ•ˆ μ œμ‹œ

πŸ’Ύ 2. λ°μ΄ν„°λ² μ΄μŠ€ μ„€κ³„μ„œ (ERD, μŠ€ν‚€λ§ˆ μ •μ˜)

β€œλ°μ΄ν„°μ˜ 청사진 - μ—”ν‹°ν‹° 관계와 μ œμ•½ 쑰건이 μ½”λ“œ ꡬ쑰λ₯Ό κ²°μ •ν•œλ‹€β€

❌ ERDλ₯Ό λ¬΄μ‹œν•œ 잘λͺ»λœ κ΅¬ν˜„

// ERDλ₯Ό μ œλŒ€λ‘œ λΆ„μ„ν•˜μ§€ μ•Šκ³  κ΅¬ν˜„ν•œ 문제 μ½”λ“œ
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 잘λͺ»λœ 연관관계 - ERD λ¬΄μ‹œ
    private Long userId;        // μ™Έλž˜ν‚€λ₯Ό λ‹¨μˆœ 숫자둜 처리
    private Long productId;     // 연관관계 λ§€ν•‘ λˆ„λ½
    private String productName; // λΉ„μ •κ·œν™” - 데이터 쀑볡
    private int price;          // Product ν…Œμ΄λΈ”μ— 이미 μžˆλŠ” 데이터 쀑볡
    
    // μ œμ•½ 쑰건 λ¬΄μ‹œ
    private int quantity;       // μ΅œμ†Œκ°’ μ œμ•½ μ—†μŒ
    private String status;      // Enum μ‚¬μš©ν•˜μ§€ μ•ŠμŒ
}

@Service
public class OrderService {
    
    public Order createOrder(OrderCreateRequest request) {
        // λΉ„νš¨μœ¨μ μΈ 데이터 쑰회 - N+1 문제 λ°œμƒ
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        
        // 별도 쑰회둜 데이터 쀑볡 μ €μž₯
        Product product = productService.findById(request.getProductId());
        order.setProductName(product.getName()); // 데이터 쀑볡!
        order.setPrice(product.getPrice());      // 데이터 쀑볡!
        
        return orderRepository.save(order);
    }
    
    // λΉ„νš¨μœ¨μ μΈ μ£Όλ¬Έ 쑰회
    public OrderDetailResponse getOrderDetail(Long orderId) {
        Order order = orderRepository.findById(orderId);
        
        // N+1 문제 - 맀번 별도 쿼리
        User user = userService.findById(order.getUserId());
        Product product = productService.findById(order.getProductId());
        
        return OrderDetailResponse.builder()
                .order(order)
                .user(user)
                .product(product)
                .build();
    }
}

πŸ”₯ λ¬Έμ œμ λ“€:

  • 데이터 쀑볡: Product 정보λ₯Ό Order에 쀑볡 μ €μž₯
  • 연관관계 λˆ„λ½: JPA 연관관계 λ§€ν•‘ λ―Έμ‚¬μš©μœΌλ‘œ N+1 문제
  • μ œμ•½ 쑰건 λ¬΄μ‹œ: 데이터 무결성 보μž₯ λΆˆκ°€
  • μ„±λŠ₯ μ €ν•˜: λΉ„νš¨μœ¨μ μΈ 쿼리둜 μ„±λŠ₯ 문제

βœ… ERD 기반 μ˜¬λ°”λ₯Έ κ΅¬ν˜„

// ERD μ„€κ³„μ„œ μ˜ˆμ‹œ:
// User (1) ←→ (N) Order (N) ←→ (1) Product
// Order (1) ←→ (N) OrderHistory
// User (1) ←→ (N) Review ←→ (1) Product

// 1. ERD 기반 μ—”ν‹°ν‹° 섀계
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true) // ERD μ œμ•½ 쑰건 반영
    private String email;
    
    @Column(nullable = false)
    private String name;
    
    @Enumerated(EnumType.STRING)
    private UserGrade grade; // VIP, REGULAR, BRONZE
    
    @Column(nullable = false)
    private LocalDate birthDate;
    
    // ERD 관계 μ •μ˜μ— λ”°λ₯Έ 연관관계 λ§€ν•‘
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();
    
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Review> reviews = new ArrayList<>();
    
    // λΉ„μ¦ˆλ‹ˆμŠ€ λ©”μ„œλ“œ - μš”κ΅¬μ‚¬ν•­ 반영
    public boolean isMinor() {
        return Period.between(birthDate, LocalDate.now()).getYears() < 19;
    }
    
    public boolean isVip() {
        return UserGrade.VIP.equals(grade);
    }
}

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false)
    private int price;
    
    @Column(nullable = false)
    private int stockQuantity;
    
    @Enumerated(EnumType.STRING)
    private ProductCategory category;
    
    @Column(nullable = false)
    private boolean isActive;
    
    // ERD 관계 μ •μ˜μ— λ”°λ₯Έ 연관관계 λ§€ν•‘
    @OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();
    
    @OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
    private List<Review> reviews = new ArrayList<>();
    
    // λΉ„μ¦ˆλ‹ˆμŠ€ λ©”μ„œλ“œ
    public boolean isAlcohol() {
        return ProductCategory.ALCOHOL.equals(category);
    }
    
    public boolean isOutOfStock() {
        return stockQuantity <= 0;
    }
    
    public boolean canOrder(int requestQuantity) {
        return isActive && stockQuantity >= requestQuantity;
    }
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // ERD μ™Έλž˜ν‚€ 관계λ₯Ό JPA μ—°κ΄€κ΄€κ³„λ‘œ μ •ν™•νžˆ λ§€ν•‘
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;
    
    // ERD μ œμ•½ 쑰건 반영
    @Column(nullable = false)
    @Min(value = 1, message = "μ£Όλ¬Έ μˆ˜λŸ‰μ€ 1개 이상이어야 ν•©λ‹ˆλ‹€")
    @Max(value = 10, message = "1회 μ£Όλ¬Έ μ΅œλŒ€ μˆ˜λŸ‰μ€ 10κ°œμž…λ‹ˆλ‹€")
    private int quantity;
    
    @Column(nullable = false)
    private int originalPrice;
    
    @Column(nullable = false)
    private int discountAmount;
    
    @Column(nullable = false)
    private int finalPrice;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status;
    
    @Column(nullable = false)
    private LocalDateTime orderDate;
    
    private LocalDate expectedDeliveryDate;
    
    private String paymentTransactionId;
    
    // ERD 관계 μ •μ˜μ— λ”°λ₯Έ 이λ ₯ 관리
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderHistory> histories = new ArrayList<>();
    
    // λΉ„μ¦ˆλ‹ˆμŠ€ λ©”μ„œλ“œ
    public void confirmPayment(String transactionId) {
        this.paymentTransactionId = transactionId;
        this.status = OrderStatus.CONFIRMED;
        addHistory(OrderStatus.CONFIRMED, "결제 μ™„λ£Œ");
    }
    
    public void cancel(String reason) {
        if (status == OrderStatus.SHIPPED) {
            throw new OrderCancelNotAllowedException("배솑 쀑인 주문은 μ·¨μ†Œν•  수 μ—†μŠ΅λ‹ˆλ‹€");
        }
        this.status = OrderStatus.CANCELLED;
        addHistory(OrderStatus.CANCELLED, reason);
    }
    
    private void addHistory(OrderStatus status, String memo) {
        OrderHistory history = OrderHistory.builder()
                .order(this)
                .status(status)
                .memo(memo)
                .createdAt(LocalDateTime.now())
                .build();
        histories.add(history);
    }
}

// ERD 이λ ₯ 관리 μš”κ΅¬μ‚¬ν•­ 반영
@Entity
@Table(name = "order_histories")
public class OrderHistory {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status;
    
    private String memo;
    
    @Column(nullable = false)
    private LocalDateTime createdAt;
}

// 2. ERD 관계λ₯Ό ν™œμš©ν•œ 효율적인 Repository
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    // ERD 관계λ₯Ό ν™œμš©ν•œ Fetch Join - N+1 문제 ν•΄κ²°
    @Query("SELECT o FROM Order o " +
           "JOIN FETCH o.user " +
           "JOIN FETCH o.product " +
           "WHERE o.id = :orderId")
    Optional<Order> findByIdWithUserAndProduct(@Param("orderId") Long orderId);
    
    // ERD 인덱슀 ν™œμš©ν•œ 효율적 쑰회
    @Query("SELECT o FROM Order o " +
           "JOIN FETCH o.user " +
           "WHERE o.user.id = :userId " +
           "AND o.orderDate BETWEEN :startDate AND :endDate " +
           "ORDER BY o.orderDate DESC")
    List<Order> findUserOrdersBetween(
        @Param("userId") Long userId,
        @Param("startDate") LocalDateTime startDate,
        @Param("endDate") LocalDateTime endDate
    );
    
    // ERD 집계 ν•¨μˆ˜ ν™œμš©
    @Query("SELECT COUNT(o) FROM Order o " +
           "WHERE o.product.id = :productId " +
           "AND o.status = :status")
    int countByProductAndStatus(
        @Param("productId") Long productId, 
        @Param("status") OrderStatus status
    );
}

// 3. ERD 기반 μ„œλΉ„μŠ€ κ΅¬ν˜„
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    
    // ERD 관계λ₯Ό ν™œμš©ν•œ 효율적 쑰회
    @Transactional(readOnly = true)
    public OrderDetailResponse getOrderDetail(Long orderId) {
        // ν•œ 번의 쿼리둜 λͺ¨λ“  κ΄€λ ¨ 데이터 쑰회
        Order order = orderRepository.findByIdWithUserAndProduct(orderId)
                .orElseThrow(() -> new OrderNotFoundException(orderId));
        
        return OrderDetailResponse.builder()
                .orderId(order.getId())
                .userName(order.getUser().getName())
                .userEmail(order.getUser().getEmail())
                .productName(order.getProduct().getName())
                .productPrice(order.getProduct().getPrice())
                .quantity(order.getQuantity())
                .finalPrice(order.getFinalPrice())
                .status(order.getStatus())
                .orderDate(order.getOrderDate())
                .histories(order.getHistories())
                .build();
    }
    
    // ERD μ œμ•½ 쑰건을 ν™œμš©ν•œ 데이터 검증
    @Transactional
    public Order createOrder(OrderCreateRequest request) {
        // ERD μ™Έλž˜ν‚€ μ œμ•½ 쑰건 ν™œμš©
        User user = userRepository.findById(request.getUserId())
                .orElseThrow(() -> new UserNotFoundException());
        Product product = productRepository.findById(request.getProductId())
                .orElseThrow(() -> new ProductNotFoundException());
        
        // ERD 체크 μ œμ•½ 쑰건 반영
        validateOrderConstraints(request, user, product);
        
        Order order = Order.builder()
                .user(user)           // μ—”ν‹°ν‹° 연관관계 ν™œμš©
                .product(product)     // μ—”ν‹°ν‹° 연관관계 ν™œμš©
                .quantity(request.getQuantity())
                .originalPrice(product.getPrice() * request.getQuantity())
                .status(OrderStatus.PENDING_PAYMENT)
                .orderDate(LocalDateTime.now())
                .build();
        
        return orderRepository.save(order);
    }
}

πŸŽ‰ ERD ν™œμš© 효과:

  • 데이터 μ •ν•©μ„±: μ™Έλž˜ν‚€μ™€ μ œμ•½ 쑰건으둜 잘λͺ»λœ 데이터 λ°©μ§€
  • μ„±λŠ₯ μ΅œμ ν™”: Fetch Join으둜 N+1 문제 ν•΄κ²°
  • μœ μ§€λ³΄μˆ˜μ„±: λͺ…ν™•ν•œ μ—”ν‹°ν‹° κ΄€κ³„λ‘œ μ΄ν•΄ν•˜κΈ° μ‰¬μš΄ μ½”λ“œ
  • ν™•μž₯μ„±: μƒˆλ‘œμš΄ μ—”ν‹°ν‹° μΆ”κ°€ μ‹œ κΈ°μ‘΄ 관계 μœ μ§€

πŸ›οΈ 3. μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ

β€œμ™ΈλΆ€ μ„Έκ³„μ™€μ˜ μ†Œν†΅ 방식 - μ‹œμŠ€ν…œ κ°„ μƒν˜Έμž‘μš©μ˜ 섀계도”

❌ μ•„ν‚€ν…μ²˜ λ¬΄μ‹œν•œ 잘λͺ»λœ κ΅¬ν˜„

// μ•„ν‚€ν…μ²˜ 섀계 없이 λ¬΄μž‘μ • κ΅¬ν˜„ν•œ 문제 μ½”λ“œ
@Service
public class PaymentService {
    
    public PaymentResult processPayment(PaymentRequest request) {
        // ν•˜λ“œμ½”λ”©λœ μ™ΈλΆ€ API 호좜 - μ„€κ³„μ„œ λ¬΄μ‹œ
        try {
            // 카카였페이 APIλ₯Ό 직접 호좜
            String url = "https://kapi.kakao.com/v1/payment/ready";
            String response = restTemplate.postForObject(url, request, String.class);
            
            // 응닡 νŒŒμ‹±λ„ ν•˜λ“œμ½”λ”©
            if (response.contains("SUCCESS")) {
                return new PaymentResult(true, "결제 성곡");
            } else {
                return new PaymentResult(false, "결제 μ‹€νŒ¨");
            }
            
        } catch (Exception e) {
            // μ—λŸ¬ μ²˜λ¦¬λ„ λŒ€μΆ©
            return new PaymentResult(false, "μ‹œμŠ€ν…œ 였λ₯˜");
        }
    }
    
    // 이미지 μ—…λ‘œλ“œλ„ μ•„ν‚€ν…μ²˜ κ³ λ € 없이 λ‘œμ»¬μ— μ €μž₯
    public String uploadProductImage(MultipartFile file) {
        String uploadDir = "/var/uploads/"; // ν•˜λ“œμ½”λ”©
        String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
        
        try {
            file.transferTo(new File(uploadDir + fileName));
            return "/images/" + fileName;
        } catch (IOException e) {
            throw new FileUploadException("파일 μ—…λ‘œλ“œ μ‹€νŒ¨");
        }
    }
}

πŸ”₯ λ¬Έμ œμ λ“€:

  • ν•˜λ“œμ½”λ”©λœ 연동: URL, 섀정값이 μ½”λ“œμ— λ°•ν˜€μžˆμŒ
  • μ—λŸ¬ 처리 λΆ€μ‘±: μ™ΈλΆ€ μ‹œμŠ€ν…œ μž₯μ•  상황 λ―Έκ³ λ €
  • ν™•μž₯μ„± λΆ€μ‘±: λ‹€λ₯Έ PG사 μΆ”κ°€ μ‹œ 전체 μ½”λ“œ μˆ˜μ • ν•„μš”
  • λ³΄μ•ˆ μ·¨μ•½: API ν‚€, 인증 정보 ν•˜λ“œμ½”λ”©

βœ… μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ 기반 μ˜¬λ°”λ₯Έ κ΅¬ν˜„

// μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ μ˜ˆμ‹œ:
/**
 * [μ‹œμŠ€ν…œ μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ 발췌]
 * 
 * 1. μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동:
 *    - 결제: 카카였페이 API β†’ ν–₯ν›„ ν† μŠ€νŽ˜μ΄λ¨ΌμΈ  μΆ”κ°€ μ˜ˆμ •
 *    - 파일 μ €μž₯: AWS S3 β†’ CDN 연동
 *    - λ©”μ‹œμ§€ 큐: Kafka β†’ 비동기 처리
 * 
 * 2. λ³΄μ•ˆ μ •μ±…:
 *    - API ν‚€λŠ” ν™˜κ²½λ³€μˆ˜ λ˜λŠ” AWS Secrets Manager μ‚¬μš©
 *    - λͺ¨λ“  μ™ΈλΆ€ API ν˜ΈμΆœμ€ Circuit Breaker νŒ¨ν„΄ 적용
 *    - κ°œμΈμ •λ³΄λŠ” AES-256 μ•”ν˜Έν™”
 * 
 * 3. μ„±λŠ₯ μš”κ΅¬μ‚¬ν•­:
 *    - 결제 API μ‘λ‹΅μ‹œκ°„: 3초 이내
 *    - 파일 μ—…λ‘œλ“œ: 10MB μ΄ν•˜, 5초 이내
 *    - λŒ€μš©λŸ‰ 처리: λ©”μ‹œμ§€ 큐 ν™œμš©ν•œ 비동기 처리
 */

// 1. μ™ΈλΆ€ 결제 μ‹œμŠ€ν…œ 연동 - μ•„ν‚€ν…μ²˜ 섀계 반영
@Configuration
@ConfigurationProperties(prefix = "payment.kakao")
@Data
public class KakaoPayConfig {
    private String apiUrl;
    private String secretKey;
    private int timeoutSeconds;
    private int retryCount;
}

// Circuit Breaker νŒ¨ν„΄ 적용 (μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ λ³΄μ•ˆ μ •μ±…)
@Component
@RequiredArgsConstructor
public class KakaoPayGateway {
    private final KakaoPayConfig config;
    private final RestTemplate restTemplate;
    private final CircuitBreaker circuitBreaker;
    
    public PaymentResult processPayment(PaymentRequest request) {
        return circuitBreaker.executeSupplier(() -> {
            try {
                // μ„€μ • 기반 API 호좜
                HttpHeaders headers = createHeaders();
                HttpEntity<KakaoPayRequest> entity = new HttpEntity<>(
                    convertToKakaoPayRequest(request), 
                    headers
                );
                
                ResponseEntity<KakaoPayResponse> response = restTemplate.exchange(
                    config.getApiUrl() + "/payment/ready",
                    HttpMethod.POST,
                    entity,
                    KakaoPayResponse.class
                );
                
                return convertToPaymentResult(response.getBody());
                
            } catch (HttpClientErrorException e) {
                // μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œμ˜ μ—λŸ¬ 처리 μ •μ±…
                handleClientError(e);
                throw new PaymentClientException("결제 μš”μ²­ 였λ₯˜: " + e.getMessage());
            } catch (HttpServerErrorException e) {
                handleServerError(e);
                throw new PaymentServerException("결제 μ„œλ²„ 였λ₯˜: " + e.getMessage());
            } catch (ResourceAccessException e) {
                throw new PaymentTimeoutException("결제 μ‹œμŠ€ν…œ 응닡 μ§€μ—°");
            }
        });
    }
    
    private HttpHeaders createHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(config.getSecretKey()); // ν™˜κ²½λ³€μˆ˜μ—μ„œ λ‘œλ“œ
        return headers;
    }
}

// 2. 파일 μ €μž₯ - AWS S3 연동 (μ•„ν‚€ν…μ²˜ 섀계)
@Configuration
@ConfigurationProperties(prefix = "aws.s3")
@Data
public class S3Config {
    private String bucketName;
    private String region;
    private String accessKey;
    private String secretKey;
    private String cdnUrl;
}

@Component
@RequiredArgsConstructor
public class S3FileUploader {
    private final S3Config s3Config;
    private final AmazonS3 s3Client;
    
    public FileUploadResult uploadFile(MultipartFile file, String directory) {
        // μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œμ˜ 파일 μ •μ±… 반영
        validateFile(file);
        
        try {
            String fileName = generateFileName(file.getOriginalFilename());
            String s3Key = directory + "/" + fileName;
            
            // S3 μ—…λ‘œλ“œ
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType(file.getContentType());
            metadata.setContentLength(file.getSize());
            
            s3Client.putObject(
                s3Config.getBucketName(),
                s3Key,
                file.getInputStream(),
                metadata
            );
            
            // CDN URL λ°˜ν™˜ (μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ 반영)
            String fileUrl = s3Config.getCdnUrl() + "/" + s3Key;
            
            return FileUploadResult.builder()
                    .originalFileName(file.getOriginalFilename())
                    .storedFileName(fileName)
                    .fileUrl(fileUrl)
                    .fileSize(file.getSize())
                    .uploadedAt(LocalDateTime.now())
                    .build();
                    
        } catch (IOException e) {
            throw new FileUploadException("파일 μ—…λ‘œλ“œ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€", e);
        }
    }
    
    // μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œμ˜ 파일 μ •μ±… 반영
    private void validateFile(MultipartFile file) {
        if (file.isEmpty()) {
            throw new InvalidFileException("빈 νŒŒμΌμ€ μ—…λ‘œλ“œν•  수 μ—†μŠ΅λ‹ˆλ‹€");
        }
        
        if (file.getSize() > 10 * 1024 * 1024) { // 10MB μ œν•œ
            throw new FileSizeExceededException("파일 ν¬κΈ°λŠ” 10MBλ₯Ό μ΄ˆκ³Όν•  수 μ—†μŠ΅λ‹ˆλ‹€");
        }
        
        String contentType = file.getContentType();
        if (!isAllowedContentType(contentType)) {
            throw new UnsupportedFileTypeException("μ§€μ›ν•˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹μž…λ‹ˆλ‹€");
        }
    }
}

// 3. λŒ€μš©λŸ‰ 처리 - Kafka λ©”μ‹œμ§€ 큐 (μ•„ν‚€ν…μ²˜ 섀계)
@Component
@RequiredArgsConstructor
public class OrderEventPublisher {
    private final KafkaTemplate<String, Object> kafkaTemplate;
    
    @Async
    public void publishOrderCreated(Order order) {
        OrderCreatedEvent event = OrderCreatedEvent.builder()
                .orderId(order.getId())
                .userId(order.getUser().getId())
                .productId(order.getProduct().getId())
                .amount(order.getFinalPrice())
                .orderDate(order.getOrderDate())
                .build();
        
        // μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œμ˜ λ©”μ‹œμ§€ 큐 ν† ν”½ 반영
        kafkaTemplate.send("order-events", event);
    }
}

@KafkaListener(topics = "order-events", groupId = "email-service")
@Component
@RequiredArgsConstructor
public class OrderEmailHandler {
    private final EmailService emailService;
    
    // 비동기 이메일 λ°œμ†‘ - μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ 반영
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            User user = userService.findById(event.getUserId());
            Product product = productService.findById(event.getProductId());
            
            EmailMessage message = EmailMessage.builder()
                    .to(user.getEmail())
                    .subject("주문이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€")
                    .template("order-confirmation")
                    .templateData(Map.of(
                        "userName", user.getName(),
                        "productName", product.getName(),
                        "amount", event.getAmount()
                    ))
                    .build();
            
            emailService.sendEmail(message);
            
        } catch (Exception e) {
            // λ©”μ‹œμ§€ 큐 재처리λ₯Ό μœ„ν•œ μ˜ˆμ™Έ 처리
            log.error("μ£Όλ¬Έ 이메일 λ°œμ†‘ μ‹€νŒ¨: orderId={}", event.getOrderId(), e);
            throw new EmailSendException("이메일 λ°œμ†‘μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€", e);
        }
    }
}

// 4. λ³΄μ•ˆ μ •μ±… κ΅¬ν˜„ - Spring Security (μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/admin/**").hasRole("ADMIN")  // κ΄€λ¦¬μž API μ œν•œ
                .requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtDecoder(jwtDecoder()))
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
            
        return http.build();
    }
}

// 5. μ„€μ • 쀑심 ꡬ성 - μ•„ν‚€ν…μ²˜ μœ μ—°μ„± 확보
@Configuration
public class ExternalSystemConfig {
    
    @Bean
    @ConditionalOnProperty(name = "payment.provider", havingValue = "kakao")
    public PaymentGateway kakaoPayGateway() {
        return new KakaoPayGateway();
    }
    
    @Bean
    @ConditionalOnProperty(name = "payment.provider", havingValue = "toss")
    public PaymentGateway tossPayGateway() {
        return new TossPayGateway();
    }
    
    @Bean
    @ConditionalOnProperty(name = "file.storage", havingValue = "s3")
    public FileUploader s3FileUploader() {
        return new S3FileUploader();
    }
    
    @Bean
    @ConditionalOnProperty(name = "file.storage", havingValue = "local")
    public FileUploader localFileUploader() {
        return new LocalFileUploader();
    }
}

πŸŽ‰ μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ ν™œμš© 효과:

  • μœ μ—°ν•œ μ‹œμŠ€ν…œ: μ„€μ • λ³€κ²½λ§ŒμœΌλ‘œ λ‹€λ₯Έ μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동 κ°€λŠ₯
  • μ•ˆμ •μ„±: Circuit Breaker, μž¬μ‹œλ„ μ •μ±…μœΌλ‘œ μž₯μ•  상황 λŒ€μ‘
  • λ³΄μ•ˆ κ°•ν™”: 인증/인가, 데이터 μ•”ν˜Έν™” 체계적 적용
  • μ„±λŠ₯ μ΅œμ ν™”: 비동기 처리, 캐싱 μ „λž΅ 반영

βš–οΈ μ–Έμ œ μ–΄λ–€ λ¬Έμ„œλ₯Ό μ€‘μ μ μœΌλ‘œ 봐야 ν• κΉŒ?

🟒 ν”„λ‘œμ νŠΈ 초기 단계 (1-2μ£Όμ°¨)

πŸ’Ύ λ°μ΄ν„°λ² μ΄μŠ€ μ„€κ³„μ„œ μš°μ„  뢄석

// λ¨Όμ € ERDλ₯Ό λΆ„μ„ν•˜μ—¬ 도메인 λͺ¨λΈ νŒŒμ•…
@Entity
public class User {
    // ERD 뢄석 β†’ μ—”ν‹°ν‹° 섀계 β†’ JPA λ§€ν•‘
}

@Entity  
public class Order {
    // 관계 μ •μ˜ β†’ 연관관계 λ§€ν•‘ β†’ Repository 섀계
}

// ERD 기반으둜 κΈ°λ³Έ CRUD λ¨Όμ € κ΅¬ν˜„
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // ERD 인덱슀 기반 쿼리 λ©”μ„œλ“œ
}

🟑 개발 μ§„ν–‰ 단계 (3-6μ£Όμ°¨)

πŸ“ μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ 쀑심 κ΅¬ν˜„

// μš”κ΅¬μ‚¬ν•­ ν•˜λ‚˜μ”© Service 계측에 κ΅¬ν˜„
@Service
public class OrderService {
    
    // μš”κ΅¬μ‚¬ν•­: "VIP νšŒμ› 5% μΆ”κ°€ 할인"
    public DiscountResult calculateVipDiscount(User user, int amount) {
        if (user.isVip()) {
            return DiscountResult.of(amount * 0.05);
        }
        return DiscountResult.empty();
    }
    
    // μš”κ΅¬μ‚¬ν•­: "μ˜€ν›„ 10μ‹œ μ΄ν›„λŠ” λ‹€μŒλ‚  배솑"  
    public DeliveryDate calculateDeliveryDate(LocalDateTime orderTime) {
        if (orderTime.getHour() >= 22) {
            return DeliveryDate.nextDay(orderTime.toLocalDate().plusDays(1));
        }
        return DeliveryDate.sameDay(orderTime.toLocalDate());
    }
}

🟠 μ‹œμŠ€ν…œ 톡합 단계 (7-8μ£Όμ°¨)

πŸ›οΈ μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ 쀑심 κ΅¬ν˜„

// μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동 및 인프라 ꡬ성
@Configuration
public class ExternalIntegrationConfig {
    
    // 결제 μ‹œμŠ€ν…œ 연동
    @Bean
    public PaymentGateway paymentGateway() {
        return PaymentGatewayFactory.create(paymentConfig);
    }
    
    // 파일 μŠ€ν† λ¦¬μ§€ 연동
    @Bean
    public FileStorage fileStorage() {
        return FileStorageFactory.create(storageConfig);
    }
    
    // λ©”μ‹œμ§€ 큐 μ„€μ •
    @Bean
    public MessageProducer messageProducer() {
        return new KafkaMessageProducer(kafkaConfig);
    }
}

🚨 자주 ν•˜λŠ” μ‹€μˆ˜λ“€κ³Ό ν•΄κ²°μ±…

❌ μ‹€μˆ˜ 1: λ¬Έμ„œ κ°„ 뢈일치 λ¬΄μ‹œ

// API λͺ…μ„Έμ„œμ—λŠ” 간단해 λ³΄μ΄μ§€λ§Œ...
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderCreateRequest request) {
    // λ‹¨μˆœν•΄ λ³΄μ΄λŠ” API
}

// μ‹€μ œ μš”κ΅¬μ‚¬ν•­μ€ λ³΅μž‘ν•¨μ„ κ°„κ³Ό
@Service
public class OrderService {
    public Order createOrder(OrderCreateRequest request) {
        // 재고 확인은?
        // 할인 계산은?
        // λ°°μ†‘λΉ„λŠ”?
        // 결제 μ²˜λ¦¬λŠ”?
        // β†’ μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œλ₯Ό μ œλŒ€λ‘œ μ•ˆ λ΄€λ‹€!
        return orderRepository.save(new Order());
    }
}

βœ… ν•΄κ²°μ±…: λ¬Έμ„œ κ°„ 일관성 확인

// API λͺ…μ„Έμ„œ + μš”κ΅¬μ‚¬ν•­ + ERD + μ•„ν‚€ν…μ²˜λ₯Ό λͺ¨λ‘ κ³ λ €ν•œ κ΅¬ν˜„
@Service
@RequiredArgsConstructor
public class OrderService {
    // ERD β†’ Repository 연관관계
    private final OrderRepository orderRepository;
    private final UserRepository userRepository;
    
    // μ•„ν‚€ν…μ²˜ β†’ μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동
    private final PaymentGateway paymentGateway;
    private final InventoryService inventoryService;
    
    // μš”κ΅¬μ‚¬ν•­ β†’ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 κ΅¬ν˜„
    private final DiscountCalculator discountCalculator;
    private final DeliveryScheduler deliveryScheduler;
    
    @Transactional
    public OrderResult createOrder(OrderCreateRequest request) {
        // λ¬Έμ„œ 기반 체계적 κ΅¬ν˜„
        // 1. ERD 기반 데이터 쑰회
        // 2. μš”κ΅¬μ‚¬ν•­ 기반 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직
        // 3. μ•„ν‚€ν…μ²˜ 기반 μ™ΈλΆ€ 연동
    }
}

❌ μ‹€μˆ˜ 2: ERD와 JPA λ§€ν•‘ 뢈일치

// ERDμ—μ„œλŠ” User와 Orderκ°€ 1:N 관계인데...
@Entity
public class Order {
    private Long userId; // λ‹¨μˆœ μ™Έλž˜ν‚€ - 연관관계 λ§€ν•‘ λˆ„λ½!
}

// λΉ„νš¨μœ¨μ μΈ 쑰회 λ°œμƒ
public OrderDetailResponse getOrderDetail(Long orderId) {
    Order order = orderRepository.findById(orderId);
    User user = userService.findById(order.getUserId()); // N+1 문제!
}

βœ… ν•΄κ²°μ±…: ERD μΆ©μ‹€ν•œ λ§€ν•‘

@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user; // ERD 관계 μ •ν™•νžˆ λ§€ν•‘
}

// ERD 기반 효율적 쑰회
@Query("SELECT o FROM Order o JOIN FETCH o.user WHERE o.id = :orderId")
Optional<Order> findByIdWithUser(@Param("orderId") Long orderId);

❌ μ‹€μˆ˜ 3: μ•„ν‚€ν…μ²˜ 섀계 λ¬΄μ‹œν•œ ν•˜λ“œμ½”λ”©

// ν•˜λ“œμ½”λ”©λœ μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동
@Service
public class PaymentService {
    public void processPayment() {
        String url = "https://api.kakaopay.com"; // ν•˜λ“œμ½”λ”©!
        String apiKey = "12345"; // λ³΄μ•ˆ μ·¨μ•½!
        // ...
    }
}

βœ… ν•΄κ²°μ±…: μ„€μ • 기반 μœ μ—°ν•œ ꡬ쑰

@Configuration
@ConfigurationProperties(prefix = "external")
public class ExternalSystemConfig {
    private PaymentConfig payment;
    private StorageConfig storage;
    private NotificationConfig notification;
}

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentConfig config; // μ„€μ • μ£Όμž…
    private final PaymentGateway gateway; // μΆ”μƒν™”λœ κ²Œμ΄νŠΈμ›¨μ΄
}

πŸŽ“ 싀무 적용 λ‘œλ“œλ§΅

πŸƒβ€β™‚οΈ 1단계: λ¬Έμ„œ 읽기 ν›ˆλ ¨ (1μ£Ό)

πŸ“‹ 체크리슀트 λ§Œλ“€κΈ°

/**
 * ν”„λ‘œμ νŠΈ μ‹œμž‘ μ „ ν•„μˆ˜ 체크리슀트
 * 
 * β–‘ μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ
 *   - λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ λͺ¨λ‘ νŒŒμ•…ν–ˆλŠ”κ°€?
 *   - μ˜ˆμ™Έ 상황 처리 λ°©μ•ˆ μ΄ν•΄ν–ˆλŠ”κ°€?
 *   - 데이터 생λͺ…μ£ΌκΈ° μ •μ±… ν™•μΈν–ˆλŠ”κ°€?
 * 
 * β–‘ ERD μ„€κ³„μ„œ  
 *   - μ—”ν‹°ν‹° κ°„ 관계 μ •ν™•νžˆ μ΄ν•΄ν–ˆλŠ”κ°€?
 *   - μ œμ•½ 쑰건과 인덱슀 νŒŒμ•…ν–ˆλŠ”κ°€?
 *   - μ„±λŠ₯ 고렀사항 ν™•μΈν–ˆλŠ”κ°€?
 * 
 * β–‘ μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ
 *   - μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동 방식 μ΄ν•΄ν–ˆλŠ”κ°€?
 *   - λ³΄μ•ˆ μ •μ±…κ³Ό 인증 방식 νŒŒμ•…ν–ˆλŠ”κ°€?
 *   - μ„±λŠ₯ μš”κ΅¬μ‚¬ν•­κ³Ό μ œμ•½μ‚¬ν•­ ν™•μΈν–ˆλŠ”κ°€?
 */

πŸšΆβ€β™‚οΈ 2단계: 단계별 κ΅¬ν˜„ (2-3μ£Ό)

Phase 1: ERD 기반 도메인 λͺ¨λΈλ§

// 1μ£Όμ°¨: ERD β†’ JPA Entity λ§€ν•‘
@Entity
public class User { }

@Entity  
public class Order { }

@Repository
public interface OrderRepository { }

Phase 2: μš”κ΅¬μ‚¬ν•­ 기반 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직

// 2μ£Όμ°¨: μš”κ΅¬μ‚¬ν•­ β†’ Service 계측 κ΅¬ν˜„
@Service
public class OrderService {
    // λͺ¨λ“  λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™ κ΅¬ν˜„
}

Phase 3: μ•„ν‚€ν…μ²˜ 기반 μ‹œμŠ€ν…œ 연동

// 3μ£Όμ°¨: μ•„ν‚€ν…μ²˜ β†’ μ™ΈλΆ€ μ‹œμŠ€ν…œ 연동
@Component
public class PaymentGateway { }

@Component
public class FileUploader { }

πŸƒβ€β™‚οΈ 3단계: 톡합 및 μ΅œμ ν™” (1-2μ£Ό)

전체 ν”Œλ‘œμš° 톡합 ν…ŒμŠ€νŠΈ

@SpringBootTest
@TestPropertySource(properties = {
    "payment.provider=kakao",
    "file.storage=s3",
    "message.queue=kafka"
})
class OrderIntegrationTest {
    
    @Test
    void μ£Όλ¬Έ_전체_ν”Œλ‘œμš°_ν…ŒμŠ€νŠΈ() {
        // Given: μ‚¬μš©μž, μƒν’ˆ, 재고 μ€€λΉ„
        // When: μ£Όλ¬Έ 생성 API 호좜
        // Then: μš”κ΅¬μ‚¬ν•­μ— λ”°λ₯Έ λͺ¨λ“  둜직 검증
        //   - 재고 차감
        //   - 결제 처리  
        //   - 배솑 일정 생성
        //   - 이메일 λ°œμ†‘
        //   - 이λ ₯ 기둝
    }
}

πŸ’‘ νŒ€ λ‹¨μœ„ 적용 μ „λž΅

πŸ‘₯ μž‘μ€ νŒ€ (2-3λͺ…)

// 핡심 λ¬Έμ„œμ—λ§Œ 집쀑
@Service
@RequiredArgsConstructor
public class OrderService {
    // μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œ β†’ 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직만 집쀑 κ΅¬ν˜„
    // ERD β†’ κΈ°λ³Έ μ—°κ΄€κ΄€κ³„λ§Œ μ •ν™•νžˆ λ§€ν•‘
    // μ•„ν‚€ν…μ²˜ β†’ μ£Όμš” μ™ΈλΆ€ μ—°λ™λ§Œ μ„€μ • 기반 κ΅¬ν˜„
}

πŸ‘₯ 쀑간 νŒ€ (4-6λͺ…)

// λͺ¨λ“ˆλ³„ λ‹΄λ‹Ήμž μ§€μ •, λ¬Έμ„œλ³„ μ „λ¬Έκ°€ μ–‘μ„±
// μš”κ΅¬μ‚¬ν•­ μ „λ¬Έκ°€: λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λ‹΄λ‹Ή
// ERD μ „λ¬Έκ°€: 데이터 λͺ¨λΈλ§ λ‹΄λ‹Ή  
// μ•„ν‚€ν…μ²˜ μ „λ¬Έκ°€: 인프라/연동 λ‹΄λ‹Ή

@Service
public class OrderDomainService {
    // 각 λ„λ©”μΈλ³„λ‘œ μ „λ¬Έμ„± 확보
}

πŸ‘₯ 큰 νŒ€ (7λͺ… 이상)

// μ™„μ „ν•œ λ¬Έμ„œ 기반 개발 ν”„λ‘œμ„ΈμŠ€
// 1. λ¬Έμ„œ 리뷰 β†’ 2. 섀계 κ²€ν†  β†’ 3. κ΅¬ν˜„ β†’ 4. λ¬Έμ„œ 기반 ν…ŒμŠ€νŠΈ

@DomainService
public class OrderDomainService {
    // λͺ¨λ“  λ¬Έμ„œμ˜ λ‚΄μš©μ΄ μ™„λ²½ν•˜κ²Œ 반영된 κ΅¬ν˜„
}

πŸ“Š 성곡 μ§€ν‘œμ™€ μΈ‘μ •

πŸ“ˆ μ •λŸ‰μ  μ§€ν‘œ

// 1. μš”κ΅¬μ‚¬ν•­ 반영λ₯ 
// Before: κΈ°λŠ₯ κ΅¬ν˜„λ₯  60% (핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 λˆ„λ½)
// After: κΈ°λŠ₯ κ΅¬ν˜„λ₯  95% (λͺ¨λ“  μš”κ΅¬μ‚¬ν•­ 체계적 반영)

// 2. 데이터 무결성
// Before: 데이터 였λ₯˜ μ›” 10건 이상
// After: ERD 기반 μ œμ•½ 쑰건으둜 데이터 였λ₯˜ 0건

// 3. μ™ΈλΆ€ 연동 μ•ˆμ •μ„±  
// Before: μ™ΈλΆ€ API μž₯μ•  μ‹œ 전체 μ‹œμŠ€ν…œ λ‹€μš΄
// After: Circuit Breaker둜 99.9% κ°€μš©μ„± 확보

πŸ“‹ 정성적 μ§€ν‘œ

  • 개발 속도: λ¬Έμ„œ 기반 체계적 개발둜 μž¬μž‘μ—… μ‹œκ°„ 단좕
  • 버그 κ°μ†Œ: μš”κ΅¬μ‚¬ν•­ λˆ„λ½μœΌλ‘œ μΈν•œ 버그 90% κ°μ†Œ
  • νŒ€ ν˜‘μ—…: 곡톡 λ¬Έμ„œ 기반으둜 μ˜μ‚¬μ†Œν†΅ λͺ…ν™•ν™”

πŸŽͺ 핡심 원칙 정리

πŸ† μ„±κ³΅ν•˜λŠ” λ°±μ—”λ“œ 개발자의 λ¬Έμ„œ ν™œμš© λ§ˆμΈλ“œμ…‹

β€œμ½”λ”©ν•˜κΈ° 전에 λ¬Έμ„œλΆ€ν„° - κΈ‰ν• μˆ˜λ‘ 섀계뢀터”

3κ°€μ§€ μ‹€μ²œ 원칙

  1. πŸ“ μš”κ΅¬μ‚¬ν•­ μš°μ„ : β€œμ΄ κΈ°λŠ₯이 μ™œ ν•„μš”ν•œμ§€, μ–΄λ–€ μ‘°κ±΄μ—μ„œ μ–΄λ–»κ²Œ λ™μž‘ν•΄μ•Ό ν•˜λŠ”μ§€ λ¨Όμ € νŒŒμ•…ν•œλ‹€β€
  2. πŸ’Ύ 데이터 쀑심: β€œERDλ₯Ό 보고 μ—”ν‹°ν‹° 관계λ₯Ό μ •ν™•νžˆ μ΄ν•΄ν•œ ν›„ JPA 맀핑을 κ΅¬ν˜„ν•œλ‹€β€
  3. πŸ›οΈ μ•„ν‚€ν…μ²˜ κ³ λ €: β€œμ™ΈλΆ€ μ‹œμŠ€ν…œ 연동은 항상 μ„€μ • 기반으둜, μž₯μ•  상황을 κ³ λ €ν•˜μ—¬ κ΅¬ν˜„ν•œλ‹€β€

πŸš€ 마무리: μ‹€λ¬΄μ—μ„œ μ‚΄μ•„λ‚¨λŠ” λ°±μ—”λ“œ 개발

⚑ 싀무 적용의 ν™©κΈˆλ₯ 

β€œλ¬Έμ„œλŠ” μ œμ•½μ΄ μ•„λ‹ˆλΌ 자유λ₯Ό μ£ΌλŠ” 섀계도닀”

λ°±μ—”λ“œ κ°œλ°œμžκ°€ 이 μ„Έ κ°€μ§€ λ¬Έμ„œλ₯Ό μ œλŒ€λ‘œ ν™œμš©ν•˜λŠ” μ΄μœ λŠ” λ¬Έμ„œ μžμ²΄κ°€ λͺ©μ μ΄ μ•„λ‹ˆλΌ, λ‹€μŒμ„ μœ„ν•΄μ„œμž…λ‹ˆλ‹€:

  • πŸ”§ μ •ν™•ν•œ κ΅¬ν˜„: 6κ°œμ›” 후에도 μ™œ μ΄λ ‡κ²Œ κ΅¬ν˜„ν–ˆλŠ”μ§€ μ•Œ 수 μžˆλŠ” μ½”λ“œ
  • πŸš€ λΉ λ₯Έ 개발: λ¬Έμ„œ 기반 체계적 μ ‘κ·ΌμœΌλ‘œ μ‹œν–‰μ°©μ˜€ μ΅œμ†Œν™”
  • πŸ§ͺ μ•ˆμ •μ„±: λͺ¨λ“  μš”κ΅¬μ‚¬ν•­κ³Ό μ œμ•½μ‘°κ±΄μ΄ 반영된 κ²¬κ³ ν•œ μ‹œμŠ€ν…œ
  • πŸ‘₯ ν˜‘μ—…: 기획자, λ””μžμ΄λ„ˆ, QA와 μ›ν™œν•œ μ†Œν†΅

🎯 싀무 적용 3단계 μš”μ•½

1️⃣ μ€€λΉ„ 단계: λ¬Έμ„œ 이해

// μ„Έ λ¬Έμ„œλ₯Ό λ¨Όμ € μ •λ…ν•˜κ³  핡심 포인트 정리
// μš”κ΅¬μ‚¬ν•­ β†’ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 ν”Œλ‘œμš°μ°¨νŠΈ μž‘μ„±
// ERD β†’ μ—”ν‹°ν‹° 관계도 그렀보기  
// μ•„ν‚€ν…μ²˜ β†’ μ‹œμŠ€ν…œ 연동 μ‹œν€€μŠ€ λ‹€μ΄μ–΄κ·Έλž¨ μž‘μ„±

2️⃣ κ΅¬ν˜„ 단계: λ¬Έμ„œ 기반 μ½”λ”©

// ERD β†’ Entity β†’ Repository β†’ Service (μš”κ΅¬μ‚¬ν•­) β†’ Controller (API λͺ…μ„Έμ„œ)
// μ•„ν‚€ν…μ²˜ β†’ μ™ΈλΆ€ 연동 κ΅¬ν˜„
public class OrderService {
    // λͺ¨λ“  λ¬Έμ„œμ˜ λ‚΄μš©μ΄ μ½”λ“œμ— μ •ν™•νžˆ 반영
}

3️⃣ 검증 단계: λ¬Έμ„œ 기반 ν…ŒμŠ€νŠΈ

@Test
void μš”κ΅¬μ‚¬ν•­_μ‹œλ‚˜λ¦¬μ˜€_ν…ŒμŠ€νŠΈ() {
    // μš”κ΅¬μ‚¬ν•­ λͺ…μ„Έμ„œμ˜ λͺ¨λ“  μ‹œλ‚˜λ¦¬μ˜€λ₯Ό ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€λ‘œ μž‘μ„±
    // Given: λ¬Έμ„œμ— μ •μ˜λœ μ „μ œ 쑰건
    // When: λ¬Έμ„œμ— μ •μ˜λœ μ•‘μ…˜  
    // Then: λ¬Έμ„œμ— μ •μ˜λœ κΈ°λŒ€ κ²°κ³Ό
}

🎁 λ§ˆμ§€λ§‰ μ‘°μ–Έ

μ„Έ λ¬Έμ„œλ₯Ό λ§Ήλͺ©μ μœΌλ‘œ λ”°λ₯΄μ§€ λ§ˆμ„Έμš”. ν”„λ‘œμ νŠΈμ˜ 규λͺ¨, νŒ€μ˜ μ—­λŸ‰, μΌμ •μ˜ ν˜„μ‹€μ„±μ„ κ³ λ €ν•΄μ„œ μ μ ˆν•œ μˆ˜μ€€μ—μ„œ ν™œμš©ν•˜λŠ” 것이 μ€‘μš”ν•©λ‹ˆλ‹€.

  • μž‘μ€ ν”„λ‘œμ νŠΈ: μš”κ΅¬μ‚¬ν•­ 쀑심 + κΈ°λ³Έ ERD λ§€ν•‘
  • 쀑간 ν”„λ‘œμ νŠΈ: μ„Έ λ¬Έμ„œ κ· ν˜• 있게 ν™œμš©
  • 큰 ν”„λ‘œμ νŠΈ: λͺ¨λ“  λ¬Έμ„œ λ‚΄μš© μ™„λ²½ 반영 + λ¬Έμ„œ 기반 μ½”λ“œ 리뷰

β€œμ˜€λŠ˜μ˜ λ¬Έμ„œ 이해가 6κ°œμ›” ν›„μ˜ λ‚˜λ₯Ό λ§Œλ“ λ‹€β€

μ§€κΈˆ λ‹Ήμž₯은 λ²ˆκ±°λ‘œμ›Œ 보일 수 μžˆμ§€λ§Œ, λ¬Έμ„œ 기반 κ°œλ°œμ„ μ²΄λ“ν•œ λ°±μ—”λ“œ κ°œλ°œμžλŠ” 더 μ •ν™•ν•˜κ³ , 더 λΉ λ₯΄κ²Œ, 더 μ•ˆμ „ν•˜κ²Œ κ°œλ°œν•  수 μžˆμŠ΅λ‹ˆλ‹€.


πŸ“š μΆ”κ°€ ν•™μŠ΅ 자료

πŸ” 심화 ν•™μŠ΅ 주제

  1. Domain Driven Design (DDD): μš”κ΅¬μ‚¬ν•­μ„ 도메인 λͺ¨λΈλ‘œ λ³€ν™˜ν•˜λŠ” 방법둠
  2. Event Sourcing: 데이터 λ³€κ²½ 이λ ₯을 이벀트둜 κ΄€λ¦¬ν•˜λŠ” νŒ¨ν„΄
  3. CQRS (Command Query Responsibility Segregation): 읽기와 μ“°κΈ° λͺ¨λΈ 뢄리
  4. Hexagonal Architecture: μ™ΈλΆ€ μ˜μ‘΄μ„±μœΌλ‘œλΆ€ν„° λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 보호

πŸ“– ꢌμž₯ λ„μ„œ

  • β€œλ„λ©”μΈ 주도 섀계” - 에릭 μ—λ°˜μŠ€
  • β€œν΄λ¦° μ•„ν‚€ν…μ²˜β€ - λ‘œλ²„νŠΈ C. λ§ˆν‹΄
  • β€œλ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ νŒ¨ν„΄β€ - 크리슀 λ¦¬μ²˜λ“œμŠ¨
  • β€œμžλ°” ORM ν‘œμ€€ JPA ν”„λ‘œκ·Έλž˜λ°β€ - κΉ€μ˜ν•œ

πŸš€ TEAM WAN / BMC CREW πŸš€