オンプレからAWSリソースへ安全にアクセスする

こちらは エキサイトホールディングス Advent Calendar 2021 22日目の記事です。

qiita.com

はじめに

オンプレとクラウドのハイブリッド環境でシステムを運用していると、外部アプリケーションからAWSリソースを操作することが多々あります。脳死で「IAMユーザを作って、クレデンシャル情報を生成して・・・」となりがちだったので、どのような認証手段があるのかを整理しました。

署名バージョン4

IAMユーザのアクセスキー(クレデンシャル情報)を使った認証です。AWS SDKを使えばアプリケーション側も手っ取り早く実装できるので、使っている方も多いはずです。

Signing AWS API requests - AWS Identity and Access Management

が、有効期限の長いクレデンシャル情報を使うことになるので、キー漏洩による不正利用などが問題になりがちです。そのため、クレデンシャル情報のローテーションなどを考える必要があったりします。

セキュリティは軽視できませんし、なるべくならクレデンシャル情報の保存は避けたいところです。

SAMLOpenID Connector

使い回せるような半永続的なクレデンシャル情報ではなく、一時的に都度作成されるものであればリスクを低減できそうです。そこでSAMLOpenID Connector(OIDC)の出番です。GitHub ActionsがOIDCをサポートしたのは記憶に新しいですね。おかげでIAMユーザを作成せずとも、GitHub ActionsのワークフローからAWSリソースを操作できるようになったわけです。

Configuring OpenID Connect in Amazon Web Services - GitHub Docs

大体はこれで解決できそうですが、IdPがSAMLやOIDCに対応していなかったり、そもそもIdPが存在しない場合には新たに構築が必要になります。当然のことながら、SAMLやOIDCの認証プロセスの実装や運用も必要なので、一から作っていくのは骨が折れそうです。

MutualTLS

その他にも、MutualTLS(mTLS)が使えると思います。相互TLS認証とかクライアント認証などとも呼ばれていますね。AWSではAPI GatewayAWS IoT CoreなどでmTLSを設定できます。古くからある認証手段ですので、アプリケーション側も容易に実装できます。

Configuring mutual TLS authentication for an HTTP API - Amazon API Gateway

Client authentication - AWS IoT Core

ただし、証明書のライフサイクル管理が必須、という課題は残ります。また、API GWでは証明書が失効したかどうかについては検証しない*1ため、別途Lambdaオーソライザーなどを挟む必要があります。

おわりに

どれもメリット・デメリットがあります。

手法 メリット デメリット
署名バージョン4 手軽に実装できる クレデンシャル情報が漏洩したときのリスクが大きい
SAML / OIDC 一時的なクレデンシャル情報を用いるのでセキュリティ的に安心 SAMLアサーションやアクセストークンのライフサイクル管理、IdPの管理などが必須
MutualTLS レガシーなシステムでも実装が比較的容易 証明書のライフサイクル管理が手間になる

今回に限った話ではないですが、アプリケーションとの相性やビジネス的な優先度などから使い分けていけると良いかなと思います。

エキサイトではエンジニアを募集しています。募集情報は以下から。

www.wantedly.com

JacksonのObjectMapperがかなり優秀だった話

はじめに

はじめまして。エキサイトでインターンをさせていただいている岡﨑です。 アドベントカレンダーの22日目を担当させていただいています。 今日はJacksonのObjectMapperがとてもとても便利だった話をさせていただきます。

Object Mapperとは

それでははじめに言葉の定義からです。 Object Mapperとはそもそも何なのか、この機会に調べてみました。 まずは公式ドキュメントからです。

ObjectMapperは、基本的なPOJO(Plain Old Java Objects)との間、または汎用JSONツリーモデル(JsonNode)との間でJSONを読み書きするための機能と、変換を実行するための関連機能を提供します。また、さまざまなスタイルのJSONコンテンツを処理し、ポリモーフィズムやオブジェクトIDなどのより高度なオブジェクトの概念をサポートするように高度にカスタマイズできます。 公式ドキュメントより

簡単に噛み砕くと、 JacksonのObjectMapperはJavaのObjectとJson間の変換を簡単にしてくれるクラスです。

どれくらい簡単になるの?

それでは実際にやってみましょう。 JSONだとこんな感じです。

{
    "sample_id": 1,
    "sub_list1": [
        {
            "sub_id": 11,
            "sub2_list": [
                {
                    "sub2_id": 111,
                    "answer": "test"
                },
                {
                    "sub2_id": 121
                }
            ]
        },
        {
            "sub_id": 12,
            "sub2_list": [
                {
                    "sub2_id": 211,
                    "answer": "test2"
                },
                {
                    "sub2_id": 212,
                    "answer": "test3"
                }
            ]
        }
    ]
}

これを受け取り、モデルに変換していきたいと思います。

@Slf4j
@Value
public class SampleForm {
    @NotNull
    Sample samle;

     @Value
    public static class Sub2 {
        @NotNull
        Integer subId2;

        @NotNull
        String answer;

        @JsonCreator
        public Sub2 (
            JsonProperty("sub_id2") Integer subId2; 
            @JsonProperty("answer") String answer; 
        ) {
            this.subId2 = subId2;
            this.answer = Optional.ofNullable(answer).orElse("");
        }
    }

    @Value
    public static class Sub1 {
        @NotNull
        Integer subId;

        @NotNull
        List<Sub2> sub2List;

        @JsonCreator
        public Sub1 (
            JsonProperty("sub_id") Integer subId; 
            @JsonProperty("sub2_list") List<Sub2> sub2List; 
        ) {
            this.subId = subId;
            this.sub2List = sub2List;
        }
    }

    @Value
    public static class Sample {
        @NotNull
        Integer sampleId;

        @NotNull
        List<Sub1> subList1;

        @JsonCreator
        public Sample (
            @JsonProperty("sample_id") Integer sampleId,
            @JsonProperty("sub_list1") List<Sub1> subList1
        ) {
            this.sampleId = sampleId; 
            this.subList1 = subList1;
        }
    }

    @ConstructorProperties({
        "sample"
    })
    public SampleForm(
        Sample sample
    ) {
        this.sample = sample;
    }
}

こんな感じでできたらいいなと思いますよね。 しかし、これでは動きません。JSONのデータからそのままモデルに変換することはできないから当然です。 この場合はSampleがnullになって落ちます。

それでは、次のコードを書き換えていきましょう。

    @ConstructorProperties({
        "sample"
    })
    public SampleForm(
        Sample sample
    ) {
        this.sample = sample;
    }

ここがモデルで受け取ることはできないので、Sampleクラスで受け取っているところをStringで受けとってあげます。 そして、Sampleクラスに変換します。

さて、本題です。 ここで長々とコードを書かずに、数行で全て変換してくれるObjectMapperの登場です。

    @ConstructorProperties({
        "sample"
    })
    public SampleForm(
        String sample
    ) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            this.sample = mapper.readValue(sample, new TypeReference<>() {});
        } catch(Excepting exceptin) {
            new Exception("モデルに変換できませんでした");
        }
    }

