<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>codemaker-kim.log</title>
        <link>https://velog.io/</link>
        <description>인생 망하기 전에 시작합니다</description>
        <lastBuildDate>Fri, 17 Apr 2026 06:07:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>codemaker-kim.log</title>
            <url>https://velog.velcdn.com/images/codemaker-kim/profile/5427b7d8-a51f-4901-abc3-957ebfef32c4/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. codemaker-kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/codemaker-kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[모듈러 연산 간단 이해해보기]]></title>
            <link>https://velog.io/@codemaker-kim/%EB%AA%A8%EB%93%88%EB%9F%AC-%EC%97%B0%EC%82%B0-%EA%B0%84%EB%8B%A8-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@codemaker-kim/%EB%AA%A8%EB%93%88%EB%9F%AC-%EC%97%B0%EC%82%B0-%EA%B0%84%EB%8B%A8-%EC%9D%B4%ED%95%B4%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 17 Apr 2026 06:07:41 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>백준 문제(<a href="https://www.acmicpc.net/problem/10844">10844번</a>)를 풀던 중, 자료형 오버플로우로 골치를 썩히다 <del>AI의 힘을 빌려</del> 알게 된 개념을 정리해보려 한다.</p>
<h2 id="개념">개념</h2>
<blockquote>
<p>&quot;나머지 연산&quot;</p>
</blockquote>
<p>어떤 정수를 특정 숫자로 나누었을 때의 나머지를 구하고, 이 나머지를 기준으로 숫자가 순환하는 수학적 체계이다.</p>
<h3 id="표현방법">표현방법</h3>
<p><em>A를 N으로 나누었을 때의 나머지가 R</em></p>
<blockquote>
<p><strong>A mod N = R</strong></p>
</blockquote>
<p>ex) 시계
시계의 숫자는 12를 주기로 숫자가 처음으로 돌아간다.
10시 + 5시간 = 오후 3시 (15 mod 12 = 3)</p>
<h3 id="분배-법칙">분배 법칙</h3>
<p>덧셈, 뺄셈, 곱셈에 대해 분배 법칙도 성립한다.</p>
<ul>
<li>덧셈: (A+B) mod N = ((A mod N) + (B mod N)) mod N</li>
<li>뺄셈: (A-B) mod N = ((A mod N) - (B mod N) + N) mod N</li>
<li>곱셈: (A X B) mod N = ((A mod N) X (B mod N)) mod N </li>
</ul>
<h2 id="문제-내에서의-모듈러-연산">문제 내에서의 모듈러 연산</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/1c052308-9408-43cb-9428-354ceb9d619d/image.png" alt=""></p>
</blockquote>
<p><a href="https://www.acmicpc.net/problem/10844">10844번</a>의 출력 부분이다.
처음에 문제를 풀 때에는 2차원 배열에 저장한 값이 <code>int</code> 자료형 범위를 넘어갈 거라는 생각은 아예 안하고 문제를 풀었고, 그냥 마지막에 <code>1,000,000,000</code>로 나눠서 출력하는 아래와 같은 방법을 사용했고,</p>
<pre><code class="language-java">   System.out.println(
            Arrays.stream(dp[N])
            .sum()
            % MOD
    );</code></pre>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/352d30f2-3fae-4b51-9330-adc42b56ec2b/image.png" alt=""></p>
</blockquote>
<p>결과는 역시 틀렸다고 나왔다.</p>
<p>그래서 2차원 배열 중간중간 값을 채울 때마다 <code>1,000,000,000</code>(MOD)로 나머지 연산을 진행해주었다.</p>
<pre><code class="language-java">        // 모듈러 연산 (오버플로우 방지)
        for (int i = 2; i &lt;= N; i++) {
            for (int j = 0; j &lt; 10; j++) {
                if (j == 0) {
                    dp[i][j] = (dp[i - 1][j + 1]) % MOD;
                } else if (j == 9) {
                    dp[i][j] = (dp[i - 1][j - 1]) % MOD;
                } else {
                    dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j + 1]) % MOD;
                }
            }
        }
</code></pre>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/e8db1026-15af-4a18-9982-b674f14088eb/image.png" alt=""></p>
</blockquote>
<h2 id="결론">결론</h2>
<p>문제를 풀면서 차라리 예외가 발생했다면, &#39;아 이 문제구나&#39; 하고 금방 찾을 수 있겠지만 예외가 발생하지 않으니 문제원인을 깨닫는데 시간이 좀 걸렸다.
자료형 오버플로우에 대한 경각심을 다시금 강하게 가지게 되었다.</p>
<hr>
<p>PS. 2026 4월 28일부로 BOJ 서비스가 종료된다고 한다.
개인적으로 이만큼 문제가 많이 있고, 알고리즘 구분이 잘 된 아카이브는 없었는데.. 알고리즘 문제 풀이를 본격적으로 깊게 파보지는 못했지만, 아무래도 아쉬움이 많이 남는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[똑똑 유지보수 일지 - 인기동아리 조회 속도 개선하기]]></title>
            <link>https://velog.io/@codemaker-kim/%EB%98%91%EB%98%91-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98-%EC%9D%BC%EC%A7%80-%EC%9D%B8%EA%B8%B0%EB%8F%99%EC%95%84%EB%A6%AC-%EC%A1%B0%ED%9A%8C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@codemaker-kim/%EB%98%91%EB%98%91-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98-%EC%9D%BC%EC%A7%80-%EC%9D%B8%EA%B8%B0%EB%8F%99%EC%95%84%EB%A6%AC-%EC%A1%B0%ED%9A%8C-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 31 Mar 2026 12:48:47 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<hr>
<p>최근, 인기 동아리 조회에 대한 체감 상으로 느린 것 같은 기분이 들었다.
다만 체감 상이라는 것 때문에, 이 모호함을 명확하게 하여 해결하면 좋은 경험이 되지않을까하는 생각에 진행하게 되었다.
기록은 아래와 같은 순서로 정리해보았다.</p>
<blockquote>
<ol>
<li>준비</li>
<li>지표 확인</li>
<li>해결 시도 1</li>
<li>해결 시도 2</li>
<li>결론</li>
</ol>
</blockquote>
<h2 id="해결-진행-방식">해결 진행 방식</h2>
<h3 id="준비">준비</h3>
<p>우선 AI에게 프로젝트의 엔티티 구조를 파악하여 스스로 정리해 MD 파일로 출력하게 했다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/8b46caf8-31c4-4443-bde8-32d9f1f6939a/image.png" alt=""></p>
</blockquote>
<p>이를 기반으로 동아리 데이터 약 <strong>110건</strong>, 사용자 <strong>1만 건</strong>, 즐겨찾기 <strong>2.5만 건</strong>의 더미데이터를 삽입하여 로컬 테스트를 진행해보았다.
데이터 삽입은 더미 SQL 파일을 로컬용 마이그레이션 패키지에 넣어 실행했다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/2421d9f3-007b-44c3-9cf0-d59a09379e63/image.png" alt=""></p>
</blockquote>
<hr>
<h3 id="지표-분석">지표 분석</h3>
<p>실행한 API의 속도는 놀랍게도 이랫다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/fc2ced4c-7d4c-4b25-9a1d-092de051d7a8/image.png" alt=""></p>
</blockquote>
<p>분명히 특정한 기준에 맞는 <code>동아리</code>를 조회하는 로직이었기에, 110건의 데이터임에도 불구하고 이 속도는 매우매우 문제가 있었다.
추출한 쿼리 로그는 이랬다.</p>
<pre><code class="language-sql">SELECT
    c1_0.id,
    c1_0.name,
    c1_0.club_type,
    c1_0.club_category,
    c1_0.custom_category,
    c1_0.summary,
    c1_0.profile_img,
    (SELECT COUNT(cm1_0.id) FROM club_members cm1_0 WHERE cm1_0.club_id = c1_0.id) AS member_count,
    COALESCE(af1_0.is_recruiting, false) AS recruiting,
    af1_0.apply_end_date 
FROM clubs c1_0 
LEFT JOIN applyforms af1_0 
    ON af1_0.club_id = c1_0.id AND af1_0.status = &#39;ACTIVE&#39;
WHERE 
    ((((SELECT COUNT(cm2_0.id) FROM club_members cm2_0 WHERE cm2_0.club_id = c1_0.id) * 0.7) + 
      ((SELECT COUNT(f1_0.id) FROM user_favorites f1_0 WHERE f1_0.club_id = c1_0.id) * 2.5)) + 
      (c1_0.view_count * 0.7)) &gt;= 7.0 
ORDER BY 
    ((((SELECT COUNT(cm3_0.id) FROM club_members cm3_0 WHERE cm3_0.club_id = c1_0.id) * 0.7) + 
      ((SELECT COUNT(f2_0.id) FROM user_favorites f2_0 WHERE f2_0.club_id = c1_0.id) * 2.5)) + 
      (c1_0.view_count * 0.7)) DESC, 
    c1_0.id DESC;</code></pre>
<p>확인해보니..카운트 쿼리가 확실히 많았다.
좀 더 확실한 지표와 성능을 확인하기 위해 위 쿼리에 <code>EXPLAIN (ANALYZE, BUFFERS)</code> 를 붙여 결과를 확인해보았다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/599affb4-b34c-485b-850d-d5ef17dc1a80/image.png" alt=""></p>
</blockquote>
<p>플랜 시간은 둘째치고, 쿼리 실행 시간이 너무 오래걸렸다.
병목 부분을 명확하게 확인해보고자 다른 부분을 탐색해보았다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/a4ae8309-dc71-4fea-94b4-440e05ea8f6f/image.png" alt=""></p>
</blockquote>
<p>당장 보이는 문제는 이랬다.</p>
<ol>
<li>지워지는 행이 2.5만 개</li>
<li>1의 과정이 100회 반복</li>
</ol>
<p>어느 부분인지 정확하게 알아보고자 별칭을 기반으로 쿼리를 찾아보았다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/af78f517-27c2-4f06-abd2-e5568fbdea99/image.png" alt=""></p>
</blockquote>
<p>확인 결과, 각 동아리의 즐겨찾기 개수를 카운팅하는 과정이 있는 서브쿼리였다.</p>
<hr>
<h3 id="해결-시도-1">해결 시도 1</h3>
<p>우선 쿼리의 복잡도를 낮추고, 복잡한 서브쿼리들을 <code>JOIN</code>로 풀어내보려했다.</p>
<pre><code class="language-sql">SELECT 
    c.id,
    c.name,
    c.club_type,
    c.club_category,
    c.custom_category,
    c.summary,
    c.profile_img,
    COUNT(DISTINCT cm.id) AS member_count,
    COALESCE(af.is_recruiting, false) AS recruiting,
    af.apply_end_date
FROM clubs c
LEFT JOIN club_members cm ON c.id = cm.club_id
LEFT JOIN user_favorites uf ON c.id = uf.club_id
LEFT JOIN applyforms af ON c.id = af.club_id AND af.status = &#39;ACTIVE&#39;
GROUP BY 
    c.id, 
    c.name, 
    c.club_type, 
    c.club_category, 
    c.custom_category, 
    c.summary, 
    c.profile_img, 
    c.view_count, 
    af.is_recruiting, 
    af.apply_end_date
HAVING 
    (COUNT(DISTINCT cm.id) * 0.7 + COUNT(DISTINCT uf.id) * 2.5 + c.view_count * 0.7) &gt;= 7.0
ORDER BY 
    (COUNT(DISTINCT cm.id) * 0.7 + COUNT(DISTINCT uf.id) * 2.5 + c.view_count * 0.7) DESC,
    c.id DESC;</code></pre>
<p>기존에 있던 서브 쿼리로직을 그대로 가져와 <code>LEFT JOIN</code> 하고 중복을 제거하는 방식을 사용해보았다.
실행 결과는 아래와 같았다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/36f6e5e8-095f-4c9f-a267-47454b0e3975/image.png" alt=""></p>
</blockquote>
<p>말도 안 되게 더 느려지고 말았다..
지표를 좀 더 살펴봤는데, 행의 개수가 정말 심상치 않았다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/c70dd4cb-1cc7-4789-97ca-674fa5dd502b/image.png" alt=""></p>
</blockquote>
<p>무려 406,459 행..
생각해보니 동아리에는 여러 개의 <em>일 대 다</em> 관계의 필드가 있었다.</p>
<ol>
<li>즐겨찾기</li>
<li>부원</li>
<li>지원서</li>
</ol>
<p>메인 테이블에 다수의 카테시안 곱을 고려하지 않고 쿼리문 단순화에만 신경써버린 탓이었다.</p>
<hr>
<h3 id="해결-시도-2">해결 시도 2</h3>
<p>차라리 <code>JOIN</code>하는 테이블의 규모를 줄이는 게 좋겠다고 생각이 들었다.
그래서 중복되어 사용되는 연산 결과들을 CTE로 생성해서 실행해보았다.</p>
<pre><code class="language-sql">WITH mc AS (
    SELECT club_id, COUNT(*) as cnt FROM club_members GROUP BY club_id
),
fc AS (
    SELECT club_id, COUNT(*) as cnt FROM user_favorites GROUP BY club_id
),
af_active AS (
    SELECT club_id, 
           MAX(CASE WHEN is_recruiting = true THEN 1 ELSE 0 END) as is_recruiting,
           MAX(apply_end_date) as apply_end_date
    FROM applyforms 
    WHERE status = &#39;ACTIVE&#39;
    GROUP BY club_id
),
scored_clubs AS (
    SELECT c.id, c.name, c.club_type, c.club_category, c.custom_category, c.summary, c.profile_img, c.view_count, c.created_at,
           COALESCE(mc.cnt, 0) as member_count,
           (COALESCE(mc.cnt, 0) * 0.7 + COALESCE(fc.cnt, 0) * 2.5 + c.view_count * 0.7) AS score,
           COALESCE(af_active.is_recruiting, 0) = 1 as recruiting,
           af_active.apply_end_date as apply_deadline
    FROM clubs c
    LEFT JOIN mc ON c.id = mc.club_id
    LEFT JOIN fc ON c.id = fc.club_id
    LEFT JOIN af_active ON c.id = af_active.club_id
)
SELECT id, name, club_type, club_category, custom_category, summary, profile_img,
       member_count, recruiting, apply_deadline, score
FROM scored_clubs
WHERE score &gt;= 7.0
ORDER BY score DESC, id DESC;</code></pre>
<p><code>EXPLAIN</code>을 통해 확인해본 결과는 이랫다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/4f17cd18-3e6e-4ef9-a8cb-cd6c32b747bd/image.png" alt=""></p>
</blockquote>
<p>초기 쿼리에 비해 훨씬 빨라졌다!
중복된 스칼라 서브쿼리의 성능이 매우 치명적임을 알 수 있었다.
아래는 처음에 캡처했던 즐겨찾기 관련 연산과 동일한 역할을 하게 한 쿼리 부분이다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/ade94cab-d344-489d-a6fb-9d00eecf9de4/image.png" alt=""></p>
</blockquote>
<p>실행 시간은 물론, 반복 횟수도 눈에 띄게 줄어들었다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>우선, 이 조회 코드 자체는 개발 처음에 급하게 짰던 레거시 코드이다.
팀원이 작성한 알고리즘과 뷰에 맞춰서 작동했던 쿼리이기에, 성능 고려는 아예 하지 못한 상태였다.</p>
<p>무엇보다, QueryDsl을 통한 hibernate가 자동으로 생성한 쿼리였기에 단순히 &#39;작동한다&#39;, &#39;긴 쿼리문보다 낫다&#39; 라는 안일한 마인드로 임한 우리 BE 팀의 실책이다.</p>
<p>다만 이번 기회를 통해</p>
<blockquote>
<ol>
<li>모호한 문제를 구체화</li>
<li>명확한 수치를 비교한 개선</li>
</ol>
</blockquote>
<p>이러한 유의미한 경험을 쌓을 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우테코 8기 프리코스] 3주차 회고]]></title>
            <link>https://velog.io/@codemaker-kim/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@codemaker-kim/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 04 Nov 2025 13:32:23 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하기-전">시작하기 전</h2>
