Java面接対策:SpringBootとRedisを活用した8つのデザインパターンの実装例

Java面接対策:SpringBootとRedisを活用した8つのデザインパターンの実装例

最近のJavaエンジニア面接において、候補者の多くが「デザインパターンを理解している」と主張しながらも、実際のプロジェクトでの応用例を説明できないケースが目立ちます。この記事では、エコマーケットの秒殺システムを題材に、SpringBootとRedisを組み合わせたプロジェクトで実際に採用されている8つのデザインパターンを解説します。それぞれのパターンの適用シーン、実装方法、面接での回答構成法を具体的に示します。

1. プロジェクト概要とアーキテクチャ設計

秒殺システムの基本的な処理フローは以下の通りです:

  1. ユーザーが秒殺ボタンをクリック
  2. システムが在庫を確認
  3. 在庫を減算
  4. 注文を生成

このシンプルなフローでも、高負荷下では在庫オーバー、DBの過負荷、レスポンス遅延などの問題が発生します。これらの課題に対応するためには、キャッシュ、キュー、レートリミットなどの技術を導入する必要がありますが、これらをどうやってコードに統合するかが重要です。

本プロジェクトでは以下のレイヤー構成を採用しています:

  • Controller層:HTTPリクエストの受信とパラメータ検証
  • Service層:ビジネスロジックの実装(デザインパターンの主な適用領域)
  • DAO層:データベース操作(RedisとMySQLを使用)
  • Utils/Component層:共通ユーティリティやカスタムコンポーネント

使用技術はSpringBoot 2.x、Redis(Lettuceクライアント)、MySQL、Lombokを含むシンプルなスタックです。

2. シングルトンパターン:グローバルな在庫チェックコンポーネント

シングルトンパターンは面接で最も頻出するテーマです。秒殺システムでは、在庫の確認・減算処理を一元管理する必要があります。SpringのBeanはデフォルトでシングルトンですが、手動で実装する場合の注意点を確認しましょう。

/**
 * 在庫チェックコンポーネント - シングルトン実装例
 */
public class InventoryChecker {
    private static volatile InventoryChecker instance;
    private final RedisTemplate<String, String> redisTemplate;

    private InventoryChecker(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public static InventoryChecker getInstance(RedisTemplate<String, String> redisTemplate) {
        if (instance == null) {
            synchronized (InventoryChecker.class) {
                if (instance == null) {
                    instance = new InventoryChecker(redisTemplate);
                }
            }
        }
        return instance;
    }

    public boolean checkAndReserve(String productId, int quantity) {
        String script = "local stock = tonumber(redis.call('get', KEYS[1]))\n" +
                        "if stock and stock >= tonumber(ARGV[1]) then\n" +
                        "    redis.call('decrby', KEYS[1], ARGV[1])\n" +
                        "    return 1\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Arrays.asList("stock:" + productId),
            String.valueOf(quantity)
        );
        return result != null && result == 1L;
    }
}

3. ファクトリーメソッドパターン:柔軟な注文処理コンポーネントの生成

商品タイプに応じて注文処理を分ける必要がある場合、ファクトリーメソッドパターンが有効です。以下にSpringと連携した実装例を示します。

public interface OrderHandler {
    PurchaseOrder createOrder(SecKillRequest request);
}

@Component
public class PhysicalOrderHandler implements OrderHandler {
    @Override
    public PurchaseOrder createOrder(SecKillRequest request) {
        PurchaseOrder order = new PurchaseOrder();
        order.setType("PHYSICAL");
        // 物理商品特有の処理
        return order;
    }
}

@Component
public class VirtualOrderHandler implements OrderHandler {
    @Override
    public PurchaseOrder createOrder(SecKillRequest request) {
        PurchaseOrder order = new PurchaseOrder();
        order.setType("VIRTUAL");
        // 虚擬商品特有の処理
        return order;
    }
}

ファクトリーコンポーネントの実装例:

@Component
public class OrderHandlerFactory {
    private final Map<String, OrderHandler> handlerMap = new ConcurrentHashMap<>();

    public OrderHandlerFactory(List<OrderHandler> handlers) {
        for (OrderHandler handler : handlers) {
            String beanName = handler.getClass().getSimpleName();
            String key = beanName.replace("Handler", "").toUpperCase();
            handlerMap.put(key, handler);
        }
    }

    public OrderHandler getHandler(String orderType) {
        OrderHandler handler = handlerMap.get(orderType);
        if (handler == null) {
            throw new IllegalArgumentException("Unsupported order type: " + orderType);
        }
        return handler;
    }
}

4. ビルダーパターン:複雑な注文オブジェクトの構築

多くの属性を持つ注文オブジェクトを構築する際、ビルダーパターンが有効です。

@Data
public class PurchaseOrder {
    private final String orderId;
    private final Long userId;
    private final Long productId;
    private String address;
    private BigDecimal discount;
    private String couponCode;
    private String remark;

    private PurchaseOrder(PurchaseOrderBuilder builder) {
        this.orderId = builder.orderId;
        this.userId = builder.userId;
        this.productId = builder.productId;
        this.address = builder.address;
        this.discount = builder.discount;
    }

    public static class PurchaseOrderBuilder {
        private String orderId;
        private Long userId;
        private Long productId;
        private String address;
        private BigDecimal discount;

        public PurchaseOrderBuilder setOrderId(String orderId) {
            this.orderId = orderId;
            return this;
        }

        public PurchaseOrderBuilder setUserId(Long userId) {
            this.userId = userId;
            return this;
        }

        public PurchaseOrderBuilder setProductId(Long productId) {
            this.productId = productId;
            return this;
        }

        public PurchaseOrderBuilder setAddress(String address) {
            this.address = address;
            return this;
        }

        public PurchaseOrderBuilder setDiscount(BigDecimal discount) {
            this.discount = discount;
            return this;
        }

        public PurchaseOrder build() {
            return new PurchaseOrder(this);
        }
    }
}

タグ: SpringBoot redis DesignPatterns Java concurrency

6月27日 16:46 投稿