LLM/Cursor

GPT의 잠재력 200% 활용하는 프롬프트 튜닝 방법

Joonfluence 2025. 4. 13. 00:45

이 글은 Cursor 시리즈의 5번째 글입니다.

이런 분들에게 이 글을 추천합니다

  • 기계적으로 반복적인 프롬프트만 반복하는 개발자
  • GPT/Copliot에게 물어봐도 원하는 결과가 나오지 않아 답답한 개발자
  • 내 옆 동료들은 잘만 활용하고 있는 것 같은데 뒤처지는 듯한 감정을 느끼고 있는 개발자

기대했던 것보다 구린 코드

"왜 이렇게 겉도는 얘기만 하지?" "내가 원하는 대답이 잘 안 나와..."

여러분은 GPT/Copliot에게 이런 감정을 느껴본 적 있으신가요?
저는 코딩 머신이라고 할 수 있는 LLM을 사용하면서 답답한 적이 많았습니다.
제가 원하는 수준의 코드가 나을 때가 많았기 때문이죠.

결국 쉬운 작업에만 AI를 활용하여 처리하고
정작 어려운 부분들은 참고만 하고, 모르는 부분을 직접 공부하거나
블로그에서 검색해서 해결했던 적이 많았습니다.

이 로직 테스트 코드 짜줘

여러분은 GPT에게 코드를 쓱 붙여넣고 이렇게 프롬프트를 작성해본 경험 없으신가요?
부끄럽지만 전 많습니다.. 아래처럼요.

코드 좀 대신 짜줘..
제발 좀 짜줘..

왜 원하는 만큼의 코드가 아니었을까요? 혹시 GPT한테 잘못 물어본 것은 아닐까요?
이번 글에서는 GPT의 잠재력 200% 활용하는 프롬프트 튜닝 방법에 대해 알아보겠습니다.

GPT 프롬프트 이렇게 구조화하세요

프롬프트는 GPT와의 대화를 "효율적인 협업"으로 바꾸는 도구입니다.
하지만 아래 방법을 몰랐던 과거의 저는 “이거 개선해줘”, “이 코드 설명해줘”처럼
모호하게만 요청해서 GPT의 잠재력을 제대로 못 끌어내고 있었습니다.

그래서 제가 오늘 소개할 프롬프트 작성법은,
‘골빈해커’님의 최고의 프롬프트 엔지니어링 강의에서 소개한 7단계 구조를 바탕으로,
누구나 3분 안에 이해할 수 있도록 재구성 해봤습니다.

프롬프트 구성 7단계 프레임워크

단계 설명 실무용 예시
1. 역할(Role) GPT가 어떤 사람처럼 행동할지 설정 “당신은 시니어 백엔드 개발자입니다”
2. 대상(Audience) 누구에게 설명하는지 지정 “Spring을 처음 배우는 주니어 개발자에게 설명해 주세요”
3. 정보(Information) 참고할 컨텍스트나 배경 정보 제공 “아래 코드와 요구사항을 바탕으로…”
4. 작업/목표(Task/Goal) 구체적으로 원하는 작업과 결과 지정 “코드를 성능 개선 중심으로 리팩토링해 주세요”
5. 정책/스타일(Policy/Style) 응답의 톤, 제약 조건 등 지정 “간결하고, 부정적인 표현은 피해주세요”
6. 형식/구조(Format) 출력될 응답의 포맷 지정 “표 형식으로 정리해 주세요”
7. 예시(Example) 출력 형식을 미리 보여줘 힌트 제공 “다음 예시처럼 답변해 주세요: {...}”

이 7단계 구조는 이후 등장할 Cursor 실무 프롬프트 패턴의 기본 베이스가 됩니다.
각 패턴에서도 어떤 구조 요소를 활용했는지 예시와 함께 설명드릴 테니,
지금은 “아, 이렇게 나눌 수 있구나” 정도만 기억해 주세요.

1️⃣ 실전 적용 예시

