Jetbrains IDE 플러그인, RefactorGPT 제작기

숑숑·2023년 7월 23일
7

회고

목록 보기
4/7
post-custom-banner

에디터에서 원하는 코드를 드래그하고, Alt(option) + R을 눌러보세요.
ChatGPT를 기반으로 리팩토링해주고, 드래그한 영역에 바로 적용할 수 있습니다.
Jetbrains Marketplace
github

뭐하는 플러그인인가요?

코드 리팩토링해주는 매우 단순한 기능의 플러그인입니다.
자세한 설명은 생략하고, 사용자 여정 순서대로 화면으로 보여드리겠습니다.

  1. 아래 화면에서 ChatGPT API 키와, API 타임아웃을 설정합니다.
  1. 리팩토링할 코드를 드래그합니다.
    리팩토링 전

  2. Alt(option) + R (변경가능)을 누르거나, 우측 마우스를 눌러 아래 영역을 클릭합니다.

  1. 팝업창이 뜨고, ChatGPT의 리팩토링 결과를 기다립니다.

  2. 리팩토링 결과를 확인하고, 필요 시 오른쪽 에디터에서 수정합니다.

  3. Apply 버튼을 눌러 에디터에 바로 반영합니다.

왜 만들었냐면

사이드 프로젝트를 할 때, 일부 코드를 복사해서 ChatGPT에 리팩토링해달라고 프롬프팅하는 일이 꽤 있었습니다. 코드가 뭔가 개선의 여지가 있어보이면, ChatGPT가 알잘딱깔센으로 조언을 잘 해줬기 때문이죠. (비즈니스 코드는 조심해서 쓰세요...!!!)
무조건 더 나은 코드를 주는건 아니지만, 고민되면 GPT에 한번 의견을 물어보게 되더라구요.

에디터에서 코드 영역 복사 -> ChatGPT 프롬프트에 붙여넣기 -> (결과 코드 검토) -> 답변 중 코드 부분을 추출 -> 복사 후, 약간의 수정을 거쳐서 에디터 코드 영역에 붙여넣기

이게 생각보다 많이 반복되자 서서히 귀찮아지기 시작했습니다.
자동화할 방법을 생각해보니, 가장 깔끔한 포맷은 IDE의 플러그인이었습니다.

아래 캡쳐로 보시다시피, 전 Jetbrains의 IDE를 가장 좋아하고 다양하게 쓰고 있습니다 ㅎㅎ

따라서 Jetbrains IDE의 플러그인으로 개발해보기로 결심했습니다.

ChatGPT와 기획하기

크게 생각한 플러그인 기능은 위에서 설명한 것과 같습니다.
어떤 UI로 표현할지를 정해야 합니다.

전 이 분야에는 크게 자신이 없으니 ChatGPT에게 한번 일을 시켜봤습니다.

요약
1. 코드를 드래그하면, context menu로 "Refactor with RefactorGPT" 를 노출한다.
2. 사이드바에서 기존 코드와 리팩토링된 코드를 보여준다.
3. diff를 하이라이팅해서 보여준다.
4. 리팩토링 정도, 코드 스타일 등 리팩토링 옵션을 선택하게 한다.
5. 사용자가 리팩토링된 코드를 반영할지 Apply/Reject 버튼으로 선택하게 한다.
6. RefactorGPT 사이드바를 여는 키보드 단축키를 세팅한다.

뭐 좋습니다. 그런데 IntelliJ만 봐도, 사이드바 말고도 다양한 영역이 있습니다.
사이드바가 최선인지 다시 한번 물어봤습니다.

요약
사용자의 기호에 따라 다르다. 그러나 아래 세가지 대안을 선택할 수 있다.
1. 팝업창
2. 인라인 제안
3. 하단 패널

전 1번, 팝업창을 선택했습니다.
의문이 들 수 있습니다. 패널이나 사이드바를 사용하면, 하단이나 우측에 붙어있기 때문에 에디터를 크게 가리지 않는 반면, 팝업창은 중간에 떡하니 자리를 차지하게 됩니다. 호불호가 갈리는 방식인 것도 알고 있지만, 그래도 선택한 이유는

팝업창을 사용하면 커서를 옮기는게 가장 힘들기 때문입니다.

이게 무슨 말인지 히스토리를 설명하자면,
전 코드를 드래그해서 리팩토링한 코드로 '대체'하는 기능을 구상하고 있습니다. 즉, apply 버튼을 누르기 전까지 드래그한 영역을 계속 유지하고 있어야 한다는 뜻입니다.

커서를 다른 곳으로 이동하는 순간, 드래그한 영역이 해제되겠죠. 그래서 ChatGPT에 요청을 보내는 동안 사용자의 액션을 블로킹할 수 밖에 없습니다.
그럴거라면 커서를 옮기는 실수를 하기 힘든 팝업창이 가장 낫다고 생각했습니다. 직관적이기도 하고요.

