2차 리팩토링 및 Notification 구현

guswls·2024년 8월 20일

플러그인 개발

목록 보기
6/7

1. 들어가기 앞서


이전 글에서는 액션 클래스에 집중된 기능들을 도메인 모델에 따라 분리하는 과정에서 마주친 여러 문제점들을 다루었다.

문제점들을 요약하면 아래와 같다. 각 파트에 대해선 관련 링크를 첨부하였다.

  1. 인텔리제이 플랫폼에 서비스 객체를 적절히 등록하지 않았다.

  2. CommitWorkflowHandlerProvider에 불필요한 인터페이스를 사용하였다.

  3. DiffProvider가 변경내역 조회와 Diff 추출 두 책임을 가지게 되었다.

  4. Diff 도메인의 범위와 구성을 명확히 정의하지 않았다.

이번 글에서는 이전 리팩토링 과정에서 발생한 문제들을 어떻게 해결하였는지 코드와 함께 살펴보고자 한다.

추가적으로 액션 수행 시 발생할 수 있는 예외들을 처리할 예외 핸들러와 이를 사용자에게 알려줄 수 있는 알림까지 구현할 예정이다.
경험상 예외 처리 핸들러를 미리 구현해두면 개발 과정에서 발생하는 예외들에 바로바로 대응할 수 있어 유용했다.

코드를 보기 전에 클래스 다이어그램으로 전체 구조를 간략하게 살펴보자.




2. 클래스 다이어그램 분석


첫 번째 사진은 클래스 다이어그램으로, 두 번째 사진의 도메인 모델 다이어그램이 어떻게 실제 구현에 가깝게 구체화되었는지를 보여준다.

클래스 다이어그램의 회색 테두리는 도메인 영역을 나타내며, 이를 통해 각 구현체가 어느 도메인에 속해있는지 확인할 수 있다.

다음은 주요 클래스에 대한 간단한 요약이다.

  • GenerateCommitMessageAction : 사용자의 액션을 받아 전체 프로세스를 시작하는 트리거 역할을 수행한다.

  • CommitMessageEntryPoint : 커밋 메시지의 생성 프로세스를 조정한다.
    (도메인 모델의 Composer는 실제 구현에서 시스템 진입점의 의미를 강조하기 위해 EntryPoint로 변경)

  • CommitService : 현재 커밋의 상태를 확인하고 변경사항을 내역을 반환 역할을 수행한다.

  • DiffService : DiffDetailGenerator와 DiffSummaryGenerator를 활용하여 변경내역의 '요약'과 '상세 내용'을 생성한다.

  • NotificationService : 사용자에게 알림을 제공하는 역할을 한다.

  • ActionExceptionHandler : 액션 수행 과정에서 발생하는 모든 예외를 처리한다.

클래스 다이어그램을 도메인 모델 다이어그램과 비교해보면, 몇 가지 눈에 띄는 변화가 존재한다.

가장 눈에 띄는 것은 Diff 도메인의 세분화다. 단순히 "Diff"로만 표현되었던 개념은 DiffService, DiffDetailGenerator, DiffSummaryGenerator 으로 구체화됐다. 이것은 구현 과정에서 "Diff엔 변경을 나타내는 모든 정보들이 포함될 수 있다"라는 변경된 요구사항의 반영된 것이다.

또한, 도메인 모델엔 없었던 '알림 기능'과 '예외 처리'가 NotificationServiceActionExceptionHandler로 추가되었다. 이러한 기능은 시스템 개념에 가깝기 때문에 도메인 모델 설계 당시엔 고려하지 않았던 부분이었다.

이러한 변화를 통해 도메인 모델과 실제 구현은 일반적으로 차이가 발생할 수밖에 없으며, 추상적 개념이 구체화되는 과정에서 자연스럽게 일어나는 현상임을 알 수 있다.




3. 글의 순서


이번 글은 코드를 위주로 풀어갈 예정이다.

