다음 글은 인프런의 무료 강의를 요약 정리한 내용입니다

 

 

링크 : https://www.inflearn.com/course/llm-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C%EA%B2%BD%ED%97%98-%EA%B3%B5%EC%9C%A0%ED%9A%8C/dashboard

 

[무료] LLM Application 개발 경험 공유회 - 인프런 | 강의

LLM Application 개발을 위한 기본 배경지식과 개발 경험을 공유합니다., 5월에 진행한 LLM Application 개발 지식 & 경험 공유회를 업로드합니다! LLM Application을 개발하시는 분들에게 조금이라도 도움이

www.inflearn.com

찾아보니 유튜브에도 똑같은 내용으로 올라와 있다

링크 : https://www.youtube.com/live/TJ2mYNpUTAY?si=lQsgGqbAZm7k10TR

 

처음엔 가볍게 듣고자 했는데 

생각보다 너무나도 알차서 어느순간 필기하고 있는 나자신을 발견...

찾다보니 유튜브에도 많은 강의가 있던데..

 

다음엔 LangChain을 다룬 강의도 들어 볼까함,,

 


1. LLM Base Knowledge

 

LLM(Large Language Model)은 현재 NLP분야에서 가장 주목
- 트랜스 포머 기반(Decoder Only)모델이 대세
- 상용으로는 ChatGPT4 , PaLM2
- 오픈 소스가 열심히 추격중 (요샌 falcon?)

 

 

 

사진출처  :  https://velog.io/@dongyoungkim/GPT-fine-tuning-7.-Glossary


Fine-Tuning
- Pre trained Model(like ChatGPT)을 용도에 맞게 튜닝하는 것
- Instruction fine tuning -> RLHF(Reinforcement Learning with Human Feedback) -> Adaptor(P-tuning,LoRA) 방식으로 발전!
- 오픈 소스 LLM 기반 파인 튜닝이 얼마나 퍼포먼스가 잘 나오는지는 아직 모름
- ChatGPT는 gpt-3.5-turbo, gpt-5모두 Fine Tuning API 

In Context Learning
 - 우리 모두 열심히 하고 있는 Prompt Engineering이 바로 이것
 - 별도의 모델 가중치 업데이트를 시키지 않고 명령 프롬프트 내에서 원하는 대답을 얻게 하는 것
 - X-Shot Learning, Chain-of-Thought, Self-Consistency 등의 여러 기법들 존재


2. LLM Application (RAG)


LLM Application 이란?
- 기존 애플리케이션: 코드를 통해 결정적인(Deterministic) Rule Base 기반으로 동작함
- LLM 애플리케이션 : 비결정성이 포함된(지멋대로의) LLM의 추론 결과를 바탕으로 동작함
- LLM은 사람 수준의 추론을 가능하게 해줘서 기존에 가지고 있던 문제를 수비게 풀어 줄 수 있음
(추천,분류,챗봇,AI어시스턴스)

 LLM Application 간단 플로우
1) 클라이언트의 입력이 들어온다
2) 미리 설정한 프롬프트에 질문을 넣어서 LLM에게 요청한다
- 필요한 경우 질문에 필욯나 데이터를 포함시킨다
- 외부 API, 데이터베이스, VectorDB 등
3) 요청 결과를 바탕으로 새로운 프롬프트,함수를 실행한다(Chaining)
4) 최종 결과가 나오면 답변으로 반환한다.

 

사실 현재 대부분은 결국 Private/Public 데이터를 기반으로 대답을 해주는 방식으로 LLM Application 개발중
  - 고객센터 챗봇
  - 법률 판례 검색
  - 주식 리포트 생성


 이는 RAG( Retrieval Augmented Generation) 이라고 하는데, 사용자 Input에 수집된 데이터를 합께 프롬프트에 담아 질문하는방식
->  결국 본질은 1. 질문에 필요한 데이터를 최대한 잘 뽑아서 2. LLM이 잘 대답해 줄 수 있도록 하는 것
 

 


<Data Retrieval>

유저 질문(Input)에 답변할 떄 필요한 정보를 가져오기 위해서 여러가지 방법이 있음

1. 외부 API 활용
 - search api (google search , bing...) -> 비쌈
 - web scraper
 - Saas API (Slack , Notion)
 - Document loading (PDF,CSV, ...)
 보통 Search API를 활용해서 답변의 신뢰도를 높이는 경우가 많음 (하지만 비쌈)
 LangChain, LlamaIndex, Unstructured 같은 오픈소스들에서 많이 지원해줌
 
2. Structured 데이터베이스 활용
 - Data Warehouse(BingQuery, Snotflake, ...), RDBMS
 - 서비스 운영에 필요한
 - 질문을 엔진에 질의할 수 있는 SQL 형태로 변경 필요 (LLM 에게 요청하기)
 
 1) Question to LLM : 1주일 전 테슬라 가격 알려줘
 2) LLM Answer : SELECt ticker, price FROM stock_price WHERE create_at = '2023-06-07 00:00:00'
 3) Action : RDBMS에 쿼리
 
3. 키워드(Keyword) 기반 혹은 전문(Full Text) 검색
 - Elastic Search , Solr , OpenSearch 등
 - 기본적으로 많이 알려진 검색 엔진들 활용하기
 - Saas(Algolia) 형태로 제공해주는 엔진을 사용하는 것도 빠른 시도에 도움이 될듯 
 
4. 유사도 기반 검색 ( Similarity Search) *** 문맥의 유사도 분석
 - 두 벡터의 거리를 기반으로 유사도를 측정함
- 일반적으로 텍스트를 임베딩(Enbedding)시킨 벡터로 변환하여 거리 기반 유사도를 검사
 - 임베딩 모델을 통해 텍스트를 벡터로 임베딩하게 됨
 - 임베딩된 벡터들이 들어간 데이터베이스를 VectorDB라고 함
- VectorDB는 벡터간 유사도 검색을 내부에서 지원해줌
- 사용자는 그냥 API로 손쉽게 검색만 하면 됨

5. VectorDB는 현재 크게 게임 체인저는 없음. 다 비슷비슷함
 - Chroma, FAISS (테스트 환경, Evaluate 등의 로컬 환경에서 돌릴 때 유용)
 - Pinecone, Milvus, Weaviate 같이 요새 핫한 친구들도 나옴 
 - Elastic Search, Redis 처럼 범용 데이터베이스에서 지원해주기도 함
 
 결국 유사도 검색의 핵심은 질문의 벡터와 답변의 벡터가 잘 연결 될 수 있도록 Embedding 하는 방식이 중요
 - Embedding API로 openAI에서 제공하는 Embedding API 많이 사용자
- OpenAI Embedding API의 가격이 75% 저렴해짐
 - Embedding Model도 계속 발전하고 있으니 계속 지켜봅시다
 
6. 우리는 Pinecone 사용
 - 선택한 가장 큰 이유는 직관성 + 쉬운 사용 (Free Trial 제공부터 하면서 넘어옴)
 - Hybrid Search(Metadata Filter) + Upsert도 지원
- 다만 Saas 형태로만 지원하고 Self Hosting이 안되는 건 아쉬움

 


<Data Retrieval Tips>


7. 사실 검색 결과를 높이기 위해선 Semantic Search + Keyword Search 를 함께 적용하는 게 좋음
 - 일반적으로 검색을 통해 다양한 후보군을 가져온 후 ReRanking 하는 작업 진행함 (구글도 그렇다고..)
Cohere가 요새 뜬다는데 쓰는 것도 고려중(API 비용이 막 저렴한 건 아니라서 고민중)
 - 전통적인 검색 엔진 방법(Keyword , full text) + 유사도 검색을 함께 사용해서 정확도를 높이는 것이 좋음
 https://txt/cohere.com/rerank
 
 
8. Vector DB 매칭 확률을 높이기 위해서 여러가지 시도를 해보는 것도 의미가 있음
텍스트 데이터를 어떤 방식으로 넣을건지 고민해보자
  1)어떻게 쪼개서 넣을거니?
   - Chunk Size, Overlap 여러 개로 테스트 해보기
   - Pinpecone은 ChunkSize를 256~512 Token Size 로 추천함(OpenAI Embedding model)
 2) Input을 어떻게 넣을 것인가?
    - 질문이 복잡하다면 쪼개보자! decompose
    - input 을 답변처럼 가공해서 쿼리하는 HYDE 방식도 있음