<hr>
<p>이번 주 회고에도 시작하기 전에 저번 주에 세웠던 목표들을 확인해보고자 한다.</p>
<p>저번 주차에 세웠던 내 개인 목표는 이렇다.</p>
<blockquote>
<ul>
<li>단위 테스트 작성하기</li>
</ul>
</blockquote>
<ul>
<li>TDD 도입하기</li>
<li>적절한 의존성 주입 활용하기</li>
<li>정적 메서드 사용 줄여보기</li>
</ul>
<h3 id="단위-테스트-작성하기">단위 테스트 작성하기</h3>
<p>-&gt;  살짝 아쉬웠다. 의존성이 없는 모듈들의 단위 테스트는 잘 작성된 것 같았지만, 의존성이 어느 정도 존재하는 클래스들에 대한 단위 테스트의 작성은 미흡했던 것 같다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/3a2770e5-8248-4446-b848-192c53047d3c/image.png" alt="">
(테스트 커버리지 확인을 처음해보는데, 뭔가 잘못 나오고 있는 것 같다. 이렇게까지 꼼꼼하게 테스트를 짜진 않았는데 좀 더 알아봐야겠다..)</p>
<h3 id="tdd-도입하기">TDD 도입하기</h3>
<p>-&gt; 생각보다 나쁘지 않았던 것 같다. 사실 <code>TDD</code> 도입을 진행하면서 모듈 단위 작업을 마치고 한번 겪었던 시행착오가 있는데, 이후 아래에서 후술하겠다.</p>
<p>(<em>추가로 <code>TDD</code> 입문이나 방향 잡기가 어려우신 분들은 이 책을 추천드립니다. 저도 아직 다 못읽었지만 상당히 좋은 내용이 많습니다.</em>)</p>
<blockquote>
<p>최범균 님 - 테스트 주도 개발 시작하기
(<a href="https://product.kyobobook.co.kr/detail/S000001248962">https://product.kyobobook.co.kr/detail/S000001248962</a>)</p>
</blockquote>
<h3 id="적절한-의존성-주입-활용하기">적절한 의존성 주입 활용하기</h3>
<p>-&gt; 저번 주에 비하면 훠어어얼씬 나아졌다. 적절한 수준의 의존성을 도입했다는 생각이 든다. 이것도 아래 설계 부분에서 후술하려 한다.</p>
<h3 id="정적-메서드-사용-줄여보기">정적 메서드 사용 줄여보기</h3>
<p>-&gt; 이번에는 <code>View</code> 역할을 담당하는 클래스를 제외하고 최대한 <code>static</code> 키워드를 사용하지 않았다. 충분히 의미 있는 변화였다!</p>
<h2 id="🚩과제-내용">🚩과제 내용</h2>
<hr>
<p>3주차 과제 내용은 아래와 같았다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/b732df12-9440-44a4-a3c4-379b63c61c5e/image.png" alt=""></p>
<p>추가로 2주차까지의 프로그래밍 요구사항 + 아래 사항이 추가되었다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/6c6a8a94-e376-442d-a22c-679b191ec2bf/image.png" alt=""></p>
<p><code>switch - case</code>문을 제한하는 건 생각보다 꽤 부담되는 조건이었던 것 같다.</p>
<p>확실히, 2주차보다 고민할 사항들이 늘어난 느낌이 들었다.
그리고 제공되는 <code>Lotto</code> 클래스를 사용하라는 조건도 붙었다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/0111252e-d7dd-47d1-9ab5-92e8a244bee7/image.png" alt="">
(사실 이 부분은 오히려 좋았다. 로또 도메인 설계에 대한 고민을 줄일 수 있었다.)</p>
<h2 id="🧐-설계-중-시행착오">🧐 설계 중 시행착오</h2>
<hr>
<p>사실 2주차에 비해서 기능이 많아졌고, 특히</p>
<blockquote>
<p> &#39;에러 발생 시 프로그램 종료가 아닌 그 부분부터 다시 입력받는다.&#39;</p>
</blockquote>
<p>이 부분이 꽤 까다롭게 느껴졌다.</p>
<p>우선 고민하면서 노트에 대략적인 흐름을 끄적여보았다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/865e1954-cd0c-4f8b-aadd-1d5811de2f3f/image.jpg" alt="">
아래는 출력과 입력에 대한 구조를 보고, 대략적인 책임을 나눠보려했다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/6f3ae33c-f97e-4849-a6bf-89792c72be71/image.jpg" alt=""></p>
<p>처음에는 2개로 나누려다가, <code>당첨번호</code> 입력을 한 번 더 나누어 세 파트로 나누었다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/d0feac2b-91eb-476b-a344-1b04ac085e91/image.jpg" alt=""></p>
<p>사실 이렇게 설계했던게, <code>REST API</code> 구조를 생각하고 설계했는데 실제로 구현하게 된 결과물은 생각했던 <code>요청 - 응답</code> 구조랑은 살짝 달라진 것 같다.</p>
<h2 id="🛠️-설계">🛠️ 설계</h2>
<hr>
<p>우선 이번에는 TDD를 적용하면서 각 모듈들 간의 의존성을 최소화하고 기능을 구현했다.
처음에는 모듈 간의 의존성이 없어 <code>README</code> 파일에 작성한 기능 목록과 예외 처리 내역을 바탕으로 실패 및 성공 테스트를 작성했다.</p>
<p>그러다, 최종적으로 로또 애플리케이션의 흐름을 담당하는 기능을 구현할 때 적잖아 당황했다. 내가 설계했던 부분만큼 잘 결합되지가 않아 중간중간 작성했던 모듈들의 기능을 수정하는 상황이 발생했다.</p>
<p>이때는 이미 제출 마감 시간은 약 8시간 남짓 남았었고..
급하게 테스트 코드를 작성할 시간도 없이 바로 기능 작성 및 구현을 하고 제출했다.
최종적으로 TDD를 반쯤 지킨 꼴이 되었다. 사실 조금 아쉬운 실패였다. </p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/fbe0457f-bed8-4299-8f34-3d57b3c2a02e/image.png" alt="">
(다행히 예제 테스트는 통과하긴 한..)</p>
<p>각 입력에 따라
로또 생성 - <code>LottoHandler</code>
당첨번호 생성 - <code>WinningNumberHandler</code>
보너스 번호 생성 - <code>BonusNumberHandler</code></p>
<p>위 클래스들을 기반으로 <code>Validator</code> 클래스와 <code>Generator</code> 클래스들을 의존성 주입하여 각 로직들을 구현했다.</p>
<p>각 입력 및 출력은 view 패키지 내부 클래스에 책임을 분배하고,</p>
<p>클래스마다 자주 이용되는 상수는 <code>LottoConstant</code>로 분리, 상금 관련된 상수는 <code>WinningPrize</code> 열거형으로 분리했다.</p>
<h2 id="😎-미션-종료-후">😎 미션 종료 후</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/96e3aa3d-1c26-46d9-a40f-4ebc1d9b5b24/image.png" alt=""></p>
<p>이번 주차 공통 피드백이다. 확실히 TDD + 단위테스트 시도 자체는 새로운 도전이라 쉽지 않았는데, 그나마 위안이 되는 말씀이었다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/6f83e347-33ca-4582-84c8-b397a2f60f44/image.png" alt=""></p>
<p>직접 구현을 진행하면서 느낀 건, <strong>메서드 15줄은 생각보다 길었다.</strong>
무난하게 이를 지킬 수 있었다.</p>
<p>다만 </p>
<blockquote>
<p>&#39;예외 상황에 대해 고민한다&#39;</p>
</blockquote>
<p>이 피드백도 1주차의 내 모습을 다시 돌아보게 했다. 설계 중 놓친 예외가 없었는지 깊게 고민하던 때가 잠시 떠올랐고, 무의미한 시간은 아니었구나 라는 안도감이 들었다.</p>
<p>그리고.. 아래는 가장 인상 깊었던 부분이다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/9ddd22b1-822d-492f-b0ef-140c6172e9fc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/c19dca5a-f4b6-4800-95f1-b6d65c1aa1d8/image.png" alt=""></p>
<p>사실 시간이 없어서라는 핑계를 댓지만, 저런식으로 메시지를 반환하는 메서드가 있음에도 불구하고 급하게 <code>Getter</code> 메서드를 구현했던 내 자신을 반성하게 되었다.</p>
<pre><code class="language-java">public class WinningNumber {

    private final List&lt;Integer&gt; numbers;

    public WinningNumber(List&lt;Integer&gt; numbers) {
        validate(numbers);
        this.numbers = numbers;
    }

    // ~~~기능 중략~~~~

    public boolean contains(int number) {
        return numbers.contains(number);
    }

    public List&lt;Integer&gt; getNumbers() {
        return List.copyOf(numbers);
    }
 }</code></pre>
<p>위쪽 <code>contains()</code> 메서드 같은 좋은 구조를 구현했음에도 안일하게 Getter 메서드를 작성한 게 떠올랐다.
(그래도 불변 리스트로 반환하겠다고 저렇게 구현한 게 웃기긴 하다.)</p>
<p>아래는 공유받은 링크이다.</p>
<blockquote>
<p><em>getter를 사용하는 대신 객체에 메시지를 보내자</em>
<a href="https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/">https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/6b8393c8-76c4-4059-87e1-c6c97f7fa9f2/image.png" alt=""></p>
<p>나중에 TDD가 깨진 가장 큰 원인이었던 것 같기도 하다.
<del>(매우매우 찔리는 나)</del></p>
<p>아래 부분도 정말 인상 깊었다.
사실 <code>private</code> 메서드가 많을 수록 분리가 필요한 시점이라는 생각도 다시금 들었다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/5bd626e1-5254-4f48-944a-42fdad5c5991/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/e1ffd96d-c348-4898-bd5d-5bdb58307c51/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/eca32ac1-0347-49fc-a448-db4a3a139e39/image.png" alt=""></p>
<blockquote>
<p><em>메서드 시그니처를 수정하여 테스트하기 좋은 메서드로 만들기</em>
<a href="https://tecoble.techcourse.co.kr/post/2020-05-07-appropriate_method_for_test_by_parameter/">https://tecoble.techcourse.co.kr/post/2020-05-07-appropriate_method_for_test_by_parameter/</a></p>
</blockquote>
<h2 id="👀-느낀-점">👀 느낀 점</h2>
<hr>
<p>이번에 가장 크게 느꼈던 건 조금씩 <code>TDD</code>에 대한 감이 잡히기 시작했던 것 같다. 아직 많이 경험해본 것은 아니지만.. 잘 깎는다면 충분히 나와 잘 맞을 것 같다는 생각이 강하게 들었다.</p>
<p>3주차 과제를 진행하면서, 가장 아쉬웠던 부분이 <code>TDD</code>를 끝까지 지켜내지 못했다는 부분이 아무래도 이번 주차에 가장 기억에 남을 것 같다.</p>
<p>사실상 클래스 분리가 좀 더 잘게 나눠졌다면, 좀 더 쉽게 구현에 성공했을 것 같다는 생각도 들었다.</p>
<p><del>저번 주차의 내가 했던 생각인데 여전히 그대로인 것 같기도 하다.</del></p>
<h2 id="✒️-다음-주-목표">✒️ 다음 주 목표</h2>
<hr>
<p>이 글을 적는 시점은, 이미 <strong>오픈 미션</strong>이 공개되고 난 후의 시점이다.</p>
<p>사실은 지금 이런 기분이다.</p>
<p>&#39;어떤 걸 해서 미션에 내고, 나 스스로가 몰입할 수 있을 지?&#39;</p>
<p>조금 큰 수영장 중앙에 던져진 기분이다.</p>
<p>일단 모르겠으니, 다음 주 목표는 </p>
<blockquote>
<ul>
<li><strong>저번주보다 더 나은 내가 되기</strong></li>
</ul>
</blockquote>
<ul>
<li><strong>이전보다 더 깊이 몰입해보기</strong></li>
<li>미션으로 진행해볼 거리 찾아보기</li>
</ul>
<p>이렇게 정했다.
사실 단순히 문제만 푸는 것보단, 정해진 답이 없는 이번 미션은 어쩌면 나한테는 또 한 번의 큰 기회일지도 모르겠다는 생각이 들었다.</p>
<p>이번 미션이 마지막인 만큼, 몰입과 나 스스로에 대한 성장에 좀 더 집중할 수 있는 시간이 되었으면 좋겠다 💪💪</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우테코 8기 프리코스] 2주차 회고]]></title>
            <link>https://velog.io/@codemaker-kim/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@codemaker-kim/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 28 Oct 2025 07:24:07 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하며">시작하며</h2>
<hr>
<p>1주차 프리코스 과제가 끝나고, 개인적으로 세웠던 목표를 이번 주차 회고 전에 되짚어보려 한다.</p>
<p>세웠던 목표는 아래와 같다.</p>
<blockquote>
<ul>
<li>MVC 패턴 도입해보기</li>
</ul>
</blockquote>
<ul>
<li>커밋 단위 더 잘게 쪼개기</li>
<li>커밋 메시지 상세히 작성하기</li>
<li>각 계층 간 분리 확실히 해보기</li>
<li>추상화 적극적으로 활용해보기</li>
</ul>
<h3 id="mvc-패턴-도입해보기">MVC 패턴 도입해보기</h3>
<p>-&gt; 사실 구조에 대한 간단한 이해와 아래 두 영상을 보고 구조를 작성하고, 구현을 진행했다.</p>
<blockquote>
<p><em>[10분 테코톡 - 제리님의 MVC 패턴]</em>
<a href="https://www.youtube.com/watch?v=ogaXW6KPc8I">https://www.youtube.com/watch?v=ogaXW6KPc8I</a></p>
</blockquote>
<blockquote>
<p><em>[10분 테코톡 - 도기님의 MVC 패턴]</em>
<a href="https://www.youtube.com/watch?v=Yzx-z6kCD2A">https://www.youtube.com/watch?v=Yzx-z6kCD2A</a></p>
</blockquote>
<p>구현에서는 특히 제리 님의 영상이 큰 도움이 되었던 것 같다. 
(MVC 패턴의 구현에 중점을 두고 설명하셔서 바로 코드에 적용해가며 하기 좋았습니다.)
다만, 중간 중간에 컨트롤러의 책임이 커지는 등의 아쉬운 부분이 좀 있었다. 아래에서 더 상세하게 적어보겠다.</p>
<h3 id="커밋-단위-더-잘게-쪼개기">커밋 단위 더 잘게 쪼개기</h3>
<p>-&gt; 나름 성공적? 인듯 하다. 저번 주 미션에는 잘 몰라서 사용하지 못했던 깃 커밋 메시지에 <code>scope</code>를 추가하고, 기능별로 커밋 단위를 나눠 진행했다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/33c099e2-72cb-4c97-bedf-0fe47d98fcfc/image.png" alt=""></p>
<h3 id="커밋-메시지-상세히-작성하기">커밋 메시지 상세히 작성하기</h3>
<p>-&gt; 아쉬웠다. 커밋 메시지 바디 작성이 소홀했던 것 같다. 전체적으로 바디에 디테일을 적기보다는 메시지 한 문장에 모든 의미를 담으려 했던 것 같다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/55b985ef-9b12-4592-b86b-e766fbc38e96/image.png" alt="">
(이제보니 조금 모호한 듯한 커밋 메시지)</p>
<h3 id="각-계층-간-분리-확실히-해보기">각 계층 간 분리 확실히 해보기</h3>
<p>-&gt; 아쉬웠다. 이것도 설계와 관련된 부분이기에 아래에 작성하겠다.</p>
<h3 id="추상화-적극적으로-활용해보기">추상화 적극적으로 활용해보기</h3>
<p>-&gt; 나름 괜찮게 사용한 듯 하다.
승리자 판별 로직과 자동차 도메인을 추상화하여, 확장성을 고려해보았다.
조금 아쉬웠던 점은 MVC 구조에 대한 이해 부족인지, 개인적으로는 적극적으로 사용하진 못한 것 같다.</p>
<h2 id="🚩과제-내용">🚩과제 내용</h2>
<hr>
<p>2주차 과제 내용은 아래와 같았다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/d89cad2e-df8c-45a2-99ba-e7fc33787625/image.png" alt=""></p>
<p>그리고, 저번 주차와는 다르게 추가적인 프로그래밍 요구사항이 생겼다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/0dc542ab-1a7f-4fd4-a90e-66ee5bc86bbb/image.png" alt=""></p>
<p>큰 제약사항은 아니라서, 구현에 어려움은 없었던 것 같다.
그나마 가장 신경 쓰이는 부분은 여기였다.</p>
<blockquote>
<p><strong>함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.</strong></p>
</blockquote>
<p>이 부분을 고려해서, 최대한 메서드를 기능별로 쪼개서 구현하려 노력했다.</p>
<h2 id="🛠️-설계">🛠️ 설계</h2>
<hr>
<p>처음에는, 정말 순수하게 <code>Model</code>, <code>View</code>, <code>Controller</code> 만을 기반으로 구현을 진행해보려 했다. 
<img src="https://velog.velcdn.com/images/codemaker-kim/post/a38f25bb-92c4-4319-8dde-ae30a5804228/image.png" alt="">
(처음에 생각했던 MVC 구조)</p>
<p>하지만 이렇게 설계하면서 자동차 경주 컨트롤러의 책임이 너무 커졌다.</p>
<p>그렇게 약간의 고민을 거쳐서 이런 구조로 변경했다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/4aff56e5-c27b-41f0-85b4-a9c157d26fc0/image.png" alt=""></p>
<p>자동차 입력을 처리하는 컨트롤러와 시도횟수, 승리자 판별 로직을 담당하는 컨트롤러로 분리했다.</p>
<p>사실 추상화는 요구사항에는 없었지만, OCP를 고려해봤을 때 자동차 도메인을 추상화하는 것까지는 괜찮은 선택이었던 것 같다.</p>
<p>자동차 도메인의 추상화는 괜찮았지만, 그 구현체인 <code>RacingCar</code> 구현체를 보았을 때, 추상 메서드를 오버라이딩한 <code>accelerate()</code>의 구현 방식이 좋은 방식은 아니었던 것 같다.</p>
<pre><code class="language-java">package racingcar.model;

