メインコンテンツにスキップ
Haril Song
Owner, Software Engineer at 42dot

Haril is a software engineer who loves to build things. He is passionate about open-source and loves to contribute to the community. He is the owner of this blog.

View all authors

[Shell] 面倒なダミーファイルを簡単に整理する方法

· 10分の読み時間
Haril Song
Owner, Software Engineer at 42dot

概要

複数のデバイスでクラウドストレージを使用していますか?それなら、おそらく衝突ファイルが少しずつ増えていく経験をしたことがあるでしょう。

衝突ファイルが増えている様子を示すアニメーション

隙あらば増えていく衝突ファイル

ファイルが同期される前に編集作業を行ったり、ネットワークの問題で同期が少し遅れたりするなど、さまざまな理由で衝突ファイルは増え続けます。

個人的には常にきれいな状態を好むので、こうしたダミーファイルを定期的に削除しています。

しかし、今日は何だか繰り返しの作業が面倒に感じます。久しぶりにシェルスクリプトを書いて、開発者らしさを出してみようと思います。

開発ツールのバージョン管理、mise

· 11分の読み時間
Haril Song
Owner, Software Engineer at 42dot

概要

  • 一つの開発言語だけでなく、さまざまな開発言語を使っていますか?
  • sdkman、rvm、nvmなどの複数のパッケージマネージャーのコマンドを覚えるのに疲れたことはありませんか?
  • 開発環境をもっと速く、便利に管理したくありませんか?

miseを使えば、どの言語やツールを使っても正確に必要なバージョンを使用でき、他のバージョンに切り替えたり、プロジェクトごとにバージョンを指定することも可能です。ファイルで明示するため、チームメンバー間でどのバージョンを使うか議論するなどのコミュニケーションコストも減らせます。

これまでこの分野で最も有名だったのはasdfでした[^fn-nth-1]。しかし、最近miseを使い始めてからは、miseの方がUXの面で少し優れていると感じています。今回は簡単な使用例を紹介しようと思います。

mise vs asdf

意図的かどうかは分かりませんが、ウェブページさえも似ています。

mise-en-place、mise

mise(「ミーズ」と発音するようです)は開発環境設定ツールです。この名前はフランス料理の用語に由来し、大まかに「設定」または「所定の位置に置く」と訳されます。料理を始める前にすべての道具と材料が所定の位置に準備されている必要があるという意味だそうです。

簡単な特徴を列挙すると以下の通りです。

マルチコネクションサーバへの旅

· 21分の読み時間
Haril Song
Owner, Software Engineer at 42dot

banner

概要

複数のクライアントリクエストを同時に処理できるサーバアプリケーションの実装は、今や非常に簡単です。Spring MVCを使うだけで、すぐに実現できます。しかし、エンジニアとして、その基礎原理に興味があります。本記事では、明らかに見えることを問い直しながら、マルチコネクションサーバを実装するための考慮事項について考察していきます。

情報

例のコードはGitHubで確認できます。

ソケット

最初の目的地は「ソケット」です。ネットワークプログラミングの観点から、ソケットはネットワーク上でデータを交換するための通信エンドポイントです。「ファイルのように使用される」という説明が重要です。これは、ファイルディスクリプタ(fd)を通じてアクセスされ、ファイルと同様のI/O操作をサポートするためです。

なぜソケットはポートではなくfdで識別されるのか?

ソケットはIP、ポート、および相手のIPとポートを使用して識別できますが、fdを使用する方が好まれます。これは、接続が受け入れられるまでソケットには情報がなく、単純な整数(fd)以上のデータが必要だからです。

ソケットを使用してサーバアプリケーションを実装するには、次の手順を踏む必要があります:

SELECT FOR UPDATEの動作

· 9分の読み時間
Haril Song
Owner, Software Engineer at 42dot

banner

PostgreSQLでは、FOR UPDATEロックはトランザクション内でSELECTクエリを実行する際にテーブルの行を明示的にロックするために使用されます。このロックモードは、選択された行がトランザクションが完了するまで変更されないようにし、他のトランザクションがこれらの行を変更したり、競合するロックをかけたりするのを防ぐために使用されます。