GPT는 불명확하고 모호한 요청을 받으면, 피상적이거나 쓸모 없는 답변을 내놓는 경우가 많습니다.
물론 운 좋게 바로 실무에 적용 할 만 한 코드가 나올 수도 있지만, 그렇지 않은 경우도 많습니다.
제 경험 상, 대체로 이렇게 나온 코드들은 재수정 해줘야 하거나 한 번에 코드리뷰를 통과하지 못하는 경우가 많았습니다.

좋은 프롬프트를 작성하려면, 위 7단계 원칙에 맞게 작성해줘야 합니다.
물론, 이렇게 프롬프트를 작성하는 것이 귀찮고 번거러울 수 있습니다.
그래서 이러한 프롬프트들을 해당 프로젝트 전체에 적용할 수 있다면 적용하는 것이 좋고,
그렇지 못하다면 별도 템플릿을 두고 그 때 그 떄 마다 활용하는 것을 권장 드립니다.

같은 코드이고 7단계 원칙을 지킨 프롬프트이더라도, 내용에 따라 조금씩 결과가 달라졌는데요.
가장 좋은 프롬프트는 무엇일지는, 저 역시 좀 더 실험해봐야 할 것 같습니다.

대표적으로 제가 사용했던 좋지 못한 프롬프트들은 하나씩 수정해보겠습니다.

❌ 나쁜 프롬프트 : "이 코드 로직 개선 해줘"

"이 코드 로직 개선 해줘"

도대체 어떤 로직을 어떻게 개선해달라는 의미일까요?
따라서 GPT는 코드를 한 줄 한 줄 읽고, 추측할 수 밖에 없습니다.

🅾️ 좋은 프롬프트

위 프롬프트를 아래와 같이 개선해볼 수 있습니다.

당신은 Kotlin 기반의 시니어 백엔드 개발자입니다. 
해당 로직은 OO을 담당하는 서비스 로직이며 성능 이슈는 없으나, 가독성 및 유지보수성이 떨어집니다. 
가독성 개선을 위해 중복 코드 제거, 책임 분리, 의미 있는 네이밍 등을 적용해 코드를 개선해 주세요. 
비즈니스 로직은 변경하지 마시고, 기존 테스트가 통과할 수 있어야 합니다. 
변경 전/후 코드를 비교 형식으로 보여주시고, 주요 변경 이유를 주석으로 간단히 붙여 주세요. 
예: `// 변경 전: OOO` → `// 변경 후: OOO` 

이렇게 구성하면 GPT는 ‘어떻게 말해야 할지’ 명확해지고,
실제로 바로 활용 가능한 응답을 제공할 가능성이 매우 높아집니다.

❌ 나쁜 프롬프트 : "이 코드 리팩토링 해줘"

"이 코드 리팩토링 해줘"

위 프롬프트 역시, 아래와 같이 개선해볼 수 있습니다.

🅾️ 좋은 프롬프트

당신은 Java를 활용한 OOP에 능숙한 시니어 백엔드 아키텍트입니다. 
아래는 Spring 기반의 도메인 서비스 코드이며, SOLID 원칙에 부합하지 않을 수 있습니다. 
이 코드를 유지 보수하기 좋고, 책임 분리 중심으로 리팩토링해 주세요. 
그 외 비즈니스 로직은 그대로 유지해 주세요. 기존 테스트가 통과할 수 있어야 합니다. 
변경 전/후 코드를 비교 형식으로 보여주시고, 주요 변경 이유를 주석으로 간단히 붙여 주세요. 
예: `// 분리 전: OOO` → `// 분리 후: OOO` 

위 프롬프트는 예시입니다. 일반적인 상황에는 그대로 적용해볼 수 있겠지만,
실제로 적용할 땐, 코드의 문제를 파악하여 프롬프트를 작성하는 것이 Best 입니다.

❌ 나쁜 프롬프트 : "이 코드에 대한 테스트코드 짜줘"

"이 코드에 대한 테스트코드 짜줘"

🅾️ 좋은 프롬프트

당신은 테스트 자동화에 능숙한 Kotlin 백엔드 개발자입니다.  
아래 코드는 외부 API를 호출하는 서비스 함수입니다. 
정상 처리, 파라미터 누락, 외부 API 실패 케이스를 포함한 JUnit5 단위 테스트 코드를 작성해 주세요.  
외부 API 호출이 있는 경우에는 Mockito를 사용해 외부 API는 mocking 처리해 주세요.  
테스트 메서드 이름은 given_when_then 스타일을 따르고, assertion은 assertThat을 사용해 주세요.  
전체 테스트 클래스 코드를 출력해 주세요.  
예: `givenValidProductId_whenStartReview_thenStatusIsReviewing()`