( 테슬라 얼만지를 답변으로 반환하게 해서 답변을 가지고 유사도)

9.  Embeddubg 방식을 바꿔보는 것도 테스트 해봐도 좋음
 - 실제 벤치마크 점수 기준으로 openAI의 text-embedding-ada-002를 이기는 임베딩 모델들 나옴
 - 하지만 편하게 쓸 수 있는 건 아직까지 OpenAI Embedding이 짱(cohere는 어떤지 궁금)
 https://huggingface.com/spaces/mteb/learderboard
 
10. 다양하게 VectorDB 인덱스를 구성하면서 테스트를 해보는 게 중요
뒷단에서 데이터를 빠르게 넣고 + 멱등하게 관리할 수 있는 Data Engineering infra 필요

<LLM Library>

11. LLM Application을 구성하기 위해서 필요한게 생각보다 있다
 - Prompt 템플릿 + 변수관리
 - 다양한 외부 데이터 접근(API)
 - Vector DB Embedding + Search
 - (이전에 답변에 컨텍스트가 필요하다면) Short Term memory
 - (복잡한 태스크 수행이 필요하다면) Agent
 - ...
 
 현재 LangChain, LlamaIndex, Semantic Kernel 오픈 소스들이 나오고 있음
 
12. LLM Library(LangChain)
 - LangChain이 LLM Application 개발에서 가장 많이 쓰임
 - LLM과 통신하며 수행하는 작업 단위를 Chain으로 만들어서 관리
 - Vector DB 관련 인터페이스가 가장 직관적이고 깔끔
 - Pyhton, Javascript 모두 지원
 - LangChain 생태계 + 커뮤니티가 가장 활발하다
- Awesome-langchain에서 이모저모 볼 수 있음
https://github.com/kyrolabs/awesome-langchain
 - 근데 다큐먼트 솔직히 너무 불칠절하다
Pinecone에서 런북 만든게 설명 잘되어 있음 (https://www.pinecone.io/learn/langchain)
 - Langchain에 뭐가 많기는 하지만 결국 유즈케이스에 맞게 직접 커스터마이징 하게 됨
 - LangChain이 좋은 건 뛰어난 확장성
- 우리는 우리 용도에 맞게 커스터마이징 많이 해서 사용중
- 여러 용도에 맞게 custom chain을 만들어서 사용자
- Conversational Ceontext를 유지하게 위해 Custom Memory(서비스 DB와 통신)를 만들어 구현
- LLM 결과 모니터링을 위해 Custom Callback 구현해서 메타데이터, 지표 모니터링 

13. LLM Library (Llama Index)
 - LangCahin과 유사하게 LLM Application 개발 올인원 툴로 사용 할 수 있음
 - Index라는 단위로 데이터를 구조화 시켜 쿼리를 용이하게 함
- 다양한 Index(VectorStore, Tree, Knowledge Graph 등)을 지원함
- 즉 기존 데이터 소스를 더 효과적으로 찾을 수 있도록 인덱스 방식을 지원해줌
 - 다양한 Query Engine을 지원해서 Index에 질의하는 방식을 다양화 함
- 복잡한 질문을 쪼개서 질문하기
 - 확실히 Langchauin에 비해서 Data Retrieval 하는 과정과 Post Processing을 잘 지원해줌
 - Langchain과 꽤 겹치는 부분이 있기도 하고 확실히 Retrieval 쪽에 강점이 있음
 - 그러나 도입하려다 제외함. Langchain에 비해 러닝커브가 높고 Index management하기 쉽지 않음
 - 여러 기법들을 적용하는 게 과연 얼마나 퍼포먼스 향상에 동무이 될지는 모르겠음 -> 결국 모두 실험해 봐야하는 데 이것 또한 비용
 - Data Retrieval에 대한 퀄리티 고민을 더하게 될 때 도입 다시 해볼듯
 
 
14. 복잡합 유즈케이스
 - 하나의 프롬프트에 많은 역할과 추론 과정을 요구할 때, 원하는 대로 말을 안들을 때가 많음
- 만약 이게 가능한 모델이 있다 하더라도(gpt-3.5는 우선 아님) 좋은 Prompt를 만지는 데 시간을 꽤 투자해야함

 - 미국 주식 Q&A 챗봇 예시
1). 질문 유형에 따른 다양한 요구사항이 있음
   - 시황을 물어볼 경우 시의성 중요함
  - 특정 주식의 Fundamental 정보를 물어볼 경우 해당 정보에 접근하는 API 활용하기
  - 질문이 여러 문맥을 포함하고 있을 수 있음
  - 최근 3개월 간 가장 수익률이 높은 주식의 PER은 몇이야?(질문금지!)

 2) 해결 방법( 두방식을 보통 같이 활용하곤 )
  가. 프롬프트의 역할을 명확하게 해서 쪼갠 후 Chaining하기
  나. Retrospective 하게 추론 & 실행을 반복하는 Agent 활용하기( 반복적으로 물어보기)

 15. Chaining하기
- Contol flows with LLMs
https://huyenchip.com/2023/04/11/llm-engineering.html#part_2_task_composability
- Sequential / Parallel / If / For loop

 - 프롬프트를 Chaining하여, 개별 단계에서 답변의 정확도를 높이기 (우리는 파이프라인이라 부름)
 - 다만, 답변 생성 시간/토큰 비용이 Trade-off + 초반 프롬프트에서 답변이 이상하면 downstream으로 에러가 전파될 수 있음
 - 파이프라인을 잘 구성하기 위해서는 엔지니어링 리소스가 많이 들어감
- 예외처리, 유닛테스트/E2E테스트 모두 진행해봐야 함
 - 우리 팀도 여러 파이프라인을 구성하여 답변 퀄리티를 높이기 위해 E2E테스트를 수시로 진행함
 
 16. Agent (LLM에게 계속 반복적으로 물어봄..)
 - Agent : 목표와 사용가능한 도구를 주면 스스로 행동하도록 하는 방식 혹은 아키텍처 혹은 코드 구현체
 - 외부 리소스(구글 서치, Open API, 기타 API 등)를 활용해서 복잡한 태스크를 수행해야 할 때 유용함
 - AGI(Artificial General Intelligence)를 구현하는 기본적인 방식임 auto gpt-3


 쉽게 설명하면 아래와 같이 동작함 ( recursive 하게 동작함)


  1) LLM에게 미리 정의한 툴(Search, VectorDB , 외부 API등)을 알려주고 질문에 대답하기 위해 툴을 선택하게 한다
  2) 선택한 툴을 코드로 실행해서 결과를 얻는다
  3) LLM에게 결과를 주고 질문에 충분히 대답할 수 있는지 물어본다. 만약 충분히 대답이 되면 결과 반환 후 종료
  4) 대답이 안되면 답변에 필요한 새로운 질문을 생성해 1-3 반복

 

- Auto-GPT, BabyAGI, Jarvis(HuggingGPT)등 다양한 아키텍처이자 구현체가 존재함
- 다만 위 친구들이 얼마나 활용도가 높은지는 모르겠음(구현체인 만큼 자유도가 떨어짐)
 - Agent를 사용하려면 자유도를 높게 가져갈 수 있는 Langchain을 활용하는 걸 추천함