사이드바로 한다면 어떻게 됐을까?
Github Copilot은 사이드바에서 현재 커서에 대해 추가적인 코드 조각을 제안하는 플러그인입니다. 사이드바로 한다면 이런 형식을 생각했습니다.
이쪽을 더 선호하는 분들도 있을 것 같습니다.

근데 어떻게 만들지?

그러나 전 Jetbrains의 플러그인을 만들어본 경험이 없었습니다.
한국어로 된 자료를 찾고 싶었으나, 플러그인 개발은 인기가 영 없는지 찾기 어려웠습니다.

그러다 Jetbrains에서 플러그인 개발자들을 위해 템플릿을 제공한다는걸 알게 됐습니다.

https://github.com/JetBrains/intellij-platform-plugin-template
CI(github actions) 파이프라인을 포함한 플러그인 프로젝트 템플릿을 제공합니다.

전 여기서 코드 정적 분석기 Qodana 설정은 제외하고 사용했습니다.
아래처럼 빌드할 때마다 Qodana 첫 설정부터 실패하는데 .. 해결책을 못 잡았습니다 🥺
이거 때문에 꽤나 시간을 썼는데 배보다 배꼽이 점점 커진다고 생각이 되어서, 추후 여유가 되면 붙이는걸 다시 시도해보려고 합니다.

개발

프로젝트 구조는 템플릿의 도움으로 갖추었고,
플러그인 개발에 대해서는 블로그로 튜토리얼을 잡은 후 진행했습니다.

혹시나 Jetbrains 플러그인 개발을 시도해보고 싶은 분들이 계시다면, 위 링크가 도움이 되실 것 같습니다.

튜토리얼 만으로 알기 어려운 부분은 ChatGPT에게 어느 정도 예제 코드를 받아가며 진행했습니다. Jetbrains 플러그인도 하나의 프레임워크처럼 어느 정도 정해져있는 구조가 있어서 그런지 잘 알고 있더라구요.

리팩토링 프롬프트

현재 소스코드에 있는 프롬프트입니다.

private fun makePrompt(fileExtension: String, code: String): String =
    """
        You role is perfect code refactoring prompt.
        Refactor the following code for better readability and maintainability.
        Don't say ANY explain. Just response the code strictly.
        This code's file extension: $fileExtension
        Here is the code:
        ```
        $code
        ```
        Respond start with the line 'Code:'
    """.trimIndent()

fileExtension 은 리팩토링할 코드의 언어를 구별하기 위해 추가했습니다.
정말 코드만 주고 리팩토링 요청했을 때, 언어를 구별 못해서 다른 언어로 응답하는 이슈가 있더라구요.. (특히 python과 ruby 구분을 잘 못 했습니다.)

나름 여러번의 트라이를 거쳐 탄생한 프롬프트긴 하나,
이게 정말 최선의 프롬프트가 맞을지 강한 의심이 들기는 합니다.

되도록 짧은 프롬프트가 좋은데,
ChatGPT API는 질의 프롬프트의 토큰 수도 포함해서 비용을 정산하기 때문입니다.

혹시나 더 좋은 아이디어가 있다면... PR 환영합니다..!!!!

기술적 난관

특히나 기술적으로 애먹었던 부분을 정리해보고자 합니다.

비동기로 ChatGPT API 응답 받아오기

그냥 비동기로 하면 되는거 아냐? 생각이 들어서 저도 처음엔 Kotlin의 동시성 패턴인 코루틴(Coroutine)을 사용해 접근했습니다.
그런데 문제는 팝업창에서 로딩화면을 보여주고 있어야 하고, 응답이 오면 로딩을 해제하며 응답을 보여줘야 합니다. UI가 정적이지 않고, 상태를 가지도록 컨트롤하는 것이 생각보다 복잡했습니다.

초기에 코루틴을 사용해 시도했던 코드입니다.

