Mockitoのテストをクリーンに保つための機能

はじめに

こんにちは、エキサイト株式会社でインターンをさせていただいている山内です。 今回はMockitoを使ってユニットテストを書いていた際に遭遇したUnnecessaryStubbingExceptionというエラーについてご紹介します。

状況

SpringBootでのサンプルコードは以下のようになっています。

// SampleRepository.java
public interface SampleRepository {
    Boolean hoge();

    Boolean fuge();
}
// SampleService.java
public interface SampleService {
    Boolean hoge();
}
// SampleServiceImpl.java
@Service
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {
    private final SampleRepository sampleRepository;

    @Override
    public Boolean hoge() {
        return sampleRepository.hoge();
    }
}
// SampleServiceImplTest.java
@ExtendWith(MockitoExtension.class)
public class SampleServiceImplTest {
    @Mock
    private SampleRepository sampleRepository;

    @InjectMocks
    private SampleServiceImpl sampleService;

    @Test
    @DisplayName("サンプルテスト")
    public void sampleTest() {
        Mockito
                .when(sampleRepository.hoge())
                .thenReturn(true);

        Mockito
                .when(sampleRepository.fuge())
                .thenReturn(true);

        Assertions.assertEquals(true, sampleService.hoge());
    }
}

テストを実行すると、下記のようなエラーが出ます。

org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.

原因

エラーログの通り、不要なスタブが定義されていることが原因です。

今回の例で言うと、SampleRepository.javaではhoge()とfuge()というメソッドが定義されていますが、SampleService.javaで使われているのはSampleRepository.hoge()のみです。つまり、SampleRepositoryImplTest.javaで定義されている以下の部分は今回テストしたいSampleService.hoge()では使われていないため不要なスタブとして検出されているのです。

Mockito
        .when(sampleRepository.fuge())
        .thenReturn(true);

スタブとは

スタブとは、テストしたい物とは別のメソッドの振る舞いを定義してあげるものです。つまり、テストしたいメソッド内で他のメソッドを呼んでいる場合に他のメソッドの動作を保証しなくてもテストが書け、それぞれのメソッドごとに動作を保証することができるというものです。

解決方法

解決方法はシンプルで、以下のように不要なスタブを消してあげるだけです。

// SampleServiceImplTest.java
@ExtendWith(MockitoExtension.class)
public class SampleServiceImplTest {
    @Mock
    private SampleRepository sampleRepository;

    @InjectMocks
    private SampleServiceImpl sampleService;

    @Test
    @DisplayName("サンプルテスト")
    public void sampleTest() {
        Mockito
                .when(sampleRepository.hoge())
                .thenReturn(true);

//        Mockito
//                .when(sampleRepository.fuge())
//                .thenReturn(true);

        Assertions.assertEquals(true, sampleService.hoge());
    }
}

その他の解決方法もあり、こちらのMockitoブログで言及されています。

なぜこのようなエラーを出すのか

このようなエラーを出している理由は、テストを読みやすく、クリーンに保つためです。 テストをクリーンに保つことで、そのテストで本当にしたいことが明確になり、見通しの良いコードになると思います。

最後に

このインターンを通して、「良いコードを書く」ということはとても難しく、日々意識して書いていくことで身に付いていくものだと実感しています。今回ご紹介したMockitoの不要なスタブの検証もその一歩になるのではないかと思っています。

まだまだ未熟ですが、これからも良いコードを意識して書いていきたいと思います!

最後までお読みいただきありがとうございました!

「WEラブ赤ちゃんプロジェクト」制作物の確認に京都へ!

f:id:excite_nishiba:20220315090111p:plain

こんにちは。エキサイト株式会社・デザイナーの西場です。

ウーマンエキサイト発足の『WEラブ赤ちゃんプロジェクト』という取り組みがあります。

woman.excite.co.jp

このたび、このプロジェクトに賛同いただいた
京都府子育て環境日本一推進会議様の制作物を現場にて確認するため
プロデューサー・ディレクターと共に京都へ行ってまいりました。

今回は20種類以上と制作物の点数も多く、
大きさ・掲載場所・素材等も多種多様だったので
見る方のシチュエーションなどを加味し、
適切な色味・内容・レイアウトについて都度チームで話し合い、検討しました。

しかし、印刷はデジタル媒体と違い
刷り上がるまでどのような色味になるのか正確にはわかりません。
素材によって仕上がりも違い
屋内・屋外の違いやライティングによって見え方も変わります。

実際に目にするまでは想像の範囲でしかなく、、、
緊張しながら見つけましたが
沢山掲示いただき、
仕上がりも綺麗で感動しました!


f:id:excite_nishiba:20220310155837j:plainf:id:excite_nishiba:20220310155841j:plainf:id:excite_nishiba:20220310155843j:plainf:id:excite_nishiba:20220310155846j:plainf:id:excite_nishiba:20220310155955j:plain


やはり社内で作業している時とは迫力が違いますね。

他社様の制作物と共に現場で確認することにより
メリハリによる明確な伝え方、素材ごとの適切な色味、設置場所への配慮、等
より良く改善していくための気づきもありました。

また、皆さまの目に触れている場面を見て、
改めて、子育てにやさしい社会づくりのお手伝いになれれば嬉しいな、と感じました。


この後も、ラッピングバスや商店街フラッグなど多く制作物が登場する予定で、
京都府子育て環境日本一推進会議×WEラブ赤ちゃんプロジェクト』は続きます。
ぜひ「泣いてもかましまへん!」と赤ちゃん、ママ・パパにエールを!

f:id:excite_nishiba:20220310154859p:plain

Spring Bootで、環境ごとに異なる値を持つモデルクラスを作るオススメ方法

こんにちは。 エキサイト株式会社の三浦です。

コードを書いていると、たまに環境ごとに異なる値を使いたくなるときがあります。 例えば、開発環境と本番環境で使用するDBが異なる場合は、DBのURLは環境ごとに異なるはずです。

今回は、Spring Bootでそのような環境ごとに異なる値をモデルクラス内で使いたい場合の、オススメの方法を紹介します。

環境ごとに異なる値

コードを書く時、たまに環境ごとに異なる値を使いたくなることがあります。 Spring Bootでは、そのような値を扱う方法の1つとして、 application.yml というものを提供しています。

例えば、 local 環境と prod 環境で別々の値を持ちたい時は、

application-local.yml

data_1: local_value

application-prod.yml

data_1: prod_value

というファイルを用意し、実行時にファイル名をそれぞれ指定すれば、コード内で data_1 を呼び出すと、指定したファイルの値を取得することができるようになるのです。

この application.yml 内のデータを、モデルクラスで使いたい場合があるとしましょう。 以下のような場合です。

public class SampleImageModel {
    /**
     * 画像ドメイン
     * TODO: ここに、application.ymlから値を持ってきたい
     */
    private String imageDomain;

    /**
     * 画像パス
     * この値は、DB等からとってきてそのまま入れる
     */
    private String imagePath;

    /**
     * 画像URLを取得する
     * @return 画像URL
     */
    public String getImageUrl() {
        return this.imageDomain + this.imagePath;
    }
}

方法はもちろんいくつかありますが、個人的にオススメの方法を紹介します。

オススメ取得方法

application.ymlの準備

まずは application.yml を環境ごとに用意します。 ここまでは同じです。

image:
  domain: https://sample/

コードからapplication.ymlのデータを呼び出す

