개발자를 위한 AI로 시간 절약하기: 상세한 설명과 예제
1. 개요
이 글은 매우 실질적인 문제를 해결합니다: AI(구체적으로는 GPT, Claude 같은 LLM)를 일상적인 코딩 작업에서 어떻게 사용하여 시간을 절약할 것인가, 그러면서도 ’무지한 복사-붙여넣기’를 하는 사람이 되지 않을 것인가 하는 문제입니다. 이 글에서는 Spring AI를 사용하여 LLM을 Spring Boot 애플리케이션에 통합하는 방법, AI를 사용하여 테스트와 Javadoc을 생성하는 방법, 그리고 피해야 할 함정들을 배우게 됩니다.
이 글을 마치면 Spring Boot에서 LLM을 호출하는 ChatClient를 직접 작성할 수 있고, 각 작업에 맞는 프롬프트를 작성하는 방법(코드 생성, 리뷰, 테스트 작성)을 알게 되며, AI가 언제 도움이 되는지, 언제는 AI가 오히려 당신을 느리게 만드는지 알게 됩니다.
2. AI가 개발 작업에서 어느 부분의 시간을 절약할 수 있을까
AI가 모든 곳에서 당신보다 빠른 것은 아닙니다. AI가 정말 사용할 가치가 있는 업무는 세 가지입니다:
- 반복적이고 패턴이 명확한 작업: entity에서 DTO 생성, getter/setter 생성(IDE가 이미 할 수 있지만), boilerplate 테스트 케이스 생성.
- 정보를 빠르게 종합해야 하는 작업: 긴 stacktrace 읽기, PR 요약, 주석이 없는 레거시 코드 설명.
- 초안이 필요한 작업: Javadoc 작성, README 작성, 복잡한 SQL 쿼리 작성(이후 수정할 용도).
반대로 AI는 시간을 절약하지 못할 때가 있습니다: 문제를 제대로 이해하지 못했을 때(모호한 프롬프트 = 모호한 결과), 회사 고유의 복잡한 비즈니스 로직, 또는 보안/금융과 관련된 높은 정확도가 필요한 코드인데 당신이 제대로 검토하지 않을 때.
주의: AI는 코드를 빠르게 생성하지만 검토가 느리면 총 시간이 줄어들지 않으며, 때로는 “올바르게 보이지만 로직이 틀린” 코드를 디버깅해야 하므로 오히려 늘어날 수도 있습니다.
3. Spring Boot에 Spring AI를 사용하여 LLM 통합하기
수동으로 RestTemplate를 사용하여 REST API를 호출하는 대신, Spring AI를 사용하여 편리한 ChatClient를 얻을 수 있습니다.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
application.properties에서 API 키를 선언합니다:
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
ChatClient를 감싸는 서비스를 만듭니다:
@Service
public class CodeAssistantService {
private final ChatClient chatClient;
public CodeAssistantService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String explainStackTrace(String stackTrace) {
String prompt = """
당신은 Senior Java 엔지니어입니다. 다음 stacktrace의 원인을 간단히
설명하고 해결 방법을 제안해 주세요:
%s
""".formatted(stackTrace);
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
ChatClient.Builder는 위의 starter 덕분에 Spring Boot가 자동으로 설정하므로, 주입하기만 하면 됩니다.prompt().user(...).call().content()는 프롬프트를 보내고 응답 문자열을 받으므로, 수동으로 JSON을 파싱할 필요가 없습니다.explainStackTrace메서드는 긴 stacktrace를 받아서 간단한 설명을 반환합니다. 이렇게 하면 이상한 에러를 디버깅할 때마다 로그를 읽는 데 5-10분이 절약됩니다.
컨트롤러에서 시도해 봅시다:
@RestController
@RequestMapping("/api/assist")
public class CodeAssistantController {
private final CodeAssistantService assistantService;
public CodeAssistantController(CodeAssistantService assistantService) {
this.assistantService = assistantService;
}
@PostMapping("/explain")
public String explain(@RequestBody String stackTrace) {
return assistantService.explainStackTrace(stackTrace);
}
}
POST /api/assist/explain을 호출하고 body에 NullPointerException stacktrace를 보내면 설명 텍스트가 반환됩니다:
`customer` 변수가 `customer.getEmail()`을 호출하기 전에 초기화되지 않아서
NullPointerException이 발생합니다. OrderService를 다시 확인하세요 — 레코드를
찾지 못할 때 repository가 null을 반환할 수도 있습니다. Optional을 사용하거나
속성에 접근하기 전에 null을 확인해야 합니다.
4. AI를 사용하여 자동으로 단위 테스트 생성하기 — 구체적인 예제
가장 시간이 많이 걸리는 작업 중 하나는 각 논리 분기에 대한 테스트 케이스를 작성하는 것입니다. DiscountCalculator 클래스가 있다고 가정해 봅시다:
public class DiscountCalculator {
public BigDecimal calculate(BigDecimal orderAmount, CustomerTier tier) {
if (orderAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Order amount must be positive");
}
BigDecimal rate = switch (tier) {
case GOLD -> new BigDecimal("0.15");
case SILVER -> new BigDecimal("0.10");
case BASIC -> new BigDecimal("0.05");
};
return orderAmount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
}
}
LLM에게 테스트를 생성하도록 요청하는 프롬프트 예제(위의 클래스를 프롬프트에 복사합니다):
DiscountCalculator 클래스를 위해 JUnit 5 + AssertJ 단위 테스트를 작성하세요.
포함할 케이스: GOLD, SILVER, BASIC, 그리고 orderAmount <= 0인 경우
IllegalArgumentException을 throw해야 합니다. 필요한 경우 @ParameterizedTest를 사용하세요.
AI는 보통 거의 정확한 초안을 반환하고, 팀의 스타일에 맞게 수정합니다:
class DiscountCalculatorTest {
private final DiscountCalculator calculator = new DiscountCalculator();
@ParameterizedTest
@CsvSource({
"GOLD, 100, 15.00",
"SILVER, 100, 10.00",
"BASIC, 100, 5.00"
})
void shouldCalculateDiscountByTier(CustomerTier tier, String amount, String expected) {
BigDecimal result = calculator.calculate(new BigDecimal(amount), tier);
assertThat(result).isEqualByComparingTo(new BigDecimal(expected));
}
@Test
void shouldThrowWhenOrderAmountIsNotPositive() {
assertThatThrownBy(() -> calculator.calculate(BigDecimal.ZERO, CustomerTier.GOLD))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Order amount must be positive");
}
}
@ParameterizedTest+@CsvSource는 3개의 tier 케이스를 하나의 테스트로 통합하여 테스트 코드 중복을 줄입니다.- 줄
assertThat(result).isEqualByComparingTo(...)는BigDecimal을 올바르게 비교합니다(scale을 구분하는equals오류를 방지). - 나머지 케이스는 특정 메시지를 포함한 exception을 확인합니다.
AI가 빠르게 처리하는 부분은 경계 케이스를 충분히 나열하는 것입니다(orderAmount <= 0 케이스를 빠뜨리기 쉬움). 당신이 해야 할 일은 expected 값이 정확한지 다시 확인하는 것입니다 — AI가 숫자를 잘못 계산하는 것은 자주 일어나는 일입니다.
5. AI를 사용하여 Javadoc 생성 및 레거시 코드 요약하기
주석이 없는 오래된 코드의 경우, AI가 한 줄씩 읽는 것보다 빠르게 요약합니다. 예를 들어 복잡한 처리 로직이 있다고 가정해 봅시다:
public List<Order> filterEligibleOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getStatus() == OrderStatus.PAID)
.filter(o -> o.getCreatedAt().isAfter(LocalDate.now().minusDays(30)))
.filter(o -> o.getItems().stream().anyMatch(Item::isRefundable))
.toList();
}
프롬프트: “이 메서드에 대한 Javadoc을 작성하세요. 3가지 필터 조건을 명확히 설명해 주세요.” AI가 반환하는 것:
/**
* 환불 처리에 적합한 주문 목록을 필터링합니다.
* 조건: 주문은 PAID 상태여야 하고, 최근 30일 이내에 생성되어야 하며,
* 최소 하나의 환불 가능한 항목을 포함해야 합니다.
*
* @param orders 입력 주문 목록
* @return 위의 세 가지 조건을 모두 만족하는 주문 목록
*/
이것은 새로운 팀원이 레거시 코드베이스에 참여할 때 시간을 절약합니다 — 여러 메서드를 붙여넣으면, AI가 Javadoc 초안을 반환하고, 당신은 비즈니스 용어 오류만 수정하면 됩니다.
6. CI에서 AI를 사용한 코드 검토 자동화
파이프라인에 AI 리뷰 차이 호출 단계를 추가하여 인간이 검토하기 전에 오류를 조기에 발견할 수 있습니다. GitHub Actions에서 API를 호출하는 스크립트의 간단한 예제:
name: ai-review
on: [pull_request]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get diff
run: git diff origin/main...HEAD > diff.txt
- name: Call AI review
run: |
curl -s https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer ${{ secrets.OPENAI_API_KEY }}" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile diff diff.txt \
'{model:"gpt-4o-mini", messages:[{role:"user", content: ("다음 Java diff를 검토하세요. 심각한 문제만 언급해 주세요: " + $diff)}]}')" \
> review.json
git diff origin/main...HEAD는 PR의 정확한 코드 변경 부분만 가져오므로, 전체 저장소를 보내는 것을 방지합니다(토큰 낭비, 느림).jq -n --rawfile은 JSON 본문을 안전하게 구성하여 코드의 특수 문자 이스케이프 오류를 방지합니다.- 결과
review.json은 다른 단계를 사용하여 PR에 댓글로 다시 게시할 수 있습니다.
주의: AI 리뷰는 시니어 리뷰를 대체할 수 없습니다. 명백한 오류만 발견합니다(null 체크 누락, 빈 catch Exception, 기본 SQL 주입). AI가 “승인”했다고 해서 PR을 병합하지 마세요.
7. 일반적인 오류 및 AI를 사용하지 말아야 할 때
Java 프로젝트에서 LLM을 사용할 때의 실제 함정들:
- 존재하지 않는 API 환각: AI는 실제로 클래스에 없는 method
Order.markAsRefundable()을 호출하도록 제안할 수 있습니다. 신뢰하기 전에 항상 컴파일을 시도해 보세요. - 올바른 구문이지만 잘못된 버전의 코드: AI는 Java 8 스타일(
new ArrayList<>())과 Java 21(List.of(...))을 일관성 없이 혼합할 수 있습니다 — 팀의 코딩 규칙에 따라 정규화해야 합니다. - 민감한 정보 유출: secret, 연결 문자열 또는 실제 고객 데이터를 포함한 전체 config를 제3자 AI 서비스로 보내는 프롬프트에 붙여넣지 마세요.
- 모호한 프롬프트 → 모호한 출력: “이 클래스에 대한 테스트를 작성하세요”는 포함해야 할 경계 케이스를 명확히 나열한 프롬프트보다 훨씬 더 공허한 결과를 산출합니다.
- AI를 사용하지 말아야 할 때: 이자 계산 로직, 세금 계산, 또는 약간의 오류가 법적 문제가 될 수 있는 규정 준수 관련 부분 — AI는 프레임워크 작성만 지원해야 하며, 핵심 계산 부분은 AI가 생각해낸 테스트 케이스가 아닌 실제 테스트 케이스로 비즈니스 전문가가 철저히 검토해야 합니다.
AI가 코드를 생성할 때마다 적용해야 할 작은 체크리스트:
1. 코드가 컴파일되나요?
2. AI가 생성한 테스트 케이스가 실제 경계 케이스를 포함하나요, 아니면 "그럴 듯한" 케이스만 포함하나요?
3. 프롬프트에 민감한 정보가 유출되었나요?
4. 코드 스타일이 현재 팀 규칙과 일치하나요?
8. 결론
AI는 반복적인 작업과 빠른 초안이 필요한 작업에서 시간을 절약합니다: 테스트 케이스 생성, stacktrace 설명, Javadoc 작성, 초기 diff 리뷰. Spring AI는 몇 줄의 구성과 ChatClient만으로 Java 애플리케이션에 LLM을 통합하는 것을 간단하게 만들어줍니다.
핵심 포인트는 모든 출력을 신중하게 검토해야 한다는 것입니다 — AI는 작성 시간을 절약하지만, 이해와 검증 시간은 절약하지 못합니다. 올바른 위치에서, 올바른 신뢰 수준으로 사용하면, 코드 품질을 희생하지 않으면서 매주 반복되는 많은 시간을 단축할 수 있습니다.