Slide 1

Slide 1 text

© DMM.com DMM.comの 課金プラットフォームにおける サーバサイドKotlin事情 DMM.com 北澤由貴 / 柏熊宏幸 2018/12/15 #devboostB 【B-2】

Slide 2

Slide 2 text

© DMM.com 自己紹介 北澤 由貴(Kitazawa Yoshitaka) 合同会社DMM.com ペイメントサービス部 2 柏熊 宏幸(Kashiwaguma Hiroyuki) 合同会社DMM.com ペイメントサービス部

Slide 3

Slide 3 text

© DMM.com アジェンダ • DMM.comについてのご紹介 • DMM.comの課金プラットフォームでKotlinを採用した理由 • 使っていく中で気づいた良い点・使いにくい点 • 課金プラットフォームのKotlin利用実績 • まとめ 3

Slide 4

Slide 4 text

© DMM.com DMM.comについての ご紹介 4

Slide 5

Slide 5 text

© DMM.com DMM.comについて 40以上のサービスを展開するサービスサイト 5

Slide 6

Slide 6 text

© DMM.com DMM.comについて 6 ※DMM.com、DMM GAMES、DMM.com 証券、DMM.com OVERRIDE、DMM.com Base、他連結 ※DMM.com サービスの会員数

Slide 7

Slide 7 text

© DMM.com DMM.comの課金プラットフォームについて 7

Slide 8

Slide 8 text

© DMM.com DMM.comの課金プラットフォームについて 8 APIにKotlin つかってるで!

Slide 9

Slide 9 text

© DMM.com DMM.comの 課金プラットフォームで Kotlinを採用した理由 9

Slide 10

Slide 10 text

© DMM.com • 新しいことがやってみたい! • 理あにかなってさえいれば技術選定は自由 • Javaの資産を再利用、コード量の削減も見込める • 今後長く使える言語 • Android公式言語に採用された • 企業(JetBrains社)でメンテナンスされている • 開発終了し、メンテナンスされなくなる可能性は低い • Javaエンジニアにとって習得が容易 • 過半数がJavaエンジニア • Javaとの互換性がある、IDEでJava→Kotlin変換 なぜKotlinを採用したのか 10

Slide 11

Slide 11 text

© DMM.com • 新しいことがやってみたい! • 理にかなってさえいれば技術選定は自由 • Javaの資産を再利用、コード量の削減も見込める • 今後長く使える言語 • Android公式言語に採用された • 企業(JetBrains社)でメンテナンスされている • 開発終了し、メンテナンスされなくなる可能性は低い • Javaエンジニアにとって習得が容易 • 過半数がJavaエンジニア • Javaとの互換性がある、IDEでJava→Kotlin変換 なぜKotlinを採用したのか 11

Slide 12

Slide 12 text

© DMM.com • 新しいことがやってみたい! • 理にかなってさえいれば技術選定は自由 • Javaの資産を再利用、コード量の削減も見込める • 今後長く使える言語 • Android公式言語に採用された • JetBrains社でメンテナンスされている • 開発終了し、メンテナンスされなくなる可能性は低い • Javaエンジニアにとって習得が容易 • 過半数がJavaエンジニア • Javaとの互換性がある、IDEでJava→Kotlin変換 なぜKotlinを採用したのか 12

Slide 13

Slide 13 text

© DMM.com • 新しいことがやってみたい! • 理にかなってさえいれば技術選定は自由 • Javaの資産を再利用、コード量の削減も見込める • 今後長く使える言語 • Android公式言語に採用された • JetBrains社でメンテナンスされている • 開発終了し、メンテナンスされなくなる可能性は低い • Javaエンジニアにとって習得が容易 • 過半数がJavaエンジニア • Javaとの互換性があり、IDEでのJava→Kotlin変換も可能 なぜKotlinを採用したのか 13

Slide 14

Slide 14 text

© DMM.com 使っていく中で気づいた 良い点・使いにくい点 14

Slide 15

Slide 15 text

© DMM.com Javaと比べて ちょっと便利なところ3つ 15

Slide 16

Slide 16 text