例えば、特定の顧客がチケット予約プロセスを進めている間に他の顧客がデータを変更するのを防ぐために使用されることがあります。

この記事で検討するケースは少し特殊です:

  • ロックされた読み取りとロックされていない読み取りが混在する場合、select for updateはどのように動作するのか?
  • 最初にロックが使用された場合、他のトランザクションが読み取ることは可能か?
  • 読み取り方法が混在してもデータの一貫した読み取りが保証されるか?

PostgreSQLでは、select for update句はトランザクション分離レベルによって異なる動作をします。したがって、各分離レベルでの動作を確認する必要があります。

以下のデータが存在する場合にデータが変更されるシナリオを仮定します。

idname
1null

Termsharkを使ったパケットによる3ウェイハンドシェイクの理解

· 9分の読み時間
Haril Song
Owner, Software Engineer at 42dot

banner

ネットワークパケットとは?

データをネットワーク経由で送信するにはどうすれば良いでしょうか?受信者と接続を確立し、一度にすべてのデータを送信するのが最も簡単な方法のように思えます。しかし、この方法は複数のリクエストを処理する際に非効率的です。なぜなら、1つの接続は1回のデータ転送しか維持できないからです。大きなデータ転送のために接続が長引くと、他のデータは待たなければなりません。

データ送信プロセスを効率的に処理するために、ネットワークはデータを複数の部分に分割し、受信側がそれらを再構成する必要があります。これらの分割されたデータ構造をパケットと呼びます。パケットには、受信側がデータを正しい順序で再構成できるようにするための追加情報が含まれています。

複数のパケットでデータを送信することで、パケットスイッチングを通じて多くのリクエストを効率的に処理できますが、データの損失や誤った順序での配信など、さまざまなエラーが発生する可能性もあります。こうした問題をどのようにデバッグすれば良いのでしょうか?🤔

AWS S3と自動化による環境変数の管理

· 8分の読み時間
Haril Song
Owner, Software Engineer at 42dot

状況

  • コードベースが成長するにつれて、Springアプリケーションを実行するために必要な設定値の数が増加しています。
  • ほとんどの状況はテストコードで検証されますが、ローカルでbootRunを使用してテストする必要がある場合もあります。

問題点

  • 設定値を環境変数に分離して管理したい。
  • .envファイルは通常Gitで無視されるため、バージョン管理が難しく、断片化しやすい。
    • 複数のマシン間でファイルを同期する方法が必要です。

質問

  • 開発者間の摩擦を最小限に抑え、簡単に適用できる便利な方法はありますか?
    • メンテナンスが容易な、馴染みのある方法が望ましいです。
  • .envファイルのバージョン管理は可能ですか?
  • 学習曲線は低いですか?
    • 解決策が問題よりも複雑になる状況は避けたいです。
  • 本番環境に直接適用できますか?

回答

AWS S3

  • AWS CLIを使用して.envファイルを更新するのは便利です。
  • .envファイルのバージョン管理はスナップショットを通じて行えます。
  • AWS S3は多くの開発者に馴染みがあり、学習曲線が低いです。
  • AWS ECSの本番環境では、S3 ARNを使用してシステム変数を直接適用できます。

.

..

...

....

それだけですか?

それだけだと、記事が少し退屈に見えるかもしれませんね?もちろん、まだいくつかの問題が残っています。

どのバケットにあるのか?

S3を使用する際、ファイル構造の最適化やビジネス特有の分類のために多くのバケットが作成されることが一般的です。

aws s3 cp s3://something.service.com/enviroment/.env .env

もし.envファイルが見つからない場合、上記のようにAWS CLIを使用してダウンロードする必要があります。事前に誰かがバケットを共有してくれない限り、環境変数ファイルを見つけるためにすべてのバケットを検索する必要があり、不便です。共有を避けるつもりでしたが、再度共有するために何かを受け取るのは少し面倒に感じるかもしれません。