import camp.nextstep.edu.missionutils.Randoms;

public class RacingCar extends Car {
    private static final int RANDOM_MIN = 0;
    private static final int RANDOM_MAX = 9;
    private static final int ACCELERATION_THRESHOLD = 4;

    public RacingCar(String name, int position) {
        super(name, position);
    }

    @Override
    public void accelerate() {
        if (Randoms.pickNumberInRange(RANDOM_MIN, RANDOM_MAX) &gt;= ACCELERATION_THRESHOLD) {
            position++;
        }
    }
}</code></pre>
<p>처음에 오버라이딩을 진행했을 때에는 이것도 하나의 전진 방식이니 이렇게 구현해도 괜찮겠지? 라는 생각이었는데,</p>
<p>제공받은 메서드도 라이브러리였기에 결국 도메인이 라이브러리에 의존해버리는 상황이 되버리고 말았다.</p>
<p>그리고 승리자 판별 로직을 단순히 인터페이스로 분리했다고 해서 유의미한 추상화가 되었는지는 이제와서 보니 오히려 그렇지 않은 것 같다고 느꼈다.</p>
<p>가장 어려웠던 부분은 역시 책임 할당인 것 같다.
컨트롤러에 자꾸만 큰 책임이 쏠렸고 이를 어디까지 허용할 것인지도 큰 고민이었다.</p>
<p>마지막으로 정적 메서드 남용이었다.
계속해서 정적 메서드로 일괄 처리하는 부분이 많다보니, 테스트하기 어려운 코드가 만들어졌고
결국 통합 테스트를 진행하는 테스트 코드 밖에 남지 않게 되었다.</p>
<p>사실 의도했던 바는 클래스에 다른 클래스에 대한 의존성(클래스 변수)을 줄이고자 했던 것인데..결과적으로는 테스트하기 어려운 코드가 되었던 것 같다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/5eeafeb4-d5dc-4fcf-8f3a-40e14089d141/image.png" alt="">
이번에도 크게 복잡한 예외 케이스는 없었던 것 같다..!</p>
<h2 id="😎-미션-종료-후">😎 미션 종료 후</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/ca43b4c6-d4e4-429c-a707-24e99790d045/image.png" alt=""></p>
<p>미션 종료 후 BE 파트 지원자들에게 온 2주차 공통 피드백이다. 
(사실 앞부분 내용이 형식적인 조언/응원일지라도 큰 힘이 되었던 것 같습니다.)</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/eeea7129-7227-4498-b044-e65a437ee33f/image.png" alt=""></p>
<p>&#39;살아있는 문서&#39; 라는 문구가 참 와닿았다.
<del>싸늘하게 식어 죽어있던</del> 내 <code>README</code> 파일을 다시 보게되는 문구였다.
다른 지원자 분들의 커밋을 잠깐씩 볼때마다 <code>README</code> 문서를 수정하는 모습을 볼 수 있었는데, </p>
<blockquote>
<p><em>처음에 다 작성하고 이에 맞춰서 구현하는게 맞지 않나?</em></p>
</blockquote>
<p>라는 고정관념이 강하게 박혀있던 것 같다.
이 생각을 환기할 수 있는 피드백이었다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/ef3f9c93-c790-4288-b2b5-3031b85628d9/image.png" alt=""></p>
<p>이 피드백 또한 좀 인상 깊었다.
항상 상수, 클래스 변수, 인스턴스 변수 순서를 어떻게 할까 고민했던 상황이 많았는데 확실한 답이 되었다.
그리고..</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/1966a674-ce02-4ee0-87f5-ff2825a2ad49/image.png" alt=""></p>
<p>그냥 나 그 자체였다.
테스트 코드 대부분이 통합 테스트였던 점에서, 어느 시점부터 작성하던 테스트 코드가 통합 테스트 코드가 되는 상황이 많았던 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/9885c0e9-d903-4642-be25-90d4086abbf6/image.png" alt="">
이번 주차 피드백에서 받은 작은 과제이다. 조금 고민해보면서 스스로 정리해보려고 한다!</p>
<h2 id="👀-느낀-점">👀 느낀 점</h2>
<p>이번에는 MVC 패턴을 적용하면서, 계속 들었던 생각이 </p>
<blockquote>
<p>&#39;이게 정말 MVC 패턴을 잘 적용하고 있는 걸까?&#39;</p>
</blockquote>
<p>이다. 물론 구현에 완벽한 정답은 없었고, 이런 궁금증 덕에 위에서 언급했던 테코톡 영상이나 이론 등을 찾아보면서 좀 더 구현할 때 구조에 대해 깊이 고민해볼 수 있던 시간이었던 것 같다.</p>
<p>사실 나 스스로와의 약속을 지키지 못할까 두려워 저번주 목표에 <code>TDD 도입하기</code> 를 적지 못했다.</p>
<p>그래서 이번 주차 과제에서 조금씩 시도를 해보았는데 결국 통합 테스트만 먼저 작성하고 작업하는 꼴이 되어버렸다. <strong>의식하고 문제를 단위 별로 쪼개보는 연습이 필요하다고 생각되었다.</strong></p>
<p>마지막으로, 생각보다 도메인 로직이 다양했던 것 같다.
관점에 따라서 <code>거리</code> 를 도메인으로 둔다던지, 자동차가 아닌 <code>참가자</code> 의 관점으로 본다던지, 
다양한 관점은 내가 생각하던 구조에서 벗어나 다른 구조를 고민해보는 시간이 되었다.</p>
<h2 id="✒️-다음-주-목표">✒️ 다음 주 목표</h2>
<blockquote>
<ul>
<li>단위 테스트 작성하기 (문제를 더 작게 쪼개서)</li>
</ul>
</blockquote>
<ul>
<li>TDD 도입해보기 (완벽하진 않아도 계속 시도해보자!)</li>
<li>적절한 의존성 주입 활용하기</li>
<li>정적 메서드 사용 줄여보기</li>
</ul>
<p>추가로..
<img src="https://velog.velcdn.com/images/codemaker-kim/post/90449160-94fc-4d37-96d2-edd0881e1523/image.png" alt=""></p>
<p>커뮤니티를 보면 정말 잘하는 분들이 많다고 느꼈고 무의식 중에 다른 지원자분들이랑 나를 비교했던 것 같다.</p>
<p>남은 프리코스 기간동안, 나 스스로의 성장에 좀 더 집중하게 되는 시간이 되었으면 좋겠다😀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[우테코 8기 프리코스] 1주차 회고]]></title>
            <link>https://velog.io/@codemaker-kim/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@codemaker-kim/%EC%9A%B0%ED%85%8C%EC%BD%94-8%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 21 Oct 2025 13:35:19 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<hr>
<p>사실 지원하기 전부터, &#39;나 따위가 본 코스 붙을 수 있으려나&#39; 같은 김칫국 드링킹을 하며 지원할지 고민하다가 한 친구에게서 들었던 말이 떠올랐다.</p>
<blockquote>
<p>&quot;프리코스는 꼭 테크코스에 붙을 목적은 아니더라도 한 번쯤 경험해보는 건 나쁘지 않다&quot;</p>
</blockquote>
<p>그래서 무작정 신청을 넣었다.
이번 8기 지원에는 프리코스 비중이 꽤 큰 것 같다.
(<del>자기소개서를 대충 썻다는 내용</del>)</p>
<p>아무튼 각설하고 1주차동안 고민하고, 느꼈던 점을 기록하고자 이 글을 남긴다.</p>
<h2 id="🚩과제-내용">🚩과제 내용</h2>
<hr>
<p>1주차 과제 내용은 아래와 같았다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/c582ca10-85f2-49e6-a071-bb77c63a5bfc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/6a82f898-3a51-478c-8422-ef85b066bb18/image.png" alt="">
확인해보니, 저번 기수 프리코스 문제와 똑같은 것 같았다. (정확하진 않다)</p>
<p>사실, 문제만 보면 간단한 콘솔 애플리케이션 구현이었다.
다만 </p>
<blockquote>
<p><strong>&#39;기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.&#39;</strong></p>
</blockquote>
<p>이 문구가 나에게 있어서는 정말 큰 부담이었다.</p>
<p>고려해야 될 사항이 끊임없이 늘어나는 기분이었다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/1bd3778a-5af8-45bf-a7d7-89b3d82d079f/image.png" alt=""></p>
<p>이런 식으로, 좀 더 세세한 로직과 예외들을 정리했다.
물론 작성하면서, 이것도 좀 예외케이스가 적거나 어색하다고 느꼈다.</p>
<p>그래서 그냥 프리코스 외의 친구들에게서 영감을 얻을 수도 있지 않을까해서 그냥 물어도 보았다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/fd7225da-9215-4c35-8579-a4a7d3421f29/image.png" alt="">
(기존에 존재하는 구분자와 겹치는 커스텀 구분자 입력 시를 고려하는 중이었다.)
<img src="https://velog.velcdn.com/images/codemaker-kim/post/2cd502e4-14b0-41dd-9d99-e2f024de609a/image.png" alt=""></p>
<p>(사실 기대도 안했다)</p>
<p>그렇게 이 예외 케이스 찾기에 혈안이 되어있다가 </p>
<blockquote>
<p>&#39;혹시 모를 입력에 대한 예외 케이스 커버가 되어 있어야 해&#39;</p>
</blockquote>
<p>라는 스스로의 모습을 발견했다.
뭐랄까, 억지로 처리해야하는 예외를 찾는 듯한 기분이었다.
이 생각이 들자마자, 현재 존재하던 예외 케이스 그대로 작업을 시작했다.</p>
<p>더 이상 예외 케이스를 찾아봐도 완벽한 예외 처리는 불가능할 것이고, 케이스가 많아질 수록 구현에 어려움이 커질 것 같았기에 해두었던 설계를 기반으로 바로 작업을 시작했다.</p>
<h2 id="🛠️설계">🛠️설계</h2>
<hr>
<p>일단, 들어오는 입력을 <code>구분자</code> 파트와 <code>계산식</code> 파트로 나누어 생각했다.
두 변수의 처리 방식이 다르고, 이는 하나의 클래스에서 담당하기엔 책임이 무겁다고 느꼈다.</p>
<p>그래서 입력에 대한 최소한의 검증(<code>NULL</code> 값 및 빈 입력 검증) 만 하고, 
아래 4개와 같이 큰 역할을 하는 클래스를 나누었다.</p>
<ul>
<li><code>구분자</code> 파트와 <code>계산식</code> 파트로 나누는 역할을 가진 클래스</li>
<li><code>구분자</code> 파트에서 커스텀 구분자를 분리해내는 클래스</li>
<li><code>계산식</code> 파트에서 계산해야 할 숫자 목록으로 변환하는 클래스</li>
<li>최종 연산을 진행하는 <code>계산기</code> 클래스</li>
</ul>
<p>그리고 검증 연산을 거치는 클래스는 정적 메서드를 기반으로 구성했다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/684bb181-c113-430b-b797-ecf09ddc280a/image.png" alt=""></p>
<p>테스트 케이스가 많지 않은 것을 보아 복잡한 예제 케이스는 없었던 것 같다.!</p>
<h2 id="👀느낀-점">👀느낀 점</h2>
<hr>
<p>우선, 처음에 작업할 때 기획한 기능을 모두 완성하고 그 후에 리팩토링 진행을 생각하고 있었다. 그렇게 순수하게 &#39;구현&#39;만 진행하니 메인 함수에는 메서드 분리조차 안 된 꼬여버린 복잡한 로직이 쌓였고, 메서드 한 번 분리하는데에도 많은 시간을 투자해야 하는 상황이 되었다.</p>
<p>또, 아예 모든 기능을 완료하고 커밋을 하는 등 스스로 커밋 단위가 너무 크다고 느꼈다.</p>
<p>마지막으로, 제일 아쉬운 부분은 정적 메서드를 사용한 클래스들인데 너무 많은 정적 메서드 이용이 눈에 띄게 보였다.</p>
<p>검증 클래스는 그렇다치고, 구분자와 계산식 파트를 분리하는 로직 등 마저 정적 메서드로 처리했다.
물론 코드는 쉽게 짤 수 있었지만, 스스로 좋은 코드라고는 도저히 생각할 수가 없었다.</p>
<p>이번 과제에서는</p>
<blockquote>
<p>&#39;리팩토링의 무게를 줄이자!&#39; (한번에 하는 작업량을 줄이자)</p>
</blockquote>
<p>라는 교훈을 얻었다.</p>
<h2 id="✒️다음-주차-목표">✒️다음 주차 목표</h2>
<hr>
<ul>
<li>MVC 패턴 도입해보기
  -&gt; 우테코 커뮤니티 내에서 MVC 패턴으로 구현하신 분들이 꽤 많았다.</li>
<li>커밋 단위 더 잘게 쪼개기</li>
<li>커밋 메시지 상세히 작성하기</li>
<li>각 계층 간 분리를 확실히 해보기</li>
<li>추상화 적극적으로 활용해보기</li>
</ul>
<p>다음 주차 과제에서는 좀 더 나은 결과물을 낼 수 있으면 좋겠다..!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[똑똑 유지보수 일지 - 무중단 DB 스키마 변경하기]]></title>
            <link>https://velog.io/@codemaker-kim/%EB%98%91%EB%98%91-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98-%EC%9D%BC%EC%A7%80-%EB%AC%B4%EC%A4%91%EB%8B%A8-DB-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@codemaker-kim/%EB%98%91%EB%98%91-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98-%EC%9D%BC%EC%A7%80-%EB%AC%B4%EC%A4%91%EB%8B%A8-DB-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 12 Oct 2025 14:49:40 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<hr>
<blockquote>
</blockquote>
<p>&quot;이거 임원이랑 부원 초기 추가할 때, 회원이 아니어도 가능하게 해주세요&quot;</p>
<p>똑똑 프로젝트 유지보수를 진행하며 받은 요청이다.</p>
<p>사실, 초기에 개발 진행하면서도 받았던 요청인데 </p>
<p>그때는 릴리즈가 얼마 남지 않은 상황이었고,
거의 다 완성된 기능에서 로직을 변경하는 데 주저함이 있었다.</p>
<p>릴리즈 후 어느정도 시간이 지나고 유지보수 단계에 들어서며 다시 이 제안이 나왔다. </p>
<p>나도 어느 정도 사용자 경험 측으로 봤을 때 불편할 수 있다는 점에 동의했다.</p>
<p>그렇게 요청대로 개선을 진행해보았다.</p>
<h2 id="작업-진행">작업 진행</h2>
<hr>
<p>사실 당장 실사용자가 많지 않았지만 실제로 운영 중인 서비스였어서 기능에 문제가 생기거나, API 호출 오류가 발생해서는 안 된다고 생각했다.</p>
<p>그래서 나름대로 이 작업을 진행하면서 세운 목표가 있다.</p>
<blockquote>
<p>무중단으로 변경하기</p>
</blockquote>
<ul>
<li>위에서 언급했듯, 실제 운영 서비스이기에 작업 후 재배포 시 정상작동해야 한다.</li>
</ul>
<p>이런 작은 목표를 가지고 작업을 시작했다.</p>
<h3 id="의존성-추가">의존성 추가</h3>
<p>우선 DB 마이그레이션 툴은 FLYWAY를 채택했다.
이유는 아래와 같다.</p>
<blockquote>
<ol>
<li>공부 진입장벽 낮음</li>
<li>주어진 시간이 많지 않았음</li>
</ol>
<p>-&gt; 진입장벽 낮음이 드는 시간 감소로 이어진다 생각했음</p>
</blockquote>
<p>어쩌면 막연한 이유일 수 있지만 근거가 없는 것보단 낫다고 생각한다..</p>
<pre><code class="language-java">    // Flyway
    implementation &#39;org.flywaydb:flyway-core&#39;
    implementation &#39;org.flywaydb:flyway-database-{DB_NAME}&#39;
</code></pre>
<p><code>DB_NAME</code>에는 본인이 사용하고 있는 DB 종류를 넣어주면 된다.</p>
<hr>
<h3 id="작업-진행-1">작업 진행</h3>
<p>처음에는 기존에 존재하는 부원과 사용자(회원)의 연관관계를 없앴다.
<del>이후 이것이 작업 진행에 영향을 주고 말았다..</del></p>
<p>FLYWAY 마이그레이션 툴을 고려하기 전에는 JPA의 <code>ddl-auto</code> 옵션을 변경하여 DB 스키마를 변경하려 했다.</p>
<blockquote>
<pre><code class="language-yaml">  jpa:
    hibernate:
      ddl-auto: update</code></pre>
