<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>daram_dev.log</title>
        <link>https://velog.io/</link>
        <description>개발하는 다람쥐</description>
        <lastBuildDate>Fri, 10 Apr 2026 01:46:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>daram_dev.log</title>
            <url>https://velog.velcdn.com/images/daram_dev/profile/6a0199f3-427e-46e3-a50d-7c4906c060d3/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. daram_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/daram_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[DataLemur] Second Highest Salary]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Second-Highest-Salary</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Second-Highest-Salary</guid>
            <pubDate>Fri, 10 Apr 2026 01:46:38 GMT</pubDate>
            <description><![CDATA[<h1 id="1-문제-이해">1. 문제 이해</h1>
<p>이 문제는 전체 직원 중에서 두 번째로 높은 급여를 구하는 문제이다.</p>
<p>주의할 점은 다음과 같다.</p>
<ul>
<li>동일 급여가 있는 경우 하나만 출력</li>
</ul>
<h1 id="2-접근-방법">2. 접근 방법</h1>
<p>처음에 내가 접근했던 방법은 다음과 같다.</p>
<pre><code class="language-MySQL">SELECT MAX(salary) as second_highest_salary
FROM employee
WHERE salary != (
                SELECT MAX(salary)
                FROM employee
                );</code></pre>
<p>이렇게 문제를 풀었던 이유는</p>
<ul>
<li>전체에서 가장 높은 급여를 구한 다음</li>
<li>가장 높은 급여를 제외한 것들 중에서 가장 큰 급여를 찾기 위함 이였다.</li>
</ul>
<h2 id="수정할-점">수정할 점</h2>
<p>제출했을 경우 틀린 답은 아니라고 나오지만
이 방식대로 접근하게 된다면 약간의 수정할 부분이 있다고 생각했다.</p>
<h3 id="1-비교-연산자">1. 비교 연산자</h3>
<pre><code class="language-MySQL">WHERE salary != (
                SELECT MAX(salary)
                FROM employee
                );</code></pre>
<p>원래 작성했던 쿼리문도 틀린 쿼리문은 아니지만 <code>!=</code>을 사용하는 것 보다 <code>&lt;</code>를 사용하는 방법이 의미를 더 명확하게 보여주는 것 같다.</p>
<pre><code class="language-MySQL">WHERE salary &lt; (
                SELECT MAX(salary)
                FROM employee
                );</code></pre>
<h3 id="2-distinct">2. DISTINCT</h3>
<pre><code class="language-MySQL">SELECT MAX(salary) as second_highest_salary</code></pre>
<p>문제의 조건에서 동일한 급여가 여러 개있으면 하나만 출력하라고 명시되어 있다.
물론 <code>MAX()</code> 함수를 사용하기 때문에 1개만 나오기 때문에 문제는 없지만 의미를 더 명확하게 하려면 <code>DISTINCT</code>를 같이 사용해주는 것이 좋은 것 같다.</p>
<pre><code class="language-MySQL">SELECT MAX(DISTINCT salary) as second_highest_salary</code></pre>
<h1 id="3-최종-쿼리">3. 최종 쿼리</h1>
<pre><code class="language-MySQL">SELECT MAX(DISTINCT salary) as second_highest_salary
FROM employee
WHERE salary &lt; (
                SELECT MAX(salary)
                FROM employee
                );</code></pre>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/712860d8-ccfd-4535-a45c-cc05aaf1f077/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DataLemur] User's Third Transaction]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Users-Third-Transaction</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Users-Third-Transaction</guid>
            <pubDate>Thu, 09 Apr 2026 01:19:30 GMT</pubDate>
            <description><![CDATA[<h1 id="1-문제-이해">1. 문제 이해</h1>
<p>이 문제는 각 사용자별 우버 거래내역 중에서 세 번째 거래 내역을 조회하는 문제이다.</p>
<p>세 번째 거래 내역을 조회해야되기 때문에 사용자별로 거래 시간(transaction_date)를 정렬해줘야 된다.</p>
<h1 id="2-문제-핵심">2. 문제 핵심</h1>
<p>위에서 말했 듯이 여기서 중요한 점이 전체 데이터에서 3번째를 찾는 것이 아니고
각 사용자별로 3번째 거래 내역을 찾아야 한다는 점이다.</p>
<p>찾는 방법 순서는 다음과 같다.</p>
<ol>
<li>사용자별로 그룹을 나눈다.</li>
<li>거래 시간 기준으로 정렬을 수행한다.</li>
<li>순서를 매겨준다.</li>
<li>매겨진 순서 중에서 3번째만 고른다.</li>
</ol>
<p>그룹을 나누고, 순서를 매겨주기 위해서 윈도우 함수와 파티션 함수를 사용해 주어야 한다.</p>
<h1 id="3-최종-쿼리">3. 최종 쿼리</h1>
<pre><code class="language-MySQL">SELECT user_id, spend, transaction_date
FROM (
  SELECT user_id,
        spend,
        transaction_date,
        ROW_NUMBER () OVER (
          PARTITION BY user_id
          ORDER BY transaction_date
        ) as row_num
  FROM transactions
) a
WHERE a.row_num = 3
;</code></pre>
<h1 id="4-윈도우-함수window-function">4. 윈도우 함수(Window Function)</h1>
<h2 id="윈도우-함수-vs-group-by">윈도우 함수 VS GROUP BY</h2>
<p>윈도우 함수란 GROUP BY와 달리 행을 그룹화하지 않고, 테이블의 기존 행은 유지하면서 특정 범위 내에서 순위, 합계, 평균 등을 계산하여 각 행에 결과를 반환하는 함수다.
OVER 절을 사용해 데이터를 파티션하고 정렬하여 행 끼리의 관계를 분석한다.</p>
<p>일반적인 집계 함수인 GROUP BY와 간단하게 차이점을 살펴 본다면 다음과 같다.</p>
<h3 id="group-by">GROUP BY</h3>
<pre><code class="language-MySQL">SELECT user_id, spend, transaction_date, COUNT(*)
FROM transactions
GROUP BY user_id;</code></pre>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/63850507-414c-40ed-ba67-8a0168f7f5db/image.png" alt=""></p>
<p>GROUP BY를 사용하게되면 사용자별로 거래 횟수는 셀 수 있지만
원래 테이블의 정보들은 확인할 수 없게 된다.</p>
<h3 id="윈도우-함수">윈도우 함수</h3>
<pre><code class="language-MySQL">SELECT user_id, spend, transaction_date,
COUNT(*) OVER (
  PARTITION BY user_id
)
FROM transactions;</code></pre>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/cc043f27-a7d1-482b-aaa7-06b0c4a8f9dc/image.png" alt=""></p>
<p>윈도우 함수를 사용하게되면 사용자별로 정보들을 유지하면서 계산 결과도 확인이 가능하다.</p>
<h2 id="사용-함수-설명">사용 함수 설명</h2>
<h3 id="row_number">ROW_NUMBER()</h3>
<p><code>ROW_NUMBER()</code>는 각 행에 순서를 매겨주는 윈도우 함수다.</p>
<p>예를 들어서 어떤 사용자의 거래가 3건이 있는 경우 정렬 기준을 주면 그 기준에 맞춰서 1, 2, 3과 같은 번호를 붙여주는 것이다.</p>
<p>윈도우 함수의 형태는 다음과 같다.</p>
<pre><code class="language-MySQL">윈도우함수() OVER (
    PARTIION BY 기준컬럼
    ORDER BY 정렬기준컬럼 [AES/DES]
)</code></pre>
<h3 id="partition-by">PARTITION BY</h3>
<p><code>PARTITION BY</code>는 윈도우 함수에서 데이터를 특정 기준에 따라 여러 파티션으로 나누는 함수이다.
GROUP BY와 달리 원본 행을 유지하면서 각 그룹 내에서 순위, 합계, 평균 등을 계산할 수 있게 해준다.</p>
<p>문제에서 사용한 <code>PARTITION BY user_id</code>의 의미는 <code>user_id</code>가 같은 행끼리 하나의 그룹으로 파티션을 나누는 것이다.</p>
<h3 id="order-by">ORDER BY</h3>
<p>이 문제를 풀 때 꼭 정렬을 해주어야 한다. ROW_NUMBER() 함수는 순번을 붙이는 함수이기 때문에 어떤 기준을 가지고 정렬을 할지가 있어야되기 때문이다.</p>
<p>세 번째 거래를 찾아야 되기 때문에 거래 시간을 기준으로 정렬을 해주는 것이 중요하다.</p>
<h2 id="서브쿼리-사용한-이유">서브쿼리 사용한 이유</h2>
<p>윈도우 함수로 만들어낸 컬럼은 같은 SELECT문 안에서 WHERE 조건을 적용하여 바로 사용할 수 없다.(쿼리 실행 순서)</p>
<p>SQL문은 작성 순서대로 실행되는 것이 아니고 실행 순서가 정해져 있다.</p>
<pre><code>1. FROM
2. WHERE
3. GROUP BY
4. HAVING
5. SELECT
  + 추가
6. WINDOW FUNCTION
7. ORDER BY</code></pre><p>윈도우 함수는 SELECT 단계 이후에 계산된다.</p>
<p>그래서 먼저 서브쿼리를 만들어서 컬럼을 만든 뒤, 바깥쿼리에서 만들어낸 컬럼에 대한 조건을 적용한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/8d812b6c-647d-41c1-b73d-621680db2cb9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DataLemur] Pharmacy Analytics (Part 3)]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Pharmacy-Analytics-Part-3</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Pharmacy-Analytics-Part-3</guid>
            <pubDate>Wed, 08 Apr 2026 15:51:57 GMT</pubDate>
            <description><![CDATA[<h1 id="1-문제-이해">1. 문제 이해</h1>
<p>이 문제는 제조사별로 총 매출을 계산한 다음에 백만 단위로 반올림하는 문제다.
그리고 결과를 &#39;$00 million&#39;이 형식의 문자로 만들어줘야 한다.</p>
<p>문제에서 요구하는 조건들을 다시 정리하면 다음과 같다.</p>
<ul>
<li>제조사별 총 매출 계산</li>
<li>백만 단위 변환 및 반올림</li>
<li>&#39;$00 million&#39;형태로 출력</li>
<li>총 매출 기준 내림차순 정렬</li>
<li>총 매출이 같으면 제조사명으로 정렬</li>
</ul>
<h1 id="2-접근-방법">2. 접근 방법</h1>
<pre><code class="language-MySQL">SELECT manufacturer, concat(&#39;$&#39;, concat(round(sum(total_sales) / 1000000), &quot; million&quot;)) as sale
FROM pharmacy_sales
GROUP BY manufacturer
ORDER BY sum(total_sales) DESC, manufacturer
;</code></pre>
<p>위의 쿼리로 문제를 통과했다.</p>
<p>문제를 풀면서 헷갈렸던 부분을 정리해보면 다음과 같다.</p>
<h1 id="3-헷갈린-부분">3. 헷갈린 부분</h1>
<h2 id="문자열-포맷-처리">문자열 포맷 처리</h2>
<p>문제를 풀 때 문자열을 만들어줘야하는 데 <code>CONCAT</code>이 죽어도 떠오르지 않아서 쿼리를 완성하는 데 시간이 더 소요됐다..</p>
<h3 id="concat"><code>CONCAT()</code></h3>
<p>CONCAT() 함수는 둘 이상의 문자열이나 컬럼 값을 하나로 연결하여 반환하는 함수다.
CONCAT(str1, str2, ...) 형태를 사용하며 여러 인수를 순서대로 합쳐준다.
숫자 등 다른 데이터 타입도 문자열로 자동 변환하여 결합하는 것이 가능하다.
그러나 인수에 NULL이 포함된다면 결과도 NULL이 나오게 된다.</p>
<p>둘 이상의 여러 인수를 합친다는 것을 기억해야 될 것 같다.</p>
<p>문제를 풀어낼 때 concat(str1, str2) 이렇게만 생각해서 문자열 포맷처리 할 때 concat을 두 번이나 사용했다.
<code>concat(&#39;$&#39;, concat(round(sum(total_sales) / 1000000), &quot; million&quot;))</code></p>
<p>물론 위 처럼 풀이도 가능하지만
<code>concat(&#39;$&#39;, round(sum(total_sales) / 1000000), &quot; million&quot;)</code>
이렇게 concat을 중복해서 사용하지 않고 사용하는 방법으로 풀어주면 된다.</p>
<h1 id="3-수정-쿼리">3. 수정 쿼리</h1>
<pre><code class="language-MySQL">SELECT manufacturer, concat(&#39;$&#39;, round(sum(total_sales) / 1000000), &quot; million&quot;) as sale
FROM pharmacy_sales
GROUP BY manufacturer
ORDER BY sum(total_sales) DESC, manufacturer
;</code></pre>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/6928457b-bd8d-4fa9-a826-811bd459de02/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DataLemur] Pharmacy Analytics (Part 2)]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Pharmacy-Analytics-Part-2</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Pharmacy-Analytics-Part-2</guid>
            <pubDate>Tue, 07 Apr 2026 09:01:15 GMT</pubDate>
            <description><![CDATA[<h1 id="1-문제-이해">1. 문제 이해</h1>
<p>이 문제는 손실 발생한 drug를 기준으로 제조사별로 손실 정보를 집계하는 문제이다.</p>
<p>문제의 요구사항은 다음과 같다.</p>
<ul>
<li>손실 발생한 drug만 선택</li>
<li>제조사별 손실이 발생한 drug 개수</li>
<li>제조사별 총 손실 금액</li>
<li>손실 금액 기준 내림차순 정렬</li>
</ul>
<p>손실을 구하려면 이전 <code>total-sales - cogs</code>를 하면 된다.
여기서 음수로 나오는 값들을 조건으로 추리면 손실 금액 기준으로 조회를 하게 된다.</p>
<h1 id="2-접근-방법">2. 접근 방법</h1>
<h2 id="처음-접근-방법">처음 접근 방법</h2>
<pre><code class="language-MySQL">SELECT
  manufacturer,
  count(drug) as drug_count,
  abs(total_sales - cogs) as total_loss
FROM pharmacy_sales
where (total_sales - cogs) &lt; 0
group by manufacturer
order by total_loss desc
;
</code></pre>
<p>이렇게 쿼리를 짰던 이유는</p>
<ul>
<li>손실 방생 데이터만 필터링</li>
<li>제조사별 그룹화</li>
<li>손실 금액 계산</li>
<li>손실 기준 정렬
이 4가지를 생각해서였다.</li>
</ul>
<h2 id="문제점">문제점</h2>
<p>이렇게 짠 쿼리문은 문제를 통과하지 못했다. 결과값도 다르게 나왔다.
이유는 손실 금액을 합치지 않아서 였다.
<code>abs(total_sales - cogs)</code></p>
<p>제조사별로 여러 손실 난 drug가 존재할 수 있을 텐데 그 부분을 고려하지 못한 것이다.
제조사별 총 손실을 구해야 하는 데 개별 행별로 손실을 계산하고 마무리한 것이다.</p>
<p>개별 drug 별로의 손실로 정렬까지 진행해서 정렬 기준도 잘못 되었다.</p>
<p>쿼리를 수정하기 위한 핵심 부분은 다음과 같다.</p>
<ol>
<li>손실 데이터 기준 조건 필터링</li>
<li>제조사 기준 집계</li>
</ol>
<p><strong>3. 제조사별 손실 총합 계산</strong>
4. 손실 총합으로 정렬</p>
<h1 id="3-수정-쿼리">3. 수정 쿼리</h1>
<pre><code class="language-MySQL">SELECT
  manufacturer,
  count(drug) as drug_count,
  sum(abs(total_sales - cogs)) as total_loss
FROM pharmacy_sales
where (total_sales - cogs) &lt; 0
group by manufacturer
order by total_loss desc
;</code></pre>
<p>행별로 계산하던 손실을 총합으로 수정해줘서 제조사별 손실 총합을 구할 수 있게 되었다.
그리고 집계한 결과를 가지고 정렬도 수행했다.</p>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/57aad147-df6d-4429-8784-850c5d8cfe7c/image.png" alt=""></p>
<p>통과!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DataLemur] Pharmacy Analytics (Part 1)]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Pharmacy-Analytics-Part-1</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Pharmacy-Analytics-Part-1</guid>
            <pubDate>Mon, 06 Apr 2026 07:35:11 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-이해">1. 문제 이해</h2>
<p>이 문제는 각 drug의 수익을 계산한 후,
가장 수익이 높은 상위 3개의 drug를 찾는 문제이다.</p>
<p>문제에서 주어진 수익 공식은 다음과 같다.</p>
<ul>
<li>Total Profit = Total Sales - Cost of Goods Sold</li>
</ul>
<p>핵심은 다음과 같다.</p>
<ul>
<li>각 drug의 수익을 계산한다.</li>
<li>수익을 기준으로 내림차순 정렬한다.</li>
<li>상위 3개를 추출한다.</li>
</ul>
<h2 id="2-접근-방법">2. 접근 방법</h2>
<pre><code class="language-MySQL">SELECT drug,
       total_sales - cogs AS total_profit
FROM pharmacy_sales
ORDER BY total_profit DESC
LIMIT 3;</code></pre>
<p>문제에서 각 의약품은 단 한 곳의 제조사만 생산할 수 있다고 명시하고 있다.
또한 전체 수익을 합산하라는 조건도 없었기 때문에
각 row를 기준으로 수익을 계산하기만 하면 되는 문제라고 판단했다.
그래서 별도의 집계 함수를 사용하지 않았다.</p>
<p>상위에서 정리한대로 핵심 3가지를 지켜서 쿼리를 작성하면 되는 문제이다.</p>
<p><code>LIMIT</code>은 <code>SELECT</code> 문으로 데이터를 조회할 때 반환되는 행의 최대 개수를 제한하는 문법이다.
주로 상위 N개의 데이터를 조회할 때 함께 사용된다.
그래서 <code>LIMIT</code>을 활용하여 상위 3개를 조회하도록 했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DataLemur] Compressed Mean]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Compressed-Mean</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Compressed-Mean</guid>
            <pubDate>Fri, 03 Apr 2026 01:46:07 GMT</pubDate>
            <description><![CDATA[<p>이 문제는 주문당 평균 아이템 개수를 구하는 문제이다.
단순 평균을 계산하는 것이 아닌 가중 평균 개념을 사용한다.</p>
<p>공식은 다음과 같다.</p>
<ul>
<li>총 아이템 수 = item_count * order_occurrences의 합</li>
<li>총 주문 수 = order_occurrences의 합</li>
<li>평균 = 총 아이템 수 / 총 주문 수</li>
</ul>
<h2 id="1-옛날-방식">1. 옛날 방식</h2>
<pre><code class="language-sql">SELECT round(
        (sum(item_count * order_occurrences) / sum(order_occurrences)) * 10
    ) / 10 as mean
FROM items_per_order;</code></pre>
<h3 id="설명">설명</h3>
<p>이 방식은 소수점 처리를 직접 하는 방식이다.
    1. 평균값에 10을 곱한다.
    2. round로 반올림한다.
    3. 다시 10으로 나눈다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>PostgreSQL, MySQL 모두에서 동작이 가능하다.
  (-&gt; 문제에서 MySQL로 풀려고 했는데 테이블이 나오지 않아서 이 방식을 사용했다.)</li>
<li>타입 문제 없이 안정적으로 사용이 가능하다.</li>
<li>직관적이지 않고 쿼리문이 좀 길어지는 단점이 있다.</li>
</ul>
<h2 id="2-round-함수-사용">2. ROUND 함수 사용</h2>
<h3 id="mysql">MySQL</h3>
<pre><code class="language-MySQL">SELECT round(
    (sum(item_count * order_occurrences) / sum(order_occurrences))
    , 1
  )as mean
FROM items_per_order;</code></pre>
<h3 id="postgresql">PostgreSQL</h3>
<pre><code class="language-PostgreSQL">SELECT round(
    (sum(item_count * order_occurrences) / sum(order_occurrences))::NUMERIC
    , 1
  )as mean
FROM items_per_order;</code></pre>
<h3 id="설명-1">설명</h3>
<ul>
<li>round(x, 반올림 위치)는 반올림 위치의 자리까지 반올림을 한다.
round(x, 1)이면 소수점 첫번째 자리까지 반올림을 하는 것이다.</li>
<li>MySQL은 타입에 관계없이 잘 동작한다.</li>
<li>PostgreSQL은 numeric 타입으로 변환해서 사용해줘야 한다.</li>
<li>PostgreSQL에서는 round(x)는 double precision에서도 동작한다.</li>
<li>하지만 소수점 자리를 지정하는 round(x, n)은 numeric 타입에서만 지원되기 때문에 명시적인 형변환이 필요하다.
<img src="https://velog.velcdn.com/images/daram_dev/post/7072cfe9-0eee-4325-ab3f-392e09c97c1d/image.png" alt=""></li>
<li>여기서 추가로 알 수 있는 것은 numeric은 정확한 반올림을 수행하고, double precision은 근사값을 기반으로 반올림을 수행한다는 점이다.</li>
<li>따라서 정확한 소수 계산이 필요한 경우 numeric 타입을 사용하는 것이 적절하다.</li>
</ul>
<h2 id="3-mysql-vs-postgresql">3. MySQL vs PostgreSQL</h2>
<h3 id="타입-처리-방식">타입 처리 방식</h3>
<p>MySQL은 타입에 비교적 관대한 편이라고 한다.</p>
<ul>
<li>대부분의 경우 자동 형변환이 이루어져 편하게 사용할 수 있다.</li>
</ul>
<p>PostgreSQL은 타입에 엄격하다.</p>
<ul>
<li>타입이 맞지 않으면 에러가 발생한다.</li>
<li>명시적 캐스팅이 필요하다.
```</li>
<li><ul>
<li>PostgreSQL
ROUND(double precision, 1) # 에러 발생
ROUND(numeric, 1) # 정상 동작<pre><code>### 나눗셈 결과 차이
MySQL은 정수끼리 나눗셈을 하면 자동으로 실수의 결과가 나온다.
![](https://velog.velcdn.com/images/daram_dev/post/2fac9e36-76ab-4a53-b076-97fbc704af58/image.png)
</code></pre></li>
</ul>
</li>
</ul>
<p>PostgreSQL은 정수끼리 나눗셈을 하면 정수의 결과가 나온다.
<img src="https://velog.velcdn.com/images/daram_dev/post/b2242614-df19-4dac-9bb9-7c1ef637111a/image.png" alt=""></p>
<p>그래서 PosgreSQL에서는 정확한 소수 결과를 얻기 위해서는 numeric 타입으로 캐스팅을 해주는 것이 필요하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DataLemur] Second Day Confirmation]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Second-Day-Confirmation-yjcgzvgl</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Second-Day-Confirmation-yjcgzvgl</guid>
            <pubDate>Thu, 02 Apr 2026 02:43:43 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-이해">1. 문제 이해</h2>
<p>이 문제는 다음 조건을 모두 만족하는 user_id를 찾는 문제이다</p>
<ul>
<li>회원가입을 했다</li>
<li>가입 당일에는 인증하지 않았다</li>
<li>가입 다음 날에 인증(Confirmed)했다</li>
</ul>
<p>즉 핵심은 다음 두가지이다.</p>
<ul>
<li>인증 날짜가 가입 날짜 + 1일</li>
<li>인증 상태가 Confirmed</li>
</ul>
<h2 id="2-처음-접근-방법">2. 처음 접근 방법</h2>
<pre><code class="language-sql">SELECT user_id 
FROM emails as e
JOIN texts as t
ON t.email_id = e.email_id
WHERE e.signup_date != t.action_date
AND (date(t.action_date) - date(e.signup_date)) = 1;</code></pre>
<h3 id="접근-의도">접근 의도</h3>
<ul>
<li>emails와 texts를 email_id로 JOIN을 진행</li>
<li>가입 날짜와 인증 날짜를 비교</li>
<li>날짜 차이가 1이면 &quot;다음 날 인증&quot;이라고 판단</li>
<li>당일 인증을 제외하기 위해 <code>!=</code> 조건 추가</li>
</ul>
<h2 id="3-문제점">3. 문제점</h2>
<p>결과값은 동일해서 문제는 통과했지만 다시 쿼리문을 보니 문제가 있었다.</p>
<h3 id="1-불필요한-조건">1. 불필요한 조건</h3>
<pre><code class="language-sql">e.signup_date != t.action_date</code></pre>
<p>이미 날짜 차이가 1인 경우(date(t.action_date) - date(e.signup_date))만 필터링하고 있기 때문에 해당 조건은 의미 없이 중복된 조건이였다.</p>
<h3 id="2-핵심-조건-누락">2. 핵심 조건 누락</h3>
<p>문제 조건 중 중요한 부분이 인증 상태가 &#39;Confirmed&#39;여야 한다는 것인데
작성한 쿼리에 이 조건을 넣지 않았다.
이렇게 되면 그냥 날짜 차이가 1인 조건만 계산되어서 &#39;Not confirmed&#39; 데이터까지 포함될 수 있다는 점이다.</p>
<h3 id="3-날짜-계산-방식">3. 날짜 계산 방식</h3>
<pre><code class="language-sql">date(t.action_date) - date(e.signup_date)</code></pre>
<p>의도를 명확하게 하기위해서 함수를 사용하여 날짜를 계산하는 방법이 더 좋다고 판단했다.</p>
<h3 id="기존-쿼리-결과">기존 쿼리 결과</h3>
<p>결론적으로 기존 쿼리의 결과를 설명하면</p>
<ul>
<li>다음 날이 아닌 경우가 포함될 가능성 있음</li>
<li>Not confirmed 데이터 포함 가능
이러한 문제들이 존재한다.</li>
</ul>
<h3 id="실제-정답">실제 정답</h3>
<p>실제 문제에서 원하는 정답은 <strong>가입 다음 날에 Confirmed 상태인 사용자만 포함</strong>하는 것이라는 것을 다시 상기할 필요가 있었다.</p>
<h2 id="5-핵심-개념-정리">5. 핵심 개념 정리</h2>
<ul>
<li>날짜 비교는 명확한 함수 사용이 중요하다</li>
<li>조건은 문제에서 요구하는 단위로 정확히 분리해야 한다</li>
<li>JOIN 이후 어떤 데이터가 포함되는지 항상 고려해야 한다</li>
</ul>
<h2 id="6-수정-쿼리">6. 수정 쿼리</h2>
<pre><code class="language-sql">SELECT DISTINCT e.user_id
FROM emails e
JOIN texts t
  ON e.email_id = t.email_id
WHERE t.signup_action = &#39;Confirmed&#39;
  AND DATEDIFF(t.action_date, e.signup_date) = 1;</code></pre>
<h2 id="7-쿼리-흐름-설명">7. 쿼리 흐름 설명</h2>
<h3 id="1단계-join">1단계: JOIN</h3>
<pre><code class="language-sql">JOIN texts t ON e.email_id = t.email_id</code></pre>
<ul>
<li>이메일과 인증 데이터를 연결</li>
</ul>
<h3 id="2단계-조건-필터링">2단계: 조건 필터링</h3>
<pre><code class="language-sql">DATEDIFF(t.action_date, e.signup_date) = 1</code></pre>
<ul>
<li>가입 다음 날 조건 필터링</li>
</ul>
<h3 id="3단계-인증-완료-데이터-필터링">3단계: 인증 완료 데이터 필터링</h3>
<pre><code class="language-sql">t.signup_action = &#39;Confirmed&#39;</code></pre>
<ul>
<li>실제 인증 완료된 데이터만 필터링</li>
</ul>
<h3 id="4단계-distinct-중복제거">4단계: DISTINCT 중복제거</h3>
<pre><code class="language-sql">DISTINCT e.user_id</code></pre>
<ul>
<li>JOIN 이후에는 하나의 사용자에 대해 여러 행이 생성될 수 있기 때문에 중복제거</li>
</ul>
<h2 id="8-헷갈렸던-부분-다시-정리">8. 헷갈렸던 부분 다시 정리</h2>
<h3 id="1-날짜-비교-방식">1. 날짜 비교 방식</h3>
<ul>
<li>DATE - DATE 방식도 가능하지만 단순 일자 차이를 구할 때 시분초를 포함하여 계산하기 때문에</li>
<li>의도치 않은 결과가 나올 수 있어 DATEDIFF 사용이 더 명확하다.</li>
</ul>
<h3 id="2-조건-해석">2. 조건 해석</h3>
<ul>
<li>단순히 날짜 차이만 보면 안되고 &quot;Confirmed 여부&quot;까지 함께 고려해야 한다. 문제의 조건을 꼭 잘 봐야될 것 같다.</li>
</ul>
<h2 id="9-패턴-정리">9. 패턴 정리</h2>
<p>이 문제는 다음 패턴으로 정리할 수 있다.</p>
<pre><code class="language-sql">JOIN
-&gt; 날짜 조건
-&gt; 상태 조건
-&gt; DISTINCT</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DataLemur] Duplicate Job Listings]]></title>
            <link>https://velog.io/@daram_dev/DataLemur-Duplicate-Job-Listings</link>
            <guid>https://velog.io/@daram_dev/DataLemur-Duplicate-Job-Listings</guid>
            <pubDate>Wed, 01 Apr 2026 02:07:12 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-이해">1. 문제 이해</h2>
<p>이 문제는 단순히 중복 데이터를 찾는 것이 아니라,
중복된 채용 공고를 올린 <strong>회사 수(company_id 기준)</strong> 를 구하는 문제이다.</p>
<p>중복의 정의는 다음과 같다.</p>
<ul>
<li>같은 company_id</li>
<li>같은 title</li>
<li>같은 description</li>
</ul>
<p>이 세 가지가 모두 동일한 경우를 중복으로 본다.</p>
<h2 id="2-처음-접근-방법">2. 처음 접근 방법</h2>
<p>처음에는 다음과 같은 방식으로 문제를 해결했다.</p>
<pre><code class="language-sql">select count(cnt) as duplicate_companies
from
  (
    SELECT count(company_id) as cnt, title, description 
    FROM job_listings
    group by company_id, title, description
  ) a
where cnt &gt; 1;</code></pre>
<h3 id="접근-의도">접근 의도</h3>
<ul>
<li>company_id, title, description으로 그룹핑</li>
<li>count를 통해 중복 여부 확인</li>
<li>cnt &gt; 1이면 중복 데이터라고 판단</li>
<li>마지막에 count(cnt)로 개수 집계</li>
</ul>
<p>이 접근 자체는 중복을 찾는 방식으로는 올바른 방향이겠지만 최종적으로는 접근 방법이 틀렸다.</p>
<h2 id="3-문제점">3. 문제점</h2>
<p>문제는 마지막 집계 부분이였다.</p>
<pre><code class="language-sql">count(cnt)</code></pre>
<p>이 쿼리는 다음을 의미한다.</p>
<ul>
<li>중복된 (company_id, title, description) 조합의 개수</li>
</ul>
<p>하지만 문제에서 요구하는 것은</p>
<ul>
<li>중복을 가진 회사의 개수이다.</li>
</ul>
<p>즉, 집계 대상이 다르다는 점이다. 문제는 통과했지만 테스트케이스가 많이 존재 했다면 실패했을 것이다.</p>
<h2 id="4-왜-틀렸는지-이해하기">4. 왜 틀렸는지 이해하기</h2>
<p>다음과 같은 데이터가 있다고 가정했을 때</p>
<table>
<thead>
<tr>
<th>company_id</th>
<th>title</th>
<th>description</th>
<th>count</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>A</td>
<td>X</td>
<td>2</td>
</tr>
<tr>
<td>1</td>
<td>B</td>
<td>Y</td>
<td>2</td>
</tr>
</tbody></table>
<p>이 경우는</p>
<ul>
<li>회사 1이 중복 공고를 2개 가지고 있는 경우이다.</li>
</ul>
<h3 id="기존-쿼리-결과">기존 쿼리 결과</h3>
<pre><code class="language-sql">count(cnt) = 2</code></pre>
<p>중복 건수 기준으로 계산될 것이다.</p>
<h3 id="실제-정답">실제 정답</h3>
<pre><code class="language-sql">COUNT(DISTINCT company_id) = 1</code></pre>
<p>실제로는 회사 수 기준으로 계산되어야 하기 때문에 1이라는 값이 계산될 것이다.</p>
<h2 id="5-핵심-개념-정리">5. 핵심 개념 정리</h2>
<p>이 문제에서 가장 중요한 포인트는 다음이다.</p>
<p>중간 결과는 &quot;중복된 공고 목록&quot;이다.
하지만 최종 결과는 &quot;그 공고를 가진 회사 수&quot;이다.</p>
<p>즉,</p>
<ul>
<li>행(row)을 세는 것이 아니고</li>
<li>회사(company_id)를 세야 한다.</li>
</ul>
<h2 id="6-수정-쿼리">6. 수정 쿼리</h2>
<p>최종적으로 아래와 같이 수정하였다.</p>
<pre><code class="language-sql">select count(DISTINCT company_id) as duplicate_companies
from
  (
    SELECT count(*), company_id, title, description 
    FROM job_listings
    GROUP BY company_id, title, description
    HAVING count(*) &gt; 1
  ) a;</code></pre>
<h2 id="7-쿼리-흐름-설명">7. 쿼리 흐름 설명</h2>
<h3 id="1단계-그룹핑">1단계: 그룹핑</h3>
<pre><code class="language-sql">GROUP BY company_id, title, description</code></pre>
<ul>
<li>동일한 공고 단위로 묶었다.</li>
</ul>
<h3 id="2단계-중복-필터링">2단계: 중복 필터링</h3>
<pre><code class="language-sql">HAVING count(*) &gt; 1</code></pre>
<ul>
<li>같은 공고가 2개 이상인 경우만 남길 수 있도록 조건을 줬다.</li>
</ul>
<h3 id="3단계-회사-기준으로-집계">3단계: 회사 기준으로 집계</h3>
<pre><code class="language-sql">COUNT(DISTINCT company_id)</code></pre>
<ul>
<li>같은 회사가 여러 번 등장할 수 있기 때문에 중복을 제거하고</li>
<li>최종적으로 회사 수만 계산한다.</li>
</ul>
<h2 id="8-헷갈렸던-부분-정리">8. 헷갈렸던 부분 정리</h2>
<h3 id="1-count의-대상">1. COUNT의 대상</h3>
<ul>
<li>COUNT(*) -&gt; 행 개수</li>
<li>COUNT(column) -&gt; 값 개수</li>
<li>COUNT(DISTINCT column) -&gt; 중복 제거된 개수</li>
</ul>
<p>문제에서는 &quot;회사 수&quot;를 구해야 했기 때문에 <code>DISTINCT</code>가 필요했다.</p>
<h3 id="2-중간-결과와-최종-결과의-차이">2. 중간 결과와 최종 결과의 차이</h3>
<ul>
<li><p>중복된 공고 목록</p>
</li>
<li><p>중복 공고를 가진 회사 수</p>
</li>
</ul>
<p>이 두 개를 혼동하면서 문제가 발생했다.</p>
<h3 id="3-group-by-이후-무엇을-세는지">3. GROUP BY 이후 무엇을 세는지</h3>
<p>GROUP BY를 사용하면 데이터가 &quot;그룹 단위&quot;로 바뀐다.</p>
<p>하지만 이후 COUNT를 할 때</p>
<ul>
<li>그룹 개수를 셀 것인지</li>
<li>특정 컬럼 기준으로 셀 것인지</li>
</ul>
<p>이 부분을 반드시 구분해야 될 것 같다.</p>
<h2 id="9-패턴-정리">9. 패턴 정리</h2>
<p>이 문제는 다음 패턴으로 정리할 수 있다.</p>
<pre><code class="language-sql">GROUP BY A, B, C
HAVING COUNT(*) &gt; 1
-&gt; DISTINCT A
-&gt; COUNT</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL, MariaDB, Oracle 날짜 차이 계산 비교]]></title>
            <link>https://velog.io/@daram_dev/MySQL-MariaDB-Oracle-%EB%82%A0%EC%A7%9C-%EC%B0%A8%EC%9D%B4-%EA%B3%84%EC%82%B0-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@daram_dev/MySQL-MariaDB-Oracle-%EB%82%A0%EC%A7%9C-%EC%B0%A8%EC%9D%B4-%EA%B3%84%EC%82%B0-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Tue, 31 Mar 2026 05:26:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>데이터베이스마다 날짜 차이를 계산하는 방식은 다르다.
특히 Oracle은 연산 중심, MySQL과 MariaDB는 함수 중심이라는 차이가 있다.</p>
</blockquote>
<h1 id="1-일day-단위-차이-계산">1. 일(day) 단위 차이 계산</h1>
<h2 id="oracle">Oracle</h2>
<p>Oracle은 DATE 타입이 내부적으로 &#39;일 단위 숫자&#39;로 표현되기 때문에 날짜 간 뺄셈 연산이 가능하다.
이 결과는 일(day) 단위이며, 시간까지 포함되면 소수로 반환된다.</p>
<pre><code class="language-sql">end_date - start_date;</code></pre>
<ul>
<li>결과는 일(day) 단위 숫자</li>
<li>시간까지 포함되면 소수로 반환된다</li>
</ul>
<h2 id="mysql--mariadb">MySQL / MariaDB</h2>
<p>MySQL과 MariaDB에서는 DATEDIFF를 사용하는 것이 표준적인 방법이다.
DATE 타입으로 변환 후 빼기 연산도 가능하지만, 이는 내부적으로 날짜가 숫자로 변환되어 계산되는 방식이므로 DBMS에 따라 동작이 달라질 수 있어 권장되지 않는다고 한다.</p>
<pre><code class="language-sql">DATEDIFF(end_date, start_date);

date(end_date) - date(start_date)</code></pre>
<p>DATEDIFF 함수를 사용했을 때의 특징은 다음과 같다.</p>
<ul>
<li>결과는 일(day) 단위 정수</li>
<li>시간 정보는 무시된다</li>
<li>두 DB는 동일한 문법을 사용한다</li>
</ul>
<h1 id="2-시간-단위-차이-계산-시-분-초">2. 시간 단위 차이 계산 (시, 분, 초)</h1>
<h2 id="oracle-1">Oracle</h2>
<p>Oracle은 날짜 차이를 구한 뒤 직접 단위 변환을 해야 한다.</p>
<pre><code class="language-sql">SELECT (end_date - start_date) * 24 AS hours FROM dual;</code></pre>
<ul>
<li>시(hour): 24 곱하기</li>
<li>분(minute): 24 * 60</li>
<li>초(second): 24 * 60 * 60</li>
</ul>
<hr>
<h2 id="mysql--mariadb-1">MySQL / MariaDB</h2>
<p>MySQL과 MariaDB는 TIMESTAMPDIFF 함수를 사용하여 단위를 직접 지정할 수 있다.</p>
<pre><code class="language-sql">SELECT TIMESTAMPDIFF(HOUR, start_date, end_date);
SELECT TIMESTAMPDIFF(MINUTE, start_date, end_date);
SELECT TIMESTAMPDIFF(SECOND, start_date, end_date);</code></pre>
<ul>
<li>원하는 단위를 직접 지정 가능</li>
</ul>
<h1 id="3-개월month-차이-계산">3. 개월(month) 차이 계산</h1>
<h2 id="oracle-2">Oracle</h2>
<p>Oracle은 MONTHS_BETWEEN 함수를 제공한다.</p>
<pre><code class="language-sql">SELECT MONTHS_BETWEEN(end_date, start_date) FROM dual;</code></pre>
<ul>
<li>결과는 소수까지 포함된 값</li>
<li>정밀한 개월 계산 가능</li>
</ul>
<p>예를 들어 1.5개월 같은 값이 나올 수 있다.</p>
<h2 id="mysql--mariadb-2">MySQL / MariaDB</h2>
<pre><code class="language-sql">SELECT TIMESTAMPDIFF(MONTH, start_date, end_date);</code></pre>
<ul>
<li>결과는 정수만 반환</li>
<li>소수 단위 개월 계산은 불가능</li>
</ul>
<h1 id="4-년월일-근무-기간-계산">4. 년/월/일 근무 기간 계산</h1>
<h2 id="oracle-3">Oracle</h2>
<pre><code class="language-sql">SELECT 
  FLOOR(MONTHS_BETWEEN(end_date, start_date)/12) AS years,
  MOD(FLOOR(MONTHS_BETWEEN(end_date, start_date)), 12) AS months
FROM dual;</code></pre>
<ul>
<li>개월을 기준으로 년과 월을 나누어 계산</li>
</ul>
<h2 id="mysql--mariadb-3">MySQL / MariaDB</h2>
<pre><code class="language-sql">SELECT 
  TIMESTAMPDIFF(YEAR, start_date, end_date) AS years,
  TIMESTAMPDIFF(MONTH, start_date, end_date) % 12 AS months,
  DATEDIFF(end_date, start_date) % 30 AS days;</code></pre>
<ul>
<li>각 단위를 별도로 계산</li>
<li>일(day)은 달마다 30일이 아니기 때문에 별도 로직 또는 날짜 함수 조합을 사용하는 것이 안전하다.</li>
</ul>
<h1 id="5-자주-사용하는-날짜-조건">5. 자주 사용하는 날짜 조건</h1>
<h2 id="특정-기간-이전-데이터-n일-이상-지난-데이터">특정 기간 이전 데이터 (N일 이상 지난 데이터)</h2>
<p>Oracle</p>
<pre><code class="language-sql">WHERE SYSDATE - created_at &gt;= 7</code></pre>
<p>MySQL / MariaDB</p>
<pre><code class="language-sql">WHERE DATEDIFF(NOW(), created_at) &gt;= 7</code></pre>
<h2 id="최근-n일-데이터">최근 N일 데이터</h2>
<p>Oracle</p>
<pre><code class="language-sql">WHERE created_at &gt;= SYSDATE - 7</code></pre>
<p>MySQL / MariaDB</p>
<pre><code class="language-sql">WHERE created_at &gt;= DATE_SUB(NOW(), INTERVAL 7 DAY)</code></pre>
<h2 id="특정-날짜-범위-조회">특정 날짜 범위 조회</h2>
<p>Oracle</p>
<pre><code class="language-sql">WHERE created_at BETWEEN 
  TO_DATE(&#39;2026-03-01&#39;,&#39;YYYY-MM-DD&#39;) 
  AND TO_DATE(&#39;2026-03-31&#39;,&#39;YYYY-MM-DD&#39;)</code></pre>
<p>MySQL / MariaDB</p>
<pre><code class="language-sql">WHERE created_at BETWEEN &#39;2026-03-01&#39; AND &#39;2026-03-31&#39;</code></pre>
<h2 id="오늘-데이터-조회">오늘 데이터 조회</h2>
<p>Oracle</p>
<pre><code class="language-sql">WHERE TRUNC(created_at) = TRUNC(SYSDATE)</code></pre>
<p>MySQL / MariaDB</p>
<pre><code class="language-sql">WHERE DATE(created_at) = CURDATE()</code></pre>
<h1 id="6-핵심-차이">6. 핵심 차이</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>Oracle</th>
<th>MySQL</th>
<th>MariaDB</th>
</tr>
</thead>
<tbody><tr>
<td>날짜 차이</td>
<td>날짜끼리 빼기</td>
<td>DATEDIFF</td>
<td>DATEDIFF</td>
</tr>
<tr>
<td>시간 계산</td>
<td>직접 연산</td>
<td>TIMESTAMPDIFF</td>
<td>TIMESTAMPDIFF</td>
</tr>
<tr>
<td>개월 계산</td>
<td>MONTHS_BETWEEN (소수 가능)</td>
<td>정수 반환</td>
<td>정수 반환</td>
</tr>
<tr>
<td>반환 타입</td>
<td>실수 가능</td>
<td>정수</td>
<td>정수</td>
</tr>
<tr>
<td>문법 유사성</td>
<td>자체 문법</td>
<td>자체 문법</td>
<td>MySQL과 거의 동일</td>
</tr>
</tbody></table>
<h1 id="7-정리">7. 정리</h1>
<p>Oracle은 날짜를 숫자처럼 다루기 때문에 연산 중심으로 처리한다.
MySQL과 MariaDB는 날짜 연산을 위해 전용 함수를 사용한다.
MariaDB가 MySQL의 기능을 더 확장하고 개선해서 만든 것이기 때문에 문법이 거의 동일하다.</p>
<hr>
<h1 id="8-사용시-주의사항">8. 사용시 주의사항</h1>
<p>Oracle, MySQL, MariaDB 모두 인자의 순서가 중요하다.</p>
<p>Oracle에서는 단순히 빼기 연산이기 때문에 순서에 따라 음수가 나올 수 있다.</p>
<pre><code class="language-sql">end - start</code></pre>
<p>MySQL과 MariaDB에서는 DATEDIFF의 인자 순서가 중요하다.</p>
<pre><code class="language-sql">DATEDIFF(end, start)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB Compass로 Aggregations 쿼리 작성하기]]></title>
            <link>https://velog.io/@daram_dev/MongoDB-Compass%EB%A1%9C-Aggregations-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@daram_dev/MongoDB-Compass%EB%A1%9C-Aggregations-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 30 Mar 2026 05:59:49 GMT</pubDate>
            <description><![CDATA[<h1 id="mongodb-compass">MongoDB Compass</h1>
<blockquote>
<p>Studio3T의 정책이 변경되면서 학생 계정으로 지원해주던 기능이 없어졌습니다.
Community Edition에서 Aggregations 생성을 할 수 가 없게되어 알아본 결과
MongoDB Compass의 기능을 이용하여 Aggregations 생성을 할 수 있는 것을 알게 되었습니다.</p>
</blockquote>
<p>간단하게 Agreegations 쿼리를 생성하는 과정을 화면 캡쳐로 정리하였습니다.</p>
<h2 id="1-db-연결">1. DB 연결</h2>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/a10193ba-5d41-4cd2-ae74-54c7fffdfe92/image.png" alt=""></p>
<h2 id="2-aggregations">2. Aggregations</h2>
<h3 id="21-aggregations-선택">2.1. Aggregations 선택</h3>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/19ff3d98-b2bb-418d-bb81-cc9a247de2a5/image.png" alt="">
<img src="https://velog.velcdn.com/images/daram_dev/post/6cc57018-c3d4-4bd0-a2f5-74d969a5c2d2/image.png" alt=""></p>
<h3 id="22-ai-helper-클릭">2.2. AI Helper 클릭</h3>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/5c757025-bc84-48f8-b718-5c142ed5cf1f/image.png" alt=""></p>
<h3 id="23-aggregate-쿼리-생성을-위한-sql-쿼리-작성-후-실행">2.3. Aggregate 쿼리 생성을 위한 SQL 쿼리 작성 후 실행</h3>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/3e13ffd7-0d24-43c8-89f6-0ee7f1b55b14/image.png" alt=""></p>
<h3 id="24-export-query-to-language-선택">2.4. Export query to language 선택</h3>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/b74b4c1d-ecbd-42f2-8a25-ae0540444a86/image.png" alt="">
<img src="https://velog.velcdn.com/images/daram_dev/post/c1f362fd-f0f5-49e1-ab28-87ad8440decb/image.png" alt=""></p>
<h3 id="25-language-변경-및-복사">2.5. language 변경 및 복사</h3>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/af7b4fa3-b941-470f-aa34-28ab8401b279/image.png" alt="">
<img src="blob:https://velog.io/fb483b0d-6fcd-4707-8833-0c23fb96cc16" alt="업로드중.."></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네이버 클라우드 자격증] NCP 200]]></title>
            <link>https://velog.io/@daram_dev/NCP-200</link>
            <guid>https://velog.io/@daram_dev/NCP-200</guid>
            <pubDate>Fri, 09 Jan 2026 10:00:41 GMT</pubDate>
            <description><![CDATA[<p>벨로그 용으로 수정하여 작성하였으나 정답과 풀이에 대한 태그가 적용되지 않아서(details, summary)
<a href="https://studyharddev.notion.site/ncp-200">notion의 정리본 링크</a>를 첨부합니다.</p>
<p>티스토리로 갈아탈까...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 8080포트 충돌시 조회 후 강제 종료하기]]></title>
            <link>https://velog.io/@daram_dev/8080%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C%EC%8B%9C-%EC%A1%B0%ED%9A%8C-%ED%9B%84-%EA%B0%95%EC%A0%9C-%EC%A2%85%EB%A3%8C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@daram_dev/8080%ED%8F%AC%ED%8A%B8-%EC%B6%A9%EB%8F%8C%EC%8B%9C-%EC%A1%B0%ED%9A%8C-%ED%9B%84-%EA%B0%95%EC%A0%9C-%EC%A2%85%EB%A3%8C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 13 Oct 2025 05:59:00 GMT</pubDate>
            <description><![CDATA[<p>Vue.js를 띄워둔 상태에서 Spring Boot를 실행했더니 포트 8080 충돌로 서버가 뜨지 않았습니다.(포트를 둘 다 8080을 사용하고 있었음)
<img src="https://velog.velcdn.com/images/daram_dev/post/2eaa1c73-8759-45c4-b2d0-c0ffaf5876bc/image.png" alt="에러화면">
에러 로그:</p>
<pre><code>Web server failed to start. Port 8080 was already in use.</code></pre><p>스프링의 포트를 변경하는 방법도 있지만
8080을 점유한 프로세스의 PID를 조회하고 해당 프로세스를 강제 종료하는 방법을 정리해 보았습니다.</p>
<h3 id="1-8080포트의-pid를-조회한다">1. 8080포트의 PID를 조회한다.</h3>
<pre><code>  netstat -ano | findstr 8080</code></pre><p>  <img src="https://velog.velcdn.com/images/daram_dev/post/86d9335b-2b5d-41b0-a9ac-6abac7751a3c/image.png" alt="8080포트의 PID를 조회하는 명령어"></p>
<table>
<thead>
<tr>
<th align="center">열 번호</th>
<th align="left">열 이름</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">①</td>
<td align="left"><strong>Proto</strong></td>
<td align="left">프로토콜 (TCP, UDP 등)</td>
</tr>
<tr>
<td align="center">②</td>
<td align="left"><strong>Local Address</strong></td>
<td align="left">내 컴퓨터의 IP와 포트 번호</td>
</tr>
<tr>
<td align="center">③</td>
<td align="left"><strong>Foreign Address</strong></td>
<td align="left">원격(상대방)의 IP와 포트 번호</td>
</tr>
<tr>
<td align="center">④</td>
<td align="left"><strong>State</strong></td>
<td align="left">현재 연결 상태 (LISTENING, ESTABLISHED 등)</td>
</tr>
<tr>
<td align="center">⑤</td>
<td align="left"><strong>PID</strong></td>
<td align="left">해당 연결을 사용하는 프로세스의 ID (Process ID)</td>
</tr>
</tbody></table>
<ul>
<li><p><strong><code>netstat</code>이란?</strong>
  netstat 명령어는 자신의 컴퓨터와 연결된 모든 네트워크 연결을 보여주는 명령어이다.</p>
</li>
<li><p><strong><code>netstat</code>에서 자주 사용하는 옵션</strong></p>
<table>
<thead>
<tr>
<th align="left">옵션</th>
<th align="left">의미</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>-a</code></td>
<td align="left">모든 연결 및 수신 포트를 표시한다.</td>
</tr>
<tr>
<td align="left"><code>-n</code></td>
<td align="left">주소와 포트를 &quot;IP주소:포트&quot;의 형태로 표시한다.</td>
</tr>
<tr>
<td align="left"><code>-o</code></td>
<td align="left">각 연결에 대한 PID(프로세스ID)를 표시해준다.</td>
</tr>
</tbody></table>
</li>
</ul>
<p><br><br></p>
<h3 id="2-pid-로-8080-프로세스-강제-종료하기">2. PID 로 8080 프로세스 강제 종료하기</h3>
<pre><code>taskkill /f /pid [PID번호]</code></pre><p><img src="https://velog.velcdn.com/images/daram_dev/post/6783bebb-2156-4eac-bd05-25903c4f22b7/image.png" alt="PID 로 8080 프로세스 강제 종료"></p>
<ul>
<li><p><strong><code>taskkill</code>이란?</strong>
  Windows 운영체제에서 실행 중인 프로세스를 종료(kill)하는 명령어이다.</p>
</li>
<li><p><strong><code>taskkill</code>과 함께 사용된 옵션</strong></p>
<table>
<thead>
<tr>
<th align="left">옵션</th>
<th align="left">의미</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>/pid</code></td>
<td align="left">종료할 프로세스의 PID 지정</td>
</tr>
<tr>
<td align="left"><code>/f</code></td>
<td align="left">강제 종료 (Force) — 프로세스가 응답하지 않아도 강제로 종료</td>
</tr>
</tbody></table>
</li>
</ul>
<p><br><br></p>
<h3 id="3-결론">3. 결론</h3>
<p>이제 다시 Spring Boot Application을 실행하게되면 정상적으로 서버가 구동되는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/daram_dev/post/43939869-9850-408f-a3af-9fbeef4e9d61/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Issue 사용 가이드]]></title>
            <link>https://velog.io/@daram_dev/Issue-%EC%82%AC%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@daram_dev/Issue-%EC%82%AC%EC%9A%A9-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Tue, 19 Aug 2025 15:36:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>팀 프로젝트를 진행하면서 협업하면서 서로 어떤 작업을 진행하고, 진행하려하는지 파악하기위한 목적으로 Issue를 사용하기로 하였습니다.
Issue에 대하여 팀원들에게 설명을 진행하였던 부분을 글로 정리해보았습니다.</p>
</blockquote>
<h1 id="1-issue란">1. Issue란?</h1>
<ul>
<li>작업할 내용, 버그, 개선 사항, 질문 등을 관리하기 위한 용도<ul>
<li>누가 어떤 작업을 하고 있는지 추적할 수 있음</li>
<li>진행 상황(진행 중/완료)을 쉽게 공유할 수 있음</li>
<li>PR과 연결되어 자동으로 닫을 수 있고 상태가 업데이트 됨으로써 진행 상황을 서로 파악하기 좋음</li>
</ul>
</li>
</ul>
<h1 id="2-issue를-사용하는-이유">2. Issue를 사용하는 이유</h1>
<ol>
<li>작업 내용 기록<ul>
<li>구두로 말하는 것보다 코드 변경 이유, 목표 등을 문서로 남길 수 있음</li>
</ul>
</li>
<li>PR과 연결 가능<ul>
<li>커밋 메시지나 PR의 본문에 closed #이슈번호를 적dmaus, PR이 merge가 될 때 Issue가 자동으로 닫히게 됨.</li>
</ul>
</li>
<li>기능 단위로 관리<ul>
<li>기능(작업) 단위로 브랜치를 만들 때, Issue 번호를 브랜치명에 포함시켜서 관리가 가능하며 추적이 가능</li>
</ul>
</li>
</ol>
<h1 id="3-issue-작성-규칙">3. Issue 작성 규칙</h1>
<ol>
<li>하나의 Issue에는 하나의 작업/기능/버그만 기록할 것</li>
<li>명확한 제목으로 어떤 작업인지 바로 이해할 수 있도록 작성할 것<ul>
<li>예: 회원가입 API 구현</li>
</ul>
</li>
<li>진행 상황 체크리스트를 포함할 것</li>
</ol>
<h1 id="4-issue-템플릿">4. Issue 템플릿</h1>
<p>Github에서 Issue를 생성할 때 팀장이 작성해둔 템플릿을 기반으로 사용</p>
<h1 id="5-issue-사용-예시">5. Issue 사용 예시</h1>
<ol>
<li><p>새 기능을 작업하는 경우</p>
<ul>
<li><p>제목: 회원가입 API 구현</p>
</li>
<li><p>내용</p>
<pre><code class="language-markdown">  ## 설명
  Spring Boot 기반으로 회원가입 API 개발
  이메일 중복 체크 적용

  ## 진행 상황
  - [ ] Controller 생성
  - [ ] Service 로직 구현
  - [ ] 테스트 코드 작성

  ## 부가 설명
  비밀번호 암호화 적용은 다음 이슈(#12)에서 처리 예정</code></pre>
<p>  <img src="https://velog.velcdn.com/images/daram_dev/post/cc9006f5-86ef-4543-9c87-8a05df822860/image.png" alt="">
  <img src="https://velog.velcdn.com/images/daram_dev/post/d43fae16-afe7-43fe-881c-5f014b58f5e7/image.png" alt=""></p>
</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git 공통 규칙 정리]]></title>
            <link>https://velog.io/@daram_dev/Git-%EA%B3%B5%ED%86%B5-%EA%B7%9C%EC%B9%99-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@daram_dev/Git-%EA%B3%B5%ED%86%B5-%EA%B7%9C%EC%B9%99-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 19 Aug 2025 03:07:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>팀프로젝트를 진행하기 앞서 git 브랜치 전략을 정하고 문서화하는 과정입니다.
오류가 있는 부분은 댓글로 알려주시면 감사하겠습니다.
프로젝트를 진행하면서 스스로 발견하는 오류사항은 수정하면서 작업하려고 하고 있습니다.
명령어를 직접 작성하면서 익숙해지는 것을 목표로 하고있었기 때문에 git-bash에서 직접 명령어를 작성하도록 안내하였습니다.</p>
</blockquote>
<h1 id="1-협업-기본-흐름-및-브랜치-전략">1. 협업 기본 흐름 및 브랜치 전략</h1>
<p><strong>git-flow</strong> 브랜치 전략을 사용한다.</p>
<h2 id="기본-흐름">기본 흐름</h2>
<ol>
<li>Organization(조직)의 Repository를 Fork한다.</li>
<li>각자 로컬에 clone을 수행한 후 develop 브랜치로 이동한다.</li>
<li>이슈를 생성한다.</li>
<li>develop 브랜치에서 새로운 기능 개발 브랜치를 생성한다.<ul>
<li>feature/{issue-number}-{feature-name}</li>
</ul>
</li>
<li>작업 완료 후 <strong>add → commit → push</strong> 한다.(본인 원격 저장소에)</li>
<li>Organization 저장소의 develop 브랜치에 PR(Pull Request)을 요청(생성)한다.</li>
<li>팀장의 리뷰가 승인 후 merge를 진행한다.</li>
<li>merge가 완료되면 브랜치를 삭제한다.(원격/로컬 모두)</li>
</ol>
<h2 id="브랜치-종류">브랜치 종류</h2>
<ul>
<li><strong>master</strong>: 배포 가능한 브랜치</li>
<li><strong>develop</strong>: 개발한 기능이 모여있는 브랜치</li>
<li><strong>feature</strong>: 기능을 개발하는 브랜치<ul>
<li>develop 에서 분기해서 작성</li>
<li>master에서 분기 금지</li>
</ul>
</li>
<li><strong>release</strong>: 품질검사(QA)를 하기위한 브랜치 (출시 준비용)</li>
<li><strong>hotfix</strong>: master 브랜치에 발생한 버그를 긴급 수정하는 브랜치</li>
</ul>
<h1 id="2-브랜치-사용-규칙">2. 브랜치 사용 규칙</h1>
<ul>
<li><strong>master</strong>: 배포용 (PR 금지, 팀장만 관리)</li>
<li><strong>develop</strong>: 공용 개발 브랜치</li>
<li><strong>feature/{issue-number}-{feature-name}</strong>: 기능 개발용(develop에서 분기)<ul>
<li>이슈 추적을 사용하기 위해서 위와 같은 방식을 사용한다.</li>
<li><strong>예시: feature/2-userinfo-api</strong></li>
</ul>
</li>
<li><strong>hotfix/{issue-number}</strong>: 운영 중 버그 수정(master에서 분기)</li>
</ul>
<h1 id="3-깃-커밋-메시지-작성-규칙">3. 깃 커밋 메시지 작성 규칙</h1>
<h2 id="작성-규칙">작성 규칙</h2>
<h3 id="탬플릿">탬플릿</h3>
<pre><code>type: 제목(50자 이내, 명령문, 끝에 마침표 X) 

본문 (무엇과 왜를 설명, 72자 단위 줄바꿈 실시)

closed #이슈번호</code></pre><h3 id="예시">예시</h3>
<pre><code>feat: Add login API

사용자 로그인 API 추가.
비밀번호 암호화 적용.

closed #2</code></pre><h2 id="커밋-유형type">커밋 유형(type)</h2>
<table>
<thead>
<tr>
<th>타입 이름</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>feat</td>
<td>새로운 기능에 대한 커밋</td>
</tr>
<tr>
<td>fix</td>
<td>버그 수정에 대한 커밋</td>
</tr>
<tr>
<td>build</td>
<td>빌드 관련 파일 수정 / 모듈 설치 또는 삭제에 대한 커밋</td>
</tr>
<tr>
<td>chore</td>
<td>그 외 자잘한 수정에 대한 커밋</td>
</tr>
<tr>
<td>ci</td>
<td>ci 관련 설정 수정에 대한 커밋</td>
</tr>
<tr>
<td>docs</td>
<td>문서 수정에 대한 커밋</td>
</tr>
<tr>
<td>style</td>
<td>코드 스타일 혹은 포맷 등에 관한 커밋</td>
</tr>
<tr>
<td>refactor</td>
<td>코드 리팩토링에 대한 커밋</td>
</tr>
<tr>
<td>test</td>
<td>테스트 코드 수정에 대한 커밋</td>
</tr>
<tr>
<td>perf</td>
<td>성능 개선에 대한 커밋</td>
</tr>
</tbody></table>
<h2 id="커밋-규칙">커밋 규칙</h2>
<ol>
<li>제목과 본문 등 사이는 <strong>빈 행으로 구분</strong>한다.</li>
<li>제목은 <strong>50글자</strong> 이내로 제한한다.</li>
<li>제목 <strong>첫 글자는 대문자</strong>로 작성한다.</li>
<li>type은 소문자로 작성한다.</li>
<li>제목 끝에 <strong>마침표를 넣지 않는다.</strong></li>
<li>제목은 <strong>명령문</strong>으로 사용하며 <strong>과거형을 사용하지 않는다.</strong></li>
<li>본문에서 <strong>각 행은 72글자 내로</strong> 제한한다.</li>
<li>어떻게를 설명하지 않고 <strong>무엇, 왜를 설명</strong>한다.</li>
</ol>
<h2 id="커밋과-함께-이슈를-닫을-수-있는-키워드">커밋과 함께 이슈를 닫을 수 있는 키워드</h2>
<ul>
<li>close</li>
<li>closes</li>
<li>closed</li>
<li>fix</li>
<li>fixes</li>
<li>fixed</li>
<li>resolve</li>
<li>resolves</li>
<li>resolved</li>
</ul>
<h1 id="4-알아야하는-git-명령어">4. 알아야하는 Git 명령어</h1>
<p><strong>일련의 단계들을 이해하기 위해서</strong></p>
<p><strong>꼭 작업 흐름 단계별로 명령어와 함께 아래의 예시를 봐야됨.</strong></p>
<h2 id="1-프로젝트-초기-세팅">1. 프로젝트 초기 세팅</h2>
<pre><code class="language-bash"># 자신의 계정으로 Fork 후 clone
# (브랜치 상관없음 — 클론한 직후)
git clone https://github.com/{내계정}/{레포지토리}.git
cd {레포지토리}

# (브랜치 상관없음)
# 원본(팀 Organization) 저장소를 upstream으로 추가
git remote add upstream https://github.com/{Organization}/{레포지토리}.git</code></pre>
<ol>
<li>.gitignore 설정</li>
<li>application.propterties 설정</li>
<li>application-*.properties 설정</li>
</ol>
<pre><code class="language-bash"># develop 브랜치로 전환
git checkout develop

git pull upstream develop</code></pre>
<ul>
<li>upstream 설정 이유<ul>
<li>팀장이 develop 브랜치를 업데이트하게되면, 본인 origin은 자동으로 갱신되지 않기 때문에
upstream을 설정해줘야 쉽게 최신 코드를 받아올 수 있음</li>
<li>sync fork와 동일한 작업이지만 터미널에서 해결하는 방법으로 변경하였음</li>
</ul>
</li>
</ul>
<hr>
<p>여기서부터 계속 반복</p>
<h3 id="2-기능-개발-시작feature-브랜치">2. 기능 개발 시작(feature 브랜치)</h3>
<pre><code class="language-bash"># 최신 develop 가져오기(작업 시작 전 필수임!)
git fetch upstream                # upstream(Organization)의 최신 내용 가져오기
git checkout develop              # develop 브랜치로 이동
git pull upstream develop         # 최신 내용으로 갱신

# 기능 브랜치 생성(develop에서 분기)
git checkout -b feature/2-userinfo-api # 브랜치명 예시로 작성한 것</code></pre>
<ul>
<li>주의<ul>
<li>feature 브랜치는 꼭 develop에서만 분기할 것.</li>
<li>브랜치명은 feature/{issue-number}-{feature-name} <strong>형태로 사용할 것</strong></li>
</ul>
</li>
</ul>
<h2 id="3-작업-후-커밋--푸시">3. 작업 후 커밋 &amp; 푸시</h2>
<pre><code class="language-bash"># (현재 브랜치: feature/2-userinfo-api)
git status              # 변경 내용 확인
git add .               # 변경 파일 모두 추가(선택적으로도 가능)
git commit              # 규칙 준수

----------------------------------------
feat: Add login API

사용자 로그인 API 추가.
비밀번호 암호화 적용.

closed #2
----------------------------------------

# 본인 fork(origin)의 브랜치로 푸시
git push origin feature/2-userinfo-api</code></pre>
<h2 id="4-pr-생성">4. PR 생성</h2>
<ul>
<li><p>Github에서 수행</p>
<ul>
<li><p>PR 대상: 내 저장소 → Organization 저장소의 <strong>develop 브랜치</strong></p>
</li>
<li><p>PR 내용</p>
<pre><code class="language-jsx">  ### PR 타입(하나 이상의 PR 타입을 작성해주세요)
  - 기능 추가
  - 기능 삭제
  - 버그 수정
  - 의존성, 환경 변수, 빌드 관련 코드 업데이트

  ### 반영 브랜치
  ex) feat/9-userLogin-api -&gt; develop

  ### 반영 사항
  ex) 로그인 시, 구글 소셜 로그인 기능을 추가했습니다.

  ### 적용 화면(선택사항)

  closed #이슈번호</code></pre>
<p>  <img src="https://velog.velcdn.com/images/daram_dev/post/4145782c-6ad8-4020-82c1-d77c3627dff4/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<ul>
<li>팀장 리뷰 후 merge 진행</li>
<li>원격 브랜치 삭제
<img src="https://velog.velcdn.com/images/daram_dev/post/aaf45936-ec5a-4bbb-a428-f5989f2bf2dc/image.png" alt=""></li>
</ul>
<h2 id="5-merge-완료-후-로컬-브랜치-정리">5. Merge 완료 후 로컬 브랜치 정리</h2>
<ul>
<li><p>브랜치 이동과 삭제는 명령어 사용하지말고 UI에서 제공하는 메뉴를 이용해서 할 것</p>
<pre><code class="language-bash">  git fetch upstream

  # develop 브랜치로 전환(git checkout develop)

  git pull upstream develop         # 최신 develop 동기화

  # 브랜치 삭제 (로컬 &amp; 원격)
  # (develop 상태에서, 병합 완료된 feature 브랜치 삭제)
  git branch -d feature/3-login-api
  git push origin --delete feature/3-login-api

  # 최신 develop으로 동기화
  git fetch upstream
  git checkout develop
  git pull upstream develop</code></pre>
</li>
<li><p>메뉴 이용해야되는 이유
  <img src="https://velog.velcdn.com/images/daram_dev/post/c41760a5-cc7e-4593-98ba-27c1e59be5df/image.png" alt=""></p>
</li>
</ul>
<pre><code>window에서 인텔리제이에서 파일을 열고 있기 때문에 삭제하지 못하도록 lock을 걸고있기 때문에 당황할 수 있음(본인이 그랬음...)    
인텔리자체 기능을 이용하는게 편함</code></pre><h3 id="1-브랜치-이동">1. 브랜치 이동</h3>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/8aae0118-36f2-477b-8685-3a015e287c8a/image.png" alt=""></p>
<h3 id="2-원격-변경사항-확인-및-최신-브랜치-동기화">2. 원격 변경사항 확인 및 최신 브랜치 동기화</h3>
<pre><code>    git fetch upstream

    git pull upstream develop         # 최신 develop 동기화</code></pre><h3 id="3-브랜치-삭제">3. 브랜치 삭제</h3>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/f1104bb5-02d0-4815-86bf-b46fc095b3cc/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[sql*plus 메모장 한글 깨짐 현상해결 방법(Notepad++ 설정 방법)]]></title>
            <link>https://velog.io/@daram_dev/sqlplus-%EB%A9%94%EB%AA%A8%EC%9E%A5-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%ED%98%84%EC%83%81%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95Notepad-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@daram_dev/sqlplus-%EB%A9%94%EB%AA%A8%EC%9E%A5-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%ED%98%84%EC%83%81%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95Notepad-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Fri, 30 May 2025 05:33:48 GMT</pubDate>
            <description><![CDATA[<p>Oracle 학습을 위해서 SQL*Plus를 사용 중, ed 명령어로 sql 파일을 열어서 컬럼명을 한글로 작성하게 될때나 주석이 깨지는 현상이 발생한다.
이 문제는 기본 메모장의 인코딩 문제로 발생한다.</p>
<blockquote>
<p>내 컴퓨터같은 경우에는 메모장에서
다른 이름으로 저장하기 -&gt; 인코딩 ANSI로 저장을 하면 한글 컬럼명이 제대로 나왔지만 다른 사람들 컴퓨터에서는 ANSI 인코딩도 인식이 제대로 안되는 경우가 있어 공통 환경을 맞추기 위해 작성한다.</p>
</blockquote>
<h2 id="문제-상황-예">문제 상황 예</h2>
<pre><code class="language-sql">set serveroutput on
declare
    vempno number(4);
    vename varchar2(10);
begin
    vempno := 7788;
    vename := &#39;SCOTT&#39;;
    DBMS_OUTPUT.PUT_LINE(&#39;회원번호 / 회원명&#39;);
    DBMS_OUTPUT.PUT_LINE(&#39;-------------&#39;);
    DBMS_OUTPUT.PUT_LINE(VEMPNO || &#39; / &#39; || VENAME);
END;
/</code></pre>
<p>위 처럼 코드를 작성하는 경우에도 실행 시에
컬럼명이 &quot; ?쀌뚫뛕 / ?뽧뜗뚕 &quot; 이런식으로 깨져서 출력되게 된다.</p>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="notepad-설치-및-경로-확인">Notepad++ 설치 및 경로 확인</h3>
<ol>
<li><p>Notepad++를 설치한다.
<a href="https://notepad-plus-plus.org/downloads/">https://notepad-plus-plus.org/downloads/</a></p>
</li>
<li><p>설치 완료 후 Notepad++의 실행파일 위치를 확인한다.</p>
<ul>
<li>정상적으로 설치되었다면 아래와 같이 경로가 출력 될 것이다.
<code>C:\Program Files\Notepad++\notepad++.exe</code></li>
</ul>
</li>
<li><p><code>glogin.sql</code> 파일 열기</p>
<ul>
<li>Oracle SQL*Plus는 실행 시에 자동으로 glogin.sql이라는 초기화 파일을 실행하게 된다.
(참고로 이 sql을 설정하지 않으면 실행 시 마다 notepad를 편집기로 설정하는 명령어를 매번 입력해줘야한다.)</li>
<li><code>glogin.sql</code> 파일의 기본 위치는 아래와 같다.
<code>&lt;Oracle 설치 경로&gt;\sqlplus\admin\glogin.sql</code></li>
</ul>
</li>
<li><p>Notepad++를 기본 편집기로 설정하기</p>
<ul>
<li><code>glogin.sql</code> 파일에 다음 명령어를 추가한다.
<code>define _editor = &quot;C:\Program Files\Notepad++\notepad++.exe&quot;</code></li>
</ul>
</li>
<li><p>SQL*Plus 재실행 후 편집기 확인</p>
<ul>
<li>SQL*Plus를 재실행 후에 편집기를 열어서 확인 해보면 설정이 잘 되어있는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/daram_dev/post/2d94cb75-a11a-4a65-96c5-d71409272adb/image.png" alt=""></li>
<li>기존에 작성 한글이 있을 때 그대로 인코딩 설정을 ANSI를 누르게 되면 한글이 다시 깨질 수 있으니 ANSI로 변환을 꼭! 눌러줘야 된다.
<img src="https://velog.velcdn.com/images/daram_dev/post/5441b79d-4ba5-4188-ac41-e6e26f465427/image.png" alt=""></li>
</ul>
</li>
<li><p>sql 파일 실행하기</p>
<ul>
<li>작성한 sql을 실행해서 확인하면 한글 인코딩이 깨지지 않고 잘 나오는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/daram_dev/post/e7040be3-4c85-424b-b0be-1d86543e6bd3/image.png" alt=""></li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[item13: clone 재정의는 주의해서 진행하라]]></title>
            <link>https://velog.io/@daram_dev/item13-clone-%EC%9E%AC%EC%A0%95%EC%9D%98%EB%8A%94-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%84%9C-%EC%A7%84%ED%96%89%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@daram_dev/item13-clone-%EC%9E%AC%EC%A0%95%EC%9D%98%EB%8A%94-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%84%9C-%EC%A7%84%ED%96%89%ED%95%98%EB%9D%BC</guid>
            <pubDate>Tue, 29 Apr 2025 07:05:51 GMT</pubDate>
            <description><![CDATA[<h2 id="1-cloneable과-clone-메서드-개요">1. Cloneable과 clone() 메서드 개요</h2>
<p>자바에서 객체 복사를 지원하기 위해 Cloneable 인터페이스와 clone() 메서드가 존재한다.</p>
<ul>
<li>Cloneable은 메서드가 하나도 없는 인터페이스다.<ul>
<li>메서드가 하나도 없이 단순하게 복사할 수 있는 객체라고 표시만 한다.<ul>
<li>이렇게 표시만 하는 인터페이스를 마커 인터페이스(Marker Interface)라고 부른다.
<img src="blob:https://velog.io/573ee6da-9f2e-4e81-9be1-7479ce655f7e" alt="업로드중.."></li>
</ul>
</li>
</ul>
</li>
<li>clone() 메서드는 Object클래스에 protected로 선언되어 있고, 직접 호출이 불가능하다.</li>
<li>Cloneable 인터페이스를 구현하지 않은 객체에서 clone()을 호출하면 CloneNotSupportedException 예외가 발생한다.</li>
<li>즉, Cloneable을 구현해야만 clone() 메서드를 정상적으로 호출할 수 있다.</li>
<li>Cloneable 인터페이스를 구현한 객체에서 clone()을 호출하면 필드별 폭사(얕은 복사)를 수행하게 된다.</li>
</ul>
<h2 id="2-clone-메서드-사용-시-주의사항">2. clone() 메서드 사용 시 주의사항</h2>
<ul>
<li>Cloneable을 구현했다고 해서 외부에서 바로 clone()을 호출할 수는 없다.<ul>
<li>clone() 메서드는 기본적으로 protected 접근 제어자이기 때문이다.</li>
<li>따라서 clone을 외부에서 사용할 수 있도록 열어주려면 public으로 재정의해줘야 한다.</li>
</ul>
</li>
<li>super.clone() 호출은 필수다.<ul>
<li>Object의 clone()은 단순한 메모리 복사를 수행하기 때문이다.</li>
<li>super.clone()을 호출하면 객체를 복사할 수 있다.<pre><code class="language-java">public class PhoneNumber implements Cloneable {
@Override
public PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone(); // ①
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(); // ② 발생할 수 없음
    }
}
}</code></pre>
① super.clone()은 Object의 protected Object clone() 메서드를 호출한다.
② AssertionError()를 던진다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>처음에 읽을 때 왜 catch문 내부에서 AssertionError()를 던지는지 이해가 안됐었다.
이 클래스는 이미 Cloneable을 구현했기 때문에 예외가 발생할 일이 없기 때문이다.
catch 블록은 사실상 절대 실행되지 않는 코드다. 그래서 AssertionError를 던져 &quot;일어날 수 없는 일이다.&quot;라는 것을 명시해주기 위해서 작성된 것이였다.</p>
</blockquote>
<h2 id="3-얕은-복사와-깊은-복사">3. 얕은 복사와 깊은 복사</h2>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="left">얕은 복사(Shallow Copy)</th>
<th align="left">깊은 복사(Deep Copy)</th>
</tr>
</thead>
<tbody><tr>
<td align="center">정의</td>
<td align="left">필드의 참조값만 복사</td>
<td align="left">참조 대상 객체 자체도 복사</td>
</tr>
<tr>
<td align="center">특징</td>
<td align="left">원본과 복사본이 같은 객체를 가리킴</td>
<td align="left">독립적인 새로운 객체 생성</td>
</tr>
<tr>
<td align="center">예시</td>
<td align="left">배열 필드 복사 시, 배열 객체 자체는 공유</td>
<td align="left">배열 필드 복사 시, 새로운 배열도 생성해서 복사</td>
</tr>
<tr>
<td align="center">위험성</td>
<td align="left">한 쪽을 수정하면 다른 쪽에도 영향</td>
<td align="left">서로 완전히 독립적</td>
</tr>
</tbody></table>
<p>clone() 메서드는 기본적으로 <strong>얕은 복사</strong>를 한다.
만약 깊은 복사를 해야된다면 clone()메서드 안에 별도로 복사 로직을 추가해줘야한다.</p>
<h2 id="4-clone-사용은-신중히">4. clone() 사용은 신중히</h2>
<ul>
<li>clone()를 사용할 때 원본 객체에 아무 해를 끼치지 않으면서 동시에 복제된 객체의 불변식을 보장해줘야한다.</li>
<li>clone()은 생성자와 비슷한 책임을 가지지만, 명확한 초기화 과정을 보장하지 않는다.</li>
<li>얕은 복사만 수행하므로 복잡한 객체 그래프(예: 배열, 다른 객체를 참조하는 경우)에서는 문제를 일으킬 수 있다.</li>
<li>동기화 문제(멀티스레드 환경)도 발생할 수 있다.</li>
</ul>
<h3 id="41-가변-객체는-주의가-필요하다">4.1. 가변 객체는 주의가 필요하다</h3>
<ul>
<li><p>가변 객체를 복사하게 되면 clone()은 얕은 복사를 수행하기 때문에 복사한 객체도 같은 객체를 가리키게되어서 복사한 객체를 수정하는 경우 원본도 수정되어 문제가 발생할 수 있다.</p>
<ul>
<li>내부 필드가 가변 객체(예: 배열, 리스트 등) 일 경우 복제된 객체와 원본이 같은 객체를 공유하게 된다.</li>
</ul>
</li>
<li><p>이런 문제를 피하려면 clone 내부에서 해당 가변 필드도 복사해줘야 한다(깊은 복사). </p>
<h4 id="stack-클래스">Stack 클래스</h4>
<pre><code class="language-java">public class Stack {
  private Object[] elements;

  // 생성자 생략

  @Override
  public Stack clone() {
      try {
          return (Stack) super.clone();
      } catch (CloneNotSupportedException e) {
          throw new AssertionError();
      }
  }

  // 메서드 생략
}</code></pre>
<p>위의 코드는 잘못된 clone() 메서드 재정의 방식이다.
원본 객체에 영향을 끼치지 않도록 하려면 아래와 같이 elements 배열의 clone을 재귀적으로 호출해줘야한다.</p>
<pre><code class="language-java">  @Override
  public Stack clone() {
      try {
          Stack result = (Stack) super.cloneO;
          result.elements = elements.clone(); // 배열까지 복제(깊은 복사)
          return result;
      } catch (CloneNotSupportedException e) {
          throw new AssertionError();
      }
  }</code></pre>
</li>
<li><p>배열은 clone 기능을 유일하게 제대로 사용하는 예시이기 때문에 배열을 복제할 때는 clone 메서드 사용을 권장한다.</p>
</li>
</ul>
<p>위의 예제에서 만약 elements 필드가 final이였다면 위의 코드는 작동하지 않을 것이다. final 키워드로 작성된 필드에는 새로운 값을 할당할 수 없기 때문이다.</p>
<ul>
<li>Cloneable 아키텍처는 가변 객체를 참조하는 필드는 final로 선언하라는 용법과 충돌한다.<ul>
<li>clone()을 사용할 때 복사 대상 객체의 불변성을 유지하고 오류 가능성을 줄이기 위해서 final로 선언하라는 뜻이다.</li>
</ul>
</li>
<li>복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 키워드를 제거해야 할 수도 있다.</li>
</ul>
<h4 id="deepcopy">deepCopy()</h4>
<p>자바의 HashTable처럼 내부에 가변 객체(예: 배열, 리스트, 키-값 쌍 등)를 가지고 있는 객체는 clone()을 재정의할 때 깊은 복사를 해주어야 한다.</p>
<pre><code class="language-java">public class MyHashTable implements Cloneable {
    private Entry[] buckets;

    static class Entry {
        final String key;
        String value;
        Entry next;

        Entry(String key, String value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        Entry deepCopy() {
            return new Entry(key, value, next == null ? null : next.deepCopy());
        }
    }

    @Override
    public MyHashTable clone() {
        try {
            MyHashTable result = (MyHashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for (int i = 0; i &lt; buckets.length; i++) {
                if (buckets[i] != null)
                    result.buckets[i] = buckets[i].deepCopy();
            }
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}</code></pre>
<ul>
<li>위의 코드는 buckets 배열 안에 있는 Entry 객체들을 <strong>재귀적으로 깊은 복사</strong> 하는 방식이다.</li>
<li>이처럼 <strong>복잡한 객체 그래프가 있는 경우</strong> clone() 구현은 매우 주의해서 작성해야 한다.</li>
<li>추가로 buckets 배열이 너무 길지 않을 때는 잘 작동하겠지만 재귀 호출을 하기 때문에 stack overflow가 발생할 수 있어서 deepCopy를 재귀 호출하는 대신에 반복자를 써서 순회하는 방법으로 사용해야 한다.<pre><code class="language-java">      Entry deepCopy() {
          Entry result = new Entry(key, value, next);
          for (Entry p = result; p.next != null; p = p.next)
          p.next = new Entry(p.next.key, p.next.value, p.next.next);
          return result
      }</code></pre>
</li>
</ul>
<h3 id="42-복사-생성자와-복사-팩터리">4.2. 복사 생성자와 복사 팩터리</h3>
<ul>
<li><p>복잡하고 위험한 clone 방식 대신 아래처럼 복사 생성자를 사용하는 것도 좋은 대안이다.</p>
<ul>
<li><p>복사 생성자 : 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자</p>
</li>
<li><p>복사 팩터리 : 복사 생성자를 모방한 정적 팩터리</p>
<pre><code class="language-java">public class PhoneNumber {
private final int areaCode, prefix, lineNumber;

public PhoneNumber(PhoneNumber original) { // 복사 생성자
    this.areaCode = original.areaCode;
    this.prefix = original.prefix;
    this.lineNumber = original.lineNumber;
}

public static PhoneNumber copyOf(PhoneNumber original) { // 복사 팩터리
    return new PhoneNumber(original);
}
}</code></pre>
</li>
</ul>
</li>
<li><p>명확하고 안전하다.</p>
</li>
<li><p>클래스를 상속하지 않아도 된다.</p>
</li>
<li><p>final 필드 용법과 출돌하지 않는다.</p>
</li>
<li><p>불필요한 예외처리가 필요하지 않다.</p>
</li>
<li><p>형변환이 필요없다.</p>
</li>
</ul>
<h2 id="6-결론">6. 결론</h2>
<ul>
<li>Cloneable을 구현하면 반드시 clone()을 재정의해야 하며, super.clone()을 호출해야 한다.</li>
<li>하지만 많은 경우 복사 생성자나 복사 팩터리를 사용하는게 더 좋은 방법이다.</li>
<li>깊은 복사를 구현할 경우, 내부 객체 구조를 꼼꼼히 관리해야 한다.</li>
<li>새로운 클래스나 라이브러리를 설계할 때는 Cloneable를 확장하면 안되고, 구현해서는 안된다.</li>
<li>배열만은 clone() 사용이 간단하고 효율적이므로 예외로 둘 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Item 12: toString을 항상 재정의하라]]></title>
            <link>https://velog.io/@daram_dev/Item-12-toString%EC%9D%84-%ED%95%AD%EC%83%81-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@daram_dev/Item-12-toString%EC%9D%84-%ED%95%AD%EC%83%81-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</guid>
            <pubDate>Mon, 03 Mar 2025 17:03:21 GMT</pubDate>
            <description><![CDATA[<h2 id="1-tostring을-재정의해야-하는-이유">1. toString을 재정의해야 하는 이유</h2>
<ul>
<li><p>기본 <code>Object.toString()</code>은 <code>클래스이름@16진수해시코드</code>를 반환한다.</p>
</li>
<li><p>의미 없는 정보만 제공하기 때문에 가독성이 떨어지게되어 <strong>사용자에게 유용한 정보를 포함하는 toString()을 직접 재정의해야 한다.</strong></p>
</li>
<li><p>예를 들어, <code>PhoneNumber</code> 클래스의 기본 <code>toString()</code>은 아래와 같다.</p>
<pre><code class="language-java">PhoneNumber p = new PhoneNumber(82, 1234, 5678);
System.out.println(p);
// 출력: PhoneNumber@3124625a (알 수 없는 정보)</code></pre>
</li>
<li><p>만약 <code>toString()</code>을 재정의해서 사람이 읽기 쉽게 만들었다면?</p>
<pre><code class="language-java">PhoneNumber p = new PhoneNumber(82, 1234, 5678);
System.out.println(p);
// 출력: +82-1234-5678</code></pre>
</li>
<li><p><strong>출력만 깔끔해지는 것이 아니라 객체의 핵심 정보를 담고 있게 되기 때문에 로그, 디버깅, 오류 메시지에서도 더 유용한 정보가 포함될 수 있게 된다.</strong></p>
</li>
</ul>
<h2 id="2-tostring을-재정의하는-방법">2. toString()을 재정의하는 방법</h2>
<h3 id="tostring-구현-예시">toString() 구현 예시</h3>
<pre><code class="language-java">@Override
public String toString() {
    return String.format(&quot;+%02d-%04d-%04d&quot;, countryCode, prefix, lineNumber);
}</code></pre>
<ul>
<li><code>String.format()</code>을 사용해 <strong>사람이 읽기 쉬운 형식</strong>으로 변환했다.</li>
<li>예제 출력:<pre><code class="language-java">PhoneNumber p = new PhoneNumber(82, 1234, 5678);
System.out.println(p); // 출력: +82-1234-5678</code></pre>
</li>
</ul>
<h2 id="3-tostring-작성-시-고려할-점">3. toString() 작성 시 고려할 점</h2>
<h3 id="3-1-모든-필드를-포함해야-하는가">3-1. <strong>모든 필드를 포함해야 하는가?</strong></h3>
<ul>
<li>필드를 전부 포함하면 너무 길어질 수 있다.</li>
<li>객체의 <strong>주요 정보만 포함</strong>하는 것이 좋다.</li>
<li>예를 들어, <strong>비밀번호 같은 민감한 정보는 제외해야 한다.</strong></li>
</ul>
<h3 id="3-2-tostring을-문서화해야-하는가">3-2. <strong>toString()을 문서화해야 하는가?</strong></h3>
<ul>
<li>포맷을 명시화해도되고 안해도 되지만 의도는 명확히 밝혀야한다.</li>
<li><strong>만약 형식이 바뀔 가능성이 있다면 명시적으로 &quot;형식이 바뀔 수 있음&quot;을 문서화해야 한다.</strong></li>
</ul>
<p>예:</p>
<pre><code class="language-java">/**
 * 이 전화번호의 문자열 표현을 반환한다.
 * 이 문자열은 &quot;+XX-YYYY-ZZZZ&quot; 형태의 13글자로 구성된다.
 * 형식: +국가코드-중간번호-끝번호 (+82-1234-5678)
 */
@Override
public String toString() {
    return String.format(&quot;+%02d-%04d-%04d&quot;, countryCode, prefix, lineNumber);
}</code></pre>
<ul>
<li>추가로 포맷 명시 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공해야 한다.</li>
</ul>
<h4 id="참고-record-클래스">참고: record 클래스</h4>
<ul>
<li>불변 데이터 객체(VO, DTO) 생성을 위해 설계된 클래스이다.</li>
<li><strong>자바 16+에서는 <code>record</code> 클래스 를 사용하면 toString이 자동 생성된다.</strong><pre><code class="language-java">public record PhoneNumber(int countryCode, int areaCode, int number) {}</code></pre>
</li>
<li>불변 데이터 객체를 간결하게 표현하기 위해 사용되는 클래스이다.</li>
<li>아래의 기능을 자동으로 생성해준다.<ul>
<li>불변 필드 (final 포함)</li>
<li>생성자</li>
<li>Getter 메서드 (필드명과 동일)</li>
<li>toString() 자동 생성</li>
<li>equals() &amp; hashCode() 자동 생성</li>
</ul>
</li>
<li>record 단점 : 필드 변경 불가능, 상속 불가능 등이 존재한다.</li>
</ul>
<h2 id="4-결론">4. 결론</h2>
<ul>
<li><code>toString()</code>을 재정의하면 <strong>가독성, 디버깅, 로깅, 유지보수성이 향상</strong>된다.</li>
<li>핵심 정보만 포함하되 형식이 고정될 경우 이를 문서화해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Item 11. equals를 재정의하려거든 hashCode도 재정의하라]]></title>
            <link>https://velog.io/@daram_dev/Item-11.-equals%EB%A5%BC-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%A0%A4%EA%B1%B0%EB%93%A0-hashCode%EB%8F%84-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@daram_dev/Item-11.-equals%EB%A5%BC-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%A0%A4%EA%B1%B0%EB%93%A0-hashCode%EB%8F%84-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</guid>
            <pubDate>Mon, 03 Mar 2025 16:28:49 GMT</pubDate>
            <description><![CDATA[<h2 id="1-왜-equals를-재정의하면-hashcode도-재정의해야-할까">1. 왜 equals를 재정의하면 hashCode도 재정의해야 할까?</h2>
<ul>
<li><code>equals</code>를 재정의하면 <strong>같은 논리적 값</strong>을 가진 객체들은 서로 <strong>동일한 <code>hashCode</code></strong>를 반환해야 한다.</li>
<li><code>hashCode</code>를 재정의하지 않으면 않으면 <code>HashMap</code>, <code>HashSet</code>, <code>HashTable</code> 같은 <strong>해시 기반 컬렉션</strong>에서 동작이 제대로 이루어지지 않는다. 즉 동일한 객체라도 다른 해시값을 가질 수 있게된다는 뜻이다.</li>
<li><code>hashCode()</code>를 재정의하지 않으면 기본적으로 <code>Object</code>의 <code>hashCode()</code>를 사용한다.<ul>
<li><code>Object</code>의 기본 <code>hashCode()</code>는 <strong>객체의 메모리 주소를 기반</strong>으로 생성되므로, <code>equals()</code>를 재정의해도 논리적으로 같은 객체가 다른 해시값을 가질 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="hashcode를-재정의하지-않았을-때-발생하는-문제"><code>hashCode()</code>를 재정의하지 않았을 때 발생하는 문제</h3>
<pre><code class="language-java">import java.util.HashMap;

public class PhoneNumber {
    private final int areaCode;
    private final int prefix;
    private final int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof PhoneNumber)) return false;
        PhoneNumber that = (PhoneNumber) o;
        return areaCode == that.areaCode &amp;&amp;
               prefix == that.prefix &amp;&amp;
               lineNumber == that.lineNumber;
    }

    // hashCode() 미구현

    public static void main(String[] args) {
        HashMap&lt;PhoneNumber, String&gt; map = new HashMap&lt;&gt;();
        PhoneNumber number1 = new PhoneNumber(123, 456, 7890);
        map.put(number1, &quot;Alice&quot;);

        PhoneNumber number2 = new PhoneNumber(123, 456, 7890);
        System.out.println(map.get(number2)); // null이 출력됨
    }
}</code></pre>
<ul>
<li>위 코드에서 <code>number1</code>과 <code>number2</code>는 <strong>같은 논리적 값</strong>을 가지지만 <code>hashCode()</code>가 없으므로 <code>HashMap</code> 내부에서 같은 객체로 인식하지 못해 <code>null</code>이 출력된다.</li>
</ul>
<h3 id="해결-방법--hashcode-재정의">해결 방법 : <code>hashCode()</code> 재정의</h3>
<pre><code class="language-java">@Override
public int hashCode() {
    return Objects.hash(areaCode, prefix, lineNumber);
}</code></pre>
<ul>
<li><code>Objects.hash()</code>를 활용하면 간결하게 <code>hashCode()</code>를 재정의할 수 있다.</li>
</ul>
<h2 id="2-hashcode-작성-요령">2. <code>hashCode()</code> 작성 요령</h2>
<h3 id="2-1-hashcode는-같은-객체에-대해-항상-같은-값을-반환해야-한다">2-1. <code>hashCode()</code>는 같은 객체에 대해 <strong>항상 같은 값을 반환</strong>해야 한다.</h3>
<ul>
<li>객체의 필드 값이 변하지 않는다면 <code>hashCode()</code>는 항상 같은 값을 반환해야 한다.</li>
<li>동일한 애플리케이션 실행 중에는 같은 객체가 동일한 해시값을 가져야 한다.</li>
</ul>
<h3 id="2-2-equals가-같은-두-객체는-반드시-같은-hashcode를-가져야-한다">2-2. <code>equals()</code>가 같은 두 객체는 <strong>반드시 같은 hashCode를 가져야 한다.</strong></h3>
<ul>
<li><code>a.equals(b) == true</code>이면 <code>a.hashCode() == b.hashCode()</code>여야 한다.</li>
<li>하지만 다른 객체라도 같은 <code>hashCode()</code>를 가질 수 있긴하다.</li>
</ul>
<h3 id="2-3-서로-다른-객체는-반드시-다른-hashcode를-가질-필요는-없지만-다르면-좋다">2-3. 서로 다른 객체는 <strong>반드시 다른 <code>hashCode()</code>를 가질 필요는 없지만 다르면 좋다.</strong></h3>
<ul>
<li>해시 충돌이 적을수록 <code>HashMap</code>, <code>HashSet</code> 등의 성능이 좋아지기 때문이다.</li>
</ul>
<h3 id="2-4-hashcode를-구현하는-방법">2-4. hashCode를 구현하는 방법</h3>
<ol>
<li><p><strong>기본적인 해시 계산 방식 (31을 사용한 해시 함수)</strong></p>
<pre><code class="language-java">@Override
public int hashCode() {
    int result = Integer.hashCode(areaCode);
    result = 31 * result + Integer.hashCode(prefix);
    result = 31 * result + Integer.hashCode(lineNumber);
    return result;
}</code></pre>
<ul>
<li>31을 곱하는 이유 : 홀수이면서 소수이기 때문에 곱했을 때 해시 충돌을 줄일 수 있다.</li>
<li>필드가 많을수록 해시값을 섞는 효과가 커진다.</li>
</ul>
</li>
<li><p><strong>Objects.hash() 사용</strong></p>
<pre><code class="language-java">@Override
public int hashCode() {
    return Objects.hash(areaCode, prefix, lineNumber);
}</code></pre>
<ul>
<li><code>Objects.hash()</code>를 사용하면 내부적으로 적절한 해시 함수를 사용해 필드 값을 결합해준다.</li>
<li>대신 속도가 느리다는 단점이 있다.</li>
</ul>
</li>
<li><p><strong>해시 코드 캐싱 (불변 객체일 때 최적화 가능)</strong></p>
<pre><code class="language-java">private int hash; // 캐시된 해시값

@Override
public int hashCode() {
    if (hash == 0) {
        hash = Objects.hash(areaCode, prefix, lineNumber);
    }
    return hash;
}</code></pre>
<ul>
<li>불변 객체라면 해시값을 한 번 계산한 후 캐싱하여 성능을 최적화할 수 있다.</li>
<li><code>String</code> 클래스도 같은 방식으로 <code>hashCode()</code>를 캐싱한다.</li>
</ul>
</li>
</ol>
<h2 id="3-google-guava와-autovalue를-이용한-자동-생성">3. Google Guava와 AutoValue를 이용한 자동 생성</h2>
<h3 id="3-1-google-guava의-objectshashcode-사용">3-1. Google Guava의 <code>Objects.hashCode()</code> 사용</h3>
<pre><code class="language-java">@Override
public int hashCode() {
    return com.google.common.base.Objects.hashCode(areaCode, prefix, lineNumber);
}</code></pre>
<ul>
<li><strong>Guava 라이브러리</strong>를 사용하면 해시 코드를 자동으로 생성할 수 있다.</li>
<li><code>Objects.hashCode()</code>와 비슷하지만 더 정교한 해싱 알고리즘을 사용한다.</li>
</ul>
<h3 id="4-2-autovalue를-활용한-자동-생성">4-2. AutoValue를 활용한 자동 생성</h3>
<pre><code class="language-java">@AutoValue
abstract class PhoneNumber {
    abstract int areaCode();
    abstract int prefix();
    abstract int lineNumber();


    static PhoneNumber create(int areaCode, int prefix, int lineNumber) {
        return new AutoValue_PhoneNumberint areaCode, int prefix, int lineNumber);
    }
}</code></pre>
<ul>
<li><strong>Google AutoValue</strong>를 사용하면 equals와 hashCode를 자동 생성할 수 있다.</li>
<li><code>@AutoValue</code>를 붙이면 <code>hashCode()</code>를 직접 구현할 필요 없이 컴파일러가 자동으로 생성해 준다.</li>
<li><strong>equals/hashCode 단위 테스트를 생략해도 된다.(item10에서와 동일한 내용)</strong></li>
</ul>
<h2 id="4-결론">4. 결론</h2>
<ol>
<li><strong>equals()를 재정의하면 hashCode()도 반드시 재정의해야 한다.</strong></li>
<li><strong>같은 객체라면 항상 같은 hashCode를 반환해야 한다.</strong></li>
<li><strong>해시 함수의 품질이 중요하며, 31을 사용한 해시 함수가 일반적으로 권장된다.</strong></li>
<li><strong>Objects.hash()를 사용하면 간결하게 구현 가능하다.</strong></li>
<li><strong>불변 객체라면 hashCode를 한 번만 계산하고 캐싱하는 것도 좋은 방법이다.</strong></li>
<li><code>Google Guava</code>, <code>AutoValue</code>를 사용하면 더욱 안전하고 최적화된 hashCode를 생성할 수 있다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Item 10. equals는 일반 규약을 지켜 재정의하라]]></title>
            <link>https://velog.io/@daram_dev/Item-10.-equals%EB%8A%94-%EC%9D%BC%EB%B0%98-%EA%B7%9C%EC%95%BD%EC%9D%84-%EC%A7%80%EC%BC%9C-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@daram_dev/Item-10.-equals%EB%8A%94-%EC%9D%BC%EB%B0%98-%EA%B7%9C%EC%95%BD%EC%9D%84-%EC%A7%80%EC%BC%9C-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC</guid>
            <pubDate>Fri, 28 Feb 2025 09:11:37 GMT</pubDate>
            <description><![CDATA[<h2 id="1-equals를-재정의하지-않는-게-좋은-상황">1. equals를 재정의하지 않는 게 좋은 상황</h2>
<p>꼭 필요한 경우가 아니라면 equals를 재정의하는 것이 좋지 않다.
equals를 재정의하지 않는 것이 좋은 상황은 대표적으로 아래 4가지 상황이 있다.</p>
<ol>
<li><strong>각 인스턴스가 본질적으로 고유하다.</strong><ul>
<li>값이 아닌 동작(행위) 중심 객체라면 동치성이 의미가 없다.</li>
<li>예 : Thread, Random<ul>
<li>Thread는 실행 흐름을 나타낸다.</li>
<li>Random은 seed값에 따라서 생성되는 난수도 다르고 동작이 다를 수 있다.</li>
</ul>
</li>
</ul>
</li>
<li><strong>인스턴스의 논리적 동치성(logical equality)을 검사할 일이 없다.</strong><ul>
<li>논리적 동치성 검사가 필요없다고 판단되는 경우 기본 Object equals만으로도 해결된다.</li>
<li>예 : REST API 응답용으로 DTO 클래스(ResponseDto)를 만들었을 때도 이 경우 인 것 같다. 한 번 생성해서 클라이언트로 전달하면 끝이기 때문에 굳이 내부 상태를 비교할 일이 없는 것 같다.</li>
</ul>
</li>
<li><strong>상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다</strong><ul>
<li>예 : Set, Map 등의 일부 구현체가 해당된다. 상위 equals가 이미 논리적 동치성을 제공하기 때문이다.</li>
</ul>
</li>
<li><strong>클래스가 private이거나 package-private이며, equals 메서드를 호출할 일이 없다</strong><ul>
<li>equlas가 실수로라도 호출되지 않도록 에러를 던져서 예방한다.<pre><code class="language-java">@Override
public boolean equals(Object o) {
   throw new AssertionError(); // 호출 금지
}</code></pre>
</li>
</ul>
</li>
</ol>
<h2 id="2-equals를-재정의해야-할-때">2. equals를 재정의해야 할 때</h2>
<ul>
<li>값 클래스(예 : <code>Integer</code>, <code>String</code>)처럼 <strong>“논리적 동치성”</strong>을 비교해야 하는 클래스는 재정의가 필요하다.</li>
<li>단, <strong>인스턴스 통제</strong>(예 : 싱글턴, 열거 타입(Enum))나 <strong>값이 같은 인스턴스를 둘 이상 만들지 않는</strong> 경우는 재정의가 필요 없다.</li>
</ul>
<h2 id="3-object-명세-규약-동치관계">3. Object 명세 규약 (동치관계)</h2>
<p>Object의 <code>equals</code>는 <strong>동치관계(equivalence relation)</strong>를 구현해야 하며, <strong>다섯 가지 요건</strong>을 만족한다.</p>
<ol>
<li><strong>반사성 (reflexivity)</strong> : <code>x.equals(x)</code>는 true</li>
<li><strong>대칭성 (symmetry)</strong> : <code>x.equals(y)</code>가 true면, <code>y.equals(x)</code>도 true</li>
<li><strong>추이성 (transitivity)</strong> : x,y,z가 각각 equals면 x,z도 equals</li>
<li><strong>일관성 (consistency)</strong> : 두 객체가 변하지 않으면, 여러 번 equals 호출 시 결과가 변하면 안 됨</li>
<li><strong>null-아님</strong> : null이 아닌 객체 x에 대해 <code>x.equals(null)</code>은 false</li>
</ol>
<h2 id="4-잘못된-코드-예시">4. 잘못된 코드 예시</h2>
<h3 id="4-1-대칭성-위배">4-1. 대칭성 위배</h3>
<pre><code class="language-java">public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = Objects.requiredNonNull(s);
    }

    // 잘못된 equals (대칭성 위배)
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString) {
            // 같거나 다른 CaseInsensitiveString이면 대소문자 구분 없이 비교
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        }
        if (o instanceof String) {
            // 만약 o가 일반 String이면, 이것도 case-insensitive로 비교
            return s.equalsIgnoreCase((String) o); 
        }
        return false;
    }

    // ...
}</code></pre>
<h4 id="코드-해석">코드 해석</h4>
<ol>
<li><code>o instanceof CaseInsensitiveString</code> → <code>CaseInsensitiveString</code> 객체라면,<ul>
<li><code>this.s.equalsIgnoreCase(((CaseInsensitiveString) o).s)</code>로 비교한다.</li>
<li>두 문자열을 대소문자 구분 없이 같은지 확인한다.</li>
</ul>
</li>
<li><code>o instanceof String</code> → <code>o</code>가 일반 <code>String</code>이면,<ul>
<li><code>this.s.equalsIgnoreCase((String) o)</code>로 비교</li>
<li><code>CaseInsensitiveString</code> vs <code>String</code> 비교도 대소문자 무시로 equals를 <code>true</code>/<code>false</code> 판단한다.</li>
</ul>
</li>
<li>위의 두 경우 모두 아니면 <code>false</code>를 반환한다.</li>
</ol>
<h4 id="대칭성-위배인-이유">대칭성 위배인 이유</h4>
<p>대칭성은 x.equals(y)가 true면, y.equals(x)도 true여야 한다.</p>
<pre><code class="language-java">CaseInsensitiveString cis = new CaseInsensitiveString(&quot;Polish&quot;);
String str = &quot;&quot;;

System.out.println(cis.equals(str)); // A
System.out.println(str.equals(cis)); // B</code></pre>
<ol>
<li><code>cis.equals(str)</code> (A 라인)<ul>
<li><code>o instanceof String</code> 분기에 해당한다.</li>
<li><code>&quot;Polish&quot;.equalsIgnoreCase(&quot;polish&quot;)</code> → <code>true</code></li>
<li>따라서 <code>cis.equals(str)</code> → <code>true</code></li>
</ul>
</li>
<li><code>str.equals(cis)</code> (B 라인)<ul>
<li>여기서 <code>str</code>은 단순한 <code>String</code> 객체 → <code>String</code>의 <code>equals</code>는 같은 문자열 타입이고, 동일한 내용인지 검사한다.</li>
<li><code>cis</code>는 타입이 <code>CaseInsensitiveString</code>이기 때문에 타입이 다르다.</li>
<li><code>str.equals(cis)</code> → <code>false</code></li>
<li><code>&quot;polish&quot;.equals( CaseInsensitiveString )</code> → <code>false</code></li>
</ul>
</li>
</ol>
<p>A라인에서는 true를 반환, B라인에서는 false를 반환하기 때문에 대칭성이 위배된 것이다.</p>
<p>이 문제를 해결하려고 String과도 연동하려고 하면 안된다.
아래와 같이 해결하는 정도로 equals를 오버라이딩 해야한다.</p>
<pre><code class="language-java">@Override
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString &amp;&amp;
        ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}</code></pre>
<p>이렇게 하면 상대가 <code>CaseInsensitiveString</code>인지 확인한 뒤에 <code>CaseInsensitiveString</code>로 캐스팅하고 대소문자를 무시하고 비교해서 대칭성을 지킬 수 있다.</p>
<h3 id="4-2-추이성-위배">4-2. 추이성 위배</h3>
<pre><code class="language-java">public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x &amp;&amp; p.y == y;
       }
    // ...
}

public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    // 잘못된 equals (대칭성은 지키지만 추이성 위배)
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;

        // 만약 o가 Point이지만 ColorPoint는 아니면,
        // o.equals(this)로 역방향 비교를 시도해서 색상을 무시하고 비교하게 된다.
        if (!(o instanceof ColorPoint)) {
            return o.equals(this);
        }

        // o가 ColorPoint면, 색상까지 비교
        ColorPoint cp = (ColorPoint) o;
        return super.equals(cp) &amp;&amp; color.equals(cp.color);
    }
}</code></pre>
<h4 id="추이성-위배인-이유">추이성 위배인 이유</h4>
<pre><code class="language-java">ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

p1.equals(p2); // A
p2.equals(p3); // B
p1.equals(p3); // C</code></pre>
<ol>
<li><code>p1.equals(p2)</code> (A라인)<ul>
<li><code>p1.equals(p2)</code> -&gt; <code>true</code></li>
<li><code>ColorPoint</code> 내부에서 <code>o</code>가 <code>Point</code>면 <code>o.equals(this)</code> 호출한다.</li>
<li><code>p2.equals(p1)</code>은 <code>x</code>,<code>y</code>만 비교</li>
</ul>
</li>
<li><code>p2.equals(p3)</code> (B라인)<ul>
<li><code>p2.equals(p3)</code> -&gt; <code>true</code></li>
<li>같은 <code>Point</code> 객체</li>
</ul>
</li>
<li><code>p1.equals(p3)</code> (C라인)<ul>
<li><code>p1.equals(p3)</code> -&gt; <code>false</code></li>
<li>같은 <code>ColorPoint</code>인 경우에는 색상까지 비교하는데 <code>p1</code>은 <code>RED</code>, <code>p3</code>는 <code>BLUE</code>로 색상이 다르다.</li>
</ul>
</li>
</ol>
<h3 id="4-3-리스코프-치환-원칙-위배">4-3. 리스코프 치환 원칙 위배</h3>
<pre><code class="language-java">@Override
public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x &amp;&amp; p.y == y;
}</code></pre>
<h4 id="리스코프-치환-원칙-위배인-이유">리스코프 치환 원칙 위배인 이유</h4>
<ol>
<li><code>getClass() != getClass()</code></li>
</ol>
<ul>
<li>상대 객체 <code>o</code>의 실제 클래스가 나의 클래스와 정확히 일치해야만 <code>equals</code>가 <code>true</code>로 나오게 된다.</li>
<li><code>Point</code>와 <code>ColorPoint</code>(하위 클래스)가 절대 같다고 나오지 않게 된다.</li>
</ul>
<ol start="2">
<li>리스코프 치환 원칙(LSP)이 깨진다.</li>
</ol>
<ul>
<li><p>LSP는 하위 클래스는 상위 클래스가 쓰이는 모든 곳에서 호환되어야 한다는 원칙이다.</p>
</li>
<li><p>여기서 <code>Point</code>를 상속한 <code>ColorPoint</code>가 <code>x</code>,<code>y</code>만 같으면 같다고 생각하고 싶어도 클래스가 다르다는 이유로 <code>equals</code>가 <code>false</code>가 돼버리게 된다.</p>
</li>
<li><p>하위 클래스는 상위 클래스와 동등해질 수 없게되어서 하위 클래스를 상위 타입으로 쓰는 호환성이 깨지는 결과가 생기는 것이다.</p>
<h4 id="해결-방법우회방법">해결 방법(우회방법)</h4>
<p>ColorPoint가 Point를 확장하므로 상위/하위 클래스 간에 equals가 안되게 되는 것이므로 합성(컴포지션) 방법으로 우회하는 방법이다.</p>
<pre><code class="language-java">public class ColorPoint {
  private final Point point; // 합성
  private final Color color;

  public ColorPoint(int x, int y, Color color) {
      this.point = new Point(x, y); // private 필드로 갖고있음
      this.color = Objects.requireNonNull(color);
  }

  // 만약 Point의 x,y로 뭔가 계산이 필요하면 point 필드를 사용

  // 뷰 메서드
  public Point asPoint() {
      return this.point; // 일반 Point 반환
  }

  @Override
  public boolean equals(Object o) {
      if (!(o instanceof ColorPoint)
          return false;
      ColorPoint cp = (ColorPoint) o;
      return cp.point.equals(point) &amp;&amp; cp.color.equals(color);
  }
  // ...
}</code></pre>
</li>
</ul>
<ol>
<li>뷰 메서드(<code>public Point asPoint()</code>)를 사용.<ul>
<li><code>ColorPoint</code>가 내부적으로 <code>Point</code>를 속성(필드)으로만 두고</li>
<li>필요할 때 <code>asPoint()</code>로 일반 <code>Point</code>를 뷰로써 반환하게 된다.</li>
<li>이렇게 하면 상속 관계가 없어져서 &#39;ColorPoint는 Point이다&#39;가 아니게되고 &#39;ColorPoint는 Point&#39;를 가지고 있다 형태가 된다.</li>
</ul>
</li>
<li>LSP 문제(<code>equals</code> 충돌)가 사라진다.<ul>
<li><code>ColorPoint</code>는 <code>Point</code>를 상속하지 않기 때문에</li>
<li><code>Point</code>인지 아닌지로 인한 <code>equals</code> 충돌이 없어진다.</li>
<li>하위 클래스 관련 고민도 필요 없게된다.</li>
<li>상위/하위 관계가 아니기 때문에 <code>Point</code>와 <code>ColorPoint</code>를 동일 타입으로 다룰 일이 없게된다.</li>
</ul>
</li>
<li>그리고 동치성 결정은 <code>ColorPoint</code>만의 로직으로 가능해진다.<ul>
<li><code>ColorPoint</code>는 색상까지 포함한 비교와 같이 어떤 식으로 <code>equals</code>를 구현할지 스스로 결정하게된다.</li>
<li><code>Point</code>는 이 과정에 대해 아무것도 모른다(별개의 클래스).</li>
</ul>
</li>
</ol>
<h2 id="5-equals-구현-시-단계">5. equals 구현 시 단계</h2>
<ol>
<li><code>==</code> 연산자를 사용해 입력이 자기 자신의 참조인지 확인<ul>
<li><code>if (this == o) return true;</code></li>
</ul>
</li>
<li><code>instanceof</code> 연산자로 입력이 올바른 타입인지 확인<ul>
<li><code>if (!(o instanceof MyClass)) return false;</code></li>
</ul>
</li>
<li>입력을 올바른 타입으로 형변환<ul>
<li><code>MyClass other = (MyClass) o;</code></li>
</ul>
</li>
<li>핵심 필드 비교<ul>
<li>모든 핵심 필드가 같아야 <code>true</code></li>
</ul>
</li>
</ol>
<h3 id="주의사항">주의사항</h3>
<ul>
<li><code>Objects.equals(a, b)</code>을 통해 NullPointException 예방</li>
<li><code>equals</code> 재정의할 때 <strong><code>hashCode()</code>도 재정의</strong>!</li>
<li>입력 타입은 반드시 <code>Object</code> -&gt; <code>@Override</code>하면 실수 예방 가능</li>
</ul>
<h2 id="6-성능-관련-팁">6. 성능 관련 팁</h2>
<ul>
<li>비교 비용이 싼 필드, 서로 다를 확률이 큰 필드를 <strong>우선 비교</strong>하면 빠르게 false를 반환해 효율적이다.</li>
<li>동기화용 lock 필드(동시 접근 제어를 위해서 존재하기 때문에 무관함), 파생된(다른 필드 조합으로 계산된) 필드 등은 꼭 논리적 상태가 아니면 비교에서 제외한다.</li>
</ul>
<h2 id="tip-구글-autovalue-활용">Tip. 구글 AutoValue 활용</h2>
<p>(<a href="https://mvnrepository.com/artifact/com.google.auto.value/auto-value/1.10.1">https://mvnrepository.com/artifact/com.google.auto.value/auto-value/1.10.1</a>)</p>
<ul>
<li><code>AutoValue</code> 라이브러리를 쓰면 <code>equals</code>, <code>hashCode</code>, <code>toString</code> 등을 자동으로 만들어준다고 한다.</li>
<li>컴파일 시점에 <code>AutoValue_MyClass</code> 클래스가 생성되어, <strong>올바른 equals</strong>가 구현된다.</li>
</ul>
<h2 id="7-결론">7. 결론</h2>
<ol>
<li><strong>값 객체</strong>(논리적 동치성 검사 필요)일 때만 equals 재정의해야 한다.</li>
<li><strong>동치관계</strong>(반사성,대칭성,추이성,일관성,null-아님)을 만족해야한다.</li>
<li>상속 구조에서 equals는 매우 조심해야 한다. ColorPoint 예시룰 확인해 보자.</li>
<li>equals를 재정의 할 때 <strong>hashCode</strong>는 반드시 재정의해야한다.</li>
<li>구글 <code>AutoValue</code> 프레임워크를 사용하면 편리하게 작성이 가능하다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Item 9. try-finally보다는 try-with-resources를 사용하라]]></title>
            <link>https://velog.io/@daram_dev/Item-9.-try-finally%EB%B3%B4%EB%8B%A4%EB%8A%94-try-with-resources%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</link>
            <guid>https://velog.io/@daram_dev/Item-9.-try-finally%EB%B3%B4%EB%8B%A4%EB%8A%94-try-with-resources%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC</guid>
            <pubDate>Wed, 26 Feb 2025 06:48:05 GMT</pubDate>
            <description><![CDATA[<h2 id="1-자바에는-직접-닫아야-하는-자원들이-많다">1. 자바에는 직접 닫아야 하는 자원들이 많다</h2>
<p>자바 라이브러리와 프레임워크에는, <strong>직접 자원을 닫아야 하는</strong> 클래스들이 많이있다.</p>
<ul>
<li><code>InputStream</code>, <code>OutputStream</code>, <code>Reader</code>, <code>Writer</code> (IO 관련)</li>
<li><code>java.sql.Connection</code>, <code>Statement</code>, <code>ResultSet</code> (JDBC 관련)</li>
</ul>
<p><strong>JPA</strong>(Hibernate 등)를 사용할 때도 <strong>DB 커넥션</strong>을 잘 닫지 않으면</p>
<ul>
<li>그 커넥션이 반환되지 않아 <strong>풀에 남은 사용 가능한 커넥션 수</strong>가 줄어들게 되고 반복해서 여러 요청이 동시에 들어오면 커넥션 풀이 바닥나서 <strong>새로운 DB 접근이 불가능</strong>해지거나 대기 시간이 길어지게 된다.</li>
<li>이럴 때 <strong>성능 문제</strong>나 <strong>Deadlock</strong>(교착상태, 프로세스 또는 트랜잭션이 서로 점유한 자원을 기다리다가 영원히 기다리게 되는 상태이다.)이 발생할 수 있다.</li>
</ul>
<p>자원 닫기를 놓치게 되면 <strong>예측할 수 없는 오류</strong>로 이어지기 때문에 빠트리면 안되는 중요한 작업이다.</p>
<h2 id="2-자원-닫는-방법">2. 자원 닫는 방법</h2>
<h3 id="2-1-전통적인-방법-try-finally">2-1. 전통적인 방법: <code>try-finally</code></h3>
<p>과거에는 다음처럼 <strong><code>try</code> 블록</strong>에서 자원을 사용하고, <strong><code>finally</code> 블록</strong>에서 자원을 닫았다.</p>
<h4 id="자원이-한-개인-경우">자원이 한 개인 경우</h4>
<pre><code class="language-java">static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}</code></pre>
<ul>
<li><code>br.readLine()</code> 중 <strong>예외</strong>가 발생해도, <code>finally</code> 블록이 실행되며 <code>br.close()</code>를 보장(실행하는 것을 보장하는 것이지 실행하면 반드시 성공한다는 것을 보장하지는 않는다.)한다.</li>
</ul>
<h4 id="자원이-여러-개인-경우">자원이 여러 개인 경우</h4>
<pre><code class="language-java">static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) &gt;= 0)
                out.write(buf, 0, n);
        } finally {
                out.close();
    } finally {
            in.close();
    }
}</code></pre>
<ul>
<li><strong>try 블록이 중첩</strong>되면서 코드가 복잡해진다.</li>
</ul>
<h4 id="예외가-2개-이상-발생하게-되는-경우">예외가 2개 이상 발생하게 되는 경우</h4>
<pre><code class="language-java">public static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine(); // 여기서 예외 발생 가능
    } finally {
        // finally는 *꼭* 실행은 됨. 하지만 close()가 또 실패할 수 있음
        br.close(); // 여기서도 IOException 가능
    }
}</code></pre>
<ul>
<li>만약 장치이상으로 <code>br.readLine()</code>에서 예외가 발생하게 된다면, 장치가 이미 이상하기 때문에 닫는 과정에서 또 다른 IO 예외 <code>close()</code>에서도 예외가 발생할 수가 있다.</li>
<li>이런 경우에 <code>close()</code>를 시도하는 경우 발생한 <strong>두 번째 예외가 첫 번째 예외를 덮어써버리기 때문에</strong> 디버깅이 어려워진다.</li>
<li>두 번째 예외 대신 첫 번째 예외를 기록하도록 만들 수도 있지만 코드가 지저분해진다.</li>
</ul>
<h5 id="두-번째-예외가-첫-번째-예외를-덮어쓰는-경우-예시">두 번째 예외가 첫 번째 예외를 덮어쓰는 경우 예시</h5>
<pre><code class="language-java">package item9;

import java.io.IOException;

class Resource implements AutoCloseable {
    private final String name;

    Resource(String name) {
        this.name = name;
    }

    @Override
    public void close() throws IOException {
        System.out.println(name + &quot; closing...&quot;);
        throw new IOException(&quot;close()에서 발생한 예외: &quot; + name);
    }

    void doSomething() throws Exception {
        // 첫 번째 예외
        throw new Exception(&quot;doSomething() 예외: &quot; + name);
    }
}

public class SuppressedExceptionFinallyDemo {

    public static void main(String[] args) {
        try {
            testFinally();
        } catch (Exception e) {
            System.out.println(&quot;메인에서 잡은 예외: &quot; + e);
            // getSuppressed()를 출력해봐도 suppressed 예외가 없을 것
            // 왜냐하면 try-finally 구조에선 첫 번째 예외가 덮어써지기 때문
            for (Throwable t : e.getSuppressed()) {
                System.out.println(&quot;숨겨진(suppressed) 예외: &quot; + t);
            }
        }
    }

    static void testFinally() throws Exception {
        Resource2 r = null;
        try {
            r = new Resource2(&quot;MyResource&quot;);
            r.doSomething(); // 첫 번째 예외 발생
        } finally {
            if (r != null) {
                r.close(); // 두 번째 예외
            }
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/2bdf053a-d56c-47a8-9614-4d05fad43e50/image.png" alt="">
<strong>실행 흐름</strong></p>
<ol>
<li>r.doSomething()가 첫 번째 예외(Exception(&quot;doSomething() 예외...&quot;))를 던진다.</li>
<li>finally 블록이 실행되며 r.close()에서 두 번째 예외(IOException(&quot;close()에서 발생한 예외...&quot;))가 발생한다.</li>
<li>자바는 두 번째 예외를 던져버리며, 첫 번째 예외 정보가 없어지게 된다(덮어써짐).</li>
</ol>
<h3 id="2-2-자바-7-이후-방법-try-with-resources">2-2. 자바 7 이후 방법: <code>try-with-resources</code></h3>
<p>자바 7부터는 <strong><code>try-with-resources</code></strong>가 도입되면서, 위 문제들이 깔끔하게 해결되었다.</p>
<h4 id="autocloseable-인터페이스"><code>AutoCloseable</code> 인터페이스</h4>
<ul>
<li>자원을 닫아야 하는 클래스는 <strong><code>AutoCloseable</code></strong>을 구현해야한다.<ul>
<li><strong><code>AutoCloseable</code></strong>는 <code>close()</code> 메서드 하나만 정의되어 있는 인터페이스다.</li>
</ul>
</li>
<li>자바 표준 라이브러리나 서드파티 라이브러리 중 대부분이 이미 <code>AutoCloseable</code>을 구현해두었다.</li>
</ul>
<blockquote>
<p><strong>서드파티 라이브러리</strong>란, 자바 표준 라이브러리(공식 JDK) 외부에서 제공되는 라이브러리이다. 예를 들어서 <code>org.apache.*</code>, <code>org.springframework.*</code> 등을 말한다.</p>
</blockquote>
<h4 id="자원이-한-개인-경우-1">자원이 한 개인 경우</h4>
<pre><code class="language-java">static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try (BufferedReader br = new BufferedReader(
                    new FileReader(path))) {
        return br.readLine();
    }
}</code></pre>
<ul>
<li><code>try( ... )</code> 안에 <strong><code>AutoCloseable</code> 구현체</strong>를 생성한다.</li>
<li>블록이 끝나면 <strong>자동으로 <code>close()</code>가 호출</strong>되어 자원 해제가 이루어진다.</li>
</ul>
<h4 id="자원이-여러-개인-경우-1">자원이 여러 개인 경우</h4>
<pre><code class="language-java">static void copy(String src, String dst) throws IOException {
    try (InputStream in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) &gt;= 0)
            out.write(buf, 0, n);
    }
}</code></pre>
<ul>
<li><strong>중첩 try-finally</strong>가 없어서 코드가 훨씬 짧고 읽기 쉽게 되었다.</li>
</ul>
<h4 id="예외가-2개-발생해도-첫-번째-예외가-사라지지-않는다">예외가 2개 발생해도 첫 번째 예외가 사라지지 않는다</h4>
<ul>
<li><code>try-with-resources</code>는 <strong>첫 번째 예외</strong>를 보존하고 두 번째 예외를 <strong>suppressed</strong> 상태로 보관한다.</li>
<li>디버깅할 때 <strong>모든 예외 정보를 볼 수</strong> 있게 되어서 문제 원인 파악이 쉬워졌다.</li>
<li><code>Throwable.getSuppressed()</code> 메서드로 프로그램에서 얻게 할 수도 있게 되었다.</li>
</ul>
<h5 id="모든-예외-정보-볼-수-있는-예시">모든 예외 정보 볼 수 있는 예시</h5>
<pre><code class="language-java">package item9;

import java.io.IOException;

class Resource2 implements AutoCloseable {
    private final String name;

    Resource2(String name) {
        this.name = name;
    }

    @Override
    public void close() throws IOException {
        System.out.println(name + &quot; closing...&quot;);
        throw new IOException(&quot;close()에서 발생한 예외: &quot; + name);
    }

    void doSomething() throws Exception {
        throw new Exception(&quot;doSomething() 예외: &quot; + name);
    }
}

public class SuppressedExceptionDemo {
    public static void main(String[] args) {
        try {
            testSuppressed();
        } catch (Exception e) {
            // 여기서 예외를 잡아서 확인
            System.out.println(&quot;메인에서 잡은 예외: &quot; + e);
            // 숨겨진(suppressed) 예외들도 확인
            for (Throwable t : e.getSuppressed()) {
                System.out.println(&quot;숨겨진(suppressed) 예외: &quot; + t);
            }
        }
    }

    static void testSuppressed() throws Exception {
        try (Resource2 r = new Resource2(&quot;MyResource&quot;)) {
            r.doSomething(); // 첫 번째 예외 발생
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/daram_dev/post/5918b498-f674-417b-869e-2db958272116/image.png" alt="">
<strong>실행 흐름</strong></p>
<ol>
<li>r.doSomething()에서 첫 번째 예외가 발생한다(예: Exception: doSomething() 예외: MyResource).</li>
<li>블록이 끝나면서 r.close() 호출 → 두 번째 예외(IOException: close()에서 발생한 예외: MyResource)가 발생한다.</li>
<li>try-with-resources는 첫 번째 예외를 주 예외로 삼고 두 번째 예외를 suppressed로 붙여서 던진다.</li>
<li>main에서 잡은 뒤 getSuppressed()를 보면 숨겨진 예외로 두 번째 예외 정보를 볼 수 있다.</li>
</ol>
<h3 id="2-3-catch-절-함께-사용-가능">2-3. <code>catch</code> 절 함께 사용 가능</h3>
<pre><code class="language-java">static String firstLineOfFile(String path, String defaultVal) {
    try (BufferedReader br = new BufferedReader(
                    new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;
    }
}</code></pre>
<ul>
<li>예외가 터져도 <code>catch</code> 블럭에서 처리가 가능하다. <strong>try 블록</strong>을 중첩하지 않아도 여러 예외 처리를 깔끔하게 할 수 있다.</li>
</ul>
<h2 id="3-결론">3. 결론</h2>
<ol>
<li><strong>try-finally</strong>는 자원을 닫을 수 있지만, 코드가 복잡해지고 예외가 여러 번 발생하면 디버깅이 어렵다.</li>
<li><strong>try-with-resources</strong>를 사용하면 <strong>코드가 깔끔</strong>해지고, <strong>다중 예외</strong>도 간단하게 처리할 수 있다.
=&gt; 자원을 닫을 땐 <strong><code>try-with-resources</code></strong>를 사용하자.</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>