バケットが多すぎる。envはどこにあるのか?

S3内のバケットを探索して必要な.envファイルを見つけてダウンロードするプロセスを自動化すると、非常に便利です。これはfzfやgumのようなツールを使用してスクリプトを書くことで実現できます。

Spring Bootはシステム環境変数を必要とし、.envではない...

一部の方はすでにお気づきかもしれませんが、Spring Bootはシステム環境変数を読み取ってYAMLファイルのプレースホルダーを埋めます。しかし、単に.envファイルを使用するだけではシステム環境変数が適用されず、Spring Bootの初期化プロセス中に拾われません。

簡単にその仕組みを見てみましょう。

# .env
HELLO=WORLD
# application.yml
something:
hello: ${HELLO} # OSのHELLO環境変数から値を取得します。
@Slf4j
@Component
public class HelloWorld {

@Value("${something.hello}")
private String hello;

@PostConstruct
public void init() {
log.info("Hello: {}", hello);
}
}

SystemEnvironmentPropertySource.java

@Valueのプレースホルダーが解決されず、Beanの登録が失敗し、エラーが発生します。

単に.envファイルがあるだけでは、システム環境変数として登録されません。

.envファイルを適用するには、exportコマンドを実行するか、IntelliJの実行構成に.envファイルを登録する必要があります。しかし、exportコマンドを使用してローカルマシンに多くの変数をグローバルに登録すると、上書きなどの意図しない動作が発生する可能性があるため、IntelliJのGUIを通じて個別に管理することをお勧めします。

IntelliJはGUIを介して.envファイルの設定をサポートしています。

プレースホルダーが解決され、正しく適用されました。

最終回答 - 本当の最終回答

ふう、問題の特定と範囲設定の長いプロセスが終わりました。もう一度ワークフローをまとめ、スクリプトを紹介しましょう。

  1. 自動化スクリプトを使用して、S3から適切な.envファイルを見つけてダウンロードします。
  2. .envをシステム環境変数として設定します。

シェルスクリプトはシンプルでありながら、gumを使用してスタイリッシュに書かれています。

フルコード

#!/bin/bash

S3_BUCKET=$(aws s3 ls | awk '{print $3}' | gum filter --reverse --placeholder "Select...") # 1.

# デプロイ環境を選択
TARGET=$(gum choose --header "Select a environment" "Elastic Container Service" "EC2")
if [ "$TARGET" = "Elastic Container Service" ]; then
TARGET="ecs"
else
TARGET="ec2"
fi

S3_BUCKET_PATH=s3://$S3_BUCKET/$TARGET/

# envファイルを検索
ENV_FILE=$(aws s3 ls "$S3_BUCKET_PATH" | grep env | awk '{print $4}' | gum filter --reverse --placeholder "Select...") # 2.

# 確認
if (gum confirm "Are you sure you want to use $ENV_FILE?"); then
echo "You selected $ENV_FILE"
else
die "Aborted."
fi

ENV_FILE_NAME=$(gum input --prompt.foreground "#04B575" --prompt "Enter the name of the env file: " --value ".env" --placeholder ".env")
gum spin -s meter --title "Copying env file..." -- aws s3 cp "$S3_BUCKET_PATH$ENV_FILE" "$ENV_FILE_NAME" # 3.

echo "Done."
  1. gum filterを使用して、目的のS3バケットを選択します。
  2. envという単語を含むアイテムを検索し、ENV_FILEという変数に割り当てます。
  3. .envファイルのオブジェクトキーを最終決定し、ダウンロードを進めます。

実行プロセスのデモビデオを作成しました。

デモ

これが終わったら、先ほど述べたように、現在のディレクトリにコピーされた.envファイルをIntelliJに適用するだけです。

ヒント

direnvとIntelliJのdirenvプラグインを使用すると、さらに便利に適用できます。

