Featured image of post コードシェフの仕込み帳【Flyweight】厨房を埋め尽くす伝票の山〜壁の品書きと一鍋のスープ〜

コードシェフの仕込み帳【Flyweight】厨房を埋め尽くす伝票の山〜壁の品書きと一鍋のスープ〜

大量の注文データによるメモリ肥大化(OOM)を解決するため、不変データ(メニュー情報)を共有し、可変情報を外部から与えるFlyweightパターンをPerl+Mooで仕込み直します。

蒸し器から吹き出す真っ白な湯気が、厨房の天井を覆い尽くしている。 ゴウゴウと牙をむくコンロの火力が、中華鍋の底を真っ赤に染め上げていた。

「五番テーブル、小籠包三枚! 焼き餃子二枚!」 「アイヤー! すぐ行く!」

ここは中華街の老舗「龍星飯店」の厨房。週末のランチラッシュと地域のフードフェスティバルが重なり、戦場のような忙しさに包まれている。私とシェフは、今日だけ助っ人(ヘルプ)としてこの厨房に入っていた。

私は次々と仕上がってくる小皿を片付けながら、厨房の熱気にただ圧倒されていた。だが、そんな中でも私の目は、隣で中華鍋を振るシェフの無駄のない動作に釘付けになっていた。

シェフは巨大なスープ鍋の前に立つと、お玉でスープをすくい、個別の丼や小鍋に迷いなく注ぎ分けていく。 醤油ラーメンのスープも、フカヒレの餡かけも、すべてこの大鍋でじっくりと仕込まれた「共通のベーススープ」から出発している。ベースを一度に大量に仕込んで共有し、手元の小鍋(コンテキスト)で個別の調味や具材を加える。だからこそ、秒刻みで注文が舞い込むこの戦場でも、一杯数秒という驚異的なスピードで、ブレのないプロの味が次々と仕上がっていくのだ。

「これが、プロの段取り……」

息を呑む私を現実に引き戻したのは、バタバタと厨房に駆け込んできた一人の青年の悲鳴だった。


本日の持ち込み素材:厨房を埋め尽くす伝票の山

駆け込んできたのは、龍星飯店のシステム開発担当エンジニアであるヤンさん(26)だった。額に汗をびっしょりとかき、手元にはノートPCを抱えている。

「ヤバいです、またサーバーが落ちました! 注文管理プロセスが、メモリオーバーフロー(Out of Memory)でクラッシュしたんです!」

ヤンさんは生真面目だが、非常にせっかちなエンジニアだ。週末の激しい注文ラッシュに耐えられるよう、急ごしらえでオンライン注文システムを構築したらしい。だが、同時注文が数万件に達した途端、サーバーのメモリが限界を迎えて息絶えてしまうのだという。

ヤンさんが画面に表示した Before コード(注文オブジェクト)は、次のような構造だった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# agents/code-chef/tests/flyweight-pattern/before.t より抜粋
package Order;
use Moo;
use v5.36;

has order_id    => (is => 'ro', required => 1);
has table_no    => (is => 'ro', required => 1);
has menu_name   => (is => 'ro', required => 1);
has price       => (is => 'ro', required => 1);
has allergens   => (is => 'ro', required => 1); # 配列リファレンス
has description => (is => 'ro', required => 1); # 重い説明文字列

ヤンさんは注文が生成されるたびに、この Order オブジェクトを new してメモリに載せていた。

1
2
3
4
5
6
7
8
my $order = Order->new(
    order_id    => 10023,
    table_no    => 5,
    menu_name   => '四川麻婆豆腐',
    price       => 980,
    allergens   => ['大豆', '小麦', '豚肉'],
    description => '四川産の豆板醤と花椒を使用した、痺れる辛さとコクが特徴の本格麻婆豆腐。' x 10, # 重いデータ
);

「注文ごとに、メニュー名も価格もアレルギー情報も、それから長文のメニュー説明文まで全部オブジェクトの中にコピーして持たせているんです。こうすれば、この注文伝票一枚(Orderオブジェクト)を見るだけで、すべての情報が自己完結しますから!」

ヤンさんは誇らしげに、しかし焦りながら説明する。 だが、その瞬間、私の頭の中に、先ほどから見ていた厨房の光景とヤンさんのコードが奇妙に重なり合った。

「……あの、ヤンさん」 私は思わず、手元にたまっていた物理的な「伝票の山」を指差した。ヤンさんが印刷して配っている厨房用の伝票だ。