코드는 Top-Down 방식으로, 액션 트리거인 GenerateCommitMessageAction을 먼저 설명할 것이다.

그 뒤, CommitMessageEntryPoint를 보며 대략적인 비지니스 로직의 흐름을 살펴본 후, CommitServiceDiffService를 차례대로 살펴볼 것이다.

DiffService의 내부 구현체는 인터페이스와 함께 간단하게만 살펴 볼 것이다.

여기까지가 리팩토링에 관련된 글이고, 이후엔 새로 구현된 ActionExceptionHandlerNotificationService를 살펴볼 것이다.

참고로, 글을 읽을 때 등장하는 키워드에 대한 설명은 다음과 같다.

글에 등장하는 각 키워드들

  • 변경 내역 : 파일에 대해 '추가', '변경', '삭제', '경로 변경'과 같이 현재의 상태에서 발생된 변경에 대한 정보

  • 요약 : 위 변경내역의 요약 정보

  • 상세 내용 : 각 변경 내역의 세부 사항. 여기서는 변경된 파일의 명칭과 변경된 내용(코드)들이 포함됨.

  • Diff : 위 내용들을 종합하여 "변경내역 전체를 나타내는 모든 데이터의 집합". 현재는 '요약'과 '상세 내용'이 이 안에 속함.



4. 2차 리팩토링


4-1. GenerateCommitMessageAction

class GenerateCommitMessageAction : DumbAwareAction() {
    override fun actionPerformed(event: AnActionEvent) {
        event.project?.let { project ->
            try {
            	//필요한 시점에 서비스 호출
                project.service<CommitMessageEntryPoint>().run(event)
            } catch (ex: Exception) {
            	//전역 예외 핸들러
                project.service<ActionExceptionHandler>().handle(ex)
            }
        }
    }

	//프로젝트가 존재할 때만 업데이트를 수행
    override fun update(e: AnActionEvent) {
        e.presentation.isEnabledAndVisible = e.project != null
    }

	//액션의 업데이트를 EDT(Event Dispatch Thread)가 아닌 BGT(Background Thread)에서 하도록 변경
    override fun getActionUpdateThread() = ActionUpdateThread.BGT
}

이전의 GenerateCommitMessageAction과 비교해보면 몇 가지 차이점이 보인다. 개선점을 정리하면 다음과 같다.

GenerateCommitMessageAction의 개선 사항

  • 필드와 생성자 주입을 모두 제거하고, 필요한 위치에서 서비스를 바로 불러와서 사용하도록 변경 (이전 글의 4-1 참고)

  • ActionHandler를 통해 액션 수행 시 발생하는 모든 예외를 중앙에서 관리하도록 구현

  • update()getActionUpdateThread() 메서드를 적절히 오버라이딩

평가

이 리팩토링을 통해 GenerateCommitMessageAction은 서비스 객체 등록과 액션 업데이트 방식에서 인텔리제이 플랫폼의 가이드라인을 준수하는 방향으로 개선되었다.

또한, ActionExceptionHandler를 전역 예외 핸들러로 두어 일관성있게 예외 처리를 할 수 있도록 만들었다.

이러한 변화를 통해 GenerateCommitMessageAction트리거의 역할에만 집중할 수 있게 되었으며, 내부 구현의 변경에 독립적으로 동작할 것이다.



4-2. CommitMessageEntryPoint

@Service(Service.Level.PROJECT)
class CommitMessageEntryPoint(private val project: Project) {
    fun run(event: AnActionEvent) {
        val commitService = project.service<CommitService>()
        val diffService = project.service<DiffService>()
		
        //변경내역 조회
        val changes = commitService.getCheckedChanges(event)
        //변경내역으로 부터 Diff 추출
        val diff = diffService.getDiff(changes)

        CustomLogger.info("\n$diff")
    }
}