続いて、コードから以下の方法で application.yml を呼び出します。

package sample;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;

@ConstructorBinding
@ConfigurationProperties(prefix = "image")
@RequiredArgsConstructor
public class SampleImageConfig {
    /**
     * 画像ドメイン
     */
    @Getter
    private final String domain;
}

取得方法は他にもありますが、こうすることで、例えばスキーマは別管理にしたいときに、

package sample;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;

@ConstructorBinding
@ConfigurationProperties(prefix = "image")
@RequiredArgsConstructor
public class SampleImageConfig {
    /**
     * 画像ドメイン
     */
    private final String domain;

    public String getDomain() {
        return "https://" + domain;
    }
}

こうできるなど、より柔軟性をあげることができたりします。

モデルクラスにデータを入れる

最後に、モデルクラスにデータを入れるには、Factoryクラスを使用します。

まずはモデルクラスで、コンストラクタからデータを入れられるようにします。

package sample;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class SampleImageModel {
    /**
     * 画像ドメイン
     * TODO: ここに、application.ymlから値を持ってきたい
     */
    private final String imageDomain;

    public SampleImageModel(String imageDomain) {
        this.imageDomain = imageDomain;
    }

    /**
     * 画像パス
     * この値は、DB等からとってきてそのまま入れる
     */
    private String imagePath;

    /**
     * 画像URLを取得する
     * @return 画像URL
     */
    public String getImageUrl() {
        return this.imageDomain + this.imagePath;
    }
}

その上で、Factoryクラスを作成します。

package sample;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class SampleImageModelFactory {
    private final SampleImageConfig sampleImageConfig;

    public SampleImageModel create() {
        return new SampleImageModel(this.sampleImageConfig.getDomain());
    }
}

これを作れば、後はこのモデルを生成したい時は、以下のようにすれば簡単に作れるようになります。

// SampleImageModelFactoryはDIして使う
SampleImageModel sampleImageModel = sampleImageModelFactory.create().setImagePath("sample/image.jpg");

この方法を取ることで、「ドメインをあらかじめ application.yml から持ってくる」などのことを考えずにモデルクラス使えるようになります。

最後に

今回は個人的なオススメ方法を紹介しましたが、あくまで個人的な考えのものであり、もしかしたらもっと良い方法があるかもしれません。 あくまで一例として参考にしていただければと思います。

【業務効率化】制作ヒアリングシートを復活させた話

f:id:excite_ny:20220310224627p:plain

こんにちは!21卒デザイナーの山﨑です。

今回は「制作ヒアリングシートを復活させた話」をしたいと思います。

制作ヒアリングシートって何?

制作ヒアリングシートとは、ビジネスやエンジニアがクリエイティブに仕事を依頼する際に記入するシートの事です。

ヒアリングシートに記入したらGoogleスプレッドシートに記入されるようになっています。

f:id:excite_ny:20220310125505p:plain

f:id:excite_ny:20220311122306p:plain
スプレッドシートに情報が反映される

何故使われていなかったのか?

去年の4月ごろにはすでに出来ていたシステムだったのですが、長らく放置されていました。

その原因は2つありました。

ヒアリングシートの質問数と回答必須項目が多く、記入側のモチベを低下させていた。

②「ビジネス側から仕事が来る」→「ヒアリングシートを渡す」という文化を定着しきれなかった為、活用の機会が失われていた。

質問の項目数を減らす

一番初めのヒアリングシートの項目数はざっと23項目ありました。

質問項目

  1. メールアドレス
  2. 案件名
  3. 納品物
  4. 制作の目的
  5. ターゲット
  6. コンバージョン
  7. SEO対策はするのか
  8. 必要な機能
  9. リリース予定時期
  10. 納品希望日
  11. 既存URL(あれば)
  12. 競合
  13. デザインテーマ、コンセプト
  14. 希望のイメージ(参考サイトなどURLあれば)
  15. 対応ブラウザ
  16. 希望のフォント
  17. サーバーの有無
  18. ドメインの有無
  19. ロゴデータの有無
  20. 画像や素材の有無
  21. 原稿データの有無
  22. SNSの有無
  23. そのほか特記すべきこと

かなり多いですね。

大体ヒアリングシートを記入する方は「作って欲しいものは決まってるけど、フォントとか雰囲気もよく分からない…」という方が大半だと思います。

そんなフワッとしたイメージのままで「競合は?サーバーの有無は?ドメインの有無は?画像や素材はあるの?」など質問攻めに合うと記入者のモチベーションは低下してしまいます。

f:id:excite_ny:20220310152400p:plain
困るビジネス

そこで、最低限の質問のみに絞ることでモチベ低下を防ごうと考えました。

f:id:excite_ny:20220310225231p:plain
質問数を減らしたヒアリングシート1

f:id:excite_ny:20220310225252p:plain
質問数を減らしたヒアリングシート2

f:id:excite_ny:20220310225304p:plain
質問数を減らしたヒアリングシート3

質問項目

  1. 案件名
  2. 納品物
  3. 納品希望日
  4. 制作の目的
  5. ターゲット
  6. 必要な機能
  7. コンバージョン
  8. SEO

23項目から8項目、名前やメールアドレス・デザインイメージなどを約三分の一まで減らしました。

そして従来ではセクションも分けないものになっていたのですが、こまめにセクションを分ける事でサクサク画面遷移で切替えられるようにしています。

slackの通知機能をつける

二つ目の『「ビジネス側から仕事が来る」→「ヒアリングシートを渡す」という文化を定着しきれなかった為、活用の機会が失われていた。』に関しては、

ヒアリングシートに記入されても通知がない

②ビジネス側への周知が薄い

というのが原因でした。

その問題を解決するために以下の施策を行っていただきました。

ヒアリングシートが記入されたらslack通知がチャンネルに行く

②事業部の締め会でヒアリングシートの周知を広める

f:id:excite_ny:20220310230622p:plain
ヒアリングシートに記入されたらslackのチャンネルに通知が行く様子

f:id:excite_ny:20220310231552p:plain
締め会で発表する様子

この2つの施策を行い、デザイナー個人にDMで仕事が来て「誰がどんなタスクを負っているのか・誰の手が空いているのかわからない」という問題を解消し、

①ビジネスorエンジニアがヒアリングシートで依頼する

②クリエイティブ全体で仕事を誰が担当するか話し合う

アサインされたデザイナーと依頼者で再度ヒアリングを行う

このような「クリエイティブ全体で仕事を受ける流れ」を作っていけたらいいなと思います。

終わりに

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があれば連絡いただければと思います🙇‍♀️

それではまた!

www.wantedly.com

認証・決済APIの移行を無事完了しました(Yahoo! ID連携v2移行)

エキサイトの菅間です。 普段は、電話占い・お悩み相談室・恋ラボなどのToC向け課金プラットフォームの開発を行っています。

今回、Yahoo! ID連携 v1のクローズに伴うv2への移行を担当し、認証・決済に関するAPIのエンドポイントの移行を無事に完了したので振り返りをしたいと思います。

プロジェクトの背景

Yahoo!版電話占いはYahoo! ID連携を使ってログイン機能、コイン購入機能を実装しています。 今回は、Yahoo! ID連携 v1のクローズに伴うv2への移行を担当しました。

プロジェクトの概要は下記の通りです。

  • プロジェクト期間:2021年11月 ~ 2022年2月
  • プロジェクトのゴール:問題を発生させずプロジェクトの移行を完了させる。

