<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>IT is True</title>
    <link>https://ittrue.tistory.com/</link>
    <description>Running for tomorrow</description>
    <language>ko</language>
    <pubDate>Tue, 12 May 2026 12:53:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>잇트루</managingEditor>
    <image>
      <title>IT is True</title>
      <url>https://tistory1.daumcdn.net/tistory/3708624/attach/76552eba71f34b109b0c42d7e9cc866a</url>
      <link>https://ittrue.tistory.com</link>
    </image>
    <item>
      <title>2048</title>
      <link>https://ittrue.tistory.com/571</link>
      <description>&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=klh7I70h3vg&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/fSSSK/hyZRcTRCwB/ENt7G3XHZDYGjVkxNGhmI1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/baB2JK/hyZRahpmsT/JyCuomxdFlE6YFkLpK53LK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;(playlist) 이 노래 너무 좋은걸? 나만 들을수 없지⎜잔잔한 피아노 연주곡 플리 &quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/klh7I70h3vg&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;game-2048-wrapper&quot; style=&quot;font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif; background-color: #faf8ef; color: #776e65; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 10px; max-width: 500px; margin: 0 auto; user-select: none; -webkit-user-select: none;&quot;&gt;
&lt;style&gt;
        /* 스타일 정의 */
        #game-2048-wrapper .header-container {
            display: flex;
            justify-content: space-between;
            align-items: center;
            width: 100%;
            max-width: 400px;
            margin-bottom: 20px;
        }

        #game-2048-wrapper .title {
            font-size: 40px;
            font-weight: bold;
            color: #776e65;
            margin: 0;
        }

        #game-2048-wrapper .score-container {
            display: flex;
            gap: 5px;
        }

        #game-2048-wrapper .score-box {
            background: #bbada0;
            padding: 5px 15px;
            border-radius: 5px;
            color: white;
            text-align: center;
            min-width: 50px;
        }

        #game-2048-wrapper .score-label {
            font-size: 10px;
            color: #eee4da;
            text-transform: uppercase;
        }

        #game-2048-wrapper .score-value {
            font-size: 20px;
            font-weight: bold;
        }

        #game-2048-wrapper .game-container {
            position: relative;
            width: 300px;
            height: 300px;
            background: #bbada0;
            border-radius: 6px;
            padding: 10px;
            touch-action: none; /* 모바일 스크롤 방지 */
        }

        #game-2048-wrapper .grid-container {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            grid-template-rows: repeat(4, 1fr);
            gap: 10px;
            width: 100%;
            height: 100%;
        }

        #game-2048-wrapper .grid-cell {
            background: rgba(238, 228, 218, 0.35);
            border-radius: 3px;
            width: 100%;
            height: 100%;
        }

        #game-2048-wrapper .tile-container {
            position: absolute;
            top: 10px;
            left: 10px;
            right: 10px;
            bottom: 10px;
            z-index: 2;
        }

        #game-2048-wrapper .tile {
            position: absolute;
            width: calc(25% - 7.5px); /* Gap 고려한 계산 */
            height: calc(25% - 7.5px);
            border-radius: 3px;
            background: #eee4da;
            color: #776e65;
            font-weight: bold;
            font-size: 30px;
            display: flex;
            justify-content: center;
            align-items: center;
            transition: transform 0.1s ease-in-out, background-color 0.1s;
            animation: pop 0.2s ease-in;
        }

        @keyframes pop {
            0% { transform: scale(0); }
            50% { transform: scale(1.2); }
            100% { transform: scale(1); }
        }

        /* 타일 색상 (값에 따라 변화) */
        .tile-2 { background: #eee4da; color: #776e65; }
        .tile-4 { background: #ede0c8; color: #776e65; }
        .tile-8 { background: #f2b179; color: #f9f6f2; }
        .tile-16 { background: #f59563; color: #f9f6f2; }
        .tile-32 { background: #f67c5f; color: #f9f6f2; }
        .tile-64 { background: #f65e3b; color: #f9f6f2; }
        .tile-128 { background: #edcf72; color: #f9f6f2; font-size: 24px; }
        .tile-256 { background: #edcc61; color: #f9f6f2; font-size: 24px; }
        .tile-512 { background: #edc850; color: #f9f6f2; font-size: 24px; }
        .tile-1024 { background: #edc53f; color: #f9f6f2; font-size: 18px; }
        .tile-2048 { background: #edc22e; color: #f9f6f2; font-size: 18px; box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.4); }

        #game-2048-wrapper .restart-btn {
            background: #8f7a66;
            color: #f9f6f2;
            border: none;
            border-radius: 3px;
            padding: 10px 20px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            outline: none;
            margin-top: 10px;
        }

        #game-2048-wrapper .game-message {
            display: none;
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(238, 228, 218, 0.73);
            z-index: 10;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            text-align: center;
        }
        
        #game-2048-wrapper .game-message.game-over { display: flex; }
        #game-2048-wrapper .game-message p { font-size: 40px; font-weight: bold; color: #776e65; margin-bottom: 20px;}
    &lt;/style&gt;
&lt;div class=&quot;header-container&quot;&gt;
&lt;h1 class=&quot;title&quot;&gt;2048&lt;/h1&gt;
&lt;div class=&quot;score-container&quot;&gt;
&lt;div class=&quot;score-box&quot;&gt;
&lt;div class=&quot;score-label&quot;&gt;SCORE&lt;/div&gt;
&lt;div id=&quot;score-now&quot; class=&quot;score-value&quot;&gt;0&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;score-box&quot;&gt;
&lt;div class=&quot;score-label&quot;&gt;BEST&lt;/div&gt;
&lt;div id=&quot;score-best&quot; class=&quot;score-value&quot;&gt;0&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;game-board-area&quot; class=&quot;game-container&quot;&gt;
&lt;div class=&quot;grid-container&quot;&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;grid-cell&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;tile-container&quot; class=&quot;tile-container&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div id=&quot;game-msg&quot; class=&quot;game-message&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Game Over!&lt;/p&gt;
&lt;button class=&quot;restart-btn&quot;&gt;Try Again&lt;/button&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;button class=&quot;restart-btn&quot; style=&quot;margin-top: 20px;&quot;&gt;New Game&lt;/button&gt;
&lt;div style=&quot;margin-top: 15px; font-size: 13px; color: #999;&quot;&gt;키보드 방향키 또는 화면을 밀어서 플레이하세요.&lt;/div&gt;
&lt;script&gt;
        // 변수 충돌 방지를 위한 전역 변수 설정 (블로그 환경 고려)
        var board2048 = [];
        var score2048 = 0;
        var bestScore2048 = localStorage.getItem('bestScore2048') || 0;
        var isGameOver2048 = false;

        document.getElementById('score-best').innerText = bestScore2048;

        function init2048Game() {
            board2048 = [
                [0, 0, 0, 0],
                [0, 0, 0, 0],
                [0, 0, 0, 0],
                [0, 0, 0, 0]
            ];
            score2048 = 0;
            isGameOver2048 = false;
            document.getElementById('score-now').innerText = 0;
            document.getElementById('game-msg').classList.remove('game-over');
            
            addNewTile();
            addNewTile();
            updateView();
        }

        // 빈 칸 찾아서 랜덤 숫자(2 또는 4) 생성
        function addNewTile() {
            let emptyCells = [];
            for(let r=0; r&lt;4; r++) {
                for(let c=0; c&lt;4; c++) {
                    if(board2048[r][c] === 0) emptyCells.push({r, c});
                }
            }
            if(emptyCells.length &gt; 0) {
                let rand = emptyCells[Math.floor(Math.random() * emptyCells.length)];
                board2048[rand.r][rand.c] = Math.random() &lt; 0.9 ? 2 : 4;
            }
        }

        // 화면 그리기
        function updateView() {
            const container = document.getElementById('tile-container');
            container.innerHTML = ''; // 초기화

            for(let r=0; r&lt;4; r++) {
                for(let c=0; c&lt;4; c++) {
                    let val = board2048[r][c];
                    if(val &gt; 0) {
                        let tile = document.createElement('div');
                        tile.classList.add('tile');
                        tile.classList.add('tile-' + (val &gt; 2048 ? 2048 : val));
                        tile.innerText = val;
                        
                        // 위치 계산 (Gap 10px 포함)
                        // Cell 크기: 약 66px, Gap: 10px -&gt; step: 76px ~ 
                        // %로 계산: Left = c * 25%, Top = r * 25%
                        tile.style.left = (c * 25) + '%';
                        tile.style.top = (r * 25) + '%';
                        
                        container.appendChild(tile);
                    }
                }
            }
        }

        // 게임 로직: 한 줄 밀기 및 합치기
        function slideRow(row) {
            // 0을 제외한 숫자만 필터링
            let filtered = row.filter(num =&gt; num !== 0);
            let slide = [];
            
            // 합치기 로직
            for (let i = 0; i &lt; filtered.length; i++) {
                if (i + 1 &lt; filtered.length &amp;&amp; filtered[i] === filtered[i + 1]) {
                    let mergedVal = filtered[i] * 2;
                    slide.push(mergedVal);
                    score2048 += mergedVal;
                    i++; // 다음 숫자는 이미 합쳐졌으므로 건너뜀
                } else {
                    slide.push(filtered[i]);
                }
            }
            
            // 남은 자리를 0으로 채움
            while (slide.length &lt; 4) {
                slide.push(0);
            }
            return slide;
        }

        // 보드 움직임 처리
        function move(direction) {
            if(isGameOver2048) return;

            let oldBoard = JSON.stringify(board2048);
            
            if (direction === 'left' || direction === 'right') {
                for (let r = 0; r &lt; 4; r++) {
                    let row = board2048[r];
                    if (direction === 'right') {
                        let reversedRow = row.slice().reverse();
                        let newRow = slideRow(reversedRow);
                        board2048[r] = newRow.reverse();
                    } else {
                        board2048[r] = slideRow(row);
                    }
                }
            } else if (direction === 'up' || direction === 'down') {
                for (let c = 0; c &lt; 4; c++) {
                    let col = [board2048[0][c], board2048[1][c], board2048[2][c], board2048[3][c]];
                    if (direction === 'down') {
                        let reversedCol = col.reverse();
                        let newCol = slideRow(reversedCol);
                        newCol.reverse();
                        for (let r = 0; r &lt; 4; r++) board2048[r][c] = newCol[r];
                    } else {
                        let newCol = slideRow(col);
                        for (let r = 0; r &lt; 4; r++) board2048[r][c] = newCol[r];
                    }
                }
            }

            // 변화가 있었으면 새 타일 생성 및 점수 업데이트
            if (oldBoard !== JSON.stringify(board2048)) {
                addNewTile();
                document.getElementById('score-now').innerText = score2048;
                
                if (score2048 &gt; bestScore2048) {
                    bestScore2048 = score2048;
                    document.getElementById('score-best').innerText = bestScore2048;
                    localStorage.setItem('bestScore2048', bestScore2048);
                }
                
                updateView();
                checkGameOver();
            }
        }

        function checkGameOver() {
            // 빈칸이 있으면 게임 진행 가능
            for(let r=0; r&lt;4; r++) {
                for(let c=0; c&lt;4; c++) {
                    if(board2048[r][c] === 0) return;
                }
            }
            // 상하좌우 합칠 수 있는게 있는지 확인
            for(let r=0; r&lt;4; r++) {
                for(let c=0; c&lt;4; c++) {
                    if (c &lt; 3 &amp;&amp; board2048[r][c] === board2048[r][c+1]) return;
                    if (r &lt; 3 &amp;&amp; board2048[r][c] === board2048[r+1][c]) return;
                }
            }
            // 여기까지 왔으면 게임 오버
            isGameOver2048 = true;
            document.getElementById('game-msg').classList.add('game-over');
        }

        // 키보드 이벤트
        document.addEventListener('keydown', (e) =&gt; {
            if([37, 38, 39, 40].includes(e.keyCode)) {
                e.preventDefault(); // 스크롤 방지
                if (e.keyCode === 37) move('left');
                else if (e.keyCode === 38) move('up');
                else if (e.keyCode === 39) move('right');
                else if (e.keyCode === 40) move('down');
            }
        });

        // 모바일 터치 이벤트 (스와이프)
        let touchStartX = 0;
        let touchStartY = 0;
        const gameArea = document.getElementById('game-board-area');

        gameArea.addEventListener('touchstart', (e) =&gt; {
            touchStartX = e.changedTouches[0].screenX;
            touchStartY = e.changedTouches[0].screenY;
        }, {passive: false});

        gameArea.addEventListener('touchend', (e) =&gt; {
            e.preventDefault();
            let touchEndX = e.changedTouches[0].screenX;
            let touchEndY = e.changedTouches[0].screenY;
            
            let dx = touchEndX - touchStartX;
            let dy = touchEndY - touchStartY;
            
            if (Math.abs(dx) &gt; Math.abs(dy)) {
                // 좌우 이동 (감도 30px 이상)
                if (Math.abs(dx) &gt; 30) {
                    if (dx &gt; 0) move('right');
                    else move('left');
                }
            } else {
                // 상하 이동
                if (Math.abs(dy) &gt; 30) {
                    if (dy &gt; 0) move('down');
                    else move('up');
                }
            }
        }, {passive: false});

        // 게임 시작
        init2048Game();

    &lt;/script&gt;
&lt;/div&gt;</description>
      <category>놀이터</category>
      <category>2048</category>
      <category>게임</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/571</guid>
      <comments>https://ittrue.tistory.com/571#entry571comment</comments>
      <pubDate>Tue, 6 Jan 2026 09:00:44 +0900</pubDate>
    </item>
    <item>
      <title>테트리스 (Tetris)</title>
      <link>https://ittrue.tistory.com/570</link>
      <description>&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=VCrxWHTCNo8&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/vQX9F/hyZQKhMmPJ/G5zn7GuxazcKGuNj2T5rK1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/bcX9SJ/hyZQNlhAqH/cKX3SBqTF6WjxZvb0e3jA1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;Tetris Theme Song 1 Hour Loop&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/VCrxWHTCNo8&quot; width=&quot;480&quot; height=&quot;360&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;tetris-final-wrapper&quot; style=&quot;background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #fff; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); max-width: 600px; margin: 0 auto; position: relative;&quot;&gt;
&lt;style&gt;
        #tetris-final-wrapper canvas {
            display: block;
            background-color: rgba(0, 0, 0, 0.8);
            border: 2px solid #4a4a6a;
            border-radius: 4px;
            box-shadow: 0 0 15px rgba(0,0,0,0.5);
        }
        
        .game-layout {
            display: flex;
            gap: 20px;
            align-items: flex-start;
        }

        .side-panel {
            display: flex;
            flex-direction: column;
            gap: 15px;
            width: 90px;
        }

        .info-box {
            background: rgba(255, 255, 255, 0.1);
            padding: 10px 5px;
            border-radius: 8px;
            text-align: center;
            min-height: 80px;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .info-label {
            font-size: 12px;
            color: #aaa;
            margin-bottom: 5px;
            text-transform: uppercase;
            font-weight: bold;
        }

        .info-value {
            font-size: 18px;
            font-weight: bold;
            color: #fff;
        }

        .mini-canvas {
            width: 80px; 
            height: 80px; 
            margin: 0 auto; 
            background: transparent; 
            border: none; 
            box-shadow: none !important;
        }

        .controls-guide {
            margin-top: 20px;
            font-size: 13px;
            color: #888;
            text-align: center;
            line-height: 1.8;
            background: rgba(0,0,0,0.2);
            padding: 15px;
            border-radius: 8px;
            width: 100%;
        }

        #message-overlay {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 32px;
            font-weight: 900;
            color: #ffeb3b;
            text-shadow: 0 0 10px rgba(255, 235, 59, 0.8);
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.2s;
            z-index: 10;
            text-align: center;
            white-space: nowrap;
        }

        .key {
            display: inline-block;
            background: #333;
            padding: 2px 6px;
            border-radius: 4px;
            border-bottom: 2px solid #111;
            font-size: 11px;
            color: #eee;
            margin: 0 2px;
        }
    &lt;/style&gt;
&lt;div id=&quot;message-overlay&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;game-layout&quot;&gt;
&lt;div class=&quot;side-panel&quot;&gt;
&lt;div class=&quot;info-box&quot;&gt;
&lt;div class=&quot;info-label&quot;&gt;Hold&lt;/div&gt;
&lt;canvas id=&quot;tetris-hold&quot; class=&quot;mini-canvas&quot; width=&quot;80&quot; height=&quot;80&quot;&gt;&lt;/canvas&gt;&lt;/div&gt;
&lt;div class=&quot;info-box&quot; style=&quot;justify-content: flex-start; padding-top: 15px;&quot;&gt;
&lt;div class=&quot;info-label&quot;&gt;Score&lt;/div&gt;
&lt;div id=&quot;score-val&quot; class=&quot;info-value&quot;&gt;0&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;canvas id=&quot;tetris-main&quot; width=&quot;240&quot; height=&quot;400&quot;&gt;&lt;/canvas&gt;
&lt;div class=&quot;side-panel&quot;&gt;
&lt;div class=&quot;info-box&quot;&gt;
&lt;div class=&quot;info-label&quot;&gt;Next&lt;/div&gt;
&lt;canvas id=&quot;tetris-next&quot; class=&quot;mini-canvas&quot; width=&quot;80&quot; height=&quot;80&quot;&gt;&lt;/canvas&gt;&lt;/div&gt;
&lt;div class=&quot;info-box&quot; style=&quot;justify-content: flex-start; padding-top: 15px;&quot;&gt;
&lt;div class=&quot;info-label&quot;&gt;Combo&lt;/div&gt;
&lt;div id=&quot;combo-val&quot; class=&quot;info-value&quot; style=&quot;color: #ff4444;&quot;&gt;0&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;controls-guide&quot;&gt;&lt;span class=&quot;key&quot;&gt;&amp;larr;&lt;/span&gt; &lt;span class=&quot;key&quot;&gt;&amp;rarr;&lt;/span&gt; 이동 &amp;nbsp; &lt;span class=&quot;key&quot;&gt;&amp;uarr;&lt;/span&gt; 회전 &amp;nbsp; &lt;span class=&quot;key&quot;&gt;&amp;darr;&lt;/span&gt; 내리기&lt;br /&gt;&lt;span class=&quot;key&quot;&gt;Space&lt;/span&gt; 하드 드롭 &amp;nbsp;&lt;span class=&quot;key&quot;&gt;Shift&lt;/span&gt; 홀드(저장)&lt;/div&gt;
&lt;script&gt;
    (function() {
        // --- 1. 기본 설정 및 유틸리티 ---
        const canvas = document.getElementById('tetris-main');
        const ctx = canvas.getContext('2d');
        const nextCanvas = document.getElementById('tetris-next');
        const nextCtx = nextCanvas.getContext('2d');
        const holdCanvas = document.getElementById('tetris-hold');
        const holdCtx = holdCanvas.getContext('2d');
        const msgOverlay = document.getElementById('message-overlay');

        ctx.scale(20, 20);
        nextCtx.scale(20, 20);
        holdCtx.scale(20, 20);

        const colors = [
            null,
            '#FF0D72', // T
            '#0DC2FF', // I
            '#0DFF72', // S
            '#F538FF', // Z
            '#FF8E0D', // L
            '#FFE138', // O
            '#3877FF', // J
        ];

        // --- 2. 게임 로직 함수들 ---

        function createMatrix(w, h) {
            const matrix = [];
            while (h--) matrix.push(new Array(w).fill(0));
            return matrix;
        }

        function createPiece(type) {
            if (type === 'I') {
                return [ [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0] ];
            } else if (type === 'L') {
                return [ [0, 2, 0], [0, 2, 0], [0, 2, 2] ];
            } else if (type === 'J') {
                return [ [0, 3, 0], [0, 3, 0], [3, 3, 0] ];
            } else if (type === 'O') {
                return [ [4, 4], [4, 4] ];
            } else if (type === 'Z') {
                return [ [5, 5, 0], [0, 5, 5], [0, 0, 0] ];
            } else if (type === 'S') {
                return [ [0, 6, 6], [6, 6, 0], [0, 0, 0] ];
            } else if (type === 'T') {
                return [ [0, 7, 0], [7, 7, 7], [0, 0, 0] ];
            }
        }

        function showMessage(text) {
            msgOverlay.innerText = text;
            msgOverlay.style.opacity = 1;
            msgOverlay.style.top = '40%';
            setTimeout(() =&gt; {
                msgOverlay.style.opacity = 0;
                msgOverlay.style.top = '50%';
            }, 800);
        }

        // T-Spin 감지
        function checkTSpin(matrix, pos, arena) {
            if (matrix.length !== 3 || matrix[0][1] !== 7) return false;
            if (!player.lastMoveRotate) return false;
            const corners = [{x: 0, y: 0}, {x: 2, y: 0}, {x: 0, y: 2}, {x: 2, y: 2}];
            let occupied = 0;
            corners.forEach(c =&gt; {
                const x = pos.x + c.x;
                const y = pos.y + c.y;
                if (x &lt; 0 || x &gt;= arena[0].length || y &gt;= arena.length || (arena[y] &amp;&amp; arena[y][x] !== 0)) {
                    occupied++;
                }
            });
            return occupied &gt;= 3;
        }

        function arenaSweep() {
            let rowCount = 0;
            outer: for (let y = arena.length - 1; y &gt; 0; --y) {
                for (let x = 0; x &lt; arena[y].length; ++x) {
                    if (arena[y][x] === 0) continue outer;
                }
                const row = arena.splice(y, 1)[0].fill(0);
                arena.unshift(row);
                ++y;
                rowCount++;
            }

            if (rowCount &gt; 0) {
                player.combo++;
                let lineScore = 0;
                if (rowCount === 1) lineScore = 100;
                else if (rowCount === 2) lineScore = 300;
                else if (rowCount === 3) lineScore = 500;
                else if (rowCount === 4) lineScore = 800;

                if (player.combo &gt; 1) {
                    lineScore += (player.combo * 50);
                    showMessage(player.combo + &quot; COMBO!&quot;);
                }

                if (player.tSpin) {
                    lineScore *= 2;
                    showMessage(&quot;T-SPIN!&quot;);
                } else if (rowCount === 4) {
                    showMessage(&quot;TETRIS!&quot;);
                }
                player.score += lineScore;
            } else {
                player.combo = 0;
            }
            updateScore();
        }

        function collide(arena, player) {
            const [m, o] = [player.matrix, player.pos];
            for (let y = 0; y &lt; m.length; ++y) {
                for (let x = 0; x &lt; m[y].length; ++x) {
                    if (m[y][x] !== 0 &amp;&amp; (arena[y + o.y] &amp;&amp; arena[y + o.y][x + o.x]) !== 0) {
                        return true;
                    }
                }
            }
            return false;
        }

        // 격자무늬 그리기 함수
        function drawGrid() {
            ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
            ctx.lineWidth = 0.05; // 20배 확대되었으므로 1픽셀은 0.05
            
            // 세로선
            for (let x = 1; x &lt; 12; x++) {
                ctx.beginPath();
                ctx.moveTo(x, 0);
                ctx.lineTo(x, 20);
                ctx.stroke();
            }
            // 가로선
            for (let y = 1; y &lt; 20; y++) {
                ctx.beginPath();
                ctx.moveTo(0, y);
                ctx.lineTo(12, y);
                ctx.stroke();
            }
        }

        function drawGhost() {
            const ghost = { pos: { ...player.pos }, matrix: player.matrix };
            while (!collide(arena, ghost)) {
                ghost.pos.y++;
            }
            ghost.pos.y--;
            drawMatrix(ghost.matrix, ghost.pos, ctx, true);
        }

        function draw() {
            ctx.fillStyle = '#000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            drawGrid(); // 격자 그리기
            drawMatrix(arena, {x: 0, y: 0}, ctx);
            drawGhost(); 
            drawMatrix(player.matrix, player.pos, ctx);
        }

        // 미니 캔버스(Next, Hold) 그리기 공용 함수
        function drawMini(matrix, context) {
            context.fillStyle = '#000';
            context.fillRect(0, 0, 4, 4); // 80x80픽셀은 scale(20) 기준 4x4
            if (!matrix) return;
            
            const xOffset = (4 - matrix[0].length) / 2;
            const yOffset = (4 - matrix.length) / 2;
            drawMatrix(matrix, {x: xOffset, y: yOffset}, context);
        }

        function drawMatrix(matrix, offset, context, isGhost = false) {
            matrix.forEach((row, y) =&gt; {
                row.forEach((value, x) =&gt; {
                    if (value !== 0) {
                        context.fillStyle = colors[value];
                        if (isGhost) {
                            context.globalAlpha = 0.2;
                            context.fillRect(x + offset.x, y + offset.y, 1, 1);
                            context.globalAlpha = 1.0;
                            context.strokeStyle = colors[value];
                            context.lineWidth = 0.05;
                            context.strokeRect(x + offset.x, y + offset.y, 1, 1);
                        } else {
                            context.fillRect(x + offset.x, y + offset.y, 1, 1);
                            context.fillStyle = 'rgba(255,255,255,0.1)';
                            context.fillRect(x + offset.x, y + offset.y, 1, 0.2);
                        }
                    }
                });
            });
        }

        function merge(arena, player) {
            player.matrix.forEach((row, y) =&gt; {
                row.forEach((value, x) =&gt; {
                    if (value !== 0) {
                        arena[y + player.pos.y][x + player.pos.x] = value;
                    }
                });
            });
        }

        function rotate(matrix, dir) {
            for (let y = 0; y &lt; matrix.length; ++y) {
                for (let x = 0; x &lt; y; ++x) {
                    [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
                }
            }
            if (dir &gt; 0) matrix.forEach(row =&gt; row.reverse());
            else matrix.reverse();
        }

        function playerDrop() {
            player.pos.y++;
            if (collide(arena, player)) {
                player.pos.y--;
                player.tSpin = checkTSpin(player.matrix, player.pos, arena);
                merge(arena, player);
                playerReset();
                arenaSweep();
                player.lastMoveRotate = false;
            }
            dropCounter = 0;
        }

        function playerHardDrop() {
            while (!collide(arena, player)) {
                player.pos.y++;
            }
            player.pos.y--;
            player.tSpin = checkTSpin(player.matrix, player.pos, arena);
            merge(arena, player);
            playerReset();
            arenaSweep();
            player.lastMoveRotate = false;
            dropCounter = 0;
        }

        function playerMove(offset) {
            player.pos.x += offset;
            if (collide(arena, player)) {
                player.pos.x -= offset;
            }
            player.lastMoveRotate = false;
        }

        // 홀드 기능 함수
        function playerHold() {
            if (!player.canHold) return; // 이번 턴에 이미 사용했으면 무시

            if (!player.holdMatrix) {
                // 저장된게 없으면: 현재것 저장, 넥스트 가져옴
                player.holdMatrix = player.matrix;
                playerReset(true); // true = skip hold reset
            } else {
                // 저장된게 있으면: 서로 교체
                const temp = player.matrix;
                player.matrix = player.holdMatrix;
                player.holdMatrix = temp;
                
                // 위치 초기화
                player.pos.y = 0;
                player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
            }
            
            player.canHold = false; // 사용 처리
            player.lastMoveRotate = false;
            drawMini(player.holdMatrix, holdCtx); // 홀드 화면 갱신
        }

        function playerReset(fromHold = false) {
            if (!fromHold) {
                 if (player.nextMatrix === null) {
                     const pieces = 'ILJOTSZ';
                     player.nextMatrix = createPiece(pieces[pieces.length * Math.random() | 0]);
                }
                player.matrix = player.nextMatrix;
                const pieces = 'ILJOTSZ';
                player.nextMatrix = createPiece(pieces[pieces.length * Math.random() | 0]);
                drawMini(player.nextMatrix, nextCtx);
                player.canHold = true; // 새 블록이 나오면 홀드 가능해짐
            }

            player.pos.y = 0;
            player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);

            if (collide(arena, player)) {
                arena.forEach(row =&gt; row.fill(0));
                player.score = 0;
                player.combo = 0;
                player.holdMatrix = null; // 게임오버시 홀드 초기화
                drawMini(null, holdCtx);
                updateScore();
                showMessage(&quot;GAME OVER&quot;);
            }
        }

        function playerRotate(dir) {
            const pos = player.pos.x;
            let offset = 1;
            rotate(player.matrix, dir);
            while (collide(arena, player)) {
                player.pos.x += offset;
                offset = -(offset + (offset &gt; 0 ? 1 : -1));
                if (offset &gt; player.matrix[0].length) {
                    rotate(player.matrix, -dir);
                    player.pos.x = pos;
                    return;
                }
            }
            player.lastMoveRotate = true;
        }

        let dropCounter = 0;
        let dropInterval = 1000;
        let lastTime = 0;

        function update(time = 0) {
            const deltaTime = time - lastTime;
            lastTime = time;
            dropCounter += deltaTime;
            if (dropCounter &gt; dropInterval) playerDrop();
            draw();
            requestAnimationFrame(update);
        }

        function updateScore() {
            document.getElementById('score-val').innerText = player.score;
            document.getElementById('combo-val').innerText = player.combo;
        }

        const arena = createMatrix(12, 20);
        const player = {
            pos: {x: 0, y: 0},
            matrix: null,
            nextMatrix: null,
            holdMatrix: null,
            canHold: true,
            score: 0,
            combo: 0,
            lastMoveRotate: false,
            tSpin: false,
        };

        document.addEventListener('keydown', event =&gt; {
            // C(67), Shift(16), Space(32), Arrows(37~40)
            if([67, 16, 32, 37, 38, 39, 40].includes(event.keyCode)) {
                event.preventDefault();
                
                if (event.keyCode === 37) playerMove(-1);
                else if (event.keyCode === 39) playerMove(1);
                else if (event.keyCode === 40) playerDrop();
                else if (event.keyCode === 38) playerRotate(1);
                else if (event.keyCode === 32) playerHardDrop();
                else if (event.keyCode === 67 || event.keyCode === 16) playerHold();
            }
        });

        playerReset();
        updateScore();
        update();
    })();
    &lt;/script&gt;
&lt;/div&gt;</description>
      <category>놀이터</category>
      <category>game</category>
      <category>tetris</category>
      <category>게임</category>
      <category>추억</category>
      <category>테트리스</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/570</guid>
      <comments>https://ittrue.tistory.com/570#entry570comment</comments>
      <pubDate>Mon, 5 Jan 2026 09:00:09 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 레코드 클래스 살펴보기 (Record Class)</title>
      <link>https://ittrue.tistory.com/569</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Record Class&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 레코드 클래스는 Java 14부터 프리뷰 상태로 도입되어 Java 16에서 정식으로 지원하게 된 새로운 클래스다. 레코드는 데이터를 담기 위한 클래스를 더욱 간결하고 안전하게 작성하기 위한 기능을 제공한다. 레코드 클래스를 사용하면 기존 POJO 클래스를 작성할 때 필요한 보일러플레이트 코드를 줄이는 효과를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레코드의 특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;final 클래스로 상속이 불가능하다.&lt;/li&gt;
&lt;li&gt;인터페이스 구현이 가능하다.&lt;/li&gt;
&lt;li&gt;모든 필드는 불변(private final)으로 값을 변경할 수 없다.&lt;/li&gt;
&lt;li&gt;getter 메서드를 기본으로 제공한다.&lt;/li&gt;
&lt;li&gt;모든 필드를 포함한 생성자를 기본으로 제공한다.&lt;/li&gt;
&lt;li&gt;toString(), equals(), hashCode() 메서드를 기본으로 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;클래스와 레코드 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 두 예제는 동일한 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717519194012&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class Person {
    private final String name;
    private final int age;
    private final LocalDate birthDate;
    private final String email;

    public Person(String name, int age, LocalDate birthDate, String email) {
        this.name = name;
        this.age = age;
        this.birthDate = birthDate;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person that = (Person) o;
        return getAge() == that.getAge() &amp;amp;&amp;amp;
                Objects.equals(getName(), that.getName()) &amp;amp;&amp;amp;
                Objects.equals(getBirthDate(), that.getBirthDate()) &amp;amp;&amp;amp;
                Objects.equals(getEmail(), that.getEmail());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getName(), getAge(), getBirthDate(), getEmail());
    }

    @Override
    public String toString() {
        return &quot;Person{&quot; +
                &quot;name='&quot; + name + '\'' +
                &quot;, age=&quot; + age +
                &quot;, birthDate=&quot; + birthDate +
                &quot;, email='&quot; + email + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 클래스에서는 모든 필드를 명시적으로 선언하고, 생성자에서 초기화해야 하며, 모든 필드에 대해 getter 메서드를 작성해야 한다. 또한, 기존 클래스에서는 equals와 hashCode, toString 메서드를 직접 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717518985613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record Person(
        String name,
        int age,
        LocalDate birthDate,
        String email
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드 클래스에서는 필드를 간단하게 선언하는 것으로 생성자와 getter, equals, hashCode, toString 메서드를 모두 자동으로 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;기본 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드 클래스는 기존 클래스와 동일하게 커스텀 메서드, 생성자 검증, 정적 변수 및 메서드 등을 작성하여 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;생성자 검증&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720705041279&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record Person(
        String name,
        int age
) {
    public Person {
        if (age &amp;lt; 0) {
            throw new IllegalArgumentException(&quot;나이는 0보다 작을 수 없습니다.&quot;);
        }
    }

    public static void main(String[] args) {
        Person person = new Person(&quot;홍길동&quot;, -1); // 에러 발생
        System.out.println(&quot;person = &quot; + person);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정적 변수와 정적 메서드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720711998066&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record Person(
        String name,
        int age
) {
    public static final int MIN_AGE = 0;
    public static final String DEFAULT_NAME = &quot;홍길동&quot;;

    public static int getMinAge() {
        return MIN_AGE;
    }

    public static String getDefaultName() {
        return DEFAULT_NAME;
    }

    public static void main(String[] args) {
        System.out.println(Person.getMinAge()); // 0
        System.out.println(Person.getDefaultName()); // 홍길동
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커스텀 메서드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720712078344&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record Person(
        String name,
        int age
) {
    public static final int ADULT_AGE = 18;

    public boolean isAdult() {
        return this.age &amp;gt;= ADULT_AGE;
    }

    public static void main(String[] args) {
        Person person = new Person(&quot;홍길동&quot;, 18);
        System.out.println(person.isAdult()); // true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 클래스의 보일러플레이트 코드를 줄여 더욱 간결하고 가독성 높은 코드를 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;불변 객체로 유지보수성이 향상될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드 클래스는 DTO 클래스로 활용하기 좋다. 레코드 클래스에 스프링 프레임워크에서 제공하는 Bean Validation을 사용하여 제약조건을 쉽게 검증할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720712541142&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-validation'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Bean Validation 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720712913907&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record Person(
        @NotBlank
        @Size(min = 2, max = 20, message = &quot;이름은 최소2자 최대20자입니다.&quot;)
        String name,

        @NotBlank
        @Max(value = 150, message = &quot;나이는 150세 이하여야 합니다.&quot;)
        @Min(value = 0, message = &quot;나이는 0세 이상이어야 합니다.&quot;)
        int age,

        @NotNull
        LocalDate birthDate,
        
        @Email(message = &quot;이메일 형식이어야 합니다.&quot;)
        String email
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨트롤러 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720713309151&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@Validated
public class PersonController {

    @PostMapping(&quot;/person&quot;)
    public ResponseEntity&amp;lt;PersonResponse&amp;gt; createPerson(@Valid @RequestBody PersonRequest personRequest) {
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(new PersonResponse(&quot;홍길동&quot;, 20, LocalDate.now(), &quot;gildong@example.com&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코틀린 Data Class와 자바 레코드 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 Record는 코틀린 data class와 같이 데이터 중심의 클래스를 정의하는 데 중점을 둔다. 또한, 공통적으로 간결한 문법과 자동으로 생성되는 메서드를 통해 보일러플레이트를 줄이는 역할을 한다. 하지만, 이 둘에는 약간의 차이점이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바 레코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720713528853&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record Person(
        String name,
        int age,
        LocalDate birthDate,
        String email
) {
    public static final int MINIMUM_AGE = 0;

    public Person {
        if (age &amp;lt; 0) {
            throw new IllegalArgumentException(&quot;나이는 0보다 작을 수 없습니다.&quot;);
        }
    }

    public static int getMinimumAge() {
        return MINIMUM_AGE;
    }

    public boolean isAdult() {
        return age &amp;gt;= 18;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코틀린 데이터 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720713644621&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class Person(
    var name: String,
    var age: Int,
    val birthDate: LocalDate,
    val email: String
) {
    init {
        require(age &amp;gt;= 0) { &quot;나이는 0보다 작을 수 없습니다.&quot; }
    }

    companion object {
        const val MINIMUM_AGE = 0

        fun getMinimumAge() = MINIMUM_AGE
    }

    fun isAdult() = age &amp;gt;= 18
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;차이점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바의 레코드 필드는 기본적으로 final이므로 불변성을 보장하지만, 코틀린의 데이터 클래스는 val 키워드를 통해 불변성을 보장한다. var 키워드를 사용하면 가변 필드가 된다.&lt;/li&gt;
&lt;li&gt;레코드와 데이터 클래스 둘 다 toString(), equals(), hashCode()를 기본으로 자동 생성하지만, 코틀린의 데이터 클래스는 추가적으로 copy(), componentN() 메서드를 생성해 준다.&lt;/li&gt;
&lt;li&gt;자바 레코드는 정적 메서드와 필드를 직접 추가할 수 있지만, 코틀린 데이터 클래스는 companion object를 통해 정적 멤버를 정의할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>언어(Language)/Java</category>
      <category>Class</category>
      <category>DTO</category>
      <category>java</category>
      <category>Record</category>
      <category>레코드</category>
      <category>비교</category>
      <category>사용법</category>
      <category>자바</category>
      <category>클래스</category>
      <category>활용</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/569</guid>
      <comments>https://ittrue.tistory.com/569#entry569comment</comments>
      <pubDate>Fri, 12 Jul 2024 01:16:22 +0900</pubDate>
    </item>
    <item>
      <title>[Algorithm] 배열의 구간 합 알고리즘 (Prefix Sum) - Java</title>
      <link>https://ittrue.tistory.com/567</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구간 합&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구간 합(Prefix Sum)은 주어진 배열의 특정 구간, 즉 배열의 연속된 요소들의 합을 의미하다. 예를 들어, 배열이 [1, 2, 3, 4, 5]라 할 때 2번째 요소부터 4번째 요수까지의 구간 합은 2 + 3 + 4로 9가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 구간 합을 구하기 위해 일반적으로 다음과 같이 반복문을 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1707775727847&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[] arr = {1, 2, 3, 4, 5};
int sum = 0;
for (int i = 1; i &amp;lt;= 3; i++) {
    sum += arr[i];
}
System.out.println(&quot;sum: &quot; + sum); // 9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 2차원 배열의 경우에는 2중 반복문을 통해 구한다.&lt;/p&gt;
&lt;pre id=&quot;code_1717076866838&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1 2 3
4 5 6
7 8 9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1717076958211&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int sum = 0;
for (int i = 0; i &amp;lt;= 1; i++) {
    for (int j = 0; j &amp;lt;= 1; j++) {
        sum += matrix[i][j];
    }
}
System.out.println(&quot;sum: &quot; + sum); // 12&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구간 합 알고리즘&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구간 합 알고리즘은 주어진 배열의 특정 구간의 합을 빠르게 계산하는 알고리즘이다. 전체 배열의 누적 합을 미리 계산하여 배열을 만든 뒤, 임의의 구간 합을 O(1)의 시간 복잡도로 빠르게 계산하는 데 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적합과 구간합을 구하는 방법은 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;누적합&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1717333067955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sum[i] = arr[0] + arr[1] + ... + arr[i]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;누적합의 i번째 요소가 배열의 0번째 요소부터 i번째 합인 것을 활용하여 다음과 같이 쉽게 구할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1717318488849&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[] arr = {1, 2, 3, 4, 5};
int[] sum = new int[arr.length + 1];

for (int i = 1; i &amp;lt;= arr.length; i++) {
    sum[i] = sum[i - 1] + arr[i - 1];
}

System.out.println(&quot;sum = &quot; + Arrays.toString(sum)); // [0, 1, 3, 6, 10, 15]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구간합&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배열의 i부터 j까지의 구간합은 j번째 누적합에서 i 직전의 누적합을 빼면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1717333351998&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;s(i, j) = sum[j] - sum[i - 1]&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1717335371557&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[] arr = {1, 2, 3, 4, 5};
int[] sum = new int[arr.length + 1];

for (int i = 1; i &amp;lt;= arr.length; i++) {
    sum[i] = sum[i - 1] + arr[i - 1];
}

int i = 2; // 2번쨰 부터
int j = 5; // 5번째 까지의 합
int result = sum[j] - sum[i - 1];

System.out.println(&quot;result = &quot; + result); // 14&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2차원 배열의 구간합&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차원 배열에서의 누적합과 구간합을 구하는 방법은 1차원 배열의 구간합 알고리즘을 응용하여 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 1부터 9까지 3x3 배열의 크기인 2차원 배열이 존재할 때 누적합과 구간합은 다음과 같이 구할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1717336799450&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1 2 3
4 5 6
7 8 9&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;누적합&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차원 배열의 누적합은 각 위치를 기준으로 (0, 0)부터 현재 위치까지의 모든 요소의 합이다. 조금 복잡할 수 있으나, i, j 번째의 누적합의 공식은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1717337325431&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sum[i][j] = sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1] + arr[i - 1][j - 1];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 공식에서 sum[i][j]는 배열 arr의 0, 0부터 (i - 1, j - 1)까지의 모든 요소의 합을 나타낸다.&lt;/li&gt;
&lt;li&gt;sum[i][j - 1]은 왼쪽 요소까지의 누적 합이고, sum[i - 1][j]는 바로 위 요소까지의 누적합이다.&lt;/li&gt;
&lt;li&gt;이를 더하면 왼쪽 위 요소(sum[i - 1][j - 1])까지의 누적합이 2번 포함되므로 sum[i - 1][j - 1]을 한 번 빼준다.&lt;/li&gt;
&lt;li&gt;이후 현재 위치의 값을 더하면 현재 위치까지의 누적합을 구할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 공식을 토대로 다음과 같이 코드를 작성할 수 있게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1721317665586&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[][] arr = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
};
int[][] sum = new int[arr.length + 1][arr[0].length + 1];

for (int i = 1; i &amp;lt;= arr.length; i++) {
    for (int j = 1; j &amp;lt;= arr[0].length; j++) {
        sum[i][j] = sum[i][j-1] + sum[i-1][j] - sum[i-1][j-1] + arr[i-1][j-1];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드의 결과는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1721317685988&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;0  0  0  0
0  1  3  6
0  5 12 21
0 12 27 45&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구간합&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 구한 2차원 배열의 누적합을 토대로 구간합을 구하는 공식은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1721317551458&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;result = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1부터 9까지 3x3 크기의 2차원 배열을 기준으로 보면, 2, 2 위치의 누적합인 12(1 + 2 + 4 + 5)를 구한다고 가정해 보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1, 1)부터 (1, 2)까지의 누적합 3과 (1, 1)부터 (2, 1)까지의 누적합 5를 더하면 8이 된다.&lt;/li&gt;
&lt;li&gt;이는 (1, 1)이 2번 더해진 결과이기 때문에 (1, 1)을 한 번 뺀다.&lt;/li&gt;
&lt;li&gt;마지막으로 (2, 2)에 해당하는 5를 더한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 구한 누적합을 통해 (1, 1)부터 (3, 3)까지의 구간합을 구한다면 다음과 같이 구할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1, 1)부터 (3, 3)까지의 누적합에서 바로 위까지의 누적합과 바로 왼쪽의 누적합을 뺀다.&lt;/li&gt;
&lt;li&gt;이는 왼쪽 위 위치까지의 누적 합이 2번 빠지게 되므로 한 번 더한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1717339032518&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int x1 = 1;
int y1 = 1;
int x2 = 3;
int y2 = 3;
int result = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]; // 45&lt;/code&gt;&lt;/pre&gt;</description>
      <category>자료구조 &amp;amp; 알고리즘(Data Structure &amp;amp; Algorithm)/알고리즘(Algorithm)</category>
      <category>Algorithm</category>
      <category>java</category>
      <category>Prefix sum</category>
      <category>구간합</category>
      <category>구현</category>
      <category>누적합</category>
      <category>알고리즘</category>
      <category>자바</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/567</guid>
      <comments>https://ittrue.tistory.com/567#entry567comment</comments>
      <pubDate>Sun, 2 Jun 2024 23:44:54 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 부트 3.2 RestClient 살펴보기</title>
      <link>https://ittrue.tistory.com/568</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RestClient&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestClient는 Spring framework 6.1(Spring boot 3.2)에 새로 추가된 동기식 HTTP Client로 Spring 애플리케이션에서 REST API 호출을 위한 HTTP 요청을 보낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestClient의 등장으로 같은 동기식 HTTP Client인 RestTemplate을 대체하여 사용할 수 있으며, fluent API를 제공하여 현대적인 방식으로 코드를 작성할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RestClient의 등장 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 RestTemplate는 2009년 Spring 3.0에 추가된 상당히 오래된 동기식 HTTP Client이다. 그로 인해 몇 가지 단점들이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RestTemplate는 수많은 메서드가 오버로딩되어 제공하기 때문에 기능을 사용하는데 혼란을 줄 수 있다.&lt;/li&gt;
&lt;li&gt;고전적인 방식인 Template method 패턴을 활용한 클래스로 현대적인 방식과는 거리가 멀다.&lt;/li&gt;
&lt;li&gt;Non-Blocking 환경에서는 적절하지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 등장한 WebClient는 동기식 처리와 비동기식 처리 모두 지원하며, fluent API를 제공한다. 하지만 Spring MVC 환경에서 사용하기 위해 WebFlux를 추가로 의존해야 한다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, Spring MVC 환경에서 현대적인 방식으로 HTTP Client 사용하기 위해 등장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RestClienet 사용하기&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RestClient 생성&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestTemplate와는 달리 static 메서드를 통해 new 생성자를 사용하지 않고 객체를 생성할 수 있다. create() 메서드를 통해 생성하거나 builder를 통해 여러 default 설정을 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716904496142&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// create()
RestClient restClient1 = RestClient.create();

// builder()
RestClient restClient2 = RestClient.builder()
        .baseUrl(&quot;http://localhost:8080&quot;)
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
        .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GET 요청&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/{key1}/{key2}와 같이 path variable 경로에 요청을 보낸다고 가정하면 다음과 같이 요청을 보낼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716992979854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String key1 = &quot;key1&quot;;
Long key2 = 1L;
ResponseEntity&amp;lt;Map&amp;gt; response = restClient.get()
        .uri(&quot;http://localhost:8080/{key1}/{key2}&quot;, key1, key2)
        .retrieve()
        .toEntity(Map.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;query parameter에 값을 담아 요청할 경우에는 uriBuilder를 사용하여 쉽게 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716993195848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String name = &quot;홍길동&quot;;
int age = 20;
ResponseEntity&amp;lt;Map&amp;gt; response = restClient.get()
        .uri(uriBuilder -&amp;gt; uriBuilder.path(&quot;/example&quot;)
                .queryParam(&quot;name&quot;, str)
                .queryParam(&quot;age&quot;, age)
                .build())
        .retrieve()
        .toEntity(Map.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;path variable과 query parameter를 동시에 사용하면 다음과 같이 작성하여 요청할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716993415029&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Long id = 1L;
String name = &quot;홍길동&quot;;
int age = 20;
ResponseEntity&amp;lt;Map&amp;gt; response = restClient.get()
        .uri(uriBuilder -&amp;gt; uriBuilder.path(&quot;/example/{id}&quot;)
                .queryParam(&quot;name&quot;, str)
                .queryParam(&quot;age&quot;, age)
                .build(id))
        .retrieve()
        .toEntity(Map.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseEntity 형태로 받고 싶지 않을 경우에는 body를 통해 객체 형태로 응답받을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716993544333&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Long id = 1L;
String name = &quot;홍길동&quot;;
int age = 20;
Map response = restClient.get()
        .uri(uriBuilder -&amp;gt; uriBuilder.path(&quot;/example/{id}&quot;)
                .queryParam(&quot;name&quot;, str)
                .queryParam(&quot;age&quot;, age)
                .build(id))
        .retrieve()
        .body(Map.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;POST 요청&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름과 나이를 가진 객체를 body에 담아 post 요청을 한다고 가정하면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716993897616&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Person person = new Person(&quot;홍길동&quot;, 20);
Map response = restClient.post()
        .uri(&quot;http://localhost:8080/example&quot;)
        .contentType(MediaType.APPLICATION_JSON)
        .body(person)
        .retrieve()
        .body(Map.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;post 요청도 마찬가지로 ResponseEntity 형태로 응답받기 위해서는 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716994258008&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Person person = new Person(&quot;홍길동&quot;, 20);
ResponseEntity&amp;lt;Map&amp;gt; response = restClient.post()
        .uri(&quot;http://localhost:8080/example&quot;)
        .contentType(MediaType.APPLICATION_JSON)
        .body(person)
        .retrieve()
        .toEntity(Map.class);

System.out.println(&quot;body = &quot; + response.getBody());
System.out.println(&quot;StatusCode = &quot; + response.getStatusCode());
System.out.println(&quot;Headers = &quot; + response.getHeaders());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;PUT&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PUT 요청은 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716994524400&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Long id = 1L;
Person person = new Person(&quot;홍길동&quot;, 20);
Map response = restClient.put()
        .uri(&quot;http://localhost:8080/example/{id}&quot;, id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(person)
        .retrieve()
        .body(Map.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;DELETE&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DELETE 요청 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1716994644709&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Long id = 1L;
ResponseEntity&amp;lt;Void&amp;gt; bodilessEntity = restClient.delete()
        .uri(&quot;http://localhost:8080/example/{id}&quot;, id)
        .retrieve()
        .toBodilessEntity();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;toBodilessEntity()는 responseBody는 없으나, Http StatusCode와 Headers를 가진 ResponseEntity다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예외처리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestClient의 예외처리 방법은 크게 2가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;builder()를 통해 RestClient를 생성할 때 defaultStatusHandler()를 통한 예외처리&lt;/li&gt;
&lt;li&gt;요청 후 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;onStatus()를 통해&lt;span&gt; 응답의 상태코드에 따른 예외처리&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716994787671&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RestClient restClient = RestClient.builder()
        .baseUrl(&quot;http://localhost:8080&quot;)
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
        .defaultStatusHandler(HttpStatusCode::is4xxClientError, (req, res) -&amp;gt; {
            throw new RestClientException(&quot;Client error: &quot; + res.getStatusCode());
        })
        .build();

ResponseEntity&amp;lt;Map&amp;gt; response = restClient.get()
        .uri(uriBuilder -&amp;gt; uriBuilder.path(&quot;/example/{id}&quot;)
                .queryParam(&quot;str&quot;, str)
                .build(id))
        .retrieve()
        .onStatus(HttpStatusCode::is5xxServerError, (req, res) -&amp;gt; {
            throw new RestClientException(&quot;Server error: &quot; + res.getStatusCode());
        })
        .toEntity(Map.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프레임워크(Framework)/Spring</category>
      <category>fluent api</category>
      <category>http client</category>
      <category>REST API</category>
      <category>RestClient</category>
      <category>Spring</category>
      <category>Spring boot</category>
      <category>스프링</category>
      <category>스프링 부트</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/568</guid>
      <comments>https://ittrue.tistory.com/568#entry568comment</comments>
      <pubDate>Thu, 30 May 2024 00:05:37 +0900</pubDate>
    </item>
    <item>
      <title>[Algorithm] 10진수를 n진수로, n진수를 10진수로 변환하기 (진법 변환) - Java</title>
      <link>https://ittrue.tistory.com/566</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;자바의 진법 변환&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진법 변환은 알고리즘 문제에서 자주 활용된다. 자바에서는 이를 쉽게 활용할 수 있도록 함수를 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 제공하는 함수를 통해 진법을 변환하는 방법과 직접 구현하여 변환하는 방법을 알아보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;10진수를 N진수로 변환하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바 내장 함수 사용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10진수의 숫자를 N진수로 변환하기 위해 java.lang 패키지에서 Integer와 Long 클래스의 toString() 메서드를 제공한다.&lt;/li&gt;
&lt;li&gt;첫 번째 인자에는 변환할 수, 두 번째 인자에는 변환할 진법을 넣어 쉽게 변환할 수 있다.&lt;/li&gt;
&lt;li&gt;이 외에도 java.math 패키지의 BigInteger 클래스의 toString() 메서드를 통해 진법 변환을 할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1705334344838&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        int decimal = 1234;

        String binary = Integer.toString(decimal, 2);
        String base4 = Integer.toString(decimal, 4);
        String octal = Integer.toString(decimal, 8);
        String hexa = Integer.toString(decimal, 16);
        String base32 = Integer.toString(decimal, 32);

        System.out.println(binary);
        System.out.println(base4);
        System.out.println(octal);
        System.out.println(hexa);
        System.out.println(base32);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1705334387546&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출력
10011010010
103102
2322
4d2
16i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;직접 구현&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 구현하는 방법에는 다음과 같이 수행할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;10진수 숫자를 변환할 진법으로 나눈 나머지를 구한다.&lt;/li&gt;
&lt;li&gt;나머지를 해당 진수의 자릿수로 사용한다.&lt;/li&gt;
&lt;li&gt;10진수 이상인 경우 나머지가 10 이상인 경우 'a'부터 대응되는 문자로 변환한 후 지수의 자릿수로 사용한다.&lt;/li&gt;
&lt;li&gt;위 과정을 10진수 숫자가 0이 될 때까지 반복한다.&lt;/li&gt;
&lt;li&gt;완성된 수를 역순으로 뒤집는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1705334057758&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        int decimal = 1234;

        String binary = convertDecimalToBase(decimal, 2);
        String base4 = convertDecimalToBase(decimal, 4);
        String octal = convertDecimalToBase(decimal, 8);
        String hexa = convertDecimalToBase(decimal, 16);
        String base32 = convertDecimalToBase(decimal, 32);

        System.out.println(binary);
        System.out.println(base4);
        System.out.println(octal);
        System.out.println(hexa);
        System.out.println(base32);
    }

    private static String convertDecimalToBase(int decimal, int base) {
        StringBuilder sb = new StringBuilder();

        // 10진수 정수가 0이 될 때까지 반복
        while (decimal &amp;gt; 0) {
            // 10진수 정수를 base로 나눈 나머지(n진법으로 변환된 수)
            int temp = decimal % base;

            if (temp &amp;gt;= 10) { // 나머지 값이 10 이상이면, 'a'부터 시작하는 문자로 변환
                sb.append((char) (temp - 10 + 'a'));
            } else { // 나머지 값이 10 미만이면 그대로 사용
                sb.append(temp);
            }

            // 다음 숫자를 구하기 위한 나눗셈
            decimal = decimal / base;
        }

        // 문자열 뒤집기
        return sb.reverse().toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1705334396618&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출력
10011010010
103102
2322
4d2
16i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;N진수를 10진수로 변환하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바 내장 함수 사용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;N진수의 숫자를 10진수로 변환하는 방법은 java.lang 패키지에서 Integer와 Long 클래스의 parseInt() 메서드를 사용한다.&lt;/li&gt;
&lt;li&gt;첫 번째 인자에는 변환할 문자열, 두 번째 인자에는 변환할 문자열의 진법을 넣어 10진수로 변환할 수 있다.&lt;/li&gt;
&lt;li&gt;이 외에도 java.math 패키지의 BigInteger 클래스의 생성자를 통해 진법 변환을 할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1705335500067&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        int binaryToDecimal = Integer.parseInt(&quot;1010110101&quot;, 2);
        int base4ToDecimal = Integer.parseInt(&quot;2101201101&quot;, 4);
        int octalToDecimal = Integer.parseInt(&quot;54210&quot;, 6);
        int hexaToDecimal = Integer.parseInt(&quot;12132&quot;, 8);
        int base32ToDecimal = Integer.parseInt(&quot;abcde&quot;, 16);

        System.out.println(binaryToDecimal);
        System.out.println(base4ToDecimal);
        System.out.println(octalToDecimal);
        System.out.println(hexaToDecimal);
        System.out.println(base32ToDecimal);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1705335516419&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출력
693
596049
7422
5210
703710&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;직접 구현&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;직접 구현하는 방법에는 다음과 같이 수행할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거듭제곱을 위한 수와 결과를 담을 변수를 준비한다.&lt;/li&gt;
&lt;li&gt;변환할 문자열의 각 자릿수를 역순으로 순회한다&lt;/li&gt;
&lt;li&gt;각 자릿수가 '0'과 '9' 사이에 포함되면 해당 숫자로 변환하고, 그보다 크면 대응하는 알파벳으로 변환한다.&lt;/li&gt;
&lt;li&gt;거듭제곱 수와 변환한 수를 곱하여 결과에 더한다.&lt;/li&gt;
&lt;li&gt;거듭제곱 수를 문자열의 진법과 곱하여 업데이트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1705335576933&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        int binaryToDecimal = convertBaseToDecimal(&quot;1010110101&quot;, 2);
        int base4ToDecimal = convertBaseToDecimal(&quot;2101201101&quot;, 4);
        int octalToDecimal = convertBaseToDecimal(&quot;54210&quot;, 6);
        int hexaToDecimal = convertBaseToDecimal(&quot;12132&quot;, 8);
        int base32ToDecimal = convertBaseToDecimal(&quot;abcde&quot;, 16);

        System.out.println(binaryToDecimal);
        System.out.println(base4ToDecimal);
        System.out.println(octalToDecimal);
        System.out.println(hexaToDecimal);
        System.out.println(base32ToDecimal);
    }

    private static int convertBaseToDecimal(String num, int base) {
        int decimal = 0;
        int power = 1; // 거듭제곱을 위한 변수

        // 문자열을 역순으로 순회
        for (int i = num.length() - 1; i &amp;gt;= 0; i--) {
            int digit; // 현재 자릿수의 숫자
            char curr = num.charAt(i); // 현재 자리의 문자

            if (curr &amp;gt;= '0' &amp;amp;&amp;amp; curr &amp;lt;= '9') { // 현재 문자가 숫자라면 해당 숫자로 변환
                digit = curr - '0';
            } else { // 알파벳 문자(A ~ F)이면 10 ~ 15의 값으로 변환
                digit = curr + 10 - 'A';
            }

            // 현재 자릿수의 숫자 거듭제곱수를 곱하여 10진수에 더함
            decimal += digit * power;
            // 거듭제곱수를 업데이트
            power *= base;
        }

        return decimal;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1705335919359&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;693
596049
7422
5210
2940670&lt;/code&gt;&lt;/pre&gt;</description>
      <category>자료구조 &amp;amp; 알고리즘(Data Structure &amp;amp; Algorithm)/알고리즘(Algorithm)</category>
      <category>10진수를 n진수</category>
      <category>Algorithm</category>
      <category>java</category>
      <category>n진수를 10진수</category>
      <category>구현하기</category>
      <category>알고리즘</category>
      <category>자바</category>
      <category>진법 변환</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/566</guid>
      <comments>https://ittrue.tistory.com/566#entry566comment</comments>
      <pubDate>Tue, 16 Jan 2024 01:30:51 +0900</pubDate>
    </item>
    <item>
      <title>[Algorithm] 파스칼의 삼각형 알고리즘 구현하기 (Pascal's triangle) - Java</title>
      <link>https://ittrue.tistory.com/565</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;파스칼 삼각형&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;pascal-triangle2.png&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyaEQT/btsCTWHe7Vb/p5am8mDVp3nRwwbJtwDLpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyaEQT/btsCTWHe7Vb/p5am8mDVp3nRwwbJtwDLpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyaEQT/btsCTWHe7Vb/p5am8mDVp3nRwwbJtwDLpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyaEQT%2FbtsCTWHe7Vb%2Fp5am8mDVp3nRwwbJtwDLpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;365&quot; data-filename=&quot;pascal-triangle2.png&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파스칼의 삼각형은 블레즈 파스칼에 의해 제안된 삼각형으로 구성된 배열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파스칼의 삼각형은 다음과 같은 규칙이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 행의 첫 번째와 마지막의 숫자는 1이다.&lt;/li&gt;
&lt;li&gt;각 행의 중간에 위치한 숫자는 이전 행의 왼쪽 위치에 있는 숫자와 바로 위에 있는 숫자의 합이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 규칙을 통해 파스칼의 삼각형을 2차원 배열로 구현해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1704291774375&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        int n = 7;

        // 파스칼의 삼각형 만들기
        int[][] pascal = new int[n][];
        for (int i = 0; i &amp;lt; n; i++) {
            pascal[i] = new int[i + 1];

            // 규칙 1. 각 행의 첫 번쨰와 마지막 숫자는 1
            pascal[i][0] = 1;
            pascal[i][i] = 1;

            // 규칙 2. 중간에 위치한 숫자는 이전 행의 왼쪽에 있는 숫자와 바로 위에 있는 숫자의 합
            for (int j = 1; j &amp;lt; i; j++) {
                pascal[i][j] = pascal[i - 1][j - 1] + pascal[i - 1][j];
            }
        }

        // 파스칼의 삼각형 출력
        for (int i = 0; i &amp;lt; pascal.length; i++) {
            for (int j = 0; j &amp;lt; pascal[i].length; j++) {
                System.out.print(pascal[i][j] + &quot; &quot;);
            }
            System.out.println();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704291809367&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출력
1 
1 1 
1 2 1 
1 3 3 1 
1 4 6 4 1 
1 5 10 10 5 1 
1 6 15 20 15 6 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>자료구조 &amp;amp; 알고리즘(Data Structure &amp;amp; Algorithm)/알고리즘(Algorithm)</category>
      <category>Algorithm</category>
      <category>java</category>
      <category>Pascal's triangle</category>
      <category>구현</category>
      <category>알고리즘</category>
      <category>자바</category>
      <category>파스칼의 삼각형</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/565</guid>
      <comments>https://ittrue.tistory.com/565#entry565comment</comments>
      <pubDate>Wed, 3 Jan 2024 23:25:12 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 병합 정렬 (Merge Sort) - 정렬 알고리즘 (Sorting Algorithm)</title>
      <link>https://ittrue.tistory.com/564</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;병합 정렬 (Merge Sort)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병합 정렬(Merge Sort)은 효율적이고 안정적인 정렬 알고리즘 중 하나로, 분할 정복(Divide and Conquer) 알고리즘을 따른다. 병합 정렬의 기본 개념은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분할(Divide)&lt;/b&gt; : 입력받은 배열을 절반으로 분할하고, 길이가 1이 될 때까지 분할을 반복한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정복(Conquer)&lt;/b&gt; : 각 분할된 배열에 대해 재귀적으로 정렬을 수행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병합(Merge)&lt;/b&gt; : 분할한 배열을 병합하면서 정렬한다. 두 개의 정렬된 배열을 하나의 정렬된 배열로 합치는 과정으로 병합 과정에서 실제 정렬이 이루어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병합 정렬의 시간 복잡도는 O(n log n)으로 길이가 긴 배열을 안정적으로 정렬할 때 적합한 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;병합 정렬의 장단점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;O(n log n)의 시간 복잡도를 가지고 있어 다른 정렬 알고리즘에 비해 효율적이다.&lt;/li&gt;
&lt;li&gt;동일하 값의 원소가 입력에 따라 순서를 보장하는 안정 정렬(Stable Sort) 알고리즘이다.&lt;/li&gt;
&lt;li&gt;최선과 최악의 경우에도 항상 동일한 시간 복잡도를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본 배열을 분할하여 새로운 배열을 추가적으로 만들기 때문에 추가적인 메모리가 필요하다.&lt;/li&gt;
&lt;li&gt;다른 정렬 알고리즘에 비해 구현하기가 복잡하다.&lt;/li&gt;
&lt;li&gt;배열의 크기가 작은 경우 퀵 정렬과 같은 알고리즘이 더 빠르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병합 정렬은 여러 상황에 따라 적절하게 사용되어야 하는데, 제한된 메모리를 가진 환경에서는 다소 적합하지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;병합 정렬의 과정&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Merge_sort.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6o40t/btsCUyLAzvj/1lyFqu5mmQcURqbI1dOmM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6o40t/btsCUyLAzvj/1lyFqu5mmQcURqbI1dOmM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6o40t/btsCUyLAzvj/1lyFqu5mmQcURqbI1dOmM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6o40t%2FbtsCUyLAzvj%2F1lyFqu5mmQcURqbI1dOmM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;578&quot; data-filename=&quot;Merge_sort.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;구현&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1704039003758&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MergeSort {
    public void sort(int[] arr, int left, int right) {
        if (left == right) {
            return;
        }

        // 중간 인덱스
        int mid = (left + right) / 2;

        // 왼쪽 정렬
        sort(arr, left, mid);
        // 오른쪽 정렬
        sort(arr, mid + 1, right);

        // 배열 합치기
        merge(arr, left, mid, right);
    }

    public void merge(int[] arr, int left, int mid, int right) {
        int n1 = mid - left + 1; // 왼쪽 배열의 길이
        int n2 = right - mid; // 오른쪽 배열의 길이

        // 왼쪽 배열 오른쪽 배열
        int[] leftTemp = new int[n1];
        int[] rightTemp = new int[n2];

        // 왼쪽 배열 담기
        for (int i = 0; i &amp;lt; n1; i++) {
            leftTemp[i] = arr[left + i];
        }
        // 오른쪽 배열 담기
        for (int i = 0; i &amp;lt; n2; i++) {
            rightTemp[i] = arr[mid + 1 + i];
        }

        // 왼쪽 배열과 오른쪽 배열의 인덱스
        int i = 0, j = 0;
        // 원본 배열 arr의 시작 인덱스
        int k = left;

        // 원본 배열에 정렬
        while (i &amp;lt; n1 &amp;amp;&amp;amp; j &amp;lt; n2) {
            if (leftTemp[i] &amp;lt;= rightTemp[j]) {
                arr[k] = leftTemp[i];
                i++;
            } else {
                arr[k] = rightTemp[j];
                j++;
            }
            k++;
        }

        // 남아 있는 요소 담기
        while (i &amp;lt; n1) {
            arr[k] = leftTemp[i];
            i++;
            k++;
        }

        while (j &amp;lt; n2) {
            arr[k] = rightTemp[j];
            j++;
            k++;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704039061355&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        int[] array = {38, 27, 43, 3, 9, 82, 10};
        MergeSort mergeSort = new MergeSort();
        
        mergeSort.sort(array, 0, array.length - 1);

        for (int i : array) {
            System.out.print(i + &quot; &quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1704039127380&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 출력
3 9 10 27 38 43 82&lt;/code&gt;&lt;/pre&gt;</description>
      <category>자료구조 &amp;amp; 알고리즘(Data Structure &amp;amp; Algorithm)/알고리즘(Algorithm)</category>
      <category>Algorithm</category>
      <category>java</category>
      <category>merge</category>
      <category>sort</category>
      <category>병합</category>
      <category>분할</category>
      <category>알고리즘</category>
      <category>자바</category>
      <category>정렬</category>
      <category>정복</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/564</guid>
      <comments>https://ittrue.tistory.com/564#entry564comment</comments>
      <pubDate>Mon, 1 Jan 2024 01:13:14 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 싱글톤 패턴(Singleton Pattern) - 개념 및 예제</title>
      <link>https://ittrue.tistory.com/563</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;싱글톤 패턴(Singleton Pattern)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴은 객체 지향 프로그래밍에서 특정 클래스가 단 하나만의 인스턴스를 생성하여 사용하기 위한 패턴이다. 생성자를 여러 번 호출하더라도 인스턴스가 하나만 존재하도록 보장하여 애플리케이션에서 동일한 객체 인스턴스에 접근할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;싱글톤 패턴을 사용하는 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀, 스레드 풀, 디바이스 설정 객체 등과 같은 경우 인스턴스를 여러 개 만들게 되면 불필요한 자원을 사용하게 되고, 프로그램이 예상치 못한 결과를 낳을 수 있다. 따라서 객체를 필요할 때마다 생성하는 것이 아닌 단 한 번만 생성하여 전역에서 이를 공유하고 사용할 수 있게 하기 위해 싱글톤 패턴을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;싱글톤 패턴의 장단점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유일한 인스턴스&lt;/b&gt; : 싱글톤 패턴이 적용된 클래스의 인스턴스는 애플리케이션 전역에서 단 하나만 존재하도록 보장한다. 이는 객체의 일관된 상태를 유지하고 전역에서 접근 가능하도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 절약&lt;/b&gt; : 인스턴스가 단 하나뿐이므로 메모리를 절약할 수 있다. 생성자를 여러 번 호출하더라도 새로운 인스턴스를 생성하지 않아 메모리 점유 및 해제에 대한 오버헤드를 줄인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연 초기화&lt;/b&gt; : 인스턴스가 실제로 사용되는 시점에 생성하여 초기 비용을 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;결합도 증가&lt;/b&gt; : 싱글톤 패턴은 전역에서 접근을 허용하기 때문에 해당 인스턴스에 의존하는 경우 결합도가 증가할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 복잡성&lt;/b&gt; : 싱글톤 패턴은 단 하나의 인스턴스만을 생성하고 자원을 공유하기 때문에 싱글톤 클래스를 의존하는 클래스는 결합도 증가로 인해 테스트가 어려울 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 관리의 어려움&lt;/b&gt; : 만약, 싱글톤 클래스가 상태를 가지고 있는 경우 전역에서 사용되어 변경될 수 있다. 이로 인해 예상치 못한 동작이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전역에서 접근 가능&lt;/b&gt; : 애플리케이션 내 어디서든 접근이 가능한 경우, 무분별한 사용을 막기 힘들다. 이로 인해 변경에 대한 복잡성이 증가할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴을 사용할 때에는 장단점을 고려하여 상황에 맞게 적절히 사용해야 한다. 단 하나의 인스턴스를 생성하여 메모리 효율성을 높일 수 있지만, 그로 인해 따를 수 있는 문제점들이 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;싱글톤 패턴의 기본 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴을 적용할 경우 두 개 이상의 객체는 존재할 수 없다. 이를 구현하기 위해서는 객체 생성을 위한 new 생성자에 제약을 걸어야 하고, 만들어진 단일 객체를 반환할 수 있는 메서드가 필요하다. 따라서 다음 세 가지 조건이 반드시 충족되어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;new 키워드를 사용할 수 없도록 생성자에 private 접근 제어자를 지정해야 한다.&lt;/li&gt;
&lt;li&gt;유일한 단일 객체를 반환할 수 있는 정적 메서드가 필요하다.&lt;/li&gt;
&lt;li&gt;유일한 단일 객체를 참조할 정적 참조 변수가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 이미지는 싱글톤 패턴의 구조다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;singleton-1.webp&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcx3J6/btsBipDsE9Y/oZR5M3bhDkoJ1Co3PWKAAk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcx3J6/btsBipDsE9Y/oZR5M3bhDkoJ1Co3PWKAAk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcx3J6/btsBipDsE9Y/oZR5M3bhDkoJ1Co3PWKAAk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcx3J6%2FbtsBipDsE9Y%2FoZR5M3bhDkoJ1Co3PWKAAk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;285&quot; data-filename=&quot;singleton-1.webp&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트는 getInstance() 메서드를 통해 싱글톤 인스턴스를 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;getInstance() 메서드 내부에는 instance가 null이면 생성하고, null이 아니면 instance를 반환한다.&lt;/li&gt;
&lt;li&gt;이로써 단 하나만의 객체를 생성하여 사용할 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구현 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Singleton {
    // 정적 참조 변수(싱글톤 객체를 담을 변수)
    private static Singleton singletonObject;

    // private 생성자
    private Singleton() {
    }
    
    // getInstance()
    public static Singleton getInstance() {
        if (singletonObject == null) {
            singletonObject = new Singleton();
        }
        
        return singletonObject;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;singletonObject는 단일 객체를 저장하기 위한 정적 참조 변수다.&lt;/li&gt;
&lt;li&gt;외부에서는 해당 객체를 생성할 수 없도록 생성자를 private로 지정한다.&lt;/li&gt;
&lt;li&gt;getInstance() 메서드를 통해 인스턴스를 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;getInstance()를 최초로 실행할 때에만 초기화한다. (지연 초기화)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;싱글톤 패턴 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Client {
    public static void main(String[] args) {
        // private 생성자(에러 발생)
        // Singleton singleton = new Singleton();

        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        Singleton instance3 = Singleton.getInstance();

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance3);

        System.out.println(instance1 == instance2);
        System.out.println(instance1 == instance3);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 출력
com.book.objects.designpattern.singleton.Singleton@24d46ca6
com.book.objects.designpattern.singleton.Singleton@24d46ca6
com.book.objects.designpattern.singleton.Singleton@24d46ca6
true
true&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글톤 객체를 getInstance()를 통해 여러 변수에서 호출하더라도 같은 인스턴스를 참조한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;싱글톤 패턴의 주의사항&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴은 단일 객체이기 때문에 공유 객체로 사용된다. 따라서 상태 값을 가지지 않는 것이 좋다. 단일 객체가 상태 값을 가지는 경우 특정 참조 변수가 상태를 변경했을 때 다른 참조 변수에도 영향을 미치기 때문이다. 상태 값이 아닌 읽기 전용 속성을 가지거나 또 다른 단일 객체를 참조하는 속성을 가지는 경우에는 문제가 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 경우 멀티 스레드 환경에서 &lt;b&gt;Thread Safe하지 않는 문제점&lt;/b&gt;이 있다. Thread Safe는 여러 스레드가 동시에 접근하는 경우 해당 애플리케이션에 어떠한 문제도 발생하지 않는 것을 의미한다. 즉, 싱글톤 패턴은 여러 스레드가 동시에 접근하는 경우 문제가 발생한다는 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 스레드 A와 B가 존재한다고 가정할 때, 스레드 A가 getInstance() 메서드의 if 검사에 통과하여 싱글톤 객체를 생성하려고 할 때, 스레드 B도 if 검사를 통과할 수 있는 문제가 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static Singleton getInstance() {
    if (singletonObject == null) {  // (2) 스레드 B 진입 가능
        singletonObject = new Singleton();  // (1) 스레드 A 진입 (객체 생성 X)
    }
    
    return singletonObject;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Client {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -&amp;gt; {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        });

        Thread thread2 = new Thread(() -&amp;gt; {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        });

        Thread thread3 = new Thread(() -&amp;gt; {
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton);
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;com.book.objects.designpattern.singleton.Singleton@2e58d931
com.book.objects.designpattern.singleton.Singleton@12985f41
com.book.objects.designpattern.singleton.Singleton@12985f41
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;thread1과 thread2가 동시에 실행되면서 싱글톤 객체가 2번 초기화되어 서로 다른 주소를 참조하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Thread Safe한 싱글톤 패턴 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thread Safe한 싱글톤 패턴을 구현하는 방법에는 여러 가지가 있다. 가장 쉬운 방법부터 시작해서 싱글톤 패턴 구현의 권장하는 방법까지 차례대로 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;sychronized 키워드 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 방법으로 sychronized 키워드를 통해 getInstance() 메서드에 하나의 스레드만 접근 가능하도록 하는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Singleton {
    private static Singleton singletonObject;

    private Singleton() {
    }
    
    public static synchronized Singleton getInstance() {
        if (singletonObject == null) {
            singletonObject = new Singleton();
        }
        
        return singletonObject;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단, 여러 스레드가 getInstance() 메서드를 동시에 접근하려 할 때 동기화 작업(Lock) 때문에 성능 저하가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이른 초기화(eager initialization) 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 싱글톤 객체를 생성하는 비용이 크지 않은 경우 이른 초기화 방법을 적용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Singleton {
    private static final Singleton SINGLETON_OBJECT = new Singleton();

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        return SINGLETON_OBJECT;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변수 선언과 동시에 초기화하여 Thread Safe한 싱글톤 패턴을 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;부가적으로 final 키워드를 붙여 상수로써 사용하면 전역에서 공유하면서 변하지 않는 속성으로 사용한다.&lt;/li&gt;
&lt;li&gt;다만, 미리 생성하는 것 자체가 단점으로 작용할 수 있다.&lt;/li&gt;
&lt;li&gt;애플리케이션 실행과 동시에 Singleton 객체가 인스턴스화하여 메모리를 점유하게 된다.&lt;/li&gt;
&lt;li&gt;만약, 해당 리소스를 사용하지 않는다면 자원 낭비일 뿐이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Double checked locking&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getInstance() 메서드를 사용할 때마다 동기화 작업을 하는 것이 아닌 초기화 할 때만 동기화 작업을 수행하는 방법으로 volatile 키워드와 더블 체크를 통한 synchronized 키워드를 활용하는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Singleton {
    // volatile 키워드 사용
    private static volatile Singleton singletonObject;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singletonObject == null) {
            // if 문 진입 시에만 Singleton 클래스에 대한 동기화 작업 수행
            synchronized (Singleton.class) {
                if (singletonObject == null) {
                    singletonObject = new Singleton();
                }
            }
        }

        return singletonObject;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 방법은 최초 getInstance() 메서드를 호출할 때 두 스레드가 if 문에 진입하더라도, 이후에 진행되는 synchronized 키워드로 인해 두 번째 if 문에서는 lock이 걸려 Thread Safe한 싱글톤 패턴의 구현 방법이다.&lt;/li&gt;
&lt;li&gt;매번 getInstance() 메서드를 호출할 때마다 동기화 작업이 걸리는 것이 아닌 최초에 초기화할 때에만 동기화 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;다만, 이 방법은 volatile 키워드를 사용하기 위해 JVM 버전이 1.5 이상이어야만 한다.&lt;/li&gt;
&lt;li&gt;따라서 JVM에 따라 Thread Safe하지 않을 수 있는 문제점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Bill Pugh Solution 사용 (권장)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private static inner class를 사용하여 Thread Safe한 싱글톤 패턴을 구현하는 방법이다. JVM의 ClassLoader에 의해서 로드될 때 내부적으로 실행되는 synchronized 키워드를 이용하는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton SINGLETON_OBJECT = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.SINGLETON_OBJECT;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Double checked locking보다 단순하면서 JVM 버전에 제약 없이 지연 초기화와 Thread Safe한 싱글톤 패턴을 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;싱글톤 패턴 구현의 권장되는 방법이지만, 이 방법 또한 문제점이 없지 않다.&lt;/li&gt;
&lt;li&gt;자바 리플렉션과 직렬화를 통해 싱글톤이 파괴될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Enum 사용 (권장)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum 클래스는 생성자를 private으로 갖게 만들고, 상수만 갖는 클래스이기 때문에 싱글톤의 성질을 가진다. 이를 이용하여 다음과 같이 싱글톤 객체를 구현할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public enum Singleton {
    SINGLETON_OBJECT
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금까지의 노력이 헛수고처럼 느껴질 수 있게 만드는 코드다.&lt;/li&gt;
&lt;li&gt;단순한 코드로 싱글톤 패턴을 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;다만, Enum 외의 클래스는 상속 불가능한 문제점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;싱글톤 패턴은 객체의 인스턴스가 단 하나만을 생성하도록 만드는 디자인 패턴이다.&lt;/li&gt;
&lt;li&gt;private 생성자를 가져야 하는 특징이 있다.&lt;/li&gt;
&lt;li&gt;단일 객체 참조 변수는 static 이어야 하고 getInstance() 메서드를 참조한다.&lt;/li&gt;
&lt;li&gt;변경 여지가 있는 상태 값을 가지지 않는 것이 좋다.&lt;/li&gt;
&lt;li&gt;멀티 스레드 환경의 경우 Thread Safe한 구현 방법을 고려해야 한다.&lt;/li&gt;
&lt;li&gt;Thread Safe한 싱글톤 패턴은 Bill Pugh Solution 방법과 Enum 사용을 권장하고 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>언어(Language)/Java</category>
      <category>Design Pattern</category>
      <category>java</category>
      <category>OOP</category>
      <category>singleton</category>
      <category>구현</category>
      <category>디자인 패턴</category>
      <category>싱글톤</category>
      <category>자바</category>
      <category>코드</category>
      <category>패턴</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/563</guid>
      <comments>https://ittrue.tistory.com/563#entry563comment</comments>
      <pubDate>Sun, 3 Dec 2023 06:20:36 +0900</pubDate>
    </item>
    <item>
      <title>[Kopring] JPA Auditing - 생성/수정 시각 설정 및 테스트</title>
      <link>https://ittrue.tistory.com/562</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JPA Auditing&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 작성 시 트래킹을 목적으로 엔티티의 생성 시간과 수정 시간 등을 테이블에 저장하여 관리해야 한다. 이를 쉽게 구현하기 위해 Spring Data JPA는 Auditing이라는 엔티티의 변경 내역을 추적하고 기록하는 기능을 제공한다. 이를 통해 엔티티의 생성일자, 수정일자 등을 쉽게 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 JPA Auditing 기능을 사용하지 않고 수동으로 관리하는 경우의 User 엔티티다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
class User(
    ...
) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    ...

    @Column(nullable = false, updatable = false)
    var createdAt: LocalDateTime = LocalDateTime.MIN
        protected set
    
    @Column(nullable = false)
    var lastModifiedAt: LocalDateTime = LocalDateTime.MIN
        protected set

    @PrePersist
    fun prePersist() {
        createdAt = LocalDateTime.now()
        lastModifiedAt = LocalDateTime.now()
    }

    @PreUpdate
    fun preUpdate() {
        lastModifiedAt = LocalDateTime.now()
    }

    ...

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성일자와 수정일자를 엔티티 필드에 작성하여 관리한다.&lt;/li&gt;
&lt;li&gt;생성일자는 @PrePersist 어노테이션을 통해 엔티티가 생성될 때 생성시각을 주입한다.&lt;/li&gt;
&lt;li&gt;수정일자는 @PrePersist, @PreUpdate 어노테이션을 통해 엔티티가 생성/수정될 때 현재시각을 주입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Auditing 기능을 사용하지 않고도 생성/수정 시각을 관리할 수 있지만, 모든 엔티티에 중복 코드를 작성해야 한다. 이를 해결하기 위해 생성/수정 시각을 자동으로 관리하기 위한 코드를 작성해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JPA Auditing 적용하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BaseTimeEntity.kt&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 엔티티의 생성/수정 시각을 추적하고 기록하기 위한 추상 클래스다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity {

    @CreatedDate
    @Column(nullable = false, updatable = false)
    var createdAt: LocalDateTime = LocalDateTime.MIN
        protected set
    
    @LastModifiedDate
    @Column(nullable = false)
    var modifiedAt: LocalDateTime = LocalDateTime.MIN
        protected set
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;@MappedSuperclass&lt;/b&gt; : 엔티티에 공통 매핑 정보를 공유하기 위해 사용하는 어노테이션으로 부모 클래스에 선언하고, 엔티티가 이를 상속하여 필드들을 사용할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@EntityListeners&lt;/b&gt; : 엔티티의 상태 변화 이벤트를 처리하는 리스너를 지정하는 어노테이션이다. AuditingEntityListener를 통해 Auditing 이벤트를 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@CreatedDate, @LastModifiedDate&lt;/b&gt; : Spring Data JPA에서 제공하는 Auditing 어노테이션으로 각각 엔티티의 생성일자와 수정일자를 추적한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MainApplication.kt&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Auditing 기능을 활성화하기 위해 메인 애플리케이션에 @EnableJpaAuditing 어노테이션을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@EnableJpaAuditing
@SpringBootApplication
class MainApplication

fun main(args: Array&amp;lt;String&amp;gt;) {
    runApplication&amp;lt;MainApplication&amp;gt;(*args)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;User.kt&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BaseTimeEntity를 상속받는 것만으로도 처음에 작성한 수동으로 관리하는 User 엔티티와 같은 기능을 하게 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
class User(
    email: String,
    name: String,
    gender: Gender
) : BaseTimeEntity() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    @Column(nullable = false, unique = true, updatable = false)
    var email = email
        protected set

    @Column(nullable = false)
    var name = name
        protected set

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    var gender = gender
        protected set
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Article.kt&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로써 생성일자와 수정일자가 필요한 모든 엔티티에 BaseTimeEntity를 상속하여 적용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
class Article(
    title: String,
    content: String
) : BaseTimeEntity() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    @Column
    var title = title
        protected set

    @Column
    var content = content
        protected set
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테이블 생성 결과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 실행한 후 생성된 테이블을 살펴보면 user 테이블과 article 테이블에 created_at, last_modified_at이 생성된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-20 151103.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZTyhw/btsAFrVHaGG/qm3q7QOXgSzgikyRijNoO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZTyhw/btsAFrVHaGG/qm3q7QOXgSzgikyRijNoO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZTyhw/btsAFrVHaGG/qm3q7QOXgSzgikyRijNoO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZTyhw%2FbtsAFrVHaGG%2Fqm3q7QOXgSzgikyRijNoO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;311&quot; height=&quot;432&quot; data-filename=&quot;스크린샷 2023-11-20 151103.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테스트 코드 작성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 엔티티를 생성 또는 수정할 때 정상적으로 createdAt과 lastModifiedAt이 의도한 대로 동작하는지 확인해 보자. 다음 코드는 kotest로 작성된 코드다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BaseTimeEntityTest(
    @Autowired
    private val userRepository: UserRepository
) : FunSpec({

    test(&quot;User 생성 시 createdAt과 lastModifiedAt이 생성된 시각으로 저장되어야 한다.&quot;) {
        val user = User(
            email = &quot;test@test.com&quot;,
            name = &quot;홍길동&quot;,
            gender = Gender.MAN
        )

        val savedUser = userRepository.save(user)

        savedUser.createdAt shouldBe savedUser.lastModifiedAt
    }

    test(&quot;User를 수정하면 lastModifiedAt이 변경된 시각으로 수정되어야 한다.&quot;) {
        val user = User(
            email = &quot;test2@test.com&quot;,
            name = &quot;홍길동&quot;,
            gender = Gender.MAN
        )
        val savedUser = userRepository.save(user)

        Thread.sleep(1000)
        savedUser.update(&quot;임꺽정&quot;, Gender.MAN)
        val updatedUser = userRepository.save(savedUser)

        updatedUser.createdAt shouldNotBe updatedUser.lastModifiedAt
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;User 엔티티를 생성하면 createdAt과 lastModifiedAt이 생성된 시점으로 설정되어 서로 같은 값을 가지게 된다.&lt;/li&gt;
&lt;li&gt;User 엔티티를 생성한 후 값을 수정하여 다시 저장하면, lastModifiedAt이 수정된 시점으로 변경되어 createdAt과 다른 값을 가지게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-11-20 163552.png&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckGXpd/btsAAPXlmQ9/kZQ1ork51lRUaDFjKwMkbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckGXpd/btsAAPXlmQ9/kZQ1ork51lRUaDFjKwMkbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckGXpd/btsAAPXlmQ9/kZQ1ork51lRUaDFjKwMkbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckGXpd%2FbtsAAPXlmQ9%2FkZQ1ork51lRUaDFjKwMkbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;112&quot; data-filename=&quot;스크린샷 2023-11-20 163552.png&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프레임워크(Framework)/Kopring(Kotlin + Spring)</category>
      <category>Auditing</category>
      <category>BaseTime</category>
      <category>CreatedDate</category>
      <category>JPA</category>
      <category>Kotlin</category>
      <category>LastModifiedDate</category>
      <category>Spring</category>
      <category>Spring Data JPA</category>
      <category>스프링</category>
      <category>코틀린</category>
      <author>잇트루</author>
      <guid isPermaLink="true">https://ittrue.tistory.com/562</guid>
      <comments>https://ittrue.tistory.com/562#entry562comment</comments>
      <pubDate>Tue, 21 Nov 2023 00:19:39 +0900</pubDate>
    </item>
  </channel>
</rss>