Spring AI를 배우기 전에 정리할 것
프론트엔드도 알아야 하지만, 백엔드는 조금 더 깊게 파고들고 싶다. 기술을 한 가지만 고르기보다 여러 기술을 경험하되, 결국에는 내가 잘 아는 도메인 하나를 만들어야 한다.
Spring AI를 배우기 전에 정리할 것
프론트엔드도 알아야 하지만, 백엔드는 조금 더 깊게 파고들고 싶다. 기술을 한 가지만 고르기보다 여러 기술을 경험하되, 결국에는 내가 잘 아는 도메인 하나를 만들어야 한다.
그 전에 객체 지향, Spring Boot의 계층 구조, 그리고 AI 기능이 어디에 들어가는지를 한 흐름으로 정리해 본다.
객체 지향: 캡, 추, 다, 상
객체 지향의 핵심은 클래스로 만든 객체가 자기 역할을 가지고 협력하게 하는 것이다. 외우기 쉽게 캡, 추, 다, 상이라고 정리할 수 있다.
| 개념 | 뜻 | 쉽게 말하면 |
|---|---|---|
| 캡슐화 | 데이터와 접근 방법을 객체 안에 묶고 통제한다 | 중요한 값은 아무나 바꾸지 못하게 숨긴다 |
| 추상화 | 복잡한 구현을 감추고 필요한 기능만 보여 준다 | 자동차를 운전할 때 엔진 구조까지 알 필요는 없다 |
| 다형성 | 같은 호출이라도 객체에 따라 다르게 동작한다 | pay()를 호출해도 카드, 계좌이체가 각자 다르게 결제한다 |
| 상속 | 기존 클래스의 특징을 물려받아 확장한다 | 공통 기능을 부모 클래스에 두고 필요한 부분만 늘린다 |
interface Payment {
void pay(int amount);
}
class CardPayment implements Payment {
@Override
public void pay(int amount) {
System.out.println("카드로 " + amount + "원 결제");
}
}
class TransferPayment implements Payment {
@Override
public void pay(int amount) {
System.out.println("계좌이체로 " + amount + "원 결제");
}
}
Payment payment = new CardPayment();
payment.pay(10_000); // 같은 pay(), 다른 동작
LLM 기본 용어
AI를 붙이기 전에 모델이 무엇을 기준으로 동작하는지 알아야 한다.
| 용어 | 한 줄 정의 | 알아 둘 점 |
|---|---|---|
| 토큰, token | 모델이 텍스트를 나누어 처리하는 단위 | 비용, 입력 길이, 출력 길이의 기준이다. 한글 토큰 수는 문장과 모델마다 달라진다. |
| 컨텍스트 윈도우 | 요청 한 번에 모델이 읽을 수 있는 총 토큰 범위 | 한도를 넘으면 입력을 줄이거나 요약해야 한다. |
| 프롬프트 | 모델에 보내는 전체 입력 | 지시, 맥락, 예시, 원하는 출력 형식을 함께 넣는다. |
| 시스템 메시지 | 모델의 역할과 규칙을 정하는 상위 지시 | 사용자 질문과 분리해서 관리한다. |
| temperature | 답변의 무작위성 | 분류, 추출은 낮게, 아이디어 생성은 조금 높게 둔다. |
| top-p | 다음 토큰 후보를 확률 범위로 제한하는 값 | temperature와 함께 무작정 조절하지 않는다. |
| 할루시네이션 | 모델이 모르는 내용을 그럴듯하게 만드는 현상 | 근거를 제공하고, 모르면 모른다고 답하게 해야 줄어든다. |
| 파인튜닝 | 모델 가중치를 추가 학습하는 일 | 최신 지식 주입보다 말투, 형식, 반복 패턴을 맞출 때 더 잘 맞는다. |
| 멀티모달 | 텍스트 외 이미지, 음성 등을 함께 다루는 기능 | 모델이 해당 입력과 출력을 지원해야 한다. |
임베딩, 벡터 검색, RAG
임베딩이란?
임베딩은 문장을 의미를 담은 숫자 배열, 즉 벡터로 바꾸는 것이다. 뜻이 비슷한 문장은 벡터 공간에서도 가까워진다.
예를 들어 사용자가 휴가 내는 법이라고 물어도, 문서에 연차 신청 규정이라고 적혀 있으면 의미가 가까워서 찾을 수 있다. 단어가 정확히 같지 않아도 검색할 수 있다는 점이 핵심이다.
| 용어 | 한 줄 정의 | 알아 둘 점 |
|---|---|---|
| 임베딩, embedding | 텍스트를 의미 벡터로 바꾸는 것 | 의미가 가까울수록 벡터도 가까워진다. |
| 차원, dimension | 벡터를 이루는 숫자의 개수 | 모델을 바꾸면 차원이 달라질 수 있어 재색인이 필요하다. |
| 코사인 유사도 | 두 벡터가 향하는 방향의 유사도 | 보통 1에 가까울수록 의미가 비슷하다. |
| 벡터 DB | 벡터, 원문, 메타데이터를 저장하고 검색하는 저장소 | pgvector, Chroma, Qdrant 등이 있다. |
| 청킹, chunking | 문서를 검색 가능한 작은 조각으로 나누는 일 | 너무 크면 잡음이 많고, 너무 작으면 문맥이 끊긴다. |
| overlap | 이웃 청크끼리 일부 문장을 겹치게 하는 것 | 문장이 중간에서 잘려 근거가 사라지는 일을 줄인다. |
| top-k | 검색 결과로 가져올 청크 개수 | 크게 하면 근거는 늘지만 비용과 잡음도 늘어난다. |
| MMR | 유사도와 결과의 다양성을 같이 보는 방식 | 비슷한 문서만 여러 개 나오는 것을 줄인다. |
| 인덱스 | 벡터를 빠르게 찾기 위한 자료구조 | 데이터가 커지면 HNSW, IVF 같은 인덱스를 고려한다. |
RAG란?
RAG, Retrieval-Augmented Generation는 질문과 관련된 문서를 먼저 찾고, 그 근거를 모델에게 함께 주어 답하게 하는 방식이다. 모델을 다시 학습하지 않아도 문서를 교체하거나 추가해서 지식을 갱신할 수 있다.
문서 파일
→ 읽기
→ 청킹
→ 임베딩
→ VectorStore에 저장
사용자 질문
→ 질문 임베딩
→ 유사한 문서 검색
→ 검색 결과를 프롬프트에 추가
→ 모델 답변 + 출처 반환
| 용어 | 한 줄 정의 | 알아 둘 점 |
|---|---|---|
| ingest | 문서를 읽고, 자르고, 임베딩해 저장하는 과정 | 보통 문서가 바뀔 때 배치 작업으로 실행한다. |
| retriever | 질문에 맞는 청크를 찾아 주는 부분 | Spring AI에서는 VectorStore, Advisor 등을 이용해 구성한다. |
| 근거, 출처 | 답변의 바탕이 된 문서 조각과 위치 | 출처를 같이 보여 줘야 답변을 확인할 수 있다. |
| rerank | 검색 후보를 다시 점수 매겨 상위 결과를 고르는 것 | 검색 품질을 올릴 때 효과가 큰 단계다. |
| 하이브리드 검색 | 키워드 검색과 의미 검색을 결합하는 방식 | 제품 코드, 고유명사처럼 정확한 단어가 중요한 경우 좋다. |
| metadata filter | 메타데이터 조건으로 검색 범위를 제한하는 것 | 권한은 프롬프트가 아니라 필터와 DB 쿼리에서 막아야 한다. |
도구와 에이전트
모델은 직접 DB를 수정하거나 이메일을 보내지 않는다. 모델은 어떤 도구를 호출할지 판단하고, 실제 실행은 우리 코드가 담당한다.
| 개념 | 뜻 | 주의할 점 |
|---|---|---|
| Tool calling | 모델이 정해진 함수 호출을 요청하는 기능 | 함수 이름, 설명, 파라미터 설명이 모델에게는 API 명세다. |
| Agent | 생각하고 도구를 호출한 뒤 결과를 보고 다시 판단하는 흐름 | 호출 횟수와 시간 제한을 두지 않으면 비용이 커질 수 있다. |
| Human approval | 되돌리기 어려운 행동 전에 사람의 확인을 받는 것 | 환불, 발송, 삭제 같은 작업에 필요하다. |
| Guardrail | 입력과 출력에 적용하는 안전 규칙 | 차단은 메모리 저장이나 외부 호출보다 먼저 해야 한다. |
| Prompt injection | 모델 지시를 무시하게 만들려는 공격 | 검색 문서 안에 숨어 들어오는 간접 공격도 확인해야 한다. |
Spring Boot 기초: 계층 구조와 어노테이션
왜 계층을 나눌까?
계층을 나누는 이유는 문제가 생겼을 때 바꿔야 할 부분만 고치기 위해서다. 화면 요구가 바뀌면 Controller, 업무 규칙이 바뀌면 Service, 저장 방식이 바뀌면 Repository나 Mapper를 중심으로 수정한다.
HTTP 요청
→ Controller: 받고, 검증하고, DTO로 응답한다
→ Service: 업무 흐름과 트랜잭션을 결정한다
→ Repository 또는 Mapper: DB와 외부 시스템에 접근한다
→ DB / 외부 API
| 계층 | 역할 | 여기 두지 않는 것 |
|---|---|---|
| Controller | HTTP 요청, 검증, 응답 DTO 변환 | 복잡한 업무 규칙, SQL |
| Service | 업무 흐름, 트랜잭션, 여러 저장소 조합 | HTTP 세부 처리, 화면 응답 형식 |
| Repository | JPA로 엔티티를 저장하고 조회 | 업무 정책 |
| Mapper | MyBatis SQL을 직접 실행 | Controller 역할 |
| DTO | API에서 주고받는 데이터 모양 | DB 구조 그 자체 |
요청 하나가 지나가는 흐름
GET /ch02/orders/12345?userId=user1 요청을 예로 들면 다음과 같다.
OrderController가@PathVariable,@RequestParam,@Valid로 값을 받는다.OrderService에 주문 번호와 사용자 ID처럼 필요한 값만 전달한다.OrderRepository또는OrderMapper가 권한 조건을 포함해 데이터를 조회한다.- 엔티티나 조회 결과를 응답 DTO로 변환한다. 원가나 소유자 ID처럼 외부에 보이면 안 되는 값은 제외한다.
- Controller가 DTO를 JSON으로 반환한다.
400은 주로 입력 검증, 404는 업무상 대상 없음, 500은 DB 연결이나 예상하지 못한 오류에서 많이 발생한다. 하지만 실제 상태 코드는 서비스의 정책과 예외 처리 방식에 따라 정해야 한다.
자주 쓰는 어노테이션
| 분류 | 대표 어노테이션, 방식 | 역할 |
|---|---|---|
| 빈 등록 | @Component, @Service, @Repository, @Controller | 클래스를 Spring이 관리하는 Bean으로 등록한다. |
| 요청 매핑 | @RestController, @GetMapping, @PostMapping | HTTP 요청을 메서드에 연결한다. |
| 의존성 주입 | 생성자 주입, @Autowired, @Qualifier | 필요한 Bean을 찾아 넣는다. |
| 설정 | @Configuration, @Bean, @ConfigurationProperties | 직접 만든 Bean과 외부 설정을 등록한다. |
| 검증, 예외 | @Valid, @ExceptionHandler, @RestControllerAdvice | 입력을 검증하고 예외를 API 응답으로 바꾼다. |
| 부가 기능 | @Transactional, @Async, @Retryable, @Aspect | 원래 업무 코드에 공통 기능을 덧붙인다. |
@SpringBootApplication은 설정 클래스, 컴포넌트 스캔, 자동 구성을 묶어 둔 시작점이다. Spring AI 스타터를 추가했을 때 관련 Bean이 자동으로 준비되는 것도 이 자동 구성 흐름에 올라탄다.
@Service, @Repository, @Controller는 모두 @Component의 구체적인 표현이다. 이름만 봐도 역할을 알 수 있게 해 주며, @Repository는 DB 접근 예외를 Spring의 예외 체계로 변환하는 의미도 가진다.
Bean과 의존성 주입
Bean은 Spring이 만들어 관리하는 객체다. 기본 스코프는 singleton이라서 애플리케이션 전체에서 보통 하나만 만든다. 그래서 Service의 필드에 사용자별 값이나 요청별 상태를 저장하면 안 된다.
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public OrderResponse findOrder(String orderId, String userId) {
return repository.findByIdAndOwnerId(orderId, userId)
.map(OrderResponse::from)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
}
필드에는 Repository처럼 바뀌지 않는 협력 객체를 두고, 주문 번호나 사용자 ID 같은 요청별 값은 메서드 파라미터로 받는다.
| 스코프 | 생성 시점 |
|---|---|
| singleton | 애플리케이션당 보통 하나, 기본값 |
| prototype | 주입 또는 요청할 때마다 새 객체 |
| request | HTTP 요청마다 하나, 웹 환경에서 사용 |
DTO와 엔티티는 분리한다
엔티티는 DB 구조와 연결된 내부 모델이고, DTO는 API에서 주고받는 데이터 모양이다. 엔티티를 그대로 반환하면 DB 구조가 API 규격이 되고, 원가나 소유자 ID 같은 내부 정보가 실수로 노출될 수 있다.
public record CreateOrderRequest(
@NotBlank String productId,
@Min(1) @Max(99) int quantity,
@Size(max = 200) String memo
) {
}
public record OrderResponse(
String orderId,
String item,
String status,
LocalDate eta
) {
public static OrderResponse from(Order order) {
return new OrderResponse(
order.getId(),
order.getItem().getName(),
order.getStatus().name(),
order.getEta()
);
}
}
@Entity
class Order {
@Id
private String id;
private String ownerId; // 내부 전용
private BigDecimal cost; // 외부에 노출하면 안 되는 원가
}
DTO 변환 위치는 팀 규칙에 따라 다를 수 있지만, 중요한 것은 한 방향으로 일관되게 두는 것이다. 위 예시는 응답 DTO의 from() 메서드에 변환을 모았다.
JPA Repository와 MyBatis Mapper
둘 다 데이터를 다루지만 기준은 다르다. 객체와 엔티티의 상태 변경이 중심이면 JPA가 편하고, 복잡한 SQL을 직접 제어해야 하면 MyBatis Mapper가 편하다. 한 프로젝트에서 함께 써도 된다.
| 구분 | JPA Repository | MyBatis Mapper |
|---|---|---|
| 중심 | 객체, 엔티티, ORM | SQL |
| SQL 작성 | 기본 CRUD는 JPA가 생성 | 개발자가 직접 작성 |
| 잘 맞는 작업 | 단건 CRUD, 엔티티 상태 변경 | 동적 검색, 집계, 통계, 리포트 |
| 반환값 | 엔티티와 영속성 컨텍스트 | 조회 DTO나 값 |
| 성능 튜닝 | 생성된 SQL을 확인하며 조정 | 작성한 SQL을 직접 조정 |
// JPA
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
}
public interface UserRepository extends JpaRepository<User, Long> {
}
userRepository.save(user);
userRepository.findById(1L);
// MyBatis
@Mapper
public interface UserMapper {
@Select("SELECT id, name FROM users WHERE id = #{id}")
User findById(Long id);
@Insert("INSERT INTO users(name) VALUES(#{name})")
void save(User user);
}
userMapper.save(user);
userMapper.findById(1L);
Service 입장에서는 둘 다 저장소 역할을 하는 협력자다. 다만 실제 권한 조건은 조회한 뒤 자바에서 거르는 것보다, 쿼리 조건에 포함해서 처음부터 가져오지 않게 하는 편이 안전하다.
Spring AI는 어디에 둘까?
AI를 Controller에 바로 붙이면 처음에는 빨라 보이지만, 프롬프트, 도구, 보안, 메모리, 검색 정책이 섞여 나중에 관리하기 어려워진다.
Controller
→ Service
→ ChatClient
→ Advisor 체인
→ 필요하면 RAG 검색, Tool 호출
- Controller는 질문과 세션 ID를 받고 서비스만 호출한다.
- Service는 업무 규칙에 맞게 프롬프트와 도구 사용 여부를 결정한다.
- Config와 Advisor는 모델 설정, 메모리, 보안, 감사처럼 공통으로 필요한 일을 맡는다.
- RAG는 문서 인제스트, 검색, 근거 조립을 맡는다.
| 패키지 예시 | 책임 | 바뀌는 이유 |
|---|---|---|
config | ChatClient, Advisor, 기본 옵션 조립 | 모델 또는 공급자 변경 |
service | 업무 흐름, 프롬프트 조립 | 업무 규칙 변경 |
rag | 문서 적재, 검색, 근거 구성 | 문서와 검색 품질 변경 |
tools | 모델이 호출할 수 있는 실제 행동 | 연동 시스템 추가 |
advisor | 로깅, 안전, 메모리, 감사 | 보안과 운영 정책 변경 |
web | REST, SSE API | 화면 요구 변경 |
eval | 골든 세트와 품질 기준 | 품질 목표 변경 |
/api/chat 요청 한 번의 흐름
POST /api/chat
→ ChatController: 인증 확인, 질문과 세션 ID 전달
→ AuditAdvisor: 감사 기록 시작
→ SafetyAdvisor: 위험 입력 차단
→ MemoryAdvisor: 같은 세션의 대화 이력 추가
→ RetrievalService: 관련 문서 검색, 근거 추가
→ HelpDeskService: 프롬프트 조립, 모델 호출
→ OrderTools: 모델이 필요할 때만 주문 정보 조회
→ TokenMeter: 토큰과 지연 시간 기록
→ AnswerDto: 답변, 출처, 도구 사용 여부를 반환
여기서 순서가 중요하다. 안전 검사는 메모리 저장이나 외부 도구 호출보다 먼저 실행해야 한다. 그리고 주문 정보의 권한 검증은 프롬프트가 아니라 Repository 쿼리나 Tool의 실제 코드에서 다시 확인해야 한다.
Spring AI의 세 가지 핵심 추상화
Spring AI는 특정 AI 공급자의 SDK에 바로 묶이지 않도록 공통 인터페이스를 제공한다. 중심에는 ChatModel, EmbeddingModel, VectorStore가 있다. 실무에서는 저수준 ChatModel을 직접 쓰기보다 ChatClient로 감싸 사용하는 경우가 많다.
| 추상화 | 하는 일 | 사용 예 |
|---|---|---|
ChatModel | 프롬프트를 받아 대화 응답을 생성 | 챗봇, 요약, 분류 |
EmbeddingModel | 텍스트를 의미 벡터로 변환 | 문서, 질문 임베딩 생성 |
VectorStore | 벡터를 저장하고 유사한 문서를 검색 | RAG의 근거 검색 |
@Service
public class EmbedService {
private final EmbeddingModel embeddingModel;
public EmbedService(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
public float[] embed(String text) {
return embeddingModel.embed(text);
}
}
이 세 가지를 조합하면 챗봇, 문서 검색, RAG, 도구를 사용하는 에이전트까지 확장할 수 있다.
공급자 독립성과 옵션
내 코드는 ChatClient, ChatModel 같은 Spring AI 추상화에 의존하고, 실제 모델 선택은 스타터 의존성과 application.yml에서 한다. 그래서 공통 기능만 쓴다면 개발 환경은 가벼운 모델, 운영 환경은 더 성능 좋은 모델로 비교적 쉽게 바꿀 수 있다.
| 구분 | 공통 옵션 예 | 공급자 고유 옵션 예 |
|---|---|---|
| 모델 선택 | model | 공급자별 모델 식별자 |
| 창의성 | temperature, topP | frequencyPenalty, presencePenalty 등 |
| 길이 제한 | maxTokens 계열 | 공급자별 완료 토큰 옵션 |
| 출력 형식 | 구조화 출력 설정 | JSON Schema 같은 공급자별 세부 설정 |
| 추론 설정 | 공통화되지 않을 수 있음 | reasoning effort, thinking 등 |
공급자 고유 옵션은 필요한 경우에만 쓴다. 그 옵션을 쓰는 코드만큼은 해당 공급자에 의존하게 된다는 점을 알고 경계를 나누면 된다.
핵심만 다시 정리
| 주제 | 기억할 한 문장 |
|---|---|
| 객체 지향 | 객체가 자기 책임을 지고 협력하게 만든다. |
| 계층 구조 | Controller는 HTTP, Service는 업무, Repository와 Mapper는 데이터 접근을 맡는다. |
| DTO | API에 필요한 값만 주고받고, 엔티티는 외부에 그대로 노출하지 않는다. |
| RAG | 문서를 검색해 근거와 함께 모델에게 전달하는 방식이다. |
| Tool | 판단은 모델이 하더라도 실제 실행과 권한 검증은 우리 코드가 한다. |
| Spring AI | ChatModel, EmbeddingModel, VectorStore를 중심으로 AI 기능을 Spring 방식으로 조립한다. |
| 안전 | 차단은 저장과 외부 호출보다 앞에 두고, 권한은 프롬프트가 아니라 코드와 쿼리로 검증한다. |
COMMENTS
GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions에 공개 저장되며, 작성 내용과 GitHub 프로필 정보가 다른 방문자에게 보일 수 있습니다.