CommitMessageEntryPoint각 서비스 간의 협력을 조정하고 전체 프로세스의 흐름을 관리하는 책임을 가지고 있다.

1차 리팩토링 땐 CommitMessageService라는 명칭이었으나, 이 객체는 직접 비지니스 로직을 수행하는 다른 서비스와는 다르다고 생각해 "EntryPoint"로 명명하였다.

CommitMessageService와의 차이점은 역시 서비스들을 메서드 내부에서 필요한 시점에만 호출하도록 변경한 것이다.
또한 @Service(Service.Level.PROJECT) 어노테이션을 붙여 서비스로 등록을 해주었다. 이 방법을 사용한 이유는 이 객체는 단일 구현체로 존재할 확률이 높기 때문이다.

평가

이 엔트리 포인트는 "코드로 구현된 기능 목록"이라고 볼 수 있다.
이곳을 통해 현재 구현된 기능과 실행 순서를 한 눈에 파악할 수 있으며, 새로운 기능을 쉽게 추가할 수도 있다.

이제 실질적인 비지니스 로직을 담당하는 CommitServiceDiffService에 대해서 알아보자.



4-3. CommitService

@Service(Service.Level.PROJECT)
class CommitService {
    fun getCheckedChanges(event: AnActionEvent): List<Change> {
    	//커밋 핸들러 조회
        val handler = getHandler(event) ?: return emptyList()

		//변경 내역이 있으면 반환하고, 없으면 NoChangeException 던짐
        return handler.ui.getIncludedChanges()
            .takeIf { it.isNotEmpty() }
            ?: throw NoChangesException()
    }

	//특정 DataKey를 통해 커밋 핸들러 가져옴.
    private fun getHandler(event: AnActionEvent) =
        event.getData(VcsDataKeys.COMMIT_WORKFLOW_HANDLER) as? AbstractCommitWorkflowHandler<*, *>
}

CommitService는 이전의 CommitWorkflowHandlerProvider를 확장하여 만들어진 객체다. 서비스의 특성을 고려하여 @Service 어노테이션을 통해 프로젝트 레벨의 서비스로 등록하였다.

만약 변경내역이 없으면 NoChangeException이라는 커스텀 예외를 던지도록 구현하였다. 이 예외에 대한 처리는 ActionExceptionHandler에서 이루어지게 된다.

평가

이전엔 단순히 커밋 핸들러만 제공했던 것과 달리, CommitService'커밋 핸들러 조회''변경내역 가져오기'내부에 캡슐화하여 책임의 경계를 명확하게 하였다.

이를 통해 외부에서는 커밋 핸들러의 존재를 알 필요 없이 직접 변경내역을 가져올 수 있게 되었다.

아쉬운 점은 서비스 객체까지 AnActionEvent를 가져오는 것이 이상적인 방법은 아니라는 점이다.

하지만 이는 책임을 명확하게 분리하고 캡슐화를 강화한다는 이점과의 trade-off로 볼 수 있다.



4-4. DiffService

@Service(Service.Level.PROJECT)
class DiffService(private val project: Project) {
    fun getDiff(changes: List<Change>): String {
    	//변경내용 요약 및 상세 내용 생성
        val summary = project.service<DiffSummaryGenerator>().generate(changes)
        val detailedChanges = project.service<DiffDetailGenerator>().generate(changes)

        return buildString {
            appendLine(DiffConstants.DEFAULT_TITLE)
            appendLine()
            appendLine(summary)
            appendLine()
            appendLine(detailedChanges)
            appendLine()
            appendLine(DiffConstants.ADDITIONAL_NOTES)
        }.trimEnd()
    }
}

DiffService는 이전의 DiffProvider를 대체하여 만들어졌다.

이전에 "변경내역 조회""Diff 추출"이라는 두 책임을 동시에 수행하던 것과 달리, 현재의 DiffService"Diff 추출"이라는 하나의 책임만을 수행하도록 개선되었다.


