<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>0_zoo.log</title>
        <link>https://velog.io/</link>
        <description>https://github.com/Y-Joo</description>
        <lastBuildDate>Sat, 13 Dec 2025 07:33:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>0_zoo.log</title>
            <url>https://velog.velcdn.com/images/0_zoo/profile/46e1dc20-6c28-4677-a047-7b0907f6e832/image.gif</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 0_zoo.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/0_zoo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2025년 회고 - Well Being]]></title>
            <link>https://velog.io/@0_zoo/25%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@0_zoo/25%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 13 Dec 2025 07:33:31 GMT</pubDate>
            <description><![CDATA[<h1 id="well-being">Well Being</h1>
<p>올해의 키워드를 생각해보다가, 문득 Well Being이라는 단어가 떠올랐다.
Well Being이란,, 잘 사는 것, 행복 정도로 직역할 수 있을 것 같다.
올해는 잘 먹고 잘 놀고 잘 마신.. 행복했던 한 해였다.</p>
<p>처음 정규직으로 입사해 좋은 사람들도 많이 만났고, 개인적인 버킷리스트 중 하나였던 동아리의 수장 역할도 해봤다. 여행과 페스티벌도 많이 갔고 한달에 약속을 20개 잡은 달도 있었다.</p>
<p>개발자를 시작한 이래로 가장 여유로웠던 한 해를 회고해보려 한다.</p>
<h1 id="카테고리">카테고리</h1>
<p>올해를 카테고리별 키워드로 정리해봤다.</p>
<ul>
<li>커리어<ul>
<li>졸업</li>
<li>이직</li>
</ul>
</li>
<li>개발<ul>
<li>여비</li>
<li>넥스터즈(회원)</li>
</ul>
</li>
<li>네트워킹<ul>
<li>넥스터즈(CEO)</li>
<li>동기 모임(오토에버)</li>
<li>이어져오던 인연들(소마, YAPP, 대학 친구 등등)</li>
</ul>
</li>
<li>건강<ul>
<li>클라이밍</li>
<li>러닝</li>
<li>몸살</li>
<li>부상</li>
</ul>
</li>
<li>행복도<ul>
<li>여행<ul>
<li>영종도</li>
<li>스키</li>
<li>일본</li>
<li>제주도</li>
<li>대천</li>
<li>호주</li>
</ul>
</li>
<li>페스티벌<ul>
<li>유다빈밴드 단콘</li>
<li>뷰민라</li>
<li>파주</li>
<li>메들리메들리</li>
<li>에픽하이 단콘</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="kpt">KPT</h1>
<h2 id="keep">Keep</h2>
<h3 id="건강">건강</h3>
<p>운동을 많이 시도했고, 많이 했다(러닝, 클라이밍 위주)
몸 자체가 많이 건강해진 느낌이 있어서 좋다.
인생 첫 종합 건강검진을 받았는데, 아직 건강하다는걸 확인해서 다행이다.</p>
<h3 id="커리어">커리어</h3>
<p>오토에버 입사를 했다. 취업 걱정이 많았었는데 운이 좋게도 금방 취업에 성공했다.
초기 창업 &gt; 초중기 스타트업 &gt; 대기업 순으로 경험하게 됐는데, 이 부분도 커리어적인 가치관을 정립하는데 있어 큰 도움이 될 것 같다.</p>
<p>그리고 졸업을 했다... 마침내..</p>
<h3 id="행복도">행복도</h3>
<p>여행을 많이 갔다. 영종도, 일본, 스키여행, 제주도, 대천, 호주, 속초, 대부도 등등..
페스티벌도 많이 갔다. 유다빈밴드 단콘, 뷰민라, 메들리메들리, 파주 평화페스티벌, 에픽하이 단콘(예정) 등등..
작년까지는 행복을 위한 빌드업을 한다는 느낌이 강했는데, 올해 본격적으로 일을 시작하면서 보상심리가 작용한 것 같다. 올해 초 목표가 잘 놀고 잘 먹는거였는데 확실히 이룬 것 같다.</p>
<h3 id="네트워킹">네트워킹</h3>
<p>넥스터즈 26기 참여, 27기 CEO 활동을 하며 새로운 사람들을 많이 만날 수 있었다.
동기들과 최대한 친해지는게 목표였는데, 모임도 많이 가졌고 대부분의 동기들과 친해져서 좋았다.
기존에 잘 지내던 모임들(YAPP, 소마 등등)도 많이 만나서 연을 이어나갈 수 있었다.</p>
<h2 id="problem">Problem</h2>
<h3 id="개발자">개발자</h3>
<p>개발자로서는 유의미한 성장을 한 해는 아니었다. 
위에서 말했듯 보상심리가 있었고, 즐기자는 마인드가 강했다
그래도 젊을때 구르자 라는 마인드가 강했는데, 현실에 안주할까..? 라는 생각을 많이 하게 된 한 해인 것 같다.
YAPP에서 출시한 여비 운영, 넥스터즈에서의 Ziine 출시 등 나름 사이드 프로젝트를 2개 하긴 했지만 개발자로서의 성장보다는 메이커로서 성장한 느낌이다.
입사하게 된 팀도 운영 팀이라서 기대했던 개발을 할 수 있는 환경은 아니라서 아쉬운 점이 여러모로 많다.</p>
<h3 id="건강-1">건강</h3>
<p>운동을 많이 했다고 했는데, 아이러니하게도 가장 자주 아팠던 해였다.
약속을 많이 잡다보니 몸살도 많이 났고, 
러닝을 하다가 다리를 심하게 다쳐 2달 간 뛰지 못했다. 
얼마 전에는 클라이밍을 하다가 강하게 떨어져서 목 근육이 놀라는 바람에 며칠째 목이 아프다.
건강해지려고 운동을 한건데 악바리 근성때문에 무리를 많이 한 것 같다.
술도 많이 마셨다.. 사실 어찌 보면 더 잘 놀고 싶어서 건강해지고 싶은 것 같기도 하다.</p>
<h3 id="넥스터즈">넥스터즈</h3>
<p>26기 회원으로 활동하며 운영진 생각은 있었는데, 전 운영진들과 팀원들의 꼬드김으로 회장직을 하게 됐다.
개인적으로는 동아리에서 할 수 있는 모든 경험을 하고 싶다는 생각에 도전했다.
결과적으로는 하고싶은걸 다 하지는 못한 것 같아서 아쉽다. 일하면서 회장직을 한다는게 얼마나 힘든건지 알 수 있었고, 조금의 변화를 위해 얼마나 많은 리소스 투자가 필요한지 깨달을 수 있었다.</p>
<h2 id="try">Try</h2>
<h3 id="개발자-1">개발자</h3>
<p>새해에는 더 개발자로서 성장할 수 있는 한 해가 되었으면 한다.
생각하고 있는 2개 옵션이 있는데,</p>
<ol>
<li>1년 채우고 스타트업으로 이직하기</li>
<li>현재 팀에서 내년에 진행하는 대규모 프로젝트에 기여하고 2년을 채운 후에 개발 팀으로 옮기기
아마 1번을 우선으로 시도하고 안되면 2번을 시도할 것 같다.</li>
</ol>
<p>사이드 프로젝트도 조금 더 열심히 하려고 한다. 여비 서비스 안드로이드 개발자를 모집하면서 밀렸던 서버 작업을 해야한다. 추가 사이드 프로젝트를 할지는 아직 미지수지만, 몇년동안 그래도 1년에 1개씩은 서비스를 출시했기에 하나는 시작하지 않을까 싶다. 다른 동아리에 참여해볼까 고민중이다.</p>
<h3 id="건강-2">건강</h3>
<p>더 안전하게.. 건강해지고 싶다.</p>
<ol>
<li>보라 클라이머</li>
<li>하프 마라톤 완주</li>
<li>몸무게 6~7키로 찌우기</li>
<li>운동하면서 크게 다치지 않기</li>
<li>술 줄이기
현재는 5가지 목표가 있다. 가장 중요한 건 다치지 않는 것이라고 생각해서, 한두개를 달성하지 못하는 상황이 오더라도 객기 부리지 않고 안전하게 해보려고 한다.</li>
</ol>
<h3 id="행복도-1">행복도</h3>
<p>올해는 충분히 행복한 한 해를 보낸 것 같아서 기분이 좋다.
행복과 통장 잔고는 반비례한다는 것도 잘 배워서.. 내년에는 조금은 덜 행복해도 좋을 것 같다.
개인적으로 내년에 계획하고 있는건</p>
<ol>
<li>지금보다 넓은 전세집으로 옮기기</li>
<li>동남아 혹은 남유럽 여행 가기
두가지 정도 있을 것 같다.</li>
</ol>
<h3 id="네트워킹-1">네트워킹</h3>
<p>올해도 좋은 사람들을 많이 알게 돼서 좋았다.
새롭게 연을 맺은 사람들, 그동안 잘 지내왔던 좋은 사람들과 새해에도 계속 좋은 관계를 유지하고 싶다.
어느 순간 연락이 뜸해져 연락하기 애매한 사람들이 생기지 않았으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[YAPP 23기 회고]]></title>
            <link>https://velog.io/@0_zoo/YAPP-23%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@0_zoo/YAPP-23%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 25 Nov 2024 09:26:17 GMT</pubDate>
            <description><![CDATA[<h2 id="yapp">YAPP</h2>
<p><a href="https://www.yapp.co.kr/">YAPP</a>은 현업자, 재학생 상관없이 모두 모여 4개월 간 사이드 프로젝트를 진행하는 IT 연합 동아리입니다! 보통 4<del>5월, 10</del>11월에 리크루팅을 진행하고 5<del>9월, 11</del>2월까지 활동을 진행하게 됩니다. 비슷한 성격의 동아리로는 넥스터즈, 디프만, DDD, DND 등이 있습니다.
<img src="https://velog.velcdn.com/images/0_zoo/post/32668c0e-8f0c-47dc-8047-5d7607a9448b/image.png" alt=""></p>
<h3 id="yapp은-뭐가-다른가요">YAPP은 뭐가 다른가요?</h3>
<p>크게는 두가지가 있습니다.</p>
<ul>
<li>진행 기간이 2개월인 동아리도 있는 반면 상대적으로 긴 4개월의 참여기간</li>
<li>PM과 함께 프로젝트를 진행할 수 있는 환경</li>
</ul>
<p>물론 다른 동아리도 각자의 장단점이 있고, 성향에 맞는 동아리에 지원하는게 제일 좋습니다! 제가 동아리를 골랐던 기준은 진행기간이 길면서 최대한 많은 직군과 협업할 수 있는가 였기에, YAPP을 선택했습니다.</p>
<h2 id="플레이어참여자와-운영진">플레이어(참여자)와 운영진</h2>
<p>저는 23기에는 플레이어로 활동했고, 24기에는 서버 운영진으로 활동했습니다. 두 경험을 한번에 정리할까 생각했지만, 분리해서 작성하는 게 나을 것 같아 이번 포스트에서는 플레이어 입장, 다음 포스트에서는 운영진 입장에서 글을 작성해보고자 합니다.</p>
<h2 id="지원-과정">지원 과정</h2>
<p>전체적인 지원 과정은 <a href="https://www.yapp.co.kr/recruit">리크루팅 페이지</a>를 참고해주시면 될 것 같습니다.
각 과정에 대한 기억과 생각을 조금 적어보겠습니다.</p>
<h3 id="1-서류-전형">1. 서류 전형</h3>
<p>사실 저는 서류 전형이 제일 힘들었습니다. 생각보다 작성할 내용이 아주 많았고, 어떤 방향으로 답변을 해야 뽑힐 지 고민을 많이 했습니다. 결론적으로 제가 집중했던 부분은</p>
<ol>
<li><strong>서비스 운영을 향한 욕구를 보여주자</strong></li>
<li><strong>서비스를 운영해봤던 경험을 어필하자</strong>
였습니다.</li>
</ol>
<p>이전에 창업을 시도했던 경험과 다수의 프로젝트를 운영하며 유저를 모았던 경험을 어필했고, 동시에 이 과정에서 겪었던 기술적인 부분들을 함께 기재했습니다. 제가 의도했던 부분이 잘 먹혀서 그런지는 모르겠지만, 면접에서도 이와 관련된 내용으로 분위기가 흘러가서 자연스럽게 대답이 가능했던 것 같습니다.</p>
<h3 id="2-면접-전형">2. 면접 전형</h3>
<p>23기 면접은 2대 2로 진행됐습니다(24기 이후부터는 면접관 2대 면접자 1로 진행됩니다). 22기 면접에서 불합격했던 경험이 있기에, 기술적인 부분 준비를 더 많이 했습니다. 기본적인 부분들(데이터베이스, Spring 등등) 준비를 많이 했으나, 질문은 대부분 자기소개서 기반으로 받았습니다. 서류에 적은 경험을 실제로 해본 것인지, 그 과정에서 무엇을 배웠는지 확인하는 질문이 많았고, 경험을 바탕으로 잘 대답할 수 있었습니다.</p>
<h2 id="활동-회고">활동 회고</h2>
<p>활동은 크게 기획(1달), 개발(2달~2달 반), 런칭 및 성과 공유(2주)로 이뤄졌습니다. 부가적으로 스터디도 따로 진행했습니다. 진행한 프로젝트는 <a href="https://disquiet.io/product/%EC%97%AC%EB%B9%84-yeo-bee">여기</a>에서 확인 가능합니다. 혹시 여행에 관심 있으시다면 써보시면 좋을 것 같습니다!</p>
<h4 id="4ls-방식으로-회고를-진행해보고자-합니다-yapp-활동-뿐만-아니라-기간이-끝난-후에도-진행했던-1년간의-프로젝트-활동을-포함한-회고라고-봐주시면-됩니다">4LS 방식으로 회고를 진행해보고자 합니다. YAPP 활동 뿐만 아니라 기간이 끝난 후에도 진행했던 1년간의 프로젝트 활동을 포함한 회고라고 봐주시면 됩니다.</h4>
<h3 id="좋았던-점-liked">좋았던 점 (Liked)</h3>
<ol>
<li><p>운이 좋게도, 정말 좋은 팀원들을 만나 프로젝트를 진행할 수 있었습니다. 모든 팀원의 성격이 너무 좋았고, 프로젝트 방향성을 잘 잡아 현재까지도 약 1년 넘게 프로젝트를 지속하고 있습니다.</p>
</li>
<li><p>생각했던 것보다 많은 유저를 경험할 수 있었습니다. 런칭 후 약 6개월 간 마케팅 없이 2000명 이상의 유저를 경험했고, 피드백을 받으며 서비스를 개선해오고 있습니다.</p>
</li>
<li><p>동아리 활동의 매력을 느낄 수 있었습니다. 다양한 배경의 사람들과 소통하고, 의견을 나누고, 스터디도 진행하면서 개발자로서 그리고 사람으로서 많이 성장할 수 있었습니다. 이 부분 때문에 24기 운영진 합류를 결정하게 됐습니다.</p>
</li>
</ol>
<h3 id="부족했던-점-lacked">부족했던 점 (Lacked)</h3>
<ol>
<li>다른 팀과도 더 적극적으로 교류할걸 하는 생각을 했습니다. 스터디도 참여하고 회식도 자주 가고 이야기도 나눴지만 사람으로서 가까워진 느낌은 아니었습니다. 배울 것이 많은 분들이었기에 더 후회가 남나 싶기도 합니다.</li>
<li>당시에 스터디 2개(Redis, 클린 코드)를 진행했는데, 욕심이었던 것 같습니다. 스터디 2개 + 프로젝트 + 학기 병행을 진행하니 너무 바빠서 원하는 만큼의 퍼포먼스를 내지 못한 것 같아 아쉬웠습니다.</li>
</ol>
<h3 id="배운-점-learned">배운 점 (Learned)</h3>
<ol>
<li>개발자에게 중요한건 하드스킬보다는 소프트스킬이라고 생각합니다. 단순히 개발만 잘하는 것은 의미가 없다는 생각을 했습니다. 팀적으로 어떻게 소통하고, 문제를 해결하는지가 더 중요합니다.</li>
<li>네트워킹은 생각보다 많이 중요합니다. 한 사회에서 맺어진 인연이 나중에 영향을 줄 수 있고, 개발 사회는 커보이지만 생각보다 작습니다. 동아리에서 만난 사람이 나중에 회사 동료가 될 수도 있고, 추천인이 될 수도 있습니다.</li>
</ol>
<h3 id="바라는-점-longed-for">바라는 점 (Longed For)</h3>
<ol>
<li>다양한 동아리를 경험해보고 싶습니다. 동아리마다 특징도 다르고 분위기도 다르기에, 겪어보며 많은 경험을 하고 네트워크를 형성하고 싶습니다.</li>
<li>동아리에서 맺은 관계가 오래 지속되면 좋겠습니다.</li>
<li>IT 연합 동아리들의 인지도가 올라가고, 더 많은 사람들이 동아리 활동에 참여할 수 있는 기회가 생기면 좋겠습니다.</li>
</ol>
<h2 id="정리">정리</h2>
<p>활동 기간에도, 활동이 끝난 후에도 정말 행복한 추억이었습니다. 기회가 된다면 YAPP이 아니더라도 IT 연합 동아리를 경험해보는 것을 추천합니다. 학교나 회사에서 배울 수 있는 것과는 별개로 네트워킹, 기술 등 자신이 원하는 분야에서 많이 성장할 수 있을 것이라고 확신합니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ICT 인턴십 합격 후기]]></title>
            <link>https://velog.io/@0_zoo/ICT-%EC%9D%B8%ED%84%B4%EC%8B%AD-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@0_zoo/ICT-%EC%9D%B8%ED%84%B4%EC%8B%AD-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 17 Mar 2024 11:04:57 GMT</pubDate>
            <description><![CDATA[<h2 id="ict-인턴십-프로그램">ICT 인턴십 프로그램</h2>
<p><a href="https://www.ictintern.or.kr/main.do">ICT 인턴십 홈페이지</a>
학교와 기업, 한국정보산업연합회가 연계해 인턴십을 수행하며 학점을 얻을 수 있는 프로그램이다.
선정된 학교 학생만 참여 가능하다(<a href="https://www.ictintern.or.kr/jsp/common/login_board_detail.do?BOARD_NO=1749&amp;S_NOTICE_TYPE=&amp;SELECT_GUBUN=S_SUBJECT&amp;SEARCH_TEXT=&amp;totalCnt=88&amp;paging=10&amp;pagingFlag=&amp;C_PAGE=1">2024년 1학기 기준 선정 학교 리스트</a>)</p>
<h2 id="신청한-이유">신청한 이유</h2>
<p>학교 수업 중 듣고 싶은 수업이 더이상 없었다.
3학년때 프로젝트로 진행하는 수업을 다 들었는데, 이론 수업중에는 더이상 듣고 싶은 수업이 없었고, 현업에서 경험을 쌓고 싶었다.</p>
<p>인당 3개 기업까지 신청 가능했는데, 스타트업이 대부분이었고 이름을 들어본 회사도 몇군데 있었다.
나는 잘하는 스타트업을 가고 싶은 마음이 있어서, 원래 잘한다고 알고 있던 스타트업 한군데를 포함해 3개 기업에 신청을 했다.
결과적으로는 그 스타트업에 합격해 백엔드 엔지니어로 일을 하고 있고, 정말 잘한 선택이라고 생각하고 있다.</p>
<h2 id="지원-과정">지원 과정</h2>
<p><a href="https://www.ictintern.or.kr/jsp/common/login_board_detail.do?BOARD_NO=1750&amp;S_NOTICE_TYPE=&amp;SELECT_GUBUN=S_SUBJECT&amp;SEARCH_TEXT=&amp;totalCnt=100&amp;paging=10&amp;pagingFlag=&amp;C_PAGE=1">지원 과정 설명 페이지</a>에서 자세한 기간 및 공통 프로세스를 확인할 수 있다.
각 학교 당 지원 방식이나 프로세스는 상이할 수 있다.</p>
<h3 id="서류">서류</h3>
<p>나는 포트폴리오 안에 진행했던 프로젝트, 외부 활동, 깃헙 링크 등을 함축해서 담았다.</p>
<h3 id="코딩테스트">코딩테스트</h3>
<p>이건 회사에서 코딩테스트 실시를 원하는 경우 진행하는데, 회사 여러개를 지원하더라도 한번만 실시하면 된다.
난이도는 크게 어렵지는 않았다.
코딩테스트 실시 후 합격 전화가 왔는데, 당일 저녁부터 과제 시작이라 조금 당황했던 기억이 있다.</p>
<h3 id="과제">과제</h3>
<p>내가 지원한 회사의 경우 과제가 있었는데, 자세한 내용은 생략하도록 하자.
특정 서비스를 원하는 언어로 구현하는 과제였는데, 이를 인터뷰에서 활용했다.
과제 전형 진행 후 다음날 아침에 합격 전화가 왔고, 인터뷰 일정을 정했다.</p>
<h3 id="인터뷰">인터뷰</h3>
<p>인터뷰는 1대1로 진행됐고, 과제를 옆에 띄운 채로 진행했다.
구체적인 내용은 말을 못하지만, 간단하게 플로우만 말하면</p>
<ol>
<li>코드 설명</li>
<li>개선사항 도출</li>
<li>코드 구현</li>
<li>구술 면접(컬쳐 핏)</li>
</ol>
<p>순으로 진행됐다.
인터뷰 결과는 당일 오후에 발표됐다.</p>
<h3 id="이후-프로세스">이후 프로세스</h3>
<p>이후 약 2주간은 특별한 일 없이, 싸인하러 학교 한두번 방문하고, 
서류 제출 요구하면 인터넷으로 제출만 했다.</p>
<h2 id="입사-2주가-지난-후-후기">입사 2주가 지난 후 후기</h2>
<p>입사한지 벌써 2주가 됐는데, 처음 일주일은 온보딩 프로세스만 계속 진행해서 정신 없었고
다음 일주일에는 바로 작업을 부여해주셔서, 일을 배우느라 정신없었지만 그래도 첫주보다는 나았던 것 같다.</p>
<p>사람들이 정말 좋고, 배울수 있는게 너무 많아서 정말 회사를 잘 골랐다는 생각을 하고 있다.
개발자분들이 회사에 임팩트를 끼치기 위해 노력하는 모습이 인상깊었고, 나도 짧은 기간이지만 나의 임팩트를 끼치고 싶다.</p>
<p>그리고 많은 분들이 ICT인턴십 프로그램을 알고 신청해서, 학생때 하기 쉽지 않은 경험을 했으면 좋겠고, 지원할 수 있는 회사도 많아졌으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고] 2023년, 불태운 한 해]]></title>
            <link>https://velog.io/@0_zoo/%ED%9A%8C%EA%B3%A0-2023%EB%85%84%EC%9D%84-%ED%9A%8C%EA%B3%A0%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@0_zoo/%ED%9A%8C%EA%B3%A0-2023%EB%85%84%EC%9D%84-%ED%9A%8C%EA%B3%A0%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 27 Feb 2024 07:41:57 GMT</pubDate>
            <description><![CDATA[<h1 id="조금-늦은-회고">조금 늦은 회고</h1>
<p>거의 두달이 지나서야 쓰는 2023년 회고. 정말 바쁘게 살았고, 많은 성장을 한 한해였다.
키워드부터 선정해보자.</p>
<h2 id="창업과-취업-그-사이-어딘가">창업과 취업, 그 사이 어딘가</h2>
<p>2023년을 상징하는 키워드는 정말 고르기 어려웠다.
1년 내내 뜨거웠지만, 유럽 여행을 다녀온 8월 이전과 그 이후는 전혀 다른 느낌으로 뜨거웠다.</p>
<p>미국을 다녀온 이후 2월~8월은 창업에 불태웠고, 
유럽을 다녀오고 얼마 안돼 9월 이후에는 개발 공부, 취업 준비에 열중했다.</p>
<p>정말 다른 두 길이라 의아해할 수도 있지만, 키워드별로 당시의 감정과 생각을 곱씹어보며 적어보려 한다.</p>
<h2 id="창업">창업</h2>
<p>어린 나이에 정말 하기 힘든 경험을 했다고 생각한다.
좋은 팀을 만났기에 가능했고, 취업을 경험하더라도 결국엔 돌아올 것 같다.</p>
<h2 id="창업-왜-하세요">창업, 왜 하세요?</h2>
<p>아이러니하게도 창업을 하는 사람들끼리 만나면 가장 많이 하는 질문이다.
창업을 하는 팀을 만나던, VC를 만나던 정말 많이 했고, 받았고, 하지만 아직도 정답은 모르겠다.</p>
<p>처음 질문을 받았을 때 나는 &quot;돈을 많이 벌고 싶어서요&quot;라고 말했고,
마지막에 질문을 받았을 때는 &quot;이것보다 큰 동기부여를 얻을 수는 없을 것 같아서요&quot;라고 말했다.</p>
<p>짧은 대답이었지만, 이 기간동안의 변화가 담긴 대답이라고 생각한다.</p>
<h2 id="얻은-것">얻은 것</h2>
<p>크게 보면 3가지를 얻은 것 같다.
경험과 가치관, 그리고 자기객관화</p>
<h3 id="경험">경험</h3>
<p>앞서 말했듯, 학생 창업은 하기 쉽지 않은 경험을 제공해준다.
서비스 기획, 개발, 배포, 가설 검증에 이르는 사이클 경험,
서비스를 개발하는 것 뿐만 아니라 팔아보는 경험,
소비자 뿐만 아니라 투자자에게 평가받는 경험.</p>
<blockquote>
<p>이 경험을 통해, 가장 크게 느낀점을 써보자면</p>
<ol>
<li>창업은 만들고 싶은 제품을 만드는 것이 아닌, 사용자가 원하는 제품을 만드는 것이다.</li>
<li>좋은 제품을 만드는 것에서 끝나면 안된다. 더 중요한건 그 제품을 잘 파는 것이다.</li>
<li>코드가 안보인다고 해서 개발 실력이 부족해도 괜찮은 것이 아니다. 
코드가 안좋으면 개발 공수가 길어지고, 이는 사이클 자체의 지연으로 이어진다. 
즉, 개발은 기본적으로 잘해야 한다.</li>
</ol>
</blockquote>
<h3 id="가치관">가치관</h3>
<p>내 스스로의 가치관을 조금은 확립할 수 있었다. 창업가로서, 개발자로서.</p>
<p><strong><em>창업가</em></strong>
내가 좋아할 수 있는 도메인의 제품을 만들되, 내가 만든 제품을 좋아하지 않는 것. </p>
<blockquote>
<p>처음에는 내가 만든 제품을 좋아하지 말라는 말이 이해가지 않았다. 
 하지만 시간이 흐르면서, 내가 만든 제품을 좋아해보면서, 말의 뜻을 이해하게 됐다.</p>
</blockquote>
<p> 내가 만든 제품을 좋아하는 순간, 모든 기능을 좋아하게 돼고, 필요없는 기능을 제거할 순간이 와도 결정이 힘들어진다. 
 객관적으로 제품을 바라봐야 하는데, 주관적인 시점에서 제품을 바라보게 된다. 
 결과적으로 소비자가 원하는 제품이 아니라, 내가 원하는 제품을 만들게 된다.</p>
<p><strong><em>개발자</em></strong>
단순 기능 개발에 초점을 두는 것이 아닌, 코드 자체의 확장성과 재사용성에 초점을 두자.
이번에 기능 개발에 사용한 코드가 이후 기능 개발에 악영향을 주는 순간, 더 많은 비용이 소모된다.</p>
<blockquote>
<p>창업은 특히 기능개발에만 초점을 두는 경우가 많은 것 같다.
나 또한 돌아가기만 하면 됐지~ 라는 마인드로 개발했고, 이때문에 이슈 처리나 신기능 개발에서 난관을 겪은 적이 많았다.
진짜 서비스를 만들고 싶다면, 어떻게 하면 다른 기능을 개발할 때 많은 비용을 들이지 않고도 개발할 수 있을 지 생각해야 한다.</p>
</blockquote>
<h3 id="자기객관화">자기객관화</h3>
<p><strong>*&quot;동기부여에 큰 영향을 받는 사람&quot;*</strong></p>
<blockquote>
<p>이 점 때문에 창업을 시도했고, 창업을 그만뒀고, 다시 돌아올 것 같다. </p>
</blockquote>
<p>본격적으로 창업을 시작하기 전 2월, 서비스에 대한 의구심이 있었고, 
내가 이 일을 열심히 할 수 있을지에 대해 나 스스로도 의심했고, 주변에서도 의심했다.</p>
<p>이때 마음을 먹었던 건 팀장의 서비스에 대한 강한 확신과, 새로운 팀원덕분이었다.
둘의 강한 확신덕분에 나도 덩달아 서비스에 대한 확신을 가질 수 있었고, 동기부여를 얻을 수 있었다.</p>
<blockquote>
<p>사실 남에 의해 내 서비스에 확신을 갖는다는 건 부끄러운 일이지만, 
돌이켜 생각해보면 그렇게라도 확신을 얻어 이런 경험을 한 것이 이기적이게도 정말 고맙다.</p>
</blockquote>
<p>본격적으로 창업을 시작한 후에는 사용자에게서 동기부여를 얻었다.
사용자를 모으고, 사용자의 얘기를 듣고, 어떻게 하면 사용자에게 WOW 포인트를 만들 수 있을지 고민했다.</p>
<p>주변 팀에게서도 많은 동기부여를 얻었다. 
창업공간에 입주해 일을 하다보니 주변에도 많은 창업팀이 있었고, 많은 대화를 할 수 있었다.
다른 팀보다 일찍 출근해 늦게 퇴근하고 싶은 마음도 있었다.</p>
<blockquote>
<p>왜 이 점 때문에 창업을 관뒀는지는 아래에서 기술할 예정이다.</p>
</blockquote>
<p><strong>*&quot;주관이 약하고, 의견 표출을 강하게 못하는 사람&quot;*</strong></p>
<p>팀에게 가장 미안함을 느꼈던 부분이고, 현재도 고치려고 노력하는 부분이다.
그동안은 나 자신이 팔로워 역할에 어울린다고 합리화하며 지냈던 것 같다.
하지만 적어도 창업가에게는 말도 안되는 변명이다. 서비스적으로도, 팀적으로도 전혀 도움이 안된다.</p>
<h2 id="왜-관뒀어요">왜 관뒀어요..?</h2>
<p><strong><em>결론부터 말하면, 동기부여를 잃었다.</em></strong></p>
<h3 id="유료화">유료화</h3>
<p>유료화는 개인적으로 정말 큰 고민거리였다. 
빠르게 개발하고 싶었지만, 최대한 늦게 진행하고 싶었다.
무료 서비스로 최대한 많은 사용자를 모은 후 진행하고 싶었다.
결과적으로 8월, 조금 이른 유료화를 진행했고, 당연히 기대에 못미치는 수준의 수익을 얻었다.</p>
<h3 id="투자-유치-실패">투자 유치 실패</h3>
<p>유료화를 이른 시기에 한 이유는 투자 유치 목적도 컸다. </p>
<blockquote>
<p>*&quot;좋은 서비스인건 알겠는데, 그래서 소비자가 돈 내고 쓸만한 서비스에요?&quot;*</p>
</blockquote>
<p>VC들은 말로 하는 대답이 아닌, 실제 데이터를 원했다.
이미 결과를 알고 한 질문이었다고 생각한다.</p>
<h3 id="마케팅-부족">마케팅 부족</h3>
<p>우리 팀은 제품을 잘 파는 팀이 아니었다.
경험이 있지도 않았고, 할 것이 많다는 변명으로 열심히 하지도 않았고, 공격적인 시도를 해보지도 못했다.</p>
<blockquote>
<p>다양한 시도는 했었다. 법원에서 전단지를 뿌려보기도 했고, 부동산 경매 학원 강사와 컨택해보기도 했다.
하지만 확실한 우리만의 채널을 뚫지 못했다.</p>
</blockquote>
<h3 id="서비스를-옮기라구요-왜요">서비스를 옮기라구요? 왜요?</h3>
<p>우리가 진입하려는 부동산 경매 시장은 이미 너무 고여있는 시장이었다.
얼마 안되는 정보를 정말 비싼 비용에 팔지만, 사용자들은 이미 사용하고 있는 서비스에서 벗어나고 싶지 않아 했다.
시장을 잘 모를 때는 정말 뚫기 쉬운 시장같아 보였지만, 알면 알 수록 뚫기 힘든 시장이라는 것을 깨달았다.</p>
<h3 id="동기부여-상실">동기부여 상실</h3>
<p>결과적으로, 동기부여를 잃었다.</p>
<h3 id="다시-돌아간다면">다시 돌아간다면</h3>
<p>개인적으로, 2월에 새 팀원이 들어왔을 때, 다른 도메인의 프로덕트를 시작했다면 어땠을까..? 하는 생각을 한다.
결정적으로 너무 뚫기 어려운 시장이었고, 재미있게 할 수 있는 도메인이 아니었다고 생각한다.</p>
<h2 id="개발">개발</h2>
<p>창업을 관두고 학교에 복학하며,
가장 먼저 든 생각은 &quot;개발을 공부하고 싶다&quot; 였다.
개발을 배우고도 싶었고, 내가 그동안 공부한 것을 나눠주고도 싶었다.</p>
<h2 id="교내동아리-아롬">교내동아리, 아롬</h2>
<p>먼저, 교내동아리 아롬을 들어갔다. 
친구가 이미 멘토로 들어가있었고, 서버 멘토가 필요한 상황이라 맡아주기로 했다.
총 인원 약 50명 중 25명정도가 서버 멘티였고, 혼자 모두 관리해야 하는 상황이었다.</p>
<h3 id="가르치는-법을-배웠다">가르치는 법을 배웠다</h3>
<p>그 전에 누군가에게 서버 프로그래밍을 가르칠 능력도, 가르친 경험도 없었다.
이 시기에 가르치는 일이 생각보다 어려운 일이라는 것을 깨달았다.
내가 머리를 박으며 배운 것들을 그냥 외우게 시키기보다 조금이라도 머리를 박는 경험을 하며 배우게 하고 싶었고,
이를 통해 공부하는 법을 알려주고 싶었다.
완벽히는 아니어도, 많이 배운 것 같다.</p>
<h3 id="절박함의-중요성">절박함의 중요성</h3>
<p>주변 사람들에게 정말 많이 하는 말이 있다.</p>
<p><strong>* &quot;절박하지 않아서 그래&quot;*</strong></p>
<p>나는 군대를 전역하면서 소프트웨어 마에스트로 12기를 준비하며 개발 공부를 시작했다.
이때는 사실 절박함까진 아니었고, 되면 좋고, 아님 말고 식의 생각을 했다.
실력이 부족했기 때문에 떨어졌고, 이 시기에 자취를 시작했다.</p>
<p>자취를 시작하며 다짐한게 있다. 절대 부모님께 지원을 받지 않아야겠다는 다짐이었다.
&quot;알바를 하면 되는거 아니야?&quot; 라고 생각할 수도 있지만, 개인적으로 알바는 도움이 별로 안된다고 생각했고, 개발을 이용해 생활비를 벌고 싶었다.</p>
<p>이때부터 절박함을 갖고 살았던 것 같다. 학교를 다니며 42서울을 했고, 소마 13기를 했고, 휴학을 한 기간에는 창업을 했다. 돈벌이가 끊기면 안된다는 생각이 있었고, 그 와중에 끊임없는 성장을 하고 싶었다.</p>
<p>아롬을 하면서도 절박함의 중요성을 느꼈다. 여유가 있는 사람은 개발 공부를 1순위로 삼기 힘들었고, 당연히 이해가 갔다. 어떻게 하면 절박함 없이도 모두가 열심히 할만한 동기부여를 줄 수 있을까 고민했지만, 결국 해답을 찾지 못한 점이 아쉽다.</p>
<h2 id="yapp">YAPP</h2>
<p>창업을 그만두며 개발 연합 동아리에 들어가는 것을 목표로 뒀다. 친구가 연합동아리 경험이 많은데, 개발 경험을 쌓고 실력을 늘리기에 그보다 좋은 것이 없어보였다.
그래서 IT 연합 동아리중 하나인 YAPP에 지원했다.</p>
<h3 id="왜-yapp이었나요">왜 YAPP이었나요?</h3>
<p>동아리가 정말 많아서 고민했지만, 간단한 기준에 부합한다면 올라오는 공고 순으로 지원할 생각이었다.</p>
<ol>
<li>서버 협업이 가능한 동아리</li>
<li>기획자가 있거나, 디자이너가 기획을 어느정도 맡아주는 동아리</li>
<li>교육 프로세스가 없고, 타이트하게 사이드 프로젝트만 진행할 수 있는 동아리</li>
</ol>
<p>이 기준에 부합하는 동아리 중 가장 빠르게 공고가 올라온 동아리가 YAPP이었고, 운이 좋게도 약 15:1 경쟁률을 뚫고 서버 직군으로 참여할 수 있었다.</p>
<h3 id="yapp은-어떻게-뽑고-어떻게-진행하나요">YAPP은 어떻게 뽑고, 어떻게 진행하나요?</h3>
<p>모집기간: 1년에 2번 - 4월, 10월
지원절차: 서류 - 온라인 면접
진행 기간: 4개월
개발 기간: 2개월(기획이 빨리 끝나면 3개월도 가능)
팀 구성: 7명(기획 1, 디자인 1, 프론트 3, 서버 2)
대학생 / 현직자 비율: 약 1:1
진행 방식: 격주 단위 오프라인 세션, 매주 팀 세션(온라인, 오프라인 자유)</p>
<h3 id="프로젝트">프로젝트</h3>
<p>프로젝트 회고는 내용이 많을 것 같아, 따로 적을 예정이다. 
추후 블로그 링크로 대체</p>
<h3 id="얻은-것-1">얻은 것</h3>
<h4 id="서버-협업-경험">서버 협업 경험</h4>
<p>캡스톤때 다른 친구와 서버 협업을 하긴 했지만, 주로 내가 피드백해주는 형태라 협업의 느낌이 나지는 않았다. 이번에는 나보다 잘하는 분과 협업했기 때문에, 피드백을 정말 많이 주고 받았고, 개발적인 부분이나 소통하는 방식을 많이 배웠다.</p>
<h4 id="기획자-협업-경험">기획자 협업 경험</h4>
<p>이전에 진행했던 프로젝트들은 모두 기획부터 일정 관리, 개발까지 모두 기획자 없이 진행했는데, 이번에 처음 PM과 협업하는 경험을 했다.</p>
<p>사실 예전에는 PM이 있으면 기획을 모두 PM에게 맡기고 개발만 하면 되지 않을까 기대했는데, 막상 해보니 기획에 참여하지 않는 건 말이 안되고, PM의 역량은 일정 산출과 개발자가 개발하기 편한 환경(문서화, 디자인 관리, 마케팅 관리 등)을 제공하는 것에 달려있다는 생각을 하게 됐다.</p>
<h4 id="또-하나의-런칭-경험">또 하나의 런칭 경험</h4>
<p><a href="https://apps.apple.com/kr/app/%EC%97%AC%EB%B9%84-%EC%97%AC%ED%96%89%EA%B2%BD%EB%B9%84-%EA%B8%B0%EB%A1%9D%EB%B6%80%ED%84%B0-%EC%A0%95%EC%82%B0%EA%B9%8C%EC%A7%80/id6475346701?l=en-GB">여비</a>를 런칭했다.
이제 런칭한 경험이 꽤 쌓여가는데, 서비스를 런칭할 때마다 설레는 마음은 처음 그대로인 것 같다. 팀원 모두 정말 열심히 노력했기 때문에, 잘 돼서 오래 운영하고 싶다.</p>
<h3 id="아쉬운-점">아쉬운 점</h3>
<h4 id="일정-관리-미숙">일정 관리 미숙</h4>
<p>서버 개발이 원래 일정보다 많이 늦어졌다. 서버 팀원도 새로 인턴 하느라 정신이 없었고, 나도 인턴 준비와 여러 일정이 겹쳐 원래 계획한 일정을 맞추지 못했다.</p>
<p>프론트는 서버 작업이 나온 이후 할 수 있는 작업이 많기 때문에, 덩달아 프론트 작업도 늦어진 부분이 있어 죄송했다.</p>
<h4 id="코드-퀄리티">코드 퀄리티</h4>
<p>위 내용과 이어지는 내용인데, 일정에 맞추지 못했다보니 급하게 작업하는 부분이 있었고, 코드 퀄리티가 좋지 못하다. 이 부분은 3월중으로 리팩토링하며 개선하기로 했다.</p>
<h3 id="또-할건가요">또 할건가요?</h3>
<p>무조건 한다고 대답할 수 있다. 그런데 또 YAPP에서 할지는 아직 모르겠다.
다른 동아리 경험도 해보고 싶고, 동아리 운영 경험도 해보고 싶어서 다음 동아리 모집 전까지 고민해보며 정할 생각이다.</p>
<h2 id="ict인턴십">ICT인턴십</h2>
<p>이건 사실 24년 이야기이긴 한데, ICT 인턴십으로 원하는 회사에 합격해 다음주부터 다니기로 했다. 이 부분도 이야기가 길 것 같아서 추후 ICT 인턴십 합격 후기 글로 대체해야겠다.</p>
<h2 id="전체-회고">전체 회고</h2>
<p>바쁘게 살았고, 뜨거웠던 한 해 였던 것 같다.
정말 많이 성장했고, 많은 기반을 닦았다.
24년은 사회에 진출해서, 더 많은 성장을 해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] Redis Persistence]]></title>
            <link>https://velog.io/@0_zoo/Redis-Redis-Persistence</link>
            <guid>https://velog.io/@0_zoo/Redis-Redis-Persistence</guid>
            <pubDate>Sun, 07 Jan 2024 08:35:05 GMT</pubDate>
            <description><![CDATA[<h1 id="redis-persistence">Redis Persistence</h1>
<p>Redis는 SSD와 같은 저장소에 데이터를 저장하는 다양한 선택지를 제공한다.
이를 Redis Persistence라 하며 다음을 포함한다:</p>
<ul>
<li>RDB(Redis Database): RDB는 특정 인터벌의 데이터셋 스냅샷을 저장하는 방식으로 이루어진다.</li>
<li>AOF(Append Only File): AOF는 서버로부터 받은 모든 write operation을 로깅하는 방식으로 이루어진다. 이 명령어들은 서버 시작시에 실행되어 데이터셋을 재구축하는데 사용될 수 있다. 명령어들이 로깅되는 방식은 레디스 프로토콜과 동일하다.</li>
<li>No Persistence: 캐싱을 사용하는 경우, persistence를 하지 않을 수도 있다.</li>
<li>RDB + AOF: 같이 사용할 수도 있다.</li>
</ul>
<h2 id="rdb-장점">RDB 장점</h2>
<ul>
<li>백업에 최적화된 방식이다.</li>
<li>disaster recovory에 용이하다.</li>
<li>Redis 성능을 극대화한다. 디스크 I/O와 같은 작업이 일어나지 않기 때문</li>
<li>AOF에 비해 큰 데이터셋을 갖고 있을 경우의 재시작 속도가 빠르다.</li>
<li>복제본에서, RDB는 <a href="https://redis.io/docs/management/replication/">partial resynchronizations after restarts and failovers</a>을 지원한다.</li>
</ul>
<h2 id="rdb-단점">RDB 단점</h2>
<ul>
<li>갑작스런 레디스 중지 시 데이터 손실 최소화를 원한다면, RDB는 좋은 선택지가 아니다. 보통 5분 혹은 그 이상의 주기로 스냅샷을 생성하기 때문에, 마지막 몇분의 데이터 손실은 발생할 수 밖에 없다.</li>
<li>RDB는 fork()를 자주 호출하기 때문에, 데이터셋이 크다면 시간을 잡아먹을 수 있고, 데이터셋이 아주 크다면 서버가 클라이언트 상대를 적게는 millisec부터 크게는 sec단위까지 못할 수도 잇다. AOF 또한 fork()호출을 하지만 그렇게 자주 하지 않고, 튜닝 또한 가능하다.</li>
</ul>
<h2 id="aof-장점">AOF 장점</h2>
<ul>
<li>AOF가 더 튼튼하다. fsync 정책을 다양하게 설정할 수 있다. 하지 않거나, 매초 하거나(default), 매 명령마다 하거나. fsync는 백그라운드 쓰레드로 작동하기 때문에, 쓰기 성능에는 영향을 거의 주지 않는다. <ul>
<li><code>fsync</code>: 파일의 내부 상태를 장치와 동기화시키는 함수(버퍼에 있는 데이터를 파일로 옮김)</li>
</ul>
</li>
<li>append-only log이기 때문에, seek 과정이 없고, 레디스의 갑작스런 종료 시에도 손실이 없다. 만약 특정 이유로 half-written 로그가 생긴다면, redis-check-aof-tool을 통해 쉽게 해결할 수 있다. </li>
<li>로깅 파일이 너무 커지면, 백그라운드에서 새 파일에 다시 쓰는 작업이 이루어진다. 새 파일이 생기는 동안 레디스는 계속해서 이전 파일에 쓰기 작업을 실행하고, 새 파일에 작성하는 명령어는 현재 데이터셋을 구축하기 위한 최소의 명령어이다. 이 작업이 완료되면 레디스는 새 파일에 쓰기 시작한다.</li>
<li>AOF는 모든 명령어를 하나하나 이해하기 쉽고 파싱하기 쉬운 방식으로 로깅한다. AOF 파일을 쉽게 추출할수도 있다. 실수로 <code>flushall</code> 키워드를 통해 모든 데이터를 flush했더라도, 서버를 잠시 중지하고, 마지막 커맨드를 제거하고, 서버를 재시작하면 레디스를 쉽게 복구할 수 있다.</li>
</ul>
<h2 id="aof-단점">AOF 단점</h2>
<ul>
<li>같은 크기의 데이터셋일 때, AOF 파일이 보통 RDB 파일보다 크기가 크다</li>
<li>일반적으로 RDB보다 AOF가 속도가 느리다.</li>
<li>Redis 7.0 이하일 경우<ul>
<li>rewrite중 database에 write가 실행될 경우 많은 메모리를 사용할 수 있다.</li>
<li>rewrite중 database에 write하는 경우 두번씩 write된다.</li>
<li>레디스는 write를 멈추고 rewrite중인 새 파일 끝에 해당 커맨드를 추가할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="so-what-should-i-use">So, what should I use?</h2>
<ul>
<li>PostgreSQL이 제공하는 만큼의 데이터 safety를 필요로 한다면 둘다 사용한다.</li>
<li>데이터가 중요하긴 하지만, 몇분정도의 데이터 손실은 상관 없는 경우, RDB를 사용한다.</li>
<li>AOF만 사용하는 유저도 많지만, RDB를 사용하는 것이 백업 측면이나, 빠른 재시작 관점이나, AOF 엔진 버그가 발생했을 시 큰 도움을 주므로, AOF만 사용하는 것은 권장하지 않는다.</li>
</ul>
<h2 id="snapshotting">Snapshotting</h2>
<ul>
<li>기본적으로 레디스는 데이터셋 스냅샷을 디스크에 dump.rdb라는 이름으로 저장한다. n초마다 최소 m개의 변화가 생겼을 시 스냅샷을 저장하도록 설정할 수 있다.</li>
</ul>
<pre><code>save n m</code></pre><h3 id="how-it-works">How it works</h3>
<p>레디스가 데이터셋을 디스크에 저장할 때, 다음 과정이 이뤄진다.</p>
<ul>
<li>레디스가 fork한다.<ul>
<li><code>fork</code>: 자식 프로세스를 생성하는 명령어</li>
</ul>
</li>
<li>자식 프로세스가 데이터셋을 임시 RDB 파일에 작성한다.</li>
<li>자식 프로세스가 새 RDB 파일 작성을 완료하면, 기존 파일을 대체한다.</li>
</ul>
<h2 id="append-only-file">Append-only file</h2>
<ul>
<li>configuration 파일에서 다음을 설정해 AOF를 enable할 수 있다.<pre><code>appendonly yes</code></pre></li>
<li>Redis 7.0부터, 레디스는 multi part AOF 매커니즘을 사용한다. 기존 하나의 AOF 파일이 아닌, 베이스 파일과 변경점 파일로 나뉘어 베이스 파일은 AOF rewritten 당시의 데이터셋 스냅샷, 변경점 파일은 생성한 이후의 변경점을 저장하는 방식이다.</li>
</ul>
<h2 id="log-rewritting">Log Rewritting</h2>
<ul>
<li>앞서 말했듯, 파일 크기를 줄이기 위해 rewrite가 진행될 수 있다.</li>
<li>Redis 2.2라면 <code>BGREWRITEAOF</code> 명령어를 통해 백그라운드 rewrite를 실행 가능핟 .</li>
<li>Redis 2.4 이상이라면 rewrite를 자동으로 실행하게 할 수 있다.</li>
<li>Redis 7.0부터는 rewrite 스케쥴 시 부모 프로세스가 새 변경점 파일을 만들어 write를 지속하고, 자식 프로세스는 새 베이스 파일을 만든다. 너무 많은 변경점 파일이 만들어지는 것을 막기 위해, 실패한 AOF rewrite가 재시도될 때 더 천천히 실행되도록 한다.</li>
</ul>
<h2 id="how-durable-is-the-aof">How durable is the AOF?</h2>
<p>fsync 옵션에 따라 비교해보자.</p>
<ul>
<li><code>appendfsync always</code>: 매우 느리지만 매우 안전하다. 매 명령마다 실행된다.</li>
<li><code>appendfsync everysec</code>: 충분히 빠르다. disaster시 1초 이내의 데이터 손실 가능성이 있다.</li>
<li><code>appendfsync no</code>: 빠르지만, 안전하지는 않다.</li>
</ul>
<p>권장되는 방식은 매초 저장하는 방식이다.</p>
<h2 id="interactions-between-aof-amd-rdb-persistence">Interactions between AOF amd RDB persistence</h2>
<ul>
<li>Redis &gt;= 2.4</li>
<li>스냅샷을 뜨는 중 rewrite 요청이 들어오면, 먼저 OK 응답을 준 후, 스냅샷이 완료되면 실행된다.</li>
<li>AOF와 RDB가 모두 활성화된 상태에서 restart하면 AOF 파일이 사용된다. 더 높은 완성도를 보장하기 때문이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] Manage Redis]]></title>
            <link>https://velog.io/@0_zoo/Redis-Manage-Redis</link>
            <guid>https://velog.io/@0_zoo/Redis-Manage-Redis</guid>
            <pubDate>Sun, 31 Dec 2023 07:30:55 GMT</pubDate>
            <description><![CDATA[<h1 id="high-availability-with-redis-sentinel">High availability with Redis Sentinel</h1>
<h2 id="what-is-redis-sentinel">What is Redis Sentinel?</h2>
<ul>
<li>Redis Cluster과는 다른 운영방식
<img src="https://velog.velcdn.com/images/0_zoo/post/cc7e3ce0-98a1-4a86-89e8-3ded9d87c00a/image.png" alt=""></li>
</ul>
<h3 id="기능">기능</h3>
<ul>
<li>Monitoring<ul>
<li>마스터와 복제본이 제대로 작동하는지 지속적 모니터링</li>
</ul>
</li>
<li>Notification<ul>
<li>문제가 생길 시 시스템 운영자나 다른 컴퓨터 프로그램에 알린다</li>
</ul>
</li>
<li>Automatic failover<ul>
<li>마스터에 문제가 생기면, 다른 복제본이 마스터로 승격되고, 그 외 복제본들은 새로운 마스터의 복제본으로 reconfigure되는 failover 과정을 진행한다.</li>
</ul>
</li>
<li>Configuration provider<ul>
<li>클라이언트가 현재 마스터 주소를 묻는 provider 역할을 한다.</li>
</ul>
</li>
</ul>
<h3 id="sentinel은-분산-시스템이다">Sentinel은 분산 시스템이다.</h3>
<p>여러 sentinel이 협력해 작동하도록 설계돼있다. 장점은 다음과 같다.</p>
<ul>
<li>장애 판단은 여러 sentinel의 동의 하에 결정된다. 이는 잘못된 판단 확률을 낮춘다.</li>
<li>하나의 sentinel에 장애가 발생하더라도, sentinel 시스템은 계속해서 장애를 감지할 수 있다.</li>
</ul>
<h2 id="running-sentinel">Running Sentinel</h2>
<p>다음 커맨드로 실행 가능하다.</p>
<pre><code>redis-sentinel /path/to/sentinel.conf</code></pre><p>아니면 다음 커맨드로 redis server 시작 시에 모드 설정도 가능하다.</p>
<pre><code>redis-server /path/to/sentinel.conf --sentinel</code></pre><ul>
<li>configuration file은 필수적이다.</li>
<li>기본적으로 26379 port를 사용한다.<h3 id="배포-전-유의해야-할-사항">배포 전 유의해야 할 사항</h3>
</li>
</ul>
<ol>
<li>최소 3개의 sentinel instance가 필요하다</li>
<li>인스턴스는 독립적인 방법으로 장애가 발생해야 한다.</li>
<li>레디스는 비동기 복제를 사용하기 때문에, 데이터 유실 가능성이 있다.</li>
<li>클라이언트에 Sentinel support가 필요하다. 대부분 클라이언트 라이브러리가 갖고 있으나, 전부는 아니다.</li>
<li>테스트가 필요하다</li>
<li>sentinel이나 docker나 다른 양식의 Network Address Translation or Port Mapping은 주의가 필요하다 - <a href="https://redis.io/docs/management/sentinel/#sentinel-docker-nat-and-possible-issues">Sentinel, Docker, NAT, and possible issues</a></li>
</ol>
<h3 id="configuring-sentinel">Configuring Sentinel</h3>
<p>sentinel.conf은 다음과 같이 작성될 수 있ㄷ .</p>
<pre><code>sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5</code></pre><ul>
<li>감시할 마스터를 지정하고, 각각 다른 이름을 준다.</li>
<li>복제본은 알아서 찾아준다.</li>
<li>복제본이 마스터로 승격하거나, 새로운 sentinel이 등장할때마다 rewrite된다.</li>
<li>위 예시에서는 두개의 마스터와 각각의 정의되지 않은 개수의 복제본을 모니터링한다.(mymaster, resque가 각각의 이름이 된다)<pre><code>sentinel monitor &lt;master-name&gt; &lt;ip&gt; &lt;port&gt; &lt;quorum&gt;</code></pre></li>
<li><code>quorom</code>은 해당 마스터가 장애라고 판단하는데 필요한 동의의 개수이다.</li>
<li>quorom은 장애를 감지하는데만 사용된다. 실제 조치는 한 sentinel이 리더로 선정돼 진행한다.</li>
<li>예를 들어 5개의 Sentinel process를 갖고 있고, quorom이 2라면 다음과 같이 작동한다.<ul>
<li>두개의 Sentinel이 장애라고 판단하면 둘중 하나가 장애 조치를 시도한다.</li>
<li>최소 3개 이상의 Sentinel이 reachable하면, 장애 조치를 시작한다.<h4 id="other-sentinel-options">Other Sentinel Options</h4>
<pre><code>sentinel &lt;option_name&gt; &lt;master_name&gt; &lt;option_value&gt;</code></pre></li>
</ul>
</li>
<li><code>down-after-milliseconds</code>: 인스턴스가 장애가 났다고 판단하는데 필요한 시간(millisec)</li>
<li><code>parrallel-syncs</code>: 동시에 동기화 가능한 복제본의 개수. 장애 조치 시 걸리는 시간에 영향을 끼친다(반비례)<h2 id="example-sentinel-deployment">Example Sentinel deployment</h2>
<h3 id="example-1-just-two-box-dont-do-this">Example 1: just two box: Don&#39;t do this</h3>
```</li>
<li>----+         +----+
| M1 |---------| R1 |
| S1 |         | S2 |</li>
<li>----+         +----+
Configuration: quorum = 1
```</li>
<li>M1에 장애가 생기면, S1도 작동하지 않기 때문에 장애 조치가 작동하지 않는다.(대부분의 sentinel이 reachable해야 함)</li>
<li>때문에 반드시 3개 이상의 Sentinel(in diffrent box)로 구성해야 함<h3 id="example-2-basic-setup-with-three-box">Example 2: basic setup with three box</h3>
```<pre><code>  +----+
 | M1 |
 | S1 |
 +----+
    |</code></pre></li>
<li>----+    |    +----+
| R2 |----+----| R3 |
| S2 |         | S3 |</li>
<li>----+         +----+
Configuration: quorum = 2
```</li>
<li>장애 조치는 정상 작동한다.</li>
<li>하지만 다음 경우에 문제가 있다.
```<pre><code>   +----+
   | M1 |
   | S1 | &lt;- C1 (writes will be lost)
   +----+
      |
      /
      /</code></pre></li>
<li>------+    |    +----+
| [M2] |----+----| R3 |
| S2   |         | S3 |</li>
<li>------+         +----+
```</li>
<li>위 M1에 장애가 생기면, C1이 write한 데이터가 유실된다</li>
<li>다음 커맨드로 해결 가능하다.<pre><code>min-replicas-to-write 1
min-replicas-max-lag 10</code></pre></li>
<li>하나의 복제본에도 write할 수 없으면, write를 더이상 accept하지 않는다.</li>
<li>max-leg second를 넘으면, write할 수 없는 것으로 판단한다.</li>
<li>하지만 이 또한 단점이 있는데, 두 복제본이 모두 stop하면 write할 수 있는 복제본이 없기 때문에 마스터는 작동중임에도 write를 accept하지 못한다.</li>
</ul>
<h3 id="example-3-sentinel-in-the-client-box">Example 3: Sentinel in the client box</h3>
<pre><code>            +----+         +----+
            | M1 |----+----| R1 |
            |    |    |    |    |
            +----+    |    +----+
                      |
         +------------+------------+
         |            |            |
         |            |            |
      +----+        +----+      +----+
      | C1 |        | C2 |      | C3 |
      | S1 |        | S2 |      | S3 |
      +----+        +----+      +----+
      Configuration: quorum = 2</code></pre><ul>
<li>레디스 box가 2개밖에 없을 경우, Example 2를 사용하지 못한다.(3개 이상의 Sentinel을 사용할 수 없으므로)</li>
<li>이때는 클라이언트 box에 Sentinel을 두는 식으로 할 수 있다.</li>
<li>클라이언트와 서버 사이 연결이 끊기면, 마스터나 복제본에 reach하지 못하는 문제가 있다.</li>
</ul>
<h3 id="example-4-example-3-with-less-than-three-clients">Example 4: Example 3 with less than three clients</h3>
<pre><code>            +----+         +----+
            | M1 |----+----| R1 |
            | S1 |    |    | S2 |
            +----+    |    +----+
                      |
               +------+-----+
               |            |
               |            |
            +----+        +----+
            | C1 |        | C2 |
            | S3 |        | S4 |
            +----+        +----+
      Configuration: quorum = 3</code></pre><ul>
<li>Example 3과 비슷하지만, 클라이언트가 3개 이하인 경우 위와 같이 구성할 수도 있다.</li>
<li>C2와 S4가 돌아가는box가 삭제되면, quorom이 2로 재조정돼야 한다.</li>
</ul>
<h2 id="sentinel-api">Sentinel API</h2>
<ul>
<li>Sentinel은 상태 체크, 모니터링중인 마스터와 복제본의 health 체크, 알림에 대한 구독이나 런타임 중 configuration 변경을 위한 api를 제공한다.</li>
<li>redis-cli나 다른 redis client를 이용해 소통 가능하다.</li>
<li>직접 접근할 수도 있고, Pub/Sub을 활용해 push style notification을 받을 수도 있다.</li>
<li><code>SENTINEL</code> 명령어를 사용할 수 있다.</li>
<li><a href="https://redis.io/docs/management/sentinel/#sentinel-commands">명령어 목록</a></li>
</ul>
<h2 id="pubsub-messages">Pub/Sub messages</h2>
<ul>
<li>클라이언트는 <code>SUBSCRIBE</code>나 <code>PSUBSCRIBE</code>를 통해 특정 채널에 대한 메세지를 수신할 수 있다.(<code>PUBLISH</code>는 불가능하다)</li>
</ul>
<h2 id="replicas-priority">Replicas priority</h2>
<ul>
<li>모든 레디스 인스턴스는 <code>replica-priority</code> 파라미터를 갖고 있다.</li>
<li>Sentinel은 이 파라미터를 활용해 장애조치 시 어떤 파라미터를 마스터로 승격시킬 지 결정한다.<ul>
<li>replica-priority가 0이라면 절대 마스터로 승격되지 않는다</li>
<li>숫자가 낮을수록 우선권이 높다.</li>
</ul>
</li>
</ul>
<h2 id="sentinels-and-replicas-auto-discovory">Sentinels and replicas auto discovory</h2>
<ul>
<li>Sentinel은 서로 다른 Sentinel과 항상 연결되어 메시지를 주고 받고, 상태를 체크한다.</li>
<li><code>__seltinel__:hello</code>라는 채널에 메시지를 전송하는 방식으로 이루어진다.<ul>
<li>모든 Sentinel은 2초마다 모니터링중인 모든 마스터와 복제본에 메시지를 전송한다.</li>
<li>모든 Sentinel은 자신이 모니터링중인 모든 마스터와 복제본의 <code>__seltinel__:hello</code> 채널을 구독한다.</li>
<li>configuration이 예전 버전이라면 업데이트도 동시에 진행한다.</li>
</ul>
</li>
</ul>
<h2 id="replica-selection-and-priority">Replica selection and priority</h2>
<ul>
<li>마스터가 <code>ODOWN</code> 상태가 되고 장애 조치를 할 준비가 되면, replica selection process가 진행된다.</li>
<li>복제본에 대해 다음을 체크한다.<ul>
<li>마스터로부터 disconnection time</li>
<li>우선권</li>
<li>replication offset processed (복제 정도)</li>
<li>run id</li>
</ul>
</li>
<li>disconnection time이 다음 값보다 크다면, 승격하지 않는다.<pre><code>(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state</code></pre></li>
<li>이 과정을 먼저 거친 후, 통과한 replica에 대해 다음 과정을 거친다.</li>
</ul>
<ol>
<li>우선권 순으로 오름차순 정렬한다.</li>
<li>우선권이 같다면, 복제 정도가 더 높은 복제본, 즉 마스터로부터 더 많은 데이터를 받은 복제본이 우선권을 갖는다.</li>
<li>우선권과 복제 정도가 모두 같다면, run ID를 사전식 정렬해 더 작은 ID를 갖는 복제본이 선택된다.<h1 id="redis-replication">Redis replication</h1>
</li>
</ol>
<ul>
<li>Redis Replication은 레디스 인스턴스가 마스터 인스턴스의 정확한 복제본이 될 수 있게 한다.</li>
<li>복제본은 연결이 끊기더라도 자동으로 다시 연결하고, 마스터에 어떤 변화가 생기더라도 정확한 복제본이 되려고 시도한다.</li>
<li>다음 세가지 메커니즘을 통해 작동한다.<ol>
<li>마스터와 복제본이 잘 연결돼있다면, 마스터는 마스터에 생긴 변화를 커맨드 스트림으로 복제본에 전송한다.</li>
<li>마스터와 복제본 사이 연결이 끊어지면, 복제본은 재연결하고 부분적 재 동기화를 진행한다: 끊겼던 기간의 커맨드 스트림만 다시 받아온다.</li>
<li>부분적 재동기화가 불가능하면, 복제본은 전체 재동기화를 요청한다. 이는 다소 복잡한 과정으로 진행되는데, 마스터가 데이터 스냅샷을 생성하고, 복제본으로 전송하고, 데이터가 변경될때마다 계속해서 커맨드 스트림을 전송해야 한다.</li>
</ol>
</li>
<li>레디스는 기본적으로 낮은 지연률과 높은 성능을 갖는 비동기 복제를 사용한다. 하지만 어떤 복제본이 어떤 커맨드를 수행했는지 알고 싶을 때, 선택적으로 동기 복제를 사용할 수 있다.<ul>
<li><code>WAIT</code> 커맨드를 사용하여 가능하다.</li>
<li>장애조치 시의 write 유실을 완전히 방지하지 못하지만, 확률을 엄청나게 낮춘다.</li>
</ul>
</li>
</ul>
<h2 id="important-facts-about-redis-replication">Important facts about Redis replication</h2>
<ul>
<li>레디스는 비동기 복제를 사용한다.</li>
<li>한 마스터는 여러 복제본을 가질 수 있다.</li>
<li>복제본은 다른 복제본으로부터의 연결을 허용할 수 있다. Redis 4.0부터, 모든 sub replica는 마스터가 보내는 커맨드 스트림을 그대로 받는다.</li>
<li>복제는 마스터 인스턴스에서 non-blocking하다. 복제본이 동기화를 진행하는 과정에서도, 마스터는 계속해서 작업을 수행한다.</li>
<li>복제는 복제본 인스턴스에서도 non-blocking하다. <ul>
<li>동기화중에도, 이전 데이터셋을 활용해 쿼리를 처리할 수 있다.(redis.conf에서 그렇게 지정했다면). </li>
<li>그렇지 않다면, 에러를 반환하게도 가능하다. </li>
<li>하지만, 동기화를 마친 후 예전 데이터셋을 삭제하고 새 데이터셋으로 대체하는 과정에서는 모든 연결을 차단한다. </li>
<li>Redis 4.0 이후에, 예전 데이터셋을 삭제하는것은 다른 스레드에서 동작하지만, 새 데이터셋으로 대체하는 것은 여전히 blocking하다.</li>
</ul>
</li>
<li>Replication은 scalability를 위해서도 사용될 수 있고, 단순히 데이터 안전성이나 높은 가용성을 위해서도 사용 가능하다.</li>
<li>모든 데이터셋을 디스크에 저장하는 것을 피하기 위해 사용할 수 있다. 하지만 마스터를 재시작할 시 빈 데이터셋으로 구동되고, 복제본이 이를 동기화하면 저장된 데이터셋이 유실되기에 주의해야 한다.</li>
</ul>
<h2 id="how-redis-replication-works">How Redis replication works</h2>
<ul>
<li>모든 레디스 마스터는 복제본 id와 발행한 커맨드 스트림 바이트를 의미하는 offset을 갖고, 이를 통해 복제본의 상태를 업데이트한다. </li>
<li>복제본 id와 offset 쌍은 데이터셋의 버전으로 사용될 수 있다.</li>
<li>복제본이 PSYNC를 통해 마스터에 접근하면, 복제본 id와 offset을 전달한다.</li>
<li>마스터에서는 받은 값을 보고, 추가된 부분만 전달하면 된다.</li>
<li>마스터에 충분한 backlog가 없거나, 받은 복제본 id가 더이상 알 수 없는 값이면, 전체 재동기화가 이뤄진다.</li>
<li>전체 재동기화는 다음 과정으로 이뤄진다.<ul>
<li>마스터는 RDB 파일을 생성한다. 동시에, 새로 들어오는 모든 write를 버퍼에 저장한다.</li>
<li>파일 생성이 완료되면, 복제본으로 전달한다.</li>
<li>복제본에서 해당 파일 저장과 메모리 로드를 진행한다.</li>
<li>마스터 버퍼에 저장된 새로운 변경 사항을 복제본에 전송한다. 이는 커맨드 스트림을 복제본에 전송하는 것과 같은 방식으로 이뤄진다.</li>
</ul>
</li>
<li>마스터가 여러 건의 동기화 요청을 한번에 받아도, 한번의 RDB 파일 생성만 이뤄진다.</li>
</ul>
<h2 id="redis-id-explained">Redis ID Explained</h2>
<ul>
<li>두 인스턴스가 같은 id, 같은 offset을 갖고 있다면, 둘은 동일한 데이터셋을 갖는다고 볼 수 있다.</li>
<li>인스턴스는 사실 두개의 replication id를 갖는다: main ID와 secondary ID<ul>
<li>복제본이 마스터로 승격될 때 필요하다.</li>
<li>복제본이 마스터로 승격되면 새로운 id가 생성되고, 다른 복제본들은 해당 id를 이용하게 된다.</li>
<li>하지만 이전 마스터의 데이터셋이 필요한 상황이 생기면, 이전 마스터의 id를 이용해야 한다.</li>
<li>이를 위해 두개의 id를 갖고 있어야 한다.</li>
</ul>
</li>
</ul>
<h2 id="read-only-replica">Read-only replica</h2>
<ul>
<li>Redis 2.6부터 기본적으로 read-only 모드를 지원한다.</li>
<li>redis.conf에서 <code>replica-read-only</code> 옵션을 통해 변경할 수 있다.</li>
<li>읽기 전용 복제본은 모든 write 커맨드를 거부한다.</li>
<li>복제본에 write를 하게 되면, 마스터와의 정합성이 깨지기 때문에 권장되지 않는다.</li>
<li>리스크를 줄이기 위해, writable한 복제본을 사용하기 위해서는 다음을 지키는 것이 좋다.<ul>
<li>마스터에서 사용하는 키에 write하지 않는다.</li>
<li>마스터로 승격될 여지가 있는 복제본을 writable하게 지정하지 않는다.</li>
</ul>
</li>
<li>역사적으로 보면 writable한 복제본을 사용하는 것이 허용되는 사례가 있었으나, Redis 7.0에서는 다른 방법으로 달성이 가능하기에 쓸모없어졌다.<ul>
<li>느린 Set이나 Sorted Set operation을 사용하는 경우. ex) SUNIONSTORE, ZINTERSTORE. </li>
<li>-&gt; 저장하지 않고 반환하는 함수를 사용하면 된다. ex) SUNION, ZINTER</li>
<li>SORT 커맨드를 사용하는 경우</li>
<li>-&gt; SORT_R0를 사용하면 된다.</li>
<li>EVAL, EVALSHA를 사용하는 경우</li>
<li>-&gt; EVAL_R0, EVALSHA_R0을 사용하면 된다.</li>
</ul>
</li>
</ul>
<h2 id="allow-writes-only-with-n-attatched-replicas">Allow writes only with N attatched replicas</h2>
<ul>
<li>Redis 2.8부터, 적어도 n개의 복제본이 연결돼있어야 write를 할 수 있게 설정할 수 있다.</li>
<li>하지만 레디스는 비동기 복제를 사용하기 때문에, 복제본이 실제 그 write를 했는지 확인할 수 없고, 데이터 유실이 발생할 수 있다.</li>
<li>다음 과정으로 write가 가능한지 판단한다.<ul>
<li>복제본이 매초 마스터에 진행한 스트림의 양을 핑으로 알려준다.</li>
<li>마스터는 지난번 모든 복제본으로부터 받은 핑을 기억한다.</li>
<li>유저는 지연이 maximum second보다 작은 복제본의 최소 개수를 정할 수 있다.</li>
<li>복제본이 n개 이상이라면, write를 진행할 수 있다.</li>
</ul>
</li>
<li>아래 옵션을 조정할 수 있다.<ul>
<li><code>min-replicas-to-write</code> : number of replicas</li>
<li><code>min-replicas-max-lag</code> : number of seconds</li>
</ul>
</li>
</ul>
<h2 id="how-redis-replication-deals-with-expires-on-keys">How Redis replication deals with expires on keys</h2>
<ul>
<li>레디스는 키에 TTL을 설정해 만료되게 할 수 있고, 복제본도 이를 반영할 수 있어야 한다.</li>
<li>마스터와 복제본이 동기화된 시계를 갖는 것은 해결할 수 없는 문제이기에, 다음 기법으로 반영한다.<ol>
<li>복제본은 키를 만료시키지 않는다. 마스터에서 키를 만료시키면, 모든 복제본에 DEL 커맨드를 수행하도록 보낸다.</li>
<li>이는 master-driven expire이기 때문에, 모종의 이유로 복제본 메모리에 키가 남아있을 수 있다. 이에 대응하기 위해, 복제본은 논리적 시계를 이용해 데이터셋 정합성을 훼손하지 않는 읽기 작업에 한해서 키가 존재하지 않는다고 리포트할 수 있다. </li>
<li>Lua Script 실행 도중에는 어떤 키 만료도 실행되지 않는다. Lua Script 실행중에는 마스터가 frozen된 상태이기 때문에, 스크립트 도중에 키는 존재하거나 존재하지 않거나 둘중 하나의 상태만 유지한다. 중간에 바뀌지 않는다.</li>
</ol>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] Use Redis]]></title>
            <link>https://velog.io/@0_zoo/Redis-Use-Redis</link>
            <guid>https://velog.io/@0_zoo/Redis-Use-Redis</guid>
            <pubDate>Sun, 10 Dec 2023 10:55:45 GMT</pubDate>
            <description><![CDATA[<h1 id="keyspace">Keyspace</h1>
<ul>
<li>Redis에서 키를 관리하는 방법<ul>
<li>키 유효시간</li>
<li>스캐닝</li>
<li>altering</li>
<li>qurying</li>
</ul>
</li>
<li>binary safe하기 때문에, 어떤 binary sequence던 키로 사용할 수 있다(빈 문자열 포함)</li>
</ul>
<h2 id="키-규칙">키 규칙</h2>
<ul>
<li>매우 긴 키는 좋지 않다. 큰 값 매칭이 필요하다면 해싱하는 것이 좋은 대안이다.<ul>
<li>메모리 관점</li>
<li>키를 비교하는 비용 관점</li>
</ul>
</li>
<li>매우 짧은 키 또한 종종 좋은 방법이 아니다.<ul>
<li>가독성 측면</li>
<li>ex) user:1000:followers를 u1000flw로 줄이는 것은 메모리를 아주 조금 줄여줄 뿐이다</li>
</ul>
</li>
<li>스키마 형태로 작성하는 것을 권장한다.<ul>
<li>ex) object-type:id 형태(user:1000)</li>
<li>.이나 -를 사용해 multi-word field를 작성할 수 있다</li>
</ul>
</li>
<li>최대 키 사이즈는 512MB이다</li>
</ul>
<h2 id="altering-and-querying-the-key-space">Altering and querying the key space</h2>
<h3 id="exists">EXISTS</h3>
<ul>
<li>존재하는지 안하는지 1, 0으로 반환<h3 id="del">DEL</h3>
</li>
<li>삭제를 성공했는지 못했는지 1, 0으로 반환<h3 id="type">TYPE</h3>
</li>
<li>type을 반환, 없으면 none 반환</li>
</ul>
<h2 id="key-expiration">Key expiration</h2>
<ul>
<li>키에 대한 TTL(Time To Live)를 설정할 수 있다</li>
<li>second, millisecond로 설정할 수 있다</li>
<li>expire 정보는 redis에 저장된다</li>
</ul>
<h3 id="commands">Commands</h3>
<pre><code>&gt; set key some-value
OK
&gt; expire key 5 // 만료시간 5초로 설정
(integer) 1
&gt; get key (immediately)
&quot;some-value&quot;
&gt; get key (after some time)
(nil)

