일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- docker
- 일정관리프로젝트
- Lv.0
- 디자인 패턴
- 알고리즘
- Java
- LV02
- LV0
- 프로그래머스
- LV.02
- CI/CD
- 연습문제
- CoffiesVol.02
- 이것이 자바다
- SQL
- GIT
- LV03
- 데이터 베이스
- LV1
- spring boot
- 포트폴리오
- Join
- JPA
- 코테
- LV01
- S3
- 일정관리 프로젝트
- Redis
- Kafka
- mysql
- Today
- Total
코드 저장소.
OpenFeign를 활용해서 일정추천기능 만들기. 본문
목차
1.도입 배경
2.기술 스택 및 선택 이유
3.전체 구성
4.서비스 구성
5.결과 및 테스트
6.회고 및 다음단계
1.도입 배경
일정관리 프로젝트를 진행하면서 사용자의 하루 일정에서 비어 있는 시간대를 찾아 AI가 추천 일정을 제공해주는 기능을 만들어 보기로 했다. 이를 위해서 OpenAI의 Chat Completions API를 사용해 프롬프트 기반 일정 생성을 시도했고, 통신은 OpenFeign + Spring Cloud를 사용해 구성했다.
2.기술 스택 및 선택 이유
- Spring Boot 3.2: 기본 백엔드 프레임워크
- OpenFeign: 외부 API(OpenAI) 호출 간결화
- Resilience4j: 장애 발생 시 fallback 처리
- OpenAI GPT-4o: 일정 추천 생성 로직 처리
- Jackson + Regex: 응답 파싱 처리
3.전체 구성
로직을 설명하자면 다음과 같습니다. 프론트 화면에서 일정추천의 버튼을 눌러서 요청이 들어오면 서비스단에서 OpenAiClient 호출을 하고 Feign으로 OpenAi API 프롬프트를 적용해서 현재 일정에 비어있는 시간을 찾아서 추천일정을 요청합니다. 여기에서 응답에 실패를 한 경우에는 CiurcuitBreaker 와 FacllbackDto를 적용해서 예외를 처리를 했고 OpenAiResponseDto로 응답을 하는 방식입니다.
4.서비스 구성
@Configuration
public class OpenApiConfig {
@Value("${openai.secret-key}")
private String openAiApiKey;
@Bean
public RequestInterceptor openAiRequestInterceptor() {
return requestTemplate -> {
requestTemplate.header("Authorization", "Bearer " + openAiApiKey);
requestTemplate.header("Content-Type", "application/json");
};
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(
1000, // initial interval (ms)
TimeUnit.SECONDS.toMillis(1), // max interval
3 // max attempts
);
}
}
OpenAiConfig 설정에서는 인증에 필요한 Header와 Retryer를 적용했습니다.
@FeignClient(name = "OpenAi", url = "https://api.openai.com/v1",
configuration = OpenApiConfig.class, fallback = OpenAiFallback.class)
public interface OpenAiClient {
@PostMapping("/chat/completions")
OpenAiResponse getChatCompletion(@RequestBody OpenAiRequest request);
}
OpenFeign 선언부에서는 설정 클래스와 에러가 났을때를 대비해서 FallbackDto를 설정을 했습니다.
다음은 서비스 코드단입니다.
일정 추천 기능은 크게 세 가지 포인트로 구성됩니다.
recommendSchedules()
// 핵심 메서드
@CircuitBreaker(name = "openAiClient", fallbackMethod = "fallbackRecommendSchedules")
public List<Schedules> recommendSchedules(String userId, Pageable pageable) throws Exception {
Long memberId = memberRepository.findByUserId(userId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."))
.getId();
List<SchedulesModel> schedulesModels = schedulesRepository.findAllByUserId(userId, pageable).getContent();
List<Schedules> schedules = mapToSchedules(schedulesModels);
// Prompt 생성 → OpenAI 호출
String prompt = buildPromptFromSchedules(schedules);
OpenAiRequest request = buildOpenAiRequest(prompt);
OpenAiResponse response = aiClient.getChatCompletion(request);
String rawContent = response.getChoices().get(0).getMessage().getContent();
String extractedJson = extractJsonFromContent(rawContent);
List<ScheduleRecommendationDto> recommendedList =
objectMapper.readValue(extractedJson, new TypeReference<>() {});
return mapToRecommendedSchedules(recommendedList, memberId);
}
- 디비에서 사용자 아이디를 기반으로 한 일정목록을 조회를 합니다.
- 조회를 한 목록을 바탕으로 프롬프트를 생성을 합니다.
- OpenAI API를 호출하고 응답(JSON)을 파싱하여 추천 일정을 반환합니다.
- 응답에 문제가 생겼을 시에는 @CircuitBreaker에서 FallbackDto를 반환합니다.
fallbackRecommendSchedules()
// fallback
public List<Schedules> fallbackRecommendSchedules(String userId, Pageable pageable, Throwable t) {
log.error("[OpenAI fallback 작동] userId={}, 이유: {}", userId, t.getMessage(), t);
return List.of(Schedules.builder()
.contents("AI 추천 일정 제공 불가 - 기본 일정")
.scheduleMonth(LocalDateTime.now().getMonthValue())
.scheduleDay(LocalDateTime.now().getDayOfMonth())
.startTime(LocalDateTime.now())
.endTime(LocalDateTime.now().plusHours(1))
.userId(Long.parseLong(userId))
.build());
}
- OpenAI API 호출이 실패하면 Fallback이 동작합니다.
- 이때는 “AI 추천 불가 – 기본 일정”이라는 대체 일정을 생성하여 반환합니다.
buildPromptFromSchedules()
if (사용자 일정이 없음) {
→ 아침/점심/저녁에 맞춘 2~3개의 추천 일정 생성 요청
→ JSON 형식으로만 응답하도록 강제
} else {
→ 사용자의 일정 목록을 나열
→ 빈 시간대 / 아침·저녁 유무 / 일정 과부하 여부 반영
→ JSON 포맷으로만 응답하도록 강제
}
- 디비를 조회를 했을때 목록이 없는 경우에는 if문에 있는 프롬프트를 적용을 합니다.
- 목록이 없는 경우에는 일정 목록 시간대에 빈 시간이 있는 곳과 아침.저녁유무를 반영을 해서 응답을 내보냅니다.
5.결과 및 테스트
요청:
GET /api/chat/recommend?userId=test&page=0&size=5
응답:
[
{"contents": "독서", "startTime": "2025-04-04T17:00:00"}
]
6.회고 및 다음 단계
이 기능을 구현하며 깨달은 점
- 이번 기능을 구현하면서 가장 크게 느낀 점은 AI 연동은 단순한 통신 문제가 아니라 데이터 구조화의 문제라는 것이다. 처음에는 단순히 OpenAI API를 호출하면 알아서 적절한 결과를 줄 것이라 기대했지만, 실제로는 입력과 출력의 포맷을 얼마나 명확하게 정의하느냐가 결과 품질을 좌우했다. 그래서 결국 프롬프트를 설계하는 과정이 마치 API 스펙을 정의하는 일과 같다는 것을 깨달았다.
- 장애 대비가 없는 AI 연동은 운영 환경에서 치명적일 수 있다는 점도 알게 되었다. 외부 API는 언제든지 느려지거나 실패할 수 있는데, CircuitBreaker나 Fallback 같은 복원력 패턴을 적용하지 않았다면 서비스 전체가 멈추는 상황이 발생할 수 있었다. 실제로 몇 차례 테스트 중 API 호출이 실패했을 때 기본 일정을 반환하도록 처리해둔 것이 얼마나 중요한지 체감했다.
- 마지막으로, 프롬프트 설계는 결국 개발 경험과 닮아 있다는 생각이 들었다. 단순히 “일정을 추천해 달라”가 아니라 “반드시 JSON 배열 형식으로 반환할 것, null은 허용하지 않을 것” 같은 제약을 주어야 예측 가능한 결과를 얻을 수 있었다. 이는 우리가 DTO나 API 응답 형식을 엄격히 정의하는 것과 다르지 않았다.
이번 경험을 통해 AI를 서비스에 접목할 때는 “데이터 구조화”, “복원력”, “명확한 계약(프롬프트)”이라는 세 가지 요소가 핵심이라는 점을 배웠다. 앞으로는 여기에 더해 추천 일정이 기존 일정과 충돌하지 않도록 자동 검증을 추가하고, 결과를 DB에 저장해 히스토리 관리까지 확장하며, 사용자 선호도를 반영한 맞춤형 추천으로 기능을 발전시켜 나갈 계획이다.
'포폴 > 일정관리앱' 카테고리의 다른 글
모듈을 변경한 이유 (0) | 2025.05.04 |
---|---|
일정알림기능 구축1 -일정 관리 프로젝트에 이벤트 드리븐 아키텍처를 적용하며 (0) | 2025.04.21 |
첨부파일업로드 기능2-AWS S3 Presigned URL을 이용한 업/다운로드 구현 (0) | 2025.04.04 |
첨부파일업로드 기능1-첨부파일에 관련된 로직의 고찰 (0) | 2025.03.23 |
System.out.println()을 쓰면 안 되는 이유 (0) | 2025.03.23 |