[{"data":1,"prerenderedAt":2031},["ShallowReactive",2],{"post-document:\u002Fbackend\u002Fspring_ai\u002F2026\u002F08\u002F18\u002F1\u002F":3},{"id":4,"title":5,"body":6,"categories":2017,"date":2020,"description":16,"extension":2021,"image":2022,"key_concepts":2022,"last_modified_at":2022,"legacyPath":2023,"meta":2024,"navigation":141,"part":2022,"path":2026,"published":141,"robots":2022,"seo":2027,"series":2022,"stem":2028,"strengths":2022,"summary":2022,"tags":2029,"tradeoffs":2022,"__hash__":2030},"posts\u002Fposts\u002FBackend\u002FSPRING_AI \u002F1.md","Spring AI를 배우기 전에 정리할 것",{"type":7,"value":8,"toc":1996},"minimark",[9,13,17,20,23,28,36,107,232,234,238,241,357,359,363,368,371,382,495,499,502,510,598,600,604,607,677,679,683,687,694,700,772,776,782,824,838,842,975,981,996,1000,1003,1077,1080,1118,1122,1125,1283,1290,1294,1297,1369,1438,1504,1507,1509,1513,1516,1522,1537,1648,1655,1661,1664,1666,1670,1688,1740,1803,1806,1810,1822,1908,1911,1913,1917,1992],[10,11,5],"h1",{"id":12},"spring-ai를-배우기-전에-정리할-것",[14,15,16],"p",{},"프론트엔드도 알아야 하지만, 백엔드는 조금 더 깊게 파고들고 싶다. 기술을 한 가지만 고르기보다 여러 기술을 경험하되, 결국에는 내가 잘 아는 도메인 하나를 만들어야 한다.",[14,18,19],{},"그 전에 객체 지향, Spring Boot의 계층 구조, 그리고 AI 기능이 어디에 들어가는지를 한 흐름으로 정리해 본다.",[21,22],"hr",{},[24,25,27],"h2",{"id":26},"객체-지향-캡-추-다-상","객체 지향: 캡, 추, 다, 상",[14,29,30,31,35],{},"객체 지향의 핵심은 클래스로 만든 객체가 ",[32,33,34],"strong",{},"자기 역할을 가지고 협력하게 하는 것","이다. 외우기 쉽게 캡, 추, 다, 상이라고 정리할 수 있다.",[37,38,39,55],"table",{},[40,41,42],"thead",{},[43,44,45,49,52],"tr",{},[46,47,48],"th",{},"개념",[46,50,51],{},"뜻",[46,53,54],{},"쉽게 말하면",[56,57,58,70,81,96],"tbody",{},[43,59,60,64,67],{},[61,62,63],"td",{},"캡슐화",[61,65,66],{},"데이터와 접근 방법을 객체 안에 묶고 통제한다",[61,68,69],{},"중요한 값은 아무나 바꾸지 못하게 숨긴다",[43,71,72,75,78],{},[61,73,74],{},"추상화",[61,76,77],{},"복잡한 구현을 감추고 필요한 기능만 보여 준다",[61,79,80],{},"자동차를 운전할 때 엔진 구조까지 알 필요는 없다",[43,82,83,86,89],{},[61,84,85],{},"다형성",[61,87,88],{},"같은 호출이라도 객체에 따라 다르게 동작한다",[61,90,91,95],{},[92,93,94],"code",{},"pay()","를 호출해도 카드, 계좌이체가 각자 다르게 결제한다",[43,97,98,101,104],{},[61,99,100],{},"상속",[61,102,103],{},"기존 클래스의 특징을 물려받아 확장한다",[61,105,106],{},"공통 기능을 부모 클래스에 두고 필요한 부분만 늘린다",[108,109,114],"pre",{"className":110,"code":111,"language":112,"meta":113,"style":113},"language-java shiki shiki-themes github-light github-dark","interface Payment {\n    void pay(int amount);\n}\n\nclass CardPayment implements Payment {\n    @Override\n    public void pay(int amount) {\n        System.out.println(\"카드로 \" + amount + \"원 결제\");\n    }\n}\n\nclass TransferPayment implements Payment {\n    @Override\n    public void pay(int amount) {\n        System.out.println(\"계좌이체로 \" + amount + \"원 결제\");\n    }\n}\n\nPayment payment = new CardPayment();\npayment.pay(10_000); \u002F\u002F 같은 pay(), 다른 동작\n","java","",[92,115,116,124,130,136,143,149,155,161,167,173,178,183,189,194,199,205,210,215,220,226],{"__ignoreMap":113},[117,118,121],"span",{"class":119,"line":120},"line",1,[117,122,123],{},"interface Payment {\n",[117,125,127],{"class":119,"line":126},2,[117,128,129],{},"    void pay(int amount);\n",[117,131,133],{"class":119,"line":132},3,[117,134,135],{},"}\n",[117,137,139],{"class":119,"line":138},4,[117,140,142],{"emptyLinePlaceholder":141},true,"\n",[117,144,146],{"class":119,"line":145},5,[117,147,148],{},"class CardPayment implements Payment {\n",[117,150,152],{"class":119,"line":151},6,[117,153,154],{},"    @Override\n",[117,156,158],{"class":119,"line":157},7,[117,159,160],{},"    public void pay(int amount) {\n",[117,162,164],{"class":119,"line":163},8,[117,165,166],{},"        System.out.println(\"카드로 \" + amount + \"원 결제\");\n",[117,168,170],{"class":119,"line":169},9,[117,171,172],{},"    }\n",[117,174,176],{"class":119,"line":175},10,[117,177,135],{},[117,179,181],{"class":119,"line":180},11,[117,182,142],{"emptyLinePlaceholder":141},[117,184,186],{"class":119,"line":185},12,[117,187,188],{},"class TransferPayment implements Payment {\n",[117,190,192],{"class":119,"line":191},13,[117,193,154],{},[117,195,197],{"class":119,"line":196},14,[117,198,160],{},[117,200,202],{"class":119,"line":201},15,[117,203,204],{},"        System.out.println(\"계좌이체로 \" + amount + \"원 결제\");\n",[117,206,208],{"class":119,"line":207},16,[117,209,172],{},[117,211,213],{"class":119,"line":212},17,[117,214,135],{},[117,216,218],{"class":119,"line":217},18,[117,219,142],{"emptyLinePlaceholder":141},[117,221,223],{"class":119,"line":222},19,[117,224,225],{},"Payment payment = new CardPayment();\n",[117,227,229],{"class":119,"line":228},20,[117,230,231],{},"payment.pay(10_000); \u002F\u002F 같은 pay(), 다른 동작\n",[21,233],{},[24,235,237],{"id":236},"llm-기본-용어","LLM 기본 용어",[14,239,240],{},"AI를 붙이기 전에 모델이 무엇을 기준으로 동작하는지 알아야 한다.",[37,242,243,256],{},[40,244,245],{},[43,246,247,250,253],{},[46,248,249],{},"용어",[46,251,252],{},"한 줄 정의",[46,254,255],{},"알아 둘 점",[56,257,258,269,280,291,302,313,324,335,346],{},[43,259,260,263,266],{},[61,261,262],{},"토큰, token",[61,264,265],{},"모델이 텍스트를 나누어 처리하는 단위",[61,267,268],{},"비용, 입력 길이, 출력 길이의 기준이다. 한글 토큰 수는 문장과 모델마다 달라진다.",[43,270,271,274,277],{},[61,272,273],{},"컨텍스트 윈도우",[61,275,276],{},"요청 한 번에 모델이 읽을 수 있는 총 토큰 범위",[61,278,279],{},"한도를 넘으면 입력을 줄이거나 요약해야 한다.",[43,281,282,285,288],{},[61,283,284],{},"프롬프트",[61,286,287],{},"모델에 보내는 전체 입력",[61,289,290],{},"지시, 맥락, 예시, 원하는 출력 형식을 함께 넣는다.",[43,292,293,296,299],{},[61,294,295],{},"시스템 메시지",[61,297,298],{},"모델의 역할과 규칙을 정하는 상위 지시",[61,300,301],{},"사용자 질문과 분리해서 관리한다.",[43,303,304,307,310],{},[61,305,306],{},"temperature",[61,308,309],{},"답변의 무작위성",[61,311,312],{},"분류, 추출은 낮게, 아이디어 생성은 조금 높게 둔다.",[43,314,315,318,321],{},[61,316,317],{},"top-p",[61,319,320],{},"다음 토큰 후보를 확률 범위로 제한하는 값",[61,322,323],{},"temperature와 함께 무작정 조절하지 않는다.",[43,325,326,329,332],{},[61,327,328],{},"할루시네이션",[61,330,331],{},"모델이 모르는 내용을 그럴듯하게 만드는 현상",[61,333,334],{},"근거를 제공하고, 모르면 모른다고 답하게 해야 줄어든다.",[43,336,337,340,343],{},[61,338,339],{},"파인튜닝",[61,341,342],{},"모델 가중치를 추가 학습하는 일",[61,344,345],{},"최신 지식 주입보다 말투, 형식, 반복 패턴을 맞출 때 더 잘 맞는다.",[43,347,348,351,354],{},[61,349,350],{},"멀티모달",[61,352,353],{},"텍스트 외 이미지, 음성 등을 함께 다루는 기능",[61,355,356],{},"모델이 해당 입력과 출력을 지원해야 한다.",[21,358],{},[24,360,362],{"id":361},"임베딩-벡터-검색-rag","임베딩, 벡터 검색, RAG",[364,365,367],"h3",{"id":366},"임베딩이란","임베딩이란?",[14,369,370],{},"임베딩은 문장을 의미를 담은 숫자 배열, 즉 벡터로 바꾸는 것이다. 뜻이 비슷한 문장은 벡터 공간에서도 가까워진다.",[14,372,373,374,377,378,381],{},"예를 들어 사용자가 ",[92,375,376],{},"휴가 내는 법","이라고 물어도, 문서에 ",[92,379,380],{},"연차 신청 규정","이라고 적혀 있으면 의미가 가까워서 찾을 수 있다. 단어가 정확히 같지 않아도 검색할 수 있다는 점이 핵심이다.",[37,383,384,394],{},[40,385,386],{},[43,387,388,390,392],{},[46,389,249],{},[46,391,252],{},[46,393,255],{},[56,395,396,407,418,429,440,451,462,473,484],{},[43,397,398,401,404],{},[61,399,400],{},"임베딩, embedding",[61,402,403],{},"텍스트를 의미 벡터로 바꾸는 것",[61,405,406],{},"의미가 가까울수록 벡터도 가까워진다.",[43,408,409,412,415],{},[61,410,411],{},"차원, dimension",[61,413,414],{},"벡터를 이루는 숫자의 개수",[61,416,417],{},"모델을 바꾸면 차원이 달라질 수 있어 재색인이 필요하다.",[43,419,420,423,426],{},[61,421,422],{},"코사인 유사도",[61,424,425],{},"두 벡터가 향하는 방향의 유사도",[61,427,428],{},"보통 1에 가까울수록 의미가 비슷하다.",[43,430,431,434,437],{},[61,432,433],{},"벡터 DB",[61,435,436],{},"벡터, 원문, 메타데이터를 저장하고 검색하는 저장소",[61,438,439],{},"pgvector, Chroma, Qdrant 등이 있다.",[43,441,442,445,448],{},[61,443,444],{},"청킹, chunking",[61,446,447],{},"문서를 검색 가능한 작은 조각으로 나누는 일",[61,449,450],{},"너무 크면 잡음이 많고, 너무 작으면 문맥이 끊긴다.",[43,452,453,456,459],{},[61,454,455],{},"overlap",[61,457,458],{},"이웃 청크끼리 일부 문장을 겹치게 하는 것",[61,460,461],{},"문장이 중간에서 잘려 근거가 사라지는 일을 줄인다.",[43,463,464,467,470],{},[61,465,466],{},"top-k",[61,468,469],{},"검색 결과로 가져올 청크 개수",[61,471,472],{},"크게 하면 근거는 늘지만 비용과 잡음도 늘어난다.",[43,474,475,478,481],{},[61,476,477],{},"MMR",[61,479,480],{},"유사도와 결과의 다양성을 같이 보는 방식",[61,482,483],{},"비슷한 문서만 여러 개 나오는 것을 줄인다.",[43,485,486,489,492],{},[61,487,488],{},"인덱스",[61,490,491],{},"벡터를 빠르게 찾기 위한 자료구조",[61,493,494],{},"데이터가 커지면 HNSW, IVF 같은 인덱스를 고려한다.",[364,496,498],{"id":497},"rag란","RAG란?",[14,500,501],{},"RAG, Retrieval-Augmented Generation는 질문과 관련된 문서를 먼저 찾고, 그 근거를 모델에게 함께 주어 답하게 하는 방식이다. 모델을 다시 학습하지 않아도 문서를 교체하거나 추가해서 지식을 갱신할 수 있다.",[108,503,508],{"className":504,"code":506,"language":507,"meta":113},[505],"language-text","문서 파일\n  → 읽기\n  → 청킹\n  → 임베딩\n  → VectorStore에 저장\n\n사용자 질문\n  → 질문 임베딩\n  → 유사한 문서 검색\n  → 검색 결과를 프롬프트에 추가\n  → 모델 답변 + 출처 반환\n","text",[92,509,506],{"__ignoreMap":113},[37,511,512,522],{},[40,513,514],{},[43,515,516,518,520],{},[46,517,249],{},[46,519,252],{},[46,521,255],{},[56,523,524,535,554,565,576,587],{},[43,525,526,529,532],{},[61,527,528],{},"ingest",[61,530,531],{},"문서를 읽고, 자르고, 임베딩해 저장하는 과정",[61,533,534],{},"보통 문서가 바뀔 때 배치 작업으로 실행한다.",[43,536,537,540,543],{},[61,538,539],{},"retriever",[61,541,542],{},"질문에 맞는 청크를 찾아 주는 부분",[61,544,545,546,549,550,553],{},"Spring AI에서는 ",[92,547,548],{},"VectorStore",", ",[92,551,552],{},"Advisor"," 등을 이용해 구성한다.",[43,555,556,559,562],{},[61,557,558],{},"근거, 출처",[61,560,561],{},"답변의 바탕이 된 문서 조각과 위치",[61,563,564],{},"출처를 같이 보여 줘야 답변을 확인할 수 있다.",[43,566,567,570,573],{},[61,568,569],{},"rerank",[61,571,572],{},"검색 후보를 다시 점수 매겨 상위 결과를 고르는 것",[61,574,575],{},"검색 품질을 올릴 때 효과가 큰 단계다.",[43,577,578,581,584],{},[61,579,580],{},"하이브리드 검색",[61,582,583],{},"키워드 검색과 의미 검색을 결합하는 방식",[61,585,586],{},"제품 코드, 고유명사처럼 정확한 단어가 중요한 경우 좋다.",[43,588,589,592,595],{},[61,590,591],{},"metadata filter",[61,593,594],{},"메타데이터 조건으로 검색 범위를 제한하는 것",[61,596,597],{},"권한은 프롬프트가 아니라 필터와 DB 쿼리에서 막아야 한다.",[21,599],{},[24,601,603],{"id":602},"도구와-에이전트","도구와 에이전트",[14,605,606],{},"모델은 직접 DB를 수정하거나 이메일을 보내지 않는다. 모델은 어떤 도구를 호출할지 판단하고, 실제 실행은 우리 코드가 담당한다.",[37,608,609,620],{},[40,610,611],{},[43,612,613,615,617],{},[46,614,48],{},[46,616,51],{},[46,618,619],{},"주의할 점",[56,621,622,633,644,655,666],{},[43,623,624,627,630],{},[61,625,626],{},"Tool calling",[61,628,629],{},"모델이 정해진 함수 호출을 요청하는 기능",[61,631,632],{},"함수 이름, 설명, 파라미터 설명이 모델에게는 API 명세다.",[43,634,635,638,641],{},[61,636,637],{},"Agent",[61,639,640],{},"생각하고 도구를 호출한 뒤 결과를 보고 다시 판단하는 흐름",[61,642,643],{},"호출 횟수와 시간 제한을 두지 않으면 비용이 커질 수 있다.",[43,645,646,649,652],{},[61,647,648],{},"Human approval",[61,650,651],{},"되돌리기 어려운 행동 전에 사람의 확인을 받는 것",[61,653,654],{},"환불, 발송, 삭제 같은 작업에 필요하다.",[43,656,657,660,663],{},[61,658,659],{},"Guardrail",[61,661,662],{},"입력과 출력에 적용하는 안전 규칙",[61,664,665],{},"차단은 메모리 저장이나 외부 호출보다 먼저 해야 한다.",[43,667,668,671,674],{},[61,669,670],{},"Prompt injection",[61,672,673],{},"모델 지시를 무시하게 만들려는 공격",[61,675,676],{},"검색 문서 안에 숨어 들어오는 간접 공격도 확인해야 한다.",[21,678],{},[10,680,682],{"id":681},"spring-boot-기초-계층-구조와-어노테이션","Spring Boot 기초: 계층 구조와 어노테이션",[24,684,686],{"id":685},"왜-계층을-나눌까","왜 계층을 나눌까?",[14,688,689,690,693],{},"계층을 나누는 이유는 문제가 생겼을 때 ",[32,691,692],{},"바꿔야 할 부분만 고치기 위해서","다. 화면 요구가 바뀌면 Controller, 업무 규칙이 바뀌면 Service, 저장 방식이 바뀌면 Repository나 Mapper를 중심으로 수정한다.",[108,695,698],{"className":696,"code":697,"language":507,"meta":113},[505],"HTTP 요청\n  → Controller: 받고, 검증하고, DTO로 응답한다\n  → Service: 업무 흐름과 트랜잭션을 결정한다\n  → Repository 또는 Mapper: DB와 외부 시스템에 접근한다\n  → DB \u002F 외부 API\n",[92,699,697],{"__ignoreMap":113},[37,701,702,715],{},[40,703,704],{},[43,705,706,709,712],{},[46,707,708],{},"계층",[46,710,711],{},"역할",[46,713,714],{},"여기 두지 않는 것",[56,716,717,728,739,750,761],{},[43,718,719,722,725],{},[61,720,721],{},"Controller",[61,723,724],{},"HTTP 요청, 검증, 응답 DTO 변환",[61,726,727],{},"복잡한 업무 규칙, SQL",[43,729,730,733,736],{},[61,731,732],{},"Service",[61,734,735],{},"업무 흐름, 트랜잭션, 여러 저장소 조합",[61,737,738],{},"HTTP 세부 처리, 화면 응답 형식",[43,740,741,744,747],{},[61,742,743],{},"Repository",[61,745,746],{},"JPA로 엔티티를 저장하고 조회",[61,748,749],{},"업무 정책",[43,751,752,755,758],{},[61,753,754],{},"Mapper",[61,756,757],{},"MyBatis SQL을 직접 실행",[61,759,760],{},"Controller 역할",[43,762,763,766,769],{},[61,764,765],{},"DTO",[61,767,768],{},"API에서 주고받는 데이터 모양",[61,770,771],{},"DB 구조 그 자체",[364,773,775],{"id":774},"요청-하나가-지나가는-흐름","요청 하나가 지나가는 흐름",[14,777,778,781],{},[92,779,780],{},"GET \u002Fch02\u002Forders\u002F12345?userId=user1"," 요청을 예로 들면 다음과 같다.",[783,784,785,802,808,818,821],"ol",{},[786,787,788,791,792,549,795,549,798,801],"li",{},[92,789,790],{},"OrderController","가 ",[92,793,794],{},"@PathVariable",[92,796,797],{},"@RequestParam",[92,799,800],{},"@Valid","로 값을 받는다.",[786,803,804,807],{},[92,805,806],{},"OrderService","에 주문 번호와 사용자 ID처럼 필요한 값만 전달한다.",[786,809,810,813,814,817],{},[92,811,812],{},"OrderRepository"," 또는 ",[92,815,816],{},"OrderMapper","가 권한 조건을 포함해 데이터를 조회한다.",[786,819,820],{},"엔티티나 조회 결과를 응답 DTO로 변환한다. 원가나 소유자 ID처럼 외부에 보이면 안 되는 값은 제외한다.",[786,822,823],{},"Controller가 DTO를 JSON으로 반환한다.",[14,825,826,829,830,833,834,837],{},[92,827,828],{},"400","은 주로 입력 검증, ",[92,831,832],{},"404","는 업무상 대상 없음, ",[92,835,836],{},"500","은 DB 연결이나 예상하지 못한 오류에서 많이 발생한다. 하지만 실제 상태 코드는 서비스의 정책과 예외 처리 방식에 따라 정해야 한다.",[24,839,841],{"id":840},"자주-쓰는-어노테이션","자주 쓰는 어노테이션",[37,843,844,856],{},[40,845,846],{},[43,847,848,851,854],{},[46,849,850],{},"분류",[46,852,853],{},"대표 어노테이션, 방식",[46,855,711],{},[56,857,858,880,899,916,935,953],{},[43,859,860,863,877],{},[61,861,862],{},"빈 등록",[61,864,865,549,868,549,871,549,874],{},[92,866,867],{},"@Component",[92,869,870],{},"@Service",[92,872,873],{},"@Repository",[92,875,876],{},"@Controller",[61,878,879],{},"클래스를 Spring이 관리하는 Bean으로 등록한다.",[43,881,882,885,896],{},[61,883,884],{},"요청 매핑",[61,886,887,549,890,549,893],{},[92,888,889],{},"@RestController",[92,891,892],{},"@GetMapping",[92,894,895],{},"@PostMapping",[61,897,898],{},"HTTP 요청을 메서드에 연결한다.",[43,900,901,904,913],{},[61,902,903],{},"의존성 주입",[61,905,906,907,549,910],{},"생성자 주입, ",[92,908,909],{},"@Autowired",[92,911,912],{},"@Qualifier",[61,914,915],{},"필요한 Bean을 찾아 넣는다.",[43,917,918,921,932],{},[61,919,920],{},"설정",[61,922,923,549,926,549,929],{},[92,924,925],{},"@Configuration",[92,927,928],{},"@Bean",[92,930,931],{},"@ConfigurationProperties",[61,933,934],{},"직접 만든 Bean과 외부 설정을 등록한다.",[43,936,937,940,950],{},[61,938,939],{},"검증, 예외",[61,941,942,549,944,549,947],{},[92,943,800],{},[92,945,946],{},"@ExceptionHandler",[92,948,949],{},"@RestControllerAdvice",[61,951,952],{},"입력을 검증하고 예외를 API 응답으로 바꾼다.",[43,954,955,958,972],{},[61,956,957],{},"부가 기능",[61,959,960,549,963,549,966,549,969],{},[92,961,962],{},"@Transactional",[92,964,965],{},"@Async",[92,967,968],{},"@Retryable",[92,970,971],{},"@Aspect",[61,973,974],{},"원래 업무 코드에 공통 기능을 덧붙인다.",[14,976,977,980],{},[92,978,979],{},"@SpringBootApplication","은 설정 클래스, 컴포넌트 스캔, 자동 구성을 묶어 둔 시작점이다. Spring AI 스타터를 추가했을 때 관련 Bean이 자동으로 준비되는 것도 이 자동 구성 흐름에 올라탄다.",[14,982,983,549,985,549,987,989,990,992,993,995],{},[92,984,870],{},[92,986,873],{},[92,988,876],{},"는 모두 ",[92,991,867],{},"의 구체적인 표현이다. 이름만 봐도 역할을 알 수 있게 해 주며, ",[92,994,873],{},"는 DB 접근 예외를 Spring의 예외 체계로 변환하는 의미도 가진다.",[24,997,999],{"id":998},"bean과-의존성-주입","Bean과 의존성 주입",[14,1001,1002],{},"Bean은 Spring이 만들어 관리하는 객체다. 기본 스코프는 singleton이라서 애플리케이션 전체에서 보통 하나만 만든다. 그래서 Service의 필드에 사용자별 값이나 요청별 상태를 저장하면 안 된다.",[108,1004,1006],{"className":110,"code":1005,"language":112,"meta":113,"style":113},"@Service\npublic class OrderService {\n\n    private final OrderRepository repository;\n\n    public OrderService(OrderRepository repository) {\n        this.repository = repository;\n    }\n\n    public OrderResponse findOrder(String orderId, String userId) {\n        return repository.findByIdAndOwnerId(orderId, userId)\n                .map(OrderResponse::from)\n                .orElseThrow(() -> new OrderNotFoundException(orderId));\n    }\n}\n",[92,1007,1008,1013,1018,1022,1027,1031,1036,1041,1045,1049,1054,1059,1064,1069,1073],{"__ignoreMap":113},[117,1009,1010],{"class":119,"line":120},[117,1011,1012],{},"@Service\n",[117,1014,1015],{"class":119,"line":126},[117,1016,1017],{},"public class OrderService {\n",[117,1019,1020],{"class":119,"line":132},[117,1021,142],{"emptyLinePlaceholder":141},[117,1023,1024],{"class":119,"line":138},[117,1025,1026],{},"    private final OrderRepository repository;\n",[117,1028,1029],{"class":119,"line":145},[117,1030,142],{"emptyLinePlaceholder":141},[117,1032,1033],{"class":119,"line":151},[117,1034,1035],{},"    public OrderService(OrderRepository repository) {\n",[117,1037,1038],{"class":119,"line":157},[117,1039,1040],{},"        this.repository = repository;\n",[117,1042,1043],{"class":119,"line":163},[117,1044,172],{},[117,1046,1047],{"class":119,"line":169},[117,1048,142],{"emptyLinePlaceholder":141},[117,1050,1051],{"class":119,"line":175},[117,1052,1053],{},"    public OrderResponse findOrder(String orderId, String userId) {\n",[117,1055,1056],{"class":119,"line":180},[117,1057,1058],{},"        return repository.findByIdAndOwnerId(orderId, userId)\n",[117,1060,1061],{"class":119,"line":185},[117,1062,1063],{},"                .map(OrderResponse::from)\n",[117,1065,1066],{"class":119,"line":191},[117,1067,1068],{},"                .orElseThrow(() -> new OrderNotFoundException(orderId));\n",[117,1070,1071],{"class":119,"line":196},[117,1072,172],{},[117,1074,1075],{"class":119,"line":201},[117,1076,135],{},[14,1078,1079],{},"필드에는 Repository처럼 바뀌지 않는 협력 객체를 두고, 주문 번호나 사용자 ID 같은 요청별 값은 메서드 파라미터로 받는다.",[37,1081,1082,1092],{},[40,1083,1084],{},[43,1085,1086,1089],{},[46,1087,1088],{},"스코프",[46,1090,1091],{},"생성 시점",[56,1093,1094,1102,1110],{},[43,1095,1096,1099],{},[61,1097,1098],{},"singleton",[61,1100,1101],{},"애플리케이션당 보통 하나, 기본값",[43,1103,1104,1107],{},[61,1105,1106],{},"prototype",[61,1108,1109],{},"주입 또는 요청할 때마다 새 객체",[43,1111,1112,1115],{},[61,1113,1114],{},"request",[61,1116,1117],{},"HTTP 요청마다 하나, 웹 환경에서 사용",[24,1119,1121],{"id":1120},"dto와-엔티티는-분리한다","DTO와 엔티티는 분리한다",[14,1123,1124],{},"엔티티는 DB 구조와 연결된 내부 모델이고, DTO는 API에서 주고받는 데이터 모양이다. 엔티티를 그대로 반환하면 DB 구조가 API 규격이 되고, 원가나 소유자 ID 같은 내부 정보가 실수로 노출될 수 있다.",[108,1126,1128],{"className":110,"code":1127,"language":112,"meta":113,"style":113},"public record CreateOrderRequest(\n        @NotBlank String productId,\n        @Min(1) @Max(99) int quantity,\n        @Size(max = 200) String memo\n) {\n}\n\npublic record OrderResponse(\n        String orderId,\n        String item,\n        String status,\n        LocalDate eta\n) {\n    public static OrderResponse from(Order order) {\n        return new OrderResponse(\n                order.getId(),\n                order.getItem().getName(),\n                order.getStatus().name(),\n                order.getEta()\n        );\n    }\n}\n\n@Entity\nclass Order {\n    @Id\n    private String id;\n    private String ownerId;      \u002F\u002F 내부 전용\n    private BigDecimal cost;     \u002F\u002F 외부에 노출하면 안 되는 원가\n}\n",[92,1129,1130,1135,1140,1145,1150,1155,1159,1163,1168,1173,1178,1183,1188,1192,1197,1202,1207,1212,1217,1222,1227,1232,1237,1242,1248,1254,1260,1266,1272,1278],{"__ignoreMap":113},[117,1131,1132],{"class":119,"line":120},[117,1133,1134],{},"public record CreateOrderRequest(\n",[117,1136,1137],{"class":119,"line":126},[117,1138,1139],{},"        @NotBlank String productId,\n",[117,1141,1142],{"class":119,"line":132},[117,1143,1144],{},"        @Min(1) @Max(99) int quantity,\n",[117,1146,1147],{"class":119,"line":138},[117,1148,1149],{},"        @Size(max = 200) String memo\n",[117,1151,1152],{"class":119,"line":145},[117,1153,1154],{},") {\n",[117,1156,1157],{"class":119,"line":151},[117,1158,135],{},[117,1160,1161],{"class":119,"line":157},[117,1162,142],{"emptyLinePlaceholder":141},[117,1164,1165],{"class":119,"line":163},[117,1166,1167],{},"public record OrderResponse(\n",[117,1169,1170],{"class":119,"line":169},[117,1171,1172],{},"        String orderId,\n",[117,1174,1175],{"class":119,"line":175},[117,1176,1177],{},"        String item,\n",[117,1179,1180],{"class":119,"line":180},[117,1181,1182],{},"        String status,\n",[117,1184,1185],{"class":119,"line":185},[117,1186,1187],{},"        LocalDate eta\n",[117,1189,1190],{"class":119,"line":191},[117,1191,1154],{},[117,1193,1194],{"class":119,"line":196},[117,1195,1196],{},"    public static OrderResponse from(Order order) {\n",[117,1198,1199],{"class":119,"line":201},[117,1200,1201],{},"        return new OrderResponse(\n",[117,1203,1204],{"class":119,"line":207},[117,1205,1206],{},"                order.getId(),\n",[117,1208,1209],{"class":119,"line":212},[117,1210,1211],{},"                order.getItem().getName(),\n",[117,1213,1214],{"class":119,"line":217},[117,1215,1216],{},"                order.getStatus().name(),\n",[117,1218,1219],{"class":119,"line":222},[117,1220,1221],{},"                order.getEta()\n",[117,1223,1224],{"class":119,"line":228},[117,1225,1226],{},"        );\n",[117,1228,1230],{"class":119,"line":1229},21,[117,1231,172],{},[117,1233,1235],{"class":119,"line":1234},22,[117,1236,135],{},[117,1238,1240],{"class":119,"line":1239},23,[117,1241,142],{"emptyLinePlaceholder":141},[117,1243,1245],{"class":119,"line":1244},24,[117,1246,1247],{},"@Entity\n",[117,1249,1251],{"class":119,"line":1250},25,[117,1252,1253],{},"class Order {\n",[117,1255,1257],{"class":119,"line":1256},26,[117,1258,1259],{},"    @Id\n",[117,1261,1263],{"class":119,"line":1262},27,[117,1264,1265],{},"    private String id;\n",[117,1267,1269],{"class":119,"line":1268},28,[117,1270,1271],{},"    private String ownerId;      \u002F\u002F 내부 전용\n",[117,1273,1275],{"class":119,"line":1274},29,[117,1276,1277],{},"    private BigDecimal cost;     \u002F\u002F 외부에 노출하면 안 되는 원가\n",[117,1279,1281],{"class":119,"line":1280},30,[117,1282,135],{},[14,1284,1285,1286,1289],{},"DTO 변환 위치는 팀 규칙에 따라 다를 수 있지만, 중요한 것은 한 방향으로 일관되게 두는 것이다. 위 예시는 응답 DTO의 ",[92,1287,1288],{},"from()"," 메서드에 변환을 모았다.",[24,1291,1293],{"id":1292},"jpa-repository와-mybatis-mapper","JPA Repository와 MyBatis Mapper",[14,1295,1296],{},"둘 다 데이터를 다루지만 기준은 다르다. 객체와 엔티티의 상태 변경이 중심이면 JPA가 편하고, 복잡한 SQL을 직접 제어해야 하면 MyBatis Mapper가 편하다. 한 프로젝트에서 함께 써도 된다.",[37,1298,1299,1312],{},[40,1300,1301],{},[43,1302,1303,1306,1309],{},[46,1304,1305],{},"구분",[46,1307,1308],{},"JPA Repository",[46,1310,1311],{},"MyBatis Mapper",[56,1313,1314,1325,1336,1347,1358],{},[43,1315,1316,1319,1322],{},[61,1317,1318],{},"중심",[61,1320,1321],{},"객체, 엔티티, ORM",[61,1323,1324],{},"SQL",[43,1326,1327,1330,1333],{},[61,1328,1329],{},"SQL 작성",[61,1331,1332],{},"기본 CRUD는 JPA가 생성",[61,1334,1335],{},"개발자가 직접 작성",[43,1337,1338,1341,1344],{},[61,1339,1340],{},"잘 맞는 작업",[61,1342,1343],{},"단건 CRUD, 엔티티 상태 변경",[61,1345,1346],{},"동적 검색, 집계, 통계, 리포트",[43,1348,1349,1352,1355],{},[61,1350,1351],{},"반환값",[61,1353,1354],{},"엔티티와 영속성 컨텍스트",[61,1356,1357],{},"조회 DTO나 값",[43,1359,1360,1363,1366],{},[61,1361,1362],{},"성능 튜닝",[61,1364,1365],{},"생성된 SQL을 확인하며 조정",[61,1367,1368],{},"작성한 SQL을 직접 조정",[108,1370,1372],{"className":110,"code":1371,"language":112,"meta":113,"style":113},"\u002F\u002F JPA\n@Entity\npublic class User {\n    @Id\n    @GeneratedValue\n    private Long id;\n    private String name;\n}\n\npublic interface UserRepository extends JpaRepository\u003CUser, Long> {\n}\n\nuserRepository.save(user);\nuserRepository.findById(1L);\n",[92,1373,1374,1379,1383,1388,1392,1397,1402,1407,1411,1415,1420,1424,1428,1433],{"__ignoreMap":113},[117,1375,1376],{"class":119,"line":120},[117,1377,1378],{},"\u002F\u002F JPA\n",[117,1380,1381],{"class":119,"line":126},[117,1382,1247],{},[117,1384,1385],{"class":119,"line":132},[117,1386,1387],{},"public class User {\n",[117,1389,1390],{"class":119,"line":138},[117,1391,1259],{},[117,1393,1394],{"class":119,"line":145},[117,1395,1396],{},"    @GeneratedValue\n",[117,1398,1399],{"class":119,"line":151},[117,1400,1401],{},"    private Long id;\n",[117,1403,1404],{"class":119,"line":157},[117,1405,1406],{},"    private String name;\n",[117,1408,1409],{"class":119,"line":163},[117,1410,135],{},[117,1412,1413],{"class":119,"line":169},[117,1414,142],{"emptyLinePlaceholder":141},[117,1416,1417],{"class":119,"line":175},[117,1418,1419],{},"public interface UserRepository extends JpaRepository\u003CUser, Long> {\n",[117,1421,1422],{"class":119,"line":180},[117,1423,135],{},[117,1425,1426],{"class":119,"line":185},[117,1427,142],{"emptyLinePlaceholder":141},[117,1429,1430],{"class":119,"line":191},[117,1431,1432],{},"userRepository.save(user);\n",[117,1434,1435],{"class":119,"line":196},[117,1436,1437],{},"userRepository.findById(1L);\n",[108,1439,1441],{"className":110,"code":1440,"language":112,"meta":113,"style":113},"\u002F\u002F MyBatis\n@Mapper\npublic interface UserMapper {\n\n    @Select(\"SELECT id, name FROM users WHERE id = #{id}\")\n    User findById(Long id);\n\n    @Insert(\"INSERT INTO users(name) VALUES(#{name})\")\n    void save(User user);\n}\n\nuserMapper.save(user);\nuserMapper.findById(1L);\n",[92,1442,1443,1448,1453,1458,1462,1467,1472,1476,1481,1486,1490,1494,1499],{"__ignoreMap":113},[117,1444,1445],{"class":119,"line":120},[117,1446,1447],{},"\u002F\u002F MyBatis\n",[117,1449,1450],{"class":119,"line":126},[117,1451,1452],{},"@Mapper\n",[117,1454,1455],{"class":119,"line":132},[117,1456,1457],{},"public interface UserMapper {\n",[117,1459,1460],{"class":119,"line":138},[117,1461,142],{"emptyLinePlaceholder":141},[117,1463,1464],{"class":119,"line":145},[117,1465,1466],{},"    @Select(\"SELECT id, name FROM users WHERE id = #{id}\")\n",[117,1468,1469],{"class":119,"line":151},[117,1470,1471],{},"    User findById(Long id);\n",[117,1473,1474],{"class":119,"line":157},[117,1475,142],{"emptyLinePlaceholder":141},[117,1477,1478],{"class":119,"line":163},[117,1479,1480],{},"    @Insert(\"INSERT INTO users(name) VALUES(#{name})\")\n",[117,1482,1483],{"class":119,"line":169},[117,1484,1485],{},"    void save(User user);\n",[117,1487,1488],{"class":119,"line":175},[117,1489,135],{},[117,1491,1492],{"class":119,"line":180},[117,1493,142],{"emptyLinePlaceholder":141},[117,1495,1496],{"class":119,"line":185},[117,1497,1498],{},"userMapper.save(user);\n",[117,1500,1501],{"class":119,"line":191},[117,1502,1503],{},"userMapper.findById(1L);\n",[14,1505,1506],{},"Service 입장에서는 둘 다 저장소 역할을 하는 협력자다. 다만 실제 권한 조건은 조회한 뒤 자바에서 거르는 것보다, 쿼리 조건에 포함해서 처음부터 가져오지 않게 하는 편이 안전하다.",[21,1508],{},[10,1510,1512],{"id":1511},"spring-ai는-어디에-둘까","Spring AI는 어디에 둘까?",[14,1514,1515],{},"AI를 Controller에 바로 붙이면 처음에는 빨라 보이지만, 프롬프트, 도구, 보안, 메모리, 검색 정책이 섞여 나중에 관리하기 어려워진다.",[108,1517,1520],{"className":1518,"code":1519,"language":507,"meta":113},[505],"Controller\n  → Service\n    → ChatClient\n      → Advisor 체인\n        → 필요하면 RAG 검색, Tool 호출\n",[92,1521,1519],{"__ignoreMap":113},[1523,1524,1525,1528,1531,1534],"ul",{},[786,1526,1527],{},"Controller는 질문과 세션 ID를 받고 서비스만 호출한다.",[786,1529,1530],{},"Service는 업무 규칙에 맞게 프롬프트와 도구 사용 여부를 결정한다.",[786,1532,1533],{},"Config와 Advisor는 모델 설정, 메모리, 보안, 감사처럼 공통으로 필요한 일을 맡는다.",[786,1535,1536],{},"RAG는 문서 인제스트, 검색, 근거 조립을 맡는다.",[37,1538,1539,1552],{},[40,1540,1541],{},[43,1542,1543,1546,1549],{},[46,1544,1545],{},"패키지 예시",[46,1547,1548],{},"책임",[46,1550,1551],{},"바뀌는 이유",[56,1553,1554,1570,1583,1596,1609,1622,1635],{},[43,1555,1556,1561,1567],{},[61,1557,1558],{},[92,1559,1560],{},"config",[61,1562,1563,1566],{},[92,1564,1565],{},"ChatClient",", Advisor, 기본 옵션 조립",[61,1568,1569],{},"모델 또는 공급자 변경",[43,1571,1572,1577,1580],{},[61,1573,1574],{},[92,1575,1576],{},"service",[61,1578,1579],{},"업무 흐름, 프롬프트 조립",[61,1581,1582],{},"업무 규칙 변경",[43,1584,1585,1590,1593],{},[61,1586,1587],{},[92,1588,1589],{},"rag",[61,1591,1592],{},"문서 적재, 검색, 근거 구성",[61,1594,1595],{},"문서와 검색 품질 변경",[43,1597,1598,1603,1606],{},[61,1599,1600],{},[92,1601,1602],{},"tools",[61,1604,1605],{},"모델이 호출할 수 있는 실제 행동",[61,1607,1608],{},"연동 시스템 추가",[43,1610,1611,1616,1619],{},[61,1612,1613],{},[92,1614,1615],{},"advisor",[61,1617,1618],{},"로깅, 안전, 메모리, 감사",[61,1620,1621],{},"보안과 운영 정책 변경",[43,1623,1624,1629,1632],{},[61,1625,1626],{},[92,1627,1628],{},"web",[61,1630,1631],{},"REST, SSE API",[61,1633,1634],{},"화면 요구 변경",[43,1636,1637,1642,1645],{},[61,1638,1639],{},[92,1640,1641],{},"eval",[61,1643,1644],{},"골든 세트와 품질 기준",[61,1646,1647],{},"품질 목표 변경",[24,1649,1651,1654],{"id":1650},"apichat-요청-한-번의-흐름",[92,1652,1653],{},"\u002Fapi\u002Fchat"," 요청 한 번의 흐름",[108,1656,1659],{"className":1657,"code":1658,"language":507,"meta":113},[505],"POST \u002Fapi\u002Fchat\n  → ChatController: 인증 확인, 질문과 세션 ID 전달\n  → AuditAdvisor: 감사 기록 시작\n  → SafetyAdvisor: 위험 입력 차단\n  → MemoryAdvisor: 같은 세션의 대화 이력 추가\n  → RetrievalService: 관련 문서 검색, 근거 추가\n  → HelpDeskService: 프롬프트 조립, 모델 호출\n  → OrderTools: 모델이 필요할 때만 주문 정보 조회\n  → TokenMeter: 토큰과 지연 시간 기록\n  → AnswerDto: 답변, 출처, 도구 사용 여부를 반환\n",[92,1660,1658],{"__ignoreMap":113},[14,1662,1663],{},"여기서 순서가 중요하다. 안전 검사는 메모리 저장이나 외부 도구 호출보다 먼저 실행해야 한다. 그리고 주문 정보의 권한 검증은 프롬프트가 아니라 Repository 쿼리나 Tool의 실제 코드에서 다시 확인해야 한다.",[21,1665],{},[24,1667,1669],{"id":1668},"spring-ai의-세-가지-핵심-추상화","Spring AI의 세 가지 핵심 추상화",[14,1671,1672,1673,549,1676,549,1679,1681,1682,1684,1685,1687],{},"Spring AI는 특정 AI 공급자의 SDK에 바로 묶이지 않도록 공통 인터페이스를 제공한다. 중심에는 ",[92,1674,1675],{},"ChatModel",[92,1677,1678],{},"EmbeddingModel",[92,1680,548],{},"가 있다. 실무에서는 저수준 ",[92,1683,1675],{},"을 직접 쓰기보다 ",[92,1686,1565],{},"로 감싸 사용하는 경우가 많다.",[37,1689,1690,1702],{},[40,1691,1692],{},[43,1693,1694,1696,1699],{},[46,1695,74],{},[46,1697,1698],{},"하는 일",[46,1700,1701],{},"사용 예",[56,1703,1704,1716,1728],{},[43,1705,1706,1710,1713],{},[61,1707,1708],{},[92,1709,1675],{},[61,1711,1712],{},"프롬프트를 받아 대화 응답을 생성",[61,1714,1715],{},"챗봇, 요약, 분류",[43,1717,1718,1722,1725],{},[61,1719,1720],{},[92,1721,1678],{},[61,1723,1724],{},"텍스트를 의미 벡터로 변환",[61,1726,1727],{},"문서, 질문 임베딩 생성",[43,1729,1730,1734,1737],{},[61,1731,1732],{},[92,1733,548],{},[61,1735,1736],{},"벡터를 저장하고 유사한 문서를 검색",[61,1738,1739],{},"RAG의 근거 검색",[108,1741,1743],{"className":110,"code":1742,"language":112,"meta":113,"style":113},"@Service\npublic class EmbedService {\n\n    private final EmbeddingModel embeddingModel;\n\n    public EmbedService(EmbeddingModel embeddingModel) {\n        this.embeddingModel = embeddingModel;\n    }\n\n    public float[] embed(String text) {\n        return embeddingModel.embed(text);\n    }\n}\n",[92,1744,1745,1749,1754,1758,1763,1767,1772,1777,1781,1785,1790,1795,1799],{"__ignoreMap":113},[117,1746,1747],{"class":119,"line":120},[117,1748,1012],{},[117,1750,1751],{"class":119,"line":126},[117,1752,1753],{},"public class EmbedService {\n",[117,1755,1756],{"class":119,"line":132},[117,1757,142],{"emptyLinePlaceholder":141},[117,1759,1760],{"class":119,"line":138},[117,1761,1762],{},"    private final EmbeddingModel embeddingModel;\n",[117,1764,1765],{"class":119,"line":145},[117,1766,142],{"emptyLinePlaceholder":141},[117,1768,1769],{"class":119,"line":151},[117,1770,1771],{},"    public EmbedService(EmbeddingModel embeddingModel) {\n",[117,1773,1774],{"class":119,"line":157},[117,1775,1776],{},"        this.embeddingModel = embeddingModel;\n",[117,1778,1779],{"class":119,"line":163},[117,1780,172],{},[117,1782,1783],{"class":119,"line":169},[117,1784,142],{"emptyLinePlaceholder":141},[117,1786,1787],{"class":119,"line":175},[117,1788,1789],{},"    public float[] embed(String text) {\n",[117,1791,1792],{"class":119,"line":180},[117,1793,1794],{},"        return embeddingModel.embed(text);\n",[117,1796,1797],{"class":119,"line":185},[117,1798,172],{},[117,1800,1801],{"class":119,"line":191},[117,1802,135],{},[14,1804,1805],{},"이 세 가지를 조합하면 챗봇, 문서 검색, RAG, 도구를 사용하는 에이전트까지 확장할 수 있다.",[24,1807,1809],{"id":1808},"공급자-독립성과-옵션","공급자 독립성과 옵션",[14,1811,1812,1813,549,1815,1817,1818,1821],{},"내 코드는 ",[92,1814,1565],{},[92,1816,1675],{}," 같은 Spring AI 추상화에 의존하고, 실제 모델 선택은 스타터 의존성과 ",[92,1819,1820],{},"application.yml","에서 한다. 그래서 공통 기능만 쓴다면 개발 환경은 가벼운 모델, 운영 환경은 더 성능 좋은 모델로 비교적 쉽게 바꿀 수 있다.",[37,1823,1824,1836],{},[40,1825,1826],{},[43,1827,1828,1830,1833],{},[46,1829,1305],{},[46,1831,1832],{},"공통 옵션 예",[46,1834,1835],{},"공급자 고유 옵션 예",[56,1837,1838,1851,1872,1886,1897],{},[43,1839,1840,1843,1848],{},[61,1841,1842],{},"모델 선택",[61,1844,1845],{},[92,1846,1847],{},"model",[61,1849,1850],{},"공급자별 모델 식별자",[43,1852,1853,1856,1863],{},[61,1854,1855],{},"창의성",[61,1857,1858,549,1860],{},[92,1859,306],{},[92,1861,1862],{},"topP",[61,1864,1865,549,1868,1871],{},[92,1866,1867],{},"frequencyPenalty",[92,1869,1870],{},"presencePenalty"," 등",[43,1873,1874,1877,1883],{},[61,1875,1876],{},"길이 제한",[61,1878,1879,1882],{},[92,1880,1881],{},"maxTokens"," 계열",[61,1884,1885],{},"공급자별 완료 토큰 옵션",[43,1887,1888,1891,1894],{},[61,1889,1890],{},"출력 형식",[61,1892,1893],{},"구조화 출력 설정",[61,1895,1896],{},"JSON Schema 같은 공급자별 세부 설정",[43,1898,1899,1902,1905],{},[61,1900,1901],{},"추론 설정",[61,1903,1904],{},"공통화되지 않을 수 있음",[61,1906,1907],{},"reasoning effort, thinking 등",[14,1909,1910],{},"공급자 고유 옵션은 필요한 경우에만 쓴다. 그 옵션을 쓰는 코드만큼은 해당 공급자에 의존하게 된다는 점을 알고 경계를 나누면 된다.",[21,1912],{},[24,1914,1916],{"id":1915},"핵심만-다시-정리","핵심만 다시 정리",[37,1918,1919,1929],{},[40,1920,1921],{},[43,1922,1923,1926],{},[46,1924,1925],{},"주제",[46,1927,1928],{},"기억할 한 문장",[56,1930,1931,1939,1947,1954,1962,1970,1984],{},[43,1932,1933,1936],{},[61,1934,1935],{},"객체 지향",[61,1937,1938],{},"객체가 자기 책임을 지고 협력하게 만든다.",[43,1940,1941,1944],{},[61,1942,1943],{},"계층 구조",[61,1945,1946],{},"Controller는 HTTP, Service는 업무, Repository와 Mapper는 데이터 접근을 맡는다.",[43,1948,1949,1951],{},[61,1950,765],{},[61,1952,1953],{},"API에 필요한 값만 주고받고, 엔티티는 외부에 그대로 노출하지 않는다.",[43,1955,1956,1959],{},[61,1957,1958],{},"RAG",[61,1960,1961],{},"문서를 검색해 근거와 함께 모델에게 전달하는 방식이다.",[43,1963,1964,1967],{},[61,1965,1966],{},"Tool",[61,1968,1969],{},"판단은 모델이 하더라도 실제 실행과 권한 검증은 우리 코드가 한다.",[43,1971,1972,1975],{},[61,1973,1974],{},"Spring AI",[61,1976,1977,549,1979,549,1981,1983],{},[92,1978,1675],{},[92,1980,1678],{},[92,1982,548],{},"를 중심으로 AI 기능을 Spring 방식으로 조립한다.",[43,1985,1986,1989],{},[61,1987,1988],{},"안전",[61,1990,1991],{},"차단은 저장과 외부 호출보다 앞에 두고, 권한은 프롬프트가 아니라 코드와 쿼리로 검증한다.",[1993,1994,1995],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":113,"searchDepth":126,"depth":126,"links":1997},[1998,1999,2000,2004,2005,2008,2009,2010,2011,2012,2014,2015,2016],{"id":26,"depth":126,"text":27},{"id":236,"depth":126,"text":237},{"id":361,"depth":126,"text":362,"children":2001},[2002,2003],{"id":366,"depth":132,"text":367},{"id":497,"depth":132,"text":498},{"id":602,"depth":126,"text":603},{"id":685,"depth":126,"text":686,"children":2006},[2007],{"id":774,"depth":132,"text":775},{"id":840,"depth":126,"text":841},{"id":998,"depth":126,"text":999},{"id":1120,"depth":126,"text":1121},{"id":1292,"depth":126,"text":1293},{"id":1650,"depth":126,"text":2013},"\u002Fapi\u002Fchat 요청 한 번의 흐름",{"id":1668,"depth":126,"text":1669},{"id":1808,"depth":126,"text":1809},{"id":1915,"depth":126,"text":1916},[2018,2019],"Backend","SPRING_AI","2026-08-18 19:28:21 +0900","md",null,"\u002Fbackend\u002Fspring_ai\u002F2026\u002F08\u002F18\u002F1\u002F",{"layout":2025},"post","\u002Fposts\u002Fbackend\u002Fspring_ai\u002F1",{"title":5,"description":16},"posts\u002FBackend\u002FSPRING_AI \u002F1",[],"Mu_Ae8JMnGx_9eGo0eu6C_s1M7_A4iBlRddxQu6bMc4",1788744785569]