persist key // 만료시간 삭제 후 영구적으로 남아있게 함

&gt; set key 100 ex 10 // 생성과 동시에 만료시간 설정
OK
&gt; ttl key // 만료시간 조회
(integer) 9</code></pre><h2 id="navigating-the-keyspace">Navigating the keyspace</h2>
<h3 id="scan">SCAN</h3>
<ul>
<li>Redis database를 incrementally iterate하기 위해서 Scan을 사용할 수 있다.<ul>
<li>Incremental iteration: 한번의 호출 당 작은 수의 엘리먼트만 반환한다. </li>
</ul>
</li>
<li>때문에 KEYS나 SMEMBERS같이 server를 block하는 커맨드들의 단점 없이 사용 가능하다.</li>
<li>SMEMBERS는 호출된 시점의 모든 엘리먼트를 반환할 수 있는 반면, SCAN은 iterate 도중 element가 증가할 가능성이 있기 때문에 해당 시점의 모든 엘리먼트라는 보장이 불가능하다.<h3 id="keys">KEYS</h3>
</li>
<li>Keys 또한 keyspace를 순회할 수 있는 방법이지만, 모든 키가 반환될때까지 레디스 서버를 block하므로 주의가 필요하다.</li>
</ul>
<h1 id="client-side-caching-in-redis">Client-side caching in Redis</h1>
<ul>
<li>application server의 가용 메모리에 캐싱하는 방법</li>
<li>local cache를 위해 사용하는 메모리는 적으나, 데이터베이스와 통신하는 시간을 비약적으로 줄여준다.</li>
<li>데이터 변경 확률이 적은 엘리먼트에 더욱 효과적이다.</li>
</ul>
<h3 id="client-caching이-없는-경우">client caching이 없는 경우</h3>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/6d423e19-c534-4e38-a496-6715a1ea7a98/image.png" alt=""></p>
<h3 id="client-caching을-사용하는-경우">client caching을 사용하는 경우</h3>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/bd2e9b53-fc17-425f-bf05-069b1532d607/image.png" alt=""></p>
<h2 id="문제점">문제점</h2>
<h3 id="데이터-동기화">데이터 동기화</h3>
<ul>
<li>정보가 업데이트할 경우, 캐싱된 정보를 삭제하고 새로운 정보를 캐싱하는 과정이 필요하다.</li>
<li>단순하게 ttl을 설정해주는 방식</li>
<li>Redis Pub/Sub을 활용해 정보 업데이트 시 client에 메시지를 보내는 방식<ul>
<li>모든 클라이언트에게 메시지를 보내기 때문에, CPU나 bandwith 관점에서 비효율적이다.</li>
<li>클라이언트에 이미 키가 expire됐을 수 있다.<h2 id="the-redis-implementation-of-client-side-caching">The Redis implementation of client-side caching</h2>
</li>
</ul>
</li>
<li>레디스 client-side 캐싱은 Tracking이라고 하며, 두가지 모드가 있다.<ul>
<li>default mode: 어떤 키를 클라이언트가 접근했는지 기억하고, 키가 수정되면 invalidation 메시지를 보낸다. 서버 메모리를 사용하지만, 클라이언트의 메모리에 존재하는 키만 취급한다.</li>
<li>broadcasting mode: 서버가 기억하는 방식이 아닌, 클라이언트가 해당 키를 subscribe하는 방식(prefix를 사용한다. ex. object:, user:,,,). 서버 메모리를 사용하지 않는다.</li>
</ul>
</li>
</ul>
<h3 id="default-mode">Default mode</h3>
<ul>
<li>클라이언트가 원할 시에 tracking을 활성화할 수 있다.</li>
<li>connection lifetime동안 서버는 클라이언트가 요청한 키를 저장한다.</li>
<li>키가 변경되거나, 만료되거나, maxmemory policy로 인해 제거될 경우 해당 키를 캐싱한 모든 클라이언트는 invalidation 메시지를 받는다.</li>
<li>메시지를 받으면 해당 키를 삭제한다.</li>
</ul>
<h3 id="문제점-1">문제점</h3>
<p>서버가 너무 많은 정보를 저장할 수 있다.</p>
<h3 id="redis-implementation에서의-해결">Redis implementation에서의 해결</h3>
<ul>
<li>invalidation table이라고 불라는 global taable로 키를 관리한다.</li>
<li>entry의 maximum number를 설정한다.</li>
<li>새로운 entry가 들어왔는데 maximum number를 넘을 경우, 오래된 키를 수정됐다고 처리하고(아닐지라도) 제거한다.</li>
<li>database number를 포함하지 않고 key namespace만 저장하기 때문에, database 2에 캐싱한 키가 database 3에서 수정되더라도 invalidation message가 전송된다.</li>
</ul>
<h3 id="two-connection-mode">Two connection mode</h3>
<ul>
<li>RESP3부터, 한 connection 안에서 data query를 실행하고 메시지를 받는 것을 동시에 할 수 있다.</li>
<li>하지만 클라이언트는 두개의 connection으로 분리하기를 원한다: 하나는 data, 다른 하나는 invalidation message</li>
<li>tracking을 활성화할 때, invalidation message를 다른 connection으로 redirect하는 방식으로 구현할 수 있다.</li>
</ul>
<h4 id="commands-1">Commands</h4>
<pre><code>CLIENT ID // invalidation message를 위한 커넥션 생성
:4
SUBSCRIBE __redis__:invalidate // invalidate 구독
*3
$9
subscribe
$20
__redis__:invalidate
:1</code></pre><pre><code>// Tracking
CLIENT TRACKING on REDIRECT 4
+OK

