TOEFL을 공부하면서 가장 어려웠던 부분 중 하나가 Listening이었다. 영어를 어느 정도 읽을 수 있어도, 실제로 영어가 빠르게 나오면 중요한 내용을 놓치는 경우가 많았다. 특히 긴 Listening 문제를 한꺼번에 풀려고 하면 시간이 많이 필요해서, 바쁜 날에는 아예 공부를 건너뛰게 되기도 했다. 그래서 짧게 하나씩 연습할 수 있는 방식 을 직접 만들어보기로 했다. 그렇게 만든 것이 DailyWordtoon 이다. 긴 문제를 한 번에 풀지 않기 DailyWordtoon에서는 TOEFL Listening을 여러 Task로 나누어 연습할 수 있도록 구성했다. 예를 들어 2026 TOEFL의 Listening에는 다음과 같은 유형이 있다. Listen and Choose a Response Listen to a Conversation Listen to an Announcement Listen to an Academic Talk 처음에는 하나의 전체 테스트를 끝까지 풀려고 했지만, 지금은 상황에 따라 하나의 작은 Practice Set만 먼저 끝내는 방식 으로 연습한다. 10~15분 정도 시간이 있으면 하나의 Set을 풀고 바로 결과를 확인한다. 중요한 건 문제를 푼 다음이었다 예전에는 문제를 틀리면 정답만 확인하고 넘어가는 경우가 많았다. 하지만 Listening에서는 왜 못 들었는지 확인하는 과정 이 더 중요하다는 것을 느꼈다. 그래서 DailyWordtoon에서는 문제를 푼 뒤 틀린 문제를 다시 확인하고, 필요한 경우 audio를 다시 들으면서 복습할 수 있도록 만들었다. 특히 짧은 문제는 부담 없이 여러 번 반복할 수 있어서 좋았다. Listen → Answer → Check → Review → Listen again 이 과정을 반복하다 보니 단순히 문제 수를 늘리는 것보다 내가 어떤 부분에서 자주 놓치는지 파악하기 쉬워졌다. 짧게 하는 것이 오히려 꾸준해졌다 TOEFL 공부에서 가장 어려운 것은 항상 공부 시간이 부족하다는 것이었다. 그래서 “오늘은 한 시간 공부해야지”라고 생각하면 오히려 시작하기가 어려웠다. 반대로 “10분만 Listening 문제를 풀자.” 라고 생각하면 훨씬 쉽게 시작할 수 있었다. 출퇴근이나 이동 시간처럼 짧은 시간이 생겼을 때 하나의 Practice Set을 끝내고, 틀린 문제만 다시 확인하는 방식도 가능했다. 매일 많은 문제를 풀지는 못하더라도 조금씩 계속 듣는 것 이 Listening 공부에서는 꽤 중요하다고 느꼈다. 2026 TOEFL을 준비한다면 2026 TOEFL은 Listening에도 새로운 Task가 많이 추가되었기 때문에, 예전 TOEFL Listening 문제만 반복하기보다는 새로운 Task 형식에 먼저 익숙해지는 것이 좋다. 나처럼 긴 시간 동안 공부하기 어려운 사람이라면, 짧은 Practice → 바로 피드백 → 틀린 문제 복습 → 다시 듣기 처럼 작은 단위로 공부해 보는 것도 추천한다. 이런 방식으로 직접 만들어 사용하면서 계속 개선하고 있는 것이 DailyWordtoon 이다. TOEFL Listening을 공부하면서 “시간이 없어서 오늘은 못 하겠다”는 생각이 자주 든다면, 한 번에 많이 공부하려고 하기보다 10분짜리 연습부터 시작해 보는 것 도 괜찮은 방법이다.
나는 우테코에 들어와서 참 많이 솔직해졌다고 스스로 생각했었다. 레벨1, 레벨2에서의 모습은 내가 지금까지 살면서 제일 솔직했다. 근데 레벨3에 들어오니 다시 처음으로 돌아간 것 같았다. 아니, 처음보다 더 좋지 않은 상황이었다. 이미 프론트끼리, 빽엔드끼리 친한 상황에서 같은 팀이지만 따로 노는 느낌을 많이 받았다. 이를 어떻게든 해결하고 싶었지만 나는 내향적이었고, 스몰토크하고 팀에게 조금 더 관심을 가지는 것이 내 노력이었다. 어떻게 보면 별 거 아닐 수 있었겠지만, 심리적 안전감이 없는 상황에서는 내 최선이라고 생각했다. 어쩔 수 없었나? 싶다가도 '내가 더 노력했어야 했나' 하는 생각도 들었다. 이 시기에는 내 마음이 참 답답했다. 이런 답답한 상황은 회의나 작업을 할 때도 나타났다. 이전보다 말을 편하게 하기 어려웠고, 이전보다 의견을 편하게 내기도 어려웠다. 작업 상황이 궁금해도 재촉하는 것 같아 말하기 무서웠고, 내가 다른 의견을 가지고 있어도 강력하게 밀고 나갈 수 없었다. 가장 말하기 어려웠던 순간은 프로젝트 2주차, 가설 검증 대시보드 제출 시기였다. 시간은 엄청 제한적이었고, 우리는 2~3일이라는 시간 내에 MVP를 완성해야 했다. 그래서 우리는 AI를 엄청 적극적으로 사용했다. 레벨3에서는 학습을 위주로 개발을 하고 싶었지만, 데드라인을 지키기 위해서 AI를 많이 사용했다. 그러나 개발 측면이 아닌, 데이터 수집 측면에서 문제가 발생했다. 데이터 수집과 정제에 시간이 오래 걸렸고, 마감일을 지키지 못해 가설 검증 대시보드는 임시로 채우고 그 외 작업들이 밀리기 시작했다. 팀이 공통으로 해야 하는 업무가 밀리면서 분야별 또는 개인별 업무가 밀리기 시작했고 결국 시간적으로 손해를 봤다는 생각밖에 떠오르지 않았다. 회의를 같이 진행하고, 의견을 하나로 정했다고 당연히 생각했지만 그게 아니었던 것이다. 모두가 생각하는 부분이 약간씩 차이가 존재했고, 소통의 부재로 인해 그 차이를 인식하지 못했다. 처음에 이러한 답답함을 팀에 얘기하려고 했지만, 입이 떨어지지 않았다. 팀 내 불편한 이야기를 전달하고 싶지도 않고, 심리적 안전감이 형성되지도 않았는데 이런 얘기로 인해 어색해지고 불편해지는 게 더 싫었다. 그렇지만 이렇게 쌓인 답답함은 쉽게 사라지지 않았고, 큰 결심을 하고 팀원들에게 털어놨다. 다소 불편할 수 있는 얘기였다. "화요일이 최종 배포날이었지만 수요일로 밀렸고, 그마저도 오후로 밀린 상황이었습니다. 배포 이후에만 할 수 있는 가설 검증 계획 수립과 요구사항 문서 작성도 자연스럽게 밀렸습니다. 리팩토링 및 기능 버그를 수정하는 와중에 계획이 틀어지니 답답한 상황이 생겼습니다. 데드라인을 지키기 위해서 학습과 클린 코드를 어느 정도 버린 상태에서, 데이터 수집으로 인해 일정이 딜레이되면서 이전 과정이 손해라는 생각이 들었습니다. AI 구현, 그 다음 구현, 대규모 코드 리펙토링, .. 거의 같은 작업을 3번 했고, 코딩 외 다른 할 일도 충분히 많았기 때문에 체력적으로도 힘든 상황이 있었습니다. 내 말로 인해 나는 팀이 어색해지고 불편해질 것이라고 생각했다. 그러나 얘기를 들은 팀원들의 반응은 내 예상과는 달랐다. "프론트가 그런 상황인지 전혀 몰랐습니다. 솔직하게 말해줘서 고맙습니다." '몇 주간의 답답한 상황이 이렇게 쉽게 풀리다니.. 참는다고 꼭 다 좋은 것은 아니었구나!' 라는 생각을 했다. 솔직하게 감정을 나누고, 그 고민을 들어줄 수 있고, 같이 해결해나가는 것이 생각보다 중요하다는 사실을 알았다. 위 고민을 털어놓은 이후, 우리는 데일리 스크럼과 회의 시간에 할 일과 계획을 공유한다. 위와 같은 상황이 다시 발생하지 않도록. 회피하지 말고, 애써 무시하지 말고, 합리화하는 것은 팀을 불안정하게 만든다. 불편해질 용기를 가져야 하고, 내 생각을 똑바로 말할 수 있어야 한다. '말하지 않으면, 아무것도 모른다'는 말은 나에게 정말 중요한 말이다.
공부하고 만들고 호작질한 것들 개발하면서 이것저것 찾아보고, 직접 만들어보고, 고장 내고, 다시 고치는 일을 자주 합니다. 그동안 프로젝트나 하드웨어 호작질을 하면서 문제를 해결했던 과정이나 새롭게 알게 된 내용들을 따로 기록하지 않아, 시간이 지난 뒤 같은 내용을 다시 찾아보는 일이 많았습니다. 그래서 앞으로는 공부하고 개발하면서 겪은 과정들을 조금씩 남겨보려고 합니다. 주로 다룰 내용 컴퓨터공학 · 개발 AI · Agent · RL 자율주행 · ROS 캡스톤 및 개인 프로젝트 서버 · 네트워크 · Docker 등 개발 인프라 PC · Server · H/W 그 외 단순한 호기심에서 시작한 호작질 완성된 결과 정리보다는, 어떤 문제가 있었고 → 무엇을 시도했고 → 왜 안 됐고 → 어떻게 해결했는지 그 과정을 기록해보려고 합니다. 거창한 기술 블로그보다는 제가 공부하고, 만들고, 망가뜨리고, 고친 것들을 쌓아두는 공간에 가깝습니다. 제가 관심 있는 분야들은 최신 정보나 사례가 해외 문서, 논문, 커뮤니티에 많이 몰려 있고, 국내 자료는 상대적으로 적거나 오래된 경우가 많았습니다. 그래서 제가 직접 찾아본 해외 자료와 정보들을 단순히 옮기기보다는, 직접 이해하고 사용해본 경험을 바탕으로 한국어로 정리해서 남겨보려고 합니다. 나중에 제가 다시 찾아볼 때 도움이 되고, 비슷한 분야에 관심 있는 분들에게도 조금이나마 도움이 되는 기록이 되었으면 합니다. 공부하고, 만들고, 망가뜨리고, 고치면서 계속 호작질해보겠습니다. !
스키마와 테이블, 행과 열 Schema : 데이터베이스의 전체 구조, 데이터 타입, 제약 조건, 테이블 간의 관계 등을 정의한 메타데이터 == 설계 “”” 이 데이터베이스는 어떻게 생겼고, 어떤 데이터를 어떻게 저장할 것이다 ””” 라는 약속 역할 : 어떤 종류의 데이터를 어떤 형식으로 저장할 것인가 - 에 대한 규칙을 정함 주요 요소 : table, column, data_type, 제약조건(데이터의 무결성을 보장하기 위한 규칙 | PK,FK ) * 데이터 무결성이란? 데이터의 수명 주기 동안 정확성, 일관성, 유효성이 유지되고 승인 없이 변경되거나 손상되지 않은 상태 주요 유형 : 개체 무결성(PK | 고유값 | NOT NULL), 참조 무결성(FK | 테이블 간의 관계가 일관되게 유지되도록 보장), 도메인 무결성(필드의 데이터 타입, NULL여부, 허용된 값 범위 등 올바르게 입력되었는지 확인) 무결성이 중요한 이유 : 오류/실수 방지, 운영 효율성, 규정 준수 [ EX ] CREATE TABLE Customers( customer_id INT PRIMARY KEY, - 고객 ID, 정수형, 기본키 name VARCHAR(100) NOT NULL, - 이름, 최대 100자 문자열, NULL아님 email VARCHAR(255) UNIQUE, - 이메일, 최대 255자 문자열, 고유해야 함 phone_number VARCHAR(20) - 전화번호, 최대 20자 문자 ); SQL을 쓸 때는 거의 항상 데이터베이스.테이블 두 단어로 위치를 가리킴 EX) FROM pharma_sales.part_d_prescriber SELECT 쿼리는 쓰는 순서와 읽는 순서가 다르다 쓰는 순서 SELECT | FROM | WHERE | AND | ORDER BY | LIMIT 읽는 순서 FROM : 먼저 어떤 테이블을 읽을지 정한다 WHERE : 원본 처방 행에서 주, 약물, 조건을 먼저 줄인다 SELECT : 강의와 분석에 필요한 컬럼만 남긴다 ORDER BY : 처방량, 비용처럼 볼 기준으로 줄을 세운다 LIMIT : 상위 몇 건만 꺼내 결과를 빠르게 확인한다 [ 집계 함수 ] 어떤 데이터를 집계하는 함수들을 의미. COUNT “ SELECT COUNT(*) FROM citykorea ; “ 모든 레코드의 수를 출력하는 query SUM “ SELECT SUM(population) FROM citykorea ; ” 인구수 합계를 출력하는 query MAX / MIN “ SELECT name, max(population) FROM citykorea ; ” 가장 많은 인구수를 가진 도시의 이름과, 인구수를 출력한 query AVG “ SELECT AVG(population) FROM citykorea ; ” citykorea의 평균 인구수를 출력하는 쿼리 [ Group Query ] 집계함수의 결과를 특정 컬럼을 기준으로 묶어 결과를 출력해주는 쿼리 GROUP BY 정의 : 같은 기준을 가진 여러 행을 한 행으로 접어 요약값을 만드는 문법 원본 행을 의사결정 단위로 접는다 원본에 대한 해상도는 떨어지지만 통합본은 볼 수 있음 SQL에서 GROUP BY에 넣는 함수를 제외하고 다 집계함수가 되어야 함 SELECT에 GROUP BY할 것만 둠 나머지 집계함수에 들어갈 것들을 COUNT() AS name SELECT 와 GROUP BY 순서를 맞추면 좋음 “”” SELECT column, 집계함수(column) FROM TABLE GROUP BY column ; “”” HAVING 정의 : GROUP BY로 만든 요약값에 조건을 거는 문법 WHERE과 차이 : WHERE는 집계 전 원본 형 필터, HAVING은 집계 후 그룹 필터 GROUP BY 이후에 생긴 SUM, AVG, COUNT 같은 요약값으로 거른다. “”” SELECT column, 집계함수(column) FROM TABLE GROUP BY column HAVING 집계함수(column) 부등호 data ; “”” COUNT DISTINCT 중복을 제거한 고유한 행의 개수를 구하기 위해 적용 “ SELECT COUNT(DISTINCT name) FROM data_table ; ” name 컬럼에서 중복을 제외한 이름의 총 개수 LEFT JOIN 왼쪽 테이블의 모든 데이터와 오른쪽 테이블에서 조건이 일치하는 데이터를 연결하여 반환하는 방 “”” SELECT * FROM tableA A LEFT JOIN tableB B ON A.key = B.key ; “”” table A는 항상 결과에 포함. A.key = B.key 값이 같은 행이 있다면 함께 출력, 없으면 table B자리에 NULL [ CASE ] 기본 문법 CASE WHEN 조건1 THEN 조건1 충족할 때 반환되는 값 WHEN 조건2 THEN 조건2 충족할 때 반환되는 값 ELSE 모든 조건 해당되지 않을 때 반환되는 값 END WHEN - THEN 항상 같이 사용 | 조건문 마지막에 END 꼭 써주기 CASE 문을 사용할 때 별칭AS을 사용해주는 것이 좋음 --> EX CASE WHEN total_claims >=100 THEN ‘고처방’ ELSE ‘저처방’ END AS name [ DECIMAL ] 실수의 값을 정확하게 표현하기 위해 사용 , DECIMAL타입은 NUMERIC을 구현하여 만들어짐 DECIMAL(7,2) == -99999.99 ~ 99999.99 까지 실수를 저장할 수 있도록 DECIMAL(A,B) == A는 실수의 총 자릿수 | B는 소수 부분 “ CAST( table_name AS DECIMAL(num , num)) ” [ with절 ] 임시 테이블 , 복잡한 서브쿼리를 가상의 테이블로 미리 정의해 재사용 할 수 있게 해주는 CTE 기능 쿼리를 단순화하고 가독성을 높일 수 있음 --> EX WITH SalesEmployees AS ( SELECT FirstName, LastName, JobTitle, Salary FROM employees e JOIN departments d ON e.DepartmentID = d.DepartmentID WHERE DepartmentName = ‘Sales’ ) SELECT FirstName, LastName, JobTitle, Salary FROM SalesEmployees ; [ WINDOW 함수 ] 정의 : 행을 줄이지 않고 계산값을 붙인다. 원본 행을 유지하고, 그룹 안에서 계산한 값을 새 컬럼으로 붙인다. partition을 어떻게 나눌것인가? , order by 정렬 , row number/lag/sum 무엇을 넣을 것인가? row_number : HCP별 가장 최근 접점 또는 첫 접점을 찾는다. LAG : 이전 접점, 이전 처방량, 이전 방문값을 현재 행 옆에 붙인다 SUM OVER : 누적 지급액, 누적 처방량, 누적 방문 수를 만듦 원본 함수를 살리고 싶으면 window함수가 좋다. [ 기본 문법 ] “ SELECT WINDOW_FUNCTION (ARGUMENTS) OVER ( [PARTITION BY column] [ODER BY column] [WINDOWING 절] ) FROM 테이블명 ; ” --> EX “ SELECT name, age, sal, sum(sal) OVER(ORDER BY sal ROWS BETWEEN unbounded preceding AND CURRENT row) 누적 연봉 FROM test” 처음부터 현재 행까지의 누적 GOOGLE_DRIVE
딥러닝 기초에서 RNN은 보통 가볍게 언급되고 넘어가는 경우가 잦다. "순차적이라 병렬화가 안 되고, Transformer에 밀렸다." 그런데 올해 ICLR에서 발표된 애플 논문이, RNN을 병렬로 학습시켜 70억 파라미터까지 키웠다고 한다. 요즘 AI 논문은 제목만 화려한 경우가 많아서 반신반의하며 읽었는데, 의외로 아이디어는 명확했고 논문의 주장도 절제되어 있었다. ParaRNN 의 개념과, 지표의 해석까지 가볍게 소개한다. TL;DR RNN은 이전 은닉 상태가 있어야 다음 상태를 계산할 수 있어서 학습을 병렬화하지 못했다. Mamba 같은 State Space Model은 점화식을 선형 으로 제한해 이 문제를 피했다. ParaRNN은 비선형을 그대로 두고, 모든 시점의 은닉 상태를 연립방정식의 해 로 본 다음 뉴턴법으로 푼다. 뉴턴법 한 스텝은 선형 문제라 병렬로 풀리고, 세 번이면 수렴한다. 다만 결과는 "RNN이 Transformer를 이겼다"가 아니라 "병렬화 장벽은 넘을 수 있다"에 가깝다. RNN 기본 개념 RNN이 하는 일은 식 하나다. $$ h_l = f(h_{l-1},, x_l) $$ $l$번째 은닉 상태를 구하려면 $l-1$번째가 먼저 있어야 한다. 토큰이 1000개면 1000번을 차례로 계산해야 하고, GPU에 코어가 수천 개 있어도 한 줄로 서서 기다릴 수밖에 없다. 모든 토큰을 한꺼번에 처리하는 Transformer와는 학습 속도에서 상대가 안 됐다. 그래도 RNN에는 미련이 남을 만한 장점이 있다. 문맥이 아무리 길어져도 토큰 하나를 만드는 비용이 일정하다는 것이다. Mamba 이 약점을 먼저 피해 간 게 Mamba와 같은 State Space Model(SSM, 상태 공간 모델)이다. 점화식을 은닉 상태에 대해 선형 으로 제한했다. $$ h_l = A_l, h_{l-1} + B_l, x_l $$ 선형이면 두 단계를 미리 합칠 수 있다. $h_1 = 2h_0 + 1$, $h_2 = 3h_1 + 4$ 라면 $h_0$ 값을 몰라도 $h_2 = 6h_0 + 7$ 로 묶어둘 수 있다. 이웃한 단계끼리 동시에 합치기를 반복하면 $L$단계가 $\log_2 L$번 만에 끝난다. parallel scan이라는 오래된 알고리즘이다. 대신 표현력을 타협한 셈이다. 논문 표현 상, 선형은 좋아서 고른 게 아니라 어쩔 수 없어서 고른 제약이다. ParaRNN ParaRNN은 관점을 바꾼다. $h_1, \dots, h_L$ 을 차례로 계산할 값이 아니라, 방정식 $L$개를 동시에 만족해야 하는 미지수 로 본다. 비선형 연립방정식이니 뉴턴 방법으로 푼다. 답을 대충 찍고, 그 근처에서 $f$ 를 직선으로 근사한 뒤, 근사한 문제를 풀어 답을 고치는 식이다. 고칠 양 $\delta h_l$ 에 대한 식은 이렇게 생겼다. $$ \delta h_l = J_l, \delta h_{l-1} + r_l $$ 바로 위의 선형 RNN과 똑같은 모양이다. $f$ 의 기울기인 야코비안 행렬 $J_l$ 이 $A_l$ 자리에, 지금 답이 얼마나 틀렸는지를 나타내는 잔차 $r_l$ 이 입력 자리에 들어갔을 뿐이다. 그러니 Mamba가 쓰는 parallel scan을 그대로 가져다 쓸 수 있다. 정리하면 비선형 RNN 한 번 = 선형 RNN 몇 번 이다. 반복이 길어지면 병렬화한 의미가 없는데, 논문에서는 세 번 이면 충분했다고 한다. 야코비안을 대각으로 은닉 차원이 $d$ 면 야코비안은 $d \times d$ 행렬이라, 큰 모델에서는 저장하고 곱하는 비용을 감당할 수 없다. 그래서 논문은 GRU와 LSTM에서 은닉 상태에 곱해지는 가중치 행렬을 대각행렬 로 제한했다. 그러면 야코비안도 대각 구조가 되어 비용이 $d$ 에 비례하게 된다. (과정을 수식화 할 수도 있겠다만, 분량이 길어지므로 생략한다) 트레이드오프는 셀 안에서 은닉 차원끼리 섞이지 않는다는 것이다. $d$차원 RNN 하나가 사실상 1차원 RNN $d$개로 쪼개진 셈이고, 섞는 일은 뒤따르는 MLP 층이 맡는다. "RNN의 부활"이라고 보기엔 기존 GRU·LSTM과 꽤 다른 구조이다. 결과 지표 출처: https://arxiv.org/abs/2510.21450 논문이 내세우는 성과는 두 가지다. 665배 빨라졌다 , 그리고 70억 파라미터 RNN이 Transformer와 경쟁한다 . 둘 다 사실이지만 오해하기 쉬우니, 명확하게 짚고 넘어가자. 665배는 같은 RNN을 차례로 돌렸을 때 와 비교한 값이다. Transformer보다 빠르다는 얘기가 아니고, 학습 스텝 하나에 걸리는 시간은 여전히 Transformer가 더 짧다. 성능도 마찬가지다. perplexity는 7B에서 RNN(9.16~9.19)이 Transformer(9.55)를 근소하게 앞서지만, 1B 이하에서는 뒤진다. 그리고 전 구간 1등은 선형인 Mamba2가 차지했다(7B에서 8.62). 비선형의 이점이 뚜렷했던 건 1의 개수가 홀수인지 짝수인지 맞히는 식의 합성 과제뿐이었다. 저자들의 목표도 더 좋은 RNN을 제안하는 게 아니라, 고전적인 비선형 RNN도 대규모로 학습시킬 수 있음을 보이는 것. 마무리 "RNN은 병렬화가 안 된다"는 말은 법칙처럼 들리지만, 정확히는 "차례로 푸는 방법밖에 몰랐다"에 가까웠다. 같은 계산을 연립방정식으로 다시 보니 직관적인 뉴턴법으로 풀리는 문제였다. 이 논문 하나로 Transformer의 입지가 위협받진 않겠지만, 의미는 충분하다. 기존 방법론을 새로운 시각으로 보려는 시도가 새 패러다임을 만드는 법이니까 말이다.
Promise 1. 비동기 처리를 위한 콜백 함수 패턴의 단점 2. 프로미스의 생성 ES6에서부터 이러한 문제점을 해결하기 위해 프로미스가 만들어졌다. // 프로미스 생성 const promise = new Promise((resolve, reject) => { // Promise 함수의 콜백 함수 내부에서 비동기 처리를 수행한다. if (/* 비동기 처리 성공 */) { resolve('result'); } else { /* 비동기 처리 실패 */ reject('failure reason'); } }); 프로미스 객체는 new Promise()를 이용하여 생성할 수 있다. 프로미스는 콜백 함수를 인자로 전달받는데 이 콜백 함수가 받는 인자는 resolve와 reject이다. resolve(value): 작업이 성공했을 때 값을 전달한다. reject(error): 작업이 실패했을 때 에러 값을 전달한다. 프로미스는 다음과 같은 상태 정보를 갖는다. 상태 의미 상태 변경 조건 pending 비동기 처리가 아직 끝나지 않아 결과가 정해지지 않은 상태 new Promise() 로 생성된 직후의 기본 상태 fulfilled 비동기 처리가 성공적으로 수행됨 resolve() 호출 rejected 비동기 처리가 실패함 reject() 호출 3. 프로미스의 후속 처리 메서드 프로미스는 비동기 작업이 완료된 후 여러가지 처리를 할 수 있도록 메서드를 제공한다. then: 비동기 처리가 성공이나 실패했을 때 실행할 콜백 함수를 등록하는 메서드이다. // fulfilled new Promise(resolve => resolve('fulfilled')) .then(v => console.log(v), e => console.error(e)); // fulfilled // rejected new Promise((_, reject) => reject(new Error('rejected'))) .then(v => console.log(v), e => console.error(e)); // Error: rejected catch: 프로미스가 rejected 상태로 실패했을 때 호출되는 메서드이다. // rejected new Promise((_, reject) => reject(new Error('rejected'))) .catch(e => console.log(e)); // Error: rejected finally: 프로미스의 성공, 실패와 무관하게 무조건 한 번 호출되는 메서드이다. new Promise(() => {}) .finally(() => console.log('finally')); // finally 4. 프로미스의 에러 처리 then, catch를 사용해 에러 처리를 할 수 있지만 가독성을 위해 catch로 에러 처리하는 것을 권장한다. then과 달리 catch는 앞에서 발생한 모든 then의 에러를 한 번에 잡을 수 있다. promiseGet('https://jsonplaceholder.typicode.com/todos/1') .then(res => console.xxx(res)) .catch(err => console.error(err)); // TypeError: console.xxx is not a function 5. 프로미스 체이닝 프로미스는 다음 코드와 같이 언제나 프로미스를 반환하므로 연속적으로 호출할 수 있다.이를 프로미스 체이닝이라고 한다. const url = 'https://jsonplaceholder.typicode.com'; // id가 1인 post의 userId를 취득 promiseGet(`${url}/posts/1`) // 취득한 post의 userId로 user 정보를 취득 .then(({ userId }) => promiseGet(`${url}/users/${userId}`)) .then(userInfo => console.log(userInfo)) .catch(err => console.error(err)); 6. 프로미스의 정적 메서드 프로미스는 5가지 정적 메서드를 제공한다. Promise.resolve / Promise.reject: 각자 fulfilled, rejected 상태의 프로미스를 반환한다. // 배열을 resolve하는 프로미스를 생성 const resolvedPromise = Promise.resolve([1, 2, 3]); resolvedPromise.then(console.log); // [1, 2, 3] // 에러 객체를 reject하는 프로미스를 생성 const rejectedPromise = Promise.reject(new Error('Error!')); rejectedPromise.catch(console.log); // Error: Error! 2. Promise.all: 여러 개의 비동기 처리를 병렬 처리한다. 하나라도 실패하면 reject 상태가 된다. ```js const requestData1 = () => new Promise(resolve => setTimeout(() => resolve(1), 3000)); const requestData2 = () => new Promise(resolve => setTimeout(() => resolve(2), 2000)); const requestData3 = () => new Promise(resolve => setTimeout(() => resolve(3), 1000)); Promise.all([requestData1(), requestData2(), requestData3()]) .then(console.log) // [ 1, 2, 3 ] ⇒ 약 3초 소요 .catch(console.error); Promise.race: 가장 먼저 처리 완료(settled)된 것을 따라간다. 가장 먼저 끝난 게 성공이면 성공, 실패면 실패이다. Promise.race([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]) .then(console.log) // 3 .catch(console.log); Promise.allSettled: 모든 프로미스가 완료되면 처리 결과를 반환한다. 처리 성공, 실패에 무관하게 모든 프로미스들의 처리 결과를 반환해 준다. ```js Promise.allSettled([ new Promise(resolve => setTimeout(() => resolve(1), 2000)), new Promise((_, reject) => setTimeout(() => reject(new Error('Error!')), 1000)) ]).then(console.log); /* [ {status: "fulfilled", value: 1}, {status: "rejected", reason: Error: Error! at :3:54} ] */ ## [7. 마이크로 태스크 큐](https://velog.io/@jang1305q/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C#micro-task-queue--macro-task-queue) # async await ## 1. async / await 예전에는 제너레이터를 이용해 비동기 처리를 했지만 ES8이후 제너레이터보다 간단하고 가독성이 좋은 async / await 가 도입되었다. 프로미스 기반으로, 별개의 후속 처리가 필요없이 동기 처리처럼 프로미스를 사용할 수 있다. ```Js // async 함수 선언문 async function foo(n) { return n; } foo(1).then(v => console.log(v)); // 1 // async 함수 표현식 const bar = async function (n) { return n; }; bar(2).then(v => console.log(v)); // 2 // async 화살표 함수 const baz = async n => n; baz(3).then(v => console.log(v)); // 3 // async 메서드 const obj = { async foo(n) { return n; } }; obj.foo(4).then(v => console.log(v)); // 4 // async 클래스 메서드 class MyClass { async bar(n) { return n; } } const myClass = new MyClass(); myClass.bar(5).then(v => console.log(v)); // 5 await는 async 함수 내부에서 사용해야 한다. 다만 클래스의 constructor 메서드는 async 메서드가 될 수 없다. 서로 반환하는 값이 인스턴스와 프로미스로 각자 다르기 때문이다. 2. await 키워드 const fetch = require('node-fetch'); const getGithubUserName = async id => { const res = await fetch(`https://api.github.com/users/${id}`); // 1 const { name } = await res.json(); // 2 console.log(name); // Ungmo Lee }; getGithubUserName('ungmo2'); await 키워드는 프로미스가 처리 완료 상태가 될 때까지 대기했다가 처리 결과를 반환한다. 그러나 모든 프로미스에 await키워드를 사용하는 것은 지양해야 한다. 비동기 작업들이 서로 연관이 없는 경우에는 Promise.all 처럼 병렬적 처리를 하는 것이 좋다. async function foo() { const res = await Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), new Promise(resolve => setTimeout(() => resolve(2), 2000)), new Promise(resolve => setTimeout(() => resolve(3), 1000)) ]); console.log(res); // [1, 2, 3] } foo(); 3. 에러 처리 async await에서 에러 처리는 try catch문을 사용할 수 있다. async 함수 내에서 catch문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject하는 프로미스를 반환한다. 따라서 async함수를 호출하고 Promise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할 수 있다 // 함수 안에서 try/catch const fetch = require('node-fetch'); const foo = async () => { try { const wrongUrl = 'https://wrong.url'; const response = await fetch(wrongUrl); const data = await response.json(); console.log(data); } catch (err) { console.error(err); // TypeError: Failed to fetch } }; foo(); // 호출하는 쪽에서 .catch const fetch = require('node-fetch'); const foo = async () => { const wrongUrl = 'https://wrong.url'; const response = await fetch(wrongUrl); const data = await response.json(); return data; }; foo() .then(console.log) .catch(console.error); // TypeError: Failed to fetch 코드 1 (함수 안에서 try/catch) 코드 2 (호출하는 쪽에서 .catch) 에러를 잡는 곳 foo 내부 foo 를 호출한 쪽 foo() 가 돌려주는 프로미스 fulfilled (값은 undefined ) rejected 호출한 쪽이 실패를 아는가 모름 앎
Spring Boot 개발 환경 준비 📚 인프런 「2026년! 백엔드 개발자를 위한 Redis 실전 가이드」 실습 자료를 Spring Boot(Java) 기준으로 바꿔 정리한 노트 1. 준비물 확인 준비물 버전 비고 JDK 21 (17 이상) java -version 으로 확인 IntelliJ IDEA Community도 가능 Spring 개발 표준 IDE Docker Desktop 최신 Redis, PostgreSQL 실행용 java -version docker --version 2. 프로젝트 만들기 (Spring Initializr) https://start.spring.io 에서 아래처럼 설정하고 GENERATE 를 누른 뒤, 압축을 풀어 IntelliJ로 엽니다. 항목 값 Project Gradle - Groovy Language Java Spring Boot 3.5.x Group / Artifact com.study / redis-practice Packaging / Java Jar / 21 Dependencies Spring Web Spring Data Redis (Access+Driver) Spring Data JPA MyBatis Framework PostgreSQL Driver Lombok 💡 Initializr 기본값이 Spring Boot 4.x라면 생성 후 build.gradle 에서 3.5.x로 맞춰 주세요. 이 노트의 라이브러리 버전은 Boot 3.5 기준입니다. 3. 왜 가상환경(venv)이 필요 없을까? Python은 라이브러리를 PC 전역에 설치하므로 프로젝트마다 venv로 격리해야 합니다. Java는 빌드 도구(Gradle)가 프로젝트별로 의존성을 관리 합니다. Python Java (Gradle) python -m venv venv 필요 없음 (프로젝트 폴더 자체가 격리 단위) pip install fastapi build.gradle 의 dependencies 에 한 줄 추가 requirements.txt build.gradle 가상환경 활성화 IntelliJ가 Gradle을 자동 인식 (Gradle 새로고침 🐘) 프로젝트 A는 Spring Boot 3.5를, 프로젝트 B는 4.0을 써도 서로 간섭하지 않습니다. 4. 의존성 확인 ( build.gradle ) plugins { id 'java' id 'org.springframework.boot' version '3.5.6' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.study' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.5' runtimeOnly 'org.postgresql:postgresql' // Swagger UI (FastAPI의 /docs 역할) - 직접 추가 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' // 코드 수정 시 자동 재시작 (uvicorn --reload 역할) - 직접 추가 developmentOnly 'org.springframework.boot:spring-boot-devtools' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() } 라이브러리 역할 spring-boot-starter-web 웹 프레임워크 + 내장 톰캣 서버 (FastAPI + uvicorn 역할) spring-boot-starter-data-redis Redis 클라이언트(Lettuce) + Spring 연동 spring-boot-starter-data-jpa JPA(Hibernate) mybatis-spring-boot-starter MyBatis springdoc-openapi Swagger UI 자동 생성 spring-boot-devtools 코드 변경 시 자동 재시작 5. 실습용 Redis, PostgreSQL 실행 # Redis docker run -d --name my-redis -p 127.0.0.1:6379:6379 redis:8 # PostgreSQL (DB 이름: redis_study) docker run -d --name my-postgres -p 127.0.0.1:5432:5432 \ -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=redis_study postgres:17 로컬에 PostgreSQL이 이미 설치되어 있다면 컨테이너 대신 redis_study DB만 만들어도 됩니다. 6. 연결 설정 ( application.yml ) src/main/resources/application.properties 를 지우고 application.yml 을 만듭니다. spring: data: redis: host: localhost port: 6379 database: 0 datasource: url: jdbc:postgresql://localhost:5432/redis_study username: postgres password: postgres jpa: open-in-view: false hibernate: ddl-auto: none show-sql: true mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true springdoc: swagger-ui: path: /docs 🚨 주의 : JPA나 MyBatis 의존성이 있는데 datasource 설정이 없으면 서버가 시작되지 않습니다 . ( Failed to configure a DataSource ) DB 설정은 처음부터 넣어 두세요. 7. 첫 번째 코드 (Hello World) com.study.redis 패키지에 HelloController.java 를 만듭니다. package com.study.redis; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController public class HelloController { @GetMapping("/") public Map<String, String> readRoot() { return Map.of("Hello", "World"); } } @RestController : 이 클래스의 메서드가 반환하는 값을 JSON으로 응답 합니다. @GetMapping("/") : 사용자가 / 로 GET 요청을 보내면 이 메서드를 실행합니다. (FastAPI의 @app.get("/") ) Map 을 반환하면 Spring(Jackson)이 JSON으로 바꿔줍니다. (파이썬의 dict 반환과 같음) 8. 서버 실행하기 방법 1. IntelliJ RedisPracticeApplication.java 의 main 옆 ▶ 버튼을 누릅니다. 방법 2. 터미널 ./gradlew bootRun # Windows: gradlew.bat bootRun uvicorn Spring Boot uvicorn main:app main() 실행 (톰캣이 내장되어 있음) 기본 포트 8000 기본 포트 8080 ( server.port 로 변경) --reload spring-boot-devtools 확인 방법 브라우저에서 http://localhost:8080 접속 {"Hello":"World"} 가 보이면 성공! 💡 IntelliJ에서 devtools 자동 재시작이 안 된다면: Settings → Build → Compiler → Build project automatically 체크, Advanced Settings → Allow auto-make to start even if developed application is currently running 체크
오늘 다나와에서 5070 Ti가 하루 +18.2%(195.9만→231.5만) 뛰면서, 중고 3090(216.8만)과 구매가 역전됐습니다. 여기에 전기세를 얹으면 판정이 또 갈립니다: 하루 8시간은 3년 총비용이 303.8만 vs 304.4만원으로 사실상 동률, 하루 24시간 에이전트 워크로드면 5070 Ti가 18만원 앞서나갑니다. 실거래가와 공식 TDP로 계산했습니다. 계산 전제 (먼저 명시) 구매가 : 2026-10-04 09:02 KST 다나와 스냅샷. 5070 Ti는 오늘 표본 4개(주말 얇은 표본)라 월요일에 되돌아올 수 있음 — 마지막에 민감도 계산 포함 전력 : 각 카드 공식 TDP, 유휴 대기 전력 별도 합산. 하루 8h=가동 8h+유휴 16h, 24h=연속 가동 전기요금 : kWh당 250원 . 4인 가구 최고 누진구간 + 부가세/전력기금 반영 실질 단가 가정. 심야 전력만 쓰면 130~150원까지 내려갑니다 환율 무관 : 원화 표본이라 1,347원 환율은 안 씁니다 카드별 3년 총비용 (구매가 + 전기) GPU 구매가(10/4) TDP 월 8h 3년 전기 8h 3년 총액 8h 3년 총액 24h RTX 5050 8GB 62.8만 130W 0.9만 32.9만 95.7만 148.2만 RTX 5060 8GB 70.8만 150W 1.1만 38.1만 108.9만 169.3만 RTX 5060 Ti 8GB 97.1만 180W 1.3만 46.0만 143.1만 215.4만 RTX 3060 12GB (중고) 53.9만 170W 1.2만 43.8만 97.7만 165.6만 RX 9070 XT 16GB 170.1만 304W 2.1만 75.3만 245.4만 369.8만 RTX 5070 12GB 164.9만 250W 1.7만 60.0만 224.9만 329.1만 RTX 5070 Ti 16GB 231.5만 300W 2.0만 72.3만 303.8만 428.6만 RTX 3090 24GB (중고) 216.8만 350W 2.4만 87.6만 304.4만 446.8만 RTX 5080 16GB 272.6만 360W 2.4만 86.7만 359.3만 509.1만 RTX 4090 24GB (중고) 380.0만 450W 3.0만 108.2만 488.2만 675.6만 판정이 갈리는 지점 3개 1. 5070 Ti vs 3090 (둘 다 "로컬 LLM 입성 카드" 양대산맥) 8시간 조건의 3년 총액: 303.8만 vs 304.4만 — 10만원 차, 사실상 동률. 5070 Ti의 신제품 프리미엄이 전력 efficiency(300W vs 350W)로 상쇄됩니다. 그런데 24시간 돌리면 5070 Ti가 18.2만 앞섭니다. overnight로 에이전트를 돌리는 사람이라면 5070 Ti, 하루 몰아서 쓰는 사람이라면 3090 — 24GB VRAM이 16GB보다 모델 수용 범위가 넓다는 건 별도 팩트입니다. 2. 중고 3060 12GB는 3년 총액 원탑 가성비 97.7만원. 오늘도 집계 7일 내내 53.9 54.0만원에 붙어 있는 그 카드입니다. 12GB는 Q4 12 14B급이 간신히 도는 최소 실사용 구간이고, 그 구간의 최저 매물이 3060입니다. 5060 Ti 8GB보다 3년 총액이 45만원 싸고 VRAM은 4GB 많습니다. 3. 4090 중고는 "전기 내면서 타는" 구간 450W × 24시간 = 3년 전기 108.2만원, 3년 총액 675.6만원. 5080 신제품(509.1만)보다 166만 비쌉니다. 24시간 워크로드에서 4090을 고를 이유는 VRAM 24GB × 성능 조합뿐이고, 그건 합리성이 아니라 수요의 문제입니다. 민감도 — 5070 Ti 오늘가가 주말 착시라면 9/27 다나와 중위가 199.1만으로 되돌아가면: 3년 총액 8h = 271.4만, 3090(304.4만)보다 33만 저렴 → 동률 판정이 뒤집힙니다. 반대로 급등이 진짜(AI 메모리 숏 파동, 어제 Shield TV Pro 50% 인상과 같은 흐름)라면 5070 Ti의 구매 타이밍은 지난 겁니다. 월요일 09:02 스냅샷에서 n이 15 안팎으로 돌아오는 게 확인 신호입니다. 월 운영비만 보면 최하위는 5050(월 0.9만원), 최상위는 4090(월 3.0만원) — 24시간 기준으론 월 2.4만원 vs 8.2만원으로 격차가 벌어집니다. "월 전기세 8만원"은 은근히 아픈 금액이라, 구매 전에 총액으로 사고하세요. 내 카드에 무슨 모델이 몇 tok/s로 도는지는 실측 표로 확인: https://daily-llm.com/vram-fit-matrix/
9/28(월) - 운영 지표를 화면에서 관찰하기 이번 주 초에는 게임 시스템에서 발생하는 요청을 단순히 처리하는 것에서 끝내지 않고, 실제 시스템이 어느 정도의 성능으로 동작하는지 확인하는 방법을 배웠다. 처리율, 평균 RTT, p95 같은 지표를 화면에서 확인할 수 있도록 구현하면서 운영 환경에서는 기능이 정상적으로 동작하는 것뿐만 아니라 얼마나 빠르고 안정적으로 처리되는지도 중요하다는 것을 알게 되었다. throughput = completed_requests / elapsed_time average_rtt = sum(rtt_values) / len(rtt_values) sorted_rtt = sorted(rtt_values) p95_index = int(len(sorted_rtt) * 0.95) p95 = sorted_rtt[p95_index] 특히 평균값만 보면 일부 느린 요청을 발견하기 어려울 수 있기 때문에 p95와 같은 지표를 함께 보는 이유도 이해할 수 있었다. 9/29(화) - Bronze 데이터 검증과 무결성 확인 Kafka에서 수집한 데이터를 Bronze 영역에 저장한 뒤, 파일이 제대로 저장되었는지 검증하는 과정을 진행했다. 이번에는 단순히 파일이 존재하는지만 확인하는 것이 아니라 bytes, SHA256, rows를 기준으로 데이터가 원본과 동일한지 확인했다. { "run_id": "capture-002", "observed": { "bytes": 28172873, "sha256": "fd07b340666f8fc2dcca17d143ae5c93c10c5a26fd82fb2c7691d7acbf019113", "rows": 33919 }, "checks": { "bytes": True, "sha256": True, "rows": True } } 세 가지 검증 결과가 모두 True 인 것을 확인했다. 이 과정을 통해 데이터 엔지니어링에서는 "파일을 저장했다"보다 "저장한 데이터가 원본과 동일하다는 것을 확인했다"가 더 중요하다는 것을 배웠다. 또한 Bronze 데이터를 바로 분석에 사용하는 것이 아니라 파싱해서 staging Parquet으로 만들고, 이후 Silver와 Gold 데이터로 발전시키는 전체적인 흐름도 조금씩 이해하기 시작했다. 9/30(수) - 중복 전달과 논리적 이벤트 구분 Kafka와 같은 메시지 시스템에서는 같은 데이터가 여러 번 전달될 가능성이 있기 때문에 물리적으로 전달된 횟수와 실제 사건의 개수를 구분해야 한다는 것을 배웠다. accepted = 3 logical = 2 duplicate_deliveries = accepted - logical print("duplicate_deliveries", duplicate_deliveries) 실행 결과는 다음과 같았다. accepted 3 logical 2 duplicate_deliveries 1 3개의 데이터가 들어왔다고 해서 실제 사건이 3개라는 의미는 아니다. event_id 를 기준으로 확인했을 때 실제 사건은 2개이고, 나머지 1개는 중복 전달일 수 있다는 것을 알게 되었다. 이전에는 데이터 개수만 맞으면 된다고 생각했는데 이번 수업을 통해 데이터의 개수뿐만 아니라 "같은 사건이 중복으로 들어온 것은 아닌가?"까지 확인해야 한다는 것을 배웠다. 10/1(목) - Parquet 압축 방식 비교 같은 데이터를 Parquet으로 저장하면서 Snappy와 Zstandard(zstd) 압축 방식을 비교했다. 처음 실행했을 때는 다음과 같은 결과가 나왔다. [ { "codec": "snappy", "rows": 33912, "write_seconds": 6.0283, "read_seconds": 1.4481 }, { "codec": "zstd", "rows": 33912, "write_seconds": 1.5716, "read_seconds": 0.4089 } ] 그런데 실행 순서를 변경해서 다시 측정했을 때는 결과가 달라졌다. [ { "codec": "zstd", "rows": 33912, "write_seconds": 6.3039, "read_seconds": 1.9649 }, { "codec": "snappy", "rows": 33912, "write_seconds": 1.3949, "read_seconds": 0.6079 } ] 이 결과를 통해 한 번 실행한 결과만 보고 "어떤 압축 방식이 무조건 더 빠르다"고 판단하면 안 된다는 것을 알게 되었다. 캐시, 디스크 상태, 실행 순서 등 여러 조건이 측정 결과에 영향을 줄 수 있기 때문에 같은 조건에서 여러 번 측정하고 비교해야 한다. 단순히 코드를 실행하는 것보다 실험 조건을 통제하고 결과를 해석하는 것이 중요하다는 점이 기억에 남았다. 10/2(금) - Django 인증과 MongoDB 데이터 상태 확인 Django 서버에서 요청을 처리하기 전에 로그인한 사용자인지 확인하는 인증 로직을 찾아보았다. if not request.user.is_authenticated: ... 프로젝트 안에서 인증 검사가 사용되는 위치를 직접 검색하면서 Django의 여러 기능이 결국 각각의 파일과 함수로 연결되어 있다는 것을 다시 확인할 수 있었다. 또한 MongoDB의 village_ads 데이터베이스와 campaigns 컬렉션 상태를 직접 확인했다. print(db.campaigns.count_documents({})) print({ "database": db.name, "campaigns": db.campaigns.count_documents({}) }) 결과는 다음과 같았다. 0 { 'database': 'village_ads', 'campaigns': 0 } 오류가 발생했다고 바로 코드를 수정하는 것이 아니라 현재 어떤 데이터베이스에 연결되어 있는지, 실제 데이터가 몇 건 존재하는지를 먼저 확인하는 과정이 중요하다는 것을 배웠다. 이번 주 전체적으로 이해한 흐름 이번 주에는 각각 다른 내용을 배운 것처럼 보였지만 전체적으로 연결해서 보면 하나의 흐름이 있었다. 데이터 발생 ↓ Kafka 등을 통해 데이터 전달 ↓ Bronze에 원본 데이터 저장 ↓ bytes / SHA256 / rows 검증 ↓ 파싱 및 Parquet 변환 ↓ event_id 기준 중복 확인 ↓ Snappy / Zstd 등 저장 방식 비교 ↓ Silver / Gold 등 분석 가능한 데이터로 가공 ↓ Django API에서 데이터 제공 ↓ 인증된 사용자가 조회 ↓ 처리율 / RTT / p95 등 운영 상태 관찰 예전에는 Kafka, Spark, Parquet, Django 같은 기술을 각각 따로 배우는 느낌이 강했다. 이번 주에는 데이터가 어디에서 만들어지고, 어떻게 전달되고, 어떻게 검증되고, 어떤 형태로 저장되고, 최종적으로 어떻게 서비스에서 사용되는지가 조금 더 연결되어 보이기 시작했다. 특히 데이터 엔지니어링에서는 데이터를 많이 처리하는 것만 중요한 것이 아니라 중복되지 않았는지, 원본이 손상되지 않았는지, 처리 결과를 다시 재현할 수 있는지 확인하는 과정이 중요하다는 것을 이해했다. Keep - 계속 유지하고 싶은 점 이번 주에도 결과만 보고 넘어가기보다는 이상한 결과가 나오면 원인을 확인하려고 했다. 특히 Snappy와 Zstd 비교에서 첫 번째 결과만 보고 결론을 내리지 않고 실행 순서를 바꿔 다시 측정해 본 것이 좋았다. 또한 MongoDB에서도 데이터가 없다는 결과가 나왔을 때 바로 코드를 수정하기보다 현재 DB 이름과 document 개수를 직접 확인했다. 최근 수업 내용이 점점 어려워지고 있지만 하나씩 출력값을 확인하면서 따라가면 전체 흐름은 이해할 수 있다는 것을 느끼고 있다. Problem - 어려웠던 점 이번 주에는 단순한 Python 문법보다 데이터 파이프라인과 운영 관점의 개념이 많이 등장해서 어려웠다. Bronze, staging, Silver, Gold처럼 데이터가 단계별로 나뉘는 이유와 event_id , 중복 전달, SHA256 같은 개념을 처음에는 각각 따로 이해하려고 해서 복잡하게 느껴졌다. 또한 성능 측정 결과가 실행할 때마다 달라질 수 있다는 점도 처음에는 혼란스러웠다. 코드가 정상 실행됐다고 해서 그 결과가 항상 정확한 결론을 의미하는 것은 아니라는 점이 이번 주에 가장 어려우면서도 중요한 부분이었다. Try - 다음 주에 보완할 점 다음 주에는 코드를 실행하기 전에 이 코드가 전체 파이프라인에서 어떤 역할을 하는지 먼저 생각해보려고 한다. 특히 다음 질문을 스스로 해보는 습관을 만들고 싶다. 1. 이 데이터는 어디에서 왔는가? 2. 지금 어떤 단계의 데이터인가? 3. 중복이나 손상 여부는 어떻게 확인하는가? 4. 이 작업의 결과는 어디에 저장되는가? 5. 다음 단계에서는 이 데이터를 어떻게 사용하는가? Kafka, Spark, Bronze, Silver, Gold, Django를 각각 외우기보다는 데이터 하나가 처음 발생해서 최종 화면에 나타날 때까지 이동하는 과정을 기준으로 복습할 예정이다. 한 주를 마치며.. 이번 주는 단순히 데이터를 처리하는 방법보다 "이 데이터를 믿어도 되는가?"를 확인하는 방법을 많이 배운 한 주였다. 처음에는 bytes, SHA256, 중복 전달, 압축 방식 비교 같은 내용이 왜 필요한지 잘 이해되지 않았지만 전체 흐름으로 연결해보니 실제 서비스를 운영하려면 반드시 필요한 과정이라는 것을 알게 되었다. 특히 이번 주를 지나면서 코드를 작성하는 것만큼 결과를 검증하고 이상한 부분의 원인을 찾아가는 과정이 중요하다는 것을 다시 느꼈다. 아직 모든 코드를 혼자 처음부터 작성할 수 있는 수준은 아니지만, 출력 결과를 보고 현재 어떤 단계인지 파악하고 문제가 생겼을 때 어디부터 확인해야 하는지는 전보다 많이 익숙해진 것 같다. 다음 주에는 개별 기술을 외우는 것보다 전체 데이터 흐름 안에서 각 기술이 어떤 역할을 담당하는지 더 확실하게 이해하는 것을 목표로 하고 싶다.
이번 주 다나와에서 가장 크게 오른 건 5090이 아니라 5060 Ti(+22.6%)와 5070(+19.4%)이었습니다. 그리고 집계된 7일 내내 단 한 푼도 안 움직인 카드가 하나 있습니다 — 중고 3060 12GB, 53.9만원. 그런데 오늘 아침 표에는 5090과 4090 행이 아예 사라졌습니다. 이게 무슨 뜻인지 숫자로 설명합니다. 먼저, 오늘 표에서 5090이 사라진 이유 우리는 매일 09:00 KST에 다나와 실거래를 스냅샷합니다. 주말에는 판매점이 접힙니다. 5070의 표본 수(n)로 보면 분명합니다. 날짜 5070 표본(n) 소스 10/1 (목) 15 다나와+searx 10/2 (금) 14 다나와+searx 10/3 (토) 11 searx 위주 10/4 (일) 2 다나와만 5090 32GB는 오늘 표본 0개 — 870만원 넘는 카드를 주말에 파는 곳이 없다는 뜻입니다. 중고 4090/3090도 오늘 집계 없음. 주말 중위가는 "가격"이 아니라 "아직 남은 재고의 가격"입니다. 그래서 이번 주 변동을 계산할 때는 소스를 다나와로 고정하고, 양쪽 끝 날짜 모두 다나와 표본이 잡히는 카드만 비교에 넣었습니다. 표본 소스가 섞이는 날의 "하루 +15.8%" 류 숫자가 부풀려질 수 있다는 걸 아는 게, 단 하루치 표를 보는 것보다 중요합니다. 주간 변동 (9/27 → 10/4, 다나와 동소스 중위가) GPU 9/27 10/4 7일 변동 RTX 5060 Ti 8GB 79.2만 97.1만 +22.6% RTX 5070 12GB 138.0만 164.9만 +19.4% RTX 5070 Ti 16GB 199.1만 231.5만 +16.3% RTX 5050 8GB 57.2만 62.8만 +9.8% RTX 5080 16GB 251.4만 272.6만 +8.4% RTX 5060 8GB 73.3만 70.8만 -3.4% RTX 3060 12GB 53.9만 53.9만 0.0% 참고: 5090 32GB는 870.0만(9/27) → 929.9만(10/3)으로 +6.9% 후 오늘 표본 소멸. 최저가 기준 5070은 131.9만 → 155.9만(+18.2%), 5080은 237.4만 → 261.6만(+10.2%)으로 중위가와 같은 방향입니다. 스토리는 하나: 메모리 대란이 중간 가격대를 찍고 있다 미국에서는 어제 7년 된 Nvidia Shield TV Pro가 $199→$299로 50% 인상됐습니다(Tom's Hardware, TechPowerUp 10/3). 원인은 AI 메모리 숏이지 성능이 아닙니다. 같은 파도가 한국 다나와에서 12~16GB 급속형 카드에 먼저 닿고 있습니다. 5070 Ti는 하루 +18.2%(195.9→231.5만)를 오늘 찍었고, 이건 표본이 4개로 얇지만 9/27 대비 +16.3%라는 같은 방향의 확인입니다. 그런데 중고 3060 12GB만 집계된 7일 전부 53.9~54.0만원에 딱 붙어 있습니다. 최저가는 오늘 41.0만원까지 나왔습니다. 이유를 데이터에서 추론하면: 신제품(5070 Ti/5080) 쪽 자금이 몰릴 때, 2021년 출시·이미 단종된 카드가는 대란 채굴 물량 탓에 수요와 무관한 재고가 남아 있음 로컬 LLM 입장에서 12GB는 Q4 12~14B가 도는 최소 실사용 구간 — 그 구간의 최저 매물이 곧 3060 54만원 = 오늘 시세로 16GB 중위(RX 9070 XT 170.1만)의 3분의 1 추정 명기: "재고 풍부"은 가설이고, 확정은 아닙니다. 다만 표본 4개인 41.0만원 매물은 마지막 구매 시도 전까지true로 간주하세요. 3060을 노린다면 주말→월요일 오전 갱신(09:02 KST)에서 n이 6개로 돌아올 때 표본을 다시 보는 게 맞습니다. 그래서 언제 사야 하나 5070/5070 Ti 급등분은 주말 착시 가능성이 있습니다 — 표본 2 4개의 중위가는 월요일 n이 11 17개로 돌아오면 회귀합니다. 우리가 9/29에 5070 Ti 최저 157.9만(searx)을 기록한 적이 있어, 진짜 바닥은 아직 어딘가에 있습니다. 5060 Ti +22.6%는 소스 고정 비교라 진짜입니다 — 이건 되돌아올 가능성이 낮습니다. 3060은 지금이 평상시입니다. 집계된 7일 내내 안 움직였으니, 평소 가성비 기준대로 사면 됩니다. 모델이 내 그래픽카드에 돌아가는지만 확인하려면 인터랙티브 표를 쓰세요: https://daily-llm.com/vram-fit-matrix/ (실측 tok/s·VRAM 기준 fits/tight/offload 판정)