プロジェクトは、自分1名で主導して行いました。 全体の進捗はマネージャーと企画の方に相談、コードレビューや相談は事業部の開発メンバーに相談しながら進めました。

移行の対象

  • Yahoo! ID連携 v2への移行
    • エンドポイントの切り替え
      • 認証
      • 決済
  • APIのレスポンスの変更とそれに伴う改修

移行の結果

  • Yahoo! Wallet API移行:1月に移行完了
  • Yahoo! ID連携v2移行:2月に移行完了

結論としては、本記事執筆時点ではトラブルが発生することなく、運用ができています。 また、不要なファイルも900ファイル以上削除することができました。

プロジェクトの移行が完了したので、振り返り・反省を行っていきたいと思います。

良かった点

まずはプロジェクトの良かった点を振り返りたいと思います。

  • 問題が発生することがなく、プロジェクトを移行できた。
  • リファクタリングを進めることができた。
  • 認証・認可・OAuth2.0周りに関してキャッチアップできた。

問題が発生することがなく、プロジェクトを移行できた。

プロジェクトのゴールである「問題を発生させずプロジェクトの移行を完了させる。」ことが達成できたのが1番良かったです。

無事に移行を完了させるために下記点を意識しました。

タスクの管理

以下理由でプロジェクト管理にスプレッドシートを活用しました。

  • 企画の人にも進捗を共有する。
  • 社内の別プロジェクトでも使われているため導入コストが軽い・ある程度最適化されたテンプレートが存在する
  • 無料
  • その日からすぐ使える

スケジュール管理

タスクを細分化して、それぞれに期限を設けるのですが、「アラートを出すタイミング」は意識をしました。 スケジュールの進捗が遅れた場合は、都度上長へ相談・共有を行いました。

今回のプロジェクトでは、Yahoo社から配布されるSDKのリリースが遅れた関係もあり、当初の期限では厳しくなりました。 その際もいち早く上長に相談し、先方とスケジュール調整に入ることができました。

リスク管理

スケジュールに間に合いそうにない場合の解決策としてメンバーの増員が考えられます。 仮にそのような状況になった場合に、素早くプロジェクトの概要を掴んでもらうためにGoogle Driveにドキュメントの一元管理・整備を行いました。

また、Pull Requestを事業部の幅広いメンバーにお願いし、何となくプロジェクトを知ってもらえるようにしました。
(レビューしてくださったみなさんありがとうございます。)

リファクタリングを進めることができた。

今回の移行対象のリポジトリには2つのサービスが同居していました。 1つはサービス終了済みなため、サービスは動いていないが、コードは残っている状態でした。

検索でヒットする、初見の人が困惑するなど開発者体験が良くないため、今回の移行に伴いほぼ全て削除しました。

認証・認可・OAuth2.0周りに関してキャッチアップできた。

プロジェクトというより個人的な良かった点なのですが、認証認可に関しての改修経験を積むことができました。

qiita.com

反省点

次に、プロジェクトの反省点を振り返りしたいと思います。

  • スケジュールの不確実性を明らかにすることにもっと着手すべきだった。
  • プロジェクト初期に客観的なフィードバックを経験者である先輩にもっと早くもらうべきだった。

スケジュールの不確実性を明らかにすることにもっと着手すべきだった。

プロジェクトの初期は、わからないことがたくさんあります。
どれくらい時間がかかるのか、見積もりが問題がないのか?など解像度が低い状態です。

今回は、サービスの開発やSEO系のタスクも並行して行なっており、プロジェクト初期に初速を出すことができませんでした。
初速が出せないリスクとして、何かあったときの対応が後手に回ってしまいます。

そのため、上長と既存のタスクの調整をし、初速を出すことに注力すべきでした。
次回からは、プロジェクトのタスクを網羅する、鍵となりそうなタスクを仕分けする、ざっと見積もりをする時間を確保する動きをしたいと思います。

プロジェクト初期に客観的なフィードバックを経験者である先輩にもっと早くもらうべきだった。

社内には様々なプロジェクトをこなしてきた先輩がいるので、もっと早い段階で巻き込むことができたら、よりスムーズに進めることができたと思います。

プロジェクトの振り返り

1人プロジェクトでしたが、事業部の先輩とプロジェクトの振り返りを行いました。 客観的にフィードバックを以下のようにいただきました。

良かった点

当事者意識を持ってプロジェクトの移行を完遂した。

  • 先方の担当者とメールのやり取りを率先して行った。
  • 古いソースコードの削除やリファクタリングなどを主体的に行った。
  • 関連する箇所のドキュメントの更新を行なっていた。

また、以下のようなフィードバックを総会でいただきました。

YahooSDKバージョンアップにおいて開発による実装だけで無く、当事者意識でyahoo担当者と密に連絡を取りバージョンアップ完了不具合も無く、うやむやになっていた連携部分の仕様が整備されました

.

自ら能動的に動き、施策にも細かく丁寧に向き合っている姿勢が素晴らしく、Yahooのv2移行について、先方との調整も含めてスピード感をもって対応しています

プロジェクトの進め方が良かった。

  • 計画書、テスト項目、タイムテーブルなどの資料を作成・共有し、緻密にスケジュール管理ができていた。
  • 様々な人にレビューをお願いすることによって、プロジェクトを知ってもらい、巻き込むことができていた。リスク管理として良かった。

カンファレンスへの登壇や既存の運用系も積極的に行なっていた。

  • 移行のプロジェクトだけではなく、それ以外のタスクも行なっていた姿勢が良かった。

tech.excite.co.jp

改善点

プロジェクトの初速が出ておらず、初期はプロジェクトが不透明だった。

「1人でやれらないといけないことだが、もっと周りの人を頼っても良い」とのことでした。

目的はプロジェクトを無事に完了させることなので、目的意識が大事ですね。

概ね、先ほどの反省点で出た内容を振り返りました。

最後に

そんなエキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。

www.wantedly.com

Terraformのディレクトリ構造と設計指針の一例を紹介します

エキサイト株式会社の武藤です。

エキサイトではAWSの構成管理にTerraformを採用しているサービスがいくつかあります。 Terraformのディレクトリ構造について検索してみると、様々なパターンが出てきます。 どのパターンが最適なのか、選択に迷ってしまう方も多いと思います。 今回は私が担当しているE・レシピについて、ディレクトリ構造と設計指針を紹介します。参考になれば幸いです。

全体構成

まずはディレクトリの全体構成についてです。

├── application
│   ├── web_server   // EC2作成
│   │   ├── dev
│   │   │    └── v1
│   │   └── prod
│   │      └── v1
│   └── modules   
├── gateway
│   ├── web_alb     // ALB作成
│   │  ├── dev
│   │  │    └── v1
│   │  └── prod
│   │     └── v1
│   └── modules 
└── infra
   └── vpc           // VPC作成
        ├── dev
        ├── modules
        └── prod

一例として、インフラリソースの作成単位にVPC、EC2、ALBがあるとします。 それぞれに対して、infra, application, gatewayというレイヤーを設けており、ディレクトリもそのように階層を分けています。 VPCは、最初に作られるインフラリソースです。その後、EC2、ALBの作成が行われます。 作成するリソースに依存関係があるように、レイヤーにも依存関係を考えます。

  • infra : VPC作成
  • application : EC2作成
  • gateway : ALB作成

