MockHttpServletRequestでcontrollerのテストする(1)

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

Java Spring Bootでcontrollerのテストをする方法を説明します。

ユースケース

  1. controllerのテストをする。
  2. Cookieを設定する

題材

コード例

@GetMapping("cookie")
public String showCookies(
        @CookieValue(name = "naka", required = false) Cookie cookieNaka
) {
    return Optional
            .ofNullable(cookieNaka)
            .map(cookie -> cookie.getValue())
            .orElseThrow(BadRequestException::new);
}

入力例

実際のTestクラス。

@ExtendWith(MockitoExtension.class)
class TestControllerTest {
    @InjectMocks
    private TestController testController;

    private MockMvc mockMvc;

    @BeforeEach
    public void before() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(this.testController).build();
    }

    @Test
    public void getShowCookies() {
        try {
            MockHttpServletRequestBuilder getRequest = MockMvcRequestBuilders.get("/cookie");
            final String contentAsString = this.mockMvc.perform(getRequest.with(request -> {
                Cookie[] cookies = new Cookie[] {
                        new Cookie("hoge", "hoge"),
                        new Cookie("abe", "abe"),
                        new Cookie("naka", "sho")
                };
                request.setCookies(cookies);
                request.setParameter("name", "");

                return request;
            }))
                    .andDo(MockMvcResultHandlers.print())
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andReturn().getResponse().getContentAsString();

            assertEquals("sho", contentAsString);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

出力例

テスト結果のコンソール。

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /cookie
       Parameters = {name=[hogehoge]}
          Headers = [Cookie:"hoge=hoge; abe=abe; naka=sho"]
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.controller.TestController
           Method = com.controller.TestController#showCookies(Cookie)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=ISO-8859-1", Content-Length:"3"]
     Content type = text/plain;charset=ISO-8859-1
             Body = sho
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
BUILD SUCCESSFUL in 2s
13 actionable tasks: 1 executed, 12 up-to-date
23:25:25: Task execution finished ':api:test --tests "com.controller.TestControllerTest.getShowCookies"'.

条件

  • Cookieに設定する

解説

@BeforeEach
public void before() {
    this.mockMvc = MockMvcBuilders.standaloneSetup(this.testController).build();
}

apiの受け口をmockで用意します。

MockHttpServletRequestBuilder getRequest = MockMvcRequestBuilders.get("/cookie");

受け口のリクエストのmockを作成します。

final String contentAsString = this.mockMvc.perform(getRequest.with(request -> {
    Cookie[] cookies = new Cookie[] {
            new Cookie("hoge", "hoge"),
            new Cookie("abe", "abe"),
            new Cookie("naka", "sho")
    };
    request.setCookies(cookies);
    request.setParameter("name", "hogehoge");

    return request;
}))

受け口の細かい設定を追加します。 クッキーやパラメータを設定することができます。

.andDo(MockMvcResultHandlers.print())

リクエストの詳細をコンソールに出力します。

.andExpect(MockMvcResultMatchers.status().isOk())

レスポンスステータス200であることをテストします。

.andReturn().getResponse().getContentAsString();

レスポンスを文字列で受け取ります。

実際のテストについて

controllerのテストは上記をみていただけたら分かる通り、かなり設定が複雑で大変です。

プロジェクトの規模によりますがUTは行わず、ITa以降からのテストで十分確認が取れると思いますので、参考程度にみていただけると幸いです。

Javaで戻り値を多値で返したい

エキサイト株式会社メディア開発の佐々木です。

Javaは、戻り値を1つしか返せない言語です。GoやPythonは複数返すことができますが、言語的にサポートされていないので、ライブラリを使って代用します。

設定

使うライブラリは、有名なcommons-lang3になります。Gradleで依存解決を行います。

dependencies {
    ...
    implements "org.apache.commons:commons-lang3:3.10"   // 追加
    ...
}

戻り値2つ

戻り値2つには、Pairをしようします。

public Pair<Integer, String> getPair(){
      Integer id = 1;
      String name = "hogehoge";
      return Pair.of(id,name);
}

// 使用側
Pair<Integer, String> pair = getPair();
pair.getLeft();   // 左側取り出し
pair.getRight();  // 右側取り出し

Pair.of(L,R) を使用して、Pairオブジェクトを作り、返却します。利用する方は、左にセットされた値と右にセットされた値をそれぞれ取り出すメソッドが実装されているので、すぐに取り出せます。

戻り値3つ

戻り値3つには、Triple を使用します。

public Triple<Integer, Boolean, String> getTriple(){
      Integer id = 1;
      Boolean isActive = true;
      String name = "hogehoge";
      return Triple.of(id,isActive,name);
}

// 使用側
Triple<Integer, Boolean, String> triple = getTriple();
triple.getLeft();  //左側取り出し
triple.getMiddle(); //真ん中取り出し
triple.getRight(); //右側取り出し

Triple.of<L,M,R>を使用して、Tripleオブジェクトを作り、返却します。利用する方は、それぞれの値を取り出すようのメソッドが実装されているので、すぐに取り出せます。

さいごに

Pairは、Map.Entryを拡張していて、Tripleは、独自実装をしています。(PairはgetKey(), getValue()のメソッドがあります)GolangやPythonのように、変数に一気入れられませんが、事は足りますので必要に迫られれば使っていきたいと思います。

エキサイト株式会社では、長期インターン等を募集しております。まだHP等ができていませんが、wantedlyから連絡いただければ対応させていただきたいと思います。

www.wantedly.com

JavaのOptional型の利用方針

エキサイト株式会社メディア開発の佐々木です。

メンバーもJavaの開発に慣れてきて、今まで触ってきた言語にはない機能だったり仕組みだったりに格闘している日々です。今日は、Optionalについてです。

Optionalとは

Javaは、nullを許容しているレガシー言語です(笑)Optional型は、値がnullかどうかわからないものをくるんでくれる型になります。nullにオブジェクトとしてアクセスしてしまうとNullPointerExceptionが発生していますので、nullかどうかわからないものには有用です。Optionalにはいくつかのメソッドが実装されています。

データ取得系

Optionalには、取得系と処理系のメソッドが実装されています。

  • get()

値を取得します。nullの場合に例外を投げます。

Optional<String> optional = Optional.of("sample");
String s = optional.get();     // Nullだと NoSuchElementExceptionを投げます
  • orElse()

値を取得して、nullの場合に返す値も設定します。orElseの中は、nullじゃなくても実行されます。

Optional<String> optional = Optional.of("sample");
String s = optional.orElse("other");     // 必ずorElseが実行されてotherが設定されて、nullだったら設定された値が返る
  • orElseGet()

値を取得して、nullの場合に返す値も設定します。上記との違いは、nullだったときに、ラムダ式が実行されている点です。nullのときに返却する処理が重い場合は、こちらを選択する方がいいでしょう。

Optional<String> optional = Optional.of("sample");
String s = optional.orElseGet(() -> "other"); // nullだったときだけ、ラムダ式が実行され"other"が返る
  • orElseThrow

値を取得できなかったら例外を設定してthrowします。

Optional<String> optional = Optional.of("sample");
optional.orElseThrow(() -> new RuntimeException("no value"));

処理系

こちらは戻り値がない処理系になります。

  • ifPresent

値があるときだけ、処理するメソッドになります。

Optional<String> optional = Optional.of("sample");
optional.ifPresent(e -> System.out.println(e));
  • ifPresentOrElse

値があるときとなかったときの処理をそれぞれ書く必要があります。

Optional<String> optional = Optional.of("sample");
optional.ifPresentOrElse(e -> System.out.println(e), () -> System.out.println("no value"));

利用箇所

Optionalは通常の型ですので、どこでも使えます。が、使う場所は選んだほうが良いと思います。

  • メソッドの戻り値
    public Optional<String> getOptionalName(){
       // 処理
    }

ここはメソッドでOptionalが返却されるということがわかっています。なんで、Optionalが返却されるかというと、このメソッドでは、 orElse() の判断がつかないからということになります。メソッドの戻り値にOptionalを使うのは有用かなと思います。

  • メソッドの引数

Javaにはオーバーロードがあるので、それで呼び分けで良いとおもいます。わざわざOptionalの生成をおこなったりするのも面倒です。

    public Optional<String> getOptionalName(Optional<String> op){
       // 処理
    }
  • インスタンスフィールド

Optionalは、Serializableではないのでインスタンスフィールドには使用しない方がよさそうです。

class Data {
    
    ...
    Optional<String> optionalName;   // やってないけない
    ...

}
  • ローカル変数

わざわざOptionalを生成するのは手間が無駄だとおもいます。大きいメソッドの場合はあるかもですが、そもそも大きいメソッドを作るべきではありません。

おわりに

OptionalはNull許容なレガシー言語であるJavaにとっては、かなり有用な型になりますが、使いどころのメンバーと認識合わせはしておいた方がよさそうです。どこでも使えますけど、有効な使用箇所はメソッドの戻り値くらいかなと思います。

【Flutter】DatePickerの色を変更する

エキサイト株式会社のメディア事業部でモバイルアプリ開発をおこなっている、辻です。

日時や時刻の選択を行うDatePickerは、モバイルアプリやWebアプリ等、フロントエンドでは使用することの多いUIコンポーネントだと思います。 今回はそんなDatePickerをFlutterで利用する際の、色の変更方法について書いていきます。

DatePickerの使用

FlutterでDatePickerを使用するためには、showDatePicker()を、DatePickerを表示したいタイミングで呼び出します。

final selectedDate = await showDatePicker(
    context: context,
    // DatePicker表示時に出す日付
    initialDate: DateTime.now(),
    // 選択できる一番古い日付
    firstDate: DateTime.utc(2000),
    // 選択できる一番新しい日付
    lastDate: DateTime.now()
);

f:id:excite_yuchiro22:20210414194013p:plain
DatePicker表示

色の変更

DatePickerの色を変更するには、builderプロパティにThemeを適用します。

final selectedDate = await showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime.utc(2000),
    lastDate: DateTime.now(),

    // 追加
    builder: (BuildContext context, Widget child) {
        return Theme(
            data: ThemeData.light().copyWith(
                colorScheme: ColorScheme.light().copyWith(
                    // 変更したい色
                    primary: Colors.red,
                ),
             ),
             child: child,
        );
    }
);

f:id:excite_yuchiro22:20210414194102p:plain
DatePicker色変更

※ 2020年に行われたFlutterのTheme Systemのアップデートに伴い、Theme.dataの設定方法が変更されています。

Before

Theme(
    data: ThemeData.light().copyWith(
        primaryColor: Colors.red, 
     ),
     child: child,
);

After

Theme(
    data: ThemeData.light().copyWith(
        colorScheme: ColorScheme.light().copyWith(
            primary: Colors.red,
        ),
    ),
    child: child,
);

参考

DatePicker

DatePicker custom theme is not working after the latest update

Theme System Updates

おわりに

徐々に気温も暖かくなり、私は早くバイクに乗りたい気分です。

さて、エキサイトでは、一緒に働けるモバイルアプリエンジニアを募集しています! もし興味がありましたら是非こちらのリンクから、お話しましょう! www.wantedly.com

【Stream API】groupingBy と mapping でリストをマッピングしつつグルーピングする

エキサイト株式会社の西牧です。

groupingBy を使うとリストをグルーピングできますが、mapping もあわせて使うことでリストに何らかの処理をしつつグルーピングできることを新たに知ったので、その方法を紹介します。

想定ケース

以下のようなリストがあるとします。

@Value
class Row {
    long id;
    String name;
    String hobby;
}

Row row1 = new Row(1, "sato", "music");
Row row2 = new Row(1, "sato", "baseball");
Row row3 = new Row(2, "suzuki", "programming");
Row row4 = new Row(2, "suzuki", "game");
List<Row> rows = List.of(row1, row2, row3, row4);

このリストを UserDetail というクラスのリストに変換する、つまり、User ごとの Hobby リストを作るというのがやりたい処理です。

UserDetail(user=User(id=1, name=sato), hobby=[Hobby(value=music), Hobby(value=baseball)])
UserDetail(user=User(id=2, name=suzuki), hobby=[Hobby(value=programming), Hobby(value=game)])
@Value
class User {
    long id;
    String name;
}

@Value
class Hobby {
    String value;
}

@Value
class UserDetail {
    User user;
    List<Hobby> hobbies;
}

mapping を使わないコード

リストを任意のキーでグルーピングするのは、Stream API の collectgroupingBy を使うことでできます。

今回は User ごとにデータをグルーピングしたいので、User インスタンスをキーとしました。

groupingBy の第一引数がグルーピングした結果のキーです。

第二引数の LinkedHashMap::new はグルーピング後のリストの順序を保つためのものです。

第三引数でリストごとにグルーピングすることを指定しています。

本当は Hobby インスタンスのリストを作った上で、それらを User インスタンス でグルーピングしたかったのですが、その 2 つを同時に行う方法がわからず、こういう形になってしまいました。

LinkedHashMap<User, List<Row>> map = rows
        .stream()
        .collect(Collectors.groupingBy(e ->
                new User(e.getId(), e.getName()
                ), LinkedHashMap::new, Collectors.toList()));

List<UserDetail> userDetails = map.keySet()
        .stream()
        .map(e -> {
            List<Row> tmpRows = map.get(e);
            List<Hobby> hobbies = tmpRows
                    .stream()
                    .map(r -> new Hobby(r.getHobby()))
                    .collect(Collectors.toList());
            return new UserDetail(e, hobbies);
        }).collect(Collectors.toList());

mapping を使ったコード

後ほど上司の方に質問して、groupingBy の第三引数で Collectors.mapping を使うと、 「Hobby リストを作った上で、それらを User でグルーピングする」ことができるとわかり、修正したものが以下です。

Collectors.mapping(e -> new Hobby(e.getHobby()), Collectors.toList()) の部分で Hobby のリストを作っています。

LinkedHashMap<User, List<Hobby>> map = rows
        .stream()
        .collect(Collectors.groupingBy(e -> new User(e.getId(), e.getName()),
             LinkedHashMap::new,
                Collectors.mapping(e -> new Hobby(e.getHobby()), Collectors.toList())
        ));

List<UserDetail> userDetails = map.keySet()
        .stream()
        .map(e -> new UserDetail(e, map.get(e)))
        .collect(Collectors.toList());

おわりに

PHP の連想配列に慣れた身からすると、Java はリストの詰替えがやりづらいなと思っていたのですが、Stream API を使えば簡単にできました。

Stream API はまだ使いこなせていない部分もあるので色々試していきたいです。

AWS ECSにおけるログ保存コスト削減のススメ

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

クラウド化・コンテナ化が普及してきた昨今、AWS ECSでWebアプリケーションを構築する場面が増えてきているのではないでしょうか。 また、Webアプリケーションを構築する際にアクセスログやエラーログを保存しておくことは、サービスの運営のためにほぼ必須と言っても良いでしょう。

ECSでアプリケーションを構築する場合、デフォルトではCloudWatchにログを流すことになりますが、CloudWatchへのログの保存は思いの外コストが掛かってしまうものです。

そこでここでは、ログ保存のコスト削減のためにどんな方法があるのか、そしてそれぞれにどんなメリット・デメリットがあるのかを紹介していきます。

ログ保存の流れ

ログの保存は、

  1. コンテナからログを送信する
  2. ログを保存しておく

という流れになっています。 この内、

2. ログを保存しておく

については、特別な理由がなければS3に保存するということで構わないと思います。 CloudWatchに比べてS3のほうが保存コストが安く、また Amazon Athena を利用することで、SQLクエリを使ってファイルの中身を閲覧することが可能です。

CloudWatchにもデフォルトで「S3にログをエクスポートする」機能が存在するので、Lambdaの定期実行等で一定時間以上経ったログをS3にエクスポートすることができます。

今回は、

1. コンテナからログを送信する

に焦点を当てていきます。

ログ流し先を変更する方法

ECSではデフォルトでCloudWatchにログを流してくれますが、CloudWatch以外にログを流すためには一工夫必要です。

Fluent Bit をログルータとして利用することで、Fluent Bitで提供している向き先にログを流すことができるようになります。

大量のログでも大してCPUやメモリを使うことはないので、気にせず使ってよいのではないでしょうか。

ログ流し先3選

ここからは実際に私が試したログ流し先3つと、それぞれのメリット・デメリット紹介します。

CloudWatch

CloudWatchを使用するのも、必ずしも悪いわけではありません。

メリット

  • 欠損なくログを流すことができる
  • 準備がほとんど必要ない

デメリット

  • コストが掛かる
  • S3にログをエクスポートする場合、ひと手間掛かる

S3

Fluent Bitを使えば、CloudWatchを使用せず直接S3にログを流すことが可能です。

メリット

  • コストが非常に安い

デメリット

  • EFSなどの共通ストレージ等を使用しないと、コンテナの破棄のタイミングによってはログが欠損する可能性がある

Firehose -> S3

Fluent Bitから Amazon Kinesis Data Firehose にログを流し、それを今度はFirehoseからS3に流す方法です。

メリット

  • CloudWatchを使う方法に比べてコストが安い
  • 欠損なくログを流すことができる

デメリット

  • あらかじめFirehoseの準備をしておく必要がある
  • ログが大量にある場合、デフォルトのFirehoseのスペックでは不足する場合があり、その場合はAWS側に申請してスペックを上げて貰う必要がある
  • S3に直接流す方法に比べてコストがかかる

まとめ

以上から、ユースケースとして、

- ログの欠損が絶対に起きてはならない場合
  -> CloudWatchかFirehose

- 多少ならログが欠損しても良い場合
  -> S3

を選ぶのが良いかと思います。

また、今回はこの3つのみを紹介しましたが、サードパーティのアプリケーションを使用するなどログの保存方法は他にも存在します。 場合によってはそちらを検討してみても良いでしょう。

ログの保存は必須であるからこそ、可能な限りコスパよく保存していきたいものです。 特にサービスへのアクセスが増え、ログのコストが気になりだした方は、一度検証してみると良いのではないでしょうか。

Javaでメソッド参照を使うかラムダ式を使うかの判断

エキサイト株式会社 メディア開発佐々木です。

現在Spring/Javaで開発するにあたって、関数型インターフェースを呼ぶときにラムダ式でもメソッド参照でも書ける場合があります。どちらを使った方が、継続的な開発にいいのかを簡単にですがまとめてみます。

引数なしの場合

関数型インターフェースでArrayListのインスタンスを返却するだけの宣言ですが、下記の2パターンがあります。

// ラムダ式
Supplier<ArrayList<Integer>> supplierList = () -> new ArrayList<>();


// メソッド参照
Supplier<ArrayList<Integer>> supplierList = ArrayList::new;

パッと見た読みやすさでは、メソッド参照の方がカッコいいし良い選択かもしれません。 ArrayList::new と書いてあるだけなので、 new ArrayList() が入ってるんだろうなと容易に想像ができます。一方、ラムダ式では () -> のような引数がないことを明示しなくてはなりません。コードを書く側からすると面倒ですが、読む側からすると、引数がないことを明示してくれていた方が理解しやすいと思います。

引数が複数の場合

引数が複数の場合は、下記のような実装になります。

// ラムダ式
BinaryOperator<Integer> sum = (a , b) -> Integer.sum(a,b);

// メソッド参照
BinaryOperator<Integer> sum = Integer::sum;

メソッド参照では、計算されていることはわかりますが、引数に何が指定されているかはパッと見はわかりません。 あとから使う人がBinaryOperatorを知っていれば引数を2つとることはわかりますが、知らない人は調べる必要があります。片やラムダ式は、引数を2つとることが明示されています。こちらは、BinaryOperatorを知っている必要がありません。

まとめ

EffectiveJavaにはメソッド参照を使った方がいいとありますが、個人的には読む人がわかりやすく汎用的なものを選びたいと思います。メソッド参照はすっきりしていてカッコイイよく、IDEからも変換を促されるかもしれませんが、読みにくかったり等するのであれば、チーム開発という上では使わないという選択もありかなと思います。IDEのアシストをそっとオフにしたいと思います。

下記、参考にさせていただきました。 nowokay.hatenablog.com

@CookieValueでクッキーの値を取得するとき、URLエンコードされる問題の解消

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

Spring BootでCookieの値を取得する時、Controllerで

@CookieValue(name = "hoge", required = false) String hoge

で取得できると思います。

しかし、Stringで受け取る際、勝手にURLデコードされて困る場合があります。

そこで、URLデコードせずに受け取る方法を説明します。

結論から申しますと、Cookieオブジェクトで取得する方法です。

ユースケース

  1. Cookieの値を取得したい。
  2. URLデコードされたくない

題材

コード例

    @GetMapping("test")
    public String test(
            @CookieValue(name = "hoge", required = false) String hoge
    ) {
        return hoge;
    }

入力例

Cookieは以下を設定。

hoge=hogehoge%0D%0Ahogehoge; Path=/; Expires=Sun, 10 Apr 2022 01:47:34 GMT;

出力例

f:id:excite-naka-sho:20210410105422p:plain

%0D%0A→\r\nに自動的に変換されている

条件

  • URLデコードできる文字列をCookieに設定する

改善

コード例

    @GetMapping("test")
    public String test(
            @CookieValue(name = "hoge", required = false) Cookie hoge
    ) {
        return hoge.getValue();
    }

Cookieで取得し、getValue()で中身を返却

入力例

Cookieは同様な設定をする

出力例

f:id:excite-naka-sho:20210410110833p:plain

エンコードされていないことがわかる。

まとめ

URLデコードされたくないケースはそんなにないかもしれませんが、 覚えておいて損はないかと思います。

MapStructで高速なオブジェクトマッピングをする

エキサイト株式会社メディア開発の佐々木です。

以前、下記の記事でJavaのオブジェクトマッピングツールでModelMapperを紹介しました。

excitech.hatenablog.com

ModelMapperは、とてもお手軽で大変便利なのですが、やや速度に問題があり大量のデータ処理等には不向きです。そこで MapStructを紹介します。

ライブラリ追加

build.gradleに下記を追加し、ライブラリを追加します。

dependencies {
  ...
        implementation 'org.mapstruct:mapstruct:1.4.2.Final'
        annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
        annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'   // lombokを併用する場合は必要
  ...
}

コード全体

コード全体は下記のようになります。

public class DemoMain {

    public static void main(String[] args) {
        InputModel inputModel = new InputModel();
        inputModel.setId(1);
        inputModel.setName("taro");

        RequestModel requestModel = RequestMapper.INSTANCE.toRequestModel(inputModel);
        System.out.println(requestModel);

        Request2Model request2Model = RequestMapper.INSTANCE.toRequest2Model(inputModel);
        System.out.println(request2Model);
    }

    @Data
    static class InputModel {
        private Integer id;
        private String name;
    }

    @Data
    static class RequestModel {
        private Integer id;
        private String name;
    }

    @Data
    static class Request2Model {
        private Integer no;
        private String title;
    }

    @Mapper
    interface RequestMapper {
        RequestMapper INSTANCE = Mappers.getMapper(RequestMapper.class);
        RequestModel toRequestModel(InputModel inputModel);

        @Mapping(source = "name", target = "title")
        @Mapping(source = "id", target = "no")
        Request2Model toRequest2Model(InputModel inputModel);
    }
}

出力結果

出力結果は下記になります。

DemoMain.RequestModel(id=1, name=taro)
DemoMain.Request2Model(no=1, title=taro)

ざっくり解説

データクラス定義

入力のクラスはこのように定義します。

    @Data
    static class InputModel {
        private Integer id;
        private String name;
    }

出力は、プロパティ名が同じものを用意します。異なったものでも可能なので、そのケースものも用意しておきます。

    // プロパティが同じクラス定義
    @Data
    static class RequestModel {
        private Integer id;
        private String name;
    }


    // プロパティが異なったクラス
    @Data
    static class Request2Model {
        private Integer no;
        private String title;
    }

マッピング処理の定義

マッピング処理はインターフェースに記述します。

    @Mapper
    interface RequestMapper {
        // Interfaceに定数を定義する
        RequestMapper INSTANCE = Mappers.getMapper(RequestMapper.class);

        // プロパティ名が同名の場合のメソッド定義
        RequestModel toRequestModel(InputModel inputModel);

        // プロパティ名が異なった場合のメソッド定義
        @Mapping(source = "name", target = "title")
        @Mapping(source = "id", target = "no")
        Request2Model toRequest2Model(InputModel inputModel);
    }

上記のようにインターフェースにアノテーションで定義を書いていきます。プロパティ名が同名である場合は、アノテーションは不要です。プロパティ名が異なった場合は、その数のアノテーション定義を書くことになります。

RequestMapperインタフェース内にオブジェクトの変換処理がまとめられるので、とても見通しがよくなります。また、ModelMapperよりはるかに高速です。(計測している記事) 毎回インターフェースを定義する必要があるので、ModelMapperよりは少々手間がかかりますが、大量データを処理する場合は、速度的に十分リターンがあると思いますので使ってみてください。

メディア開発では、中途採用をはじめ長期インターンの募集もしております。興味があればぜひお声がけください。

www.wantedly.com

CI/CDについて

エキサイト株式会社 新規事業の開発を担当している森脇です。

エキサイトでは2、3年前からオンプレからクラウドへの移行を行っています。 移行したサービス中心にCI/CDを導入するケースが増えてきています。

私が担当している新規のサービスに関しても開発当初から導入をしています。

CI/CDとは

改めてCI/CDとは、Continuous Integration / Continuous Delivery の略で、日本語だと継続的インティグレーション/ 継続的デリバリー、意味としては自動的にテストをして本番へリリースをする、もしくはリリース可能な状態にしておくことですね

CI/CDを導入すると、バグが減ったり、変更を自動的にリリースしたりすることができとても便利です。

CI/CD構成

f:id:moriwaki111:20210402095607p:plain

GitHub ActionとAWSのCodePipelineを使ってCI/CDを構築しています。 プルリクをフックにして、GitHub Actionでlinttestを実行します、テストがOKだったものに関してコードレビューをするようにしています。 (図では省略していますが、GitHub Actionとマージの間に人為的なレビューがあります)

コードレビューでLGTMになれば、mainブランチにマージされ、AWSのCodePipelineが動いて、本番環境へデプロイされるようになっています。

よかった事

  • バグが減った事
  • 仕様に注力してコードレビューが行える事 (インデントとか使ってない変数があるとかを見なくて良くなった事)
  • 自動で網羅的にテストが行える事

課題

  • フロントのテストでは、apiの通信をmock化しており、レスポンスをjsonで保存し使用している、apiの改修があった時にjsonの更新を手動で行わないといけない

課題はありつつ、CI/CDを導入すると、自分たちも楽できるし、ユーザさんにとっても質の高いサービスを提供できるので、導入がまだ行われていないサービスは是非やる良いと思います

「連想配列」と「ドメインモデル」の違い

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

エキサイトは昔からPHPがよく使われてきましたが、特に古いコードだとその中で連想配列が頻繁に利用されています。 一方で最近ではエキサイト内でもドメイン駆動設計が考慮されることが増え、連想配列ではなくドメインモデルが利用されることが増えてきました。

ここでは、「連想配列」と「ドメインモデル」の違いはなんなのか、簡単に説明していきます。

連想配列とドメインモデルの共通点

連想配列もドメインモデルも、「1~複数のデータを格納する」という役割は共通です。 例えば「記事」のデータだと、タイトル・本文・公開日など複数のデータの複合で成り立っています。 こうしたデータについて、それぞれ別々に変数に入れるのではなく、連想配列やドメインモデルといった形でまとめて保存することで、取り扱いを容易にすることができます。

連想配列

$article = [
    'title' => '記事タイトル',
    'story' => '記事本文',
    'publishDate' => '2021-01-01 00:00:00'
];

ドメインモデル

class Article {
    private $title;
    private $story;
    private $publishDate;
    
    public function __construct($title, $story, $publishDate) {
        $this->title = $title;
        $this->story = $story;
        $this->publishDate = $publishDate;
    }

    public function getTitle() {
        return $this->title;
    }

    public function getStory() {
        return $this->story;
    }

    public function getPublishDate() {
        return $this->publishDate;
    }
}

$article = new Article('記事タイトル', '記事本文', '2021-01-01 00:00:00');

連想配列とドメインモデルの違い

では次に、連想配列とドメインモデルの違いを、それぞれの利点から見ていきます。

連想配列の利点

連想配列の利点は、何と言ってもその使いやすさかと思います。 上記の例を見て分かる通り、同じデータを入れるのでも、ドメインモデルに比べて連想配列を使うほうが圧倒的に簡単にコードを書くことができます。

ドメインモデルの利点

入りうるデータが確定している

連想配列は簡単にコードを書くことはできますが、その反面好きな時に好きなようにデータを入れることができるため、どんなデータがその連想配列に入っているかが分かりにくくなってしまっています。 連想配列を使っている場合、今どんなデータが入っているかを確認するため、要所要所で var_dump を使うという方も多いのではないでしょうか。 その点ドメインモデルは、最初にクラスを作る段階で入れるデータを決めているため、どの時点であっても入りうるデータは確定しています。

IDEの補完が効く

ドメインモデルのようにプロパティやメソッドを通してデータを取得するようにすることで、IDEが補完を効かせてくれます。 連想配列だと文字列でキーを指定することになり、どうしてもtypoや勘違いによるミスが起こりえますが、ドメインモデルで書くことによってそのリスクを大幅に減らすことができます。

ドメインルールを入れることができる

例えば「タイトルは100文字以内でなくてはならない」といったルールがあったとしましょう。 記事データがいろいろなところで使われる場合は、連想配列を使っているといろいろな場所で下記の条件を書く必要が出てくるかもしれません。

// これを、コードのいろいろな場所で書く必要があるかも
if (100 < mb_strlen($article['title'])) {
    // エラー処理を書く
}

その場合、今後「タイトルは120文字以内でなくてはならない」というルールに変わった際は、この条件が書かれているすべての部分を探し出し、修正する必要があります。 エンジニアであれば一度は体験したという方も少なくないと思いますが、これは非常に大変な作業です。

一方ドメインモデルを使うと、モデルそのものにルールを書くことができます。


class Article {
    private $title;
    private $story;
    private $publishDate;
    
    public function __construct($title, $story, $publishDate) {

        // Articleモデルを作成するときにのみチェックすれば良い
        if (100 < mb_strlen($title)) {
            // エラー処理を書く
        }

        $this->title = $title;
        $this->story = $story;
        $this->publishDate = $publishDate;
    }

    public function getTitle() {
        return $this->title;
    }

    public function getStory() {
        return $this->story;
    }

    public function getPublishDate() {
        return $this->publishDate;
    }
}

このように、ドメインモデル生成時に必ずチェックするようにすれば、ルールを書く部分が1箇所だけで済み、仮にルールが変更になっても修正が非常に容易です。

最後に

以上のことから、多少最初書くのが面倒であったとしても、ドメインモデルを使うようにしたほうが利点が大きいのではないでしょうか。 またこれは、ドメイン駆動設計の概念でもあります。 今後新しく連想配列を使う機会があったら、ぜひドメインモデルで書いてみることをおすすめします。

DBのテーブル構造のアンチパターンと改善

みなさんこんにちは。 エキサイトでエンジニアをしているAです。

エキサイト内で過去に一部テーブル構造の見直しを行い、運用コストの効率化を行ったため今回はその一例をご紹介いたします。

最初にテーブル構造からしっかり考える

最初のテーブル構造は非常に大事です。今回は以下の3点を特に重視していきました。

  • テーブル構成は要件に合わせて基本的に細かく分ける
  • 後々拡張しやすい造りにする
  • テーブルを見るだけでなんのデータか把握しやすい形にする

上記は当たり前のことですが、ここを疎かにしてしまうと後々何の為に使うデータか分からなくなったり、データが増えてしまいデータの更新処理に時間がかかったりします。

後付けで謎テーブルを謎ロジックでJOINしたりで、とんでもなく複雑な運用になってしまいがちです。

テーブル定義は時間をかけて行っていったかどうかで運用の手間が全然変わります。

見切り発車で決めるのだけは絶対にやめましょう。

テーブル例

`example_table` (
id,
name,
profile_json,
snsA_profile_json,
snsB_profile_json,
url_information_json,
TypeA_json,
TypeB_json,
TypeC_json,
...
...
...
active_flag,
delete_flag)

実際に存在していたテーブルの一例です。 この場合、カラムが非常に長いため一つのテーブルに大量のカラムを作成せず、分けられる箇所は分けるようにしていきます。

問題点1 データの更新に無駄に手間がかかる

恐ろしい事にこのケースだとデータをSQLに投げる際に、jsonをカラムにそのまま突っ込んでいます。

そのため、既に登録したデータの中身の情報を変えたい時にデータを投げ直す事になり迂闊にデータを変えることができなくなります。

例えばID1のTypeAの情報を一部変更したいなどの要件があった場合、以下の操作をするしか変える手段はありません。

  • jsonの中身を把握してUPDATEをかける
  • データを再度入れ直して上書きする

この場合TypeAの情報を一部分変えるだけでも苦労します。これだけでもう地獄です。

解決策

今回はTypeAはTypeA_json用のデータをまとめたTypeAテーブルなど分けるように変更を加えました。

その後DBから受け取るAPI側でjsonに変換して送るようにする事で、TypeAJsonカラムに影響されることはなく TypeAの情報は単純なUPDATE文一つで更新できるようになりました。

問題点2 状態をカラムを持っている

今回のテーブルにはactive_flagというものが存在します。 active_flagとみたら誰もが「表示、非表示の状態を持つカラムなんだな」と思うかもしれません。

実際にこのテーブルのコメントには「アクティブフラグ」とだけ書いてありました。

パッと見0や1が書いてあるので1が表示で0が非表示なんだなーとなんとなくわかりそうです。

しばらくデータを眺めていると1や0の中に-1や-128という知らない数字が見えました。

どうやらこの表示フラグ、表示のためのフラグなのに4つのカオスな状態を持っているようです。 実際に使われていた箇所を追っていくと、

  • データが入ってきたばかりの状態 => 0
  • 管理画面で公開許可された状態 => 1
  • 管理画面で非許可にされた状態 => -1
  • 削除予定の状態 => -128

と言った形で使われていることがわかりました。

実装当時の状況はわかりませんが、恐らくはじめは1,0で表示管理されていた物が表示要件が新たに出てきたので付け足されていったのだと推測しました。

解決策

結局のところ0,1,-1,-128が要らないので、いっそ分けた方がやりやすいです。

active_flag自体はカーディナリティが低いので、example_tableのactive_flagで状態管理するのをやめました。

activeな状態のIDを下記のようにactiveテーブルに入れることで分かり辛さが解決します。

`example_active` (
id
)

activeにactiveな情報を持つIDだけ入れておけば、テーブルを見るだけでどれが今activeなのかは自明です。

さらに後から追加要件で変な状態を持たせられることもなくなります。

ちなみにアクティブかどうかと、非許可かどうか、削除するかどうかは要件がそもそも別なのでテーブルを分けるべきです。

どうしても値で状態を持たなければならなくなった場合は、当たり前の話ではありますがせめてどの値がどれを示すかのコメントはしっかり残しましょう。

その他、紹介し切れていない改善点などはまだまだありますが ご覧の方々には、最初のテーブル定義にはしっかり時間をかけるという認識を持っていただけたら、後々の運用もやりやすくなる筈なのでこれを機にテーブル構造には時間をかけて考えていただく切っ掛けになれば幸いです。

SQL Serverのdockerコンテナにバックアップ復元する方法(2020)

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

SQL Serverをローカル環境に用意するのにハマったことを記載します。

ユースケース

  1. SQL Serverを使ったローカル開発をしたい。
  2. test環境のデータを、ローカル環境に復元したい。

題材

docs.microsoft.com

1のみの場合、手順通りにやればSQL Serverを使ったローカル開発をすることできます。

しかし、2のtest環境のデータをローカル環境に復元するとき、エラーは出ることがあります。

github.com

理由は、docker-composeのvolumes mountの際、rootユーザになってしまうからです。

それを回避するためにdocker volumeを使いましょう。

docker volume --rm で明示的に削除しない限り、消えることはありません。

以下に、docker-compose.yamlの記載例を表示します。

入力例

docker-compose -f docker-compose-sqlserver.yml up -d

出力例

mcr.microsoft.com/mssql/server:2017-latest   "/opt/mssql/bin/nonr…"   4 days ago   Up 47 hours   0.0.0.0:1433->1433/tcp   tool_sqlserver_1

条件

  • testデータは、既存のtest用DBサーバーからエクスポートする

コード例

version: "3.7"

services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2017-latest
    ports:
      - 1433:1433
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "abc%ABC%123"
    volumes:
      - "sqlserver-data:/var/opt/mssql/data"
      - "sqlserver-log:/var/opt/mssql/log"
      - "sqlserver-secrets:/var/opt/mssql/secrets"
volumes:
  sqlserver-data:
    driver: local
  sqlserver-log:
    driver: local
  sqlserver-secrets:
    driver: local

Lambda + Goでネストされたアプリケーションを構築

f:id:moriwaki111:20210323212504p:plain

エキサイト株式会社で新規事業の開発を行っている森脇です。

新規事業ではawsを使いシステムの構築を行っています。 当初の計画ではAPIをLambdaで作成する予定になっており開発を進めておりましたが、 幾つか課題が出てきてしまい、その時の話を書こうと思います。

言語はGoを採用し、最初は小規模の認識で開発を行っていたが、仕様がどんどん膨らみ気づいた時にはエンドポイントが150を超え、そしてやってきたリソース制限

Template format error: Number of resources, 206, is greater than maximum allowed, 200

ネストさせることで回避できることを知り、ネストするもsam buildできず、結局ネストされた各アプリケションを個別にビルドし、マージするシェルを書いて運用していました。 苦労した話を書こうと思って調べていると、いつの間にかネストされたアプリケーションのビルドができるようになっていた!!!

f:id:moriwaki111:20210323210711p:plain

前段の話が長くなりましたが、Lambdaでネストされたアプリケーションを簡単に構築できる話です

構築

2つのアプリケーションにネストさせる例です

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  lambda-go
  Sample SAM Template for lambda-go

Parameters:
  Region:
    Type: String
    Default: ap-northeast-1
  Stage:
    Type: String
    Default: Dev
  ApiDomainName:
    Type: String
    Default: api.example.com

Resources:
  # Lambda Application
  App1Application:
    Type: AWS::Serverless::Application
    Properties:
      Location: app1.yaml
      Parameters:
        Region: !Ref Region
        Stage: !Ref Stage
        ApiDomainName: !Ref ApiDomainName
  App2Application:
    Type: AWS::Serverless::Application
    Properties:
      Location: app2.yaml
      Parameters:
        Region: !Ref Region
        Stage: !Ref Stage
        ApiDomainName: !Ref ApiDomainName

アプリケーション1

app1.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  lambda-go
  Sample SAM Template for lambda-go
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 10
    Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
      Variables:
        REGION: !Ref Region

Parameters:
  Stage:
    Type: String
  Region:
    Type: String
  ApiDomainName:
    Type: String

Resources:
  # ロール
  App1Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: app1-role
      Policies:
        - PolicyName: app1-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateNetworkInterface
                  - ec2:DescribeNetworkInterfaces
                  - ec2:DetachNetworkInterface
                  - ec2:DeleteNetworkInterface
                Resource: '*'
      AssumeRolePolicyDocument: {
        Version: "2012-10-17",
        Statement: [
          {
            Effect: "Allow",
            Principal: {
              Service: [
                  "lambda.amazonaws.com"
              ]
            },
            Action: [
                "sts:AssumeRole"
            ]
          }
        ]
      }
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /

  # Api Gateway
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage

  # Base path mapping
  ApiGatewayBasePathMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
      DomainName: !Ref ApiDomainName
      RestApiId: !Ref ApiGateway
      BasePath: app1
      Stage: !Ref ApiGateway.Stage

  # Lambda Function
  Test1Function:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ../../test1/
      Handler: test1
      Runtime: go1.x
      Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
      Role: !GetAtt App1Role.Arn
      Events:
        GET:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /test1
            Method: GET
  Test2Function:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ../../test2/
      Handler: test2
      Runtime: go1.x
      Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
      Role: !GetAtt App1Role.Arn
      Events:
        GET:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /test2
            Method: GET
  Test3Function:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ../../test3/
      Handler: test3
      Runtime: go1.x
      Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
      Role: !GetAtt App1Role.Arn
      Events:
        GET:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /test3
            Method: GET

アプリケーション2

app2.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  lambda-go
  Sample SAM Template for lambda-go
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 10
    Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
      Variables:
        REGION: !Ref Region

Parameters:
  Stage:
    Type: String
  Region:
    Type: String
  ApiDomainName:
    Type: String

Resources:
  # ロール
  App2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: app2-role
      Policies:
        - PolicyName: app2-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateNetworkInterface
                  - ec2:DescribeNetworkInterfaces
                  - ec2:DetachNetworkInterface
                  - ec2:DeleteNetworkInterface
                Resource: '*'
      AssumeRolePolicyDocument: {
        Version: "2012-10-17",
        Statement: [
          {
            Effect: "Allow",
            Principal: {
              Service: [
                  "lambda.amazonaws.com"
              ]
            },
            Action: [
                "sts:AssumeRole"
            ]
          }
        ]
      }
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /

  # Api Gateway
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Stage

  # Base path mapping
  ApiGatewayBasePathMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
      DomainName: !Ref ApiDomainName
      RestApiId: !Ref ApiGateway
      BasePath: app2
      Stage: !Ref ApiGateway.Stage

  # Lambda Function
  Test10Function:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ../../test10/
      Handler: test10
      Runtime: go1.x
      Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
      Role: !GetAtt App2Role.Arn
      Events:
        GET:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /test10
            Method: GET
  Test11Function:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ../../test11/
      Handler: test11
      Runtime: go1.x
      Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
      Role: !GetAtt App2Role.Arn
      Events:
        GET:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /test11
            Method: GET
  Test12Function:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ../../test12/
      Handler: test12
      Runtime: go1.x
      Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
      Role: !GetAtt App2Role.Arn
      Events:
        GET:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            RestApiId: !Ref ApiGateway
            Path: /test12
            Method: GET

ネスト以外のポイントとして、API GatewayのAPI マッピングを使ってネストされたアプリケーションをマッピングしています。

  ApiGatewayBasePathMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
      DomainName: !Ref ApiDomainName
      RestApiId: !Ref ApiGateway
      BasePath: app2
      Stage: !Ref ApiGateway.Stage

これをすると、template.yamlで指定している、ApiDomainName(api.example.com)で各ネストされたアプリケーションにアクセスできるようになる 例えば、https://api.example.com/app1/test1 とか https://api.example.com/app2/test10 など *実際に試して頂く場合はApiDomainNameを変更して頂く必要があります

デプロイ

デプロイ時にはcapabilitiesCAPABILITY_AUTO_EXPANDを指定しデプロイをおこないます、成功するとCloudFormationでネストされたアプリケーションが表示されます

f:id:moriwaki111:20210323210818j:plain

ソース

参考までにソースを公開します

https://github.com/akihiro-moriwaki/lambda-nest-go

エキサイトに新卒入社するまでの6ヶ月間のインターンで学んだこと

はじめに

エキサイト株式会社 21年度新卒 山縣と申します。 新卒入社までの6ヶ月間を、内定者としてインターンをしてきました。

本記事では、これまでに学んできたことや、感じてきたことなどを中心にまとめていきます。

21卒のデザイナーが作成した素敵なロゴ

インターン前の技術力

インターンを始める前までは、大学での講義を除き、個人でプログラミングを学んでいました。 当時は、Qiitaの記事やはてなブックマークの記事を見たりしながら、動くものを作ったり、書籍を買ったりしていました。 下記にインターン前の技術力をまとめたとおり、特段強い技術力は持ち合わせていなかったです。

  • Java:学部の講義で1年程触った後、演習科目でAndroidアプリを作成
  • Python:3年程使用しており、少しだけ書ける
  • Go:趣味で少しだけ書いていた
  • JavaScript:Nuxt.jsでのみ使用
  • Nuxt.js:趣味でヘッドレスCMSを使用した技術ブログの作成
  • Git:基本的な使い方がわかる
  • チーム開発経験:なし (他社の短期インターンで雰囲気を掴んだ程度)

Javaの勉強

CMSの再開発をSpringBoot / Javaで行うことが決定された後、SpringBoot / Javaの勉強を始めることになりました。 社内では、これまでPHPで開発が行われてきたため、SpringBoot / Javaの知見が溜まっていませんでした。 そこで、自身の学習と並行して、SpringBoot / Javaの学んだことを社内ドキュメントにまとめていきました。 社内ドキュメントにまとめたものの一例を下記にまとめます。

  • Stream API
  • 関数型インタフェース
  • バリデーション
  • Spring AOP

Streamの簡単な使い方についてまとめたドキュメントの一部です。

社内ドキュメントの例

CMSの再開発

CMSは、フロントエンドをNuxt.js / TypeScriptで開発し、バックエンドをSpringBoot / Javaで開発しています。 フロントエンドの開発とバックエンドの開発を行き来するため、JavaのコードをTypeScriptに書いてエラーが起きてしまい、「あれれ?」となったことも多かったです。

CMSの再開発では、与えられた仕様をもとにコードを書くのではなく、自分の頭を使ってコードを書くことがほとんどでした。 頑張って書いたコードが褒められた時は嬉しかったですし、よりよいコードの書き方を教えていただいた時は「こんな書き方が!」と感動することも多かったです。

また、1つのPRに対して、119件のやり取りが行われたこともあり、熱心にコードレビューをしていただいた社員の方々には感謝の気持でいっぱいです。

1つのPRに119件のコメント

既存のシステムのリビルド

現在は、既存のPHPで書かれたBEAR.Saturdayのコードをもとに、SpringBoot / JavaAPIのリビルドを進めています。 これまでに取り組んできたことと比較すると格段に難易度が上がっており、「仕様がわからない」「処理の内容がよくわからない」といったことが多く、とても大変だと感じています。 つらいことも多いですが、アンチパターンとして受け止めて、今後のソフトウェア開発に活かしていけたらと考えています。

おわりに

以上のように、インターンではJavaの勉強や、CMSの再開発、既存のシステムのリビルドに取り組んで来ました。 6ヶ月間のインターンを通じて、実際にソフトウェア開発の現場で働くことで、チーム開発の取り組み方や、今後どのように働いていくのかのイメージを掴むことができました。 インターン生としての就業は終わり、明日4月1日からは新卒として、働いていくこととなります。

エキサイトでは、自社サービス開発に携わることができます。 エキサイトに興味をもってくださった方は、下記リンクよりお願いします!

www.wantedly.com