[{"content":"개요 2026년 3월 30일(현지 시간), 주당 1억 회 이상의 다운로드를 기록하는 가장 인기 있는 HTTP 클라이언트 라이브러리인 Axios의 NPM 계정이 해킹되었다. 요약하면, Axios 관리자 계정을 탈취하고 plain-crypto-js라는 가짜 의존성이 추가된 axios@1.14.1 및 axios@0.30.4 버전을 배포했다.\n자세한 건 아래 사이트에 정리되어 있기 때문에 나는 개발자로서 어떻게 대응해야 할지를 중점적으로 작성하려고 한다.\n타임라인 StepSecurity SK쉴더스\n대응 전략 1. npm install 시 --ignore-scripts 옵션을 사용 이번 사태에 핵심 원인이기도 했던 자동 스크립트 실행을 막는 옵션이다. 프로젝트에 따라 사용할 때도 있겠지만, 무지성 업그레이드하는 것도 지양해야 한다.\n2. Egress 설정 아웃바운드 설정만 해도 밖으로 통신하는 것을 막기 때문에, 허용된 도메인 외에 통신은 막아두자.\n3. Lock 파일 활용 npm, yarn 등을 사용한다면 lock 파일이 있을 텐데, 검증된 버전인지 확인하자.\n4. 그 외 환경변수 파일을 공개적으로 올리지 않고 별도로 관리하거나, 암호화하는 것은 잘 알 것이다.\nAI가 추천해 준 방법으로 패키지 내 소스 변화를 분석해 주는 Socket.dev, 알려진 취약점이 있는 패키지인지 확인하는 npm audit / Snyk 등도 있다.\n마무리 Axios는 Javascript를 통해 통신을 구현해 봤다면 한 번쯤 사용해 보거나, 어떤 스킬인지는 알 것이다. 이러한 라이브러리도 보안 문제가 발생할 수 있다는 것을 알게된 사례였던 것 같다.\n어찌보면 레거시 쓰는 기업들이 업그레이드를 고민하는 이유가 될 수도 있고, 무조건 최신 기술이 좋다는 것에 대한 위협을 알려준 사건이 아닌가 싶다.\n참고자료 Axios Github Issue\n","permalink":"https://4d4cat.com/posts/2026/axios-compromised-on-npm/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e2026년 3월 30일(현지 시간), 주당 1억 회 이상의 다운로드를 기록하는 가장 인기 있는 HTTP 클라이언트 라이브러리인 Axios의 NPM 계정이 해킹되었다.\n요약하면, Axios 관리자 계정을 탈취하고 \u003ccode\u003eplain-crypto-js\u003c/code\u003e라는 가짜 의존성이 추가된 \u003ccode\u003eaxios@1.14.1\u003c/code\u003e 및 \u003ccode\u003eaxios@0.30.4\u003c/code\u003e 버전을 배포했다.\u003c/p\u003e\n\u003cp\u003e자세한 건 아래 사이트에 정리되어 있기 때문에 나는 개발자로서 어떻게 대응해야 할지를 중점적으로 작성하려고 한다.\u003c/p\u003e\n\u003ch3 id=\"타임라인\"\u003e타임라인\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan\"\u003eStepSecurity\u003c/a\u003e   \u003cbr\u003e\n\u003ca href=\"https://www.skshieldus.com/security-insights/trends/axios-supply-chain-attack-insight-report\"\u003eSK쉴더스\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"대응-전략\"\u003e대응 전략\u003c/h2\u003e\n\u003ch3 id=\"1-npm-install-시---ignore-scripts-옵션을-사용\"\u003e1. npm install 시 \u003ccode\u003e--ignore-scripts\u003c/code\u003e 옵션을 사용\u003c/h3\u003e\n\u003cp\u003e이번 사태에 핵심 원인이기도 했던 자동 스크립트 실행을 막는 옵션이다. 프로젝트에 따라 사용할 때도 있겠지만, 무지성 업그레이드하는 것도 지양해야 한다.\u003c/p\u003e","title":"Axios compromised on npm"},{"content":"개요 최근 Claude가 2월 말부터 시작하여 오늘까지 잔 버그(?) 같은 에러가 계속 발생하고 있다. 무슨 문제길래 해결된 것 같아도 다시 발생하는 것일까?\n시기가 미국-이란 전쟁과 맞물려 있어서 AWS 중동 리전 이슈, 군사 작전에 Claude를 사용하여 트래픽 급증했다는 말도 많이 나오고 있다.\nUptime over the past 90 days Claude Status 요약 사건 이력을 확인해 봤을 때, 3월 1일에는 특별한 이슈가 없던 것 같다.\n따라서, 3월 1일 전후로 살펴보겠다.\n2월 여러 장애가 있었지만 대표적으로 3가지로 요약된다.\nClaude 앱, Claude Code 이슈: 앱이 안 열리거나 Claude Code가 파일을 과도하게 생성했다. 원인: 앱 업데이트 과정에서 발생한 코드 버그 (e.g. Windows 환경의 write 경합) 해결: 사용자가 앱을 업데이트 인프라 이슈: Usage reporting이 멈추고 대시보드가 보이지 않는 현상 원인: 사용 데이터를 수집하고 보여주는 내부 서비스 장애 해결: 내부 데이터베이스 및 서비스 복구 추론 모델 이슈: Opus 4.6, Sonnet 4.6 모델이 답변을 못 하고 대신 에러로 응답 원인: 트래픽 과부하 혹은 모델 서버 자체의 불안정 해결: Anthropic 측의 서버 핫픽스 3월 이슈: 서비스 전반적으로 오류 발생 원인: API, 로그인 인증 시스템의 불안정 해결: 핫픽스(라고 하지만 현재까지도 재발하는 중) 어떻게 대응해야할까? Claude API를 직접적으로 사용하는 곳이 얼마나 많을지는 모르겠지만, 대체로 코딩 생산성에 사용하는 유저가 대부분이 아닐까 싶다.\n개발 도구로서의 생산성 Claude가 먹통일 때, 다른 AI를 사용할 수 있으면 되지 않을까?\n개인적으로 개발 도구로서 쓸 때는 컨텍스트를 어떻게 작성했는지가 제일 클 것 같다. Claude Code, Gemini CLI, Codex 등 여러 도구가 있지만 모델마다 프롬프트 스타일은 다르므로 같은 컨텍스트라도 다른 성능이 나온다. 그러므로 각 모델에 맞게 프롬프트를 커스텀해야할텐데 이 부분에서 서비스 규모에 따라 유지비용의 차이가 발생할 것 같다. Pro 모델 가격만 본다면 월 2~3만 원 정도 책정된다. 오히려 Cursor와 같이 특정 기업의 모델만 사용하지 않는 서비스가 좋을 수도 있다. 위처럼 새로운 모델을 추가로 사용할 때 투자 대비 수익/안정성이 단일 AI 사용할 때보다 과연 괜찮을까? Claude API Claude API를 직접 사용해 보지 않았지만, 다른 API와 마찬가지로 에러가 발생한다면 그에 대한 예외 처리가 필요할 것이다. 장애가 발생했을 때 대체할 API가 있는지, API Gateway가 설정되지 않았다면 구축하여 다른 API로 요청을 돌릴 수도 있다.\n마무리 작년 이맘때쯤 연구독을 했었는데 하반기에 들어서 Claude를 사용할 때 불편한 점이 점점 보이기 시작했다.\n응답 포맷(백틱 문자 등)이 깨짐 요청 당 입력 토큰 사이즈가 타 AI에 비해 적음 컨텍스트가 일정 크기가 넘어가면 채팅을 새로 해야 함 남은 구독 기간 동안 여러 AI를 거쳐봤는데 그나마 Gemini가 내 스타일대로 답변해줘서 Gemini CLI도 같이 사용할 겸 넘어가게 되었다. 물론 전체적인 답변 퀄리티만 보면 Claude가 좋긴 한데, 불편함과 결과 사이에서 저울질했던 것이 점점 기울여진 것 같다.\n참고자료 ClaudeStatus\n","permalink":"https://4d4cat.com/posts/2026/claude-outage/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e최근 Claude가 2월 말부터 시작하여 오늘까지 잔 버그(?) 같은 에러가 계속 발생하고 있다. 무슨 문제길래 해결된 것 같아도 다시 발생하는 것일까?\u003c/p\u003e\n\u003cp\u003e시기가 미국-이란 전쟁과 맞물려 있어서 AWS 중동 리전 이슈, 군사 작전에 Claude를 사용하여 트래픽 급증했다는 말도 많이 나오고 있다.\u003c/p\u003e\n\u003ch3 id=\"uptime-over-the-past-90-days\"\u003eUptime over the past 90 days\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"outage\" loading=\"lazy\" src=\"/images/2026/claude-outage.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"claude-status-요약\"\u003eClaude Status 요약\u003c/h2\u003e\n\u003cp\u003e사건 이력을 확인해 봤을 때, 3월 1일에는 특별한 이슈가 없던 것 같다.\u003c/p\u003e\n\u003cp\u003e따라서, 3월 1일 전후로 살펴보겠다.\u003c/p\u003e\n\u003ch3 id=\"2월\"\u003e2월\u003c/h3\u003e\n\u003cp\u003e여러 장애가 있었지만 대표적으로 3가지로 요약된다.\u003c/p\u003e","title":"What Happened to Claude? A Timeline of Service Disruptions"},{"content":"개요 2025년 11월 18일에 이미 장애가 한 번 났는데, 2주가 좀 지난 오늘 또 장애가 발생했다. 다행히 설정 변경 직후 곧바로 감지가 되어서 장애 발생 후, 약 25분만에 문제가 해결되었다.\n장애 원인 이번 주 공개된 React Server Components 관련하여 WAF의 요청 파싱 방식을 변경 중이었다. 그런데 변경 이후, cloudflare 네트워크 전체적으로 다운되는 현상이 발생했다.\nResolved\nThis incident has been resolved. A change made to how Cloudflare\u0026rsquo;s Web Application Firewall parses requests caused Cloudflare\u0026rsquo;s network to be unavailable for several minutes this morning. This was not an attack; the change was deployed by our team to help mitigate the industry-wide vulnerability disclosed this week in React Server Components. We will share more information as we have it today. Dec 05, 2025 - 09:20 UTC\nMonitoring A fix has been implemented and we are monitoring the results.\nDec 05, 2025 - 09:12 UTC\nInvestigating Cloudflare is investigating issues with Cloudflare Dashboard and related APIs. Customers using the Dashboard / Cloudflare APIs are impacted as requests might fail and/or errors may be displayed. Dec 05, 2025 - 08:56 UTC\n보안 취약점에 대응하기 위해 기본 버퍼 크기를 128KB에서 1MB로 늘려서 점진적으로 배포하고 있었다. 그런데 내부 WAF 테스트 도구가 이 버퍼 크기 변경을 지원하지 않아서 테스트 도구를 비활성화시켰는데, 이 변경이 즉각적으로 전 세계에 전파되었다.\n일부 FL1 Proxy 라고 불리는 구형 프록시에서는 테스트 도구를 비활성화시키는 것이 오류를 유발하여 HTTP 500 에러를 발생시키기도 했다.\nFL1 프록시의 코드 실행에서 버그로 인해 Lua 예외가 발생\n[lua] Failed to run module rulesets callback late_routing: /usr/local/nginx-fl/lua/modules/init.lua:314: attempt to index field \u0026#39;execute\u0026#39; (a nil value) 공식 블로그 내용 Cloudflare\u0026rsquo;s Web Application Firewall (WAF) provides customers with protection against malicious payloads, allowing them to be detected and blocked. To do this, Cloudflare’s proxy buffers HTTP request body content in memory for analysis. Before today, the buffer size was set to 128KB.\nAs part of our ongoing work to protect customers who use React against a critical vulnerability, CVE-2025-55182, we started rolling out an increase to our buffer size to 1MB, the default limit allowed by Next.js applications, to make sure as many customers as possible were protected.\nThis first change was being rolled out using our gradual deployment system. During rollout, we noticed that our internal WAF testing tool did not support the increased buffer size. As this internal test tool was not needed at that time and had no effect on customer traffic, we made a second change to turn it off.\nUnfortunately, in our FL1 version of our proxy, under certain circumstances, the second change of turning off our WAF rule testing tool caused an error state that resulted in 500 HTTP error codes to be served from our network.\nAs soon as the change propagated to our network, code execution in our FL1 proxy reached a bug in our rules module which led to the following Lua exception:\nReact Server Components가 뭐길래? 12월 3일 React 팀에서 인증 없이 원격 코드 실행(Remote Code Execution, RCE)이 가능한 심각한 취약점을 공개했다. 해당 취약점을 요약하면 RSC에서 사용하는 “Flight” 프로토콜의 직렬화/역직렬화 처리 로직이 안전하지 않다. 공격자가 악의적으로 조작된 HTTP 요청(payload)을 보내면, 서버에서 이 데이터를 역직렬화하는 과정에서 임의의 코드가 실행될 수 있다.\n해당 기능이 실제로 Server Function endpoint를 직접 구현하지 않았더라도, 단순히 RSC를 지원하는 설정만으로 취약 상태가 될 수 있다는 것이다. 이 취약점들의 공식 식별자는 다음과 같다.\nCVE-2025-55182 CVE-2025-66478\n관련 포스트 데일리시큐 WIZ\n티스토리\n조치 과정 빠르게 감지한만큼 이전 설정으로 롤백을 했다.\n변경하려고 했던 부분이 완료되지 못한 것 같은데 대신 내부 시스템을 정비한다고 한다.\n점진적 롤아웃, 빠른 롤백 기능 강화 간소화된 비상 조치 기능\n: 추가 장애가 발생하더라도 중요 작업을 계속 수행하도록 보장 Fail-Open 방식의 오류 처리 도입\n: 설정 파일이 손상되거나 허용 범위를 벗어나도 요청을 차단하기보다 가능한 한 가용성을 유지하도록 변경 We know it is disappointing that this work has not been completed yet. It remains our first priority across the organization. In particular, the projects outlined below should help contain the impact of these kinds of changes:\nEnhanced Rollouts \u0026amp; Versioning: Similar to how we slowly deploy software with strict health validation, data used for rapid threat response and general configuration needs to have the same safety and blast mitigation features. This includes health validation and quick rollback capabilities among other things.\nStreamlined break glass capabilities: Ensure that critical operations can still be achieved in the face of additional types of failures. This applies to internal services as well as all standard methods of interaction with the Cloudflare control plane used by all Cloudflare customers.\n\u0026ldquo;Fail-Open\u0026rdquo; Error Handling: As part of the resilience effort, we are replacing the incorrectly applied hard-fail logic across all critical Cloudflare data-plane components. If a configuration file is corrupt or out-of-range (e.g., exceeding feature caps), the system will log the error and default to a known-good state or pass traffic without scoring, rather than dropping requests.\n영향 받은 서비스 지난 번과 다르게 Gen AI에서는 Claude만 먹통이고, ChatGPT와 Gemini는 문제가 없는듯하다.\n내가 확인한 서비스 중에서는 링크드인 밖에 없지만, 단톡이나 커뮤니티에서는 티맵, 배달의민족, 요기요, 무신사 등 문제가 있는 것 같다. 또, 대부분 FL1(구형) 프록시 환경인 곳에서 오류가 발생했다고 하지만 대형 서비스에도 영향은 미친 것 같다. Shopify, Zoom 등\n마무리 역시나 이번 사태에 대해 레딧에도 글이 하나 올라왔는데 거기서 괜찮은 사이트를 알게 됐다.\n레딧\n다운 디텍터\n11월 사태에 비해 엄청 빨리 해결됐다고 생각했는데 클라우드플레어에서도 이전 사태가 발생한지 얼마 안됐고, 우선은 서비스 안정화가 우선이다보니 롤백을 바로 결정한 것 같다. 나도 이전 팀에 있었을 때, 설정 몇개 바꾸는 것으로 서비스에 문제가 생길까봐 백업을 준비한 적이 있는데 이 분야에 있는 사람이라면 한 번쯤 겪는 상황인 것 같다.\n참고자료 CloudflareBlog\nCloudflareStatus ChatGPT\n","permalink":"https://4d4cat.com/posts/2025/cloudflare-outage-2/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e2025년 11월 18일에 이미 장애가 한 번 났는데, 2주가 좀 지난 오늘 또 장애가 발생했다. 다행히 설정 변경 직후 곧바로 감지가 되어서 장애 발생 후, 약 25분만에 문제가 해결되었다.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"outage\" loading=\"lazy\" src=\"/images/2025/cloudflare-outage-2.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"장애-원인\"\u003e장애 원인\u003c/h2\u003e\n\u003cp\u003e이번 주 공개된 \u003cem\u003e\u003cstrong\u003eReact Server Components\u003c/strong\u003e\u003c/em\u003e 관련하여 \u003cem\u003e\u003cstrong\u003eWAF의 요청 파싱 방식을 변경\u003c/strong\u003e\u003c/em\u003e 중이었다. 그런데 변경 이후, cloudflare 네트워크 전체적으로 다운되는 현상이 발생했다.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eResolved\u003cbr\u003e\nThis incident has been resolved.\nA change made to how Cloudflare\u0026rsquo;s Web Application Firewall parses requests caused Cloudflare\u0026rsquo;s network to be unavailable for several minutes this morning. This was not an attack; the change was deployed by our team to help mitigate the industry-wide vulnerability disclosed this week in React Server Components. We will share more information as we have it today.  \u003cbr\u003e\nDec 05, 2025 - 09:20 UTC\u003c/p\u003e","title":"Cloudflare is down Again: What is the root cause this december?"},{"content":"개요 2025년 11월 29일 17시 53분경 문자 메시지로 개인정보 노출 통지를 받았다. 솔직히 이런 사건 있을 때마다 나는 운 좋게 대상자가 아니었지만, 이번에는 피해 대상자가 되었다. 문자 메시지와 쿠팡 FAQ 내용으로 현 피해자는 약 4500명으로 인지했다고 하는데, 18시 이후에 나온 뉴스를 통해 피해자가 3370만명이 넘는 것으로 확인됐다.\n쿠팡이 사건을 인지한 날짜는 11월 18일이지만, 유출이 시작한 일자는 6월 24일부터로 추정 중이다.\n이후 국정원과 협력하여 조사를 진행했고, 유출자로부터 새로운 사실, 진술서 등을 받은 즉시 정부에 제출했다고 한다. 여기서 밝혀진 내용이 3300만명의 개인정보가 유출된 것이 아니라 그만큼 접근만 한 것이고, 실제 데이터 저장은 약 3천개만 저장했다고 한다. 물론 이 역시 모두 삭제했다고 한다.\n유출 건수는 상당히 줄긴 했지만 개인정보 탈취 경로는 이전과 큰 차이가 없는 것 같다.\n문자 메시지 내용 전문 보기/접기 쿠팡을 이용해 주시는 고객님께 진심으로 사과드립니다. 고객님의 소중한 개인정보가 일부 노출되는 사고가 발생하였습니다.\n쿠팡은 이번 노출을 인지한 즉시 관련 당국에 신속하게 신고하였습니다. 이번 사건은 비인가 조회로 파악되었으며 경찰청, 개인정보보호위원회, 한국인터넷진흥원 등 관련 당국과 협력하여 조사 중에 있습니다.\n현재까지 조사된 결과에 따르면 노출된 정보는 고객님의 이름, 이메일 주소, 배송지 주소록 (입력하신 이름, 전화번호, 주소) 그리고 주문정보입니다. 고객님의 카드정보 등 결제정보 및 패스워드 등 로그인 관련 정보는 노출이 없었음을 확인하였으며 안전하게 보호되고 있습니다.\n쿠팡은 비정상 접근 경로를 즉시 차단하였고, 내부 모니터링을 한층 더 강화하였습니다.\n고객님께 심려를 끼쳐 드린 점에 대해 다시 한번 진심으로 사과의 말씀을 드리며 쿠팡을 사칭하는 전화, 문자 등에 각별한 주의를 부탁드립니다. 쿠팡의 모든 임직원은 고객님의 불편과 심려를 신속하게 해소할 수 있도록 최선의 노력을 다하겠습니다.\n링크(https://mc.coupang.com/ssr/mobile/faqlist) 에는 자주 묻는 질문(FAQ)과 문의처가 포함되어 있으며 추가 내용은 해당 링크를 통해 지속적으로 업데이트하도록 하겠습니다.\n주요 원인 \u0026lsquo;액세스 토큰을 통한 비인가 접속\u0026rsquo; 으로 추정되는데, 정확한 것은 결과가 나와야 알 것 같다.\n하지만, 그 이후에 나오는 속보에 의하면 중국 국적의 전 쿠팡 직원의 소행이라고 다시 추정되고 있다. 왜냐하면 해당 직원은 사건 발생 직후 한국을 떠나 중국으로 귀국했으며, 이미 퇴사 처리가 됐다고 한다.\n11월 30일 오후 4시, 과학기술정보통신부에 의하면, 공격자가 쿠팡 서버의 인증 취약점을 악용한 것으로 발표했다. 퇴사 후 즉시 폐기되어야 할 구형 액세스 토큰을 악용한 것으로 보인다.\n내부자 위협 비인가 접속으로 정보를 탈취했다면 퇴사한 직원의 퇴사 처리가 제대로 이루어지지 않았기 때문에 해당 권한을 악용한 것이다. 여전히 고객 DB에 접근할 수 있었고, 외부 침입이 아니다 보니 탐지가 어려웠던 것 같다. 정상 로그인 절차 없이도 DB에 접근이 가능한 경로나 내부 API가 있거나 인증을 우회할 수 있는 부분이 있던 것 같다.\n퇴사자 임직원몰 악용 사례 경험 나도 비슷한 경험이 있었는데 퇴사자를 퇴사 처리해도 임직원몰의 계정은 아직 살아있어서 여러 할인 혜택을 계속해서 이용한 사례가 있었다. 나중에 해당 케이스를 발견한 현업에 의해 보고되고, 퇴사 처리가 됐을 때 임직원 계정도 절차에 맞춰서 탈퇴하도록 처리했다.\n노출 정보 이름 전화번호 이메일 배송주소록 주문정보 개인정보 노출 관련 FAQ 추가 조사 이후 안내사항 접기/펼치기 쿠팡은 유출자를 특정하였고, 고객 정보 유출에 사용된 모든 장치가 회수되었음을 확인하였습니다. 현재까지 조사에 의하면, 유출자는 약 3,000개 계정의 제한된 고객 정보만 저장했고, 이후 이를 모두 삭제하였습니다. 현재까지 조사에 의하면,\n-유출자, 3300만 고객 정보에 접근했지만 약 3000개 계정만 저장하였고, 이 역시 모두 삭제하였음\n-저장한 고객 정보는 공동현관 출입번호 2609개 포함. 다만, 결제정보·로그인·개인통관고유번호는 없음\n-외부 전송 등 추가 유출 없음\n최근 발생한 개인정보 유출이 고객들에게 얼마나 큰 우려를 불러일으켰는지 책임을 통감합니다.쿠팡 개인정보 유출 사태로 인해 수많은 국민들이 걱정과 불편을 겪게 된 것에 대해 진심으로 사과드립니다. 이 사태를 책임감 있게 수습하기 위해 정부기관과 쿠팡의 전 구성원들이 함께 최선의 노력을 다했고, 중요한 업데이트 내용을 상세히 설명드립니다.\n쿠팡은 디지털 지문(digital fingerprints) 등 포렌식 증거를 활용해 고객 정보를 유출한 전직 직원을 특정했습니다. 유출자는 행위 일체를 자백하고 고객 정보에 접근한 방식을 구체적으로 진술했습니다.\n유출자가 쿠팡 고객 정보를 접근 및 탈취하는 데 사용된 모든 장치와 하드 드라이브는 검증된 절차에 따라 모두 회수되어 안전하게 확보되었습니다. 쿠팡은 지난 12월 17일 유출자의 진술서 제출을 시작으로, 관련 장치 등 일체 자료를 확보하는 즉시 정부에 제출해 왔습니다. 쿠팡은 현재 진행 중인 정부기관의 관련 조사에도 성실히 협조해 왔습니다.\n사건 초기부터, 쿠팡은 엄격한 포렌식 조사를 진행하기 위해서 전세계 최상위 3개 글로벌 사이버 보안 업체인 맨디언트, 팔로알토 네트웍스, 언스트앤영에 조사를 의뢰했습니다.\n현재까지 조사 결과는 유출자의 진술 내용과 부합합니다. 이에 의하면, 1) 유출자는 탈취한 보안 키를 사용하여 3,300만 고객 계정의 기본적인 고객 정보에 접근했고, 2) 약 3,000개 계정의 고객 정보(이름, 이메일, 전화번호, 주소, 일부 주문정보)만 실제 저장했으며, 3) 여기에 포함된 공동현관 출입번호는 2,609개였고, 4) 사태에 대한 언론보도를 접한 후 저장했던 정보를 모두 삭제했으며, 5) 고객 정보 중 제3자에게 전송된 데이터는 일체 없습니다.\n유출자는 탈취한 보안 키를 이용해 기본적인 고객 정보에 접근함 유출자는 재직 중에 취득한 내부 보안 키를 탈취해 이름, 이메일, 주소, 전화번호 등 일부 고객 개인정보에 접근했다고 진술했습니다. 데이터 로그 및 포렌식 조사를 통해, 해당 접근은 탈취된 내부 보안 키를 이용했으며, 접근된 데이터의 유형 또한 유출자가 진술한 범위(이름, 이메일, 주소, 전화번호)에 한정되었음을 확인했습니다. 결제정보, 로그인 관련 정보, 개인통관번호에 대한 접근은 없었습니다.\n유출자는 주문 이력 및 공동현관 출입 정보에는 매우 제한적으로 접근함 유출자는 다수 고객의 기본 고객 정보에 접근하는 과정에서 약 3,000개의 계정에 대한 주문정보와 공동현관 출입번호에 접근했다고 진술했습니다. 독립적인 외부 전문업체의 포렌식 분석 결과, 2,609개의 공동현관 출입번호가 접근된 것으로 확인되며, 이는 유출자의 진술과 부합합니다.\n유출자는 데스크톱 PC와 MacBook Air노트북을 사용해 공격함 유출자는 개인용 데스크톱 PC와 MacBook Air 노트북을 사용해 공격을 시도했고 접근한 정보 중 일부를 해당 기기에 저장했다고 진술했습니다. 독립적인 포렌식 조사 결과 쿠팡 시스템에 대한 불법접근은 유출자가 진술한 대로 1대의 PC 시스템과 1대의 애플 시스템을 통해 수행된 것으로 확인됐습니다. 유출자는 해당 데스크톱 PC와 PC에서 사용된 4개의 하드 드라이브를 제출했으며, 분석 결과 이들 저장장치에서 공격에 사용된 스크립트가 발견됐습니다.\n유출자는 MacBook Air노트북을 삭제 및 파기하기 위해 하천에 투기했음 유출자는 언론을 통해 데이터 유출 보도가 나오자 극도의 불안 상태에 빠져 증거의 은폐·파기를 시도했다고 진술했습니다. 유출자는 MacBook Air 노트북을 물리적으로 파손한 뒤 쿠팡 로고가 있는 에코백에 넣고 벽돌을 채워 인근 하천에 던졌다고 진술했습니다. 유출자가 제공한 지도와 설명을 바탕으로 잠수부들이 해당 하천에서 MacBook Air 노트북을 회수했으며, 회수된 기기는 유출자의 진술 그대로 “벽돌이 담긴 쿠팡 에코백” 안에 들어 있었고, 일련번호 또한 유출자의 iCloud 계정에 등록된 일련번호와 정확히 일치했습니다.\n유출자는 소량의 고객 정보만 저장했으며, 외부 전송은 없었고 이후 모두 삭제했음 유출자는 단독으로 이를 저질렀으며, 약 3,000개 계정의 제한적인 고객 정보만을 저장했고, 해당 고객 정보는 개인 데스크톱 PC 와 MacBook Air 노트북에만 저장됐으며 외부로 전송된 적은 없다고 진술했습니다. 또한 언론 보도를 접한 직후 저장돼 있던 고객 정보를 모두 삭제했다고 진술했습니다. 현재까지 조사 결과는 유출자의 진술 내용과 부합하며, 유출자의 진술과 모순되는 증거가 발견되지 않았습니다.\n쿠팡은 향후 진행될 조사 경과에 따라 지속적으로 안내를 드릴 예정이며, 이번 사태로 인한 고객보상 방안을 조만간 별도로 발표할 예정입니다.\n쿠팡은 앞으로도 고객들의 소중한 개인정보를 보호하기 위해 최선을 다할 것입니다. 정부 조사에 적극 협조할 것이며, 2차 피해를 예방하는 데 최선을 다하겠습니다. 아울러 금번 사태를 계기로 재발 방지를 위한 모든 방안을 강구할 것임을 약속드립니다.\n모든 고객분들께 다시 한 번 진심으로 사과드립니다.\n참고자료 쿠팡 FAQ\n과학기술정보통신부 조선일보\n머니투데이 로톡\n","permalink":"https://4d4cat.com/posts/2025/coupang-data-breach/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e2025년 11월 29일 17시 53분경 문자 메시지로 개인정보 노출 통지를 받았다. 솔직히 이런 사건 있을 때마다 나는 운 좋게 대상자가 아니었지만, 이번에는 피해 대상자가 되었다. 문자 메시지와 쿠팡 FAQ 내용으로 현 피해자는 약 4500명으로 인지했다고 하는데, 18시 이후에 나온 뉴스를 통해 피해자가 3370만명이 넘는 것으로 확인됐다.\u003c/p\u003e\n\u003cp\u003e쿠팡이 사건을 인지한 날짜는 11월 18일이지만, 유출이 시작한 일자는 6월 24일부터로 추정 중이다.\u003c/p\u003e\n\u003cp\u003e이후 국정원과 협력하여 조사를 진행했고, 유출자로부터 새로운 사실, 진술서 등을 받은 즉시 정부에 제출했다고 한다. 여기서 밝혀진 내용이 3300만명의 개인정보가 유출된 것이 아니라 그만큼 접근만 한 것이고, 실제 데이터 저장은 약 3천개만 저장했다고 한다. 물론 이 역시 모두 삭제했다고 한다.\u003c/p\u003e","title":"쿠팡 3370만 개 넘는 계정 개인정보 유출이라 했지만, 사실은 3천개?"},{"content":"개요 AWS 장애 사태가 약 한 달이 되어가는데 Cloudflare에서도 장애가 발생했다. 한국 시간 기준으로 20시 48분경부터 Cloudflare가 적용된 일부 서비스가 간헐적으로 500 에러가 발생했다. 현재도 대부분의 서비스가 먹통인데, 내 블로그도 초반에는 먹통이었지만 지금은 접속이 잘 되고 있다. 4시 28분경에 장애가 해결됐고 관련 내용을 발표했다.\n장애 원인 포스팅 작성 시간 기준으로는 아직 명확한 원인이 나오지 않았다. Cloudflare 측은 문제가 발견되어 수정 작업을 진행 중인데, 아마도 대부분의 서비스가 안정화됐을 때 다시 보고해 주지 않을까 싶다.\n내부 데이터베이스 시스템 중 하나의 권한 변경이 원인이었고, 이에 따라 해당 DB가 ClickHouse 클러스터에서 반복적인 쿼리를 통해 feature file을 생성하는 과정에서 중복된 항목이 다수 출력되었다.\n최근 발생한 AWS 장애처럼 외부의 공격이나 악의적인 활동으로 인한 것은 아니라고 한다. 해당 파일은 봇 관리 시스템이 사용하는 파일이며, 출력 항목이 갑작스럽게 두 배로 늘어난 뒤 네트워크를 구성하는 모든 서비스로 전파되었다. 해당 파일을 참조해서 트래픽을 라우팅하거나 봇 위협을 판단하는 소프트웨어에는 해당 파일 크기 제한이 있었고, 이 수치를 넘자 장애가 발생했다.\n이례적인 그래프 그래프를 보면 11:30 시점부터 트래픽이 급증하지만 이례적으로 12:00~12:30 사이에 일시적으로 회복되었다.\n왜 이런 현상이 일어났는지 보면, Cloudflare 시스템은 ClickHouse DB 클러스터에서 실행되는 쿼리가 5분 주기로 feature file을 생성한다. 권한을 변경하는 과정에서 클러스터 일부 노드만 먼저 업데이트가 됐는데, 업데이트된 노드에서만 잘못된 feature file이 생성됐다. 업데이트되지 않은 노드에서는 정상 파일이 생성됐기 때문에 정상적인 파일, 잘못된 파일이 5분마다 랜덤하게 네트워크 전체로 전파됐다. 그래서 그래프와 같이 트래픽이 급증했다가 감소했다가 다시 급증하는 현상이 발생했다.\n5분마다 반복: 정상 파일 -\u0026gt; 잘못된 파일 -\u0026gt; 정상 파일 -\u0026gt; 잘못된 파일\n하지만 시간이 지나면서 클러스터 전체 노드가 업데이트되어 지속적으로 장애가 발생하게 됐다.\nCloudflare Blog The issue was not caused, directly or indirectly, by a cyber attack or malicious activity of any kind. Instead, it was triggered by a change to one of our database systems\u0026rsquo; permissions which caused the database to output multiple entries into a “feature file” used by our Bot Management system. That feature file, in turn, doubled in size. The larger-than-expected feature file was then propagated to all the machines that make up our network.\nThe software running on these machines to route traffic across our network reads this feature file to keep our Bot Management system up to date with ever changing threats. The software had a limit on the size of the feature file that was below its doubled size. That caused the software to fail.\nThe explanation was that the file was being generated every five minutes by a query running on a ClickHouse database cluster, which was being gradually updated to improve permissions management. Bad data was only generated if the query ran on a part of the cluster which had been updated. As a result, every five minutes there was a chance of either a good or a bad set of configuration files being generated and rapidly propagated across the network.\nThis fluctuation made it unclear what was happening as the entire system would recover and then fail again as sometimes good, sometimes bad configuration files were distributed to our network. Initially, this led us to believe this might be caused by an attack. Eventually, every ClickHouse node was generating the bad configuration file and the fluctuation stabilized in the failing state.\nCloudflare Status Resolved\nThis incident has been resolved. Nov 18, 2025 - 19:28 UTC\nUpdate Cloudflare services are currently operating normally. We are no longer observing elevated errors or latency across the network. Our engineering teams continue to closely monitor the platform and perform a deeper investigation into the earlier disruption, but no configuration changes are being made at this time. At this point, it is considered safe to re-enable any Cloudflare services that were temporarily disabled during the incident. We will provide a final update once our investigation is complete. Nov 18, 2025 - 17:44 UTC\nUpdate - We are continuing to work on a fix for this issue.\nNov 18, 2025 - 14:22 UTC\nIdentified - The issue has been identified and a fix is being implemented. Nov 18, 2025 - 13:09 UTC\nUpdate - During our attempts to remediate, we have disabled WARP access in London. Users in London trying to access the Internet via WARP will see a failure to connect. Nov 18, 2025 - 13:04 UTC\nInvestigating - Cloudflare is experiencing an internal service degradation. Some services may be intermittently impacted. We are focused on restoring service. We will update as we are able to remediate. More updates to follow shortly. Nov 18, 2025 - 11:48 UTC\n조치 과정 먼저 잘못된 파일을 만들어내는 자동 쿼리 실행을 중단하고, 네트워크 전체로 퍼지지 않도록 차단했다. 그 후, 이전에 검증된 정상적인 파일을 직접 Distribution Queue에 넣고 강제 재시작을 했다.\n지속적인 모니터링 결과, 14:30부터 에러가 감소하며 정상화되기 시작했다.\nErrors continued until the underlying issue was identified and resolved starting at 14:30. We solved the problem by stopping the generation and propagation of the bad feature file and manually inserting a known good file into the feature file distribution queue. And then forcing a restart of our core proxy.\nThe remaining long tail in the chart above is our team restarting remaining services that had entered a bad state, with 5xx error code volume returning to normal at 17:06.\n관련 내부 서비스 Core CDN Turnstile Worker KV Dashboard Email Security Access 영향 받은 서비스 내가 확인한 서비스는 생성형 AI(ChatGPT, Claude, Gemini 등)는 전원 먹통이고, Gemini의 경우 페이지는 접근되지만 프롬프트 요청이 실패한다. 나무위키에 따르면 X(구 트위터), 리그 오브 레전드를 포함하여 게임 관련 프로그램인 배틀아이가 적용된 게임들도 먹통이라고 한다.\n오프라인에서는 맥도날드 키오스크가 먹통이 됐었다.\n마무리 현재는 계속해서 복구 작업이 진행 중이라서 정확한 원인은 모르겠지만 AWS와 다르게 사이버 공격일 가능성도 있을 것 같다. 왜냐하면 Cloudflare는 이전에 보안 취약점 문제가 있었고 DDoS 공격도 이미 있었기 때문이다. 복구 작업이 진행되는 대로 내용도 다시 업데이트해야겠다.\n한 달 전 AWS 장애 사태를 겪고 나서 그런지 상대적으로 빨리 조치된 느낌을 받았다. 장애 초기에는 위에 언급한 대로 장애가 줄어드는 시점이 있어서 금방 조치되는 건가 싶었는데 단순히 이례적인 상황이었다.\n사건이 발생하고 여러 사이트에 접속을 해봤는데 생각보다 Cloudflare도 AWS처럼 여러 곳에 적용되어있다는 것을 알게 되었다. 웬만하면 CDN 서비스는 Cloudflare를 많이 쓰는 걸까? 대부분의 사이트를 접속하면 아래 문구가 나를 맞이했다.\n계속하려면 challenges.cloudflare.com을 차단 해제하세요.\nPlease unblock challenges.cloudflare.com to proceed.\n이번에도 외부 공격보다는 내부 이슈로 발생한 장애였다. 저번에는 클라우드였고, 이번에는 CDN인데 결국 CDN도 멀티 CDN으로 관리해야 하는 걸까? 이렇게 적용한다면 비용이나 운영 측면에서 성능과 안정성을 어떻게 유지할 수 있을까?\n참고자료 CloudflareBlog CloudflareStatus 나무위키 ChatGPT\n","permalink":"https://4d4cat.com/posts/2025/cloudflare-outage/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003eAWS 장애 사태가 약 한 달이 되어가는데 Cloudflare에서도 장애가 발생했다. 한국 시간 기준으로 20시 48분경부터 Cloudflare가 적용된 일부 서비스가 간헐적으로 500 에러가 발생했다. \u003cdel\u003e현재도 대부분의 서비스가 먹통인데,\u003c/del\u003e 내 블로그도 초반에는 먹통이었지만 지금은 접속이 잘 되고 있다. 4시 28분경에 장애가 해결됐고 관련 내용을 발표했다.\u003c/p\u003e\n\u003ch2 id=\"장애-원인\"\u003e장애 원인\u003c/h2\u003e\n\u003cp\u003e\u003cdel\u003e포스팅 작성 시간 기준으로는 아직 명확한 원인이 나오지 않았다. Cloudflare 측은 문제가 발견되어 수정 작업을 진행 중인데, 아마도 대부분의 서비스가 안정화됐을 때 다시 보고해 주지 않을까 싶다.\u003c/del\u003e\u003c/p\u003e","title":"Cloudflare outage: What went wrong?"},{"content":"개요 취준생 시절 나는 머신러닝에 관심이 생겨서 GPU가 나름 빵빵한 스펙으로 데스크탑을 구매했다. 지금 생각하면 이해가 안 가지만 왜 SSD를 2개를 샀을까? 그냥 용량 합친 것으로 1개 구매하면 됐을 텐데. 그 당시 RAID를 알게 되고 해결된 줄 알았지만 이게 지금에서야 문제가 될지는 한 달 전까지 몰랐다.\n사건의 시작 2025년 9월에 TPM 설정 후 윈 11로 업그레이드하고 나서는 큰 문제가 없었는데 10월이 되고 몇몇 업데이트 이후 부팅이 멈추기 시작했다. 이때는 마침 해당 업데이트 전에 시스템 복원 지점이 생성되어 복구하는 것으로 끝났다.\n이틀 전에 비슷한 상황이 발생했는데 시스템 복원을 하다가 복원 지점이 사라져서 부득이하게 재설치하게 됐다. 하지만 하루가 지나고 같은 현상이 발생해서 하루 날 잡고 깊이 있게 파고들게 되었다.\nWindows 11 권장 환경 시스템을 복구하는 과정에서 ChatGPT와 Gemini를 많이 사용했는데, 거기서 얻은 내용을 정리하고자 한다.\nFirmware: UEFI Secure Boot를 활성화하기 위한 필수 모드 부팅 속도 향상과 2TB 이상의 디스크 용량 지원을 위함 보안 설정: WHQL 활성화, Secure Boot 운영체제 무결성 보호를 위해 필수 적용 부팅 과정에서 서명된 파일만 로드하고, 악성 소프트웨어가 부팅 프로세스를 가로채는 것을 차단 디스크 형식: GPT UEFI의 요구사항이며, 부트로더를 저장하는 EFI 시스템 파티션을 관리하는데 필수 요소 SATA: AHCI 흔한 이슈는 아니지만 RAID 모드보다 RAID 컨트롤러의 드라이버가 UEFI/Secure Boot 환경과 호환되지 못해 RAID 볼륨 인식에 실패했을 가능성이 높다.\n후기 끝나고 나니까 정말 지쳤다. 재설치만 4번 정도 한 것 같은데 이래서 아는 것이 많아야 몸이 덜 힘든 것 같다. 여담으로 학생 시절 Home 버전을 Education으로 업그레이드했었다. 이걸 기준으로 Education 버전으로 재설치했는데 정품 인증이 안 돼서 Home 버전으로 다시 USB 만드는 일도 있었다.\n기존 Windows 10 시절 BIOS 세팅 - SATA 모드: RAID - Firmware 모드: Legacy + UEFI (CSM 활성화) - 디스크 형식: MBR (Legacy 부팅) - 보안 설정: WHQL 비활성화 Windows 11 충돌 이후 최종 설정 - SATA 모드: AHCI - Firmware 모드: UEFI 전용 (CSM 비활성화) - 디스크 형식: GPT (UEFI 부팅) - 보안 설정: WHQL 활성화 (Secure Boot 활성화) 참고 Quasarzone ChatGPT Gemini\n","permalink":"https://4d4cat.com/posts/2025/uefi-raid-conflict-win11-boot/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e취준생 시절 나는 머신러닝에 관심이 생겨서 GPU가 나름 빵빵한 스펙으로 데스크탑을 구매했다.\n지금 생각하면 이해가 안 가지만 왜 SSD를 2개를 샀을까? 그냥 용량 합친 것으로 1개 구매하면 됐을 텐데.\n그 당시 RAID를 알게 되고 해결된 줄 알았지만 이게 지금에서야 문제가 될지는 한 달 전까지 몰랐다.\u003c/p\u003e\n\u003ch3 id=\"사건의-시작\"\u003e사건의 시작\u003c/h3\u003e\n\u003cp\u003e2025년 9월에 \u003ccode\u003eTPM\u003c/code\u003e 설정 후 윈 11로 업그레이드하고 나서는 큰 문제가 없었는데 \u003cem\u003e\u003cstrong\u003e10월이 되고 몇몇 업데이트 이후\u003c/strong\u003e\u003c/em\u003e 부팅이 멈추기 시작했다.\n이때는 마침 해당 업데이트 전에 시스템 복원 지점이 생성되어 복구하는 것으로 끝났다.\u003c/p\u003e","title":"윈도우 10에서 11로 업그레이드 이후 어느 날 부팅 자체가 멈춘 일"},{"content":"개요 Thomas Eccel(구글 Ads 스페셜리스트)의 글을 포함한 외국 저널과 AWS에 Service health 내용을 참고하여 작성했습니다.\n한국 시간 기준, 2025년 10월 20일 오후 4시 경부터 US-EAST-1 리전을 사용하는 다수의 서비스에서 장애가 발생\n원인: US-EAST-1 리전의 DynamoDB API 엔드포인트의 DNS\n영향: IAM, DynamoDB 테이블을 포함한 US-EAST-1 엔드포인트에 의존하는 다른 서비스\n장애 원인 해당 리전의 DynamoDB API 엔드포인트의 DNS에서 문제가 발생했다고 하는데 구체적으로 어떤 내용일까?\nOct 20 2:01 AM PDT We have identified a potential root cause for error rates for the DynamoDB APIs in the US-EAST-1 Region. Based on our investigation, the issue appears to be related to DNS resolution of the DynamoDB API endpoint in US-EAST-1. We are working on multiple parallel paths to accelerate recovery.\nOct 20 8:43 AM PDT We have narrowed down the source of the network connectivity issues that impacted AWS Services. The root cause is an underlying internal subsystem responsible for monitoring the health of our network load balancers. We are throttling requests for new EC2 instance launches to aid recovery and actively working on mitigations.\nOct 20 10:03 AM PDT We continue to apply mitigation steps for network load balancer health and recovering connectivity for most AWS services. Lambda is experiencing function invocation errors because an internal subsystem was impacted by the network load balancer health checks.\nAWS Health에 적힌 내용을 보니 DNS 문제 때문에 IP 변환에 실패했고, 이것이 연쇄적으로 작용하여 과한 트래픽이 발생했다.\n왜 실패한지에 대해서는 모니터링/로드밸런서 서브시스템의 문제인데, 헬스 체크나 비정상적인 상태 감지 등의 문제로 오류가 더 확대된 것이 아닐까?\n2:01 AM PDT 내용을 보면 복구하기 위해 복합적으로 연구 중인 것으로 보이는데 단순 재기동이 아니라 DNS 경로를 다시 설정하고, 로드밸런서를 복구하고, 백로그 처리 등 필요해보인다.\n이로 인해 다른 서비스(EC2, IAM, 특히 Lambda)까지 영향을 준 것으로 보인다.\nReddit에 올라온 하나의 글 How TF did AWS mess up so bad that the entire us-east-1 region is down, all 6 AZs are fucked.\nAWS 장애 관련 내용을 찾다가 레딧에서 하나 글을 보게 되었는데 여러 댓글이 있었다.\nAWS 리전 중에서도 의존도가 상당한데 왜 아직까지도 해결 못하고 있는지.. 복원력을 극대화하려면 여러 리전 간의 중복이 필요하다. 결국 모든 시스템은 어딘가에 단일 장애 지점이 있다. 그 지역은 저주 받았고 100% 유령 나온다. AWS: 코드 업데이트의 75%를 AI를 사용하고 있습니다. 어찌보면 US-EAST-1가 기본 리전이어서 점유율이 가장 높은데 과거에도 피해를 입은 사람들이 대부분인 것 같다.\n영향 받은 서비스 나무위키에 올라온 서비스 목록은 다음과 같다 접기/펼치기 Airtable,Brightspace,Canva,Docker Hub,Fall Guys,Heroku,Hell Let Loose,Parsec,Perplexity,PlayStation Network,Reddit,Roblox,Samsung Account,Signal,Steam,TIDAL,Udemy,Venmo,Vercel,Pokémon GO,닌텐도 홈페이지,더 파이널스,듀오링고,디즈니+,레인보우 식스 시즈 X,로켓 리그,배틀그라운드,브롤스타즈,스냅챗,슬랙,아마존 뮤직,아마존 알렉사,아마존닷컴,위불,코인베이스,클래시 로얄,죠죠의 기묘한 모험 올 스타 배틀 R,클래시 오브 클랜,페이트 그랜드 오더,헤이데이,붐비치,스쿼드 버스터즈,Mo.co,포트나이트,프라임 비디오,플링,에픽게임즈 스토어,VRChat 조치 과정 Oct 20 2:22 AM PDT We have applied initial mitigations and we are observing early signs of recovery for some impacted AWS Services. During this time, requests may continue to fail as we work toward full resolution. We recommend customers retry failed requests. While requests begin succeeding, there may be additional latency and some services will have a backlog of work to work through, which may take additional time to fully process\nOct 20 3:35 AM PDT The underlying DNS issue has been fully mitigated, and most AWS Service operations are succeeding normally now. Some requests may be throttled while we work toward full resolution. Additionally, some services are continuing to work through a backlog of events such as Cloudtrail and Lambda. While most operations are recovered, requests to launch new EC2 instances (or services that launch EC2 instances such as ECS) in the US-EAST-1 Region are still experiencing increased error rates. We continue to work toward full resolution. If you are still experiencing an issue resolving the DynamoDB service endpoints in US-EAST-1, we recommend flushing your DNS caches. We will provide an update by 4:15 AM, or sooner if we have additional information to share.\nOct 20 4:48 AM PDT We continue to work to fully restore new EC2 launches in US-EAST-1. We recommend EC2 Instance launches that are not targeted to a specific Availability Zone (AZ) so that EC2 has flexibility in selecting the appropriate AZ. The impairment in new EC2 launches also affects services such as RDS, ECS, and Glue. We also recommend that Auto Scaling Groups are configured to use multiple AZs so that Auto Scaling can manage EC2 instance launches automatically. We are pursuing further mitigation steps to recover Lambda’s polling delays for Event Source Mappings for SQS. AWS features that depend on Lambda’s SQS polling capabilities such as Organization policy updates are also experiencing elevated processing times. We will provide an update by 5:30 AM PDT.\nOct 20 5:10 AM PDT We confirm that we have now recovered processing of SQS queues via Lambda Event Source Mappings. We are now working through processing the backlog of SQS messages in Lambda queues.\nOct 20 12:15 PM PDT We continue to observe recovery across all AWS services, and instance launches are succeeding across multiple Availability Zones in the US-EAST-1 Regions. For Lambda, customers may face intermittent function errors for functions making network requests to other services or systems as we work to address residual network connectivity issues. To recover Lambda’s invocation errors, we slowed down the rate of SQS polling via Lambda Event Source Mappings. We are now increasing the rate of SQS polling as we experience more successful invocations and reduced function errors. We will provide another update by 1:00 PM PDT.\nOct 20 1:03 PM PDT Service recovery across all AWS services continues to improve. We continue to reduce throttles for new EC2 Instance launches in the US-EAST-1 Region that were put in place to help mitigate impact. Lambda invocation errors have fully recovered and function errors continue to improve. We have scaled up the rate of polling SQS queues via Lambda Event Source Mappings to pre-event levels. We will provide another update by 1:45 PM PDT.\n2:22 AM PDT : 초기 완화 조치\n3:35 AM PDT : 근본적인 DNS 완화가 되었지만 복구를 위해 일부 요청은 제한시킴. EC2 생성 요청은 여전히 오류 발생\n4:48 AM PDT : EC2 생성 요청 시 AZ 선택 안함 권장, Auto Scaling 그룹이 여러 AZ 사용하도록 권장\n5:10 AM PDT : Lambda를 통한 SQS 대기열 처리 복구 완료\n12:15 PM PDT : 여러 AZ에서 EC2 생성 완료 확인, Lambda 오류를 복구하기 위해 SQS 폴링 속도 감소\n1:03 PM PDT : EC2 생성 제한 정도 감소, Lambda 호출 오류 복구, SQS 폴링 속도 확장\n여기까지가 AWS Health에서 보고한 내용이고, 지금은 대부분의 문제는 해결됐을 것 같다.\n비슷한 사례 2018년 AWS 한국 리전 장애 이번처럼 대규모는 아니고 한국 리전에서만 발생했던 사례지만 장애 원인은 DNS 문제로 유사하다. 이때도 Failover가 되던 기업 외에는 서비스가 중단되어 공지만 하고 AWS에서 복구해주기를 기다렸다고 한다. 로컬 규모라서 그런지 1시간 반만에 해결되었다.\n이 사태 이후로 국내 클라우드(NCP, NHN, KT 등)를 추가 또는 전환한 기업도 많다고 한다. 아무래도 국내를 대상으로 하니 비슷한 사례가 발생해도 더 빨리 대응할 수 있기 때문이다.\n2024년 CrowdStrike 전산 마비 사태 클라우드 관련 사태는 아니지만 이런 대규모 사태는 불과 1년 전에도 있었다. 클라우드에 AWS, GCP, Azure가 있듯이 OS는 Windows, Mac이 있는데 여기서 Windows의 문제였다.\n이거는 DNS가 아니라 문제되는 패치가 배포됐기 때문인데, 이 패치가 적용된 곳은 부팅이 되지 않았다.\n마무리 여느 때와 비슷하게 링크드인, 긱뉴스에서 사건사고들을 보고 있었는데 우연히 이 사태를 보게 되었다. 왠지 모를 흥미를 갖고 정보를 모아봤는데 단순한 또는 단순하지 않은 오류 하나가 전 세계를 들썩인 것이 신기했다. 원래 잠들려고 했는데 이것 때문에 눈이 번뜩 떠져서 정보를 수집하게 되었다.\n양날의 검 어느 순간부터 어떤 서비스의 점유율이 높다는 것이 과연 좋기만 한건지 의문을 갖게 되었다. 물론 그 시장에서 우리의 서비스를 더 많이 사용하고 관심을 가져주는 것은 좋은 신호다.\n최근 정부 행정망 화재를 비롯하여 과거 22년 카카오 데이터센터 화재 등을 보면 점유율이 높다는 것은 양날의 검인 것 같다. 나도 그 순간에 카카오톡을 사용 중이었는데 메시지 발송이 안됐고, 당시에는 재직중이면서 알림톡 담당이기도 해서 서비스에 영향이 갔는지 확인했었다. 이렇게 대부분의 국민에게 영향을 끼친 카카오는 최근에 UI 변경 및 숏폼 관련 이슈로 또 비판을 많이 받았다.\n그런데 나를 포함하여 대중들은 아직도 카카오톡을 사용 중이다. 그렇다고 영원히 벗어나지 못할까?\n근본적인 원인 다시 본론으로 돌아와서 위에서 발생한 문제들을 잘 살펴보면 어떤 조직의 사이버 공격이 아니라 내부 설계/운영의 문제로 발생한 사례들이다. 대체로 이번에 처음 문제가 발생한 사례가 아니다. 이미 과거에 발생했었는데 비슷한 문제로 또 발생했고 아직 해결을 못 한 상황이라고 봐야 할 것이다. 해결하는 게 쉽지는 않겠지만 그렇다고 추후 또다시 발생할 확률이 있는데 아무 조치도 하지 않으면 똑같은 결과 혹은 더 큰 리스크를 떠안게 될 것이다.\n일차적으로 비용 문제가 있겠지만 이중화, 가능하다면 멀티 리전도 고려해 볼 수 있다. AWS에 기능이 있는지 모르겠지만 특정 리전에서 장애가 발생했을 때, 다른 리전으로 임시 적용해 줄 수는 없을까?\n내부 스펙에 맞게 헬스 체크, TTL 조정 등을 하고 모니터링을 철저히 하는 방법도 있다. 그나마 다행인 것이 이번 모니터링에서 비정상적인 트래픽이 발생한 것을 빠르게 확인해서 초기 조치가 잘 이루어진 것 같다.\n큰 힘에는 큰 책임이 따른다\n입장문 나온 이후 요약 해당 지역 시간 기준 2025년 10월 19일 오후 11시 48분부터 10월 20일 오후 2시 20분까지 경과했다. 장애 원인은 DynamoDB의 DNS 관리 시스템의 잠재적 Race Condition 때문에 해당 지역 엔드포인트(dynamodb.us-east-1.amazonaws.com)에 대한 잘못된 빈 DNS 레코드가 발생하여 자동화가 복구되지 못했다.\n여기에는 DNS Planner, DNS Enactor 라는 두 구성 요소가 DynamoDB의 자동 DNS 관리 아키텍처 내에 존재한다. 간단하게 Planner는 plan을 생성하고, Enactor는 해당 plan을 내부 DNS 서비스에 적용하는 역할을 한다. 그런데 Enactor는 예상보다 적용되는 시간이 지연되었고, Planner는 계속 plan을 생성했다. 이 과정에서 다른 Enactor는 새로운 plan을 적용한 뒤, \u0026ldquo;이전 plan은 더 이상 유효하지 않다\u0026quot;는 이유로 클린 작업을 수행했다. 결국 지연된 Enactor가 뒤늦게 작업을 마치면서 (older) plan을 적용해버리고, 이전 plan은 클린된 상태여서 DNS 레코드가 빈(empty) 상태로 남는 상황이 발생하게 된다.\nDynamoDB의 리전 엔드포인트(e.g. dynamodb.us-east-1.amazonaws.com)에 대한 IP 주소 레코드가 모두 제거됨\nDynamoDB에 접근할 수 없게 되면서 그 데이터 저장소나 상태 저장으로 사용하는 AWS 시스템들이 정상적으로 작동하지 않았다. 대표적으로 NLB의 헬스 체크 실패가 반복되며, Connection errors나 지연이 증가했다.\n향후 대책으로 언급한 것은 위에서 말한 DNS Planner, DNS Enactor 자동화 기능을 일시적으로 비활성화하고, 새로운 plan이 이전 plan보다 최신인지 체크하는 로직 등 자동화 DNS 관리 시스템의 설계 및 검증 메커니즘을 강화한다고 한다. NLB 는 헬스 체크로 AZ 장애가 발생하면 단일 NLB가 제거할 수 있는 용량을 제한하는 속도 제어 메커니즘을 추가한다고 한다. EC2 는 throttling 메커니즘을 개선하여 대기열의 크기에 따라 들어오는 작업의 속도를 제한시킨다고 한다.\n공식 입장문 \u0026hellip; The root cause of this issue was a latent race condition in the DynamoDB DNS management system that resulted in an incorrect empty DNS record for the service’s regional endpoint (dynamodb.us-east-1.amazonaws.com) that the automation failed to repair.\n\u0026hellip; First, the DNS Planner continued to run and produced many newer generations of plans. Second, one of the other DNS Enactors then began applying one of the newer plans and rapidly progressed through all of the endpoints. The timing of these events triggered the latent race condition.\n\u0026hellip; We are making several changes as a result of this operational event. We have already disabled the DynamoDB DNS Planner and the DNS Enactor automation worldwide. In advance of re-enabling this automation, we will fix the race condition scenario and add additional protections to prevent the application of incorrect DNS plans. For NLB, we are adding a velocity control mechanism to limit the capacity a single NLB can remove when health check failures cause AZ failover. For EC2, we are building an additional test suite to augment our existing scale testing, which will exercise the DWFM recovery workflow to identify any future regressions. We will improve the throttling mechanism in our EC2 data propagation systems to rate limit incoming work based on the size of the waiting queue to protect the service during periods of high load.\n참고 자료 Thomaseccel\nAWS Health\nOutage Summary\nReuters Reddit Namuwiki\n","permalink":"https://4d4cat.com/posts/2025/inside-the-aws-us-east-1-outage/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003eThomas Eccel(구글 Ads 스페셜리스트)의 글을 포함한 외국 저널과 AWS에 Service health 내용을 참고하여 작성했습니다.\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e한국 시간 기준, 2025년 10월 20일 오후 4시 경부터 \u003ccode\u003eUS-EAST-1\u003c/code\u003e 리전을 사용하는 다수의 서비스에서 장애가 발생\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e원인\u003c/strong\u003e: \u003cem\u003eUS-EAST-1 리전의 DynamoDB API 엔드포인트의 DNS\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e영향\u003c/strong\u003e: IAM, DynamoDB 테이블을 포함한 US-EAST-1 엔드포인트에 의존하는 다른 서비스\u003c/p\u003e\n\u003ch2 id=\"장애-원인\"\u003e장애 원인\u003c/h2\u003e\n\u003cp\u003e해당 리전의 DynamoDB API 엔드포인트의 DNS에서 문제가 발생했다고 하는데 구체적으로 어떤 내용일까?\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eOct 20 2:01 AM PDT We have identified a potential root cause for error rates for the DynamoDB APIs in the US-EAST-1 Region. Based on our investigation, the issue appears to be related to DNS resolution of the DynamoDB API endpoint in US-EAST-1. We are working on multiple parallel paths to accelerate recovery.\u003c/p\u003e","title":"2025 AWS US-East-1 region outage: DynamoDB down"},{"content":"개요 하이 리스크였던 첫 퇴사 이후 지금까지 약 15개월 간의 이야기를 정리해보고자 한다.\n커머스 관련 회사에 약 4년 반동안 다니고 건강 문제를 퇴사하게 되었다. 건강이 악화된 이유는 요약하면 스트레스로 인한 건강 악화지만 자세한 이야기는 가까운 지인들만 알고 있다.\n결론부터 말하면 약 150곳의 서류를 넣었고 이 중에서 50개는 결과를 알려주지 않았다.\n면접은 8번을 봤고, 처우 협의까지 간 곳은 1곳이다.\n24년 7월 ~ 9월, 휴식기 일단 내 몸 먼저 어쨋든 퇴사를 하고나서 마음이 한결 가볍긴 했지만 육체는 아직 후유증이 있었나보다. 그래서 처음 2개월은 생각을 비우고 건강 회복에 집중했다. 안그래도 평소에 이것 저것 잡생각이 많은 편이라 맘 먹고 쉬기만 했다.\n새로운 언어를 공부해볼까? 일을 너무 하면 쉬고 싶고, 너무 쉬면 일을 하고싶듯이 어느 순간 쉬는 것도 지루해져서 회사에서 했던 일을 정리할겸 이력서, 경력기술서를 쓰기 시작했다. 더불어 자바, 스프링 말고도 다른 언어에 관심이 생겨서 Typescript(이하 타스), NodeJS(이하 노드)를 공부해봤다.\n사람들이 왜 타스를 사용하게 됐는지, 한국은 자바 점유율이 높은데 노드를 사용하는 이유 등 알게 됐다. 하지만 결국 비용이나 용도에 따라 거기에 맞는 스펙을 사용하는 것 같다. 아무튼 이렇게 새로운 언어도 알아가면서 기존 내 프로젝트도 타스/노드 환경으로 바꿔봤지만 당장 내 경력에는 도움이 안된다는 차가운 현실과 마주했다.\n24년 10월 ~ 12월, 이 길이 맞는 길일까? 불경기 이력서를 준비할 쯤에 알게 된 첫 번째 현실은 불경기였다. 코로나가 시작될 시기에 입사를 했던 나는 그 시절이 운이 좋았고 거품이 정말 많이 껴있던 시절이라는 것을 이 때가 되어서야 알게 되었다. 그리고 경력을 정리하고 요약해보니 내가 지금까지 했던 일들은 단순 반복 업무가 대다수라 경쟁력이 너무 약했다.\nCS 스터디 대충 검색해서 나오는 이력서를 참고하여 무지성으로 내가 했던 업무를 정리하고 제출해봤지만 광탈하기 바빴다. 뭐가 부족한건가 싶을 때, CS 스터디를 발견했고 부족한 기본기도 채울겸 한달 정도 진행해봤다. 하루마다 정해진 시간에 파트너와 전화 면접처럼 본인이 정리한 글에 대해서 문답하는 방식이었다.\n상대방 질문을 예측하여 내가 정리한 글에 대해 알고있어야 하니 기억에 오래 남고, 상대방 글에 대해서도 질문을 준비해야하니 집중은 잘됐다. 하지만 어느 순간부터 블로그에 글만 채우게 되는 느낌을 받아서 탈퇴했다.\n멘토링 앞으로 무엇을 더 준비해야할지 몰라서 인프런 멘토링을 하게 됐다. 두 번의 멘토링을 진행했는데 첫 번째는 내가 백엔드 개발자로서 계속 가는게 맞는지, 아니면 추후 수요가 더 있다고 판단하는 클라우드 엔지니어로 전향할 수 있는지에 대한 것이었다. 약 1시간 동안 여러 정보를 받았지만 결국 백엔드 경력을 살려서 진행하는 것으로 결정했다.\n\u0026quot; .. 신입으로, 또 국비지원으로 클라우드 엔지니어가 된 케이스는 못 본 것 같다. \u0026quot;\n직군 전환도 힘든 부분이지만 일단 내 능력이 부족하다고 생각하여 스프링 관련 국비지원이라도 다녀야할까? 라는 생각도 들었다. OKKY나 Blind에서 고민 상담을 해보니 경력자가 국비지원을 다니면 그만큼 본인 역량에 자신감이 없고, 오히려 마이너스 요소가 될 것이라는 답변을 받았다. 뼈를 맞은 기분\n그 후에 내 이력서/경력기술서에 대한 멘토링을 진행하고 첨삭을 받았다. 이 기점으로 어떻게 써야하는지, 면접관 입장에서 어떤 부분을 보는지 등에 대해 알게 되니 전략을 바꾸게 됐다.\n정량적 수치가 있어야 한다 단순 업무인지, 의미있는 업무인지는 본인이 잘 알거다 개발 과정을 적는 것보다 어떤 문제가 터졌을 때 어떤식으로 해결했는가? 1월 ~ 3월, 첫 사이드 프로젝트 서류는 합격하는데.. 확실히 첨삭을 한 번 받고나니 서류 합격률은 올라갔다. 대신 면접 경험이 부족해서 직무면접에 대한 대응을 제대로 못했다. 내가 이력서에 적은 것들을 내 입으로 설명할 줄 알고, 세부사항에 대해서도 자연스럽게 답할 수 있어야 했는데 생각해보니 내가 했던 일의 대부분이 시키는 것을 그대로 했을 뿐이라서 기술적인 부분은 많이 부족했다.\n내 역량 테스트를 위한 사이드 프로젝트 시작 선퇴사를 하고 다음 이직을 준비할 때 중요한 것 중 하나가 공백기 때 무엇을 했냐가 1순위라고 한다. 내가 과연 다른 사람들과 협업할 때 무리 없이 진행할 수 있을까? 소통은 잘 할 수 있지만, 기술적인 부분을 내가 못 따라갈까봐 걱정이 많았다. 그래도 일단 도전했다.\n어떤 아키텍처로 구현할 것인가? 컨벤션은 어떤식으로? 이런 경우 어떻게 하셨었나요? 여러 부분으로 부딪히고 의견 조율하기까지 서로 양보하는게 많았지만 결과적으로 잘 마무리 되었다. 특히 이 활동을 통해서 프로젝트, 기술적인 면에서 많은 것을 알게 되니 정말 좋은 경험이었다. 회사에서 보지 못했던 아키텍처, 컨벤션, 커뮤니케이션 등을 알게 됐고 내가 부족한 부분이 어떤 것인지 인지하고 공부하게 되는 계기였다.\n스위프 프로젝트 후기 Github/PhotoPic 4월 ~ 7월, 최신 트렌드를 따라가려면 Just do it 사이드 프로젝트 하면서 여러 기술(Spring Boot, JPA, JWT 등)들을 적용해봤지만 잠깐 따라치는 정도일 뿐 실제 서비스를 배포한 경험이 없었다. 그래서 이번 경험을 통해 간단하면서 아직 서비스가 안되고 있는 것을 배포하되, 필요한 기술을 접목시키는 중이다.\n사차원 주머니 이 시기에 클린 코드, 클린 아키텍처도 접하게 되면서 프로젝트에 적용해보고있다. 물론 책 내용 그대로 적용하기보다 오버엔지니어링이 되지 않도록, 내 서비스에 맞는 방향으로 최대한 개발중이다.\nAI한테 의지하는걸까? 내가 아는 선에서 개발할 수 있는 것이 있지만 최신 트렌드나 더 나은 코드, 아키텍처를 경험하기 위해서는 많은 정보가 필요했다. 그러다보니 요즘은 없으면 불편한 AI한테 의지하게 됐다. ChatGPT가 나올 때부터 ChatGPT만 사용했지만 다른 플랫폼(Claude, Gemini, Perplexity 등)과 비교를 해보니 내가 원하는 답변을 Claude가 가장 근접하게 알려주었다. 프롬프트에 따라 원하는 결과가 다르겠지만 내가 작성하는 프롬프트 형식으로는 Claude가 나한테 적합했다.\n그렇게 내가 베이스 코드와 프로젝트 구조를 만들고 방향을 알려주면 AI가 최대한 내가 구현하려는 방향을 인지하고 방향을 알려주었는데 솔직히 이렇게 구현된 것 조차 정답인지도 잘 모르겠다. 확실한 것은 내가 기존에 실무에서 했던 구조와 다르고, 스케일이 좀 더 커지면 AI가 알려준 방식이 확장성과 유지보수 면에서 좋았던 것 같다.\n불경기 + AI 채용시장에서 내 경쟁력이 높은 것은 아니지만 요즘은 기존의 불경기에 더해 여러 AI까지 나오면서 더 채용을 안하는 추세라고 한다. 특히 신입 개발자들은 뽑지 않고 경력직을 뽑으려는 경향이 많아졌다는데, AI를 사용하면서 개발을 하다보니 확실히 주니어급(?) 업무에서는 AI로 충분히 대체가 가능하다고 본다.\n2차 첨삭에도 합격률이 저조해서 다른 멘토를 찾아보고 멘토링을 해봤는데 요즘 IT 회사들의 현실을 구체적으로 알 수 있었다. 내가 했던 업무를 상대방한테 구체적으로 전달 못하는 것과 AI 활용 능력, 특히 Cursor를 이용한 자동화 업무는 점점 늘어나는 중이기 때문에 AI 툴도 잘 다룰 수 있어야 한다.\n8월 ~ 10월, 기술의 깊이 표현 방식의 차이 최근까지 월 1~2회 면접을 봤지만 탈락 소식만 접했다. 무엇이 문제일까? 어떤 부분이 면접관한테 점수를 받지 못한걸까?\n단순히 나보다 더 나은 사람이 있다고 생각할 수 있지만, 서류에 합격하고나서 그 내용을 증명하는 것은 내 몫이다.\n그럼 면접관은 나한테 어떤 기대를 가졌고, 나는 어떤 답변을 말할 수 있어야 했을까?\n면접이 끝나면 집에 가서 어떤 질문이 왔고, 나는 어떤 답변을 했는지 기록해둔다. 이것을 가지고 최근 3개의 면접은 AI한테 피드백을 부탁해봤다. 잘했다고 한 부분이 있었지만, 그렇지 못한 부분이 좀 크리티컬한 것 같다.\n경력에 비해 얕은 경험, 지식 나에 대해 표현하는 방식 얼만큼 알고 얼마나 경험했는가 어떤 경험이라도 어떻게 표현하는지에 따라 듣는 사람이 받아들이는 부분이 다를 수 있다. 한편으로 너무 딥하게 답하면 지루하거나 길게 늘어지는 것 같아서, 최대한 요약해서 말했던 것이 오히려 독이 된 것 같기도 하다.\n블라인드에 있는 IT 게시판에도 질문해봤는데 결론으로는 내 생각과 지식이 주니어 수준에 머물러 있었다. 주니어가 기초 지식을 풍부히 갖고 있어야 한다면 미들급은 문제 해결 경험이 많아야 한다고 한다.\n내가 문제를 해결했던 것들이 있지만, 어떻게 해야 그것을 면접관이 만족하도록 답할 수 있을까?\n지원 현황 서류 지원 건수 약 150건 이직을 준비하기 전에 채용 플랫폼에 이력서를 대충 적었었는데 이것만 보고도 포지션 제안이 많이 왔어서 어깨가 많이 올라가있었다. 최근 들어서 느낀 부분이기도 하지만 당시 코로나 시기였던 문제도 있고 여기 저기 제안해보는 헤드헌터가 많았던 것을 내 역량이라고 착각했었다.\n그것도 모르고 초반 20건은 중견급 이상, 네임드 회사 위주로 지원했다가 광탈 1차 첨삭 이후로 80건 정도는 난사하듯이 지원했는데 여기서 절반은 열람도 하지 않은 기업도 있었다. 2차 첨삭 이후로 조금씩 지원해보면서 합격률이 떨어지면 다시 이력서 수정하는 것을 반복했다. 면접 횟수 8번 중소, 중견, 스타트업 등 있었지만 대체로 스타트업 면접 경험이 정말 좋았다. 특히 두 번째 면접이면서, 스타트업 한 곳은 내가 사이드 프로젝트 진행 중일 때 면접을 봤었는데 실무에서 들어보지도 못한 용어들에 당황했다.\n파티셔닝이나 샤딩을 고려해보지는 않았는지? 카프카를 적용해 볼 생각은 안해보셨나요? 지금 와서 생각해보면 공백기 질문, 전직장 질문, 기술 질문 등 어떤 것 하나 버릴 것 없이 다 준비를 해둬야 면접 때 답변이 술술 나오는 것 같다. 하지만 준비했던 것들 중에 질문이 나온 적은 거의 없던 것 같다.\n이건 안나오겠지.. 이건 나오려나.. 추가로 코딩테스트를 봤던 곳도 있긴한데 프로그래머스 레벨 3~4 기준으로 풀 수만 있다면 문제 없을 것 같다. 평소 코테 준비도 했지만 열심히 준비할 때는 코테 없는 면접이 생기고, 조금 나태해졌을 때는 코테가 있는 곳으로 면접을 본 것 같다.\n하루에 한 문제라도 풀어보기 LeetCode 75 처우 협의 1번 최종 합격된 회사 이전에 처우 협의까지 갔던 스타트업이 있다. 신기하게도 여기서 진행한 면접은 내가 진행했던 업무보다 최근에 경험한 사이드 프로젝트에 대해 더 많이 물어봤다. 다른 스타트업들도 사이드 프로젝트에 대해서 한 두가지는 물어봤다.\n면접 분위기는 굉장히 좋았다. 자만하지 않기 위해 마인드 컨트롤을 자주 하는 편이지만, 면접 도중에도 많이 웃게 되는 분위기여서 면접은 통과할 것이라고 생각을 했다. 물론 불투명한 성장성과 지원한 팀 상황이 채용공고와 다르고, 내가 생각한 최소 연봉을 맞춰주지 못 할 것 같다는 인사팀의 답변으로 채용 취소를 택했다.\n공백기가 있기 때문에 직전 연봉에 비해 깎아 들어가는 것은 어느 정도 감안했고, 심지어 1천만원 이상 깎이는 것까지 생각했다.\n하지만 좀 더 준비해서 다른 기회를 찾는게 좋다고 생각했다.\n공백기의 고통을 이겨내는 방법 내 레벨이 정말 낮구나 24년까지는 단순히 불경기니까 다음 기회가 생길 때까지 계속 기본기와 실무에서 자주 사용하는 기술(Spring Boot, JPA 등) 위주로 공부했다. 하지만 계속 되는 서류/면접탈락에 의지가 많이 꺾였다.\n내 경력은 경쟁력이 없다 새로운 기술을 알아도 실무에서는 사용해본 것이 아니니 의미없다 나를 버티게 해준 하나의 쇼츠 사이드 프로젝트를 끝내고 개인 프로젝트를 진행하면서 공부 중인 어느 날, 회의감과 의지 박탈이 강하게 오던 날이 있었다. 거의 일주일은 아무 것도 하기 싫고 그냥 음악 들으면서 게임이나 했었는데 그것도 지쳐서 유튜브를 끄적이던 중에 이지영 강사님의 쇼츠가 내 알고리즘에 들어왔다.\n하늘이 장차 큰 인물이 될 사람에게는 그 배를 굶주리게 하고 그 뼈를 아프게 하여 그 사람의 그 시련과 고난을 이겨낼 수 있는 기국과 역량이 있는지를 시험하나니, 인생에서 큰 위기를 만나거든 내가 혹시 하늘의 선택을 받은자가 아닌지 돌아보아라\n실제로 맹자 - 고자장에 나오는 어록이다. 이것 말고도 다른 가르침도 많았지만 나에게는 이 문장이 가장 와닿았다. 진짜 세상이 나를 억까하는 일도 있고 내 능력 부족으로 열심히 한다고 생각했지만 큰 결실을 맺지 못한다고 생각했다. 위 문장이 모든 고민을 해결해주지는 않았지만 최소 1%의 희망이 생겼고 이것을 위해 포기하지 않고 계속 부족한 부분을 공부했다.\n내게 힘이 됐던 음악 평소에 여러 장르의 음악을 듣는 편인데, 올해 초부터 음율의 노래가 내 알고리즘에 들어왔다. 단순하게 노래도 좋은데 가사까지 누군가를 응원하는 메시지였다. 누구나 한 번 쯤 경험해본 내 이야기를 노래로 만든 것은 아닌지 생각했다.\n음율 - 파도혁명\n노라조 - 형도 많이 들었다. 이미 옛날부터 입증된 응원곡 부동의 1위이니, 퇴사하기 전에도 들었지만 느껴지는 감정이 달랐다.\n후기 언제부턴가 그랬다 사실 퇴사를 고민한건 퇴사하기 2년 전부터 생각이 있었다. 흔히 성과 지표를 나타내는 KPI에 불만을 갖고 있었기 때문이다. 평가를 잘 받는 방법은 무엇인지 알지만, 그 방법이 내 역량이나 기술적인 면에서 도움이 안됐다. 그래서 어느 해에 맘 먹고 준비해서 평가 A를 받았는데 성취감보다는 허탈감이 컸다. 이 평가 덕분에 조기 진급도 했다.\n이런식으로 해야 A를 준다고?\n다음 해에는 서비스 개선에 도움이 되면서 기술적인 성과를 이루기 위해, 성과가 있었던 업무 위주로 KPI를 계획했다. 하지만 이것을 KPI로 작성하면 안된다고 한다.\n\u0026hellip; 당연히 업무의 일환이기 때문에\n그 당시 KPI로 작성하려했던 것은 다음과 같다\n월 평균 알림톡 발송 성공률 증가 85% -\u0026gt; 95% 등록된 템플릿과 매칭되지 않아서 OO 로직이 추가되면서 예외 처리가 발생한 부분들을 수정하여 월 평균 LMS 발송 성공률 증가 89% -\u0026gt; 98% 기타 버그 픽스 다른 회사는 어떤 기준으로 KPI를 작성하는지 모르겠다. 사실 내가 생각하는 성과와 회사에서 말하는 성과가 다를지도 모른다. 그렇기 때문에 결국 회사에서 말하는 성과에 맞춰서 업무를 하고 그 다음 KPI를 작성하게 된 것 같다.\n한 번 무너질 뻔 했지만 그것으로 얻은 것 이후에는 서론에도 언급했던 이야기 때문에 스트레스가 쌓이고, 신체에도 무리가 오면서 건강을 챙기기 위해 퇴사를 했다. 다행히도 같이 일했던 동료들과 이사님 또한 많은 걱정과 아쉬움을 내비췄다. 특히 이사님께서는 건강을 챙기고나서 다음 구직 때까지의 나를 걱정하시면서 조언도 아끼지 않으셨다. 확실히 말씀해주셨던 것처럼 시장은 많이 추웠지만 이 또한 내 도전이고 미래의 나를 위한 경험치라고 생각한다.\n나보다 경쟁력이 뛰어난 사람은 많다. 이제 1년 3개월이 지나가지만 이 시간동안 얻은 것이 많다. 과거의 나보다 경험치가 쌓인 것은 맞기 때문에 많이 성장했다고 느낀다. 물론 아직 부족할 뿐\n결과로 증명하겠다. 누군가 이 글을 본다면 포기하지 말라고 말하고싶다. 이런 나도 계속 도전 중인데 나보다 더 안좋은 상황에서 고민 중인 사람은 몇 없을것이라 생각한다. 최근에 신인감독 김연경을 우연히 보게 됐는데 재밌더라\n??: 해보자 해보자 해보자 해보자 후회하지 말고\n","permalink":"https://4d4cat.com/posts/2025/career-gap-story-one-year-later/","summary":"\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e하이 리스크였던 첫 퇴사 이후 지금까지 약 15개월 간의 이야기를 정리해보고자 한다.\u003c/p\u003e\n\u003cp\u003e커머스 관련 회사에 약 4년 반동안 다니고 건강 문제를 퇴사하게 되었다. 건강이 악화된 이유는 요약하면 \u003ccode\u003e스트레스로 인한 건강 악화\u003c/code\u003e지만 자세한 이야기는 가까운 지인들만 알고 있다.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e결론부터 말하면 약 150곳의 서류를 넣었고 이 중에서 50개는 결과를 알려주지 않았다.\u003c/p\u003e\n\u003cp\u003e면접은 8번을 봤고, 처우 협의까지 간 곳은 1곳이다.\u003c/p\u003e\n\u003ch2 id=\"24년-7월--9월-휴식기\"\u003e24년 7월 ~ 9월, 휴식기\u003c/h2\u003e\n\u003ch3 id=\"일단-내-몸-먼저\"\u003e일단 내 몸 먼저\u003c/h3\u003e\n\u003cp\u003e어쨋든 퇴사를 하고나서 마음이 한결 가볍긴 했지만 육체는 아직 후유증이 있었나보다. 그래서 처음 2개월은 생각을 비우고 건강 회복에 집중했다. 안그래도 평소에 이것 저것 잡생각이 많은 편이라 맘 먹고 쉬기만 했다.\u003c/p\u003e","title":"퇴사하고 공백기가 1년이 넘은 현재까지의 이야기"},{"content":"🔔 사이드 프로젝트 근황\n🔔 추후 개선 방향\n이전 요약 사차원 주머니 프로젝트를 처음에는 개발 공부용으로만 생각했지만, 실제 서비스를 운영하는 방식으로 방향을 전환했다. 이전에는 템플릿 엔진(Thymeleaf)으로 프론트엔드를 구성하고, 소셜 로그인 과 일별 구글 검색 키워드 순위 기능을 구현했었다.\n작업 내용 개요 하지만 이 방식은 백엔드 개발자로서 API Response 처리에 대한 고민이 부족했고, 실제 배포를 해도 사용자에게 큰 의미가 없는 웹사이트가 되어 방향성을 잃은 상태였다.\n그래서 과감하게 백엔드 개발에만 집중하고 프론트엔드는 AI에게 맡기는 방식으로 구조를 변경했다.\n구체적인 내용은 다음과 같다.\n1차 리팩토링 실제 서비스를 운영하려면 핵심 기능이 필요했는데, 먼저 Lorem Picsum 사이트를 벤치마킹했다. 이 사이트는 페이지를 새로고침할 때마다 새로운 이미지를 보여주며, URL 파라미터로 이미지 크기나 흑백 필터 같은 옵션을 적용할 수 있다.\n여기서 영감을 받아, 저작권 없는 영상이나 음원을 랜덤으로 제공하는 서비스를 구현했다. Lorem Picsum 처럼 랜덤 이미지 서비스도 계획에 있지만, 다른 곳에서 쉽게 찾아볼 수 없는 차별화된 서비스를 먼저 만들고 싶었다.\n백엔드 Language: Java 21 Framework/Library: Spring Boot, Spring AOP CI/CD: Github Actions Infra: Gabia Cloud AI: Claude Code, Gemini CLI 프론트엔드 Language: TypeScript Framework/Library: React 18, Vite UI/Styling: Tailwind CSS, Radix UI, shadcn/ui Infra: Replit 템플릿 엔진을 제거하고 API 서버를 독립된 형태로 구성했지만, 사용자에게 보여줄 프론트엔드 화면이 필요했다. 그렇다고 내가 프론트엔드까지 직접 개발하며 풀스택 개발자를 지향해야 했을까? 물론 프론트엔드까지 직접 개발할 수 있다면 더할 나위 없이 좋겠지만, 나는 AI에게 맡기기로 했다.\nAI 활용(Replit AI)에 대해서는 다른 포스팅에서 더 자세히 다루겠다.\n서비스 소개 세상에는 이미 많은 서비스가 있지만, 나는 최대한 기존 서비스와 겹치지 않는 특별한 기능을 제공하고 싶었다. 그래서 Lorem Picsum 사이트와 원리는 비슷하지만, 랜덤 영상과 음원을 제공한다는 차별점을 둔 서비스를 핵심 기능으로 제공하고 있다.\nRandom 영상, 음원은 Pixabay에서 제공하는 소스로 저작권 없고 자유롭게 사용하도록 제공한다. 기본적으로 이미지가 더 많기 때문에 랜덤 이미지 노출도 적용할 예정이다. Last Message 추가로 Last Message 라는 서비스를 만들었다. 이 서비스는 사이트에 접속한 사용자가 정해진 길이 내에서 원하는 메시지를 남길 수 있는 기능이다. 어떤 내용을 남길지는 온전히 사용자의 자유에 맡겨서 비속어든 칭찬이든 상관없이, 마지막에는 어떤 메시지가 남게 될지 궁금해서 만들게 되었다. Keyword 다음으로 키워드 통계 기능을 구현 중이다. 현재는 Google Trends 데이터를 실시간으로 가져와 보여주는 방식인데 추후 실시간 순위와 일별 키워드 순위를 분리하고, 클릭 이벤트를 통해 더 다양한 정보를 제공하는 것이 목표이다.\n마무리 올해 초만 해도 나는 개발 공부를 위해 프로젝트의 존재 자체에만 의미를 두었다. 하지만 시간이 지나면서 좀 더 실무에 가깝게 프로젝트를 발전시키고, 백엔드 개발자로서 제 역할을 확실히 정의해야겠다는 생각으로 바뀌었다.\n그래서 템플릿 엔진을 사용하는 대신 프론트엔드와 백엔드를 분리하여 백엔드 코드에 더 집중하고, 프론트엔드는 AI에게 맡겨 내가 원하는 스타일로 구현하도록 했다. 이를 통해 백엔드 역량 과 AI 활용 능력 을 함께 기를 수 있게 되었다.\n이 포스팅에는 현재 어떤 서비스를 운영 중인지, 왜 리팩토링을 하게 됐는지에 대해 작성했다. 다음 사차원 주머니 포스팅에는 구체적으로 어떤 스킬이 적용했는지를 작성하고, AI를 어떤 식으로 활용했는지에 대해서도 생각 중이다.\n","permalink":"https://4d4cat.com/posts/2025/4d4cat-service1/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e사이드 프로젝트 근황\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e추후 개선 방향\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"이전-요약\"\u003e이전 요약\u003c/h2\u003e\n\u003cp\u003e사차원 주머니 프로젝트를 처음에는 개발 공부용으로만 생각했지만, 실제 서비스를 운영하는 방식으로 방향을 전환했다. 이전에는 템플릿 엔진(\u003cstrong\u003e\u003ccode\u003eThymeleaf\u003c/code\u003e\u003c/strong\u003e)으로 프론트엔드를 구성하고, \u003cstrong\u003e\u003ccode\u003e소셜 로그인\u003c/code\u003e\u003c/strong\u003e 과 \u003cstrong\u003e\u003ccode\u003e일별 구글 검색 키워드 순위\u003c/code\u003e\u003c/strong\u003e 기능을 구현했었다.\u003c/p\u003e\n\u003ch3 id=\"작업-내용\"\u003e\u003ca href=\"/posts/2024/4d4cat1\"\u003e작업 내용\u003c/a\u003e\u003c/h3\u003e\n\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e하지만 이 방식은 백엔드 개발자로서 \u003cstrong\u003e\u003ccode\u003eAPI Response\u003c/code\u003e\u003c/strong\u003e 처리에 대한 고민이 부족했고, 실제 배포를 해도 사용자에게 큰 의미가 없는 웹사이트가 되어 방향성을 잃은 상태였다.\u003c/p\u003e\n\u003cp\u003e그래서 과감하게 백엔드 개발에만 집중하고 프론트엔드는 AI에게 맡기는 방식으로 구조를 변경했다.\u003c/p\u003e","title":"사차원 주머니 프로젝트 근황 1"},{"content":"🔔 Windows에서 WSL 설치 시 발생할 수 있는 문제\n개요 요즘 흔히 말하는 \u0026ldquo;바이브 코딩\u0026ldquo;에 관심을 가져서 이것저것 하던 와중, Claude에서 Claude Code를 발표한 것을 보았다. 커서 AI를 이용한 개발은 봤지만 Claude Code는 기본적으로 CLI 방식이어서 신선했다.\nIntelliJ나 VSCode에 통합하여 사용할 수도 있기 때문에 데스크탑(Windows)에 적용해보려고 WSL을 설치했다. 그런데 문제는 이때부터 시작되었다.\n1. WSL 설치 WSL(Windows Subsystem for Linux)는 Windows에서 Linux 환경을 사용할 수 있게 해주는 기능이다. 가상머신 없이도 Windows 안에서 리눅스 환경으로 작업할 수 있다는 정도로 알고 있었다.\nClaude Code 셋업 페이지 안내대로 설치를 시작했다.\nnpm config set os linux 1-1. 재부팅의 함정 WSL 기능을 제대로 적용하려면 재부팅해야 한다고 해서 재시작했다. 실제 문제는 여기서 시작되었다.\n부팅하면서 로딩이 돌다가 갑자기 멈췄다!?!? 강제로 재시작해봐도 같은 현상이 반복되었고, WSL 설치 문제라고 생각하여 곧바로 모바일로 여기저기 찾아봤다.\nClaude를 통해 확인해보니 추정되는 원인과 해결방법을 안내해주었다\n가능한 원인들: - Hyper-V 충돌: WSL2는 Hyper-V를 사용하는데, 기존 가상화 소프트웨어와 충돌 가능 - BIOS/UEFI 설정: 가상화 기능이 제대로 활성화되지 않았거나 Secure Boot 설정 문제 - Fast Startup 기능: Windows의 빠른 시작 기능이 WSL과 충돌 해결 방법: 1. 안전 모드로 부팅하여 WSL 기능 비활성화 2. BIOS에서 가상화 기능 설정 확인 3. Fast Startup 비활성화 ... 우선 안전모드부터 들어가려고 했는데\u0026hellip;\n2. 안전모드부터 막힌 절망의 순간 보통 시스템에 문제가 생기면 부팅 시 \u0026ldquo;PC 복구\u0026rdquo; 관련 화면으로 넘어간다. 복구 화면에서 진단을 하지만 문제 해결이 안 되면 다시 재시작 또는 안전모드 재시작을 선택하게 된다.\n이렇게 되니 이상한 악순환이 만들어졌다.\n안전모드 시도 → PC 복구 화면 진입 → 진단 실패 → 재시작(또는 안전모드) → 무한반복 3. 마지막 희망 - 시스템 복구 WSL 기능을 비활성화하려면 최소한 안전모드라도 들어가야 하는데 그러지 못하니 답답했다.\nClaude가 제안한 다른 방법으로 BIOS에서 가상화 기능(AMD라서 SVM 모드)을 비활성화해봤지만, 이것도 안전모드와 동일하게 PC 복구 화면으로만 넘어갔다.\n진짜 운이 없는 건가 싶던 찰나에, 약 2시간 전 Windows 업데이트를 설치했던 것이 떠올랐다. 이를 기점으로 시스템 복구가 가능했다! WSL 설치 후 바로 업데이트할까 생각도 했었는데, 다행히 내 판단이 훌륭한 백업 환경을 만들어주었다.\n마무리 시스템 복구 전후 차이가 WSL 설치 여부뿐인 환경이라 원인이 명확했다.\n대부분의 개발은 Mac을 이용하지만 단순 작업이나 일상은 데스크탑을 쓰고 있는데 맥북 꺼내기 귀찮고 Windows에도 세팅해두면 편할 거야라는 안일한 생각이 이 사태를 만들었다. 앞으로 개발 관련은 무조건 Mac으로만 할 것 같다.\nWSL이 내 데스크탑에 말썽을 부린 정확한 원인은 좀 더 봐야겠지만, Windows 11 호환성이나 레지스트리 충돌 등이 원인일 수 있다고 한다. 정말로 Windows 환경에서 WSL을 다시 설치하게 되는 날이 온다면, 그때 자세히 알아볼 것 같다.\n그때까지는 만나지 말자.\n시간이 지나고.. 최근 윈도우 11 업데이트 관련 이슈를 겪고 나서 이 글을 다시 보게 되었는데 이것도 Windows 11 RAID 컨트롤러 관련 이슈이지 않을까? 윈도우 10에서 11로 업그레이드 이후 어느 날 부팅 자체가 멈춘 일\n","permalink":"https://4d4cat.com/posts/2025/windows-wsl/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eWindows에서 WSL 설치 시 발생할 수 있는 문제\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"개요\"\u003e개요\u003c/h2\u003e\n\u003cp\u003e요즘 흔히 말하는 \u0026ldquo;\u003cstrong\u003e바이브 코딩\u003c/strong\u003e\u0026ldquo;에 관심을 가져서 이것저것 하던 와중, \u003ccode\u003eClaude\u003c/code\u003e에서 \u003cstrong\u003eClaude Code\u003c/strong\u003e를 발표한 것을 보았다. 커서 AI를 이용한 개발은 봤지만 \u003cstrong\u003eClaude Code\u003c/strong\u003e는 기본적으로 \u003cstrong\u003eCLI\u003c/strong\u003e 방식이어서 신선했다.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIntelliJ\u003c/strong\u003e나 \u003cstrong\u003eVSCode\u003c/strong\u003e에 통합하여 사용할 수도 있기 때문에 데스크탑(Windows)에 적용해보려고 WSL을 설치했다. 그런데 문제는 이때부터 시작되었다.\u003c/p\u003e\n\u003ch2 id=\"1-wsl-설치\"\u003e1. WSL 설치\u003c/h2\u003e\n\u003cp\u003eWSL(Windows Subsystem for Linux)는 Windows에서 Linux 환경을 사용할 수 있게 해주는 기능이다. 가상머신 없이도 Windows 안에서 리눅스 환경으로 작업할 수 있다는 정도로 알고 있었다.\u003c/p\u003e","title":"Windows에서 WSL 설치하다가 컴퓨터 부팅도 못 할뻔한 후기"},{"content":"Introduction The Atom Syndication Format is an XML language used for web feeds. A web feed (also called \u0026rsquo;news feed\u0026rsquo; or \u0026lsquo;RSS feed\u0026rsquo;) is a data format used for providing users with frequently updated content. Content distributors syndicate a web feed, thereby allowing users to subscribe a channel to it. A typical scenario of web-feed use might involve the following: a content provider publishes a feed link on its site which end users can register with an aggregator program (also called a feed reader or a news reader) running on their own machines.\nHow it works The atom feed, in the \u0026lsquo;feed.xml\u0026rsquo; file uses Liquid to build the XML. The file contains the following code.\nInstallation Step 1. Download the file feed.xml\nStep 2. Save the file in the root of your Jekyll project\nStep 3. Add the following line to your head/\u0026lsquo;head.html\u0026rsquo; include:\n\u0026lt;link rel=\u0026#34;alternate\u0026#34; type=\u0026#34;application/rss+xml\u0026#34; href=\u0026#34;{{ site.url }}/feed.xml\u0026#34;\u0026gt; 이 블로그도 지킬 페이지로 되어있어서 위 href 값이 내 URL로 되어있다. 따라서 아래 링크를 들어가 실제 코드를 복사하는 것을 추천한다.\n원본 링크 https://jekyllcodex.org/without-plugin/rss-feed/\n","permalink":"https://4d4cat.com/posts/2025/jekyll-rss/","summary":"\u003ch3 id=\"introduction\"\u003e\u003cstrong\u003eIntroduction\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003eThe Atom Syndication Format is an XML language used for web feeds. A web feed (also called \u0026rsquo;news feed\u0026rsquo; or \u0026lsquo;RSS feed\u0026rsquo;) is a data format used for providing users with frequently updated content. Content distributors syndicate a web feed, thereby allowing users to subscribe a channel to it. A typical scenario of web-feed use might involve the following: a content provider publishes a feed link on its site which end users can register with an aggregator program (also called a feed reader or a news reader) running on their own machines.\u003c/p\u003e","title":"Jekyll RSS를 생성할 때, jekyll-feed로 적용해보기"},{"content":" 이전 포스트 를 읽고 오시는 것을 추천드립니다.\n이번 프로젝트를 하면서 가장 좋은 경험 중 하나가 개발 문화였다. 백엔드 개발자로서 같은 백엔드와, 다른 직군들과 소통을 할 때 어떤 용어를 사용하고 협업하는게 이전 직장에서는 경험하지 못했던 새로운 경험이었다.\n이전 직장에서는 개발 용어집은 있었지만 제대로 적용하지 않은 부분이 많았고, 코드 리뷰가 없고 알아서 잘 했겠지 하는 마인드로 테스트는 작업한 사람만 했다. 그래서 나는 중요한 작업의 경우 내가 1차로 테스트 후 PR을 올릴 때, 전체 플로우랑 중요하게 봤으면 하는 부분들을 언급해서 요청했었다.\n소통 개발 소통은 오프라인 회의가 아니라면 디스코드를 많이 이용했다. 기획자, 디자이너는 피그마에서 소통하긴 했지만 자세한 부분은 디스코드에서 얘기를 더 많이 했다. 아무래도 프론트, 백엔드 둘다 피그마가 어색하고 디스코드 화면 공유를 통해 피그마를 볼 수도 있어서 그런 것 같다.\n컨벤션 코드 컨벤션은 구글 자바 컨벤션을 참고했는데 그대로 적용하지는 않고 우선 컨벤션에 맞춰서 개발하다가 불편하다고 느낀 부분은 서로 피드백하여 우리만의 룰을 적용했다. 커밋 컨벤션이나 이슈 템플릿 등은 우테코 저장소를 참고했다.\n코드 컨벤션 static import 허용 테스트 코드만 와일드 카드 허용 ApiResponse Error인 경우에는 {code, errorMsg}만 success인 경우에는 전달할 값(id, name, …)만 //200 { id: 1 name: hi } //200 { id: 1 key: hi } //200 이외 400 ... { code: 1234 errorMsg: \u0026#34;MEMBER_NOT_FOUND\u0026#34; } 이슈 ## 📋 추가 기능 ## 🛠️ 작업 내용 - [ ] [TODO1] - [ ] [TODO2] PR ## 🚀 작업 내용 설명 ## 📢 그 외 ## 📌 관련 이슈 테스트 코드 @DisplayName(\u0026#34;사용자 정보 조회\u0026#34;) void select_member(){} @DisplayName(\u0026#34;사용자 정보 수정 - 유저 없는 경우\u0026#34;) void update_member_memberNotFoundException(){} @DisplayName(\u0026#34;토큰 생성\u0026#34;) void createToken_customException(){} 구글 자바 컨벤션\n우테코-팀바팀-컨벤션\nCI/CD 워크플로우 개인 프로젝트 할 때 직접 빌드하고 배포 서버에 파일을 옮기는 일은 정말 귀찮다. 지금에서 생각해보면 CI/CD를 왜 미리 구현 안했을까 라는 생각이 든다. 뽀또픽에서는 Github Actions를 이용하여 구축을 했다. 같은 팀원의 말로 본인은 Jenkins만 사용하고 Github Actions는 처음 써보는데 이게 더 편한 것 같다고 했다. 나는 아직 Jenkins를 안써봤지만 채용공고를 보면 Jenkins가 좀 더 많이 쓰이는 것 같다.\n각자 역할이 있겠지만 차이를 간단히 확인해보니 Github Actions는 큰 프로젝트의 경우 비용이 많이 들어가고 Github에 상당히 의존적이다. 그래서 깃허브를 쓰거나 소~중규모 프로젝트의 경우에는 적합하지만 그렇지 않은 경우 Jenkins를 쓰는 것 같다. Jenkins가 오픈 소스인 것도 있고 설정이 복잡할 수는 있지만 그만큼 커스터마이징이 자유로워서 실무에서 더 사용되는 것 같다.\n테스트 코드 이전 직장에서는 테스트 코드 작성 환경이 열악했다. 우선 테스트 프레임워크인 JUnit이나 Mockito가 없었고, 내부망을 사용하고 기존에 없던 의존성을 추가하는 것이 상당히 제한적이었다.\n그래서 테스트 코드를 작성할 때 로컬 환경에서 최대한 use case들을 확인해야했다. 물론 환경 문제로 로컬/개발 환경에서 테스트가 불가능한 경우 어쩔 수 없이 스테이징 서버까지 코드를 올리면서 테스트를 했다.\n이런 상황을 피하고자 이번 프로젝트에서 테스트 코드를 적용해보았다. 레포지토리, 서비스, 컨트롤러 단으로 각 단위테스트를 진행하면서 어떤 플로우로, 어떤 함수가 쓰이는지 알게 되는 경험이었다. 물론 아직 익숙하지 않지만 Mock을 만든다? 어떤 상황이 나타난다고 가정한다? 는 식의 테스트 방식이 굉장히 재밌었다.\n후기 이번 활동에서 얻은 가장 좋은 경험이 개발 문화였다. 같은 직군의 팀원이 졸업 예정인 대학생이었는데 정말 많이 배웠다. 오히려 이 친구한테 내가 어떤 것을 알려줘야 서로 윈윈일지 걱정될 정도로 기술적인 부분을 많이 배웠다. 그리고 다른 직군 팀원들도 각자의 분야에서 전문성을 잘 보여줘서 개발하는 시간은 알찼다.\n물론 처음부터 끝까지 어떤 불만이 없던 것은 아니지만 결과적으로 보면 필요한 부분에서 싸우고 조율해서 다같이 수료하는 결과를 얻은 것이라고 생각한다. 덕분에 생각하는 범위가 넓어지고 백엔드에서는 어떤 것들이 더 있고, 다른 직군 관점에서는 어떻게 생각할 수도 있다는 등 이타적인 것들이 많이 생긴 것 같다.\n","permalink":"https://4d4cat.com/posts/2025/photopic-engineering-culture/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/2025/photopic-aws-lambda\"\u003e이전 포스트\u003c/a\u003e 를 읽고 오시는 것을 추천드립니다.\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e이번 프로젝트를 하면서 가장 좋은 경험 중 하나가 개발 문화였다. 백엔드 개발자로서 같은 백엔드와, 다른 직군들과 소통을 할 때 어떤 용어를 사용하고 협업하는게 이전 직장에서는 경험하지 못했던 새로운 경험이었다.\u003c/p\u003e\n\u003cp\u003e이전 직장에서는 개발 용어집은 있었지만 제대로 적용하지 않은 부분이 많았고, 코드 리뷰가 없고 알아서 잘 했겠지 하는 마인드로 테스트는 작업한 사람만 했다. 그래서 나는 중요한 작업의 경우 내가 1차로 테스트 후 PR을 올릴 때, 전체 플로우랑 중요하게 봤으면 하는 부분들을 언급해서 요청했었다.\u003c/p\u003e","title":"Photopic) 여기서 경험한 개발 문화"},{"content":" 이전 포스트 를 읽고 오시는 것을 추천드립니다.\n내가 뽀또픽을 진행하면서 맡은 업무 중 이미지 업로드가 있는데, 이 작업을 하면서 왜 Lambda를 쓰게 됐는지 기록하고자 한다.\n초기 설계 (시작 ~ 1차 MVP) 인프라는 AWS 서버를 사용하니 이미지 저장을 위해 S3 스토리지를 이용하고 필요한 부분이 생기면 Lambda도 사용하자는 계획이 있었다. 예시로 추후 리사이징 기능을 추가한다면 메인 서버에서 처리하기보다 람다에 리사이징을 맡기는 것이 트래픽 분산이나 유지보수에 좋다고 판단을 했다.\n하지만 규모가 작은 서비스에서 굳이 Lambda를 쓰지 않아도 충분히 주요 기능을 구현했기 때문에 Lambda는 안쓰는 것으로 결정했다. 또, 1차 MVP에서 이미지 확장자를 jpg(jpeg), png, gif만 허용하기로 결정했기 때문에 람다는 쓰지 않는 방향으로 결정했다.\n중간 수정 (1차 MVP ~ 2차 MVP) 오히려 S3 스토리지를 Cloudflare의 R2 스토리지로 변경해야했다. 왜냐하면 AWS Lambda에서 제공하는 무료 한도량이 우리 서비스에서 발생할 수 있는 읽기/쓰기 횟수보다 높았기 때문이다. 그리고 R2 SDK가 AWS S3 기반이기 때문에 호환성도 좋았다.\n최종 설계 (2차 MVP ~ 고도화) 아이폰은 기본 확장자가 Heic, Heif 확장자를 쓰기 때문에 아이폰 유저를 고려해서라도 확장자를 추가해야했다. 하지만 자바 라이브러리에서 Heic 확장자를 호환해주지 않기 때문에 람다를 사용하여 heic-convert 라이브러리를 사용하는 함수를 만들었다\n최종 이미지 업로드 확장자: jpg(jpeg), png, gif, webP, heic, heif 워크플로우 jpg(jpeg), png, gif : 서버에서 R2 스토리지에 업로드\nwebP, heic, heif : 람다로 해당 파일을 스트림으로 전송 후, jpeg로 변환하여 R2 스토리지에 업로드\n후기 서비스의 핵심 기능 중 하나인 이미지 업로드를 구현했는데 값진 경험이었다. 최종 서비스에 이미지 업로드를 할 때, 각 이미지는 최대 10MB, 총 9개까지만 업로드할 수 있다. 로컬에서는 금방 업로드했지만 프리티어 인스턴스에서 올릴 때는 최소 3초, 최대 1분 30초까지 걸렸다. 최소는 트래픽이 없을 때 저용량 이미지 2장, 최대는 여러 테스트 도중에 평균 4MB 9장을 올렸을 때 발생한 시간이다.\n실제 운영하는 것처럼 적용한다면 충분한 메모리에, 쓰레드 풀도 적용하여 업로드를 해야할 것 같다. 나는 동기식으로 적용했지만 상황에 따라서는 Cloudflare의 Image API처럼 리사이징할 때는 비동기도 필요해보인다. 좀 더 큰 서비스에서 업로드 같은 기능을 관리할 때는 어떤식으로 적용하는걸까?\n","permalink":"https://4d4cat.com/posts/2025/photopic-aws-lambda/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"/posts/2025/photopic\"\u003e이전 포스트\u003c/a\u003e 를 읽고 오시는 것을 추천드립니다.\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e내가 뽀또픽을 진행하면서 맡은 업무 중 이미지 업로드가 있는데, 이 작업을 하면서 왜 Lambda를 쓰게 됐는지 기록하고자 한다.\u003c/p\u003e\n\u003ch2 id=\"초기-설계-시작-\"\u003e\u003cstrong\u003e초기 설계 (시작 ~ 1차 MVP)\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e인프라는 AWS 서버를 사용하니 이미지 저장을 위해 S3 스토리지를 이용하고 필요한 부분이 생기면 Lambda도 사용하자는 계획이 있었다. 예시로 추후 리사이징 기능을 추가한다면 메인 서버에서 처리하기보다 람다에 리사이징을 맡기는 것이 트래픽 분산이나 유지보수에 좋다고 판단을 했다.\u003c/p\u003e\n\u003cp\u003e하지만 규모가 작은 서비스에서 굳이 Lambda를 쓰지 않아도 충분히 주요 기능을 구현했기 때문에 Lambda는 안쓰는 것으로 결정했다. 또, 1차 MVP에서 이미지 확장자를 jpg(jpeg), png, gif만 허용하기로 결정했기 때문에 람다는 쓰지 않는 방향으로 결정했다.\u003c/p\u003e","title":"Photopic) AWS Lambda를 왜 써야할까?"},{"content":"🔔 프로젝트 참여 과정\n🔔 프로젝트 소개\n참여 계기 이전 직장에서 레거시한 경험만 있다 보니 스킬업을 하기 위해, 내 레벨이 시장에서 어느 정도인지 측정하기 위해 참가했다. 스위프 8기 프로그램은 서류접수와 참가비만 있으면 됐다. 팀 빌딩 때 프로필을 보니 대부분이 20대이며, 취준생과 신입들이 많았다.\n프로젝트 기간은 팀 빌딩기간부터 마지막 발표하는 날까지 1/15 ~ 3/15, 두달간 진행했다.\n프로젝트와 팀원 우리 팀은 기획1, 디자이너1, 프론트2, 백엔드2(나 포함)로 구성했다. 스위프에서 팀 빌딩을 위해 각 프로필을 노션 페이지를 공유해줬는데 거기서 지금의 팀원들을 만났다. 프로젝트 아이디어는 여러 의견이 있었지만 \u0026ldquo;SNS에 어떤 사진을 올리는게 좋을까?\u0026ldquo;라는 의문을 해결하기 위해 사진을 투표하고 공유할 수 있는 \u0026ldquo;뽀또픽\u0026quot;이라는 서비스를 만들었다.\n사이트\nGithub\n기술 스택 Java 21 Spring boot 3.4.2 MySQL, H2 JPA, Security, Oauth/JWT Github Actions Nginx AWS Postman/Rest Docs 인사이트 내가 이 프로그램에 참가하면서 얻고자한 것은 프로그램 수료와 이전 팀에서는 안써본 스킬들을 적용해보는 것이었다. 특히 요즘 채용공고를 보면 자주 보이는 JPA와 Security 부분은 꼭 적용해보고 싶었다. 단기 프로젝트에 적용해본 것이 실무와 비교했을 때 큰 차이는 없더라도, 혼자서 적용해보는 것과 여러 명이 모인 프로젝트에서 적용해본 경험의 차이는 있다고 본다.\n이 프로젝트를 하면서 겪은 기술적인 고민에 대해 앞으로 더 쓸 예정이다.\n개발 문화 AWS Lambda를 왜 쓰게 되었나 (AWS S3 vs Cloudflare R2) 디비 커넥션 누수 Security 커스텀 JPA 쿼리 호출, 지연 로딩 후기 스위프 프로그램에 불만이 있는데 너무 방치한다는 생각이 들었다. 웹 호스팅 혜택, 네이버 클라우드 크레딧 제공, 기업 회의실 대여 등 있었지만 실제로 우리팀이 이용한건 없었다. 우선 프론트는 vercel을 쓴다고 해도 API 서버도 도메인이 필요했기 때문에 웹호스팅 혜택은 의미없다고 생각했다.\n그리고 네이버 클라우드 크레딧 제공해주는것은 정말 좋은 혜택인데 기간이 6개월이었고, 장기간 서비스할 계획이라면 그 이후에는 다른 서버로 변경하거나 사비로 네이버 클라우드 연장을 했을 것이다. 하지만 우리 서비스 특성 상으로 쓰는 장비가 서버, DB 정도만 쓸 것 같아서 AWS 프리 티어를 이용하기로 했다.\n다른 팀들은 모르겠지만 우리팀 입장에서는 스위프에서 제공해주는 것들을 활용할 필요성을 느끼지 못했다. 그래서 그런지 나는 참가비 24.9만원(실제 9.9만, 리워드 15만)이 아깝다고 생각했다. 스위프가 아니더라도 다른 무료 프로그램도 있고 신청과정에 서류나 면접도 있기 때문에 좀 더 퀄리티 있는 프로젝트를 할 수 있지 않을까싶다.\n팀원들과 프로젝트 하는 것에는 불만이 거의 없다. 처음 보는 사람들과 8주간 소통하면서 끝까지 함께 하고, 수료한다는 것만으로도 대단하다고 생각한다. 특히 프로젝트 설계 과정부터 고도화 단계까지 내가 실무에서 경험해보지 못한 단계나 용어들이 신기하게 다가왔다.\n","permalink":"https://4d4cat.com/posts/2025/photopic/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e프로젝트 참여 과정\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e프로젝트 소개\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"참여-계기\"\u003e\u003cstrong\u003e참여 계기\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e이전 직장에서 레거시한 경험만 있다 보니 스킬업을 하기 위해, 내 레벨이 시장에서 어느 정도인지 측정하기 위해 참가했다. 스위프 8기 프로그램은 서류접수와 참가비만 있으면 됐다. 팀 빌딩 때 프로필을 보니 대부분이 20대이며, 취준생과 신입들이 많았다.\u003c/p\u003e\n\u003cp\u003e프로젝트 기간은 팀 빌딩기간부터 마지막 발표하는 날까지 1/15 ~ 3/15, 두달간 진행했다.\u003c/p\u003e\n\u003ch2 id=\"프로젝트와-팀원\"\u003e\u003cstrong\u003e프로젝트와 팀원\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e우리 팀은 기획1, 디자이너1, 프론트2, 백엔드2(나 포함)로 구성했다. 스위프에서 팀 빌딩을 위해 각 프로필을 노션 페이지를 공유해줬는데 거기서 지금의 팀원들을 만났다. 프로젝트 아이디어는 여러 의견이 있었지만 \u0026ldquo;SNS에 어떤 사진을 올리는게 좋을까?\u0026ldquo;라는 의문을 해결하기 위해 사진을 투표하고 공유할 수 있는 \u0026ldquo;뽀또픽\u0026quot;이라는 서비스를 만들었다.\u003c/p\u003e","title":"Photopic) 스위프 8기 단기 프로젝트 후기, 뽀또픽"},{"content":"🔔 ResponseEntity 개요\n🔔 다른 방법과 비교\nWhat? Why? 스프링 3 버전부터 도입되었으며, 스프링 MVC에서 HTTP 응답을 다루는 주요 클래스 중 하나이다. HTTP 응답의 전체 내용을 제어할 수 있도록 해주며, 이를 통해 다음과 같은 요소들을 설정할 수 있다.\n상태 코드 (HTTP Status Code)\n: 200 OK, 404 Not Found, 500 Internal Server Error 등\n헤더 (Headers) : 응답 헤더에 특정 값을 추가/수정\n본문 (Body)\n: 실제로 클라이언트에게 전달될 데이터\n@GetMapping(\u0026#34;/example\u0026#34;) public ResponseEntity\u0026lt;String\u0026gt; getExample() { String body = \u0026#34;Hello, World!\u0026#34;; HttpHeaders headers = new HttpHeaders(); headers.add(\u0026#34;Custom-Header\u0026#34;, \u0026#34;CustomValue\u0026#34;); return new ResponseEntity\u0026lt;\u0026gt;(body, headers, HttpStatus.OK); } @ResponseBody 객체를 직렬화하여 반환하는 어노테이션으로, 이것만 사용할 경우 헤더를 유연하게 설정할 수 없다.\n@ResopnseStatus를 사용하여 헤더를 설정할 수 있지만, 어노테이션을 별도로 추가해야하는 단점이 있다.\n@GetMapping(\u0026#34;/accounts/{id}\u0026#34;) @ResponseStatus(HttpStatus.OK) @ResponseBody public Account handle() { // ... } WebFlux의 Mono, Flux 반응형 프로그래밍을 지원하는 WebFlux를 사용하는 경우, Mono나 Flux를 반환 타입으로 사용할 수 있다. 이는 비동기 및 논블로킹 방식으로 HTTP 응답을 처리할 때 유용하다.\n@GetMapping(\u0026#34;/reactive\u0026#34;) public Mono\u0026lt;ResponseEntity\u0026lt;String\u0026gt;\u0026gt; getReactive() { return Mono.just(ResponseEntity.ok(\u0026#34;Hello, Reactive World!\u0026#34;)); } 따라서, SpringMVC 환경에서는 ResponseEntity를 사용하는 것이 바디는 물론 헤더를 제어하는데에 더 유연하게 조작이 가능하다. 물론 HTTP 응답을 세밀하게 제어하는 정도가 아니면 ResponseBody를 사용하거나 RestController를 사용하는 것이 더 좋을 수 있다.\n자료 출처 ChatGPT\n","permalink":"https://4d4cat.com/posts/2025/response-entity/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eResponseEntity 개요\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e다른 방법과 비교\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"what-why\"\u003e\u003cstrong\u003eWhat? Why?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e스프링 3 버전부터 도입되었으며, 스프링 MVC에서 HTTP 응답을 다루는 주요 클래스 중 하나이다. HTTP 응답의 전체 내용을 제어할 수 있도록 해주며, 이를 통해 다음과 같은 요소들을 설정할 수 있다.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e상태 코드 (HTTP Status Code)\u003cbr\u003e\n: 200 OK, 404 Not Found, 500 Internal Server Error 등\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e헤더 (Headers) \u003cbr\u003e\n: 응답 헤더에 특정 값을 추가/수정\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e본문 (Body)\u003cbr\u003e\n: 실제로 클라이언트에게 전달될 데이터\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@GetMapping\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/example\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eResponseEntity\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003egetExample\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ebody\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Hello, World!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eHttpHeaders\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eheaders\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eHttpHeaders\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Custom-Header\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;CustomValue\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eResponseEntity\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebody\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eheaders\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eHttpStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eOK\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"responsebody\"\u003e\u003cstrong\u003e@ResponseBody\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e객체를 직렬화하여 반환하는 어노테이션으로, 이것만 사용할 경우 헤더를 유연하게 설정할 수 없다.\u003cbr\u003e\n\u003ccode\u003e@ResopnseStatus\u003c/code\u003e를 사용하여 헤더를 설정할 수 있지만, 어노테이션을 별도로 추가해야하는 단점이 있다.\u003c/p\u003e","title":"springframework.http.ResponseEntity"},{"content":"🔔 어노테이션 종류와 용도\nRequiredArgsConstructor Lombok 라이브러리에서 제공하는 어노테이션으로, 클래스 내의 final 필드나 @NonNull 어노테이션이 붙은 필드들을 초기화하는 생성자를 자동으로 생성해준다. 보일러 플레이트 코드를 줄여준다는 것에 이점이 있다.\n@Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; private final EmailService emailService; // Lombok이 아래와 같은 생성자를 자동으로 생성해줍니다. // public UserService(UserRepository userRepository, EmailService emailService) { // this.userRepository = userRepository; // this.emailService = emailService; // } // 서비스 로직... } Autowired와 차이점 실제로 기능은 동일하지만, 코드량의 차이가 있다.\n생성자 주입\nprivate final로 선언을 했더라도 생성자를 만들고 @Autowired를 설정해주기 때문에 불필요한 코드 증가\n필드 주입 필드 주입을 하게 되면 final로 선언이 불가하여 의존성이 변경될 수도 있다.\nTransactional Spring 프레임워크에서 제공하는 어노테이션으로, 메서드 또는 클래스 수준에서 트랜잭션의 경계를 정의한다.\n데이터 무결성 보장 여러 데이터베이스 작업이 하나의 트랜잭션으로 묶여 원자성을 유지\n롤백 관리 예외 발생 시 자동으로 트랜잭션을 롤백하여 데이터의 일관성을 유지\n트랜잭션 전파 메서드 호출 간 트랜잭션의 전파 방식을 설정 가능\n주요 속성 propagation: 트랜잭션 전파 방식을 정의\nREQUIRED(Default)\n: 현재 트랜잭션이 존재하면 해당 트랜잭션을 사용하고 없으면 새로운 트랜잭션을 시작, 일관성 있는 트랜잭션 관리가 필요할 때 적합\nREQUIRES_NEW : 현재 트랜잭션이 존재하면 일시 중단하고 새로운 트랜잭션을 시작, 기존 트랜잭션과 독립적으로 동작해야 하는 작업(Log, Audit 등)\nSUPPORTS : 현재 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행, 트랜잭션에 의존하지 않는 읽기 작업\nMANDATORY\n: 반드시 기존 트랜잭션 내에서 실행되어야 한다. 트랜잭션이 없으면 예외 발생, 반드시 트랜잭션 내에서 실행되어야 하는 중요 작업\nisolation: 트랜잭션의 격리 수준을 설정\nREAD_COMMITTED: 커밋된 데이터만 읽을 수 있다. 더티 리드를 방지 SERIALIZABLE: 가장 높은 격리 수준. 트랜잭션이 순차적으로 실행되는 것처럼 동작하여 모든 동시성 문제를 방지 timeout: 트랜잭션이 타임아웃되기까지의 시간을 설정. 설정된 시간이 초과되면 트랜잭션이 롤백\nreadOnly: 트랜잭션이 읽기 전용인지 설정\n일반 사례 기본 트랜잭션: @Transactional만 사용하거나 propagation과 isolation을 기본값으로 설정 읽기 전용 작업: @Transactional(readOnly = true) 독립적인 트랜잭션 필요 시: @Transactional(propagation = Propagation.REQUIRES_NEW) 트랜잭션 타임아웃 설정: @Transactional(timeout = 30) @Service public class OrderService { private final OrderRepository orderRepository; private final PaymentService paymentService; // 생성자 주입 (예제 간결화를 위해 생략) @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 60, readOnly = false, rollbackFor = { PaymentException.class, OrderException.class }, noRollbackFor = { ValidationException.class } ) public void placeOrder(Order order) throws PaymentException, OrderException, ValidationException { orderRepository.save(order); paymentService.processPayment(order.getPaymentDetails()); // ... } } MappedSuperclass JPA에서 제공하는 어노테이션으로, 공통 매핑 정보를 상속받기 위한 슈퍼클래스를 정의할 때 사용한다. @Entity로 선언되지 않은 클래스에 적용되며, 이를 상속받는 엔티티 클래스들이 상속받은 필드들을 매핑할 수 있다.\n@MappedSuperclass를 상속받는 클래스는 @Entity가 아니므로, 데이터베이스 테이블에 직접 매핑되지 않지만 상속받는 @Entity 클래스들은 @MappedSuperclass의 필드를 매핑합니다.\nAbstract Class? 공통 필드를 정의하는 역할만 하므로, 직접 인스턴스를 생성할 필요가 없다. 추상 클래스로 정의하면서 설계의 명확성과 인스턴스화 방지할 수 있다.\nEntityListeners 엔티티의 라이프사이클 이벤트(예: @PrePersist, @PostLoad 등)를 처리하기 위한 리스너 클래스를 지정할 때 사용한다. 엔티티가 생성, 업데이트, 삭제될 때 특정 로직을 자동으로 실행할 수 있다. 감사(Audit) 기능 (생성 시간, 수정 시간, 생성자, 수정자 등)의 필드를 자동으로 관리할 때 유용\npublic class AuditListener { @PrePersist public void prePersist(Object target) { if (target instanceof BaseEntity) { BaseEntity entity = (BaseEntity) target; entity.setCreatedAt(LocalDateTime.now()); entity.setUpdatedAt(LocalDateTime.now()); } } @PreUpdate public void preUpdate(Object target) { if (target instanceof BaseEntity) { BaseEntity entity = (BaseEntity) target; entity.setUpdatedAt(LocalDateTime.now()); } } } @MappedSuperclass @EntityListeners(AuditListener.class) public abstract class BaseEntity { // 필드 정의 private Long id; private LocalDateTime createdAt; private LocalDateTime updatedAt; // getters and setters } @Entity @Table(name = \u0026#34;products\u0026#34;) public class Product extends BaseEntity { private String name; private Double price; // getters and setters } 자료 출처 ChatGPT\n","permalink":"https://4d4cat.com/posts/2025/4d4cat-etc1/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e어노테이션 종류와 용도\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"requiredargsconstructor\"\u003e\u003cstrong\u003eRequiredArgsConstructor\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eLombok\u003c/code\u003e 라이브러리에서 제공하는 어노테이션으로, 클래스 내의 \u003ccode\u003efinal\u003c/code\u003e 필드나 \u003ccode\u003e@NonNull\u003c/code\u003e 어노테이션이 붙은 필드들을 초기화하는 생성자를 자동으로 생성해준다. 보일러 플레이트 코드를 줄여준다는 것에 이점이 있다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Service\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nd\"\u003e@RequiredArgsConstructor\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUserService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003efinal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eUserRepository\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003euserRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003efinal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eEmailService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eemailService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e// Lombok이 아래와 같은 생성자를 자동으로 생성해줍니다.\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e// public UserService(UserRepository userRepository, EmailService emailService) {\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e//     this.userRepository = userRepository;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e//     this.emailService = emailService;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e// }\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 서비스 로직...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"autowired와-차이점\"\u003e\u003cstrong\u003eAutowired와 차이점\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e실제로 기능은 동일하지만, 코드량의 차이가 있다.\u003c/p\u003e","title":"4D4cat) 이 어노테이션은 왜 쓰는걸까?"},{"content":"🔔 Oauth2 적용 전\n🔔 Oauth2 실습\n개요 최근 개인 프로젝트에서 네이버 로그인을 위해 필요한 부분을 설정하고 로직을 구현했다. 개발 가이드에 맞게 각 단계별로 진행을 했으나\u0026hellip;\n아무리 봐도 과정 하나 하나를 내 손으로 직접 구현하는게 맞는건가? 라는 의문을 시작으로 좀 더 효율적으로 구현하는 방법을 찾아봤고, 그렇게 Oauth 2.0으로 로그인하는 방식을 찾게 되었다.\nOauth2 적용 전 코드 스타일을 떠나서 지금 와서 보니 마치 Oauth 1.0처럼 구현을 하고 있었다. 나 또한, 각 플로우마다 하드 코딩은 줄이고 변수나 함수를 재활용하기 위해, 보안 요소를 생각하면서 구현했었다.\n서드 파티 관련 API를 호출할 때, 응답 값을 Json으로 가져오기 위해 JsonNode를 많이 사용했는데 동일하게 적용하려고 보니 소스가 너무 길어진 것 같다.\nprivate final NaverApiService naverApiService; private final SnsInfoService snsInfoService; private final UserService userService; public String getNaverOauth2LoginUrl(HttpServletRequest request) { String authorizeUrl = BASE_URL+\u0026#34;/authorize\u0026#34;; String callbackUrl = getServiceUrl()+\u0026#34;/login/naver/callback\u0026#34;; String state = NakjiUtil.generateTokenState(); request.getSession().setAttribute(\u0026#34;state\u0026#34;, state); return UriComponentsBuilder .fromHttpUrl(authorizeUrl) .queryParam(\u0026#34;response_type\u0026#34;, \u0026#34;code\u0026#34;) .queryParam(\u0026#34;client_id\u0026#34;, secrets.naver().naverId()) .queryParam(\u0026#34;redirect_uri\u0026#34;, callbackUrl) .queryParam(\u0026#34;state\u0026#34;, state) .toUriString(); } public String processNaverLogin(LoginProfile profile, Oauth2AccessToken tokenInfo) { SnsInfo checkSnsInfo = snsInfoService.getOrCreateUser(profile, tokenInfo); Optional\u0026lt;User\u0026gt; user = userService.getUserByUserId(checkSnsInfo.getSnsId()); if (user.isPresent()) { // Update user info } else { // Create user } return \u0026#34;redirect:/\u0026#34;; } public String processNaverLoginCallback(String state, String code, HttpSession session) { Oauth2AccessToken token = getToken(state, code, session); if (token == null) { throw new BadRequestException(ErrorCode.INVALID_PARAMETER); } try (Response response = naverApiService.naverProfileConnection(token.tokenType(), token.accessToken())) { if (response.status() == 200) { JsonNode resultJson = NakjiUtil.readBody(response.body().asInputStream()); if (\u0026#34;00\u0026#34;.equals(resultJson.get(\u0026#34;resultcode\u0026#34;)) \u0026amp;\u0026amp; \u0026#34;success\u0026#34;.equals(resultJson.get(\u0026#34;message\u0026#34;))) { JsonNode profileInfo = resultJson.get(\u0026#34;response\u0026#34;); LoginProfile profile = new LoginProfile(\u0026#34;naver\u0026#34;, Optional.ofNullable(profileInfo.get(\u0026#34;id\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;), Optional.ofNullable(profileInfo.get(\u0026#34;nickname\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;), Optional.ofNullable(profileInfo.get(\u0026#34;gender\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;), Optional.ofNullable(profileInfo.get(\u0026#34;age\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;) ); return processNaverLogin(profile, token); } else { throw new Exception(); } } else { log.info(\u0026#34;NaverLoginService.getNaverProfileUrl Request Status: {}, Body: {}\u0026#34;, response.status(), response.body().toString()); //throw new BadRequestException(\u0026#34;Bad request with status: \u0026#34; + response.status()); } } catch (Exception e) { log.error(\u0026#34;NaverLoginService.getNaverProfileUrl Error: \u0026#34;, e); } return \u0026#34;\u0026#34;; } public Oauth2AccessToken getToken(String state, String code, HttpSession session) { try (Response response = getGenerateToken(state, code, session)) { if (response.status() == 200) { JsonNode resultJson = NakjiUtil.readBody(response.body().asInputStream()); Oauth2AccessToken resultToken = new Oauth2AccessToken( Optional.ofNullable(resultJson.get(\u0026#34;access_token\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;), Optional.ofNullable(resultJson.get(\u0026#34;refresh_token\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;), Optional.ofNullable(resultJson.get(\u0026#34;token_type\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;), Optional.ofNullable(resultJson.get(\u0026#34;expires_in\u0026#34;)).map(JsonNode::asInt).orElse(0), Optional.ofNullable(resultJson.get(\u0026#34;error\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;), Optional.ofNullable(resultJson.get(\u0026#34;error_description\u0026#34;)).map(JsonNode::asText).orElse(\u0026#34;\u0026#34;) ); if (\u0026#34;invalid_request\u0026#34;.equals(resultToken.errorCode())) { throw new BadRequestException(ErrorCode.INVALID_PARAMETER); } else if (\u0026#34;unauthorized_client\u0026#34;.equals(resultToken.errorCode())) { throw new UnauthorizedException(ErrorCode.INVALID_AUTH_CODE); } return resultToken; } else { log.info(\u0026#34;NaverLoginService.getToken Request Status: {}, Body: {}\u0026#34;, response.status(), response.body().toString()); //throw new BadRequestException(\u0026#34;Bad request with status: \u0026#34; + response.status()); } } catch (Exception e) { log.error(\u0026#34;NaverLoginService.getToken Error: \u0026#34;, e); } return null; } private Response getGenerateToken(String state, String code, HttpSession session) { String storedState = (String)session.getAttribute(\u0026#34;state\u0026#34;); if (!state.equals(storedState)) { throw new UnauthorizedException(ErrorCode.INVALID_AUTH_PATH); } return Feign.builder() .encoder(new GsonEncoder()) .decoder(new GsonDecoder()) .target(NaverGenerateTokenClient.class, BASE_URL) .generateToken(secrets.naver().naverId(), secrets.naver().naverKey(), state, code); } Oauth2 적용 후 gpt와 일부 블로그를 찾아보면서 내 방식대로 적용해봤다. 우선 로그인은 잘 되고, 초기 세팅만 해두면 그 이후에는 소스를 재활용하거나 조금만 더 추가하는 것으로 해결될 것 같다.\n다음에는 페이지마다 역할 부여하는 것과 jwt을 이용한 로그인을 추가할 예정이다.\nsecurity.yml spring: security: oauth2: client: registration: naver: client-id: ${NAVER_CLIENT_ID} client-secret: ${NAVER_CLIENT_SECRET} authorization-grant-type: authorization_code redirect-uri: ${SERVICE_URL}${NAVER_REDIRECT_URI} scope: name, nickname, email, gender, birthyear, profile_image provider: naver: authorization-uri: https://nid.naver.com/oauth2.0/authorize token-uri: https://nid.naver.com/oauth2.0/token user-info-uri: https://openapi.naver.com/v1/nid/me user-name-attribute: response SecurityConfig @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -\u0026gt; csrf.disable()) .authorizeHttpRequests(authorize -\u0026gt; authorize ... ) .oauth2Login(oauth2 -\u0026gt; oauth2 .loginPage(\u0026#34;/login\u0026#34;) .defaultSuccessUrl(\u0026#34;/\u0026#34;) .failureUrl(\u0026#34;/login?error\u0026#34;) .userInfoEndpoint(userInfo -\u0026gt; userInfo .userService(customOAuth2UserService) ) ) .logout(logout -\u0026gt; logout .logoutSuccessUrl(\u0026#34;/\u0026#34;) .invalidateHttpSession(true) .deleteCookies(\u0026#34;JSESSIONID\u0026#34;) ) .build(); } } OAuthAttributes @Builder public record OAuthAttributes(Map\u0026lt;String, Object\u0026gt; attributes, String nameAttributeKey, User user) { public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map\u0026lt;String, Object\u0026gt; attributes) { if (\u0026#34;naver\u0026#34;.equals(registrationId)) { return ofNaver(userNameAttributeName, attributes); } else { return null; } } private static OAuthAttributes ofNaver(String userNameAttributeName, Map\u0026lt;String, Object\u0026gt; attributes) { Map\u0026lt;String, Object\u0026gt; response = (Map\u0026lt;String, Object\u0026gt;) attributes.get(\u0026#34;response\u0026#34;); return OAuthAttributes.builder() .attributes(attributes) .nameAttributeKey(userNameAttributeName) .user(User.builder() .oauthId((String) response.get(\u0026#34;id\u0026#34;)) .provider(\u0026#34;naver\u0026#34;) .name((String) response.get(\u0026#34;name\u0026#34;)) .nickname((String) response.get(\u0026#34;nickname\u0026#34;)) .profileImage((String) response.get(\u0026#34;profile_image\u0026#34;)) .email((String) response.get(\u0026#34;email\u0026#34;)) .gender((String) response.get(\u0026#34;gender\u0026#34;)) .birthYear((String) response.get(\u0026#34;birthYear\u0026#34;)) .role(Role.USER) .build()) .build(); } public User toEntity() { return user; } } OAuth2UserService @Service @RequiredArgsConstructor public class CustomOAuth2UserService implements OAuth2UserService\u0026lt;OAuth2UserRequest, OAuth2User\u0026gt; { private final UserRepository userRepository; private final HttpSession httpSession; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2UserService\u0026lt;OAuth2UserRequest, OAuth2User\u0026gt; delegate = new DefaultOAuth2UserService(); OAuth2User oAuth2User = delegate.loadUser(userRequest); String registrationId = userRequest.getClientRegistration().getRegistrationId(); String userNameAttributeName = userRequest.getClientRegistration() .getProviderDetails() .getUserInfoEndpoint() .getUserNameAttributeName(); OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); User user = saveOrUpdate(attributes); httpSession.setAttribute(\u0026#34;user\u0026#34;, new SessionUser(user)); return new DefaultOAuth2User( Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())), attributes.getAttributes(), attributes.getNameAttributeKey()); } private User saveOrUpdate(OAuthAttributes attributes) { User userInfo = attributes.getUser(); User target = userRepository.findByOauthId(userInfo.getOauthId()) .map(entity -\u0026gt; entity.update(userInfo.getNickname(), userInfo.getProfileImage(), userInfo.getEmail())) .orElse(attributes.toEntity()); return userRepository.save(target); } } 후기 여담이지만 키값을 관리할 때, 해당 설정 파일을 저장소에는 커밋하지 않았다. 다른 PC에서 접속할 때는 개인 클라우드에서 키값이 있는 파일을 불러와서 사용했는데, 이번에 oauth를 경험하면서 점점 불편해질 수 있을 것 같다.\n그래서 dotenv를 사용하고 기존 환경설정 값들은 env 파일에서 가져오는 방식으로 수정했다. 큰 차이는 없을 수 있지만, 내가 환경설정할 때 어떤 구조로 설정했는지는 볼 수 있게 된 것이 개선점이다.\n참고 자료 Spring io Velog\nChatGPT\n","permalink":"https://4d4cat.com/posts/2025/oauth2-deepdive/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eOauth2 적용 전\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003eOauth2 실습\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"개요\"\u003e\u003cstrong\u003e개요\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e최근 개인 프로젝트에서 네이버 로그인을 위해 필요한 부분을 설정하고 로직을 구현했다. 개발 가이드에 맞게 각 단계별로 진행을 했으나\u0026hellip;\u003c/p\u003e\n\u003cp\u003e아무리 봐도 \u003cem\u003e\u003cstrong\u003e과정 하나 하나를 내 손으로 직접 구현하는게 맞는건가?\u003c/strong\u003e\u003c/em\u003e 라는 의문을 시작으로 좀 더 효율적으로 구현하는 방법을 찾아봤고, 그렇게 \u003ccode\u003eOauth 2.0\u003c/code\u003e으로 로그인하는 방식을 찾게 되었다.\u003c/p\u003e\n\u003ch2 id=\"oauth2-적용-전\"\u003e\u003cstrong\u003eOauth2 적용 전\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e코드 스타일을 떠나서 지금 와서 보니 마치 \u003ccode\u003eOauth 1.0\u003c/code\u003e처럼 구현을 하고 있었다. 나 또한, 각 플로우마다 하드 코딩은 줄이고 변수나 함수를 재활용하기 위해, 보안 요소를 생각하면서 구현했었다.\u003c/p\u003e","title":"OAuth2 Deepdive"},{"content":"🔔 Oauth2? 그럼 Oauth1도?\n🔔 Oauth2 동작 과정\nRFC6749(Oauth 2.0) 문서를 토대로 요약하여 정리한 글이니, 자세한 내용은 해당 문서를 참고해주시기 바랍니다.\nWhat? Why? 인증(authentication) 이 아닌 인가(authorization)에 초점을 맞추며, 리소스 소유자가 클라이언트에게 자신의 리소스에 대한 제한적 접근 권한을 부여할 수 있도록 하는 프레임워크\n역할 Resource Owner\n보호된 리소스(Protected Resource)의 실제 소유자 혹은 주체 ex) 사용자의 계정, 사용자 데이터 Resource Server\n보호된 리소스를 호스팅(저장 및 관리)하고 있는 서버 클라이언트가 제공하는 액세스 토큰을 검증하고, 유효한 토큰이면 리소스 접근을 허용 Client\n리소스 소유자를 대신하여 리소스에 접근하려고 요청하는 애플리케이션 ex) 웹 애플리케이션, 모바일 앱, 데스크톱 프로그램 등 Authorization Server\n클라이언트의 권한 요청을 인증/인가하고, 액세스 토큰(혹은 리프레시 토큰)을 발급하는 서버 Resource Owner와 Client 사이에서 안전하게 토큰을 주고받도록 중재 Oauth 1.0 vs 2.0 기존 Oauth 1.0은 다음과 같은 문제점이 있었는데, 이를 개선하기 위해 Oauth 2.0이 만들어졌다.\n1.0 복잡한 서명 프로세스\n각 요청마다 서명을 생성 유연성 부족\n특정한 인증 플로우에 국한되어 있음 ex) 모바일 앱, 단일 페이지 애플리케이션(SPA) 등 보안상의 제약\nHTTPS와 같은 전송 계층 보안을 완전히 대체할 수 없음 서명 관리 자체가 보안 위험 요소 가능성 확장성의 한계\n2.0 단순화된 인증 프로세스\n서명 기반 인증을 제거하고 HTTPS를 기본 보안 계층으로 사용 다양한 인증 플로우 지원\n다양한 인증 플로우(그랜트 타입)를 지원 ex) Authorization Code, Implicit, Client Credentials 등 확장성과 유연성 향상\n모듈식 아키텍처를 채택하여 필요에 따라 확장 가능 ex) PKCE(Proof Key for Code Exchange) 향상된 보안 기능\n토큰 기반 인증 토큰의 범위(scope) 세분화 더 나은 사용자 경험\nSPA나 네이티브 모바일 애플리케이션에서도 쉽게 통합 가능 흐름도 +--------+ +---------------+ | |--(A)- Authorization Request -\u0026gt;| Resource | | | | Owner | | |\u0026lt;-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant --\u0026gt;| Authorization | | Client | | Server | | |\u0026lt;-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------\u0026gt;| Resource | | | | Server | | |\u0026lt;-(F)--- Protected Resource ---| | +--------+ +---------------+ 출처\n(A) 클라이언트가 승인 요청 클라이언트는 리소스 소유자에게 직접 승인 요청을 할 수 있다. 인증 요청은 리소스 소유자 직접 이루어질 수도 있고 중개자로서 인증 서버를 통해 간접적으로 이루어질 수도 있다.\n(B) 인증 부여를 받음 클라이언트는 리소스 소유자의 인증을 나타내는 자격 증명인 인증 부여를 받는다. 명세서에 정의된 네 가지 유형 중 하나를 사용하거나 확장 부여 유형을 사용하여 표현된다. 인증 부여 유형은 클라이언트가 인증을 요청하는 데 사용하는 방법과 인증 서버가 지원하는 유형에 따라 다르다.\n(C) 엑세스 토큰 요청 클라이언트는 권한 부여 서버로 인증하고 권한 부여를 제시하여 액세스 토큰을 요청한다.\n(D) 엑세스 토큰 발급 인증 서버는 클라이언트를 인증하고 인증 부여를 검증한 후에, 유효한 경우 액세스 토큰을 발급한다.\n(E) 인증 클라이언트는 리소스 서버에서 보호되는 리소스를 요청하고 액세스 토큰을 제시하여 인증한다.\n(F) 요청 처리 리소스 서버는 액세스 토큰의 유효성을 검사하고 유효한 경우, 요청을 처리한다.\n다음 장에 이어서..\n자료 출처 RFC 6749 ChatGPT\n","permalink":"https://4d4cat.com/posts/2025/oauth2/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eOauth2? 그럼 Oauth1도?\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003eOauth2 동작 과정\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eRFC6749(Oauth 2.0) 문서를 토대로 요약하여 정리한 글이니, 자세한 내용은 \u003ca href=\"https://datatracker.ietf.org/doc/html/rfc6749\"\u003e해당 문서\u003c/a\u003e를 참고해주시기 바랍니다.\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"what-why\"\u003e\u003cstrong\u003eWhat? Why?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cem\u003e인증(authentication)\u003c/em\u003e 이 아닌 \u003ccode\u003e인가(authorization)\u003c/code\u003e에 초점을 맞추며, 리소스 소유자가 클라이언트에게 자신의 리소스에 대한 제한적 접근 권한을 부여할 수 있도록 하는 프레임워크\u003c/p\u003e\n\u003ch3 id=\"역할\"\u003e\u003cstrong\u003e역할\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eResource Owner\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e보호된 리소스(Protected Resource)의 실제 소유자 혹은 주체\u003c/li\u003e\n\u003cli\u003eex) 사용자의 계정, 사용자 데이터\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eResource Server\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e보호된 리소스를 호스팅(저장 및 관리)하고 있는 서버\u003c/li\u003e\n\u003cli\u003e클라이언트가 제공하는 액세스 토큰을 검증하고, 유효한 토큰이면 리소스 접근을 허용\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eClient\u003c/strong\u003e\u003c/p\u003e","title":"OAuth2"},{"content":"🔔 BodyPublisher, BodyHandler 역할\n🔔 HttpClient 실습\nBodyPublisher? BodyHandler? BodyPublisher와 BodyHandlers는 요청과 응답의 본문을 처리하는 데 있어 상호 보완적인 역할을 한다. BodyPublisher는 클라이언트가 서버로 전송할 데이터를 정의하고, BodyHandlers는 서버로부터 받은 데이터를 어떻게 처리할지를 정의한다. 이 두 구성 요소를 적절히 활용하면, HTTP 통신을 더욱 유연하고 효율적으로 구현할 수 있다.\nBodyPublisher HTTP 요청의 본문을 제공 데이터를 전송할 때 어떻게 제공할지 정의 BodyPublishers.ofString(String): 문자열 데이터 BodyPublishers.ofByteArray(byte[]): 바이트 배열 데이터 BodyPublishers.ofFile(Path): 파일 데이터 BodyPublishers.ofInputStream(Supplier\u0026lt;InputStream\u0026gt;): 동적으로 생성되는 입력 스트림 BodyHandler HTTP 응답의 본문을 처리하는 방법을 정의 서버로부터 받은 응답 데이터를 어떻게 처리할지 정의 BodyHandlers.ofString(): 문자열 데이터 BodyHandlers.ofByteArray(): 바이트 배열 데이터 BodyHandlers.ofFile(Path): 파일 데이터 BodyHandlers.ofInputStream(): InputStream으로 데이터 제공 HttpResponse.BodyHandler 클래스는 BodyHandler를 생성하기 위한 여러 가지 편리한 정적 팩토리 메서드를 제공한다. 이들 중 다수는 응답 바이트가 완전히 수신될 때까지 메모리에 축적되며, 그 후 응답 바이트는 상위 수준의 Java 유형(ofString, ofByteArray 등)으로 변환된다.\nGet 동기 BodyHandler는 응답 상태 코드와 헤더가 사용 가능해지면 응답 본문 바이트가 수신되기 전에 호출 public void get(String uri, String param) throws Exception { String uriWithParam = uri+\u0026#34;?\u0026#34;+param; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uriWithParam)) .build(); HttpResponse\u0026lt;String\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(\u0026#34;Sync status code: \u0026#34;+ response.statusCode()); } 비동기 비동기 API는 사용 가능해지면 HttpResponse로 완료되는 CompletableFuture를 즉시 반환 CompletableFuture.thenApply(Function) 메서드를 사용하여 HttpResponse를 본문 유형, 상태 코드 등에 매핑할 수 있다. join(): 비동기 작업이 끝날 때까지 대기 public void getAsync(String uri, String param) { String uriWithParam = uri+\u0026#34;?\u0026#34;+param; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uriWithParam)) .build(); client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::statusCode) .thenAccept(statusCode -\u0026gt; System.out.println(\u0026#34;Async status code: \u0026#34; + statusCode)) .join(); } Post discard BodyHandler는 관심이 없는 응답 본문을 수신하고 삭제하는 데 사용될 수 있다. public void post(String uri, String data) throws Exception { HttpClient client = HttpClient.newBuilder().build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uri)) .POST(HttpRequest.BodyPublishers.ofString(data)) .build(); HttpResponse\u0026lt;?\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.discarding()); System.out.println(\u0026#34;Post status code: \u0026#34; + response.statusCode()); } 실습 예제 네이버 로그인 호출에 대한 예제이다. 개인 프로젝트에 적용 중인 설정으로 requestUrl과 redirectUrl은 이미 설정되어있는 값으로 사용했다. 실제 결과는 페이지가 리다이렉트하면서 text/html 타입을 반환하기 때문에 상태코드만 출력하도록 했다.\npublic static void main(String[] args) { HttpClientExample httpClientExample = new HttpClientExample(); String requestUrl = \u0026#34;https://nid.naver.com/oauth2.0/authorize\u0026#34;; String redirectUrl = \u0026#34;https://4d4cat.site/login/naver/callback\u0026#34;; String clientKey = \u0026#34;네이버키값\u0026#34;; NaverLoginRequest requestParam = new NaverLoginRequest(\u0026#34;code\u0026#34;, clientKey, redirectUrl, \u0026#34;state\u0026#34;); try { httpClientExample.get(requestUrl, requestParam.toString()); httpClientExample.getAsync(requestUrl, requestParam.toString()); httpClientExample.post(requestUrl, requestParam.toString()); } catch (Exception e) { e.printStackTrace(); } } 출력 Sync status code: 200 Async status code: 200 Post status code: 200 공부해야하는 개념 BodySubscriber CompletableFuture Backpressure 깃허브 ","permalink":"https://4d4cat.com/posts/2025/httpclient-deepdive/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eBodyPublisher, BodyHandler 역할\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003eHttpClient 실습\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"bodypublisher-bodyhandler\"\u003e\u003cstrong\u003eBodyPublisher? BodyHandler?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eBodyPublisher\u003c/code\u003e와 \u003ccode\u003eBodyHandlers\u003c/code\u003e는 요청과 응답의 본문을 처리하는 데 있어 상호 보완적인 역할을 한다. \u003ccode\u003eBodyPublisher\u003c/code\u003e는 클라이언트가 서버로 전송할 데이터를 정의하고, \u003ccode\u003eBodyHandlers\u003c/code\u003e는 서버로부터 받은 데이터를 어떻게 처리할지를 정의한다. 이 두 구성 요소를 적절히 활용하면, HTTP 통신을 더욱 유연하고 효율적으로 구현할 수 있다.\u003c/p\u003e\n\u003ch3 id=\"bodypublisher\"\u003e\u003cstrong\u003eBodyPublisher\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eHTTP 요청의 본문을 제공\u003c/li\u003e\n\u003cli\u003e데이터를 전송할 때 어떻게 제공할지 정의\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eBodyPublishers.ofString(String)\u003c/code\u003e: 문자열 데이터\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBodyPublishers.ofByteArray(byte[])\u003c/code\u003e: 바이트 배열 데이터\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBodyPublishers.ofFile(Path)\u003c/code\u003e: 파일 데이터\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBodyPublishers.ofInputStream(Supplier\u0026lt;InputStream\u0026gt;)\u003c/code\u003e: 동적으로 생성되는 입력 스트림\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"bodyhandler\"\u003e\u003cstrong\u003eBodyHandler\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eHTTP 응답의 본문을 처리하는 방법을 정의\u003c/li\u003e\n\u003cli\u003e서버로부터 받은 응답 데이터를 어떻게 처리할지 정의\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eBodyHandlers.ofString()\u003c/code\u003e: 문자열 데이터\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBodyHandlers.ofByteArray()\u003c/code\u003e: 바이트 배열 데이터\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBodyHandlers.ofFile(Path)\u003c/code\u003e: 파일 데이터\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBodyHandlers.ofInputStream()\u003c/code\u003e: \u003ccode\u003eInputStream\u003c/code\u003e으로 데이터 제공\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003eHttpResponse.BodyHandler 클래스는 BodyHandler를 생성하기 위한 여러 가지 편리한 정적 팩토리 메서드를 제공한다. 이들 중 다수는 응답 바이트가 완전히 수신될 때까지 메모리에 축적되며, 그 후 응답 바이트는 상위 수준의 Java 유형(ofString, ofByteArray 등)으로 변환된다.\u003c/p\u003e","title":"HttpClient Deepdive"},{"content":"🔔 HttpClient 개요\n🔔 HttpClient 개선 과정\n🔔 다른 라이브러리 비교\nWhat? Why? 자바에서 HTTP 요청을 생성하고, 서버와 통신하며, 응답을 처리하기 위한 API Java 11에서 표준 라이브러리로 도입되었으며, 이전의 HttpURLConnection보다 사용하기 쉽고 기능이 풍부하다.\n빌더 패턴 도입: HttpRequest.newBuilder() 역할 분리: HttpClient, HttpRequest, HttpResponse 비동기 처리 지원: HttpClient.sendAsync() 주요 클래스 HttpClient: HTTP 요청을 보내기 위한 클라이언트 객체를 생성 HttpRequest: HTTP 요청의 세부 사항(URI, 메서드, 헤더 등) HttpResponse: 서버로부터 받은 HTTP 응답 주요 메서드 HttpClient.newHttpClient() 기본 설정을 사용한 HttpClient 인스턴스를 생성 - 요청(GET), 프로토콜(HTTP/2), 리다이렉트(NEVER), SSL(Default)\nHttpClient client = HttpClient.newHttpClient(); HttpRequest.newBuilder() 새로운 HttpRequest 빌더를 생성\nHttpRequest request = HttpRequest.newBuilder() .uri(URI.create(\u0026#34;https://foo.com/\u0026#34;)) .timeout(Duration.ofMinutes(2)) .header(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) .POST(BodyPublishers.ofFile(Paths.get(\u0026#34;file.json\u0026#34;))) .build(); client.send(request, HttpResponse.BodyHandlers.ofString()) 동기적으로 HTTP 요청을 보내고, 응답을 문자열로 받음\nHttpResponse\u0026lt;String\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body()); client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) 비동기적으로 HTTP 요청을 보내고, 응답을 CompletableFuture로 받음\nclient.sendAsync(request, BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept(System.out::println); 기능 업데이트 요약 java 11: HttpClient 도입 java 14: TLS 1.3 지원 java 15: OAuth2 등 다양한 인증 방식을 지원하는 기능이 추가 java 17: 커넥션 풀 관리 기능이 향상 java 18~21: HTTP/3의 정식 지원이 추가, 보안 강화 다른 HTTP 라이브러리 RestTemplate 동기 방식 간단한 사용법: 간단한 REST 호출에 적합 제한 사항: 비동기나 반응형 프로그래밍을 지원하지 않으며, 기능이 제한적 import org.springframework.web.client.RestTemplate; public class RestTemplateExample { public static void main(String[] args) { RestTemplate restTemplate = new RestTemplate(); String response = restTemplate.getForObject(\u0026#34;https://api.example.com/data\u0026#34;, String.class); System.out.println(response); } } WebClient 비동기 및 반응형 지원: 비동기 호출과 리액티브 스트림을 지원 유연성: 다양한 HTTP 요청과 응답 처리를 유연하게 지원 모던한 사용법: 현대적인 웹 애플리케이션에 적합 import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; public class WebClientExample { public static void main(String[] args) { WebClient client = WebClient.create(\u0026#34;https://api.example.com\u0026#34;); Mono\u0026lt;String\u0026gt; response = client.get() .uri(\u0026#34;/data\u0026#34;) .retrieve() .bodyToMono(String.class); response.subscribe(System.out::println); } } 비교 출처 Oracle OpenJDK ChatGPT\n","permalink":"https://4d4cat.com/posts/2025/httpclient/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eHttpClient 개요\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003eHttpClient 개선 과정\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e다른 라이브러리 비교\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"what-why\"\u003e\u003cstrong\u003eWhat? Why?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e자바에서 HTTP 요청을 생성하고, 서버와 통신하며, 응답을 처리하기 위한 API     \u003cbr\u003e\nJava 11에서 표준 라이브러리로 도입되었으며, 이전의 \u003ccode\u003eHttpURLConnection\u003c/code\u003e보다 사용하기 쉽고 기능이 풍부하다.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e빌더 패턴 도입: \u003ccode\u003eHttpRequest\u003c/code\u003e.newBuilder()\u003c/li\u003e\n\u003cli\u003e역할 분리: \u003ccode\u003eHttpClient\u003c/code\u003e, \u003ccode\u003eHttpRequest\u003c/code\u003e, \u003ccode\u003eHttpResponse\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e비동기 처리 지원: \u003ccode\u003eHttpClient\u003c/code\u003e.sendAsync()\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"주요-클래스\"\u003e\u003cstrong\u003e주요 클래스\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eHttpClient\u003c/code\u003e: HTTP 요청을 보내기 위한 클라이언트 객체를 생성\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHttpRequest\u003c/code\u003e: HTTP 요청의 세부 사항(URI, 메서드, 헤더 등)\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHttpResponse\u003c/code\u003e: 서버로부터 받은 HTTP 응답\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"주요-메서드\"\u003e\u003cstrong\u003e주요 메서드\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eHttpClient\u003c/code\u003e.newHttpClient()\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e기본 설정을 사용한 HttpClient 인스턴스를 생성  \u003cbr\u003e\n- 요청(GET), 프로토콜(HTTP/2), 리다이렉트(NEVER), SSL(Default)\u003c/p\u003e","title":"java.net.http.HttpClient"},{"content":"🔔 직무면접 내용 🔔 직무면접 후기\n메시징 솔루션 기업 1차 직무면접\n전체적인 분위기 가벼운 마음으로 갔지만 막상 자리에 가니 떨리고 초반에 말이 잘 안나왔다. 3대1 면접이었고 다행히 긴장을 풀고 편안하게 해주시려는 모습이 보여서 너무 좋았다.\n가벼운 질문 1. 자기소개 및 선퇴사한 것에 대해서 2. 이전 팀에 대해서, 어떤 환경이었는지 등에 대해서 3. 내가 생각하는 백엔드 개발자란? 4. 내 강점은 무엇인지? 5. 우리 회사에 대해 얼마나 아는지? 느낀점 \u0026#39;내 경력에 대해서 어떻게 설명할 수 있을까\u0026#39;에 대해서만 준비했었다. 그래서 업무 관련해서 가벼운 질문은 짧게나마 답할 수 있었지만 그 외에는 즉흥으로 답변해서 좀 아쉬웠다. 특히 \u0026#39;백엔드 개발자란?\u0026#39; 질문을 너무 어버버하게 답변했는데 다음 면접에는 제대로 준비해야할 것 같다. 경력 질문 1. 검색 키워드 관리에 대해서 2. 메시지 성능 개선에 대해서 3. Itsm 알림 대체 서비스에 대해서 느낀점 메시지 솔루션 기업이다보니 2번의 답변을 준비한 시간이 길었다. 물론 현장에서 답변을 하다보니 순서가 꼬이는 부분도 있었고, 내가 기술 용어를 제대로 이해 못한 부분도 많았다. 하지만 내가 이해를 못해도 다시 설명해주셔서 너무 감사했다. 이 과정에서 내가 잘못 알고 있는 용어와 전체 프로세스의 과정 중 부족한 부분을 수정 중이다. 나머지 질문에 대해서는 크게 설명할 부분은 없었지만 이번 면접을 경험해보니 같이 수정해야할 것 같다. 코딩테스트 코딩테스트를 직무면접 합격 후에 할 줄 알았는데 바로 하는건줄은 몰랐다. 물론 당일에 하나 나중에 하나 큰 차이 없을 것 같아서 바로 진행한다고 했다.\n문제는 총 6문제로, 2문제는 회사 API를 이용하여 결과를 출력하는 것이고, 나머지 4문제는 알고리즘 문제였다. 1시간에 풀 수 있을만큼 하는거라서 내 코딩 스타일을 보기 위한 것이라고 생각했는데, 그러기엔 너무 완벽하게 풀려고 하다보니 API 이용하는 2문제와 알고리즘 1문제 밖에 풀지 못했다.\n어떻게 평가해주실지는 모르겠지만 내가 개인 프로젝트에 적용했던 부분과 비슷해서 어렵지 않았다.\n","permalink":"https://4d4cat.com/posts/2025/job-interview1/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e직무면접 내용\u003c/strong\u003e \u003cbr\u003e\n🔔 \u003cstrong\u003e직무면접 후기\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e메시징 솔루션 기업 1차 직무면접\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"전체적인-분위기\"\u003e\u003cstrong\u003e전체적인 분위기\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e가벼운 마음으로 갔지만 막상 자리에 가니 떨리고 초반에 말이 잘 안나왔다.  \u003cbr\u003e\n3대1 면접이었고 다행히 긴장을 풀고 편안하게 해주시려는 모습이 보여서 너무 좋았다.\u003c/p\u003e\n\u003ch2 id=\"가벼운-질문\"\u003e\u003cstrong\u003e가벼운 질문\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"1-자기소개-및-선퇴사한-것에-대해서\"\u003e1. 자기소개 및 선퇴사한 것에 대해서\u003c/h3\u003e\n\u003ch3 id=\"2-이전-팀에-대해서-어떤-환경이었는지-등에-대해서\"\u003e2. 이전 팀에 대해서, 어떤 환경이었는지 등에 대해서\u003c/h3\u003e\n\u003ch3 id=\"3-내가-생각하는-백엔드-개발자란\"\u003e3. 내가 생각하는 백엔드 개발자란?\u003c/h3\u003e\n\u003ch3 id=\"4-내-강점은-무엇인지\"\u003e4. 내 강점은 무엇인지?\u003c/h3\u003e\n\u003ch3 id=\"5-우리-회사에-대해-얼마나-아는지\"\u003e5. 우리 회사에 대해 얼마나 아는지?\u003c/h3\u003e\n\u003ch3 id=\"느낀점\"\u003e느낀점\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026#39;내 경력에 대해서 어떻게 설명할 수 있을까\u0026#39;에 대해서만 준비했었다.\n그래서 업무 관련해서 가벼운 질문은 짧게나마 답할 수 있었지만 그 외에는 즉흥으로 답변해서 좀 아쉬웠다.\n특히 \u0026#39;백엔드 개발자란?\u0026#39; 질문을 너무 어버버하게 답변했는데 다음 면접에는 제대로 준비해야할 것 같다.\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"경력-질문\"\u003e\u003cstrong\u003e경력 질문\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"1-검색-키워드-관리에-대해서\"\u003e1. 검색 키워드 관리에 대해서\u003c/h3\u003e\n\u003ch3 id=\"2-메시지-성능-개선에-대해서\"\u003e2. 메시지 성능 개선에 대해서\u003c/h3\u003e\n\u003ch3 id=\"3-itsm-알림-대체-서비스에-대해서\"\u003e3. Itsm 알림 대체 서비스에 대해서\u003c/h3\u003e\n\u003ch3 id=\"느낀점-1\"\u003e느낀점\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e메시지 솔루션 기업이다보니 2번의 답변을 준비한 시간이 길었다.\n물론 현장에서 답변을 하다보니 순서가 꼬이는 부분도 있었고, 내가 기술 용어를 제대로 이해 못한 부분도 많았다.\n하지만 내가 이해를 못해도 다시 설명해주셔서 너무 감사했다.\n이 과정에서 내가 잘못 알고 있는 용어와 전체 프로세스의 과정 중 부족한 부분을 수정 중이다.\n나머지 질문에 대해서는 크게 설명할 부분은 없었지만 이번 면접을 경험해보니 같이 수정해야할 것 같다.\n\u003c/code\u003e\u003c/pre\u003e\u003ch2 id=\"코딩테스트\"\u003e\u003cstrong\u003e코딩테스트\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e코딩테스트를 직무면접 합격 후에 할 줄 알았는데 바로 하는건줄은 몰랐다. 물론 당일에 하나 나중에 하나 큰 차이 없을 것 같아서 바로 진행한다고 했다.\u003c/p\u003e","title":"직무면접 후기 1"},{"content":"🔔 네이버 로그인 검증 과정\n네이버 로그인하기까지 어떤 과정을 거치는가?\n1. 세션 유지 및 위조 방지용 상태 토큰 생성 // CSRF 방지를 위한 상태 토큰 생성 코드 // 상태 토큰은 추후 검증을 위해 세션에 저장되어야 한다. public String generateState() { SecureRandom random = new SecureRandom(); return new BigInteger(130, random).toString(32); } // 상태 토큰으로 사용할 랜덤 문자열 생성 String state = generateState(); // 세션 또는 별도의 저장 공간에 상태 토큰을 저장 request.session().attribute(\u0026#34;state\u0026#34;, state); return state; CSRF 공격을 방지하기 위해 애플리케이션과 사용자 간의 상태를 보유하는 고유한 세션 토큰을 만들어야 한다. 이 세션 토큰을 상태 토큰(state token) 이라 하며, 상태 토큰의 값은 사용자가 네이버 로그인을 진행하는 동안 유지되어야 하며 고유한 값이어야 한다. 생성한 상태 토큰은 세션이나 별도의 저장 공간에 저장해야 한다.\n2. 로그인 인증 요청문 생성 URL:\nhttps://nid.naver.com/oauth2.0/authorize? client_id={클라이언트 아이디}\u0026amp; response_type=code\u0026amp; redirect_uri={개발자 센터에 등록한 콜백 URL(URL 인코딩)}\u0026amp; state={상태 토큰} 네이버 로그인 및 동의 화면 응답문 URL {개발자 센터에 등록한 콜백 URL}?state={상태 토큰}\u0026amp;code={인증 코드}\n전달받은 state 파라미터의 값과 처음에 생성한 상태 토큰이 일치하는지 확인하여 만약 상태 토큰과 일치하지 않다면, 해당 세션은 유효하지 않다고 판단할 수 있다.\n3. 상태 토큰 검증 // CSRF 방지를 위한 상태 토큰 검증 검증 // 세션 또는 별도의 저장 공간에 저장된 상태 토큰과 콜백으로 전달받은 state 파라미터의 값이 일치해야 함 // 콜백 응답에서 state 파라미터의 값을 가져옴 String state = request.queryParams(\u0026#34;state\u0026#34;); // 세션 또는 별도의 저장 공간에서 상태 토큰을 가져옴 String storedState = request.session().attribute(\u0026#34;state\u0026#34;); if(!state.euals(storedState)) { return RESPONSE_UNAUTHORIZED; //401 unauthorized } else { Return RESPONSE_SUCCESS; //200 success } 4. 접근 토큰 요청 URL:\nhttps://nid.naver.com/oauth2.0/token? client_id={클라이언트 아이디}\u0026amp; client_secret={클라이언트 시크릿}\u0026amp; grant_type=authorization_code\u0026amp; state={상태 토큰}\u0026amp; code={인증 코드} 상태 토큰에 대한 검증이 성공적으로 끝났다면 응답으로 전달받은 인증 코드를 이용해 최종 인증 값인 접근 토큰을 발급받는다. 인증 코드는 접근 토큰을 발급할 때 1번만 사용하며 이미 사용한 인증 코드는 더 이상 사용할 수 없다.\n응답문 JSON { \u0026#34;access_token\u0026#34;: \u0026#34;AAAAQosjWDJieBiQZc3to9YQp6HDLvrmyKC+6+iZ3gq7qrkqf50ljZC+Lgoqrg\u0026#34;, \u0026#34;refresh_token\u0026#34;: \u0026#34;c8ceMEJisO4Se7uGisHoX0f5JEii7JnipglQipkOn5Zp3tyP7dHQoP0zNKHUq2gY\u0026#34;, \u0026#34;token_type\u0026#34;: \u0026#34;bearer\u0026#34;, \u0026#34;expires_in\u0026#34;: \u0026#34;3600\u0026#34; } 접근 토큰 요청에 성공하면 접근 토큰과 갱신 토큰이 포함된 JSON 형식의 결괏값을 반환한다. 접근 토큰은 expires_in 속성에 설정된 시간 동안 유효하기 때문에 유효 기간이 지난 뒤에는 갱신 토큰을 사용해 접근 토큰을 다시 발급받아야 한다.\n5. 네이버 사용자 프로필 조회 URL: https://openapi.naver.com/v1/nid/me\n접근 토큰을 성공적으로 발급받았다면 접근 토큰을 이용해 네이버 사용자의 프로필 정보를 조회할 수 있다. 사용자 프로필 정보는 사용자를 구분할 수 있는 식별값인 아이디와 별명, 메일 주소와 같은 사용자 정보를 포함하고 있다.\n프로젝트에 적용 상태 토큰 생성은 공통 유틸 함수에 만들고 이를 가져와서 사용하려고 한다. 검증 부분이 좀 고민이 되는데 위조된 값을 가져온 경우 적절한 예외 처리가 필요할 것 같다. 이를 위해 커스텀 예외 처리 클래스를 만드려고 하는데 여러 방법이 있겠지만 괜찮다고 생각한 저장소가 있다.\n우테코 Festgo enum 클래스로 에러 코드를 관리하고 각 에러 타입에 맞는 커스텀 클래스를 불러와서 처리하고 있다. 이를 위해 필요한 개념을 정리하면서 설계를 해봐야겠다.\n공부해야하는 부분 커스텀 예외 클래스 구조 NestedRuntimeException vs RuntimeException ResponseEntity 용도 ","permalink":"https://4d4cat.com/posts/2024/naver-login/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e네이버 로그인 검증 과정\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e네이버 로그인하기까지 어떤 과정을 거치는가?\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"1-세션-유지-및-위조-방지용-상태-토큰-생성\"\u003e\u003cstrong\u003e1. 세션 유지 및 위조 방지용 상태 토큰 생성\u003c/strong\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// CSRF 방지를 위한 상태 토큰 생성 코드\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"c1\"\u003e// 상태 토큰은 추후 검증을 위해 세션에 저장되어야 한다.    \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003egenerateState\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eSecureRandom\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003erandom\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eSecureRandom\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e  \u003c/span\u003e\u003cspan class=\"n\"\u003eBigInteger\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e130\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003erandom\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"na\"\u003etoString\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003e32\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"c1\"\u003e// 상태 토큰으로 사용할 랜덤 문자열 생성  \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egenerateState\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"c1\"\u003e// 세션 또는 별도의 저장 공간에 상태 토큰을 저장 \u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003erequest\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esession\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"na\"\u003eattribute\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;state\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003estate\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003eCSRF 공격을 방지하기 위해 애플리케이션과 사용자 간의 상태를 보유하는 고유한 세션 토큰을 만들어야 한다. 이 세션 토큰을 \u003ccode\u003e상태 토큰(state token)\u003c/code\u003e 이라 하며, 상태 토큰의 값은 사용자가 네이버 로그인을 진행하는 동안 유지되어야 하며 고유한 값이어야 한다. 생성한 상태 토큰은 세션이나 별도의 저장 공간에 저장해야 한다.\u003c/p\u003e","title":"네이버 로그인 과정"},{"content":"🔔 MVC Test 코드 작성\n네이버 연동 페이지에 접근이 잘 되는가?\nLoginController.java @Controller @RequiredArgsConstructor @RequestMapping(\u0026#34;/login\u0026#34;) public class LoginController { private final NaverLoginService naverLoginService; @GetMapping(\u0026#34;/naver\u0026#34;) public void naverLogin(HttpServletResponse response) throws IOException { response.sendRedirect(naverLoginService.getNaverOauth2LoginUrl()); } } naverLogin {URL}/login/naver로 접근하면 Naver 로그인 인증을 위한 페이지로 이동된다.\n리다이렉트 URL은 naverLoginService에서 받는다.\nNaverLoginService.java @Service @RequiredArgsConstructor public class NaverLoginService { private static final String BASE_URL = \u0026#34;https://nid.naver.com/oauth2.0\u0026#34;; private final AppProperties app; private final ThirdPartyProperties secrets; public String getNaverOauth2LoginUrl() { String authorizeUrl = BASE_URL+\u0026#34;/authorize\u0026#34;; String callbackUrl = getServiceUrl()+\u0026#34;/login/naver/callback\u0026#34;; return UriComponentsBuilder .fromHttpUrl(authorizeUrl) .queryParam(\u0026#34;response_type\u0026#34;, \u0026#34;code\u0026#34;) .queryParam(\u0026#34;client_id\u0026#34;, secrets.naver().naverId()) .queryParam(\u0026#34;redirect_uri\u0026#34;, callbackUrl) .queryParam(\u0026#34;state\u0026#34;, \u0026#34;random\u0026#34;) .toUriString(); } public String getServiceUrl() { return app.serviceUrl(); } } AppProperties 현재는 도메인 URL만 세팅되어 있다.\n로컬 서버 http://localhost 운영 서버 https://4d4cat.site ThirdPartyProperties 네이버 포함 여러 서드파티의 키값이 세팅되어 있다.\n여기서는 네이버 Client_ID 값이 사용되었다.\nLoginControllerTest @WebMvcTest(LoginController.class) @EnableConfigurationProperties({AppProperties.class, ThirdPartyProperties.class}) @ActiveProfiles(\u0026#34;local\u0026#34;) class LoginControllerTest { @Autowired private AppProperties app; @Autowired private ThirdPartyProperties secrets; @Autowired private MockMvc mockMvc; @MockBean private NaverLoginService naverLoginService; @Nested @DisplayName(\u0026#34;네이버 로그인 컨트롤러 테스트\u0026#34;) class naverLoginTest { @Test @DisplayName(\u0026#34;로컬 URL 정상적으로 적용되는가?\u0026#34;) void testServiceUrl_Local() { String serviceUrl = app.serviceUrl(); assertNotNull(serviceUrl); assertTrue(serviceUrl.startsWith(\u0026#34;http://localhost\u0026#34;), \u0026#34;localUrl: localhost\u0026#34;); } @Test @DisplayName(\u0026#34;GET /login/naver 정상적으로 접근되는가?\u0026#34;) void testNaverLoginPageAccess() throws Exception{ String baseUrl = app.serviceUrl(); String expectedRedirectUrl = \u0026#34;https://nid.naver.com/oauth2.0/authorize\u0026#34; + \u0026#34;?client_id=\u0026#34;+secrets.naver().naverId() + \u0026#34;\u0026amp;redirect_uri=\u0026#34;+baseUrl+\u0026#34;/login/naver/callback\u0026#34; + \u0026#34;\u0026amp;response_type=code\u0026#34;; when(naverLoginService.getNaverOauth2LoginUrl()).thenReturn(expectedRedirectUrl); mockMvc.perform(get(\u0026#34;/login/naver\u0026#34;)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrlPattern(\u0026#34;https://nid.naver.com/**\u0026#34;)) ; } } } 네이버 로그인 인증 URL이 정상적으로 접근되는지 테스트하는 코드만 있지만, 추후 테스트를 그룹화 하기 위해 @Nested로 구분하였다. 그리고 로컬 환경을 세팅할 때 로컬 도메인이 제대로 적용됐는지, 해당 도메인으로 redirect_uri 값이 들어가는지 테스트하는 코드를 작성하였다. 다음 테스트는 인증 이후 토큰을 발급, 갱신하는 테스트를 작성해볼 예정이다.\n@WebMvcTest LoginController와 관련된 Bean만 로드하여 테스트를 진행\n@EnableConfigurationProperties 환경설정 클래스인 AppProperties와 ThirdPartyProperties를 사용하기 위해 세팅\n@ActiveProfiles 현재는 local 환경에서 테스트를 진행한다.\nwhen when(naverLoginService.getNaverOauth2LoginUrl()).thenReturn(expectedRedirectUrl);\ngetNaverOauth2LoginUrl()가 호출될 때, expectedRedirectUrl에 초기화된 값을 반환\n다른 고민 네이버 로그인 토큰 발급 유형의 grant_type을 미리 enum에 만들어서 관리하면 어떨까? 접근 테스트 외에 서비스 단위로 테스트하는 코드를 작성하려면 어디부터 작성해야할까? ","permalink":"https://4d4cat.com/posts/2024/4d4cat2/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eMVC Test 코드 작성\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e네이버 연동 페이지에 접근이 잘 되는가?\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"logincontrollerjava\"\u003e\u003cstrong\u003eLoginController.java\u003c/strong\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Controller\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nd\"\u003e@RequiredArgsConstructor\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nd\"\u003e@RequestMapping\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/login\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eLoginController\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003efinal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eNaverLoginService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003enaverLoginService\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@GetMapping\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/naver\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003enaverLogin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpServletResponse\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003ethrows\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eIOException\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eresponse\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esendRedirect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enaverLoginService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003egetNaverOauth2LoginUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"naverlogin\"\u003enaverLogin\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003e{URL}/login/naver\u003c/code\u003e로 접근하면 Naver 로그인 인증을 위한 페이지로 이동된다.\u003cbr\u003e\n리다이렉트 URL은 \u003ccode\u003enaverLoginService\u003c/code\u003e에서 받는다.\u003c/p\u003e\n\u003ch2 id=\"naverloginservicejava\"\u003e\u003cstrong\u003eNaverLoginService.java\u003c/strong\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Service\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nd\"\u003e@RequiredArgsConstructor\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eNaverLoginService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003estatic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003efinal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBASE_URL\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;https://nid.naver.com/oauth2.0\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003efinal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eAppProperties\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003efinal\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eThirdPartyProperties\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esecrets\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003egetNaverOauth2LoginUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eauthorizeUrl\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eBASE_URL\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/authorize\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ecallbackUrl\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egetServiceUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/login/naver/callback\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eUriComponentsBuilder\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003efromHttpUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eauthorizeUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003equeryParam\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;response_type\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;code\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003equeryParam\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;client_id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esecrets\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003enaver\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"na\"\u003enaverId\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003equeryParam\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;redirect_uri\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ecallbackUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003equeryParam\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;state\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;random\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003etoUriString\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003egetServiceUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eserviceUrl\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e  \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"appproperties\"\u003eAppProperties\u003c/h3\u003e\n\u003cp\u003e현재는 도메인 URL만 세팅되어 있다.\u003c/p\u003e","title":"4D4cat) 네이버 로그인 연동을 위한 MVC 테스트 1"},{"content":"🔔 스프링 시큐리티 개념\n스프링 가이드를 참고하여 스프링 시큐리티의 기본을 공부해보았다.\nWebSecurityConfig @Configuration @EnableWebSecurity public class WebSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { RequestCache nullRequestCache = new NullRequestCache(); http .requestCache((cache) -\u0026gt; cache .requestCache(nullRequestCache) ) .authorizeHttpRequests((requests) -\u0026gt; requests .requestMatchers(\u0026#34;/\u0026#34;, \u0026#34;/home\u0026#34;).permitAll() .anyRequest().authenticated() ) .formLogin((form) -\u0026gt; form .loginPage(\u0026#34;/login\u0026#34;) .permitAll() ) .logout((logout) -\u0026gt; logout.permitAll()); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withDefaultPasswordEncoder() .username(\u0026#34;user\u0026#34;) .password(\u0026#34;password\u0026#34;) .roles(\u0026#34;USER\u0026#34;) .build(); return new InMemoryUserDetailsManager(user); } } @EnableWebSecurity를 설정하여 시큐리티를 활성화시키고 securityFilterChain와 userDetailsService를 구현했다.\nsecurityFilterChain는 어떤 경로를 보안 설정할지 보여준다. 여기서는 /와 /home 경로는 인증이 필요하지 않고 나머지 경로에 대해서만 인증을 받도록 설정했다. 인증이 필요한 페이지는 사용자가 성공적으로 로그인하면 이전에 요청한 페이지로 리다이렉션된다. 첫 번째에 RequestCache가 선언되어있는데 자세한건 다음 섹션을 참고하길 바란다.\n여기서는 RequestCache를 적용하지 않기 위해 NullRequestCache를 사용했다.\nuserDetailsService는 사용자명(user), 비밀번호(password), 역할(USER)을 갖는 유저를 하나 생성한다. 이것을 InMemoryUserDetailsManager를 통해 in-메모리에 저장하게 된다.\ncontinue parameter? In Spring Security 5, the default behavior is to query the saved request on every request. This means that in a typical setup, that in order to use the RequestCache the HttpSession is queried on every request.\nIn Spring Security 6, the default is that RequestCache will only be queried for a cached request if the HTTP parameter continue is defined. This allows Spring Security to avoid unnecessarily reading the HttpSession with the RequestCache.\nIn Spring Security 5 the default is to use HttpSessionRequestCache which will be queried for a cached request on every request. If you are not overriding the defaults (i.e. using NullRequestCache), then the following configuration can be used to explicitly opt into the Spring Security 6 behavior in Spring Security 5.8:\nRequestCache Only Checks for Saved Requests if continue Parameter Present\n인증이 필요한 리소스에 대해 인증 성공 후 다시 요청하려면 해당 리소스에 대한 요청을 저장할 필요가 있다. 이 때 사용하는 것이 RequestCache이다. 위 내용은 RequestCache에 대해 스프링 시큐리티 5 버전과 6 버전에서의 적용하는 차이를 알려준다. 스프링 시큐리티 5 버전에서는 RequestCache가 적용되는 모든 요청에 저장을 하는 반면, 6 버전에서는 continue 같은 명시적인 파라미터가 들어간 경우에만 동작하도록 제한되어 있다.\nSpring Security 5.8\nRequestCache\nWebMvcTest 코드 작성 @WebMvcTest @Import(WebSecurityConfig.class) public class WebSecurityConfigTest { @Autowired private MockMvc mockMvc; @WebMvcTest\nSpring MVC 관련 컴포넌트(컨트롤러, 필터 등)만 로드하여 빠른 테스트를 가능하게 한다. 그러나 기본적으로는 보안 설정을 포함하지 않는다.\n@Import\nWebSecurityConfig 클래스의 보안 설정을 테스트에 포함시킨다.\nMockMvc\n시뮬레이션할 Mock 객체\n@Nested\n테스트를 그룹화하고 계층구조로 보기 위해 사용했다.\n@WithMockUser 가상의 인증 사용자를 설정\nUnauthenticated Page Test @Nested @DisplayName(\u0026#34;공개된 엔드포인트 테스트\u0026#34;) class PublicEndpoints { @Test @DisplayName(\u0026#34;GET /home은 인증 없이 접근 가능해야 함\u0026#34;) void testHomePageAccessibleWithoutAuth() throws Exception { mockMvc.perform(get(\u0026#34;/home\u0026#34;)) .andExpect(status().isOk()) .andExpect(view().name(\u0026#34;home\u0026#34;)); } @Test @DisplayName(\u0026#34;GET /은 인증 없이 접근 가능해야 함\u0026#34;) void testRootAccessibleWithoutAuth() throws Exception { mockMvc.perform(get(\u0026#34;/\u0026#34;)) .andExpect(status().isOk()) .andExpect(view().name(\u0026#34;home\u0026#34;)); } @Test @DisplayName(\u0026#34;GET /login은 인증 없이 접근 가능해야 함\u0026#34;) void testLoginPageAccessibleWithoutAuth() throws Exception { mockMvc.perform(get(\u0026#34;/login\u0026#34;)) .andExpect(status().isOk()) .andExpect(view().name(\u0026#34;login\u0026#34;)); } } Authenticated Page Test @Nested @DisplayName(\u0026#34;보호된 엔드포인트 테스트\u0026#34;) class ProtectedEndpoints { @Test @DisplayName(\u0026#34;GET /hello는 인증이 필요하며, 인증되지 않은 사용자는 로그인 페이지로 리다이렉트되어야 함\u0026#34;) void testHelloPageRequiresAuth() throws Exception { mockMvc.perform(get(\u0026#34;/hello\u0026#34;)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrlPattern(\u0026#34;**/login\u0026#34;)); } @Test @WithMockUser(username = \u0026#34;user\u0026#34;, roles = {\u0026#34;USER\u0026#34;}) @DisplayName(\u0026#34;GET /hello는 인증된 사용자에게 접근 가능해야 함\u0026#34;) void testHelloPageAccessibleWithAuth() throws Exception { mockMvc.perform(get(\u0026#34;/hello\u0026#34;)) .andExpect(status().isOk()) .andExpect(view().name(\u0026#34;hello\u0026#34;)); } } Login Test @Nested @DisplayName(\u0026#34;로그인 테스트\u0026#34;) class LoginTests { @Test @DisplayName(\u0026#34;올바른 자격 증명으로 로그인 시도 시, 성공적으로 인증되고 /으로 리다이렉트되어야 함\u0026#34;) void testSuccessfulLogin() throws Exception { mockMvc.perform(formLogin(\u0026#34;/login\u0026#34;) .user(\u0026#34;user\u0026#34;) .password(\u0026#34;password\u0026#34;)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl(\u0026#34;/\u0026#34;)); } @Test @DisplayName(\u0026#34;잘못된 자격 증명으로 로그인 시도 시, 로그인 페이지로 리다이렉트되고 에러가 표시되어야 함\u0026#34;) void testFailedLogin() throws Exception { mockMvc.perform(formLogin(\u0026#34;/login\u0026#34;) .user(\u0026#34;user\u0026#34;) .password(\u0026#34;wrongpassword\u0026#34;)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl(\u0026#34;/login?error\u0026#34;)); } @Test @DisplayName(\u0026#34;로그인 시 CSRF 토큰이 필요함\u0026#34;) void testLoginRequiresCsrf() throws Exception { mockMvc.perform(formLogin(\u0026#34;/login\u0026#34;) .user(\u0026#34;user\u0026#34;) .password(\u0026#34;password\u0026#34;)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl(\u0026#34;/\u0026#34;)); } } Logout Test @Nested @DisplayName(\u0026#34;로그아웃 테스트\u0026#34;) class LogoutTests { @Test @WithMockUser(username = \u0026#34;user\u0026#34;, roles = {\u0026#34;USER\u0026#34;}) @DisplayName(\u0026#34;로그아웃 시도 시, 세션이 무효화되고 /login?logout으로 리다이렉트되어야 함\u0026#34;) void testLogout() throws Exception { mockMvc.perform(logout(\u0026#34;/logout\u0026#34;)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl(\u0026#34;/login?logout\u0026#34;)); } @Test @DisplayName(\u0026#34;로그아웃은 인증되지 않은 사용자도 시도할 수 있으며, /login?logout으로 리다이렉트되어야 함\u0026#34;) void testLogoutByUnauthenticatedUser() throws Exception { mockMvc.perform(logout(\u0026#34;/logout\u0026#34;)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl(\u0026#34;/login?logout\u0026#34;)); } } 저장소 깃허브\n","permalink":"https://4d4cat.com/posts/2024/spring-security-example/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e스프링 시큐리티 개념\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://spring.io/guides/gs/securing-web\"\u003e스프링 가이드\u003c/a\u003e를 참고하여 스프링 시큐리티의 기본을 공부해보았다.\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"websecurityconfig\"\u003e\u003cstrong\u003eWebSecurityConfig\u003c/strong\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Configuration\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"nd\"\u003e@EnableWebSecurity\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eWebSecurityConfig\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Bean\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eSecurityFilterChain\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esecurityFilterChain\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpSecurity\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003ethrows\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eException\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eRequestCache\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003enullRequestCache\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eNullRequestCache\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003ehttp\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003erequestCache\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003ecache\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ecache\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003erequestCache\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enullRequestCache\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eauthorizeHttpRequests\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003erequests\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003erequests\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003erequestMatchers\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/home\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"na\"\u003epermitAll\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eanyRequest\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"na\"\u003eauthenticated\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eformLogin\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eform\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eloginPage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/login\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003epermitAll\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003elogout\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"n\"\u003elogout\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elogout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003epermitAll\u003c/span\u003e\u003cspan class=\"p\"\u003e());\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ehttp\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ebuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Bean\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eUserDetailsService\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003euserDetailsService\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eUserDetails\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                \u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ewithDefaultPasswordEncoder\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eusername\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;user\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003epassword\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eroles\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;USER\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e                        \u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ebuild\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eInMemoryUserDetailsManager\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003e@EnableWebSecurity\u003c/code\u003e를 설정하여 시큐리티를 활성화시키고 \u003ccode\u003esecurityFilterChain\u003c/code\u003e와 \u003ccode\u003euserDetailsService\u003c/code\u003e를 구현했다.\u003c/p\u003e","title":"스프링 시큐리티 기본"},{"content":"🔔 빌더 패턴이란?\n🔔 기존 생성자 방식과의 차이점\nWhat? Why? 빌더 패턴(Builder Pattern)은 복잡한 객체를 단계별로 생성할 수 있게 해주는 디자인 패턴이다. 객체의 생성 과정을 추상화하여, 다양한 형태의 객체를 유연하게 생성할 수 있도록 도와준다.\n기존 생성자 방식과의 비교 기존 생성자 방식 기존의 생성자 방식은 객체를 생성할 때 필요한 모든 파라미터를 생성자에 전달해야 한다.\npublic class User { private Long id; private String userId; private String username; private Set\u0026lt;Role\u0026gt; roles; // 기타 필드 및 메서드 public User(Long id, String userId, String username, Set\u0026lt;Role\u0026gt; roles) { this.id = id; this.userId = userId; this.username = username; this.roles = roles; } } 빌더 패턴 방식 빌더 패턴을 사용하면 객체의 필드를 단계별로 설정할 수 있으며, 가독성이 높아지고 선택적인 필드를 쉽게 처리할 수 있다.\nUser user = User.builder() .userId(\u0026#34;uniqueUserId123\u0026#34;) .username(\u0026#34;홍길동\u0026#34;) .roles(new HashSet\u0026lt;\u0026gt;()) .build(); 빌더 패턴이 등장하게 된 계기 기존의 생성자 방식은 매개변수가 많아지면 코드가 복잡해지고 가독성이 떨어지는 단점이 있다.\n특히, 동일한 타입의 여러 파라미터가 있을 경우 실수하기 쉽다. 이러한 문제를 해결하기 위해 빌더 패턴이 등장하게 되었다.\n장단점 비교 빌더 패턴의 장점\n가독성 향상 메서드 체이닝을 통해 어떤 필드에 어떤 값을 설정하는지 명확하게 드러난다.\n유연성 선택적인 필드를 쉽게 설정할 수 있으며, 필드의 순서에 상관없이 값을 지정할 수 있다.\n불변 객체 생성\n빌더 패턴을 사용하면 객체를 불변으로 만들기 쉽다.\n복잡한 객체 생성 용이\n객체 생성 과정이 복잡한 경우 빌더 패턴이 더욱 효과적이다.\n빌더 패턴의 단점\n코드량 증가 별도의 빌더 클래스를 작성하거나, Lombok과 같은 라이브러리를 사용하지 않으면 코드가 길어질 수 있다.\n성능 오버헤드\n매우 빈번한 객체 생성을 요구하는 경우 빌더 패턴은 약간의 성능 오버헤드를 유발할 수 있다. 하지만 대부분의 경우 이는 큰 문제가 되지 않는다.\n생성자 방식의 장점\n간단함 단순한 객체를 생성할 때는 생성자 방식이 더 간단하고 직관적이다.\n성능\n빌더 패턴에 비해 약간의 성능 이점을 가질 수 있다.\n생성자 방식의 단점\n가독성 저하 매개변수가 많아지면 생성자의 가독성이 떨어진다.\n유연성 부족 선택적인 필드를 설정하기 어렵고, 필드의 순서에 의존하게 된다.\n유지보수 어려움 필드가 추가되거나 변경될 때마다 생성자도 수정해야 한다.\n요약 빌더 패턴은 객체 생성 시 가독성과 유연성을 높이고, 복잡한 객체를 보다 쉽게 생성할 수 있게 해준다. 특히 필드가 많거나 선택적인 필드가 있는 경우 빌더 패턴이 매우 유용하다.\n반면, 단순한 객체 생성에는 기존의 생성자 방식이 더 간단할 수 있다. 상황에 맞게 적절한 패턴을 선택하는 것이 중요하다.\n","permalink":"https://4d4cat.com/posts/2024/builder-pattern/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e빌더 패턴이란?\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e기존 생성자 방식과의 차이점\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"what-why\"\u003e\u003cstrong\u003eWhat? Why?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e빌더 패턴(Builder Pattern)은 복잡한 객체를 단계별로 생성할 수 있게 해주는 디자인 패턴이다. 객체의 생성 과정을 추상화하여, 다양한 형태의 객체를 유연하게 생성할 수 있도록 도와준다.\u003c/p\u003e\n\u003ch2 id=\"기존-생성자-방식과의-비교\"\u003e\u003cstrong\u003e기존 생성자 방식과의 비교\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"기존-생성자-방식\"\u003e\u003cstrong\u003e기존 생성자 방식\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e기존의 생성자 방식은 객체를 생성할 때 필요한 모든 파라미터를 생성자에 전달해야 한다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kd\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUser\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eLong\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eusername\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003eprivate\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eSet\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eRole\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eroles\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"c1\"\u003e// 기타 필드 및 메서드\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eLong\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eString\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eusername\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eSet\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eRole\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eroles\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003euserId\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003euserId\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eusername\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eusername\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eroles\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eroles\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"빌더-패턴-방식\"\u003e\u003cstrong\u003e빌더 패턴 방식\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e빌더 패턴을 사용하면 객체의 필드를 단계별로 설정할 수 있으며, 가독성이 높아지고 선택적인 필드를 쉽게 처리할 수 있다.\u003c/p\u003e","title":"빌더 패턴 (Builder Pattern)"},{"content":"🔔 테스트 도구 JUnit, Mockito\n🔔 테스트 시나리오 작성, GWT\nJUnit @Test 해당 메서드가 테스트 메서드임을 나타냅니다.\n@Test void myTest() { /* 테스트 로직 */ } @BeforeEach 각 테스트 메서드가 실행되기 전에 실행되는 메서드를 지정합니다.\n@BeforeEach void setUp() { /* 초기화 로직 */ } @AfterEach 각 테스트 메서드가 실행된 후에 실행되는 메서드를 지정합니다.\n@AfterEach void tearDown() { /* 정리 로직 */ } @BeforeAll 모든 테스트 메서드가 실행되기 전에 한 번만 실행되는 메서드를 지정합니다. 정적 메서드이어야 합니다.\n@BeforeAll static void initAll() { /* 초기화 로직 */ } @AfterAll 모든 테스트 메서드가 실행된 후에 한 번만 실행되는 메서드를 지정합니다. 정적 메서드이어야 합니다.\n@AfterAll static void tearDownAll() { /* 정리 로직 */ } @DisplayName 테스트 클래스나 메서드의 이름을 지정하여 가독성을 높입니다.\n@Test @DisplayName(\u0026#34;네이버 검색 API 성공 테스트\u0026#34;) void test() { /*...*/ } Mockito @Mock 설명: Mockito에서 제공하는 어노테이션으로, 단순히 모의 객체를 생성합니다. 사용 용도: 주로 순수한 단위 테스트(Unit Test)에서 사용됩니다. 스프링 컨텍스트와는 별개로 동작합니다. @RunWith(MockitoJUnitRunner.class) public class MyServiceTest { @Mock private DependencyService dependencyService; @InjectMocks private MyService myService; // 테스트 메서드들 } @MockBean 설명: Spring Boot의 테스트 지원 어노테이션으로, 스프링 애플리케이션 컨텍스트 내의 빈(Bean)을 모의 객체로 대체합니다. 사용 용도: 스프링 통합 테스트(Integration Test)나 스프링 부트의 애플리케이션 컨텍스트를 사용하는 테스트에서 주로 사용됩니다. 특징: 스프링의 ApplicationContext에 존재하는 실제 빈을 모의 객체로 교체합니다. 스프링의 의존성 주입(Dependency Injection) 메커니즘과 통합되어 동작합니다. @SpringBootTest public class MyServiceIntegrationTest { @MockBean private DependencyService dependencyService; @Autowired private MyService myService; // 테스트 메서드들 } @InjectMocks 설명: Mockito에서 제공하는 어노테이션으로, @Mock 또는 @Spy로 생성된 모의 객체들을 주입하여 테스트 대상 객체를 생성합니다. 사용 용도: 주로 단위 테스트에서 테스트 대상 객체에 의존성을 주입할 때 사용됩니다. 특징: 생성자, 세터, 필드 주입 방식을 자동으로 처리합니다. @Mock으로 생성된 모든 모의 객체들을 주입합니다. @RunWith(MockitoJUnitRunner.class) public class MyServiceTest { @Mock private DependencyService dependencyService; @InjectMocks private MyService myService; // 테스트 메서드들 } 추가 어노테이션 @Spy 설명: 실제 객체를 스파이(부분 모의 객체)로 감쌉니다. 일부 메서드는 실제로 호출하고, 특정 메서드만 모의할 수 있습니다. 사용 용도: 부분적으로 동작을 변경하고 싶을 때 유용합니다. @Spy private List\u0026lt;String\u0026gt; spyList = new ArrayList\u0026lt;\u0026gt;(); @Captor 설명: ArgumentCaptor를 간편하게 사용하기 위한 어노테이션입니다. 메서드 호출 시 전달된 인자를 캡처할 수 있습니다. 사용 용도: 특정 메서드가 호출될 때 전달된 인자를 검증하고 싶을 때 사용합니다. @Captor ArgumentCaptor\u0026lt;String\u0026gt; captor; @ExtendWith(MockitoExtension.class) (JUnit 5) 설명: JUnit 5에서 Mockito 어노테이션을 활성화하기 위한 확장 기능입니다. 사용 용도: JUnit 5 환경에서 Mockito 어노테이션을 사용하려면 필요합니다. @ExtendWith(MockitoExtension.class) public class MyServiceTest { @Mock private DependencyService dependencyService; @InjectMocks private MyService myService; // 테스트 메서드들 } Mock vs Spy @Mock: 객체의 모든 메서드를 모의(Mock)합니다. 실제 메서드 호출이 일어나지 않으며, 기본값이 반환됩니다. @Spy: 객체의 일부 메서드는 실제로 호출하고, 나머지는 모의(Mock)합니다. 부분적으로 동작을 변경하고 싶을 때 유용합니다. Given-When-Then 패턴 GWT 패턴은 테스트의 구조를 Given, When, Then으로 나누어 명확하게 테스트 시나리오를 작성하는 방식입니다. Mockito의 어노테이션을 활용하면 각 단계에서 필요한 모의 객체를 손쉽게 설정할 수 있습니다.\nGiven: 테스트의 사전 조건을 설정합니다. 예를 들어, 모의 객체의 동작을 정의하거나 초기 데이터를 준비합니다. @Mock, @MockBean When: 테스트 대상 메서드를 호출합니다. Then: 메서드 호출 후의 결과나 상태를 검증합니다. @Captor, verify ","permalink":"https://4d4cat.com/posts/2024/test-annotation/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e테스트 도구 JUnit, Mockito\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e테스트 시나리오 작성, GWT\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"junit\"\u003e\u003cstrong\u003eJUnit\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"test\"\u003e\u003cstrong\u003e@Test\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e해당 메서드가 테스트 메서드임을 나타냅니다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Test\u003c/span\u003e\u003cspan class=\"w\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003emyTest\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"cm\"\u003e/* 테스트 로직 */\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"beforeeach\"\u003e\u003cstrong\u003e@BeforeEach\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e각 테스트 메서드가 실행되기 전에 실행되는 메서드를 지정합니다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@BeforeEach\u003c/span\u003e\u003cspan class=\"w\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003esetUp\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"cm\"\u003e/* 초기화 로직 */\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"aftereach\"\u003e\u003cstrong\u003e@AfterEach\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e각 테스트 메서드가 실행된 후에 실행되는 메서드를 지정합니다.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@AfterEach\u003c/span\u003e\u003cspan class=\"w\"\u003e \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003etearDown\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"cm\"\u003e/* 정리 로직 */\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"beforeall\"\u003e\u003cstrong\u003e@BeforeAll\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e모든 테스트 메서드가 실행되기 전에 한 번만 실행되는 메서드를 지정합니다. 정적 메서드이어야 합니다.\u003c/p\u003e","title":"테스트 도구는 어떤게 있을까?"},{"content":"🔔 테스트 방법\n테스트 방법 새로운 기능을 개발하거나 수정할 때, 내가 원하는 시나리오대로 동작을 하는지 검증하는 과정은 필요하다. 그것을 위해 테스트 과정이 있고 원하는 상황에 원하는 결과가 나오는지 판단해봐야 한다. 어디까지 테스트를 해야하는지는 각 상황마다 다르겠지만 내가 이전 회사에서 테스트할 때 꼭 확인했던 부분은 아래와 같다.\n브라우저 환경(Chrome, Edge, IE 등) 운영/개발 환경 로그인/비로그인 위 항목을 포함하여 상황별 테스트 케이스를 작성하고 테스트를 했었지만 효율적인 테스트 코드를 작성한 것이 아니었다. dev 서버는 해당 서버에 소스를 올린 후에 테스트했었는데 하나라도 잘못된 결과가 나오면 다시 재커밋 후 테스트를 진행했다. 로컬도 마찬가지이지만 이처럼 테스트 코드를 작성하면서 테스트하는 환경이 아니었기 때문에 검증 과정이 불편했다.\n테스트를 제대로 하려면? 테스트는 크게 단위 테스트와 통합 테스트로 나눌 수 있다. 단위 테스트는 개별 메서드나 클래스를 테스트하여 각 단위가 예상대로 동작하는지 검증하는 것이고, 통합 테스트는 여러 단위들이 함께 작동하여 전체 시스템이 기대하는대로 동작하는지 검증한다.\n어썰션(Assertions) 은 테스트에서 특정 조건이 참인지 확인하는 구문이다. 테스트의 성공 여부를 판단하는 요소로, 기대하는 결과와 실제 결과를 비교하여 테스트가 통과했는지 실패했는지를 결정한다.\nassertEquals(expected, actual) assertNotEquals(unexpected, actual) assertTrue(condition) assertFalse(condition) assertNull(object) assertNotNull(object) 단위 테스트 단위 테스트에서는 외부 의존성을 모킹(Mock) 하거나 스텁(Stub) 을 사용하여 독립적으로 수행된다.\n모킹은 테스트 대상 코드가 의존하는 외부 컴포넌트의 행동을 동적으로 정의하고, 호출 여부나 횟수 등을 검증할 수 있다. 따라서 상태 검증보다는 행동 검증에 중점을 둔다.\n스텁은 테스트 대상 코드가 의존하는 외부 컴포넌트의 대체 구현체를 제공하여, 특정 입력에 대해 미리 정의된 출력을 반환한다. 주로 고정된 응답을 제공하여 상태 검증에 중점을 둔다.\n통합 테스트 여러 단위의 테스트를 통합적으로 검증하기 때문에 단위 테스트보다 실행 속도가 느리지만 전체 흐름에 대한 테스트를 검증할 수 있다.\n","permalink":"https://4d4cat.com/posts/2024/test-code/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e테스트 방법\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"테스트-방법\"\u003e\u003cstrong\u003e테스트 방법\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e새로운 기능을 개발하거나 수정할 때, 내가 원하는 시나리오대로 동작을 하는지 검증하는 과정은 필요하다. 그것을 위해 테스트 과정이 있고 원하는 상황에 원하는 결과가 나오는지 판단해봐야 한다.\n어디까지 테스트를 해야하는지는 각 상황마다 다르겠지만 내가 이전 회사에서 테스트할 때 꼭 확인했던 부분은 아래와 같다.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e브라우저 환경(Chrome, Edge, IE 등)\u003c/li\u003e\n\u003cli\u003e운영/개발 환경\u003c/li\u003e\n\u003cli\u003e로그인/비로그인\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e위 항목을 포함하여 상황별 테스트 케이스를 작성하고 테스트를 했었지만 효율적인 테스트 코드를 작성한 것이 아니었다. dev 서버는 해당 서버에 소스를 올린 후에 테스트했었는데 하나라도 잘못된 결과가 나오면 다시 재커밋 후 테스트를 진행했다. 로컬도 마찬가지이지만 이처럼 테스트 코드를 작성하면서 테스트하는 환경이 아니었기 때문에 검증 과정이 불편했다.\u003c/p\u003e","title":"테스트는 어떻게 하는게 좋을까?"},{"content":"🔔 프로젝트 소개\n🔔 프로젝트 개선 과정\n프로젝트 소개 4d4cat 의미 4d4cat 프로젝트는 내가 구현해보고 싶은 기능을 다 넣어서 원하는 때에 사용해보고자 만든 프로젝트이다. 이는 만화 도라에몽에서 나오는 4차원 주머니에서 여러 가지 물건을 꺼내는 것처럼 **4차원(4th-Dimension), 주머니(pocket)**를 변형해서 만든 명칭이다.\n목적 구현하고 싶은 기능은 크게 데이터 수집과 서드 파티 사용이다.\n데이터 수집\n현재 구글 트렌드 데이터 기반으로 일별 인기 검색어를 저장 노출 기간은 최근 한달 이내의 데이터 구글 트렌드 API 관련 오픈소스 사용 인스타그램의 키워드도 수집하려고 했으나 최근에 지원 종료(예정) 서드 파티\n현재 구글 검색, 네이버 검색, ChatGPT 적용 아직 운영 서버에는 배포를 하지 않은 상태 API의 일일 최대 호출 횟수가 정해져 있어서 추가 보안 설정 후에 배포 예정 구글 검색 화면 프로젝트 개선하는 여정 1. 여러 사용자가 각자의 패키지에서 서비스 개발 한 프로젝트에 여러 사용자가 각자 서비스를 만드는게 초기 목적 불필요한 프로젝트 패키지 다수 백은 API 호출만 처리하고 프론트에서 페이지 렌더링 부트스트랩을 활용한 버튼 활성화 및 이미지 처리 다수 2. 프론트와 백을 분리하고 각각 운영해볼까? 구글 트렌드 키워드만 적용한 상태로 프론트, 백 서버를 분리하여 운영 분리하여 운영함으로써 각각 뷰, API 호출이라는 목적을 명확히 하기 위함 프론트(Typescript, Node.js) / 백(Java/Spring) 프론트는 데이터베이스 데이터만 노출시킴 백은 REST API만 적용하여 필요한 데이터만 전달 프론트 Github 3. 하지만 굳이 나눠야할까? 그냥 하나로 처리하자 뷰는 템플릿 엔진으로 대체하고 백엔드에서 유연하게 대체 초기 프로젝트 구조에서 중복 소스 제거 및 디자인 단순화 Cloudflare를 통해 https 적용 리팩토링 커밋\n현재 프로젝트는? Github Pinned 참고\n","permalink":"https://4d4cat.com/posts/2024/4d4cat1/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e프로젝트 소개\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e프로젝트 개선 과정\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"프로젝트-소개\"\u003e\u003cstrong\u003e프로젝트 소개\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"4d4cat-의미\"\u003e\u003cstrong\u003e4d4cat 의미\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e4d4cat 프로젝트는 내가 구현해보고 싶은 기능을 다 넣어서 원하는 때에 사용해보고자 만든  프로젝트이다. 이는 만화 도라에몽에서 나오는 4차원 주머니에서 여러 가지 물건을 꺼내는 것처럼 **4차원(4th-Dimension), 주머니(pocket)**를 변형해서 만든 명칭이다.\u003c/p\u003e\n\u003ch3 id=\"목적\"\u003e\u003cstrong\u003e목적\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e구현하고 싶은 기능은 크게 \u003cstrong\u003e데이터 수집\u003c/strong\u003e과 \u003cstrong\u003e서드 파티\u003c/strong\u003e 사용이다.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e데이터 수집\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e현재 구글 트렌드 데이터 기반으로 일별 인기 검색어를 저장\u003c/li\u003e\n\u003cli\u003e노출 기간은 최근 한달 이내의 데이터\u003c/li\u003e\n\u003cli\u003e구글 트렌드 API 관련 \u003ca href=\"https://github.com/pat310/google-trends-api\"\u003e오픈소스\u003c/a\u003e 사용\u003c/li\u003e\n\u003cli\u003e인스타그램의 키워드도 수집하려고 했으나 최근에 지원 종료(예정)\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e서드 파티\u003c/strong\u003e\u003c/p\u003e","title":"4D4cat) 프로젝트 개선"},{"content":"🔔 TCP/IP 모델의 개념과 동작 원리\n🔔 TCP/IP 모델 계층별 특징 🔔 TCP와 IP 비교\nWhat? Why? OSI 7 Layer 모델이 이론적 모델로 만들어진 것에 비해 TCP/IP 4 layer 모델은 실제 인터넷 프로토콜을 반영하여 실용적으로 설계되었다.\nOSI 모델과의 차이점 Application OSI 모델에서 상위 3개의 계층이 통합된 모습인데, 실제 네트워크에서는 OSI 모델의 각 계층이 반드시 구분될 필요가 없다. 기능이 중복되거나 복잡해지기 때문에 단순화가 필요했다.\nNetwork Access OSI 모델에서 물리 계층이 빠진 모습인데, 물리적 전송 매체와 데이터(Frame)를 하나의 계층에서 관리함으로써 설계를 단순화하고, 다양한 물리 매체에 대한 호환성을 높였다.\n계층별 특징 L4, Application 사용자와 네트워크 간의 인터페이스 제공 이메일, 파일 전송, 웹 브라우징 등 지원 HTTP, FTP, SMTP, DNS L3, Transport 호스트 간의 데이터 전송을 관리 데이터를 패킷으로 분할, 전송, 재조립, 오류 검출 및 복구를 담당 TCP, UDP L2, Internet 네트워크 간의 데이터 패킷을 전달 IP 주소 지정, 라우팅, 패킷 포워딩을 담당 IP, ICMP, ARP L1, Network Access 물리적 네트워크 매체를 통해 데이터 전송 데이터의 물리적 전송, 프레이밍(Framing), 오류 검출을 담당 Ethernet, Wi-Fi TCP vs IP TCP/IP의 주요 작업은 데이터를 다른 장치로 전송하는 것인데, 중요한 것은 데이터를 정확하게 전달하여 수신 측과 발신 측의 데이터가 동일하도록 하는 것이다. 이 과정에서 IP는 데이터를 목적지까지 전달하는 역할을 하고, TCP는 그 데이터가 정확하게 전달되도록 보장한다. 이 두 프로토콜이 함께 작동하여 신뢰성 있는 통신이 가능하게 된다.\nTCP(Transmission Control Protocol) 데이터의 신뢰성 있는 전송 보장 연결 설정, 데이터 전송 확인, 손실된 패킷 재전송 연결형 프로토콜, 무결성 보장, 속도 느림 IP(Internet Protocol) 데이터 패킷의 주소 지정과 전달 경로 설정, 데이터 패킷 라우팅 비연결형 프로토콜, 무결성 보장 못함, 속도 빠름 동작 원리 TCP/IP 모델은 절차에 따라 해당 데이터를 패킷으로 나눈다. 이후 한 순서로 계층을 거친 다음 수신 측에서 데이터가 다시 조립되면서 역순으로 이동한다.\n❗ 그림에 있는 숫자 번호가 Layer 번호를 의미하는 것이 아님\n🌐 흐름도 웹사이트 접근 전체 과정(www.google.com 예시) ╔══════════════════════════════════════════════════════════════════════════╗ ║ 클라이언트 영역 ║ ╚══════════════════════════════════════════════════════════════════════════╝ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ 1. 사용자가 브라우저에 URL 입력 │ │ 🔗 www.google.com │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 2. 브라우저 DNS 캐시 확인 │ │ 💾 브라우저 캐시 → OS 캐시 → 로컬 DNS 캐시 │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ┌─────────┴─────────┐ │ 캐시 히트? │ └─────────┬─────────┘ YES │ NO ▼ ╔══════════════════════════════════════════════════════════════════════════╗ ║ DNS 시스템 ║ ╚══════════════════════════════════════════════════════════════════════════╝ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ 3. 로컬 DNS 서버에 질의 │ │ 🔍 DNS Query: www.google.com │ │ 📍 OSI Layer 7 (Application) / UDP Port 53 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 4. 루트 DNS 서버 질의 │ │ 🌐 Root DNS Server (.com TLD 정보 반환) │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 5. TLD DNS 서버 질의 │ │ 🔗 .com TLD Server (google.com 권한 서버 정보 반환) │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 6. 권한 있는 DNS 서버 질의 │ │ 📧 google.com DNS Server │ │ 💡 IP 주소 반환: 142.250.190.78 │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ╔══════════════════════════════════════════════════════════════════════════╗ ║ 네트워크 연결 ║ ╚══════════════════════════════════════════════════════════════════════════╝ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ 7. TCP 3-Way Handshake │ │ 🤝 클라이언트 → 서버 │ │ SYN → │ │ ← SYN + ACK │ │ ACK → │ │ 📍 OSI Layer 4 (Transport) / Port: 443 (HTTPS) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 8. TLS/SSL Handshake (HTTPS인 경우) │ │ 🔐 암호화 연결 설정 │ │ • 인증서 검증 │ │ • 암호화 알고리즘 협상 │ │ • 세션 키 교환 │ │ 📍 OSI Layer 5 (Session) / TLS 1.3 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ╔══════════════════════════════════════════════════════════════════════════╗ ║ 서버 인프라 ║ ╚══════════════════════════════════════════════════════════════════════════╝ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ 9. 로드밸런서 \u0026amp; CDN │ │ ⚖️ 트래픽 분산 및 지역 최적화 │ │ 🌍 가장 가까운 서버로 라우팅 │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 10. HTTP 요청 전송 │ │ 📤 GET / HTTP/2 │ │ Host: www.google.com │ │ User-Agent: Chrome/120.0 │ │ Accept: text/html,application/xhtml+xml │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 11. 웹서버 요청 처리 │ │ 🖥️ Apache/Nginx가 요청 수신 │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 12. 애플리케이션 서버 처리 │ │ ⚙️ 비즈니스 로직 실행 │ │ 📊 필요시 데이터베이스 조회 │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 13. HTTP 응답 생성 및 전송 │ │ 📨 HTTP/2 200 OK │ │ Content-Type: text/html; charset=UTF-8 │ │ Content-Encoding: gzip │ │ + HTML 데이터 │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ╔══════════════════════════════════════════════════════════════════════════╗ ║ 브라우저 렌더링 ║ ╚══════════════════════════════════════════════════════════════════════════╝ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ 14. HTML 파싱 및 DOM 생성 │ │ 🔍 HTML 문서 파싱 │ │ 🌳 DOM 트리 구성 │ │ 📍 Browser Rendering Engine │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 15. CSS 파싱 및 CSSOM 생성 │ │ 🎨 CSS 스타일시트 파싱 │ │ 📐 CSSOM 트리 구성 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 16. 추가 리소스 병렬 다운로드 │ │ ⚡ HTTP/2 멀티플렉싱 활용 │ │ • JavaScript 파일 │ │ • 이미지 파일 │ │ • 폰트 파일 │ │ • 기타 CSS 파일 │ │ 📍 OSI Layer 7 (Application) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 17. 렌더 트리 구성 및 레이아웃 │ │ 🖼️ DOM + CSSOM = 렌더 트리 │ │ 📏 레이아웃 계산 (리플로우) │ │ 🎨 페인팅 (리페인트) │ │ 🔧 컴포지팅 │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 18. JavaScript 실행 │ │ ⚡ DOM 조작 및 이벤트 처리 │ │ 🔄 AJAX 요청 (필요시) │ │ 📍 Browser JavaScript Engine │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ╔══════════════════════════════════════════════════════════════════════════╗ ║ 연결 정리 ║ ╚══════════════════════════════════════════════════════════════════════════╝ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ 19. 연결 관리 │ │ 🔄 HTTP Keep-Alive (연결 유지) 또는 │ │ 👋 TCP 4-Way Handshake (연결 종료) │ │ FIN → │ │ ← ACK │ │ ← FIN │ │ ACK → │ │ 📍 OSI Layer 4 (Transport) │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 🎉 웹페이지 로딩 완료! │ │ 👤 사용자가 구글 홈페이지를 볼 수 있음 │ └─────────────────────────────────────────────────────────────────────────┘ 🔄 데이터 흐름 분석 클라이언트 ────DNS 질의───► DNS 서버들 ────IP 응답───► 클라이언트 │ │ └──────────────── TCP/TLS 연결 설정 ────────────────────┘ │ │ └─────────── HTTP 요청/응답 (양방향 통신) ────────────────┘ 🏗️ 네트워크 스택별 처리 애플리케이션 계층 (L7) │ HTTP, DNS, TLS 핸드셰이크 세션 계층 (L5) │ TLS/SSL 암호화 전송 계층 (L4) │ TCP 연결 관리 네트워크 계층 (L3) │ IP 라우팅 데이터링크 계층 (L2) │ MAC 주소 매핑 물리 계층 (L1) │ 실제 네트워크 전송 ⏱️ 시간별 처리 순서 DNS 해석 (1-100ms) → TCP 연결 (10-100ms) → TLS 핸드셰이크 (50-200ms) → HTTP 요청/응답 (10-500ms) → 브라우저 렌더링 (100-2000ms) 🚀 성능 최적화 포인트 DNS 캐싱: 반복 질의 시간 단축 HTTP/2 멀티플렉싱: 병렬 리소스 다운로드 CDN 활용: 지역별 서버 분산 Keep-Alive: TCP 연결 재사용 압축: Gzip/Brotli로 전송 데이터 크기 감소 🔒 보안 요소 HTTPS/TLS: 데이터 암호화 인증서 검증: 서버 신원 확인 HSTS: 강제 HTTPS 연결 CSP: 콘텐츠 보안 정책 자료 출처 Geeksforgeeks AVG Tistory ChatGPT\n","permalink":"https://4d4cat.com/posts/2024/tcpip-4-layer/","summary":"\u003cp\u003e🔔 \u003cstrong\u003eTCP/IP 모델의 개념과 동작 원리\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003eTCP/IP 모델 계층별 특징\u003c/strong\u003e  \u003cbr\u003e\n🔔 \u003cstrong\u003eTCP와 IP 비교\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"what-why\"\u003e\u003cstrong\u003eWhat? Why?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003eOSI 7 Layer 모델이 이론적 모델로 만들어진 것에 비해 TCP/IP 4 layer 모델은 실제 인터넷 프로토콜을 반영하여 실용적으로 설계되었다.\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"https://blog.kakaocdn.net/dn/Go5Mi/btrKwcuCMl2/KNImS8Scha5xinTYAyNm11/img.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"osi-모델과의-차이점\"\u003e\u003cstrong\u003eOSI 모델과의 차이점\u003c/strong\u003e\u003c/h2\u003e\n\u003ch3 id=\"application\"\u003eApplication\u003c/h3\u003e\n\u003cp\u003eOSI 모델에서 상위 3개의 계층이 통합된 모습인데, 실제 네트워크에서는 OSI 모델의 각 계층이 반드시 구분될 필요가 없다. 기능이 중복되거나 복잡해지기 때문에 단순화가 필요했다.\u003c/p\u003e\n\u003ch3 id=\"network-access\"\u003eNetwork Access\u003c/h3\u003e\n\u003cp\u003eOSI 모델에서 물리 계층이 빠진 모습인데, 물리적 전송 매체와 데이터(Frame)를 하나의 계층에서 관리함으로써 설계를 단순화하고, 다양한 물리 매체에 대한 호환성을 높였다.\u003c/p\u003e","title":"TCP/IP 4계층"},{"content":"🔔 라우터의 개념\n🔔 라우터와 비슷한 장치\n🔔 라우팅 테이블과 라우팅 동작 원리\nWhat? Why? 네트워크 장치 중 하나로, 서로 다른 네트워크 간의 데이터를 전달하고 관리하는 역할을 한다. 주로 LAN, WAN을 연결하며, 데이터 패킷이 목적지까지 효율적으로 도달할 수 있도록 경로를 결정한다.\n패킷 전달 수신된 데이터 패킷의 헤더 정보를 분석하여, 목적지 주소에 따라 적절한 네트워크 경로로 전달한다.\n라우팅 라우팅 테이블을 유지하며, 이를 통해 최적의 경로를 결정한다.\n네트워크 분할 대규모 네트워크를 작은 서브넷으로 분할하여 트래픽을 효율적으로 관리할 수 있다.\n규모나 용도에 따라 여러 유형이 있다. 나무위키\n주요 기능 NAT(Network Address Translation)\n사설 IP 주소를 공인 IP주소로 변환하여 여러 장치가 하나의 공인 IP 주소를 공유할 수 있게 하는 기술\nVPN(Virtual Private Network)\n공용 인터넷을 통해 사설 네트워크에 접근할 수 있게 하는 기술\nVLAN(Virtual LAN) 하나의 물리적 네트워크를 논리적으로 여러 개의 네트워크로 분할\nDHCP(Dynamic Host Configuration Protocol) 네트워크 장치에 자동으로 IP 주소와 기타 네트워크 설정을 할당하는 프로토콜\n라우터와 비슷한 장치들의 차이점 스위치 동일한 네트워크 내에서 장치 간의 데이터 통신을 관리하고 전달한다. 기본적으로 L2에서 MAC 주소를 기반으로 데이터를 전달하는 역할이지만 L3에서 동작하는 장비도 있다.\n공유기 라우터의 기능을 포함하면서 무선 접속 기능(Wi-Fi)을 제공하는 네트워크 장비\n라우팅과 라우팅 테이블 라우팅이란 하나 이상의 네트워크에서 경로를 선택하는 프로세스이다. 인터넷 프로토콜(IP)을 통해 타겟으로 이동할 경로를 선택한다. 아래 이미지에서 데이터가 컴퓨터 A에서 컴퓨터 B로 전달되려면 어떤 경로를 선택하는게 좋을까요?\n개수만 봤을 때는 네트워크 2, 4를 통과하는게 짧을 수 있지만 네트워크 1, 3, 5를 통과할 때 더 빨리 전달될 수 있습니다. 라우터에서는 지속해서 이런 종류의 선택을 하게 된다.\n라우터는 내부 라우팅 테이블을 참조하여 네트워크 경로를 따라 패킷을 라우팅하는 방법을 결정한다. 라우팅 테이블에는 패킷이 라우터가 담당하는 모든 대상에 도달하기 위해 택해야 하는 경로가 기록된다. 열차 승객이 어떤 열차를 타야 할지 결정하기 위해 참고하는 열차 시간표를 생각해보면 좋다.\n위 그림에서는 1.1.1.1 컴퓨터 A가 3.3.3.3 컴퓨터 B로 데이터를 송신하기 위해 라우팅하는 과정이다. 라우팅 테이블에 저장된 정보를 이용하여 A가 라우터 2를 거쳐서 B에 라우팅된다. 그러면 라우팅 테이블에 데이터가 어떤 식으로 저장이 될까?\n정적 라우팅 테이블\n네트워크 관리자가 네트워크 경로를 수동으로 구성하고 선택 네트워크 설계나 파라미터가 일정하게 유지되는 경우에 유용함 데이터를 업데이트하지 않으면 유연성이 저하되어 성능에 제한이 생김 동적 라우팅 테이블\n다양한 라우팅 프로토콜을 사용하여 최단 경로를 결정함 트래픽 볼륨, 네트워크 장애 등 변화하는 네트워크 조건에 대응 가능 더 많은 컴퓨팅 성능이 필요함 라우팅 프로토콜 네트워크 경로를 식별하거나 알리는 데 사용되는 프로토콜이다.\nRIP(Routing Information Protocol, 라우팅 정보 프로토콜)\n홉 수를 기준으로 네트워크 간의 최단 경로를 결정 대규모 네트워크를 구현하는 데 적합하지 않기 때문에 현재는 사용되지 않는 레거시 프로토콜 BGP(Border Gateway Protocol, 경계 게이트웨이 프로토콜)\n어떤 네트워크에서 어떤 IP 주소를 제어하고 어떤 네트워크가 서로 연결되는지 알리는 데 사용 위 같은 알림을 자율 시스템이라고 하는데 이런 자율 시스템 간에 사용되는 프로토콜 OSPF(Open Shortest Path First, 최단 경로 우선 프로토콜)\n비용이 최소인 경로를 결정 L3 스위치 스위치는 라우터를 통해 들어온 트래픽을 MAC 주소에 맞는 목적지로 전달한다. 기본적으로 스위치는 L2에서 사용되지만 L3에서 사용하는 스위치도 있다. 이것을 각각 L2 스위치, L3 스위치라고 부른다.\nL3 스위치는 라우터와 동일하게 네트워크 계층에서 라우팅 기능을 제공하지만 라우터가 다른 네트워크 간의 트래픽을 관리한다면, L3 스위치는 내부 네트워크에서 라우팅을 제공한다.\n자료 출처 Cloudflare Velog AWS\nChatGPT\n추가 질문 라우터의 주요 기능들의 이점? NAT\n제한된 수의 공인 IP 주소만을 사용하여 더 많은 장치들이 인터넷에 접근 가능하고 외부 네트워크 접근을 통제하여 보안을 강화\nVLAN\nVLAN을 통해 네트워크를 세분화 함으로써 VLAN 별로 다른 네트워크 정책을 적용 가능\nDHCP\nDHCP를 사용하지 않으면 수동으로 IP를 할당하게 된다. 그러면 추후 추가 작업이 생길 수 있으며, 중복 IP가 발생할 수도 있고 네트워크 확장에 어려움이 생김\n라우팅 프로토콜은 각각 어느 상황에 사용? RIP\n작은 규모의 네트워크에서 사용되며, 홉 수를 기준으로 최단 경로를 결정함\nBGP\n인터넷에서 사용되며, 자율 시스템 간의 경로 정보를 교환함\nOSPF\n대규모 네트워크에서 사용되며, 비용이 최소인 경로를 계산하여 경로를 결정함\n","permalink":"https://4d4cat.com/posts/2024/router/","summary":"\u003cp\u003e🔔 \u003cstrong\u003e라우터의 개념\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e라우터와 비슷한 장치\u003c/strong\u003e\u003cbr\u003e\n🔔 \u003cstrong\u003e라우팅 테이블과 라우팅 동작 원리\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"what-why\"\u003e\u003cstrong\u003eWhat? Why?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e네트워크 장치 중 하나로, 서로 다른 네트워크 간의 데이터를 전달하고 관리하는 역할을 한다. 주로 \u003ca href=\"https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-a-wan/\"\u003eLAN, WAN\u003c/a\u003e을 연결하며, 데이터 패킷이 목적지까지 효율적으로 도달할 수 있도록 경로를 결정한다.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e패킷 전달\u003c/strong\u003e  \u003cbr\u003e\n수신된 데이터 패킷의 헤더 정보를 분석하여, 목적지 주소에 따라 적절한 네트워크 경로로 전달한다.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e라우팅\u003c/strong\u003e \u003cbr\u003e\n라우팅 테이블을 유지하며, 이를 통해 최적의 경로를 결정한다.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e네트워크 분할\u003c/strong\u003e  \u003cbr\u003e\n대규모 네트워크를 작은 서브넷으로 분할하여 트래픽을 효율적으로 관리할 수 있다.\u003c/p\u003e","title":"라우터"},{"content":"사용자가 도메인을 입력했을 때, 우선적으로 해당 도메인의 IP가 캐싱 되어있는지 확인하게 된다. 캐싱 되어있지않다면 해당 IP 주소와 관련된 지정 TTL이 허용하는 기간 동안 응답을 캐시에 저장한다.\nDNS 쿼리를 조기에 확인하여 로딩 시간이 향상되고, 대역폭/CPU 소비가 줄어듬 - DNS 캐시되지 않은 응답 - DNS 캐시된 응답 1. 브라우저 캐싱 : 최신 브라우저는 기본적으로 정해진 시간 동안 DNS 레코드를 캐시하도록 설계되어있다. 웹 브라우저와 가까울수록 캐시를 확인하고 IP 주소에 대한 요청을 처리하는 단계가 적어지기 때문이다.\n예시로 Chrome의 경우 아래 주소에서 DNS 캐시의 상태를 볼 수 있다.\nchrome://net-internals/#dns\n2. OS 수준 캐싱 : 운영체제마다 DNS 캐싱 방식과 관리하는 방법이 다르지만 공통으로 DNS Resolver로 쿼리가 요청되기 전에 요청을 받는다. Stub Resolver라고 불리우며 DNS Resolver의 하위 집합이라고 보면 된다. DNS Resolver와의 차이점은 스스로 재귀를 수행하지 않고 캐시를 공동으로 공유되기 때문에 DNS Resolver에 캐시가 있다면 더 빨리 IP 주소를 확인하여 반환한다.\nDNS 조회 과정에서 DNS Resolver 앞에 Stub Resolver가 추가됐다고 보면 된다.\n장점 성능 향상 : 도메인 이름 해석 시간을 단축시켜 웹 페이지 로딩 속도를 개선 네트워크 부하 감소: DNS 서버로의 반복적인 조회 요청을 줄여 네트워크 트래픽을 감소 가용성 증대: DNS 서버에 일시적인 장애가 발생해도 캐시된 정보를 통해 도메인 해석이 가능 단점 정보의 최신화: DNS 레코드가 변경되었을 때, 캐시된 정보가 만료될 때까지 새로운 정보를 반영하지 못함 보안 취약점: Cache Poisoning 공격 등으로 악성 IP 주소가 캐시에 삽입될 수 있음 캐시 관리의 복잡성: TTL 값 설정이 부적절할 경우, 캐시의 효율성과 최신성 사이에 균형을 맞추기 어려움 DNS 레코드 : 도메인 이름과 관련된 정보를 저장하고 제공한다. 각 레코드는 특정한 목적과 기능을 가지며, 인터넷 상에서 도메인 이름 해석, 이메일 라우팅, 서비스 위치 지정 등 여러 가지 작업을 지원한다.\nA : 웹사이트 접속 시 도메인 이름을 IP 주소로 해석하여 연결\nAAAA\n: IPv6 네트워크를 사용하는 환경에서 웹사이트 접속\nCNAME : 여러 도메인을 동일한 IP 주소로 관리할 때 사용\n레코드별 캐싱 전략 예시 1. 자주 변경을 하는가? 변경이 적은 레코드\n: NS, SOA 레코드 등 변경이 드물게 발생하면 높은 TTL을 설정하여 캐시 효율성을 극대화할 수 있다. 변경이 잦은 레코드\n: A, AAAA 등 IP 주소가 자주 변경될 가능성이 있으면 낮은 TTL을 설정하여 변경 사항을 빠르게 반영할 수 있도록 한다. 2. 사용 목적 A, AAAA\n: 웹사이트 접속 속도를 최적화하기 위해 캐시 효율성을 높이는 것이 중요함\nMX\n: 이메일 서비스의 가용성과 신뢰성을 보장하기 위해, TTL과 우선순위를 신중하게 설정\n3. 적정 시간 : 단위는 초(sec)로 표현하며 시스템 환경마다, 레코드마다 전부 다르겠지만 일반적으로 아래와 같이 추정한다고 한다.\n300초 = 5분 = Very Short\n: 빠른 변경을 위해 낮은 TTL에 초점을 맞춤\n3,600초 = 1시간 = Short\n: 위와 동일하게 빠른 변경을 위해 낮은 TTL에 초점을 맞춤\n86,400초 = 1일 = Long\n: 일일 캐시 활용에 더 초점을 둔 상태\n604,800초 = 7일 = Very Long\n: 흔하지 않지만, 자주 변경되지 않는 게시되거나 신뢰할 수 있는 정보가 포함된 사이트에 사용됨\nDNS 보안 : 캐싱은 여러 이점을 제공하지만 잘못된 정보나 만료된 레코드을 해결하지 못할 때 여러 위협을 받을 수 있다.\n1. DNS cache poisoning(DNS spoofing) : DNS 캐시에 잘못된 정보를 입력하여 DNS 쿼리가 잘못된 응답을 반환하고, 사용자가 잘못된 사이트로 연결되도록 하는 행위. 공격자는 DNS 네임 서버를 가장하여 DNS Resolver에 요청을 보낸 후, 해당 쿼리를 처리할 때 응답을 위조하여 DNS 캐시를 감염시킬 수 있다. 이는 DNS 서버가 UDP를 사용하고 현재 DNS 정보에 대한 확인이 없기 때문이다.\nDNS 캐시 악성 침입 프로세스 example.com 도메인에 대한 IP 요청 권한 있는 네임 서버에 example.com에 대한 IP 요청 권한 있는 네임 서버가 example.com의 IP가 192.0.0.16라는 것을 반환\n3-1. (3이 실행될 때 공격자가) example.com의 IP가 192.0.0.17라는 것을 반환 악성 침입된 DNS 캐시 example.com(192.0.0.16)을 원했지만 공격자에 의해 example.com를 접근할 때 192.0.0.17로 이동하게 된다.\n2. 어떻게 막아야 할까? : 통신을 시작하기 위해 handshake를 수행해야 하는 TCP를 사용하는 대신, DNS 요청 및 응답에는 UDP가 사용된다. UDP는 연결이 열려 있거나 받는 사람이 받을 준비가 되었다는 보장이 없기 때문에 위조에 취약하다.\n하지만 DNS 악성 침입이 쉽지가 않은데 DNS Resolver가 실제로 권한 있는 네임 서버를 쿼리하기 때문에, 공격자가 권한 있는 네임 서버의 실제 응답이 도착하기 전의 몇 밀리초 만에 가짜 응답을 보내야 하기 때문이다. 그리고 다음과 같은 요소를 알고 있거나 추측해야 공격이 가능하다.\n아직 캐시되지 않은 상태로 DNS 쿼리가 진행될 때 DNS Resolver가 사용 중인 포트 예전에는 모든 쿼리가 동일한 포트를 사용했지만, 이제는 매번 다른 무작위 포트를 사용함 요청 ID 번호 쿼리를 받는 권한 있는 네임 서버 3. DNSSEC(Domain Name System Security Extensions) 기존의 DNS 레코드에 암호화 서명을 추가하여 A, AAAA, MX, CNAME 등과 같은 일반적인 레코드 유형과 함께 DNS 네임 서버에 저장된다. 관련된 서명을 점검함으로써 요청된 DNS 레코드가 권한 있는 네임 서버에서 왔으며 중간자 공격으로 삽입된 가짜 레코드와 달리 전송 중 경로에서 변경되지 않았음을 확인할 수 있다. TLS/SSL과 흡사하게 공개 키 암호화 방식을 사용하여 데이터를 확인하고 인증한다. 하지만 아직 주류가 아니므로 DNS는 여전히 공격에 취약하다. 자료 출처 Cloudflare NsLookup varonis\nChatGPT\n추가 질문 Stub Resolver가 구체적으로 뭐지? DNS 요청을 처리하는 클라이언트 측의 DNS Resolver 운영체제나 어플리케이션에서 실행되며, 주로 DNS 쿼리를 시작하고 결과를 반환 일반적으로 도메인을 입력했을 때 IP를 가져오는 것은 브라우저를 통한 방식으로\n브라우저에서 실행되는게 아닌 경우 Stub Resolver를 우선으로 거치게 됨 DNS의 장비를 교체하면 캐시 웜업은 어떻게 되나? 서버 환경, 관리자 기준마다 다르겠지만 캐시 히트율, 쿼리 응답시간, CPU/메모리 사용량 등에 따라 방법이 달라진다.\n트래픽 기반\n신규 장비는 캐시가 비어 있으므로, 기존 DNS 서버의 쿼리 로그를 수집하여 자주 요청되는 도메인 목록을 추출한다. 이후 추출된 도메인으로 새로운 DNS 장비에 쿼리를 실행하여 캐시를 저장한다.\n점진적 트래픽 전환\n신규 장비에 일부 트래픽만 우선 전달하고 트래픽 비율을 단계적으로 증가시킨다. 이렇게 해서 캐시가 자연스럽게 채워지도록 유도한다.\n","permalink":"https://4d4cat.com/posts/2024/dns-caching/","summary":"\u003cp\u003e사용자가 도메인을 입력했을 때, 우선적으로 해당 도메인의 IP가 캐싱 되어있는지 확인하게 된다. 캐싱 되어있지않다면 해당 IP 주소와 관련된 지정 \u003ca href=\"https://www.cloudflare.com/ko-kr/learning/cdn/glossary/time-to-live-ttl\"\u003eTTL\u003c/a\u003e이 허용하는 기간 동안 응답을 캐시에 저장한다.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eDNS 쿼리를 조기에 확인하여 로딩 시간이 향상되고, 대역폭/CPU 소비가 줄어듬\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e- DNS 캐시되지 않은 응답\u003c/strong\u003e\n\u003cimg loading=\"lazy\" src=\"/images/2024/dns3.svg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e- DNS 캐시된 응답\u003c/strong\u003e\n\u003cimg loading=\"lazy\" src=\"/images/2024/dns4.svg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-브라우저-캐싱\"\u003e\u003cstrong\u003e1. 브라우저 캐싱\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e: 최신 브라우저는 기본적으로 정해진 시간 동안 DNS 레코드를 캐시하도록 설계되어있다. 웹 브라우저와 가까울수록 캐시를 확인하고 IP 주소에 대한 요청을 처리하는 단계가 적어지기 때문이다.\u003cbr\u003e\n예시로 \u003ccode\u003eChrome\u003c/code\u003e의 경우 아래 주소에서 DNS 캐시의 상태를 볼 수 있다.\u003c/p\u003e","title":"DNS 캐싱"},{"content":"What? Why? 사용자에게 친숙한 도메인 이름을 컴퓨터가 네트워크에서 서로를 식별하는 데 사용하는 인터넷 프로토콜(IP) 주소로 변환하는것\n초기 인터넷에서는 각 컴퓨터의 이름과 IP 주소를 호스트 파일이라는 단일 파일에 저장하여 관리했다. 이 방식은 소규모 네트워크에서는 효과적이었지만, 인터넷이 급격히 성장하면서 수많은 도메인과 IP 주소를 관리하는 것이 비효율적이고 불가능해졌다. 이를 해결하기 위해 DNS가 개발되었는데 분산형 계층 구조를 통해 도메인 이름을 효율적으로 관리하고, 사용자가 웹사이트에 쉽게 접근할 수 있도록 도와준다.\n요약하면 아래와 같다.\n확장성: 수많은 도메인과 IP 주소를 효율적으로 관리 편의성: 사람이 이해하기 쉬운 도메인 이름을 사용하여 웹사이트에 접근 분산 관리: 전 세계적으로 분산된 서버를 통해 빠르고 안정적인 이름 해석을 제공 주요 DNS 목록 DNS 서버 유형 사용자 검색을 IP 주소로 변환하는 과정에는 네 가지 유형의 DNS 서버가 관여한다.\n1. 재귀 DNS 서버 (Recursive Resolver, Local DNS) : 사용자의 DNS 쿼리를 처음으로 받아 처리하는 서버\n캐시된 데이터를 이용해 빠르게 응답하거나, 캐시가 없을 경우 다른 DNS 서버에 질의 응답 받은 데이터를 캐시에 저장하여 향후 검색 속도 향상 번역된 내용에서 재귀 확인자 또는 재귀적 리졸버라고 직역해서 나오는데 같은거라고 보면 됩니다.\n2. 루트 네임 서버 : 재귀 DNS 서버가 캐시된 데이터가 없을 때 질의를 전달하는 최상위 서버\n최상위 도메인(TLD) 이름 서버의 위치 정보를 제공 ICANN이 운영하는 13개의 주요 루트 서버가 존재 3. 최상위 도메인(TLD) 네임 서버 : 특정 최상위 도메인(.com, .org, .net 등)에 대한 정보를 관리\n해당 도메인 확장자에 맞는 권한 있는 이름 서버로 질의를 전달 4. 권한 있는 네임 서버 (Authoritative Name Server) : 특정 도메인에 대한 최종 DNS 응답을 제공하는 서버\n도메인 이름과 관련된 DNS 레코드를 저장 및 제공 올바른 IP 주소를 재귀 DNS 서버에 반환하거나, 정보가 없을 경우 오류 메시지 전달 DNS 조회 과정 위 그림은 example.com을 입력했을 때 DNS를 조회하는 단계를 나타낸 것이다.\n1. 사용자 요청 사용자가 웹 브라우저에 example.com을 입력하면 DNS Resolver에 전달된다. 2. DNS Resolver 요청 사용자의 DNS 요청을 처리하는 중간 서버로, 수신된 쿼리를 루트 서버에 전달한다. 3. 캐시 확인 캐시를 확인 후 example.com의 IP 주소가 있는지 확인한다. 캐시가 있으면 사용자에게 반환하고 과정이 종료되며, 캐시가 없다면 다음 단계를 진행한다. 4. 루트 서버 조회 캐시에 IP 주소가 없으므로 Resolver는 루트 서버에 쿼리를 보내고, 루트 서버는 .com TLD 서버의 주소를 반환한다. 5. TLD 서버 조회 Resolver는 전달 받은 TLD 서버 주소를 보내고, 요청된 도메인의 권한 있는 네임 서버 주소를 반환한다. 6-7. 권한 있는 네임 서버 조회 Resolver는 전달 받은 권한 있는 네임 서버 주소를 보내고, 최종적으로 요청된 도메인에 대한 정확한 IP 주소를 반환한다. 8. IP 주소 반환 및 캐싱 사용자한테 전달 받은 IP 주소를 반환하고 이 IP 주소를 자체 캐시에 저장하여 향후 동일한 도메인에 대한 요청 시 빠르게 응답할 수 있도록 한다. 9. HTTP 요청 및 응답 브라우저는 해당 IP 주소로 HTTP 요청을 보낸다. 10. 웹 페이지 반환 해당 IP의 서버가 브라우저에서 렌더링할 웹 페이지를 반환한다. 재귀 쿼리 (Recursive Query) : DNS 클라이언트가 DNS 서버에 모든 필요한 조회를 수행하여 최종적인 응답을 반환하도록 요청한다. 클라이언트는 하나의 요청만 보내며, DNS 서버가 필요한 모든 조회를 수행한다.\n반복 쿼리 (Iterative Query) : DNS 클라이언트가 DNS 서버에 직접적으로 답변할 수 없는 경우, 다른 DNS 서버의 주소를 반환하도록 요청한다. 클라이언트는 반환된 주소로 다시 조회를 수행한다.\n관련 내용이 너무 많아서 레코드, 캐시, 보안 등은 다음 포스트에서 이어집니다.\n자료 출처 IBM 위키백과 Cloudflare ChatGPT\n추가 질문 기본 DNS는 무엇인지? 아래 process의 재귀 DNS ip인가? 기본 DNS는 사용자가 별도로 설정하지 않았을 때 자동으로 할당되는 DNS 서버이다.\n일반적으로 인터넷 서비스 제공업체(ISP)가 고객에게 기본 DNS 서버 주소를 제공하며, 컴퓨터는 이 기본 DNS 서버를 통해 도메인 이름을 해석한다.\nDNS의 운영주체는 왜 나누어져 있는가? 운영주체가 각각의 DNS라면 내가 naver.com을 가고싶을때 어떤 운영주체의 DNS로 이동하는가? 도메인은 DNS Resolver(= 기본 DNS or ISP 등)를 거치게 된다. 여기에 naver.com에 대한 IP 주소가 있으면 바로 사용자한테 반환하고, 주소가 없으면 그 다음 단계를 따른다.\n보조 DNS는 의도가 무엇인가? 기본 DNS를 보조하는 DNS 서버이다. 기본 DNS와 트래픽을 분산하여 처리할 수도 있고, 기본 DNS에 문제가 생겼을 때 대체 서버 역할을 하기도 한다.\n","permalink":"https://4d4cat.com/posts/2024/dns/","summary":"\u003ch2 id=\"what-why\"\u003e\u003cstrong\u003eWhat? Why?\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e사용자에게 친숙한 도메인 이름을 컴퓨터가 네트워크에서 서로를 식별하는 데 사용하는 인터넷 프로토콜(IP) 주소로 변환하는것\u003c/p\u003e\n\u003cp\u003e초기 인터넷에서는 각 컴퓨터의 이름과 IP 주소를 \u003ccode\u003e호스트 파일\u003c/code\u003e이라는 단일 파일에 저장하여 관리했다. 이 방식은 소규모 네트워크에서는 효과적이었지만, 인터넷이 급격히 성장하면서 수많은 도메인과 IP 주소를 관리하는 것이 비효율적이고 불가능해졌다. 이를 해결하기 위해 DNS가 개발되었는데 \u003cstrong\u003e분산형 계층 구조\u003c/strong\u003e를 통해 도메인 이름을 효율적으로 관리하고, 사용자가 웹사이트에 쉽게 접근할 수 있도록 도와준다.\u003c/p\u003e\n\u003cp\u003e요약하면 아래와 같다.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e확장성\u003c/strong\u003e: 수많은 도메인과 IP 주소를 효율적으로 관리\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e편의성\u003c/strong\u003e: 사람이 이해하기 쉬운 도메인 이름을 사용하여 웹사이트에 접근\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e분산 관리\u003c/strong\u003e: 전 세계적으로 분산된 서버를 통해 빠르고 안정적인 이름 해석을 제공\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"주요-dns-목록\"\u003e\u003cstrong\u003e주요 DNS 목록\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2024/dns1.png\"\u003e\u003c/p\u003e","title":"DNS (Domain Name System)"},{"content":"HTTPS (HTTP Secure) : HTTP의 보안이 강화된 버전이다. 소켓 통신에서 일반 텍스트를 이용하는 대신에, SSL이나 TLS 프로토콜을 통해 세션 데이터를 암호화한다.\n1. HTTP와의 차이점 요약하면 HTTPS는 다음과 같은 차이점이 있다.\n보안성: 보안 인증서를 통해 데이터 전송 시 암호화를 제공하여 보안을 강화 무결성: 데이터가 전송 중에 변경되지 않았음을 보장 신뢰성: 검색 엔진에서 우선순위를 우대받으며, 사용자에게 더 신뢰감을 보장 2. 동작 원리 1) URL 파싱 및 DNS 조회 브라우저는 URL을 확인하고 DNS를 통해 도메인을 IP 주소로 변환하여 전달한다. 2) 보안 연결 설정 서버는 TLS handshake를 통해 인증서를 클라이언트에 전송한다. 이 인증서에는 서버의 공개 키와 인증기관(CA)의 서명이 포함되어 있다. 3) 서버 처리 및 응답 생성 보안 연결이 설정되면 암호화된 요청을 서버로 보낸다.\n서버는 요청을 해독하여 요청된 데이터, 상태코드 등으로 구성된 HTTPS 응답을 생성한다. 4) 응답 암호화 및 전송 서버는 TLS를 이용하여 응답을 암호화하고 클라이언트의 브라우저로 다시 전송한다. 이 암호화를 통해 전송되는 동안 데이터의 보안이 유지되고 데이터를 가로채더라도 내용을 해독할 수 없다. 5) 클라이언트에서의 처리 암호화된 응답을 수신한 브라우저는 이를 해독한다.\n여기에는 웹 페이지를 표시하도록 렌더링하는 작업이 포함된다. 데이터가 완전히 전송되고 렌더링되면 TLS 연결은 종료된다. 3. 전달 내용 비교 ex) HTTP 요청 (평문 전송)\nPOST /login HTTP/1.1 Host: www.example.com Content-Type: application/x-www-form-urlencoded Content-Length: 40 username=alice\u0026amp;password=securepassword 공격자가 볼 수 있는 내용\nPOST /login HTTP/1.1 Host: www.example.com ... HTTPS 적용시 공격자가 보는 내용\n(읽을 수 없는 암호화된 데이터) �\u0012�#�\u0017�l�\u0018��*�\u0018�E�J�... SSL (Secure Sockets Layer) : 클라이언트와 서버 간의 안전한 링크를 통해 송수신되는 모든 데이터를 안전하게 보장하는 과거의 보안 표준 기술이다. 현재는 보안 취약점 때문에 TLS 프로토콜을 대신 사용하고 있으며 문제가 됐던 취약점은 아래와 같다.\nPOODLE 공격 (Padding Oracle On Downgraded Legacy Encryption) BEAST 공격 (Browser Exploit Against SSL/TLS) CRIME 공격 (Compression Ratio Info-leak Made Easy) Heartbleed 버그 로그 제이션 공격 (Logjam Attack) FREAK 공격 (Factoring RSA Export Keys) 대체로 보안 허점을 노려 암호화된 데이터를 복호화해서 정보를 탈취하는 방식이다.\nTLS (Transport Layer Security) : 위와 같은 문제로 TLS가 등장했으며, SSL의 취약점을 개선하고 표준으로 자리잡게 되었다.\n다음은 TLS의 버전별 개선사항이다. TLS 1.0 SSL 3.0을 기반으로 개발된 초기 버전 SSL 보안취약점 일부 대응 가능 현재 대부분의 환경에서 지원 중단 TLS 1.1 초기화 벡터(IV) 방식 개선으로 BEAST 공격 완화 현재 대부분의 환경에서 지원 중단 TLS 1.2 SHA-256 해시 함수 도입 인증서 기반 handshake 강화 현재 널리 사용되며, 기본 TLS 버전으로 채택 TLS 1.3 handshake 과정을 대폭 간소화하여 지연 시간 감소 암호화 알고리즘을 최신 것으로 제한하여 보안 강화 대부분의 기존 취약점 제거 및 PFS 기본으로 지원 TLS 1.3의 경우 최신 브라우저와 호환이 되지만 구버전의 브라우저는 일부 설정이 필요할 수 있음\n자료 출처 MDN Medium\nChatGPT\n","permalink":"https://4d4cat.com/posts/2024/https/","summary":"\u003ch2 id=\"https-http-secure\"\u003eHTTPS (HTTP Secure)\u003c/h2\u003e\n\u003cp\u003e: HTTP의 보안이 강화된 버전이다. 소켓 통신에서 일반 텍스트를 이용하는 대신에, SSL이나 TLS 프로토콜을 통해 세션 데이터를 암호화한다.\u003c/p\u003e\n\u003ch3 id=\"1-http와의-차이점\"\u003e1. HTTP와의 차이점\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2024/https0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e요약하면 HTTPS는 다음과 같은 차이점이 있다.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e보안성\u003c/strong\u003e: 보안 인증서를 통해 데이터 전송 시 암호화를 제공하여 보안을 강화\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e무결성\u003c/strong\u003e: 데이터가 전송 중에 변경되지 않았음을 보장\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e신뢰성\u003c/strong\u003e: 검색 엔진에서 우선순위를 우대받으며, 사용자에게 더 신뢰감을 보장\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-동작-원리\"\u003e2. 동작 원리\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2024/https1.png\"\u003e\u003c/p\u003e\n\u003cdl\u003e\n\u003cdt\u003e\u003cstrong\u003e1) URL 파싱 및 DNS 조회\u003c/strong\u003e\u003c/dt\u003e\n\u003cdd\u003e브라우저는 URL을 확인하고 DNS를 통해 도메인을 IP 주소로 변환하여 전달한다.\u003c/dd\u003e\n\u003cdt\u003e\u003cstrong\u003e2) 보안 연결 설정\u003c/strong\u003e\u003c/dt\u003e\n\u003cdd\u003e서버는 \u003ccode\u003eTLS\u003c/code\u003e handshake를 통해 인증서를 클라이언트에 전송한다.   \u003cbr\u003e\n이 인증서에는 서버의 공개 키와 인증기관(CA)의 서명이 포함되어 있다.\u003c/dd\u003e\n\u003cdt\u003e\u003cstrong\u003e3) 서버 처리 및 응답 생성\u003c/strong\u003e\u003c/dt\u003e\n\u003cdd\u003e보안 연결이 설정되면 암호화된 요청을 서버로 보낸다.\u003cbr\u003e\n서버는 요청을 해독하여 요청된 데이터, 상태코드 등으로 구성된 HTTPS 응답을 생성한다.\u003c/dd\u003e\n\u003cdt\u003e\u003cstrong\u003e4) 응답 암호화 및 전송\u003c/strong\u003e\u003c/dt\u003e\n\u003cdd\u003e서버는 TLS를 이용하여 응답을 암호화하고 클라이언트의 브라우저로 다시 전송한다. \u003cbr\u003e\n이 암호화를 통해 전송되는 동안 데이터의 보안이 유지되고 데이터를 가로채더라도 내용을 해독할 수 없다.\u003c/dd\u003e\n\u003cdt\u003e\u003cstrong\u003e5) 클라이언트에서의 처리\u003c/strong\u003e\u003c/dt\u003e\n\u003cdd\u003e암호화된 응답을 수신한 브라우저는 이를 해독한다.\u003cbr\u003e\n여기에는 웹 페이지를 표시하도록 렌더링하는 작업이 포함된다.  \u003cbr\u003e\n데이터가 완전히 전송되고 렌더링되면 TLS 연결은 종료된다.\u003c/dd\u003e\n\u003c/dl\u003e\n\u003ch3 id=\"3-전달-내용-비교\"\u003e3. 전달 내용 비교\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eex) HTTP 요청 (평문 전송)\u003c/strong\u003e\u003c/p\u003e","title":"HTTPS"},{"content":"HTTP (HyperText Transfer Protocol) : 웹상에서 데이터를 송수신하기 위한 통신 규약으로 클라이언트와 서버 사이에 이루어지는 요청/응답 프로토콜이다. 예를 들어 클라이언트인 웹 브라우저가 HTTP를 통하여 서버로부터 웹 페이지(HTML) 등 정보를 요청하면, 서버는 이 요청에 응답하여 필요한 정보를 사용자에게 전달한다.\n🔔 HTTPS를 정리하기 전, 최소한의 HTTP 정보만 정리한 것으로 일부 개념이 생략되어있습니다.\nHTTP 요청 : HTTP 요청을 할 때, 서로 다른 유형의 정보를 전달하는 일련의 인코딩된 데이터를 전달한다.\n1. HTTP 버전 유형 HTTP 버전은 시간이 지나면서 여러 버전으로 업데이트되어 왔습니다. 간단히 요약하면 아래와 같은데 자세한 내용은 RFC 문서를 참고하시기 바랍니다. HTTP/1.0 초기 HTTP는 각 요청마다 새로운 TCP 연결을 생성하고 종료했습니다. 따라서 다수의 요청 시 지연시간이 증가합니다. HTTP/1.1 (파이프라이닝) 1.0에 비해 다수의 요청을 주고 받을 수 있게 됐지만 실질적인 성능 향상에는 제한이 있습니다. 현재도 레거시 환경이나 소규모 사이트에서는 사용되고 있습니다. HTTP/2 (멀티플렉싱) 하나의 TCP 연결에서 여러 개의 요청과 응답을 동시에 주고받을 수 있습니다. 헤더 정보를 효율적으로 압축하여 전송 데이터 크기가 줄었습니다. 대다수의 사이트와 서비스에서 표준으로 채택되고 있습니다. HTTP/3 (QUIC, TLS) TCP 대신 UDP 기반의 QUIC 프로토콜을 사용하여 연결 속도와 전송 효율성이 향상됐습니다. 이전 연결 정보를 활용하여 초기 handshake 없이 빠르게 연결을 설정할 수 있습니다. TLS 1.3이 기본적으로 통합되어 보안성이 높습니다. 2. 요청 메서드 서버에게 요청하는 행동이나 작업을 나타냅니다.\n요약을 하면 아래와 같은데 여기서는 일부분만 다뤄보겠습니다.\nGET 특정 리소스의 표시를 요청하고, 데이터를 받기만 합니다. HEAD GET 메서드의 요청과 동일한 응답을 요구하지만, 응답 본문을 포함하지 않습니다. POST 특정 리소스에 엔티티를 제출할 때 쓰입니다. PUT 지정된 자원의 전체 표현을 서버에 전송하여 기존 자원을 완전히 대체합니다. DELETE 특정 리소스를 삭제합니다. PATCH 리소스의 부분만을 수정하는데 쓰입니다. 3. 요청 헤더 User-Agent 클라이언트 소프트웨어의 정보(브라우저, 버전 등)를 전달합니다. Accept 클라이언트가 처리할 수 있는 미디어 타입을 서버에 알립니다. ex) text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8 Content-Type 요청 본문의 미디어 타입을 지정합니다. POST PUT PATCH 요청 시 데이터 형식을 명확히 하기 위해 사용합니다. Authorization 인증 정보를 포함하여 서버에 접근 권한을 전달합니다. ex) Bearer abcdef123456 Cookie 클라이언트에 저장된 쿠키 데이터를 서버에 전송합니다. 사용자 세션 관리, 상태 유지 등에 사용 Connection 현재 연결의 제어 옵션을 지정합니다. ex) keep-alive Cache-Control 요청 및 응답의 캐싱 동작을 제어합니다. ex) no-cache 4. 요청 본문 클라이언트가 서버로 데이터를 전송할 때, 전송되는 데이터의 형식에 따라 다양한 유형이 존재합니다.\napplication/json\nJSON 형식의 데이터를 전송하며, 다양한 언어에서 쉽게 파싱이 가능합니다. { \u0026#34;id\u0026#34;: 123, \u0026#34;name\u0026#34;: \u0026#34;홍길동\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;hong@example.com\u0026#34;, \u0026#34;age\u0026#34;: 30 } application/x-www-form-urlencoded\nHTML 폼 데이터를 키-값 형태로 인코딩하여 전송합니다. 크기가 제한되어 대용량 데이터 전송에는 적합하지 않습니다. name=홍길동\u0026amp;email=hong%40example.com\u0026amp;age=30 application/xml\nXML 형식의 데이터를 전송합니다. \u0026lt;user\u0026gt; \u0026lt;id\u0026gt;123\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;홍길동\u0026lt;/name\u0026gt; \u0026lt;email\u0026gt;hong@example.com\u0026lt;/email\u0026gt; \u0026lt;age\u0026gt;30\u0026lt;/age\u0026gt; \u0026lt;/user\u0026gt; HTTP 응답 1. 응답 코드 클라이언트가 서버에 요청을 보냈을 때, 서버는 응답코드와 함께 응답합니다.\n1xx (정보): 요청을 받았으며 프로세스를 계속한다. 2xx (성공): 요청을 성공적으로 받았으며 인식했고 수용하였다. 3xx (리다이렉션): 요청 완료를 위해 추가 작업 조치가 필요하다. 4xx (클라이언트 오류): 요청의 문법이 잘못되었거나 요청을 처리할 수 없다. 5xx (서버오류): 서버가 명백히 유효한 요청에 대해 충족을 실패했다. 2. 응답 헤더 Cache-Control 응답의 캐싱 동작을 제어합니다. ex) no-cache, no-store, must-revalidate Expires 클라이언트가 응답을 캐시할 수 있는 유효 기간을 설정합니다. ex) Wed, 21 Oct 2025 07:28:00 GMT Location 클라이언트를 다른 URL로 리디렉션합니다. 주로 3xx 리디렉션 상태 코드와 함께 사용됩니다. ETag 클라이언트가 리소스의 변경 여부를 확인하는 데 사용되며, 조건부 요청에 활용됩니다. 예를 들어, If-None-Match 헤더와 함께 사용하여 리소스의 변경 여부를 확인할 수 있습니다. Access-Control-Allow-Origin CORS 정책에서 어떤 출처(origin)가 리소스에 접근할 수 있는지 지정합니다. ex) Access-Control-Allow-Origin: * Strict-Transport-Security (HSTS) 브라우저에게 항상 HTTPS를 사용하도록 지시합니다. ex) max-age=31536000; includeSubDomains Content-Security-Policy (CSP) 웹 페이지가 로드할 수 있는 리소스의 출처를 제한합니다. ex) default-src \u0026lsquo;self\u0026rsquo;; script-src \u0026lsquo;self\u0026rsquo; https://apis.example.com 3. 응답 본문 HTML\n웹 브라우저는 HTML을 해석하여 웹 페이지를 렌더링합니다. \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;예제 페이지\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;안녕하세요!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;이것은 예제 HTML 페이지입니다.\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; JSON\nRESTful API의 응답으로, 클라이언트는 이 데이터로 사용자 정보를 표시하거나 처리할 수 있습니다. { \u0026#34;id\u0026#34;: 123, \u0026#34;name\u0026#34;: \u0026#34;홍길동\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;hong@example.com\u0026#34;, \u0026#34;age\u0026#34;: 30 } XML\nXML 형식으로 사용자 정보를 제공하며, 일부 레거시 시스템에서 데이터 교환에 사용됩니다. \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;user\u0026gt; \u0026lt;id\u0026gt;123\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;홍길동\u0026lt;/name\u0026gt; \u0026lt;email\u0026gt;hong@example.com\u0026lt;/email\u0026gt; \u0026lt;age\u0026gt;30\u0026lt;/age\u0026gt; \u0026lt;/user\u0026gt; 자료 출처 위키백과\n제타위키\nMDN\nCloudflare ChatGPT\n","permalink":"https://4d4cat.com/posts/2024/http/","summary":"\u003ch2 id=\"http-hypertext-transfer-protocol\"\u003eHTTP (HyperText Transfer Protocol)\u003c/h2\u003e\n\u003cp\u003e: \u003cstrong\u003e웹상에서 데이터를 송수신하기 위한 통신 규약\u003c/strong\u003e으로 클라이언트와 서버 사이에 이루어지는 요청/응답 프로토콜이다. 예를 들어 클라이언트인 웹 브라우저가 \u003ccode\u003eHTTP\u003c/code\u003e를 통하여 서버로부터 웹 페이지(HTML) 등 정보를 요청하면, 서버는 이 요청에 응답하여 필요한 정보를 사용자에게 전달한다.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e🔔 \u003cstrong\u003eHTTPS를 정리하기 전, 최소한의 HTTP 정보만 정리한 것으로 일부 개념이 생략되어있습니다.\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"http-요청\"\u003eHTTP 요청\u003c/h2\u003e\n\u003cp\u003e: HTTP 요청을 할 때, 서로 다른 유형의 정보를 전달하는 일련의 인코딩된 데이터를 전달한다.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2024/http0.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-http-버전-유형\"\u003e1. HTTP 버전 유형\u003c/h3\u003e\n\u003cp\u003eHTTP 버전은 시간이 지나면서 여러 버전으로 업데이트되어 왔습니다. \u003cbr\u003e\n간단히 요약하면 아래와 같은데 자세한 내용은 RFC 문서를 참고하시기 바랍니다. \u003cbr\u003e \u003cbr\u003e\n\u003cimg loading=\"lazy\" src=\"/images/2024/http1.png\"\u003e\u003c/p\u003e","title":"HTTP"},{"content":"#!/bin/bash ######### NOTICE ############## #\tItsm 요청이 왔을 때 #\t메일 발송되는 것이 목적이라서 #\t아래 소스는 ChatGPT로 #\t최소한의 로직만 구현했습니다 ############################### ######## MAIL API ############ # 우리메일(WOORIMAIL) # https://woorimail.com #\t# 월 1만건 무료 발송 가능합니다 # 메일 1만건 모두 소진 시 # 당월 메일 발송이 중지됩니다 ############################## ###### TIME SETTING ########### # 현재 9시 ~ 18시까지만 #\t동작하도록 세팅되어있으니 #\t수정이 필요하면 아래 함수에서 #\t시간 수정 부탁드립니다 # check_working_hours() ############################### ##### RECEIVER INFO ########### # 메일 발신자 정보 수정은 # 60번 줄 영역에 있는 값을 # 수정해주시면 됩니다 ############################### # Function to make API call and check response check_response() { # API details METHOD=\u0026#34;POST\u0026#34; CONTENT_TYPE=\u0026#34;application/x-www-form-urlencoded\u0026#34; ITSM_CHECK_URL=\u0026#34;\u0026#34; # ITSM 접수대기 건수 URL ITSM_LIST_URL=\u0026#34;\u0026#34; # ITSM 접수대기 리스트 URL BODY=\u0026#34;id=1520\u0026#34; # 접수대기(1520), 처리중(1526) # ITSM 처리중 #ITSM_LIST_URL=\u0026#34;\u0026#34; #BODY=\u0026#34;id=1526\u0026#34; # 고정값 MAIL_URL=\u0026#34;https://woorimail.com/\u0026#34; TYPE=\u0026#34;api\u0026#34; MID=\u0026#34;auth_woorimail\u0026#34; ACT=\u0026#34;dispWwapimanagerMailApi\u0026#34; DOMAIN=\u0026#34;\u0026#34; # 지정한 도메인 URL AUTHKEY=\u0026#34;\u0026#34; # 키값 ################################################## # 아래처럼 세팅했을 경우 메일은 다음과 같이 발송됩니다 # # 발신자: YuY\u0026lt;wms_nick@mx.woorimail.com\u0026gt; # 회신할 경우, 받는사람: ID@이메일주소 ################################################## WMS_NICK=\u0026#34;wms_nick\u0026#34; # 보낸사람 계정 SENDER_EMAIL=\u0026#34;\u0026#34; # 회신할 사람 이메일 SENDER_NICKNAME=\u0026#34;YuY\u0026#34; # 회신할 사람 닉네임 RECEIVER_NICKNAME=\u0026#34;\u0026#34; # 받는 사람(나) 닉네임 RECEIVER_EMAIL=$RECEIVER_EMAIL # 받는 사람(나) 이메일 # Add cookies to the request COOKIE=\u0026#34;JSESSIONID=$JSESSIONID\u0026#34; # ITSM JSessionID 값 # Make the API call and print the response RESPONSE=$(curl -s -X $METHOD \\ -H \u0026#34;Content-Type: $CONTENT_TYPE\u0026#34; \\ -H \u0026#34;Cookie: $COOKIE\u0026#34; \\ -d \u0026#34;$BODY\u0026#34; \\ $ITSM_CHECK_URL) # Extract result value from RESPONSE message RESULT_ITSM_READY_CNT=$(echo \u0026#34;$RESPONSE\u0026#34; | grep -oP \u0026#39;result: \\(\\K[0-9]+\u0026#39;) # Check if RESULT is greater than 0 and show dialog if validate_email \u0026#34;$RECEIVER_EMAIL\u0026#34;; then if [ \u0026#34;$RESULT_ITSM_READY_CNT\u0026#34; -gt 0 ]; then RESPONSE=$(curl -s -X $METHOD \\ -H \u0026#34;Content-Type: $CONTENT_TYPE\u0026#34; \\ -H \u0026#34;Cookie: $COOKIE\u0026#34; \\ -d \u0026#34;$BODY\u0026#34; \\ $ITSM_LIST_URL) RESULT_ITSM_READY_LIST=$(echo \u0026#34;$RESPONSE\u0026#34; | sed -n \u0026#39;/\u0026lt;table width=\u0026#34;100%\u0026#34;/,/\u0026lt;\\/table\u0026gt;/p\u0026#39; | sed -n \u0026#39;1,/\u0026lt;\\/table\u0026gt;/p\u0026#39; | grep -o \u0026#39;\u0026lt;td[^\u0026gt;]*\u0026gt;[^\u0026lt;]*\u0026lt;/td\u0026gt;\u0026#39; | sed -E \u0026#39;s/\u0026lt;\\/?td[^\u0026gt;]*\u0026gt;//g\u0026#39;) RESULT_ITSM_ALL_HTML=$(echo \u0026#34;$RESPONSE\u0026#34; | sed \u0026#39;:a;N;$!ba;s/\\n/ /g\u0026#39; | grep -oP \u0026#39;\u0026lt;table width=\u0026#34;100%\u0026#34;.*?\u0026lt;/table\u0026gt;\u0026#39;) # Convert the multiline string to an array IFS=$\u0026#39;\\n\u0026#39; read -r -d \u0026#39;\u0026#39; -a TD_VALUES_ARRAY \u0026lt;\u0026lt;\u0026lt; \u0026#34;$RESULT_ITSM_READY_LIST\u0026#34; MAIL_TITLE=\u0026#34;ITSM \u0026#34; # Process the array in chunks of 7 num_values=${#TD_VALUES_ARRAY[@]} for ((i = 0; i \u0026lt; num_values; i += 7)); do MAIL_TITLE=\u0026#34;$MAIL_TITLE ${TD_VALUES_ARRAY[i]} \u0026#34; done MAIL_TITLE=$(url_encode \u0026#34;$MAIL_TITLE\u0026#34;) MAIL_CONTENT=$(url_encode \u0026#34;ITSM 대기건수: $RESULT_ITSM_READY_CNT 건 $RESULT_ITSM_ALL_HTML\u0026#34;) MAIL_BODY=\u0026#34;authkey=$AUTHKEY\u0026amp;domain=$DOMAIN\u0026amp;type=$TYPE\u0026amp;mid=$MID\u0026amp;act=$ACT\u0026amp;callback=\u0026amp;title=$MAIL_TITLE\u0026amp;content=$MAIL_CONTENT\u0026amp;wms_domain=woorimail.com\u0026amp;wms_nick=$WMS_NICK\u0026amp;sender_email=$SENDER_EMAIL\u0026amp;sender_nickname=$SENDER_NICKNAME\u0026amp;receiver_nickname=$RECEIVER_NICKNAME\u0026amp;receiver_email=$RECEIVER_EMAIL\u0026amp;member_regdate=20240604170101\u0026#34; RESPONSE=$(curl -s -X $METHOD \\ -H \u0026#34;Content-Type: $CONTENT_TYPE\u0026#34; \\ -d \u0026#34;$MAIL_BODY\u0026#34; \\ $MAIL_URL) echo \u0026#34;Ready Itsm: $RESULT_ITSM_READY_CNT, $RESPONSE\u0026#34; else echo \u0026#34;Empty Itsm.\u0026#34; fi else echo \u0026#34;Invalid email format. Please provide a valid email address.\u0026#34; fi } # Function to check if current time is within working hours check_working_hours() { current_hour=$(date +%H) if [ \u0026#34;$current_hour\u0026#34; -ge 9 ] \u0026amp;\u0026amp; [ \u0026#34;$current_hour\u0026#34; -lt 18 ]; then return 0 # Within working hours else return 1 # Outside working hours fi } # Function to validate email format validate_email() { # Regular expression for email validation regex=\u0026#39;^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}$\u0026#39; if [[ $1 =~ $regex ]]; then return 0 # Valid email format else return 1 # Invalid email format fi } # Function to URL encode a string url_encode() { local string=\u0026#34;${1}\u0026#34; local encoded=\u0026#34;\u0026#34; # Convert the string to hex representation hex_string=$(echo -n \u0026#34;$string\u0026#34; | od -An -tx1 | tr \u0026#39; \u0026#39; \u0026#39;\\n\u0026#39; | grep -v \u0026#39;^$\u0026#39;) # Iterate over each character in the hex string for hex_char in $hex_string; do encoded+=\u0026#34;%$hex_char\u0026#34; done echo \u0026#34;$encoded\u0026#34; } INTRODUCTION=\u0026#34; ################################################ ###### ITSM 로그인 후 ###### 쿠키에 등록된 JSESSIONID 값을 입력해주세요 ################################################ ################################################ # 입력 파라미터, 몇분마다 이메일로 발송할지 세팅 # 1: 시간, 분(minute) # 2: 이메일 주소 # # ex) ./checkItsm.sh 5 이메일주소 # -\u0026gt; 5분마다 접수대기 중인 itsm을 체크하여 # -\u0026gt; 1건 이상 있을 경우 입력한 이메일로 발송 # # Itsm이 장시간으로 접수대기 상태인 경우가 많을수록 # 시간을 길게 세팅하는게 좋습니다 # 권장 시간(분) 5 ~ 20 ################################################ 값을 제대로 입력하면 아래와 같은 형식의 문구가 나옵니다 - 접수대기 건수가 없는 경우 \u0026gt; Empty Itsm. - 접수대기 건수가 있는 경우, 메일 발송 \u0026gt; Ready Itsm: 1, result: ~~, error_msg: ~~ ... JSESSIONID: \u0026#34; # Prompt user to input JSESSIONID read -p \u0026#34;$INTRODUCTION\u0026#34; JSESSIONID # Check if the second argument is provided if [ -z \u0026#34;$2\u0026#34; ]; then echo \u0026#34;Please provide a RECEIVER_EMAIL as the second argument.\u0026#34; exit 1 else RECEIVER_EMAIL=\u0026#34;$2\u0026#34; fi # Check if argument is provided for interval, default is set to 5 minutes interval=${1:-5} # Loop indefinitely and check response every $interval minutes while true; do # Check if current time is within working hours if ! check_working_hours; then echo \u0026#34;Outside working hours. Exiting...\u0026#34; exit 0 fi check_response sleep \u0026#34;$interval\u0026#34;m done ","permalink":"https://4d4cat.com/posts/2024/woorimail-api/","summary":"\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e#!/bin/bash\n\n######### NOTICE ##############\n#\t Itsm 요청이 왔을 때\n#\t메일 발송되는 것이 목적이라서\n#\t 아래 소스는 ChatGPT로\n#\t최소한의 로직만 구현했습니다\n###############################\n\n######## MAIL API ############\n#    우리메일(WOORIMAIL)\n#  https://woorimail.com\n#\t\t\t\t\t\t\n#  월 1만건 무료 발송 가능합니다\n# \t 메일 1만건 모두 소진 시\n# \t당월 메일 발송이 중지됩니다\n##############################\n\n###### TIME SETTING ###########\n# \t 현재 9시 ~ 18시까지만\n#\t 동작하도록 세팅되어있으니\n#\t수정이 필요하면 아래 함수에서\n#\t  시간 수정 부탁드립니다\n#  check_working_hours()\n###############################\n\n##### RECEIVER INFO ###########\n# \t 메일 발신자 정보 수정은\n# \t60번 줄 영역에 있는 값을\n# \t  수정해주시면 됩니다\n###############################\n\n# Function to make API call and check response\ncheck_response() {\n  # API details\n  METHOD=\u0026#34;POST\u0026#34;\n  CONTENT_TYPE=\u0026#34;application/x-www-form-urlencoded\u0026#34;\n  ITSM_CHECK_URL=\u0026#34;\u0026#34; # ITSM 접수대기 건수 URL\n  ITSM_LIST_URL=\u0026#34;\u0026#34; # ITSM 접수대기 리스트 URL\n  BODY=\u0026#34;id=1520\u0026#34; # 접수대기(1520), 처리중(1526)\n\n  # ITSM 처리중\n  #ITSM_LIST_URL=\u0026#34;\u0026#34;\n  #BODY=\u0026#34;id=1526\u0026#34;\n  \n  # 고정값\n  MAIL_URL=\u0026#34;https://woorimail.com/\u0026#34;\n  TYPE=\u0026#34;api\u0026#34;\n  MID=\u0026#34;auth_woorimail\u0026#34;\n  ACT=\u0026#34;dispWwapimanagerMailApi\u0026#34;\n  DOMAIN=\u0026#34;\u0026#34; # 지정한 도메인 URL\n  AUTHKEY=\u0026#34;\u0026#34; # 키값\n  \n  ##################################################\n  # 아래처럼 세팅했을 경우 메일은 다음과 같이 발송됩니다\n  #\n  # 발신자: YuY\u0026lt;wms_nick@mx.woorimail.com\u0026gt;\n  # 회신할 경우, 받는사람: ID@이메일주소\n  ##################################################\n  WMS_NICK=\u0026#34;wms_nick\u0026#34;               # 보낸사람 계정\n  SENDER_EMAIL=\u0026#34;\u0026#34;                   # 회신할 사람 이메일\n  SENDER_NICKNAME=\u0026#34;YuY\u0026#34;             # 회신할 사람 닉네임\n  RECEIVER_NICKNAME=\u0026#34;\u0026#34;              # 받는 사람(나) 닉네임\n  RECEIVER_EMAIL=$RECEIVER_EMAIL    # 받는 사람(나) 이메일\n  \n  # Add cookies to the request\n  COOKIE=\u0026#34;JSESSIONID=$JSESSIONID\u0026#34;   # ITSM JSessionID 값\n\n  # Make the API call and print the response\n  RESPONSE=$(curl -s -X $METHOD \\\n                    -H \u0026#34;Content-Type: $CONTENT_TYPE\u0026#34; \\\n                    -H \u0026#34;Cookie: $COOKIE\u0026#34; \\\n                    -d \u0026#34;$BODY\u0026#34; \\\n                    $ITSM_CHECK_URL)\n\n  # Extract result value from RESPONSE message\n  RESULT_ITSM_READY_CNT=$(echo \u0026#34;$RESPONSE\u0026#34; | grep -oP \u0026#39;result: \\(\\K[0-9]+\u0026#39;)\n\n  # Check if RESULT is greater than 0 and show dialog\n\tif validate_email \u0026#34;$RECEIVER_EMAIL\u0026#34;; then\n\t\tif [ \u0026#34;$RESULT_ITSM_READY_CNT\u0026#34; -gt 0 ]; then \n\t\t\n\t\t\tRESPONSE=$(curl -s -X $METHOD \\\n                                            -H \u0026#34;Content-Type: $CONTENT_TYPE\u0026#34; \\\n                                            -H \u0026#34;Cookie: $COOKIE\u0026#34; \\\n                                            -d \u0026#34;$BODY\u0026#34; \\\n                                            $ITSM_LIST_URL)\n\t\t\t\t\t\t\n\t\t\tRESULT_ITSM_READY_LIST=$(echo \u0026#34;$RESPONSE\u0026#34; | sed -n \u0026#39;/\u0026lt;table width=\u0026#34;100%\u0026#34;/,/\u0026lt;\\/table\u0026gt;/p\u0026#39; | sed -n \u0026#39;1,/\u0026lt;\\/table\u0026gt;/p\u0026#39; | grep -o \u0026#39;\u0026lt;td[^\u0026gt;]*\u0026gt;[^\u0026lt;]*\u0026lt;/td\u0026gt;\u0026#39; | sed -E \u0026#39;s/\u0026lt;\\/?td[^\u0026gt;]*\u0026gt;//g\u0026#39;)\n\t\t\tRESULT_ITSM_ALL_HTML=$(echo \u0026#34;$RESPONSE\u0026#34; | sed \u0026#39;:a;N;$!ba;s/\\n/ /g\u0026#39; | grep -oP \u0026#39;\u0026lt;table width=\u0026#34;100%\u0026#34;.*?\u0026lt;/table\u0026gt;\u0026#39;)\n\t\t\t\t\t\t\n\t\t\t# Convert the multiline string to an array\n\t\t\tIFS=$\u0026#39;\\n\u0026#39; read -r -d \u0026#39;\u0026#39; -a TD_VALUES_ARRAY \u0026lt;\u0026lt;\u0026lt; \u0026#34;$RESULT_ITSM_READY_LIST\u0026#34;\n\t\t\tMAIL_TITLE=\u0026#34;ITSM \u0026#34;\n\t\t\t\n\t\t\t# Process the array in chunks of 7\n\t\t\tnum_values=${#TD_VALUES_ARRAY[@]}\n\t\t\tfor ((i = 0; i \u0026lt; num_values; i += 7)); do\n\t\t\t  MAIL_TITLE=\u0026#34;$MAIL_TITLE ${TD_VALUES_ARRAY[i]} \u0026#34;\n\t\t\tdone\n\t\t\t\n\t\t\tMAIL_TITLE=$(url_encode \u0026#34;$MAIL_TITLE\u0026#34;)\n\t\t\tMAIL_CONTENT=$(url_encode \u0026#34;ITSM 대기건수: $RESULT_ITSM_READY_CNT 건 $RESULT_ITSM_ALL_HTML\u0026#34;)\n\t\t\tMAIL_BODY=\u0026#34;authkey=$AUTHKEY\u0026amp;domain=$DOMAIN\u0026amp;type=$TYPE\u0026amp;mid=$MID\u0026amp;act=$ACT\u0026amp;callback=\u0026amp;title=$MAIL_TITLE\u0026amp;content=$MAIL_CONTENT\u0026amp;wms_domain=woorimail.com\u0026amp;wms_nick=$WMS_NICK\u0026amp;sender_email=$SENDER_EMAIL\u0026amp;sender_nickname=$SENDER_NICKNAME\u0026amp;receiver_nickname=$RECEIVER_NICKNAME\u0026amp;receiver_email=$RECEIVER_EMAIL\u0026amp;member_regdate=20240604170101\u0026#34;\n\t\t\t\n\t\t\tRESPONSE=$(curl -s -X $METHOD \\\n                                            -H \u0026#34;Content-Type: $CONTENT_TYPE\u0026#34; \\\n                                            -d \u0026#34;$MAIL_BODY\u0026#34; \\\n                                            $MAIL_URL)\n\t\t\techo \u0026#34;Ready Itsm: $RESULT_ITSM_READY_CNT, $RESPONSE\u0026#34;\n\t\telse\n\t\t\techo \u0026#34;Empty Itsm.\u0026#34;\n\t\tfi\n\telse\n\t\techo \u0026#34;Invalid email format. Please provide a valid email address.\u0026#34;\n\tfi\n}\n\n# Function to check if current time is within working hours\ncheck_working_hours() {\n  current_hour=$(date +%H)\n  if [ \u0026#34;$current_hour\u0026#34; -ge 9 ] \u0026amp;\u0026amp; [ \u0026#34;$current_hour\u0026#34; -lt 18 ]; then\n    return 0 # Within working hours\n  else\n    return 1 # Outside working hours\n  fi\n}\n\n# Function to validate email format\nvalidate_email() {\n  # Regular expression for email validation\n  regex=\u0026#39;^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}$\u0026#39;\n  \n  if [[ $1 =~ $regex ]]; then\n    return 0 # Valid email format\n  else\n    return 1 # Invalid email format\n  fi\n}\n\n# Function to URL encode a string\nurl_encode() {\n  local string=\u0026#34;${1}\u0026#34;\n  local encoded=\u0026#34;\u0026#34;\n\n  # Convert the string to hex representation\n  hex_string=$(echo -n \u0026#34;$string\u0026#34; | od -An -tx1 | tr \u0026#39; \u0026#39; \u0026#39;\\n\u0026#39; | grep -v \u0026#39;^$\u0026#39;)\n\n  # Iterate over each character in the hex string\n  for hex_char in $hex_string; do\n    encoded+=\u0026#34;%$hex_char\u0026#34;\n  done\n\n  echo \u0026#34;$encoded\u0026#34;\n}\n\nINTRODUCTION=\u0026#34;\n################################################\n###### ITSM 로그인 후\n###### 쿠키에 등록된 JSESSIONID 값을 입력해주세요\n################################################\n\n################################################\n# 입력 파라미터, 몇분마다 이메일로 발송할지 세팅\n# 1: 시간, 분(minute)\n# 2: 이메일 주소\n#\n# ex) ./checkItsm.sh 5 이메일주소\n# -\u0026gt; 5분마다 접수대기 중인 itsm을 체크하여\n# -\u0026gt; 1건 이상 있을 경우 입력한 이메일로 발송\n#\n# Itsm이 장시간으로 접수대기 상태인 경우가 많을수록\n# 시간을 길게 세팅하는게 좋습니다\n# 권장 시간(분) 5 ~ 20\n################################################\n\n값을 제대로 입력하면 아래와 같은 형식의 문구가 나옵니다\n- 접수대기 건수가 없는 경우\n\u0026gt; Empty Itsm.\n\n- 접수대기 건수가 있는 경우, 메일 발송\n\u0026gt; Ready Itsm: 1, result: ~~, error_msg: ~~ ...\n\nJSESSIONID: \u0026#34;\n\n# Prompt user to input JSESSIONID\nread -p \u0026#34;$INTRODUCTION\u0026#34; JSESSIONID\n\n# Check if the second argument is provided\nif [ -z \u0026#34;$2\u0026#34; ]; then\n  echo \u0026#34;Please provide a RECEIVER_EMAIL as the second argument.\u0026#34;\n  exit 1\nelse\n  RECEIVER_EMAIL=\u0026#34;$2\u0026#34;\nfi\n\n# Check if argument is provided for interval, default is set to 5 minutes\ninterval=${1:-5}\n\n# Loop indefinitely and check response every $interval minutes\nwhile true; do\n\t# Check if current time is within working hours\n  if ! check_working_hours; then\n    echo \u0026#34;Outside working hours. Exiting...\u0026#34;\n    exit 0\n  fi\n  \n  check_response\n  sleep \u0026#34;$interval\u0026#34;m\ndone\n\u003c/code\u003e\u003c/pre\u003e","title":"우리메일 API를 이용한 배치 스크립트"},{"content":" ** 아래 내용은 GPT4 기반으로 정리했습니다. **\njvm 실행 옵션 중 Djava.security.egd 가 의미하는게 무엇인가 Djava.security.egd 옵션은 Java Virtual Machine(JVM)에서 암호화 작업을 위해 사용하는 엔트로피 소스를 지정하는 데 사용됩니다. EGD는 Entropy Gathering Daemon의 약자로, 시스템에서 무작위성(엔트로피)을 수집하는 메커니즘을 가리킵니다.\n엔트로피란? 엔트로피는 무작위성의 정도를 나타내며, 암호화 작업에서 매우 중요합니다. 충분한 엔트로피가 없으면 암호화 알고리즘이 예측 가능해져 보안이 약화될 수 있습니다.\nDjava.security.egd 옵션의 역할: 이 JVM 옵션을 사용하여 애플리케이션이 엔트로피를 수집할 소스를 지정할 수 있습니다. 예를 들어, Linux 시스템에서는 /dev/random 또는 /dev/urandom 파일을 사용하여 엔트로피를 수집할 수 있습니다. /dev/random은 블로킹 모드로 운영되어 엔트로피 풀이 충분할 때만 데이터를 제공하는 반면, /dev/urandom은 비블로킹이며 더 빠르지만 엔트로피가 낮을 수 있습니다. 옵션 사용 예: -Djava.security.egd=file:/dev/urandom -jar your-application.jar\n이 명령은 JVM이 /dev/urandom을 사용하여 엔트로피를 수집하도록 지시합니다.\n왜 중요한가? 보안 강화: 암호화 연산에 충분하고 예측 불가능한 엔트로피를 제공하여 보안을 강화합니다. 성능 향상: 특히 /dev/urandom을 사용할 경우, 엔트로피 수집 지연을 줄여 시스템의 성능을 향상시킬 수 있습니다. 애플리케이션의 보안 요구 사항과 성능 요구 사항에 따라 적절한 엔트로피 소스를 선택하는 것이 중요합니다.\n보안 측면에서 /dev/random을 사용하면 어떻게 차이가 있는건가 /dev/random을 사용하는 것 자체가 보안을 약화시킨다는 것은 아닙니다. 실제로 /dev/random은 /dev/urandom에 비해 엔트로피 풀을 더 엄격하게 관리하기 때문에, 이론적으로 더 강력한 보안을 제공할 수 있습니다. /dev/random은 엔트로피 풀이 충분할 때만 데이터를 제공하기 때문에 더 예측하기 어려운 데이터를 생성합니다.\n문제는 성능과 가용성 측면에서 발생할 수 있습니다. /dev/random은 엔트로피 풀이 충분한 무작위성을 갖추고 있지 않으면 데이터 읽기를 차단(블로킹)합니다. 이는 다음과 같은 상황에서 문제가 될 수 있습니다:\n성능 저하: 시스템이 고도의 무작위성을 필요로 하는 암호화 작업을 많이 수행할 경우, /dev/random은 필요한 무작위 데이터를 생성하기 위해 충분한 엔트로피를 수집할 때까지 작업을 중단시킬 수 있습니다. 이로 인해 애플리케이션의 반응성이 떨어지고, 성능이 저하될 수 있습니다.\n가용성 문제: 특히 엔트로피가 빠르게 고갈되는 환경에서는 /dev/random이 데이터를 제공할 수 없는 상태가 길어질 수 있습니다. 이는 시스템이나 서비스의 가용성에 영향을 줄 수 있으며, 특히 고가용성이 중요한 서비스에는 심각한 문제가 될 수 있습니다.\n반면, /dev/urandom은 비블로킹 방식으로 작동하며, 충분한 엔트로피가 없을 때에도 예측 불가능한 데이터를 제공합니다. 최신 시스템에서는 /dev/urandom도 매우 안전하며, 대부분의 경우에 충분한 보안을 제공합니다. 그러나 /dev/random을 사용하는 것이 권장되는 경우(예: 매우 높은 보안이 요구되는 키 생성 등)에는 성능 저하를 감수하면서 보안을 최우선으로 고려할 수 있습니다.\n보안과 성능 사이의 균형을 고려하여, 애플리케이션의 요구 사항과 환경에 가장 적합한 옵션을 선택하는 것이 중요합니다.\n","permalink":"https://4d4cat.com/posts/2023/difference-between-random-urandom/","summary":"\u003cblockquote\u003e\n\u003cp\u003e** 아래 내용은 GPT4 기반으로 정리했습니다. **\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"jvm-실행-옵션-중-djavasecurityegd-가-의미하는게-무엇인가\"\u003ejvm 실행 옵션 중 \u003ccode\u003eDjava.security.egd\u003c/code\u003e 가 의미하는게 무엇인가\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eDjava.security.egd\u003c/code\u003e 옵션은 Java Virtual Machine(JVM)에서 암호화 작업을 위해 사용하는 엔트로피 소스를 지정하는 데 사용됩니다.\nEGD는 Entropy Gathering Daemon의 약자로, 시스템에서 무작위성(엔트로피)을 수집하는 메커니즘을 가리킵니다.\u003c/p\u003e\n\u003ch3 id=\"엔트로피란\"\u003e엔트로피란?\u003c/h3\u003e\n\u003cp\u003e엔트로피는 무작위성의 정도를 나타내며, 암호화 작업에서 매우 중요합니다.\n충분한 엔트로피가 없으면 암호화 알고리즘이 예측 가능해져 보안이 약화될 수 있습니다.\u003c/p\u003e\n\u003ch3 id=\"djavasecurityegd-옵션의-역할\"\u003e\u003ccode\u003eDjava.security.egd\u003c/code\u003e 옵션의 역할:\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e이 JVM 옵션을 사용하여 애플리케이션이 엔트로피를 수집할 소스를 지정할 수 있습니다.\u003c/li\u003e\n\u003cli\u003e예를 들어, Linux 시스템에서는 \u003ccode\u003e/dev/random\u003c/code\u003e 또는 \u003ccode\u003e/dev/urandom\u003c/code\u003e 파일을 사용하여 엔트로피를 수집할 수 있습니다.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e/dev/random\u003c/code\u003e은 블로킹 모드로 운영되어 엔트로피 풀이 충분할 때만 데이터를 제공하는 반면, \u003ccode\u003e/dev/urandom\u003c/code\u003e은 비블로킹이며 더 빠르지만 엔트로피가 낮을 수 있습니다.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"옵션-사용-예\"\u003e옵션 사용 예:\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003e-Djava.security.egd=file:/dev/urandom -jar your-application.jar\u003c/code\u003e\u003c/p\u003e","title":"Difference between /dev/random and /dev/urandom"},{"content":" ** 아래 내용은 GPT4 기반으로 정리했습니다. **\nVACUUM의 목적 저장 공간 회수: PostgreSQL은 MVCC (Multiversion Concurrency Control, 다중 버전 동시성 제어)라는 저장 시스템을 사용합니다. 이 시스템은 행이 업데이트되거나 삭제될 때마다 새로운 버전의 행을 생성합니다. 이로 인해 오래된 행 버전들이 시간이 지나며 쌓이게 됩니다. VACUUM은 이러한 오래되고 불필요한 행 버전을 정리하고 공간을 확보합니다.\n트랜잭션 ID Wraparound 방지: PostgreSQL은 트랜잭션을 추적하기 위해 4바이트 트랜잭션 ID를 사용합니다. 이 ID는 크기에 제한이 있으며, 결국 wraparound될 수 있습니다. 이런 상황이 발생하면 데이터베이스는 더 이상 어떤 트랜잭션이 오래되었는지 새로운 것인지를 결정할 수 없게 됩니다. VACUUM은 오래된 트랜잭션 ID를 정리함으로써 이를 방지하는 데 도움이 됩니다.\nWraparound: 트랜잭션 ID가 그 범위를 초과하여 초기값으로 되돌아가는 현상\n쿼리 플래너를 위한 통계 업데이트: VACUUM은 또한 PostgreSQL의 쿼리 플래너에 의해 사용되는 통계를 업데이트하여, 데이터베이스가 효율적인 쿼리 실행 계획을 만들 수 있도록 합니다.\nVACUUM의 유형 Regular VACUUM: 이 과정은 테이블에 락을 걸지 않고 죽은 튜플(오래된 행 버전)을 정리합니다. 이는 일상적인 유지 관리 작업입니다. VACUUM FULL: 이는 전체 테이블을 새로운 디스크 파일로 다시 write하여, 공간을 압축하고 회수합니다. 이 작업은 테이블에 락을 걸기 때문에 신중하게 사용해야 합니다. VACUUM ANALYZE: 이는 표준 VACUUM 작업을 ANALYZE 명령어와 결합한 것으로, 더 나은 쿼리 계획을 위한 통계를 업데이트합니다. 작업 주기 빈도: Vacuum 작업의 빈도는 데이터베이스 작업의 성격에 따라 다릅니다. 데이터베이스가 많은 업데이트와 삭제를 겪는 경우, 더 자주 작업을 필요로 할 수 있습니다. Autovacuum을 통한 자동화: PostgreSQL에는 vacuum 과정을 자동화하는 autovacuum 데몬이 포함되어 있습니다. 데이터베이스 활동에 기반하여 vacuum 빈도를 동적으로 조정하므로 대부분의 데이터베이스에 대해 autovacuum 사용이 권장됩니다. 고려사항 자원 소비: Vacuum 작업은 자원 집약적일 수 있으므로, 가능한 한 활동 시간이 적을 때 스케줄링하는 것이 중요합니다. 모니터링: 데이터베이스 로그와 성능의 정기적인 모니터링은 필요에 따라 vacuum 작업 스케줄을 조정하는 데 도움이 될 수 있습니다. 요약하자면, PostgreSQL에서 VACUUM은 공간을 확보하고, ID wraparound를 방지하며, 쿼리 효율성을 유지하는 데 중요합니다. 적절한 작업 주기는 데이터베이스 상태에 따라 다르지만, PostgreSQL의 autovacuum 기능은 이 과정을 효과적으로 자동화하도록 설계되었습니다.\n원문 Purpose of VACUUM:\nReclaims Storage: PostgreSQL uses a storage system known as MVCC (Multiversion Concurrency Control), which creates a new version of a row each time it\u0026rsquo;s updated or deleted. This means that old versions of rows accumulate over time. VACUUM cleans up these old, obsolete row versions and frees up space. Prevents Transaction ID Wraparound: PostgreSQL uses a 4-byte transaction ID to track transactions. This ID is limited in size and can eventually wrap around. If this happens, the database will no longer be able to determine which transactions are old or new. VACUUM helps prevent this by cleaning up old transaction IDs. Updates Statistics for the Query Planner: VACUUM also updates the statistics used by PostgreSQL\u0026rsquo;s query planner, ensuring that the database can create efficient query execution plans. Types of VACUUM:\nRegular VACUUM: This process cleans up dead tuples (old versions of rows) without locking the table. It\u0026rsquo;s a routine maintenance task. VACUUM FULL: This rewrites the entire table to a new disk file, compacting it and reclaiming disk space. This operation locks the table, so it should be used cautiously. VACUUM ANALYZE: This combines the standard VACUUM operation with an ANALYZE command, which updates statistics for better query planning. Work Cycle:\nFrequency: The frequency of vacuuming depends on the nature of your database operations. If your database undergoes a lot of updates and deletions, more frequent vacuuming might be necessary. Automation with Autovacuum: PostgreSQL includes an autovacuum daemon that automates the vacuuming process. It\u0026rsquo;s recommended to use autovacuum for most databases because it dynamically adjusts the frequency of vacuuming based on database activity. Considerations:\nResource Consumption: Vacuuming can be resource-intensive, so it\u0026rsquo;s important to schedule it during periods of low activity if possible. Monitoring: Regular monitoring of database logs and performance can help you adjust the vacuuming schedule as needed. In summary, VACUUM in PostgreSQL is crucial for reclaiming space, preventing ID wraparound, and maintaining query efficiency. The appropriate work cycle varies based on database activity, but the autovacuum feature in PostgreSQL is designed to automate this process effectively.\n","permalink":"https://4d4cat.com/posts/2023/postgresql-vacuum-and-cycle/","summary":"\u003cblockquote\u003e\n\u003cp\u003e** 아래 내용은 GPT4 기반으로 정리했습니다. **\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"vacuum의-목적\"\u003eVACUUM의 목적\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e저장 공간 회수: \u003cbr\u003e\nPostgreSQL은 MVCC (Multiversion Concurrency Control, 다중 버전 동시성 제어)라는 저장 시스템을 사용합니다.\n이 시스템은 행이 업데이트되거나 삭제될 때마다 새로운 버전의 행을 생성합니다. 이로 인해 오래된 행 버전들이 시간이 지나며 쌓이게 됩니다.\nVACUUM은 이러한 오래되고 불필요한 행 버전을 정리하고 공간을 확보합니다.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e트랜잭션 ID Wraparound 방지: \u003cbr\u003e\nPostgreSQL은 트랜잭션을 추적하기 위해 4바이트 트랜잭션 ID를 사용합니다. \u003cbr\u003e\n이 ID는 크기에 제한이 있으며, 결국 wraparound될 수 있습니다.\n이런 상황이 발생하면 데이터베이스는 더 이상 어떤 트랜잭션이 오래되었는지 새로운 것인지를 결정할 수 없게 됩니다. \u003cbr\u003e\nVACUUM은 오래된 트랜잭션 ID를 정리함으로써 이를 방지하는 데 도움이 됩니다.\u003c/p\u003e","title":"PostgreSQL Vacuum and Cycle"},{"content":"용어 자체가 거창해보이지만 하나씩 해석해보면\nAES 대칭키 암호화 방식 중 하나이고\nCBC는 블록 단위로 암호화를 하는 AES 방식의 운영모드 중 하나이다. PKCS7은 AES128 방식을 쓴다고 하면, 128비트보다 작은 블록이 나오면 뒤에 값을 붙여주는 \u0026lsquo;패딩\u0026lsquo;의 한 방식이다.\n크게 암호화 방식은 대칭키, 비대칭키 방식이 있다. 대칭키는 암호화/복호화에 쓰이는 키가 같아서 속도가 빠르지만 해당 키값이 노출되면 문제가 생기고 관리가 쉽지 않다.\n위에서 말한 AES는 DES방식의 결함이 발견되어 채택된 방식으로 128/192/256비트의 고정 블록 단위로 암호화를 수행한다.\n특히 블록 암호화 방식은 평문의 길이와 상관없이 고정된 길이가 나오게 된다.\n평문을 패딩을 통해 블록 크기의 배수로 만들고나면 일정한 크기의 블록으로 나누어서 암호화를 하고 연결하게 된다.\nex) 12345678abcdefgh -\u0026gt; 1234, 5678, abcd, efgh\n=\u0026gt; AKFM, IJRM, CSjs, Q23I -\u0026gt; AKFMIJRMCSjsQ23I\n여기에 사용되는 운용 방식으로 CBC 방식이 있는데 키가 고정되어 있는 경우 암호화한 결과가 동일하게 나올 수 있으므로 이를 해결하고자 나온 방식이다.\n이는 첫 번째 블록(1234)을 **Iv값(첫 번째 블록을 암호화하는 키값)**으로 암호화를 하여 AKFM 라는 값을 얻었으면 이 AKFM값으로 다음 블록을 암호화시킨다.\n따라서 위의 예시로 들었던 평문을 **\u0026lsquo;a1b2\u0026rsquo;**라는 값으로 암호화하여 나온 값이라면 CBC 방식을 사용했을 때 첫 번째 블록의 암호화값이 AKFM이 나올 수 있지만 그 다음 블록의 값부터는 달라지게 된다.\n출처 Tistory 1 Tistory 2\nSecmem\nVelog\n","permalink":"https://4d4cat.com/posts/2022/aes-cbc-pkcs7/","summary":"\u003cp\u003e용어 자체가 거창해보이지만 하나씩 해석해보면\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAES\u003c/strong\u003e \u003cem\u003e대칭키 암호화 방식\u003c/em\u003e 중 하나이고\u003cbr\u003e\n\u003cstrong\u003eCBC\u003c/strong\u003e는 블록 단위로 암호화를 하는 \u003cem\u003eAES 방식의 운영모드\u003c/em\u003e 중 하나이다. \u003cbr\u003e\n\u003cstrong\u003ePKCS7\u003c/strong\u003e은 AES128 방식을 쓴다고 하면, 128비트보다 \u003cem\u003e작은 블록이 나오면 뒤에 값을 붙여주는\u003c/em\u003e \u0026lsquo;\u003cstrong\u003e패딩\u003c/strong\u003e\u0026lsquo;의 한 방식이다.\u003c/p\u003e\n\u003cp\u003e크게 암호화 방식은 대칭키, 비대칭키 방식이 있다. \u003cbr\u003e\n\u003cstrong\u003e대칭키\u003c/strong\u003e는 암호화/복호화에 쓰이는 키가 같아서 \u003cem\u003e속도가 빠르지만\u003c/em\u003e 해당 키값이 노출되면 문제가 생기고 관리가 쉽지 않다.\u003c/p\u003e\n\u003cp\u003e위에서 말한 \u003cstrong\u003eAES\u003c/strong\u003e는 DES방식의 결함이 발견되어 채택된 방식으로 128/192/256비트의 \u003cem\u003e고정 블록 단위\u003c/em\u003e로 암호화를 수행한다.\u003cbr\u003e\n특히 블록 암호화 방식은 \u003cem\u003e평문의 길이와 상관없이 고정된 길이\u003c/em\u003e가 나오게 된다.\u003c/p\u003e","title":"AES/CBC/PKCS7"},{"content":" 지금은 취업한 상태고 취업 전에 면접 봤던 곳 중 정리해둔 것이 있어서 올려봅니다. 추가로, 작년 말에 면접 본거라서 자세하게 생각나지 않은 부분도 있습니다.\n면접방식 4:4 임원면접, 발표 순서는 맨 우측 사람부터 시작했다가 중간부터는 왼쪽부터 진행함 자소서 질문하는 부분에서는 랜덤\n면접 질문 1. 자기소개 약 1~2분 정도 준비했던 것을 발표함\n소통과 도전에 대한 일화를 짧게 소개하면서\n이 회사에서도 성과 있는 신입사원으로 거듭나겠다는 식으로 말함\n2. 프로젝트 경험 및 협업에 중요한 것 프로젝트는 졸업 프로젝트했던 것을 발표했고\n협업에 중요한 것은 소통과 시간, 소통을 하면서 서로가 지금 ~를 하고 있고 ~는 어떻게 해야 한다\u0026hellip; 약속된 시간을 엄수하면서 진행해야 일정을 놓치지 않고\u0026hellip; 어쩌고저쩌고\n3. 자신 있는 과목과 그렇지 않은 것 자신 있는 과목은 프로그래밍, 그렇지 않은 것은 이론 과목 코딩하는 것 자체를 좋아하고 새로운 언어를 배우는 것도 ~~ 하다.\n(그렇지 않은 것의 이유는 생각이..)\n4. 만약 내 코드가 문제가 있는데 약속이 있다면 (휴일이나 휴가 중에) 오류의 경중을 따져서 심각한 것이라면 빨리 복귀해서 해결하고 좀 가벼운 일이라면 해결방법을 전할 것 같다.\n5. 누군가가 나에게 일을 맡긴다면 맡긴 데는 이유가 있을 것이고, 나를 신뢰해서 맡기는 거라고 생각하기 때문에.. 6. 학교 생활 중 보람 있게(?) 느낀 것 (동아리 활동하면서 느꼈던 이것저것)\n7. 새로운 거에 대한 느낌? 새로운 걸 배우고 탐구하는 것을 좋아한다.. 등등\n8. 회사에 대해 아는 것 (회사 사이트에 대해 봤던 것을 아는 대로 말했음) ex) 어느 분야에서 어떤 방향으로 나아가는?, 그 회사의 솔루션 관련된 것 등\n9. 자소서 내용 (적었던 내용에 대해 질문받은 것을 그대로 답변함)\n10. 마지막으로 회사에 대해 궁금한 것 (기억 안 남)\n면접 후기 - 말 더듬 때문에 대부분의 질문을 답변하기 힘들었다는 점..\n나는 전공자, 다른 한 명은 개발직?이라 했고, 나머지 두 명은 국비 전공 - 신입한테는 기술보다는 인성 질문을 많이 하는 것 같음\n- 면접은 굉장히 편안 분위기에서 진행됨. 초반에 말 더듬을 많이 했었는데 분위기 띄우고 물 좀 마시고 하라면서 긴장을 풀어주심. 그래도 안 사라졌던 말 더듬\n자기소개서 쓸 때 - 자소서를 쓸 때, 양식을 주는 회사가 있고 안주는 회사가 있을 것이다.\n양식이 있으면 그 양식에 맞게 쓰면 되지만 양식이 없을 때는 기본적인 공통 질문을 쓰는 것이 좋다.\nEx) 지원동기, 성격의 장단점, 협업에 필요한 요소 몇 가지 등\n- 모든 질문에 대한 답변은 두괄식으로 쓰는 것이 좋다.\n내가 면접관은 아니지만 자소서의 질문과 답변을 읽을 때 첫 줄에 답이 먼저 나와있는 것이\n그다음 문장을 읽을 때 이어지기 때문이라고 생각한다.\n- 질문에 대한 답변을 쓰면 그 이유가 있을 것이다.\n이유는 본인이 선택한 전공과 관련이 있으면 더욱 좋고 전공과 관련이 없더라도 구체적인 사례나 자신의 인성 또는 성장하는 데에 도움을 준 것이면 괜찮다고 생각한다.\n- 지원서는 많이 써볼수록 좋다. 쓰다 보면 공통으로 쓸만한 질문이 생길 것이고 조금씩 바꿔가면서 자소서를 완성해가면 된다.\n세 줄 요약 1. 신입은 기술보다 인성 관련하여 질문을 많이 받는다. 2. 자소서는 두괄식으로 쓰고 구체적인 사례가 있는 것이 좋다. 3. 나는 이제 개발자다. (추가) 말 더듬은 어떻게 극복해야 할까? \u0026lsquo;말 더듬\u0026rsquo;으로 이 글을 읽는 분들이 많아서 내용을 추가해봅니다. 개개인에 따라 다르겠지만 당연히 그 상황에 침착해야 더듬지 않고 말할 수 있겠죠\n그럴려면 질문이 나왔을 때 바로 답이 생각날 정도로 연습을 해야할 것이고\n갑작스러운 질문에도 잘 대처할 것 같아요\n저는 집안에서 막내삼촌과 큰고모도 말 더듬을 가지고 계셔서\n유전적으로 말 더듬이 있는 것 같습니다.\n그래서 이것 때문에 놀림도 받고 대화가 정말 불편했는데\n여러 가지 방법으로 최대한 극복해냈습니다.\n(말 더듬이 사라진 것은 아닙니다.)\n아나운서들이 발음 연습할 때 흔히 쓰는 방법이라 해서 볼펜 같은 것을 입에 물고 말을 해보고 또박또박 말하기 위해서 랩 가사를 여러 번 읽기도 했습니다.\n누군가가 \u0026lsquo;인간은 적응의 동물\u0026rsquo;이라고 했던 것이 생각나네요 많이 연습해보고 실전에서도 극복해서 좋은 결과가 있기를 바랍니다.\n* 모든 회사가 다 이렇다는 것이 아니고 한 사례를 적은 것이므로 참고만 하시길 바랍니다.\n티스토리\n","permalink":"https://4d4cat.com/posts/2020/new-developer-interviews/","summary":"\u003cblockquote\u003e\n\u003cp\u003e지금은 취업한 상태고 취업 전에 면접 봤던 곳 중 정리해둔 것이 있어서 올려봅니다. \u003cbr\u003e\n추가로, 작년 말에 면접 본거라서 자세하게 생각나지 않은 부분도 있습니다.\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch2 id=\"면접방식\"\u003e면접방식\u003c/h2\u003e\n\u003cp\u003e4:4 임원면접, 발표 순서는 맨 우측 사람부터 시작했다가 중간부터는 왼쪽부터 진행함  \u003cbr\u003e\n자소서 질문하는 부분에서는 랜덤\u003c/p\u003e\n\u003ch2 id=\"면접-질문\"\u003e면접 질문\u003c/h2\u003e\n\u003ch3 id=\"1-자기소개\"\u003e1. 자기소개\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e약 1~2분 정도 준비했던 것을 발표함\u003cbr\u003e\n소통과 도전에 대한 일화를 짧게 소개하면서\u003cbr\u003e\n이 회사에서도 성과 있는 신입사원으로 거듭나겠다는 식으로 말함\u003c/p\u003e\u003c/blockquote\u003e\n\u003ch3 id=\"2-프로젝트-경험-및-협업에중요한-것\"\u003e2. 프로젝트 경험 및 협업에 중요한 것\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e프로젝트는 졸업 프로젝트했던 것을 발표했고\u003cbr\u003e\n협업에 중요한 것은 소통과 시간, 소통을 하면서 서로가 지금 ~를 하고 있고 ~는 어떻게 해야 한다\u0026hellip;  \u003cbr\u003e\n약속된 시간을 엄수하면서 진행해야 일정을 놓치지 않고\u0026hellip; 어쩌고저쩌고\u003c/p\u003e","title":"신입 웹 개발자 면접 일화"},{"content":"🤔 Who Am I? 저는 문제를 깊이 생각하고 해결책을 찾는 것을 즐깁니다. 같은 문제를 다양한 관점에서 바라보고 제 사고가 확장되는 과정에서 큰 성취감을 느낍니다. ✏️ Skill Languages Framework DevOps Communication Once I used Algorithm Stats ➰ Contributions demos-java #348\n","permalink":"https://4d4cat.com/about/","summary":"about","title":"About"}]