요즘 취업 준비하면서 드는 고민 IT 쪽으로 취업을 준비한 지 꽤 됐는데, 요즘은 정말 쉽지 않다는 생각이 계속 든다. 몇 년 전만 해도 개발 직군은 사람이 부족하다는 말이 많았는데 지금은 분위기가 완전히 달라졌다. 채용 공고 자체가 눈에 띄게 줄었고 신입을 뽑는 곳은 더 적어졌다.여기에 AI까지 빠르게 발전하면서 고민이 하나 더 생겼다. 간단한 코드는 AI가 금방 짜주는 시대에 신입 개발자에게 기업이 기대하는 게 뭘까. 지금 내가 준비하는 방향이 맞는 걸까. 이런 생각이 들 때마다 막막했다. 혼자 공고를 찾아보고, 자소서를 쓰고, 포트폴리오를 고치는 걸 반복했지만 잘하고 있는 건지 확인할 방법이 없었다. 제대로 된 피드백을 받을 곳이 없다는 게 제일 답답했다. 그러다 알게 된 게 제베(제로베이스)이다. 취업정보회사 제로베이스는 어떤 서비스일까 간단히 말하면 취업을 준비하는 사람들을 위한 취업 코칭 서비스다. 컨설턴트와 현직자 멘토가 함께 붙어서 서류부터 면접까지 전반적인 취업 준비를 도와준다. 혼자 준비할 때는 방향을 잡는 것부터가 어려웠는데, 여기서는 지금 내 상태를 먼저 점검하고 거기에 맞춰서 무엇을 준비해야 할지 같이 정해준다는 점이 좋았다. 컨설턴트 상담은 상담 전에 작성하는 사전 질문지를 바탕으로 진행된다. 처음엔 그냥 형식적인 설문이겠거니 했는데, 막상 채워보니 생각이 달라졌다. 지금 내가 어디쯤 와 있는지, 무엇이 고민인지, 상담에서 어떤 걸 얻고 싶은지를 차근차근 정리하게 만들어준다. 상담 전에 뭘 준비해야 할지 막막한 사람에게는 이 질문지가 좋은 지침서가 되어줄 것 같다. 덕분에 상담 시간도 헤매지 않고 핵심적인 이야기에 집중할 수 있었다. 부담 없이 질문할 수 있는 구조 솔직히 이런 서비스를 쓰기 전에 가장 걱정했던 건 "이런 것까지 물어봐도 되나?" 하는 부분이었다. 정해진 상담 시간에만 질문할 수 있으면 사소한 궁금증은 그냥 넘기게 되니까. 그런데 제베는 슬랙으로 소통하다 보니 궁금한 게 생길 때마다 바로 메시지를 남길 수 있었다. 컨설턴트님과 멘토님도 친절하게 답해주셔서 질문하는데 부담이 덜했다. 마무리 취업 시장이 어렵다는 건 여전히 사실이다. 그래도 혼자 막막하게 버티는 것과 방향을 같이 잡아주고 언제든 물어볼 수 있는 사람이 있는 건 정말 다르다는 걸 느꼈다. 나처럼 IT 취업 준비하면서 어디서부터 손대야 할지 막막한 사람이라면 한 번쯤 제베를 알아보는 걸 추천하고 싶다.
D-ask는 내가 처음으로 진행했던 프로젝트다. 1학년 때 처음 만들었던 프로젝트를 지금 다시 꺼내 v2라는 이름으로 처음부터 다시 만들고 있다. 왜 이미 한 번 만들었던 프로젝트를 다시 만들게 되었는지, 그리고 이번에는 무엇을 다르게 해보고 싶은지 정리해보려고 한다. 1. D-ask v1은 왜 시작했는가 학교생활을 하다 보면 학교 홈페이지에 올라온 문서를 찾아볼 일이 종종 있다. 하지만 필요한 정보가 여러 공지와 문서에 흩어져 있다 보니 학생들이 매번 직접 문서를 찾아 내용을 확인해야 했다. 그래서 이런 생각에서 D-ask를 시작했다. 학교 문서가 한곳에 모여 있고, 학생이 직접 문서를 찾는 대신 질문만 하면 필요한 정보를 알려줄 수 없을까? 이 문제를 해결하기 위해 학교 문서를 수집하고, 문서를 기반으로 학생의 질문에 답변하는 서비스를 만들어보기로 했다. 그게 D-ask v1의 시작이었다. 2. v1에서는 무엇을 만들었는가 D-ask v1에서는 RAG를 이용해 학교 문서를 기반으로 답변하는 Q&A 챗봇 을 만들었다. 사용자가 질문하면 관련된 학교 문서를 검색하고, 검색한 내용을 LLM에 전달해 답변을 생성하는 방식이었다. 당시에는 RAG를 직접 구현하고 실제 학교 문서를 이용해 답변이 생성되는 것만으로도 신기했다. 하지만 프로젝트를 진행하면서 여러 문제를 만나기 시작했다. 문서 안에 분명 답이 있는데 관련 내용을 제대로 검색하지 못하는 경우가 있었고, 검색 결과가 있어도 답변 생성 과정에서 문제가 발생하기도 했다. 그때의 나는 이런 문제가 발생하면 주로 한 가지를 생각했다. “어떻게 하면 일단 이 문제를 해결할 수 있을까?” D-ask v1 프로젝트 회고 3. 만들고 나서 어떤 한계와 의문을 느꼈는가 대표적인 사례가 임베딩 모델이었다. 검색이 제대로 되지 않는 문제가 발생했을 때, 당시 사용하던 임베딩 모델이 한국어 문서를 충분히 잘 처리하지 못한다고 판단했다. 그래서 한국어 성능이 더 좋다고 알려진 다른 임베딩 모델로 교체했다. 모델을 변경한 뒤 당시 문제가 발생했던 일부 질문에서는 이전보다 정상적으로 검색되는 것을 확인할 수 있었다. 그런데 지금 다시 생각해보면 여기에는 문제가 있었다. 왜 그 모델이어야 하는지 설명할 수 없었다. 모델 A보다 모델 B가 정말 전체적으로 검색을 더 잘하는지 측정하지 않았다. 어떤 종류의 질문에서 얼마나 좋아졌는지도 알 수 없었다. 결국 당시의 선택 과정은 이랬다. 문제 발생 ↓ 다른 모델로 교체 ↓ 잘 되는 것 같음 ↓ 채택 지금 생각하면 모델을 바꾸기 전에 먼저 이런 질문을 했어야 했다. 현재 검색 성능이 실제로 얼마나 나쁜가? 모델을 바꿨을 때 얼마나 좋아졌는가? 그 차이를 무엇으로 판단할 것인가? 하지만 v1에는 이 질문에 답할 수 있는 평가 기준이 없었다. 돌아가는 것과 운영할 수 있는 것은 달랐다 평가만의 문제도 아니었다. D-ask v1은 챗봇 자체는 동작했지만, 프로젝트를 완성하고 나니 한 번 동작하는 RAG를 만드는 것과 지속적으로 운영할 수 있는 서비스를 만드는 것은 다른 문제 라는 생각이 들었다. 새로운 학교 문서가 추가되거나 기존 문서가 수정되면 이를 어떻게 반영할 것인지에 대한 구조가 부족했다. 검색 방식을 변경했을 때 이전보다 정말 좋아졌는지 검증하기도 어려웠다. 오류가 발생했을 때도 원인이 문서 파싱인지, 검색인지, 임베딩인지, 답변 생성인지 명확하게 분리해서 판단하기 어려웠다. 무엇보다 프로젝트에서 했던 여러 기술 선택에 대해 “왜 이렇게 했나요?” 라는 질문을 받았을 때 충분한 근거를 제시하기 어려웠다. 처음 만드는 프로젝트였던 만큼 일단 기능을 완성하는 데 집중했고, 평가와 운영은 그다음 문제라고 생각했기 때문이다. 4. 왜 v1을 고치는 대신 v2를 시작했는가 처음에는 v1을 계속 수정하는 것도 생각했다. 하지만 다시 생각해보니 바꾸고 싶은 것은 임베딩 모델 하나나 특정 기능 하나가 아니었다. 문서를 어떻게 준비할 것인지부터 검색 결과를 어떻게 평가할 것인지, 모델을 어떤 기준으로 선택할 것인지, 문제가 발생하면 어떻게 찾아낼 것인지까지 프로젝트를 만드는 방식 자체를 바꿔보고 싶었다. 기존 프로젝트에 기능을 하나씩 덧붙이기보다 v1을 만들면서 생긴 의문들을 가지고 처음부터 다시 설계해보기로 했다. 그래서 D-ask v2를 개인 프로젝트로 다시 시작했다. 5. v2에서는 무엇을 다르게 해보고 싶은가 v2의 목표는 단순히 v1보다 기능이 많은 챗봇을 만드는 것이 아니다. 이번에는 내가 내린 기술적인 결정을 측정하고 설명할 수 있는 서비스를 만드는 것 을 목표로 잡았다. 예를 들어 임베딩 모델을 선택한다면 단순히 유명하거나 성능이 좋다고 알려진 모델 하나를 가져오는 방식으로 결정하지 않으려고 한다. 여러 후보를 선정하고 동일한 데이터와 평가 기준으로 비교한 뒤, 실제 측정 결과를 근거로 선택하려고 한다. Chunking, Retrieval, Reranking, LLM 같은 다른 요소도 같은 방식으로 접근하고 싶다. 후보 선정 ↓ 동일한 조건에서 실험 ↓ 성능 측정 ↓ Trade-off 비교 ↓ 기술 선택 ↓ 선택한 이유 기록 그래서 D-ask v2에서는 RAG 파이프라인부터 빠르게 구현하지 않았다. 먼저 Golden Dataset을 만들고, 평가 기준을 정의하고, 동일한 조건에서 여러 방법을 비교할 수 있는 Benchmark 환경을 만드는 작업 부터 시작했다. 이 과정은 다음 글에서 자세히 다뤄보려고 한다. 그리고 이번에는 개발에서 끝내지 않고 실제 서비스까지 만들어보는 것 도 목표로 하고 있다. v1처럼 “챗봇이 동작한다”에서 프로젝트를 끝내는 것이 아니라, 실제로 배포해 사용자가 사용할 수 있는 형태로 만들고 싶다. 6. D-ask v2를 통해 무엇을 배우고 검증하고 싶은가 D-ask v2를 통해 처음부터 완벽한 RAG 시스템을 만드는 것이 목표는 아니다. 오히려 실제로 서비스를 운영하면서 다음 과정을 경험해보고 싶다. 배포 ↓ 사용 ↓ 문제 발견 ↓ 원인 분석 ↓ 개선 ↓ 다시 측정 내가 선택한 임베딩 모델이 정말 적절했는지, Retrieval 방식을 변경했을 때 실제로 검색 성능이 좋아지는지, 검색 성능의 개선이 최종 답변 품질의 개선으로 이어지는지 직접 측정하고 검증해보고 싶다. 그리고 배포 이후에는 개발하면서 예상하지 못했던 문제도 실제 사용자와 데이터를 통해 발견해보고 싶다. 결국 이번 프로젝트에서 경험하고 싶은 것은 단순히 RAG를 구현하는 방법 만은 아니다. 문제를 발견하고, 원인을 찾고, 여러 해결 방법을 비교하고, 하나를 선택한 뒤, 그 선택이 실제로 효과가 있었는지 다시 측정하는 과정까지 경험해보는 것이 목표다. D-ask v1이 나에게 RAG 서비스를 처음 만들어본 프로젝트 였다면, D-ask v2는 RAG 서비스를 어떻게 평가하고, 기술을 선택하고, 실제로 운영할 것인지 고민하는 프로젝트 로 만들어보려고 한다. 다음 글에서는 그 첫 번째 과정으로, D-ask v2에서 RAG 파이프라인을 구현하기 전에 왜 평가 환경부터 만들기 시작했는지 정리해보려고 한다.
[Outbreak Breaker] 인게임 일시정지 메뉴, 적 사운드 재생 구조, 그리고 해머 텔레포트 버그 오늘은 커밋 두 개를 올렸다. 하나는 어제 발견한 해머 텔레포트 버그를 잡은 거고, 다른 하나는 일시정지 메뉴 기본 구현이랑 적 사운드 재생 구조를 새로 짜넣은 거다. 1. 해머 패시브→선택 전환 시 무기 위치 텔레포트 버그 수정 어제 해머 패시브 이동 방식을 무기 액터 자체를 옮기는 방식으로 바꾸고 나서, 패시브랑 선택(액티브) 상태를 왔다갔다 전환할 때 무기가 갑자기 엉뚱한 위치로 순간이동하는 버그가 있었다. 원인은 상태 전환 시 해머 패시브 이동용 타임라인을 재생 여부와 상관없이 무조건 Stop + Set New Time으로 리셋하고 있던 것이었다. Set New Time 은 타임라인이 재생 중이 아니어도 지정한 시간 지점에서 Update를 한 번 동기적으로 발동시켜버리는데( SetPlaybackPosition(NewTime, bFireUpdate=true, bFireUpdateEventsInBetween=false) ), 그 시점의 커브 위치/회전 값이 즉시 액터에 적용되는 구조였다. 그러니까 타임라인이 idle 상태여도 이 호출 자체는 실행되면서, 무기가 커브상의 임의 좌표로 스냅되어 텔레포트하는 것처럼 보였던 거다. IsPlaying() 체크를 추가해서 실제로 재생 중일 때만 Stop + Set New Time을 호출하도록 가드를 걸어서 해결했다. 겸사겸사 정리도 좀 했다. 마그넷 마인 스폰용 액터를 Actor/Weapon 폴더에서 Weapon/Props 폴더로 이동 — 무기 본체랑 무기가 만들어내는 부속 오브젝트를 폴더 구조상 구분해두려고 나이아가라 이펙트가 일시적으로 비정상 표시되던 현상이 있었는데, 참조 텍스처를 재오픈하니 정상화됐다. 로직/데이터 문제가 아니라 에디터 텍스처 컴파일 캐시가 스테일해진 이슈로 추정하고 있고, 재발하면 패턴을 더 확인해볼 예정 2. 인게임 일시정지 메뉴 기본 구현 어제 만든 레이아웃 기준으로 일시정지 메뉴 위젯을 붙였다. 계속하기 / 설정 / 타이틀로 돌아가기 / 게임 종료 네 개 항목에 하단엔 "ESC 눌러 계속하기" 안내 텍스트를 넣었다. 지금은 계속하기(ESC)만 실제로 동작하고 나머지 세 개는 아직 로직을 안 붙인 상태. 지금 단계에서는 기본 UMG 구조로 단순하게 구현해뒀고, 나중에 CommonUI를 도입하게 되면 그 구조로 교체할 계획이다. 3. 적 사운드 재생 구조 추가 및 컨커런시 설정 어제 SK_Whisper 사운드 프롬프트 정리했던 걸 바탕으로, 적 사운드를 실제로 재생하는 구조를 짰다. FOBEnemySoundSet 구조체를 추가해서 Spawn/Attack/Death 사운드를 각각 배열로 관리한다. 뽑을 때 직전에 재생한 인덱스를 제외하고 범위를 하나 줄여서 뽑는 방식으로, 같은 사운드가 루프 없이 연속으로 중복 재생되는 걸 방지했다 AOBEnemy 에 GetRandomSpawnSound / GetRandomAttackSound / GetRandomDeathSound BlueprintCallable 래퍼 함수를 추가 근접/원거리 공용으로 쓸 수 있는 AnimNotify( OBAnimNotify_EnemyAttackSound )를 추가해서 공격 사운드 재생을 처리하도록 했다 적 스폰/공격/사망 사운드 각각에 Sound Concurrency를 구성했다: CC_EnemySpawn (Max 6, Stop Oldest), CC_EnemyAttack (Max 8, Stop Lowest Priority, Retrigger 0.1초), CC_EnemyDeath (Max 5, Stop Lowest Priority, Retrigger 0.05초) — 셋 다 Volume Scale Mode를 Priority로 둬서 겹칠수록 자연스럽게 감쇠되도록 처리했다 4. 원거리 공격 이펙트 적용 및 발사 방향 버그 수정 적 원거리 공격 히트 이펙트를 실제로 적용 완료했다. 그 과정에서 벽(WorldStatic)에 대한 오버랩 처리가 빠져있던 걸 발견해서 추가 — 기존엔 대상 판정에 WorldStatic이 안 들어가 있어서 벽에 맞아도 히트 이펙트가 안 터졌었다 원거리 공격 발사 방향도 손봤다. 기존엔 플레이어의 현재 위치를 직접 조준해서 쏘고 있었는데, 이게 부자연스러워서 적의 Forward Vector 방향으로 발사하도록 변경했다 정리하면서 안 쓰던 ShockWaves 이펙트도 제거 다음에 할 것 일시정지 메뉴는 지금은 구조/레이아웃만 미리 만들어둔 것이고, 설정/타이틀로 돌아가기/게임 종료 로직 연결은 CommonUI를 먼저 공부하고 적용해볼 예정이라 시기는 미정 내일은 무기 쪽 작업을 계속 이어갈 예정
문자열을 2글자씩 한 칸씩 이동하며 자른다. 두 문자 모두 영문자인 경우만 List에 저장. 다중집합이므로 중복을 제거하면 안 된다. 교집합 계산 시 매칭된 원소를 복사본에서 제거. 합집합 = A크기 + B크기 - 교집합. 자카드 = 교집합 / 합집합. int 나눗셈 주의 → double 형변환. 합집합이 0이면 65536. 핵심: 중복을 허용하는 집합에서 매칭 문제 → 한 번 매칭한 원소를 다시 사용하지 않는다.
파이썬 초보 탈출 - 코딩 면허시험 문제풀이 목차 Q1. 문자열 바꾸기 Q2. 딕셔너리 값 추출하기 Q3. 리스트 더하기와 extend() Q4. 리스트 총합 구하기 Q5. 피보나치 수열 Q6. 숫자의 총합 구하기 Q7. 한 줄 구구단 Q8. 파일 역순 저장 Q9. 총합과 평균 구하기 Q10. Calculator 클래스 Q11. 모듈 불러오기 Q12. 오류와 예외 처리 Q13. DashInsert Q16. 모스 부호 해독 Q17. 기초 메타 문자 Q18. 문자열 검색 Q19. 그루핑 Q20. 전방 탐색 Q1. 문자열 바꾸기 문자열 a:b:c:d 에서 : 을 기준으로 문자열을 나누고 다시 # 으로 연결한다. 풀이 a = "a:b:c:d" result = "#".join(a.split(":")) print(result) 실행 결과 a#b#c#d 동작 과정 먼저 a.split(":") 을 실행하면 ['a', 'b', 'c', 'd'] 가 된다. 그리고 "#".join(...) 을 사용하면 a#b#c#d 처럼 리스트의 요소 사이에 # 을 넣어 다시 문자열로 만든다. "a:b:c:d" ↓ split(":") ↓ ['a', 'b', 'c', 'd'] ↓ "#".join() ↓ "a#b#c#d" 핵심 split() → 문자열 나누기 join() → 문자열 연결하기 Q2. 딕셔너리 값 추출하기 딕셔너리에 존재하지 않는 Key를 a['C'] 처럼 조회하면 KeyError 가 발생한다. a = { 'A': 90, 'B': 80 } 이때 C 가 없다면 기본값 70 을 얻고 싶다. 풀이 get() 을 사용하면 간단하다. a = { 'A': 90, 'B': 80 } print(a.get('C', 70)) 실행 결과 70 get() 딕셔너리.get(Key, 기본값) 형태로 사용할 수 있다. a.get('A', 70) A 가 존재하므로 90 을 반환한다. 반면 a.get('C', 70) 에서는 C 가 없기 때문에 70 을 반환한다. 핵심 a['C'] → Key가 없으면 오류 a.get('C') → Key가 없으면 None a.get('C', 70) → Key가 없으면 70 Q3. 리스트 더하기와 extend() 다음 두 코드는 결과만 보면 동일하다. + a = [1, 2, 3] a = a + [4, 5] print(a) [1, 2, 3, 4, 5] extend() a = [1, 2, 3] a.extend([4, 5]) print(a) [1, 2, 3, 4, 5] 하지만 동작 방식에는 차이가 있다. + a = a + [4, 5] 은 기존 리스트를 수정하는 것이 아니라 새로운 리스트를 만들어 a 가 다시 가리키게 한다. a = [1, 2, 3] print(id(a)) a = a + [4, 5] print(id(a)) 두 id() 값이 달라진다. 기존 리스트 [1, 2, 3] + [4, 5] ↓ 새로운 리스트 [1, 2, 3, 4, 5] extend() a.extend([4, 5]) 는 기존 리스트 자체에 값을 추가한다. a = [1, 2, 3] print(id(a)) a.extend([4, 5]) print(id(a)) 객체의 id() 가 유지된다. 핵심 방법 동작 a + b 새로운 리스트 생성 a.extend(b) 기존 리스트 수정 Q4. 리스트 총합 구하기 점수 중에서 50점 이상인 값만 더한다. A = [20, 55, 67, 82, 45, 33, 90, 87, 100, 25] 풀이 A = [20, 55, 67, 82, 45, 33, 90, 87, 100, 25] result = 0 for score in A: if score >= 50: result += score print(result) 실행 결과 481 동작 과정 20 → X 55 → +55 67 → +67 82 → +82 45 → X 33 → X 90 → +90 87 → +87 100 → +100 25 → X 조금 더 간단하게 result = sum(score for score in A if score >= 50) 하지만 지금 단계에서는 for + if 방식의 흐름을 정확히 이해하는 것이 더 중요하다. Q5. 피보나치 수열 피보나치 수열은 앞의 두 값을 더해 다음 값을 만든다. 0, 1, 1, 2, 3, 5, 8, 13 ... 즉 0 + 1 = 1 1 + 1 = 2 1 + 2 = 3 2 + 3 = 5 ... 형태이다. n개의 피보나치 수 만들기 def fibonacci(n): result = [] a = 0 b = 1 for _ in range(n): result.append(a) a, b = b, a + b return result n = int(input("숫자 입력: ")) print(fibonacci(n)) 예를 들어 숫자 입력: 8 결과 [0, 1, 1, 2, 3, 5, 8, 13] 핵심 코드 a, b = b, a + b 예를 들어 a = 0 b = 1 이라면 a = 1 b = 1 이 되고 다음에는 a = 1 b = 2 가 된다. Q6. 숫자의 총합 구하기 사용자가 다음과 같이 입력한다고 해보자. 65,45,2,3,45,8 콤마를 기준으로 값을 나눈 후 숫자로 변환하여 합계를 구한다. 풀이 data = input("값을 입력하세요: ") numbers = map(int, data.split(",")) result = sum(numbers) print(result) 실행 결과 168 동작 과정 "65,45,2,3,45,8" ↓ split(",") ['65', '45', '2', '3', '45', '8'] ↓ map(int, ...) 65, 45, 2, 3, 45, 8 ↓ sum() 168 Q7. 한 줄 구구단 사용자로부터 2~9 중 하나를 입력받아 해당 구구단의 결과를 한 줄로 출력한다. 풀이 dan = int( input("구구단을 출력할 숫자를 입력하세요(2~9): ") ) for i in range(1, 10): print(dan * i, end=" ") 예를 들어 2 를 입력하면 2 4 6 8 10 12 14 16 18 이 출력된다. end=" " 기본적인 print() 는 출력이 끝나면 줄을 바꾼다. print(1) print(2) 1 2 하지만 print(1, end=" ") print(2, end=" ") 처럼 사용하면 1 2 한 줄로 출력할 수 있다. Q8. 파일 역순 저장 파일 내용이 다음과 같다고 해보자. AAA BBB CCC DDD EEE 이를 역순으로 변경한다. EEE DDD CCC BBB AAA 풀이 with open("abc.txt", "r", encoding="utf-8") as f: lines = f.readlines() lines.reverse() with open("abc.txt", "w", encoding="utf-8") as f: f.writelines(lines) readlines() f.readlines() 파일을 줄 단위로 읽어 리스트로 반환한다. 예를 들어 [ "AAA\n", "BBB\n", "CCC\n", "DDD\n", "EEE\n" ] 와 같은 형태이다. reverse() lines.reverse() 리스트의 순서를 실제로 뒤집는다. AAA EEE BBB DDD CCC → CCC DDD BBB EEE AAA 주의할 점 텍스트 파일의 마지막 줄에 \n 이 없다면 역순 저장 후 EEEDDD 처럼 두 줄이 붙는 문제가 생길 수 있다. 즉 파일에서 줄바꿈 문자 \n 도 데이터의 일부 라는 점을 기억하자. Q9. 총합과 평균 구하기 파일에 숫자가 한 줄에 하나씩 저장되어 있다고 해보자. 70 60 55 75 95 90 80 80 85 100 이 값을 읽어 총합 평균 을 계산하고 평균을 result.txt 에 저장한다. 풀이 with open( "sample.txt", "r", encoding="utf-8" ) as f: numbers = [ int(line.strip()) for line in f ] total = sum(numbers) average = total / len(numbers) print("총합:", total) print("평균:", average) with open( "result.txt", "w", encoding="utf-8" ) as f: f.write(str(average)) 실행 결과 총합: 790 평균: 79.0 result.txt 79.0 평균 공식 평균 = 전체 합계 / 데이터 개수 즉 790 / 10 = 79.0 이다. Q10. Calculator 클래스 숫자 리스트를 입력받아 sum() avg() 기능을 제공하는 클래스를 만든다. 풀이 class Calculator: def __init__(self, numbers): self.numbers = numbers def sum(self): return sum(self.numbers) def avg(self): return sum(self.numbers) / len(self.numbers) cal1 = Calculator([1, 2, 3, 4, 5]) print(cal1.sum()) print(cal1.avg()) cal2 = Calculator([6, 7, 8, 9, 10]) print(cal2.sum()) print(cal2.avg()) 실행 결과 15 3.0 40 8.0 왜 self.result 를 계속 누적하면 안 될까? 예를 들어 def sum(self): for i in self.numbers: self.result += i 를 실행한 뒤 다시 avg() 에서 self.result += i 를 하면 앞에서 계산했던 값이 남아 있어 중복으로 더해진다. 따라서 합계와 평균처럼 단순 계산은 매번 sum(self.numbers) 를 사용하는 것이 훨씬 깔끔하다. Q11. 모듈 불러오기 다음 위치에 C:\doit\mymod.py 모듈이 있다고 가정해 보자. 파이썬에서 import mymod 를 사용할 수 있도록 하는 대표적인 방법은 세 가지가 있다. 1. sys.path 추가 import sys sys.path.append(r"C:\doit") import mymod 파이썬이 모듈을 검색하는 경로에 C:\doit 을 추가한다. 2. PYTHONPATH Windows 명령 프롬프트에서 set PYTHONPATH=C:\doit 환경 변수를 설정한 후 python 을 실행하면 import mymod 를 사용할 수 있다. 3. 모듈이 있는 위치에서 실행 cd C:\doit python 이후 import mymod 를 실행한다. 핵심 파이썬은 모듈을 sys.path 에 등록된 경로에서 찾는다. 확인하려면 import sys print(sys.path) 를 사용할 수 있다. Q12. 오류와 예외 처리 다음 코드를 살펴보자. result = 0 try: [1, 2, 3][3] "a" + 1 4 / 0 except TypeError: result += 1 except ZeroDivisionError: result += 2 except IndexError: result += 3 finally: result += 4 print(result) 실행 과정 초기값 result = 0 먼저 [1, 2, 3][3] 이 실행된다. 리스트 인덱스는 0 1 2 ↓ ↓ ↓ 1 2 3 까지만 존재한다. 따라서 IndexError 가 발생한다. except 이동 except IndexError: result += 3 가 실행된다. 0 + 3 = 3 그리고 try 블록의 나머지 코드는 실행하지 않는다. 따라서 "a" + 1 과 4 / 0 은 실행되지 않는다. finally finally 는 오류 발생 여부와 관계없이 실행된다. finally: result += 4 따라서 3 + 4 = 7 최종 결과 7 이다. Q13. DashInsert 숫자로 이루어진 문자열에서 홀수 + 홀수 가 연속되면 - 를 넣고, 짝수 + 짝수 가 연속되면 * 을 넣는다. 예를 들어 4546793 을 처리하면 454*67-9-3 이 된다. 풀이 data = "4546793" numbers = list(map(int, data)) result = [] for i, num in enumerate(numbers): result.append(str(num)) if i == len(numbers) - 1: continue next_num = numbers[i + 1] if num % 2 == 1 and next_num % 2 == 1: result.append("-") elif num % 2 == 0 and next_num % 2 == 0: result.append("*") print("".join(result)) 실행 결과 454*67-9-3 enumerate() for i, num in enumerate(numbers): 을 사용하면 인덱스 + 값 을 동시에 가져올 수 있다. 예를 들어 numbers = [4, 5, 4] 이면 i=0, num=4 i=1, num=5 i=2, num=4 순서로 반복한다. 홀수 판별 num % 2 == 1 나머지가 1 이면 홀수이다. 짝수 판별 num % 2 == 0 나머지가 0 이면 짝수이다. Q14 / Q15 참고한 게시글에는 Q13 다음이 바로 Q16으로 이어져 있어 Q14와 Q15 문제 및 풀이가 포함되어 있지 않다. Q16. 모스 부호 해독 모스 부호를 영어 문자로 변경하려면 모스 부호 → 알파벳 형태의 딕셔너리를 만들 수 있다. 모스 부호 딕셔너리 morse_code = { '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E', '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J', '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O', '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T', '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y', '--..': 'Z' } 해독 함수 def morse(src): result = [] words = src.split(" ") for word in words: chars = word.split() decoded = "" for char in chars: decoded += morse_code[char] result.append(decoded) return " ".join(result) 즉 모스 부호 ↓ 단어 단위 분리 ↓ 문자 단위 분리 ↓ 딕셔너리 조회 ↓ 영어 문자 변환 ↓ 다시 합치기 순서로 처리한다. Q17. 기초 메타 문자 정규식 a[.]{3,}b 를 해석해 보자. [.] 실제 점(.) 문자 를 의미한다. {3,} 앞의 패턴이 3번 이상 이라는 의미이다. 따라서 전체적으로 a + 점(.) 3개 이상 + b 이다. 보기 중 a....b 가 조건을 만족한다. 정답 2번 코드 확인 import re p = re.compile(r"a[.]{3,}b") print(p.match("acccb")) print(p.match("a....b")) print(p.match("aaab")) print(p.match("a.cccb")) a....b 만 정상적으로 매치된다. Q18. 문자열 검색 import re p = re.compile(r"[a-z]+") m = p.search("5 python") print(m.start() + m.end()) 패턴 분석 [a-z]+ 는 영문 소문자가 1개 이상 연속 이라는 의미이다. 따라서 5 python 에서 python 이 매치된다. 인덱스 문자 5 공백 p y t h o n 인덱스 0 1 2 3 4 5 6 7 따라서 m.start() 은 2 이고, m.end() 는 마지막 문자의 다음 위치이므로 8 이다. 따라서 2 + 8 = 10 정답 10 Q19. 그루핑 다음과 같은 데이터가 있다고 해보자. park 010-9999-9998 kim 010-9909-7789 lee 010-8789-7768 전화번호의 마지막 네 자리를 #### 로 변경한다. 정규식 작성 전화번호의 앞부분을 그룹으로 만든다. r"(\d{3}-\d{4})-\d{4}" 해석하면 ( 숫자 3개 - 숫자 4개 ) ↓ 그룹 1 - 숫자 4개 이다. 풀이 import re data = """ park 010-9999-9998 kim 010-9909-7789 lee 010-8789-7768 """ pattern = re.compile( r"(\d{3}-\d{4})-\d{4}" ) result = pattern.sub( r"\g<1>-####", data ) print(result) 결과 park 010-9999-#### kim 010-9909-#### lee 010-8789-#### \g<1> 첫 번째 그룹에서 매치된 문자열 을 의미한다. 따라서 010-9999 부분은 그대로 유지하고 9998 부분만 #### 로 변경한다. Q20. 전방 탐색 이메일 주소 중에서 마지막 도메인이 .com .net 인 경우만 매치하도록 만든다. 기본 패턴 .*[@].*[.].*$ 대략적으로 문자들 @ 문자들 . 문자들 형태의 이메일을 찾는다. 긍정형 전방 탐색 (?=...) 은 뒤에 특정 패턴이 있는지 확인하지만 해당 문자열 자체는 소비하지 않는다. 이를 이용하면 r".*[@].*[.](?=com$|net$).*$" 처럼 작성할 수 있다. 코드 import re pattern = re.compile( r".*[@].*[.](?=com$|net$).*$" ) print( pattern.match("test@gmail.com") ) print( pattern.match("test@daum.net") ) print( pattern.match("test@myhome.co.kr") ) 결과는 개념적으로 test@gmail.com → Match O test@daum.net → Match O test@myhome.co.kr → Match X 가 된다. 핵심 부분 (?=com$|net$) 을 해석하면 뒤에 com으로 끝나거나 또는 net으로 끝나야 한다. 라는 의미이다. (?=...) → 긍정형 전방 탐색 | → OR $ → 문자열 끝 📌 코딩 면허시험 핵심 정리 이번 문제들을 통해 지금까지 배운 파이썬 문법을 전체적으로 복습할 수 있다. 문제 핵심 개념 Q1 split() , join() Q2 딕셔너리 get() Q3 리스트 + , extend() Q4 for , if , 누적 Q5 함수, 피보나치 Q6 split() , map() , sum() Q7 range() , print(end=) Q8 파일 입출력, readlines() Q9 파일 입출력, 합계, 평균 Q10 클래스, 생성자, 메서드 Q11 모듈, sys.path Q12 try , except , finally Q13 enumerate() , 홀짝 판별 Q16 딕셔너리, 함수, 문자열 분리 Q17 정규식 [.] , {m,} Q18 search() , start() , end() Q19 그룹, sub() Q20 긍정형 전방 탐색 ⭐ 전체 학습 흐름 파이썬 기초 ↓ 자료형 ↓ 문자열 / 리스트 / 딕셔너리 ↓ if / while / for ↓ 함수 ↓ 파일 입출력 ↓ 클래스 ↓ 모듈 ↓ 예외 처리 ↓ 정규 표현식 ↓ 종합 문제 풀이 이번 문제에서 특히 다시 확인하면 좋은 부분은 split / join get extend map enumerate 파일 입출력 클래스 try-except-finally 정규 표현식 이다. 코딩 문제를 풀 때는 바로 정답 코드를 작성하려고 하기보다 입력값 → 필요한 처리 → 출력값 으로 문제를 먼저 나눈 뒤, 지금까지 배운 문법 중 무엇을 사용할지 생각하는 습관을 들이는 것이 중요하다.
오늘 아침에 제 자동화 루틴이 몇 개인지 세어봤습니다. 서른여섯 개였습니다. 지난주 화요일 글에서 "하루 열일곱 개가 다섯 채널에 글을 쓴다"고 적었는데, 일주일 만에 두 배가 넘게 늘었더군요. 저는 그동안 뭘 하고 있었길래 숫자가 이렇게 뛴 걸까요? 세어본 김에 실제 운영 기록도 같이 뽑아봤습니다. 지난 7일 동안 이 서른여섯 개 루틴이 실행된 횟수는 630번, 쓴 토큰은 57억 4천만 개, 그중 97.4%가 새로 읽은 게 아니라 캐시에서 다시 읽은 값이었습니다. 실패는 630번 중 3번, 비율로 치면 0.48%입니다. 숫자만 보면 거의 완벽한 시스템처럼 보입니다. 그런데 저는 왜 이 숫자를 보고 마음이 편해지지 않았을까요? 루틴이 두 배로 늘어난 이유 지난주에는 자동화가 다섯 채널(네이버 블로그, Threads, velog, 인스타그램, 유튜브)에 글을 쓰는 걸 지켜보는 게 제 역할의 전부였습니다. 이번 주에 늘어난 건 그 다섯 채널의 후속 작업이었습니다. 스마트스토어 입점을 마치고 위탁판매 상품을 찾는 루틴, 로또 번호를 사고 복기하는 루틴, 크몽 문의에 즉시 답하는 루틴, AI 검색엔진에 제 콘텐츠가 얼마나 인용되는지 추적하는 루틴 같은 것들이 새로 켜졌습니다. 실제 구성을 표로 정리하면 이렇습니다. 트리거 종류 개수 비고 시간 기반(schedule) 35개 매일, 매주 정해진 시각에 실행 이벤트 기반(event) 1개 특정 조건이 오면 깨어남 소속 프로젝트 개수 없음(독립 실행) 8개 유튜브 6개 인스타그램 4개 Threads 4개 크몽 4개 네이버 블로그 3개 공모전 2개 그 외(아이디어, 인류오류노트, 오래된 질문, E:LAB 대시보드) 4개 velog 1개 이 글을 쓰는 velog 루틴도 이 서른여섯 개 중 하나입니다. 제가 지금 쓰고 있는 이 문장도 사실 그 서른여섯 개 중 하나가 만들고 있는 셈이죠. 이상하지 않나요? 저는 분명 제 블로그에 글을 쓰고 있다고 생각했는데, 정작 손가락을 움직이는 건 제가 아니었습니다. 숫자가 늘어난 속도도 곱씹어볼 만합니다. 채널 다섯 개를 돌리던 열일곱 개에서, 채널의 "다음 단계"를 처리하는 열아홉 개가 더 붙었습니다. 늘어난 절반 가까이가 유튜브(6개)에 몰려 있는데, 이건 제가 채널을 하나 더 만들 때마다(실험실 채널, 테크 가십 채널, 인류오류노트, 오래된 질문 순서로) 발행과 회고, 댓글고정까지 세트로 켜기 때문입니다. 채널 하나를 늘린다는 게 실제로는 루틴 서너 개를 늘린다는 뜻이었다는 걸, 표로 정리하고 나서야 알았습니다. 모델을 두 등급으로 나눈 이유 서른여섯 개 중 스물한 개는 무거운 모델(Opus), 열네 개는 가벼운 모델(Sonnet)을 씁니다. 관찰과 댓글처럼 빈도는 높지만 판단이 단순한 루틴은 가벼운 모델로, 제작과 고객 응대처럼 결과물의 질이 직접 매출에 연결되는 루틴은 무거운 모델로 갈라뒀습니다. 이 배분을 처음 할 때는 "다 비싼 모델로 돌리면 안 되나"라는 생각도 했습니다. 그런데 단가를 계산해보니 무거운 모델이 가벼운 모델보다 입력 기준 2.5배 비쌌습니다. 하루 여러 번 도는 관찰과 댓글류 루틴까지 전부 비싼 모델로 돌리면, 한 달 안에 제한에 걸려서 정작 중요한 제작 루틴이 못 도는 역설이 생깁니다. 단가를 조금 더 구체적으로 적으면, 무거운 모델은 입력 기준 100만 토큰당 5달러, 가벼운 모델은 2달러입니다. 출력은 각각 25달러와 10달러, 캐시를 다시 읽는 비용은 0.5달러와 0.2달러입니다. 정확히 2.5배 차이인데, 지난 7일 소비의 97.4%가 캐시를 다시 읽는 항목이었으니 이 2.5배가 그대로 누적됩니다. 그래서 이번 주에 실제로 한 일 중 하나가 velog 루틴 자체를 비싼 모델에서 가벼운 모델로 옮긴 것이었습니다. 이 글도 그 가벼운 모델이 쓰고 있습니다. 아이러니하게도, 제 역할을 "루틴이 뭘 써야 할지 정하는 사람"으로 좁혀갈수록 정작 제가 직접 결정한 건 "무엇을 쓸지"가 아니라 "무엇으로 쓸지"였습니다. 실패율 0.48%가 저를 더 불안하게 만든 이유 숫자로만 보면 이건 아주 잘 돌아가는 시스템입니다. 630번 중 3번만 실패했으니까요. 그런데 저는 이 숫자를 처음 봤을 때 오히려 등골이 서늘했습니다. 왜였을까요? 지난주 토요일 글에 자동화 실패 4건을 적은 적이 있습니다. 전부 "성공했다"는 메시지를 받고도 실제로는 실패한 경우였습니다. 로그에는 정상 종료로 남아 있는데 실제 산출물을 열어보면 아무것도 없거나, 중복으로 두 번 올라가 있거나, 엉뚱한 값이 저장돼 있었습니다. 이번 주 3건의 실패도 마찬가지 유형이 섞여 있었을 겁니다. 630번 중 3번이라는 숫자는 정확하지만, 그 3번 중 몇 번이 "실패라고 표시된 실패"이고 몇 번이 "성공이라고 표시된 실패"인지는 이 숫자만으로는 알 수 없습니다. 이 구분을 하려면 로그의 마지막 줄이 아니라 실제 산출물을 봐야 합니다. 채널에 실제로 올라간 글의 목록, 대시보드에 실제로 찍힌 행, 실제로 발행된 영상 링크 같은 것들이요. 멈춘 줄 알았던 루틴이 사실은 끝까지 실행돼서 결과물까지 남겨놓은 경우도 있었고, 반대로 로그에는 완료라고 찍혔는데 결과물이 하나도 없는 경우도 있었습니다. 로그를 믿는 것과 결과물을 믿는 것 사이에는 생각보다 큰 간극이 있습니다. 저는 여기서 제 역할이 뭔지 다시 생각하게 됐습니다. 처음에는 제가 "글을 쓰는 사람"이라고 생각했습니다. 그다음에는 "통과시킬지 막을지 정하는 사람"이라고 생각을 바꿨습니다. 그런데 지금은 그것도 아닌 것 같습니다. 저는 "성공이라고 적힌 걸 의심하는 사람"이 된 것 같습니다. 권한을 세 단계로 나눈 이유 루틴이 서른여섯 개가 되면서 제가 실제로 손댄 건 개별 루틴의 프롬프트가 아니라 권한 구조였습니다. 각 루틴은 읽기 전용(read-only), 확인 필요(guard), 전체 권한(full-access) 세 단계 중 하나로 실행됩니다. 읽기 전용은 데이터를 조회만 하고 아무것도 바꾸지 못합니다. 확인 필요는 돈이 나가거나 계약이 걸리는 작업 직전에 저한테 먼저 물어봅니다. 전체 권한은 발행이나 댓글처럼 이미 정해진 규칙 안에서는 혼자 결정해서 실행합니다. 인스타그램 팔로우 루틴은 하루 최대 5명, 네이버 이웃 댓글 루틴은 하루 최대 25개처럼 상한을 걸어둔 것도 전체 권한이 폭주하지 않게 하는 장치입니다. 숫자 상한을 정하는 기준이 뭐냐고 물으신다면, 솔직히 처음엔 감이었습니다. 팔로우 5명, 댓글 25개는 "이 정도면 하루치 활동처럼 보이겠다"는 감각으로 잡은 값입니다. 그런데 이 감각도 한 번 부러진 적이 있습니다. 인스타그램 팔로우 루틴이 상한을 넘겨서 실행된 날, 계정이 일시적으로 제한 경고를 받았습니다. 그날 이후로 상한은 "넉넉하게 잡은 목표치"가 아니라 "넘으면 진짜 문제가 생기는 선"으로 다시 정의했습니다. 이 구조를 만들면서 든 생각은, 제가 하는 일이 "자동화를 만드는 일"이 아니라 "자동화가 넘지 말아야 할 선을 긋는 일"에 가깝다는 것이었습니다. 코드 한 줄을 더 짜는 것보다 "이건 물어보고 가", "이건 상한 5", "이건 그냥 해"를 정하는 데 더 많은 시간을 씁니다. 이게 개발일까요, 아니면 다른 이름이 필요한 일일까요? 캐시 97.4%가 말해주는 것 토큰 57억 4천만 개 중 97.4%가 캐시에서 다시 읽은 값이라는 것도 곱씹어볼 만한 숫자입니다. 이 말은 루틴들이 매번 새로운 걸 생각해내는 게 아니라, 같은 맥락(이전 대화, 메모리 파일, 규칙 문서)을 계속 다시 불러다 쓰고 있다는 뜻입니다. 새로 만들어내는 부분은 2.6%뿐입니다. 이 숫자를 보고 처음 든 생각은 "낭비가 심하다"였습니다. 같은 걸 왜 이렇게 자주 다시 읽을까 싶었죠. 그런데 다시 생각해보면 이게 오히려 제가 원했던 그림이기도 합니다. 매번 처음부터 판단하는 루틴보다, 지난 실수와 규칙을 기억하고 그 위에서 판단하는 루틴이 더 낫습니다. 가운뎃점을 쓰지 말라는 규칙 하나를 서른네 개 루틴 프롬프트 맨 앞에 박아넣은 것도, DM을 유도하는 문구를 쓰지 말라는 규칙도, 전부 "한 번 정한 규칙을 계속 다시 읽게" 만든 결과입니다. 낭비처럼 보이는 97.4%가 사실은 제가 계속 지키고 싶은 규칙들의 무게였습니다. 규칙을 규칙 문서 한 곳에 몰아두지 않고 여러 층으로 나눈 것도 같은 이유입니다. 계정 전체에 적용되는 규칙, 채널마다 다른 규칙, 루틴 하나에만 적용되는 규칙을 구분해두지 않으면, 규칙을 고칠 때마다 서른여섯 개 프롬프트를 전부 열어봐야 합니다. 층을 나눠두면 계정 전체 규칙 하나를 고치는 것만으로 서른여섯 개가 동시에 업데이트됩니다. 이게 제가 이번 주에 한 일 중 "글쓰기"라고 부를 수 없는 또 하나의 작업이었습니다. 실제로 있었던 일: 로그는 멀쩡한데 결과물이 없던 날 말로만 하면 추상적이니 실제 사례 하나를 적습니다. 이번 주 초, 유튜브 채널 하나의 롱폼 발행 루틴이 "정상 종료"로 기록됐습니다. 세션 로그 마지막 줄도 평범했습니다. 그런데 실제 채널에 들어가보니 새 영상이 없었습니다. 원인을 찾으려고 로그가 아니라 세션 기록 테이블을 직접 열어봤습니다. 실행 시간과 턴 수를 보니, 렌더링 단계에서 멈춘 뒤 그 상태로 "완료"라고 잘못 표시된 경우였습니다. 메시지가 수십 개 쌓이고 토큰도 정상적으로 소비된 걸 보면 작업은 분명히 진행됐는데, 마지막에 결과를 업로드하는 단계에서 조용히 실패한 것이었습니다. 이런 실패는 로그 줄 수만 봐서는 절대 안 걸립니다. 메시지 4개짜리 빈 실행(시작하자마자 죽은 경우)과 메시지 수십 개짜리 실행(끝까지 갔다가 마지막에 죽은 경우)은 원인도 다르고 고치는 방법도 다릅니다. 이 사례를 겪고 나서 바꾼 게 하나 있습니다. 이제는 "루틴이 끝났다"는 말을 믿지 않고, 채널에 실제로 뭐가 올라갔는지를 먼저 확인합니다. 저한테는 이게 이번 주에 배운 가장 중요한 교훈이었습니다. 자동화를 늘리는 것보다, 자동화가 거짓으로 "성공"이라고 말하지 않는지 확인하는 절차를 만드는 게 더 오래 걸렸습니다. 다른 개발자들은 이 질문에 뭐라고 답했을까 이번 주 트렌딩 글 중에 비슷한 질문을 던진 글이 몇 개 있었습니다. SI 회사에서 4년을 일하다 두 번째 이직을 고민한 분의 글, 개발자 1년차가 "최강이 되고 싶었다"고 적은 글, "Jev가 뭐냐"는 제목으로 새로운 직무 이름 자체를 다시 묻는 글까지, 전부 "나는 지금 뭘 하는 사람인가"를 스스로에게 묻고 있었습니다. 공통점은 답을 명확하게 내린 글이 별로 없다는 것이었습니다. 대신 질문을 더 구체적으로 다듬어가는 과정 자체가 글이 됐습니다. 저도 이번 글을 쓰면서 비슷한 걸 느꼈습니다. "저는 개발자입니다"라고 딱 잘라 말하고 싶었는데, 서른여섯 개 루틴 중 제가 직접 코드를 짠 건 몇 개 안 됩니다. 대신 권한 구조를 설계하고, 상한을 정하고, 실패를 의심하는 일에 시간을 씁니다. 이게 새로운 직무라면 이름이 있어야 할 텐데, 아직 그 이름을 못 찾았습니다. 그래서 저는 뭘 하는 사람일까요 이 질문에 아직 답을 못 찾았습니다. 개발자라고 하기엔 제가 짜는 코드가 없는 날이 더 많습니다. 운영자라고 하기엔 실제 운영 판단(뭘 팔지, 뭘 그만둘지)은 여전히 제가 직접 합니다. 관리자라고 하면 제일 가깝긴 한데, 관리하는 대상이 사람이 아니라 서른여섯 개의 자동화라는 게 좀 낯섭니다. 확실한 건 하나입니다. 루틴이 열일곱 개였을 때는 각각을 다 기억할 수 있었는데, 서른여섯 개가 되니 표로 정리하지 않으면 전체 그림이 안 보입니다. 다음에 이 숫자를 다시 셀 때는 더 늘어 있을 것 같습니다. 그때는 또 어떤 이름으로 제 역할을 불러야 할지, 지금은 잘 모르겠습니다. 제 결론은 일단 이렇습니다. 저는 글을 쓰는 사람도 아니고, 코드를 짜는 사람도 아니고, 결정을 대신 내려주는 사람도 아닙니다. 서른여섯 개의 판단 기준을 정해두고, 그 기준이 실제로 지켜지고 있는지 매주 확인하는 사람에 가깝습니다. 이게 개발자의 새로운 형태인지, 아니면 완전히 다른 직무인지는 아직 판단이 안 섭니다. 혹시 여러분은 이런 걸 뭐라고 부르시나요? 저와 비슷한 구조를 돌리고 계신 분이 있다면, 권한을 어떻게 나누고 계신지, 실패를 어떻게 걸러내고 계신지 댓글로 들어보고 싶습니다. 긴 글 읽어주셔서 감사합니다.
09-2 AI와 함께 파이썬 공부하기 목차 1. AI를 파이썬 공부에 활용하기 2. AI가 도움이 되는 순간 3. 질문을 잘하는 방법 4. 오류 원인 찾기 5. 코드 수정하기 6. 코드 설명 받기 7. 연습문제 만들기 8. 코드 스타일 개선하기 9. AI 답변 검증하기 10. AI를 이용한 공부 방법 정리 1. AI를 파이썬 공부에 활용하기 파이썬을 공부하다 보면 책이나 강의를 보면서도 이해되지 않는 부분이 생긴다. 예를 들어 왜 오류가 발생하지? 왜 결과가 내가 생각한 것과 다르지? 이 코드는 왜 이렇게 작성하지? 이 개념을 더 쉽게 설명할 수 없을까? 와 같은 궁금증이 생긴다. 과거에는 이런 문제가 생기면 검색 ↓ 여러 글 확인 ↓ 내 상황과 비슷한 내용 찾기 ↓ 직접 해결 하는 과정이 필요했다. 하지만 AI를 활용하면 현재 작성하고 있는 코드와 오류 내용을 그대로 보여 주면서 질문할 수 있다. 즉 AI를 모르는 내용을 바로 질문할 수 있는 개인 학습 도우미 처럼 활용할 수 있다. 2. AI가 특히 도움이 되는 순간 파이썬을 처음 공부할 때 자주 막히는 상황은 크게 세 가지이다. 1. 오류 메시지를 이해하지 못할 때 IndexError TypeError KeyError NameError 등의 오류가 발생했지만 원인을 모르겠을 때이다. 2. 코드가 원하는 대로 동작하지 않을 때 코드는 실행되지만 내가 기대한 결과 ≠ 실제 결과 인 경우이다. 3. 개념을 이해하기 어려울 때 예를 들어 클래스 클로저 데코레이터 이터레이터 제너레이터 정규 표현식 같은 개념이 책의 설명만으로 잘 이해되지 않을 수 있다. 이때 AI에게 더 쉽게 설명해 줘 예제를 추가해서 설명해 줘 한 줄씩 설명해 줘 처럼 요청할 수 있다. 3. 질문을 잘하는 방법 AI에게 질문할 때 단순히 이거 왜 안 돼? 라고 물어보는 것보다 필요한 정보를 함께 제공하는 것이 좋다. 특히 다음 네 가지를 알려 주면 된다. 1. 내가 하고 싶은 것 2. 내가 작성한 코드 3. 실제 실행 결과 또는 오류 메시지 4. 내가 기대한 결과 질문 구조 예를 들어 다음과 같은 형태이다. 목표 ↓ 리스트의 모든 값을 더하고 싶습니다. 코드 ↓ numbers = [1, 2, 3] ... 실제 결과 ↓ IndexError가 발생합니다. 원하는 결과 ↓ 6이 출력되었으면 좋겠습니다. 이렇게 질문하면 AI도 무엇을 만들고 있는지 어디에서 문제가 발생했는지 어떤 결과를 원하는지 를 정확하게 파악할 수 있다. 4. 오류 원인 찾기 다음 코드를 실행했다고 해보자. numbers = [1, 2, 3] print(numbers[3]) 오류가 발생한다. IndexError: list index out of range 이럴 때 AI에게 코드와 오류를 함께 전달한다. 왜 오류가 발생할까? 리스트가 numbers = [1, 2, 3] 이라면 인덱스는 다음과 같다. 값 1 2 3 인덱스 0 1 2 따라서 numbers[3] 은 존재하지 않는 네 번째 위치를 요청하는 것이다. 그래서 IndexError 가 발생한다. 정상적인 마지막 값은 numbers[2] 이다. 오류를 질문할 때 중요한 점 오류 메시지가 길더라도 가능하면 중간 부분을 임의로 제거하지 않는 것이 좋다. 예를 들어 Traceback ... 파일 위치 오류가 발생한 줄 오류 종류 오류 메시지 에는 문제를 찾는 데 필요한 정보가 포함되어 있다. 따라서 코드 + 전체 오류 메시지 를 같이 전달하는 것이 좋다. 5. 코드 수정하기 다음 코드를 보자. numbers = [1, 2, 3] total = 0 for i in range(4): total += numbers[i] print(total) 목표는 1 + 2 + 3 = 6 을 출력하는 것이다. 하지만 오류가 발생한다. 문제 원인 range(4) 는 0 1 2 3 을 생성한다. 따라서 반복 과정은 numbers[0] → 1 numbers[1] → 2 numbers[2] → 3 numbers[3] → 오류 가 된다. numbers[3] 은 존재하지 않는다. 수정 방법 1 numbers = [1, 2, 3] total = 0 for i in range(3): total += numbers[i] print(total) 결과 6 수정 방법 2 리스트의 길이를 직접 사용하면 더 좋다. numbers = [1, 2, 3] total = 0 for i in range(len(numbers)): total += numbers[i] print(total) 이렇게 하면 리스트 길이가 바뀌어도 사용할 수 있다. 수정 방법 3 인덱스가 필요 없다면 값을 직접 반복할 수 있다. numbers = [1, 2, 3] total = 0 for number in numbers: total += number print(total) 결과 6 이 방법이 가장 읽기 쉽다. 6. AI에게 코드를 수정해 달라고 할 때 단순히 이 코드 고쳐 줘. 라고 요청하는 것보다 현재 결과 + 원하는 결과 를 알려 주는 것이 중요하다. 예를 들어 현재는 IndexError가 발생합니다. 리스트의 숫자를 모두 더해서 6이 출력되도록 수정해 주세요. 수정한 이유도 설명해 주세요. 처럼 질문할 수 있다. 그러면 단순히 정답 코드만 얻는 것이 아니라 왜 문제가 발생했는지도 함께 공부할 수 있다. 7. 코드 설명 받기 처음 보는 코드를 만났을 때 AI에게 한 줄씩 설명해 달라고 요청할 수도 있다. 예를 들어 for i in range(1, 6): if i % 2 == 0: print(i, "는 짝수입니다.") else: print(i, "는 홀수입니다.") 한 줄씩 살펴보기 range() range(1, 6) 은 1, 2, 3, 4, 5 를 만든다. for for i in range(1, 6): i 에 숫자가 하나씩 들어간다. i = 1 i = 2 i = 3 i = 4 i = 5 나머지 연산자 i % 2 숫자를 2로 나눈 나머지를 구한다. 짝수라면 나머지 = 0 이다. 따라서 if i % 2 == 0: 은 i가 짝수인가? 를 검사하는 코드이다. 전체 흐름 1부터 5까지 반복 ↓ 현재 숫자를 2로 나눔 ↓ 나머지가 0인가? ↙ ↘ Yes No ↓ ↓ 짝수 출력 홀수 출력 8. 이해 수준을 지정해서 질문하기 AI의 설명이 너무 어렵다면 질문할 때 이해 수준을 지정할 수 있다. 예를 들어 파이썬을 처음 배우는 사람 기준으로 설명해 줘. 중학생도 이해할 수 있게 설명해 줘. 코드를 한 줄씩 실행 순서대로 설명해 줘. 변수 값이 어떻게 바뀌는지 표로 보여 줘. 처럼 요청할 수 있다. 특히 코드를 공부할 때는 실행 순서 + 변수 변화 를 같이 보는 것이 이해에 도움이 된다. 9. 연습문제 만들기 AI는 현재 배운 범위에 맞는 문제를 만드는 데도 활용할 수 있다. 예를 들어 현재 if문 for문 함수 까지만 공부했다고 해보자. 이때 if문, for문, 함수까지만 사용해서 풀 수 있는 연습문제를 만들어 줘. 라고 요청할 수 있다. 난이도도 지정 가능 초급 문제 3개 만들어 줘. 중간 난이도로 만들어 줘. 한 문제씩 내 줘. 내가 답을 말하기 전까지 정답은 알려 주지 마. 틀렸을 때는 힌트만 줘. 처럼 공부 방식까지 지정할 수 있다. 10. 문제 풀이에 AI를 사용하는 좋은 방법 AI가 문제를 만들자마자 정답까지 보여 주면 직접 생각할 시간이 줄어든다. 따라서 다음과 같은 방식이 좋다. 문제 출제 ↓ 직접 풀이 ↓ 코드 실행 ↓ 결과 확인 ↓ 모르겠으면 힌트 요청 ↓ 다시 풀이 ↓ 마지막에 정답과 비교 AI를 답을 바로 알려 주는 도구 가 아니라 문제 출제자 + 힌트를 주는 선생님 처럼 사용할 수 있다. 11. 코드 스타일 개선하기 프로그램이 정상적으로 동작하더라도 코드가 읽기 어려울 수 있다. 예를 들어 a = [1, 2, 3, 4, 5] b = 0 for c in a: b += c print(b) 결과 자체는 정상이다. 15 하지만 변수 이름만 보면 각각 무엇을 의미하는지 알기 어렵다. 읽기 쉽게 수정 numbers = [1, 2, 3, 4, 5] total = 0 for number in numbers: total += number print(total) 기능은 동일하지만 훨씬 쉽게 읽을 수 있다. a → numbers b → total c → number 처럼 변수의 역할을 알 수 있기 때문이다. 12. 리팩터링 기존 프로그램의 기능은 유지하면서 코드 구조를 개선하는 것을 리팩터링(Refactoring) 이라고 한다. AI에게 다음과 같이 요청할 수 있다. 기능은 그대로 유지해 주세요. 코드를 더 읽기 쉽게 수정해 주세요. 변수 이름도 역할을 알 수 있게 바꿔 주세요. 수정 전후의 차이도 설명해 주세요. 단순히 짧은 코드가 항상 좋은 코드는 아니다. 중요한 것은 읽기 쉬운가? 의도를 알 수 있는가? 수정하기 쉬운가? 이다. 13. AI의 답변을 그대로 믿으면 안 되는 이유 AI는 매우 유용하지만 항상 정확한 답변만 하는 것은 아니다. 예를 들어 존재하지 않는 함수 잘못된 문법 논리적으로 틀린 코드 현재 버전에서는 동작하지 않는 코드 를 제시할 수도 있다. 따라서 AI가 코드를 만들어 줬다면 반드시 직접 실행 해야 한다. 14. 직접 실행하기 AI가 다음과 같은 코드를 알려 주었다고 해도 numbers = [1, 2, 3] print(sum(numbers)) 눈으로만 보고 맞겠지 라고 생각하는 것이 아니라 실제로 실행한다. 실행 결과 6 처럼 확인한다. 15. 입력값을 바꿔서 테스트하기 한 번 정상 동작했다고 해서 모든 상황에서 정상이라고 볼 수는 없다. 예를 들어 def divide(a, b): return a / b 다음 코드는 정상이다. divide(10, 2) 5.0 하지만 divide(10, 0) 을 실행하면 ZeroDivisionError 가 발생한다. 따라서 여러 입력값을 사용해 보는 것이 중요하다. 정상적인 값 경계값 예외적인 값 등을 테스트해 본다. 16. 테스트하는 습관 예를 들어 양수를 판별하는 함수를 만들었다고 해보자. def is_positive(number): return number > 0 한 가지 값만 확인하지 않는다. print(is_positive(10)) print(is_positive(-10)) print(is_positive(0)) 결과 True False False 처럼 여러 상황에서 확인한다. 17. 이해가 안 되면 다시 질문하기 AI가 설명했지만 여전히 이해되지 않는다면 그대로 넘어가지 않는다. 예를 들어 왜 이렇게 되는지 모르겠어. 더 쉽게 설명해 줘. 변수 값이 어떻게 변하는지 단계별로 보여 줘. 작은 숫자로 예시를 들어 줘. 그림처럼 흐름으로 보여 줘. 처럼 다시 질문하면 된다. 같은 개념도 설명 방법을 바꾸면 훨씬 쉽게 이해될 수 있다. 18. 초보자에게 좋은 질문 방식 처음부터 너무 많은 내용을 한 번에 질문하면 답변도 복잡해질 수 있다. 따라서 하나씩 질문하는 것이 좋다. 예를 들어 파이썬 함수, 클래스, 상속, 오버라이딩, 클로저, 데코레이터를 전부 설명해 줘. 보다는 클래스가 무엇인지 먼저 설명해 줘. 그리고 이해한 뒤 이번에는 상속을 설명해 줘. 처럼 진행한다. 19. 코드만 받지 않기 AI에게 코드를 요청하면 정답 코드를 빠르게 얻을 수 있다. 하지만 코드 복사 ↓ 실행 ↓ 끝 으로 공부하면 실력이 잘 늘지 않는다. 더 좋은 방법은 코드 확인 ↓ 왜 이렇게 작성했는지 이해 ↓ 직접 다시 작성 ↓ 값을 변경해서 테스트 하는 것이다. 20. 좋은 질문과 좋지 않은 질문 비교 좋지 않은 질문 이거 왜 안 돼? 문제 상황을 알기 어렵다. 더 좋은 질문 리스트 [1, 2, 3]의 합계를 구하려고 합니다. 현재 코드는 다음과 같습니다. numbers = [1, 2, 3] total = 0 for i in range(4): total += numbers[i] print(total) IndexError가 발생합니다. 원하는 결과는 6입니다. 왜 오류가 발생하는지 초보자 기준으로 설명해 주세요. AI가 문제 상황을 훨씬 정확하게 이해할 수 있다. 21. 질문 템플릿 파이썬 공부 중 오류가 발생했다면 다음 형식을 사용할 수 있다. [목표] 제가 만들고 싶은 것은 ______ 입니다. [현재 코드] 코드는 다음과 같습니다. ```python 코드 작성 [현재 결과] 실행하면 __ 결과가 나오거나 __ 오류가 발생합니다. [원하는 결과] 제가 원하는 결과는 __ 입니다. [질문] 왜 이런 문제가 발생하는지 파이썬 초보자 기준으로 설명해 주세요. 수정 코드뿐 아니라 수정하는 이유도 같이 설명해 주세요. --- # 22. 코드 공부용 질문 템플릿 코드를 이해하고 싶을 때는 다음처럼 사용할 수 있다. ```text 다음 파이썬 코드를 공부하고 있습니다. ```python 코드 작성 다음 순서로 설명해 주세요. 전체 코드의 목적 한 줄씩 설명 실행 순서 변수 값이 어떻게 변하는지 핵심 문법 초보자가 헷갈릴 만한 부분 23. 연습문제용 질문 템플릿 현재 파이썬에서 다음 내용을 공부했습니다. - 리스트 - if문 - for문 - 함수 이 범위 안에서만 풀 수 있는 연습문제 3개를 만들어 주세요. 조건 - 쉬운 문제부터 순서대로 - 정답은 바로 알려 주지 않기 - 한 문제씩 출제 - 틀리면 힌트만 제공 - 마지막에 전체 풀이 설명 이런 방식으로 AI를 개인 문제 출제 도구처럼 사용할 수 있다. 24. AI를 이용한 추천 학습 흐름 파이썬을 공부할 때 다음과 같은 흐름으로 활용할 수 있다. 책 / 강의 공부 ↓ 직접 코드 작성 ↓ 직접 실행 ↓ 이해됨? ↙ ↘ Yes No ↓ ↓ 다음 내용 AI에게 질문 ↓ 설명 확인 ↓ 코드 수정 ↓ 다시 직접 실행 ↓ 값 변경 테스트 ↓ 직접 다시 작성 핵심은 항상 AI 답변 ↓ 직접 확인 과정을 거치는 것이다. 25. AI에게 질문할 때 기억할 것 좋은 질문에는 보통 다음 정보가 포함된다. 무엇을 하려고 하는가? 현재 코드는 무엇인가? 현재 어떤 일이 발생하는가? 어떤 결과를 원하는가? 그리고 답변을 받은 뒤에는 직접 실행 다른 값으로 테스트 이해되지 않는 부분 재질문 과정을 거친다. 📌 핵심 정리 AI가 유용한 상황 오류 원인을 모를 때 코드가 원하는 대로 동작하지 않을 때 개념이 이해되지 않을 때 코드 설명이 필요할 때 연습문제가 필요할 때 코드를 더 읽기 쉽게 만들고 싶을 때 좋은 질문의 4가지 요소 1. 목표 2. 현재 코드 3. 실제 결과 / 오류 메시지 4. 기대하는 결과 오류 질문 코드 + 전체 오류 메시지 + 원하는 동작 을 함께 전달한다. 코드 설명 한 줄씩 설명해 줘 실행 순서대로 설명해 줘 변수 값이 어떻게 변하는지 보여 줘 처럼 구체적으로 요청한다. 연습문제 현재 배운 범위를 알려 주고 정답은 바로 말하지 않기 힌트만 제공 한 문제씩 진행 처럼 학습 방식까지 지정할 수 있다. 코드 개선 AI에게 단순히 코드를 짧게 만들어 달라고 하기보다 가독성 변수 이름 중복 코드 코드 구조 를 개선해 달라고 요청한다. AI 답변 검증 AI가 알려 준 답변은 반드시 직접 실행 ↓ 입력값 변경 ↓ 2~3번 추가 테스트 ↓ 이상하면 다시 확인 하는 과정이 필요하다. ⭐ 09-2 전체 흐름 파이썬 공부 ↓ 직접 생각하고 코드 작성 ↓ 문제 발생 ↓ AI에게 질문 ↓ ┌─────────────────┐ │ 목표 │ │ 코드 │ │ 실제 결과 │ │ 원하는 결과 │ └─────────────────┘ ↓ AI의 설명 확인 ↓ 직접 코드 수정 ↓ 직접 실행 ↓ 다른 값으로 테스트 ↓ 왜 동작하는지 이해 ↓ 직접 다시 작성 AI는 정답을 대신 작성해 주는 도구로 사용하기보다, 막힌 부분을 설명받고 스스로 이해할 수 있도록 돕는 학습 도구로 사용하는 것이 가장 좋다. 결국 중요한 것은 질문 → 답변 복사 → 끝 이 아니라 질문 → 원리 이해 → 직접 실행 → 테스트 → 직접 다시 작성 의 과정을 반복하는 것이다.
대표 문제 클라이언트가 POST 요청을 보낸 순간부터 Spring Controller가 실행되기까지 실제로 무슨 일이 일어나는가? 스터디 전 최초 답변 클라이언트 POST 요청 -> DNS로 서버 IP 확인 -> TCP 커넥션 생성 -> 서버로 전송 -> Filter -> DispatcherServlet -> Controller 실행 진지하게 대학을 다시 가야 할 것 같고, 다시 정리한 전체 흐름은 아래와 같다. POST https://api.example.com/orders ↓ DNS 도메인 → IP 주소 확인 ↓ TCP 서버와 Connection 생성 ↓ TLS 서버 인증 + 암호화 통신 준비 ↓ HTTP Method / Path / Header / Body 전송 ↓ Web Server / Servlet Container ↓ Filter Chain ↓ DispatcherServlet ↓ HandlerMapping / HandlerAdapter ↓ Controller 1. 사용자가 POST 요청을 보냈다 예를 들어 프론트엔드에서 다음 API를 호출한다고 해보자. POST https://api.example.com/orders Content-Type: application/json { "itemId": 10, "quantity": 2 } 보기에는 단순히 POST /orders 를 호출한 것처럼 보인다. 하지만 클라이언트가 알고 있는 것은 api.example.com 이라는 도메인 이름 이다. 네트워크에서 실제 목적지를 찾아가기 위해서는 먼저 서버의 IP 주소를 알아내야 한다. 여기서 DNS가 등장한다. 2. DNS - 도메인을 IP 주소로 바꾼다 DNS는 Domain Name System의 약자다. 사람은 api.example.com 같은 이름을 사용하기 편하지만 네트워크에서는 실제 목적지를 찾기 위해 IP 주소가 필요하다. 따라서 개념적으로 다음 변환이 필요하다. api.example.com ↓ DNS 203.0.113.10 DNS의 핵심 역할은 도메인 이름을 해당 서비스의 IP 주소 등의 DNS Record로 해석하는 것 이다. 2-1. 요청할 때마다 DNS 서버까지 찾아갈까? 처음에는 Client ↓ DNS Server ↓ IP 반환 정도로 생각하기 쉽다. 하지만 매 요청마다 DNS 전체 조회를 수행한다면 비용이 너무 커지기 때문에 여러 단계에서 Cache를 사용한다. 애플리케이션 / 브라우저 Cache ↓ miss OS DNS Cache ↓ miss Recursive DNS Resolver ↓ 필요하다면 Root DNS ↓ TLD DNS ↓ Authoritative DNS 단, 정확한 Cache 계층과 순서는 OS, Browser, Runtime, Network 환경마다 달라질 수 있다. 2-2. Recursive DNS Resolver 일반 사용자가 Root DNS부터 직접 하나씩 질의하는 것은 아니다. 보통 ISP나 회사, Cloudflare, Google 같은 Recursive Resolver 에게 api.example.com의 IP가 뭐야? 라고 요청한다. Resolver가 이미 Cache하고 있다면 바로 응답한다. 없다면 필요한 DNS 서버들을 조회한다. 2-3. Root → TLD → Authoritative Cache가 전혀 없다고 단순화하면 다음과 같은 흐름이 될 수 있다. Client ↓ Recursive Resolver ↓ Root DNS "example.com은 어디서 알아?" ↓ .com TLD DNS 정보 ↓ TLD DNS "example.com은 어디서 관리하지?" ↓ Authoritative DNS 정보 ↓ Authoritative DNS "api.example.com 주소는?" ↓ IP Address Root DNS가 최종 IP를 모두 가지고 있는 것이 아니다. 각 단계가 다음에 어디를 확인해야 하는지 알려주는 구조에 가깝다. 2-4. TTL DNS Record에는 TTL(Time To Live)이 설정될 수 있다. 예를 들어 TTL = 300 seconds 라면 Resolver 등이 해당 결과를 일정 시간 Cache할 수 있다. 그래서 서버 IP가 변경되더라도 DNS 변경 사항이 모든 사용자에게 즉시 반영된다고 보장할 수는 없다. 핵심 질문 1 POST 요청을 보낼 때마다 DNS Lookup이 발생하는가? 반드시 그렇지는 않다. Browser, OS, DNS Resolver 등의 Cache에 결과가 남아 있다면 Cache된 결과를 사용할 수 있다. 즉 HTTP 요청 1회 = DNS 전체 조회 1회 가 아니다. 3. IP를 알았다면 서버와 연결해야 한다 DNS를 통해 다음 IP를 알아냈다고 가정하자. api.example.com ↓ 203.0.113.10 HTTPS 기본 Port를 사용한다면 목적지는 대략 203.0.113.10:443 이다. 이제 실제 통신을 위한 Connection이 필요하다. 전통적인 HTTP/1.1과 HTTP/2의 HTTPS 통신에서는 일반적으로 TCP 위에서 통신한다. 3-1. TCP 3-Way Handshake TCP Connection을 만들기 위해 대표적으로 3-Way Handshake를 수행한다. Client Server ───────── SYN ────────────→ ←────── SYN + ACK ───────── ───────── ACK ────────────→ 이를 통해 양쪽이 통신 가능한 상태인지 확인하고 Connection 상태를 만든다. 3-2. Connection은 무엇으로 구분할까? TCP Connection은 일반적으로 다음 정보의 조합으로 식별할 수 있다. Source IP Source Port Destination IP Destination Port 예를 들어 192.168.0.10:51000 ↓ 203.0.113.10:443 와 같은 연결이 만들어질 수 있다. Client의 Source Port는 일반적으로 OS가 사용 가능한 Ephemeral Port 중 하나를 선택한다. 3-3. TCP가 제공하는 것 TCP는 애플리케이션에게 신뢰성 있는 순서 보장 Byte Stream 을 제공한다. 대표적으로 다음 기능들이 있다. 순서 보장 손실 감지 및 재전송 중복 처리 Flow Control Congestion Control 예를 들어 데이터 일부가 네트워크에서 손실되더라도 TCP가 재전송할 수 있다. 3-4. 그렇다면 TCP가 있으면 요청 처리가 보장될까? 아니다. TCP가 보장하는 것은 Byte Stream이 상대 TCP Endpoint까지 신뢰성 있게 전달되는 것 에 가깝다. TCP가 주문 DB 저장 성공 결제 성공 Controller 실행 성공 Transaction Commit 성공 같은 비즈니스 처리를 보장하지는 않는다. 즉 TCP 전송 성공 ≠ Business 처리 성공 이다. 핵심 질문 2 TCP 연결에 성공했다는 것은 서버가 POST 요청을 성공적으로 처리했다는 의미인가? 아니다. TCP Connection이 존재한다는 것은 네트워크 통신 경로가 마련됐다는 의미이지, 애플리케이션 비즈니스 로직의 성공을 의미하지 않는다. HTTP 응답을 받아야 하고, 그 HTTP 응답 역시 애플리케이션의 처리 결과를 표현하는 별도의 계층이다. 4. HTTPS라면 TLS가 필요하다 우리는 실제 서비스에서 보통 http:// 보다 https:// 를 사용한다. HTTPS는 개념적으로 HTTP ↓ TLS ↓ TCP 구조로 이해할 수 있다. TLS는 통신 과정에서 주로 다음을 제공한다. Confidentiality → 내용을 암호화 Integrity → 전송 중 변조 여부 확인 Authentication → 인증서를 통해 서버의 신원을 검증 4-1. TLS Handshake에서는 무슨 일이 일어날까? TLS 1.3 기준으로 크게 단순화하면 다음과 같이 볼 수 있다. Client │ │ ClientHello │ 지원 TLS Version │ Cipher Suites │ Key Share 등 ↓ Server │ │ ServerHello │ 사용할 알고리즘 선택 │ Key Share │ Certificate │ Certificate Verify 등 ↓ Client 서로 Key Material 계산 ↓ 암호화된 Application Data 통신 실제 과정은 더 복잡하지만 핵심은 다음 세 가지다. 1. 어떤 암호화 방식을 사용할지 협상한다. 2. 서버의 인증서를 검증한다. 3. 이후 사용할 Session Key를 안전하게 만들어낸다. 4-2. 서버가 비밀키를 클라이언트에게 보내는 걸까? 아니다. 서버의 Private Key를 클라이언트에게 보내면 보안이 완전히 무너진다. TLS에서는 공개키 암호와 Key Exchange를 이용해 양쪽이 공통된 비밀 값을 안전하게 만들어낸다. 실제 Application Data는 성능상 효율적인 대칭키 암호화 를 사용한다. 4-3. 인증서는 왜 필요한가? 암호화만 한다고 안전한 것은 아니다. 공격자가 중간에서 "내가 api.example.com이야" 라고 속이고 자신과 TLS Connection을 만들게 한다면 문제가 된다. 그래서 인증서를 통해 이 서버가 정말 api.example.com의 서버인지 를 검증한다. Client는 보통 다음과 같은 것들을 확인한다. Certificate가 신뢰 가능한 CA Chain으로 이어지는가? Hostname이 일치하는가? 유효 기간이 정상인가? 4-4. TCP 다음에는 무조건 TLS일까? HTTP가 HTTPS라면 HTTP/1.1과 HTTP/2에서는 대체로 TCP ↓ TLS ↓ HTTP 로 이해할 수 있다. 하지만 현대 HTTP에는 예외가 있다. HTTP/3 HTTP/3는 TCP를 사용하지 않고 QUIC 을 사용한다. QUIC은 UDP를 기반으로 하면서 TLS 1.3 기능을 통합한다. 따라서 HTTP/1.1 / HTTP/2 HTTP ↓ TLS ↓ TCP 와 HTTP/3 HTTP/3 ↓ QUIC + TLS ↓ UDP 는 구분할 필요가 있다. 이번 학습에서는 Spring 서버에서 흔하게 접하는 HTTP/1.1 / HTTP/2의 TCP 기반 흐름을 중심으로 본다. 핵심 질문 3 HTTPS는 HTTP와 완전히 다른 프로토콜인가? HTTP의 의미 자체가 완전히 달라지는 것이 아니다. HTTP Message를 TLS로 보호해서 전송하는 구조라고 이해하면 된다. HTTP Request ↓ TLS 암호화 ↓ Network 따라서 HTTPS에서도 GET, POST, Header, Status Code 같은 HTTP 의미는 그대로 존재한다. 5. 이제 HTTP Request를 보낸다 연결과 암호화 준비가 끝나면 실제 HTTP Request를 전달한다. HTTP/1.1의 형태를 단순화하면 다음과 같다. POST /orders HTTP/1.1 Host: api.example.com Content-Type: application/json Authorization: Bearer xxx Accept: application/json Content-Length: ... { "itemId": 10, "quantity": 2 } HTTP Request는 크게 Request Line Headers Body 로 볼 수 있다. 5-1. Method 여기서는 POST 이다. HTTP Method는 Request가 가진 의미를 나타낸다. 대표적으로 GET POST PUT PATCH DELETE 등이 있다. 다만 POST = 무조건 생성 PUT = 무조건 수정 처럼 HTTP Method를 CRUD와 완전히 동일시하면 안 된다. HTTP가 정의하는 각 Method의 Semantics 가 있고 REST API를 설계할 때 이를 Resource 처리와 연결해서 사용하는 것이다. 5-2. Request Target /orders 는 서버에서 어떤 Resource나 Endpoint를 대상으로 요청하는지를 표현한다. Spring에서는 이후 @PostMapping("/orders") 같은 Mapping과 연결될 수 있다. 5-3. Header HTTP Header에는 Request에 대한 Metadata가 들어간다. 예를 들어 Content-Type Authorization Accept Cookie Host User-Agent 등이다. 5-4. Content-Type과 Accept는 다르다 자주 혼동하는 부분이다. Content-Type: application/json 은 내가 지금 보내는 Body가 JSON이다. 라는 의미다. 반면 Accept: application/json 은 나는 JSON 형태의 Response를 받을 수 있다 / 원한다. 라는 의미다. 5-5. Body POST에서는 Request Body에 데이터를 담는 경우가 많다. { "itemId": 10, "quantity": 2 } Spring에서는 이후 HttpMessageConverter 등을 통해 이 JSON을 Java 객체로 변환할 수 있다. 예를 들어 @PostMapping("/orders") public ResponseEntity<?> create( @RequestBody CreateOrderRequest request ) { } 의 JSON ↓ CreateOrderRequest 변환이 일어난다. 6. HTTP 요청마다 TCP Connection을 새로 만들까? 처음에는 다음처럼 생각할 수 있다. Request 1 TCP 연결 TLS 연결 Request Response 연결 종료 Request 2 TCP 연결 TLS 연결 Request Response 연결 종료 이렇게 하면 Request마다 TCP Handshake TLS Handshake 비용을 계속 지불해야 한다. 비효율적이다. 그래서 기존 Connection을 재사용한다. 6-1. HTTP Keep-Alive HTTP/1.1에서는 Persistent Connection이 기본 동작이다. 즉 Response를 받았다고 TCP Connection을 반드시 바로 종료하지 않는다. TCP + TLS Connection 생성 ↓ Request 1 Response 1 ↓ Request 2 Response 2 ↓ Request 3 Response 3 ↓ Connection 종료 같은 Connection을 여러 Request에 재사용할 수 있다. 6-2. 왜 Connection을 재사용할까? 매번 Connection을 새로 만들면 다음 비용이 발생한다. TCP Handshake TLS Handshake Socket 생성/정리 Network RTT 증가 CPU 암호 연산 Connection을 재사용하면 이러한 비용을 줄일 수 있다. 6-3. Keep-Alive와 Connection Pool Client가 Backend 서버를 호출한다고 생각해보자. 예를 들어 Spring Server A ↓ 외부 API Server B A가 B를 호출할 때마다 Connection을 새로 만들기보다는 HTTP Client가 Connection Pool 을 관리할 수 있다. Connection Pool Connection 1 ─ Server B Connection 2 ─ Server B Connection 3 ─ Server B Request가 들어오면 기존 Connection을 빌려 사용하고 다시 Pool에 돌려준다. 6-4. Connection Pool도 무한하지 않다 예를 들어 Pool에 Connection이 100개밖에 없는데 동시에 1,000개 요청이 외부 서버를 호출한다고 해보자. 100개 → Connection 사용 나머지 900개 → Connection을 기다림 그러면 여기에서도 Queue와 Timeout 문제가 생긴다. 1주차에서 Thread Pool을 봤듯이 Network에도 제한된 Resource가 존재한다. Thread Pool DB Connection Pool HTTP Connection Pool 모두 비슷한 Capacity 문제를 가진다. 6-5. HTTP/2에서는? HTTP/1.1 Connection 재사용보다 더 발전된 방식이 있다. HTTP/2에서는 하나의 TCP Connection 안에서 여러 Stream 을 Multiplexing할 수 있다. 하나의 TCP Connection ├─ Stream 1 → Request A ├─ Stream 3 → Request B ├─ Stream 5 → Request C └─ Stream 7 → Request D 즉 Connection 하나 = Request 하나 가 아니다. 핵심 질문 4 Keep-Alive는 단순히 서버가 살아 있는지 확인하는 기능인가? 여기서 말하는 HTTP Persistent Connection의 Keep-Alive는 서버 Health Check 기능이 아니다. 핵심은 한 Request가 끝난 뒤에도 Connection을 유지하고 다음 Request에서 재사용하는 것 이다. Connection 생성 비용을 줄여 성능을 높일 수 있다. 7. 요청이 서버에 도착했다 이제 Request가 서버 측 Network Stack까지 도착했다고 해보자. Spring Boot + Tomcat 기반의 전통적인 Spring MVC 서버를 예로 들면 크게 다음과 같은 흐름이 된다. Network ↓ Tomcat Connector ↓ Servlet Container ↓ Filter Chain ↓ DispatcherServlet ↓ HandlerMapping ↓ HandlerAdapter ↓ Controller 실제 환경에서는 앞에 CDN Load Balancer Reverse Proxy Nginx API Gateway 등이 추가될 수도 있다. 7-1. Tomcat이 요청을 받는다 Spring Boot에서 Spring MVC를 사용하면 기본적으로 Embedded Tomcat을 사용하는 구성이 흔하다. Tomcat은 Network Connection에서 HTTP Request를 읽고 HTTP Message를 Parsing한다. 이를 Servlet API의 HttpServletRequest HttpServletResponse 형태로 다룰 수 있게 한다. 7-2. Worker Thread 전통적인 Spring MVC + Servlet 기반 서버에서는 Worker Thread가 Request 처리를 담당한다. Request A → Thread 1 Request B → Thread 2 Request C → Thread 3 그래서 지난 주에 공부한 Thread Pool Thread Queue Context Switching I/O Bound 개념이 여기서 그대로 연결된다. 7-3. Filter Servlet 요청은 Spring MVC Controller로 가기 전에 Filter Chain을 통과할 수 있다. 대표적으로 Spring Security도 Filter Chain을 활용한다. Request ↓ Security Filter ↓ Authentication 확인 ↓ Authorization 확인 ↓ DispatcherServlet 예전에 Spring Security에서 공부했던 UsernamePasswordAuthenticationFilter AuthenticationManager AuthenticationProvider SecurityContext 등의 흐름도 결국 Controller보다 앞쪽 Filter 계층에서 시작될 수 있다. 7-4. DispatcherServlet Spring MVC의 핵심 Front Controller다. 모든 Controller가 직접 HTTP Request를 처음 받는 것이 아니라 DispatcherServlet이 중앙에서 Request를 받아 적절한 Handler로 연결한다. Request ↓ DispatcherServlet ↓ 어떤 Controller가 처리하지? 7-5. HandlerMapping DispatcherServlet은 HandlerMapping 등을 통해 현재 Request를 처리할 Handler를 찾는다. 예를 들어 @RestController @RequestMapping("/orders") public class OrderController { @PostMapping public void createOrder() { } } 가 존재하고 POST /orders 가 들어왔다면 해당 Handler Method를 찾는다. 7-6. HandlerAdapter DispatcherServlet이 모든 종류의 Handler를 직접 실행하는 대신 HandlerAdapter가 Hand
안녕하세요~! 오늘은 데이터프레임 결합 및 병합에 대해 학습해보겠습니다. 1. 데이터 재구조화 📌 데이터 재구조화: 데이터를 목적에 맞게 변형하고 정리하는 과정 필요에 따라 1️⃣그룹화, 2️⃣특정 기준에 따른 통계량 계산, 3️⃣새로운 파생 변수를 생성하기로 함 이러한 변형을 통해 기존에 보이지 않던 패턴이나 관계를 발견하기도 하고, 모델이 다양한 패턴을 학습할 수 있도록 함 2. 데이터 가로 결합 📌 가로 결합: 두 개 이상의 데이터 프레임을 열(컬럼) 기준으로 병합 하는 방법 이는 동일한 관측 대상에 대한 서로 다른 속성이나 정보를 하나의 행에 결합함 ✅ 사용 시기 동일한 키나 인덱스를 가진 데이터프레임에서 서로 다른 정보를 결합하고자 할 때 특정 키 값이나 조건에 맞는 데이터만 선택하여 결합하여 원하는 데이터만 집중적으로 분석 장점 👍 관련된 정보를 하나의 데이터프레임으로 결합하여 분석과 시각화 용이 여러
Task 1. With what kind of tool can intercept web traffic? 웹 트래픽을 인터셉트하는 툴은 대표적으로 BurpSuite가 있다. 해당 툴은 Proxy처럼 동작해 서버로 리퀘스트를 전송하기 전 HTTP 패킷을 수정하거나 내부 패킷을 확인하는 기능을 제공한다. 따라서 해당 task의 답은 Proxy이다. 사실 이전에 문제를 대충 읽어서 답은 burpsuite로 적었다가 다시 읽고 답이 proxy임을 알았다... Task 2. What is the path to the directory on the webserver that returns a login page? 로그인 페이지 경로가 무엇인지 물어본다. 가장 먼저 생각나는 방법인 gobustser 를 활용해보자. 로그인 경로가 따로 보이지는 않는다. 이 문제에서 프록시에 대해 언급했으므로 Burpsuite를 이용해 확인해보자. 지금까지 써 본 기능이 Proxy 기능밖에 없어서 크게 도움이 되지는 않았다. 이전까지는 헤더를 조작하는 웹해킹 문제만 풀어 봤으니... 따라서 Burpsuite 기능을 좀 더 살펴보았다. 맨 처음 메뉴인 Dashboard 의 가운뎃부분에 좋은 정보가 떠있다! gobuster 로는 탐지하지 못했던 서브디렉토리 정보가 자세히 나와있었다. 추측컨데 gobuster의 wordlist에 담긴 문자열로는 생성할 수 있는 URL이 한정적이다 보니 그런 것 같다. 찾아보니 burpsuite는 실제 HTTP 트래픽을 인터셉트해서 이를 Target 모듈에 기록한다고 한다. 이전의 HTTP Response 패킷의 Body를 분석해 해당 Site Map을 생성한다고 한다. 그래서 워드리스트에 의존하는 gobuster와 달리 직접 HTTP 패킷을 분석하므로 이전에 발견하지 못한 디렉토리/파일들을 발견할 수 있는 것이다. 단, /admin 처럼 숨겨진 웹페이지에 대해서는 실제로 연결해 볼 수 없기 때문에 이는 찾을 수 없다. 따라서 쉽게 말해 Gobuster와 상호보완적인 관계라 할 수 있을 것이다. 위 이미지처럼 /cdn-cgi/login 디렉토리에 script.js 파일이 존재한다. 따라서 Task 2의 답은 /cdn-cgi/login . Task 3.What can be modified in Firefox to get access to the upload page? 이전의 /cdn-cgi/login에 접속한 모습이다. 현재 알고 있는 admin 정보가 없으므로 관련 정보를 알아내야 한다. 먼저 웹사이트부터 조사해보기 위해 Login as Guest 를 눌러 게스트로 로그인한다. 5가지 메뉴 (Account, Branding, Clients, Uploads, Logged in as Guest)가 존재한다. 본 Task에서 질문하는 Uploads 페이지에 접근해보자. super admin 권한이 필요하다고 한다. 그렇다면 admin 계정과 관련된 정보를 알아야 할 듯 하다. 그런데 잘 생각해보면 문제는 Firefox 내부에서 수정할 수 있는 것이 무엇이냐고 물었다. Firefox는 잘 쓰지 않아 자세한 기능들은 모르지만 웹브라우저라면 쿠키는 있을 것이므로 쿠키를 찾아보기로 했다. devtool 키는 동일하게 f12였다. 이미지에서 볼 수 있듯, role 쿠키와 user 쿠키가 존재한다. 따라서 해당 Task의 답은 cookie . Task 4. What is the access ID of the admin user? 이전에 살펴보지 않은 나머지 메뉴를 살펴보자. 우선 Account 메뉴를 확인해보자. URL에 파라미터로 content , id 가 전달되었다. id를 1로 수정해보자. Access ID와 Name, Email이 변경되었다! 이로 인해 Admin의 Access ID가 34322임을 알 수 있다. Task 5. On uploading a file, what directory does that file appear in on the server? 파일을 업로드할 시 서버의 어느 디렉토리에 업로드되는지 물어보고 있다. 이전 문제에서 얻은 cookie 정보를 이용해 해당 웹페이지에 접근해보자. 이전과는 달리 파일을 업로드할 수 있는 웹페이지가 반환된다. 해당 페이지에 <?php system('id'); ?> 가 작성된 shell.php 를 업로드한다. Brand Name은 뭔지 모르니 우선 nike로 기입했다. 업로드된 파일은 이전에 gobuster로 찾은 /uploads 디렉토리에 저장된다. Task 6. What is the file that contains the password that is shared with the robert user? 해당 Task를 수행하기 전, 우리가 업로드한 php파일에 접근해보자. 정상적으로 php 파일이 실행된다. 이를 통해 php 리버스쉘을 실행할 수 있을 것이다. 리버스쉘 php파일으로 shell.php를 수정한 후 재업로드한다. 이후 파일을 탐색하던 중 아래와 같은 파일을 발견했다. 따라서 Task 6의 답은 db.php . Task 7. What executible is run with the option "-group bugtracker" to identify all files owned by the bugtracker group? -group 옵션을 사용하고 파일을 찾는 명령어는 find 이다. Task 8. Regardless of which user starts running the bugtracker executable, what's user privileges will use to run? 먼저, 이전 Task에서 사용한 -group bugtracker 를 이용해서 find를 수행한다. bugtracker 라는 실행파일이 있다. 해당 파일에 대해 ls -l을 실행해 보자. SetUID 프로그램이다. 즉, 실행하는 사용자와 무관하게 Root 권한으로 실행된다. bugtracker는 위 이미지처럼 일반사용자는 r 권한밖에 없다. 따라서 이전에 얻은 robert의 권한으로 로그인해 해당 파일을 실행한다. 위 이미지에서 cat /root/reports/<입력값> 을 실행한다. 이때 cat가 root 권한으로 실행되므로(SUID) 환경변수조작을 통해 cat 대신 쉘을 실행할 수 있다. 먼저 PATH 환경변수 맨 앞에 /tmp를 추가한다. 이후 /tmp 디렉토리에 /bin/sh 이 적힌 cat 를 생성한다. 생성 후 실행권한을 부여한다. 위 이미지처럼 루트쉘을 획득했다. 이제 /root 디렉토리의 flag.txt를 읽으면 되는데, 이전에 PATH 환경변수를 수정했으므로 이를 복원한 후 cat명령어를 사용해야 한다. 이후 다시 robert로 로그인해 user.txt를 읽는다.