- Langchain으로 autoGPT 구현한 코드도 있음( https://python.langchain.com/en/latest/use_cases/autonomous_agents/autogpt.html )
 - Agnet는 Action의 Full Cycle이 LLM의 추론으로 돌아가는 방식임. 따로 운영시 테스트/디버깅이 힘들 수 있음.
충분히 잘 검토해보고 도입 필요가 있음
 - 개인적으로는 예측 가능한 솔루션을 만들려면, Agent보다는 Prompt Chain + Rule Base(if-else 같은)로 가져가는 게 더 나아보임
 
17. 운영에서 신경 쓸 것
유저 인터페이스를 어떻게 가져가냐에 따라 기능적 요구사항이 달라짐
  1) 챗봇 형태인가?
    - 응답 Latency가 중요 -> Streaming 구현
    - 이전 대화를 바탕으로 대답할 것인가? -> Memory 구현
   2) 사용자가 어떻게 사용할 수 있는가?
    - Quota가 따로 없다면 ChatGPT의 경우 Rate Limit을 신경 써야함
         - 백그라운드에서 Bulk로 돌리는 경우가 많으면 OpenAI Organization 추가하는 게 좋음 API 두개로 들고?
    - 인증이 없다면 외부 공격에 취약할 수 있음(다 돈이다!)
   3) LLM Application 운영시 주요 metric
     - Token Size
     - First touch latency : 얼마나 답을 빠르게 시작했나
     - Last touch latency : 얼마나 답을 빠르게 종료 했냐
 4)후행 지표로 답변 Quality도 체크해야함
    - 답변에 대한 Evaluation을 하는 파이프라인을 뒷단에 구성하는 것도 방법
 5) 답변 퀄리티에 대해서 지속적으로 Evaluation하고 Quality Control 필요
    평가 기준을 세우고 일관된 템플릿으로 답변 결과에 대한 퀄리티 비교 및 제안
 6)Data Ingestion Infra 고민해봐야함
   - 결국 원본 데이터를 보관하기 위한 Stage(Data Warehouse, 운영 DB 등)를 두고 용도에 맞게 Ingest 시켜야함 
   - 워크플로우 툴(e.g Airflow)을 적용하는 게 추후 좋을 수 있음 

18. 느낀점
 - 해당 애플리케이션을 개발한다는 것은 AI Engineering + Data Engineering(+MLOps) + Backend
 - Engineering 이 적절하게 섞여는 느낌
 - 생각보다 답변을 제대로 하는 LLM Application을 만들기까지 노력이 꽤 들어감
- LLM 답변에 대한 퀄리티와 신뢰성을 높이기 위한 작업이 쉽지 않음
- 결국 답변 퀄리티를 높이기 위한 Ops 환경 구성이 생각보다 비용이 들음 (MLOps와 유사)
 - 결국 LLM Application의 품질은 답변과 직접적으로 연결되어 있음. 답변 퀄리티를 높이기 위해선 Data Retrieval이 제일 중요한 것 같음
- 프롬프트를 계속 만지는 것보다 질문에 적합한 Data를 가져오도록 하는 게 더 나을수도
- 더 나은 답변이 나오도록 계속해서 실험해봐야함 이를 위한 실험 환경도 구성되어야함

 




3. LLM OPs

 

 1.LLM을 위한 MLOps 환경
    - LLM : 언어를 출력으로 하는 대규모 딥러닝 모델
    - MLOps : ML기 1. LLM을 위한 MLOps 환경
    - LLM : 언어를 출력으로 하는 대규모 딥러닝 모델
     - MLOps : ML기반 애플리케이션 수명 주기를 관리하는 전체 환경
 - 이미 학습된 LLM이 있다는 전제 하에 환경이 구성되었다는 점에서 MLOps와 다름
     - 실험 측면에서도 MLOps는 모델의 아키텍처, 하이퍼 파라미터, 데이터 보강을 집중
     - LLMOps는 프롬프트, 파인튜닝을 주로 봄 

 


 <LLM Ops 란?>
 2. LLMOps는 무엇을 가능하게 하는가
1) LLM 관련
   - Foundation Model 선택
  - Fine tuning
2)Prompt Engineering
  - 프롬프트 별 버저닝 및 히스토리 관리 
  - 프롬프트 실행
3)Enxternal data Management
  - Test Data Set (보통 Q&A 셋)
  - Vector DB (& Embedding)
4)Evaluation
  - 프롬프트, 파이프라인(Prompt Chained)결과 평가
  - 유저 피드백 루프
  - A/B Testing
5)Deployment
   - Prompt, 파이프라인 배포
  - LLM Metric 모니터링
 3. Tool 소개
- Vellum 
- HoneyHive
- FlowGpt, langflow ,flow eyes : prompt 끼리 chaining

 


 4. 오픈소스/ Saas 보면서 느낀점
1)뭔가 하나를 딱 쓰고 싶지만, 다 애매함. 펀딩받고 있느 ㄴ단계의 서비스들이 많다 보니 아직까지 성숙도가 떨어짐
 - 프롬프트 테스트 후 배포해도, 결국 배포된 api 하나만 사용하는게 아님
   - 보통 프롬프트간 Control Flow가 적용된 파이프라인이 필요함
   - +파이프라인이라는 작업 단위에 대한 Fine grained 설정이 필요함
 - Visual Interface로 프롬프트를 체이닝 하는 경우, 중간에 output을 transform하는 경우들이 존재함
 - External Data Management(VectorDB, Others...)에 대한 환경 제공은 아직 부족함
2)사내 LLMOps 툴 개발
 - OpenAI Playground 에서 계속 테스트 하는건 더이상 힘들다고 판단
 - 결국 인하우스 툴이 필요하다고 느껴져, 최소한의 기능으로 어드민 개발
거시적인 관점에서 하나의 질문도 여러번 해보는 것이 좋다 (매번 다른 답이 나오므로)

 


 5. 느낀점
- 초반에 만들어둔 노력대비 아주 잘 사용하고 있음
아무리 못해도 프롬프트 버저닝, 배치실행 기능은 꼭 필요한듯. 지금이 가장 낫다는 보장이 없음
조금 더 예측 가능하고 정량적으로 평가를 진행하는 게 중요
- 이게 더 나을거 같아요 X
- 테스트에서 이런 경향성을 보이고 있어요. 이어서 ~~~을 설정해서 운영중인 프롬프트와 비교해볼게요 O
다만 하나의 프로덕트를 만드는 느낌이라 비용이 꽤 들어갈 수 있는 걸 고려해야함
- DB 설계부터 버저닝, 배포 환경 격리 등 신경써야 할 게 꽤 많음

 



