diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ + diff --git a/404.html b/404.html new file mode 100644 index 0000000..9c472b1 --- /dev/null +++ b/404.html @@ -0,0 +1 @@ + 404: Page not found | Ruggy Blog
Home 404: Page not found
404: Page not found
Cancel
diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000..3292352 --- /dev/null +++ b/about/index.html @@ -0,0 +1 @@ + About | Ruggy Blog
Home About
About
Cancel
diff --git a/app.js b/app.js new file mode 100644 index 0000000..b2ab751 --- /dev/null +++ b/app.js @@ -0,0 +1 @@ +/* Registering Service Worker */ if('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); }; diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 0000000..647bffd --- /dev/null +++ b/archives/index.html @@ -0,0 +1 @@ + Archives | Ruggy Blog
Home Archives
Archives
Cancel
diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..c67ea1d --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,9 @@ +/*! + * The styles for Jekyll theme Chirpy + * + * Chirpy v5.1.0 (https://github.com/cotes2020/jekyll-theme-chirpy) + * © 2019 Cotes Chung + * MIT Licensed + */#search-results a,h5,h4,h3,h2,h1{color:var(--heading-color);font-weight:400;font-family:'Lato', 'Microsoft Yahei', sans-serif}#core-wrapper h5,#core-wrapper h4,#core-wrapper h3,#core-wrapper h2{margin-top:2.5rem;margin-bottom:1.25rem}#core-wrapper h5:focus,#core-wrapper h4:focus,#core-wrapper h3:focus,#core-wrapper h2:focus{outline:none}h5 .anchor,h4 .anchor,h3 .anchor,h2 .anchor{font-size:80%}@media (hover: hover){h5 .anchor,h4 .anchor,h3 .anchor,h2 .anchor{visibility:hidden;opacity:0;transition:opacity 0.25s ease-in, visibility 0s ease-in 0.25s}h5:hover .anchor,h4:hover .anchor,h3:hover .anchor,h2:hover .anchor{visibility:visible;opacity:1;transition:opacity 0.25s ease-in, visibility 0s ease-in 0s}}.post-tag:hover,.tag:hover{background:var(--tag-hover);transition:background 0.35s ease-in-out}.table-wrapper>table tbody tr td,.table-wrapper>table thead th{padding:0.4rem 1rem;font-size:95%;white-space:nowrap}#page-category a:hover,#page-tag a:hover,.license-wrapper>a:hover,#post-list .post-preview a:hover,#search-results a:hover,#topbar #breadcrumb a:hover,.post-content a:not(.img-link):hover,.post-meta a:hover,.post a:hover code,#access-lastmod a:hover,footer a:hover{color:#d2603a !important;border-bottom:1px solid #d2603a;text-decoration:none}#search-results a,#search-hints .post-tag,a{color:var(--link-color)}.post-tail-wrapper .post-meta a:not(:hover),.post-content a:not(.img-link){border-bottom:1px solid var(--link-underline-color)}#sidebar .sidebar-bottom .mode-toggle>i,#sidebar .sidebar-bottom a,#sidebar .nav-item:not(.active)>a,#sidebar .site-title a{transition:color 0.35s ease-in-out}#sidebar .sidebar-bottom .icon-border,.post a.img-link,i.far,i.fas,.code-header{user-select:none}#page-category ul>li>a,#page-tag ul>li>a,#core-wrapper .categories a:not(:hover),#core-wrapper #tags a:not(:hover),#core-wrapper #archives a:not(:hover),#search-results a,#access-lastmod a{border-bottom:none}.share-wrapper .share-icons>i,#search-cancel,.code-header button{cursor:pointer}#related-posts em,#post-list .post-preview .post-meta em,.post-meta em{font-style:normal}.post-content a.img-link+em,img[data-src]+em{display:block;text-align:center;font-style:normal;font-size:80%;padding:0;color:#6d6c6c}#sidebar .sidebar-bottom .mode-toggle,#sidebar a{color:rgba(117,117,117,0.9);user-select:none}@media (prefers-color-scheme: light){html:not([data-mode]),html [data-mode=light]{--highlight-bg-color: #f7f7f7;--highlighter-rouge-color: #2f2f2f;--highlight-lineno-color: #c2c6cc;--inline-code-bg: #f3f3f3;--code-header-text-color: #a3a3b1;--code-header-muted-color: #ebebeb;--code-header-icon-color: #d1d1d1;--clipboard-checked-color: #43c743}html:not([data-mode]) .highlight .hll,html [data-mode=light] .highlight .hll{background-color:#ffffcc}html:not([data-mode]) .highlight .c,html [data-mode=light] .highlight .c{color:#999988;font-style:italic}html:not([data-mode]) .highlight .err,html [data-mode=light] .highlight .err{color:#a61717;background-color:#e3d2d2}html:not([data-mode]) .highlight .k,html [data-mode=light] .highlight .k{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .o,html [data-mode=light] .highlight .o{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .cm,html [data-mode=light] .highlight .cm{color:#999988;font-style:italic}html:not([data-mode]) .highlight .cp,html [data-mode=light] .highlight .cp{color:#999999;font-weight:bold;font-style:italic}html:not([data-mode]) .highlight .c1,html [data-mode=light] .highlight .c1{color:#999988;font-style:italic}html:not([data-mode]) .highlight .cs,html [data-mode=light] .highlight .cs{color:#999999;font-weight:bold;font-style:italic}html:not([data-mode]) .highlight .gd,html [data-mode=light] .highlight .gd{color:#d01040;background-color:#ffdddd}html:not([data-mode]) .highlight .ge,html [data-mode=light] .highlight .ge{color:#000000;font-style:italic}html:not([data-mode]) .highlight .gr,html [data-mode=light] .highlight .gr{color:#aa0000}html:not([data-mode]) .highlight .gh,html [data-mode=light] .highlight .gh{color:#999999}html:not([data-mode]) .highlight .gi,html [data-mode=light] .highlight .gi{color:#008080;background-color:#ddffdd}html:not([data-mode]) .highlight .go,html [data-mode=light] .highlight .go{color:#888888}html:not([data-mode]) .highlight .gp,html [data-mode=light] .highlight .gp{color:#555555}html:not([data-mode]) .highlight .gs,html [data-mode=light] .highlight .gs{font-weight:bold}html:not([data-mode]) .highlight .gu,html [data-mode=light] .highlight .gu{color:#aaaaaa}html:not([data-mode]) .highlight .gt,html [data-mode=light] .highlight .gt{color:#aa0000}html:not([data-mode]) .highlight .kc,html [data-mode=light] .highlight .kc{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .kd,html [data-mode=light] .highlight .kd{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .kn,html [data-mode=light] .highlight .kn{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .kp,html [data-mode=light] .highlight .kp{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .kr,html [data-mode=light] .highlight .kr{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .kt,html [data-mode=light] .highlight .kt{color:#445588;font-weight:bold}html:not([data-mode]) .highlight .m,html [data-mode=light] .highlight .m{color:#009999}html:not([data-mode]) .highlight .s,html [data-mode=light] .highlight .s{color:#d01040}html:not([data-mode]) .highlight .na,html [data-mode=light] .highlight .na{color:#008080}html:not([data-mode]) .highlight .nb,html [data-mode=light] .highlight .nb{color:#0086b3}html:not([data-mode]) .highlight .nc,html [data-mode=light] .highlight .nc{color:#445588;font-weight:bold}html:not([data-mode]) .highlight .no,html [data-mode=light] .highlight .no{color:#008080}html:not([data-mode]) .highlight .nd,html [data-mode=light] .highlight .nd{color:#3c5d5d;font-weight:bold}html:not([data-mode]) .highlight .ni,html [data-mode=light] .highlight .ni{color:#800080}html:not([data-mode]) .highlight .ne,html [data-mode=light] .highlight .ne{color:#990000;font-weight:bold}html:not([data-mode]) .highlight .nf,html [data-mode=light] .highlight .nf{color:#990000;font-weight:bold}html:not([data-mode]) .highlight .nl,html [data-mode=light] .highlight .nl{color:#990000;font-weight:bold}html:not([data-mode]) .highlight .nn,html [data-mode=light] .highlight .nn{color:#555555}html:not([data-mode]) .highlight .nt,html [data-mode=light] .highlight .nt{color:#000080}html:not([data-mode]) .highlight .nv,html [data-mode=light] .highlight .nv{color:#008080}html:not([data-mode]) .highlight .ow,html [data-mode=light] .highlight .ow{color:#000000;font-weight:bold}html:not([data-mode]) .highlight .w,html [data-mode=light] .highlight .w{color:#bbbbbb}html:not([data-mode]) .highlight .mf,html [data-mode=light] .highlight .mf{color:#009999}html:not([data-mode]) .highlight .mh,html [data-mode=light] .highlight .mh{color:#009999}html:not([data-mode]) .highlight .mi,html [data-mode=light] .highlight .mi{color:#009999}html:not([data-mode]) .highlight .mo,html [data-mode=light] .highlight .mo{color:#009999}html:not([data-mode]) .highlight .sb,html [data-mode=light] .highlight .sb{color:#d01040}html:not([data-mode]) .highlight .sc,html [data-mode=light] .highlight .sc{color:#d01040}html:not([data-mode]) .highlight .sd,html [data-mode=light] .highlight .sd{color:#d01040}html:not([data-mode]) .highlight .s2,html [data-mode=light] .highlight .s2{color:#d01040}html:not([data-mode]) .highlight .se,html [data-mode=light] .highlight .se{color:#d01040}html:not([data-mode]) .highlight .sh,html [data-mode=light] .highlight .sh{color:#d01040}html:not([data-mode]) .highlight .si,html [data-mode=light] .highlight .si{color:#d01040}html:not([data-mode]) .highlight .sx,html [data-mode=light] .highlight .sx{color:#d01040}html:not([data-mode]) .highlight .sr,html [data-mode=light] .highlight .sr{color:#009926}html:not([data-mode]) .highlight .s1,html [data-mode=light] .highlight .s1{color:#d01040}html:not([data-mode]) .highlight .ss,html [data-mode=light] .highlight .ss{color:#990073}html:not([data-mode]) .highlight .bp,html [data-mode=light] .highlight .bp{color:#999999}html:not([data-mode]) .highlight .vc,html [data-mode=light] .highlight .vc{color:#008080}html:not([data-mode]) .highlight .vg,html [data-mode=light] .highlight .vg{color:#008080}html:not([data-mode]) .highlight .vi,html [data-mode=light] .highlight .vi{color:#008080}html:not([data-mode]) .highlight .il,html [data-mode=light] .highlight .il{color:#009999}html:not([data-mode]) [class^=prompt-],html [data-mode=light] [class^=prompt-]{--inline-code-bg: #fbfafa;--highlighter-rouge-color: rgb(82 82 82)}html[data-mode=dark]{--highlight-bg-color: #252525;--highlighter-rouge-color: #de6b18;--highlight-lineno-color: #6c6c6d;--inline-code-bg: #272822;--code-header-text-color: #6a6a6a;--code-header-muted-color: rgb(60 60 60);--code-header-icon-color: rgb(86 86 86);--clipboard-checked-color: #2bcc2b;--filepath-text-color: #bdbdbd}html[data-mode=dark] .highlight pre{background-color:var(--highlight-bg-color)}html[data-mode=dark] .highlight .hll{background-color:var(--highlight-bg-color)}html[data-mode=dark] .highlight .c{color:#75715e}html[data-mode=dark] .highlight .err{color:#960050;background-color:#1e0010}html[data-mode=dark] .highlight .k{color:#66d9ef}html[data-mode=dark] .highlight .l{color:#ae81ff}html[data-mode=dark] .highlight .n{color:#f8f8f2}html[data-mode=dark] .highlight .o{color:#f92672}html[data-mode=dark] .highlight .p{color:#f8f8f2}html[data-mode=dark] .highlight .cm{color:#75715e}html[data-mode=dark] .highlight .cp{color:#75715e}html[data-mode=dark] .highlight .c1{color:#75715e}html[data-mode=dark] .highlight .cs{color:#75715e}html[data-mode=dark] .highlight .ge{color:inherit;font-style:italic}html[data-mode=dark] .highlight .gs{font-weight:bold}html[data-mode=dark] .highlight .kc{color:#66d9ef}html[data-mode=dark] .highlight .kd{color:#66d9ef}html[data-mode=dark] .highlight .kn{color:#f92672}html[data-mode=dark] .highlight .kp{color:#66d9ef}html[data-mode=dark] .highlight .kr{color:#66d9ef}html[data-mode=dark] .highlight .kt{color:#66d9ef}html[data-mode=dark] .highlight .ld{color:#e6db74}html[data-mode=dark] .highlight .m{color:#ae81ff}html[data-mode=dark] .highlight .s{color:#e6db74}html[data-mode=dark] .highlight .na{color:#a6e22e}html[data-mode=dark] .highlight .nb{color:#f8f8f2}html[data-mode=dark] .highlight .nc{color:#a6e22e}html[data-mode=dark] .highlight .no{color:#66d9ef}html[data-mode=dark] .highlight .nd{color:#a6e22e}html[data-mode=dark] .highlight .ni{color:#f8f8f2}html[data-mode=dark] .highlight .ne{color:#a6e22e}html[data-mode=dark] .highlight .nf{color:#a6e22e}html[data-mode=dark] .highlight .nl{color:#f8f8f2}html[data-mode=dark] .highlight .nn{color:#f8f8f2}html[data-mode=dark] .highlight .nx{color:#a6e22e}html[data-mode=dark] .highlight .py{color:#f8f8f2}html[data-mode=dark] .highlight .nt{color:#f92672}html[data-mode=dark] .highlight .nv{color:#f8f8f2}html[data-mode=dark] .highlight .ow{color:#f92672}html[data-mode=dark] .highlight .w{color:#f8f8f2}html[data-mode=dark] .highlight .mf{color:#ae81ff}html[data-mode=dark] .highlight .mh{color:#ae81ff}html[data-mode=dark] .highlight .mi{color:#ae81ff}html[data-mode=dark] .highlight .mo{color:#ae81ff}html[data-mode=dark] .highlight .sb{color:#e6db74}html[data-mode=dark] .highlight .sc{color:#e6db74}html[data-mode=dark] .highlight .sd{color:#e6db74}html[data-mode=dark] .highlight .s2{color:#e6db74}html[data-mode=dark] .highlight .se{color:#ae81ff}html[data-mode=dark] .highlight .sh{color:#e6db74}html[data-mode=dark] .highlight .si{color:#e6db74}html[data-mode=dark] .highlight .sx{color:#e6db74}html[data-mode=dark] .highlight .sr{color:#e6db74}html[data-mode=dark] .highlight .s1{color:#e6db74}html[data-mode=dark] .highlight .ss{color:#e6db74}html[data-mode=dark] .highlight .bp{color:#f8f8f2}html[data-mode=dark] .highlight .vc{color:#f8f8f2}html[data-mode=dark] .highlight .vg{color:#f8f8f2}html[data-mode=dark] .highlight .vi{color:#f8f8f2}html[data-mode=dark] .highlight .il{color:#ae81ff}html[data-mode=dark] .highlight .gu{color:#75715e}html[data-mode=dark] .highlight .gd{color:#f92672;background-color:#561c08}html[data-mode=dark] .highlight .gi{color:#a6e22e;background-color:#0b5858}html[data-mode=dark] .highlight .gp{color:#818c96}html[data-mode=dark] pre{color:#bfbfbf}}@media (prefers-color-scheme: dark){html:not([data-mode]),html[data-mode=dark]{--highlight-bg-color: #252525;--highlighter-rouge-color: #de6b18;--highlight-lineno-color: #6c6c6d;--inline-code-bg: #272822;--code-header-text-color: #6a6a6a;--code-header-muted-color: rgb(60 60 60);--code-header-icon-color: rgb(86 86 86);--clipboard-checked-color: #2bcc2b;--filepath-text-color: #bdbdbd}html:not([data-mode]) .highlight pre,html[data-mode=dark] .highlight pre{background-color:var(--highlight-bg-color)}html:not([data-mode]) .highlight .hll,html[data-mode=dark] .highlight .hll{background-color:var(--highlight-bg-color)}html:not([data-mode]) .highlight .c,html[data-mode=dark] .highlight .c{color:#75715e}html:not([data-mode]) .highlight .err,html[data-mode=dark] .highlight .err{color:#960050;background-color:#1e0010}html:not([data-mode]) .highlight .k,html[data-mode=dark] .highlight .k{color:#66d9ef}html:not([data-mode]) .highlight .l,html[data-mode=dark] .highlight .l{color:#ae81ff}html:not([data-mode]) .highlight .n,html[data-mode=dark] .highlight .n{color:#f8f8f2}html:not([data-mode]) .highlight .o,html[data-mode=dark] .highlight .o{color:#f92672}html:not([data-mode]) .highlight .p,html[data-mode=dark] .highlight .p{color:#f8f8f2}html:not([data-mode]) .highlight .cm,html[data-mode=dark] .highlight .cm{color:#75715e}html:not([data-mode]) .highlight .cp,html[data-mode=dark] .highlight .cp{color:#75715e}html:not([data-mode]) .highlight .c1,html[data-mode=dark] .highlight .c1{color:#75715e}html:not([data-mode]) .highlight .cs,html[data-mode=dark] .highlight .cs{color:#75715e}html:not([data-mode]) .highlight .ge,html[data-mode=dark] .highlight .ge{color:inherit;font-style:italic}html:not([data-mode]) .highlight .gs,html[data-mode=dark] .highlight .gs{font-weight:bold}html:not([data-mode]) .highlight .kc,html[data-mode=dark] .highlight .kc{color:#66d9ef}html:not([data-mode]) .highlight .kd,html[data-mode=dark] .highlight .kd{color:#66d9ef}html:not([data-mode]) .highlight .kn,html[data-mode=dark] .highlight .kn{color:#f92672}html:not([data-mode]) .highlight .kp,html[data-mode=dark] .highlight .kp{color:#66d9ef}html:not([data-mode]) .highlight .kr,html[data-mode=dark] .highlight .kr{color:#66d9ef}html:not([data-mode]) .highlight .kt,html[data-mode=dark] .highlight .kt{color:#66d9ef}html:not([data-mode]) .highlight .ld,html[data-mode=dark] .highlight .ld{color:#e6db74}html:not([data-mode]) .highlight .m,html[data-mode=dark] .highlight .m{color:#ae81ff}html:not([data-mode]) .highlight .s,html[data-mode=dark] .highlight .s{color:#e6db74}html:not([data-mode]) .highlight .na,html[data-mode=dark] .highlight .na{color:#a6e22e}html:not([data-mode]) .highlight .nb,html[data-mode=dark] .highlight .nb{color:#f8f8f2}html:not([data-mode]) .highlight .nc,html[data-mode=dark] .highlight .nc{color:#a6e22e}html:not([data-mode]) .highlight .no,html[data-mode=dark] .highlight .no{color:#66d9ef}html:not([data-mode]) .highlight .nd,html[data-mode=dark] .highlight .nd{color:#a6e22e}html:not([data-mode]) .highlight .ni,html[data-mode=dark] .highlight .ni{color:#f8f8f2}html:not([data-mode]) .highlight .ne,html[data-mode=dark] .highlight .ne{color:#a6e22e}html:not([data-mode]) .highlight .nf,html[data-mode=dark] .highlight .nf{color:#a6e22e}html:not([data-mode]) .highlight .nl,html[data-mode=dark] .highlight .nl{color:#f8f8f2}html:not([data-mode]) .highlight .nn,html[data-mode=dark] .highlight .nn{color:#f8f8f2}html:not([data-mode]) .highlight .nx,html[data-mode=dark] .highlight .nx{color:#a6e22e}html:not([data-mode]) .highlight .py,html[data-mode=dark] .highlight .py{color:#f8f8f2}html:not([data-mode]) .highlight .nt,html[data-mode=dark] .highlight .nt{color:#f92672}html:not([data-mode]) .highlight .nv,html[data-mode=dark] .highlight .nv{color:#f8f8f2}html:not([data-mode]) .highlight .ow,html[data-mode=dark] .highlight .ow{color:#f92672}html:not([data-mode]) .highlight .w,html[data-mode=dark] .highlight .w{color:#f8f8f2}html:not([data-mode]) .highlight .mf,html[data-mode=dark] .highlight .mf{color:#ae81ff}html:not([data-mode]) .highlight .mh,html[data-mode=dark] .highlight .mh{color:#ae81ff}html:not([data-mode]) .highlight .mi,html[data-mode=dark] .highlight .mi{color:#ae81ff}html:not([data-mode]) .highlight .mo,html[data-mode=dark] .highlight .mo{color:#ae81ff}html:not([data-mode]) .highlight .sb,html[data-mode=dark] .highlight .sb{color:#e6db74}html:not([data-mode]) .highlight .sc,html[data-mode=dark] .highlight .sc{color:#e6db74}html:not([data-mode]) .highlight .sd,html[data-mode=dark] .highlight .sd{color:#e6db74}html:not([data-mode]) .highlight .s2,html[data-mode=dark] .highlight .s2{color:#e6db74}html:not([data-mode]) .highlight .se,html[data-mode=dark] .highlight .se{color:#ae81ff}html:not([data-mode]) .highlight .sh,html[data-mode=dark] .highlight .sh{color:#e6db74}html:not([data-mode]) .highlight .si,html[data-mode=dark] .highlight .si{color:#e6db74}html:not([data-mode]) .highlight .sx,html[data-mode=dark] .highlight .sx{color:#e6db74}html:not([data-mode]) .highlight .sr,html[data-mode=dark] .highlight .sr{color:#e6db74}html:not([data-mode]) .highlight .s1,html[data-mode=dark] .highlight .s1{color:#e6db74}html:not([data-mode]) .highlight .ss,html[data-mode=dark] .highlight .ss{color:#e6db74}html:not([data-mode]) .highlight .bp,html[data-mode=dark] .highlight .bp{color:#f8f8f2}html:not([data-mode]) .highlight .vc,html[data-mode=dark] .highlight .vc{color:#f8f8f2}html:not([data-mode]) .highlight .vg,html[data-mode=dark] .highlight .vg{color:#f8f8f2}html:not([data-mode]) .highlight .vi,html[data-mode=dark] .highlight .vi{color:#f8f8f2}html:not([data-mode]) .highlight .il,html[data-mode=dark] .highlight .il{color:#ae81ff}html:not([data-mode]) .highlight .gu,html[data-mode=dark] .highlight .gu{color:#75715e}html:not([data-mode]) .highlight .gd,html[data-mode=dark] .highlight .gd{color:#f92672;background-color:#561c08}html:not([data-mode]) .highlight .gi,html[data-mode=dark] .highlight .gi{color:#a6e22e;background-color:#0b5858}html:not([data-mode]) .highlight .gp,html[data-mode=dark] .highlight .gp{color:#818c96}html:not([data-mode]) pre,html[data-mode=dark] pre{color:#bfbfbf}html[data-mode=light]{--highlight-bg-color: #f7f7f7;--highlighter-rouge-color: #2f2f2f;--highlight-lineno-color: #c2c6cc;--inline-code-bg: #f3f3f3;--code-header-text-color: #a3a3b1;--code-header-muted-color: #ebebeb;--code-header-icon-color: #d1d1d1;--clipboard-checked-color: #43c743}html[data-mode=light] .highlight .hll{background-color:#ffffcc}html[data-mode=light] .highlight .c{color:#999988;font-style:italic}html[data-mode=light] .highlight .err{color:#a61717;background-color:#e3d2d2}html[data-mode=light] .highlight .k{color:#000000;font-weight:bold}html[data-mode=light] .highlight .o{color:#000000;font-weight:bold}html[data-mode=light] .highlight .cm{color:#999988;font-style:italic}html[data-mode=light] .highlight .cp{color:#999999;font-weight:bold;font-style:italic}html[data-mode=light] .highlight .c1{color:#999988;font-style:italic}html[data-mode=light] .highlight .cs{color:#999999;font-weight:bold;font-style:italic}html[data-mode=light] .highlight .gd{color:#d01040;background-color:#ffdddd}html[data-mode=light] .highlight .ge{color:#000000;font-style:italic}html[data-mode=light] .highlight .gr{color:#aa0000}html[data-mode=light] .highlight .gh{color:#999999}html[data-mode=light] .highlight .gi{color:#008080;background-color:#ddffdd}html[data-mode=light] .highlight .go{color:#888888}html[data-mode=light] .highlight .gp{color:#555555}html[data-mode=light] .highlight .gs{font-weight:bold}html[data-mode=light] .highlight .gu{color:#aaaaaa}html[data-mode=light] .highlight .gt{color:#aa0000}html[data-mode=light] .highlight .kc{color:#000000;font-weight:bold}html[data-mode=light] .highlight .kd{color:#000000;font-weight:bold}html[data-mode=light] .highlight .kn{color:#000000;font-weight:bold}html[data-mode=light] .highlight .kp{color:#000000;font-weight:bold}html[data-mode=light] .highlight .kr{color:#000000;font-weight:bold}html[data-mode=light] .highlight .kt{color:#445588;font-weight:bold}html[data-mode=light] .highlight .m{color:#009999}html[data-mode=light] .highlight .s{color:#d01040}html[data-mode=light] .highlight .na{color:#008080}html[data-mode=light] .highlight .nb{color:#0086b3}html[data-mode=light] .highlight .nc{color:#445588;font-weight:bold}html[data-mode=light] .highlight .no{color:#008080}html[data-mode=light] .highlight .nd{color:#3c5d5d;font-weight:bold}html[data-mode=light] .highlight .ni{color:#800080}html[data-mode=light] .highlight .ne{color:#990000;font-weight:bold}html[data-mode=light] .highlight .nf{color:#990000;font-weight:bold}html[data-mode=light] .highlight .nl{color:#990000;font-weight:bold}html[data-mode=light] .highlight .nn{color:#555555}html[data-mode=light] .highlight .nt{color:#000080}html[data-mode=light] .highlight .nv{color:#008080}html[data-mode=light] .highlight .ow{color:#000000;font-weight:bold}html[data-mode=light] .highlight .w{color:#bbbbbb}html[data-mode=light] .highlight .mf{color:#009999}html[data-mode=light] .highlight .mh{color:#009999}html[data-mode=light] .highlight .mi{color:#009999}html[data-mode=light] .highlight .mo{color:#009999}html[data-mode=light] .highlight .sb{color:#d01040}html[data-mode=light] .highlight .sc{color:#d01040}html[data-mode=light] .highlight .sd{color:#d01040}html[data-mode=light] .highlight .s2{color:#d01040}html[data-mode=light] .highlight .se{color:#d01040}html[data-mode=light] .highlight .sh{color:#d01040}html[data-mode=light] .highlight .si{color:#d01040}html[data-mode=light] .highlight .sx{color:#d01040}html[data-mode=light] .highlight .sr{color:#009926}html[data-mode=light] .highlight .s1{color:#d01040}html[data-mode=light] .highlight .ss{color:#990073}html[data-mode=light] .highlight .bp{color:#999999}html[data-mode=light] .highlight .vc{color:#008080}html[data-mode=light] .highlight .vg{color:#008080}html[data-mode=light] .highlight .vi{color:#008080}html[data-mode=light] .highlight .il{color:#009999}html[data-mode=light] [class^=prompt-]{--inline-code-bg: #fbfafa;--highlighter-rouge-color: rgb(82 82 82)}}figure.highlight,.highlight,.highlighter-rouge{background:var(--highlight-bg-color)}.highlight,.highlighter-rouge{border-radius:6px}td.rouge-code{padding-left:1rem;padding-right:1.5rem}.highlighter-rouge{color:var(--highlighter-rouge-color);margin-top:0.5rem;margin-bottom:1.2em}.highlight{overflow:auto;padding-top:0.5rem;padding-bottom:1rem}.highlight pre{margin-bottom:0;font-size:.85rem;line-height:1.4rem;word-wrap:normal}.highlight table td pre{overflow:visible;word-break:normal}.highlight .lineno{padding-right:0.5rem;min-width:2.2rem;text-align:right;color:var(--highlight-lineno-color);-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.highlight .gp{user-select:none}code{-webkit-hyphens:none;-ms-hyphens:none;-moz-hyphens:none;hyphens:none}code.highlighter-rouge{font-size:.85rem;padding:3px 5px;border-radius:4px;background-color:var(--inline-code-bg)}code.filepath{background-color:inherit;color:var(--filepath-text-color);font-weight:600;padding:0}a>code.highlighter-rouge{padding-bottom:0;color:inherit}a:hover>code.highlighter-rouge{border-bottom:none}blockquote code{color:inherit}.highlight>code{color:transparent}td.rouge-code a{color:inherit !important;border-bottom:none !important;pointer-events:none}div[class^='highlighter-rouge'] pre.lineno,div.language-plaintext.highlighter-rouge pre.lineno,div.language-console.highlighter-rouge pre.lineno,div.language-terminal.highlighter-rouge pre.lineno,div.nolineno pre.lineno{display:none}div[class^='highlighter-rouge'] td.rouge-code,div.language-plaintext.highlighter-rouge td.rouge-code,div.language-console.highlighter-rouge td.rouge-code,div.language-terminal.highlighter-rouge td.rouge-code,div.nolineno td.rouge-code{padding-left:1.5rem}.code-header{border-top-left-radius:6px;border-top-right-radius:6px;display:flex;justify-content:space-between;align-items:center;height:2.25rem}.code-header::before{content:"";display:inline-block;margin-left:1rem;width:.75rem;height:.75rem;border-radius:50%;background-color:var(--code-header-muted-color);box-shadow:1.25rem 0 0 var(--code-header-muted-color),2.5rem 0 0 var(--code-header-muted-color)}.code-header span i{font-size:1rem;margin-right:0.4rem;color:var(--code-header-icon-color)}.code-header span i.small{font-size:70%}[file] .code-header span>i{position:relative;top:1px}.code-header span::after{content:attr(data-label-text);font-size:0.85rem;font-weight:600;color:var(--code-header-text-color)}.code-header button{border:1px solid transparent;border-radius:6px;height:2.25rem;width:2.25rem;padding:0;background-color:inherit}.code-header button i{color:var(--code-header-icon-color)}.code-header button[timeout]:hover{border-color:var(--clipboard-checked-color)}.code-header button[timeout] i{color:var(--clipboard-checked-color)}.code-header button:not([timeout]):hover{background-color:rgba(128,128,128,0.37)}.code-header button:not([timeout]):hover i{color:white}.code-header button:focus{outline:none}@media all and (max-width: 576px){.post-content>div[class^='language-']{margin-left:-1.25rem;margin-right:-1.25rem;border-radius:0}.post-content>div[class^='language-'] .highlight{padding-left:0.25rem}.post-content>div[class^='language-'] .code-header{border-radius:0;padding-left:0.4rem;padding-right:0.5rem}}html{font-size:16px}@media (prefers-color-scheme: light){html:not([data-mode]),html [data-mode=light]{--body-bg: #fafafa;--mask-bg: #c1c3c5;--main-wrapper-bg: white;--main-border-color: #f3f3f3;--text-color: #34343c;--text-muted-color: gray;--heading-color: black;--blockquote-border-color: #eee;--blockquote-text-color: #9a9a9a;--link-color: #2a408e;--link-underline-color: #dee2e6;--button-bg: #fff;--btn-border-color: #e9ecef;--btn-backtotop-color: #686868;--btn-backtotop-border-color: #f1f1f1;--btn-box-shadow: #eaeaea;--checkbox-color: #c5c5c5;--checkbox-checked-color: #07a8f7;--sidebar-bg: #eeeeee;--sidebar-muted-color: #a2a19f;--sidebar-active-color: #424242;--nav-cursor-color: #757575;--sidebar-btn-bg: white;--topbar-text-color: rgb(78, 78, 78);--topbar-wrapper-bg: white;--search-wrapper-bg: rgb(245 245 245 / 50%);--search-wrapper-border-color: rgb(245 245 245);--search-tag-bg: #f8f9fa;--search-icon-color: #c2c6cc;--input-focus-border-color: var(--btn-border-color);--post-list-text-color: dimgray;--btn-patinator-text-color: #555555;--btn-paginator-hover-color: var(--sidebar-bg);--btn-paginator-border-color: var(--sidebar-bg);--btn-text-color: #676666;--pin-bg: #f5f5f5;--pin-color: #999fa4;--btn-share-hover-color: var(--link-color);--card-border-color: #f1f1f1;--card-box-shadow: rgba(234, 234, 234, 0.7686274509803922);--label-color: #616161;--relate-post-date: rgba(30, 55, 70, 0.4);--footnote-target-bg: lightcyan;--tag-bg: rgba(0, 0, 0, 0.075);--tag-border: #dee2e6;--tag-shadow: var(--btn-border-color);--tag-hover: rgb(222, 226, 230);--tb-odd-bg: #fbfcfd;--tb-border-color: #eaeaea;--dash-color: silver;--preview-img-bg: radial-gradient(circle, rgb(255 255 255) 0%, rgb(249 249 249) 100%);--kbd-wrap-color: #bdbdbd;--kbd-text-color: var(--text-color);--kbd-bg-color: white;--prompt-text-color: rgb(46 46 46 / 77%);--prompt-tip-bg: rgb(123 247 144 / 20%);--prompt-tip-icon-color: #03b303;--prompt-info-bg: #e1f5fe;--prompt-info-icon-color: #0070cb;--prompt-warning-bg: rgb(255 243 205);--prompt-warning-icon-color: #ef9c03;--prompt-danger-bg: rgb(248 215 218 / 56%);--prompt-danger-icon-color: #df3c30;--categories-hover-bg: var(--btn-border-color);--categories-icon-hover-color: darkslategray;--timeline-color: rgba(0, 0, 0, 0.075);--timeline-node-bg: #c2c6cc;--timeline-year-dot-color: #ffffff}html:not([data-mode]) [class^=prompt-],html [data-mode=light] [class^=prompt-]{--link-underline-color: rgb(219 216 216)}html[data-mode=dark]{--body-bg: var(--main-wrapper-bg);--mask-bg: rgb(68, 69, 70);--main-wrapper-bg: rgb(27, 27, 30);--main-border-color: rgb(44, 45, 45);--text-color: rgb(175, 176, 177);--text-muted-color: rgb(107, 116, 124);--heading-color: #cccccc;--blockquote-border-color: rgb(66, 66, 66);--blockquote-text-color: rgb(117, 117, 117);--link-color: rgb(138, 180, 248);--link-underline-color: rgb(82, 108, 150);--button-bg: rgb(39, 40, 43);--btn-border-color: rgb(63, 65, 68);--btn-backtotop-color: var(--text-color);--btn-backtotop-border-color: var(--btn-border-color);--btn-box-shadow: var(--main-wrapper-bg);--card-header-bg: rgb(51, 50, 50);--label-color: rgb(108, 117, 125);--checkbox-color: rgb(118 120 121);--checkbox-checked-color: var(--link-color);--sidebar-bg: radial-gradient(circle, #242424 0%, #1d1f27 100%);--sidebar-muted-color: #6d6c6b;--sidebar-active-color: rgb(255 255 255 / 80%);--nav-cursor-color: rgb(183, 182, 182);--sidebar-btn-bg: rgb(117 116 116 / 20%);--topbar-text-color: var(--text-color);--topbar-wrapper-bg: rgb(39, 40, 43);--search-wrapper-bg: rgb(34, 34, 39);--search-wrapper-border-color: rgb(34, 34, 39);--search-icon-color: rgb(100, 102, 105);--input-focus-border-color: rgb(112, 114, 115);--post-list-text-color: rgb(175, 176, 177);--btn-patinator-text-color: var(--text-color);--btn-paginator-hover-color: rgb(64, 65, 66);--btn-paginator-border-color: var(--btn-border-color);--btn-text-color: var(--text-color);--pin-bg: rgb(34 35 37);--pin-color: inherit;--toc-highlight: rgb(116, 178, 243);--tag-bg: rgb(41, 40, 40);--tag-hover: rgb(43, 56, 62);--tb-odd-bg: rgba(42, 47, 53, 0.52);--tb-even-bg: rgb(31, 31, 34);--tb-border-color: var(--tb-odd-bg);--footnote-target-bg: rgb(63, 81, 181);--btn-share-color: #6c757d;--btn-share-hover-color: #bfc1ca;--relate-post-date: var(--text-muted-color);--card-bg: rgb(39, 40, 43);--card-border-color: rgb(53, 53, 60);--card-box-shadow: var(--main-wrapper-bg);--preview-img-bg: radial-gradient(circle, rgb(22 22 24) 0%, rgb(32 32 32) 100%);--kbd-wrap-color: #6a6a6a;--kbd-text-color: #d3d3d3;--kbd-bg-color: #242424;--prompt-text-color: rgb(216 212 212 / 75%);--prompt-tip-bg: rgba(77, 187, 95, 0.2);--prompt-tip-icon-color: rgb(5 223 5 / 68%);--prompt-info-bg: rgb(7 59 104 / 80%);--prompt-info-icon-color: #0075d1;--prompt-warning-bg: rgb(90 69 3 / 95%);--prompt-warning-icon-color: rgb(255 165 0 / 80%);--prompt-danger-bg: rgb(86 28 8 / 80%);--prompt-danger-icon-color: #cd0202;--tag-border: rgb(59, 79, 88);--tag-shadow: rgb(32, 33, 33);--search-tag-bg: var(--tag-bg);--dash-color: rgb(63, 65, 68);--categories-border: rgb(64, 66, 69);--categories-hover-bg: rgb(73, 75, 76);--categories-icon-hover-color: white;--timeline-node-bg: rgb(150, 152, 156);--timeline-color: rgb(63, 65, 68);--timeline-year-dot-color: var(--timeline-color);color-scheme:dark}html[data-mode=dark] .post img[data-src]{filter:brightness(95%)}html[data-mode=dark] hr{border-color:var(--main-border-color)}html[data-mode=dark] nav[data-toggle=toc] .nav-link.active,html[data-mode=dark] nav[data-toggle=toc] .nav-link.active:focus,html[data-mode=dark] nav[data-toggle=toc] .nav-link.active:hover,html[data-mode=dark] nav[data-toggle=toc] .nav>li>a:focus,html[data-mode=dark] nav[data-toggle=toc] .nav>li>a:hover{color:var(--toc-highlight) !important;border-left-color:var(--toc-highlight) !important}html[data-mode=dark] .categories.card,html[data-mode=dark] .list-group-item{background-color:var(--card-bg)}html[data-mode=dark] .categories .card-header{background-color:var(--card-header-bg)}html[data-mode=dark] .categories .list-group-item{border-left:none;border-right:none;padding-left:2rem;border-color:var(--categories-border)}html[data-mode=dark] .categories .list-group-item:last-child{border-bottom-color:var(--card-bg)}html[data-mode=dark] #archives li:nth-child(odd){background-image:linear-gradient(to left, #1a1a1e, #27272d, #27272d, #27272d, #1a1a1e)}html[data-mode=dark] #disqus_thread{color-scheme:none}}@media (prefers-color-scheme: dark){html:not([data-mode]),html[data-mode=dark]{--body-bg: var(--main-wrapper-bg);--mask-bg: rgb(68, 69, 70);--main-wrapper-bg: rgb(27, 27, 30);--main-border-color: rgb(44, 45, 45);--text-color: rgb(175, 176, 177);--text-muted-color: rgb(107, 116, 124);--heading-color: #cccccc;--blockquote-border-color: rgb(66, 66, 66);--blockquote-text-color: rgb(117, 117, 117);--link-color: rgb(138, 180, 248);--link-underline-color: rgb(82, 108, 150);--button-bg: rgb(39, 40, 43);--btn-border-color: rgb(63, 65, 68);--btn-backtotop-color: var(--text-color);--btn-backtotop-border-color: var(--btn-border-color);--btn-box-shadow: var(--main-wrapper-bg);--card-header-bg: rgb(51, 50, 50);--label-color: rgb(108, 117, 125);--checkbox-color: rgb(118 120 121);--checkbox-checked-color: var(--link-color);--sidebar-bg: radial-gradient(circle, #242424 0%, #1d1f27 100%);--sidebar-muted-color: #6d6c6b;--sidebar-active-color: rgb(255 255 255 / 80%);--nav-cursor-color: rgb(183, 182, 182);--sidebar-btn-bg: rgb(117 116 116 / 20%);--topbar-text-color: var(--text-color);--topbar-wrapper-bg: rgb(39, 40, 43);--search-wrapper-bg: rgb(34, 34, 39);--search-wrapper-border-color: rgb(34, 34, 39);--search-icon-color: rgb(100, 102, 105);--input-focus-border-color: rgb(112, 114, 115);--post-list-text-color: rgb(175, 176, 177);--btn-patinator-text-color: var(--text-color);--btn-paginator-hover-color: rgb(64, 65, 66);--btn-paginator-border-color: var(--btn-border-color);--btn-text-color: var(--text-color);--pin-bg: rgb(34 35 37);--pin-color: inherit;--toc-highlight: rgb(116, 178, 243);--tag-bg: rgb(41, 40, 40);--tag-hover: rgb(43, 56, 62);--tb-odd-bg: rgba(42, 47, 53, 0.52);--tb-even-bg: rgb(31, 31, 34);--tb-border-color: var(--tb-odd-bg);--footnote-target-bg: rgb(63, 81, 181);--btn-share-color: #6c757d;--btn-share-hover-color: #bfc1ca;--relate-post-date: var(--text-muted-color);--card-bg: rgb(39, 40, 43);--card-border-color: rgb(53, 53, 60);--card-box-shadow: var(--main-wrapper-bg);--preview-img-bg: radial-gradient(circle, rgb(22 22 24) 0%, rgb(32 32 32) 100%);--kbd-wrap-color: #6a6a6a;--kbd-text-color: #d3d3d3;--kbd-bg-color: #242424;--prompt-text-color: rgb(216 212 212 / 75%);--prompt-tip-bg: rgba(77, 187, 95, 0.2);--prompt-tip-icon-color: rgb(5 223 5 / 68%);--prompt-info-bg: rgb(7 59 104 / 80%);--prompt-info-icon-color: #0075d1;--prompt-warning-bg: rgb(90 69 3 / 95%);--prompt-warning-icon-color: rgb(255 165 0 / 80%);--prompt-danger-bg: rgb(86 28 8 / 80%);--prompt-danger-icon-color: #cd0202;--tag-border: rgb(59, 79, 88);--tag-shadow: rgb(32, 33, 33);--search-tag-bg: var(--tag-bg);--dash-color: rgb(63, 65, 68);--categories-border: rgb(64, 66, 69);--categories-hover-bg: rgb(73, 75, 76);--categories-icon-hover-color: white;--timeline-node-bg: rgb(150, 152, 156);--timeline-color: rgb(63, 65, 68);--timeline-year-dot-color: var(--timeline-color);color-scheme:dark}html:not([data-mode]) .post img[data-src],html[data-mode=dark] .post img[data-src]{filter:brightness(95%)}html:not([data-mode]) hr,html[data-mode=dark] hr{border-color:var(--main-border-color)}html:not([data-mode]) nav[data-toggle=toc] .nav-link.active,html:not([data-mode]) nav[data-toggle=toc] .nav-link.active:focus,html:not([data-mode]) nav[data-toggle=toc] .nav-link.active:hover,html:not([data-mode]) nav[data-toggle=toc] .nav>li>a:focus,html:not([data-mode]) nav[data-toggle=toc] .nav>li>a:hover,html[data-mode=dark] nav[data-toggle=toc] .nav-link.active,html[data-mode=dark] nav[data-toggle=toc] .nav-link.active:focus,html[data-mode=dark] nav[data-toggle=toc] .nav-link.active:hover,html[data-mode=dark] nav[data-toggle=toc] .nav>li>a:focus,html[data-mode=dark] nav[data-toggle=toc] .nav>li>a:hover{color:var(--toc-highlight) !important;border-left-color:var(--toc-highlight) !important}html:not([data-mode]) .categories.card,html:not([data-mode]) .list-group-item,html[data-mode=dark] .categories.card,html[data-mode=dark] .list-group-item{background-color:var(--card-bg)}html:not([data-mode]) .categories .card-header,html[data-mode=dark] .categories .card-header{background-color:var(--card-header-bg)}html:not([data-mode]) .categories .list-group-item,html[data-mode=dark] .categories .list-group-item{border-left:none;border-right:none;padding-left:2rem;border-color:var(--categories-border)}html:not([data-mode]) .categories .list-group-item:last-child,html[data-mode=dark] .categories .list-group-item:last-child{border-bottom-color:var(--card-bg)}html:not([data-mode]) #archives li:nth-child(odd),html[data-mode=dark] #archives li:nth-child(odd){background-image:linear-gradient(to left, #1a1a1e, #27272d, #27272d, #27272d, #1a1a1e)}html:not([data-mode]) #disqus_thread,html[data-mode=dark] #disqus_thread{color-scheme:none}html[data-mode=light]{--body-bg: #fafafa;--mask-bg: #c1c3c5;--main-wrapper-bg: white;--main-border-color: #f3f3f3;--text-color: #34343c;--text-muted-color: gray;--heading-color: black;--blockquote-border-color: #eee;--blockquote-text-color: #9a9a9a;--link-color: #2a408e;--link-underline-color: #dee2e6;--button-bg: #fff;--btn-border-color: #e9ecef;--btn-backtotop-color: #686868;--btn-backtotop-border-color: #f1f1f1;--btn-box-shadow: #eaeaea;--checkbox-color: #c5c5c5;--checkbox-checked-color: #07a8f7;--sidebar-bg: #eeeeee;--sidebar-muted-color: #a2a19f;--sidebar-active-color: #424242;--nav-cursor-color: #757575;--sidebar-btn-bg: white;--topbar-text-color: rgb(78, 78, 78);--topbar-wrapper-bg: white;--search-wrapper-bg: rgb(245 245 245 / 50%);--search-wrapper-border-color: rgb(245 245 245);--search-tag-bg: #f8f9fa;--search-icon-color: #c2c6cc;--input-focus-border-color: var(--btn-border-color);--post-list-text-color: dimgray;--btn-patinator-text-color: #555555;--btn-paginator-hover-color: var(--sidebar-bg);--btn-paginator-border-color: var(--sidebar-bg);--btn-text-color: #676666;--pin-bg: #f5f5f5;--pin-color: #999fa4;--btn-share-hover-color: var(--link-color);--card-border-color: #f1f1f1;--card-box-shadow: rgba(234, 234, 234, 0.7686274509803922);--label-color: #616161;--relate-post-date: rgba(30, 55, 70, 0.4);--footnote-target-bg: lightcyan;--tag-bg: rgba(0, 0, 0, 0.075);--tag-border: #dee2e6;--tag-shadow: var(--btn-border-color);--tag-hover: rgb(222, 226, 230);--tb-odd-bg: #fbfcfd;--tb-border-color: #eaeaea;--dash-color: silver;--preview-img-bg: radial-gradient(circle, rgb(255 255 255) 0%, rgb(249 249 249) 100%);--kbd-wrap-color: #bdbdbd;--kbd-text-color: var(--text-color);--kbd-bg-color: white;--prompt-text-color: rgb(46 46 46 / 77%);--prompt-tip-bg: rgb(123 247 144 / 20%);--prompt-tip-icon-color: #03b303;--prompt-info-bg: #e1f5fe;--prompt-info-icon-color: #0070cb;--prompt-warning-bg: rgb(255 243 205);--prompt-warning-icon-color: #ef9c03;--prompt-danger-bg: rgb(248 215 218 / 56%);--prompt-danger-icon-color: #df3c30;--categories-hover-bg: var(--btn-border-color);--categories-icon-hover-color: darkslategray;--timeline-color: rgba(0, 0, 0, 0.075);--timeline-node-bg: #c2c6cc;--timeline-year-dot-color: #ffffff}html[data-mode=light] [class^=prompt-]{--link-underline-color: rgb(219 216 216)}}body{line-height:1.75rem;background:var(--body-bg);color:var(--text-color);-webkit-font-smoothing:antialiased;font-family:'Source Sans Pro', 'Microsoft Yahei', sans-serif}h1{font-size:1.9rem}h2{font-size:1.5rem}h3{font-size:1.2rem}h4{font-size:1.15rem}h5{font-size:1.1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:1rem}img{max-width:100%;height:auto}blockquote{border-left:5px solid var(--blockquote-border-color);padding-left:1rem;color:var(--blockquote-text-color)}blockquote[class^="prompt-"]{display:flex;border-left:0;border-radius:6px;padding:0.75rem 1.2rem;color:var(--prompt-text-color)}blockquote[class^="prompt-"]::before{margin-right:1rem;font-family:"Font Awesome 5 Free";text-align:center;width:1.25rem}blockquote[class^="prompt-"] p:last-child{margin-bottom:0rem}blockquote.prompt-tip{background-color:var(--prompt-tip-bg)}blockquote.prompt-tip::before{content:"";color:var(--prompt-tip-icon-color);font-weight:400}blockquote.prompt-info{background-color:var(--prompt-info-bg)}blockquote.prompt-info::before{content:"";color:var(--prompt-info-icon-color);font-weight:900}blockquote.prompt-warning{background-color:var(--prompt-warning-bg)}blockquote.prompt-warning::before{content:"";color:var(--prompt-warning-icon-color);font-weight:900}blockquote.prompt-danger{background-color:var(--prompt-danger-bg)}blockquote.prompt-danger::before{content:"";color:var(--prompt-danger-icon-color);font-weight:900}kbd{font-family:inherit;display:inline-block;vertical-align:middle;line-height:1.3rem;min-width:1.75rem;text-align:center;margin:0 0.3rem;padding-top:0.1rem;color:var(--kbd-text-color);background-color:var(--kbd-bg-color);border-radius:0.25rem;border:solid 1px var(--kbd-wrap-color);box-shadow:inset 0 -2px 0 var(--kbd-wrap-color)}footer{position:absolute;bottom:0;padding:0 1rem;height:5rem;font-size:0.8rem}footer>div.d-flex{line-height:1.2rem;width:95%;max-width:1045px;border-top:1px solid var(--main-border-color);margin-bottom:1rem}footer>div.d-flex>div{width:350px}footer a:link{text-decoration:none}footer a:hover{text-decoration:none}footer .footer-right{text-align:right}@keyframes fade-in{from{opacity:0}to{opacity:1}}img[data-src]{margin:0.5rem 0}img[data-src][data-loaded=true]{animation:fade-in linear 0.5s}img.left[data-src]{float:left;margin:0.75rem 1rem 1rem 0}img.right[data-src]{float:right;margin:0.75rem 0 1rem 1rem}img.shadow[data-src]{filter:drop-shadow(2px 4px 6px rgba(0,0,0,0.08));box-shadow:none !important}.access{top:2rem;transition:top 0.2s ease-in-out;margin-right:1.5rem;margin-top:3rem;margin-bottom:4rem}.access:only-child{position:-webkit-sticky;position:sticky}.access>div{padding-left:1rem;border-left:1px solid var(--main-border-color)}.access>div:not(:last-child){margin-bottom:4rem}.access .post-content{font-size:0.9rem}#panel-wrapper .panel-heading{color:var(--label-color);font-size:inherit;font-weight:600}#panel-wrapper .post-tag{display:inline-block;line-height:1rem;font-size:0.85rem;background:none;border:1px solid var(--btn-border-color);border-radius:0.8rem;padding:0.3rem 0.5rem;margin:0 0.35rem 0.5rem 0}#panel-wrapper .post-tag:hover{background-color:#2a408e;border-color:#2a408e;color:#fff;transition:none}[data-topbar-visible=true] #panel-wrapper>div{top:6rem}#access-lastmod li{height:1.8rem;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;list-style:none}#access-lastmod a{color:inherit}.footnotes>ol{padding-left:2rem;margin-top:0.5rem}.footnotes>ol>li:not(:last-child){margin-bottom:0.3rem}.footnotes>ol>li>p{margin-left:0.25em;margin-top:0;margin-bottom:0}.footnotes>ol>li:target:not([scroll-focus]),.footnotes>ol>li[scroll-focus=true]>p{background-color:var(--footnote-target-bg);width:fit-content;-webkit-transition:background-color 1.5s ease-in-out;transition:background-color 1.5s ease-in-out}a.footnote{margin-left:1px;margin-right:1px;padding-left:2px;padding-right:2px;border-bottom-style:none !important;-webkit-transition:background-color 1.5s ease-in-out;transition:background-color 1.5s ease-in-out}sup:target:not([scroll-focus]),sup[scroll-focus=true]>a.footnote{background-color:var(--footnote-target-bg)}a.reversefootnote{font-size:0.6rem;line-height:1;position:relative;bottom:0.25em;margin-left:0.25em;border-bottom-style:none !important}.table-wrapper{overflow-x:auto;margin-bottom:1.5rem}.table-wrapper>table{min-width:100%;overflow-x:auto;border-spacing:0}.table-wrapper>table thead{border-bottom:solid 2px rgba(210,215,217,0.75)}.table-wrapper>table tbody tr{border-bottom:1px solid var(--tb-border-color)}.table-wrapper>table tbody tr:nth-child(2n){background-color:var(--tb-even-bg)}.table-wrapper>table tbody tr:nth-child(2n + 1){background-color:var(--tb-odd-bg)}.post h1{margin-top:3rem;margin-bottom:1.5rem}.post a.popup{cursor:zoom-in}.post a.popup>img[data-src]:not(.normal):not(.left):not(.right){position:relative;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.pageviews .fa-spinner{font-size:80%}.post-meta{font-size:0.85rem;word-spacing:1px}.post-meta a:not(:last-child){margin-right:2px}.post-content{font-size:1.08rem;line-height:1.8;margin-top:2rem;overflow-wrap:break-word;word-wrap:break-word}.post-content ul .task-list-item[hide-bullet]{list-style-type:none}.post-content ul .task-list-item[hide-bullet]>i{margin:0 0.4rem 0.2rem -1.4rem;vertical-align:middle;color:var(--checkbox-color)}.post-content ul .task-list-item[hide-bullet]>i.checked{color:var(--checkbox-checked-color)}.post-content ul input[type=checkbox]{margin:0 0.5rem 0.2rem -1.3rem;vertical-align:middle}.post-content>ol,.post-content>ul{padding-left:2rem}.post-content>ol li ol,.post-content>ol li ul,.post-content>ul li ol,.post-content>ul li ul{padding-left:2rem;margin-top:0.3rem}.post-content>ol li{padding-left:0.25em}.post-content dl>dd{margin-left:1rem}.post-tag{display:inline-block;min-width:2rem;text-align:center;background:var(--tag-bg);border-radius:0.3rem;padding:0 0.4rem;color:inherit;line-height:1.3rem}.post-tag:not(:last-child){margin-right:0.2rem}.post-tag:hover{border-bottom:none;text-decoration:none;color:#d2603a}.btn-lang{border:1px solid !important;padding:1px 3px;border-radius:3px;color:var(--link-color)}.btn-lang:focus{box-shadow:none}.loaded{display:block !important}.d-flex.loaded{display:flex !important}.unloaded{display:none !important}.visible{visibility:visible !important}.hidden{visibility:hidden !important}.flex-grow-1{-ms-flex-positive:1 !important;flex-grow:1 !important}.btn-box-shadow{box-shadow:0 0 8px 0 var(--btn-box-shadow) !important}.no-text-decoration{text-decoration:none}.tooltip-inner{font-size:0.7rem;max-width:220px;text-align:left}.disabled{color:#cec4c4;pointer-events:auto;cursor:not-allowed}.hide-border-bottom{border-bottom:none !important}.input-focus{box-shadow:none;border-color:var(--input-focus-border-color) !important;background:center !important;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out}figure .mfp-title{text-align:center;padding-right:0;margin-top:0.5rem}.mermaid{text-align:center}#sidebar{padding-left:0;padding-right:0;position:fixed;top:0;left:0;height:100%;overflow-y:auto;width:260px;z-index:99;background:var(--sidebar-bg);-ms-overflow-style:none;scrollbar-width:none}#sidebar::-webkit-scrollbar{display:none}#sidebar a:hover{text-decoration:none;color:var(--sidebar-active-color) !important}#sidebar #avatar>a{display:block;width:6rem;height:6rem;border-radius:50%;border:2px solid rgba(222,222,222,0.7);overflow:hidden;transform:translateZ(0);-webkit-transition:border-color 0.35s ease-in-out;-moz-transition:border-color 0.35s ease-in-out;transition:border-color 0.35s ease-in-out}#sidebar #avatar>a:hover{border-color:white}#sidebar #avatar img{width:100%;height:100%;-webkit-transition:transform 0.5s;-moz-transition:transform 0.5s;transition:transform 0.5s}#sidebar #avatar img:hover{-ms-transform:scale(1.2);-moz-transform:scale(1.2);-webkit-transform:scale(1.2);transform:scale(1.2)}#sidebar .site-title a{font-weight:900;font-size:1.5rem;letter-spacing:0.5px;color:#868585}#sidebar .site-subtitle{font-size:95%;color:var(--sidebar-muted-color);line-height:1.2rem;word-spacing:1px;margin:0.5rem 1.5rem 0.5rem 1.5rem;min-height:3rem;user-select:none}#sidebar .nav-link{border-radius:0;font-size:0.95rem;font-weight:600;letter-spacing:1px;display:table-cell;vertical-align:middle}#sidebar .nav-item{text-align:center;display:table;height:3rem}#sidebar .nav-item.active .nav-link{color:var(--sidebar-active-color)}#sidebar ul{height:15rem;margin-bottom:2rem;padding-left:0}#sidebar ul li{width:100%}#sidebar ul li:last-child a{position:relative;left:1px;width:100%}#sidebar ul li:last-child::after{display:table;visibility:hidden;content:"";position:relative;right:1px;width:2px;height:1.6rem;border-radius:1px;background-color:var(--nav-cursor-color);pointer-events:none}#sidebar ul>li.active:nth-child(1)~li:last-child::after,#sidebar ul>li.nav-item:nth-child(1):hover~li:last-child::after{top:-11.3rem;visibility:visible}#sidebar ul>li.active:nth-child(2)~li:last-child::after,#sidebar ul>li.nav-item:nth-child(2):hover~li:last-child::after{top:-8.3rem;visibility:visible}#sidebar ul>li.active:nth-child(3)~li:last-child::after,#sidebar ul>li.nav-item:nth-child(3):hover~li:last-child::after{top:-5.3rem;visibility:visible}#sidebar ul>li.active:nth-child(4)~li:last-child::after,#sidebar ul>li.nav-item:nth-child(4):hover~li:last-child::after{top:-2.3rem;visibility:visible}#sidebar ul>li.active:nth-child(5):last-child::after,#sidebar ul>li.nav-item:nth-child(5):last-child:hover::after{top:.7rem;visibility:visible}#sidebar .sidebar-bottom{margin-bottom:2.1rem;margin-left:auto;margin-right:auto;padding-left:1rem;padding-right:1rem}#sidebar .sidebar-bottom .mode-toggle,#sidebar .sidebar-bottom a{width:2.4rem;text-align:center}#sidebar .sidebar-bottom i{font-size:1.2rem;line-height:1.75rem}#sidebar .sidebar-bottom .mode-toggle{padding:0;border:0;margin-bottom:1px;background-color:transparent}#sidebar .sidebar-bottom .mode-toggle:hover>i{color:var(--sidebar-active-color)}#sidebar .sidebar-bottom .icon-border{background-color:var(--sidebar-muted-color);content:"";width:3px;height:3px;border-radius:50%}@media (hover: hover){#sidebar ul>li:last-child::after{-webkit-transition:top 0.5s ease;-moz-transition:top 0.5s ease;-o-transition:top 0.5s ease;transition:top 0.5s ease}}.profile-wrapper{margin-top:2rem;width:100%}#search-result-wrapper{display:none;height:100%;overflow:auto}#search-result-wrapper .post-content{margin-top:2rem}#topbar-wrapper{height:3rem;position:fixed;top:0;left:260px;right:0;transition:top 0.2s ease-in-out;z-index:50;border-bottom:1px solid rgba(0,0,0,0.07);background-color:var(--topbar-wrapper-bg)}[data-topbar-visible=false] #topbar-wrapper{top:-3rem}#topbar i{color:#999}#topbar #breadcrumb{font-size:1rem;color:gray;padding-left:0.5rem}#topbar #breadcrumb span:not(:last-child)::after{content:"›";padding:0 0.3rem}#sidebar-trigger,#search-trigger{display:none}#search-wrapper{display:flex;width:85%;border-radius:1rem;border:1px solid var(--search-wrapper-border-color);background:var(--search-wrapper-bg);padding:0 0.5rem}#search-wrapper i{z-index:2;font-size:0.9rem;color:var(--search-icon-color)}#search-cancel{color:var(--link-color);margin-left:1rem;display:none}#search-input{background:center;border:0;border-radius:0;padding:0.18rem 0.3rem;color:var(--text-color);height:auto}#search-input:focus{box-shadow:none;background:center}#search-input.form-control:focus::-webkit-input-placeholder{opacity:0.6}#search-input.form-control:focus::-moz-placeholder{opacity:0.6}#search-input.form-control:focus:-ms-input-placeholder{opacity:0.6}#search-input.form-control:focus::placeholder{opacity:0.6}#search-hints{padding:0 1rem}#search-hints h4{margin-bottom:1.5rem}#search-hints .post-tag{display:inline-block;line-height:1rem;font-size:1rem;background:var(--search-tag-bg);border:none;padding:0.5rem;margin:0 1.25rem 1rem 0}#search-hints .post-tag::before{content:"#";color:var(--text-muted-color);padding-right:0.2rem}#search-results{padding-bottom:6rem}#search-results a{font-size:1.4rem;line-height:2.5rem}#search-results>div{width:100%}#search-results>div:not(:last-child){margin-bottom:1rem}#search-results>div i{color:#818182;margin-right:0.15rem;font-size:80%}#search-results>div>p{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}#topbar-title{display:none;font-size:1.1rem;font-weight:600;font-family:sans-serif;color:var(--topbar-text-color);text-align:center;width:70%;overflow:hidden;text-overflow:ellipsis;word-break:keep-all;white-space:nowrap}#core-wrapper{min-height:calc(100vh - 3rem - 5rem - 35rem) !important}#mask{display:none;position:fixed;top:0;right:0;bottom:0;left:0;height:100%;width:100%;z-index:1}[sidebar-display] #mask{display:block !important}#main-wrapper{background-color:var(--main-wrapper-bg);position:relative;min-height:100vh;padding-bottom:5rem;padding-left:0;padding-right:0}#main .row:first-child>div:nth-child(1),#main .row:first-child>div:nth-child(2){margin-top:3rem}#main .row:first-child>div:first-child{min-height:calc(100vh - 3rem - 5rem - 35rem)}#main div.row:first-of-type:last-of-type{margin-bottom:4rem}#topbar-wrapper.row,#main>.row,#search-result-wrapper>.row{margin-left:0;margin-right:0}#back-to-top{display:none;z-index:1;cursor:pointer;position:fixed;background:var(--button-bg);color:var(--btn-backtotop-color);padding:0;width:2.7em;height:2.7em;border-radius:50%;border:1px solid var(--btn-backtotop-border-color);transition:transform 0.2s ease-out;-webkit-transition:transform 0.2s ease-out}#back-to-top i{line-height:2.7em;position:relative;bottom:2px}#back-to-top:hover{transform:translate3d(0, -5px, 0);-webkit-transform:translate3d(0, -5px, 0)}@media all and (max-width: 576px){footer{height:6rem}footer>div.d-flex{width:100%;padding:1.5rem 0;margin-bottom:0.3rem;flex-wrap:wrap;-ms-flex-pack:distribute !important;justify-content:space-around !important}footer .footer-left,footer .footer-right{text-align:center}#main>div.row:first-child>div:first-child{min-height:calc(100vh - 3rem - 6rem)}#core-wrapper{min-height:calc(100vh - 3rem - 6rem - 35rem) !important}#core-wrapper h1{margin-top:2.2rem;font-size:1.75rem}#core-wrapper .post-content>blockquote[class^=prompt-]{margin-left:-1.25rem;margin-right:-1.25rem;border-radius:0}#avatar>a{width:5rem;height:5rem}.site-subtitle{margin-left:1.8rem;margin-right:1.8rem}#main-wrapper{padding-bottom:6rem}}@media all and (max-width: 849px){html,body{overflow-x:hidden}[sidebar-display] #sidebar{transform:translateX(0)}[sidebar-display] #topbar-wrapper,[sidebar-display] #main-wrapper{transform:translateX(260px)}#sidebar{-webkit-transition:transform 0.4s ease;transition:transform 0.4s ease;transform:translateX(-260px);-webkit-transform:translateX(-260px)}#sidebar .cursor{-webkit-transition:none;-moz-transition:none;transition:none}#main-wrapper{-webkit-transition:transform 0.4s ease;transition:transform 0.4s ease;padding-top:3rem}#search-result-wrapper{width:100%}#breadcrumb,#search-wrapper{display:none}#topbar-wrapper{-webkit-transition:transform 0.4s ease, top 0.2s ease;transition:transform 0.4s ease, top 0.2s ease;left:0}#main>div.row:first-child>div:nth-child(1),#main>div.row:first-child>div:nth-child(2){margin-top:0}#topbar-title,#sidebar-trigger,#search-trigger{display:block}#search-wrapper.loaded~a{margin-right:1rem}#search-input{margin-left:0;width:95%}#search-result-wrapper .post-content{letter-spacing:0}#tags{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}h1.dynamic-title{display:none}h1.dynamic-title~.post-content{margin-top:3rem}}@media all and (max-width: 849px) and (orientation: portrait){[data-topbar-visible=false] #topbar-wrapper{top:0}}@media all and (min-width: 577px) and (max-width: 1199px){footer>.d-flex>div{width:312px}}@media all and (min-width: 850px){html{overflow-y:scroll}#main-wrapper{margin-left:260px}.profile-wrapper{margin-top:3rem}#search-wrapper{width:22%;min-width:150px}#search-hints{display:none}#search-result-wrapper{margin-top:3rem}div.post-content .table-wrapper>table{min-width:70%}#back-to-top{bottom:5.5rem;right:1.2rem}#topbar-title{text-align:left}footer>div.d-flex{width:92%}}@media all and (min-width: 992px) and (max-width: 1199px){#main .col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 96%;flex:0 0 96%;max-width:96%}}@media all and (min-width: 850px) and (max-width: 1199px){#sidebar{width:210px}#sidebar .site-subtitle{margin-left:1rem;margin-right:1rem}#sidebar .sidebar-bottom a,#sidebar .sidebar-bottom span{width:2rem}#sidebar .sidebar-bottom .icon-border{left:-3px}#topbar-wrapper{left:210px}#search-results>div{max-width:700px}.site-title{font-size:1.3rem;margin-left:0 !important}.site-subtitle{margin-left:1rem;margin-right:1rem;font-size:90%}#main-wrapper{margin-left:210px}#breadcrumb{width:65%;overflow:hidden;text-overflow:ellipsis;word-break:keep-all;white-space:nowrap}}@media all and (max-width: 1199px){#panel-wrapper{display:none}#topbar{padding:0}#main>div.row{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}}@media all and (min-width: 1200px){#main>div.row>div.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%;padding-left:3%}#topbar{padding:0;max-width:1070px}#panel-wrapper{max-width:300px}#back-to-top{bottom:6.5rem;right:4.3rem}#search-input{-webkit-transition:all 0.3s ease-in-out;transition:all 0.3s ease-in-out}#search-results>div{width:46%}#search-results>div:nth-child(odd){margin-right:1.5rem}#search-results>div:nth-child(even){margin-left:1.5rem}#search-results>div:last-child:nth-child(odd){position:relative;right:24.3%}.post-content{font-size:1.03rem}footer>div.d-felx{width:85%}}@media all and (min-width: 1400px){#main>div.row{padding-left:calc((100% - 1150px) / 2)}#main>div.row>div.col-xl-8{max-width:850px}#search-result-wrapper{padding-right:2rem}#search-result-wrapper>div{max-width:1110px}}@media all and (min-width: 1400px) and (max-width: 1650px){#topbar{padding-right:2rem}}@media all and (min-width: 1650px){#breadcrumb{padding-left:0}#main>div.row>div.col-xl-8{padding-left:0}#main>div.row>div.col-xl-8>div:first-child{padding-left:0.55rem !important;padding-right:1.9rem !important}#main-wrapper{margin-left:350px}#panel-wrapper{margin-left:calc((100% - 1150px) / 10)}#topbar-wrapper{left:350px}#topbar{max-width:1150px}#search-wrapper{margin-right:3%}#sidebar{width:350px}#sidebar .profile-wrapper{margin-top:4rem;margin-bottom:1rem}#sidebar .profile-wrapper.text-center{text-align:left !important}#sidebar .profile-wrapper .site-subtitle,#sidebar .profile-wrapper .site-title,#sidebar .profile-wrapper #avatar{margin-left:4.5rem}#sidebar .profile-wrapper #avatar>a{width:6.2rem;height:6.2rem}#sidebar .profile-wrapper #avatar>a.mx-auto{margin-left:0 !important}#sidebar .profile-wrapper .site-title a{font-size:1.7rem;letter-spacing:1px}#sidebar .profile-wrapper .site-subtitle{word-spacing:0;margin-top:0.3rem}#sidebar ul{padding-left:2.5rem}#sidebar ul>li:last-child>a{position:static}#sidebar ul .nav-item{text-align:left}#sidebar ul .nav-item .nav-link>span{letter-spacing:2px}#sidebar ul .nav-item .nav-link>i.unloaded{display:inline-block !important}#sidebar .sidebar-bottom{padding-left:3.5rem;width:100%}#sidebar .sidebar-bottom.justify-content-center{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}#sidebar .sidebar-bottom>span,#sidebar .sidebar-bottom>button.mode-toggle,#sidebar .sidebar-bottom>a{margin-left:.15rem;margin-right:.15rem;height:2rem;margin-bottom:0.5rem}#sidebar .sidebar-bottom i{background-color:var(--sidebar-btn-bg);font-size:1rem;width:2rem;height:2rem;border-radius:50%;position:relative}#sidebar .sidebar-bottom i::before{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%)}#sidebar .sidebar-bottom .icon-border{top:0.9rem}footer>div.d-flex{width:92%;max-width:1140px}#search-result-wrapper>div{max-width:1150px}}@media all and (min-width: 1700px){#topbar-wrapper{padding-right:calc(100% - 350px - (1920px - 350px))}#topbar{max-width:calc(1150px + 20px)}#main>div.row{padding-left:calc((100% - 1150px - 2%) / 2)}#panel-wrapper{margin-left:3%}footer{padding-left:0;padding-right:calc(100% - 350px - 1180px)}#back-to-top{right:calc(100% - 1920px + 15rem)}}@media (min-width: 1920px){#main>div.row{padding-left:190px}#search-result-wrapper{padding-right:calc(100% - 350px - 1180px)}#panel-wrapper{margin-left:41px}}.pagination{color:var(--btn-patinator-text-color);font-family:'Lato', sans-serif}.pagination a:hover{text-decoration:none}.pagination .page-item .page-link{color:inherit;width:2.5rem;height:2.5rem;padding:0;display:-webkit-box;-webkit-box-pack:center;-webkit-box-align:center;border-radius:50%;border:1px solid var(--btn-paginator-border-color);background-color:var(--button-bg)}.pagination .page-item .page-link:hover{background-color:var(--btn-paginator-hover-color)}.pagination .page-item.active .page-link{background-color:var(--btn-paginator-hover-color);color:var(--btn-text-color)}.pagination .page-item.disabled{cursor:not-allowed}.pagination .page-item.disabled .page-link{color:rgba(108,117,125,0.57);border-color:var(--btn-paginator-border-color);background-color:var(--button-bg)}.pagination .page-item:first-child .page-link,.pagination .page-item:last-child .page-link{border-radius:50%}#post-list{margin-top:1rem;padding-right:0.5rem}#post-list .post-preview{padding-top:1.5rem;padding-bottom:1rem;border-bottom:1px solid var(--main-border-color)}#post-list .post-preview h1{font-size:1.4rem;margin:0}#post-list .post-preview .post-meta i{font-size:0.73rem}#post-list .post-preview .post-meta i:not(:first-child){margin-left:1.2rem}#post-list .post-preview .post-content{margin-top:0.6rem;margin-bottom:0.6rem;color:var(--post-list-text-color)}#post-list .post-preview .post-content>p{margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}#post-list .post-preview .pin>i{transform:rotate(45deg);padding-left:3px;color:var(--pin-color)}#post-list .post-preview .pin>span{display:none}@media all and (max-width: 830px){.pagination{justify-content:space-evenly}.pagination .page-item:not(:first-child):not(:last-child){display:none}}@media all and (min-width: 831px){#post-list{margin-top:1.5rem}#post-list .post-preview .post-meta .pin{background:var(--pin-bg);border-radius:5px;line-height:1.4rem;height:1.3rem;margin-top:3px;padding-left:1px;padding-right:6px}#post-list .post-preview .post-meta .pin>span{display:inline}.pagination{font-size:0.85rem}.pagination .page-item:not(:last-child){margin-right:0.7rem}.pagination .page-item .page-link{width:2rem;height:2rem}.pagination .page-index{display:none}}@media all and (max-width: 1200px){#post-list{padding-right:0}}#related-posts .card h3,h1+.post-meta em a,h1+.post-meta em,footer a{color:var(--text-color)}h1+.post-meta span+span::before{content:"\2022";padding-left:.25rem;padding-right:.25rem}img.preview-img{margin-top:3.75rem;margin-bottom:0;border-radius:6px}img.preview-img.bg[data-loaded=true]{background:var(--preview-img-bg)}.post-tail-wrapper{margin-top:6rem;border-bottom:1px double var(--main-border-color);font-size:0.85rem}.post-tags{line-height:2rem}.post-navigation{padding-top:3rem;padding-bottom:4rem}.post-navigation .btn{width:50%;position:relative;border-color:var(--btn-border-color);color:var(--link-color)}.post-navigation .btn:hover{background:#2a408e;color:#fff;border-color:#2a408e}.post-navigation .btn.disabled{width:50%;position:relative;border-color:var(--btn-border-color);pointer-events:auto;cursor:not-allowed;background:none;color:gray}.post-navigation .btn.disabled:hover{border-color:none}.post-navigation .btn.btn-outline-primary.disabled:focus{box-shadow:none}.post-navigation .btn::before{color:var(--text-muted-color);font-size:0.65rem;text-transform:uppercase;content:attr(prompt)}.post-navigation .btn:first-child{border-top-right-radius:0;border-bottom-right-radius:0;left:0.5px}.post-navigation .btn:last-child{border-top-left-radius:0;border-bottom-left-radius:0;right:0.5px}.post-navigation p{font-size:1.1rem;line-height:1.5rem;margin-top:0.3rem;white-space:normal}@keyframes fade-up{from{opacity:0;position:relative;top:2rem}to{opacity:1;position:relative;top:0}}#toc-wrapper{border-left:1px solid rgba(158,158,158,0.17);position:-webkit-sticky;position:sticky;top:4rem;transition:top 0.2s ease-in-out;animation:fade-up 0.8s}#toc li a{font-size:0.8rem}#toc li a.nav-link:not(.active){color:inherit}nav[data-toggle=toc] .nav .nav>li>a.active{font-weight:600 !important}#related-posts>h3{color:var(--label-color);font-size:1.1rem;font-weight:600}#related-posts .card{border-color:var(--card-border-color);background-color:var(--card-bg);box-shadow:0 0 5px 0 var(--card-box-shadow);-webkit-transition:all 0.3s ease-in-out;-moz-transition:all 0.3s ease-in-out;transition:all 0.3s ease-in-out}#related-posts .card:hover{-webkit-transform:translate3d(0, -3px, 0);transform:translate3d(0, -3px, 0);box-shadow:0 10px 15px -4px rgba(0,0,0,0.15)}#related-posts .timeago{color:var(--relate-post-date)}#related-posts p{font-size:0.9rem;margin-bottom:0.5rem;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}#related-posts a:hover{text-decoration:none}#related-posts ul{list-style-type:none;padding-inline-start:1.5rem}#related-posts ul>li::before{background:#c2c9d4;width:5px;height:5px;border-radius:1px;display:block;content:"";position:relative;top:1rem;right:1rem}#tail-wrapper{min-height:2rem}#tail-wrapper>div:last-of-type{margin-bottom:2rem}#tail-wrapper #disqus_thread{min-height:8.5rem}.post-tail-bottom a{color:inherit}.share-wrapper .share-icons>i:hover,.share-wrapper .share-icons a:hover>i{color:var(--btn-share-hover-color) !important}.share-wrapper{vertical-align:middle;user-select:none}.share-wrapper .share-icons{font-size:1.2rem}.share-wrapper .share-icons a:not(:last-child){margin-right:0.25rem}.share-wrapper .share-icons a:hover{text-decoration:none}.share-wrapper .share-icons>i{position:relative;bottom:1px}.share-wrapper .share-icons .fab.fa-twitter{color:var(--btn-share-color, #1da1f2)}.share-wrapper .share-icons .fab.fa-facebook-square{color:var(--btn-share-color, #425f9c)}.share-wrapper .share-icons .fab.fa-telegram{color:var(--btn-share-color, #279fd9)}.share-wrapper .share-icons .fab.fa-weibo{color:var(--btn-share-color, #e5142b)}.share-wrapper .fas.fa-link{color:var(--btn-share-color, #ababab)}.share-label{color:inherit;font-size:inherit;font-weight:400}.share-label::after{content:":"}.license-wrapper{line-height:1.2rem}.license-wrapper>a{color:var(--text-color)}.license-wrapper span:last-child{font-size:0.85rem}@media all and (max-width: 576px){.preview-img[data-src]{margin-top:2.2rem}.post-tail-bottom{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.post-tail-bottom>div:first-child{width:100%;margin-top:1rem}}@media all and (max-width: 768px){.post-content>p>img{max-width:calc(100% + 1rem)}}@media all and (max-width: 849px){.post-navigation{padding-left:0;padding-right:0;margin-left:-0.5rem;margin-right:-0.5rem}.preview-img[data-src]{max-width:100vw;border-radius:0}}.tag{border-radius:0.7em;padding:6px 8px 7px;margin-right:0.8rem;line-height:3rem;letter-spacing:0;border:1px solid var(--tag-border) !important;box-shadow:0 0 3px 0 var(--tag-shadow)}.tag span{margin-left:0.6em;font-size:0.7em;font-family:'Oswald', sans-serif}#archives ul li:first-child::before,#archives ul li::after{content:"";width:4px;left:75px;display:inline-block;float:left;position:relative;background-color:var(--timeline-color)}#archives{letter-spacing:0.03rem}#archives span.lead{font-size:1.5rem;position:relative;left:8px}#archives span.lead::after{content:"";display:block;position:relative;-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%;width:12px;height:12px;top:-26px;left:63px;border:3px solid;background-color:var(--timeline-year-dot-color);border-color:var(--timeline-node-bg);box-shadow:0 0 2px 0 #c2c6cc;z-index:1}#archives span.lead:not(:first-child){position:relative;left:4px}#archives span.lead:not(:first-child)::after{left:67px}#archives ul li{font-size:1.1rem;line-height:3rem}#archives ul li div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#archives ul li div a{margin-left:2.5rem;position:relative;top:0.1rem}#archives ul li:nth-child(odd){background-color:var(--main-wrapper-bg, #fff);background-image:linear-gradient(to left, #fff, #fbfbfb, #fbfbfb, #fbfbfb, #fff)}#archives ul li::after{height:2.8rem;top:-1.3rem}#archives ul li:first-child::before{height:3.06rem;top:-1.61rem}#archives ul:not(:last-child)>li:last-child::after{height:3.4rem}#archives ul:last-child>li:last-child::after{display:none}#archives .date{white-space:nowrap;display:inline-block}#archives .date.month{width:1.4rem;text-align:center}#archives .date.month~a::before{content:"";display:inline-block;position:relative;-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%;width:8px;height:8px;float:left;top:1.35rem;left:69px;background-color:var(--timeline-node-bg);box-shadow:0 0 3px 0 #c2c6cc;z-index:1}#archives .date.day{font-size:85%;font-family:'Lato', sans-serif;text-align:center;margin-right:-2px;width:1.2rem;position:relative;left:-0.15rem}@media all and (max-width: 576px){#archives{margin-top:-1rem}#archives ul{letter-spacing:0}}.categories i{color:gray}.categories{margin-bottom:2rem}.categories .card-header{padding-right:12px}.categories i{font-size:86%}.categories .list-group-item{border-left:none;border-right:none;padding-left:2rem}.categories .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.category-trigger{width:1.7rem;height:1.7rem;border-radius:50%;text-align:center;color:#6c757d !important}.category-trigger:hover i{color:var(--categories-icon-hover-color)}.category-trigger i{position:relative;height:0.7rem;width:1rem;transition:transform 300ms ease}@media (hover: hover){.category-trigger:hover{background-color:var(--categories-hover-bg)}}.rotate{-ms-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.dash{margin:0 0.5rem 0.6rem 0.5rem;border-bottom:2px dotted var(--dash-color)}#page-category ul>li,#page-tag ul>li{line-height:1.5rem;padding:0.6rem 0}#page-category ul>li::before,#page-tag ul>li::before{background:#999;width:5px;height:5px;border-radius:50%;display:block;content:"";position:relative;top:0.6rem;margin-right:0.5rem}#page-category ul>li>a,#page-tag ul>li>a{font-size:1.1rem}#page-category ul>li>span:last-child,#page-tag ul>li>span:last-child{white-space:nowrap}#page-tag h1>i{font-size:1.2rem}#page-category h1>i{font-size:1.25rem}#page-category a:hover,#page-tag a:hover,#access-lastmod a:hover{margin-bottom:-1px}@media all and (max-width: 576px){#page-category ul>li::before,#page-tag ul>li::before{margin:0 0.5rem}#page-category ul>li>a,#page-tag ul>li>a{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}} + +/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/assets/css/style.css.map b/assets/css/style.css.map new file mode 100644 index 0000000..4bf4bb3 --- /dev/null +++ b/assets/css/style.css.map @@ -0,0 +1,44 @@ +{ + "version": 3, + "file": "style.css", + "sources": [ + "style.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/jekyll-theme-chirpy.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/colors/light-typography.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/colors/dark-typography.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/addon/module.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/addon/variables.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/variables-hook.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/addon/syntax.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/colors/light-syntax.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/colors/dark-syntax.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/addon/commons.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/layout/home.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/layout/post.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/layout/tags.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/layout/archives.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/layout/categories.scss", + "vendor/bundle/ruby/2.7.0/gems/jekyll-theme-chirpy-5.1.0/_sass/layout/category-tag.scss" + ], + "sourcesContent": [ + "/*\n If the number of TAB files has changed, the following variable is required.\n And it must be defined before `@import`.\n*/\n$tab-count: 5; // plus 1 for home tab\n\n@import \"jekyll-theme-chirpy\";\n\n/* append your custom style below */\n", + "/*!\n * The styles for Jekyll theme Chirpy\n *\n * Chirpy v5.1.0 (https://github.com/cotes2020/jekyll-theme-chirpy)\n * © 2019 Cotes Chung\n * MIT Licensed\n */\n\n@import\n \"colors/light-typography\",\n \"colors/dark-typography\",\n\n \"addon/module\",\n \"addon/variables\",\n \"variables-hook\",\n \"addon/syntax\",\n \"addon/commons\",\n\n \"layout/home\",\n \"layout/post\",\n \"layout/tags\",\n \"layout/archives\",\n \"layout/categories\",\n \"layout/category-tag\";\n", + "/*\n * The syntax light mode typography colors\n */\n\n@mixin light-scheme {\n /* Framework color */\n --body-bg: #fafafa;\n --mask-bg: #c1c3c5;\n --main-wrapper-bg: white;\n --main-border-color: #f3f3f3;\n\n /* Common color */\n --text-color: #34343c;\n --text-muted-color: gray;\n --heading-color: black;\n --blockquote-border-color: #eee;\n --blockquote-text-color: #9a9a9a;\n --link-color: #2a408e;\n --link-underline-color: #dee2e6;\n --button-bg: #fff;\n --btn-border-color: #e9ecef;\n --btn-backtotop-color: #686868;\n --btn-backtotop-border-color: #f1f1f1;\n --btn-box-shadow: #eaeaea;\n --checkbox-color: #c5c5c5;\n --checkbox-checked-color: #07a8f7;\n\n /* Sidebar */\n --sidebar-bg: #eeeeee;\n --sidebar-muted-color: #a2a19f;\n --sidebar-active-color: #424242;\n --nav-cursor-color: #757575;\n --sidebar-btn-bg: white;\n\n /* Topbar */\n --topbar-text-color: rgb(78, 78, 78);\n --topbar-wrapper-bg: white;\n --search-wrapper-bg: rgb(245 245 245 / 50%);\n --search-wrapper-border-color: rgb(245 245 245);\n --search-tag-bg: #f8f9fa;\n --search-icon-color: #c2c6cc;\n --input-focus-border-color: var(--btn-border-color);\n\n /* Home page */\n --post-list-text-color: dimgray;\n --btn-patinator-text-color: #555555;\n --btn-paginator-hover-color: var(--sidebar-bg);\n --btn-paginator-border-color: var(--sidebar-bg);\n --btn-text-color: #676666;\n --pin-bg: #f5f5f5;\n --pin-color: #999fa4;\n\n /* Posts */\n --btn-share-hover-color: var(--link-color);\n --card-border-color: #f1f1f1;\n --card-box-shadow: rgba(234, 234, 234, 0.7686274509803922);\n --label-color: #616161;\n --relate-post-date: rgba(30, 55, 70, 0.4);\n --footnote-target-bg: lightcyan;\n --tag-bg: rgba(0, 0, 0, 0.075);\n --tag-border: #dee2e6;\n --tag-shadow: var(--btn-border-color);\n --tag-hover: rgb(222, 226, 230);\n --tb-odd-bg: #fbfcfd;\n --tb-border-color: #eaeaea;\n --dash-color: silver;\n --preview-img-bg: radial-gradient(circle, rgb(255 255 255) 0%, rgb(249 249 249) 100%);\n --kbd-wrap-color: #bdbdbd;\n --kbd-text-color: var(--text-color);\n --kbd-bg-color: white;\n --prompt-text-color: rgb(46 46 46 / 77%);\n --prompt-tip-bg: rgb(123 247 144 / 20%);\n --prompt-tip-icon-color: #03b303;\n --prompt-info-bg: #e1f5fe;\n --prompt-info-icon-color: #0070cb;\n --prompt-warning-bg: rgb(255 243 205);\n --prompt-warning-icon-color: #ef9c03;\n --prompt-danger-bg: rgb(248 215 218 / 56%);\n --prompt-danger-icon-color: #df3c30;\n\n [class^=prompt-] {\n --link-underline-color: rgb(219 216 216);\n }\n\n /* Categories */\n --categories-hover-bg: var(--btn-border-color);\n --categories-icon-hover-color: darkslategray;\n\n /* Archive */\n --timeline-color: rgba(0, 0, 0, 0.075);\n --timeline-node-bg: #c2c6cc;\n --timeline-year-dot-color: #ffffff;\n\n} /* light-scheme */\n", + "/*\n * The main dark mode styles\n */\n\n@mixin dark-scheme {\n /* Framework color */\n --body-bg: var(--main-wrapper-bg);\n --mask-bg: rgb(68, 69, 70);\n --main-wrapper-bg: rgb(27, 27, 30);\n --main-border-color: rgb(44, 45, 45);\n\n /* Common color */\n --text-color: rgb(175, 176, 177);\n --text-muted-color: rgb(107, 116, 124);\n --heading-color: #cccccc;\n --blockquote-border-color: rgb(66, 66, 66);\n --blockquote-text-color: rgb(117, 117, 117);\n --link-color: rgb(138, 180, 248);\n --link-underline-color: rgb(82, 108, 150);\n --button-bg: rgb(39, 40, 43);\n --btn-border-color: rgb(63, 65, 68);\n --btn-backtotop-color: var(--text-color);\n --btn-backtotop-border-color: var(--btn-border-color);\n --btn-box-shadow: var(--main-wrapper-bg);\n --card-header-bg: rgb(51, 50, 50);\n --label-color: rgb(108, 117, 125);\n --checkbox-color: rgb(118 120 121);\n --checkbox-checked-color: var(--link-color);\n\n /* Sidebar */\n --sidebar-bg: radial-gradient(circle, #242424 0%, #1d1f27 100%);\n --sidebar-muted-color: #6d6c6b;\n --sidebar-active-color: rgb(255 255 255 / 80%);\n --nav-cursor-color: rgb(183, 182, 182);\n --sidebar-btn-bg: rgb(117 116 116 / 20%);\n\n /* Topbar */\n --topbar-text-color: var(--text-color);\n --topbar-wrapper-bg: rgb(39, 40, 43);\n --search-wrapper-bg: rgb(34, 34, 39);\n --search-wrapper-border-color: rgb(34, 34, 39);\n --search-icon-color: rgb(100, 102, 105);\n --input-focus-border-color: rgb(112, 114, 115);\n\n /* Home page */\n --post-list-text-color: rgb(175, 176, 177);\n --btn-patinator-text-color: var(--text-color);\n --btn-paginator-hover-color: rgb(64, 65, 66);\n --btn-paginator-border-color: var(--btn-border-color);\n --btn-text-color: var(--text-color);\n --pin-bg: rgb(34 35 37);\n --pin-color: inherit;\n\n /* Posts */\n --toc-highlight: rgb(116, 178, 243);\n --tag-bg: rgb(41, 40, 40);\n --tag-hover: rgb(43, 56, 62);\n --tb-odd-bg: rgba(42, 47, 53, 0.52); /* odd rows of the posts' table */\n --tb-even-bg: rgb(31, 31, 34); /* even rows of the posts' table */\n --tb-border-color: var(--tb-odd-bg);\n --footnote-target-bg: rgb(63, 81, 181);\n --btn-share-color: #6c757d;\n --btn-share-hover-color: #bfc1ca;\n --relate-post-date: var(--text-muted-color);\n --card-bg: rgb(39, 40, 43);\n --card-border-color: rgb(53, 53, 60);\n --card-box-shadow: var(--main-wrapper-bg);\n --preview-img-bg: radial-gradient(circle, rgb(22 22 24) 0%, rgb(32 32 32) 100%);\n --kbd-wrap-color: #6a6a6a;\n --kbd-text-color: #d3d3d3;\n --kbd-bg-color: #242424;\n --prompt-text-color: rgb(216 212 212 / 75%);\n --prompt-tip-bg: rgba(77, 187, 95, 0.2);\n --prompt-tip-icon-color: rgb(5 223 5 / 68%);\n --prompt-info-bg: rgb(7 59 104 / 80%);\n --prompt-info-icon-color: #0075d1;\n --prompt-warning-bg: rgb(90 69 3 / 95%);\n --prompt-warning-icon-color: rgb(255 165 0 / 80%);\n --prompt-danger-bg: rgb(86 28 8 / 80%);\n --prompt-danger-icon-color: #cd0202;\n\n /* tags */\n --tag-border: rgb(59, 79, 88);\n --tag-shadow: rgb(32, 33, 33);\n --search-tag-bg: var(--tag-bg);\n --dash-color: rgb(63, 65, 68);\n\n /* categories */\n --categories-border: rgb(64, 66, 69);\n --categories-hover-bg: rgb(73, 75, 76);\n --categories-icon-hover-color: white;\n\n /* archives */\n --timeline-node-bg: rgb(150, 152, 156);\n --timeline-color: rgb(63, 65, 68);\n --timeline-year-dot-color: var(--timeline-color);\n\n .post img[data-src] {\n filter: brightness(95%);\n }\n\n hr {\n border-color: var(--main-border-color);\n }\n\n /* posts' toc, override BS */\n nav[data-toggle=toc] .nav-link.active,\n nav[data-toggle=toc] .nav-link.active:focus,\n nav[data-toggle=toc] .nav-link.active:hover,\n nav[data-toggle=toc] .nav > li > a:focus,\n nav[data-toggle=toc] .nav > li > a:hover {\n color: var(--toc-highlight) !important;\n border-left-color: var(--toc-highlight) !important;\n }\n\n /* categories */\n .categories.card,\n .list-group-item {\n background-color: var(--card-bg);\n }\n\n .categories {\n .card-header {\n background-color: var(--card-header-bg);\n }\n\n .list-group-item {\n border-left: none;\n border-right: none;\n padding-left: 2rem;\n border-color: var(--categories-border);\n\n &:last-child {\n border-bottom-color: var(--card-bg);\n }\n }\n }\n\n #archives li:nth-child(odd) {\n background-image:\n linear-gradient(\n to left,\n rgb(26, 26, 30),\n rgb(39, 39, 45),\n rgb(39, 39, 45),\n rgb(39, 39, 45),\n rgb(26, 26, 30)\n );\n }\n\n color-scheme: dark;\n\n #disqus_thread {\n color-scheme: none;\n }\n\n} /* dark-scheme */\n", + "/*\n* Mainly scss modules, only imported to `assets/css/main.scss`\n*/\n\n/* ---------- scss placeholder --------- */\n\n%heading {\n color: var(--heading-color);\n font-weight: 400;\n font-family: 'Lato', 'Microsoft Yahei', sans-serif;\n}\n\n%section {\n #core-wrapper & {\n margin-top: 2.5rem;\n margin-bottom: 1.25rem;\n\n &:focus {\n outline: none; /* avoid outline in Safari */\n }\n }\n}\n\n%anchor {\n .anchor {\n font-size: 80%;\n }\n\n @media (hover: hover) {\n .anchor {\n visibility: hidden;\n opacity: 0;\n transition: opacity 0.25s ease-in, visibility 0s ease-in 0.25s;\n }\n\n &:hover {\n .anchor {\n visibility: visible;\n opacity: 1;\n transition: opacity 0.25s ease-in, visibility 0s ease-in 0s;\n }\n }\n }\n}\n\n%tag-hover {\n background: var(--tag-hover);\n transition: background 0.35s ease-in-out;\n}\n\n%table-cell {\n padding: 0.4rem 1rem;\n font-size: 95%;\n white-space: nowrap;\n}\n\n%link-hover {\n color: #d2603a !important;\n border-bottom: 1px solid #d2603a;\n text-decoration: none;\n}\n\n%link-color {\n color: var(--link-color);\n}\n\n%link-underline {\n border-bottom: 1px solid var(--link-underline-color);\n}\n\n%clickable-transition {\n transition: color 0.35s ease-in-out;\n}\n\n%no-cursor {\n user-select: none;\n}\n\n%no-bottom-border {\n border-bottom: none;\n}\n\n%cursor-pointer {\n cursor: pointer;\n}\n\n%normal-font-style {\n font-style: normal;\n}\n\n%img-caption {\n + em {\n display: block;\n text-align: center;\n font-style: normal;\n font-size: 80%;\n padding: 0;\n color: #6d6c6c;\n }\n}\n\n%sidebar-links {\n color: rgba(117, 117, 117, 0.9);\n user-select: none;\n}\n\n/* ---------- scss mixin --------- */\n\n@mixin no-text-decoration {\n text-decoration: none;\n}\n\n@mixin ml-mr($value) {\n margin-left: $value;\n margin-right: $value;\n}\n\n@mixin pl-pr($val) {\n padding-left: $val;\n padding-right: $val;\n}\n\n@mixin input-placeholder {\n opacity: 0.6;\n}\n\n@mixin label($font-size: 1rem, $font-weight: 600, $color: var(--label-color)) {\n color: $color;\n font-size: $font-size;\n font-weight: $font-weight;\n}\n\n@mixin align-center {\n position: relative;\n left: 50%;\n -webkit-transform: translateX(-50%);\n -ms-transform: translateX(-50%);\n transform: translateX(-50%);\n}\n\n@mixin prompt($type, $fw-icon, $icon-weight: 900) {\n &.prompt-#{$type} {\n background-color: var(--prompt-#{$type}-bg);\n\n &::before {\n content: $fw-icon;\n color: var(--prompt-#{$type}-icon-color);\n font-weight: $icon-weight;\n }\n }\n}\n", + "/*\n * The SCSS variables\n */\n\n/* sidebar */\n\n$sidebar-width: 260px !default; /* the basic width */\n$sidebar-width-small: 210px !default; /* screen width: >= 850px, <= 1199px (iPad landscape) */\n$sidebar-width-large: 350px !default; /* screen width: >= 1650px */\n\n/* tabs of sidebar */\n\n$tab-count: 5 !default; /* backward compatible (version <= 4.0.2) */\n$tab-height: 3rem !default;\n$tab-cursor-height: 1.6rem !default;\n\n$cursor-width: 2px !default; /* the cursor width of the selected tab */\n\n/* other framework sizes */\n\n$topbar-height: 3rem !default;\n\n$footer-height: 5rem !default;\n$footer-height-mobile: 6rem !default; /* screen width: <= 576px */\n\n$main-content-max-width: 1150px !default;\n\n$panel-max-width: 300px !default;\n\n$bottom-min-height: 35rem !default;\n\n/* syntax highlight */\n\n$code-font-size: 0.85rem !default;\n", + "/*\n Appending custom SCSS variables will override the default ones in `_sass/addon/variables.scsss`\n*/\n", + "/*\n* The syntax highlight.\n*/\n\n@import \"colors/light-syntax\";\n@import \"colors/dark-syntax\";\n\nhtml {\n @media (prefers-color-scheme: light) {\n &:not([data-mode]),\n [data-mode=light] {\n @include light-syntax;\n }\n\n &[data-mode=dark] {\n @include dark-syntax;\n }\n }\n\n @media (prefers-color-scheme: dark) {\n &:not([data-mode]),\n &[data-mode=dark] {\n @include dark-syntax;\n }\n\n &[data-mode=light] {\n @include light-syntax;\n }\n }\n}\n\n/* -- Codes Snippet -- */\n\n$code-radius: 6px;\n\n%code-snippet-bg {\n background: var(--highlight-bg-color);\n}\n\n%code-snippet-radius {\n border-radius: $code-radius;\n}\n\n%code-snippet-padding {\n padding-left: 1rem;\n padding-right: 1.5rem;\n}\n\n.highlighter-rouge {\n @extend %code-snippet-bg;\n @extend %code-snippet-radius;\n\n color: var(--highlighter-rouge-color);\n margin-top: 0.5rem;\n margin-bottom: 1.2em; /* Override BS Inline-code style */\n}\n\n.highlight {\n @extend %code-snippet-radius;\n @extend %code-snippet-bg;\n\n @at-root figure#{&} {\n @extend %code-snippet-bg;\n }\n\n overflow: auto;\n padding-top: 0.5rem;\n padding-bottom: 1rem;\n\n pre {\n margin-bottom: 0;\n font-size: $code-font-size;\n line-height: 1.4rem;\n word-wrap: normal; /* Fixed Safari overflow-x */\n }\n\n table {\n td pre {\n overflow: visible; /* Fixed iOS safari overflow-x */\n word-break: normal; /* Fixed iOS safari linenos code break */\n }\n }\n\n .lineno {\n padding-right: 0.5rem;\n min-width: 2.2rem;\n text-align: right;\n color: var(--highlight-lineno-color);\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n -o-user-select: none;\n user-select: none;\n }\n\n /* set the dollar sign to non-selectable */\n .gp {\n user-select: none;\n }\n\n} /* .highlight */\n\ncode {\n -webkit-hyphens: none;\n -ms-hyphens: none;\n -moz-hyphens: none;\n hyphens: none;\n\n &.highlighter-rouge {\n font-size: $code-font-size;\n padding: 3px 5px;\n border-radius: 4px;\n background-color: var(--inline-code-bg);\n }\n\n &.filepath {\n background-color: inherit;\n color: var(--filepath-text-color);\n font-weight: 600;\n padding: 0;\n }\n\n a > &.highlighter-rouge {\n padding-bottom: 0; /* show link's underlinke */\n color: inherit;\n }\n\n a:hover > &.highlighter-rouge {\n border-bottom: none;\n }\n\n blockquote & {\n color: inherit;\n }\n\n .highlight > & {\n color: transparent;\n }\n}\n\ntd.rouge-code {\n @extend %code-snippet-padding;\n\n /*\n Prevent some browser extends from\n changing the URL string of code block.\n */\n a {\n color: inherit !important;\n border-bottom: none !important;\n pointer-events: none;\n }\n\n}\n\n/* Hide line numbers for default, console, and terminal code snippets */\ndiv {\n &[class^='highlighter-rouge'],\n &.language-plaintext.highlighter-rouge,\n &.language-console.highlighter-rouge,\n &.language-terminal.highlighter-rouge,\n &.nolineno {\n pre.lineno {\n display: none;\n }\n\n td.rouge-code {\n padding-left: 1.5rem;\n }\n }\n}\n\n.code-header {\n @extend %no-cursor;\n\n $code-header-height: 2.25rem;\n\n border-top-left-radius: $code-radius;\n border-top-right-radius: $code-radius;\n display: flex;\n justify-content: space-between;\n align-items: center;\n height: $code-header-height;\n\n &::before {\n $dot-size: 0.75rem;\n $dot-margin: 0.5rem;\n\n content: \"\";\n display: inline-block;\n margin-left: 1rem;\n width: $dot-size;\n height: $dot-size;\n border-radius: 50%;\n background-color: var(--code-header-muted-color);\n box-shadow:\n ($dot-size + $dot-margin) 0 0 var(--code-header-muted-color),\n ($dot-size + $dot-margin) * 2 0 0 var(--code-header-muted-color);\n }\n\n /* the label block */\n span {\n /* label icon */\n i {\n font-size: 1rem;\n margin-right: 0.4rem;\n color: var(--code-header-icon-color);\n\n &.small {\n font-size: 70%;\n }\n }\n\n @at-root [file] #{&} > i {\n position: relative;\n top: 1px; /* center the file icon */\n }\n\n /* label text */\n &::after {\n content: attr(data-label-text);\n font-size: 0.85rem;\n font-weight: 600;\n color: var(--code-header-text-color);\n }\n }\n\n /* clipboard */\n button {\n @extend %cursor-pointer;\n\n border: 1px solid transparent;\n border-radius: $code-radius;\n height: $code-header-height;\n width: $code-header-height;\n padding: 0;\n background-color: inherit;\n\n i {\n color: var(--code-header-icon-color);\n }\n\n &[timeout] {\n &:hover {\n border-color: var(--clipboard-checked-color);\n }\n\n i {\n color: var(--clipboard-checked-color);\n }\n }\n\n &:not([timeout]):hover {\n background-color: rgba(128, 128, 128, 0.37);\n\n i {\n color: white;\n }\n }\n\n &:focus {\n outline: none;\n }\n\n }\n\n}\n\n@media all and (max-width: 576px) {\n .post-content {\n > div[class^='language-'] {\n @include ml-mr(-1.25rem);\n\n border-radius: 0;\n\n .highlight {\n padding-left: 0.25rem;\n }\n\n .code-header {\n border-radius: 0;\n padding-left: 0.4rem;\n padding-right: 0.5rem;\n }\n }\n }\n}\n", + "/*\n * The syntax light mode code snippet colors.\n */\n\n@mixin light-syntax {\n /* see: */\n .highlight .hll { background-color: #ffffcc; }\n .highlight .c { color: #999988; font-style: italic; } /* Comment */\n .highlight .err { color: #a61717; background-color: #e3d2d2; } /* Error */\n .highlight .k { color: #000000; font-weight: bold; } /* Keyword */\n .highlight .o { color: #000000; font-weight: bold; } /* Operator */\n .highlight .cm { color: #999988; font-style: italic; } /* Comment.Multiline */\n .highlight .cp { color: #999999; font-weight: bold; font-style: italic; } /* Comment.Preproc */\n .highlight .c1 { color: #999988; font-style: italic; } /* Comment.Single */\n .highlight .cs { color: #999999; font-weight: bold; font-style: italic; } /* Comment.Special */\n .highlight .gd { color: #d01040; background-color: #ffdddd; } /* Generic.Deleted */\n .highlight .ge { color: #000000; font-style: italic; } /* Generic.Emph */\n .highlight .gr { color: #aa0000; } /* Generic.Error */\n .highlight .gh { color: #999999; } /* Generic.Heading */\n .highlight .gi { color: #008080; background-color: #ddffdd; } /* Generic.Inserted */\n .highlight .go { color: #888888; } /* Generic.Output */\n .highlight .gp { color: #555555; } /* Generic.Prompt */\n .highlight .gs { font-weight: bold; } /* Generic.Strong */\n .highlight .gu { color: #aaaaaa; } /* Generic.Subheading */\n .highlight .gt { color: #aa0000; } /* Generic.Traceback */\n .highlight .kc { color: #000000; font-weight: bold; } /* Keyword.Constant */\n .highlight .kd { color: #000000; font-weight: bold; } /* Keyword.Declaration */\n .highlight .kn { color: #000000; font-weight: bold; } /* Keyword.Namespace */\n .highlight .kp { color: #000000; font-weight: bold; } /* Keyword.Pseudo */\n .highlight .kr { color: #000000; font-weight: bold; } /* Keyword.Reserved */\n .highlight .kt { color: #445588; font-weight: bold; } /* Keyword.Type */\n .highlight .m { color: #009999; } /* Literal.Number */\n .highlight .s { color: #d01040; } /* Literal.String */\n .highlight .na { color: #008080; } /* Name.Attribute */\n .highlight .nb { color: #0086b3; } /* Name.Builtin */\n .highlight .nc { color: #445588; font-weight: bold; } /* Name.Class */\n .highlight .no { color: #008080; } /* Name.Constant */\n .highlight .nd { color: #3c5d5d; font-weight: bold; } /* Name.Decorator */\n .highlight .ni { color: #800080; } /* Name.Entity */\n .highlight .ne { color: #990000; font-weight: bold; } /* Name.Exception */\n .highlight .nf { color: #990000; font-weight: bold; } /* Name.Function */\n .highlight .nl { color: #990000; font-weight: bold; } /* Name.Label */\n .highlight .nn { color: #555555; } /* Name.Namespace */\n .highlight .nt { color: #000080; } /* Name.Tag */\n .highlight .nv { color: #008080; } /* Name.Variable */\n .highlight .ow { color: #000000; font-weight: bold; } /* Operator.Word */\n .highlight .w { color: #bbbbbb; } /* Text.Whitespace */\n .highlight .mf { color: #009999; } /* Literal.Number.Float */\n .highlight .mh { color: #009999; } /* Literal.Number.Hex */\n .highlight .mi { color: #009999; } /* Literal.Number.Integer */\n .highlight .mo { color: #009999; } /* Literal.Number.Oct */\n .highlight .sb { color: #d01040; } /* Literal.String.Backtick */\n .highlight .sc { color: #d01040; } /* Literal.String.Char */\n .highlight .sd { color: #d01040; } /* Literal.String.Doc */\n .highlight .s2 { color: #d01040; } /* Literal.String.Double */\n .highlight .se { color: #d01040; } /* Literal.String.Escape */\n .highlight .sh { color: #d01040; } /* Literal.String.Heredoc */\n .highlight .si { color: #d01040; } /* Literal.String.Interpol */\n .highlight .sx { color: #d01040; } /* Literal.String.Other */\n .highlight .sr { color: #009926; } /* Literal.String.Regex */\n .highlight .s1 { color: #d01040; } /* Literal.String.Single */\n .highlight .ss { color: #990073; } /* Literal.String.Symbol */\n .highlight .bp { color: #999999; } /* Name.Builtin.Pseudo */\n .highlight .vc { color: #008080; } /* Name.Variable.Class */\n .highlight .vg { color: #008080; } /* Name.Variable.Global */\n .highlight .vi { color: #008080; } /* Name.Variable.Instance */\n .highlight .il { color: #009999; } /* Literal.Number.Integer.Long */\n\n /* --- custom light colors --- */\n --highlight-bg-color: #f7f7f7;\n --highlighter-rouge-color: #2f2f2f;\n --highlight-lineno-color: #c2c6cc;\n --inline-code-bg: #f3f3f3;\n --code-header-text-color: #a3a3b1;\n --code-header-muted-color: #ebebeb;\n --code-header-icon-color: #d1d1d1;\n --clipboard-checked-color: #43c743;\n\n [class^=prompt-] {\n --inline-code-bg: #fbfafa;\n --highlighter-rouge-color: rgb(82 82 82);\n }\n\n} /* light-syntax */\n", + "/*\n * The syntax dark mode styles.\n */\n\n@mixin dark-syntax {\n /* syntax highlight colors from https://raw.githubusercontent.com/jwarby/pygments-css/master/monokai.css */\n .highlight pre { background-color: var(--highlight-bg-color); }\n .highlight .hll { background-color: var(--highlight-bg-color); }\n .highlight .c { color: #75715e; } /* Comment */\n .highlight .err { color: #960050; background-color: #1e0010; } /* Error */\n .highlight .k { color: #66d9ef; } /* Keyword */\n .highlight .l { color: #ae81ff; } /* Literal */\n .highlight .n { color: #f8f8f2; } /* Name */\n .highlight .o { color: #f92672; } /* Operator */\n .highlight .p { color: #f8f8f2; } /* Punctuation */\n .highlight .cm { color: #75715e; } /* Comment.Multiline */\n .highlight .cp { color: #75715e; } /* Comment.Preproc */\n .highlight .c1 { color: #75715e; } /* Comment.Single */\n .highlight .cs { color: #75715e; } /* Comment.Special */\n .highlight .ge { color: inherit; font-style: italic; } /* Generic.Emph */\n .highlight .gs { font-weight: bold; } /* Generic.Strong */\n .highlight .kc { color: #66d9ef; } /* Keyword.Constant */\n .highlight .kd { color: #66d9ef; } /* Keyword.Declaration */\n .highlight .kn { color: #f92672; } /* Keyword.Namespace */\n .highlight .kp { color: #66d9ef; } /* Keyword.Pseudo */\n .highlight .kr { color: #66d9ef; } /* Keyword.Reserved */\n .highlight .kt { color: #66d9ef; } /* Keyword.Type */\n .highlight .ld { color: #e6db74; } /* Literal.Date */\n .highlight .m { color: #ae81ff; } /* Literal.Number */\n .highlight .s { color: #e6db74; } /* Literal.String */\n .highlight .na { color: #a6e22e; } /* Name.Attribute */\n .highlight .nb { color: #f8f8f2; } /* Name.Builtin */\n .highlight .nc { color: #a6e22e; } /* Name.Class */\n .highlight .no { color: #66d9ef; } /* Name.Constant */\n .highlight .nd { color: #a6e22e; } /* Name.Decorator */\n .highlight .ni { color: #f8f8f2; } /* Name.Entity */\n .highlight .ne { color: #a6e22e; } /* Name.Exception */\n .highlight .nf { color: #a6e22e; } /* Name.Function */\n .highlight .nl { color: #f8f8f2; } /* Name.Label */\n .highlight .nn { color: #f8f8f2; } /* Name.Namespace */\n .highlight .nx { color: #a6e22e; } /* Name.Other */\n .highlight .py { color: #f8f8f2; } /* Name.Property */\n .highlight .nt { color: #f92672; } /* Name.Tag */\n .highlight .nv { color: #f8f8f2; } /* Name.Variable */\n .highlight .ow { color: #f92672; } /* Operator.Word */\n .highlight .w { color: #f8f8f2; } /* Text.Whitespace */\n .highlight .mf { color: #ae81ff; } /* Literal.Number.Float */\n .highlight .mh { color: #ae81ff; } /* Literal.Number.Hex */\n .highlight .mi { color: #ae81ff; } /* Literal.Number.Integer */\n .highlight .mo { color: #ae81ff; } /* Literal.Number.Oct */\n .highlight .sb { color: #e6db74; } /* Literal.String.Backtick */\n .highlight .sc { color: #e6db74; } /* Literal.String.Char */\n .highlight .sd { color: #e6db74; } /* Literal.String.Doc */\n .highlight .s2 { color: #e6db74; } /* Literal.String.Double */\n .highlight .se { color: #ae81ff; } /* Literal.String.Escape */\n .highlight .sh { color: #e6db74; } /* Literal.String.Heredoc */\n .highlight .si { color: #e6db74; } /* Literal.String.Interpol */\n .highlight .sx { color: #e6db74; } /* Literal.String.Other */\n .highlight .sr { color: #e6db74; } /* Literal.String.Regex */\n .highlight .s1 { color: #e6db74; } /* Literal.String.Single */\n .highlight .ss { color: #e6db74; } /* Literal.String.Symbol */\n .highlight .bp { color: #f8f8f2; } /* Name.Builtin.Pseudo */\n .highlight .vc { color: #f8f8f2; } /* Name.Variable.Class */\n .highlight .vg { color: #f8f8f2; } /* Name.Variable.Global */\n .highlight .vi { color: #f8f8f2; } /* Name.Variable.Instance */\n .highlight .il { color: #ae81ff; } /* Literal.Number.Integer.Long */\n .highlight .gu { color: #75715e; } /* Generic.Subheading & Diff Unified/Comment? */\n .highlight .gd { color: #f92672; background-color: #561c08; } /* Generic.Deleted & Diff Deleted */\n .highlight .gi { color: #a6e22e; background-color: #0b5858; } /* Generic.Inserted & Diff Inserted */\n\n /* ----- custom styles ------ */\n\n --highlight-bg-color: #252525;\n --highlighter-rouge-color: #de6b18;\n --highlight-lineno-color: #6c6c6d;\n --inline-code-bg: #272822;\n --code-header-text-color: #6a6a6a;\n --code-header-muted-color: rgb(60 60 60);\n --code-header-icon-color: rgb(86 86 86);\n --clipboard-checked-color: #2bcc2b;\n --filepath-text-color: #bdbdbd;\n\n .highlight {\n .gp { color: #818c96; }\n }\n\n pre { color: #bfbfbf; } /* override Bootstrap */\n}\n", + "/*\n The common styles\n*/\n\nhtml {\n @media (prefers-color-scheme: light) {\n &:not([data-mode]),\n [data-mode=light] {\n @include light-scheme;\n }\n\n &[data-mode=dark] {\n @include dark-scheme;\n }\n }\n\n @media (prefers-color-scheme: dark) {\n &:not([data-mode]),\n &[data-mode=dark] {\n @include dark-scheme;\n }\n\n &[data-mode=light] {\n @include light-scheme;\n }\n }\n\n font-size: 16px;\n}\n\nbody {\n line-height: 1.75rem;\n background: var(--body-bg);\n color: var(--text-color);\n -webkit-font-smoothing: antialiased;\n font-family: 'Source Sans Pro', 'Microsoft Yahei', sans-serif;\n}\n\n/* --- Typography --- */\n\nh1 {\n @extend %heading;\n\n font-size: 1.9rem;\n}\n\nh2 {\n @extend %heading;\n @extend %section;\n @extend %anchor;\n\n font-size: 1.5rem;\n}\n\nh3 {\n @extend %heading;\n @extend %section;\n @extend %anchor;\n\n font-size: 1.2rem;\n}\n\nh4 {\n @extend %heading;\n @extend %section;\n @extend %anchor;\n\n font-size: 1.15rem;\n}\n\nh5 {\n @extend %heading;\n @extend %section;\n @extend %anchor;\n\n font-size: 1.1rem;\n}\n\nol,\nul {\n ol,\n ul {\n margin-bottom: 1rem;\n }\n}\n\na {\n @extend %link-color;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\nblockquote {\n border-left: 5px solid var(--blockquote-border-color);\n padding-left: 1rem;\n color: var(--blockquote-text-color);\n\n &[class^=\"prompt-\"] {\n display: flex;\n border-left: 0;\n border-radius: 6px;\n padding: 0.75rem 1.2rem;\n color: var(--prompt-text-color);\n\n &::before {\n margin-right: 1rem;\n font-family: \"Font Awesome 5 Free\";\n text-align: center;\n width: 1.25rem;\n }\n\n p:last-child {\n margin-bottom: 0rem;\n }\n }\n\n @include prompt(\"tip\", \"\\f0eb\", 400);\n\n @include prompt(\"info\", \"\\f06a\");\n\n @include prompt(\"warning\", \"\\f06a\");\n\n @include prompt(\"danger\", \"\\f071\");\n}\n\nkbd {\n font-family: inherit;\n display: inline-block;\n vertical-align: middle;\n line-height: 1.3rem;\n min-width: 1.75rem;\n text-align: center;\n margin: 0 0.3rem;\n padding-top: 0.1rem;\n color: var(--kbd-text-color);\n background-color: var(--kbd-bg-color);\n border-radius: 0.25rem;\n border: solid 1px var(--kbd-wrap-color);\n box-shadow: inset 0 -2px 0 var(--kbd-wrap-color);\n}\n\nfooter {\n position: absolute;\n bottom: 0;\n padding: 0 1rem;\n height: $footer-height;\n font-size: 0.8rem;\n\n > div.d-flex {\n line-height: 1.2rem;\n width: 95%;\n max-width: 1045px;\n border-top: 1px solid var(--main-border-color);\n margin-bottom: 1rem;\n\n > div {\n width: 350px;\n }\n }\n\n a {\n @extend %text-color;\n\n &:link {\n @include no-text-decoration;\n }\n\n &:hover {\n @extend %link-hover;\n\n @include no-text-decoration;\n }\n }\n\n .footer-right {\n text-align: right;\n }\n}\n\ni { /* fontawesome icons */\n &.far,\n &.fas {\n @extend %no-cursor;\n }\n}\n\n@keyframes fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\nimg[data-src] {\n margin: 0.5rem 0;\n\n &[data-loaded=true] {\n animation: fade-in linear 0.5s;\n }\n\n &.left {\n float: left;\n margin: 0.75rem 1rem 1rem 0;\n }\n\n &.right {\n float: right;\n margin: 0.75rem 0 1rem 1rem;\n }\n\n &.shadow {\n filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.08));\n box-shadow: none !important; /* cover the Bootstrap 4.6.1 styles */\n }\n\n @extend %img-caption;\n}\n\n/* --- Panels --- */\n\n.access {\n top: 2rem;\n transition: top 0.2s ease-in-out;\n margin-right: 1.5rem;\n margin-top: 3rem;\n margin-bottom: 4rem;\n\n &:only-child {\n position: -webkit-sticky; /* Safari */\n position: sticky;\n }\n\n > div {\n padding-left: 1rem;\n border-left: 1px solid var(--main-border-color);\n\n &:not(:last-child) {\n margin-bottom: 4rem;\n }\n }\n\n .post-content {\n font-size: 0.9rem;\n }\n\n}\n\n#panel-wrapper {\n /* the headings */\n .panel-heading {\n @include label(inherit);\n }\n\n .post-tag {\n display: inline-block;\n line-height: 1rem;\n font-size: 0.85rem;\n background: none;\n border: 1px solid var(--btn-border-color);\n border-radius: 0.8rem;\n padding: 0.3rem 0.5rem;\n margin: 0 0.35rem 0.5rem 0;\n\n &:hover {\n background-color: #2a408e;\n border-color: #2a408e;\n color: #fff;\n transition: none;\n }\n }\n\n [data-topbar-visible=true] & > div {\n top: 6rem;\n }\n}\n\n#access-lastmod {\n li {\n height: 1.8rem;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n list-style: none;\n }\n\n a {\n &:hover {\n @extend %link-hover;\n }\n\n @extend %no-bottom-border;\n\n color: inherit;\n }\n\n}\n\n.footnotes > ol {\n padding-left: 2rem;\n margin-top: 0.5rem;\n\n > li {\n &:not(:last-child) {\n margin-bottom: 0.3rem;\n }\n\n > p {\n margin-left: 0.25em;\n margin-top: 0;\n margin-bottom: 0;\n }\n\n /* [scroll-focus] added by `smooth-scroll.js` */\n &:target:not([scroll-focus]),\n &[scroll-focus=true] > p {\n background-color: var(--footnote-target-bg);\n width: fit-content;\n -webkit-transition: background-color 1.5s ease-in-out; /* Safari prior 6.1 */\n transition: background-color 1.5s ease-in-out;\n }\n }\n}\n\n.footnote {\n @at-root a#{&} {\n @include ml-mr(1px);\n @include pl-pr(2px);\n\n border-bottom-style: none !important;\n -webkit-transition: background-color 1.5s ease-in-out; /* Safari prior 6.1 */\n transition: background-color 1.5s ease-in-out;\n }\n\n /* [scroll-focus] added by `smooth-scroll.js` */\n @at-root sup:target:not([scroll-focus]),\n sup[scroll-focus=true] > a#{&} {\n background-color: var(--footnote-target-bg);\n }\n}\n\n.reversefootnote {\n @at-root a#{&} {\n font-size: 0.6rem;\n line-height: 1;\n position: relative;\n bottom: 0.25em;\n margin-left: 0.25em;\n border-bottom-style: none !important;\n }\n}\n\n/* --- Begin of Markdown table style --- */\n\n/* it will be created by Liquid */\n.table-wrapper {\n overflow-x: auto;\n margin-bottom: 1.5rem;\n\n > table {\n min-width: 100%;\n overflow-x: auto;\n border-spacing: 0;\n\n thead {\n border-bottom: solid 2px rgba(210, 215, 217, 0.75);\n\n th {\n @extend %table-cell;\n }\n }\n\n tbody {\n tr {\n border-bottom: 1px solid var(--tb-border-color);\n\n &:nth-child(2n) {\n background-color: var(--tb-even-bg);\n }\n\n &:nth-child(2n + 1) {\n background-color: var(--tb-odd-bg);\n }\n\n td {\n @extend %table-cell;\n }\n }\n } /* tbody */\n }/* table */\n}\n\n/* --- post --- */\n\n.post {\n h1 {\n margin-top: 3rem;\n margin-bottom: 1.5rem;\n }\n\n a {\n &.img-link {\n @extend %no-cursor;\n }\n\n /* created by `_includes/img-extra.html` */\n &.popup {\n cursor: zoom-in;\n\n > img[data-src]:not(.normal):not(.left):not(.right) {\n @include align-center;\n }\n }\n\n &:hover {\n code {\n @extend %link-hover;\n }\n }\n } /* a */\n\n}\n\n.pageviews .fa-spinner {\n font-size: 80%;\n}\n\n.post-meta {\n font-size: 0.85rem;\n word-spacing: 1px;\n\n a {\n &:not(:last-child) {\n margin-right: 2px;\n }\n\n &:hover {\n @extend %link-hover;\n }\n }\n\n em {\n @extend %normal-font-style;\n }\n}\n\n.post-content {\n font-size: 1.08rem;\n line-height: 1.8;\n margin-top: 2rem;\n overflow-wrap: break-word;\n word-wrap: break-word;\n\n a {\n &:not(.img-link) {\n @extend %link-underline;\n\n &:hover {\n @extend %link-hover;\n }\n }\n\n &.img-link {\n @extend %img-caption;\n }\n\n }\n\n ul {\n /* attribute 'hide-bullet' was added by liquid */\n .task-list-item[hide-bullet] {\n list-style-type: none;\n\n > i { /* checkbox icon */\n margin: 0 0.4rem 0.2rem -1.4rem;\n vertical-align: middle;\n color: var(--checkbox-color);\n\n &.checked {\n color: var(--checkbox-checked-color);\n }\n }\n\n }\n\n input[type=checkbox] {\n margin: 0 0.5rem 0.2rem -1.3rem;\n vertical-align: middle;\n }\n\n } /* ul */\n\n > ol,\n > ul {\n padding-left: 2rem;\n\n li {\n ol,\n ul { /* sub list */\n padding-left: 2rem;\n margin-top: 0.3rem;\n }\n }\n\n }\n\n > ol {\n li {\n padding-left: 0.25em;\n }\n }\n\n dl > dd {\n margin-left: 1rem;\n }\n\n} /* .post-content */\n\n.tag:hover {\n @extend %tag-hover;\n}\n\n.post-tag {\n display: inline-block;\n min-width: 2rem;\n text-align: center;\n background: var(--tag-bg);\n border-radius: 0.3rem;\n padding: 0 0.4rem;\n color: inherit;\n line-height: 1.3rem;\n\n &:not(:last-child) {\n margin-right: 0.2rem;\n }\n\n &:hover {\n @extend %tag-hover;\n\n border-bottom: none;\n text-decoration: none;\n color: #d2603a;\n }\n}\n\n/* --- buttons --- */\n.btn-lang {\n border: 1px solid !important;\n padding: 1px 3px;\n border-radius: 3px;\n color: var(--link-color);\n\n &:focus {\n box-shadow: none;\n }\n}\n\n/* --- Effects classes --- */\n\n.loaded {\n display: block !important;\n\n @at-root .d-flex#{&} {\n display: flex !important;\n }\n}\n\n.unloaded {\n display: none !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.hidden {\n visibility: hidden !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.btn-box-shadow {\n box-shadow: 0 0 8px 0 var(--btn-box-shadow) !important;\n}\n\n.no-text-decoration {\n @include no-text-decoration;\n}\n\n.tooltip-inner { /* Overrided BS4 Tooltip */\n font-size: 0.7rem;\n max-width: 220px;\n text-align: left;\n}\n\n.disabled {\n color: rgb(206, 196, 196);\n pointer-events: auto;\n cursor: not-allowed;\n}\n\n.hide-border-bottom {\n border-bottom: none !important;\n}\n\n.input-focus {\n box-shadow: none;\n border-color: var(--input-focus-border-color) !important;\n background: center !important;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;\n}\n\n/* --- Overriding --- */\n\n/* magnific-popup */\nfigure .mfp-title {\n text-align: center;\n padding-right: 0;\n margin-top: 0.5rem;\n}\n\n/* mermaid */\n.mermaid {\n text-align: center;\n}\n\n/* --- sidebar layout --- */\n\n$sidebar-display: \"sidebar-display\";\n\n#sidebar {\n @include pl-pr(0);\n\n position: fixed;\n top: 0;\n left: 0;\n height: 100%;\n overflow-y: auto;\n width: $sidebar-width;\n z-index: 99;\n background: var(--sidebar-bg);\n\n /* Hide scrollbar for Chrome, Safari and Opera */\n &::-webkit-scrollbar {\n display: none;\n }\n\n /* Hide scrollbar for IE, Edge and Firefox */\n -ms-overflow-style: none; /* IE and Edge */\n scrollbar-width: none; /* Firefox */\n\n a {\n @extend %sidebar-links;\n\n &:hover {\n @include no-text-decoration;\n\n color: var(--sidebar-active-color) !important;\n }\n }\n\n #avatar {\n > a {\n display: block;\n width: 6rem;\n height: 6rem;\n border-radius: 50%;\n border: 2px solid rgba(222, 222, 222, 0.7);\n overflow: hidden;\n transform: translateZ(0); /* fixed the zoom in Safari */\n -webkit-transition: border-color 0.35s ease-in-out;\n -moz-transition: border-color 0.35s ease-in-out;\n transition: border-color 0.35s ease-in-out;\n\n &:hover {\n border-color: white;\n }\n }\n\n img {\n width: 100%;\n height: 100%;\n -webkit-transition: transform 0.5s;\n -moz-transition: transform 0.5s;\n transition: transform 0.5s;\n\n &:hover {\n -ms-transform: scale(1.2);\n -moz-transform: scale(1.2);\n -webkit-transform: scale(1.2);\n transform: scale(1.2);\n }\n }\n } /* #avatar */\n\n .site-title {\n a {\n @extend %clickable-transition;\n\n font-weight: 900;\n font-size: 1.5rem;\n letter-spacing: 0.5px;\n color: rgba(134, 133, 133, 99%);\n }\n }\n\n .site-subtitle {\n font-size: 95%;\n color: var(--sidebar-muted-color);\n line-height: 1.2rem;\n word-spacing: 1px;\n margin: 0.5rem 1.5rem 0.5rem 1.5rem;\n min-height: 3rem; /* avoid vertical shifting in multi-line words */\n user-select: none;\n }\n\n .nav-link {\n border-radius: 0;\n font-size: 0.95rem;\n font-weight: 600;\n letter-spacing: 1px;\n display: table-cell;\n vertical-align: middle;\n }\n\n .nav-item {\n text-align: center;\n display: table;\n height: $tab-height;\n\n &.active {\n .nav-link {\n color: var(--sidebar-active-color);\n }\n }\n\n &:not(.active) > a {\n @extend %clickable-transition;\n }\n }\n\n ul {\n height: $tab-height * $tab-count;\n margin-bottom: 2rem;\n padding-left: 0;\n\n li {\n width: 100%;\n\n &:last-child {\n a {\n position: relative;\n left: $cursor-width / 2;\n width: 100%;\n }\n\n &::after { /* the cursor */\n display: table;\n visibility: hidden;\n content: \"\";\n position: relative;\n right: 1px;\n width: $cursor-width;\n height: $tab-cursor-height;\n border-radius: 1px;\n background-color: var(--nav-cursor-color);\n pointer-events: none;\n }\n }\n } /* li */\n\n @mixin fix-cursor($top) {\n top: $top;\n visibility: visible;\n }\n\n @for $i from 1 through $tab-count {\n $offset: $tab-count - $i;\n $top: -$offset * $tab-height + ($tab-height - $tab-cursor-height) / 2;\n\n @if $i < $tab-count {\n > li.active:nth-child(#{$i}),\n > li.nav-item:nth-child(#{$i}):hover {\n ~ li:last-child::after {\n @include fix-cursor($top);\n }\n }\n } @else {\n > li.active:nth-child(#{$i}):last-child::after,\n > li.nav-item:nth-child(#{$i}):last-child:hover::after {\n @include fix-cursor($top);\n }\n }\n\n } /* @for */\n\n } /* ul */\n\n .sidebar-bottom {\n margin-bottom: 2.1rem;\n\n @include ml-mr(auto);\n @include pl-pr(1rem);\n\n %icon {\n width: 2.4rem;\n text-align: center;\n }\n\n a {\n @extend %icon;\n @extend %clickable-transition;\n }\n\n i {\n font-size: 1.2rem;\n line-height: 1.75rem;\n }\n\n .mode-toggle {\n padding: 0;\n border: 0;\n margin-bottom: 1px;\n background-color: transparent;\n\n @extend %icon;\n @extend %sidebar-links;\n\n > i {\n @extend %clickable-transition;\n }\n\n &:hover > i {\n color: var(--sidebar-active-color);\n }\n }\n\n .icon-border {\n @extend %no-cursor;\n\n background-color: var(--sidebar-muted-color);\n content: \"\";\n width: 3px;\n height: 3px;\n border-radius: 50%;\n }\n\n } /* .sidebar-bottom */\n\n} /* #sidebar */\n\n@media (hover: hover) {\n #sidebar ul > li:last-child::after {\n -webkit-transition: top 0.5s ease;\n -moz-transition: top 0.5s ease;\n -o-transition: top 0.5s ease;\n transition: top 0.5s ease;\n }\n}\n\n.profile-wrapper {\n margin-top: 2rem;\n width: 100%;\n}\n\n#search-result-wrapper {\n display: none;\n height: 100%;\n overflow: auto;\n\n .post-content {\n margin-top: 2rem;\n }\n}\n\n/* --- top-bar --- */\n\n#topbar-wrapper {\n height: $topbar-height;\n position: fixed;\n top: 0;\n left: $sidebar-width; /* same as sidebar width */\n right: 0;\n transition: top 0.2s ease-in-out;\n z-index: 50;\n border-bottom: 1px solid rgba(0, 0, 0, 0.07);\n background-color: var(--topbar-wrapper-bg);\n\n [data-topbar-visible=false] & {\n top: -$topbar-height; /* same as topbar height. */\n }\n}\n\n#topbar {\n i { /* icons */\n color: #999;\n }\n\n #breadcrumb {\n font-size: 1rem;\n color: gray;\n padding-left: 0.5rem;\n\n a:hover {\n @extend %link-hover;\n }\n\n span {\n &:not(:last-child) {\n &::after {\n content: \"›\";\n padding: 0 0.3rem;\n }\n }\n }\n }\n} /* #topbar */\n\n#sidebar-trigger,\n#search-trigger {\n display: none;\n}\n\n#search-wrapper {\n display: flex;\n width: 85%;\n border-radius: 1rem;\n border: 1px solid var(--search-wrapper-border-color);\n background: var(--search-wrapper-bg);\n padding: 0 0.5rem;\n\n i {\n z-index: 2;\n font-size: 0.9rem;\n color: var(--search-icon-color);\n }\n}\n\n#search-cancel { /* 'Cancel' link */\n color: var(--link-color);\n margin-left: 1rem;\n display: none;\n\n @extend %cursor-pointer;\n}\n\n#search-input {\n background: center;\n border: 0;\n border-radius: 0;\n padding: 0.18rem 0.3rem;\n color: var(--text-color);\n height: auto;\n\n &:focus {\n box-shadow: none;\n background: center;\n\n &.form-control {\n &::-webkit-input-placeholder { @include input-placeholder; }\n &::-moz-placeholder { @include input-placeholder; }\n &:-ms-input-placeholder { @include input-placeholder; }\n &::placeholder { @include input-placeholder; }\n }\n }\n}\n\n#search-hints {\n padding: 0 1rem;\n\n h4 {\n margin-bottom: 1.5rem;\n }\n\n .post-tag {\n display: inline-block;\n line-height: 1rem;\n font-size: 1rem;\n background: var(--search-tag-bg);\n border: none;\n padding: 0.5rem;\n margin: 0 1.25rem 1rem 0;\n\n &::before {\n content: \"#\";\n color: var(--text-muted-color);\n padding-right: 0.2rem;\n }\n\n @extend %link-color;\n }\n}\n\n#search-results {\n padding-bottom: 6rem;\n\n a {\n &:hover {\n @extend %link-hover;\n }\n\n @extend %link-color;\n @extend %no-bottom-border;\n @extend %heading;\n\n font-size: 1.4rem;\n line-height: 2.5rem;\n }\n\n > div {\n width: 100%;\n\n &:not(:last-child) {\n margin-bottom: 1rem;\n }\n\n i { /* icons */\n color: #818182;\n margin-right: 0.15rem;\n font-size: 80%;\n }\n\n > p {\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n }\n }\n} /* #search-results */\n\n#topbar-title {\n display: none;\n font-size: 1.1rem;\n font-weight: 600;\n font-family: sans-serif;\n color: var(--topbar-text-color);\n text-align: center;\n width: 70%;\n overflow: hidden;\n text-overflow: ellipsis;\n word-break: keep-all;\n white-space: nowrap;\n}\n\n#core-wrapper {\n min-height: calc(100vh - #{$topbar-height} - #{$footer-height} - #{$bottom-min-height}) !important;\n\n .categories,\n #tags,\n #archives {\n a:not(:hover) {\n @extend %no-bottom-border;\n }\n }\n}\n\n#mask {\n display: none;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n height: 100%;\n width: 100%;\n z-index: 1;\n\n @at-root [#{$sidebar-display}] & {\n display: block !important;\n }\n}\n\n/* --- main wrapper --- */\n\n#main-wrapper {\n background-color: var(--main-wrapper-bg);\n position: relative;\n min-height: 100vh;\n padding-bottom: $footer-height;\n\n @include pl-pr(0);\n}\n\n#main {\n .row:first-child {\n > div {\n &:nth-child(1),\n &:nth-child(2) {\n margin-top: $topbar-height; /* same as the height of topbar */\n }\n\n &:first-child {\n /* 3rem for topbar, 6rem for footer */\n min-height: calc(100vh - #{$topbar-height} - #{$footer-height} - #{$bottom-min-height});\n }\n }\n }\n\n div.row:first-of-type:last-of-type { /* alone */\n margin-bottom: 4rem;\n }\n}\n\n#topbar-wrapper.row,\n#main > .row,\n#search-result-wrapper > .row {\n @include ml-mr(0);\n}\n\n/* --- button back-to-top --- */\n\n#back-to-top {\n $size: 2.7em;\n\n display: none;\n z-index: 1;\n cursor: pointer;\n position: fixed;\n background: var(--button-bg);\n color: var(--btn-backtotop-color);\n padding: 0;\n width: $size;\n height: $size;\n border-radius: 50%;\n border: 1px solid var(--btn-backtotop-border-color);\n transition: transform 0.2s ease-out;\n -webkit-transition: transform 0.2s ease-out;\n\n i {\n line-height: $size;\n position: relative;\n bottom: 2px;\n }\n}\n\n#back-to-top:hover {\n transform: translate3d(0, -5px, 0);\n -webkit-transform: translate3d(0, -5px, 0);\n}\n\n/*\n Responsive Design:\n\n {sidebar, content, panel} >= 1120px screen width\n {sidebar, content} >= 850px screen width\n {content} <= 849px screen width\n\n*/\n\n@media all and (max-width: 576px) {\n\n $footer-height: $footer-height-mobile; /* overwrite */\n\n footer {\n height: $footer-height;\n\n > div.d-flex {\n width: 100%;\n padding: 1.5rem 0;\n margin-bottom: 0.3rem;\n flex-wrap: wrap;\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n\n .footer-left,\n .footer-right {\n text-align: center;\n }\n }\n\n #main > div.row:first-child > div:first-child {\n min-height: calc(100vh - #{$topbar-height} - #{$footer-height});\n }\n\n #core-wrapper {\n min-height: calc(100vh - #{$topbar-height} - #{$footer-height} - #{$bottom-min-height}) !important;\n\n h1 {\n margin-top: 2.2rem;\n font-size: 1.75rem;\n }\n\n .post-content {\n > blockquote[class^=prompt-] {\n @include ml-mr(-1.25rem);\n border-radius: 0;\n }\n }\n\n }\n\n #avatar > a {\n width: 5rem;\n height: 5rem;\n }\n\n .site-subtitle {\n @include ml-mr(1.8rem);\n }\n\n #main-wrapper {\n padding-bottom: $footer-height;\n }\n\n}\n\n/* hide sidebar and panel */\n@media all and (max-width: 849px) {\n @mixin slide($append: null) {\n $basic: transform 0.4s ease;\n @if $append {\n -webkit-transition: $basic, $append;\n transition: $basic, $append;\n } @else {\n -webkit-transition: $basic;\n transition: $basic;\n }\n }\n\n html,\n body {\n overflow-x: hidden;\n }\n\n [#{$sidebar-display}] {\n #sidebar {\n transform: translateX(0);\n }\n\n #topbar-wrapper,\n #main-wrapper {\n transform: translateX(#{$sidebar-width});\n }\n }\n\n #sidebar {\n @include slide;\n\n transform: translateX(-#{$sidebar-width}); /* hide */\n -webkit-transform: translateX(-#{$sidebar-width});\n\n .cursor {\n -webkit-transition: none;\n -moz-transition: none;\n transition: none;\n }\n }\n\n #main-wrapper {\n @include slide;\n\n padding-top: $topbar-height;\n }\n\n #search-result-wrapper {\n width: 100%;\n }\n\n #breadcrumb,\n #search-wrapper {\n display: none;\n }\n\n #topbar-wrapper {\n @include slide(top 0.2s ease);\n\n left: 0;\n }\n\n #main > div.row:first-child > div:nth-child(1),\n #main > div.row:first-child > div:nth-child(2) {\n margin-top: 0;\n }\n\n #topbar-title,\n #sidebar-trigger,\n #search-trigger {\n display: block;\n }\n\n #search-wrapper {\n &.loaded ~ a {\n margin-right: 1rem;\n }\n }\n\n #search-input {\n margin-left: 0;\n width: 95%;\n }\n\n #search-result-wrapper .post-content {\n letter-spacing: 0;\n }\n\n #tags {\n -webkit-box-pack: center !important;\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n\n h1.dynamic-title {\n display: none;\n\n ~ .post-content {\n margin-top: 3rem;\n }\n }\n\n} /* max-width: 849px */\n\n@media all and (max-width: 849px) and (orientation: portrait) {\n [data-topbar-visible=false] #topbar-wrapper {\n top: 0;\n }\n}\n\n/* Phone & Pad */\n@media all and (min-width: 577px) and (max-width: 1199px) {\n footer > .d-flex > div {\n width: 312px;\n }\n}\n\n/* Sidebar is visible */\n@media all and (min-width: 850px) {\n /* Solved jumping scrollbar */\n html {\n overflow-y: scroll;\n }\n\n #main-wrapper {\n margin-left: $sidebar-width;\n }\n\n .profile-wrapper {\n margin-top: 3rem;\n }\n\n #search-wrapper {\n width: 22%;\n min-width: 150px;\n }\n\n #search-hints {\n display: none;\n }\n\n #search-result-wrapper {\n margin-top: 3rem;\n }\n\n div.post-content .table-wrapper > table {\n min-width: 70%;\n }\n\n /* button 'back-to-Top' position */\n #back-to-top {\n bottom: 5.5rem;\n right: 1.2rem;\n }\n\n #topbar-title {\n text-align: left;\n }\n\n footer > div.d-flex {\n width: 92%;\n }\n\n}\n\n/* Pad horizontal */\n@media all and (min-width: 992px) and (max-width: 1199px) {\n #main .col-lg-11 {\n -webkit-box-flex: 0;\n -ms-flex: 0 0 96%;\n flex: 0 0 96%;\n max-width: 96%;\n }\n}\n\n/* Compact icons in sidebar & panel hidden */\n@media all and (min-width: 850px) and (max-width: 1199px) {\n #sidebar {\n width: $sidebar-width-small;\n\n .site-subtitle {\n margin-left: 1rem;\n margin-right: 1rem;\n }\n\n .sidebar-bottom {\n a,\n span {\n width: 2rem;\n }\n\n .icon-border {\n left: -3px;\n }\n }\n }\n\n #topbar-wrapper {\n left: 210px;\n }\n\n #search-results > div {\n max-width: 700px;\n }\n\n .site-title {\n font-size: 1.3rem;\n margin-left: 0 !important;\n }\n\n .site-subtitle {\n @include ml-mr(1rem);\n\n font-size: 90%;\n }\n\n #main-wrapper {\n margin-left: 210px;\n }\n\n #breadcrumb {\n width: 65%;\n overflow: hidden;\n text-overflow: ellipsis;\n word-break: keep-all;\n white-space: nowrap;\n }\n\n}\n\n/* panel hidden */\n@media all and (max-width: 1199px) {\n #panel-wrapper {\n display: none;\n }\n\n #topbar {\n padding: 0;\n }\n\n #main > div.row {\n -webkit-box-pack: center !important;\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n}\n\n/* --- desktop mode, both sidebar and panel are visible --- */\n\n@media all and (min-width: 1200px) {\n #main > div.row > div.col-xl-8 {\n -webkit-box-flex: 0;\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n padding-left: 3%;\n }\n\n #topbar {\n padding: 0;\n max-width: 1070px;\n }\n\n #panel-wrapper {\n max-width: $panel-max-width;\n }\n\n #back-to-top {\n bottom: 6.5rem;\n right: 4.3rem;\n }\n\n #search-input {\n -webkit-transition: all 0.3s ease-in-out;\n transition: all 0.3s ease-in-out;\n }\n\n #search-results > div {\n width: 46%;\n\n &:nth-child(odd) {\n margin-right: 1.5rem;\n }\n\n &:nth-child(even) {\n margin-left: 1.5rem;\n }\n\n &:last-child:nth-child(odd) {\n position: relative;\n right: 24.3%;\n }\n }\n\n .post-content {\n font-size: 1.03rem;\n }\n\n footer > div.d-felx {\n width: 85%;\n }\n\n}\n\n@media all and (min-width: 1400px) {\n #main > div.row {\n padding-left: calc((100% - #{$main-content-max-width}) / 2);\n\n > div.col-xl-8 {\n max-width: 850px;\n }\n }\n\n #search-result-wrapper {\n padding-right: 2rem;\n\n > div {\n max-width: 1110px;\n }\n }\n\n}\n\n@media all and (min-width: 1400px) and (max-width: 1650px) {\n #topbar {\n padding-right: 2rem;\n }\n}\n\n@media all and (min-width: 1650px) {\n #breadcrumb {\n padding-left: 0;\n }\n\n #main > div.row > div.col-xl-8 {\n padding-left: 0;\n\n > div:first-child {\n padding-left: 0.55rem !important;\n padding-right: 1.9rem !important;\n }\n }\n\n #main-wrapper {\n margin-left: $sidebar-width-large;\n }\n\n #panel-wrapper {\n margin-left: calc((100% - #{$main-content-max-width}) / 10);\n }\n\n #topbar-wrapper {\n left: $sidebar-width-large;\n }\n\n #topbar {\n max-width: #{$main-content-max-width};\n }\n\n #search-wrapper {\n margin-right: 3%;\n }\n\n #sidebar {\n width: $sidebar-width-large;\n\n .profile-wrapper {\n margin-top: 4rem;\n margin-bottom: 1rem;\n\n &.text-center {\n text-align: left !important;\n }\n\n %profile-ml {\n margin-left: 4.5rem;\n }\n\n #avatar {\n @extend %profile-ml;\n\n > a {\n width: 6.2rem;\n height: 6.2rem;\n\n &.mx-auto {\n margin-left: 0 !important;\n }\n }\n }\n\n .site-title {\n @extend %profile-ml;\n\n a {\n font-size: 1.7rem;\n letter-spacing: 1px;\n }\n }\n\n .site-subtitle {\n @extend %profile-ml;\n\n word-spacing: 0;\n margin-top: 0.3rem;\n }\n\n } /* .profile-wrapper (min-width: 1650px) */\n\n ul {\n padding-left: 2.5rem;\n\n > li:last-child {\n > a {\n position: static;\n }\n }\n\n .nav-item {\n text-align: left;\n\n .nav-link {\n > span {\n letter-spacing: 2px;\n }\n\n > i {\n &.unloaded {\n display: inline-block !important;\n }\n }\n }\n\n }\n }\n\n .sidebar-bottom {\n padding-left: 3.5rem;\n width: 100%;\n\n $icon-block-size: 2rem;\n\n &.justify-content-center {\n -webkit-box-pack: start !important;\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n\n > span,\n > button.mode-toggle,\n > a {\n @include ml-mr(0.15rem);\n\n height: $icon-block-size;\n margin-bottom: 0.5rem; /* wrap line */\n }\n\n i {\n background-color: var(--sidebar-btn-bg);\n font-size: 1rem;\n width: $icon-block-size;\n height: $icon-block-size;\n border-radius: 50%;\n position: relative;\n\n &::before {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n }\n }\n\n .icon-border {\n top: 0.9rem;\n }\n\n } /* .sidebar-bottom */\n\n } /* #sidebar */\n\n footer > div.d-flex {\n width: 92%;\n max-width: 1140px;\n }\n\n #search-result-wrapper {\n > div {\n max-width: #{$main-content-max-width};\n }\n }\n\n} /* min-width: 1650px */\n\n@media all and (min-width: 1700px) {\n #topbar-wrapper {\n /* 100% - 350px - (1920px - 350px); */\n padding-right: calc(100% - #{$sidebar-width-large} - (1920px - #{$sidebar-width-large}));\n }\n\n #topbar {\n max-width: calc(#{$main-content-max-width} + 20px);\n }\n\n #main > div.row {\n padding-left: calc((100% - #{$main-content-max-width} - 2%) / 2);\n }\n\n #panel-wrapper {\n margin-left: 3%;\n }\n\n footer {\n padding-left: 0;\n padding-right: calc(100% - #{$sidebar-width-large} - 1180px);\n }\n\n #back-to-top {\n right: calc(100% - 1920px + 15rem);\n }\n\n}\n\n@media (min-width: 1920px) {\n #main > div.row {\n padding-left: 190px;\n }\n\n #search-result-wrapper {\n padding-right: calc(100% - #{$sidebar-width-large} - 1180px);\n }\n\n #panel-wrapper {\n margin-left: 41px;\n }\n}\n", + "/*\n Style for Homepage\n*/\n\n.pagination {\n color: var(--btn-patinator-text-color);\n font-family: 'Lato', sans-serif;\n\n a:hover {\n text-decoration: none;\n }\n\n .page-item {\n .page-link {\n color: inherit;\n width: 2.5rem;\n height: 2.5rem;\n padding: 0;\n display: -webkit-box;\n -webkit-box-pack: center;\n -webkit-box-align: center;\n border-radius: 50%;\n border: 1px solid var(--btn-paginator-border-color);\n background-color: var(--button-bg);\n\n &:hover {\n background-color: var(--btn-paginator-hover-color);\n }\n }\n\n &.active {\n .page-link {\n background-color: var(--btn-paginator-hover-color);\n color: var(--btn-text-color);\n }\n }\n\n &.disabled {\n cursor: not-allowed;\n\n .page-link {\n color: rgba(108, 117, 125, 0.57);\n border-color: var(--btn-paginator-border-color);\n background-color: var(--button-bg);\n }\n }\n\n &:first-child .page-link,\n &:last-child .page-link {\n border-radius: 50%;\n }\n } // .page-item\n\n} // .pagination\n\n#post-list {\n margin-top: 1rem;\n padding-right: 0.5rem;\n\n .post-preview {\n padding-top: 1.5rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--main-border-color);\n\n a:hover {\n @extend %link-hover;\n }\n\n h1 {\n font-size: 1.4rem;\n margin: 0;\n }\n\n .post-meta {\n i {\n font-size: 0.73rem;\n\n &:not(:first-child) {\n margin-left: 1.2rem;\n }\n }\n\n em {\n @extend %normal-font-style;\n }\n }\n\n .post-content {\n margin-top: 0.6rem;\n margin-bottom: 0.6rem;\n color: var(--post-list-text-color);\n\n > p {\n /* Make preview shorter on the homepage */\n margin: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n }\n\n .pin {\n > i {\n transform: rotate(45deg);\n padding-left: 3px;\n color: var(--pin-color);\n }\n\n > span {\n display: none;\n }\n }\n\n } // .post-preview\n} // #post-list\n\n/* Hide SideBar and TOC */\n@media all and (max-width: 830px) {\n .pagination {\n justify-content: space-evenly;\n\n .page-item {\n &:not(:first-child):not(:last-child) {\n display: none;\n }\n\n }\n\n }\n}\n\n/* Sidebar is visible */\n@media all and (min-width: 831px) {\n #post-list {\n margin-top: 1.5rem;\n\n .post-preview .post-meta {\n .pin {\n background: var(--pin-bg);\n border-radius: 5px;\n line-height: 1.4rem;\n height: 1.3rem;\n margin-top: 3px;\n padding-left: 1px;\n padding-right: 6px;\n\n > span {\n display: inline;\n }\n }\n }\n }\n\n .pagination {\n font-size: 0.85rem;\n\n .page-item {\n &:not(:last-child) {\n margin-right: 0.7rem;\n }\n\n .page-link {\n width: 2rem;\n height: 2rem;\n }\n\n }\n\n .page-index {\n display: none;\n }\n\n } // .pagination\n\n}\n\n/* Pannel hidden */\n@media all and (max-width: 1200px) {\n #post-list {\n padding-right: 0;\n }\n}\n", + "/*\n Post-specific style\n*/\n\n@mixin btn-sharing-color($light-color, $important: false) {\n @if $important {\n color: var(--btn-share-color, $light-color) !important;\n } @else {\n color: var(--btn-share-color, $light-color);\n }\n}\n\n@mixin btn-post-nav {\n width: 50%;\n position: relative;\n border-color: var(--btn-border-color);\n}\n\n@mixin dot($pl: 0.25rem, $pr: 0.25rem) {\n content: \"\\2022\";\n padding-left: $pl;\n padding-right: $pr;\n}\n\n%text-color {\n color: var(--text-color);\n}\n\nh1 + .post-meta {\n span + span::before {\n @include dot;\n }\n\n em {\n @extend %text-color;\n\n a {\n @extend %text-color;\n }\n }\n}\n\nimg.preview-img {\n margin-top: 3.75rem;\n margin-bottom: 0;\n border-radius: 6px;\n\n &.bg[data-loaded=true] {\n background: var(--preview-img-bg);\n }\n}\n\n.post-tail-wrapper {\n margin-top: 6rem;\n border-bottom: 1px double var(--main-border-color);\n font-size: 0.85rem;\n\n .post-meta a:not(:hover) {\n @extend %link-underline;\n }\n}\n\n.post-tags {\n line-height: 2rem;\n}\n\n.post-navigation {\n padding-top: 3rem;\n padding-bottom: 4rem;\n\n .btn {\n @include btn-post-nav;\n\n color: var(--link-color);\n\n &:hover {\n background: #2a408e;\n color: #fff;\n border-color: #2a408e;\n }\n\n &.disabled {\n @include btn-post-nav;\n\n pointer-events: auto;\n cursor: not-allowed;\n background: none;\n color: gray;\n\n &:hover {\n border-color: none;\n }\n }\n\n &.btn-outline-primary.disabled:focus {\n box-shadow: none;\n }\n\n &::before {\n color: var(--text-muted-color);\n font-size: 0.65rem;\n text-transform: uppercase;\n content: attr(prompt);\n }\n\n &:first-child {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n left: 0.5px;\n }\n\n &:last-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n right: 0.5px;\n }\n }\n\n p {\n font-size: 1.1rem;\n line-height: 1.5rem;\n margin-top: 0.3rem;\n white-space: normal;\n }\n\n} /* .post-navigation */\n\n@keyframes fade-up {\n from {\n opacity: 0;\n position: relative;\n top: 2rem;\n }\n to {\n opacity: 1;\n position: relative;\n top: 0;\n }\n}\n\n#toc-wrapper {\n border-left: 1px solid rgba(158, 158, 158, 0.17);\n position: -webkit-sticky;\n position: sticky;\n top: 4rem;\n transition: top 0.2s ease-in-out;\n animation: fade-up 0.8s;\n}\n\n#toc li a {\n font-size: 0.8rem;\n\n &.nav-link:not(.active) {\n color: inherit;\n }\n\n}\n\nnav[data-toggle=toc] {\n .nav {\n .nav > li > a.active {\n font-weight: 600 !important;\n }\n }\n}\n\n/* --- Related Posts --- */\n\n#related-posts {\n > h3 {\n @include label(1.1rem, 600);\n }\n\n em {\n @extend %normal-font-style;\n }\n\n .card {\n border-color: var(--card-border-color);\n background-color: var(--card-bg);\n box-shadow: 0 0 5px 0 var(--card-box-shadow);\n -webkit-transition: all 0.3s ease-in-out;\n -moz-transition: all 0.3s ease-in-out;\n transition: all 0.3s ease-in-out;\n\n h3 {\n @extend %text-color;\n }\n\n &:hover {\n -webkit-transform: translate3d(0, -3px, 0);\n transform: translate3d(0, -3px, 0);\n box-shadow: 0 10px 15px -4px rgba(0, 0, 0, 0.15);\n }\n }\n\n .timeago {\n color: var(--relate-post-date);\n }\n\n p {\n font-size: 0.9rem;\n margin-bottom: 0.5rem;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n\n a:hover {\n text-decoration: none;\n }\n\n ul {\n list-style-type: none;\n padding-inline-start: 1.5rem;\n\n > li::before {\n background: #c2c9d4;\n width: 5px;\n height: 5px;\n border-radius: 1px;\n display: block;\n content: \"\";\n position: relative;\n top: 1rem;\n right: 1rem;\n }\n }\n}\n\n#tail-wrapper {\n min-height: 2rem;\n\n > div:last-of-type {\n margin-bottom: 2rem;\n }\n\n #disqus_thread {\n min-height: 8.5rem;\n }\n\n}\n\n.post-tail-bottom a {\n color: inherit;\n}\n\n%btn-share-hovor {\n color: var(--btn-share-hover-color) !important;\n}\n\n.share-wrapper {\n vertical-align: middle;\n user-select: none;\n\n .share-icons {\n font-size: 1.2rem;\n\n a {\n &:not(:last-child) {\n margin-right: 0.25rem;\n }\n\n &:hover {\n text-decoration: none;\n\n > i {\n @extend %btn-share-hovor;\n }\n }\n }\n\n > i {\n position: relative;\n bottom: 1px;\n\n @extend %cursor-pointer;\n\n &:hover {\n @extend %btn-share-hovor;\n }\n }\n\n .fab {\n &.fa-twitter {\n @include btn-sharing-color(rgba(29, 161, 242, 1));\n }\n\n &.fa-facebook-square {\n @include btn-sharing-color(rgb(66, 95, 156));\n }\n\n &.fa-telegram {\n @include btn-sharing-color(rgb(39, 159, 217));\n }\n\n &.fa-weibo {\n @include btn-sharing-color(rgb(229, 20, 43));\n }\n }\n\n } /* .share-icons */\n\n .fas.fa-link {\n @include btn-sharing-color(rgb(171, 171, 171));\n }\n\n} /* .share-wrapper */\n\n.share-label {\n @include label(inherit, 400, inherit);\n\n &::after {\n content: \":\";\n }\n}\n\n.license-wrapper {\n line-height: 1.2rem;\n\n > a {\n color: var(--text-color);\n\n &:hover {\n @extend %link-hover;\n }\n }\n\n span:last-child {\n font-size: 0.85rem;\n }\n\n} /* .license-wrapper */\n\n@media all and (max-width: 576px) {\n .preview-img[data-src] {\n margin-top: 2.2rem;\n }\n\n .post-tail-bottom {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n\n > div:first-child {\n width: 100%;\n margin-top: 1rem;\n }\n }\n}\n\n@media all and (max-width: 768px) {\n .post-content > p > img {\n max-width: calc(100% + 1rem);\n }\n}\n\n/* Hide SideBar and TOC */\n@media all and (max-width: 849px) {\n .post-navigation {\n padding-left: 0;\n padding-right: 0;\n margin-left: -0.5rem;\n margin-right: -0.5rem;\n }\n\n .preview-img[data-src] {\n max-width: 100vw;\n border-radius: 0;\n }\n}\n", + "/*\n Styles for Tab Tags\n*/\n\n.tag {\n border-radius: 0.7em;\n padding: 6px 8px 7px;\n margin-right: 0.8rem;\n line-height: 3rem;\n letter-spacing: 0;\n border: 1px solid var(--tag-border) !important;\n box-shadow: 0 0 3px 0 var(--tag-shadow);\n\n span {\n margin-left: 0.6em;\n font-size: 0.7em;\n font-family: 'Oswald', sans-serif;\n }\n}\n", + "/*\n Style for Archives\n*/\n\n%date-timeline {\n content: \"\";\n width: 4px;\n left: 75px;\n display: inline-block;\n float: left;\n position: relative;\n background-color: var(--timeline-color);\n}\n\n#archives {\n letter-spacing: 0.03rem;\n\n span.lead {\n font-size: 1.5rem;\n position: relative;\n left: 8px;\n\n &::after { /* Year dot */\n content: \"\";\n display: block;\n position: relative;\n -webkit-border-radius: 50%;\n -moz-border-radius: 50%;\n border-radius: 50%;\n width: 12px;\n height: 12px;\n top: -26px;\n left: 63px;\n border: 3px solid;\n background-color: var(--timeline-year-dot-color);\n border-color: var(--timeline-node-bg);\n box-shadow: 0 0 2px 0 #c2c6cc;\n z-index: 1;\n }\n\n &:not(:first-child) {\n position: relative;\n left: 4px;\n\n &::after {\n left: 67px;\n }\n }\n\n } // #archives span.lead\n\n ul {\n li {\n font-size: 1.1rem;\n line-height: 3rem;\n\n div {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n\n a {\n /* post title in Archvies */\n margin-left: 2.5rem;\n position: relative;\n top: 0.1rem;\n }\n }\n\n &:nth-child(odd) {\n background-color: var(--main-wrapper-bg, #fff);\n background-image: linear-gradient(to left, #fff, #fbfbfb, #fbfbfb, #fbfbfb, #fff);\n }\n\n &::after {\n @extend %date-timeline;\n\n height: 2.8rem;\n top: -1.3rem;\n }\n\n &:first-child::before {\n @extend %date-timeline;\n\n height: 3.06rem;\n top: -1.61rem;\n }\n }\n\n &:not(:last-child) > li:last-child::after {\n height: 3.4rem;\n }\n\n &:last-child > li:last-child::after {\n display: none;\n }\n } // #archives ul\n\n .date {\n white-space: nowrap;\n display: inline-block;\n\n &.month {\n width: 1.4rem;\n text-align: center;\n\n ~ a::before {\n /* A dot for Month and Day */\n content: \"\";\n display: inline-block;\n position: relative;\n -webkit-border-radius: 50%;\n -moz-border-radius: 50%;\n border-radius: 50%;\n width: 8px;\n height: 8px;\n float: left;\n top: 1.35rem;\n left: 69px;\n background-color: var(--timeline-node-bg);\n box-shadow: 0 0 3px 0 #c2c6cc;\n z-index: 1;\n }\n }\n\n &.day {\n font-size: 85%;\n font-family: 'Lato', sans-serif;\n text-align: center;\n margin-right: -2px;\n width: 1.2rem;\n position: relative;\n left: -0.15rem;\n }\n } // #archives .date\n\n} // #archives\n\n@media all and (max-width: 576px) {\n #archives {\n margin-top: -1rem;\n\n ul {\n letter-spacing: 0;\n }\n }\n}\n", + "/*\n Style for Tab Categories\n*/\n\n%category-icon-color {\n color: gray;\n}\n\n.categories {\n margin-bottom: 2rem;\n\n .card-header {\n padding-right: 12px;\n }\n\n i {\n @extend %category-icon-color;\n\n font-size: 86%; // fontawesome icons\n }\n\n .list-group-item {\n border-left: none;\n border-right: none;\n padding-left: 2rem;\n\n &:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n\n }\n\n} // .categories\n\n.category-trigger {\n width: 1.7rem;\n height: 1.7rem;\n border-radius: 50%;\n text-align: center;\n color: #6c757d !important;\n\n &:hover {\n i {\n color: var(--categories-icon-hover-color);\n }\n }\n\n i {\n position: relative;\n height: 0.7rem;\n width: 1rem;\n transition: transform 300ms ease;\n }\n}\n\n@media (hover: hover) { // only works on desktop\n .category-trigger:hover {\n background-color: var(--categories-hover-bg);\n }\n}\n\n.rotate {\n -ms-transform: rotate(-90deg); /* IE 9 */\n -webkit-transform: rotate(-90deg); /* Safari 3-8 */\n transform: rotate(-90deg);\n}\n", + "/*\n Style for page Category and Tag\n*/\n\n.dash {\n margin: 0 0.5rem 0.6rem 0.5rem;\n border-bottom: 2px dotted var(--dash-color);\n}\n\n#page-category,\n#page-tag {\n ul > li {\n line-height: 1.5rem;\n padding: 0.6rem 0;\n\n &::before { // dot\n background: #999;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n display: block;\n content: \"\";\n position: relative;\n top: 0.6rem;\n margin-right: 0.5rem;\n }\n\n > a { /* post's title */\n @extend %no-bottom-border;\n\n font-size: 1.1rem;\n }\n\n > span:last-child {\n white-space: nowrap;\n } /* post's date */\n }\n}\n\n#page-tag h1 > i { // tag icon\n font-size: 1.2rem;\n}\n\n#page-category h1 > i {\n font-size: 1.25rem;\n}\n\n#page-category,\n#page-tag,\n#access-lastmod {\n a:hover {\n @extend %link-hover;\n\n margin-bottom: -1px; // Avoid jumping\n }\n}\n\n@media all and (max-width: 576px) {\n #page-category,\n #page-tag {\n ul > li {\n &::before {\n margin: 0 0.5rem;\n }\n\n > a {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n }\n }\n}\n" + ], + "names": [], + "mappings": "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" +} \ No newline at end of file diff --git a/assets/img/favicons/android-chrome-192x192.png b/assets/img/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..a949d2f Binary files /dev/null and b/assets/img/favicons/android-chrome-192x192.png differ diff --git a/assets/img/favicons/android-chrome-512x512.png b/assets/img/favicons/android-chrome-512x512.png new file mode 100644 index 0000000..a0cdd95 Binary files /dev/null and b/assets/img/favicons/android-chrome-512x512.png differ diff --git a/assets/img/favicons/apple-touch-icon.png b/assets/img/favicons/apple-touch-icon.png new file mode 100644 index 0000000..648097f Binary files /dev/null and b/assets/img/favicons/apple-touch-icon.png differ diff --git a/assets/img/favicons/browserconfig.xml b/assets/img/favicons/browserconfig.xml new file mode 100644 index 0000000..54217f7 --- /dev/null +++ b/assets/img/favicons/browserconfig.xml @@ -0,0 +1 @@ + #da532c diff --git a/assets/img/favicons/favicon-16x16.png b/assets/img/favicons/favicon-16x16.png new file mode 100644 index 0000000..f44237a Binary files /dev/null and b/assets/img/favicons/favicon-16x16.png differ diff --git a/assets/img/favicons/favicon-32x32.png b/assets/img/favicons/favicon-32x32.png new file mode 100644 index 0000000..d5d021d Binary files /dev/null and b/assets/img/favicons/favicon-32x32.png differ diff --git a/assets/img/favicons/favicon.ico b/assets/img/favicons/favicon.ico new file mode 100644 index 0000000..5611568 Binary files /dev/null and b/assets/img/favicons/favicon.ico differ diff --git a/assets/img/favicons/mstile-150x150.png b/assets/img/favicons/mstile-150x150.png new file mode 100644 index 0000000..c0d045e Binary files /dev/null and b/assets/img/favicons/mstile-150x150.png differ diff --git a/assets/img/favicons/site.webmanifest b/assets/img/favicons/site.webmanifest new file mode 100644 index 0000000..dc225c0 --- /dev/null +++ b/assets/img/favicons/site.webmanifest @@ -0,0 +1 @@ +{ "name": "Ruggy Blog", "short_name": "Ruggy Blog", "description": "Junior Software Engineer, Spring boot", "icons": [ { "src": "/assets/img/favicons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/assets/img/favicons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }], "start_url": "/index.html", "theme_color": "#2a1e6b", "background_color": "#ffffff", "display": "fullscreen" } diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..e6f6ede --- /dev/null +++ b/assets/index.html @@ -0,0 +1,11 @@ + + + + Redirecting… + + + + +

Redirecting…

+ Click here if you are not redirected. + diff --git a/assets/js/data/search.json b/assets/js/data/search.json new file mode 100644 index 0000000..72b921c --- /dev/null +++ b/assets/js/data/search.json @@ -0,0 +1 @@ +[ { "title": "Spring Boot Caching에서 에러 핸들링하는 방법", "url": "/posts/spring-boot-cahce-error-handling/", "categories": "Development", "tags": "Spring Boot, Redis, Caching", "date": "2024-03-31 11:22:00 +0900", "snippet": "본 글은 아래 링크의 글의 내용과 이어집니다. https://choieungi.github.io/posts/spring-redis-cache-serialization-exceptionSpring에서 제공하는 @Cacheable을 이용하면 캐쉬를 AOP 기반으로 쉽게 사용할 수 있습니다. 이전 글에서 다뤘듯, 기본적으로 @Cacheable 을 실행하는 Aspect 메소드에서 예외가 발생하면 전체 메소드 자체가 실패하게 됩니다. 해당 상황에서 에러 핸들링을 통해 특정 상황에서 Exception이 발생하지 않도록 변경하는 방법을 소개합니다.@Cacheable 의 CacheErrorHandler 동작 원리Spring 문서에 따르면 Error Handler는 SimpleCacheErrorHandler를 기본 값으로 사용하고 있으며 기본적으로 에러를 클라이언트에 직접 반환합니다.SimpleCacheErrorHandler 코드는 다음과 같습니다.// package org.springframework.cache.interceptor;public class SimpleCacheErrorHandler implements CacheErrorHandler { @Override public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { throw exception; } @Override public void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value) { throw exception; } @Override public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { throw exception; } @Override public void handleCacheClearError(RuntimeException exception, Cache cache) { throw exception; }}해당 ErrorHandler를 CachingConfigurer 에 대한 구현체 중 errorHandler() 의 리턴값으로 등록하면 됩니다.// package org.springframework.cache.annotation;public interface CachingConfigurer { ... @Nullable default CacheManager cacheManager() { return null; } ... @Nullable default CacheErrorHandler errorHandler() { return null; }}CachingConfigurer 를 빈으로 등록하면 spring-context에서에서 기본 빈으로 등록되는 AbstractCachingConfiguration 로 인해 등록되게 됩니다. 코드는 다음과 같습니다.// package org.springframework.cache.annotation;@Configuration(proxyBeanMethods = false)public abstract class AbstractCachingConfiguration implements ImportAware { @Nullable protected AnnotationAttributes enableCaching; @Nullable protected Supplier&lt;CacheManager&gt; cacheManager; @Nullable protected Supplier&lt;CacheResolver&gt; cacheResolver; @Nullable protected Supplier&lt;KeyGenerator&gt; keyGenerator; @Nullable protected Supplier&lt;CacheErrorHandler&gt; errorHandler; ... @Autowired void setConfigurers(ObjectProvider&lt;CachingConfigurer&gt; configurers) { Supplier&lt;CachingConfigurer&gt; configurer = () -&gt; { List&lt;CachingConfigurer&gt; candidates = configurers.stream().collect(Collectors.toList()); if (CollectionUtils.isEmpty(candidates)) { return null; } if (candidates.size() &gt; 1) { throw new IllegalStateException(candidates.size() + " implementations of " + "CachingConfigurer were found when only 1 was expected. " + "Refactor the configuration such that CachingConfigurer is " + "implemented only once or not at all."); } return candidates.get(0); }; useCachingConfigurer(new CachingConfigurerSupplier(configurer)); }실제로 setConfigurers()에 @Autowired 를 이용해 setter Injection을 통해 의존성 주입을 진행해주게 됩니다.여기서 CachingConfigurer를 추가로 빈으로 등록하지 않는다면, @Cacheable 이 실제로 동작하는 CacheAspectSupport 에서 초기화 할 때 기본값인 SimpleCacheErrorHandler가 등록되게 됩니다. 관련 코드는 다음과 같습니다.// package org.springframework.cache.interceptor;public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { ... public void configure( @Nullable Supplier&lt;CacheErrorHandler&gt; errorHandler, @Nullable Supplier&lt;KeyGenerator&gt; keyGenerator, @Nullable Supplier&lt;CacheResolver&gt; cacheResolver, @Nullable Supplier&lt;CacheManager&gt; cacheManager) { this.errorHandler = new SingletonSupplier&lt;&gt;(errorHandler, SimpleCacheErrorHandler::new); this.keyGenerator = new SingletonSupplier&lt;&gt;(keyGenerator, SimpleKeyGenerator::new); this.cacheResolver = new SingletonSupplier&lt;&gt;(cacheResolver, () -&gt; SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager))); }}CacheAspectSupport 는 AspectJCachingConfiguration에서 빈으로 등록합니다. AspectJCachingConfiguration는 앞서 설정 값(CachingConfigurer)들을 setter injection으로 의존성 주입을 받은 AbstractCachingConfiguration를 상속받으며, 이 주입받은 값들을 이용해 빈으로 등록하게 됩니다. 해당 코드는 다음과 같으며 등록하는 빈인 AnnotationCacheAspect는 CacheAspectSupport를 상속받습니다.// package org.springframework.cache.aspectj;// @Configuration(proxyBeanMethods = false)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public class AspectJCachingConfiguration extends AbstractCachingConfiguration { @Bean(name = CacheManagementConfigUtils.CACHE_ASPECT_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public AnnotationCacheAspect cacheAspect() { AnnotationCacheAspect cacheAspect = AnnotationCacheAspect.aspectOf(); cacheAspect.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager); return cacheAspect; }}// package org.springframework.cache.aspectj;@Aspectpublic class AnnotationCacheAspect extends AbstractCacheAspect { ...}구현 코드내부 원리를 확인해봤으니 실제 코드를 작성해보겠습니다. 문제 상황은 캐쉬 조회(CacheGet) 시 SerializationException이 발생하는 경우이기에, 이를 상속받아 원하는 대로 동작하도록 변경하면 다음과 같습니다. SerializationException 가 발생했을 때 로그만 남기고 예외를 무시하도록 처리했습니다@Slf4jpublic class CustomCacheErrorHandler extends SimpleCacheErrorHandler { @Override public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { if (exception instanceof SerializationException) { log.warn("Failed to deserialize cache value for key: {}", key, exception); return; } super.handleCacheGetError(exception, cache, key); }}@Slf4jpublic class CustomCacheErrorHandler extends SimpleCacheErrorHandler { @Override public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { if (exception instanceof SerializationException) { log.warn("Failed to deserialize cache value for key: {}", key, exception); return; } super.handleCacheGetError(exception, cache, key); }}캐시 설정은 다음과 같습니다. 전체 캐시(Spring Boot Cache)에 대한 책임을 CacheConfig에 두었고 레디스 캐시 설정에 대한 책임을 RedisCacheConfig에 뒀습니다,@Configuration(proxyBeanMethods = false)@EnableCaching@RequiredArgsConstructorpublic class CacheConfig implements CachingConfigurer { private final CacheManager redisCacheManager; @Override @Bean @Primary public CacheManager cacheManager() { return redisCacheManager; } @Override @Bean public CacheErrorHandler errorHandler() { return new CustomCacheErrorHandler(); }}@Configuration(proxyBeanMethods = false)public class RedisCacheConfig{ @Bean public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(cacheConfiguration()) .withCacheConfiguration(PRODUCT_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10))) .build(); } public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(60)) .disableCachingNullValues() .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); } public static class CacheName { public static final String PRODUCT_CACHE = "productCache_V2"; }}캐시를 이용해 비즈니스 로직을 처리하는 서비스 코드는 다음과 같습니다. @Cacheable을 이용해 해당 key에 대한 값이 캐시에 존재하면 캐시에서 조회하고 그렇지 않으면 캐시를 등록하는 코드입니다.@Service@Slf4j@RequiredArgsConstructorpublic class ProductService { private final ProductRepository productRepository; @PostConstruct void initProducts() { productRepository.saveAll(List.of( new Product("box", new BigDecimal(1000)), new Product("snack", new BigDecimal(4000)), new Product("chicken", new BigDecimal(20000)) ) ); } @Cacheable(cacheNames = PRODUCT_CACHE, key = "'top10'") public List&lt;ProductResponse&gt; getTenProduct() { log.warn("NO CACHE - find top 10 products from DB"); return ProductResponse.listOf(productRepository.findTop10By()); } @CacheEvict(cacheNames = PRODUCT_CACHE, key = "'top10'") public void evict() { log.warn("Cache Evicted"); }}@RestController@RequestMapping("/api/v1")@RequiredArgsConstructorpublic class ProductController { private final ProductService productService; @GetMapping("/products/top10") public ResponseEntity&lt;?&gt; getTop10Products() { return ResponseEntity.ok(productService.getTenProduct()); }}위의 CustomCacheErrorHandler와 CachingConfigurer 를 설정해주지 않은 상태에서api.ProductResponse 패키지 경로로 해당 객체의 캐시를 만들고 api.v2.ProductResponse 패키지로 옮긴 다음에 동일한 키로 조회를 하면 SerializationException이 발생하면서 ExceptionHandler를 통한 에러 응답이 오게됩니다.위의 코드를 적용하고 동일한 상황을 재현해보면 warn 로그가 찍히고 응답이 정상적으로 가는 것을 볼 수 있습니다. 이로 인해 새로운 버전의 클래스로 캐시를 덮어쓰게 되면서 이후 요청에 대해서는 정상적으로 캐시를 조회하게 됩니다.2024-03-31 20:13:28.248 WARN 43092 --- [nio-8080-exec-1] c.e.r.config.CustomCacheErrorHandler : Failed to deserialize cache value for key: top10org.springframework.data.redis.serializer.SerializationException: Cannot deserialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is org.springframework.core.NestedIOException: Failed to deserialize object type; nested exception is java.lang.ClassNotFoundException: com.example.redisinactions.api.v2.ProductResponse at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:84) ~[spring-data-redis-2.7.2.jar:2.7.2] at org.springframework.data.redis.serializer.DefaultRedisElementReader.read(DefaultRedisElementReader.java:49) ~[spring-data-redis-2.7.2.jar:2.7.2] at org.springframework.data.redis.serializer.RedisSerializationContext$SerializationPair.read(RedisSerializationContext.java:272) ~[spring-data-redis-2.7.2.jar:2.7.2] at org.springframework.data.redis.cache.RedisCache.deserializeCacheValue(RedisCache.java:298) ~[spring-data-redis-2.7.2.jar:2.7.2] at org.springframework.data.redis.cache.RedisCache.lookup(RedisCache.java:95) ~[spring-data-redis-2.7.2.jar:2.7.2] at org.springframework.cache.support.AbstractValueAdaptingCache.get(AbstractValueAdaptingCache.java:58) ~[spring-context-5.3.22.jar:5.3.22] at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:73) ~[spring-context-5.3.22.jar:5.3.22] ...2024-03-31 20:13:28.250 WARN 43092 --- [nio-8080-exec-1] c.e.redisinactions.api.ProductService : NO CACHE - find top 10 products from DB2024-03-31 20:13:28.298 DEBUG 43092 --- [nio-8080-exec-1] org.hibernate.SQL : select product0_.id as id1_0_, product0_.description as descript2_0_, product0_.price as price3_0_, product0_.quantity as quantity4_0_ from product product0_ limit ?Hibernate: select product0_.id as id1_0_, product0_.description as descript2_0_, product0_.price as price3_0_, product0_.quantity as quantity4_0_ from product product0_ limit ?2024-03-31 20:13:28.311 DEBUG 43092 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]2024-03-31 20:13:28.311 DEBUG 43092 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [[com.example.redisinactions.api.ProductResponse@3c54979a, com.example.redisinactions.api.ProductResp (truncated)...]2024-03-31 20:13:28.317 DEBUG 43092 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK2024-03-31 20:14:47.332 DEBUG 43092 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : GET "/api/v1/products/top10", parameters={}2024-03-31 20:14:47.332 DEBUG 43092 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.redisinactions.api.ProductController#getTop10Products()2024-03-31 20:14:47.336 DEBUG 43092 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]2024-03-31 20:14:47.336 DEBUG 43092 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [[com.example.redisinactions.api.ProductResponse@39bba9c2, com.example.redisinactions.api.ProductResp (truncated)...]2024-03-31 20:14:47.337 DEBUG 43092 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Completed 200 OK하지만 소개한 해결 방법은 배포 전략에 따라 문제가 될 수 있습니다. 단번에 배포를 변경하는 Blue-Green 전략의 경우 롤백하지 않는다면 새로 배포된 인스턴스는 새로운 캐시만 바라보게 되므로 문제가 되지 않습니다.하지만 만약 캐시에 해당하는 클래스의 패키지를 변경하고 카나리 배포를 진행하고 구 버전과 신 버전의 인스턴스가 동시에 떠있는 상태라면 CacheName은 동일하기에 새로운 버전의 인스턴스(api.v2.ProductResponse)와 구 버전의 인스턴스(api.ProductResponse)에 대한 캐시 쓰기가 반복될 수 있습니다. 이 경우 트래픽이 많은 서비스라면 캐시 저장소에 대한 부하가 커질 수 있다는 단점이 존재합니다. 따라서 카나리의 경우는 이전에 제시한 해결책인 새로운 CacheName을 가진 캐시를 새로 만드는게 더 안전한 방법일 수 있습니다. 따라서 본 글의 목적인 상황에 맞게 캐시의 에러 핸들링 전략을 취하면 적절할 것 같습니다.코드는 다음 링크에서 확인해볼 수 있습니다. https://github.com/ChoiEungi/redis-in-actions" }, { "title": "분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법", "url": "/posts/spring-cloud-refresh/", "categories": "Development", "tags": "Spring Boot, Spring Cloud Config", "date": "2024-01-21 16:12:00 +0900", "snippet": "근래에 요즘 우아한 개발이라는 책을 읽으면서 내용 중에 배포 없이 Spring Cloud Config에서 받아오는 프로퍼티를 변경해 서버에 적용하려는 내용을 접했습니다. 해당 팀은 외부 메시지 플랫폼을 이중화하면서 각 외부 플랫폼 연동에 대한 트래픽 분배를 어플리케이션 실행 중에도 변경할 수 있도록 구현해 단일 장애 지점(SPOF)을 제거하려 했습니다.이를 위해 어플리케이션의 배포 없이 Config 서버의 프로퍼티를 변경함으로 트래픽 분배를 변경하는 방법을 고려했고, 팀에서 최종적으로 일반적으로 사용하는 Spring Cloud Bus를 이용하기보다 Spring Boot만을 활용해 프로퍼티를 재배포 없이 수정하는 방법을 선택했습니다. 이 부분이 흥미롭게 느껴져 호기심에 구현해보게 되었습니다.배포 없이 프로퍼티를 변경하는 방법, refreshSpring Boot를 이용하면 로깅 레벨, 데이터 소스 정보, 타임아웃 설정, 환경 변수 등 여러 설정 정보(이하 프로퍼티)를 application.yml 혹은 application.properties 파일에 명시해서 외부화(externalize)할 수 있습니다. 이를 통해 다양한 환경(local, dev, prod 등) 설정을 코드가 아닌 외부 파일에 명시함으로 해당 관심사를 코드에서 분리할 수 있습니다.책에 나온 사례와 같이 외부 시스템 장애와 같은 기민한 대응을 하려면 어플리케이션 배포 없이도 런타임 환경에서 변경할 수 있어야 합니다. 만약 해당 기능이 비즈니스에서 중요한 역할을 하는 부분이라면 더더욱 조심스럽고 기민하게 다뤄야 합니다.Spring Cloud Config 프로퍼티를 직접 클라우드 서비스에 올려 사용할 수 있도록 해줍니다. 이를 이용하면 Cloud Config 서버를 호출해서 스프링 프로퍼티값을 받아올 수 있습니다. 또 Cloud Config를 이용하면 @RefreshScope 빈을 손쉽게 reload할 수 있게 됩니다. 간단한 예로 Spring Cloud Config 서버의 프로퍼티를 변경한 이후에 actuator에서 /refresh endpoint를 활성화 한 상태에서 호출하게 되면 변경된 프로퍼티 값을 받아올 수 있습니다. 참고 링크일반적으로 트래픽이 많은 서비스의 서버는 멀티 인스턴스 환경에서 운영됩니다. 여기서 모든 인스턴스의 Config 프로퍼티는 동일해야 하며, 이를 api 호출로는 하나의 인스턴스 밖에 Config 프로퍼티 변경이 안되고 이는 서버마다 설정값이 달라지게 됩니다.이러한 문제를 해결하기 위해 Spring Cloud Bus를 통해 멀티 인스턴스 환경에서도 프로퍼티를 Refresh할 수 있습니다. 간략하게 소개하면 Config Server의 프로퍼티에 변경이 감지되면 Kafka, Redis, AMQP 중 하나를 이용해 변경된 프로퍼티 사용하는 모든 서버에 전달하는 역할을 합니다.다만 Spring Cloud Bus 라이브러리는 Kafka, Redis, AMQP를 사용해 외부 인프라 의존성을 가지게 되고 해당 인프라 상태가 정상적이지 않을 때 사용하기 어렵습니다.이를 해결하기 위해서 책에서는 Spring Cloud Config + Scheduled Polling을 이용한 아키텍처를 제안합니다. 외부 인프라를 관리하는 비용이 없다는 장점이 있습니다. 이를 구현해보도록 해보면 다음과 같습니다.이제 코드를 통해 살펴보도록 하겠습니다.@Schedule 와 ConteextRefresher를 이용해 구현@Component@RequiredArgsConstructor@ConditionalOnProperty(value = "application.config.refresh.auto.enabled", havingValue = "true", matchIfMissing = true)@Slf4jpublic class ConfigRefreshScheduler { private final ContextRefresher contextRefresher; private final TestValueProperties testValueProperties; @Scheduled(fixedDelay = 5L, timeUnit = TimeUnit.SECONDS) @Async("refreshThreadPoolExecutor") public void refreshConfig() { try { Set&lt;String&gt; refreshedKeys = contextRefresher.refreshEnvironment(); if (!CollectionUtils.isEmpty(refreshedKeys)) { log.info("[Refreshed] " + String.join(",", refreshedKeys)); contextRefresher.refresh(); log.info("Changed value: {}", testValueProperties.getValue()); } } catch (Exception e) { log.error("config refresh failed {}", e); } } @ConditionalOnBean(value = ConfigRefreshScheduler.class) @Bean public ThreadPoolTaskExecutor refreshThreadPoolExecutor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); threadPoolTaskExecutor.setThreadNamePrefix("refresh"); threadPoolTaskExecutor.setCorePoolSize(1); threadPoolTaskExecutor.setMaxPoolSize(1); threadPoolTaskExecutor.setQueueCapacity(Integer.MAX_VALUE); threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; } @ConfigurationProperties(prefix = "my") @Configuration @Getter @Setter @RefreshScope public static class TestValueProperties { // get my.value from config properties private String value; @PostConstruct void init() { log.info("-------------------------------- my properties value --------------------------------"); log.info("{} Bean Loaded", TestValueProperties.class.getName()); log.info(value); log.info("-------------------------------- my properties value --------------------------------"); } }}해당 코드는 5초마다 Config Server의 프로퍼티와 서버의 프러퍼티를 비교해 변경된 프로퍼티가 있으면 @RefreshScope 빈(TestValueProperties)을 초기화하는 코드입니다. TestProperties는 Config Server에서 my.value라는 프로퍼티 값을 바인딩하는 빈입니다.refresh에서 중요한 역할을 하는 ContextRefresher를 간략하게 살펴보면 refreshEnvironment()는 빈을 refresh하지 않으며, config 설정만 refresh하고 이로 인해 변경된 config의 key값을 리턴합니다. refresh()의 경우 refreshEnvironment() 을 먼저 호출해서 config를 spring conetext에 저장하며 그 이후 @RefreshScope 빈을 모두 refresh하고 변경된 config의 key값을 리턴합니다.실제로 서버를 구동하면 다음 값을 가지고 있습니다.이후 config Server 값을 변경하면 다음과 같습니다.만약 Config Server의 yml 형식이 잘못되었거나 config 서버에 이상이 생긴다면 다음과 같은 에러 로그가 발생합니다. 이는 프로퍼티를 원활히 받아오지 못했기에 기존에 정상적으로 받아온 프로퍼티로 서비스가 운영되게 됩니다.이제 ContextRefresher 내부 코드를 통해 더 구체적인 동작 원리를 알아보겠습니다.Context Refresher 원리ContextRefresher은 Config 서버에서 받아와 API 서버 프로퍼티를 Refresh하거나 RefreshScope 빈을 Refresh하는 역할을 합니다. ContextRefresher는 Spring Cloud Client 라이브러리 의존성을 추가하면 RefreshAutoConfiguration으로 인해 자동으로 빈으로 등록됩니다. 다음 코드를 보면 알 수 있듯 spring.cloud.bootstrap.enabled: true 면 LegacyContextRefresher가 빈으로 등록되고 그렇지 않으면 ConfigDataContextRefresher가 빈으로 등록됩니다. Spring Cloud에서 bootstrap 기본 옵션은 false이기에 ConfigDataContextRefresher 가 빈으로 등록됩니다. 두 차이를 간략하게 소개하면 내부 코드에서 ConfigDataContextRefresher는 빈 후처리기인 EnvironmentPostProcessor를 이용해 ApplicationContext를 Refresh하기 때문에 ApplicationContext를 전체를 refresh하는 LegacyContextRefresher보다 더 효율적이라고 볼 수 있습니다. 더 자세한 내용은 본 글의 주제와 벗어나 더 다루지는 않겠습니다.//package org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;@Configuration(proxyBeanMethods = false)@ConditionalOnClass(RefreshScope.class)@ConditionalOnProperty(name = RefreshAutoConfiguration.REFRESH_SCOPE_ENABLED, matchIfMissing = true)@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)@EnableConfigurationProperties(RefreshAutoConfiguration.RefreshProperties.class)public class RefreshAutoConfiguration { /** * Name of the refresh scope name. */ public static final String REFRESH_SCOPE_NAME = "refresh"; /** * Name of the prefix for refresh scope. */ public static final String REFRESH_SCOPE_PREFIX = "spring.cloud.refresh"; /** * Name of the enabled prefix for refresh scope. */ public static final String REFRESH_SCOPE_ENABLED = REFRESH_SCOPE_PREFIX + ".enabled"; @Bean @ConditionalOnMissingBean(RefreshScope.class) public static RefreshScope refreshScope() { return new RefreshScope(); } ... @Bean @ConditionalOnMissingBean @ConditionalOnBootstrapEnabled public LegacyContextRefresher legacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, RefreshProperties properties) { return new LegacyContextRefresher(context, scope, properties); } @Bean @ConditionalOnMissingBean @ConditionalOnBootstrapDisabled public ConfigDataContextRefresher configDataContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, RefreshProperties properties) { return new ConfigDataContextRefresher(context, scope, properties); } @Bean public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) { return new RefreshEventListener(contextRefresher); } ...}ConfigDataContextRefresher는 ContextRefresher의 구현체입니다. ContextRefresher는 spring web actuator에서 /refresh endpoint를 enable할 떄 ContextRefresher를 사용해 @RefreshScope 빈을 refresh합니다. 실제로 spirng web actuator의 RefreshEndpoint 클래스는 다음과 같으며, contextRefresher.refresh()를 통해 @RefreshScope 빈을 refresh해주고 있습니다.// package org.springframework.cloud.context.refresh.ConfigDataContextRefresher;public class ConfigDataContextRefresher extends ContextRefresher implements ApplicationListener&lt;ApplicationPreparedEvent&gt; { private SpringApplication application; @Deprecated public ConfigDataContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) { super(context, scope); } public ConfigDataContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, RefreshAutoConfiguration.RefreshProperties properties) { super(context, scope, properties); } @Override public void onApplicationEvent(ApplicationPreparedEvent event) { application = event.getSpringApplication(); } @Override protected void updateEnvironment() { ... }}// package org.springframework.cloud.endpoint.RefreshEndpoint@Endpoint(id = "refresh")public class RefreshEndpoint{ private ContextRefresher contextRefresher; public RefreshEndpoint(ContextRefresher contextRefresher) { this.contextRefresher = contextRefresher; } @WriteOperation public Collection&lt;String&gt; refresh() { Set&lt;String&gt; keys = this.contextRefresher.refresh(); return keys; }}ContextRefresher를 조금 더 들어가보겠습니다. 먼저 refreshEnvironment()는 빈을 refresh하지 않으며, config 설정만 refresh하고 이로 인해 변경된 config의 key값을 리턴합니다. refresh()의 경우 refreshEnvironment() 을 먼저 호출해서 config를 spring conetext에 저장하며 그 이후 @RefreshScope 빈을 모두 refresh하고 변경된 config의 key값을 리턴합니다.이를 통해 bean overriding option에 따라 ConextRefresher의 구현체가 달라지는 것은 refresh할 때 빈을 정의하는 방식이 달라지기 때문임을 알 수 있습니다. 이는 updateEnvironment()를 구현체를 통해 구현한다는 것으로 이해할 수 있습니다.// package org.springframework.cloud.context.refresh.ContextRefresher;public abstract class ContextRefresher { private ConfigurableApplicationContext context; private RefreshScope scope; @SuppressWarnings("unchecked") protected ContextRefresher(ConfigurableApplicationContext context, RefreshScope scope, RefreshAutoConfiguration.RefreshProperties properties) { this.context = context; this.scope = scope; additionalPropertySourcesToRetain = properties.getAdditionalPropertySourcesToRetain(); } public synchronized Set&lt;String&gt; refresh() { Set&lt;String&gt; keys = refreshEnvironment(); this.scope.refreshAll(); return keys; } public synchronized Set&lt;String&gt; refreshEnvironment() { Map&lt;String, Object&gt; before = extract(this.context.getEnvironment().getPropertySources()); updateEnvironment(); Set&lt;String&gt; keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys)); return keys; } protected abstract void updateEnvironment(); ...}이를 통해 서비스 점검 시간 등 잘 변하지 않지만 종종 변경해줘야 값들 등 property에 넣어서 사용하는 값들을 배포 없이 config 서버의 값 변경만으로 준 실시간으로 적용할 수 있게됩니다. 또, 피쳐 플래그, 스프링 스케줄러, 외부 API 등 on/off 관련 기능에도 적용해볼 수 있습니다.주의사항시스템이 커지다 보면 코드량이 많아지면서 빌드 시간이 늘어나게 되고 스프링의 경우 어플리케이션 빈이 많아지다보면 실행 시간이 늘어나게됩니다. 이에 따라 설정값 변경만으로 인해 다시 배포하는데 시간이 오래 소요됩니다. 그래서 배포 없이 런타임 환경에서 설정 정보를 변경하는건 굉장히 유용하다고 생각합니다. 하지만 은탄환은 존재하지 않습니다.먼저 @RefreshScope를 남용하면 안됩니다. datasource, redis, kafka와 같은 설정 정보들은 빈이 복잡하게 얽혀있고 서비스가 운영되는 중에 refresh를 하면 서버에 부담이 크게 작용할 수 있습니다. 실제로 사내에서 DataourceConfiguration 관련 빈에 RefreshScope을 넣어 서버가 죽은 경험도 있습니다. 차라리 해당 상황의 경우 배포를 다시해서 설정 정보를 초기화 하는게 더 안정적으로 서비스를 제공하는 방법이라고 생각합니다.또, ConetxtRefresher를 활용할 때 ENC(value) 값과 같이 Jasypt를 이용해 암호화된 설정 값을 이용하게 되면 refreshEnvironment()에서 복호화된 값과 복호화되기 전의 값을 가져오게 되어 명확하게 변경된 설정값을 가져오지 못하는 현상도 존재합니다. 해당 이슈에 대해서는 다음 글에서 알아보도록 하겠습니다.실제로 config 폴링에 대해 spring-cloud-commons docs에서는 폴링을 권장하지 않는 방법이라고 언급합니다. Note that the Spring Cloud Config Client does not, by default, poll for changes in the Environment. Generally, we would not recommend that approach for detecting changes (although you can set it up with a @Scheduled annotation).폴링의 경우 Config Server에 대한 트래픽이 더 커지게 되고 조직의 규모가 커지고 해당 Config Server를 사용하는 API 서버들이 많아질수록 더 부하가 많이갈 수 있습니다. 이는 예상치 못한 일이 발생할 수 있기에 해당 방법을 차용하게되면 Config Server를 더 세심하게 모니터링해야 합니다. 현재 조직 상황에 맞는 방법을 적절히 사용하는게 가장 중요하다고 생각합니다.실제로 Toss Slash 23 영상(20:35~)에 따르면 운영 환경에서는 사용하지 않고 휴면 에러가 발생할 수 있다는 의견도 있기에 상황에 맞게 적절히 사용해야 합니다.해당 소스 코드는 다음 링크에서 확인해볼 수 있습니다.참고 링크 https://techblog.woowahan.com/7724/ https://www.youtube.com/watch?v=Zs3jVelp0L8&amp;t=1235s https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#http-clients:~:text=Note%20that%20the%20Spring%20Cloud%20Config%20Client%20does%20not%2C%20by%20default%2C%20poll%20for%20changes%20in%20the%20Environment.%20Generally%2C%20we%20would%20not%20recommend%20that%20approach%20for%20detecting%20changes%20(although%20you%20can%20set%20it%20up%20with%20a%20%40Scheduled%20annotation)" }, { "title": "Spring MVC에서 redisson으로 분산락을 구현하는 방법들", "url": "/posts/spring-redis-distributed-lock/", "categories": "Development", "tags": "Spring Boot, Redis", "date": "2023-12-24 23:13:00 +0900", "snippet": "멀티 인스턴스 환경에서 동시성을 해결하는 방법으로 Redis의 이벤트 루프 기반 싱글스레드 특성을 이용해 분산락을 사용해 쉽게 해결할 수 있습니다. 동시 호출은 DB 데이터의 정합이 깨지거나 메시지 이벤트의 중복 발행 등 예상치 못한 동작으로 이어지는 경우가 많아 해결해야 하는 경우가 빈번합니다.분산락은 동시성을 제어하기 위한 부가 기능으로 비즈니스 로직과 섞이지 않도록 관심사를 잘 분리하는게 좋습니다. 관련해서 스프링은 Dependency Injection, AOP와 같은 여러 기능을 쉽게 사용할 수 있어 여러 구현 방법을 소개해보려 합니다. 구현은 Java, Spring MVC 기반으로 Redisson을 이용해 구현할 예정입니다.요구사항 분산락이 실패하는 경우 null을 리턴하게 됩니다. 레디스 장애가 발생해도 원래 메소드는 동작해야 합니다. 락 내부 트랜잭션을 사용할 수 있어야 하며 락이 끝나기 전에 트랜잭션이 종료되어야 합니다. 트랜잭션이 종료되어야 하는 이유는 트랜잭션이 종료함으로 DB에 저장된 시점에 락을 해제해야 동시 호출에 대한 온전히 제어할 수 있기 때문입니다. 더 자세한 내용은 다음 글에서 소개하고 있기에 본 글에서는 생략하겠습니다. 락 설정 정보를 하나의 enum으로 관리할 수 있어야 합니다.함수형 인터페이스를 이용한 구현 (template callback 패턴)락을 잡으려고 하는 구간을 함수형 인터페이스(콜백 함수) 인자로 받아서 처리하는 방법입니다. 트랜잭션을 사용할 수 있으며, 트랜잭션의 종료를 보장하기 위해 전파 옵션을 PROPAGATION_REQUIRES_NEW로 설정했습니다. 구현 코드 다음과 같습니다.Lock 설정(enum) 코드public enum LockConfig { PRODUCT_DECREASE("PRODUCT", 1L, 1L, TimeUnit.SECONDS), LOGIN_LOCK("LOGIN_LOCK", 2L, 2L, TimeUnit.SECONDS); public final Long waitTime; public final Long leaseTime; public final TimeUnit timeUnit; private final String lockPrefix; LockConfig(String lockPrefix, Long waitTime, Long leaseTime, TimeUnit timeUnit) { this.lockPrefix = lockPrefix; this.waitTime = waitTime; this.leaseTime = leaseTime; this.timeUnit = timeUnit; } public String generateKey(String key) { if (!StringUtils.hasText(key)) { throw new IllegalArgumentException("key must not be empty"); } return String.format("%s_%s", lockPrefix, key); }}@Component @Slf4j public class RedissonDistributedLockTemplate { private final RedissonClient redissonClient; private final TransactionTemplate transactionTemplate; public RedissonDistributedLockTemplate(RedissonClient redissonClient, PlatformTransactionManager platformTransactionManager) { this.redissonClient = redissonClient; this.transactionTemplate = new TransactionTemplate(platformTransactionManager); this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); this.transactionTemplate.afterPropertiesSet(); } public void executeWithLock(String key, LockConfig lockConfig, Runnable callback) { executeWithLock(key, lockConfig, toVoidSupplier(callback)); } public &lt;T&gt; T executeWithLock(String key, LockConfig lockConfig, Supplier&lt;T&gt; callback) { RLock lock = redissonClient.getLock(lockConfig.generateKey(key)); try { boolean isAcquired = lock.tryLock(lockConfig.waitTime, lockConfig.leaseTime, lockConfig.timeUnit); if (!isAcquired) { log.warn("[lock 획득 실패] {}, key : {}", lockConfig, key); return null; } return callback.get(); } catch (RedisConnectionException redisUnavailableException) { log.warn("", redisUnavailableException); return callback.get(); } catch (InterruptedException e) { log.error("", e); Thread.currentThread().interrupt(); return null; } finally { if (lock.isLocked() &amp;&amp; lock.isHeldByCurrentThread()) { lock.unlock(); } } } public void executeWithLockAndTransaction(String key, LockConfig lockConfig, Runnable callback) { executeWithLockAndTransaction(key, lockConfig, toVoidSupplier(callback)); } public &lt;T&gt; T executeWithLockAndTransaction(String key, LockConfig lockConfig, Supplier&lt;T&gt; callback) { RLock lock = redissonClient.getLock(lockConfig.generateKey(key)); try { boolean isAcquired = lock.tryLock(lockConfig.waitTime, lockConfig.leaseTime, lockConfig.timeUnit); if (!isAcquired) { log.warn("[lock 획득 실패] {}, key : {}", lockConfig, key); return null; } return transactionTemplate.execute(status -&gt; callback.get()); } catch (RedisConnectionException redisUnavailableException) { log.warn("", redisUnavailableException); return transactionTemplate.execute(status -&gt; callback.get()); } catch (InterruptedException e) { log.error("", e); Thread.currentThread().interrupt(); return null; } finally { if (lock.isLocked() &amp;&amp; lock.isHeldByCurrentThread()) { lock.unlock(); } } } private Supplier&lt;Void&gt; toVoidSupplier(Runnable runnable) { return () -&gt; { runnable.run(); return null; }; } } waitTime의 경우 락을 획득하는데 기다리는 시간입니다. waitTime이 지나면 락 획득에 실패하며 tryLock의 리턴값은 false가 됩니다. leaseTime은 락을 획득한 이후에 락을 잡고있는 시간을 의미합니다. 락을 획득하고 leaseTime보다 더 오래 작업이 걸리면 작업이 종료된 유무와 관계없이 락을 해제하게 됩니다. 이후 동일키로 다른 락 획득 요청이 있는 경우 성공적으로 획득하게 됩니다. 일반적으로 leaseTime으로 인해 락이 해제되기 전에 작업이 종료되어야 하기 때문에 leaseTime을 작업의 최대 시간으로 설정하는게 좋습니다. redis의 상태에 이상이 생기고 작업을 요청하게되면RedisConnectionException이 발생하게 됩니다. 레디스가 문제가 생겼을 때에도 본 메소드에는 영향이 없어야 하는 상황이 요구사항이기에 해당 상황에서 메소드를 정상적으로 실행합니다. 이 경우 동시 요청이 다시 발생할 수 있게됩니다. 해당 상황에서 동시 요청 처리가 비즈니스에 중요하다면 해당 상황에서 실패처리 하는 것도 방법입니다. 따라서 상황에 맞게 효용가치가 큰 방법을 취사하는게 중요합니다. 트랜잭션의 경우 선언형으로 사용하기 어려워 TransactionTemplate을 이용해 구현했습니다. Mvc에서 일반적으로 트랜잭션을 관리하는데 사용하는 PlatformTransactionManager을 주입받아 사용하였습니다.사용하는 코드는 다음과 같습니다.@Service @RequiredArgsConstructor public class V2ProductService { private final ProductRepository productRepository; private final RedissonDistributedLockTemplate redissonDistributedLockTemplate; ... public Product decreaseWithCallback(Long id, Long quantity) { Product result = redissonDistributedLockTemplate.executeWithLock(id.toString(), PRODUCT_DECREASE, () -&gt; { Product product = productRepository.findById(id).orElseThrow(); product.decrease(quantity); return productRepository.save(product); }); return result; } public Product decreaseWithCallbackTransaction(Long id, Long quantity) { Product result = redissonDistributedLockTemplate.executeWithLockAndTransaction(id.toString(), PRODUCT_DECREASE, () -&gt; { Product product = productRepository.findById(id).orElseThrow(); product.decrease(quantity); return product; }); return result; } }테스트 코드@SpringBootTestclass V2ProductServiceTest { @Autowired private V2ProductService v2ProductService; @Autowired private ProductRepository productRepository; private Product product; @BeforeEach void setUp() { product = productRepository.save(new Product("description", new BigDecimal(10000), 100L)); } ... @Test void decreaseWithCallback() { // given int requestCount = 10; List&lt;CompletableFuture&lt;?&gt;&gt; futureList = new ArrayList&lt;&gt;(); for (int i = 0; i &lt; requestCount; i++) { CompletableFuture&lt;Object&gt; objectCompletableFuture = CompletableFuture.supplyAsync(() -&gt; { v2ProductService.decreaseWithCallback(product.getId(), 1L); return null; }); futureList.add(objectCompletableFuture); } // then Product result = productRepository.findById(product.getId()).orElseThrow(); assertThat(result.getQuantity()).isEqualTo(90L); }}장점 특별한 클래스 분리가 필요 없이 Template 주입만을 통해서 메소드 내부에서 직접 락 구간을 지정할 수 있습니다.단점 기존 코드의 depth가 깊거나 콜백(함수형 인터페이스)을 사용하는 코드가 많다면, 콜백 지옥에 빠질 수 있습니다. 콜백 메소드 내부에서 값들이 여러개라면 해당 값들을 모두 리턴해야 합니다. 그렇게 되면 이를 위한 객체를 만들어야 합니다. 부가 기능이 테스트에 영향을 주게 됩니다. 콜백 메소드 내부만 테스트하고 싶다면 template에 대한 Mocking혹은 Stubbing 필요합니다.AOP(Aspect Oriented Programming)를 이용한 구현스프링 부트는 어노테이션 기반 AOP 기능을 손쉽게 사용할 수 있게 제공합니다. AOP를 사용하기 위해서는@EnableAspectJAutoProxy설정을 등록해야 합니다. 코드는 다음과 같습니다.AOP 구현체 JoinPointSpELParser는 직접 정의한 spring Expression Language parser로 #매개변수명을 통해 값을 접근할 수 있습니다. @Cacheable, @PreAuthorize, @Value 에서 활용하는 것과 동일하며, 직접 구현해 사용했습니다. 자세한 구현은 코드에서 확인해볼 수 있으며 본 내용에서는 생략하도록 하겠습니다.@Component@Slf4j@Aspect@RequiredArgsConstructorpublic class DistributedLockAspect { private final RedissonClient redissonClient; private final JoinPointSpELParser joinPointSpELParser; @Around("@annotation(distributedLock)") public Object lock(ProceedingJoinPoint pjp, DistributedLock distributedLock) throws Throwable { String pa = joinPointSpELParser.parseSpEL(pjp, distributedLock.key()); final String key = distributedLock.lockConfig().generateKey(pa); RLock lock = redissonClient.getLock(key); try { final boolean isAcquired = lock.tryLock(distributedLock.lockConfig().waitTime, distributedLock.lockConfig().leaseTime, distributedLock.lockConfig().timeUnit); if (!isAcquired) { return null; } return pjp.proceed(); } catch (RedisConnectionException redisUnavailableException) { log.warn("", redisUnavailableException); return pjp.proceed(); } catch (InterruptedException e) { log.error("", e); Thread.currentThread().interrupt(); return null; } finally { if (lock.isLocked() &amp;&amp; lock.isHeldByCurrentThread()) { lock.unlock(); } } }}@Target(value = ElementType.METHOD)@Retention(value = RetentionPolicy.RUNTIME)public @interface DistributedLock { String key(); LockConfig lockConfig();}장점 비즈니스 로직과 부가 기능의 관심사를 완전히 분리할 수 있습니다. 이를 통해 비즈니스 로직 테스트를 작성하기 더 쉬워집니다. Spring Expression Langugae을 이용해 인자값으로 키값 설정을 간편하게 활용할 수 있습니다.단점 타 AOP 어노테이션(@Transactional, @Async 등)과의 순서(@Order) 지정 및 동작 예측이 어려워집니다. 고질적인 AOP의 단점인 자기 호출(self-invocation) 문제가 존재합니다. 오버로딩을 많이 하는 코드에서는 사용하기 어려울 수 있습니다.AOP와 함수형 인터페이스를 이용한 구현사실 AOP와 콜백의 장단은 서로 보완관계라고 생각해 상황에 맞게 취사선택하는 방법도 좋은 방법이라고 생각합니다. 사실 AOP 내부에서 함수형 인터페이스를 이용한 구현체를 사용하면 일관된 구현을 통해 락을 활용할 수 있습니다. 이를 @V2DistributedLock으로 만들어 구현해보겠습니다.@Component @Aspect @RequiredArgsConstructor public class V2DistributedLockAspect { private final JointPointSpELParser jointPointSpELParser; private final RedissonDistributedLockTemplate redissonDistributedLockTemplate; @Around("@annotation(v2DistributedLock)") public Object lock(ProceedingJoinPoint pjp, V2DistributedLock v2DistributedLock) { String parsedKey = jointPointSpELParser.parseSpEL(pjp, v2DistributedLock.key()); final String key = v2DistributedLock.lockConfig().generateKey(parsedKey); if (v2DistributedLock.isTransactionEnabled()) { return redissonDistributedLockTemplate.executeWithLockAndTransaction(key, v2DistributedLock.lockConfig(), proceed(pjp)); } return redissonDistributedLockTemplate.executeWithLock(key, v2DistributedLock.lockConfig(), proceed(pjp)); } private Supplier&lt;Object&gt; proceed(ProceedingJoinPoint pjp) { return () -&gt; { try { return pjp.proceed(); } catch (Throwable e) { throw new RuntimeException(e); } }; } }@Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) public @interface V2DistributedLock { String key(); LockConfig lockConfig(); boolean isTransactionEnabled() default false; }사용 코드@Service@RequiredArgsConstructorpublic class V2ProductService { private final ProductRepository productRepository; @DistributedLock(key = "#id", lockConfig = PRODUCT_DECREASE) public Product decreaseWithAOP(Long id, Long quantity) { Product product = productRepository.findById(id).orElseThrow(); product.decrease(quantity); return productRepository.save(product); } @V2DistributedLock(key = "#id", lockConfig = PRODUCT_DECREASE, isTransactionEnabled = true) public Product decreaseWithAOPV2(Long id, Long quantity) { Product product = productRepository.findById(id).orElseThrow(); product.decrease(quantity); return product; }}마무리일반적인 상황에서 오버헤드는 많이 발생하지 않지만 트래픽이 많아지거나 redis 지연 발생, 혹은 장애 상황이 발생할 때의 고민도 필요합니다. 또한, DB 작업은 테이블의 데이터양이 변화함에 따라 실행 속도가 일관되지 않을 수 있기 때문에 수행 시간에 대한 모니터링, 타임아웃 발생에 대한 모니터링을 진행해 락 시간(waitTime, leaseTime)의 유동적인 조절이 필요할 수도 있습니다.나아가 본 글에서 동시성에 대해 해결 방법을 소개했지만 근본적으로 동시성이 어디서, 왜 발생하는가에 대한 근본적인 질문을 해보는 것도 방법입니다. 단순히 분산락은 동시 호출이라는 문제를 해결하기 위한 수단일 뿐 절대적인 해결 방법으로만 고려하는건 적절하지 않다고 생각합니다.하지만 모든 동시성 상황이 선착순이나 재고 관리와 같은 상황이 아닐 수 있습니다. 간혹 동시성이 일어나는 경우 클라이언트의 잘못된 구현으로 동시 호출이 일어날 수도 있고, UX 상으로 동시성이 일어날 수 있는 설계일 수도 있습니다. 특히 클라이언트의 경우 이벤트 기반으로 동작하는 경우가 많아 동시 호출을 놓치는 경우도 종종 있었습니다.모든 예외는 존재하지만 문제의 근본적인 원인과 그 문제의 임팩트에 대해 고민해보고 상황에 맞는 해결책을 제시하고 실행하는게 가장 중요합니다.더 자세한 코드들과 테스트 코드들은 다음 링크에서 확인할 수 있습니다. https://github.com/ChoiEungi/redis-in-actions참고 링크 https://helloworld.kurly.com/blog/distributed-redisson-lock/#3-%EB%B6%84%EC%82%B0%EB%9D%BD%EC%9D%84-%EB%B3%B4%EB%8B%A4-%EC%86%90%EC%89%BD%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98%EB%8A%94-%EC%97%86%EC%9D%84%EA%B9%8C" }, { "title": "Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점", "url": "/posts/spring-redis-cache-serialization-exception/", "categories": "Development", "tags": "Spring Boot, Redis", "date": "2023-12-10 23:30:00 +0900", "snippet": "사내에서 패키지 구조 변경 작업을 하고 배포를 했는데 갑자기 특정 API에서 transaction silently rolled back이 발생했었습니다. 관련해서 확인해보니 DB조회 값을 Dto 객체로 변환해 캐싱한 값을 역직렬화하는 과정에서 문제가 발생했었습니다. 해당 캐시는 월마다 한번씩 바뀌는 주기를 갖는 값으로, 조회가 많은 비율을 차지합니다. 캐시로 사용하는 정보가 DB에서 열거형으로 관리되고 있어 이를 자바 Dto 객체로 직렬화해서 redis에 저장해 캐시로 활용하고 있었습니다.코드를 확인해보면 다음과 같습니다.문제 상황설정 값들// package com.example.redisinactions.api;@Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProductResponse implements Serializable { private String description; private BigDecimal price; private static ProductResponse of(Product product) { return new ProductResponse(product.getDescription(), product.getPrice()); } public static List&lt;ProductResponse&gt; listOf(List&lt;Product&gt; productList) { return productList.stream() .map(ProductResponse::of) .toList(); } }@Configuration@EnableCachingpublic class CacheConfig { @Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(60)) .disableCachingNullValues() .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); } @Bean public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { return (builder) -&gt; builder .withCacheConfiguration(PRODUCT_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10))); } public static class CacheName { public static final String PRODUCT_CACHE = "productCache"; }}레디스 캐시를 사용하는 서비스@Service@Slf4j@RequiredArgsConstructorpublic class ProductService { private final ProductRepository productRepository; @PostConstruct void initProducts() { productRepository.saveAll(List.of( new Product("box", new BigDecimal(1000)), new Product("snack", new BigDecimal(4000)), new Product("chicken", new BigDecimal(20000)) ) ); } @Cacheable(cacheNames = PRODUCT_CACHE, key = "'top10'") public List&lt;ProductResponse&gt; getTenProduct() { log.warn("NO CACHE - find top 10 products from DB"); return ProductResponse.listOf(productRepository.findTop10By()); } @CacheEvict(cacheNames = PRODUCT_CACHE, key = "'top10'") public void evict() { log.warn("Cache Evicted"); }}@RestController@RequestMapping("/api/v1")@RequiredArgsConstructorpublic class ProductController { private final ProductService productService; @GetMapping("/products/top10") public ResponseEntity&lt;?&gt; getTop10Products() { return ResponseEntity.ok(productService.getTenProduct()); }}해당 코드에서 getTenProduct()를 먼저 호출하면 다음과 같은 응답이 오며 redis에 잘 쌓이게 됩니다.해당 상황을 도식화 하면 다음과 같습니다.이후 ProductResponse를 v2 패키지로 변경한 이후 어플리케이션을 재실행해서 동일한 API를 호출하면 SerializationException이 발생합니다.// package com.example.redisinactions.api.v2;@Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProductResponse implements Serializable { ...}Exception의 cause를 확인해보면 ClassNotFountException이 발생합니다. com.example.redisinactions.api.ProductResponse 클래스를 역직렬화해야 하는데 해당 클래스가 com.example.redisinactions.api.v2.ProductResponse로 변경되어 발생한 현상입니다.Caused by: java.lang.ClassNotFoundException: com.example.redisinactions.api.ProductResponse at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[na:na] ...해결 방법: Cache Key Prefix 변경해당 문제를 해결하기 위해서 @Cacheable에서 키값 deserialization에서 오류가 나는 것이므로 키값을 바꿔줘서 해결할 수 있습니다. 기존에 저장된 캐시를 재사용하는 부분에서 문제가 발생하는 것이기에 새로운 캐시를 다시 저장하고 이를 활용하면 됩니다. 기존 키값에 해당하는 값은 역직렬화할 수 없으므로 자연스럽게 TTL로 인해 사라지게 됩니다. 이를 통해 서비스에 지장 없이 안정적으로 캐시를 변경해서 사용할 수 있습니다. 코드로 나타나면 다음과 같습니다.@Configuration@EnableCachingpublic class CacheConfig { ... public static class CacheName { public static final String PRODUCT_CACHE = "V2_productCache"; // as-is: productCache }}사실 해당 값을 캐싱하는 부분에서 꼭 Redis를 이용해야 하는 부분에 대해서도 고민해볼 필요가 있습니다. Redis가 아니더라도 LocalCache를 이용한다면 빈을 주입할 때 값을 DB에서 조회해서 캐싱해서 사용하는 방법도 좋은 방법이라고 생각합니다.본래 문제는 이로 인한 트랜잭션의 실패였습니다. 더 생각해볼 점은 @Cacheable은 Cahce aside pattern을 사용하는데 해당 전략은 캐시 조회가 실패한다면 원본 데이터에서 가져오는 전략입니다. 따라서 해당 작업이 트랜잭션에서 캐시 조회에서 오류가 발생한다고 롤백 마크로 인해 전체 트랜잭션이 실패하면 안된다고 생각합니다. 이는 트랜잭션을 사용할 때 두고두고 고민해야 하는 부분이라고 생각합니다.관련 소스 코드는 다음 링크에서 확인할 수 있습니다." }, { "title": "MySQL 커넥션, I/O 연산, 잠금에 대한 고찰", "url": "/posts/mysql-resources/", "categories": "Development", "tags": "database, MySQL", "date": "2023-07-16 23:49:00 +0900", "snippet": "실무에서 MySQL 데이터베이스를 활용하면서 커넥션, I/O 연산, 잠금에 대해 더 유심히 살펴보게 되었다. 요 세가지 자원이 중요하다고 배웠는데 실제로 사용해보면서 정말 그렇다는걸 느낄 수 있었다. 그렇기에 이를 통해 배운점들을 다시 상기해고자 한다.커넥션데이터베이스를 사용하면서 가장 중요한 자원을 꼽는다면 커넥션이다. 커넥션이 부족해지는 순간 쿼리를 날릴 수 없다는 의미이며, 이는 곧 장애로 이어진다. 그렇기에 데이터베이스 커넥션 개수를 원활하게 모니터링하고 관리하는 부분은 굉장히 중요하다.커넥션이 부족해지게 만드는 요소로는 트래픽 증가로 서버의 스케일링으로 커넥션이 부족해질 때가 존재한다. 또, 트랜잭션이 길어져 해당 작업이 커넥션을 계속 잡고 있으면 db 커넥션이 부족해질 수 있다. 혹은 어플리케이션에서 커넥션을 제대로 반환하지 않는 경우도 존재할 수 있다.항상 예상치 못한 상황이 발생하기에 db 커넥션 수를 잘 모니터링하고 제대로된 알람을 받는게 가장 중요하다. 이외에도 각 구성요소의 커넥션 풀과 timeout을 잘 설정해 db 커넥션이 부족하지 않도록 신경써야 한다. 부하테스트를 통해 선제적으로 방지하는 것도 방법이 될 수 있다.I/O 최적화커넥션을 적절히 확보한다면, I/O에 대해서 고민해볼 것 같다. DBMS는 결국 데이터를 HDD나 SSD에 읽기와 쓰기 에 대한 I/O를 진행해야 한다. 이 I/O는 사소한 차이에 성능이 천차만별 만큼 차이가 나기 때문에, I/O 연산이 정확한 수치까지는 아니더라도 대략적으로 얼마나 나오는 지 알아두는 건 굉장히 중요하다.조회 I/O 최적화는 조인과 인덱스가 큰 영향을 준다. 조인을 잘못하면 테이블 간의 레코드 개수의 곱만큼의 순차 I/O가 발생할 수 있으며, 인덱스가 없는 컬럼의 테이블을 조회하면 레코드 수만큼의 순차 I/O가 발생할 수 있다. 그렇기에 인덱스를 활용해 대량의 순차 I/O를 소량의 랜덤 I/O로 변경하면 유의미하게 성능이 향상된다. 하지만 인덱스를 생성하는 I/O로 인해 insert, update, delete query의 성능을 저하시킬 수 있으며, 데이터 분포도가 고르지 않을 때 인덱스를 활용하면 조회 성능 마저 기존보다 느려질 수 있다.또, 인덱스를 제대로 활용하지 않고 다량의 데이터를 조회하는 쿼리가 여러개가 작동하게 되면 CPU 사용량이 급증하게 된다. 이 역시 상황에 맞게 적절히 인덱스를 활용하고 조인문을 적절히 활용하는게 좋겠다. 실제로 나는 조인절 인덱스를 고려하지 않아 슬로 쿼리를 만들어 CPU를 높이는 실수한 경험이 있다..커멘드 작업에도 I/O에 대해 고민해보는게 좋다. insert문과 update문을 사용할 때, 다량의 데이터를 넣을 때에는 단건의 쿼리를 고려해보는 것도 방법이다. 이는 서버와 db 간 네트워크 I/O와 인덱스 업데이트 과정의 Disk I/O를 줄일 수 있게된다. 실제로 mysql 공식 문서에서도 insert 속도 성능 향상을 위해 여러 단건 insert문보다 하나의 bulk insert문을 활용하라고 권장한다.물론 bulk insert가 orm에서 기본적으로 제공하지 않을 수도 있다. 예를 들어 현재 회사에서 활용하는 spring jpa에서 컬렉션 타입에 대해 insert 혹은 update를 진행하게 되면 기본적으로 단건으로 여러 쿼리가 발생하는데, 수십~수백건에 대해서는 문제 없이 작동하지만 수천건이 넘어가는 순간부터 유의미하게 속도가 느려진다. 이 상황에서 jdbcTemplate 등을 활용해 raw query를 만드는 방법 등을 통해 bulk insert를 구현해 문제를 해결할 수도 있다.하지만 모든 상황에서 이를 활용하는게 정답은 아니다. 저장해야할 데이터가 수천건이 넘어가 비즈니스 로직에 문제가 생긴다면 bulk insert를 구현해 성능을 최적화 할 수도 있을 것이고, 이 구현 역시 비용이 될 수 있으므로 단순히 spring data jpa의 saveAll()이나 변경 감지를 활용해 간단하게 처리할 수도 있다. 결국 상황에 맞게 적절히 활용해야 한다.잠금(Lock)I/O를 잘 진행하면 그다음 고려할 것은 잠금이다. MySQL은 MVCC를 제공하기에 여러 잠금 레벨을 제공하며, 잠금에 따라 성능이 천차만별이다. 잠금은 데이터 정합을 위한 장치이다. 따라서 성능과 데이터 정합은 반비례한다고 보면 된다. 정합을 중요시해서 잠금을 높은 수준(길게)으로 설정한다면 성능에 이슈가 발생할 수 있다. 반면 정합이 상황에 따라 조금씩 틀리더라도 잠금을 낮은 수준으로 설정하면 성능에 큰 이점을 가져올 수 있다.실제로 잠금을 직접 건드릴 일이 크게 많지는 않았지만, 이로 인한 문제가 발생할 때가 종종 있엇고 어떤 잠금이 문제를 일으키는 지 확인하는 과정은 중요했던 것 같다. 작업을 하면서 고려해야 할 잠금은 여러 개가 존재하겠지만 몇몇 사례를 소개한다. 수천만건 데이터가 있는 테이블의 alter 문 실행, index 추가 및 제거 작업을 진행할 때 잠금 트랜잭션 격리 레벨에 따른 잠금 외래키의 잠금 전파 유니크 인덱스의 읽기/쓰기 잠금 어플리케이션의 낙관적/비관적 잠금결국 쿼리든, 잠금이든 정답은 존재하지 않는다. 도메인과 풀어야 하는 문제 상황에 맞게 적절히 쿼리를 작성하고 잠금 수준을 고려하는게 중요하다. 결제 정보와 같이 정합이 중요하다면 잠금을 직접 걸거나 고려해볼 수도 있으며, 가볍게 여러번 조회하는 타임라인, 목록과 같은 정보의 경우 조회 결과가 조금씩 달라도 큰 문제가 없다면 잠금 수준을 낮게 설정하는 것도 방법이다. 이 상황에서 오히려 정합을 위해 잠금 수준을 높게 두어 속도가 느리다면 고객 경험 측면에서 더 손해일 수도 있다.잠금이나 쿼리에 대해서 최적화를 할 때 꼭 RDBMS에만 의존해야할 필요가 없을 수도 있다. 조회를 위해 인덱스를 거는 대신에 key-value db, document db로 캐싱을 고려하는 것도 방법이며, 어떤 작업에 대한 잠금이 필요할 때 Redis로 분산 락을 거는 것도 방법이다. 결국 방법은 여러 개이기에 지금 상황의 문제에 맞게 팀원과 합의해 최선의 방법으로 해결하는게 가장 중요하다." }, { "title": "Amazon Aurora 스토리지 엔진과 MySQL InnoDB 스토리지 엔진 비교", "url": "/posts/amazon-aurora-storage-with-innodb/", "categories": "Development", "tags": "database, MySQL, Amazon Aurora", "date": "2023-04-09 23:45:00 +0900", "snippet": "우리 회사를 포함해 많은 회사는 RDBMS를 사용할 때 MySQL Amazon Aurora DB(이하 오로라)를 사용하는 경우가 존재한다. 왜 오로라를 사용하는 지 궁금했는데 기존 전통적인 MySQL보다 가용성, 확장성, 연산 비용 등이 더 싸서 대규모 처리 작업에 용이해서 사용한다고 들었다. 또, 오로라는 컴퓨팅과 스토리지 인스턴스가 각각 분리되어 있다. 여기서 오로라 스토리지 엔진이 기존 MySQL 스토리지 엔진인 InnoDB와 큰 차이가 있는지도 궁금했었다.마침 Real MySQL 스터디에서 MySQL에서 대체로 쓰이는 스토리지 엔진인 InnoDB 스토리지 엔진의 구조에 대해 공부하면서, 영속성을 제공하는 DoubleWrite buffer 기능이 흥미로웠다. 언뜻보면 비효율적으로 보일 수 있는 연산으로 보였기 때문이다. 물론 옵션을 끌 수 있겠지만, 정합성 측면에서 끄는 것을 권장하지 않아 사용한다고 가정했을 때 더 효율적인 방법이 있을 지도 궁금했다. 그렇기에 스토리지 엔진을 개선한 오로라는 어떻게 효율적으로 처리하는 지 알아보자.Amazon Aurora오로라같은 경우 기존 전통적인 MySQL에 비해 더 좋은 퍼포먼스, 확장성, 가용성과 내구성을 제공한다. 이는 컴퓨팅과 스토리지 엔진을 분리해서 제공함을 통해 제공하며, 기존 MySQL 엔진과 InnoDB 스토리지 엔진을 커스터마이징해서 Aurora로 제공한다. Aurora Storage 엔진에서 더 좋은 퍼포먼스를 이야기할 때 특히나 I/O 연산 최적화를 다음과 같이 언급한다. Aurora는 비용을 절감하고 읽기/쓰기 트래픽을 위해 사용할 수 있는 리소스를 확보하기 위해 불필요한 I/O를 제거하도록 설계되었습니다. 쓰기 I/O는 안정적인 쓰기를 위해 트랜잭션 로그 기록을 스토리지 계층으로 푸시할 때만 사용됩니다. (중략) 기존 데이터베이스 엔진과는 달리 Amazon Aurora는 변경된 데이터베이스 페이지를 스토리지 계층으로 푸시하지 않으므로 I/O 사용을 좀 더 줄일 수 있습니다. 링크트랜잭션 로그 기록을 스토리지 계층으로 푸시한다는 의미는 무엇이고, 기존 InnoDB는 그렇다면 데이터를 스토리지 엔진으로 푸시하는 것일까? 이 두 개의 관점을 비교하면서 알아보자.InnoDB 스토리지 엔진 구조InnoDB 스토리지 엔진은 RDBMS에서 말 그대로 스토리지 디스크로부터 데이터를 잘 가져오는 역할을 한다. InnoDB 스토리지 엔진은 트랜잭션, 장애 복구, 락, 백업 등 여러가지 스토리지와 관련된 기능을 제공한다. 공식 문서에 나와있는 아키텍쳐는 다음과 같다.여기서 주의 깊게 봐야할 것은 DoubleWrite Buffer와 Buffer Pool이다. 여러 기능을 제공하지만, 버퍼 풀은 쓰기 지연 작업과 데이터 파일을 캐싱하는 역할을 하며, DoubleWrite Buffer는 데이터 정합성을 위해 버퍼풀에서 데이터 파일에 쓰기 작업을 하기 직전에 디스크에 따로 작성하는 로그이다.MySQL Doublewrite bufferinnodb의 버퍼 풀에서는 기본적으로 데이터 파일에 flush하기 전에 doublewrite buffer를 스토리지에 저장한다. 이는 데이터의 무결성을 위함으로, 버퍼 풀에서 데이터 파일로 쓰기 작업 실패가 발생할 때를 사용된다. 실패가 나게 되면, doublewrite buffer의 내용과 데이터 파일을 비교해서 다른 내용을 담고 있는 페이지가 존재하면 doublewrite buffer의 내용을 데이터 파일의 페이지로 복사하게 된다. 이를 통해 시스템의 비정상적 종료에도 무결성을 보장할 수 있게된다.예를 들어 다음 그림에서 innodb 버퍼 풀에서 데이터 파일에 쓰기 전에 먼저 DoubleWrite Buffer에 페이지를 작성하게 된다. 그 이후 버퍼 풀에서 flush를 진행해 데이터 파일에 쓰기를 진행한다. 만약 여기서 C 데이터 파일에 쓰는 과정에서 MySQL 서버가 종료되었다고 하면, 재시작할 때 forcing recovery로 인해 Doublewrite buffer의 내용이 해당 데이터 파일로 쓰기 작업이 일어난다.이를 통해 데이터 무결성을 보장할 수 있게된다.Doublewrite buffer 작업은 디스크에 실질적으로 쓰기 작업을 두 번 진행하는 것으로 볼 수 있다. 이에 대해 공식 문서에서는 2번의 쓰기 작업 I/O가 발생하지만 오버헤드는 2번 만큼 발생하지 않는다고 언급했다. 하지만 오로라는 이 Doublewrite buffer를 배제하는 방법으로 쓰기 작업을 진행한다. 즉, Doublewrite buffer를 작업하지 않으기에 표면적으로 봤을 떄 연산이 더 적다고 볼 수 있다.Amazon Aurora의 쓰기 연산 작업다음 그림에서 볼 수 있듯이, insert를 진행할 때 MySQL은 doublewrite buffer과 Datafile에 쓰기 작업을 진행하지만, Aurora 같은 경우에는 스토리지에 로그를 쌓는 것이 전부이다. 그 이후의 작업은 오로라 스토리지 내부적으로 작업을 진행하게 된다.Aurora같은 경우에는 스토리지 구조가 log-structured storage이다. 위 그림에서 볼 수 있듯 오로라 스토리지에 로그만 쌓고 쓰기 작업이 끝난다. 그렇기에 오로라의 스토리지 쓰기 작업은 위에서 인용구에서 언급했듯, 트랜잭션 로그 기록(리두 로그)을 스토리지 계층으로 푸시할 때만 쓰기 I/O가 발생한다. 그렇기에 DoubleWrite buffer가 없을 뿐더러 데이터 파일도 직접 쓰기 연산을 하지 않는다. 이는 리두 로그(WAL)만을 디스크에 쓰기 연산함으로 I/O 연산을 극단적으로 줄일 수 있다. 또, 로그 파일은 데이터 파일에 비해 상대적으로 데이터 크기가 작을 것이기 때문에 실질적으로 더 많은 데이터 파일을 적은 I/O로 쓸 수 있게 되는 것이다.언뜻 생각해보면 MySQL에서 리두 로그(WAL)를 모두 쌓지 않는 이유는 로그를 모두 쌓아서 연산함으로 데이터를 기록하게 되면 디스크 연산이 많아져 너무 느려져서라고 생각했었다. 하지만 오로라는 이를 Log Stream과 병렬 연산을 통해 연산 속도 문제를 해결했으며 그 관련 구조는 다음과 같다.글에 요지에 벗어나서 구체적으로 다루지는 않지만, 로그 데이터를 incoming queue에서 받아 update queue로 동기식으로 전달하고 그 이후는 비동기 및 병렬로 처리해 데이터 파일에 쓰기 작업이 일어나게 된다. 또, 이렇게 작업된 것들은 S3에 저장함으로 손쉽게 백업을 구현한다. 이를 통해 오로라가 I/O를 기존 RDS MySQL에 비해 더 좋은 성능을 낼 수 있게 되었다.디스크 I/O는 상대적으로 비싼 작업인 만큼 이는 수백, 수천만 I/O가 발생하는 서비스에서는 엄청난 차이를 만드며, 장기적인 관점으로 봤을 때 엄청난 비용 절감을 가져올 수 있다. 하지만 적은 I/O가 발생할 때는 오히려 RDS를 활용하는게 좋을 수 있다. 실제로 오로라의 최소 인스턴스(의 비용은 0.073 USD/h지만, RDS는 0.016USD/h이다. 또, DBMS가 AWS에 의존도가 높아진다는 단점이 존재한다. 결국 모든 기술에는 정답이 없는 만큼 현재 문제 상황에 맞는 데이터베이스를 적절히 고르는게 무엇보다도 중요할 것이다.###각주 데이터 파일에서 쓰기 작업이란 실제 레코드가 디스크에 존재하는 테이블에 저장된다는 의미입니다. 쓰기 연산이란 메모리에서 디스크로 직접 I/O 작업을 진행함을 의미합니다. 여기서 디스크는 SSD가 될 수도 HDD가 될 수 있습니다.참고 https://hoing.io/archives/1114 Real MySQL 1권, InnoDB 스토리지 엔진 구조 https://dev.mysql.com/doc/refman/8.0/en/innodb-doublewrite-buffer.html https://www.youtube.com/watch?v=7_VXMqYixS4 AWS Reinvent 2021: https://www.youtube.com/watch?v=SEXbvl2oQGs" }, { "title": "querydsl의 transform 메서드에서 발생하는 connection leak 현상", "url": "/posts/querydsl-connection-leak/", "categories": "Development", "tags": "Spring, querydsl", "date": "2023-03-12 23:30:00 +0900", "snippet": "문제 상황회사에서 모든 환불은 어드민 서버를 거쳐 환불 서버에 환불 요청을 보내 환불 프로세스가 진행된다. 하지만 환불 서버에서 요청을 제대로 보내고 환불을 완료했지만, 어드민 서버에서 히스토리를 DB에 기록하는 작업이 제대로 이뤄지지 않았다. 그렇기에 어드민 히스토리와 환불 기록의 불일치가 발생했고, 이 운영 이슈를 해결하는 과정을 남기려고 한다.실제로 핀포인트 로그는 다음과 같이 나타났다.대략적으로 보면 알듯, 모두 30초(혹은 그 이상)에서 오류가 발생한 이력이 있는데 이는 어딘가에서 Timeout이 발생했음을 알 수 있다. 더 구체적으로 들어가서 확인해보면 에러가 다음과 같이 발생했다. HikariPool-1 - Connection is not available, request timed out after 30000ms.해결 과정처음에 생각한 문제는 슬로우 쿼리가 커넥션을 오래 잡거나 배포 중 ECS 오토스케일링이 활성화되어 DB 커넥션이 부족해진 이유인 줄 알았다. 하지만 AWS 로그를 직접 확인해본 결과 당시 배포가 이뤄지지 않았으며 DB 커넥션 개수는 넉넉했었고 쿼리들도 문제가 없었다.그렇기에 인프라적 문제보다는 어플리케이션 서버의 문제라고 생각했고, 어플리케이션 로그와 코드를 살펴봤다. 그 결과 문제는 querydsl에서 transform() 메서드를 잘못 사용하고 있어 발생했음을 확인할 수 있었다. 실제로 문제 상황과 비슷한 상황을 개발 서버에서 재현했을 때 transform() 메서드를 동시에 호출할 때 여러 요청들이 쿼리가 실행된 이후에도 커넥션을 계속 물고 있었으며 hikari의 모든 커넥션을 물게 되면, 다른 요청들은 hikari로부터 커넥션을 대기하게 되고 타임 아웃(30초)가 발생했다.querydsl에서 transform() 메서드는 쿼리 결과를 grouping해서 Map으로 변환해주는 기능을 제공한다. 하지만 querydsl에서 query가 종료될 때 사용되는 메서드들이 queryTerminatingMehtods에 존재하는 메서드들이라면 JPA EntityManager를 close해준다. queryTerminatingMehtods에 존재하는 항목은 다음과 같다.querydsl에서 자주 쓰는 fetch()나 fetchOne()같은 메서드는 queryTerminationMethods가 위의 메서드 리스트에 존재한다.반면, 쿼리가 종료되는 메서드가 존재하는 ResultTransformer 인터페이스의 transfrom 메서드가 사용하는 query는 모두 iterate()로 종료된다. 따라서 queryTerminationMethods의 메서드가 아니며, EntityManager가 제대로 커넥션을 닫히지 않게 된다.해결 방법문제를 찾는 데에 비해 해결하는 방법은 굉장히 간단했다. querydsl transform() 메서드를 사용하는 쿼리에 @Transactional(readOnly=true)를 붙여주면 된다. querydsl도 결국 JPA를 기반으로 만들어진 라이브러리이며, @Transactional 을 갖는 메서드가 끝날 때 JpaTransactionManager 의 doCleanupAfterCompletion()을 통해 커넥션을 모두 정리해준다.아쉬운 점transform이 connection을 계속 물고 있어 오류가 발생하는 부분은 이해했지만, 결국에는 Hikari pool로 커넥션을 언젠가 돌려주게 된다. 이 돌려주는 원리가 hikari의 max-lifetime(기본 180초)로 인해 돌려주는 것인지, OS에 의해 좀비 스레드가 정리되는 것인 지 명확히 알아내지는 못했다.또, querydsl 5.0.0 기준으로 transform() 메서드에서 커넥션이 왜 반납이 안되는 지 내부 원리를 구체적이고 명확히 알아보려 한다. 관련해서는 다음 글에서 풀어내 보자.참고 https://github.com/querydsl/querydsl/issues/3089 https://colin-d.medium.com/querydsl-에서-db-connection-leak-이슈-40d426fd4337 https://cljdoc.org/d/hikari-cp/hikari-cp/3.0.1/doc/readme" }, { "title": "감정적 결정과 상황 귀인", "url": "/posts/%EA%B0%90%EC%A0%95%EC%A0%81-%EA%B2%B0%EC%A0%95%EA%B3%BC-%EC%83%81%ED%99%A9-%EA%B7%80%EC%9D%B8/", "categories": "Thinking", "tags": "Thinking, 회고", "date": "2023-03-06 01:01:00 +0900", "snippet": "근래에 감정적인 결정과 발언이 늘었다. 가진 환경에 대한 불만족 때문이었다. 모든 환경에는 장단이 존재하지만 비교와 기대 불일치로 인한 스트레스는 감정적인 결정을 유발했다. 이유를 분석해보고 왜 그랬는 지에 대해 생각해보자.The Conscious Discipline Brain State ModelThe Conscious Discipline Brain State Model에 따르면, 사람의 뇌는 Executive, Emotional, Survival로 세가지 활성 상태가 존재한다. Executive 상태는 문제 해결 능력과 학습을 할 때 활성화되는 영역이며, 이성적인 판단을 할 때 주로 사용한다. Emotional와 Survival 상태는 말 그대로 감정적인 상태에 돌입하게 되며 보호 본능이 발생하게 된다. 다시 말해, 흑백 논리와 극단적 사고를 하는 감정적인 판단을 하게 된다. 이는 생존을 위한 본능으로 생각할 수 있다.여기서 사람이 스트레스나 충격을 받기 시작하면 판단을 할 때 Executive 상태(전두엽)가 비활성화 된다. 이는 Emotional 상태와 Survival 상태가 활성화되어 사람이 감정적이고 이분법적으로 판단해 생각이 짧아지기 시작한다. 이러한 상태가 1달이넘게 지속되었으며 이를 스스로 빠져나오기 어려웠던 것 같다.어떻게 나는 알았을까?내가 처한 상황에 대해 감정적으로 접근하기 시작했다. 상황에 대한 단순히 좋고 나쁨을 판단하는 이분법적 접근이 늘었으며, 문제 상황을 해결할 수 있다는 낙관보다는 회의가 더 늘었다. 그러다보니 자연스럽게 책임을 상황으로 돌리기 시작했으며 이는 너무나도 스스로를 불행하게 만들었다. 또, 자기객관화 마저 잊어버리며 주변에서 이유를 찾았다.비교와 기대, 억한 감정개인적인 성격으로는 욕심이 많은 편이다. 기왕 하는거 조금 더 잘하고 싶어 스스로 동기부여를 많이한다. 이로 인해 내가 생각했던 기대하던 바가 컸다. 다만 현실은 생각한 것보다 우아하지는 못했고 투박했다. 내가 기대했던 것들과는 너무나도 달랐다. 그렇기에 타인의 “좋아보이는” 환경에 더 집중하고 이와 비교하게 되었으며 내 억한 감정은 더 커져갔다. 가장 아쉬운건 불행의 원인이 비교와 기대 관리에서 온다는 걸 스스로 알고도 계속 기대 관리를 실패하고 비교를 해나갔다는 점이다.이러한 요인으로 하지 말아야 할 실수와 당연한 실수를 반복하게 되었다. 이는 스스로를 불행하게 만들었고 성장에는 전혀 도움이 되지 않았다. 그리고 타인에게 많은 회의적이고 부정적인 감정을 노출했으며 나 뿐만 아니라 타인의 시간을 낭비하도록 만들었다.상황 귀인성장에 대한 욕심으로 상황을 탓하는 건 정말 좋지 못하다. 얻을 결과에 대해만 집중하면 가진 것보다 부족한 부분에 집중하게 된다. 그렇다면 내가 처한 환경이 어떤지 확인하고 지금 집중해야 할 중요한 것들을 파악해보는건 어떨까? 이를 통해 상황을 개선하는 방법에 대해 고민할 수 있으며 얻을 수 있는 점에 집중해볼 수 있다.예를 들어 회사의 레거시 코드에 아쉬움을 느낀다면 이 상황에서 어떤 점을 개선하면 좋을 지 함께 고민해보고 해결해나갈 수 있다. 이는 시스템 개선을 해본 성장할 수 있으며 팀에서 신뢰를 얻는데 도움이 된다. 또, 비슷한 상황에 대한 다른 관점도 찾아보는 점도 방법 중 하나이다. 하지만 상황에 대해 안 좋은 부분에만 집중하고 불평만 늘여놓는다면, 오히려 상황 귀인을 통해 스스로가 정체된다.좋아하는 웹툰인 찌질의 역사에서 실수에 대한 구절이 인상적이었는데 어른이 되어서도 여전히 실수를 하고 잘못을 저지르고 누군가에게 상처를 입히고…그럴 수 있어.그렇다고 그게 찌질한 게 아니야. 실수를 합리화하고 정당화하고… 권위와 노련함으로 약한 사람에게 뒤집어 씌우고… 자신은 고결한 척, 완벽한 척. 잘못을 부정하고 외면하고 그게 찌질한 거야 잘못을 저지른 기억은 괴롭지. 하지만 잘못을 고치려는 노력조차 하지 않았던 기억은 훨씬 더 너를 괴롭힐 거야.항상 부족함을 느끼지만, 본인의 잘못과 실수 인정할 수 있는 성숙한 어른이 되는건 정말 어려운 것 같다. 말로만 하는건 쉬우니 행동으로 옮길 수 있도록 스스로 꾸준하게 인지하자.참고 문헌https://consciousdiscipline.com/methodology/brain-state-model/https://carmelmountainpreschool.com/conscious-discipline-the-three-brain-states/" }, { "title": "중요한 일에 집중하기", "url": "/posts/%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%BC%EC%97%90-%EC%A7%91%EC%A4%91%ED%95%98%EA%B8%B0/", "categories": "Thinking", "tags": "Thinking, 회고", "date": "2023-02-12 22:50:00 +0900", "snippet": "중요한 일에 집중하기샘 알트만과 폴 그레이엄은 공통적으로 인생은 짧다라는 말을 자주 한다. 인생이 짧다라는 의미는 결국 본인에게 중요하지 않은 일보다 중요한 일에 더 집중하게 만들기 때문이다.그렇다면, 중요한 일은 무엇일까? 여러 방면으로 존재하겠지만, 근래에 가장 많이 시간을 사용하는 업무적인 부분에서 고민해보자. 진부하지만, 우선순위가 높은 일들과 임팩트가 큰 일들이라 생각한다. 그를 위한 역량으로는 결정은 시시각각 변하는 복잡한 시스템에서 최적의 결정을 할 수 있는거라 생각한다. 폴 그레이엄의 말을 조금 더 빌려보자면, 당신은 진짜 일이 어떤 것인지를 이해해야 하고, 당신이 어떤 일에 적합한지를 파악해야 하고, 그 일의 핵심에 가능한한 가깝게 접근해야 하고, 매 순간 당신이 할 수 있는 노력을 다하는지와 당신이 얼마나 잘하고 있는지를 정확하게 판단해야 하고, 결과를 해치지 않는 선에서 하루 몇 시간을 투자해야 하는지를 판단해야 합니다. 이는 모든 것이 연결된 매우 복잡한 방정식입니다. 하지만 매 순간 당신이 스스로에게 정직하고 스스로를 잘 판단할 수 있다면, 당신은 저절로 최적의 상태에 돌입하게 되며, 이 세상에 얼마 없는 생산적인 사람이 될 수 있을 것입니다.무엇보다 중요한 건, 이 우선순위 내에서 맡은 일을 차질 없이 꾸준하게 “잘” 해내고 팀의 신뢰를 쌓아나가는 거라고 생각한다.꾸준히 열심히 하는 점도 중요하지만, 잘하려고 노력하는 부분도 중요하다. 그렇기 때문에 개인적인 성장을 해야한다고 생각한다. 그리고 복리 효과(Compound Effect)를 나는 강력하게 믿고 있기 때문에 지금 더 많이, 그리고 이전의 나보다 훨씬 더 나아지려 최선을 다하고 있다.업무 외적으로 중요한 부분은 소중한 관계, 여유, 업무 외 이뤄보고 싶은 성취 같은 것들이라 생각한다. 개인적으로는 주변에 있었던 좋은 사람들 덕분에 취업을 할 수 있었고, 금전적 여유를 통해 더 많은 자유가 생겼다. 그렇기에 20대 초반까지는 금전적인 고민을 줄이면서, 스스로에게 투자를 많이 해보고 싶다. 이를 바탕으로 주변 사람들에게 더 많은걸 배풀고 싶다. 좋은 사람이 되는 것은 너무나도 어렵지만, 더 많은 것들을 배우고 시행착오를 겪어보며 더 많은 사람에게 선한 영향력을 줄 수 있도록, 그리고 조금 더 낙관적이고 싶다.또, 바쁘다라는 이유로 주변에 소중한 사람들에 소홀해지는 것도 다시 생각해보면, 정말 나에게 중요하지 않은 일을 하고 있을 때도 있었던 것 같다. 하루에 할 수 있는 가용성은 제한되어 있으며, 가용성을 넘어서 비효율적이더라도 무언가를 하려는 습관에 대해 되돌아 볼 필요성을 느끼게 되는 대목이다.아직 하고 싶은 것과 나에게 잘 맞는 게 무엇인 지 구체적으로 모르겠지만, 분명히 온전히 몰입하고 성취하고 싶은게 생길거라 생각한다. 그게 우연이든, 필연이든 기회가 왔을 때 잘 소화하기 위해서는 복리 효과로 쌓아놓은 자산들(학습 능력, 네트워크 등)이라고 생각한다. 그렇기에 더 최선을 다할 것이며, 더 성장하고 싶다.또, 성장의 일환으로 글또 활동을 참여하게 되었다. 글을 작성할 때 가장 큰 어려움은 꾸준함과 피드백 받기이다. 이를 글또 활동을 통해 잘 보완할 것이라 생각한다. 글또에 지원 동기에서, 감명받는 글을 더 많이 접하고 싶어서도 있었다. 나에게는, 샘 알트만, 폴 그레이엄, 샌드버드 김동신님의 블로그가 있다. 실제로 글또 슬랙에 올라오는 글 중에 인사이트가 많은 글들도 있어 너무 감사하며, 글또 활동을 꾸준히 이어나가고 싶다." }, { "title": "[Spring] Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기", "url": "/posts/domain-event-1/", "categories": "Development", "tags": "Spring, SW 마에스트로", "date": "2022-11-13 12:55:00 +0900", "snippet": "Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기Goal 업적 달성 시, 데이터베이스에 유저의 업적 달성을 기록한다. 업적 달성은 편지 송신, 좋아요와 같은 유저의 행위가 발생한 후 조건을 만족하면 이뤄진다. 본 글에서는 유저가 편지를 작성할 떄, 이벤트를 발행해 유저의 업적을 달성하는 상황에 대한 코드를 다룹니다.Solution 도메인 이벤트를 활용해 업적 조건을 만족시키는 것을 이벤트 리스너로 추적한다. 이를 통해 서로 다른 도메인의 결합도를 낮춘다.문제 상황업적 달성은 데이터베이스의 유저의 정보 변경이 일어날 때 발생한다. 이는 곧, 유저 테이블에서 Insert와 Update 작업이 발생할 때, 업적 달성이 이뤄진다. 그렇기에 이 작업들에 대해 추적을 해야할 것이다. 여러가지 방법이 존재하겠지만, 이를 코드에서 추적할 수 있다면 좋겠다고 생각했고, 본 프로젝트에서는 Spring data를 사용하고 있어 관련된 해결책을 모색했다.가장 첫 번째로 생각이 들었던건 단순히 비즈니스 로직을 처리하는 서비스 레이어에서 업적이 일어날 때마다 분기를 넣어 해결하는 방법인데, 이는 서로 다른 도메인의 강결합이 일어나는 문제가 존재했다. 편지 서비스 계층에서 편지 도메인의 편지라는 엔티티가 생성될 때, 유저 도메인의 유저 업적 엔티티를 생성하게 된다면 편지 도메인과 유저 업적 도메인이 강결합을 갖게 된다. 이렇게 다른 컨텍스트에 존재하는 도메인의 결합이 된다면, 추후 편지 작성이 아닌 다른 도메인에 해당하는 행위에 대한 업적을 생성할 때도 해당하는 엔티티와 유저 업적 엔티티는 강결합을 갖게 된다. 이는 극단적으로 유저 도메인과 다른 모든 도메인이 의존관계를 갖게 되는 잠재적 위험성이 있다.그렇기 때문에 이러한 결합을 끊고, 스프링에서 제공하는 좋은 기능인 Domain Event를 발행해 해결한 사례를 공유합니다.Domain Event란?도메인 객체에서 어떤 작업이 실행됬을 때, 발행할 수 있는 이벤트를 의미한다. 이를 통해 객체의 생성이나 변경을 다른 객체와 결합 없이 Event Linstener를 통해 추적할 수 있다. 이를 통해 얻을 수 있는 장점은 다음과 같다. 서로 다른 도메인 로직이 섞일 일이 없다. 확장할 때 발행한 이벤트에 대해 추적하는 Listener만 추가해주면 되기 때문에 확장에 용이하다. 이벤트 발생 이후의 작업(이벤트 리스너의 작업)을 비동기로 처리할 수 있다.스프링에서 기본적으로 제공하는 ApplicationContext는 이벤트를 발행할 수 있기 때문에, 이를 활용해 만들어진 도메인 이벤트를 활용하는데 어렵지 않게 사용할 수 있다.코드AggregateRoot먼저, spring data에서 제공하는 AbstractAggregateRoot&lt;A&gt; 를 활용한다면, 도메인 이벤트를 어렵지 않게 구현할 수 있다. AbstractAggregateRoot 는 domain event를 간편하게 발행할 수 있도록 만든 모듈이다. 이는 이름에서 볼 수 있듯, 도메인 이벤트를 발행하는 주체는 DDD의 AggregateRoot가 된다는 의미를 내포하고 있다.Aggregate는 관련 객체를 하나로 묶은 군집을 의미하며, AggregateRoot는 군집 내에서 여러 객체들을 관리하는 루트 엔티티이다. 일반적으로 하나의 엔티티와 여러 개의 Value Object(값 객체)를 지니고 있으며, 하나의 Aggregate에 속한 객체는 같은 라이프사이클을 지닌다. Aggregate를 통해 간의 관계를 확인한다면, 더 상위 수준에서 도메인 간의 관계를 파악하는데 수월해진다.본론으로 AbstractAggregateRoot&lt;A&gt; 를 이벤트를 발행할 Entity에 상속시키면 된다. 여기서 제너릭 타입(&lt;A&gt;)은 Entity의 타입이 된다. 이를 통해 registerEvent(event) 메소드를 상속받을 수 있으며 이 메소드를 통해 이벤트를 등록할 수 있다.public class AbstractAggregateRoot&lt;A extends AbstractAggregateRoot&lt;A&gt;&gt; { private transient final @Transient List&lt;Object&gt; domainEvents = new ArrayList&lt;&gt;(); /** * Registers the given event object for publication on a call to a Spring Data repository's save methods. * * @param event must not be {@literal null}. * @return the event that has been added. * @see #andEvent(Object) */ protected &lt;T&gt; T registerEvent(T event) { Assert.notNull(event, "Domain event must not be null!"); this.domainEvents.add(event); return event; } ...}이를 통해 편지 도메인 코드에 반영을 하면, 다음과 같다.@Entity@NoArgsConstructor(access = AccessLevel.PROTECTED)@Getterpublic class Letter extends AbstractAggregateRoot&lt;Letter&gt; { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Lob private String content; private LocalDate sendDate; private boolean isRead; @ManyToOne(fetch = FetchType.LAZY) private User sender; ... @PostPersist public void created() { this.registerEvent(new LetterCreatedEvent(this.id, this.sender.getId(), this.sender.getUserAchievement().getSendLetterCountValue(), this.receiver.getReceiveCount())); }}여기서 @PostPersist 를 통해 이벤트를 발행했는데, 이는 JPA 엔티티의 라이프사이클에서 영속화가 된 이후에 이벤트를 등록하려 했기 때문에 다음과 같이 진행했다. 이는 서비스 계층에서 특별히 호출을 안해도 될 뿐더러, PK값 정책이 IDENTITY이기 때문에 영속화가 된 이후에 키 값을 받은 상태로 이벤트를 발행할 수 있다.@EventListener@EventListener를 통해 선언적으로 이벤트를 처리할 수 있다. 이를 통해 이벤트를 받는 코드를 작성하면 다음과 같다. 뿐만 아니라, 특정 조건을 만족하려 한다면 @EventListener(condition) 을 사용한다면 간편하게 활용할 수 있다.@Component@RequiredArgsConstructorpublic class AchievementPolicy { private final UserRepository userRepository; private final AchievementRepository achievementRepository; private final LetterRepository letterRepository; ... @EventListener public void achieveLevelTwo(LetterCreatedEvent letterCreatedEvent) { Long userId = letterCreatedEvent.getUserId(); if (userRepository.existsById(userId) &amp;&amp; letterRepository.existsById(letterCreatedEvent.getId())) { achievementRepository.save(new Achievement(LEVEL_TWO.getLevel(), LEVEL_TWO.getName(), LEVEL_TWO.getTag(), userId)); userRepository.increaseUserPoint(userId, LEVEL_TWO.getPoint()); } } @EventListener(condition = "#letterReadEvent.read == true") public void achieveLevelThree(LetterReadEvent letterReadEvent) { Long userId = letterReadEvent.getUserId(); if (userRepository.existsById(userId) &amp;&amp; letterRepository.existsById(letterReadEvent.getId())) { achievementRepository.save(new Achievement(LEVEL_THREE.getLevel(), LEVEL_THREE.getName(), LEVEL_THREE.getTag(), userId)); userRepository.increaseUserPoint(userId, LEVEL_THREE.getPoint()); } }}테스트이벤트 발행에 대한 테스트는 @*RecordApplicationEvents* 옵션을 활용할 수 있다. 이는 단일 테스트 실행 시 발행되는 어플리케이션 이벤트를 ApplicationEvents 라는 객체에 저장된다. 공식 문서에는 다음과 같이 나타나 있다. @RecordApplicationEvents is a class-level annotation that is used to instruct the Spring TestContext Framework to record all application events that are published in the ApplicationContext during the execution of a single test.The recorded events can be accessed via the ApplicationEvents API within your tests.이에 대한 코드를 작성하면 다음과 같다.@RecordApplicationEvents@SpringBootTest@ActiveProfiles("test")public class LetterEventTest { @Autowired private ApplicationEvents applicationEvents; @Autowired private LetterService letterService; @Autowired private UserRepository userRepository; private User sender; private User receiver; @BeforeEach void setup(){ sender = userRepository.save(Fixtures.UserStub.defaultGoogleUser("gmail@gmail.com")); receiver = userRepository.save(Fixtures.UserStub.defaultGoogleUser("receiver@gmail.com")); } @Test void letter_created_event_test(){ letterService.writeLetter(new LetterRequest("content", List.of()), sender); letterService.writeLetter(new LetterRequest("content", List.of()), sender); assertThat(applicationEvents.stream(LetterCreatedEvent.class).count()).isEqualTo(2); }}하나의 편지를 작성할 떄(DB에 편지를 저장할 떄) 정상적으로 이벤트가 발행되는 것을 볼 수 있다.아쉬운 점현재 EventListener 내 작업은 트렌젝션이 보장되어야 한다. 이에 대해 이벤트 리스너의 트랜잭션을 어떻게 처리할 지 알아보면 좋을 듯 싶다.레퍼런스" }, { "title": "[Testing] Mockito를 이용한 Service Layer Unit Testing", "url": "/posts/Mocking-test/", "categories": "Testing", "tags": "Testing", "date": "2022-10-16 12:02:00 +0900", "snippet": "Testing에 관한 고찰일반적으로 스프링으로 테스트를 작성할 때, @SpringBootTest를 이용해 통합테스트와 인수테스트를 진행했습니다. 이를 이용해 테스트를 진행하면, 개별적으로 테스트를 실행하기에도, 전체를 테스트를 실행하기에도 너무 속도가 느리다는 단점이 있었습니다. 뿐만 아니라, 테스트 간의 격리성을 확보하기 위해서 모든 데이터를 지우는 과정에서 DataIntegrityException 이 자주 발생해 어려움이 있었습니다.이를 위해서 격리가 쉽고 빠른 테스트를 진행할 수 있는 테스트 방법에 대해 알아보았고, 마침 Mock을 이용한 Service Layer에 대한 단위테스트를 진행해보고 느낀 경험을 공유합니다.Mockito를 이용한 Service Layer의 Unit Test저희의 ArgumentResolver에서 로그인한 유저를 조회하기 위해 사용하는 Service의 메소드 중 하나는 다음과 같습니다. jwtProvider를 통해 decode를 진행한 후에, email을 통해 user를 조회하게 됩니다.@Service@RequiredArgsConstructorpublic class UserService { private final UserRepository userRepository; private final JWTProvider jwtProvider; ... public User loginUser(String token) { String email = jwtProvider.decodeJWTToSubject(token); returnuserRepository.findUserByEmail(email).orElseThrow(NoSuchRecordException::new); }}이를 Mockito를 사용해 Service Layer의 의존성을 격리해 테스팅을 진행하면 코드는 다음과 같습니다.@ExtendWith(MockitoExtension.class)public class UserServiceMockTest { private static final String ACCESS_TOKEN = "accessToken"; private static final String EMAIL = "swm.team.goyukkuri@gmail.com"; private static final String TOKEN = "token"; @Mock private UserRepository userRepository; @Mock private JWTProvider jwtProvider; @InjectMocks private UserService userService; private User googleUser; @BeforeEach void setup(){ googleUser = Fixtures.UserStub.defaultGoogleUser(EMAIL); } ... @Test void user_google_login_test() { when(userRepository.findUserByEmail(anyString())).thenReturn(Optional.of(googleUser)); when(jwtProvider.decodeJWTToSubject(TOKEN)).thenReturn(EMAIL); User user = userService.loginUser(TOKEN); assertThat(user.getEmail()).isEqualTo(EMAIL); verify(userRepository, times(1)).findUserByEmail(anyString()); verifyNoMoreInteractions(userRepository); }}UserService를 위한 의존관계인 UserRepository와 jwtProvider를 주입하려면, 먼저 Mocking을 진행해야 합니다. @Mock를 통해 가짜 빈을 넣어줄 수 있으며, 실제 구현된 객체와 무관하게 작동하게 됩니다. 다시말해 userRepository의 메서드들이 껍데기만 존재할 뿐, 구현체가 없게 됩니다. 만약 실제 객체를 주입하고 싶으면 @Spy 를 이용하면 됩니다.의존 관계를 다 Mocking을 했다면, @InjectMock 을 통해 의존관계를 주입할 수 있습니다. 이를 통해 격리의 주체를 userService로만 둘 수 있게 되고, 다른 실제 객체들의 의존성이 모두 제거됩니다.이후 when() 을 이용해서 Mock 객체의 메소드의 결과를 어떻게 설정할 것인지 정해준 후, userService.loginUser 메소드를 실행하게 됩니다. 만약 jwtProvider 혹은 userRepository 같은 Mock 객체의 메소드를 정의를 안해준 것이 있다면, 실행할 수 없게 됩니다.이후 verify 를 통해 Mock 객체의 행위에 대해 검증해볼 수도 있습니다. 뿐만 아니라, when() 으로 설정한 것 이외의 행위가 있었는 지도 verifyNoMoreInteractions 를 통해 확인해볼 수 있었습니다.Mock Unit Test의 장점 속도SpringBootTest보다 압도적으로 속도가 빠르다는 점은 비교 불가한 장점이었습니다. 이를 통해 피드백을 더 빨리 받을 수 있었고, 빌드 시간도 단축할 수 있을 것이라는 생각이 들었습니다. 이는 곧 개발 생산성 향상으로 이어질 것입니다. unit testing을 통해 코드의 품질 확인Mock을 사용해 다른 의존성들을 테스트 대역으로 사용하니, 내부 구현에 대해서 모두 Mocking을 진행해야 했습니다. 이는 내부 구현을 한번 더 확인해보게 되었으며, unit testing이 어려워지는 객체는 Mocking을 많이 진행해야 하고 고려해야 할 부분이 많아졌습니다. 이는, 메소드의 결합도가 높아졌다는 것을 알 수 있었습니다. 이는 통합테스트만으로는 알기 어려웠습니다. 상대적으로 편리한 테스트 격리Mockito를 통해 빈으로 주입받는 의존성을 모두 Mocking을 통해 테스트 대역을 편리하게 만들 수 있었습니다. @SpringBootTest도 @MockBean 을 통해 가능했지만, 이는 통합테스트 환경에서 특별한 의존성이 아닌 것들을 Mocking을 하는 것은 시스템 간의 상호작용을 확인하기 어려워지기 때문에 적절치 못하다고 생각했습니다.Mock Unit test의 아쉬운 점 구현 세부에 대해 굉장히 잘 알아야하며, 유지 보수에 대한 부담이 커집니다.모든 repository가 무엇이 사용되는 지 알아야 하고 이에 대한 return 값을 일일히 정해줘야 합니다. 이 부분은 관리 포인트가 많아진다는 단점을 지니고 있습니다. 뿐만 아니라, 인가 정책이 바뀌어 jwt가 아닌 session으로 바뀐다면, 테스트에 jwtProvider 절을 변경해야 할 것입니다. 이는 통합테스트에 비해 변경에 취약하게 됩니다. 영속성 전이 테스트의 어려움저희 프로젝트의 도메인 서비스 로직에서 프로필 정보를 입력하는 다음과 같은 로직이 존재합니다.@Transactionalpublic voidupdateUserProfile(User user, ProfileRequest request) { Profile profile =newProfile(request.getNickname(), request.getGender(), request.getAge(), request.getJob(), user); profile.addPsychologicalExam(request.getPsychologicalExams()); user.updateProfile(profile);}본 로직은 영속성 전이(CASCADE.ALL)를 통해 프로필 정보에 대한 생명주기를 하나로 뒀습니다. 이는 Repository를 구현해 save를 하는 것은 객체 지향적이지 못하다고 생각해 위와 같이 구현했습니다. 이 같은 경우, 실제 쿼리가 어떻게 나가는 지에 대해 확인이 필요하다고 생각합니다. 다만, Mockito를 통해 unit testing을 진행한다면, 영속성 전이가 잘 이뤄졌는지에 대해서 확인이 어려울 것입니다. 뿐만 아니라, 전반적인 트랜잭션에 대한 테스트도 어려울 듯 싶습니다.결론결론적으로 은탄환은 없다고, Mocking을 이용한 방법이 속도는 빠르지만 여러 단점이 존재했던 것 같습니다. 결국 각각의 장단점을 명확하게 인식하고 문제 상황에 맞게 조직에서 합의한 기준을 바탕으로 테스트를 잘 작성하는 것이 중요한 것 같습니다. 마지막으로, Mocking에 대한 여러 견해가 존재하는데 특히 테스트의 고전파와 런던파에 대한 마틴 파울러의 글(본 글의 테스트는 런던파에 해당합니다)을 읽어보는 것도 좋을 듯 합니다.Mock Test 작성에 도움이 된 글Unit Testing the Service Layer of Spring boot Application책블라디미르 코리코프, 단위테스트, 생산성과 품질을 위한 단위테스트 원칙과 패턴" }, { "title": "[Thinking] UC Berkeley를 마치며 느낀 부족함 그리고 강점의 중요성", "url": "/posts/Berkeley-review/", "categories": "Thinking", "tags": "Thinking", "date": "2022-09-07 02:02:00 +0900", "snippet": "지난 6월 말부터 8월 중순까지 두달 간 UC Berkeley에서 교환학생으로 학업을 진행했다. UC Berkeley에서 UI Development 수업(CS160)과 Operating System and System Programming(CS162) 수업을 수강했다. 두 수업다 upper division, 주로 junior ~ senior 학생들이 많이 듣는 수업이었는데, 도전을 해보자는 목적으로 조금 빡빡하게 과목을 신청했다. 실제로 수업 로드도 빡세고 영어라는 언어적 장벽으로 다른 사람들이 사용하는 시간보다 1.5배는 적어도 더 사용해야 원활하게 업무를 진행할 수 있었다.UC Berkeley에서 느낀 부족함학습적인 측면에서도 부족함을 많이 느꼈지만, 특히나 팀 프로젝트에서 느낀 점이 너무 많았다. 우선, 너무나도 성장 속도가 빠르다는 점이었다. 팀적으로는 이점이지만, 내 개인과 비교했을 때 배우는 속도가 적어도 1.5배 가량 차이가 났다. 이 뿐만 아니라 질문도 굉장히 뛰어나며, 두려움이 없다. 분위기 또한, 누구나 모를 수 있다는게 기본 전제였다. 이를 통해 이후에는 본인의 이해를 바탕으로 명쾌하게 설명까지 할 수 있었으며, 팀적인 기여에도 좋은 퍼포먼스를 보였다. 22년 동안 모르는 것을 혼자 찾아보며 해결한 나에게는 속도적인 측면이건, 방법론적인 측면이건 충격으로 다가왔다. 한 번도 그래보지 못했기 때문이다.프로젝트 경험을 통해 코드를 어느 정도 잘 짠다고 생각했었고, GIST에서 System Programming 수업을 들어 OS적인 내용도 미리 많이 사전에 준비해 간다고 생각했었다. 뿐만 아니라, 버클리에서도 열심히 수업듣고 여가 시간 없이 하루의 대부분을 투자했음에도, 버클리 학생들이 훨씬 더 잘했다. 내가 학기 동안 아무리 열심히해도 이들에게 닿기 어려웠다. 너무나도 우물 안의 개구리였다고 느꼈고, 잘한다고 생각했던 것 조차 부끄럽게 다가왔다.이 경험은 내게 너무나도 충격적으로 다가왔고, 자신감을 크게 잃었으며 근 몇달간 고민으로 이어졌다. 다행히도 이 고민을 먼저 느끼셨던 여러 멘토님들이 존재했었고, 그 답변들은 고민을 해결하는데 큰 도움이 되었다.다양한 사람들의 조언먼저, 미국에서 SW마에스트로 활동을 통해 실리콘밸리에서 커리어를 이어오셨던 한기용 멘토님께서 흔쾌히 산호세에서 만나주셨다. 관련된 상황을 말씀드렸고, 비교가 나를 갉아먹을 정도 가면 너무나도 불행해지고 과거의 본인과 비교하는게 중요하다고 하셨다. 그리고 분명히 내가 가진 강점이 있을거기 때문에 이를 빨리 찾아내고 발전해서 임팩트(결과)를 내는 것이 중요하다고 말씀주셨다. 일을 잘하는 것과 학습을 잘하는 것은 다르기 때문이다. 관련해서도 글을 작성해주셨다.이 글을 통해 다시 한번 이야기를 나눴던 것을 상기할 수 있었고, 위로받을 수 있었다. 너무나도 멘토님께 감사드리며, 이 기억은 아마 평생 잊지 못할 듯 싶다.재학 중인 심리학 교수님께서도 수업시간에 지능에 관해 이야기를 해주시면서, 미국에 가면 주변의 동기들 때문에 자괴감을 많이 느끼셨다고 하셨다. 정확히 나와 일치해 질문을 드렸다. 답변은 결국에는 조금 더 많이 준비하고 시간을 사용했고, 그러다보니 조금 익숙해지면서 3~4년차부터는 어느 정도 맞춰나가실 수 있으셨다고 말씀주셨다. 이 답변은 굉장히 정직하고 명확했기 때문에 와닿았다. 부족하면, 간단하게 조금씩 더 하면 되는 것이다.뿐만 아니라, 조대협 멘토님께도 우연히 질문할 기회가 생겨 내 경험에 비추어 질문을 드렸었는데 비슷한 경험을 인생에서 두 번 느끼셨고 그 경험들은 평생 못잊으실 경험이라고 말씀주셨다. 실제로 멘토님께서도 자바 커뮤니티를 운영하셨고, 자바를 어느 정도 잘하신다고 생각하셨는데 외국계 회사에 커리어를 진행하시면서, 주변 사람들의 굉장히 높은 수준에 좌절감을 느끼셨다고 하셨다. 너무나도 분함을 느끼셨다고 하셨는데 너무나도 공감이 많이 되었다.그래서 그 상황에서 멘토님께서 생각하셨던 강점을 찾으려 하셨고 그 강점은 더 많이 시간을 사용하는 것이었다. 그로 인해, 멘토님께서는 기존보다 더 많은 업무를 먼저 도맡아 진행하셨고, 이를 바탕으로 회사에서 시스템 장애를 가장 많이 해결하신 사람이 되셨다고 하셨다. 구글에서도 세계에서 탑급 인재들을 보면서 생각을 많이 들으셨다고 하셨는데, 기존에 해결하신 방법과 동일하게, 강점을 바탕으로 입지를 다져나가셨다고 하셨다.강점의 중요성결국 이에 관해 고찰해본 결과, 현재 위치와 무관하게 누구나 다 느끼는 감정이며, 극복 방법도 대부분 비슷하다는게 가장 놀라웠다. 이 감정은 좋든 나쁘던 인생에 중요한 영향을 누구에게나 주었다. 결국 극복해야 하고, 극복하기 위해서는 본인의 강점을 잘 살려 그 강점을 바탕으로 자신감을 찾아야한다. 분명히 사람마다 본인이 가진 강점이 있을 것이고 그 강점을 최대한 경쟁력있게 가져갈 수 있는 사람이 되는게 중요하다고 느꼈다. 이를 통해 작은 자신감을 더 찾아나가고, 더 좋은 결과를 내면 된다.이와 더불어 결국 잘한다는 기준은 끝도 없기에 비교에도 끝이 없다. 특히 나는 압도되는 격차에 좌절감을 많이 느껴 자신감을 많이 잃었다. 하지만 시간이 지나고 조금 나아졌고, 끝없는 성장의 필요성과 겸손함을 배웠다. 그렇기에 부족함을 느끼는 데에 적당한 동기부여로서 활용하는 것은 성장에 도움이 될 수 있지만, 나를 갉아먹을 때까지 가는 것은 위험다고 느꼈다.결국에는 지금 가진 것들에 대한 감사함, 그리고 잘할 수 있는게 무엇인지 찾아가는게 중요하다. 굳이 따지면, 항상 노력으로 성취를 이뤄왔기 때문에 조대협 멘토님과 비슷하게 무언가에 대한 집착과 몰입이 지금 갖고 있는 나의 강점이라고 생각한다. 아직 모호한 부분이지만 이를 바탕으로 가시적인 강점까지 이어질 수 있기를 바라며, 지금까지 노력해온 나에게 감사하다고 말하고 싶다." }, { "title": "[Spring] spring data jpa에서 save 내부 원리", "url": "/posts/Spring-data-jpa-save/", "categories": "Spring Boot Development", "tags": "Development, spring data jpa, spring", "date": "2022-08-24 17:32:00 +0900", "snippet": "문제 @Test void retrieveInboxAllLettersTest() { Letter letter = new Letter("content", user); letter.send(user1); letterRepository.save(letter); letterRepository.save(letter); letterRepository.save(letter); List&lt;InboxLetterResponse&gt; inboxLetterResponses = letterService.retrieveInboxAllLetters(user1.getId()); assertThat(inboxLetterResponses.size()).isEqualTo(3); }전체 편지를 조회하는 로직에서 3개를 insert한 후 이를 전체 조회해서 size를 비교하는 테스트코드를 작성하던 중 실패했습니다. 결과는 다음과 같이 나왔는데 편지에 대해서 insert쿼리가 1번만 발생한 것으로 보입니다. 실제로 전체 조회 후 size를 조회하면 1개가 나왔습니다.기본적으로 save가 identical하게 작동하고, flush를 진행하지 않아 발생한 것으로 유추되었는데 계속 실수가 발생하는 부분이기에 내부 동작 원리를 직접 확인해보겠습니다.JpaRepository의 구조기본적으로 JpaRepository 는 다음과 같은 구조를 갖고 있습니다. 여기서 save는 CrudRepository 에서 인터페이스를 제공합니다. 참고로 PagingAndSortingRepository는 docs에서 나와있듯 CrudRepository 의 Extension으로, pagination과 sorting을 이용한 조회 method를 제공합니다.CrudRepositoryCrudRepository에서 save는 다음과 같이 인터페이스를 제공합니다. 여기서 docs의 설명이 굉장히 중요한데, 인스턴스가 변했을 수 있으므로, 저장 작업(insert query)이 save에서 return된 인스턴스를 사용한다고 되어있습니다. 제가 겪은 이슈에서는 이 return된 instance가 중복된 것으로 유추할 수 있습니다.구현체인 SimpleJpaRepository로 들어가보면 다음과 같습니다.만약 identity가 새로운 entity라면 persist한 후 entity를 리턴하는데, 그렇지 않다면 merge를 진행하게 됩니다. merge를 진행하면 Persistent Context에 객체가 추가되지 않기 때문에 이후에 save가 진행되는 객체가 insert되지 않게됩니다.디버깅을 통한 실제 작동 확인실제로 문제를 파악하기 위해서 디버깅을 진행해보면. 먼저 save가 실행 지점에 breakpoint를 잡고 이후 내부 동작을 확인하기 위해 Spring-data-jpa의 jar에 들어가서 SimpleJpaReposiotry 에서 breakpoint를 잡으면 다음과 같습니다.디버깅을 실행하면 다음과 같습니다.첫 번째 save두 번째 save두번 째에 대해서는 Persistent Context에 이미 존재하기 때문에 merge를 진행하게 됩니다. 이는 곧 identity가 똑같은 객체가 변경되었을 때 Persistent Context에 적용되고 이후 flush를 진행하면 쿼리가 발생하게 됩니다.뿐만 아니라 해결책으로 saveAndFlush 를 사용하면 되지 않나? 싶어서 진행했는데도 통과하지 않았습니다. 이는 flush는 Persistent Context를 지우는 것이 아니라, Persistent Context의 내용을 DB에 쿼리를 날려 반영하는 것인 것도 놓쳤던 것 같습니다. 결국에는 identity가 다른 객체를 각각 만들어 save를 진행해 해결할 수 있었습니다.@Testvoid retrieveInboxAllLettersTest() { for (int i = 0; i &lt; 3; i++) { Letter letter = new Letter("content", user); letter.send(user1); letterRepository.save(letter); } List&lt;InboxLetterResponse&gt; inboxLetterResponses = letterService.retrieveInboxAllLetters(user1.getId()); assertThat(inboxLetterResponses.size()).isEqualTo(3);}굉장히 기본적인 내용인데 생각보다 놓치기 쉬운 부분이기에 글을 작성합니다. JPA에서는 객체의 Identity(객체의 id값)를 통해 새로운 객체임을 구분하고 save메소드에서는 이를 기준으로 insert가 작동하기 때문에 이를 유의해야 할 것 같습니다." }, { "title": "[Thinking] MSA에 대한 단상", "url": "/posts/Choosing-Architecture-for-developer/", "categories": "생각", "tags": "Development, Thinking", "date": "2022-08-09 18:39:00 +0900", "snippet": "SW 마에스트로 과정에서 너무나도 영광스럽게 조대협 멘토님의 k8s 멘토링을 4회 가량을 듣게 되었다. 오늘이 첫번째 강의였고 들으면서 기술에 대한 깨달음이 꽤나 커서 글을 작성한다. 뿐만 아니라 블로그에 좋은 글을 써야 한다는 강박이 계속되어 지금이라도 작은 글들을 조금씩 써나가는 연습을 해보려 한다.k8s를 배우기 앞서 k8s가 가장 잘 활용될 수 있는 구조인 MSA와 그 기반이 되는 Container에 대해서 먼저 다뤘다. 여기서 누차 강조하는 것은 기술을 배우기 전에 그 Background를 알고 그 기술을 왜 사용하는지에 대한 의문을 꾸준히 제기해야 한다는 것이다.먼저 MSA를 왜 사용하는가?에 대한 질문을 답한다면, 조직마다 사용하는 이유는 각기 다르겠지만 기능 단위로 나눈다는 점이었다. 나는 기능 단위로 나눈다는 것을 통해 가용성을 높이는 것(어떤 단위의 서비스가 죽어도 다른 서비스는 정상적으로 작동한다는 의미)로서 가장 크게 받아들였다. 이를 통해 서비스를 최대한 안정적으로 돌릴 수 있다는 장점이 가장 먼저 떠오르는 이유였다. 나름 타당한 이유이지만, 멘토링 시간에 들은 내용으로는 기능 단위로 독립적으로 분리함을 통해 그 기능을 구현하는 조직의 의사결정을 명확히 위임할 수 있다는 측면이었다. 이는 곧 의사결정의 속도가 빨라진다는 것이고, 생산성 향상으로 이어져 소프트웨어를 개발하는 방법론으로서 굉장히 좋은 방법일 수 있었다.관련한 법칙으로 Conway’s Law도 소개해주셨다. 이는 다음과 같다. Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.조직의 communication structure가 곧 조직의 system의 design에서 나타난다는 것이다. 만약 조직의 커뮤니케이션 구조가 복잡하다면 조직의 system design도 복잡해진다는 의미이다. 즉, 위계 조직과 같이 복잡한 커뮤니케이션 구조에서는 MSA를 사용한다고 하더라도 그 의미에 맞고 적합하게 사용하고 있지 못할 수 있다는 말이다. 결국 아키텍쳐의 가장 큰 원칙은 DDD와 같이 MSA를 잘 만드는 방법론보다도 팀의 구조에 맞춰 설계를 하는 것이 가장 중요함을 느낄 수 있었다.이런 생산성 증대에 있어 궁금한 점이 있어 “그렇다면 MSA를 진행하는데 생산성 향상에 이를 수 있을 정도의 팀의 규모는 어느 정도인가?”에 대한 질문을 드렸다. 기능이 independent적인 관점에서 혼자 진행할 수도 있다고 했고 실제로 미국의 많은 스타트업에서는 작은 스타트업들도 MSA를 사용한다고 한다. 현업에서는 일반적으로 2-pizza 법칙으로 팀당 8명 내외로 꾸리고 여러 팀들을 묶어 30~40명 가량으로 운영할 수 있다고 한다. 그래서 팀마다 한달에 한번 릴리즈를 한다고 해도 4주에 4번 릴리즈가 되어 굉장히 빠르게 릴리즈를 가능하다고 하셨다.하지만, MSA가 꼭 정답만은 아니다. 팀의 이해관계가 맞지 않는다면 오히려 학습하는데에도 시간을 낭비하고 맞지 않는 옷을 입으려니 생산성이 저하될 수도 있다. 그렇기 때문에 비즈니스에 맞지 않는 맹목적인 기술에 대한 맹신과 오버엔지니어링은 엔지니어로서 좋은 자세는 아닌 것 같다." }, { "title": "[Spring] Bulk insertion in Spring Project", "url": "/posts/Spring-Bulk-Insertion/", "categories": "Spring Boot, Development", "tags": "JDBC, Spring Boot, Java", "date": "2022-06-20 16:03:00 +0900", "snippet": "Spring bulk Insertion문제 상황설문 조사 플랫폼을 만들던 와중에, 질문에 대한 응답 문항과, 응답 문항에 응답할 때 결과들을 insert하는 과정에서 문제가 발생했다. 질문에 상응하는 응답 문항 만큼 insert 쿼리가 날라간다는 점과 응답 문항에 상응하는 점이 문제이다.question -&lt; answer answer을 진행한 만큼 Insert 쿼리가 날라간다.@DataJpaTestpublic class AnswerTest { @Autowired private SurveyRepository surveyRepository; @Autowired private AnswerRepository answerRepository; @Autowired private QuestionRepository questionRepository; @Test void answerBulkInsertTest(){ Survey survey = surveyRepository.save(new Survey("지방 선거 관련 설문", "2022 6월 1일에 시행되는 지방선거 관련 설문입니다.", Instant.now(), Instant.now().plusSeconds(100L), "pw")); Question question1 = new Question("윤석열 정부에 대해 긍정적이십니까?", 5, 1, survey); List&lt;Answer&gt; answerList = new ArrayList&lt;&gt;(); for (int i=0; i&lt;100; i++){ Answer answer = new Answer("ans", question1); answerList.add(answer); } question1.addAnswer(answerList); questionRepository.save(question1); answerRepository.saveAll(answerList); }}해결책 1 - Spring Data JPA 에서 Batch SEQUENCE 방식을 사용. Id 값을 위해 새로운 table를 만들기 때문에 관리 포인트가 늘어난다. 이미 사용하는 자동키 전략 방식이 IDENTITY면 변경해야 하는 부담이 생긴다. annotation 지정 방식이 매우 번거롭다.관련해서 실제로 프로젝트를 진행할 때, Navi Project를 진행할 때 Spring DATA JPA를 이용해 5000건 가량의 데이터를 insert했는데 속도도 13초 가량 걸릴 뿐더러 annotation 지정 방식이 굉장히 번거로웠다.해결책 2 - jdbcTemplate 사용MySQL Docs에서 many rows에 insert할 때 다음과 같이 조언을 했다. If you are inserting many rows from the same client at the same time, use [INSERT] statements with multiple VALUES lists to insert several rows at a time. This is considerably faster (many times faster in some cases) than using separate single-row [INSERT] statements.다중으로 값을 넣을 예정이라면, 한번에 쿼리로 넣으라는 말이다. 관련해서 구체적인 테스팅은 다음 글을 참고해도 좋을 듯하다.그렇기에 bulk insert에 대한 sql문을로작성할 수 있으면 되는 것이다. 이는 Spirng에 내장된 jdbcTemplate 의 batchUpdate 이용하면 쉽게 bulk insert query를 작성할 수 있었다. Bult insert가 필요한 도메인에 대한 Repository에 공통적으로 사용할 수 있도록 BulkRepository 를 만들고 이에 대한 구현체인 BulkRepositoryImpl를 Bean으로 등록했다.JdbcTemplate을 이용해 batchUpdate를 통해 batchsize를 100으로 설정하고 Answer 도메인에 대한 query를 날리는 코드는 다음과 같이 구현할 수 있다. .@Repository@RequiredArgsConstructorpublic class BulkRepositoryImpl implements BulkRepository { private final JdbcTemplate jdbcTemplate; @Override public void answerBatchInsert(List&lt;Answer&gt; answers){ String sql = "INSERT INTO survey_answer" + "(answer_question, question_id) values (?, ?)"; jdbcTemplate.batchUpdate(sql, answers, 100, (ps, argument) -&gt; { ps.setString(1, argument.getAnswerQuestion()); ps.setLong(2, argument.getQuestionId()); }); };} 이를 통해 최종적인 성능 비교를 하면 다음과 같다. Spring Data JPA의 saveAll() - 20.892초 JdbcTemplate를 통한 native query - 1.371초batchsize를 정해준 것은 insert query가 너무 크다면 mysql의 max_allowed_packet 을 초과할 수 있기 때문이다. Mysql8.0은 64MB를 디폴드인 것으로 알고 있으며, 이를 변경할 수 있지만 너무 커지면 세션 당 부하가 커질 수 있다.만약 Bulk Insert한 것들에 대한 PK(id)값이 필요하다면 다음 글을 참고해보도록 하면 좋을듯 하다.참고 및 주의사항 hibernate in-memory DB를 사용하면 성능을 체감하기 어렵다! 꼭 local에서 RDB를 띄워서 진행해보자! yml에서 mysql url에 rewriteBatchedStatements=true 을 넣어줘야 작동한다!참고 문헌 및 래포https://github.com/ChoiEungi/surbey-serverhttps://sabarada.tistory.com/195https://kapentaz.github.io/jpa/JPA-Batch-Insert-with-MySQL/#[https://homoefficio.github.io/2020/01/25/Spring-Data에서-Batch-Insert-최적화/#about](" }, { "title": "[Project] Gijol MVP 런칭 회고 ", "url": "/posts/gijol-%ED%9A%8C%EA%B3%A0/", "categories": "Project, 회고", "tags": "Gijol, Agile, Spring Boot, Java", "date": "2022-05-31 18:43:00 +0900", "snippet": "배포 링크: GijolBE배경GIST 청원 프로젝트를 진행하면서 크게 제품의 코드 작성법과 테스트, Operation, Git flow 등을 배울 수 있었다. 이를 청원팀의 팀원으로서 배웠던 점이 많은데 과연 이 배운 것들을 내가 스스로 해나갈 수 있을지에 대해서 의문이 들었다. 이에 대한 중요성과 필요성이 너무나도 중요하게 느껴졌기에 프로젝트에서 사용하면 좋은 부가적인 것들이 아니라, 필수적인 것들로 느껴졌다. 그렇기에 새로운 프로젝트를 진행하면서 배운 방법론적인 부분을 적용해보고 싶었다.Gijol 프로젝트는 기존에 진행하던 프로젝트를 완성하지 못해 아쉬움이 남아 진행한 프로젝트이다. 학교 졸업요건을 특별히 확인할 수 있는 서비스가 아직 없기 때문에 이를 학사편람과 직접 비교와 대조하면서 졸업요건을 확인해야 했고, 이 부분에서 졸업요건 확인을 심지어 놓쳐 한 과목 때문에 졸업을 못하는 사례도 발생했다. 이 부분에서 Pain Point는 확실하다고 느꼈고 본격 프로젝트를 진행하게 되었다.성장뿐만 아니라 내가 생각하는 좋은 팀이라는 기준에 맞아서 진심으로 몰입해보고 싶다는 생각이 들었다. 팀 상태는 한 명은 운영은 아니지만, 기본적인 FE 개발 경험이 있었고. 한 팀원는 개발을 처음 진행해봤지만, 몰입의 중요성을 너무나도 잘 알고 현 상황에서 팀에 도움이 되는 본인이 할 수 있는 최선을 다했었다. 그에 따라 개발적인 성장도 놓치지 않으면서 최대한 도움을 받을 수 있는 부분을 활용하면서 성장을 해나갔다. 그 결과 React + Typescript를 2달 가량만에 코드적인 부분에 기여를 할 수 있었다.이 팀원의 성장은 크게 모르는 것과 대해 투명하게 공유한다는 점에서 기인했다고 생각한다. 처음 개발을 해보는 팀원이 본인이 겪는 어려움에 대해 우연히 말을 하다가 겪었던 어려움을 들었다. 이에 대한 어려움을 호소할 때 혼자 고민했던 부분, 병목이 되었던 부분, 그로 인한 본인이 느끼는 감정을 공유함으로 충분히 공감이 되었다. 이를 통해 우리 프로젝트를 진행하는데 학습에 대한 시간이 더 필요하다고 판단해 일정 조율을 하는 현실적인 대안을 세울 수 있었다. 그 결과 팀원이 성공적으로 코드를 기여하고 자신감을 되찾을 수 있었다. 뿐만 아니라 팀의 전체적인 생산성도 높였다. 이를 통해 같은 팀원과 투명한 의사소통을 통해 발생할 수 있는 위험을 줄일 수 있었다. 이후 현업에서 나도 같은 상황에 놓일 수 있는데, 일정에 영향이 갈 정도의 정말 어려운 부분에 대해 부끄럽다고 느껴 위축되고 문제를 만들기보다 투명한 공유가 필요할 수 있겠다는 생각이 들었다.개인적으로 반성하게 되는 점이 나는 무언가를 잘못한다는 생각이 들때 마다 너무나도 부끄러워 위축되는 경향이 있다. 항상 거기서 현실적인 대안으로 한 걸음 더 발전해나가려 생각해보지는 못하는 것 같다. 이에 대한 의식적인 개선이 필요하고 적용해나가야 한다.What I did내가 진행하고 고민한 부분들을 요약하면 다음과 같다.전반적인 Project Management 및 조직 관리 Backlog 작성을 진행한 후 Sprint를 진행 Linear 도입을 통해 스크럼 보드 도입 팀 작업 규칙(Working Agreement) 도입 1차 릴리즈 후 Sailboat 회고 진행 새로운 기술 도입에 대한 의사결정 조율 → 이 기술이 우리에게 정말 필요한가?에 대한 고민프로젝트 Automation of Operation Github Action을 통해 FE, BE 배포 자동화 → 구축을 통해 배포 자동화를 통한 생산성 향상을 팀에 알릴 수 있었다. Git Flow → dev, prod를 통해 브런치를 관리하고, merge 규칙에 대한 논의를 진행했다. 또한, 브랜치 네이밍 컨벤션에 대해서도 논했다. 이를 통해 스타일에 대한 통일을 진행했다. Github와 Linear의 통합, Linear과 Slack 알림 통합벡엔드 API 개발 졸업요건 확인 객체에 대한 도메인 설계 및 구현 POI를 활용한 Excel 파싱What I Learned기본적인 프로젝트 운영 방식함께 일하는 것은 효율을 극대화하기 위해 진행하다는 것을 배울 수 있었다. 기존에 토이프로젝트는 여러번 리딩을 진행해봤지만, 실제로 런칭할 프로젝트를 리딩해본 경험은 처음이었다. 프로젝트를 진행해보면서 어느 정도 서로가 일적인 가치가 맞는 팀원이었기에 방법론 도입에 대한 공감대 형성이 잘 되었던 것 같다.SW 마에스트로를 진행하면서 에자일, 스크럼, 협업에 관한 특강을 관심을 갖고 열심히 들었다. 이를 Gijol 프로젝트에서 바로바로 사용할 수 있었던 점이 굉장히 체화하기 좋았었다. 이 경험을 바탕으로 SW마에스트로 과정도 비슷하게 운영해나갈 계획이다.도메인 설계와 테스트졸업요건은 전공, 교양, 기초과학, 기타과목과 같은 각각 분야에 대한 조건을 맞춰야 충족할 수 있다. 이를 하나의 객체에 모두 책임을 지게하는 것은 데이터 주도적인 설계로 느껴 각각 분야를 하나의 객체로 둬서 책임을 분할했다. 예를 들어, 전공은 Major라는 객체로, 기초과학은 BasicScience 라는 객체로 분리했다. 이는 각각의 책임이 명확해질 뿐더러 각 기능에 대한 테스트도 용이했다.특별히 데이터베이스를 사용하지 않기 때문에 비즈니스로직이라고 할 것들이 크게 없었다. 다만, 졸업요건을 확인하는 알고리즘을 짜는데 시간이 걸렸다. 처음에는 단순 알고리즘이기에 테스트를 미뤘다. 이는 굉장히 오만했음을 느낄 수 있었다. 테스트를 안짜고 변경에 취약한 부분들이 테스트를 통해 오류가 너무나도 많이 발견되었다. 이는 결국 운영서버에 배포까지 오류가 생겼고, 결국 각각 전공에 대한 유닛테스트를 모두 작성하게 되었고 기본적이고 중요한 기능에 대한 기능은 유닛테스트를 반드시 해야된다는 것을 느꼈다.TMI로 jar 빌드에서 classResource를 getFile() 하게 되면 빌드가 깨진다. 이는 jar에서는 File://와 같은 프로토콜을 제공하지 않아서라고 한다. 이는 getInputStream() 을 사용해야된다는 것을 느끼게 해줬다.To-do회고를 진행했을 때, 코드적인 부분의 투명한 공유가 부족했다는 점이 우리팀의 합의안 중 하나였다. 그렇기에 코드리뷰 문화를 일의 진행에 차질이 생기지 않는 선에서 강제하기로 Working Agreement를 추가했다. 나는 Java Spring 기반의 코드를 주로 작성하기에 FE 부분에 코드리뷰에 관여를 하기 어려웠다. 다만, 팀원들 스스로 객체 지향 설계의 필요성과 코드 개선에 대한 필요성을 너무 느끼고 있는 상황에서 내가 할 수 있는 것을 생각해보게 되었다.Spring은 SOLID를 어느정도 Framework에서 강제한다. 그렇기 때문에 객체지향 설계에 대해 꾸준히 고민하게 된다. 뿐만 아니라, 우아한테크코스 프리코스 과정을 통해 객체지향 설계에 대한 고민을 진행해봤고, 최근에 객체지향 프로그래밍 수업에서 프로젝트(프로젝트는 객체지향과 사실과 오해라는 책에 소개한 객체 지향을 입각해 진행했다)를 진행하면서 객체 지향 설계에 대한 설명을 가장 잘할 수 있을 때가 아닌가라는 생각을 하게되었다. 뿐만 아니라 기존에 나는 데이터 주도 설계를 진행해보고 이는 객체 지향적이지 못하다는 것을 느꼈고 이 경험을 공유해주면 팀적으로 너무나도 좋을 것 같다는 생각이 들었다.이러한 생각을 바탕으로 React를 코드리뷰가 가능한 선에서 공부해보기로 결심했다. 이는 장기적인 팀적 성장으로 봤을 때 팀적 생산성이 복리로 돌아올 수 있을거라 느껴 공부를 시작한 것이다. 또한, 새로운 것에 대한 학습으로 받아들일 수도 있겠다 내가 생각하는 학습이 맞는지에 대한 검증을 할 수도 있겠다. 이를 바탕으로 최대한 빠른 시간으로 React라는 것에대해 공부를 해보고 코드를 어느 정도 작성해보면서 Gijol 팀의 리뷰 문화를 개선하는 것을 목표로 두고있다.벡엔드 개발자로서 역량도 향상에 힘쓸 생각이다. Gijol MVP는 DB 사용이 필수는 아니라는 것이 결론이었기 때문에, 아직 DB를 사용하지 않는다. 다만, 졸업요건 확인의 상태 유지를 할 계획이라는 점과 강의평가 기능을 추가한다는 점으로 사용자의 uniqueness를 검증해야 한다. 그렇기 때문에 다음 스프린트부터는 DB 도입을 진행할 계획이다. 그렇기에 DB와 설계에 대한 고민과 JPA 도입을 염두하고 있다.마지막으로아직 부족하게 많은 프로젝트였지만, Gijol 첫 MVP는 개발자로서 내가 중요하게 여기는 가치, 개발자를 너머 삶의 가치까지 생각해보게 되는 뜻깊은 시간이었다. 생각했던 것이 실현되는 것만큼 기쁘고 자아실현에 도움을 주는 것은 없는 것 같다. 물론 이 기대가 너무 커지면 불행으로 다가오는 것을 항상 인지해야 한다. 기대가 너무 크면 실망도 그에 상응한다. 공동 목표를 지닌 팀원들과 함께 정말 진심어린 몰입을 통해 기대 이상의 가치를 느끼는 것은 어떤 가치와도 맞바꾸기 어려운 것같다.프로젝트는 본 링크에서 확인하실 수 있습니다." }, { "title": "[Think] 삶의 가치", "url": "/posts/%EC%82%B6%EC%9D%98-%EA%B0%80%EC%B9%98-%EC%98%A4%ED%9B%84-10.56.38/", "categories": "생각, Book", "tags": "Book, 죽음이란 무엇인가, 셸리 케이건", "date": "2022-05-07 23:55:00 +0900", "snippet": "삶의 가치셀리 케이건의 “죽음이란 무엇인가”에서 삶의 가치에 대해 논할 때, 삶-그릇 이론을 정의하고 내용을 전개한다. 삶-그릇 이론이란, 삶은 우리가 스스로 정의한 좋은 것과 나쁜 것들을 채워넣을 수 있는 그릇이라는 것이다. 그렇다면 삶이 얼마나 가치 있는지, 좋은지에 대해 평가하려면 그릇에 담긴 좋은 것과 나쁜 것들의 합을 구해 평가를 하는 것이다. 여기서 삶은 결국 그릇에 불과한 것이며, 그것 자체로는 아무런 가치가 없다고 볼 수 있다. 그렇다고 살아있음을 안좋다고 느끼는 것은 아니다.이는 결국 내가 살아가는 가치는 내가 스스로 정의할 수 있다는 것이고, 내가 정한 기간 동안에 인생을 구성하게 될 가치물을 정하는 것이 중요하다는 말이다. 결국 내가 원하는 것들, 하고싶은 것들을 찾는 과정이라는 것은 내 삶의 가치물을 무엇인지 찾아가는 과정이라는 것이다. 이는 내 삶에 담을 그릇에 넣을, 내가 정의한 가치들이기 때문이다.삶은 내가 정의한 가치 죽음이라는게 나쁜 것이라고 하자.(살아있음에 감사함과 기쁨을 느끼고 행복한 것들이 좋은 것들이라고 나는 느끼고 있다) 반면에 내 삶에 정의한 가치들이 아무 것도 없다면, 그 삶은 가치가 없어진다고 볼 수 있다는 것다. 그렇기 때문에 하고 싶은 것들을 스스로 정의하고 내가 정의한 가치들로 채워나가는게 중요하고 가치 있는 삶이지 않을까 싶다. 또한, 그 과정에서 느끼는 살아있음의 가치와 소중함을 느끼고 남아있는 여생에 최선을 다하고 싶을 따름이다.그렇기에 내 삶에서 나는 가치를 1개월, 1년, 5년 등 단위로 내리고 그 기간동안 이 가치들로 채우는데 최선을 다하고 있는 중이다. 지금으로서 내 삶의 가치는 어떤 문제든 잘 해결하고 싶으며, 작은 부분에서라도 문제를 잘 찾아내는 것이다. 모든 상황을 부정적으로 본다는게 아니다. 그보다는 살아가면서 느낀 다수에게 pain point가 되는 부분이면서 해결할 때 피해가 없는(극 소수인) 부분에 대해 소프트웨어를 통해 해결하고 싶다." }, { "title": "Spring Boot에서 git submodule로 민감 정보(yml) 관리", "url": "/posts/git-submodule/", "categories": "Development, Git", "tags": "Spring Boot, GIST 청원, Git", "date": "2022-02-20 09:30:00 +0900", "snippet": "서브 모듈을 통해 민감 정보 관리3줄 요약 민감정보 데이터를 담을 private repo 생성 민감정보가 담긴 private 레포지토리를 public 레포지토리의 서브모듈로 git add submodule ${서브 모듈로 등록할 github repository의 주소} 을 사용해 등록한다. 원격 서브모듈 레포지토리에 있는 파일들을 git submodule update --remote을 이용해 로컬에 있는 서브모듈 폴더로 가져 온다.++ gradle이나 github action으로 서브모듈을 잘 사용한다.구체적 과정 privates Repo 생성 submodule 등록한다.그렇다면 다음 나와있듯 /be 경로(프로젝트의 최상단 경로)에 PDG-privates(submodule repo 이름)라는 폴더가 생성된다.(submodule의 내용들)git add submodule ${서브 모듈로 등록할 github repository의 주소} .gitmodules 추가submodule의 path(file 명)와 url(github url)을 추가해준다. 단 여기서 submodule의 default branch가 master가 아니라면 반드시 branch git submodule update --remote git submodule 방식은 branch의 hash를 작성하는 방식이다. 그렇기 때문에 git submodule update --remote 을 진행하면 submodule의 내용이 update 된다. 본 프로젝트에는 hash가 변경 된다. 이후 반드시 본 프로젝트의 git commit을 진행해야 hash가 제대로 업데이트 된다. 다시 말해, git commit -am "message" 를 진행하고 Push를 해야 한다!Example현재 프로젝트는 다음과 같이 설정돼 있다.이후 협업을 진행하거나 외부에서 submodule을 수정했다는 것을 가정하고 Remote에서 다음과 같이 변경한다.이후 다음 커맨드를 입력한다.git submodule update --remote그렇다면 다음과 같이 hash 값이 checkout 됐다고 나온다.실제로 PDG-privates 폴더 안의 내용이 remote와 같이 변경된다. git diff 를 통해 hash를 확인해봐도 9330cf ~로 변경된 것을 볼 수 있다.이를 커밋하고 push하면Gradle를 이용해 local에서 submodules의 내용을 빌드 시 가져오기로컬 privates 를 받아올 때, gradle를 사용하면 편리하게 submodule의 내용을 가져올 수 있다.task copyPrivate(type: Copy) { copy { from './PDG-privates' include "*.yml" into 'src/main/resources/privates' }}이를 설정하고 build시 submodule의 yml파일들을 ‘src/main/resources/privates’로 가져온다. 이 때, 반드시 ‘src/main/resources/privates’를 .gitignore에 추가해줘야 한다.CI/CD in Github Action- name: Checkout uses: actions/checkout@v1 with: token: $ submodules: true를 workflow file에 추가해주면 된다.ErrorProblem branch를 찾을 수 없다고 표시됐다. 아마 default가 master라서 그런듯 하다.fatal: Needed a single revisionUnable to find current origin/HEAD revision in submodule path 'PDG-privates'Solution branch가 main인데 설정이 HEAD로 돼있었다. .gitmodule에서 branch를 main으로 변경해줬더니 성공했다.[submodule "PDG-privates"] path = PDG-privates url = &lt;https://github.com/2022-solution-challenge/PDG-privates&gt; branch=mainReferenceGit - 서브모듈Github Action 에서 Submodule 설정 방법" }, { "title": "Spring Boot log4j vulnerability 확인 및 변경", "url": "/posts/log4j-vulnerability/", "categories": "Development, Spring Boot", "tags": "Spring Boot, log4j, gradle, GIST 청원, Java", "date": "2022-01-16 23:51:00 +0900", "snippet": "Log4j 이슈로 인해 우리 프로젝트에서 log4j를 버전을 확인해봤는데 2.14.1 버전을 사용하고 있었다. 이는 Intellij에서 External Library에서 확인할 수 있다.log4j 2.14.1 버전은 CVE-2021-44228 에서 나와있듯, 보안취약점에 영향을 받는 버전이다. 다음 사진에서 나와있듯, 2.15.0 버전으로 변경해야 한다.하지만 CVE-2021-44832 에서 2022년 1월 16일 기준으로 log4j에 대한 보안 취약점이 Java8 이상을 기준으로 2.17.0 버전까지 발견됐다. 관련해서는 다음 사진에 나와있다.따라서 우리 프로젝트는 Java11을 사용하기 때문에 log4j 2.17.1로 변경했다. gradle(build.gradle)에서 사용하는 log4j에 대한 dependency를 다음과 같이 추가하면 된다.implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.17.1'implementation 'org.apache.logging.log4j:log4j-api:2.17.1'구체적인 것은 다음 PR에서 확인할 수 있습니다.https://github.com/GIST-Petition-Site-Project/GIST-petition-server/pull/167" }, { "title": "[우아한테크코스 4기 프리코스] 1주차 및 2주차 후기", "url": "/posts/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4_%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4_1_2%EC%A3%BC%EC%B0%A8%ED%9B%84%EA%B8%B0/", "categories": "Project, 회고", "tags": "우아한테크코스, Java", "date": "2021-12-08 23:51:00 +0900", "snippet": "이번 우아한테크코스 프리코스 4기에 함께 성장하는 방법을 배워보려 지원했다. 그러기 위해서는 함께 사용할 수 있는 코드(객체지향적 코드)를 작성해야 했고 이를 프리코스라는 과정을 통해 간접적으로 경험해볼 수 있었다.1주차 과제는 숫자야구게임으로, 1주차 피드백만으로도 충분히 받아들일 수 있었고, 고통스럽게 하는 고민은 없었다. 다만, 2주차 과제에 고민이 있어 객체지향에 대해 고민을 더 많이 해본 선배님께 피드백을 받고 포스팅으로 고민을 공유하려한다. 1, 2주차 래포는 다음과 같다.1주차https://github.com/ChoiEungi/java-baseball-precourse2주차https://github.com/ChoiEungi/java-racingcar-precourse1. InputRole의 멤버 변수에 대한 고민본래 코드는 다음과 같았다. 본 코드에서 고민은 크게 2가지 였다. try- catch를 이용해 에러 헨들링을 진행해야 하는데, depth가 늘어나고, 코드의 중복 부분이 많아지고 이후 확장할 때 메서드의 길이가 15을 넘어갈 수 있을 것 같다. 꼭 nameList와 trialNumber가 class의 멤버 변수여야 하는가?public class InputRole { private String[] nameList; private Integer trialNmber; public void inputStart() { while (true) { try { inputNames(); break; } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } while (true) { try { inputTrialNumber(); break; } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } } public String[] getNameList() { return nameList; } public int getTrialNmber() { return trialNmber; }...public class GameController { public void gameStart() { InputRole inputRole = new InputRole(); OutputRole outputRole = new OutputRole(); inputRole.inputStart(); changeInputToCar(inputRole); outputRole.pirntResultInstruction(); for (int i = 0; i &lt; inputRole.getTrialNmber(); i++) { tryGameOnce(); outputRole.printOneGame(carList); } findWinner(); outputRole.printWinner(winnersList); }}우선 1번 고민에 대해서는 try-catch 문의 기능적으로 크게 좋은 솔루션이 없어, 단순히 메서드를 분리하려 했다. 이는 캡슐화가 제대로 이뤄진다고 보기는 어렵지만, 지금 현실적인 상황에서 가장 최선의 방법이었다. 그리고 인풋을 받는 것이 극단적인 스케일인 몇 만개 이런 식으로 늘어나는 변화는 현실적으로 어렵기 때문에, 단순히 메서드를 분리하는 것으로 해결했다. 그러다보니 class 멤버 변수를 크게 사용할 이유가 없었고 inputStart() 메서드에 구애 받기 보다는 Util로서의 Input을 설정했다.InputRole 이 단순히 인스턴스로서 상태를 갖는 객체일 수 있는데, 어디서든 특정 역할에 대한 인풋을 받고 싶으면 상태를 갖는 것이 아니라, 필요할 때 마다 필요한 인풋을 받아서 인풋의 결과값을 출력해주면 여러 클래스에서 여러 인스턴스를 선언 하지 않고 사용할 수 있게 된다.고민 후 수정한 코드는 다음과 같다.public String[] getNameList() { while (true) { try { return inputNames(); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } } public Integer getTrialNmber() { while (true) { try { return inputTrialNumber(); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } } }private String[] inputNames() { System.out.println(NAME_INPUT_INSTRUCTION); String inputNames = Console.readLine(); String[] nameList = inputNames.split(","); for (String name : nameList) { checkNameWhiteSpaceValid(name); checkNameLengthValid(name); } return nameList; }private Integer inputTrialNumber() { System.out.println(TRIAL_NUMBER_INPUT_INSTRUCTION); String inputNumber = Console.readLine(); checkTrialNumberValid(inputNumber); return Integer.valueOf(inputNumber); }...public class GameController { private static final InputRole inputRole = new InputRole(); public void gameStart() { OutputRole outputRole = new OutputRole(); changeInputToCar(inputRole.getNameList()); outputRole.pirntResultInstruction(); for (int i = 0; i &lt; inputRole.getTrialNmber(); i++) { tryGameOnce(); outputRole.printOneGame(carList); } findWinner(); outputRole.printWinner(winnersList); }이 코드를 보면 inputRole을 더 유연하게 사용할 수 있다는 점과 코드가 간결해진 점에서 크게 유용할 수 있었다.2. Game 도메인 추가 GameController의 역할을 고민하다 보니 GameController의 역할이 너무 많다는게 느껴졌다. 내가 생각한 역할은 다음과 같다. InputRole에서 받아온 후 이를 게임을 진행한 후 OutputRole에 출력하는 역할 Game을 진행한다. 랜덤 숫자 생성 게임을 진행한다. 게임의 승자를 찾는다. 이러한 방식으로 역할이 나뉘었는데, 1번 2번은 같은 역할로서 볼 수 있겠지만, 3번은 충분히 분리해도 좋을 법할 것 같다는 생각을 해볼 수 있었다. 3번을 분리하기 전에 원래 코드를 보면 다음과 같다.public class GameController { private static final InputRole inputRole = new InputRole(); private static final int MAX_PICK_NUMBER = 9; private static final int MIN_PICK_NUMBER = 0; private static final int MOVE_FORWARD_CONTION_NUMBER = 4; private final ArrayList&lt;Car&gt; carList = new ArrayList&lt;&gt;(); private final ArrayList&lt;String&gt; winnersList = new ArrayList&lt;&gt;(); public void gameStart() { OutputRole outputRole = new OutputRole(); changeInputToCar(inputRole.getNameList()); outputRole.pirntResultInstruction(); for (int i = 0; i &lt; inputRole.getTrialNmber(); i++) { tryGameOnce(); outputRole.printOneGame(carList); } findWinner(); outputRole.printWinner(winnersList); } private void changeInputToCar(String[] nameList) { for (String name : nameList) { this.carList.add(new Car(name)); } } private int getRandomNumber() { int randomNumber = Randoms.pickNumberInRange(MIN_PICK_NUMBER, MAX_PICK_NUMBER); return randomNumber; } private boolean checkMoveForward(int randomNumber) { return randomNumber &gt;= MOVE_FORWARD_CONTION_NUMBER; } private void tryGameOnce() { for (Car car : carList) { int randomNumber = getRandomNumber(); if (checkMoveForward(randomNumber)) { car.moveForward(); } } } private void findWinner() { int maxValue = findMaxInCarList(carList); for (Car car : carList) { if (car.getPosition() == maxValue) { winnersList.add(car.getName()); } } } private int findMaxInCarList(ArrayList&lt;Car&gt; carList) { int maxValue = -1; for (Car car : carList) { if (maxValue &lt; car.getPosition()) { maxValue = car.getPosition(); } } return maxValue; }코드가 너무나도 길다. 그리고 역할이 너무 많다. 이후 확장할 때 코드를 작성하는데 점점 부담이 커질 수 밖에 없는 구조인 것이다. 그렇기 때문에 이를 분리해보면 다음과 같이 코드를 깔끔하게 변경해볼 수 있었다.public class Game { private static final int MOVE_FORWARD_CONTION_NUMBER = 4; private static final int MAX_PICK_NUMBER = 9; private static final int MIN_PICK_NUMBER = 0; public void startOnce(List&lt;Car&gt; carList) { for (Car car : carList) { int randomNumber = getRandomNumber(); if (checkMoveForward(randomNumber)) { car.moveForward(); } } } public List&lt;String&gt; winner(List&lt;Car&gt; carList) { int maxValue = findMaxInCarList(carList); List&lt;String&gt; winnersList = new ArrayList&lt;&gt;(); for (Car car : carList) { if (car.getPosition() == maxValue) { winnersList.add(car.getName()); } } return winnersList; } private int getRandomNumber() { int randomNumber = Randoms.pickNumberInRange(MIN_PICK_NUMBER, MAX_PICK_NUMBER); return randomNumber; } private boolean checkMoveForward(int randomNumber) { return randomNumber &gt;= MOVE_FORWARD_CONTION_NUMBER; } private int findMaxInCarList(List&lt;Car&gt; carList) { int maxValue = -1; for (Car car : carList) { if (maxValue &lt; car.getPosition()) { maxValue = car.getPosition(); } } return maxValue; }public class GameController { private static final InputRole inputRole = new InputRole(); private final List&lt;Car&gt; carList = new ArrayList&lt;&gt;(); public void gameStart() { Game game = new Game(); OutputRole outputRole = new OutputRole(); changeInputToCar(inputRole.getNameList()); Integer trialNumber = inputRole.getTrialNmber(); outputRole.pirntResultInstruction(); for (int i = 0; i &lt; trialNumber; i++) { game.startOnce(carList); outputRole.printOneGame(carList); } outputRole.printWinner(game.winner(carList)); } private void changeInputToCar(String[] nameList) { for (String name : nameList) { this.carList.add(new Car(name)); } }}이렇게 역할(클래스)가 명확하게 분리돼 더 보기 좋고 확장성이 좋은 코드를 작성해볼 수 있었다. 리펙토링한 코드는 choieungi_refactor branch에서 확인해볼 수 있다.추가적인 소감객체지향적으로 코드를 변경하려 하니 클래스를 매개변수로 쓰는 것이 아닌, 클래스의 멤버 변수 하나를 매개변수로 쓸 수 있는 등 놓칠 수 있는 부분을 개선하게 되는 계기가 될 수 있었다. 이처럼 코드에 대해 고민을 계속하다보면 정말 필요한 코드만 작성할 수 있을 것이라 느낄 수 있었다.코드 이외에는 2주차 피드백에서 다른 사람의 코드를 보고 함께 성장할 수 있기에 코드를 작성하면서 느낀 소감을 PR에 올리면 좋을 것 같다는 피드백이 굉장히 인상적이었고 다른 의미로 기뻤다. 나는 1, 2주차 PR에 모두 소감을 올렸는데, 내가 느낀 것들을 다른 사람도 볼 수 있으면 같이 성장할 수 있겠다 느꼈기 때문이다.프리코스를 통해 단순히 시험의 과정이 아닌, 성장의 과정으로 느낄 수 있었던 뜻깊던 시간이 아닐까 싶다. 그렇기에 프리코스 과정이 굉장히 몰입감을 주고 재미도 있다. 다만, 자만은 하면 안된다. 아직 부족한 점이 많고, 생각해볼 여지가 많은 부분이 있는 코드이므로 3주차 과제에서는 더 큰 고통을 느껴보고 성장해나갈 것이다.개인적으로는 피드백을 통해 고민해볼 수 있는 기회가 많아졌다. 그렇기 때문에 프리코스 과정 피드백에서 이런 부분을 구현해보면 어떻게 될까요? 와 같이 challenging해볼 수 있는 여지를 줘도 재밌을 것 같다는 느낌이 든다." }, { "title": "[Git] 오래 전 커밋한 깃허브 민감 정보 지우기", "url": "/posts/git-%EC%98%A4%EB%9E%98%EC%A0%84-%EC%BB%A4%EB%B0%8B%ED%95%9C-%EB%AF%BC%EA%B0%90-%EC%A0%95%EB%B3%B4-%EC%A7%80%EC%9A%B0%EA%B8%B0/", "categories": "Development, Git", "tags": "Guide, Git", "date": "2021-09-30 02:22:00 +0900", "snippet": "팀원분께서 예전에 aws key를 yml파일에 넣으셨다. 이때 문제를 인식하고 커밋이 안 쌓였을 때 지웠어야 하는데 이런저런 핑계를 대며 지우지 않았는데 오늘 레포를 public으로 변경했는데 문제가 터졌다.보통 가장 최근 커밋은 git reset HEAD 로 지울 수 있는데 한참 전 기록이 발목을 잡았던 것이다. 이를 해결하는데 겪은 고난을 소개하고자 한다.민감 정보를 지우는데는 2가지 방법이 있다. 전체 커밋 기록을 지우고 다시 push 하기 git bfg를 사용하기 → 커밋 중 파일의 몇몇 부분을 변경할 수 있다.1번은 개인적으로 커밋한 흔적은 어떻게보면 버전인데 버전을 모두 지우기에 risk가 너무 크다는 생각이 들었다.(고생한 흔적도 지워지고) 그렇기에 2번을 이용해 커밋 기록을 그대로 남기면서 어떻게 민감 정보를 변경했는지 소개하고자 한다.brew를 사용할 수 있다는 가정하에 작성한다.bfg는 git-filter-branch의 대안으로 나온 repo cleaner이다. scala로 작성돼 git filter branch보다 빠를 뿐더러 사용하기 매우 간편하다.mac의 경우 brew를 이용해 간편히 설치할 수 있다.brew install bfg지우려는 기록은 다음과 같다.이후 project의 root directory에서 password.txt라는 파일을 만든다.(다른 이름도 괜찮다.) regex 문법을 이용하는데, regex를 크게 몰라도 간편하게 사용할 수 있다. 다음과 같은 형식을 사용하면 된다.{민감정보}==&gt;{변경정보}관련해서 구체적인 예시는 다음 사이트를 보면 이해가 더 잘된다.링크이 때 yml파일 같은 경우 앞의 공백이 생기기 때문에 이를 맞춰주려면 공백도 그대로 포함해야 한다.이를 저장하고 다음과 같은 커멘드를 입력한다.bfg --replace-text password.txt성공적으로 진행되면 다음과 같이 나온다.민감 정보가 들어있는 yml 파일이 변경됬다고 잘 나온다. 그리고 마지막에 있는 command를 입력한다.git reflog expire --expire=now --all &amp;&amp; git gc --prune=now --aggressive이후 다른 repo를 새로 만들어서 git push하면 된다.ReferenceBFG Repo-Cleaner" } ] diff --git a/assets/js/data/swcache.js b/assets/js/data/swcache.js new file mode 100644 index 0000000..3bf1566 --- /dev/null +++ b/assets/js/data/swcache.js @@ -0,0 +1 @@ +const resource = [ /* --- CSS --- */ '/assets/css/style.css', /* --- PWA --- */ '/app.js', '/sw.js', /* --- HTML --- */ '/index.html', '/404.html', '/categories/', '/tags/', '/archives/', '/about/', /* --- Favicons & compressed JS --- */ '/assets/img/favicons/android-chrome-192x192.png', '/assets/img/favicons/android-chrome-512x512.png', '/assets/img/favicons/apple-touch-icon.png', '/assets/img/favicons/favicon-16x16.png', '/assets/img/favicons/favicon-32x32.png', '/assets/img/favicons/favicon.ico', '/assets/img/favicons/mstile-150x150.png', '/assets/js/dist/categories.min.js', '/assets/js/dist/commons.min.js', '/assets/js/dist/home.min.js', '/assets/js/dist/misc.min.js', '/assets/js/dist/page.min.js', '/assets/js/dist/post.min.js', '/assets/js/dist/pvreport.min.js' ]; /* The request url with below domain will be cached */ const allowedDomains = [ 'www.googletagmanager.com', 'www.google-analytics.com', 'choieungi.github.io', 'fonts.gstatic.com', 'fonts.googleapis.com', 'cdn.jsdelivr.net', 'polyfill.io' ]; /* Requests that include the following path will be banned */ const denyUrls = [ ]; diff --git a/assets/js/dist/categories.min.js b/assets/js/dist/categories.min.js new file mode 100644 index 0000000..c369f68 --- /dev/null +++ b/assets/js/dist/categories.min.js @@ -0,0 +1,6 @@ +/*! + * Chirpy v5.1.0 (https://github.com/cotes2020/jekyll-theme-chirpy/) + * © 2019 Cotes Chung + * MIT Licensed + */ +$(function(){$(window).scroll(()=>{50<$(this).scrollTop()&&"none"===$("#sidebar-trigger").css("display")?$("#back-to-top").fadeIn():$("#back-to-top").fadeOut()}),$("#back-to-top").click(()=>($("body,html").animate({scrollTop:0},800),!1))});const LocaleHelper=function(){const e=$('meta[name="prefer-datetime-locale"]'),o=0o,attrTimestamp:()=>t,attrDateFormat:()=>a,getTimestamp:e=>Number(e.attr(t)),getDateFormat:e=>e.attr(a)}}();$(function(){$(".mode-toggle").click(e=>{const o=$(e.target);let t=o.prop("tagName")==="button".toUpperCase()?o:o.parent();t.blur(),flipMode()})});const ScrollHelper=function(){const e=$("body"),o="data-topbar-visible",t=$("#topbar-wrapper").outerHeight();let a=0,r=!1,l=!1;return{hideTopbar:()=>e.attr(o,!1),showTopbar:()=>e.attr(o,!0),addScrollUpTask:()=>{a+=1,r=r||!0},popScrollUpTask:()=>--a,hasScrollUpTask:()=>0!0===r,unlockTopbar:()=>r=!1,getTopbarHeight:()=>t,orientationLocked:()=>!0===l,lockOrientation:()=>l=!0,unLockOrientation:()=>l=!1}}();$(function(){const e=$("#sidebar-trigger"),o=$("#search-trigger"),t=$("#search-cancel"),a=$("#main"),r=$("#topbar-title"),l=$("#search-wrapper"),n=$("#search-result-wrapper"),s=$("#search-results"),i=$("#search-input"),c=$("#search-hints"),d=function(){let e=0;return{block(){e=window.scrollY,$("html,body").scrollTop(0)},release(){$("html,body").scrollTop(e)},getOffset(){return e}}}(),p={on(){e.addClass("unloaded"),r.addClass("unloaded"),o.addClass("unloaded"),l.addClass("d-flex"),t.addClass("loaded")},off(){t.removeClass("loaded"),l.removeClass("d-flex"),e.removeClass("unloaded"),r.removeClass("unloaded"),o.removeClass("unloaded")}},u=function(){let e=!1;return{on(){e||(d.block(),n.removeClass("unloaded"),a.addClass("unloaded"),e=!0)},off(){e&&(s.empty(),c.hasClass("unloaded")&&c.removeClass("unloaded"),n.addClass("unloaded"),a.removeClass("unloaded"),d.release(),i.val(""),e=!1)},isVisible(){return e}}}();function f(){return t.hasClass("loaded")}o.click(function(){p.on(),u.on(),i.focus()}),t.click(function(){p.off(),u.off()}),i.focus(function(){l.addClass("input-focus")}),i.focusout(function(){l.removeClass("input-focus")}),i.on("input",()=>{""===i.val()?f()?c.removeClass("unloaded"):u.off():(u.on(),f()&&c.addClass("unloaded"))})}),$(function(){var e=function(){const e="sidebar-display";let o=!1;const t=$("body");return{toggle(){!1===o?t.attr(e,""):t.removeAttr(e),o=!o}}}();$("#sidebar-trigger").click(e.toggle),$("#mask").click(e.toggle)}),$(function(){$('[data-toggle="tooltip"]').tooltip()}),$(function(){const o=$("#search-input"),t=ScrollHelper.getTopbarHeight();let e,a=0;function r(){0!==$(window).scrollTop()&&(ScrollHelper.lockOrientation(),ScrollHelper.hideTopbar())}screen.orientation?screen.orientation.onchange=()=>{var e=screen.orientation.type;"landscape-primary"!==e&&"landscape-secondary"!==e||r()}:$(window).on("orientationchange",()=>{$(window).width()<$(window).height()&&r()}),$(window).scroll(()=>{e=e||!0}),setInterval(()=>{e&&(function(){var e=$(this).scrollTop();if(!(Math.abs(a-e)<=t)){if(e>a)ScrollHelper.hideTopbar(),o.is(":focus")&&o.blur();else if(e+$(window).height()<$(document).height()){if(ScrollHelper.hasScrollUpTask())return;ScrollHelper.topbarLocked()?ScrollHelper.unlockTopbar():ScrollHelper.orientationLocked()?ScrollHelper.unLockOrientation():ScrollHelper.showTopbar()}a=e}}(),e=!1)},250)}),$(function(){var o="div.post>h1:first-of-type";const t=$(o),n=$("#topbar-title");if(0!==t.length&&!t.hasClass("dynamic-title")&&!n.is(":hidden")){const s=n.text().trim();let a=t.text().trim(),r=!1,l=0;($("#page-category").length||$("#page-tag").length)&&/\s/.test(a)&&(a=a.replace(/[0-9]/g,"").trim()),t.offset().top<$(window).scrollTop()&&n.text(a);let e=new IntersectionObserver(e=>{var o,t;r?(o=$(window).scrollTop(),t=l{50<$(this).scrollTop()&&"none"===$("#sidebar-trigger").css("display")?$("#back-to-top").fadeIn():$("#back-to-top").fadeOut()}),$("#back-to-top").click(()=>($("body,html").animate({scrollTop:0},800),!1))});const LocaleHelper=function(){const e=$('meta[name="prefer-datetime-locale"]'),o=0o,attrTimestamp:()=>t,attrDateFormat:()=>a,getTimestamp:e=>Number(e.attr(t)),getDateFormat:e=>e.attr(a)}}();$(function(){$(".mode-toggle").click(e=>{const o=$(e.target);let t=o.prop("tagName")==="button".toUpperCase()?o:o.parent();t.blur(),flipMode()})});const ScrollHelper=function(){const e=$("body"),o="data-topbar-visible",t=$("#topbar-wrapper").outerHeight();let a=0,r=!1,l=!1;return{hideTopbar:()=>e.attr(o,!1),showTopbar:()=>e.attr(o,!0),addScrollUpTask:()=>{a+=1,r=r||!0},popScrollUpTask:()=>--a,hasScrollUpTask:()=>0!0===r,unlockTopbar:()=>r=!1,getTopbarHeight:()=>t,orientationLocked:()=>!0===l,lockOrientation:()=>l=!0,unLockOrientation:()=>l=!1}}();$(function(){const e=$("#sidebar-trigger"),o=$("#search-trigger"),t=$("#search-cancel"),a=$("#main"),r=$("#topbar-title"),l=$("#search-wrapper"),n=$("#search-result-wrapper"),s=$("#search-results"),c=$("#search-input"),i=$("#search-hints"),d=function(){let e=0;return{block(){e=window.scrollY,$("html,body").scrollTop(0)},release(){$("html,body").scrollTop(e)},getOffset(){return e}}}(),p={on(){e.addClass("unloaded"),r.addClass("unloaded"),o.addClass("unloaded"),l.addClass("d-flex"),t.addClass("loaded")},off(){t.removeClass("loaded"),l.removeClass("d-flex"),e.removeClass("unloaded"),r.removeClass("unloaded"),o.removeClass("unloaded")}},u=function(){let e=!1;return{on(){e||(d.block(),n.removeClass("unloaded"),a.addClass("unloaded"),e=!0)},off(){e&&(s.empty(),i.hasClass("unloaded")&&i.removeClass("unloaded"),n.addClass("unloaded"),a.removeClass("unloaded"),d.release(),c.val(""),e=!1)},isVisible(){return e}}}();function f(){return t.hasClass("loaded")}o.click(function(){p.on(),u.on(),c.focus()}),t.click(function(){p.off(),u.off()}),c.focus(function(){l.addClass("input-focus")}),c.focusout(function(){l.removeClass("input-focus")}),c.on("input",()=>{""===c.val()?f()?i.removeClass("unloaded"):u.off():(u.on(),f()&&i.addClass("unloaded"))})}),$(function(){var e=function(){const e="sidebar-display";let o=!1;const t=$("body");return{toggle(){!1===o?t.attr(e,""):t.removeAttr(e),o=!o}}}();$("#sidebar-trigger").click(e.toggle),$("#mask").click(e.toggle)}),$(function(){$('[data-toggle="tooltip"]').tooltip()}),$(function(){const o=$("#search-input"),t=ScrollHelper.getTopbarHeight();let e,a=0;function r(){0!==$(window).scrollTop()&&(ScrollHelper.lockOrientation(),ScrollHelper.hideTopbar())}screen.orientation?screen.orientation.onchange=()=>{var e=screen.orientation.type;"landscape-primary"!==e&&"landscape-secondary"!==e||r()}:$(window).on("orientationchange",()=>{$(window).width()<$(window).height()&&r()}),$(window).scroll(()=>{e=e||!0}),setInterval(()=>{e&&(function(){var e=$(this).scrollTop();if(!(Math.abs(a-e)<=t)){if(e>a)ScrollHelper.hideTopbar(),o.is(":focus")&&o.blur();else if(e+$(window).height()<$(document).height()){if(ScrollHelper.hasScrollUpTask())return;ScrollHelper.topbarLocked()?ScrollHelper.unlockTopbar():ScrollHelper.orientationLocked()?ScrollHelper.unLockOrientation():ScrollHelper.showTopbar()}a=e}}(),e=!1)},250)}),$(function(){var o="div.post>h1:first-of-type";const t=$(o),n=$("#topbar-title");if(0!==t.length&&!t.hasClass("dynamic-title")&&!n.is(":hidden")){const s=n.text().trim();let a=t.text().trim(),r=!1,l=0;($("#page-category").length||$("#page-tag").length)&&/\s/.test(a)&&(a=a.replace(/[0-9]/g,"").trim()),t.offset().top<$(window).scrollTop()&&n.text(a);let e=new IntersectionObserver(e=>{var o,t;r?(o=$(window).scrollTop(),t=l{50<$(this).scrollTop()&&"none"===$("#sidebar-trigger").css("display")?$("#back-to-top").fadeIn():$("#back-to-top").fadeOut()}),$("#back-to-top").click(()=>($("body,html").animate({scrollTop:0},800),!1))});const LocaleHelper=function(){const t=$('meta[name="prefer-datetime-locale"]'),e=0e,attrTimestamp:()=>o,attrDateFormat:()=>a,getTimestamp:t=>Number(t.attr(o)),getDateFormat:t=>t.attr(a)}}();$(function(){$(".mode-toggle").click(t=>{const e=$(t.target);let o=e.prop("tagName")==="button".toUpperCase()?e:e.parent();o.blur(),flipMode()})});const ScrollHelper=function(){const t=$("body"),e="data-topbar-visible",o=$("#topbar-wrapper").outerHeight();let a=0,r=!1,l=!1;return{hideTopbar:()=>t.attr(e,!1),showTopbar:()=>t.attr(e,!0),addScrollUpTask:()=>{a+=1,r=r||!0},popScrollUpTask:()=>--a,hasScrollUpTask:()=>0!0===r,unlockTopbar:()=>r=!1,getTopbarHeight:()=>o,orientationLocked:()=>!0===l,lockOrientation:()=>l=!0,unLockOrientation:()=>l=!1}}();$(function(){const t=$("#sidebar-trigger"),e=$("#search-trigger"),o=$("#search-cancel"),a=$("#main"),r=$("#topbar-title"),l=$("#search-wrapper"),n=$("#search-result-wrapper"),i=$("#search-results"),s=$("#search-input"),c=$("#search-hints"),d=function(){let t=0;return{block(){t=window.scrollY,$("html,body").scrollTop(0)},release(){$("html,body").scrollTop(t)},getOffset(){return t}}}(),p={on(){t.addClass("unloaded"),r.addClass("unloaded"),e.addClass("unloaded"),l.addClass("d-flex"),o.addClass("loaded")},off(){o.removeClass("loaded"),l.removeClass("d-flex"),t.removeClass("unloaded"),r.removeClass("unloaded"),e.removeClass("unloaded")}},u=function(){let t=!1;return{on(){t||(d.block(),n.removeClass("unloaded"),a.addClass("unloaded"),t=!0)},off(){t&&(i.empty(),c.hasClass("unloaded")&&c.removeClass("unloaded"),n.addClass("unloaded"),a.removeClass("unloaded"),d.release(),s.val(""),t=!1)},isVisible(){return t}}}();function f(){return o.hasClass("loaded")}e.click(function(){p.on(),u.on(),s.focus()}),o.click(function(){p.off(),u.off()}),s.focus(function(){l.addClass("input-focus")}),s.focusout(function(){l.removeClass("input-focus")}),s.on("input",()=>{""===s.val()?f()?c.removeClass("unloaded"):u.off():(u.on(),f()&&c.addClass("unloaded"))})}),$(function(){var t=function(){const t="sidebar-display";let e=!1;const o=$("body");return{toggle(){!1===e?o.attr(t,""):o.removeAttr(t),e=!e}}}();$("#sidebar-trigger").click(t.toggle),$("#mask").click(t.toggle)}),$(function(){$('[data-toggle="tooltip"]').tooltip()}),$(function(){const e=$("#search-input"),o=ScrollHelper.getTopbarHeight();let t,a=0;function r(){0!==$(window).scrollTop()&&(ScrollHelper.lockOrientation(),ScrollHelper.hideTopbar())}screen.orientation?screen.orientation.onchange=()=>{var t=screen.orientation.type;"landscape-primary"!==t&&"landscape-secondary"!==t||r()}:$(window).on("orientationchange",()=>{$(window).width()<$(window).height()&&r()}),$(window).scroll(()=>{t=t||!0}),setInterval(()=>{t&&(function(){var t=$(this).scrollTop();if(!(Math.abs(a-t)<=o)){if(t>a)ScrollHelper.hideTopbar(),e.is(":focus")&&e.blur();else if(t+$(window).height()<$(document).height()){if(ScrollHelper.hasScrollUpTask())return;ScrollHelper.topbarLocked()?ScrollHelper.unlockTopbar():ScrollHelper.orientationLocked()?ScrollHelper.unLockOrientation():ScrollHelper.showTopbar()}a=t}}(),t=!1)},250)}),$(function(){var e="div.post>h1:first-of-type";const o=$(e),n=$("#topbar-title");if(0!==o.length&&!o.hasClass("dynamic-title")&&!n.is(":hidden")){const i=n.text().trim();let a=o.text().trim(),r=!1,l=0;($("#page-category").length||$("#page-tag").length)&&/\s/.test(a)&&(a=a.replace(/[0-9]/g,"").trim()),o.offset().top<$(window).scrollTop()&&n.text(a);let t=new IntersectionObserver(t=>{var e,o;r?(e=$(window).scrollTop(),o=lt.toUpperCase())),$(this).text()!==t&&$(this).text(t)}else--o}),0===o&&void 0!==e&&clearInterval(e),o}dayjs.locale(LocaleHelper.locale()),dayjs.extend(window.dayjs_plugin_relativeTime),dayjs.extend(window.dayjs_plugin_localizedFormat),0!==o&&(t.each(function(){var t,e=$(this).attr("data-toggle");void 0!==e&&"tooltip"===e&&(t=$(this).attr("data-tooltip-df"),e=LocaleHelper.getTimestamp($(this)),t=dayjs.unix(e).format(t),$(this).attr("data-original-title",t),$(this).removeAttr("data-tooltip-df"))}),r()&&(e=setInterval(r,6e4)))}); \ No newline at end of file diff --git a/assets/js/dist/misc.min.js b/assets/js/dist/misc.min.js new file mode 100644 index 0000000..c4cb36d --- /dev/null +++ b/assets/js/dist/misc.min.js @@ -0,0 +1,6 @@ +/*! + * Chirpy v5.1.0 (https://github.com/cotes2020/jekyll-theme-chirpy/) + * © 2019 Cotes Chung + * MIT Licensed + */ +$(function(){$(window).scroll(()=>{50<$(this).scrollTop()&&"none"===$("#sidebar-trigger").css("display")?$("#back-to-top").fadeIn():$("#back-to-top").fadeOut()}),$("#back-to-top").click(()=>($("body,html").animate({scrollTop:0},800),!1))});const LocaleHelper=function(){const e=$('meta[name="prefer-datetime-locale"]'),t=0t,attrTimestamp:()=>o,attrDateFormat:()=>a,getTimestamp:e=>Number(e.attr(o)),getDateFormat:e=>e.attr(a)}}();$(function(){$(".mode-toggle").click(e=>{const t=$(e.target);let o=t.prop("tagName")==="button".toUpperCase()?t:t.parent();o.blur(),flipMode()})});const ScrollHelper=function(){const e=$("body"),t="data-topbar-visible",o=$("#topbar-wrapper").outerHeight();let a=0,r=!1,l=!1;return{hideTopbar:()=>e.attr(t,!1),showTopbar:()=>e.attr(t,!0),addScrollUpTask:()=>{a+=1,r=r||!0},popScrollUpTask:()=>--a,hasScrollUpTask:()=>0!0===r,unlockTopbar:()=>r=!1,getTopbarHeight:()=>o,orientationLocked:()=>!0===l,lockOrientation:()=>l=!0,unLockOrientation:()=>l=!1}}();$(function(){const e=$("#sidebar-trigger"),t=$("#search-trigger"),o=$("#search-cancel"),a=$("#main"),r=$("#topbar-title"),l=$("#search-wrapper"),n=$("#search-result-wrapper"),s=$("#search-results"),i=$("#search-input"),c=$("#search-hints"),d=function(){let e=0;return{block(){e=window.scrollY,$("html,body").scrollTop(0)},release(){$("html,body").scrollTop(e)},getOffset(){return e}}}(),p={on(){e.addClass("unloaded"),r.addClass("unloaded"),t.addClass("unloaded"),l.addClass("d-flex"),o.addClass("loaded")},off(){o.removeClass("loaded"),l.removeClass("d-flex"),e.removeClass("unloaded"),r.removeClass("unloaded"),t.removeClass("unloaded")}},u=function(){let e=!1;return{on(){e||(d.block(),n.removeClass("unloaded"),a.addClass("unloaded"),e=!0)},off(){e&&(s.empty(),c.hasClass("unloaded")&&c.removeClass("unloaded"),n.addClass("unloaded"),a.removeClass("unloaded"),d.release(),i.val(""),e=!1)},isVisible(){return e}}}();function f(){return o.hasClass("loaded")}t.click(function(){p.on(),u.on(),i.focus()}),o.click(function(){p.off(),u.off()}),i.focus(function(){l.addClass("input-focus")}),i.focusout(function(){l.removeClass("input-focus")}),i.on("input",()=>{""===i.val()?f()?c.removeClass("unloaded"):u.off():(u.on(),f()&&c.addClass("unloaded"))})}),$(function(){var e=function(){const e="sidebar-display";let t=!1;const o=$("body");return{toggle(){!1===t?o.attr(e,""):o.removeAttr(e),t=!t}}}();$("#sidebar-trigger").click(e.toggle),$("#mask").click(e.toggle)}),$(function(){$('[data-toggle="tooltip"]').tooltip()}),$(function(){const t=$("#search-input"),o=ScrollHelper.getTopbarHeight();let e,a=0;function r(){0!==$(window).scrollTop()&&(ScrollHelper.lockOrientation(),ScrollHelper.hideTopbar())}screen.orientation?screen.orientation.onchange=()=>{var e=screen.orientation.type;"landscape-primary"!==e&&"landscape-secondary"!==e||r()}:$(window).on("orientationchange",()=>{$(window).width()<$(window).height()&&r()}),$(window).scroll(()=>{e=e||!0}),setInterval(()=>{e&&(function(){var e=$(this).scrollTop();if(!(Math.abs(a-e)<=o)){if(e>a)ScrollHelper.hideTopbar(),t.is(":focus")&&t.blur();else if(e+$(window).height()<$(document).height()){if(ScrollHelper.hasScrollUpTask())return;ScrollHelper.topbarLocked()?ScrollHelper.unlockTopbar():ScrollHelper.orientationLocked()?ScrollHelper.unLockOrientation():ScrollHelper.showTopbar()}a=e}}(),e=!1)},250)}),$(function(){var t="div.post>h1:first-of-type";const o=$(t),n=$("#topbar-title");if(0!==o.length&&!o.hasClass("dynamic-title")&&!n.is(":hidden")){const s=n.text().trim();let a=o.text().trim(),r=!1,l=0;($("#page-category").length||$("#page-tag").length)&&/\s/.test(a)&&(a=a.replace(/[0-9]/g,"").trim()),o.offset().top<$(window).scrollTop()&&n.text(a);let e=new IntersectionObserver(e=>{var t,o;r?(t=$(window).scrollTop(),o=l{50<$(this).scrollTop()&&"none"===$("#sidebar-trigger").css("display")?$("#back-to-top").fadeIn():$("#back-to-top").fadeOut()}),$("#back-to-top").click(()=>($("body,html").animate({scrollTop:0},800),!1))});const LocaleHelper=function(){const t=$('meta[name="prefer-datetime-locale"]'),e=0e,attrTimestamp:()=>o,attrDateFormat:()=>a,getTimestamp:t=>Number(t.attr(o)),getDateFormat:t=>t.attr(a)}}();$(function(){$(".mode-toggle").click(t=>{const e=$(t.target);let o=e.prop("tagName")==="button".toUpperCase()?e:e.parent();o.blur(),flipMode()})});const ScrollHelper=function(){const t=$("body"),e="data-topbar-visible",o=$("#topbar-wrapper").outerHeight();let a=0,r=!1,l=!1;return{hideTopbar:()=>t.attr(e,!1),showTopbar:()=>t.attr(e,!0),addScrollUpTask:()=>{a+=1,r=r||!0},popScrollUpTask:()=>--a,hasScrollUpTask:()=>0!0===r,unlockTopbar:()=>r=!1,getTopbarHeight:()=>o,orientationLocked:()=>!0===l,lockOrientation:()=>l=!0,unLockOrientation:()=>l=!1}}();$(function(){const t=$("#sidebar-trigger"),e=$("#search-trigger"),o=$("#search-cancel"),a=$("#main"),r=$("#topbar-title"),l=$("#search-wrapper"),n=$("#search-result-wrapper"),i=$("#search-results"),c=$("#search-input"),s=$("#search-hints"),d=function(){let t=0;return{block(){t=window.scrollY,$("html,body").scrollTop(0)},release(){$("html,body").scrollTop(t)},getOffset(){return t}}}(),p={on(){t.addClass("unloaded"),r.addClass("unloaded"),e.addClass("unloaded"),l.addClass("d-flex"),o.addClass("loaded")},off(){o.removeClass("loaded"),l.removeClass("d-flex"),t.removeClass("unloaded"),r.removeClass("unloaded"),e.removeClass("unloaded")}},u=function(){let t=!1;return{on(){t||(d.block(),n.removeClass("unloaded"),a.addClass("unloaded"),t=!0)},off(){t&&(i.empty(),s.hasClass("unloaded")&&s.removeClass("unloaded"),n.addClass("unloaded"),a.removeClass("unloaded"),d.release(),c.val(""),t=!1)},isVisible(){return t}}}();function h(){return o.hasClass("loaded")}e.click(function(){p.on(),u.on(),c.focus()}),o.click(function(){p.off(),u.off()}),c.focus(function(){l.addClass("input-focus")}),c.focusout(function(){l.removeClass("input-focus")}),c.on("input",()=>{""===c.val()?h()?s.removeClass("unloaded"):u.off():(u.on(),h()&&s.addClass("unloaded"))})}),$(function(){var t=function(){const t="sidebar-display";let e=!1;const o=$("body");return{toggle(){!1===e?o.attr(t,""):o.removeAttr(t),e=!e}}}();$("#sidebar-trigger").click(t.toggle),$("#mask").click(t.toggle)}),$(function(){$('[data-toggle="tooltip"]').tooltip()}),$(function(){const e=$("#search-input"),o=ScrollHelper.getTopbarHeight();let t,a=0;function r(){0!==$(window).scrollTop()&&(ScrollHelper.lockOrientation(),ScrollHelper.hideTopbar())}screen.orientation?screen.orientation.onchange=()=>{var t=screen.orientation.type;"landscape-primary"!==t&&"landscape-secondary"!==t||r()}:$(window).on("orientationchange",()=>{$(window).width()<$(window).height()&&r()}),$(window).scroll(()=>{t=t||!0}),setInterval(()=>{t&&(function(){var t=$(this).scrollTop();if(!(Math.abs(a-t)<=o)){if(t>a)ScrollHelper.hideTopbar(),e.is(":focus")&&e.blur();else if(t+$(window).height()<$(document).height()){if(ScrollHelper.hasScrollUpTask())return;ScrollHelper.topbarLocked()?ScrollHelper.unlockTopbar():ScrollHelper.orientationLocked()?ScrollHelper.unLockOrientation():ScrollHelper.showTopbar()}a=t}}(),t=!1)},250)}),$(function(){var e="div.post>h1:first-of-type";const o=$(e),n=$("#topbar-title");if(0!==o.length&&!o.hasClass("dynamic-title")&&!n.is(":hidden")){const i=n.text().trim();let a=o.text().trim(),r=!1,l=0;($("#page-category").length||$("#page-tag").length)&&/\s/.test(a)&&(a=a.replace(/[0-9]/g,"").trim()),o.offset().top<$(window).scrollTop()&&n.text(a);let t=new IntersectionObserver(t=>{var e,o;r?(e=$(window).scrollTop(),o=l'),$("input[type=checkbox]:not([checked])").before('')}),$(function(){var t="#main > div.row:first-child > div:first-child";if(!($(t+" img").length<=0)){var e=document.querySelectorAll(t+" img[data-src]");const o=lozad(e);o.observe(),$(t+` p > img[data-src],${t} img[data-src].preview-img`).each(function(){let t=$(this).next();var e="EM"===t.prop("tagName")?t.text():"",o=$(this).attr("data-src");$(this).wrap(``)}),$(".popup").magnificPopup({type:"image",closeOnContentClick:!0,showCloseBtn:!1,zoom:{enabled:!0,duration:300,easing:"ease-in-out"}}),$(t+" a").has("img").addClass("img-link")}}),$(function(){var t=".code-header>button";const e="timeout",r="data-title-succeed",l="data-original-title";function n(t){if($(t)[0].hasAttribute(e)){t=$(t).attr(e);if(Number(t)>Date.now())return 1}}function i(t){$(t).attr(e,Date.now()+2e3)}function c(t){$(t).removeAttr(e)}const o=new ClipboardJS(t,{target(t){let e=t.parentNode.nextElementSibling;return e.querySelector("code .rouge-code")}});$(t).tooltip({trigger:"hover",placement:"left"});const a=function(t){let e=$(t).children();return e.attr("class")}(t);o.on("success",t=>{t.clearSelection();const e=t.trigger;var o;n(e)||(function(t){let e=$(t),o=e.children();o.attr("class","fas fa-check")}(e),o=e,t=$(o).attr(r),$(o).attr(l,t).tooltip("show"),i(e),setTimeout(()=>{var t;t=e,$(t).tooltip("hide").removeAttr(l),function(t){let e=$(t),o=e.children();o.attr("class",a)}(e),c(e)},2e3))}),$("#copy-link").click(t=>{let e=$(t.target);if(!n(e)){t=window.location.href;const o=$("");$("body").append(o),o.val(t).select(),document.execCommand("copy"),o.remove();const a=e.attr(l);t=e.attr(r);e.attr(l,t).tooltip("show"),i(e),setTimeout(()=>{e.attr(l,a),c(e)},2e3)}})}),$(function(){const t=$("#topbar-title"),c="scroll-focus";$("a[href*='#']").not("[href='#']").not("[href='#0']").click(function(r){if(this.pathname.replace(/^\//,"")===location.pathname.replace(/^\//,"")&&location.hostname===this.hostname){const i=decodeURI(this.hash);let e=RegExp(/^#fnref:/).test(i),o=!e&&RegExp(/^#fn:/).test(i);var l=i.includes(":")?i.replace(/\:/g,"\\:"):i;let a=$(l);var n=t.is(":visible"),l=$(window).width()<$(window).height();if(void 0!==a){r.preventDefault(),history.pushState&&history.pushState(null,null,i);r=$(window).scrollTop();let t=a.offset().top-=8;t(a.focus(),$(`[${c}=true]`).length&&$(`[${c}=true]`).attr(c,!1),$(":target").length&&$(":target").attr(c,!1),(o||e)&&a.attr(c,!0),a.is(":focus")?!1:(a.attr("tabindex","-1"),a.focus(),void(ScrollHelper.hasScrollUpTask()&&ScrollHelper.popScrollUpTask()))))}}})}); \ No newline at end of file diff --git a/assets/js/dist/post.min.js b/assets/js/dist/post.min.js new file mode 100644 index 0000000..97f33a1 --- /dev/null +++ b/assets/js/dist/post.min.js @@ -0,0 +1,6 @@ +/*! + * Chirpy v5.1.0 (https://github.com/cotes2020/jekyll-theme-chirpy/) + * © 2019 Cotes Chung + * MIT Licensed + */ +$(function(){$(window).scroll(()=>{50<$(this).scrollTop()&&"none"===$("#sidebar-trigger").css("display")?$("#back-to-top").fadeIn():$("#back-to-top").fadeOut()}),$("#back-to-top").click(()=>($("body,html").animate({scrollTop:0},800),!1))});const LocaleHelper=function(){const t=$('meta[name="prefer-datetime-locale"]'),e=0e,attrTimestamp:()=>o,attrDateFormat:()=>a,getTimestamp:t=>Number(t.attr(o)),getDateFormat:t=>t.attr(a)}}();$(function(){$(".mode-toggle").click(t=>{const e=$(t.target);let o=e.prop("tagName")==="button".toUpperCase()?e:e.parent();o.blur(),flipMode()})});const ScrollHelper=function(){const t=$("body"),e="data-topbar-visible",o=$("#topbar-wrapper").outerHeight();let a=0,r=!1,l=!1;return{hideTopbar:()=>t.attr(e,!1),showTopbar:()=>t.attr(e,!0),addScrollUpTask:()=>{a+=1,r=r||!0},popScrollUpTask:()=>--a,hasScrollUpTask:()=>0!0===r,unlockTopbar:()=>r=!1,getTopbarHeight:()=>o,orientationLocked:()=>!0===l,lockOrientation:()=>l=!0,unLockOrientation:()=>l=!1}}();$(function(){const t=$("#sidebar-trigger"),e=$("#search-trigger"),o=$("#search-cancel"),a=$("#main"),r=$("#topbar-title"),l=$("#search-wrapper"),n=$("#search-result-wrapper"),i=$("#search-results"),c=$("#search-input"),s=$("#search-hints"),d=function(){let t=0;return{block(){t=window.scrollY,$("html,body").scrollTop(0)},release(){$("html,body").scrollTop(t)},getOffset(){return t}}}(),p={on(){t.addClass("unloaded"),r.addClass("unloaded"),e.addClass("unloaded"),l.addClass("d-flex"),o.addClass("loaded")},off(){o.removeClass("loaded"),l.removeClass("d-flex"),t.removeClass("unloaded"),r.removeClass("unloaded"),e.removeClass("unloaded")}},u=function(){let t=!1;return{on(){t||(d.block(),n.removeClass("unloaded"),a.addClass("unloaded"),t=!0)},off(){t&&(i.empty(),s.hasClass("unloaded")&&s.removeClass("unloaded"),n.addClass("unloaded"),a.removeClass("unloaded"),d.release(),c.val(""),t=!1)},isVisible(){return t}}}();function f(){return o.hasClass("loaded")}e.click(function(){p.on(),u.on(),c.focus()}),o.click(function(){p.off(),u.off()}),c.focus(function(){l.addClass("input-focus")}),c.focusout(function(){l.removeClass("input-focus")}),c.on("input",()=>{""===c.val()?f()?s.removeClass("unloaded"):u.off():(u.on(),f()&&s.addClass("unloaded"))})}),$(function(){var t=function(){const t="sidebar-display";let e=!1;const o=$("body");return{toggle(){!1===e?o.attr(t,""):o.removeAttr(t),e=!e}}}();$("#sidebar-trigger").click(t.toggle),$("#mask").click(t.toggle)}),$(function(){$('[data-toggle="tooltip"]').tooltip()}),$(function(){const e=$("#search-input"),o=ScrollHelper.getTopbarHeight();let t,a=0;function r(){0!==$(window).scrollTop()&&(ScrollHelper.lockOrientation(),ScrollHelper.hideTopbar())}screen.orientation?screen.orientation.onchange=()=>{var t=screen.orientation.type;"landscape-primary"!==t&&"landscape-secondary"!==t||r()}:$(window).on("orientationchange",()=>{$(window).width()<$(window).height()&&r()}),$(window).scroll(()=>{t=t||!0}),setInterval(()=>{t&&(function(){var t=$(this).scrollTop();if(!(Math.abs(a-t)<=o)){if(t>a)ScrollHelper.hideTopbar(),e.is(":focus")&&e.blur();else if(t+$(window).height()<$(document).height()){if(ScrollHelper.hasScrollUpTask())return;ScrollHelper.topbarLocked()?ScrollHelper.unlockTopbar():ScrollHelper.orientationLocked()?ScrollHelper.unLockOrientation():ScrollHelper.showTopbar()}a=t}}(),t=!1)},250)}),$(function(){var e="div.post>h1:first-of-type";const o=$(e),n=$("#topbar-title");if(0!==o.length&&!o.hasClass("dynamic-title")&&!n.is(":hidden")){const i=n.text().trim();let a=o.text().trim(),r=!1,l=0;($("#page-category").length||$("#page-tag").length)&&/\s/.test(a)&&(a=a.replace(/[0-9]/g,"").trim()),o.offset().top<$(window).scrollTop()&&n.text(a);let t=new IntersectionObserver(t=>{var e,o;r?(e=$(window).scrollTop(),o=l img[data-src],${t} img[data-src].preview-img`).each(function(){let t=$(this).next();var e="EM"===t.prop("tagName")?t.text():"",o=$(this).attr("data-src");$(this).wrap(``)}),$(".popup").magnificPopup({type:"image",closeOnContentClick:!0,showCloseBtn:!1,zoom:{enabled:!0,duration:300,easing:"ease-in-out"}}),$(t+" a").has("img").addClass("img-link")}}),$(function(){const a=LocaleHelper.attrTimestamp(),t=$(".timeago");let o=t.length,e=void 0;function r(){return t.each(function(){if(void 0!==$(this).attr(a)){let t=function(t){const e=dayjs(),o=dayjs.unix(LocaleHelper.getTimestamp(t));return 10t.toUpperCase())),$(this).text()!==t&&$(this).text(t)}else--o}),0===o&&void 0!==e&&clearInterval(e),o}dayjs.locale(LocaleHelper.locale()),dayjs.extend(window.dayjs_plugin_relativeTime),dayjs.extend(window.dayjs_plugin_localizedFormat),0!==o&&(t.each(function(){var t,e=$(this).attr("data-toggle");void 0!==e&&"tooltip"===e&&(t=$(this).attr("data-tooltip-df"),e=LocaleHelper.getTimestamp($(this)),t=dayjs.unix(e).format(t),$(this).attr("data-original-title",t),$(this).removeAttr("data-tooltip-df"))}),r()&&(e=setInterval(r,6e4)))}),$(function(){$("input[type=checkbox]").addClass("unloaded"),$("input[type=checkbox][checked]").before(''),$("input[type=checkbox]:not([checked])").before('')}),$(function(){var t=".code-header>button";const e="timeout",r="data-title-succeed",l="data-original-title";function n(t){if($(t)[0].hasAttribute(e)){t=$(t).attr(e);if(Number(t)>Date.now())return 1}}function i(t){$(t).attr(e,Date.now()+2e3)}function c(t){$(t).removeAttr(e)}const o=new ClipboardJS(t,{target(t){let e=t.parentNode.nextElementSibling;return e.querySelector("code .rouge-code")}});$(t).tooltip({trigger:"hover",placement:"left"});const a=function(t){let e=$(t).children();return e.attr("class")}(t);o.on("success",t=>{t.clearSelection();const e=t.trigger;var o;n(e)||(function(t){let e=$(t),o=e.children();o.attr("class","fas fa-check")}(e),o=e,t=$(o).attr(r),$(o).attr(l,t).tooltip("show"),i(e),setTimeout(()=>{var t;t=e,$(t).tooltip("hide").removeAttr(l),function(t){let e=$(t),o=e.children();o.attr("class",a)}(e),c(e)},2e3))}),$("#copy-link").click(t=>{let e=$(t.target);if(!n(e)){t=window.location.href;const o=$("");$("body").append(o),o.val(t).select(),document.execCommand("copy"),o.remove();const a=e.attr(l);t=e.attr(r);e.attr(l,t).tooltip("show"),i(e),setTimeout(()=>{e.attr(l,a),c(e)},2e3)}})}),$(function(){const t=$("#topbar-title"),c="scroll-focus";$("a[href*='#']").not("[href='#']").not("[href='#0']").click(function(r){if(this.pathname.replace(/^\//,"")===location.pathname.replace(/^\//,"")&&location.hostname===this.hostname){const i=decodeURI(this.hash);let e=RegExp(/^#fnref:/).test(i),o=!e&&RegExp(/^#fn:/).test(i);var l=i.includes(":")?i.replace(/\:/g,"\\:"):i;let a=$(l);var n=t.is(":visible"),l=$(window).width()<$(window).height();if(void 0!==a){r.preventDefault(),history.pushState&&history.pushState(null,null,i);r=$(window).scrollTop();let t=a.offset().top-=8;t(a.focus(),$(`[${c}=true]`).length&&$(`[${c}=true]`).attr(c,!1),$(":target").length&&$(":target").attr(c,!1),(o||e)&&a.attr(c,!0),a.is(":focus")?!1:(a.attr("tabindex","-1"),a.focus(),void(ScrollHelper.hasScrollUpTask()&&ScrollHelper.popScrollUpTask()))))}}})}); \ No newline at end of file diff --git a/assets/js/dist/pvreport.min.js b/assets/js/dist/pvreport.min.js new file mode 100644 index 0000000..2a8aca1 --- /dev/null +++ b/assets/js/dist/pvreport.min.js @@ -0,0 +1,6 @@ +/*! + * Chirpy v5.1.0 (https://github.com/cotes2020/jekyll-theme-chirpy/) + * © 2019 Cotes Chung + * MIT Licensed + */ +const getInitStatus=function(){let t=!1;return()=>{var e=t;return t=t||!0,e}}(),PvOpts=function(){function t(e){return $(e).attr("content")}function e(e){e=t(e);return void 0!==e&&!1!==e}return{getProxyMeta(){return t("meta[name=pv-proxy-endpoint]")},getLocalMeta(){return t("meta[name=pv-cache-path]")},hasProxyMeta(){return e("meta[name=pv-proxy-endpoint]")},hasLocalMeta(){return e("meta[name=pv-cache-path]")}}}(),PvStorage=function(){const a={KEY_PV:"pv",KEY_PV_SRC:"pv_src",KEY_CREATION:"pv_created_date"},t={LOCAL:"same-origin",PROXY:"cors"};function r(e){return localStorage.getItem(e)}function o(e,t){localStorage.setItem(e,t)}function n(e,t){o(a.KEY_PV,e),o(a.KEY_PV_SRC,t),o(a.KEY_CREATION,(new Date).toJSON())}return{keysCount(){return Object.keys(a).length},hasCache(){return null!==localStorage.getItem(a.KEY_PV)},getCache(){return JSON.parse(localStorage.getItem(a.KEY_PV))},saveLocalCache(e){n(e,t.LOCAL)},saveProxyCache(e){n(e,t.PROXY)},isExpired(){let e=new Date(r(a.KEY_CREATION));return e.setHours(e.getHours()+1),Date.now()>=e.getTime()},isFromLocal(){return r(a.KEY_PV_SRC)===t.LOCAL},isFromProxy(){return r(a.KEY_PV_SRC)===t.PROXY},newerThan(e){return PvStorage.getCache().totalsForAllResults["ga:pageviews"]>e.totalsForAllResults["ga:pageviews"]},inspectKeys(){if(localStorage.length===PvStorage.keysCount())for(let e=0;er&&countUp(r,o,a.attr("id"))):a.text((new Intl.NumberFormat).format(o))}function displayPageviews(e){if(void 0!==e){let t=getInitStatus();const a=e.rows;0<$("#post-list").length?$(".post-preview").each(function(){var e=$(this).find("a").attr("href");tacklePV(a,e,$(this).find(".pageviews"),t)}):0<$(".post").length&&(e=window.location.pathname,tacklePV(a,e,$("#pv"),t))}}function fetchProxyPageviews(){PvOpts.hasProxyMeta()&&$.ajax({type:"GET",url:PvOpts.getProxyMeta(),dataType:"jsonp",jsonpCallback:"displayPageviews",success:e=>{PvStorage.saveProxyCache(JSON.stringify(e))},error:(e,t,a)=>{console.log("Failed to load pageviews from proxy server: "+a)}})}function fetchLocalPageviews(t=!1){return fetch(PvOpts.getLocalMeta()).then(e=>e.json()).then(e=>{t&&PvStorage.isFromProxy()&&PvStorage.newerThan(e)||(displayPageviews(e),PvStorage.saveLocalCache(JSON.stringify(e)))})}$(function(){$(".pageviews").length<=0||(PvStorage.inspectKeys(),PvStorage.hasCache()?(displayPageviews(PvStorage.getCache()),PvStorage.isExpired()?PvOpts.hasLocalMeta()?fetchLocalPageviews(!0).then(fetchProxyPageviews):fetchProxyPageviews():PvStorage.isFromLocal()&&fetchProxyPageviews()):PvOpts.hasLocalMeta()?fetchLocalPageviews().then(fetchProxyPageviews):fetchProxyPageviews())}); \ No newline at end of file diff --git a/categories/book/index.html b/categories/book/index.html new file mode 100644 index 0000000..040074d --- /dev/null +++ b/categories/book/index.html @@ -0,0 +1 @@ + Book | Ruggy Blog
Home Categories Book
Category
Cancel
diff --git a/categories/development/index.html b/categories/development/index.html new file mode 100644 index 0000000..d07f0cc --- /dev/null +++ b/categories/development/index.html @@ -0,0 +1 @@ + Development | Ruggy Blog
Home Categories Development
Category
Cancel
diff --git a/categories/git/index.html b/categories/git/index.html new file mode 100644 index 0000000..09c9abc --- /dev/null +++ b/categories/git/index.html @@ -0,0 +1 @@ + Git | Ruggy Blog
Home Categories Git
Category
Cancel
diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 0000000..ae578e1 --- /dev/null +++ b/categories/index.html @@ -0,0 +1 @@ + Categories | Ruggy Blog
Home Categories
Categories
Cancel
diff --git a/categories/project/index.html b/categories/project/index.html new file mode 100644 index 0000000..f22c73c --- /dev/null +++ b/categories/project/index.html @@ -0,0 +1 @@ + Project | Ruggy Blog
Home Categories Project
Category
Cancel
diff --git a/categories/spring-boot-development/index.html b/categories/spring-boot-development/index.html new file mode 100644 index 0000000..b20ef5c --- /dev/null +++ b/categories/spring-boot-development/index.html @@ -0,0 +1 @@ + Spring Boot Development | Ruggy Blog
Home Categories Spring Boot Development
Category
Cancel
diff --git a/categories/spring-boot/index.html b/categories/spring-boot/index.html new file mode 100644 index 0000000..ee0bfbf --- /dev/null +++ b/categories/spring-boot/index.html @@ -0,0 +1 @@ + Spring Boot | Ruggy Blog
Home Categories Spring Boot
Category
Cancel
diff --git a/categories/testing/index.html b/categories/testing/index.html new file mode 100644 index 0000000..e6da659 --- /dev/null +++ b/categories/testing/index.html @@ -0,0 +1 @@ + Testing | Ruggy Blog
Home Categories Testing
Category
Cancel
diff --git a/categories/thinking/index.html b/categories/thinking/index.html new file mode 100644 index 0000000..a76486b --- /dev/null +++ b/categories/thinking/index.html @@ -0,0 +1 @@ + Thinking | Ruggy Blog
Home Categories Thinking
Category
Cancel
diff --git "a/categories/\354\203\235\352\260\201/index.html" "b/categories/\354\203\235\352\260\201/index.html" new file mode 100644 index 0000000..4848984 --- /dev/null +++ "b/categories/\354\203\235\352\260\201/index.html" @@ -0,0 +1 @@ + 생각 | Ruggy Blog
Home Categories 생각
Category
Cancel
diff --git "a/categories/\355\232\214\352\263\240/index.html" "b/categories/\355\232\214\352\263\240/index.html" new file mode 100644 index 0000000..484752d --- /dev/null +++ "b/categories/\355\232\214\352\263\240/index.html" @@ -0,0 +1 @@ + 회고 | Ruggy Blog
Home Categories 회고
Category
Cancel
diff --git a/feed.xml b/feed.xml new file mode 100644 index 0000000..a6a5834 --- /dev/null +++ b/feed.xml @@ -0,0 +1 @@ + https://choieungi.github.io/Ruggy BlogJunior Software Engineer, Spring boot 2024-03-31T23:29:17+09:00 Eungi, Choi https://choieungi.github.io/ Jekyll © 2024 Eungi, Choi /assets/img/favicons/favicon.ico /assets/img/favicons/favicon-96x96.png Spring Boot Caching에서 에러 핸들링하는 방법2024-03-31T11:22:00+09:00 2024-03-31T11:22:00+09:00 https://choieungi.github.io/posts/spring-boot-cahce-error-handling/ Eungi, Choi 본 글은 아래 링크의 글의 내용과 이어집니다. https://choieungi.github.io/posts/spring-redis-cache-serialization-exception Spring에서 제공하는 @Cacheable을 이용하면 캐쉬를 AOP 기반으로 쉽게 사용할 수 있습니다. 이전 글에서 다뤘듯, 기본적으로 @Cacheable 을 실행하는 Aspect 메소드에서 예외가 발생하면 전체 메소드 자체가 실패하게 됩니다. 해당 상황에서 에러 핸들링을 통해 특정 상황에서 Exception이 발생하지 않도록 변경하는 방법을 소개합니다. @Cacheable 의 CacheErrorHandler 동작 원리 Spring 문서에 따르면 Error Handler는 SimpleCacheErrorHa... 분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법2024-01-21T16:12:00+09:00 2024-01-21T22:50:10+09:00 https://choieungi.github.io/posts/spring-cloud-refresh/ Eungi, Choi 근래에 요즘 우아한 개발이라는 책을 읽으면서 내용 중에 배포 없이 Spring Cloud Config에서 받아오는 프로퍼티를 변경해 서버에 적용하려는 내용을 접했습니다. 해당 팀은 외부 메시지 플랫폼을 이중화하면서 각 외부 플랫폼 연동에 대한 트래픽 분배를 어플리케이션 실행 중에도 변경할 수 있도록 구현해 단일 장애 지점(SPOF)을 제거하려 했습니다. 이를 위해 어플리케이션의 배포 없이 Config 서버의 프로퍼티를 변경함으로 트래픽 분배를 변경하는 방법을 고려했고, 팀에서 최종적으로 일반적으로 사용하는 Spring Cloud Bus를 이용하기보다 Spring Boot만을 활용해 프로퍼티를 재배포 없이 수정하는 방법을 선택했습니다. 이 부분이 흥미롭게 느껴져 호기심에 구현해보게 되었습니다. 배포 ... Spring MVC에서 redisson으로 분산락을 구현하는 방법들2023-12-24T23:13:00+09:00 2023-12-24T23:45:14+09:00 https://choieungi.github.io/posts/spring-redis-distributed-lock/ Eungi, Choi 멀티 인스턴스 환경에서 동시성을 해결하는 방법으로 Redis의 이벤트 루프 기반 싱글스레드 특성을 이용해 분산락을 사용해 쉽게 해결할 수 있습니다. 동시 호출은 DB 데이터의 정합이 깨지거나 메시지 이벤트의 중복 발행 등 예상치 못한 동작으로 이어지는 경우가 많아 해결해야 하는 경우가 빈번합니다. 분산락은 동시성을 제어하기 위한 부가 기능으로 비즈니스 로직과 섞이지 않도록 관심사를 잘 분리하는게 좋습니다. 관련해서 스프링은 Dependency Injection, AOP와 같은 여러 기능을 쉽게 사용할 수 있어 여러 구현 방법을 소개해보려 합니다. 구현은 Java, Spring MVC 기반으로 Redisson을 이용해 구현할 예정입니다. 요구사항 분산락이 실패하는 경우 null을 리턴하게 됩니... Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점2023-12-10T23:30:00+09:00 2023-12-24T23:09:02+09:00 https://choieungi.github.io/posts/spring-redis-cache-serialization-exception/ Eungi, Choi 사내에서 패키지 구조 변경 작업을 하고 배포를 했는데 갑자기 특정 API에서 transaction silently rolled back이 발생했었습니다. 관련해서 확인해보니 DB조회 값을 Dto 객체로 변환해 캐싱한 값을 역직렬화하는 과정에서 문제가 발생했었습니다. 해당 캐시는 월마다 한번씩 바뀌는 주기를 갖는 값으로, 조회가 많은 비율을 차지합니다. 캐시로 사용하는 정보가 DB에서 열거형으로 관리되고 있어 이를 자바 Dto 객체로 직렬화해서 redis에 저장해 캐시로 활용하고 있었습니다. 코드를 확인해보면 다음과 같습니다. 문제 상황 설정 값들 // package com.example.redisinactions.api; @Getter @AllArgsConstructor(access = Ac... MySQL 커넥션, I/O 연산, 잠금에 대한 고찰2023-07-16T23:49:00+09:00 2023-07-16T23:49:00+09:00 https://choieungi.github.io/posts/mysql-resources/ Eungi, Choi 실무에서 MySQL 데이터베이스를 활용하면서 커넥션, I/O 연산, 잠금에 대해 더 유심히 살펴보게 되었다. 요 세가지 자원이 중요하다고 배웠는데 실제로 사용해보면서 정말 그렇다는걸 느낄 수 있었다. 그렇기에 이를 통해 배운점들을 다시 상기해고자 한다. 커넥션 데이터베이스를 사용하면서 가장 중요한 자원을 꼽는다면 커넥션이다. 커넥션이 부족해지는 순간 쿼리를 날릴 수 없다는 의미이며, 이는 곧 장애로 이어진다. 그렇기에 데이터베이스 커넥션 개수를 원활하게 모니터링하고 관리하는 부분은 굉장히 중요하다. 커넥션이 부족해지게 만드는 요소로는 트래픽 증가로 서버의 스케일링으로 커넥션이 부족해질 때가 존재한다. 또, 트랜잭션이 길어져 해당 작업이 커넥션을 계속 잡고 있으면 db 커넥션이 부족해질 수 있다. ... diff --git a/index.html b/index.html new file mode 100644 index 0000000..a686b6a --- /dev/null +++ b/index.html @@ -0,0 +1 @@ + Ruggy Blog
Home
Ruggy Blog
Cancel

Spring Boot Caching에서 에러 핸들링하는 방법

본 글은 아래 링크의 글의 내용과 이어집니다. https://choieungi.github.io/posts/spring-redis-cache-serialization-exception Spring에서 제공하는 @Cacheable을 이용하면 캐쉬를 AOP 기반으로 쉽게 사용할 수 있습니다. 이전 글에서 다뤘듯, 기본적으로 @Cacheable ...

분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법

근래에 요즘 우아한 개발이라는 책을 읽으면서 내용 중에 배포 없이 Spring Cloud Config에서 받아오는 프로퍼티를 변경해 서버에 적용하려는 내용을 접했습니다. 해당 팀은 외부 메시지 플랫폼을 이중화하면서 각 외부 플랫폼 연동에 대한 트래픽 분배를 어플리케이션 실행 중에도 변경할 수 있도록 구현해 단일 장애 지점(SPOF)을 제거하려 했습니...

Spring MVC에서 redisson으로 분산락을 구현하는 방법들

멀티 인스턴스 환경에서 동시성을 해결하는 방법으로 Redis의 이벤트 루프 기반 싱글스레드 특성을 이용해 분산락을 사용해 쉽게 해결할 수 있습니다. 동시 호출은 DB 데이터의 정합이 깨지거나 메시지 이벤트의 중복 발행 등 예상치 못한 동작으로 이어지는 경우가 많아 해결해야 하는 경우가 빈번합니다. 분산락은 동시성을 제어하기 위한 부가 기능으로 비즈니...

Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점

사내에서 패키지 구조 변경 작업을 하고 배포를 했는데 갑자기 특정 API에서 transaction silently rolled back이 발생했었습니다. 관련해서 확인해보니 DB조회 값을 Dto 객체로 변환해 캐싱한 값을 역직렬화하는 과정에서 문제가 발생했었습니다. 해당 캐시는 월마다 한번씩 바뀌는 주기를 갖는 값으로, 조회가 많은 비율을 차지합니다...

MySQL 커넥션, I/O 연산, 잠금에 대한 고찰

실무에서 MySQL 데이터베이스를 활용하면서 커넥션, I/O 연산, 잠금에 대해 더 유심히 살펴보게 되었다. 요 세가지 자원이 중요하다고 배웠는데 실제로 사용해보면서 정말 그렇다는걸 느낄 수 있었다. 그렇기에 이를 통해 배운점들을 다시 상기해고자 한다. 커넥션 데이터베이스를 사용하면서 가장 중요한 자원을 꼽는다면 커넥션이다. 커넥션이 부족해지는 순...

Amazon Aurora 스토리지 엔진과 MySQL InnoDB 스토리지 엔진 비교

우리 회사를 포함해 많은 회사는 RDBMS를 사용할 때 MySQL Amazon Aurora DB(이하 오로라)를 사용하는 경우가 존재한다. 왜 오로라를 사용하는 지 궁금했는데 기존 전통적인 MySQL보다 가용성, 확장성, 연산 비용 등이 더 싸서 대규모 처리 작업에 용이해서 사용한다고 들었다. 또, 오로라는 컴퓨팅과 스토리지 인스턴스가 각각 분리되어 ...

querydsl의 transform 메서드에서 발생하는 connection leak 현상

문제 상황 회사에서 모든 환불은 어드민 서버를 거쳐 환불 서버에 환불 요청을 보내 환불 프로세스가 진행된다. 하지만 환불 서버에서 요청을 제대로 보내고 환불을 완료했지만, 어드민 서버에서 히스토리를 DB에 기록하는 작업이 제대로 이뤄지지 않았다. 그렇기에 어드민 히스토리와 환불 기록의 불일치가 발생했고, 이 운영 이슈를 해결하는 과정을 남기려고 한다...

감정적 결정과 상황 귀인

근래에 감정적인 결정과 발언이 늘었다. 가진 환경에 대한 불만족 때문이었다. 모든 환경에는 장단이 존재하지만 비교와 기대 불일치로 인한 스트레스는 감정적인 결정을 유발했다. 이유를 분석해보고 왜 그랬는 지에 대해 생각해보자. The Conscious Discipline Brain State Model The Conscious Discipline...

중요한 일에 집중하기

중요한 일에 집중하기 샘 알트만과 폴 그레이엄은 공통적으로 인생은 짧다라는 말을 자주 한다. 인생이 짧다라는 의미는 결국 본인에게 중요하지 않은 일보다 중요한 일에 더 집중하게 만들기 때문이다. 그렇다면, 중요한 일은 무엇일까? 여러 방면으로 존재하겠지만, 근래에 가장 많이 시간을 사용하는 업무적인 부분에서 고민해보자. 진부하지만, 우선순위가 높은...

[Spring] Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기

Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기 Goal 업적 달성 시, 데이터베이스에 유저의 업적 달성을 기록한다. 업적 달성은 편지 송신, 좋아요와 같은 유저의 행위가 발생한 후 조건을 만족하면 이뤄진다. 본 글에서는 유저가 편지를 작성할 떄, 이벤트를 발행해 유저의 업적을 달성하는 상황에 대한 코...

diff --git a/norobots/index.html b/norobots/index.html new file mode 100644 index 0000000..e6f6ede --- /dev/null +++ b/norobots/index.html @@ -0,0 +1,11 @@ + + + + Redirecting… + + + + +

Redirecting…

+ Click here if you are not redirected. + diff --git a/page2/index.html b/page2/index.html new file mode 100644 index 0000000..b705eab --- /dev/null +++ b/page2/index.html @@ -0,0 +1 @@ + Ruggy Blog
Home
Ruggy Blog
Cancel

[Testing] Mockito를 이용한 Service Layer Unit Testing

Testing에 관한 고찰 일반적으로 스프링으로 테스트를 작성할 때, @SpringBootTest를 이용해 통합테스트와 인수테스트를 진행했습니다. 이를 이용해 테스트를 진행하면, 개별적으로 테스트를 실행하기에도, 전체를 테스트를 실행하기에도 너무 속도가 느리다는 단점이 있었습니다. 뿐만 아니라, 테스트 간의 격리성을 확보하기 위해서 모든 데이터를 지...

[Thinking] UC Berkeley를 마치며 느낀 부족함 그리고 강점의 중요성

지난 6월 말부터 8월 중순까지 두달 간 UC Berkeley에서 교환학생으로 학업을 진행했다. UC Berkeley에서 UI Development 수업(CS160)과 Operating System and System Programming(CS162) 수업을 수강했다. 두 수업다 upper division, 주로 junior ~ senior 학생들이 ...

[Spring] spring data jpa에서 save 내부 원리

문제 @Test void retrieveInboxAllLettersTest() { Letter letter = new Letter("content", user); letter.send(user1); letterRepository.save(letter); letterRepository.s...

[Thinking] MSA에 대한 단상

SW 마에스트로 과정에서 너무나도 영광스럽게 조대협 멘토님의 k8s 멘토링을 4회 가량을 듣게 되었다. 오늘이 첫번째 강의였고 들으면서 기술에 대한 깨달음이 꽤나 커서 글을 작성한다. 뿐만 아니라 블로그에 좋은 글을 써야 한다는 강박이 계속되어 지금이라도 작은 글들을 조금씩 써나가는 연습을 해보려 한다. k8s를 배우기 앞서 k8s가 가장 잘 활용될...

[Spring] Bulk insertion in Spring Project

Spring bulk Insertion 문제 상황 설문 조사 플랫폼을 만들던 와중에, 질문에 대한 응답 문항과, 응답 문항에 응답할 때 결과들을 insert하는 과정에서 문제가 발생했다. 질문에 상응하는 응답 문항 만큼 insert 쿼리가 날라간다는 점과 응답 문항에 상응하는 점이 문제이다. question -&lt; answer answ...

[Project] Gijol MVP 런칭 회고

배포 링크: Gijol BE 배경 GIST 청원 프로젝트를 진행하면서 크게 제품의 코드 작성법과 테스트, Operation, Git flow 등을 배울 수 있었다. 이를 청원팀의 팀원으로서 배웠던 점이 많은데 과연 이 배운 것들을 내가 스스로 해나갈 수 있을지에 대해서 의문이 들었다. 이에 대한 중요성과 필요성이 너무나도 중요하게 느껴졌기에...

[Think] 삶의 가치

삶의 가치 셀리 케이건의 “죽음이란 무엇인가”에서 삶의 가치에 대해 논할 때, 삶-그릇 이론을 정의하고 내용을 전개한다. 삶-그릇 이론이란, 삶은 우리가 스스로 정의한 좋은 것과 나쁜 것들을 채워넣을 수 있는 그릇이라는 것이다. 그렇다면 삶이 얼마나 가치 있는지, 좋은지에 대해 평가하려면 그릇에 담긴 좋은 것과 나쁜 것들의 합을 구해 평가를 하는 ...

Spring Boot에서 git submodule로 민감 정보(yml) 관리

서브 모듈을 통해 민감 정보 관리 3줄 요약 민감정보 데이터를 담을 private repo 생성 민감정보가 담긴 private 레포지토리를 public 레포지토리의 서브모듈로 git add submodule ${서브 모듈로 등록할 github repository의 주소} 을 사용해 등록한다. 원격 서브모듈 레포지토리에 있는 파일들을 g...

Spring Boot log4j vulnerability 확인 및 변경

Log4j 이슈로 인해 우리 프로젝트에서 log4j를 버전을 확인해봤는데 2.14.1 버전을 사용하고 있었다. 이는 Intellij에서 External Library에서 확인할 수 있다. log4j 2.14.1 버전은 CVE-2021-44228 에서 나와있듯, 보안취약점에 영향을 받는 버전이다. 다음 사진에서 나와있듯, 2.15.0 버전으로 변경...

[우아한테크코스 4기 프리코스] 1주차 및 2주차 후기

이번 우아한테크코스 프리코스 4기에 함께 성장하는 방법을 배워보려 지원했다. 그러기 위해서는 함께 사용할 수 있는 코드(객체지향적 코드)를 작성해야 했고 이를 프리코스라는 과정을 통해 간접적으로 경험해볼 수 있었다. 1주차 과제는 숫자야구게임으로, 1주차 피드백만으로도 충분히 받아들일 수 있었고, 고통스럽게 하는 고민은 없었다. 다만, 2주차 과제에...

diff --git a/page3/index.html b/page3/index.html new file mode 100644 index 0000000..80d0586 --- /dev/null +++ b/page3/index.html @@ -0,0 +1 @@ + Ruggy Blog
Home
Ruggy Blog
Cancel

[Git] 오래 전 커밋한 깃허브 민감 정보 지우기

팀원분께서 예전에 aws key를 yml파일에 넣으셨다. 이때 문제를 인식하고 커밋이 안 쌓였을 때 지웠어야 하는데 이런저런 핑계를 대며 지우지 않았는데 오늘 레포를 public으로 변경했는데 문제가 터졌다. 보통 가장 최근 커밋은 git reset HEAD 로 지울 수 있는데 한참 전 기록이 발목을 잡았던 것이다. 이를 해결하는데 겪은 고난을 ...

diff --git a/posts/Berkeley-review/index.html b/posts/Berkeley-review/index.html new file mode 100644 index 0000000..7803887 --- /dev/null +++ b/posts/Berkeley-review/index.html @@ -0,0 +1 @@ + [Thinking] UC Berkeley를 마치며 느낀 부족함 그리고 강점의 중요성 | Ruggy Blog
Home [Thinking] UC Berkeley를 마치며 느낀 부족함 그리고 강점의 중요성
Post
Cancel

[Thinking] UC Berkeley를 마치며 느낀 부족함 그리고 강점의 중요성

지난 6월 말부터 8월 중순까지 두달 간 UC Berkeley에서 교환학생으로 학업을 진행했다. UC Berkeley에서 UI Development 수업(CS160)과 Operating System and System Programming(CS162) 수업을 수강했다. 두 수업다 upper division, 주로 junior ~ senior 학생들이 많이 듣는 수업이었는데, 도전을 해보자는 목적으로 조금 빡빡하게 과목을 신청했다. 실제로 수업 로드도 빡세고 영어라는 언어적 장벽으로 다른 사람들이 사용하는 시간보다 1.5배는 적어도 더 사용해야 원활하게 업무를 진행할 수 있었다.

UC Berkeley에서 느낀 부족함

학습적인 측면에서도 부족함을 많이 느꼈지만, 특히나 팀 프로젝트에서 느낀 점이 너무 많았다. 우선, 너무나도 성장 속도가 빠르다는 점이었다. 팀적으로는 이점이지만, 내 개인과 비교했을 때 배우는 속도가 적어도 1.5배 가량 차이가 났다. 이 뿐만 아니라 질문도 굉장히 뛰어나며, 두려움이 없다. 분위기 또한, 누구나 모를 수 있다는게 기본 전제였다. 이를 통해 이후에는 본인의 이해를 바탕으로 명쾌하게 설명까지 할 수 있었으며, 팀적인 기여에도 좋은 퍼포먼스를 보였다. 22년 동안 모르는 것을 혼자 찾아보며 해결한 나에게는 속도적인 측면이건, 방법론적인 측면이건 충격으로 다가왔다. 한 번도 그래보지 못했기 때문이다.

프로젝트 경험을 통해 코드를 어느 정도 잘 짠다고 생각했었고, GIST에서 System Programming 수업을 들어 OS적인 내용도 미리 많이 사전에 준비해 간다고 생각했었다. 뿐만 아니라, 버클리에서도 열심히 수업듣고 여가 시간 없이 하루의 대부분을 투자했음에도, 버클리 학생들이 훨씬 더 잘했다. 내가 학기 동안 아무리 열심히해도 이들에게 닿기 어려웠다. 너무나도 우물 안의 개구리였다고 느꼈고, 잘한다고 생각했던 것 조차 부끄럽게 다가왔다.

이 경험은 내게 너무나도 충격적으로 다가왔고, 자신감을 크게 잃었으며 근 몇달간 고민으로 이어졌다. 다행히도 이 고민을 먼저 느끼셨던 여러 멘토님들이 존재했었고, 그 답변들은 고민을 해결하는데 큰 도움이 되었다.

다양한 사람들의 조언

먼저, 미국에서 SW마에스트로 활동을 통해 실리콘밸리에서 커리어를 이어오셨던 한기용 멘토님께서 흔쾌히 산호세에서 만나주셨다. 관련된 상황을 말씀드렸고, 비교가 나를 갉아먹을 정도 가면 너무나도 불행해지고 과거의 본인과 비교하는게 중요하다고 하셨다. 그리고 분명히 내가 가진 강점이 있을거기 때문에 이를 빨리 찾아내고 발전해서 임팩트(결과)를 내는 것이 중요하다고 말씀주셨다. 일을 잘하는 것과 학습을 잘하는 것은 다르기 때문이다. 관련해서도 글을 작성해주셨다.

이 글을 통해 다시 한번 이야기를 나눴던 것을 상기할 수 있었고, 위로받을 수 있었다. 너무나도 멘토님께 감사드리며, 이 기억은 아마 평생 잊지 못할 듯 싶다.

재학 중인 심리학 교수님께서도 수업시간에 지능에 관해 이야기를 해주시면서, 미국에 가면 주변의 동기들 때문에 자괴감을 많이 느끼셨다고 하셨다. 정확히 나와 일치해 질문을 드렸다. 답변은 결국에는 조금 더 많이 준비하고 시간을 사용했고, 그러다보니 조금 익숙해지면서 3~4년차부터는 어느 정도 맞춰나가실 수 있으셨다고 말씀주셨다. 이 답변은 굉장히 정직하고 명확했기 때문에 와닿았다. 부족하면, 간단하게 조금씩 더 하면 되는 것이다.

뿐만 아니라, 조대협 멘토님께도 우연히 질문할 기회가 생겨 내 경험에 비추어 질문을 드렸었는데 비슷한 경험을 인생에서 두 번 느끼셨고 그 경험들은 평생 못잊으실 경험이라고 말씀주셨다. 실제로 멘토님께서도 자바 커뮤니티를 운영하셨고, 자바를 어느 정도 잘하신다고 생각하셨는데 외국계 회사에 커리어를 진행하시면서, 주변 사람들의 굉장히 높은 수준에 좌절감을 느끼셨다고 하셨다. 너무나도 분함을 느끼셨다고 하셨는데 너무나도 공감이 많이 되었다.

그래서 그 상황에서 멘토님께서 생각하셨던 강점을 찾으려 하셨고 그 강점은 더 많이 시간을 사용하는 것이었다. 그로 인해, 멘토님께서는 기존보다 더 많은 업무를 먼저 도맡아 진행하셨고, 이를 바탕으로 회사에서 시스템 장애를 가장 많이 해결하신 사람이 되셨다고 하셨다. 구글에서도 세계에서 탑급 인재들을 보면서 생각을 많이 들으셨다고 하셨는데, 기존에 해결하신 방법과 동일하게, 강점을 바탕으로 입지를 다져나가셨다고 하셨다.

강점의 중요성

결국 이에 관해 고찰해본 결과, 현재 위치와 무관하게 누구나 다 느끼는 감정이며, 극복 방법도 대부분 비슷하다는게 가장 놀라웠다. 이 감정은 좋든 나쁘던 인생에 중요한 영향을 누구에게나 주었다. 결국 극복해야 하고, 극복하기 위해서는 본인의 강점을 잘 살려 그 강점을 바탕으로 자신감을 찾아야한다. 분명히 사람마다 본인이 가진 강점이 있을 것이고 그 강점을 최대한 경쟁력있게 가져갈 수 있는 사람이 되는게 중요하다고 느꼈다. 이를 통해 작은 자신감을 더 찾아나가고, 더 좋은 결과를 내면 된다.

이와 더불어 결국 잘한다는 기준은 끝도 없기에 비교에도 끝이 없다. 특히 나는 압도되는 격차에 좌절감을 많이 느껴 자신감을 많이 잃었다. 하지만 시간이 지나고 조금 나아졌고, 끝없는 성장의 필요성과 겸손함을 배웠다. 그렇기에 부족함을 느끼는 데에 적당한 동기부여로서 활용하는 것은 성장에 도움이 될 수 있지만, 나를 갉아먹을 때까지 가는 것은 위험다고 느꼈다.

결국에는 지금 가진 것들에 대한 감사함, 그리고 잘할 수 있는게 무엇인지 찾아가는게 중요하다. 굳이 따지면, 항상 노력으로 성취를 이뤄왔기 때문에 조대협 멘토님과 비슷하게 무언가에 대한 집착과 몰입이 지금 갖고 있는 나의 강점이라고 생각한다. 아직 모호한 부분이지만 이를 바탕으로 가시적인 강점까지 이어질 수 있기를 바라며, 지금까지 노력해온 나에게 감사하다고 말하고 싶다.

This post is licensed under CC BY 4.0 by the author.

[Spring] spring data jpa에서 save 내부 원리

[Testing] Mockito를 이용한 Service Layer Unit Testing

diff --git a/posts/Choosing-Architecture-for-developer/index.html b/posts/Choosing-Architecture-for-developer/index.html new file mode 100644 index 0000000..0d6024c --- /dev/null +++ b/posts/Choosing-Architecture-for-developer/index.html @@ -0,0 +1 @@ + [Thinking] MSA에 대한 단상 | Ruggy Blog
Home [Thinking] MSA에 대한 단상
Post
Cancel

[Thinking] MSA에 대한 단상

SW 마에스트로 과정에서 너무나도 영광스럽게 조대협 멘토님의 k8s 멘토링을 4회 가량을 듣게 되었다. 오늘이 첫번째 강의였고 들으면서 기술에 대한 깨달음이 꽤나 커서 글을 작성한다. 뿐만 아니라 블로그에 좋은 글을 써야 한다는 강박이 계속되어 지금이라도 작은 글들을 조금씩 써나가는 연습을 해보려 한다.

k8s를 배우기 앞서 k8s가 가장 잘 활용될 수 있는 구조인 MSA와 그 기반이 되는 Container에 대해서 먼저 다뤘다. 여기서 누차 강조하는 것은 기술을 배우기 전에 그 Background를 알고 그 기술을 왜 사용하는지에 대한 의문을 꾸준히 제기해야 한다는 것이다.

먼저 MSA를 왜 사용하는가?에 대한 질문을 답한다면, 조직마다 사용하는 이유는 각기 다르겠지만 기능 단위로 나눈다는 점이었다. 나는 기능 단위로 나눈다는 것을 통해 가용성을 높이는 것(어떤 단위의 서비스가 죽어도 다른 서비스는 정상적으로 작동한다는 의미)로서 가장 크게 받아들였다. 이를 통해 서비스를 최대한 안정적으로 돌릴 수 있다는 장점이 가장 먼저 떠오르는 이유였다. 나름 타당한 이유이지만, 멘토링 시간에 들은 내용으로는 기능 단위로 독립적으로 분리함을 통해 그 기능을 구현하는 조직의 의사결정을 명확히 위임할 수 있다는 측면이었다. 이는 곧 의사결정의 속도가 빨라진다는 것이고, 생산성 향상으로 이어져 소프트웨어를 개발하는 방법론으로서 굉장히 좋은 방법일 수 있었다.

관련한 법칙으로 Conway’s Law도 소개해주셨다. 이는 다음과 같다.

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.

조직의 communication structure가 곧 조직의 system의 design에서 나타난다는 것이다. 만약 조직의 커뮤니케이션 구조가 복잡하다면 조직의 system design도 복잡해진다는 의미이다. 즉, 위계 조직과 같이 복잡한 커뮤니케이션 구조에서는 MSA를 사용한다고 하더라도 그 의미에 맞고 적합하게 사용하고 있지 못할 수 있다는 말이다. 결국 아키텍쳐의 가장 큰 원칙은 DDD와 같이 MSA를 잘 만드는 방법론보다도 팀의 구조에 맞춰 설계를 하는 것이 가장 중요함을 느낄 수 있었다.

이런 생산성 증대에 있어 궁금한 점이 있어 “그렇다면 MSA를 진행하는데 생산성 향상에 이를 수 있을 정도의 팀의 규모는 어느 정도인가?”에 대한 질문을 드렸다. 기능이 independent적인 관점에서 혼자 진행할 수도 있다고 했고 실제로 미국의 많은 스타트업에서는 작은 스타트업들도 MSA를 사용한다고 한다. 현업에서는 일반적으로 2-pizza 법칙으로 팀당 8명 내외로 꾸리고 여러 팀들을 묶어 30~40명 가량으로 운영할 수 있다고 한다. 그래서 팀마다 한달에 한번 릴리즈를 한다고 해도 4주에 4번 릴리즈가 되어 굉장히 빠르게 릴리즈를 가능하다고 하셨다.

하지만, MSA가 꼭 정답만은 아니다. 팀의 이해관계가 맞지 않는다면 오히려 학습하는데에도 시간을 낭비하고 맞지 않는 옷을 입으려니 생산성이 저하될 수도 있다. 그렇기 때문에 비즈니스에 맞지 않는 맹목적인 기술에 대한 맹신과 오버엔지니어링은 엔지니어로서 좋은 자세는 아닌 것 같다.

This post is licensed under CC BY 4.0 by the author.
diff --git a/posts/Mocking-test/index.html b/posts/Mocking-test/index.html new file mode 100644 index 0000000..3c792c4 --- /dev/null +++ b/posts/Mocking-test/index.html @@ -0,0 +1,115 @@ + [Testing] Mockito를 이용한 Service Layer Unit Testing | Ruggy Blog
Home [Testing] Mockito를 이용한 Service Layer Unit Testing
Post
Cancel

[Testing] Mockito를 이용한 Service Layer Unit Testing

Testing에 관한 고찰

일반적으로 스프링으로 테스트를 작성할 때, @SpringBootTest를 이용해 통합테스트와 인수테스트를 진행했습니다. 이를 이용해 테스트를 진행하면, 개별적으로 테스트를 실행하기에도, 전체를 테스트를 실행하기에도 너무 속도가 느리다는 단점이 있었습니다. 뿐만 아니라, 테스트 간의 격리성을 확보하기 위해서 모든 데이터를 지우는 과정에서 DataIntegrityException 이 자주 발생해 어려움이 있었습니다.

이를 위해서 격리가 쉽고 빠른 테스트를 진행할 수 있는 테스트 방법에 대해 알아보았고, 마침 Mock을 이용한 Service Layer에 대한 단위테스트를 진행해보고 느낀 경험을 공유합니다.

Mockito를 이용한 Service Layer의 Unit Test

저희의 ArgumentResolver에서 로그인한 유저를 조회하기 위해 사용하는 Service의 메소드 중 하나는 다음과 같습니다. jwtProvider를 통해 decode를 진행한 후에, email을 통해 user를 조회하게 됩니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+
@Service
+@RequiredArgsConstructor
+public class UserService {
+
+    private final UserRepository userRepository;
+    private final JWTProvider jwtProvider;
+
+		...
+
+		public User loginUser(String token) {
+		    String email = jwtProvider.decodeJWTToSubject(token);
+		returnuserRepository.findUserByEmail(email).orElseThrow(NoSuchRecordException::new);
+		}
+}
+

이를 Mockito를 사용해 Service Layer의 의존성을 격리해 테스팅을 진행하면 코드는 다음과 같습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+
@ExtendWith(MockitoExtension.class)
+public class UserServiceMockTest {
+
+    private static final String ACCESS_TOKEN = "accessToken";
+    private static final String EMAIL = "swm.team.goyukkuri@gmail.com";
+    private static final String TOKEN = "token";
+
+    @Mock
+    private UserRepository userRepository;
+
+    @Mock
+    private JWTProvider jwtProvider;
+
+    @InjectMocks
+    private UserService userService;
+
+		private User googleUser;
+
+    @BeforeEach
+    void setup(){
+        googleUser = Fixtures.UserStub.defaultGoogleUser(EMAIL);
+    }
+
+		...
+
+		@Test
+		void user_google_login_test() {
+		    when(userRepository.findUserByEmail(anyString())).thenReturn(Optional.of(googleUser));
+		    when(jwtProvider.decodeJWTToSubject(TOKEN)).thenReturn(EMAIL);
+		
+		    User user = userService.loginUser(TOKEN);
+				
+		    assertThat(user.getEmail()).isEqualTo(EMAIL);
+		    verify(userRepository, times(1)).findUserByEmail(anyString());
+		    verifyNoMoreInteractions(userRepository);
+		}
+}
+

UserService를 위한 의존관계인 UserRepositoryjwtProvider를 주입하려면, 먼저 Mocking을 진행해야 합니다. @Mock를 통해 가짜 빈을 넣어줄 수 있으며, 실제 구현된 객체와 무관하게 작동하게 됩니다. 다시말해 userRepository의 메서드들이 껍데기만 존재할 뿐, 구현체가 없게 됩니다. 만약 실제 객체를 주입하고 싶으면 @Spy 를 이용하면 됩니다.

의존 관계를 다 Mocking을 했다면, @InjectMock 을 통해 의존관계를 주입할 수 있습니다. 이를 통해 격리의 주체를 userService로만 둘 수 있게 되고, 다른 실제 객체들의 의존성이 모두 제거됩니다.

이후 when() 을 이용해서 Mock 객체의 메소드의 결과를 어떻게 설정할 것인지 정해준 후, userService.loginUser 메소드를 실행하게 됩니다. 만약 jwtProvider 혹은 userRepository 같은 Mock 객체의 메소드를 정의를 안해준 것이 있다면, 실행할 수 없게 됩니다.

이후 verify 를 통해 Mock 객체의 행위에 대해 검증해볼 수도 있습니다. 뿐만 아니라, when() 으로 설정한 것 이외의 행위가 있었는 지도 verifyNoMoreInteractions 를 통해 확인해볼 수 있었습니다.

Mock Unit Test의 장점

  1. 속도

SpringBootTest보다 압도적으로 속도가 빠르다는 점은 비교 불가한 장점이었습니다. 이를 통해 피드백을 더 빨리 받을 수 있었고, 빌드 시간도 단축할 수 있을 것이라는 생각이 들었습니다. 이는 곧 개발 생산성 향상으로 이어질 것입니다.

  1. unit testing을 통해 코드의 품질 확인

Mock을 사용해 다른 의존성들을 테스트 대역으로 사용하니, 내부 구현에 대해서 모두 Mocking을 진행해야 했습니다. 이는 내부 구현을 한번 더 확인해보게 되었으며, unit testing이 어려워지는 객체는 Mocking을 많이 진행해야 하고 고려해야 할 부분이 많아졌습니다. 이는, 메소드의 결합도가 높아졌다는 것을 알 수 있었습니다. 이는 통합테스트만으로는 알기 어려웠습니다.

  1. 상대적으로 편리한 테스트 격리

Mockito를 통해 빈으로 주입받는 의존성을 모두 Mocking을 통해 테스트 대역을 편리하게 만들 수 있었습니다. @SpringBootTest@MockBean 을 통해 가능했지만, 이는 통합테스트 환경에서 특별한 의존성이 아닌 것들을 Mocking을 하는 것은 시스템 간의 상호작용을 확인하기 어려워지기 때문에 적절치 못하다고 생각했습니다.

Mock Unit test의 아쉬운 점

  1. 구현 세부에 대해 굉장히 잘 알아야하며, 유지 보수에 대한 부담이 커집니다.

모든 repository가 무엇이 사용되는 지 알아야 하고 이에 대한 return 값을 일일히 정해줘야 합니다. 이 부분은 관리 포인트가 많아진다는 단점을 지니고 있습니다. 뿐만 아니라, 인가 정책이 바뀌어 jwt가 아닌 session으로 바뀐다면, 테스트에 jwtProvider 절을 변경해야 할 것입니다. 이는 통합테스트에 비해 변경에 취약하게 됩니다.

  1. 영속성 전이 테스트의 어려움

저희 프로젝트의 도메인 서비스 로직에서 프로필 정보를 입력하는 다음과 같은 로직이 존재합니다.

1
+2
+3
+4
+5
+6
+
@Transactional
+public voidupdateUserProfile(User user, ProfileRequest request) {
+    Profile profile =newProfile(request.getNickname(), request.getGender(), request.getAge(), request.getJob(), user);
+    profile.addPsychologicalExam(request.getPsychologicalExams());
+    user.updateProfile(profile);
+}
+

본 로직은 영속성 전이(CASCADE.ALL)를 통해 프로필 정보에 대한 생명주기를 하나로 뒀습니다. 이는 Repository를 구현해 save를 하는 것은 객체 지향적이지 못하다고 생각해 위와 같이 구현했습니다. 이 같은 경우, 실제 쿼리가 어떻게 나가는 지에 대해 확인이 필요하다고 생각합니다. 다만, Mockito를 통해 unit testing을 진행한다면, 영속성 전이가 잘 이뤄졌는지에 대해서 확인이 어려울 것입니다. 뿐만 아니라, 전반적인 트랜잭션에 대한 테스트도 어려울 듯 싶습니다.

결론

결론적으로 은탄환은 없다고, Mocking을 이용한 방법이 속도는 빠르지만 여러 단점이 존재했던 것 같습니다. 결국 각각의 장단점을 명확하게 인식하고 문제 상황에 맞게 조직에서 합의한 기준을 바탕으로 테스트를 잘 작성하는 것이 중요한 것 같습니다. 마지막으로, Mocking에 대한 여러 견해가 존재하는데 특히 테스트의 고전파와 런던파에 대한 마틴 파울러의 글(본 글의 테스트는 런던파에 해당합니다)을 읽어보는 것도 좋을 듯 합니다.

Mock Test 작성에 도움이 된 글

Unit Testing the Service Layer of Spring boot Application

블라디미르 코리코프, 단위테스트, 생산성과 품질을 위한 단위테스트 원칙과 패턴

This post is licensed under CC BY 4.0 by the author.

[Thinking] UC Berkeley를 마치며 느낀 부족함 그리고 강점의 중요성

[Spring] Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기

diff --git a/posts/Spring-Bulk-Insertion/index.html b/posts/Spring-Bulk-Insertion/index.html new file mode 100644 index 0000000..69c6c99 --- /dev/null +++ b/posts/Spring-Bulk-Insertion/index.html @@ -0,0 +1,83 @@ + [Spring] Bulk insertion in Spring Project | Ruggy Blog
Home [Spring] Bulk insertion in Spring Project
Post
Cancel

[Spring] Bulk insertion in Spring Project

Spring bulk Insertion

문제 상황

설문 조사 플랫폼을 만들던 와중에, 질문에 대한 응답 문항과, 응답 문항에 응답할 때 결과들을 insert하는 과정에서 문제가 발생했다. 질문에 상응하는 응답 문항 만큼 insert 쿼리가 날라간다는 점과 응답 문항에 상응하는 점이 문제이다.

question -< answer

  • answer을 진행한 만큼 Insert 쿼리가 날라간다.
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+
@DataJpaTest
+public class AnswerTest {
+
+    @Autowired private SurveyRepository surveyRepository;
+    @Autowired private AnswerRepository answerRepository;
+    @Autowired private QuestionRepository questionRepository;
+
+    @Test
+    void answerBulkInsertTest(){
+        Survey survey = surveyRepository.save(new Survey("지방 선거 관련 설문", "2022 6월 1일에 시행되는 지방선거 관련 설문입니다.", Instant.now(), Instant.now().plusSeconds(100L), "pw"));
+        Question question1 = new Question("윤석열 정부에 대해 긍정적이십니까?", 5, 1, survey);
+
+        List<Answer> answerList = new ArrayList<>();
+        for (int i=0; i<100; i++){
+            Answer answer = new Answer("ans", question1);
+            answerList.add(answer);
+        }
+
+        question1.addAnswer(answerList);
+        questionRepository.save(question1);
+        answerRepository.saveAll(answerList);
+    }
+}
+

해결책 1 - Spring Data JPA 에서 Batch SEQUENCE 방식을 사용.

  • Id 값을 위해 새로운 table를 만들기 때문에 관리 포인트가 늘어난다.
    • 이미 사용하는 자동키 전략 방식이 IDENTITY면 변경해야 하는 부담이 생긴다.
  • annotation 지정 방식이 매우 번거롭다.

관련해서 실제로 프로젝트를 진행할 때, Navi Project를 진행할 때 Spring DATA JPA를 이용해 5000건 가량의 데이터를 insert했는데 속도도 13초 가량 걸릴 뿐더러 annotation 지정 방식이 굉장히 번거로웠다.

해결책 2 - jdbcTemplate 사용

MySQL Docs에서 many rows에 insert할 때 다음과 같이 조언을 했다.

If you are inserting many rows from the same client at the same time, use [INSERT] statements with multiple VALUES lists to insert several rows at a time. This is considerably faster (many times faster in some cases) than using separate single-row [INSERT] statements.

다중으로 값을 넣을 예정이라면, 한번에 쿼리로 넣으라는 말이다. 관련해서 구체적인 테스팅은 다음 글을 참고해도 좋을 듯하다.

그렇기에 bulk insert에 대한 sql문을로작성할 수 있으면 되는 것이다. 이는 Spirng에 내장된 jdbcTemplatebatchUpdate 이용하면 쉽게 bulk insert query를 작성할 수 있었다. Bult insert가 필요한 도메인에 대한 Repository에 공통적으로 사용할 수 있도록 BulkRepository 를 만들고 이에 대한 구현체인 BulkRepositoryImpl를 Bean으로 등록했다.

JdbcTemplate을 이용해 batchUpdate를 통해 batchsize를 100으로 설정하고 Answer 도메인에 대한 query를 날리는 코드는 다음과 같이 구현할 수 있다. .

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+
@Repository
+@RequiredArgsConstructor
+public class BulkRepositoryImpl implements BulkRepository {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    @Override
+    public void answerBatchInsert(List<Answer> answers){
+        String sql = "INSERT INTO survey_answer"
+                + "(answer_question, question_id) values (?, ?)";
+
+        jdbcTemplate.batchUpdate(sql, answers, 100, (ps, argument) -> {
+            ps.setString(1, argument.getAnswerQuestion());
+            ps.setLong(2, argument.getQuestionId());
+        });
+    };
+
+} 
+

이를 통해 최종적인 성능 비교를 하면 다음과 같다.

  • Spring Data JPA의 saveAll() - 20.892초

  • JdbcTemplate를 통한 native query - 1.371초

batchsize를 정해준 것은 insert query가 너무 크다면 mysql의 max_allowed_packet 을 초과할 수 있기 때문이다. Mysql8.0은 64MB를 디폴드인 것으로 알고 있으며, 이를 변경할 수 있지만 너무 커지면 세션 당 부하가 커질 수 있다.

만약 Bulk Insert한 것들에 대한 PK(id)값이 필요하다면 다음 글을 참고해보도록 하면 좋을듯 하다.

참고 및 주의사항

  • hibernate in-memory DB를 사용하면 성능을 체감하기 어렵다! 꼭 local에서 RDB를 띄워서 진행해보자!
  • yml에서 mysql url에 rewriteBatchedStatements=true 을 넣어줘야 작동한다!

참고 문헌 및 래포

https://github.com/ChoiEungi/surbey-server

https://sabarada.tistory.com/195

https://kapentaz.github.io/jpa/JPA-Batch-Insert-with-MySQL/#

[https://homoefficio.github.io/2020/01/25/Spring-Data에서-Batch-Insert-최적화/#about](

This post is licensed under CC BY 4.0 by the author.

[Project] Gijol MVP 런칭 회고

[Thinking] MSA에 대한 단상

diff --git a/posts/Spring-data-jpa-save/index.html b/posts/Spring-data-jpa-save/index.html new file mode 100644 index 0000000..4a8a55b --- /dev/null +++ b/posts/Spring-data-jpa-save/index.html @@ -0,0 +1,43 @@ + [Spring] spring data jpa에서 save 내부 원리 | Ruggy Blog
Home [Spring] spring data jpa에서 save 내부 원리
Post
Cancel

[Spring] spring data jpa에서 save 내부 원리

문제

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+
  @Test
+  void retrieveInboxAllLettersTest() {
+        Letter letter = new Letter("content", user);
+        letter.send(user1);
+        letterRepository.save(letter);
+        letterRepository.save(letter);
+        letterRepository.save(letter);
+        List<InboxLetterResponse> inboxLetterResponses = letterService.retrieveInboxAllLetters(user1.getId());
+        assertThat(inboxLetterResponses.size()).isEqualTo(3);
+    }
+

전체 편지를 조회하는 로직에서 3개를 insert한 후 이를 전체 조회해서 size를 비교하는 테스트코드를 작성하던 중 실패했습니다. 결과는 다음과 같이 나왔는데 편지에 대해서 insert쿼리가 1번만 발생한 것으로 보입니다. 실제로 전체 조회 후 size를 조회하면 1개가 나왔습니다.

기본적으로 save가 identical하게 작동하고, flush를 진행하지 않아 발생한 것으로 유추되었는데 계속 실수가 발생하는 부분이기에 내부 동작 원리를 직접 확인해보겠습니다.

JpaRepository의 구조

기본적으로 JpaRepository 는 다음과 같은 구조를 갖고 있습니다. 여기서 save는 CrudRepository 에서 인터페이스를 제공합니다. 참고로 PagingAndSortingRepository는 docs에서 나와있듯 CrudRepository 의 Extension으로, pagination과 sorting을 이용한 조회 method를 제공합니다.

CrudRepository

CrudRepository에서 save는 다음과 같이 인터페이스를 제공합니다. 여기서 docs의 설명이 굉장히 중요한데, 인스턴스가 변했을 수 있으므로, 저장 작업(insert query)이 save에서 return된 인스턴스를 사용한다고 되어있습니다. 제가 겪은 이슈에서는 이 return된 instance가 중복된 것으로 유추할 수 있습니다.

구현체인 SimpleJpaRepository로 들어가보면 다음과 같습니다.

만약 identity가 새로운 entity라면 persist한 후 entity를 리턴하는데, 그렇지 않다면 merge를 진행하게 됩니다. merge를 진행하면 Persistent Context에 객체가 추가되지 않기 때문에 이후에 save가 진행되는 객체가 insert되지 않게됩니다.

디버깅을 통한 실제 작동 확인

실제로 문제를 파악하기 위해서 디버깅을 진행해보면. 먼저 save가 실행 지점에 breakpoint를 잡고 이후 내부 동작을 확인하기 위해 Spring-data-jpa의 jar에 들어가서 SimpleJpaReposiotry 에서 breakpoint를 잡으면 다음과 같습니다.

디버깅을 실행하면 다음과 같습니다.

첫 번째 save

두 번째 save

두번 째에 대해서는 Persistent Context에 이미 존재하기 때문에 merge를 진행하게 됩니다. 이는 곧 identity가 똑같은 객체가 변경되었을 때 Persistent Context에 적용되고 이후 flush를 진행하면 쿼리가 발생하게 됩니다.

뿐만 아니라 해결책으로 saveAndFlush 를 사용하면 되지 않나? 싶어서 진행했는데도 통과하지 않았습니다. 이는 flush는 Persistent Context를 지우는 것이 아니라, Persistent Context의 내용을 DB에 쿼리를 날려 반영하는 것인 것도 놓쳤던 것 같습니다. 결국에는 identity가 다른 객체를 각각 만들어 save를 진행해 해결할 수 있었습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+
@Test
+void retrieveInboxAllLettersTest() {
+
+    for (int i = 0; i < 3; i++) {
+        Letter letter = new Letter("content", user);
+        letter.send(user1);
+        letterRepository.save(letter);
+    }
+    List<InboxLetterResponse> inboxLetterResponses = letterService.retrieveInboxAllLetters(user1.getId());
+    assertThat(inboxLetterResponses.size()).isEqualTo(3);
+}
+

굉장히 기본적인 내용인데 생각보다 놓치기 쉬운 부분이기에 글을 작성합니다. JPA에서는 객체의 Identity(객체의 id값)를 통해 새로운 객체임을 구분하고 save메소드에서는 이를 기준으로 insert가 작동하기 때문에 이를 유의해야 할 것 같습니다.

This post is licensed under CC BY 4.0 by the author.

[Thinking] MSA에 대한 단상

[Thinking] UC Berkeley를 마치며 느낀 부족함 그리고 강점의 중요성

diff --git a/posts/amazon-aurora-storage-with-innodb/index.html b/posts/amazon-aurora-storage-with-innodb/index.html new file mode 100644 index 0000000..ef390c6 --- /dev/null +++ b/posts/amazon-aurora-storage-with-innodb/index.html @@ -0,0 +1 @@ + Amazon Aurora 스토리지 엔진과 MySQL InnoDB 스토리지 엔진 비교 | Ruggy Blog
Home Amazon Aurora 스토리지 엔진과 MySQL InnoDB 스토리지 엔진 비교
Post
Cancel

Amazon Aurora 스토리지 엔진과 MySQL InnoDB 스토리지 엔진 비교

우리 회사를 포함해 많은 회사는 RDBMS를 사용할 때 MySQL Amazon Aurora DB(이하 오로라)를 사용하는 경우가 존재한다. 왜 오로라를 사용하는 지 궁금했는데 기존 전통적인 MySQL보다 가용성, 확장성, 연산 비용 등이 더 싸서 대규모 처리 작업에 용이해서 사용한다고 들었다. 또, 오로라는 컴퓨팅과 스토리지 인스턴스가 각각 분리되어 있다. 여기서 오로라 스토리지 엔진이 기존 MySQL 스토리지 엔진인 InnoDB와 큰 차이가 있는지도 궁금했었다.

마침 Real MySQL 스터디에서 MySQL에서 대체로 쓰이는 스토리지 엔진인 InnoDB 스토리지 엔진의 구조에 대해 공부하면서, 영속성을 제공하는 DoubleWrite buffer 기능이 흥미로웠다. 언뜻보면 비효율적으로 보일 수 있는 연산으로 보였기 때문이다. 물론 옵션을 끌 수 있겠지만, 정합성 측면에서 끄는 것을 권장하지 않아 사용한다고 가정했을 때 더 효율적인 방법이 있을 지도 궁금했다. 그렇기에 스토리지 엔진을 개선한 오로라는 어떻게 효율적으로 처리하는 지 알아보자.

Amazon Aurora

오로라같은 경우 기존 전통적인 MySQL에 비해 더 좋은 퍼포먼스, 확장성, 가용성과 내구성을 제공한다. 이는 컴퓨팅과 스토리지 엔진을 분리해서 제공함을 통해 제공하며, 기존 MySQL 엔진과 InnoDB 스토리지 엔진을 커스터마이징해서 Aurora로 제공한다. Aurora Storage 엔진에서 더 좋은 퍼포먼스를 이야기할 때 특히나 I/O 연산 최적화를 다음과 같이 언급한다.

Aurora는 비용을 절감하고 읽기/쓰기 트래픽을 위해 사용할 수 있는 리소스를 확보하기 위해 불필요한 I/O를 제거하도록 설계되었습니다. 쓰기 I/O는 안정적인 쓰기를 위해 트랜잭션 로그 기록을 스토리지 계층으로 푸시할 때만 사용됩니다. (중략) 기존 데이터베이스 엔진과는 달리 Amazon Aurora는 변경된 데이터베이스 페이지를 스토리지 계층으로 푸시하지 않으므로 I/O 사용을 좀 더 줄일 수 있습니다. 링크

트랜잭션 로그 기록을 스토리지 계층으로 푸시한다는 의미는 무엇이고, 기존 InnoDB는 그렇다면 데이터를 스토리지 엔진으로 푸시하는 것일까? 이 두 개의 관점을 비교하면서 알아보자.

InnoDB 스토리지 엔진 구조

InnoDB 스토리지 엔진은 RDBMS에서 말 그대로 스토리지 디스크로부터 데이터를 잘 가져오는 역할을 한다. InnoDB 스토리지 엔진은 트랜잭션, 장애 복구, 락, 백업 등 여러가지 스토리지와 관련된 기능을 제공한다. 공식 문서에 나와있는 아키텍쳐는 다음과 같다.

여기서 주의 깊게 봐야할 것은 DoubleWrite Buffer와 Buffer Pool이다. 여러 기능을 제공하지만, 버퍼 풀은 쓰기 지연 작업과 데이터 파일을 캐싱하는 역할을 하며, DoubleWrite Buffer는 데이터 정합성을 위해 버퍼풀에서 데이터 파일에 쓰기 작업을 하기 직전에 디스크에 따로 작성하는 로그이다.

MySQL Doublewrite buffer

innodb의 버퍼 풀에서는 기본적으로 데이터 파일에 flush하기 전에 doublewrite buffer를 스토리지에 저장한다. 이는 데이터의 무결성을 위함으로, 버퍼 풀에서 데이터 파일로 쓰기 작업 실패가 발생할 때를 사용된다. 실패가 나게 되면, doublewrite buffer의 내용과 데이터 파일을 비교해서 다른 내용을 담고 있는 페이지가 존재하면 doublewrite buffer의 내용을 데이터 파일의 페이지로 복사하게 된다. 이를 통해 시스템의 비정상적 종료에도 무결성을 보장할 수 있게된다.

예를 들어 다음 그림에서 innodb 버퍼 풀에서 데이터 파일에 쓰기 전에 먼저 DoubleWrite Buffer에 페이지를 작성하게 된다. 그 이후 버퍼 풀에서 flush를 진행해 데이터 파일에 쓰기를 진행한다. 만약 여기서 C 데이터 파일에 쓰는 과정에서 MySQL 서버가 종료되었다고 하면, 재시작할 때 forcing recovery로 인해 Doublewrite buffer의 내용이 해당 데이터 파일로 쓰기 작업이 일어난다.이를 통해 데이터 무결성을 보장할 수 있게된다.

Doublewrite buffer 작업은 디스크에 실질적으로 쓰기 작업을 두 번 진행하는 것으로 볼 수 있다. 이에 대해 공식 문서에서는 2번의 쓰기 작업 I/O가 발생하지만 오버헤드는 2번 만큼 발생하지 않는다고 언급했다. 하지만 오로라는 이 Doublewrite buffer를 배제하는 방법으로 쓰기 작업을 진행한다. 즉, Doublewrite buffer를 작업하지 않으기에 표면적으로 봤을 떄 연산이 더 적다고 볼 수 있다.

Amazon Aurora의 쓰기 연산 작업

다음 그림에서 볼 수 있듯이, insert를 진행할 때 MySQL은 doublewrite buffer과 Datafile에 쓰기 작업을 진행하지만, Aurora 같은 경우에는 스토리지에 로그를 쌓는 것이 전부이다. 그 이후의 작업은 오로라 스토리지 내부적으로 작업을 진행하게 된다.

Aurora같은 경우에는 스토리지 구조가 log-structured storage이다. 위 그림에서 볼 수 있듯 오로라 스토리지에 로그만 쌓고 쓰기 작업이 끝난다. 그렇기에 오로라의 스토리지 쓰기 작업은 위에서 인용구에서 언급했듯, 트랜잭션 로그 기록(리두 로그)을 스토리지 계층으로 푸시할 때만 쓰기 I/O가 발생한다. 그렇기에 DoubleWrite buffer가 없을 뿐더러 데이터 파일도 직접 쓰기 연산을 하지 않는다. 이는 리두 로그(WAL)만을 디스크에 쓰기 연산함으로 I/O 연산을 극단적으로 줄일 수 있다. 또, 로그 파일은 데이터 파일에 비해 상대적으로 데이터 크기가 작을 것이기 때문에 실질적으로 더 많은 데이터 파일을 적은 I/O로 쓸 수 있게 되는 것이다.

언뜻 생각해보면 MySQL에서 리두 로그(WAL)를 모두 쌓지 않는 이유는 로그를 모두 쌓아서 연산함으로 데이터를 기록하게 되면 디스크 연산이 많아져 너무 느려져서라고 생각했었다. 하지만 오로라는 이를 Log Stream과 병렬 연산을 통해 연산 속도 문제를 해결했으며 그 관련 구조는 다음과 같다.

글에 요지에 벗어나서 구체적으로 다루지는 않지만, 로그 데이터를 incoming queue에서 받아 update queue로 동기식으로 전달하고 그 이후는 비동기 및 병렬로 처리해 데이터 파일에 쓰기 작업이 일어나게 된다. 또, 이렇게 작업된 것들은 S3에 저장함으로 손쉽게 백업을 구현한다. 이를 통해 오로라가 I/O를 기존 RDS MySQL에 비해 더 좋은 성능을 낼 수 있게 되었다.

디스크 I/O는 상대적으로 비싼 작업인 만큼 이는 수백, 수천만 I/O가 발생하는 서비스에서는 엄청난 차이를 만드며, 장기적인 관점으로 봤을 때 엄청난 비용 절감을 가져올 수 있다. 하지만 적은 I/O가 발생할 때는 오히려 RDS를 활용하는게 좋을 수 있다. 실제로 오로라의 최소 인스턴스(의 비용은 0.073 USD/h지만, RDS는 0.016USD/h이다. 또, DBMS가 AWS에 의존도가 높아진다는 단점이 존재한다. 결국 모든 기술에는 정답이 없는 만큼 현재 문제 상황에 맞는 데이터베이스를 적절히 고르는게 무엇보다도 중요할 것이다.

###

각주

  • 데이터 파일에서 쓰기 작업이란 실제 레코드가 디스크에 존재하는 테이블에 저장된다는 의미입니다.
  • 쓰기 연산이란 메모리에서 디스크로 직접 I/O 작업을 진행함을 의미합니다. 여기서 디스크는 SSD가 될 수도 HDD가 될 수 있습니다.

참고

  • https://hoing.io/archives/1114
  • Real MySQL 1권, InnoDB 스토리지 엔진 구조
  • https://dev.mysql.com/doc/refman/8.0/en/innodb-doublewrite-buffer.html
  • https://www.youtube.com/watch?v=7_VXMqYixS4
  • AWS Reinvent 2021: https://www.youtube.com/watch?v=SEXbvl2oQGs
This post is licensed under CC BY 4.0 by the author.

querydsl의 transform 메서드에서 발생하는 connection leak 현상

MySQL 커넥션, I/O 연산, 잠금에 대한 고찰

diff --git a/posts/domain-event-1/index.html b/posts/domain-event-1/index.html new file mode 100644 index 0000000..ee57a93 --- /dev/null +++ b/posts/domain-event-1/index.html @@ -0,0 +1,211 @@ + [Spring] Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기 | Ruggy Blog
Home [Spring] Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기
Post
Cancel

[Spring] Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기

Spring boot에서 Domain Event 활용해 도메인 간의 결합도 낮추기

Goal

  • 업적 달성 시, 데이터베이스에 유저의 업적 달성을 기록한다.
  • 업적 달성은 편지 송신, 좋아요와 같은 유저의 행위가 발생한 후 조건을 만족하면 이뤄진다.
  • 본 글에서는 유저가 편지를 작성할 떄, 이벤트를 발행해 유저의 업적을 달성하는 상황에 대한 코드를 다룹니다.

Solution

  • 도메인 이벤트를 활용해 업적 조건을 만족시키는 것을 이벤트 리스너로 추적한다. 이를 통해 서로 다른 도메인의 결합도를 낮춘다.

문제 상황

업적 달성은 데이터베이스의 유저의 정보 변경이 일어날 때 발생한다. 이는 곧, 유저 테이블에서 Insert와 Update 작업이 발생할 때, 업적 달성이 이뤄진다. 그렇기에 이 작업들에 대해 추적을 해야할 것이다. 여러가지 방법이 존재하겠지만, 이를 코드에서 추적할 수 있다면 좋겠다고 생각했고, 본 프로젝트에서는 Spring data를 사용하고 있어 관련된 해결책을 모색했다.

가장 첫 번째로 생각이 들었던건 단순히 비즈니스 로직을 처리하는 서비스 레이어에서 업적이 일어날 때마다 분기를 넣어 해결하는 방법인데, 이는 서로 다른 도메인의 강결합이 일어나는 문제가 존재했다. 편지 서비스 계층에서 편지 도메인의 편지라는 엔티티가 생성될 때, 유저 도메인의 유저 업적 엔티티를 생성하게 된다면 편지 도메인과 유저 업적 도메인이 강결합을 갖게 된다. 이렇게 다른 컨텍스트에 존재하는 도메인의 결합이 된다면, 추후 편지 작성이 아닌 다른 도메인에 해당하는 행위에 대한 업적을 생성할 때도 해당하는 엔티티와 유저 업적 엔티티는 강결합을 갖게 된다. 이는 극단적으로 유저 도메인과 다른 모든 도메인이 의존관계를 갖게 되는 잠재적 위험성이 있다.

그렇기 때문에 이러한 결합을 끊고, 스프링에서 제공하는 좋은 기능인 Domain Event를 발행해 해결한 사례를 공유합니다.

Domain Event란?

도메인 객체에서 어떤 작업이 실행됬을 때, 발행할 수 있는 이벤트를 의미한다. 이를 통해 객체의 생성이나 변경을 다른 객체와 결합 없이 Event Linstener를 통해 추적할 수 있다. 이를 통해 얻을 수 있는 장점은 다음과 같다.

  • 서로 다른 도메인 로직이 섞일 일이 없다.
  • 확장할 때 발행한 이벤트에 대해 추적하는 Listener만 추가해주면 되기 때문에 확장에 용이하다.
  • 이벤트 발생 이후의 작업(이벤트 리스너의 작업)을 비동기로 처리할 수 있다.

스프링에서 기본적으로 제공하는 ApplicationContext는 이벤트를 발행할 수 있기 때문에, 이를 활용해 만들어진 도메인 이벤트를 활용하는데 어렵지 않게 사용할 수 있다.

코드

AggregateRoot

먼저, spring data에서 제공하는 AbstractAggregateRoot<A> 를 활용한다면, 도메인 이벤트를 어렵지 않게 구현할 수 있다. AbstractAggregateRoot 는 domain event를 간편하게 발행할 수 있도록 만든 모듈이다. 이는 이름에서 볼 수 있듯, 도메인 이벤트를 발행하는 주체는 DDD의 AggregateRoot가 된다는 의미를 내포하고 있다.

Aggregate는 관련 객체를 하나로 묶은 군집을 의미하며, AggregateRoot는 군집 내에서 여러 객체들을 관리하는 루트 엔티티이다. 일반적으로 하나의 엔티티와 여러 개의 Value Object(값 객체)를 지니고 있으며, 하나의 Aggregate에 속한 객체는 같은 라이프사이클을 지닌다. Aggregate를 통해 간의 관계를 확인한다면, 더 상위 수준에서 도메인 간의 관계를 파악하는데 수월해진다.

본론으로 AbstractAggregateRoot<A> 를 이벤트를 발행할 Entity에 상속시키면 된다. 여기서 제너릭 타입(<A>)은 Entity의 타입이 된다. 이를 통해 registerEvent(event) 메소드를 상속받을 수 있으며 이 메소드를 통해 이벤트를 등록할 수 있다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+
public class AbstractAggregateRoot<A extends AbstractAggregateRoot<A>> {
+
+	private transient final @Transient List<Object> domainEvents = new ArrayList<>();
+
+	/**
+	 * Registers the given event object for publication on a call to a Spring Data repository's save methods.
+	 *
+	 * @param event must not be {@literal null}.
+	 * @return the event that has been added.
+	 * @see #andEvent(Object)
+	 */
+	protected <T> T registerEvent(T event) {
+
+		Assert.notNull(event, "Domain event must not be null!");
+
+		this.domainEvents.add(event);
+		return event;
+	}
+
+	...
+}
+

이를 통해 편지 도메인 코드에 반영을 하면, 다음과 같다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+
@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class Letter extends AbstractAggregateRoot<Letter> {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Lob
+    private String content;
+
+    private LocalDate sendDate;
+
+    private boolean isRead;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    private User sender;
+
+		...
+
+		@PostPersist
+    public void created() {
+        this.registerEvent(new LetterCreatedEvent(this.id, this.sender.getId(), this.sender.getUserAchievement().getSendLetterCountValue(), this.receiver.getReceiveCount()));
+    }
+}
+

여기서 @PostPersist 를 통해 이벤트를 발행했는데, 이는 JPA 엔티티의 라이프사이클에서 영속화가 된 이후에 이벤트를 등록하려 했기 때문에 다음과 같이 진행했다. 이는 서비스 계층에서 특별히 호출을 안해도 될 뿐더러, PK값 정책이 IDENTITY이기 때문에 영속화가 된 이후에 키 값을 받은 상태로 이벤트를 발행할 수 있다.

@EventListener

@EventListener를 통해 선언적으로 이벤트를 처리할 수 있다. 이를 통해 이벤트를 받는 코드를 작성하면 다음과 같다. 뿐만 아니라, 특정 조건을 만족하려 한다면 @EventListener(condition) 을 사용한다면 간편하게 활용할 수 있다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+
@Component
+@RequiredArgsConstructor
+public class AchievementPolicy {
+
+    private final UserRepository userRepository;
+    private final AchievementRepository achievementRepository;
+    private final LetterRepository letterRepository;
+
+    ...
+
+    @EventListener
+    public void achieveLevelTwo(LetterCreatedEvent letterCreatedEvent) {
+        Long userId = letterCreatedEvent.getUserId();
+        if (userRepository.existsById(userId) && letterRepository.existsById(letterCreatedEvent.getId())) {
+            achievementRepository.save(new Achievement(LEVEL_TWO.getLevel(), LEVEL_TWO.getName(), LEVEL_TWO.getTag(), userId));
+            userRepository.increaseUserPoint(userId, LEVEL_TWO.getPoint());
+        }
+    }
+
+    @EventListener(condition = "#letterReadEvent.read == true")
+    public void achieveLevelThree(LetterReadEvent letterReadEvent) {
+        Long userId = letterReadEvent.getUserId();
+        if (userRepository.existsById(userId) && letterRepository.existsById(letterReadEvent.getId())) {
+            achievementRepository.save(new Achievement(LEVEL_THREE.getLevel(), LEVEL_THREE.getName(), LEVEL_THREE.getTag(), userId));
+            userRepository.increaseUserPoint(userId, LEVEL_THREE.getPoint());
+        }
+    }
+}
+

테스트

이벤트 발행에 대한 테스트는 @*RecordApplicationEvents* 옵션을 활용할 수 있다. 이는 단일 테스트 실행 시 발행되는 어플리케이션 이벤트를 ApplicationEvents 라는 객체에 저장된다. 공식 문서에는 다음과 같이 나타나 있다.

@RecordApplicationEvents is a class-level annotation that is used to instruct the Spring TestContext Framework to record all application events that are published in the ApplicationContext during the execution of a single test. The recorded events can be accessed via the ApplicationEvents API within your tests.

이에 대한 코드를 작성하면 다음과 같다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+
@RecordApplicationEvents
+@SpringBootTest
+@ActiveProfiles("test")
+public class LetterEventTest {
+
+    @Autowired
+    private ApplicationEvents applicationEvents;
+
+    @Autowired
+    private LetterService letterService;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    private User sender;
+    private User receiver;
+
+    @BeforeEach
+    void setup(){
+        sender = userRepository.save(Fixtures.UserStub.defaultGoogleUser("gmail@gmail.com"));
+        receiver = userRepository.save(Fixtures.UserStub.defaultGoogleUser("receiver@gmail.com"));
+    }
+
+    @Test
+    void letter_created_event_test(){
+        letterService.writeLetter(new LetterRequest("content", List.of()), sender);
+        letterService.writeLetter(new LetterRequest("content", List.of()), sender);
+        assertThat(applicationEvents.stream(LetterCreatedEvent.class).count()).isEqualTo(2);
+    }
+}
+

하나의 편지를 작성할 떄(DB에 편지를 저장할 떄) 정상적으로 이벤트가 발행되는 것을 볼 수 있다.

아쉬운 점

현재 EventListener 내 작업은 트렌젝션이 보장되어야 한다. 이에 대해 이벤트 리스너의 트랜잭션을 어떻게 처리할 지 알아보면 좋을 듯 싶다.

레퍼런스

This post is licensed under CC BY 4.0 by the author.

[Testing] Mockito를 이용한 Service Layer Unit Testing

중요한 일에 집중하기

diff --git "a/posts/gijol-\355\232\214\352\263\240/index.html" "b/posts/gijol-\355\232\214\352\263\240/index.html" new file mode 100644 index 0000000..d10b84e --- /dev/null +++ "b/posts/gijol-\355\232\214\352\263\240/index.html" @@ -0,0 +1 @@ + [Project] Gijol MVP 런칭 회고 | Ruggy Blog
Home [Project] Gijol MVP 런칭 회고
Post
Cancel

[Project] Gijol MVP 런칭 회고

배포 링크: Gijol

BE

배경

GIST 청원 프로젝트를 진행하면서 크게 제품의 코드 작성법과 테스트, Operation, Git flow 등을 배울 수 있었다. 이를 청원팀의 팀원으로서 배웠던 점이 많은데 과연 이 배운 것들을 내가 스스로 해나갈 수 있을지에 대해서 의문이 들었다. 이에 대한 중요성과 필요성이 너무나도 중요하게 느껴졌기에 프로젝트에서 사용하면 좋은 부가적인 것들이 아니라, 필수적인 것들로 느껴졌다. 그렇기에 새로운 프로젝트를 진행하면서 배운 방법론적인 부분을 적용해보고 싶었다.

Gijol 프로젝트는 기존에 진행하던 프로젝트를 완성하지 못해 아쉬움이 남아 진행한 프로젝트이다. 학교 졸업요건을 특별히 확인할 수 있는 서비스가 아직 없기 때문에 이를 학사편람과 직접 비교와 대조하면서 졸업요건을 확인해야 했고, 이 부분에서 졸업요건 확인을 심지어 놓쳐 한 과목 때문에 졸업을 못하는 사례도 발생했다. 이 부분에서 Pain Point는 확실하다고 느꼈고 본격 프로젝트를 진행하게 되었다.

성장

뿐만 아니라 내가 생각하는 좋은 팀이라는 기준에 맞아서 진심으로 몰입해보고 싶다는 생각이 들었다. 팀 상태는 한 명은 운영은 아니지만, 기본적인 FE 개발 경험이 있었고. 한 팀원는 개발을 처음 진행해봤지만, 몰입의 중요성을 너무나도 잘 알고 현 상황에서 팀에 도움이 되는 본인이 할 수 있는 최선을 다했었다. 그에 따라 개발적인 성장도 놓치지 않으면서 최대한 도움을 받을 수 있는 부분을 활용하면서 성장을 해나갔다. 그 결과 React + Typescript를 2달 가량만에 코드적인 부분에 기여를 할 수 있었다.

이 팀원의 성장은 크게 모르는 것과 대해 투명하게 공유한다는 점에서 기인했다고 생각한다. 처음 개발을 해보는 팀원이 본인이 겪는 어려움에 대해 우연히 말을 하다가 겪었던 어려움을 들었다. 이에 대한 어려움을 호소할 때 혼자 고민했던 부분, 병목이 되었던 부분, 그로 인한 본인이 느끼는 감정을 공유함으로 충분히 공감이 되었다. 이를 통해 우리 프로젝트를 진행하는데 학습에 대한 시간이 더 필요하다고 판단해 일정 조율을 하는 현실적인 대안을 세울 수 있었다. 그 결과 팀원이 성공적으로 코드를 기여하고 자신감을 되찾을 수 있었다. 뿐만 아니라 팀의 전체적인 생산성도 높였다. 이를 통해 같은 팀원과 투명한 의사소통을 통해 발생할 수 있는 위험을 줄일 수 있었다. 이후 현업에서 나도 같은 상황에 놓일 수 있는데, 일정에 영향이 갈 정도의 정말 어려운 부분에 대해 부끄럽다고 느껴 위축되고 문제를 만들기보다 투명한 공유가 필요할 수 있겠다는 생각이 들었다.

개인적으로 반성하게 되는 점이 나는 무언가를 잘못한다는 생각이 들때 마다 너무나도 부끄러워 위축되는 경향이 있다. 항상 거기서 현실적인 대안으로 한 걸음 더 발전해나가려 생각해보지는 못하는 것 같다. 이에 대한 의식적인 개선이 필요하고 적용해나가야 한다.

What I did

내가 진행하고 고민한 부분들을 요약하면 다음과 같다.

전반적인 Project Management 및 조직 관리

  • Backlog 작성을 진행한 후 Sprint를 진행
  • Linear 도입을 통해 스크럼 보드 도입
  • 팀 작업 규칙(Working Agreement) 도입
  • 1차 릴리즈 후 Sailboat 회고 진행
  • 새로운 기술 도입에 대한 의사결정 조율 → 이 기술이 우리에게 정말 필요한가?에 대한 고민

프로젝트 Automation of Operation

  • Github Action을 통해 FE, BE 배포 자동화 → 구축을 통해 배포 자동화를 통한 생산성 향상을 팀에 알릴 수 있었다.
  • Git Flow → dev, prod를 통해 브런치를 관리하고, merge 규칙에 대한 논의를 진행했다. 또한, 브랜치 네이밍 컨벤션에 대해서도 논했다. 이를 통해 스타일에 대한 통일을 진행했다.
  • Github와 Linear의 통합, Linear과 Slack 알림 통합

벡엔드 API 개발

  • 졸업요건 확인 객체에 대한 도메인 설계 및 구현
  • POI를 활용한 Excel 파싱

What I Learned

기본적인 프로젝트 운영 방식

함께 일하는 것은 효율을 극대화하기 위해 진행하다는 것을 배울 수 있었다. 기존에 토이프로젝트는 여러번 리딩을 진행해봤지만, 실제로 런칭할 프로젝트를 리딩해본 경험은 처음이었다. 프로젝트를 진행해보면서 어느 정도 서로가 일적인 가치가 맞는 팀원이었기에 방법론 도입에 대한 공감대 형성이 잘 되었던 것 같다.

SW 마에스트로를 진행하면서 에자일, 스크럼, 협업에 관한 특강을 관심을 갖고 열심히 들었다. 이를 Gijol 프로젝트에서 바로바로 사용할 수 있었던 점이 굉장히 체화하기 좋았었다. 이 경험을 바탕으로 SW마에스트로 과정도 비슷하게 운영해나갈 계획이다.

도메인 설계와 테스트

졸업요건은 전공, 교양, 기초과학, 기타과목과 같은 각각 분야에 대한 조건을 맞춰야 충족할 수 있다. 이를 하나의 객체에 모두 책임을 지게하는 것은 데이터 주도적인 설계로 느껴 각각 분야를 하나의 객체로 둬서 책임을 분할했다. 예를 들어, 전공은 Major라는 객체로, 기초과학은 BasicScience 라는 객체로 분리했다. 이는 각각의 책임이 명확해질 뿐더러 각 기능에 대한 테스트도 용이했다.

특별히 데이터베이스를 사용하지 않기 때문에 비즈니스로직이라고 할 것들이 크게 없었다. 다만, 졸업요건을 확인하는 알고리즘을 짜는데 시간이 걸렸다. 처음에는 단순 알고리즘이기에 테스트를 미뤘다. 이는 굉장히 오만했음을 느낄 수 있었다. 테스트를 안짜고 변경에 취약한 부분들이 테스트를 통해 오류가 너무나도 많이 발견되었다. 이는 결국 운영서버에 배포까지 오류가 생겼고, 결국 각각 전공에 대한 유닛테스트를 모두 작성하게 되었고 기본적이고 중요한 기능에 대한 기능은 유닛테스트를 반드시 해야된다는 것을 느꼈다.

TMI로 jar 빌드에서 classResourcegetFile() 하게 되면 빌드가 깨진다. 이는 jar에서는 File:// 같은 프로토콜을 제공하지 않아서라고 한다. 이는 getInputStream() 을 사용해야된다는 것을 느끼게 해줬다.

To-do

회고를 진행했을 때, 코드적인 부분의 투명한 공유가 부족했다는 점이 우리팀의 합의안 중 하나였다. 그렇기에 코드리뷰 문화를 일의 진행에 차질이 생기지 않는 선에서 강제하기로 Working Agreement를 추가했다. 나는 Java Spring 기반의 코드를 주로 작성하기에 FE 부분에 코드리뷰에 관여를 하기 어려웠다. 다만, 팀원들 스스로 객체 지향 설계의 필요성과 코드 개선에 대한 필요성을 너무 느끼고 있는 상황에서 내가 할 수 있는 것을 생각해보게 되었다.

Spring은 SOLID를 어느정도 Framework에서 강제한다. 그렇기 때문에 객체지향 설계에 대해 꾸준히 고민하게 된다. 뿐만 아니라, 우아한테크코스 프리코스 과정을 통해 객체지향 설계에 대한 고민을 진행해봤고, 최근에 객체지향 프로그래밍 수업에서 프로젝트(프로젝트는 객체지향과 사실과 오해라는 책에 소개한 객체 지향을 입각해 진행했다)를 진행하면서 객체 지향 설계에 대한 설명을 가장 잘할 수 있을 때가 아닌가라는 생각을 하게되었다. 뿐만 아니라 기존에 나는 데이터 주도 설계를 진행해보고 이는 객체 지향적이지 못하다는 것을 느꼈고 이 경험을 공유해주면 팀적으로 너무나도 좋을 것 같다는 생각이 들었다.

이러한 생각을 바탕으로 React를 코드리뷰가 가능한 선에서 공부해보기로 결심했다. 이는 장기적인 팀적 성장으로 봤을 때 팀적 생산성이 복리로 돌아올 수 있을거라 느껴 공부를 시작한 것이다. 또한, 새로운 것에 대한 학습으로 받아들일 수도 있겠다 내가 생각하는 학습이 맞는지에 대한 검증을 할 수도 있겠다. 이를 바탕으로 최대한 빠른 시간으로 React라는 것에대해 공부를 해보고 코드를 어느 정도 작성해보면서 Gijol 팀의 리뷰 문화를 개선하는 것을 목표로 두고있다.

벡엔드 개발자로서 역량도 향상에 힘쓸 생각이다. Gijol MVP는 DB 사용이 필수는 아니라는 것이 결론이었기 때문에, 아직 DB를 사용하지 않는다. 다만, 졸업요건 확인의 상태 유지를 할 계획이라는 점과 강의평가 기능을 추가한다는 점으로 사용자의 uniqueness를 검증해야 한다. 그렇기 때문에 다음 스프린트부터는 DB 도입을 진행할 계획이다. 그렇기에 DB와 설계에 대한 고민과 JPA 도입을 염두하고 있다.

마지막으로

아직 부족하게 많은 프로젝트였지만, Gijol 첫 MVP는 개발자로서 내가 중요하게 여기는 가치, 개발자를 너머 삶의 가치까지 생각해보게 되는 뜻깊은 시간이었다. 생각했던 것이 실현되는 것만큼 기쁘고 자아실현에 도움을 주는 것은 없는 것 같다. 물론 이 기대가 너무 커지면 불행으로 다가오는 것을 항상 인지해야 한다. 기대가 너무 크면 실망도 그에 상응한다.

공동 목표를 지닌 팀원들과 함께 정말 진심어린 몰입을 통해 기대 이상의 가치를 느끼는 것은 어떤 가치와도 맞바꾸기 어려운 것같다.

프로젝트는 본 링크에서 확인하실 수 있습니다.

This post is licensed under CC BY 4.0 by the author.

[Think] 삶의 가치

[Spring] Bulk insertion in Spring Project

diff --git a/posts/git-submodule/index.html b/posts/git-submodule/index.html new file mode 100644 index 0000000..ff0f70a --- /dev/null +++ b/posts/git-submodule/index.html @@ -0,0 +1,39 @@ + Spring Boot에서 git submodule로 민감 정보(yml) 관리 | Ruggy Blog
Home Spring Boot에서 git submodule로 민감 정보(yml) 관리
Post
Cancel

Spring Boot에서 git submodule로 민감 정보(yml) 관리

서브 모듈을 통해 민감 정보 관리

3줄 요약

  • 민감정보 데이터를 담을 private repo 생성
  • 민감정보가 담긴 private 레포지토리를 public 레포지토리의 서브모듈로 git add submodule ${서브 모듈로 등록할 github repository의 주소} 을 사용해 등록한다.
  • 원격 서브모듈 레포지토리에 있는 파일들을 git submodule update --remote을 이용해 로컬에 있는 서브모듈 폴더로 가져 온다.

++ gradle이나 github action으로 서브모듈을 잘 사용한다.

구체적 과정

  • privates Repo 생성

  • submodule 등록한다.

그렇다면 다음 나와있듯 /be 경로(프로젝트의 최상단 경로)에 PDG-privates(submodule repo 이름)라는 폴더가 생성된다.(submodule의 내용들)

git add submodule ${서브 모듈로 등록할 github repository의 주소}

  • .gitmodules 추가

submodule의 path(file 명)와 url(github url)을 추가해준다. 단 여기서 submodule의 default branch가 master가 아니라면 반드시 branch

  • git submodule update --remote

git submodule 방식은 branch의 hash를 작성하는 방식이다. 그렇기 때문에 git submodule update --remote 을 진행하면 submodule의 내용이 update 된다. 본 프로젝트에는 hash가 변경 된다. 이후 반드시 본 프로젝트의 git commit을 진행해야 hash가 제대로 업데이트 된다. 다시 말해, git commit -am "message" 를 진행하고 Push를 해야 한다!

Example

현재 프로젝트는 다음과 같이 설정돼 있다.

이후 협업을 진행하거나 외부에서 submodule을 수정했다는 것을 가정하고 Remote에서 다음과 같이 변경한다.

이후 다음 커맨드를 입력한다.

1
+
git submodule update --remote
+

그렇다면 다음과 같이 hash 값이 checkout 됐다고 나온다.

실제로 PDG-privates 폴더 안의 내용이 remote와 같이 변경된다. git diff 를 통해 hash를 확인해봐도 9330cf ~로 변경된 것을 볼 수 있다.

이를 커밋하고 push하면

Gradle를 이용해 local에서 submodules의 내용을 빌드 시 가져오기

로컬 privates 를 받아올 때, gradle를 사용하면 편리하게 submodule의 내용을 가져올 수 있다.

1
+2
+3
+4
+5
+6
+7
+
task copyPrivate(type: Copy) {
+    copy {
+        from './PDG-privates'
+        include "*.yml"
+        into 'src/main/resources/privates'
+    }
+}
+

이를 설정하고 build시 submodule의 yml파일들을 ‘src/main/resources/privates’로 가져온다. 이 때, 반드시 ‘src/main/resources/privates’를 .gitignore에 추가해줘야 한다.

CI/CD in Github Action

1
+2
+3
+4
+5
+
- name: Checkout 
+		uses: actions/checkout@v1 
+		with:
+		  token: $ 
+		  submodules: true
+

를 workflow file에 추가해주면 된다.

Error

Problem

  • branch를 찾을 수 없다고 표시됐다. 아마 default가 master라서 그런듯 하다.
1
+2
+
fatal: Needed a single revision
+Unable to find current origin/HEAD revision in submodule path 'PDG-privates'
+

Solution

  • branch가 main인데 설정이 HEAD로 돼있었다.
  • .gitmodule에서 branch를 main으로 변경해줬더니 성공했다.
1
+2
+3
+4
+
[submodule "PDG-privates"]
+   path = PDG-privates
+   url = <https://github.com/2022-solution-challenge/PDG-privates>
+   branch=main
+

Reference

Git - 서브모듈

Github Action 에서 Submodule 설정 방법

This post is licensed under CC BY 4.0 by the author.
diff --git "a/posts/git-\354\230\244\353\236\230\354\240\204-\354\273\244\353\260\213\355\225\234-\353\257\274\352\260\220-\354\240\225\353\263\264-\354\247\200\354\232\260\352\270\260/index.html" "b/posts/git-\354\230\244\353\236\230\354\240\204-\354\273\244\353\260\213\355\225\234-\353\257\274\352\260\220-\354\240\225\353\263\264-\354\247\200\354\232\260\352\270\260/index.html" new file mode 100644 index 0000000..0c92e10 --- /dev/null +++ "b/posts/git-\354\230\244\353\236\230\354\240\204-\354\273\244\353\260\213\355\225\234-\353\257\274\352\260\220-\354\240\225\353\263\264-\354\247\200\354\232\260\352\270\260/index.html" @@ -0,0 +1,9 @@ + [Git] 오래 전 커밋한 깃허브 민감 정보 지우기 | Ruggy Blog
Home [Git] 오래 전 커밋한 깃허브 민감 정보 지우기
Post
Cancel

[Git] 오래 전 커밋한 깃허브 민감 정보 지우기

팀원분께서 예전에 aws key를 yml파일에 넣으셨다. 이때 문제를 인식하고 커밋이 안 쌓였을 때 지웠어야 하는데 이런저런 핑계를 대며 지우지 않았는데 오늘 레포를 public으로 변경했는데 문제가 터졌다.

보통 가장 최근 커밋은 git reset HEAD 로 지울 수 있는데 한참 전 기록이 발목을 잡았던 것이다. 이를 해결하는데 겪은 고난을 소개하고자 한다.

민감 정보를 지우는데는 2가지 방법이 있다.

  1. 전체 커밋 기록을 지우고 다시 push 하기
  2. git bfg를 사용하기 → 커밋 중 파일의 몇몇 부분을 변경할 수 있다.

1번은 개인적으로 커밋한 흔적은 어떻게보면 버전인데 버전을 모두 지우기에 risk가 너무 크다는 생각이 들었다.(고생한 흔적도 지워지고) 그렇기에 2번을 이용해 커밋 기록을 그대로 남기면서 어떻게 민감 정보를 변경했는지 소개하고자 한다.

brew를 사용할 수 있다는 가정하에 작성한다.

bfg는 git-filter-branch의 대안으로 나온 repo cleaner이다. scala로 작성돼 git filter branch보다 빠를 뿐더러 사용하기 매우 간편하다.

mac의 경우 brew를 이용해 간편히 설치할 수 있다.

1
+
brew install bfg
+

지우려는 기록은 다음과 같다.

이후 project의 root directory에서 password.txt라는 파일을 만든다.(다른 이름도 괜찮다.) regex 문법을 이용하는데, regex를 크게 몰라도 간편하게 사용할 수 있다. 다음과 같은 형식을 사용하면 된다.

1
+
{민감정보}==>{변경정보}
+

관련해서 구체적인 예시는 다음 사이트를 보면 이해가 더 잘된다.

링크

이 때 yml파일 같은 경우 앞의 공백이 생기기 때문에 이를 맞춰주려면 공백도 그대로 포함해야 한다.

이를 저장하고 다음과 같은 커멘드를 입력한다.

1
+
bfg --replace-text password.txt
+

성공적으로 진행되면 다음과 같이 나온다.

민감 정보가 들어있는 yml 파일이 변경됬다고 잘 나온다. 그리고 마지막에 있는 command를 입력한다.

1
+
git reflog expire --expire=now --all && git gc --prune=now --aggressive
+

이후 다른 repo를 새로 만들어서 git push하면 된다.

Reference

BFG Repo-Cleaner

This post is licensed under CC BY 4.0 by the author.

-

[우아한테크코스 4기 프리코스] 1주차 및 2주차 후기

diff --git a/posts/index.html b/posts/index.html new file mode 100644 index 0000000..e6f6ede --- /dev/null +++ b/posts/index.html @@ -0,0 +1,11 @@ + + + + Redirecting… + + + + +

Redirecting…

+ Click here if you are not redirected. + diff --git a/posts/log4j-vulnerability/index.html b/posts/log4j-vulnerability/index.html new file mode 100644 index 0000000..f9d7bc2 --- /dev/null +++ b/posts/log4j-vulnerability/index.html @@ -0,0 +1 @@ + Spring Boot log4j vulnerability 확인 및 변경 | Ruggy Blog
Home Spring Boot log4j vulnerability 확인 및 변경
Post
Cancel

Spring Boot log4j vulnerability 확인 및 변경

Log4j 이슈로 인해 우리 프로젝트에서 log4j를 버전을 확인해봤는데 2.14.1 버전을 사용하고 있었다. 이는 Intellij에서 External Library에서 확인할 수 있다.

log4j 2.14.1 버전은 CVE-2021-44228 에서 나와있듯, 보안취약점에 영향을 받는 버전이다. 다음 사진에서 나와있듯, 2.15.0 버전으로 변경해야 한다.

하지만 CVE-2021-44832 에서 2022년 1월 16일 기준으로 log4j에 대한 보안 취약점이 Java8 이상을 기준으로 2.17.0 버전까지 발견됐다. 관련해서는 다음 사진에 나와있다.

3

따라서 우리 프로젝트는 Java11을 사용하기 때문에 log4j 2.17.1로 변경했다. gradle(build.gradle)에서 사용하는 log4j에 대한 dependency를 다음과 같이 추가하면 된다.

implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.17.1'

implementation 'org.apache.logging.log4j:log4j-api:2.17.1'

구체적인 것은 다음 PR에서 확인할 수 있습니다.

https://github.com/GIST-Petition-Site-Project/GIST-petition-server/pull/167

This post is licensed under CC BY 4.0 by the author.
diff --git a/posts/mysql-resources/index.html b/posts/mysql-resources/index.html new file mode 100644 index 0000000..b8a0db1 --- /dev/null +++ b/posts/mysql-resources/index.html @@ -0,0 +1 @@ + MySQL 커넥션, I/O 연산, 잠금에 대한 고찰 | Ruggy Blog
Home MySQL 커넥션, I/O 연산, 잠금에 대한 고찰
Post
Cancel

MySQL 커넥션, I/O 연산, 잠금에 대한 고찰

실무에서 MySQL 데이터베이스를 활용하면서 커넥션, I/O 연산, 잠금에 대해 더 유심히 살펴보게 되었다. 요 세가지 자원이 중요하다고 배웠는데 실제로 사용해보면서 정말 그렇다는걸 느낄 수 있었다. 그렇기에 이를 통해 배운점들을 다시 상기해고자 한다.

커넥션

데이터베이스를 사용하면서 가장 중요한 자원을 꼽는다면 커넥션이다. 커넥션이 부족해지는 순간 쿼리를 날릴 수 없다는 의미이며, 이는 곧 장애로 이어진다. 그렇기에 데이터베이스 커넥션 개수를 원활하게 모니터링하고 관리하는 부분은 굉장히 중요하다.

커넥션이 부족해지게 만드는 요소로는 트래픽 증가로 서버의 스케일링으로 커넥션이 부족해질 때가 존재한다. 또, 트랜잭션이 길어져 해당 작업이 커넥션을 계속 잡고 있으면 db 커넥션이 부족해질 수 있다. 혹은 어플리케이션에서 커넥션을 제대로 반환하지 않는 경우도 존재할 수 있다.

항상 예상치 못한 상황이 발생하기에 db 커넥션 수를 잘 모니터링하고 제대로된 알람을 받는게 가장 중요하다. 이외에도 각 구성요소의 커넥션 풀과 timeout을 잘 설정해 db 커넥션이 부족하지 않도록 신경써야 한다. 부하테스트를 통해 선제적으로 방지하는 것도 방법이 될 수 있다.

I/O 최적화

커넥션을 적절히 확보한다면, I/O에 대해서 고민해볼 것 같다. DBMS는 결국 데이터를 HDD나 SSD에 읽기와 쓰기 에 대한 I/O를 진행해야 한다. 이 I/O는 사소한 차이에 성능이 천차만별 만큼 차이가 나기 때문에, I/O 연산이 정확한 수치까지는 아니더라도 대략적으로 얼마나 나오는 지 알아두는 건 굉장히 중요하다.

조회 I/O 최적화는 조인과 인덱스가 큰 영향을 준다. 조인을 잘못하면 테이블 간의 레코드 개수의 곱만큼의 순차 I/O가 발생할 수 있으며, 인덱스가 없는 컬럼의 테이블을 조회하면 레코드 수만큼의 순차 I/O가 발생할 수 있다. 그렇기에 인덱스를 활용해 대량의 순차 I/O를 소량의 랜덤 I/O로 변경하면 유의미하게 성능이 향상된다. 하지만 인덱스를 생성하는 I/O로 인해 insert, update, delete query의 성능을 저하시킬 수 있으며, 데이터 분포도가 고르지 않을 때 인덱스를 활용하면 조회 성능 마저 기존보다 느려질 수 있다.

또, 인덱스를 제대로 활용하지 않고 다량의 데이터를 조회하는 쿼리가 여러개가 작동하게 되면 CPU 사용량이 급증하게 된다. 이 역시 상황에 맞게 적절히 인덱스를 활용하고 조인문을 적절히 활용하는게 좋겠다. 실제로 나는 조인절 인덱스를 고려하지 않아 슬로 쿼리를 만들어 CPU를 높이는 실수한 경험이 있다..

커멘드 작업에도 I/O에 대해 고민해보는게 좋다. insert문과 update문을 사용할 때, 다량의 데이터를 넣을 때에는 단건의 쿼리를 고려해보는 것도 방법이다. 이는 서버와 db 간 네트워크 I/O와 인덱스 업데이트 과정의 Disk I/O를 줄일 수 있게된다. 실제로 mysql 공식 문서에서도 insert 속도 성능 향상을 위해 여러 단건 insert문보다 하나의 bulk insert문을 활용하라고 권장한다.

물론 bulk insert가 orm에서 기본적으로 제공하지 않을 수도 있다. 예를 들어 현재 회사에서 활용하는 spring jpa에서 컬렉션 타입에 대해 insert 혹은 update를 진행하게 되면 기본적으로 단건으로 여러 쿼리가 발생하는데, 수십~수백건에 대해서는 문제 없이 작동하지만 수천건이 넘어가는 순간부터 유의미하게 속도가 느려진다. 이 상황에서 jdbcTemplate 등을 활용해 raw query를 만드는 방법 등을 통해 bulk insert를 구현해 문제를 해결할 수도 있다.

하지만 모든 상황에서 이를 활용하는게 정답은 아니다. 저장해야할 데이터가 수천건이 넘어가 비즈니스 로직에 문제가 생긴다면 bulk insert를 구현해 성능을 최적화 할 수도 있을 것이고, 이 구현 역시 비용이 될 수 있으므로 단순히 spring data jpa의 saveAll()이나 변경 감지를 활용해 간단하게 처리할 수도 있다. 결국 상황에 맞게 적절히 활용해야 한다.

잠금(Lock)

I/O를 잘 진행하면 그다음 고려할 것은 잠금이다. MySQL은 MVCC를 제공하기에 여러 잠금 레벨을 제공하며, 잠금에 따라 성능이 천차만별이다. 잠금은 데이터 정합을 위한 장치이다. 따라서 성능과 데이터 정합은 반비례한다고 보면 된다. 정합을 중요시해서 잠금을 높은 수준(길게)으로 설정한다면 성능에 이슈가 발생할 수 있다. 반면 정합이 상황에 따라 조금씩 틀리더라도 잠금을 낮은 수준으로 설정하면 성능에 큰 이점을 가져올 수 있다.

실제로 잠금을 직접 건드릴 일이 크게 많지는 않았지만, 이로 인한 문제가 발생할 때가 종종 있엇고 어떤 잠금이 문제를 일으키는 지 확인하는 과정은 중요했던 것 같다. 작업을 하면서 고려해야 할 잠금은 여러 개가 존재하겠지만 몇몇 사례를 소개한다.

  • 수천만건 데이터가 있는 테이블의 alter 문 실행, index 추가 및 제거 작업을 진행할 때 잠금
  • 트랜잭션 격리 레벨에 따른 잠금
  • 외래키의 잠금 전파
  • 유니크 인덱스의 읽기/쓰기 잠금
  • 어플리케이션의 낙관적/비관적 잠금

결국 쿼리든, 잠금이든 정답은 존재하지 않는다. 도메인과 풀어야 하는 문제 상황에 맞게 적절히 쿼리를 작성하고 잠금 수준을 고려하는게 중요하다. 결제 정보와 같이 정합이 중요하다면 잠금을 직접 걸거나 고려해볼 수도 있으며, 가볍게 여러번 조회하는 타임라인, 목록과 같은 정보의 경우 조회 결과가 조금씩 달라도 큰 문제가 없다면 잠금 수준을 낮게 설정하는 것도 방법이다. 이 상황에서 오히려 정합을 위해 잠금 수준을 높게 두어 속도가 느리다면 고객 경험 측면에서 더 손해일 수도 있다.

잠금이나 쿼리에 대해서 최적화를 할 때 꼭 RDBMS에만 의존해야할 필요가 없을 수도 있다. 조회를 위해 인덱스를 거는 대신에 key-value db, document db로 캐싱을 고려하는 것도 방법이며, 어떤 작업에 대한 잠금이 필요할 때 Redis로 분산 락을 거는 것도 방법이다. 결국 방법은 여러 개이기에 지금 상황의 문제에 맞게 팀원과 합의해 최선의 방법으로 해결하는게 가장 중요하다.

This post is licensed under CC BY 4.0 by the author.

Amazon Aurora 스토리지 엔진과 MySQL InnoDB 스토리지 엔진 비교

Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점

diff --git a/posts/querydsl-connection-leak/index.html b/posts/querydsl-connection-leak/index.html new file mode 100644 index 0000000..7b1a791 --- /dev/null +++ b/posts/querydsl-connection-leak/index.html @@ -0,0 +1 @@ + querydsl의 transform 메서드에서 발생하는 connection leak 현상 | Ruggy Blog
Home querydsl의 transform 메서드에서 발생하는 connection leak 현상
Post
Cancel

querydsl의 transform 메서드에서 발생하는 connection leak 현상

문제 상황

회사에서 모든 환불은 어드민 서버를 거쳐 환불 서버에 환불 요청을 보내 환불 프로세스가 진행된다. 하지만 환불 서버에서 요청을 제대로 보내고 환불을 완료했지만, 어드민 서버에서 히스토리를 DB에 기록하는 작업이 제대로 이뤄지지 않았다. 그렇기에 어드민 히스토리와 환불 기록의 불일치가 발생했고, 이 운영 이슈를 해결하는 과정을 남기려고 한다.

실제로 핀포인트 로그는 다음과 같이 나타났다.

대략적으로 보면 알듯, 모두 30초(혹은 그 이상)에서 오류가 발생한 이력이 있는데 이는 어딘가에서 Timeout이 발생했음을 알 수 있다. 더 구체적으로 들어가서 확인해보면 에러가 다음과 같이 발생했다.

HikariPool-1 - Connection is not available, request timed out after 30000ms.

해결 과정

처음에 생각한 문제는 슬로우 쿼리가 커넥션을 오래 잡거나 배포 중 ECS 오토스케일링이 활성화되어 DB 커넥션이 부족해진 이유인 줄 알았다. 하지만 AWS 로그를 직접 확인해본 결과 당시 배포가 이뤄지지 않았으며 DB 커넥션 개수는 넉넉했었고 쿼리들도 문제가 없었다.

그렇기에 인프라적 문제보다는 어플리케이션 서버의 문제라고 생각했고, 어플리케이션 로그와 코드를 살펴봤다. 그 결과 문제는 querydsl에서 transform() 메서드를 잘못 사용하고 있어 발생했음을 확인할 수 있었다. 실제로 문제 상황과 비슷한 상황을 개발 서버에서 재현했을 때 transform() 메서드를 동시에 호출할 때 여러 요청들이 쿼리가 실행된 이후에도 커넥션을 계속 물고 있었으며 hikari의 모든 커넥션을 물게 되면, 다른 요청들은 hikari로부터 커넥션을 대기하게 되고 타임 아웃(30초)가 발생했다.

querydsl에서 transform() 메서드는 쿼리 결과를 grouping해서 Map으로 변환해주는 기능을 제공한다. 하지만 querydsl에서 query가 종료될 때 사용되는 메서드들이 queryTerminatingMehtods에 존재하는 메서드들이라면 JPA EntityManager를 close해준다. queryTerminatingMehtods에 존재하는 항목은 다음과 같다.

querydsl에서 자주 쓰는 fetch()fetchOne()같은 메서드는 queryTerminationMethods가 위의 메서드 리스트에 존재한다.

반면, 쿼리가 종료되는 메서드가 존재하는 ResultTransformer 인터페이스의 transfrom 메서드가 사용하는 query는 모두 iterate()로 종료된다. 따라서 queryTerminationMethods의 메서드가 아니며, EntityManager가 제대로 커넥션을 닫히지 않게 된다.

해결 방법

문제를 찾는 데에 비해 해결하는 방법은 굉장히 간단했다. querydsl transform() 메서드를 사용하는 쿼리에 @Transactional(readOnly=true)를 붙여주면 된다. querydsl도 결국 JPA를 기반으로 만들어진 라이브러리이며, @Transactional 을 갖는 메서드가 끝날 때 JpaTransactionManagerdoCleanupAfterCompletion()을 통해 커넥션을 모두 정리해준다.

아쉬운 점

transform이 connection을 계속 물고 있어 오류가 발생하는 부분은 이해했지만, 결국에는 Hikari pool로 커넥션을 언젠가 돌려주게 된다. 이 돌려주는 원리가 hikari의 max-lifetime(기본 180초)로 인해 돌려주는 것인지, OS에 의해 좀비 스레드가 정리되는 것인 지 명확히 알아내지는 못했다.

또, querydsl 5.0.0 기준으로 transform() 메서드에서 커넥션이 왜 반납이 안되는 지 내부 원리를 구체적이고 명확히 알아보려 한다. 관련해서는 다음 글에서 풀어내 보자.

참고

  • https://github.com/querydsl/querydsl/issues/3089
  • https://colin-d.medium.com/querydsl-에서-db-connection-leak-이슈-40d426fd4337
  • https://cljdoc.org/d/hikari-cp/hikari-cp/3.0.1/doc/readme
This post is licensed under CC BY 4.0 by the author.

감정적 결정과 상황 귀인

Amazon Aurora 스토리지 엔진과 MySQL InnoDB 스토리지 엔진 비교

diff --git a/posts/spring-boot-cahce-error-handling/index.html b/posts/spring-boot-cahce-error-handling/index.html new file mode 100644 index 0000000..a1f84fe --- /dev/null +++ b/posts/spring-boot-cahce-error-handling/index.html @@ -0,0 +1,517 @@ + Spring Boot Caching에서 에러 핸들링하는 방법 | Ruggy Blog
Home Spring Boot Caching에서 에러 핸들링하는 방법
Post
Cancel

Spring Boot Caching에서 에러 핸들링하는 방법

본 글은 아래 링크의 글의 내용과 이어집니다.

Spring에서 제공하는 @Cacheable을 이용하면 캐쉬를 AOP 기반으로 쉽게 사용할 수 있습니다. 이전 글에서 다뤘듯, 기본적으로 @Cacheable 을 실행하는 Aspect 메소드에서 예외가 발생하면 전체 메소드 자체가 실패하게 됩니다. 해당 상황에서 에러 핸들링을 통해 특정 상황에서 Exception이 발생하지 않도록 변경하는 방법을 소개합니다.

@CacheableCacheErrorHandler 동작 원리

Spring 문서에 따르면 Error Handler는 SimpleCacheErrorHandler를 기본 값으로 사용하고 있으며 기본적으로 에러를 클라이언트에 직접 반환합니다.SimpleCacheErrorHandler 코드는 다음과 같습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+
// package org.springframework.cache.interceptor;
+public class SimpleCacheErrorHandler implements CacheErrorHandler {
+
+	@Override
+	public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
+		throw exception;
+	}
+
+	@Override
+	public void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value) {
+		throw exception;
+	}
+
+	@Override
+	public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
+		throw exception;
+	}
+
+	@Override
+	public void handleCacheClearError(RuntimeException exception, Cache cache) {
+		throw exception;
+	}
+}
+

해당 ErrorHandler를 CachingConfigurer 에 대한 구현체 중 errorHandler() 의 리턴값으로 등록하면 됩니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+
// package org.springframework.cache.annotation;
+
+public interface CachingConfigurer {
+
+  ...
+  
+	@Nullable
+	default CacheManager cacheManager() {
+		return null;
+	}
+	
+	...
+
+	@Nullable
+	default CacheErrorHandler errorHandler() {
+		return null;
+	}
+
+}
+

CachingConfigurer 를 빈으로 등록하면 spring-context에서에서 기본 빈으로 등록되는 AbstractCachingConfiguration 로 인해 등록되게 됩니다. 코드는 다음과 같습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+
// package org.springframework.cache.annotation;
+
+@Configuration(proxyBeanMethods = false)
+public abstract class AbstractCachingConfiguration implements ImportAware {
+
+	@Nullable
+	protected AnnotationAttributes enableCaching;
+
+	@Nullable
+	protected Supplier<CacheManager> cacheManager;
+
+	@Nullable
+	protected Supplier<CacheResolver> cacheResolver;
+
+	@Nullable
+	protected Supplier<KeyGenerator> keyGenerator;
+
+	@Nullable
+	protected Supplier<CacheErrorHandler> errorHandler;
+
+  ...
+
+	@Autowired
+	void setConfigurers(ObjectProvider<CachingConfigurer> configurers) {
+		Supplier<CachingConfigurer> configurer = () -> {
+			List<CachingConfigurer> candidates = configurers.stream().collect(Collectors.toList());
+			if (CollectionUtils.isEmpty(candidates)) {
+				return null;
+			}
+			if (candidates.size() > 1) {
+				throw new IllegalStateException(candidates.size() + " implementations of " +
+						"CachingConfigurer were found when only 1 was expected. " +
+						"Refactor the configuration such that CachingConfigurer is " +
+						"implemented only once or not at all.");
+			}
+			return candidates.get(0);
+		};
+		useCachingConfigurer(new CachingConfigurerSupplier(configurer));
+	}
+
+

실제로 setConfigurers()@Autowired 를 이용해 setter Injection을 통해 의존성 주입을 진행해주게 됩니다.여기서 CachingConfigurer를 추가로 빈으로 등록하지 않는다면, @Cacheable 이 실제로 동작하는 CacheAspectSupport 에서 초기화 할 때 기본값인 SimpleCacheErrorHandler가 등록되게 됩니다. 관련 코드는 다음과 같습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+
// package org.springframework.cache.interceptor;
+public abstract class CacheAspectSupport extends AbstractCacheInvoker
+		implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
+		
+		...
+		
+		public void configure(
+					@Nullable Supplier<CacheErrorHandler> errorHandler, @Nullable Supplier<KeyGenerator> keyGenerator,
+					@Nullable Supplier<CacheResolver> cacheResolver, @Nullable Supplier<CacheManager> cacheManager) {
+		
+				this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new);
+				this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new);
+				this.cacheResolver = new SingletonSupplier<>(cacheResolver,
+						() -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager)));
+			}
+}
+

CacheAspectSupportAspectJCachingConfiguration에서 빈으로 등록합니다. AspectJCachingConfiguration는 앞서 설정 값(CachingConfigurer)들을 setter injection으로 의존성 주입을 받은 AbstractCachingConfiguration를 상속받으며, 이 주입받은 값들을 이용해 빈으로 등록하게 됩니다. 해당 코드는 다음과 같으며 등록하는 빈인 AnnotationCacheAspectCacheAspectSupport를 상속받습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+
// package org.springframework.cache.aspectj;
+// 
+@Configuration(proxyBeanMethods = false)
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+public class AspectJCachingConfiguration extends AbstractCachingConfiguration {
+
+	@Bean(name = CacheManagementConfigUtils.CACHE_ASPECT_BEAN_NAME)
+	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+	public AnnotationCacheAspect cacheAspect() {
+		AnnotationCacheAspect cacheAspect = AnnotationCacheAspect.aspectOf();
+		cacheAspect.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
+		return cacheAspect;
+	}
+
+}
+
+// package org.springframework.cache.aspectj;
+@Aspect
+public class AnnotationCacheAspect extends AbstractCacheAspect {
+	 ...
+}
+

구현 코드

내부 원리를 확인해봤으니 실제 코드를 작성해보겠습니다. 문제 상황은 캐쉬 조회(CacheGet) 시 SerializationException이 발생하는 경우이기에, 이를 상속받아 원하는 대로 동작하도록 변경하면 다음과 같습니다. SerializationException 가 발생했을 때 로그만 남기고 예외를 무시하도록 처리했습니다

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+
@Slf4j
+public class CustomCacheErrorHandler extends SimpleCacheErrorHandler {
+
+    @Override
+    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
+        if (exception instanceof SerializationException) {
+            log.warn("Failed to deserialize cache value for key: {}", key, exception);
+            return;
+        }
+
+        super.handleCacheGetError(exception, cache, key);
+    }
+
+}
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+
@Slf4j
+public class CustomCacheErrorHandler extends SimpleCacheErrorHandler {
+
+    @Override
+    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
+        if (exception instanceof SerializationException) {
+            log.warn("Failed to deserialize cache value for key: {}", key, exception);
+            return;
+        }
+
+        super.handleCacheGetError(exception, cache, key);
+    }
+
+}
+

캐시 설정은 다음과 같습니다. 전체 캐시(Spring Boot Cache)에 대한 책임을 CacheConfig에 두었고 레디스 캐시 설정에 대한 책임을 RedisCacheConfig에 뒀습니다,

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+
@Configuration(proxyBeanMethods = false)
+@EnableCaching
+@RequiredArgsConstructor
+public class CacheConfig implements CachingConfigurer {
+
+    private final CacheManager redisCacheManager;
+
+    @Override
+    @Bean
+    @Primary
+    public CacheManager cacheManager() {
+        return redisCacheManager;
+    }
+
+    @Override
+    @Bean
+    public CacheErrorHandler errorHandler() {
+        return new CustomCacheErrorHandler();
+    }
+
+}
+
+@Configuration(proxyBeanMethods = false)
+public class RedisCacheConfig{
+
+    @Bean
+    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
+        return RedisCacheManager.builder(redisConnectionFactory)
+            .cacheDefaults(cacheConfiguration())
+            .withCacheConfiguration(PRODUCT_CACHE,
+                RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)))
+            .build();
+    }
+
+    public RedisCacheConfiguration cacheConfiguration() {
+        return RedisCacheConfiguration.defaultCacheConfig()
+            .entryTtl(Duration.ofMinutes(60))
+            .disableCachingNullValues()
+            .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
+            .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
+    }
+
+    public static class CacheName {
+        public static final String PRODUCT_CACHE = "productCache_V2";
+    }
+}
+

캐시를 이용해 비즈니스 로직을 처리하는 서비스 코드는 다음과 같습니다. @Cacheable을 이용해 해당 key에 대한 값이 캐시에 존재하면 캐시에서 조회하고 그렇지 않으면 캐시를 등록하는 코드입니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+
@Service
+@Slf4j
+@RequiredArgsConstructor
+public class ProductService {
+
+    private final ProductRepository productRepository;
+
+    @PostConstruct
+    void initProducts() {
+        productRepository.saveAll(List.of(
+                        new Product("box", new BigDecimal(1000)),
+                        new Product("snack", new BigDecimal(4000)),
+                        new Product("chicken", new BigDecimal(20000))
+                )
+        );
+    }
+
+    @Cacheable(cacheNames = PRODUCT_CACHE, key = "'top10'")
+    public List<ProductResponse> getTenProduct() {
+        log.warn("NO CACHE - find top 10 products from DB");
+        return ProductResponse.listOf(productRepository.findTop10By());
+    }
+
+    @CacheEvict(cacheNames = PRODUCT_CACHE, key = "'top10'")
+    public void evict() {
+        log.warn("Cache Evicted");
+    }
+
+}
+
+@RestController
+@RequestMapping("/api/v1")
+@RequiredArgsConstructor
+public class ProductController {
+
+    private final ProductService productService;
+
+    @GetMapping("/products/top10")
+    public ResponseEntity<?> getTop10Products() {
+        return ResponseEntity.ok(productService.getTenProduct());
+    }
+}
+

위의 CustomCacheErrorHandlerCachingConfigurer 를 설정해주지 않은 상태에서api.ProductResponse 패키지 경로로 해당 객체의 캐시를 만들고 api.v2.ProductResponse 패키지로 옮긴 다음에 동일한 키로 조회를 하면 SerializationException이 발생하면서 ExceptionHandler를 통한 에러 응답이 오게됩니다.

위의 코드를 적용하고 동일한 상황을 재현해보면 warn 로그가 찍히고 응답이 정상적으로 가는 것을 볼 수 있습니다. 이로 인해 새로운 버전의 클래스로 캐시를 덮어쓰게 되면서 이후 요청에 대해서는 정상적으로 캐시를 조회하게 됩니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+
2024-03-31 20:13:28.248  WARN 43092 --- [nio-8080-exec-1] c.e.r.config.CustomCacheErrorHandler     : Failed to deserialize cache value for key: top10
+
+org.springframework.data.redis.serializer.SerializationException: Cannot deserialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to deserialize payload. Is the byte array a result of corresponding serialization for DefaultDeserializer?; nested exception is org.springframework.core.NestedIOException: Failed to deserialize object type; nested exception is java.lang.ClassNotFoundException: com.example.redisinactions.api.v2.ProductResponse
+	at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:84) ~[spring-data-redis-2.7.2.jar:2.7.2]
+	at org.springframework.data.redis.serializer.DefaultRedisElementReader.read(DefaultRedisElementReader.java:49) ~[spring-data-redis-2.7.2.jar:2.7.2]
+	at org.springframework.data.redis.serializer.RedisSerializationContext$SerializationPair.read(RedisSerializationContext.java:272) ~[spring-data-redis-2.7.2.jar:2.7.2]
+	at org.springframework.data.redis.cache.RedisCache.deserializeCacheValue(RedisCache.java:298) ~[spring-data-redis-2.7.2.jar:2.7.2]
+	at org.springframework.data.redis.cache.RedisCache.lookup(RedisCache.java:95) ~[spring-data-redis-2.7.2.jar:2.7.2]
+	at org.springframework.cache.support.AbstractValueAdaptingCache.get(AbstractValueAdaptingCache.java:58) ~[spring-context-5.3.22.jar:5.3.22]
+	at org.springframework.cache.interceptor.AbstractCacheInvoker.doGet(AbstractCacheInvoker.java:73) ~[spring-context-5.3.22.jar:5.3.22]
+	...
+2024-03-31 20:13:28.250  WARN 43092 --- [nio-8080-exec-1] c.e.redisinactions.api.ProductService    : NO CACHE - find top 10 products from DB
+2024-03-31 20:13:28.298 DEBUG 43092 --- [nio-8080-exec-1] org.hibernate.SQL                        : select product0_.id as id1_0_, product0_.description as descript2_0_, product0_.price as price3_0_, product0_.quantity as quantity4_0_ from product product0_ limit ?
+Hibernate: select product0_.id as id1_0_, product0_.description as descript2_0_, product0_.price as price3_0_, product0_.quantity as quantity4_0_ from product product0_ limit ?
+2024-03-31 20:13:28.311 DEBUG 43092 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
+2024-03-31 20:13:28.311 DEBUG 43092 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [[com.example.redisinactions.api.ProductResponse@3c54979a, com.example.redisinactions.api.ProductResp (truncated)...]
+2024-03-31 20:13:28.317 DEBUG 43092 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 200 OK
+2024-03-31 20:14:47.332 DEBUG 43092 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : GET "/api/v1/products/top10", parameters={}
+2024-03-31 20:14:47.332 DEBUG 43092 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.redisinactions.api.ProductController#getTop10Products()
+2024-03-31 20:14:47.336 DEBUG 43092 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json, application/json, application/*+json]
+2024-03-31 20:14:47.336 DEBUG 43092 --- [nio-8080-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [[com.example.redisinactions.api.ProductResponse@39bba9c2, com.example.redisinactions.api.ProductResp (truncated)...]
+2024-03-31 20:14:47.337 DEBUG 43092 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet        : Completed 200 OK
+
+

하지만 소개한 해결 방법은 배포 전략에 따라 문제가 될 수 있습니다. 단번에 배포를 변경하는 Blue-Green 전략의 경우 롤백하지 않는다면 새로 배포된 인스턴스는 새로운 캐시만 바라보게 되므로 문제가 되지 않습니다.

하지만 만약 캐시에 해당하는 클래스의 패키지를 변경하고 카나리 배포를 진행하고 구 버전과 신 버전의 인스턴스가 동시에 떠있는 상태라면 CacheName은 동일하기에 새로운 버전의 인스턴스(api.v2.ProductResponse)와 구 버전의 인스턴스(api.ProductResponse)에 대한 캐시 쓰기가 반복될 수 있습니다. 이 경우 트래픽이 많은 서비스라면 캐시 저장소에 대한 부하가 커질 수 있다는 단점이 존재합니다. 따라서 카나리의 경우는 이전에 제시한 해결책인 새로운 CacheName을 가진 캐시를 새로 만드는게 더 안전한 방법일 수 있습니다. 따라서 본 글의 목적인 상황에 맞게 캐시의 에러 핸들링 전략을 취하면 적절할 것 같습니다.

코드는 다음 링크에서 확인해볼 수 있습니다.

This post is licensed under CC BY 4.0 by the author.

분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법

-

diff --git a/posts/spring-cloud-refresh/index.html b/posts/spring-cloud-refresh/index.html new file mode 100644 index 0000000..cb865a6 --- /dev/null +++ b/posts/spring-cloud-refresh/index.html @@ -0,0 +1,377 @@ + 분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법 | Ruggy Blog
Home 분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법
Post
Cancel

분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법

근래에 요즘 우아한 개발이라는 책을 읽으면서 내용 중에 배포 없이 Spring Cloud Config에서 받아오는 프로퍼티를 변경해 서버에 적용하려는 내용을 접했습니다. 해당 팀은 외부 메시지 플랫폼을 이중화하면서 각 외부 플랫폼 연동에 대한 트래픽 분배를 어플리케이션 실행 중에도 변경할 수 있도록 구현해 단일 장애 지점(SPOF)을 제거하려 했습니다.
이를 위해 어플리케이션의 배포 없이 Config 서버의 프로퍼티를 변경함으로 트래픽 분배를 변경하는 방법을 고려했고, 팀에서 최종적으로 일반적으로 사용하는 Spring Cloud Bus를 이용하기보다 Spring Boot만을 활용해 프로퍼티를 재배포 없이 수정하는 방법을 선택했습니다. 이 부분이 흥미롭게 느껴져 호기심에 구현해보게 되었습니다.

배포 없이 프로퍼티를 변경하는 방법, refresh

Spring Boot를 이용하면 로깅 레벨, 데이터 소스 정보, 타임아웃 설정, 환경 변수 등 여러 설정 정보(이하 프로퍼티)를 application.yml 혹은 application.properties 파일에 명시해서 외부화(externalize)할 수 있습니다. 이를 통해 다양한 환경(local, dev, prod 등) 설정을 코드가 아닌 외부 파일에 명시함으로 해당 관심사를 코드에서 분리할 수 있습니다.

책에 나온 사례와 같이 외부 시스템 장애와 같은 기민한 대응을 하려면 어플리케이션 배포 없이도 런타임 환경에서 변경할 수 있어야 합니다. 만약 해당 기능이 비즈니스에서 중요한 역할을 하는 부분이라면 더더욱 조심스럽고 기민하게 다뤄야 합니다.

Spring Cloud Config 프로퍼티를 직접 클라우드 서비스에 올려 사용할 수 있도록 해줍니다. 이를 이용하면 Cloud Config 서버를 호출해서 스프링 프로퍼티값을 받아올 수 있습니다. 또 Cloud Config를 이용하면 @RefreshScope 빈을 손쉽게 reload할 수 있게 됩니다. 간단한 예로 Spring Cloud Config 서버의 프로퍼티를 변경한 이후에 actuator에서 /refresh endpoint를 활성화 한 상태에서 호출하게 되면 변경된 프로퍼티 값을 받아올 수 있습니다. 참고 링크

일반적으로 트래픽이 많은 서비스의 서버는 멀티 인스턴스 환경에서 운영됩니다. 여기서 모든 인스턴스의 Config 프로퍼티는 동일해야 하며, 이를 api 호출로는 하나의 인스턴스 밖에 Config 프로퍼티 변경이 안되고 이는 서버마다 설정값이 달라지게 됩니다.

이러한 문제를 해결하기 위해 Spring Cloud Bus를 통해 멀티 인스턴스 환경에서도 프로퍼티를 Refresh할 수 있습니다. 간략하게 소개하면 Config Server의 프로퍼티에 변경이 감지되면 Kafka, Redis, AMQP 중 하나를 이용해 변경된 프로퍼티 사용하는 모든 서버에 전달하는 역할을 합니다.

다만 Spring Cloud Bus 라이브러리는 Kafka, Redis, AMQP를 사용해 외부 인프라 의존성을 가지게 되고 해당 인프라 상태가 정상적이지 않을 때 사용하기 어렵습니다.

이를 해결하기 위해서 책에서는 Spring Cloud Config + Scheduled Polling을 이용한 아키텍처를 제안합니다. 외부 인프라를 관리하는 비용이 없다는 장점이 있습니다. 이를 구현해보도록 해보면 다음과 같습니다.

이제 코드를 통해 살펴보도록 하겠습니다.

@Schedule 와 ConteextRefresher를 이용해 구현

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+
@Component
+@RequiredArgsConstructor
+@ConditionalOnProperty(value = "application.config.refresh.auto.enabled", havingValue = "true", matchIfMissing = true)
+@Slf4j
+public class ConfigRefreshScheduler {
+
+  private final ContextRefresher contextRefresher;
+  private final TestValueProperties testValueProperties;
+
+  @Scheduled(fixedDelay = 5L, timeUnit = TimeUnit.SECONDS)
+  @Async("refreshThreadPoolExecutor")
+  public void refreshConfig() {
+    try {
+      Set<String> refreshedKeys = contextRefresher.refreshEnvironment();
+      if (!CollectionUtils.isEmpty(refreshedKeys)) {
+        log.info("[Refreshed] " + String.join(",", refreshedKeys));
+        contextRefresher.refresh();
+        log.info("Changed value: {}", testValueProperties.getValue());
+      }
+    } catch (Exception e) {
+      log.error("config refresh failed {}", e);
+    }
+  }
+
+  @ConditionalOnBean(value = ConfigRefreshScheduler.class)
+  @Bean
+  public ThreadPoolTaskExecutor refreshThreadPoolExecutor() {
+    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
+    threadPoolTaskExecutor.setThreadNamePrefix("refresh");
+    threadPoolTaskExecutor.setCorePoolSize(1);
+    threadPoolTaskExecutor.setMaxPoolSize(1);
+    threadPoolTaskExecutor.setQueueCapacity(Integer.MAX_VALUE);
+    threadPoolTaskExecutor.initialize();
+    return threadPoolTaskExecutor;
+  }
+
+  @ConfigurationProperties(prefix = "my")
+  @Configuration
+  @Getter
+  @Setter
+  @RefreshScope
+  public static class TestValueProperties {
+
+    // get my.value from config properties
+    private String value;
+
+    @PostConstruct
+    void init() {
+      log.info("-------------------------------- my properties value --------------------------------");
+      log.info("{} Bean Loaded", TestValueProperties.class.getName());
+      log.info(value);
+      log.info("-------------------------------- my properties value --------------------------------");
+    }
+
+  }
+
+}
+

해당 코드는 5초마다 Config Server의 프로퍼티와 서버의 프러퍼티를 비교해 변경된 프로퍼티가 있으면 @RefreshScope 빈(TestValueProperties)을 초기화하는 코드입니다. TestProperties는 Config Server에서 my.value라는 프로퍼티 값을 바인딩하는 빈입니다.

refresh에서 중요한 역할을 하는 ContextRefresher를 간략하게 살펴보면 refreshEnvironment()는 빈을 refresh하지 않으며, config 설정만 refresh하고 이로 인해 변경된 config의 key값을 리턴합니다. refresh()의 경우 refreshEnvironment() 을 먼저 호출해서 config를 spring conetext에 저장하며 그 이후 @RefreshScope 빈을 모두 refresh하고 변경된 config의 key값을 리턴합니다.

실제로 서버를 구동하면 다음 값을 가지고 있습니다.

이후 config Server 값을 변경하면 다음과 같습니다.

만약 Config Server의 yml 형식이 잘못되었거나 config 서버에 이상이 생긴다면 다음과 같은 에러 로그가 발생합니다. 이는 프로퍼티를 원활히 받아오지 못했기에 기존에 정상적으로 받아온 프로퍼티로 서비스가 운영되게 됩니다.

이제 ContextRefresher 내부 코드를 통해 더 구체적인 동작 원리를 알아보겠습니다.

Context Refresher 원리

ContextRefresher은 Config 서버에서 받아와 API 서버 프로퍼티를 Refresh하거나 RefreshScope 빈을 Refresh하는 역할을 합니다. ContextRefresher는 Spring Cloud Client 라이브러리 의존성을 추가하면 RefreshAutoConfiguration으로 인해 자동으로 빈으로 등록됩니다. 다음 코드를 보면 알 수 있듯 spring.cloud.bootstrap.enabled: trueLegacyContextRefresher가 빈으로 등록되고 그렇지 않으면 ConfigDataContextRefresher가 빈으로 등록됩니다. Spring Cloud에서 bootstrap 기본 옵션은 false이기에 ConfigDataContextRefresher 가 빈으로 등록됩니다. 두 차이를 간략하게 소개하면 내부 코드에서 ConfigDataContextRefresher는 빈 후처리기인 EnvironmentPostProcessor를 이용해 ApplicationContext를 Refresh하기 때문에 ApplicationContext를 전체를 refresh하는 LegacyContextRefresher보다 더 효율적이라고 볼 수 있습니다. 더 자세한 내용은 본 글의 주제와 벗어나 더 다루지는 않겠습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+
//package org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
+
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnClass(RefreshScope.class)
+@ConditionalOnProperty(name = RefreshAutoConfiguration.REFRESH_SCOPE_ENABLED, matchIfMissing = true)
+@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
+@EnableConfigurationProperties(RefreshAutoConfiguration.RefreshProperties.class)
+public class RefreshAutoConfiguration {
+
+	/**
+	 * Name of the refresh scope name.
+	 */
+	public static final String REFRESH_SCOPE_NAME = "refresh";
+
+	/**
+	 * Name of the prefix for refresh scope.
+	 */
+	public static final String REFRESH_SCOPE_PREFIX = "spring.cloud.refresh";
+
+	/**
+	 * Name of the enabled prefix for refresh scope.
+	 */
+	public static final String REFRESH_SCOPE_ENABLED = REFRESH_SCOPE_PREFIX + ".enabled";
+
+	@Bean
+	@ConditionalOnMissingBean(RefreshScope.class)
+	public static RefreshScope refreshScope() {
+		return new RefreshScope();
+	}
+
+	...
+
+	@Bean
+	@ConditionalOnMissingBean
+	@ConditionalOnBootstrapEnabled
+	public LegacyContextRefresher legacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope,
+			RefreshProperties properties) {
+		return new LegacyContextRefresher(context, scope, properties);
+	}
+
+	@Bean
+	@ConditionalOnMissingBean
+	@ConditionalOnBootstrapDisabled
+	public ConfigDataContextRefresher configDataContextRefresher(ConfigurableApplicationContext context,
+			RefreshScope scope, RefreshProperties properties) {
+		return new ConfigDataContextRefresher(context, scope, properties);
+	}
+
+	@Bean
+	public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
+		return new RefreshEventListener(contextRefresher);
+	}
+  ...
+
+}
+

ConfigDataContextRefresherContextRefresher의 구현체입니다. ContextRefresher는 spring web actuator에서 /refresh endpoint를 enable할 떄 ContextRefresher를 사용해 @RefreshScope 빈을 refresh합니다. 실제로 spirng web actuator의 RefreshEndpoint 클래스는 다음과 같으며, contextRefresher.refresh()를 통해 @RefreshScope 빈을 refresh해주고 있습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+
// package org.springframework.cloud.context.refresh.ConfigDataContextRefresher;
+public class ConfigDataContextRefresher extends ContextRefresher
+		implements ApplicationListener<ApplicationPreparedEvent> {
+
+	private SpringApplication application;
+
+	@Deprecated
+	public ConfigDataContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
+		super(context, scope);
+	}
+
+	public ConfigDataContextRefresher(ConfigurableApplicationContext context, RefreshScope scope,
+			RefreshAutoConfiguration.RefreshProperties properties) {
+		super(context, scope, properties);
+	}
+
+	@Override
+	public void onApplicationEvent(ApplicationPreparedEvent event) {
+		application = event.getSpringApplication();
+	}
+
+	@Override
+	protected void updateEnvironment() {
+	  ...
+	}
+}
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+
// package org.springframework.cloud.endpoint.RefreshEndpoint
+
+@Endpoint(id = "refresh")
+public class RefreshEndpoint{
+
+	private ContextRefresher contextRefresher;
+
+	public RefreshEndpoint(ContextRefresher contextRefresher) {
+		this.contextRefresher = contextRefresher;
+	}
+
+	@WriteOperation
+	public Collection<String> refresh() {
+		Set<String> keys = this.contextRefresher.refresh();
+		return keys;
+	}
+
+}
+

ContextRefresher를 조금 더 들어가보겠습니다. 먼저 refreshEnvironment()는 빈을 refresh하지 않으며, config 설정만 refresh하고 이로 인해 변경된 config의 key값을 리턴합니다. refresh()의 경우 refreshEnvironment() 을 먼저 호출해서 config를 spring conetext에 저장하며 그 이후 @RefreshScope 빈을 모두 refresh하고 변경된 config의 key값을 리턴합니다.

이를 통해 bean overriding option에 따라 ConextRefresher의 구현체가 달라지는 것은 refresh할 때 빈을 정의하는 방식이 달라지기 때문임을 알 수 있습니다. 이는 updateEnvironment()를 구현체를 통해 구현한다는 것으로 이해할 수 있습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+
// package org.springframework.cloud.context.refresh.ContextRefresher;
+public abstract class ContextRefresher {
+  
+	private ConfigurableApplicationContext context;
+	private RefreshScope scope;
+
+	@SuppressWarnings("unchecked")
+	protected ContextRefresher(ConfigurableApplicationContext context, RefreshScope scope,
+			RefreshAutoConfiguration.RefreshProperties properties) {
+		this.context = context;
+		this.scope = scope;
+		additionalPropertySourcesToRetain = properties.getAdditionalPropertySourcesToRetain();
+	}
+
+	public synchronized Set<String> refresh() {
+		Set<String> keys = refreshEnvironment();
+		this.scope.refreshAll();
+		return keys;
+	}
+
+	public synchronized Set<String> refreshEnvironment() {
+		Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
+		updateEnvironment();
+		Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
+		this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
+		return keys;
+	}
+
+	protected abstract void updateEnvironment();
+
+	...
+}
+

이를 통해 서비스 점검 시간 등 잘 변하지 않지만 종종 변경해줘야 값들 등 property에 넣어서 사용하는 값들을 배포 없이 config 서버의 값 변경만으로 준 실시간으로 적용할 수 있게됩니다. 또, 피쳐 플래그, 스프링 스케줄러, 외부 API 등 on/off 관련 기능에도 적용해볼 수 있습니다.

주의사항

시스템이 커지다 보면 코드량이 많아지면서 빌드 시간이 늘어나게 되고 스프링의 경우 어플리케이션 빈이 많아지다보면 실행 시간이 늘어나게됩니다. 이에 따라 설정값 변경만으로 인해 다시 배포하는데 시간이 오래 소요됩니다. 그래서 배포 없이 런타임 환경에서 설정 정보를 변경하는건 굉장히 유용하다고 생각합니다. 하지만 은탄환은 존재하지 않습니다.

먼저 @RefreshScope를 남용하면 안됩니다. datasource, redis, kafka와 같은 설정 정보들은 빈이 복잡하게 얽혀있고 서비스가 운영되는 중에 refresh를 하면 서버에 부담이 크게 작용할 수 있습니다. 실제로 사내에서 DataourceConfiguration 관련 빈에 RefreshScope을 넣어 서버가 죽은 경험도 있습니다. 차라리 해당 상황의 경우 배포를 다시해서 설정 정보를 초기화 하는게 더 안정적으로 서비스를 제공하는 방법이라고 생각합니다.

또, ConetxtRefresher를 활용할 때 ENC(value) 값과 같이 Jasypt를 이용해 암호화된 설정 값을 이용하게 되면 refreshEnvironment()에서 복호화된 값과 복호화되기 전의 값을 가져오게 되어 명확하게 변경된 설정값을 가져오지 못하는 현상도 존재합니다. 해당 이슈에 대해서는 다음 글에서 알아보도록 하겠습니다.

실제로 config 폴링에 대해 spring-cloud-commons docs에서는 폴링을 권장하지 않는 방법이라고 언급합니다.

Note that the Spring Cloud Config Client does not, by default, poll for changes in the Environment. Generally, we would not recommend that approach for detecting changes (although you can set it up with a @Scheduled annotation).

폴링의 경우 Config Server에 대한 트래픽이 더 커지게 되고 조직의 규모가 커지고 해당 Config Server를 사용하는 API 서버들이 많아질수록 더 부하가 많이갈 수 있습니다. 이는 예상치 못한 일이 발생할 수 있기에 해당 방법을 차용하게되면 Config Server를 더 세심하게 모니터링해야 합니다. 현재 조직 상황에 맞는 방법을 적절히 사용하는게 가장 중요하다고 생각합니다. 실제로 Toss Slash 23 영상(20:35~)에 따르면 운영 환경에서는 사용하지 않고 휴면 에러가 발생할 수 있다는 의견도 있기에 상황에 맞게 적절히 사용해야 합니다.

해당 소스 코드는 다음 링크에서 확인해볼 수 있습니다.

참고 링크

This post is licensed under CC BY 4.0 by the author.

Spring MVC에서 redisson으로 분산락을 구현하는 방법들

Spring Boot Caching에서 에러 핸들링하는 방법

diff --git a/posts/spring-redis-cache-serialization-exception/index.html b/posts/spring-redis-cache-serialization-exception/index.html new file mode 100644 index 0000000..0708f0a --- /dev/null +++ b/posts/spring-redis-cache-serialization-exception/index.html @@ -0,0 +1,211 @@ + Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점 | Ruggy Blog
Home Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점
Post
Cancel

Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점

사내에서 패키지 구조 변경 작업을 하고 배포를 했는데 갑자기 특정 API에서 transaction silently rolled back이 발생했었습니다. 관련해서 확인해보니 DB조회 값을 Dto 객체로 변환해 캐싱한 값을 역직렬화하는 과정에서 문제가 발생했었습니다. 해당 캐시는 월마다 한번씩 바뀌는 주기를 갖는 값으로, 조회가 많은 비율을 차지합니다. 캐시로 사용하는 정보가 DB에서 열거형으로 관리되고 있어 이를 자바 Dto 객체로 직렬화해서 redis에 저장해 캐시로 활용하고 있었습니다.

코드를 확인해보면 다음과 같습니다.

문제 상황

설정 값들

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+
// package com.example.redisinactions.api;
+@Getter  
+@AllArgsConstructor(access = AccessLevel.PRIVATE)  
+@NoArgsConstructor(access = AccessLevel.PROTECTED)  
+public class ProductResponse implements Serializable {  
+	private String description;  
+	private BigDecimal price;  
+  
+	private static ProductResponse of(Product product) {  
+		return new ProductResponse(product.getDescription(), product.getPrice());  
+	}  
+  
+	public static List<ProductResponse> listOf(List<Product> productList) {  
+		return productList.stream()  
+		.map(ProductResponse::of)  
+		.toList();  
+	}  
+}
+
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+    @Bean
+    public RedisCacheConfiguration cacheConfiguration() {
+        return RedisCacheConfiguration.defaultCacheConfig()
+                .entryTtl(Duration.ofMinutes(60))
+                .disableCachingNullValues()
+                .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
+                .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
+    }
+
+    @Bean
+    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
+        return (builder) -> builder
+                .withCacheConfiguration(PRODUCT_CACHE,
+                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)));
+    }
+
+    public static class CacheName {
+        public static final String PRODUCT_CACHE = "productCache";
+    }
+}
+
+

레디스 캐시를 사용하는 서비스

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+
@Service
+@Slf4j
+@RequiredArgsConstructor
+public class ProductService {
+
+    private final ProductRepository productRepository;
+
+    @PostConstruct
+    void initProducts() {
+        productRepository.saveAll(List.of(
+                        new Product("box", new BigDecimal(1000)),
+                        new Product("snack", new BigDecimal(4000)),
+                        new Product("chicken", new BigDecimal(20000))
+                )
+        );
+    }
+
+    @Cacheable(cacheNames = PRODUCT_CACHE, key = "'top10'")
+    public List<ProductResponse> getTenProduct() {
+        log.warn("NO CACHE - find top 10 products from DB");
+        return ProductResponse.listOf(productRepository.findTop10By());
+    }
+
+    @CacheEvict(cacheNames = PRODUCT_CACHE, key = "'top10'")
+    public void evict() {
+        log.warn("Cache Evicted");
+    }
+
+}
+
+@RestController
+@RequestMapping("/api/v1")
+@RequiredArgsConstructor
+public class ProductController {
+
+    private final ProductService productService;
+
+    @GetMapping("/products/top10")
+    public ResponseEntity<?> getTop10Products() {
+        return ResponseEntity.ok(productService.getTenProduct());
+    }
+}
+

해당 코드에서 getTenProduct()를 먼저 호출하면 다음과 같은 응답이 오며 redis에 잘 쌓이게 됩니다.

해당 상황을 도식화 하면 다음과 같습니다.

이후 ProductResponse를 v2 패키지로 변경한 이후 어플리케이션을 재실행해서 동일한 API를 호출하면 SerializationException이 발생합니다.

1
+2
+3
+4
+5
+6
+7
+
// package com.example.redisinactions.api.v2;
+@Getter  
+@AllArgsConstructor(access = AccessLevel.PRIVATE)  
+@NoArgsConstructor(access = AccessLevel.PROTECTED)  
+public class ProductResponse implements Serializable {
+   ...
+}
+

Exception의 cause를 확인해보면 ClassNotFountException이 발생합니다. com.example.redisinactions.api.ProductResponse 클래스를 역직렬화해야 하는데 해당 클래스가 com.example.redisinactions.api.v2.ProductResponse로 변경되어 발생한 현상입니다.

1
+2
+3
+
Caused by: java.lang.ClassNotFoundException: com.example.redisinactions.api.ProductResponse
+	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[na:na]
+	...
+

해결 방법: Cache Key Prefix 변경

해당 문제를 해결하기 위해서 @Cacheable에서 키값 deserialization에서 오류가 나는 것이므로 키값을 바꿔줘서 해결할 수 있습니다. 기존에 저장된 캐시를 재사용하는 부분에서 문제가 발생하는 것이기에 새로운 캐시를 다시 저장하고 이를 활용하면 됩니다. 기존 키값에 해당하는 값은 역직렬화할 수 없으므로 자연스럽게 TTL로 인해 사라지게 됩니다. 이를 통해 서비스에 지장 없이 안정적으로 캐시를 변경해서 사용할 수 있습니다. 코드로 나타나면 다음과 같습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+
@Configuration
+@EnableCaching
+public class CacheConfig {
+   ...
+
+    public static class CacheName {
+        public static final String PRODUCT_CACHE = "V2_productCache"; // as-is: productCache
+    }
+}
+

사실 해당 값을 캐싱하는 부분에서 꼭 Redis를 이용해야 하는 부분에 대해서도 고민해볼 필요가 있습니다. Redis가 아니더라도 LocalCache를 이용한다면 빈을 주입할 때 값을 DB에서 조회해서 캐싱해서 사용하는 방법도 좋은 방법이라고 생각합니다.

본래 문제는 이로 인한 트랜잭션의 실패였습니다. 더 생각해볼 점은 @CacheableCahce aside pattern을 사용하는데 해당 전략은 캐시 조회가 실패한다면 원본 데이터에서 가져오는 전략입니다. 따라서 해당 작업이 트랜잭션에서 캐시 조회에서 오류가 발생한다고 롤백 마크로 인해 전체 트랜잭션이 실패하면 안된다고 생각합니다. 이는 트랜잭션을 사용할 때 두고두고 고민해야 하는 부분이라고 생각합니다.

관련 소스 코드는 다음 링크에서 확인할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.

MySQL 커넥션, I/O 연산, 잠금에 대한 고찰

Spring MVC에서 redisson으로 분산락을 구현하는 방법들

diff --git a/posts/spring-redis-distributed-lock/index.html b/posts/spring-redis-distributed-lock/index.html new file mode 100644 index 0000000..be89710 --- /dev/null +++ b/posts/spring-redis-distributed-lock/index.html @@ -0,0 +1,559 @@ + Spring MVC에서 redisson으로 분산락을 구현하는 방법들 | Ruggy Blog
Home Spring MVC에서 redisson으로 분산락을 구현하는 방법들
Post
Cancel

Spring MVC에서 redisson으로 분산락을 구현하는 방법들

멀티 인스턴스 환경에서 동시성을 해결하는 방법으로 Redis의 이벤트 루프 기반 싱글스레드 특성을 이용해 분산락을 사용해 쉽게 해결할 수 있습니다. 동시 호출은 DB 데이터의 정합이 깨지거나 메시지 이벤트의 중복 발행 등 예상치 못한 동작으로 이어지는 경우가 많아 해결해야 하는 경우가 빈번합니다.

분산락은 동시성을 제어하기 위한 부가 기능으로 비즈니스 로직과 섞이지 않도록 관심사를 잘 분리하는게 좋습니다. 관련해서 스프링은 Dependency Injection, AOP와 같은 여러 기능을 쉽게 사용할 수 있어 여러 구현 방법을 소개해보려 합니다. 구현은 Java, Spring MVC 기반으로 Redisson을 이용해 구현할 예정입니다.

요구사항

  • 분산락이 실패하는 경우 null을 리턴하게 됩니다.
  • 레디스 장애가 발생해도 원래 메소드는 동작해야 합니다.
  • 락 내부 트랜잭션을 사용할 수 있어야 하며 락이 끝나기 전에 트랜잭션이 종료되어야 합니다. 트랜잭션이 종료되어야 하는 이유는 트랜잭션이 종료함으로 DB에 저장된 시점에 락을 해제해야 동시 호출에 대한 온전히 제어할 수 있기 때문입니다. 더 자세한 내용은 다음 글에서 소개하고 있기에 본 글에서는 생략하겠습니다.
  • 락 설정 정보를 하나의 enum으로 관리할 수 있어야 합니다.

함수형 인터페이스를 이용한 구현 (template callback 패턴)

락을 잡으려고 하는 구간을 함수형 인터페이스(콜백 함수) 인자로 받아서 처리하는 방법입니다. 트랜잭션을 사용할 수 있으며, 트랜잭션의 종료를 보장하기 위해 전파 옵션을 PROPAGATION_REQUIRES_NEW로 설정했습니다. 구현 코드 다음과 같습니다.

Lock 설정(enum) 코드

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+
public enum LockConfig {
+
+  PRODUCT_DECREASE("PRODUCT", 1L, 1L, TimeUnit.SECONDS),
+  LOGIN_LOCK("LOGIN_LOCK", 2L, 2L, TimeUnit.SECONDS);
+
+  public final Long waitTime;
+  public final Long leaseTime;
+  public final TimeUnit timeUnit;
+  private final String lockPrefix;
+
+  LockConfig(String lockPrefix, Long waitTime, Long leaseTime, TimeUnit timeUnit) {
+    this.lockPrefix = lockPrefix;
+    this.waitTime = waitTime;
+    this.leaseTime = leaseTime;
+    this.timeUnit = timeUnit;
+  }
+
+  public String generateKey(String key) {
+    if (!StringUtils.hasText(key)) {
+      throw new IllegalArgumentException("key must not be empty");
+    }
+    return String.format("%s_%s", lockPrefix, key);
+  }
+
+}
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+
@Component  
+@Slf4j  
+public class RedissonDistributedLockTemplate {  
+  
+  private final RedissonClient redissonClient;  
+  private final TransactionTemplate transactionTemplate;  
+  
+  public RedissonDistributedLockTemplate(RedissonClient redissonClient, PlatformTransactionManager platformTransactionManager) {  
+    this.redissonClient = redissonClient;  
+    this.transactionTemplate = new TransactionTemplate(platformTransactionManager);  
+    this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);  
+    this.transactionTemplate.afterPropertiesSet();  
+  }  
+  
+  public void executeWithLock(String key, LockConfig lockConfig, Runnable callback) {  
+    executeWithLock(key, lockConfig, toVoidSupplier(callback));  
+  }  
+  
+  public <T> T executeWithLock(String key, LockConfig lockConfig, Supplier<T> callback) {  
+    RLock lock = redissonClient.getLock(lockConfig.generateKey(key));  
+  
+    try {  
+      boolean isAcquired = lock.tryLock(lockConfig.waitTime, lockConfig.leaseTime, lockConfig.timeUnit);  
+      if (!isAcquired) {  
+        log.warn("[lock 획득 실패] {}, key : {}", lockConfig, key);  
+        return null;      }  
+      return callback.get();  
+    } catch (RedisConnectionException redisUnavailableException) {  
+      log.warn("", redisUnavailableException);  
+      return callback.get();  
+    } catch (InterruptedException e) {  
+      log.error("", e);  
+      Thread.currentThread().interrupt();  
+      return null;    } finally {  
+      if (lock.isLocked() && lock.isHeldByCurrentThread()) {  
+        lock.unlock();  
+      }  
+    }  
+  }  
+  
+  public void executeWithLockAndTransaction(String key, LockConfig lockConfig, Runnable callback) {  
+    executeWithLockAndTransaction(key, lockConfig, toVoidSupplier(callback));  
+  }  
+  
+  public <T> T executeWithLockAndTransaction(String key, LockConfig lockConfig, Supplier<T> callback) {  
+    RLock lock = redissonClient.getLock(lockConfig.generateKey(key));  
+  
+    try {  
+      boolean isAcquired = lock.tryLock(lockConfig.waitTime, lockConfig.leaseTime, lockConfig.timeUnit);  
+      if (!isAcquired) {  
+        log.warn("[lock 획득 실패] {}, key : {}", lockConfig, key);  
+        return null;      }  
+      return transactionTemplate.execute(status -> callback.get());  
+    } catch (RedisConnectionException redisUnavailableException) {  
+      log.warn("", redisUnavailableException);  
+      return transactionTemplate.execute(status -> callback.get());  
+    } catch (InterruptedException e) {  
+      log.error("", e);  
+      Thread.currentThread().interrupt();  
+      return null;    } finally {  
+      if (lock.isLocked() && lock.isHeldByCurrentThread()) {  
+        lock.unlock();  
+      }  
+    }  
+  }  
+  
+  private Supplier<Void> toVoidSupplier(Runnable runnable) {  
+    return () -> {  
+      runnable.run();  
+      return null;    };  
+  }  
+  
+}
+
+
  • waitTime의 경우 락을 획득하는데 기다리는 시간입니다. waitTime이 지나면 락 획득에 실패하며 tryLock의 리턴값은 false가 됩니다.
  • leaseTime은 락을 획득한 이후에 락을 잡고있는 시간을 의미합니다. 락을 획득하고 leaseTime보다 더 오래 작업이 걸리면 작업이 종료된 유무와 관계없이 락을 해제하게 됩니다. 이후 동일키로 다른 락 획득 요청이 있는 경우 성공적으로 획득하게 됩니다. 일반적으로 leaseTime으로 인해 락이 해제되기 전에 작업이 종료되어야 하기 때문에 leaseTime을 작업의 최대 시간으로 설정하는게 좋습니다.
  • redis의 상태에 이상이 생기고 작업을 요청하게되면RedisConnectionException이 발생하게 됩니다. 레디스가 문제가 생겼을 때에도 본 메소드에는 영향이 없어야 하는 상황이 요구사항이기에 해당 상황에서 메소드를 정상적으로 실행합니다. 이 경우 동시 요청이 다시 발생할 수 있게됩니다. 해당 상황에서 동시 요청 처리가 비즈니스에 중요하다면 해당 상황에서 실패처리 하는 것도 방법입니다. 따라서 상황에 맞게 효용가치가 큰 방법을 취사하는게 중요합니다.
  • 트랜잭션의 경우 선언형으로 사용하기 어려워 TransactionTemplate을 이용해 구현했습니다. Mvc에서 일반적으로 트랜잭션을 관리하는데 사용하는 PlatformTransactionManager을 주입받아 사용하였습니다.

사용하는 코드는 다음과 같습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+
+@Service  
+@RequiredArgsConstructor  
+public class V2ProductService {  
+  
+  private final ProductRepository productRepository;  
+  private final RedissonDistributedLockTemplate redissonDistributedLockTemplate;  
+  
+  ... 
+  
+  public Product decreaseWithCallback(Long id, Long quantity) {  
+    Product result = redissonDistributedLockTemplate.executeWithLock(id.toString(), PRODUCT_DECREASE, () -> {  
+      Product product = productRepository.findById(id).orElseThrow();  
+      product.decrease(quantity);  
+      return productRepository.save(product);  
+    });  
+    return result;  
+  }  
+  
+  public Product decreaseWithCallbackTransaction(Long id, Long quantity) {  
+    Product result = redissonDistributedLockTemplate.executeWithLockAndTransaction(id.toString(), PRODUCT_DECREASE, () -> {  
+      Product product = productRepository.findById(id).orElseThrow();  
+      product.decrease(quantity);  
+      return product;  
+    });  
+    return result;  
+  }  
+  
+}
+
+

테스트 코드

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+
@SpringBootTest
+class V2ProductServiceTest {
+
+  @Autowired
+  private V2ProductService v2ProductService;
+
+  @Autowired
+  private ProductRepository productRepository;
+
+  private Product product;
+
+  @BeforeEach
+  void setUp() {
+    product = productRepository.save(new Product("description", new BigDecimal(10000), 100L));
+  }
+  
+  ...
+
+  @Test
+  void decreaseWithCallback() {
+    // given
+    int requestCount = 10;
+
+    List<CompletableFuture<?>> futureList = new ArrayList<>();
+    for (int i = 0; i < requestCount; i++) {
+      CompletableFuture<Object> objectCompletableFuture = CompletableFuture.supplyAsync(() -> {
+        v2ProductService.decreaseWithCallback(product.getId(), 1L);
+        return null;
+      });
+      futureList.add(objectCompletableFuture);
+    }
+
+    // then
+    Product result = productRepository.findById(product.getId()).orElseThrow();
+    assertThat(result.getQuantity()).isEqualTo(90L);
+
+  }
+}
+

장점

  • 특별한 클래스 분리가 필요 없이 Template 주입만을 통해서 메소드 내부에서 직접 락 구간을 지정할 수 있습니다.

단점

  • 기존 코드의 depth가 깊거나 콜백(함수형 인터페이스)을 사용하는 코드가 많다면, 콜백 지옥에 빠질 수 있습니다.
  • 콜백 메소드 내부에서 값들이 여러개라면 해당 값들을 모두 리턴해야 합니다. 그렇게 되면 이를 위한 객체를 만들어야 합니다.
  • 부가 기능이 테스트에 영향을 주게 됩니다. 콜백 메소드 내부만 테스트하고 싶다면 template에 대한 Mocking혹은 Stubbing 필요합니다.

AOP(Aspect Oriented Programming)를 이용한 구현

스프링 부트는 어노테이션 기반 AOP 기능을 손쉽게 사용할 수 있게 제공합니다. AOP를 사용하기 위해서는@EnableAspectJAutoProxy설정을 등록해야 합니다. 코드는 다음과 같습니다.

AOP 구현체

  • JoinPointSpELParser는 직접 정의한 spring Expression Language parser로 #매개변수명을 통해 값을 접근할 수 있습니다. @Cacheable, @PreAuthorize, @Value 에서 활용하는 것과 동일하며, 직접 구현해 사용했습니다. 자세한 구현은 코드에서 확인해볼 수 있으며 본 내용에서는 생략하도록 하겠습니다.
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+
@Component
+@Slf4j
+@Aspect
+@RequiredArgsConstructor
+public class DistributedLockAspect {
+
+  private final RedissonClient redissonClient;
+  private final JoinPointSpELParser joinPointSpELParser;
+
+  @Around("@annotation(distributedLock)")
+  public Object lock(ProceedingJoinPoint pjp, DistributedLock distributedLock) throws Throwable {
+    String pa = joinPointSpELParser.parseSpEL(pjp, distributedLock.key());
+    final String key = distributedLock.lockConfig().generateKey(pa);
+    RLock lock = redissonClient.getLock(key);
+    try {
+      final boolean isAcquired = lock.tryLock(distributedLock.lockConfig().waitTime, distributedLock.lockConfig().leaseTime,
+          distributedLock.lockConfig().timeUnit);
+      if (!isAcquired) {
+        return null;
+      }
+      return pjp.proceed();
+    } catch (RedisConnectionException redisUnavailableException) {
+      log.warn("", redisUnavailableException);
+      return pjp.proceed();
+    } catch (InterruptedException e) {
+      log.error("", e);
+      Thread.currentThread().interrupt();
+      return null;
+    } finally {
+      if (lock.isLocked() && lock.isHeldByCurrentThread()) {
+        lock.unlock();
+      }
+    }
+  }
+
+}
+
+@Target(value = ElementType.METHOD)
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface DistributedLock {
+
+  String key();
+
+  LockConfig lockConfig();
+
+}
+

장점

  • 비즈니스 로직과 부가 기능의 관심사를 완전히 분리할 수 있습니다. 이를 통해 비즈니스 로직 테스트를 작성하기 더 쉬워집니다.
  • Spring Expression Langugae을 이용해 인자값으로 키값 설정을 간편하게 활용할 수 있습니다.

단점

  • 타 AOP 어노테이션(@Transactional, @Async 등)과의 순서(@Order) 지정 및 동작 예측이 어려워집니다.
  • 고질적인 AOP의 단점인 자기 호출(self-invocation) 문제가 존재합니다. 오버로딩을 많이 하는 코드에서는 사용하기 어려울 수 있습니다.

AOP와 함수형 인터페이스를 이용한 구현

사실 AOP와 콜백의 장단은 서로 보완관계라고 생각해 상황에 맞게 취사선택하는 방법도 좋은 방법이라고 생각합니다. 사실 AOP 내부에서 함수형 인터페이스를 이용한 구현체를 사용하면 일관된 구현을 통해 락을 활용할 수 있습니다. 이를 @V2DistributedLock으로 만들어 구현해보겠습니다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+
+@Component  
+@Aspect  
+@RequiredArgsConstructor  
+public class V2DistributedLockAspect {  
+  
+  private final JointPointSpELParser jointPointSpELParser;  
+  private final RedissonDistributedLockTemplate redissonDistributedLockTemplate;  
+  
+  @Around("@annotation(v2DistributedLock)")  
+  public Object lock(ProceedingJoinPoint pjp, V2DistributedLock v2DistributedLock) {  
+    String parsedKey = jointPointSpELParser.parseSpEL(pjp, v2DistributedLock.key());  
+    final String key = v2DistributedLock.lockConfig().generateKey(parsedKey);  
+  
+    if (v2DistributedLock.isTransactionEnabled()) {  
+      return redissonDistributedLockTemplate.executeWithLockAndTransaction(key, v2DistributedLock.lockConfig(), proceed(pjp));  
+    }  
+  
+    return redissonDistributedLockTemplate.executeWithLock(key, v2DistributedLock.lockConfig(), proceed(pjp));  
+  }  
+  
+  private Supplier<Object> proceed(ProceedingJoinPoint pjp) {  
+    return () -> {  
+      try {  
+        return pjp.proceed();  
+      } catch (Throwable e) {  
+        throw new RuntimeException(e);  
+      }  
+    };  
+  }
+  
+}
+
+@Target(value = ElementType.METHOD)  
+@Retention(value = RetentionPolicy.RUNTIME)  
+public @interface V2DistributedLock {  
+  
+  String key();  
+  
+  LockConfig lockConfig();  
+  
+  boolean isTransactionEnabled() default false;  
+  
+}
+

사용 코드

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+
@Service
+@RequiredArgsConstructor
+public class V2ProductService {
+
+  private final ProductRepository productRepository;
+
+  @DistributedLock(key = "#id", lockConfig = PRODUCT_DECREASE)
+  public Product decreaseWithAOP(Long id, Long quantity) {
+    Product product = productRepository.findById(id).orElseThrow();
+    product.decrease(quantity);
+    return productRepository.save(product);
+  }
+
+  @V2DistributedLock(key = "#id", lockConfig = PRODUCT_DECREASE, isTransactionEnabled = true)
+  public Product decreaseWithAOPV2(Long id, Long quantity) {
+    Product product = productRepository.findById(id).orElseThrow();
+    product.decrease(quantity);
+    return product;
+  }
+
+}
+
+

마무리

일반적인 상황에서 오버헤드는 많이 발생하지 않지만 트래픽이 많아지거나 redis 지연 발생, 혹은 장애 상황이 발생할 때의 고민도 필요합니다. 또한, DB 작업은 테이블의 데이터양이 변화함에 따라 실행 속도가 일관되지 않을 수 있기 때문에 수행 시간에 대한 모니터링, 타임아웃 발생에 대한 모니터링을 진행해 락 시간(waitTime, leaseTime)의 유동적인 조절이 필요할 수도 있습니다.

나아가 본 글에서 동시성에 대해 해결 방법을 소개했지만 근본적으로 동시성이 어디서, 왜 발생하는가에 대한 근본적인 질문을 해보는 것도 방법입니다. 단순히 분산락은 동시 호출이라는 문제를 해결하기 위한 수단일 뿐 절대적인 해결 방법으로만 고려하는건 적절하지 않다고 생각합니다.

하지만 모든 동시성 상황이 선착순이나 재고 관리와 같은 상황이 아닐 수 있습니다. 간혹 동시성이 일어나는 경우 클라이언트의 잘못된 구현으로 동시 호출이 일어날 수도 있고, UX 상으로 동시성이 일어날 수 있는 설계일 수도 있습니다. 특히 클라이언트의 경우 이벤트 기반으로 동작하는 경우가 많아 동시 호출을 놓치는 경우도 종종 있었습니다.

모든 예외는 존재하지만 문제의 근본적인 원인과 그 문제의 임팩트에 대해 고민해보고 상황에 맞는 해결책을 제시하고 실행하는게 가장 중요합니다.

더 자세한 코드들과 테스트 코드들은 다음 링크에서 확인할 수 있습니다. https://github.com/ChoiEungi/redis-in-actions

참고 링크

This post is licensed under CC BY 4.0 by the author.

Spring Boot에서 Redis @Cacheable을 사용할 때 주의할 점

분산 시스템 환경에서 Spring Cloud Bus 없이 Spring Cloud Config 프로퍼티 Refresh하는 방법

diff --git "a/posts/\352\260\220\354\240\225\354\240\201-\352\262\260\354\240\225\352\263\274-\354\203\201\355\231\251-\352\267\200\354\235\270/index.html" "b/posts/\352\260\220\354\240\225\354\240\201-\352\262\260\354\240\225\352\263\274-\354\203\201\355\231\251-\352\267\200\354\235\270/index.html" new file mode 100644 index 0000000..c65f305 --- /dev/null +++ "b/posts/\352\260\220\354\240\225\354\240\201-\352\262\260\354\240\225\352\263\274-\354\203\201\355\231\251-\352\267\200\354\235\270/index.html" @@ -0,0 +1 @@ + 감정적 결정과 상황 귀인 | Ruggy Blog
Home 감정적 결정과 상황 귀인
Post
Cancel

감정적 결정과 상황 귀인

근래에 감정적인 결정과 발언이 늘었다. 가진 환경에 대한 불만족 때문이었다. 모든 환경에는 장단이 존재하지만 비교와 기대 불일치로 인한 스트레스는 감정적인 결정을 유발했다. 이유를 분석해보고 왜 그랬는 지에 대해 생각해보자.

The Conscious Discipline Brain State Model

The Conscious Discipline Brain State Model에 따르면, 사람의 뇌는 Executive, Emotional, Survival로 세가지 활성 상태가 존재한다. Executive 상태는 문제 해결 능력과 학습을 할 때 활성화되는 영역이며, 이성적인 판단을 할 때 주로 사용한다. Emotional와 Survival 상태는 말 그대로 감정적인 상태에 돌입하게 되며 보호 본능이 발생하게 된다. 다시 말해, 흑백 논리와 극단적 사고를 하는 감정적인 판단을 하게 된다. 이는 생존을 위한 본능으로 생각할 수 있다.

여기서 사람이 스트레스나 충격을 받기 시작하면 판단을 할 때 Executive 상태(전두엽)가 비활성화 된다. 이는 Emotional 상태와 Survival 상태가 활성화되어 사람이 감정적이고 이분법적으로 판단해 생각이 짧아지기 시작한다. 이러한 상태가 1달이넘게 지속되었으며 이를 스스로 빠져나오기 어려웠던 것 같다.

어떻게 나는 알았을까?

내가 처한 상황에 대해 감정적으로 접근하기 시작했다. 상황에 대한 단순히 좋고 나쁨을 판단하는 이분법적 접근이 늘었으며, 문제 상황을 해결할 수 있다는 낙관보다는 회의가 더 늘었다. 그러다보니 자연스럽게 책임을 상황으로 돌리기 시작했으며 이는 너무나도 스스로를 불행하게 만들었다. 또, 자기객관화 마저 잊어버리며 주변에서 이유를 찾았다.

비교와 기대, 억한 감정

개인적인 성격으로는 욕심이 많은 편이다. 기왕 하는거 조금 더 잘하고 싶어 스스로 동기부여를 많이한다. 이로 인해 내가 생각했던 기대하던 바가 컸다. 다만 현실은 생각한 것보다 우아하지는 못했고 투박했다. 내가 기대했던 것들과는 너무나도 달랐다. 그렇기에 타인의 “좋아보이는” 환경에 더 집중하고 이와 비교하게 되었으며 내 억한 감정은 더 커져갔다. 가장 아쉬운건 불행의 원인이 비교와 기대 관리에서 온다는 걸 스스로 알고도 계속 기대 관리를 실패하고 비교를 해나갔다는 점이다.

이러한 요인으로 하지 말아야 할 실수와 당연한 실수를 반복하게 되었다. 이는 스스로를 불행하게 만들었고 성장에는 전혀 도움이 되지 않았다. 그리고 타인에게 많은 회의적이고 부정적인 감정을 노출했으며 나 뿐만 아니라 타인의 시간을 낭비하도록 만들었다.

상황 귀인

성장에 대한 욕심으로 상황을 탓하는 건 정말 좋지 못하다. 얻을 결과에 대해만 집중하면 가진 것보다 부족한 부분에 집중하게 된다. 그렇다면 내가 처한 환경이 어떤지 확인하고 지금 집중해야 할 중요한 것들을 파악해보는건 어떨까? 이를 통해 상황을 개선하는 방법에 대해 고민할 수 있으며 얻을 수 있는 점에 집중해볼 수 있다.

예를 들어 회사의 레거시 코드에 아쉬움을 느낀다면 이 상황에서 어떤 점을 개선하면 좋을 지 함께 고민해보고 해결해나갈 수 있다. 이는 시스템 개선을 해본 성장할 수 있으며 팀에서 신뢰를 얻는데 도움이 된다. 또, 비슷한 상황에 대한 다른 관점도 찾아보는 점도 방법 중 하나이다. 하지만 상황에 대해 안 좋은 부분에만 집중하고 불평만 늘여놓는다면, 오히려 상황 귀인을 통해 스스로가 정체된다.

좋아하는 웹툰인 찌질의 역사에서 실수에 대한 구절이 인상적이었는데

어른이 되어서도 여전히 실수를 하고 잘못을 저지르고 누군가에게 상처를 입히고…그럴 수 있어.그렇다고 그게 찌질한 게 아니야.

실수를 합리화하고 정당화하고…

권위와 노련함으로 약한 사람에게 뒤집어 씌우고…

자신은 고결한 척, 완벽한 척. 잘못을 부정하고 외면하고

그게 찌질한 거야

잘못을 저지른 기억은 괴롭지. 하지만 잘못을 고치려는 노력조차 하지 않았던 기억은

훨씬 더 너를 괴롭힐 거야.

항상 부족함을 느끼지만, 본인의 잘못과 실수 인정할 수 있는 성숙한 어른이 되는건 정말 어려운 것 같다. 말로만 하는건 쉬우니 행동으로 옮길 수 있도록 스스로 꾸준하게 인지하자.

참고 문헌

https://consciousdiscipline.com/methodology/brain-state-model/

https://carmelmountainpreschool.com/conscious-discipline-the-three-brain-states/

This post is licensed under CC BY 4.0 by the author.

중요한 일에 집중하기

querydsl의 transform 메서드에서 발생하는 connection leak 현상

diff --git "a/posts/\354\202\266\354\235\230-\352\260\200\354\271\230-\354\230\244\355\233\204-10.56.38/index.html" "b/posts/\354\202\266\354\235\230-\352\260\200\354\271\230-\354\230\244\355\233\204-10.56.38/index.html" new file mode 100644 index 0000000..aebbad0 --- /dev/null +++ "b/posts/\354\202\266\354\235\230-\352\260\200\354\271\230-\354\230\244\355\233\204-10.56.38/index.html" @@ -0,0 +1 @@ + [Think] 삶의 가치 | Ruggy Blog
Home [Think] 삶의 가치
Post
Cancel

[Think] 삶의 가치

삶의 가치

셀리 케이건의 “죽음이란 무엇인가”에서 삶의 가치에 대해 논할 때, 삶-그릇 이론을 정의하고 내용을 전개한다. 삶-그릇 이론이란, 은 우리가 스스로 정의한 좋은 것과 나쁜 것들을 채워넣을 수 있는 그릇이라는 것이다. 그렇다면 삶이 얼마나 가치 있는지, 좋은지에 대해 평가하려면 그릇에 담긴 좋은 것과 나쁜 것들의 합을 구해 평가를 하는 것이다. 여기서 삶은 결국 그릇에 불과한 것이며, 그것 자체로는 아무런 가치가 없다고 볼 수 있다. 그렇다고 살아있음을 안좋다고 느끼는 것은 아니다.

이는 결국 내가 살아가는 가치는 내가 스스로 정의할 수 있다는 것이고, 내가 정한 기간 동안에 인생을 구성하게 될 가치물을 정하는 것이 중요하다는 말이다. 결국 내가 원하는 것들, 하고싶은 것들을 찾는 과정이라는 것은 내 삶의 가치물을 무엇인지 찾아가는 과정이라는 것이다. 이는 내 삶에 담을 그릇에 넣을, 내가 정의한 가치들이기 때문이다.

삶은 내가 정의한 가치 죽음이라는게 나쁜 것이라고 하자.(살아있음에 감사함과 기쁨을 느끼고 행복한 것들이 좋은 것들이라고 나는 느끼고 있다) 반면에 내 삶에 정의한 가치들이 아무 것도 없다면, 그 삶은 가치가 없어진다고 볼 수 있다는 것다. 그렇기 때문에 하고 싶은 것들을 스스로 정의하고 내가 정의한 가치들로 채워나가는게 중요하고 가치 있는 삶이지 않을까 싶다. 또한, 그 과정에서 느끼는 살아있음의 가치와 소중함을 느끼고 남아있는 여생에 최선을 다하고 싶을 따름이다.

그렇기에 내 삶에서 나는 가치를 1개월, 1년, 5년 등 단위로 내리고 그 기간동안 이 가치들로 채우는데 최선을 다하고 있는 중이다. 지금으로서 내 삶의 가치는 어떤 문제든 잘 해결하고 싶으며, 작은 부분에서라도 문제를 잘 찾아내는 것이다. 모든 상황을 부정적으로 본다는게 아니다. 그보다는 살아가면서 느낀 다수에게 pain point가 되는 부분이면서 해결할 때 피해가 없는(극 소수인) 부분에 대해 소프트웨어를 통해 해결하고 싶다.

This post is licensed under CC BY 4.0 by the author.

Spring Boot에서 git submodule로 민감 정보(yml) 관리

[Project] Gijol MVP 런칭 회고

diff --git "a/posts/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244_\355\224\204\353\246\254\354\275\224\354\212\244_1_2\354\243\274\354\260\250\355\233\204\352\270\260/index.html" "b/posts/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244_\355\224\204\353\246\254\354\275\224\354\212\244_1_2\354\243\274\354\260\250\355\233\204\352\270\260/index.html" new file mode 100644 index 0000000..f77858a --- /dev/null +++ "b/posts/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244_\355\224\204\353\246\254\354\275\224\354\212\244_1_2\354\243\274\354\260\250\355\233\204\352\270\260/index.html" @@ -0,0 +1,465 @@ + [우아한테크코스 4기 프리코스] 1주차 및 2주차 후기 | Ruggy Blog
Home [우아한테크코스 4기 프리코스] 1주차 및 2주차 후기
Post
Cancel

[우아한테크코스 4기 프리코스] 1주차 및 2주차 후기

이번 우아한테크코스 프리코스 4기에 함께 성장하는 방법을 배워보려 지원했다. 그러기 위해서는 함께 사용할 수 있는 코드(객체지향적 코드)를 작성해야 했고 이를 프리코스라는 과정을 통해 간접적으로 경험해볼 수 있었다.

1주차 과제는 숫자야구게임으로, 1주차 피드백만으로도 충분히 받아들일 수 있었고, 고통스럽게 하는 고민은 없었다. 다만, 2주차 과제에 고민이 있어 객체지향에 대해 고민을 더 많이 해본 선배님께 피드백을 받고 포스팅으로 고민을 공유하려한다. 1, 2주차 래포는 다음과 같다.

1주차

https://github.com/ChoiEungi/java-baseball-precourse

2주차

https://github.com/ChoiEungi/java-racingcar-precourse

1. InputRole의 멤버 변수에 대한 고민

본래 코드는 다음과 같았다. 본 코드에서 고민은 크게 2가지 였다.

  1. try- catch를 이용해 에러 헨들링을 진행해야 하는데, depth가 늘어나고, 코드의 중복 부분이 많아지고 이후 확장할 때 메서드의 길이가 15을 넘어갈 수 있을 것 같다.
  2. 꼭 nameList와 trialNumber가 class의 멤버 변수여야 하는가?
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+
public class InputRole {
+		private String[] nameList;
+    private Integer trialNmber;
+		public void inputStart() {
+        while (true) {
+            try {
+                inputNames();
+                break;
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+        while (true) {
+            try {
+                inputTrialNumber();
+                break;
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+	
+    public String[] getNameList() {
+        return nameList;
+    }
+		
+    public int getTrialNmber() {
+        return trialNmber;
+    }
+...
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+
public class GameController {
+	public void gameStart() {
+      InputRole inputRole = new InputRole();
+      OutputRole outputRole = new OutputRole();
+      inputRole.inputStart();
+      changeInputToCar(inputRole);
+
+      outputRole.pirntResultInstruction();
+      for (int i = 0; i < inputRole.getTrialNmber(); i++) {
+          tryGameOnce();
+          outputRole.printOneGame(carList);
+      }
+      findWinner();
+      outputRole.printWinner(winnersList);
+  }
+}
+
+

우선 1번 고민에 대해서는 try-catch 문의 기능적으로 크게 좋은 솔루션이 없어, 단순히 메서드를 분리하려 했다. 이는 캡슐화가 제대로 이뤄진다고 보기는 어렵지만, 지금 현실적인 상황에서 가장 최선의 방법이었다. 그리고 인풋을 받는 것이 극단적인 스케일인 몇 만개 이런 식으로 늘어나는 변화는 현실적으로 어렵기 때문에, 단순히 메서드를 분리하는 것으로 해결했다. 그러다보니 class 멤버 변수를 크게 사용할 이유가 없었고 inputStart() 메서드에 구애 받기 보다는 Util로서의 Input을 설정했다.

InputRole 이 단순히 인스턴스로서 상태를 갖는 객체일 수 있는데, 어디서든 특정 역할에 대한 인풋을 받고 싶으면 상태를 갖는 것이 아니라, 필요할 때 마다 필요한 인풋을 받아서 인풋의 결과값을 출력해주면 여러 클래스에서 여러 인스턴스를 선언 하지 않고 사용할 수 있게 된다.

고민 후 수정한 코드는 다음과 같다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+
public String[] getNameList() {
+        while (true) {
+            try {
+                return inputNames();
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+    public Integer getTrialNmber() {
+        while (true) {
+            try {
+                return inputTrialNumber();
+            } catch (IllegalArgumentException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+private String[] inputNames() {
+        System.out.println(NAME_INPUT_INSTRUCTION);
+        String inputNames = Console.readLine();
+        String[] nameList = inputNames.split(",");
+        for (String name : nameList) {
+            checkNameWhiteSpaceValid(name);
+            checkNameLengthValid(name);
+        }
+        return nameList;
+    }
+
+private Integer inputTrialNumber() {
+        System.out.println(TRIAL_NUMBER_INPUT_INSTRUCTION);
+        String inputNumber = Console.readLine();
+        checkTrialNumberValid(inputNumber);
+        return Integer.valueOf(inputNumber);
+    }
+
+...
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+
public class GameController {
+    private static final InputRole inputRole = new InputRole();
+
+    public void gameStart() {
+        OutputRole outputRole = new OutputRole();
+        changeInputToCar(inputRole.getNameList());
+
+        outputRole.pirntResultInstruction();
+        for (int i = 0; i < inputRole.getTrialNmber(); i++) {
+            tryGameOnce();
+            outputRole.printOneGame(carList);
+        }
+        findWinner();
+        outputRole.printWinner(winnersList);
+    }
+

이 코드를 보면 inputRole을 더 유연하게 사용할 수 있다는 점과 코드가 간결해진 점에서 크게 유용할 수 있었다.

2. Game 도메인 추가

  • GameController의 역할을 고민하다 보니 GameController의 역할이 너무 많다는게 느껴졌다. 내가 생각한 역할은 다음과 같다.
  1. InputRole에서 받아온 후 이를 게임을 진행한 후 OutputRole에 출력하는 역할
  2. Game을 진행한다.
    • 랜덤 숫자 생성
    • 게임을 진행한다.
    • 게임의 승자를 찾는다.

이러한 방식으로 역할이 나뉘었는데, 1번 2번은 같은 역할로서 볼 수 있겠지만, 3번은 충분히 분리해도 좋을 법할 것 같다는 생각을 해볼 수 있었다. 3번을 분리하기 전에 원래 코드를 보면 다음과 같다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+
public class GameController {
+    private static final InputRole inputRole = new InputRole();
+    private static final int MAX_PICK_NUMBER = 9;
+    private static final int MIN_PICK_NUMBER = 0;
+    private static final int MOVE_FORWARD_CONTION_NUMBER = 4;
+    private final ArrayList<Car> carList = new ArrayList<>();
+    private final ArrayList<String> winnersList = new ArrayList<>();
+
+    public void gameStart() {
+        OutputRole outputRole = new OutputRole();
+        changeInputToCar(inputRole.getNameList());
+
+        outputRole.pirntResultInstruction();
+        for (int i = 0; i < inputRole.getTrialNmber(); i++) {
+            tryGameOnce();
+            outputRole.printOneGame(carList);
+        }
+        findWinner();
+        outputRole.printWinner(winnersList);
+    }
+
+    private void changeInputToCar(String[] nameList) {
+        for (String name : nameList) {
+            this.carList.add(new Car(name));
+        }
+    }
+
+    private int getRandomNumber() {
+        int randomNumber = Randoms.pickNumberInRange(MIN_PICK_NUMBER, MAX_PICK_NUMBER);
+        return randomNumber;
+    }
+
+    private boolean checkMoveForward(int randomNumber) {
+        return randomNumber >= MOVE_FORWARD_CONTION_NUMBER;
+    }
+
+    private void tryGameOnce() {
+        for (Car car : carList) {
+            int randomNumber = getRandomNumber();
+            if (checkMoveForward(randomNumber)) {
+                car.moveForward();
+            }
+        }
+    }
+
+    private void findWinner() {
+        int maxValue = findMaxInCarList(carList);
+        for (Car car : carList) {
+            if (car.getPosition() == maxValue) {
+                winnersList.add(car.getName());
+            }
+        }
+    }
+
+    private int findMaxInCarList(ArrayList<Car> carList) {
+        int maxValue = -1;
+        for (Car car : carList) {
+            if (maxValue < car.getPosition()) {
+                maxValue = car.getPosition();
+            }
+        }
+        return maxValue;
+    }
+

코드가 너무나도 길다. 그리고 역할이 너무 많다. 이후 확장할 때 코드를 작성하는데 점점 부담이 커질 수 밖에 없는 구조인 것이다. 그렇기 때문에 이를 분리해보면 다음과 같이 코드를 깔끔하게 변경해볼 수 있었다.

1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+
public class Game {
+    private static final int MOVE_FORWARD_CONTION_NUMBER = 4;
+    private static final int MAX_PICK_NUMBER = 9;
+    private static final int MIN_PICK_NUMBER = 0;
+
+    public void startOnce(List<Car> carList) {
+        for (Car car : carList) {
+            int randomNumber = getRandomNumber();
+            if (checkMoveForward(randomNumber)) {
+                car.moveForward();
+            }
+        }
+    }
+
+    public List<String> winner(List<Car> carList) {
+        int maxValue = findMaxInCarList(carList);
+        List<String> winnersList = new ArrayList<>();
+        for (Car car : carList) {
+            if (car.getPosition() == maxValue) {
+                winnersList.add(car.getName());
+            }
+        }
+        return winnersList;
+    }
+
+    private int getRandomNumber() {
+        int randomNumber = Randoms.pickNumberInRange(MIN_PICK_NUMBER, MAX_PICK_NUMBER);
+        return randomNumber;
+    }
+
+    private boolean checkMoveForward(int randomNumber) {
+        return randomNumber >= MOVE_FORWARD_CONTION_NUMBER;
+    }
+
+    private int findMaxInCarList(List<Car> carList) {
+        int maxValue = -1;
+        for (Car car : carList) {
+            if (maxValue < car.getPosition()) {
+                maxValue = car.getPosition();
+            }
+        }
+        return maxValue;
+    }
+
+
1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+
public class GameController {
+    private static final InputRole inputRole = new InputRole();
+    private final List<Car> carList = new ArrayList<>();
+
+    public void gameStart() {
+        Game game = new Game();
+        OutputRole outputRole = new OutputRole();
+        changeInputToCar(inputRole.getNameList());
+        Integer trialNumber = inputRole.getTrialNmber();
+
+        outputRole.pirntResultInstruction();
+        for (int i = 0; i < trialNumber; i++) {
+            game.startOnce(carList);
+            outputRole.printOneGame(carList);
+        }
+        outputRole.printWinner(game.winner(carList));
+    }
+
+    private void changeInputToCar(String[] nameList) {
+        for (String name : nameList) {
+            this.carList.add(new Car(name));
+        }
+    }
+}
+

이렇게 역할(클래스)가 명확하게 분리돼 더 보기 좋고 확장성이 좋은 코드를 작성해볼 수 있었다. 리펙토링한 코드는 choieungi_refactor branch에서 확인해볼 수 있다.

추가적인 소감

객체지향적으로 코드를 변경하려 하니 클래스를 매개변수로 쓰는 것이 아닌, 클래스의 멤버 변수 하나를 매개변수로 쓸 수 있는 등 놓칠 수 있는 부분을 개선하게 되는 계기가 될 수 있었다. 이처럼 코드에 대해 고민을 계속하다보면 정말 필요한 코드만 작성할 수 있을 것이라 느낄 수 있었다.

코드 이외에는 2주차 피드백에서 다른 사람의 코드를 보고 함께 성장할 수 있기에 코드를 작성하면서 느낀 소감을 PR에 올리면 좋을 것 같다는 피드백이 굉장히 인상적이었고 다른 의미로 기뻤다. 나는 1, 2주차 PR에 모두 소감을 올렸는데, 내가 느낀 것들을 다른 사람도 볼 수 있으면 같이 성장할 수 있겠다 느꼈기 때문이다.

프리코스를 통해 단순히 시험의 과정이 아닌, 성장의 과정으로 느낄 수 있었던 뜻깊던 시간이 아닐까 싶다. 그렇기에 프리코스 과정이 굉장히 몰입감을 주고 재미도 있다. 다만, 자만은 하면 안된다. 아직 부족한 점이 많고, 생각해볼 여지가 많은 부분이 있는 코드이므로 3주차 과제에서는 더 큰 고통을 느껴보고 성장해나갈 것이다.

개인적으로는 피드백을 통해 고민해볼 수 있는 기회가 많아졌다. 그렇기 때문에 프리코스 과정 피드백에서 이런 부분을 구현해보면 어떻게 될까요? 와 같이 challenging해볼 수 있는 여지를 줘도 재밌을 것 같다는 느낌이 든다.

This post is licensed under CC BY 4.0 by the author.
diff --git "a/posts/\354\244\221\354\232\224\355\225\234-\354\235\274\354\227\220-\354\247\221\354\244\221\355\225\230\352\270\260/index.html" "b/posts/\354\244\221\354\232\224\355\225\234-\354\235\274\354\227\220-\354\247\221\354\244\221\355\225\230\352\270\260/index.html" new file mode 100644 index 0000000..992d879 --- /dev/null +++ "b/posts/\354\244\221\354\232\224\355\225\234-\354\235\274\354\227\220-\354\247\221\354\244\221\355\225\230\352\270\260/index.html" @@ -0,0 +1 @@ + 중요한 일에 집중하기 | Ruggy Blog
Home 중요한 일에 집중하기
Post
Cancel

중요한 일에 집중하기

중요한 일에 집중하기

샘 알트만과 폴 그레이엄은 공통적으로 인생은 짧다라는 말을 자주 한다. 인생이 짧다라는 의미는 결국 본인에게 중요하지 않은 일보다 중요한 일에 더 집중하게 만들기 때문이다.

그렇다면, 중요한 일은 무엇일까? 여러 방면으로 존재하겠지만, 근래에 가장 많이 시간을 사용하는 업무적인 부분에서 고민해보자. 진부하지만, 우선순위가 높은 일들과 임팩트가 큰 일들이라 생각한다. 그를 위한 역량으로는 결정은 시시각각 변하는 복잡한 시스템에서 최적의 결정을 할 수 있는거라 생각한다. 폴 그레이엄의 말을 조금 더 빌려보자면,

당신은 진짜 일이 어떤 것인지를 이해해야 하고, 당신이 어떤 일에 적합한지를 파악해야 하고, 그 일의 핵심에 가능한한 가깝게 접근해야 하고, 매 순간 당신이 할 수 있는 노력을 다하는지와 당신이 얼마나 잘하고 있는지를 정확하게 판단해야 하고, 결과를 해치지 않는 선에서 하루 몇 시간을 투자해야 하는지를 판단해야 합니다. 이는 모든 것이 연결된 매우 복잡한 방정식입니다. 하지만 매 순간 당신이 스스로에게 정직하고 스스로를 잘 판단할 수 있다면, 당신은 저절로 최적의 상태에 돌입하게 되며, 이 세상에 얼마 없는 생산적인 사람이 될 수 있을 것입니다.

무엇보다 중요한 건, 이 우선순위 내에서 맡은 일을 차질 없이 꾸준하게 “잘” 해내고 팀의 신뢰를 쌓아나가는 거라고 생각한다.

꾸준히 열심히 하는 점도 중요하지만, 잘하려고 노력하는 부분도 중요하다. 그렇기 때문에 개인적인 성장을 해야한다고 생각한다. 그리고 복리 효과(Compound Effect)를 나는 강력하게 믿고 있기 때문에 지금 더 많이, 그리고 이전의 나보다 훨씬 더 나아지려 최선을 다하고 있다.

업무 외적으로 중요한 부분은 소중한 관계, 여유, 업무 외 이뤄보고 싶은 성취 같은 것들이라 생각한다. 개인적으로는 주변에 있었던 좋은 사람들 덕분에 취업을 할 수 있었고, 금전적 여유를 통해 더 많은 자유가 생겼다. 그렇기에 20대 초반까지는 금전적인 고민을 줄이면서, 스스로에게 투자를 많이 해보고 싶다. 이를 바탕으로 주변 사람들에게 더 많은걸 배풀고 싶다. 좋은 사람이 되는 것은 너무나도 어렵지만, 더 많은 것들을 배우고 시행착오를 겪어보며 더 많은 사람에게 선한 영향력을 줄 수 있도록, 그리고 조금 더 낙관적이고 싶다.

또, 바쁘다라는 이유로 주변에 소중한 사람들에 소홀해지는 것도 다시 생각해보면, 정말 나에게 중요하지 않은 일을 하고 있을 때도 있었던 것 같다. 하루에 할 수 있는 가용성은 제한되어 있으며, 가용성을 넘어서 비효율적이더라도 무언가를 하려는 습관에 대해 되돌아 볼 필요성을 느끼게 되는 대목이다.

아직 하고 싶은 것과 나에게 잘 맞는 게 무엇인 지 구체적으로 모르겠지만, 분명히 온전히 몰입하고 성취하고 싶은게 생길거라 생각한다. 그게 우연이든, 필연이든 기회가 왔을 때 잘 소화하기 위해서는 복리 효과로 쌓아놓은 자산들(학습 능력, 네트워크 등)이라고 생각한다. 그렇기에 더 최선을 다할 것이며, 더 성장하고 싶다.

또, 성장의 일환으로 글또 활동을 참여하게 되었다. 글을 작성할 때 가장 큰 어려움은 꾸준함과 피드백 받기이다. 이를 글또 활동을 통해 잘 보완할 것이라 생각한다. 글또에 지원 동기에서, 감명받는 글을 더 많이 접하고 싶어서도 있었다. 나에게는, 샘 알트만, 폴 그레이엄, 샌드버드 김동신님의 블로그가 있다. 실제로 글또 슬랙에 올라오는 글 중에 인사이트가 많은 글들도 있어 너무 감사하며, 글또 활동을 꾸준히 이어나가고 싶다.

This post is licensed under CC BY 4.0 by the author.
diff --git a/redirects.json b/redirects.json new file mode 100644 index 0000000..cc5a306 --- /dev/null +++ b/redirects.json @@ -0,0 +1 @@ +{"/norobots/":"https://choieungi.github.io/404.html","/assets/":"https://choieungi.github.io/404.html","/posts/":"https://choieungi.github.io/404.html"} \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..a803ad2 --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * + +Disallow: /norobots/ + +Sitemap: https://choieungi.github.io/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..96ea731 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,229 @@ + + + +https://choieungi.github.io/posts/git-%EC%98%A4%EB%9E%98%EC%A0%84-%EC%BB%A4%EB%B0%8B%ED%95%9C-%EB%AF%BC%EA%B0%90-%EC%A0%95%EB%B3%B4-%EC%A7%80%EC%9A%B0%EA%B8%B0/ +2022-06-01T21:27:25+09:00 + + +https://choieungi.github.io/posts/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4_%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4_1_2%EC%A3%BC%EC%B0%A8%ED%9B%84%EA%B8%B0/ +2022-08-09T18:36:56+09:00 + + +https://choieungi.github.io/posts/log4j-vulnerability/ +2022-06-01T21:27:25+09:00 + + +https://choieungi.github.io/posts/git-submodule/ +2022-06-01T21:27:25+09:00 + + +https://choieungi.github.io/posts/%EC%82%B6%EC%9D%98-%EA%B0%80%EC%B9%98-%EC%98%A4%ED%9B%84-10.56.38/ +2022-05-07T23:55:00+09:00 + + +https://choieungi.github.io/posts/gijol-%ED%9A%8C%EA%B3%A0/ +2022-06-01T21:27:25+09:00 + + +https://choieungi.github.io/posts/Spring-Bulk-Insertion/ +2022-08-09T18:43:48+09:00 + + +https://choieungi.github.io/posts/Choosing-Architecture-for-developer/ +2022-08-10T02:06:34+09:00 + + +https://choieungi.github.io/posts/Spring-data-jpa-save/ +2022-08-24T17:42:37+09:00 + + +https://choieungi.github.io/posts/Berkeley-review/ +2022-10-16T16:18:53+09:00 + + +https://choieungi.github.io/posts/Mocking-test/ +2022-11-13T12:56:31+09:00 + + +https://choieungi.github.io/posts/domain-event-1/ +2022-11-13T12:55:00+09:00 + + +https://choieungi.github.io/posts/%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%BC%EC%97%90-%EC%A7%91%EC%A4%91%ED%95%98%EA%B8%B0/ +2023-03-06T01:03:06+09:00 + + +https://choieungi.github.io/posts/%EA%B0%90%EC%A0%95%EC%A0%81-%EA%B2%B0%EC%A0%95%EA%B3%BC-%EC%83%81%ED%99%A9-%EA%B7%80%EC%9D%B8/ +2023-03-06T01:20:39+09:00 + + +https://choieungi.github.io/posts/querydsl-connection-leak/ +2023-03-12T23:40:01+09:00 + + +https://choieungi.github.io/posts/amazon-aurora-storage-with-innodb/ +2023-04-09T23:45:00+09:00 + + +https://choieungi.github.io/posts/mysql-resources/ +2023-07-16T23:49:00+09:00 + + +https://choieungi.github.io/posts/spring-redis-cache-serialization-exception/ +2023-12-24T23:09:02+09:00 + + +https://choieungi.github.io/posts/spring-redis-distributed-lock/ +2023-12-24T23:45:14+09:00 + + +https://choieungi.github.io/posts/spring-cloud-refresh/ +2024-01-21T22:50:10+09:00 + + +https://choieungi.github.io/posts/spring-boot-cahce-error-handling/ +2024-03-31T11:22:00+09:00 + + +https://choieungi.github.io/categories/ +2024-03-31T23:29:17+09:00 + + +https://choieungi.github.io/tags/ +2024-03-31T23:29:17+09:00 + + +https://choieungi.github.io/archives/ +2024-03-31T23:29:17+09:00 + + +https://choieungi.github.io/about/ +2024-03-31T23:29:17+09:00 + + +https://choieungi.github.io/ + + +https://choieungi.github.io/tags/guide/ + + +https://choieungi.github.io/tags/git/ + + +https://choieungi.github.io/tags/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4/ + + +https://choieungi.github.io/tags/java/ + + +https://choieungi.github.io/tags/spring-boot/ + + +https://choieungi.github.io/tags/log4j/ + + +https://choieungi.github.io/tags/gradle/ + + +https://choieungi.github.io/tags/gist-%EC%B2%AD%EC%9B%90/ + + +https://choieungi.github.io/tags/book/ + + +https://choieungi.github.io/tags/%EC%A3%BD%EC%9D%8C%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80/ + + +https://choieungi.github.io/tags/%EC%85%B8%EB%A6%AC-%EC%BC%80%EC%9D%B4%EA%B1%B4/ + + +https://choieungi.github.io/tags/gijol/ + + +https://choieungi.github.io/tags/agile/ + + +https://choieungi.github.io/tags/jdbc/ + + +https://choieungi.github.io/tags/development/ + + +https://choieungi.github.io/tags/thinking/ + + +https://choieungi.github.io/tags/spring-data-jpa/ + + +https://choieungi.github.io/tags/spring/ + + +https://choieungi.github.io/tags/testing/ + + +https://choieungi.github.io/tags/spring/ + + +https://choieungi.github.io/tags/sw-%EB%A7%88%EC%97%90%EC%8A%A4%ED%8A%B8%EB%A1%9C/ + + +https://choieungi.github.io/tags/%ED%9A%8C%EA%B3%A0/ + + +https://choieungi.github.io/tags/querydsl/ + + +https://choieungi.github.io/tags/database/ + + +https://choieungi.github.io/tags/mysql/ + + +https://choieungi.github.io/tags/amazon-aurora/ + + +https://choieungi.github.io/tags/redis/ + + +https://choieungi.github.io/tags/spring-cloud-config/ + + +https://choieungi.github.io/tags/caching/ + + +https://choieungi.github.io/categories/development/ + + +https://choieungi.github.io/categories/git/ + + +https://choieungi.github.io/categories/project/ + + +https://choieungi.github.io/categories/%ED%9A%8C%EA%B3%A0/ + + +https://choieungi.github.io/categories/spring-boot/ + + +https://choieungi.github.io/categories/%EC%83%9D%EA%B0%81/ + + +https://choieungi.github.io/categories/book/ + + +https://choieungi.github.io/categories/spring-boot-development/ + + +https://choieungi.github.io/categories/thinking/ + + +https://choieungi.github.io/categories/testing/ + + +https://choieungi.github.io/page2/ + + +https://choieungi.github.io/page3/ + + diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..40be3fa --- /dev/null +++ b/sw.js @@ -0,0 +1 @@ +self.importScripts('/assets/js/data/swcache.js'); const cacheName = 'chirpy-20240331.2329'; function verifyDomain(url) { for (const domain of allowedDomains) { const regex = RegExp(`^http(s)?:\/\/${domain}\/`); if (regex.test(url)) { return true; } } return false; } function isExcluded(url) { for (const item of denyUrls) { if (url === item) { return true; } } return false; } self.addEventListener('install', e => { self.skipWaiting(); e.waitUntil( caches.open(cacheName).then(cache => { return cache.addAll(resource); }) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { if (response) { return response; } return fetch(event.request) .then(response => { const url = event.request.url; if (event.request.method !== 'GET' || !verifyDomain(url) || isExcluded(url)) { return response; } /* see: */ let responseToCache = response.clone(); caches.open(cacheName) .then(cache => { /* console.log('[sw] Caching new resource: ' + event.request.url); */ cache.put(event.request, responseToCache); }); return response; }); }) ); }); self.addEventListener('activate', e => { e.waitUntil( caches.keys().then(keyList => { return Promise.all( keyList.map(key => { if(key !== cacheName) { return caches.delete(key); } }) ); }) ); }); diff --git a/tags/agile/index.html b/tags/agile/index.html new file mode 100644 index 0000000..9fcfeaf --- /dev/null +++ b/tags/agile/index.html @@ -0,0 +1 @@ + Agile | Ruggy Blog
Home Tags Agile
Tag
Cancel
diff --git a/tags/amazon-aurora/index.html b/tags/amazon-aurora/index.html new file mode 100644 index 0000000..3b5ceee --- /dev/null +++ b/tags/amazon-aurora/index.html @@ -0,0 +1 @@ + Amazon Aurora | Ruggy Blog
Home Tags Amazon Aurora
Tag
Cancel
diff --git a/tags/book/index.html b/tags/book/index.html new file mode 100644 index 0000000..24b7d54 --- /dev/null +++ b/tags/book/index.html @@ -0,0 +1 @@ + Book | Ruggy Blog
Home Tags Book
Tag
Cancel
diff --git a/tags/caching/index.html b/tags/caching/index.html new file mode 100644 index 0000000..b894a54 --- /dev/null +++ b/tags/caching/index.html @@ -0,0 +1 @@ + Caching | Ruggy Blog
Home Tags Caching
Tag
Cancel
diff --git a/tags/database/index.html b/tags/database/index.html new file mode 100644 index 0000000..19fca18 --- /dev/null +++ b/tags/database/index.html @@ -0,0 +1 @@ + database | Ruggy Blog
Home Tags database
Tag
Cancel
diff --git a/tags/development/index.html b/tags/development/index.html new file mode 100644 index 0000000..660300e --- /dev/null +++ b/tags/development/index.html @@ -0,0 +1 @@ + Development | Ruggy Blog
Home Tags Development
Tag
Cancel
diff --git a/tags/gijol/index.html b/tags/gijol/index.html new file mode 100644 index 0000000..75d82ac --- /dev/null +++ b/tags/gijol/index.html @@ -0,0 +1 @@ + Gijol | Ruggy Blog
Home Tags Gijol
Tag
Cancel
diff --git "a/tags/gist-\354\262\255\354\233\220/index.html" "b/tags/gist-\354\262\255\354\233\220/index.html" new file mode 100644 index 0000000..a78d46d --- /dev/null +++ "b/tags/gist-\354\262\255\354\233\220/index.html" @@ -0,0 +1 @@ + GIST 청원 | Ruggy Blog
Home Tags GIST 청원
Tag
Cancel
diff --git a/tags/git/index.html b/tags/git/index.html new file mode 100644 index 0000000..9f568ab --- /dev/null +++ b/tags/git/index.html @@ -0,0 +1 @@ + Git | Ruggy Blog
Home Tags Git
Tag
Cancel
diff --git a/tags/gradle/index.html b/tags/gradle/index.html new file mode 100644 index 0000000..d4c5662 --- /dev/null +++ b/tags/gradle/index.html @@ -0,0 +1 @@ + gradle | Ruggy Blog
Home Tags gradle
Tag
Cancel
diff --git a/tags/guide/index.html b/tags/guide/index.html new file mode 100644 index 0000000..a338da6 --- /dev/null +++ b/tags/guide/index.html @@ -0,0 +1 @@ + Guide | Ruggy Blog
Home Tags Guide
Tag
Cancel
diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000..5c0824f --- /dev/null +++ b/tags/index.html @@ -0,0 +1 @@ + Tags | Ruggy Blog
Home Tags
Tags
Cancel
diff --git a/tags/java/index.html b/tags/java/index.html new file mode 100644 index 0000000..25a6d18 --- /dev/null +++ b/tags/java/index.html @@ -0,0 +1 @@ + Java | Ruggy Blog
Home Tags Java
Tag
Cancel
diff --git a/tags/jdbc/index.html b/tags/jdbc/index.html new file mode 100644 index 0000000..ae50591 --- /dev/null +++ b/tags/jdbc/index.html @@ -0,0 +1 @@ + JDBC | Ruggy Blog
Home Tags JDBC
Tag
Cancel
diff --git a/tags/log4j/index.html b/tags/log4j/index.html new file mode 100644 index 0000000..790fa58 --- /dev/null +++ b/tags/log4j/index.html @@ -0,0 +1 @@ + log4j | Ruggy Blog
Home Tags log4j
Tag
Cancel
diff --git a/tags/mysql/index.html b/tags/mysql/index.html new file mode 100644 index 0000000..35acae6 --- /dev/null +++ b/tags/mysql/index.html @@ -0,0 +1 @@ + MySQL | Ruggy Blog
Home Tags MySQL
Tag
Cancel
diff --git a/tags/querydsl/index.html b/tags/querydsl/index.html new file mode 100644 index 0000000..014d0e0 --- /dev/null +++ b/tags/querydsl/index.html @@ -0,0 +1 @@ + querydsl | Ruggy Blog
Home Tags querydsl
Tag
Cancel
diff --git a/tags/redis/index.html b/tags/redis/index.html new file mode 100644 index 0000000..567251c --- /dev/null +++ b/tags/redis/index.html @@ -0,0 +1 @@ + Redis | Ruggy Blog
Home Tags Redis
Tag
Cancel
diff --git a/tags/spring-boot/index.html b/tags/spring-boot/index.html new file mode 100644 index 0000000..54d5779 --- /dev/null +++ b/tags/spring-boot/index.html @@ -0,0 +1 @@ + Spring Boot | Ruggy Blog
Home Tags Spring Boot
Tag
Cancel
diff --git a/tags/spring-cloud-config/index.html b/tags/spring-cloud-config/index.html new file mode 100644 index 0000000..5d10278 --- /dev/null +++ b/tags/spring-cloud-config/index.html @@ -0,0 +1 @@ + Spring Cloud Config | Ruggy Blog
Home Tags Spring Cloud Config
Tag
Cancel
diff --git a/tags/spring-data-jpa/index.html b/tags/spring-data-jpa/index.html new file mode 100644 index 0000000..7cd6907 --- /dev/null +++ b/tags/spring-data-jpa/index.html @@ -0,0 +1 @@ + spring data jpa | Ruggy Blog
Home Tags spring data jpa
Tag
Cancel
diff --git a/tags/spring/index.html b/tags/spring/index.html new file mode 100644 index 0000000..66db075 --- /dev/null +++ b/tags/spring/index.html @@ -0,0 +1 @@ + Spring | Ruggy Blog
Home Tags Spring
Tag
Cancel
diff --git "a/tags/sw-\353\247\210\354\227\220\354\212\244\355\212\270\353\241\234/index.html" "b/tags/sw-\353\247\210\354\227\220\354\212\244\355\212\270\353\241\234/index.html" new file mode 100644 index 0000000..e8b789b --- /dev/null +++ "b/tags/sw-\353\247\210\354\227\220\354\212\244\355\212\270\353\241\234/index.html" @@ -0,0 +1 @@ + SW 마에스트로 | Ruggy Blog
Home Tags SW 마에스트로
Tag
Cancel
diff --git a/tags/testing/index.html b/tags/testing/index.html new file mode 100644 index 0000000..8d7f525 --- /dev/null +++ b/tags/testing/index.html @@ -0,0 +1 @@ + Testing | Ruggy Blog
Home Tags Testing
Tag
Cancel
diff --git a/tags/thinking/index.html b/tags/thinking/index.html new file mode 100644 index 0000000..840bb07 --- /dev/null +++ b/tags/thinking/index.html @@ -0,0 +1 @@ + Thinking | Ruggy Blog
Home Tags Thinking
Tag
Cancel
diff --git "a/tags/\354\205\270\353\246\254-\354\274\200\354\235\264\352\261\264/index.html" "b/tags/\354\205\270\353\246\254-\354\274\200\354\235\264\352\261\264/index.html" new file mode 100644 index 0000000..fc1d7f9 --- /dev/null +++ "b/tags/\354\205\270\353\246\254-\354\274\200\354\235\264\352\261\264/index.html" @@ -0,0 +1 @@ + 셸리 케이건 | Ruggy Blog
Home Tags 셸리 케이건
Tag
Cancel
diff --git "a/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244/index.html" "b/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244/index.html" new file mode 100644 index 0000000..bffa22f --- /dev/null +++ "b/tags/\354\232\260\354\225\204\355\225\234\355\205\214\355\201\254\354\275\224\354\212\244/index.html" @@ -0,0 +1 @@ + 우아한테크코스 | Ruggy Blog
Home Tags 우아한테크코스
Tag
Cancel
diff --git "a/tags/\354\243\275\354\235\214\354\235\264\353\236\200-\353\254\264\354\227\207\354\235\270\352\260\200/index.html" "b/tags/\354\243\275\354\235\214\354\235\264\353\236\200-\353\254\264\354\227\207\354\235\270\352\260\200/index.html" new file mode 100644 index 0000000..04c8aff --- /dev/null +++ "b/tags/\354\243\275\354\235\214\354\235\264\353\236\200-\353\254\264\354\227\207\354\235\270\352\260\200/index.html" @@ -0,0 +1 @@ + 죽음이란 무엇인가 | Ruggy Blog
Home Tags 죽음이란 무엇인가
Tag
Cancel
diff --git "a/tags/\355\232\214\352\263\240/index.html" "b/tags/\355\232\214\352\263\240/index.html" new file mode 100644 index 0000000..f17e6b3 --- /dev/null +++ "b/tags/\355\232\214\352\263\240/index.html" @@ -0,0 +1 @@ + 회고 | Ruggy Blog
Home Tags 회고
Tag
Cancel