前提
きっかけ
元々はプロンプトの書き方による影響がどのぐらいあるものなのか検証していたのですが、そこから派生して、同じプロンプトをChatGPTとGitHub Copilotに与えたらどうなるのかが気になったので検証してみました。
参考資料
プロンプトの書き方は下記記事を参考にさせていただきました。
環境
環境 | 値 |
---|---|
OS | Mac(M1) |
Language | Kotlin |
IDE | IntelliJ IDEA 2024.1.1 (Community Edition) |
SDK | corretto-21(Amazon Corretto 21.0.3 - aarch64) |
プロンプト
# 役割
あなたはKotlinの熟練プログラマーです。
# タスク
以下の要件に基づいて、Kotlinコードを作成してください。
# 要件
file: Resourceというリクエストを受け付けるUploadコントローラし、AWSのs3バケットにアップロードできるようにしたい。
# 制約条件
- MVCモデルに則ってください
- アップロード先はapplication.propertiesで設定可能とし、プログラム上はConfigクラスで表現したい
- 依存関係としてはaws.sdk.kotlin:s3の最新バージョンを利用したい
- ドメイン駆動開発の考え方に則ってください
- テストコードも書いて欲しい
# 出力形式
- コードブロックを使用してください
- 必要に応じて、使用方法の例を追加してください
生成内容比較
実行内容の宣言
ChatGPT
Kotlinを使用して、AWS S3バケットにファイルをアップロードするシステムをMVCアーキテクチャに基づいて実装します。以下は、UploadController
と S3へのアップロードを管理するコードです。また、application.properties
でアップロード先の設定を行い、ドメイン駆動開発の原則に基づいて実装します。
GitHub Copilot
以下の手順に従って、Resourceというリクエストを受け付けるUploadコントローラを作成し、ドメイン駆動開発(DDD)の考え方に則ってAWSのS3バケットにアップロードできるようにします。
比較結果からわかったこと
特になし。
AWS SDK for Kotlin依存関係の追加
ChatGPT
plugins {
kotlin("jvm") version "1.9.0" // 最新のKotlinバージョンを指定
id("org.springframework.boot") version "3.1.0" // Spring Bootの最新バージョンを指定
kotlin("plugin.spring") version "1.9.0"
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-test") // テスト用
// AWS SDK for Kotlin (S3)
implementation("aws.sdk.kotlin:s3:1.14.3") // AWS SDK for Kotlin (S3) の最新バージョン
// Jackson(JSONシリアライズ/デシリアライズ用)
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
// テスト関連のライブラリ
testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.7.0")
testImplementation("org.mockito:mockito-core:4.5.1")
testImplementation("org.springframework.boot:spring-boot-starter-test")
// Spring Testのサポート
testImplementation("org.springframework:spring-test")
}
tasks.test {
useJUnitPlatform() // JUnit5の使用を明示的に指定
}
GitHub Copilot
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("aws.sdk.kotlin:s3:0.20.0-beta")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
比較結果からわかったこと
GitHub Copilotの方は、そのプロンプトを実行しているプロジェクトファイル構造をもとに必要な修正を提案してくれるので、最小限の提案となっていることがわかりますね。
ただ、GitHub Copilotの方は、プロンプトで依頼したaws.sdk.kotlin:s3の最新バージョン
が2023年2月にリリースされた0.20.0-beta
になっているのが気になりました。
基本的に学習時期のことを考えると完全に最新バージョンになることはない認識でしたが、GitHub Copilotの方が若干情報が古い?
これはGitHub Copilotがオープンソースコードやプログラミングに関する書籍・資料などを対象として学習していることを起因としている可能性が高そうですね。Web上のテキストデータも学習データに含めているChatGPTの方が、最新情報は拾いやすいのかなと感じました。
application.properties設定
ChatGPT
aws.s3.bucketName=my-bucket-name
aws.s3.region=us-east-1
aws.accessKeyId=your-access-key-id
aws.secretAccessKey=your-secret-access-key
GitHub Copilot
aws.s3.bucket-name=your-bucket-name
aws.s3.access-key=your-access-key
aws.s3.secret-key=your-secret-key
aws.s3.region=your-region
比較結果からわかったこと
ほぼほぼ一緒ですが、強いていえばChatGPTのバケット名はmyなのがちょっと面白いですね。作成済バケットを対象としている想定なのかな。
Configクラス
ChatGPT
package com.example.config
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
@Configuration
class AwsS3Config {
@Value("\${aws.s3.bucketName}")
private lateinit var bucketName: String
@Value("\${aws.s3.region}")
private lateinit var region: String
@Value("\${aws.accessKeyId}")
private lateinit var accessKeyId: String
@Value("\${aws.secretAccessKey}")
private lateinit var secretAccessKey: String
@Bean
fun s3Client(): S3Client {
val credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey)
return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build()
}
@Bean
fun getBucketName(): String {
return bucketName
}
}
GitHub Copilot
package com.example.demo.config
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
@Configuration
class AwsConfig {
@Value("\${aws.s3.bucket-name}")
lateinit var bucketName: String
@Value("\${aws.s3.access-key}")
lateinit var accessKey: String
@Value("\${aws.s3.secret-key}")
lateinit var secretKey: String
@Value("\${aws.s3.region}")
lateinit var region: String
}
比較結果からわかったこと
GitHub Copilotは既存のプロジェクト構造を読み取って生成するので、package名がきちんと現在のプロジェクト構造に合わせたものなっているのがありがたいですね(自分がdemoというプロジェクト名にしているせいで分かり辛いですが)。
また、ChatGPTの方はConfigクラスにS3Clientが定義されてて???となりました。プログラミングにおけるフレームワークの理解や実装ノウハウについては、さすがにChatGPTに軍配が上がる形でしょうか。
ドメイン層の定義
ChatGPT
package com.example.service
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import software.amazon.awssdk.services.s3.model.S3Exception
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import software.amazon.awssdk.services.s3.S3Client
import java.nio.file.Path
import java.nio.file.Files
import java.io.File
@Service
class S3UploadService(
private val s3Client: S3Client,
@Value("\${aws.s3.bucketName}") private val bucketName: String
) {
fun uploadFile(file: File): String {
try {
val filePath = file.toPath()
val putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(file.name)
.build()
s3Client.putObject(putObjectRequest, filePath)
return "File uploaded successfully: ${file.name}"
} catch (e: S3Exception) {
throw RuntimeException("Failed to upload file to S3: ${e.message}", e)
}
}
}
GitHub Copilot
エンティティの作成。
package com.example.demo.domain
data class Resource(
val name: String,
val content: ByteArray
)
リポジトリの作成。
package com.example.demo.repository
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
import com.example.demo.config.AwsConfig
import com.example.demo.domain.Resource
import org.springframework.stereotype.Repository
import java.io.ByteArrayInputStream
@Repository
class S3Repository(private val awsConfig: AwsConfig) {
private val s3Client = S3Client { region = awsConfig.region }
suspend fun uploadFile(resource: Resource) {
val putObjectRequest = PutObjectRequest {
bucket = awsConfig.bucketName
key = resource.name
body = ByteArrayInputStream(resource.content)
}
s3Client.putObject(putObjectRequest)
}
}
比較結果からわかったこと
ChatGPTは、ドメイン層にサービスが定義されていて、ドメイン駆動開発に基づいている感じがしないですね。一方でGitHub Copilotはエンティティやリポジトリといったドメイン駆動開発の概念に基づいてクラス定義してくれているのがわかります。
サービス層の定義
ChatGPT
生成なし(ドメイン層の定義と言いながらサービスクラスを定義しているため)
GitHub Copilot
package com.example.demo.service
import com.example.demo.domain.Resource
import com.example.demo.repository.S3Repository
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
@Service
class ResourceService(private val s3Repository: S3Repository) {
suspend fun uploadResource(file: MultipartFile) {
val resource = Resource(
name = System.currentTimeMillis().toString() + "_" + file.originalFilename,
content = file.bytes
)
s3Repository.uploadFile(resource)
}
}
比較結果からわかったこと
GitHub Copilotの方がやはり内容の精度は高いですね。
コントローラの定義
ChatGPT
package com.example.controller
import com.example.service.S3UploadService
import org.springframework.web.bind.annotation.*
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.multipart.MultipartFile
import org.springframework.beans.factory.annotation.Autowired
@RestController
@RequestMapping("/api/upload")
class UploadController(
private val s3UploadService: S3UploadService
) {
@PostMapping
fun uploadFile(@RequestParam("file") file: MultipartFile): ResponseEntity<String> {
val tempFile = File.createTempFile("upload", file.originalFilename)
file.transferTo(tempFile)
return try {
val uploadMessage = s3UploadService.uploadFile(tempFile)
ResponseEntity(uploadMessage, HttpStatus.OK)
} catch (e: Exception) {
ResponseEntity("Error uploading file: ${e.message}", HttpStatus.INTERNAL_SERVER_ERROR)
} finally {
// 一時ファイルを削除
tempFile.delete()
}
}
}
GitHub Copilot
package com.example.demo.controllers
import com.example.demo.service.ResourceService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
class UploadController(private val resourceService: ResourceService) {
@PostMapping("/upload")
suspend fun uploadFile(@RequestParam("file") file: MultipartFile): ResponseEntity<String> {
resourceService.uploadResource(file)
return ResponseEntity("File uploaded successfully", HttpStatus.OK)
}
}
比較結果からわかったこと
ChatGPTはなぜ一時ファイルを生成することにしたんだろう。逆にどういう原理でそのコードを生成したのか気になりますね。ひとまず、GitHub Copilotの方がクリーンな構造定義で気持ちいいです。
テストコード定義
ChatGPT
package com.example.controller
import com.example.service.S3UploadService
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mockito.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.mock.web.MockMultipartFile
@ExtendWith(SpringExtension::class)
@SpringBootTest
class UploadControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var s3UploadService: S3UploadService
@Test
fun `should upload file successfully`() {
val mockFile = MockMultipartFile("file", "testfile.txt", "text/plain", "content".toByteArray())
`when`(s3UploadService.uploadFile(any())).thenReturn("File uploaded successfully: testfile.txt")
mockMvc.perform(
MockMvcRequestBuilders.multipart("/api/upload")
.file(mockFile)
.contentType(MediaType.MULTIPART_FORM_DATA)
)
.andExpect(status().isOk)
.andExpect(content().string("File uploaded successfully: testfile.txt"))
}
@Test
fun `should handle upload failure`() {
val mockFile = MockMultipartFile("file", "testfile.txt", "text/plain", "content".toByteArray())
`when`(s3UploadService.uploadFile(any())).thenThrow(RuntimeException("Error uploading file"))
mockMvc.perform(
MockMvcRequestBuilders.multipart("/api/upload")
.file(mockFile)
.contentType(MediaType.MULTIPART_FORM_DATA)
)
.andExpect(status().isInternalServerError)
.andExpect(content().string("Error uploading file: Error uploading file"))
}
}
GitHub Copilot
package com.example.demo
import com.example.demo.service.ResourceService
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.web.multipart.MultipartFile
@WebMvcTest
class UploadControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var resourceService: ResourceService
@Test
fun `test file upload`() {
val file: MultipartFile = mock()
mockMvc.perform(MockMvcRequestBuilders.multipart("/upload").file("file", file.bytes))
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(MockMvcResultMatchers.content().string("File uploaded successfully"))
verify(resourceService).uploadResource(file)
}
}
比較結果からわかったこと
ここまでくると、テスト以外の構造が違いすぎて比較し辛いですが、テストも生成できるのはどちらも素晴らしいですね。その上で、テストの実装もやはり細かいところでGitHub Copilotの方がクリーンな印象を受けますね。
まとめ
比較項目 | ChatGPT | GitHub Copilot |
---|---|---|
コードの質 | △ | ⚪︎ |
フレームワークや設計技法の理解 | △(ものによるけど、今回はイマイチ) | ⚪︎ |
プロジェクト構造の考慮 | ×(これはやむなし) | ⚪︎ |
情報の最新性 | ⚪︎(Copilotと比べての話) | △ |
実行する度に生成結果が変わるのもありますし、プロンプトの定義の仕方にも改善の余地はあるのだろうとは思いますが、今回に関しては上記のような結果となりました。
なお、当然のことですが、今回の記事は「ChatGPTはだめ」ということが言いたい訳ではないです。
ChatGPTはプログラミングに関わらないところも含めて色々聞けるので、汎用性の高さはずば抜けている印象です。誰でも無料で使えるのもすごいですしね。
ただ、プログラミングという点について言えば、やはりGitHub Copilotの方がより質の高い実装を提供してくれるように感じました。まあそうでないとお金払ってまで使おうとはならないという話ではありますが……。
GitHub Copilotがどこまでよりプログラミングに適しているのか、今までは漠然と「そうなんだろうな」と推測していただけだったので、肌で感じてより実感を持てたのは良い機会だったかなと思います。
余談
aws.sdk.kotlin:s3
の最新バージョンを具体的に指示した場合、プロンプトを元に学習が行われ、以後は指定したバージョンを最新のものと認識してコード生成するようになりました。
曖昧さをなるべく排除するのも大事そうですね。