神腦外部伺服器遭自動化攻擊,部分訂單資訊遭擷取
iThome 新聞
臺灣通訊產品及3C商品通路公司神腦國際(2450)於9月29日,在股市公開資訊觀測站發布重大訊息,表示他們的外部伺服器遭受自動化攻擊,攻擊者對部分訂單資訊進行擷取,該公司獲報後啟動應變會議,採取必要措施降低影響,初步評估對營運、財務,以及服務提供無重大影響。
Score: 57.09Confidence: 54%
View offerLoading the catalog…
THE AI OPPORTUNITY INDEX
Find your next AI tool. Explore free access, trials, and credits — all in one place.
iThome 新聞
臺灣通訊產品及3C商品通路公司神腦國際(2450)於9月29日,在股市公開資訊觀測站發布重大訊息,表示他們的外部伺服器遭受自動化攻擊,攻擊者對部分訂單資訊進行擷取,該公司獲報後啟動應變會議,採取必要措施降低影響,初步評估對營運、財務,以及服務提供無重大影響。
Score: 57.09Confidence: 54%
View offeriThome 新聞
美國總統川普(Donald Trump)周二(9/29)簽署行政命令,正式要求美國行政部門以「超級智慧」(Super Intelligence,SI)取代「人工智慧」(Artificial Intelligence,AI),未來政府機構的官方文件、公開溝通、網站、報告及政策文件,都應改用SI一詞。
Score: 57.09Confidence: 54%
View offerZennの「AI」のフィード
Claude Code でサイトを100ページ以上公開して1か月。売上は0円でした。複数のエージェントが同じリポジトリで動く日の回し方、30秒差でデプロイを上書きされた話、数えていなかったクリックと二重に数えていたクリック、PowerShell からの文字化け、そして売上0円の理由を、実際の数字とコードで書いた運用記録です。『Claude Codeでサイト・アプリを作ろう』の続編。
Score: 57.09Confidence: 54%
View offer掘金
让 AI 改接口,它常顺手把可选参数改成必填、改字段类型、删字段,服务端测试全过,调用方却出错。这篇用 oasdiff 实测。
Score: 57.09Confidence: 54%
View offeriThome 新聞
9月29日Google發布電腦版與Android版Chrome更新,總共修補32個資安漏洞,從嚴重程度來看,1個重大等級、25個為高風險等級,其餘為中度或低風險等級。
Score: 57.08Confidence: 54%
View offeriThome 新聞
9月29日遊戲軟體公司鈊象(3293)於股市公開資訊觀測站發布重大訊息,他們偵測到有不明IP位址異常入侵的活動,目標是該公司的營運資訊設備,資安團隊察覺後隨即啟動資安應變機制,根據初步評估,本起事故對公司營運無重大影響。鈊象並未進一步透露遭異常侵入的設備資訊、入侵方式,以及是否有資料遭竊取或系統遭破壞。
Score: 57.08Confidence: 54%
View offer人人都是产品经理
Zeta 在日本单月收入从 370 万美元涨到近 800 万美元,接近日本一线手游,约为 Character.AI 公开年化收入的三倍。韩国 Scatter Lab 用 300 万个用户创建的 AI 角色,把日本 AI 社交做成完整生意。 你能想象吗? 在全球“年轻人最不爱谈恋爱”的国家,年轻人却在疯狂给AI伴侣氪金。 它就是日本。 2026年5月,Zeta在日本单月收入大约370万美元。两个月之后,这个数字已经逼近800万美元,整整翻了一倍。这个收入规模,几乎相当于日本国内的一线手游。 如果简单按照800万美元来说,日本一个市场对应的年收入接近9600万美元,这已经是AI社交老大Character.AI公开年化收入的3倍左右。 最夸张的是,Zeta的成功并不是个例。 韩国Wrtn推出的Kyarapu,8月单月收入已经达到百万美元级;LoveyDovey、Saylo这样的腰部产品,即使热度回落,在日本一个市场每个月依然能做出六七万美元流水。 与此同时,Aily、XenoChat、anini、Mellows、mimel等一批新产品还在不断往里挤。 头部能赚大钱,腰部能活下来,新玩家还愿意继续进场,AI社交第一次真正长出了一套完整的商业生态。 这就很反常识了。日本年轻人连真人恋爱都不想谈了,为什么反而愿意给AI恋人花钱? 今天,硅基君就来扒一扒,日本到底是怎么把AI社交这门生意跑通的。 01 日本年轻人给疯狂AI陪伴打钱 今年日本娱乐App里,冒出来一个挺反常识的产品,叫Zeta。 这是韩国AI公司Scatter Lab做的一款AI角色聊天产品。你可以把它理解成一个AI版角色社区,平台上有超过300万个用户创建的AI角色,涵盖恋爱、乙女、BL、校园、异世界等题材。 用户既可以直接和喜欢的角色聊天,也可以自己设定人物性格、背景和说话方式,创建一个新的AI角色。 Zeta角色及剧情设定页面 听起来,其实很像过去那些AI陪伴产品。下载量看上去也平平无奇,从5月开始,基本都维持在30万左右。 别看下载数据平平,Zeta收入却涨得飞起。 AppMagic数据显示,2026年5月,Zeta在日本单月收入大约370万美元。到了7月,已经接近800万美元。 两个月,就翻了整整一倍多。这个成绩,在日本可以说是相当夸张了。 要知道,今年日本暑期档收入第一的游戏收入第一的《Fate/Grand Order》,两个月也就1800万美元。也就是说,Zeta的收入已经能够和日本一线手游平起平坐了。 日本App分析公司Fuller的数据也能说明这一点。2026年第二季度,Zeta已经进入日本非游戏App收入Top 15。排在它前面的,很多都是ChatGPT、Spotify这种全球超级应用。 这样的表现,直接打破了业内对AI陪伴产品“只赚吆喝不赚钱”的刻板印象 。 最典型的就是Character.AI,月活大约2000万,但过去公开的公司年化收入大概只有3000万美元。 而Zeta的用户规模远小于Character.AI,仅日本一个市场,7月单月收入就已经接近800万美元。 如果简单按照800万美元一个月年化,日本一个市场对应的年收入接近9600万美元,已经是Character.AI此前公开年化收入的3倍左右。 更夸张的是,在日本,AI陪伴赚钱还不是个例 。 除了Zeta,韩国公司Wrtn面向日本推出的Kyarapu,2026年8月单月收入已经做到百万美元级。 Kyarapu产品界面 Wrtn披露,旗下日韩两款故事型AI产品用户日均使用时长大约2小时,付费用户留存超过70%。 韩国TainAI推出的LoveyDovey、中国元象旗下Saylo,都曾冲进日本娱乐下载榜前十。即使热度过去,到2026年5月,两款产品在日本依然能稳定做到每月6万至8万美元流水。 甚至还有越来越多公司专门跑来日本做AI社交。2026年,从AI恋人、角色社区到实时生成剧情,Aily、XenoChat、anini、Mellows、mimel接连出现。 已经积累15万Web用户的加拿大AI陪伴产品Yapo,也专门给日本做了一套本地化版本。 这就是日本最特殊的地方。当全球AI社交还在赌下一款爆款时,日本的年轻用户,已经开始稳定地为“AI关系”买单了。 02 不谈恋爱的日本人,没有停止消费爱情 为什么为“AI关系”买单的偏偏是日本? 要回到这个问题,我们必须认识到日本年轻人里蔓延着一个普遍趋势: 日本年轻人越来越不想进入真实恋爱,但他们从来没有停止为“喜欢一个人”花钱 。 过去几十年,日本的婚恋市场发生了一次非常剧烈的变化。战后高速工业化时期,日本形成过一套非常稳定的家庭模型。男人进入公司,成为终身雇佣的正式员工,女人更多回归家庭。 男人挣钱,女人持家。在当时看来,婚姻更像一种稳定的社会分工。 但随着经济泡沫的破裂,这套模式慢慢撑不住了。 工作越来越不稳定,一个人的工资越来越难支撑整个家庭,女性大量进入职场,家务、育儿这些传统家庭责任却没有同步消失。 于是,找对象突然从“分工”变成了一场复杂的“匹配”。 男人开始看女性有没有收入。女性除了看男性挣多少钱,也开始看他愿不愿意做家务、带孩子。 日本国立社会保障人口问题研究所调查显示,2025年,55.6%的未婚男性会考虑女性的经济能力;68.4%的未婚女性会考虑男性承担家务和育儿的能力。 结果就是,越来越多年轻人干脆退出婚姻。 1972年,日本正处在结婚潮的顶峰。那一年有近110万对新人结婚,平均每1000人口对应10.4对婚姻。到了2025年,日本一年只剩48.9万对新人结婚,婚姻率降到4.1‰,只剩当年的四成左右。 当然,婚姻退潮在欧美等很多发达国家都在发生。但不同的是,当发生婚姻退潮后,政府往往会推动政策和福利制度不断为同居、非婚伴侣、非婚生育补上制度位置。 法国就是一个很典型的例子。上世纪70年代以后,法国结婚率也一路往下掉。但1999年,法国推出PACS,为不愿结婚的伴侣提供税收、财产、住房和社会权利。 于是,婚姻虽然少了,亲密关系并没有同步消失。上世纪60年代,法国97%的伴侣还是夫妻;到了2022年,已经有接近三成伴侣生活在婚姻之外。 而日本则走了另一条路。 由于其税收、社保、户籍和家庭权利长期围绕婚姻组织起来,因此当婚姻越来越难进入,婚姻之外又没有形成足够成熟的替代关系。 最后,很多年轻人干脆连恋爱一起退了。 2021年,日本18—34岁未婚人群中,正在同居的只有约3%。到2025年,未婚男性中有恋人的只剩19.6%,女性也只有27.8%。 也就是说,日本人退出的,已经不只是婚姻。甚至夸张点说,连恋爱本身,都开始变成一件“可以不要”的东西。 但最有意思的地方来了。 虽然日本人不喜欢谈恋爱,但确实全世界最擅长把亲密关系里的需求做成商品的的国家之一 。 婚恋退潮,不代表欲望和情感需求一起消失。 于是,日本把它们拆了出来。性需求,用一套庞大的成人产业承接;情感需求,则被游戏、动漫、偶像和角色经济一点点接走。 上世纪90年代,《心跳回忆》《安琪莉可》这些恋爱模拟游戏开始大量出现,“心动”被做成了内容。日本年轻人不需要进入一段真实关系,也能完整经历认识、接触、追求、告白和恋爱。 到了2000年代,这种关系开始从游戏剧情里走进日常。《LovePlus》让虚拟女友变成一个长期陪伴对象。初音未来这样的虚拟偶像,则让“长期喜欢一个虚拟角色”变得越来越正常。 再后来,角色手游、VTuber和“推活”(围绕自己喜欢、支持的对象做的一整套消费和情感投入)又开始贩卖“长期关系”。更新、直播、生日、周年、限定周边,让用户可以围绕同一个角色持续投入很多年。 从心动,到陪伴,再到长期关系,日本把一段恋爱里的情感体验,一层层拆成了可以反复消费的内容。 而且这早就不是一个小众亚文化。2025年,矢野经济研究所调查显示,日本15—69岁人群中,58.6%都有自己喜欢的角色。15—19岁人群更是超过七成。 另一项调查估算,日本大约有1400万人参与“推活”,平均每人一年花费约25万日元,对应潜在消费规模达到3.5万亿日元,相当于日本全年零售额的2.1%。 一边,真人恋爱越来越难。另一边,日本用户早已经习惯把钱花在虚拟角色、虚拟关系和情绪价值上。 AI社交真正进入日本的时候,其实几乎不用重新教育用户。它只不过就是接过了一个已经存在几十年的消费习惯 。 03 AI恋人终于长成了日本人熟悉的样子 有意思的是,AI社交在日本的发展,也不是一帆风顺的。 2025年,日本其实已经出现过一批AI陪伴产品。LINE推出AI Friends,曾经做陪伴机器人的夏普也推出过Poketomo,AINE、Prizm Chat等AI社交产品。 但在当时,真正跑出来的产品几乎没有。 原因很简单,这些产品的核心体验依然没有跳出“陪聊”。 简单来说,就是选一个AI角色,然后聊天。 比如,Poketomo会记住用户说过什么、去过哪里,陪用户聊天、安慰情绪。Prizm Chat也加入了恋爱角色、亲密度和情境卡,核心玩法依然是不断发消息。 但光有聊天显然不够。 到了这一轮,AI社交产品开始明显换了一种形态 。 最早的Zeta,本质上也是一个AI角色聊天产品,但如今它已经开始把“角色”改造成“故事”。一个内容里可以同时存在完整世界观、多个角色、人物关系和背景设定。 用户进去之后,也不再只是围着某个角色聊天,你会真正进入那个世界里,官方后来干脆把原来的“角色”统一改名为“故事”,产品定位也从“AI角色聊天服务”,变成了“AI故事平台”。 Kyarapu更彻底。它从第一天开始卖的就是互动故事。 用户进入一个世界,成为主角,每一次选择,都会实时生成新的小说、图片和声音。剧情会继续往前,人物关系也会随时变化。 AI社交到这里,玩法已经彻底变了,它开始越来越像一款永远写不完的恋爱游戏。 过去的乙女游戏和恋爱模拟,内容全部由制作团队提前写好。再大的游戏,也终究会打完。AI第一次把这个上限拿掉了,剧情可以一直生成,角色可以永远陪着你。 更重要的是,同一个角色面对不同用户,可以长出完全不同的关系。 而这个变化,恰好撞上了日本用户最熟悉的消费方式。 日本过去几十年的恋爱游戏、角色手游和“推活”,一直在训练用户为三样东西付费: 角色、剧情和关系进度 。 这一下,日本用户熟悉的那套消费逻辑,全部接上了。角色,让用户喜欢上它;剧情,让用户不断回来;关系进度,让用户越来越舍不得离开。 AI再把内容生产的边际成本压下去,让这段关系几乎可以无限延长。于是,一个以前很难成立的商业模式,突然开始跑起来:让用户为一段持续生长的虚拟关系付钱。 这可能也是理解日本AI社交最重要的一点。日本人并没有突然爱上AI,他们只是早就习惯了喜欢一个虚拟角色、追一段虚拟关系、给一份虚拟感情持续花钱。 与其说,日本人更愿意为AI社交产品买单,倒不如说AI社交产品,终于长成了日本人最熟悉的样子 。 撰文:元元 本文由人人都是产品经理作者【硅基观察Pro】,微信公众号:【硅基观察Pro】,原创/授权 发布于人人都是产品经理,未经许可,禁止转载 题图来自Unsplash,基于 CC0 协议
InfoQ - 促进软件开发领域知识与创新的传播
点击查看原文>
Score: 56.77Confidence: 54%
View offervelog
아침에 눈을 떴는데 슬랙과 로그에 찍힌 섬뜩한 메시지: *"치명적: 헬스체크 실패로 인한 자동 롤백까지 실패했습니다. 서비스 중단 위험!"* 심장이 철렁해서 급히 EC2 운영 서버를 확인했더니... 서비스는 1시간 전부터 아무 문제 없이 평온하게 200 OK를 뱉고 있었습니다. 멀쩡한 새 버전 배포를 '실패'로 오판하고 불필요한 롤백 지옥에 빠뜨렸던 범인, macOS 배터리 모드의 'Power Nap'과 장기 실행 SSH 파이프의 함정 을 파헤친 기록입니다. 🚨 1. 증상: 아침을 공포로 몰아넣은 새벽 배포 로그 새벽 3시 자동 배포 파이프라인의 로그( allteachers-nightly-deploy.log )를 열어보았을 때 등골이 서늘해졌습니다: [03:15:22] 새 버전(v1.0.72) 배포 완료, 헬스체크 대기 중... [03:16:10] Connection to ec2-... broken pipe [03:16:10] ❌ 헬스체크 실패! 자동 롤백을 시도합니다... [03:16:45] 💥 치명적: 롤백 스크립트 실행 중 응답 없음. 서비스가 중단된 상태일 수 있습니다! 즉시 서버 터미널을 열고 상태를 확인했습니다: $ sudo systemctl status my-app ● my-app.service - Backend Service Active: active (running) since Thu 2026-09-17 03:15:30 KST; 4h ago Main PID: 12489 (java) 실제 서버 상태 : v1.0.72 버전이 배포 직후부터 4시간 내내 CPU를 정상 점유하며 완벽하게 떠 있었음. 진실 : 서버는 너무나 잘 배포되었는데, 로컬 배포 스크립트가 혼자 "서버 죽었다!"라고 착각(오탐) 해서 멀쩡한 배포를 롤백시키려 난리를 쳤던 것입니다. 🔍 2. 원인 분석: caffeinate 를 켰는데 왜 맥북이 잠들었을까? 이미 이전 트러블슈팅을 통해 배포 도중 맥북이 잠들지 않도록 caffeinate -i 명령어를 스크립트에 물려둔 상태였습니다. 그런데 왜 새벽에 연결이 끊겼을까요? pmset -g log 로 새벽 03시~05시 사이의 맥북 전원 시스템 로그를 까보았습니다: 2026-09-17 03:16:02 +0900 DarkWake DarkWake [CDN] due to EC.PowerNap/Maintenance: Using Batt (Charge:62%) 2026-09-17 03:16:12 +0900 Sleep Entering Sleep state due to 'Maintenance Sleep': Using Batt 범인은 배터리 모드의 'Power Nap (Maintenance Sleep)' caffeinate -i 의 맹점 : 사용자의 입력 유휴(Idle)로 인한 슬립만 막을 뿐, macOS 시스템이 주기적으로 도는 '유지보수 슬립/파워냅(Power Nap) 사이클' 은 막아주지 못합니다. 특히 맥북이 배터리( Using Batt )로 구동 중 일 때는 전력 절약을 위해 15분 주기로 찰나의 순간 잠들었다 깨어나는 동작을 반복합니다. 더 치명적이었던 배포 스크립트의 구조적 결함 당시 배포 스크립트의 헬스체크 로직은 다음과 같았습니다: # ❌ 기존 위험한 구조: 단 하나의 SSH 세션 안에서 150초 동안 루프 돌기 ssh -i $KEY $HOST " for i in {1..30}; do curl -sf http://localhost:8080/health && exit 0 sleep 5 done exit 1 " 헬스체크를 위해 단 하나의 SSH 연결을 최대 150초 동안 열어두고 원격 루프 를 돌렸습니다. 그 150초 사이에 맥북이 파워냅 주기로 0.5초만 네트워크를 끊어도, SSH는 즉시 Broken pipe 를 뱉고 튕겨 나옵니다. 스크립트는 이 Broken pipe 에러 코드를 보고 "헬스체크 30번 다 돌았는데 서버가 안 떴구나!"라고 오판 하여 즉시 롤백 루틴을 발동시킨 것입니다. 🛠️ 3. 해결책: "안 끊기게" 하지 말고 "끊겨도 다시 시도하게" 네트워크와 로컬 전원 상태는 언제든 튈 수 있습니다. 핵심은 단일 SSH 세션에 의존하지 않는 멱등한 재시도 구조 로 전환하는 것이었습니다. 1 헬스체크를 '개별 독립 SSH 요청'으로 분리 # 🚀 개선된 구조: 매 시도마다 새로운 독립 SSH 세션 생성 restart_and_wait_healthy() { local max_retries=30 local retry_count=0 echo "🚀 서비스 재시작 후 헬스체크 시작..." while [ $retry_count -lt $max_retries ]; do # 짧은 타임아웃을 가진 1회성 단발 SSH 호출 if ssh -o ConnectTimeout=10 -o BatchMode=yes $SSH_TARGET "curl -sf http://localhost:8080/health" > /dev/null 2>&1; then echo "✅ 헬스체크 통과! 정상 서비스 중입니다." return 0 fi retry_count=$((retry_count + 1)) echo "⏳ 헬스체크 대기 중... ($retry_count/$max_retries)" sleep 5 done echo "❌ 헬스체크 최종 실패" return 1 } 이제 헬스체크 도중 와이파이나 맥북 전원이 1~2초 끊겨도, 해당 1회의 핑만 실패할 뿐 다음 5초 뒤 루프에서 새로운 SSH 연결을 맺고 재검증 합니다. 특정 원인(Power Nap이든, 공유기 재부팅이든)에 구애받지 않는 강력한 내성을 확보했습니다. 2 SSH 접속 자체 타임아웃 명시 ( ConnectTimeout=10 ) 네트워크 순단 시 SSH 클라이언트가 무한정 멈추지 않고 10초 만에 깨끗하게 빠져나와 다음 시도로 넘어가도록 강제했습니다. 3 배포 시작 전 배터리 전원 상태 경고 로그 if pmset -g batt | grep -q "InternalBattery"; then echo "⚠️ 경고: 맥북이 배터리로 구동 중입니다. 슬립 방지를 위해 전원 어댑터 연결을 권장합니다." fi 💡 이번 트러블슈팅을 통해 얻은 교훈 로그를 맹신하지 말고 '실제 원격 상태'를 교차 검증하라 : 배포 모니터링 스크립트 자체가 버그를 일으킬 수 있습니다. 롤백 에러가 떴다고 당황하지 말고 서버의 systemd 저널과 실제 JAR 체크섬을 확인하는 습관이 중요합니다. 긴 세션 하나보다 짧은 세션 여러 개가 낫다 : 무인 자동화 환경에서 하나의 원격 커넥션을 오래 물고 있는 것은 시한폭탄입니다. 매 시도를 분리하고 멱등하게 만드세요. "안 끊기게 막는 것"보다 "끊겨도 다시 이어붙이는 설계"가 언제나 승리합니다.
velog
"탭 A에서는 관리자 계정으로 작업 중인데, 탭 B에서 테스트용 학생 계정으로 로그인했더니 탭 A의 화면이 관리자 권한 그대로 남아있다?!" XSS 방어를 위해 Access Token은 탭 메모리 에 두고, Refresh Token은 HttpOnly 쿠키 로 관리하는 모던 웹 인증 아키텍처에서 필연적으로 마주치는 '멀티 탭 세션 불일치' 와 '토큰 회전(RTR) 동시성 경합' 을 해결한 실전 트러블슈팅 기록입니다. 🚨 1. 증상: 탭 사이에서 엇갈리는 계정의 유령 사용자(특히 관리자/매니저)가 여러 탭을 띄워두고 작업할 때 다음과 같은 기묘하고 위험한 현상이 발견되었습니다: [시나리오] 1. 탭 A: '관리자'로 로그인 ➡️ 관리자 전용 대시보드 조회 중 2. 탭 B: 테스트를 위해 로그아웃 후 '일반 학생' 계정으로 로그인 3. 결과: 탭 A는 여전히 관리자 화면과 권한이 그대로 유지됨! 더 심각한 문제는 Access Token 만료 시점(30분 뒤) 에 터졌습니다. 탭 A의 메모리에 있던 옛 Access Token이 만료되자, 백그라운드에서 /auth/refresh 를 호출합니다. 이때 브라우저는 공유 쿠키에 들어있는 '학생 계정의 Refresh Token' 을 서버로 실어 보냅니다. 그 결과, 탭 A의 화면은 여전히 '관리자' 화면인데, API 요청은 '학생' 토큰으로 나가며 권한 오류(403)가 터지거나 화면과 실제 요청 주체가 뒤죽박죽 꼬이는 현상이 발생했습니다. 🔍 2. 근본 원인: 메모리 토큰과 공유 쿠키의 태생적 간극 보안을 위해 설계한 아키텍처 자체가 원인이었습니다: Access Token & User State : 각 탭의 독립된 JS 메모리( AuthContext ) 에만 존재합니다. Refresh Token : 브라우저의 모든 탭이 공유하는 HttpOnly 쿠키 에 존재합니다. 탭 B에서 로그인/로그아웃을 해도 탭 A는 이 사실을 전혀 전달받지 못하므로, 최대 30분 동안 옛날 계정의 신분(Identity)을 붙잡고 있었던 것입니다. ⚠️ 또 다른 복병: Refresh Token Rotation (RTR) 동시성 폭탄 우리 백엔드는 보안을 위해 토큰 갱신 시마다 리프레시 토큰을 교체(Rotate)하고, 이미 사용된 옛날 토큰이 들어오면 '토큰 탈취'로 간주하여 사용자의 모든 세션을 강제 파기 하는 정책을 가지고 있었습니다. 만약 여러 탭이 동시에 켜진 상태에서 *"다른 탭 로그인 시 모든 탭이 각자 refresh를 호출하게 하자"*라고 단순하게 접근하면: 탭 A, B, C가 동시에 /auth/refresh 를 호출 ➡️ 찰나의 차이로 늦게 도착한 요청이 이전 토큰을 들고 감 ➡️ 서버가 탈취로 오인해 모든 탭을 강제 로그아웃시키는 참사 로 이어집니다. 🛠️ 3. 해결 방안: 브라우저 최신 표준 API 2가지의 조합 서버 코드는 단 한 줄도 건드리지 않고, 브라우저 표준 Web API인 BroadcastChannel 과 Web Locks API 를 도입하여 문제를 깔끔하게 종결지었습니다. 1 BroadcastChannel 로 탭 간 실시간 신분 동기화 AuthContext 에 브로드캐스트 채널을 연결하여, 로그인/로그아웃/회원전환이 일어날 때마다 모든 탭에 확성기를 켭니다: // frontend/src/contexts/AuthContext.tsx const authChannel = new BroadcastChannel('auth-sync-channel'); export const AuthProvider = ({ children }) => { // 1. 로그인/로그아웃 시 다른 탭으로 전파 const login = (token: string, user: User) => { setAccessToken(token); setUser(user); authChannel.postMessage({ type: 'LOGIN', accessToken: token, user }); }; const logout = () => { resetAuthState(); authChannel.postMessage({ type: 'LOGOUT' }); }; // 2. 다른 탭에서 날아온 메시지 수신 useEffect(() => { authChannel.onmessage = (event) => { const { type, accessToken, user } = event.data; if (type === 'LOGIN') { // 다른 탭이 각자 refresh를 때리지 않고, 받은 최신 토큰/유저로 즉시 교체! setAccessToken(accessToken); setUser(user); } else if (type === 'LOGOUT') { resetAuthState(); } }; }, []); }; 핵심 포인트 : 다른 탭이 직접 서버에 refresh 를 요청하게 만들지 않고, 로그인 성공 탭이 이미 발급받은 새 토큰과 유저 정보를 그대로 복사 전달 함으로써 불필요한 네트워크 요청과 RTR 충돌을 원천 차단했습니다. 2 Web Locks API ( navigator.locks )로 탭 간 토큰 갱신 직렬화 여러 탭이 동시에 켜져 있다가 거의 같은 시점에 Access Token이 만료될 때를 대비해, 갱신 요청을 브라우저 탭들 사이에서 Mutex(상호 배제) 로 묶어주었습니다: // frontend/src/api/apiClient.ts export async function refreshAccessToken(): Promise<string> { // Web Locks API가 지원되는 최신 브라우저 환경 if (navigator.locks) { return await navigator.locks.request('auth-token-refresh-lock', async () => { // 락(Lock)을 획득한 딱 하나의 탭만 네트워크 요청을 보냄 // 이미 다른 탭이 갱신을 끝냈다면 최신 쿠키를 들고 안전하게 진입 return await executeRefreshRequest(); }); } // 미지원 구형 브라우저 폴백 return await executeRefreshRequest(); } 🧪 4. 검증 결과 멀티 탭 동기화 테스트 : 탭 A(관리자 대시보드) 열어둔 상태에서 탭 B에서 학생 로그인 ➡️ 탭 A가 새로고침 없이 즉시 학생 권한으로 동기화되며 관리자 메뉴 자동 소멸 . 탭 B에서 로그아웃 ➡️ 탭 A 즉시 로그인 안내 페이지로 전환 (새로고침 불필요) . 토큰 갱신 레이스 컨디션 테스트 : 5개 탭을 동시에 띄워두고 토큰 만료 시뮬레이션 ➡️ Web Locks 덕분에 1개 탭만 직렬로 갱신을 수행하고, RTR 세션 파기 오류 발생률 0건 달성. 💡 이번 트러블슈팅을 통해 얻은 교훈 메모리 저장소의 맹점 : SPA에서 Access Token을 변수(메모리)에 저장하는 것은 XSS 방어에 훌륭하지만, 멀티 탭 상태 불일치라는 부작용을 동반합니다. 현대 브라우저 API를 적극 활용하라 : 예전에는 localStorage 의 storage 이벤트 꼼수를 썼지만, 지금은 BroadcastChannel 과 navigator.locks 라는 훨씬 우아하고 표준적인 해결책이 존재합니다. 보안(RTR)과 사용자 경험(멀티탭)은 상충하기 쉽지만, 프론트엔드 단계의 세밀한 동기화 설계로 두 마리 토끼를 모두 잡을 수 있습니다.
velog
Multi-Agent Workflow에서는 여러 Agent가 각자의 작업을 수행한다. 그렇다면 여러 Agent 중 하나가 실패했을 때는 어떻게 해야 할까? 처음에는 Agent 하나가 실패하면 전체 Workflow도 실패하는 흐름을 생각했는데, 05_partial_failure.py 실습에서는 같은 실패를 서로 다른 정책으로 처리하고 있었다. Agent 하나가 실패했다고 항상 전체 Workflow를 중단해야 할까? 이번 실습에서는 Fail Fast , Best Effort , Required/Optional 세 가지 정책을 비교하고, 실패하는 Agent를 직접 바꿔보면서 결과가 어떻게 달라지는지 확인했다. 선택 Agent가 실패한 경우 기본 실습에서는 Weather와 Budget Agent는 성공하고, Place Agent가 실패한 상황이었다. Weather Agent → 성공 [필수] Budget Agent → 성공 [필수] Place Agent → 실패 [선택] 코드에서는 필수 Agent가 다음과 같이 정의되어 있었다. REQUIRED_AGENTS = {"weather_agent", "budget_agent"} 따라서 실패한 place_agent 는 필수 결과에 포함되지 않는다. 이 상태에서 세 가지 실패 정책을 적용하자 결과가 달라졌다. 정책 계속 실행 이유 Fail Fast ❌ Agent 하나라도 실패하면 중단 Best Effort ✅ 성공한 결과가 있으므로 계속 Required/Optional ✅ 실패한 Place가 필수 Agent가 아니므로 계속 실제 실행 결과에서도 Required/Optional 의 경우 필수 결과 누락이 없었다. failed_agents: ['place_agent'] missing_required: [] can_continue: True termination_reason: 'continue_with_available_results' 그렇다면 필수 Agent가 실패하면 어떻게 될까? 여기서 한 가지가 더 궁금해졌다. Required/Optional 정책에서는 Place Agent가 실패해도 Workflow를 계속할 수 있었다. 그렇다면 선택 Agent가 아니라 필수 Agent가 실패하면 결과가 달라질까? 이를 확인하기 위해 기존 코드에서 실패 대상을 place_agent 에서 budget_agent 로 직접 변경해봤다. 기존 코드는 다음과 같았다. RESULTS = { "weather_agent": {"status": "completed"}, "budget_agent": {"status": "completed"}, } ERRORS = { "place_agent": "Provider timeout" } 실패 Agent를 Budget으로 바꾸면서 성공 결과도 함께 변경했다. RESULTS = { "weather_agent": {"status": "completed"}, "place_agent": {"status": "completed"}, } ERRORS = { "budget_agent": "Provider timeout" } 필수 Agent를 정의하는 REQUIRED_AGENTS 는 그대로 유지했다. REQUIRED_AGENTS = { "weather_agent", "budget_agent" } 즉 이번에는 다음 상황을 만든 것이다. Weather Agent → 성공 [필수] Place Agent → 성공 [선택] Budget Agent → 실패 [필수] 같은 정책인데 결과가 달라졌다 코드를 다시 실행하자 budget_agent 가 missing_required 에 포함됐다. failed_agents: ['budget_agent'] missing_required: ['budget_agent'] 그리고 Required/Optional 정책은 더 이상 Workflow를 계속할 수 없다고 판단했다. policy: 'required_optional' can_continue: False termination_reason: 'required_result_failed' 두 실험을 비교하면 차이가 더 명확하다. 실패한 Agent Fail Fast Best Effort Required/Optional Place Agent [선택] 중단 계속 계속 Budget Agent [필수] 중단 계속 중단 Fail Fast 는 Agent 하나가 실패하면 중단했고, Best Effort 는 성공한 결과가 남아 있으면 계속 진행했다. 가장 차이가 명확했던 것은 Required/Optional 이었다. Place Agent가 실패했을 때는 필수 결과가 모두 남아 있어 계속할 수 있었지만, 필수 Agent인 Budget이 실패하자 missing_required 에 포함되면서 Workflow가 중단됐다. 실패 여부만 보는 것이 아니었다 이번 실습에서 중요한 것은 어떤 정책이 항상 더 좋은지를 고르는 것이 아니었다. Workflow마다 반드시 필요한 결과와 허용할 수 있는 실패가 다르기 때문이다. 이번 실습에서는 Weather와 Budget을 필수 Agent로 정의하고 Place를 선택 Agent로 두었다. 따라서 Place가 실패해도 Required/Optional 정책에서는 계속 진행할 수 있다고 판단하지만, Budget이 실패하면 필수 결과가 누락된 것으로 판단해 중단한다. 특히 실패가 발생한 뒤 그때그때 계속할지 결정하는 것이 아니라, 어떤 결과가 필수인지와 실패했을 때 어떻게 처리할지를 Workflow 정책으로 미리 정의한다는 점 이 중요했다. 정리 이번 실습에서는 선택 Agent였던 Place의 실패를 필수 Agent인 Budget의 실패로 직접 바꿔 실행해봤다. 그 결과 같은 Required/Optional 정책에서도 Place가 실패했을 때는 계속 진행할 수 있었지만, Budget이 실패하자 required_result_failed 로 중단되는 것을 확인했다. 이를 통해 Multi-Agent Workflow에서는 단순히 “Agent가 실패했는가?” 만 판단하는 것이 아니라, 어떤 결과가 반드시 필요한지, 어떤 실패를 허용할지, 언제 Workflow를 중단할지를 미리 정책으로 정의하는 것 이 중요하다는 점을 이해할 수 있었다. GitHub 이번 실습과 관련된 코드는 GitHub에서 확인할 수 있다. 📂 Orchestration - Partial Failure https://github.com/jbbdyee/aidevs/blob/main/07_multi-agent-service-ops/04_orchestration/05_partial_failure.py
velog
새 버전을 무중단 배포하고 나면 이상하게 배포 직후 1~2초 동안만 간헐적으로 500 에러 가 튀는 현상이 발생했습니다. 잠시 후 다시 요청하면 아무 일도 없었다는 듯 정상 동작하는 기묘한 버그... 범인은 바로 JJWT 라이브러리 내부의 Java ServiceLoader 동시성 경합이었습니다. 🚨 1. 증상: 배포 직후 첫 요청들의 의문의 500 폭탄 서비스를 새 버전으로 빌드하여 배포한 직후, 로그인된 상태로 대기 중이던 사용자의 요청이나 헬스체크 트래픽 중 일부가 500 Internal Server Error를 뱉었습니다. 서버의 app.log 를 열어보니 아래와 같은 스택트레이스가 찍혀 있었습니다: java.util.NoSuchElementException at java.base/java.util.ServiceLoader$2.next(ServiceLoader.java:1308) at io.jsonwebtoken.impl.lang.Services.loadFirst(Services.java:98) at io.jsonwebtoken.impl.DefaultJwtParserBuilder.build(DefaultJwtParserBuilder.java:128) at kr.co.allteachers.global.auth.jwt.JwtUtil.parseToken(JwtUtil.java:50) 더 당황스러웠던 점은: 배포 직후 첫 몇 번의 요청에서만 발생하고, 그 이후에는 아무리 부하를 주어도 100% 정상 작동하며 재현되지 않았습니다. 🔍 2. 원인 분석: ServiceLoader 는 스레드 안전(Thread-safe)하지 않다 스택트레이스를 따라 io.jsonwebtoken 라이브러리의 소스코드를 파고들었습니다. JJWT는 JWT 토큰을 파싱하기 위해 JwtParser 를 처음 빌드할 때, JSON 파서 프로바이더(예: Jackson 프로바이더인 jjwt-jackson )를 찾기 위해 Java 표준 유틸리티인 java.util.ServiceLoader 를 사용합니다. // JJWT 내부의 Services.java 발췌 public static <T> T loadFirst(Class<T> spi) { ServiceLoader<T> l = ServiceLoader.load(spi); Iterator<T> i = l.iterator(); if (i.hasNext()) { // 💥 여러 스레드가 동시에 접근하면? return i.next(); } // ... } 왜 기동 직후에만 터졌을까? 스프링 부트 애플리케이션이 뜨자마자 임베디드 톰캣(Tomcat)이 외부 요청을 받기 시작합니다. 이때 여러 사용자의 요청이 동시에 인입 되면서, 각자의 스레드에서 아직 초기화되지 않은 JwtUtil.parseToken() 을 동시에 호출합니다. 문제의 ServiceLoader 는 Thread-safe 하지 않은 내부 Iterator 상태를 가집니다. 스레드 A가 hasNext() 를 확인하고 next() 를 꺼내려는 찰나에, 스레드 B가 이미 요소를 소모해 버려 스레드 A에서 NoSuchElementException 이 터진 것입니다! 한 번이라도 프로바이더 로딩이 끝나면 메모리에 캐싱되므로, 그 이후에 들어오는 요청들은 경합 없이 정상 통과되었던 것입니다. 🛠️ 3. 해결책: 애플리케이션 시작 시 웜업(Warm-up) 시키기 원인을 알았으니 해결은 명쾌했습니다. "실제 사용자의 요청이 들어오기 전에, 스프링 컨텍스트가 뜰 때 미리 동기적으로 JWT 파서를 한 번 호출해서 캐싱을 끝내두자!" 스프링 빈의 생명주기를 활용하여 @PostConstruct 를 통해 웜업 로직을 추가했습니다: @Component public class JwtUtil { // ... 기존 JWT 유틸 코드 ... /** * JJWT 내부 ServiceLoader 동시성 경합 방지를 위한 웜업 * 임베디드 웹서버가 실제 트래픽을 받기 전, 스프링 초기화 시점에 * 더미 토큰을 한 번 파싱하여 JSON 프로바이더 캐싱을 완료한다. */ @PostConstruct void warmUpJwtParser() { try { String dummyToken = generateAccessToken(0L, "WARMUP_WARMUP"); parseToken(dummyToken); log.info("JWT Parser warmup completed successfully."); } catch (Exception e) { log.warn("JWT Parser warmup failed: {}", e.getMessage()); } } } 🧪 4. 검증 결과 멀티스레드 동시 요청 테스트 : 웜업 적용 전: 기동 직후 20개 스레드로 동시 요청 시 1~3건 간헐적 500 에러 발생. 웜업 적용 후: 기동 직후 50개 스레드 동시 요청 시 500 에러 0건 , 전건 200 OK 통과. 운영 환경 배포 : 배포 직후 헬스체크 및 실사용자 인입 시 500 에러 발생률 0% 달성. 💡 이번 트러블슈팅의 핵심 교훈 지연 로딩(Lazy Loading)의 동시성 위험 : 라이브러리 내부에서 첫 호출 시점에 리소스를 로딩하는 구조는 멀티스레드 환경에서 언제든 경합(Race Condition)을 유발할 수 있습니다. 배포 직후 발생하는 에러는 '웜업'을 의심하라 : 배포 직후에만 잠깐 터졌다가 사라지는 에러는 대부분 클래스 로딩, 커넥션 풀 초기화, 캐시 미스에 의한 동시성 이슈일 가능성이 높습니다. @PostConstruct 를 통한 사전 웜업은 복잡한 서드파티 라이브러리의 동시성 문제를 가장 깔끔하고 안전하게 방어하는 테크닉 중 하나입니다.
Score: 57.02Confidence: 54%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%
Score: 54.4Confidence: 49%