Rustはその高いパフォーマンス、安全性、並列性への注力により、近年人気を集めるプログラミング言語です。その中でAxumは、Rust向けに設計された軽量で強力なWebフレームワークであり、RESTful API構築に最適です。本記事では、RustとAxumを活用し、シンプルで拡張性の高いRESTful APIを構築する具体的な手順を解説します。初心者から中級者まで、Axumの基本的な機能から高度なテクニックまでを順を追って学べる内容となっています。
RustとAxumの概要
Rustの特徴
Rustは、安全性とパフォーマンスを両立するために設計されたプログラミング言語です。所有権システムを採用しており、メモリ管理の安全性をコンパイル時に保証します。また、並列処理が容易で、高いパフォーマンスが求められるシステムレベルのプログラムやWebサービスに適しています。
Axumの特徴
AxumはRustで構築されたWebフレームワークで、以下の特徴があります:
- タイプセーフティ:型システムを活用して、安全で信頼性の高いコードを記述できます。
- 拡張性:ハンドラーの構成や中間処理の設定が容易で、大規模なAPI開発にも対応可能です。
- シンプルな設計:少ないコードでRESTful APIを実装でき、開発効率が向上します。
Axumの適用領域
Axumは軽量であるため、小規模なマイクロサービスから大規模なWeb APIまで幅広い用途に使用できます。また、非同期処理をサポートしており、リアルタイム通信や高負荷なサービスにも適しています。
RESTful APIの基本概念
RESTful APIとは
RESTful APIは、Webアプリケーションで一般的に使用されるAPI設計のアーキテクチャスタイルで、以下の原則に基づいて構築されます:
- リソース指向:APIはリソース(エンティティ)を識別するURLを中心に設計されます。
- HTTPメソッドの活用:HTTPメソッド(GET、POST、PUT、DELETEなど)を活用して、リソースへの操作を表現します。
- ステートレス性:リクエスト間でサーバーがクライアントの状態を保持しないため、各リクエストは必要な情報を含むべきです。
RESTの原則
- 一貫性:API設計は統一された命名規則やデータフォーマットを採用します。
- クライアント・サーバー分離:クライアントとサーバーの役割を明確に分離することで、システムの拡張性を向上させます。
- キャッシュ可能性:適切なHTTPヘッダーを活用し、レスポンスのキャッシュを可能にしてパフォーマンスを向上させます。
RESTful APIの利用例
例えば、書籍管理システムを考えます。
- GET /books: 書籍リストを取得する。
- POST /books: 新しい書籍を追加する。
- PUT /books/{id}: 指定したIDの書籍を更新する。
- DELETE /books/{id}: 指定したIDの書籍を削除する。
RESTful APIは、シンプルで直感的な設計が可能であり、クライアントとサーバー間の通信を効率的に管理できます。
Axumのインストールとセットアップ
プロジェクトの作成
まず、Rustの開発環境が整っていることを確認し、新しいプロジェクトを作成します。
cargo new axum_rest_api
cd axum_rest_api
必要な依存関係を追加
Axumを使用するために、Cargo.toml
ファイルに以下の依存関係を追加します。
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
hyper = "0.14"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Axumの基本セットアップ
Axumをセットアップし、サーバーを起動するコードを記述します。以下は、最小限のAxumサーバーの例です。
src/main.rs
use axum::{routing::get, Router};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// ルーターを作成
let app = Router::new().route("/", get(handler));
// サーバーを起動
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server is running on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// ハンドラー関数
async fn handler() -> &'static str {
"Hello, Axum!"
}
アプリケーションの実行
以下のコマンドを実行して、Axumサーバーを起動します。
cargo run
ブラウザまたはAPIクライアントで http://127.0.0.1:3000/
にアクセスすると、Hello, Axum!
が表示されます。
このセットアップにより、Axumを用いたRESTful API開発の基盤が整います。次のステップでは、エンドポイントの詳細な構築方法を学びます。
シンプルなエンドポイントの作成
Axumでの基本的なエンドポイント構築
Axumを使用すると、簡単にRESTful APIのエンドポイントを作成できます。ここでは、基本的なGETリクエストとPOSTリクエストのエンドポイントを構築します。
エンドポイントのコード例
以下のコードは、シンプルなGETおよびPOSTエンドポイントを持つAxumアプリケーションの例です。
src/main.rs
use axum::{
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
// レスポンスデータの構造体
#[derive(Serialize)]
struct Greeting {
message: String,
}
// リクエストデータの構造体
#[derive(Deserialize)]
struct InputData {
name: String,
}
#[tokio::main]
async fn main() {
// ルーターを作成
let app = Router::new()
.route("/", get(get_handler))
.route("/greet", post(post_handler));
// サーバーを起動
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server is running on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// GETリクエストのハンドラー
async fn get_handler() -> Json<Greeting> {
Json(Greeting {
message: "Hello, Axum!".to_string(),
})
}
// POSTリクエストのハンドラー
async fn post_handler(Json(payload): Json<InputData>) -> Json<Greeting> {
Json(Greeting {
message: format!("Hello, {}!", payload.name),
})
}
エンドポイントの動作確認
- GETリクエストの確認
ブラウザまたはAPIクライアントでhttp://127.0.0.1:3000/
にアクセスします。以下のレスポンスが得られます。
{
"message": "Hello, Axum!"
}
- POSTリクエストの確認
APIクライアント(PostmanやcURLなど)を使用してhttp://127.0.0.1:3000/greet
にJSONデータをPOSTします。
リクエスト例:
{
"name": "Rustacean"
}
レスポンス例:
{
"message": "Hello, Rustacean!"
}
解説
get
とpost
ルートの設定:Router
を使って、GETとPOSTのリクエストに対応するルートを設定します。- ハンドラー関数: 非同期関数でリクエストを処理し、レスポンスを返します。
Json
型を使用してデータを送受信します。 - データのシリアル化/デシリアル化:
serde
を活用して、構造体をJSON形式に変換します。
この方法で基本的なエンドポイントを構築し、API機能を拡張していくことができます。
リクエストとレスポンスのハンドリング
Axumでのリクエスト処理
Axumでは、リクエストデータを構造体として受け取り、解析することができます。これにより、型安全かつ直感的にデータを扱うことが可能です。
リクエストの例
以下のコードは、クライアントからJSON形式でデータを受け取り、処理する例です。
src/main.rs
use axum::{
routing::post,
Json, Router,
};
use serde::Deserialize;
use std::net::SocketAddr;
// リクエストデータの構造体
#[derive(Deserialize)]
struct InputData {
username: String,
age: u32,
}
#[tokio::main]
async fn main() {
// ルーターを作成
let app = Router::new().route("/submit", post(handle_request));
// サーバーを起動
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server is running on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// POSTリクエストのハンドラー
async fn handle_request(Json(payload): Json<InputData>) -> String {
format!(
"Received data: username = {}, age = {}",
payload.username, payload.age
)
}
レスポンスのカスタマイズ
レスポンスは、単純な文字列やJSON形式のデータなど、さまざまな形式で返すことが可能です。
レスポンスの例
以下は、カスタムJSONレスポンスを返すコード例です。
src/main.rs
use axum::{
routing::get,
Json, Router,
};
use serde::Serialize;
use std::net::SocketAddr;
// レスポンスデータの構造体
#[derive(Serialize)]
struct ApiResponse {
status: String,
data: Option<String>,
}
#[tokio::main]
async fn main() {
// ルーターを作成
let app = Router::new().route("/status", get(handle_response));
// サーバーを起動
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server is running on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// GETリクエストのハンドラー
async fn handle_response() -> Json<ApiResponse> {
Json(ApiResponse {
status: "success".to_string(),
data: Some("Hello, Axum!".to_string()),
})
}
動作確認
- POSTリクエストの動作確認
APIクライアントで以下を送信:
- URL:
http://127.0.0.1:3000/submit
- メソッド: POST
- ボディ:
json { "username": "JohnDoe", "age": 30 }
レスポンス:
Received data: username = JohnDoe, age = 30
- GETリクエストの動作確認
- URL:
http://127.0.0.1:3000/status
レスポンス:
{
"status": "success",
"data": "Hello, Axum!"
}
解説
- リクエストの解析: Axumは
Json
型を使用して、リクエストデータを自動的に解析し、構造体にマッピングします。 - レスポンスの生成: 構造体を
Json
型で包むことで、クライアントに適切なJSON形式でレスポンスを返せます。 - エラー処理: Axumはエラー時にHTTPステータスコードをカスタマイズして返すことも簡単です(次章で説明)。
このように、リクエストとレスポンスを柔軟に扱うことが、Axumを活用したRESTful API構築の重要な要素です。
データベースの統合
Axumとデータベースの連携
RESTful APIの多くは、データの永続化にデータベースを使用します。Axumでは、SQLxのような非同期対応のデータベースライブラリを活用して簡単にデータベースを統合できます。
SQLxを使用したデータベース接続
以下の手順で、Axumとデータベースを統合する例を示します。PostgreSQLを例に説明します。
必要な依存関係を追加
Cargo.toml
に以下の依存関係を追加します。
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.6", features = ["runtime-tokio-native-tls", "postgres"] }
dotenvy = "0.15"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
環境変数の設定
データベース接続文字列を管理するため、.env
ファイルを作成します。
DATABASE_URL=postgres://username:password@localhost/dbname
データベース接続の初期化
以下のコードを追加して、データベース接続を初期化します。
src/main.rs
use axum::{routing::get, Json, Router};
use serde::Serialize;
use sqlx::PgPool;
use std::net::SocketAddr;
use dotenvy::dotenv;
use std::env;
// レスポンスデータの構造体
#[derive(Serialize)]
struct User {
id: i32,
name: String,
}
#[tokio::main]
async fn main() {
// 環境変数をロード
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// データベースプールを作成
let pool = PgPool::connect(&database_url).await.expect("Failed to connect to the database");
// ルーターを作成
let app = Router::new().route("/users", get(get_users)).with_state(pool);
// サーバーを起動
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server is running on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// データベースからユーザーリストを取得
async fn get_users(pool: axum::extract::State<PgPool>) -> Json<Vec<User>> {
let users = sqlx::query_as!(
User,
r#"
SELECT id, name FROM users
"#
)
.fetch_all(&**pool)
.await
.expect("Failed to fetch users");
Json(users)
}
動作確認
- データベースの準備
PostgreSQLで以下のSQLスクリプトを実行し、テーブルを作成します。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');
- アプリケーションの実行
サーバーを起動し、http://127.0.0.1:3000/users
にGETリクエストを送信します。以下のようなレスポンスが得られます:
[
{"id":1,"name":"Alice"},
{"id":2,"name":"Bob"},
{"id":3,"name":"Charlie"}
]
解説
- SQLxの使用: 非同期で安全なデータベース操作を提供するライブラリです。
query_as!
マクロを使ってデータを型安全にマッピングします。 - データベースプール: 接続を効率的に再利用するためにプールを使用します。これにより、スケーラブルなアプリケーションが構築可能です。
- Axumの状態管理:
with_state
を使用して、データベースプールを共有します。
このようにして、Axumを使ったAPIにデータベース統合を追加することで、データ駆動型のアプリケーションを構築できます。
認証と認可の実装
Axumでの認証と認可
Web APIでは、セキュリティ確保のために認証と認可が重要です。Axumでは、トークンベースの認証を簡単に実装でき、RBAC(ロールベースのアクセス制御)を用いた認可の設定も可能です。
JWTを用いた認証の例
JSON Web Token (JWT) を使用した認証を実装します。
必要な依存関係を追加
Cargo.toml
に以下を追加します:
[dependencies]
axum = "0.6"
jsonwebtoken = "8.2"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
JWTトークンの生成
以下のコードでトークンを生成するAPIを作成します。
src/main.rs
use axum::{routing::post, Json, Router};
use jsonwebtoken::{encode, Header, EncodingKey};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
// ユーザーデータ構造
#[derive(Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
// トークンリクエストデータ構造
#[derive(Deserialize)]
struct LoginRequest {
username: String,
password: String,
}
// トークンレスポンスデータ構造
#[derive(Serialize)]
struct TokenResponse {
token: String,
}
#[tokio::main]
async fn main() {
// ルーターを作成
let app = Router::new().route("/login", post(login_handler));
// サーバーを起動
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server is running on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// ログインハンドラー
async fn login_handler(Json(payload): Json<LoginRequest>) -> Json<TokenResponse> {
// 簡易認証(実際にはデータベースで確認)
if payload.username == "admin" && payload.password == "password" {
let claims = Claims {
sub: payload.username.clone(),
exp: chrono::Utc::now().timestamp() as usize + 3600, // 1時間有効
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret("secret".as_ref()),
)
.unwrap();
Json(TokenResponse { token })
} else {
panic!("Invalid credentials");
}
}
保護されたエンドポイントの作成
以下のコードでJWTトークンを検証し、保護されたエンドポイントを作成します。
src/main.rs
(追加部分)
use jsonwebtoken::{decode, DecodingKey, Validation};
// 認証付きハンドラー
async fn protected_handler(Json(token): Json<String>) -> &'static str {
let token_data = decode::<Claims>(
&token,
&DecodingKey::from_secret("secret".as_ref()),
&Validation::default(),
)
.unwrap();
println!("Authenticated user: {}", token_data.claims.sub);
"Access granted"
}
ルート設定
保護されたエンドポイントをルーターに追加します:
let app = Router::new()
.route("/login", post(login_handler))
.route("/protected", post(protected_handler));
動作確認
- トークンの取得
- URL:
http://127.0.0.1:3000/login
- メソッド: POST
- ボディ:
json { "username": "admin", "password": "password" }
- レスポンス:
json { "token": "<JWT_TOKEN>" }
- 保護されたエンドポイントへのアクセス
- URL:
http://127.0.0.1:3000/protected
- メソッド: POST
- ボディ:
json "<JWT_TOKEN>"
- レスポンス:
Access granted
解説
- JWTの利用: ユーザーごとにトークンを発行し、クライアントがそれを使用してAPIにアクセスする仕組みです。
- 簡易認証: 本例ではハードコードされたユーザー名とパスワードを使用していますが、実際にはデータベースでの認証が必要です。
- 認可の適用: トークンにロール情報を含め、エンドポイントごとにアクセス制限を実装できます。
このように、Axumで認証と認可を実装することで、セキュアなRESTful APIを構築できます。
APIのテストとデバッグ
APIのテスト方法
APIの正確性と信頼性を確保するためには、適切なテストを行うことが重要です。Axumで構築したRESTful APIは、以下のような方法でテストできます。
1. ユニットテストの実装
Rustの標準テストフレームワークを使用して、ハンドラー関数の動作を検証します。以下のコードは、ユニットテストの例です。
src/main.rs
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt; // for `oneshot`
#[tokio::test]
async fn test_get_handler() {
let app = Router::new().route("/", get(get_handler));
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(body, r#"{"message":"Hello, Axum!"}"#);
}
}
2. 統合テストの実装
エンドポイント全体の動作を確認するため、アプリケーション全体をテストします。以下のコードは統合テストの例です。
tests/integration_test.rs
(新規ファイル)
use axum::Router;
use hyper::http::{Request, StatusCode};
use tower::ServiceExt; // for `oneshot`
#[tokio::test]
async fn test_full_app() {
let app = my_app(); // メインアプリケーションの関数を呼び出す
let response = app
.oneshot(Request::builder().uri("/").body(hyper::Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(body, r#"{"message":"Hello, Axum!"}"#);
}
デバッグ方法
1. ログの活用
tracing
クレートを使用して、リクエストのログを詳細に記録します。Cargo.toml
に以下を追加します:
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
src/main.rs
に以下を追加します:
use tracing::{info, Level};
use tracing_subscriber;
#[tokio::main]
async fn main() {
// ログの設定
tracing_subscriber::fmt()
.with_max_level(Level::INFO)
.init();
info!("Starting server...");
let app = Router::new().route("/", get(get_handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
info!("Server is running on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
2. デバッガの利用
Rustのデバッガ(gdb
やlldb
)を使用して、コードをステップ実行することで問題箇所を特定します。
3. エラーのハンドリング
適切なエラーレスポンスを返すために、AxumでResult
型を活用します。例:
async fn get_handler() -> Result<Json<Greeting>, axum::http::StatusCode> {
Ok(Json(Greeting {
message: "Hello, Axum!".to_string(),
}))
}
動作確認
- ユニットテスト
コマンド:
cargo test
結果:
- すべてのテストが成功することを確認します。
- ログの確認
アプリケーション実行時にログが正しく出力されることを確認します。
まとめ
- ユニットテスト: 各ハンドラーの動作を個別に検証。
- 統合テスト: アプリケーション全体の動作を確認。
- ログとデバッグ: 問題発生時に詳細情報を収集。
これらの手法を活用することで、Axumで構築したRESTful APIの品質を確保できます。
応用例: フル機能のAPI構築
Axumを用いた完全なCRUD APIの構築
ここでは、Axumを使用して、フル機能のCRUD(Create, Read, Update, Delete)APIを構築する方法を解説します。データベースにはPostgreSQLを使用します。
コード例
以下は、簡単なタスク管理APIの例です。
依存関係
Cargo.toml
に以下を追加:
[dependencies]
axum = "0.6"
sqlx = { version = "0.6", features = ["runtime-tokio-native-tls", "postgres"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
dotenvy = "0.15"
データベースセットアップ
PostgreSQLで以下のスクリプトを実行してテーブルを作成:
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT FALSE
);
フル機能API
src/main.rs
use axum::{routing::*, Json, Router};
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, FromRow};
use std::net::SocketAddr;
use dotenvy::dotenv;
use std::env;
// タスクデータの構造
#[derive(Serialize, FromRow)]
struct Task {
id: i32,
title: String,
completed: bool,
}
#[derive(Deserialize)]
struct TaskInput {
title: String,
completed: Option<bool>,
}
#[tokio::main]
async fn main() {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPool::connect(&database_url).await.expect("Failed to connect to the database");
let app = Router::new()
.route("/tasks", get(get_tasks).post(create_task))
.route("/tasks/:id", get(get_task).put(update_task).delete(delete_task))
.with_state(pool);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server running on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// 全タスク取得
async fn get_tasks(pool: axum::extract::State<PgPool>) -> Json<Vec<Task>> {
let tasks = sqlx::query_as!(Task, "SELECT * FROM tasks")
.fetch_all(&**pool)
.await
.unwrap();
Json(tasks)
}
// タスク作成
async fn create_task(
pool: axum::extract::State<PgPool>,
Json(input): Json<TaskInput>,
) -> Json<Task> {
let task = sqlx::query_as!(
Task,
"INSERT INTO tasks (title, completed) VALUES ($1, $2) RETURNING *",
input.title,
input.completed.unwrap_or(false)
)
.fetch_one(&**pool)
.await
.unwrap();
Json(task)
}
// 特定タスク取得
async fn get_task(
pool: axum::extract::State<PgPool>,
axum::extract::Path(id): axum::extract::Path<i32>,
) -> Json<Task> {
let task = sqlx::query_as!(Task, "SELECT * FROM tasks WHERE id = $1", id)
.fetch_one(&**pool)
.await
.unwrap();
Json(task)
}
// タスク更新
async fn update_task(
pool: axum::extract::State<PgPool>,
axum::extract::Path(id): axum::extract::Path<i32>,
Json(input): Json<TaskInput>,
) -> Json<Task> {
let task = sqlx::query_as!(
Task,
"UPDATE tasks SET title = $1, completed = $2 WHERE id = $3 RETURNING *",
input.title,
input.completed.unwrap_or(false),
id
)
.fetch_one(&**pool)
.await
.unwrap();
Json(task)
}
// タスク削除
async fn delete_task(
pool: axum::extract::State<PgPool>,
axum::extract::Path(id): axum::extract::Path<i32>,
) -> &'static str {
sqlx::query!("DELETE FROM tasks WHERE id = $1", id)
.execute(&**pool)
.await
.unwrap();
"Task deleted"
}
動作確認
- GET /tasks: 全タスクを取得。
- POST /tasks: 新規タスクを作成。例:
{
"title": "Learn Axum"
}
- GET /tasks/:id: 特定のタスクを取得。
- PUT /tasks/:id: タスクを更新。例:
{
"title": "Learn Axum in depth",
"completed": true
}
- DELETE /tasks/:id: タスクを削除。
解説
- CRUD操作: Axumのルーティング機能を活用して、CRUDエンドポイントを実装。
- SQLx統合: 非同期のデータベース操作で効率的なAPIを構築。
- 構造体の利用: 型安全なデータ操作が可能。
この応用例を基に、完全なRESTful APIを構築し、実用的なWebサービスを開発できます。
まとめ
本記事では、RustのAxumフレームワークを活用して、RESTful APIを構築する方法を解説しました。Axumの基本セットアップから始まり、シンプルなエンドポイントの作成、リクエストとレスポンスのハンドリング、データベース統合、認証と認可、そしてCRUD操作を備えたフル機能のAPI構築までの手順を段階的に学びました。
Axumはその軽量かつ高性能な特性により、小規模なプロジェクトから大規模なシステムまで柔軟に対応できます。これを活用すれば、型安全で拡張性の高いAPIを効率的に構築できるでしょう。ぜひ本記事を参考に、実践的なプロジェクトに取り組んでみてください。
コメント