結論

  • スクリプトはシンプルであるため、メンテナンスが容易です。
  • チームの反応は非常に良好です。
  • 開発者は美学を評価しています。
  • 機密性の高い資格情報については、AWS Secret Managerの使用を検討してください。

空間インデックスを使用した空間データクエリの最適化

· 7分の読み時間
Haril Song
Owner, Software Engineer at 42dot

banner

この記事では、既存の非効率な実装について議論し、それを改善するために試みた方法を記録します。

既存の問題点

複数のデータベースに分散されたテーブルを単一のクエリで結合することは不可能ではなかったが、困難だった...

  1. 特定の座標がエリア「a」に含まれているか?
  2. テーブルが物理的に異なるサーバーに存在するため、結合クエリの記述が難しかった
    1. なぜ単一のクエリが必要なのか?クエリ対象のデータが大規模であるため、アプリケーションメモリに読み込む量を最小限に抑えたかったからです。
  3. データベース結合が不可能だったため、アプリケーション結合が必要となり、約240億回のループ(60000 * 40000)が発生した
    1. パーティショニングによって処理時間は最小限に抑えられたが、ループによるCPU負荷は依然として高かった。
  4. 物理的に異なるデータベースを1つに統合する移行プロセスを通じて、結合が可能になったため、クエリの最適化の機会が得られた。

アプローチ

データベース結合ができなかった主な理由が解決されたため、ジオメトリ処理にインデックススキャンを活用することを積極的に検討しました。

  • PostGISのGISTインデックスを使用すると、R-treeに似た空間インデックスを作成でき、インデックススキャンを通じて直接クエリが可能です。
  • 空間インデックスを使用するには、ジオメトリ型のカラムが必要です。
  • 緯度と経度の座標は利用可能でしたが、ジオメトリ型がなかったため、まず座標を使用してジオメトリPOINT値を作成する必要がありました。

このプロセスをシミュレートするために、本番DBと同じデータを用意し、実験を行いました。

まず、インデックスを作成しました:

CREATE INDEX idx_port_geom ON port USING GIST (geom);

次に、PostGISのcontains関数を実行しました:

SELECT *
FROM ais AS a
JOIN port AS p ON st_contains(p.geom, a.geom);

素晴らしい...

結果

空間インデックス適用前

1分47秒から2分30秒

空間インデックス適用後

0.23ミリ秒から0.243ミリ秒

キャプチャは用意していませんが、インデックス適用前のクエリは1分30秒以上かかっていました。

結論から始めて、なぜこれらの結果が得られたのかを掘り下げていきましょう。

GiST(Generalized Search Tree)

複雑なジオメトリデータのクエリに非常に有用なインデックスで、その内部構造は以下の通りです。

R-treeのアイデアは、平面を長方形に分割してすべてのインデックスされたポイントを包含することです。インデックス行は長方形を格納し、次のように定義できます:

"探しているポイントは指定された長方形の中にある。"

R-treeのルートには、いくつかの最大の長方形(交差することもある)が含まれます。子ノードには、親ノードに含まれる小さな長方形が含まれ、すべての基本ポイントを包含します。

理論的には、リーフノードにはインデックスされたポイントが含まれるべきですが、すべてのインデックス行は同じデータ型を持つ必要があるため、ポイントに縮小された長方形が繰り返し格納されます。

この構造を視覚化するために、R-treeの3つのレベルの画像を見てみましょう。ポイントは空港の座標を表しています。

レベル1:2つの大きな交差する長方形が見えます。

2つの交差する長方形が表示されています。

レベル2:大きな長方形が小さなエリアに分割されています。

大きな長方形が小さなエリアに分割されています。

レベル3:各長方形には1つのインデックスページに収まるだけのポイントが含まれています。

各長方形には1つのインデックスページに収まるポイントが含まれています。

これらのエリアはツリー構造になっており、クエリ中にスキャンされます。詳細な情報については、次の記事を参照することをお勧めします。

結論