4.Prompt Engineering

 


 1.
 - 영어로 작성하기 (한글로 쓰고 ChatGPT로 번역해서라도 넣으세요)
 - 간결하게 + 명확하게 작성하기
 - 프롬프트 Instruction 문단에 대한 구분은 $$$ , """" 같은 구분자로 명확하게
 - 예제를 넣자
 - 하지 말라고 하기보단 하라고 하는게 나음
 
 2. X-shot Learning : 예시를 주면서
 단순한 프롬프트일수록 적용해줄 때 효과가 좋음
 zero-shot , one-shot, few-shot : 예제를 넣어라
 
 3. Chain-of-Thought : 생각하는 과정 넣기 추론 하게 하기 
 CoT와 X-Shot은 보통 함께 쓰는 게 좋음
 우리도 기본적으로 CoT + X-Shot 활용함(아예 팀에서 사용할 템플릿 만드는 것 추천)
 
 4.Self-Consistency
 LLM의 비결정적인 문제를 해결하기 위해 여러 번 돌려서 결과 채택하기
 비용 측면에서 비효율적(우린 안씀)
 다만 Data Retrieval을 할때 비슷한 방식을 적용해보는 시도는 가치가 있을지도 
 
 5. Tree-of-Thought
 Tree 형식으로 후보 추론 -> 선택 -> 탐색을 반복하면서 최적의 답을 찾는 방식
 근래 나온 Prompt 기법 중 퍼포먼스가 가장 높다고함
 하지만 꽤 많은 비용(시간+돈)이 Trade-off
 https://github.com/kyegomez/tree-of-thoughts
 


 6. ChatGPT
 결정성 관련
 - 결정성(Determinitsic)을 높여야 하는 경우 Temperature, Top P를 낮게(0이 거의 기본값) 반면 
 창의적인 콘텐츠라면 Temperature나 Top P를 높게 가져가는 게 좋음
 - OpenAI에서는 temperature와 top_p를 함께 만지는 것을 권장하지 않는다고 함
 - 답변이 긴 경우, 반복을 줄이기 위해 Frequency Penalty를 높이는 것 권장
 - 개인적으로, 출력 토큰이 많지 않은 경우에는 Temperature만 조절해도 충분했음
 - ChatGPT 서비스의 gpt-3.5 모델과 gpt-3.5-turbo API는 다름. Gpt-3.5-turbo API 성능이 더 안좋음
 - 따라서 API를 사용하는 경우 ChatGPT 서비스가 아닌 OpenAI Playground로만 테스트 해야함
 - gpt-3.5-turbo가 text-davinch-003(Complete)와 10배 저렴+효율이 비슷하다고 했는데 잘 모르겠음
  text-davinch-003가 거의 대부분 결과를 더 잘 만들어줌
  - gpt-3.5-turbo-0301의 경우 System Message보단 User Message 쓰라고 권장
  (gpt-3.5-turbo에서 system message에 대한 개선이 얼마나 되었는지는 모르겠음)
  
  
  7. Hallucination
  제일 중요한 건 Ground Truth를 잘 쥐어주는 것
  시점에 대해서 제약사항을 주는 것도 좋음
  
 - 답변의 신뢰성이 중요한 유즈케이스에서는 여러 조건으로 프롬프트를 옥죄는 게 나아보임
- ex1. 정말 맞는 지 다시 한번 생각하게 하기
- ex2. 인용을 달게 하기
- 물론 옥죄는 만큼 신뢰성을 높아지지만 답변의 다양성은 낮아질 수 있음
(그럼에도 불구하고 Hallucination이 종종 발생함)
 - PaLM2 같이 최신 정보를 바탕으로 대답이 가능한 모델을 쓰거나 ChatGPT4를 쓰는 것도 좋음
 - 하지만 결국 직접 파인튜닝 하지 않을거라면, 댇바에 필요한 Ground Truth 소스들을 잘넣어 주는 게 퀄리티에 갖아 큰 영향을 끼침
- 결국 데이터를 잘 가져오자!


 8. 프롬프트 평가(Evaluation)
 우리가 만든 프롬프트는 정말 나은걸까?

 

1) 정답이 있다면?
- 정답이 객관식이라면 Equal Check
- 정답과 얼마나 유사한지 ChatGPT에게 물어보기
- 정답과의 유사도(Embedding)를 통한 점수화
- 사람이 측정


2) 정답이 없다면?
- 프롬프트 간 비교 결과를 메트릭으로(자동화)
- 사람이 측정(평가 기준을 명확하게 세우기)


 프롬프트를 평가하면서 느낀건데,
 
프롬프트에 역할, 규칙이 많아질수록 말을 안듣는다
- 즉, 몇개 돌려보고 끝내는게 아니라 충분하 데이터 셋을 여러번 돌려보자
동일한 질문도 여러 번 돌려보는 것도 방법
- 포맷에 대한 확인도 꼭해보기
(이번 OepnAI Update에서 ChatGPT가 Structured format을 지원한다고 함)

9. 프롬프트 관리
 팀에서 프롬프트를 관리하려면, 애초에 기본적인 포맷에 대한 템플릿은 Source of Truth로 잡고 가는 게 낫다
 (가독성도 가독성이지만 프롬프트를 처리하는 코드-레벨에서 편리함)
 
 
10. 느낀점
 - 프롬프트에 정답은 없다. 너무 개별적인 것들에 집중하면 머리아픔(거시적으로 봐야함)
 - 프롬프트 엔지니어링은 어느정도만 잘해놓고 다른일 하는게 나을수도
- LLM의 성능이 계속 더 발전하니, 우리가 고민하는 프롬프트 엔지니어링 이슈들이 금방 해결 될수도 있다.
 - Hallucination의 본질적은 문제는 결국 Ground Truth를 잘 줘야함. 즉, Input에 해당하는 정보들을 잘 가져오는게 
 프롬프트를 튜닝하는 것보다 답변 퀄리티가 더 잘나올 수 있음
 
 

python이란?

  • 병렬 실행 불가능 (https://it-eldorado.tistory.com/160)
    • Python 인터프리터 : Python으로 작성된 코드를 한 줄 씩 읽으면서 실행 하는 프로그램 → CPython
    • GIL ( Global Interpreter Lock) → 한 프로세스 내에서, Python 인터프리터는 한 시점에 하나의 쓰레드에 의해서만 실행 가능 ( Python 의 객체들에 대한 접근을 보호하는 일종의 뮤텍스 Mutex)
      • Mutex : 멀티 쓰레딩 환경에서 여러개의 쓰레더가 어떠한 공유 자원에 접근 가능 할 떄, 그 공유 자원에 접근하기 위해 가지고 있어야 하는 일종의 열쇠
    •  
    • GIL이 필요한 이유? Race Condition을 방지하기 위해 Mutex 필요
      • Python 에서 GC(Garbage Collection)는 Object의 참조횟수가 0이 되면 해당 객체를 메모리에서 삭제 시키는 메커니즘으로 동작
      • 여러개의 Thread 가 Python 인터프리터를 동시에 실행하면 Race Condition(하나의 값에 여러 쓰레드가 동시에 접근해서 값이 올바르지 않게 Read/Write 할 수 있는 상태)이 발생 할 수도 있음
    • → CPU연산이 비중이 적은, 즉 외부 연산(I/O, Sleep 등) 의 비중이 큰 작업을 할 때는 멀티 쓰레딩이 굉장히 좋은 성능!

 

LangFlow

 

 

FastAPI

 

 

 

LangChain

  • LLM 프롬프트의 실행과 외부 소스의 실행(계산기, 구글 검색, 슬랙 메시지 전송이나 소스코드 실행 등)을 엮어 연쇄(Chaining)하는 것
  • https://python.langchain.com/docs/get_started/introduction
  • https://corp.onda.me/post/developing-llm-applications-with-langchain
    • LangChain Library : Python 및 JavaScript 라이브러리. 수많은 구성 요소에 대한 인터페이스 및 통합, 이러한 구성 요소를 체인 및 에이전트로 결합하기 위한 기본 런타임, 체인 및 에이전트의 기성 구현이 포함
    • LangChain Template : 다양한 작업을 위해 쉽게 배포할 수 있는 참조 아키텍처 모음
    • LangServe: LangChain REST API로 배포하기 위한 라이브러리
    • LangSmith: LLM 프레임워크에 구축된 체인을 디버그, 테스트, 평가 및 모니터링하고 LangChain과 원활하게 통합할 수 있는 개발자 플랫폼
  • Pydantic
    • https://seoyeonhwng.medium.com/pydantic-살펴보기-27c67273f0be
    • pydantic은 파이썬 타입 어노테이션을 사용해서 데이터 유효성 검사와 설정 관리를 하는 라이브러리
    • pydantic은 validation이 아닌 parsing 라이브러리이기 때문에 input data를 정의된 타입으로 변환하여 output model의 타입과 제약 조건을 보장

 

 

 

IDE for Python 

 

부모 객체를 상속받아 여러 자식객체들로 이루어진 jsonString을 java backend에서 

VO로 mapping하고 싶은 경우가 있다. (deserialize)

 

 

다음과 같은 구조의 class들이 있다. 

public abstract class Animal{
 private String type;
 private String name;
}
public class Dog extends Animal{
 private int age;
 
 //getter , setter
}
public class Cat extends Animal{
 private boolean isHome;
 
 //getters, setters
}

 

Animal이 부모이고 Dog과 Cat이 자식이다.  Dog과 Cat 모두 Animal을 상속 받고 있다.

 

 

다음과 같은 json String이 있으면 이 code를 사용해서 vo객체에 mapping 해줄 수 있다.

 

public List<Animal> deserializeJsonStringToObj(String str){
 // example :  str = "[{\"name\":\"Milo\",\"age\":\"9\",\"type\":\"Dog\"},{\"name\":\"miyao\",\"isHome\":\"true\",\"type\":\"Cat\"}]";

 Type listOfAnimals = new TypeToken<ArrayList<Animal>>(){}.getType();
 RuntimeTypeAdapterFactory<Animal> adapter = RuntimeTypeAdapterFactory.of(Animal.class, "type")
      .registerSubtype(Dog.class)
      .registerSubtype(Cat.class);
      
Gson gson = new GsonBuilder().registerTypeAdapterFactory(adapter).create();
return gson.fromJson(str, listOfFilterJson);
}

 

 

 

RuntimeTypeAdapterFactory을 사용하는게 쉬운데,

gson에 RuntimeTypeAdapterFactory가 종종 없다고도 한다.

그러면 다음 파일을 불러와서 쓰면된다.

 

 

 

package com.sds.lowcode.data.functions;/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Adapts values whose runtime type may differ from their declaration type. This
 * is necessary when a field's type is not the same type that GSON should create
 * when deserializing that field. For example, consider these types:
 * <pre>   {@code
 *   abstract class Shape {
 *     int x;
 *     int y;
 *   }
 *   class Circle extends Shape {
 *     int radius;
 *   }
 *   class Rectangle extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Diamond extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Drawing {
 *     Shape bottomShape;
 *     Shape topShape;
 *   }
 * }</pre>
 * <p>Without additional type information, the serialized JSON is ambiguous. Is
 * the bottom shape in this drawing a rectangle or a diamond? <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * This class addresses this problem by adding type information to the
 * serialized JSON and honoring that type information when the JSON is
 * deserialized: <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "type": "Diamond",
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "type": "Circle",
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * Both the type field name ({@code "type"}) and the type labels ({@code
 * "Rectangle"}) are configurable.
 *
 * <h3>Registering Types</h3>
 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
 * name to the {@link #of} factory method. If you don't supply an explicit type
 * field name, {@code "type"} will be used. <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
 *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
 * }</pre>
 * Next register all of your subtypes. Every subtype must be explicitly
 * registered. This protects your application from injection attacks. If you
 * don't supply an explicit type label, the type's simple name will be used.
 * <pre>   {@code
 *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
 *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
 *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
 * }</pre>
 * Finally, register the type adapter factory in your application's GSON builder:
 * <pre>   {@code
 *   Gson gson = new GsonBuilder()
 *       .registerTypeAdapterFactory(shapeAdapterFactory)
 *       .create();
 * }</pre>
 * Like {@code GsonBuilder}, this API supports chaining: <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
 *       .registerSubtype(Rectangle.class)
 *       .registerSubtype(Circle.class)
 *       .registerSubtype(Diamond.class);
 * }</pre>
 *
 * <h3>Serialization and deserialization</h3>
 * In order to serialize and deserialize a polymorphic object,
 * you must specify the base type explicitly.
 * <pre>   {@code
 *   Diamond diamond = new Diamond();
 *   String json = gson.toJson(diamond, Shape.class);
 * }</pre>
 * And then:
 * <pre>   {@code
 *   Shape shape = gson.fromJson(json, Shape.class);
 * }</pre>
 */
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
    private final Class<?> baseType;
    private final String typeFieldName;
    private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
    private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();
    private final boolean maintainType;

    private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
        if (typeFieldName == null || baseType == null) {
            throw new NullPointerException();
        }
        this.baseType = baseType;
        this.typeFieldName = typeFieldName;
        this.maintainType = maintainType;
    }

    /**
     * Creates a new runtime type adapter using for {@code baseType} using {@code
     * typeFieldName} as the type field name. Type field names are case sensitive.
     * {@code maintainType} flag decide if the type will be stored in pojo or not.
     */
    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
        return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, maintainType);
    }

    /**
     * Creates a new runtime type adapter using for {@code baseType} using {@code
     * typeFieldName} as the type field name. Type field names are case sensitive.
     */
    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
        return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName, false);
    }

    /**
     * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
     * the type field name.
     */
    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
        return new RuntimeTypeAdapterFactory<T>(baseType, "type", false);
    }

    /**
     * Registers {@code type} identified by {@code label}. Labels are case
     * sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or {@code label}
     *     have already been registered on this type adapter.
     */
    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
        if (type == null || label == null) {
            throw new NullPointerException();
        }
        if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
            throw new IllegalArgumentException("types and labels must be unique");
        }
        labelToSubtype.put(label, type);
        subtypeToLabel.put(type, label);
        return this;
    }

    /**
     * Registers {@code type} identified by its {@link Class#getSimpleName simple
     * name}. Labels are case sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or its simple name
     *     have already been registered on this type adapter.
     */
    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
        return registerSubtype(type, type.getSimpleName());
    }

    public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
        if (type.getRawType() != baseType) {
            return null;
        }

        final Map<String, TypeAdapter<?>> labelToDelegate
            = new LinkedHashMap<String, TypeAdapter<?>>();
        final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
            = new LinkedHashMap<Class<?>, TypeAdapter<?>>();
        for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
            TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
            labelToDelegate.put(entry.getKey(), delegate);
            subtypeToDelegate.put(entry.getValue(), delegate);
        }

        return new TypeAdapter<R>() {
            @Override public R read(JsonReader in) throws IOException {
                JsonElement jsonElement = Streams.parse(in);
                JsonElement labelJsonElement;
                if (maintainType) {
                    labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
                } else {
                    labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
                }

                if (labelJsonElement == null) {
                    throw new JsonParseException("cannot deserialize " + baseType
                        + " because it does not define a field named " + typeFieldName);
                }
                String label = labelJsonElement.getAsString();
                @SuppressWarnings("unchecked") // registration requires that subtype extends T
                    TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
                if (delegate == null) {
                    throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
                        + label + "; did you forget to register a subtype?");
                }
                return delegate.fromJsonTree(jsonElement);
            }

            @Override public void write(JsonWriter out, R value) throws IOException {
                Class<?> srcType = value.getClass();
                String label = subtypeToLabel.get(srcType);
                @SuppressWarnings("unchecked") // registration requires that subtype extends T
                    TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
                if (delegate == null) {
                    throw new JsonParseException("cannot serialize " + srcType.getName()
                        + "; did you forget to register a subtype?");
                }
                JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();

                if (maintainType) {
                    Streams.write(jsonObject, out);
                    return;
                }

                JsonObject clone = new JsonObject();

                if (jsonObject.has(typeFieldName)) {
                    throw new JsonParseException("cannot serialize " + srcType.getName()
                        + " because it already defines a field named " + typeFieldName);
                }
                clone.add(typeFieldName, new JsonPrimitive(label));

                for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
                    clone.add(e.getKey(), e.getValue());
                }
                Streams.write(clone, out);
            }
        }.nullSafe();
    }
}

 

 

 


참고

https://www.baeldung.com/gson-list


Test Double 이란?

실제 객체를 대신해서 테스팅에서 사용하는 모든 방법을 일컬어 호칭한다. 

Java 진영에서는 대표적으로 Mockito가 있습니다.



Mockito의 어노테이션

@Mock
@MockBean
@Spy
@SpyBean
@InjectMocks



1. Java의 test double : Mockito

1. @Mock

Mockito.mock() 코드를 대체

@Mock으로 mock 객체 생성


 

1.2 @InjectMocks

해당 클래스가 필요한 의존성과 맞는 Mock 객체들을 감지하여 , 해당 클래스의 객체가 만들어질때 사용하여

객체를 만들고 해당변수에 객체를 주입하게된다.

1.2 @Spy

- 실제 객체의 스파이를 생성하여 실제 객체의 메소드를 호출 할 수 있게 합니다.

- stub 하면 stub 하는 객체 , 아니면 실제 객체를 호출 합니다. 
- 하나의 객체를 선택적으로 stub 할 수 있도록 하는 기능 

- mockito.spy()도 사용가능

- When Returns 해서 어떤값이 들어갔을때 해당 값이 리턴되도록 미리 선언해둔다

이유는.. 해당 method는 부가적인 기능이라 중점적인 기능을 test 하기위해 미리 선언해두는 것을 stubbing이라고 한다.

- 둘의 가장 큰 차이점은 @Spy 실제 인스턴스를 사용해서 mocking을 하고, @Mock은 실제 인스턴스 없이 가상의 mock 인스턴스를 직접 만들어 사용한다는 것이다. 그래서 @Spy Mockito.when() 이나 BDDMockito.given() 메서드 등으로 메서드의 행위를 지정해 주지 않으면 @Spy 객체를 만들 때 사용한 실제 인스턴스의 메서드를 호출한다.

 

stubbing 예제

// stubbing
when(mockedList.get(0)).thenReturn("ok");
when(mockedList.get(1)).thenThrow(new RuntimeException());

 

- @Spy 는 객체 instance의 초기화를 해주어야한다 

@Spy
List<String> spyList = new ArrayList<String>(); //초기화

@Test
public void whenUsingTheSpyAnnotation_thenObjectIsSpied() {
    spyList.add("one");
    spyList.add("two");

    Mockito.verify(spyList).add("one");
    Mockito.verify(spyList).add("two");

    assertEquals(2, spyList.size());
}



2. SpringBootTest의 Test double


1. @MockBean


@MockBean은 스프링 컨텍스트에 mock객체를 등록하게 되고 스프링 컨텍스트에 의해 @Autowired가 동작할 때 등록된 mock객체를 사용할 수 있도록 동작합니다.

 


- Spring 영역의 어노테이션
- @Mock은 @InjectMocks에 대해서만 해당 클래스안에서 정의된 객체를 찾아서 의존성을 해결합니다.
- @MockBean은 mock 객체를 스프링 컨텍스트에 등록하는 것이기 때문에 @SpringBootTest를 통해서 Autowired에 의존성이 주입되게 됩니다.

- @Autowired라는 강력한 어노테이션으로 컨텍스트에서 알아서 생성된 객체를 주입받아 테스트를 진행할 수 있도록 합니다.

Mock 종류

의존성 주입
@Mock @InjectMocks 
@MockBean @Autowired

 

 


2. @SpyBean

- @MockBean과 마찬가지로 스프링 컨테이너에 Bean으로 등록된 객체에 대해 Spy를 생성

- @SpyBean이 Interface일 경우 구현체가 반드시 Spring Context에 등록되어야 합니다. => 등록되지 않은 상태라면, @MockBean을 사용하는 것이 좋은 방법이 될 수 있습니다.

- @SpyBean은 실제 구현된 객체를 감싸는 프록시 객체 형태이기 때문에 스프링 컨텍스트에 실제 구현체가 등록되어 있어야 합니다.


참고

https://cobbybb.tistory.com/16

https://www.baeldung.com/mockito-spy

https://velog.io/@june0313/Mockito-Mock-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%A3%BC%EC%9E%85%ED%95%98%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EA%B8%B0

 

1. SQLExcpetion

JDBC는 모든 Exception을 SQLException 에 하나에 모두 담아버린다. 대부분의 SQLException은 복구가 불가능하다. DAO 밖에서 SQLException을 다룰 수 있는 가능성은 거의 없다. 따라서 필요도 없는 기계적인 throws 선언이 등장하도록 방치하지 말고 가능한 한 빨리 언체크/런타임 예외로 전환해줘야 한다.

 

2. DataAccessException

런타임 예외이고 DataAccessException 중 가장 루트 class이다.

JdbcTemplate 템플릿과 콜백 안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져준다.

Spring에서 DB 관련 Exception 은 DataAccessException 으로 한번 감싸줘서 알려준다고 보면 된다.

DBMS에 따라서 에러코드가 다를텐데 그래도 코드는 스프링에서 일관적으로 보여주니 일관성이 있어서 좋을 것이다. 

 

  • dbcTemplate은 SQLException을 단지 런타임 예외인 DataAccessException으로 포장하는 것이 아니라 DB의 에러 코드를 DataAccessException 계층구조의 클래스 중 하나로 매핑해준다.JdbcTemplate에서 던지는 예외는 모두 DataAccessException의 서브클래스 타입이다.
  • 드라이버나 DB 메타정보를 참고해서 DB 종류를 확인하고 DB별로 미리 준비된 매핑정보를 참고해서 적절한 예외 클래스를 선택하기 때문에 DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있는 것이다.

 

주의할점은, 

키값 중복이 되는 같은 상황이라도 똑같은 예외 발생하지 않을 수 있다.

  • 데이터 액세스 기술에 상관없이 키 값이 중복이 되는 상황에 아래와 같이 동일한 예외가 발생하지 않는다.
  • JDBC는 DuplicateKeyException, 하이버네이트는 ContraintViolationException을 발생시킬 것이다.

 

3. 결론

결론적으로, 스프링이 잘 정립한 DataAccessException을 활용하는 것이 바람직 하지만, 특정기술에 따라 예상하지 못한 결과가 나올수 있다. 

아직 모든 예외가 명확하게 추상화되어있지 않다. 

이런 경우 로직에서 적절한 DataAccessException의 하위 클래스로 전환하는 방법 등을 사용해서 최대한 일관된 예외 전략을 가져가는 것이 좋을 것 같다.

스프링은 DataAccessException을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외계층을 제공한다.

DAO를 데이터 액세스 기술에서 독립시키려면 인터페이스 도입과 런타임 예외 전환, 기술에 독립적인 추상화된 예외로 전환이 필요하다.

 


참고)

https://gunju-ko.github.io/toby-spring/2018/11/07/%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC.html

https://velog.io/@kyle/%ED%86%A0%EB%B9%84-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC

https://withseungryu.tistory.com/95

https://namocom.tistory.com/913

업무하면서 예외처리에 대해서 고민할 일이 있었다

 

정리해보자

 

보통 예외처리는 공통으로 처리하는게 많다

그 공통을 어떻게 처리할까?

 


 

1. @ExceptionHandler 

- Controller 에서 일어나는 예외를 잡아서 한번에 처리해 줌

- controller, RestController에서 사용 가능

- 리턴타입 , parameter 타입 자유 

- 아래 예시처럼 여러개 Exception class 를 여러개 나열 가능 

 

@RestController
public class TestController{

	@ExceptionHandler(NullPointerException.class)
    public Object nullex(Exception e){
    	System.err.println(e.getClass());
        return;
    }
    
     @ExceptionHandler({JdbcSQLException.class, BadSqlGrammarException.class, MyBatisSystemException.class})
    public ResponseEntity<customedException> handleJdbcSqlException(Exception exception) {
        return customedException(exception);
    }
}

 

 

2. @RestControllerAdvice

- 전역으로 예외처리 관리 해주는 어노테이션이다

@RestControllerAdvice
public class GlovalExceptionTest{
	@ExceptionHandler(NullPointer.class)
    public ResponseEntity<CustomException> handleNullpointerException(Exception e){
    	return new CustomException(e);
	}
}

 

 

- @RestControllerAdvice = @ControllerAdvice + @ResponseBody

=> 즉, @ControllerAdvice 와 같은 역할인데 @ResponseBody가 추가돼서 , 객체도 리턴이 가능한 것이다.

 예외처리 페이지로 리다이렉트만 할것이면 @ControllerAdvice만 써도되고 , 

API 서버라서 객체를 리턴해야 한다면 @ResponseBody 어노테이션이 추가된 @RestControllerAdvice 를 사용하면 된다.

 

보통은 backend와 frontend를 따로 띄워서 하는게 대부분이기 때문에 (MSA..)

@RestControllerAdvice를 쓰면 될거같다 객체로 리턴!!

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
    @AliasFor(
        annotation = ControllerAdvice.class
    )
    String[] value() default {};

    @AliasFor(
        annotation = ControllerAdvice.class
    )
    String[] basePackages() default {};

    @AliasFor(
        annotation = ControllerAdvice.class
    )
    Class<?>[] basePackageClasses() default {};

    @AliasFor(
        annotation = ControllerAdvice.class
    )
    Class<?>[] assignableTypes() default {};

    @AliasFor(
        annotation = ControllerAdvice.class
    )
    Class<? extends Annotation>[] annotations() default {};
}

 

 


참고

https://jeong-pro.tistory.com/195

인강을 듣고

Event라는 개념을 공부했다.

 

어렴풋이 - 

이해는 되는데 이걸 도대체 업무에서 어떻게 쓸 수 있을까?

란 의문점이 생겼다

 

도대체 Async로 하는거랑, 그냥 method 호출하는거랑

다른점이 뭐지? 이점이 뭐지???

 

그래서 서치해보았다~

 


1.  Application Event 란?

 

스프링 ApplicationEventPublisher는 스프링에서 이벤트 프로그래밍에 필요한 인터페이스를 제공한다. ApplicationContext 인터페이스에 이미 상속되어있어서 ApplicationContext의 구현체에서도 접근이 가능하다.

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext applicationContext;  

    @Override
    public void run(ApplicationArguments args) throws Exception {
        applicationContext.publishEvent(new MyEvent(this, 1)); // event에 접근 가능
    }
}

 

 

이벤트 수신 방법? @EventListener를 사용해서 빈의 메소드에 사용 

기본적으로는 synchronized 이다.

 

@Component
public class MyEventHandler {

@EventListener
// 반드시 다음 어노테이션을 써주어야 한다.
public void handle(MyEvent event){
	System.out.println("Event 수신 !!!" + event.getData());
	}
}

 

이벤트 순서를 정하고 싶다면 @Order 을 사용

비동기적으로 실행하고 싶다면 @Async을 사용하면 된다. 

 

 


https://engkimbs.tistory.com/718?category=767795

 

[Spring] ApplicationEventPublisher를 통한 스프링 이벤트 처리(ApplicationEventPublisher, Spring Event Processing)

| 스프링 ApplicationEventPublisher 스프링 ApplicationEventPublisher는 스프링에서 이벤트 프로그래밍에 필요한 인터페이스를 제공한다. ApplicationContext 인터페이스에 이미 상속되어있어서 ApplicationCon..

engkimbs.tistory.com

 

https://supawer0728.github.io/2018/03/24/spring-event/

 

Spring Event + Async + AOP 적용해보기

서론원래 글을 쓰기 위해 준비하던 내용은 Event를 강조하는 것이었는데, 준비를 하다 보니 Async와 AOP를 다 쓰게 되어버렸다. 이번 글에서는 하나의 transaction 안에서 많은 일을 처리하는 소스 코드

supawer0728.github.io

 

https://medium.com/@SlackBeck/spring-framework%EC%9D%98-applicationevent-%ED%99%9C%EC%9A%A9%EA%B8%B0-845fd2d29f32

 

Spring Framework의 ApplicationEvent 활용기

이 글은 필자가 현재 진행하고 있는 프로젝트에서 Event 식별하는 과정과 Spring Framework에서 제공하는 ApplicationEvent로 처리한 사례를 공유한다.

medium.com

 

 

ㅎㅎ

일하면서 이런 이슈가 있었다.

 

Interceptor에서 데이터를 쌓고있는데

preHandle에서 왜 데이터가 두번 쌓이지?

 

라는 이슈가 있었다 

그래서 검색해보니 

 

https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/async-intercept.html

 

Spring MVC - Intercepting Async Requests using AsyncHandlerInterceptor

Spring MVC - Intercepting Async Requests using AsyncHandlerInterceptor [Updated: Feb 13, 2018, Created: Dec 9, 2016]

www.logicbig.com

 

이 링크를 찾았다

세상에 친절할 수가 

 

https://stackoverflow.com/questions/26995395/spring-mvc-interceptorhandler-called-twice-with-deferredresult

 

Spring MVC InterceptorHandler called twice with DeferredResult

When I am using custom HandlerInterceptor and my controller returns DeferredResult, the preHandle method of my custom interceptor called twice on each request. Consider a toy example. My custom

stackoverflow.com

 

 

즉 간단히 정리하자면 Async로 동작하면 Sync로 동작하는것과는  Interceptor가 다르게 동작한다.

ㅇ ㅏ , 물론 AsyncInterceptor를 사용했을때 말이다.

요 순서로 돌아간다고 생각하면 된다.

 

preHandle
afterConcurrentHandlingStarted
preHandle
postHandle
afterCompletion

 

그리고 첫번째 링크에 있는 그림을 보면 더 잘 이해가 간다.

다른 New Thread가 PreHandle 을 통과 하기 때문에 저 순서 인 것이다.

최근에 업무하면서

TaskExecutor과 TaskScheduler를 모두 쓸 일이 있었다

 

그래서 개념에 대해서 깊게 보고싶어서

토비의 스프링을 펼쳐보았다^^7777

역시 이거만한게 없다..

 

정리해보쟛...ㅁ7ㅁ8

 

 


1. TaskExecutor

task실행기.. 그렇담 task는 뭘까?

 

Task : 독립적으로 실행한 가능한 작업

 

스프링은 이러한 task들을 다양하게 실행하도록 추상화하여 TaskExecutor라는 인터페이스를 제공한다.

 

package org.springframework.core.task;

import java.util.concurrent.Executor;

@FunctionalInterface
public interface TaskExecutor extends Executor {
    void execute(Runnable var1);
}

 

java5의 Executor 인터페이스를 상속한다.

 

Runnable타입의 태스크를 받아 실행하는데,

다음 인터페이스는 독립적인 스레드에 의해 실행 되도록 의도된 오브젝트를 만들 때 주로 사용된다.

package java.lang;

@FunctionalInterface
public interface Runnable {
    void run();
}

 

Spring의 TaskExecutor는 java.lang.concurrent 패키지 의 Executor와 똑같은 메소드를 가지고 있다. 

 

package java.util.concurrent;

public interface Executor {
    void execute(Runnable var1);
}

 

그럼에도 스프링에서 다시 만든이유는 다음과 같다.

 

1. 다른 기술의 태스크 실행기에 대한 어댑터를 제공 ( Quartz , CommonJ WorkManager...)

2. 스프링에 최적화된 방식의 태스크 실행기 확장 

 

반드시 비동기 독립적 스레드에서 실행될 필요는 없지만 ,

대부분 비동기로 쓴닷 ㅎ_ㅎ

 

 

2. TaskExecutor 구현체

 

2.1 ThreadPoolExecutor

corePoolSize, maxPoolSize , queueCapacity 속성을 설정할 수 있다.

지정된 크기의 스레드 풀을 이용하며 , 작업 요청은 큐를 통해 관리된다.

가장 대표적인 태스크 실행기다.

 

 

   @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }

 

2.2 SimpleThreadPoolTaskExecutor

Quartz의 SimpleThreadPool을 이용해 만들어진 태스크 실행기이다.

 

2.3 WorkManagerTaskExecutor

CommonJ WorkManager의 태스크 실행기에 대한 어댑터이다.

 

 

2.4 SyncTaskExecutor

별도의 스레드에서 수행되는게 아니라 호출한 스레드 상에서 호출

 

 


3. TaskScheduler 

다음 인터페이스는 주어진 태스크를 조건에 따라 실행하거나 반복하는 작업을 수행

태스크의 실행조건은

1) 특정시간

2) 일정한 간격을 두고 반복

3) Trigger인터페이스를 구현해서 유연한 조건 

 - > cron 서버의 실행시간 설정 포맷을 활용한 cronTrigger가 가장 대표적인 구현 클래스이다. 

 

package org.springframework.scheduling;

import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
import org.springframework.lang.Nullable;

public interface TaskScheduler {
    @Nullable
    ScheduledFuture<?> schedule(Runnable var1, Trigger var2);

    default ScheduledFuture<?> schedule(Runnable task, Instant startTime) {
        return this.schedule(task, Date.from(startTime));
    }

    ScheduledFuture<?> schedule(Runnable var1, Date var2);

    default ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
        return this.scheduleAtFixedRate(task, Date.from(startTime), period.toMillis());
    }

    ScheduledFuture<?> scheduleAtFixedRate(Runnable var1, Date var2, long var3);

    default ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) {
        return this.scheduleAtFixedRate(task, period.toMillis());
    }

    ScheduledFuture<?> scheduleAtFixedRate(Runnable var1, long var2);

    default ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) {
        return this.scheduleWithFixedDelay(task, Date.from(startTime), delay.toMillis());
    }

    ScheduledFuture<?> scheduleWithFixedDelay(Runnable var1, Date var2, long var3);

    default ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay) {
        return this.scheduleWithFixedDelay(task, delay.toMillis());
    }

    ScheduledFuture<?> scheduleWithFixedDelay(Runnable var1, long var2);
}

 

 

4. TaskScheduler 구현체

 

4.1 ThreadPoolTaskScheduler

JDK의 ShcdeuledThreadPoolExecutor 스케쥴러에 대한 어댑터이다.

 

 

    @Bean
    public TaskScheduler scheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("Scheduler-Thread-");
        scheduler.initialize();
        return scheduler;
    }

 

 

4.2 TimerManagerTaskScheduler

 

참고) 유연한 조건을 이용!할때 쓰는 Trigger 인터페이스

 

package org.springframework.scheduling;

import java.util.Date;
import org.springframework.lang.Nullable;

public interface Trigger {
    @Nullable
    Date nextExecutionTime(TriggerContext var1);
}

 

그 Trigger 인터페이스를 구현한 CronTrigger  CronExpression을 지원한다.

package org.springframework.scheduling.support;

import java.util.Date;
import java.util.TimeZone;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;

public class CronTrigger implements Trigger {
    private final CronSequenceGenerator sequenceGenerator;

    public CronTrigger(String expression) {
        this.sequenceGenerator = new CronSequenceGenerator(expression);
    }

    public CronTrigger(String expression, TimeZone timeZone) {
        this.sequenceGenerator = new CronSequenceGenerator(expression, timeZone);
    }

    public String getExpression() {
        return this.sequenceGenerator.getExpression();
    }

    public Date nextExecutionTime(TriggerContext triggerContext) {
        Date date = triggerContext.lastCompletionTime();
        if (date != null) {
            Date scheduled = triggerContext.lastScheduledExecutionTime();
            if (scheduled != null && date.before(scheduled)) {
                date = scheduled;
            }
        } else {
            date = new Date();
        }

        return this.sequenceGenerator.next(date);
    }

    public boolean equals(Object other) {
        return this == other || other instanceof CronTrigger && this.sequenceGenerator.equals(((CronTrigger)other).sequenceGenerator);
    }

    public int hashCode() {
        return this.sequenceGenerator.hashCode();
    }

    public String toString() {
        return this.sequenceGenerator.toString();
    }
}

 

 


5. Annotaion 활용 ( @Scheduled , @Async )

 

5.1 @Scheduled

태스크 역할을 맡은 메소드에 직접 스케줄 정보를 어노테이션을 통해 부여해 수케줄이 적용되게 해준다.

@Scheduled 부여되는 메소드는 파라미터를 가질 수 없으며 반드시 void형 리턴 타입이어야한다.

 

-  fixedDelay : 이전 작업이 끝난 시점부터 일정시간이 지난후에 동작하도록 설정 ,

 이전 작업이 끝난후로 부터 정해진 시간이 지난후 다음작업이 시작된다.

@Schduled (fixedDelay=60000)
public void testFixedDelay(){...}

 

- fixedRate : 밀리초로 설정된 일정한 시간간격으로 메소드 실행 

@Schduled (fixedRate=60000)
public void testFixedRate(){...}

 

- cron : Cron expression 으로 메소드 실행

@Schduled (cron = "0 0 12 1 * ?")
public void testCron(){...}

 

 

5.2 @Async 

TaskExecutor를 코드로 사용하지 않고도 비동기 실행이 가능하게 해주는 어노테이션이다.

( 그런데 설정이 필요하면.. Bean으로 만들어주는게 좋다.. threadpool size 등등..)

 

리턴타입은  void 또는 Future 타입이어야한다.

메소드는 다른 코드에 의해 직접 호출 되므로 파라미터를 가질 수 있다. 

 

더자세한 내용은 이전에 포스팅한 이글을 참조한다.

https://hyeonyeee.tistory.com/55

 

Spring에서 Async 처리 (@Async )

Spring에서 Async처리를 해보겠다..! 블로그에 정리되어 있는게 많았는데 그중에 어떤 방법을 택할까 고민을 했다. 가장 간단한 방법으로 구현하였다. @Async annotaion을 붙여주는 방법이다. 1. @EnableAsyn

hyeonyeee.tistory.com

 

 

 


6. 결론

 

정리하다보니 더 자세히 이해가 되었다.

그리고 이전에 개발한 코드가 더 잘 와닿게 되었다 후후

 

그래서 한마디로 정리하자고 하면

 

TaskExecutor는 task를 주로 비동기적으로 처리할때 쓰고

TaskScheduler는 스케줄링할때 쓴다.

 

이 두개 모두 @Async , @Scheduled 라는 어노테이션으로 대체가 가능한데,

자세한 설정이 필요하면

bean으로 만들어 주는것이좋다~ (@Configuraion...)

 

그럼 정리 끝 - 

이전에 OAuth 2.0에 대해서 정리하는 글을 올렸었다.

그렇다면 Spring 에서 OAuth를 어떻게 구현할까?

 

그게 바로 Spring Security인데 그것에 대해서 알아보고자한다.

 

OAuth와 Spring Security는 여러번 보았는데

볼때마다 헷갈리고 다시 공부하는 느낌이다

 

부디 이번에 조금이나마 더 정리되길 바라며,,, 포스팅 시작한닷..ㅁ7ㅁ8

 


1. OAuth란?

Open Authorization , Open Authentication ( Open 인증 , Open 인가) 로

자신의 애플리케이션 서버의 데이터를 다른 Third Party 에게 공유나 인증을 처리해줄 수 있는 오픈 표준 프로토콜이다

 

즉,,

내 서버의 어플리케이션에 접근 하려면 인증을 거쳐야 하는데, 

이것의 표준이라고 생각하면 된다 ( 인증 & 인가)

 

이 개념이 궁금하면?

https://hyeonyeee.tistory.com/48

 

OAuth 2.0이란?

Oauth 2.0 는 무엇인가,, 알아보겠다 이전엔 보통 open api를 이용할때 토큰을 받아와서 헤더값이 넣어주는 걸 즉, 토큰값을 받아오는걸 구현한 방식이 OAuth 라고 알고있었다 이것도 맞지만 더 자세히

hyeonyeee.tistory.com

다음 포스팅을 참고하자.

 

 

2. Spring에서 OAuth 2.0?

OAuth가 오픈 표준 프로토콜이면 , 

Spring에서 OAuth를 구현하려면? 

Spring Security를 쓰면된닷,,!

 

 

 

3. Spring Security

이제 어느 부분을 구현해보면 되는지 알아보자.

다행히 Spring에서는 많은 소스를 제공해준다

엄청 복잡하게 공부할 수도있지만 우선, 나는 간단하게,, 이해해보겠다.

 

5가지 Grant Type중 

Resource Owner Password Credentials 방식으로 구현해보자.

 

여기서 크게 구현해야 될것은 세가지다.

 

 

1. AuthenticationProvider (Interface)

인증을 담당할 클래스를 구현

UserDetailServiceImpl에서 받아은 유저의 정보를 인증이 완료되면 Authentication 객체를 리턴해준다.

 

 

2. UserDetailService (Interface)

UserDetailService를 implements해서 custom UserDetailService를 만들어주어야한다.

user의 정보를 DB에서 받아온다.

 

3. Authentication Provider 추가하기 

 - SecurityConfig extends WebSecurityConfigurerAdapter

 

1번에서 만든 내가만든 인증 클래스를 ProviderManager에게 등록해주어야 한다.

 

 

 

 

 

 

 

 

 


 

참고)

HTTP Basic Auth 
가장 기본적이고 단순한 형태의 인증 방식으로 사용자 ID와 PASSWD를 HTTP Header에 Base64 인코딩 형태로 넣어서 인증을 요청한다.
예를 들어 사용자 ID가 terry이고 PASSWD가 hello world일 때, 다음과 같이 HTTP 헤더에 “terry:hello world”라는 문자열을 Base64 인코딩을해서 “Authorization”이라는 이름의 헤더로 서버에 전송하여 인증을 요청한다.
Authorization: Basic VGVycnk6aGVsbG8gd29ybGQ=
중간에 패킷을 가로채서 이 헤더를 Base64로 디코딩하면 사용자 ID와 PASSWD가 그대로 노출되기 때문에 반드시 HTTPS 프로토콜을 사용해야 한다.

 

 

en.wikipedia.org/wiki/Basic_access_authentication

 

 

 


https://jeong-pro.tistory.com/205

 

로그인 과정으로 살펴보는 스프링 시큐리티 아키텍처(Spring Security Architecture)

Spring Security Architecture 학습 목표 스프링 시큐리티를 처음 배우는 사람 또는 적어도 한 번은 적용해본 사람을 기준으로 "가장 기본이자 뼈대인 구조를 이해한다"는 학습 목표가 있다. 수 많은 블��

jeong-pro.tistory.com

 

https://minwan1.github.io/2018/03/11/2018-03-11-Spring-OAuth%EA%B5%AC%ED%98%84/

 

Wan Blog

WanBlog | 개발블로그

minwan1.github.io

 

https://spring.io/guides/topicals/spring-security-architecture

 

Spring Security Architecture

this topical is designed to be read and comprehended in under an hour, it provides broad coverage of a topic that is possibly nuanced or requires deeper understanding than you would get from a getting started guide

spring.io

 

+ Recent posts