2️⃣ 실전 적용 예시

이론적으론 좋은 프롬프트를 작성하기 위해선, 위 7단계 원칙에 맞게 작성해줘야 합니다.
실무에서, 이렇게 프롬프트를 기억하고 작성하는 것은 번거롭게 느껴질 수 있습니다.
그 경우에는, 저는 제가 자주 사용하는 약식 템플릿을 권해드리고 싶습니다.

컨텍스트(Contenxt) : 이 코드가 어떤 기능을 담당하는지 요약
목적(Goal) : 무엇을 위해 리팩토링/개선하고 싶은지 명확히
현재 구조의 문제(Problem) : 현재 코드에서 불편하거나 불만족스러운 점

예시는 아래와 같습니다.

아래 코드는 상품 검토 요청을 처리하는 서비스 로직이야.

이 코드를 SRP 원칙에 따라 책임 분리하고, 테스트 가능한 구조로 리팩토링해줘.
현재 문제는 모든 로직이 한 메서드에 몰려 있어서 가독성이 떨어지는 것 같아.
우선 고려사항은 유지보수성과 테스트 용이성이야.

Role, Policy/Style은 프로젝트 전체에 선언 할 수 있으니, 충분히 생략 할 수 있을 것입니다.

실전 경험: 설계까지 바뀐 프롬프트의 힘

이번엔 실제 코드 기반으로 살펴보겠습니다.
최근 제가 진행했던 프로젝트는 다국어 기반 전자상거래 플랫폼의 상품 관리 백엔드 서버 개발이었습니다.
기본 흐름은 이렇습니다.

  • 상품은 작성 → 검토요청 → 검토중 → 승인/거절 → 판매완료 상태를 따라 흐르고,
  • 상태 전이에 따라 각기 다른 사용자 권한과 외부 시스템 연동이 필요합니다.
  • 예를 들어 검토중으로 상태가 바뀔 때는 번역 API와 문자 발송 API가 호출되어야 하죠.

저는 이 때, 상품 상태가 '검토중'으로 바뀔 때 문자를 보내는 로직을 고민하게 됐습니다.
처음엔 GPT에게 이렇게 물었습니다.

검토 시작 시 문자 메시지를 보내야 하는데, 이벤트 기반으로 처리하는 좋은 방법 있을까요?

GPT는 서비스 레이어에서 문자 발송 이벤트를 직접 발행하라고 제안했습니다.
틀린 말은 아니지만, 이 구조의 경우에는 "해외 파트너에게는 메일로 발송" 같은 정책이 생기면 서비스 레이어를 수정해야 하는 구조가 됐습니다.
그래서 GPT에게 다시 이렇게 물었습니다.

1) 컨텍스트(Contenxt) : 검토 상태 전환 시, 작가에게 문자 메시지를 발송하는 로직 
2) 목적(Goal)
    - 향후 요구사항이 추가되어, 문자 외에도 메일 발송 정책이 추가될 수 있. 
    - OCP 원칙을 지키면서 이벤트 설계를 어떻게 개선할 수 있을까? 
        - OCP 외에도 SOLID 원칙에 부합하는지 검토해줘. 
3) 현재 구조의 문제(Problem) 
    - 현재는 서비스 레이어에서 메세지 채널 단위로 이벤트를 직접 발행하기 때문에, 변경에 취약합니다. 
4) 우선순위 : 유지보수성과 테스트 용이성이야.
5) 포맷 : Kotlin + Spring 스타일로, 함수 단위 분리 중심으로 제안해줘.

위 컨텍스트 기반으로, 해당 로직을 개선해줘

이번엔 완전히 다른 제안을 받았습니다.

  • 문자 메시지 이벤트가 아닌 "검토 시작"이라는 도메인 이벤트를 먼저 발행
  • 리스너에서 채널 분기 처리 (SMS, 메일 등)