plugin.xml에 서비스 등록

<extensions defaultExtensionNs="com.intellij">
    <projectService
            serviceInterface="com.github.guswlsdl0121.messagemaker.services.diff.generator.DiffDetailGenerator"
            serviceImplementation="com.github.guswlsdl0121.messagemaker.services.diff.generator.SimpleDiffDetailGenerator"/>
    <projectService
            serviceInterface="com.github.guswlsdl0121.messagemaker.services.diff.generator.DiffSummaryGenerator"
            serviceImplementation="com.github.guswlsdl0121.messagemaker.services.diff.generator.SimpleDiffSummaryGenerator"/>
</extensions>

DiffService는 변경된 요구사항을 반영하여 '요약'과 '상세 내용'을 생성하는 두 개의 하위 구현체를 조율하는 역할을 수행한다.

이 구현체들은 서비스 객체로 등록되며, plugin.xml에 등록하였다.

이러한 등록 방식을 사용한다면, 내부의 변경을 최소화하여 서비스 구현체를 변경할 수 있다.


DiffConstants

//Diff에 대한 추가적인 설명을 도울 정적인 프롬프트
object DiffConstants {
    const val DEFAULT_TITLE = "Update project structure and modify content"
    val ADDITIONAL_NOTES = """
        # Additional Notes
        - The project structure has been updated with new and deleted files.
        - Content modifications have been made to improve functionality.
    """.trimIndent()
}

이외에도 Diff 생성 시 사용되는 정적인 텍스트를 DiffConstants에서 일관되게 관리하도록 만들었다. 특히 ADDITIONAL_NOTES생성된 Diff에 대한 추가적인 컨텍스트를 제공하여 ChatGPT가 변경사항을 더 잘 이해할 수 있도록 돕는다.


DiffSummaryGenerator

interface DiffSummaryGenerator {
    fun generate(changes: List<Change>): String
}

class SimpleDiffSummaryGenerator : DiffSummaryGenerator {
    override fun generate(changes: List<Change>): String {
        val changeTypeCounts = countChangeTypes(changes)

        return buildString {
            appendLine("# Summary of Changes")
            appendLine("- Added files: ${changeTypeCounts[Change.Type.NEW] ?: 0}")
            appendLine("- Deleted files: ${changeTypeCounts[Change.Type.DELETED] ?: 0}")
            appendLine("- Modified files: ${changeTypeCounts[Change.Type.MODIFICATION] ?: 0}")
            appendLine("- Moved files: ${changeTypeCounts[Change.Type.MOVED] ?: 0}")
        }.trimEnd()
    }

	//변경 유형별 파일의 개수를 집계
    private fun countChangeTypes(changes: List<Change>): Map<Change.Type, Int> {
        return changes.groupingBy { it.type }.eachCount()
    }
}

DiffSummaryGenerator는 전체 변경내역의 '요약'을 생성하는 인터페이스이다. 그리고 SimpleDiffSummaryGenerator는 이 인터페이스의 간단한 구현체이다.

현재 SimpleDiffSummaryGenerator는 위 사진과 같이 변경 유형별 파일 개수를 집계한 것을 요약 정보를 제공한다. 즉, 추가된 파일, 삭제된 파일, 수정된 파일, 이동된 파일의 각각의 개수를 보여준다.

추후 프롬프트의 정확도를 높이기 위해서, Change 객체 내부에 존재하는 더 많은 정보를 요약에 담을 수도 있다.

이러한 변경은 구현을 모두 마친 뒤에, 여러 번의 테스트를 통해 진행될 예정이다.


DiffDetailGenerator

interface DiffDetailGenerator {
    fun generate(changes: List<Change>): String
}

class SimpleDiffDetailGenerator : DiffDetailGenerator {
    override fun generate(changes: List<Change>): String {
        return changes.joinToString("\n\n") { change ->
            generateSingleChange(change)
        }.trimEnd()
    }