このように依存関係があることで、作成するインフラリソースの順番、破棄する順番を示して、運用ルールとしています。 新しくインフラリソースを追加する際は、どこのレイヤーに依存しているかを考えてディレクトリを追加します。

moduleディレクトリは、基本的に各レイヤーごとにmoduleを作るようにします。 moduleが使われる範囲を狭めて、管理しやすくしています。

workspace 機能を使わない

Terraformには環境ごとの構成差分を管理するためにworkspace機能が用意されています。 しかし、弊チームでは可読性やわかりやすさを重視するため、workspace機能を使わずにディレクトリ階層によってdev, prodを分けています。 ディレクトリで示すことで、ひと目で環境差異が把握できる点、map関数、lookup関数を使わずにシンプルに書ける点があります。 (メンバーのTerraform習熟度が高ければ、workspaceを使う方が効率的なケースもあると思います。)

ディレクトリの環境分けの方法では重複コードも出てきますが、そこは諦めています。。

module化するタイミング

公式ドキュメントの考え方に則って考えています。 www.terraform.io

過度にmodule化すると全体構成が複雑になってしまい、メンテナンスしにくくなってしまいます。

We do not recommend writing modules that are just thin wrappers around single other resource types. If you have trouble finding a name for your module that isn't the same as the main resource type inside it, that may be a sign that your module is not creating any new abstraction and so the module is adding unnecessary complexity. Just use the resource type directly in the calling module instead.

上記では、リソースの薄いラッパーになるようなmodule化は推奨しないと説明しています。 基本的には複数のリソースを意味のある単位でmodule化しています。

moduleを定数として扱う

過度なmodule化に注意しつつ、極力重複する記述は避けたいところです。 AWSの場合は、セキュリティグループの設定が様々なリソース作成のたびに出てきますので、共通化したい設定の一つです。 セキュリティグループのインバウンド, アウトバウンドルールは、社内オンプレからのアクセス、AWSネットワークからのアクセスなど、同じIPレンジを使うことが多いので共通の定数として扱います。

下記は、定数を出力するだけのmoduleを作成し、リソースで読み込むサンプルコードです。

