AI로 도메인 수준 단위 테스트를 빠르게 만들 수는 있지만, 그 과정에서 비즈니스 규칙을 직접 분해·이해하며 도메인 지식을 쌓을 기회를 놓칠 수 있다는 점을 다룬다.
이 글의 핵심은 AI가 생성한 코드를 불신하자는 것이 아니라, 도메인 규칙을 배울 기회를 놓치게 된다는 데 있다. 도메인 테스트를 수동으로 작성하면 개발자가 비즈니스 로직에 직접 몰입하게 되고, 그 과정에서 이해가 깊어지는데, 이는 견고한 소프트웨어를 만드는 데 필수적이다.
지난 20년 동안 저는 리테일, 헬스케어, 석유·가스, 에너지 등 다양한 산업의 대규모 조직과 함께 일해 왔습니다. 제가 마주한 가장 큰 도전은 프로그래밍 언어, 기술, 플랫폼이 아니었습니다. 도메인 지식이었습니다. 고품질 소프트웨어를 만들려면 비즈니스 규칙을 완전히 이해하고, 뒤에서 돌아가는 복잡한 프로세스를 파악하는 것이 매우 중요합니다.
도메인을 이해하는 두 번째로 좋은 방법은 단위 테스트를 작성하는 것입니다. 단위 테스트는 비즈니스 규칙을 더 작고 다루기 쉬운 구성요소로 쪼개도록 강제합니다. 이를 통해 핵심 로직을 명확히 하고 검증하며, 소프트웨어가 도메인의 특정 요구사항과 일치하도록 보장할 수 있습니다.
저는 여전히 새로운 도메인을 배우는 가장 효과적인 방법은 도메인 전문가와 대화하는 것이라고 믿습니다. 가능하다면 식사 같은 편안한 자리에서요. 미리 질문을 준비해 두면, 전문가가 편안하고 맛있는 음식을 즐기는 동안 최대한 많은 통찰력 있는 질문을 할 수 있습니다. 아, 그리고 잊지 마세요—계산은 당신이 하는 겁니다! 😉
이 글에서는 AI를 활용해 도메인 수준 단위 테스트를 작성하면 속도는 빨라질 수 있지만, 그 대가로 도메인을 배우고 깊이 이해할 결정적 기회를 놓치게 된다는 점을 설명하겠습니다. 이 테스트들을 수동으로 작성하면 비즈니스 규칙에 직접 맞닥뜨리게 되고, 효과적이고 정확한 소프트웨어를 만드는 데 필수적인 가치 있는 인사이트를 얻게 됩니다.
도메인은 모든 애플리케이션의 핵심이며, 중요한 비즈니스 규칙과 로직이 모두 들어 있습니다. 도메인은 애플리케이션이 어떻게 동작하는지를 규정하고, 비즈니스의 실제 운영 및 프로세스와의 정렬을 보장합니다. 따라서 비즈니스 목표를 충족하는 소프트웨어를 제공하는 데 필수적입니다.
깊은 도메인 지식이 없어도 유능한 개발자가 될 수는 있습니다. 하지만 그 이해를 갖추면 당신의 가치는 기하급수적으로 올라갈 수 있습니다. 도메인을 마스터하면 단순한 코더가 아니라, 비즈니스의 고유한 요구와 목표에 맞는 해결책을 더 잘 설계할 수 있는 비즈니스 성공의 핵심 기여자가 됩니다. 이는 어떤 팀이나 조직에서도 더 가치 있는 자산이 되게 합니다.
앞서 말했듯이, 낯선 도메인을 이해하는 가장 좋은 방법 중 하나는 그 도메인을 대상으로 단위 테스트를 작성하는 것입니다. 의료 보험 산업의 사용자 스토리 예시를 들어 보겠습니다.
SwiftUI 아키텍처에 대해 더 배우고 싶다면 제 책을 확인해 보세요. "
".
청구 처리 시스템으로서, 청구 심사(adjudication) 과정에서 각 환자의 보험 플랜에 특화된 규칙을 정확히 적용하여 보장 범위(coverage), 본인부담금(co-pay), 공제금(deductible), 공동부담률(co-insurance), 환자 부담(patient responsibility)을 올바르게 계산하고 싶다.
이 대형 보험 프로젝트에서 일하는 개발자는 의료 보험 도메인 경험이 거의 없거나 전혀 없을 수 있습니다. 프로젝트를 진행하다 보면 아래와 같은 코드를 마주칠 수 있습니다:
swiftstruct InsurancePlan { let deductible: Double let coInsurance: Double let coPayForGP: Double? let outOfPocketMax: Double let exclusions: [String] var paidTowardsDeductible: Double = 0 var totalOutOfPocket: Double = 0 func isServiceExcluded(_ service: String) -> Bool { return exclusions.contains(service) } mutating func processVisit(for visit: Visit) -> (insurancePays: Double, patientPays: Double) { var patientResponsibility = 0.0 var insurancePayment = 0.0 // If this is a GP visit and there's a co-pay if visit.isGeneralPractitioner, let coPay = coPayForGP { patientResponsibility += coPay insurancePayment = max(0, visit.cost - coPay) return (insurancePayment, patientResponsibility) } // Deductible logic let remainingDeductible = deductible - paidTowardsDeductible if remainingDeductible > 0 { if visit.cost > remainingDeductible { // Apply the rest of the deductible patientResponsibility += remainingDeductible paidTowardsDeductible = deductible // Apply co-insurance after deductible is met let remainingCostAfterDeductible = visit.cost - remainingDeductible patientResponsibility += remainingCostAfterDeductible * coInsurance insurancePayment = remainingCostAfterDeductible * (1 - coInsurance) } else { // Entire cost goes toward the deductible patientResponsibility += visit.cost paidTowardsDeductible += visit.cost } } else { // Deductible has already been met, apply co-insurance directly patientResponsibility += visit.cost * coInsurance insurancePayment = visit.cost * (1 - coInsurance) } // Check if out-of-pocket max is exceeded totalOutOfPocket += patientResponsibility if totalOutOfPocket > outOfPocketMax { let overage = totalOutOfPocket - outOfPocketMax patientResponsibility -= overage totalOutOfPocket = outOfPocketMax insurancePayment += overage } return (insurancePayment, patientResponsibility) } }
이 코드는 환자의 방문(진료) 비용에 대한 재무 처리를 담당하므로 매우 중요합니다. 정확한 처리는 환자에게 올바르게 청구하고, 의료 제공자(병원/의사)가 적절히 보상받으며, 시스템이 의료 규정을 준수하도록 보장하는 데 필수적입니다. 어떤 오류든 재무 불일치, 환자 불만, 심지어 법적 문제로 이어질 수 있습니다.
함수를 더 잘 이해하는 한 가지 방법은 코드를 AI에 제공하고 로직을 설명하게 하는 것입니다. 다만 AI는 보이는 코드에 기반해 가정을 세우며, 제공되지 않은 더 작은 함수나 의존 함수의 구현 세부사항까지 완전히 이해하지는 못할 수 있다는 점을 염두에 두세요.
개발자는 이 코드가 단위 테스트로 충분히 보호되도록 해야 합니다. 적절한 테스트는 공제금(deductible), 본인부담금(co-pay), 제외 항목(exclusions) 같은 복잡한 비즈니스 규칙이 정확히 적용되도록 보장하여, 환자와 의료 제공자 모두에게 영향을 줄 수 있는 오류 위험을 줄여 줍니다. 포괄적인 단위 테스트는 모든 시나리오에서 코드가 기대대로 동작하도록 보장하는 안전망입니다.
테스트를 작성하는 한 가지 접근은, 모든 의존 코드를 ChatGPT 같은 시스템에 넣고 자동으로 테스트 케이스를 생성하도록 AI를 활용하는 것입니다. 아래는 ChatGPT가 구현한 테스트들입니다. 이 테스트들은 의료 보험 도메인에서 다양한 환자 시나리오에 대해 공제금, 본인부담금, 제외 항목, 본인부담 상한(out-of-pocket maximum) 적용 같은 복잡한 비즈니스 로직의 정확성을 보장합니다.
swiftimport XCTest class InsurancePlanTests: XCTestCase { func testSpecialistVisitWithDeductibleNotMet() { var insurancePlan = InsurancePlan( deductible: 1000, coInsurance: 0.20, // 20% coPayForGP: nil, outOfPocketMax: 5000, exclusions: [] ) let patient = Patient(name: "Sarah", insurancePlan: insurancePlan) let specialistVisit = Visit(cost: 350, isGeneralPractitioner: false, serviceType: "cardiology") let (insurancePays, patientPays) = insurancePlan.processVisit(for: specialistVisit) XCTAssertEqual(insurancePays, 120.0, "Insurance should pay $120 for the specialist visit after the deductible") XCTAssertEqual(patientPays, 230.0, "Patient should pay $230 towards the deductible and co-insurance") } func testGeneralPractitionerVisitWithCoPay() { var insurancePlan = InsurancePlan( deductible: 1000, coInsurance: 0.20, coPayForGP: 40, // $40 co-pay for GP visits outOfPocketMax: 5000, exclusions: [] ) let patient = Patient(name: "John", insurancePlan: insurancePlan) let gpVisit = Visit(cost: 100, isGeneralPractitioner: true, serviceType: "general practitioner") let (insurancePays, patientPays) = insurancePlan.processVisit(for: gpVisit) XCTAssertEqual(insurancePays, 60.0, "Insurance should pay $60 for a GP visit with a $40 co-pay") XCTAssertEqual(patientPays, 40.0, "Patient should pay the $40 co-pay for the GP visit") } func testSpecialistVisitWhenDeductibleIsMet() { var insurancePlan = InsurancePlan( deductible: 1000, coInsurance: 0.20, coPayForGP: nil, outOfPocketMax: 5000, exclusions: [] ) // Deductible has been met insurancePlan.paidTowardsDeductible = 1000 let specialistVisit = Visit(cost: 500, isGeneralPractitioner: false, serviceType: "cardiology") let (insurancePays, patientPays) = insurancePlan.processVisit(for: specialistVisit) XCTAssertEqual(insurancePays, 400.0, "Insurance should pay 80% ($400) after the deductible is met") XCTAssertEqual(patientPays, 100.0, "Patient should pay 20% ($100) after the deductible is met") } func testVisitThatExceedsOutOfPocketMax() { var insurancePlan = InsurancePlan( deductible: 1000, coInsurance: 0.20, coPayForGP: nil, outOfPocketMax: 2000, exclusions: [] ) // Total out-of-pocket already near the limit insurancePlan.totalOutOfPocket = 1950 let specialistVisit = Visit(cost: 200, isGeneralPractitioner: false, serviceType: "cardiology") let (insurancePays, patientPays) = insurancePlan.processVisit(for: specialistVisit) XCTAssertEqual(insurancePays, 150.0, "Insurance should cover the rest of the out-of-pocket limit ($150)") XCTAssertEqual(patientPays, 50.0, "Patient should only pay $50 to reach the out-of-pocket maximum") } func testExclusionForPhysicalTherapy() { var insurancePlan = InsurancePlan( deductible: 1000, coInsurance: 0.20, coPayForGP: nil, outOfPocketMax: 5000, exclusions: ["physical therapy"] ) let physicalTherapyVisit = Visit(cost: 300, isGeneralPractitioner: false, serviceType: "physical therapy") XCTAssertTrue(insurancePlan.isServiceExcluded(physicalTherapyVisit.serviceType), "Physical therapy should be excluded from coverage") } func testSpecialistVisitWithPartialDeductible() { var insurancePlan = InsurancePlan( deductible: 1000, coInsurance: 0.20, // 20% coPayForGP: nil, outOfPocketMax: 5000, exclusions: [] ) // Deductible partially met insurancePlan.paidTowardsDeductible = 600 let specialistVisit = Visit(cost: 350, isGeneralPractitioner: false, serviceType: "cardiology") let (insurancePays, patientPays) = insurancePlan.processVisit(for: specialistVisit) XCTAssertEqual(insurancePays, 120.0, "Insurance should pay $120 for the specialist visit") XCTAssertEqual(patientPays, 230.0, "Patient should pay $230 (remaining deductible and co-insurance)") } }
하지만 ChatGPT나 AI가 단위 테스트를 작성하도록 맡기면, 개발자는 도메인에 깊이 관여하고 학습할 중요한 기회를 놓칠 수 있습니다. 테스트를 수동으로 작성하면 개발자가 비즈니스 규칙을 쪼개고 이해하도록 강제되며, 시스템이 의료라는 맥락 안에서 어떻게 동작하는지를 완전히 파악하게 됩니다. 이런 손으로 하는 과정은 도메인 전문성을 쌓는 데 필수적이며, 궁극적으로 개발자가 소프트웨어를 구현·유지보수할 때 더 가치 있고 효과적인 사람이 되게 합니다.
AI가 생성한 단위 테스트가 개발자에게 훌륭한 출발점이 된다고 주장할 수도 있습니다. 코드가 기대대로 동작하는지 빠르게 확인할 수 있으니까요. 하지만 제 반박은, 그것은 출발점이 아니라 종착점이 되기 쉽다는 것입니다. 개발자가 각 테스트 옆의 안심되는 초록 체크 표시를 보는 순간, 같은 깊이로 다시 코드를 들여다볼 가능성은 낮아집니다. AI 생성 테스트에만 의존하면 개발자는 도메인에 진짜로 관여하고, 소프트웨어를 움직이는 비즈니스 규칙에 대한 이해를 다듬을 기회를 놓칠 위험이 있습니다. 이런 참여 부족은 도메인을 피상적으로만 이해하게 만들고, 시간이 지나면서 더 깊은 문제를 발견하거나 시스템을 개선·수정하는 능력을 떨어뜨릴 수 있습니다.
물론 모든 개발자가 그렇다는 말은 아닙니다. 많은 개발자가 도메인을 철저히 이해하려고 노력합니다. 하지만 때로는 촉박한 마감 같은 통제 불가능한 상황 때문에 깊이보다 속도를 우선해야 할 때가 있습니다. 그런 상황에서는 AI 생성 단위 테스트의 편의성이 유혹적일 수 있습니다.
퇴근길에 도넛을 피하는 가장 좋은 방법은 아예 다른 길로 가는 것입니다. 이렇게 경로를 바꾸면 의지력에만 기대는 게 아니라 유혹 자체를 제거하게 되어, 목표를 지키기가 더 쉬워집니다.
테스트에서 AI를 사용하는 것이 생산성을 높이고 개발자의 워크플로를 보완하는 특정 상황에서는 매우 유익할 수 있습니다. AI를 테스트에 활용하기 특히 좋은 경우는 다음과 같습니다.
AI는 반복적인 테스트 생성 자동화에 탁월합니다. 특히 여러 유사한 메서드를 다양한 입력으로 테스트해야 하는 경우에 효과적입니다. 덕분에 개발자는 엣지 케이스나 성능 문제 같은 더 복잡하고 가치 높은 작업에 집중할 수 있습니다.
AI는 사람이 놓치기 쉬운 엣지 케이스를 찾는 데 도움을 줄 수 있습니다. 다양한 테스트 입력을 생성해 시스템을 비정상적이거나 극단적인 경우로 시험함으로써, 그렇지 않으면 발견되지 않을 버그를 찾아낼 수 있습니다.
AI가 생성한 테스트는 수동으로 작성한 테스트를 보완하는 역할을 할 수 있습니다. 개발자가 도메인 특화, 행위 중심(behavior-driven) 테스트 작성에 집중하는 동안, AI는 보다 기계적인 테스트 생성 부분을 처리해 더 적은 노력으로 더 넓은 테스트 커버리지를 확보할 수 있습니다.
새 프로젝트에서 AI는 기본 단위 테스트를 생성해 개발자에게 즉각적인 안전망을 제공할 수 있습니다. 이 테스트들은 출발점 역할을 하며 초기 코드 커버리지를 보장합니다. 개발자들은 비즈니스 규칙에 대한 이해가 쌓이는 대로 더 도메인 특화된 테스트를 점진적으로 추가할 수 있습니다.
AI로 단위 테스트를 생성하면 개발 속도를 높이고, 반복 작업을 줄이며, 엣지 케이스 탐색에도 도움이 될 수 있습니다. 하지만 상당한 트레이드오프도 존재합니다. 가장 큰 우려는 AI가 만든 코드를 믿지 못해서가 아니라, 개발자가 도메인을 진짜로 학습할 기회를 놓친다는 것입니다. 테스트를 수동으로 작성하면 개발자는 비즈니스 규칙에 깊이 관여하게 되고, 소프트웨어를 움직이는 복잡한 프로세스를 더 잘 이해하게 됩니다. 이런 이해는 장기적인 코드 품질을 보장하고, 더 견고하며 도메인에 정렬된 솔루션을 만드는 데 매우 가치가 큽니다.
AI 생성 테스트에만 의존하면 개발자는 이를 관여의 종착점으로 여겨, 전문성을 강화하고 접근 방식을 다듬을 기회를 놓칠 수 있습니다. AI는 반복 작업 자동화와 빠른 성과를 제공하는 데 분명 자리가 있지만, 테스트의 진짜 가치는 개발자가 도메인에 능동적으로 참여하는 데 있습니다. 그것이 결국 더 나은 소프트웨어와 더 깊은 지식으로 이어집니다. AI 활용과 수동 노력을 균형 있게 가져가면, 속도와 이해를 모두 얻을 수 있습니다.