	//단일 변경사항에 대한 상세 Diff 생성
    private fun generateSingleChange(change: Change): String {
        val beforeContent = change.beforeRevision?.content ?: ""
        val beforeName = change.beforeRevision?.file?.name
        val afterContent = change.afterRevision?.content ?: ""
        val afterName = change.afterRevision?.file?.name

        val changeType = when (change.type) {
            Change.Type.NEW -> "New file"
            Change.Type.DELETED -> "Deleted file"
            Change.Type.MOVED -> "Moved file"
            else -> "Modified file"
        }

        val diffContent = generateDiff(beforeContent, afterContent)

        return buildString {
            appendLine("### $changeType: ${afterName ?: beforeName}")
            appendLine("```diff")
            appendLine(diffContent)
            appendLine("```")
        }.trimEnd()
    }
	
    //라인 단위로 변경 내용을 비교하여 Diff 생성
    private fun generateDiff(beforeContent: String, afterContent: String): String {
        val beforeLines = beforeContent.lines()
        val afterLines = afterContent.lines()

        return buildString {
            var i = 0
            var j = 0

            while (i < beforeLines.size || j < afterLines.size) {
                when {
                    i >= beforeLines.size -> append("+${afterLines[j++]}\n")
                    j >= afterLines.size -> append("-${beforeLines[i++]}\n")
                    beforeLines[i] == afterLines[j] -> {
                        i++
                        j++
                    }

                    else -> {
                        if (beforeLines[i].isNotEmpty()) append("-${beforeLines[i]}\n")
                        if (afterLines[j].isNotEmpty()) append("+${afterLines[j]}\n")
                        i++
                        j++
                    }
                }
            }
        }.trimEnd()
    }
}

DiffDetailGenerator각각의 변경 내역들을 조회하여 '상세 내용'을 생성한다.

그리고, SimpleDiffDetailGenerator는 이 인터페이스에 대한 간단한 구현체이다.

위 사진은 SimpleDiffDetailGenerator가 생성한 실제 Diff 출력의 예시이다.
이와 같이 상세한 Diff 정보는 ChatGPT가 변경의 맥락을 파악하는 데 도움을 줄 것이다.

현재 구현은 간단한 문자열 비교 알고리즘을 사용하고 있지만, 향후 성능 개선 및 정확도 향상을 위해 LCS 알고리즘이나 외부 라이브러리 사용을 고려할 수 있다.

이러한 변경을 고려하면, generateDiff() 메서드는 별도의 객체로 분리될 필요가 있어보이지만, 복잡도를 고려하여 추후에 진행하는 것으로 보류하였다.

DiffSummaryGenerator와 마찬가지로, 개발이 마무리된 후에 여러 테스트를 통해 개선해나갈 예정이다.


평가

이러한 구조적 변화를 통해 DiffService확장된 Diff의 개념을 체계적으로 구현하고 있다.

'요약'과 '상세 내용'을 각각 다른 모듈에서 관리하기 때문에, 변경 및 확장에 용이하고, 단위테스트를 작성도 쉬워졌다.

하지만 장점만 존재하는 것은 아니다. 비교적 간단한 구현임에도 총 4개의 파일이 생겨났으며, 이는 코드의 복잡도를 증가시키는 원인이 되었다.

이 상황에서 절대적인 정답은 없을 것이다. 그러나 코드의 복잡도를 고려하더라도 앞에서 언급한 유지보수성, 테스트 용이성과 같은 장점들이 더 크기 때문에 이러한 변경이 적절하다고 생각한다.




5. 새로운 구현


4챕터에서는 기존의 코드에 대한 리팩토링이었다면, 5챕터부터는 이 과정에서 구현한 예외 핸들러와 Notification에 대해서 설명하고자 한다.


5-1. ActionExceptionHandler

