サイバーエージェントのインターン「CYCOMPE」に参加しました
はじめに
参加した2日間のインターンCYCOMPEがとてもよかったので、振り返りとともに来年以降の参加者が増えれば良いなと思って初めてzennで記事を書いています!
最後の方に技術的なところもちょろっとだけ記載します。
CYCOMPEとは?
自分は2024年8月24日(土)~25日(日)開催のバックエンドコースに参加しました!
また、参加者特典として優秀者には15万円、入賞者には3万円分の賞品が与えられます...!すごい
CYCOMPEの大きな特徴は参加者同士の競争で、1日目に上位に残っている人のみが2日目の採点対象となります。
CYCOMPEの課題
ABEMAの成功を羨ましく思った架空の会社の社長が開催するコンペで、新たな動画サービス「あれまぁTV」を開発するというものでした。(ネーミングセンス結構好きです)
レギュレーション
- 与えられたAPIドキュメントを満たすAPIを実装する
- 言語はGoで、Dockerfile内で完結させる
- Pull Requestがマージされると、採点環境で自動で採点が行われる
採点方式
採点方式は以下の2つがあります。
- 自動的なスコアリング
- アセスメントによるスコアリング
さらに、以下の項目も評価対象となっており、動くだけでなくプロダクトとしての完成度も求められます。
- テストコードの拡充
- lintの指摘項目数の少なさ
- ドキュメントの整備
- プロダクションを意識したコード設計、コーディング
API仕様
開始時にはv1の仕様書が渡され、時間が経つにつれv2、v3、v4と実装量が多くなっていきます。。自分はv3の途中で完全に追いつかなくなりました笑
主に3つのAPI仕様書が渡されます
- ユーザー向け
- 管理者向け
- 決算エミュレーター(外部API)
結果と感想
1日目
全体17人参加で、上位10人が突破できる中、10位でギリッギリ通過しました。1日目終了2時間前まで得点率2割くらいで低迷していたので最後の最後になんとか点数を取ることができました。
2日目
2日目の最初に初日通過者が発表され、なんとか通過したという喜びから順位上げてやるぞ!と意気込んだのも束の間、v3の実装が全然終わらずあっという間に終了時間になりました...
最終的な結果は17人中8位でした!!悔しい!!!!!!!
感想
総じてとても満足した2日間でした!!周りの学生のレベルが本当に高く、かなり刺激を受けることができました。ボリュームが多いので、Goのバックエンドエンジニアで実力を試したいという方にはピッタリかと思います!
実装
せっかくなので、工夫した点だけ軽く紹介して終わろうと思います。
2日間で89Commitと、個人的にはかなり集中して実装できました!
スキーマ駆動開発
OpenAPIとしてAPI仕様が与えられるので、それを元にGoのコードを自動生成し、仕様に沿ったコードを意識しました。自動生成はoapi-codegenを使いました!
リクエストやレスポンスなど、必要なスキーマを構造体として自動生成してくれ、ルーティングも定義されているので、サーバーのインターフェース通りに関数を書くだけでAPIが実装できます。
2日間でこれだけの量の実装を行いました
// ServerInterface represents all server handlers.
type ServerInterface interface {
// チャンネル入稿
// (POST /admin/v4/channels)
CreateChannel(w http.ResponseWriter, r *http.Request)
// 番組入稿
// (POST /admin/v4/programs)
CreateProgram(w http.ResponseWriter, r *http.Request)
// チャンネル一覧取得
// (GET /user/v4/channels)
ListChannels(w http.ResponseWriter, r *http.Request)
// コメント一覧取得
// (GET /user/v4/comments)
ListComments(w http.ResponseWriter, r *http.Request, params ListCommentsParams)
// コメント作成
// (POST /user/v4/comments)
CreateComment(w http.ResponseWriter, r *http.Request)
// 番組一覧取得
// (GET /user/v4/programs)
ListPrograms(w http.ResponseWriter, r *http.Request, params ListProgramsParams)
// 番組単体取得
// (GET /user/v4/programs/{programId})
GetProgram(w http.ResponseWriter, r *http.Request, programId string)
// 番組視聴可否確認
// (POST /user/v4/programs/{programId}/canWatch)
CanWatchProgram(w http.ResponseWriter, r *http.Request, programId string)
// 購読一覧取得
// (GET /user/v4/subscriptions)
ListSubscriptions(w http.ResponseWriter, r *http.Request)
// 購読登録
// (POST /user/v4/subscriptions)
CreateSubscription(w http.ResponseWriter, r *http.Request)
// ユーザーログイン
// (POST /user/v4/users/login)
LoginUser(w http.ResponseWriter, r *http.Request)
}
アーキテクチャ
プロダクションを意識したコード設計、コーディングが評価対象となるため、アーキテクチャも意識して設計しました。
.
├── cmd
├── domain # ドメイン層。エンティティや値オブジェクトを定義
├── handler # ハンドラ層。APIのリクエストを受け取り、ユースケースを呼び出す。また、OpenAPIの定義もここにあり、定義に従ってバリデーションする
├── repository # リポジトリ層。データベースの操作を行う
├── server # サーバ層。APIサーバの起動や終了を行う
└── usecase # ユースケース層。ビジネスロジックを実装
各レイヤーにchanel.goやprogram.goなど、domainに対応するファイルを配置し、責務を明確にします。
mainとなる関数で、それぞれの依存関係を整理しつつ初期化します。
r := repository.Repositories{
Program: repository.NewProgramRepository(db),
User: repository.NewUserRepository(db),
Channel: repository.NewChannelRepository(db),
Comment: repository.NewCommentRepository(db),
Subscription: repository.NewSubscriptionRepository(db),
}
u := usecase.Usecases{
Program: usecase.NewProgramUsecase(r.Program),
User: usecase.NewUserUsecase(r.User),
Channel: usecase.NewChannelUsecase(r.Channel),
Comment: usecase.NewCommentUsecase(r.Comment),
Subscription: usecase.NewSubscriptionUsecase(r.Subscription),
}
h := handler.Handlers{
Channel: handler.NewChannelHandler(u.Channel),
User: handler.NewUserHandler(u.User),
Program: handler.NewProgramHandler(u.Program),
Comment: handler.NewCommentHandler(u.Comment),
Subscription: handler.NewSubscriptionHandler(u.Subscription),
}
svr := server.NewServer(h)
ctx := context.Background()
router, err := server.NewRouter(ctx)
ユーザー認証
ユーザーのログイン時にJWTトークンを発行し、認証が必要なAPIでは、responseのtokenをAuthorization Headerに、Bearerトークンとして設定するという仕様があり、ここに大苦戦しました。。
トークンの生成はUsecaseで行うようにしています
func (u *UserUsecase) generateToken(userId string) (string, error) {
// JWTのペイロードを作成
claims := jwt.MapClaims{
"userId": userId,
"exp": time.Now().Add(time.Hour * 1).Unix(), // トークンの有効期限は1時間
}
// トークンを生成
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secret := []byte("your-secret-key")
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, nil
}
oapi-codegenでは、OpitonとしてAuthenticationFuncを設定することで、認証が通を通すことができます。ここでトークンの取得、バリデーションを行うことができます。
おわりに
改めてサイバーエージェントさん素敵なコンペを用意してくださりありがとうございました!
バックエンドエンジニアとして技術力を上げていきたいというモチベーションになりました。学生の皆さんはぜひ腕試しに受けてみてください!
Discussion