「これ、すごく無駄じゃないですか? 伝票1枚1枚に、詳細なアレルギー表と、料理のこだわり説明文が全部ホチキスで留められていて、カウンターの上が伝票の束でパンパンに埋まっています。でも、アレルギー情報や詳しい説明は、壁に貼ってある大きなメニュー板(マスタ)を見れば済むはずです。厨房の人が見たいのは、『5番テーブルに麻婆豆腐を1つ』という情報だけですよね? なんで伝票ごとに同じ説明書きを全部くっつけて回しているんですか?」

ヤンさんは目を丸くした。

「そ、それは注文情報を自己完結させておかないと、アレルギー対策のチェックで厨房が間違えるかもしれないじゃないですか! それに、もし将来メニューマスタが書き換わって『麻婆豆腐』が値上げされたり、アレルギー情報が改定されたりしたとき、過去の注文オブジェクトが持っている価格まで一緒に変わってしまったら、売上の集計や会計データが壊れてしまいます! だから毎回コピーして完全に分離させておくのが、安全で当然の設計だと思っていました……」

ヤンさんの主張は、エンジニアとしての確かなプロ意識に基づいていた。整合性を守り、事故を防ぐ。そのために彼は、愚直にすべてのデータを「伝票(オブジェクト)」にコピーして抱え込ませていたのだ。

だが、その几帳面さが、サーバーを窒息させていた。


素材を見る目:メモリ肥大化(Memory Bloat)の匂い

シェフはガコンと中華鍋をコンロに置くと、タオルで顔の汗を拭い、ヤンさんのコードを覗き込んだ。

「なるほどな。安全のためにホチキスを留めまくった結果、伝票の重みで机が抜け落ちたってわけだ」

シェフはヤンさんの肩をぽんと叩いた。

「お前がやらかしているのは『メモリ肥大化(Memory Bloat)』という仕込みミスだ。同じ内容が書かれた分厚いアレルギー表(不変データ)を、数万個の伝票オブジェクトのすべてに丸ごと二重・三重にコピーしてメモリに載せている。これじゃあ、どんなに広大な厨房(メモリ)があっても、ラッシュ時にはすぐに埋まっちまう」

ヤンさんは不安そうにシェフを見つめた。

「でも、共有オブジェクトにして参照を持たせるだけにすると、さっき言った『マスタ変更時のデータ書き換わり問題』や『アレルギー情報の勝手な変更』が防げないのでは……?」

「だから、仕分けが必要なんだよ」 シェフは包丁を静かにまな板に置いた。 「オブジェクトの自己完結性を気にするあまり、すべてを同一の皿に盛り付けるな。『絶対に変わらないもの(内部状態)』と『その都度変わるもの(外部状態)』の境界を、きれいに切り分けるんだ」


包丁を入れ直す:Flyweightによる「スープとお玉」の仕込み

シェフは調理台の上に、一鍋のスープベースと、いくつかの空の取り皿を用意した。

「いいか、ヤン。大鍋の中のベーススープは、全テーブルの客に共有される。これが『内部状態(不変データ)』だ。このスープ自体は、読み取り専用で、絶対に後から勝手に味を変えちゃいけない。もしある客が『辛くしてくれ』と言ったからといって、大鍋のスープ全体に唐辛子をぶち込んだら、他の客のスープまで辛くなって大惨事になるだろ?」

ヤンさんはコクコクと頷く。

「だから、味を変える(個別のカスタマイズを適用する)のは、各自の取り皿(Order)の上だけだ。取り皿には、お玉ですくったベーススープへの『参照』と、その客固有の『辛さの度合い(外部状態)』だけを載せる。こうすれば、ベーススープはたった一鍋(単一オブジェクト)を共有するだけで済むし、他の客の味を壊すこともない」

そして、シェフは私の方を向いた。

「おい、見習い。前回のスープカレー屋(宮本さん)の時は、ベースオブジェクトを丸ごと『複製(Prototypeのクローン)』して各自の皿を作った。覚えているか?」

「はい! あの時は dclone を使って、中身を丸ごとコピーして個別に調整しました」

「そうだ。だが今回は、複製すら不要だ。なぜなら、メニューの名前やアレルギー情報、説明文は、注文ごとに書き換える必要が一切ない『不変のデータ』だからだ。複製するのではなく、ただ全員で同じものを『指し示す(共有)』だけでいい。この技法を『Flyweight(フライウェイト)』と呼ぶ」