//GenerateCommitMessageAction의 actionPerformed()의 일부
try {
    project.service<CommitMessageEntryPoint>().run(event)
} catch (ex: Exception) {
    project.service<ActionExceptionHandler>().handle(ex)
}

@Service(Service.Level.PROJECT)
class ActionExceptionHandler(private val project: Project) {
    fun handle(ex: Exception) {
        when (ex) {
            is CustomException -> handlerCustomException(ex)
            else -> handleUnexpectedException(ex)
        }
    }

    private fun handlerCustomException(ex: CustomException) {
        val notificationService = project.service<NotificationService>()
        when (ex) {
            is NoChangesException -> {
                CustomLogger.warn(ex.message ?: "변경사항이 없습니다.")
                notificationService.show(Notification.NO_CHANGES_DETECTED)
            }
            is ProjectNullException -> {
                CustomLogger.warn(ex.message ?: "프로젝트가 null입니다.")
                notificationService.show(Notification.PROJECT_NOT_FOUND)
            }
        }
    }

    private fun handleUnexpectedException(ex: Exception) {
        CustomLogger.error("예상치 못한 오류 발생", ex, true)
        val notificationService = project.service<NotificationService>()
        notificationService.show(Notification.UNEXPECTED_ERROR, ex.message)
    }
}

이 코드는 GenerateCommitMessageAction의 실행 도중에 발생하는 모든 예외를 처리하는 예외 핸들러이다. 이 클래스는 프로젝트 레벨의 서비스로 등록되어 있다.

이 핸들러에서는 CustomExceptionUnexpectedException 두 종류의 예외를 다루고 있다.

여기서는 예외가 발생하면 간단하게 로그를 남기고 Notification을 통해 사용자에게 피드백을 주는 정도의 로직을 처리하고 있다.


CustomException

sealed class CustomException(message: String) : Exception(message)
class NoChangesException : CustomException("변경사항이 없거나 CommitWorkflowHandler가 설정되지 않았습니다.")
class ProjectNullException : CustomException("현재 프로젝트가 정상적으로 인식되지 않습니다.")

CustomExceptionsealed class를 사용하여 '특수한 예외'들을 체계적으로 관리한다. 여기서 말하는 '특수한 예외'란 플러그인의 특정 상황이나 조건에서 발생할 수 있는 특정 예외들을 의미한다. (ex. 체크한 변경 내역이 없을 때)

이러한 구조를 통해 예외와 그에 대한 메시지를 일관성있게 다룰 수 있으며, 새로운 커스텀 예외도 쉽게 추가할 수 있다.


CustomLogger

import com.intellij.openapi.diagnostic.thisLogger

object CustomLogger {
    private val logger = thisLogger()

    fun info(message: String, addCallerInfo: Boolean = false) {
        log(LogLevel.INFO, message, null, addCallerInfo)
    }

    fun warn(message: String, addCallerInfo: Boolean = false) {
        log(LogLevel.WARN, message, null, addCallerInfo)
    }

    fun error(message: String, throwable: Throwable? = null, addCallerInfo: Boolean = false) {
        log(LogLevel.ERROR, message, throwable, addCallerInfo)
    }

    fun debug(message: String, addCallerInfo: Boolean = false) {
        if (logger.isDebugEnabled) {
            log(LogLevel.DEBUG, message, null, addCallerInfo)
        }
    }

    private fun log(level: LogLevel, message: String, throwable: Throwable?, addCallerInfo: Boolean) {
        val finalMessage = if (addCallerInfo) addCallerInfo(message) else message
        when (level) {
            LogLevel.INFO -> logger.info(finalMessage)
            LogLevel.WARN -> logger.warn(finalMessage)
            LogLevel.ERROR -> logger.error(finalMessage, throwable)
            LogLevel.DEBUG -> logger.debug(finalMessage)
        }
    }

    private fun addCallerInfo(message: String): String {
        val caller = Thread.currentThread().stackTrace[4]
        return "[${caller.className.substringAfterLast('.')}.${caller.methodName}] $message"
    }