© DMM.com ①DataClass 16 public class User { private String name; private int age; public void setName(String name) { this.name = name; } ~~~ 略 ~~~ @Override public boolean equals(Object o) { ~~~ 略 ~~~ @Override public String toString() { return "User{name='"+name+'\''+",age="+age +'}'; } } data class User(var name: String, var age: Int) Kotlin Java • データ格納クラスを、簡単かつコード量少なく実装できる

Slide 17

Slide 17 text

© DMM.com ①DataClass • データ格納クラスを、簡単かつコード量少なく実装できる 17 public class User { private String name; private int age; public void setName(String name) { this.name = name; } ~~~ 略 ~~~ @Override public boolean equals(Object o) { ~~~ 略 ~~~ @Override public String toString() { return "User{name='"+name+'\''+",age="+age +'}'; } } data class User(var name: String, var age: Int) Kotlin Java getter/ setter equalsなど実装が必要

Slide 18

Slide 18 text

© DMM.com ①DataClass • データ格納クラスを、簡単かつコード量少なく実装できる 18 public class User { private String name; private int age; public void setName(String name) { this.name = name; } ~~~ 略 ~~~ @Override public boolean equals(Object o) { ~~~ 略 ~~~ @Override public String toString() { return "User{name='"+name+'\''+",age="+age +'}'; } } data class User(var name: String, var age: Int) Kotlin Java getter/ setter equalsなど実装が必要 dataクラスにすることで 同じ効果!

Slide 19

Slide 19 text

© DMM.com ①DataClass • データ格納クラスを、簡単かつコード量少なく実装できる 19 public class User { private String name; private int age; public void setName(String name) { this.name = name; } ~~~ 略 ~~~ @Override public boolean equals(Object o) { ~~~ 略 ~~~ @Override public String toString() { return "User{name='"+name+'\''+",age="+age +'}'; } } data class User(var name: String, var age: Int) Kotlin Java getter/ setter equalsなど実装が必要 dataクラスにすることで 同じ効果! Java : 51行     ↓ Kotlin: 1行!

Slide 20

Slide 20 text

© DMM.com ②raw string • コード上でSQLやJSONを表記したいとき、見やすく表現できる 20 @Query("SELECT" + " * " + "FROM" + " history_detail" + "WHERE" + "transaction_id = :transaction_id" + "AND valid_flag = 1", nativeQuery = true) public List findByTransactionId(@Param("transaction_id") String transactionId); @Query(""" SELECT * FROM history_detail WHERE transaction_id = :transaction_id AND valid_flag = 1 """, nativeQuery = true) fun findByTransactionId(@Param("transaction_id") transactionId: String): List Kotlin Java

Slide 21

Slide 21 text

© DMM.com ②raw string • コード上でSQLやJSONを表記したいとき、見やすく表現できる 21 @Query("SELECT" + " * " + "FROM" + " history_detail" + "WHERE" + "transaction_id = :transaction_id" + "AND valid_flag = 1", nativeQuery = true) public List findByTransactionId(@Param("transaction_id") String transactionId); @Query(""" SELECT * FROM history_detail WHERE transaction_id = :transaction_id AND valid_flag = 1 """, nativeQuery = true) fun findByTransactionId(@Param("transaction_id") transactionId: String): List Kotlin Java Javaは改行すると見づらい Kotlinはraw stringで改行表現できる

Slide 22

Slide 22 text

© DMM.com ②raw string • コード上でSQLやJSONを表記したいとき、見やすく表現できる 22 @Query("SELECT" + " * " + "FROM" + " history_detail" + "WHERE" + "transaction_id = :transaction_id" + "AND valid_flag = 1", nativeQuery = true) public List findByTransactionId(@Param("transaction_id") String transactionId); @Query(""" SELECT * FROM history_detail WHERE transaction_id = :transaction_id AND valid_flag = 1 """, nativeQuery = true) fun findByTransactionId(@Param("transaction_id") transactionId: String): List Kotlin Java 実はスペース開け忘れ! 実行時エラー! Javaは改行すると見づらい Kotlinはraw stringで改行表現できる

Slide 23

Slide 23 text

© DMM.com ③null safety • NullPointerExceptionを、コンパイル時に防げる! 23 Kotlin var str :String = nullReturnMethod() print( str.toLowerCase() ) Java String str = nullReturnMethod(); System.out.print( str.toLowerCase() );

Slide 24

Slide 24 text

© DMM.com ③null safety • NullPointerExceptionを、コンパイル時に防げる! 24 Kotlin Javaは実行時エラー!(NullPointerException) Kotlinはコンパイル時エラー!(変数代入時に) var str :String = nullReturnMethod() print( str.toLowerCase() ) Java String str = nullReturnMethod(); System.out.print( str.toLowerCase() );

Slide 25

Slide 25 text

© DMM.com ③null safety • NullPointerExceptionを、コンパイル時に防げる! 25 Kotlin var str :String? = nullReturnMethod() print( str.toLowerCase() ) nullが入る可能性がある場合、変数に「?」を付与 Java String str = nullReturnMethod(); System.out.print( str.toLowerCase() );

Slide 26

Slide 26 text

© DMM.com ③null safety • NullPointerExceptionを、コンパイル時に防げる! 26 Kotlin var str :String? = nullReturnMethod() print( str.toLowerCase() ) Java String str = nullReturnMethod(); System.out.print( str.toLowerCase() ); すると、変数参照時にコンパイルエラーに変わる

Slide 27

Slide 27 text

© DMM.com ③null safety • NullPointerExceptionを、コンパイル時に防げる! 27 Kotlin var str :String? = nullReturnMethod() print( str?.toLowerCase() ) Java String str = nullReturnMethod(); System.out.print( str.toLowerCase() ); コンパイルエラーを解消するには、 safe call演算子を使う(null以外のみ関数実行)

Slide 28

Slide 28 text

© DMM.com ③null safety • NullPointerExceptionを、コンパイル時に防げる! 28 Kotlin var str :String? = nullReturnMethod() print( str?.toLowerCase() ?: "default" ) Java String str = nullReturnMethod(); System.out.print( str.toLowerCase() ); また、エルビス演算子を使うことで、 nullの場合のデフォルト値を指定可能

Slide 29

Slide 29 text

© DMM.com • NullPointerExceptionを、コンパイル時に防げる! var str :String? = nullReturnMethod() print( str?.toLowerCase() ?: "default" ) ③null safety 29 Kotlin ちなみに、Javaでも同じようなコードを実現できるが コード量が多くつらい... Java Optional str = Optional.ofNullable(nullReturnMethod()); str.ifPresentOrElse( s->System.out.print( str.toLowerCase() ), ()->System.out.print("default") );

Slide 30

Slide 30 text

© DMM.com • コードが簡潔に、短く書ける! • コードの表現力が上がった! • Null safetyで、早い段階からバグを防げる! ちょっと便利なところ3つのまとめ 30

Slide 31

Slide 31 text

© DMM.com Javaと比べて ラムダ式が扱いやすい 31

Slide 32

Slide 32 text

© DMM.com 32 そもそもラムダ式とは? • 無名関数の記述方法のひとつ • Java8からラムダ式で簡潔に書ける public class Main { public static void main(String[] args) { Runnable r = new Runnable(){ @Override public void run() { System.out.println("Hello world."); } }; r.run(); } } public class Main { public static void main(String[] args) { Runnable r = () -> { System.out.println("Hello world."); }; r.run(); } } Java 無名関数 Java ラムダ式

Slide 33

Slide 33 text

© DMM.com 33 そもそもラムダ式とは? • 無名関数の記述方法のひとつ • Java8からラムダ式で簡潔に書ける public class Main { public static void main(String[] args) { Runnable r = new Runnable(){ @Override public void run() { System.out.println("Hello world."); } }; r.run(); } } public class Main { public static void main(String[] args) { Runnable r = () -> { System.out.println("Hello world."); }; r.run(); } } Java 無名関数 Java ラムダ式 無名関数だと本質的でない コードが多い

Slide 34

Slide 34 text

© DMM.com 34 そもそもラムダ式とは? • 無名関数の記述方法のひとつ • Java8からラムダ式で簡潔に書ける public class Main { public static void main(String[] args) { Runnable r = new Runnable(){ @Override public void run() { System.out.println("Hello world."); } }; r.run(); } } public class Main { public static void main(String[] args) { Runnable r = () -> { System.out.println("Hello world."); }; r.run(); } } Java 無名関数 Java ラムダ式 newとrun()のオーバライドを省略 無名関数だと本質的でない コードが多い

Slide 35

Slide 35 text

© DMM.com 35 ラムダ式をつかった例を比較 public class Main { public static void main(String[] args) { Runnable r = () -> { System.out.println("Hello world."); }; r.run(); } } public class Main { fun main(args: Array) { val r = { print("Hello world.") } r() } } • Kotlinでもラムダ式を利用できる • JavaもKotlinも、この状態だと大差ない Kotlin Java

Slide 36

Slide 36 text

© DMM.com 36 例1. Intを2つ受け取って演算 • JavaではBiFunctionという関数型インタフェースを使用 public class Main { fun main(args: Array) { val op = { a:Int, b:Int -> a+b} calc(1,2,op) // 3 } fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int { return op(a, b) } } public class Main { public static void main(String[] args) { BiFunction op = (a, b) -> a+b; calc(1, 2, op); // 3 } public static Integer calc(Integer a, Integer b,      BiFunction op){ return op.apply(a,b); } } Kotlin Java

Slide 37

Slide 37 text

© DMM.com 37 例1. Intを2つ受け取って演算 • JavaではBiFunctionという関数型インタフェースを使用 public class Main { fun main(args: Array) { val op = { a:Int, b:Int -> a+b} calc(1,2,op) // 3 } fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int { return op(a, b) } } public class Main { public static void main(String[] args) { BiFunction op = (a, b) -> a+b; calc(1, 2, op); // 3 } public static Integer calc(Integer a, Integer b,      BiFunction op){ return op.apply(a,b); } } Kotlin Java Integerを2つ受け取って 足し算結果を返す

Slide 38

Slide 38 text

© DMM.com 38 例1. Intを2つ受け取って演算 • JavaではBiFunctionという関数型インタフェースを使用 public class Main { fun main(args: Array) { val op = { a:Int, b:Int -> a+b} calc(1,2,op) // 3 } fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int { return op(a, b) } } public class Main { public static void main(String[] args) { BiFunction op = (a, b) -> a+b; calc(1, 2, op); // 3 } public static Integer calc(Integer a, Integer b,      BiFunction op){ return op.apply(a,b); } } Kotlin Java 型名はBiFunction Integerを2つ受け取って 足し算結果を返す

Slide 39

Slide 39 text

© DMM.com • JavaではBiFunctionという関数型インタフェースを使用 39 例1. Intを2つ受け取って演算 public class Main { fun main(args: Array) { val op = { a:Int, b:Int -> a+b} calc(1,2,op) // 3 } fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int { return op(a, b) } } public class Main { public static void main(String[] args) { BiFunction op = (a, b) -> a+b; calc(1, 2, op); // 3 } public static Integer calc(Integer a, Integer b,      BiFunction op){ return op.apply(a,b); } } Kotlin Java 型名はBiFunction Integerを2つ受け取って 足し算結果を返す リファレンスが必要 覚えるのはツライ

Slide 40

Slide 40 text

© DMM.com • JavaではBiFunctionという関数型インタフェースを使用 40 例1. Intを2つ受け取って演算 public class Main { fun main(args: Array) { val op = { a:Int, b:Int -> a+b} calc(1,2,op) // 3 } fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int { return op(a, b) } } public class Main { public static void main(String[] args) { BiFunction op = (a, b) -> a+b; calc(1, 2, op); // 3 } public static Integer calc(Integer a, Integer b,      BiFunction op){ return op.apply(a,b); } } Kotlin Java 型名はBiFunction Integerを2つ受け取って 足し算結果を返す リファレンスが必要 覚えるのはツライ Kotlinは関数自体が オブジェクト

Slide 41

Slide 41 text

© DMM.com • JavaではBiFunctionという関数型インタフェースを使用 41 例1. Intを2つ受け取って演算 public class Main { fun main(args: Array) { val op = { a:Int, b:Int -> a+b} calc(1,2,op) // 3 } fun calc(a: Int, b: Int, op: (Int, Int) -> Int): Int { return op(a, b) } } public class Main { public static void main(String[] args) { BiFunction op = (a, b) -> a+b; calc(1, 2, op); // 3 } public static Integer calc(Integer a, Integer b,      BiFunction op){ return op.apply(a,b); } } Kotlin Java 型名はBiFunction Integerを2つ受け取って 足し算結果を返す リファレンスが必要 覚えるのはツライ 定義したラムダ式の型 を指定 Kotlinは関数自体が オブジェクト

Slide 42

Slide 42 text

© DMM.com 42 例2. Intを3つ受け取って演算 fun main(args: Array) { val op = {a:Int,b:Int,c:Int -> a+b+c} calc(1,2,3,op) // 6 } fun calc(a: Int, b: Int, c: Int, op: (Int, Int, Int) -> Int): Int { return op(a, b, c) } public static void main(String[] args) { TriFunction op = (a, b, c) -> a+b+c; calc(1, 2, 3, op); //6 } public static Integer calc(Integer a,Integer b,Integer c,   TriFunction op){ return op.apply(a,b,c); } @FunctionalInterface interface TriFunction{ R apply(A a, B b, C c); } • Javaでは3つ受け取る関数型インタフェース定義が必要 Kotlin Java

Slide 43

Slide 43 text

© DMM.com 43 例2. Intを3つ受け取って演算 fun main(args: Array) { val op = {a:Int,b:Int,c:Int -> a+b+c} calc(1,2,3,op) // 6 } fun calc(a: Int, b: Int, c: Int, op: (Int, Int, Int) -> Int): Int { return op(a, b, c) } public static void main(String[] args) { TriFunction op = (a, b, c) -> a+b+c; calc(1, 2, 3, op); //6 } public static Integer calc(Integer a,Integer b,Integer c,   TriFunction op){ return op.apply(a,b,c); } @FunctionalInterface interface TriFunction{ R apply(A a, B b, C c); } • Javaでは3つ受け取る関数型インタフェース定義が必要 Kotlin Java TriFunctionを自作

Slide 44

Slide 44 text

© DMM.com 44 例2. Intを3つ受け取って演算 fun main(args: Array) { val op = {a:Int,b:Int,c:Int -> a+b+c} calc(1,2,3,op) // 6 } fun calc(a: Int, b: Int, c: Int, op: (Int, Int, Int) -> Int): Int { return op(a, b, c) } public static void main(String[] args) { TriFunction op = (a, b, c) -> a+b+c; calc(1, 2, 3, op); //6 } public static Integer calc(Integer a,Integer b,Integer c,   TriFunction op){ return op.apply(a,b,c); } @FunctionalInterface interface TriFunction{ R apply(A a, B b, C c); } • Javaでは3つ受け取る関数型インタフェース定義が必要 Kotlin Java TriFunctionを自作 定義したラムダ式の型 を指定 Kotlinは関数自体が オブジェクト

Slide 45

Slide 45 text

© DMM.com 改めてまとめると • Java • 関数型インタフェースを使用する必要がある • 用意されていない場合は自身で関数型インタフェースの定義が必要 • Kotlin • 言語として、第一級関数をサポート • 関数自体をオブジェクトとして扱うことが可能 • 関数を定義する際にインタフェース/クラス定義は不要 45

Slide 46

Slide 46 text

© DMM.com Javaとの互換性で 使いにくい点 46

Slide 47

Slide 47 text

© DMM.com Nullを扱うライブラリと相性が良くない • テストコードでMockitoを利用して一部の挙動をMock化したい • Mockito.any()はnullをreturnする • Kotlinではnullを許容していない場合例外になる 47 class HistoryServiceTest { @Mock lateinit var historyRepository: HistoryRepository @Test fun searchTest() { Mockito.doReturn(History("T001", ~)) .`when`(historyRepository).findByTransactionId(Mockito.any()) } } Kotlin java.lang.IllegalStateException: Mockito.any() must not be null

Slide 48

Slide 48 text

© DMM.com Nullを扱うライブラリと相性が良くない 48 Kotlin • テストコードでMockitoを利用して一部の挙動をMock化したい • Mockito.any()はnullをreturnする • Kotlinではnullを許容していない場合例外になる class HistoryServiceTest { @Mock lateinit var historyRepository: HistoryRepository @Test fun searchTest() { Mockito.doReturn(History("T001", ~)) .`when`(historyRepository).findByTransactionId(com.nhaarman.mockito_kotlin.any()) } } nullを考慮したライブラリで 回避可能

Slide 49

Slide 49 text

© DMM.com 課金プラットフォームの Kotlin利用実績 49

Slide 50

Slide 50 text

© DMM.com 課金プラットフォームの言語比率 50 ※ 課金プラットフォームの24プロダクトの集計結果 Python : 4% PHP : 58% Java : 25% Kotlin : 13%

Slide 51

Slide 51 text

© DMM.com 課金プラットフォームの言語比率 51 PHP : 58% Java : 25% DMMポイントを操作する APIの例をご紹介 Python : 4% ※ 課金プラットフォームの24プロダクトの集計結果 Kotlin : 13%

Slide 52

Slide 52 text

© DMM.com DMMポイントとは 52 • DMMで1ポイント1円で利用できるポイントサービス • DMMの利用でポイントプレゼント • プリペイドカードなどでポイントチャージ可能

Slide 53

Slide 53 text

© DMM.com 売上70億円 500万リクエスト DMMポイントとは 月間で... 53

Slide 54

Slide 54 text

© DMM.com ポイントAPIをリプレイス • DMMポイントの発行や消費をするAPIをリプレイス • 一般的でない技術を使用していて可読性が悪い • レガシーなミドルウェアを撤去したい 54

Slide 55

Slide 55 text

© DMM.com ポイントAPIをリプレイス • DMMポイントの発行や消費をするAPIをリプレイス • 一般的でない技術を使用していて可読性が悪い • レガシーなミドルウェアを撤去したい 55 リードタイムが長い

Slide 56

Slide 56 text

© DMM.com • DMMポイントの発行や消費をするAPIをリプレイス • 一般的でない技術を使用していて可読性が悪い • レガシーなミドルウェアを撤去したい • アーキテクチャ • Kotlin 1.2 • Spring Framework 5.0 • Spring Boot 2.0 • MySQL リードタイムが長い ポイントAPIをリプレイス 56

Slide 57

Slide 57 text

© DMM.com ポイントAPIリプレイスの実績 • 全部で11種類のAPIを作成 • 開発メンバーは4名 • ビジネスロジックの開発期間は2ヶ月 57

Slide 58

Slide 58 text

© DMM.com ポイントAPIリプレイスの実績 • 全部で11種類のAPIを作成 • 開発メンバーは4名 • ビジネスロジックの開発期間は2ヶ月 58 全員Kotlin未経験 Javaができるエンジニア

Slide 59

Slide 59 text

© DMM.com ポイントAPIリプレイスの実績 • 全部で11種類のAPIを作成 • 開発メンバーは4名 • ビジネスロジックの開発期間は2ヶ月 59 全員Kotlin未経験 Javaができるエンジニア Kotlinの学習コストが低いので 短い期間で成果が出せる

Slide 60

Slide 60 text

© DMM.com リプレイス前後のコード量の比較 60 コード量 31%削減 ※ ビジネスロジックの集計結果 Java Kotlin

Slide 61

Slide 61 text

© DMM.com 実績のまとめ 61 • コード量が減って可読性UP • レガシーなミドルウェアを撤去 • Kotlinは学習コストが低いので短期間で成果を出せる

Slide 62

Slide 62 text

© DMM.com 実績のまとめ 62 • コード量が減って可読性UP • レガシーなミドルウェアを撤去 • Kotlinは学習コストが低いので短期間で成果を出せる リードタイムの短縮!

Slide 63

Slide 63 text

© DMM.com 実績のまとめ 63 • コード量が減って可読性UP • レガシーなミドルウェアを撤去 • Kotlinは学習コストが低いので短期間で成果を出せる サーバサイドKotlinは月間70億円を売り上げる DMM.comの大規模課金プラットフォームを支えている! リードタイムの短縮!

Slide 64

Slide 64 text

© DMM.com まとめ 64

Slide 65

Slide 65 text

© DMM.com まとめ • 「新しいことをやってみたい!」からKotlinにチャレンジ • ちょっと注意も必要だが多くの恩恵を受けられた • Kotlin未経験のメンバーだけで 大規模な課金プラットフォームのリプレイスに成功 65

Slide 66

Slide 66 text

© DMM.com 66 Join Our Team! DMMグループでは一緒に働く仲間を募集しています。 https://dmm-corp.com/recruit/engineer Serverside Engineer Server Engineer iOS / Android Engineer Frontend Engineer Technical Consultant UI Designer

Slide 67

Slide 67 text

© DMM.com Thank you for your kind attention!