하루에도 수십 번 스마트폰 화면을 터치하지만, 그 '클릭 한 번' 뒤에서 어떤 마법이 벌어지는지 상상해 본 적 있으신가요? 혹시 개발자와의 회의 시간마다 쏟아지는 '프론트엔드', 'API', 'DB' 같은 외계어에 남몰래 식은땀을 흘려본 적이 있다면 정말 잘 찾아오셨습니다. IT 비전공자 지식을 쌓고 싶은 기획자, 마케터, 혹은 예비 개발자라면 이 글 하나로 충분합니다. 단 10분의 시간 투자로, 여러분의 머릿속에 파편처럼 흩어져 있던 IT 개념들을 하나의 완벽한 그림으로 완성해 드리겠습니다. 지금부터 우리가 매일 쓰는 앱 뒤에서 벌어지는 은밀하고 위대한 데이터의 여정을 시작하겠습니다. 1. 클릭 한 번에 숨겨진 거대한 미로 우리가 스마트폰 앱에서 '로그인' 버튼을 누르거나 웹사이트에서 '상품 검색'을 하는 행위는 1초도 안 되는 찰나에 끝납니다. 하지만 화면 너머 보이지 않는 세계에서는 거대한 시스템이 톱니바퀴처럼 맞물려 돌아가고 있죠. 이 복잡한 과정을 이해하는 가장 완벽한 방법은 바로 '식당'에 비유하는 것입니다. 웹과 앱이 작동하는 기본 뼈대인 3계층 아키텍처(3-Tier Architecture)를 식당의 모습에 빗대어 살펴볼까요? 자연스럽게 프론트엔드 백엔드 차이를 완벽하게 이해하실 수 있을 것입니다. 2. 한눈에 보는 3계층 아키텍처: 식당 주방에 빗대어 보기 프론트엔드(Frontend): 손님을 맞이하는 화려한 홀 식당의 문을 열고 들어가면 가장 먼저 화려한 인테리어와 메뉴판이 보입니다. IT 시스템에서 이곳이 바로 '프론트엔드'입니다. 주요 역할 : 웹 브라우저나 모바일 앱처럼 사용자의 눈에 직접 보이는 화면(UI)을 만듭니다. 핵심 기능 : 버튼 클릭, 화면 스와이프 등 사용자의 행동(이벤트)을 감지하고, "이 메뉴 주문할게요!"라고 뒷단에 요청을 보냅니다. (HTML, CSS, JavaScript 등으로 구현됩니다.) 백엔드(Backend): 주문을 처리하는 치열한 주방 홀에서 받은 주문은 주방으로 넘어갑니다. 손님 눈에는 보이지 않지만, 식당이 돌아가게 하는 핵심 두뇌 역할을 하는 곳이죠. 주요 역할 : 프론트엔드의 요청을 받아 비즈니스 로직을 처리합니다. 보안 중계자 : 프론트엔드가 식재료 창고(DB)에 직접 접근하면 도둑이 들 위험(보안 사고)이 큽니다. 그래서 백엔드 서버가 중간에서 권한을 확인하고 안전하게 요리를 완성하는 중계 역할을 수행합니다. (Java Spring, Python Django, Node.js 등이 쓰입니다.) 데이터베이스(DB): 모든 재료가 모인 철통 보안 창고 요리를 하려면 신선한 식재료가 필요하듯, 서버가 로직을 처리하려면 '데이터'가 필요합니다. 데이터베이스 구조 : 서비스의 모든 정보(회원 정보, 상품 목록 등)가 엑셀 시트처럼 체계적인 '테이블' 형태로 영구 저장되는 금고입니다. 구성 : 가로 한 줄을 '레코드', 세로 항목을 '컬럼'이라고 부르며 방대한 데이터를 오차 없이 관리합니다. (MySQL, Oracle 등이 대표적입니다.) 3. 데이터는 어떻게 대화하는가? (API와 CRUD) 각자의 구역이 정해졌다면, 이제 이들이 서로 어떻게 소통하는지 알아야 합니다. 홀의 직원과 주방의 요리사는 어떻게 대화를 나눌까요? 프론트엔드와 백엔드의 소통 창구, API와 JSON 프론트엔드가 백엔드에 요청을 보낼 때 사용하는 규격화된 창구가 바로 API(Application Programming Interface)입니다. 식당의 '주문서' 같은 역할이죠. 이때 주고받는 데이터는 주로 JSON이라는 가볍고 읽기 쉬운 텍스트 형식(포맷)으로 포장되어 오고 갑니다. 데이터베이스를 움직이는 마법의 주문, SQL 주방장(백엔드)이 창고 관리인(DB)에게 재료를 찾아오라고 시킬 때는 SQL이라는 전용 언어를 사용합니다. 앱에서 일어나는 모든 활동은 결국 데이터를 만들고(Create), 읽고(Read), 수정하고(Update), 삭제하는(Delete) CRUD 작업으로 요약할 수 있습니다. [핵심 요약: API와 CRUD 매핑 표] HTTP 요청 (API) CRUD 기능 데이터베이스 작업 (SQL) 실제 서비스 적용 예시 POST Create (생성) 새로운 데이터를 추가 (INSERT) 회원가입, 새 글 작성 GET Read (읽기) 저장된 데이터를 조회 (SELECT) 내 프로필 보기, 상품 검색 PUT Update (수정) 기존 데이터를 변경 (UPDATE) 비밀번호 변경, 닉네임 수정 DELETE Delete (삭제) 데이터를 완전히 제거 (DELETE) 회원 탈퇴, 게시글 삭제 (실무에서는 이 표 하나만 머릿속에 있어도 개발자와의 소통이 200% 원활해집니다!) 4. 실전! 데이터의 긴 여정 5단계 자, 이제 배운 내용을 종합해서 웹 서비스 동작 원리의 전체 흐름을 따라가 볼까요? 여러분이 '마이페이지 조회' 버튼을 클릭했다고 가정해 보겠습니다. 이벤트 발생 (프론트엔드): 사용자가 '조회' 버튼을 클릭합니다. API 호출 (요청): 프론트엔드는 HTTP 프로토콜이라는 도로를 타고 백엔드 서버에 GET 방식의 API 요청을 보냅니다. 로직 처리 및 SQL 실행 (백엔드): 백엔드 서버는 사용자의 권한을 확인한 뒤, DB에 보낼 SELECT SQL 명령을 만듭니다. 데이터 검색 (데이터베이스): DB는 거대한 테이블에서 요청받은 레코드(회원 정보)를 찾아내어 백엔드로 돌려줍니다. 응답 및 화면 렌더링 (프론트엔드): 백엔드는 이 데이터를 JSON 형태로 예쁘게 포장해 프론트엔드로 보냅니다. 프론트엔드는 이를 분석하여 우리 눈에 보이는 예쁜 마이페이지 화면으로 띄워줍니다. 5. 마무리: IT 지식이 여러분의 강력한 무기가 됩니다 프론트엔드에서 시작된 작은 클릭 하나가 백엔드를 거쳐 데이터베이스의 심장부까지 닿은 뒤, 다시 우리 화면으로 돌아오기까지의 여정. 어떠셨나요? 생각보다 복잡하지 않죠? 이제 여러분은 '프론트엔드가 화면을 그리고, 백엔드가 API를 통해 CRUD를 처리하며 DB를 조작한다'는 문장을 완벽하게 이해할 수 있게 되었습니다. 이 기본적인 IT 시스템 아키텍처에 대한 이해는 기획을 더 날카롭게 만들고, 마케팅 전략에 기술적 실현 가능성을 더해주며, 업무 효율을 폭발적으로 높여줄 강력한 무기가 될 것입니다. 오늘의 가이드가 여러분의 커리어에 든든한 날개가 되기를 바랍니다! 다음 스텝을 위한 질문: 여러분이 현재 속한 실무 환경에서 프론트엔드와 백엔드 중 어떤 파트의 개념이 가장 헷갈리셨나요? 댓글로 여러분의 경험이나 궁금한 점을 남겨주시면, 다음 포스팅에서 더 깊이 있는 인사이트로 찾아오겠습니다!
결과부터 선생님은 시트에 요청만 적음. 나머지는 전부 알아서 돌아감. 1. 선생님이 요청표에 적음 공유 구글 시트의 월별 요청표 날짜, 교시 칸에 이름이랑 "2학년 4반 수업 지원"처럼 적으면 끝 내가 근무 안 하는 시간은 회색이라 애초에 못 적음 2. 몇 초 뒤 내 폰 캘린더에 등록됨 실제 내 폰(갤럭시) 캘린더 화면임 "[디지털튜터] 1교시 ○○○ - 2학년 2반 수업 지원" 형태로 바로 들어감 선생님이 요청을 고치면 일정도 고쳐지고, 지우면 일정도 지워짐 수업 시간이 지나면 앞에 [완료]가 자동으로 붙음 일정을 눌러 보면 요청 선생님, 요청사항, 처리상태가 같이 들어 있음 10분 전에 폰 알림이 옴 새 요청이 들어오면 메일로도 알려줌 3. 월말 서류가 알아서 작성돼서 메일로 옴 매월 1일 아침, 지난달 출근기록부, 활동일지, 급식확인서 PDF 3장이 폰 메일로 옴 10월 1일 오전 8시 24분에 9월 서류가 실제로 도착함 메일 안에서 바로 PDF 미리보기가 뜸 출퇴근 시간은 근무일정에서, 비고는 그달 요청이랑 정보 수업 시간표에서 채움 총 근무시간(59.5시간)도 계산돼 있고, 작성자 서명까지 찍혀서 옴 나는 인쇄해서 내기만 하면 됨 결론은 이거임. 선생님이 요청사항을 적는 것만으로 캘린더 등록, 알림, 월말 서류까지 전부 자동으로 됨. 원래는 이랬음 중학교에서 디지털튜터로 일하고 있음. 선생님이 요청한 교시에 들어가서 기기 세팅이나 수업 보조를 함. 자동화 전에는 전부 손으로 했음. 요청 시트를 수시로 열어서 새 요청이 있는지 확인 보이면 캘린더에 옮겨 적기 처리상태 드롭다운 바꾸기 월말엔 근무표랑 요청 기록을 대조하면서 출근기록부, 활동일지, 급식확인서를 손으로 채우기 빼먹으면 수업에 못 들어가거나 서류가 틀림. 만든 것 서버 없이 구글 시트에 붙은 Apps Script 하나로 돌아감. 비용 0원. 시트 탭 구성 월별 요청표: 선생님이 적는 곳 근무일정: 학교에서 받은 근무표를 옮겨 둔 탭. 날짜, 출근, 퇴근, 서류에 찍힐 활동 교시표: 교시별 시작, 종료 시간 정보수업시간표: 내가 기본으로 들어가는 정보 수업 설정: 이름, 이메일, 학교명, 서명 이미지 같은 값 코드는 브라우저 에이전트 Aside가 짰음. 나는 요구사항 말하고, 결과 보고, 틀린 거 짚는 역할이었음. 1단계: 요청 적으면 캘린더 등록 (9/14) 시트 편집 트리거로 요청 칸이 바뀌면 바로 캘린더 일정을 만듦 일정 ID를 숨김 열에 저장해 둠. 행 하나 = 일정 하나라서 몇 번을 고쳐도 중복이 안 생김 요청을 지우면 일정도 삭제 혹시 놓친 게 있을까 봐 매시간 전체 동기화도 한 번 더 돎 첫 실행에서 일정 34개가 한 번에 등록됨 처리상태는 시간 기준으로 바꿈 처음엔 "등록되면 처리중"으로 했는데 써보니 의미가 없었음. 어차피 다 등록되니까 지금 시각이 그 교시 안에 있는지로 판단하게 바꿈 등록 전은 확인중, 수업 전은 대기, 수업 중은 진행중, 끝나면 완료. 5분마다 갱신 보류만 내가 손으로 고르고, 스크립트는 절대 안 건드림 2단계: 월말 서류 자동 작성 (9/14) 학교 종이 양식을 사진 찍어서 보여주고 똑같이 만들어 달라고 함. 시트 안에 출근기록부, 활동일지, 급식확인서 탭을 양식 그대로 그림 출퇴근 시간은 근무일정 탭에서 가져옴 지도반은 요청의 "2학년 2반"을 "2-2"로 줄여서 넣음 요청 없는 날은 "디지털 기기 점검 및 수업 지원 준비"가 기본값 급식확인서는 13시 넘어서까지 근무한 날만 넣음 (9월은 13일 중 12일) 각 탭을 A4 PDF로 내보내서 메일로 보냄 매월 1일 오전 8시 트리거가 지난달 서류를 만들어서 보냄 3단계: 요청표가 근무표랑 안 맞던 문제 (9/28) 전부 요청 기준으로 돌아가니까 요청표가 틀리면 다 틀림. 실제로 틀려 있었음. 요청표를 월·화·목 고정 패턴으로 복사해서 만들어 둔 상태였음 실제 근무는 대체근무, 동아리 날 시간 변경 같은 게 계속 있어서 매주 다름 근무일 아닌데 요청표에 있는 날이 10일, 근무일인데 없는 날이 7일 10~12월 탭 제목은 전부 "9월" 선생님이 근무일이 아닌 9/28에 수업 지원 요청을 4건 적어 둔 상태였음 근무일정 하나만 믿게 바꿈 근무일정 탭이 유일한 원본. 요청표는 스크립트가 여기 보고 다시 그림 근무일만 나오고, B열에 그날 실제 근무시간이 찍힘 근무시간 밖 교시는 회색, 입력 자체가 막힘 (허용 오차 10분) 6교시 끝나고도 30분 이상 더 일하는 날은 "방과후" 칸이 생김 이미 적힌 요청은 같은 날짜, 같은 교시 칸으로 다시 들어감. 갈 곳 없는 요청은 지우지 않고 맨 아래 조정 구역으로 빼서 메일로 알림 근무일정 한 줄 고치면 요청표가 따라 바뀜. 11/30 퇴근을 15:00에서 14:30으로 바꿔 보니 약 25초 뒤 6교시가 회색이 됨 맨 위 1단계 이미지가 고친 뒤 10월 요청표임. 근무 안 하는 10/5는 빠지고 대체근무일 10/7이 들어감. 4단계: 서류 마무리 (9/28, 9/30) 설정 탭에 서명 이미지를 넣어 두면 세 서류에 서명이 자동으로 찍힘. 사람이 바뀌면 이미지만 바꾸면 됨 요청이 없어도 근무시간 안의 정보 수업은 활동일지에 기본으로 들어가게 함. 정보 수업 시간표 탭을 따로 둠 그래서 9월 출근기록부 비고에 "2-8, 2-4 정보 수업 보조 / 2-5 수업지원"처럼 기본 수업이랑 요청이 같이 찍힘 삽질 첫 전체 동기화가 6분 제한에 걸림 코드가 느린 줄 알았는데 원인은 "완료" 팝업( getUi().alert() )이었음 편집기에서 실행하면 시트 탭에 팝업이 뜨고, 누를 때까지 실행이 안 끝남 팝업은 toast() 로 바꿈 시간이 "9:00"으로 찍힘 "09:00" 문자열을 넣으면 시트가 시간값으로 바꿔버림 setNumberFormat('@') 을 값 넣기 전에 걸어야 함. 넣고 나서 걸면 늦음 날짜가 하루씩 밀림 스크립트는 한국 시간인데 이 시트는 미국 서부 시간으로 잡혀 있었음 날짜를 '2026-10-01' 문자열로 쓰는 걸로 해결 반복문 안에서 시트를 읽다가 에러 근무일 50일을 돌면서 매번 교시표를 읽었더니 "Service Spreadsheets failed" 에러 한 번만 읽고 재사용하게 바꿈 남한테 줄 수도 있게 이름, 이메일, 학교명 같은 값을 코드에서 빼서 설정 탭으로 옮김 시트 사본만 만들면 스크립트도 같이 복사됨. 설정 탭 채우고 메뉴에서 트리거 설치 한 번 누르면 끝 코드는 개인정보 빼고 GitHub에 올려 둠 정리 선생님은 요청만 적음 요청은 몇 초 만에 내 폰 캘린더로 들어감 월말 서류는 매월 1일 아침에 PDF로 메일함에 와 있음 근무가 바뀌면 근무일정 한 줄만 고치면 됨 요청 적는 것 하나로 전부 돌아가게 만드는 게 목표였고, 지금은 그렇게 돌고 있음. https://github.com/Kangchanghwan/digital-tutor-automation
컴파일된 eBPF 오브젝트 파일( .o )에 미리 서명해 두면, 커널은 로드할 때 그 서명을 확인할 수 있을까. 2022년 말에 쓰인 책의 답은 "쉽지 않다"였다. 이유는 암호학이 아니었다. 사용자 공간 로더가 map 위치와 CO-RE 정보로 프로그램을 고친 뒤에 커널에 올리기 때문이다. 책은 이것이 서명 관점에서는 악의적 수정과 구분하기 어렵다고 썼다. 9주차 끝에서 "언어별 라이브러리는 커널 쪽 보장을 얼마나 드러내는가"를 물었다. 10장이 소개하는 라이브러리들을 열어 보면 공통으로 하는 일이 하나 있다. 바로 이 "고쳐 쓰기"다. 그리고 11장이 미해결로 남긴 서명 문제는 2025년 커널 6.18에서, 고쳐 쓰기를 커널 안으로 옮기는 방식으로 풀렸다. 이 글을 읽고 나면 다음을 설명할 수 있어야 한다. bpftrace는 책 설명과 달리 지금 무엇 위에서 도는가 로더는 BPF_PROG_LOAD 직전에 바이트코드의 어느 필드에 무엇을 써 넣는가 ebpf-go, Aya, libbpfgo, libbpf-rs는 그 고쳐 쓰기를 누가 구현하는가 책이 소개한 라이브러리와 예제 코드는 2026년에 어떤 상태인가 서명은 왜 어려웠고, 6.18은 그 문제를 어떤 구조로 풀었는가 책의 나머지 예측(kptr, 메모리 할당, Windows, 표준화)은 어떻게 됐는가 이 글에서 "2026년 현재"는 조사 시점에 고정한 Linux v7.2와 각 출처의 조사 결과를 뜻한다. v7.2 이후의 개발 중인 변경은 이 글의 현재 판정에 포함하지 않는다. 1절은 가장 높은 추상화인 bpftrace에서 출발한다. 2 3절은 로더가 하는 일을 실제 바이트 수준으로 본 뒤 라이브러리별 구현 위치를 비교하고, 4 5절은 책의 목록과 예제를 현재 기준으로 채점한다. 6 7절은 2절의 고쳐 쓰기가 서명을 어떻게 막았고 어떻게 풀렸는지를 실측과 함께 따라가며, 8 9절은 나머지 예측과 책 이후의 변화를 정리한다. 1. bpftrace는 무엇을 감추고, 지금은 무엇 위에서 도는가 책은 eBPF 프로그래밍을 두 부분으로 나눈다. 커널에서 도는 eBPF 프로그램, 그리고 그 프로그램을 관리하고 상호작용하는 사용자 공간 코드다. bpftrace는 이 구분을 사용자에게서 감추는 가장 높은 추상화다(p.185). 책은 bpftrace 절 끝에서 bpftrace가 BCC 위에 만들어졌고, 스크립트가 BCC 프로그램으로 변환된 뒤 런타임에 LLVM/Clang으로 컴파일된다고 쓴다(p.188). 이 설명은 책이 출간되기 전에 이미 바뀌어 있었다. bpftrace는 2022년 6월 PR #2265("Move from bcc to libbpf")로 BCC 대신 libbpf를 쓰기 시작했고, 이 변경은 2022년 8월 v0.16.0에 들어갔다. 전환은 한 번에 끝나지 않았다. 프로그램과 map을 libbpf의 bpf_object 단위로 올리는 변경은 2024년 7월 커밋("Load BPF programs and maps via libbpf bpf_object")으로 v0.22.0에 들어갔다. 2026년 9월에 나온 v0.27.0의 README는 "LLVM을 컴파일러 백엔드로, libbpf를 BPF 서브시스템과의 상호작용에" 쓴다고 설명한다. 그렇다고 BCC를 완전히 뗀 것도 아니다. v0.27.0의 CMakeLists.txt 는 여전히 find_package(LibBcc REQUIRED) 를 요구한다. 그래서 정확한 서술은 "커널과 대화하는 부분은 libbpf가 맡고, 빌드에는 아직 libbcc가 필요하다"이다. BCC가 지금 정확히 어떤 기능에 쓰이는지는 이 글에서 코드로 확인하지 않았다. bpftrace를 쓰는 사람은 이 변화를 알아차릴 필요가 없다. 그게 bpftrace가 감추는 것이다. 감춰진 아래층, 즉 사용자 공간 로더가 실제로 무슨 일을 하는지는 다음 절에서 바이트 수준으로 본다. 2. 로더는 BPF_PROG_LOAD 직전에 무엇을 써 넣는가 아래는 이 글의 실측에 쓴 최소 프로그램이다. map 하나를 조회하고, task_struct 의 필드 하나를 CO-RE로 읽는다. /* lab/hello.bpf.c 발췌 */ struct { __uint(type, BPF_MAP_TYPE_HASH); ... } counter SEC(".maps"); SEC("tracepoint/syscalls/sys_enter_execve") int hello(void *ctx) { struct task_struct *t = (void *)bpf_get_current_task(); u32 tgid = BPF_CORE_READ(t, tgid); /* CO-RE 재배치 대상 */ ... v = bpf_map_lookup_elem(&counter, &tgid); /* map fd 재배치 대상 */ 이 파일을 clang으로 컴파일한 .o 와, 로드한 뒤 커널이 보여 주는 프로그램을 같은 명령어 위치에서 비교하면 다음과 같다(커널 7.0, bpftool v7.7.0). .o (llvm-objdump) 11: r1 = 0x0 ll ; ELF 재배치 레코드: R_BPF_64_64 counter 로드 후 (bpftool xlated) 11: (18) r1 = map[id:51] 첫 줄의 r1 = 0x0 ll 은 64비트 즉치값을 레지스터에 넣는 명령어다. 값이 0인 이유는 컴파일러가 counter map이 어디 있을지 모르기 때문이다. 대신 ELF에 "이 자리는 counter 를 가리켜야 한다"는 재배치 레코드를 남긴다. 이 자리를 빈칸이라고 부르겠다. 둘째 줄은 같은 명령어가 커널 안에서 특정 map을 가리키게 바뀐 모습이다. 빈칸을 채우는 일은 두 단계로 나뉜다. 사용자 공간 로더가 빈칸에 map fd를 적고, 커널 verifier가 그 fd를 실제 map 포인터로 바꾼다. bpftool은 그 포인터를 map id로 보여 준다. libbpf에서 첫 단계를 하는 함수는 bpf_object__relocate_data() 다. /* src/libbpf.c, libbpf v1.7.0, bpf_object__relocate_data() 6367-6379행 */ case RELO_LD64: map = &obj->maps[relo->map_idx]; if (obj->gen_loader) { insn[0].src_reg = BPF_PSEUDO_MAP_IDX; insn[0].imm = relo->map_idx; } else if (map->autocreate) { insn[0].src_reg = BPF_PSEUDO_MAP_FD; insn[0].imm = map->fd; } else { ... 소스 - src/libbpf.c#L6367-L6379 일반적인 경우( else if 분기)에는 명령어의 src_reg 필드에 "이 즉치값은 map fd다"라는 표시( BPF_PSEUDO_MAP_FD )를 하고, imm 필드에 이 프로세스의 map fd 번호를 적는다. 첫 번째 if (obj->gen_loader) 분기는 7절의 light skeleton이 쓰는 경로다. 거기서는 fd 대신 map의 인덱스를 적는다. 이 차이가 서명 문제를 푸는 열쇠가 된다. map 참조만 빈칸인 것은 아니다. 같은 함수와 CO-RE 처리 함수가 채우는 빈칸을 종류별로 모으면 다음과 같다. 재배치 종류 무엇을 가리키는가 명령어에 써 넣는 값 처리 위치(libbpf v1.7.0) RELO_LD64 map src_reg=BPF_PSEUDO_MAP_FD , imm =map fd bpf_object__relocate_data() RELO_DATA 전역 변수(.data/.bss 등) src_reg=BPF_PSEUDO_MAP_VALUE , map fd + 오프셋 같은 함수 RELO_EXTERN_LD64 (typed ksym) 커널 변수 src_reg=BPF_PSEUDO_BTF_ID , 커널 BTF ID + BTF obj fd 같은 함수 RELO_EXTERN_CALL kfunc src_reg=BPF_PSEUDO_KFUNC_CALL , BTF ID + fd 배열 인덱스 같은 함수 CO-RE 구조체 필드 오프셋 등 대상 커널 BTF로 계산한 값 bpf_object__relocate_core() 의 resolve, patch 두 단계 표의 값에는 공통점이 있다. map fd, BTF obj fd, 대상 커널의 필드 오프셋은 모두 그 호스트에서 로드하는 순간에만 정해진다 . 같은 .o 를 두 번 로드해도 fd 번호가 다를 수 있고, 다른 커널에 올리면 CO-RE 오프셋이 달라질 수 있다. 위 실측에서 tgid 오프셋이 .o (0x774)와 로드 후(1908)에 같은 값인 것은, vmlinux.h 를 같은 커널에서 뽑았기 때문이다. 순서에도 작은 장치가 있다. libbpf의 bpf_object_prepare() 는 map을 만들기 전에 재배치부터 한다. resolve_externs → relocate(빈칸에 fd 기록) → BTF 로드 → create_maps → prepare_progs ↑ ↓ placeholder memfd로 fd 번호를 먼저 확보 실제 map을 같은 번호에 덮어씀(reuse_fd) map이 아직 없는데 fd를 적을 수 있는 이유는, map 객체를 만들 때 memfd로 fd 번호부터 맡아 두고 나중에 실제 map을 그 번호에 앉히기 때문이다. BPF 메인테이너 Alexei Starovoitov는 LPC 2022 발표에서 이 역할을 한 줄로 "libbpf is a linker"라고 표현했다. 함수를 붙이고 참조를 확정한다는 점에서 링커가 맞다. 다만 이 비유는 한 지점에서 깨진다. 보통 링커는 오프라인에서 결과 파일을 만들고, 그 결과 파일에 서명할 수 있다. eBPF 로더가 확정하는 값은 대상 호스트에서 로드하는 순간에만 존재한다. 그래서 사용자 공간이 빈칸을 채워 넘기는 일반 경로에서는, 확정된 결과물에 미리 서명해 둘 방법이 없다. 6절에서 이 지점으로 돌아온다. 같은 발표는 CO-RE에 대해서도 선을 긋는다. CO-RE, kconfig, 타입 정보로 이식성을 얻지만 "It's not guaranteed"이고, 이식성을 유지하려면 프로그램을 고쳐야 할 수 있다고 했다. CO-RE는 필드 오프셋 차이를 로드 시점에 맞춰 줄 뿐, 대상 커널에 없는 helper나 kfunc, 프로그램 타입까지 만들어 주지는 않는다. 3. 같은 빈칸, 누가 채우는가: 위임과 재구현 책도 라이브러리를 두 갈래로 나눴다. ebpf-go는 CO-RE 지원을 포함해 "purely in Go"로 구현됐고, Aya는 libbpf에 의존하지 않고 syscall 수준까지 Rust로 만들었다. 반대로 libbpfgo는 libbpf C 코드를 감싼 Go 래퍼이고, libbpf-rs는 libbpf의 Rust 래퍼다. 책은 libbpfgo에 대해 CGo 경계가 성능 등의 문제를 낳을 수 있다는 우려도 적었다(p.193-197). 이 분류는 2026년 소스에서도 그대로다. 2절의 빈칸을 누가 채우는지로 다시 정리하면 다음과 같다. 라이브러리 빈칸을 채우는 코드 근거(태그 기준 소스) libbpf(C) libbpf 자신 bpf_object__relocate_data() , bpf_object__relocate_core() (v1.7.0) libbpfgo(Go) 링크된 C libbpf(시스템 공유 라이브러리 또는 정적 vendoring). Go는 CGo로 bpf_object__load() 를 호출 module.go 의 C.bpf_object__load(m.obj) , README의 dynamic/static 빌드 (v0.11.0-libbpf-1.8-dev-2bbc483) libbpf-rs(Rust) C libbpf(기본값은 vendored libbpf). Rust는 FFI로 같은 함수 호출 object.rs 의 libbpf_sys::bpf_object__load(...) (v0.27.2) ebpf-go(Go) Go로 재구현. CO-RE 코드는 "derived from libbpf"라고 명시 btf/core.go , linker.go 의 applyRelocations , fixupKfuncs → sys.ProgLoad (v0.22.0) Aya(Rust) Rust로 재구현. libbpf·bcc 비의존 aya-obj/src/relocation.rs 가 BPF_PSEUDO_MAP_FD 와 fd를 기록 (aya-obj-v0.3.0) Aya의 map 재배치 코드는 libbpf와 같은 빈칸에 같은 종류의 값을 적는다. /* aya-obj/src/relocation.rs, aya-obj-v0.3.0, 263-268행 발췌 */ instructions[ins_index].set_src_reg(BPF_PSEUDO_MAP_FD as u8); ... instructions[ins_index].imm = *fd; 소스 - aya-obj/src/relocation.rs#L263-L268 이 구분은 취향 문제가 아니다. 래퍼(libbpfgo, libbpf-rs)의 재배치 기능과 버그는 실제로 링크된 libbpf 버전을 그대로 따라간다. libbpf-rs는 기본값이 vendored libbpf이고, libbpfgo는 시스템 libbpf에 동적으로 링크하거나 정적으로 넣는 방식을 고를 수 있다. 재구현(ebpf-go, Aya)은 알고리즘의 출처가 libbpf라도 코드가 별개라서, 새 재배치 종류나 커널 기능을 지원하는 시점이 libbpf와 다를 수 있다. 구체적인 시차 사례는 이 글에서 조사하지 않았다. 커널 쪽 코드를 무엇으로 컴파일하는지도 갈린다. C는 Clang이 주류이고, Aya는 rustc의 BPF 타깃으로 커널 쪽 코드까지 Rust로 쓴다. 여기서 Aya의 libbpf 비의존은 로더 구현에 대한 설명이다. 커널 쪽 프로그램의 빌드 도구 체인까지 LLVM과 무관하다는 뜻으로 확대하면 안 된다. 책은 GCC도 버전 10부터 eBPF를 지원하지만 LLVM 대비 공백이 있다고 했다(p.191). 2024년 9월 LWN 보도에 따르면 GCC BPF 백엔드는 커널 테스트 케이스를 모두 컴파일할 수 있지만 아직 전부 올바르게 돌지는 않았고, CO-RE selftest는 GCC 빌드로 통과했다. 2025년 LSF/MM/BPF의 BPF CI 발표도 GCC가 selftest를 .bpf.o 로 빌드하는 데는 성공하지만 "About half of the tests fail, so we don't run them yet"이라고 했다. 남은 공백이 "컴파일할 수 있는가"에서 "만든 코드가 verifier를 통과하고 올바르게 도는가"로 옮겨 간 것이다. 실패 원인별 내역은 발표에 나오지 않는다. 이 수치는 2025년 발표 시점의 관찰이고, 2026년 현재 실패율은 확인하지 않았다. 4. 책의 라이브러리 목록을 2026년에 다시 보면 책은 몇 가지 판정을 남겼다. gobpf는 한동안 유지보수되지 않았고 deprecate 논의가 있다. ebpf.io 분류에서 Aya는 "emerging", redbpf는 major 프로젝트였지만, 흐름은 Aya 쪽으로 옮겨 가는 것 같다(p.193, p.197). 2026-10-02에 GitHub API로 각 저장소를 조회한 결과는 아래와 같다. 판정은 archived 여부, README 문구, 마지막 릴리스와 푸시 날짜로만 내렸다. 스타 수는 판정에 쓰지 않았다. 라이브러리 2026년 판정 근거 신호 redbpf 종료(공식 아카이브) archived=true, 마지막 릴리스 v2.3.0(2022-01), 기본 브랜치 마지막 커밋 2022-12 rust-bcc 종료(공식 비유지보수 선언) archived=false, README "Warning! Unmaintained!", 마지막 커밋 2023-10 gobpf 선언 없는 휴면 archived=false, deprecation 문구 없음, GitHub 릴리스 없음(최신 태그 v0.2.0), 기본 브랜치 마지막 커밋 2022-10 ebpf-go(cilium/ebpf) 활동 중 v0.22.0(2026-06) libbpfgo 활동 중 v0.11.0-libbpf-1.8-dev-2bbc483(2026-07) libbpf-rs 활동 중 v0.27.2(2026-09) Aya 활동 중 aya-ebpf-v0.2.1(2026-06), aya-v0.14.0 BCC(Python/Lua/C++ 바인딩), libbpf-tools 활동 중 v0.37.0(2026-07). 바인딩과 libbpf-tools는 별도 릴리스 없이 BCC 저장소 하나에 있어, 바인딩별 상태는 따로 판정하지 않았다 libxdp(xdp-tools) 활동 중 v1.6.3(2026-03) 보조 신호로 ebpf.io의 현재 인프라 목록도 봤다. Go 라이브러리로 ebpf-go와 libbpfgo, Rust 라이브러리로 Aya와 libbpf-rs가 올라 있고, gobpf, redbpf, rust-bcc는 없다. 미등재가 곧 폐기 선언은 아니어서 판정에는 위 신호와 함께만 썼다. 책의 예측은 맞았다. Rust 쪽은 redbpf가 아카이브되고 Aya가 남았다. 책이 인용한 ebpf.io의 "emerging" 분류도 지금은 의미가 없다. Aya는 ebpf-go 등과 나란히 언어별 라이브러리로 올라 있다. 다만 "죽었다"에도 종류가 셋이다. gobpf를 "deprecated"라고 쓰면 근거보다 강한 서술이 된다. gobpf에 남은 근거는 기본 브랜치에 2022년 10월 이후 커밋이 없다는 사실뿐이다. 예제 코드도 바뀌었다. 아래는 책 코드와 현재 문서·API가 다른 지점이다. 실제로 로드가 막히는 것은 legacy map을 libbpf 1.0 이상으로 올릴 때이고, Aya의 Bpf 는 deprecated 경고를 낸다. 나머지는 권장 표기와 호출 방식이 바뀐 것이다. 상태 책 원문 서술 실제(2026-10 기준) 근거 표기 변경 // +build ignore (p.193, 책도 //go:build 로 전환 중이라고 언급) ebpf-go 문서는 //go:build ignore ebpf-go Getting Started 호출 방식 변경 go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG ... (p.193) go get -tool .../bpf2go 로 설치 후 go tool bpf2go ... ebpf-go Getting Started 지원 제거(libbpf) struct bpf_map_def SEC("maps") kprobe_map (p.194) libbpf 1.0부터 legacy SEC("maps") 제거, BTF 기반 SEC(".maps") 만 지원. ebpf-go v0.22.0은 아직 maps 섹션을 파싱 libbpf wiki "the road to v1.0", ebpf-go elf_reader.go 이름 변경 Aya Bpf::load(...) (p.198) Aya 0.13.0(2024-10)에서 Bpf 를 Ebpf 로 이름 변경. Bpf 는 deprecated 별칭으로 남음 aya CHANGELOG.md , aya/src/bpf.rs 매크로 변경 Aya #[xdp(name="myapp")] (p.198) #[xdp] 는 이름 인자 없이 섹션을 xdp 로 정하고, 프로그램 이름은 함수 식별자에서 얻음 aya-ebpf-macros xdp.rs 세 번째 행은 로더마다 판정이 다르다는 점에서 2~3절과 이어진다. 같은 .o 를 libbpf 1.0 이상은 거부하고 ebpf-go는 받아 준다. 재구현 라이브러리가 libbpf와 별개 코드라는 것이 실제 동작 차이로 드러나는 사례다. 5. BPF_PROG_RUN과 여러 프로그램: 테스트 명령이 로더가 되기까지 책은 프로그램을 테스트하는 수단으로 BPF_PROG_RUN 을 소개하면서, 이 명령이 "주로 네트워킹 관련인 일부 프로그램 타입"에서만 동작한다고 썼다(p.199). v7.2 커널에서 .test_run 콜백이 등록된 타입을 세면, skb 계열·XDP·flow_dissector·sk_lookup 같은 네트워킹 타입에 더해 raw_tracepoint(CONFIG_NET일 때), tracing(fentry/fexit 등), syscall, struct_ops(CONFIG_NET일 때), netfilter가 있다. 책을 쓰던 무렵의 커널(v6.1)과 비교하면 책의 서술은 당시에도 좁았다. tracing, raw_tracepoint, syscall, struct_ops는 v6.1에 이미 .
최근에 Kaggle(캐글) https://www.kaggle.com/ 에서 "텍스트 이미지 기반 질의응답 모델 개발 AI 챌린지"를 진행했다. AI 챌린지에 참여하면서 AI에 대해 많은 것을 공부할 수 있었고, 평소에 궁금했던 질문의 답을 얻을 수 있었다. 사실 나는 개발을 배우면서 새롭게 알게 되는 사이트 하나하나가 수상하고 궁금했다. 이번에도 Kaggle 이라는 사이트가 너무 수상해서 이것저것 눌러 보고 무엇을 할 수 있는지 찾아보다가 굉장히 흥미로운 것들을 발견하였다! 내가 무엇을 발견했는지 하나씩 소개해 보려고 한다. 캐글(Kaggle)에 대해 어디까지 알아?? 수업 시간에 하라는 챌린지는 안 하고 Kaggle 홈페이지를 깊숙이 깊숙이 둘러보았다. 그 결과 'Kaggle은 정말 흥미로운 사이트!'라는 생각이 들었다. 우선 뻘하게(?) 신기했던 점은 Kaggle 에도 쥬피터( jupyter ) 와 코랩( Colab )처럼 코드를 작성하고 실행할 수 있는 노트북이 있다는 것이다. 그리고 이 노트북은 다른 사람과 함께 공유할 수도 있으며, 일정한 사용량 안에 GPU를 무료로 빌려쓸수 있다! 실행 환경으로는 CPU, P100 GPU, T4 ×2 GPU, TPU 1VM 등 제공된다. 더 자세한것 내용은 [Kaggle 노트북 공식 문서] ( https://www.kaggle.com/docs/notebooks)에서 확인할 수 있다. Claude랑 Chat GPT가 싸운다고?!! Kaggle의 Game Arena에 들어가면 다양한 AI모델들이 게임으로 대결 하는 모습을 볼 수 있다. 게임은 총 17개가 있으며, 그중에서도 Word Art , Werewolf , Chess 가 흥미로웠다. 아래 사진에 나오는 Werewolf 는 한국의 마피아 게임과 비슷하다. TMI:중국에서는 狼人杀(랑런샤)와 비슷하다. 게임을 보고 있으면 Gemini, Grok, Claude, Gpt처럼 서로 다른 AI 모델들이 "누가 마피아야!" 라고 추리하고 의심되는 마피아를 죽이면서 게임을 진행하는 모습을 볼 수 있다. 나는 AI들끼리 서로 의심하고 추리하면서 마피아 게임하는 모습이 귀엽다고 생각한다. 게임 화면에서는 타임라인을 따라 각 AI가 어떤 생각을 하였고 어떤 결정을 내렸는지 확인할 수 있었다. 대화 내용 보고 있으면 "GPT-5.4, 당신 생각은 어떤가요?” 라며 다른 AI의견을 묻기도 한다. 2026년 10월 2일 기준, Werewolf 순위에서는 GPT-5.4가 1위를 차지하고 있었다. 최근에 OpenAI에서 GPT-6 Astra가 출시되었지만, 이 평가에서는 1위가 아니 었다. 최신 모델이라고 해서 모든 분야에서 가장 뛰어난 것은 아니며, AI 모델마다 잘하는 분야와 부족한 분야가 다르다는 것을 알 수 있었다. 한편 Game Arena 전체 게임의 종합 순위표에서는 Claude Opus 5 가 1위를 달리고 있었다. 캐글(Kaggle) 에서 코딩도 배울수 있다고? Kaggle 홈페이지를 보면 Learn이라는 학습 공간이 있다. 이곳에서는 Python, Pandas, SQL, Deep Learning, Machine Learning 등 다양한 강의를 무료로 제공하고 있었다. 영어를 무척 잘한다면... (Kaggle은 다 영어로 되어있다.) Kaggle 노트북을 통하여 코드를 직접 실행하며 공부할 수 있고 내가 지금 얼마나 공부하고 있는지 진행 상황도 단계별로 확인할 수 있었다. 또한 Discussions에는 개발하면서 생긴 궁금한 점을 질문하고, 해결 방법과 경험을 공유하며 자유롭게 토론하는 커뮤니티 공간도 마련되어 있었다. 초보자를 위한 안내부터 대회 운영, 기술적인 질문, 제품 의견 및 공지까지 다양한 주제의 게시판을 이용해 볼 수 있다. 영어를 잘한다면... 누군가는 Kaggle에서 매일 경쟁을 하고 있다. Kaggle이 가장 많이 알려진 이유는 AI·데이터 분석 대회 가 열리는 플랫폼이기 때문일 것이다. 나도 이번에 텍스트 이미지 기반 질의응답 모델 개발 AI 챌린지 을 참여했는데, 그 밖에도 Kaggle에서는 정말 다양한 AI대회가 상금을 걸고 열리고 있었다. 각 대회 페이지에서는 해결해야 할 문제와 평가 방법, 참가 현황, 상금 등을 확인할 수 있다. 특히 일부 AI 에이전트 대회에서는 내가 만든 모델을 제출해 다른 참가자의 모델과 직접 대결하게 할 수도 있다. 단순히 주어진 데이터를 분석하는 것을 넘어, AI가 게임을하거나 가상 환경에서 경쟁하는 대회도 있다는 점이 흥미로웠다. ‘아는 것이 힘이다’ 라는 말이 있다. Kaggle을 자세히 둘러보지 않았다면 이런 세상이 있다는 존재를 몰랐을 것이고, 매년 큰 상금을 걸고 크고 작은 대회들이 열리고 있다는 사실조차 몰랐을 것이다. 그중에서 내가 흥미롭게 본 대회들을 몇 가지 모아 보았다. AI Mathematical Olympiad – Progress Prize 3 국제 수학 올림피아드 수준의 문제를 해결하는 AI 모델을 개발하는 대회이며, 상금은 약 221만 달러이다. 한국돈으로...30억원 앜! The Pokémon Company – PTCG AI Battle Challenge Playground 포켓몬 카드게임을 자동으로 플레이하는 AI 에이전트를 만들어 다른 참가자의 AI와 대전시키는 대회이며, 카드 조합과 확률, 상대의 숨겨진 정보를 고려하는 전략적 의사결정 능력을 평가한다. 진행상황을 보고있으면 신기하다 Predicting Airline Satisfaction 탑승객의 여행 정보와 서비스 평가 데이터를 이용하여 고객이 항공 서비스에 만족했는지 예측하는 표 형식 데이터 분류 대회이다. Kaggriculture 가상 농장에서 작물을 심고, 동물을 돌보고, 토지를 확장하고, 시장에서 거래하는 자율 농장 운영 AI 에이전트를 만드는 대회이며, 다른 참가자의 에이전트와 경쟁하여 30일 동안 가장 많은 수익을 올리는 것이 목표이다. (상금은 5만 달러) 농부는 대체되었다? NFL Big Data Bowl 2026 – Analytics 미식축구 경기에서 공이 공중에 있는 동안 선수들의 위치와 움직임을 분석하여 새로운 전략적 통찰이나 방송용 시각화를 제안하는 대회이다. (상금은 5만 달러)
출력 스트림 출력 스트림은 <ostream> 헤더 파일에 정의되어 있다. 프로그램을 작성할 때 흔히 인클루드하는 <iostream> 헤더에는 입력과 출력 스트림이 모두 정의되어 있다. <iostream> 에는 미리 정의된 스트림 인스턴스인 cout , cin , cerr , clog 와 각각에 대한 와이드 문자 버전 도 있다. 출력 스트림 을 사용하는 가장 간편한 방법은 << 연산자 를 이용하는 것이다. int, 포인터, double, char을 비롯한 C++ 기본 타입은 모두 << 로 출력할 수 있다. 또한 C++의 string 클래스 뿐만 아니라 C 스타일 스트링도 << 로 처리할 수 있다. << 의 사용 예를 몇 가지 들면 다음과 같다. int i = 7; cout << i << endl; char ch = 'a'; cout << ch << endl; string myString = "Hello World."; cout << myString << endl; 이 코드를 실행하면 다음과 같이 출력된다. 7 a Hello World. cout 스트림 은 C++에서 기본으로 제공하는 내장(built-in) 스트림 으로 콘솔(표준 출력, standard output) 에 값을 쓴다. 여러 조각으로 된 데이터를 하나로 합쳐서 출력할 때도 << 연산자를 사용한다. << 연산자는 스트림에 대한 레퍼런스 를 리턴하기 때문에 그 결과를 동일한 스트림의 << 연산자에 연달아 적용할 수 있다. 예를 들면 다음과 같다. int j = 11; cout << "The value of j is" << j << "!" << endl; 이 코드를 실행하면 다음과 같이 출력된다. The value of j is 11; C++ 스트림은 C 스타일의 스트링에 담긴 \n 과 같은 이스케이프 시퀀스(탈출/이탈 문자열, escape sequence) 도 정확히 처리한다. 이때 줄바꿈하는 부분을 \n 대신 std::endl 로 표현해도 된다. \n 은 단순히 새 줄을 시작하는 데 반해 endl 은 버퍼를 내보내는 플러시(flush) 연산도 수행한다. 단, 플러시 연산이 너무 많으면 성능이 떨어질 수 있으니 endl 을 조심해서 사용해야 한다. 예를 들어 endl 을 사용하려 텍스트를 여러 줄에 걸쳐서 출력하면서 매번 버퍼를 내보내는 작업을 한 문장으로 표현하면 다음과 같다. cout << "Line 1" << endl << "Line 2" << endl << "Line 3" << endl; 이 문장을 실행하면 다음과 같이 출력된다. Line 1 Line 2 Line 3 ※ endl은 목적지 버퍼를 내보낸다. 따라서 복잡한 루프문과 같이 성능에 민감한 코드에서는 주의해서 사용해야 한다. 출력 스트림 메서드 출력 스트림에서 가장 대표적인 연산자는 << 다. 그런데 이 연산자는 출력 기능 외에도 다양한 기능을 제공한다. <ostream> 헤더 파일에서 << 연산자를 오버로딩으로 정의한 코드를 보면 온갖 종류의 데이터 타입에 대한 출력 기능을 제공하는 것을 알 수 있다. 그 밖에도 <ostream> 에서 제공하는 유명한 public 메서드 중 몇 가지만 살펴보면 다음과 같다. put()과 write() put() 과 write 는 저수준 출력 메서드로서 출력 동작을 갖춘 객체나 변수를 인수로 받지 않고, 문자 하나를 받거나( put() 의 경우), 문자 배열 하나를 인수로 받는다( write() 의 경우). 이 메서드는 전달된 데이터에 특정한 포맷을 적용하거나 데이터의 내용을 가공하지 않고 본래 상태 그대로 출력한다. 예를 들어 << 연산자를 사용하지 않고 C 스타일 스트링을 콘솔에 출력하려면 다음과 같이 작성한다. const char* test = "hello there\n"; cout.write(test, strlen(test)); put() 메서드로 문자 하나를 콘솔에 출력하는 방법은 다음과 같다. cout.put('a'); flush() 출력 스트림에 데이터를 쓰면 목적지에 즉시 전달되지 않을 수도 있다. 일반적으로 출력 스트림은 들어온 데이터를 곧바로 쓰지 않고 버퍼(buffer) 에 잠시 쌓아두는데, 이렇게 하면 대체로 성능을 높일 수 있다. 목적지가 파일과 같은 스트림일 때는 한 문자씩 처리하기보다는 블록 단위로 묶어서 처리하는 것이 훨씬 효율적이다. 그러다가 다음과 같은 조건을 만족하면 그동안 쌓아둔 데이터를 모두 내보내서 버퍼를 비운다. 이 동작을 제공하는 메서드가 바로 flush() 이다. endl 과 같은 경곗값에 도달할 때 스트림이 스코프를 벗어나 소멸될 때 스트림 버퍼가 가득 찼을 때 flush() 를 호출해서 스트림 버퍼를 명시적으로 비울 때 출력 스트림에 대응되는 입력 스트림으로부터 요청이 들어올 때 (예를 들어 cin 으로 입력받으면 cout 의 버퍼를 비움). flush() 메서드를 호출해서 스트림 버퍼의 내용을 명시적으로 내보내려면 다음과 같이 작성한다. cout << "abc"; cout.flush(); // 콘솔에 abc가 출력된다. cout << "def"; cout.flush(); // 콘솔에 def가 출력된다. ※ 출력 스트림이라고 해서 모두 버퍼를 사용하는 것은 아니다. 예를 들어 cerr 스트림은 버퍼를 사용하지 않고 출력한다. 출력 에러 처리 출력 에러(ouput error) 가 발생하는 경우는 다양하다. 예를 들어 존재하지 않는 파일을 열려고 하거나 디스크가 꽉 차서 쓰기 연산을 처리할 수 없으면 에러가 발생한다. 이런 발생 가능한 모든 에러에 항상 대처하도록 코드를 작서아는 것이 바람직하다. good() 메서드는 스트림을 정상적으로 사용할 수 있는 상태인지 확인한다. 사용법은 다음과 같이 스트림에 대해 곧바로 호출하면 된다. if (cout.good()) { cout << "All good" << endl; } good() 메서드를 이용하면 스트림의 상태 정보를 조회할 수 있다. 하지만 사용할 수 없는 상태일 때는 그 원인을 구체적으로 알려주지 않는다. 이런 정보는 bad() 메서드로 자세히 볼 수 있다. bad() 메서드가 true를 리턴한다는 것은 심각한 에러가 발생했다는 뜻이다 (반면 파일의 끝에 도달했는지 확인하는 eof() 가 true라는 것은 심각한 상태가 아니다). 또한 fail() 메서드를 사용하면 최근 수행한 연산에 오류가 발생했는지 확인할 수 있다. 그러나 그 뒤에 일어날 연산의 상태는 알려주지 않기 때문에 fail() 의 리턴값에 관계없이 후속 연산이 성공적으로 수행될 수도 있고 아닐 수도 있다. 예를 들어 출력 스트림에 대해 flush() 를 호출한 뒤 fail() 을 호출하면 바로 직전의 flush() 연산이 성공했는지 확인할 수 있다. cout.flush(); if (cout.fail()) { cerr << "Unable to flush to standard out" << endl; } 스트림을 bool 타입으로 변환하는 연산자도 있다. 이 연산자는 fail() 을 호출할 때와 똑같은 결과를 리턴한다. 따라서 위의 코드를 다음과 같이 작성해도 된다. cout.flush(); if (!cout) { cerr << "Unable to flush to standard out << endl; } 여기서 주의할 점은 good() 과 fail() 은 스트림이 파일 끝에 도달할 때도 false를 리턴한다는 것이다. 이 관계를 코드로 표현하면 다음과 같다. good() == (!fail() && !eof()) 스트림에 문제가 있으면 익셉션을 던지도록 만들 수도 있다. ios_base::failure 익셉션 을 처리하도록 catch 문을 작성하면 된다. 이 익셉션에 대해 what() 메서드를 호출하면 현재 발생한 에러에 대한 정보 를 볼 수 있고, code() 를 호출하면 에러 코드 를 볼 수 있다. 하지만 얼마나 쓸모 있는 정보를 제공하는지는 표준 라이브러리의 구현에 따라 달라진다. cout.exceptions(ios::failbit | ios::badbit | ios::eofbit); try { cout << "Hello World." << endl; } catch (const ios_base::failure& ex) { cerr << "Caught exception: " << ex.what() << ", error code = " << ex.code() << endl; } exceptions()는 스트림에서 어떤 에러 상태가 발생했을 때 익셉션을 던질지 설정하거나 조회하는 메서드이다. 스트림의 에러 상태를 초기화하려면 clear() 메서드를 호출한다. cout.clear(); 출력 매니퓰레이터 C++의 스트림은 단순히 데이터만 전달하는 데 그치지 않고 매니퓰레이터(manipulator) 라는 객체를 받아서 스트림의 동작을 변경할 수도 있다. 이때 스트림의 동작을 변경하는 작업만 할 수도 있고, 스트림에 데이터를 전달하면서 동작도 변경할 수 있다. 흔히 보았던 endl 이 바로 매니퓰레이터이다. endl 은 데이터와 동작을 모두 담고 있다. 그러므로 스트림에 전달될 때 줄끝(end-of-line, EOL) 문자를 출력하고 버퍼를 비운다. 몇 가지 유용한 매니퓰레이터를 소개하면 다음과 같다. 대부분 <ios> 나 <iomanip> 표준 헤더 파일에 정의되어 있다. boolalpha 와 noboolalpha : 스트림에 bool 값을 true나 false로 출력하거나( boolalpha ), 1이나 0으로 출력하도록( noboolalpha ) 설정한다. 기본값은 noboolalpha 다. hex , oct , dec : 각각 숫자를 16진수, 8진수, 10진수로 출력한다. setprecision : 분숫값을 표현할 때 적용할 소수점 자릿수를 지정한다. 이를 위해 자릿수를 표현하는 인수를 받는다. setw : 숫자 데이터를 출력할 필드의 너비를 지정한다. 이 매니퓰레이터도 인수를 받는다. setfill : 지정된 너비보다 숫자가 작을 때 빈 공간을 채울 문자를 지정한다. 이 매니퓰레이터도 인수를 받는다. showpoint 와 noshowpoint : 스트림에서 분수 부분이 없는 부동소수점수를 표현할 때 소수점 표시 여부를 설정한다. put_money : 스트림에서 화폐 금액을 일정한 형식에 맞게 표현할 때 사용한다. 이 매니퓰레이터도 인수를 받는다. put_time : 스트림에서 시간을 일정한 형식에 맞게 표현할 때 사용한다. 이 매니퓰레이터도 인수를 받는다. quoted : 주어진 문자열을 인용 분호(따옴표)로 감싸고, 문자열 안에 있던 인용 부호를 이스케이프 문자로 변환한다. 이 매니퓰레이터도 인수를 받는다. 여기서 소개하는 매니퓰레이터는 모두 한 번 설정되면 명시적으로 리셋하기 전까지 다음 출력에 계속 반영된다. 단 setw 는 바로 다음 출력에만 적용된다. 이 매니퓰레이터들로 출력 형태를 커스터마이즈하는 예는 다음과 같다. // 부울값 bool myBool = true; cout << "This is the default: " << myBool << endl; cout << "This should be true: " << boolalpha << myBool << endl; cout << "This should be 1: " << noboolalpha << myBool << endl; // "%6d"와 같은 효과를 스트림에 적용하는 방법 int i = 123; printf("This should be ' 123': %6d\n", i); cout << "This should be ' 123': " << setw(6) << i << endl; // "%06d"와 같은 효과를 스트림에 적용하는 방법 printf("This should be '000123': %06d\n", i); cout << "This should be '000123': " << setfill('0') << setw(6) << i << endl; // *로 채우기 cout << "This should be '***123': " << setfill('*') << setw(6) << i << endl; // 빈칸 채우기 문자 리셋 cout << setfill(' '); // 부동소수점수 double dbl = 1.452; double dbl2 = 5; cout << "This should be ' 5': " << setw(2) << noshowpoint << dbl2 << endl; cout << "This should be '@@1.452': " << setw(7) << setfill('@') << dbl << endl; // 빈칸 채우기 문자 리셋 cout << setfill(' '); // cout에서 숫자 포맷을 지역(국가)에 맞게 설정한다. cout.imbue(locale("")); // 현재 지역(국가)에 맞게 숫자를 표현한다. cout << "This is 1234567 formatted according to your location: " << 1234567 << endl; // 화폐 금액을 표기한다. 정확한 액수는 현재 지역(국가)에 맞게 표현한다. // 예를 들면 미국이라면 12000은 12000 센트를 의미한다. // 이를 달러 단위로 표현하면 1200.00이다. cout << "This should be a monetary value of 12000, " << "formatted according to your location: " << put_money("120000") << endl; // 날짜와 시간 time_t t_t = time(nullptr); // 현재 시스템 시각 tm* t = localtime(&t_t); // 현지 시각으로 변환 cout << "This should be the current date and time " << "formatted according to your location: " << put_time(t, "%c") << endl; // 인용 부호로 묶은 스트링 cout << "This should be: \"Quoted string with \\\"embedded quotes\\\".\\": " << quoted("Quoted string with \"embedded quotes\".") << endl; ※ localtime()을 호출할 때 보안 에러나 경고가 발생할 수 있다. 마이크로소프트 비주얼 C++는 좀 더 안전한 버전인 localtime_s()를 제공한다. 리눅스에는 localtime_r()이 있다. 매니퓰레이터를 쓰고 싶지 않다면 같은 효과를 내는 다른 방법이 있다. 스트림에서 제공하는 매니퓰레이터와 동등한 메서드 (예: precision() )를 사용하면 된다. 예를 들어 다음과 같은 문장이 있다고 하자. cout << "This should be '1.2346': " << setprecision(5) << 1.23456789 << endl; 이 문장 대신에 다음과 같이 precision() 을 호출하도록 작성해도 된다. 이 메서드는 이전에 설정된 값을 리턴하기 때문에 그 값을 저장해두면 얼마든지 이전 상태로 되돌릴 수 있다. cout.precision(5); cout << "This should be '1.2346': " << 1.23456789 << endl; 【 본 포스팅은 '전문가를 위한 C++ (개정 5판)'을 참고하여 작성되었습니다. 】
면접 갔다와서 피드백은 바로 적기 기아인 이유와 직무 선택이유? Loyalty를 보고 싶은 건데 입사를 해서 뭘 할 수 있는지, 뭘 배웠고 어떤 경험을 했었고 ~에 흥미가 있는데 이 직무랑 Fit한 것 같은데 ~에 기여하고 싶다. 자소사나 면접이나 다 똑같다. 두괄식!! 면접도 질문에 대한 핵심 내용, 경험, 알고 있는 거 서술하는 것. (면접왕 ᅵᄋ형, 강** 그냥 느낌만 잡고) 혼자서 계속 주저리주저리 자기암시를 생생하게 하는 게 중요하다. 철두철미하게 핵심 위주로 -> 일부러 말을 천천히. 앞으로 2년간 채용은 꾸준히 이루어질 것. 2학기 전략 학점관리 (고고익선) 2. 취업준비 (자소서-면접, 인적성) -> 원익, LS까지는 지원하기. 경험정리 STARL 정리하고 혼자 계속 연습해야 한다. 경험을 말하면 키워드가 딱 떠오르도록. 리플리증후군처럼 연습하자.
개념 컴퓨터의 주요 구성 요소인 CPU, 주기억장치(메모리), 입출력장치(I/O) 사이를 연결하여 데이터와 명령어를 주고받는 하드웨어 통신 경로 버스의 종류 1. Address bus CPU에서 다른 장치로 주소 정보를 전달하는 버스 CPU가 주소를 전달만 함 → 단방향 2. Control bus CPU가 다른 장치에 보내는 제어 명령(읽기/쓰기 등)이나 각 장치가 보내는 상태 신호(인터럽트, 응답 등)를 전달 CPU가 보내는 명령 + 장치의 인터럽트, 응답 → 양방향 3. Data bus CPU와 다른 장칙 간 데이터가 지나가는 통로 데이터를 주고 받음 → 양방향 예시 예를 들어 CPU가 100번 주소의 데이터를 읽을 때 : 버스 전달 내용 방향 주소 버스 “100번 위치에 접근할게” CPU → 메모리 제어 버스 “읽기 작업이야” CPU → 메모리 데이터 버스 100번 위치에 저장된 값 메모리 → CPU 공부하면서 느낀 의문 Q. 그럼 명령을 의미단위로 쪼개서 버스에 넣어줘야되는거 아닌가? 그 작업은 누가 하지? ⇒ 이 의문은 명령어 형식과 디코딩, CPU의 데이터패스 및 제어 신호와 관련되어 있다. 앞으로 디코더·멀티플렉서 등 기초 논리회로를 공부하고, 명령어의 비트가 해석되어 주소·데이터·제어 신호로 전달되는 과정을 살펴볼 예정이다. 📚 참고자료 위키백과 - 시스템 버스
안녕하세요. 이번에 청년 공공 AX 서포터즈로 활동하게 된 이성윤입니다. 저는 행정학을 전공하면서 컴퓨터학을 복수전공하고 있습니다. 두 전공을 함께 공부하며 개발을 계속해 왔고, 인턴으로 일할 때는 AI를 활용해 실제 업무의 불편을 해결하는 과정도 경험했습니다. 단순히 새로운 기술을 사용하는 것을 넘어, AI가 기존의 일하는 방식 자체를 바꿀 수 있다는 점이 재미있었습니다. 이 경험을 계기로 AX에 대한 관심도 자연스럽게 커졌습니다. 그러던 중 청년 공공AX 서포터즈 모집 공고를 발견했습니다. 공공서비스에서 문제를 찾고, AI와 데이터를 활용해 개선 아이디어를 직접 구현하고 검증한다는 활동이었습니다. 공고를 읽자마자 제가 공부하고 경험해 온 것과 정말 잘 맞는다는 생각이 들었습니다. 그래서 망설이지 않고 지원했고, 감사하게도 합격해 첫 활동을 시작했습니다. 빵과 커피, 그리고 게임 2등으로 시작한 OT 9월 18일에는 첫 오리엔테이션에 다녀왔습니다. 앞으로 어떤 활동을 하게 되는지 안내받고, 공공 AX와 AI 정부 실험실에 대한 설명도 들었습니다. AI 정부 실험실은 공무원분들이 업무 중 발견한 문제를 AI로 직접 구현하고 실험할 수 있는 환경입니다. 설명을 들으면서 AI 정부 실험실에서 아이디어가 실제 결과물로 발전하는 과정을 경험할 수 있다는 점이 흥미로웠습니다. 앞으로 AI 정부 실험실을 직접 활용하게 될 활동도 기대됐습니다. 설명도 재미있었지만, 함께 준비해 주신 빵과 커피도 정말 맛있었습니다. 팀원들과 참여한 게임에서는 2등을 해서 상품도 받았습니다. 첫 만남이라 조금 어색할 수 있었는데 게임을 하면서 자연스럽게 이야기를 나눌 수 있었고, 덕분에 기분 좋게 활동을 시작했습니다. 재미있게 시작한 만큼 앞으로 맡은 활동도 열심히 해야겠다고 다짐했습니다. 공무원분들이 GitLab으로 개발 경험을 공유한다는 것 1차 활동으로 공공 GitLab 프로젝트 현황판 도 둘러봤습니다. GitLab은 개발자에게 익숙한 협업 플랫폼입니다. 저도 프로젝트를 진행하며 코드와 이슈를 공유할 때 사용해 왔습니다. 그런데 공무원분들이 업무 중 만든 도구와 개발 과정을 공공 GitLab에 올리고, 서로의 결과물을 공유하고 있다는 점이 새롭게 느껴졌습니다. 공공 GitLab을 살펴보니 단순히 코드를 저장하는 곳이 아니라, 실제 행정 현장의 고민과 해결 과정이 쌓이는 공간처럼 느껴졌습니다. 공공 GitLab에서는 서로 다른 기관에서 시작된 아이디어를 한곳에서 볼 수 있었습니다. 한 사람이 자신의 업무를 편하게 만들고 끝내는 것이 아니라, 다른 기관의 공무원도 참고하고 발전시킬 수 있도록 공개하는 문화가 특히 좋았습니다. 공공 GitLab에 결과물이 남으면 다른 기관에서도 그 아이디어를 이어서 발전시킬 수 있습니다. 공공 GitLab을 통해 각자의 개발 경험이 공공의 자산으로 쌓인다는 점이 특히 인상적이었습니다. 프로젝트들을 살펴보면서 도메인 지식의 중요성도 다시 느꼈습니다. 어떤 일이 반복되고 어디에서 시간이 오래 걸리는지, 어떤 예외가 자주 발생하는지는 그 업무를 직접 해본 사람이 가장 잘 알고 있습니다. 좋은 기술을 사용하는 것만큼 현장의 문제를 제대로 이해하는 것이 중요하다는 생각이 들었습니다. 문서 정리와 검색, 반복 입력처럼 매일 쌓이는 일을 자동화하려는 프로젝트도 많았습니다. 업무 자동화가 단순히 시간을 줄이는 데서 끝나는 것이 아니라, 사람이 더 중요한 판단에 집중할 수 있게 해준다는 점에서 AX가 점점 중요해지고 있음을 체감했습니다. 뉴스에서 봤던 kordoc 을 다시 만났습니다 프로젝트 목록을 보다가 kordoc 이라는 이름에서 멈췄습니다. 서울특별시 광진구 류승인 주무관님이 개발한 프로젝트입니다. kordoc 은 HWP·HWPX·PDF·Office 문서를 마크다운으로 변환하고, 양식 채우기와 문서 비교 등을 지원하는 도구입니다. 공공기관에서 많이 사용하는 한글 문서를 AI가 활용할 수 있는 형태로 바꿔 반복적인 문서 업무를 줄여줍니다. 사실 이 프로젝트는 이전에 뉴스에서 본 적이 있습니다. 광진구 공무원이 한글 문서를 처리하며 겪은 불편을 해결하기 위해 직접 도구를 만들었다는 내용이었습니다. 당시에도 현장의 문제를 가장 잘 아는 사람이 직접 해결책을 만들었다는 점이 대단하다고 생각했습니다. 뉴스에서 인상 깊게 봤던 프로젝트를 AI 정부 실험실의 공공 GitLab에서 다시 발견하니 반갑고 신기했습니다. kordoc 을 보며 공공 AX가 꼭 거대한 시스템을 새로 만드는 일만을 뜻하지는 않는다는 생각이 들었습니다. 매일 반복되는 불편을 발견하고, 작은 도구부터 직접 만들어 공유하는 것도 분명한 변화였습니다. 저도 이번 청년 공공AX 서포터즈 활동을 통해 그런 변화에 기여해 보고 싶습니다. 행정과 개발을 함께 공부한 경험을 살려 현장의 문제를 제대로 이해하고, 실제로 도움이 되는 결과물을 팀원들과 만들어 보겠습니다. AI 정부 실험실과 공공 GitLab을 앞으로 더 직접 활용해 볼 활동도 기대됩니다. 첫 활동을 즐겁게 시작했습니다. 앞으로의 청년 공공 AX 서포터즈 활동도 하나씩 기록해 보겠습니다! 참고 공공 GitLab 프로젝트 현황판 서울특별시 광진구 류승인 / kordoc “hwp 공문서 고통받다 직접 코딩”...AI 행정비서 만든 8급 공무원
[JPA] 다양한 연관관계 매핑 - 다대일, 일대다, 일대일, 다대다 김영한님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 정리한 글입니다. 실습 환경: PostgreSQL + Hibernate 6(jakarta) 1. 연관관계 매핑 시 고려사항 3가지 연관관계를 매핑할 때는 항상 이 3가지를 생각하면 됩니다. 1 다중성 관계 어노테이션 예 다대일 (N:1) @ManyToOne 회원 여러 명 → 팀 하나 일대다 (1:N) @OneToMany 팀 하나 → 회원 여러 명 일대일 (1:1) @OneToOne 회원 하나 ↔ 사물함 하나 다대다 (N:M) @ManyToMany 회원 여러 명 ↔ 상품 여러 개 💡 다중성이 헷갈리면 반대로 생각해 보면 됩니다. 다대일의 반대는 일대다, 일대일의 반대는 일대일입니다. 2 단방향, 양방향 방향 테이블 외래 키 하나로 양쪽 조인 가능 → 사실 방향이라는 개념이 없음 객체 참조용 필드가 있는 쪽으로만 참조 가능. 한쪽만 참조하면 단방향, 서로 참조하면 양방향 3 연관관계의 주인 테이블은 외래 키 하나 로 두 테이블이 연관관계를 맺습니다. 객체 양방향 관계는 A → B , B → A 처럼 참조가 2군데 있습니다. 둘 중 테이블의 외래 키를 관리할 곳 을 지정해야 합니다. 역할 연관관계의 주인 외래 키를 관리하는 참조 (등록, 수정) 주인의 반대편 외래 키에 영향을 주지 않음. 단순 조회만 가능 2. 다대일 [N:1] 다대일 단방향 [객체] Member ──────→ Team - team [테이블] MEMBER.TEAM_ID (FK) ─── TEAM.TEAM_ID (PK) @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; } @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; } 가장 많이 사용하는 연관관계 입니다. DB에서 외래 키가 있는 N 쪽에 참조를 두니 객체와 테이블이 자연스럽게 맞아떨어집니다. 다대일 양방향 [객체] Member ←─────→ Team - team - members @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") // 읽기 전용 private List<Member> members = new ArrayList<>(); } 외래 키가 있는 쪽(Member)이 연관관계의 주인 입니다. 양쪽이 서로 참조하도록 개발합니다. (편의 메서드로 양쪽 세팅) 테이블에는 아무 변화가 없습니다. Team에 members 필드만 추가된 것입니다. ✅ 실무 기본 공식: 다대일 단방향으로 시작 → 필요하면 다대일 양방향 3. 일대다 [1:N] 일대다 단방향 이번에는 반대로 "일(1)"이 주인 이 되는 경우입니다. [객체] Team ──────→ Member - members (team 필드 없음) [테이블] TEAM ─── MEMBER.TEAM_ID (FK) @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID를 관리 private List<Member> members = new ArrayList<>(); } @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; // Team 참조 없음 } 무엇이 이상할까? 테이블의 일대다 관계에서 외래 키는 항상 N(MEMBER) 쪽 에 있습니다. 그런데 객체는 Team이 외래 키를 관리 합니다. 내 테이블이 아닌 반대편 테이블의 외래 키를 관리 하는 특이한 구조가 됩니다. 실제로 나가는 쿼리 Member member = new Member(); member.setUsername("member1"); em.persist(member); Team team = new Team(); team.setName("teamA"); team.getMembers().add(member); // Team을 건드렸는데... em.persist(team); tx.commit(); insert into member (username, member_id) values (?, ?) -- TEAM_ID는 null로 들어감 insert into team (name, team_id) values (?, ?) update member set team_id = ? where member_id = ? -- ← 추가 UPDATE! Member를 INSERT할 때는 Member가 Team을 모르니 TEAM_ID를 넣을 수 없습니다. Team을 저장할 때 members 컬렉션을 보고 MEMBER 테이블을 다시 UPDATE 합니다. 일대다 단방향의 단점 엔티티가 관리하는 외래 키가 다른 테이블에 있습니다. 연관관계 관리를 위해 추가로 UPDATE SQL 이 실행됩니다. 코드는 Team을 수정했는데 MEMBER 테이블에 UPDATE가 나가니 실무에서 헷갈립니다. 테이블이 수십 개면 쿼리 추적이 정말 어렵습니다. ⚠️ @JoinColumn 을 꼭 써야 합니다. 빼먹으면 JPA가 조인 테이블 방식 을 사용해서 TEAM_MEMBERS 같은 중간 테이블을 하나 더 만들어 버립니다. ✅ 결론: 일대다 단방향보다는 다대일 양방향을 사용하자. "Member에 Team 참조가 필요 없는데요?"라고 해도, 약간의 객체지향적 손해를 보더라도 DB 설계에 맞춰 Member에 참조를 두고 다대일 양방향으로 가는 것이 운영하기 훨씬 쉽습니다. 일대다 양방향 @Entity public class Member { // ... @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) // 읽기 전용 private Team team; } 이런 매핑은 공식적으로 존재하지 않습니다. insertable = false, updatable = false 로 읽기 전용 필드 를 만들어서 양방향처럼 쓰는 꼼수입니다. 둘 다 주인처럼 동작하면 꼬이니까 Member 쪽을 강제로 읽기 전용으로 막은 것입니다. ✅ 결론: 다대일 양방향을 사용하자. 4. 일대일 [1:1] 특징 일대일 관계는 그 반대도 일대일 입니다. 주 테이블이나 대상 테이블 중 어디에 외래 키를 둘지 선택 할 수 있습니다. 주 테이블에 외래 키 대상 테이블에 외래 키 외래 키에 유니크(UNIQUE) 제약 조건 을 추가해야 일대일이 보장됩니다. 💡 주 테이블 / 대상 테이블이란? 더 많이 조회하는 쪽, 비즈니스의 중심이 되는 쪽이 주 테이블입니다. 예제에서는 회원(MEMBER)이 주 테이블, 사물함(LOCKER)이 대상 테이블입니다. 1 주 테이블에 외래 키 - 단방향 [객체] Member ──────→ Locker - locker [테이블] MEMBER.LOCKER_ID (FK, UNI) ─── LOCKER.LOCKER_ID (PK) @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @OneToOne @JoinColumn(name = "LOCKER_ID") private Locker locker; } @Entity public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; } 다대일( @ManyToOne ) 단방향 매핑과 거의 같습니다. 어노테이션만 @OneToOne 으로 바뀌고 유니크 제약이 붙는 차이입니다. 💡 Hibernate 6.2부터는 @OneToOne 의 외래 키 컬럼에 유니크 제약 조건을 자동으로 만들어 줍니다. 2 주 테이블에 외래 키 - 양방향 @Entity public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; @OneToOne(mappedBy = "locker") // 읽기 전용 private Member member; } 다대일 양방향처럼 외래 키가 있는 곳(Member)이 연관관계의 주인 입니다. 반대편은 mappedBy 를 적용합니다. 3 대상 테이블에 외래 키 - 단방향 [객체] Member ──────→ Locker - locker [테이블] MEMBER ─── LOCKER.MEMBER_ID (FK, UNI) Member 객체의 locker 로 LOCKER 테이블의 외래 키를 관리 하고 싶은 경우입니다. JPA가 지원하지 않습니다. (일대다 단방향은 지원했지만 일대일은 안 됨) 4 대상 테이블에 외래 키 - 양방향 @Entity public class Locker { // ... @OneToOne @JoinColumn(name = "MEMBER_ID") // 주인 private Member member; } @Entity public class Member { // ... @OneToOne(mappedBy = "member") // 읽기 전용 private Locker locker; } Locker.member를 주인 으로 잡으면 됩니다. 사실 12의 주 테이블 외래 키 양방향을 뒤집은 것 과 매핑 방법이 같습니다. 💡 일대일 규칙 요약 : 내 엔티티에 있는 외래 키는 내가 직접 관리 합니다. 다른 테이블의 외래 키는 관리할 수 없습니다. 일대일 정리: 어디에 외래 키를 둘까? 주 테이블에 외래 키 대상 테이블에 외래 키 구조 주 객체가 대상 객체를 참조하듯, 주 테이블에 FK를 두고 대상 테이블을 찾음 대상 테이블에 FK가 존재 선호 객체지향 개발자 전통적인 DB 개발자 장점 JPA 매핑이 편리. 주 테이블만 조회해도 대상 데이터가 있는지 확인 가능 나중에 일대일 → 일대다로 바뀔 때 테이블 구조 유지 단점 값이 없으면 FK에 null 허용 프록시 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨 일대다로 바뀌는 상황 예시 "회원 한 명이 사물함을 여러 개 쓸 수 있게 해 주세요." 대상 테이블(LOCKER)에 FK 가 있었다면 → 유니크 제약만 지우면 끝. 그대로 일대다 구조입니다. 주 테이블(MEMBER)에 FK 가 있었다면 → MEMBER.LOCKER_ID를 지우고 LOCKER에 MEMBER_ID를 새로 만들어야 합니다. 반대로 "사물함 하나를 여러 회원이 같이 쓸 수 있게"로 바뀐다면 주 테이블 FK가 유리합니다. 어떤 방향으로 확장될지 예측해서 결정 하는 게 핵심입니다. 대상 테이블 FK는 왜 지연 로딩이 안 될까? (간단히) Member를 조회할 때 locker 필드에 값이 있는지 없는지 알아야 null을 넣을지, 프록시를 넣을지 정할 수 있습니다. 그런데 FK가 LOCKER 테이블에 있으니 MEMBER만 조회해서는 알 수 없습니다. 결국 LOCKER를 어차피 조회해야 하므로 지연 로딩이 의미가 없어지고 즉시 로딩됩니다. 프록시와 지연 로딩은 다음 장에서 자세히 다룹니다. 5. 다대다 [N:M] 테이블과 객체의 차이 관계형 DB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없습니다. → 연결 테이블 을 추가해서 일대다, 다대일 관계로 풀어내야 합니다. MEMBER ─── MEMBER_PRODUCT ─── PRODUCT - MEMBER_ID (PK, FK) - PRODUCT_ID (PK, FK) 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능 합니다. Member (List<Product>) ←→ Product (List<Member>) @ManyToMany 매핑 @Entity public class Member { // ... @ManyToMany @JoinTable(name = "MEMBER_PRODUCT") // 연결 테이블 지정 private List<Product> products = new ArrayList<>(); } @Entity public class Product { @Id @GeneratedValue private Long id; private String name; @ManyToMany(mappedBy = "products") // 양방향일 때 private List<Member> members = new ArrayList<>(); } @ManyToMany 사용 @JoinTable 로 연결 테이블 지정 단방향, 양방향 모두 가능 ⚠️ 다대다 매핑의 한계 → 실무에서 사용 X 편리해 보이지만 실무에서는 쓰지 않습니다. 연결 테이블이 단순히 연결만 하고 끝나지 않습니다. 주문 시간, 수량 같은 데이터가 들어올 수 있습니다. MEMBER_PRODUCT - MEMBER_ID (PK, FK) - PRODUCT_ID (PK, FK) - ORDERAMOUNT ← 이런 컬럼을 추가하고 싶은데 - ORDERDATE ← @ManyToMany로는 매핑할 방법이 없음 @ManyToMany 의 연결 테이블은 JPA가 숨겨서 관리 하므로 추가 컬럼을 넣을 수 없습니다. 연결 테이블이 숨겨져 있어서 예상하지 못한 조인 쿼리 가 나가기도 합니다. ✅ 다대다 한계 극복: 연결 테이블을 엔티티로 승격 @ManyToMany → @OneToMany + @ManyToOne 으로 풀어냅니다. Member 1 ──── * MemberProduct * ──── 1 Product - member - product - orderAmount - orderDate @Entity public class MemberProduct { @Id @GeneratedValue @Column(name = "MEMBER_PRODUCT_ID") private Long id; // ★ 별도의 PK @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @ManyToOne @JoinColumn(name = "PRODUCT_ID") private Product product; private int orderAmount; private LocalDateTime orderDate; } @Entity public class Member { // ... @OneToMany(mappedBy = "member") private List<MemberProduct> memberProducts = new ArrayList<>(); } 이제 연결 테이블도 평범한 엔티티 이므로 원하는 컬럼을 마음껏 추가할 수 있습니다. 실제로 이런 테이블은 결국 "주문(ORDER)" 같은 의미 있는 이름의 엔티티가 됩니다. 💡 연결 테이블의 PK는 (MEMBER_ID, PRODUCT_ID) 복합키 vs 별도 ID? 강의에서는 별도의 ID(대리키)를 쓰는 것을 권장 합니다. 복합키는 @IdClass 나 @EmbeddedId 로 매핑이 번거롭습니다. "같은 회원이 같은 상품을 두 번 주문"하는 요구사항이 생기면 복합키로는 막힙니다. 다른 테이블이 이 테이블을 참조할 때 FK가 컬럼 1개면 됩니다. 중복 방지가 필요하면 (MEMBER_ID, PRODUCT_ID)에 유니크 제약 을 따로 걸면 됩니다. 6. 실전 예제 3 - 다양한 연관관계 매핑 배송, 카테고리 추가 주문과 배송 은 1:1 ( @OneToOne ) 상품과 카테고리 는 N:M ( @ManyToMany ) 회원 1 ── * 주문 1 ── * 주문상품 * ── 1 상품 * ── * 카테고리 1 │ 1 배송 ERD 테이블 컬럼 MEMBER MEMBER_ID, NAME, CITY, STREET, ZIPCODE ORDERS ORDER_ID, MEMBER_ID (FK), DELIVERY_ID (FK) , ORDERDATE, STATUS DELIVERY DELIVERY_ID, CITY, STREET, ZIPCODE, STATUS ORDER_ITEM ORDER_ITEM_ID, ORDER_ID (FK), ITEM_ID (FK), ORDERPRICE, COUNT ITEM ITEM_ID, NAME, PRICE, STOCKQUANTITY CATEGORY CATEGORY_ID, PARENT_ID (FK) , NAME CATEGORY_ITEM CATEGORY_ID (FK), ITEM_ID (FK) 코드 Order - Delivery (일대일, 주 테이블에 FK) @Entity @Table(name = "ORDERS") public class Order { // ... 기존 필드 @OneToOne @JoinColumn(name = "DELIVERY_ID") // 주인 (ORDERS에 FK) private Delivery delivery; } @Entity public class Delivery { @Id @GeneratedValue @Column(name = "DELIVERY_ID") private Long id; private String city; private String street; private String zipcode; @Enumerated(EnumType.STRING) private DeliveryStatus status; @OneToOne(mappedBy = "delivery") // 읽기 전용 private Order order; } public enum DeliveryStatus { READY, COMP } 💡 왜 ORDERS에 FK를 뒀을까? 주문을 조회할 때 배송 정보를 같이 보는 경우가 많고, 주문이 비즈니스의 중심(주 테이블)이기 때문입니다. Category - Item (다대다) + Category 자기 참조 @Entity public class Category { @Id @GeneratedValue @Column(name = "CATEGORY_ID") private Long id; private String name; // 자기 자신을 참조 (부모 카테고리) @ManyToOne @JoinColumn(name = "PARENT_ID") private Category parent; // 자식 카테고리 목록 @OneToMany(mappedBy = "parent") private List<Category> child = new ArrayList<>(); // 다대다 (실습용. 실무에서는 연결 엔티티로 풀어야 함) @ManyToMany @JoinTable(name = "CATEGORY_ITEM", joinColumns = @JoinColumn(name = "CATEGORY_ID"), // 내 쪽 FK inverseJoinColumns = @JoinColumn(name = "ITEM_ID")) // 상대 쪽 FK private List<Item> items = new ArrayList<>(); } @Entity public class Item { // ... 기존 필드 @ManyToMany(mappedBy = "items") private List<Category> categories = new ArrayList<>(); } 💡 자기 참조(셀프 조인) "가전 > 주방가전 > 냉장고"처럼 계층 구조 를 표현할 때 씁니다. 같은 엔티티 안에서 parent (다대일)와 child (일대다)로 양방향 매핑하면 됩니다. 💡 @JoinTable 속성 joinColumns : 현재 엔티티(Category) 를 참조하는 FK inverseJoinColumns : 반대편 엔티티(Item) 를 참조하는 FK N:M 관계는 1:N, N:1로 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N
A. Lights Out time limit per test2 seconds memory limit per test256 megabytes Lenny is playing a game on a 3 × 3 grid of lights. In the beginning of the game all lights are switched on. Pressing any of the lights will toggle it and all side-adjacent lights. The goal of the game is to switch all the lights off. We consider the toggling as follows: if the light was switched on then it will be switched off, if it was switched off then it will be switched on. Lenny has spent some time playing with the grid and by now he has pressed each light a certain number of times. Given the number of times each light is pressed, you have to print the current state of each light. Input The input consists of three rows. Each row contains three integers each between 0 to 100 inclusive. The j-th number in the i-th row is the number of times the j-th light of the i-th row of the grid is pressed. Output Print three lines, each containing three characters. The j-th character of the i-th line is "1" if and only if the corresponding light is switched on, otherwise it's "0". cpp #include<iostream> #include<string> #include<cctype> using namespace std; int main() { ios_base::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); string s; cin >> s; int size = s.length(), cnt = 0; for (int i = 0; i < size; i++) { if (isupper(s[i])) cnt++; } if (size - cnt < cnt) { for (int i = 0; i < size; i++) { cout << (char) toupper(s[i]); } } else { for (int i = 0; i < size; i++) { cout <<(char) tolower(s[i]); } } return 0; } c# using System; using System.IO; using System.Text; using System.Threading; using System.Linq; class Program { static BufferedStream sb = new BufferedStream(Console.OpenStandardInput()); static StreamReader sr = new StreamReader(new BufferedStream(Console.OpenStandardInput()), Encoding.Default); static StreamWriter sw = new StreamWriter(new BufferedStream(Console.OpenStandardOutput()), Encoding.Default); static int ReadInt() { int c = sb.ReadByte(); while (c <= 32) { if (c == -1) return -1; c = sb.ReadByte(); } bool neg = false; if (c == '-') { neg = true; c = sb.ReadByte(); } int val = 0; while (c > 32) { val = val * 10 + (c - '0'); c = sb.ReadByte(); } return neg ? -val : val; } static void Solve() { string s = sr.ReadLine(); if (s.Count(char.IsUpper) > s.Count(char.IsLower)) { sw.Write(s.ToUpper()); } else sw.Write(s.ToLower()); sw.Flush(); } static void Main(string[] args) { Thread thread = new Thread(Solve, 1024 * 1024 * 32); thread.Start(); thread.Join(); } } python3 import sys input = sys.stdin.readline # data = sys.stdin.buffer.read() # ndata = len(data) # ii = 0 # def read_int(): # global ii, ndata, data # while ii < ndata and data[ii] <= 32: # ii += 1 # if ii >= ndata: # return 0 # sign = 1 # if data[ii] == 45: # ASCII 45 == '-' # sign = -1 # ii += 1 # num = 0 # while ii < ndata and data[ii] > 32: # num = num * 10 + (data[ii] - 48) # ii += 1 # return num * sign s=input().rstrip() upper=sum( 1 for x in s if x.isupper()) lower=sum( 1 for x in s if x.islower()) if upper>lower: print(s.upper()) else: print(s.lower()) 대충 문제가 알파벳 대문자 소문자 섞어쓰는게 짜증나서 하나로 통일하겠다는 건데 출력에서 보면 대문자가 많으면 대문자 반대면 소문자로 통일해서 춢력하는 문제로 해석됨. ᄀ
이산분포와 연속분포 지금까지는 이항분포, 포아송분포, 초기하분포 등 이산분포(Discrete distribution) 를 배웠다. 이제 연속분포(Continuous distribution) 를 배운다. 연속분포의 많은 개념은 이산분포와 대응된다. 이산분포 연속분포 확률변수 $X$ 확률변수 $X$ PMF PDF CDF CDF 합 $\sum$ 적분 $\int$ $E(X)=\sum xP(X=x)$ $E(X)=\int xf(x),dx$ $\operatorname{Var}(X)$ $\operatorname{Var}(X)$ 핵심은 이산분포에서 합으로 계산하던 것을 연속분포에서는 적분으로 계산한다는 것 이다. PMF와 PDF 이산분포: PMF 이산확률변수에서는 확률질량함수(PMF, Probability Mass Function) 를 사용한다. $$ P(X=x) $$ 확률변수 $X$가 정확히 $x$의 값을 가질 확률이다. 예를 들어 $X$가 양의 정수만 가질 수 있다면, $$P(X=1),P(X=2),P(X=3),\cdots$$와 같이 각각의 값에 확률이 대응한다. 연속분포: PDF 연속확률변수에서는 특정한 한 점을 가질 확률이 0이다. $$ P(X=x)=0 $$ 예를 들어 $X$가 $0$과 $1$ 사이의 모든 실수 중 하나를 가질 수 있다고 하자. $0.5$, $\pi/4$ 등 특정한 하나의 값을 정확히 가질 확률은 모두 $0$이다. 따라서 연속분포에서는 PMF 대신 확률밀도함수(PDF, Probability Density Function) 를 사용한다. 보통 $$f_X(x)$$ 또는 간단하게 $f(x)$로 쓴다. 중요한 점은 PDF 자체는 확률이 아니다. PDF는 확률밀도(density) 를 나타낸다. 확률밀도와 확률의 관계 확률을 질량이라고 생각하면 직관적이다. 이산분포 → 조약돌 연속분포 → 진흙 이산분포에서는 각각의 조약돌에 질량이 존재하고, 모든 질량을 더하면 $1$이 된다. 연속분포에서는 진흙이 연속적으로 퍼져 있고, 전체 질량이 $1$ 이다. 따라서 PDF의 값 자체가 확률이 아니라, PDF를 특정 구간에서 적분한 값이 확률 이다. $$ P(a\le X\le b)=\int_a^b f(x),dx$$ 즉, 확률밀도를 적분하면 확률이 된다. 왜 $P(X=x)=0$인가? 특정한 하나의 값은 길이가 $0$인 구간으로 생각할 수 있다. $$ P(X=x)=\int_x^x f(t),dt=0$$ 그래프에서 보면 $x$ 하나만 선택하는 것은 폭이 $0$인 구간의 넓이 를 구하는 것과 같다. 따라서 넓이가 $0$이 된다. 반면 길이가 있는 구간에서는 넓이가 생기므로 확률이 생긴다. PDF의 직관적 의미 어떤 지점 $x_0$에서 PDF의 값이 $f(x_0)$라고 하자. $f(x_0)$ 자체는 확률이 아니다. 하지만 $x_0$ 주변의 아주 작은 길이 $\varepsilon$의 구간을 생각하면, 다음과 같다. $$ P\left(x_0-\frac{\varepsilon}{2}\le X\le x_0+\frac{\varepsilon}{2}\right) \approx f(x_0)\varepsilon $$ 왜냐하면 $\varepsilon$이 매우 작다면 그 구간에서 $f(x)$가 거의 변하지 않기 때문이다. 따라서 작은 구간에서는 $$ \text{확률}\approx\text{밀도}\times\text{구간의 길이}$$ 가 된다. 즉, $$f(x_0)\times\varepsilon$$을 통해 density scale에서 probability scale로 변환 할 수 있다. 단, 정확한 확률은 항상 적분으로 계산한다. PDF가 유효하기 위한 조건 PMF에서는 모든 확률이 음수가 아니어야 하고 전체 확률의 합이 $1$이어야 한다. PDF에서도 비슷하다. 1. 음수가 아니어야 한다. $$ f(x)\ge0$$ 2. 전체 넓이가 $1$이어야 한다. $$ \int_{-\infty}^{\infty}f(x),dx=1$$ 따라서 PDF가 되기 위한 핵심 조건은 다음과 같다. $$ \boxed{f(x)\ge0,\qquad \int_{-\infty}^{\infty}f(x),dx=1}$$ PDF의 값 자체는 확률이 아니기에, $1$보다 클 수도 있다. CDF와 PDF CDF는 이산분포와 연속분포 모두에서 사용할 수 있는 매우 일반적인 개념이다. CDF의 정의는 $$F_X(x)=P(X\le x)$$이다. 여기서 아래첨자 $X$는 어떤 확률변수의 CDF인지 나타낸다. PDF → CDF 연속확률변수 $X$의 PDF가 $f$라면, $$ F_X(x)=P(X\le x)$$ 이고, $X$가 $x$ 이하일 확률은 $-\infty$부터 $x$까지의 넓이이므로 다음과 같다. $$ F_X(x)=\int_{-\infty}^{x}f(t),dt$$ 즉, $$ \boxed{F_X(x)=\int_{-\infty}^{x}f(t),dt}$$ 이다. CDF → PDF 반대로 연속확률변수의 CDF $F_X(x)$를 알고 있다면 PDF는 미분해서 얻을 수 있다. $$ f_X(x)=F_X'(x)$$ 왜냐하면 $$ F_X(x)=\int_{-\infty}^{x}f(t),dt$$ 이고, 미적분학의 기본정리(FTC, Fundamental Theorem of Calculus) 에 의해 미분하면 원래 함수 $f(x)$가 나오기 때문이다. 따라서 $$ \text{PDF}=\text{CDF의 미분}, \text{CDF}=\text{PDF의 적분}$$ 연속분포에서 구간의 양 끝점 이산분포에서는 $$P(a\le X\le b),\quad P(a<X<b)$$에서 양 끝점을 포함하는지가 중요하다. 하지만 연속분포에서 $$P(X=a)=P(X=b)=0$$이므로 결과가 같다. 따라서 $$ P(a\le X\le b) = P(a<X<b) $$ 이고, $$ P(a\le X\le b)=F_X(b)-F_X(a)$$ 이다. 즉, 연속분포에서는 구간의 양 끝점을 포함하는지 여부가 확률에 영향을 주지 않는다. 이산분포와 연속분포의 기대값 이산분포에서는 $$E(X)=\sum_x xP(X=x)$$였다. 연속분포에서는 특정 값의 확률 $P(X=x)$가 모두 $0$이므로 합 대신 적분을 사용한다. 따라서 $$ E(X)=\int_{-\infty}^{\infty}xf(x),dx $$ 만약 $X$가 $0$과 $1$ 사이에서만 값을 가진다면, 다음과 같이 실제 범위에서만 적분해도 된다. $$ E(X)=\int_0^1xf(x),dx$$ 분산과 표준편차 기대값은 분포의 중심을 나타내지만, 분포가 얼마나 퍼져 있는지(spread) 는 알려주지 않는다. 이를 나타내기 위해 분산(Variance) 을 사용한다. 평균에서 얼마나 떨어져 있는지를 생각하면 $$X-E(X)$$가 된다. 하지만 $$E(X-E(X))=0$$이므로 단순히 평균을 내면 항상 $0$이 된다. 그래서 차이를 제곱한다. $$ \operatorname{Var}(X)=E\left[(X-E(X))^2\right]$$ 즉 분산은 평균으로부터 떨어진 거리의 제곱의 평균이다. 왜 절댓값 대신 제곱을 사용하는가? 절댓값을 사용하면 $$E(|X-E(X)|)$$를 생각할 수도 있다. 하지만 절댓값 함수는 $0$에서 미분하기 어렵고 수학적으로 다루기 불편하다. 반면 제곱은 미분하기 쉽고, 기하학적으로도 유클리드 거리 및 피타고라스 정리와 연결되는 좋은 성질을 가진다. 단점은 단위가 바뀐다는 것이다. 예를 들어 $X$의 단위가 mile이라면 $$\operatorname{Var}(X)$$의 단위는 $\text{mile}^2$가 된다. 그래서 표준편차(Standard Deviation) 를 사용한다. $$ \operatorname{SD}(X)=\sqrt{\operatorname{Var}(X)}$$ 표준편차는 다시 원래 단위로 돌아오기 때문에 해석하기 편하다. 분산의 계산 공식 분산의 정의는 다음과 같다. $$ \operatorname{Var}(X)=E[(X-E(X))^2]$$ 이를 전개하고 풀어보면 다음과 같다. $$ \operatorname{Var}(X)=E(X^2)-[E(X)]^2 \=E\left[X^2-2XE(X)+(E(X))^2\right] \=E(X^2)-[E(X)]^2 $$ 따라서 $$ \operatorname{Var}(X)=E(X^2)-[E(X)]^2$$ 이다. 여기서 매우 중요한 차이가 있다. $$ E(X^2)\neq [E(X)]^2 $$ 일반적으로 제곱을 먼저 하고 기대값을 구하는 것 과 기대값을 구한 뒤 제곱하는 것 은 다르다. 또한 관습적으로 $$EX^2=E(X^2)$$라고 표기한다. 분산은 항상 음이 아닌가? 분산의 정의에서 $$(X-E(X))^2\ge0$$이므로 그 기대값도 음수가 될 수 없다. 따라서 $$\operatorname{Var}(X)\ge0$$이다. 그리고 $X$가 상수일 때만 $$\operatorname{Var}(X)=0$$이다. 즉, $X$가 변하지 않는 상수라면 모든 값이 평균과 같으므로 퍼짐이 없다. 균등분포 Uniform Distribution 연속분포의 가장 간단한 예가 균등분포(Uniform distribution) 이다. $X$가 $a$와 $b$ 사이에서 균등하게 선택된다고 하자. $$ X\sim\operatorname{Unif}(a,b) $$ 균등하다는 것은 단순히 "각 숫자가 나올 확률이 같다"는 뜻으로 생각하면 안 된다. 연속분포에서는 특정한 한 점이 나올 확률이 모두 $0$이기 때문이다. 대신, 같은 길이의 구간은 같은 확률을 가지며, 구간의 확률은 그 구간의 길이에 비례한다. 즉, $$\text{확률}\propto\text{구간의 길이}$$이다. 예를 들어 어떤 구간의 길이가 다른 구간의 2배라면, 그 구간이 선택될 확률도 2배가 된다. 균등분포의 PDF 균등분포에서는 $a$와 $b$ 사이에서 확률밀도가 일정해야 한다. 따라서 PDF는 $$ f(x)= \begin{cases} c,&a\le x\le b\ 0,&\text{otherwise} \end{cases} $$ 형태이다. 전체 적분값이 $1$이어야 하므로 $$\int_a^b c,dx=1$$이고,$$c(b-a)=1$$이 성립해야한다. 따라서 $$c=\frac1{b-a}$$이다. 따라서 균등분포의 PDF는 $$ f(x)= \begin{cases} \frac1{b-a},&a\le x\le b\ 0,&\text{otherwise} \end{cases} $$ 이다. 즉, 구간의 길이가 길수록 전체 넓이를 $1$로 만들기 위해 PDF의 높이는 낮아진다. 균등분포의 CDF CDF는 $$F_X(x)=P(X\le x)$$이다. 균등분포에서는 $x$의 위치에 따라 세 경우로 나누어진다. 1. $x<a$ $X$는 항상 $a$ 이상이므로 $$F_X(x)=0$$이다. 2. $a\le x\le b$ $a$부터 $x$까지의 넓이를 구한다. PDF의 높이는 $\frac1{b-a}$이므로 $$F_X(x)=\int_a^x\frac1{b-a},dt$$ 따라서 $$F_X(x)=\frac{x-a}{b-a}$$이다. 3. $x>b$ $X$는 항상 $b$ 이하이므로 $$F_X(x)=1$$이다. 따라서 전체 CDF는 다음과 같다. $$ F_X(x)= \begin{cases} 0,&x<a\ \frac{x-a}{b-a},&a\le x\le b\ 1,&x>b \end{cases} $$ $a$에서 CDF는 $0$이고 $b$에서 $1$이 된다. $a$와 $b$ 사이에서는 1차 함수처럼 선형적으로 증가 한다. 균등분포의 기대값 연속분포의 기대값 공식에 따라 $$E(X)=\int_a^b xf(x),dx$$이다. 균등분포에서는 $$f(x)=\frac1{b-a}$$이므로 다음과 같다. $$ E(X)=\int_a^b\frac{x}{b-a},dx$$ 적분하면 $$E(X)=\frac1{b-a}\left[\frac{x^2}{2}\right]_a^b = \frac{b^2-a^2}{2(b-a)}$$이고, $$b^2-a^2=(b-a)(b+a)$$이므로 다음과 같다. $$ E(X)=\frac{a+b}{2} $$ 즉, 균등분포의 기대값은 구간의 중간값 이다. LOTUS: Law of the Unconscious Statistician 분산을 계산하려면 $$\operatorname{Var}(X)=E(X^2)-[E(X)]^2$$이므로 $E(X^2)$를 구해야 한다. 처음에는 $Y=X^2$라는 새로운 확률변수를 만들고 $Y$의 PDF를 구해야 할 것처럼 보인다. 하지만 Y의 PDF를 굳이 구할 필요가 없다. 이때 사용하는 것이 LOTUS (Law of the Unconscious Statistician) 이다. 연속확률변수에서의 LOTUS $X$의 PDF가 $f_X(x)$이고 $g(X)$의 기대값을 구하고 싶다면, 다음과 같다. $$ E[g(X)]=\int_{-\infty}^{\infty}g(x)f_X(x),dx $$ 즉, $g(X)$의 PDF를 직접 구하지 않고, 원래 $X$의 PDF를 그대로 사용하면 된다. 예를 들어 $E(X^2)$를 구하고 싶다면, 다음과 같다. $$ E(X^2)=\int_{-\infty}^{\infty}x^2f_X(x),dx $$ 따라서 $Y=X^2$의 PDF를 따로 구할 필요가 없다. 이산분포에서의 LOTUS 이산분포에서도 같은 아이디어가 있다. $$ E[g(X)]=\sum_x g(x)P(X=x) $$ 즉, $g(X)$의 PMF를 구할 필요 없이 원래 $X$의 PMF에 $g(x)$만 곱하면 된다. $U\sim\operatorname{Unif}(0,1)$의 분산 $$U\sim\operatorname{Unif}(0,1)$$을 생각하자. PDF는 $$f_U(u)=1,0\le u\le1$$이고, 기대값은 $E(U)=\frac12$ 이다. LOTUS를 사용하면 $U^2$의 PDF를 구하지 않고 바로 $$E(U^2)=\int_0^1u^2f_U(u),du$$로 계산할 수 있다. $f_U(u)=1$이므로 $$E(U^2)=\int_0^1u^2,du=\frac13$$이다. 따라서 $$ \operatorname{Var}(U)=E(U^2)-[E(U)]^2=\frac13-\left(\frac12\right)^2=\frac1{12}$$ 균등분포는 왜 전체 실수에서 정의할 수 없는가? 균등분포에서는 PDF가 어떤 구간에서 일정한 상수이다. 예를 들어 $a$에서 $b$까지만 균등하면 $$f(x)=\frac1{b-a}$$이다. 그런데 모든 실수에서 균등하다고 생각해보자. 그러면 모든 실수에서 같은 상수 $c$를 가져야 한다. 하지만 $$\int_{-\infty}^{\infty}c,dx$$는 어떤 양의 상수 $c$에 대해서도 무한대가 된다. 따라서 전체 적분을 $1$로 만들 수 없다. 즉 균등분포는 반드시 제한된 범위가 필요하다. $U\sim\operatorname{Unif}(0,1)$의 보편성 $0$과 $1$ 사이의 균등분포는 특별한 역할을 한다. $$ U\sim\operatorname{Unif}(0,1) $$ 하나만 가지고도 원칙적으로 원하는 다른 연속분포를 생성할 수 있다. 이것을 균등분포의 보편성(Universality) 이라고 볼 수 있다. 컴퓨터에서 난수를 생성할 때도 중요한 아이디어이다. 컴퓨터가 직접 원하는 복잡한 분포에서 난수를 생성하기 어려워도, $0$과 $1$ 사이의 균등 난수 $U$를 생성하고 적절한 변환을 적용하여 원하는 분포를 따르는 확률변수를 만들 수 있다. 역변환을 이용한 분포 생성 원하는 CDF를 $F$라고 하자. $$U\sim\operatorname{Unif}(0,1)$$이고 $F$가 연속적이며 강한 증가(strictly increasing)한다고 가정하자. 그러면 $F^{-1}$이 존재한다. 이때 $$X=F^{-1}(U)$$라고 정의하면 $X$는 CDF가 $F$인 분포를 따른다. 즉, $$X\sim F$$이다. 원하는 CDF의 역함수에 균등분포 난수를 넣으면 원하는 분포를 만들 수 있다. 왜 $X=F^{-1}(U)$가 원하는 분포를 만드는가? $X$의 CDF를 직접 구해보자. $$P(X\le x)$$ 에서 $$X=F^{-1}(U)$$이므로 $$P(F^{-1}(U)\le x)$$가 된다. $F$가 증가함수이므로 양변에 $F$를 적용하면 $$P(U\le F(x))$$가 된다. 그런데 $U\sim\operatorname{Unif}(0,1)$이고 $F(x)$가 CDF이므로 $0\le F(x)\le1$이다. 따라서 $U$가 $0$부터 $F(x)$까지 있을 확률은 그 구간의 길이와 같으므로 $$P(U\le F(x))=F(x)$$이다. $$ P(X\le x)=F(x) $$ 즉, $X$의 CDF가 정확히 $F$이므로 $$X\sim F$$이다.
지난 2006년 도입되어 대한민국 공공 및 금융 보안의 철옹성 역할을 했던 '물리적 망분리' 제도가 20년 만에 역사 속으로 사라집니다. 과거 워너크라이(WannaCry) 랜섬웨어 사태 등 외부 위협을 원천 차단하는 데는 탁월했지만, 코로나19 이후 원격근무의 일상화와 클라우드, 특히 생성형 AI(Generative AI) 의 폭발적인 성장은 기존의 획일적인 망분리 환경에서 심각한 '데이터 사일로(Data Silo)' 현상을 낳았습니다. 2026년, 정부와 기업은 혁신을 가로막는 규제를 허물고 보안성과 데이터 활용성의 완벽한 균형을 찾는 '국가 망 보안체계(N2SF, National Network Security Framework)' 시대로 본격 진입하며 IT 리더와 실무자가 반드시 알아야 할 N2SF의 핵심 프레임워크와 대응 전략을 정리했습니다. 1. N2SF(국가 망 보안체계)란 무엇인가? N2SF는 기존의 '망(Network) 중심의 획일적 물리적 단절'에서 벗어나, '데이터(Data)의 중요도에 따른 논리적이고 차등적인 보안 통제' 로 패러다임을 전환하는 새로운 국가 보안 프레임워크입니다. 기존 망분리 : 시스템과 네트워크를 무조건 물리적으로 분리 ➔ 유연성 저하, 신기술 도입 불가 N2SF 체계 : 데이터 등급에 맞춰 클라우드와 AI를 적극 수용 ➔ 제로 트러스트(Zero Trust) 기반의 동적 접근 제어 적용 이러한 변화는 공공기관과 금융권이 민간의 혁신적인 SaaS(서비스형 소프트웨어)와 AI 기술을 도입하여 스마트 업무 환경을 구축할 수 있는 '데이터 경제의 빗장'을 푸는 핵심 열쇠가 됩니다. 2. 핵심 프레임워크: C / S / O 데이터 등급 분류 N2SF의 성공적인 안착을 위해 조직이 가장 먼저 수행해야 할 작업은 보유한 업무 정보와 데이터를 정확하게 분류하는 것입니다. 정보공개법을 근거로 데이터는 다음 세 가지 등급으로 나뉩니다. 등급 정의 및 대상 정보 활용 인프라 권장 C (Classified, 기밀) 안보, 국방, 외교 및 국민 생명·안전과 직결된 비공개 정보 정부·공공 전용 데이터센터 (폐쇄망 유지) S (Sensitive, 민감) 공개 시 사생활 침해나 경영 비밀, 개인정보 유출 우려 정보 CSAP 인증 민간 클라우드 (강력한 접근통제) O (Open, 공개) C/S 등급 외의 일반 정보 및 법령에 따라 공개 요건을 충족한 데이터 일반 민간 클라우드 및 생성형 AI 적극 활용 3. N2SF가 가져올 3가지 결정적 변화 N2SF의 도입은 조직의 일하는 방식을 근본적으로 뒤바꿉니다. 생성형 AI의 전면 도입 (O등급 데이터 중심) : 외부망 연결이 자유로워지면서, 보안 가이드라인을 준수하는 범위 내에서 챗GPT(ChatGPT), 네이버 하이퍼클로바X 등 생성형 AI를 실무에 도입해 업무 생산성을 극대화할 수 있습니다. 클라우드 생태계의 폭발적 확장 : 민감(S) 정보와 공개(O) 정보가 민간 클라우드(CSAP 인증)로 대거 이관되면서, 인프라의 유연성과 확장성이 획기적으로 개선됩니다. '제로 트러스트' 아키텍처의 필수화 : 경계가 허물어지는 만큼, '아무것도 신뢰하지 않고 항상 검증한다'는 제로 트러스트 모델이 기본 탑재되어야 합니다. 4. 지능형 위협에 대비하는 C/S/O 보안 아키텍처 전략 망이 열리면 필연적으로 지능화된 사이버 위협(예: 에이전틱 AI 악용, 4중 갈취 랜섬웨어)에 노출될 확률이 높아집니다. 데이터 등급에 따른 촘촘한 차등 방어 체계 설계가 필수적입니다. 보안 요소 C (기밀) S (민감) O (공개) 인증 (Authn) 강화된 MFA (생체인식+FIDO2) 표준 MFA (OTP, 공동인증서) 기본 MFA 및 ID/PW 권한 (Authz) JIT(Just-In-Time) 임시 권한 부여 최소 권한 원칙(PoLP) 적용 역할 기반 접근 제어(RBAC) 모니터링 실시간 행위 분석 및 SIEM 연계 CNAPP 기반 클라우드 상시 점검 기본 로그 수집 및 분석 데이터 보호 국산 암호 모듈 기반 전 구간 암호화 TLS 1.2 이상 통신 구간 암호화 표준 암호화 적용 다가오는 2026년, 귀사의 인프라는 N2SF에 준비되어 있습니까? 국가 망 보안체계(N2SF)의 도입은 단순한 보안 지침의 변경이 아닙니다. AI와 클라우드를 활용해 비즈니스의 민첩성을 높이고, 글로벌 수준의 디지털 혁신을 이루기 위한 '생존 인프라' 재설계의 출발점입니다. 데이터 등급 분류(C/S/O)부터 레거시 시스템의 클라우드 마이그레이션, 그리고 제로 트러스트 기반의 신원·접근 관리(IAM) 체계 구축까지, 변화를 주도할 전략이 필요합니다.