    private enum class LogLevel { INFO, WARN, ERROR, DEBUG }
}

이 코드는 로깅 기능을 중앙화하는 래퍼 클래스이다.

thisLogger() 함수를 통해 로거 생성 과정을 단순화했고, 다양한 로그 레벨에 대한 메서드를 제공한다. 또한, addCallerInfo() 메서드를 통해 호출자 정보를 선택적으로 추가할 수 있다.

이 코드를 통해 각 클래스에서 별도의 로거 인스턴스를 생성할 필요가 없어지고, 로그 형식과 사용 방법이 일관되어 코드의 가독성이 올라간다.


평가

이러한 중앙 예외 핸들러의 구현을 통해 각 클래스의 비즈니스 로직과 예외 처리를 분리하여 코드의 가독성을 높였다.

또한, 이 과정에서 CustomException을 통해 사용자 정의 예외를 쉽게 추가할 수 있도록 만들었으며, CustomLogger로 로그 처리를 간소화했다.

이러한 구조는 향후 개발 과정에서 비즈니스 로직에 더 집중할 수 있게 해줄 것이다.



NotificationService

인텔리제이 플랫폼에서의 Notification이란, 위 사진과 같이 경고창을 통해 사용자에게 시각적인 피드백을 줄 수 있는 기능을 의미한다.

자세한 정보는 공식 문서를 통해 확인할 수 있다. 위와 같은 형태를 BALLOON 타입이라고 말하며, 이 외에도 많은 타입들이 존재한다.

@Service(Service.Level.PROJECT)
class NotificationService(private val project: Project) {
    fun show(notification: Notification, vararg args: Any?) {
        val formattedContent = notification.content.format(*args)
        show(notification.title, formattedContent, notification.type)
    }

    private fun show(title: String, content: String, type: NotificationType) {
        NotificationGroupManager.getInstance()
            .getNotificationGroup(NotificationConstants.NOTIFICATION_GROUP_ID)
            .createNotification(createTitle(title), content, type)
            .notify(project)
    }

    private fun createTitle(title: String): String {
        return "${NotificationConstants.PLUGIN_NAME}: $title"
    }
}

NotificationService 클래스는 사용자에게 알림을 표시하는 역할을 수행한다.

show() 메서드에서는 Notification 타입의 값을 파라미터로 받아 알림의 내용을 구성한다.

내부적으로는 인텔리제이 플랫폼의 NotificationGroupManager를 사용하여 실제 알림을 생성하고 표시한다. 이에 관련된 내용은 여기에서 확인할 수 있다.


Notification

enum class Notification(
    val title: String,
    val content: String,
    val type: NotificationType
) {
    NO_CHANGES_DETECTED(
        "No Changes Detected",
        "There are no changes to generate a commit message for.",
        NotificationType.WARNING
    ),
    PROJECT_NOT_FOUND(
        "Project Not Found",
        "Current project is not recognized or is null. Failed to generate commit message.",
        NotificationType.ERROR
    ),
    UNEXPECTED_ERROR(
        "Unexpected Error",
        "An unexpected error occurred. Please check the logs for more details.",
        NotificationType.ERROR
    );
}

Notification은 Enum타입으로 여러 유형의 알림과 그 내용을 정의하고 있다. 각 Enum은 제목, 내용, 알림 타입으로 알림을 생성할 때 필요한 정보들로 구성되어있다.

이렇게 알림을 생성할 때 필요한 정보를 한 곳에서 관리함으로써 재사용성 및 확장성을 챙길 수 있다.


NotificationConstants

object NotificationConstants {
    const val NOTIFICATION_GROUP_ID = "Commit Message Generator"
    const val PLUGIN_NAME = "MessageMaker"
}

알림을 생성할 때 필요한 플러그인의 이름과 그룹의 id는 상수로 관리하고 있다.


plugin.xml에 등록