GET foo
$3
bar</code></pre><h3 id="opt-in-caching">Opt-in caching</h3>
<ul>
<li>클라이언트가 선택된 키만 캐싱할 수 있는 기능이다.<pre><code>CLIENT TRACKING on REDIRECT 1234 OPTIN</code></pre></li>
<li>클라이언트는 기본적으로 모든 키를 캐싱하지 않는다.</li>
<li>캐싱하고 싶으면, 값을 얻기 전에 특정 커맨드를 입력한다.
```
CLIENT CACHING YES</li>
<li>OK
GET foo
&quot;bar&quot;
```</li>
<li>다음 커맨드가 MULTI거나 Lua Script라면, 해당 트랜잭션/스크립트에 포함되는 모든 커맨드가 tracking된다.</li>
</ul>
<h3 id="broadcasting-mode">Broadcasting mode</h3>
<ul>
<li>서버 메모리를 사용하지 않는 대신 더 많은 메시지를 보내는 방식</li>
<li>BCAST option을 활용해 캐싱할 수 있다<pre><code>CLIENT TRACKING on REDIRECT 10 BCAST PREFIX object: PREFIX user:</code></pre></li>
</ul>
<h1 id="redis-pipelining">Redis pipelining</h1>
<ul>
<li>레디스 커맨드를 batch해서 RTT(round-trip time)을 최적화하는 방법<ul>
<li>RTT란, 클라이언트에서 서버, 다시 서버에서 클라이언트로 돌아오는 시간을 의미한다.</li>
</ul>
</li>
<li>Redis pipelining은 클라이언트가 응답을 읽지 않았더라도 새 요청을 처리하는 방식으로 구현된다.</li>
</ul>
<h3 id="netcat">netcat</h3>
<pre><code>$ (printf &quot;PING\r\nPING\r\nPING\r\n&quot;; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG</code></pre><p>위와 같은 방식으로 호출하면 3개의 커맨드를 한번의 RTT로 실행한다.</p>
<h3 id="rtt-뿐만-아니라-분당-처리-쿼리-수-또한-증가한다">RTT 뿐만 아니라 분당 처리 쿼리 수 또한 증가한다.</h3>
<p>pipelining을 사용하게 되면, 여러 건의 커맨드를 하나의 read로 읽고, 여러 건의 응답을 한번의 write로 처리하게 된다. 이로 인해 분당 처리할 수 있는 쿼리의 수가 pipelining을 사용하지 않았을 때보다 10배 가까이 증가한다.</p>
<h1 id="redis-keyspace-notifications">Redis keyspace notifications</h1>
<ul>
<li><p>클라이언트가 레디스 데이터에 영상을 미치는 이벤트에 대한 알림을 받을 수 있게 subscribe할 수 있게 한다.</p>
</li>
<li><p>subscribe한 클라이언트가 연결이 끊어지면, 그 시간동안 받은 이벤트는 사라진다.</p>
</li>
<li><p>받는 이벤트 예시는 아래와 같다</p>
<ul>
<li>키에 영향을 주는 모든 커맨드</li>
<li>LPUSH한 모든 키</li>
<li>database 0에서 만료된 모든 키<h2 id="type-of-events">Type of events</h2>
<pre><code>PUBLISH __keyspace@0__:mykey del
PUBLISH __keyevent@0__:del mykey</code></pre></li>
</ul>
</li>
<li><p>첫번째 채널은 mykey에 대한 모든 변화에 대한 알림을 받는다</p>
</li>
<li><p>두번째 채널은 mykey delete에 대한 알림만 받는다</p>
</li>
<li><p>첫번째 방식을 Key-space notification이라고 하며,</p>
</li>
<li><p>두번째 방식을 Key-event notification이라고 한다.</p>
</li>
</ul>
<h2 id="configuration">Configuration</h2>
<p>redis.conf의 notify-keyspace-events나 CONFIG SET을 통해 활성화할 수 있다.</p>
<pre><code>K     Keyspace events, published with __keyspace@&lt;db&gt;__ prefix.
E     Keyevent events, published with __keyevent@&lt;db&gt;__ prefix.
g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
$     String commands
l     List commands
s     Set commands
h     Hash commands
z     Sorted set commands
t     Stream commands
d     Module key type events
x     Expired events (events generated every time a key expires)
e     Evicted events (events generated when a key is evicted for maxmemory)
m     Key miss events (events generated when a key that doesn&#39;t exist is accessed)
n     New key events (Note: not included in the &#39;A&#39; class)
A     Alias for &quot;g$lshztxed&quot;, so that the &quot;AKE&quot; string means all the events except &quot;m&quot; and &quot;n&quot;.</code></pre><ul>
<li>K나 E는 적어도 하나 들어가야 한다.</li>
<li>KEA를 사용하면 대부분의 이벤트를 활성화할 수 있다.</li>
<li>인자가 없으면 알림이 비활성화된다.<h2 id="timing-of-expired-events">Timing of expired events</h2>
</li>
<li>TTL 관련 만료 이벤트는 두가지 경우에 발생한다<ul>
<li>command에 의해 키에 접근했는데 만료된 경우</li>
<li>만료된 키를 수집하는 백그라운드에 의해 수집되는 경우(한번도 접근하지 않는 키 또한 함께 수집한다)</li>
</ul>
</li>
<li>때문에 어떤 command도 키에 접근하지 않는 경우, 그리고 많은 키가 TTL을 갖는 경우 실제 만료된 시간과 만료 이벤트가 발생하는 시간에 상당한 딜레이가 있을 수 있다.</li>
</ul>
<h1 id="redis-programming-patterns">Redis programming patterns</h1>
<h2 id="bulk-loading">Bulk loading</h2>
<ul>
<li>Bulk loading에 일반적인 클라이언트를 사용하는건 좋은 방법이 아니다<ul>
<li>RTT가 매 command마다 들기 때문</li>
</ul>
</li>
<li>권장되는 방식은 다음과 같다<ul>
<li>먼저 insert 커맨드를 담은 text format을 담는다<pre><code>SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN</code></pre></li>
<li>그 후 처리하는 방식은 과거에는 netcat을 사용했지만, 모든 데이터가 처리됐는지 알 수 없고 에러를 체크할 수없어서 2.6버전부터는 redis-cli의 pipe mode를 사용한다.<pre><code>cat data.txt | redis-cli --pipe</code></pre></li>
<li>결과는 아래와 같이 나온다<pre><code>All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000000</code></pre></li>
</ul>
</li>
</ul>
<h3 id="generating-redis-protocol">Generating Redis Protocol</h3>
<ul>
<li>다음과 같은 방식으로 레디스 프로토콜을 만들 수 있다
```</li>
<li><args><cr><lf>
$<len><cr><lf>
<arg0><cr><lf>
<arg1><cr><lf>
...
<argN><cr><lf>
```</li>
<li>SET key value는 프로토콜로 다음과 같이 표현될 수 있다
```</li>
<li>3<cr><lf>
$3<cr><lf>
SET<cr><lf>
$3<cr><lf>
key<cr><lf>
$5<cr><lf>
value<cr><lf><pre><code></code></pre></li>
</ul>
<h2 id="distributed-locks-with-redis">Distributed Locks with Redis</h2>
<ul>
<li>공유된 자원을 다른 프로세스에서 사용할 때 효과적이다</li>
<li>DLM(Distributed Lock Manager)를 레디스에서 구현하는 방식은 다양하고, 각 라이브러리나 블로그 포스트마다 다른 접근을 하기 때문에 docs에서는 표준적인 알고리즘을 설명한다. </li>
<li>Redlock이라고 불리는 알고리즘이다.</li>
</ul>
<h3 id="safety-and-liveness-guaratees">Safety and Liveness Guaratees</h3>
<ul>
<li>최소 아래 세가지 특징을 보장한다<ul>
<li>Safety Property: mutual exclusion. 어떤 순간에도 하나의 클라이언트만 락을 걸 수 있다</li>
<li>Deadlock free: 락을 건 클라이언트가 터지거나 분리되더라도, 결국 락을 걸 수 있다</li>
<li>Fault tolerance: 대부분의 레디스 노드가 작동하는 한, 클라이언트는 락을 획득하고 릴리즈할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="failover-based-implementations">Failover-based Implementations</h3>
<p>자원에 락을 거는 가장 간단한 방법은 인스턴스에 키를 만들고, 만료 시간을 두고 관리하는 것이다. 락을 걸때 키를 만들었다가, 릴리즈할 때 키를 삭제하는 방식이다.</p>
<h4 id="레디스-마스터-노드가-죽으면">레디스 마스터 노드가 죽으면?</h4>
<p>복제본을 만들어두고 사용하는 방법이 있다. 하지만 이는 위에서 말했던 mutual exclusion을 위반한다. 복제본은 동기화되지 않기 때문이다.</p>
<h3 id="single-instance일-때-구현-방">Single instance일 때 구현 방</h3>
<pre><code>SET resource_name my_random_value NX PX 30000</code></pre><ul>
<li>키가 존재하지 않을 경우에만 set한다. value는 모든 클라이언트와 lock 요청에서 unique해야 한다.<pre><code>if redis.call(&quot;get&quot;,KEYS[1]) == ARGV[1] then
  return redis.call(&quot;del&quot;,KEYS[1])
else
  return 0
end</code></pre></li>
<li>lock을 안전하게 release하는 Lua Script이다.</li>
<li>키에 저장된 값과 기대하는 값이 같아야 한다.</li>
<li>다른 클라이언트가 생성한 lock을 삭제하면 안되기 때문에, 중요하다</li>
<li>lock validity time(위 커맨드에서 30000)은 자동 릴리즈 시간임과 동시에 클라이언트가 명령을 수행해야 하는 시간 제한 역할을 한다.</li>
</ul>
<h2 id="redlock-algorithm">Redlock Algorithm</h2>
<p>레디스 마스터가 여러개 운영되는 경우에는 각각이 독립적이기 때문에 분산 락 구현을 위해 Redlock 알고리즘을 사용한다.
분산 환경에서 락을 획득하기 위해 클라이언트는 아래 과정을 수행한다. (n이 5라는 가정)</p>
<ol>
<li>현재 시간을 밀리초로 가져온다.</li>
<li>n개, 즉 모든 인스턴스에 대해 락 획득을 시도한다. 이때 자동 릴리즈보다는 훨씬 적은 시간으로 타임아웃을 두어 락 획득에 너무 많은 시간을 쓰지 않도록 한다.</li>
<li>대부분의 인스턴스(최소 3개)에서 락 획득이 가능하고, 락을 획득하기까지 걸린 시간이 lock validity time보다 적다면, 해당 락을 획득한다.</li>
<li>락을 획득한 경우, lock validity time에서 흐른 시간을 뺀다.</li>
<li>락을 획득하지 못한 경우, 모든 락을 해제한다.(획득한 락이 없더라도)</li>
</ol>
<h3 id="retry-on-failure">Retry on Failure</h3>
<p>클라이언트가 락 획득에 실패하면, 랜덤한 딜레이 후에 재시도한다. 클라이언트는 여러 노드에 대한 락 시도가 빠를수록 효과적이기 때문에, 멀티플렉싱을 사용해 n개의 인스턴스에 동시에 SET 커맨드를 실행하는 것이 이상적이다</p>
<h1 id="secondary-indexing">Secondary Indexing</h1>
<p>레디스는 기본적으로 primary key access만 제공한다. 하지만 다양한 종류의 secondary index(복합 인덱스 등)을 만들기 위해 용량을 사용할 수 있다.</p>
<h2 id="sorted-sets">Sorted sets</h2>
<p>가장 간단한 방법은 sorted set을 사용하는 방법이다. 각 요소에 점수를 매겨 정렬하는 방식이다.</p>
<pre><code>ZADD myindex 25 Manuel
ZADD myindex 18 Anna
ZADD myindex 35 Jon
ZADD myindex 67 Helen</code></pre><p>아래와 같은 방식으로 범위 조회가 가능하다. WITHSCORES 옵션을 붙여 점수와 함께 반환받을 수도 있다.</p>
<pre><code>ZRANGE myindex 20 40 BYSCORE
1) &quot;Manuel&quot;
2) &quot;Jon&quot;</code></pre><h3 id="using-id-as-associated-values">Using id as associated values</h3>
<pre><code>HMSET user:1 id 1 username antirez ctime 1444809424 age 38
HMSET user:2 id 2 username maria ctime 1444808132 age 42
HMSET user:3 id 3 username jballard ctime 1443246218 age 33</code></pre><pre><code>ZADD user.age.index 38 1
ZADD user.age.index 42 2
ZADD user.age.index 33 3</code></pre><p>id로 인덱싱한 후, 나이로 한번 더 인덱싱하기 위해 위와 같이 구성할 수 있다.
하지만 나이가 변경될 경우 두 테이블을 함께 업데이트해야 한다</p>
<h3 id="lexicographical-indexes">Lexicographical indexes</h3>
<p>sorted set에는 재밌는 특징이 있는데, 같은 점수일 경우 memcomp() 함수를 이용해 사전식 정렬을 한다.</p>
<ul>
<li>&#39;BYSCORE&#39; 대신 &#39;BYLEX&#39;를 통해 사전순 범위 검색이 가능하다.</li>
<li><code>[</code>는 inclusive, <code>(</code>는 exclusive 이다.<pre><code>ZRANGE myindex [a (b BYLEX
1) &quot;aaaa&quot;
2) &quot;abbb&quot;</code></pre>위 구문은 a(포함)과 b 사이 모든 값을 가져오는 구문이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Test Code]]></title>
            <link>https://velog.io/@0_zoo/Test-Code</link>
            <guid>https://velog.io/@0_zoo/Test-Code</guid>
            <pubDate>Tue, 28 Nov 2023 11:39:43 GMT</pubDate>
            <description><![CDATA[<p>서버 개발을 시작한 후 정말 많이 들었던 얘기 중 하나는 테스트 코드가 정말 중요하다는 얘기였다. 김영한님의 강의를 들을 때부터 테스트코드는 정말 중요하며, 대부분의 기업에서 테스트 코드를 작성하는 데 정말 많은 시간을 쏟는다는 얘기를 들었고, 소마와 다른 동아리를 거치면서도 테스트 코드의 중요성에 대해 배워왔다. 그럼 테스트 코드는 뭐고, 왜 중요한걸까? </p>
<h2 id="test-code란">Test Code란?</h2>
<p>말 그대로 작성한 코드에 문제가 없는지 테스트하기 위해 작성하는 코드이다.</p>
<h3 id="테스트-기본-원칙">테스트 기본 원칙</h3>
<ol>
<li>테스팅은 결함이 없는 것이 아니라, 결함의 존재를 보여주는 것이다.</li>
<li>완벽한 테스트는 불가능하다.</li>
<li>테스트 구성은 가능한 빠른 시기에 시작한다.</li>
<li>결함은 군집되어 있다.(결함의 80%는 20%의 코드로 인해 발생한다.)</li>
<li>Pesticide paradox(살충제 역설) - 비슷한 테스트가 반복되면 새로운 결함을 발견할 수 없다.</li>
<li>테스트는 정황에 의존적이다.(상황에 따라 다르다.)</li>
<li>오류 부재의 오해 - 사용되지 않는 시스템이나 사용자의 기대에 부응하지 않는 기능의 결함을 찾고 수정하는 것은 의미가 없다.<h3 id="테스트-코드는-왜-작성해야-할까">테스트 코드는 왜 작성해야 할까?</h3>
<h4 id="디버깅-비용-절감">디버깅 비용 절감</h4>
애플리케이션은 항상 내적 결함을 가지고 있고, 좋은 코드와 설계를 하더라도 결함의 존재 자체를 부정할 수 없다. 
때문에 버그는 항상 발생하고, 개발자는 디버깅을 통해 이를 해결한다. </li>
</ol>
<p>하지만 문제를 해결하다보면 실제 해결하는 시간보다 문제를 찾기 위한 시간이 더 많다는 것을 깨닫게 된다. </p>
<p>테스트 코드 작성 습관화는 이 문제를 찾는 과정에 드는 시간을 줄여줌으로써 디버깅 비용을 줄이고, 개발자가 비즈니스 로직에 집중할 수 있게 해준다.</p>
<h4 id="코드-변경에-대한-불안감-해소">코드 변경에 대한 불안감 해소</h4>
<h5 id="회귀-버그">회귀 버그</h5>
<p>이전에 잘 작동하던 기능에 문제가 생기는 것을 가리킨다.</p>
<p>개발을 하다 보면 회귀 버그가 정말 많이 발생하게 된다.</p>
<h5 id="왜-발생할까">왜 발생할까?</h5>
<p>사실 이유는 간단한데, 애플리케이션은 단일 요소가 아닌 함수, 객체, 도메인 등 여러 요소가 상호작용하며 이루어지기 때문이다. 때문에 하나의 기능을 수정하더라도 수정된 요소가 다른 기능에 영향을 줄 수 있기 때문에 회귀 버그가 발생하게 된다.</p>
<h5 id="회귀-테스트가-필요한-이유">회귀 테스트가 필요한 이유</h5>
<p>회귀 테스트는 기능 추가나 오류 수정으로 인해 새롭게 유입되는 오류가 없는지 겁증해준다.
테스트 코드는 당시의 기능을 만들기 위해서만 필요한 코드가 아니다.
그 이후</p>
<ul>
<li>기능 변경을 위해 기존 코드를 수정하거나</li>
<li>더 나은 코드를 위해 리팩토링을 하거나
서비스가 지속 가능하게 발전하기 위해 필요한 코드이다.</li>
</ul>
<h4 id="더-나은-문서-자료">더 나은 문서 자료</h4>
<h5 id="문서의-방치">문서의 방치</h5>
<p>코드를 이해하기 편하게 하기 위해 문서화를 진행하지만, 쉽게 방치되기도 한다.
이는 문서와 코드를 같은 유지 보수 대상으로서 가져가는 것이 어렵기 때문인데, 이 때문에 신뢰하기가 어려워진다.</p>
<h5 id="코드와-가장-가까운-문서">코드와 가장 가까운 문서</h5>
<p>Behavior spec 스타일의 테스트코드를 보면 given(todo 저장), when(id가 중복되면), then(저장에 실패한다) 처럼 코드가 어떤 역할을 갖는지 명세를 작성하게 된다. 이는 기존 코드를 이해하기 위한 문서의 역할을 해 도움을 주게 된다.</p>
<h4 id="좋은-코드는-테스트하기-쉽다">좋은 코드는 테스트하기 쉽다.</h4>
<p>좋은 코드는 &quot;변경하기 쉬운&quot;이라는 형용사를 내포한다.
이는 약한 결합을 가지고 있는 코드를 뜻하며 강한 결합을 갖는 코드는 당연히 테스트하기 어렵다.</p>
<h5 id="좋은-코드의-지표">좋은 코드의 지표</h5>
<p>이것이 의미하는 것은 만약 내가 작성한 코드가 테스트하기 어려운 코드라면 안좋은 코드일 가능성이 높다는 것이다.</p>
<h4 id="테스트-자동화">테스트 자동화</h4>
<h5 id="개발자의-기도메타">개발자의 기도메타</h5>
<p>본인이 작성한 코드가 실제 운영 환경에 배포됐을 때 불안감에 휩싸여 기도하는 것을 말한다. 특히 실제 운영중인 서비스라면 더욱 더 큰 불안감에 휩싸일 수 밖에 없다.</p>
<h5 id="ci를-통한-테스트-자동화">CI를 통한 테스트 자동화</h5>
<p>CI를 통해 우리가 작성한 코드의 병합 순간 우리가 작성한 테스트 코드를 통해 버그가 배포되는 것을 막을 수 있다.
이를 통해 안정감 있는 프로젝트를 진행할 수 있고, 우리도 안정감이 생기게 된다.</p>
<h3 id="테스트-코드의-종류">테스트 코드의 종류</h3>
<h4 id="단위-테스트unit-test">단위 테스트(Unit Test)</h4>
<ul>
<li>가장 작은 단위의 테스트이며, 모든 테스트의 시작점이다.</li>
<li>개별적인 코드 단위(메소드 등)이 의도한 대로 작동하는지 확인하는 과정</li>
<li>F.I.R.S.T 원칙을 갖는다.<ul>
<li>Fast: 유닛 테스트는 빨라야 한다</li>
<li>Isolated: 다른 테스트에 종속적인 테스트는 작성하지 않는다</li>
<li>Repeatable: 테스트는 실행할 때마다 같은 결과를 내야 한다.</li>
<li>Self-validating: 테스트는 스스로 결과물이 옳은지 그른지 판단할 수 있어야 한다.</li>
<li>Timely: 유닛 테스트는 프로덕션 코드가 테스트를 성공하기 직전에 구성되어야 한다.</li>
</ul>
</li>
</ul>
<h4 id="통합-테스트integration-test">통합 테스트(Integration Test)</h4>
<ul>
<li>각각 시스템들이 서로 어떻게 상호작용하고 제대로 작동하는지 테스트하는 것을 의미한다 .</li>
<li>유닛 테스트는 다른 컴포넌트와 독립적이지만, 통합 테스트는 그렇지 않다. 유닛 테스트에서 데이터베이스에 접근하는 코드는 실제 데이터베이스와 통신하는 것은 아니지만, 통합 테스트는 실제 통신해야 한다.</li>
<li>통합 테스트는 서로 다른 시스템끼리 잘 소통하고 있는지(예를 들어 어플리케이션과 데이터베이스가 잘 상호작용하고 있는지..) 테스트할 때 사용된다.</li>
<li>때문에 유닛 테스트를 작성하는 것보다 복잡하고 오랜 시간이 걸리게 된다.</li>
</ul>
<h4 id="기능-테스트function-test">기능 테스트(Function Test)</h4>
<ul>
<li>E2E 테스트(End-to-end Test) 혹은 브라우저 테스트(Browser Test)라고도 불린다.</li>
<li>어떤 어플리케이션이 제대로 동작하는지 완전한 기능을 테스트하는 것을 의미한다.</li>
<li>최종 사용자의 흐름에 대한 테스트이며, 외부로부터의 요청부터 응답까지 기능이 잘 동작하는 지에 대한 테스트이다.</li>
</ul>
<h2 id="test-double">Test Double</h2>
<p>테스트 목적으로 실제 객체 대신 사용되는 모든 종류의 가상 객체를 뜻한다. xUnit Test Patterns의 저자인 Gerard Meszaros는 이를 <strong>Dummy, Fake, Stub, Spy, Mock</strong> 5가지 종류로 분류했다.</p>
<h4 id="dummy">Dummy</h4>
<p>전달되지만 실제로는 사용되지 않는다. 일반적으로 매개변수 목록을 채우는 데만 사용한다.</p>
<pre><code>test(&quot;FROM 계좌의 잔액이 부족하면 Failure 리턴&quot;) {
  // arrange
  val bankPortStub = object : BankPort {
      override fun getBalance(bankCode: String, accountNumber: String): Long {
          return 1000L
      }
      override fun withdraw(bankCode: String, accountNumber: String, amount: Long): BankPort.Result = TODO(&quot;Not yet implemented&quot;)
      override fun deposit(bankCode: String, accountNumber: String, amount: Long): BankPort.Result = TODO(&quot;Not yet implemented&quot;)
  }
  val sut = TransferBank(
      transferHistoryRepository = mockk(), // Dummy 객체
      bankPort = bankPortStub,
      emailPort = mockk(), // Dummy 객체
  )

  // act
  val actual = sut.invoke(
      from = TransferBankUseCase.BankAccount(&quot;088&quot;, &quot;1212121212&quot;),
      to = TransferBankUseCase.BankAccount(&quot;088&quot;, &quot;4242424242&quot;),
      amount = 100_000L,
  )

  // assert
  (actual is TransferBankUseCase.Result.Failure) shouldBe true
}</code></pre><p>emailPort와 TransferHistoryRepository는 사용되지는 않지만 전달되야 하는 값으로, mockk를 이용해 생성된 Dummy 객체를 전달한다.</p>
<h4 id="fake">Fake</h4>
<p>일의 능률을 향상시켜주지만 일반적으로 프로덕션에 적합하지 않다.(In-memory같은 방식)</p>
<pre><code>import org.jetbrains.exposed.sql.Database

// H2 In-memory database에 접속
val h2Database = Database.connect(&quot;jdbc:h2:mem:test;DB_CLOSE_DELAY=-1&quot;, driver = &quot;org.h2.Driver&quot;)

// mysql database에 접속
val mysqlDatabase = Database.connect(&quot;jdbc:mysql://localhost/test&quot;, driver = &quot;com.mysql.jdbc.Driver&quot;)</code></pre><p>프로덕션에서는 운영중인 MySQL 서버에 접속해서 데이터를 저장하고 조회하지만, 테스트코드에서는 In-memory Database(Fake 객체)를 사용해 기능을 테스트하는 방식이다.</p>
<h4 id="stub">Stub</h4>
<p>테스트할 동안 준비된 대답을 제공해준다. 테스트를 위해 프로그래밍된 것 외에는 응답하지 않는다.</p>
<pre><code>test(&quot;FROM 계좌의 잔액이 부족하면 Failure 리턴&quot;) {
  // arrange
  val bankPortStub = object : BankPort {
      override fun getBalance(bankCode: String, accountNumber: String): Long {
          return 1000L
      }
      override fun withdraw(bankCode: String, accountNumber: String, amount: Long): BankPort.Result = TODO(&quot;Not yet implemented&quot;)
      override fun deposit(bankCode: String, accountNumber: String, amount: Long): BankPort.Result = TODO(&quot;Not yet implemented&quot;)
  }
  val sut = TransferBank(
      transferHistoryRepository = mockk(), 
      bankPort = bankPortStub, // Stub 객체
      emailPort = mockk(), 
  )
  ...
}</code></pre><p>잔액을 테스트하는 함수에서 입력된 계좌번호에 상관 없이 1000을 반환한다.</p>
<h4 id="spy">Spy</h4>
<p>어떻게 부름을 받았는지에 따라 일부 정보를 기록하는 stub이다. ex) 전송된 메세지 수를 기록하는 이메일 서비스</p>
<pre><code>test(&quot;송금을 성공하면 이메일을 한 번 발송&quot;) {
    // arrange
    val transferHistoryRepositoryStub = object : TransferHistoryRepository {
        override fun findById(id: Long): TransferHistory = TODO(&quot;Not yet implemented&quot;)
        override fun save(history: TransferHistory): TransferHistory {
            return history
        }
    }
    val bankPortStub = object : BankPort {
        override fun getBalance(bankCode: String, accountNumber: String): Long {
            return 100_000L
        }
        override fun withdraw(bankCode: String, accountNumber: String, amount: Long): BankPort.Result {
            return BankPort.Result(&quot;success&quot;)
        }
        override fun deposit(bankCode: String, accountNumber: String, amount: Long): BankPort.Result {
            return BankPort.Result(&quot;success&quot;)
        }
    }
    val emailPortSpy = object : EmailPort {
        var emailCount = 0

        override fun sendEmail(content: String) {
            emailCount++
        }
        fun countSentEmail(): Int {
            return emailCount
        }
    }
    val sut = TransferBank(
        transferHistoryRepository = transferHistoryRepositoryStub,
        bankPort = bankPortStub,
        emailPort = emailPortSpy,
    )

    // act
    val actual = sut.invoke(
        from = TransferBankUseCase.BankAccount(&quot;088&quot;, &quot;1212121212&quot;),
        to = TransferBankUseCase.BankAccount(&quot;088&quot;, &quot;4242424242&quot;),
        amount = 100_000L,
    )

    // assert
    check(actual is TransferBankUseCase.Result.Success)
    emailPortSpy.countSentEmail() shouldBe 1
}</code></pre><p>Spy는 내가 확인하고자 하는 대상(emailCount)을 기록하는 것이 핵심이고 검증 단계에서 이 정보를 활용한다.</p>
<h4 id="mock">Mock</h4>
<p>수신할 것으로 예상되는 호출의 사양을 형성하는 기댓값으로, 미리 프로그래밍된 객체이다.</p>
<pre><code>test(&quot;송금 성공&quot;) {
    // arrange
    val transferHistoryRepositoryMock = mockk&lt;TransferHistoryRepository&gt;()
    val bankPortMock = mockk&lt;BankPort&gt;()
    val emailPort = mockk&lt;EmailPort&gt;()
    val sut = TransferBank(
        transferHistoryRepository = transferHistoryRepositoryMock,
        bankPort = bankPortMock,
        emailPort = emailPort,
    )
    every { bankPortMock.getBalance(any(), any()) } returns 100_000L
    every { bankPortMock.withdraw(any(), any(), any()) } returns BankPort.Result(&quot;success&quot;)
    every { bankPortMock.deposit(any(), any(), any()) } returns BankPort.Result(&quot;success&quot;)
    every { transferHistoryRepositoryMock.save(any()) } returns TransferHistory(
        id = 1L,
        fromBankCode = &quot;088&quot;,
        fromBankAccountNumber = &quot;1212121212&quot;,
        toBankCode = &quot;088&quot;,
        toBankAccountNumber = &quot;4242424242&quot;,
        amount = 100_000L,
    )
    every { emailPort.sendEmail(any()) } returns Unit

    // act
    val actual = sut.invoke(
        from = TransferBankUseCase.BankAccount(&quot;088&quot;, &quot;1212121212&quot;),
        to = TransferBankUseCase.BankAccount(&quot;088&quot;, &quot;4242424242&quot;),
        amount = 100_000L,
    )

    // assert
    (actual is TransferBankUseCase.Result.Success) shouldBe true
}</code></pre><p>Mock을 사용하면 내가 어떤 호출을 기대하고 그 호출에 대한 결과가 무엇인지 명세(specification)를 만들어놔야 한다. 위 코드에서 행동(behavior)에 대한 명세(specification)를 MockK에서 제공하는 every — returns 구문을 이용해서 정의했다.</p>
<h3 id="stubbing-vs-mocking">Stubbing vs Mocking</h3>
<ul>
<li>Stub: 테스트에 필요한 호출에 대해 미리 준비된 답을 제공하는 객체</li>
<li>Mock: 예상된 동작을 가진 객체</li>
</ul>
<p>의미만 놓고 보면 같아 보이지만, 테스트 코드를 작성하는 관점에서 바라보면 크게 2가지 차이가 있다.</p>
<h4 id="서로-다른-스타일로-작성된다">서로 다른 스타일로 작성된다</h4>
<ul>
<li>Stub: 실제 객체처럼 동작하는 클래스를 직접 구현하는데, 테스트에 필요한 구현에 집중하고 부가적인 기능은 구현하지 않는다.</li>
<li>Mock: 다양한 Mock Framework를 통해서 Mock 객체를 생성하고 특정 액션에 대한 출력을 정의한다.</li>
</ul>
<h4 id="상태-검증state-verification과-행동-검증behavior-verification">상태 검증(state verification)과 행동 검증(behavior verification)</h4>
<ul>
<li>Stub: 상태 검증을 사용한다. 어떤 입력에 대해서 어떤 출력이 발생하는지 검증한다.</li>
<li>Mock: 행동 검증을 사용한다. 입력과 상관없이 출력을 어떻게 만들어 내는지에 집중한다(위에 Mock 예시 코드에서 every — returns 구문을 사용한 부분 참고).</li>
</ul>
<h4 id="mockist-testing-vs-classical-testing-취향">Mockist Testing vs Classical Testing (취향)</h4>
<ul>
<li>Mockist Testing<ul>
<li>동작하는 모든 객체에 대해 항상 Mock을 사용한다.</li>
</ul>
</li>
<li>Classical Testing<ul>
<li>가능하면 실제 객체를 사용하고, 실제 객체를 사용하는 것이 어색할 때 Mock이나 Test Double을 사용하고, 되도록 Mock 사용을 지양한다.</li>
</ul>
</li>
</ul>
<h4 id="검증하고-싶은-대상에-따라-구분해-사용한다">검증하고 싶은 대상에 따라 구분해 사용한다.</h4>
<ul>
<li>내가 검증하고 싶은 대상이 입력에 관계없이 어떤 행동을 했을때 내가 원하는 출력이 나오기만 해도 상관없다면 Mock을 사용하면 된다.</li>
<li>내가 검증하고 싶은 대상에 상태 검증이 필요하다면 Stub을 사용하면 된다.</li>
</ul>
<h3 id="test-fixture">Test Fixture</h3>
<p>중복 발생되는 무언가를 고정시켜 한곳에 관리하도록 하겠다는 개념</p>
<h4 id="test-fixture-메소드-사용">Test Fixture 메소드 사용</h4>
<pre><code>class LottoTicketsTest {
    private List&lt;LottoTicket&gt; ascendingLottoTickets;
    private List&lt;LottoNumber&gt; ascendingLottoNumbers;
    private LottoTickets lottoTickets;

    @BeforeEach
    void setUp() {
        ascendingLottoNumbers = IntStream.rangeClosed(1, 6)
                .mapToObj(LottoNumber::new)
                .collect(Collectors.toList());
        ascendingLottoTickets = IntStream.range(0, 2)
                .mapToObj(ticketCount -&gt; new LottoTicket(ascendingLottoNumbers))
                .collect(Collectors.toList());
        lottoTickets = new LottoTickets(ascendingLottoTickets);
    }

    @Test
    @DisplayName(&quot;여러장의 로또 생성&quot;)
    void create() {
        assertThat(lottoTickets).isEqualTo(new LottoTickets(ascendingLottoTickets));
    }

    @Test
    @DisplayName(&quot;일치 번호 개수 리스트 반환&quot;)
    void numberOfMatches() {
        List&lt;LottoNumber&gt; winningLottoNumbers = Arrays.asList(new LottoNumber(1), new LottoNumber(2), new LottoNumber(3), new LottoNumber(4), new LottoNumber(5), new LottoNumber(45));
        List&lt;LottoRank&gt; lottoRanks = lottoTickets.lottoRanks(winningLottoNumbers);
        assertThat(lottoRanks).isEqualTo(Arrays.asList(LottoRank.SECOND, LottoRank.SECOND));
    }
}</code></pre><p>@BeforeAll, @AfterAll, @BeforeEach, @AfterEach를 사용하는 방식. 각 메소드 시작 전/ 종료 후 혹은 테스트 메소드 시작 전 / 종료 후에 실행된다.</p>
<h4 id="fixture-분리">Fixture 분리</h4>
<pre><code>public class LottoTicketsFixtures{
    public static LottoTicket createLottoTickets(){
        ascendingLottoNumbers = IntStream.rangeClosed(1, 6)
                .mapToObj(LottoNumber::new)
                .collect(Collectors.toList());
        ascendingLottoTickets = IntStream.range(0, 2)
                .mapToObj(ticketCount -&gt; new LottoTicket(ascendingLottoNumbers))
                .collect(Collectors.toList());
        return new LottoTickets(ascendingLottoTickets);
    }
}</code></pre><p>Fixture 클래스로 분리하는 방식</p>
<pre><code>public class LottoTicketTests{
    private LottoTickets lottotickets = LottoTicketsFixtures.createLottoTickets();
    @Test
    ...</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] Interact with data in Redis]]></title>
            <link>https://velog.io/@0_zoo/Redis-Interact-with-data-in-Redis</link>
            <guid>https://velog.io/@0_zoo/Redis-Interact-with-data-in-Redis</guid>
            <pubDate>Sun, 26 Nov 2023 07:19:56 GMT</pubDate>
            <description><![CDATA[<h2 id="search-and-query">Search and query</h2>
<h3 id="주요-특징">주요 특징</h3>
<h4 id="secondary-indexing">Secondary Indexing</h4>
<ul>
<li>레디스는 기본적으로 primary key access만 제공한다</li>
<li>자료구조 서버이기 때문에, 용량을 secondary index를 위해 사용할 수 있다.</li>
<li>다음 자료구조에서 동작한다.</li>
<li>Sorted Sets<ul>
<li>id 혹은 다른 정수형 필드를 이용한 인덱스</li>
<li>사전식 정렬 혹은 복합 인덱스와 그래프 순회 인덱스</li>
</ul>
</li>
<li>Sets<ul>
<li>랜덤 인덱스</li>
</ul>
</li>
<li>Lists<ul>
<li>단순 순회 인덱스와 마지막 N개의 아이템 인덱스<h4 id="multi-field-queries">Multi-field queries</h4>
<h4 id="aggregations">Aggregations</h4>
</li>
</ul>
</li>
<li>Group과 각 group에 reducer 사용 가능</li>
<li>하나 이상의 필드에 대한 정렬</li>
<li>문자열이나 수학 함수 사용(선택적으로 새로운 필드를 만들거나 존재하는 필드를 대체하거나)</li>
<li>Limit</li>
<li>Filter<h5 id="commands">Commands</h5>
<pre><code>FT.AGGREGATE
{index_name:string}
{query_string:string}
[VERBATIM]
[LOAD {nargs:integer} {property:string} ...]
[GROUPBY
  {nargs:integer} {property:string} ...
  REDUCE
    {FUNC:string}
    {nargs:integer} {arg:string} ...
    [AS {name:string}]
  ...
] ...
[SORTBY
  {nargs:integer} {string} ...
  [MAX {num:integer}] ...
] ...
[APPLY
  {EXPR:string}
  AS {name:string}
] ...
[FILTER {EXPR:string}] ...
[LIMIT {offset:integer} {num:integer} ] ...
[PARAMS {nargs} {name} {value} ... ]</code></pre><h4 id="다수-field에-대한-full-text-indexing">다수 field에 대한 full-text indexing</h4>
<h4 id="기능-저하-없는-증분-색인">기능 저하 없는 증분 색인</h4>
<blockquote>
<p>증분 색인(incremental indexing)이란?
동적 색인(dynamic indexing)이라고도 하며, 전체 색인이 완료된 이후 추가된 데이터에 대한 수집/ 색인을 말한다.</p>
</blockquote>
</li>
</ul>
<h4 id="document-ranking">Document ranking</h4>
<h4 id="boolean-queriesand-or-not">Boolean queries(AND, OR, NOT)</h4>
<h4 id="optional-match-query-절">Optional Match query 절</h4>
<ul>
<li>Optional한 값 리턴 가능<h4 id="prefix-based-searches">Prefix-based searches</h4>
<pre><code>hel* world</code></pre><h4 id="field-weights">Field weights</h4>
</li>
<li>필드에 중요성에 따라 weight 부여 가능<h4 id="auto-complete과-prefix-제안">Auto-complete과 prefix 제안</h4>
<h4 id="정확한-문구-search와-slop-based-search">정확한 문구 search와 slop-based search</h4>
<blockquote>
<p>Slop-based search란?
정확한 문구 뿐만 아니라 value만큼의 단어가 추가적으로 들어가있어도 함께 보여주는 방식</p>
</blockquote>
</li>
</ul>
<h4 id="형태소-기반-다국어-지원">형태소 기반 다국어 지원</h4>
<ul>
<li>hiring을 query 해도 hire,hired 결과 함께 리턴<h4 id="쿼리-확장과-scoring을-위한-custom-function-지원">쿼리 확장과 scoring을 위한 custom function 지원</h4>
<h4 id="numeric-filter과-range-지원">numeric filter과 range 지원</h4>
<h4 id="geo-filtering-지원">Geo Filtering 지원</h4>
</li>
<li>GEOADD, GEODIST, GEOHASH 등등..<h4 id="벡터-유사도-검색-지원">벡터 유사도 검색 지원</h4>
<h4 id="unicode-지원">Unicode 지원</h4>
<h4 id="full-document-contents-혹은-id만-조회-가능">full document contents 혹은 ID만 조회 가능</h4>
<h4 id="document-deletion-and-updating-with-index-garbage-collection">document deletion and updating with index garbage collection</h4>
</li>
<li>redis에서 delete는 실제 삭제하는것이 아닌, deleted로 표시하는 방식으로 함</li>
<li>deleted된 데이터의 id는 garbage가 됨</li>
<li>updating 또한 비슷한 방식. 데이터를 deleted 처리한 후 새 id로 데이터를 추가하는 방식<blockquote>
<p>GC 작동 방식 (이해 잘 안됨..)</p>
<ol>
<li>각 블럭에 대해 reader, writer 생성</li>
<li>블럭의 record 하나씩 읽음</li>
<li>invalid한 record가 없으면 아무것도 안함</li>
<li>garbage record 찾으면 reader가 advanced됨</li>
<li>하나 이상의 garbage record를 찾으면, 다음 record가 writer에 encode되며, delta를 다시 계산함</li>
</ol>
</blockquote>
</li>
</ul>
<h2 id="redis-programmability">Redis programmability</h2>
<ul>
<li>Lua와 Redis Function을 통해 프로그래밍 인터페이스를 제공한다.</li>
<li>Redis 7 이상에서는 Redis Function을, 6.2 이하에서는 Lua Scripting with Eval command를 사용할 수 있다.<h3 id="lua-scripting-redis-260">Lua Scripting (Redis 2.6.0~)</h3>
<h4 id="eval-command">EVAL command</h4>
</li>
<li>server-side scripts를 돌릴 수 있다</li>
<li>반드시 소스 코드를 갖고 있어야 한다.</li>
<li>script는 서버에만 캐시로 저장되며 휘발성이기 때문이다.</li>
<li>이는 어플리케이션이 커질수록 개발과 유지보수를 힘들게 만든다.<pre><code>&gt; EVAL &quot;return &#39;Hello, scripting!&#39;&quot; 0
&quot;Hello, scripting!&quot;</code></pre>첫번째 인자는 Lua source code, 두번째 인자는 스크립트에 뒤따르는 인자의 개수, 그 뒤는 전달되는 레디스 키의 개수이다.<h4 id="scripting-parameterization">Scripting parameterization</h4>
<pre><code>redis&gt; EVAL &quot;return ARGV[1]&quot; 0 Hello
&quot;Hello&quot;
redis&gt; EVAL &quot;return ARGV[1]&quot; 0 Parameterization!
&quot;Parameterization!&quot;
</code></pre></li>
</ul>
<p>redis&gt; EVAL &quot;return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }&quot; 2 key1 key2 arg1 arg2 arg3</p>
<p>1) &quot;key1&quot;
2) &quot;key2&quot;
3) &quot;arg1&quot;
4) &quot;arg2&quot;
5) &quot;arg3&quot;</p>
<pre><code>ARGV를 통해 받은 인자 사용, KEY를 통해 받은 redis key를 사용할 수 있다. 두번째 인자는 받을 redis key의 개수를 나타낸다.
#### Interacting with Redis from a script</code></pre><blockquote>
<p>EVAL &quot;return redis.call(&#39;SET&#39;, KEYS[1], ARGV[1])&quot; 1 foo bar
OK</p>
</blockquote>
<pre><code>- redis.call() 혹은 redis.pcall()을 통해 Redis commands를 호출할 수 있다.
  - redis.call(): 런타임 에러가 클라이언트에게 전달됨 
  - redis.pcall(): 런타임 에러가 스크립트에 전달됨

#### Script Cache
- EVAL을 사용할 때, 항상 script code를 포함한다.
- 계속해서 같은 코드를 포함하는 것은 비효율적이기 때문에, Redis에서는 script cache를 제공한다.
- EVAL로 실행하는 모든 script는 cache에 저장된다.
- SCRIPT LOAD command와 그 소스 코드를 이용해 SHA1 digest를 얻을 수 있다.
- EVALSHA SHA1 digest를 통해 캐싱된 스크립트를 실행 가능하다.
- Redis Script Cache는 휘발성이기 때문에, 서버 재시작, SCRIPT FLUSH 등 다양한 이유로 언제든 사라질 수 있다.</code></pre><p>redis&gt; SCRIPT LOAD &quot;return &#39;Immabe a cached script&#39;&quot;
&quot;c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f&quot;
redis&gt; EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
&quot;Immabe a cached script&quot;</p>
<pre><code>
#### SCRIPT Commands
##### SCRIPT FLUSH
Redis cache에 저장된 모든 script를 지운다. 보통 Redis instance를 다른 유저에게 할당할 때 유용하다.
##### SCRIPT EXISTS
SHA1 digests를 인자로 받아 캐시에 존재하는 script인지 아닌지를 1, 0으로 리턴한다
##### SCRIPT Load script
script를 cache에 등록한다.
##### SCRIPT KILL
long-running script를 멈춘다.
##### SCRIPT DEBUG
script를 디버그한다.

### Redis Function (Redis 7~)
- script를 어플리케이션과 분리해서 script의 독자적인 개발, 테스팅, 그리고 배포를 가능케 한다.
- function을 사용하기 위해, load만 하면 된다.
#### atomic execution
- script나 function을 실행하면, 레디스는 원자성을 보장한다.
- 실행동안 서버는 block 된다.
- 트랜잭션과 비슷한 의미를 갖는데, 일어나거나 아무 일도 일어나지 않거나.
- 때문에 느린 스크립트를 사용하려 하면 다른 클라이언트가 모두 block된다는 점을 유의해야 한다.
#### Loading function</code></pre><p>FUNCTION LOAD &quot;#!lua<engine name> name=<library name>&quot;</p>
<pre><code>library payload는 Shebang format을 따라야 한다.
#### Registering function</code></pre><p>#!lua name=mylib
redis.register_function(
  &#39;knockknock&#39;,
  function() return &#39;Who&#39;s there?&#39; end
)</p>
<pre><code>#### Calling function</code></pre><p>redis&gt; FUNCTION LOAD &quot;#!lua name=mylib\nredis.register_function(&#39;knockknock&#39;, function() return &#39;Who\&#39;s there?&#39; end)&quot;
mylib
redis&gt; FCALL knockknock 0
&quot;Who&#39;s there?&quot;</p>
<pre><code>### Read-only scripts
- 말 그대로 Read-only, 어떤 데이터도 수정하지 않음
- script에 no-write flag를 추가하거나, EVAL_RO, EVALSHA_RO, FCALL_RO같은 readonly command를 통해 가능하다
- 다음 특징을 갖는다.
  - 복제본에 대해 실행될 수 있다.
  - script kill 커맨드에 의해 kill될 수 있다.
  - redis가 memory limit을 넘어서도 OOM이 발생하지 않는다.
  - write pause동안 block되지 않는다.
  - data를 수정하는 어떤 command도 실행할 수 없다
#### Read-only script history
- Redis 7.0에서 처음 소개됐다.
- Redis 7.0.1 전까지
  - PUBLISH, SPUBLISH, PFCOUNT는 write command로 고려되지 않았다.
  - no-write flag가 allow-oom을 포함하지 않았다.
  - no-write flag가 write pause동안 스크립트 작동을 허용하지 않았다.
### Sandboxed script context
- Redis는 user script를 sandbox내에서 실행할 수 있는 엔진을 마련해놨다.
- sandbox는 스크립트 오용과 잠재적 위험으로부터의 서버 보호를 위한 장치로 사용된다.
- script는 redis의 host system(파일 시스테 , 네트워크, 시스템 콜 등)에 절대 접근해서는 안된다.
- script는 redis에 저장된 데이터와 실행 인자로 받은 데이터로만 작동해야 한다.
### Maximum execution time
- script는 최대 실행 시간을 갖는다.(default 값은 5초이다.)
- 굉장히 널널한 편인데, script가 보통 1 ms 이내에 종료되기 때문이다. 사실 이 시간은 무한루프를 방지하기 위한 장치라고 보면 된다.
- redis.conf에서 바꾸거나 CONFIG SET 명령어를 통해 바꿀 수 있다. busy-reply-threshold를 변경하면 된다.
#### Maximum execution time 작동 방식
최대 실행 시간을 넘는다고 Redis가 바로 종료시키는 것이 아니다. 그렇게 하면 데이터셋에 절반만 변화를 주고 멈출 수 있기 때문에, 다음 과정을 거친다.

1. script가 너무 오래 실행중이라고 로그를 남긴다.
2. command를 받기 시작하나 모든 command에 대해 busy error를 리턴한다. 이 상태에서 허용되는 command는 SCRIPT KILL, FUNCTION KILL, 그리고 SHUTDOWN NOSAVE뿐이다.
3. readonly command만 실행한 경우에는 SCRIPT KILL, FUNCTION KILL을 사용할 수 있다.
4. 한번이라도 write를 수행한 경우에는 SHUTDOWN NOSAVE만 가능하다. (SHUTDOWN NOSAVE는 현재 데이터를 저장하지 않고 서버를 멈추는 키워드이다.)

## Transactions
- Redis Transactions는  command들이 single step으로 실행되도록 한다.
- 이는 MULTI, EXEC, DISCARD, WATCH 등의 command를 통해 이루어진다.
- 두가지를 보장한다. 
  - transaction 내에서 모든 command는 복호화되고 순서대로 작동한다. 다른 클라이언트의 요청이 절대 실행 중간에 실행되지 않는다.
  - EXEC command가 transaction 내의 모든 command 실행을 촉발하기 때문에, EXEC command 실행 전 서버와의 연결이 끊기면 아무 명령도 실행되지 않고, EXEC command를 실행한다면 모든 명령이 실행된다. append-only file을 사용하면 redis는 single write syscall을 사용해 디스크에 transaction을 쓰는데, 이 경우 조금 어려운 일이지만 서버가 crash나거나 kill될 경우 일부 명령만 등록될 수 있다. 레디스는 재시작 시에 이를 인지하고, 에러를 내며 종료한다. redis-check-aof tool을 사용하면 기록된 일부 transaction을 제거하고, 재시작할 수 있다

### Commands</code></pre><blockquote>
<p>MULTI // Transaction 진입, 항상 ok를 응답한다.
OK
INCR foo // 명령이 바로 수행되는 것이 아닌 queue에 쌓인다. QUEUED 응답을 받는다.
QUEUED
INCR bar
QUEUED
EXEC // queue에 쌓인 모든 명령이 수행된다. 응답이 배열로 반환된다.</p>
</blockquote>
<p>1) (integer) 1
2) (integer) 1</p>
<blockquote>
<p>DISCARD // transaction 탈출, 아무 명령도 수행되지 않는다.</p>
</blockquote>
<pre><code>
### Error inside a transaction
두가지 방식의 에러를 만날 수 있다.

1. queue에 넣는 과정에서 에러 발생
- EXEC 호출 전 에러가 나는 경우이다. syntax적으로 잘못됐거나(인자 개수가 잘못됐거나, command name이 잘못됐거나 등등,,), critical condition 문제가 있거나(OOM 등)
2. EXEC 실행 후 에러 발생
- key에 대해 잘못된 명령을 실행하는 경우(string value에 list operation 사용 등..)

Redis 2.6.5부터, command를 accumulate하는 과정에서 에러를 감지한다.
&gt; 2.6.5 전에는 반환값이 QUEUED인지 error인지 클라이언트가 확인 후, 에러가 났다면 transaction을 discard하는 방식으로 했다고 한다.
</code></pre><p>MULTI
+OK
INCR a b c
-ERR wrong number of arguments for &#39;incr&#39; command</p>
<pre><code>
EXEC 이후 발생하는 에러는 따로 처리되지 않는다. 몇몇 command가 fail한다고 해도, 다른 모든 명령은 정상적으로 실행된다. command가 실패하더라도, 멈추지 않는다.</code></pre><p>MULTI
+OK
SET a abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-WRONGTYPE Operation against a key holding the wrong kind of value</p>
<pre><code>
### Rollback
Redis는 Rollback을 지원하지 않는다. Rollback을 지원하는 것은 Redis의 단순함과 성능에 엄청난 영향이 있기 때문이다.

### Optimistic locking using check-and-set
- WATCH command는 check-and-set(CAS)를 제공하는데 사용된다.
- WATCH된 key가 EXEC 전에 변경되면, 해당 transaction은 abort되고 EXEC은 transaction이 실패했다는 것을 알려주기 위해 null을 응답한다.</code></pre><p>WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC</p>
<pre><code>위 상황에서, WATCH와 EXEC 사이에 다른 client에 의해 mykey가 변경된다면 transaction은 abort된다.

- 몇개의 key든 상관없이 WATCH할 수 있다.
- EXEC이 실행되거나 client connection이 종료되면, 모든 키는 UNWATCH 된다.
- UNWATCH command를 통해 모든 key를 UNWATCH할 수도 있다.
## Redis Pub/Sub
SUBSCRIBE, UNSUBSCRIBE, 그리고 PUBLISH는 Publish/Subscribe messaging paradigm을 따른다.
senders(publishers)는 receivers(subscribers)에게 직접 message를 보내는 것이 아닌 channels에 publish한다. 당연히 publishers는 누가 message를 읽을 지 모르고, Subscribers는 하나 이상의 channel에 interest를 표시하고 해당 channel의 message를 받을 수 있다. 이런 식의 publisher과 subscriber의 분리는 scalable하고 dynamic하게 네트워크를 구성할 수 있게 해준다.

### Commands</code></pre><p>// SUBSCTIBE
SUBSCRIBE channel_name channel_name ... // 채널 subscribe
PSUBSCRIBE pattern pattern ... // 패턴에 해당하는 채널 subscribe</p>
<p>// UNSUBSCRIBE
UNSUBSCRIBE channel_name channel_name ... // 채널 unsubscribe, channel_name 안쓰면 모든 채널 unsubscribe
PUNSUBSCRIBE pattern pattern ... // 패턴에 해당하는 채널 unsubscribe, pattern 안쓰면 모든 채널 unsubscribe</p>
<p>// PUBLISH
PUBLISH channel_name message // message를 channel로 보냄</p>
<p>// PUBSUB
PUBSUB channels pattern // pattern에 해당하는 channel 조회, 비워두면 모든 channel 조회
PUBSUB numsub channel_name channel_name ... // 각 channel에 등록된 client 개수 조회
PUBSUB numpat // 서버에 등록된 pattern 개수 조회</p>
<pre><code>
### Protocol에 따른 차이
RESP2 Client는 Subscribe 상태에서 다른 channel을 subscribe하거나 unsubscribe하는 command밖에 실행하지 못한다. 하지만 RESP3에서는 어떤 command라도 실행할 수 있다. 

### Message Format
message는 3개의 element로 구성된 array형식이다.
- 첫번째 element는 message의 종류를 나타낸다.
  - subscribe: 두번째 element인 channel에 성공적으로 subscribe했음을 나타냄. 세번쩨 element는 현재 subscribe중인 channel의 수를 보여준다.
  - unsubscribe: 두번째 element인 channel에 성공적으로 unsubscribe했음을 나타냄. 세번쩨 element는 현재 subscribe중인 channel의 수를 보여준다. 세번째 element가 0이라면, 더는 구독중인 상태가 아니기 때문에 어떤 redis command든 입력할 수 있는 상태가 된다.
  - message: 다른 client의 publish를 통해 받는 message를 의미한다. 두번째 element는 channel 이름, 세번째 element는 실제 message를 보여준다.

### Sharded Pub/Sub
Redis 7.0부터 지원하는 방식이다.
#### Redis Cluster에서의 Pub/Sub 문제점
![](https://velog.velcdn.com/images/0_zoo/post/0a8a6d87-2f63-4caf-9db8-4f8bec0baf45/image.png)
- Redis Cluster에서 Pub/Sub은 모든 노드에 데이터를 뿌리게 됨
- 한대의 서버에 publish하면 해당 노드는 모든 노드에 publish를 broadcast하게 됨
- 항상 broadcast하는 것이 문제가 됨
#### Sharded Pub/Sub은?
![](https://velog.velcdn.com/images/0_zoo/post/3c16f6e2-015a-4f6c-a8a1-b58a2db29ec5/image.png)
- key를 crc16으로 hash해서 해당 키가 속한 slot을 처리하는 서버로만 메시지를 전달한다.
- client는 primary 혹은 그 replicas 중 하나에 연결함으로써 메시지를 받을 수 있다.
- Redis cluster의 Pub/Sub에서 일어나던 노드간의 전파가 일어나지 않기에, horizontal하게 scale을 올릴 수 있게 해준다.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] Index]]></title>
            <link>https://velog.io/@0_zoo/MySQL-Index</link>
            <guid>https://velog.io/@0_zoo/MySQL-Index</guid>
            <pubDate>Tue, 21 Nov 2023 09:50:16 GMT</pubDate>
            <description><![CDATA[<h2 id="인덱스란">인덱스란?</h2>
<p>인덱스는 데이터베이스에서 데이터를 빠르게 검색하고, 조회하기 위한 자료구조이다. 사전 뒷편에 있는 찾아보기와 같은 역할을 한다고 생각하면 된다. 인덱스는 특정 칼럼 또는 칼럼의 조합에 대한 값과 해당 값이 존재하는 테이블 내의 물리적인 위치를 매핑한다. 이를 통해 데이터베이스는 효율적으로 데이터를 검색하고 필요한 정보를 빠르게 가져올 수 있다.</p>
<h3 id="단점">단점</h3>
<ul>
<li>인덱스는 대략 테이블 크기의 10% 공간이 추가로 필요하다.</li>
<li>Select가 아닌 데이터의 변경 작업(Insert, Update, Delete)가 자주 일어나면 오히려 성능에 악영향(정렬 작업으로 인해)<h2 id="mysql-인덱스-유형">MySQL 인덱스 유형</h2>
<h3 id="b-tree-인덱스">B-Tree 인덱스</h3>
가장 일반적으로 사용되는 인덱스 유형으로, 검색 및 정렬 작업에 효과적
데이터를 B-Tree 구조로 저장하여 빠른 검색이 가능하다.<h4 id="b-tree">B-Tree</h4>
<img src="https://velog.velcdn.com/images/0_zoo/post/55b4dda8-9af6-405b-b3db-a49e8e3d707c/image.png" alt=""></li>
<li>루트로부터 리프까지의 거리가 일정한 트리 구조</li>
<li>항상 정렬된 균형 상태를 유지한다. 따라서 탐색이 빠르다(O(logN))</li>
<li>하지만 재 정렬하는 작업으로 인해 노드 삽입 및 삭제 시 일반적인 트리보다 성능이 떨어진다.<h4 id="b-tree를-선택한-이유">B-Tree를 선택한 이유</h4>
B-Tree의 각 노드는 배열로서 실제 메모리 상에 저장이 되어 있다. 같은 노드 공간의 데이터들끼리 굳이 자식 노드처럼 참조 포인터 값으로 접근할 필요가 없는 것이다. 즉 같은 노드 안에서 데이터를 탐색할 때, 포인터 접근이 아닌 실제 메모리 디스크에서 바로 다음 인덱스에 접근을 하게 된다.
<img src="https://velog.velcdn.com/images/0_zoo/post/93dc24ad-a242-4b9b-bc63-c55160da73d0/image.png" alt="">
위 사진에서 200이라는 값을 찾는다고 가정해보자. 순서는 다음과 같다.</li>
</ul>
<ol>
<li>루트 노드에서 100, 155, 226을 탐색한다. 순차적으로 저장됐기에 빠르게 탐색할 수 있다.</li>
<li>루트 노드에 200이 없기 때문에, 155와 226 사이의 포인터가 존재하는 지 확인 후 해당 포인터를 통해 자식 노드로 접근한다.</li>
<li>자식 노드로 가 168, 200을 탐색 후 200을 찾아낸다. 이 과정 또한 순차적으로 저장됐기에 빠르다.</li>
</ol>
<h3 id="해시-인덱스">해시 인덱스</h3>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/69d8c749-bf04-4166-935e-653faf974eb8/image.png" alt=""></p>
<ul>
<li>특정 칼럼의 값을 해시 함수를 사용해 해시 값으로 변환하여 인덱스를 생성한다.</li>
<li>해시 값은 고유한 값으로, 해시 함수에 의해 계산된 값에 해당하는 위치에 데이터를 저장한다.</li>
<li>해시 인덱스는 해시 값에 해당하는 데이터를 빠르게 검색할 수 있지만, 데이터의 정렬이나 범위 검색에는 적합하지 않을 수 있다.</li>
<li>해대량의 데이터 속에서 특정 값을 빠르게 찾을 때 유용하다.</li>
<li>메모리 기반 db에서 많이 쓰인다.</li>
</ul>
<h2 id="인덱스-종류">인덱스 종류</h2>
<h3 id="클러스터-인덱스clustered-index">클러스터 인덱스(Clustered Index)</h3>
<ul>
<li>테이블당 1개만 존재할 수 있음</li>
<li>Primary key로 지정된 컬럼은 자동으로 클러스터링 인덱스가 생성됨</li>
<li>실제 저장된 데이터와 같은 무리의 페이지 구조를 가짐</li>
<li>클러스터링 인덱스를 기준으로 데이터 자동 정렬<ul>
<li>기본 키를 변경할 시 해당 키를 기준으로 다시 정렬<h3 id="보조-인덱스secondary-index">보조 인덱스(Secondary Index)</h3>
</li>
</ul>
</li>
<li>한 테이블에 여러개 설정 가능</li>
<li>UNIQUE 키워드로 고유 컬럼 지정시 자동으로 보조 인덱스가 생성됨</li>
<li>실제 저장된 데이터와 다른 무리의 페이지 구조를 가짐</li>
<li>클러스터링 데이터와 달리 데이터를 정렬하지 않음</li>
<li>CREATE INDEX 문으로 직접 보조 인덱스 생성 가능</li>
</ul>
<h2 id="인덱스-동작-원리">인덱스 동작 원리</h2>
<p>위에서 말했듯, 클러스터 인덱스와 보조 인덱스는 모두 B-Tree로 만들어진다.
B-Tree에서 데이터가 저장되는 공간을 노드라고 하고, MySQL에서는 이러한 노드들을 페이지라고 부른다.
MySQL에서는 페이지를 나누어 데이터를 저장하는데, 페이지는 MySQL 기준 최소 16KB의 크기를 가지며 페이지 데이터 공간이 추가적으로 필요할 경우 페이지 분할 작업을 통해 페이지를 생성하고 분할하는 작업을 거친다. 이 작업이 자주 일어나게 되면 데이터베이스 성능에 큰 영향을 미친다.</p>
<h3 id="클러스터-인덱스clustered-index-1">클러스터 인덱스(Clustered Index)</h3>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/6743e7ec-8c39-4bc9-9ef4-5bff62977390/image.png" alt=""></p>
<ul>
<li>실제 데이터가 정렬된다.</li>
<li>검색 시 먼저 루트 노드에서 탐색할 페이지를 찾고 해당 페이지에서 검색할 데이터를 찾게 됨으로써 검색 시간을 줄인다.<h3 id="보조-인덱스secondary-index-1">보조 인덱스(Secondary Index)</h3>
<img src="https://velog.velcdn.com/images/0_zoo/post/ec55f54f-81cf-4066-a3f9-a093387f16c9/image.png" alt=""></li>
<li>실제 데이터를 정렬하지 않는다.</li>
<li>보조 인덱스를 설정해도 데이터 페이지에는 영향이 없고, 별도 장소에 인덱스 페이지를 생성한다. </li>
<li>별도로 생성된 인덱스 페이지는 인덱스가 걸린 컬럼 값에 따라 정렬되어 별도의 페이지를 생성한다.</li>
<li>리프 페이지는 실제 데이터가 저장된 위치를 가리킨다. 클러스터 인덱스와 함께 사용할 경우, 정렬된 데이터 페이지를 가리킨다.</li>
</ul>
<h2 id="commands">Commands</h2>
<h3 id="인덱스-생성">인덱스 생성</h3>
<pre><code>CREATE [UNIQUE] INDEX 인덱스 이름
    ON 테이블 이름 (열 이름) [ASC | DESC]</code></pre><ul>
<li>UNIQUE 옵션은 중복이 안되는 고유 인덱스를 생성하는 것인데, 생략하면 중복이 허용된다.</li>
<li>ASC 또는 DESC로 인덱스 정렬 방향 변경 가능<h3 id="인덱스-제거">인덱스 제거</h3>
<pre><code>DROP INDEX 인덱스_이름 ON 테이블_이름</code></pre></li>
<li>기본 키, 고유 키(UNIQUE)로 자동 생성된 인덱스는 제거 불가<ul>
<li>기본 키나 고유 키를 제거하면 가능</li>
</ul>
</li>
<li>인덱스를 제거할 때는 보조 인덱스부터 제거하는 것이 좋다.<h3 id="인덱스-확인">인덱스 확인</h3>
<pre><code>SHOW INDEX FROM 테이블 이름</code></pre><h3 id="인덱스-사용-확인">인덱스 사용 확인</h3>
<pre><code>EXPLAIN
Query...</code></pre></li>
<li>어떤 table에서 어떤 인덱스를 사용했는 지 볼 수 있다.<h2 id="단일-인덱스-다중-컬럼-인덱스">단일 인덱스, 다중 컬럼 인덱스</h2>
</li>
</ul>
<h3 id="table1단일-인덱스">Table1(단일 인덱스)</h3>
<pre><code>CREATE TABLE table1(
    uid INT(11) NOT NULL auto_increment,
    id VARCHAR(20) NOT NULL,
    name VARCHAR(50) NOT NULL,
    address VARCHAR(100) NOT NULL,
    PRIMARY KEY(&#39;uid&#39;),
    key idx_name(name),
    key idx_address(address)
)</code></pre><h3 id="table2다중-컬럼-인덱스">Table2(다중 컬럼 인덱스)</h3>
<pre><code>CREATE TABLE table2(
    uid INT(11) NOT NULL auto_increment,
    id VARCHAR(20) NOT NULL,
    name VARCHAR(50) NOT NULL,
    address VARCHAR(100) NOT NULL,
    PRIMARY KEY(&#39;uid&#39;),
    key idx_name(name, address)    
)</code></pre><p>Table 1은 name과 address에 대한 단일 인덱스, Table 2는 (name, address)에 대한 다중 컬럼 인덱스를 설정했다.</p>
<h3 id="query1">Query1</h3>
<pre><code>SELECT * FROM table1 WHERE name=&#39;홍길동&#39; AND address=&#39;경기도&#39;;</code></pre><p>Table1의 경우 단일 인덱스가 걸려있기 때문에 name과 address 중 어떤 컬럼이 더 빠르게 검색되는지 판단 후 빠른쪽을 먼저 검사라게 된다.
Table2의 경우 name과 address를 같이 저장하기 때문에 검색 시에도 &#39;홍길동경기도&#39;와 같이 검색을 시도하고, 바로 원하는 값을 찾을 수 있다.</p>
<h3 id="query2">Query2</h3>
<pre><code>SELECT * FROM table2 WHERE address=&#39;경기도&#39;;</code></pre><p>반면 Query2의 경우, Table 1에서는 address 인덱스가 적용되지만, Table 2에서는 인덱싱이 적용되지 않는다. name이 사용되지 않았기 때문이다</p>
<h2 id="인덱스-사용시-유의사항">인덱스 사용시 유의사항</h2>
<ul>
<li>Cardinality가 높은 (중복도가 낮은) 열에 인덱스를 설정해야 한다.</li>
<li>WHERE 절에 자주 사용되는 열에 인덱스를 만들어야 한다.</li>
<li>잘 사용하지 않는 인덱스는 과감히 제거한다.<ul>
<li>인덱스는 테이블 데이터의 약 10% 차지</li>
<li>데이터 변경 시 페이지 분할로 인해 성능에 악영향</li>
</ul>
</li>
<li>Insert, Update, Delete등의 데이터 작업이 빈번한 테이블에서는 인덱스 사용을 고민해볼 필요가 있다.<ul>
<li>페이지 분할로 인해 조회를 제외한 다른 작업 성능이 느려지게 됨</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] Data Types]]></title>
            <link>https://velog.io/@0_zoo/Redis-Data-Types</link>
            <guid>https://velog.io/@0_zoo/Redis-Data-Types</guid>
            <pubDate>Sat, 18 Nov 2023 14:33:32 GMT</pubDate>
            <description><![CDATA[<h2 id="strings">Strings</h2>
<ul>
<li>최대 512mbyte 길이 지원</li>
<li>Text 뿐만 아니라 숫자(정수, 실수 구분이 없다)나 Binary File(JPEG같은)까지 저장할 수 있다<h3 id="command">Command</h3>
<pre><code>// Set
set key value
set key value ex 10 // 만료시간 지정
set key value px 10000 // 만료시간 밀리초 단위 지정
set key value nx // 키값이 없는 경우에 저장, 존재하면 nil
set key value xx // 키값이 있는 경우에 저장, 없으면 nil
</code></pre></li>
</ul>
<p>// Get
get key
getset key // 이전 값을 출력하고, 새 값을 설정</p>
<p>// 숫자 (&quot;&quot;로 감싸도 숫자 처리가 가능)
incr key // 정수가 아닐 경우 에러
incrby key value
incrbyfloat key value // float 형</p>
<p>// multiple set, get
mset key value key value ...
mget key key key</p>
<pre><code>### 궁금증
![](https://velog.velcdn.com/images/0_zoo/post/8cc0f1b7-d1d2-459c-a35a-1f35477459b0/image.png)
- float형 계산하면 뒤에 이상한 소수가 붙어서 나옴
## Lists
- key value value value 형태 String list
- 처음과 마지막에 데이터를 삽입 / 조회할 수 있는 Quick List 사용
    - 처음과 마지막 부분에 element 추가 / 삭제 / 조회는 O(1)
    - 특정 인덱스 값 조회는 O(N)
    - Stack이나 Dequeue으로 사용 가능
- 최대 약 40억 요소 유지 가능
### Quick List란?
![](https://velog.velcdn.com/images/0_zoo/post/61d7fea7-c615-46d4-89a5-3543d201b776/image.png)
- Linked List of ziplists
- Linked List의 각 노드가 Zip List로 이뤄져있는 구조
- 처음과 마지막 노드를 제외하고 Zip List를 압축한다.
- 오버헤드가 압축하지 않은 Quick List에 비해 1/10 ~ 1/30까지 줄어든다
- 메모리 절약이 획기적으로 되는 방법

### Zip List란?
메모리를 절약하기 위한 데이터 구조
레디스는 모든 데이터를 메모리에 저장하기 때문에, 
비싼 자원인 메모리를 절약해 사용하기 위해 메모리 절약형 자료구조를 사용한다.
데이터 구조를 간단히 보면,
&gt; #### prevlen + itself len + value
prevlen : 이전 엔트리의 길이
itself len : 엔트리 자신의 값의 길이
value : 값은 문자와 정수로 구분해서 저장한다. 실수는 문자로 구분(메모리 효율 때문), 문자는 문자 그대로저장
itself len은 값이 문자열일 때는 1,2,5 바이트로 구분해서 길이를 저장하고, 숫자(정수) 일 때는 1 바이트만 사용한다.
prevlen은 이전 엔트리의 길이를 저장하는 것이므로 문자, 숫자 구분 없이 1,5 바이트 두 종류를 사용한다.
값(value)은 문자열일 때는 그대로 저장하고, 숫자(정수) 일 때는 4,8,16,24,32,64 비트로 구분해서 저장한다.
값(value)이 실수일 때는 문자열로 취급한다.

레디스는 이런 방식으로 데이터의 형태, 길이에 맞게 최대한 메모리를 절약할 수 있는 구조로 Zip List를 설계해 사용하고 있다.

### Command</code></pre><p>lpush, rpush key element // 삽입
lpop, rpop key // pop
lrange key start end // 조회
lset
blpop, blmove // blocking commands</p>
<pre><code>### Blocking Commands
**BLPOP, BRPOP**
- 리스트 헤드 원소 하나를 제거하면서 가져온다. 리스트가 비어있을 시 리스트가 차거나 타임아웃이 날 때까지 기다린다.

**BLMOVE**
- target list로 source list의 모든 원소를 옮긴다. 비어있다면 리스트가 차거나 타임아웃이 날 때까지 기다린다.
#### 필요성
message queue나 task queue를 만드는데 유용하게 사용할 수 있다. 이 방법이 아니면 지속적으로 요청을 보내는 polling 방식으로 해야 할 것 같은데, 코스트가 크다

## Sets
- 정렬되지 않은 unique한 String의 집합
- 다음과 같은 경우에 주로 사용한다
  - unique한 데이터 관리(ex.특정 블로그에 접근하는 모든 IP 추적)
  - 관계 표시(ex. 주어진 역할을 갖는 모든 사용자)
  - set 함수 사용이 필요할 때(intersection, union, diff 등등)
- 최대 크기는 2^32 - 1
- 기본적으로 대부분의 커맨드는 O(1)이지만, SMEMBERS 커맨드는 모든 멤버를 보여주는데 O(N)이다.
- 크기가 크다면 SSCAN 사용을 고려해야 한다.(iterative하게 조회 가능)
### Command</code></pre><p>sadd key value // 추가
srem key value // 제거
sismember key key // 포함 여부. 특정 데이터가 Set에 포함되는지
sisinter key key // intersection
scard key // cardinality, 크기 반환</p>
<pre><code>## Hashes
- field-value 쌍으로 저장되는 자료구조
- object 표현할 때 좋다
- 안에 넣는 데이터 양의 실질적인 제한이 없다(메모리가 꽉 차지 않는 한)
- 대부분 command는 O(1)
- hkeys, hvals, hgetall 등 일부는 O(n)
### Command</code></pre><p>hset key field value field value... // 생성
hget key field // 단건 조회
hmget key field field field... // 다수 조회
hgetall key // 전부 조회
hincrby key field value // 필드를 value만큼 증가시킴</p>
<pre><code>## Sorted sets
- 정렬된 set
- 연관된 점수(float)로 정렬되는 방식
- 같은 점수면 사전식 정렬
- Sorted Set 구조
  - dual-ported data structure
  - skip list + hash table
  - 데이터를 저장할 때 O(log N) 연산을 통해 정렬하는 방식

#### SkipList
![](https://velog.velcdn.com/images/0_zoo/post/4daaa9b3-0f86-4d0a-ab55-9df5f64e5d54/image.png)

- Zip List가 메모리 절약에 초점을 맞춘 데이터구조라면, SkipList는 성능에 초점을 맞춘 데이터 구조
- List의 엔트리를 스킵할 수 있는 개념을 차용해서 만들어진 자료구조
- 선형적인 탐색을 한다는 개념은 같지만, 한번에 하나씩 모든 엔트리를 탐색하는 것이 아닌, Skip List의 레벨에 따라 여러개의 엔트리를 건너뛰며 순회한다.
- 데이터가 많을 수록 Zip List에 비해 탐색 시간이 훨씬 적게 걸림
- forward 포인터, span 값, backward 포인터 등 관리용 데이터로 인해 메모리 오버헤드 발생
- 데이터가 많을 수록 삭제하는 데 시간이 오래 걸림
### Command</code></pre><p>zadd key score name score name score name...
zrange key start end [withscores] // 오름차순, zrevvrange는 내림차순
zrangebyscore key start end // 해당 범위 내 score를 갖는 데이터
zrem key name // 삭제
zremrangebyscore key start end // 범위 내 score 갖는 데이터 삭제 후 삭제된 개수 출력
zrank key name // 순위 출력</p>
<pre><code>## Streams
- append-only log 처럼 동작
- 메시징 시스템인 Kafka와 비슷하게 동작, Kafka의 topic은 Redis의 stream과 비슷하게 작동, Producer와 Consumer가 있다.
- append-only log의 한계를 극복했다(ex. O(1) 시간에 random access 가능, consumer group 사용 가능)
- Stream에 entry를 추가하는 것은 O(1), Any entry에 접근하는 것은 O(n)(n은 id 의 길이).
- Stream id는 보통 짧고 고정된 길이이기 때문에, 시간적으로 굉장히 효울적이다.
- Stream이 radix tree로 이뤄졌기 때문에 가능하다.
### Commands</code></pre><p>xadd key messageId field name field name... // messageId에 * 넣으면 자동 생성, 권장
xrange key start-id end-id [count count-num] // 조회, -는 최소 id, +는 최대 id
xread <count count-num> <block block-num> 구독, streams <key> <start-id> // block으로 blocking 옵션 줄 수 있음
xdel key messageId // 삭제</p>
<p>// Consumer Group
xgroup create key group-name start-id [mkstream] // start-id가 $이면 생성 후 발생한 최신 메시지, 0이면 처음부터 전체 메시지, mkstream을 통해 스트림 없으면 생성 가능
xreadgroup group group-name consimer-name count count-num stream key message-id 
xack key group-name message-id // 메시지 처리 완료
xpending key group-name [idle <min-idle-time>] start-id end-id count [consumer-name] // group 별 pending된 메시지 조회, idle 옵션 통해 min-idle-time 지난 메시지 조회
xclaim key group-name consumer-name min-idle-time id-1, id-2, ... // consumer 변경</p>
<p>// Streams 정보 조회
xinfo stream key // stream 정보 조회
xinfo groups key // stream consumer group 정보 조회
xinfo consumers key group-name // consumer group 내 consumer 정보 조회</p>
<pre><code>## Geospatial
- coordinates를 저장할 수 있게 하고 조회할 수 있게 한다.
- 주어진 지름이나 box 내의 point를 찾는 데 유용하다.
- Sorted Set Data Structure를 사용한다.
### Commands</code></pre><p>geoadd key longitude latitude name // 추가
GEOSEARCH key &lt;FROMMEMBER member | FROMLONLAT longitude latitude&gt;
  &lt;BYRADIUS radius &lt;M | KM | FT | MI&gt; | BYBOX width height &lt;M | KM |
  FT | MI&gt;&gt; [ASC | DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST]
  [WITHHASH] // radius / box 내의 point 조회</p>
<pre><code>## Bitmaps
- data type은 아니지만, String 자료구조에서 사용할 수 있는 bit 기반 command 집합이다.
- bit, 즉 0과 1만 갖기 때문에 1000만 건을 입력해도 1.19MB 공간만 차지하는 등 공간 효율성이 좋다.
- 실시간으로 많은 단순 데이터를 쌓는 구조에서 유용하게 쓰인다.
  - 가장 큰 유저 ID가 750,000이면서, 이들 참여정보를 담는 경우엔 SET 구조에선 36,583,112 Byte가, Bitmap 구조에선 114,768 Byte가 필요함
- 하지만 모든 상황에 좋은 것은 아님
  - 최대 유저 ID가 1,000,000 이면서 대상은 유저 10명일 경우, SET이 효율적임
  - 상황에 따라 SET과의 효율성 비교 필요
- 또한 Bitmap 자료 내에서 Bit가 1인 모든 유저의 ID를 구하는 것은 지원하지 않기 때문에, Lua Scripting을 이용해 해결해야 함
### Commands</code></pre><p>setbit key offset value // 추가
getbit key offset // 해당 offset의 value get</p>
<pre><code>## Bitfields
- arbitrary length를 갖는 정수를 다룰 수 있는 자료형
- 1비트 unsigned int부터 signed 63비트 정수까지 모두 다룰 수 있다.
- binary-encoded Redis String에 저장된다.
- atomic한 read, write, increment를 지원한다.
  - counter나 비슷한 정수형 처리에 좋다
- 0-15 사이의 숫자를 많이 저장해야 한다면, 4byte integer보다는 4bit integer를 사용하면 메모리를 많이 절약할 수 있다.
### Commands</code></pre><p>BITFIELD key [GET encoding offset | [OVERFLOW &lt;WRAP | SAT | FAIL&gt;]
  &lt;SET encoding offset value | INCRBY encoding offset increment&gt;
  [GET encoding offset | [OVERFLOW &lt;WRAP | SAT | FAIL&gt;]
  &lt;SET encoding offset value | INCRBY encoding offset increment&gt;
  ...]] // get, set, incrby
BITFIELD_RO key [GET encoding offset [GET encoding offset ...]] // Read-Only</p>
<pre><code>## HyperLogLog
- Set의 cardinality를 추정하기 위한 자료구조
- 1퍼센트 미만의 오차를 갖는, 공간 효율성을 극대화한 자료구조
- Redis HLL은 최대 12KB를 차지하며 0.81%의 오차를 갖는다
- 1백만개의 데이터 저장 시 SADD는 8.64s, PFADD는 5.23s(약 1.6배 빠름)
- 조회속도는 O(1)로 SCARD와 비슷
### Commands</code></pre><p>PFADD key value value ... // unique 값 추가
PFCOUNT key // count 조회
PFMERGE merged_key key key... // 병합
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC]]></title>
            <link>https://velog.io/@0_zoo/Spring-MVC</link>
            <guid>https://velog.io/@0_zoo/Spring-MVC</guid>
            <pubDate>Tue, 14 Nov 2023 11:22:17 GMT</pubDate>
            <description><![CDATA[<h1 id="mvc-패턴이란">MVC 패턴이란?</h1>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/2bef2160-adf4-4179-968c-7f52246ecfcf/image.png" alt=""></p>
<p>소프트웨어 디자인 패턴 중 하나로, 다음 세가지로 구성된다</p>
<ul>
<li><p><strong>Model</strong></p>
</li>
<li><p><strong>View</strong></p>
</li>
<li><p><strong>Controller</strong></p>
<h3 id="model">Model</h3>
</li>
<li><p>모델은 뷰에 출력할 <strong>데이터</strong>를 담아둔다.</p>
</li>
<li><p>모델은 <strong>뷰</strong>나 <strong>컨트롤러</strong>에 대해 <strong>알지 못한다.</strong></p>
<h3 id="view">View</h3>
</li>
<li><p>뷰는 모델에 담겨있는 데이터를 활용해 <strong>화면을 그린다.</strong></p>
</li>
<li><p>뷰는 <strong>모델</strong>이나 <strong>컨트롤러</strong>에 대해 <strong>알지 못한다.</strong></p>
<h3 id="controller">Controller</h3>
</li>
<li><p>컨트롤러는 요청을 받아 <strong>비즈니스 로직</strong>을 실행하고, <strong>뷰</strong>에 <strong>결과 데이터를 모델에 담아 전달</strong>한다.</p>
</li>
<li><p>때문에 컨트롤러는 <strong>뷰와 모델을 모두 알고 있어야</strong> 한다.</p>
</li>
<li><p>모델과 뷰가 서로의 존재를 모르기 때문에, 컨트롤러는 둘 사이에서 <strong>사용자 입력</strong>과 <strong>데이터 전달</strong>을 담당하며 <strong>중재하는 역할</strong>을 한다.</p>
<h1 id="spring-mvc-구조">Spring MVC 구조</h1>
<h3 id="dispatcherservlet">DispatcherServlet</h3>
</li>
<li><p><strong>Front Controller</strong>의 역할을 한다. </p>
</li>
<li><p>가장 앞단에서 클라이언트의 요청을 처리하는 Controller로, <strong>전반적인 처리 과정</strong>을 통제한다.</p>
</li>
<li><p>DispatcherServlet은 <strong>HttpServlet</strong>을 상속받아 <strong>서블릿</strong>으로 동작한다.</p>
<blockquote>
<p><strong>Servlet이란?</strong>
서블릿(Servlet)이란 동적 웹 페이지를 만들 때 사용되는 <strong>자바</strong> 기반의 <strong>웹 애플리케이션 프로그래밍 기술</strong>이다. 서블릿은 웹 요청과 응답의 흐름을 간단한 <strong>메서드 호출</strong>만으로 체계적으로 다룰 수 있게 해준다.</p>
</blockquote>
</li>
<li><p><em>주요 특징*</em>은 다음과 같다.</p>
<blockquote>
<ul>
<li>클라이언트의 <strong>Request</strong>에 대해 <strong>동적으로 작동</strong>하는 웹 어플리케이션 컴포넌트</li>
<li>기존의 정적 웹 프로그램의 문제점을 보완하여 <strong>동적인 여러 가지 기능</strong>을 제공</li>
<li>JAVA의 <strong>스레드</strong>를 이용하여 동작</li>
<li>MVC패턴에서 <strong>컨트롤러</strong>로 이용됨</li>
<li><strong>컨테이너</strong>에서 실행된다</li>
<li><strong>보안 기능</strong>을 적용하기 쉬움</li>
</ul>
</blockquote>
</li>
<li><p>서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.</p>
</li>
<li><p>스프링 MVC는 FrameworkServlet.service()를 시작으로 여러 메서드가 호출되며 최종적으로 DispatcherServlet.doDispatch()가 호출된다.</p>
</li>
</ul>
<h3 id="handlermapping">HandlerMapping</h3>
<ul>
<li>요청을 직접 처리할 <strong>컨트롤러를 탐색</strong>한다. </li>
<li>구체적인 매핑은 xml 파일이나 java config 관련 어노테이션 등을 통해 처리할 수 있다</li>
</ul>
<h3 id="handleradapter">HandlerAdapter</h3>
<ul>
<li>매핑된 컨트롤러 <strong>Handler 메소드</strong>의 실행을 요청한다.</li>
</ul>
<h3 id="handlercontroller">Handler(Controller)</h3>
<ul>
<li>Spring MVC에서 <strong>Controller = Handler</strong>라고 생각하면 편하다. </li>
<li>핸들러는 DispatcherServlet가 전달해준 <strong>HTTP 요청</strong>을 처리하고 결과를 <strong>Model</strong>에 저장한다.</li>
</ul>
<h3 id="modelandview">ModelAndView</h3>
<ul>
<li>ModelAndView는 controller에 의해 반환된 <strong>Model</strong>과 <strong>View</strong>가 Wrapping된 객체이다.</li>
</ul>
<h3 id="viewresolver">ViewResolver</h3>
<ul>
<li>ViewResolver는 <strong>View name</strong>에 맞는 View를 찾아 <strong>View 객체</strong>를 생성한다. 이 객체를 이용해 View 내용을 생성하게 된다.</li>
</ul>
<h3 id="httpmessageconverter">HttpMessageConverter</h3>
<ul>
<li>HttpMessageConverter은 @RequestBody, @ResponseBody등을 사용하게 되면 동작하는데, <strong>JSON 데이터를 HTTP 메시지 바디 내 직접 읽거나 쓰는 경우</strong> 사용한다.</li>
<li>String <strong>문자 처리</strong>에는 <strong>StringMessageConverter</strong>, <strong>객체 처리</strong>에는 <strong>MappingJackson2HttpMessageConverter</strong>를 사용한다. </li>
<li>응답의 경우 클라이언트의 <strong>Accept 헤더</strong>와 <strong>컨트롤러의 반환 타입</strong>을 조합해<strong>MessageConverter</strong>가 선택된다.</li>
<li>HttpMessageConverter는 <strong>ArgumentResolver</strong>와 <strong>ReturnValueHandler</strong>에서 작동한다.</li>
</ul>
<h3 id="argumentresolver">ArgumentResolver</h3>
<ul>
<li>컨트롤러 메소드의 <strong>파라미터</strong>를 전달할 때 작동한다. </li>
<li><strong>@RequestParam, @RequestBody, @Model</strong> 등을 이용해 파라미터를 전탈할 때 사용된다. </li>
<li>이때 HttpMessageConverter를 사용해 값들의 <strong>타입을 확인</strong>하고, 알맞은 <strong>객체로 변환</strong>한다.</li>
</ul>
<h3 id="returnvaluehandler-handlermethodreturnvaluehandler">ReturnValueHandler (HandlerMethodReturnValueHandler)</h3>
<ul>
<li><strong>@ResponseBody, HttpEntity</strong>를 처리한다. </li>
<li>반환값을 HttpMessageConverter를 이용해 처리하고, <strong>Http 메시지에 입력</strong>한다.</li>
</ul>
<h1 id="spring-mvc-동작-원리-view-반환-시">Spring MVC 동작 원리 (View 반환 시)</h1>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/b166ac24-92c8-4245-85e5-669257a072ba/image.png" alt=""></p>
<ol>
<li>클라이언트가 <strong>서버에 요청</strong>을 보내면, <strong>DispatcherServlet</strong> 클래스가 요청을 받는다.</li>
<li>DispatcherServlet은 요청을 처리할 <strong>컨트롤러에 대한 검색</strong>을 <strong>HandlerMapping 인터페이스</strong>에게 요청한다.</li>
<li>HandlerMapping은 클라이언트 요청에 해당하는 <strong>컨트롤러 정보</strong>를 찾아 DispatcherServlet에게 응답한다<ul>
<li>이때 컨트롤러 정보에는 해당 컨트롤러 안의 <strong>Handler 메소드 정보</strong>가 포함된다.</li>
<li>Handler 메소드란 컨트롤러 클래스 내에서 구현된, <strong>요청을 처리해주는 메소드</strong>이다.</li>
</ul>
</li>
<li>컨트롤러를 찾은 후, 해당 요청을 처리할 Handler 메소드를 찾아 호출해야 하는데, 이를 직접 호출하는 방식이 아닌 <strong>HandlerAdapter에게 책임을 위임</strong>한다.</li>
<li><strong>HandlerAdapter</strong>은 <strong>DispatcherServlet 요청, 컨트롤러 정보</strong>를 가지고 해당 컨트롤러의 <strong>Handler 메소드를 호출</strong>한다.</li>
<li>컨트롤러의 Handler 메소드는 <strong>비즈니스 로직</strong>을 처리한 후 <strong>결과</strong>를 HandlerAdapter에게 <strong>반환</strong>한다.</li>
<li><strong>HandlerAdapter</strong>는 반환받은 결과를 <strong>ModelAndView</strong> 객체로 변환해 <strong>DispatcherServlet</strong>에 전달한다. </li>
<li><strong>DispatcherServlet</strong>은 반환받은 <strong>ModelAndView</strong> 중 View name을 가지고 <strong>ViewResolver</strong>를 호출한다.</li>
<li><strong>ViewResolver</strong>에서는 View name을 통해 해당하는 <strong>View</strong>를 찾아 <strong>View 객체</strong>를 리턴한다</li>
<li><strong>DispatcherServlet</strong>은 받은 View 객체에 Model 데이터를 넘겨주면서 최종적으로 클라이언트에게 전달할 <strong>응답 데이터</strong>를 생성한다.</li>
<li><strong>DispatcherServlet</strong>은 받은 응답데이터를 마지막으로 <strong>클라이언트에 응답</strong>한다.</li>
</ol>
<h1 id="spring-mvc-동작-원리-data-반환-시">Spring MVC 동작 원리 (Data 반환 시)</h1>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/f9c5b06a-6513-45de-817e-5f4ee07e62c0/image.png" alt=""></p>
<ol>
<li><strong>HandlerAdapter</strong>에서 Handler 함수를 호출할 때, <strong>Parameter(@RequestBody, @Model, @RequestParam)</strong>가 있다면 <strong>ArgumentResolver</strong>를 호출한다.</li>
<li><strong>Argument Handler</strong>에서는 <strong>HttpMessageConverter</strong>를 호출해 <strong>parameter 값을 확인</strong>하고 <strong>객체를 생성</strong>한다.</li>
<li>Controller에서 요청을 처리하고 반환할 시, <strong>@ResponseBody</strong>가 달려있다면 <strong>ReturnValueHandler</strong>를 호출한다.</li>
<li><strong>ReturnValueHandler</strong>에서는 H<strong>ttpMessageConverter</strong>를 호출해 <strong>반환값을 확인</strong>하고 <strong>Http 메시지</strong>에 입력한다.</li>
<li><strong>HandlerAdapter</strong>에서 값을 반환한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring + MySQL] batch size 조정을 통한 대용량 데이터 삽입 성능 향상]]></title>
            <link>https://velog.io/@0_zoo/Spring-MySQL-batch-size-%EC%A1%B0%EC%A0%95%EC%9D%84-%ED%86%B5%ED%95%9C-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%BD%EC%9E%85-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81</link>
            <guid>https://velog.io/@0_zoo/Spring-MySQL-batch-size-%EC%A1%B0%EC%A0%95%EC%9D%84-%ED%86%B5%ED%95%9C-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%BD%EC%9E%85-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81</guid>
            <pubDate>Wed, 02 Nov 2022 05:34:31 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>프로젝트를 진행하며, 수십만건에 이르는 데이터를 수집해야 할 일이 생겼다. 처음에는 수십만건, 후에는 지속적으로 업데이트해야 하는 상황이었다. 처음에는 그냥 db 모델링을 하고, api를 작성해 진행했지만 약 11000건을 수집하는 데 16분이 걸려, 너무 느리다고 생각해서 구글링을 통해 문제를 해결했다. 마침 우아한 형제들 블로그에 <a href="https://techblog.woowahan.com/2695/">관련 포스트</a>가 있어 참고했다.</p>
<h2 id="하이버네이트-배치">하이버네이트 배치</h2>
<p> 배치 작업은 대량의 작업을 한번에 처리하는 경우를 말한다. 하이버네이트 배치는 jdbc에서 제공하는 배치기능을 활용하는데, 설정한 배치 갯수에 도달할때까지 addBatch를 호출하고, 설정한 배치 갯수에 도달하면 executeBatch를 호출해 addBatch를 통해 추가된 쿼리를 재조합해 DB로 한번에 전송하게 된다. 여러개의 쿼리를 한번에 모아서 처리하기 때문에 쿼리를 하나씩 수행할 때에 비해 실행 속도가 향상된다.
 프로젝트에 직접 적용을 해본 결과, 원래는 1분당 평균 약 790개의 물건을 저장할 수 있었는데, 적용 후에는 1분당 평균 약 3000개의 물건을 저장할 수 있었다.</p>
<h2 id="적용하기">적용하기</h2>
<p>application.yml에 아래 코드만 추가해주면 배치 설정을 할 수 있다.
batch_size는 한번에 insert/update할 크기이다.</p>
<pre><code>spring:
  jpa:
    properties:
      hibernate:
        jdbc:
            batch_size: 1000</code></pre><h3 id="mysql">MySQL</h3>
<p>MySQL JDBC의 경우, rewriteBatchedStatements=true 옵션을 추가해야 적용이 된다고 한다. 따라서 application.yml에 url을 수정해줘야 한다.</p>
<pre><code>spring:
  datasource:
    url: jdbc:mysql://localhost:3306/hibernate_batch?rewriteBatchedStatements=true</code></pre><h2 id="id가-자동증가-값일-때">ID가 자동증가 값일 때</h2>
<p> 프로젝트에 위 내용을 적용시켰지만, 처음에는 실행 시간이 거의 줄어들지 않았다. 이유를 찾아보니, ID가 자동증가 값일 때(ex. GenerationType.IDENTITY일 때)는 배치 적용이 안된다고 한다. <a href="https://vladmihalcea.com/jpa-persist-and-merge/">Vlad Mihalcea의 포스트</a>를 통해 그 이유를 알 수 있었다.</p>
<blockquote>
<p>Whenever an entity is persisted, Hibernate must attach it to the currently running Persistence Context which acts as a Map of entities. The Map key is formed of the entity type (its Java Class) and the entity identifier.
-&gt; Hibernate는 entity를 그 클래스와 id를 이용해 Persistence Context에 Attatch한다.</p>
</blockquote>
<p>For IDENTITY columns, the only way to know the identifier value is to execute the SQL INSERT. Hence, the INSERT is executed when the persist method is called and cannot be disabled until flush time.
-&gt; IDENTITY의 경우, id를 알려면 insert 쿼리를 먼저 수행해야 한다.</p>
<blockquote>
</blockquote>
<p>For this reason, Hibernate disables JDBC batch inserts for entities using the IDENTITY generator strategy. 
-&gt; 이런 이유로, IDENTITY 생성전략에서는 batch insert가 적용되지 않는다.</p>
<h2 id="id-변경">id 변경</h2>
<p>현재 삽입하고자 하는 entity의 id는 IDENTITY 전략 생성이었고, 때문에 batch insert가 동작하지 않았다. 때문에 id를 생성자에서 uuid 랜덤값으로 생성하는 방식으로 변경했다. 그 결과, batch insert가 제대로 작동하는 것을 확인할 수 있었고, 위에서 말했듯 1분당 평균 약 790개 -&gt; 1분당 평균 약 3000개로 비약적 향상을 볼 수 있었다.</p>
<h2 id="결론">결론</h2>
<p>대용량 데이터 삽입/수정이 필요한 경우, batch size 설정을 통해 훨씬 효율적으로 수행 가능하다. 이 프로젝트의 경우, 3배 이상 시간이 감소됐다. MySQL의 경우에는 rewriteBatchedStatements=true 옵션이 추가적으로 필요하고, IDENTITY 생성전략의 경우에는 batch insert가 불가능하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot + MySQL] 대용량 데이터 조회 최적화]]></title>
            <link>https://velog.io/@0_zoo/Spring-Boot-MySQL-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@0_zoo/Spring-Boot-MySQL-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Mon, 10 Oct 2022 15:55:27 GMT</pubDate>
            <description><![CDATA[<h1 id="1-최적화-계기">1. 최적화 계기</h1>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/a34c58ab-6e3e-45a6-94c7-341151fb7053/image.png" alt="사건의 발단">
진행중인 프로젝트에서 많은 데이터를 필요로 했고, 약 40만개의 데이터를 수집했다. 별 문제 없을 거라 생각했지만, 큰 오산이었다. 기존 무한스크롤로 끊김없이 보이던 리스트는 버벅이기 시작했고, 40만개에 이르자 api 요청 한번에 길게는 6~7초가 걸렸다.</p>
<blockquote>
<p>현재 프로젝트에서 사용중인 조회 api는 jpa specification을 이용한 다중 필터, page리턴, 여러 조건에 의한 정렬을 사용한다.</p>
</blockquote>
<h1 id="2-가설-생성">2. 가설 생성</h1>
<p>이정도 규모의 데이터를 다뤄본 적이 없기에, 어디에서 시간이 오래 걸리는 지 또한 알 수 없었다. 때문에, 몇가지 가설을 세우고 차례대로 검증해보기로 했다.</p>
<blockquote>
<ol>
<li>다수의 onetomany 필드로 인한 많은 수의 쿼리 실행</li>
<li>복잡한 필터 구조 + 대용량 데이터로 인한 쿼리 실행 시간 증가</li>
<li>대용량 데이터로 인한 정렬 시간 증가</li>
</ol>
</blockquote>
<p>결과부터 말하자면 3번이 정답이었다. 앞으로는 가설을 검증한 과정과, 문제를 어떻게 해결했는지, 그 과정에서 무엇을 얻었는지 적어보려 한다.</p>
<h1 id="3-가설-검증">3. 가설 검증</h1>
<h2 id="1-다수의-onetomany-필드로-인한-많은-수의-쿼리-실행">1. 다수의 onetomany 필드로 인한 많은 수의 쿼리 실행</h2>
<p>조회할 Entity에는 4개의 onetomany field와 하나의 onetoone field가 있었고, 이 부분에서 쿼리가 많이 나갈 수 있다고 생각했다.
하지만 loading 방식을 몰랐기 때문에 한 오해였다.
onetomany 방식은 기본적으로 lazy loading 방식으로 작동하기 때문에, 해당 객체를 조회하지 않는다면 쿼리가 나가지 않았다.
또한 onetoone field는 기본적으로 eager loading 방식이기 때문에 쿼리가 한번만 실행되고, 큰 부하가 걸리지 않았다.
 이 부분을 구글링하며 onetoone field를 lazy field 로 해두고 필요한 메소드에서 Entity Graph를 붙여 사용하려 했으나, 왠지 모르게 어떻게 해도 계속해서 eager 방식으로 작동했다.
 고민하다 찾아보니, onetoone 매핑에서는 매핑의 주인(mapped by)만 lazy loading을 사용할 수 있었다. 자세한 이유는 조금 더 공부해봐야겠다.</p>
<h2 id="2-복잡한-필터-구조--대용량-데이터로-인한-쿼리-실행-시간-증가">2. 복잡한 필터 구조 + 대용량 데이터로 인한 쿼리 실행 시간 증가</h2>
<p>현재 조회 쿼리에서는 10개 이상의 파라미터를 받아 복잡한 필터를 거쳐 페이지 형식으로 리턴하는데, 데이터량이 많아지다 보니 필터 혹은 페이지를 만드는 시간이 증가하는 것이 아닐까 생각했다.
결국 이 부분을 테스트해보다 문제점을 발견했는데, 페이지만 리턴해보고, 필터만 해보고, 필터 후 페이지까지 해봤는데 큰 문제가 없었다. 문제가 되는 부분은 정렬 부분이었다..(QueryDSL로 다 바꿔야 하나 심각하게 고민하고 있었다..)</p>
<h2 id="3-대용량-데이터로-인한-정렬-시간-증가">3. 대용량 데이터로 인한 정렬 시간 증가</h2>
<p>결론적으로 데이터가 많아지며 정렬에서 시간이 오래 걸린다는 것을 알았다. 실제로 sql query를 직접 날려보며 테스트해보니 정렬만 해도 시간이 4초 이상 걸렸다.</p>
<h1 id="문제-해결">문제 해결</h1>
<p>대부분의 개발자가 그렇듯, 오랜 시간 구글링을 통해 해답을 얻을 수 있었는데, 키워드는 보조 인덱스였다.
db에서 인덱스를 사용하면 삽입, 수정, 삭제에는 더 많은 시간이 소요되지만, 조회 시간은 비약적으로 감소시켜줄 수 있었다. 때문에 수정이 잘 되지 않는 칼럼을 인덱스로 선정하면 조회 및 정렬 단계에서 많은 시간적 이득을 볼 수 있었다.
많은 물건 중 필요한 물건은 진행중인 물건이었기에, 진행 상태를 보조 인덱스로 선정하고, 조회를 할 때 where progress = &#39;진행중&#39;과 같은 식으로 범위를 좁힌 후 정렬을 진행하면, 5~6초가 걸리던 쿼리가 0.2초만에 완료되는 것을 볼 수 있었다.
<img src="https://velog.velcdn.com/images/0_zoo/post/905e7a98-dabd-4855-86b9-9efafed1ae91/image.png" alt="api 속도 향상 결과"></p>
<h1 id="깨달은-점">깨달은 점</h1>
<p>지금껏 겪어보지 못했던 문제를 겪고, 많은 생각을 해보며, 더 잘하는 개발자였다면 어땠을까 하는 생각을 했다. 아마도 훨씬 일찍 문제를 파악하고, 해결했을텐데 더 열심히 성장해야겠다는 동기를 얻었다. 그래도 이렇게 하나씩 깨우치다보면 문제를 마추져도 저번에 그 문제네? 하고 쉽게 해결하는 날이 오지 않을까 하는 생각을 해본다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[애자일과 스크럼, 그리고 칸반]]></title>
            <link>https://velog.io/@0_zoo/%EC%95%A0%EC%9E%90%EC%9D%BC%EA%B3%BC-%EC%8A%A4%ED%81%AC%EB%9F%BC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%B9%B8%EB%B0%98</link>
            <guid>https://velog.io/@0_zoo/%EC%95%A0%EC%9E%90%EC%9D%BC%EA%B3%BC-%EC%8A%A4%ED%81%AC%EB%9F%BC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%B9%B8%EB%B0%98</guid>
            <pubDate>Mon, 02 May 2022 09:50:27 GMT</pubDate>
            <description><![CDATA[<h2 id="멘토링">멘토링</h2>
<p>SW마에스트로에서 애자일과 스크럼, 그리고 칸반이라는 주제에 대해 남상수멘토님의 멘토링을 들었고, 너무 유익한 시간이 됐다고 생각해 정리해두려 한다.</p>
<h2 id="폭포수waterfall-모델">폭포수(Waterfall) 모델</h2>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/05328f64-3625-4218-844a-c60ab68b45b1/image.png" alt="">
먼저, 애자일과 자주 비교되는 
폭포수 모델은 요구사항 분석, 설계, 구현, 검증 단계를 순차적으로 진행하는 개발방법이다. 폭포수라는 이름과 그림에서 알 수 있듯이, 하나의 단계가 끝나면 다음 단계로 넘어가며 전 단계로 돌아가는 일은 거의 없다.</p>
<h3 id="문제점">문제점</h3>
<p>문제점에 주목해보면, 첫번째로는 시간이 오래 걸린다. 하나의 단계가 끝나야 다음 단계로 넘어갈 수 있기 때문에 시간이 오래 걸릴 수 밖에 없다.
또 다른 문제점으로는 책임이 분리되고, 의사소통에 한계가 있을 수 밖에 없다. 한 단계를 한 팀에서 마무리하면 다른 팀으로 일이 넘어가고 또 완료하면 다른 팀으로 넘어가는 구조이기 때문에, 한 과정에서 오류가 일어나면 돌아가지 못해 전체적인 흐름이 꼬이는 일이 일어나게 된다.
마지막으로는 빠른 소프트웨어 시장에 대응할 수 없다. 소프트웨어 시장은 엄청나게 빠르게 변하는데, 워터폴 모델에서 개발 시간은 느리기 때문에 정확한 타겟을 두고 화살을 발사하더라도 이동하는 중에 타겟이 이동해버리면 엉뚱한 곳으로 날아가게 되는 것이다.</p>
<h2 id="애자일agile">애자일(Agile)</h2>
<p>Agile = 민첩한, 날렵한
애자일은 소프트웨어 개발이</p>
<ol>
<li>예측하기 어렵고</li>
<li>복잡하며</li>
<li>변경될 수 있다
는 관찰을 기반으로 한 일련의 원칙 및 관행이다. </li>
</ol>
<h3 id="애자일-소프트웨어-개발-선언">애자일 소프트웨어 개발 선언</h3>
<ul>
<li>공정의 도구보다 <strong>개인과 상호작용</strong>을</li>
<li>포괄적인 문서보다 <strong>작동하는 소프트웨어</strong>를</li>
<li>계약 협상보다 <strong>고객과의 협력</strong>을</li>
<li>계획을 따르기보다 <strong>변화에 대응하기</strong>를
가치있게 여긴다.  </li>
</ul>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/bdc88164-b016-400d-9c48-c7cd7e7b8143/image.png" alt=""></p>
<p>애자일은 위 사진과 같은 방식으로 이루어진다. 요구사항 분석, 설계, 구현, 검증을 한 팀에서 진행하며 이를 여러 차례 진행한다. 이 과정을 통해 새로운 가치가 빠르게 탄생하게 된다.</p>
<h2 id="스크럼scrum">스크럼(Scrum)</h2>
<p>스크럼은 애자일적인 개발방법 중 하나로, 스크럼 마스터가 존재하는 팀에서 이루어진다.
<img src="https://velog.velcdn.com/images/0_zoo/post/ffaf7d0c-e284-4ed4-9952-cabe827392e8/image.png" alt=""></p>
<p>일을 출시 가능한 작은 단위 목록으로 작성해 프로덕트 백로그에 두고, 1-4주 주기의 스프린트에서 진행할 일을 정해 스프린트 백로그에 넣은 후 스프린트 기간동안 계획, 설계, 구현, 테스트까지 진행한다. 매일 스크럼 마스터가 주도하는 회의가 열려 어느 로그를 담당하고 있으며 어디까지 진행했는지 공유하는 시간을 갖는다. 스프린트가 끝난 후에는 스프린트 회고를 통해 어느 부분이 해결이 되었고, 어느 부분이 부족했는지 리뷰하는 시간을 갖는다. 이 또한 스크럼 마스터에 의해 진행이 된다.</p>
<h3 id="역할">역할</h3>
<h4 id="프로젝트-오너po">프로젝트 오너(PO)</h4>
<ul>
<li>제품 전략 및 로드맵 수립</li>
<li>제품 백로그 설명 및 우선순위 관리</li>
<li>제품 결정 검증</li>
</ul>
<h4 id="스크럼-마스터">스크럼 마스터</h4>
<ul>
<li>일일 스크럼 회의 진행</li>
<li>팀의 작업 환경 확인 및 장애 요소를 해결</li>
<li>이해관계자와 개발 팀 구성원의 의사결정 도움</li>
</ul>
<h2 id="칸반">칸반</h2>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/fcb21df0-a671-4a0f-8789-0a72a586b97d/image.png" alt="">
칸반은 스크럼처럼 주기적으로 스프린트를 하며 진행하는 것은 아니지만, 작업 흐름을 시각화하여 관리하는 것을 의미한다. 지라(Jira)와 같은 툴을 이용해 온라인으로 관리할 수도 있고, 화이트보드같은 곳에 포스트잇으로 붙여 관리할 수도 있다.
이때 한 과정에 많은 작업이 몰리는 것을 막기 위해 과정 당 작업의 개수를 제한하기도 한다.
<img src="https://velog.velcdn.com/images/0_zoo/post/e1ba8885-3d56-42a8-9017-4ce01b7a31ec/image.png" alt=""></p>
<h2 id="정리">정리</h2>
<p><img src="https://velog.velcdn.com/images/0_zoo/post/59bc1b57-05a6-4b5e-812c-f6e6336492c7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내가 보려고 쓰는 마크다운 정리]]></title>
            <link>https://velog.io/@0_zoo/%EB%82%B4%EA%B0%80-%EB%B3%B4%EB%A0%A4%EA%B3%A0-%EC%93%B0%EB%8A%94-%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@0_zoo/%EB%82%B4%EA%B0%80-%EB%B3%B4%EB%A0%A4%EA%B3%A0-%EC%93%B0%EB%8A%94-%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 18 Apr 2022 07:51:31 GMT</pubDate>
            <description><![CDATA[<h3 id="마크다운">마크다운</h3>
<p>블로그나 깃허브 프로필, 혹은 노션을 쓸 때 필요한 언어가 바로 마크다운이다.
이제 블로그도 시작했고, 깃 리드미도 조금 더 이쁘게 써보고 싶은 마음에 마크다운 문법을 정리해두려 한다.</p>
<hr>
<h2 id="문법정리">문법정리</h2>
<hr>
<h3 id="제목">제목</h3>
<p>#을 문장 앞에 붙여 제목 1~제목 6까지 표현할 수 있다.</p>
<pre><code># 제목1
## 제목2
### 제목3
#### 제목4
##### 제목5
###### 제목6</code></pre><h1 id="제목1">제목1</h1>
<h2 id="제목2">제목2</h2>
<h3 id="제목3">제목3</h3>
<h4 id="제목4">제목4</h4>
<h5 id="제목5">제목5</h5>
<h6 id="제목6">제목6</h6>
<hr>
<p>추가적으로, 제목 1과 제목 2는 다음과 같이 표현할 수도 있다.</p>
<pre><code>제목1
========
제목2
--------</code></pre><h1 id="제목1-1">제목1</h1>
<h2 id="제목2-1">제목2</h2>
<hr>
<h3 id="강조">강조</h3>
<pre><code>이텔릭체는 *별표* 혹은 _언더바_
두껍게는 **별표 2개** 혹은 __언더바 2개__
취소선은 ~~물결표시~~
&lt;u&gt;밑줄&lt;/u&gt;은 &lt;u&gt;&lt;/u&gt;</code></pre><p>이텔릭체는 <em>별표</em> 혹은 <em>언더바</em>
두껍게는 <strong>별표 2개</strong> 혹은 <strong>언더바 2개</strong>
취소선은 <del>물결표시</del>
<u>밑줄</u>은 &quot;<u></u>&quot;</p>
<hr>
<h3 id="목록">목록</h3>
<pre><code>1. 순서가 필요한 목록
1. 순서가 필요한 목록
  - 순서가 필요하지 않은 목록(서브) 
  - 순서가 필요하지 않은 목록(서브) 
1. 순서가 필요한 목록
  1. 순서가 필요한 목록(서브)
  1. 순서가 필요한 목록(서브)
1. 순서가 필요한 목록

- 순서가 필요하지 않은 목록에 사용 가능한 기호
  - 대쉬(hyphen)
  * 별표(asterisks)
  + 더하기(plus sign)</code></pre><ol>
<li>순서가 필요한 목록</li>
<li>순서가 필요한 목록<ul>
<li>순서가 필요하지 않은 목록(서브) </li>
<li>순서가 필요하지 않은 목록(서브) </li>
</ul>
</li>
<li>순서가 필요한 목록<ol>
<li>순서가 필요한 목록(서브)</li>
<li>순서가 필요한 목록(서브)</li>
</ol>
</li>
<li>순서가 필요한 목록</li>
</ol>
<ul>
<li>순서가 필요하지 않은 목록에 사용 가능한 기호<ul>
<li>대쉬(hyphen)</li>
<li>별표(asterisks)</li>
<li>더하기(plus sign)</li>
</ul>
</li>
</ul>
<hr>
<h3 id="링크">링크</h3>
<pre><code>[링크 제목](링크 &quot;링크 설명&quot;)로 표현 가능
ex)
[Naver](www.naver.com&quot;네이버 링크입니다.&quot;)

다음과 같이 문서 내 일반 URL이나 꺾쇠 괄호(`&lt; &gt;`, Angle Brackets)안의 URL은 자동으로 링크를 사용합니다.
구글 홈페이지: https://google.com
네이버 홈페이지: &lt;https://naver.com&gt;</code></pre><p><a href="%EB%A7%81%ED%81%AC" title="링크 설명">링크 제목</a>로 표현 가능
ex)
<a href="www.naver.com" title="네이버 링크입니다.">Naver</a></p>
<p>다음과 같이 문서 내 일반 URL이나 꺾쇠 괄호(<code>&lt; &gt;</code>)안의 URL은 자동으로 링크를 사용합니다.
구글 홈페이지: <a href="https://google.com">https://google.com</a>
네이버 홈페이지: <a href="https://naver.com">https://naver.com</a></p>
<hr>
<h3 id="이미지">이미지</h3>
<p>링크와 비슷하지만 앞에 !가 붙습니다.</p>
<pre><code>![Cat][image]

[image]: https://image.shutterstock.com/image-illustration/cute-cartoon-kitten-taking-power-600w-1790302490.jpg &quot;Cute cat&quot;</code></pre><p><img src="https://image.shutterstock.com/image-illustration/cute-cartoon-kitten-taking-power-600w-1790302490.jpg" alt="Cat" title="Cute cat"></p>
<hr>
<h3 id="코드블럭">코드블럭</h3>
<pre><code>    ```
    사이에 코드를 넣으면 됩니다.
    ```
    언어를 나타내고 싶다면
    ```html
    &lt;a&gt;Hello World&lt;/a&gt;
    ```
    이런식으로 하면 됩니다</code></pre><hr>
<h3 id="테이블">테이블</h3>
<pre><code>- 3개 이상을 사용하여 헤더셀 구분
:을 사용하여 좌측, 우측, 중앙 정렬 가능
가장 좌측과 우측의 | 생략 가능
| 값 | 의미 | 기본값 |
|---|:---:|---:|
| `static` | 유형(기준) 없음 / 배치 불가능 | `static` |
| `relative` | 요소 자신을 기준으로 배치 |  |
| `absolute` | 위치 상 부모(조상)요소를 기준으로 배치 |  |
| `fixed` | 브라우저 창을 기준으로 배치 |  |</code></pre><table>
<thead>
<tr>
<th>값</th>
<th align="center">의미</th>
<th align="right">기본값</th>
</tr>
</thead>
<tbody><tr>
<td><code>static</code></td>
<td align="center">유형(기준) 없음 / 배치 불가능</td>
<td align="right"><code>static</code></td>
</tr>
<tr>
<td><code>relative</code></td>
<td align="center">요소 자신을 기준으로 배치</td>
<td align="right"></td>
</tr>
<tr>
<td><code>absolute</code></td>
<td align="center">위치 상 부모(조상)요소를 기준으로 배치</td>
<td align="right"></td>
</tr>
<tr>
<td><code>fixed</code></td>
<td align="center">브라우저 창을 기준으로 배치</td>
<td align="right"></td>
</tr>
</tbody></table>
<hr>
<h3 id="인용문">인용문</h3>
<pre><code>&gt;기호를 활용하여 표현 가능 
&gt;&gt;중첩도 가능
&gt;&gt;&gt;3중 중첩도 가능(몇중까지 되는지는 안해봄)</code></pre><blockquote>
<p>기호를 활용하여 표현 가능</p>
<blockquote>
<p>중첩도 가능</p>
<blockquote>
<p>3중 중첩도 가능(몇중까지 되는지는 안해봄)</p>
</blockquote>
</blockquote>
</blockquote>
<hr>
<h3 id="수평선">수평선</h3>
<pre><code>-, *, _ 3개 이상 사용

-----
***
___</code></pre><p>-, *, _ 3개 이상 사용</p>
<hr>
<hr>
<hr>
<h3 id="줄바꿈">줄바꿈</h3>
<pre><code>줄바꿈: &lt;br&gt; 사용</code></pre><p>줄바꿈: <br> 사용</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SW마에스트로 13기 면접 후기]]></title>
            <link>https://velog.io/@0_zoo/SW%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C-13%EA%B8%B0-%EB%A9%B4%EC%A0%91-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@0_zoo/SW%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C-13%EA%B8%B0-%EB%A9%B4%EC%A0%91-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Fri, 15 Apr 2022 08:38:54 GMT</pubDate>
            <description><![CDATA[<h2 id="코딩테스트-후기"><a href="https://velog.io/@0_zoo/SW%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C-13%EA%B8%B0-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%9B%84%EA%B8%B0">코딩테스트 후기</a></h2>
<p>코딩 테스트 후기는 위 링크를 클릭하면 볼 수 있다.
지난번에 말했듯 3월 22일 코딩테스트 발표가 났고, 면접 날짜는 29일이었다.
28~31일 월요일부터 목요일까지 다양하게 면접 순서가 배정이 됐고, 나는 화요일이었다.
일찍 봐서 다행이라고 생각했다.</p>
<h2 id="포트폴리오-제출-및-발표">포트폴리오 제출 및 발표</h2>
<p>이번 기수에 새롭게 추가된 시스템이고, 면접에 있어 가장 중요한 부분이었다고 생각한다.
발표는 22일(화요일)에 났고, 25일(금요일)까지 포트폴리오 제출을 해야 했다.
자기소개와 원하는 프로젝트 최대 2개 발표를 3분안에 해야했기에, 디테일하게 작성할 수는 없었다.
나는 간단한 자기소개와 42서울 활동, 그리고 진행했던 프로젝트 한개와 진행하고 있는 프로젝트를 적었다.</p>
<h2 id="면접-당일">면접 당일</h2>
<h3 id="면접보기-30분-전">면접보기 30분 전</h3>
<p>면접은 코엑스 회의실에서 이루어졌다. 생각보다 엄청 컸다.
40분 정도 일찍 도착해서 20분정도 기다리다가 들어갔다.
밖에 나와 같은 지원자분들이 많았다. (한 타임에 25명이었다)
들어가자 소마 스티커, 교통비, 신원 확인 목걸이를 줬다.
5분반으로 나누어 진행했는데, 나는 3분반이었다.
세 분반정도 모여있던 대기실에서 기다렸다.</p>
<h3 id="면접보기-7분-전">면접보기 7분 전</h3>
<p>7분이 남았을 때, 면접실 앞으로 가서 분반별로 대기했다.
이 때가 진짜 떨렸던 순간이었는데, 옆에 계시던 지원자분과 얘기하면서 긴장을 풀었다.
간단한 면접 과정 소개를 해주셨다.
분반에 5명이 있었는데, 모두 분야가 다른 것이 신기했다.(백앤드, 프론트, 임베디드, 머신러닝, 보안)</p>
<h3 id="면접">면접..!</h3>
<p>면접실에 들어가면 의자에 코딩테스트 문제가 올려져있고 그걸 들고 앉으면 된다.
엄청 딱딱한 분위기라 식은땀이 났던 기억이 있다.
1번부터 순서대로 자신의 포트폴리오를 보며 3분간 발표를 했다. 나는 마지막 순서였다.
모두 발표를 하고 나면 멘토님들이 질문을 시작하는데,
공통질문을 하고 5명이 순서대로 답변하거나, 개인별 질문을 주시기도 했다.
기억나는 질문을 리스트해서 적어보겠다. 
기억이 안나는 질문이 있을 수 있다..!</p>
<blockquote>
<ol>
<li>42서울 활동이 끝난 상태인가요? 아니면 블랙홀에 빠진 상태인가요?</li>
<li>Django Serializer에 대해 간단히 설명해주세요.</li>
<li>Spring에서 Serializer의 기능을 하는건 무엇이 있을까요?</li>
<li>(위 질문에 모른다고 하자) Mixin에 대해 들어보신 적 있나요?</li>
<li>AWS EC2를 사용해 첫 배포할 때 어떤 점이 힘드셨나요?(포트폴리오 기반)</li>
<li>(공통질문) 자신이 팀에서 어떤 부분에 기여할 수 있을 지, 기술적인 부분을 포함해 간략하게 말해주세요.</li>
<li>(공통질문) 생각하고 있는 프로젝트가 있다면 말해주세요.</li>
<li>(공통질문) 코딩테스트 문제 중에 아쉬웠던 부분이 있으면 모두 말씀해주세요.</li>
<li>마지막으로 하고 싶은 말이 있다면 말씀해주세요.</li>
<li>(기억나서 추가) Coryn 서비스를 종료하셨는데, 종료하신 이유는?</li>
</ol>
</blockquote>
<h3 id="면접-후기">면접 후기</h3>
<p>질문을 보면 알겠지만, 개인 질문은 거의 포트폴리오 기반으로 나왔다.
다른 지원자들도 마찬가지였다. 올해부터는 발표가 많은 부분을 차지할 것 같다는 생각이 든다..!
CS 공부 열심히 했는데 조금 아쉽긴 하지만, 하나의 양분이 되지 않을까 하는 생각이 든다.
결과적으로 4월 8일, 최종 합격 메일이 왔고, 소마 연수생이 됐다.
특이한 점은 250명을 뽑는다고 써놓았으나 320명을 뽑았다는 점..?
오티때 코로나때문에 중간 이탈하는 수료생이 많을 것 같아 많이 뽑으셨다고 했기 때문에,
내년부터는 어떻게 바뀔 지 모르겠다.
소마 활동 열심히 해서 미국 가야겠다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SW마에스트로 13기 코딩테스트 후기]]></title>
            <link>https://velog.io/@0_zoo/SW%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C-13%EA%B8%B0-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@0_zoo/SW%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C-13%EA%B8%B0-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 14 Apr 2022 06:14:54 GMT</pubDate>
            <description><![CDATA[<p>소마 최종 합격을 한 후, 기록을 남겨두고 싶어 블로그에 남겨두기로 했다.
이번 포스트에서는 1차, 그리고 2차 코딩테스트에 대한 간략한 설명을 할 것이다.</p>
<h2 id="1차-코딩테스트">1차 코딩테스트</h2>
<p>1차는 사실 한두문제 제외하고는 그렇게 어려운 문제는 아니었다.</p>
<h3 id="1번-문제">1번 문제</h3>
<blockquote>
<p>간단한 문자열 문제였고, 어렵지 않았다
<strong>실버 하위권</strong></p>
</blockquote>
<h3 id="2번-문제">2번 문제</h3>
<blockquote>
<p>구현 문제였는데, 시간이 조금 걸려 마지막에 풀려고 미뤄놨다가 결국 풀지 못했다.
그냥 시키는대로만 하면 되는 문제라 크게 어렵지는 않았던 것 같다.
<strong>실버 중위권</strong></p>
</blockquote>
<h3 id="3번-문제">3번 문제</h3>
<blockquote>
<p>문자열 문제였다.
<strong>실버 하위권</strong></p>
</blockquote>
<h3 id="4번-문제">4번 문제</h3>
<blockquote>
<p>간단하게 풀면 브루트포스를 이용해 풀 수 있는 문제였지만, 시간복잡도가 우려되는 문제였다.
<strong>실버 하위권~ 골드 중하위권(시간복잡도 고려 시)</strong></p>
</blockquote>
<h3 id="5번-문제">5번 문제</h3>
<blockquote>
<p>정렬 문제였는데, 이 문제도 시간복잡도가 우려됐다.
<strong>실버 중위권~ 골드 중위권(시간복잡도 고려 시)</strong></p>
</blockquote>
<h3 id="6번-문제">6번 문제</h3>
<blockquote>
<p>최단경로 문제였고, 알고리즘만 안다면 어렵지 않았다.
<strong>골드 하위권</strong></p>
</blockquote>
<h3 id="7번-문제sql">7번 문제(SQL)</h3>
<blockquote>
<p>Join을 사용하는 문제였다.</p>
</blockquote>
<h3 id="8번-문제웹">8번 문제(웹)</h3>
<blockquote>
<p>문제를 안봐서 모르겠다..!</p>
</blockquote>
<h3 id="1차-코테-감상평">1차 코테 감상평(?)</h3>
<p>전체적으로 문제의 난이도는 어렵지 않았지만, 시간복잡도를 생각하게 하는 문제가 좀 있었던 것 같다. 백준 실버 정도만 되도 무난하게 통과할 수 있었던 수준이라고 생각한다.</p>
<h2 id="2차-코딩테스트">2차 코딩테스트</h2>
<p>2차 코딩테스트는 1차보다 전반적으로 난이도가 많이 올라간 느낌을 받았다.</p>
<h3 id="1번-문제-1">1번 문제</h3>
<blockquote>
<p>브루트포스 문제였고, 그리디로 오해할 여지가 있는 문제였다.
시간복잡도 때문에 그리디나 DP를 활용하려 했으나 풀이방법이 생각나지 않았다.
<strong>실버 상위권</strong></p>
</blockquote>
<h3 id="2번-문제-1">2번 문제</h3>
<blockquote>
<p>유니온파인드 문제였다. 
문제에 대놓고 유니온파인드라고 알게끔 적어놨기에,적용만 시키면 됐던 것 같다.
후작업이 조금 필요했다.
<strong>골드 중하위권</strong></p>
</blockquote>
<h3 id="3번-문제-1">3번 문제</h3>
<blockquote>
<p>가장 어려웠던 문제였다. 푼사람도 거의 없을 거라 생각이 든다.
다이나믹 프로그래밍 문제였고, 나중에 다른사람들 얘기를 들어보니 5중 DP를 이용해 풀어야 한다는 것 같았다. 시간안에 풀기는 조금 힘든 문제였던 것 같다.
<strong>플레 하위권</strong></p>
</blockquote>
<h3 id="4번-문제웹">4번 문제(웹)</h3>
<blockquote>
<p>문제를 안봐서 모르겠다</p>
</blockquote>
<h3 id="5번-문제sql">5번 문제(SQL)</h3>
<blockquote>
<p>2중 조인 + 날짜연산 문제였다. 크게 어렵지는 않았다.</p>
</blockquote>
<h3 id="2차-코테-감상평">2차 코테 감상평</h3>
<p> 전체적으로 1차보다 난이도가 많이 올라갔고, 100점을 맞기 힘든 문제가 많았다.
부분점수로 순위가 많이 바뀌지 않았나 생각이 든다.(추측일 뿐이지만..)
2차 코딩테스트를 토요일에 본 후, 그 다음주 화요일에 결과 발표가 났다.
이번 기수부터는 처음으로 포트폴리오 발표가 면접에 추가됐기 때문에, 
금요일까지 포트폴리오를 제출해야 했다.
면접 관련 포스트는 다음에 이어서 작성할 예정이다!</p>
]]></description>
        </item>
    </channel>
</rss>