</blockquote>
<pre><code>
**이 방법은 운영 환경에서는 쓰면 안 된다고 한다.**

`update` 옵션은 기존 스키마를 유지하면서 변경된 부분을 반영한다는데, 정상적으로 작동하지 않을 가능성이 존재한다고 한다. 즉 안정성이 떨어진다.
*(ex: 컬럼 삭제 등 스키마 구조를 크게 변경하는 행위)*

실제 운영환경에서 리스크를 감안할 만한가? 라는 질문을 생각해봤을 때 그럴만한 가치가 있는 것 같지 않아, 이 방법을 포기했다.

그렇게 DB 마이그레이션 툴을 이용하기로 했고, 기존에 있던 더미데이터와 마이그레이션 쿼리를 분리해 resources 패키지에 저장했다.

![](https://velog.velcdn.com/images/codemaker-kim/post/78b8621c-d0ee-4626-a263-2479db0f938d/image.png)

+ `migration`: 실제 운영환경에 사용할 마이그레이션용 쿼리

+ `seed`: 로컬 환경에서의 테스트를 위한 테스트용 쿼리 - gitIgnore 처리 된 것을 볼 수 있다.

+ `testdata`: 기존에 사용하던 더미데이터 삽입용 쿼리

로컬 개발 환경에서 사용할 yml 설정을 지정해주었다.

```yaml
  flyway:
    enabled: true
    baseline-on-migrate: true
    baseline-version: 0
    locations:
      - classpath:db/seed</code></pre><ul>
<li><p><code>enabled</code>: flyway 사용여부이다.</p>
</li>
<li><p><code>baseline-on-migrate</code>: flyway의 실행 기록(히스토리 테이블)을 확인하고, 현재 버전에 맞는 baseline 버전을 기반으로 그보다 높은 버전의 쿼리만 실행한다. false 처리되어 있다면, 실행 기록을 확인하지 않고, baseline-version 값 이후의 쿼리를 서버 작동마다 계속 실행한다.</p>
</li>
<li><p><code>baseline-version</code>: DB 버전 지정. 이 버전보다 높은 버전의 쿼리만 실행된다.(기본값 1, 보통은 현재 존재하는 스크립트와 버전을 맞춘다.) </p>
</li>
</ul>
<hr>
<h3 id="겪은-문제">겪은 문제</h3>
<p>우선 위에서 언급했듯이, 코드 레벨에서 구조를 먼저 변경한 부분에서
문제가 생길 것 같다고 느꼈다.</p>
<p>현재 운영환경에서는 JPA의 <code>ddl-auto</code> 옵션을 <code>none</code> 으로 사용 중인데
분명히 운영 환경 DB와 코드의 불일치로 인한 에러 발생 가능성이 높았고,</p>
<p>그래서 총 3번의 작업을 거쳐 DB 마이그레이션을 진행했다.</p>
<blockquote>
<ol>
<li>DB에 컬럼 추가</li>
<li>연관관계 삭제 시 영향 받는 로직 수정</li>
<li>코드레벨 연관관계 삭제와 동시에 DB 연관관계 삭제 및 백업 테이블 생성</li>
</ol>
</blockquote>
<h4 id="1-db에-컬럼-추가">1. DB에 컬럼 추가</h4>
<p><em>V1__clubmember_add_column.sql</em> 
-&gt; Flyway 파일명명 규칙에 따라 작성했기에 이런 이름이다</p>
<pre><code class="language-sql">-- 부원 테이블에 사용자 이름을 추가

-- 1. club_members 테이블에 member_name 컬럼 추가
ALTER TABLE club_members ADD COLUMN member_name VARCHAR(255);

-- 2. 기존 데이터의 member_name을 users 테이블에서 가져와서 업데이트
UPDATE club_members
SET member_name = (
    SELECT u.name
    FROM users u
    WHERE u.id = club_members.member_id
);

-- 3. member_name 컬럼을 NOT NULL로 변경
ALTER TABLE club_members ALTER COLUMN member_name SET NOT NULL;</code></pre>
<h4 id="2-영향받는-로직-수정">2. 영향받는 로직 수정</h4>
<p>코드레벨의 수정이고, 크게 복잡했던 내용은 없어 생략하겠다.</p>
<h4 id="3-코드레벨-연관관계-삭제와-동시에-db-연관관계-삭제-및-백업-테이블-생성">3. 코드레벨 연관관계 삭제와 동시에 DB 연관관계 삭제 및 백업 테이블 생성</h4>
<p><em>V2__clubmember_discard_user_join.sql</em></p>
<pre><code class="language-sql">-- 1. 백업 테이블 생성
CREATE TABLE IF NOT EXISTS club_members_backup_v2 AS
SELECT * FROM club_members;

-- 2. 인덱스 삭제 (존재한다면)
DROP INDEX IF EXISTS idx_club_members_member_id;

-- 3. 외래키 제거 (있다면)
ALTER TABLE club_members DROP CONSTRAINT IF EXISTS fk_club_members_member_id;

-- 4. member_id 컬럼 삭제
ALTER TABLE club_members DROP COLUMN IF EXISTS member_id;</code></pre>
<p>이런 작업들을 거쳐서 기존에 존재하던 부원과 회원의 연관관계(1:N)를 안정적으로 제거하고 재배포하는 작업에 성공했다.</p>
<h3 id="아쉬웠던-점">아쉬웠던 점</h3>
<p>우선, 실제 사용자의 데이터가 많이 않았다는 점이 실제 환경과 큰 차이인 것 같다. 
현재 운영환경은 롤링 배포 방식을 이용하고 있는데,</p>
<p>더 다량의 데이터와 큰 변경을 진행했다면 구조가 바뀌는데 시간이 걸렸을 테고, 이를 직접 경험해보지 못한 부분이 아쉬웠다.</p>
<p>또 작업을 끝내고 난 후 든 생각이었지만, &#39;이 방식이 정말 최선이었을까?&#39; 하는 생각이 들었다. (<del>매번 남는 아쉬움인것 같다.</del>)</p>
<h3 id="배운-점">배운 점</h3>
<ul>
<li>내가 생각했던 것보다, SQL은 중요했다.</li>
<li><blockquote>
<p>ORM으로 인해 약간 소홀하게 생각했던 것 같다.</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] TreeSet의 정렬 기준]]></title>
            <link>https://velog.io/@codemaker-kim/JAVA-TreeSet%EC%9D%98-%EC%A0%95%EB%A0%AC-%EA%B8%B0%EC%A4%80</link>
            <guid>https://velog.io/@codemaker-kim/JAVA-TreeSet%EC%9D%98-%EC%A0%95%EB%A0%AC-%EA%B8%B0%EC%A4%80</guid>
            <pubDate>Fri, 05 Sep 2025 07:03:37 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>List, Set, Map 자료구조에 대해 공부하던 중, TreeSet, TreeMap과 같이 내부 정렬을 지원하는 자료구조가 있음을 알게 되었고, 내부 정렬의 작동 원리를 이해하고자 이 글을 작성하게 되었습니다.</p>
<hr>
<h3 id="treeset의-기본-정렬-기준">TreeSet의 기본 정렬 기준</h3>
<p>단순히 TreeSet을 생성했을 때에는 이렇다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/cf24bf22-3603-4ec9-bafe-24ad5201e836/image.png" alt=""></p>
<pre><code class="language-java">        Set&lt;Integer&gt; treeSet = new TreeSet&lt;&gt;();

        treeSet.add(1);
        treeSet.add(2);
        treeSet.add(3);
        treeSet.add(4);
        treeSet.add(5);

        treeSet.add(0);

        System.out.println(treeSet);
</code></pre>
<p>이런 식의 코드로 테스트해봤을 때의 결과는 오름차순 정렬된 숫자들을 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/a1960643-a127-4625-91cd-101d9d782622/image.png" alt=""></p>
<p>그렇다면 <code>String</code> 타입은 어떨까?</p>
<pre><code class="language-java">        Set&lt;String&gt; treeSet2 = new TreeSet&lt;&gt;();

        treeSet2.add(&quot;123&quot;);
        treeSet2.add(&quot;안녕하세요&quot;);
        treeSet2.add(&quot;ghi&quot;);
        treeSet2.add(&quot;abc&quot;);
        treeSet2.add(&quot;def&quot;);
        treeSet2.add(&quot;jkl&quot;);

        System.out.println(treeSet2);
</code></pre>
<p>이런식으로 테스트 해본 결과는 이랬다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/2e0b346b-37b7-4ca0-9363-97c37ef86182/image.png" alt="">
ASCII 코드 값 기준으로 오름차순 정렬되는 것 같다.</p>
<p>궁금해서 한글으로만 테스트도 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/9fd34626-03ec-498c-b847-57e655d570b8/image.png" alt=""></p>
<p>마찬가지로 오름차순 정렬이 작동하는 것을 확인했다.</p>
<hr>
<h3 id="treeset이-제공하는-메서드">TreeSet이 제공하는 메서드</h3>
<pre><code class="language-java">    public static void main(String[] args) {
        TreeSet&lt;Integer&gt; treeSet = new TreeSet&lt;&gt;();

        treeSet.add(1);
        treeSet.add(2);
        treeSet.add(3);
        treeSet.add(4);
        treeSet.add(5);

        treeSet.add(0);

        // 맨 첫번째 값
        System.out.println(treeSet.first());

        // 맨 마지막 값
        System.out.println(treeSet.last());

        // 특정한 값 바로 앞에 있는 값 찾기
        System.out.println(treeSet.lower(3));

        // 특정한 값 바로 뒤에 있는 값 찾기
        System.out.println(treeSet.higher(0));

        // 내림차순의 Set으로
        NavigableSet&lt;Integer&gt; newSet = treeSet.descendingSet();

        /*
        * floor: 특정한 값이 존재하면, 그 값 반환
        * 없으면 바로 뒷 값 반환
        * ceiling: 그 반대. 바로 뒷 값 반환
        * */
        System.out.println(treeSet.floor(6));

        System.out.println(treeSet.ceiling(-1));

        // 가장 첫번째 순서의 값을 추출하고 없앰.
        System.out.println(treeSet.pollFirst());

        // 가장 마지막 순서의 값을 추출하고 없앰.
        System.out.println(treeSet.pollLast());
    }</code></pre>
<blockquote>
<p>참고로 <code>NavigableSet</code>은 TreeSet처럼 <code>First()</code>, <code>Last()</code>, <code>floor()</code>, <code>ceiling()</code>, <code>lower()</code>, <code>higher()</code> 메서드를 제공한다.</p>
</blockquote>
<h3 id="treeset-내의-객체-비교">TreeSet 내의 객체 비교</h3>
<p>여기까지는 단순히 자바에서 제공하는 기본 타입이기에 비교가 가능하지만, 객체를 담는 <code>TreeSet</code>의 경우는 조금 다르다.</p>
<p>이전 정리의 <code>Set</code>의 비교의 경우에는(Hash 기반 구현체들)
<code>equals()</code>와 <code>hashCode()</code> 메서드를 오버라이딩하여 비교 기준을 잡아주었다.</p>
<p><code>TreeSet</code>의 경우는, <code>Comparable&lt;T&gt;</code> 인터페이스를 상속받고,
<code>compareTo()</code> 메서드를 오버라이딩하여 비교 기준을 정한다.</p>
<p>그냥 생성하면 이렇게 노란 경고 줄이 보인다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/2def7671-a2dc-4711-8fcc-d800addfb2a0/image.png" alt=""></p>
<p><code>Fruit</code> 객체를 비교 가능하게 하기 위해, <code>equals()</code> 와 <code>hashCode()</code> 메서드를 상속받아 구현해보겠다.</p>
<p>우선은 <code>TreeSet</code>만 사용할 것이기에 <code>compareTo()</code> 메서드만 구현했지만, 나중에 다른 구현체로 바뀔 것을 고려한다면, <code>equals()</code>와 <code>hashCode()</code> 메서드까지 오버라이딩하는게 좋다고 한다.</p>
<pre><code class="language-java">public class Fruit implements Comparable&lt;Fruit&gt; {
    private final String name;

    public Fruit(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    // 이름이 같다면 같은 객체로 판별
    @Override
    public int compareTo(Fruit o) {
        return this.name.compareTo(o.name);
    }
}</code></pre>
<p>각 객체를 저장하고, 이름을 출력해보았다.</p>
<pre><code class="language-java">public class TreeSetEx2 {
    public static void main(String[] args) {
        Set&lt;Fruit&gt; fruits = new TreeSet&lt;&gt;();

        fruits.add(new Fruit(&quot;사과&quot;));
        fruits.add(new Fruit(&quot;배&quot;));
        fruits.add(new Fruit(&quot;한라봉&quot;));
        fruits.add(new Fruit(&quot;천혜향&quot;));

        fruits.forEach(fruit -&gt; System.out.println(fruit.getName()));
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/4ed835f2-dd7d-4148-a1d4-f53708555496/image.png" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>추가적으로 더 조사해보니, </p>
<blockquote>
<p>“비교는 Comparable/Comparator, 동등성은 equals, 해시 기반에선 hashCode는 보조 값으로 쓰이고 최종 비교는 여전히 equals가 한다.”</p>
</blockquote>
<p>여기서 각 구현체마다의 비교방식은 조금씩 다르다는 결론을 얻어낼 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] List, Set, Map 비교]]></title>
            <link>https://velog.io/@codemaker-kim/JAVA-List-Set-Map-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@codemaker-kim/JAVA-List-Set-Map-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Sun, 31 Aug 2025 11:59:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>List, Set, Map? 명확하게 이해하기 위한 정리</p>
</blockquote>
<h2 id="개요">개요</h2>
<p>사실 그동안 명확하게 자료구조를 공부하고, 이해하려는 노력은 거의 하지 않았던 것 같습니다.
실제로 당장 하던 프로젝트 등에서도 <strong>List</strong> 자료 구조 외에는 거의 써본 적이 없고, 필요할 때만 대충 JAVA 언어에서 사용하는 방식만 확인하고 사용하기 급급했었습니다. </p>
<p>이번 정리를 통해 <strong>List, Set, Map 자료구조에 대한 이해를 명확히 하고, 상황에 맞게 사용할 수 있도록</strong> 하려 합니다.</p>
<hr>
<h2 id="list">List?</h2>
<h3 id="1-핵심">1. 핵심</h3>
<p>순서가 있는 원소 집합을 저장하는 자료구조
특징은 아래와 같다.</p>
<ul>
<li><strong>중복 허용</strong>: 동일한 값이 여러 번 들어갈 수 있다.</li>
<li><strong>인덱스 기반 접근</strong>: 0부터 시작하는 인덱스로, 원소를 조회 / 수정 / 삽입이 가능하다.</li>
</ul>
<hr>
<h3 id="2-java에서의-list">2. JAVA에서의 List</h3>
<p><code>JAVA</code> 컬렉션에서는 List는 <strong>인터페이스</strong>이며, 아래와 같은 구현체들로 구성된다.</p>
<ul>
<li><p><code>ArrayList</code></p>
</li>
<li><p><code>LinkedList</code> </p>
</li>
<li><p><code>Vector</code> - 레거시, 요즘은 잘 사용하지 않음</p>
</li>
</ul>
<p><code>ArrayList</code> 특징
-&gt; <strong>배열 기반</strong>, <strong>빠른 조회</strong>, 삽입 / 삭제는 느리다.(중간 원소 이동)</p>
<p><code>LinkedList</code> 특징
-&gt; <strong>이중 연결 리스트</strong> 기반 (이전 노드, 이후 노드 둘 다 접근 가능),</p>
<p><code>ArrayList</code>와 반대로 삽입 / 삭제가 빠르다.(노드 포인터만 변경), </p>
<p><strong>조회는 느리다.</strong>(첫 노드부터 탐색)</p>
<h3 id="두-구현체-시간-복잡도-차이">두 구현체 시간 복잡도 차이</h3>
<table>
<thead>
<tr>
<th>연산</th>
<th>ArrayList</th>
<th>LinkedList</th>
</tr>
</thead>
<tbody><tr>
<td><code>get(index)</code></td>
<td>O(1)</td>
<td>O(n)</td>
</tr>
<tr>
<td><code>add(element)</code> (끝)</td>
<td>평균 O(1)</td>
<td>O(1)</td>
</tr>
<tr>
<td><code>add(index, elem)</code></td>
<td>O(n)</td>
<td>O(1)</td>
</tr>
<tr>
<td><code>remove(index)</code></td>
<td>O(n)</td>
<td>O(1)</td>
</tr>
<tr>
<td><code>contains(element)</code></td>
<td>O(n)</td>
<td>O(n)</td>
</tr>
</tbody></table>
<p>간단하게 결론 내자면, </p>
<p>빠른 조회에는 -&gt; <code>ArrayList</code>
많은 삽입 / 삭제 -&gt; <code>LinkedList</code></p>
<p>다만, LinkedList는 순수하게 삽입, 삭제만 생각했을 때는 복잡도가 O(1)이 맞지만,</p>
<p><strong>삽입, 삭제 과정을 진행하기 위한 위치를 알기 위해선</strong> 앞에서부터 차례대로 탐색해야 하는 과정이 필요하다.</p>
<p>다행히도, Java의 LinkedList는 최적화가 어느 정도 되어있어</p>
<ul>
<li><p>찾는 인덱스가 앞쪽 -&gt; head 부터 탐색,</p>
</li>
<li><p>찾는 인덱스가 뒷쪽 -&gt; tail 부터 역순으로 탐색한다.</p>
</li>
</ul>
<p>그래도 결국 최대는 <code>O(n)</code> 만큼의 시간이 소요된다.</p>
<pre><code class="language-java">public void add(int index, E element) {
    // 1. 인덱스가 범위 내에 있는지 판단한다.
    checkPositionIndex(index);

    // 2. 인덱스가 길이와 같다면 바로 제일 뒷 원소에 삽입.
    // 그렇지 않다면 인덱스에 위치한 노드의 바로 앞에 삽입.
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
 }

// 인덱스가 범위 내에 존재하는 지 확인하고, 없으면 예외를 던진다.
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 인덱스가 범위 내의 값인지 판별한다.
private boolean isPositionIndex(int index) {
    return index &gt;= 0 &amp;&amp; index &lt;= size;
}
</code></pre>
<blockquote>
<p>-&gt; linkedlist의 add 함수이다. 위에서 본대로 약간의 인덱스 보정(?)이 되어있다.</p>
</blockquote>
<p>추가로, 양 끝쪽 원소의 삽입, 삭제 로직이라면
addFirst(), addLast(), removeFirst(), removeLast()와 같은 메서드도 있어서, 약간의 성능 최적화를 원한다면 위 선택지도 나쁘지 않을 것 같다!</p>
<h3 id="3-java에서의-list-사용">3. Java에서의 List 사용</h3>
<p><code>java.util</code> 패키지의 <code>List</code> 인터페이스로 import한다.</p>
<pre><code class="language-java">public class ListEx {
    public static void main(String[] args) {
        List list = new ArrayList();
    }
}</code></pre>
<blockquote>
<p>제네릭을 이용해서, <code>List</code> 에 들어갈 특정한 타입을 명시해줄 수도 있다.
오히려, 이 방식이 더 권장되는 편이다.</p>
</blockquote>
<pre><code class="language-java">public class ListEx {
    public static void main(String[] args) {
        Element e = new Element(&quot;객체&quot;);

        // 제네릭으로 리스트에 들어갈 타입 명시
        List&lt;Element&gt; list = new ArrayList&lt;&gt;();
    }
}</code></pre>
<blockquote>
<p>정적 메서드를 이용해서 요소들을 명시적으로 삽입하며 생성도 가능하다.</p>
</blockquote>
<pre><code class="language-java">public class ListEx {
    public static void main(String[] args) {
        // 빈 리스트 생성
        List&lt;Element&gt; list = List.of();

        // 요소들을 명시적으로 넣어 생성
        List&lt;Element&gt; list2 = List.of(
            new Element(&quot;Hydrogen&quot;),
            new Element(&quot;Helium&quot;),
            new Element(&quot;Lithium&quot;),
            new Element(&quot;Beryllium&quot;),
            new Element(&quot;Boron&quot;),
            new Element(&quot;Carbon&quot;),
            new Element(&quot;Nitrogen&quot;),
            new Element(&quot;Oxygen&quot;),
            new Element(&quot;Fluorine&quot;),
            new Element(&quot;Neon&quot;)
        );
    }
}
</code></pre>
<blockquote>
<p>참고로, <code>.of()</code> 로 생성된 List는 불변이다. 따라서 생성 이후에 추가로 값을 삽입, 삭제하려 하면 <code>UnsupportedOperationException</code>이 발생하게 된다.</p>
</blockquote>
<pre><code class="language-java">// 빈 리스트를 생성할 때의 of 메서드
static &lt;E&gt; List&lt;E&gt; of() {
        return (List&lt;E&gt;) ImmutableCollections.EMPTY_LIST;
    }


// 요소가 있을 때의 of 메서드
static &lt;E&gt; List&lt;E&gt; of(E e1) {
        return new ImmutableCollections.List12&lt;&gt;(e1);
}


// ImmutableCollections 클래스의 필드 중 일부
private static Object[] archivedObjects;

    private static final Object EMPTY;
    static final ListN&lt;?&gt; EMPTY_LIST;
    static final ListN&lt;?&gt; EMPTY_LIST_NULLS;
    static final SetN&lt;?&gt; EMPTY_SET;
    static final MapN&lt;?,?&gt; EMPTY_MAP;


// ImmutableCollections 클래스의 내부 클래스인 List12 클래스
    static final class List12&lt;E&gt; extends AbstractImmutableList&lt;E&gt;
            implements Serializable {
</code></pre>
<blockquote>
<p>이런 식으로 삽입 테스트를 해보았다.</p>
</blockquote>
<pre><code class="language-java">public class ListEx {
    public static void main(String[] args) {
        // 1. 빈 리스트 생성
        List&lt;Element&gt; list = List.of();

        // 2. 원소 생성 후 리스트에 추가
        Element neon = new Element(&quot;Neon&quot;);

        list.add(neon); // UnsupportedOperationException 발생
    }

}</code></pre>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/f9b92cda-a89f-4986-810a-a26a58f2bc10/image.png" alt=""></p>
<blockquote>
<p>-&gt; <code>UnsupportedOperationException</code>이 발생하는 것을 확인할 수 있다.
다만, List의 <strong>내부 요소의 변경을 불가능케 하는 것은 아니다.</strong>
요소 자체의 필드가 변경이 가능하다면, 그건 가능하다.</p>
</blockquote>
<hr>
<h2 id="set">Set?</h2>
<h3 id="1-핵심-1">1. 핵심</h3>
<p>중복을 허용하지 않는 원소 집합이다.</p>
<ul>
<li><strong>순서 없음</strong>: 저장 순서를 보장하지 않음.</li>
<li><strong>null 허용 여부</strong>: 구현체마다 다르지만, <code>HashSet</code>의 경우 1개 허용, <code>TreeSet</code>의 경우는 null 불가능</li>
</ul>
<h3 id="2-java에서의-set">2. Java에서의 Set</h3>
<p>JAVA에서의 구현체는 아래와 같다.</p>
<ul>
<li><p><code>HashSet</code></p>
</li>
<li><p><code>LinkedHashSet</code></p>
</li>
<li><p><code>TreeSet</code></p>
</li>
</ul>
<p><code>HashSet</code>: <code>HashMap</code>을 이용하여 구현. 
<strong>해시값을 이용하여 원소를 저장</strong> 검색, 삽입, 삭제 속도는 평균 O(1) 이다.
<strong>순서 유지되지 않음</strong>, null값 1개 허용</p>
<p><code>LinkedHashSet</code>: <code>HashSet</code> + 이중 연결 리스트의 구조로 생각하면 편하다.
<strong>입력된 순서를 유지</strong>, 다만 HashSet보다 조금 느리다는 단점이 존재.
(순서 유지 + 중복 제거)</p>
<p><code>TreeSet</code>: 내부적으로 TreeMap(R-B Tree) 기반, 원소가 <strong>자동으로 정렬 상태 유지</strong>
검색, 삽입, 삭제 속도는 <strong>O(log n)</strong>
<strong>null 허용 x</strong> -&gt; 정렬이 불가능하기 때문.</p>
<table>
<thead>
<tr>
<th>연산</th>
<th>HashSet</th>
<th>LinkedHashSet</th>
<th>TreeSet</th>
</tr>
</thead>
<tbody><tr>
<td><code>add</code>, <code>remove</code>, <code>contains</code></td>
<td>평균 O(1)</td>
<td>평균 O(1)</td>
<td>O(log n)</td>
</tr>
<tr>
<td>순서 유지</td>
<td>X</td>
<td>입력 순서 유지</td>
<td>정렬 순서 유지</td>
</tr>
<tr>
<td>null 허용</td>
<td>1개 가능</td>
<td>1개 가능</td>
<td>불가능</td>
</tr>
</tbody></table>
<p>여기서 궁금한 점은 왜 <code>TreeSet</code>은 다른 구현체에 비해 더 빠른 걸까?</p>
<h4 id="treeset">TreeSet</h4>
<p>기본적으로 <strong>R-B 트리</strong>로 구현되어 있다.</p>
<p>R-B 트리:</p>
<ul>
<li>균형 이진 탐색 트리</li>
<li>특정한 규칙을 통해 항상 균형에 가까운 depth 를 유지(log n)</li>
<li>삽입 / 삭제 시 회전과 색 변환으로 높이를 조정.</li>
</ul>
<p>-&gt; R-B 트리에 관한 내용은 추후에 정리해볼 예정입니다.</p>
<p>일단 R-B트리의 강력한 규칙에 의해 안정적인 성능이 나온다는 점만 기억해두자.</p>
<h3 id="3-java에서의-set-사용">3. Java에서의 Set 사용</h3>
<p>마찬가지로, java.util 패키지를 import 하여 사용한다.
Set도 제네릭을 이용하여 들어갈 타입의 명시가 가능하다.</p>
<pre><code class="language-java"> public class SetEx {
    public static void main(String[] args) {
        Set&lt;Element&gt; set = Set.of();
    }
}
</code></pre>
<blockquote>
<p>Set도 마찬가지로 <code>of()</code> 정적 메서드 기반 생성이 가능하고 불변 객체 이며,
조회같은 경우는, List와 다르게 인덱스가 존재하지 않아 <strong>전체 요소를 순회하며 조회해야 한다.</strong></p>
</blockquote>
<pre><code class="language-java">public class SetEx {
    public static void main(String[] args) {
        // HashSet 생성
        Set&lt;Element&gt; fruits = new HashSet&lt;&gt;();
        fruits.add(new Element(&quot;Apple&quot;));
        fruits.add(new Element(&quot;Orange&quot;));
        fruits.add(new Element(&quot;halabong&quot;));

        // 1) 반복문으로 조회
        for (Element fruit : fruits) {
            if (fruit.hasName(&quot;halabong&quot;)) {
                System.out.println(&quot;Found element: &quot; + fruit); // halabong
            }
        }

        // 2) 스트림을 사용한 조회
        fruits.stream()
                .filter(f -&gt; f.hasName(&quot;halabong&quot;))
                .forEach(f -&gt; System.out.println(&quot;Found (Name is &#39;Halabong&#39;): &quot; + f));
    }
}</code></pre>
<blockquote>
<p>Set에는 두 컬렉션(Set)에 포함된 원소들만 유지하는 메서드도 지원한다. 
(교집합, retainAll(Collection&lt;?&gt; c))</p>
</blockquote>
<pre><code class="language-java">
public class SetEx {
    public static void main(String[] args) {

        Element apple = new Element(&quot;Apple&quot;);
        Element orange = new Element(&quot;Orange&quot;);
        Element halabong = new Element(&quot;halabong&quot;);
        Element pineapple = new Element(&quot;pineapple&quot;);


        Set&lt;Element&gt; fruits = new HashSet&lt;&gt;();
        Set&lt;Element&gt; fruits2 = new HashSet&lt;&gt;();

        fruits.add(apple);
        fruits.add(orange);
        fruits.add(halabong);

        fruits2.add(apple);
        fruits2.add(orange);
        fruits2.add(pineapple);

        fruits.retainAll(fruits2);

        for (Element fruit : fruits) {
            System.out.println(fruit.getName());
        }
    }
}</code></pre>
<blockquote>
<p>결과는 아래와 같다. 교집합이고 이름이 Apple과 Orange인 원소만 남은 것을 확인했다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/059c8a61-4a73-4893-971d-72b64191c557/image.png" alt=""></p>
<h4 id="set의-중복판단-원리">Set의 중복판단 원리?</h4>
<p>Set에서는 원소를 저장할 때 <em>값이 이미 들어있나?</em> 를 판단해야 한다.</p>
<p>이를 위해서 Java에서는 두 가지 메서드를 사용한다.</p>
<ul>
<li><code>hashCode()</code>: 객체의 해시 값을 반환한다.(정수)</li>
<li><code>equals(Object)</code>: 두 객체가 실제로 같은지 비교한다.</li>
</ul>
<p>여기서, <code>hashCode()</code>와 <code>equals()</code>의 규칙이 존재한다.</p>
<ol>
<li><strong><code>equals()</code>가 <code>true</code>면, <code>hashCode()</code>도 반드시 같아야 한다.</strong></li>
</ol>
<ul>
<li>두 객체가 같다면, 같은 해시 버킷 안에 있어야 하기 때문이다.</li>
</ul>
<ol start="2">
<li><strong><code>hashCode()</code>가 같다고, <code>equals()</code>가 반드시 true일 필요는 없다.</strong></li>
</ol>
<ul>
<li>다른 객체지만 해시 충돌로 같은 값이 나올 수도 있다.</li>
</ul>
<ol start="3">
<li><strong>실행 중에 객체 상태가 변하지 않는 한, hashCode는 항상 동일해야 한다.</strong></li>
</ol>
<blockquote>
<p>궁금하니, 직접 <code>equals()</code>와 <code>hashCode()</code> 메서드를 재정의하여 테스트해보자.
기존에 있던 <code>Element</code> 클래스의 중복 기준을 이름이 같다면 같은 요소로 판단하도록 재정의해보았다.</p>
</blockquote>
<pre><code class="language-java">    // 요소의 이름 해시코드 반환
    @Override
    public int hashCode() {
        return name.hashCode();
    }

    // 이름 기반 비교 로직으로 변경
    @Override
    public boolean equals(Object obj) {
        if (this==obj)
            return true;
        if (obj==null || getClass()!=obj.getClass())
            return false;

        Element e = (Element) obj;
        return Objects.equals(this.name, e.name);
    }
</code></pre>
<blockquote>
<p>이제 다시 <code>retainAll</code> 메서드를 테스트해보았다. 기존에는 객체를 생성할 때마다 변수로 할당 후, 삽입하여 중복 비교를 했다면 이제는 이름이 같다면 중복 취급되게 될 것이다.</p>
</blockquote>
<pre><code class="language-java">public class SetEx {
    public static void main(String[] args) {
        Set&lt;Element&gt; fruits = new HashSet&lt;&gt;();
        Set&lt;Element&gt; fruits2 = new HashSet&lt;&gt;();

        fruits.add(new Element(&quot;Apple&quot;));
        fruits.add(new Element(&quot;Orange&quot;));
        fruits.add(new Element(&quot;halabong&quot;));

        fruits2.add(new Element(&quot;Apple&quot;));
        fruits2.add(new Element(&quot;Orange&quot;));
        fruits2.add(new Element(&quot;pineapple&quot;));

        fruits.retainAll(fruits2);

        for (Element fruit : fruits) {
            System.out.println(fruit.getName());
        }
    }
}</code></pre>
<blockquote>
<p>실행 결과도 정상적으로 겹치는 요소인 Apple과 Orange 라는 name을 가진 요소만 남겼다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/c6a97914-05d8-431f-a06e-e073663740fd/image.png" alt=""></p>
</blockquote>
<p>이때 중요한 점은, 꼭 <code>equals()</code>와 <code>hashCode()</code>는 같이 재정의되야 한다는 것이다.
hashCode가 달라서 다른 객체로 저장되면, <strong>Set에 중복된 값이 들어가거나, 제대로 찾지 못하는 문제</strong>가 발생할 수 있다.</p>
<p>따라서, 사용자 정의 클래스를 비교 대상으로 이용하려면 꼭 <code>hashCode()</code>와 <code>equals()</code>를 같이 재정의 해줘야 한다.</p>
<hr>
<h2 id="map">Map?</h2>
<h3 id="1-핵심-2">1. 핵심</h3>
<p>key-value 쌍을 저장하는 자료구조이다.</p>
<ul>
<li><strong>key는 중복 불가, Value는 중복 허용</strong></li>
<li>key를 통해서 value를 빠르게 검색, 수정, 삭제할 수 있다.</li>
</ul>
<h3 id="2-java에서의-map">2. Java에서의 Map</h3>
<ul>
<li><p><code>HashMap</code></p>
<ul>
<li>해시 테이블 기반</li>
<li>평균적으로 <strong>검색, 삽입, 삭제 O(1)</strong></li>
<li><code>equals()</code>, <code>hashCode()</code>로 Key의 중복 여부 판단</li>
<li>순서 보장 X</li>
<li><code>null</code> Key 1개, <code>null</code> Value 여러 개 허용</li>
</ul>
</li>
<li><p><code>LinkedHashMap</code></p>
<ul>
<li>HashMap + 이중 연결 리스트</li>
<li>삽입 순서 혹은 접근 순서 유지 가능</li>
<li>캐시 구현할 때 자주 사용(LRU 캐시)</li>
</ul>
</li>
<li><p><code>TreeMap</code></p>
<ul>
<li>내부적으로 <code>R-B트리</code></li>
<li>Key가 정렬된 상태로 저장됨.</li>
<li>검색/삽입/삭제 O(log n)</li>
<li>Key 정렬 기준: <code>Comparable</code> 인터페이스 혹은 <code>Comparator</code></li>
<li><strong><code>null</code> Key 불가</strong></li>
</ul>
</li>
<li><p><code>ConcurrentHashMap</code></p>
<ul>
<li>멀티스레드 환경에서 안전하게 사용 가능</li>
<li><code>HashMap</code>을 여러 구간으로 나누어 lock 분할.(동시성 성능 향상)</li>
</ul>
</li>
</ul>
<p>Map은 기존 List, Set과는 조금 다른 구현 메서드를 사용한다.</p>
<pre><code class="language-java">V put(K key, V value) → Key-Value 저장

V get(Object key) → Key로 Value 조회

V remove(Object key) → 해당 Key 삭제

boolean containsKey(Object key) → Key 존재 여부

boolean containsValue(Object value) → Value 존재 여부

int size() → 엔트리 수

boolean isEmpty() → 비어있는지 확인

void clear() → 모든 엔트리 삭제

Set&lt;K&gt; keySet() → 모든 Key 반환

Collection&lt;V&gt; values() → 모든 Value 반환

Set&lt;Map.Entry&lt;K,V&gt;&gt; entrySet() → 모든 Key-Value 쌍 반환</code></pre>
<table>
<thead>
<tr>
<th>연산</th>
<th>HashMap</th>
<th>LinkedHashMap</th>
<th>TreeMap</th>
</tr>
</thead>
<tbody><tr>
<td><code>get(key)</code></td>
<td>O(1)</td>
<td>O(1)</td>
<td>O(log n)</td>
</tr>
<tr>
<td><code>put(key, value)</code></td>
<td>O(1)</td>
<td>O(1)</td>
<td>O(log n)</td>
</tr>
<tr>
<td><code>remove(key)</code></td>
<td>O(1)</td>
<td>O(1)</td>
<td>O(log n)</td>
</tr>
<tr>
<td>순서 유지 여부</td>
<td>❌</td>
<td>삽입/접근 순서</td>
<td>정렬 순서</td>
</tr>
</tbody></table>
<h3 id="3-java에서-map-사용">3. Java에서 Map 사용</h3>
<p>마찬가지로 java.util 패키지에서 import 해서 사용한다.
<code>of()</code> 정적 메서드를 지원하며, 이렇게 생성 시 불변 객체가 된다.</p>
<pre><code class="language-java">public class MapEx {
    public static void main(String[] args) {
        // 빈 맵 생성
        Map&lt;Integer, Element&gt; map = Map.of();
    }
}</code></pre>
<p>간단한 예제를 만들어보았다.</p>
<pre><code class="language-java">public class MapEx {
    public static void main(String[] args) {
        // 빈 맵 생성
        Map&lt;Integer, Element&gt; map = new HashMap&lt;&gt;();

        // 원소 생성 후 맵에 추가
        Element element = new Element(&quot;Hydrogen&quot;);
        Element helium = new Element(&quot;Helium&quot;);
        Element lithium = new Element(&quot;Lithium&quot;);
        Element beryllium = new Element(&quot;Beryllium&quot;);
        Element boron = new Element(&quot;Boron&quot;);

        map.put(1, element);
        map.put(2, helium);
        map.put(3, lithium);
        map.put(4, beryllium);
        map.put(5, boron);

        // 조회
        System.out.println(&quot;map.get(1) = &quot; + map.get(1)); // Hydrogen

        //containsKey
        System.out.println(&quot;map.containsKey(1) = &quot; + map.containsKey(6)); // false

        // 엔트리 순회
        Set&lt;Map.Entry&lt;Integer, Element&gt;&gt; elements = map.entrySet();

        for (Map.Entry&lt;Integer, Element&gt; entry : elements) {
            System.out.println(&quot;entry.getKey() = &quot; + entry.getKey() + &quot;, entry.getValue() = &quot; + entry.getValue());
        }

        // key 순회
        Set&lt;Integer&gt; keys = map.keySet();

        for (Integer key : keys) {
            System.out.println(&quot;key = &quot; + key);
        }

        // value 순회
        Collection&lt;Element&gt; values = map.values();
        Set&lt;Element&gt; valueSet = (Set&lt;Element&gt;) map.values();

        for (Element value : valueSet) {
            System.out.println(&quot;value = &quot; + value);
        }
    }
}</code></pre>
<hr>
<h2 id="마치며">마치며</h2>
<p>정말 간단하게 List, Set, Map에 대한 내용을 정리해보았습니다.
이번 정리를 통해 각 자료구조의 특징에 대해 이해하고, 어떤 구현체를 어떤 상황에 쓰면 좋을지 조금이나마 고민할 수 있게 되었습니다.</p>
<p>다음에는 TreeSet의 정렬 기준과 ConcurrentHashMap의 동시성에 대해 정리해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체 지향 설계 5원칙이란? (SOLID)]]></title>
            <link>https://velog.io/@codemaker-kim/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84-5%EC%9B%90%EC%B9%99%EC%9D%B4%EB%9E%80-SOLID</link>
            <guid>https://velog.io/@codemaker-kim/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84-5%EC%9B%90%EC%B9%99%EC%9D%B4%EB%9E%80-SOLID</guid>
            <pubDate>Sat, 21 Dec 2024 14:52:46 GMT</pubDate>
            <description><![CDATA[<h2 id="객체-지향-5원칙">객체 지향 5원칙?</h2>
<blockquote>
<p>객체 지향? 절차 지향 안 따르고 객체 만드는거 ㅋㅋ</p>
</blockquote>
<p>내가 객체 지향에 대한 이론을 제대로 알기 전까지의 생각이었다.</p>
<p>뭐 사실..틀린 마인드?는 아닌거 같긴한데</p>
<p>제대로 알기 전까지는 별 생각 없이 개발을 했던 것 같다.</p>
<p>그래서 다시금 복기해볼 겸 이 글을 적는다.</p>
<h2 id="solid">SOLID?</h2>
<p>SOLID는 5원칙의 앞 글자들을 따서 만들어진 단어인데, 5원칙은 아래와 같다.</p>
<blockquote>
<ul>
<li><strong>SRP</strong>(Single responsibility princlple): <em>단일 책임 원칙</em></li>
</ul>
</blockquote>
<ul>
<li><strong>OCP</strong>(Open/Closed priciple): <em>개방-폐쇄 원칙</em></li>
<li><strong>LSP</strong>(Liskov subsitution principle): <em>리스코프 치환 원칙</em></li>
<li><strong>ISP</strong>(Interface segregation principle): <em>인터페이스 분리 원칙</em></li>
<li><strong>DIP</strong>(Dependency inversion principle): <em>의존관계 역전 원칙</em></li>
</ul>
<p>보통 그냥 다 앞글자 따서 XXP 이렇게 만들어진 원칙들이다. (<em><del>하츄핑</del></em>)</p>
<p>위에서부터 찬찬히 알아보자!</p>
<h3 id="1-srp--단일-책임-원칙">1. SRP / 단일 책임 원칙</h3>
<p>단일 책임 원칙은 말 그대로다. 하나의 객체, 즉 <em>클래스가 하나의 책임만 가져야 한다</em>는 것이다.</p>
<p>이때 중요한 기준은 <strong>코드의 변경</strong>이다.
➡️ Ex: 코드를 바꿨을 때, 전체적인 파급이 적은가?</p>
<p>이 원칙은 사실 완벽하게 지켜지기는 쉽지 않은 듯하다.</p>
<p>한 클래스의 책임을 너무 작게, 너무 크게 해도 법칙을 위반하기 때문이다.</p>
<p>그렇기에, 이 클래스들의 역할을 적절하게 조절, 분배하는 것이 객체지향의 묘미라고 할 수 있다고 한다!</p>
<h3 id="2-ocp--개방---폐쇄-원칙">2. OCP / 개방 - 폐쇄 원칙</h3>
<blockquote>
<p>확장에는 열려있지만, 변경에는 닫혀있어야 한다?</p>
</blockquote>
<p>개방 폐쇄 원칙의 핵심이다. 사실 위 문장 하나만으로는 조금 이해하기 모호하다.</p>
<p>사실 이건 객체지향의 3요소 중 하나인 <strong>다형성</strong>과 관련이 있는데,</p>
<p>핵심적인 기능은 최대한 변하지 않되, 새로운 기능의 확장은 쉽도록 설계해야 한다는 원칙 같다.</p>
<p>정말 좋은 원칙이다..좋은 원칙인데, 이 객체 지향 5원칙에서의 모순되는 문제가 하나 생기는데, 아래에서 설명하도록 하겠다.</p>
<h3 id="3-lsp--리스코프-치환-원칙">3. LSP / 리스코프 치환 원칙</h3>
<p>간단하게 설명하자면, 다음과 같다.</p>
<blockquote>
<p>인터페이스가 있으면, 그 인터페이스의 <em>규약</em>을 맞추어야 한다.</p>
</blockquote>
<p>이것은 프로그램의 정확성과 연관되어 있다고 볼 수 있다.</p>
<p>당연히 인터페이스가 만들어진 의도와 다르게 구현되어도 컴파일이 되지만, 그렇게 구현된 인터페이스는 <strong>의미가 없지 않을까?</strong></p>
<p>➡️ 인터페이스의 의도에 맞게 구현하는 것이 중요하다는 원칙이라고 할 수 있겠다.</p>
<h3 id="4-isp--인터페이스-분리-원칙">4. ISP / 인터페이스 분리 원칙</h3>
<blockquote>
<p>특정 클라이언트를 위한 인터페이스 여러개가, 범용 인터페이스 하나보다 낫다.</p>
</blockquote>
<p>인터페이스 구성을 모호하게 하지말고, 구체적으로 만들라는 원칙 같다.</p>
<p>EX: 자동차 인터페이스-&gt; 운전 인터페이스, 정비 인터페이스
    사용자 -&gt; 운전자 클라이언트와 정비사 클라이언트</p>
<p>이런 식으로 분리했을 때, 어느 <strong>한 쪽의 인터페이스가 변해도</strong>, 다른 쪽 인터페이스에 영향을 미치지 않는다.</p>
<p>즉, 인터페이스가 명확해지고 이를 대체하는 것도 쉬워진다.</p>
<h3 id="5-dip--의존-관계-역전-원칙">5. DIP / 의존 관계 역전 원칙</h3>
<blockquote>
<p>추상화에 의존해야지, 구체화에 의존하면 안 된다.</p>
</blockquote>
<p>개인적으로 가장 와닿은 원칙이었다. 별 생각 없이 개발하던 나에게는 좀 큰 충격으로 다가왔는데, 각설하고 설명하겠다.</p>
<p>그니까 역할에 의존해야지, 구현에 의존하면 안 된다는 원칙인데..</p>
<p>이러게 되면, 순수 자바에서는 위에서 언급했던 OCP / 개방 - 폐쇄 원칙과 모순되게 된다!</p>
<p>예를 들자면, TestService라는 인터페이스가 있다면, 이 TestService는 이 인터페이스를 구현한 TestServiceImpl이라는 구현 클래스로 선언될 수 있는데</p>
<pre><code class="language-java">
public class ExServiceImpl implements ExService{
    // 인터페이스가 구체화된 클래스를 고르게 되는 상황
    TestService testService = new TestServiceImpl();
}</code></pre>
<p>이렇게 되면 사실 구현체와 추상, 둘 다에 의존하는 상황이 되게 된다.</p>
<p>사실 그래서 우리가 JAVA 프레임워크로 스프링을 쓰는 이유기도 하다.</p>
<p>(근데 스프링을 설명하려는 글은 아니니까 대충 여기서 마무리하려 한다.)</p>
<h2 id="결론">결론</h2>
<p>사실 아직도 내가 이 원칙을 잘 지키며 개발하고 있는지는 모르겠다.</p>
<p>하지만 이전보다는 내가 &#39;잘 지키고 있나?&#39; 라고 의식하며 코드를 작성하게 된 시점에서 무지한 상태였던 나보다는 훨씬 발전했다고 생각한다.</p>
<p>뭔가 되게 투박하고 조잡하게 설명을 하게된 느낌이지만, </p>
<p>정말 혹시라도 이 영양가 없는 글을 읽어주신 분이 있다면</p>
<p>정말 감사합니다!</p>
<p>좋은 하루 되세요!!👍👍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RAG 란 무엇일까...]]></title>
            <link>https://velog.io/@codemaker-kim/RAG-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@codemaker-kim/RAG-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sat, 21 Dec 2024 13:55:12 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>바야흐로 대 LLM의 시대..</p>
<p>필자도 졸작 심사 통과를 위해선 졸작에 LLM 사용이 필수 불가결했는데..</p>
<p>그래서 간단하게 LLM에 대한 공부 겸, 우리가 사용할 기술인 RAG에 대해 </p>
<p>공부해보고자 이 글을 적는다.</p>
<p>(<del>이 방법을 쓰라는 교수님의 강력 추천도 있었다.</del>)</p>
<h2 id="그래서-llm이-뭔데🧐">그래서 LLM이 뭔데?🧐</h2>
<p>LLM이란, 요새 흔히 말하는 <strong>지능형 챗봇</strong> 이나 자연어 처리 앱을 지원하는 핵심 인공 지능 기술이다.</p>
<p>흥미로운 건 이 LLM이라는 놈도 신뢰할 수 있는 모종의 지식 소스, 그러니까 대충 어딘가에 적힌 정보, 내용을 참고해서 답변한다는 것이다.</p>
<p>그리고, 이 녀석의 훈련 데이터는 정적이기에, 자신이 학습했던 이후에 생긴 일에 대한 내용은 모른다.</p>
<p>(<del>대충 과거에 챗 지피티로 비교적 최신 백준 문제를 물어봤다가, 데이터에 없다고 풀이를 거부당한 적이 있다.</del>)</p>
<p>그 외 추가적인 문제점들은 아래와 같다.</p>
<ol>
<li><p>답변이 없을 때 허위 정보를 제공한다.
➡️ 여러분도 당해본 경험이 좀 있을 것이다. 일명 할루시네이션 효과(Hallucination) 없는 일을 있는 것처럼 그럴싸하게 만들어서 말한다던가..</p>
</li>
<li><p>사용자가 구체적인 정보 혹은 최신의 정보를 원할 때, 구식이거나 일반적인 정보를 제공한다.
➡️ 우리 모두가 챗봇을 감정 쓰레기통으로 만들게 되는 이유 중 하나다.</p>
</li>
<li><p>신뢰할 수 없는 출처로부터 응답을 생성한다.
➡️ 이것도 1번과 유사한 느낌인듯 하다. 정보의 출처를 정확히 알 수 없다.</p>
</li>
<li><p>용어 / 언어 혼동으로 인한 응답이 정확하지 않다.
➡️ 다양한 훈련 소스가 동일한 용어를 사용하여 서로 다른 내용을 설명하기에..이 놈도 헷갈리나 보다..</p>
</li>
</ol>
<p>아무튼, 우리는 챗봇이 질문의 의도와 정확한 답변을 원하지, 구식이거나 이미 알고있는 내용을 답변받고 싶지는 않다..</p>
<p>RAG는 이런 문제 중의 일부를 해결하기 위한 하나의 접근 방식이라고 한다!</p>
<h2 id="📕-rag란">📕 RAG란?</h2>
<p>일단 약자 먼저 알아보자.</p>
<p><strong>R</strong>etrieval-<strong>A</strong>ugmented <strong>G</strong>eneration</p>
<p>이라서 <em>RAG</em> 다.</p>
<p>그래서 이 RAG는 무엇이냐면, 위에서 간단히 설명한 LLM의 응답을 생성하기 전, 이미 이 LLM 녀석이 <strong>학습해둔 데이터 외에 신뢰할 수 있는 지식 베이스</strong>를 참조하도록 하는 일종의 프로세스이다!</p>
<p>사실 이렇게 설명해도 잘 이해가 안 될 수도 있다. 
뭐 <strong>파인튜닝과 차이</strong>도 궁금하실 수도 있고..</p>
<p>먼저 졸작을 마치고 조언을 구한 선배님의 예시를 잠시 빌려서 여기에 사용해보면, </p>
<p>동일한 AI 모델이 있고, 하나의 시험을 본다고 생각할 때</p>
<ul>
<li><p>파인튜닝은 모델이 스스로 공부를 좀 더 한 것이다. </p>
</li>
<li><p>반면에 RAG는 모델에게 작은 커닝 페이퍼 하나가 주어지는 셈이다.</p>
</li>
</ul>
<p>대충 이런 느낌으로 생각하면 그나마 이해하기 쉬울 것 같다..
이해가 어려우셨다면..죄송합니다 저도 사실 그렇게 잘 아는 편은 아니에요😢</p>
<p>아무튼, 파인튜닝과 RAG에 대해서 더 알아본 내용은 다음과 같다.</p>
<ul>
<li><p>파인튜닝은 새로운 데이터로 모델을 일부 수정, 업데이트하는 작업인 것 같다.</p>
</li>
<li><p>RAG는 모델에 대규모 코퍼스(커닝 페이퍼)를 추가해서, 주어진 입력을 기반으로 관련 문서, 정보를 가져온다. 
➡️ 이것을 통해서 모델을 따로 수정, 새로 학습할 필요가 없다. 다만, 딸려있는 이 코퍼스에서 데이터를 검색하는 비용이 추가로 든다.(커닝 페이퍼에서 유사한 내용을 찾는 시간)</p>
</li>
</ul>
<h2 id="📖-rag의-이점">📖 RAG의 이점?</h2>
<p>RAG는 이와 같은 이점을 가진다!</p>
<ul>
<li><p><strong>비용이 효율적이다.</strong>
➡️ 챗봇은 보통 FM(파운데이션 모델)이라는 LLM을 이용하여 개발된다. 근데, 이눔을 다시 학습시키는 데에는 비용이 많이 드는 반면, RAG는 이에 비하면 새 데이터를 모델에 도입하기 훨씬 간편하다.</p>
</li>
<li><p><strong>최신 정보를 지속적으로 갱신할 수 있다.</strong>
➡️ 위와 유사한 이유로, 새 데이터를 도입하기 쉬워진만큼 모델의 데이터 추가 주기도 더 짧아질 수 있다!</p>
</li>
<li><p><strong>개발자의 모델 제어가 더 쉬워진다.</strong>
➡️ RAG를 사용해 테스트가 효율적이게 된다! LLM이 잘못된 정보 소스를 참조하는 등의 문제를 해결하고, 수정할 수도 있다.</p>
</li>
</ul>
<h2 id="rag의-작동-방식">RAG의 작동 방식</h2>
<p>그렇다면, RAG는 어떻게 작동하는 걸까?</p>
<p>기존의 LLM은 사용자에게 입력을 받고, 훈련한 정보 또는 이미 알고 있던 정보를 기반으로 응답을 만들지만,</p>
<p>RAG는 이 과정 중간에 <strong>사용자 입력으로 먼저 코퍼스에서 정보를 가져오는 작업</strong>이 추가됬다고 생각하면 된다.</p>
<p>이제, 이 RAG의 작동 방식을 분석해보면 아래와 같다.</p>
<ul>
<li><p>외부 데이터 생성
➡️ 모델 외부에 존재하는 데이터 소스들(EX: API, DB, 문서 등)을 정제해서 데이터(EX: 특정한 형식의 파일, DB 레코드, 긴 형식의 텍스트)로 만듦. 혹은 <strong><em>임베딩 언어 모델</em></strong>이라는 방법을 이용해 데이터를 수치로 변환하고, 벡터 DB에 저장한다.  </p>
</li>
<li><p>관련 정보 검색
➡️ 사용자가 입력한 데이터가 벡터 표현으로 바뀌고, 벡터 DB와 매칭되게 된다. (EX: 직원이 <em>제 연차 며칠 남았죠?</em> 라는 질문을 통해 검색하면, 개별 직원의 과거 휴가 기록과 연차 휴가 정책 문서를 검색하는 방식)
즉 사용자가 입력한 내용과 매우 연관 있는 데이터를 검색, 반환한다.</p>
</li>
<li><p>LLM 프롬프트 확장
➡️ RAG 모델이 검색을 통해 받아온 데이터들을 컨텍스트에 추가하여 사용자 입력을 보강한다.(신속한 엔지니어링 기술을 이용한다는데, 이건 개발하는 사람 기량 차이인가? 잘 모르겠다.) 이후는 기존 LLM과 동일하게 프롬프트를 기반으로 응답을 생성한다.</p>
</li>
<li><p>외부 데이터 업데이트
➡️ 아예 동적으로, RAG의 데이터들을 비동기 방식으로 업데이트하는 방식을 채택할 수 있다. (자동화 실시간 프로세스 혹은 주기적 배치 처리)
RAG의 기반이 되는 데이터의 변경 관리라고 생각하면 편하다!</p>
</li>
</ul>
<p><em>RAG를 이용한 LLM의 FLOW</em></p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/6e338c0a-8c55-4fd8-92ad-7751370992e1/image.png" alt=""></p>
<h2 id="정리를-마치며">정리를 마치며..</h2>
<p>사실.. AI에는 큰 관심이 없던 무지한 개발자 지망생으로써 공부하는데 좀 거리낌이 있었지만..잘 정리된 문서를 보면서 읽어보며 이해하다 보니 어느정도 감이 잡힌 것 같다!
(아예 이해 못한 것 보단 훨씬 수확이 있다고 생각한다!!)</p>
<p>이 글의 참고 문서</p>
<blockquote>
<p><a href="https://aws.amazon.com/ko/what-is/retrieval-augmented-generation/">https://aws.amazon.com/ko/what-is/retrieval-augmented-generation/</a></p>
</blockquote>
<p>혹시나 오늘도 이 긴 글을 읽어주신 분들께 </p>
<p>정말 감사드립니다..! 좋은 하루 되세용😀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2에 JAVA 설치 및 환경 변수 설정하기]]></title>
            <link>https://velog.io/@codemaker-kim/AWS-EC2%EC%97%90-JAVA-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@codemaker-kim/AWS-EC2%EC%97%90-JAVA-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 01 Dec 2024 15:40:16 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<hr>
<p>이번 학기에 듣는 수업이 과제부터 시험까지 모두 프로젝트 기반으로 점수가 매겨진다. (배포된 프로젝트를 채점)</p>
<p>이 수업에서는 지원해주는 비용으로 오직 AWS EC2만을 이용해서 배포를 실행하도록 지원해주는데, </p>
<p>한 가지 정말 불편한 점이 있었다면, 과제와 같은 하나의 프로젝트를 채점하고 나면,</p>
<p><strong>다시 새로운 EC2 인스턴스 환경에 배포하라는 규칙</strong>이 있어서, JAVA 설치부터, 환경 변수 설정까지 싹 다 다시해야 하는게 너무 귀찮았다.</p>
<p>무엇보다, 매번 배포를 진행할 때마다 순탄하게 흘러가지 않고 항상 어디선가 막혀서 꼭 구글링을 다시 하게 되었다.</p>
<p>그래서 EC2에 간단하게 배포하는 법을 복기할 겸하여 이 글을 작성한다.
<br></p>
<h2 id="ec2-접속하기">EC2 접속하기</h2>
<hr>
<p>  우선 EC2 인스턴스에서 사용하는 키의 권한을 설정해주어야 한다.
  키 파일 종류는 PEK(<del>맞는지 기억 안남</del>)와 PEM 키 이렇게 2종류가 있던 걸로 기억하는데, 보통 PEM 형식을 많이 쓰는 것 같다.</p>
<p>  이 글에서도 .pem 키를 이용해서 EC2에 접근하는 방법을 정리한다.</p>
<p>  우선 pem 키의 권한을 설정해주었다.
  (설정해주지 않으면, 키의 권한이 너무 개방적이어서 EC2 접근이 제한되는 것 같음.)
  <br></p>
<p>  <strong>그렇게 발생한 에러</strong> ⬇️
  <img src="https://velog.velcdn.com/images/codemaker-kim/post/99aa35c6-27e5-4bc6-aa8f-80901a9a38e3/image.png" alt=""></p>
<p> PEM키 읽기 권한만 설정하는 명령어. (Linux)</p>
<blockquote>
<p><code>chmod 400 {.pem 키 이름}</code></p>
</blockquote>
<p>PEM키 읽기 권한만 설정하는 명령어. (Window)
    (<del>사실 윈도우 명령어가 잘 기억이 안나서 메모 겸</del>)</p>
<blockquote>
<p><code>icacls {파일 경로} /grant:r {사용자명}:R</code></p>
</blockquote>
<p> 이렇게 pem 키에 접근 권한 설정을 마치면, ssh 명령어를 통해 EC2 콘솔로 접근이 가능해진다!</p>
<p><em><strong>추가</strong></em>
이 .pem키의 권한이 이미 많이 열려있다면</p>
<blockquote>
<p><code>icacls {PEM키 경로} /inheritance:r</code></p>
</blockquote>
<p>을 실행하면 된다. 이 명령어는 현재 모든 사용자에 대한 권한 제거 +  파일이 부모 폴더에서 권한을 상속받지 않도록 설정한다.</p>
<p>SSH 접속 명령어</p>
<blockquote>
<p><code>ssh -i {PEM키 경로} {사용자명}@{EC2 인스턴스 PUBLIC IP}</code></p>
</blockquote>
<p>참고로, PEM키 경로는 상대 경로를 사용해도 되고, 사용자명은 EC2 인스턴스의 운영체제 종류에 따라 조금씩 다른데, 내가 알고 있는 사용자명은 이정도다.</p>
<ul>
<li><strong>Amazon Linux2</strong>: ec2-user    </li>
<li><strong>ubuntu</strong>: ubuntu</li>
</ul>
<br>

<p>아무튼 무사히 접속을 완료했다.<br></p>
<p><img src="https://velog.velcdn.com/images/codemaker-kim/post/96096a9c-2eba-4600-b1e6-966ab2f62216/image.png" alt=""></p>
<br>    

<h2 id="java-설치하기">JAVA 설치하기</h2>
<hr>
<p>이것도 위에서 언급한 것처럼 운영체제마다 사용하는 명령어가 다른데,</p>
<p>Amazon Linux 2의 경우에는</p>
<blockquote>
<p><code>sudo yum install java-11-openjdk-devel</code></p>
</blockquote>
<p>ubuntu의 경우에는 </p>
<blockquote>
<p><code>sudo apt update</code> 
    <code>sudo apt install openjdk-11-jdk</code></p>
</blockquote>
<p>apt update 명령어를 실행하는 이유는 apt가 업데이트되지 않아서 깔릴 jdk도 안 깔리는 문제가 생길 수도 있기에, 해주는 것도 좋은 방법이다.
    (<del>내가 당한 일</del>)</p>
<p>우선 명령어가 실행되고 나면, <strong>[y/n]</strong> 으로 긴 영어 문구가 위협하는데, </p>
<p>난 jdk를 다운받을거니까 y 입력해주면 된다. </p>
<p>대충 기다린 후 다 된 것 같다 싶으면</p>
<p><code>java --version</code> 명령어로 잘 설치되었는지 확인해주면 끗!</p>
<p>나는 jdk21을 받아주었다.
<img src="https://velog.velcdn.com/images/codemaker-kim/post/953c208e-69b4-40a5-a1ed-364ce4c57830/image.png" alt=""></p>
<h2 id="환경변수-지정해주기">환경변수 지정해주기</h2>
<hr>
<p>대망의 마지막이다. EC2 인스턴스에 환경 변수를 설정한다.</p>
<p>환경 변수를 지정해주는 방법도 나름 여러가진데, </p>
<p>전역적으로 설정하려면 /etc/profile 파일에 환경 변수를,</p>
<p>특정 사용자에 대해 설정하려면 특정 사용자의 홈 디렉토리 하위에 있는 .bashrc 파일을 편집하면 된다.</p>
<p>이 글에서는 /etc/profile 에 java 환경변수를 추가하려고 한다.</p>
<p>이렇게 파일 끝에 이 값들을 추가해준다.</p>
<blockquote>
<p><code>export JAVA_HOME={JAVA 설치경로}</code>
<code>export PATH=$PATH:$JAVA_HOME/bin</code></p>
</blockquote>
<p>여기서, JAVA 설치 경로는 <code>readlink -f $(which java)</code> 명령어로 나온 경로를 넣어주면 된다!</p>
<p>작성하고 저장한 파일을 반영하는 명령어를 실행해주자.</p>
<blockquote>
<p><code>source /etc/profile</code></p>
</blockquote>
<p>이러고 <code>echo $JAVA_HOME</code> 명령어와 <code>echo $PATH</code> 명령어로 환경변수가 잘 지정되었는지 확인해보자!</p>
<p>무언가가 잘 나온다면, 환경 변수 지정은 성공한 것이다.</p>
<p>만약 환경 변수 지정했는데도 echo 명령어로 확인할 수 없다면, <strong>EC2 인스턴스를 재부팅</strong>해보거나, 재접속해보자.</p>
<h2 id="결론">결론</h2>
<hr>
<p>사실 정말 맘 잡고 하면 간단한 일이지만, 해놓았던 환경설정을 다시 하고 또 하는게 정말 번거롭고 매번 할 때마다 가물가물한게 더 짜증이 났었는데, </p>
<p>이렇게 정리하고 복기해보니 좀 더 익숙해진 기분이 든다.</p>
<p>오늘도 혹시나 이 길고 영양가 없는 글을 끝까지 읽어주신 분이 있다면,</p>
<p>정말 감사드립니다😊 좋은 하루 되세요!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트를 이용해 이미지를 저장, 불러오기를 경험하며.. ]]></title>
            <link>https://velog.io/@codemaker-kim/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EC%A0%80%EC%9E%A5-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B8%B0%EB%A5%BC-%EA%B2%BD%ED%97%98%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@codemaker-kim/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EC%A0%80%EC%9E%A5-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B8%B0%EB%A5%BC-%EA%B2%BD%ED%97%98%ED%95%98%EB%A9%B0</guid>
            <pubDate>Sat, 23 Nov 2024 13:42:12 GMT</pubDate>
            <description><![CDATA[<h3 id="개요">개요</h3>
<hr>
<blockquote>
<p><strong>아니 그냥 이미지 url로 따서 그거 db에 넣으면 되는거 아님? 🧐</strong></p>
</blockquote>
<p>팀 프로젝트를 진행하며, 사용자 프로필 이미지 저장, 처리를 해야된다고 들었을 때의 나의 생각이었다.</p>
<p>직접 해보기 전까진 말이다..</p>
<h3 id="뭐가-문젠데">뭐가 문젠데?</h3>
<hr>
<p>사실 이 아이디어가 완전히 틀리다 -&gt; 이건 아니긴 했다. 정확히는 반은 맞고, 반은 틀리다 에 가까웠다.</p>
<p>지금부터 내가 공부하고 겪은 시행착오에 대해 복기해보고자 이 글을 적는다!</p>
<h3 id="이미지-저장-방식">이미지 저장 방식</h3>
<p>DB에 이미지 파일을 저장하는 방식은 두 가지라고 한다.</p>
<blockquote>
<ol>
<li><strong>이미지 데이터 자체</strong>를 저장하는 방법.</li>
</ol>
</blockquote>
<ul>
<li>이렇게 저장되는 이미지를 BLOB 객체라고 하며, 이진값으로 저장되게 된다.</li>
<li>데이터 백업이나 처리가 간단하다.</li>
<li>이미지를 통째로 넣기에, DB나 서버에 부담이 간다.</li>
<li>과정이 복잡하고, 비효율적이다.</li>
</ul>
<p>➡️ 이 방법은 개인적으로 이번 프로젝트에서 사용하자는 생각은 들지 않았다. 애초에 DB에 부담가는게 싫기도 했고(<em><del>RDS가 본인 사비로 나가고 있기도 하고</del></em>), 잘 안 쓴다고도 해서..무엇보다 내가 생각했던 것과 비슷한 2번째 방법이 훨씬 낫다는 생각이 들기도 했다.
(물론 이렇게 처리하는 방법도 배워야겠다는 생각도 들었다.)</p>
<blockquote>
<ol start="2">
<li>이미지는 저장소 다른 어딘가에 저장하고, 이미지가 저장된 경로를 데이터베이스에 저장한다.</li>
</ol>
</blockquote>
<p>➡️ 이 방법이 내가 위에서 말했던 반은 맞고, 반은 틀리다고 했던 방법이다. 단순히 경로만 따서 가져오는 것이 아니라, 사용자가 업로드한 이미지를 내 <strong>서버에서 접근할 수 있는 경로로 저장</strong>하고, 그에 대한 경로를 DB에 저장해야 했기 때문이다. (서버에 이미지를 저장해두는 것까진 아예 생각하지 못했었음😮)</p>
<p>우선 내가 생각했던 방식에 가까운 2번 방식을 채택해 기능을 구현하기로 결정했다.</p>
<h3 id="처리-과정-📕">처리 과정 📕</h3>
<p>이 방식을 알아보고 내가 생각했던 방식은 간단했다.</p>
<p>사용자가 업로드한 이미지 파일은 
➡️ EC2 서버 내에 디렉토리를 하나 만들어 저장하고, </p>
<p>이 EC2에 생성된 디렉토리에 대한 경로를 접근할 수 있게 하여 이미지를 보여줄 수 있게 API를 생성하고,</p>
<p>사용자 프로필 이미지 정보에는 
➡️이미지에 대한 URL를 저장할 수 있게 하는 것이다.</p>
<h3 id="스프링-부트에서-처리">스프링 부트에서 처리?</h3>
<p>스프링 부트에서는 MultipartFile이라는 클래스가 제공되는데, </p>
<p>이 녀석은 form 형식으로 파일이 날아오면, 날아온 파일을 이 타입으로 사용한다.</p>
<p>그 후에 transferTo() 메서드를 거쳐 JAVA의 File 객체로 변환이 가능해진다.</p>
<h3 id="저장-로직">저장 로직</h3>
<p>Hayden 님의 블로그에 있는 예제와 이미지를 참고하였습니다!</p>
<p><img src="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6X4pg%2FbtsHPHYmPwG%2FBQlQSozMbSuLVHR81gZyvK%2Fimg.png" alt="저장 로직"></p>
<p>사실 서비스-이미지 핸들러 부분만 제외하면 나머지는 스프링 백엔드 구조와 큰 차이는 없다.</p>
<p>1.사용자는 이미지 파일을 POST 요청으로 서버에 전송한다. 이때 데이터 타입은 <strong><em>multipart/form-data</em></strong> 형식으로 전송한다.</p>
<ol start="2">
<li><p>컨트롤러는 해당 요청을 받는다. 이때 저장할 이미지를 MultipartFile 형태로 받아와 서비스 계층에 넘겨준다.</p>
</li>
<li><p>서비스는 먼저 전달받은 이미지를 서버 PC에 저장한다. 이때 파일 저장 과정은 ImageHandler 클래스에게 위임한다.</p>
</li>
<li><p>ImageHandler는 이미지를 저장한 후 이미지가 저장된 경로를 리턴한다. 서비스는 이미지 저장 후 리턴 받은 이미지 저장 경로를 레퍼지토리 계층에 넘겨준다. 이때 이미지 경로는 Entity 형태로 전달된다.</p>
</li>
<li><p>리포지토리는 전달 받은 경로를 DB에 저장한다.</p>
</li>
</ol>
<h3 id="시행착오">시행착오</h3>
<p>그래서 위 예제 부분을 참고해서 필요한 부분만 발췌해서 코드를 작성했다.</p>
<p>그런데 웬걸, 400 에러가 계속해서 뜬다. </p>
<p>당시 내가 이미지 핸들링을 위해 컨트롤러 메서드로 받은 값들의 형태는 이랫다.</p>
<pre><code class="language-java">    @ResponseBody
    @PostMapping(&quot;엔드포인트&quot;)
    public ResponseEntity&lt;String&gt; signUp(
            @RequestParam String name,
            @RequestParam String nickname,
            @RequestParam boolean sex ,
            @RequestParam Age age,
            @RequestParam String intro,
            @RequestParam MultipartFile profileImg,
            @RequestHeader String sub
    ) {
    // 기능 구현..
    }</code></pre>
<p>으와..보기만해도 너무 조잡했다. 사실 위 예제에서 formData를 파라미터로 받았기에, </p>
<blockquote>
<p>fromData 처리는 파라미터로 진행하면 되는구나!</p>
</blockquote>
<p>라는 안일한 생각으로 같이 작업하고 있던 프론트엔드 팀원에게 같이 테스트해보자고 요청했다.</p>
<p>위에서 언급했듯이..400 에러가 났다.😢</p>
<p>프론트엔드 쪽에서는 제대로 Multipart/form-data 형식으로 Content-Type을 지정해주어 요청을 보냈다는데 뭐가 문제일까 싶어 고민하다가, </p>
<p>원래 사용하던 DTO 객체를 전달받는 방식에 </p>
<p>이 객체를 @RequestBody 어노테이션으로 받아와야겠다는 생각이 들어 이렇게 처리했다.</p>
<p>그렇게 바꾼 두 번째 코드</p>
<pre><code class="language-java">@ResponseBody
@PostMapping(&quot;엔드포인트&quot;)
public ResponseEntity&lt;String&gt; signUp(
@RequestHeader String sub, 
@RequestBody AddUserRequest request) {
    // 기능 구현..
}
</code></pre>
<p>딱히 기대는 안하고 다시 프론트 팀원과 테스트.</p>
<p>오잉? 이번엔 415 에러가 났다.</p>
<p>415 에러는 발생하는 데 다양한 이유가 있지만, 주된 이유는 &#39;Content-Type 불일치&#39; 라는 것을 확인했다.</p>
<p>사실 이때 적잖아 당황했다.🙁 </p>
<p>그동안 처리해왔던 데이터 타입이 대부분 JSON 방식을 이용했었기에, formData에 대한 무지식이 발목을 붙잡았다. </p>
<p>무엇보다 이때까지 요청의 데이터 타입을 컨트롤러에 지정해주는 방식을 인지하지 못하고 있었다.</p>
<p>그렇게 컨트롤러에 formdata 처리 방식을 추가하여 다시 시도해보기로 했다.</p>
<p>그렇게 완성한..세 번째 코드다.</p>
<pre><code class="language-java"> @ResponseBody
 @PostMapping(value = &quot;엔드포인트&quot;, 
       consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
    public ResponseEntity&lt;String&gt; signUp(@RequestHeader String sub, 
    @RequestBody AddUserRequest request) {
        // 기능 구현..
    }</code></pre>
<blockquote>
<p>이젠 됬겠지?? 🙏</p>
</blockquote>
<p><strong>&#39;오!&#39;</strong>
테스트를 진행해본 프론트 팀원의 반응에 매우 기대했다.</p>
<p>아쉽게도 성공은 아니었지만, 415 에러는 아니었다는 점이다!</p>
<p>문제는 발생한 에러가 400 에러였다는 것이다. </p>
<p><del>당연히</del> 프론트와 백 둘 중 하나의 문제는 맞았는데, 
프론트 팀원은 자신은 확실하게 네가 원하는 요청 양식을 다 맞춰서 보내주었다고 말했다.</p>
<p>사실 나도 아예 문외한은 아니었기에 요청 양식을 확인하기 위해 프론트엔드 코드를 같이 복기해보기로 했다.</p>
<p>내가 보기에도 역시 별 문제는 없었기에, </p>
<p>아무래도 &#39;또 난가?&#39; 싶었다.</p>
<p>다시 찬찬히 되짚어보다, 차라리 formdata 요청 방식을 좀 더 정확하게 알아야겠다 싶었다.</p>
<p>그렇게 되짚다보니 </p>
<p>&#39;우리가 생각한 요청구조&#39; 는 동일했지만, 내가 작성한 코드가 우리의 요청방식을 받지 못한다는 사실을 알게 되었다..</p>
<p>그 사실을 알고 다시 고친 4번째 코드다.</p>
<pre><code class="language-java">    @ResponseBody
    @PostMapping(value = &quot;엔드포인트&quot;, 
    consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
    public ResponseEntity&lt;String&gt; signUp(
            @RequestPart(&quot;profileImg&quot;) MultipartFile profileImg,
            @RequestPart(&quot;addUserRequest&quot;) AddUserRequest request
    ) {
        //기능 구현..
    }</code></pre>
<blockquote>
<h2 id="됬다">됬다!!</h2>
</blockquote>
<p>결국 Spring의 formData 처리 방식에 대한 무지로 인한 내 잘못이 컸다.</p>
<p>이 과정을 통해 알게된 것들을 간단하게 정리하자면 </p>
<ol>
<li><p>@RequestBody는 formdata 방식을 처리하는데 적합하지 못하다.</p>
</li>
<li><p>formdata 요청 처리는 @RequestPart 어노테이션에 키 값을 지정해주어 처리하는 게 좋다.</p>
</li>
<li><p>요청 타입 설정도 컨트롤러에서 가능하다. (매핑 어노테이션에 cousumes = {MediaType.<em>요청 타입 이름</em>})</p>
</li>
</ol>
<h3 id="그래서-이미지는">그래서 이미지는?</h3>
<p>여기까진 마냥 좋았다. 내 집도 아니고 친구집에서 새벽까지 잠도 안자고 팀원과 같이 작업을 하다가</p>
<p>다음날 시원하게 늦잠까지 때려버리니 기분이 매우 상쾌했다.😀</p>
<p>다만 이 생각이 조금 마음에 걸렸다.</p>
<blockquote>
<p><em>그래서 이미지 어떻게 브라우저에 띄울건데?</em></p>
</blockquote>
<p>처음에는 뭐 별거 아니지 않을까? 라는 생각이었다. </p>
<p>&#39;서버에 있는 이미지가 저장된 경로를 환경변수로 지정해서, 이미지를 받아올 수 있게 해야지&#39;</p>
<p>라는 생각하며 </p>
<p>return 타입을 Resource 타입을 준다고 생각하고 코드를 작성했다.</p>
<p>그 전에 서버 배포 시에 이미지 받아올 용으로 쓸 경로를 config에 설정해주었다.</p>
<pre><code class="language-java">    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(&quot;/uploads/**&quot;)
                .addResourceLocations(img_save_path);
    }</code></pre>
<p>이렇게 하면 서버경로+/uploads/ 밑으로 오는 요청은 img_save_path에 저장된 경로 값을 기반으로 처리한다.</p>
<p>img_save_path 변수는 스프링 부트에서 제공하는 @Value 어노테이션을 이용하여 환경변수의 값을 받아와주었다.</p>
<p>참고로 @Value 어노테이션은 전역 변수에만 사용할 수 있다.</p>
<p>아무튼 그래서 작성한 컨트롤러 코드는 이랫다.</p>
<pre><code class="language-java">    @ResponseBody
    @GetMapping(&quot;엔드포인트&quot;)
    public ResponseEntity&lt;Resource&gt; showProfileImg(@PathVariable String profileImg) 
    throws MalformedURLException {
        Path imgPath = Paths.get(img_save_path, profileImg).toAbsolutePath();
        if (!Files.exists(imgPath)) {
            throw new MalformedURLException(&quot;File not found: &quot; + imgPath);
        }
        return ResponseEntity.ok()
        .body(new UrlResource(imgPath.toUri()));
    }</code></pre>
<p>이런 식으로 반환을 했던 것 같다.</p>
<p>물론 포스트맨 테스트 중에는 200 코드로 작동은 정상적으로 하는 것을 확인했지만,</p>
<p>결과값에는 알 수 없는 바이너리 값들만 브라우저에도, 포스트맨에도 뜰 뿐이었다.</p>
<p>그래서 프론트 팀 쪽에서 이 값을 파싱해줄 수 있나? 라는 생각을 했다가</p>
<p>너무 말도 안된다고 생각이 들었다. </p>
<p>보통 이미지 url로 들어가면 이미지 자체가 나오지 바이너리 값이 나오는 건 아니니까..</p>
<p>그래서 파싱 과정을 거쳐주었다.</p>
<p>파싱까지 거친 코드 ⬇️</p>
<pre><code class="language-java"> @ResponseBody
 @GetMapping(&quot;엔드포인트&quot;)
public ResponseEntity&lt;Resource&gt; showProfileImg(@PathVariable String profileImg) 
throws MalformedURLException {
        Path imgPath = Paths.get(img_save_path, profileImg).toAbsolutePath();

        if (!Files.exists(imgPath)) {
            throw new MalformedURLException(&quot;File not found: &quot; + imgPath);
        }
        Resource resource = new UrlResource(imgPath.toUri());

        String contentType;
        try{
            contentType = Files.probeContentType(imgPath);
        } catch (Exception e) {

            //자동 변환이 불가능할 경우의 기본 타입 지정
            contentType = &quot;application/octet-stream&quot;;
        }
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_TYPE, contentType)
                .body(resource);
    }
</code></pre>
<p>이렇게 API를 최종적으로 구성해주니, 브라우저 창에서도 이미지가 잘 나오는 것을 볼 수 있었다.</p>
<p>추가로 이미지 핸들링을 위해 사용한 핸들러 코드는 이렇다.</p>
<pre><code class="language-java">    public String createImgURL(MultipartFile image) throws IOException {
        String filename = image.getOriginalFilename();
        String newUrl = &quot;절대 경로&quot; + filename;

        image.transferTo(new File(newUrl));

        return newUrl;
    }
}</code></pre>
<h3 id="느낀-점">느낀 점</h3>
<hr>
<blockquote>
<p><strong><em>프로필 이미지 따위? 이미지 님이 니 친구냐?</em></strong></p>
</blockquote>
<p>이미지 처리를 너무 쉽게 보고 아예 제대로 된 사전 조사나 준비조차 안하고 들어가니 약간의 시행착오가 있었지만,</p>
<p>나름 문제를 해결하고 이렇게 복기해보니 이 기능을 개발하며 배운 부분이 꽤 많다는 사실을 깨달았다.</p>
<p>결론은 개발하기 전에 사전으로 이론에 대해 정확히 아는 것도 매우 중요하다는 것을 가장 크게 느꼈던 것 같다.</p>
<p>개발하면서 찾아보는 것도 좋지만, 너무 조급하게 개발을 시작하려 했던 내 잘못이 있는 것 같기도 했다.</p>
<p>다른 선배님한테 조언을 구해보니 아예 내가 이미지를 저장하려는 방식과 다르게 처리하신 선배님도 계셨다.(코드까지 공유해주신..👍)</p>
<p>파일 저장 용량 제한 등의 구현은 제대로 못한채 하드코딩한 느낌이 확 들었지만..</p>
<p>아무튼 나름 즐거운 경험이었다.😀</p>
<p>혹시나 이 기나 길고 영양가 없을지 모르는 글을 끝까지 읽어주신 분들은 </p>
<p>정말 감사할 따름입니다.</p>
<p>보신 분들 모두 좋은 하루 보내셨으면 하네요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NullPointerException에 호되게 당한 일]]></title>
            <link>https://velog.io/@codemaker-kim/NullPointerException%EC%97%90-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%8B%B9%ED%95%B4%EB%B2%84%EB%A6%B0-%EB%82%A0</link>
            <guid>https://velog.io/@codemaker-kim/NullPointerException%EC%97%90-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%8B%B9%ED%95%B4%EB%B2%84%EB%A6%B0-%EB%82%A0</guid>
            <pubDate>Tue, 29 Oct 2024 14:35:10 GMT</pubDate>
            <description><![CDATA[<p>때는 바야흐로 2024중간고사 프로젝트 시험을 치던 중이었다.</p>
<p>웹서버를 MVC 구조를 사용해서 특정한 서비스를 구현하는 과제였는데,</p>
<p>근거없는 자신감을 가지고 공부를 1도 안하고 시험에 드가게 되었다.</p>
<p>.
.
.
그러다 어느 시점에서 계속 NullPointerException (이하 NPE)이 발생하는 것이다..</p>
<p>아무래도 졸리면 집중력이 확 떨어지는 스타일인지라, 내가 약 1~2시간 동안 작성해온 코드들을 복기하며 짚어보고, 또 되짚어보았다.</p>
<p>하지만 어림도 없지 NPE 나와버리기~❤️(🤮🤮🤮)</p>
<p>그래서 그 날의 개발은 거기까지하고 나 스스로와 시험을 위해서 쪽잠이라도 자기로 했다.
(<del>다음날 1교시 수업에 시원하게 지각함</del>)</p>
<p>수업을 졸며 들은 후 훨씬 맑아진 머리로 개발에 드가니 생각을 좀 더 넓게 펼칠 수 있었다.</p>
<p>그러다가 발견하게 된 에러인데..</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Controller extends HttpServlet{
    private SessionUtil util;
    private Service service;

    //메서드들
}</code></pre>
<p>약간 이런 형식의 코드였던 것 같은데, 위에 지정된 전역변수들이 애초에 new 명령어든 어떤 형식으로도 생성 / 선언되지 않아서 생긴 문제였다.</p>
<p>디버깅 툴을 돌리면서도 전역변수는 생각하지 않고 있다가 별 생각없이 스크롤을 위로 올리다 문제를 발견해버렸다.</p>
<p>사실 시간에 쫒기면서 개발을 하다보던 것도 있구, 스프링 부트로 하는 작업에 익숙해져 있었는지 별 생각없이 썻던 어노테이션이 큰 악영향을 미치고 말았다..(<del>핑계</del>)</p>
<p>사실 그냥 변수들을 final로 선언해주면 됬는데 그것마저도 생각을 못했나보다.</p>
<p>지금 복기해보아도 정말 너무 바보같은 실수라는 생각이 들긴하지만</p>
<p>이런 실수 하나하나가 마냥 나쁜 경험은 아니라고 생각이 들기도 했다. </p>
<p>이번 실수로 앞으로는 이런 일이 없을테니 말이다.</p>
<p>첫 에러 복기? 겸 일기가 되어 버린것 같다. </p>
<p>읽으신 분이 있으시다면 긴 글 읽어주셔서 감사합니다❤️</p>
]]></description>
        </item>
    </channel>
</rss>