<extensions defaultExtensionNs="com.intellij">
	<!--생략-->
    <notificationGroup id="Commit Message Generator" displayType="BALLOON" key="notification.group.commit.message.generator"/>
</extensions>

위와 같이 extensions 태그에 알림의 그룹 id와 타입을 지정해주어야 한다.


평가

이러한 구조로 NotificationService를 구현함으로써 플러그인 전반에서 사용자에게 일관성 있고 쉽게 알림을 제공할 수 있게 되었다.

특히, 알림을 생성할 때 필요한 정보를 Notification이라는 enum에서 일괄적으로 관리하게 하여 서비스에서 사용 중인 알림을 한 눈에 파악할 수 있을 뿐만 아니라, 추가에도 용이하게 만들었다.

현재는 알림이 예외처리에서만 쓰이고 있지만, 구현 중에 다른 곳에서 알림이 쓰인다고 하더라도 쉽고 빠르게 구현할 수 있을 것이다.




6. 전체 plugin.xml


<idea-plugin>
    <id>com.github.guswlsdl0121.messagemaker</id>
    <name>MessageMaker</name>
    <vendor email="guswlsdl0121@gmail.com" url="https://github.com/guswlsdl0121">안현진</vendor>
    <version>1.0.0</version>

    <description><![CDATA[
    <p>This plugin helps to generate commit messages based on selected changes.</p>
    ]]></description>

    <change-notes><![CDATA[
    <p>Initial release</p>
    ]]></change-notes>

    <idea-version since-build="222.3345.118"/>

    <depends>com.intellij.modules.platform</depends>
    <depends>com.intellij.modules.vcs</depends>

    <resource-bundle>messages.MessageBundle</resource-bundle>

    <actions>
        <action id="com.github.guswlsdl0121.messagemaker.actions.GenerateCommitMessageAction"
                class="com.github.guswlsdl0121.messagemaker.actions.GenerateCommitMessageAction"
                text="Generate Commit Message"
                description="Generate a commit message based on selected changes"
                icon="/icons/commitIcon.svg">
            <!--            해당 그룹의 첫번째에 이 Action을 삽입한다.-->
            <add-to-group group-id="Vcs.MessageActionGroup" anchor="first"/>
        </action>
    </actions>

    <extensions defaultExtensionNs="com.intellij">
        <projectService
                serviceInterface="com.github.guswlsdl0121.messagemaker.services.diff.generator.DiffDetailGenerator"
                serviceImplementation="com.github.guswlsdl0121.messagemaker.services.diff.generator.SimpleDiffDetailGenerator"/>
        <projectService
                serviceInterface="com.github.guswlsdl0121.messagemaker.services.diff.generator.DiffSummaryGenerator"
                serviceImplementation="com.github.guswlsdl0121.messagemaker.services.diff.generator.SimpleDiffSummaryGenerator"/>
        <notificationGroup id="Commit Message Generator" displayType="BALLOON" key="notification.group.commit.message.generator"/>
    </extensions>
</idea-plugin>




7. 마치며


두 번에 거친 리팩토링 및 추가적인 구현을 통해 코드의 기틀을 잡을 수 있었다.

액션에 대한 추가적인 기능은 서비스 객체로 등록한 후에 GenerateCommitMessageAction에서 순서에 맞게 호출하면 될 것이며, 새로운 예외가 생기더라도 CustomException에 추가하고 새로운 핸들러를 구현하면 될 것이다.

이러한 변경은 SDK 내부에 또다른 나만의 프레임워크를 구축한 작업과 비슷하다고 느껴졌다.

코드를 리팩토링하면서 겪은 의사 결정 과정을 그대로 글로 작성하다보니, 글도 길어지고 너무 많은 시간을 할애했던 것 같다.

다음 글에서는 Test에 대해서 간략하게 다루고, 신규 개발을 진행할 예정이다.

profile
안녕하세요

0개의 댓글