프롬프트도 계약이다: 소프트웨어 엔지니어가 이미 아는 규율을 프롬프트에 적용하는 법
이 글은 이미 코드를 계약으로 설계하는 법을 아는 소프트웨어 엔지니어를 위한 글입니다. 읽고 나면 프롬프트를 즉흥적인 글쓰기가 아니라 입력과 출력이 명시된 함수처럼 다루는 구체적인 방법을 얻을 수 있습니다.
핵심 주장은 단순합니다. 프롬프트 엔지니어링에서 반복적으로 무너지는 프로젝트와 안정적으로 굴러가는 프로젝트를 가르는 기준은 모델의 성능이 아니라 계약의 유무입니다. 입력 계약, 출력 계약, 변경 이력 계약이라는 세 겹의 규율을 프롬프트에 적용하지 않으면 아무리 좋은 모델을 붙여도 프로덕션에서는 흔들립니다.
많은 팀이 프롬프트를 마케팅 카피처럼 다룹니다. 느낌이 좋을 때까지 문장을 고치고, 결과가 그럴듯하면 배포하고, 나중에 이상한 출력이 나오면 다시 문장을 만집니다. 이 방식은 데모 단계에서는 통합니다. 하지만 사용자가 늘고 입력이 다양해지는 순간 재현되지 않는 실패가 쌓입니다. 아래에서는 이 실패를 막는 구체적인 규율을 하나씩 살펴봅니다.
글의 핵심 개념을 형상화했습니다.
프롬프트를 함수로 다시 보기
소프트웨어 엔지니어링에서 함수를 설계할 때는 입력을 무엇으로 받을지, 출력을 무엇으로 반환할지, 부수 효과는 무엇인지를 먼저 정의합니다. 프롬프트도 본질적으로 같은 구조를 가집니다. 프롬프트는 언어 모델이라는 시스템에 넘기는 함수 호출이고, 입력은 문맥이며 출력은 모델이 생성한 텍스트입니다. 이렇게 보는 순간 프롬프트 작성은 감으로 하는 글쓰기가 아니라 엔지니어링 작업으로 바뀝니다.
함수 시그니처를 떠올려 보면 이해가 빠릅니다. 좋은 함수는 이름이 명확하고 입력과 출력의 계약이 분명합니다. 인물 정보를 추출하는 함수를 만든다고 하면, 함수의 이름만 봐도 무엇을 하는지 짐작할 수 있어야 하고 타입 힌트만 봐도 무엇이 들어오고 무엇이 나가는지 알 수 있어야 합니다. 프롬프트도 마찬가지입니다. 지시문 안에 입력의 형태, 기대하는 출력의 형태, 처리 규칙을 명시하지 않으면 모델은 매번 다른 방식으로 빈칸을 채웁니다.
이 관점이 왜 중요한지는 실패 사례를 보면 분명해집니다. 프롬프트에 “텍스트에서 중요한 정보를 뽑아줘” 같은 문장만 넣으면 무엇이 중요한 정보인지, 결과를 어떤 형식으로 돌려줄지, 정보가 없을 때는 어떻게 할지 전부 모델의 재량에 맡겨집니다. 재량은 곧 변동성입니다. 같은 요청을 열 번 보내면 열 가지 형태의 응답이 돌아올 수 있습니다. 반면 입력의 범위를 텍스트로 제한하고 출력을 특정 스키마의 목록으로 못 박으면 모델이 채워야 할 빈칸이 확 줄어듭니다.
여기서 부수 효과라는 개념도 코드와 똑같이 적용됩니다. 함수가 외부 상태를 건드리면 그 부수 효과를 문서화해야 하듯, 프롬프트가 외부 도구를 호출하거나 이전 대화 맥락을 참조하거나 특정 톤을 강제한다면 그 조건도 계약의 일부로 명시해야 합니다. 계약 문서를 프롬프트 바깥의 주석이나 사람만 아는 암묵적 지식으로 남겨두면, 프롬프트를 고치는 다음 사람이 그 규칙을 깨뜨리기 쉽습니다.
결국 이 재구성이 주는 실질적 이득은 협업 가능성입니다. 프롬프트를 함수 계약으로 취급하면 코드 리뷰를 하듯 프롬프트를 리뷰할 수 있고, 다른 엔지니어가 그 프롬프트를 수정할 때 무엇을 지켜야 하는지 계약만 보고 알 수 있습니다. 이것이 다음 두 절에서 다룰 입력 계약과 출력 계약의 출발점입니다.
입력 계약: Few-Shot과 Chain-of-Thought가 실제로 하는 일
모델에게 예제를 어떻게 주입하느냐에 따라 결과의 품질이 크게 달라집니다. Few-Shot 프롬프팅은 이 원리를 체계적으로 활용하는 기법이고, Chain-of-Thought는 추론 과정을 단계별로 유도하는 기법입니다. 두 기법을 계약의 언어로 다시 표현하면 이해가 훨씬 쉬워집니다. Few-Shot은 출력의 형태를 계약하는 방법이고, Chain-of-Thought는 추론의 절차를 계약하는 방법입니다.
Zero-Shot은 지시문만으로 결과를 요청하는 방식입니다. 모델은 훈련 데이터에서 학습한 패턴을 스스로 판단해 답을 생성합니다. 여기에 예제 몇 개를 덧붙이는 순간 상황이 달라집니다. 예제는 단순한 참고 자료가 아니라 입력과 출력 사이의 관계를 보여주는 실제 계약서입니다. 감정 분류 작업에서 긍정과 부정 예제를 각각 하나씩 보여주면, 모델은 그 패턴을 읽고 새로운 입력에도 같은 규칙을 적용합니다. 예제가 정확할수록 출력도 안정적으로 정확해집니다.
예제를 고를 때는 몇 가지 원칙이 필요합니다. 예제는 작업의 경계 사례를 대표해야 합니다. 쉬운 케이스만 넣으면 모델은 어려운 케이스에서 실패합니다. 애매한 표현, 중의적인 문장, 정답이 갈리는 경계선 사례를 섞어야 실전에서 버티는 계약이 됩니다. 예제의 개수도 중요한 변수입니다. 너무 적으면 패턴이 충분히 전달되지 않고, 너무 많으면 모델이 예제를 암기하듯 취급해 새로운 입력에 오히려 틀리게 적용하는 역효과가 생깁니다. 일반적으로 서너 개에서 여덟 개 사이가 안정적인 출발점이 됩니다.
Chain-of-Thought는 다른 축의 문제를 해결합니다. Few-Shot이 출력의 모양을 가르쳐 준다면, Chain-of-Thought는 그 모양에 도달하는 추론의 경로를 가르쳐 줍니다. 단계별 사고를 요청하면 모델은 최종 답변에 이르기까지의 중간 논리를 함께 생성합니다. 산술 추론이나 다단계 판단이 필요한 작업에서 이 차이는 큽니다. 최종 답만 요구하면 모델은 중간 단계를 생략하고 패턴 매칭에 가까운 방식으로 답을 내놓을 수 있는데, 이 경우 오답률이 눈에 띄게 높아집니다. 반대로 과정을 먼저 서술하게 하면 모델이 스스로 오류를 잡아내는 경우도 생깁니다.
두 기법을 계약이라는 관점으로 묶어 보면 실무에서의 우선순위가 명확해집니다. 출력 형식이 흔들리는 문제라면 Few-Shot 예제를 강화하고, 추론 정확도가 흔들리는 문제라면 Chain-of-Thought를 추가하는 식으로 처방을 구분할 수 있습니다. 이 구분 없이 두 기법을 뭉뚱그려 아무렇게나 섞으면 프롬프트만 길어지고 어느 쪽이 효과가 있었는지 알 수 없게 됩니다.
출력 계약: 구조화 출력이 주는 것과 주지 않는 것
프롬프트 결과가 자연어 텍스트로 돌아오면 사람이 읽기엔 편하지만 프로그램이 파싱하려면 손이 더 갑니다. 출력 형식을 강제할 수 있으면 파싱 오류를 미리 없애고 후속 시스템과의 통합도 안정적으로 가져갈 수 있습니다. 대부분의 주요 모델 API는 출력을 JSON 형태로 강제하는 기능을 제공합니다. 이 기능을 쓰면 모델이 자연어를 생성하다가도 지정한 구조를 따르게 됩니다.
여기서 스키마가 하는 역할을 정확히 이해해야 합니다. 스키마는 출력의 형식을 계약하는 도구이지, 출력의 내용을 계약하는 도구가 아닙니다. 인물 정보 추출 작업에서 이름과 직함과 소속을 담는 배열 스키마를 정의하면, 모델은 그 구조를 벗어나지 않습니다. 하지만 실제 텍스트에 없는 직함을 지어내거나 소속을 엉뚱한 조직으로 채워 넣는 문제는 스키마가 막아 주지 않습니다. 형식은 지키면서 내용은 틀린 응답이 그대로 시스템 다음 단계로 흘러갈 수 있다는 뜻입니다.
이 차이를 놓치면 팀이 흔히 저지르는 실수가 하나 생깁니다. 구조화 출력을 도입한 뒤 파싱 에러가 사라졌다고 안심하고 검증 단계를 건너뛰는 것입니다. 파싱이 성공했다는 사실은 JSON 문법이 맞았다는 뜻일 뿐 값이 옳다는 뜻이 아닙니다. 프로덕션에서 실제로 문제가 되는 것은 대부분 문법 오류가 아니라 의미 오류입니다. 존재하지 않는 필드를 지어내거나, 숫자를 반올림하지 않고 임의로 바꾸거나, 목록이 비어야 하는데 억지로 항목을 채우는 식의 오류는 스키마 검증기를 통과하고도 발생합니다.
그래서 구조화 출력을 쓰는 팀은 두 단계의 검증을 함께 설계해야 합니다. 첫 번째는 스키마 검증으로, 필드 이름과 타입이 맞는지 기계적으로 확인합니다. 두 번째는 내용 검증으로, 값이 원본 입력에 실제로 근거하는지 확인합니다. 인물 정보 추출이라면 추출된 이름이 원문에 실제로 등장하는지 문자열 매칭으로 재확인하는 식의 간단한 규칙만으로도 지어낸 값의 상당수를 걸러낼 수 있습니다. 이 두 단계를 하나로 착각하면 스키마만 통과한 오답이 그대로 사용자에게 노출됩니다.
결국 출력 계약을 온전히 지키려면 모델에게 형식을 맡기고 검증은 코드가 맡는 분업이 필요합니다. 모델은 확률적으로 그럴듯한 문장을 만드는 데 강하고, 결정론적인 규칙을 지키는 데는 약합니다. 반대로 코드는 규칙을 어기지 않는 데 강합니다. 이 두 강점을 각자의 자리에 배치하는 것이 구조화 출력을 프로덕션 수준으로 끌어올리는 핵심입니다.
계약이 흔들리는 실제 지점들
지금까지 다룬 입력 계약과 출력 계약을 모두 지켜도 계약이 조용히 무너지는 지점이 남아 있습니다. 가장 흔한 지점은 모델 버전이 바뀌는 순간입니다. 같은 프롬프트를 다른 버전의 모델에 그대로 넣으면 이전 버전에서는 지켜지던 암묵적 규칙이 새 버전에서는 지켜지지 않을 수 있습니다. 프롬프트는 특정 모델의 습성에 어느 정도 맞춰 튜닝된 결과물이기 때문에, 모델을 바꾸는 일은 코드에서 런타임을 바꾸는 일과 비슷한 무게로 다뤄야 합니다.
두 번째 지점은 입력 분포가 서서히 이동하는 경우입니다. 처음 프롬프트를 설계할 때 썼던 예제 텍스트와 실제 서비스에 들어오는 텍스트의 성격이 시간이 지나면서 달라질 수 있습니다. 사용자층이 넓어지거나 새로운 언어가 섞여 들어오거나 문서의 길이가 길어지는 식의 변화가 누적되면, 처음에는 잘 맞았던 Few-Shot 예제가 더 이상 대표성을 갖지 못합니다. 이 변화는 갑자기 터지지 않고 서서히 정확도를 갉아먹기 때문에 알아차리기가 더 어렵습니다.
세 번째 지점은 프롬프트를 여러 사람이 각자 판단으로 고치는 경우입니다. 한 엔지니어가 지시문의 한 문장을 다듬고, 다른 엔지니어가 예제를 하나 추가하고, 또 다른 엔지니어가 출력 형식을 살짝 바꾸는 일이 리뷰 없이 누적되면 원래의 계약이 무엇이었는지조차 알 수 없는 상태가 됩니다. 코드에서는 이런 일이 생기면 컴파일러나 테스트가 즉시 알려주지만, 프롬프트는 자연어이기 때문에 문법적으로는 항상 유효해 보입니다. 겉보기에 문제가 없어 보인다는 점이 오히려 위험을 키웁니다.
이 세 가지 지점을 관통하는 공통점은 하나입니다. 문제가 코드처럼 즉시 드러나지 않고 조용히 누적된다는 것입니다. 그래서 프롬프트 엔지니어링에서는 변경을 추적하고 결과를 측정하는 장치를 처음부터 심어 두어야 합니다. 이 장치가 다음 절에서 다룰 버전 관리와 평가 체계입니다.
프롬프트도 코드처럼 버전 관리해야 하는 이유
코드와 마찬가지로 프롬프트에도 변경 이력이 필요합니다. 같은 입력을 넣었을 때 프롬프트 버전에 따라 결과가 어떻게 달라지는지 추적할 수 있어야 하고, 문제가 생기면 이전 버전으로 되돌릴 수 있어야 하며, 팀원 누구나 지금 쓰이는 프롬프트가 무엇인지 알 수 있어야 합니다. 프롬프트 문자열을 데이터베이스나 설정 파일 대신 형상 관리 도구로 관리하면 코드와 똑같은 워크플로를 그대로 쓸 수 있습니다. 변경 사항은 커밋으로 남고 풀 리퀘스트로 리뷰받으며, 필요하면 이전 버전으로 곧바로 돌아갈 수 있습니다.
실무에서는 프롬프트를 템플릿 형태로 저장하는 방식을 씁니다. 템플릿에는 플레이스홀더를 넣어두고 실행 시점에 값을 채워 넣습니다. 이렇게 지시문과 파라미터를 분리해두면 변경 이력을 볼 때 지시문 자체가 바뀐 것인지 단순히 넘기는 값만 바뀐 것인지 구분할 수 있습니다. 이 구분이 없으면 결과가 달라졌을 때 원인을 지시문에서 찾아야 할지 데이터에서 찾아야 할지조차 알 수 없습니다.
버전마다 무엇이 왜 바뀌었는지, 그 결과 정확도나 일관성이 어떻게 달라졌는지를 기록으로 남겨두면 나중에 문제가 생겼을 때 원인을 찾고 롤백하기가 수월해집니다. 이 기록은 거창할 필요가 없습니다. 어떤 버전에서 어떤 문제를 발견해 어떤 문장을 고쳤고 그 결과 어느 지표가 얼마나 움직였는지 몇 줄만 남겨도 다음 사람이 같은 실수를 반복하지 않습니다. 반대로 기록 없이 문장만 계속 고치면 팀 전체가 같은 시행착오를 몇 번이고 되풀이하게 됩니다.
버전 관리가 특히 힘을 발휘하는 순간은 모델을 교체하거나 업그레이드할 때입니다. 이전 절에서 언급했듯 모델 버전이 바뀌면 프롬프트의 암묵적 규칙도 함께 흔들립니다. 버전 관리 체계가 갖춰져 있으면 새 모델에 맞춰 프롬프트를 조정하는 과정을 안전하게 실험할 수 있습니다. 기존 버전을 그대로 둔 채 새 버전을 나란히 만들어 비교하고, 새 버전이 확실히 더 낫다는 판단이 서면 그때 기존 버전을 대체하는 식의 점진적 전환이 가능해집니다.
평가 없는 버전 관리는 의미가 없다
버전 관리만으로는 부족합니다. 버전을 기록해도 각 버전의 품질을 재는 기준이 없으면 어떤 버전이 더 나은지 판단할 근거가 없습니다. 프롬프트도 테스트가 필요합니다. 가장 기본적인 테스트는 정답 데이터셋으로 정확도를 재는 것입니다. 입력과 기대 출력을 미리 준비해두고 현재 프롬프트가 어느 정도 정확도를 내는지 측정합니다. 이 데이터셋은 처음부터 완벽할 필요는 없습니다. 실제 서비스에서 나온 까다로운 사례를 하나씩 추가해 나가면서 점점 대표성을 갖춰 가면 됩니다.
정확도 외에 함께 살펴야 할 지표가 몇 가지 더 있습니다. 일관성은 같은 입력을 여러 번 실행했을 때 매번 같은 출력이 나오는지를 보는 지표입니다. 모델은 확률적으로 답을 생성하기 때문에 같은 질문에도 매번 조금씩 다른 답이 나올 수 있는데, 이 변동폭이 지나치게 크면 사용자 입장에서는 신뢰할 수 없는 기능으로 느껴집니다. 구조 일치는 출력이 요청한 형태를 실제로 지켰는지를 보는 지표이고, 앞서 다룬 스키마 검증과 내용 검증을 자동화한 결과가 여기에 해당합니다.
이 세 지표를 함께 보면 문제의 성격을 훨씬 빠르게 좁힐 수 있습니다. 정확도는 낮은데 일관성이 높다면 프롬프트가 잘못된 방향으로 일관되게 유도하고 있다는 뜻이므로 지시문 자체를 의심해야 합니다. 반대로 정확도는 준수한데 일관성이 낮다면 모델의 확률적 변동이 원인일 가능성이 크므로 온도 설정이나 재시도 전략을 손봐야 합니다. 지표를 분리해서 보지 않으면 원인을 엉뚱한 곳에서 찾다가 시간을 허비하게 됩니다.
이 평가 체계를 배포 파이프라인에 연결하면 프롬프트 변경도 코드 변경과 똑같은 안전망을 갖게 됩니다. 새 버전을 배포하기 전에 정답 데이터셋으로 자동 채점하고, 기존 버전보다 지표가 떨어지면 배포를 막는 식의 게이트를 걸 수 있습니다. 이렇게 하면 프롬프트를 고치는 일이 더 이상 감에 의존한 도박이 아니라 측정 가능한 개선 작업이 됩니다.
그래서 무엇을 오늘 바꿀 것인가
지금까지 살펴본 세 겹의 계약을 정리하면 다음과 같습니다. 프롬프트를 함수처럼 다뤄 입력과 출력의 범위를 명시하고, Few-Shot과 Chain-of-Thought로 입력 계약을 강화하며, 구조화 출력과 내용 검증으로 출력 계약을 지키고, 버전 관리와 평가 체계로 변경 이력의 계약을 관리합니다. 이 네 가지는 순서대로 쌓이는 것이지 어느 하나만 골라 적용해도 되는 선택지가 아닙니다. 입력 계약 없이 출력 계약만 지키면 형식은 맞는데 내용이 틀린 응답이 쌓이고, 두 계약을 다 지켜도 버전 관리가 없으면 왜 지난주보다 품질이 떨어졌는지 알 수 없습니다.
오늘 당장 시작할 수 있는 가장 작은 단위는 지금 쓰는 프롬프트 하나를 골라 입력과 출력을 명시적으로 다시 써 보는 것입니다. 지시문에 무엇이 들어오고 무엇이 나가야 하는지 한 줄씩 적어 보면, 지금까지 모델의 재량에 맡겨 두었던 빈칸이 얼마나 많았는지 바로 드러납니다. 그 빈칸을 하나씩 채워 나가는 과정이 곧 프롬프트 엔지니어링입니다.
이 접근이 특별히 새로운 발상은 아닙니다. 소프트웨어 엔지니어가 함수를 설계하고 테스트하고 버전을 관리해온 방식을 그대로 프롬프트에 옮겨 오는 것뿐입니다. 다만 프롬프트가 자연어라는 이유로 이 규율을 생략해도 된다고 착각하는 팀이 여전히 많습니다. 자연어이기 때문에 오히려 계약을 명시하지 않으면 아무도 그 경계를 모르게 됩니다. 이미 알고 있는 엔지니어링 규율을 프롬프트에도 똑같이 적용하는 것, 그것이 이 글에서 전하고 싶은 전부입니다.
더 깊은 예제와 코드로 확인하고 싶다면 전자책 전문을 참고하시기 바랍니다.
챕터 삽화
