Java面接対策:SpringBootとRedisを活用した8つのデザインパターンの実装例
最近のJavaエンジニア面接において、候補者の多くが「デザインパターンを理解している」と主張しながらも、実際のプロジェクトでの応用例を説明できないケースが目立ちます。この記事では、エコマーケットの秒殺システムを題材に、SpringBootとRedisを組み合わせたプロジェクトで実際に採用されている8つのデザインパターンを解説します。それぞれのパターンの適用シーン、実装方法、面接での回答構成法を具体的に示します。
1. プロジェクト概要とアーキテクチャ設計
秒殺システムの基本的な処理フローは以下の通りです:
- ユーザーが秒殺ボタンをクリック
- システムが在庫を確認
- 在庫を減算
- 注文を生成
このシンプルなフローでも、高負荷下では在庫オーバー、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);
}
}
}