この記事では、具体的な条件、遭遇した問題、それを解決するために行った努力、およびこれらの問題に対処するために必要な基本概念を簡単に紹介しました。要約すると:

  • 物理的に分離されたデータベースでは、インデックスを使用した効率的な結合ができなかった。
  • 移行によって物理的な結合が可能になり、パフォーマンスが大幅に向上した。
  • インデックススキャンを活用することで、全体的なパフォーマンスが大幅に向上した。
  • アプリケーションメモリにデータを不必要に読み込む必要がなくなった。
  • ループによるCPU負荷が軽減された。

参考文献

Fixture Monkeyでテストを簡単かつ便利に

· 10分の読み時間
Haril Song
Owner, Software Engineer at 42dot

"Write once, Test anywhere"

Fixture Monkeyは、Naverがオープンソースとして開発しているテストオブジェクト生成ライブラリです。この名前は、NetflixのオープンソースツールであるChaos Monkeyにインスパイアされたようです。テストフィクスチャをランダムに生成することで、カオスエンジニアリングを実践的に体験できます。

約2年前に初めて出会って以来、私のお気に入りのオープンソースライブラリの一つとなりました。これまでに2つの記事も書きました。

バージョンアップごとに変更が多く、追加の記事は書いていませんでしたが、バージョン1.xがリリースされた今、新たな視点で再訪することにしました。

以前の記事はJavaをベースにしていましたが、今回は現在のトレンドに合わせてKotlinで書いています。この記事の内容は公式ドキュメントに基づいており、実際の使用経験から得た洞察も加えています。

Fixture Monkeyが必要な理由

従来のアプローチでどのような問題があるか、以下のコードを見てみましょう。

情報

例ではJava開発者に馴染みのあるJUnit5を使用しましたが、個人的にはKotlin環境ではKotestをお勧めします。

data class Product (
val id: Long,

val productName: String,

val price: Long,

val options: List<String>,

val createdAt: Instant,

val productType: ProductType,

val merchantInfo: Map<Int, String>
)

enum class ProductType {
ELECTRONICS,
CLOTHING,
FOOD
}
@Test
fun basic() {
val actual: Product = Product(
id = 1L,
price = 1000L,
productName = "productName",
productType = ProductType.FOOD,
options = listOf(
"option1",
"option2"
),
createdAt = Instant.now(),
merchantInfo = mapOf(
1 to "merchant1",
2 to "merchant2"
)
)

// テスト目的に比べて準備プロセスが長い
actual shouldNotBe null
}

テストオブジェクト生成の課題

テストコードを見ると、アサーションのためにオブジェクトを生成するだけで多くのコードを書かなければならないと感じます。実装の性質上、プロパティが設定されていないとコンパイルエラーが発生するため、意味のないプロパティでも書かなければなりません。

テストコードでアサーションのための準備が長くなると、コード内のテスト目的の意味が不明瞭になることがあります。このコードを初めて読む人は、意味のないプロパティにも隠れた意味があるかどうかを確認する必要があり、このプロセスは開発者の疲労を増加させます。

エッジケースの認識の難しさ

プロパティを直接設定してオブジェクトを生成する場合、さまざまなシナリオで発生する可能性のある多くのエッジケースが見落とされがちです。

val actual: Product = Product(
id = 1L, // idが負の値になったらどうなる?
// ...省略
)

エッジケースを見つけるためには、開発者はプロパティを一つ一つ設定して確認する必要がありますが、実際にはランタイムエラーが発生して初めてエッジケースに気づくことが多いです。エラーが発生する前にエッジケースを簡単に発見するためには、オブジェクトのプロパティをある程度ランダムに設定する必要があります。

オブジェクトマザーパターンの問題

テストオブジェクトを再利用するために、オブジェクトマザーパターンと呼ばれるパターンでは、オブジェクトを生成するファクトリークラスを作成し、そのクラスから生成されたオブジェクトを使用してテストコードを実行します。

しかし、この方法はテストコードだけでなくファクトリーの管理も継続的に必要であり、エッジケースの発見には役立ちません。

Fixture Monkeyの使用