Flyweightパターンの設計

Flyweight パターン(Flyweight=フライ級、軽量級)の核心は、**「共有を利用して、大量の細粒度オブジェクトを効率的にサポートする」**ことです。

オブジェクトの持つ状態を以下の2つに完全に分離します。

  1. 内部状態(Intrinsic State): オブジェクトそのものが持つ、文脈(コンテキスト)に依存しない不変の情報。今回の例では「メニュー名」「アレルギー情報」「メニュー説明文」がこれに当たります。複数の注文オブジェクト間で安全に「共有」できます。
  2. 外部状態(Extrinsic State): オブジェクトを使用する状況や場面によって変化する情報。今回の例では「注文ID」「テーブル番号」「注文個数」、そして決済時の「確定価格」がこれに該当します。共有はできず、利用する側からその都度与えられます。

Before(改善前)と After(改善後)のオブジェクト構成を可視化すると、以下のようになります。

Flyweightパターンのメモリ共有比較図。BeforeではOrderごとに同一内容のMenuインスタンスが重複保持されていますが、AfterではひとつのSharedMenuインスタンスを全員で共有(指し示す)することでメモリ使用量を節約する構成を示しています。

これらを Perl と Moo で実装してみましょう。


補足:Afterコード(解決パターン)の実装

まず、共有される内部状態を保持する Menu クラスを定義します。このクラスの属性はすべて is => 'ro'(読み取り専用)として定義し、不変性を強制します。

1
2
3
4
5
6
7
8
# 1. Flyweight (共有される不変オブジェクト)
package Menu;
use Moo;
use v5.36;

has menu_name   => (is => 'ro', required => 1);
has allergens   => (is => 'ro', required => 1); # 不変の配列リファレンス
has description => (is => 'ro', required => 1); # 重い説明テキスト

次に、同じ Menu オブジェクトを二重に生成せず、プール(キャッシュ)から再利用するための MenuFactory を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2. Flyweight Factory (オブジェクトの共有・キャッシュ管理)
package MenuFactory;
use Moo;
use v5.36;

has _cache => (
    is      => 'ro',
    default => sub { {} },
);

sub get_menu ($self, $name, $allergens, $description) {
    # メニュー名を決定的なキャッシュキーにする
    my $key = $name;
    
    unless (exists $self->_cache->{$key}) {
        $self->_cache->{$key} = Menu->new(
            menu_name   => $name,
            allergens   => $allergens,
            description => $description,
        );
    }
    return $self->_cache->{$key};
}

そして、外部状態と Menu への参照を持つ Order クラスを定義します。

ヤンさんが最も懸念していた「過去の会計整合性」を守るため、注文時点の確定価格(決済価格)は、マスタ改定の影響を受けない「注文固有の外部状態(actual_price)」として Order 自身に直接持たせる設計にします。これこそが、安全性と効率性を両立させるポイントです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 3. Context / Client (外部状態と共有オブジェクトの参照を持つ)
package Order;
use Moo;
use v5.36;

has order_id     => (is => 'ro', required => 1);
has table_no     => (is => 'ro', required => 1);
has quantity     => (is => 'ro', required => 1);
has menu         => (is => 'ro', required => 1); # 共有Menuオブジェクトへの参照
has actual_price => (is => 'ro', required => 1); # 注文時点の確定価格(外部状態)

sub total_amount ($self) {
    return $self->actual_price * $self->quantity;
}

完成:涼しい顔でさばかれるオーダー

「……なるほど!」 ヤンさんは PC の画面を叩きながら、目からウロコが落ちたような表情を見せた。

「これなら、メニューの説明文やアレルギー情報の配列は、メモリ上にメニューの種類分(せいぜい数十個)しか生成されませんね! 注文オブジェクト(Order)が何万件増えても、増えるのは『Menuオブジェクトへの細い矢印(リファレンス)』と、テーブル番号などの数字データだけだ!」

「その通り!」と私は自分の言葉で言い直した。「伝票1枚ごとにアレルギー表の分厚いコピーをホチキス留めする代わりに、『壁のメニュー板のここを見てね』という極細の矢印(鉛筆線)を書き込むだけ。矢印をいくら増やしたって、カウンターの上が紙の束で埋まることはありません!」

ヤンさんはさっそく、リファレンス実装を適用して、1万件のダミー注文を生成する検証テストを走らせた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# agents/code-chef/tests/flyweight-pattern/after.t の検証コード
# ※この検証コードは、Perlの標準テストモジュールを使用した抜粋です
use Test::More;

