현재 스펙 학교 한양대학교 ERICA ICT융합학부 미디어테크놀로지전공 (26.02 졸업), 학점 3.83 / 4.5 자격증 정보처리기사, 빅데이터분석기사, ADsP, SQLD, TOPCIT 수준 3, 토익스피킹 IH 알고리즘 백준 골드1 (서비스 종료 당시) 수상 교내 5건, SSAFY 2건 활동 SSAFY, 창업동아리, 카카오 안산 임팩트 챌린지, 연합 IT 동아리 UMC, 교내 SW창업과제, 학생 창업유망팀 300+ 포트폴리오 LinkedIn에서 보기 2026년 지원 결과 서류 50곳 지원, 합격 7 / 탈락 30 / 미발표 13 서류 합격 결과 신협중앙회 시험 탈락 뱅크웨어글로벌 최종면접 포기 BNK시스템 최종 합격 (IT개발 정규직, 입사하지 않음) DB Inc. 최종면접 탈락 제논 최종 합격 (AI Engineer 전환형 인턴) 에이피알 전형 포기 포스코DX 전형 포기 SSAFY에 들어가기까지 대학생활은 한마디로 하고 싶은 걸 하면서 보냈다. 2학년 때는 학점이 2점대까지 떨어질 정도로 방황했지만, 3학년부터는 학점을 조금씩 올리면서 창업, 대외활동, 동아리, 밴드까지 끌리는 건 일단 다 해봤다. 개발자가 되겠다는 생각이 뚜렷하지 않던 때라, 뭐라도 해봐야 할 것 같았다. 처음 해보는 일에 계속 부딪히다 보니, 일단 해보면 어떻게든 된다는 믿음 같은 것도 조금씩 생겼다. 문제는 그 활동들이 취업 준비와는 거리가 있었다는 거다. 4학년 2학기가 되어 돌아보니 정작 취업을 위해 준비해둔 건 거의 없었다. 동아리와 졸업 프로젝트로 백엔드 프로젝트를 세 개 정도 해본 게 전부였고, 코테도, 어학도, 자격증도 없었다. 이제는 진짜 해야겠다는 생각이 들었고, 4학년 때부터는 좋아하던 게임도 끊었다. 마지막 학기에는 재수강까지 포함해 전공 18학점을 들으며 학점을 마저 올렸다. 휴학 한 번 없이 계절학기와 재수강을 이어가다 보니, 졸업할 때까지 들은 학점이 170학점 정도 됐다. 자격증이 하나도 없는 것도 마음에 걸려서 막학기에 SQLD부터 하나 따뒀다. 우연히 알게 된 SSAFY 졸업하자마자 바로 취업하기는 어렵겠다고 생각하던 중에 우연히 SSAFY를 알게 됐다. 대전캠퍼스가 본가 근처였고, 알고리즘을 기를 시간을 따로 준다는 점이 다른 부트캠프보다 나한테 잘 맞아 보였다. 한 달에 140만 원 정도를 지원해줘서 취준 생활의 부담을 덜 수 있다는 것도 컸다. 알고리즘은 거의 해본 적이 없어서 붙을 거라는 확신은 없었고, 붙으면 좋고 아니면 말자 는 마음으로 지원했다. 다행히 면접까지 갔는데, 하필 기말 과제와 시험이 한꺼번에 몰린 시기라 정신없이 준비했다. 결과는 불합격이었다. 종강하고 본가로 내려왔을 때는 꽤 막막했다. 나는 경쟁할 환경과 해야 할 일이 주어지면 집중을 잘하는 편이라, SSAFY 같은 곳에 들어가는 게 그때의 나한테는 중요하다고 생각했다. 그러다 예비 합격자 발표 날, 연락이 시작되자마자 바로 전화가 왔다. 아마 예비 1번이었던 것 같다. 그 전화 한 통이 정말 반가웠다. SSAFY 상반기: 부족한 것부터 채우기 SSAFY에 들어가며 세운 목표는 상반기에는 준비하고, 하반기에는 꼭 취업하는 것 이었다. 기준은 연봉 4천 이상이었고, 방향은 금융권이나 대기업 SI 쪽으로 잡았다. 개발을 못 하는 건 아니었지만, 서비스 기업 백엔드 개발자들처럼 기술을 깊게 파고든 쪽은 아니라고 생각했다. 그렇다고 공기업처럼 시험과 블라인드 채용 위주로 가기에는 그동안 해온 활동들이 있었다. 그 중간 지점이 금융권이나 대기업 SI였고, 내 스펙으로 갈 수 있는 가장 합리적인 선택이라고 봤다. AI 때문에 개발자 채용이 줄어든다는 이야기가 계속 들리던 때라, AI 시대에도 비교적 안정적으로 일할 수 있을 것 같은 금융 쪽에 특히 마음이 갔다. 주변에서는 상반기에도 연습 삼아 지원해보라는 말이 많았다. 그 말이 틀렸다고 생각하지는 않았지만, 나는 서류와 시험을 통과할 준비부터 하기로 했다. 면접은 상대적으로 덜 걱정했다. 대학 때 이것저것 활동을 하면서 발표처럼 사람들 앞에 나서서 나를 보여줘야 하는 자리를 많이 겪어봤기 때문에, 면접도 크게 다르지 않을 거라고 믿었다. 스스로 정한 약속도 있었다. 끊은 게임은 취업할 때까지 다시 하지 않기 1학기 동안 결석이나 지각, 조퇴 없이 다니기 헬스 꾸준히 하기 공부 계획은 자주 어겼지만, 게임과 출결 약속은 끝까지 지켰고 헬스도 빠지지 않으려고 노력했다. 코테와 자격증 가장 먼저 붙잡은 건 몇 년 동안 미뤄온 코테였다. 백준 브론즈에서 시작해서 한 달 반 정도를 거의 미친 듯이 풀었고, 그사이 골드1까지 올라갔다. 막상 해보니 생각보다 재미있었다. 다만 어느 정도 목표를 이루고 나니 다시 손을 놓게 됐는데, 이건 지금도 아쉬움으로 남아 있다. ▲ 당시엔 백준과 SWEA를 같이 풀었다. 백준이 폐쇄된 후론 프로그래머스로 주로 풀고 있다. 그다음은 자격증이었다. 서류에 필요한 스펙을 상반기 안에 한 번에 갖춰두자는 생각으로 정보처리기사, 빅데이터분석기사, ADsP, 토익스피킹까지 일단 시험부터 줄줄이 신청해뒀다. 일을 먼저 벌여놓고 수습하는 게 원래 내 방식이다. 단기간에 몰아서 하는 공부가 잘 맞는 편이라 시험마다 일부러 짧은 기간만 잡고 준비했다. 다행히 상반기 안에 목표했던 자격증은 다 챙길 수 있었고, 토익스피킹은 IH를 받았다. 첫 지원과 1학기 마무리 상반기에 지원을 아예 하지 않은 건 아니다. 본격적인 첫 지원은 신협중앙회였다. 아직 부족하다는 생각에 망설이다가 마감 2시간 전에야 자소서를 쓰기 시작했고, 마감 1분 전에 겨우 냈다. 그런데 뜻밖에도 서류에 합격했다. 일주일 동안 NCS와 전공필기, 코테를 처음으로 준비해 시험을 봤고, 결과는 탈락이었다. 그래도 이런 시험을 한 번씩 직접 겪어본 경험은 이후에 큰 도움이 됐다. 1학기 마지막에는 2인으로 진행하는 관통 프로젝트가 있었다. 빅분기 실기 준비와 겹쳐서 초반에 제대로 참여하지 못해 팀원에게 많이 미안했다. 남은 5일 정도는 최대한 집중해서 마무리했고, 1학기 프로젝트 우수상을 받았다. SSAFY 하반기: 지원하고, 붙고, 다시 고민하기 7월에 2학기가 시작되면서 공통 프로젝트에 들어갔다. 마침 기존 포트폴리오가 조금 오래됐다는 생각이 들던 참이었다. 그때도 이미 백엔드든 어떤 IT 직무든 AI를 잘 다루는 게 하나의 능력이 되어가고 있었고, 원래 목표대로 금융권 IT로 가더라도 AI를 다뤄본 경험이 도움이 될 것 같았다. 그래서 팀원들에게 AI 에이전트 프로젝트를 해보자고 제안했다. MOA 프로젝트 그렇게 시작한 게 MOA(My Office Agent) 프로젝트다. 쉽게 말하면 회사에서 쓰는 AI 비서다. “지난달 회의 자료 찾아줘”, “다음 주에 팀 회의 잡아줘”처럼 말로 부탁하면 AI가 알아서 사내 문서를 찾아 답해주거나 일정을 잡아준다. 나는 백엔드 아키텍처와 에이전트 구축을 맡았다. 에이전트는 LangGraph 기반의 ReAct 구조로 만들어서, 요청에 맞게 문서 검색이나 일정 확인 같은 도구를 스스로 골라 쓰게 했다. 백엔드에서는 두 가지에 특히 신경을 썼다. 권한: 사람마다 볼 수 있는 문서가 다르기 때문에, AI는 저장소에 직접 접근하지 못하게 하고 Spring 백엔드가 권한을 확인한 문서만 넘겨주도록 했다. 오래 걸리는 AI 작업: 별도 Worker로 떼어내 RabbitMQ로 비동기 처리하고, Outbox와 Inbox 패턴으로 메시지가 유실되거나 중복 반영되는 걸 막았다. 만들다 보니 회사의 기존 업무를 AI로 바꿔나가는 일, 흔히 말하는 AX 직무가 어떤 건지도 처음으로 감이 왔다. 그전에는 뭔지 잘 몰라서 금융권이나 SI 쪽만 바라봤는데, 직접 해보니 이쪽도 해볼 만하겠다는 생각이 들었다. 한 가지 직무에만 지원하는 것보다는 나을 것 같아서, 이때부터 AX 직무 공고도 함께 보기 시작했다. 분량을 꽤 크게 잡아서 팀원들이 많이 힘들었을 텐데, 다들 끝까지 함께해준 덕분에 공통 프로젝트 우수상까지 받을 수 있었다. 세 곳의 면접 프로젝트를 하는 동안 회사 지원도 본격적으로 시작했다. 상반기에 코테와 자격증으로 서류와 시험을 통과할 준비는 어느 정도 해뒀으니, 이제는 처음 세운 목표대로 하반기 취업을 향해 움직일 차례였다. 여기까지는 계획대로 흘러가고 있었다. 혼자 꾸준히 공부하는 타입이 아니라는 걸 알았기 때문에, 지원을 많이 해서 해야 할 일이 계속 생기게 만들었다. 떨어지는 걸 기본값으로 두고 한 곳만 붙으면 된다는 마음으로, 거의 매주 코테와 AI 역량검사를 봤다. 그렇게 뱅크웨어글로벌(금융IT), BNK시스템(IT개발), DB Inc.(보험 계열사 S/W엔지니어) 세 곳의 시험을 통과해 면접까지 가게 됐다. 셋 다 처음 목표로 잡았던 금융권 직무였다. 뱅크웨어글로벌 은 SSAFY를 빼면 처음 본 취업 면접이었다. 처음이라 오히려 덜 긴장했던 것 같다. 질문들이 심오한 편이었는데, AI 시대에 개발자의 역할은 어떻게 될 것 같은지, 이 일을 평생 할 수 있을 것 같은지 같은 것들이었다. 평소에 이런 고민을 하고 내 나름대로 정리해두는 걸 좋아해서, 나한테는 잘 맞는 면접이었다. 면접관분들도 진중하게 들어주셔서 지금도 좋은 기억으로 남아 있다. 1차에 합격했다. DB Inc. 는 면접이 한 번뿐이라 그게 곧 최종면접이었는데, 하필 뱅크웨어글로벌 최종면접과 날짜가 겹쳤다. 면접 시간을 바꿔줄 사람까지 여기저기 찾아봤지만 결국 하나를 골라야 했다. 1차를 잘 본 뱅크웨어글로벌이 더 안전한 선택이었지만, 대기업인 DB에 도전해보기로 했다. 쉽지 않은 선택이었다. 당시 면접은 PT 면접이었다. DB가 AX 전환에 관심이 많아 보여서 MOA로 PT를 준비했는데, 결과는 탈락이었다. 가장 긴장했던 면접이라 하루 정도는 꽤 속상했다. 한편 BNK시스템 1차 면접은 뱅크웨어글로벌 면접 바로 다음 날이었다. 서울에서 뱅크웨어글로벌 면접을 마치고 곧장 부산으로 내려가 하룻밤을 자고, BNK 면접까지 본 뒤 대전으로 돌아왔다. 체력적으로 많이 지쳐 있었고, 면접도 질문을 계속 파고드는 압박면접이었다. 그래도 꾸며서 말하기보다는 차분하게 내 생각을 말하려고 했고, 면접관분이 침착하다고 말씀해주시기도 했다. 1차를 통과하고 나서는 DB 면접 전 주에 따로 2차 임원면접을 봤다. 개인적으로는 1차 압박면접보다 이쪽이 더 어려웠다. 질문 하나하나가 까다로웠다. 그리고 BNK시스템에 최종 합격 했다. 붙고 나서 시작된 고민 BNK금융그룹의 IT 계열사라는 점이 나한테는 의미가 컸다. 면접 보러 갔던 부산국제금융센터는 건물이 거의 호텔 같아서, 들어서는 순간 취업이 정말 실감 났다. 그런데 막상 붙고 나니 부산에서의 생활이 현실적으로 다가왔다. 혹시 나중에 이직을 하게 되면 어떻게 될지, 가족과 친구들이 있는 대전과 서울에서 멀어져도 괜찮을지 고민이 많아졌다. 오래 고민한 끝에 가지 않기로 했다. 취준 기간을 통틀어 가장 심란했던 선택이었다. 원래는 연봉 말고 지역은 따지지 않았는데, 이 일을 겪고 나서 기준에 서울이 하나 더 붙었다. 주변에서도 이해하기 어렵다는 반응이 많았고, 솔직히 지금도 가끔 생각난다. 다시 원서를 쓰며 이 결정은 생각보다 훨씬 무거웠다. 떨어졌을 때보다 내가 직접 기회를 놓았을 때가 훨씬 힘들었다. 이러다 아무 데도 못 가게 되면, 그때 BNK를 놓은 걸 두고두고 후회할 것 같았다. 그 무렵 특화 프로젝트가 시작됐다. 이번에도 에이전트 쪽으로, AI로 주식 전략을 짜고 돌릴 수 있게 해주는 MCP 프로젝트를 했다. 그러면서 3주 동안 40곳 가까이, 쉬지 않고 원서를 썼다. 서류 발표는 생각보다 오래 걸렸고, 그사이 하나씩 뜨는 결과는 대부분 불합격이었다. 서류 하나 붙는 것도 쉽지 않다는 건 이미 알고 있었다. 예전에는 서류에서 떨어져도 별 타격이 없었는데, BNK를 놓고 나니 탈락 소식 하나하나가 크게 다가왔다. 그럴 때마다 BNK가 눈에 아른거렸다. 늘 떨어지는 걸 기본값으로 두던 나였는데, 이때만큼은 그게 잘 안 됐다. 솔직히 많이 무서웠다. 이때가 제일 크게 흔들렸던 것 같다. AI 엔지니어가 되기까지 그러던 중에 우연히 제논의 AI Engineer 공고를 봤다. 제논은 스타트업이라 원래는 내가 보던 범위 밖에 있는 회사였다. 연봉 기준 때문에 늘 중견기업 이상 공고만 찾아봤으니까. 읽어보니 MOA에서 했던 일과 비슷한 AX 쪽 업무였고, 연봉도 내 기준에 맞았다. 그렇게 처음으로 스타트업에 지원했다. 지원하고 한 시간 만에 면접을 보러 오라는 전화가 왔다. 너무 빨라서 오히려 수상했다. 가도 되는 건가 잠깐 진지하게 고민했을 정도다. 두 번의 면접 1차는 1:1로 40분 동안 진행된 기술면접이었다. 내 포트폴리오는 아키텍처와 흐름 위주였는데, 질문은 데이터 분석과 평가, 지표 쪽에 집중돼 있었다. 그동안 인성 위주의 금융권 면접만 봐왔던 데다, AI는 원래 내 전공 분야도 아니라서 부족할 수밖에 없었다. 모르는 질문에는 솔직하게 모른다고 답했는데, 그 말만 다섯 번은 한 것 같다. 그나마 빅분기를 공부하며 알게 된 내용과 MOA를 직접 만들어본 경험으로 겨우 버텼다. 직무를 잘못 이해하고 왔다는 생각에 솔직히 기대하지 않았다. 그런데 다음 날 1차 합격 연락을 받았다. 2차까지는 일주일이 있었다. 1차에서 받은 질문들을 떠올리며 포트폴리오를 다시 고쳤고, 인사팀에 양해를 구해 보강한 포트폴리오로 바꿔 제출했다. 2차는 1차보다 훨씬 나았다. 일주일 사이에 이 직무가 무엇을 중요하게 보는지 조금은 알게 된 것 같았다. 그리고 제논에서 최종 합격 연락을 받았다. 아이러니하게도, 그렇게 기다리던 다른 회사 서류 합격 소식은 그 뒤에야 두 기업에서 처음 오게 되었다. 추후 합격한 기업들의 전형은 더 진행하지 않았다. 돌아보면 좀 신기하다. 열심히 준비한 코테는 정작 이번 채용에서 쓰이지 않았고, 2년 동안 해온 Spring 백엔드가 아니라 AI 엔지니어로 붙었다. 요즘 흐름에 맞춰보려고 시작한 MOA와 스펙을 채우려고 땄던 빅분기가 여기까지 이어졌다. 마치며 취준을 하면서 내 생각은 점점 보수적으로 바뀌어 갔다. 채용시장이 어렵다는 얘기를 계속 듣다 보니 안정적인 회사가 정답처럼 보였고, BNK를 놓고 서류에서 계속 떨어질 때는 더 그랬다. 다만 금융 IT를 준비하는 방식은 코테를 풀고, 자격증을 따고, 정해진 전형을 넘는 식으로 AI가 뜨기 전과 크게 다르지 않았다. 업계가 AI를 중심으로 바뀌는 게 보이는데, 거기에만 머물러 있고 싶지는 않았다. 게다가 AX 직무에도 백엔드 지식이 어느 정도 필요해서, 해오던 걸 버리고 가는 느낌도 아니었다. 그런데 막상 제논 합격을 앞에 두고는 마음이 달랐다. BNK로 금융 IT라는 목표를 한 번 이뤄봤기 때문인지, 오히려 다시 도전해보고 싶어졌다. 전환형 인턴이라 정규직 전환이 남아 있긴 하지만, 안정적인 쪽만 고르기보다 이쪽에 한번 걸어보기로 했다. 이제 들어가서 잘해야 하는 게 남았고, 바라는 실력이나 연봉까지는 아직 갈 길이 멀다. 요즘은 LLMOps 쪽에 관심이 많다. 모델을 실제 서비스에 올리고 쿠버네티스 위에서 운영하는 것까지 해보고 싶다. 시간이 되면, 다음 편에서는 대학생활부터 취업 준비 과정까지 조금 더 자세히 정리해보려고 한다.
0. 용어정리 1) LLM 단계 단계 용어 쉽게 말하면 예시 1 Tokenization 문장을 token으로 쪼갬 "Explain HTTP" → 여러 token 2 Prompt 사용자가 LLM에게 넣는 입력 "Explain HTTP in one sentence." 3 Prefill 입력된 token들을 모델이 처리 질문을 "읽고 이해할 준비" 4 Decode 모델이 답변 token을 하나씩 생성 HTTP → is → a → ... 5 Response 생성된 token들이 모여 최종 답변이 됨 "HTTP is a protocol..." 2) 측정값 항목 의미 쉽게 이해하기 prompt_eval_count 입력 token 수 질문이 몇 token이야? prompt_eval_duration 입력 처리 시간 질문 처리하는 데 얼마나 걸렸어? eval_count 출력 token 수 답변을 몇 token 만들었어? eval_duration 출력 생성 시간 답변 만드는 데 얼마나 걸렸어? load_duration 모델 로딩 시간 모델을 메모리에 올리는 데 얼마나 걸렸어? total_duration 전체 처리 시간 처음부터 끝까지 얼마나 걸렸어? stream 출력 전달 방식 답변을 조금씩 받을까, 한꺼번에 받을까? tok/s 초당 token 생성량 1초에 답변을 몇 token 만들 수 있어? 1. 로컬 환경 GPU: RTX 3050 VRAM: 3926 MiB / 8192 MiB GPU Util: 5% Process: /llama-server 2. benchmark 1) thinking여부 실험 Model Tokens Decode Time Throughput Thinking Qwen3 4B 259 4.17s 62.1 tok/s Thinking Qwen3 4B 947 14.88s 63.6 tok/s Instruct Qwen3 4B Instruct 2 0.017s 짧아서 참고용 Instruct Qwen3 4B Instruct 200 3.19s 62.7 tok/s 2) Prefill + TTFT (길이별) Prompt 실제 입력 토큰 Prefill 출력 Decode 전체 짧음 15 28.2 ms 20 314.9 ms 392.3 ms 중간 76 338.8 ms 20 297.1 ms 650.9 ms 장문 194 92.2 ms 20 295.8 ms 405.4 ms -> 중간길이의 테스트가 장문보다 오래걸림. 좀더 확인요망 3) HTTP 설명 요청을 10회 보내서 LLM의 첫 응답까지 걸리는 시간과 전체 응답 시간, 생성 속도를 측정. 회차 TTFT 전체 응답 시간 출력 Token 생성 속도 1 30.24 ms 578.37 ms 37 67.62 tok/s 2 32.54 ms 587.84 ms 34 61.33 tok/s 3 31.46 ms 532.05 ms 34 68.01 tok/s 4 29.71 ms 609.03 ms 37 64.00 tok/s 5 27.92 ms 622.20 ms 37 62.35 tok/s 6 38.02 ms 555.61 ms 34 65.75 tok/s 7 24.88 ms 579.96 ms 34 61.33 tok/s 8 25.06 ms 592.20 ms 37 65.36 tok/s 9 28.55 ms 596.12 ms 37 65.30 tok/s 10 25.60 ms 528.41 ms 34 67.76 tok/s 전체요약 지표 평균 P50 P95 의미 TTFT 29.40 ms 29.13 ms 35.55 ms 첫 번째 출력까지 걸린 시간 전체 응답 시간 578.18 ms 583.90 ms 616.27 ms 요청부터 답변 완료까지 걸린 시간
개인 프로젝트 챗봇이 한 말을 얼마나 믿을 수 있을까 오늘 작업은 챗봇이 한 답변을 사용자가 얼마나 믿을 수 있게 만들 것인지 를 계속 확인하는 데 가까웠다. 근거 표시를 다시 다듬고, AI가 했던 채점을 사람이 직접 확인하고, 역할 대화에서 생기는 말투와 시점 문제를 수정한 뒤 실록 청크에 쉬운 말 풀이를 붙이는 작업까지 진행했다. 답변마다 근거의 성격 표시하기 현재 만들고 있는 챗봇에서 등장할 역사적 인물의 답변에는 실제 실록에 기록된 사실도 있지만, 실록을 바탕으로 풀어 설명한 부분이나 당시 인물의 입장에서 말하는 표현도 섞일 수 있다. 그래서 근거 표시 시 구분을 해주기 위해 답변의 문장마다 성격을 나누어 표시하도록 했다. 실록 기록 : 실록에 직접 기록된 내용 실록 풀이 : 실록 내용을 현대적으로 풀어 설명한 내용 인물 시점 : 해당 인물이 그 시점에 알 수 있는 범위에서 한 말 AI 창작 : 연출을 위해 추가된 표현 처음에는 해석 이라는 표현을 사용했지만 조금 애매하게 느껴져 실록 풀이 로 바꾸었다. 표본을 다시 확인하면서 근거 표시 방식도 수정했고, 표시 정확도는 기존 82%에서 99%까지 올라갔다. 단순히 답변 아래에 출처 링크를 붙이는 것보다 어느 부분이 실제 기록이고 어느 부분이 AI가 풀어쓴 내용인지 구분해서 보여주는 것 이 학습용 서비스에서는 더 중요하다고 생각했다. AI가 만든 채점도 사람이 다시 확인하기 어느새 한 주가 끝나는 금요일이라 강사님께서 중간 점검을 제출해 달라고 하셨다. 그런데 자료를 정리하다 보니 한 가지 문제가 눈에 들어왔다. 평가셋의 정답 기준도 AI와 함께 만들었는데, 결과 채점 역시 AI에게 맡기고 있었다. 결국 AI 가 만든 결과를 AI가 다시 평가하고 있는 셈이었다. 그래서 AI의 기존 판정을 보지 않은 상태에서 답변 30개를 내가 다시 채점해보았다. 처음 나와 AI 의 정답 판정이 일치한 것은 19/30 이었다. 차이가 나는 문항들을 하나씩 확인하면서 다른 시대의 함정 질문은 자료에 없다고 답해도 정답 질문한 장면과 다른 시점의 이야기로 답하면 오답 맞는 질문인데 첫 문장에서 부정하면 부분 점수 처럼 채점 기준도 더 구체적으로 정리했다. AI 채점기가 사람보다 지나치게 엄격하게 판단한 경우도 있었고, 반대로 내가 너무 엄격하게 봤던 기준도 있었다. 그래서 서로 다른 판정을 하나씩 비교하면서 기준을 다시 정리했다. 진작 한 번 확인했어야 했다는 생각도 들었지만, 이런 의미에서 중간 점검을 적절한 시기에 한 번 거친 것이 다행이었다. 규칙 하나를 고치면 다른 문제가 생긴다 직접 결과를 검토하다 보니 역할 대화에서 반복되는 말투 문제도 발견했다. 틀린 전제를 바로잡으라는 규칙 때문에 답변이 계속 "아니다." 로 시작하는 버릇이 있었는데, 프롬프트에서 예시를 주며 규칙 문장을 조금 더 자연스럽게 수정하자 이 문제는 사라졌다. 또 당시에는 아직 사용되지 않던 후대의 호칭을 사용하는 경우도 있었는데 실록 자체가 나중 시점의 표현을 포함하고 있기 때문에 생긴 문제였고, 역할 대화에서는 당시 인물이 실제로 사용할 수 있는 호칭만 사용하도록 다시 제한했다. 그런데 규칙을 고친 뒤 전체 평가를 다시 돌리자 새로운 부작용도 나타났다. 틀린 전제를 고치는 규칙이 너무 강하게 적용되어, 정상적인 질문에도 질문에 없는 대조 표현을 만들어내는 경우가 생겼다. 그래서 다시 전제가 맞으면 불필요하게 부정하거나 비교하지 않고 바로 답한다 는 규칙을 추가했고, 전체 평가를 다시 진행했다. 이 과정을 보면서 몇 개의 예시만 보고 수정이 잘 됐다고 판단하면 안 되고, 규칙을 바꿀 때마다 전체 평가셋으로 다시 확인해야 한다 는 점을 느꼈다. 검색을 사용하는 ChatGPT와 비교하기 이후 중간 점검을 제출하고 나니 강사님의 피드백을 받게 되었다. 제출한 내용 중 범용 AI와 현재 만든 RAG 챗봇의 평가 결과를 비교한 표가 있었는데 GPT 가 웹 검색을 사용할 경우 단순 사실 확인에서는 거의 대부분 정답을 맞혔었고 단순한 정답률만으로는 프로젝트의 특장점을 보여주기 어려워 보였다. 반면 역사 인물 역할 대화에서는 아직 해당 인물이 알 수 없는 미래의 정보를 말하는 경우가 있었고, 현재 프로젝트에서는 지금까지 이 미래 정보 누설을 0으로 유지 하고 있다. 그래서 프로젝트의 장점을 단순히 ChatGPT보다 정확하다 라고 주장하기보다는, 실제 비교에서 차이가 확인된 시점 제한과 근거 확인 기능 을 중심으로 보여주는 쪽이 맞다고 판단했다. 실록 용어를 쉬운 말로 찾을 수 있게 만들기 마지막으로 강사님 조언을 받아 실록 청크마다 쉬운 말 풀이도 추가하기 시작했다. 예를 들어 사용자는 어떤 기관을 정확한 역사 용어로 기억하지 못하고 "세조 때 불교 책을 찍던 기관" 처럼 질문할 수 있다. 하지만 실록에는 정확한 역사 용어만 들어 있기 때문에 표현이 너무 다르면 검색에서 놓칠 수 있다. 그래서 질문을 미리 정해둔 단어로 바꾸는 방식보다는 실록 청크 자체에 쉬운 말 풀이를 추가하는 방향 으로 잡았다. 원래 번역문에는 역사 용어를 그대로 유지하고, 별도의 풀이에는 해당 용어가 무엇을 의미하는지 현대적인 표현을 붙인다. 이렇게 하면 검색할 때는 쉬운 표현도 활용할 수 있고, 화면에서는 사용자가 모르는 역사 용어를 눌러 설명을 확인하는 기능으로도 사용할 수 있다. 현재 803개의 청크를 확인해 그중 722개에 쉬운 말 풀이를 추가했고, 다음에는 이 풀이가 실제 검색 성능에 도움이 되는지 확인할 예정이다. 마무리 오늘은 눈에 띄는 새로운 화면이나 기능을 많이 만든 날은 아니었다. 대신 답변의 근거가 맞는가 → 채점 기준이 맞는가 → 역할 대화의 표현이 시대에 맞는가 → 규칙을 바꾼 뒤 다른 오류가 생기지 않았는가 를 계속 확인했다. 프로젝트가 진행될수록 단순히 정답률 하나만 높이는 것보다 왜 이 답을 믿을 수 있는지 설명할 수 있게 만드는 과정 이 더 중요하게 느껴진다. 다음에는 오늘 추가한 쉬운 말 풀이가 실제 검색 결과를 개선하는지 확인하고, 효과가 있다면 Embedding과 화면의 용어 주석 기능까지 연결해볼 예정이다.
If you have received a message containing “SNM”, you may have wondered what the person was trying to say. Short abbreviations are common in texting, social media comments, and direct messages, and their meanings are not always obvious at first glance. In most casual conversations, SNM means “Say No More.” It is generally used when someone understands what another person is saying and does not need additional explanation. What Does SNM Mean in Texting? The most common SNM Meaning in Text is “Say No More.” People use this expression to show that they understand a situation, request, suggestion, or feeling without needing the other person to explain further. For example: Person 1: “I have an early meeting tomorrow, so I need to leave soon.” Person 2: “SNM, I understand.” Here, SNM communicates that the second person understands the situation. It can make a conversation shorter while still showing that the message has been understood. Where Is SNM Commonly Used? SNM can appear across several online communication platforms. You might encounter it in text messages, Instagram conversations, Snapchat messages, TikTok comments, Discord chats, or group discussions. Because texting often encourages quick and informal communication, abbreviations such as SNM have become part of everyday digital language. However, the exact meaning of an abbreviation can sometimes change depending on the conversation. If SNM appears in an unfamiliar or formal context, consider the surrounding words before assuming its meaning. How to Use SNM in a Conversation SNM is generally suitable for casual conversations with friends, classmates, or online contacts. For example, someone could write: “You want me to bring pizza? SNM!” In this context, the person is expressing that they understand and there is no need for further explanation. It is usually better to avoid abbreviations such as SNM in formal emails, business communication, or professional documents unless you are certain the audience understands the expression. Why Understanding Internet Slang Matters Digital communication continues to introduce new abbreviations, phrases, and slang. Knowing common terms can make conversations easier to follow and help prevent misunderstandings. Websites such as Nodecurio.com also explore practical technology, digital tools, and online trends, while TechyZeno.com provides useful explanations of technology-related terms and internet language. Final Thoughts The SNM Meaning in Text is most commonly “Say No More.” It indicates that someone understands what has been communicated and does not require additional details. When you see SNM in a casual message or social media conversation, the surrounding context can help confirm its intended meaning
MVCC란? MVCC는 Multi-Version Concurrency Control의 줄임말 즉, 다중 버전 동시성 제어 이다. -> 여러 버전의 데이터를 이용한 동시성 제어 핵심 아이디어 데이터가 변경되더라도 이전 버전의 데이터를 조회할 수 있도록 관리한다. 트랜잭션은 자신의 Snapshot(Read View)을 기준으로 해당 트랜잭션이 어떤 버전의 데이터를 볼 수 있는지 판단한다. 예시를 들어보자. 트랜잭션 1이 데이터 A에 대해 쓰기 작업을 진행 중 트랜잭션 2가 A에 대해 읽기를 시도 이때, 트랜잭션 2에게 A의 과거 버전을 제공 따라서 읽기와 쓰기를 동시에 가능하다. 왜 MVCC가 필요한가? 만약 Lock만을 사용해서 동시성을 제어한다고 해보자. 트랜잭션 1이 데이터를 수정하고 트랜잭션 2가 데이터 조회를 시도한다고 하면 트랜잭션 2는 트랜잭션 1이 끝날 때까지 대기해야 한다. 읽기 요청이 많다면 대기가 지속되면서 동시성이 크게 떨어지게 된다. 따라서 "읽기 작업이 쓰기 작업을 막지 않고, 쓰기 작업도 읽기 작업을 최대한 막지 않도록 한다." READ COMMITTED 와 REPEATABLE READ 비교 ※ MySQL 기준 READ COMMITTED를 생각해보자. READ COMMITTED 에서는 NON-REPEATABLE READ 문제가 발생한다고 했다. 이는 READ COMMITTED 단계에서 조회할 때마다 새로운 스냅샷을 사용하기 때문이다. 예를 들어, T1이 A를 조회했을 때 100이라고 하자. T2가 A를 200이라고 수정 후 COMMIT T1이 A를 다시 조회할 때 보는 스냅샷은 T2가 A를 200으로 수정한 버전이다. 따라서 T1은 A의 값을 200이라고 읽는다. 이번에는 REPEATABLE READ를 생각해보자. REPEATABLE READ에서는 항상 동일한 값을 읽는다고 했다. 이는 REPEATABLE READ 단계에서 트랜잭션 동안 항상 동일한 스냅샷을 유지하는 방식이기 때문이다. 위의 예시와 같은 예를 들어보면, T1이 A를 조회했을 때 100이라고 하자. T2가 A를 200이라고 수정 후 COMMIT T1이 A를 다시 조회했을 때에는 T2가 200으로 수정하기 이전 버전을 본다. 따라서 T1은 A의 값을 100이라고 읽는다. 어떻게 이전 버전을 복원할까? 데이터가 변경될 때 이전 버전을 복원할 수 있도록 Undo Log에 관련 정보를 기록 해당 log는 트랜잭션 Rollback 뿐만 아니라 MVCC에서 과거 버전의 데이터를 조회하는 데도 사용 ※ Snapshot은 과거 데이터를 직접 저장한 것이 X, 어떤 버전의 데이터를 볼 수 있는지 판단하는 기준 Lock과 MVCC 차이 Lock는 다른 트랜잭션의 접근을 제한한다. MVCC는 접근을 막기보다 적절한 버전을 보여준다. 따라서 Lock과 MVCC는경쟁 관계가 아니라 함께 사용된다. 예를 들어, 일반적인 SELECT는 MVCC로 처리하고 수정을 위해서 데이터를 확실히 잡아야한다면 Lock을 사용한다. -> 즉, MVCC는 읽기 동시성을 높이는데 매우 유용, Lock은 충돌하는 작업의 접근을 제어하는 데 사용된다. 정리 MVCC는 Multi-Version Concurrency Control, 즉 다중 버전 동시성 제어이다. MVCC는 데이터가 변경되더라도 이전 버전의 데이터를 조회할 수 있도록 한다. MVCC는 읽기 동시성을 높이기 위해 사용한다. MVCC의 Read View 생성 및 사용 방식에 따라 격리 수준에 따른 조회 결과가 달라질 수 있다. Read View를 기준으로 어떤 데이터 버전을 바라볼지 판단하며, 필요한 과거 데이터는 Undo Log를 이용해 조회한다. 일반 SELECT는 주로 MVCC를 이용한 Snapshot Read로 처리된다. SELECT ... FOR UPDATE, DELETE 등은 Current Read를 수행하고 필요한 경우 Lock을 사용한다. MVCC와 Lock은 대체 관계가 아니라 서로 다른 문제를 해결하기 위해 함께 사용된다.
Python_Day.10 클래스 복습 문제: 상속과 super() , 파일을 쓰는 클래스, 클래스 변수로 상태 관리. 파이썬 열흘 돌아보기. 파이썬 열 번째 날이다. 새 개념보다는 클래스 문제 세 개 를 푸는 복습이었다. 상속( super() ), 파일 입출력, 클래스 변수와 상태 검사를 한 문제씩 다시 짚었다. 1. 상속과 super() : 탈것 Vehicle 을 상속해서 Car 와 Bicycle 을 만들고, 각자 move() 를 오버라이딩한다. class Vehicle: def __init__(self, name, speed): # 변수에 처음 값을 대입 → 초기화 self.name = name self.speed = speed def move(self): return f"{self.name}은 시속{self.speed} 이다" class Car(Vehicle): def move(self): return super().move() + "소나타 자동차" class Bicycle(Vehicle): def move(self): return super().move() + "자전거" car = Car("소나타", 120) bike = Bicycle("자전거", 20) print(car.move()) # 소나타은 시속120 이다소나타 자동차 print(bike.move()) # 자전거은 시속20 이다자전거 흐름은 이렇다. Car("소나타", 120) 을 만들면 Car 에는 __init__ 이 없어서 부모 Vehicle 의 __init__ 이 호출 되고 name , speed 가 초기화된다. car.move() 는 Car 의 move() 가 실행된다. 그 안의 super().move() 가 부모의 move() 를 불러 "소나타은 시속120 이다" 를 얻고, 뒤에 문자열을 덧붙인다. bike.move() 도 같은 방식이다. __init__ 을 따로 정의하지 않은 자식 클래스는 부모의 __init__ 을 그대로 쓴다. Day.5에서 자식이 __init__ 을 다시 정의할 때만 super().__init__() 이 필요하다고 정리한 내용의 반대 경우다. 출력 문장 다듬기 위 결과를 보면 문장이 어색하다. 소나타은 처럼 조사가 어긋나고, 이름( 소나타 )이 뒤에 또 붙었다. 이름은 이미 self.name 에 들어 있으므로 덧붙일 것은 종류 만 있으면 된다. class Vehicle: def __init__(self, name, speed): self.name = name self.speed = speed def move(self): return f"{self.name}: 시속 {self.speed}km" class Car(Vehicle): def move(self): return super().move() + " (자동차)" class Bicycle(Vehicle): def move(self): return super().move() + " (자전거)" for v in [Car("소나타", 120), Bicycle("따릉이", 20), Vehicle("탈것", 0)]: print(v.move()) # 소나타: 시속 120km (자동차) # 따릉이: 시속 20km (자전거) # 탈것: 시속 0km for 문에서 세 객체에 똑같이 v.move() 를 호출했는데 각자의 move() 가 실행됐다. Day.8의 다형성 이다. 한글 문장에서 이름 + 은/는 을 코드로 붙이면 받침에 따라 조사가 달라져서 어긋나기 쉽기 때문에, 위처럼 조사를 피하는 형식으로 바꿨다. 상속 관계는 코드로도 확인할 수 있다. print(isinstance(Car("a", 1), Vehicle)) # True (Car 객체는 Vehicle이기도 하다) print(issubclass(Bicycle, Vehicle)) # True print(Car.__mro__) # (<class 'Car'>, <class 'Vehicle'>, <class 'object'>) __mro__ 는 메소드를 찾는 순서다. Car → Vehicle → object 순으로 찾는다. Day.7의 "모든 클래스는 object 를 상속한다"를 눈으로 볼 수 있다. 2. 파일을 쓰는 클래스: 일기장 날짜와 함께 일기를 파일에 쌓고, 한꺼번에 읽어 온다. from datetime import date class Diary: def __init__(self, filename="diary.txt"): # 파일 이름의 기본값 self.filename = filename def write(self, content): today = date.today() # a 모드: 기존 내용 뒤에 이어쓰기 with open(self.filename, "a", encoding="utf-8") as f: f.write(f"{today} : {content}\n") # 파일에 저장한 문장들을 읽어 온다 def read_all(self): try: with open(self.filename, "r", encoding="utf-8") as f: print(f.read()) except FileNotFoundError: print("파일이없어 일기 못읽어옴") diary = Diary() diary.write("파이썬 복습") diary.write("파이썬 오늘 정리함") diary.read_all() # 2026-10-03 : 파이썬 복습 # 2026-10-03 : 파이썬 오늘 정리함 (날짜는 실행한 날로 나온다.) date.today() 는 오늘 날짜 객체고, f-string에 넣으면 2026-09-18 형태의 문자열이 된다. filename="diary.txt" 처럼 매개변수 기본값 을 두면 Diary() 로 만들 때 파일 이름을 안 넘겨도 된다. 다른 파일을 쓰고 싶으면 Diary("work.txt") 로 만들면 된다. 'a' 모드는 이어쓰기 라서 실행할 때마다 줄이 계속 쌓인다. 'w' 였다면 매번 지워진다. (Day.4) read_all 은 파일이 없으면 FileNotFoundError 를 except 로 처리한다. 아직 한 번도 쓴 적 없는 Diary("nofile.txt").read_all() 을 하면 메시지만 출력되고 프로그램은 계속된다. Day.4의 파일 입출력, Day.6의 예외 처리, Day.5에서 본 time 처럼 표준 모듈( datetime )을 가져다 쓰는 것을 한 클래스 안에 모은 문제다. 3. 클래스 변수로 상태 관리: 주문 주문의 상태가 정해진 값 중 하나일 때만 바꿀 수 있게 한다. class Order: ORDER_STATUS = ["결제대기", "결제완료", "배송준비", "배송중", "배송완료"] # 클래스 변수 def __init__(self, order_id, customer): self.order_id = order_id self.customer = customer self.status = "결제대기" # 처음 상태 def update_status(self, new_status): # new_status가 리스트 안에 없으면 "잘못된 상태" if new_status not in self.ORDER_STATUS: print("잘못된 상태") return self.status = new_status print(f"{self.order_id} 상태가 {new_status} 로 변경되었다") def show(self): print(f"주문번호 : {self.order_id}, 주문자 : {self.customer}, 상태 : {self.status}") order1 = Order("A001", "Tom") order2 = Order("A002", "Amy") order1.update_status("결제완료") # A001 상태가 결제완료 로 변경되었다 order1.update_status("배송준비") # A001 상태가 배송준비 로 변경되었다 order2.update_status("배송중") # A002 상태가 배송중 로 변경되었다 order2.update_status("문앞배송") # 잘못된 상태 order1.show() # 주문번호 : A001, 주문자 : Tom, 상태 : 배송준비 order2.show() # 주문번호 : A002, 주문자 : Amy, 상태 : 배송중 ORDER_STATUS 는 클래스 변수 다. 주문 객체마다 따로 갖지 않고 모두가 공유 한다. 이름을 대문자로 쓴 것은 "바꾸지 않는 값(상수)"이라는 관례다. 확인해 보면 Order.ORDER_STATUS is order1.ORDER_STATUS 가 True 로, 같은 리스트다. (Day.7의 클래스 변수) not in 으로 유효한 상태인지 검사하고, 아니면 return 으로 바로 끝낸다. 이 경우 status 는 바뀌지 않는다. 문앞배송 은 목록에 없어서 변경되지 않았다. 필기에서는 주문번호와 주문자를 input() 으로 받았는데, 실행할 때마다 입력해야 해서 값을 직접 넣었다. 한 걸음 더: 단계 건너뛰기 막기 위 코드는 결제대기 에서 바로 배송중 으로 바꿔도 통과한다. ( order2 가 그렇게 바뀌었다) 상태에 순서 가 있으니 리스트의 위치( index )를 비교하면 거꾸로 돌아가는 것은 막을 수 있다. class Order2(Order): def update_status(self, new_status): if new_status not in self.ORDER_STATUS: print("잘못된 상태") return if self.ORDER_STATUS.index(new_status) < self.ORDER_STATUS.index(self.status): print("이전 단계로는 돌아갈 수 없다") return super().update_status(new_status) # 나머지는 부모의 기능을 그대로 o = Order2("B1", "Kim") o.update_status("배송중") # B1 상태가 배송중 로 변경되었다 o.update_status("결제완료") # 이전 단계로는 돌아갈 수 없다 o.update_status("문앞배송") # 잘못된 상태 Day.5의 상속 + 오버라이딩 + super() 로 부모는 건드리지 않고 검사만 덧붙였다. 반대로 건너뛰는 것( 결제대기 → 배송중 )까지 막을지는 업무 규칙에 달려 있어서, 여기서는 거꾸로 가는 것만 막았다. 4. 파이썬 열흘 돌아보기 열흘간 배운 것을 한 장으로 정리하면 이렇다. Day 주제 1 자료형 9가지, 슬라이싱, 문자열 포맷( % , format , f-string), 리스트 메소드 2 튜플, 딕셔너리, 집합, 참/거짓 판별, id / copy / is , if / elif , in , while 3 for / range , 리스트 컴프리헨션, 함수, *args / **kwargs , 기본값 매개변수, input 4 변수 스코프, lambda , 내장 함수, 파일 입출력, 클래스 기초 5 상속, super() , 오버라이딩, 내장 함수와 time / random / pickle , 모듈 6 패키지, 예외 처리와 사용자 정의 예외, 클로저, 데코레이터 7 __str__ , 이터레이터, 제너레이터, JSON, sys 8 예외·상속 실습, 다형성, asyncio , threading , sys.argv 9 정규 표현식( re ), os 모듈 10 클래스 복습: 상속, 파일, 클래스 변수 JavaScript(Day.1~10)에서 Promise 와 async/await 로 끝낸 비동기를 파이썬에서도 asyncio 로 다시 만났다. 언어는 달라도 async / await 로 기다리고 동시에 실행한다는 구조는 같았다. 마무리 자식이 __init__ 을 정의하지 않으면 부모의 __init__ 이 그대로 쓰인다. 메소드를 오버라이딩하면서 부모 기능도 쓰려면 super().메소드() 를 호출한다. 같은 이름의 메소드를 호출해도 객체의 종류에 따라 다르게 동작한다. (다형성) 파일에 쌓는 기능은 'a' 모드와 with , 파일이 없을 때는 FileNotFoundError 처리를 함께 쓴다. 클래스 변수 는 모든 객체가 공유한다. 정해진 값 목록을 두고 in / not in 으로 검사하면 잘못된 값을 걸러 낼 수 있다. 한글 문장을 코드로 조립할 때는 조사( 은/는 )가 어긋나지 않는지 출력해서 확인한다. 다음부터는 React와 Git이다. Tags : Python 클래스 상속 super 다형성 파일입출력 클래스변수 복습 개발자 학습기록
"HTTPS는 어떻게 동작할까?"를 파고들다 보니 생각보다 엮인 개념이 많았어. 내가 헷갈렸던 순서 그대로 정리해 볼게. "공개키로 암호화하면 되는 거 아냐?" → "근데 그 공개키를 어떻게 믿지?" → "중간에 해커가 끼어들면?" 1. HTTP는 왜 위험할까? HTTP는 데이터를 평문 그대로 보내. 클라이언트와 서버 사이에는 공유기, ISP, 라우터 같은 장비가 잔뜩 있는데, 그중 어디서든 패킷을 들여다보면 세 가지 문제가 생겨. 문제 설명 도청 비밀번호, 카드번호를 그대로 읽을 수 있어 변조 중간에서 내용을 바꿔치기할 수 있어 사칭 내가 접속한 서버가 진짜인지 알 수 없어 그래서 HTTP를 TLS 로 감싼 게 HTTPS야. 참고로 SSL은 TLS의 옛 이름이야. SSL은 보안 문제 때문에 이제 안 쓰고, 지금은 TLS 1.2랑 1.3을 써. 2. 먼저 알아둘 것: 대칭키와 비대칭키 대칭키 암호화 키랑 복호화 키가 같아. 빨라. 근데 이 키를 상대한테 어떻게 안전하게 전달하지? 이게 문제야. 비대칭키 (공개키 / 개인키) 키가 한 쌍 이야. 공개키는 누구한테나 줘도 되고, 개인키는 주인만 가져. 용도는 두 가지야. 암호화 : 공개키로 잠그면 개인키로만 열 수 있어. 서명 : 개인키로 서명하면 공개키로 "진짜 주인이 서명했네"를 확인할 수 있어. 대신 느려. 그래서 TLS는 둘을 섞어 써. 비대칭키로 안전하게 준비하고, 실제 데이터는 빠른 대칭키(세션 키)로 주고받는 거지. 3. 처음 떠올린 방법: RSA 키 교환 (TLS 1.2까지) 제일 직관적인 방법은 이거야. 서버는 통신 전에 미리 공개키랑 개인키 를 만들어 둬. 공개키는 암호화용, 개인키는 복호화용이야. 클라이언트는 세션 키(대칭키) 를 만들어. 이 키 하나로 암호화도 하고 복호화도 해. 클라이언트는 서버한테 공개키 를 받아. 그 공개키로 세션 키를 암호화해서 서버한테 안전하게 보내. 서버는 개인키로 복호화 해서 세션 키를 얻게 돼. 이제 클라이언트랑 서버는 같은 세션 키로 암호화해서 보내고, 받은 쪽은 복호화해. 중간에서 엿봐도 암호화된 세션 키만 보이고, 개인키가 없으니 못 풀어. 완벽해 보이지? 실제 TLS에서는 세션 키를 그대로 보내지 않고, pre-master secret 이라는 비밀값을 보낸 다음 양쪽이 이 값으로 세션 키를 만들어. 그래도 "공개키로 암호화해서 보내고 개인키로 푼다"는 원리는 같아. 근데 3번 에 큰 구멍이 있어. 4. 문제: 그 공개키, 진짜 서버 거 맞아? 클라이언트는 받은 공개키가 진짜 서버 건지 확인할 방법이 없어. 여기서 MITM(Man-In-The-Middle, 중간자 공격) 이 가능해지는 거야. MITM 공격 시나리오 해커가 공용 와이파이 같은 데서 클라이언트랑 서버 사이에 끼어들었다고 해 보자. 해커가 서버 공개키를 가로채고, 자기 공개키 를 서버 건 척 클라이언트한테 줘. 클라이언트는 아무것도 모르고 해커 공개키 로 세션 키를 암호화해. 해커는 자기 개인키로 복호화 해서 세션 키를 얻어. 그다음 서버 공개키로 다시 암호화 해서 서버한테 넘겨. 클라이언트도 서버도 정상적으로 통신하는 줄 알지만, 해커는 대화를 다 읽고 바꿀 수도 있게 돼. 암호화는 잘 되고 있었어. 문제는 "누구랑 암호화하고 있는지"를 확인 안 한 거 지. 5. 해결: CA와 인증서 그럼 "이 공개키는 진짜 naver.com 거야"라고 믿을 만한 제3자가 보증 해 주면 되겠지? 그게 CA(Certificate Authority, 인증기관) 야. 5-1. 인증서는 어떻게 발급될까? 서버가 키 쌍을 직접 만들어. 개인키는 절대 서버 밖으로 안 나가. CA가 대신 만들어 주면 CA도 개인키를 알게 되니까 안 되거든. 서버가 공개키 + 도메인 정보 를 담은 요청서( CSR )를 CA에 보내. CA(정확히는 등록 업무를 하는 RA )가 "이 도메인 주인이 진짜 너 맞아?" 를 확인해. 확인되면 CA가 인증서를 만들어. 인증서 내용(도메인, 서버 공개키, 유효기간, 발급자 등)으로 해시값 을 만들고, 그 해시값에 CA 개인키로 서명 해. 인증서 내용 + CA 서명 을 묶어서 서버한테 발급해. 처음엔 "CA가 서버 공개키를 암호화 한다"고 이해했는데 틀렸어. 서버 공개키는 인증서 안에 그대로 보여. CA가 하는 건 암호화가 아니라 서명 이야. 내용을 숨기려는 게 아니라 "CA가 이 내용을 보증하고, 중간에 안 바뀌었다" 를 증명하려는 거지. 5-2. 인증서에는 뭐가 들어 있을까? 5-3. 그럼 클라이언트는 인증서를 어떻게 검증할까? OS랑 브라우저에는 믿을 수 있는 CA의 인증서(공개키) 가 미리 들어 있어. 이걸 루트 인증서 저장소 라고 해. Microsoft, Apple, Google, Mozilla가 각자 기준을 정해 두고, 그걸 통과한 CA만 이 목록에 넣어줘. 검증은 이렇게 해. 인증서 내용으로 직접 해시값을 계산 해. 인증서에 붙은 서명을 미리 갖고 있던 CA 공개키로 검증 해. 서명이 맞으면 → CA가 보증한 내용이고, 중간에 안 바뀌었다 는 거지. 추가로 확인해. 도메인 이 지금 접속하려는 주소랑 같아? 유효기간 은 안 지났어? 실제로는 루트 CA가 직접 서명하지 않고 루트 CA → 중간 CA → 서버 인증서 순으로 서명이 이어져. 이걸 인증서 체인 이라고 하고, 브라우저는 이 체인을 따라 올라가서 루트 저장소에 있는 CA까지 닿는지 확인해. 5-4. 근데 인증서는 복사하면 그만 아냐? 맞아. 인증서는 공개된 정보라 해커도 naver.com의 진짜 인증서를 그대로 복사해서 보여줄 수 있어. 근데 해커한테는 그 인증서 속 공개키의 개인키 가 없잖아. RSA 키 교환 에서는 클라이언트가 인증서 속 공개키로 세션 키를 암호화해서 보내. 해커는 개인키가 없으니 못 풀어. TLS 1.3 에서는 서버가 핸드셰이크 내용에 자기 개인키로 서명 해서 보내( CertificateVerify ). 해커는 개인키가 없으니 이 서명을 못 만들어. 인증서 로 "이 공개키는 naver.com 거"라는 걸 확인하고, 개인키를 쓸 수 있는지 로 "지금 대화하는 상대가 그 주인"이라는 걸 확인하는 거야. 둘 다 있어야 신원 확인이 끝나. 5-5. 그럼 MITM은 어떻게 막히는 걸까? 해커가 할 수 있는 건 두 가지인데, 둘 다 실패해. 해커의 시도 결과 자기 공개키 로 가짜 인증서를 만들어 보여줌 믿을 만한 CA 서명이 없거나 도메인이 안 맞아서 브라우저가 경고 를 띄워 진짜 인증서를 복사 해서 보여줌 개인키가 없어서 복호화도 서명도 못 해 6. 남은 문제: 개인키가 나중에 털리면? RSA 키 교환엔 문제가 하나 더 있어. 서버 개인키 하나로 모든 세션 키를 풀 수 있다 는 거야. 해커가 오늘 암호화된 통신을 전부 녹화 해 뒀다가 몇 년 뒤에 서버 개인키를 훔치면? → 녹화해 둔 세션 키를 복호화해서 → 과거 대화를 전부 풀 수 있어. 그래서 나온 게 ECDHE 야. 6-1. ECDHE: 세션 키를 아예 안 보낸다 핵심은 "세션 키를 암호화해서 보내지 말고, 양쪽이 각자 계산해서 같은 값을 얻자" 는 거야. 작은 숫자로 직접 해 보면 바로 이해돼. (원조인 DH 방식이고, ECDHE는 거듭제곱 대신 타원곡선 연산을 쓸 뿐 원리는 같아.) 1 미리 공개된 숫자 표준으로 정해져 있어서 브라우저랑 서버 코드에 이미 들어 있어. g = 5, p = 23 ( mod 23 은 23으로 나눈 나머지) 2 각자 비밀 숫자를 골라 (절대 안 보냄) 클라이언트: a = 6 서버: b = 15 3 공개값을 계산해서 주고받아 클라이언트: 56 mod 23 = 8 → 서버한테 보내 서버: 515 mod 23 = 19 → 클라이언트한테 보내 4 받은 공개값에 내 비밀 숫자로 계산해 클라이언트: 196 mod 23 = 2 서버: 815 mod 23 = 2 둘 다 2가 나와. 이게 공유 비밀값이야. 왜 같은 값이 나올까? 클라이언트: 196 = (515)6 = 5^(15×6) 서버: 815 = (56)15 = 5^(6×15) 곱하는 순서만 다르지 결국 둘 다 5^(a×b)를 계산한 거야. 엿본 사람은 왜 못 구할까? 엿본 사람은 5, 23, 8, 19만 알아. 공유 비밀값을 구하려면 "5를 몇 번 거듭제곱해야 8이 되지?"를 풀어야 하는데, 실제로는 수백 자리 숫자를 써서 현실적으로 못 풀어. 6-2. 전방 비밀성 (Forward Secrecy) ECDHE의 E(Ephemeral) 는 "일회용"이란 뜻이야. 비밀 숫자를 연결할 때마다 새로 만들고 끝나면 버려. 그리고 서버 개인키는 이제 세션 키 계산엔 안 쓰고, 신원 증명(서명)에만 써. 그래서 나중에 개인키가 털려도 과거 대화는 안전 해. RSA 키 교환 ECDHE 세션 키 클라이언트가 만들어서 암호화해 보냄 안 보내고 양쪽이 각자 계산 서버 개인키 역할 세션 키 복호화 신원 증명(서명)만 개인키 유출 시 과거 대화까지 다 풀림 과거 대화는 안전 TLS 1.3 제거됨 이것만 남음 6-3. 그럼 ECDHE만 있으면 MITM도 막힐까? 아니야. ECDHE만으로는 MITM을 못 막아. 해커가 중간에서 클라이언트랑은 해커 공개값으로, 서버랑은 또 다른 해커 공개값으로 각각 키 교환을 하면, 연결 두 개를 따로 맺고 중계 할 수 있거든. 4장에서 본 공격이랑 똑같은 구조야. 그래서 TLS 1.3에서는 서버가 ECDHE 공개값이 들어 있는 핸드셰이크 전체에 개인키로 서명 해. 해커가 공개값을 바꿔치기하면 서명이 안 맞아서 바로 들통나. ECDHE는 "도청"을 막고, 인증서 + 서명은 "사칭(MITM)"을 막아. 둘 다 있어야 해. 7. TLS 1.3 핸드셰이크 전체 흐름 단계 하는 일 ClientHello / ServerHello 쓸 방식을 맞추고, 랜덤값이랑 키 교환 재료를 주고받아 세션 키 계산 각자 공유 비밀값을 계산하고 랜덤값이랑 섞어서 세션 키를 만들어 Certificate 서버가 인증서를 보내고, 브라우저가 CA 체인으로 검증해 CertificateVerify 서버가 개인키로 서명해서 인증서의 진짜 주인이라는 걸 증명해 Finished 지금까지 주고받은 메시지가 안 바뀌었는지 서로 확인해 TLS 1.2는 왕복이 2번 필요했는데, TLS 1.3은 키 교환 재료를 첫 인사에 바로 실어서 왕복 1번(1-RTT) 으로 줄였어. 세션 키가 일찍 만들어지니까 인증서부터 암호화 되는 것도 차이점이야. 헷갈리는 키 세 종류 키 언제 만들어지나 용도 ECDHE 비밀 숫자 / 공개값 연결마다 새로 만들고 버림 공유 비밀값 계산 서버 개인키 / 공개키 서버가 미리 만들어서 오래 보관 신원 증명 (서명) 세션 키 핸드셰이크 중에 양쪽이 계산 실제 데이터 암호화 (대칭키) 8. 그래도 MITM이 성공하는 경우 TLS가 MITM을 막아 주긴 하는데, 이런 경우엔 뚫릴 수 있어. 사용자가 인증서 경고를 무시할 때 "이 연결은 안전하지 않습니다"에서 "계속 진행"을 누르면 해커 인증서를 믿게 돼. 해커의 루트 인증서가 PC에 설치됐을 때 악성 프로그램이 루트 저장소에 가짜 CA를 넣으면, 그 CA로 서명한 인증서는 다 통과해. (회사 보안 프록시가 트래픽을 검사하는 것도 사실 이 방식이야.) CA가 해킹당하거나 잘못 발급했을 때 이걸 대비해서 인증서 폐기 목록(CRL, OCSP)이나, 발급 기록을 공개하는 Certificate Transparency 같은 장치가 있어. 처음 접속을 HTTP로 할 때 (SSL Stripping) http:// 로 들어오는 순간 해커가 HTTPS로 넘어가는 걸 막고 평문으로 중계할 수 있어. 서버가 HSTS 헤더로 "앞으로 무조건 HTTPS로만 접속해"라고 알려줘서 막아. 9. 정리 HTTP는 평문이라 도청, 변조, 사칭 에 취약해. 공개키로 세션 키를 암호화해 보내면 도청은 막지만, 공개키가 진짜인지 모르면 MITM에 당해. 그래서 CA가 서버 공개키에 서명한 인증서 로 공개키 주인을 보증하고, 서버는 개인키 서명 으로 자기가 그 주인이라는 걸 증명해. RSA 키 교환은 개인키가 털리면 과거 대화까지 풀려서, TLS 1.3은 세션 키를 아예 안 보내는 ECDHE 만 남겼어. 결국 비대칭키로 신원 확인이랑 키 합의를 하고, 실제 데이터는 대칭키(세션 키)로 빠르게 암호화 하는 거야.