Fixture Monkeyは、上記の再利用性とランダム性の問題をエレガントに解決します。どのようにこれらの問題を解決するか見てみましょう。

まず、依存関係を追加します。

testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter-kotlin:1.0.13")

Kotlin環境でFixture Monkeyがスムーズに動作するようにするためにKotlinPlugin()を適用します。

@Test
fun test() {
val fixtureMonkey = FixtureMonkey.builder()
.plugin(KotlinPlugin())
.build()
}

先ほど使用したProductクラスを使って再度テストを書いてみましょう。

data class Product (
val id: Long,

val productName: String,

val price: Long,

val options: List<String>,

val createdAt: Instant,

val productType: ProductType,

val merchantInfo: Map<Int, String>
)

enum class ProductType {
ELECTRONICS,
CLOTHING,
FOOD
}
@Test
fun test() {
val fixtureMonkey = FixtureMonkey.builder()
.plugin(KotlinPlugin())
.build()

val actual: Product = fixtureMonkey.giveMeOne()

actual shouldNotBe null
}

不要なプロパティ設定なしにProductのインスタンスを作成できます。すべてのプロパティ値はデフォルトでランダムに埋められます。

image 複数のプロパティがうまく埋められる

ポストコンディション

しかし、ほとんどの場合、特定のプロパティ値が必要です。例えば、例ではidが負の数として生成されましたが、実際にはidは正の数として使用されることが多いです。次のようなバリデーションロジックがあるかもしれません:

init {
require(id > 0) { "idは正の数である必要があります" }
}

テストを数回実行した後、idが負の数として生成されるとテストが失敗します。すべての値がランダムに生成されるため、予期しないエッジケースを見つけるのに特に役立ちます。

image

ランダム性を維持しつつ、バリデーションロジックが通るように範囲を少し制限しましょう。

@RepeatedTest(10)
fun postCondition() {
val fixtureMonkey = FixtureMonkey.builder()
.plugin(KotlinPlugin())
.build()

val actual = fixtureMonkey.giveMeBuilder<Product>()
.setPostCondition { it.id > 0 } // 生成されたオブジェクトのプロパティ条件を指定
.sample()

actual.id shouldBeGreaterThan 0
}

テストを10回実行するために@RepeatedTestを使用しました。

image

すべてのテストが通ることがわかります。

様々なプロパティの設定

postConditionを使用する際は、条件を狭めすぎるとオブジェクト生成がコスト高になることに注意してください。これは、条件を満たすオブジェクトが生成されるまで内部で生成が繰り返されるためです。このような場合、特定の値を固定するためにsetExpを使用する方がはるかに良いです。

val actual = fixtureMonkey.giveMeBuilder<Product>()
.setExp(Product::id, 1L) // 指定された値のみ固定され、他はランダム
.sample()

actual.id shouldBe 1L

プロパティがコレクションの場合、sizeExpを使用してコレクションのサイズを指定できます。

val actual = fixtureMonkey.giveMeBuilder<Product>()
.sizeExp(Product::options, 3)
.sample()

actual.options.size shouldBe 3

maxSizeminSizeを使用すると、コレクションの最大サイズと最小サイズの制約を簡単に設定できます。

val actual = fixtureMonkey.giveMeBuilder<Product>()
.maxSizeExp(Product::options, 10)
.sample()

actual.options.size shouldBeLessThan 11

他にも様々なプロパティ設定方法があるので、必要に応じて探索してみてください。

結論

Fixture Monkeyは、ユニットテストを書く際の不便さを本当に解消してくれます。この記事では触れませんでしたが、ビルダーに条件を作成して再利用したり、プロパティにランダム性を追加したり、開発者が見逃しがちなエッジケースを発見するのに役立ちます。その結果、テストコードが非常に簡潔になり、Object Motherのような追加コードが不要になり、メンテナンスが容易になります。

Fixture Monkey 1.xのリリース前でも、テストコードを書くのに非常に役立ちました。今や安定版となったので、ぜひ導入してテストコードを書く楽しさを味わってください。

参考