[{"content":"BaekjoonHub를 직접 커스텀하게 된 이유 최근에 백준을 본격적으로 사용하게 되면서 BaekjoonHub 익스텐션을 알게 됐음.\n처음 써보니 “풀이를 자동으로 GitHub에 올려준다”는 점은 정말 편했지만, 실제로 내 작업 방식에 맞추기엔 몇 가지 불편한 부분이 있었음.\n그래서 먼저 공식 저장소 이슈를 찾아봤음.\n확인해보니 나와 비슷한 고민을 가진 분들이 이미 있었고, 관련 기능을 추가하려는 PR도 올라와 있었음. 다만 당시 기준으로는 해당 기능을 바로 반영할 계획이 없다는 흐름이 보여서, 기다리기보다 내가 필요한 부분을 직접 커스텀해서 쓰기로 결정함.\n원본 저장소: https://github.com/BaekjoonHub/BaekjoonHub\n커스텀 저장소: https://github.com/0AndWild/baekjoonhub_custom\n내가 겪은 불편함 업로드 경로가 레포 루트 기준이라 프로젝트 구조에 맞추기 어려움 티어 경로가 세분화되지 않아 문제 정리가 아쉬움 문제 디렉토리명이 패키지/경로로 쓰기 불편한 형태 Java 파일명과 실행 엔트리(Main.java)를 수동으로 맞춰야 하는 경우 발생 기존에 풀어둔 문제를 한 번에 정리해서 올리기 어려움 그래서 커스텀에서 바꾼 부분 Base Directory 지정 기능 추가 티어 경로를 Bronze/V처럼 세분화 문제 디렉토리명 정규화 Java 파일명 Main.java 고정 + package 자동 삽입 백준 맞은 문제 전체 업로드(벌크 업로드) 기능 추가 정리 이번 커스텀은 “기능을 새로 많이 만들자”보다는,\n실제로 문제를 풀고 정리하는 과정에서 반복되는 불편함을 줄이는 데 초점을 맞춤.\n공식 저장소에서도 이미 비슷한 요구가 있었던 만큼, 같은 고민을 가진 분들에게는 이 커스텀 방향이 참고가 될 수 있다고 생각함.\n","date":"2026-02-20T22:35:24+09:00","image":"/posts/260220_baekjoonhub/featured.png","permalink":"/posts/260220_baekjoonhub/","title":"Baekjoonhub 크롬 익스텐션 커스텀"},{"content":"서론 이전 포스트인 웹소켓 이전의 양방향 통신에 이어서 Websocket 프로토콜 문서인 RFC 6455 를 읽어보며 Websocket 에 대해 상세하게 다뤄보고자 함.\n내용 중 자세한 설명을 위한 예시들은 AI를 이용하여 생성하였음.\nRFC 6455 (The Websocket Protocol) Websocket 프로토콜의 목적은 서버와의 양방향 통신이 필요한 브라우저 기반 애플리케이션에 다중 Http 연결 (ex: XmlHttpRequest, long polling)에 의존하지 않는 매커니즘을 제공하는 것이라고 함. Websocket 은 TCP 위의 기본 메시지 프레이밍에 이어지는 초기 핸드쉐이크로 구성되어짐.\n과거 클라이언트와 서버간 양방향 통신이 필요했던 어플리케이션의 경우 HTTP 를 남용하여 서버 업데이트를 폴링하면서 그 위에 단에서의 알림을 별개의 HTTP 호출로 전송해야 했음 (RFC 6202).\n이 문제를 해결하는 방법은 양방향 트래픽에 단일 TCP 연결을 사용하는 방법임. Websocket 은 이 방식을 지원함. websocket API 문서: https://websockets.spec.whatwg.org//\nWebsocket 은 프록시, 필터링, 인증 과 같은 기존 인프라 구조의 이점을 이용하기 위해 HTTP 를 전송계층으로 사용한는 양방향 통신기술을 대체하기위해 고안되었음. 기존 기술은 효율성과 신뢰성 사이의 절충안으로 구현되었음 그 이유는 HTTP 가 애초에 양방향 통신을 위해 구현된게 아니기 때문이라고 함.\nWebsocket 프로토콜은 HTTP 인프라 환경에서 기존의 양방향 HTTP 기술들의 목표를 해결하기위한 시도를 함. 따라서 HTTP 80, 443 port 를 통해 작동하도록 설계되었으며, 현재 환경에서 복잡성을 수반하더라도 HTTP 프록시 및 중개자를 지원함.\n그렇다고 이 설계가 웹소켓을 HTTP 로 제한하지 않음. 향후 구현에서는 전체 프로토콜울 재구축하지 않고도 전용 포트를 사용하되 전체 프로토콜을 재구축하지 않고도 가능하다고 함\n문서에서 이부분을 강조하는데 이는 대화형 메시징의 트래픽 패턴이 표준 HTTP 트래픽과 유사하지 않아 일부 구성 요소에 비정상적인 부하를 유발할 수 있기 때문에 중요하다고 함.\nProtocol Overview Websocket 프로토콜은 handshake, data transfer 이렇게 두 파트로 나뉨.\n// Client 의 handshake 요청 GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 // Server 의 handshake 응답 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat Client 는 Request-Line 형식을 따르고 Server 는 Status-Line 형식을 따름. (RFC 2616)\nClient 와 Server 양쪽 모두 handshake 를 보내고 성공이 되었다면 data transfer 가 시작됨. 이것이 각자 독립적으로 어떠한 요청 없이 자신의 의지로 데이터를 전송할 수 있는 양방향 통신 채널임.\n이제 Client 와 Server 는 websocket spec 에서 message 라고 지칭하는 단위로 데이터를 주고받음.\nWebsocket message 는 특정 네트워크 계층의 프레임과 일치하지 않을 수 있음. 그 이유는 중간 장치에 의해서 분할된 메세지가 병합되거나 그 반대의 경우가 발생할 수 있기 때문이라고 함.\n동일한 message에 속하는 각 프레임은 동일한 유형의 데이터를 포함함. 크게 textual data, binary data, control frames(어플리케이션 데이터 전달이 아님 ex 프로토콜 수준의 신호용으로 연결 종료 신호) 가 있음.\nWebsocket 프로토콜은 여섯 가지 프레임 유형을 정의하고 향후 사용을 위한 열 가지를 예약을 해둔다고 함.\nOpcode 타입 이름 설명 0x0 데이터 Continuation 이전 프레임의 연속 데이터 0x1 데이터 Text UTF-8 텍스트 데이터 0x2 데이터 Binary 바이너리 데이터 0x3~0x7 데이터 Reserved 향후 데이터 프레임 확장용 (5개) 0x8 제어 Close 연결 종료 요청 0x9 제어 Ping 연결 상태 확인 (heartbeat) 0xA 제어 Pong Ping에 대한 응답 0xB~0xF 제어 Reserved 향후 제어 프레임 확장용 (5개) 분할된 메세지가 병합되거나 반대의 경우가 발생할 수 있다? 라는 부분이 이해가 잘 안갈 수 있는데 예시를 들어보겠음.\n우선 왜 분할(fragmentaion)이 발생하는지? 그 이유는 아래와 같음\nMTU(Maximum Transmission Unit) 제한(네트워크 패킷 크기 제한 : 보통 1500 bytes) 일반적으로 1500 bytes 보다 큰 패킷은 경로상의 중간장치에서 더 작은 조각으로 나뉘어져 전송(단편화)됨. 예시로 지하터널의 높이 제한을 생각하면 됨.\n서버에서 특정 크기로 쪼개서 보내도록 설정되어 있을 수 있음\n1번과 같은 맥락인데 프록시, 로드밸러서, API 게이트웨이 같은 중간장치가 큰 프레임을 쪼갬\n메모리 효율: 대용량 데이터를 한번에 버퍼링하지 않기 위해 쪼갬\n다시 본론으로 돌아가서 병합되는 예시와 반대의 경우를 살펴보겠음\n1. Coalesced(병합) (원본: 쪼개서 발송) [Frame 1: FIN=0, opcode=text, \u0026#34;Hello \u0026#34;] [Frame 2: FIN=0, opcode=continuation, \u0026#34;World\u0026#34;] [Frame 3: FIN=1, opcode=continuation, \u0026#34;!\u0026#34;] (중간자가 위의 세 프레임을 받아서 하나의 프레임으로 합쳐서 전달함) [Frame: FIN=1, opcode=text, \u0026#34;Hello World!\u0026#34;] 2. Split(분할) (원본) [Frame: FIN=1, \u0026#34;Hello World!\u0026#34;] (중간자가 원본 프레임을 쪼갬) [Frame 1: FIN=0, \u0026#34;Hello \u0026#34;] [Frame 2: FIN=1, \u0026#34;World!\u0026#34;] 다음 예시로 Websocket 으로 실시간 주식 데이터를 수신하는 Spring boot 서버 (client 역할)와 금융 서버(server 역할) 가 있다고 가정하고 코드를 살펴보겠음.\n금융 서버에서 아래와 같이 하나의 message 를 쪼개서 보냈다고 가정을 하겠음. (중간에 병합되지 않는다고 가정) [WebSocket Frame 1] FIN: 0 (아직 끝 아님) Opcode: 0x1 (text) Payload: {\u0026#34;stockCode\u0026#34;:\u0026#34;005930\u0026#34;,\u0026#34;price\u0026#34;:71500,\u0026#34;vol [WebSocket Frame 2] FIN: 0 (아직 끝 아님) Opcode: 0x0 (continuation) Payload: ume\u0026#34;:50000,\u0026#34;time\u0026#34;:\u0026#34;09:00:01\u0026#34;,\u0026#34;seller\u0026#34;:\u0026#34; [WebSocket Frame 3] FIN: 1 (이게 마지막) Opcode: 0x0 (continuation) Payload: foreign\u0026#34;,\u0026#34;buyer\u0026#34;:\u0026#34;institution\u0026#34;,...} @Component public class WebSocketHandler extends TextWebSocketHandler { @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 여기서 message.getPayload()는 이미 완전한 message임 // {\u0026#34;stockCode\u0026#34;:\u0026#34;005930\u0026#34;,\u0026#34;price\u0026#34;:71500,\u0026#34;volume\u0026#34;:50000,...} 전체가 옴 String payload = message.getPayload(); } } payload 가 완전한 message 인 이유는 Spring 이 내부적으로 Frame 1 과 2 가 FIN 이 0 이기 때문에 버퍼에 저장하고 이어붙이다가 Frame 3 에서 FIN 이 1 을 확인하고 버퍼 내용을 합쳐서 handleTextMessage() 를 호출하기 때문임.\n이런 부분을 공부해 보면서 느낀점은 평소 개발시에 프레임워크에서 이미 잘 구현된 기능들을 사용하다보니 이렇게 네트워크 레벨에서 일어나는 일들을 잘 모르고 지나치게 되는 것 같다는 생각이 들었음.\n다시 Websocket protocol 의 큰 틀을 정리해보면\nHandshake 와 Data transfer 이렇게 두 파트로 나뉨. Handshake 가 성공하면 이제 양방향 통신을 할 수 있는 상태가 되고 data transfer 가 이루어짐. 이때 websocket spec 에서 사용하는 data 단위인 message 를 주고 받음. Message 는 특정 네트워크 프레임과 일치하지 않을 수 있음. 중간장치에 의해 message 가 분할되거나 병합될 수 있기 때문 Websocket 에 정의된 프레임은 data frame 3개, controle frame 3개 그리고 각각 예약 프레임 5개 씩 총 16개로 구성 됨 다음으로는 Opening Handshake 가 어떻게 일어나는지 살펴보겠음.\nOpening Handshake Header 용도 Upgrade 프로토콜 업그레이드 요청 Connection 연결 업그레이드 요청 Sec-Websocket-Key 보안 키(Base64 인코딩된 16 bytes 랜덤 값) Sec-Websocket-Version 프로토콜 버전(현재 13) Sec-Websocekt-Protocol 서브프로토콜(선택) Origin 브라우저에서 보내는 출처 정보 초기 handshake 는 HTTP 기반 서버와 중개자와 호환되도록 설계되어 서버와 통신하는 HTTP 클라이언트와 웹소켓 클라이언트 모두 단일 포트를 사용할 수 있음. 따라서 웹소켓 클라이언트의 handshake 는 HTTP 업그레이드 요청으로 이루어짐.\nHandshake 의 header 필드는 클라이언트 측에서 임의의 순서로 전송할 수 있으므로 서로 다른 header 필드가 수신되는 순서는 중요하지 않음. 클라이언트는 handshake 의 |Host| header 필드에 호스트명을 포함시켜 클라이언트와 서버 모두 사용중인 호스트에 대해 합의했는지 확인 할 수 있음.\n추가 header 필드는 Websocket 프로토콜에서 옵션을 선택하는데 사용됨. 일반적으로 사용하는 옵션으로 서브프로토콜 선택기 (Sec-Websocket-Protocol), 클라이언트가 지원하는 확장 목록(Sec-Websocket-Extension), Origin 필드 등이 있음. |Sec-Websocket-Protocol| 요청 header 필드는 클라이언트가 허용하는 서브프로토콜(웹소켓 프로토콜 위에 계층화된 어플리케이션 레벨 프로토콜)을 표시하는 데 사용할 수 있음. 서버는 허용가능한 프로토콜 중 하나 또는 아무것도 선택지 않을 수 있고 선택한 프로토콜을 나타내기 위해 handshake 에서 그 값을 echo 함.\n// Client 의 handshake 요청 GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 // Server 의 handshake 응답 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat \u0026lt;- server 가 선택한 프로토콜을 echo 함 서버는 클라이언트의 handshake 를 수신하면 두 가지 정보를 응답에 포함해야 하는 데 첫번째는 Sec-Websocket-Accept 임.\nSec-Websocket-Accept 계산 방법 |Sec-Websocket-Accept| 필드는 서버가 websocket 연결을 수락할 의사가 있는지 여부를 나타냄.\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 를 가져옴 dGhlIHNhbXBsZSBub25jZQ== + \u0026ldquo;258EAFA5-E914-47DA-95CA-C5AB0DC85B11\u0026rdquo; SHA-1 해시 계산 : 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea Base64 인코딩 : \u0026ldquo;s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\u0026rdquo; Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 를 응답에 포함 이렇게 Sec-Websocket-Key 를 만들고 상태값 101 을 같이 응답에 포함하여 handshake 를 수락했음을 client 에게 알려줌.\n// 서버측에서 client 의 handshake 연결을 수락하고 정상적으로 연결이되면 상태코드로 101 을 가지게 됨. 101 이외의 상태 코드는 웹소켓 handshake가 완료되지 않은것임. HTTP/1.1 101 Switching Protocols 클라이언트는 이 응답을 기반으로 Sec-Websocket-Accept 값이 예상된 값이 아니거나, 헤더 필드가 누락되었거나, HTTP 상태 코드가 101 이 아닌 경우를 확인하고 웹소켓 프레임은 전송되지 않음.\n다시 정리를 해보자면\nHandshake 는 HTTP 기반 프로토콜 upgrade 요청으로 시작됨 header 의 순서는 상관없음 서버의 연결 응답에는 Sec-Websocket-Key 를 이용해 Sec-Websocket-Accept 의 값을 계산하고 이와 함께 101 상태값을 내려주어야 함 다음으로는 Websocket 종료는 어떻게 일어나는지 살펴보겠음.\nClosing HandShake Websocket 은 연결 종료시에도 handshake 방식으로 진행을 함.\n한쪽에서 종료를 나타내는 control frame(제어 프레임) 을 전송 반대쪽에서 종료 control frame 으로 응답 양쪽 모두 close frame 을 보내고 받으면 TCP 연결 종료 이렇게 자체 handshake 를 통해 종료를 하는 이유는 TCP closing handshake(FIN/ACK) 을 보완하기 위한 것으로 중간에 가로채는 프록시나 기타 중개자가 존재 할 경우 TCP closing handshake 가 항상 종단 간 신뢰할 수 있는 것은 아니라는 것에 근거한다고 함.\n종단간 신뢰할 수 없다는 부분을 좀 더 살펴보면 TCP closing handshake(4-way handshake) 를 사용하면 데이터 손실이 발생할 수 있기 때문임.\n// TCP-Closing handshake Client Server | | |-------- FIN -----------\u0026gt;| \u0026#34;나 보낼 거 다 보냄\u0026#34; |\u0026lt;------- ACK ------------| \u0026#34;알겠어\u0026#34; |\u0026lt;------- FIN ------------| \u0026#34;나도 다 보냄\u0026#34; |-------- ACK -----------\u0026gt;| \u0026#34;알겠어\u0026#34; | | 이 문서에서 말하는 핵심 문제는 중개자(프록시, 로드밸런서)가 있으면 TCP 종료가 end-to-end 로 전달되지 않을 수 있다는 것임.\n만약 TCP closing handshake 로 종료를 한다고 했을 때 예시를 살펴보겠음.\n플래그를 모를 수 있기 때문에 표를 첨부함.\n플래그 목적 언제 사용 SYN 연결 시작 3-way handshake 시작 ACK 수신 확인 거의 모든 패킷에 포함 FIN 정상 종료 보낼 데이터 없을 때 RST 강제 종료 에러 상황, 비정상 종료 // 상황 재현: 증권 서버가 데이터를 보내고 있는 중에 Client가 소켓을 닫음 // 증권 Server 측 session.sendMessage(new TextMessage(stockData1)); // 전송됨 session.sendMessage(new TextMessage(stockData2)); // 전송됨, 아직 Client가 안 읽음 // Client 측 - 갑자기 소켓 닫음 socket.close(); // stockData2가 receive queue에 있는 상태에서 닫힘 이때 OS 레벨에서 일어나는 일:\n[Client 의 receive queue] +------------------+ | stockData2 (미처리) | ← 아직 애플리케이션이 안 읽음 +------------------+ socket.close() 호출됨 ↓ OS: \u0026#34;어? 읽지 않은 데이터가 있는데 닫으라고?\u0026#34; ↓ OS가 RST 패킷 전송 (정상적인 FIN 대신) ↓ 증권 Server: RST 수신 ↓ 증권 Server의 recv() 호출 실패 (Connection reset by peer) RST vs FIN 차이 FIN: \u0026#34;나 할 말 다 했어, 깔끔하게 끝내자\u0026#34; RST: \u0026#34;비상! 연결 강제 종료! 뭔가 잘못됐어!\u0026#34; RST를 받은 증권 Server는:\n보내려던 나머지 데이터도 버림 에러 로그 남김 연결 상태 추적이 어려워짐 이걸 보면 이런 생각이 들 수 도 있음. 아니 그럼 HTTP request, response 에서도 똑같이 이 문제가 생길 수 있는거 아닌가?\n맞음. 동일한 문제가 발생할 수 있음. 하지만 크게 상관이 없음.\n이미 요청/응답 이 완료됨 연결이 일회성임 다음 요청은 새 연결로 하면 됨 HTTP 예시 Client Proxy Server │ │ │ │── GET /stock ─────────\u0026gt;│── GET /stock ─────────\u0026gt;│ │ │ │ │\u0026lt;── 200 OK + data ──────│\u0026lt;── 200 OK + data ──────│ │ │ │ │── FIN ────────────────\u0026gt;│ │ │ │ (전달할 수도, 안 할 수도) │ │ │ HTTP 는 stateless 하고 요청/응답이 끝나면 연결의 역할이 끝나기 때문에 비정상 종료되어도 큰 문제가 없음.\nWebsocket 예시 Client Proxy Server │ │ │ │══ WebSocket 연결 (지속) ══════════════════════════│ │ │ │ │\u0026lt;── stockData1 ─────────│\u0026lt;── stockData1 ─────────│ │\u0026lt;── stockData2 ─────────│\u0026lt;── stockData2 ─────────│ │\u0026lt;── stockData3 ─────────│\u0026lt;── stockData3 ─────────│ │ ... │ ... │ │ │ │ │── FIN ────────────────\u0026gt;│ │ │ │ (전달 안 됨) │ │ │ │ │ │\u0026lt;── stockData4 ─────────│ ← Server는 계속 보냄! │ │\u0026lt;── stockData5 ─────────│ │ │\u0026lt;── stockData6 ─────────│ │ │ │ │ │ Proxy 버퍼에 쌓임 │ │ │ 또는 어디선가 손실 │ 하지만 Websocket 은 stateful 하고 연결이 계속 유지되면서 데이터가 흐르기 때문에 문제가 됨.\n이러한 문제 때문에 Websocket 은 자체 handshake 를 통해 종료를 하는거임. Close frame 은 애플리케이션 레이어 메세지라 proxy 가 반드시 전달을 해야함. TCP FIN 처럼 proxy 가 임의로 처리가 불가능함.\nClient Proxy Server │ │ │ │── Close Frame ────────\u0026gt;│── Close Frame ────────\u0026gt;│ ← 애플리케이션 레벨 │ │ │ │ │ Server: \u0026#34;아 종료구나\u0026#34; │ │ │ 구독 해제 │ │ │ 데이터 전송 중단│ │ │ │ │\u0026lt;── Close Frame ────────│\u0026lt;── Close Frame ────────│ │ │ │ │── FIN ────────────────\u0026gt;│── FIN ────────────────\u0026gt;│ ← 이제 TCP 종료 WebSocket 설계 철학: 최소한의 프레이밍 핵심 원칙 RFC 6455에서 WebSocket의 설계 원칙을 이렇게 명시하고 있음:\n\u0026ldquo;The WebSocket Protocol is designed on the principle that there should be minimal framing\u0026rdquo;\nWebSocket이 제공하는 프레이밍은 딱 두 가지 목적만을 위한 것임:\n스트림 → 메시지 단위 변환: TCP는 연속적인 바이트 스트림인데, 애플리케이션은 \u0026ldquo;메시지\u0026rdquo; 단위로 생각함 텍스트 vs 바이너리 구분: UTF-8 텍스트인지 임의의 바이너리 데이터인지 구분 그 외의 모든 메타데이터(메시지 타입, 라우팅, 인증 등)는 애플리케이션 레이어에서 알아서 처리하라는 철학임.\nTCP의 문제: 메시지 경계가 없음 TCP는 바이트 스트림 프로토콜임. 데이터가 파이프를 타고 흐르듯 연속적으로 전달될 뿐, \u0026ldquo;여기서 메시지 끝\u0026quot;이라는 구분이 없음.\n보내는 쪽: send(\u0026#34;Hello\u0026#34;) send(\u0026#34;World\u0026#34;) TCP 파이프 안: [H][e][l][l][o][W][o][r][l][d] ← 전부 붙어있음 받는 쪽이 실제로 받을 수 있는 것: recv() → \u0026#34;Hel\u0026#34; recv() → \u0026#34;loWor\u0026#34; recv() → \u0026#34;ld\u0026#34; 받는 쪽은 \u0026ldquo;Hello\u0026quot;가 어디서 끝나고 \u0026ldquo;World\u0026quot;가 어디서 시작하는지 알 수 없음.\nWebSocket의 해결: 프레임으로 경계 복원 WebSocket은 각 메시지를 프레임으로 감싸서 경계를 만들어줌:\n보내는 쪽: ws.send(\u0026#34;Hello\u0026#34;) ws.send(\u0026#34;World\u0026#34;) WebSocket 프레이밍 후: [FIN=1, len=5, \u0026#34;Hello\u0026#34;][FIN=1, len=5, \u0026#34;World\u0026#34;] ├─── 프레임 1 ───────┤├─── 프레임 2 ────────┤ 받는 쪽: onMessage(\u0026#34;Hello\u0026#34;) ← 정확히 원본 메시지 단위로 받음 onMessage(\u0026#34;World\u0026#34;) \u0026ldquo;최소한\u0026quot;의 의미 WebSocket 프레임 헤더에 들어있는 정보는 이게 전부임:\n필드 용도 FIN 메시지의 마지막 프레임인지 Opcode 텍스트(0x1) vs 바이너리(0x2) vs 컨트롤(0x8, 0x9, 0xA) Length 페이로드 길이 Mask 마스킹 여부 (보안용) HTTP 헤더와 비교해보면 차이가 확연함:\nHTTP 헤더 (수백 바이트): Content-Type: application/json Content-Length: 42 Authorization: Bearer xxx X-Request-ID: abc123 Cache-Control: no-cache ... 등등 WebSocket 프레임 헤더 (2~14 바이트): [FIN + opcode][MASK + length] 끝. WebSocket이 해주지 않는 것들 \u0026ldquo;최소한의 프레이밍\u0026rdquo; 철학은 곧 나머지는 알아서 하라는 뜻임:\n클라이언트가 보내는 실제 메시지: { \u0026#34;type\u0026#34;: \u0026#34;SUBSCRIBE\u0026#34;, \u0026#34;channel\u0026#34;: \u0026#34;stock.005930\u0026#34;, \u0026#34;userId\u0026#34;: \u0026#34;gun0\u0026#34;, \u0026#34;token\u0026#34;: \u0026#34;abc123\u0026#34; } WebSocket이 아는 것:\n\u0026ldquo;이건 텍스트 프레임이고, 길이는 120바이트다\u0026rdquo; WebSocket이 모르는 것:\ntype이 뭔지 channel로 어디에 라우팅해야 하는지 userId와 token으로 인증을 어떻게 처리할지 이런 것들은 전부 애플리케이션 레이어가 직접 구현해야 함.\n그래서 STOMP 같은 서브프로토콜을 사용하는 것 WebSocket만 사용하면:\n@OnMessage public void onMessage(String message) { // message = 그냥 문자열 // 이게 뭔지, 누구한테 보낼지 직접 파싱해야 함 JSONObject json = new JSONObject(message); String type = json.getString(\u0026#34;type\u0026#34;); if (type.equals(\u0026#34;SUBSCRIBE\u0026#34;)) { // 구독 로직 직접 구현 } else if (type.equals(\u0026#34;UNSUBSCRIBE\u0026#34;)) { // 구독 해제 로직 직접 구현 } else if (type.equals(\u0026#34;SEND\u0026#34;)) { // 메시지 전송 로직 직접 구현 } } STOMP를 얹으면:\n@MessageMapping(\u0026#34;/stock/{stockCode}\u0026#34;) public void handleStock(@DestinationVariable String stockCode, StockRequest request) { // 메시지 타입, 라우팅이 이미 처리되어 있음 } TCP 위에 WebSocket이 추가하는 것들 RFC 6455에서 WebSocket의 역할을 명확히 정의하고 있음:\n1. 웹 Origin 기반 보안 모델 Origin: http://example.com 브라우저 환경에서 \u0026ldquo;이 스크립트가 어디서 왔는지\u0026quot;를 서버에 알려줌. 서버가 cross-origin 요청을 거부할 수 있는 근거를 제공함.\n2. 주소 지정 및 프로토콜 네이밍 GET /chat HTTP/1.1 Host: server.example.com Sec-WebSocket-Protocol: stomp, mqtt 하나의 IP + 하나의 포트에서 여러 서비스를 제공할 수 있음:\nPath로 엔드포인트 구분 (/chat, /notifications) Host 헤더로 가상 호스팅 Sec-WebSocket-Protocol로 서브프로토콜 협상 3. 프레이밍 메커니즘 RFC에서 흥미로운 표현을 쓰고 있음:\n\u0026ldquo;layers a framing mechanism on top of TCP to get back to the IP packet mechanism that TCP is built on, but without length limits\u0026rdquo;\n레이어 특성 IP 패킷 기반, 경계 명확, 크기 제한 있음 (~1500 bytes) TCP 스트림 기반, 경계 없음, 크기 제한 없음 WebSocket 프레임 기반, 경계 명확, 크기 제한 없음 TCP가 IP 패킷들을 이어붙여서 \u0026ldquo;연속적인 스트림\u0026quot;으로 만들어버렸는데, WebSocket이 다시 \u0026ldquo;메시지 단위\u0026quot;를 복원해주는 것임.\n4. 프록시 친화적 Closing Handshake TCP FIN/ACK만으로는 중간에 프록시가 있을 때 데이터 손실이 발생할 수 있음:\n[Client] ----data----\u0026gt; [Proxy] ----data----\u0026gt; [Server] [Client] \u0026lt;---FIN------ [Proxy] [Server] ← 프록시가 임의로 끊을 수 있음 WebSocket Close 프레임은 애플리케이션 레이어에서 종료를 협상하기 때문에 더 안전함:\n[Client] ---Close Frame---\u0026gt; [Proxy] ---Close Frame---\u0026gt; [Server] [Client] \u0026lt;--Close Frame---- [Proxy] \u0026lt;--Close Frame---- [Server] [Client] -------TCP FIN-------\u0026gt; ... -------TCP FIN-------\u0026gt; [Server] \u0026ldquo;Raw TCP에 최대한 가깝게\u0026rdquo; RFC의 핵심 문장:\n\u0026ldquo;Basically it is intended to be as close to just exposing raw TCP to script as possible given the constraints of the Web.\u0026rdquo;\n브라우저에서 JavaScript로 raw TCP 소켓을 직접 열 수는 없음 (보안상 이유). WebSocket은 그 제약 내에서 최대한 TCP에 가까운 경험을 제공하려는 것임.\nWebSocket이 추가하지 않는 것들:\n기능 이유 메시지 ID 애플리케이션이 알아서 요청-응답 매핑 양방향 스트림일 뿐 재전송/순서 보장 TCP가 이미 제공 메시지 압축 기본은 raw (확장으로 가능) 인증 HTTP 핸드셰이크에서 처리 라우팅 서브프로토콜이 처리 HTTP 인프라와의 공존 \u0026ldquo;It\u0026rsquo;s also designed in such a way that its servers can share a port with HTTP servers\u0026rdquo;\n이건 실용적으로 매우 중요한 설계 결정임:\nPort 80/443 │ ├── GET /api/users HTTP/1.1 → 일반 HTTP 처리 ├── GET /index.html HTTP/1.1 → 일반 HTTP 처리 └── GET /ws HTTP/1.1 → WebSocket 업그레이드 Upgrade: websocket 장점:\n기존 로드밸런서, 프록시, 방화벽 통과 가능 추가 포트 오픈 불필요 SSL 인증서 공유 가능 핸드셰이크가 HTTP Upgrade 형태인 이유도 이것 때문임. RFC에서 이렇게 언급하고 있음:\n\u0026ldquo;the design does not limit WebSocket to HTTP, and future implementations could use a simpler handshake over a dedicated port without reinventing the entire protocol\u0026rdquo;\nHTTP 호환은 현재 웹 인프라를 위해 선택한 것이지, 프로토콜의 본질은 아니라는 뜻임.\n확장성 \u0026ldquo;The protocol is intended to be extensible; future versions will likely introduce additional concepts such as multiplexing.\u0026rdquo;\n확장을 위해 예약해둔 것들:\n예약 항목 용도 RSV1, RSV2, RSV3 비트 프레임별 확장 플래그 Opcode 0x3-0x7 추가 데이터 프레임 타입 Opcode 0xB-0xF 추가 컨트롤 프레임 타입 Sec-WebSocket-Extensions 헤더 확장 협상 실제 확장 예시 - permessage-deflate:\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits 메시지 압축 확장인데, RSV1 비트를 사용해서 \u0026ldquo;이 프레임은 압축됨\u0026quot;을 표시함.\n비유로 정리 TCP = 고속도로\n차(바이트)들이 줄줄이 달림 어디서 한 무리가 끝나고 다음 무리가 시작하는지 구분선 없음 WebSocket = 컨테이너 트럭\n화물(메시지)을 컨테이너(프레임)에 담아서 운송 \u0026ldquo;컨테이너 안에 뭐가 들었는지\u0026quot;는 컨테이너 자체는 모름 그냥 \u0026ldquo;컨테이너 크기가 얼마다\u0026rdquo; 정도만 표시 STOMP = 물류 시스템\n컨테이너 안에 송장을 붙여서 \u0026ldquo;이건 A에게, 저건 B에게\u0026rdquo; 화물 종류별로 분류 배송 추적 WebSocket은 컨테이너 트럭처럼 안전하게 덩어리째 전달만 해주고, 그 안의 내용물 관리는 STOMP 같은 상위 프로토콜이 담당하는 구조임.\n정리 WebSocket의 설계 철학은 \u0026ldquo;최소한만 하고, 나머지는 위임한다\u0026rdquo; 로 요약할 수 있음:\nWebSocket이 하는 것 WebSocket이 안 하는 것 메시지 경계 구분 메시지 타입/라우팅 텍스트/바이너리 구분 인증/권한 연결 유지 재연결 로직 Ping/Pong heartbeat 비즈니스 로직 Origin 기반 보안 애플리케이션 레벨 보안 이런 철학 덕분에 WebSocket은 가볍고 범용적임. 그 위에 STOMP, Socket.IO, 또는 직접 만든 프로토콜을 얹어서 각자의 요구사항에 맞게 확장할 수 있음.\n이렇게 RFC 6455 문서를 살펴보면서 Websocket 에 대해 알아 보았음. 다음 포스트 에서는 SpringBoot 에서 Websocket 이 어떻게 어떻게 구현되어져 있는지 살펴보는 시간을 가져볼까함.\n","date":"2026-01-21T15:38:26+09:00","image":"/posts/260210_websocket/featured.png","permalink":"/posts/260210_websocket/","title":"Websocket 이란? (RFC 6455)"},{"content":"서론 모의투자 프로젝트를 진행해보면서 주식의 가격변동 데이터를 실시간성으로 서빙해야하는 상황이 생겼고 Websocket 기술을 사용하게 됨. 양방향 실시간 통신 기술인 Websocket 을 이전부터 알고는 있었지만 상세하게 파고든적은 없어 이번 기회에 깊게 파고들어 가보고자함.\nWebsocket 기술 문서인 RFC 6455 를 읽어보다 더 과거의 처리방식과 문제점을 설명하며 RFC 6202(Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP)를 인용한 부분이 있었음. 오늘은 RFC 6202 를 읽어보며 Websocket이 나오기 전에는 어떤 방식으로 처리를 하였고 그 당시의 생각과 Best practice 들에 관하여 알아가보고자 함.\nRFC 6202 (Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP) 2011년 4월 작성된 문서로 이 문서에서는 그 당시의 양방향 HTTP 통신의 잘 알려진 문제점과 best practice 로 HTTP long polling 과 HTTP streaming 에 대해 다룸. 또한 HTTP long polling과 HTTP streaming 모두 HTTP 를 확장한 것이며 HTTP 프로토콜이 양방향 통신을 위해 설계되지 않았음을 인정함. 문서의 저자들은 이 문서가 위 두가지 방식의 사용을 권장하지도 사용하지 말라는 것도 아니며 그저 좋은 사용사례와 문제점들을 얘기하는 것에 중점을 둔다고 표기함.\n기본적으로 HTTP(Hypertext Transfer Protocol:RFC2616)은 request/response 프로토콜임. HTTP 는 clients, proxies, servers 이 세 가지 엔티티를 정의 하고 있음. client 는 HTTP request 를 서버에 보내기 위해 연결을 생성하고, 서버는 응답을 반환하여 HTTP request 를 처리하기 위해 연결을 수락함. Proxies 는 클라이언트와 서버 사이에서 요청과 응답을 전달하는데 개입 할 수 있는 객체임.\n기본적으로 표준 HTTP 모델은 서버가 먼저 클라이언트에게 연결을 시작할 수 없고 요청하지 않은 HTTP 응답을 보낼 수 없기 때문에 서버가 클라이언트에게 비동기 이벤트를 보낼 수 없음.\n그래서 비동기 이벤트를 최대한 빠르게 받기 위해 클라이언트는 주기적으로 서버를 폴링을 해야하는데 이런 지속적인 폴링은 데이터가 없을때도 요청/응답을 강제로 발생시켜 네트워크 리소스를 잡아먹고, 데이터가 다음 폴링 요청을 서버가 수신할 때 까지 큐에 쌓이기 때문에 애플리케이션의 응답 효율을 떨어뜨림.\nHTTP long polling \u0026amp; HTTP streaming 1. HTTP long polling 전통적인 short polling 기술은 클라이언트에서 서버측으로 주기적으로 요청을 보내어 데이터를 업데이트 하는 방식이지만 새로운 이벤트가 없어도 빈 응답을 받거나 다음 polling 까지 대기를 해야함. 이 기술은 클라이언트가 설정한 지연시간에 따라 요청 주기가 결정되고 이 주기가 짧을 경우(폴링빈도=높음) 서버,네트워크 양쪽 모두에 감당하기 어려운 부담을 초래할 수 있음.\n이와 반대로 long polling은 특정 이벤트, 상태 또는 네트워크 타임아웃이 발생 하였을때만 요청에 대해 응답을 하여 메시지 전달 지연과 네트워크 자원 사용을 최소화 하려고 시도함.\nHTTP long polling life cycle 클라이언트가 초기 요청을 생성하고 응답을 기다림 서버는 업데이트가 가능하거나 특정상태 또는 타임아웃이 발생할때까지 응답을 보류함 업데이트가 가능해지면 서버는 클라이언트로 응답을 전송함 클라이언트는 응답을 받은 직후 새로운 long poll request 를 바로 생성하거나 허용가능한 일정 지연시간 동안 정지 후 생성함. HTTP long polling issue Header overhead : 매 요청/응답이 HTTP 메세지 이므로 데이터가 작더라도 HTTP header 가 항상 따라붙음. 작고 데이터의 경우 헤더가 데이터 전송의 상당부분을 차지하게 된다. 만약 네트워크 MTU(Maximum Transmission Unit)가 헤더를 포함한 모든 정보를 단일 IP 패킷에 수용 가능하다면 네트워크 부담은 크게 없음. 하지만 작은 메시지가 자주 오갈 때, 실제 데이터 대비 전송량이 커지는 문제가 발생함. 예시로 편지지 한 장(20g)을 보내는데 택배 박스(300g) 에 넣어서 보내는 것 과 같은 맥락임.\nMaximal latency : long poll 응답을 보낸 직후 서버가 바로 새 메시지를 보내고 싶어도 클라이언트의 다음 요청이 올때까지 기다려야 함. 평균 지연은 1 network trasit 에 가깝지만 최악의 경우 3 network transit(response-request-response) 까지 늘어날 수 있고 TCP 패킷 손실이 되었을 경우 재전송까지 고려하면 그 이상이 발생할 수 있음.\nConnection Establishment : short polling, long polling 모두 TCP/IP 연결을 열고 닫는 것을 자주 한다는 비판이 있음. 하지만 두 polling 메커니즘은 재사용될 수 있는 presistent HTTP connection 과 잘 작동함.\nAllocated Resources : 운영체제는 TCP/IP 연결 및 연결 보류 중인 HTTP 요청들에게 자원을 할당함. HTTP long polling 은 각 클라이언트에 대해 TCP/IP 연결과 HTTP 요청이 모두 열린 상태로 유지되도록 요구함. 따라서 HTTP 롱 폴링 애플리케이션의 규모를 결정할 때 이 두가지와 관련된 리소스를 고려하는 것이 중요함.\nGraceful Degradation : 서버나 클라이언트가 과부하 상태일 때 메시지가 큐에 쌓이다가 한 응답에 여러 메시지를 묶어서 보낼 수 있음. 지연은 늘어나지만 메시지당 오버헤드가 줄어서 부하가 자연스럽게 분산됨.\nTimeouts : long poll 요청은 서버가 보낼 데이터가 생길 때까지 계속 대기(hanging) 상태를 유지해야 하기 때문에 타임아웃 문제가 발생 할 수 있음.\nCaching : 중간 프록시나 CDN 이 응답을 캐싱하면 최신 데이터가 아닌 오래된 데이터를 받는 문제가 생길 수 있음. 클라이언트나 호스트가 HTTP 중개자에게 롱 폴링이 사용중임을 알릴 방법이 없으나 양방향 흐름을 방해 할 수 있는(=캐싱)은 표준 헤더나 쿠키로 제어가 가능함. 최선의 관행으로 롱 폴링 요청이나 응답에서는 항상 의도적으로 캐싱을 억제함. \u0026ldquo;Cache-Control\u0026rdquo; 헤더를 \u0026ldquo;no-cache\u0026rdquo; 로 설정함.\n2. HTTP Streaming HTTP streaming 의 메커니즘은 절대 request 를 종료 하거나 서버가 클라이언트에게 데이터를 보낸 후 에도 연결을 끊지 않는것임. 이러한 메커니즘은 클라이언트와 서버가 계속해서 연결을 시작하고 끊는 것을 하지 않아도 되기 때문에 네트워크 지연을 상당히 낮추어줌.\nHTTP Streaming life cycle 클라이언트가 초기 요청을 생성하고 응답을 기다림 서버는 업데이트가 가능하거나 특정상태 또는 타임아웃이 발생할때까지 응답을 보류함 업데이트가 가능해지면 서버는 클라이언트로 응답을 전송함 데이터를 보내고 서버는 요청을 종료 하거나 연결을 끊지 않고 3번을 계속해서 진행함 HTTP Streaming issue Network Intermediaries : HTTP 프로토콜은 서버에서 클라이언트로 응답을 전송하는 과정에서 중개자(프록시, 투명 프록시, 게이트웨이 등)가 개입할 수 있도록 허용함. HTTP Streaming 은 이러한 중개자와 함께 작동하지 않음.\nMaximal Latency : 이론상 1 network transit 이지만 실제로는 Javascript / DOM 요소와 관련된 메모리 사용량의 무제한 증가 방지를 위해 주기적으로 연결을 끊고 다시 맺어야 함. 결국 long polling 처럼 최대 지연은 3 network transit 이 발생함.\nClient Buffering : HTTP 스펙상 부분 응답을 즉시 처리할 의무가 없음. 대부분의 브라우저는 응답의 JS 를 실행하긴 하지만 일부는 버퍼 오버플로우가 발생해야 실행함. 공백 문자를 보내 버퍼를 채우는 방법을 사용할 수 있음.\nFraming Techniques : HTTP Streaming 을 사용하면 단일 HTTP 응답에 여러 애플리케이션 메시지를 전송할 수 있음. 하지만 중간 객체인 프록시에서 청크단위를 다시 재청크 하는 상황이 발생할 수 있기 때문에 청크 단위로 메시지를 구분 할 수 없음. 따라서 애플리케이션 레벨에서 별도로 구분자를 정의 해야함. Long polling 은 응답하나에 메시지가 하나이기 때문에 이와 같은 문제가 발생하지 않음.\n그 외의 server-push 메커니즘 소개 여기서는 위 두 가지의 메커니즘 외에 Bayeux(4.1), BOSH(4.2), Server-Sent Events(4.3) 등을 소개함. SSE 메커니즘을 사용할 때 권고사항을 다루고 있는데 아래와 같음.\n스펙상 HTTP chunking을 비활성화 하라고 권장한다. 그 이유는 위에서 설명한 HTTP streaming issue 와 같음.\n중간 프록시가 청크를 re-chunking 할 수 있음 일부 프록시가 전체 응답을 버퍼링 할 수 있음 Best Practice 요약 항목 핵심 내용 권장사항 연결 수 제한 브라우저당 6~8개 제한 Long poll은 하나로, 쿠키로 중복 감지 파이프라이닝 일반 요청이 long poll 뒤에 막힐 수 있음 지원 여부 확인 후 사용, 폴백 준비 프록시 연결 공유 시 starvation 발생 비동기 프록시 사용, 연결 공유 피하기 타임아웃 너무 높으면 408/504, 너무 낮으면 트래픽 낭비 30초 권장 캐싱 실시간 데이터가 캐시되면 안 됨 Cache-Control: no-cache 필수 보안 Injection, DoS 취약 입력 검증, 연결 수 제한 1. Limits to the Maximum Number of Connections 배경 HTTP 스펙(RFC 2616)에서는 원래 클라이언트 하나가 서버에 최대 2개 연결까지만 유지하라고 권장했음. 이유는 두 가지임:\n서버 과부하 방지 혼잡한 네트워크에서 예상치 못한 부작용 방지 최근 브라우저들은 이 제한을 6~8개로 늘렸지만, 여전히 제한이 존재함. 문제는 사용자가 여러 탭이나 프레임을 열면 이 연결들을 금방 소진한다는 것임.\n왜 문제가 되는가? Long polling은 연결을 오래 점유함. 만약 탭 3개에서 각각 long poll을 2개씩 열면:\n탭1: long poll 연결 2개 탭2: long poll 연결 2개 탭3: long poll 연결 2개 ───────────────────── 총 6개 연결 → 브라우저 한계 도달 이 상태에서 일반 HTTP 요청(이미지, API 호출 등)을 보내려면 기존 연결이 끝날 때까지 대기해야 함. 이걸 **connection starvation(연결 고갈)**이라고 부름.\n권장사항 클라이언트 측:\nLong poll 요청을 하나로 제한하고, 여러 탭/프레임이 이걸 공유하는 게 이상적임 하지만 브라우저 보안 모델 때문에 탭 간 리소스 공유가 어려움 (Same-Origin Policy 등) 서버 측:\n쿠키를 사용해서 같은 브라우저에서 오는 중복 long poll 요청을 감지해야 함 중복 요청이 감지되면 둘 다 대기시키지 말고, 하나는 즉시 응답해서 해제해야 함 [잘못된 처리] 요청1: 대기 중... 요청2: 대기 중... ← 둘 다 대기하면 connection starvation 발생 [올바른 처리] 요청1: 대기 중... 요청2: 들어옴 → 요청1에 즉시 빈 응답 → 요청2만 대기 2. Pipelined Connections 파이프라이닝이란? HTTP/1.1에서 지원하는 기능으로, 응답을 기다리지 않고 여러 요청을 연속으로 보내는 것임.\n[파이프라이닝 없음] 요청1 → 응답1 → 요청2 → 응답2 → 요청3 → 응답3 [파이프라이닝 있음] 요청1 → 요청2 → 요청3 → 응답1 → 응답2 → 응답3 Long Polling에서의 장점 서버가 짧은 시간에 여러 메시지를 보내야 할 때 유용함. 파이프라이닝이 있으면 서버가 응답 후 클라이언트의 새 요청을 기다리지 않아도 됨. 이미 큐에 요청이 쌓여있으니까.\n문제점: 일반 요청이 막힘 파이프라이닝의 치명적인 문제가 있음. 일반 요청이 long poll 뒤에 큐잉되면, long poll이 끝날 때까지 기다려야 함.\n[파이프라인 큐] 1. Long poll 요청 (30초 대기 중...) 2. 이미지 요청 ← long poll 끝날 때까지 대기 3. API 요청 ← long poll 끝날 때까지 대기 이러면 페이지 로딩이 30초씩 지연될 수 있음.\n주의사항 HTTP POST 파이프라이닝은 RFC 2616에서 권장하지 않음 BOSH나 Bayeux 같은 프로토콜은 POST를 파이프라이닝하면서 요청 ID로 순서를 보장하는 방식을 씀 파이프라이닝을 쓰려면 클라이언트, 중간 장비, 서버 모두 지원하는지 확인해야 함 지원 안 되면 파이프라이닝 없는 방식으로 폴백해야 함 3. Proxies 일반 프록시와의 호환성 Long Polling: 대부분의 프록시와 잘 동작함. 왜냐하면 결국 완전한 HTTP 응답을 보내기 때문임 (이벤트 발생 시 또는 타임아웃 시).\nHTTP Streaming: 문제가 있음. 두 가지 가정에 의존하는데:\n프록시가 각 청크를 즉시 전달할 것 → 보장 안 됨 브라우저가 도착한 JS 청크를 즉시 실행할 것 → 보장 안 됨 리버스 프록시 문제 리버스 프록시는 클라이언트 입장에서는 실제 서버처럼 보이지만, 뒤에 있는 진짜 서버로 요청을 전달하는 역할을 함.\n클라이언트 → [리버스 프록시] → 실제 서버 (Nginx, Apache 등) Long polling과 streaming 모두 동작하긴 하지만, 성능 문제가 있음. 대부분의 프록시는 많은 연결을 오래 유지하도록 설계되지 않았음.\n연결 공유 문제 (Connection Sharing) 이게 가장 심각한 문제임. Apache mod_jk 같은 프록시는 여러 클라이언트가 소수의 연결을 공유하도록 설계됨.\n[Apache mod_jk 연결 풀: 8개] 클라이언트A의 long poll → 연결1 점유 (30초...) 클라이언트B의 long poll → 연결2 점유 (30초...) 클라이언트C의 long poll → 연결3 점유 (30초...) ... 클라이언트H의 long poll → 연결8 점유 (30초...) 클라이언트I의 일반 요청 → 연결 없음! 대기... 클라이언트J의 일반 요청 → 연결 없음! 대기... 8개 연결이 모두 long poll에 점유되면, 다른 모든 요청(long poll이든 일반 요청이든)이 막혀버림. 이걸 connection starvation이라고 함.\n근본 원인: 동기식 vs 비동기식 모델 동작 방식 Long Poll 영향 동기식 요청 하나당 스레드/연결 하나 점유 리소스 고갈 심각 비동기식 이벤트 기반, 연결당 최소 리소스 영향 적음 동기식 예시: Apache mod_jk, Java Servlet 2.5 비동기식 예시: Nginx, Node.js, Java Servlet 3.0+\n결론: Long polling/streaming을 쓸 때는 연결 공유를 피해야 함. HTTP의 기본 가정은 \u0026ldquo;각 요청이 최대한 빨리 완료된다\u0026quot;인데, long poll은 이 가정을 깨기 때문임.\n4. HTTP Responses 이건 단순함. 표준 HTTP 그대로 따르면 됨.\n서버가 요청을 성공적으로 받으면 200 OK로 응답 응답 시점: 이벤트 발생, 상태 변화, 또는 타임아웃 응답 body에 실제 이벤트/상태/타임아웃 정보 포함 특별한 건 없고, 그냥 HTTP 스펙 준수하면 됨.\n5. Timeouts 딜레마 Long poll 타임아웃 값을 정하는 건 까다로움:\n너무 높게 설정하면:\n서버에서 408 Request Timeout 받을 수 있음 프록시에서 504 Gateway Timeout 받을 수 있음 네트워크 연결이 끊어진 걸 늦게 감지함 너무 낮게 설정하면:\n불필요한 요청/응답이 증가함 네트워크 트래픽 낭비 서버 부하 증가 실험 결과와 권장값 브라우저 기본 타임아웃: 300초 (5분) 실험에서 성공한 값: 최대 120초 안전한 권장값: 30초 대부분의 네트워크 인프라(프록시, 로드밸런서 등)는 브라우저만큼 긴 타임아웃을 갖고 있지 않음. 중간에 있는 장비가 먼저 연결을 끊어버릴 수 있음.\n네트워크 장비 벤더를 위한 권장사항 Long polling 호환성을 원하면, 타임아웃을 30초보다 충분히 길게 설정해야 함. 여기서 \u0026ldquo;충분히\u0026quot;란 평균 네트워크 왕복 시간의 몇 배 이상을 의미함.\n6. Impact on Intermediary Entities 투명성 문제 Long poll 요청은 중간 장비(프록시, 게이트웨이 등) 입장에서 일반 HTTP 요청과 구분이 안 됨. \u0026ldquo;이건 long poll이니까 특별하게 처리해줘\u0026quot;라고 알려줄 방법이 없음.\n이로 인해 중간 장비가 불필요한 작업을 할 수 있음:\n캐싱 시도 (실시간 데이터인데 캐시하면 안 됨) 타임아웃 적용 (long poll은 원래 오래 걸림) 연결 재사용 시도 (long poll은 점유 시간이 김) 캐싱 방지 가장 중요한 건 캐싱 방지임. 실시간 데이터가 캐시되면 클라이언트가 과거 데이터를 받게 됨.\n반드시 설정해야 하는 헤더:\nCache-Control: no-cache 요청과 응답 모두에 이 헤더를 포함시켜야 함. 이건 표준 HTTP 헤더라서 대부분의 중간 장비가 이해하고 존중함.\n7. Security Considerations RFC 6202는 HTTP의 새로운 기능을 제안하는 게 아니라, 기존 사용 방식을 설명하는 문서임. 따라서 새로운 보안 취약점을 만들지는 않음. 하지만 이미 배포된 솔루션들에 존재하는 보안 이슈들이 있음.\n1. Injection 공격 (Cross-Domain Long Polling) 문제 상황:\n크로스 도메인 long polling에서 JSONP 방식을 쓸 때, 서버가 반환한 JavaScript를 브라우저가 실행함.\n// 서버 응답 (JSONP) callback({\u0026#34;price\u0026#34;: 52300}); 만약 서버가 injection 공격에 취약하면, 공격자가 악성 코드를 삽입할 수 있음:\n// 공격자가 조작한 응답 callback({\u0026#34;price\u0026#34;: 52300}); stealCookies(); 브라우저는 이걸 그대로 실행해버림.\n대응책:\n서버 측 입력 검증 철저히 CORS를 사용하고 JSONP 피하기 Content-Type 헤더 정확히 설정 2. DoS (Denial of Service) 공격 문제 상황:\nLong polling과 HTTP streaming은 많은 연결을 오래 유지해야 함. 공격자가 대량의 long poll 연결을 열어두면:\n공격자 → 연결 1,000개 열어둠 (각각 30초 대기) ↓ 서버 리소스 고갈 → 정상 사용자 서비스 불가 일반 HTTP 요청은 금방 끝나니까 연결당 리소스 점유 시간이 짧음. 하지만 long poll은 의도적으로 오래 유지하니까 DoS에 취약함.\n대응책:\nIP당 연결 수 제한 인증된 사용자만 long poll 허용 Rate limiting 적용 비동기 서버 사용 (연결당 리소스 최소화) 정리 RFC 6202 를 읽어보면서 과거의 서버 푸쉬 이벤트를 어떤식으로 만들었는지 알아보았음. 기존에 알고 있던 polling, streaming, SSE 메커니즘에 대한 지식과 문제점들을 좀 더 상세하게 알아볼 수 있어서 좋았음.\n이 문서를 읽어보며 정리된 생각은 새로운 프로토콜이 아닌 HTTP 프로토콜을 확장해서 쓰는 개념이고 HTTP 의 설계상 양방향 비동기 통신을 위한 프로토콜이 아니기 때문에 남용을 해서 server event push 의 목적을 달성한 느낌이었음.\n다음 포스팅 에서는 RFC 6455 Websocket 문서를 정리해보면서 RFC 6202 에서의 문제점과 그 발전과정을 연결지어 작성해보려 함.\n","date":"2026-01-12T21:18:15+09:00","image":"/posts/260112_before_websocket/featured.png","permalink":"/posts/260112_before_websocket/","title":"Websocket 이전의 양방향 통신"},{"content":"25년을 되돌아보며 24년 7월과 11월 궤양성 대장염이 악화되어 더 이상 치료할 수 있는 약이 없어 대장을 모두 절제하는 수술을 두 차례 받게 되었다. 회복할 시간이 필요했고, 25년 1월을 끝으로 첫 회사를 퇴사했다. 2년 2개월이라는 짧지 않은 시간 동안 많은 것을 배웠고 아쉬움도 많이 남았지만, 이렇게 마무리되었다.\n학생 때부터 알바와 일을 계속 해왔던 터라, 집에서 아무것도 하지 않고 휴식하는 것이 처음에는 어색하기도 하고 불안하기도 했다. 하지만 점차 적응해 나갔다. 수술의 여파가 커서 활동적인 것들은 할 수 없었지만 집에서 하고 싶었던 게임도 마음껏 하고 심적으로 힘들었던 부분도 많이 회복되었다.\n돌이켜보면 휴식 기간 동안에도 정보처리기사와 SQLD를 취득하고 사이드 프로젝트를 꾸준히 진행하며 프로그래밍 감각을 유지하려 노력했던 것 같다. (완전히 아무것도 하지 않고 쉬는 것은 안되는 것 같다\u0026hellip;)\n25년은 \u0026lsquo;퇴사\u0026rsquo;와 \u0026lsquo;회복\u0026rsquo;, 이 두 단어로 요약할 수 있는 한 해였다. 건강이 좋지 않아 힘든 시간이 많았지만, 잘 버텨낸 나 자신에게 칭찬을 해주고 싶다.\n26년 앞으로의 계획 새해를 맞이하며 몸도 많이 회복되었고, 다시 취업을 준비하고 있다. 관심 있는 분야는 금융권 또는 이커머스 도메인이며, 이쪽 분야에 맞춰 준비 중이다.\n앞으로의 일은 한 치 앞도 알 수 없지만, 꾸준히 준비하다 보면 좋은 기회가 올 것이라 믿는다.\n","date":"2026-01-01T23:03:28+09:00","image":"/posts/260101_plan/featured.jpg","permalink":"/posts/260101_plan/","title":"25년 간단한 회고와 26년 앞으로의 계획"},{"content":"스팀 게임에서 재미난 게임을 발견해서 소개를 해보고자 한다. 😂\n농부는 대체되었다 라는 게임인데 기본적으로 드론이 있고 이 드론을 파이썬 코드를 작성해서 제어하고 농작물 재배를 자동화 하는 게임이다. ㅋㅋㅋㅋㅋ 😂\n생각보다 파이썬 문법에 대한 설명도 잘 되어 있고 파이썬을 처음 써보시는 분들도 재미있게 공부하면서 연습하기에 나쁘지 않을 수 도 있을 것 같다 ?\n아직 한글 모드를 적용안해서 영어로 나오지만 한글 모드도 지원하는 것 같다..!\n게임을 해보면서 처음에 변수도 선언이 안되어서 뭐지 싶었는데 함수, 변수 등 모두 작물을 재배한 재화로 스킬을 찍어야 사용할 수 있는 그런 게임이다\u0026hellip;\n나름 중독성 있고 최종적으로 재배지를 늘리고 농작물을 심고 자라나는 속도에 맞추어 수확하는 최적화 움직임을 알고리즘으로 구현 하는 컨텐츠 인 것 같다.\n오늘 기준으로 스팀에서 할인도 20% 하고 있으니 관심 있는 분들(개발자 ?)은 한번쯤 해보시는 걸 추천\u0026hellip;.? 😄\n스팀 링크: 농부는 대체되었다\n","date":"2025-12-15T16:26:41+09:00","image":"/posts/251215_game/featured.png","permalink":"/posts/251215_game/","title":"코딩게임 추천 : 농부는 대체되었다"},{"content":"Giscus란? Giscus는 GitHub Discussions를 백엔드로 사용하는 오픈소스 댓글 시스템입니다.\n주요 특징 ✅ 완전 무료 (GitHub 기능 활용) ✅ 서버 불필요 (GitHub이 모든 것을 처리) ✅ Markdown 완벽 지원 (코드 블록, 이미지, 표 등) ✅ 반응(Reactions) (👍, ❤️, 😄 등) ✅ GitHub 알림 (댓글 달리면 알림 받음) ✅ 다크모드 (블로그 테마와 자동 동기화) ✅ 데이터 소유 (본인 저장소에 저장) Utterances와의 차이점 기능 Giscus Utterances 백엔드 GitHub Discussions GitHub Issues 반응 ✅ ❌ 대댓글 ✅ (중첩) ⚠️ (flat) 댓글 정렬 ✅ ⚠️ 적합성 댓글 전용 이슈 트래킹 결론: Giscus가 Utterances의 상위 호환입니다.\n사전 준비 필요한 것 GitHub 계정 Public GitHub 저장소 (블로그 저장소) Hugo + Blowfish 테마 제약사항 ⚠️ Public 저장소만 가능 (Private 저장소는 Discussions 기능 제한) ⚠️ GitHub 계정 필요 (익명 댓글 불가) 1단계: GitHub Discussions 활성화 1.1 저장소 설정 페이지 이동 GitHub에서 블로그 저장소 접속\n예: https://github.com/0AndWild/0AndWild.github.io Settings 탭 클릭\n1.2 Discussions 활성화 페이지를 아래로 스크롤하여 Features 섹션 찾기\nDiscussions 체크박스를 ✅ 체크\n자동으로 저장됨\n1.3 확인 저장소 상단에 Discussions 탭이 생성되었는지 확인\nCode | Issues | Pull requests | Discussions | ← 새로 생김! 2단계: Giscus App 설치 2.1 Giscus GitHub App 설치 https://github.com/apps/giscus 접속\nInstall 버튼 클릭\n권한 선택:\nAll repositories (모든 저장소) Only select repositories (특정 저장소만 - 권장) 블로그 저장소 선택:\n0AndWild/0AndWild.github.io Install 클릭\n2.2 권한 확인 Giscus가 요청하는 권한:\n✅ Read access to discussions (토론 읽기) ✅ Write access to discussions (토론 쓰기) ✅ Read access to metadata (메타데이터 읽기) 3단계: Giscus 설정 생성 3.1 Giscus 웹사이트 접속 https://giscus.app/ko 방문\n3.2 저장소 연결 저장소 섹션에 입력:\n0AndWild/0AndWild.github.io 아래에 성공 메시지가 표시되어야 함:\n✅ 성공! 이 저장소는 모든 조건을 만족합니다. 만약 오류가 뜨면:\nDiscussions 활성화 확인 Giscus App 설치 확인 저장소가 Public인지 확인 3.3 페이지 ↔️ Discussion 연결 방식 Discussion 카테고리 섹션에서 선택:\n권장: pathname (경로명) 매핑: pathname 선택 각 블로그 포스트의 경로가 Discussion 제목이 됩니다.\n예시:\n포스트: /posts/giscus-guide/ Discussion 제목: posts/giscus-guide 대안들: URL: 전체 URL 사용 (도메인 변경 시 문제) title: 포스트 제목 사용 (제목 변경 시 문제) og:title: OpenGraph 제목 specific term: 직접 지정 추천: pathname 사용\n3.4 Discussion 카테고리 선택 Discussion 카테고리 드롭다운에서 선택:\n권장: Announcements 카테고리: Announcements 선택 특징:\n관리자만 새 Discussion 생성 가능 댓글은 누구나 가능 블로그 포스트용으로 최적 대안: General 누구나 Discussion 생성 가능 더 개방적 추천: Announcements (블로그에 적합)\n3.5 기능 선택 반응 활성화 ✅ 반응 활성화 사용자가 👍, ❤️, 😄 등으로 반응 가능\n메타데이터 보내기 □ 메타데이터 보내기 (체크 해제 권장) 불필요한 기능, 꺼두는 것이 좋음\n댓글 입력란 위치 ⚪ 댓글 위에 ⚪ 댓글 아래 (권장) 권장: 댓글 아래\n기존 댓글을 먼저 읽고 작성하도록 유도 느긋한 로딩 ✅ 느긋한 로딩 페이지 로딩 속도 향상 (권장)\n3.6 테마 선택 권장: preferred_color_scheme 테마: preferred_color_scheme 동작:\n사용자의 시스템 설정에 따라 자동 전환 다크모드 ↔️ 라이트모드 자동 대안: light: 항상 밝은 테마 dark: 항상 어두운 테마 transparent_dark: 투명 다크 기타 GitHub 테마들 추천: preferred_color_scheme (자동 전환)\n3.7 언어 설정 언어: ko (한국어) 4단계: 생성된 코드 복사 4.1 스크립트 복사 페이지 하단에 Enable giscus 섹션에서 생성된 코드 복사:\n\u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; data-repo=\u0026#34;0AndWild/0AndWild.github.io\u0026#34; data-repo-id=\u0026#34;R_kgDOxxxxxxxx\u0026#34; data-category=\u0026#34;Announcements\u0026#34; data-category-id=\u0026#34;DIC_kwDOxxxxxxxx\u0026#34; data-mapping=\u0026#34;pathname\u0026#34; data-strict=\u0026#34;0\u0026#34; data-reactions-enabled=\u0026#34;1\u0026#34; data-emit-metadata=\u0026#34;0\u0026#34; data-input-position=\u0026#34;bottom\u0026#34; data-theme=\u0026#34;preferred_color_scheme\u0026#34; data-lang=\u0026#34;ko\u0026#34; data-loading=\u0026#34;lazy\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; async\u0026gt; \u0026lt;/script\u0026gt; 4.2 중요한 값들 data-repo-id: 저장소 고유 ID (자동 생성) data-category-id: 카테고리 고유 ID (자동 생성) 이 값들은 본인의 저장소마다 다르므로, 반드시 Giscus 웹사이트에서 생성된 코드를 사용해야 합니다.\n5단계: Blowfish 테마에 통합 5.1 디렉토리 생성 터미널에서 블로그 루트 디렉토리로 이동 후:\nmkdir -p layouts/partials 5.2 comments.html 파일 생성 touch layouts/partials/comments.html 또는 IDE/에디터에서 직접 생성:\nlayouts/ └── partials/ └── comments.html ← 새로 생성 5.3 Giscus 코드 삽입 layouts/partials/comments.html 파일에 다음 내용 추가:\n\u0026lt;!-- Giscus 댓글 시스템 --\u0026gt; \u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; data-repo=\u0026#34;0AndWild/0AndWild.github.io\u0026#34; data-repo-id=\u0026#34;R_kgDOxxxxxxxx\u0026#34; data-category=\u0026#34;Announcements\u0026#34; data-category-id=\u0026#34;DIC_kwDOxxxxxxxx\u0026#34; data-mapping=\u0026#34;pathname\u0026#34; data-strict=\u0026#34;0\u0026#34; data-reactions-enabled=\u0026#34;1\u0026#34; data-emit-metadata=\u0026#34;0\u0026#34; data-input-position=\u0026#34;bottom\u0026#34; data-theme=\u0026#34;preferred_color_scheme\u0026#34; data-lang=\u0026#34;ko\u0026#34; data-loading=\u0026#34;lazy\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; async\u0026gt; \u0026lt;/script\u0026gt; ⚠️ 주의: 위의 data-repo-id와 data-category-id 값을 본인의 값으로 교체해야 합니다!\n5.4 params.toml 설정 config/_default/params.toml 파일을 열고 [article] 섹션에 추가:\n[article] showComments = true # 이 줄 추가 또는 확인 # ... 기타 설정들 이미 showComments 항목이 있다면 true로 설정되어 있는지 확인하세요.\n6단계: 로컬 테스트 6.1 Hugo 서버 실행 hugo server -D 6.2 브라우저에서 확인 http://localhost:1313 포스트 페이지 하단에 Giscus 댓글 위젯이 표시되어야 합니다.\n6.3 테스트 댓글 작성 GitHub으로 로그인 버튼 클릭 GitHub OAuth 인증 테스트 댓글 작성 댓글이 표시되는지 확인 6.4 GitHub Discussions 확인 GitHub 저장소 → Discussions 탭 Announcements 카테고리에 새 Discussion 생성되었는지 확인 Discussion 제목이 포스트 경로인지 확인 7단계: 배포 7.1 Git에 커밋 git add layouts/partials/comments.html git add config/_default/params.toml git commit -m \u0026#34;Add Giscus comments system\u0026#34; 7.2 GitHub에 푸시 git push origin main 7.3 GitHub Actions 확인 GitHub Actions가 자동으로 빌드 및 배포를 진행합니다.\n배포 상태 확인:\nGitHub 저장소 → Actions 탭 7.4 배포된 사이트 확인 https://0andwild.github.io 포스트 페이지에 댓글 위젯이 정상적으로 표시되는지 확인하세요.\n고급 설정 다크모드 및 언어 동적 설정 (권장) Blowfish 테마의 다크모드 토글과 언어 전환에 따라 Giscus가 자동으로 변경되도록 설정하는 완전한 방법입니다.\n완전한 동적 설정 layouts/partials/comments.html 전체 코드:\n\u0026lt;!-- Giscus Comments with Dynamic Theme and Language --\u0026gt; {{ $lang := .Site.Language.Lang }} {{ $translationKey := .File.TranslationBaseName }} \u0026lt;script\u0026gt; (function() { // Get current theme (dark/light) function getGiscusTheme() { const isDark = document.documentElement.classList.contains(\u0026#39;dark\u0026#39;); return isDark ? \u0026#39;dark_tritanopia\u0026#39; : \u0026#39;light_tritanopia\u0026#39;; } // Get language from Hugo template const currentLang = \u0026#39;{{ $lang }}\u0026#39;; // Use file directory path for unified comments across languages // Example: \u0026#34;posts/subscription_alert\u0026#34; for both index.ko.md and index.en.md const discussionId = \u0026#39;{{ .File.Dir | replaceRE \u0026#34;^content/\u0026#34; \u0026#34;\u0026#34; | replaceRE \u0026#34;/$\u0026#34; \u0026#34;\u0026#34; }}\u0026#39;; // Wait for DOM to be ready if (document.readyState === \u0026#39;loading\u0026#39;) { document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, initGiscus); } else { initGiscus(); } function initGiscus() { // Create and insert Giscus script with dynamic settings const script = document.createElement(\u0026#39;script\u0026#39;); script.src = \u0026#39;https://giscus.app/client.js\u0026#39;; script.setAttribute(\u0026#39;data-repo\u0026#39;, \u0026#39;0AndWild/0AndWild.github.io\u0026#39;); script.setAttribute(\u0026#39;data-repo-id\u0026#39;, \u0026#39;R_kgDOQAqZFA\u0026#39;); script.setAttribute(\u0026#39;data-category\u0026#39;, \u0026#39;General\u0026#39;); script.setAttribute(\u0026#39;data-category-id\u0026#39;, \u0026#39;DIC_kwDOQAqZFM4CwwRg\u0026#39;); script.setAttribute(\u0026#39;data-mapping\u0026#39;, \u0026#39;specific\u0026#39;); script.setAttribute(\u0026#39;data-term\u0026#39;, discussionId); script.setAttribute(\u0026#39;data-strict\u0026#39;, \u0026#39;0\u0026#39;); script.setAttribute(\u0026#39;data-reactions-enabled\u0026#39;, \u0026#39;1\u0026#39;); script.setAttribute(\u0026#39;data-emit-metadata\u0026#39;, \u0026#39;0\u0026#39;); script.setAttribute(\u0026#39;data-input-position\u0026#39;, \u0026#39;bottom\u0026#39;); script.setAttribute(\u0026#39;data-theme\u0026#39;, getGiscusTheme()); script.setAttribute(\u0026#39;data-lang\u0026#39;, currentLang); script.setAttribute(\u0026#39;data-loading\u0026#39;, \u0026#39;lazy\u0026#39;); script.setAttribute(\u0026#39;crossorigin\u0026#39;, \u0026#39;anonymous\u0026#39;); script.async = true; // Find giscus container or create one const container = document.querySelector(\u0026#39;.giscus-container\u0026#39;) || document.currentScript?.parentElement; if (container) { container.appendChild(script); } } // Monitor theme changes and update Giscus function updateGiscusTheme() { const iframe = document.querySelector(\u0026#39;iframe.giscus-frame\u0026#39;); if (!iframe) return; const theme = getGiscusTheme(); try { iframe.contentWindow.postMessage( { giscus: { setConfig: { theme: theme } } }, \u0026#39;https://giscus.app\u0026#39; ); } catch (error) { console.log(\u0026#39;Giscus theme update delayed, will retry...\u0026#39;); } } // Watch for theme changes using MutationObserver const observer = new MutationObserver((mutations) =\u0026gt; { mutations.forEach((mutation) =\u0026gt; { if (mutation.attributeName === \u0026#39;class\u0026#39;) { // Delay update to ensure iframe is ready setTimeout(updateGiscusTheme, 100); } }); }); // Start observing after a short delay setTimeout(() =\u0026gt; { observer.observe(document.documentElement, { attributes: true, attributeFilter: [\u0026#39;class\u0026#39;] }); }, 500); // Update theme when Giscus iframe loads window.addEventListener(\u0026#39;message\u0026#39;, (event) =\u0026gt; { if (event.origin !== \u0026#39;https://giscus.app\u0026#39;) return; if (event.data.giscus) { // Giscus is ready, update theme setTimeout(updateGiscusTheme, 200); } }); })(); \u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; /* Ensure Giscus iframe has proper height and displays all content */ .giscus-container { min-height: 300px; } .giscus-container iframe.giscus-frame { width: 100%; border: none; min-height: 300px; } /* Make sure comment actions are visible */ .giscus { overflow: visible !important; } \u0026lt;/style\u0026gt; \u0026lt;div class=\u0026#34;giscus-container\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 동작 방식 설명 1. 언어 동적 설정 {{ $lang := .Site.Language.Lang }} const currentLang = \u0026#39;{{ $lang }}\u0026#39;; Hugo 템플릿에서 현재 페이지 언어 가져오기 한국어 페이지: ko, 영어 페이지: en Giscus에 해당 언어로 설정 결과:\n한국어 페이지 → Giscus UI가 한국어로 표시 영어 페이지 → Giscus UI가 영어로 표시 언어 전환 시 페이지 리로드되면서 자동으로 변경 2. 다크모드 동적 설정 function getGiscusTheme() { const isDark = document.documentElement.classList.contains(\u0026#39;dark\u0026#39;); return isDark ? \u0026#39;dark_tritanopia\u0026#39; : \u0026#39;light_tritanopia\u0026#39;; } Blowfish 테마는 다크모드 시 \u0026lt;html class=\u0026quot;dark\u0026quot;\u0026gt; 추가 이를 감지하여 테마 결정 dark_tritanopia / light_tritanopia 테마 사용 (색맹 친화적) 결과:\n페이지 로드 시: 현재 테마 상태로 Giscus 로드 다크모드 토글 클릭 시: 실시간으로 Giscus 테마 변경 3. 언어별 댓글 통합 const discussionId = \u0026#39;{{ .File.Dir | replaceRE \u0026#34;^content/\u0026#34; \u0026#34;\u0026#34; | replaceRE \u0026#34;/$\u0026#34; \u0026#34;\u0026#34; }}\u0026#39;; 파일 디렉토리 경로를 Discussion ID로 사용 content/posts/subscription_alert/index.ko.md → posts/subscription_alert content/posts/subscription_alert/index.en.md → posts/subscription_alert 같은 ID이므로 한국어/영어 버전이 같은 댓글 공유 결과:\n한국어 포스트에서 작성한 댓글 영어 포스트에서도 동일하게 표시 포스트별로는 별도 Discussion 생성 4. 실시간 테마 변경 감지 const observer = new MutationObserver((mutations) =\u0026gt; { mutations.forEach((mutation) =\u0026gt; { if (mutation.attributeName === \u0026#39;class\u0026#39;) { setTimeout(updateGiscusTheme, 100); } }); }); MutationObserver로 HTML 클래스 변경 감지 다크모드 토글 클릭 시 즉시 감지 postMessage로 Giscus iframe에 테마 변경 명령 전송 테스트 방법 # 1. 로컬 서버 실행 hugo server -D # 2. 브라우저에서 확인 http://localhost:1313/posts/subscription_alert/ 테스트 항목:\n✅ 페이지 로드 시 현재 테마(라이트/다크)로 Giscus 표시 ✅ 다크모드 토글 클릭 시 Giscus 테마 즉시 변경 ✅ 언어 전환 (ko → en) 시 Giscus 언어 변경 ✅ 한국어/영어 페이지에서 같은 댓글 표시 테마 옵션 변경 다른 테마를 사용하려면 getGiscusTheme() 함수 수정:\n// 기본 테마 function getGiscusTheme() { const isDark = document.documentElement.classList.contains(\u0026#39;dark\u0026#39;); return isDark ? \u0026#39;dark\u0026#39; : \u0026#39;light\u0026#39;; } // 고대비 테마 function getGiscusTheme() { const isDark = document.documentElement.classList.contains(\u0026#39;dark\u0026#39;); return isDark ? \u0026#39;dark_high_contrast\u0026#39; : \u0026#39;light_high_contrast\u0026#39;; } // GitHub 스타일 테마 function getGiscusTheme() { const isDark = document.documentElement.classList.contains(\u0026#39;dark\u0026#39;); return isDark ? \u0026#39;dark_dimmed\u0026#39; : \u0026#39;light\u0026#39;; } 사용 가능한 테마:\nlight / dark light_high_contrast / dark_high_contrast light_tritanopia / dark_tritanopia (색맹 친화적) dark_dimmed transparent_dark preferred_color_scheme (시스템 설정 따름) 정적 테마 설정 (간단한 방법) 동적 변경이 필요 없다면 정적으로 설정 가능:\n\u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; data-repo=\u0026#34;0AndWild/0AndWild.github.io\u0026#34; data-repo-id=\u0026#34;R_kgDOxxxxxxxx\u0026#34; data-category=\u0026#34;General\u0026#34; data-category-id=\u0026#34;DIC_kwDOxxxxxxxx\u0026#34; data-mapping=\u0026#34;pathname\u0026#34; data-theme=\u0026#34;preferred_color_scheme\u0026#34; data-lang=\u0026#34;ko\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; async\u0026gt; \u0026lt;/script\u0026gt; 장점: 간단함 단점: 실시간 테마 변경 불가, 언어별 댓글 분리됨\n포스트별 댓글 숨기기 특정 포스트에서만 댓글을 숨기려면, 해당 포스트의 front matter에:\n--- title: \u0026#34;댓글 없는 포스트\u0026#34; showComments: false # 이 포스트만 댓글 숨김 --- 카테고리별 댓글 분리 다른 카테고리의 포스트에 다른 Discussion 카테고리를 사용하려면:\n\u0026lt;!-- 조건부 카테고리 설정 --\u0026gt; \u0026lt;script\u0026gt; const category = {{ if in .Params.categories \u0026#34;Tutorial\u0026#34; }} \u0026#34;DIC_kwDOxxxxTutorial\u0026#34; {{ else }} \u0026#34;DIC_kwDOxxxxGeneral\u0026#34; {{ end }}; \u0026lt;/script\u0026gt; \u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; ... data-category-id=\u0026#34;{{ category }}\u0026#34; ...\u0026gt; \u0026lt;/script\u0026gt; 문제 해결 댓글 위젯이 표시되지 않음 원인 1: Discussions 미활성화 해결: GitHub 저장소 → Settings → Discussions 체크 원인 2: Giscus App 미설치 해결: https://github.com/apps/giscus 에서 Install 원인 3: 저장소 ID 오류 해결: giscus.app에서 코드 재생성 원인 4: showComments 설정 누락 # config/_default/params.toml [article] showComments = true # 확인 로그인 버튼만 보이고 댓글 못 씀 원인: GitHub OAuth 승인 필요 1. \u0026#34;GitHub으로 로그인\u0026#34; 클릭 2. OAuth 권한 승인 3. 저장소로 리다이렉트 4. 댓글 작성 가능 댓글이 저장되지 않음 원인: 저장소 권한 문제 확인 사항: 1. 저장소가 Public인지 2. Giscus App 권한에 저장소 포함되어 있는지 3. Discussion 카테고리가 존재하는지 다크모드가 동기화 안 됨 해결: JavaScript 동기화 코드 추가 위의 \u0026ldquo;고급 설정 \u0026gt; 다크모드 자동 전환\u0026rdquo; 참고\nGiscus 관리 댓글 관리 GitHub Discussions에서 관리 1. GitHub 저장소 → Discussions 탭 2. 해당 Discussion 클릭 3. 관리 작업: - 댓글 수정 (본인 댓글만) - 댓글 삭제 (관리자) - 사용자 차단 (관리자) - Discussion 잠금 (관리자) 스팸 댓글 처리 1. GitHub Discussions에서 스팸 댓글 찾기 2. 댓글 옆 ... 메뉴 → \u0026#34;Delete\u0026#34; 3. 사용자 차단: 프로필 → Block user 알림 설정 GitHub 알림으로 댓글 알림 받기 1. GitHub → Settings → Notifications 2. Watching에 저장소 추가 3. 이메일로 알림 받기 설정 특정 Discussion만 알림 받기 1. Discussions 탭 → 해당 Discussion 2. 오른쪽 \u0026#34;Subscribe\u0026#34; 버튼 3. \u0026#34;Notify me\u0026#34; 선택 통계 및 분석 댓글 통계 보기 GitHub Discussions에서:\n1. Discussions 탭 2. 카테고리별 Discussion 수 확인 3. 각 Discussion의 댓글 수 확인 GitHub Insights 활용 GitHub 저장소 → Insights → Community → Discussions 활동 확인 비용 및 제한사항 비용 완전 무료\nGitHub 계정만 있으면 사용 가능 저장소 크기 제한 내에서 무제한 댓글 제한사항 GitHub API Rate Limit 시간당 60회 (미인증) 시간당 5,000회 (인증) Giscus는 캐싱으로 최적화되어 있어 문제 없음 저장소 크기 GitHub Free: 저장소당 1GB 텍스트 댓글만으로는 제한 도달 불가능 Discussions 제한 없음 (무제한) 대안 비교 Giscus vs Utterances 항목 Giscus Utterances 백엔드 Discussions Issues 반응 ✅ ❌ 대댓글 중첩 지원 Flat 추천 ⭐⭐⭐⭐⭐ ⭐⭐⭐ 결론: Giscus 사용 권장\nGiscus vs Disqus 항목 Giscus Disqus 비용 무료 무료 (광고) 광고 ❌ ✅ 익명 댓글 ❌ ✅ (Guest) Markdown ✅ ⚠️ 데이터 소유 ✅ ❌ 추천 개발자 블로그 일반 블로그 마이그레이션 가이드 Utterances → Giscus 1. GitHub Issues를 Discussions로 변환 - 수동 작업 필요 (자동화 없음) - 또는 Issues 그대로 두고 Giscus 새로 시작 2. comments.html 파일 교체 - Utterances 코드 삭제 - Giscus 코드 추가 3. 배포 Disqus → Giscus 1. Disqus 데이터 Export (XML) 2. GitHub Discussions로 수동 이전 - 자동화 도구 없음 - 스크립트 직접 작성 필요 - 또는 새로 시작 권장 추가 리소스 공식 문서 Giscus 공식 사이트 Giscus GitHub 커뮤니티 Giscus Discussions Blowfish 문서 체크리스트 설치 완료 확인:\nGitHub Discussions 활성화 Giscus App 설치 layouts/partials/comments.html 생성 Giscus 코드 삽입 (본인의 ID로) params.toml에 showComments = true 로컬 테스트 완료 GitHub에 푸시 배포된 사이트에서 확인 테스트 댓글 작성 GitHub Discussions에 생성 확인 결론 Giscus는 Hugo/GitHub Pages 블로그에 가장 적합한 댓글 시스템입니다:\n장점 정리 ✅ 완전 무료 ✅ 설정 간단 (10분) ✅ 서버 불필요 ✅ Markdown 완벽 지원 ✅ GitHub 통합 ✅ 데이터 소유\n단점 ❌ GitHub 계정 필수 (익명 불가) ❌ 기술 블로그에 적합 (일반 사용자는 허들 있음)\n추천 대상 ✅ 개발자 블로그 ✅ 기술 문서 ✅ 오픈소스 프로젝트 ","date":"2025-10-17T12:00:00+09:00","image":"/posts/251017_comments_giscus/featured.png","permalink":"/posts/251017_comments_giscus/","title":"Giscus로 Hugo 블로그에 댓글 기능 추가하기"},{"content":"개요 정적 사이트 생성기(Hugo)로 만든 블로그에 댓글 기능을 추가하는 모든 방법을 비교 분석합니다. 익명 댓글, GitHub 로그인, 소셜 로그인 등 다양한 요구사항에 맞는 솔루션을 제시합니다.\n댓글 시스템 분류 인증 방식에 따른 분류 인증 방식 시스템 GitHub 전용 Giscus, Utterances 익명 가능 Remark42, Commento, Comentario, HashOver 익명 + 소셜 로그인 Remark42, Commento, Disqus 소셜 로그인만 Disqus, Hyvor Talk 호스팅 방식에 따른 분류 호스팅 시스템 SaaS (관리 불필요) Giscus, Utterances, Disqus, Hyvor Talk 셀프 호스팅 Remark42, Commento, Comentario, HashOver 하이브리드 Cusdis (Vercel 무료 배포) 1. Giscus (최고 추천 - GitHub 사용자용) 개념 GitHub Discussions를 백엔드로 사용하는 댓글 시스템\n동작 방식 1. 사용자가 블로그 방문 ↓ 2. Giscus 위젯 로드 ↓ 3. GitHub OAuth로 로그인 ↓ 4. 댓글 작성 ↓ 5. GitHub Discussions에 자동 저장 ↓ 6. 블로그에 실시간 표시 장점 ✅ 완전 무료 (GitHub 기능 활용) ✅ 서버 불필요 (GitHub이 백엔드) ✅ 데이터 소유 (본인 저장소에 저장) ✅ Markdown 지원 (코드 블록, 이미지 등) ✅ 반응(Reactions) 지원 (👍, ❤️ 등) ✅ 알림 (GitHub 알림으로 댓글 알림) ✅ 다크 모드 (블로그 테마와 동기화) ✅ 스팸 방지 (GitHub 계정 필요) ✅ 관리 간편 (GitHub Discussions에서 관리) ✅ 검색 가능 (GitHub 검색으로 댓글 검색) 단점 ❌ 익명 댓글 불가 (GitHub 계정 필수) ❌ 기술 블로그에 적합 (일반 사용자는 GitHub 계정 없을 수 있음) ❌ GitHub 의존성 (GitHub 장애 시 댓글 불가) 구현 난이도 ⭐⭐ (2/5)\n설정 방법 1단계: GitHub Discussions 활성화 1. GitHub 저장소 → Settings 2. Features 섹션 → Discussions 체크 2단계: Giscus 설정 giscus.app 방문 저장소 입력: username/repository 설정 선택: 페이지 ↔️ Discussion 연결: pathname (권장) Discussion 카테고리: Announcements 또는 General 기능: 반응, 댓글 위로 테마: 블로그에 맞게 선택 3단계: Blowfish에 추가 \u0026lt;!-- layouts/partials/comments.html --\u0026gt; \u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; data-repo=\u0026#34;0AndWild/0AndWild.github.io\u0026#34; data-repo-id=\u0026#34;YOUR_REPO_ID\u0026#34; data-category=\u0026#34;Announcements\u0026#34; data-category-id=\u0026#34;YOUR_CATEGORY_ID\u0026#34; data-mapping=\u0026#34;pathname\u0026#34; data-strict=\u0026#34;0\u0026#34; data-reactions-enabled=\u0026#34;1\u0026#34; data-emit-metadata=\u0026#34;0\u0026#34; data-input-position=\u0026#34;bottom\u0026#34; data-theme=\u0026#34;preferred_color_scheme\u0026#34; data-lang=\u0026#34;ko\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; async\u0026gt; \u0026lt;/script\u0026gt; 4단계: params.toml 설정 [article] showComments = true 테마 동기화 (다크모드) \u0026lt;script\u0026gt; // 블로그 테마 변경 시 Giscus 테마도 변경 const giscusTheme = document.querySelector(\u0026#39;iframe.giscus-frame\u0026#39;); if (giscusTheme) { const theme = document.documentElement.getAttribute(\u0026#39;data-theme\u0026#39;); giscusTheme.contentWindow.postMessage({ giscus: { setConfig: { theme: theme === \u0026#39;dark\u0026#39; ? \u0026#39;dark\u0026#39; : \u0026#39;light\u0026#39; } } }, \u0026#39;https://giscus.app\u0026#39;); } \u0026lt;/script\u0026gt; 비용 완전 무료\n추천 대상 ✅ 개발자 블로그 ✅ 기술 문서 ✅ 오픈소스 프로젝트 블로그 2. Utterances 개념 GitHub Issues를 백엔드로 사용하는 댓글 시스템 (Giscus의 전신)\n동작 방식 1. GitHub OAuth 로그인 ↓ 2. 댓글 작성 ↓ 3. GitHub Issues에 저장 (각 포스트 = 1개 Issue) ↓ 4. 블로그에 표시 장점 ✅ 완전 무료 ✅ 가벼움 (TypeScript) ✅ 간단한 설정 ✅ Markdown 지원 단점 ❌ Issues 사용 (Discussions보다 덜 적합) ❌ Giscus보다 기능 적음 ❌ 익명 불가 Giscus vs Utterances 기능 Giscus Utterances 백엔드 Discussions Issues 반응 ✅ ❌ 댓글에 댓글 ✅ (nested) ⚠️ (flat) 적합성 댓글 전용 이슈 트래킹용 결론: Giscus가 Utterances의 상위 호환\n구현 난이도 ⭐⭐ (2/5)\n설정 방법 \u0026lt;!-- layouts/partials/comments.html --\u0026gt; \u0026lt;script src=\u0026#34;https://utteranc.es/client.js\u0026#34; repo=\u0026#34;username/repository\u0026#34; issue-term=\u0026#34;pathname\u0026#34; theme=\u0026#34;github-light\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; async\u0026gt; \u0026lt;/script\u0026gt; 추천 대상 특별한 이유가 없다면 Giscus 사용 권장 3. Remark42 (최고 추천 - 익명 + 소셜 로그인) 개념 오픈소스 셀프 호스팅 댓글 시스템으로, 익명 및 다양한 소셜 로그인 지원\n동작 방식 1. Remark42 서버 배포 (Docker) ↓ 2. 블로그에 Remark42 스크립트 삽입 ↓ 3. 사용자 선택: - 익명 댓글 작성 - GitHub/Google/Twitter 로그인 후 작성 ↓ 4. Remark42 DB에 저장 ↓ 5. 블로그에 표시 장점 ✅ 익명 댓글 가능 (설정으로 켜고 끌 수 있음) ✅ 다양한 소셜 로그인 (GitHub, Google, Facebook, Twitter, Email) ✅ 완전 무료 (오픈소스) ✅ 광고 없음 ✅ 데이터 소유 (본인 서버) ✅ Markdown 지원 ✅ 댓글 수정/삭제 ✅ 관리자 모드 (댓글 승인/차단/삭제) ✅ 알림 (이메일/Telegram) ✅ Import/Export (다른 시스템에서 마이그레이션) ✅ 투표 (찬성/반대) ✅ 스팸 필터 단점 ❌ 셀프 호스팅 필요 (Docker 서버) ❌ 유지보수 책임 ❌ 호스팅 비용 (월 $5~, 무료 티어 가능) 구현 난이도 ⭐⭐⭐⭐ (4/5)\n호스팅 옵션 옵션 1: Railway (추천) 1. Railway.app 회원가입 2. \u0026#34;New Project\u0026#34; → \u0026#34;Deploy from GitHub\u0026#34; 3. Remark42 Docker 이미지 선택 4. 환경변수 설정: - REMARK_URL=https://your-remark42.railway.app - SECRET=your-random-secret - AUTH_ANON=true # 익명 댓글 허용 - AUTH_GITHUB_CID=your_client_id - AUTH_GITHUB_CSEC=your_client_secret Railway 무료 티어:\n월 $5 크레딧 소규모 블로그 충분 옵션 2: Fly.io # fly.toml app = \u0026#34;my-remark42\u0026#34; [build] image = \u0026#34;umputun/remark42:latest\u0026#34; [env] REMARK_URL = \u0026#34;https://my-remark42.fly.dev\u0026#34; AUTH_ANON = \u0026#34;true\u0026#34; AUTH_GITHUB_CID = \u0026#34;xxx\u0026#34; AUTH_GITHUB_CSEC = \u0026#34;xxx\u0026#34; fly launch fly deploy Fly.io 무료 티어:\n3개 앱 소규모 블로그 충분 옵션 3: Docker Compose (VPS) # docker-compose.yml version: \u0026#39;3.8\u0026#39; services: remark42: image: umputun/remark42:latest restart: always environment: - REMARK_URL=https://remark.your-blog.com - SECRET=your-secret-key-change-this - AUTH_ANON=true # 익명 허용 - AUTH_GITHUB_CID=xxx # GitHub 로그인 - AUTH_GITHUB_CSEC=xxx - AUTH_GOOGLE_CID=xxx # Google 로그인 - AUTH_GOOGLE_CSEC=xxx - ADMIN_SHARED_ID=github_username # 관리자 volumes: - ./data:/srv/var ports: - \u0026#34;8080:8080\u0026#34; docker-compose up -d 블로그 삽입 코드 \u0026lt;!-- layouts/partials/comments.html --\u0026gt; \u0026lt;div id=\u0026#34;remark42\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; var remark_config = { host: \u0026#39;https://your-remark42.railway.app\u0026#39;, site_id: \u0026#39;0andwild-blog\u0026#39;, components: [\u0026#39;embed\u0026#39;], theme: \u0026#39;light\u0026#39;, locale: \u0026#39;ko\u0026#39;, max_shown_comments: 10, simple_view: false, no_footer: false }; (function(c) { for(var i = 0; i \u0026lt; c.length; i++){ var d = document, s = d.createElement(\u0026#39;script\u0026#39;); s.src = remark_config.host + \u0026#39;/web/\u0026#39; +c[i] +\u0026#39;.js\u0026#39;; s.defer = true; (d.head || d.body).appendChild(s); } })(remark_config.components || [\u0026#39;embed\u0026#39;]); \u0026lt;/script\u0026gt; 익명 + GitHub 동시 사용 설정 # 환경변수 AUTH_ANON=true # 익명 허용 AUTH_GITHUB_CID=xxx # GitHub OAuth App ID AUTH_GITHUB_CSEC=xxx # GitHub OAuth App Secret ANON_VOTE=false # 익명 사용자 투표 불가 (스팸 방지) 사용자는 선택 가능:\n\u0026ldquo;익명으로 댓글 달기\u0026rdquo; \u0026ldquo;GitHub으로 로그인\u0026rdquo; 관리자 기능 # 관리자 지정 ADMIN_SHARED_ID=github_yourusername # 또는 이메일 ADMIN_SHARED_EMAIL=you@example.com 관리자 가능 작업:\n댓글 삭제 사용자 차단 댓글 고정 읽기 전용 모드 비용 Railway: 무료 또는 월 $5 Fly.io: 무료 티어 가능 VPS (DigitalOcean 등): 월 $5~ 추천 대상 ✅ 익명 + 소셜 로그인 모두 원하는 경우 ✅ 기술적으로 Docker 다룰 수 있는 사용자 ✅ 데이터 완전 통제 원하는 경우 4. Commento / Comentario 개념 프라이버시 중심의 경량 댓글 시스템\nCommento vs Comentario 항목 Commento Comentario 상태 개발 중단 활발히 개발 중 (Commento 포크) 라이선스 MIT MIT 언어 Go Go 추천 ❌ ✅ 결론: Comentario 사용 권장\nComentario 장점 ✅ 익명 댓글 가능 ✅ 소셜 로그인 (GitHub, Google, GitLab, SSO) ✅ 가벼움 (Go 기반) ✅ 프라이버시 중심 ✅ Markdown 지원 ✅ 투표 기능 단점 ❌ 셀프 호스팅 필요 ❌ Remark42보다 기능 적음 구현 난이도 ⭐⭐⭐⭐ (4/5)\nDocker 배포 version: \u0026#39;3.8\u0026#39; services: comentario: image: registry.gitlab.com/comentario/comentario ports: - \u0026#34;8080:8080\u0026#34; environment: - COMENTARIO_ORIGIN=https://comments.your-blog.com - COMENTARIO_BIND=0.0.0.0:8080 - COMENTARIO_POSTGRES=postgres://user:pass@db/comentario depends_on: - db db: image: postgres:15 environment: - POSTGRES_DB=comentario - POSTGRES_USER=comentario - POSTGRES_PASSWORD=change-this volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data: 블로그 삽입 \u0026lt;script defer src=\u0026#34;https://comments.your-blog.com/js/commento.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;div id=\u0026#34;commento\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 추천 대상 Remark42 대안 더 간단한 시스템 원하는 경우 5. Disqus (전통적 SaaS) 개념 가장 오래되고 널리 사용되는 클라우드 댓글 시스템\n동작 방식 1. Disqus 계정 생성 및 사이트 등록 ↓ 2. 블로그에 Disqus 스크립트 삽입 ↓ 3. 사용자 선택: - Guest (익명 - 이메일 필요) - Disqus 계정 - Facebook/Twitter/Google 로그인 ↓ 4. Disqus 서버에 저장 ↓ 5. 블로그에 표시 장점 ✅ 설정 초간단 (5분) ✅ 서버 불필요 (SaaS) ✅ Guest 모드 (이메일만으로 댓글) ✅ 소셜 로그인 (Facebook, Twitter, Google) ✅ 강력한 관리자 도구 ✅ 스팸 필터 (Akismet 통합) ✅ 모바일 앱 (iOS/Android) ✅ 분석/통계 단점 ❌ 광고 표시 (무료 플랜) ❌ 무거움 (스크립트 크기) ❌ 프라이버시 우려 (데이터 추적) ❌ 데이터 소유권 없음 (Disqus 서버) ❌ GitHub 로그인 없음 ❌ 광고 제거 비용 (월 $11.99~) 구현 난이도 ⭐ (1/5) - 가장 쉬움\n설정 방법 1단계: Disqus 사이트 등록 1. disqus.com 가입 2. \u0026#34;I want to install Disqus on my site\u0026#34; 선택 3. Website Name 입력 (예: andwild-blog) 4. Category 선택 5. Plan 선택 (Basic - Free) 2단계: Blowfish 설정 # config/_default/config.toml [services.disqus] shortname = \u0026#34;andwild-blog\u0026#34; # 1단계에서 생성한 이름 # config/_default/params.toml [article] showComments = true Hugo는 Disqus를 기본 지원하므로 자동으로 댓글 표시됨!\n3단계: Guest 댓글 허용 Disqus Dashboard → Settings → Community → Guest Commenting: Allow guests to comment (체크) 광고 제거 방법 방법 1: 유료 플랜 ($11.99/월~) Plus Plan: 광고 없음 Pro Plan: 광고 없음 + 고급 기능 방법 2: CSS로 숨기기 (비추천 - 약관 위반 가능) /* 비추천: Disqus 약관 위반 가능 */ #disqus_thread iframe[src*=\u0026#34;ads\u0026#34;] { display: none !important; } 비용 무료: 광고 있음 Plus: $11.99/월 (광고 없음) Pro: $89/월 (고급 기능) 추천 대상 ✅ 빠르게 댓글 추가하고 싶은 경우 ✅ 비기술적 블로거 ✅ 광고 신경 안 쓰는 경우 ❌ 프라이버시 중시하는 경우는 비추천 6. Cusdis (Vercel 무료 배포) 개념 경량 오픈소스 댓글 시스템, Vercel에 무료 배포 가능\n동작 방식 1. Cusdis를 Vercel에 배포 (1-Click) ↓ 2. PostgreSQL 연결 (Vercel 무료) ↓ 3. 대시보드에서 사이트 추가 ↓ 4. 블로그에 스크립트 삽입 ↓ 5. 사용자가 이메일 + 이름으로 댓글 장점 ✅ 완전 무료 (Vercel 무료 티어) ✅ 익명 댓글 (이메일 + 이름만) ✅ 가벼움 (50KB) ✅ 설정 간단 (Vercel 1-Click 배포) ✅ 프라이버시 중심 ✅ 오픈소스 단점 ❌ Markdown 미지원 ❌ 소셜 로그인 없음 ❌ 기능 단순 구현 난이도 ⭐⭐⭐ (3/5)\n설정 방법 1단계: Vercel 배포 1. https://cusdis.com/ 방문 2. \u0026#34;Deploy with Vercel\u0026#34; 클릭 3. GitHub 연결 4. PostgreSQL 추가 (Vercel Storage) 5. 배포 완료 2단계: 사이트 추가 1. 배포된 Cusdis 대시보드 접속 2. \u0026#34;Add Website\u0026#34; 클릭 3. Domain 입력: 0andwild.github.io 4. App ID 복사 3단계: 블로그 삽입 \u0026lt;!-- layouts/partials/comments.html --\u0026gt; \u0026lt;div id=\u0026#34;cusdis_thread\u0026#34; data-host=\u0026#34;https://your-cusdis.vercel.app\u0026#34; data-app-id=\u0026#34;YOUR_APP_ID\u0026#34; data-page-id=\u0026#34;{{ .File.UniqueID }}\u0026#34; data-page-url=\u0026#34;{{ .Permalink }}\u0026#34; data-page-title=\u0026#34;{{ .Title }}\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script async defer src=\u0026#34;https://your-cusdis.vercel.app/js/cusdis.es.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 비용 완전 무료 (Vercel 무료 티어)\n추천 대상 ✅ 간단한 익명 댓글만 필요한 경우 ✅ 완전 무료 원하는 경우 ✅ Vercel 사용 경험 있는 경우 7. HashOver 개념 PHP 기반의 완전 익명 댓글 시스템\n장점 ✅ 완전 익명 (아무 정보도 필요 없음) ✅ PHP + flat file (DB 불필요) ✅ 오픈소스 단점 ❌ PHP 필요 (정적 사이트에 부적합) ❌ GitHub 로그인 없음 ❌ 오래된 프로젝트 구현 난이도 ⭐⭐⭐⭐ (4/5)\n추천 대상 ❌ 정적 블로그에는 비추천 PHP 서버 있을 때만 고려 8. Hyvor Talk (프리미엄 SaaS) 개념 광고 없는 프리미엄 댓글 시스템\n장점 ✅ 광고 없음 ✅ 익명 댓글 가능 ✅ 소셜 로그인 ✅ 강력한 스팸 필터 단점 ❌ 유료 (월 $5~) ❌ GitHub 로그인 없음 비용 Starter: $5/월 (1 사이트) Pro: $15/월 (3 사이트) 추천 대상 Disqus 유료 대안 광고 없는 SaaS 원하는 경우 비교표 인증 방식별 시스템 익명 GitHub Google 기타 소셜 난이도 비용 Giscus ❌ ✅ ❌ ❌ ⭐⭐ 무료 Utterances ❌ ✅ ❌ ❌ ⭐⭐ 무료 Remark42 ✅ ✅ ✅ ✅ ⭐⭐⭐⭐ $5/월 Comentario ✅ ✅ ✅ ✅ ⭐⭐⭐⭐ $5/월 Disqus ⚠️ ❌ ✅ ✅ ⭐ 무료 (광고) Cusdis ✅ ❌ ❌ ❌ ⭐⭐⭐ 무료 Hyvor Talk ✅ ❌ ✅ ✅ ⭐ $5/월 기능별 시스템 Markdown 반응 투표 알림 관리자 스팸필터 Giscus ✅ ✅ ❌ ✅ ⚠️ ✅ Remark42 ✅ ❌ ✅ ✅ ✅ ✅ Disqus ⚠️ ❌ ✅ ✅ ✅ ✅ Cusdis ❌ ❌ ❌ ⚠️ ✅ ⚠️ 호스팅별 시스템 호스팅 데이터 위치 의존성 Giscus GitHub GitHub Discussions GitHub Remark42 셀프 본인 서버 Docker Disqus Disqus Disqus 서버 Disqus Cusdis Vercel Vercel DB Vercel 선택 가이드 시나리오별 추천 1. \u0026ldquo;개발자 블로그, GitHub 사용자 대상\u0026rdquo; → Giscus ⭐⭐⭐⭐⭐\n무료, 간단, Markdown 지원 GitHub 통합으로 알림도 편함 2. \u0026ldquo;일반 블로그, 익명 댓글 필수\u0026rdquo; → Cusdis (간단) 또는 Remark42 (고급)\nCusdis: 5분 설정, 완전 무료 Remark42: 더 많은 기능, 소셜 로그인 포함 3. \u0026ldquo;익명 + GitHub 로그인 둘 다\u0026rdquo; → Remark42 ⭐⭐⭐⭐⭐\n유일하게 둘 다 지원 관리자 기능 강력 4. \u0026ldquo;기술 없음, 빠르게 설정\u0026rdquo; → Disqus\n5분 설정 광고는 감수 5. \u0026ldquo;완전 무료 + 서버 관리 싫음\u0026rdquo; → Giscus (GitHub) 또는 Cusdis (익명)\n6. \u0026ldquo;프라이버시 최우선\u0026rdquo; → Remark42 또는 Comentario (셀프 호스팅)\n데이터 완전 통제 실전 구현: Blowfish + Giscus 전체 설정 과정 1. GitHub Discussions 활성화 GitHub 저장소 → Settings → Features → Discussions 체크 2. Giscus App 설치 https://github.com/apps/giscus 방문 → Install → 저장소 선택 3. Giscus 설정 생성 giscus.app/ko에서:\n저장소: 0AndWild/0AndWild.github.io 매핑: pathname 카테고리: Announcements 테마: preferred_color_scheme 언어: ko 생성된 코드 복사\n4. 파일 생성 # 디렉토리 생성 (없으면) mkdir -p layouts/partials # 파일 생성 touch layouts/partials/comments.html 5. 코드 삽입 \u0026lt;!-- layouts/partials/comments.html --\u0026gt; \u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; data-repo=\u0026#34;0AndWild/0AndWild.github.io\u0026#34; data-repo-id=\u0026#34;R_xxxxxxxxxxxxx\u0026#34; data-category=\u0026#34;Announcements\u0026#34; data-category-id=\u0026#34;DIC_xxxxxxxxxxxxx\u0026#34; data-mapping=\u0026#34;pathname\u0026#34; data-strict=\u0026#34;0\u0026#34; data-reactions-enabled=\u0026#34;1\u0026#34; data-emit-metadata=\u0026#34;0\u0026#34; data-input-position=\u0026#34;bottom\u0026#34; data-theme=\u0026#34;preferred_color_scheme\u0026#34; data-lang=\u0026#34;ko\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; async\u0026gt; \u0026lt;/script\u0026gt; 6. params.toml 수정 [article] showComments = true 7. 로컬 테스트 hugo server -D # http://localhost:1313 에서 확인 8. 배포 git add . git commit -m \u0026#34;Add Giscus comments\u0026#34; git push 실전 구현: Blowfish + Remark42 (Railway) 전체 설정 과정 1. GitHub OAuth App 생성 GitHub → Settings → Developer settings → OAuth Apps → New OAuth App Application name: AndWild Blog Comments Homepage URL: https://0andwild.github.io Authorization callback URL: https://your-remark42.railway.app/auth/github/callback 생성 후: Client ID 복사 Client Secret 생성 및 복사 2. Railway 배포 1. railway.app 가입 2. \u0026#34;New Project\u0026#34; → \u0026#34;Deploy Docker Image\u0026#34; 3. Image: umputun/remark42:latest 4. 환경변수 추가: REMARK_URL=https://your-project.railway.app SECRET=randomly-generated-secret-key-change-this SITE=0andwild-blog AUTH_ANON=true AUTH_GITHUB_CID=your_github_client_id AUTH_GITHUB_CSEC=your_github_client_secret ADMIN_SHARED_ID=github_yourusername 3. 배포 확인 Railway가 자동으로 URL 생성: https://your-project.railway.app 브라우저에서 접속하여 Remark42 UI 확인 4. Blowfish 설정 mkdir -p layouts/partials touch layouts/partials/comments.html \u0026lt;!-- layouts/partials/comments.html --\u0026gt; \u0026lt;div id=\u0026#34;remark42\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; var remark_config = { host: \u0026#39;https://your-project.railway.app\u0026#39;, site_id: \u0026#39;0andwild-blog\u0026#39;, components: [\u0026#39;embed\u0026#39;], theme: \u0026#39;light\u0026#39;, locale: \u0026#39;ko\u0026#39; }; (function(c) { for(var i = 0; i \u0026lt; c.length; i++){ var d = document, s = d.createElement(\u0026#39;script\u0026#39;); s.src = remark_config.host + \u0026#39;/web/\u0026#39; +c[i] +\u0026#39;.js\u0026#39;; s.defer = true; (d.head || d.body).appendChild(s); } })(remark_config.components || [\u0026#39;embed\u0026#39;]); \u0026lt;/script\u0026gt; 5. params.toml [article] showComments = true 6. 테스트 및 배포 hugo server -D # 확인 후 git add . git commit -m \u0026#34;Add Remark42 comments\u0026#34; git push 마이그레이션 가이드 Disqus → Giscus 1. Disqus에서 데이터 Export (XML) 2. GitHub Discussions로 수동 이전 (자동화 스크립트 없음, 수동 작업 필요) Disqus → Remark42 1. Disqus XML Export 2. Remark42 Admin → Import → Disqus 선택 3. XML 파일 업로드 결론 최종 추천 상황 추천 시스템 이유 개발자 블로그 Giscus 무료, GitHub 통합, Markdown 일반 블로그 (익명 필요) Cusdis 무료, 간단, 익명 익명 + 소셜 둘 다 Remark42 유연함, 모든 기능 빠른 설정 Disqus 5분 완료 (광고 감수) 완전 통제 Remark42 셀프 호스팅, 커스터마이징 개인 추천 (0AndWild 블로그) Giscus 사용 권장\nGitHub Pages 블로그에 완벽히 어울림 기술 블로그는 GitHub 사용자가 주 독자 무료, 간단, 유지보수 없음 대안: Remark42 (익명 댓글 원할 때)\n빠른시작 Giscus로 시작 (10분) 사용자 피드백 수집 익명 댓글 요청 많으면 Remark42로 전환 고려 댓글 시스템은 나중에도 바꿀 수 있으니, 일단 Giscus로 시작하는 것을 강력히 권장합니다!\n","date":"2025-10-17T11:00:00+09:00","image":"/posts/251017_comments_guide/featured.png","permalink":"/posts/251017_comments_guide/","title":"Hugo \u0026 GithubPages 블로그 댓글 시스템 구현 가이드"},{"content":"개요 정적 사이트 생성기(Hugo)로 만든 블로그에 구독 및 이메일 알림 기능을 추가하는 방법을 분석합니다. 특히 키워드 기반 선택적 알림 기능 구현까지 다룹니다.\n1. RSS Feed + 이메일 서비스 개념 Hugo의 기본 RSS Feed를 이메일로 변환하는 서비스를 활용하는 방식입니다.\n방법 A: Blogtrottr 동작 방식 1. Hugo가 자동 생성한 RSS Feed (index.xml) ↓ 2. 사용자가 Blogtrottr에 RSS URL 등록 ↓ 3. Blogtrottr가 주기적으로 RSS 확인 ↓ 4. 새 글 감지 시 이메일 발송 장점 ✅ 개발자 작업 없음 (링크만 제공) ✅ 완전 무료 ✅ 즉시 사용 가능 ✅ 서버 없이 동작 단점 ❌ 구독자 관리 불가 ❌ 이메일 디자인 커스텀 불가 ❌ 통계 없음 ❌ 키워드 필터링 불가 ❌ 사용자가 직접 외부 사이트에서 등록해야 함 구현 난이도 ⭐ (1/5) - 가장 쉬움\n사용 예시 블로그에 링크 추가: [이메일로 구독하기](https://blogtrottr.com) (사이트에서 https://0andwild.github.io/index.xml 입력) 방법 B: FeedBurner (Google) 동작 방식 1. FeedBurner에 RSS Feed 등록 ↓ 2. FeedBurner가 RSS를 프록시/관리 ↓ 3. 구독 폼을 블로그에 삽입 ↓ 4. 사용자가 블로그에서 직접 구독 ↓ 5. 새 글 발행 시 자동 이메일 발송 장점 ✅ 기본 통계 제공 ✅ 구독 폼 제공 ✅ 무료 ✅ RSS 관리 기능 단점 ❌ Google의 지원 중단 가능성 (업데이트 중단됨) ❌ 키워드 필터링 불가 ❌ 커스텀 제한적 ❌ 오래된 UI 구현 난이도 ⭐⭐ (2/5)\n2. Mailchimp + RSS Campaign (추천) 개념 전문 이메일 마케팅 플랫폼을 활용하여 RSS Feed를 자동으로 이메일로 변환\n동작 방식 1. Mailchimp에 RSS Campaign 생성 ↓ 2. RSS URL 등록 및 체크 주기 설정 (일/주/월) ↓ 3. 블로그에 Mailchimp 구독 폼 삽입 ↓ 4. 사용자가 이메일 입력하여 구독 ↓ 5. 새 글 감지 시 자동으로 이메일 템플릿 생성 ↓ 6. 전체 구독자에게 발송 장점 ✅ 무료 티어: 2,000명 구독자까지 ✅ 전문적인 이메일 디자인 (드래그 앤 드롭 에디터) ✅ 구독자 관리 (추가/삭제/세그먼트) ✅ 상세한 통계 (오픈율, 클릭율, 구독 해지율) ✅ 구독 폼 자동 생성 (임베드 코드 제공) ✅ 자동화 (새 글만 발송) ✅ 모바일 최적화 ✅ 스팸 필터 회피 (전문 발송 서버) 단점 ❌ 키워드 필터링 기본 미지원 (Pro 플랜에서 태그별 세그먼트 가능) ❌ 무료 티어에서 Mailchimp 로고 표시 ❌ 2,000명 초과 시 유료 ($13/월~) 구현 난이도 ⭐⭐ (2/5)\n설정 단계 1. Mailchimp 계정 생성 2. Audience 생성 3. Campaign → Create → Email → RSS Campaign 4. RSS URL 입력: https://your-blog.com/index.xml 5. 발송 주기 설정 (Daily/Weekly) 6. 이메일 템플릿 디자인 7. 구독 폼 코드 복사 8. Hugo에 삽입 (layouts/partials/subscribe.html) 블로그 삽입 코드 예시 \u0026lt;!-- Mailchimp 구독 폼 --\u0026gt; \u0026lt;div id=\u0026#34;mc_embed_signup\u0026#34;\u0026gt; \u0026lt;form action=\u0026#34;https://your-mailchimp-url.com/subscribe\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;EMAIL\u0026#34; placeholder=\u0026#34;이메일 주소\u0026#34; required\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;구독하기\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/div\u0026gt; 3. Buttondown (개발자 친화적, 추천) 개념 Markdown 기반의 뉴스레터 플랫폼으로, API를 통한 커스터마이징이 가능\n동작 방식 1. Buttondown에 RSS Feed 연동 ↓ 2. 자동으로 RSS 항목을 Markdown 이메일로 변환 ↓ 3. 구독자가 태그/키워드 선택 가능 ↓ 4. API를 통해 특정 태그 구독자만 필터링 가능 ↓ 5. 매칭되는 구독자에게만 발송 장점 ✅ 무료 티어: 1,000명까지 ✅ Markdown 기반 (개발자 친화적) ✅ 강력한 API (커스텀 가능) ✅ 태그 기반 구독 (키워드 필터링 구현 가능) ✅ 광고 없음 ✅ 깔끔한 UI ✅ RSS import 자동화 ✅ 프라이버시 중심 단점 ❌ 이메일 디자인이 단순 (Markdown만) ❌ 통계 기능이 Mailchimp보다 약함 ❌ 한국어 지원 부족 구현 난이도 ⭐⭐⭐ (3/5) - API 사용 시 난이도 증가\n키워드 알림 구현 예시 1단계: 구독 폼에 태그 선택 추가 \u0026lt;form action=\u0026#34;https://buttondown.email/api/emails/embed-subscribe/YOUR_ID\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; placeholder=\u0026#34;이메일\u0026#34; required\u0026gt; \u0026lt;label\u0026gt;관심 주제 선택:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;tags\u0026#34; value=\u0026#34;kubernetes\u0026#34;\u0026gt; Kubernetes \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;tags\u0026#34; value=\u0026#34;docker\u0026#34;\u0026gt; Docker \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;tags\u0026#34; value=\u0026#34;golang\u0026#34;\u0026gt; Go \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;구독하기\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; 2단계: GitHub Actions로 선택적 발송 name: Send Newsletter on: push: paths: - \u0026#39;content/posts/**\u0026#39; jobs: send: runs-on: ubuntu-latest steps: - name: Extract tags from post run: | TAGS=$(grep \u0026#34;^tags = \u0026#34; content/posts/*/index.md | cut -d\u0026#39;\u0026#34;\u0026#39; -f2) echo \u0026#34;POST_TAGS=$TAGS\u0026#34; \u0026gt;\u0026gt; $GITHUB_ENV - name: Send to matching subscribers run: | curl -X POST https://api.buttondown.email/v1/emails \\ -H \u0026#34;Authorization: Token ${{ secrets.BUTTONDOWN_API_KEY }}\u0026#34; \\ -d \u0026#34;subject=New Post\u0026#34; \\ -d \u0026#34;body=...\u0026#34; \\ -d \u0026#34;tag=$POST_TAGS\u0026#34; 4. SendGrid + GitHub Actions (완전 커스텀) 개념 이메일 발송 API와 CI/CD를 결합하여 완전히 커스터마이징된 알림 시스템 구축\n동작 방식 1. 새 글 작성 후 Git Push ↓ 2. GitHub Actions 트리거 ↓ 3. Action에서 Front Matter 파싱 - 글 제목, 요약, 태그 추출 ↓ 4. 구독자 DB 조회 (Supabase/JSON 파일) - 각 구독자의 관심 키워드와 매칭 ↓ 5. 매칭되는 구독자만 필터링 ↓ 6. SendGrid API로 개별 이메일 발송 장점 ✅ 완전한 통제 (모든 로직 커스터마이징) ✅ 키워드 알림 완벽 구현 ✅ 무료 티어: SendGrid 월 100통 ✅ 자동화 (Git push만 하면 됨) ✅ 확장 가능 (DB, 로직 자유롭게) ✅ 구독자 데이터 소유 단점 ❌ 개발 작업 필요 ❌ 유지보수 부담 ❌ SendGrid 무료 티어 제한적 (월 100통) ❌ 구독 폼, DB 직접 구현 필요 ❌ 스팸 필터 회피 설정 필요 구현 난이도 ⭐⭐⭐⭐⭐ (5/5) - 가장 복잡\n아키텍처 구독자 데이터베이스 옵션 옵션 A: JSON 파일 (간단)\n// subscribers.json (GitHub 저장소에 암호화하여 저장) [ { \u0026#34;email\u0026#34;: \u0026#34;user@example.com\u0026#34;, \u0026#34;keywords\u0026#34;: [\u0026#34;kubernetes\u0026#34;, \u0026#34;docker\u0026#34;], \u0026#34;active\u0026#34;: true }, { \u0026#34;email\u0026#34;: \u0026#34;dev@example.com\u0026#34;, \u0026#34;keywords\u0026#34;: [\u0026#34;golang\u0026#34;, \u0026#34;rust\u0026#34;], \u0026#34;active\u0026#34;: true } ] 옵션 B: Supabase (권장)\n-- subscribers 테이블 CREATE TABLE subscribers ( id UUID PRIMARY KEY, email TEXT UNIQUE NOT NULL, keywords TEXT[], -- 배열 형태 active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW() ); GitHub Actions 워크플로우 name: Email Notification on: push: branches: [main] paths: - \u0026#39;content/posts/**\u0026#39; jobs: notify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: \u0026#39;18\u0026#39; - name: Extract Post Metadata id: metadata run: | # 가장 최근 수정된 포스트 찾기 POST_FILE=$(git diff-tree --no-commit-id --name-only -r ${{ github.sha }} | grep \u0026#39;content/posts\u0026#39; | head -1) # Front Matter 파싱 TITLE=$(grep \u0026#34;^title = \u0026#34; $POST_FILE | cut -d\u0026#39;\u0026#34;\u0026#39; -f2) TAGS=$(grep \u0026#34;^tags = \u0026#34; $POST_FILE | sed \u0026#39;s/tags = \\[//;s/\\]//;s/\u0026#34;//g\u0026#39;) SUMMARY=$(grep \u0026#34;^summary = \u0026#34; $POST_FILE | cut -d\u0026#39;\u0026#34;\u0026#39; -f2) URL=\u0026#34;https://0andwild.github.io/$(dirname $POST_FILE | sed \u0026#39;s/content\\///\u0026#39;)\u0026#34; echo \u0026#34;title=$TITLE\u0026#34; \u0026gt;\u0026gt; $GITHUB_OUTPUT echo \u0026#34;tags=$TAGS\u0026#34; \u0026gt;\u0026gt; $GITHUB_OUTPUT echo \u0026#34;summary=$SUMMARY\u0026#34; \u0026gt;\u0026gt; $GITHUB_OUTPUT echo \u0026#34;url=$URL\u0026#34; \u0026gt;\u0026gt; $GITHUB_OUTPUT - name: Query Matching Subscribers id: subscribers run: | # Supabase에서 매칭되는 구독자 조회 curl -X POST https://YOUR_PROJECT.supabase.co/rest/v1/rpc/get_matching_subscribers \\ -H \u0026#34;apikey: ${{ secrets.SUPABASE_KEY }}\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#34;{\\\u0026#34;post_tags\\\u0026#34;: \\\u0026#34;${{ steps.metadata.outputs.tags }}\\\u0026#34;}\u0026#34; \\ \u0026gt; subscribers.json - name: Send Emails via SendGrid run: | # Node.js 스크립트 실행 cat \u0026gt; send-emails.js \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; const sgMail = require(\u0026#39;@sendgrid/mail\u0026#39;); const fs = require(\u0026#39;fs\u0026#39;); sgMail.setApiKey(process.env.SENDGRID_API_KEY); const subscribers = JSON.parse(fs.readFileSync(\u0026#39;subscribers.json\u0026#39;)); const title = process.env.POST_TITLE; const summary = process.env.POST_SUMMARY; const url = process.env.POST_URL; subscribers.forEach(async (subscriber) =\u0026gt; { const msg = { to: subscriber.email, from: \u0026#39;noreply@0andwild.github.io\u0026#39;, subject: `새 글: ${title}`, html: ` \u0026lt;h2\u0026gt;${title}\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;${summary}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;관심 키워드와 일치: ${subscriber.matched_keywords.join(\u0026#39;, \u0026#39;)}\u0026lt;/p\u0026gt; \u0026lt;a href=\u0026#34;${url}\u0026#34;\u0026gt;글 읽기\u0026lt;/a\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;small\u0026gt;\u0026lt;a href=\u0026#34;https://0andwild.github.io/unsubscribe?token=${subscriber.token}\u0026#34;\u0026gt;구독 취소\u0026lt;/a\u0026gt;\u0026lt;/small\u0026gt; ` }; await sgMail.send(msg); console.log(`Email sent to ${subscriber.email}`); }); EOF npm install @sendgrid/mail node send-emails.js env: SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }} POST_TITLE: ${{ steps.metadata.outputs.title }} POST_SUMMARY: ${{ steps.metadata.outputs.summary }} POST_URL: ${{ steps.metadata.outputs.url }} 구독 폼 구현 (Hugo Shortcode) \u0026lt;!-- layouts/shortcodes/subscribe.html --\u0026gt; \u0026lt;div class=\u0026#34;subscription-form\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;블로그 구독하기\u0026lt;/h3\u0026gt; \u0026lt;form id=\u0026#34;subscribe-form\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;email\u0026#34; id=\u0026#34;email\u0026#34; placeholder=\u0026#34;이메일 주소\u0026#34; required\u0026gt; \u0026lt;fieldset\u0026gt; \u0026lt;legend\u0026gt;관심 주제 선택 (선택한 주제의 글만 알림)\u0026lt;/legend\u0026gt; \u0026lt;label\u0026gt;\u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;keywords\u0026#34; value=\u0026#34;kubernetes\u0026#34;\u0026gt; Kubernetes\u0026lt;/label\u0026gt; \u0026lt;label\u0026gt;\u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;keywords\u0026#34; value=\u0026#34;docker\u0026#34;\u0026gt; Docker\u0026lt;/label\u0026gt; \u0026lt;label\u0026gt;\u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;keywords\u0026#34; value=\u0026#34;golang\u0026#34;\u0026gt; Go\u0026lt;/label\u0026gt; \u0026lt;label\u0026gt;\u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;keywords\u0026#34; value=\u0026#34;rust\u0026#34;\u0026gt; Rust\u0026lt;/label\u0026gt; \u0026lt;label\u0026gt;\u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;keywords\u0026#34; value=\u0026#34;devops\u0026#34;\u0026gt; DevOps\u0026lt;/label\u0026gt; \u0026lt;/fieldset\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;구독하기\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;script\u0026gt; document.getElementById(\u0026#39;subscribe-form\u0026#39;).addEventListener(\u0026#39;submit\u0026#39;, async (e) =\u0026gt; { e.preventDefault(); const email = document.getElementById(\u0026#39;email\u0026#39;).value; const keywords = Array.from(document.querySelectorAll(\u0026#39;input[name=\u0026#34;keywords\u0026#34;]:checked\u0026#39;)) .map(cb =\u0026gt; cb.value); // Supabase에 저장 const response = await fetch(\u0026#39;https://YOUR_PROJECT.supabase.co/rest/v1/subscribers\u0026#39;, { method: \u0026#39;POST\u0026#39;, headers: { \u0026#39;apikey\u0026#39;: \u0026#39;YOUR_ANON_KEY\u0026#39;, \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39; }, body: JSON.stringify({ email, keywords, active: true }) }); if (response.ok) { alert(\u0026#39;구독이 완료되었습니다!\u0026#39;); } else { alert(\u0026#39;오류가 발생했습니다.\u0026#39;); } }); \u0026lt;/script\u0026gt; \u0026lt;/div\u0026gt; Supabase 함수 (키워드 매칭) -- 매칭되는 구독자를 찾는 함수 CREATE OR REPLACE FUNCTION get_matching_subscribers(post_tags TEXT) RETURNS TABLE(email TEXT, matched_keywords TEXT[], token TEXT) AS $$ BEGIN RETURN QUERY SELECT s.email, ARRAY( SELECT unnest(s.keywords) INTERSECT SELECT unnest(string_to_array(post_tags, \u0026#39;,\u0026#39;)) ) as matched_keywords, s.unsubscribe_token as token FROM subscribers s WHERE s.active = true AND s.keywords \u0026amp;\u0026amp; string_to_array(post_tags, \u0026#39;,\u0026#39;) -- 배열 겹침 연산자 ; END; $$ LANGUAGE plpgsql; 비용 분석 SendGrid: 월 100통 무료 (이후 $19.95/월) Supabase: 월 500MB DB, 2GB 전송 무료 GitHub Actions: 월 2,000분 무료 총 비용: 완전 무료 (소규모 블로그) 5. 완전 커스텀 (Supabase + GitHub Actions + Resend) SendGrid 대안: Resend SendGrid보다 개발자 친화적인 최신 이메일 API\n장점 ✅ 무료 티어: 월 3,000통 (SendGrid의 30배!) ✅ 더 간단한 API ✅ React Email 지원 (JSX로 이메일 작성) ✅ 더 나은 개발자 경험 Resend 사용 예시 import { Resend } from \u0026#39;resend\u0026#39;; const resend = new Resend(process.env.RESEND_API_KEY); await resend.emails.send({ from: \u0026#39;blog@0andwild.github.io\u0026#39;, to: subscriber.email, subject: `새 글: ${title}`, html: `\u0026lt;p\u0026gt;${summary}\u0026lt;/p\u0026gt;\u0026lt;a href=\u0026#34;${url}\u0026#34;\u0026gt;읽기\u0026lt;/a\u0026gt;` }); 비교표 방법 무료 한도 키워드 알림 난이도 구독자 관리 커스텀 추천 Blogtrottr 무제한 ❌ ⭐ ❌ ❌ 테스트용 FeedBurner 무제한 ❌ ⭐⭐ ⚠️ ⚠️ 비추천 (지원 중단) Mailchimp 2,000명 ⚠️ (Pro) ⭐⭐ ✅ ⚠️ 일반 구독용 Buttondown 1,000명 ✅ ⭐⭐⭐ ✅ ✅ 개발자용 SendGrid + Actions 100통/월 ✅ ⭐⭐⭐⭐⭐ ✅ ✅✅ 고급 사용자 Resend + Actions 3,000통/월 ✅ ⭐⭐⭐⭐⭐ ✅ ✅✅ 완벽한 통제 추천 로드맵 단계 1: 빠른 시작 (즉시) Mailchimp RSS Campaign\n10분 설정 전체 구독자에게 모든 글 알림 단계 2: 개선 (1주 후) Buttondown으로 마이그레이션\n더 깔끔한 경험 기본 태그 기능 단계 3: 고급 기능 (필요 시) Resend + GitHub Actions + Supabase\n키워드 기반 선택적 알림 완전한 통제 확장 가능성 결론 일반 블로거라면: → Mailchimp (가장 쉽고 전문적)\n개발자 블로그라면: → Buttondown (개발자 친화적, API 제공)\n키워드 알림이 필수라면: → Resend + GitHub Actions + Supabase (완전 커스텀)\n돈 안 쓰고 테스트하려면: → Blogtrottr (30초 설정)\n빠른시작 실제 구현을 원하신다면:\nMailchimp로 시작 (학습 곡선 낮음) 트래픽 증가 시 Buttondown 고려 고급 기능 필요 시 커스텀 솔루션 구축 키워드 알림은 초기엔 과한 기능일 수 있으니, 기본 구독부터 시작하는 것을 권장합니다.\n","date":"2025-10-17T10:00:00+09:00","image":"/posts/251017_subscription_alert/featured.jpg","permalink":"/posts/251017_subscription_alert/","title":"Hugo \u0026 GithubPages 블로그 구독 및 이메일 알림 시스템 구현 가이드"},{"content":" 제목 (H2) 소제목 (H3) 일반 텍스트입니다. 굵게, 기울임, 취소선\n이미지 삽입 방법 1: 로컬 이미지 포스트 폴더 내에 이미지 파일을 넣고 사용:\n![이미지 설명](image.jpg) 방법 2: 외부 이미지 URL ![이미지 설명](https://example.com/image.jpg) 방법 3: HTML 태그 (크기 조정 가능) \u0026lt;img src=\u0026#34;image.jpg\u0026#34; alt=\u0026#34;이미지 설명\u0026#34; width=\u0026#34;500\u0026#34; /\u0026gt; 캐러셀 이미지 (슬라이드 효과) 16:9 21:9 코드 삽입 인라인 코드 inline code 형식으로 작성\n코드 블록 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello, World!\u0026#34;) } def hello(): print(\u0026#34;Hello, World!\u0026#34;) docker run -d -p 8080:80 nginx 링크 기본 링크 링크 텍스트\n참조 스타일 링크 링크 텍스트\narticle 참조 /docs/welcome/Open linked article 리스트 순서 없는 리스트 항목 1 항목 2 하위 항목 2-1 하위 항목 2-2 항목 3 순서 있는 리스트 첫 번째 두 번째 세 번째 체크리스트 할 일 1 완료된 일 할 일 2 인용문 인용문 내용입니다. 여러 줄도 가능합니다.\n표 (Table) 항목 설명 비고 A 설명 A 비고 A B 설명 B 비고 B 링크 임베드 (Shortcodes) YouTube 영상 {{\u0026lt; youtube VIDEO_ID \u0026gt;}}\nTwitter/X {{\u0026lt; twitter user=\u0026ldquo;username\u0026rdquo; id=\u0026ldquo;tweet_id\u0026rdquo; \u0026gt;}}\nGitHub Gist {{\u0026lt; gist username gist_id \u0026gt;}}\n알림 박스 (Blowfish Alert) {{\u0026lt; alert \u0026ldquo;circle-info\u0026rdquo; \u0026gt;}} 정보 알림입니다. {{\u0026lt; /alert \u0026gt;}}\n{{\u0026lt; alert \u0026ldquo;lightbulb\u0026rdquo; \u0026gt;}} 팁이나 아이디어입니다. {{\u0026lt; /alert \u0026gt;}}\n{{\u0026lt; alert \u0026ldquo;triangle-exclamation\u0026rdquo; \u0026gt;}} 경고 메시지입니다. {{\u0026lt; /alert \u0026gt;}}\n접기/펼치기 (Details) 클릭하여 펼치기 숨겨진 내용이 여기에 표시됩니다.\n주석 수평선 위아래로 구분선을 만들 때 사용:\n각주 텍스트에 각주1를 추가할 수 있습니다.\n그래프 차트 Mermaid 차트 graph LR; A[Lemons]--\u003eB[Lemonade]; B--\u003eC[Profit] Swatched (color showcase) #64748b #3b82f6 #06b6d4 TypeLt (Ex1)\n\u0026lt;p id=\"stack-typeit-6\" class=\"stack-typeit stack-typeit-cursor\"\u003e\u0026lt;/p\u003e (Ex2)\n\u0026lt;h1 id=\"stack-typeit-7\" class=\"stack-typeit stack-typeit-cursor\"\u003e\u0026lt;/h1\u003e (Ex3)\n\u0026lt;h3 id=\"stack-typeit-8\" class=\"stack-typeit stack-typeit-cursor\"\u003e\u0026lt;/h3\u003e Youtube Lite 작성 팁:\nFront matter의 draft: true를 false로 변경하면 배포됩니다 description과 summary를 작성하면 SEO에 도움이 됩니다 이미지는 포스트 폴더에 함께 넣는 것을 권장합니다 각주 내용입니다.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-10-16T18:36:52+09:00","image":"/posts/251016_blowfish_markdown/featured.png","permalink":"/posts/251016_blowfish_markdown/","title":"Hugo markdown 설명서"},{"content":"왜 Hugo \u0026amp; GitHub Pages로 넘어왔는가? 기존 Tstory로 운영하였던 기술 블로그를 Hugo \u0026amp; GitHub Pages로 마이그레이션을 하기로 결심하게 되었다.\n1. 흩어진 콘텐츠 관리의 어려움 여러가지 노트 툴을 사용하다보니 회사를 다니면서 또는 공부를 하면서 정리하는 글들이 중구난방하게 흩어지게 되었고 이걸 또 다시 블로그로 옮겨야 하는 번거로움이 생겨 블로그 관리를 소홀히 하게 되었다.\n2. 마크다운 호환성 문제 기존 노트 툴에서 사용하는 마크다운 문법이 Tstory에서 글을 올릴때 완벽히 호환되지 않아 수정을 해야하는 일들이 자주 발생했었고 이 또한 번거로움이 생기는 일이였다.\n특히 다음과 같은 문제들이 있었다:\n코드 블록의 syntax highlighting 지원 부족 테이블 렌더링 오류 이미지 경로 처리 문제 수식 표현의 제한 3. Tstory Open API 지원 종료 마지막으로는 최근에 다시 그동안 공부한 자료들을 다시 정리해서 Tstory에 올릴겸 블로그 스킨도 다시 이쁘게 꾸미고 Tstory 공식 Open API를 활용하여 기존 노트 툴들과 연동하는 작업을 진행해보려 하였지만 Open API의 공식 지원이 종료 되어있었고 더이상 Tstory를 사용할 이유가 없어졌다.\n블로그 플랫폼 선택 기준 여러 블로그를 참고하여 어떤 방식이 좋을지 고민을 많이 하였고 아래와 같이 기준점을 가지고 Hugo \u0026amp; GitHub Pages로 확정을 하게 되었다.\n블로그를 구축하는게 쉬운가? 코드로 관리가 가능한가? 내가 원하는 기능을 추가하는 자유도가 높은가? GitHub Pages를 사용해서 빌드하고 배포할때 속도가 빠른가? Obsidian과 같은 노트 툴들과 연동하기 편한가? Hugo란? Hugo는 Go 언어로 작성된 빠르고 유연한 정적 사이트 생성기(Static Site Generator)이다.\n주요 특징:\n빠른 빌드 속도: 수천 개의 페이지도 몇 초 안에 빌드된다 단순한 구조: Markdown으로 콘텐츠를 작성하면 Hugo가 HTML로 변환해준다 제로 의존성: 단일 바이너리로 실행되며 별도의 런타임이나 데이터베이스가 필요없다 풍부한 테마 생태계: 다양한 용도의 테마를 쉽게 적용할 수 있다 GitHub Pages와 함께 사용되는 정적 사이트 생성기 비교 특징 Hugo Jekyll Gatsby Next.js (SSG) VuePress 언어 Go Ruby React (JavaScript) React (JavaScript) Vue.js 빌드 속도 ⚡ 매우 빠름 (\u0026lt; 1ms/page) 🐢 느림 🚶 보통 🚶 보통 🚶 보통 설치 복잡도 ✅ 단일 바이너리 ⚠️ Ruby 환경 필요 ⚠️ Node.js + 많은 의존성 ⚠️ Node.js + 의존성 ⚠️ Node.js + 의존성 GitHub Pages 기본 지원 ❌ (Actions 필요) ✅ 네이티브 지원 ❌ (Actions 필요) ❌ (Actions 필요) ❌ (Actions 필요) 학습 곡선 낮음 낮음 높음 중간-높음 중간 테마/플러그인 풍부 매우 풍부 풍부 (React 생태계) 풍부 (React 생태계) 보통 적합한 용도 블로그, 문서, 포트폴리오 블로그, GitHub 기본 복잡한 웹앱, 블로그 복잡한 웹앱, 하이브리드 기술 문서 빌드 시간 (1000 페이지) ~1초 ~2분 ~30초 ~30초 ~20초 Hugo를 선택한 이유:\n압도적인 빌드 속도: 콘텐츠가 많아져도 빌드 시간이 거의 증가하지 않는다 간단한 설정: 복잡한 JavaScript 프레임워크 없이 Markdown에 집중할 수 있다 제로 의존성: 단일 실행 파일로 환경 설정 문제가 없다 풍부한 테마: Blowfish 같은 고품질 테마를 쉽게 적용할 수 있다 GitHub Pages 배포 Hugo로 작성한 블로그는 GitHub Actions를 통해 자동으로 빌드되고 배포된다.\n배포 워크플로우 main 브랜치에 변경사항을 push한다 GitHub Actions가 자동으로 트리거된다 Hugo가 정적 사이트를 빌드한다 빌드된 파일이 GitHub Pages로 자동 배포된다 장점 자동화된 배포: 코드를 push하면 자동으로 배포가 진행된다 버전 관리: Git을 통해 모든 변경사항을 추적할 수 있다 무료 호스팅: GitHub Pages는 무료로 제공된다 커스텀 도메인: 원하는 도메인을 연결할 수 있다 HTTPS 지원: 기본적으로 HTTPS가 제공된다 Obsidian 연동 Hugo는 마크다운 기반이기 때문에 Obsidian과 같은 노트 툴과 완벽하게 호환된다.\n연동 방법 Hugo 블로그의 content/posts 디렉토리를 Obsidian vault로 설정한다 Obsidian에서 글을 작성하고 편집한다 작성이 완료되면 Git을 통해 commit \u0026amp; push한다 GitHub Actions가 자동으로 빌드하고 배포한다 이점 일관된 작성 환경: 모든 노트와 블로그 글을 같은 툴에서 관리한다 완벽한 마크다운 호환: 추가 변환 작업이 필요없다 로컬 우선: 인터넷 없이도 글을 작성할 수 있다 강력한 링크 기능: Obsidian의 백링크와 그래프 뷰를 활용할 수 있다 기본적인 Hugo Terminal 명령어 개발 서버 실행 hugo server 로컬 개발 서버를 시작한다. 기본적으로 http://localhost:1313에서 사이트를 확인할 수 있다.\n주요 옵션:\n-D 또는 --buildDrafts: 초안(draft) 콘텐츠도 함께 빌드한다 --bind 0.0.0.0: 모든 네트워크 인터페이스에서 접근 가능하도록 설정한다 --port 8080: 기본 포트(1313) 대신 다른 포트를 사용한다 파일 변경 시 자동으로 브라우저가 새로고침된다 (Live Reload) 예시:\nhugo server -D hugo server --bind 0.0.0.0 --port 8080 프로덕션 빌드 hugo --cleanDestinationDir 프로덕션용 정적 사이트를 빌드한다. public/ 디렉토리에 결과물이 생성된다.\n주요 기능:\n--cleanDestinationDir: 빌드 전에 대상 디렉토리(public/)를 완전히 정리한다 이전 빌드의 불필요한 파일을 제거하여 깨끗한 상태로 빌드를 수행한다 파일명이 변경되거나 삭제된 경우에도 이전 버전의 파일이 남아있지 않도록 보장한다 예시:\nhugo --cleanDestinationDir hugo --cleanDestinationDir --minify # 파일 최소화 옵션 추가 테마 정보 Hugo Blowfish Theme 이 블로그는 Blowfish 테마를 사용하고 있다.\n특징:\n현대적이고 반응형 디자인을 제공한다 다크 모드를 지원한다 빠른 로딩 속도와 SEO 최적화가 되어있다 다국어를 지원한다 풍부한 커스터마이징 옵션을 제공한다 설정 파일:\nconfig/_default/hugo.toml - 기본 Hugo 설정 config/_default/params.toml - Blowfish 테마 파라미터 config/_default/languages.en.toml - 언어별 설정 config/_default/menus.en.toml - 메뉴 구성 마치며 Tstory에서 Hugo \u0026amp; GitHub Pages로의 마이그레이션은 개발자 친화적인 환경을 위한 선택이었다. 이제는 코드 버전 관리와 동일한 방식으로 블로그를 관리할 수 있게 되었고, Obsidian과의 완벽한 연동으로 노트 작성부터 블로그 포스팅까지 하나의 워크플로우로 통합할 수 있게 되었다.\n무엇보다 Hugo의 빠른 빌드 속도와 GitHub Actions의 자동화된 배포는 글 작성에만 집중할 수 있게 해주었고, 더 이상 플랫폼의 제약에 얽매이지 않고 자유롭게 커스터마이징할 수 있게 되었다.\n앞으로는 Tstory에 있던 기존 글들을 천천히 마이그레이션하면서 새로운 콘텐츠도 꾸준히 추가해 나갈 예정이다.\n참고 자료 Hugo Official Site: https://gohugo.io/ Blowfish Theme: https://blowfish.page/ Blowfish Creator: @nunocoracao Creator Blog: https://n9o.xyz/ Official Docs: https://blowfish.page/docs/ ","date":"2025-10-15T17:21:09+09:00","image":"/posts/251015_about_hugo/featured.png","permalink":"/posts/251015_about_hugo/","title":"Hugo \u0026 GithubPages 블로그로 넘어온 이유"},{"content":"1) Kind of Highlighter ES에서는 검색 결과에 대한 강조 표시를 지원하고 있으며 3가지 방식을 지원을 함.\n아래 타입은 Highlight 할 필드마다 각각 따로 적용할 수 있음\nUnified (Default : 기본 하이라이터) 통합 형광펜은 Lucene 통합 형광펜을 사용함. 이 하이라이터는 텍스트를 문장으로 나누고 BM25 알고리즘을 사용해 마치 말뭉치에 있는 문서처럼 개별 문장에 점수를 매김. 또한 정확한 구문 및 다중 용어(퍼지, 접두사, 정규식) 하이라이트를 지원함.\n특징\nBM25 알고리즘 기반 문장 점수 매기기 정확한 구문 하이라이트 지원 다중 용어 쿼리 지원 (퍼지, 접두사, 정규식) Plain 일반 형광펜은 단일 필드에서 간단한 쿼리 일치 항목을 강조 표시하는 데 가장 적합.\n쿼리 로직을 정확하게 반영하기 위해 작은 인메모리 인덱스를 생성하고 Lucene의 쿼리 실행 플래너를 통해 원래 쿼리 기준을 다시 실행하여 현재 문서에 대한 낮은 수준의 일치 정보에 액세스함함.\n이 작업은 강조 표시해야 하는 모든 필드와 모든 문서에 대해 반복됨. 복잡한 쿼리가 있는 많은 문서에서 많은 필드를 강조 표시하려면 게시글 또는 term_vector 필드에 통합 형광펜을 사용하는 것이 좋음.\n특징\n단일 필드의 간단한 쿼리에 적합 인메모리 인덱스 생성 모든 필드와 문서에 대해 반복 실행 FVH (Fast Vector Highlighter) fvh 형광펜은 Lucene 고속 벡터 형광펜을 사용함. 이 형광펜은 매핑에서 term_vector가 with_positions_offsets로 설정된 필드에 사용할 수 있음.\n특징\nboundary_scanner로 사용자 지정 가능 term_vector를 with_positions_offsets로 설정해야 하므로 인덱스 크기가 커짐 여러 필드에서 일치하는 항목을 하나의 결과로 결합 가능 (matched_fields 참조) 구문 일치를 용어 일치보다 부스팅하는 부스팅 쿼리를 강조 표시할 때 구문 일치가 용어 일치보다 정렬되는 것과 같이 서로 다른 위치의 일치에 서로 다른 가중치를 할당 가능 주의: fvh 형광펜은 스팬 쿼리를 지원하지 않음. 스팬 쿼리에 대한 지원이 필요한 경우 통합형 하이라이터와 같은 다른 하이라이터를 사용.\n2) Offsets Strategy 검색 중인 용어에서 의미 있는 검색 스니펫을 만들려면 하이라이터가 원본 텍스트에서 각 단어의 시작 및 끝 문자 오프셋을 알아야 함. 이러한 오프셋은 다음에서 얻을 수 있음.\nPostings List (글 목록) 매핑에서 index_options가 offsets로 설정되어 있으면 통합 하이라이터는 이 정보를 사용하여 텍스트를 다시 분석하지 않고 문서를 강조 표시함.\n원래 쿼리를 글에 대해 직접 다시 실행하고 색인에서 일치하는 오프셋을 추출하여 컬렉션을 강조 표시된 문서로 제한.\n강조 표시할 텍스트를 다시 분석할 필요가 없으므로 큰 필드가 있는 경우에 유용. 또한 term_vectors를 사용할 때보다 디스크 공간도 덜 필요.\n장점\n텍스트 재분석 불필요 큰 필드에 유용 term_vectors보다 디스크 공간 절약 Term Vectors (용어 벡터) 매핑에서 term_vector를 with_positions_offsets로 설정하여 용어 벡터 정보를 제공하면 unified 하이라이터가 자동으로 용어 벡터를 사용하여 필드를 강조 표시함.\n각 문서의 용어 사전에 액세스할 수 있기 때문에 특히 대용량 필드(1MB 초과)나 접두사 또는 와일드카드와 같은 다중 용어 쿼리를 강조 표시할 때 빠름.\nfvh 하이라이터는 항상 용어 벡터를 사용함.\n장점\n대용량 필드(1MB 초과)에 빠른 성능 다중 용어 쿼리에 효율적 (접두사, 와일드카드) 문서의 용어 사전 직접 액세스 Plain Highlighting (일반 하이라이팅) 이 모드는 다른 대안이 없을 때 통합에서 사용함.\n작은 인메모리 인덱스를 생성하고 Lucene의 쿼리 실행 플래너를 통해 원래 쿼리 기준을 다시 실행하여 현재 문서에 대한 낮은 수준의 일치 정보에 액세스함함.\n이 작업은 강조 표시가 필요한 모든 필드와 모든 문서에 대해 반복.\nplain highlighter은 항상 plain highlighting을 사용.\n특징\n다른 옵션이 없을 때 사용 인메모리 인덱스 생성 모든 필드/문서에 대해 반복 실행 참고 자료 Highlighting | Elasticsearch Guide [8.13]\n","date":"2024-02-07T21:24:29+09:00","image":"/posts/240207_es/featured.png","permalink":"/posts/240207_es/","title":"Elasticsearch Highlighting 기법"},{"content":"ElasticSearch Pagination 3가지 옵션 1. From/Size Pagination from + size = offset 방식으로 그때그때 메모리에 적재하여 검색을 함. 최대 10,000건까지 지원\nindex.max_result_window 옵션을 사용하여 10,000건 이상을 로드할 수 있지만 추천하지 않음\n특징\n간단한 구현 최대 10,000건 제한 Deep pagination 시 성능 저하 2. Search After Pagination이 최대 10,000건까지 페이징 처리할 수 있는 한계를 극복할 수 있음. 일반적인 커서 방식과 유사함.\n검색 결과의 sort 조건 필드를 키 값으로 사용하며 그 다음부터 조회를 함.\n단점\nsearch-after만 사용할 경우 중간에 인덱싱이 업데이트되거나 하면 응답 결과가 불규칙해질 수 있음.\nPIT(Point In Time)와 함께 사용\n이를 보완하기 위해 PIT(Point In Time)를 사용함\nPOST /my-index-000001/_pit?keep_alive=1m 위 요청을 통해 해당 인덱스의 현재 시점의 스냅샷을 찍어두고 다음과 같이 id 값으로 사용함\n{ \u0026#34;query\u0026#34;: {}, \u0026#34;size\u0026#34;: 100, \u0026#34;sort\u0026#34;: { \u0026#34;my_sort\u0026#34;: \u0026#34;desc\u0026#34; }, \u0026#34;search_after\u0026#34;: {}, \u0026#34;pit\u0026#34;: { \u0026#34;id\u0026#34;: \u0026#34;{{pit_value}}\u0026#34; } } 중간에 인덱스에 변경사항이 있어도 스냅샷을 기준으로 응답 결과가 반환됨.\nkeep_alive는 해당 PIT의 유효기간과 같음. 최신 시점을 기준으로 PIT를 관리하는 것이 권장됨.\n특징\n10,000건 이상 페이징 가능 커서 기반 방식 PIT와 함께 사용 시 일관된 결과 보장 Deep pagination에 효율적 3. Scroll Note: ES 공식 문서에 따라 Scroll 방식 대신 Search After 방식을 권장함\n특징\n대량 데이터 추출용 실시간 검색에는 부적합 현재는 Search After + PIT 조합을 권장 참고 자료 Elasticsearch Pagination Techniques: SearchAfter, Scroll, Pagination \u0026amp; PIT Elasticsearch Search After 성능 체크 Paginate search results | Elasticsearch Guide [7.17] Point in time API | Elasticsearch Guide [7.17] Scroll API | Elasticsearch Guide [7.17] ","date":"2024-02-06T21:16:24+09:00","image":"/posts/240206_es/featured.png","permalink":"/posts/240206_es/","title":"Elasticsearch Pagination Technique"},{"content":"Elastic search 자동완성 처리 방법 Edge N-Gram Tokenizer Configuration\nmin_gram: 1 max_gram: 10 token_chars: letter 적절한 사용처\n용어의 순서가 중요하지 않은 곳 토큰의 시작점 및 위치가 중요하지 않은 곳 Edge N-Gram Token Filter Configuration\nmin_gram: 1 max_gram: 10 적절한 사용처\n용어의 순서가 중요하지 않은 곳 토큰의 시작점 및 위치가 중요하지 않은 곳 Index_prefixes Parameter Configuration\nmin_chars: 1 max_chars: 10 적절한 사용처\nN-gram과 동일함\n다만 한가지 차이점은 후자의 경우 생성된 토큰을 넣는 추가적인 필드를 넣는다는 것\nSearch-as-you-type Data Type Configuration\nmax_shingle_size: 3 Generated Tokens (지원하는 서브 필드)\n예시: \u0026ldquo;real panda blog\u0026rdquo;\n._2gram additional field: real panda, panda blog (shingle token filter 적용됨) ._3gram additional field: real panda blog (shingle token filter 적용됨) ._index_prefix additional field: r, re, rea, real, \u0026ldquo;real \u0026ldquo;, real p, real pa, real pan, real pand, real panda, \u0026ldquo;real panda \u0026ldquo;, real panda b, real panda bl, real panda blo, real panda blog, p, pa, pan, pand, panda, \u0026ldquo;panda \u0026ldquo;, panda b, panda bl, panda blo, panda blog, \u0026ldquo;panda blog \u0026ldquo;, b, bl, blo, blog, \u0026ldquo;blog \u0026quot; (._3gram 필드에 적용이 되고 n gram max 는 3으로 적용된다) ES에서 말하는 가장 효율적인 쿼리 방법은 루트 필드와 해당 shingle 하위 필드를 대상으로 하는 bool_prefix 타입의 다중 일치 쿼리이다.\n이 쿼리는 어떤 순서로든 쿼리 용어와 일치할 수 있지만, 문서에서 shingle 서브 필드에 순서대로 용어가 포함된 경우 더 높은 점수를 부여한다.\n쿼리 용어와 문서의 용어가 정확하게 순서대로 일치하게 검색을 하거나 구문 쿼리의 다른 속성을 사용하려면 루트 필드에 match_phrase_prefix 쿼리를 사용할 수 있다. 접두사가 아닌 마지막 용어가 정확하게 일치해야 하는 경우도 그렇다. 하지만 match_bool_prefix 쿼리를 사용하는 것 보다 효율성이 떨어질 수 있다.\nshingle token filter 는 default 로 2가 적용됨\n적절한 사용처\n용어의 순서가 중요한 곳 토큰의 시작점과 위치가 중요한 곳 인덱싱을 할때 설정된 analyzer 가 없으면 default 로 standard analyzer 가 적용된다.\nSuggester API In-memory (Completion Suggester, Context Suggester) completion suggester는 auto-complete과 search-as-your-type 기능을 제공함. (오타 수정은 지원 x)\ncompletion suggester는 속도에 최적화 되어 있어 유저가 타이핑 하는 것에 즉각적으로 반응해줌.\n하지만 빌드와 인메모리 방식에 저장을 하는데 있어 많은 리소스 비용이 부담됨.\nTerm Suggester 사용자가 입력한 텍스트에 대한 결과가 없을경우 추천 단어를 기준으로 검색결과를 제공할때 사용\n잘못된 철자에 대해 추천 단어 제안\n편집거리를 사용해 단어를 제안한다. 편집거리 척도란 어떤 문자열이 다른 문자열과 얼마나 비슷한가를 편집거리를 사용해 알아볼 수 있다.\n편집거리를 측정하는 방식은 대부분 각 단어를 추가, 삭제, 치환을 통해 이루어진다.\n예를들어 tamming test 문자열을 taming text로 바꾸는데 m을 삭제하는 연산 1회와 s를 x로 바꾸는 연산 1회가 필요하다. 그러므로 편집거리는 2가 된다.\n만약 색인된 데이터와 일치하는 텀이 존재하지 않을 경우 term suggest 결과로 비슷한 단어를 추천해준다.\n결과에서 text는 제안한 문자를 나타내고, score는 제안하고자 하는 텍스트가 원본과 얼마나 가까운지를 나타낸다.\n알고리즘\n엘라스틱 서치에서는 편집거리 계산 알고리즘으로 Levenshtein 편집거리 측정 또는 Jaro-Winkler 편집거리 측정을 사용한다. 한글 처리\n한글의 경우 term suggest를 이용해도 데이터가 추천되지 않는다. 기본적으로 한글 유니코드 체계가 복잡하기 때문이다. ICU 분석기를 통해 한글 오타를 처리하는 것이 가능. ICU 분석기는 국제화 처리를 위해 특별히 개발된 분석기로 내부에 한글 자소를 분해하는 기능과 합치는 기능을 가지고 있음. 하지만 정교한 오타교정, 한영 변환, 자동완성 등의 전문적인 기능은 별도의 플러그인을 개발해서 사용하는 것이 좋다. (Ex. 자바카페 플러그인) Phrase Suggester (추천 문장 제안) Completion Suggester 자동완성 제안, 사용자가 입력을 완료하기 전에 자동완성을 사용해 검색어를 예측해서 보여줌\nContext Suggester 추천 문맥 제안\n","date":"2024-02-04T20:59:35+09:00","image":"/posts/240204_es/featured.png","permalink":"/posts/240204_es/","title":"Elasticsearch 자동완성 검색 처리 방법"},{"content":"Elasticsearch Query 처리 순서 Summary Filtered Query는 최종 search와 aggregation 둘 다의 결과에 영향을 미치지만, PostFiltered Query는 최종 search 결과에만 영향을 미치고 aggregation에는 영향을 미치지 않는다.\nQuery 처리 순서 SearchRequest → (Filtered) → Query → (PostFilter) → Result → RescoreQuery ↓ Aggregation → AggregationResult Filter Query 예시 { \u0026#34;query\u0026#34;: { \u0026#34;filtered\u0026#34;: { \u0026#34;filter\u0026#34;: { \u0026#34;term\u0026#34;: { \u0026#34;location\u0026#34;: \u0026#34;denver\u0026#34; } } } } } Filter Query는 검색 결과와 Aggregation 모두에 영향을 미친다.\nPostFilter Query 예시 { \u0026#34;post_filter\u0026#34;: { \u0026#34;term\u0026#34;: { \u0026#34;location\u0026#34;: \u0026#34;denver\u0026#34; } } } PostFilter Query는 검색 결과에만 영향을 미치고, Aggregation 결과에는 영향을 미치지 않는다.\nRescore Query 파라미터 window_size: 각 shard에서 재점수화할 상위 결과의 수 score_mode: main query score와 rescore query의 점수를 결합하는 방식 ","date":"2024-02-03T21:45:06+09:00","image":"/posts/240203_es_query/featured.png","permalink":"/posts/240203_es_query/","title":"Elasticsearch Query 처리 순서"},{"content":"\nToken Filter Token Filter는 Tokenizer에서 생성된 토큰 스트림을 받아 토큰을 추가, 제거 또는 변경하는 역할을 한다.\nWord Delimiter Graph Filter Word delimiter graph 필터는 제품 ID나 부품 번호와 같은 복잡한 식별자에서 문장 부호를 제거하도록 설계되었다. 이러한 사용 사례의 경우 keyword tokenizer와 함께 사용하는 것이 좋다.\nwi-fi와 같은 하이픈으로 연결된 단어를 분리할 때는 word delimiter graph 필터를 사용하지 않는 것이 좋다. 사용자들은 하이픈을 포함하기도 하고 포함하지 않고도 검색을 하기 때문에 synonym graph 필터를 사용하는 것이 좋다.\n변환 규칙 다음과 같은 방식으로 토큰을 분할한다:\n비영숫자 문자로 토큰 분할: Super-Duper → Super, Duper 앞뒤 구분 기호 제거: XL---42+'Autocoder' → XL, 42, Autocoder 대소문자 전환 시 분할: PowerShot → Power, Shot 문자-숫자 전환 시 분할: XL500 → XL, 500 영어 소유격 제거: Neil's → Neil API 사용 예시 GET /_analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;filter\u0026#34;: [\u0026#34;word_delimiter_graph\u0026#34;], \u0026#34;text\u0026#34;: \u0026#34;Neil\u0026#39;s-Super-Duper-XL500--42+AutoCoder\u0026#34; } // Result -\u0026gt; [ Neil, Super, Duper, XL, 500, 42, Auto, Coder ] Custom Analyzer 설정 PUT /my-index-000001 { \u0026#34;settings\u0026#34;: { \u0026#34;analysis\u0026#34;: { \u0026#34;analyzer\u0026#34;: { \u0026#34;my_analyzer\u0026#34;: { \u0026#34;tokenizer\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;filter\u0026#34;: [\u0026#34;word_delimiter_graph\u0026#34;] } } } } } Configurable Parameters adjust_offsets Default: true true일 경우 필터는 토큰 스트림에서 실제 위치를 더 잘 반영하도록 분할 토큰 또는 체이닝된 토큰의 시작점을 조정함 만약 trim 같은 필터를 사용한다면 offset의 변화 없이 token의 길이를 변경하기 때문에 함께 사용할 때는 false로 해야 함 catenate_all Default: false true이면 필터는 영숫자가 아닌 구분 기호로 구분된 영숫자 체인에 대해 체이닝 토큰을 생성함 예시: super-duper-xl-500 → [ superduperxl500, super, duper, xl, 500 ] catenate_numbers Default: false true이면 필터는 알파벳이 아닌 구분 기호로 구분된 숫자 문자 체인에 대해 카테네이트 토큰을 생성함 예시: 01-02-03 → [ 010203, 01, 02, 03 ] catenate_words Default: false true이면 필터는 알파벳이 아닌 구분 기호로 구분된 알파벳 문자 체인에 대해 카테네이트 토큰을 생성함 예시: super-duper-xl → [ superduperxl, super, duper, xl ] ⚠️ Catenate 파라미터 사용 시 주의점\n이 매개변수를 true로 설정하면 인덱싱에서 지원되지 않는 다중 위치 토큰이 생성함\n이 매개변수가 true이면 인덱스 분석기에서 이 필터를 사용하지 않거나 이 필터 뒤에 flatten_graph 필터를 사용하여 토큰 스트림을 인덱싱에 적합하게 만들어야함\n검색 분석에 사용할 경우, 카테네이트된 토큰은 match_phrase 쿼리 및 일치하는 토큰 위치에 의존하는 다른 쿼리에 문제를 일으킬 수 있음. 이러한 쿼리를 사용하려는 경우 이 매개변수를 true로 설정하지 않아야 함.\ngenerate_number_parts Default: true true이면 필터는 출력에 숫자로 구성된 토큰을 포함함 false인 경우 필터는 이러한 토큰을 출력에서 제외함 generate_word_parts Default: true true이면 필터는 출력에 알파벳 문자로 구성된 토큰을 포함함 false일 경우 이러한 토큰을 출력에서 제외함 ignore_keywords Default: false true일 경우 필터는 키워드 속성이 true인 토큰을 건너뜀 preserve_original Default: false true이면 필터는 출력에 분할된 토큰의 원본 버전을 포함함 이 원본 버전에는 영숫자가 아닌 구분 기호가 포함됨 예시: super-duper-xl-500 → [ super-duper-xl-500, super, duper, xl, 500 ] ⚠️ preserve_original 파라미터 사용 시 주의점\n이 매개변수를 true로 설정하면 인덱싱에서 지원되지 않는 다중 위치 토큰이 생성함\n이 매개변수가 true이면 인덱스 분석기에서 이 필터를 사용하지 않거나 이 필터 뒤에 flatten_graph 필터를 사용하여 토큰 스트림을 인덱싱에 적합하게 만들어야함\nprotected_words (Optional, array of strings)\n필터가 분할하지 않는 토큰의 배열 protected_words_path (Optional, string)\n필터가 분할하지 않는 토큰 목록이 포함된 파일의 경로 이 경로는 구성 위치에 대한 절대 경로이거나 상대 경로여야 하며, 파일은 UTF-8로 인코딩되어야 함 파일의 각 토큰은 줄 바꿈으로 구분해야 함 split_on_case_change (Optional, Boolean)\nDefault: true true이면 필터는 대소문자 전환 시 토큰을 분할함 예시: camelCase → [ camel, Case ] split_on_numerics (Optional, Boolean)\nDefault: true true이면 필터는 문자-숫자 전환 시 토큰을 분할함 예시: j2se → [ j, 2, se ] stem_english_possessive (Optional, Boolean)\nDefault: true true이면 필터는 각 토큰의 끝에서 영어 소유격(\u0026rsquo;s)을 제거함 예시: O'Neil's → [ O, Neil ] type_table (Optional, array of strings)\n문자에 대한 사용자 지정 유형 매핑의 배열 이를 통해 영숫자가 아닌 문자를 숫자 또는 영숫자로 매핑하여 해당 문자의 분할을 방지할 수 있음 예시\n[ \u0026#34;+ =\u0026gt; ALPHA\u0026#34;, \u0026#34;- =\u0026gt; ALPHA\u0026#34; ] 위 배열은 더하기(+) 및 하이픈(-) 문자를 영숫자로 매핑하므로 구분 기호로 취급되지 않음.\n지원되는 타입\nALPHA (Alphabetical) ALPHANUM (Alphanumeric) DIGIT (Numeric) LOWER (Lowercase alphabetical) SUBWORD_DELIM (Non-alphanumeric delimiter) UPPER (Uppercase alphabetical) type_table_path (Optional, string)\ncustom type mapping 파일 경로 작성 예시\n# Map the $, %, \u0026#39;.\u0026#39;, and \u0026#39;,\u0026#39; characters to DIGIT # This might be useful for financial data. $ =\u0026gt; DIGIT % =\u0026gt; DIGIT . =\u0026gt; DIGIT \\u002C =\u0026gt; DIGIT # in some cases you might not want to split on ZWJ # this also tests the case where we need a bigger byte[] # see https://en.wikipedia.org/wiki/Zero-width_joiner \\u200D =\u0026gt; ALPHANUM 이 파일 경로는 설정 위치에 대한 절대 경로이거나 상대 경로여야 하며, 파일은 UTF-8로 인코딩되어야 함. 파일의 각 매핑은 줄 바꿈으로 구분함.\n사용 시 주의사항 Standard tokenizer와 같이 구두점을 제거하는 토큰화기와 함께 word_delimiter_graph 필터를 사용하지 않는 것이 좋다. 이렇게 하면 word_delimiter_graph 필터가 토큰을 올바르게 분할하지 못할 수 있다.\n또한 catenate_all 또는 preserve_original과 같은 필터의 구성 가능한 매개변수를 방해할 수도 있다. 대신 keyword 또는 whitespace tokenizer를 사용하는 것이 좋다.\n","date":"2024-02-02T21:30:06+09:00","image":"/posts/240202_es_analyzer3/featured.png","permalink":"/posts/240202_es_analyzer3/","title":"Elasticsearch Token Filter"},{"content":"\nTokenizer Tokenizer는 문자열 스트림을 받고 각각의 토큰 단위로 자른다. (주로 각각의 단어별로 토큰화가 이루어짐)\n가장 standard하게 사용되는 whitespace tokenizer는 공백을 기준으로 잘라 토큰화를 진행한다.\nWhitespace Tokenizer 예시 // character streams Quick brown fox! // Result [Quick, brown, fox!] Tokenizer의 책임 각 용어의 정렬과 포지션 (구 또는 단어 유사성 쿼리에서 사용) 변형 전의 기존 단어의 시작과 끝 문자는 검색 스니펫 하이라이팅에 사용됨 Token별 타입: \u0026lt;ALPHANUM\u0026gt;, \u0026lt;HANGUL\u0026gt;, \u0026lt;NUM\u0026gt; 등 (단순한 analyzer는 오직 단어 토큰 타입만 제공함) Word Oriented Tokenizer 아래 tokenizer들은 풀 텍스트에서 각각의 단어로 토큰화를 하는데 사용된다.\nStandard Tokenizer standard tokenizer는 Unicode Text Segmentation algorithm을 기반으로 토큰화를 진행한다.\nConfiguration max_token_length: 지정된 길이를 초과하는 문자열을 기점으로 잘라 토큰화를 진행 Default = 255 변환 예시 POST _analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;standard\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;The 2 QUICK Brown-Foxes jumped over the lazy dog\u0026#39;s bone.\u0026#34; } // Result -\u0026gt; [ The, 2, QUICK, Brown, Foxes, jumped, over, the, lazy, dog\u0026#39;s, bone ] max_token_length 적용 예시 PUT my-index-000001 { \u0026#34;settings\u0026#34;: { \u0026#34;analysis\u0026#34;: { \u0026#34;analyzer\u0026#34;: { \u0026#34;my_analyzer\u0026#34;: { \u0026#34;tokenizer\u0026#34;: \u0026#34;my_tokenizer\u0026#34; } }, \u0026#34;tokenizer\u0026#34;: { \u0026#34;my_tokenizer\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;standard\u0026#34;, \u0026#34;max_token_length\u0026#34;: 5 } } } } } POST my-index-000001/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;my_analyzer\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;The 2 QUICK Brown-Foxes jumped over the lazy dog\u0026#39;s bone.\u0026#34; } // Result -\u0026gt; [ The, 2, QUICK, Brown, Foxes, jumpe, d, over, the, lazy, dog\u0026#39;s, bone ] Letter Tokenizer letter tokenizer는 문자열이 아닌 것을 기점으로 잘라 토큰화를 진행한다. 이는 유럽권 언어(영미권)에는 적합하나 아시아 언어, 특히 공백을 기점으로 단어가 나뉘지 않는 언어에는 적합하지 않다.\n변환 예시 POST _analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;letter\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;The 2 QUICK Brown-Foxes jumped over the lazy dog\u0026#39;s bone.\u0026#34; } // Result -\u0026gt; [ The, QUICK, Brown, Foxes, jumped, over, the, lazy, dog, s, bone ] Lowercase Tokenizer lowercase tokenizer는 letter tokenizer처럼 문자열이 아닌 것을 기점으로 잘라 토큰화를 진행하며, 추가적으로 모든 문자열을 소문자로 변환한다. 기능적으로는 letter tokenizer와 동일하면서도 소문자로 변환하는 기능을 한번에 수행함으로 효율적이다.\n변환 예시 POST _analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;lowercase\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;The 2 QUICK Brown-Foxes jumped over the lazy dog\u0026#39;s bone.\u0026#34; } // Result -\u0026gt; [ the, quick, brown, foxes, jumped, over, the, lazy, dog, s, bone ] Whitespace Tokenizer whitespace tokenizer는 공백 문자열을 기준으로 토큰화를 진행한다.\nConfiguration max_token_length: 지정된 길이를 초과하는 문자열을 기점으로 잘라 토큰화를 진행 Default = 255 변환 예시 POST _analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;whitespace\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;The 2 QUICK Brown-Foxes jumped over the lazy dog\u0026#39;s bone.\u0026#34; } // Result -\u0026gt; [ The, 2, QUICK, Brown-Foxes, jumped, over, the, lazy, dog\u0026#39;s, bone. ] UAX URL Email Tokenizer uax_url_email tokenizer는 standard tokenizer와 동일하지만, 한 가지 다른점은 URLs이나 email 주소를 인식하고 하나의 토큰으로 취급을 한다.\nConfiguration max_token_length: 지정된 길이를 초과하는 문자열을 기점으로 잘라 토큰화를 진행 Default = 255 변환 예시 POST _analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;uax_url_email\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;Email me at john.smith@global-international.com\u0026#34; } // Result -\u0026gt; [ Email, me, at, john.smith@global-international.com ] // 위의 예시를 만약 standard tokenizer로 한다면 다음과 같은 결과가 나옴 // Result -\u0026gt; [ Email, me, at, john.smith, global, international.com ] Configuration 예시 PUT my-index-000001 { \u0026#34;settings\u0026#34;: { \u0026#34;analysis\u0026#34;: { \u0026#34;analyzer\u0026#34;: { \u0026#34;my_analyzer\u0026#34;: { \u0026#34;tokenizer\u0026#34;: \u0026#34;my_tokenizer\u0026#34; } }, \u0026#34;tokenizer\u0026#34;: { \u0026#34;my_tokenizer\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;uax_url_email\u0026#34;, \u0026#34;max_token_length\u0026#34;: 5 } } } } } POST my-index-000001/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;my_analyzer\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;john.smith@global-international.com\u0026#34; } // Result (email 형식을 무시하고 max_token_length가 우선순위가 됨) // [ john, smith, globa, l, inter, natio, nal.c, om ] Classic Tokenizer classic tokenizer는 문법 베이스의 토큰화를 진행하고 영어 document에 좋다. 이 토큰화 방식은 약어, 회사 이름, 이메일 주소, 인터넷 호스트 이름을 특별하게 처리하는 방식이 있다. 그러나 이러한 규칙들이 항상 동작하지는 않고, 영어 이외의 언어에서는 잘 동작하지 않는다.\nTokenizing 규칙 대부분 구두점을 기준으로, 구두점을 제거하며 단어를 나눈다. 하지만 공백이 뒤따르지 않는 점은 토큰의 일부로 간주된다. 하이픈을 기준으로 단어를 나누긴 하지만 만약 해당 단어에 하이픈이 있을 경우 제품번호로 인식을 하고 나누지 않는다. (예: 123-23) email과 internet hostname은 하나의 토큰으로 간주한다. Configuration max_token_length: 지정된 길이를 초과하는 문자열을 기점으로 잘라 토큰화를 진행 Default = 255 변환 예시 POST _analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;classic\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;The 2 QUICK Brown-Foxes jumped over the lazy dog\u0026#39;s bone.\u0026#34; } // Result -\u0026gt; [ The, 2, QUICK, Brown, Foxes, jumped, over, the, lazy, dog\u0026#39;s, bone ] Thai Tokenizer thai tokenizer는 태국어 텍스트를 단어 단위로 토큰화를 시킨다. Java에 포함되어 있는 Thai segmentation algorithm을 사용한다. 만약 input text에 태국어가 아닌 다른 언어의 문자열이 함께 포함되어 있는 경우는 해당 문자열에 한해 standard tokenizer가 적용된다.\n⚠️ 주의사항: 해당 토큰화 방식은 일부 JRE에서 지원되지 않을 수 있음. 이 토큰화 방식은 Sun/Oracle 및 OpenJDK에서 작동하는 것으로 알려져 있음. 어플리케이션에 완전한 이식성을 고려하는 경우 ICU tokenizer를 대신 사용하는 것을 고려하는 것이 좋음\n변환 예시 POST _analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;thai\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;การที่ได้ต้องแสดงว่างานดี\u0026#34; } // Result -\u0026gt; [ การ, ที่, ได้, ต้อง, แสดง, ว่า, งาน, ดี ] 참고 문서 Tokenizer reference | Elasticsearch Guide [8.8] ","date":"2024-02-02T21:23:06+09:00","image":"/posts/240202_es_analyzer2/featured.png","permalink":"/posts/240202_es_analyzer2/","title":"Elasticsearch Tokenizer"},{"content":"\nCharacter Filters Character Filter는 토크나이저 단계 이전에 입력된 문자열을 전처리하는 과정이다.\n문자열들에 더하거나, 제거하거나, 문자열을 바꾸는 작업을 한다.\nElasticsearch에서는 다음과 같이 기본적인 Character Filter들을 제공하고 커스텀 필터도 적용 가능하다.\nHTML Strip Character Filter HTML 형태로 입력받은 값을 decoding된 값으로 변환시켜준다.\n변환 예시 GET /_analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;char_filter\u0026#34;: [\u0026#34;html_strip\u0026#34;], \u0026#34;text\u0026#34;: \u0026#34;\u0026lt;p\u0026gt;I\u0026amp;apos;m so \u0026lt;b\u0026gt;happy\u0026lt;/b\u0026gt;!\u0026lt;/p\u0026gt;\u0026#34; } // Result -\u0026gt; [ \\nI\u0026#39;m so happy!\\n ] 적용 방법 PUT /my-index-000001 { \u0026#34;settings\u0026#34;: { \u0026#34;analysis\u0026#34;: { \u0026#34;analyzer\u0026#34;: { \u0026#34;my_analyzer\u0026#34;: { \u0026#34;tokenizer\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;char_filter\u0026#34;: [\u0026#34;html_strip\u0026#34;] } } } } } Mapping Character Filter Mapping Character Filter는 입력받은 문자열이 key 값으로 지정된 문자와 동일하면 해당 key의 value 값으로 변환을 시켜준다.\nMatching 방식은 탐욕법으로 가장 많이 매칭된 패턴으로 변환되며 변환값(value)은 빈 문자열도 가능하다.\n변환 예시 GET /_analyze { \u0026#34;tokenizer\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;char_filter\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;mapping\u0026#34;, \u0026#34;mappings\u0026#34;: [ \u0026#34;٠ =\u0026gt; 0\u0026#34;, \u0026#34;١ =\u0026gt; 1\u0026#34;, \u0026#34;٢ =\u0026gt; 2\u0026#34;, \u0026#34;٣ =\u0026gt; 3\u0026#34;, \u0026#34;٤ =\u0026gt; 4\u0026#34;, \u0026#34;٥ =\u0026gt; 5\u0026#34;, \u0026#34;٦ =\u0026gt; 6\u0026#34;, \u0026#34;٧ =\u0026gt; 7\u0026#34;, \u0026#34;٨ =\u0026gt; 8\u0026#34;, \u0026#34;٩ =\u0026gt; 9\u0026#34; ] } ], \u0026#34;text\u0026#34;: \u0026#34;My license plate is ٢٥٠١٥\u0026#34; } // Result -\u0026gt; [ My license plate is 25015 ] Pattern Replace Character Filter pattern_replace 필터는 정규식에 매칭되는 문자열들을 지정된 문자열로 변환시켜준다.\n⚠️ 주의사항: 정규식은 Java 정규식을 따르며, 안 좋은 정규식은 성능 저하 또는 StackOverflow 에러를 발생시키고, 실행 중인 노드를 갑자기 종료시킬 수 있다.\n파라미터 pattern: Java 정규표현식 replacement: 치환할 문자열 flags: Java의 정규 표현식 플래그로 |로 분리되어야 함 (예: \u0026ldquo;CASE_INSENSITIVE|COMMENTS\u0026rdquo;) 변환 예시 PUT my-index-000001 { \u0026#34;settings\u0026#34;: { \u0026#34;analysis\u0026#34;: { \u0026#34;analyzer\u0026#34;: { \u0026#34;my_analyzer\u0026#34;: { \u0026#34;tokenizer\u0026#34;: \u0026#34;standard\u0026#34;, \u0026#34;char_filter\u0026#34;: [\u0026#34;my_char_filter\u0026#34;] } }, \u0026#34;char_filter\u0026#34;: { \u0026#34;my_char_filter\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;pattern_replace\u0026#34;, \u0026#34;pattern\u0026#34;: \u0026#34;(\\\\d+)-(?=\\\\d)\u0026#34;, \u0026#34;replacement\u0026#34;: \u0026#34;$1_\u0026#34; } } } } } POST my-index-000001/_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;my_analyzer\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;My credit card is 123-456-789\u0026#34; } // Result -\u0026gt; [ My, credit, card, is, 123_456_789 ] 참고 문서 Character filters reference | Elasticsearch Guide [8.8] ","date":"2024-02-02T21:00:06+09:00","image":"/posts/240202_es_analyzer1/featured.png","permalink":"/posts/240202_es_analyzer1/","title":"Elasticsearch Character Filter"},{"content":"Elastic Search 는 Apache Lucene 기반의 Java 오픈소스 분산 검색 엔진이다.\nElasticSearch 를 통해 루씬 라이브러리(Java에서 개발한 정보 검색용 라이브러리)를 단독으로 사용할 수 있으며, 방대한 양의 데이터를 실시간에 가깝게 저장, 검색, 분석을 수행할 수 있다.\nElasticeSearch 는 검색을 위해 단독으로 사용되기도 하며, ELK(Elasticsearch / Logstash / Kibana) 스택으로 사용되기도 한다.\nELK 스택 구성 요소 Elasticsearch: Logstash로 부터 받은 데이터를 검색 및 집계하여 필요한 정보를 획득 Logstash: 다양한 소스(DB, csv 파일 등)의 로그 또는 트랜잭션 데이터를 수집, 집계, 파싱하여 Elasticsearch로 전달 Kibana: Elasticsearch 의 빠른 검색을 통해 데이터를 시각화 및 모니터링 💡 주로 ELK는 로드밸런싱되어 있는 WAS의 흩어져있는 로그를 한 곳으로 모으고, 원하는 데이터를 빠르게 검색한 뒤 시각화하여 모니터링하기 위해 사용한다.\nElasticsearch 와 RDB 의 명칭 비교 Elasticsearch 7.0 부터는 하나의 인덱스에 하나의 타입만 가질 수 있음 이 이유는 Elasticsearch는 하나의 인덱스(DB) 안에서의 타입은 같은 Lucene 필드를 사용한다. 따라서 타입은 다를지라도 동일한 이름을 가진 필드는 독립적이지 않아으므로 여러가지 문제가 발생할 수 있으므로 하나의 인덱스는 하나의 타입만을 갖도록 수정이 되었다.\nRDB와의 비교 RDB의 경우\n하나의 DB에 여러 테이블이 있고 각 테이블에 동일한 이름의 컬럼이 존재해도 서로 영향을 미치지 않는다. Elasticsearch의 경우\n하나의 인덱스(=DB) 내의 각 타입(=테이블)에 동일한 이름을 가진 필드(=컬럼)가 있을 경우, 해당 필드는 독립적이지 않고 동일한 Lucene 필드에 저장되며 동일한 정의를 가져야 한다. Elasticsearch 구조 클러스터 (Cluster) 클러스터란 Elasticsearch 에서 가장 큰 시스템 단위를 의미하며, 최소 하나 이상의 노드로 이루어진 노드의 집합이다.\n서로 다른 클러스터는 데이터의 접근, 교환을 할 수 없는 독립적인 시스템으로 유지됨 여러 대의 서버가 하나의 클러스터를 구성할 수 있음 한 서버에 여러 개의 클러스터가 존재할 수 있음 노드 (Node) 노드는 클러스터에 포함된 단일 서버로서 데이터를 저장하고 클러스터의 색인화 및 검색 기능에 참여한다. 노드는 역할에 따라 다음과 같이 구분한다.\nMaster-eligible Node 클러스터를 제어하는 마스터로 선택할 수 있는 노드\n인덱스 생성, 삭제 클러스터 노드의 추적 관리 데이터 입력 시 할당할 샤드 선택 Data Node 데이터(Document)가 저장되는 노드이며, 데이터가 분산 저장되는 공간인 샤드가 배치되는 노드\nCRUD, 색인, 검색, 통계 등의 데이터 작업을 수행 많은 리소스(CPU, 메모리 등)를 필요로 함 모니터링 작업이 필요하며, 마스터 노드와는 분리해야 함 Ingest Node 데이터를 변환하는 등 사전 처리 파이프라인을 실행하는 역할\nCoordination Only Node 사용자의 요청을 받고 라운드 로빈 방식으로 분산하는 노드\n클러스터에 관련된 것은 마스터 노드로 전달 데이터와 관련된 것은 데이터 노드로 전달 로드밸런싱 역할을 수행 인덱스 / 샤드 / 복제 인덱스 (Index) RDB의 데이터베이스와 대응하는 개념\n샤드 (Shard) 인덱스 내부에 색인된 데이터들이 하나로 뭉쳐서 존재하지 않고 여러 부분으로 나뉘어 존재함. 스케일 아웃을 위해 하나의 인덱스를 여러 샤드로 쪼갬.\n💡 샤드는 프라이머리 샤드와 레플리카 샤드로 나뉜다.\n프라이머리 샤드 (Primary Shard)\n데이터의 원본 데이터 업데이트 요청은 프라이머리 샤드에 전달됨 업데이트된 내용은 레플리카 샤드에 복제됨 레플리카 샤드 (Replica Shard)\n프라이머리 샤드의 복제본 원본 데이터가 손실되었을 때 대신 사용하면서 장애를 극복하는 역할을 수행 기본적으로 원본인 프라이머리 샤드와 다른 노드에 배정됨 세그먼트 (Segment) 세그먼트는 ElasticSearch 에서 문서의 빠른 검색을 위해 설계된 자료 구조이며, 샤드의 데이터를 가지고 있는 물리적인 파일이다.\n세그먼트의 특징 각 샤드는 다수의 세그먼트로 구성되어 있어 검색 요청을 분산 처리하여 효율적인 검색이 가능 샤드에서 검색 시, 먼저 각 세그먼트를 검색하여 결과를 조합한 후 최종 결과를 해당 샤드의 결과로 반환 세그먼트 내부에 색인된 데이터가 역색인 구조로 저장되어 있어 검색 속도가 매우 빠름 세그먼트 생성 과정 매 요청마다 새로운 세그먼트를 만들면 너무 많은 세그먼트가 생성되므로 이를 방지하기 위해 인메모리 버퍼를 사용한다.\nFlush: 인메모리 버퍼에 쌓인 내용이 일정 시간이 지나거나 버퍼가 가득 차면 flush를 수행하고, 시스템 캐시에 세그먼트가 생성됨\n이 시점부터 데이터 검색이 가능 이 상태는 세그먼트가 시스템 캐시에 저장된 상태이지 디스크에 저장된 상태가 아님 Commit: 일정 시간이 지나면 commit을 통해 물리적인 디스크에 세그먼트를 저장\n병합: 저장된 세그먼트는 시간이 지날수록 하나로 병합하는 과정을 수행\n병합을 통해 세그먼트를 하나로 줄여주면 검색할 세그먼트 개수가 줄어 검색 성능이 향상됨 ","date":"2024-02-01T21:07:14+09:00","image":"/posts/240201_es/featured.png","permalink":"/posts/240201_es/","title":"ElasticSearch 란?"},{"content":"Clustering SELECT * FROM user_signups WHERE country = \u0026#39;Lebanon\u0026#39; AND registration_date = \u0026#39;2023-12-01\u0026#39; Clustering을 통해 BigQuery가 데이터 접근에 있어 더 적은 일을 수행하게 하여 Query 속도를 올릴 수 있음. 하지만 Clustering을 하기 전에 테이블의 데이터 양을 생각하여 클러스터링을 처리하는 비용이 더 안 좋을 수 있을지를 생각하는 것이 좋음.\n예를 들어 BigQuery의 column based 테이블이 10 rows밖에 데이터가 없다고 생각을 하면 clustering을 하는 비용이 데이터를 풀스캔하는 비용보다 더 많이 나올 것임을 인지해야 함.\n전 구글 엔지니어의 말을 인용하면 클러스터링을 하기 위한 데이터 그룹당 100MB 미만이라면 클러스터링을 하는 것보다 풀스캔을 하는 것이 더 나을 수 있다고 함.\n참고: Google BigQuery clustered table not reducing query size\n중요 사항\n추가적으로 클러스터링을 한 base 컬럼을 query 시에 필터링하지 않으면 아무런 query performance에 아무런 도움이 되지 않음.\nExample of Creating Clustered Tables CREATE TABLE `myproject.mydataset.clustered_table` ( registration_date DATE, country STRING, tier STRING, username STRING ) CLUSTER BY country; Clustering 특징\n최대 4개의 column까지 클러스터링 가능 Partitioning과 다르게 INT64, DATE 타입만 사용할 수 있지 않음 STRING과 GEOGRAPHY와 같은 타입도 사용 가능 Combine Clustering with Partitioning 파티셔닝과 클러스터링을 함께 사용하면 더욱 효율적인 데이터 접근이 가능함.\n조합 전략\nPartitioning: 날짜 기반 데이터 분할 Clustering: 파티션 내부에서 추가적인 정렬 쿼리 성능과 비용 최적화 참고 자료 Add One Line of SQL to Optimise Your BigQuery Tables Use the Partitions, Luke! A Simple and Proven Way to Optimise Your SQL Queries ","date":"2023-12-20T21:50:16+09:00","image":"/posts/231220_bigquery/featured.png","permalink":"/posts/231220_bigquery/","title":"BigQuery Clustering 최적화"},{"content":"BigQuery 특징 Column-based Database 일반적인 로우 단위로 저장이 되는 RDB와 다르게 특정 컬럼의 데이터를 접근할 때 해당 로우를 모두 스캔하지 않고 찾고자 하는 컬럼 파일 하나만 스캔하여 접근.\n특정 컬럼만을 읽어 개수를 세거나 통계를 내는 분석용 데이터베이스(OLAP) 작업에 유리.\n장점\n컬럼 단위 스캔으로 빠른 조회 분석 쿼리에 최적화 스토리지 효율성 데이터 처리 구조 Colossus (분산 스토리지)\nGoogle File System(GFS)를 잇는 Google의 클러스터 수준의 파일시스템 맨 아래에서 저장소를 제공하고 Jupiter라는 TB급 네트워크 망을 통해 컴퓨터 노드와 통신 연산 계층 (Leaf, Mixer1, Mixer0)\n디스크 없이 Colossus에서 읽은 데이터를 처리 각각 위의 계층으로 데이터를 올리는 역할 분산 병렬 처리를 통한 고속 연산 No Key, No Index 키와 인덱스 개념이 존재하지 않음. 풀스캔 only\n특징\n인덱스 관리 불필요 컬럼 기반 스캔으로 성능 확보 대용량 데이터 분석에 최적화 No Update, Delete 성능을 위해 추가만 가능하며 한번 입력된 데이터는 수정되거나 삭제될 수 없음.\n데이터가 잘못 입력된 경우 테이블을 삭제하고 다시 만들어야 함.\n제약사항\nINSERT만 지원 UPDATE/DELETE 미지원 데이터 수정 시 재생성 필요 Eventual Consistency 데이터를 3개의 데이터 센터에 복제를 하기에 데이터 쓰기 후 바로 조회가 안 될 수 있음.\n특징\n3중 복제를 통한 고가용성 최종 일관성 보장 쓰기 후 즉시 읽기 불가능할 수 있음 참고 자료 BigQuery 성능/비용 팁 BigQuery UNNEST, ARRAY, STRUCT 사용 방법 구글 빅데이터 플랫폼 빅쿼리 아키텍처 소개 ","date":"2023-12-18T21:37:29+09:00","image":"/posts/231218_bigquery/featured.png","permalink":"/posts/231218_bigquery/","title":"BigQuery란?"},{"content":"QueryString 처리 방식 비교 Spring에서 QueryString을 처리하는 두 가지 방식을 비교해보겠습니다.\n방식 1: Object로 받기 (ParameterObject) @Operation(tags = {\u0026#34;swagger\u0026#34;}) @GetMapping(\u0026#34;/hello/parameters1\u0026#34;) public ResponseEntity\u0026lt;List\u0026lt;ResponseTest\u0026gt;\u0026gt; parameterObjectTest(ParameterObjectReq req) { ResponseTest response = new ResponseTest(req.email(), req.password(), req.occupation()); return ResponseEntity.ok(List.of(response)); } 방식 2: 개별 파라미터로 받기 (@RequestParam) @Operation(tags = {\u0026#34;swagger\u0026#34;}) @GetMapping(\u0026#34;/hello/parameters2\u0026#34;) public ResponseEntity\u0026lt;List\u0026lt;ResponseTest\u0026gt;\u0026gt; parameterObjectTest2( @RequestParam(value = \u0026#34;email\u0026#34;) String email, @RequestParam(value = \u0026#34;pw\u0026#34;) String password, @RequestParam(value = \u0026#34;oq\u0026#34;) OccupationStatus status ) { ResponseTest response = new ResponseTest(email, password, status); return ResponseEntity.ok(List.of(response)); } 모델 정의 ParameterObjectReq (Request DTO)\npublic record ParameterObjectReq( String email, String password, OccupationStatus occupation ) { } OccupationStatus (Enum)\npublic enum OccupationStatus { STUDENT, EMPLOYEE, UNEMPLOYED } 두 방식의 차이점 일반적으로 request를 QueryString으로 받을 경우 @RequestParam을 사용하지만, 받는 인자가 많을 경우 첫 번째 방식처럼 QueryString을 Object 형태로 받을 수 있습니다.\n@RequestParam vs ParameterObject @RequestParam: 기본적으로 required = true로 설정되어 있어 request value를 필수로 받습니다. ParameterObject: Spring에서 별도의 어노테이션 없이도 QueryString을 객체의 필드값에 자동으로 바인딩합니다. 하지만 required가 기본 설정되지 않아 null 값이 들어올 수 있습니다. Springdoc에서의 ParameterObject vs @RequestParam 변환 비교 ParameterObject 사용 시 @RequestParam 사용 시 @ParameterObject 어노테이션 활용 ParameterObject를 Springdoc이 @RequestParam을 사용했을 때처럼 변환해주고 Required 여부를 표시하려면 다음과 같이 설정합니다.\n코드 예제 @ParameterObject public record ParameterObjectReq( @NotNull String email, @NotNull String password, OccupationStatus occupation ) { } @ParameterObject는 Springdoc 어노테이션으로, 여러 개의 QueryString을 Object 형태로 받을 경우 해당 클래스 위에 명시하면 @RequestParam처럼 인식하고 변환해줍니다.\nJSR-303 지원 Springdoc은 JSR-303을 지원하며, 다음과 같은 validation 어노테이션을 사용할 수 있습니다\n@NotNull @Min, @Max @Size 기타 validation 어노테이션 Springdoc 공식 문서에 따르면\nThis library supports\nOpenAPI 3 Spring-boot (v1, v2 and v3) JSR-303, specifically for @NotNull, @Min, @Max, and @Size Swagger-ui OAuth 2 GraalVM native images 변환 결과 ParameterObject도 @RequestParam으로 인식되도록 spec 파일이 작성되었고, @NotNull을 붙이지 않은 occupation에는 Required가 optional 형태로 표시됩니다.\n좌측 이미지: @ParameterObject를 명시했을 경우 Springdoc이 인식하고 정상적인 spec으로 변환\nSwagger2 → Swagger3 Annotations Swagger2 Swagger3 설명 @Api @Tag 클래스단에 swagger 리소스 표시(그룹화 시켜줌)\nname : 태그의 이름\ndescription : 태그에 대한 설명 @ApiIgnore @Parameter(hidden = true)\n@Operation(hidden = true)\n@Hidden 해당 어노테이션을 통해 파라미터를 swagger-ui 에서 숨길 수 있음.\nrequestBody 나 ResponseBody 의 경우는\n@JsonProperty(access = JsonProperty.Access.READ_ONLY) 를 사용 @ApiImplicitParam @Parameter 단일 RequestParam 에 대한 설정 및 리소스 표시 @ApiImplicitParams @Parameters 여러개의 RequestParam 을 설정 @ApiModel @Schema description : 한글명\ndefaultValue : 기본값\nallowableValues : 허용가능한 값(열거형으로 정의가능할 경우 설정합니다) @ApiModelProperty(hidden = true) @Schema(accessMode = READ_ONLY) @ApiOperation(value = \u0026ldquo;foo\u0026rdquo;, notes = \u0026ldquo;bar\u0026rdquo;) @Operation(summary = \u0026ldquo;foo\u0026rdquo;, description = \u0026ldquo;bar\u0026rdquo;) summary : api에 대한 간략 설명\ndescription : api에 대한 상세 설명\nresponses : api Response 리스트\nparameters : api 파라미터 리스트 @ApiParam @Parameter name : 파라미터 이름\ndescription : 파라미터 설명\nin : 파라미터 위치 (query, header, path, cookie) @ApiResponse(code = 404, message = \u0026ldquo;foo\u0026rdquo;) @ApiResponse(responseCode = \u0026ldquo;404\u0026rdquo;, description = \u0026ldquo;foo\u0026rdquo;) responseCode : http 상태코드\ndescription : response에 대한 설명\ncontent : Response payload 구조\nschema : payload에서 이용하는 Schema\nhidden : Schema 숨김여부\nimplementation : Schema 대상 클래스 여러 개의 request query params를 캡처하기 위해 객체를 사용하는 경우, 해당 메서드 인자에 @ParameterObject 어노테이션을 사용하세요 이 단계는 선택사항입니다: 여러 개의 Docket 빈이 있는 경우에만 GroupedOpenApi 빈으로 교체하세요 @Tag 어노테이션 활용 @Tag 어노테이션을 사용하면 다음과 같은 그룹핑이 가능합니다\nController 단위로 그룹핑 Controller 내부의 메서드 단위로 그룹핑 @Tag에 명명한 이름에 따라 spec 파일로 전환 시 그룹핑 수행 OpenAPI Generator를 이용한 client code 생성 시 해당 이름으로 파일 생성 @Tag 중복 사용 시 주의사항 질문: 최상위 레벨에 @Tag로 그룹핑하고, 하위 메서드의 @Operation에서 다른 이름으로 tag를 설정하면 어떻게 될까요?\n테스트 결과 최상위에 @Tag(name = \u0026quot;swagger\u0026quot;)를 설정하고, postHello 메서드의 @Operation에서 tags = {\u0026quot;swagger123\u0026quot;}을 추가한 경우, 같은 엔드포인트가 다른 그룹으로 중복 생성됩니다.\n문제점 이 상태에서 OpenAPI Generator를 사용하면 아래와 같이 중복된 client 코드가 생성되는 문제가 발생합니다.\n권장사항: 특별한 경우가 아니라면 @Tag를 이용한 그룹핑은 Controller의 최상위에서만 사용하는 것을 권장합니다.\nOpenAPI Generator Client Code 생성 시 파일명 Client 코드 생성 시 @Tag에서 명명한 이름 + -api가 postfix로 붙습니다. 이 부분을 커스터마이징하려면 Mustache 파일을 수정해야 합니다.\n참고 자료 Using Templates | OpenAPI Generator Mustache.js GitHub OpenAPI Generator 사용법 OpenAPI Generator로 API의 안전한 Model과 정형화된 구현코드 자동생성하기 인증 관련 OpenAPI 스펙 OpenAPI는 다양한 인증 방식을 지원합니다. 주요 설정 항목은 다음과 같습니다.\ntype (인증 형식) 현재 API Key, HTTP, OAuth2, OpenID Connect 방식을 지원합니다. 참고: OpenAPI v2 스펙에서는 OpenID Connect 방식을 지원하지 않습니다.\n지원되는 타입\nhttp: Basic, Bearer 및 기타 HTTP 인증 체계 apiKey: API 키 및 쿠키 인증 oauth2: OAuth2 인증 openIdConnect: OpenID Connect 검색 주요 설정 항목 name: 인증 키 이름 (API Key 방식 사용 시 필요) in: 인증 키의 위치 지정 (query, header, cookie 중 선택, API Key 방식 사용 시 필요) scheme: 인증 방식 지정 (Basic 또는 Bearer, HTTP 인증 방식 사용 시 필요) bearerFormat: Bearer 토큰 형식 (일반적으로 JWT 사용) flows: OAuth2 플로우 타입 (implicit, password, clientCredentials, authorizationCode 중 선택) openIdConnectUrl: OpenID Connect URL (OpenAPI v2 스펙에서는 OAuth2나 Bearer 토큰 방식으로 대체 권장) @Deprecated 전략 API 버전 업데이트로 DTO 스펙에 변경이 있을 경우, 다음과 같은 단계적 전략을 사용합니다.\n1단계: @Deprecated 표시 먼저 변경될 필드에 @Deprecated 어노테이션을 붙입니다.\npublic class UserDto { @Deprecated private String oldField; private String newField; } OpenAPI spec 상에도 해당 스키마의 필드에 deprecated가 표시되고, 프론트엔드에서 코드 생성 시 해당 필드에 deprecated 표시가 나타납니다. 이를 통해 프론트엔드 팀에게 곧 해당 필드가 제거될 것임을 미리 알립니다.\n2단계: @Schema(hidden = true) 적용 프론트엔드에서 새로운 스펙으로 마이그레이션이 완료되면, 서버에서 해당 @Deprecated 필드에 @Schema(hidden = true)를 추가하여 더 이상 OpenAPI spec에 해당 필드가 생성되지 않도록 합니다.\npublic class UserDto { @Deprecated @Schema(hidden = true) private String oldField; // spec에서 제외됨 private String newField; } 3단계: 필드 제거 충분한 시간이 지난 후 해당 필드를 완전히 제거합니다.\n이러한 단계적 접근 방식을 통해 프론트엔드와 백엔드 간의 안전한 API 버전 관리가 가능합니다.\n","date":"2023-10-17T17:12:52+09:00","image":"/posts/231017_swagger/featured.png","permalink":"/posts/231017_swagger/","title":"Springdoc과 OpenAPI (어노테이션 활용법)"},{"content":"Swagger란? Swagger는 OAS(OpenAPI Specification)를 위한 프레임워크로, API들이 가지고 있는 스펙을 명세하고 관리할 수 있는 프로젝트입니다. Swagger를 통해 REST API 서비스를 설계, 빌드, 문서화할 수 있습니다.\nSwagger Tools Swagger는 다음과 같은 주요 도구들을 제공합니다\nSwagger UI: Swagger API 명세서를 HTML 형식으로 시각화하여 확인할 수 있도록 해주는 도구 Swagger Codegen: Swagger에 정의된 스펙에 따라 클라이언트 및 서버 코드를 자동으로 생성해주는 CLI 툴 Swagger Editor: Swagger 표준에 따른 API 설계서 및 명세서를 작성하기 위한 에디터 Springfox vs Springdoc Springfox Swagger는 Spring 또는 Spring Boot를 사용하는 프로젝트에서 Swagger를 이용해 API 문서를 쉽게 작성할 수 있도록 도와주는 라이브러리입니다.\nSpringfox가 업데이트를 중단하는 시점에 Springdoc이 등장했으며, 활발한 업데이트가 이루어지면서 급부상하게 되었습니다. Springdoc 역시 Swagger 문서 작성을 지원하는 라이브러리로, 현재는 Springfox보다 더 권장되는 선택입니다.\nSwagger Codegen vs OpenAPI Generator Swagger는 SmartBear사의 트레이드마크이며, Swagger Codegen은 그 안에 포함되어 있는 프로젝트입니다.\nOpenAPI Generator는 Swagger Codegen 프로젝트를 포크(fork)하여 시작된 커뮤니티 주도 오픈소스 프로젝트입니다. 현재 40명 이상의 상위 프로젝트 기여자와 Swagger Codegen의 창립 멤버들이 함께 참여하고 있습니다.\nOpenAPI Generator License OpenAPI Generator는 Apache License 2.0을 따르고 있습니다.\nApache License 2.0이란? Apache License 2.0에 따라 다음과 같은 권리가 부여됩니다\n누구나 해당 소프트웨어에서 파생된 프로그램을 제작할 수 있음 저작권을 양도, 전송할 수 있음 부분 또는 전체를 개인적 또는 상업적 목적으로 이용 가능 재배포 시 원본 또는 수정한 소스코드를 반드시 포함시키지 않아도 됨 단, Apache License 버전 및 표기는 반드시 포함해야 함 (Apache License로 개발된 소프트웨어임을 명확히 표시) OpenAPI Generator의 탄생 배경 OpenAPI Generator의 공식 Q\u0026amp;A를 보면 프로젝트의 탄생 배경을 확인할 수 있습니다\n버전 철학의 차이 Swagger Codegen의 창립 멤버들은 Swagger Codegen 3.0.0이 2.x의 철학과 너무 많이 다르다고 느꼈습니다. 두 개의 개별 브랜치(2.x, 3.x)를 유지 관리하는 오버헤드가 Python 커뮤니티에서 겪었던 것과 유사한 문제를 야기할 수 있다고 우려했습니다.\n빠른 배포 주기 창립 멤버들은 사용자가 원하는 안정적인 배포 버전을 사용하기 위해 몇 달을 기다리지 않도록 더 빠른 배포 주기를 원했습니다.\n주간 패치 배포 (Weekly patch releases) 월간 마이너 배포 (Monthly minor releases) 커뮤니티 주도 개발 커뮤니티가 주도하는 오픈소스 프로젝트 형식으로 진행하면 혁신과 신뢰성, 그리고 커뮤니티가 소유하는 로드맵을 확보할 수 있습니다.\n위와 같은 이유들로 OpenAPI Generator 프로젝트가 탄생했습니다. OpenAPI Generator는 MySQL과 MariaDB의 관계와 유사한 느낌입니다.\nSwagger Codegen에서 마이그레이션 공식 문서에 따르면, 기존에 Swagger Codegen 2.x 버전을 사용하고 있을 경우 OpenAPI Generator로 편리하게 마이그레이션할 수 있습니다. OpenAPI Generator는 Swagger Codegen 2.4.0-SNAPSHOT 버전을 기반으로 하고 있기 때문입니다.\n자세한 마이그레이션 방법은 공식 가이드를 참조하세요\nMigrating from Swagger Codegen | OpenAPI Generator 참고 자료 OpenAPI Generator 공식 사이트 Migrating from Swagger Codegen ","date":"2023-10-16T16:56:35+09:00","image":"/posts/231016_swagger/featured.png","permalink":"/posts/231016_swagger/","title":"OpenAPI Generator 정복하기"},{"content":"","date":"2022-11-12T20:23:37+09:00","image":"/posts/221112_jenkins_springboot/featured.png","permalink":"/posts/221112_jenkins_springboot/","title":"Jenkins \u0026 Springboot CI/CD 정리 마지막 (4)"},{"content":"","date":"2022-11-11T20:23:37+09:00","image":"/posts/221111_jenkins_springboot2/featured.png","permalink":"/posts/221111_jenkins_springboot2/","title":"Jenkins \u0026 Springboot CI/CD 정리 (3)"},{"content":"","date":"2022-11-11T20:23:30+09:00","image":"/posts/221111_jenkins_springboot/featured.png","permalink":"/posts/221111_jenkins_springboot/","title":"Jenkins \u0026 Springboot CI/CD 정리 (2)"},{"content":"","date":"2022-11-05T20:23:37+09:00","image":"/posts/221105_jenkins_springboot/featured.png","permalink":"/posts/221105_jenkins_springboot/","title":"Jenkins \u0026 Springboot CI/CD 정리 (1)"},{"content":"","date":"2022-11-03T20:16:58+09:00","image":"/posts/221103_jenkins_sornaqube/featured.png","permalink":"/posts/221103_jenkins_sornaqube/","title":"Jenkins\u0026Sonaqube\u0026Checkstyle 을 이용한 코드컨벤션 적용기(Naver Code Convention)"},{"content":"","date":"2022-10-27T00:00:00+09:00","image":"/posts/221027_jenkins_slack/featured.png","permalink":"/posts/221027_jenkins_slack/","title":"Jenkins \u0026 Slack Notification 연동"},{"content":" Nginx란? Nginx(엔진엑스)는 경량 고성능 웹 서버 소프트웨어입니다. 웹 서버로 동작할 뿐만 아니라 리버스 프록시, 로드 밸런서, HTTP 캐시로도 사용됩니다.\nNginx는 높은 동시 접속 처리를 위해 설계되었으며, 현재 전 세계 수많은 대규모 웹사이트에서 사용되고 있습니다.\n왜 Nginx가 필요했을까? 과거에는 Apache 웹서버가 업계 표준이었습니다. 하지만 2000년대 초반, 인터넷 사용자가 폭발적으로 증가하면서 C10k 문제라는 병목 현상이 발생했습니다.\nC10k 문제 C10k 문제는 \u0026ldquo;Connection 10,000\u0026quot;의 약자로, 하나의 서버에서 동시에 10,000개의 클라이언트 연결을 처리하는 것을 의미합니다.\nℹ️ 중요한 개념 구분\n동시 처리 (Concurrent): 많은 연결을 동시에 유지하고 관리 처리 속도 (Throughput): 초당 처리할 수 있는 요청의 개수 동시 접속 처리는 빠른 속도보다는 효율적인 자원 관리와 스케줄링이 핵심입니다.\nApache의 구조적 한계 기존 Apache는 다음과 같은 구조적 문제를 가지고 있었습니다:\n1. 프로세스 기반 처리 요청이 들어올 때마다 새로운 프로세스 또는 스레드를 생성 사용자가 많아질수록 프로세스 수가 비례 증가 결과적으로 메모리 부족 발생 2. 높은 리소스 소비 Apache의 강력한 확장성 덕분에 다양한 모듈 추가 가능 하지만 각 프로세스가 모든 모듈을 메모리에 로드 프로세스당 메모리 사용량 증가 3. Context-Switching 오버헤드 CPU 코어가 여러 프로세스를 번갈아 실행 프로세스 전환 시 Context-Switching 비용 발생 요청이 많을수록 CPU 오버헤드 증가 이러한 문제로 인해 Apache는 대규모 동시 접속 환경에 부적합했습니다.\nNginx의 탄생 2002년, 러시아의 개발자 **이고르 시쇼브(Igor Sysoev)**가 이 문제를 해결하기 위해 Nginx 개발을 시작했고, 2004년 첫 릴리즈를 공개했습니다.\nNginx의 핵심 목표 높은 동시 접속 처리 낮은 메모리 사용량 높은 성능과 안정성 Nginx의 주요 역할 HTTP Server: 정적 파일(HTML, CSS, JS, 이미지)을 빠르게 제공 Reverse Proxy Server: 백엔드 애플리케이션 서버 앞단에서 요청 중계 Load Balancer: 여러 서버로 트래픽 분산 Mail Proxy Server: 메일 서버 프록시 기능 Nginx의 내부 구조 Nginx는 1개의 Master Process와 여러 개의 Worker Process로 구성됩니다.\nMaster Process의 역할 Master Process는 다음 작업을 담당합니다:\n설정 파일 읽기 및 유효성 검증 Worker Process 생성 및 관리 설정 변경 시 Worker Process 재시작 # Master Process 확인 ps aux | grep nginx Worker Process의 역할 Worker Process가 실제 클라이언트 요청을 처리합니다:\n1. 커넥션 관리 Master Process로부터 listen socket 할당받음 클라이언트와 커넥션 형성 Keep-Alive 시간 동안 커넥션 유지 하나의 Worker가 수천 개의 커넥션 동시 처리 2. Non-blocking I/O 커넥션에 요청이 없으면 다른 작업 처리 요청이 들어오면 즉시 응답 비동기 Event-Driven 방식으로 효율적 처리 3. Thread Pool 시간이 오래 걸리는 작업(파일 I/O, DB 쿼리)은 Thread Pool에 위임 Worker Process는 다른 요청 계속 처리 Blocking 작업의 영향 최소화 4. CPU 코어 최적화 Worker Process는 CPU 코어 개수만큼 생성 권장 각 Worker를 특정 CPU 코어에 고정 (CPU Affinity) Context-Switching 최소화로 성능 향상 # nginx.conf 설정 예시 worker_processes auto; # CPU 코어 수만큼 자동 생성 worker_cpu_affinity auto; # CPU 친화성 자동 설정 Event-Driven 아키텍처 Nginx는 멀티프로세스 + 싱글스레드 + Event-Driven 방식으로 동작합니다:\n여러 커넥션을 Event Handler가 관리 비동기 Non-blocking 방식으로 처리 먼저 준비된 이벤트부터 순차 처리 대기 중인 프로세스 없이 자원 효율성 극대화 이는 Apache처럼 요청을 기다리며 방치되는 프로세스가 없어 메모리와 CPU를 효율적으로 사용합니다.\nNginx의 장단점 장점 1. 높은 동시 접속 처리 능력 Apache 대비 동시 커넥션 수 10배 이상 증가 동일 커넥션에서 처리 속도 2배 향상 2. 낮은 리소스 사용 적은 수의 프로세스로 동작 메모리 사용량 최소화 경량 구조로 빠른 응답 속도 3. 무중단 설정 리로드 nginx -s reload # 서비스 중단 없이 설정 적용 Master Process가 새 설정 읽기 기존 Worker는 현재 요청 완료 후 종료 새 Worker가 새 설정으로 요청 처리 서비스 중단 없이 설정 변경 가능 4. 우수한 정적 파일 처리 이미지, CSS, JS 등 정적 콘텐츠를 빠르게 제공 Apache보다 정적 파일 처리 성능 우수 단점 1. 동적 모듈 개발의 어려움 모듈 추가 시 Worker Process 재시작 필요 Apache처럼 손쉬운 모듈 개발 어려움 대신 Lua 스크립팅으로 어느 정도 보완 가능 2. Windows 환경 제한 Linux/Unix 환경에 최적화 Windows에서는 성능과 안정성 저하 프로덕션 환경에서는 Linux 사용 권장 3. .htaccess 미지원 Apache의 .htaccess 파일 사용 불가 모든 설정을 중앙 설정 파일에서 관리 호스팅 환경에서 유연성 떨어질 수 있음 Nginx의 주요 기능 1. 리버스 프록시 (Reverse Proxy) 리버스 프록시는 클라이언트와 백엔드 서버 사이에서 중계자 역할을 합니다.\n주요 이점 보안 강화: 실제 서버 IP 숨김 캐싱: 자주 요청되는 응답 캐싱 압축: 응답 데이터 압축으로 대역폭 절약 SSL 처리: HTTPS 암호화/복호화 담당 # 리버스 프록시 설정 예시 location / { proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } 실무 활용 패턴 Nginx + Apache: Nginx가 정적 파일 처리, Apache가 동적 처리 Nginx + Node.js/Python/Java: Nginx가 프론트엔드, 백엔드 애플리케이션 보호 Nginx + Nginx: 여러 Nginx 서버를 계층적으로 구성 2. 로드 밸런싱 (Load Balancing) 여러 백엔드 서버로 트래픽을 분산하여 부하를 균등하게 배분합니다.\n로드 밸런싱 알고리즘 Round Robin (기본값) 순차적으로 요청을 각 서버에 분배 가장 간단하고 공평한 방식 upstream backend { server backend1.example.com; server backend2.example.com; server backend3.example.com; } Least Connections 현재 연결 수가 가장 적은 서버로 전송 처리 시간이 다른 요청에 적합 upstream backend { least_conn; server backend1.example.com; server backend2.example.com; } IP Hash 클라이언트 IP 해시값으로 서버 결정 세션 유지(Session Persistence)에 유용 upstream backend { ip_hash; server backend1.example.com; server backend2.example.com; } Weight (가중치) 서버 성능에 따라 가중치 부여 고성능 서버에 더 많은 요청 전달 upstream backend { server backend1.example.com weight=3; server backend2.example.com weight=2; server backend3.example.com weight=1; } Health Check (헬스 체크) upstream backend { server backend1.example.com max_fails=3 fail_timeout=30s; server backend2.example.com max_fails=3 fail_timeout=30s; } max_fails: 실패 허용 횟수 fail_timeout: 서버를 다운으로 간주할 시간 장애 서버 자동 제외로 가용성 향상 3. SSL/TLS 터미네이션 Nginx가 클라이언트와 HTTPS 통신, 백엔드와 HTTP 통신을 담당합니다.\n주요 이점 백엔드 서버의 SSL 처리 부담 제거 중앙화된 인증서 관리 백엔드는 비즈니스 로직에 집중 Nginx와 백엔드는 같은 내부 네트워크에서 HTTP 통신 (보안상 안전) server { listen 443 ssl http2; server_name example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://backend; } } HTTP/2 지원 Nginx는 HTTP/2를 지원하여:\n멀티플렉싱: 하나의 커넥션으로 여러 요청 동시 처리 헤더 압축: 대역폭 절약 Server Push: 클라이언트 요청 전 리소스 전송 4. 캐싱 (Caching) 서버 응답을 메모리나 디스크에 저장하여 반복 요청 시 빠르게 응답합니다.\n# 캐시 경로 설정 proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g; server { location / { proxy_cache my_cache; proxy_cache_valid 200 60m; # 200 응답은 60분 캐싱 proxy_cache_valid 404 10m; # 404 응답은 10분 캐싱 proxy_pass http://backend; } } 캐싱 전략 프록시 캐싱: 백엔드 응답 캐싱 FastCGI 캐싱: PHP-FPM 등 동적 콘텐츠 캐싱 정적 파일 캐싱: 브라우저 캐시 헤더 설정 # 정적 파일 캐시 헤더 설정 location ~* \\.(jpg|jpeg|png|gif|ico|css|js)$ { expires 1y; add_header Cache-Control \u0026#34;public, immutable\u0026#34;; } 5. 압축 (Gzip) 응답 데이터를 압축하여 네트워크 대역폭 절약\ngzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; 텍스트 기반 콘텐츠 60-80% 압축 전송 시간 단축으로 사용자 경험 개선 6. Rate Limiting (속도 제한) DDoS 공격 방어 및 서버 보호\n# Zone 정의 limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; server { location /api/ { limit_req zone=mylimit burst=20 nodelay; proxy_pass http://backend; } } IP당 초당 요청 수 제한 burst: 순간적인 트래픽 증가 허용 API 서버 보호에 필수적 Nginx vs Apache: 어떤 것을 선택할까? Nginx를 선택해야 하는 경우 높은 동시 접속 처리가 필요한 경우 정적 파일 서비스가 주요 목적인 경우 리버스 프록시/로드 밸런서가 필요한 경우 리소스 효율성이 중요한 경우 최신 프로토콜(HTTP/2, HTTP/3) 지원 필요 Apache를 선택해야 하는 경우 .htaccess 파일 기반 설정이 필요한 경우 다양한 써드파티 모듈이 필요한 경우 Windows 환경에서 사용해야 하는 경우 레거시 애플리케이션 호환성이 중요한 경우 동적 모듈 개발이 빈번한 경우 최적의 조합: Nginx + Apache 많은 기업이 Nginx를 프론트엔드, Apache를 백엔드로 사용합니다:\n[클라이언트] → [Nginx] → [Apache] → [애플리케이션] 정적 파일 동적 처리 SSL 처리 PHP/Python 캐싱 모듈 활용 실무 팁 1. Worker Connections 설정 events { worker_connections 1024; # Worker당 처리 가능한 커넥션 수 use epoll; # Linux에서 최적의 이벤트 모델 } 2. Keepalive 최적화 http { keepalive_timeout 65; keepalive_requests 100; } 3. 버퍼 크기 조정 http { client_body_buffer_size 16K; client_header_buffer_size 1k; client_max_body_size 8m; large_client_header_buffers 4 8k; } 4. 로그 최적화 http { access_log /var/log/nginx/access.log combined buffer=32k; error_log /var/log/nginx/error.log warn; } 5. 보안 강화 # 버전 정보 숨기기 server_tokens off; # 보안 헤더 추가 add_header X-Frame-Options \u0026#34;SAMEORIGIN\u0026#34; always; add_header X-Content-Type-Options \u0026#34;nosniff\u0026#34; always; add_header X-XSS-Protection \u0026#34;1; mode=block\u0026#34; always; 마무리 Nginx는 현대 웹 인프라의 핵심 구성 요소로 자리잡았습니다. Event-Driven 아키텍처를 통한 높은 성능과 효율성으로 Netflix, Airbnb, GitHub 등 대규모 서비스에서 사용되고 있습니다.\nApache의 안정성과 확장성도 여전히 가치가 있지만, 대규모 트래픽 처리와 리소스 효율성이 중요한 현대 웹 환경에서는 Nginx가 더 적합한 선택입니다.\n💡 추천 학습 경로\n로컬 환경에서 Nginx 설치 및 기본 설정 실습 리버스 프록시 구성해보기 로드 밸런싱 설정 및 테스트 SSL 인증서 적용 (Let\u0026rsquo;s Encrypt) 성능 모니터링 및 최적화 참고 자료 Nginx 공식 문서 Nginx 설정 생성기 Nginx Performance Tuning Guide 다만 주의할 점: Nginx는 Windows 환경에서 제한적인 성능과 호환성을 보이므로, 프로덕션 환경에서는 반드시 Linux/Unix 시스템을 사용하는 것을 권장합니다!\n","date":"2022-10-25T19:17:04+09:00","image":"/posts/221025_about_nginx/featured.png","permalink":"/posts/221025_about_nginx/","title":"Nginx란 무엇일까? 웹 서버의 진화와 구조"},{"content":" 지난 글에 이어 도커의 명령어와 사용방법을 정리해볼까 합니다 =)\nDocker 설치하기 우선 도커를 사용하려면 설치를 해주어야 겠죠?\n필자의 경우 AWS EC2 인스턴스로 Ubuntu 환경에서 Docker를 설치하였습니다.\nUbuntu 및 다른 환경에서 설치가 필요하신 경우 아래 공식 문서를 참고해주세요!\nInstall Docker Engine on Ubuntu - docs.docker.com\nDocker 구버전 제거 및 신버전 설치 만약 Docker 구버전을 삭제 후 신버전을 설치하려 한다면 아래 명령어를 통해 구버전을 삭제해주도록 합니다.\nUbuntu 기준\nsudo apt-get remove docker docker-engine docker.io containerd runc 저장소 업데이트\nsudo apt-get update apt가 https를 통해 repository를 사용할 수 있도록 패키지 설치\nsudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common Docker 저장소 키를 apt에 등록\ncurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - Docker 저장소 등록\nsudo add-apt-repository \u0026#34;deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\u0026#34; 지금까지 작업 내용 반영을 위해 apt update\nsudo apt-get update Docker 설치\nsudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin 위의 명령어를 통해 Docker 설치 작업을 끝냈다면 확인을 해봐야 겠죠?!\nsudo docker version 또는\nsudo docker run hello-world 위와 같이 run 명령어를 사용하면 hello-world 이미지를 local에서 찾고 만약 없으면 docker hub에서 이미지를 다운받고 실행시켜 컨테이너로 띄워질 것입니다 =)\nDocker 권한 설정 만약 도커 명령어 실행시 다음과 같은 문구가 나온다면 걱정하지 마세요 ㅎㅎ\n이 메세지는 Docker를 root 외의 사용자가 사용할 수 있는 권한이 없어 그런 것 입니다 =)\n우선 docker의 권한을 확인합니다.\ncat /etc/group | grep docker 필자의 경우 이미 사용자 권한을 추가하여 뒤에 ubuntu 라는 사용자 이름으로 권한이 추가되어 있습니다.\n만약 추가가 되어 있지 않다면 docker:x:999: 와 같은 문구를 확인할 수 있을 겁니다!\nDocker 그룹에 사용할 사용자 아이디를 추가해줍니다\nsudo usermod -aG docker [사용자이름] 사용자이름은 예시로 필자는 위의 사진과 같이 ubuntu라는 사용자 이름으로 사용중이기에 ubuntu를 넣어 주었습니다.\nLinux의 경우 기본유저이름이 ec2-user로 잡혀 있을 겁니다 =)\n시스템 재시작을 해줍니다.\nsudo reboot 이제 docker 명령어 앞에 sudo 를 빼고 다시 버전확인을 해보도록 하겠습니다.\ndocker version 다음과 같이 Client와 Server 정보가 나오면 정상적으로 권한 부여가 이루어진겁니다. =)\n만약 Got permission denied ... 메세지가 다시 나온다면 권한 설정이 잘 이루어지지 않을 것이니 확인을 다시 해보신 후 추가적인 에러메세지를 함께 검색해보시길 바랍니다!\n에러메세지를 댓글에 공유 해주시면 저도 함께 찾아보도록 하겠습니다 ㅎㅎ\nWindows \u0026amp; Mac OS Docker Desktop + 추가적으로 Windows 와 Mac OS의 경우 docker desktop을 지원하여 간편한 설치를 통해 GUI를 사용할 수 있습니다!\n개인 사용자나 250인 이하 또는 $1000만 달러 미만 매출의 회사에서만 무료로 사용할 수 있다니 참고 바랍니다!\n설치방법은 아래 공식문서 링크를 참고하세요 =)\nInstall Docker Desktop on Windows - docs.docker.com\nInstall Docker Desktop on Mac - docs.docker.com\nDocker 명령어 Docker Image 검색 Docker 공식 registry인 Docker hub에서 이미지를 검색합니다.\nsudo docker search [검색 할 Image이름] 이미지 다운받기 sudo docker pull [이미지이름]:[태그] 일반적으로 이미지 생성시 태그명을 따로 지정하지 않으면 default 값으로 latest가 붙습니다 =)\n이미지를 docker hub 계정에 push 하기 필자의 경우 좀전에 받은 hello-world 이미지를 필자의 docker-hub 계정에 push 해보도록 하겠습니다.\n먼저 push를 하기에 앞서 docker-hub에 hello-world 라는 repository를 만들어두도록 하겠습니다.\nsudo docker push [docker-hub ID]/[이미지 이름]:[태그] 음\u0026hellip; 다음과 같은 메세지와 함께 이미지 push가 실패하였네요. =(\n이러한 이유는 docker hub의 repository 이름과 로컬의 도커 이미지 repository 이름을 똑같게 해줘야 한다고 합니다.\n해결방법\nsudo docker image tag [이미지 repo 이름]:[태그] [변경할 이미지 repo이름 지정]:[태그] 이 방식은 해당 이미지의 이름을 바꾸는 것이 아닌 이미지를 복사하여 새로운 이름의 이미지를 생성해줍니다. =)\n필자의 경우 tag 부분은 변경할 repo 도 default인 latest를 사용할거기 때문에 따로 지정을 해주지 않았습니다.\n다시 push를 해보도록 하겠습니다 ㅎㅎ.\n이제 정상적으로 이미지가 push 된 것을 확인할 수 있습니다! =)\n다운받은 이미지 확인 sudo docker images Docker image를 실행하여 container로 띄우기 docker run -d -i -t --name [생성할 컨테이너 name 설정] -p [host port:container port] [image name or ID] 필자의 경우 이미 springboot project를 docker image로 빌드 해둔 것이 있어 해당 image를 실행시켜 컨테이너로 띄워보도록 하겠습니다. =)\n일반적으로는 -i -t 옵션을 함께 사용하여 -it 이렇게 옵션을 주기도 합니다 =)\nhost port는 컨테이너가 띄워진 후 사용자가 접근할 외부 port이고 container port는 다음과 같이 docker file을 이용하여 docker image를 빌드할때 지정해준 port라 생각하시면 될 것 같습니다. =)\n필자의 경우 이미지를 생성할 때 .yml 파일에 존재하는 local, dev, prod 환경 중 dev환경으로 지정해주었고 해당 dev 환경의 server port는 8081로 지정해주었기 때문에 containerport를 8081로 지정해주었습니다!\nimage name or ID 는 실행시킬 이미지의 이름 또는 해당 이미지의 아이디 값을 넣어주면 됩니다. =)\nDocker의 다양한 옵션에 대한 내용은 아래에 정리를 해두었으니 참고 바랍니다 !\n이제 이미지를 실행 시켰으니 컨테이너가 잘 띄워 졌는지 확인을 해봐야 겠죠?\n실행중인 컨테이너 확인 sudo docker ps 컨테이너가 실행된 후 http://[public ip]:8080 으로 접속을 하니 서버가 잘 띄워진 것을 확인 할 수 있습니다! =)\n또한 현재 띄워진 서버의 운영 환경이 dockerfile에서 지정해준 dev1 환경이라는 것을 만들어둔 api를 통해 확인할 수 있었습니다. =)\n포트 매핑 실험 그럼 여기서 추가적으로 실험을 하나더 해보도록 하겠습니다. 필자는 아까 말했듯이 docker file에서 dev1으로 운영환경을 지정해주었고 해당 dockerfile을 통해 빌드된 이미지는 내부적으로 server port가 8081 인 이미지 입니다.\n그럼 이 이미지를 실행시킬 때 -p 8080:8080 으로 container port를 8081이 아닌 8080으로 주게되면 어떻게 될까요??\n우선 컨테이너는 정상적으로 띄워졌군요!\n그럼 해당 서버의 ip와 외부접근 port인 8080으로 접근을 시도해보도록 하겠습니다!\n이번에는 컨테이너는 정상적으로 띄워 졌지만 서버는 제대로 작동을 하지 않는 것 같군요 =)\n이렇게 dockerfile에서 설정한 운영환경 지정이 제대로 동작하는 것을 확인 할 수 있습니다!\n기본적인 Docker 명령어 $ sudo docker pull [다운받을 이미지 이름]:[태그] $ sudo docker push [docker-hub ID]/[이미지 이름]:[태그] $ docker images # pull 또는 run을 통해 다운받아 local에 존재하는 image들을 확인할 수 있음 $ docker run -d -i -t --name [생성할 컨테이너 name 설정] -p [host port:container port] [image name or ID] # Docker image를 실행하여 컨테이너로 띄움. 만약 docker hub에 존재하는 공식이미지의 경우 # pull을 미리 하지 않고도 local에 없으면 자동으로 다운받아 실행시켜줌. $ sudo docker ps # 이미지를 실행시켜 컨테이너로 띄워지고 실행중인 컨테이너 항목들을 보여줌 $ sudo docker ps -a # 실행중인 컨테이너 외에 종료된 컨테이너 항목들을 모두 보여줌 $ sudo docker stop [컨테이너이름 or 컨테이너 ID] # 현재 실행중인 컨테이너를 중지시킴 $ sudo docker start [컨테이너 이름 or 컨테이너 아이디] # 종료된 컨테이너를 실행시킴 $ sudo docker restart [컨테이너이름 or 컨테이너 ID] # 실행중인 컨테이너를 재시작함 $ sudo docker rm [컨테이너 이름 or 컨테이너 ID] # 컨테이너를 삭제시킴 # 컨테이너를 삭제시키기 위해선 먼저 컨테이너를 stop 해주어야 합니다 =) # +tip 컨테이너 ID를 입력할 경우 모두 적지 않고 2~3글자만 적어도 됩니다. $ sudo docker rmi [이미지이름 or 이미지 ID] # 이미지를 삭제합니다. # 이또한 마찬가지로 ID를 이용해 삭제할경우 2~3글자만 입력하여도 됩니다 =) $ sudo docker logs [컨테이너이름 or 컨테이너 ID] # 실행한 컨테이너의 로그를 확인할 수 있습니다 $ sudo docker exec -it [컨테이너ID] /bin/bash # 컨테이너 내부 접근 # 종료시에는 $ exit Docker 명령어 옵션 -i : --interactive : 표준입력을 활성화하며, 컨테이너와 연결(attach)되어 있지 않더라도 표준입력을 유지함. 이 옵션을 통해 Bash 명령어를 입력함.\n-t : --tty : TTY(pseudo-TTY)를 사용함. Bash를 사용하려면 이 옵션을 설정해야하고 설정하지 않으면 명령어를 입력할 순 있지만 셸이 표시되지 않음.\n-d : --detach : Detached 모드로 데몬 모드라고 부릅니다. 컨테이너가 백그라운드로 실행됩니다.\n-p : --publish : 호스트와 컨테이너 포트를 연결합니다. (포트포워딩) ex) -p 80:80\n\u0026ndash;privileged : 컨테이너 안에서 호스트의 리눅스 커널 기능(Capability)을 모두 사용합니다. 이를통해 호스트의 주요 자원에 접근할 수 있음\n\u0026ndash;rm : 프로세스 종료시 컨테이너 자동 제거\n\u0026ndash;restart : 컨테이너 종료 시, 재시작 정책을 설정합니다.\n-v : --volume : 데이터 볼륨설정으로 호스트와 컨테이너의 디렉토리를 연결하여, 파일 설정등을 호스트에서 변경하면 컨테이너 내부도 동일하게 변경사항이 적용됩니다. 싱크의 개념.\n-u : --user : 컨테이너가 실행될 리눅스 사용자 계정 이름 또는 UID를 설정합니다.\nex) --user ubuntu -e : --env : 컨테이너 내부에서 사용할 환경변수를 설정합니다. 일반적으로 설정값이나 비밀번호를 전달할 때 사용합니다.\n\u0026ndash;link : 컨테이너끼리 연결합니다. [컨테이너명:별칭]\nex) --link \u0026quot;mysql:mysql\u0026quot; -h : --hostname : 컨테이너의 호스트 이름을 설정합니다.\n-w : --workdir : 컨테이너 안의 프로세스가 실행될 디렉토리를 설정합니다.\n-a : --attach : 컨테이너에 표준입력(stdin), 표준출력(stdout), 표준에러(stderr)를 연결합니다.\n-c : --cpu-shares : CPU 자원 분배 설정입니다. 기본값은 1024이고 각 값은 상대적으로 적용됩니다.\n-m : --memory : 메모리 한계를 설정합니다.\nex) --memory=\u0026quot;100m\u0026quot; \u0026ndash;gpus : 컨테이너에서 호스트의 NVIDIA GPU 를 사용할 수 있도록 설정합니다. 이 방식을 사용하기 위해선 호스트는 NVIDIA GPU가 장착된 Linux 서버 + NVIDIA driver 설치 완료 + docker 19.03.5 버전 이상이여야 합니다\n--gpus all : GPU 모두 사용하기 --gpus \u0026quot;device=0.1\u0026quot; : GPU 지정하여 사용 \u0026ndash;security-opt : SELinux, AppArmor 옵션을 설정합니다.\n--security-opt=\u0026quot;label:level:TopSecret\u0026quot; ","date":"2022-10-25T17:59:31+09:00","image":"/posts/221025_docker_command/featured.png","permalink":"/posts/221025_docker_command/","title":"도커(Docker) 설치 \u0026 명령어 사용방법 총정리"},{"content":" Docker는 오픈소스 컨테이너화 플랫폼으로, 코드와 의존성을 패키징하여 다양한 컴퓨팅 환경에서 애플리케이션을 빠르고 안정적으로 실행할 수 있게 해줍니다. 🐳 Docker란? Docker의 핵심 개념은 크게 두 가지입니다: 컨테이너(Container) 와 이미지(Image)\nDocker Image (도커 이미지) 💡 Docker Image는 애플리케이션 실행에 필요한 코드, 런타임, 시스템 도구, 시스템 라이브러리, 설정 등을 포함하는 경량의 독립적인 소프트웨어 패키지입니다. 실제 사용 예시 기존 방식으로 Linux에 Jenkins를 설치한다면:\n$ sudo apt-get install jenkins 위 명령어를 실행하면 여러 의존성 패키지들을 함께 다운로드해야 합니다.\n반면 Docker를 사용하면:\n$ docker pull jenkins/jenkins:lts 필요한 모든 구성 요소가 포함된 사전 구성된 이미지를 한 번에 다운로드할 수 있습니다.\n📦 Docker Registry \u0026amp; Docker Hub ℹ️ Docker Registry는 Docker 이미지를 공유하는 저장소 역할을 합니다. \u0026ldquo;Docker용 GitHub\u0026quot;라고 생각하면 쉽습니다. Docker Hub는 공식 Docker 레지스트리로, 벤더가 제공하는 공식 이미지들을 제공합니다.\n동작 흐름 사용자가 레지스트리에서 이미지를 다운로드 이미지를 컨테이너로 실행 하나의 컴퓨터에서 여러 개의 격리된 환경 구성 가능 🔄 Container Virtualization (컨테이너 가상화) ✅ 컨테이너 기술은 \u0026ldquo;하나의 시스템 내에서 여러 개의 격리된 인스턴스를 실행할 수 있게 하는 서버 가상화 방식\u0026quot;으로, 각 컨테이너는 사용자에게 개별 서버처럼 보입니다. 중요한 점: 컨테이너는 Docker만의 전유물이 아닙니다. OpenVZ, Libvirt, LXC 등 다양한 컨테이너 기술이 존재합니다.\n🖥️ 가상화 방식의 종류 1. Host Virtualization (호스트 가상화) 구조: Guest OS가 Host OS 위에서 가상화 소프트웨어를 통해 실행됩니다.\n예시: VM Workstation, VMware Player, VirtualBox 등 📝 장점:\n설치 및 구성이 간단함 하드웨어 에뮬레이션으로 최소한의 호스트 요구사항 단점:\nOS 위에 OS를 실행하므로 리소스 집약적 성능 오버헤드가 큼 2. Hypervisor Virtualization (하이퍼바이저 가상화) 구조: Host OS 없이 하드웨어에 직접 소프트웨어를 설치하여 실행합니다.\n하이퍼바이저 가상화의 두 가지 접근 방식:\n1) Full Virtualization (전가상화) Guest OS가 하드웨어에 직접 접근하지 않고 하이퍼바이저를 통해 접근 더 안정적이지만 성능 오버헤드 존재 2) Paravirtualization (반가상화) Guest OS가 하이퍼바이저를 통해 하드웨어에 직접 접근 더 빠르지만 OS 수정 필요 📝 장점:\nHost OS가 없어 더 효율적 리소스를 더 효과적으로 활용 단점:\n시작 시간이 느림 각 VM이 독립적인 OS를 실행하므로 여전히 리소스 소모가 큼 3. Container Virtualization (컨테이너 가상화) ⭐ 구조: 애플리케이션들이 호스트 OS 커널을 공유하면서도 격리된 환경을 유지합니다.\n📝 장점:\n경량: 일반적으로 수십 MB (VM은 수십 GB) 빠른 시작 속도: 별도의 OS 부팅이 필요 없음 적은 리소스 사용: 시스템 리소스를 효율적으로 활용 높은 밀도: 같은 하드웨어에서 더 많은 컨테이너 실행 가능 단점:\n호스트 시스템과 동일한 OS 환경이 필요함 크로스 플랫폼 배포가 어려울 수 있음 (예: Linux 컨테이너는 Linux 호스트 필요) 📊 가상화 방식 비교 구분 호스트 가상화 하이퍼바이저 가상화 컨테이너 가상화 용량 수십 GB 수십 GB 수십 MB 시작 속도 느림 느림 매우 빠름 리소스 사용 높음 중간 낮음 격리 수준 높음 높음 중간 이식성 낮음 중간 높음 설정 난이도 쉬움 어려움 중간 💡 정리 ✅ Docker 컨테이너 가상화의 핵심 가치:\n효율성: 기존 가상화 방식보다 훨씬 적은 리소스로 동일한 기능 제공 속도: 애플리케이션을 몇 초 안에 시작하고 중지 가능 일관성: 개발, 테스트, 프로덕션 환경에서 동일하게 실행 확장성: 필요에 따라 컨테이너를 쉽게 추가하거나 제거 Docker는 현대적인 애플리케이션 개발과 배포의 핵심 도구로, DevOps와 마이크로서비스 아키텍처의 기반이 되고 있습니다.\n","date":"2022-10-24T00:00:00+09:00","image":"/posts/221024_about_docker/featured.png","permalink":"/posts/221024_about_docker/","title":"도커(Docker)란? \u0026 Docker Container 그리고 가상화 방식의 종류"},{"content":" 사설망과 공중망, 어디선가 들어는 보았지만 개념은 잘 몰랐기에 정리를 해보고자 합니다. 사설 IP와 공인 IP의 관계\nℹ️ 위 그림을 처음 보면 잉? 하겠지만 글을 모두 읽고 나면 아! 하면서 어느 정도 이해를 하실 수 있을 겁니다. 📅 2011년, IPv4 주소 고갈 선언 인터넷 주소 관리기구인 IANA(Internet Assigned Numbers Authority)에서는 더 이상의 IPv4의 할당은 없을 것이라고 선언을 하였습니다. IPv4는 대략 43억 개의 한정된 주소를 사용할 수 있는데 반해 인터넷의 수요가 빠르게 증가하여 각 대륙에 할당한 IPv4가 동이 나버린 거죠.\n💡 IANA(Internet Assigned Numbers Authority)는 인터넷 할당 번호 관리기관의 약자로 IP 주소, 최상위 도메인 등을 관리하는 단체입니다. 현재 ICANN이 관리하고 있습니다. 그런데 어떻게 아직도 IPv4를 사용할까? 그럼 현재 2022년, 무려 11년 전에 동이 나버린 IPv4임에도 불구하고 현재에도 우리는 IPv4를 잘 사용하고 있습니다. 이게 어떻게 된 걸까요?\n이 때문에 IPv6가 오래전 개발되어 조금씩 상용화되고 있습니다. 그럼에도 IPv4의 사용이 훨씬 더 많을 텐데 어떻게 11년이 지난 지금까지 잘 유지를 하고 있는 걸까요?\n✅ 이러한 이유는 사설망(Private Network) 덕분입니다. 🔌 사설망(Private Network)이란? 사설망은 IPv4 중 특정 대역을 공인 인터넷이 아닌 가정, 기업 등의 한정된 공간에 사용한 네트워크를 의미합니다. 사설망에 소속된 IP인 사설 IP 대역은 오로지 사설망(내부망)에서만 사용이 가능하기 때문에 공인망(외부망, 인터넷)에선 사용을 할 수 없습니다.\n사설 IP 대역\n🌐 공인 IP란? 인터넷 상에서 서로 다른 PC끼리 통신하기 위해 필요한 IP로써 다음과 같은 용도로 사용됩니다:\n홈페이지 서버 구축 PC 인터넷 연결 인터넷을 통한 통신 ✅ 공인 IP는 각 나라마다 관리하는 기관이 있는데, 우리나라는 한국 인터넷진흥원(KISA)에서 관리하고 있습니다. 공인 IP 주소 체계\n💡 개념 정리 ℹ️ 사설망은 가정 또는 기업 등의 한정된 공간에서만 사용이 가능합니다. 그럼 우리가 같은 사설망을 사용하지 않는 다른 PC들과 통신하기 위해선 어떻게 해야 할까요?\n공인 IP 가 필요합니다!\n즉, 사설망에서 공인 인터넷 통신을 하기 위해선 특별한 조치가 필요합니다. 사설 IP는 사설망에서만 사용하도록 규정이 되어 공인 인터넷에서는 사설 IP를 사용할 수 없는 거죠.\n🔄 NAT (Network Address Translation) 이에 IP를 변환하기 위한 방법으로 고안된 것이 네트워크 주소 변환(NAT: Network Address Translation) 입니다.\n💡 NAT란?\nIP 패킷의 TCP/UDP 포트 숫자와 소스 및 목적지의 IP 주소 등을 재기록하면서 라우터를 통해 네트워크 트래픽을 주고받는 기술을 말합니다. 패킷에 변화가 생기기 때문에 IP나 TCP/UDP의 체크섬(checksum)도 다시 계산되어 재기록해야 합니다.\nNAT를 이용하는 이유는 대개 사설 네트워크에 속한 여러 개의 호스트가 하나의 공인 IP 주소를 사용하여 인터넷에 접속하기 위함입니다.\n즉, 사설망에서 공인망으로, 공인망에서 사설망으로 통신하고자 할 때 공인망/사설망에서 사용하는 IP로 변환하는 것을 의미합니다. 위 설명에 따르면 IP 패킷의 TCP/UDP 포트 숫자를 변환한다고 하는 것은 실제로 NAT의 의미가 IP 주소뿐만 아니라 Port까지 변환시켜 사용하는 것을 포함하기 때문이라고 하네요!\nPAT 또는 NAPT (Port Address Translation)이라고 부릅니다.\n📡 공유기의 기능 요즘 대부분의 모든 집이 공유기를 (예: iptime, olleh 등) 설치하고 사용하고 있죠.\n이 공유기에는 다양한 기능이 존재합니다.\n1. DHCP 서버 기능 일단 하나의 공유기를 통해 연결된 다양한 기기에 IP를 할당해주는 DHCP(Dynamic Host Configuration Protocol) 서버의 기능이 존재합니다.\n💡 동적 호스트 구성 프로토콜(DHCP)\nDHCP는 호스트 IP 구성 관리를 단순화하는 IP 표준입니다. DHCP 서버를 사용하여 IP 주소 및 관련된 기타 구성 세부 정보를 네트워크의 DHCP 사용 클라이언트에게 동적으로 할당하는 방법을 제공합니다.\n이를 통해 공유기에 연결한 집 내부의 스마트 기기 및 PC는 각각의 사설 IP를 할당받게 됩니다.\n⚠️ 왜 사설 IP를 할당받는 걸까요?\n맨 처음 설명으로 돌아가 보면 이해가 되겠죠\u0026hellip;?!\nIP 할당 개수는 한정되어 있기 때문에 집집마다, 아니 모든 기기마다 공인 IP를 할당할 수 없으니 사설 IP를 할당하여 사설망을 구축하는 거죠! 이렇게 사설망을 구축하여 내부적으로는 통신이 가능하지만 아직 외부 인터넷과는 통신을 할 수 없습니다.\n2. NAT 기능 그래서 공유기는 NAT 기능을 갖추고 있습니다.\n사설 IP를 공인 IP로 변환하는 기능 매핑 테이블을 자체 구축하여 NAT 테이블로 변환 전과 변환 후 값을 관리 ✅ 물론 공유기가 자체적으로 공인 IP를 갖고 있는 것은 아닙니다! 공유기는 인터넷 업체(KT, SKT, LG 등)에서 제공받은 공인 IP 대역을 사용하는 것이죠! 🛡️ VPN(Virtual Private Network)이란? 그럼 더 나아가 우리가 사용은 해봤을 수 있지만 정확히 어떤 역할을 하는지 잘 모르는 VPN에 대해 알아봅시다!\nVPN은 가상 사설망으로 이름 그대로 사설망이지만 가상인 사설망을 말합니다. 🔥 원래 알고 있던 VPN은 뭔가 IP를 변경하거나 IP를 속여 불법\u0026hellip; 🤔\n그런 건 줄 알고 있었지만 반은 맞고 반은 틀린 내용입니다!\nVPN의 진짜 의미 VPN은 외부에 있는 컴퓨터에서 내부 네트워크(사설망)에 접속해 있는 것처럼 사용할 수 있는 것을 말합니다.\nVPN을 사용하였을 때 IP가 변경되는 이유도 위의 사설망/공중망을 잘 생각해 보면 이해할 수 있습니다.\n✅ VPN을 통해 내부 네트워크(사설망) 안에 접속을 하였기 때문에 IP가 변경되는 것이죠! 💼 VPN 활용 사례 1. 재택근무/원격 근무 이를 통해 사설 네트워크가 구축되어 있는 회사에서 VPN 서버 설정을 해주고 외부 공인 IP 주소와 설정된 아이디/비밀번호를 통해 어디에서든 회사의 사설망에 접근을 할 수 있는 거죠.\n2. 원격 컴퓨터 접속 또한 개인 컴퓨터도 마찬가지로 VPN 설정을 통해 외부 공인 IP 주소만 알아놓고 있으면 어디에서든 VPN을 통해 서울에 있는 내 컴퓨터를 제주도에서도 접근할 수 있습니다.\n3. 지역 제한 우회 어떤 한 나라의 사이트에서 우리나라 IP로 접속하는 것을 차단했다고 하였을 때 우리는 그 사이트에 접속을 할 수 없습니다. 이 사이트에 접속을 하기 위해서 우리나라가 아닌 다른 나라의 IP 주소로 접근을 해주어야 하는데 이때 VPN을 통해 다른 나라의 내부 네트워크에 접속한 것처럼 하여 차단된 방화벽을 통과할 수 있습니다.\n4. 방화벽 우회 메커니즘 ⚠️ 가상 시나리오\n회사에서 내부 규칙으로 회사 근무 중에 SNS에 접속하지 못하도록 차단하였다고 하였을 때 우리는 집에 구축해둔 VPN 또는 해외 VPN을 통해 접속을 해줍니다. 그럼 SNS에 접속을 할 수 있게 됩니다.\n왜 이러는 걸까요?\nVPN을 연결하는 순간 가상의 터널이 형성되고 터널 간의 통신을 위해 보내는 패킷들을 잘게 나누고 암호화와 캡슐화를 합니다. 이때 회사의 방화벽을 거치게 되지만 암호화/캡슐화된 패킷이기 때문에 내가 VPN을 통해 접속하려는 곳이 SNS인지 알아차리지 못하기 때문에 패킷을 그대로 통과시키게 됩니다.\nVPN 터널링 구조\n📋 VPN 정리 👍 장점 ✅ 🔒 데이터 보안 확보 🔒 온라인 프라이버시 보호 📍 IP 주소 변경 🛡️ 신변 보호 🚀 대역폭 제한 방지 👎 단점 VPN은 위와 같이 여러 장점도 존재하지만 단점도 존재합니다.\n⚠️ 🐢 VPN에 접속한 장비는 VPN 서버와 암호화 통신을 해야 하기 때문에 네트워크 속도가 매우 느립니다 ⚠️ 신뢰성이 낮은 일부 VPN도 존재합니다 💰 보안성이 높은 VPN 사용 시에는 비용을 지불해야 합니다 🚫 일부 국가에서는 이용이 불가능합니다 ","date":"2022-10-05T17:34:36+09:00","image":"/posts/221005_about_ip/featured.jpg","permalink":"/posts/221005_about_ip/","title":"사설IP/공인IP? 사설망/공중망? VPN?"},{"content":"Dispatcher Servlet이란? 디스패처 서블릿의 dispatch는 \u0026ldquo;보내다\u0026quot;라는 뜻을 가지고 있다. 이러한 단어의 뜻을 내포하는 디스패처 서블릿은 **HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러(Front Controller)**라고 정의할 수 있다.\n동작 개요 좀 더 자세한 절차는 다음과 같다:\n클라이언트로부터 어떠한 요청이 들어오면 Tomcat과 같은 서블릿 컨테이너가 요청을 받게 된다 이 모든 요청을 프론트 컨트롤러인 디스패처 서블릿이 가장 먼저 받게 된다 디스패처 서블릿은 공통적인 작업을 먼저 처리한 후에 해당 요청을 처리해야 하는 컨트롤러를 찾아서 작업을 위임한다 Front Controller 패턴 여기서 Front Controller라는 용어는 주로 서블릿 컨테이너의 제일 앞에서 서버로 들어오는 클라이언트의 모든 요청을 받아서 처리해주는 컨트롤러로써, MVC 구조에서 함께 사용되는 디자인 패턴이다.\nDispatcher Servlet의 동작 방식 디스패처 서블릿은 가장 먼저 요청을 받는 Front-Controller이다.\n**서블릿 컨텍스트(Web Context)**에서 필터들을 지나 **스프링 컨텍스트(Spring Context)**에서 디스패처 서블릿이 가장 먼저 요청을 받게 된다 디스패처 서블릿은 적합한 컨트롤러와 메소드를 찾아 요청을 위임해야 하며 동작 방식은 아래와 같다.\n상세 동작 과정 1. HTTP Request가 Filter를 거쳐 Dispatcher Servlet이 받는다 2. 요청 정보를 확인하고 위임할 Controller를 찾는다 HandlerMapping의 구현체 중 하나인 RequestMappingHandlerMapping은 @Controller로 작성된 모든 컨트롤러 빈을 파싱하여 HashMap으로 (요청정보, 처리대상)을 관리한다.\n요청에 매핑되는 컨트롤러와 해당 메소드 등을 갖는 HandlerMethod 객체를 찾는다. 그렇기 때문에 HandlerMapping은 요청이 오면 HTTP Method, URI 등을 사용해 Key 객체인 요청 정보를 만들고, Value인 요청을 처리할 HandlerMethod를 찾아 HandlerMethodExecutionChain으로 감싸서 반환한다.\n이렇게 감싸는 이유는 컨트롤러로 요청을 넘겨주기 전에 처리해야 하는 인터셉터 등을 포함하기 위함이다.\n3. Controller로 위임해줄 HandlerAdapter를 찾아 전달한다 디스패처 서블릿은 컨트롤러로 요청을 직접 위임하는 것이 아닌 HandlerAdapter를 통해 컨트롤러로 요청을 위임한다.\nHandlerAdapter 인터페이스를 거치는 이유는 컨트롤러를 구현하는 방식이 다양하기 때문이다. @Controller에 @RequestMapping 관련 어노테이션을 사용해 컨트롤러 클래스를 주로 작성하지만, Controller 인터페이스를 구현하여 컨트롤러 클래스를 작성할 수도 있다.\n그렇기 때문에 스프링은 HandlerAdapter라는 인터페이스를 통해 어댑터 패턴을 적용함으로써 컨트롤러의 구현 방식에 상관없이 요청을 Controller에 위임할 수 있는 것이다.\n4. HandlerAdapter가 Controller로 요청을 위임한다 HandlerAdapter가 Controller로 요청을 넘기기 전에 공통적인 전/후 처리 과정이 필요하다.\n대표적으로:\n인터셉터 처리 요청 시 @RequestParam, @RequestBody 등을 처리하기 위한 ArgumentResolver 응답 시 ResponseEntity의 Body를 JSON으로 직렬화하는 등의 처리를 하는 ReturnValueHandler 이러한 처리들이 어댑터에서 컨트롤러로 전달되기 전에 처리된다. 그리고 컨트롤러의 메소드를 호출하도록 요청을 위임한다.\n5. Business Logic을 처리한다 Controller는 서비스를 호출하고 비즈니스 로직을 진행한다.\n6. Controller가 반환값을 return한다 ResponseEntity 또는 View 이름을 반환한다.\n7. HandlerAdapter가 return값을 처리한다 HandlerAdapter는 컨트롤러로부터 받은 응답을 응답 처리기인 ReturnValueHandler가 후처리한 후에 디스패처 서블릿으로 돌려준다.\n컨트롤러가 ResponseEntity를 반환하면 → HttpEntityMethodProcessor가 MessageConverter를 사용해 응답 객체를 직렬화하고 응답 상태(HttpStatus)를 설정 View 이름을 반환하면 → ViewResolver를 통해 View를 반환 8. 서버의 응답을 클라이언트에게 전달한다 DispatcherServlet을 통해 반환되는 응답은 다시 Filter를 거쳐 클라이언트에게 반환된다.\n","date":"2022-09-27T23:10:15+09:00","image":"/posts/220927_dispatcher/featured.png","permalink":"/posts/220927_dispatcher/","title":"Spring Dispatcher Servlet의 이해"},{"content":"JVM의 구성요소 1. 클래스 로더 (Class Loader) JVM의 Class Loader는 javac에 의해 변환된 바이트코드 파일인 *.class 파일을 Runtime Data Areas에 로딩하여 프로그램을 구동한다.\n💡 Class Loader의 로딩은 런타임에 일어나는데, 클래스에 처음 접근될 때 일어난다. 이를 통해 Lazy Loading Singleton이 구현되기도 한다.\nClass Loading 시간엔 Thread-safe 하다. 2. 실행 엔진 (Execution Engine) Class Loader가 Runtime Data Areas에 불러온 바이트 코드를 실행한다. 바이트 코드를 기계어로 변경해 명령어 단위로 실행하는데, 1바이트의 OpCode와 피연산자로 구성이 된다.\n주요 구성요소 인터프리터 (Interpreter) 컴파일러 (Just-in-Time) 3. 가비지 콜렉터 (Garbage Collector) Heap 영역에 참조되지 않는 오브젝트를 제거하는 역할을 한다.\n자바 이전에는 프로그래머가 모든 프로그램의 메모리를 관리했다. 자바에서는 JVM이 가비지 컬렉션이라는 프로세스를 통해 프로그램 메모리를 관리한다.\nℹ️ 가비지 컬렉션은 자바 프로그램에서 사용되지 않는 메모리를 지속적으로 찾아내서 제거하는 역할을 한다. 4. 런타임 데이터 영역 (Runtime Data Areas) OS로부터 할당받은 JVM의 메모리 영역이다. 자바 어플리케이션을 실행하는데 필요한 데이터를 담는다.\nRuntime Data Areas는 아래와 같이 5개의 영역으로 나뉘어 진다.\nℹ️ 공유 영역\nMethod와 Heap 영역은 모든 Thread가 공유 Thread별 영역\nStack, PC Register, Native Method 영역은 각 Thread 마다 존재 (1) Method Area JVM이 시작될 때 생성되고 JVM이 읽은 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드 및 메서드 코드, 정적 변수, 메서드의 바이트 코드 등을 보관한다.\n💡 Non-Heap 영역으로 Permanent 영역에 저장이된다. JVM 옵션 중 PermSize(Permanent Generation의 크기)를 지정할 때 고려해야 할 요소이다. 1-1 Type Information Interface 여부 패키지 명을 포함한 Type 이름 Type의 접근 제어자 연관된 Interface 리스트 1-2 Runtime Constant Pool Type, Field, Method로의 모든 레퍼런스를 저장 JVM은 Runtime Contant Pool을 통해 메모리 상 주소를 찾아 참조한다. 1-3 Field Information Field의 타입 Field의 접근 제어자 1-4 Method Information Constructor를 포함한 모든 Method의 메타데이터를 저장 Method의 이름, 파라미터 수와 타입, 리턴 타입, 접근 제어자, 바이트코드, 지역 변수 section의 크기 등을 저장 1-5 Class Variable static 키워드로 선언된 변수를 저장 기본형이 아닌 static 변수의 실제 인스턴스는 Heap 메모리에 저장 (2) Heap Area new 연산자로 생성된 객체를 저장하는 공간이다.\nℹ️ 참조하는 변수나 필드가 존재하지 않으면 **GC(Garbage Collector)**의 대상이 된다. (3) Stack Area Thread마다 별개의 Frame으로 저장하며, 저장되는 요소는 아래와 같다.\n3-1 Local Variable Area 지역변수, 매개변수, 메소드를 호출한 주소 등 Method 수행 중 발생하는 임시데이터를 저장한다. 4바이트 단위로 저장되며, int, float 등 4바이트 기본형은 1개의 셀, double 등 8바이트의 기본형은 2개의 셀을 차지한다. bool은 일반적으로 1개의 셀을 차지한다. 3-2 Operand Stack Method의 workspace 이다. 어떤 명령을 어떤 피연산자로 수행할 지 나타낸다. 3-3 Frame Data Constant Pool Resolution, Method Return, Exception Dispatch 등을 포함한다. 참조된 Exception의 테이블도 가지고 있다. Exception이 발생하면 JVM은 이 테이블을 참고하여 어떻게 Exception을 처리할 지 정한다. (4) PC Register Thread가 시작될 때 생성되며 생성될 때마다 생성되는 공간으로, 스레드마다 하나씩 존재한다.\nThread가 어떤 부분을 어떤 명령으로 실행해야할 지에 대한 기록을 하는 부분으로 Thread가 현재 실행하고 있는 부분의 주소를 갖는다.\n💡 OS는 PC(Program Counter) Register를 참고하여 CPU 스케줄링 시 해당 Thread가 다음에 어떤 명령어를 수행해야 하는지 알 수 있다. (5) Native Method Stack 자바 프로그램이 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다.\nJava가 아닌 다른 언어로 작성된 코드를 위한 공간이다. Java Native Interface를 통해 바이트 코드로 전환하여 저장하게 된다. 일반 프로그램처럼 커널이 스택을 잡아 독자적으로 프로그램을 실행시키는 영역이다. JVM 실행 순서 메모리 할당\n프로그램이 실행되면 JVM은 OS로부터 이 프로그램을 실행하는데 필요한 메모리를 할당받음 JVM은 이 메모리를 여러 영역으로 나누어 사용한다 컴파일\nJava Compiler(javac)가 *.java 파일을 컴파일하여 *.class 인 자바 바이트코드로 변환시킨다 클래스 로딩\n컴파일된 *.class 파일들을 Class Loader를 통해 JVM 메모리 위에 로딩을 한다 바이트코드 해석\n로딩된 *.class 파일들은 Execution Engine을 통해 기계어로 해석된다 실행 및 관리\n해석된 바이트코드들은 메모리 영역에 배치되어 실질적인 수행을 하게 된다 실행과정 속에서 JVM은 필요에 따라 스레드 동기화나 가비지 컬렉터와 같은 메모리 관리 작업을 수행한다 ","date":"2022-09-23T22:27:28+09:00","image":"/posts/220923_jvm_2/featured.png","permalink":"/posts/220923_jvm_2/","title":"JVM(JavaVirtualMachine) 파헤치기 (2)"},{"content":"문득 Java라는 언어를 공부하면서 JVM에 대한 궁금증이 생겼다. 단순히 작성한 코드를 실행시켜주는 가상컴퓨터 이다 라고만 알고 있었기에 어떻게 동작을하고 하는 역할은 무엇인지 궁금해졌기에 파헤쳐보고자 한다.\nJVM이란? Java Virtual Machine의 줄임말로 Java를 실행시키기 위한 가상컴퓨터 환경을 말한다.\n그럼 JVM이 하는 역할의 무엇일까? Java는 OS에 종속적이지 않다.\n위와 같은 조건을 충족 시키며 작성한 코드가 실행되기 위해선 Java와 OS사이에 무언가가 필요하다.\n그게 바로 JVM이다.\n코드 실행 과정 작성한 소스코드인(원시코드) *.java 를 CPU가 인식하기 위해선 기계어(010101000101\u0026hellip;)로 변환이 이루어져야 한다.\n그럼 *.java 가 바로 기계어로 변환되어 실행이 되는건가\u0026hellip;? 아니다. *.java 파일은 우선 JVM이 인식을 할 수 있도록 java bytecode(*.class)로 변환이 이루어진다.\n이 변환과정은 java 컴파일러에 의해 수행이 되어진다.\n✅ java 컴파일러는 JDK를 설치하면 bin폴더에 존재하는 javac.exe 이다.\njavac 명령어를 통해 .class 파일을 생성할 수 있고 java 명령어를 통해 이 .class파일을 실행시킬 수 있다. 이제 OS에서 실행이 되는건가..? ⚠️ 아니다\u0026hellip;. bytecode는 기계어가 아니므로 OS에서 바로 실행되지 않는다\u0026hellip;! 이때 JVM이 OS가 이 bytecode를 이해할 수 있도록 해석해주는 역할을 한다.\n이러한 JVM의 역할 덕분에 한 번 작성한 Java 코드가 OS에 상관 없이 실행이 될 수 있는 것이다.\n전체 프로세스 *.java → *.class 인 bytecode 형태로 변환 → JIT(Just In Time) 컴파일러를 통해 기계어(binary code)로 변환\nJIT (Just In Time) 컴파일러란? JIT 컴파일 또는 **동적번역(dynamic translation)**이라고 불린다.\nJIT는 인터프리터 방식의 단점을 보완하기 위해 도입되었다.\n프로그램이 실제 실행하는 시점에 기계어로 번역을 한다.\n성능 특징 💡 기계어는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 빠르게 수행이 된다. JIT 컴파일러가 기계어로 컴파일 하는 과정은 바이트 코드를 인터프리팅하는 것보다 훨씬 느리지만 한 번 수행되면 그 이후로는 빠르다 그러나 한 번만 실행되는 코드라면 컴파일을 하지 않고 바로 인터프리팅하는 것이 유리하다 JIT 컴파일러를 사용하는 JVM은 해당 메서드가 얼마나 자주 수행되는지 체크를 하고 일정 정도를 넘을때에만 컴파일을 수행한다.\n인터프리터 방식이란? 인터프리터는 실행 시마다 소스 코드를 한 줄씩 기계어로 번역하는 방식이기 떄문에 실행 속도는 정적 컴파일 언어보다 느리다.\n대표적인 인터프리터 언어 파이썬 (Python) 자바스크립트 (JavaScript) 데이터베이스 언어인 SQL 장단점 구분 설명 장점 프로그램 수정이 간단하다 단점 실행 속도가 컴파일 언어보다 느리다 💡 컴파일러는 소스코드를 번역해서 실행 파일을 만들기 때문에 프로그램에 수정 사항이 발생하면 소스 코드를 다시 컴파일해야 한다.\n프로그램이 작고 간단하면 문제가 없지만 프로그램 덩치가 커지면 컴파일이 시간 단위가 되는 일이 많아진다.\n하지만 인터프리터는 소스코드를 수정해서 실행시키면 끝이기에 수정이 빈번히 발생하는 용도의 프로그래밍에서 많이 사용된다.\n","date":"2022-09-22T22:06:50+09:00","image":"/posts/220922_jvm_1/featured.png","permalink":"/posts/220922_jvm_1/","title":"JVM(JavaVirtualMachine) 파헤치기 (1)"},{"content":"객체지향(OOP)과 절차적 프로그래밍(PP) 객체지향언어와 절차지향언어는 절대 반대되는 개념이 아니다. 그렇다면 객체지향언어와 절차지향언어는 무엇인가?\n우리는 보통 Java, Python, C# 등의 언어를 객체지향 언어라고 부르며 C언어는 절차지향언어라고 부른다. 하지만 어디까지나 이 언어들이 지향하는 것이지 C언어는 절차적 프로그래밍만 가능하고 Java나 Python 등은 객체적 프로그래밍만 가능하다는 것이 아니다.\n어떤 언어를 사용하든 상관없이 절차지향적 프로그래밍을 할 수 있다. 반대로 C언어를 사용하더라도 객체지향적으로 코딩을 할 수 있는 것이다.\n\u0026lsquo;절차지향\u0026rsquo;이라는 용어의 오해 사실 절차지향적 언어라 하는 것은 잘못된 것이다. 모든 프로그래밍 언어가 절차를 기반으로 두고 있는데 절차를 지향한다는 말은 앞뒤가 맞지 않는다.\n하나의 예를 비유하자면:\n역도라는 스포츠는 바벨을 이용한 운동을 기반으로 하는 것인데 바벨을 지향하는 스포츠라고 하는 것과 같은 맥락이다. 그럼 역도를 덤벨로 해야 하나\u0026hellip;? 다시 말해 절차지향이 아닌 절차적 프로그래밍이 맞는 것이다.\n💡 객체지향 프로그래밍(OOP)와 절차적 프로그래밍(PP)는 어디까지나 프로그래밍을 하는데 있어 접근 방식의 차이가 있을 뿐 반대 개념은 아니다! 핵심 차이점 절차적 프로그래밍: 데이터를 중심으로 함수를 만들어 사용 객체지향 프로그래밍: 데이터와 기능(함수)들을 묶어 하나의 객체로 만들어 사용 절차적 언어와 객체지향언어를 구분하는 기준 여러가지 방식이 있겠지만 큰 틀에서는 아래와 같이 나뉜다.\n캡슐화, 다형성, 클래스 상속을 지원하는가? 데이터 접근 제한을 걸 수 있는가? 대게 위 기준을 만족하면 객체지향 성향이 강해진다고 보면 된다.\n절차적 프로그래밍 절차적 프로그래밍은 말 그대로 절차적으로 코드를 구성한다는 것이다.\n데이터에 대한 순서를 파악하고 필요한 기능을 함수로 만들어 절차적(순서대로) 진행시키는 방식\n객체지향 프로그래밍 객체지향 프로그래밍의 경우 기능들을 묶어 하나의 객체로 만든다.\n다시 말하면 각각의 객체를 생성하고 그 객체마다 할 수 있는 행위(기능)들과 데이터를 하나로 묶어주는 것이다.\n예시 자동차 호출 서비스를 구현한다고 가정해보자:\n자동차 객체: 자동차가 할 수 있는 행위(기능)를 하나로 묶음 기사 객체: 기사가 할 수 있는 행위를 묶음 승객 객체: 승객이 할 수 있는 행위를 묶음 각 객체의 메소드나 필드를 호출하면서 서로 간의 상호작용을 통해 알고리즘을 구성하는 방식이다.\n그럼 어떤 방식이 더 좋은가? ℹ️ 정답은 없다. 필요에 맞게 사용을 하고 자신이 선호하는 스타일을 사용하면 된다. 과거의 프로그래밍 과거에는 현재처럼 큰 규모의 하드웨어와 소프트웨어가 필요치 않았다. 오래된 언어인 C, 포트란, 코볼 같은 절차적 언어의 대표라 할 수 있는 언어들이 사용되어졌다.\n현대의 프로그래밍 현대에 들어서면서 점점 소프트웨어 발전이 빨라졌고 이에 따라 코드들도 복잡해져갔다.\n그러다 보니 복잡한 알고리즘들이 꼬이기 시작했고 작성한 코드를 사람이 읽었을 때 이해하기 힘들거나 이해할 수 없는 스파게티 코드가 되어버린 것이다.\n이러한 문제의 대안으로 객체지향적 프로그래밍이 나온 것이다.\n왜 객체지향이 우세한가? 다만 현재 기준 객체지향 프로그래밍이 우세하게 사용되어지고는 있다. 그 이유는:\n복잡한 프로그래밍일수록 절차적 프로그래밍을 사용한다면 코드들이 꼬이기 쉽다 확장성 측면에서도 유지 보수를 할 때 메리트가 떨어진다 절차적 프로그래밍 장단점 장점 객체나 클래스 생성 없이 바로 프로그래밍 필요한 기능을 함수로 만들어 복붙하지 않고 호출하여 사용 프로그램 흐름을 쉽게 추적 단점 각 코드들의 끈끈한 우정 때문에 수정이 힘들다 (유기성이 높아 추가, 수정이 힘듦) 디버그(오류검사)가 힘듦 객체지향 프로그래밍 장단점 장점 모듈화, 캡슐화로 유지보수가 편함 객체지향적으로 현실 세계와 유사성에 의해 코드를 이해하기 쉽다 객체는 그 자체가 하나의 프로그램으로 다른 프로그램에서도 재사용이 가능 단점 대부분의 객체 지향 프로그램은 속도가 상대적으로 느려지고 많은 양의 메모리를 사용하는 경향이 있음 현실세계와 유사성에 의해 코드를 이해하기 쉽게 만들기 위해 설계 과정에 있어 많은 시간이 들어간다 정답은 없다! 적재적소에 맞추어 사용하자 절차적 프로그래밍을 사용하는 경우 보통 프로젝트 규모가 크지 않고 재사용할 일이 크지 않는 경우에 많이 사용된다.\n장점:\n프로그램 자체가 가벼워짐 객체지향으로 만드는 것보다 개발시간과 인력도 줄어듦 객체지향 프로그래밍을 사용하는 경우 큰 규모의 프로젝트에서 코드들을 재사용해야 한다면 초기 개발비용을 제외하고 객체지향 프로그래밍이 적합하다.\n장점:\n유지보수 측면에서 안정적 마무리 ℹ️ 오늘은 이렇게 객체지향 프로그래밍과 절차적 프로그래밍에 대해 알아보았다. 아직은 깊이있는 내용에 대해서는 알지 못하지만 여러 글들을 찾아보며 객체지향과 절차적 프로그래밍에 대한 큰 틀을 이해하고 넘어가고 다음번에 좀 더 깊이있게 들어가보자 한다! ","date":"2022-08-31T20:02:58+09:00","image":"/posts/220831_about_oop/featured.png","permalink":"/posts/220831_about_oop/","title":"객체지향 프로그래밍과 절차적 프로그래밍에 대해 알아보자"}]