TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

936

EncryptedSharedPreferencesからTink + DataStoreに置き換えた話 こんにちは。Toyota Woven City Payment 開発グループの大杉です。 私たちのチームでは、 Woven by Toyota の Toyota Woven City で使用される決済システムの開発をしており、バックエンドからWebフロントエンド、そして、モバイルアプリケーションまで決済関連の機能を幅広く担当しています。 今回は公式にDeprecatedになってしまったEncryptedSharedPreferencesを実装していたAndroidアプリの置き換えをした話をまとめました。 はじめに EncryptedSharedPreferencesがv1.1.0-alpha07からDeprecatedになり、 Android KeyStoreへの置き換えが公式から推奨 されました。 ![Updates of security-crypto](/assets/blog/authors/osugi/20250616/security-crypto.png =600x) EncryptedSharedPreferencesの代替技術調査 EncryptedSharedPreferencesがDeprecatedとなったことで、永続化手段と暗号化技術の調査を始めました。 永続化手段の選定 私たちのアプリにおけるユースケースでは、設定データの保存にEncryptedSharedPreferencesを使用していただけであったので、SharedPreferencesを使用するだけでも十分ではありました。 ですが、せっかくの置き換えタイミングであったので公式推奨に則り、永続化手段として DataStore を採用しました。 暗号ライブラリの選定 こちらも前述の公式推奨の通り、 Android KeyStore を使用する方針で進めていこうとしたのですが、APIレベルによって機能の制約があるだけでなく、セキュリティレベルの高い実装(StrongBox)を使用するにはデバイスのスペックも関係するため、単純にプログラミングするだけでは想定したセキュリティレベルを担保できない可能性もありました。 今回のアプリは、MDMで管理されたデバイス上で動作する前提であり、StrongBoxにも対応しているデバイスを元々選定していたため、この制約については問題になりませんでした。 また、暗号ライブラリ調査の中で、 Tink というGoogleが提供している暗号ライブラリの存在を知りました。 Tinkのリポジトリ を見ると、マスターキーの保存にAndroid KeyStoreを利用されていることがわかります。 メンテナンスの容易さやパフォーマンスの観点でAndroid KeyStoreとTinkを比較するため、サンプル実装を行いました。 暗号ライブラリの実装比較 Android KeyStoreのStrongBoxとTEEを使用した場合とTinkを使用した場合のサンプルコードを以下にまとめています。 両者とも基本的な実装はそこまで苦労せず着手できたと感じました。 一方で、Android KeyStoreは 暗号アルゴリズムによってAndroid KeyStoreの鍵発行設定を変える必要がある 初期化ベクトル(IV)の管理が開発者依存になる 実装サンプルが少ない Tinkはこの辺りをうまくラップしてくれている良さがあります。 Android KeyStoreを使用した暗号・復号処理の実装例 class AndroidKeyStoreClient( private val useStrongKeyBox: Boolean = false ) { private val keyStoreAlias = "key_store_alias" private val KEY_STORE_PROVIDER = "AndroidKeyStore" private val keyStore by lazy { KeyStore.getInstance(KEY_STORE_PROVIDER).apply { load(null) } } private val cipher by lazy { Cipher.getInstance("AES/GCM/NoPadding") } private fun generateSecretKey(): SecretKey { val keyStore = keyStore.getEntry(keyStoreAlias, null) if (keyStore != null) { return (keyStore as KeyStore.SecretKeyEntry).secretKey } return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_PROVIDER) .apply { init( KeyGenParameterSpec.Builder( keyStoreAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setIsStrongBoxBacked(useStrongKeyBox) .setKeySize(256) .build() ) }.generateKey() } fun encrypt(inputByteArray: ByteArray): Result<String> { return runCatching { val secretKey = generateSecretKey().getOrThrow() cipher.init(Cipher.ENCRYPT_MODE, secretKey) val encryptedData = cipher.doFinal(inputByteArray) cipher.iv.joinToString("|") + ":iv:" + encryptedData.joinToString("|") } } fun decrypt(inputEncryptedString: String): Result<ByteArray> { return runCatching { val (ivString, encryptedString) = inputEncryptedString.split(":iv:", limit = 2) val iv = ivString.split("|").map { it.toByte() }.toByteArray() val encryptedData = encryptedString.split("|").map { it.toByte() }.toByteArray() val secretKey = generateSecretKey() val gcmParameterSpec = GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec) cipher.doFinal(encryptedData) } } } Tinkを使用した暗号・復号処理の実装例 class TinkClient( context: Context ) { val keysetName = "key_set" val prefFileName = "pref_file" val packageName = context.packageName var aead: Aead init { AeadConfig.register() aead = buildAead(context) } private fun buildAead(context: Context): Aead { return AndroidKeysetManager.Builder() .withKeyTemplate(KeyTemplates.get("AES256_GCM")) .withSharedPref( context, "$packageName.$keysetName", "$packageName.$prefFileName" ) .withMasterKeyUri("android-keystore://tink_master_key") .build() .keysetHandle .getPrimitive(RegistryConfiguration.get(), Aead::class.java) } fun encrypt(inputByteArray: ByteArray): Result<String> { return runCatching { val encrypted = aead.encrypt(inputByteArray, null) Base64.getEncoder().encodeToString(encrypted) } } fun decrypt(inputEncryptedString: String): Result<ByteArray> { return runCatching { val encrypted = Base64.getDecoder().decode(inputEncryptedString) aead.decrypt(encrypted, null) } } } 暗号ライブラリのパフォーマンス検証 Android KeyStoreとTinkの暗号化処理時間のベンチマークを計測しました。 Android KeyStoreについては、 StrongBox と TEE の2つの実行基盤を利用したケースで評価しています。 テストコードでは、共通の暗号化アルゴリズム(AES_GCM)を設定し、10KBのデータを繰り返し暗号化する処理を Microbenchmark を使用して計測しました。Microbenchmarkを使用することで、Google Pixel Tabletの実機上でかつUIスレッド以外のスレッドを利用して計測を行っています。 テストコードは以下です。 import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ExampleBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() @Test fun benchmarkTinkEncrypt() { val context = InstrumentationRegistry.getInstrumentation().context val client = TinkClient(context) val plainText = ByteArray(1024 * 10) benchmarkRule.measureRepeated { client.encrypt(plainText).getOrThrow() } } @Test fun benchmarkStrongBoxEncrypt() { val context = InstrumentationRegistry.getInstrumentation().context val client = AndroidKeyStoreClient(context, true) val plainText = ByteArray(1024 * 10) benchmarkRule.measureRepeated { client.encrypt(plainText).getOrThrow() } } @Test fun benchmarkTeeEncrypt() { val context = InstrumentationRegistry.getInstrumentation().context val client = AndroidKeyStoreClient(context, false) val plainText = ByteArray(1024 * 10) benchmarkRule.measureRepeated { client.encrypt(plainText).getOrThrow() } } } 以下に計測結果をまとめました。 暗号化基盤 平均処暗号理時間 (ms) アロケーション数 Android KeyStore (StrongBox) 209 4646 Android KeyStore (TEE) 7.07 4786 Tink 0.573 38 Android KeyStore (StrongBox)およびAndroid KeyStore (TEE)ではハードウェアへのアクセスが発生するため、ソフトウェア側で暗号化処理を行っているTinkと比べてかなり処理に時間がかかっていることがわかります。 今回採用したデバイスはAndroidの中でも比較的スペックが高いものですが、特にAndroid KeyStore (StrongBox)を採用する場合は、UXの検討が必要になりそうです。 備考 ちなみに、実際にAndroid KeyStoreの鍵生成で適用されている実行基盤は以下のコードから判別できます。 val secretKey = generateSecretKey() val kf = SecretKeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_PROVIDER) val ki = kf.getKeySpec(secretKey, KeyInfo::class.java) as KeyInfo val securityLevelString = when (ki.securityLevel) { KeyProperties.SECURITY_LEVEL_STRONGBOX -> "STRONGBOX" KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> "TEE" KeyProperties.SECURITY_LEVEL_SOFTWARE -> "SOFTWARE" else -> "UNKNOWN" } Log.d("KeyStoreSecurityLevel", "Security Level: ${ki.securityLevel}") まとめ EncryptedSharedPreferencesがDeprecatedとなったため、移植先の技術選定を行いました。 公式推奨に則り、永続化手段としてDataStoreを採用しました。 暗号化技術に関してはAndroid KeyStoreとTinkの比較検証を行い、Tinkの方が鍵の発行や暗号化処理が抽象化されていて利用しやすく、また、処理速度も優れていることがわかり、セキュリティ要件としても十分であるためTinkを採用することにしました。 Android KeyStoreを採用する場合は、動作環境のデバイススペックも考慮した実装が求められるため、セキュリティ要件とのバランスを考慮する必要がありそうです。
アバター
Self-Introduction My name is Morino, and I work at KINTO Technologies, mainly in product security and security governance. I'm a fan of RB Omiya Ardija and Chiikawa. For the past 10 years, I've been involved in cybersecurity and information security. Prior to that, I spent many years as a web application engineer, focusing on the development and operation of web performance measurement systems and front-end platforms for e-commerce sites. In this article, I’d like to share our Kaizen efforts related to the Vulnerability Disclosure Program (VDP). What is the Vulnerability Disclosure Program (VDP)? The VDP is a system that enables companies and organizations to receive vulnerability reports from external security researchers and white hat hackers. Public awareness grew in October 2023 when Rakuten Group placed a "security.txt" on a public server and launched its VDP. Reference: Rakuten places text on public server — "security.txt" to help improve security (in Japanese) What is security.txt? Published as RFC 9116 in April 2022, security.txt: A File Format to Aid in Security Vulnerability Disclosure helps standardize how companies and organizations share vulnerability reporting information, making it easier for security researchers to report issues. We also published our own security.txt in November 2023. Reactions After Installing security.txt Most inquiries were about whether we offer rewards (we don't), and many reports didn't clearly indicate whether they actually involved vulnerabilities in KINTO/KTC. A White Hat Hacker Reported a Vulnerability! In August 2024, we received a report about a vulnerability in our service. After reviewing the report within our team, we confirmed the issue was real and asked the development team to fix it. Kaizen for Our VDP Although we recognized the value of the VDP, we identified several issues. As a result, we started using the VDP service offered by IssueHunt in November 2024. Clarify VDP guidelines, including whether a bounty is offered Specify which web services and applications are covered Provide a report template As of March 7, 2025, we've received six reports; two of which involved vulnerabilities that required action and have been fixed. Honestly, I'm surprised at the results, which exceeded my expectations. Our company is featured as a case study on the IssueHunt website. Please have a look if you're interested. A fresh perspective on boosting security! VDP success story in the car subscription industry (in Japanese) Our Contribution to the P3NFEST Bug Bounty 2025 Winter As mentioned above, we do not currently offer a rewards program. However, to evaluate the effectiveness of incentive-based systems and to support students working to secure the future of the internet we’ve decided to contribute some of our services to a student-focused bug bounty program hosted by IssueHunt. The following services are eligible for bug bounties: KINTO Technologies Corporate Site KINTO Tech Blog (this website) The event runs from Monday, February 17, 2025 to Monday, March 31, 2025. For more information, please visit the event page. We welcome students to take on the challenge. P3NFEST Bug Bounty 2025 Winter
アバター
「こんな世界観の映像、作ってみたいな…」 そう思ったとき、皆さんならどうしますか? 私は迷わず、ChatGPTに丸投げしました。 こんにちは。KINTOテクノロジーズのクリエイティブ室でデザイナーをしている桃井( @momoitter )です。 この記事では、ChatGPT、Midjourney、RunwayなどのAIツールを駆使して、「しぇるぱ」というピンク髪のバーチャルキャラクターの映像を、ほぼ会話だけで形にしていった過程をまとめています。 専門的なスキルや時間がなくても、「こんな映像を作ってみたい」というアイデアさえあれば大丈夫。 AIと一緒に、そのイメージを少しずつかたちにしていくプロセスを体験してみたい方に向けて書きました。 まずは完成した映像をご覧ください。 https://www.youtube.com/watch?v=GH9CdNqTyHQ きっかけは、1体のキャラクターの“リニューアル” この「しぇるぱ」というキャラクターは、もともと2024年11月の社内イベント「超本部会」のために誕生したものでした。 制作過程はこちらの記事をご覧ください。 https://blog.kinto-technologies.com/posts/2025-03-07-creating_a_mascot_with_generative_AI/ 当時としては最先端のAI技術を使い、社内でも注目を集めたキャラクターでした。 ……が、あれからわずか4ヶ月。画像生成AIや動画生成AIはさらに進化し、当時「すごい!」と思っていた表現が、いま見ると少し古く感じてしまうように。 そこで、「せっかくなら最新の技術で、このキャラの世界をアップグレードしよう」と思い立ち、ChatGPTと一緒に再構築を始めることにしました。 Step1 世界観の共有と画像生成 まず最初に行ったのは、キャラクターとその世界観の共有です。 もともと生成していた「しぇるぱ」の画像をChatGPTにアップロードし、こう伝えました。 このキャラクターは少し前の画像生成AI技術で作られたため、見た目をアップデートしたいです。 彼女には「バーチャルオペレーター」という設定があります。 その設定をもとに世界観を膨らませ、Midjourney v7で表現できるようなシーンのバリエーションとプロンプトを提案してください。 Midjourneyを選んだ理由は、v7にアップデートされて以降、キャラクター描写の精度や質感が大きく向上したと感じていたからです。 今回のように、既存キャラの見た目をアップグレードしたい場面にぴったりだと思いました。 するとすぐに、「その世界観なら、こういうシーンはどうでしょう?」といった具体的なシチュエーション案と、それに対応するプロンプトが次々と返ってきました。 まるで映像監督とのブレストをしているような感覚です。 試しにそのプロンプトをMidjourneyに入力してみたところ、 自分の想像をはるかに超えるビジュアルが次々と生成され、その表現力に驚かされました。 この映像を作り始めた当初は、Midjourney v7に「Omni-Reference」のようなキャラクターの一貫性を保つ機能がまだ搭載されていませんでした。 そのため、「ピンク髪のショートヘア」という分かりやすい特徴を意識的にプロンプトへ含めることで、「一貫性があるように見せる」工夫をしていました。 もしイメージと違うものが出てきても、 「もう少し顔に寄って」「背景を明るくクリーンな雰囲気に」などとChatGPTに伝えるだけで、再調整されたプロンプトを即座に出力してくれます。 Step2 画像から動画生成へ 気に入った画像が生成できたら、次はそれをChatGPTに添付し、以下のように依頼します。 この画像は提案していただいた〇〇のシーンのプロンプトをMidjourneyで生成した画像です。 この画像をRunwayのGen-4のキーフレーム機能のファーストフレームとして設定し、動画を生成したいです。 よりこのシーンが魅力的になるような、動きを加えたプロンプトを生成してください。 ChatGPTは画像の内容を読み取った上で、その魅力を最大化するRunway用プロンプトを作成してくれます。 Runwayを使った理由は、バージョンがGen-4へと進化したことによって、Midjourneyの高精細なビジュアルの魅力を損なわずに動画化できると感じたからです。 Runway Gen-4のimage to videoにMidjourneyで生成した画像をアップ。 ChatGPTが出力したプロンプトを貼り付けると、画像の世界観を最大限に引き出す高クオリティな映像が生成されました。 キャラクターやカメラの動きのイメージが違った場合も、 ChatGPTに「生成された動画はこうなっていたので、ここをこう変えてほしい」と伝えるだけで、プロンプトを再提案してくれます。 Step3 BGM選定もChatGPTと一緒に 映像のBGMを探すときも、ChatGPTが大活躍。 この世界観に合うBGMをAdobe Stockでどのようなキーワードで探せばいいですか? と聞くと、「futuristic」「sci-fi」「cyberpunk」など、雰囲気に合ったワードをいくつも提案してくれました。 Step4 編集して完成 生成した動画とBGMをPremiere Proでつなぎ合わせ、構成・長さ・テンポ感を調整します。 シーンの切り替えにフェードイン・アウトを加えたり、音の入り方に緩急をつけたりすることで、映像全体の完成度がグッと高まります。 Midjourneyで作成した静止画と、Runwayで生まれた滑らかな動きが合わさることで、 静止画だけでは伝わりきらなかった「息づかい」や「空気感」が加わり、しぇるぱの世界観が一段とリアルに感じられるイメージビデオが完成しました。 https://www.youtube.com/watch?v=GH9CdNqTyHQ AIと一緒に、想像をかたちにするということ 今回のプロセスで一番感じたのは、 自分の頭の中の曖昧なイメージを、ChatGPTがどんどん「言語化&具現化」してくれること。 Midjourneyでも、Runwayでも、「ちょっと違う」「もっとこう」と伝えるだけで、理想の表現に近づいていく実感がありました。 AIと一緒に進めることで、創造の幅が大きく広がることを実感できるはずです。 ぜひ一度、体験してみてください。
アバター
Hi, this is Nakanishi from the QA Group (though I also wear a few other hats at the Developer Relations Group and the KINTO FACTORY Development Group ^^) This year at KINTO Technologies, we're embracing an "AI First, Release First" mindset. As part of this shift, our QA team has been exploring ways to make the most of AI to speed up our release cycles. This time, a group of QA members who shared the same passion came together for a lively brainstorming session. "Wouldn't it be awesome if we could do this?" "I really want to try something like this!" Together, we discussed ideas and possibilities. In this article, I'll be sharing some of the most exciting concepts that emerged out of the session, exploring how AI can help transform the future of QA. Issues That Emerged from Our Discussion QA teams produce a huge volume of documents every day, but the sheer amount of information makes it hard to find what's actually needed. Specification formats also vary by project or person, which complicates sharing across teams. On top of that, there's no solid system for reviewing incidents or implementing preventive measures—making it tough to stop issues from recurring. The review process itself is also a heavy workload, and there's growing demand to streamline it. A Future Made Possible by AI Effective Information Use (RAG) RAG (Retrieval-Augmented Generation) is a cutting-edge method for retrieving and analyzing information using the latest in generative AI technology. It quickly pulls key insights from vast archives of documents and incident data, delivering the right information to users when they need it. For instance, when an incident occurs, AI can instantly scan and analyze similar past cases to suggest effective solutions on the spot. It's like having a top-tier assistant who remembers every past experience and offers instant advice right when you need it. Already in use across industries like finance and customer support, it's dramatically speeding up response times and boosting problem-solving efficiency. Organizing and Supporting Specifications and Designs AI helps identify inconsistencies and omissions in your specifications, clearly highlighting and organizing any issues. By analyzing the inputted specifications, it can also automatically generate suitable test scenarios. For example, if you provide the specs for an e-commerce site cart feature, the AI can instantly create a scenario like: "Add product → Change quantity → Payment → Order confirmation." It can even generate additional scenarios, such as error handling flows and boundary value tests. This drastically cuts down the time and effort spent on manual scenario creation, boosting both the accuracy and efficiency of QA tasks. Automate and Streamline Your QA Process AI analyzes user operation logs to catch even the small mistakes that humans might miss. Specifically, it detects frequent user errors and unusual operations, like "errors triggered during specific screen transitions" or "fields often filled in incorrectly on input forms." With this, you can refine your test scenarios and address potential problem areas in advance. AI also automates the hassle of managing test case numbering on Conflu, cutting down on manual mistakes and saving a significant amount of time. Incident Analysis and Prevention AI that analyzes past incidents and offers concrete measures to prevent them from happening again. For example, it thoroughly reviews cases like "display issues on specific browsers" or "payment system failures" on e-commerce sites. Based on the findings, it suggests actionable steps such as "regularly checking browser behavior by version" or "enhancing error handling before and after payment processing." When a critical issue arises, the AI immediately assesses the risk level and sends automatic alerts to the relevant teams, enabling swift, real-time response and resolution. Streamlining Test Data Creation AI instantly generates the data required for testing. From new vehicle and used car details to user information, it can quickly produce large volumes of diverse data tailored to realistic business scenarios. In addition, by integrating AI with browser automation tools like Selenium and Appium, tasks that once required manual input can now be automated. With just a few simple settings, you can generate massive amounts of test data in no time. This integration not only reduces human error but also slashes the workload required for data creation, greatly improving the overall efficiency of your QA process. Tool Integration and Process Automation Connect Slack with tools like JIRA or Asana to receive timely, automated updates on what matters. Streamline your workflow by centralizing information from various tools in one place. Let AI serve as the bridge between platforms, smoothing out your entire process flow. Using AI Models for QA We're exploring the use of ChatGPT fine-tuned specifically for QA tasks. By building our own in-house AI model, we dramatically improved the response speed of the AI. Furthermore, we're working to create an environment where developers can easily use AI for quick self-checks. Future Action Plans Prioritize organizing and consolidating information using AI. AI-assisted reviews will help us work more efficiently and lighten the human load. Continuous data collection for the development of dedicated AI models. Actively utilizing AI-based incident analysis for process improvement. Automation between various tools will lead to further improvements in efficiency. Even when something feels too big to handle alone, new paths can open up when we think together. Inspired by the ideas from this brainstorming session, we'll launch a range of QA initiatives powered by AI. If you're a QA engineer interested in exploring new possibilities with AI or taking on fresh challenges with us, we'd love to connect. Casual chats and info sessions are always welcome. Feel free to reach out!
アバター
はじめに こんにちは、2025年3月入社の tetsu です! 本記事では、2025年3月入社のみなさまに、入社直後の感想をお伺いし、まとめてみました。 KINTO テクノロジーズ(以下、KTC)に興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! オサダヨシヒロ 自己紹介 グループコアシステム部のオサダです。ビジネスデベロップメントチームとグローバルコミュニティサイトの“TOYOTA Community by KINTO”を担当しています。 所属チームの体制は? グループコアシステム部は約40名います。多国籍なメンバーがいて日本語、英語他様々な言語でコミュニケーションしています。ビジネスデベロップメント、システム開発グループ、共通サービス開発グループがあります。 ビジネスデベロップメントはグローバルのリースシステムを担当しています。 システム開発グループは“Global KINTO ID Platform”や“TOYOTA Community by KINTO”の企画/開発/運用しています。 共通サービス開発チームは”会員プラットフォーム”と“決済プラットフォーム”、最近立ち上げた“AIプラットフォーム”の開発/運用をしています。 KTCへ入社したときの第一印象?ギャップはあった? 業務外の部活動、イベント(任意参加の“Beer Bash”など)がとても多いと思いました。特に自動車部は活動活発な印象で、レース観戦やサーキットカートイベント等があります。部員でなくても飛び入り参加企画もあり、仕事も部活動も活発な印象です。 現場の雰囲気はどんな感じ? “Good Morning!”で元気よく1日がはじまる感じです。多国籍で様々な会社を経験しているメンバーなので、和気あいあい議論しながらお仕事進めています。 ブログを書くことになってどう思った? KTC、グループコアシステム部を知ってもらういいチャンスと思いました! また、自分自身の振り返りにもなりました。 KJさん ⇒ オサダヨシヒロさんへの質問 映画がお好きとのことですが、これ観とかないと損するぞって映画を何点か教えていただきたいです。 KTCの皆さまへは、「タッカー」を観て頂きたいです。監督はフランシス・フォード・コッポラさん、製作総指揮 はジョージ・ルーカスさんと豪華なスタッフ陣で1988年のアメリカ映画です。時代は違いますが、車への熱い想いがあります。 tetsu 自己紹介 プラットフォームGでPlatformエンジニアをしています、tetsuと申します! 所属チームの体制は? 私が所属するプラットフォームG Platform Engineeringチームは、神保町オフィスに6人、Osaka Tech Labに3人の体制となります。 東京と大阪で物理的に距離がありますが、SlackやGatherなどを使ってコミュニケーションをとっています。また東京と大阪のメンバーで一緒に勉強会を企画もしてます! KTCへ入社したときの第一印象?ギャップはあった? ギャップは特にありませんでした。オフィスが離れている他チームの人とのコミュニケーションが大変なのかなと思っていましたが、BeerBashのようなイベントや社内サークルが活発なので、他チームの人との接点も多く、仕事がしやすいです。 現場の雰囲気はどんな感じ? 真面目に技術の議論をしたり、休憩中には雑談をしたりと和気あいあいとしています。私がKubernetesの勉強をしたいと話していたら、大阪のメンバーも一緒にしたいとなり、一緒に勉強会を企画することになり、今ではCloudInfraグループ、DBREグループとグループ横断で実施しています。 ブログを書くことになってどう思った? 入社ブログを見ていて転職するかどうか考えていました。私と同じような転職を考えている人が見ているのかなと思うと、緊張しています(笑) オサダヨシヒロさん ⇒ tetsuへの質問 神保町オフィス周辺のお勧めランチを教えてほしいです! 麺類が好きなので、「丸亀製麺」や油そばの「春日亭」によく行きます!天丼の「はちまき」という店も気になっているので、神保町でお会いしたときにぜひ行きましょう! YY 自己紹介 共通サービス開発G所属で、バックエンドエンジニアです 所属チームの体制は? 室町オフィスにて13名で開発を行なっています。 KTCへ入社したときの第一印象?ギャップはあった? 入社時のフォローアップがしっかり整備されている点がとても安心できました。チームではドキュメントにしっかり残す文化が定着していて、運用ノウハウを蓄積できている点が驚きでした。 現場の雰囲気はどんな感じ? それぞれが尊重し合って仕事していると思います。チーム方針として、「遠慮しない」という点を明記していたり、困っていたら誰かが力になってくれる環境でとても心強いです。 ブログを書くことになってどう思った? こういうのってなかなか腰が上がらなかったので、機会を与えていただけてとても嬉しいですし、今後執筆するハードルが下がったらいいなと思っています。 tetsuさん ⇒ YYさんへの質問 入社時に車を持っている話をしたと思うのですが、愛車へのこだわりがあれば教えてほしいです! こだわりで合ってるのか微妙なとこですが、好きな車体カラーを選ぶとこですかね。有料色でもリセールも気にせずビビッときた色を選ぶようにしています! ナミキ ユウジ 自己紹介 コーポレートITG ID (Innovation Drive)チームのナミキ ユウジです。 主にKTCのM365環境の課題解決と新しい技術の検証・導入、販売店様の情シス業務支援を行っています。 所属チームの体制は? 私を含めて9名のチームです。 室町、神保町、名古屋という複数拠点のメンバーで構成されています。 KTCへ入社したときの第一印象?ギャップはあった? Slackのレスポンスが著しく速い!!と感じたのが第一印象ですね。コミュニケーションのスピード感に驚きました。「Slackの通知をパッと見て、すぐにアクションを返す習慣」が組織全体に根付いているんだなと感じました。 ギャップとしては、設立して間もない組織であるのに、社内向けドキュメントがきちんと整備されていることですね。また、ConfluenceやSharePointが使いこなされていることに感動しました。何かわからないことがあれば、内製の生成AIに聞いたり、社内ポータルにアクセスすれば、たいてい自己解決ができます。そのような環境であることは予想外でした。 現場の雰囲気はどんな感じ? 気軽に声を掛けたり、ご飯に誘える雰囲気です。オフィス周辺には美味しいお店がたくさんあるので、ランチが楽しいです。仕事終わりのまぜそばがこれまた最高です。 チームの別拠点の方とも良い関係性で仕事ができています。数ヶ月に1回、別拠点の方とも直接お会いして仕事できる機会があることが大きいのかなと思っています。 また、社内イベントを通じて他部署の方とも気軽に交流できていることが嬉しいです!(まだ入社3ヶ月目ですが、50~70人近くの他部署の方と交流できています。嬉しい!) ブログを書くことになってどう思った? エンジニアブログ運営チームに直接声をかけてもらって、素直に嬉しいと思いました。入社後は技術ブログを積極的に書きたいなと思っていましたし、入社同期と再び交流する機会にもなったので。これを機に、今後も継続して技術ブログを投稿していきたいと思います。(めざせ月イチ投稿!) YYさん ⇒ ナミキ ユウジさんへの質問 同じサウナ部ですが、今気になってるサウナ施設があれば教えて欲しいです! 今年の5月にオープンした 毎日サウナ越谷店 です! 1号店の前橋店、2号店の八王子店に次ぐ3号店となるのですが、越谷店には毎日サウナ初のお風呂(しかも薬草泡風呂!)があるとのことで、メチャメチャ気になっています。サウナ部で行きたいですね!! KJ 自己紹介 ゴルフが大好きでした。血豆ができるまで練習しました。30代後半から始めたのですがシニアプロになりたいと半ば本気で思ってました(笑)。ただ、コロナを境にプレー頻度が減って練習もしなくなり下手になりました。2025年4月、マスターズでのマキロイのキャリアグランドスラム達成に感動し、もう一度、出直そうと思っています。 所属チームの体制は? 出向先のKINTOでは業務PJ改善Tというリース業務の改善を行うチームに所属しており、そこは4人体制です。KTCでは部付きです。 KTCへ入社したときの第一印象?ギャップはあった? 官僚的・縦割りな組織ではなく、会社全体を見回し、そのうえで自身で出来ること、すべきことを自立的に考えることができる人が多い組織だなというのが第一印象です。 良い意味でトヨタらしさが無いことがギャップでした。 現場の雰囲気はどんな感じ? KINTOサービスの根幹であるリース業務を回す部署なのでキッチリカッチリした雰囲気がある半面、メンバーの方々は多様性あふれる面白い人ばかりです。 どうすれば業務の品質が上がり、工数/リードタイムが削減できるだろう、と日々考えており、良い意味で現状を疑う雰囲気を持っていると思います。 ブログを書くことになってどう思った? 面白い取り組みだと思いました。一般的に埋もれやすい新入社員にスポットライトを当ててもらえるのはありがたいですし、応募・入社検討中の方々にとっても近しい存在の発信として参考になる情報だと思います。 ナミキ ユウジさん ⇒ KJさんへの質問 他部署のメンバーとの関わりなどどのようにされていますか? 自部署では1人だけの名古屋採用で、普段は室町オフィスではなく名古屋オフィスにいます。名古屋オフィスのKTCメンバーは室町に比べるとかなり少ないですが、少数がゆえの距離感の近さもあり他部署の方々とはよく仕事の話や雑談をさせてもらっています。一緒にランチに行くことも多いです。また、私はKINTOの業務部に出向させていただいており、リース業務を通して交わる人も徐々に増えてきいます。向こう1-2年くらいで友達100人を達成したいです(笑)。 さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTO テクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTO テクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
アバター
Introduction Hello! I'm Momoi ( @momoitter ), a designer at KINTO Technologies. I belong to the Creative Office where I work on projects like the corporate website and Kumobii (KINTO's official mascot) related sites, incorporating cutting-edge web design along the way. In November 2024, our company hosted an internal event called the CHO All-Hands Meeting, and I was in charge of creating the opening movie for it and I used three generative AIs to create a female character who kicks off the event with a voice announcement. Actual Video https://www.youtube.com/watch?v=pVj_UQ_3-tg When asked at the event start, "Are you ready?", she answers "Of course!" In this article, I'll share the process of creating this original talking character and my thoughts along the way. How I wanted to create our own character that talks About incorporating AI to easily create a memorable video If you're like me, then definitely keep reading! Background The request from the art director overseeing the event's overall creative direction was to "re-edit the video used in our corporate website 's key visual and turn it into a one-minute opening movie." But just re-editing existing footage felt a bit too familiar for employees, and I wanted something more eye-catching, something that instantly energizes the event from the start. That's when I turned to our AI chatbot in our company Slack. I thought, what if I used AI to bring our chatbot to life as a surprise? A personified version in the video could definitely catch everyone's eye. AIs Used To create a talking character, I used the following three AI tools: Adobe Firefly (character image generation) TTSMaker (text-to-speech conversion) Runway (character animation with voice) From here on, I'll show you how I used these AIs to make the video. 1. Character Image Generation Adobe Firefly An image generation AI tool provided by Adobe. Since Adobe Firefly is trained on copyright-free images like Adobe Stock images, it can be used without worrying about copyright issues. Many of the well-known image generation AIs claim to be copyright-free, but some operate in a gray area, such as being able to generate images that closely resemble anime characters. Since this was a corporate event, we wanted to avoid any copyright concerns, so we chose an AI that can be used without such worries. Adobe Firefly Image Generation This character is displayed on our company's Slack with an icon like this: From the icon we established that: The pink color of the icon led us to give her pink hair She's an AI chatbot so we wanted to give her a smart, digital vibe in her overall look And by doing so we expanded the character's image. On-screen Operations When you open Firefly, you'll see a screen like this. Roughly speaking, you enter the prompts to generate an image in the input area below, and then use the menu on the left to adjust the aspect ratio, composition, style, tone, etc. This time, after much trial and error, I generated the character using the prompt "3D character, female, pink hair, white background, upper body, white simple digital clothing, facing forward." Mass Generation Once the prompt was more solidified, it was a matter of luck whether I would get a good result, so I just generated 100 to 200 sheets. Selection To give the event a fresh start, I aimed for a character that felt like an "AI operator" with an official and trustworthy vibe. So, I filtered out these images: The ones looking too young Having weird clothing Scary faces Selected Image After carefully narrowing down the options, I chose this image because it felt cool yet approachable. 2. Text-to-Speech Conversion TTSMaker An AI voice generator that converts typed text into speech. There are many similar AI-based text-to-speech services, but many of them are paid services or require crediting the source if they are free. I went with this tool because it's free to use without any credit attribution. TTSMaker On-screen Operations When you open TTSMaker, you'll see a screen like this. The procedure is as follows: Select a language Enter the text you want to convert to speech Listen to sample voices and choose the tone Set the details like speed and pitch Convert I aimed for a voice that felt like an "AI operator" with an official and trustworthy vibe. After comparing sample voices, I chose "406 - Yuki Tsumiyuki -🇯🇵 Japanese female" and had her read the line: "Mochirondesu. Cho-honbukai o hajimemasu." ("Of course. We'll now start the CHO All-Hands Meeting.") Actual Audio https://www.youtube.com/watch?v=r4Zw2by669I 3. Character Animation with Voice Runway An AI-powered tool that makes it easy to create and edit high-quality videos. I chose this tool for its "Lip Sync Video" feature, which syncs character images with voice audio. Runway On-screen Operations 1.Open "Lip Sync Video" under the "Generative Audio" section, then drag and drop the previously created character image. 2. Check that the facial range of the character image is recognized correctly, and if there are no problems, click "Upload Audio." 3. Drag and drop the previously generated voice, then click "Generate." Generated Video https://www.youtube.com/watch?v=usY4yB9Z1YA A video of the character speaking in sync with the audio was generated. Bonus Tip 1 https://www.youtube.com/watch?v=raYsnhwZONo By uploading a song, the character can actually be made to sing. Bonus Tip 2 https://www.youtube.com/watch?v=3edVClgoLug In this way, it's also possible to make a still image of a real person talk. At a recent internal study session (held in Tokyo), the speaker was suddenly unable to make the business trip from Osaka to attend, so we asked him to prepare still images and voice memos, and we used this AI-generated video to make our presentation. 4. Final Touches That's all for how to create a talking character video. What comes next is a little something extra. Actually, getting a character to talk using AI has become so easy that anyone can do it with the AIs I introduced above. But since I'm a designer in the Creative Office, I wanted to go beyond what any other department could produce. So as a final touch, I added a 3D balloon-style CG made in Illustrator, gave it a soft floating motion in After Effects, and composited it into the video. This added a polished finish and added the value only a creator could bring. Final Video https://www.youtube.com/watch?v=pVj_UQ_3-tg By adding one final touch, the video evolved from just a talking character to something with added graphical expression. Conclusion How it looked when projected at the event Each creative generative AI has its own characteristics and limitations. By understanding and combining those characteristics, I was able to create a level of quality that wouldn't have been possible with just one tool. On the day of the event, this character was projected on a large screen, and I was grateful to hear comments like: "That was amazing! How did you make it?" "The quality was so high I thought it was outsourced!" It felt great to see the impact it made, just as I aimed for. All tools used are simple enough that even non-creators can use it. As long as you have an idea, you can create memorable videos like this. If this article sparked your interest, definitely give it a try! Thank you for reading till the end.
アバター
Introduction Hello! I'm Momoi ( @momoitter ), a designer at KINTO Technologies. I belong to the Creative Office where I work on projects like the corporate website and Kumobii (KINTO's official mascot) related sites, incorporating cutting-edge web design along the way. In November 2024, our company hosted an internal event called the CHO All-Hands Meeting, and I was in charge of creating the opening movie for it and I used three generative AIs to create a female character who kicks off the event with a voice announcement. Actual Video https://www.youtube.com/watch?v=pVj_UQ_3-tg When asked at the event start, "Are you ready?", she answers "Of course!" In this article, I'll share the process of creating this original talking character and my thoughts along the way. How I wanted to create our own character that talks About incorporating AI to easily create a memorable video If you're like me, then definitely keep reading! Background The request from the art director overseeing the event's overall creative direction was to "re-edit the video used in our corporate website 's key visual and turn it into a one-minute opening movie." But just re-editing existing footage felt a bit too familiar for employees, and I wanted something more eye-catching, something that instantly energizes the event from the start. That's when I turned to our AI chatbot in our company Slack. I thought, what if I used AI to bring Sherpa to life as a surprise? A personified Sherpa in the video could definitely catch everyone's eye. AIs Used To create the original talking character, I used the following three AI tools: Adobe Firefly (character image generation) TTSMaker (text-to-speech conversion) Runway (character animation with voice) From here on, I'll show you how I used these AIs to make the video. 1. Character Image Generation Adobe Firefly An image generation AI tool provided by Adobe. Since Adobe Firefly is trained on copyright-free images like Adobe Stock images, it can be used without worrying about copyright issues. Many of the well-known image generation AIs claim to be copyright-free, but some operate in a gray area, such as being able to generate images that closely resemble anime characters. Since this was a corporate event, we wanted to avoid any copyright concerns, so we chose an AI that can be used without such worries. Adobe Firefly Image Generation "Sherpa," the original inspiration for this character, is displayed on our company's Slack with an icon like this: From the icon we established that: The "pa" sound in Sherpa inspired a feminine feel The pink color of the icon led us to give her pink hair She's an AI chatbot so we wanted to give her a smart, digital vibe in her overall look And by doing so we expanded the character's image. On-screen Operations When you open Firefly, you'll see a screen like this. Roughly speaking, you enter the prompts to generate an image in the input area below, and then use the menu on the left to adjust the aspect ratio, composition, style, tone, etc. This time, after much trial and error, I generated the character using the prompt "3D character, female, pink hair, white background, upper body, white simple digital clothing, facing forward." Mass Generation Once the prompt was more solidified, it was a matter of luck whether I would get a good result, so I just generated 100 to 200 sheets. Selection To give the event a fresh start, I aimed for a character that felt like an "AI operator" with an official and trustworthy vibe. So, I filtered out these images. Look too young Weird clothing Scary face Selected Image After carefully narrowing down the options, I chose this image because it felt cool yet approachable. 2. Text-to-Speech Conversion TTSMaker An AI voice generator that converts typed text into speech. There are many similar AI-based text-to-speech services, but many of them are paid services or require crediting the source if they are free. I went with this tool because it's free to use without any credit attribution. TTSMaker On-screen Operations When you open TTSMaker, you'll see a screen like this. The procedure is as follows: Select a language Enter the text you want to convert to speech Listen to sample voices and choose the tone Set the details like speed and pitch Convert I aimed for a voice that felt like an "AI operator" with an official and trustworthy vibe. After comparing sample voices, I chose "406 - Yuki Tsumiyuki -🇯🇵 Japanese female" and had her read the line: "Mochirondesu. Cho-honbukai o hajimemasu." ("Of course. We'll now start the CHO All-Hands Meeting.") Actual Audio https://www.youtube.com/watch?v=r4Zw2by669I 3. Character Animation with Voice Runway An AI-powered tool that makes it easy to create and edit high-quality videos. I chose this tool for its "Lip Sync Video" feature, which syncs character images with voice audio. Runway On-screen Operations 1.Open "Lip Sync Video" under the "Generative Audio" section, then drag and drop the previously created character image. 2. Check that the facial range of the character image is recognized correctly, and if there are no problems, click "upload audio." 3. Drag and drop the previously generated voice, then click "Generate." Generated Video https://www.youtube.com/watch?v=usY4yB9Z1YA A video of the character speaking in sync with the audio was generated. Bonus Tip 1 https://www.youtube.com/watch?v=raYsnhwZONo By uploading a song, the character can actually be made to sing. Bonus Tip 2 https://www.youtube.com/watch?v=3edVClgoLug In this way, it's also possible to make a still image of a real person talk. At a recent internal study session (held in Tokyo), the speaker was suddenly unable to make the business trip from Osaka to attend, so we asked him to prepare still images and voice memos, and we used this AI-generated video to make our presentation. 4. Final Touches That's all for how to create a talking character video. What comes next is a little something extra. Actually, getting a character to talk using AI has become so easy that anyone can do it with the AIs I introduced above. But since I'm a designer in the Creative Office, I wanted to go beyond what any other department could produce. So as a final touch, I added a 3D balloon-style CG of Sherpa made in Illustrator, gave it a soft floating motion in After Effects, and composited it into the video. This added a polished finish and added the value only a creator could bring. Final Video https://www.youtube.com/watch?v=pVj_UQ_3-tg By adding one final touch, the video evolved from just a talking character to something with added graphical expression. Conclusion How it looked when projected at the event Each creative generative AI has its own characteristics and limitations. By understanding and combining those characteristics, I was able to create a level of quality that wouldn't have been possible with just one tool. On the day of the event, this character was projected on a large screen, and I was grateful to hear comments like: "That was amazing! How did you make it?" "The quality was so high I thought it was outsourced!" It felt great to see the impact it made, just as I aimed for. All tools used are simple enough that even non-creators can use it. As long as you have an idea, you can create memorable videos like this. If this article sparked your interest, definitely give it a try! Thank you for reading to the end.
アバター
はじめに こんにちは。KINTOテクノロジーズ(以下KTC)プラットフォームグループ Platform Engineeringチームで内製ツールの開発・運用を担当している山田です。普段はアプリケーションエンジニアとして、JavaとSpring Bootを使ったバックエンドや、JavaScriptとReactを使ったフロントエンドでCMDB(Configuration Management Database)の開発をしています。ここ1年ほどは生成AIブームに乗り、CMDBにRAGやText-to-SQLを活用したチャットボットの開発にも取り組んでいます。 こちらはText-to-SQLについて書いた前回の記事です。ご興味ある方はぜひご覧ください! https://blog.kinto-technologies.com/posts/2025-01-16-generativeAI_and_Text-to-SQL/ 今回は生成AI関連で、2025年5月20日にGAされたSpring AIを試してみた内容をご紹介したいと思います。 Spring AIの概要 2025年5月20日にGAされたSpringエコシステムの一つで、生成AI関連のフレームワークです。生成AI分野ではPythonベースのLlamaIndexやLangChainが有名ですが、既存のSpringアプリケーションに生成AI機能を追加したい場合や、Javaで生成AI開発をやってみたい方にとっての選択肢になるかと思います。 https://spring.pleiades.io/spring-ai/reference/ 検証内容 今回はSpring AIを使って、以下の2つの機能を実装してみました。 チャット機能(LLM対話) : AWS BedrockのClaude 3.7 Sonnetモデルを利用した対話機能 Embedding機能 : CohereのエンベディングモデルとChroma(ベクトルデータベース)を組み合わせた文書埋め込み・類似文書検索機能 技術スタック Spring Boot 3.5.0 Java 21 Spring AI 1.0.0 Gradle Chroma 1.0.0 AWS Bedrock LLMモデル:Anthropic Claude 3.7 Sonnet Embeddingモデル:Cohere Embed Multilingual v3 環境構築 依存関係の設定 まずは build.gradle にSpring AIを使うための依存関係を追加します。 plugins { id 'java' id 'org.springframework.boot' version '3.5.0' id 'io.spring.dependency-management' version '1.1.7' } java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } ext { set('springAiVersion', "1.0.0") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.ai:spring-ai-starter-model-bedrock-converse' implementation 'org.springframework.ai:spring-ai-starter-model-bedrock' implementation 'org.springframework.ai:spring-ai-bedrock' implementation 'org.springframework.ai:spring-ai-starter-vector-store-chroma' testImplementation 'org.springframework.boot:spring-boot-starter-test' } dependencyManagement { imports { mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}" } } アプリケーション設定 application.yml でAWS Bedrockの認証情報やモデル選択、ベクトルストアの接続情報を設定します。 spring: application: name: spring-ai-sample ai: bedrock: aws: region: ap-northeast-1 access-key: ${AWS_ACCESS_KEY_ID} secret-key: ${AWS_SECRET_ACCESS_KEY} session-token: ${AWS_SESSION_TOKEN} converse: chat: options: model: arn:aws:bedrock:ap-northeast-1:{account_id}:inference-profile/apac.anthropic.claude-3-7-sonnet-20250219-v1:0 cohere: embedding: model: cohere.embed-multilingual-v3 model: embedding: bedrock-cohere vectorstore: chroma: client: host: http://localhost port: 8000 initialize-schema: true 設定はこれだけです。あとはアプリケーション内で必要なクラスを簡単にDIできるようになります。 実装例 1. 生成AI(LLM)との対話機能 Spring AIでは ChatClient インターフェースを使って、簡単にLLMとの対話を実装できます。 サービス層の実装 ChatServiceクラスでLLMとの対話機能を実装します。 @Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String generate(String message) { return this.chatClient.prompt(message).call().content(); } } ChatClient.Builder をDIすることで、設定ファイルに基づいたクライアントを自動的に構築できます。 コントローラーの実装 RESTコントローラーでAPIエンドポイントを提供します。 @RestController public class ChatController { private final ChatService chatService; private final EmbeddingService embeddingService; public ChatController(ChatService chatService, EmbeddingService embeddingService) { this.chatService = chatService; this.embeddingService = embeddingService; } @PostMapping("/generate") public ResponseEntity<String> generate(@RequestBody ChatRequest request) { return ResponseEntity.ok(chatService.generate(request.getMessage())); } } これで /generate エンドポイントにメッセージをPOSTすると、Claude 3.7 Sonnetによる応答を受け取ることができます。 以下が呼び出し例です。 curl -X POST http://localhost:8080/generate -H "Content-Type: application/json" -d '{"message": "こんにちわわ"}' こんにちは!何かお手伝いできることはありますか? 2. Embedding検索機能の実装 次に、ドキュメントをベクトル化して検索する機能を実装します。 Embeddingサービスの実装 このサービスでは、サンプルテキストを Document オブジェクトとしてベクトル化し、Chromaに保存します。そして「Spring」というクエリに対して類似度の高いドキュメントを検索します。この処理の裏側では、Cohere Embed Multilingual v3モデルによるテキストのベクトル変換が行われています。 @Service public class EmbeddingService { private final VectorStore vectorStore; public EmbeddingService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public List<Document> embed() { List<Document> documents = List.of( new Document("Spring AI is a framework for building AI applications with the familiar Spring ecosystem and programming model."), new Document("LlamaIndex is a data framework for LLM applications to ingest, structure, and access private or domain-specific data."), new Document("LangChain is a framework for developing applications powered by language models through composability.") ); vectorStore.add(documents); return vectorStore.similaritySearch(SearchRequest.builder().query("Spring").topK(5).build()); } } APIエンドポイントの実装 @GetMapping("/embedding") public ResponseEntity<List<Document>> embedding() { return ResponseEntity.ok(embeddingService.embed()); } この実装により、 /embedding エンドポイントにアクセスすると、サンプルドキュメントのベクトル化と検索が行われ、その結果が返されます。 以下が呼び出し例です。「Spring」という言葉が入ったドキュメントの類似度(score)が一番高い結果になっていますね。 curl http://localhost:8080/embedding [ { "id": "af885f07-20c9-4db4-913b-95406f1cb0cb", "text": "Spring AI is a framework for building AI applications with the familiar Spring ecosystem and programming model.", "media": null, "metadata": { "distance": 0.5593532 }, "score": 0.44064682722091675 }, { "id": "5a8b8071-b8d6-491e-b542-611d33e16159", "text": "LlamaIndex is a data framework for LLM applications to ingest, structure, and access private or domain-specific data.", "media": null, "metadata": { "distance": 0.6968217 }, "score": 0.3031783103942871 }, { "id": "336b3e07-1a70-4546-920d-c4869e77e4bb", "text": "LangChain is a framework for developing applications powered by language models through composability.", "media": null, "metadata": { "distance": 0.71094555 }, "score": 0.2890544533729553 } ] さいごに 簡単ではありますが、今回はSpring AIを使った生成AI機能についてご紹介しました。Spring AIを試してみて、Javaのシステムに生成AI関連の処理を組み込む場合には選択肢の一つになるかなと思いました。また、今回ご紹介したチャット機能やEmbedding検索機能を組み合わせることで、RAG機能も比較的簡単に実装できると感じています。 現時点では、生成AI関連の機能やエコシステムはLlamaIndexやLangChainといったPython系フレームワークの方が充実している印象ですが、Spring AIはリリースされたばかりなので今後の機能追加に期待したいと思います!
アバター
Introduction Hello! I'm Alex, a Generative AI Engineer on the Generative AI Development Project team at KINTO Technologies. Lately, there's been a growing interest in building multi-agent systems using large language models (LLMs). So in this article, I'd like to share a case where I built a Supervisor-style multi-agent system in less than 30 minutes using the latest LangGraph update, langgraph_supervisor. This system allows a central supervisor agent to coordinate and manage multiple specialized agents. It can be applied to a wide range of use cases, from automating workflows to generating customizable proposal scenarios. What is LangGraph? LangGraph is a Python library designed to simplify the development of AI agents and Retrieval-Augmented Generation (RAG) systems. Combined with LangChain, complex workflows and tasks can be designed and implemented efficiently. It supports state persistence, tool invocation, and features like human-in-the-loop and post-task validation through a centralized persistence layer which served as the foundation for this Supervisor-style system. What is langgraph-supervisor? langgraph-supervisor is a Python library for building hierarchical multi-agent systems using LangGraph, which was recently released by LangChain. A central supervisory agent coordinates task assignments and facilitates communication among specialized agents. This enables the development of adaptable systems that can tackle complex tasks with ease and efficiency. Key Features Supervisor agent creation: Manages multiple specialized agents and orchestrates the overall workflow. Tool-based agent handoff mechanism: Provides a mechanism for achieving smooth communication between agents. Flexible message history management: Enables easy control over conversation flow by managing message history dynamically. @ card What is a Supervisor-style Multi Agent System? Source: https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/ A Supervisor-style multi-agent system is a structure where a central controlling agent called the Supervisor coordinates with tool-enabled LLM agents, deciding which agent to call, when to do so, and what arguments to pass along. Building a Multi-Agent System with langgraph-supervisor The following steps outline how to build a multi-agent system using langgraph-supervisor. In this example, we’ll create a system that recommends a car and engine type based on simple, anonymized customer information. Additionally, the agent will retrieve car and engine information from the locally stored "Vehicle_Information.csv" file. Environment Setup Prepare Azure OpenAI API keys This time, we'll be using GPT-4o via Azure OpenAI. Depending on your situation, you can also use the OpenAI API, or Anthropic API, etc. And set Azure OpenAI API keys and endpoint as environment variables. os.environ["AZURE_OPENAI_API_KEY"] = "YOUR API KEY" os.environ["AZURE_OPENAI_ENDPOINT"] = "YOUR ENDPOINT" os.environ["AZURE_OPENAI_API_VERSION"] = "YOUR API VERSION" os.environ["AZURE_OPENAI_DEPLOYMENT"] = "YOUR DEPLOYMENT NAME" Install LangGraph and LangChain pip install langgraph pip install langchain pip install langchain_openai Install langgraph-supervisor pip install langgraph-supervisor Setup of tool functions used by LLM and each Agent Define the model using Azure OpenAI's GPT-4o. Next, define the tool functions for agents. from langchain_openai import AzureChatOpenAI import pandas as pd # LLMの初期化 llm = AzureChatOpenAI( azure_deployment=os.getenv("AZURE_OPENAI_DEPLOYMENT"), api_version=os.getenv("AZURE_OPENAI_API_VERSION"), ) car_information_path = "/xxx/xxx/車両情報.csv" # ツール関数の定義例 # CSVファイルから車両情報を読み込み、候補を生成する例 def get_car_features(): """The python code to get car information.""" path = car_information_path # CSVファイルのパスを指定 df = pd.read_csv(car_information_path) car_features = df[["車種名", "ボディタイプ", "説明"]].drop_duplicates() return car_features.to_dict(orient="records") # 選択された車種に対してエンジンタイプを抽出する例 def get_engine_type(selected_car): """The python code to get engine type and engine information.""" path = car_information_path df = pd.read_csv(car_information_path) engine_types = list(df[df["車種名"] == selected_car]["エンジンタイプ"].unique()) return engine_types, "エンジンに関する補足情報" Defining Each Agent We use LangGraph's "create_react_agent" to define each specialized agent (car model recommendation, engine type selection). from langgraph.prebuilt import create_react_agent # 車種推薦エージェント car_agent = create_react_agent( model=llm, tools=[get_car_features], name="car_agent", prompt=""" # Instructions Based on the recommended pattern, choose a car model to recommend and explain the reasoning in about 200 characters. """ ) # Engine type selection agent engine_agent = create_react_agent( model=llm, tools=[get_engine_type], name="engine_agent", prompt=""" # Instructions Choose the most suitable engine type for the recommended car and explain the reasoning in about 200 characters. """ ) Defining the Supervisor Create a Supervisor to oversee each agent and generate a final recommendation based on customer information. One improvement we made here was modularizing the prompt input to the Supervisor, allowing for greater versatility. By changing the contents of the role and task variables and the associated agent, the system can easily be used for other tasks. from langgraph_supervisor import create_supervisor # Create the contents of each module in the prompt role = "車のセールスマン" task = "顧客情報をもとに、最適な車とエンジンタイプを提案してください。" guideline = """ - 出力は必ず上記JSON形式で行ってください。 - 各根拠は200文字程度の豊かな文章で記述してください。" """ output_format = """ { "提案内容": { "提案する車種", "車種の根拠", "選択したエンジンタイプ", "エンジン選定理由" } } """ # Create a prompt system_prompt = f""" ## Role あなたは優れた{role}です。 ## Task {task} ## Guidelines {guideline} ## 最終生成物のフォーマット {output_format} """ # Create the Supervisor workflow = create_supervisor( # 先ほど作成したエージェントと紐づく [car_agent, engine_agent], model=llm, prompt=system_prompt ) # Compile the graph app = workflow.compile() Example Run from langchain.schema import HumanMessage import time # Example of customer information (add more details as needed) customer_info = "顧客情報: Age 35, prioritizes fuel efficiency, currently drives a compact car. # Example run start_time = time.time() result = app.invoke({ "messages": [ {"role": "user", "content": customer_info} ] }) end_time = time.time() # Display the final output print("最終提案:") print(result["messages"][-1].content) print("実行時間: {:.3f}秒".format(end_time - start_time)) Response from the Supervisor 最終提案: { "提案内容": { "提案する車種": "ハイブリッド技術を採用した最新コンパクトカー", "車種の根拠": "顧客は現在コンパクトカーに乗車中であり、燃費性能の良さを重視しています。 ハイブリッド技術を採用した車は、燃料使用の効率が高く、経済的負担や環境への配慮が優れています。 また、ハイブリッドシステムは短距離や都市部での走行にも最適です。 そのため、最新のハイブリッドコンパクトカーが最適な選択肢となります。", "選択したエンジンタイプ": "ハイブリッドエンジン", "エンジン選定理由": "ハイブリッドエンジンは燃費性能が非常に優れており、顧客のニーズである燃料効率を最優先に考慮しています。 さらに、コンパクトカーとの相性も良く、都市部での利用や日常の移動において高いパフォーマンスを発揮します。 このエンジンの選択は、快適性、経済性、環境性能を全て満たします。" } } Run Time: 21.938 seconds Impressions After Building the System Rapid Prototyping The complex inter-agent coordination and state management typically required in LangGraph were implemented with simple code using langgraph_supervisor, and a prototype was successfully built in under 30 minutes. A pretty impressive result. Flexibility and Scalability Each agent can be implemented with its own prompt and tool functions, allowing for easy customization to meet specific business needs. In addition, since state management is handled centrally, future improvements and feature additions are expected to be carried out smoothly. We Are Hiring! KINTO Technologies is looking for passionate colleagues to help drive AI adoption in our businesses. We're happy to start with a casual interview. If you’re even slightly interested, feel free to reach out via the link below or through X DMs . We look forward to hearing from you!! @ card Thanks for reading all the way through!
アバター
My name is Yuya Sakamaki from KINTO Technologies. I am usually involved in anything from data analysis to proposing strategies, and developing features using machine learning. Previously, I was in charge of AI functional development at Prism Japan . I conducted an internal comparison test of Cursor and GitHub Copilot, and I would like to share the results with you. Premise At KINTO Technologies, there's a policy that allows GitHub Copilot to be used as a standard tool. I will test whether using Cursor Editor can further improve productivity. Cursor An AI-powered code editor, created as a fork of Visual Studio Code. Github Copilot It’s a tool co-developed by GitHub and OpenAI, available as a plug-in for various IDEs such as Visual Studio Code, JetBrains, Eclipse, and more. Disclaimer This article contains many of my own opinions. This is not meant to undermine the usefulness of Cursor. Tested Plans Copilot Enterprise ... $39 / month Cursor Business ... $40 / month Conclusion As of February 2025 , I feel that there is almost no difference in functionality between GitHub Copilot and Cursor. As of December 2024 , I would have liked to recommend Cursor, but with the incredible speed at which Copilot has progressed, I no longer feel the need to choose Cursor Editor again. Evaluation Period December 2024 to February 2025 Comparison Table (Copilot vs. Cursor) I have put together a table of some of the most common points that came up in the discussions using Copilot and Cursor together. The details of each item will be provided later. Item GitHub Copilot Cursor UI/Operability - Inline suggestions and chat ability in VSCode - Many operations by right-clicking - AI-powered shortcuts are displayed instantly - Inline menus are easy to understand QuickFix (completion of import, etc.) - VSCode standard quick fixes are powerful - Sometimes it is unstable and doesn't work - Fix in Composer etc., is a little troublesome Automatic reflection of similar corrections - Need to start writing something first - Tabs predict and suggest the next correction to make (useful for making corrections to multiple places) Model selection - Can be changed, but the variety is limited - Abundant additions and selections (multiple AI models can be used) Rule file - Can be done by changing the settings in .github/copilot-instructions.md - The .cursorrules file enables you to apply rules per project Loading documents (such as @Docs) - Supported by plug-ins, etc. - The @Docs feature enables you to load documents for frameworks and libraries Agent functions - Similar functions available in Copilot Chat / Copilot CLI - Automatic execution in agent mode, try & error Automatic test code generation - Generation is possible (although accuracy varies) - Can be generated in the same way, but the accuracy is questionable. Corrections are needed. Price (Business/Enterprise) $39 $40 *The content of the evaluation is subjective as of February 2025. Pros of Cursor (when compared to GitHub Copilot) AI-powered shortcuts are displayed immediately Since AI-powered shortcuts are displayed automatically when you select a code, the need to remember special operations can be eliminated. Is there something similar in Copilot? Inline chat is possible. However, there is no COMPOSE mode to reference other files, and you need to call it up from the right-click menu, so it requires a little bit of effort. Suggests similar corrections together When you want to apply similar corrections to multiple places, Cursor will predict "where and how to fix next" in order on the tab. Is there something similar in Copilot? It doesn't make predictions until you start writing something first, so it is difficult for it to provide bulk suggestions as smoothly as with Cursor. A similar function is now available in Copilot *There were some changes as I was writing this article. Flexibility with selecting and adding models Cursor is designed to make it easy to add and select any model you want. Is there something similar in Copilot? Copilot Chat also enables model switching, but the scope of what can be done is currently limited. The @Docs feature enables you to load documents for frameworks and libraries Having it read the documents in advance will improve the accuracy of its answers. Is there something similar in Copilot? It is possible to achieve something similar using plug-ins, but the standard features are not that extensive. Automatic execution of try & error in agent mode In agent mode within Cursor, the AI will automatically execute commands under a certain level of authority and perform trial and error. Is there something similar in Copilot? Similar features are starting to appear in Copilot CLI and Copilot Chat, but they still require some fine tuning. While still just a preview version, agent mode is now available in Copilot . Cons of Cursor (when compared to GitHub Copilot) Import completion is a bit troublesome VSCode + Copilot will automatically complete imports, but Cursor will not import unless you hover the mouse cursor over it and select a quick fix. Copilot offers greater stability and overall superiority Automatic test code generation is not very accurate Automatically generated tests often don’t pass on the first run and usually require some adjustments. The same is true for Copilot, but I didn't get the impression that Cursor was much of an improvement. There are still many parts that are not suitable for operation Depending on the app being developed, Cursor contained more AI functions than needed, which confused some team members. The Difference between Cursor's "Composer" and "Chat" Cursor is divided into two main modes: Composer and Chat . Since there are differences in usage and the handling of context, I have put together a quick summary of the results of the internal comparison. Functional Items Composer Chat Context understanding Automatically links to the current file. Auto-suggests related files. Initially the context is empty. Must be shared manually as needed Main uses Mainly code generation and editing. Easy to correct suggested content immediately using inline Mainly used for receiving general questions and explanations. Suitable for longer exchanges History management Auto-saves in the control panel Chat history is selectable UI Also usable in inline menu. Can also be operated from the side menu Side menu only Code block execution Not possible Possible (execute commands via Chat in the side menu) When to use Composer Code corrections with simple context When you want to generate and edit code quickly When to use Chat Code corrections with larger context Analyzing error messages, general programming questions When you want to work while maintaining context for a long period of time Conclusion Again As of February 2025, there are no huge functional differences between Copilot and Cursor In fact, Copilot's progress has been faster than expected, and it seems that within a few months, Copilot filled the gap that I thought Cursor was going to win as of December 2024. There are certainly pros to using Cursor As an AI-powered IDE, Cursor still has its strengths, such as dedicated shortcuts and agent mode. However, since Copilot has also been making progress in supporting these features, the pros of newly introducing it may be somewhat diminishing. Which one will you use in the end? If GitHub Copilot is already available as standard equipment within the company, you will likely be able to get by with just Copilot for the time being. That said, if you're wanting to embrace large-scale code corrections and chat-driven development, it's worth giving Cursor's Composer / Chat features a try, even though the Github Copilot AI agent is currently available to VSCode Insiders. Since they are almost the same in terms of price, it is good idea to choose based on the UI and ease of use. Development App Prism Japan - iOS Prism Japan - Android References Cursor - The AI Code Editor Getting code suggestions in your IDE with GitHub Copilot The above are the results of my extensive use of Cursor and GitHub Copilot. The speed of product updates is accelerating, so I would like to continue to test them regularly.
アバター
Hello! My name is Kita and I work on a travel media platform called Prism Japan (for Web, iOS, and Android) at KINTO Technologies. Since November 2024, I've been working on social media management for Prism Japan. Going beyond traditional advertising, the goal is to actively use social media as a way to grow brand awareness and create a channel to attract new customers. So here's a quick look back at how things went over the past four months! What We Achieved Let’s begin by looking at what we’ve accomplished while managing our social media accounts. Creating short-form videos Short-form content, especially on TikTok and Instagram, is a key element in today's social networking environment. The algorithms are designed to show short videos to users beyond your followers, and depending on how viewers react, your reach can grow quickly. Increasing awareness using social media was a required subject, so we started by imitating popular videos that were going viral in similar categories. Based on those ideas, I picked up editing skills by experimenting with different editing tools. Here are the main tools I used: Tool Name Features Canva Has a free plan available at no cost Images and videos can be freely edited and easily shared with co-editors, making management simple. There was only one time the server crashed during editing, and I had to start over. Adobe Express Has a free plan available at no cost It's not much different from Canva, but I personally prefer this one because the server is stable that I never experienced any crashes while working. Capcut Has a free plan available at no cost Its main feature is video editing. It offers tons of social media-friendly features, such as text-to-speech, 3D image conversion, and the ability to use fonts that are commonly seen on social media. Note: The watermark in the free version doesn’t go over well on Instagram. Depending on your goals and preferences, you might favor one tool over another, but generally, all of them can get the job done—even with their free versions. Gaining impressions Across all platforms, we were able to gain a lot of impressions. Let's take a look at the period from November 1, 2024, when the full-scale operations started, to March 17, 2025 (the time of writing). X Impressions increased 293% to 62,500. Tiktok Since we only started using TikTok during this period, there’s no earlier data to compare, but our videos have been viewed 440,000 times so far. Compared to other platforms, it appeared to steadily accumulate views over time. Instagram Reach increased by 90%, reaching 50,000. *Note: Accurate comparisons for views and interactions couldn’t be made due to the absence of data prior to August 2024. This is especially noticeable on TikTok and Instagram, with peaks occurring around the New Year period. We think this growth was driven by a combination of people being on holiday and a well-timed post introducing shrines that were perfect for the New Year's visits. When comparing the period before and after full-scale social media operations, factors like posting more frequently and simply being active on the platforms definitely played a role. However, what really helped boost views was tapping into trends like video format, seasonality, and timely topics. I'd say these efforts helped "raise awareness" to some extent. Communication with users On X in particular, we were able to discover and connect with new audiences. Previously, we hadn't taken any active steps like following users or replying to posts. But since X prioritizes engagement in its algorithm, we began directly interacting with users interested in tourism. As a result, one user not only installed our app, but also posted a review after actually visiting one of the spots we featured! Even better, they gave us some great content ideas. If you have a product or service, I definitely recommend reaching out actively. It can lead to both new users and valuable feedback. What We Couldn't Achieve Next, let's take a look at what didn't go so well. Did not result in app installations or website traffic As mentioned earlier, we saw a noticeable increase in views and impressions. However, those numbers didn't translate into app installations or visits to the website. Below, I'll break down the results by platform. The evaluation period is set from November 1, 2024, when the full-scale operations started, to March 17, 2025. The following data reflects the number of app installs measured by AppsFlyer during the period. Note: We've decided not to disclose the specific number of installations. Tiktok & Number of app installs First, we investigated whether there was any correlation between TikTok metrics and app install numbers. Number of views This chart was shown earlier, but here we compared the number of TikTok views with the app installs. As you can see from a quick comparison of the graphs, there doesn't appear to be a clear correlation. For example, even though views peaked on January 2, 2025, there was no noticeable increase in installs. On the other hand, January 29, 2025 saw a spike in installs when there was no change in views. This also leads us to conclude that there is no correlation between TikTok views and app installs. Profile views Next is the number of profile views. We assumed that users who visited our profile showed some level of interest. Thankfully, profile views have been trending upward, but for now, it doesn't seem like they're having much of an impact on app installs. Likes What about the number of likes? At a glance, the trend in likes seemed to follow a similar pattern to views. Similarly, on January 2, 2025, when we got the highest number of likes, app installs didn't change. And on January 29, 2025, when installs spiked, likes didn't seem to contribute to that increase. Comments The total number of comments was 81, which is relatively low considering the number of views. There were even days with zero comments. The number of comments on February 17, 2025 was slightly higher at 4, but there was no change in installs. Shares The trend in shares was also similar to that of views and likes. We noticed firsthand how TikTok's algorithm tends to promote videos that get a lot of likes and shares, placing them on the For You feed, which in turn helps boost views. Instagram & Number of app installs Next up is Instagram. Views Similarly, large peaks around the New Year holiday. As mentioned earlier, that timing didn't lead to any noticeable change in app installs. On the other hand, there was some correlation over multiple days. Overall, the correlation wasn't strong, but compared to TikTok, there might be slightly more of a relationship between views and installs. Also, the reach (i.e. number of unique users) showed a similar trend as above, so I'll skip the details here. Interactions Lastly, interaction. On Instagram, this includes likes, saves, comments, and shares. Again, no correlation was found with app installs. Summary Overall, there was no correlation between social media activity and app installs. So for now, it's hard to say that our social media efforts drove user acquisition. However, this analysis is based on correlation only, so it's possible that some viewers went on to install the app later. But to be honest, it's challenging to measure that accurately. Traffic to the website We expected that social media would also contribute to attracting customers as a route other than SEO. In reality, we were able to drive some visits through Instagram stories and posts on X, but the total number was only around 300, which is quite small. We believe this is mainly due to the low follower count on X which limited our reach, and the lack of Instagram story viewers. Impressions increased, but follower growth was limited X: 425 followers On X, we were still lacking in follows and actions such as likes. Part of the reason is that we took a slower, more cautious approach while juggling other platforms over a long period of time. I think it's best to be more proactive with follows and likes early on to gain traction faster. But be aware that excessive following can lead to shadowbans or account suspensions. Tiktok: 155 followers Despite the high number of views, we barely gained any followers. I think there are two main reasons for this: One is that the content was too generic. I think it lacked originality and creativity. There wasn't anything that made users feel they had to follow the account or risk missing out. Second, the target was unclear due to the nature of Prism Japan targeting a wide range of people who are interested in travel. But on TikTok, we didn't narrow down the demographic (age, gender), so information ended up reaching a wide range of people. Instagram: 6,667 followers At first glance, this number may seem high, but since we stopped running ads, follower growth has been on the decline. It's often said that followers gained through ads tend to be less engaged. On top of that, our post style shifted significantly after we paused the ads, which may have further accelerated the drop-off in engagement. Key Characteristics & Improvement Points by Platform Here, I'll break down the strengths of each platform and post performances we observed during actual operations. TikTok: Content can go viral even without a large number of followers Top-performing post: 3 hidden spots in Tokyo perfect for a girls' trip! We intentionally set a very specific target audience for this post: "Girls traveling around Tokyo." This content guides users through the actual process of using the app to discover hidden gems. The spots featured are hidden gems with themes like "overseas-inpired," "anime-related," and "en-musubi (shrines dedicated to love and relationships)." As a result, 94% of traffic came from search, with queries like "Tokyo places to hang out for two girls" or "Tokyo sightseeing." We believe narrowing the target and showcasing true hidden spots played a key role in its success. Lowest-performing post: The Hibiya illuminations were so beautiful! The video that didn't perform well was the one about the Hibiya illuminations. We actually visited the location, captured the lights and ambiance, and tried to create an engaging video by syncing with the background music. Despite these efforts, the video barely got any views. The likely reason is we entered a highly competitive area filled with high-quality content under popular keywords like "Christmas" and "illuminations," making it hard to stand out. Summary Comparing the two, we found that the successful post had a clear target audience and offered useful content to that audience. As a result, the number of views increased significantly, along with key metrics like average watch time and completion rate. X: Communication with users is its strength Top-performing post: 4 recommended shrines and temples for autumn foliage This post highlighted a selection of spots where you can enjoy the beautiful combination of autumn leaves and traditional shrines or temples. It includes location details for each spot, highlights, and accompanying images. Lowest-performing post: Today is Christmas! This post featured a video created using generative AI, accompanied by the trending hashtag #MerryChristmas. The goal was to bring a more "human" feel to the account and to gain some impressions with the hashtag but the post didn't generate much engagement. Summary Looking across other popular posts on X, content that ties into seasonal themes or current trends tends to perform better. I also found that useful content, whether it's well-organized or clearly valuable to the viewer, plays a key role in performance. Instagram: Short videos make it easier to reach non-followers Top-performing post: [2024-2025 Season] 4 recommended ski resorts near Tokyo With the start of the winter ski season, we introduced ski resorts around the Kanto area using an image post. We listed the opening periods, hours, and pricing for each resort. As a result, the post received 10 saves, which was noticeably higher than other posts, and it reached 55.4% non-followers. In my opinion, Reels tend to reach non-followers even without many saves, while image posts usually stay within your followers, around 90 percent of the time. Looking back, I realized that creating posts people want to "save" is the most important. Lowest-performing post: AI diagnoses you! In contrast, this video introduced our app's "Search by mood" feature using a Reel. The intention was to have users recall how they would use the app by recording the actual app screen and walked through the steps, but it ended up being the least played video. I think the target audience wasn't clearly defined, and the value to the user wasn't communicated well. Summary While Reels and image posts differ in how easily they reach non-followers, I got the impression that a post's performance often depends on whether it gets saved. More than just offering useful content, it's important to actively encourage saves and review it regularly. Overall Summary & Future Direction Overall, we found that social media didn't make a major contribution to attracting customers. That said, we can't say no effect either. So, we'll continue to reflect and refine our approach moving forward. This article highlights two particularly important points: Clarify your target Information that is useful to viewers These may sound obvious, but I realized that these two points are essential for videos to driving performance regardless of the platform. We also used the same content from TikTok on Instagram, but because the user demographics differ between platforms, it didn't get much of a response. While it's ideal to tailor posts for each platform, our limited resources made it difficult to do so effectively. Each platform has its own unique characteristics and user demographics, so it’s important to identify what works best based on the specific purpose. References The 7 Golden Rules of Social Media Marketing
アバター
はじめに はじめまして。KINTOテクノロジーズでAndroidアプリ開発を担当しているJongSeokです。 ある日、資料を整理していて、Google Sheets に内容を移しながら整えていると、ふとこう思いました。 「これ、めっちゃ面倒くさくない……?」 同じような内容を何度も入力して、セルのフォーマットも整えて…… 難しくはないけれど、これって本当に人間がやるべきことなんだろうか?と感じました。 そこで、最近よく目にするAIツールで自動化できないか調べてみたところ、 出会ったのが MCP(Model Context Protocol) でした。 いくつかのツールを試してみた結果、 なんと自然言語の「一行だけ」で Google Sheets を操作できました。 この体験は、正直かなり衝撃的でした。 MCPとは? MCPは「Model Context Protocol」の略です。 名前は少し堅く聞こえるかもしれませんが、実際はとても直感的な役割を持っています。 簡単に言うと、 AIが実際のツールやサービスを自由に操作できるようにする「通訳」のような存在です。 この記事ではその中でも、Googleサービスと連携する事例にフォーカスして紹介していきます。 もっとわかりやすく言うと 普段使っているGmailやGoogle Drive、Google Sheetsなどのサービスは、人がクリックして操作します。 MCPは、そういったサービスを AIが自動で操作できるようにするものです。 たとえば、こんなことができます。 Google Sheetsに何かを記録したい時、通常はこんな感じかと思います。 シートを開いて セルをクリックして データを入力する MCPは、そういったサービスをAIが自動で操作できるようにするものです。 それでは、実際のデモを見てみましょう。 https://www.kinto-technologies.com/products/ 次のWebページを参考にして、新しいGoogleスプレッドシートを作成し、紹介されている製品・サービスを整理してください。 上記のように指示した場合の実際の操作イメージが、以下のデモ動画です。 ![demo](/assets/blog/authors/jongseok/mcp/demo.gif =1080x) このように一文で指示するだけで、AIがGoogle Sheets APIを呼び出し、シートに自動でデータを入力してくれます。 本当に簡単にデータ整理ができてしまいました。 MCP最近流行ってるの? 技術的に可能ということと、実際に人々が関心を持っているというのは別の話。 そこで、Google Trendsで「MCP」というキーワードの検索トレンドをチェックしてみました。 2025年3月から5月までのデータを見ると、 特に4月中旬から検索数が急激に増加しているのが分かります。 これは単なる興味を超えて、 開発者だけでなく一般ユーザーまでがMCPを実際に「使い始めている」ことを示しているとも言えます。 自分もその波に乗った、という感じです。 では、実際にMCPを試してみたい場合、どのような方法があるのでしょうか? MCPを活用するには、自然言語の命令を受け取り、それを実行できる環境が必要です。 実際、Claude(Anthropic)など、MCPと連携できるAIエージェントはいくつか存在しますが、今回私が選んだのは、AI機能が組み込まれたコードエディタ「Cursor」でした。 Cursorとは? Cursorは一言で言えば、AI機能が組み込まれたコードエディタです。 自然言語でコードを依頼すれば、AIが自動で提案・修正・説明などをしてくれます。 MCP の命令を書く際にも、Cursorを使えばよりスムーズに記述できます。 Cursorについてもっと詳しく知りたい方は、公式サイトやブログを参考にするのがおすすめです。 https://www.cursor.sh それでは、一緒に始めてみましょう MCPとGoogle Sheetsを連携して実際に自動化を体験するには、まずいくつかの準備が必要です。 難しい知識は不要なので、安心してください。一つずつゆっくり進めていきましょう。 事前準備 自動化を始める前に、以下の項目を準備しておきましょう。 MCPをサポートするエディタのインストール(今回はCursorを使用) Googleアカウント 自動化したいGoogleサービス(DriveやSheetsなど) MCP Server( https://github.com/xing5/mcp-google-sheets ) ←本記事ではこれを利用します。 簡単な自然言語での命令文を考える準備 Google Cloud Platform (GCP)の設定 まず、Google Sheets APIを利用するためにGCP側の設定を行います。 1-1. プロジェクトを作成 GCP コンソールにアクセスし、新しいプロジェクトを作成します。 ![Create Project1](/assets/blog/authors/jongseok/mcp/mcp_01_01.png =1080x) ![Create Project2](/assets/blog/authors/jongseok/mcp/mcp_01_02.png =360x) 1-2-1. サービスアカウントを作成 AIがGoogle Sheetsにアクセスするための「サービスアカウント」を作成します。 ![Service Account1](/assets/blog/authors/jongseok/mcp/mcp_02_01.png =1080x) Service Accountsの+Create service accountを押します。 ![Service Account2](/assets/blog/authors/jongseok/mcp/mcp_02_02.png =360x) Service account nameを入力してDoneを押します。 1-2-2. Permission追加 ![Service Account3](/assets/blog/authors/jongseok/mcp/mcp_02_03.png =1080x) Permission→Manage access→RoleをEditorにしてSaveを押します。 1-2-3. Keys追加 ![Service Account4](/assets/blog/authors/jongseok/mcp/mcp_02_04.png =1080x) Keys→Add Keyに押してJSONを選択してCreateします。 そうしたら認証ファイルがDownloadされます。 ![Service Account5](/assets/blog/authors/jongseok/mcp/mcp_02_05.png =1080x) ファイルの中身のこんな感じになります。 これでサービスアカウント発行は終わりです。 このファイル client_email は後で使います。 1-3. Googleサービスを設定 ![Service Account5](/assets/blog/authors/jongseok/mcp/mcp_03_01.png =1080x) APIS & ServicesのLibraryを押します。 ![Service Account5](/assets/blog/authors/jongseok/mcp/mcp_03_02.png =1080x) 今回利用したいServiceが見えますね。 Drive Sheets 二つのServiceをEnableを押します。 2-1. GoogleDriveの設定 ![Google Drive1](/assets/blog/authors/jongseok/mcp/mcp_04_01.png =1080x) Google Driveに入ってAIを利用するFolderを作ります。 例)Sheets For MCPで作りました → その後ShareのShareを押します。 ![Google Drive2](/assets/blog/authors/jongseok/mcp/mcp_04_02.png =360x) 上の欄には 1-2. サービスアカウントを作成 したJSONファイルに中にある client_email をcopyして入力します。 ![Google Drive3](/assets/blog/authors/jongseok/mcp/mcp_04_03.png =360x) 追加されたか確認してDoneを押します。 2-2. Folder IDを取得 ![Google Drive4](/assets/blog/authors/jongseok/mcp/mcp_04_04.png =1080x) 設定が終わったら Folder ID を取得します。 これで設定に必要な物は全部準備できました。 実際に設定して使ってみましょう! MCP設定(Cursor&Terminal) Windowsでも可能ですが、ここではMacを基準に説明します。 MCPのセットアップには簡単なCloudServerを利用する方法もありますが、今回はLocalServerでやってみます。 まずはuv,uvxが必要なのでTerminalでインストールしましょう。 uvx は、高速なPythonパッケージインストーラとレゾルバである uv の一部らしいです。 1. uv,uvxインストール & 環境変数セット curl -LsSf https://astral.sh/uv/install.sh | sh ![Terminal1](/assets/blog/authors/jongseok/mcp/mcp_05_01.png =1080x) 上手くインストールできました。 ご自身の環境に合わせて値を入力してください。 export SERVICE_ACCOUNT_PATH="/path/to/your/service-account-key.json" export DRIVE_FOLDER_ID="YOUR_DRIVE_FOLDER_ID" 準備した二つ SERVICE_ACCOUNT_PATH, DRIVER_FOLDER_ID を環境変数でセットします。 2. Project設定 git clone https://github.com/yourusername/mcp-google-sheets.git cd mcp-google-sheets ProjectをCloneし、Cloneしたディレクトリに移動します。 どこに置いても問題ありませんが、管理しやすくするため、発行された service_account.json ファイルはProjectの下に移動しました。 ![Terminal2](/assets/blog/authors/jongseok/mcp/mcp_05_02.png =360x) サーバーを起動してみましょう。 uv run mcp-google-sheets ![Terminal3](/assets/blog/authors/jongseok/mcp/mcp_05_03.png =1080x) これでLocalのサーバーは準備できました。 最後に使うためにMCPを設定しましょう。 3. MCP設定 ![Cursor1](/assets/blog/authors/jongseok/mcp/mcp_06_01.png =1080x) MCP設定するためには右上にある⚙️を押します。 ![Cursor2](/assets/blog/authors/jongseok/mcp/mcp_06_02.png =1080x) MCP項目中にある+Add new global MCP Serverを押します。 ![Cursor3](/assets/blog/authors/jongseok/mcp/mcp_06_03.png =1080x) 入力画面が表示されます。 { "mcpServers": { "mcp-google-sheets-local": { "command": "uv", "args": [ "run", "--directory", "/Users/jongseok.bae/MCP/GoogleServices/mcp-google-sheets", "mcp-google-sheets" ], "env": { "SERVICE_ACCOUNT_PATH": "/Users/jongseok.bae/MCP/GoogleServices/mcp-google-sheets/service_account.json", "DRIVE_FOLDER_ID": "1n4HUOiglwjiTKHcw8jSATYcPtCRqNn6O" } } } } args にはご自身のProjectがあるPathを入力 env にはSERVICE_ACCOUNT_PATH,DRIVE_FOLDER_IDを入力 上記の値を入力して保存します。 ![Cursor4](/assets/blog/authors/jongseok/mcp/mcp_06_04.png =1080x) 🟢が点灯していれば、正しくセットアップされています。 このMCPで利用可能な機能はTools:list_spreadsheets,create_spreadsheet,get_sheet_data...など表示されます。 これでセットアップは完了です。お疲れさまでした! それでは実際に、どのように自然言語で命令を出して、Google Sheets に自動入力を行うのかを試してみましょう。 実際に使ってみる 準備が整ったところで、いよいよ本番です。 例えば、以下のように自然言語で指示を出すだけで… https://news.google.com/home?hl=en-US&gl=US&ceid=US:en 「本日のGoogleニュース」 という名前の新しいGoogleスプレッドシートを作成してください。 掲載されているトップニュースを、タイトル、概要、URLの3列で整理してシートに追加してください。 実際には裏側で、次のような流れが走っています: Cursorで命令を入力 MCP Serverが命令を受信 自然言語を解析 → Google Sheets APIに変換 自動でスプレッドシート作成 + データ挿入 ![Cursor4](/assets/blog/authors/jongseok/mcp/mcp_06_05.png =720x) MCP実行する中身を見るとこんな感じになってます。(自然言語を解析して処理を実行) ![demo2](/assets/blog/authors/jongseok/mcp/demo2.gif =1080x) 結果として、上で紹介したようなスプレッドシートが生成されました! まとめ 今回紹介したMCPとGoogle Sheetsの連携は、単なる技術紹介にとどまるものでがありません。 「自然言語一文で業務を動かす」という体験は、 もはや未来の話ではなく、 今この場で実現可能なこと です。 もちろん、 セキュリティやアクセス制御など、まだ検討すべき課題もあります。 しかし、小さな自動化から始めていくことで、業務効率化の可能性は十分に感じられました。 もし業務の自動化を考えている方は、今回の事例を参考に、ぜひ一度試してみてください。 想像よりも早く、変化を実践できるかもしれません。
アバター
はじめに こんにちは!Yao Xie です。 KINTOテクノロジーズのモバイルアプリ開発グループで、 Android の KINTO かんたん申込みアプリ を開発しています。本記事では、Compose Multiplatform とネイティブの UI フレームワークの両方を活用したハイブリッドモデルを実装する方法について、私の考えをいくつか共有したいと思います。 モバイルアプリの開発では、限られた予算や多様なチーム規模、厳しい納期などに直面しつつ、長期的な保守性と一貫したユーザー体験の確保が求められる場合が多くあります。Kotlin Multiplatform (KMP) 上に構築されたハイブリッド アーキテクチャには、コア UI フレームワークとして Compose Multiplatform が組み込まれ、iOS の SwiftUI や Android の Jetpack Compose といったネイティブフレームワークと組み合わせることで、そういった課題に柔軟に対応することができます。少ない予算で MVP を素早く立ち上げる場合でも、完成度の高いアプリへとスケールアップする場合でも、このアプローチなら制約に適応でき、コード品質も長期的に維持できます。 さまざまな制約に対応できる、柔軟で保守しやすいアプローチ 一貫性を保ちながらの迅速な開発 限られた予算、小規模なチーム、タイトなスケジュールといった制約のあるプロジェクトにおいて、私が感じている主なメリットは次のとおりです。 共有ビジネスロジック: ネットワーク処理、データモデル、ビジネスルールといったコア機能を KMP のモジュールに一度記述することで、Android と iOS の両方で一貫した動作が保証され、コードの重複を避けつつ将来的なメンテナンスが楽になります。 Compose Multiplatform による共有 UI:Compose Multiplatform は Kotlin Multiplatform エコシステムの一部で、共有 Kotlin コードを基盤にして自然なかたちで成り立ち、UI コンポーネントを構築します。UI の大部分を、共用のコンポーザブルなコンポーネントとして開発します。Android では Jetpack Compose を通じてそのまま活用でき、iOS でも ComposeUIViewController などのヘルパーを使ってこれらのコンポーネントを組み込むことが可能です。このように統一されたアプローチを取ることで、両プラットフォームにおいて共通の設計・動作ガイドラインに従うことができ、長期的なサポートも容易になります。 この戦略で、一貫性を維持しながら開発時間を最小限に抑えることができます。高品質なユーザーエクスペリエンスの提供と将来を見据えたコード設計にとって、この要素は重要なポイントとなります。 Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、クリエイティブ・コモンズ表示 3.0 ライセンスに記載された条件に従って使用しています。* ネイティブ拡張によるスケールアップ プロジェクトに多くの資金、人員、追加の時間が確保できるようになると、次のようなことが可能になります。 段階的なネイティブ統合:強固な共有基盤があれば、ネイティブの UI 要素を使用して主要な画面を徐々に置き換えたり、改善したりすることができますたとえば、SwiftUI で iOS の重要な画面を強化し、プラットフォーム固有のデザイン標準により適した形に仕上げられます。 expect/actual メカニズム:Kotlin の expect/actual パターンを使用することで、プラットフォーム固有のボタンなどといった汎用的な共有コンポーネントを定義し、それぞれのネイティブ実装を提供できます。こうすると、まずは共有 UI で開発を始め、洗練されたネイティブの改良を後から一番重要な箇所に限定して加えることができるようになります。 多様なチーム規模とプロジェクトのタイムラインに対応 ハイブリッドアプローチは、プロジェクトの成長に応じて臨機応変に対応できるように設計されています。 小規模なチームやタイトなスケジュールの場合:コードの再利用を最大限に活用することが焦点になります。少人数のチームでも共有のビジネスロジックや UI を構築・維持することができ、一貫性を保ちつつ、市場投入までのスピードを加速できます。 大規模なチームや開発期間に余裕がある場合:チームが拡大したりスケジュールに余裕ができたら、ネイティブコンポーネントの開発に追加のリソースを投入して、ユーザーエクスペリエンスを向上させます。こういった段階的な戦略を取ることでリスクを最小限に抑え、コアロジックの安定性と保守性を長期にわたって確保できます。 共有ロジックと UI の一貫性をプラットフォーム間で保つと、複雑さと技術的な負債が軽減され、プロジェクトの保守や拡張がもっと楽になります。 Kotlin Multiplatform ハイブリッドモードを示すコードスニペット 共有ビジネスロジック この共有モジュールでは、Android アプリと iOS アプリの両方で同じコアロジックを利用することができます。このため、一貫性が保たれてメンテナンスにかかるオーバーヘッドが軽減されます。 // Api interface Api { suspend fun getUserProfile():User } class ApiImpl(private val client:HttpClient) :Api { override suspend fun getUserProfile():User { return client.get { url { protocol = URLProtocol.HTTPS host = "yourdomain.com" path("yourpath") } }.safeBody() } } // Repository interface UserRepository { suspend fun getUserProfile():Flow<User> } class UserRepositoryImpl(private val api:Api) :UserRepository { override suspend fun getUserProfile():Flow<User> { return flowOf(api.getUserProfile()) } } // ViewModel class ProfileViewModel( private val repository:UserRepository, ) :ViewModel() { private val _uiState = MutableStateFlow(ProfileUiState()) val uiState:StateFlow<ProfileUiState> = _uiState.asStateFlow() @OptIn(ExperimentalCoroutinesApi::class) fun onAction(action:ProfileScreenAction) { when (action) { is ProfileScreenAction.OnInit -> { viewModelScope.launch { // Set loading state to true _uiState.update { it.copy(isLoading = true) } // Retrieve authorization info and then fetch user profile data repository.getAuthorizationInfo() .flatMapLatest { authInfo -> // If authInfo exists, call getUserProfile() to get user data; otherwise, emit null if (authInfo != null) { repository.getUserProfile() } else { flowOf(null) } } // Catch any exceptions in the Flow chain .catch { e -> e.printStackTrace() } // Collect the user profile data and update the UI state .collect { userProfile -> _uiState.update { state -> state.copy( isLoading = false, data = userProfile, errorMessage = if (userProfile == null) "User profile data is null" else null ) } } } } } } } Compose Multiplatform による共有 UI Kotlin Multiplatform に統合されている Compose Multiplatform は、共有の Kotlin コード上で UI コンポーネントの作成が可能です。 @Composable fun ProfileScreen( viewModel:ProfileViewModel = koinViewModel() ) { val uiState = viewModel.uiState.collectAsState().value LaunchedEffect(Unit) { viewModel.onAction(ProfileScreenAction.OnInit) } Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .background(White) .padding(16.dp) ) { // Profile header with image and user details Row( modifier = Modifier .fillMaxWidth() .background(White, shape = RoundedCornerShape(16.dp)) .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { AsyncImage( model = uiState.data.profileImage, contentDescription = "Profile Image", modifier = Modifier .size(64.dp) .clip(CircleShape) ) Spacer(modifier = Modifier.width(16.dp)) Column { Text( text = uiState.data.userName, fontSize = 16.sp, color = Color.Black ) Text( text = uiState.data.email, fontSize = 14.sp, color = Color.Blue.copy(alpha = 0.6f) ) } } Spacer(modifier = Modifier.height(16.dp)) MenuLabel(label = "Account Settings") .... } } リーンアプローチ:このコンポーザブルを両プラットフォームで直接使用すると、外観と動作の一貫性が維持できて便利だと思います。 スケールアプローチ:基盤となるビジネスロジックには手を加えず、必要に応じてネイティブコンポーネントを使用して UI を強化します。 expect/actual 経由のネイティブ UI expect コンポーネントを定義します。 @Composable expect fun PlatformButton( modifier:Modifier = Modifier, label:String, onClick: () -> Unit ) Android 向けの実装 (androidMain) @Composable actual fun PlatformButton( modifier:Modifier, label:String, onClick: () -> Unit ) { Button( onClick = onClick, modifier = modifier ) { Text(text = label) } } iOS 向けの実装 (iosMain) @Composable actual fun PlatformButton( modifier:Modifier, label:String, onClick: () -> Unit ) { UIKitView( modifier = modifier, factory = { // Create a native UIButton instance. val button = UIButton.buttonWithType(buttonType = 0) button.setTitle(label, forState = UIControlStateNormal) // Style the button ... // Create a target object to handle button clicks. val target = object :NSObject() { @ObjCAction fun onButtonClicked() { onClick() } } // Add the target with an action selector. button.addTarget( target = target, action = NSSelectorFromString("onButtonClicked"), forControlEvents = UIControlEventTouchUpInside ) button } ) } ブレンド UI:Flutter に対する大きな強み Flutter と異なり、Compose Multiplatform は1 つの画面上で共有 UI とネイティブコンポーネントをシームレスに組み合わせることをサポートしてくれます。 共有 UI からネイティブ UI へ完全に切り替える必要のない場合が多く、1つの画面上で両者を組み合わせることができます。このアプローチで、画面全体をどちらか一方のフレームワークに完全に任せることなくCompose Multiplatform とネイティブフレームワークそれぞれの強みを活かすことができます。たとえば、iOS で SwiftUI のネイティブリストと並べて共有の コンポーザブルヘッダーを表示したいとします。 ブレンド UI は、次のように構築できます。 ネイティブレイアウトに共有コンポーザブルを埋め込む Compose Multiplatform の強みのひとつは、共有 Compose UI コンポーネントをネイティブ SwiftUI レイアウト内にシームレスに埋め込めることです。開発者は画面全体を書き直さなくとも、共有コンポーネントを段階的に導入することができます。この柔軟性は、Flutter のオーバーレイ方式では実現が難しいものです。 たとえば、SwiftUI をコンテナとして使用して、その中に UIViewController でラップした Compose Multiplatform の共有 UI を埋め込むことができます。 import SwiftUI import shared // Import the shared KMP framework struct ComposeContainerView:UIViewControllerRepresentable { let user:User func makeUIViewController(context:Context) -> UIViewController { // Use the Compose wrapper to create the UIViewController return AppComposeUI().createProfileVC(user) } func updateUIViewController(_ uiViewController:UIViewController, context:Context) { } } struct ContentView:View { let user:User var body: some View { VStack { Text("Native SwiftUI Header") .font(.headline) .padding() // Embed the Compose UI inside SwiftUI. ComposeContainerView(user: user) .frame(height:250) List { Text("Native SwiftUI Item 1") Text("Native SwiftUI Item 2") } } } } Compose Multiplatform 画面にネイティブ iOS UI を 埋め込む Compose Multiplatform では、共有 Compose 画面内に SwiftUI や UIKit などのネイティブ iOS ビューを埋め込むことも可能です。これも、Flutter では簡単に実現できない機能のひとつです。 埋め込む SwiftUI ビューを定義します。 import SwiftUI struct MySwiftUIView:View { let user:User var body: some View { VStack { Text("Native SwiftUI Content") .font(.body) Text("Welcome, \(user.firstName)") } .padding() .background(Color.gray.opacity(0.2)) .cornerRadius(8) } } 次に、UIKitView を使用してこのネイティブ ビューを共有 Compose UI に統合します。 @Composable fun ComposeWithNativeUI(user:User) { Column { // Shared Compose UI header. ProfileHeader(user = user) Spacer(modifier = Modifier.height(16.dp)) // Embed a native iOS view using UIKitView. UIKitView(factory = { // Create a UIHostingController with a SwiftUI view. val hostingController = UIHostingController(rootView = MySwiftUIView(user = user)) hostingController.view }, modifier = Modifier.fillMaxWidth().height(100.dp)) } } この双方向のインライン統合により、Compose Multiplatform が Flutter と明確に差別化されます。これでネイティブのルックアンドフィールを保ちながら、共有 UI コンポーネントを段階的に導入できるようになります。共有コードの効率性とネイティブ UI の豊かさ・品質を兼ね備えているので、開発者は柔軟に対応でき、大規模な書き換えリスクも抑えられます。 Compose Multiplatform は、共有 UI とネイティブプラットフォーム UI をより柔軟できめ細かく統合できます。この点で、Flutter よりも大きな強みがあります。Flutter は通常、オーバーレイ方式で共有 UI コンポーネントをレンダリングします。それとは異なり、Compose Multiplatform はインラインレンダリングを採用していて、共有 UI とネイティブ UIの要素を自然に組み合わせることができます。 長所 柔軟性、保守性、拡張性のベストプラクティス ラピッドプロトタイピングを取り入れる ユーザーがまだアプリに触れていない段階で、ピクセル単位の細部を完璧に仕上げようと何週間も費やすのは、リスクが高いことだと痛感しました。それを痛感しました。最近では、私たちのチームは 1〜2 週間で共有 UI のプロトタイプを作成し、関係者に配布して、早期にフィードバックを収集するようにしています。アイデアに手応えを感じたら、それまでに作ったものは捨てずに、ネイティブの装飾(アニメーション、プラットフォーム固有のジェスチャーなど)を重ねていきます。 段階的な開発を計画する 私たちの経験則は、「まずは小さくリリース、後からリファクタリング」です。最初は、コアロジック用の Kotlin Multiplatform モジュールと単独の共有 Compose 画面からスタートします。その構成で上手く動くことが確認できたら、次の画面の実装に進んだり、共有ウィジェットを SwiftUI や Jetpack Compose のバージョンに置き換えたりと、作業を1つずつ進めます。徐々に進めるやり方で、開発スピードを保ち、書き直しを避けることができます。 リソース、品質、スケーラビリティのバランスをとる チームがまだ2人だけだった頃は、UI の9割を共有コードで作っていました。それが、限られた体制でできる精一杯の方法だったからです。メンバーが増えてデザインへのこだわりも強くなるにつれて、重要なフローはネイティブ実装に切り替え、その他の部分は共有コードのままにしました。このように段階的に移行したことで、バックログを現実的な状態に保ち、リリースに支障をきたすことなく品質を高めることができました。 一貫性を保つ ビジネスロジックを1つの Kotlin モジュールに集約することで、マージに伴う悩みの種が数え切れないほど軽減されました。1回のバグ修正で、すべてのプラットフォームに反映されます。「iOS は直ったけど、Android はまだバグってる」のように取り残されることは、もうありません。派手さはありません。ですが、この共通コアがあるからこそ、チームは締め切り直前の夜でも安眠することができます。 変化への適応力を確保する 複数のプラットフォームにまたがるプロジェクトを率いた経験から言えるのは、成功するチームはアーキテクチャを静的な設計図ではなく、生き物のような存在として扱っています。予算が厳しくなったり、スケジュールが変わったり、突然新しいスキルを持つメンバーが加わっても対応できるよう、すべてのレイヤーを柔軟に設計しています。Compose Multiplatform を使用することで共有 UI とネイティブ UI をオンデマンドで組み合わせられるので、「MVP モード」から「ネイティブ仕上げモード」へ、数ヶ月とかからずに数日で切り替えることができます。製品戦略や市場が一夜にして変わったときも、この臨機応変さには何度も助けられてきました。 結論 Kotlin Multiplatform と Compose Multiplatform を基盤に、ネイティブ UI フレームワークと併用して構築されたこの適応力の高いハイブリッドアーキテクチャは、他に類を見ない柔軟性を備えています。このハイブリッドアーキテクチャがあれば、一貫性と長期的な保守性を維持しつつ、予算、チーム規模、スケジュールといった制約に応じて開発戦略を柔軟に調整できます。小規模なプロジェクトでは、コードの再利用を最大限に活用してスピーディに立ち上げましょう。そしてリソースが増えてきたら、コアロジックはそのままに、重要な画面をネイティブ UI で徐々に強化します。 重複の削減、メンテナンスの効率化に、柔軟で段階的なこのモデルを取り入れてみてください。そしてプロジェクトの成長に応じて拡張していける、一貫性のある高品質なユーザーエクスペリエンスを実現してください。
アバター
本記事は KINTOテクノロジーズアドベントカレンダー2024 の24日目の記事です🎅🎄 はじめに こんにちは、 Rasel です。現在、KINTOテクノロジーズ株式会社でAndroidエンジニアとして働いています。今日は、Kotlin Multiplatform (KMP) でのテストへのアプローチについて簡単に紹介します。 KMPでのクロスプラットフォームテストにより、AndroidとiOS間で共有されるコードの信頼性を確保できます。シンプルなテストは素晴らしい開始点ですが、多くのアプリケーションでは、より複雑なセットアップを行って、ビジネスロジックの検証、非同期関数の処理、コンポーネント間の相互依存性のテストなどを行う必要があります。 この投稿では、非同期コードの処理、依存関係のテスト、パラメーター化されたテストの使用、複雑なセットアップの構築など、高度なテストシナリオについて説明します。詳細を掘り下げてみましょう! 高度なクロスプラットフォームテストの設定 共通のテスト依存関係を構成する こちらは、アサーション、コルーチン、モック用のテストライブラリを使用した強化されたセットアップです。 // In shared module’s build.gradle.kts plugins { id("dev.mokkery") version "2.5.1" // Mocking library for multiplatform } sourceSets { val androidInstrumentedTest by getting { dependencies { implementation(libs.core.ktx.test) implementation(libs.androidx.test.junit) implementation(libs.androidx.espresso.core) implementation(libs.kotlin.test) } } commonTest.dependencies { implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) implementation(libs.kotlin.test.annotations.common) } iosTest.dependencies { implementation(libs.kotlin.test) } } このセットアップにより、非同期テストや、テストに必要な依存関係のモック化を処理できるようになります。 KMPにおける基本的なテスト KMPを使用すると、 commonTest で共有テストを簡単に記述できると同時に、 androidTest と iosTest でプラットフォーム固有のテストが可能になります。開始方法の概要は次のとおりです。 class GrepTest { companion object { val sampleData = listOf( "123 abc", "abc 123", "123 ABC", "ABC 123", ) } @Test fun shouldFindMatches() { val results = mutableListOf<String>() // Let's get the matching results with our global function grep which is defined in commonMain module grep(sampleData, "[a-z]+") { results.add(it) } // Check results with expectations assertEquals(2, results.size) for (result in results) { assertContains(result, "abc") } } } commonTest におけるこの簡単なテストを使用すると、プラットフォーム間で commonMain モジュールの共有ロジックを検証できます。 高度なテストシナリオ 1.コルーチンを使用した非同期関数のテスト 多くの共有KMPプロジェクトでは、非同期作業にコルーチンを使用します。コルーチンを効果的にテストするには、テストでコルーチンの実行を制御および進行を制御できるツールである kotlinx-coroutines-test を使用する必要があります。 サンプルシナリオは次のとおりです。ユーザーデータを非同期的に取得するUserRepositoryを持っているとします。データ検索をシミュレートするためにコルーチンフローを制御するテストを記述します。 @OptIn(ExperimentalCoroutinesApi::class) class UserRepositoryTest { private val testDispatcher = StandardTestDispatcher() private val userRepository = UserRepository(testDispatcher) @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) // Set test dispatcher as main } @AfterTest fun tearDown() { Dispatchers.resetMain() // Reset main dispatcher } @Test fun `fetch user successfully`() = runTest { val user = userRepository.fetchUser("123") assertEquals("John Doe", user.name) } } この例では、 Dispatchers.setMain(testDispatcher) がデフォルトのディスパッチャを置き換えることで、コルーチンの実行を制御できるようにします。 runTest {} は、コルーチンのセットアップとクリーンアップを自動的に処理し、サスペンド関数の管理を容易にします。 runTest 内のフローを制御して、遅延やその他の非同期動作をシミュレートできます。 2.モックの使用と相互関係の検証 マルチプラットフォームセットアップで依存関係をモックすると、クラス間の複雑な相互作用のテストを効率化します。ここでは、APIクライアントをモックしてネットワーク呼び出しをシミュレートし、リポジトリとのやり取りを確認します。 class NewsRepositoryTest { private val testDispatcher = StandardTestDispatcher() private val apiService = mock<ApiService> { everySuspend { getNews(defaultSectionName) } returns TopStoryResponse(mockNewsArticles) } private val repository = NewsRepository(apiService, testDispatcher) @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) // Set test dispatcher as main } @AfterTest fun tearDown() { Dispatchers.resetMain() // Reset main dispatcher } @Test fun `fetch news list for home section successfully`() = runTest { val articleList:List<Article> = repository.fetchNews() assertEquals(mockNewsArticles.size, articleList.size) assertEquals(mockNewsArticles.first(), articleList.first()) assertEquals(mockNewsArticles.last(), articleList.last()) verifySuspend { apiService.getNews(defaultSectionName) } } } const val defaultSectionName = "home" この例では、 mock<ApiService>() を使用すると、明示的な動作定義がなくてもモックがデフォルト値を返すことができます。 verifySuspend は、 getNews() が適切に呼び出されたかどうかを確認し、リポジトリ内の正しいフローを確保します。 everySuspend は、 getNews() の呼び出しごとに戻されるモックデータを設定します。 NewsRepository の getNews() メソッドは、 ApiService から TopStoryResponse としてデータを取得し、結果を List<Article> として返します。 3.さまざまな入力シナリオに関するパラメーター化されたテスト KMPは現在、JUnit5などのライブラリと同様に、パラメーター化されたテストにネイティブに対応していません。ただし、Kotlinの機能を使用してパラメーター化されたようなテスト動作を実現する回避策があり、さまざまな入力を使用して関数を簡潔かつ再利用可能な方法でテストできます。以下は、DateUtils関数のさまざまな日付フォーマットをテストする例です。 class DateUtilsTest { private val testCases = listOf( Triple("2024-11-18T00:00:00", "dd MMM yyyy", "18 Nov 2024"), Triple("2024-11-18T15:34:00", "hh:mm", "03:34"), Triple("2024-11-18T15:34:00", "dd MMM yyyy hh:mma", "18 Nov 2024 03:34PM"), ) @Test fun `check date format`() { testCases.forEach { (dateString, outputFormat, expectedOutput) -> val actualOutput = formatDatetime(dateString = dateString, inputFormat = "yyyy-MM-dd'T'HH:mm:ss", outputFormat = outputFormat) assertEquals(expectedOutput, actualOutput, "Failed for date $dateString and output format $outputFormat") } } } [!注記] ここで、 formatDatetime() は、日付文字列をフォーマットするために DateUtils 内で定義されたグローバル関数です。この例では、 入力ごとに予想される結果を定義して、コードを重複させることなくテスト範囲を簡単に拡張できます。 プラットフォーム固有のテストライブラリに依存せずに、 androidTest と iosTest の両方で動作します。 複雑なセットアップによるプラットフォーム固有のテスト 一部のテストケースでは、特にAndroidの SharedPreferences またはiOSの NSUserDefaults を使用する場合、プラットフォーム固有のAPIまたは動作を使用する必要があります。両方のプラットフォームの内訳は次のとおりです。 Android固有のテスト:SharedPreferencesのテスト Android では、 SharedPreferences を使用してデータストレージをテストする必要がある場合があります。AndroidXの ApplicationProvider を使用してテストコンテキストにアクセスするセットアップを次に示します。 @RunWith(AndroidJUnit4::class) class SharedPreferencesTest { private lateinit var sharedPreferences:SharedPreferences private val expectedAppStartCount = 10 @BeforeTest fun setUp() { val context = ApplicationProvider.getApplicationContext<Context>() sharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) } @AfterTest fun tearDown() { sharedPreferences.edit().clear().apply() } @Test fun checkAppStartCountStoresCorrectly() { sharedPreferences.edit().putInt(KEY_APP_START_COUNT, expectedAppStartCount).apply() val actualAppStartCount = sharedPreferences.getInt(KEY_APP_START_COUNT, -1) assertEquals(expectedAppStartCount, actualAppStartCount) } companion object { private const val SHARED_PREF_NAME = "test_prefs" private const val KEY_APP_START_COUNT = "app_start_count" } } このプラットフォーム依存的なテストは、 androidInstrumentedTest 内に配置する必要があり、実行するにはエミュレーターまたは実際のAndroidデバイスが必要になります。 iOS固有のテスト:NSUserDefaultsのテスト iOSでは、同様のロジックを NSUserDefaults でテストできます。通常、このテストは、以下に示すように iosTest において記述します。 class NSUserDefaultsTest { private lateinit var userDefaults:NSUserDefaults private val expectedAppStartCount = 10 @BeforeTest fun setUp() { userDefaults = NSUserDefaults.standardUserDefaults() } @AfterTest fun tearDown() { userDefaults.removeObjectForKey(KEY_APP_START_COUNT) // Clean up after test } @Test fun `test app start count stores correctly in NSUserDefaults`() { userDefaults.setObject(expectedAppStartCount, forKey = KEY_APP_START_COUNT) val actualAppStartCount = userDefaults.integerForKey(KEY_APP_START_COUNT) assertEquals(expectedAppStartCount, actualAppStartCount.toInt()) } companion object { private const val KEY_APP_START_COUNT = "app_start_count" } } 複雑なテストセットアップ:相互依存関係のテスト 高度なテストでは、相互に作用する複数の依存関係をシミュレートする必要がある場合があります。以下は、API クライアントとローカルキャッシュの両方に依存するリポジトリが関与する例です。 class ComplexRepositoryTest { private val apiService = mock<ApiService>() private val localCache = mock<LocalCache>() private val repository = ComplexRepository(apiService, localCache) @Test fun `fetch data from local cache`() = runTest { everySuspend { localCache.getArticles() } returns mockNewsArticles val data = repository.getNewsArticles() assertEquals(mockNewsArticles.size, data.size) verifySuspend { localCache.getArticles() } } @Test fun `fetch data from remote source`() = runTest { everySuspend { localCache.getArticles() } returns emptyList() everySuspend { apiService.getNews(defaultSectionName) } returns TopStoryResponse(mockNewsArticles) everySuspend { localCache.insertAllArticles(mockNewsArticles) } returns Unit val data = repository.getNewsArticles() assertEquals(mockNewsArticles.size, data.size) verifySuspend { localCache.getArticles() apiService.getNews(defaultSectionName) localCache.insertAllArticles(mockNewsArticles) } } } この例では、 リポジトリは最初にキャッシュをチェックし、キャッシュが空の場合にのみAPIを呼び出します。 私たちは相互作用を検証することで、リポジトリが意図したフローに従い、不必要にAPIを呼び出さないようにしています。 上記のすべてにより、共通、Android、およびiOS実装のテストの記述が完了しました。これらのテストを個別に実行することも、 allTests という名前のGradleタスクを使用してすべてのテストを一度に実行することもできます。これにより、プロジェクト内のすべてのテストが、対応するテストランナーで実行されます。![Gradle task allTests](/assets/blog/authors/ahsan_rasel/2024-12-24-testing-kmp/gradle-all-test-task.png =800x) テストを実行するたびに、 shared/build/reports/ ディレクトリ内にHTMLレポートが生成されます。 ![Test report](/assets/blog/authors/ahsan_rasel/2024-12-24-testing-kmp/test-report.png =550x) このようにして、当社のKMPの取り組みを進めながら、適切に調整されたテストでアプリの品質を確保することができます。 複雑なKMPテストのベストプラクティス テストセットアップを分離する :ヘルパー関数やクラスを作成してモックの設定を行い、繰り返しコードを削減します。 非同期動作を制御する :kotlinx-coroutines-testを使用して、コルーチンのタイミングを制御し、遅延やネットワーク応答をシミュレートします。 クロスプラットフォームテストとプラットフォーム固有のテストをミックスする :ネイティブ動作にはプラットフォーム固有のテストを使用し、共有ロジックにはクロスプラットフォームテストを使用します。 テスト範囲をモニターする :特に、依存関係のある複雑なフローについては、包括的なカバレッジを確保します。 結論 Kotlin Multiplatformにおけるクロスプラットフォームテストでは、単純なテストシナリオと複雑なテストシナリオを処理でき、AndroidとiOS全体でコードの信頼性を確保するための強力なツールが得られます。再利用可能なテストをセットアップし、非同期動作を制御し、依存関係を分離することで、重要なビジネスロジックを検証して全体的なコード品質を向上させる高度なテストを記述することができます。 これらの手法により、KMPプロジェクトはプラットフォーム間で一貫性のある信頼性の高いパフォーマンスを発揮できるようになり、あらゆる場所のユーザーにシームレスな体験を提供できるようになります。KMPを楽しんでください!
アバター
Self-introduction Nice to meet you! I'm Tsuji, a manager in the Cloud Infrastructure Group at KINTO Technologies. I joined the company back in May 2020, so I've been around for a while. Since graduating, I've been working as an infrastructure engineer, handling both on-prem and cloud systems. Overview GitHub Copilot Agent Mode for VSCode officially launched recently (as of April 16, 2025), I'll refer to it as "Agent" from here. Seems like the perfect excuse to try building something useful with it. Premise I'm an infra engineer through and through. Frontend? Backend? Not really my thing. But when it comes to infra—especially AWS—I can hold my own. This article won't dive deep into technical stuff. What I Want to Do See how far infra-only engineers can go using Agents. Develop a practical system we can actually use in our daily work, as an MVP (Minimum Viable Product). Absolutely no writing code by hand. So... What Should We Build? Basically, all of KINTO Technologies' infrastructure is created and managed with IaC (Terraform). If you have used Terraform before, you know the plan output can be hard to read unless you're used to it, right? So this time I'm aiming to create something that makes it easier to view Terraform plans. What I Want to Make Make Terraform plans easier to read: I might as well throw in some generative AI for summaries. Since the Agent's building it, why not have it create a slick frontend too? Development Environment *Here's the development environment used this time just for reference: Items What I used OS Linux(WSL2) IDE VSCode(1.99.3) Agent GitHub Copilot Agent Generative AI Model Claude 3.7 Sonnet Infra AWS Let's Give It a Go First things first: we need a prompt. Yep, that's where it all starts. This time, I gave the Agent a super rough idea of what I wanted using the following prompt: I'd like to implement a mechanism to view the result of Terraform plan in the browser. AWS configuration: CloudFront + S3 + Lambda + Bedrock Planned workflow: 1. Run Terraform plan -out foo.plan locally and output the plan contents to a file 2. Upload foo.plan to S3. 3. A lambda is triggered, runs Terraform show foo.plan, converts it into a human-readable format, and stores it to S3 4. Bedrock analyzes the result of Terraform show and generates a summary of the plan 5. Users can view both the summary and full plan content via a browser. I'm planning to proceed with this approach. If you have a better suggestion, I'm happy to hear it. If not, please help me build the above. If any code needs to be generated, please create a directory under /home/mizuki/ and save the files there. Got a better prompt in mind? No need to get too deep into the details. I'm just an infra engineer here. But then the brilliant Agent took my idea, leveled it up by 300% and made it happen. You're looking to implement a system to view the result of a Terraform plan in a browser. The proposed idea is feasible and well thought out. First, I will evaluate the plan, suggest a few improvements, and then help with the implementation. # Evaluation of the current plan 1. Run Terraform plan -out foo.plan locally and save the output to a file 2. Upload foo.plan to S3 3. An S3 event triggers a Lambda function, which runs Terraform show foo.plan and outputs the result in a human-readable version to S3. 4. Bedrock analyzes the result of Terraform show and generates a summary of the plan 5. Users access the summary and full plan details via a browser # Possible improvements 1. **Security enhancement**: Restrict access to S3 and configure IAM permissions appropriately 2. **Visual representation of differences**: Visualize the plan result using graphs or colored text 3. **History management**: Save and compare past plan results 4. **Notifications**: Notify via Slack or email when important changes are detected 5. **Interactive UI**: Expand/collapse resource sections for easier browsing. Let’s move forward with these points in mind. First, the directory structure will be created. Next, we'll implement each component of this project. After creating a Terraform configuration file and define the necessary AWS resources. The directory structure has been created. Now, let's start implementing each component. Then, create `main.tf` to define the infrastructure using Terraform. This file will define the required resources such as the S3 bucket, CloudFront, Lambda, and IAM roles. : : (omitted) ... Wow, you're killing it. The Agent's killer feature is its ability to work locally. It even sets up a working directory and fills it with the Terraform files. After that, the Agent explains its actions and checks in with "Is it okay if I do this?" at each step. I have zero clue about Python, so I just kept mashing the "Accept" button every time it asked. In the end, it didn't just generate JavaScript, CSS, Python, and Terraform files, but also gave me a shell for building the Lambda. And on top of that? It included usage instructions, deployment steps, and suggestions for improvements. Time to Deploy It's awesome that the Agent made it for me—but… does it actually work??? No idea, but I'll go ahead and follow the deployment steps exactly as provided. Apply Terraform and create S3, CloudFront, etc. on AWS. Build it and create a zip for Lambda. Deploy the Lambda. Upload the JS and CSS files to S3. The deployment was completed without any errors. As Expected… Error. When I accessed the environment the Agent set up, got hit with a Lambda error. Not surprising, to be honest. I don't know anything about Python, there's no way to debug it myself. So... I'll just hand the Lambda logs over to the Agent and let it figure things out. Yes—just as I thought. It analyzed the logs, found the problem, and even fixed it. I redeployed the updated files, and this time, it actually worked. What Kind of System Did We End Up With? Here's what the final system architecture looks like! Here's how the whole thing works: The user sends Terraform plan results to S3. That triggers a Lambda function via an event. Lambda sends the plan results to Bedrock for analysis and summarization. Place the generated plan summary and the summary page's HTML file to S3. Step Functions deletes CloudFront cache. The user just drops the plan file into S3, and everything else happens automatically. Let's Try Accessing It Looks super cool! But hey, did it actually summarize the Terraform plan results like we originally set out to do? Perfect... The indentation's a little wonky here and there, but, let's not get hung up on the details. What's Good About It? The number of created, updated, and deleted resources is clearly displayed at the top for easy viewing. It explains the specific changes being made to each resource. A risk summary is provided to help you grasp what could go wrong with this plan. It even points out things you might want to improve going forward. This is exactly what I was hoping for. Add Detailed Requirements After that, I had the Agent make a few minor tweaks here and there: Timestamps are in UTC, change to JST. Fix indentations that are off. Attach a screenshot. Once the plan is generated, clear the CloudFront cache. Stuff like that. Even with all those detailed requests, the Agent handled everything without breaking a sweat . My Impressions After Trying It Building this system was way easier than I expected. I put it together in a single day, and honestly, most of that time was just waiting for the Agent to do its thing. The actual hands-on time was just about an hour. Even someone like me, who's only ever touched infrastructure, managed to build out both the frontend and backend solo. Now, to be fair—when I looked at the Python code, even I could tell there were some parts that didn't need to be there. Definitely room for improvement. But for something at the Minimum Viable Product (MVP) stage, this is more than good enough. From here, it'll be easy to optimize the code or expand the features as needed. The important thing is that we were able to create a functioning system in a short period of time. This allows us to quickly shape our ideas and find areas for improvement through actual operation. The key point is this: I was able to build a working system by myself in a short amount of time. I think Agent is the best tool for building an MVP (Minimum Viable Product) Summary It might still take some time before we can build full-scale enterprise systems using only the Agent, but even as a beginner in writing prompts, I was able to develop a working system with ease. What surprised me the most was how smoothly the Agent handled everything. All I had to do was keep clicking the "accept" button, and it built the system for me. Up until now, I was used to copying and pasting suggestions from generative AI and running commands manually. But this time, I barely touched the terminal. I just kept pressing "accept." I was able to create a system that can reduce workloads and prevent mistakes just by pressing a button. Also, the system I built is already in use, and I plan to fully integrate it into our operational workflow. Finally The Cloud Infrastructure Group I belong to is looking for people to join us. Whether you're confident in your cloud skills or just getting started and eager to learn, we'd love to hear from you. Please feel free to contact us. For more details, please check here and here (in Japanese). We’ve also posted a group introduction on Wantedly! You can read more about the Cloud Infrastructure Group here ! (in Japanese)
アバター
Hello, I am Ito, an Administrative Assistant in the Global Development Group at KINTO Technologies. My role is to support team members with various tasks including answering their inquiries and translating documents. I love cats and reading novels. Lying down and reading with my cat used to feel like heaven. But ever since my cat passed on to another heaven some time ago, I’ve been spending my days reading alone. I have read 5 or 6 English novels this year. Reading in English used to be nothing but a struggle, but lately, I’ve started to enjoy experiencing them in their original language. However, I think the most enjoyable part of speaking foreign languages is having conversations. If you speak in another language, not just English, you definitely open yourself up to more opportunities and choices. Of course, there are challenges along the way. What should you do to be able to speak foreign languages? Unlike children, who can absorb things effortlessly, most adults need to put in a significant amount of effort. Effort to create many opportunities to speak, based on understanding basic grammar and vocabulary. I think this is essential to mastering a language. In this article, I would like to introduce the Language Skill Up Project, organized by the Business Enhance Team of the Global Development Group, which aims to help improve the language skills of working adults. Introduction Currently, the Global Development Group has almost sixty members from diverse backgrounds, including various nationalities, cultures, and languages. Among them, some aren’t very proficient in Japanese, while others aren’t used to speaking English due to limited use in their previous roles. Although the members share strong development skills, it would be a loss for the company if language barriers prevented effective communication. To help address this issue, the Language Skill-Up Project was launched, led primarily by the Business Enhance Team, whose mission is to maximize the members' potential. The Ministry of Internal Affairs and Communications defines global human resources as individuals who possess language proficiency, communication skills, and cultural awareness. The Global Development Group offers an environment that fosters the development of these qualities. One of the goals of this project is to cultivate global human resources by making use of this environment. People from outside of the Group can also join our study sessions. The key to language learning is not telling yourself that you can’t do it. Our goal is to help learners build confidence by first becoming comfortable speaking in English or Japanese. To support this, we organize and run the following events. What Shall We Do? What shall we do to help improve people's language proficiency? Everyone has different starting points. What needs to be done to learn languages? Focus on mastering basic vocabulary and grammar, such as junior high school-level English. Read (also read aloud) Write Listen Practice shadowing to improve your pronunciation and intonation Speak with others as much as possible I think you can do steps 1 through 5 on your own. However, number 6 requires you to take the initiative and create opportunities on your own, unless you're willing to spend money on language schools or online conversation lessons. Probably what many of our members are lacking is opportunities to speak the language. Since we could add steps 1 to 5 later if needed, we decided to start by providing speaking opportunities first. Japanese Cafe It is a casual meeting to enjoy conversations held in lunch time, fortnightly, also known as the pizza party. It's been nine months since we've started in April 2022. Since the event is conducted entirely in Japanese, it mainly attracts participants with intermediate to advanced Japanese proficiency. Having fun is key, and our goal is to help people communicate more smoothly in Japanese. Japanese cafe Japanese Study Sessions While providing opportunities to speak is essential, the Japanese Cafe alone cannot meet everyone's diverse needs. That's why we launched the Japanese study sessions in August 2022: 30-minute weekly sessions held in small groups. Beginner class - Learn greetings, Hiragana, Katakana, useful phrases for everyday life. Conversation Class (Beginner to Intermediate) - Practice role-playing situations like shopping or making phone calls. Use newly learned phrases in conversation and expand vocabulary with related words. Kanji class (Beginner to Intermediate) - Learn Kanji while practicing new phrases. Business Class (Intermediate to Advanced) - Learn business Japanese vocabulary and expressions using real examples from Slack communications. English Cafe It started in July 2022. Each session lasts 30 to 60 minutes and is held weekly in the late afternoon. The event is held at the Jimbocho office, but participants also join from outside the Global Development Team, including some who attend remotely from the Muromachi office. (Unfortunately, no pizza is provided.) So far, we've organized various activities such as English games, presentation practice, and group discussions. Similar to the Japanese Cafe, most participants are at an intermediate level or higher and are looking to practice conversation. However, since the sessions are typically held in small groups, we can adjust the level based on the participants each time. We also welcome beginner-level participants and those who want to discuss their concerns about learning English. English cafe (It's packed. People gathered for the photo shooting.) Individual English Sessions These 30-minute weekly sessions began in April 2022, designed for those who aren’t very familiar with English. For the first six months, the sessions were held twice a week. As participants grew more comfortable speaking English, the frequency was reduced to once a week.Participants have become noticeably more proactive in speaking English compared to when the sessions first began. They said, "I feel more comfortable speaking English," and "I have more confidence now." Having something to say is a key factor in learning a foreign language. I hope they will continue improving their skills. Reflections and Future Plans We started with the goal of enhancing communication among team members. Both the Japanese and English sessions have a friendly, relaxed atmosphere and have been very effective for team building. Even within the same group, some people rarely have the chance to talk to those they don’t work with directly. By attending the Japanese cafe or the English cafe, you can communicate with people you usually don’t have a chance to talk to. Participants come from diverse backgrounds, and some are native speakers of neither Japanese nor English. Through these conversations, you get the chance to explore a variety of topics and gain insights into different cultures. The Language Skill Up Project is still in its infancy and we are exploring more effective ways. I hope those who haven’t joined yet will give it a try. We also welcome your ideas and suggestions. I hope this can be a place that helps people stay motivated and inspires them to create more opportunities to speak outside of the sessions as well. There’s no magic potion to improve your foreign language skills. It all comes down to your own effort. In other words, you will be able to speak the language as long as you keep on doing it without giving up. If you don't try to create opportunities to speak the language after reaching a certain level, unfortunately it is easy to get rusty. There will always be something new to learn, so continuous study is essential. Once studying becomes a habit, it turns into a natural part of your life rather than just a task.
アバター
Hello Hello! My name is Nakagawara from the KINTO ONE Development Group. I work as a front-end engineer on a project, using Next.js and TypeScript for development. This time, I will talk about code reviews, which are essential for team-based development. I would like to introduce the points I pay attention to when performing code reviews, both as a reviewee or a reviewer. When Requesting a Code Review as a Reviewee Let's get straight to the point. First, I would like to share two things I pay attention to when creating a pull request (I'll call them PR from now) and requesting a code review. 1. Write clear commit messages I believe that by making an effort to write concise and clear commit messages, the granularity of commits will naturally improve. If necessary, I include supplemental explanations or reference links from the second line onward. I also add emoji prefixes to my commit messages. It makes it visually easier to understand the purpose of each commit , and since the emojis carry meaning, they naturally help discourage cramming too many changes into a single commit 🌈 Below is a commit template that the FE team actually uses. # ==== Format ==== # :emoji: PBI_id Subject # # Commit body... # ==== Emojis ==== # 🐛 :bug: Bug fixes # 👍 :+1: Functionality improvement # ✨ :sparkles: Partial feature addition # 🎉 :tada: A major feature addition worth celebrating # 🎨 :art: Visual additions and tweaks # 🔧 :wrench: Feature fix # ♻ :recycle: Refactoring # 🚿 :shower: Clean-up of deprecated or unused features # 📝 :pencil: Documentation and comment updates # 🚚 :truck: File relocation # 👕 :shirt: Lint fixes and style adjustments # 🤖 :robot: Test additions and fixes # 🚀 :rocket: Performance improvements # 🆙 :up: Updating dependencies and related packages # 👮 :cop: Security improvements and resolving warnings When creating the template, the following articles served as reference: atom Getting Fun and Beautiful Commits Using Emojis And here is an actual commit👇 I consciously make sure the purpose of the commit is clear when viewed in isolation. 2. Using a template to enrich PRs The FE team has a template available for PRs. Since it clarifies the perspectives from which the reviewer should check during a review, it is expected to improve review efficiency. Below is a template we actually use. Since we use JIRA for the ticket management of tasks, we include a link to the ticket , specify what this PR will and will not address , and outline what will become possible . ## Link to ticket * https://example.com ## What was done * What was accomplished in this pull request? ## What was not done * What is explicitly not included in this pull request? (If nothing, write "None". If excluded, indicate when it will be addressed.) ## What this enables * What new functionality or behavior does this pull request enable? (If none, write "None"). ## What this disables * What functionality or behavior is no longer possible due to this pull request? (If none, write "None"). ## Other notes * Additional context for reviewers (e.g., implementation concerns, areas to pay special attention to, etc.) When creating the template, the following articles served as reference: Let’s Create a Pull Request Template to Review Efficiently! In addition to the above, I consciously add information as needed. For example, in the case of a PR addressing a bug, I also include the cause , and if there is a UI change, I attach screenshots or videos showing the states before and after the fix . This is because I believe this will facilitate a smoother understanding of the intent of the PR and the code. When Reviewing Code as a Reviewer Next, I'd like to talk about three points I pay attention to when reviewing my team members' PRs. 1. Understanding the overview of a PR In the previous section, I mentioned using a template to write a PR carefully. So first, I make sure to read the overview carefully. I believe that understanding the background, intent, and what is not being addressed here , and then reviewing the code improves review efficiency and helps avoid off-target comments. 2. Check locally The code can be checked from a PR change file. However, for the following reasons, when there is a fix that affects behavior, I always pull the branch into my local environment to check it. To check the behavior and appearance To review the entire code To check the behavior and appearance By running the code myself, I can check whether the expected behavior and appearance have been achieved, as well as understand the purpose and intent of the change. If you only look at the code without understanding why the change was made, you may not be able to perform an appropriate review, and there is a risk of regression when you make future changes to that part. So, I try to follow the processing of the code with my own eyes as much as possible to better understand it. By doing this, I can catch overlooked issues or unintended side effects caused by fixing such issues, which helps prevent unexpected bugs from occurring .🐛 To review the entire code In addition to understanding the processing, you may also notice areas that could be optimized, such as "this and this perform similar processing, so it seems like they could be combined." 3. Labeling review comments I often add labels such as [imo] or [nits] at the beginning of comments to clarify their tone or nature. (though I still tend to forget this at times, so I want to make it a habit). Conclusion I've written a lot, but to sum it up: when doing code reviews, I try to stay mindful of what would make the review process easier, both from the reviewer’s and the reviewee’s perspective. My future challenges include not just being thorough but also improving the speed of my reviews and developing my own consistent set of review criteria to avoid variations in quality. It might also be valuable to align on these review perspectives as a team. I hope this article offered you some helpful insights into code review. Thank you for reading to the end!
アバター
0. はじめに Androidアプリ「 KINTO かんたん申し込み 」の開発を担当している Choi Garamoi です。 このアプリでは KMP を導入し、一部のビジネスロジックを iOSアプリ と共有しています。 今回はよりスムーズなKMPプロジェクtの開発のため Tuist を試した結果をまとめました。 1. 概要 Android Studioでは、KMP Applicationプロジェクトは新規プロジェクトウィザードを使って作成します。作成後は以下のような Monorepo になります: KmpProject/ ├── androidApp/ # Android専用モジュール(Kotlin/Android) ├── iosApp/ # iOS専用モジュール(Swift) └── shared/ # 共通ビジネスロジック(Kotlin/Multiplatform) Androidの Gradle と同様に、iOSでもモジュール化とチーム開発に適したビルド環境が重要です。 本記事では、それを Tuist を使って構築する方法を紹介します。 2. Xcodeプロジェクトの問題 Xcodeはアプリのビルドに必要な情報を *.xcodeproj で管理していますが、いくつかの課題があります。 マージコンフリクトが頻発する : Xcodeは設定を変更すると、 project.pbxproj ファイルを自動的に更新します。このファイルは構造化されていないテキスト形式のため、複数人が同時に編集するとGit上でコンフリクトが頻繁に発生します。 実質的に意味のない差分が生成される : XcodeのGUI操作によって、実質的な変更がなくても多くの差分が生じ、履歴が煩雑になります。 自動化が困難 : 多くの設定がGUI依存であるため、CI/CDやスクリプトによるビルド自動化が困難です。 レビューし難い : project.pbxproj は可読性が低く、変更内容のレビューがしづらくなります。 拡張性に限界がある : チーム規模が大きくなると、複数ターゲットやビルド設定の管理が煩雑になります。 *.xcodeproj ディレクトリは、Androidプロジェクトで言うところの Gradle と .idea ディレクトリ を合わせたようなもので、ローカルのXcode設定とiOSアプリのビルド設定を分離できません。 Googleトレンド でも「xcode conflict」の検索数が「xcode dev」に比べて多く、開発時の衝突が多いことがうかがえます。 3. Tuist とは Swift 言語で XcodeのProjectsとworkspaces を生成と Xcode のターミナルツールを組み合わせてビルドも出来るツールです。 主な機能は モジュール化サポート 環境独立的ビルド(チーム開発指向) 自動化サポート Swift Package Manager サポート です。 他のXcodeのビルドツールとしては Swift Package Manager 、 XcodeGen 、 Bazel があります。 Swift Package Manager : Gradleの依存性管理機能( dependencies ブロック)だけを提供します。 XcodeGen : ツールの設定値チェックが足りないため人的ミスが発生しやすいです。 Bazel : 大規模プロジェクトを対象にするため使い方が複雑で、中小規模のプロジェクトにはオーバースペックです。 4. Tuist を導入する 以下は Migrate an Xcode project を基本に、最新情報を反映した手順です。 4-1. Tuistをインストールする brew update brew upgrade tuist brew install tuist Homebrew 以外のインストール方法は マニュアル をご参照ください。 4-2. Tuist設定ファイルを追加する Tuist.swift 、 Project.swift 、 Tuist/Package.swift の3つのファイル(Manifestファイル)を追加します。 KmpWithSwift/ ├── Tuist.swift ├── Tuist/ │ └── Package.swift ├── Project.swift ├── androidApp/ │ └── ... ├── iosApp/ │ └── ... └── shared/ └── ... 4-3. 設定にモジュール( Target )を追加する Project.swift が主な設定ファイルで、他のファイルは Migrate an Xcode project のサンプルをそのまま利用しても問題ありません。 4-3-1. Tuist.swift import ProjectDescription let tuist = Tuist(project: .tuist()) 4-3-2. Project.swift ターゲットの設定とKMP共通モジュールのビルドスクリプトを追加する。 infoPlist : 全体画面の設定。 scripts : KMP共通モジュールをビルドするコマンド。 import ProjectDescription let project = Project( name: "KmpWithSwift", targets: [ .target( name: "App", destinations: .iOS, product: .app, bundleId: "ktc.garamoi.choi.kmp.with.tuist.App", infoPlist: .extendingDefault( with: [ "UILaunchScreen": [ "UIColorName": "", "UIImageName": "", ], ] ), sources: ["iosApp/iosApp/**"], resources: ["iosApp/iosApp/**"], scripts: [ .pre( script: """ cd "$SRCROOT" ./gradlew :shared:embedAndSignAppleFrameworkForXcode """, name: "Build KMP" ) ] ) ] ) 4-3-3. Tuist/Package.swift // swift-tools-version: 6.0 import PackageDescription #if TUIST import struct ProjectDescription.PackageSettings let packageSettings = PackageSettings( productTypes: [:] ) #endif let package = Package( name: "App", dependencies: [ ] ) 4-4. 古いXcodeプロジェクトを削除する ./iosApp/iosApp.xcodeproj を削除します。 ./.gitignore に *.xcodeproj を追加します。 4-5. 確認 Tuistの設定が正しくできたか確認します。 TuistのManifestファイルをXcodeで開けることを確認します。 # KmpWithSwiftディレクトリで tuist edit TuistでXcodeプロジェクトを生成します。 # KmpWithSwiftディレクトリで tuist generate Xcodeが開いたらアプリを実行します。 アプリが正常に起動すれば完了です。 5. 共通機能からiOS設定を分離する 共通モジュールの ./shared/build.gradle.kts は、共通のビジネスロジックのビルド設定とiOS専用の XCFramework のビルドの責任範囲が適切に分離されていません。 kotlin { // ... listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { baseName = "shared" isStatic = true } } // ... } 5-1. 対応 下記のiOSの XCFramework 設定を :shared から分離して ios へ移動すると、より自然な形で設定でき、マルチモジュール化も楽になります。 it.binaries.framework { baseName = "shared" isStatic = true } 5-2. 手順 ./iosApp/shared モジュールからXCFrameworkをビルドする。 App ターゲット( ./iosApp/iosApp ディレクトリー)のスクリプトを更新する。 5-2-1. :iosApp:shared モジュール追加 ./iosApp/shared/build.gradle.kts ファイルを追加し、 ./settings.gradle.kts にモジュールを追加します。 Androidの設定は不要です。 // ./iosApp/shared/build.gradle.kts plugins { alias(libs.plugins.kotlin.multiplatform) } kotlin { listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { baseName = "shared" isStatic = true export(projects.shared) } } sourceSets { commonMain.dependencies { api(projects.shared) } } } // ./settings.gradle.kts // ... 省略 ... rootProject.name = "KmpProject" include( ":androidApp", ":iosApp:shared", ":shared" ) ./shared/build.gradle.kts からXCFramework設定を削除します。 // ./shared/build.gradle.kts plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) } kotlin { // ... 省略 ... iosX64() iosArm64() iosSimulatorArm64() // ... 省略 ... } // ... 省略 ... 5-2-2. Tuistターゲット更新 Project.swift の scripts のGradleコマンドを更新します。 scripts のモジュールを :shared から追加した :iosApp:shared へ変更します( ./gradlew :shared:embedAndSignAppleFrameworkForXcode ➡️ ./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode )。 // ./Project.swift import ProjectDescription let project = Project( name: "KmpWithSwift", targets: [ .target( name: "App", destinations: .iOS, product: .app, bundleId: "ktc.garamoi.choi.kmp.with.tuist.App", infoPlist: .extendingDefault( with: [ "UILaunchScreen": [ "UIColorName": "", "UIImageName": "", ], ] ), sources: ["iosApp/iosApp/**"], resources: ["iosApp/iosApp/**"], scripts: [ .pre( script: """ cd "$SRCROOT" ./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode """, name: "Build KMP" ) ] ) ] ) 6. マルチモジュール化 アプリが成長するとフィーチャーモジュール化が必要になります。 %%{ init: { 'theme': 'neutral' } }%% graph TB App ==> FeatureA App ==> FeatureB FeatureA --> :iosApp:shared FeatureB --> :iosApp:shared しかし各フィーチャーが :iosApp:shared が必要な場合、Tuist設定は下記のようになります。 // ./Project.swift import ProjectDescription let project = Project( name: "KmpWithSwift", targets: [ .target( name: "App", destinations: .iOS, product: .app, bundleId: "ktc.garamoi.choi.kmp.with.tuist.App", infoPlist: .extendingDefault( with: [ "UILaunchScreen": [ "UIColorName": "", "UIImageName": "", ], ] ), sources: ["iosApp/iosApp/**"], resources: ["iosApp/iosApp/**"], dependencies: [ .target("FeatureA"), .target("FeatureB") ] ), .target( name: "FeatureA", destinations: .iOS, product: .framework, bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureA", infoPlist: .default, sources: ["iosApp/FeatureA/**"], resources: ["iosApp/FeatureA/**"], scripts: [ .pre( script: """ cd "$SRCROOT" ./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode """, name: "Build KMP" ) ] ), .target( name: "FeatureB", destinations: .iOS, product: .framework, bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureB", infoPlist: .default, sources: ["iosApp/FeatureB/**"], resources: ["iosApp/FeatureB/**"], scripts: [ .pre( script: """ cd "$SRCROOT" ./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode """, name: "Build KMP" ) ] ) ] ) この設定では下記の2つの大きな課題があります。 共通のXCFrameworkがフィーチャーモジュール( :iosApp:shared )の数の分ビルドされてしまいます。 フィーチャーモジュールのターゲット設定やビルドオプションによっては、アプリが複数バージョンの :iosApp:shared を利用してしまう恐れが有ります。 この問題を解決するために、 :iosApp:shared をTuistターゲットでラップします。 6-1. Wrappingターゲット追加 フィーチャーモジュールが直接にGradleの :shared モジュールを使わず、Xcodeのターゲットを共有するように KmpCore ターゲットを追加します。 %%{ init: { 'theme': 'neutral' } }%% graph TB App ==> FeatureA App ==> FeatureB FeatureA ==> KmpCore FeatureB ==> KmpCore KmpCore --> :iosApp:shared ソースコードは iosApp/shared/** で :iosApp:shared と同じですが、KMPで生成するネームスペースとカプセル化してWrappingターゲットを使うように KmpCore にします。 この対応により、KMP共通コードの情報を持っているターゲットは KmpCore のみになります。 // ./Project.swift import ProjectDescription let project = Project( name: "KmpWithSwift", targets: [ .target( name: "App", destinations: .iOS, product: .app, bundleId: "ktc.garamoi.choi.kmp.with.tuist.App", infoPlist: .extendingDefault( with: [ "UILaunchScreen": [ "UIColorName": "", "UIImageName": "", ], ] ), sources: ["iosApp/iosApp/**"], resources: ["iosApp/iosApp/**"], dependencies: [ .target("FeatureA"), .target("FeatureB") ] ), .target( name: "FeatureA", destinations: .iOS, product: .framework, bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureA", infoPlist: .default, sources: ["iosApp/FeatureA/**"], resources: ["iosApp/FeatureA/**"], dependencies: [.target(name: "KmpCore")] ), .target( name: "FeatureB", destinations: .iOS, product: .framework, bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureB", infoPlist: .default, sources: ["iosApp/FeatureB/**"], resources: ["iosApp/FeatureB/**"], dependencies: [.target(name: "KmpCore")] ), .target( name: "KmpCore", destinations: .iOS, product: .framework, bundleId: "ktc.garamoi.choi.kmp.with.tuist.KmpCore", infoPlist: .default, sources: ["iosApp/shared/**"], resources: ["iosApp/shared/**"], scripts: [ .pre( script: """ cd "$SRCROOT" ./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode """, name: "Build KMP" ) ] ) ] ) 6-2. KmpCore で shared を露出する 単純に KmpCore ターゲットが shared へ依存性を持つだけでは FeatureA と FeatureB から :shared のコードへアクセスができません。 FeatureA と FeatureB から KmpCore を経由してKMP共通コード(Gradleの :shared モジュール)へアクセスできるように追加設定が必要です。 まずは KmpCore ターゲットに settings 設定を追加します。 // ./Project.swift import ProjectDescription let project = Project( name: "KmpWithSwift", targets: [ // ... 省略 ... .target( name: "KmpCore", destinations: .iOS, product: .framework, bundleId: "ktc.garamoi.choi.kmp.with.tuist.KmpCore", infoPlist: .default, sources: ["iosApp/shared/**"], resources: ["iosApp/shared/**"], scripts: [ .pre( script: """ cd "$SRCROOT" ./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode """, name: "Build KMP" ) ], settings: .settings(base: [ "FRAMEWORK_SEARCH_PATHS": "iosApp/shared/build/xcode-frameworks/**", "OTHER_LDFLAGS": "-framework shared" ]) ) ] ) KmpCore ターゲットが shared ネームスペースを KmpCore ネームスペースで露出するするように下記のSwiftファイルを追加します。 // ./iosApp/shared/KmpCore.swift @_exported import shared 6-3. 確認 Xcodeでプロジェクトをビルドしたら FeatureA と FeatureB が import KmpCore したら :shared へアクセス出来ます。 例えば :shared モジュールに SomeModel ( shared/src/commonMain/kotlin/ktc/garamoi/choi/kmp/with/tuist/SomeModel.kt )のクラスがある場合 FeatureA から下記のようにアクセスできます。 import Foundation import KmpCore public class SomeFeatureAClass { let model: SomeModel // ... } もしコンパイルエラーが発生する場合は、ビルド順やキャッシュの影響で一度目のビルドが不安定になることがあります。その場合はクリーンビルド又は複数回のビルドを試すことで解決できます。 7. 結論 Xcodeの *.xcodeproj は自動化とチーム開発に適していません。 Xcodeプロジェクトの *.xcodeproj の代替として Tuist の使用を推奨します。 KMP共通モジュールのXCFrameworkの生成をXcodeプロジェクトのターゲットでラップすることで、フィーチャーモジュール化が容易になります。 8. 参考 Kotlin Multiplatform : Kotlin言語でクロスプラットフォーム開発ができる色んなツールを提供するKotlin公式プロジェクト。 Gradle : Android、Javaプロジェクトのde-factoビルドツール。 Tuist : Xcodeプロジェクトのビルドツール。 Swift : AppleがObjective-Cの代わりに開発したOOP言語。 Xcode : Appleプラットフォーム用のIDE。 Xcode / Projects and workspaces Swift Package Manager : Swift 言語の公式依存性管理ツール。 XcodeGen : YAMLとJSONで Xcode Project を生成するツール。 Bazel : Googleが開発したビルドツール。大規模 Monorepo を対象にする。 Monorepo Explained : 複数のSWを1つのレポジトリーで管理する仕組み。 Google Trends : xcode conflict , xcode merge , xcode dev : Xcodeの開発全般的な検索に比べてXcodeのコンフリクトの検索の割合が高い。 What is project.pbxproj in Xcode Project configuration / Projects / Project settings : Android Studio, IntelliJ IDEAの .idea ディレクトリーの説明。 Migrate an Xcode project : マニュアルで既存のXcodeプロジェクトをTuistプロジェクト化する手順。 Homebrew : macOSのシステムパッケージ管理ツール。 Install Tuist Xcode / Bundles and frameworks / Creating a static framework Swift logo : 下段から公式ロゴをダウンロード出来ます。 Kotlin logo Gradle logo Tuist logo KINTO かんたん申し込み : Androidアプリ KINTO かんたん申し込み : iOSアプリ Choi Garamoi
アバター
はじめに こんにちは!セキュリティ・プライバシーG所属の たなちゅー です。 本記事では、弊社で最近発生した生成AIチャットツールに関連するセキュリティ事案についてお話しします。技術的に特別新しいものではありませんが、発生した状況が少し珍しいケースだったため、紹介させていただきます。 事案の概要 弊社では、生成AIチャットツールを全社員が気軽に利用できる環境を整えています。ある日、そのツールを使用していた社員が生成AIへ質問した際に、回答として提示されたリンクへアクセスしたところ、「サポート詐欺サイト」が表示される事案が発生しました。 生成AIチャット イメージ図 サポート詐欺サイト イメージ図 セキュリティ製品でもブロックできず結果としてサポート詐欺サイトへ誘導されましたが、社員の冷静な判断により、大きな被害を防ぐことができました。 このことから、ブラウザによるインターネット検索と同様ですが、生成AIが提示するリンクが常に安全であるとは限らないことを実感する事案となりました。 事案の発生要因 生成AIがサポート詐欺サイトを提示した要因調査を目的に、インターネットアーカイブサービス「 WAYBACK MACHINE 」で生成AIが提示したWebサイトを検索したところ、このWebサイトが過去に正規と思われるコンテンツを提供していることが確認されました。 また、このWebサイトを参考情報として紹介しているサイトが数件、存在することも確認されました。 このことから、問題となったWebサイトの過去のコンテンツ情報や紹介しているWebサイトの情報をもとに生成AIが学習した結果、生成AIが誤った情報を提示するハルシネーションに類する現象が発生し、サポート詐欺サイトのリンクを提示した可能性が考えられます。 以下は調査結果をもとに問題となったWebサイトのコンテンツ変遷を整理した内容です。 2012年~2018年前半 ドメイン名に合致したコンテンツ履歴あり。この時点では信頼性のあるサイトと判断されていたと推測される。 2018年後半~2019年前半 ドメイン管理サービスの販売画面が表示され、運営者がドメインを手放した可能性がある。 2019年後半以降 ドメイン名に関連性のないコンテンツ(病気、オンラインカジノ、偽警告画面、ドメインパーキングなど)の履歴あり。 セキュリティ製品で検知できなかった要因調査については、「 付録:調査メモ 」をご覧ください。 類似事案への対策 以下の観点から、このような事案に対して現時点では根本的な対策を講じることは非常に難しいと考えられます。 生成AIの学習の課題 この事案が発生した背景として、問題となったWebサイトの過去のコンテンツ情報や紹介しているWebサイトの情報をもとに生成AIが学習した可能性があります。一度学習されたWebサイトのコンテンツが変更されても、そのセキュリティリスクが生成AIの回答に反映されることは難しいと考えられます。 最大の対策は「知ること」 この事案から得られる教訓は、「生成AIが提示するリンクが常に安全であるとは限らない」ということを知ることです。事例を学び、実際に遭遇したときにどのように対処すべきかを理解することが大切です。 まとめ 今回の事案は、生成AIが提示するリンクが必ずしも安全ではないことを示す、少し珍しいケースでした。不正サイトの特性によっては、セキュリティ製品でも検知が難しい場合があります。また、生成AIが学習した後にサイトコンテンツが変更されると、そのリスクを反映できない可能性もあります。 現時点では、根本的な対策は難しいと考えられますが、こうした事例を知ることで、生成AIを利用する際のセキュリティ意識を高めるきっかけになればと思います。 付録:調査メモ セキュリティ製品で検知できなかった要因調査のメモです。あくまでも簡単な調査となりますので、参考程度にご覧ください。 1. セキュリティベンダーの検知状況 弊社で利用しているセキュリティ製品や「 VirusTotal 」で問題となったWebサイトのドメインを検索したところ、ほぼ全てのベンダーが「安全」判定となっていました。 2. Webサイトのソースコード 問題となったWebサイトのソースコードを確認したところ、「domaincntrol[.]com(c の後ろに o 無し)」へリクエストを送信後、レスポンス情報をもとに、訪問者の誘導先を動的に決定する仕組みが実装されていると考えられます。 実際に安全な環境で数回アクセスを試みたところ、サポート詐欺サイトやドメインパーキングなど、異なるWebサイトへ遷移することが確認されました。これにより、セキュリティベンダーによる悪性判定を回避した可能性があります。 3. サポート詐欺サイトのホスティング環境 最終的に表示されるサポート詐欺サイトは「web.core.windows[.]net」ドメインにホストされており、Microsoft Azure環境が利用されていると推測されます。Microsoft Azureに限らず、クラウドサービスを利用した不正サイトは、環境構築の容易さや業務影響の観点からクラウドサービスのドメインをブロックすることが事実上不可能であることから、セキュリティ製品でブロックすることが難しいと考えられます。 ※本記事公開時点で今回のサポート詐欺サイトが、Microsoft Azureから削除されていることを確認しています。 4. PublicWWWでの調査結果 Webサイトのソースコードを検索できるツール「 PublicWWW 」を用いて、問題となったWebサイトの特徴的な文字列「domaincntrol[.]com/?orighost=」を検索したところ、2万件以上のサイトでこの文字列を含むコードが使用されていることが確認されました。また、その中からいくつかのサイトを調査した結果、同様にサポート詐欺サイトへ誘導される挙動が確認されました。
アバター
Introduction My name is Endo, and I am the leader of the QA group at KINTO Technologies. In this article, I'd like to give you a look into our daily QA work especially through our daily efforts on subjects that anyone involved in QA may have encountered in the course of their daily tasks. There are already a couple of articles about QA work: Manager Zume's Quality Assurance Group Introduction Team Member Okapi's Increased Awareness of QA So I hope you will read them as well. I hope this article sparks conversations like, 'Here’s how we do it at our workplace,' or 'How do you usually handle this kind of task? I'd love to hear your thoughts! QA Workflow and What We Focus On Our QA work mainly involves: QA for development projects QA for maintenance and updates Test Automation Internal QA improvements QA work improvement activities Study sessions   In this article, I'll walk you through the QA workflow for projects and share the key perspectives we keep in mind. Different Views on QA The outline of QA work is explained during the orientation when joining the company and at the kickoff of each project. However, since all project members are experienced professionals, they have their own ideas and expectations of QA based on their past experience. Because of that, we sometimes run into misunderstandings like, "Isn't QA supposed to check things down to this level?" Such gaps in understanding can even pop up among team members. However, despite differences in what people expect from QA, I think we're mostly aligned on one key point:  "We want to identify and resolve any lingering quality concerns during the QA phase." I think the key is to figuring out how to address those concerns, ease that uncertainty, and make sure the release goes smoothly. To start, we go over the following three main points during the project kickoff, so everyone involved has a clear understanding of QA. (1) Let's build quality together! (2) If you have any concerns about quality, don't hesitate to reach out. (3) QA's feedback isn't absolute, so let's consider whether it can be addressed as a project. Quality is something we build together! Point (1) might go without saying, but quality can't be improved by QA alone. As a result of the aforementioned misunderstandings, we sometimes get requests that lean more toward white-box testing, like: Checking the details at the unit level Checking with a focus on source and data flow And more. QA work mainly focuses on black-box testing, so when it comes to white-box testing, we usually explain that it's more suited to be performed by the development side. These requests often come from past experiences where QA helped before, or from a hope that QA will "definitely" catch issues they might have missed. While there's generally a shared understanding that QA handles things like system tests, acceptance tests, and checks at each phase, people's expectations can still vary depending on their past experience. Some may expect QA to dig into system-level checks like source and data flow, while others expect a more user-centered checks. In general, though, I think QA is widely seen as a team that supports and checks quality control using its own indicators, including the standardization of all development processes. While we in QA want to meet all those expectations, there are limits to what we can catch, and the testing time is not endless. So, it is necessary for everyone to approach quality as something we build "together," especially when working under tight timelines to achieve our quality goals. However, it's not just about QA verifying requirements, pointing out issues, and having them fixed as a routine task. We believe better results come when we're aligned on QA specifications, test plans, and test timing by matching requirements together , aligning test perspectives together , and considering improvements together . And through those thorough discussions, we can apply those insights to test design. Developer Concerns Are a Goldmine of Tips for Improving Quality In reviews from a testing perspective, we can usually identify specific concerns that the development team has. But sometimes, the feedback is more vague, like simply feeling risky about a certain process. Even if those concerns aren't clearly articulated, they're incredibly valuable for QA when defining test perspectives and designing tests. That vague sense of unease often comes from things like complex code, unclear requirements, or uncertainty about whether all the finer details were fully nailed down. Even when everything seems to be working fine, so it's hard to put into words, but developers still have that gut feeling that something might be off. Surprisingly, testing these areas can sometimes reveal unexpected issues, so these instincts shouldn't be taken lightly. As QA, we review the requirements and plan and design tests to address specific concerns. These instincts offer valuable hints that help us strengthen our testing and apply deeper coverage than originally planned. In the QA phase, our goal isn't really to catch unit- or integration-level bugs. Instead, it's about executing hundreds of test cases based on requirements and catching that one critical or fatal bug. That's where QA really shows its value. So, rather than worrying about whether a developer's gut feeling might lead to wasted effort, we try to create an environment where those instincts and hints can be freely shared. By encouraging open conversation, we can uncover areas to strengthen beyond the test perspectives which helps us design better tests and ultimately improve quality. For example, instead of ending a test perspective meeting like, "Here's how we plan to proceed. Please let us know if anything comes up." We try to add a simple question like, "Is there anything in this spec that makes you feel uneasy?" By adding this, the conversation can turn into something like, "Now that you mention it..." Even if you have concerns but can't put them into words, you may feel there's nothing worth sharing. But let's talk to QA anyway! By taking the aforementioned together approach, we can strengthen our test coverage and often uncover a goldmine of valuable insights in the process. Don't Get Distracted by QA Feedback, Stay Focused on the Original Goal If QA is seen as only working within a limited test scope, it can sometimes lead to misunderstandings such as thinking QA merely follows the requirements without understanding the code, focuses on overly detailed issues, and causes delays to the release schedule. However, by providing the above-mentioned explanation in advance, people start to see QA in a completely different light, seeing it as a team that builds quality "together." On the other hand, with the change in perspective, some people start to worry that everything QA "points out" must be addressed before the release can go live. What QA "points out" with testing fall into the following three categories:   Bugs: QA points out behavior (display) that differs from the specifications as understood by QA Improvement requests: There is no problem with the specifications, but QA suggests a change to improve the functionality Questions: Ask about unclear points in the specifications For bugs, QA points them out when we notice behavior that doesn't match the expected specifications. The development team reviews the issue, makes any necessary fixes, and then QA rechecks the fix. Improvement requests are suggestions where there is no issue with the specifications, but it might be better to improve them. For example, if most screens have a button in the top right, but one screen has it in the top left, there's no problem with the button's functionality, but QA might propose moving it for consistency. Questions arise when QA finds unclear parts of the specifications while testing. We ask for clarification first, before labeling anything as a bug. It doesn't mean that all of these points need to be addressed before release. Ideally, every issue should be addressed, and every question answered, but depending on the project's circumstances, it is not realistic to tackle everything before release. If it's difficult to address all of the issues, it's the project, not QA, that decides which ones to tackle before release, based on their importance and priority. QA's work is to conduct testing based on the idea of how things should be ideally according to the requirements. Naturally, even if the requirements are well-defined, there may be some parts of the specifications that are unclear. Ideally, everything would be addressed, but within the limited time available, it is important not to lose sight of the original goal by checking and organizing the final specifications through QA's feedback. This leads to point (3), as a project, it's crucial to align on what kind of service will be delivered, and when. With that shared understanding, we can work toward building quality to meet that goal. This is where it's important for QA to provide solid support in terms of quality. Now, this might sound like we're suggesting that, given the time constraints, it's okay to just meet the bare minimum requirements and compromise on quality to release the service, but that's not the case at all. As mentioned earlier, QA's work is to help the team move toward the project's defined goals by verifying whether the specified requirements are fully met. At the same time, if there are issues that could negatively impact the user experience, we will persistently discuss issues with stakeholders to address them. It's not about making compromises, nor is it about QA forcing our feedback onto the team. Instead, we focus on the project goals and the end users receiving the service, and we work together to decide how the project should respond and take action accordingly. If a critical issue were to occur after release, the impact could be significant. So, QA stays fully engaged to help ensure quality is upheld. However, in larger projects, potential concerns are typically addressed early through the interviews mentioned in point (2). And given the nature of the development process, it's rare for a critical issue to suddenly appear at the very end. As a result, it’s extremely uncommon for the release date to be delayed due to QA work. Ultimately, what matters most is working together to achieve the target quality by the target release date. So, to repeat the important point: QA doesn't just point out issues and ask for fixes. It's about staying focused on the project's original goal to build quality together . For example, delivering the intended service to customers within the planned timeframe while making sure users aren't affected by any inconvenience or negative impact. Conclusion In this article, I focused on our mindset as QA and the communication practices we value, but from a more technical perspective, QA is also the only team that can take a cross-functional, bird's-eye view across all projects and products. If the opportunity arises, I'd like to share how QA provides support along with our approaches to test planning, defining test perspectives, and test design. Sometimes, people approach us with QA requests a bit hesitantly. But the point is that we're in this together . We want to build a relationship where everyone feels comfortable collaborating without hesitation. After release, we often hear, "It was a huge help!" "Thanks so much!" At those times I always say, "Not at all! Thank you for taking QA's feedback seriously and responding with such care."I'm deeply grateful to everyone involved in the project and have great respect for their commitment. We strive every day to build trust by fully supporting the quality of the services we deliver. And when a service we've built together ends up being truly useful to users, I believe that's the true reward of working in QA. I hope this article has sparked some interest in working in QA. Also, if you're working in QA, I'd love to hear your thoughts and exchange ideas. Feel free to share your feedback on Twitter! https://twitter.com/KintoTech_Dev/status/1619979941856280577?s=20
アバター