my $factory = MenuFactory->new;

# 同じメニュー名であれば、同一インスタンスが Factory から返される
my $menu1 = $factory->get_menu('四川麻婆豆腐', ['大豆'], '説明文...' x 10);
my $menu2 = $factory->get_menu('四川麻婆豆腐', ['大豆'], '説明文...' x 10);

my $order1 = Order->new(
    order_id     => 1001,
    table_no     => 5,
    quantity     => 2,
    menu         => $menu1,
    actual_price => 980, # 注文時点の確定価格を固定
);

my $order2 = Order->new(
    order_id     => 1002,
    table_no     => 3,
    quantity     => 1,
    menu         => $menu2,
    actual_price => 980,
);

# 共有の検証
is($order1->menu, $order2->menu, 'After: 同一のメニューオブジェクトを共有している');

テストを実行すると、コンソールには鮮やかな ok の文字が並び、警告一つなくクリアした。 大量の注文データを生成しても、メモリの使用量は以前の10分の一以下に激減し、ガベージコレクションの負荷が下がったことで応答速度も格段に向上した。

さらに、ヤンさんが心配していた整合性も、Order に直接 actual_price を持たせることで解決した。後からマスタデータとしての Menu の想定価格が改定されようとも、すでに確定した Order が持つ actual_price は影響を受けず、会計監査の整合性は完璧に守られる。

「動いた……! これなら、フードフェスティバルのピークが来ても、サーバーは涼しい顔で動き続けます!」 ヤンさんは勢いよく立ち上がり、深く頭を下げた。「シェフ、見習いさん、本当にありがとうございました! 最高の仕込みです!」

シェフは再び中華鍋を手に取り、コンロの火を最大にした。

「美味い中華は、スピードと無駄のなさが命だ。伝票が軽くなったんなら、さっさとシステムをデプロイして、次のオーダーを捌いてこい!」

「はい!」 ヤンさんは軽やかになったノートPCを抱え、風のように走り去っていった。 厨房には再び、中華鍋の小気味よい金属音と、小籠包の美味しそうな湯気の香りが満ちていった。私の手元も、すっきりと軽くなった伝票のおかげで、いつもよりずっとスムーズに動き始めていた。


シェフの仕込み工程表

問題(調理ミス)技法(パターン)効果(仕上がり)
メモリ肥大化(Memory Bloat)
大量の注文オブジェクトが、同一のメニュー詳細(不変情報)をそれぞれコピーして保持し、メモリを枯渇させていた。
Flyweight パターン
不変の内部状態(Menu)をオブジェクトとして切り出して共有し、可変の外部状態(テーブル番号、確定価格など)は Order 側で個別管理する。
メモリ消費の激減と整合性の両立
同一メニューの重複インスタンスが排除され、メモリ使用量が1/10以下に。注文固有の価格を保持することで過去データの整合性も担保。

工程

  1. 状態の分類と分離: 対象クラス(Order)が持っている属性のうち、すべての注文で使い回せる「不変のデータ(内部状態:メニュー名、説明文など)」と、コンテキストごとに異なる「可変のデータ(外部状態:テーブル番号、決済価格など)」を洗い出す。
  2. Flyweight クラスの作成: 洗い出した内部状態のみを持つ Menu クラスを新規作成し、属性をすべて読み取り専用(is => 'ro')にする。
  3. Factory によるキャッシュ管理: MenuFactory を作成し、ハッシュキャッシュを用いて一度生成した Menu オブジェクトをキー(メニュー名など)に基づいて再利用する仕組み(オブジェクトプール)を実装する。
  4. コンテキスト側のリファクタリング: Order クラスから内部状態の属性を削除し、代わりに Factory から取得した Menu オブジェクトへの参照(menu)を持たせる。注文固有の外部状態(actual_price など)はそのまま残す。

シェフより

几帳面なやつほど、何でもかんでも手元の皿にコピーして溜め込みたがる。だがな、厨房の広さにも、サーバーのメモリにも上限があるんだ。

「何が不変で、何が可変か」を見極めろ。共通のレシピは壁の品書きに預け、一鍋のスープベースをみんなで共有すればいい。手元に置くのは、必要最小限の調味料と、それを指し示すための軽い鉛筆書きだけだ。伝票を軽く仕込めば、厨房はもっと速く、遠くまで走れるようになるぜ。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。