Click To Message My WhatsApp 24/7 https://wa.link/st5uhb Click To Visit My Website https://www.delhifungirl.com/ Calling Number 7703891840 Call girls in Sector 17 Rohini Delhi WhatsApp Number +91-7703891840 offers the best Delhi Escorts Service by young and pretty Independent Call Girls in Delhi. Call Girls in Delhi IN Call facility Rs 5000 with A/c room by web series and Up coming model for all 5-Star hotels service free home delivery 30 minutes. High profile Call Girls in Delhi location. Hire Independent Delhi call girls for late-night fun, hotel appointments and exclusive parties. Enjoy a reliable escort service available 24/7 near hotels. In-Call: – You Can Reach At Our Place in Delhi Our place Which Is Very Clean Hygienic 100% safe Accommodation. Out-Call: – Service For Out Call You have To Come Pick The Girl From My Place We Also Provide Door Step Services Note: – Pic Collectors Time Passers Bargainers Stay Away As We Respect The Value For Your Money Time And Expect The Same From You Hygienic: – Full Ac Neat And Clean Rooms Available In Hotel 24 * 7 Hrs In Delhi Ncr We Are Providing
오늘의 칼럼 링크텍스트 출처 : 내 안의 가능성을 끌어내는 메타인지의 비밀 | 리사 손 컬럼비아대학교 바너드칼리지 심리학과 교수 | 자기계발 학습 자신감 세바시 강연 Sebasi Talk 내용 정리 거절 당할 때 용기를 잃는 이유 공부를 잘한다고 '거절'의 순간이 올때 용기를 낼 수있는건 아니다. 오히려 완벽하게 해내는 것에 익숙해져 있기 때문에 거절이나 실패 앞에서 더 쉽게 용기를 잃을 수도 있다. 처음부터 완벽해야 한다는 생각이 새로운 도전과 자신의 생각을 표현하는데 걸림돌이 되는것이다. 메타인지란 무엇인가? 강연자은 대학생 시절 메타인지에 관한 책을 읽고 다음 세 가지 내용에 깊은 인상을 받았다고 한다. 메타인지란 자기 자신의 거울이다. 그 거울 보면서 내 자신을 믿는 능력이다. 그 거울 계속 보면서 나의 완벽하지 않은 모습을 인정하는 것이다. 강연자는 이를 통해 메타인지를 어렵고 복잡한 개념이 아닌, 나 자신을 알아가는 과정이라고 정의했다. 메타인지를 방해하는 '완성의 착각' 우리는 흔히 처음부터 완벽한 결과물을 만들어야 한다고 생각한다. 이러한 생각을 강연자는 '완성의 착각'이라고 설명한다. 완벽해야 한다는 생각에 사로잡히면 자신의 부족한 모습을 드러내기 어려워지고, 실수나 실패를 두려워하게 된다. 결국 이러한 착각이 자신의 생각을 표현하고 새로운 도전을 하는 데 방해가 된다. 특히 강연자는 한국인 유학생들이 수업 시간에 토론이나 브레인스토밍을 할 때, 틀린 답을 말할까 봐 자신의 의견을 적극적으로 표현하지 못하는 모습을 많이 보았다고 한다. 불완전함을 인정하면서 생긴 변화 강연자는 메타인지를 이해하고 자신의 부족함을 인정하면서 조금씩 용기를 얻기 시작했다. 완벽하지 않아도 괜찮다는 것을 받아들이자 다른 사람 앞에서 자신의 생각을 표현하는 것도 한결 자연스러워졌다. 그 결과 한국어 실력도 빠르게 향상되었다고 한다. 즉, 자신의 부족함을 인정하는 것이 오히려 배움과 성장의 출발점이 된 것이다. 자녀 교육에서의 메타인지 강연자는 두 자녀의 사례를 통해 메타인지에 대해 설명했다. -첫 번째 아이: 숙제를 하지 않고 공부도 잘하지 않는 아이 -두 번째 아이: 공부를 잘하고 숙제도 성실하게 하는 아이 일반적으로는 공부를 잘하는 아이가 메타인지 능력도 높을 것이라고 생각하기 쉽다. 하지만 강연자는 오히려 첫 번째 아이가 자신의 생각과 감정을 더 잘 알고 있을 수도 있다고 이야기했다. 자신이 무엇을 하고 싶고, 무엇을 하기 싫어하는지 스스로 알고 있다. 자신이 원하는 것과 원하지 않는 것을 구분할 수 있다. 어려움이 닥쳤을 때도 스스로 해결하려는 과정을 통해 성장할 수 있다. 반면 늘 완벽한 모습을 보여온 아이는 실패나 어려움을 경험했을 때 쉽게 좌절하거나 포기할 수도 있다. 따라서 부모는 아이에게 정답을 바로 알려주기보다, 스스로 고민하고 문제를 해결할 수 있도록 기다려 주어야 한다. 브레인스토밍의 진정한 의미, 스톰(Storm) 브레인스토밍은 처음부터 완벽한 답을 찾아내는 과정이 아니다. 혼란스럽고 어렵더라도 다양한 생각을 나누고, 고민하고, 부딪히면서 답을 찾아가는 과정이다. 강연자는 이러한 혼란과 고민의 과정을 '스톰(Storm)'이라고 설명했다. 부모가 아이에게 답을 바로 알려주는 것은 아이가 직접 스톰을 겪으며 배울 기회를 줄이는 것일 수 있다. 따라서 부모의 역할은 정답을 알려주는 것이 아니라, 아이가 스스로 어려움을 마주하고 해결해 나갈 수 있는 환경을 만들어 주는 것이다. 메타인지의 중요성 메타인지는 학교 공부에만 필요한 능력이 아니다. 살아가면서 마주하는 거절과 실패, 용기가 필요한 순간에도 중요한 역할을 한다. 메타인지를 갖추었다고 해서 언제나 용기를 낼 수 있는 것은 아니다. 용기를 얻었다고 해서 모든 순간에 두려움이 사라지는 것도 아니다. 그렇기 때문에 끊임없이 새로운 스톰을 경험하고, 그 과정을 통해 다시 용기를 얻는 것이 중요하다. 오늘의 생각 이번 강연을 들으며 저 또한 강연자 님 같은 비슷한 일을 겪었을 때 거절을 못 견뎌 회피하고 했던 경험이 생각났습니다. 그동안 저 스스로 메타인지가 잘 되어있다고 자부했는데, 그게 아니었다는 것을 깨달았습니다. 그런데 그랬던 행동들이 내가 부족해서가 아니라, 처음부터 완벽해야 한다는 생각에 사로잡혀 있었기 때문이라는 것도 알게 되었습니다. 특히 메타인지는 내가 무엇을 알고 모르는지 아는 것뿐만 아니라, 내가 부족하고 완벽하지 않을 수 있다는 사실을 인정하는 것이라는 말이 마음에 와닿았습니다. 처음부터 완벽한 사람은 없고, 다른 사람의 의견을 듣고 때로는 실수하고 실패하는 과정에서 진짜 배움과 성장이 시작된다는 것을 알게 되었습니다. 거절당할 것 같은 순간이 왔을 때 지금 맞닥뜨린 이 스톰이 상대방인 나를 제외한 모든 사람들한테도 스톰일 것이고 그들도 완벽하지 않다는 것을 마음에 새기며 헤쳐나가야겠다는 생각을 했습니다. 그리고 앞으로는 완벽하게 준비된 모습만 보여주려고 애쓰기보다, 부족하더라도 내 생각을 솔직하게 표현해 보려고 합니다. 거절이나 피드백을 받더라도 상처받는 데서 멈추지 않고, 나를 돌아보고 한 걸음 더 성장할 수 있는 기회로 삼고 싶습니다. 완벽해야 시작할 수 있는 것이 아니라, 부족하더라도 시작하고 부딪히는 과정에서 조금씩 성장할 수 있다는 것. 이번 강연을 통해 배운 이 마음을 앞으로도 잊지 않고 실천해 나가고 싶습니다.
아키텍처에 대한 고민 UMC라는 연합동아리에서 2번의 백엔드 개발자로써 참여하게 되면서 백엔드 전체적인 흐름과 구조를 파악할 수 있게 되었고, 최근 들어서는 큰 구조와 틀을 알게 되고 나서 , 이 구조와 틀을 다른 방식으로 구성해보고 싶다는 생각을 많이 하였다. 두번의 프로젝트 모두 하나의 서버에 모든 도메인 로직과 기능들이 구현되어있는 모놀리식 서버 구조였고, 하나의 HTTP 요청에 대하여 하나의 트랜잭션으로 모든 도메인들이 처리되는 구조였다. "확장성 ❌ , 거대한 트랜잭션 안의 동기적 요청 , 도메인간 강결합" 모든 요청들이 동기적으로 연결되는 것에 대한 문제점을 인지하여 Event-Driven Architecture 에 관해 공부해보았다. 확장성 좋은 코드와 좋은 설계는 여러가지 기준이 있겠지만, 확장에 열려 있으며, 변경에는 닫혀있다. 구현에 의존하지 말고, 추상화에 의존하라 라는 것이 크게 가장 신경쓰는 부분들인것같다. 2번의 프로젝트에서의 MVC 아키텍처 , DDD 아키텍처 모두 도메인간의 결합을 최소화 하고자 Port , Adapter , Usecase간의 인터페이스를 이용한 도메인간 소통을 구현해 보았는데, 도메인이 다른 도메인을 호출해야하는 일들이 늘어날때마다 인터페이스를 늘려줘야한다는것은 해결하지 못하는 문제였다. 매번 동료의 usecase추가 요청이 지겨웠다. 그래서 EDA란 ? 앞에서 서술한 문제점 , 서론에서 보았던 문제를 일부분 해결할 수 있는 Event-Driven Architecture이다. 도메인간의 소통의 방식을 설계한 Architecture이다. 가장 다른 점은 도메인에서 이벤트를 발행 그 이벤트를 동시 다발적으로 필요한 Consumer 도메인들이 그 이벤트를 소비한다. 그래서 Event의 Payload를 정해두고, 어떤 Event를 소비하고 발행할것인지 Contract를 정한 후 , Event Hamndler 을 도메인별로 정의해두면 , 도메인간의 결합을 이벤트를 통한 소통으로 변경하여 Loosely Coupling을 구현할 수 있게 되는것이다. EDA 관점 OrderCreated ↓ Message Broker ├─ Inventory Service Event Handler ├─ Notification Service Event Handler └─ Analytics Service Event Handler 이 아키텍처의 가장 큰 장점은 비동기 이벤트 처리를 통해, 트랜잭션을 가볍게 가져가며, 반드시 처리되어야하는 후속 이벤트 처리들을 남겨둠으로써 사용자에게 보다 더 빠른 지연시간을 제공해줄 수 있다는 장점을 가진다. EDA가 적합한 경우 하나의 이벤트에 대하여 여러개의 도메인이 반응해야하는경우. 다시말해, 주문 발행이라는 이벤트에 대하여 알림 도메인 , 광고 도메인 , 유저 도메인 등등 다양한 도메인에서 그 이벤트를 병렬적으로 처리해야하는 경우 적합하다. 서비스간의 통신에서 동기적으로 통신해야하는경우 (HTTP 요청 , gRPC 통신 , 다른 서비스의 응답값이 반드시 필요한)가 많은 경우에는 적합하지 않다. Ex) 유저에게 즉시 결제 성공과 결제 실패를 반환해줘야하는 경우. EDA = MSA ? 처음에는 EDA 와 MSA를 동일한 설계구조라고 생각했다. 🤔 당연히 MSA 마이크로 서비스 환경에서는 EDA가 강제되는 것 아닌가? 하지만 분명히 두 아키텍처 설계의 목표는 다르다는 것. EDA는 도메인간의 소통의 방식을 설계하고 , 도메인간의 결합을 Loosely하게 하기 위한 아키텍처 MSA는 서비스마다 서버를 독립적을 하나씩 구성해 서비스의 독립성을 높이고, 시스템 경계를 정확히 하는데에 목적이 있다. MSA를 통해 독립적인 서비스들의 확장을 가져갈 수 있다는 목적 또한 가진다. 그리고 MSA 마이크로 서비스에서 모두 EDA 를 통한 서비스간의 소통을 하는것은 아니라는것이다. 그 반대도 마찬가지이다. EDA도 모두 MSA로 독립된 서버에서 무조건 사용되는것이 아니라 모놀리식 서버에서 도메인간의 처리를 비동기 이벤트 기반으로 처리할 수 있게 설계할 수 있다. 이 집합관계의 차이를 이해하게 되면 그 차이와 아키텍처의 목적이 다르다는 것을 알 수 있다. 하지만 결국 이 둘은 같이 가는 느낌이기는 하다. Message Broker EDA에서 필수 불가결하게 중요한 것이 이벤트들을 관리하는 기능을 추가해줘야한다는 것이다. 이 Message Broker 의 기능은 메세지 저장 및 이벤트 재발행 등의 책임들을 가진다. 이 Message Broker 의 대표적인 솔루션으로 언급되는것이 바로 RabbitQ, Kafka 이 두개이다. 그 중에서도 대규모 데이터 파이프라인에서 가장 주류를 이루는 것은 카프카. 카프카는 대용량 이벤트 스트림 처리, 메시지 보존 및 재처리, 수평 확장성의 장점을 가진다. Message Broker 의 책임 메시지 전달: Producer가 발행한 이벤트를 적절한 Consumer에게 전달 Producer–Consumer 결합도 감소: Producer는 누가 소비하는지 몰라도 됨 비동기 처리 지원: Consumer가 즉시 응답하지 않아도 메시지를 나중에 처리 가능 버퍼링: 순간적으로 이벤트가 몰려도 Consumer가 처리 가능한 속도로 소화할 수 있도록 완충 내구성 보장: 브로커에 따라 메시지를 디스크에 저장해 장애 후에도 재처리 가능 재전송 / 재처리 지원: Consumer 처리 실패 시 retry나 replay 가능 순서 보장: Kafka의 partition처럼 특정 범위 내 이벤트 순서를 보장할 수 있음 Consumer 분산 처리: 여러 Consumer 인스턴스에 메시지를 나눠 처리해 수평 확장 지원 Fan-out: 하나의 이벤트를 여러 Consumer가 각각 독립적으로 받을 수 있음
Cisco Packet Tracer 실행 구성은 RAM 에 저장 시작 구성은 NVRAM 에 저장 IOS 작동 운영체제 이미지는 Flash 에 저장 디바이스가 부팅될 때 시작구성에서 러닝 구성이 RAM에 로드 Router# : Cisco IOS의 권한 실행 모드 Router> : Cisco IOS의 사용자 실행 모드 Router(config)# : Cisco IOS의 전역 구성 모드 Router(config-if)# : Cisco IOS의 인터페이스 구성 모드 Console Cable : 라우터나 스위치의 콘솔 포트에 직접 연결하는 케이블 Privileged Exec mode : 'show', 'debug'와 같은 명령어가 입력됨 IOS(Internetworking Operating System) : 대부분의 시스코 기업 네트워크 장비에서 사용되는 시스코 운영체제 Enable : 사용자 실행 모드에서 권한이 있는 실행 모드로 전환할 때 사용하는 명령어 configure terminal : 권한 실행 모드에서 전역 실행 모드로 접근하기 위해 입력하는 명령어 exit : Cisco IOS CLI에서 한 단계 아래로 내려갈 때 사용하는 명령어 end : 모든 단계에서 권한 실행 모드로 내려갈 때 사용하는 명령어 show running-config (show run) : 라우터의 전체 실행 중인 구성을 보여주는 'show' 명령어 copy running-config startup-config (copy run start) : 재부팅 후에도 실행설정이 유지되도록 하는 명령어 ? : 도움말 접속 startup configuration : 라우터가 다음에 시작되거나 재부팅될 때 적용되는 설정 running configuration에서 입력한 명령어는 running configuration에서 startup configuration으로 명시적으로 복사하기 전까지는 영구적으로 저장되지 않음
문제 ᄂ 링크 문자열로 구성된 리스트 strings와, 정수 n이 주어졌을 때, 각 문자열의 인덱스 n번째 글자를 기준으로 오름차순 하여라. 예시) 예시 1: 1번째 인덱스 기준 → u, e, a → ["car", "bed", "sun"] 예시 2: 2번째 인덱스 기준 → c, c, x → cdx 가 마지막. c 가 같은 abce , abcd 는 사전순 으로 정렬 → ["abcd", "abce", "cdx"] 풀이 파이썬 내장함수와 기본 기능에 충실할 것 sorted 함수는 문자열 리스트를 정렬할 수 있고, key로 함수를 받아 정렬 기준을 바꿀 수 있다. 이 두 가지를 떠올리며 풀어 보자. 1. strings의 각 문자열을 [n번째 글자, 원본 문자열] 쌍으로 묶는다. 2. 쌍들을 정렬한다. n번째 글자로 먼저 비교하고, 같으면 원본 문자열의 사전순으로 비교한다. 3. 정렬된 쌍에서 원본 문자열만 꺼내 반환한다. # n번째 글자와 원본 문자열을 쌍으로 묶어 정렬하는 함수 def make_sorted_pairs(strings, n): pairs = [] for s in strings: pairs.append([s[n], s]) pairs.sort() return pairs # 실행 def solution(strings, n): pairs = make_sorted_pairs(strings, n) answer = [] for pair in pairs: answer.append(pair[1]) # 정렬된 문자열에서 원본 문자열만 남겨두기 return answer 다른 사람 풀이 람다 함수를 이용한 풀이 def strange_sort(strings, n): return sorted(strings, key=lambda x: x[n]) strings = ["sun", "bed", "car"] print(strange_sort(strings, 1)) 생각한 점 람다 함수를 쓸 수 있는 상황에서 람다 함수를 잘 떠올리지 못하는 경우가 많다. 코드가 길어질 듯한 상황에서는 람다함수로 보기 좋은 코드를 작성해보는 것도 좋을 듯 하다.
Industrial and construction projects in Saudi Arabia require dependable lifting and rigging equipment for handling heavy materials, machinery, structural components, and other demanding loads. Choosing the right Rigging Supplier Saudi Arabia can help businesses source suitable products for their specific lifting and material-handling requirements. Heavy Tools provides industrial and lifting equipment solutions for construction, oil and gas, marine, and other heavy-duty industries across Saudi Arabia. Its product range includes wire ropes, shackles, hooks, synthetic slings, ropes, lifting clamps, and other rigging equipment. Why Quality Rigging Equipment Matters Rigging equipment plays an important role in lifting and securing heavy loads. Products such as wire rope slings, shackles, synthetic slings, hooks, and lifting clamps need to be selected according to the load, application, working load limit, and operating environment. A professional Rigging Supplier Saudi Arabia can help businesses identify appropriate products for different industrial applications while supporting consistent procurement requirements. Rigging Products for Different Applications Heavy Tools offers a broad selection of products for industrial lifting and load-handling applications. These include: Wire rope lifting slings Synthetic slings Shackles Wire ropes Ropes and rope accessories Hooks Beam lifting clamps Rigging equipment Hoists and lifting equipment Load-securement solutions Having access to different categories allows construction and industrial companies to source multiple lifting requirements from one supplier. Applications Across Saudi Arabia Rigging equipment is used in several sectors, including construction, oil and gas, marine operations, manufacturing, logistics, and industrial maintenance. Saudi Arabia's lifting-equipment market also serves major industrial locations such as Riyadh, Dammam, Jubail, Jeddah, and other project areas. Whether the requirement involves lifting machinery, securing cargo, moving construction materials, or supporting industrial maintenance, selecting equipment with the correct specifications is essential. Choosing a Rigging Supplier When selecting a Rigging Supplier Saudi Arabia, businesses should consider product specifications, working load limits, quality documentation, availability, delivery capabilities, and technical support. It is also important to follow the manufacturer's instructions and applicable lifting-safety requirements. Rigging equipment should be inspected regularly for signs of wear, deformation, cuts, corrosion, or other damage. Damaged equipment should not be used for lifting operations. Why Choose Heavy Tools? Heavy Tools focuses on supplying industrial and lifting equipment for demanding applications in Saudi Arabia. The company supports bulk B2B requirements and provides lifting, rigging, safety, and material-handling solutions for different industries. For companies searching for a dependable Rigging Supplier Saudi Arabia, Heavy Tools offers a range of products designed to address different lifting and load-handling requirements. Conclusion Reliable rigging equipment is an important part of safe and organized material handling. From synthetic slings and wire ropes to shackles, hooks, and lifting clamps, choosing suitable products can support efficient industrial operations. Heavy Tools provides a wide range of lifting and rigging solutions for businesses across Saudi Arabia, making it a practical source for companies looking to meet their industrial lifting and material-handling needs. Visit : https://share.google/ZdhazvDpWSYZvWoOb Contact : 554655724
📚 스터디 교재 : SQL 첫걸음 (MySQL 기준) 📌 범위 : 5장 집계와 서브쿼리 / 6장 데이터베이스 객체 작성과 삭제 5장. 집계와 서브쿼리 20강. 행 개수 구하기 - COUNT 집계함수 일반 함수는 인수로 값 하나 를 받지만, 집계함수 는 인수로 집합(여러 행) 을 받아 값 하나 로 계산해 반환 → 이를 집계 라고 함 대표 집계함수 5가지: COUNT , SUM , AVG , MIN , MAX COUNT(*) 의 * 는 "모든 열 = 테이블 전체"를 의미 WHERE 구와 함께 쓰면 검색된 행 만 대상으로 개수를 셈 (SELECT 구는 WHERE 구보다 나중에 처리되므로) 집계함수와 NULL 집계함수는 집합 안에 NULL이 있으면 무시 하고 계산 단, COUNT(*) 만은 예외로 NULL도 포함해서 전체 행 수를 셈 DISTINCT로 중복 제거 DISTINCT 는 예약어(열명 아님). SELECT 구에서 지정하면 중복된 행을 제거 반대 개념은 ALL (기본값, 생략 시 ALL로 간주 → 중복 제거 안 함) 집계함수 + DISTINCT "NULL 제외하고 중복 없는 데이터 개수"를 구하려면 집계함수의 인수 에 DISTINCT를 붙인다 (콤마 없음) 21강. COUNT 이외의 집계함수 SUM( [ALL|DISTINCT] 집합) AVG( [ALL|DISTINCT] 집합) MIN( [ALL|DISTINCT] 집합) MAX( [ALL|DISTINCT] 집합) 함수 설명 적용 가능 자료형 SUM 합계 수치형만 AVG 평균 수치형만 MIN 최솟값 수치형, 문자열형, 날짜시간형 MAX 최댓값 수치형, 문자열형, 날짜시간형 SUM , AVG 도 NULL은 무시 하고 계산 (NULL을 0으로 간주하고 싶으면 CASE 로 변환 후 계산) 22강. 그룹화 - GROUP BY SELECT * FROM 테이블명 GROUP BY 열1, 열2, ... GROUP BY 로 지정한 열의 값이 같은 행끼리 하나의 그룹 으로 묶임 GROUP BY 만 쓰고 집계함수를 안 쓰면 DISTINCT 와 비슷한 효과 (중복 제거) 진짜 의미 는 집계함수와 함께 쓸 때 — 그룹마다 하나의 집합으로 집계함수에 넘겨짐 실무 활용: 점포별·상품별·월별 매출 집계 등에 자주 사용 HAVING 구로 조건 지정 집계함수는 WHERE 구에서 사용할 수 없음 (WHERE가 GROUP BY보다 먼저 처리되기 때문) 집계 결과에 조건을 걸고 싶으면 HAVING 구 사용 (GROUP BY 뒤에 기술) 내부 처리 순서 WHERE 구 → GROUP BY 구 → HAVING 구 → SELECT 구 → ORDER BY 구 HAVING도 SELECT보다 먼저 처리되므로 SELECT에서 붙인 별명은 HAVING/GROUP BY에서 쓸 수 없음 (단, MySQL은 예외적으로 허용 — 데이터베이스 제품마다 다름) ORDER BY는 그룹화 이후 처리이므로 집계함수를 문제없이 사용 가능 복수열 그룹화 주의 GROUP BY 에 지정하지 않은 열 은 집계함수 없이 SELECT 구에 쓸 수 없음 (어떤 값을 반환해야 할지 모호하므로 에러) 여러 열로 그룹화하려면: GROUP BY no, quantity 처럼 콤마로 나열 정렬 GROUP BY 만으로는 결과가 정렬되지 않음 → ORDER BY 구를 추가로 사용 23강. 서브쿼리 (SELECT 명령) 서브쿼리 = SQL 명령 안에 괄호로 묶어 지정하는 하부 SELECT 명령 주로 WHERE 구에서 사용되지만 SELECT , FROM , SET 구 등 다양한 곳에서 사용 가능 WHERE 구에서 서브쿼리 사용 열 값이 가장 작은 행을 몰라도 서브쿼리로 찾아서 바로 삭제 스칼라 값 / 스칼라 서브쿼리 SELECT 명령이 값 하나(행 1개 × 열 1개)만 반환 하는 것 = 스칼라 값 스칼라 값을 반환하는 서브쿼리 = 스칼라 서브쿼리 = 연산자로 비교하려면 반드시 스칼라 값끼리 비교해야 함 SELECT 구, SET 구에서 서브쿼리를 쓸 때는 스칼라 서브쿼리 여야 함 WHERE 구에 스칼라 서브쿼리를 쓰면 HAVING 없이도 집계 결과를 조건식으로 사용 가능 MySQL은 FROM 구 생략 가능, Oracle은 FROM DUAL 필요 (DUAL = 시스템이 기본 제공하는 테이블) FROM 구에서 서브쿼리 사용 (중첩구조/네스티드 쿼리) FROM 구의 서브쿼리는 스칼라 값이 아니어도 됨 서브쿼리에는 AS 키워드로 별명을 붙여 사용 (3장의 열 별명과 같은 방식) 여러 단계로 중첩 가능 (실무에서는 잘 안 씀) INSERT 명령과 서브쿼리 INSERT SELECT: VALUES 대신 SELECT 명령으로 여러 행 복사/이동 24강. 상관 서브쿼리 EXISTS (SELECT 문) EXISTS 서브쿼리가 반환하는 행이 있는지 를 조사 (행이 있으면 참, 없으면 거짓 — 스칼라 값일 필요 없음) NOT EXISTS : 반대로 행이 없을 때 참 상관 서브쿼리란 위 예제처럼 부모 쿼리(UPDATE)와 자식인 서브쿼리가 서로 연관 되어 있는 경우 (서브쿼리의 no2 = no 에서 no 는 부모 테이블의 열을 참조) 상관 서브쿼리는 부모 명령과 떼어내 단독으로 실행할 수 없음 (단순 서브쿼리와 차이점) 두 테이블의 열 이름이 같을 경우 테이블명.열명 형식으로 명시 IN 열명 IN (집합) 집합 안에 값이 있으면 참 ( NOT IN 은 반대) 집합 부분에 서브쿼리 도 지정 가능 IN과 NULL 주의 : 집계함수는 NULL을 무시하지만, IN 은 무시하지 않음 — NULL = NULL 은 비교 불가하므로 NULL 비교는 IS NULL 사용. 특히 NOT IN 은 집합 안에 NULL이 있으면 결과가 전부 불명(UNKNOWN) 이 되어버림 6장. 데이터베이스 객체 작성과 삭제 25강. 데이터베이스 객체 데이터베이스 객체 : 테이블, 뷰, 인덱스, 프로시저 등 데이터베이스 안에 정의하는 모든 것 (실체를 가짐) SELECT, INSERT 같은 SQL 명령은 객체가 아님 (실체가 없음) 명명 규칙 기존 이름·예약어와 중복 금지 / 숫자로 시작 불가 / 언더스코어( _ ) 외 기호 사용 불가 / 한글 사용 시 백쿼트( ` )로 감쌈(MySQL) / 시스템이 허용하는 길이 초과 불가 이름은 의미 있게 짓는 것이 좋음 (단순 번호 지양) 같은 스키마 내에서는 객체 종류와 상관없이 이름 중복 불가 (테이블 t1 이 있으면 같은 이름의 뷰도 불가) 스키마 데이터베이스 객체는 스키마 라는 "그릇" 안에 생성됨 → 스키마가 다르면 이름이 같아도 무방 객체 = 스키마 객체 라고도 불림. DDL로 정의 데이터베이스 제품마다 무엇이 스키마가 되는지 다름 (MySQL: CREATE DATABASE 로 만든 데이터베이스 자체가 스키마 / Oracle: 데이터베이스+사용자가 계층적 스키마) 이름 충돌을 막는 그릇 = 네임스페이스 . 스키마와 테이블 모두 네임스페이스 역할을 함 26강. 테이블 작성·삭제·변경 CREATE TABLE 테이블명 (열 정의1, 열 정의2, ...) DROP TABLE 테이블명 ALTER TABLE 테이블명 하부명령 테이블 작성 열명 자료형 [DEFAULT 기본값] [NULL|NOT NULL] 자료형 뒤 NOT NULL 생략 시 기본적으로 NULL 허용 테이블 삭제 DROP TABLE 테이블명 테이블과 저장된 데이터가 모두 삭제 됨. 확인 메시지 없이 바로 실행되므로 주의 테이블 정의는 남기고 데이터만 지우려면 DELETE (행 단위 처리라 느림) 또는 TRUNCATE TABLE (조건 지정 불가, 전체 삭제 전용, 훨씬 빠름) 테이블 변경 (ALTER TABLE) ALTER TABLE 테이블명 변경명령 데이터를 유지한 채 구조만 변경 가능. 크게 1 열 추가·삭제·변경, 2 제약 추가·삭제 로 구분 작업 하부명령 예시 열 추가 ADD ALTER TABLE sample62 ADD newcol INTEGER; 열 속성 변경 MODIFY ALTER TABLE sample62 MODIFY newcol VARCHAR(20); 열 이름 변경 CHANGE ALTER TABLE sample62 CHANGE newcol c VARCHAR(20); 열 삭제 DROP ALTER TABLE sample62 DROP c; ADD 로 열 추가 시 기존 행의 새 열 값은 전부 NULL (기본값 지정 시 그 값으로 채워짐). NOT NULL 제약을 걸며 추가하려면 반드시 기본값을 지정 해야 함 MODIFY 는 열 이름은 못 바꾸지만 자료형·기본값·제약은 변경 가능 (MySQL/Oracle 전용 문법 — 방언 존재) 열 이름 변경은 CHANGE 사용 (Oracle은 RENAME TO ) 실무 활용: 문자열 최대 길이 늘리기( MODIFY ), 열 추가( ADD ) — 단, 열 추가 시 기존 INSERT 문에 영향 없는지 꼭 확인 27강. 제약 테이블에 제약 을 설정해 저장될 데이터를 제한 (예: NOT NULL ) 그 외 대표적 제약: 기본키(Primary Key) 제약 , 외부참조(정합) 제약 열 제약 vs 테이블 제약 하나의 열에 설정하는 제약 = 열 제약 ( NOT NULL , UNIQUE 등) 여러 열에 걸친 제약 = 테이블 제약 (예: 복합 기본키). CONSTRAINT 제약명 으로 이름을 붙여두면 관리가 쉬움 제약 추가/삭제 제약 추가 시 기존 데이터가 제약을 위반하면 에러 발생 (기존 행 먼저 검사) 기본키(Primary Key) 테이블의 행 한 개를 특정 할 수 있는 검색키 → 같은 값 중복 불가 (= 유일성 제약 ) 기본키로 지정할 열은 반드시 NOT NULL 이어야 함 중복되는 값으로 INSERT/UPDATE 시도 시 에러 발생 ( Duplicate entry ... for key 'PRIMARY' ) 복수 열로 기본키 구성 가능 : 이 경우 열들을 조합한 값 이 유일해야 함 (한 열만 보면 중복이어도, 다른 열과 합쳐서 유일하면 OK) 28강. 인덱스 구조 인덱스란 색인 이라고도 함. 역할은 검색 속도 향상 책의 목차·색인처럼, 검색 키워드와 대응하는 데이터 위치를 저장 테이블과 별개의 독립된 객체지만 테이블에 의존 (테이블 삭제 시 인덱스도 함께 삭제됨) 검색 알고리즘 방식 설명 속도 풀 테이블 스캔 처음부터 모든 행을 차례로 조사 데이터 수에 비례해 느려짐 이진 탐색 정렬된 집합을 절반씩 나눠가며 탐색 데이터가 2배 늘어도 비교 횟수는 +1회 데이터베이스 인덱스는 보통 이진 트리(binary tree) 구조로 저장 (그 외 해시 방식도 있음) 각 노드는 왼쪽(작은 값)·오른쪽(큰 값) 가지로 분기, 루트 노드 에서 시작해 가지를 타고 내려가며 탐색 유일성 : 이진 트리는 기본적으로 중복값을 가질 수 없는 구조 → 기본키 제약이 이진 트리 인덱스로 구현되는 경우가 많음 29강. 인덱스 작성과 삭제 CREATE INDEX 인덱스명 ON 테이블명 (열명1, 열명2, ...) DROP INDEX 인덱스명 [ON 테이블명] 표준 SQL에는 CREATE INDEX 명령이 없음 (제품 의존적 선택 항목이지만 대부분 지원) Oracle/DB2: 인덱스가 스키마 객체 (스키마 내 이름 중복 불가) / SQL Server·MySQL: 테이블 내 객체 (테이블 내 이름 중복 불가) 인덱스를 만들면 SELECT 검색은 빨라지지만 , INSERT 시 인덱스도 갱신해야 하므로 속도가 다소 느려짐 EXPLAIN EXPLAIN SQL명령 실제로 실행하지 않고 실행계획 (인덱스 사용 여부 등)을 확인하는 명령 (표준 SQL은 아니지만 대부분 제품이 지원) possible_keys : 사용 가능한 인덱스 / key : 실제 사용된 인덱스 (조건에 해당 열이 없으면 둘 다 NULL) 최적화 인덱스 사용 여부는 데이터베이스 내부 최적화 처리 가 실행계획을 세워 결정 값의 종류가 적은 열(예: '예'/'아니오'만 있는 열)은 인덱스를 걸어도 이진 트리 구조가 비효율적 이 되어 품질이 낮음 → 서로 다른 값이 많을수록 인덱스 효율이 좋아짐 30강. 뷰 작성과 삭제 CREATE VIEW 뷰명 AS SELECT문 DROP VIEW 뷰명 뷰란 FROM 구의 서브쿼리에 이름을 붙여 객체화 한 것 SELECT 명령 자체는 객체가 될 수 없지만, 뷰로 등록하면 이름 붙여 관리 가능 뷰를 참조하면 정의된 SELECT 명령의 실행 결과 를 테이블처럼 사용 가능 뷰의 열을 직접 지정할 수도 있음 (지정 안 하면 SELECT 구의 열이 그대로 뷰의 열이 됨) 가상 테이블 뷰는 실체(저장 데이터)가 없는 "가상 테이블" — SELECT 명령만 저장 데이터로 가짐 INSERT/UPDATE/DELETE도 조건에 따라 가능은 하지만, SELECT 전용으로 사용하는 것을 권장 뷰의 약점과 대안 뷰를 참조할 때마다 내부 SELECT 명령이 매번 재실행 됨 → 원본 데이터가 많거나 뷰를 중첩 사용하면 처리 속도 저하 머티리얼라이즈드 뷰(Materialized View) : 결과를 저장장치에 실제로 저장 해두고 재사용 (원본 데이터가 바뀌면 자동으로 재계산). MySQL은 미지원, Oracle·DB2 등에서 사용 가능 상관 서브쿼리처럼 부모와 연관된 쿼리는 뷰로 단독 실행이 안 됨 → 이런 경우 함수 테이블 (인수를 받아 테이블을 반환하는 사용자 정의 함수)로 대체 가능
Local LLM (google/gemma-4-26b-a4b-qat)으로 실습 중... 클로드 코드가 대단하다는 것은 알고 있었지만, 무료 플랜만으로는 사용할 수 없다는 한계로 인하여 그동안 사용을 미뤄두고 있었다. 그러나 LM Studio 또는 Ollama와 연동하여 Local LLM을 이용하는 방법이 있어서 Ch3 : 이슈 생성 스킬 만들기 - create-issue gh auth login : 깃허브 인증이 완료되지 않은 경우 최종 : TODO.md 파일 생성 완료 TODO.md 파일 내용 [Bug] GET /tasks 응답 속도 저하 (N+1 Query 발생) 현상: 할 일 개수만큼 추가 쿼리가 발생하여 속도가 저하됨. 원인: app/services/task_service.py 의 list_tasks() 에서 selectinload 미사용. /create-issue 실행 에러 발생 Setting 수정 후 다시 실행 코드 수정
리랭커란, RAG가 생성한 후보 문서들에 대해 질문에 대한 관련성 및 일관성을 판단하여 문서의 우선 순위를 재정렬하는 것 즉, 질문과 관련성 있는 문서들을 컨텍스트의 상위권에 위치시킴으로써, 답변의 정확도를 올림 1. 기존 RAG에서의 문제 RAG는 문서에서 의미론적 검색(Semantic search) 과정을 수행함 → 벡터 검색 이 과정에서 발생되는 정보 손실 문서의 임베딩 벡터 변환 과정에서 손실 : 문서가 긴 경우에 정해진 벡터의 차원으로 표현하기 어려움 검색 과정에서 손실 : 시간 단축을 위해 ANNs 기술을 사용함 (Approximate Nearest Neighbor search) → 이러한 문제를 해결하기 위해 검색 후 반환되는 문서 수를 늘림 (k 증가) → but, LLM에게 전달되는 컨텍스트가 늘어나서 비용이 비효율적 => 컨텍스트 내 존재 유무가 중요한 게 아니라, 순서가 중요함 2. Lost in the Middle Lost in Middle: 질문에 대한 관련 문서가 컨텍스트 중간에 위치할 경우, LLM 응답 정확도가 낮아진다. (출처: AWS 기술 블로그, 한국어 Reranker를 활용한 검색 증강 생성(RAG) 성능 올리기 / 원 출처: Liu et al., 2023 ) RAG의 정확도는 관련 정보의 컨텍스트 내 존재 유무가 아니라 순서이다. → 관련 정보가 컨텍스트 내 상위권에 위치하고 있을 때 좋은 답변을 얻을 수 있다. 3. 리랭커 💡 가장 관련성 높은 문서를 상위권에 배치(순위 재정렬, Reranking)하기 질문과 문서 사이의 유사도를 측정하는 것이 목적 Cross-encoder 사용 그림 2: Encoder 종류 (a) Bi-encoder (b) Cross-encoder (출처: AWS 기술 블로그, 한국어 Reranker를 활용한 검색 증강 생성(RAG) 성능 올리기 ) 질문과 문서를 하나의 input으로 활용 : 동시에 분석(Self-attention) rerank를 사용할 때는 검색 단계에서 상위 k개 문서에 한해서만 순위를 재조정함 유사도를 이용한 검색은 전체 문서에 대해서 빠르게 결과값을 얻을 수 있지만, rerank는 질의와 문서 사이의 의미론적 유사성을 탐색함 → 오래 걸려서 검색을 통해서 추출된 상위 문서에만 한해서 수행 4. 1차 검색(Bi-encoder)과 리랭커(Cross-encoder)의 차이 💡 둘 다 의미를 계산한다. 차이는 "질문과 문서가 모델 안에서 언제 서로를 보느냐"이다. Bi-encoder (1차 검색) 질문과 문서를 각각 따로 인코더에 넣는다. 질문 → 인코더 → 벡터 q 문서 → 인코더 → 벡터 d 점수 = cos(q, d) (또는 내적) 문서 벡터 d를 만들 때 질문은 입력에 없다. → 문서 벡터는 질문과 상관없이 미리 계산해서 저장 해 둘 수 있다 (벡터 DB, 인덱스). 검색 시점에 하는 일: 질문 인코딩 1번 + 저장된 벡터와의 내적 N번 → 빠르다. 한계 문서 벡터를 만들 때 어떤 질문이 올지 모르므로, 문서의 모든 정보를 고정된 차원의 벡터 하나에 미리 압축해야 한다. 질문의 단어와 문서의 단어가 모델 안에서 직접 비교되는 과정이 없다. 비교는 맨 마지막에 벡터 두 개끼리 한 번만 일어난다. 이 방식을 representation-based (표현 기반) 방식이라고 부른다. 참고: late interaction 은 ColBERT처럼 토큰 단위 벡터들을 마지막에 비교하는 방식을 가리키는 용어로, Bi-encoder와는 구분된다. Cross-encoder (리랭커) 질문과 문서를 하나의 시퀀스로 이어 붙여 한 번에 넣는다. 입력: [CLS] 질문 토큰들 [SEP] 문서 토큰들 [SEP] Transformer의 모든 layer에서 self-attention이 시퀀스 안의 모든 토큰 사이에 계산된다. → 질문 토큰과 문서 토큰이 처음부터 서로를 직접 참조 한다. 예: 질문의 데뷔 토큰이 문서의 2022년 , 데뷔하여 토큰에 직접 attention 할 수 있다. 마지막 layer의 [CLS] 출력 → linear layer → 관련도 점수(실수 1개). 이 방식을 interaction-based (상호작용 기반) 방식이라고 부른다. 한계 점수가 (질문, 문서) 쌍 에 대해서만 정의된다. 질문이 들어오기 전에는 아무것도 미리 계산할 수 없다. 문서가 N개면 모델 forward를 N번 해야 한다. 그래서 2단계로 나눈다 1단계: Bi-encoder로 전체 문서에서 상위 k개를 빠르게 추린다. 2단계: Cross-encoder로 그 k개만 정밀하게 다시 점수 매긴다. 비용 예시 후보 문서 N개 전체에 cross-encoder → 검색어 1개당 forward N번 1차 검색 top-50에만 cross-encoder → 검색어 1개당 forward 50번 5. 코드 파이썬: v3.8.16 transformers: v4.41.2 Dongjin-kr/ko-reranker · Hugging Face BAAI/bge-reranker-large 기반 한국어 데이터에 대한 fine-tuned model # 1. 모델과 데이터 처리에 필요한 라이브러리 불러오기 from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch import numpy as np # 2. 비교할 문장 쌍 데이터 설정 (질문과 4개의 후보 답변들) pairs = [ ['뉴진스는 몇 년도에 데뷔했나요?', '안녕하세요.'], ['뉴진스는 몇 년도에 데뷔했나요?', '뉴진스의 데뷔 연도는 2022년입니다.'], ['뉴진스는 몇 년도에 데뷔했나요?', '2022년에는 뉴진스, 르세라핌, 엔믹스가 데뷔했습니다.'], ['뉴진스는 몇 년도에 데뷔했나요?', '뉴진스는 2022년에 데뷔하여 Hype boy 등 여러 노래를 발표했습니다.'], ] # 3. 로짓(모델의 원시 출력값)을 확률 분포(0~1)로 변환하는 정규화 함수 정의 def exp_normalize(x): b = x.max() # 수치적 안정성을 위해(오버플로우 방지) 최댓값 추출 y = np.exp(x - b) # 각 값에서 최댓값을 뺀 후 자연상수 e의 지수승 계산 return y / y.sum() # 모든 값의 합으로 나누어 총합이 1이 되는 확률 값으로 반환 # 4. 사전 학습된 한국어 Reranker 모델의 Hugging Face 저장소 경로 지정 model_path = "Dongjin-kr/ko-reranker" # 5. 지정된 경로에서 텍스트를 처리할 토크나이저와 모델 가중치 불러오기 tokenizer = AutoTokenizer.from_pretrained(model_path) model = AutoModelForSequenceClassification.from_pretrained(model_path) # 6. 모델을 추론(평가) 모드로 전환 (학습용 기능인 Dropout 등 비활성화) model.eval() # 7. 기울기(Gradient) 계산을 비활성화하여 메모리 사용량을 줄이고 속도 향상 with torch.no_grad(): # 문장 쌍을 모델이 이해할 수 있는 토큰 형태로 변환 (패딩 및 절단 적용, 최대 512토큰) inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512) # 모델에 토큰화된 입력을 통과시켜 연관성 점수(logits)를 계산하고, 1차원 실수 배열로 변환 scores = model(**inputs, return_dict=True).logits.view(-1, ).float() # 텐서를 NumPy 배열로 변환한 뒤, 직접 정의한 함수를 통해 0~1 사이의 확률값으로 정규화 scores = exp_normalize(scores.numpy()) # 8. 계산된 확률값에 100을 곱해 퍼센트(%)로 바꾸고, 소수점 둘째 자리까지 반올림하여 출력 print(np.round(scores * 100, 2)) pairs 의 각 원소가 [질문, 문서] 이고, tokenizer(pairs, ...) 가 이 둘을 하나의 시퀀스로 합친다. → 4개 쌍 = 시퀀스 4개 = cross-encoder 입력 4개. 6. 코드의 점수(logits)와 exp_normalize (softmax)의 의미 코드에서 실제로 일어나는 일 model(**inputs).logits → shape (4, 1) . 쌍 하나당 실수 1개. .view(-1) → shape (4,) 로 펴기. exp_normalize → 4개 값의 합이 1이 되도록 변환. 이 함수는 softmax 와 같은 계산이다. 포인트 1: 모델은 쌍마다 독립적으로 점수를 매긴다 (pointwise) 4개 쌍을 batch로 한 번에 넣었지만, self-attention은 각 시퀀스 내부에서만 계산된다. 즉 2번 쌍의 점수를 계산할 때 모델은 1·3·4번 문서를 전혀 보지 않는다. 이처럼 (질문, 문서 1개) → 점수 1개 구조를 pointwise 방식이라고 한다. 포인트 2: softmax 확률은 "후보들 사이의 상대값"이다 softmax는 모델이 계산을 끝낸 뒤에 적용하는 후처리다. 모델이 후보들을 서로 비교한 결과가 아니다. softmax 값은 같이 넣은 후보 집합에 따라 달라진다. 가상의 logit으로 계산한 예: 4개 후보 logit이 [-5, 5, 3, 4] 일 때 softmax → 약 [0.0%, 66.5%, 9.0%, 24.5%] 같은 2번 문서를 1번 문서( 안녕하세요. )와만 비교하면 logit [-5, 5] → 약 [0.0%, 100.0%] 2번 문서의 logit(5)은 그대로인데, 확률은 66.5% → 100%로 바뀐다. 따라서 softmax 확률은 "이 문서가 절대적으로 얼마나 관련 있는가"를 나타내지 않는다. 포인트 3: 순위만 필요하면 softmax는 없어도 된다 softmax는 큰 값이 항상 큰 값으로 남는 변환(단조 증가)이다. → softmax 전후의 순위는 같다. 재정렬 목적이면 logit을 그대로 정렬해도 결과가 같다. "점수 X 이상만 남긴다" 같은 절대 기준이 필요하면 softmax 확률이 아니라 logit 자체나 sigmoid(logit)을 기준으로 삼아야 한다. (어떤 변환을 쓰는지는 모델 카드 확인) 7.End-to-End PipeLine 질문 → 1단계 Bi-encoder로 top-k 후보 추출 → 2단계 Cross-encoder로 후보 재점수 → 상위 n개를 컨텍스트 앞쪽에 배치 → LLM 문서 임베딩은 질문이 들어오기 전에 미리 계산한다. (4장의 Bi-encoder 특징) 예제는 문서 수가 적어서 전체 문서와 내적을 계산했지만, 실제로는 벡터 DB와 ANN 검색을 사용한다. 리랭킹 단계는 softmax 없이 logit을 그대로 정렬한다. (6장 포인트 3) top_k (리랭커에 넘길 후보 수)와 top_n (LLM에 넘길 문서 수)은 다른 값이다. top_k 가 클수록 1차 검색에서 놓치는 문서가 줄지만, 리랭커 forward 횟수가 늘어난다. top_n 이 작을수록 LLM에 전달되는 컨텍스트가 줄어든다. from sentence_transformers import SentenceTransformer from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch import numpy as np # 문서 저장소 documents = [ '안녕하세요.', '뉴진스의 데뷔 연도는 2022년입니다.', '2022년에는 뉴진스, 르세라핌, 엔믹스가 데뷔했습니다.', '뉴진스는 2022년에 데뷔하여 Hype boy 등 여러 노래를 발표했습니다.', '르세라핌은 2022년 5월에 데뷔했습니다.', '아이브는 2021년에 데뷔했습니다.', ] query = '뉴진스는 몇 년도에 데뷔했나요?' # 1단계: Bi-encoder 1차 검색 embedder = SentenceTransformer('BAAI/bge-m3') doc_embs = embedder.encode(documents, normalize_embeddings=True) # 질문과 무관하게 미리 계산 query_emb = embedder.encode(query, normalize_embeddings=True) sims = doc_embs @ query_emb # 정규화된 벡터의 내적 = 코사인 유사도 top_k = 4 candidate_idx = np.argsort(-sims)[:top_k] candidates = [documents[i] for i in candidate_idx] # 2단계: Cross-encoder 리랭킹 model_path = 'Dongjin-kr/ko-reranker' tokenizer = AutoTokenizer.from_pretrained(model_path) reranker = AutoModelForSequenceClassification.from_pretrained(model_path) reranker.eval() pairs = [[query, doc] for doc in candidates] with torch.no_grad(): inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512) logits = reranker(**inputs, return_dict=True).logits.view(-1).float().numpy() top_n = 2 order = np.argsort(-logits) # logit 그대로 내림차순 정렬 reranked = [candidates[i] for i in order[:top_n]] # 3단계: 관련도 높은 문서를 컨텍스트 앞쪽에 배치하여 LLM에 전달 context = '\n'.join(f'[문서 {i + 1}] {doc}' for i, doc in enumerate(reranked)) prompt = f"""다음 문서를 참고하여 질문에 답하세요. {context} 질문: {query} 답변:""" # answer = call_llm(prompt) # 사용하는 LLM API로 대체 print(prompt) 8. Pointwise / Pairwise / Listwise Pointwise: (질문, 문서 1개) → 점수. 5장의 코드가 이 방식. Pairwise: (질문, 문서 A, 문서 B) → A와 B 중 어느 쪽이 나은가. Listwise: (질문, 문서 여러 개를 한 입력에) → 순서 전체. 모델이 입력 안에서 후보들을 직접 비교 한다. 5장 코드의 softmax는 겉보기에 후보를 비교하는 것처럼 보이지만, 비교는 모델 밖에서 사후에 일어난 것이다. Listwise는 비교가 모델 안에서 일어난다는 점이 다르다. 방식 평가 메커니즘 특징 장단점 Pointwise (포인트와이즈) 하나의 쿼리와 단 하나의 문서 를 독립적으로 비교하여 점수를 매김 주로 Cross-Encoder 모델 사용 ➕ 속도가 빠르고 병렬 처리가 쉬움 ➖ 다른 문서들과의 상대적 우위를 반영하지 못함 Pairwise (페어와이즈) 하나의 쿼리에 대해 두 개의 문서 를 짝지어 어느 쪽이 더 우수한지 비교 토너먼트 형식의 비교 가능 ➕ 상대적 비교가 가능해짐 ➖ 문서 수가 많아질수록 비교 횟수가 제곱에 비례하여 증가 Listwise (리스트와이즈) 하나의 쿼리와 전체 후보 문서 목록 을 동시에 입력받아 최종 순열(Permutation)을 한 번에 출력 최신 고성능 모델(LLM 기반)에서 주로 차용 ➕ 문서 간 중복성, 다양성, 미세한 품질 차이를 가장 잘 포착함 ➖ 연산 비용이 크고 실시간 처리 속도(Latency)가 느림 Listwise 관련: ZipRerank Pointwise와 Listwise의 처리 방식 💡 처리 방식(병렬이냐 한 번이냐)은 결과로 따라오는 특징이다. 본질적인 차이는 "모델이 다른 후보를 보느냐"이다. Pointwise: 병렬로 처리"해야" 하는 게 아니라, 병렬로 처리"할 수 있다" 후보마다 계산이 독립적이다. → 순서대로 10번 돌려도, 동시에 돌려도 결과가 같다. 실무에서는 속도 때문에 batch로 묶어서 한 번에 처리한다. 5장 코드에서 pairs 4개를 tokenizer(pairs, ...) 로 한꺼번에 넣은 것이 batch 처리다. batch로 묶여 있어도 각 쌍은 서로를 보지 않는다. 함께 넣는 것은 GPU를 효율적으로 쓰기 위한 것일 뿐이다. Listwise: 기본은 한 번, 후보가 많으면 여러 번 후보가 10개 정도면 호출 1번으로 끝난다. 후보가 입력 길이 한도를 넘으면 여러 번 나눠서 호출한다. 예: 후보 50개 → 뒤쪽 20개를 먼저 정렬 → 창을 앞으로 옮기며 다시 정렬 (sliding window) 각 호출이 앞 호출의 결과를 이어받으므로 순서대로 처리해야 한다. 병렬 처리가 안 된다. 정리 Pointwise 본질: 후보 하나씩 따로 판단 호출: K번. 서로 독립적이라 병렬 가능 출력: 후보마다 점수 Listwise 본질: 후보 여러 개를 같이 보고 판단 호출: 기본 1번. 후보가 많으면 여러 번 순차 처리 출력: 순서 "한 번에 처리" ≠ "빠르다" Listwise는 호출이 1번이어도 느릴 수 있다. 후보 전부가 들어가므로 입력이 길다. 순서를 token 하나씩 생성한다. Pointwise는 호출이 여러 번이어도 병렬로 돌리면 전체 시간이 짧을 수 있다. 어느 쪽이 빠른지는 모델 크기, 후보 수, 서빙 환경에 따라 달라진다. Listwise와 Lost in the Middle 여러 문서를 한 입력에 넣는 listwise 리랭커 자체도 입력 중간의 후보를 덜 보는 같은 문제를 겪을 수 있다. → 후보 순서를 바꿔 여러 번 돌리거나, 작은 묶음 단위로 나눠 정렬하는 방식으로 대응한다. 마무리 리랭커는 전체 문서를 정밀하게 보는 비용과 1차 검색의 정보 손실 사이에서, 상위 k개만 다시 보는 절충 지점이다. 관련 문서를 컨텍스트 앞쪽으로 옮겨 Lost in the Middle을 피해 간다. 참고 자료 한국어 Reranker를 활용한 검색 증강 생성(RAG) 성능 올리기 | Amazon Web Services Lost in the Middle: How Language Models Use Long Contexts (Liu et al., 2023) CH11 리랭커(Reranker) Very Efficient Listwise Multimodal Reranking for Long Documents <랭체인LangChain 노트> - LangChain 한국어 튜토리얼🇰🇷 RAG 시스템 성능을 한 단계 끌어올리는 재순위 지정 모델(Reranker) 완벽 가이드
DLL 사이드로딩이란? 1. DLL 사이드로딩을 정리하게 된 이유 이번에 EDR/MDR을 활용한 위협 행위 분석 및 대응 프로젝트를 진행하면서 여러 악성코드 샘플을 분석해볼 기회가 있었다. 프로젝트에서는 팀원들이 각자 샘플 하나씩을 맡아 동적 분석과 EDR 로그 분석을 진행했고, 나는 그중 USB를 통해 확산되는 코인마이너 악성코드를 담당했다. 분석하면서 여러 행위를 확인했는데, 그중 하나가 DLL Side-Loading(DLL 사이드로딩) 이었다. 정상 프로그램을 이용해 악성 DLL을 실행할 수 있다는 특징 때문에 다양한 공격에서 활용되고 있었고, 한 번 제대로 정리해두면 좋겠다는 생각이 들었다. 이번 글에서는 당시 분석했던 내용을 바탕으로 DLL 사이드로딩이 어떤 방식으로 동작하는지, 그리고 분석할 때 어떤 부분을 확인해야 하는지 정리해보려고 한다. 2. DLL이란? DLL은 Dynamic Link Library 의 약자로, 프로그램에서 사용할 함수나 리소스를 모아놓은 파일이다. Windows 프로그램은 필요한 기능을 전부 실행 파일 안에 넣기보다 DLL로 분리해두고, 실행 중 필요한 DLL을 불러와 사용하는 경우가 많다. 예를 들면 아래와 같은 형태다. program.exe ├─ A.dll ├─ B.dll └─ C.dll 프로그램이 DLL을 사용하려면 해당 DLL을 찾아 메모리에 올리는 과정이 필요한데, DLL 사이드로딩은 이 과정을 악용한다. 3. DLL 사이드로딩이란? DLL 사이드로딩은 정상 실행 파일이 원래 불러와야 할 DLL 대신, 공격자가 준비한 DLL을 불러오도록 만드는 기법 이다. 예를 들어 program.exe 가 실행될 때 example.dll 이 필요한 경우, 정상적인 경우라면 아래처럼 정상 DLL을 불러온다. program.exe ↓ 정상 example.dll 그런데 공격자가 프로그램이 DLL을 찾는 위치에 같은 이름의 악성 DLL을 넣어두면, 프로그램이 그 DLL을 불러올 수 있다. 특정 디렉터리 ├─ program.exe ← 정상 실행 파일 └─ example.dll ← 악성 DLL 이 경우 program.exe 자체에는 문제가 없어도, 실행 과정에서 악성 example.dll 이 로드되면서 악성 코드가 실행된다. 정상 EXE + 악성 DLL ↓ 악성 코드 실행 DLL 사이드로딩이 헷갈리는 이유도 이 부분이다. 실행된 EXE만 보면 정상 파일이고, 전자서명도 정상일 수 있다. 그래서 파일 하나만 보고 판단하면 이상 징후를 놓칠 수 있다. 결국 중요한 건 실행 파일 자체가 정상인지보다 어디에서 실행됐는지, 어떤 DLL을 불러왔는지를 같이 보는 것 이다. 4. DLL 사이드로딩은 왜 가능한가? 프로그램이 DLL을 불러올 때 항상 DLL의 전체 경로를 직접 지정하는 것은 아니다. 경우에 따라서는 DLL 이름만 지정하고, Windows가 정해진 방식에 따라 해당 DLL을 찾는다. 공격자는 이 점을 이용해 정상 프로그램이 찾을 만한 이름의 DLL을 만들어 정상 EXE와 같은 위치에 두기도 한다. 공격자가 준비한 디렉터리 │ ├─ 정상 program.exe └─ 악성 example.dll 이 상태에서 프로그램이 실행되면 공격자가 준비한 DLL이 함께 로드될 수 있다. 다만 같은 폴더에 DLL이 있다고 해서 무조건 DLL 사이드로딩이 발생하는 것은 아니다. 프로그램이 DLL을 어떤 방식으로 불러오는지에 따라 달라지기 때문에 실제 분석에서는 해당 프로세스가 어떤 DLL을 어떤 경로에서 로드했는지 를 직접 확인해야 한다. 5. 실제 분석한 샘플에서의 DLL 사이드로딩 먼저 전체 공격 흐름을 보면, DLL 사이드로딩은 초기 스크립트 실행 이후 정상 printui.exe 와 악성 DLL을 위장 경로에 배치하고 실행하는 과정에서 발생했다. 분석한 샘플의 전체 공격 흐름 이 중 DLL 사이드로딩이 발생한 부분을 자세히 보면, 정상 printui.exe 를 위장 경로에 복사한 뒤 악성 파일을 printui.dll 로 변경하고 printui.exe 를 실행하는 흐름을 확인할 수 있었다. printui.exe 와 printui.dll 이 배치되고 실행되는 과정 이 과정에서 사용된 printui.exe 는 Microsoft에서 서명한 정상 Windows 실행 파일이었다. Process Explorer에서 확인한 printui.exe 의 정상 서명 정보 그런데 실행 위치를 확인해보니 일반적인 System32 경로가 아니라, C:\Windows \System32 처럼 후행 공백이 포함된 위장 경로에 파일이 복사돼 있었다. 같은 디렉터리에는 printui.dll 도 함께 존재했다. 위장 디렉터리 │ ├─ printui.exe ← 정상 실행 파일 └─ printui.dll ← 악성 DLL 이후 printui.exe 가 실행되면서 같은 위치의 printui.dll 을 로드했고, 여기서부터 악성 행위가 이어졌다. printui.exe 실행 ↓ printui.dll 로드 ↓ DLL Side-Loading ↓ 후속 악성 행위 printui.exe 자체만 보면 Microsoft에서 서명된 정상 파일이기 때문에 크게 이상해 보이지 않았다. 하지만 실행 경로를 확인해보니 정상 System32가 아닌 위장 경로에서 실행되고 있었고, 같은 위치의 악성 printui.dll 을 로드한 뒤 추가 행위까지 이어지고 있었다. 즉, printui.exe 하나만 봤을 때보다 비정상 실행 경로 + 악성 DLL + 실제 DLL 로드 를 함께 봐야 전체 흐름을 파악할 수 있었다. 6. DLL 사이드로딩 분석 시 확인한 부분 Procmon에서 전체 실행 흐름을 확인해보면 DLL 사이드로딩 앞뒤로 여러 이벤트가 연결되어 있었다. Procmon에서 확인한 DLL 사이드로딩 전후의 프로세스 흐름 1) 실행 경로 이번 샘플에서는 printui.exe 가 정상 System32가 아닌 위장 경로에서 실행되고 있었기 때문에, 먼저 실행 경로를 확인했다. 정상 Windows 파일이라도 평소와 다른 경로에서 실행됐다면 확인해볼 필요가 있다. 특히 아래와 같은 위치에서 실행된다면 한 번 더 확인해볼 필요가 있다. Temp AppData Downloads 이동식 저장장치 정상 시스템 경로와 비슷하게 만든 폴더 파일 이름이 정상이어도 실행 경로까지 같이 확인해야 한다. 2) 로드된 DLL 다음으로 실제 어떤 DLL을 불러왔는지 확인했다. 특히 아래와 같은 부분을 같이 확인했다. 실행 파일과 같은 비정상 경로의 DLL 서명되지 않은 DLL 정상 DLL과 이름은 같지만 경로나 해시가 다른 경우 실행 직전에 생성된 DLL 여기서 중요한 건 같은 폴더에 DLL이 있다는 사실만으로 판단하지 않는 것이다. 실제로 해당 프로세스가 그 DLL을 로드했는지까지 확인해야 한다. 3) DLL 생성·변경 시점 DLL이 언제 생겼는지도 같이 봤다. 예를 들어 아래처럼 짧은 시간 안에 이어진다면 흐름을 의심해볼 수 있다. 스크립트 실행 ↓ 정상 printui.exe 복사 ↓ 악성 파일 복사 ↓ printui.dll로 이름 변경 ↓ printui.exe 실행 ↓ 악성 DLL 로드 EDR에서 이런 이벤트를 시간 순서대로 따라가면, 개별 이벤트만 봤을 때는 놓치기 쉬운 흐름을 확인할 수 있다. 4) 이후 행위 DLL 사이드로딩 자체가 끝은 아니다. 내가 분석한 샘플에서도 이후 추가 파일 생성, 서비스 등록, 예약 작업 등록 같은 행위가 이어졌다. 그래서 DLL 로드까지만 보고 끝내기보다는 이후 어떤 프로세스가 실행됐고, 어떤 설정이 추가됐는지까지 같이 확인하는 게 필요했다. 7. 마무리 이번 프로젝트에서 DLL 사이드로딩을 분석하면서 가장 인상 깊었던 건 정상 파일도 공격에 이용될 수 있다는 점 이었다. printui.exe 자체는 정상 파일이었지만, 실행된 위치와 함께 로드한 DLL을 같이 보면 정상적인 실행이라고 보기 어려웠다. 결국 DLL 사이드로딩을 볼 때는 파일 하나만 정상/악성으로 나누기보다, 어디에서 실행됐는지, 어떤 DLL을 불러왔는지, 그리고 이후 어떤 행위가 이어졌는지를 함께 보는 게 중요했다. 이번 분석을 통해 EDR 로그에서도 개별 이벤트 하나만 보기보다, 여러 이벤트를 시간 순서와 프로세스 관계로 연결해서 보는 게 더 중요하다는 점을 다시 느꼈다.
안녕하세요 Goldy입니다:) 이번 주차에는 디지털 증거의 수집 방식과 파일 시스템 구조를 학습했습니다. 1. 디스크 이미징 디스크 이미징은 저장매체 전체를 바이트 단위로 복제해 하나의 파일로 저장 하는 작업이다. 관련 용어를 정리하면 다음과 같다. 용어 의미 디스크 이미지 저장매체의 데이터를 바이트 단위로 복제한 파일 디스크 이미징 원본에서 디스크 이미지를 생성하는 과정 디스크 복제 원본과 동일한 데이터를 가진 복사본 매체를 만드는 것 복사 파일·폴더 단위로 데이터를 옮기는 것 핵심은 '복사'와 '이미징'의 차이다. 복사는 눈에 보이는 파일만 가져오지만, 이미징은 삭제된 흔적과 빈 공간까지 통째로 떠온다. 포렌식에서 원본을 직접 분석하지 않고 이미지 사본으로 분석하는 이유는 원본의 무결성을 지키기 위해서 다. 2. 메모리(RAM)와 메모리 덤프 메모리(RAM)는 CPU가 빠르게 연산하도록 데이터를 임시로 올려두는 공간 이다. 강의에서는 요리에 비유했는데, CPU는 재료를 써는 칼, 메모리는 재료를 올려두는 도마, 프로그램·데이터는 요리 재료에 해당한다. 도마 크기가 정해져 있어 모든 재료를 한 번에 못 올리듯, 메모리도 큰 프로그램을 한꺼번에 담을 수 없다. 여기서 프로그램과 프로세스를 구분해야 한다. 프로그램 : 하드디스크에 저장된 실행 코드 프로세스 : 실행 중인 프로그램 (실행 상태, 할당받은 자원 포함) 메모리에는 실행 중 열어본 파일이나 입력한 비밀번호가 남기도 해서 포렌식에서 가치가 높다. 이 메모리 데이터를 파일로 저장하는 것이 메모리 덤프 다. 단, 메모리는 실시간으로 변하므로 덤프를 수행한 시점의 데이터 만 결과로 남는다는 점이 중요하다. 이 덤프 파일을 분석하는 것이 메모리 포렌식이다. 3. 해시 함수 해시 함수는 임의 길이의 데이터를 고정된 길이의 값 으로 변환하는 함수이고, 그 결과가 해시값이다. 강의에서는 HashCalc라는 프로그램으로 직접 실습했다. 실습에서 확인한 핵심은, 같은 데이터는 입력 형태가 달라도 같은 해시값 이 나온다는 점이었다. HashCalc에서 Data Format을 File로 두고 파일을 넣었을 때와, Text로 두고 같은 내용의 텍스트를 넣었을 때 해시값이 동일하게 나오는 걸 확인했다. 반대로 데이터가 조금만 바뀌면 해시값이 완전히 달라지는데, 이를 눈사태 효과(Avalanche Effect) 라고 한다. 해시 함수의 종류 알고리즘 특징 MD5 출력 128비트(16바이트). 길이가 짧아 충돌 저항성이 낮음 SHA-1 MD5 이후 쓰였으나 취약점 발견으로 현재는 잘 안 씀 SHA-2 (224/256/384/512) 현재 가장 널리 사용. 숫자는 출력 길이를 의미 SHA는 1993년 미국 국가안보국(NSA)이 설계해 NIST 표준으로 지정됐다. 해시값 길이가 짧을수록 안전성(충돌 저항성)이 낮다는 점이 포인트다. 4. 데이터 표현 - 엔디언과 인코딩 리틀 엔디언 데이터를 메모리에 저장할 때 작은 바이트부터 저장 하는 방식이다. 헥스 에디터(HxD)로 값을 읽을 때 바이트 순서를 알아야 데이터를 제대로 해석할 수 있어서, 포렌식에서 기본적으로 알아야 하는 개념이다. 인코딩 데이터를 정해진 규칙에 따라 특정 형식으로 변환하는 것이다. (암호화와 달리 비밀 유지가 목적이 아니라 형식 변환이 목적이다.) ASCII 인코딩 : ASCII 테이블에 따라 값을 문자로 변환 Base64 인코딩 : 바이너리 데이터를 64개 문자로 표현 UTF-8 인코딩 : 가변 길이 방식. 현재 웹에서 가장 많이 사용 5. 파일 시그니처 파일 시그니처는 파일의 형식을 식별하는 데이터 다. 프로그램은 파일만 봐서는 형식을 알 수 없는데, 시그니처(Magic Bytes)를 확인하면 실제 형식을 구별할 수 있다. 파일 확장자는 이름에 붙는 표시일 뿐이라 쉽게 바꿀 수 있지만, 시그니처는 파일 내부에 있어 더 신뢰할 수 있다. 6. 저장장치와 인터페이스 디지털 포렌식에서는 대상 기기를 분해해 저장장치를 식별하고 데이터를 꺼내야 하므로, 각 장치의 특성을 알아야 한다. 저장장치 특징 HDD 플래터가 회전하고 헤드가 데이터를 읽는 방식 SSD 반도체로 데이터를 저장, HDD보다 빠름 USB 플래시 드라이브 USB 규격으로 데이터 저장 SD카드 USB보다 작고 가벼운 저장장치 저장장치를 분석 장비에 연결하는 방식이 인터페이스 다. 과거 HDD용 IDE에서 시작해, 더 빠르고 안정적인 SATA, 주변장치 연결용 PCI/PCIe, 범용 USB 등으로 발전했다. 디지털 포렌식 도구 증거 수집에는 전용 장비가 쓰인다. 디스크 이미지 장비, 쓰기 방지 장치 (원본 변조 방지), 패러데이 백 (외부 네트워크 신호 차단으로 원격 삭제 방지) 등이 대표적이다. 이번 주차의 핵심은 디지털 증거의 수집과 무결성 보존 이었습니다. 감사합니다!