GlobalScope.launch(Dispatchers.IO) {
    runCatching {
        chatGptService.refactorCode(fileExtension, selectedCode)
    }.fold(
        onSuccess = { refactored ->
            withContext(Dispatchers.Main) {
                updateDialogWithRefactoredCode(refactored)
                setLoading(false)
            }
        },
        onFailure = {
            withContext(Dispatchers.Main) {
                Messages.showErrorDialog(
                    project,
                    "Failed to refactor code: ${it.message}",
                    "Refactor Error"
                )
                setLoading(false)
            }
        }
    )

이 코드는 제가 기대하는 동작을 잘 수행했습니다. 로컬! 테스트에서는요...
팝업창 잘 떴고, 로딩하다가 응답코드도 뜨고, apply도 잘 됐으니까요.

그런데 테스트로 띄운 IDE가 아닌, 다른 프로젝트에 플러그인을 import 해보면
로딩에서 무한대기를 하고 있었습니다.

API 응답이 오래걸리는게 아닙니다. 엄연히 타임아웃 커스텀 설정을 만들어두었거든요.
타임아웃에 도달하면, 에러창을 띄우도록 프로그래밍 되어있습니다.

GlobalScope 로 일단 해뒀던 점이 영향을 줬는지 해서, 스코프를 바꿔서 시도해보기도 했지만 마찬가지였습니다.

그래서 결국 찾은 이유는,

보통 UI를 포함하는 애플리케이션은 모든 UI작업을 처리하는 단일 이벤트 디스패치 스레드가 있다. 그 외의 다른 스레드에서 UI 상태를 수정하는 것은 보통 허용하지 않는다. 모든 UI라이브러리들은 어떤 실행구문을 UI쓰레드에서 작동하게 해주는 기본 요소를 제공합니다. 예를 들면 Swing의 SwingUtilities.invokeLater나 JavaFX의 Platform.runLater가 있고 Android는 Activity.runOnUiThread 등이 제공된다.
출처: https://github.com/hikaMaeng/kotlinCoroutineKR

Swing은 thread-safe 하지 않아서, 정해진 디스패처 스레드에서 비동기 작업을 해줘야한다는걸 몰랐습니다.
핑계를 대자면... 웹 프론트엔드를 제외한 UI 개발은 처음이었거든요 🥺
그래도 앞으로는 이런 경우 공식문서부터 서치를 해본다는 교훈을 얻었습니다. 언제나 돌고돌아 공식문서네요...

저는 Swing 기반의 UI를 구축했기 때문의 SwingUtilities.invokeLater 를 사용하는 것이 맞았습니다.

그래서 아래처럼 변경했습니다.

Thread {
    runCatching {
        chatGptService.refactorCode(fileExtension, selectedCode)
    }.fold(
        onSuccess = { refactored ->
            SwingUtilities.invokeLater {
                updateDialogWithRefactoredCode(refactored)
                setLoading(false)
            }
        },
        onFailure = { exception ->
            SwingUtilities.invokeLater {
                Messages.showErrorDialog(
                    project,
                    "Failed to refactor code: ${exception.message}",
                    "Refactor Error"
                )
                setLoading(false)
                super.close(OK_EXIT_CODE)
            }
        }
    )
}.start()

setLoading(true)
super.show()

한가지 차이를 둔 점은 코루틴을 사용하지 않고 스레드를 사용했는데,
Swing과 코루틴을 함께 사용하려면 특정한 라이브러리를 사용해야하는 등 약간 복잡해지더라구요.
항상 단일 네트워크 요청만 하는 구조기도 하고, 성능 상 큰 차이가 없을거 같아서 스레드로 했습니다.
그래도 코루틴 공부하는 겸 추후에 개선해볼까 합니다.

그렇게 퇴근하고 짬짬이 해줬더니 어느새 최소 개발을 끝냈고, 심사를 올렸습니다.
몇번 호환성 피드백을 반영한 후에, 그 결과..!!!

심사 결과


와!!!!!!!!

짜잔!!! 🎉🎉🎉🎉🎉
IDE에 제 이름 석자를 새겼습니다..!! (가끔 깃허브네임인 lauvsong으로 뜨기도 하더라구요?)

각국에서 꽤 많은 유저들이 다운로드를 해주셨더라구요..!!
잘 쓰고 계셨으면 좋겠네요🥺

앞으로는

솔직히 말하자면 그저 제가 필요해서 개발한거라, 크게 유지보수하면서 지낼 생각은 없었습니다.
그래도 다운로드 수를 보니 쓰는 분이 꽤 있는 것 같아서, 뭔가 아쉬운 소리를 해주신다면 적극 반영해보려 합니다.
모두 조금이라도 편하게 개발하시는데 도움이 됐으면 좋겠습니다...!!!

profile
툴 만들기 좋아하는 삽질 전문(...) 주니어 백엔드 개발자입니다.
post-custom-banner

4개의 댓글

comment-user-thumbnail
2023년 7월 25일

API키 발급 받아서 넣었는데 인증 실패 뜨네요.

답글 달기
comment-user-thumbnail
2023년 7월 27일

멋지십니다. 저도 자극받아서 플러그인 만들어보고싶다는 생각이드네요!

답글 달기
comment-user-thumbnail
2023년 12월 12일

글 잘 읽었습니다.
필요에 의한 동기부여가 그저 생각에서 끝나지 않고 행동으로 이어졌다는 것이 놀랍고 멋집니다.
추후에 저도 사용해보겠습니다.

답글 달기
comment-user-thumbnail
2024년 3월 11일

Failed to refactor code: Authentication failed. Check API key settings.
인증에 실패 하셨다면 openAI의 계정이 무료 등급인지 확인하세요
Assistants API 의 경우 한 세션당 0.03$ 가 필요해요

답글 달기