결과적으로 GPT가 제안해준 로직은 제가 생각하는 이상적인 구조와 정확히 일치했습니다.
실제 코드를 살펴보겠습니다.


실제 코드 비교

개선 전

    @Transactional
    fun startReview(productId: Long) {
        val product = productRepository.findByIdOrThrow(productId, BadRequestException(ErrorCodes.PRODUCT_NOT_FOUND))
        val translation =
            productTranslationRepository.findByProductIdAndLanguage(product.id, Language.KO)

        require(product.status == ProductStatus.REQUESTED) {
            "상품은 검토 요청 상태여야 합니다."
        }

        val previousStatus = product.status
        val newStatus = ProductStatus.REVIEWING

        product.status = newStatus
        productRepository.save(product)

        val history = ProductReviewHistoryEntity(
            product = product,
            previousStatus = previousStatus,
            newStatus = newStatus,
            user = UserContext.get(),
        )

        productReviewHistoryRepository.save(history)
        eventPublisher.publishEvent(
            ProductTranslationEvent(
                productId = product.id,
                koTitle = translation.title,
                koDescription = translation.description
            )
        )

        eventPublisher.publishEvent(
            SmsNotificationEvent(
                phone = product.partner.phone,
                message = "[ACON] 상품 검토가 시작되었습니다."
            )
        )
    }
  • 모든 로직을 한 메서드에서 수행
  • 상태 전이, 이벤트 발행, 검증, 저장이 뒤엉켜 있음
  • 발송 채널이 서비스에 하드코딩됨

개선 후

    @Transactional
    fun startReview(productId: Long) {
        val product = productRepository.findByIdOrThrow(productId, BadRequestException(ErrorCodes.PRODUCT_NOT_FOUND))
        val translation = productTranslationRepository.findByProductIdAndLanguage(product.id, Language.KO)

        validateProductStatus(product, ProductStatus.REQUESTED)

        val previousStatus = product.status
        val newStatus = ProductStatus.REVIEWING

        updateProductStatus(product, newStatus)
        saveReviewHistory(product, previousStatus, newStatus)

        eventPublisher.publishEvent(
            ProductReviewStartedEvent(
                productId = product.id,
                partnerId = product.partner.id,
                koTitle = translation.title,
                koDescription = translation.description
            )
        )
    }

    private fun validateProductStatus(product: Product, expectedStatus: ProductStatus) {
        require(product.status == expectedStatus) {
            "상품은 ${expectedStatus.description} 상태여야 합니다."
        }
    }

    private fun updateProductStatus(product: Product, newStatus: ProductStatus) {
        product.status = newStatus
        productRepository.save(product)
    }

    private fun saveReviewHistory(
        product: Product,
        previousStatus: ProductStatus,
        newStatus: ProductStatus
    ) {
        val history = ProductReviewHistoryEntity(
            product = product,
            previousStatus = previousStatus,
            newStatus = newStatus,
            user = UserContext.get(),
        )
        productReviewHistoryRepository.save(history)
    }
  • 서비스는 "검토 시작됨"이라는 의미적 이벤트만 발행
  • 발송 채널은 이후 리스너에서 결정 가능 -> 그로인해 설계가 유연해지고 추후 채널이 늘어나더라도 서비스 레이어는 건드릴 일이 없음
  • validateProductStatus, updateProductStatus, saveReviewHistory로 분리
  • 하나하나 테스트/리팩토링/확장 가능

이 경험을 통해 알게 된 것

GPT는 어떻게 쓰느냐에 따라 단순한 질문 도우미에서
설계 파트너로 진화할 수 있다는 사실.

처음엔 "이벤트로 처리해줘"라고만 했던 GPT는,
프롬프트를 구조화하자 OCP 원칙, 도메인 이벤트의 의미, 확장 가능한 설계까지 제안했습니다.

마무리 정리

GPT는 어떻게 쓰느냐에 따라 무능하거나 유능해집니다.
프롬프트는 “개발자와 GPT 사이의 설계 문서”입니다.
Cursor 같은 AI 개발 환경에서는, 구조화된 프롬프트가 생산성 핵심이 됩니다.

반응형