ObjectMapper mapper = new ObjectMapper();

this.sample = mapper.readValue(sample, new TypeReference<>() {});

この二行を使うだけで、意図している通りにモデルを変換することができます。 とても便利ですね。

終わりに

Object Mapperがここまでできることを知らないで実装すると、とてもごちゃごちゃとしたやばいコードを書くことになります。 それを防ぐためにも、こうして使えるものは正しく使っていくことが大切だと思いました。

アドベントカレンダーも終盤ですが、最後まで楽しんでいただけると幸いです。

qiita.com

quarkusを使う(コンパイル編)

こんばんは、エキサイト株式会社の中尾です。

quarkusといえば、native compileでしょう。 まずはjvmで。

shogo.nakao@localhost:(main*) $ docker build -f src/main/docker/Dockerfile.jvm -t quarkus/hobby-jvm . 
[+] Building 100.3s (11/11) FINISHED
 => [internal] load build definition from Dockerfile.jvm                                                                                                                                                                                                                   0.0s
 => => transferring dockerfile: 2.15kB                                                                                                                                                                                                                                     0.0s
 => [internal] load .dockerignore                                                                                                                                                                                                                                          0.0s
 => => transferring context: 111B                                                                                                                                                                                                                                          0.0s
 => [internal] load metadata for registry.access.redhat.com/ubi8/ubi-minimal:8.4                                                                                                                                                                                           7.9s
 => [1/6] FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4@sha256:c536d4c63253318fdfc1db499f8f4bb0881db7fbd6f3d1554b4d54c812f85cc7                                                                                                                                    14.1s
 => => resolve registry.access.redhat.com/ubi8/ubi-minimal:8.4@sha256:c536d4c63253318fdfc1db499f8f4bb0881db7fbd6f3d1554b4d54c812f85cc7                                                                                                                                     0.0s
 => => sha256:d46336f50433ab27336fad8f9b251b2f68a66d376c902dfca23a6851acae502c 39.29MB / 39.29MB                                                                                                                                                                          12.5s
 => => sha256:be961ec6866344c06fe85e53011321da508bc495513bb75a45fc41f6182921b6 1.74kB / 1.74kB                                                                                                                                                                             3.5s
 => => sha256:c536d4c63253318fdfc1db499f8f4bb0881db7fbd6f3d1554b4d54c812f85cc7 1.47kB / 1.47kB                                                                                                                                                                             0.0s
 => => sha256:4d6547bbb7c5fe0f0d37b491c0c1975ff96bf4cf1c26dd665e3a5d70918b5564 737B / 737B                                                                                                                                                                                 0.0s
 => => sha256:e7685639f55280ffe8da3724cb1d342091a1ceea79af5eb91a07ce97cd32f221 4.28kB / 4.28kB                                                                                                                                                                             0.0s
 => => extracting sha256:d46336f50433ab27336fad8f9b251b2f68a66d376c902dfca23a6851acae502c                                                                                                                                                                                  1.4s
 => => extracting sha256:be961ec6866344c06fe85e53011321da508bc495513bb75a45fc41f6182921b6                                                                                                                                                                                  0.0s
 => [internal] load build context                                                                                                                                                                                                                                          0.3s
 => => transferring context: 22.66MB                                                                                                                                                                                                                                       0.3s
 => [2/6] RUN microdnf install curl ca-certificates java-17-openjdk-headless     && microdnf update     && microdnf clean all     && mkdir /deployments     && chown 1001 /deployments     && chmod "g+rwX" /deployments     && chown 1001:root /deployments     && curl  75.7s
 => [3/6] COPY --chown=1001 build/quarkus-app/lib/ /deployments/lib/                                                                                                                                                                                                       0.1s
 => [4/6] COPY --chown=1001 build/quarkus-app/*.jar /deployments/                                                                                                                                                                                                          0.0s
 => [5/6] COPY --chown=1001 build/quarkus-app/app/ /deployments/app/                                                                                                                                                                                                       0.0s
 => [6/6] COPY --chown=1001 build/quarkus-app/quarkus/ /deployments/quarkus/                                                                                                                                                                                               0.0s
 => exporting to image                                                                                                                                                                                                                                                     2.4s
 => => exporting layers                                                                                                                                                                                                                                                    2.4s
 => => writing image sha256:7a4f731341a6d3c9b04e1ffe98783be021280fe109fea48425fe49cb983cf860                                                                                                                                                                               0.0s
 => => naming to docker.io/quarkus/hobby-jvm                                                                                                                                                                                                                               0.0s```
shogo.nakao@localhost:(main*) $ docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/hobby-jvm
exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/quarkus-run.jar
Listening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-12-20 14:04:51,601 INFO  [io.quarkus] (main) hobby v1.0.0 on JVM (powered by Quarkus 2.5.1.Final) started in 1.055s. Listening on: http://0.0.0.0:8080
2021-12-20 14:04:51,602 INFO  [io.quarkus] (main) Profile prod activated.
2021-12-20 14:04:51,603 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-validator, jdbc-mysql, mybatis, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]
^C2021-12-20 14:05:17,653 INFO  [io.quarkus] (Shutdown thread) hobby stopped in 0.036s
shogo.nakao@localhost:(main*) $ docker run -i --rm -p 8080:8080 quarkus/hobby-jvm
exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/quarkus-run.jar
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-12-20 14:05:21,306 INFO  [io.quarkus] (main) hobby v1.0.0 on JVM (powered by Quarkus 2.5.1.Final) started in 0.982s. Listening on: http://0.0.0.0:8080
2021-12-20 14:05:21,307 INFO  [io.quarkus] (main) Profile prod activated.
2021-12-20 14:05:21,307 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-validator, jdbc-mysql, mybatis, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]

debug on で 1.055s,debug offで0.982sです。もちろんサイズが小さいこともありますが、、、早すぎじゃないですか?

続いてnativeです。初回でエラーが出る方は、sdk man でgraalVMを入れて下さい。

Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image`

そして待ちに待ったnative compile開始!!

...

......

.........

compileできなかった。。。

なんと、、、recordクラスに対応していなかった、mybatisも対応していなかった。。

どうやらissuesで21.3.0.r17でrecordクラスどうにかして欲しいみたいなのを見つけた。

https://github.com/quarkusio/quarkus/issues/20891

くっそう。。。悲しい、、、

できた瞬間、速攻ためそう。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

Spring Bootで、Webアクセスのパラメータをクラスで受け取るときの注意点

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

Spring BootでAPIなどを作る時は、Webからのアクセスを受け取ることになります。 そのアクセスにクエリパラメータ等でパラメータが付いている場合、そのパラメータを何かしらの方法で受け取る必要があります。

今回は、クラスを使ってパラメータを受け取るときに、予期しないデータを受け取ってしまう場合がある点について説明していきます。

クラスでパラメータを受け取る方法

例えば、以下のアクセスを受け付けるとします。

http://localhost/sample?test1=aaa&test2=bbb

その場合、以下のコードで受け取ることができます。

import lombok.Value;

import java.beans.ConstructorProperties;

@Value
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;
        this.test2 = test2;
    }
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class SampleController {
    @GetMapping("sample")
    public String sample(
            @ModelAttribute SampleModel sampleModel
    ) {
        return "OK";
    }
}

実際にデバッグモードで実行してみると、SampleModelに以下のデータが入っていることがわかります。

SampleModel(test1=aaa, test2=bbb)

上記のコードであれば問題はありませんが、実は特定のパターンだと想定外のデータが入ってしまうことがあります。

想定外のデータが入ってしまうパターン

アクセスは同じく

http://localhost/sample?test1=aaa&test2=bbb

とし、受け取りクラスのコードを少し変えてみます。

import lombok.Data;

import java.beans.ConstructorProperties;

// ValueではなくDataで受け取る(各プロパティにSetterが付く)
@Data
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;

        // test2は、一部文字列を変更してからプロパティに代入する
        this.test2 = test2 + "ccc";
    }
}

こちらは同じコードです。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class SampleController {
    @GetMapping("sample")
    public String sample(
            @ModelAttribute SampleModel sampleModel
    ) {
        return "OK";
    }
}

本来は

SampleModel(test1=aaa, test2=bbbccc)

こうなってほしいはずですが、実際は

SampleModel(test1=aaa, test2=bbb)

こうなってしまいます。

どうやら、「 Setter が存在し、かつアクセスのパラメータと同じ名前のプロパティ」があると、コンストラクタでプロパティに値を代入した後に、直接アクセスのパラメータの値をプロパティに代入してしまい、結果として上書きされてしまうようです。

このような場合、 Setter をつけないようにすることで解決できます。

import lombok.Value;

import java.beans.ConstructorProperties;

// SetterがつかないValueを使用する
@Value
public class SampleModel {
    String test1;
    String test2;

    @ConstructorProperties({"test1", "test2"})
    public SampleModel(String test1, String test2) {
        this.test1 = test1;
        this.test2 = test2 + "ccc";
    }
}

このコードであれば、想定通り以下のデータとなります。

SampleModel(test1=aaa, test2=bbbccc)

最後に

今回はシンプルな例でしたが、複雑になってくると見つけるのが大変になっていく恐れがあります。 コンストラクタでデータを入れる場合は Setter は不要なはずなので、可能な限り外していくことをおすすめします。

TABLE定義を設計する時、カラム名に半角数値を入れるのはやめよう

こんにちは、エキサイト株式会社の中尾です。

出落ちみたいなタイトルなのですが、みなさんはテーブル設計の時、カラム名はどのようにしていますか?

基本は - 半角英字のみ - 極力正式名称をカラム名に、妙な省略はしない - package、classなど特定の言語の予約後になる可能性のある単語は避ける

だと、ほとんどの言語で互換性があり安定して使えると思います。

また、カラム名に半角数値を入れて横にカラムを伸ばすのは極力やめたほうが良いです。

そういう時は、

  • そもそも複数入るのか確認する。
  • 縦にデータを持ち、typeで分ける。

などいろいろアプローチがあるはずです。

例えば、仮に半角数値を使う時、どっちを使いますか?

  • tag_1
  • tag1

どっちを使っても、ローワーキャメルケースでは以下で現れます。

public String getTag1(){
    retrurn this->tag_1;
}

public String getTag1(){
    retrurn this->tag1;
}

キャメルケースでは半角英字をアッパーキャメルで表示しますが、数値では表現できません。これは不便ですよね。

つまり、ORMで自動でモデルを作成する場合など、キャメルケースにした場合にどっちにでも取れるカラム名にすると、大体不具合が発生することが多いです、 うまくコード変換が行われずに以下のようになる場合もあります。

public String getTag_1(){
    retrurn this->tag_1;
}

テーブル設計でそもそものデータ構造を考えるのも大事ですが、カラム名にも気をつけてあげられたら嬉しいと思います。

Rxdartを使用して値の変化を他タブなど別画面に通知する方法

これは エキサイトホールディングス Advent Calendar 2021 21日目の記事です。

エキサイト株式会社の高野です。今回はFlutterにおける別画面への値渡しや通知をする方法についてです。

実装

まずはじめに通知を受ける変数として以下を定義します。

final hoge = BehaviorSubject<void>.seeded({});

この void の部分を任意の型にすることによって通知を投げる側からその型の値を受け渡しすることができます。

受け取る際には任意の画面のinitStateなりで以下のように定義します。

hoge.listen((value) => _exFunction())
        .addTo(subscription);

これで通知の受け取りができました。
次に投げる側です

hoge.sink.add({})

こちらを任意の場所で呼んであげることによって通知を投げれます。
addのなかに定義した型の渡したい値をいれます。(ex: add('fugafuga') )

以上で受け渡しができるようになります。これを別クラスにシングルトンで作って呼び出せるようにすると使いやすいと思います。

最後に

弊社では絶賛採用強化中です。もしご興味がある方がいましたら下記リンクよりアクセスいただけると幸いです。(カジュアルからもOKです) www.wantedly.com

SpringBoot DevToolsのHot RestartとLiveReloadでサクサク開発を行う

エキサイト株式会社エンジニアの佐々木です。2021年エキサイトホールディングス アドベントカレンダー19日を担当させていただきます。

余談

エキサイトのメディア事業部では、SpringBootを使用して過去の技術的負債に立ち向かっております。私がジョインしてから1.5年以上が経過しましたが、半年以上の開発を経てメンバーも、悪い設計の見直しや改善、無駄処理の置き換え、クエリチューニングやパフォーマンス改善を通してサーバコストの圧縮を行っており成果がではじめています。サーバコストが半分以下になっていながらも、アプリケーションの負荷対策や開発速度などのパフォーマンスは上がっているところがほとんどで、技術観点や設計観点でも、ジョインしたときと比べて、成長をしているかと思います。

はじめに

SpringBootで開発する際に、Javaコンパイル言語であるのでコードを編集したらアプリケーションの再起動が必要になります。スクリプト言語のように次回実行時に動的にロードされるとかは通常ありません。SpringBootでは、Hot Reloadはないけど、Hot Restartはあるよといったものです。Hot Reloadより遅いけど、Hot Restartでも十分戦えるものになっているかと思います。バックエンドとフロントエンドの両方をご紹介します。

バックエンド

まず、バックエンド側のHot Restartの設定を紹介になります。

設定

依存関係を解消してくれるbuild.gradleに、SpringBoot devtoolsのパッケージを追加します。

dependencies {
    ...
        developmentOnly 'org.springframework.boot:spring-boot-devtools'    // この設定を追加
    ...

SpringBootの設定ファイルであるapplication.yaml(application.properties)に下記の設定を追加します。

spring.devtools.restart.enabled=true

これで準備は完了になります。

動作確認

動作確認は、

  1. IntelliJでSpringBootを起動
  2. コードを編集(コメントアウトしているコードのコメントを解除)
  3. ビルド (ファイル単体ビルド ⌘ + Shift + F9 or プロジェクトビルド ⌘ + F9 )

を実行します。これを行うと、SpringBoot DevToolsが変更を検知して、Hot Restartをしてくれます。このHot Restartは、アプリケーション全部を再起動しているわけではなく、必要な箇所だけやっているっぽいので、通常の再起動よりかなり高速です。

f:id:earu:20211219141741g:plain

さらに

ビルドのショートカットすら押したくない人もいるかと思います。IntelliJは、SpringBoot実行時には、コンパイルが走らないという設定があるので、これを解除します。

  • Preferences > Build, Execution, Deployment > Compiler > Build Project Automatically にチェックを入れます
  • Preferences > Advanced Settings > Allow auto-make to start even if developed application is currently running にチェックを入れます ※ Intellijの古いバージョンだと registly設定からauto-make で検索してください!!!

Auto Make は、ちょっとディレイがあるので、個人的には面倒でもビルドショートカットを押すのがオススメです。

フロントエンド

フロントエンドでは、LiveReloadという機構がありますので、これを使います。SpringBoot DevToolsは、このLiveReloadに依存しています。

livereload.com

設定

application.properties(application.yaml)に下記の設定を行います。

spring.devtools.livereload.enabled=true
spring.thymeleaf.cache=false

Chrome Extensionをインストール

Chromeに下記のExtensionをインストールします。

chrome.google.com

LiveReloadを動作させたいURLを開き、有効にします。

f:id:earu:20211219122336p:plain
LiveReloadのExteion

設定は以上で完了です。

動作確認

動作確認は、

  1. IntelliJでSpringBootを起動
  2. コードを編集(Hello, Spring -> Hello, LiveReload)
  3. ビルド (ファイル単体ビルド ⌘ + Shift + F9 or プロジェクトビルド ⌘ + F9 )

標準出力(テンプレートを通さない出力)もサーバサイドテンプレート出力もどちらにも対応しています。

標準出力

f:id:earu:20211219144738g:plain

サーバサイドテンプレート出力

f:id:earu:20211219143631g:plain

テンプレートのみ修正の場合

テンプレートのみ修正の場合は、再起動なしでOKです。SpringBoot DevToolsがファイル変更を検知して、LiveReloadをキックしてくれます。

f:id:earu:20211219143144g:plain

まとめ

バックエンドはスクリプト言語のようにまではいきませんが、少しはサクサク開発ができるようにはなります。設定がやや必要にはなりますが、静的型付け言語が持つ便利さを享受するのと引き換えにこのくらいの簡単な設定であれば、許容範囲ないかなと思います。フロントエンドの方は、最近ではReact.jsやVue.jsなどクライアントサイドのフレームワークが充実し、サーバサイドテンプレートは流行りませんが、SPAだとSEOは不安ですし、SSGやISRだとビルドのタイミング等に気を配る必要があります。規模によって適切なものを選択するのがいいかとおもいます。

最後に

最後まで読んでいただいてありがとうございます。引き続き、2021年エキサイトホールディングス アドベントカレンダーをお楽しみいただけると幸いです。

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

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

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

Jetpack ComposeでSwipe to Refreshのインジケータの表示位置をずらす

これは エキサイトホールディングス Advent Calendar 2021 19日目の記事です。

こんにちは。エキサイト株式会社 Androidエンジニアの克です。

今回は、AndroidJetpack ComposeでのSwipe to Refreshについてのお話です。

Swipe to Refreshとは

GoogleMaterial Designには、画面上の表示を最新の状態に更新するための仕組みとしてSwipe to Refreshというものが存在します。

Swipe-to-Refresh

AndroidのViewとしてはSwiperefreshlayoutが存在しますが、Jetpack Compose本体には相当するものが無いため今回はAccompanistSwipe Refreshを使用します。

Swipe to Refreshとリスト表示を実装する

まずはSwipe to Refresh本体と、セットになることが多いリスト表示を実装していきます。

今回はインジケータの調整が目的のため、実装内容については公式のドキュメントを参照してください。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
    ) {
        LazyColumn {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

@Composable
private fun ListItem(id: Int) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp)
            .background(color = if (id % 2 == 0) Color.LightGray else Color.Gray),
        contentAlignment = Alignment.Center,
    ) {
        Text(text = "item $id")
    }
}

下記はこのコードの動作イメージです。

固定のコンテンツとリストを組み合わせる

追加の要件として、画面上部にスクロールに左右されない固定表示のコンテンツを追加します。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    val contentHeight = 128.dp
    val contentPadding = 16.dp

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
    ) {
        Content(
            height = contentHeight,
            contentPadding = contentPadding,
            modifier = Modifier.zIndex(1F),
        )

        LazyColumn(
            contentPadding = PaddingValues(top = contentHeight),
        ) {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

@Composable
private fun Content(
    height: Dp,
    contentPadding: Dp,
    modifier: Modifier = Modifier,
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .height(height = height)
            .padding(all = contentPadding),
        backgroundColor = MaterialTheme.colors.primary,
        contentColor = MaterialTheme.colors.onPrimary,
    ) {
        Box(
            contentAlignment = Alignment.Center,
        ) {
            Text(text = "Content")
        }
    }
}

リストアイテムの先頭がコンテンツの下部に位置するように、LazyColumncontentPaddingにコンテンツの高さ分を指定しています。

また、コンテンツがリストよりも上のレイヤーとなるようにコンテンツに対して zIndexを指定しています。

下記はこのコードの動作イメージです。

インジケータがコンテンツの裏側に入り込んでしまっているのがわかるでしょうか。

こちらを対応するのが今回の目的となります。

インジケータの表示位置を変更する

とはいえ、インジケータの位置を変更するのは非常に簡単です。

LazyColumnのアイテム位置をcontentPaddingで変更したのと同様に、SwipeRefreshにもindicatorPaddingというパラメータが存在するのでこれを設定するだけです。

@Composable
private fun Screen() {
    var isRefreshing by remember { mutableStateOf(false) }

    val contentHeight = 128.dp
    val contentPadding = 16.dp

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = { isRefreshing = true },
        indicatorPadding = PaddingValues(top = contentHeight - contentPadding),
    ) {
        Content(
            height = contentHeight,
            contentPadding = contentPadding,
            modifier = Modifier.zIndex(1F),
        )

        LazyColumn(
            contentPadding = PaddingValues(top = contentHeight),
        ) {
            items(30) { index ->
                ListItem(id = index + 1)
            }
        }
    }
}

コンテンツには余白が設定されているので、コンテンツの高さからコンテンツの余白分(下部のみ)を引いた値にしました。

下記はこのコードの動作イメージです。

インジケータがコンテンツの下部からきれいに現れていることが確認できますね。

まとめ

Swipe to Refreshのインジケータの表示位置は、indicatorPaddingで設定することができます。

基本的にはデフォルトの設定で問題はないかと思いますが、レイアウトによっては直感的でより指に馴染むアプリになるのでぜひお試しください。

iOSでネイティブ広告を出す際にIBOutletを繋げられない時の対処法

これは エキサイトホールディングス Advent Calendar 2021 18日目の記事です。

エキサイト株式会社の高野です。今回はFlutterにおけるネイティブ広告の話です。

はじめに

今回の記事はgoogle_mobile_ads(1.0.0)の話ですので他のライブラリを使っていたり、バージョンが上がって修正されているかもしれませんのでご容赦ください。

対処法

まず、原因としてですがSDKのIssueを除いてみると以下のようです。

xcframeworkは一般的に.xibファイルと互換性がありません。 これはAppleのバグであり、いつ修正されるかはわかりません。

このような状況ですのでいつ治るかというのは不明みたいです。これによってIBOutletを接続することができないのできません。

解決方法ですが、以下ディレクトリに存在するGADNativeAdをRunnerと同階層に一度コピーしてあげることでIBOutletの接続が可能になります。

/Pods/Google-Mobile-Ads-SDK/Frameworks/GoogleMobileAdsFramework-Current/GoogleMobileAds.xcframework/ios-arm64_armv7/GoogleMobileAds.framework/Headers/GADNativeAd.h

接続することができましたらコピーしたGADNativeAdは削除していただいて構いません。
元のGADNativeAdと連携されているので、そちらを確認してみると繋がれています。
こちらの仕組みに関しましては自分の方でも詳しく調べられていないのでご教示いただけると大変助かります。

最後に

弊社では絶賛採用強化中です。もしご興味がある方がいましたら下記リンクよりアクセスいただけると幸いです。(カジュアルからもOKです) www.wantedly.com

SolrJを使ってSolr検索をする

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

エキサイトホールディングス Advent Calendar 2021の18日目の記事です。

qiita.com

今回は、Spring Boot プロジェクトでSolrJを使ってSolr検索を実装する手順について説明します。

SolrJ

SolrのJava用のAPIです。 solr.apache.org

Spring Bootには、Solr用のSpring Dataがありますが、新規プロジェクトへの利用は非推奨です。

spring.io

This project is about to move to the Spring Attic and is not recommended for new projects. The last Release (4.3.0) will, as part of the spring data release 2020.0, see patch updates till mid 2022.

そういった経緯もあり、今回はSolrJを採用しました。

実装

build.gradle に下記の行を追加します。

implementation "org.apache.solr:solr-solrj:x.xx.x" 

SolrスキーマJavaで定義します。

@Fieldスキーマのフィールド名からJavaのプロパティーへの変換を設定します。

@Data
public class BookSolrModel {

    @Field("book_id")
    private String bookId;

    @Field
    private String name;

    @Field
    private String description;
}

次に、リクエストからレスポンスの取得です。Spring BootのRepository層での実装例です。

SolrQueryをインスタンス化し、クエリーをセットします。

Solrから取得したBookSolrModelをドメインモデルにマッピングしています。

HttpSolrClient.query()はThrowableなので、例外ハンドリングをしています。

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepository {

    private final HttpSolrClient httpSolrClient;

    @Override
    public List<BookModel> getBookList(String bookId) {
        try {
            final SolrQuery query = new SolrQuery();
            query
                    .setQuery("book_id:" + bookId)
                    .setStart(0)
                    .setRows(1);

            final QueryResponse response = httpSolrClient.query(query);

            final List<BookSolrModel> bookSolrModelList = httpSolrClient
                    .getBinder()
                    .getBeans(BookSolrModel.class, response.getResults());

            return bookSolrModelList
                    .stream()
                    .map(bookSolrModel ->
                            new BookModel()
                                    .setBookId(bookSolrModel.getBookId())
                                    .setName(bookSolrModel.getName())
                                    .setDescription(bookSolrModel.getDescription())
                    )
                    .collect(Collectors.toList());

        } catch (SolrServerException | IOException e) {
            return List.of();
        }
    }
}

HttpSolrClientは @Bean によりDIコンテナに登録することで、他のRepositoryでもインスタンスを使い回せるようにしています。

Solrのホストはapplication.yml で定義しておき、 @Value で呼び出します。これにより環境の差異をコードに記載しなくて済みます。

@Configuration
public class SolrConfig {
    @Value("${solr.host}")
    private String solrHost;

    @Bean
    public HttpSolrClient httpSolrClient() {
        return new HttpSolrClient.Builder(solrHost).build();
    }
}

類似要素検索

MoreLikeThisを使った類似検索の例です。

            final SolrQuery query = new SolrQuery();
            query
                    .setQuery("book_id:" + bookId)
                    .setStart(0)
                    .setRows(1)
                    .setMoreLikeThis(true)
                    .setMoreLikeThisFields("name")
                    .setMoreLikeThisCount(3);

            final QueryResponse response = httpSolrClient.query(query);

            final List<BookSolrModel> bookSolrModelList = httpSolrClient
                    .getBinder()
                    .getBeans(BookSolrModel.class, response.getMoreLikeThis().get(bookId));

最後に

簡単にですが、Spring BootプロジェクトでのSolrJの使い方について説明しました。 他の言語で提供されているSolr APIと大きな差はなく、使いやすいものでした。 参考になれば幸いです。

エキサイトでは、エンジニア募集を随時行っております。 www.wantedly.com

引き続きエキサイトホールディングスのアドベントカレンダーをお楽しみいただければ幸いです。 qiita.com

参考

solr.apache.org

SpringBootでEventListenerの実装

エキサイト株式会社エンジニアの佐々木です。2021年Exciteホールディングスアドベントカレンダー17日目を担当させていただきます。

SpringBootでのイベントリスナーの実装についてです。

はじめに

SpringBootでは、アノテーションを用いたイベントリスナーの実装が比較的簡単にできるかと思います。

コード

今回は、イベント、パブリッシャー、リスナーと3つのクラスを書いていきます。

イベント

イベントのオブジェクトのコードは下記になります。

public class DemoEvent extends ApplicationEvent {
    private String message;

    public DemoEvent(Object source, String name) {
        super(source);
        this.message = name;
    }

    public String getMessage(){
        return this.message;
    }

}

ApplicationEventを継承して、あとはそれに必要な要素やメッセージを取得するメソッドをつけてあげれば実装完了です。

パブリッシャー

イベント発火を管理するパブリッシャーは下記のように記載します。

@Component
@AllArgsConstructor
public class DemoPublisher {

    private final ApplicationEventPublisher publisher; // Springで用意されている

    public void publishEvent(String name){
        publisher.publishEvent(new DemoEvent(this, name));
    }
}

Spring側で用意されているApplicationEventPublisherをDIし、そこにイベントのオブジェクトをセットするメソッドを実装しておきます。

リスナー

リスナーになります。パブリッシャーが発火したイベントを受け取るオブジェクトになります。

@Component
@AllArgsConstructor
@Slf4j
public class DemoListener {

    @EventListener // Springで用意されているアノテーションを付与
    public void onEvent(DemoEvent demoEvent){   // 引数にイベントのオブジェクトを設定する
        log.info("class: {} , event: {} , timestamp: {} ",
                new Object(){}.getClass().getEnclosingClass().getName(),
                demoEvent.getMessage(),
                demoEvent.getTimestamp()
        );
    }
}

リスナークラス自体を@ComponentでDIコンテナに登録し、実行したいメソッドに、@EventListnerを付与し、引数にイベントオブジェクトを設定します。

パブリッシャーの呼び出し

パブリッシャーを呼び出す処理を記述します。

@RestController
@RequestMapping
@AllArgsConstructor
public class DemoController {

    private final DemoPublisher publisher;  // パブリッシャーをDIする

    @GetMapping
    public void index(){
        publisher.publishEvent("index");
    }

}

今回は、コントローラにリクエストがあったら、パブリッシャーを呼んでもらう実装にしています。

実行

$ curl http://localhost:8080/

2021-12-17 07:59:16.248  INFO 14746 --- [nio-8080-exec-1] c.e.excite20211212.listner.DemoListener  : class: com.example.excite20211212.listner.DemoListener , event: index , timestamp: 1639695556247 

このようなログが出力されます。ちゃんとDemoListenerからログがでていることがわかります。

リスナーを増やす

アプリケーションが大きくなってきて、リスナーの処理を増やしたいが、別々で処理したいとします。その場合パブリッシャーとリスナーをそれぞれ修正せずとも、リスナーだけ増やせば可能になります。

@Component
@AllArgsConstructor
@Slf4j
public class Demo2Listener {  //クラス名変更

    @EventListener
    public void onEvent2(DemoEvent demoEvent){  // メソッド名も変更(変更しなくてもOK)
        log.info("class: {} , event: {} , timestamp: {} ",
                new Object(){}.getClass().getEnclosingClass().getName(),
                demoEvent.getMessage(),
                demoEvent.getTimestamp());
    }
}

これで、パブリッシャーに気付かれずにリスナーを増やすことが可能になりました。

$ curl http://localhost:8080/

2021-12-17 08:09:34.669  INFO 15034 --- [nio-8080-exec-2] c.e.e.listner.Demo2Listener              : class: com.example.excite20211212.listner.Demo2Listener , event: index , timestamp: 1639696174669 
2021-12-17 08:09:34.669  INFO 15034 --- [nio-8080-exec-2] c.e.excite20211212.listner.DemoListener  : class: com.example.excite20211212.listner.DemoListener , event: index , timestamp: 1639696174669 

まとめ

SpringのDIコンテナのおかげで、比較的簡単にイベント処理を疎結合に実装することが可能になりました。

おまけ

イベントは、ApplicationEventの継承がなくても動作しました。

public class DemoEvent  {
    private String message;

    public DemoEvent(Object source, String name) {
        this.message = name;
    }

    public String getMessage(){
        return this.message;
    }

}

ApplicationEventは、実行時間やシリアライズのインタフェースを実装してくれているので、継承はしていた方がいいと思いますが、DIがよしなに解釈して実行できていました。

最後に

エキサイトホールディングスアドベントカレンダー17日目を担当させていただきました。読んでいただいてありがとうございます。引き続き、アドベントカレンダーをお楽しみいただけると幸いです。

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

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

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

Illustratorのシェイプ形成ツールが便利!

f:id:designsazuka:20211216140127p:plain

はじめに

こんにちは!エキサイトのデザイナーのSAZUKAです。 今年もあと2週間ちょっとで終わりだなんて信じられません… 今年は毎日のようにfigmaを使っていたなと思います。

なのでfigmaについて語ろうか迷ったのですが…今日はIllustratorの話を少しさせてください(唐突)。 Illustratorのツールでいちばん好きなツールがありまして。

\シェイプ形成ツール/

こいつこそIllustratorを使う意味を見出せるのではないかと私は思っています( ˘ω˘ )🤘

調べた限りでは今現在figmaにはこの機能はないと思われます。

重なっている図形はこう作られている

シェイプ形成ツールを使えば、よくある知恵の輪のような図形も一瞬でできます。 f:id:designsazuka:20211216164320p:plain

重なった中抜きした円を2つ用意します。 f:id:designsazuka:20211216164749p:plain

今回は茶色が上に重なるようにしたいので、茶色い円をクリックしスウォッチのカラーを茶色にしておきます。(後からでも変えられます) f:id:designsazuka:20211216164752p:plain

二つの円を選択し、シェイプ形成ツールをクリック。 f:id:designsazuka:20211216164755p:plain

カーソルが触れたところが網掛け状になるので、上部の重なったところでプラスマークが表示されたらクリックします。 f:id:designsazuka:20211216164757p:plain

完成です!! f:id:designsazuka:20211216164800p:plain

オリジナルのロゴを作るときにも活躍

オリジナルのロゴを作るときにも活躍します。 今回はexciteの”e”を使ってぽてっとしたロゴマークを作ってみます。(探せば似たようなフォントはすでにありますがw)

あらかじめ少し太めのフォントを用意し、アウトライン化します。 まずは”e”の先端を丸くしたいので、同じ幅の円を重ねます。 (細かい部分は割愛させていただきます…) f:id:designsazuka:20211216164931p:plain

”e”と円を選択し、シェイプ形成ツールをクリック。 網掛け状になった部分は不要になるので消していきます。 altを押すとマイナスマークが出るのでクリック。 f:id:designsazuka:20211216164934p:plain

同様に左側のとんがりも削除するとこんな形になります。 f:id:designsazuka:20211216164937p:plain

あとはその繰り返しです。 内側の3箇所は逆に凹んだとんがりなので、今度は足してあげます。 f:id:designsazuka:20211216164940p:plain

拡大するとこんな感じ。 円と文字部分の隙間にカーソルを持っていくとプラスマークが出るので クリックします。 f:id:designsazuka:20211216164943p:plain

円を削除するとこのようになりました。 f:id:designsazuka:20211216164946p:plain

水色の部分は黒に修正すれば完成です! f:id:designsazuka:20211216164928p:plain

いかがでしたでしょうか?少し味のある”e”のロゴマークが出来上がりました。

シェイプ形成ツールは他にも出来ることがあるのですが、今回は実践的な内容でお届けしてみました。 Illustratorの自由度は本当に素晴らしいですね。想像力が無限に広がります。興味のある方は是非試してみてください٩( ᐛ )و

MySQLの機能でスロークエリ関連の解析を行う方法の紹介

はじめに

XTechグループ Advent Calendar 2021の16日目は、iXIT株式会社 エンジニアの蝦名がお送りします。
最近ハマっているものは音楽系Vtuberです。VIRTUAFREAK良かった…。

qiita.com

本題

ツールなどを導入しなくてもSlowQueryを解析できる機能がMySQLには存在するので、今回はその一部を紹介します。
ちなみに私が開発しているサービスのMySQLバージョンは5.6です。

1. mysqldumpslow

一言で言うとスロークエリーログファイルを解析して内容のサマリーを出力してくれる機能です。
前提としてスロークエリーログを出力している必要があります。

使い方

コマンド
※合計実行時間が長い順に10件のSQLを出力する
mysqldumpslow -s at -t 10 /opt/fio1/slog/sp-prd-db1-slow.log

出力例(1件):

Count: 23  Time=0.12s (2s)  Lock=0.00s (0s)  Rows=1.0 (23), host
  SELECT COUNT(*) FROM `user` WHERE user.STATUS=N AND (user.ACCESS_DATE>='S' AND user.ACCESS_DATE<='S')

本家のマニュアル

dev.mysql.com

弊サービスでは0.1秒以下のSQLをスロークエリーログに出力しており、
1時間毎のサマリーがメールとslackに送られてきます。
基本的にはmysqldumpslowの出力にある上位のSQLからチューニングしていけばいいので、重宝すると思います。

2. performance_schema

一言で言うとパフォーマンスモニタリング用のストレージエンジンです。
MySQL5.6で強化され、デフォルトで使用されるようになりました。
MySQL5.7でもっと強化されるのですが、残念ながら弊サービスのMySQLのバージョンは5.6です。

performance_schemaを使えばSlowQueryでは可視化されない

  • トランザクションの実行時間
  • 一回の実行時間は短いが、大量に実行して塵積になっているもの

などが取得できます。

使い方

MySQL5.5以上を使用していると、performance_schemaというデータベースが存在するかと思います。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| hoge               |
+--------------------+
6 rows in set (0.00 sec)
mysql> show tables;
+----------------------------------------------------+
| Tables_in_performance_schema                       |
+----------------------------------------------------+
| accounts                                           |
| cond_instances                                     |
| events_stages_current                              |
| events_stages_history                              |
| events_stages_history_long                         |
| events_stages_summary_by_account_by_event_name     |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| table_io_waits_summary_by_index_usage              |
| table_io_waits_summary_by_table                    |
| table_lock_waits_summary_by_table                  |
| threads                                            |
| users                                              |
+----------------------------------------------------+
52 rows in set (0.00 sec)

performance_schemaデータベース内にはテーブルが大量にあるのですが、
それぞれどんなテーブルなのかは以下のドキュメントを参照してください。

dev.mysql.com

performance_schemaは私もあまり触れていないので、良さげな記事をいくつか紹介します。

thinkit.co.jp

tech.stmn.co.jp

本家のマニュアル

dev.mysql.com

最後に

どちらもとっつきやすさはピカイチですね。
見てみるだけでも面白いと思いますので、
知らなかった!という方はぜひご自身のサービスで遊んでみてください。

【休日にリフレッシュ】都内でオススメの美術館5選

はじめに

エキサイト21卒デザイナーの山﨑です。

エキサイトホールディングス Advent Calendar 2021の15日目の記事を担当させていただいています。

qiita.com  

今回は、「都内でオススメの美術館5選」を紹介します。

西洋絵画や日本画より現代美術やデザインが好きなので、現代美術系の美術館・ギャラリー中心です。

なぜ美術館や展覧会に行くのか?

美術館や展覧会にはポスターデザイン、インフォグラフィックス、壁面に掲示されるキャプションデザインなど、たくさんのデザインで構成されています。

「展示の目的に適したフォントの選び方」や「展示の目的に適したインフォグラフィックス」、展覧会の構成・導線などを考えながら作品を見るととても良いインプットになるので休日には展示に足を運ぶようにしています。

六本木駅から徒歩5分「21_21 design sight」

王道中の王道、「21_21 design sight」です。建物の美しさもさることながら、展示内容はいつも斬新で面白く毎回といっていいほど通っています。

国立新美術館森美術館と比べて館内は小さめで1時間くらいでサクッと見られるので、初心者向きの美術館かなと思います。

21_21 DESIGN SIGHTでは三宅一生、佐藤 卓、深澤直人の3名がディレクターを務めており、デザイン系の展示が開催されることが多いです。

www.2121designsight.jp

清澄白河駅から徒歩9分「東京都現代美術館

現代美術の企画展が面白い美術館です。「TOKYO ART BOOK FAIR」も毎年開催地でもあります。

二階のカフェ「二階のサンドイッチ」の店内も可愛くて展示を回り終えたらそこで一服すると最高です😊

www.mot-art-museum.jp

東京駅から徒歩5分「アーティゾン美術館」

2019年7月1日にブリヂストン美術館から改名し、2020年1月18日にミュージアムタワー京橋内で新たにオープンした美術館です!

西洋美術、日本近代美術を主に収蔵している美術館ですが、現代美術の企画展なども開催しています。

なんと大学生まで入場料が無料…!

www.artizon.museum

新橋駅から徒歩3分「クリエイションギャラリーG8」

リクルートが運営しているギャラリーです。

クリエイションギャラリーG8は、日本のトップレベルのグラフィックデザインブランディングデザインを見ることができ、最先端の表現に触れることができます。

ADC展や1_WALLも展示されていて、クリエイティブ職の方なら一度はきたことがあるギャラリーではないでしょうか。

小さめのギャラリーなので、G8を見た後はガーディアンガーデン→資生堂ギャラリーgggのルーティーンで回るのがオススメです!

rcc.recruit.co.jp

新橋駅から徒歩5分「アド・ミュージアム東京

日本で唯一の広告美術館です。ここ数年あまり行けていないのですが、広告のクリエイティブがたくさん展示されていてとても楽しい美術館です。

アドミュージアム東京は日本の広告の歴史のほかにも企画展があり、世界的に評価が高い広告を見ることが見ることができます。

www.admt.jp

終わりに

いかがだったでしょうか?

美術館は面白い視点の作品を見たり美しい作品を見てリフレッシュできる場所です。気になる美術館があったらぜひ足を運んでみてください!

口下手流のプレゼンスライド制作のコツ

f:id:KAJIJI_Design:20211202185418p:plain こんにちは!SaaS事業部新卒デザイナーのかじもとです🐧
前回はプレゼンのコツとして記事を書かせていただきました(たくさんの方に見ていただけてめちゃ嬉しい)
tech.excite.co.jp


さて、今回はエキサイトのアドベントカレンダーに参加させていただきました🎄
年末や年度末は総まとめの機会、カンファレンスや発表会などなど増えますよね。なので今回は、プレゼンをする上で欠かせない「スライド制作」について書いてみようと思います。
スライドはプレゼンをする上でカンペになるものです。単に話す内容を記せばいいのですが、抑揚のない内容ではなかなか共感を得づらいですよね。 そうならないためのスライド作りのコツを、ここではお伝えします。

スライド制作の4つのコツ


① 話の流れは「つかむ・深める・ゆさぶる・動かす」

これは先輩デザイナーから教えていただいたもので、よくある起承転結と同じようなものです。以前私がLT会で使ったスライドを例に解説しましょう。
このスライドは、配属されて約2ヶ月たったビジネス職・デザイナー職の進捗報告LT会で、「配属した部署で学んだ事」をテーマに発表しました。

つかむ

つかむは、聞き手が話に引き込まれるような「なんだろう?」「続きが気になる」と思えるような内容にします。
f:id:KAJIJI_Design:20211201134037p:plain 例えばタイトルを2段階にしていて、デザインを学んでいた!のにもかかわらず「実際は学べていなかった!」と否定する入り方をしています。
これは発表内容に合わせていて、学生時代学んできたグラフィックの知識を全く活かせていない…というストーリーを表したタイトルにしました。
f:id:KAJIJI_Design:20211201132836g:plain また、配属されてから関わってきた制作物の一覧を自動スクロールさせたのですが、話のスピードに合わせて動かしたこともあり「なんだこれ?!」と思わせることができました😂

深める

深めるは、実体験や例を挙げて具体的な話をしていきます。
f:id:KAJIJI_Design:20211201134114p:plain 私の場合は、デザインが学べていなかったと感じた実体験を2つ取り上げ、具体的にどうダメだったのかをまとめました。
f:id:KAJIJI_Design:20211201134235p:plain ここでは、具体的な話を比較して伝えることで、よりわかりやすくなります。分野で一般的な考え方や理論と見比べてどうなっているか表現すると、別職種で馴染みない聞き手でも伝わりやすくなります。

ゆさぶる

ゆさぶるでは、深めるの内容からどのような行動をしたかをまとめます。前のステップで、聞き手は「こんなふうにできていなかったのか」となっているはず。ここではなるべく聞き手も一緒に「なるほど、その手があったか」と『一緒に解釈』してもらうことが大事です。
今回で言えば「デザインできていない」実体験を、どうやって乗り越えたかをスライドにしました。
f:id:KAJIJI_Design:20211201134250p:plain 文字の見た目である文字組ができていなかったため、直すための方法である図形置き換えの技を図にしています。このビフォーアフターによって、具体的にどんなアクションをしたのか追体験してもらいました。

動かす

動かすでは、最終的に聞き手に伝えた内容を覚えてもらうフェーズです。 これまで「深める」「揺さぶる」で伝えた内容は、ぜひとも実践につなげて欲しい、そのためには印象に残すことが欠かせません。ここではその熱い思いを、簡潔なメッセージにしましょう。
ただし「終わりよければ全てよし」のように、短いスローガンだけ言い渡されても印象には残りません。深める・揺さぶるで伝えたことに見出しをつけてあげる作業が「動かす」が担う役割となっています。
f:id:KAJIJI_Design:20211201134304p:plain 私の場合は、学んだことをテーマとしていたので、文字組をなんとなく作り失敗した体験の戒めとして「雰囲気で作らない」と銘打ちました。

② いいたい言葉を全部載せない

①では、スライド制作にあたる骨組みと肉付け方法をお伝えしました。次は付けた肉の削ぎ落とし作業です。
よく、資料が少なくならないように全部載せてしまう、カンペのように読み上げる文字が全て入っているスライドを見かけます。そういった見せ方が効果的な場面もありますが、LTのような短い発表では時間が足りなくなってしまいがちです。
さらにオンラインが増えた今、聞き手には発表者の身振り手振りよりも、画面に映るプレゼンスライドの情報量が多いでしょう。そこに不要な情報が紛れていたら…?おそらく聞き手はスライドに集中してしまい、発表者の言葉は流れてしまいます。
f:id:KAJIJI_Design:20211201134941p:plain 「お役所ポンチ絵」が情報量の多いスライドの代表格でしょうか。例えば、コーヒーについてまとめたスライドがあるとしましょう。
1枚のスライドで載せられる情報は「種類をまとめた表」「サイフォン式コーヒーの入れ方」といった、1要素を載せるくらいが妥当です。対して情報量が多いスライドは、コーヒーの種類も銘柄も淹れ方もいっぺんに示しているような物です。発表の短い時間の中では、情報を見るだけで手一杯になってしまいます。
f:id:KAJIJI_Design:20211201141104p:plain そうならないためにも、出てこない内容はスライドから省いてしまいましょう。1画面に載せる情報はなるべく少なくすることを意識してみてください。不安な場合は、スキップスライドや補足スライドとして別で用意するのがおすすめです。

③ 意図したグラフィックにする

デザインあるあるで「デザイナーはイケてるグラフィックを作れる」と思われがちですが、どれもきちんとした理由を持って作っています。装飾を施す引き出しが多いだけで、根本は質素なものを作っている方がほとんどだと思います。
これは②と通じていて「不必要な情報を置かない」作業と同じですが、どちらかというと「理解を促す図をわかりやすいものにする」作業になります。

私自身もグラフィック制作に慣れていないとき(今でも慣れているわけではないのですが…)、何もない余白を恐れ、開いたスペースにイラストを入れていました。しかしこれが落とし穴。スペースが空いたからといって、とりあえず埋め合わせて素材を追加したことで、明らかに「埋めました」感が出てしまうのです。
f:id:KAJIJI_Design:20211201142731p:plain 言葉だけではわからない情報(例えば表や図形など)はグラフィックとして補助となる情報源に。逆にグラフィックではわからない情報(例えば見た人によって印象が変わるもの)は文字として書き示すことが大切です。

④ 完成したらプレ・プレゼン

時間がなくなると発表練習が後回しになりがちですよね…。
前回記事の最後に「発表前に自分のプレゼンを聞き直す」とまとめたのですが、最低1回は本番と同じような環境で発表をしてみてください。プレ・プレゼンを行う目的として、作ったスライドの内容が頭からお尻までブレていないか確認する事、言いたい内容に必要な情報の過不足がないか見る事の2つが挙げられます。

1枚1枚のスライド制作をしていると具象的なところに集中してしまい、スライド全体を通しての確認を忘れてしまいがちになりますよね。最後に一度でいいので「言いたい内容が散らばりすぎていないかな?」「最初の内容が終わりではズレていないかな?」を確認する『一歩引いて客観視』をぜひしてみてください。
普段、スライドがうまくまとまらない…となる方はこちらを意識してみるとモヤモヤが解消できるかもしれません。
(スライド制作で絵コンテのようなものを作るのもおすすめです)

おわりに

ここまで長々と書きましたが、この記事でスライド制作に悩む方のお手伝いができたら嬉しいです。長文したためた甲斐が出るので…
最後に、「プレゼン」や「発表する」行為はとても緊張するものですよね。私自身も毎度そうで、緊張して膝やら指先を震わせてしまいます。

自分の中に留めておくだけではもったいないアイデアが、世の中にはたくさんあります。おそらく読んでくださった方の中にも、誰かが求めている素敵なアイデアがきっとあります。それを、外に出すきっかけの一つにプレゼンがあると思います。
ぜひ、素敵なアイデアを伝える機会として発表の場に登壇してみてください。陰ながら口下手デザイナーも応援させていただきます🚩