├── constants
│   ├── output.tf
│   └── security_group_rules.tf     // セキュリティグループに関する定数
# security_group_rules.tf
locals {
  # エキサイトAWSネットワーク
  security_group_excite_aws_network = {
    from_port = ****
    to_port   = ****
    protocol  = "tcp"
    cidr_blocks = [
       ***.***.***.***/**,
       ***.***.***.***/**,
       ***.***.***.***/**
    ]
    description = "hogehoge"
  },
  # ICMP
  security_group_icmp = {
    from_port = -1
    to_port   = -1
    protocol  = "icmp"
    cidr_blocks = [
      "0.0.0.0/0"
    ]
    description = "ICMP Rule"

  }

...
}
# 定数を読み込むリソースファイル
module "constants" {
  source = "../constants"
}

module "ec2" {
  source = "../modules/ec2"

...

  security_group_rule_ingress = [
    module.constants.security_group_excite_aws_network,
    module.constants.security_group_icmp,

  ]

...
}

Terraformのlocal valuesの場合、外部からの入力できなくなるため、定数の用途で扱えます。

下記の記事を参考にさせていただきました。 qiita.com

最後に

Terraformのディレクトリ構造と設計指針について、一例を紹介しました。

今回は、可読性やわかりやすさを重視したディレクトリ構造と設計指針で実装を進めました。 何を重視するかはチームメンバーやサービスによって変わると思います。 メンバーと話し合いながら、運用しやすいTerraformを書きましょう。

参考記事

www.terraform.io

qiita.com

AWS GlueでSparkのDataFrameとしてOracleテーブルの情報を取得する

f:id:e125731:20220309103322p:plain

エキサイト株式会社 エンジニアのあはれん です。

弊社では、アプリケーションのデータベースとしてはAmazon RDS for Oracleを利用し、 データ分析にAmazon Redshiftを使っています。

Amazon RDS for Oracleデータ(データストア)からAmazon Redshift(データターゲット)にデータを転送するため、AWS Glueを利用しています。

今回は、AWS Glueを使ってデータを転送する際に発生した問題とその対処方法について説明します。

問題

AWS Glue ジョブを実行時に以下のエラーが発生しました。

pyspark.sql.utils.IllegalArgumentException: Oracle identifier cannot be more than 30 characters. 

Oracleの識別子(テーブル名)が30文字を超えている」というエラーです。

Oracle 12cR1以前は30文字以内という仕様になっていましたが、Oracle 12cR1以降は128文字まで利用可能になっています。 弊社で利用しているOracleは12cR1以降のものでしたので、アプリケーションで使っている時は128文字でも利用できていました。

この件をAWSのサポートセンターに相談したところ、 2022年3月時点では、 GlueのJDBCドライバーの接続種別(connectionType)Oracleを指定した場合、 Glue サービス側で当該エラーを出力している状況となっており、利用者側から回避することができないという回答をいただきました。

対処方法として、SparkのDataFrameとしてOracle テーブルの情報を取得する方法を提案いただきました。

これまでは、GlueのJDBCドライバーを利用して設定したデータカタログからOracleテーブルの情報を取得していましたが、 SparkのDataFrameとしてOracle テーブルの情報を取得することにしました。

対処方法

df = spark.read \
    .format("jdbc") \
    .option("url", "[JDBC データストアのURL]") \
    .option("dbtable", "[テーブル名]") \
    .option("user", "[データベースのユーザ名]") \
    .option("password", "[データベースのパスワード]") \
    .load()

SparkのDataFrameの取得方法は上記のようになります。 詳細は以下のドキュメントを参照ください。

spark.apache.org

JDBC データストアのURLについてAWS公式が、データベースエンジンごとの構文を紹介しています。

Oracleの場合は以下の構文パターンになります。

jdbc:oracle:thin://@host:port/service_name
jdbc:oracle:thin://@host:port:SID

docs.aws.amazon.com

SparkのDataFrameとしてデータを取得後は、AWS Glueで利用されているDynamicFrameに変換します。

DynamicFrame.fromDF([変換するDataFrame],[GlueContextクラスオブジェクト],[結果の DynamicFrame の名前])

docs.aws.amazon.com

DynamicFrameに変換後は、Glueで自動生成されてるスクリプトと同じ処理を行うことができます。

コード例

import sys

from awsglue.context import GlueContext
from awsglue.dynamicframe import DynamicFrame
from awsglue.job import Job
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)

# SparkのDataFrameとしてOracle テーブルの情報を取得する
df = spark.read \
    .format("jdbc") \
    .option("url", "[JDBC データストアのURL]") \
    .option("dbtable", "[テーブル名]") \
    .option("user", "[データベースのユーザ名]") \
    .option("password", "[データベースのパスワード]") \
    .load()

# DataFrameをDynamicFrameに変換
datasource0 = DynamicFrame.fromDF(df, glueContext, 'datasource0')

// 以降はGlueで自動生成されてるスクリプトと同じ処理で問題ない
applymapping1 = ApplyMapping.apply(....

その他

ジョブのバージョンや利用言語について

Glue version :Glue 2.0 - Support spark 2.4, Scala 2, Python 3
Language: Python 3

Glueの接続

AWS Glue ジョブはAWS Glue 接続を使用して特定の種類のデータストアにアクセスします。 今回の場合も、Amazon RDS for Oracleデータ(データストア)にアクセスするために設定しました。設定方法に関しては以下を参照ください。

docs.aws.amazon.com

最後に

同じエラーが出た方のお役に少しでもなれたらと思います。 最後まで読んでいただきありがとうございます。

エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております! 興味がありましたらご連絡ください。

www.wantedly.com

【Excite × iXIT TechCon2022】Core Web Vitalsの取り組みに関してLTをしました。

f:id:excite_sugama:20220303165158p:plain

エキサイト株式会社L&C事業部の菅間です。 普段は電話占い・お悩み相談室・恋ラボといったToC向け課金プラットフォーム事業の開発に関わっています。

今回はExcite × iXIT TechCon2022に「終わらないSEO対策と向き合うために」というテーマでLTを行いました。

Excite × iXIT TechCon2022とは?

詳しくはこちらの記事をご確認ください。

tech.excite.co.jp

終わらないSEO対策と向き合うために

発表内容の結論としては、システムは日々成長し、変化するため、一時的な対策では意味がなく、改善し続ける体制・文化の構築が必要になるという主張で発表をしました。

発表内容の本編は下記で公開しています。

speakerdeck.com

LTを振り返って

今回、登壇者には参加者からのフィードバックをもらえる仕組みとなっていました。

  • 現在SEOに関するタスクをしているので、非常に為になった
  • 資料がとてもわかり易く、発表もハキハキとして聞き取りやすく、締めの宣言もナイスでした。
  • 単純に対策というより、悪化の要因からの仕組み作りっていうまとまりかたがよかったです

など、30を超えるフィードバックをいただけました。 発表した良かった!と思えるので素敵な仕組みですね。

今回をきっかけに外部発信の機会を増やしていこうと思います!

AWS Lambdaのhandler内外でのexitの挙動について

こんにちは。 エキサイト株式会社の三浦です。

AWSには、サーバーレスで処理を実行できる「AWS Lambda」というサービス(以降Lambda)があります。 Lambdaでは、実行の起点となるメソッド(handler)を指定することで、Lambdaの実行イベントが走ったときにそのhandlerメソッドが実行される、という流れになっています。

そのためコードの実装は基本的にはそのhandlerメソッドに書くことになるのですが、実は一部の状況ではあえてhandlerメソッドの外にコードを書くことがあります。

今回は、そういった状況で、handlerメソッドの内外で終了処理( exit )を実行したときの挙動の違いについて説明していきます。

Lambdaとは

LambdaはAWSのサービスの1つで、公式ページでは以下のように説明されています。

AWS Lambda は、サーバーレスでイベント駆動型のコンピューティングサービスであり、サーバーのプロビジョニングや管理をすることなく、事実上あらゆるタイプのアプリケーションやバックエンドサービスのコードを実行することができます。

Lambdaでは様々な言語でコードを書くことができるのですが、今回はPythonで試してみます。

言語をPythonで設定すると、以下のようなコードが自動生成されます。

import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

handlerメソッドは lambda_handler になっており、これをテスト実行してみると、以下のようなLogが出力されます。

Function Logs
START RequestId: xxxx Version: $LATEST
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1.28 ms   Billed Duration: 2 ms   Memory Size: 128 MB Max Memory Used: 39 MB  Init Duration: 160.50 ms

handlerメソッドが lambda_handler であるため、このLambdaは実行のたびに lambda_handler が実行されることになります。

そのため、実装したいコードはその lambda_handler の中に書いていけば良いのですが、実は場合によっては lambda_handler の外に処理を書いたほうがいい場合もあります。

handlerメソッドの外に処理を書いたほうがいい場合

DBとの接続のコードのサンプルでは、以下のように書かれています。

ハンドラの外部で pymysql.connect() を実行すると、関数がデータベース接続を再利用できるようになるため、パフォーマンスが向上します。

実はhandlerメソッドの外で定義したコードは、handlerメソッド内に定義したコードと異なり、Lambda実行の度に毎回実行されることはありません。

代わりに、Lambdaのコンテナ(Lambdaの実態はコンテナです)が立ち上がった時の最初の一回しか実行されず、以降はそのコンテナが終了するまで結果を保持し続けるため、例えばDBとの接続のような一回実行すれば問題ないような処理を書くにはもってこいの場所になっています。

実際にサンプルコードで試してみます。

import json

print('external printing')

def lambda_handler(event, context):
    print('internal printing')

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

このコードを実行してみると、初回実行時は以下のようなログが出力されます。

START RequestId: xxxx Version: $LATEST
external printing
internal printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1.67 ms   Billed Duration: 2 ms   Memory Size: 128 MB Max Memory Used: 39 MB  Init Duration: 139.88 ms

二回目以降は以下のようになります。

Function Logs
START RequestId: xxxx Version: $LATEST
internal printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1.21 ms  Billed Duration: 2 ms  Memory Size: 128 MB    Max Memory Used: 39 MB

初回にあった、handlerメソッドの外で定義している external printing の出力が、二回目以降は消えているのがわかります。

handlerメソッド内外でのexitの挙動

さて、突然ですが、コードを書く上で exit をしたい状況はたまに存在します。

例えば、本来なってほしくない結果が得られてしまったときなど、それ以降の処理を続行せずにそこで終わらせたいときなどです。

Lambdaでももちろん exit をすることは可能なのですが、上記の通りLambdaでのコードは、handlerメソッド内外で異なる挙動をします。 では、 exit実行時にはどのような違いがあるのでしょうか?

handlerメソッド内部での exit 実行

handlerメソッド内部で、以下のコードで exit を実行してみます。

import json
import sys

print('external printing')

def lambda_handler(event, context):
    print('internal printing')
    sys.exit(0)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

初回・二回目以降のログは以下のようになります。

初回

START RequestId: xxxx Version: $LATEST
external printing
internal printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 144.18 ms Billed Duration: 145 ms Memory Size: 128 MB Max Memory Used: 39 MB  Init Duration: 215.69 ms    
RequestId: xxxx Error: Runtime exited without providing a reason
Runtime.ExitError

二回目以降

START RequestId: xxxx Version: $LATEST
internal printing
END RequestId: xxxx
REPORT RequestId:xxxx   Duration: 104.91 ms Billed Duration: 105 ms Memory Size: 128 MB Max Memory Used: 11 MB  
RequestId: xxxx Error: Runtime exited without providing a reason
Runtime.ExitError

exit しない時と同様、二回目以降はhandlerメソッド外部の処理は呼ばれていないことがわかります。

handlerメソッド外部での exit 実行

handlerメソッド外部で、以下のコードで exit を実行してみます。

import json
import sys

print('external printing')
sys.exit(0)

def lambda_handler(event, context):
    print('internal printing')
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

初回・二回目以降のログは以下のようになります。

初回

START RequestId: xxxx Version: $LATEST
external printing
external printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1369.88 ms    Billed Duration: 1370 ms    Memory Size: 128 MB Max Memory Used: 11 MB  
RequestId: 19a1b5ac-519a-4d6e-bbaa-afee39439c57 Error: Runtime exited without providing a reason
Runtime.ExitError

二回目以降

START RequestId: xxxx Version: $LATEST
external printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1328.81 ms    Billed Duration: 1329 ms    Memory Size: 128 MB Max Memory Used: 11 MB  
RequestId: xxxx Error: Runtime exited without providing a reason
Runtime.ExitError

初回に external printing が二回出てしまっているのは謎ですが、少なくとも二回目以降でも external printing が出ている、すなわちhandlerメソッド外部の処理が実行されているのがわかります。

上記の結果から、 exit をhandlerメソッド内部で実行するときと異なり、外部で実行する時は全体的に終了されていることがわかります。

DBとの接続設定など、handler外部での処理ごと終了させたい場合はhandler外部で exit を実行し、そうではなく毎回実行したい処理だけ終了したい場合はhandler内部で exit を実行すると良いでしょう。

最後に

Lambdaは便利ですが、上記のように細かい設定も存在します。 活用していけばよりパフォーマンスを改善したりすることもできるので、ぜひ気にかけていきましょう。

SpringBootのInterceptorでテンプレートをモバイルとデスクトップでわける

エキサイト株式会社のエンジニアの佐々木です。SpringBootとThymeleafを使ってリビルドしていますが、既存テンプレートがモバイル用とデスクトップ用でわかれているので、Interceptorを使って対応します。

前提

SpringBoot x Gradleを使います。

build.gradleの依存関係は下記のようになっています。

dependencies {

        implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
        implementation 'org.springframework.boot:spring-boot-starter-web'

        implementation 'org.springframework.boot:spring-boot-starter-validation'
        implementation 'org.springframework.session:spring-session-core'
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation 'io.projectreactor:reactor-test'

}

インターセプターの実装

今回はUser-Agentを使用して、モバイルかデスクトップかをわけていこうと思います。User-Agent内にモバイルと識別できる文字列があったらモバイルのディレクトリにします。

@Slf4j
@Configuration
public class WebTemplateInterceptor implements HandlerInterceptor {

    private static final Pattern MOBILE_USER_AGENT = Pattern.compile("(iphone|android)");
    private static final String MOBILE_TEMPLATE_DIRECTORY = "mobile";
    private static final String DESKTOP_TEMPLATE_DIRECTORY = "desktop";

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        if (modelAndView == null || response.getStatus() != 200) {
            HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
            return;
        }

        String header = request.getHeader("User-Agent").toLowerCase();   // ヘッダを取得
        Matcher matcher = MOBILE_USER_AGENT.matcher(header); // User-Agentで判定

        String templateFile = String.join("/"
                , matcher.find() ? MOBILE_TEMPLATE_DIRECTORY : DESKTOP_TEMPLATE_DIRECTORY
                , modelAndView.getViewName()); // mobile と desktop をprefixとして設定する

        modelAndView.setViewName(templateFile);
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}

HandlerInterceptorインタフェースimplementsし、 必要なメソッドをoverrideして処理を追加します。インターセプターは3種類、preHandle(コントローラ処理開始前)postHandle(コントローラ処理終了後)afterCompletion(レスポンス返却後)になります。 今回は、templateのパスを変更するので、コントローラの処理が終わったあとのpostHandlerメソッドをoverrideしました。また、正常なHTTPステータスの時のみパスを変更するようにしています。エラーページについては、モバイルもデスクトップも同じテンプレートになっているのでこのように対応しています。

インターセプターを登録する

インターセプターを実装したら、DIコンテナに登録が必要になります。

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final WebTemplateInterceptor webInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(webInterceptor);
    }
}

WebMvcConfigurerインターフェースimplementsして、addInterceptorsをoverrideして、先程作成したInterceptorをDIして、登録します。これで設定完了になります。

テスト

テストしてみます。User-Agentに指定した文字列を投入すると下記のようにモバイルとデスクトップが振り分けられます。

SPの場合
$ curl http://localhost:8080/ -H 'User-Agent: iphone'
----
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>Mobile</h1>
<div><h2>common</h2></div>
</body>
</html>
----

PCの場合
$ curl http://localhost:8080/ -H 'User-Agent: sample'
----
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Sample</title>
</head>
<body>
  <h1>Desktop</h1>
  <div><h2>common</h2></div>
</body>
</html>
----

まとめ

レスポンシブが実装されていれば必要ないのですが、今回はこのような形で回避することにしています。この処理をすると、IntelliJでのThymeleafのコードジャンプが使えなくなるので、不便ですが仕方ありません。次のときはレスポンシブで作りたいところです。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

GitHubとIntelliJでmermaid記法が使えるようになりましたね!

いつものtaanatsuです。

GitHubのissueやPullRequestなどでmermaid記法が使えるようになっていますね!

f:id:taanatsu:20220304144332p:plainf:id:taanatsu:20220304144412p:plain

使い方は簡単で、コードブロックの名前をmermaidにするだけです

```mermaid
  graph TD;
      A-->B;
      A-->C;
      B-->D;
      C-->D;
```

これ、IntelliJでも使えるようです!

上記の様にプラグインの追加からもいけますが、
実はmarkdownに直書きするとレコメンドが出てきます。

f:id:taanatsu:20220304145542p:plain

レコメンドをクリックすると勝手にプラグインがインストールされるので、IntelliJを再起動すると…

f:id:taanatsu:20220304145611p:plain

あら素敵!
マークダウンファイルのプレビューにmermaid記法のものがレンダリングされました!

ということで便利機能のご紹介でした。
ではまた次回〜

【Canvaで簡単】SNS発信用テンプレを作ってみた話

こんにちは。21卒デザイナーの山﨑です。 今回は「CanvaでSNS発信用テンプレを作ってみた話」について書こうと思います。

制作したのはこちらです。著名人のインタビュー記事の宣伝なので、写真メインでスタイリッシュになるよう意識しました。

挙動は大体こんな感じになります。(容量の問題で一部しか載せられませんでした…)

さてこのアニメーションですが、実はCanvaで簡単に作ることができます。

制作方法

Canvaの左上の「パン(※通常はアニメートという名称)」をクリックすると、サイドから色々なアニメーションの挙動が出てきます。

その中から好きなアニメーションを選べば、勝手にアニメーションを作成してくれます👏

CanvaはストーリーやInstagramの記事デザインも簡単に作れるので、これからもSNSの発信クリエイティブはCanvaで制作したいと思います😃

終わりに

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があれば連絡いただければと思います🙇‍♀️

それではまた!

www.wantedly.com

Excite × iXIT TechConで「Spring Bootという強すぎるフレームワークについて」を発表しました!

こんにちは。 エキサイト株式会社の三浦です。

先日、弊社内で Excite × iXIT TechCon というカンファレンスが行われました!

tech.excite.co.jp

私は「Spring Bootという強すぎるフレームワークについて」というタイトルで発表させていただきました。

Spring Bootとは何なのか、そしてその強み・弱みは何かを説明させていただいているので、ぜひ見てみていただけると幸いです。

社内限定のカンファレンスでしたが、良い経験となりました。

今後もこういった催しがあると、更なる技術組織の活性化やレベルアップに繋がっていきそうです!

【失敗談】MyBatisでNOLOCKと同じことをやりたかった

f:id:excite-kazuki:20220228135312p:plain

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣です。

現在は、既存サービスのリビルド(PHP / BEAR.Saturday → Java / SpringBoot)を担当しています。 SpringBootでDBアクセスをするにあたってMyBatisを利用していますが、MyBatisのMapperではNOLOCKヒントに対応していないため、 簡単なSQL文であったとしてもその都度NOLOCKヒントを付与したクエリーを書く必要があります。 そこで、MyBatisを使用しつつREAD UNCOMMITTEDでSELECT文を実行する方法について考えてみました。

本記事では、上記方法と、それを導入できなかった理由について紹介します。

トランザクション分離レベルを確認する

SQL Serverでは下記を実行することでトランザクション分離レベルを確認することができます。

DBCC USEROPTIONS

現状ではREAD COMMITTEDが設定されていることが確認できました。 この状態でSELECT文を実行すると、行ロックが発生してしまう可能性があります。 そのため、行ロックを回避するためにNOLOCKヒントが付与されたクエリー(= READ UNCOMMITTEDで実行されるクエリー)が多くアプリケーションに存在します。

SQL ServerでNOLOCKヒントを付与する

SQL ServerではNOLOCKヒントを付与することで、READ UNCOMMITTEDでSELECT文を実行することができます。 クエリーは下記のように記述します。

SELECT * FROM articles  WITH(NOLOCK) WHERE article_id = 100;

しかし、MyBatisで自動生成されるMapperにはNOLOCKヒントが付与されません。 そのため、READ UNCOMMITTEEDでSELECT文を実行したいときはMapperアノテーションを使用してクエリーを記述しなくてはなりません。

@Mapper
public interface MyArticleMapper {
  @Select("SELECT * FROM articles  WITH(NOLOCK) WHERE article_id = #{articleId}")
  Optional<Article> getArticle(Long articleId);
}

クエリー実行毎にトランザクション分離レベルを設定する

SQLクエリーで表すと下記のようになります。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN TRANSACTION;

SELECT * FROM articles  WHERE article_id = 100;

COMMIT TRANSACTION;

Javaでは下記のように記述することができます。

@Override
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public Article findArticle(Long articleId) {
    return articleMapper.selectByPrimaryKey(articleId);
}

実際に上記2つを実行してみると、どちらもREAD UNCOMMITTEDでSELECT文を実行することができました。 そのため、リポジトリでクエリーを実行する箇所に@Transactionalアノテーションを付与するとよさそうです。

実行結果

1つのエンドポイントで2つのクエリーを実行している箇所の実行時間を比較した結果、下記の結果を得ることができました。

Webページでは1ページで複数のAPI呼び出しを行っているため、実際にページが表示されるのが何倍にも遅くなってしまうようになりました😨 これだけの性能差があると、クエリー実行毎にトランザクション分離レベルを設定する方法は実運用に耐えられないことがわかりました。

おわりに

いつもはうまく解決できたことを記事にしていますが、今回はうまくいかなかったことについて紹介してみました。 クエリーを実行するときにトランザクション分離レベルを設定するのはコストがかかることなんだと実感できました。 SQL ServerでREAD UNCOMMITTEDでSELECT文を実行するときは、素直にNOLOCKヒントを付与するのがよさそうです。 最後まで読んでいただき、ありがとうございました!

過去最大規模の社内カンファレンス「Excite × iXIT TechCon」を開催しました!

f:id:KAJIJI_Design:20220225103706p:plain

はじめに

エキサイトでエンジニアをしている おおしげ( @_ohshige ) です。
このたび、エキサイトHD(エキサイトとiXIT)の技術者向けの社内カンファレンス「Excite × iXIT TechCon」を開催し、大成功と言ってもいい内容となったのでご報告いたします。

これまでは勉強会やLT会といった小規模なものは開催されてきましたが、それを超える規模のものとしては初めての開催になりました。
これまでの勉強会等についてはこちらを御覧ください。

TechConの概要

開催の背景とか目的とか準備期間についてとか色々とお話する前に、まずはどのような会だったのかを軽く説明しておこうと思います。

参加者

エキサイトHDの技術職全員 (技術職以外の社員は任意参加)

開催時間

10:30-18:00

大まかな目的
  • エキサイトHD技術職の交流・横のつながり強化・技術成長
  • 外部カンファレンスに向けた練習の場
発表内容
タイトル
セッション 1 オブジェクト指向を知らないメンバーがいる中で、
クリーンアーキテクチャを目指した話
セッション 2 Docker環境がチームに浸透するまで
セッション 3 Spring Bootという強すぎるフレームワークについて
パネルディスカッション DBの設計とか運用ってどうやってる?
セッション 4 MobileAppGuildって知ってる?知らないの?!
セッション 5 宣言的UI時代のソフトウェア設計について考える
セッション 6 (非公開)
セッション 7 slackアラート攻略ガイド
セッション 8 個人的 AWS アップデートランキング
セッション 9 AWSへの移行 1年間の軌跡
LT 1 差分検出を利用したDBマイグレーション
LT 2 エキサイトブログをリビルドする
LT 3 接客態度とサービスイメージの良さの大切さ
LT 4 複数案件(環境)を抱える人向けの便利なDocker小技
LT 5 dotfilesのススメ
LT 6 (非公開)
LT 7 (非公開)
LT 8 さっくりみる、デザインの仕事
LT 9 終わらないSEOとの戦いと向き合うために
LT 10 初めてのバーチャル美肉
LT 11 FirebaseにLINE認証でアカウント情報を作成してみた

開催時間が7時間半で発表が全部で21もあり、技術職は業務として基本的に参加するということからも、規模と全社の巻き込み具合がわかるかなと思います。

開催の背景と目的

エキサイトHDでは技術組織活性のために動いているチームがあり、そこで様々な活動を行ってきました。上述した勉強会もそうですし、これまでこのブログでもいくつか紹介してきました。
今回のTechCon開催もその技術組織活性の一環であり、技術組織活性チームで作り上げたものです。

ブレストレベルの「エキサイトカンファレンスって面白そうだよね」というただただ好奇心と思いつきだけで最初の案が持ち上がりました。
定例としてゆっくり動き出したのが8月なので、約7ヶ月もの期間を使って準備を進めていたことになります。
もちろん最初のうちはブレストベースだったりでそこまで詰め詰めの会議であったわけではないですが、振り返ってみると結構長い時間をかけて準備をしていたんだなといまさら実感しています。

技術組織活性

f:id:KAJIJI_Design:20220227133337p:plain 技術組織活性の一環として始まったものなので、根幹の目的は「技術組織の活性」です。
もう少し具体的に言うと、横のつながりを強化するために他部署のエンジニアが何をしているかを知って、インプット・アウトプットすることで技術的な成長を促すということです。

横のつながりについては、エキサイト内の他部署同士という意味もありますが、エキサイトとiXITの交流という意味合いも強くあります。
2020年8月にエキサイトがiXITの株式を取得してグループ会社化し、今現在同じオフィスで働いている(コロナ禍でリモートワーク中心ではありますが)のですが、エキサイトとiXITの交流が盛んに行われているともなかなか言えない状況でした。 f:id:KAJIJI_Design:20220227133422p:plain

外部カンファレンスへの登壇の練習

そして、もう1つの目的は「外部カンファレンスへの登壇の練習」です。

弊社はこれまで様々なカンファレンスに協賛させていただいてきました。
協賛させていただくことでカンファレンスの成功に貢献できることは光栄なことではありますが、やはりエキサイトから登壇者をもっと出したい(せめてプロポーザルを出したい)という思いがあります。
とは言え、人前で発表するのはとても緊張しますし、準備も大変です。こんな発表でいいんだろうかと考えてしまったり、ネタが無いと思ってしまったり、そうして登壇まで至らないという現状です。

そこで、外部カンファレンスへの登壇の足がかりとして練習の場として、思いっきり社内を巻き込んでしまえというのがもう1つの目的です。
完全に身内だけなので外部カンファレンスほど緊張はしないですし、内容もいつもの小規模なLT会ではなく大規模なカンファレンスっぽいものとすることで外部カンファレンス登壇のハードルを越えるお手伝いを目指しました。

開催にあたって工夫した点

開催するにあたって工夫した点はたくさんありますが、基本的に上述した目的を達成できるように様々なことをやりました。

大きくわけると以下の2点に注意して、様々な工夫を凝らしました。

  • 多様性
  • 本番さながらであり練習台

多様性

技術組織活性につながるように、横のつながりをより強固なものとするために、これまでのLT会とは一線を画するように、多様性はかなり重視しました。
これまでのLT会などはどうしても決まったメンバーが発表しているような感じでしたが、この多様性をかなり意識することで、最終的に今回のカンファレンスでは様々なメンバーにスポットを浴びせることができました。

まず、登壇者の部署・職種・職歴・性別はバラバラになるようにかなり注意深く配慮しました。
最終的には、新卒1年目から技術マネージャーまで幅広く、バックエンドエンジニア・アプリエンジニア・インフラエンジニアだけでなくデザイナーや企画職まで多くの職種があり、もちろん各部署から最低1名は登壇しました。

さらに、登壇以外の方法で技術メンバーにスポットを当てるために、各発表をいくつかのブロックに分けて、それぞれのブロックに司会者を立てました。
この司会者は運営スタッフから選ぶのではなく、そのブロックの内容について把握できるメンバーでありながらも部署・職種・職歴・性別に多様性が出るようにしました。

さらには、発表内容が多様になるためには様々なプロポーザルが提出されないといけない(後述しますが、発表内容はプロポーザルを提出してもらいその中から選出する方式でした)ので、プロポーザルの提出具合をGoogleフォームとスプレッドシートとGASとSlackを使って匿名にした上で見える化し、プロポーザル提出の相乗効果を狙いました。 f:id:KAJIJI_Design:20220227133512p:plain

本番さながらであり練習台

最終的には外部カンファレンスで登壇してもらうために今回のカンファレンスはその練習台となれるように工夫しました。

そのための一番の工夫としては、空気感づくりです。
可能な限り外部カンファレンスと同じような体裁となるようにすることで、より本番に近い環境での練習になるようにしました。
そのために、発表内容はスタッフで指名して作り上げていくのではなく、プロポーザルを提出してもらいスタッフはその中から選出してタイムテーブルを作っていきました。
また、本物に近い雰囲気の醸成のために、カンファレンス専用のサイトやノベルティなど様々なものをつくりました。
f:id:KAJIJI_Design:20220225103753p:plain デザイナーさんには運営スタッフとしてがっつり入っていただき、専用サイト・ロゴ・スライドマスター・Zoom背景・Tシャツ・ステッカーなどをつくり、より本物のカンファレンスかのような仕上がりとなりました。
このような工夫をすることで、緊張感も出て良い空気感となりました。

また、本番さながらな空気感のなか何かに失敗してしまうと良くないので、失敗を極力下げるために手厚いサポートをして、さらに発表者の労力を可能な限り減らすようにしました。
上述の通り、スライドマスターやZoom背景をあらかじめ用意しておくことで、発表者には資料の内容に集中してもらうようにしました。
また、当日はウェビナーを使った開催だったのですが、画面共有やマイク・カメラに慣れ当日焦って失敗してしまわないようにするために、登壇者全員に対して事前に接続テストを実施しました。 司会者もスタッフではないメンバーなので、台本は綿密に練って事前に渡しておき、打ち合わせをすることで、進行に詰まってしまわないように注意を払いました。

さらに、せっかく忙しい合間を縫って発表の準備を進めてもらっているので、それに感謝しながらもさらに外部カンファレンスでも生かせるように、参加者にはしつこいほどに発表者へのフィードバックを促しました。
フィードバックフォームを用意し、あらゆる発表終わりにフィードバックを書いてもらうよう促し、カンファレンス終了後にそれらフィードバックをすべてお渡しするということを行いました。

開催当日

様々な準備を行ってカンファレンス当日を迎え、多少トラブルもありましたが無事に開催することができました!

カンファレンス開催の様子
カンファレンス開催の様子

エキサイトHD全体で技術職は80名ほどいるのですが、参加者は95名にも及びました。
全社に対して告知していたので企画職や営業職の方などもたくさん来ていただけたようで、技術職だけでない横のつながりの強化になったのではないかなと思います。
ウェビナーのコメント欄も常時動いており、大変盛り上がったのですが、すっかりその様子を撮影するのをわすれていたのが悔やまれます。
また、接続テストや台本を綿密に準備したため発表者も司会者にスムーズにすすめることができ、参加者からのフィードバックも発表者はとても喜んでくれました。

実は、一番良かったセッションを投票で決める「ベストスピーカー賞」、一番良かったLTを投票で決める「ベストLT賞」、たくさんフィードバックをしてくれた参加者に対する「ベストフィードバック賞」を用意しており、表彰のときはとても盛り上がりました。
ベストスピーカー賞はオンプレからAWSへの移行の軌跡を大変面白く発表してくれた『AWSへの移行 1年間の軌跡』、ベストLT賞は徹夜でバ美肉を実現し披露してくれた『初めてのバーチャル美肉』、そしてベストフィードバック賞は内定者として参加していた学生が受賞しました!
ベストフィードバック賞を内定者が受賞するとは思ってなかったのですが、本当にたくさんのフィードバックをしてくれてとても真剣に聞いてくれたということが伝わってきました。
多様性を示す良い例にもなったかなと思います。

『AWSへの移行 1年間の軌跡』の発表の様子
AWSへの移行 1年間の軌跡

『初めてのバーチャル美肉』の発表の様子
初めてのバーチャル美肉

カンファレンスの最後にアンケートをとったのですが、95.6%の参加者が満足したと回答してくれました。
また、以下のような大変ありがたいコメントもたくさんいただきました!

  • 他の部署のやっていること、知らない技術分野について知ることができてよかったです。
  • 外部登壇への挑戦のきっかけになりました!
  • 運営がとてもしっかりしていたので、発表者側で参加していて不安なく発表に集中できました。ありがとうございます!
  • 色んなジャンルの話を聞けてよかった。全員の発表のクオリティがとても高かった!運営もとてもスムーズで素晴らしかったです。
  • 次回はぜひ外部に向けた発表もしてほしいです
  • 自分がプロポーザルを出さなかったことを後悔しました

開催してみて、展望

初めての試みでしたが、総じて大成功でした!
これもすべて、質の高い発表をしてくれた発表者・パネリスト、うまく回してくれた司会者、コメントで盛り上げフィードバックもたくさんしてくれた参加者、半年かけて準備をしてくれたスタッフ、全員がしっかりと噛み合ってできたおかげかなと思います。

「多様性」で様々な種類の発表になり、「本番さながらであり練習台」を目指したために質が担保された発表になったのだと思いますし、社内限定だったからこそハードルが高くなりすぎずに日頃の成果を出せたのではないかと思います。

せっかく第1回が成功したのでぜひ続けていきたいところですが、次回はまだ未定です。
この規模で続けるのであればおそらく頻度としては年に1回が限界だと思うので、半年後の動きに期待です。
今回はコロナ禍ということもありオンラインでの開催でしたがオフラインでの開催も目指したいですし、他社様でたくさん開催されているように社内限定ではなく社外に向けた開催も考えていきます。

そして、これを機に、様々なカンファレンスに私を含めて弊社員が挑戦してくれるのではないかと思っていますし願っています。