1. 캐스케이드 (Cascade) CSS를 작성하다 보면 하나의 HTML 요소에 서로 다른 스타일 규칙이 동시에 적용될 수 있습니다. 이때 브라우저는 정해진 우선순위에 따라 어떤 값을 화면에 적용할지 결정합니다. 이 과정을 캐스케이드(Cascade) 라고 합니다. 예를 들어 같은 문단에 다음 두 규칙이 적용되면, 두 규칙의 다른 우선순위 조건이 같을 때 나중에 작성한 값이 적용됩니다. p { color: red; } p { color: blue; } 이 경우 문단 글자색은 파란색입니다. 캐스케이드의 주요 판별 기준 기초적으로 캐스케이드의 우선순위를 중요도와 출처, 명시도(특수성), 코드 순서 로 나누어 이해할 수 있습니다. 실제 브라우저는 캐스케이드 레이어와 같은 기준도 함께 확인하므로, 코드 순서는 앞의 우선순위 조건이 같을 때 최종적으로 비교됩니다. 1. 중요도와 스타일 출처 CSS 규칙에는 브라우저 기본 스타일(User Agent), 사용자 스타일, 개발자가 작성한 스타일이 있습니다. 일반 선언끼리는 보통 개발자 스타일이 사용자 스타일보다 우선하고, 사용자 스타일은 브라우저 기본 스타일보다 우선합니다. 속성값 뒤에 !important 를 붙이면 일반 선언보다 우선합니다. 하지만 !important 가 출처와 관계없이 무조건 가장 강한 것은 아닙니다. 중요 선언끼리는 우선순위가 다시 출처에 따라 달라지며, 브라우저의 중요 선언이나 사용자의 중요 선언은 개발자의 중요 선언보다 우선할 수 있습니다. CSS 전환 중인 값처럼 별도로 높은 우선순위를 갖는 경우도 있습니다. p { color: blue; } p { color: red !important; } 위 예시에서는 !important 가 붙은 red 가 적용됩니다. 다만 !important 를 자주 사용하면 스타일의 우선순위를 추적하기 어려워지므로 꼭 필요한 경우에만 사용하는 편이 좋습니다. 인라인 스타일( style="..." )은 일반적인 스타일시트의 일반 선언보다 우선하지만, 이것도 모든 CSS 선언을 무조건 이기는 것은 아닙니다. 특히 !important 가 붙은 선언과의 우선순위도 함께 따져야 합니다. 2. 명시도(특수성, Specificity) 중요도 조건이 같으면 선택자가 얼마나 구체적인지 비교합니다. 특수성은 보통 다음 세 항목의 묶음으로 나타냅니다. 선택자 종류 특수성 항목 예시 ID 선택자 ID 개수 #header → (1, 0, 0) 클래스·속성·의사 클래스 해당 개수 .btn , [type="text"] , :hover → (0, 1, 0) 태그·의사 요소 해당 개수 div , ::before → (0, 0, 1) 세 항목은 왼쪽부터 차례로 비교합니다. 따라서 ID 항목이 더 큰 선택자는 클래스나 태그 개수가 많더라도 우선합니다. 조합자( , > , + , ~ )와 전체 선택자( * )는 특수성을 더하지 않습니다. 선택자 계산 특수성 h1 태그 1개 (0, 0, 1) .box .title 클래스 2개 (0, 2, 0) #nav ul li ID 1개, 태그 2개 (1, 0, 2) a.btn:hover 클래스 2개, 태그 1개 (0, 2, 1) 예를 들어 다음 규칙은 같은 요소에 적용될 때 ID 선택자를 포함한 #title 쪽이 더 높은 특수성을 갖습니다. .title { color: blue; } #title { color: green; } 특수성은 흔히 점수라고 부르지만, (1, 0, 0) 을 (0, 100, 0) 처럼 단순 합산해 비교하지 않습니다. 각 자릿수를 왼쪽부터 비교합니다. 인라인 스타일은 이 선택자 특수성 표에 단순히 (1, 0, 0, 0) 으로 더하는 방식이 아니라 별도의 우선순위로 다룹니다. !important 도 특수성 점수가 아니라 중요도를 나타냅니다. 3. 코드 순서 (Source Order) 중요도와 출처, 레이어, 특수성이 모두 같으면 나중에 작성된 선언이 적용됩니다. h1 { color: red; } h1 { color: blue; } 두 선택자는 모두 h1 이라 특수성이 같습니다. 따라서 아래쪽에 나중에 작성된 color: blue 가 최종 적용됩니다. 2. 캐스케이드 우선순위 규칙 (Specificity) 선택자끼리 명시도를 비교할 때는 (ID, 클래스·속성·의사 클래스, 태그·의사 요소) 순서로 셉니다. 왼쪽 항목부터 비교하고, 앞 항목이 같을 때 다음 항목을 비교합니다. 계산 예시 h1 → 태그 1개: (0, 0, 1) .box .title → 클래스 2개: (0, 2, 0) #nav ul li → ID 1개, 태그 2개: (1, 0, 2) a.btn:hover → 클래스 2개, 태그 1개: (0, 2, 1) 예를 들어 (1, 0, 0) 은 (0, 99, 99) 보다 우선합니다. ID 항목이 클래스와 태그 항목보다 왼쪽에 있기 때문입니다. 반대로 같은 항목의 개수가 같으면 다음 항목과 코드 순서를 차례로 비교합니다. 종합 예시 <h1 id="title" class="main-title" style="color: purple;"> CSS 캐스케이드 테스트 </h1> h1 { color: gray; } .main-title { color: blue; } #title { color: green; } .main-title { color: orange; } 위 코드에서 인라인 스타일은 일반적인 스타일시트의 일반 선언보다 우선하므로 글자색은 보라색입니다. 인라인 스타일을 제거하면 ID 선택자인 #title 의 초록색이 적용됩니다. ID 규칙도 제거하면 클래스 선택자가 태그 선택자보다 우선하며, 두 .main-title 규칙끼리는 나중에 작성된 주황색이 적용됩니다. 정리 캐스케이드는 여러 CSS 선언 중 최종 스타일을 결정하는 과정입니다. 먼저 출처와 중요도 같은 상위 조건을 비교하고, 선택자의 특수성을 확인합니다. 이 조건들이 같을 때 소스 순서가 마지막 판단 기준이 됩니다. 따라서 스타일이 예상과 다르게 적용되면 무조건 !important 를 추가하기보다, 어떤 선언들이 충돌하는지 확인하고 우선순위를 차례로 비교하는 것이 좋습니다. 참고 자료 W3C CSS Cascading and Inheritance MDN: Specificity
파이썬 예외처리 제대로 이해하기 코드를 짜다 보면 생각지도 못한 곳에서 프로그램이 멈추는 일이 생긴다. 숫자가 들어올 줄 알았던 자리에 문자열이 들어오기도 하고, 있어야 할 파일이 없기도 하다. 예외처리는 이런 상황에서 프로그램이 그냥 죽어버리지 않고 내가 정한 방식대로 대응하게 만드는 장치다. 이번 글에서는 기본 문법부터 예외 계층, 사용자 정의 예외, 그리고 3.11 이후에 추가된 기능까지 순서대로 정리해 봤다. 1. 예외와 문법 오류는 다르다 먼저 헷갈리기 쉬운 부분부터 짚고 넘어가자. 문법 오류(SyntaxError)는 코드를 해석하는 단계에서 걸린다. 실행 자체가 안 되기 때문에 예외처리로 잡을 수 없다. 예외는 문법은 맞는데 실행 중에 문제가 생기는 경우다. 10 / 0 을 하면 ZeroDivisionError, int("abc") 를 하면 ValueError, 없는 파일을 열면 FileNotFoundError가 난다. 예외가 발생하면 파이썬은 호출 스택을 거슬러 올라가면서 이걸 처리해 줄 except 를 찾는다. 끝까지 못 찾으면 프로그램이 종료되고 우리가 흔히 보는 Traceback이 찍힌다. 2. 기본 구조와 실행 흐름 try: value = int(user_input) # 예외가 날 수 있는 코드 except ValueError as e: print(f"숫자가 아닙니다: {e}") # 해당 예외가 났을 때 else: print(f"변환 성공: {value}") # 예외가 없을 때만 실행 finally: print("항상 실행") # 성공이든 실패든 실행 블록별로 하는 일은 이렇다. try : 감시할 코드. 범위는 최대한 좁게 잡는 게 좋다. except : 특정 예외를 처리한다. 여러 개를 둘 수 있고 위에서부터 차례로 검사한다. else : try가 성공했을 때만 실행된다. 성공 이후의 작업을 try 밖으로 빼두면, 원래 잡으려던 게 아닌 예외까지 같이 잡히는 걸 막을 수 있다. finally : 파일 닫기나 DB 연결 해제 같은 정리 작업을 둔다. 중간에 return 이 있어도 실행된다. 여러 예외를 한 번에 잡고 싶으면 튜플로 묶으면 된다. except (ValueError, TypeError) as e: ... 3. 예외 계층 구조를 알아야 하는 이유 파이썬의 예외는 클래스 상속 구조로 되어 있다. BaseException ├── SystemExit, KeyboardInterrupt, GeneratorExit └── Exception ├── ArithmeticError → ZeroDivisionError ├── LookupError → KeyError, IndexError ├── OSError → FileNotFoundError, PermissionError └── ValueError, TypeError ... 이 구조를 알면 자연스럽게 따라오는 원칙이 몇 가지 있다. 첫째, 구체적인 예외를 위에 쓴다. except Exception 을 먼저 써버리면 그 아래에 있는 except KeyError 는 영원히 실행되지 않는다. 둘째, 아무것도 지정하지 않은 except: 는 쓰지 않는다. 이렇게 하면 KeyboardInterrupt 까지 잡혀서 Ctrl+C로도 프로그램이 안 꺼지는 상황이 생긴다. 셋째, except Exception: pass 처럼 예외를 그냥 삼키지 않는다. 당장은 조용해서 좋아 보여도, 나중에 버그가 어디서 났는지 찾을 방법이 사라진다. 4. 예외를 직접 발생시키기: raise와 예외 체이닝 조건에 맞지 않을 때 직접 예외를 던질 수도 있다. def withdraw(balance, amount): if amount > balance: raise ValueError("잔액이 부족합니다") return balance - amount 어떤 예외를 잡아서 더 의미 있는 예외로 바꿔 던지고 싶을 때는 raise ... from ... 을 쓴다. try: price = float(row["close"]) except (KeyError, ValueError) as e: raise DataParseError(f"종가 파싱 실패: {row}") from e from e 를 붙여두면 원래 원인이 Traceback에 같이 남기 때문에 왜 실패했는지 추적하기가 훨씬 쉽다. 원인을 일부러 숨기고 싶다면 from None 을 쓰면 된다. 잡은 예외를 손대지 않고 그대로 다시 던질 때는 인자 없이 raise 만 쓴다. 5. 사용자 정의 예외 내 코드의 상황에 맞는 예외를 직접 만들어 두면 코드가 무슨 의도로 쓰였는지 훨씬 잘 드러난다. class DataPipelineError(Exception): """파이프라인 공통 상위 예외""" class DataParseError(DataPipelineError): pass class MissingDataError(DataPipelineError): pass 공통 상위 클래스를 하나 두는 게 포인트다. 이렇게 하면 호출하는 쪽에서 except DataPipelineError 로 한꺼번에 잡을 수도 있고, 필요하면 세부 예외만 골라서 잡을 수도 있다. 라이브러리들이 많이 쓰는 방식이기도 하다. 6. with문으로 정리 작업 맡기기 try/finally 로 직접 자원을 닫는 대신 with 를 쓰면 예외가 나더라도 알아서 정리된다. with open("prices.csv", encoding="utf-8") as f: data = f.read() # 블록을 빠져나오면 예외가 났든 안 났든 파일은 닫혀 있다 내부적으로는 __exit__ 메서드가 예외 정보를 넘겨받아 정리를 처리한다. 직접 만들어 쓰고 싶다면 contextlib.contextmanager 데코레이터가 편하다. 특정 예외는 그냥 무시해도 되는 상황이라면 contextlib.suppress(FileNotFoundError) 처럼 쓰는 것도 깔끔하다. 7. EAFP vs LBYL 예외를 다루는 스타일에는 크게 두 가지가 있다. 방식 의미 예시 LBYL (Look Before You Leap) 먼저 확인하고 실행 if key in d: x = d[key] EAFP (Easier to Ask Forgiveness than Permission) 일단 해보고 실패하면 처리 try: x = d[key] / except KeyError: 파이썬에서는 전통적으로 EAFP를 더 선호한다. 확인하고 실행하는 사이에 상태가 바뀌는 문제를 피할 수 있기 때문이다. 예를 들어 파일이 있는지 확인한 직후에 다른 프로세스가 그 파일을 지워버리면 확인한 의미가 없어진다. 다만 예외가 아주 자주 발생하는 상황이라면 예외 처리 비용이 쌓이기 때문에 LBYL이 오히려 빠를 수 있다. 결국 상황을 보고 고르면 된다. 8. Python 3.11부터 추가된 기능 ExceptionGroup과 except* 비동기 작업처럼 여러 예외가 동시에 터질 수 있는 상황을 위해 생겼다. try: raise ExceptionGroup("여러 오류", [ValueError("a"), KeyError("b")]) except* ValueError as eg: print("ValueError 처리:", eg.exceptions) except* KeyError as eg: print("KeyError 처리:", eg.exceptions) 일반 except 와 달리 except* 는 그룹 안에서 해당하는 예외만 골라서 처리하고, 나머지는 다음 except* 로 넘긴다. add_note() 예외에 맥락 정보를 덧붙일 수 있다. Traceback을 볼 때 어느 데이터에서 문제가 났는지 바로 알 수 있어서 꽤 유용하다. except ValueError as e: e.add_note(f"문제 행 번호: {i}") raise 9. 실무에서 기억해 둘 것들 마지막으로 실제로 코드를 짤 때 챙기면 좋은 것들을 모아봤다. try 블록에는 예외가 날 만한 줄만 넣는다. 처리할 수 있는 예외만 잡고, 처리할 수 없으면 다시 던진다. 로그는 logging.exception("메시지") 로 남기면 Traceback까지 같이 기록된다. 데이터를 반복 처리할 때는 한 행의 실패 때문에 전체가 멈추지 않도록 행 단위로 잡고, 실패한 건 따로 모아둔다. 정리 작업은 finally 보다 with 를 먼저 떠올린다. 4번은 결측치나 이상값이 섞인 대량 데이터를 다룰 때 특히 쓸모가 많다. errors = [] for i, row in enumerate(rows): try: process(row) except DataPipelineError as e: errors.append((i, str(e))) print(f"실패 {len(errors)}건") 마치며 예외처리는 단순히 에러 메시지를 숨기는 기술이 아니라, 실패했을 때 프로그램이 어떻게 행동할지를 설계하는 일이다. 어떤 예외를 잡고 어떤 예외는 위로 올려보낼지, 실패한 정보를 어떻게 남길지를 고민하다 보면 코드가 한결 단단해진다. 참고 자료 Python 공식 튜토리얼 - Errors and Exceptions Python 공식 문서 - Built-in Exceptions PEP 654 - Exception Groups and except* PEP 3134 - Exception Chaining and Embedded Tracebacks
웹사이트는 HTML로 뼈대 를 세우고, CSS로 옷 을 입히며, JavaScript로 생명력(기능/동작) 을 불어넣습니다. HTML (HyperText Markup Language) 뼈대와 구조 — 웹사이트의 '내용' 담당 "여기는 제목", "여기는 버튼이야", "여기는 이미지 자리야" CSS (Cascading Style Sheets) 꾸미기와 디자인 — 웹사이트의 '겉모습' 담당 "제목은 빨간색", "버튼은 둥글게", "이미지는 오른쪽에 배치해줘" JavaScript (JS) * 상호작용과 동작 * — 웹사이트의 '행동' 담당 "클릭하면 로그인 창을 띄워줘", "데이터를 서버로 보내줘"
렌더링 (Rendering)은 웹 브라우저의 본질과 브라우저가 일하는 과정을 의미하며, 서버로부터 브라우저가 받은 코드(텍스트)를 화면으로 그리는 과정 을 말한다. 웹 브라우저의 본질 · Browse (가볍게 둘러보다, 훑어보다): 전 세계에 흩어진 정보를 편하게 둘러보게 해 주는 도구 · 번역기: 서버가 주는 코드(텍스트)를 해석해서 이해 가능 하도록 우리가 보는 화면으로 번역 브라우저가 일하는 과정 (Rendering 구조) 브라우저 주소창에 www.naver.com 을 치면 요청 (Request) — "네이버 메인 페이지 좀 보여줘!" 전달 — 서버가 HTML·CSS·JS 파일을 보냄 해석 및 렌더링 (Rendering) — 코드를 위에서부터 한 줄씩 읽으며 그림을 그림 완성 (Completion) — 익숙한 네이버 화면이 나타남
양자 하드웨어에 새 백엔드를 추가할 때는 문제·평가 지표·허용 오차·측정 예산·후처리 범위 를 먼저 정해야 한다. 동일한 문제를 풀었다고 해도 컴파일 결과, 측정 횟수, 후처리가 다르면 성능 차이의 원인을 설명하기 어렵다. 2026년 10월 6일 한국경제가 전한 회사 발표에 따르면 비드래프트는 퀀티넘 Nexus 이용 기업으로 선정돼 평가용 할당량 안에서 H-Series를 활용하고 기술지원·온보딩을 받게 된다. 후속 협업은 초기 이용 이후 논의할 예정이다. 발표 내용 아래 절차는 재현 가능한 실기 실험을 위한 제안이다. 비드래프트의 내부 구현이나 실제 수행 결과를 설명한 것이 아니다. AI 생성 개념 이미지. 실제 H-Series 장비 사진 또는 실험 데이터 시각화가 아니다. 1. 백엔드를 바꾸기 전에 무엇을 고정할 것인가 같은 소스 회로를 두 환경에 제출했다는 사실만으로 비교 조건이 같아지는 것은 아니다. 회로가 각 장비에 맞게 변환되면 연산 구성과 비용이 달라질 수 있다. 실험자는 최소한 다음 항목을 먼저 정해야 한다. 문제의 입력과 크기: 동일한 사례를 사용하는지 확인한다. 평가 지표: 성공 확률, 관측량의 오차 등 목표에 맞는 지표를 정한다. 허용 오차: 어느 정도의 차이를 의미 있는 결과로 볼지 정한다. 자원 예산: 총 측정 횟수 또는 사용 가능한 자원 기준을 정한다. 비교 범위: 양자 실행 외에 전처리와 후처리를 어디까지 포함할지 정한다. 공정한 비교의 기준은 연구 질문에 따라 달라질 수 있다. 같은 측정 예산에서 정확도를 비교할 수도 있고, 같은 목표 정확도에 도달하는 데 필요한 자원을 비교할 수도 있다. 어느 쪽이든 결과를 본 뒤 유리한 기준을 고르는 일을 피하려면 사전에 정의하는 편이 좋다. 2. 이상적 계산과 잡음 모형과 실기를 연결하기 검증 경로는 세 단계로 나눠 설계할 수 있다. 첫째, 다룰 수 있는 작은 문제에서 이상적 계산 결과를 확보한다. 알고리즘이 의도한 값을 내는지 먼저 점검하는 단계다. 문제를 선택할 때는 고전 계산으로 기준값을 구할 수 있는 범위를 택하면 초기 오류를 찾기 쉽다. 둘째, 잡음을 반영한 에뮬레이터 결과를 비교한다. 퀀티넘 공식 문서는 에뮬레이터가 장비의 물리·오차 모형을 사용하며 잡음 설정을 조정할 수 있다고 설명한다. 따라서 ‘시뮬레이션 결과’라는 표지만으로 이상적 계산인지, 잡음을 포함한 계산인지 판단하면 안 된다. 공식 에뮬레이터 설명 셋째, 실제 장비에 실행한 결과를 앞의 두 결과와 비교한다. 모형의 예측과 실기 측정의 간격이 크다면 원인을 분해해야 한다. 회로 변환, 측정량, 오차 모형의 설정을 차례로 검토하고 한 번에 여러 조건을 바꾸지 않는 편이 해석에 유리하다. 이 경로의 목적은 세 결과가 항상 같아야 한다고 요구하는 데 있지 않다. 어느 단계에서 차이가 생겼는지 관찰하고, 다음 실험으로 확인할 가설을 줄여 가는 데 있다. 실기 접근의 가치는 이 피드백을 얻을 수 있다는 점에 있다. 3. 결과 파일보다 먼저 정할 실험 기록 Nexus는 여러 계산 백엔드의 작업 관리와 실험 데이터 보관을 지원한다. 공식 소개에는 백엔드 정보, 설정, 변수를 함께 저장하는 기능도 설명돼 있다. 이런 기능은 기록을 남기는 기반이며, 비교에 필요한 항목을 선택하는 작업은 실험 설계의 일부다. Nexus 공식 소개 개발 관점에서는 실험 하나를 다음 정보가 연결된 단위로 관리할 수 있다. 입력: 문제 식별자, 데이터 버전, 생성에 사용한 난수 시드 회로: 원본 회로, 컴파일 결과, 변환 옵션 환경: 백엔드 식별자, 사용한 SDK와 컴파일러 버전 실행: 작업 식별자, 제출·완료 시각, 요청·완료된 측정 횟수 분석: 원시 측정값, 후처리 코드 버전, 제외한 데이터와 이유 결과: 중심값, 변동 범위, 기준값과의 차이 이 목록은 Nexus API의 필드 명세가 아니라 연구팀이 관리할 기록의 예시다. 플랫폼에서 자동으로 얻는 항목과 직접 남겨야 하는 항목을 구분해서 구현해야 한다. 장비 제공자가 공개하지 않는 정보는 비워 두고, 알 수 없는 상태 자체를 표시하는 편이 추정값을 채우는 것보다 낫다. 원시 측정값을 보존하면 분석 코드를 바꿨을 때 같은 데이터를 다시 계산할 수 있다. 최종 평균값만으로는 후처리 변경의 영향을 검토하기 어렵다. 4. 제한된 할당량을 실험 질문에 배분하기 평가용 접근에서는 사용할 수 있는 자원을 먼저 확인해야 한다. Nexus 자체의 CPU·저장 공간 할당량과 외부 하드웨어 제공자의 실행 제한은 구분되는 개념이다. 플랫폼 화면의 사용량 하나만 보고 모든 실험 비용을 판단하기 어렵다. Nexus 할당량 문서 작은 진단 실험으로 컴파일과 결과 수집 경로를 확인한 뒤, 다음 실행을 결정하는 데 필요한 측정을 한다. 이때 초기 데이터를 본 뒤 지표나 제외 기준을 바꾸었다면 그 변경도 기록해야 한다. 탐색 실험과 최종 평가를 구분하면 결과 해석이 명확해진다. 또한 자원 일부를 반복 검증에 남겨 두는 편이 좋다. 한 설정에 예산을 모두 사용하면 결과가 예상 밖일 때 원인을 확인할 여지가 줄어든다. 배분 비율은 회로와 연구 목표에 따라 달라지므로 보편적인 숫자를 정하기보다, 어떤 결과가 나오면 다음 실험을 실행할지 기준을 마련해야 한다. 5. 성공을 보고할 때 함께 공개할 정보 좋은 결과를 얻었을 때는 개선 폭을 보여 주는 그림과 함께 비교의 범위를 설명해야 한다. 기준 방법이 충분히 최적화됐는지, 각 방법의 목표 정확도가 같았는지, 성공한 실행만 골라 보여 주지는 않았는지 확인할 수 있어야 한다. 측정 횟수가 다르면 관측된 차이에 포함된 통계적 불확실성도 달라질 수 있다. 반복 결과와 불확실성을 같이 제시하고, 사용한 계산 방법을 밝혀야 숫자의 의미를 판단하기 쉽다. 실험 중 실패한 작업이나 제외한 측정이 있다면 그 처리 방식도 남길 필요가 있다. 속도를 주장할 때는 시간의 정의가 특히 중요하다. 장치에서 실제 계산한 시간과 사용자가 결과를 받기까지의 시간은 측정 범위가 다르다. 후자에는 대기, 데이터 이동, 고전 연산이 포함될 수 있다. 어떤 시간을 비교했는지 명시하면 후속 연구가 같은 기준으로 결과를 해석할 수 있다. 새 백엔드의 결과를 다른 사람이 이해하고 검증할 수 있도록 조건을 설명하는 일까지가 실험 설계다. FAQ 다른 백엔드로 옮겼다면 회로 코드만 공개해도 될까 코드에 더해 컴파일 조건과 실행 환경, 측정 횟수, 후처리 기준이 필요하다. 원본 회로가 같아도 실행에 사용된 연산 구성이 달라질 수 있으므로 컴파일 결과를 남기는 것이 비교에 도움이 된다. 에뮬레이터와 실기 결과가 다르면 실험에 실패한 것일까 차이는 추가 검증이 필요한 관찰값이다. 충분한 측정을 했는지, 모형과 실기의 설정이 대응하는지부터 확인해야 한다. 차이가 발생하는 조건을 재현 가능하게 설명했다면 후속 실험에 유용한 결과가 된다. 작은 실기 실험으로 산업적 양자 우위를 주장할 수 있을까 주장의 범위는 실험한 문제와 비교 조건에 맞춰야 한다. 작은 문제의 성공률과 대규모 문제의 계산 효율은 다른 검증 항목이다. 확장에 필요한 자원과 적절한 고전 기준선까지 확인해야 산업적 의미를 평가할 수 있다. 참고 자료 한국경제의 비드래프트 Nexus 이용 기업 선정 보도 , 2026.10.06 Quantinuum Nexus 소개 , 2024.07.31 Quantinuum Emulators , 2026.10.06 확인 Quotas in Nexus , 2026.10.06 확인
이 시리즈는 Databricks 환경에서 구축했던 항공/운송/물류 분야의 RAG Agent 구축 프로젝트 관련 내용을 정리하는 시리즈입니다. 요구사항과 관련하여 Databricks의 어떤 기술을 사용해 이를 충족하였으며, 수행 중 발생한 문제 상황을 어떻게 해결했는지 정리합니다. 실제 고객사에서 수행한 프로젝트와는 별도의 내용으로, 기술적인 내용만을 정리합니다. 1. 데이터 수집 개요( Data Ingestion ) 데이터 수집(Data Ingestion) 작업은 외부 데이터 소스로부터 최신 문서를 주기적으로 스캔하여, 변경되거나 신규로 추가된 문서를 감지하고 Databricks 환경(Unity Catalog 및 Managed Volume)에 안전하게 파일 형태로 다운로드 및 적재하는 파이프라인의 첫 번째 단계입니다. 이 파이프라인은 Databricks Workflow 상에서 각각 독립적인 Task 노트북으로 구성되어 동작합니다. 1.1. 전체 데이터 수집 및 적재 흐름도 1.2. 주요 태스크(Task) 정의 태스크명 역할 및 핵심 기능 Task #0. Initialize 파이프라인 구동에 필수적인 Delta Table(메타데이터, 이벤트 로그 등)을 부트스트랩하고 최신 스키마 상태로 유지 관리합니다. Task #1. Metadata Sync Source Storage를 스캔하여 현재 파일 목록의 스냅샷을 생성하고, 변경 감지 대상이 되는 Delta Table에 병합하여 변경 이력(CDF)을 발생시킵니다. Task #2. Content Export Task #1에서 발생시킨 변경 범위(Delta Version)를 기준으로 실제 본문 콘텐츠와 이미지 데이터를 추출하여 Managed Volume 경로에 병렬 적재합니다. 2. [Task #0] 파이프라인 환경 초기화 및 리셋(Pipeline Initialize) 수집 프로세스가 시작되기 전 필요한 스토리지 스키마를 동기화하고, 데이터 분석 오류나 수집 장애 등으로 인해 전체 인덱싱이 필요할 때 안전하고 영속적인 리셋을 수행하는 메커니즘을 다룹니다. Delta Lake와 Delta Table Databricks의 표준 고성능 테이블 포맷입니다. 일반적인 관계형 데이터베이스(RDB)나 Parquet 파일과 달리 트랜잭션 로그를 생성하여 데이터 무결성을 보장하고, 테이블 버전별 타임 트래블(이전 상태 복구) 및 변경 사항만 정밀 추적하는 기능(Change Data Feed)을 지원합니다. 2.1. bootstrap.py 모듈 및 리소스 부트스트랩 데이터 적재를 위한 Delta 테이블들의 최초 생성 및 구조 갱신에 관련된 책임을 지는 모듈입니다. 2.1.1. ensure_all(spark, names) 역할 : 파이프라인 실행에 필수적인 모든 Delta 테이블과 뷰를 자동으로 생성하는 메인 오케스트레이터입니다. 동작 로직 테이블 정보가 정의된 Names 객체( names )를 참조하여 내부 함수인 ensure_file_meta() , ensure_events() , ensure_log_run() 등을 순차적으로 호출합니다. 테이블 생성이 완료된 후에는 _ensure_columns() 함수를 이용해 최신 소스코드 스키마 정의와 데이터베이스 내 실물 스키마 간의 싱크를 강제로 유지합니다. 2.1.2. ensure_file_meta(spark, names) 역할 : 원본 파일의 메타데이터와 최종 상태 스냅샷을 보관하는 raws.file_meta 테이블을 생성합니다. 동작 로직 DeltaTable.createIfNotExists() API를 사용해 물리적인 테이블을 생성합니다. 테이블 속성(Properties)으로 delta.enableChangeDataFeed = true 를 주입하여 생성합니다. Change Data Feed (CDF) 기능이란? 데이터베이스 테이블에 발생하는 모든 변화(추가, 수정, 삭제)를 마치 Git 커밋 로그처럼 차례대로 기록해 두는 기능입니다. 이를 활성화해 두어야만 Task #2 단계에서 "지난 수집 주기 이후 새로 추가되거나 바뀐 문서만" 정확히 구분하여 효율적으로 다운로드할 수 있습니다. 핵심 관리 테이블 구조 및 스키마 요약 파이프라인 구동 및 관리를 위해 사용되는 3가지 핵심 Delta 테이블의 구조는 다음과 같습니다. 테이블 논리명 물리 테이블 경로 데이터 보존/적재 방식 핵심 속성 및 파티션 원천 파일 메타데이터 raws.file_meta 현재 최신 스냅샷 상태 유지 (MERGE) CDF 활성화 ( enableChangeDataFeed=true ) 수집 완료 이벤트 _meta.file_export_events 수집 성공/삭제 이벤트 누적 (Append-only) 파티션 키: event_date 수집 주기 실행 이력 _meta.cdf_export_run 수집 주기(Cycle)별 런타임 결과 적재 실행 메트릭 및 에러 추적 2.1.3. ensure_events(spark, names) / ensure_log_run(spark, names) 역할 : 작업 성공 로그 적재용 _meta.file_export_events 및 실행 상태 로깅용 _meta.cdf_export_run 테이블을 생성합니다. 동작 로직 ensure_events 는 파티션 키로 event_date 컬럼을 주입하여 생성함으로써 날짜별 쿼리 성능을 최적화합니다. 데이터 형식뿐만 아니라 스키마 정의에 기술된 주석(Comment) 정보까지 보존하여 데이터 카탈로그 명세서를 실시간으로 동기화합니다. 2.1.4. _ensure_columns(spark, table_name, schema) 역할 : 소스코드에 새로운 nullable 필드가 추가되는 등 설계 사양이 변경되었을 때, 기존의 테이블에 Alter 조작을 수행하는 스키마 진화(Schema Evolution) 도구입니다. 동작 로직 Spark API를 통해 대상 테이블의 실물 필드 세트를 가져와 schemas.py 에 선언된 스키마 필드 명세와 대조(Difference)합니다. 신규 컬럼이 발견되면 누락된 필드의 타입(DataType)과 코멘트 정보를 조합하여 ALTER TABLE {table_name} ADD COLUMNS (...) SQL 쿼리를 동적으로 조립하고 스파크 세션을 통해 실행합니다. # bootstrap.py - 스키마 동적 확장을 위한 ALTER SQL 실행 로직 col_defs = [] for f in missing: comment = (f.metadata.get("comment") or "").replace("'", "''") col_defs.append(f"`{f.name}` {f.dataType.simpleString()} COMMENT '{comment}'") spark.sql(f"ALTER TABLE {table_name} ADD COLUMNS ({', '.join(col_defs)})") 2.2. 전체 수집 및 인덱싱 리셋( full_reset.py ) TRUNCATE와 DELETE의 역할 분할 TRUNCATE (구조 유지 내용 소거) : 파싱( _parsed ), 청킹( _chunked ), 벡터 데이터( _vectorized ) 테이블의 경우 테이블의 권한 정보, 주석 메타데이터, 그리고 연동되어 작동 중인 AI Search Index 서비스가 깨지지 않도록 물리 구조는 그대로 두고 알맹이 데이터만 비우는 TRUNCATE TABLE 을 실행합니다. DELETE (선택적 소거) : 메타 테이블( file_meta , file_export_events )은 여러 카테고리의 데이터가 공존하므로, 특정 subject 범위만 골라 DELETE WHERE category IN (...) 구문을 수행해 대상을 정확히 구분하여 소거합니다. 안전장치 관리자의 실수로 인한 데이터 유실을 방지하기 위해 변수 파라미터 confirm 의 기본값을 "false" 로 고정합니다. 노트북 실행부 첫 머리에서 해당 파라미터가 정확히 "true" 문자열로 매칭되지 않을 경우 dbutils.notebook.exit() 를 호출하여 실행을 즉각 차단합니다. 3. [Task #1] 소스 스토리지 메타데이터 동기화(Metadata Sync) 지정된 소스 스토리지 내의 폴더 구조와 파일 목록을 조회하여 이전에 수집된 목록과 대조하고, 수정 및 신규 삽입에 해당하는 데이터 변경을 Delta Change Data Feed(CDF) 상에 각인하는 흐름입니다. 3.1. API 인증 및 크리덴셜 관리 ( SourceServiceFactory ) 3.1.1. from_sa_secret(dbutils, secret_scope, secret_key, delegate_email) 역할 : Databricks Secret Manager로부터 암호화된 소스 스토리지 어카운트의 프라이빗 키 JSON을 안전하게 꺼내와, 이를 기반으로 구글 클라이언트 라이브러리에 연동 가능한 인증 개체 및 서비스를 구성합니다. 동작 로직 : dbutils.secrets.get 메서드로 암호 키 페이로드를 가져온 뒤 JSON 역직렬화를 거쳐 Google Credentials 객체를 초기화합니다. 3.2. 수집 대상 디렉토리 스캔( FolderListingSource ) 3.2.1. FolderListingSource.fetch_snapshot(spark) 역할 : 소스 스토리지에 분산되어 있는 대상 디렉토리 ID들을 전체 조회하여 표준 수집 컬럼 규격에 맞는 단일 Spark DataFrame을 빌드합니다. 동작 로직 노트북 레벨에서 주입된 folder_config.json 을 읽어 active 상태인 폴더 ID 목록을 획득합니다. 각 폴더 정보(ID, Category, Subcategory)를 토대로 루프를 돌며 해당 폴더의 직속 파일 객체를 순차적으로 취합합니다. 획득된 파일 목록을 _rows_to_df() 헬퍼 메서드를 통해 file_meta_schema 스키마 규격으로 정규화하여 스파크 데이터프레임화합니다. 이 과정에서 각 파일의 고유 메타 정보에 카테고리 태그 및 한국 표준시(KST)로 변환된 변경 시점 시각 정보( modified_at )를 병합합니다. 3.2.2. StorageClient.list_files_in_folder(folder_id, mime_types) 역할 : 특정 폴더의 직속 하위 파일들을 페이지네이션을 처리하며 스캔합니다. 동작 로직 : 전달받은 mime_types 목록을 OR 구문으로 묶어 드라이브 조회 쿼리 스트링( q )을 작성합니다. ( 'folder_id' in parents and trashed=false and (mimeType='...' or ...) ) supportsAllDrives=True 설정을 통해 공유 드라이브를 포함한 모든 클라우드 저장 공간을 빠짐없이 스캔하며, nextPageToken 을 추적하여 단 하나의 누락 문서 없이 모든 하위 파일 데이터 구조를 리턴합니다. 3.3. 메타데이터 병합 적재( FileMetaMerger ) 3.3.1. merge(other_df) 역할 : 스토리지 스캔 데이터( other_df )를 기존의 메타 테이블( names.file_meta )에 물리적인 MERGE INTO 방식으로 반영하고, 그 이력 버전을 추적합니다. 핵심 병합 조건 및 로직 식별 키 조건 : t.file_id = s.file_id (기존 DB 테이블 t 와 소스 데이터프레임 s 매핑) 수집 격리 범위 조건( _scope_predicate ) : 카탈로그 데이터 정합성을 해치지 않기 위해 t.category IN (subject_categories) 조건을 병합 타깃 범위로 강제 지정합니다. 이를 통해 해당 영역의 데이터만 수정 및 매칭 비교 프로세스가 일어나며, 동일 테이블을 공유하는 다른 subject 행은 안전하게 보호됩니다. 고아 대상 자동 소거( whenNotMatchedBySourceDelete ) : 드라이브에서 삭제되었거나 타 폴더로 옮겨가면서 이번 스냅샷 소스 목록에서 제외된 수집 범위 내의 파일이 감지될 경우, Delta Engine에 의해 자동으로 감지되어 메타 데이터베이스에서 물리 삭제( DELETE )됩니다. # file_meta_merger.py - Delta Lake 병합(MERGE INTO) 조작부 (file_meta_dt.alias("t") .merge(source=other_df.alias("s"), condition=f"t.file_id = s.file_id AND {scope}") .whenMatchedUpdate( condition="t.modified_at < s.modified_at", set=self._update_set(other_df) ) .whenNotMatchedInsert(values=self._insert_set(other_df)) .whenNotMatchedBySourceDelete(condition=scope) .execute()) 메타데이터 테이블 MERGE 처리 조건표 변경 시나리오 병합 분기 조건 (Merge Clause) 결과 change_type DB 물리 동작 설명 신규 파일 생성 WHEN NOT MATCHED THEN INSERT INSERT 새로운 file_id 를 가진 행을 raws.file_meta 에 추가 삽입 기존 파일 내용 수정 WHEN MATCHED AND (t.modified_at != s.modified_at) THEN UPDATE UPDATE 원본의 수정 시각 정보와 다를 경우, 파일명 및 modified_at 등을 갱신 파일 삭제 또는 폴더 이탈 WHEN NOT MATCHED BY SOURCE AND (t.category IN (...)) THEN DELETE DELETE 수집 스코프 내 에 포함되어 있으나 업로드 스냅샷 소스에서 사라진 대상을 감지해 삭제 3.3.2. _collect_result(dt, pre_version) 역할 : 병합 작업 완료 후 발생한 Delta Transaction History 내역을 역추적하여 수집 통계를 집계하고 병합 결과를 반환합니다. 동작 로직 대상 Delta Table의 메타 히스토리 정보를 필터링하여 이전에 획득했던 pre_version 보다 큰 트랜잭션 버전을 조회합니다. 트랜잭션 내부의 operationMetrics 딕셔너리에서 numTargetRowsInserted , numTargetRowsUpdated , numTargetRowsDeleted 항목을 추출하여 가산(Sum)해 낸 뒤 이를 MergeResult 구조화된 데이터 클래스 객체로 가공해 리턴합니다. 3.4. MergeTaskValues (상태값 전파) set_from_merge_result() 메서드를 호출하여 FileMetaMerger 의 결과값( MergeResult )을 Databricks Task Values에 저장합니다. start_v 와 end_v 버전 번호가 전파되어 후속 다운로드 작업(Task #2)이 대상 테이블 전체를 풀 스캔하는 대신, 지정된 최소 Delta 버전을 타깃으로 하여 증분 변경 내역(CDF)만 선별적으로 다운로드할 수 있게 조율합니다. 4. [Task #2] Source Data 다운로드 및 Volume 적재(Content Export) 이전 단계인 Task #1에서 전파된 버전 정보를 추적하여 실제 변경사항이 존재하는 문서들의 콘텐츠를 직접 내려받고, 병렬 스레드를 사용해 최적화된 경로의 Databricks Volume에 파일로 저장합니다. 4.1. CDF 기반 변경분 추출( FileMetaCdfReader ) 4.1.1. read_changes(start_v, end_v) 역할 : 지정된 Delta Version 범위 내에서 발생한 테이블의 실시간 이벤트 로그를 Change Data Feed에서 가공 처리합니다. 동작 로직 .option("readChangeFeed", "true") 옵션을 활성화하여 Delta 테이블을 로드합니다. 작업 중 파일 상태 갱신 과정에서 중복 수집 및 불필요 로직 수행을 방지하기 위해 변경 타입( _change_type )이 insert , delete , update_postimage 에 해당하는 변화 시점 로그만을 격리 선별한 뒤 데이터프레임을 .cache() 처리합니다. # cdf_reader.py - Change Data Feed 기반 변경 행 조회 return (self._spark.read.format("delta") .option("readChangeFeed", "true") .option("startingVersion", start_v) .option("endingVersion", end_v) .table(tableName=self._table_name) .filter(F.col("_change_type").isin("insert", "delete", "update_postimage")) .cache()) 4.1.2. extract_export_targets(changes) 역할 : CDF 변경 히스토리 중 실제 물리적인 파일 다운로드 조작이 수반되어야 하는 파일 객체만을 별도로 필터링하여 드라이버 메모리에 수집합니다. 동작 로직 삭제( delete ) 이벤트를 배제하고 파일 신규 삽입( insert ) 및 수정 완료본( update_postimage ) 로그에 매칭되는 건들만 추출하여 드라이버 내부의 list[dict] 데이터 형태로 가공 반환합니다. 4.2. 파일 본문 다운로드 및 Volume 저장( Exporter ) 4.2.1. export_to_volume(file_meta, output_dir) 역할 : 특정 문서 파일에 대한 수집 요청 정보를 인계받아 해당 문서의 본문(content)을 다운로드하고 Unity Catalog Volume의 로컬 파일 시스템 레이아웃으로 실체화합니다. 동작 로직 _MIME_HANDLERS 매핑 딕셔너리에 지정된 규칙에 맞춰 문서 타입별 전용 메서드( fetch_document , fetch_spreadsheet , fetch_slide )를 호출하여 본문 전체의 페이로드를 입수합니다. 다운로드 성공 시 Volume 디렉터리 내에 {output_dir}/{folder}/{file_id}/ 형태로 하위 격리 디렉터리를 만들고, 해당 디렉터리 내에 document.json (순수 본문 페이로드)과 document_metadata.json (수집 카테고리 및 URL 등의 파일 메타데이터) 구조로 저장 처리합니다. 이후 ImageExporter 인스턴스를 즉각 구동하여 본문 내 이미지 데이터 저장을 시도합니다. 이때 이미지 저장 처리 도중 예외가 발생하더라도 문서 수집 자체는 영향이 없도록 프로세스를 예외 처리 블록으로 격리합니다. 4.2.2. 재시도 가드 데코레이터( _retry ) 역할 : API 통신 시 흔히 발생하는 일시적인 통신 단절 및 API Quota 초과 현상에 대해 자동 재시도 루프를 수행합니다. 동작 로직 tenacity 라이브러리를 기반으로 구현되어 있습니다. HTTP 상태 코드 중 429 (Too Many Requests), 500 , 502 , 503 , 504 에러나 ConnectionError 와 같은 전송망 에러 발생을 체크( _is_retryable )하여 최대 6회까지 동적 대기 시간 지터(Jittered Exponential Wait) 규칙에 따라 자동으로 지연 및 재실행을 보정 처리합니다. 4.3. 병렬 다운로드 실행 엔진( ParallelExporter ) 4.3.1. run(export_targets) 역할 : 다량의 수집 타깃 목록( export_targets )을 병렬 다중 스레드로 전환하여 다운로드 실행 시간을 최소화합니다. 동작 로직 파이프라인 매개변수로 공급된 max_workers 정밀 값을 가져와 ThreadPoolExecutor 스레드 풀을 선언합니다. 각 태스크 스레드는 드라이브 엑스포터의 export_to_volume 메서드를 참조하여 다운로드를 동시 수행하며, 수집 과정의 성공/실패 여부를 모아 취합한 수집 성공 로그 리스트( file_logs_rows )와 에러 건수를 최종 카운트하여 메인 드라이버에 리턴합니다. # export_runner.py - ThreadPoolExecutor 기반 병렬 다운로드 오케스트레이션 with ThreadPoolExecutor(max_workers=self._max_workers) as executor: futures = [executor.submit(self._exp