<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tech_bae.log</title>
        <link>https://velog.io/</link>
        <description>전 아무고토 몰루고 아무고토 못해여</description>
        <lastBuildDate>Tue, 14 Oct 2025 17:45:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>tech_bae.log</title>
            <url>https://velog.velcdn.com/images/tech_bae/profile/16cb20bd-d440-4209-aa4a-67b62cf5b902/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. tech_bae.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tech_bae" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[DevChat: 우당탕탕 인증 구현기(Race Condition)]]></title>
            <link>https://velog.io/@tech_bae/DevChat-%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%EA%B8%B0Race-Condition</link>
            <guid>https://velog.io/@tech_bae/DevChat-%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%EA%B8%B0Race-Condition</guid>
            <pubDate>Tue, 14 Oct 2025 17:45:42 GMT</pubDate>
            <description><![CDATA[<h1 id="jwt인증">JWT인증</h1>
<h3 id="access-token과-refresh-token을-어디에-저장해야-하는가">Access Token과 Refresh Token을 어디에 저장해야 하는가</h3>
<p>JWT를 구현하면서 처음 고민했던 부분은 Access Token과 Refresh Token을 어디에 저장할 것 인가에 대한 것이었다. 로컬스토리지와 세션 스토리지는 자바스크립트로 토큰 값을 조작할 수 있어 XSS공격에 취약하기 때문에 쿠키를 사용하기로 했다. 물론 쿠키도 CSRF 공격에 활용될 수 있지만 SameSite나 Secure 정책으로 충분히 보완할 수 있다고 판단했다.</p>
<p>결과적으로 두 토큰을 서버(Redis) 에 저장하고, 클라이언트에는 Access Token만 쿠키로 전달하는 방식을 선택했다. 재발급 흐름은 다음과 같다. 만료된 Access Token을 사용해 Redis에서 Refresh Token을 조회하고, Refresh Token이 유효하면 새로운 Access Token을 발급해 Redis에 저장한 뒤 클라이언트에 다시 전달한다. Access Token을 Redis에 저장하는 이유는, 만료된 토큰을 Redis의 키로 관리하여 이미 사용된(혹은 재사용된) 토큰을 즉시 식별·차단함으로써 타인이 해당 토큰을 악용하지 못하도록 하기 위함이다.</p>
<p>토큰의 완전한 무상태(Stateless)를 포기하고, 토큰과 세션의 장점을 결합하려 했다.</p>
<p>이같은 구조를 채택했을 때의 장단점은 다음과 같다.</p>
<ol>
<li><strong>쿠키 탈취 우려</strong><ul>
<li>Refresh Token가 쿠키로 전달되면 이를 탈취하여 토큰 재발급을 시도할 수 있다. HttpOnly 옵션으로 토큰 유출 위험을 낮출 수 있지만, 허점이 있을 수 있으므로 서버에 저장하는 것이 더 안전하다고 판단했다. 특히, Refresh Token은 유효기간을 길게 설정하므로 탈취 성공 시 피해가 더욱 크다.</li>
</ul>
</li>
<li><strong>서버 측 즉시 제어 가능</strong><ul>
<li>Refresh Token을 서버에 보관하기 때문에 이를 삭제하여 해당 세션을 무효화 할 수 있다.</li>
</ul>
</li>
<li><strong>만료된 쿠키 재사용 방지</strong><ul>
<li>Access Token을 Redis 키로 관리하면, 이미 만료되어 재발급 흐름에 사용된 토큰이나 재사용 시도를 즉시 탐지·차단할 수 있다.</li>
</ul>
</li>
<li><strong>서버부하(트레이드오프)</strong><ul>
<li>완전한 무상태가 아니므로, Access Token의 재발급시 서버에 일정 부하가 발생한다. 하지만 Access Token이 유효할 때는 단순 검증만 수행하므로 서버부담이 크지 않다.</li>
</ul>
</li>
<li><strong>구현 복잡성 증가(트레이드오프)</strong><ul>
<li>세션 또는 완전 stateless JWT보다 구현, 운영 복잡성이 높다(쿠키 정책, 재발급 흐름 등). 그럼에도 보안과 운영 제어의 이점이 이를 감수할 가치가 있다고 판단했다.</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/1e1da9d1-6a4a-427b-9503-c75c5bb68473/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/9e1baf1f-6fae-439a-b64e-d7f9f39eb6c0/image.png" alt=""></p>
<hr>
<h3 id="jwt-필터에서-재발급">JWT 필터에서 재발급</h3>
<p>(<a href="https://github.com/prgrms-be-devcourse/NBE5-7-2-Team08/blob/b86080e9334d2b2fd2377bf587cd34196e44aac6/backend/src/main/java/project/backend/global/config/security/handler/JwtAuthenticationFilter.java">JWT 인증필터</a>, <a href="https://github.com/prgrms-be-devcourse/NBE5-7-2-Team08/blob/b86080e9334d2b2fd2377bf587cd34196e44aac6/backend/src/main/java/project/backend/global/config/security/app/JwtProvider.java">Jwt Provider</a>)</p>
<p>초기 구현에서는 재발급 로직을 JWT 인증 필터에 위임했다. 필터가 토큰의 유효성 검사와 함께 Access Token이 만료되면 즉시 재발급하도록 했다. Access Token 만료 자체가 토큰 손상이나 위변조를 의미하는 것은 아니므로, “바로 재발급해 응답하면 별도의 재발급 API는 불필요하다”는 판단이었다. </p>
<p>그러나 프론트엔드와 연동해 동시다발적으로 API를 호출해 보니 문제가 드러났다. 어떤 요청은 서버에서 재발급까지 성공했는데도, 다른 동시 요청들이 인증 실패가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/02da2049-2137-412e-99ba-d5b6e85be4a4/image.png" alt=""></p>
<p>문제의 근본 원인은 경쟁 상태(Race Condition) 였다. 프론트엔드에서는 한 페이지에서 다수의 AJAX 요청이 동시에 발생한다. 이 비동기적/동시다발적 요청 중 하나가 재발급 로직을 통해 새로운 Access Token을 발급받는 동안, 나머지 요청들은 여전히 갱신되기 전의 만료된 토큰을 담아 서버로 전송된다. 이로 인해, 서버는 일부 요청에 대해서는 재발급이 성공했음에도 불구하고 다른 요청들은 유효하지 않은 토큰으로 인증을 시도하게되어 인증에 실패하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/740154f5-39ec-433e-8fc6-a0aaebd4d4c2/image.png" alt=""></p>
<hr>
<h3 id="요청-인터셉터와-토큰-유효성-확인-api">요청 인터셉터와 토큰 유효성 확인 API</h3>
<p>결국 깨달은 건, 재발급을 서버가 알아서 해주길 바라면 Race Condition은 피할 수 없다는 거였다. 그래서 Axois 요청 인터셉터를 사용하여 메인 요청을 보내기 전에 클라이언트가 /auth로 토큰의 유효성을 확인 후 메인 요청을 수행하도록 리펙토링을 진행했다.</p>
<p>지금 생각해보면 정말 멍청한 생각이다. 문제를 제대로 이해하지 못한 상태에서 성급하게 해결책을 도입한 전형적인 임시 방편에 불과했다. 겉으로는 문제를 줄이는 것처럼 보였지만, 오히려 구조는 꼬이고 보안성은 떨어졌다.</p>
<ol>
<li>비효율성과 서버과부화<ul>
<li>요청 인터셉터는 모든 요청마다 /auth를 선행 호출한다. 이로 인해 토큰이 유효함에도 불필요하게 /auth가 호출되어 네트워크 비용이 증가하고, 서버에 과도한 부하를 유발하는 비효율적인 구조가 되었다.</li>
</ul>
</li>
<li>재발급로직의 중복<ul>
<li>/auth와 요청인터셉터를 도입했음에도 Jwt 인증필터의 재발급로직은 삭제하지 않았기에 재발급로직이 중복되어 쿠키 갱신 타이밍과 Redis 갱신이 요청 단위로 뒤섞이면서 복잡성이 증가했다.</li>
</ul>
</li>
<li>요청 인터셉터간 경쟁 상태<ul>
<li>단순 요청 인터셉터의 도입은 전의 문제와 근본적으로 같은 문제를 반복했다. 단지 그냥 요청의 재발급 경쟁상태가 /auth에서 일어난 것 뿐이다. 덕분에 표면적인 인증실패를 줄이기 위해 /auth의 재발급 로직을 엑세스토큰-Redis 바인딩 체크을 하지 않고, Redis에서 토큰의 payload에 있는 유저의 id으로 Refresh Token을 조회 후 재발급하는 끔찍한 구조를 만들고 말았다.</li>
</ul>
</li>
</ol>
<p>결과적으로 이 리팩토링은 구조의 복잡성은 올라가고, 보안성은 후퇴한 실패한 리팩토링이었다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/1479b6c8-1902-4af0-8eab-c8cfded4de28/image.png" alt=""></p>
<hr>
<h3 id="응답-인터셉터와-재발급-단일화-구조"><strong>응답 인터셉터와 재발급 단일화 구조</strong></h3>
<p>지금까지의 JWT 인증필터는 너무 많은 책임이 부여되어 토큰의 검증과 만료된 토큰을 재발급까지 수행했다. 모든 요청은 JWT 인증필터를 거치기 때문에 이곳에서 재발급이 이루어 진다면 경쟁상태(Race Condition)이 발생할 가능성이 너무 높았다. 또한 서로 다른 책임을 하나의 필터가 담당하므로 단일 책임 원칙에 반하며 이는 코드 복잡도 증가와 유지보수성이 저하되게 했다. 애초에 인증필터는 요청을 가로채 인증을 확인하는 역할인데 왜 이렇게 만들었는지 모르겠다.</p>
<p>그래서 JWT 인증필터에서는 토큰의 인증만 확인하도록 리펙토링했다. 이제 만료가 되었든 이상한 토큰이든 Valid한 토큰이 아니면 401을 던지도록 설계했다. 재발급은 별도의 API(/token/refresh)를 구현하여 재발급을 수행하도록 했다. 인증과 재발급 로직을 분리함으로써 각 컴포넌트가 단일 책임을 가지게 되어 클라이언트가 토큰 상태를 명확히 제어할 수 있게 되었다.</p>
<p>(<a href="https://github.com/prgrms-be-devcourse/NBE5-7-2-Team08/blob/19251cdbb8a99899d7050858386974a304cdd3ad/backend/src/main/java/project/backend/global/security/handler/JwtAuthenticationFilter.java">JWT인증필터</a>, <a href="https://github.com/prgrms-be-devcourse/NBE5-7-2-Team08/blob/19251cdbb8a99899d7050858386974a304cdd3ad/backend/src/main/java/project/backend/global/security/jwt/JwtProvider.java">JWT Provider</a>, <a href="https://github.com/prgrms-be-devcourse/NBE5-7-2-Team08/blob/19251cdbb8a99899d7050858386974a304cdd3ad/backend/src/main/java/project/backend/global/security/api/AuthController.java">재발급 API</a>)</p>
<p>서버의 설계는 끝났다. 그렇다면 이제 클라이언트 쪽에서는 토큰을 어떻게 관리해야 할까? 이전 시도에서처럼 요청 인터셉터를 사용해 /auth를 매번 호출하는 방식은 결국 서버 부하만 늘리고 근본적인 문제를 해결하지 못했다. 결국 내가 내린 결론은 “서버는 알아서 재발급을 처리할 수 없고 재발급의 주도권은 반드시 클라이언트가 가져야 한다.” 였다. 그렇다면 가장 자연스러운 방법은, 서버가 만료된 토큰에 대해 401을 던지면 클라이언트는 이 응답을 토대로 토큰이 만료되었다는 판단 후 재발급을 요청하는 것 이다.</p>
<p>이때 사용할 수 있는 것이 <strong>Axios의 응답 인터셉터(Response Interceptor)</strong> 이다. 응답 인터셉터는 요청 인터셉터와 비슷하게 서버의 응답을 가로채 공통 로직을 실행할 수 있는 훅이다. 이를 통해 “401이 발생하면 /token/refresh로 재발급을 요청하고, 재발급이 완료되면 원래의 요청을 다시 시도한다”라는 흐름을 구현할 수 있다. 하지만 여기까지만 구현하면 경쟁 상태가 다시 발생할 수 밖에 없다. 전의 경우와 동일하게 여러 API를 동시에 호출하고, 그 모든 요청의 토큰이 만료된 상태라면, 각 요청의 인터셉터가 거의 동시에 /token/refresh를 호출하게 된다. 경쟁상태가 다른 형태로 반복되는 것 이다.</p>
<p>이를 해결하기 위해 <strong>Axios 인터셉터 기반의 단일 재발급(Single-Flight) 패턴</strong>을 도입했다.</p>
<blockquote>
<p>Single-Flight란 같은 키에 대한 중복 작업을 한 번만 실행하고, 나머지 경쟁자들은 그 결과를 공유하도록 하는 중복 억제(deduplication) 기법이다. Go에서 널리 쓰이는 듯 하다.</p>
</blockquote>
<p>이를 응답 인터셉터와 조합하면 다음과 같은 흐름이 가능하다.</p>
<ol>
<li>최초의 401 응답을 받은 요청만이 /token/refresh API를 호출한다.</li>
<li>그 외의 모든 요청은 재발급이 완료될 때까지 대기열에 등록된다.</li>
<li>재발급이 완료되면 대기 중이던 모든 요청이 순차적으로 다시 실행된다.</li>
</ol>
<p>즉, 한 번의 재발급 요청만 수행하고, 그 사이 들어온 다른 요청들은 잠시 큐에 대기시킨 뒤, 재발급이 완료되면 새 토큰으로 모두 다시 시도하도록 설계했다.</p>
<p>좀 더 자세한 로직은 다음과 같다.</p>
<ol>
<li>401 응답을 받은 요청이 _retry 플래그로 중복 재시도를 방지한다.</li>
<li>현재 재발급 중(isRefreshing = true)이라면, 해당 요청을 refreshSubscribers 큐에 등록한다.</li>
<li>재발급이 완료되면 onRefreshed()가 호출되어 대기 중이던 요청들이 한꺼번에 다시 실행된다.</li>
<li>재발급이 실패하면 모든 대기 요청은 취소되고, 사용자는 로그인 페이지로 이동된다.</li>
</ol>
<pre><code class="language-jsx">let isRefreshing = false;
let refreshSubscribers = [];

const onRefreshed = () =&gt; {
  refreshSubscribers.forEach(callback =&gt; callback());
  refreshSubscribers = [];
};

const addRefreshSubscriber = (callback) =&gt; {
  refreshSubscribers.push(callback);
};</code></pre>
<p>결과적으로 재발급은 오직 한 번만 수행되고, 모든 요청이 새 토큰으로 정상 재시도되는 안정적인 구조가 완성되었다. 이제 서버는 토큰 검증에만 집중하고, 클라이언트가 토큰 라이프사이클을 완전히 제어할 수 있게 되었다. 결과적으로 JWT 인증 구조 전반이 훨씬 단순하고 안정적으로 작동하게 되었다.</p>
<p>(<a href="https://github.com/prgrms-be-devcourse/NBE5-7-2-Team08/blob/19251cdbb8a99899d7050858386974a304cdd3ad/frontend/src/components/api/axiosInstance.jsx">응답 인터셉터 전체 코드</a>)</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/02894046-4670-4ea0-bcab-4a5869477889/image.png" alt=""></p>
<hr>
<h2 id="이런-웹소켓-핸드쉐이크가-인터셉터를-안타잖아">이런 웹소켓 핸드쉐이크가 인터셉터를 안타잖아?</h2>
<p>WebSocket 연결은 new WebSocket(url)로 시작되는 HTTP 업그레이드(handshake) 요청이고, 이 경로는 Axios가 만드는 HTTP 요청이 아니다. 당연히 Axios 인터셉터는 Axios가 만든 요청/응답에만 개입할 수 있으니, 웹소켓 핸드셰이크에는 애초에 끼어들 수 없다. 그래서 분명 재발급이 이루어 졌음에도 웹소켓 연결이 어떤 이유로 연결이 끊겨 재연결을 시도하면 인증문제로 실패했다.</p>
<p>결국 웹소켓 핸드쉐이크와 Axios요청 모두 하나의 재발급 요청 후 이를 공유해야한다. 이를위해 <code>refreshManager</code>라는 중간 매개체를 만들었다. <code>refreshManager</code>는 <code>safeRefreshToken()</code> 함수를 가지는데 이 함수는 실제 재발급 요청을 날리는 함수다.</p>
<pre><code class="language-jsx">// refreshManager.js
import axios from &#39;axios&#39;;

let isRefreshing = false;
let refreshPromise = null;

export const safeRefreshToken = async () =&gt; {
  if (isRefreshing) return refreshPromise;

  isRefreshing = true;
  refreshPromise = axios.get(`${process.env.REACT_APP_API_URL}/token/refresh`, {
    withCredentials: true,
  })
    .catch((err) =&gt; {
      throw err;
    })
    .finally(() =&gt; {
      isRefreshing = false;
    });

  return refreshPromise;
}; </code></pre>
<p><code>axiosInstance</code>는 내부에서 <code>safeRefreshToken()</code>를 호출하여 재발급을 처리한다.</p>
<pre><code class="language-jsx">// axiosInstance.js
import axios from &#39;axios&#39;;
import { safeRefreshToken } from &#39;./refreshManager&#39;; //

const instance = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  withCredentials: true,
});

let refreshSubscribers = [];

const onRefreshed = () =&gt; {
  refreshSubscribers.forEach(callback =&gt; callback());
  refreshSubscribers = [];
};

const addRefreshSubscriber = (callback) =&gt; {
  refreshSubscribers.push(callback);
};

// 응답 인터셉터
instance.interceptors.response.use(
  (response) =&gt; response,
  async (error) =&gt; {
    const { config, response } = error;
    const originalRequest = config;

    if (response?.status === 401 &amp;&amp; !originalRequest._retry) {
      originalRequest._retry = true;

      return new Promise((resolve, reject) =&gt; {
        addRefreshSubscriber(() =&gt; {
          resolve(instance(originalRequest)); // 재요청 실행
        });

        // 단 한 번만 refresh 실행
        safeRefreshToken()
          .then(() =&gt; {
            onRefreshed(); // 대기 중인 요청들 처리
          })
          .catch((err) =&gt; {
            window.location.replace(&#39;/login&#39;);
            reject(err);
          });
      });
    }

    return Promise.reject(error);
  }
);

export default instance;</code></pre>
<p>그리고 웹소켓은 연결이 끊어졌을 때(onWebSocketClose) 즉시 재연결하지 않고, 먼저 <code>await safeRefreshToken()</code>으로 최신 쿠키/토큰 확보 후 재연결한다. 이렇게 하면 한번의 재발급 요청으로 Axios와 웹소켓이 같은 Promise(같은 결과)를 공유하게 되어 갱신된 토큰으로 모든 요청이 처리될 수 있다.</p>
<pre><code>🚶 axios 요청1  ┐
🚶 axios 요청2  ┼─────┐
🧍 websocket    ┘     │
                    ⏳ [safeRefreshToken 호출]
                      ↓
                🔄 /token/refresh
                      ↓
         ☑️ 성공  → 대기하던 요청들 다 실행
         ❌ 실패  → 로그인 페이지로 이동</code></pre><hr>
<h2 id="마무리">마무리</h2>
<p>Race Condition은 이 프로젝트를 진행하면서 나를 계속 괴롭게 했던 문제였다. JWT토큰은 물론이고 인증구현이 처음이라 안그래도 하나하나 알아가며 구현했어야 했는데, 동시성 문제까지 닥치니 매우 힘들었던 기억이 생생하다. 심지어 프론트 쪽 코드도 처음 작성해봤기에 배로 힘들었다. 그러나 이러한 경험이 없었다면 오랜 기간 Race Condition을 알거나 마주치기 어려웠을 것 이다. Axios 인터셉터라는 백엔드 개발자에게는 조금은 생소한 개념도 알게되었다. 결과적으로 인증관련 흐름을 깨지고 부서지며 체득할 수 있었던 경험이다. 또한, 설계의 중요성도 깨닫게 된것 같다. 무작정 구현만 일단 달려들면 더 큰 시련에 부딪힐 수 있다는 것을 몸으로 직접 깨달았다. 뭔가를 빨리 구현하고싶은 마음은 여전하지만 이때에 비해 설계에 훨씬 많은 시간을 쏟고 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[영속성] JDBC, MyBatis는 구닥다리 잖아요! JPA (이론편)]]></title>
            <link>https://velog.io/@tech_bae/%EC%98%81%EC%86%8D%EC%84%B1-JDBC-MyBatis%EB%8A%94-%EA%B5%AC%EB%8B%A5%EB%8B%A4%EB%A6%AC-%EC%9E%96%EC%95%84%EC%9A%94-JPA-%EC%9D%B4%EB%A1%A0%ED%8E%B8</link>
            <guid>https://velog.io/@tech_bae/%EC%98%81%EC%86%8D%EC%84%B1-JDBC-MyBatis%EB%8A%94-%EA%B5%AC%EB%8B%A5%EB%8B%A4%EB%A6%AC-%EC%9E%96%EC%95%84%EC%9A%94-JPA-%EC%9D%B4%EB%A1%A0%ED%8E%B8</guid>
            <pubDate>Wed, 13 Aug 2025 17:11:48 GMT</pubDate>
            <description><![CDATA[<p>JDBC, MyBatis를 전에 정리했지만, 실제 프로젝트에서 사용한건 JPA밖에 없다. 저 둘에 비하면 딸깍충 그자체인 JPA에 대해서 알아보자. 이 글은 JPA의 이론을 간략하게 정리한 것 이다.</p>
<h1 id="orm-object-relational-mapping">ORM (Object Relational Mapping)</h1>
<p><strong>Java와 DB는 서로 데이터를 관리하는 방식이 다르다.</strong> </p>
<p>Java는 객체의 형태로 데이터를 관리하지만 RDB는 이를 표 형식으로 저장하고 연관관계를 외래키를 이용하여 형성한다. </p>
<p>이런 두 패러다임의 괴리를 객체-관계 불일치라고 하고, 이 때문에 DB의 데이터를 객체로 변환하는 과정에서 적절한 매핑이 필요하다.</p>
<p><strong>ORM은 바로 이러한 괴리를 해결하기위해 데이터베이스와 객체의 매핑을 수행하는 방법을 제공</strong>한다.</p>
<p>추가로 ORM은 이러한 매핑을 통해 SQL문을 직접 생성하고 수행까지 한다.</p>
<p>물론 단점(trade-off)도 존재하는데 SQL 쿼리가 개발자을 안거치고 자동으로 만들어져 수행되기에 의도와 다른 쿼리가 발생할 수도 있고, 성능이 떨어질 수도 있다.</p>
<p>그렇기에 상황에 따라 ORM에 쿼리를 맡길지 직접 작성할지 판단이 필요하다.</p>
<hr>
<h1 id="jpa-java-persistensce-api">JPA (Java Persistensce API)</h1>
<p><strong>JPA</strong>는 자바 진영의 <strong>ORM 표준 명세</strong>로, 애플리케이션과 데이터베이스 간 객체-관계 매핑을 일관된 방식으로 다룰 수 있도록 하는 인터페이스 집합이다.</p>
<p><strong>JPA 자체가 기능을 구현하는 것은 아니며</strong>, Hibernate, EclipseLink와 같은 구현체가 실제 동작을 담당한다.</p>
<p>JPA를 사용하면 다음과 같은 장점을 얻을 수 있다.
    •    <strong>엔티티와 테이블 매핑</strong>: 자바 클래스와 DB 테이블 간 매핑을 애노테이션으로 정의
    •    <strong>DDL 자동 생성</strong>: 매핑 정보를 기반으로 테이블 스키마 자동 생성
    •    <strong>JPQL 지원</strong>: 객체 지향 쿼리를 작성하면 구현체가 이를 SQL로 변환하여 실행
    •    <strong>DB 독립성 향상</strong>: 여러 DB 벤더 간 쿼리 차이를 최소화</p>
<p>단, 자동 쿼리 생성과 매핑 편의성 뒤에는 성능 문제가 숨어 있을 수 있다.
따라서 연관관계 설계와 쿼리 튜닝은 여전히 중요하며, 상황에 따라 직접 SQL/네이티브 쿼리를 사용하는 판단도 필요하다.</p>
<hr>
<h1 id="hibernate">Hibernate</h1>
<p><strong>Hibernate는 JPA를 구현한 구현체 프레임워크</strong> 중 하나이다. </p>
<blockquote>
<p>Spring Data JPA는 JPA 위의 추상화입니다. 
기본적으로 Hibernate를 많이 쓰지만 필수는 아님(EclipseLink 등 다른 구현체도 가능)</p>
</blockquote>
<p>이를 한층 더 추상화하여 개발자가 Repository만 정의해도 쿼리 없이 DB 연동이 가능하게 해준다.</p>
<p>Hibernate는 JDBC나 MyBatis에 비해 설정해야 할 것들이 적고, <strong>상당부분이 자동화</strong>되어 있다. </p>
<p>또한 <strong>변경감지, 지연로딩, 캐시, 트랜잭션 관리</strong> 등의 기능을 통해 <strong>강력한 영속성 관리 기능</strong>을 제공한다.</p>
<p>순수 Hibernate환경에서는 <code>/resources/META-INF/persistence.xml</code>을 통해 <strong>데이터베이스 연결 정보</strong>, <strong>사용할 엔티티 클래스</strong>, <strong>JPA 구현체(Hibernate)</strong> 등을 설정해야 한다. 
이 파일은 <strong>JPA의 표준 설정 파일</strong>로, Hibernate가 아닌 다른 JPA 구현체에도 공통적으로 사용된다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/ec1538bf-602d-490f-807b-c20eef0c86ff/image.png" alt=""></p>
<p>하지만 스프링부트에선 application.yml로 보다 간편한 설정이 가능하다. 또한 스프링부트는 @Entity가 붙은 클래스를 스캔해서 자동으로 등록하기에 다른 설정이 필요하지 않다. <del>persistence.xml로 설정해본적 한번도 없다.스프링부트 최고!</del> </p>
<pre><code class="language-yaml">spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth
    username: dev_bae
    password:

  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true</code></pre>
<h2 id="entitymanagerfactory">EntityManagerFactory</h2>
<p>밑에서 설명할 EntityManager을 생성하는 팩토리 객체이다.</p>
<p>EntityManagerFactory는 EntityManager뿐만 아니라 설정정보를 토대로 <strong>DB연결 정보, 커넥션 풀, 매핑된 엔터티 클래스들, 캐시전략, 영속성 컨텍스트</strong>등을 생성하기 때문에 <strong>생성비용이 크다</strong>. </p>
<p>따라서 <strong>어플리케이션 전체에서 한 번 생성하고 어플리케이션이 종료되면 close()를 호출해 닫는다.</strong></p>
<h2 id="entitymanager">EntityManager</h2>
<p>EntityManager 말 그대로 <strong>Entity를 관리하는 객체</strong>이다. </p>
<p>즉, 자바객체와 DB의 테이블을 매핑하고 <code>INSERT</code>, <code>SELECT</code>, <code>UPDATE</code>, <code>DELETE</code>을 통한 <strong>CRUD작업을 객체 중심으로 쉽게 수행</strong>할 수 있게 한다.</p>
<p>EntityManager는 <strong>영속성 컨텍스트(Persistence Context)를 이용해 엔터티 객체의 생명주기를 관리</strong>한다. 영속성 컨텍스트는 DB에서 가져오거나 저장하는 엔터티 객체를 1차 캐시에 보관하고, 해당 객체의 상태를 추적하여 변경 사항이 감지되면 자동으로 SQL을 생성한다.</p>
<p>또한 트랜잭션의 관리도 맡고있다. 트랜잭션의 시작, 커밋, 롤백을 제어하는 역할도 한다. 보통 Spring에서는 <code>@Transactional</code>을 사용하여 선언적 트랜잭션을 처리한다.</p>
<p><del>EntityManager는 쓰레드당 하나가 생성되며, EntityManager는 트랜잭션 단위로 Entity를 관리한다.</del></p>
<blockquote>
<p><strong>😓 추가 학습에 따른 수정</strong>
<strong>EntityManager는 스레드 세이프하지 않다</strong>고 합니다. 그러므로 여러 스레드에서 공유해서는 안 됩니다.
EntityManager는 보통 요청(Request) 또는 트랜잭션 단위로 생성·관리되며, 트랜잭션이 시작되면 해당 범위 내에서 엔티티를 관리합니다.
스프링에서는 이미 생성된 EntityManager 프록시가 주입되고, 트랜잭션이 시작되면 실제 DB 연결이 바인딩됩니다.</p>
</blockquote>
<h2 id="영속성-컨텍스트-persistence-context">영속성 컨텍스트 (Persistence Context)</h2>
<p>EntityManager는 영속성컨텍스트를 이용하여 Entity를 관리한다고 했다. 그럼 영속성컨텍스트는 어떻게 이를 관리하고 어떠한 동작들을 하는가?</p>
<p>영속성컨텍스트는 Entity의 상태를 추적하고, 해당 Entity가 실제 DB와 상호작용하는 방법과 시점을 결정하는 역할을 가지고 있다.</p>
<p>영속성 컨텍스트는 여러 역할을 맡고있다.</p>
<ol>
<li><p><strong>Entity 상태 관리(상태 추적)</strong></p>
<p> Entity의 생명주기(Transient → Managed → Detached → Removed)를 관리</p>
<p> 영속화(<code>persist()</code>)하면 Managed(영속상태)로 진입하여, <strong>영속성 컨텍스트가 이를 추적</strong>한다.</p>
<p> ⇒ 이로인해 <strong>변경감지가(Dirty Checking)</strong>이 가능.</p>
<ul>
<li><p><strong>Entity의 생명주기 상태</strong></p>
<ul>
<li><p><strong>비영속(<code>new</code> / transient)</strong></p>
<p>  영속성 컨텍스트가 관리하지 않는 상태이다. 그러므로 변경감지가 이루어지지 않고 DB에 저장되지도 않는다. 이를 영속성 컨텍스트가 관리하게 하기 위해선 merge나 persist되어야 한다.</p>
</li>
<li><p><strong>영속(<code>persist()</code>/managed)</strong></p>
<p>  영속성 컨텍스트에 의해 관리되고 있는 상태이다. Entity의 변경사항을 추적하고, 트랜잭션이 커밋되면 변경사항이 DB에 반영된다.</p>
</li>
<li><p><strong>준영속(<code>detach()</code> / detached)</strong></p>
<p>  더 이상 영속성 컨텍스트의 관리를 받지 않는 상태이다. (영속상태였다가 분리 된 상태) 비영속 상태와 동일하게 변경감지가 되지 않고 커밋하여도 DB에 반영되지 않는다. <code>merge()</code>를 통해 영속상태로 변경이 가능하다.</p>
</li>
<li><p><strong>삭제(<code>remove()</code> / removed)</strong></p>
<p>  영속성 컨텍스트 뿐만 아니라 DB에서도 삭제를 하기 위한 상태이다. 커밋 시 DB에서 삭제된다.</p>
<p>  <img src="https://velog.velcdn.com/images/tech_bae/post/6ac0fe47-d8e1-486d-a403-23224a7c77f1/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<ol start="2">
<li><p><strong>변경감지(Dirty Checking)</strong></p>
<p> 영속성 컨텍스트에서 관리되고 있는 <strong>Entity객체의 상태변화를 자동으로 감지하여 이를 DB에 반영</strong>한다.</p>
<p> 이를 위해 <strong>스냅샷(Snapshot)</strong>이 사용되는데, 이는 <strong>Entity객체의 초기 상태를 저장</strong>하고 있다. 이후, Entity 객체체의 상태가 변경되면 가장 최신의 상태와 스냅샷을 비교하여 변경을 파악한다. 이 변경을 커밋시에 DB에 반영된다.</p>
</li>
</ol>
<ol>
<li><p><strong>1차캐시</strong></p>
<p> <strong>영속성 컨텍스트에 저장된 Entity는 DB에 조회하지 않고 바로 꺼내 반환</strong>한다.</p>
<p> 동일한 Entity가 동일한 영속성 컨텍스트에서 여러번 조회된다면, 하나하나 DB에 접근하여 반환받아오는 것이 아니고 이를 캐시된 Entity를 반환한다. ⇒ 성능 최적화</p>
<p> 영속성 컨텍스트가 종료되면 해당 캐시는 소멸함.</p>
</li>
<li><p><strong>지연로딩(Lazy Loading)를 통한 연관관계 관리</strong></p>
<p> 지연로딩이란 객체의 전체 데이터를 즉시 불러오지 않고, <strong>실제로 필요한 시점이 되서야 느긋하게 로딩</strong>을 하는 전략이다.</p>
<p> 지연로딩을 위해 Hibernate는 <strong>프록시객체</strong>를 사용한다.</p>
<p> 실제 Entity객체 대신 프록시객체를 반환하는데, 이 프록시 객체는 해당 Entity객체의 메타데이터와 식별자만 보유하고 있고 실제 이 객체에 접근해야할때 실제 DB에서 해당 데이터를 로드한다.  </p>
</li>
</ol>
<blockquote>
</blockquote>
<p>예를 들어, Member 객체가 Profile를 참조하고 있다. 
     이때 <code>Member</code> 객체를 조회하면 <code>Profile</code>의 실제객체가 아니라 프록시객체가 Member필드에 할당된다. <code>Profile</code>의 데이터를 실제 가져와야 할 때 해당 쿼리가 실행되어 Profile 데이터가 로딩된다.</p>
<ol start="3">
<li><p><strong>쓰기지연(Write-behind)</strong></p>
<p> 쓰기지연을 이해하기위해 선행되어야할 개념인 Action Queue라는게 있다.</p>
<p> Action Queue는 Entity를 관리할 쿼리들을 모아놓은 큐이다.</p>
<p> 즉, Hibernate는 Action Queue를 통해 쿼리를 생성할때마다 DB에 질의하지 않고 모아놨다가 한번에 반영시킨다. </p>
</li>
</ol>
<blockquote>
<p>   그러면 안되겠지만 변기에 쓰레기를 버리는 못된 사람이 있다고 치자 이사람이 쓰레기를 하나 버릴때마다 물을 내리면 비효율적이지 않겠나? 이를 쓰레기를 변기에 모아놨다가 한번에 물을 내리는(<strong>flush</strong>)것 이다. <br>
물을 내리는 행위 즉, 모아놨던 쿼리를 DB에 한번에 반영하는 방법은 <code>flush()</code>와 <strong>트랜잭션 커밋</strong>, <strong>JPQL쿼리 발생</strong>이 있다.</p>
</blockquote>
<ol>
<li><p><strong>동일성(identity) 보장</strong></p>
<p> 같은 PK를 가지는 Entity를 영속성 컨텍스트 내에서 같은 객체로 판단하여 관리한다. 이는 <code>==</code>비교도 동일성 확인이 가능하며, DB의 일관성을 보장해준다.</p>
</li>
</ol>
<hr>
<blockquote>
<aside>
  💡 <br> 개인적으로 표현하자면 영속성컨텍스트는 DB와의 효율적인 상호작용을 위해 어플리케이션과 DB의 중간다리를 통제하는 것이라고 이해하고 있다.
</blockquote>
</aside>

<hr>
<h1 id="매핑객체">매핑객체</h1>
<p>JPA가 사용할 Entity를 정의하여야 이를 DB에 매핑할 수 있다. </p>
<h2 id="entity">@Entity</h2>
<p>Entity는 DB의 테이블을 자바 클래스 형태로 표현한 것이다. 이 Entity클래스의 인스턴스는 DB의 행(Row)를 의미한다.</p>
<p><code>@Entity</code> 어노테이션으로 선언한다.</p>
<p><code>@Table</code>어노테이션을 통해 DB의 어떤 테이블과 매핑되어야 하는지를 지정한다. <code>@Table</code>을 사용하지 않으면 클래스의 이름으로 매칭될 테이블의 이름을 유추하여 매핑을 수행한다. </p>
<p>JPA는 내부적으로 리플렉션을 사용해 Entity객체를 만드므로 <strong>기본생성자가 필수</strong>로 필요하다.</p>
<pre><code class="language-sql">CREATE TABLE members(
    member_id bigint not null primary key auto_increment,
    name varchar(20) not null,
    email varchar(50) not null
);</code></pre>
<pre><code class="language-java">@Getter
@Entity
@Table(name = &quot;members&quot;)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Members {

   ...
}</code></pre>
<h2 id="id">@Id</h2>
<p>테이블의 PK와 매핑되는 필드를 <strong><code>@Id</code>어노테이션으로</strong> 선언할 수 있다.</p>
<p>@GeneratedValue를 통해 자동으로 생성되게 하거나, 수동으로 설정할 수 도 있다.</p>
<p>일반적으로 auto_increment와 동일한 IDENTITY전략을 사용한다. 물론 id 생성전략은 여러가지가 있다. 이에 대해 아래에서 알아보겠다<strong>.</strong></p>
<ul>
<li><p><strong>자동 전략 (GenerationType.AUTO)</strong>
  JPA 구현체가 현재 사용 중인 데이터베이스 방언(dialect)에 맞춰 적절한 기본키 생성 전략을 자동으로 선택한다.
  예를 들어, MySQL에서는 IDENTITY, Oracle에서는 SEQUENCE 전략을 선택하는 식이다.</p>
  <br></li>
<li><p><strong>시퀀스 전략 (GenerationType.SEQUENCE)</strong>
  시퀀스 객체를 사용하여 자동으로 id값을 생성한다. <code>@SequenceGenerator</code>를 통해 시퀀스의 이름과 시작 값등을 정의할 수도 있다. </p>
<pre><code class="language-java">  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = &quot;user_seq_gen&quot;)
  @SequenceGenerator(
      name = &quot;user_seq_gen&quot;,
      sequenceName = &quot;user_seq&quot;,   // DB 시퀀스 이름과 일치
      allocationSize = 1
  )
  private Long id;</code></pre>
<p>  이렇게 해놓으면 우리가 시퀀스를 직접 호출하지 않아도 JPA가 INSERT전에 자동으로 시퀀스를 호출하여 id값을 생성한다.</p>
<pre><code>&lt;br&gt;</code></pre></li>
<li><p><strong>식별자 전략 (GenerationType.IDENTITY)</strong>
  <strong>auto_increment와 같은 방식</strong>이라고 생각해면 된다. DB에서 auto_increment방식으로 생성된 PK값을 가져와 매핑한다.</p>
<pre><code>&lt;br&gt;</code></pre></li>
<li><p><strong>테이블 전략(GenerationType.TABLE)</strong>
  시퀀스 기능이 없는 DB에서 시퀀스전략과 비슷한 전략을 취할 수 있는 전략이다. <code>@TableGenerator</code>를 사용해 기본키 값을 생성하는 테이블의 설정을 정의할 수 있다.</p>
<pre><code>&lt;br&gt;</code></pre></li>
<li><p><strong>UUID 전략 (GenerationType.UUID)</strong>
  우주유일값을 기본키값으로 설정하는 전략이다! <strong>짱 멋지다.</strong></p>
</li>
</ul>
<h2 id="column">@Column</h2>
<p>Hibernate는 기본적으로 필드 이름을 기반으로 DB의 칼럼을 매핑한다.</p>
<p>하지만 <strong>서로의 이름이 다르거나 명시적으로 표현해야할 때 @Column의 name을 통해 이름을 명시적으로 매핑</strong> 시킬 수 있다.</p>
<p>또한 DB 속성의 제약조건을 표현해 줄 수 있다. (columnDefinition, nullable, unique …) </p>
<p>이를 바탕으로 AutoDDL시 좀 더 구체적으로 테이블을 생성할 수도 있다.</p>
<h3 id="enumerated">@Enumerated</h3>
<p>자바의 enum타입이랑 매핑할때 사용</p>
<ul>
<li>@Enumerated(EnumType.ORDINAL: enum클래스에 정의된 순서대로 숫자로 매핑</li>
<li>@Enumerated(EnumType.STRING : enum의 이름이 그래도 매핑</li>
</ul>
<blockquote>
<p>기본값은 ORDINAL이라 굉장히 킹받는 상황이 생길 수 있다. 웬만하면 @Enumerated(EnumType.STRING)을 사용하자.(순서 변경 시 데이터 깨짐 방지).</p>
</blockquote>
<hr>
<h1 id="연관관계">연관관계</h1>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/23171241-5f7f-4b9c-a50b-0d7ce89ad2b7/image.png" alt=""></p>
<p>위에서 정리한 내용을 토대로 팀과 선수 Entity를 만들었다고 생각해보자. 이는 이대로 괜찮을까?</p>
<p>그렇지 않다 왜냐하면 둘의 관계가 정의되어 있지 않다.</p>
<p>객체와 테이블의 불일치를 해소하는 것이 orm이거늘.. 하지만 객체의 세상엔 관계라는 것이 따로 존재하지 않는다. 그로인해 앞으로 알아볼 Entity의 연관관계에는 Table의 관계와는 차이가 있을 수 밖에 없다.</p>
<p>Entity의 연관관계 매핑을 위해서는 3가지의 개념을 생각해봐야 한다.</p>
<hr>
<h2 id="다중성">다중성</h2>
<p>다중성이란 테이블이 다른 대상과 얼마다 연결될 수 있는지를 정량적으로 표현한 것 이다.</p>
<p>DB를 기준으로 다중성을 결정한다.</p>
<h3 id="일대일">일대일</h3>
<p>말 그대로 하나의 객체가 다른 단 하나의 객체와만 연관될 때의 연관관계이다.</p>
<p>ex) 학생과 책상(학생은 각 하나의 책상을 사용한다.)</p>
<pre><code class="language-java">@Entity
public class Student{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long sudentId;

    @OneToOne
    private Desk desk;

}</code></pre>
<p><code>@OneToOne</code> 어노테시션으로 선언한다.</p>
<h3 id="일대다-다대일">일대다, 다대일</h3>
<p>실생활에서도 가장 흔하게 볼 수 있는 연관관계 다중성이다. 하나의 엔터티가 여러 엔터티와 연관될 때 사용한다.</p>
<p>ex) 팀과 선수관계, 반과 학생관계, 게시판과 게시글의 관계 등</p>
<pre><code class="language-java">@Entity
public class Post{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long postId;

    @ManyToOne
    @JoinColumn(name = &quot;board_id&quot;)
    private Board board;
</code></pre>
<pre><code class="language-java">@Entity
public class Board{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long boardId;

    @OneToMany(mappedBy = &quot;board&quot;)
    private List&lt;Post&gt; posts;
</code></pre>
<p><code>@ManyToOne</code>과 <code>@OneToMany</code>를 사용하여 지정한다.</p>
<h3 id="다대다">다대다</h3>
<p>다대다 연관관계는 여러 엔터티가 여러 엔터티와 연관되는 관계이다. </p>
<p>ex) 학생과 전공수업</p>
<pre><code class="language-java">@Entity
public class Student{

    ...

    @ManyToMany
    private List&lt;Class&gt; classes;</code></pre>
<pre><code class="language-java">@Entity
public class Class{

    ...
    @ManyToMany
    private List&lt;Student&gt; students;</code></pre>
<p>위에서 언급했듯이 다대다 관계는 문제를 일으킬 가능성이 높다.</p>
<p>다대다연관관계가 생성되면 자동으로 중간테이블(외래키만 가지는)이 생기는데 이를 통하는 조인발생 시 예상보다 복잡한 쿼리 때문에 성능의 저하가 일어날 수 있다.</p>
<p>⇒ <strong>다대다를 일대다 혹은 다대일로 풀어서 설계하는 것이 좋겠다.</strong></p>
<hr>
<h2 id="방향">방향</h2>
<p>DB는 관계는 따로  방향이 없다. 데이터베이스 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다.</p>
<pre><code class="language-sql">CREATE TABLE players(
    id bigint primary key not null auto_increment,
    name varchar(20) not null,
    team_id bigint not null ,
    CONSTRAINT fk_team
                    FOREIGN KEY (team_id)
                    REFERENCES teams(id)
                    ON DELETE CASCADE
                    ON UPDATE CASCADE
);

CREATE TABLE teams(
    id bigint primary key not null auto_increment,
    name varchar(20) not null
)</code></pre>
<p>위처럼 teams테이블의 id를 참조하는 player테이블을 생성하면</p>
<img src="https://velog.velcdn.com/images/tech_bae/post/d8d02247-7d2e-4317-ab29-bfb4b907cc66/image.png" width="400"/>


<p>위와 같은 관계가 생긴다. players가 외래키를 가지고 있지만 DB에서는 양쪽에서 조인이 가능하다.</p>
<pre><code class="language-sql">SELECT 
    p.name AS player_name,
    t.name AS team_name
FROM 
    players p
JOIN 
    teams t ON p.team_id = t.id;</code></pre>
<pre><code class="language-sql">SELECT 
    t.name AS team_name,
    p.name AS player_name
FROM 
    teams t
JOIN 
    players p ON t.id = p.team_id;</code></pre>
<h3 id="객체관계의-방향">객체관계의 방향</h3>
<p>그렇다면 Entity객체는 어떨까?</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/6e0fde99-8187-48ce-adf2-6fdbb4ba25ad/image.png" alt=""></p>
<p>Players이 Team을 가지고 있기에 이제 접근이 가능하다. (플레이어에서 팀으로)</p>
<p>하지만 팀에서 플레이어한테 접근을 할 수가 없다..! DB에서와는 다른 특징을 보인다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/25cf53c1-2e93-4f12-b9fa-21be41045e15/image.png" alt=""></p>
<p>그래서 Team에서도 Players에 접근이 가능하도록 Team에 players를 추가했다. </p>
<p>이제 서로 참조가 가능하다.</p>
<p>이를 양방향 관계라고 하지만 사실 양방향 관계는 단방향 참조를 각각 가지는 것이므로 단방향 두개라고 생각할 수 있다.</p>
<blockquote>
<aside><img src="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1677813562/noticon/z1wvbmjlizsmcvmfksfq.png" alt="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1677813562/noticon/z1wvbmjlizsmcvmfksfq.png" width="40px" />그럼 걍 전부다 양방향으로 하면 되는거 아님?
</blockquote>
</aside>


<blockquote>
<aside>
<img src="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1701358143/noticon/rdgvkj1ghnucix4urkek.png" alt="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1701358143/noticon/rdgvkj1ghnucix4urkek.png" width="40px"/> ㄴㄴ 그럼 안됨. 딱봐도 복잡해질 거 같지 않음? 유지보수도 힘들어지고 성능도 안좋아지고 JPA가 어떻게 매핑해야하는 지 헷갈려함. Post와 Board가 양방향 관계를 맺고 있을 때 님이 게시글을 다른 게시판으로 옮기고 싶다고 해보면, 이걸 Post에서 setBoard()로 할지 Board에서 getPosts()로 할지 헷갈릴 수 있지 않겠음?
<br>
그래서 일반적으로 모두 단방향으로 해놓고, 나중에 꼭 필요하다고 생각되는 것만 양방향을 고려하는 식으로 함.
<br>
그래서 이제 JPA가 양방향 관계에서 안헷갈리게 하기 위해서 연관관계 소유자라는 걸 지정해줘야함.
</blockquote>
</aside>

<h2 id="연관관계-소유자">연관관계 소유자</h2>
<p>위 처럼 양방향 관계에서 어떤 Entity가 외래키를 관리하는지 JPA에게 알려줘야 안헷갈려하지 않겠는가.</p>
<p>이를 위해서 우리는 양방향 연관관계  <strong>어떤 연관관계가 외래키를 진짜 가지고있는지를 설정</strong>해주어야 한다.</p>
<p>연관관계의 주인만이 두 객체 사이에서 조회, 수정, 삭제가 가능하고 아니라면 조회만 가능하다.</p>
<p>외래키를 가지는(연관관계 소유자)객체에는 <code>@JoinColumn</code>으로 지정하고</p>
<p>연관 관계의 주인이 아닌 객체에 <code>mappedBy</code>속성으로 지정할 수 있다.(Table로 따지면 외래키를 가지지 않는 /  참조당하는 쪽)</p>
<p>⇒ 참고로 단방향일땐 <code>@JoinColumn</code>만 사용하면 된다.</p>
<p>Team과 Players의 관계로 예를 들어보자.</p>
<pre><code class="language-java">@Entity
public class Players {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;team_id&quot;)
    private Teams team;</code></pre>
<p>Players가 다(Many)쪽에 속하므로 Playsers가 외래키를 갖는 것이 적합하여 Players에 <code>@JoinColumn</code>을 붙여주고, Team에는 <code>mappedBy</code>을 사용하여 연관관계 소유자를 지정해주어야 한다.</p>
<pre><code class="language-java">@Entity
public class Teams {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Players&gt; players;
}</code></pre>
<p>이처럼 <code>mappedBy</code>로 지정할 때 값은 대상이 되는 변수명을 따라 지정한다.</p>
<h3 id="다대다의-연관관계-소유자">다대다의 연관관계 소유자</h3>
<p>다대다 관계에선 자동으로 중간테이블(외래키만 가지는)을 생성한다. 이 중간 테이블의 이름이나 칼럼명을 명시적으로 커스터마이징할 때 <code>@JoinTable</code>을 사용한다. 물론 <code>mappedBy</code>을 통해 주인이 아님을 명시적으로 정의해야한다.</p>
<pre><code class="language-java">@Entity
public class Student{

    ...

    @ManyToMany
    @JoinTable(
        name = &quot;student_class_table&quot;,
        joinColumns = @JoinColumn(name=&quot;student_id&quot;), //현재 엔터티의 외래키
        inverseJoinColumns = @JoinColumn(name=&quot;class_id&quot;) //상대 엔터티의 외래키
    )
    private List&lt;Classes&gt; classes;

    ...

}</code></pre>
<p>다만, 위에서도 언급했듯이 다대다연관관계는 설계적 결함이 있는 경우라고 생각되어진다. 고로 일대다/일대일 구조로 바꾸어 설계하는 것이 성능/유지보수등 여러 방면에서 이로울 것 이다.</p>
<hr>
<h2 id="영속성-전이--cascade">영속성 전이 : CASCADE</h2>
<p>게시판과 게시글 엔터티가 있을 때, 게시판은 여러개의 게시글을 자식 엔터티로 가지고 있을테다. 이때 만약 게시판을 영속상태로 만들고 싶을때 자식 엔터티인 게시글들도 일일히 영속상태로 만들기 귀찮고 복잡할 수 있다. 이 때 사용할 수 있는 것이 영속성 전이이다. </p>
<p><strong>영속성 전이는 JPA에서 특정 엔터티를 영속상태로 만들때 이와 연관된 엔터티도 함께 영속상태로 만들때 사용한다.</strong></p>
<p>영속성 전이를 적용하면 게시판(부모 엔터티)을 저장하면 게시글(자식 엔터티)도 함께 저장되고, 삭제를 하는 경우에도 같은 방식으로 게시판이 삭제되면 게시글도 삭제 된다. </p>
<p>영속성 전이는 <code>@ManyToOne</code>과 <code>@OneToMany</code>관계에서 <code>cascade</code>속성을 통해 사용한다.</p>
<pre><code class="language-java">@Entity
public class Boards{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String description;

    @OneToMany(mappedBy = &quot;board&quot;, cascade = CascadeType.ALL)
    private List&lt;Posts&gt; posts;
}</code></pre>
<h3 id="cascade의-종류">CASCADE의 종류</h3>
<ul>
<li><code>CascadeType.ALL</code> : 모든 Cascade 옵션을 적용</li>
<li><code>CascadeType.PERSIST</code> : 엔티티를 영속화할 때, 연관된 엔티티도 함께 영속화</li>
<li><code>CascadeType.REMOVE</code> : 엔티티를 제거할 때, 연관된 엔티티도 함께 제거</li>
<li><code>CascadeType.MERGE</code> : 엔티티 상태를 병합할 때, 연관된 엔티티도 함께 병합</li>
<li><code>CascadeType.REFRESH</code> : 부모 엔티티를 Refresh하면, 연관된 엔티티도 함께 Refresh</li>
<li><code>CascadeType.DETACH</code> : 부모 엔티티를 Detach하면, 연관된 엔티티도 함께 Detach</li>
</ul>
<p>대부분의 경우 ALL을 사용하지만, 상황과 목적에 맞게 사용하면 된다.</p>
<hr>
<h2 id="고아객체">고아객체</h2>
<p>이름부터 폭력적인 이 고아객체는 <strong>말 그대로 부모가 버리거나, 부모가 없어진 객체</strong>이다.</p>
<pre><code class="language-java">Board board = em.find(Board .class, 1L);
board.getPosts().remove(0);</code></pre>
<p>위 코드처럼 게시판에서 게시글을 삭제하면 삭제된 게시글은 부모가 없어져 고아객체가 된다. 이때 고아객체 제거 기능이 참이라면 해당 게시글은 게시판에서만 삭제되는 것이아니라 DB에서 삭제된다.</p>
<pre><code class="language-java">DELETE FROM posts WHERE ID = xxx;</code></pre>
<p>실제로 위와 같은 SQL쿼리가 DB에 적용되며 부모잃은 불쌍한 게시글은 DB에서 삭제되고 만다.</p>
<p>이는 게시판 자체가 삭제되어도 같은 원리로 삭제된 게시판과 연관된 모든 게시글이 삭제된다.(<code>CascadeType.REMOVE</code>와 비슷)</p>
<blockquote>
<p>부모 삭제 시 자식 자동 삭제는 orphanRemoval만으로 보장되지 않을 수 있으므로 보통 <code>cascade = REMOVE</code>를 함께 설정한다.</p>
</blockquote>
<br>

<p>고아객체 제거는 위 설명과 같이 자식 엔터티가 부모 엔터티에 종속되어있을 때 사용된다. 부모로부터 참조가 제거되었다면 유일한 참조를 잃었다고 판단되는 객체가 고아객체이다. <strong>즉 고아객체 제거 기능은 자식엔터티를 참조하는 곳이 하나만 존재할 때 사용</strong>할 수 있다.</p>
<p>고아객체 제거 기능은 <code>@OneToOne</code>, <code>@OneToMany</code>관계에서만 사용가능한데 orphanRemoval속성을 true로 주어 활성화한다.(기본값은 false이다)</p>
<pre><code class="language-java">@Entity
public class Boards{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String description;

    @OneToMany(mappedBy = &quot;board&quot;, cascade = CascadeType.ALL, orphanRemoval= true)
    private List&lt;Posts&gt; posts;
}</code></pre>
<p>참고로 위와 같이 cascade와 orphanRemoval= true를 함께 사용하면 자식 엔터티의 생명주기를 부모 엔터티로 관리가 가능하다. ⇒ 자식엔터티의 생명주기가 부모 엔터티에 의존</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데브코스] DevChat: 첫 프로젝트 기획과 트러블슈팅(1)]]></title>
            <link>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-DevChat-%EC%B2%AB-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D%EA%B3%BC-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%851</link>
            <guid>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-DevChat-%EC%B2%AB-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D%EA%B3%BC-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%851</guid>
            <pubDate>Wed, 25 Jun 2025 15:11:51 GMT</pubDate>
            <description><![CDATA[<h1 id="프로젝트-시작">프로젝트 시작</h1>
<p>1차 프로젝트를 마치고 얼마되지 않아 바로 2차 프로젝트를 시작하게 되었다. 1차완 다르게 2차부터는 직접 주제를 정하고 기획을 했어야 했다. 그렇기에 팀장의 자리는 너무나 부담이 되어 피하고 싶었지만..이런 당첨되고 말았다. 그래도 어찌하리 막중한 책임감을 갖고 열심히 프로젝트를 이끌려고 했다. 그렇게 우리는 주제선정을 위해 시간을 가졌고 내가 구상한 주제와 팀원의 주제의 치열한 경쟁 끝에 데브코스 2차프로젝트로 <strong>개발자를 위한 채팅 서비스</strong>을 하게 되었다. 솔직히 처음엔 마음에 들지 않았다. 그때 나에겐 채팅이란 다 비슷비슷한 서비스라는 인식이 있었던 것 같다. 나중에 프로젝트 회고에서 다시 한 번 말할 것 같지만, 지금은 너무나 애정이 있는 프로젝트이다.</p>
<blockquote>
<p>프로젝트의 목표는 협업중 깃허브에서의 코드리뷰보다 <strong>가볍고 쉬운 소통을 도와주는 채팅프로그램</strong>이었다.</p>
</blockquote>
<p>그렇게 주제선정을 하고 각자 역할분담을 했다. 나의 첫 역할은 회원가입/로그인/마이페이지였다. 즉 유저와 관련한 기능을 맡게되었다. <del>난 이게 금방 끝날 줄 알았다..인증에 늪에 빠질줄은..</del></p>
<p>그러던 어느날 우리 팀에겐 큰 시련이 닥쳐왔다. 선정된 주제를 제안하신 팀원의 데브코스 하차 소식.. 개인사정이라고 하셔서 자세히 여쭤보진 않았다. 그건 그렇고 그분이 담당하신 부분은 깃허브 이벤트 알림 기능인 WebHook부분이었다. 우리팀은 일단 WebHook부분은 제쳐두고 채팅방기능을 완성한 뒤에 구현하는 것으로 합의했다. 이것이 나의 첫 팀장으로서의 시련이 아니었나싶다. 지금 생각해보니 이때부터 작업량에 대한 집착이 생긴 것 같기도 하다.</p>
<p>그렇게 우리는 작업을 시작하게 되었고, 나로썬 첫 주도적인 프로젝트의 시작이었다. 아래는 초반 개발을 하면서 마주했던 문제들의 극복과정의 일부 정리했다. 트러블 슈팅은 앞으로 몇개 더 업로드 해볼 생각이다.</p>
<p><em>참고로 이 프로젝트의 이름은 <strong>DevChat</strong>이다. 로그인 화면은 디자인하는 과정에서 임시로 작명한거였는데, 의외로 팀원들의 반응이 좋아서 그대로 채택되었다..너무 흔한것 같긴 하다만..</em></p>
<hr>
<h1 id="로그인-실패-시-커스텀-예외-처리-문제">로그인 실패 시 커스텀 예외 처리 문제</h1>
<h3 id="문제상황">문제상황</h3>
<p>로그인 시 존재하지 않는 이메일로 접근하면, <code>MemberService</code>에서 커스텀 예외를 던지도록 구현했는데, Spring Security가 이를 인증 실패로 간주하지 않고 전역 예외로 처리했다.</p>
<p>결과적으로 <code>AuthenticationFailureHandler</code>가 호출되지 않고 예외가 앱 전체로 퍼지는 문제가 발생했다.</p>
<hr>
<h3 id="원인분석">원인분석</h3>
<p><code>AuthenticationManager</code>는 기본적으로 <code>AuthenticationException</code>을 상속받는 <code>UsernameNotFoundException</code> 같은 인증 관련 예외만 “정상적인 인증 실패”로 처리한다. 그러나 내가 정의한 커스텀 예외 <code>AuthenticationException</code>을 상속받이 않았기에 인증 실패가 아닌 시스템 예외로 간주되어 필터 체인을 벗어났다.</p>
<hr>
<h3 id="해결방안">해결방안</h3>
<p><code>MemberService</code>에서 유저를 찾을 수 없는 경우 기존 커스텀 예외가 아닌 <code>UsernameNotFoundException</code>을 던지도록 수정:</p>
<pre><code class="language-java">.orElseThrow(() -&gt; new UsernameNotFoundException(&quot;이메일을 찾을 수 없습니다&quot;));</code></pre>
<p>이로써 <code>Spring Security</code>가 정상적으로 로그인 실패로 인식하고 <code>AuthenticationFailureHandler</code>를 타게 됨.</p>
<hr>
<h3 id="느낀점">느낀점</h3>
<p>Spring Security의 인증 흐름을 더 깊이 들여다보면, <code>ExceptionTranslationFilter</code>가 <code>AuthenticationException</code> 및 <code>AccessDeniedException</code>만 처리하고, 나머지 예외는 그대로 전파된다는 구조를 가진다. 따라서 커스텀 예외를 만들더라도, 그것이 <code>AuthenticationException</code>을 상속받지 않는다면 해당 필터에서 잡히지 않아 인증 실패 흐름이 아닌 전역 예외 처리 흐름으로 넘어간다는 점을 알게 되었다.</p>
<p>이 전까진 예외의 의도 정도만 생각하고 개발을 해왔다. 그러나 이번 경험을 통해서 예외의 의도뿐 아니라 상속 구조, 그리고 프레임워크 내부의 처리 흐름에 대한 이해의 중요성을 체감하게 되었다.</p>
<br>

<hr>
<br>

<h1 id="cors설정-문제">CORS설정 문제</h1>
<h3 id="문제상황-1">문제상황</h3>
<p>프론트엔드(React)에서 백엔드(Spring Boot)로 API 요청을 보낼 때, 브라우저 콘솔에 CORS 관련 에러가 발생하며 요청이 차단되었다.</p>
<p>분명 요청 URL과 포트도 맞췄고, 서버도 정상적으로 동작 중이었는데, 클라이언트 쪽에서 아예 응답 자체를 받지 못하는 상황이었다.</p>
<hr>
<h3 id="원인분석-1">원인분석</h3>
<p>Spring에서는 CORS 문제가 일어날 수 있는 곳은MVC단과 Security단이다.</p>
<ul>
<li>인증/인가가 필요 없는 요청은 <code>DispatcherServlet</code>을 통해 MVC에의해 처리되고,</li>
<li>인증/인가가 필요한 요청은 Spring Security 필터 체인을 먼저 거치게 된다.</li>
</ul>
<p>문제는 브라우저의 Preflight 요청(OPTIONS 메서드) 때문이다. 이 요청은 실제 요청 전에 브라우저가 서버의 CORS 정책을 확인하기 위해 보내는 것으로, Security를 거져야하는 요청이면 Security가 응답을 한다. 이때  <code>WebMvcConfigurer</code>에만 CORS 설정을 해둔 상태였기 때문에, 브라우저가 CORS 에러를 발생시킨 것이다.</p>
<p>즉, CORS 설정을 WebMvc 단에만 한 것은 MVC에서만 유효하고, Security를 통과하는 요청에는 전혀 영향을 주지 못한다는 것을 놓쳤던 것이 문제의 본질이었다.</p>
<hr>
<h3 id="해결방안-1">해결방안</h3>
<p><code>SecurityFilterChain</code> 내부에 다음과 같이 <code>cors()</code> 설정을 추가해 Spring Security가 CORS 요청을 허용하도록 수정했다:</p>
<p><code>http.cors(Customizer.withDefaults());</code></p>
<p>그리고 별도로 작성한 <code>WebConfig</code> 클래스에서 다음과 같이 CORS 정책을 명시적으로 설정하여 클라이언트에서의 요청을 허용했다:</p>
<pre><code class="language-java">@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
                .allowedOrigins(&quot;http://localhost:3000&quot;)
                .allowedMethods(&quot;*&quot;)
                .allowedHeaders(&quot;*&quot;)
                .allowCredentials(true);
    }
}</code></pre>
<p>이 두 설정이 함께 작동하면서 정상적으로 요청이 허용되었고, 클라이언트도 응답을 받을 수 있게 되었다.</p>
<hr>
<h3 id="느낀-점">느낀 점</h3>
<p>이 문제를 통해 웹의 요청/응답 흐름이 생각보다 더 복잡한 구조와 메커니즘을 가지고 있다는 것을 알게 되었다. 특히 Spring에서는 요청이 Security와 MVC 두 레이어로 나뉘어 처리되고, 같은 설정(CORS)이라도 어느 레이어에서 적용되는지가 중요하다는 점을 명확히 느낄 수 있었다. 이번 경험을 계기로 웹 통신 구조나 브라우저의 동작 방식(Preflight 요청 등)에 대한 이해를 더 깊게 할 필요성을 느꼈다. 단순히 문제가 발생했을 때 설정만 고치는 것이 아니라, 그 이유와 내부 흐름까지 이해하는 자세가 중요하다는 것을 다시 한번 깨닫게 되었다.</p>
<br>

<hr>
<br>

<h1 id="사용자-정보-수정이-반영되지-않는-문제">사용자 정보 수정이 반영되지 않는 문제</h1>
<h3 id="문제상황-2">문제상황</h3>
<p>사용자 정보(예: 닉네임, 프로필 이미지 등)를 수정한 뒤에도, 프론트엔드에서 호출하는 &quot;내 정보 조회 API&quot; 응답 값이 <strong>여전히 수정 전 데이터</strong>를 반환했다.</p>
<hr>
<h3 id="원인분석-2">원인분석</h3>
<p>회원정보를 조회하는 API의 메소드는 아래와 같았는데</p>
<pre><code class="language-java">public MemberResponse getMemberDetails(Authentication auth) {
    MemberDetails member = (MemberDetails) auth.getPrincipal();
    return MemberDetails.toResponse(member);
}</code></pre>
<p>Spring Security는 로그인 시 <code>UserDetails</code> 객체를 기반으로 <code>Authentication</code>을 생성하고, 이 객체를 <code>SecurityContext</code>에 저장하여 <strong>세션 단위로 인증 상태를 유지</strong>한다.</p>
<p>이 구조에서는 로그인 당시 생성된 <code>MemberDetails</code> 인스턴스가 <code>Authentication.getPrincipal()</code>에 그대로 담겨 있으며, 이후 사용자 정보가 DB에서 변경되더라도 <strong>기존 세션의 인증 객체는 자동으로 갱신되지 않는다.</strong> 그러므로 DM의 데이터를 수정했어도 인증객체는 로그인시의 유저정보를 여전히 담고 있어, 수정된 유저정보를 조회할 수 없었던 것 이다.</p>
<hr>
<h3 id="해결방안-2">해결방안</h3>
<p>이 문제를 해결하기 위해 생각한 방법은 크게 두 가지다:</p>
<h3 id="--방법-1-authentication-객체를-직접-갱신">- 방법 1. <strong>Authentication 객체를 직접 갱신</strong></h3>
<p>사용자 정보 수정 이후, 새로 생성한 <code>MemberDetails</code>로 인증 객체를 재구성하여 <code>SecurityContext</code>에 다시 설정하는 방식:</p>
<pre><code class="language-java">SecurityContextHolder.getContext().setAuthentication(newAuth);</code></pre>
<p>이 방법이 세션 내 인증 정보와 사용자 정보가 항상 일치시키기에 깔끔하고, 성능에도 조금은 유리할 것 같았지만, 인증객체를 생성하는 로직을 따로 구성해야 하고 재인증 처리도 번거로울 수 있어서 이 방법은 선택하지 않았다.</p>
<hr>
<h3 id="--방법-2-요청마다-db에서-사용자-정보를-다시-조회">- 방법 2. <strong>요청마다 DB에서 사용자 정보를 다시 조회</strong></h3>
<p><code>Authentication.getPrincial()</code>에서 사용자 ID만 추출한 뒤, <strong>DB에서 최신 사용자 정보를 조회해서 응답에 반영</strong></p>
<pre><code class="language-java">    public MemberResponse getMemberDetails(Authentication auth) {
        MemberDetails loginMember = (MemberDetails) auth.getPrincipal();
        Long memberId = loginMember.getId();

        Member member = getMemberById(memberId);
        return MemberMapper.toResponse(member);
    }</code></pre>
<p>나는 이 방식으로 구현했다. 인증 객체는 인증 상태 유지를 위해 그대로 두고, 실제 응답에 필요한 사용자 정보는 <strong>항상 최신 상태 기준으로 처리</strong>할 수 있도록 했다.</p>
<p>인증 객체를 직접 갱신하는 방법도 고려했지만, 구조가 복잡해지고 로직이 불필요하게 늘어나는 점이 부담스러웠다. 반면, DB에서 사용자 정보를 다시 조회하는 방식은 단순하고 변경된 내용을 바로 반영할 수 있어 더 명확하게 느껴졌다. 특히 닉네임이나 프로필처럼 <strong>보안과 직접 관련 없는 정보</strong>라면 굳이 인증 객체까지 건드릴 필요는 없다고 판단했다.</p>
<p>물론, 인가 정보가 DB에 존재하고 그 정보가 변경될 수 있었다면, 인증 객체와 DB 상태 간의 불일치로 인해 인가 로직에 문제가 생겼을 수도 있다. 하지만 당시에는 별도의 인가 시스템이 없는 상태였기 때문에, 이렇게 간단하게 해결하는 방식이 더 적절하다고 판단했다.</p>
<p>추가적인 성능 이슈에 대해서는, 향후 캐싱 등을 통해 충분히 개선할 수 있다고 보았고, 현재 서비스 구조와 요구 수준에서는 <strong>이 방식이 가장 깔끔하고 안전한 선택</strong>이라고 생각했다.</p>
<hr>
<h3 id="느낀점-1">느낀점</h3>
<p>이 문제를 마주하기 전까지 나는 <code>Authentication</code> 객체를 단순히 Spring Security가 사용하는 유저 정보 정도로만 생각하고 있었다. 하지만 실제로는 이 객체가 <strong>로그인 시점의 사용자 상태를 유지하는 일종의 스냅샷</strong>이라는 사실을 알게 되었다. 이러한 구조를 제대로 이해하지 못한 채, 사용자 정보를 수정하면 자동으로 <code>Authentication</code> 객체에도 반영될 것이라고 단순하게 생각했고, 변경 사항이 반영되지 않아 한참을 헤맸다.결국 내가 <strong>생각 없이 사용해오던 Spring Security의 구조</strong>, 특히 <code>Authentication</code> 객체의 역할과 동작 방식에 대해 처음부터 다시 이해하게 되었고, 그동안 기능 위주로만 개발해왔던 내 태도에 대해서도 반성하게 되었다. 이 경험을 통해 단순히 &quot;작동한다&quot;는 수준을 넘어서, Spring Security가 내부적으로 <strong>어떻게 인증 상태를 유지하고 처리하는지</strong>, 그 원리를 정확히 이해하는 것이 얼마나 중요한지 깊이 느낄 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[영속성] MyBatis 쓴다고 구닥다리 아님 ㅇㅈ?]]></title>
            <link>https://velog.io/@tech_bae/%EC%98%81%EC%86%8D%EC%84%B1-MyBatis-%EC%93%B4%EB%8B%A4%EA%B3%A0-%EA%B5%AC%EB%8B%A5%EB%8B%A4%EB%A6%AC-%EC%95%84%EB%8B%98-%E3%85%87%E3%85%88</link>
            <guid>https://velog.io/@tech_bae/%EC%98%81%EC%86%8D%EC%84%B1-MyBatis-%EC%93%B4%EB%8B%A4%EA%B3%A0-%EA%B5%AC%EB%8B%A5%EB%8B%A4%EB%A6%AC-%EC%95%84%EB%8B%98-%E3%85%87%E3%85%88</guid>
            <pubDate>Tue, 06 May 2025 14:27:42 GMT</pubDate>
            <description><![CDATA[<p>JDBC로 모든걸 다 하려하니 너무 번거로웠다. 이를 어느정도 해결할 수 있는 수단 중 하나인 <strong>MyBatis</strong>에 대해 간단히 정리해보겠다. </p>
<hr>
<h1 id="mybatis란">MyBatis란</h1>
<p>JDBC에서 SQL의 결과를 우리가 가지고 있는 Java객체에 매핑하려면 ResultSet을 불러와서 값을 직접 객체에 설정해주어야 한다. 즉 무지 귀찮다.</p>
<p>이를 쉽게 해주는 것이 MyBatis이다. MyBatis는 Java객체와 SQL데이터베이스 사이의 매핑을 쉽게 해주는 SQL 매퍼 프레임워크이다. 주의해야 할 건 이후 MyBatis는 완전한 ORM이 아니다. </p>
<p>MyBatis는 <strong>직접 작성한 SQL</strong>을 통한 결과를 XML이나 Annotation을 통해 수동으로 매핑하는 도구다.</p>
<p>이러한 이유로 MyBatis는 완전한 ORM이 아니다.(수동으로 매핑하므로)</p>
<p>혹자는 MyBatis가 구닥다리 프레임워크라고 할 수 있지만, 상황에 따라 다르다고 볼 수 있다.</p>
<p>특히 SQL 최적화나 복잡한 쿼리 작성이 중요하다면 JPA보다 좋은 선택이 될 수 있다.</p>
<hr>
<h1 id="spring-boot--mybatis-통합-설정-application-yaml-설정">Spring Boot + MyBatis 통합 설정 (Application YAML 설정)</h1>
<p>이는 사실 Spring Boot에서 제공하는 설정인데 우리는 spring boot로 웹 개발할거니 적극 사용하도록 하자.</p>
<p>application.yml파일을 통해 여러 편리한 설정을 할 수 있는데 대충 아래와 같이 생겼다.</p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis
    username: dev_bae
    password:

mybatis:
  type-aliases-package: io.moon.mybatistest.entity
  configuration:
    map-underscore-to-camel-case: true
  mapper-locations: classpath:mappers/*.xml</code></pre>
<p><code>spring.datasource</code> 부분으로 연결정보를 입력하여 자동으로 DB에 접속할 수 있다. JDBC에 비해 아주 깔끔 간단한 모습</p>
<p>mybatis에 여러 설정을 걸 수 있는데</p>
<ul>
<li><code>type-aliases-package</code>: XML에서 저 패키지 안의 클래스들은 간단한 이름으로 참조가 가능하다.(패키지명을 줄줄히 안 써도 된다.)</li>
<li><code>configuration.map-underscore-to-camel-cas</code> : SQL결과의 칼럼명이 스네이크케이스(item_id)일 경우 자바객체의 카멜케이스(itemId)로 자동 매핑해준다.</li>
<li><code>mapper-location</code> : 우리가 작성할 XML기반 매퍼파일(SQL이 작성되는 XML파일)의 위치를 지정한다. classpath: 는 resources/폴더를 기준으로 탐색한다.</li>
</ul>
<hr>
<h1 id="mapper인터페이스">Mapper인터페이스</h1>
<p>MyBatis에서 <code>mapper.xml</code>에 작성한 SQL 쿼리를 수행하려면, 이를 호출할 <strong>메소드를 인터페이스에 정의</strong>해두어야 한다.</p>
<p>이때 <code>mapper.xml</code>의 <code>&lt;select&gt;</code>, <code>&lt;insert&gt;</code> 등 태그의 <code>id</code>는 해당 인터페이스의 <strong>메소드 이름과 정확히 일치</strong>해야 한다.</p>
<p>이 <strong>Mapper 인터페이스는 <code>@Mapper</code> 어노테이션을 사용</strong>하여 선언할 수 있으며,</p>
<p>해당 어노테이션이 붙은 인터페이스에 대해 <strong>MyBatis가 프록시 기반의 구현체를 자동 생성</strong>한다.</p>
<p>생성된 구현체는 Spring에서 <strong>스프링 빈으로 등록</strong>되어 의존성 주입 등을 통해 사용할 수 있다.</p>
<blockquote>
<p>🤔 <strong>프록시 구현체가 뭐임?</strong></p>
</blockquote>
<blockquote>
<img src="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1701358143/noticon/rdgvkj1ghnucix4urkek.png" width="40px"/>
프록시는 자주 듣지않음? 대리인 역할을 하는 객체라고 생각하면 됌. 님 자바에서 인터페이스만     있는데 이걸 구현도 안하고 사용할 줄 앎? 그럼 나도 알려주셈. 안되는거임.
    그래서 MyBatis가 @Mapper가 사용된 인터페이스를 찾아서 프록시 구현체를 지가 만들어 주는 거임. 이 프록시 구현체는 인터페이스의 메소드가 호출되면 인터페이스 대신 해당 메소드를 실행함. 내부적으로 <code>java.lang.reflect.Proxy</code> 또는 CGLIB 같은 기술을 사용해 이 객체를 생성함. 이건 나도 잘 몰라서 추후에 정리할 예정임.
</blockquote>
<br>


<br>
<br>


<pre><code class="language-java">@Mapper
public interface ItemMapper {

    void save(Item item);
    int updatePrice(@Param(&quot;itemCode&quot;) String itemCode, @Param(&quot;price&quot;) Integer price);
    Optional&lt;Item&gt; findByItemCode(@Param(&quot;itemCode&quot;) String itemCode);
}</code></pre>
<p>뭐 이렇게 생겼는데. <code>@Param</code> 은 예를들어 @Param(&quot;itemCode&quot;)라면 XML파일의 #{itemCode}부분과 매핑될 매개변수를 지정한다. </p>
<p>매개변수가 하나라면 @Param이 없어도 혼동이 없겠지만 두 개 이상이 <strong>바인딩</strong>되야 한다면 @Param으로 적확하게 이름을 알려주어야한다. 바인딩이란 <strong>어떤 변수나 매개변수에 실제 값을 연결(할당)하는 것</strong>을 말한다.</p>
<hr>
<h1 id="xml-mapper">XML Mapper</h1>
<p>MyBatis에서 SQL매핑의 방식은 어노테이션 매핑방법과 XML기반 매핑방법이 있다. 이글에선 XML기반 매핑만 다루어보겠다. </p>
<p>JDBC에서는 쿼리를 DB에게 날리려면 연결을 따와서 Statement를 가져와서 이를 실행시켜야 했지만 MyBatis에서는 <strong>XML에 SQL만 매핑시켜놓고 이를 실행시킬 메소드를 인터페이스에 정의해놓기만하면 해당 쿼리를 DB에 실행</strong>시킬 수 있다.</p>
<p>참 편하다.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;
&lt;mapper namespace=&quot;io.moon.mybatistest.mybatis.ItemMapper&quot;&gt;

    &lt;insert id=&quot;save&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;id&quot;&gt;
        INSERT INTO items
            (name, code, price)
        VALUES
            (#{name}, #{itemCode}, #{price})
    &lt;/insert&gt;

    &lt;update id=&quot;updatePrice&quot;&gt;
        UPDATE
            items
        SET
            price = #{price}
        WHERE
            code = #{itemCode}
    &lt;/update&gt;

    &lt;select id=&quot;findByItemCode&quot; resultType=&quot;Item&quot;&gt;
        SELECT
        --  자바객체의 필드이름을 기준으로 결과를 가져와야한다. as를 통해 이를 수행. 
            i.item_id as id,
            i.name,
            i.code as itemCode,
            i.price,
            i.created_at as createdAt
        FROM
            items i
        WHERE
            i.code = #{itemCode}
    &lt;/select&gt;
&lt;/mapper&gt;</code></pre>
<p>저 위 세줄은 그냥 어디선가 가져오길 바란다.</p>
<p><code>mapper</code>태그 안에 매핑될 SQL을 작성하는 방식이고 이 안의 <code>namespace</code>로 작성한 SQL을 실행할 메소드가 정의되어있는 <strong>인터페이스의 경로</strong>를 입력한다.</p>
<p>mapper태그 안에 작성할 SQL문에 따라 지정하고(INSERT,UPDATE,SELECT,DELETE) <code>id</code>에는 자바메소드 이름을 적으면 된다. <code>resultType</code>은 반환타입을 정해준다.</p>
<p><code>useGeneratedKeys</code>은 DB에서 자동 생성된 키(ex. AUTO_INCREMENT)를 자바 객체에 넣어주려면 true로 입력하고, </p>
<p><code>keyProperty</code>는 이 키값를 자바객체의 어느 필드에 저장할지를 지정한다.</p>
<p>추가로 주의해야할 부분은 SELECT시 <strong>DB의 칼럼명과 자바객체의 필드명이 상이하게 다를때</strong>(단, 스케이크케이스에서 카멜케이스는 설정하면 자동으로 매핑)</p>
<p>이를 DB에서 가져올때 <strong>AS를 통해 객체 필드명과 동일하게 가져와야한다.</strong></p>
<hr>
<h1 id="mybatis-실습">MyBatis 실습</h1>
<p>위의 개념들을 총동원해서 간단한 예제를 실습해보자.</p>
<p>Product테이블은 id, name, category, price 칼럼을 가진다. 이를 가격 범위로 검색하는 실습을 해보겠다. </p>
<p>우선 DB에 products테이블을 생성하자</p>
<pre><code class="language-sql">create table products(
    id int auto_increment primary key,
    name varchar(50) not null,
    category varchar(100) not null,
    price int not null
);</code></pre>
<p><code>Products</code> Entity클래스를 만들자</p>
<pre><code class="language-java">
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Products {
    private int id;
    private String name;
    private String category;
    private int price;

    public Products(String name, String category, int price) {
        this.name = name;
        this.category = category;
        this.price = price;
    }

    @Override
    public String toString() {
        return &quot;Products{&quot; +
                &quot;id=&quot; + id +
                &quot;, name=&#39;&quot; + name + &#39;\&#39;&#39; +
                &quot;, category=&#39;&quot; + category + &#39;\&#39;&#39; +
                &quot;, price=&quot; + price +
                &#39;}&#39;;
    }
}</code></pre>
<blockquote>
<img src="https://noticon-static.tammolo.com/dgggcrkxq/image/upload/v1701358143/noticon/rdgvkj1ghnucix4urkek.png" width="40px"/>
 본인 이 Entity클래스에 생성자를 만들지 않아 수많은 에러를 맛봐야했음..<br>
MyBatis는 생성자와 Setter를 기반으로 SELECT의 결과를 객체로 만들기 때문에 생성자가 있어야함..
</blockquote>
<br>
<br>

<p>Mapper인터페이스도 만들자</p>
<pre><code class="language-java">@Mapper
public interface ProductsMapper {
    void saveProducts(Products products);

    List&lt;Products&gt; searchProductsByPriceRange(
            @Param(&quot;category&quot;) String category,
            @Param(&quot;min&quot;) int min,
            @Param(&quot;max&quot;) int max
    );
}</code></pre>
<p>이제 저 @Param에 입력된 값을 잘 기억해서 xml파일도 구성하자.</p>
<pre><code class="language-java">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;
&lt;mapper namespace=&quot;io.moon.mybatistest.mybatis.ProductsMapper&quot;&gt;

    &lt;insert id=&quot;saveProducts&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;id&quot;&gt;
        INSERT INTO products
            (name, category, price)
        VALUES
            (#{name}, #{category}, #{price})
    &lt;/insert&gt;

    &lt;select id=&quot;searchProductsByPriceRange&quot; resultType=&quot;Products&quot;&gt;

        SELECT * FROM products
        WHERE category = #{category}
          AND price BETWEEN #{min} AND #{max}

    &lt;/select&gt;

&lt;/mapper&gt;</code></pre>
<p>반환타입이 <code>List&lt;Products&gt;</code>로 설정되어있고(인터페이스) xml의 <code>resultType</code>이 <code>Products</code>이므로 MyBatis가 결과값을 List에 넣어준다. </p>
<p>즉 각 행이 Products의 객체로 매핑된다.</p>
<p>이제 검색을 하면 되겠지만 테이블에 검색할 데이터가 없다. 이를 간단히 채울 수 있는 메소드를 만들어봤다.</p>
<pre><code class="language-sql">@Component
public class ProductsUtil {

    @Autowired
    private ProductsMapper productsMapper;

    public ProductsUtil(ProductsMapper productsMapper) {
        this.productsMapper = productsMapper;
    }

    ArrayList&lt;Products&gt; productsList = new ArrayList&lt;&gt;();
    List&lt;String&gt; categories = List.of(&quot;Food&quot;, &quot;Beverage&quot;, &quot;Snack&quot;);
    public List&lt;Products&gt; genProducts(int quantity) {

        for (int i = 0; i &lt; quantity; i++) {
            String name = &quot;product&quot; + i;
            Products products = new Products(name, categories.get(i%3), (int) (Math.random() * 10000));
            productsMapper.saveProducts(products);
            productsList.add(products);
        }

        return productsList;
    }
}</code></pre>
<p>Food, Beverage, Snack카테고리를 가진 랜덤한 가격을가진 값들을 테이블에 INSERT하는 역할이다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/290f8c0e-84ad-4bda-b02d-223fec7f95af/image.png" alt=""></p>
<p>잘 작동하는 모습</p>
<p>마지막으로 카테고리별 금액 범위 검색을 해보면</p>
<pre><code class="language-sql">    @Test
    @DisplayName(&quot;searchProducts&quot;)
    void searchProducts() throws Exception {

        List&lt;Products&gt; results = mapper.searchProductsByPriceRange(&quot;Food&quot;,2000,6000);
        for (Products products : results) {
            log.info(&quot;products = {}&quot;, products);
        }
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/4a60a8c7-8c21-402f-9b9f-5187bb0c381e/image.png" alt=""></p>
<p>아주 잘 작동한다.</p>
<p>이처럼 MyBatis는 비교적 복잡한 쿼리가 많다면 JPA보다 좋은 선택이 될 수도? 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[영속성] Spring Data JPA을 위한 첫걸음 : JDBC]]></title>
            <link>https://velog.io/@tech_bae/%EC%98%81%EC%86%8D%EC%84%B1-Spring-Data-JPA%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%B2%AB%EA%B1%B8%EC%9D%8C-JDBC</link>
            <guid>https://velog.io/@tech_bae/%EC%98%81%EC%86%8D%EC%84%B1-Spring-Data-JPA%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%B2%AB%EA%B1%B8%EC%9D%8C-JDBC</guid>
            <pubDate>Mon, 21 Apr 2025 14:50:55 GMT</pubDate>
            <description><![CDATA[<p>어플리케이션은 DB를 사용하여 데이터를 보관 및 관리한다. 왜 DB를 사용해야 할까? 당연히 데이터를 영구적으로 보관하고 싶기 때문이다. DB가 없다면 어플리케이션이 종료되면 그 동안 생성되었던 모든 데이터는 사라진다.</p>
<p>데이터를 DB에 보관하여 오랫동안 저장되게 하는 것을 <strong>영속성(Persistence)</strong>라고 한다. </p>
<p>이는 앞으로 자주 등장할 단어이다. 좀 더 심오한 뜻이 있을 것도 같지만 영속성은 데이터가 휘발되게 하지 않고, 오랫동안 존재하도록 하는 것 이라고 이해하면 될 것이다.</p>
<p>그렇다면 우리는 데이터의 영속성을 위해 우리의 어플리케이션에 DB를 연결시켜주어야한다!</p>
<p>만약 당신이 Oracle을 연결해서 사용하고 있다고 가정해보자 근데 세상에 주식이 떨어져서 OracleDB를 지속할 돈이 부족하다. 그래서 당신은 MySQL로 DB를 바꾸려한다.</p>
<p>이런 이런 세상에 너무나 번거롭다. 연결에 관한 모든 것을 당신은 바꿔야한다. 앞으로 또 이런 일이 있을 때 당신은 이짓을 또하고 싶진 않을테다.</p>
<p>이를 위해 JAVA생태계에선 JPA를 사용한다. JPA를 제대로 이해하기 위한 개념, 기술들을 몇개의 글로 나누어 정리할 생각이다. 이번 글은 이해를 위한 몇가지 개념들과 JDBC에대해서 알아보겠다.</p>
<hr>
<h1 id="데이터베이스-커넥터">데이터베이스 커넥터</h1>
<p>소프트웨어의 데이터를 보관하려면 DB와 연결 및 통신을 할 수 있어야 한다. 이를 가능케하는 것이 <strong>데이터베이스 커넥터</strong>이다.</p>
<p>데이터베이스 커넥터는 어플리케이션에서 작성한 SQL쿼리를 DB로 전달함으로써 데이터를 저장 및 관리하게 해준다.</p>
<p>데이터베이스 커넥터는 크게 두가지 요소로 구성되는데 <strong>드라이버(Driver)</strong>와 클라이언트 <strong>라이브러리(Client Library)</strong>이다.</p>
<h2 id="드라이버driver">드라이버(Driver)</h2>
<p>드라이버는 DB와 실제로 통신하며, SQL문을 전송하고 결과를 받아오는 역할이다. 각 DB마다 통신규약이 다르기 때문에  DB마다 다른 드라이버를 사용한다.</p>
<h2 id="클라이언트-라이브러리client-library">클라이언트 라이브러리(Client Library)</h2>
<p>드라이버는 저수준에서 DB와 연결되어 통신하기 때문에 개발자가 이를 직접 이를 다루기엔 복잡하다.</p>
<p>클라이언트 라이브러리는 이를 추상화하여 개발자가 쉽게 DB와 상호작용할 수 있게하는 고수준 API를 제공한다. 즉, <strong>클라이언트 라이브러리는 드라이버를 추상화한 계층이다.</strong> </p>
<p>우리는 이를 이용해서 쉽게 DB와 연결할 수 있다.</p>
<hr>
<p>그런데 보통 어플리케이션 특히 웹에서는 수많은 요청으로 인해 데이터가 수시로 바뀐다. 그렇다면 우리는 이 DB와의 연결을 어떻게 관리해야 좋을까?</p>
<ol>
<li>계속 연결을 지속한다</li>
<li>데이터가 필요할 때마다 연결을 하고 작업이 끝나면 연결을 끊는다.</li>
</ol>
<p>둘 다 뭔가 낭비가 심해보인다. 그렇다면 어떻게 하면 효율적으로 연결(Connection)을 관리할 수 있을까. </p>
<p>바로 <strong>커넥션 풀링(Connection Pooling)</strong>을 사용하는 것 이다.</p>
<h3 id="커넥션-풀링connection-pooling">커넥션 풀링(Connection Pooling)</h3>
<p>Pool이 무엇인가. 수영장, 연못… 무언가 담겨있는 것정도로 이해가 되지 않는가? </p>
<p>우리는 약수터를 생각해보자. 어르신들이 약수터에 물마실라고 바가지를 들고다니는가? 바가지는 약수터에 둥둥 떠 있고, 약숫물이 필요할때만 사용하고 다시 띄어놓는다.</p>
<p>그렇다 커넥션 풀링도 비슷하다.</p>
<p><strong>즉, 연결들을 미리 생성해두고 연결이 필요할 때마다 이를 할당해서 사용 후 반환하여 재사용이 가능케 하는 방식</strong>이다.</p>
<p>이 방식은 연결 생성 및 폐기에 따른 메모리 손해(오버헤드)를 줄여주고, 만약 폭발적인 연결수가 필요한 상황에서 모든 연결을 생성하면 자원고갈이 일어 날 수 있지만 커넥션 풀링은 그냥 좀 기다리면 된다. (콘서트 예매 대기열이 바로 이런 상황이다.)</p>
<hr>
<h1 id="jdbcjava-database-connectivity">JDBC(Java Database Connectivity)</h1>
<p>글 시작에서 제시한 상황이 기억나는가.</p>
<p>어플리케이션에 이미 연결한 DB를 다른 DB로 바꿔야하는데, DB들은 접속방식과 SQL문법이 각자 다르다. 너무 피곤한 작업일 것 이다.</p>
<p>이를 극복할 수 있도록 하는 것이 JDBC이다.</p>
<p><strong>JDBC는 다양한 DB들과의 연결을 일관된 방식으로 처리할 수 있는 표준 인터페이스(API)를 제공</strong>한다.</p>
<p>위에서 설병한 클라이언트 라이브러리 자바판이라고 생각해도 될 것 같다.</p>
<p>JDBC는 각 DBMS에 맞는 JDBC드라이버를 통해 DB와 연결하기 때문에 DB를 바꿔야 한다면 이 JDBC드라이버만 갈아끼우면 된다. 이러한 특성은 여러 DB을 동시에 연결하여 사용할 수 있게도 한다.</p>
<blockquote>
</blockquote>
<p>  아니; 그럼 연결방식은 드라이버를 바꾸면 된다쳐도 SQL은 어떻게 바뀌어 적용되는거임?</p>
<blockquote>
</blockquote>
<p>니 말이 맞음. 못바꿈. SQL이 다 비슷비슷해서 운좋으면 그냥 써도 되는 SQL문이 있을 수 있겠지만 다른게 있으면 님이 바꿔야함;; 그래서 JPA가 있는거임 기다리셈</p>
<hr>
<h2 id="jdbc실습">JDBC실습</h2>
<p>JPA모른다고 DB연결 못하는 거 아니다. 추후에 불편한 상황이 생길 수 있지만 JDBC만으로도 충분히 DB와 통신하여 데이터를 관리할 수 있다. 이번엔 JDBC를 사용해서 앞선 개념들을 코드로 확인해보자.</p>
<pre><code class="language-java">
@Slf4j
public class JdbcConnection {
        //연결 정보를 상수로 저장해 놓는다.
    public static class MysqlDbConnectionConstant {
        public static final String URL = &quot;jdbc:mysql://localhost:3306/jdbc_exam&quot;;
        public static final String USERNAME = &quot;dev_bae&quot;;
        public static final String PASSWORD = &quot;mypassword&quot;;
    }

    public static Connection getConnection() throws SQLException {
            //DriverManager로 Connection객체를 생성 후 연결한다.
        Connection conn = DriverManager.getConnection(
                MysqlDbConnectionConstant.URL,
                MysqlDbConnectionConstant.USERNAME,
                MysqlDbConnectionConstant.PASSWORD
        );
        log.info(&quot;Connection = {}&quot;, conn);
        return conn;
    }
}</code></pre>
<pre><code class="language-java">@Data
@RequiredArgsConstructor
@AllArgsConstructor
public class Member {
    private int id;
    private String username;
    private String password;
}</code></pre>
<pre><code class="language-java">@Slf4j
public class JdbcExample {

    @Test
    @DisplayName(&quot;JDBC Insert&quot;)
    void insert() throws Exception {

                // 새로운 Member객체를 생성한다. 이 정보가 DB에 들어가게 된다.
        Member member1 = getMember(&quot;bae&quot;,&quot;very_hard_password&quot;);
        Member member2 = getMember(&quot;admin&quot;,&quot;super_password&quot;);

                // DB에 전송할 SQL문을 작성한다. 따로 메소드로 빼서 사용하면 더 편하다.
        String sql = &quot;INSERT INTO users (username, password) values (&#39;%s&#39;, &#39;%s&#39;)&quot;
                .formatted(member1.getUsername(), member1.getPassword());

        String sql2 = &quot;INSERT INTO users (username, password) values (&#39;%s&#39;, &#39;%s&#39;)&quot;
                .formatted(member2.getUsername(), member2.getPassword());

                //Connection객체를 생성, DB와 연결한다.
        Connection conn = JdbcConnection.getConnection();

        //Statement: SQL을 실제로 실행하는 객체이다
        //사실 이는 보안상 이슈가 있다. 이는 아래에서 다루도록 하겠다.
        Statement stmt = conn.createStatement();

                //exeuteUpdate를 통해 SQL을 실행한다. exeuteUpdate는 변경된 row수가 반환된다.
        int rowsAffected = stmt.executeUpdate(sql);
        log.info(&quot;rowsAffected = {}&quot;, rowsAffected);

        int rowsAffected2 = stmt.executeUpdate(sql2);
        log.info(&quot;rowsAffected2 = {}&quot;, rowsAffected2);

    }

    private static Member getMember(String username, String password) {
        return new Member(0,username,password);
    }
}</code></pre>
<p>설레는 마음으로 실행시켜보면</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/fdcb5ca2-d5d3-4102-8577-2791ba805ed0/image.png" alt=""></p>
<p>log와 함께 정상적으로 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/3a67c127-cecb-4413-9bbe-a84269eba042/image.png" alt="">
DB에도 제대로 <code>INSERT</code>된 걸 확인할 수 있다.</p>
<hr>
<h2 id="statement는-위험하다--sql-injection">Statement는 위험하다 : SQL Injection</h2>
<p>사실 <code>Statement</code> 아주 위험한 취약점을 가지고 있다. 바로 <strong>SQL Injection</strong>에 아주 무기력하다. </p>
<p><code>Statement</code>는 입력값을 직접 SQL에 삽입하기 때문인데 코드로 보는 것이 이해가 빠를 것이다.</p>
<pre><code class="language-java">@Test
    @DisplayName(&quot;SQL Injection&quot;)
    void injectTest() throws Exception {

        Member hacker = getMember(&quot;admin&quot;,&quot;&#39; or &#39;&#39; = &#39;&quot;);
        String sql = &quot;SELECT * FROM users WHERE username = &#39;%s&#39; AND password = &#39;%s&#39;&quot;.
                formatted(hacker.getUsername(), hacker.getPassword());

        Connection conn = JdbcConnection.getConnection();
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql);
        while (rs.next()) {
            log.info(&quot;rs.getString(username) = {}&quot;, rs.getString(&quot;username&quot;));
            log.info(&quot;rs.getString(password) = {}&quot;, rs.getString(&quot;password&quot;));
        }
    }
}</code></pre>
<p>위 코드의 결과를 예상해보자. </p>
<p><strong>username</strong>에는 <code>admin</code>이 입력되었고 <strong>password</strong>에는 이상한 문자열이 삽입되있다. 상식적으로 아무런 결과값을 반환하면 안된다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/f10c619b-646b-474e-8b1b-389824fae9c7/image.png" alt=""></p>
<p>어머나 세상에 테이블에 존재하는 모든 계정 정보가 반환되었다.</p>
<p>왜 이런 일이 생기는 걸까? 위의 입력값을 삽입한 SQL문을 보자.</p>
<pre><code class="language-sql">SELECT * FROM users WHERE username = &#39;admin&#39; AND password = &#39;&#39; or &#39;&#39; = &#39;&#39;</code></pre>
<p>SQL에서 <code>AND</code> 보다 <code>OR</code>의 우선순위가 낮음을 인지하고 이를 보기 좋게 변형하면</p>
<pre><code class="language-sql">( (username = &#39;admin&#39; AND password = &#39;&#39;) OR (&#39;&#39; = &#39;&#39;) )</code></pre>
<p>이렇게 된다. 이러면 앞의 AND문이 거짓이라도 위의 <code>&#39;&#39; = &#39;&#39;</code> 가 무조건 참이기에 이 SQL문은 테이블의 모든 행을 가져와 버린다.</p>
<p>정보보안학을 전공한 나로써 용서가 되지 않는다. 이를 어떻게 해결할 수 있을까?</p>
<p>바로 <code>PreparedStatement</code> 을 사용하는 것 이다.</p>
<hr>
<h2 id="preparedstatement">PreparedStatement</h2>
<p><code>PreparedStatement</code> 는 입력값을 자동으로 이스케이프 처리해주기 때문에 SQL Injection을 방어할 수 있다.</p>
<p><code>PreparedStatement</code> 은 SQL문안에 <code>?</code>을 삽입하여 파라미터를 설정할 수 있다.</p>
<pre><code class="language-sql">&quot;SELECT * FROM users WHERE username = ? AND password = ?&quot;;</code></pre>
<p><code>?</code>에 값을 집어넣으려면 <code>setString()</code> 등을 사용한다. </p>
<p>두개의 매개변수를 갖는데 <strong>첫번째 매개변수는 <code>?</code>의 위치</strong> (1부터 시작) </p>
<p><strong>두번째 배개변수에는 집어넣을 값</strong>을 넣는다.</p>
<pre><code class="language-sql">@Test
    @DisplayName(&quot;PreparedStatement&quot;)
    void preparedStmt() throws Exception {

        Member hacker = getMember(&quot;admin&quot;,&quot;&#39; or &#39;&#39; = &#39;&quot;);
        String sql = &quot;SELECT * FROM users WHERE username = ? AND password = ?&quot;;

        Connection conn = JdbcConnection.getConnection();
        PreparedStatement pstmt = conn.prepareStatement(sql);

        pstmt.setString(1, hacker.getUsername());
        pstmt.setString(2, hacker.getPassword());

        ResultSet rs = pstmt.executeQuery();

        if (rs.next()) {
            log.info(&quot;rs.getString(username) = {}&quot;, rs.getString(&quot;username&quot;));
            log.info(&quot;rs.getString(password) = {}&quot;, rs.getString(&quot;password&quot;));
        }else{
            log.info(&quot;rs.getString(username) = null&quot;);
        }
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/015c926d-eb9a-45e4-b86e-e3dc90fbcd13/image.png" alt=""></p>
<p>아까와 달리 아무런 값을 불러오지 못한다.</p>
<p>위와 같이 <strong>SQL Injection방지</strong>하고, <strong>심지어 성능도 좋으니(쿼리캐시)</strong> 무조건  <code>prepareStatement</code>을 사용하는 걸 많이들 추천한다.</p>
<hr>
<h2 id="connection-poolinghikaridatasource">Connection Pooling(HikariDataSource)</h2>
<p>위와 같이 연결이 필요할때 연결을 생성하고 폐기하는 식은 비효율적이므로 Connection Pooling을 사용한다고 위에서 설명했다. 이번엔 이를 어떻게 사용하는지를 코드로 알아보자</p>
<pre><code class="language-java">  @Test
  @DisplayName(&quot;히카리풀에서 멀티쓰레드가 연결 요청하는 테스트&quot;)
  void hikari_multiThread() throws Exception {

      //히카리데이터소스 생성
      HikariDataSource hikari = new HikariDataSource();
      //커넥션풀의 최대 커넥션 수 설정
      hikari.setMaximumPoolSize(2);

      //접속 정보 설정
      hikari.setJdbcUrl(JdbcConnection.MysqlDbConnectionConstant.URL);
      hikari.setUsername(JdbcConnection.MysqlDbConnectionConstant.USERNAME);
      hikari.setPassword(JdbcConnection.MysqlDbConnectionConstant.PASSWORD);

      String sql = &quot;insert into users (username, password) values(?,?)&quot;;

      //각 쓰레드가 실행할 함수 - 히카리풀에서 커넥션을 불러오고 로그, SQL실행 후 반납 로그
      BiConsumer&lt;String, String&gt; multiConn = (user, password) -&gt; {
          try{
              Connection conn = hikari.getConnection();
              PreparedStatement ps = conn.prepareStatement(sql);
              log.info(&quot;{} conn 획득 = {}&quot;, Thread.currentThread().getName(), conn);

              ps.setString(1, user);
              ps.setString(2, password);

              ps.executeUpdate();

              Thread.sleep(3000);

              ps.close();
              conn.close();
              log.info(&quot;{} conn 반환 = {}&quot;, Thread.currentThread().getName(), conn);

          } catch (Exception e) {
              throw new RuntimeException(e);
          }

      };

      Thread thread1 = new Thread(() -&gt; multiConn.accept(&quot;test1&quot;, &quot;test1_password&quot;),&quot;Thread-1&quot;);
      Thread thread2 = new Thread(() -&gt; multiConn.accept(&quot;test2&quot;, &quot;test2_password&quot;),&quot;Thread-2&quot;);
      Thread thread3 = new Thread(() -&gt; multiConn.accept(&quot;test3&quot;, &quot;test3_password&quot;),&quot;Thread-3&quot;);

      thread1.start();
      thread2.start();
      thread3.start();

      //로그 확인을 위해 스레드 종료를 기다림..
      thread1.join();
      thread2.join();
      thread3.join();
  }</code></pre>
<p><code>HikariDataSource</code>은 <code>HikariCP</code>라는 <strong>커넥션 풀을 사용하는 객체</strong>이다.</p>
<p>이를 이용해 커넥션을 생성해놓고 커넥션이 필요할 때마다 꺼내 사용하고 작업이 끝나면 이를 커넥션풀에 다시 반납한다. </p>
<p>위 코드는 최대 2개의 커넥션을 가지게 설정되었고, 이를 3개의 쓰레드로 동시에 사용해보면 아래와 같은 로그를 확인 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/e32d8ab4-99fb-4aa3-87fa-c9ce966b9438/image.png" alt=""></p>
<p>로그의 <code>com.mysql.cj.jdbc.ConnectionImpl@1f9f798c</code> 부분을 보면 <strong>쓰레드2</strong>가 <code>46db63cf</code>연결을 반환한 뒤 <strong>쓰레드 3이 같은 <code>46db63cf</code>을 재사용하는 것을 확인</strong> 할 수 있다.  </p>
<blockquote>
<p>HikariProxyConnection은 다 다른데? 이건 뭐임?</p>
</blockquote>
<blockquote>
</blockquote>
<p><strong>HikariProxyConnection</strong>은 연결 즉 <strong>ConnectionImpl을 감싸는 프록시</strong>임. 
님이 dataSource.getConnection()하면 HikariProxyConnection이게 반환되는 거임.
근데 이 프록시커넥션은 요청마다 새롭게 생성됨.  그래서 저 로그에서 프록시는 다 다른객체임.
사실 <code>close</code>를 하는데 끊어지는게 아니고 반납한다는게 이상하지 않음??
사실은 너가 연결을 컨트롤하는게아니고 저 프록시가 해줌. 임마는 너가 <code>close()</code> 를 하면 는 커넥션을 끊는게 아니고 커넥션 풀로 다시 반환하는걸 하는게 이 녀석임.
그 외에도 너가 이미 닫힌 커넥션을 사용하는 폐급짓을 막아주고 커넥션 사용 시간을 측정, 어떤 스레드가 어떤 커넥션을 얼마나 썼는지 등을 하는 아주 휼룡한 커넥션의 비서임.</p>
<hr>
<h2 id="getgeneratedkeys--resultset">getGeneratedKeys / ResultSet</h2>
<p>이번엔 <code>getGeneratedKeys</code>과 <code>ResultSet</code>을 코드로 간단하게 알아보겠다. </p>
<p>DB와 연결해서 테이블의 로우를 새로 <code>INSERT</code>했다고 생각해보자.  근데 테이블의 id에 <code>auto_increment</code>이 설정되어있다면 이 <code>id</code>값을 어떻게 가져 올 수 있을까? </p>
<p>이때 사용가능한 것이 <strong>getGeneratedKeys</strong> 이다.</p>
<ul>
<li><p><strong>getGeneratedKeys</strong></p>
<pre><code class="language-java">  @Test
  @DisplayName(&quot;getGeneratedKeysTest&quot;)
  void getGeneratedKeysTest() throws Exception {
      String spl = &quot;INSERT INTO users (username, password) values(?,?)&quot;;

      try(
          Connection connection = hikari.getConnection();
          //getGeneratedKeys을 사용하려면 ****Statement.RETURN_GENERATED_KEYS을 입력해야 한다.
          PreparedStatement pstmt = 
                          connection.prepareStatement(spl, Statement.RETURN_GENERATED_KEYS)){

          pstmt.setString(1,&quot;ilikeJDBC&quot;);
          pstmt.setString(2,&quot;imaLiar&quot;);

                  //executeUpdate는 SQL이 반영된 row수만 반환한다.
          pstmt.executeUpdate();

                  //getGeneratedKeys를 통해 마지막 id값을 가져온다.
          ResultSet rs = pstmt.getGeneratedKeys();

          while (rs.next()){
                  //DB가 생성한 키 값만 담은 ResultSet리턴하므로 칼럼인덱스를 이용해 꺼낸다.
              int findByColIdx = rs.getInt(1);
              log.info(&quot;findByColIdx = {}&quot;, findByColIdx);
          }

      }catch(SQLException e){
          throw new RuntimeException(e);
      }</code></pre>
</li>
</ul>
<blockquote>
<p>왜 <code>rs.getInt(&quot;id&quot;)</code>는 안됨? </p>
</blockquote>
<blockquote>
<p>JDBC의 getGeneratedKeys()에  <strong>“DB가 생성한 키 값만” 담은 ResultSet을 리턴</strong>함. 즉 칼럼명이 없는 표를 주면 어떻게 접근할꺼임? 인덱스로 접근할 수밖에 없음.</p>
</blockquote>
<p><strong>ResultSet</strong>은 쿼리 실행 후 반환된 결과 행(Row)들을 저장한다. <code>next()</code>을 통해 다음 행으로 이동한다.</p>
<ul>
<li><p><strong>ResultSet</strong></p>
<pre><code class="language-java">  @Test
  @DisplayName(&quot;ResultSetTest&quot;)
  void resultSetTest() throws Exception {
      String spl = &quot;SELECT * FROM users&quot;;
      try(
            Connection connection = hikari.getConnection();
            PreparedStatement pstmt = 
                            connection.prepareStatement(spl, Statement.RETURN_GENERATED_KEYS)){

                  //실행 결과를 표형식으로 가져온다.
          ResultSet resultSet = pstmt.executeQuery();

                  //next로 행이동, 행이없으면 false를 반환한다.
          while (resultSet.next()){
              String id = resultSet.getString(&quot;id&quot;);
              String username = resultSet.getString(&quot;username&quot;);

              log.info(&quot;id = {}&quot;, id);
              log.info(&quot;username = {}&quot;, username);
          }

      }catch(SQLException e){
          throw new RuntimeException(e);
      }

  }
</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/235d6fca-ab6b-43c2-a419-cf97ffa75dae/image.png" alt=""></p>
<blockquote>
<p>근데 왜 아까부터 close()안함 폐급임?</p>
</blockquote>
<blockquote>
<p>try-with-resources임. 예외가 발생하든 안하든 사용한 자원을 자동으로 닫아줌. 처음봐서 한번 써봤음.</p>
</blockquote>
<hr>
<h2 id="트랜잭션">트랜잭션</h2>
<p>그대는 트랜잭션을 아는가. 대충 안다고 생각하고 설명하겠다. 트랜잭션에는 원자성이라는 특징이 존재한다. </p>
<p><strong>원자성</strong>이랑 한 트랜젝션을 수행하면 이는 실행이 완전히 되거나 아예 실행이 안되야한다는 걸 뜻한다.</p>
<pre><code class="language-java">void logic() {
        int result = repository.a(request);
        repository.b(result);
    }</code></pre>
<p>대충 이런 로직이 서비스단에 있다고 생각해보자 이 로직은 하나의 트랜잭션으로 묶어야한다.</p>
<p>그런데 a()만 성공하고 b()늘 실패한다면 a()결과만 반영되고 b()의 결과는 반영되지 못한다. </p>
<p><strong>트랜잭션의 원자성을 맞족하지 못하게 된다.</strong></p>
<p>그러므로 중간에 예외가 발생하면 <code>logic</code>의 모든 로직을 롤백해야한다. </p>
<p>그런데 repository에서 각 함수를 다른 Connection으로 수행하면 이를 만족하기 어렵다. </p>
<p>즉 하나의 로직에서는 하나의 Connection이 사용되어야 트랜잭션의 원자성을 만족할 수 있다.</p>
<p>지금까지는 <code>dataSource.getConnection();</code>이런식으로 커넥션을 꺼내왔으므로 작업마다 다른 커넥션이 사용됐다. 그렇다면 어떻게 하면 좋을까 </p>
<p>이를 순수 JDBC로 해결하려면 DataSource단위로 오토커밋을 끄고(<code>dataSource.setAutoCommit(false);</code> : 이것도 hikari만 된다.)</p>
<p>모든 작업이 끝나면 commit하는 방법이 있을 거 같다.. 하지만 우리에겐 스프링이 있다.</p>
<p>스프링이 제공하는  <strong>DataSourceUtils</strong>과 <strong>PlatformTransactionManager</strong> 이용해서 안전하고 편하게 트랜잭션을 적용할 수 있다.</p>
<ul>
<li><p><strong>DataSourceUtils</strong></p>
<pre><code class="language-java">  private Connection getConnection(){
          return DataSourceUtils.getConnection(hikari);
      }</code></pre>
<p>  <code>DataSourceUtils.getConnection()</code>을 사용하면 현재 트랜잭션이 있으면 이미 사용했던 커넥션을 반환한다. 아니면 새로운 커넥션을 반환한다. </p>
<p>  즉, <strong>한 트랜잭션 안에서 하나의 커넥션을 사용할 수 있도록 한다</strong>.</p>
<pre><code class="language-java">  private void close(Connection conn, Statement stmt, ResultSet rs){
      JdbcUtils.closeResultSet(rs);
      JdbcUtils.closeStatement(stmt);
      DataSourceUtils.releaseConnection(conn, hikari);
  }</code></pre>
<p>  <code>DataSourceUtils.releaseConnection()</code>도 위의 논리와 같이 재사용을 위해 <strong>커넥션을 재우는게(idle) 아니라 트랜잭션 안이라면 이를 보류</strong>한다.</p>
</li>
</ul>
<blockquote>
<p><code>JdbcUtils.closeStatement(stmt)</code>는 <code>stmt.close()</code>는 뭔 차이임?</p>
</blockquote>
<blockquote>
<p><code>JdbcUtils.closeStatement()</code>내부적으로 이렇게 생김</p>
</blockquote>
<pre><code class="language-java">    public static void closeStatement(@Nullable Statement stmt) {
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException ex) {
                logger.trace(&quot;Could not close JDBC Statement&quot;, ex);
            } catch (Throwable ex) {
                logger.trace(&quot;Unexpected exception on closing JDBC Statement&quot;, ex);
            }
        }
        }</code></pre>
<pre><code>그냥 stmt가 null이어도 짜증나는 NPE 안터지고 무시하고 중간에 예외생기면 그냥 삼켜버리고(안 던짐) 로그남기는 기능 가지고 있음.</code></pre><ul>
<li><p><strong>PlatformTransactionManager</strong></p>
<pre><code class="language-java">  TransactionStatus transaction = platformTransactionManager.getTransaction(
          new DefaultTransactionDefinition()
  );</code></pre>
<p>  위와같이 사용할 수 있는데 하나씩 뜯어서 알아보자</p>
<ul>
<li><p><strong><code>platformTransactionManager</code></strong>는 <strong>스프링에서 제공하는 트랜잭션 관리자 인터페이스</strong>이다.</p>
<p>  이 녀석이 <strong>실제로 트랜잭션을 시작, 커밋, 롤백</strong>하는 주인공이다.</p>
</li>
<li><p><strong><code>getTransaction</code></strong>는 <strong>트랜잭션이 지금 있는지 판단후 트랜잭션을 시작</strong>한다. <strong>기존 트랜잭션이 있으면 기존 트랜잭션을 사용</strong>한다. 이 메소드의 결과로 TransactionStatus 를 반환한다.</p>
</li>
<li><p><strong><code>DefaultTransactionDefinition</code></strong>은 <strong>트랜잭션의 정책을 정의</strong>한다.</p>
<p>  항목으로는 PropagationBehavior, IsolationLevel, Timeout, ReadOnly이 있다.</p>
<p>  아무 설정을 안하면 기본정책이 적용된다.</p>
</li>
</ul>
</li>
</ul>
<pre><code>- **`TransactionStatus`**는 **현재 트랜잭션의 상태를 나타내고 제어**할 수 있는 스프링의 트랜젝션 핸들러다.

    주요 기능으로는 isNewTransaction(), isCompleted(), setRollbackOnly(), isRollbackOnly()등이 있다.</code></pre><p>위를 통합하여 사용하면 트랜잭션이 실제로 적용되는지 테스트해봅시다.</p>
<p><code>repository</code>에 <strong><code>DataSourceUtils</code></strong>적용되있으나 코드로는 서비스만 보도록 하자.(너무 길다.)</p>
<p>레포지토리에는 <code>save()</code>와 <code>update()</code>가 구현되어 있다.</p>
<pre><code class="language-java">public void transactional(Member member, boolean exception) {
        TransactionStatus transaction = platformTransactionManager.getTransaction(
                new DefaultTransactionDefinition()
        );

        try{
            Member saved = repository.save(member);

            if(exception){
                throw new SQLException();
            }
            String newPassword = &quot;Updated&quot;;
            repository.update(saved, newPassword);
            platformTransactionManager.commit(transaction);
            log.info(&quot;커밋되었습니다.&quot;);
        } catch (SQLException e) {
            log.info(&quot;롤백됩니다.&quot;);
            platformTransactionManager.rollback(transaction);
        }
    }</code></pre>
<p>위 코드에서 exception이 false로 들어온다면 정상적으로 save와 update가 적용 될 것 이다.</p>
<p>하지만 <code>save()</code>를 수행하고 중간에 예외가 발생한다면 어떻게 될까. 트랜잭션을 적용하지 않았다면 save만 적용이 되고 update는 적용이 되지않아 원자성을 만족할 수 없다.</p>
<pre><code class="language-java">@Test
    @DisplayName(&quot;transaction_test&quot;)
    void transactional() throws Exception {

        Member member = new Member(null, &quot;TransacionTest&quot;, &quot;Original&quot;);
        boolean exception = true;
        service.transactional(member,exception);

    }</code></pre>
<p>실제로 위 코드처럼 중간에 예외를 터트려보면</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/8513fa9d-58b6-4743-b5c9-e05d5df2ce3c/image.png" alt=""></p>
<p>롤백된다는 로그가 찍히고</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/db1a7f38-6e30-4794-8eb3-bb96cf814b57/image.png" alt=""></p>
<p><strong>실제로 DB상으로도</strong> <code>save()</code>도 적용이 안되고 <strong>롤백되어 버린걸 확인 할 수 있다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] Roy아저씨한테 칭찬 받는 REST정리]]></title>
            <link>https://velog.io/@tech_bae/CS-RESTful%ED%95%B4%EC%A7%80%EA%B3%A0-%EC%8B%B6%EB%82%98</link>
            <guid>https://velog.io/@tech_bae/CS-RESTful%ED%95%B4%EC%A7%80%EA%B3%A0-%EC%8B%B6%EB%82%98</guid>
            <pubDate>Sat, 05 Apr 2025 18:09:53 GMT</pubDate>
            <description><![CDATA[<h1 id="rest란">REST란?</h1>
<p><strong>REST는 Representational State Transfer</strong>의 약자로 표현적 상태 전송이라고 번역할 수 있으나</p>
<p><strong>자원 표현을 통한 상태 전달</strong>이라고 설명하는 것이 이해가 쉬울 것 같습니다.</p>
<p>그럼 한 단어씩 뜯어서 본격적으로 REST를 알아보기 전에 대충 감을 잡아 봅시다.</p>
<ol>
<li><p><strong>자원(Resource)</strong></p>
<p> 자원은 상태의 주체가 되는 데이터입니다.</p>
<p> 예를 들어 회원정보를 수정하고 싶다면 회원정보가 자원이 됩니다.</p>
<p> 자원은 URI로 식별됩니다. <code>https://csi/user/4567</code> </p>
</li>
<li><p><strong>표현(Representation)</strong></p>
<p> 회원정보에는 다양한 정보들이 있을 수 있습니다. 예를들어 ID, email, 나이 등</p>
<p> 이를 조작하려 할 때 <strong>자원의 현재 상태나 의도된 상태에 대한 값들과 무엇을 수행할지에 대한 표현을 Representation</strong>이라고 합니다.</p>
<p> JSON, XML등의 형식으로 http 메시지 본문(Payload)에 담겨 전송됩니다. </p>
<pre><code> PUT /users/123 HTTP/1.1
 Content-Type: application/json

 {
   &quot;email&quot;: &quot;user@example.com&quot;,
   &quot;age&quot;: 25
 }</code></pre></li>
<li><p><strong>상태 전달(State Transfer)</strong></p>
<p> 클라이언트가 서버의 자원상태를 요청(Request)하면 서버는 그에 대한 응답(Response)를 하여 자원의 상태를 전달합니다.(CRUD)</p>
<p> 자원 상태를 전달할 때 위에서 설명한 표현을 통해 특정 형태로 전송합니다. </p>
</li>
</ol>
<p>즉, REST는 자원을 URI로 구분하여 해당 자원의 상태를 주고받는 것을 의미합니다.</p>
<p>여기서 중요한 점은 REST은 상태전달의 목적에 따라 <strong>HTTP 메소드</strong>를 통해 상태전달이 이루어집니다.</p>
<p>예를들어 34번 게시글을 보고싶다면 GET방식으로</p>
<p> <a href="http://posts.com/post/34"><code>http://posts.com/post/34</code></a> 라는 URI에 요청을 보내면 </p>
<p>서버는 이에 해당하는 페이지로 응답해서 그 게시글을 볼 수 있게 되는 것 입니다.</p>
<br>


<hr>
<br>

<h1 id="rest-아키텍처">REST 아키텍처</h1>
<h2 id="rest-설계-원칙">REST 설계 원칙</h2>
<p>위의 설명은 REST를 정말 간략히 설명한 것입니다.</p>
<p>REST를 처음 제안한 Roy Fielding은 REST API가 만족해야할 아키텍처 제약 조건이라고도 하는 <strong>6가지의 설계원칙</strong>을 소개했습니다.</p>
<hr>
<h3 id="클라이언트-서버-구조client-server">클라이언트-서버 구조(Client-Server)</h3>
<p>클라이언트와 서버 구조의 핵심 원리는 <strong>역할의 분리(Separation of Concerns)</strong>입니다. 말이 어려워 보이지만 우리는 이미 이 구조를 사용하고 있습니다.</p>
<p>자원을 요청하는 <strong>클라이언트와 자원이 있는 서버로 나눈 구조</strong>를 따라야 합니다.즉, 서버에서는 비즈니스 로직 처리 및 저장을 책임지고, 클라이언트에서는 요청한 자원을 화면에 나타내는 역할을 책임 집니다.</p>
<p>이 구조를 통해서 사용자 <strong>인터페이스의 다양한 플랫폼 간 이식성이 향상</strong>되고, <strong>서버 구성요소의 단순화를 통해 확장성</strong> 또한 향상될 수 있습니다.</p>
<p>추가로 구성요소들의 의존성이 줄어들어 독립적으로 발전할 수 있게 됩니다.</p>
<p>(프론트와 백엔드가 너무나도 독립적으로 발전해버렸다..)
<img src="https://velog.velcdn.com/images/tech_bae/post/2b514a77-8a4b-46d2-ab0c-a75c40e51d10/image.png" alt=""></p>
<hr>
<h3 id="무상태stateless">무상태(Stateless)</h3>
<p>무상태란 <strong>서버가 클라이언트의 상태를 고려하지 않는 것을 의미</strong>합니다. 클라이언트가 서버에 보내는 요청에는 이를 이해하기 위한 모든 정보를 포함되어야 합니다. 즉, 서버는 클라이언트의 컨텍스트(ex. 쿠키, 세션)를 저장해선 안됩니다.</p>
<p>클라이언트가 이전 요청과 동일한 요청을 보냈다고 해도 서버는 이를 별개의 요청으로 판단하고 하나하나 처리만 하면 됩니다. 즉, 이전 요청이 다음 요청의 처리에 연관 되면 안됩니다.</p>
<p>무상태를 통해 3가지의 이점이 생깁니다.</p>
<ul>
<li><strong>가시성(Visibility) 향상</strong> : 하나의 요청만으로 요청 전체의 의미 파악이 가능</li>
<li><strong>신뢰성(Reliability) 향상</strong> : 부분적인 오류 발생 시 쉬운 복구</li>
<li><strong>확장성(Scalability) 향상</strong> : 요청간 상태를 저장, 관리 하지 않기 때문에 서버는 빠르게 자원을 해제할 수 있고, 구현이 간단해 집니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/504b18e3-7b9f-4d2c-842a-9227e7a5d021/image.png" alt=""></p>
<p>무상태의 단점(trade-off)도 있습니다.</p>
<p>한 요청은 그 요청에 필요한 모든 정보를 가지고 있기 때문에 요청마다 반복적인 데이터(오버헤드)를 포함하여 성능이 저하 될 수 있습니다.</p>
<p>또 일관성 유지가 어렵습니다. 서버가 사용자의 상태를 모르므로, 항상 같은 경험을 제공하기 어려울 수 있습니다.</p>
<p>⇒ 클라이언트의 버전이 서로 다르다면, 각 버전마다 어떻게 동작할지 서버 알 수 없게 되기에(의존적 향상 : 어플리케이션의 동작을 클라이언트의 버전에 의존)</p>
<hr>
<h3 id="캐시cache">캐시(Cache)</h3>
<p>서버의 응답 데이터가 암시적 또는 명시적으로 ‘캐시가능’ 또는 ‘불가능’으로 표시되어야 합니다. 캐시가능한 응답이라면 클라이언트의 <strong>캐시는 같은 요청에 대해 해당 데이터를 재사용할 수 있는 권한</strong>을 가지게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/6133f7a1-fcc1-4b0e-80f8-df4ae06e8734/image.png" alt=""></p>
<p>캐시는 반복되는 요청을 줄이거나 생략함으로 서버의 부담이 줄어 대량의 요청을 효율적으로 처리할수 있다.</p>
<p>이는 네트워크 효율성을 높여 <strong>응답 지연 시간(latency)을 줄일 수 있습니다.</strong> </p>
<p>단, 오래된 캐시가 서버의 최신 데이터와 다르다면 <strong>신뢰성을 저하</strong> 시킬 수 있습니다.</p>
<hr>
<h3 id="일관된-인터페이스uniform-interface">일관된 인터페이스(Uniform interface)</h3>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/528bc106-e3a1-47b1-979c-129f1ea691ab/image.png" alt=""></p>
<p>REST라는 아키텍처 스타일이 다른 네트워크 방식과 가장 뚜렷하게 다른 점은, 구성 요소들(예: 클라이언트와 서버)이 서로 소통할 때 <strong>&#39;통일된 인터페이스&#39;</strong>를 사용한다는 점입니다.</p>
<p>모든 기계가 동일한 콘센트를 쓰게 해서, 플러그를 바꿀 필요 없이 꽂기만 하면 되게 만든 것과 같습니다.
즉, 특정 상황에만 맞게 만드는 게 아니라 모든 구성 요소가 일관된 방식으로 소통할 수록 하여야 합니다. 이러한 원칙을 <strong>일반화(generality)</strong>라고 합니다.</p>
<p>일관된 인터페이스을 위한 조건들은 따로 존재하므로 이에 대한 설명은 아래에서 추가로 설명하겠습니다.</p>
<hr>
<h3 id="계층화layered-system"><strong>계층화(Layered System)</strong></h3>
<p><strong>계층화는 각 구성요가 자신과 직접 상호작용하는 계층 외에는 볼 수 없도록(관심 없음) 제한</strong> 하는 것을 뜻합니다. 이를 통해 전체 시스템의 복잡도를 줄이고, 각 시스템의 독립성을 높입니다.</p>
<p>따라서 계층화 구조에서는 클라이언트가 대상 서버에 직접 연결되었는지, 중간 서버를 통새 연결되었는지 알 수 없습니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/97cff679-f8bd-49dd-8e9c-9bc3ac818e3f/image.png" alt="">
$$이해하기 어렵지만 근본있는 그림이다(Roy아저씨가 그린 그림)$$</p>
<br>

<p>계층화는 여러 목적에서 유용합니다.</p>
<ul>
<li>본 서버는 순수 비지니스 로직만을 수행하고, 중간 서버들이 다른 부가적인 기능(보안, 암호화, 로드밸런싱 등)을 수행하도록 하여 <strong>구조의 유연성을 기대</strong>할 수 있습니다.</li>
<li>중간계층을 통해 연결하므로 <strong>보안성을 향상</strong> 할 수 있습니다.</li>
<li>여러 중간 서버를 두어 <strong>로드밸런싱을 수행하여 확장성을 향상</strong> 시킬 수 있습니다. (갑작스럽게 트래픽이 증가해도 대응이 가능)</li>
<li>계층 구조는 중간 서버를 들려서 통신하기 때문에 오버헤드와 지연이 생겨, 성능의 저하가 생길 수 있습니다. 이를 <strong>중간계층의 캐시기능을 통해 상쇄</strong>가 가능합니다.</li>
</ul>
<hr>
<h3 id="code-on-demandoptional"><strong>Code-On-Demand(Optional)</strong></h3>
<p>COD는 클라이언트가 <strong>서버로부터 코드를 받아서 클라이언트에서 실행하는 것</strong>을 의미합니다.</p>
<p>이를 통해 클라이언트에 미리 구현해두어야할 기능이 줄어들기 때문에 클라이언트의 구현이 간단해지고, <strong>배포 이후에 기능을 다운로드할 수 있기에 시스템의 확장성이 향상됩니다.</strong></p>
<p>하지만 COD는 시스템의 <strong>가시성을 감소시키기 때문에</strong>(클라이언트에서 실행되는 코드이기 때문에 이를 실행하지 않는 외부에선 이해하기 어렵다.) <strong>선택적 제약(Optional)</strong>으로 간주됩니다.</p>
<br>


<hr>
<br>

<h2 id="일관된-인터페이스의-조건">일관된 인터페이스의 조건</h2>
<h3 id="자원에-대한-식별identification-of-resources">자원에 대한 식별(Identification of resources)</h3>
<p>REST에서 <strong>자원이란(resourse) 이름을 가질 수 있는 모든 정보</strong>입니다. 즉, 특정 시점의 어떤 객체 그 자체가 아니라, 시간에 따라 바뀔 수도 있는 개체 (문서, 날씨, 이미지 etc)들에 대한 <strong>개념적 매핑</strong>이라고 말 할 수 있습니다.</p>
<p>리소스는 특정 시점의 결과물이 아니라, 그 뒤에 있는 개념이기 때문에 이를 나타내는 <strong>변하지 않는 식별자</strong>가 필요합니다.</p>
<p>즉 <strong>자원의 값이 변하여도 변하지 않는 일관적인 식별자URI</strong>가 중요합니다.</p>
<p>좋은 식별자를 통해 우리는 어떠한 객체 그 자체가 아니라 <strong>변화가 가능한 개념에 접근</strong> 할 수 있게 됩니다.</p>
<br>


<hr>
<br>

<h3 id="표현을-통한-리소스의-조작manipulation-of-resources-through-representations"><strong>표현을 통한 리소스의 조작(Manipulation of resources through representations)</strong></h3>
<p>REST의 구성요소들은 리소스에 대해 어떠한 행동(조작)을 할 때, <strong>리소스의 현재 상태나 의도된 상태에 대한 표현(representation)</strong>을 사용하여 리소스를 조작합니다.</p>
<p>이 표현에는 리소스의 상태에 대한 값과 <strong>무엇을 수행할 지에 대한 표현(http method)</strong>도 포함되어 있습니다.</p>
<p>즉, 표현을 통해 리소스를 조작하여야지 이를 URI을 통해 조작한다면 일관된 인터페이스가 불가 합니다.</p>
<p>아래와 같이 표현에 자원을 조작할 때 필요한 모든 정보가 담겨 전달 되게 해야한다.</p>
<pre><code>PUT /users/123 HTTP/1.1
Content-Type: application/json

    {
    &quot;userID&quot; : 123
  &quot;email&quot;: &quot;new@example.com&quot;,
}</code></pre><p>상태와 동작을 URI로 표현시키면 ❌ </p>
<pre><code> /updateUserEmail?userId=123&amp;email=new@example.com</code></pre><br>


<hr>
<br>

<h3 id="자기-서술적-메세지self-descriptive-message">자기 서술적 메세지(Self-descriptive message)</h3>
<p><strong>자기 서술적 메세지는 이를 이해하는데 필요한 모든 정보를 포함하는 메세지</strong>입니다.</p>
<p>이는 무상태(Stateless)와 관련이 있습니다. 앞서 설명했듯이 서버는 클라이언트의 상태에 관심이 없기 때문에 요청마다 그 요청을 설명하는데 필요한 모든 정보를 포함하여 요청해야합니다. 이는 응답도 마찬가지 입니다. (클라이언트도 서버안의 컨텍스트를 모른다.)</p>
<p>자기 서술적 메세지는 일반적으로 다음의 요소들을 포함하고 있다.</p>
<ul>
<li><strong>HTTP 메서드</strong> : 동작을 명확하게 표현 (GET, POST, PUT 등)</li>
<li><strong>URI :</strong> 대상이 되는 리소스를 나타낸다.</li>
<li><strong>헤더</strong> : <code>Content-Type</code>, <code>Accept</code>, <code>Authorization</code> 등 메세지의 형식, 요구, 제약을 설명</li>
<li><strong>본문</strong>: 리소스의 상태나 입력 값(JSON, XML형태)</li>
</ul>
<p>그렇다면 아래는 자기 서술적인 메세지 일까요?</p>
<pre><code>HTTP/1.1 200 OK
Date: Sat, 06 Apr 2025 10:15:00 GMT
Content-Type: application/json
Cache-Control: no-store
{
  &quot;id&quot;: 123,
  &quot;name&quot;: &quot;Jane Doe&quot;,
  &quot;email&quot;: &quot;new.email@example.com&quot;,
  &quot;createdAt&quot;: &quot;2022-01-01T10:00:00Z&quot;,
  &quot;updatedAt&quot;: &quot;2025-04-06T10:15:00Z&quot;
}</code></pre><p>놀랍게도 아니랍니다. </p>
<p>왜냐? 이 응답 메시지는 <code>id</code> , <code>name</code> <code>email</code> 의 값만을 알려주고 있고, <code>id</code> , <code>name</code> <code>email</code>이 정확이 무엇을 뜻하는 지에 대한 정보는 제공하지 않고 있습니다. </p>
<p><code>id</code>는 사용자 id일수도 있고, 주문번호일 수도 있고</p>
<p><code>name</code>은 실명인지 닉네임인지</p>
<p><code>email</code>은 로그인용인지 알림 수신용인지</p>
<p>정확하게 제시하고 있지 않기에 이 응답 메시지는 Self-descriptive하지 않습니다.</p>
<p>그렇다면 Self-descriptive해지려면 어떻게 해야할까?</p>
<ol>
<li><p><strong>API 명세 문서화(OpenAPI / Swagger 등)</strong></p>
<p> <strong>API의 구조, 동작, 데이터 타입 등을 명세화하여 문서화</strong></p>
<pre><code class="language-yaml"> User:
   type: object
   required:
     - id
     - name
     - email
   properties:
     id:
       type: integer
       description: 사용자 고유 식별자(로그인시 사용)
     name:
       type: string
       description: 사용자의 실명
     email:
       type: string
       format: email
       description: 알림 수신용 이메일 주소</code></pre>
<p> 이런 API 명세 문서를 클라이언트측과 서버측이 공유하여 사용하면 양측이 메세지에 대한 정확한 이해를 할 수 있다. </p>
</li>
<li><p><strong>JSON Sechema링크</strong></p>
<p> 헤더에 <code>Link</code>를 사용하여 JSON Sechema같은 명세를 링크할 수 있습니다.</p>
<pre><code> Link: &lt;https://api.example.com/schema/users.json&gt;; rel=&quot;describedby&quot;; type=&quot;application/schema+json&quot;
</code></pre><p> 이 링크를 통해 클라이언트는응답에 있는 리소스의 명세를 파악할 수 있습니다. </p>
<p> 리소스 명세를 링크할때 HAL을 이용할 수도 있는데(좀 더 일반적인 것 같다.), HAL을 이후에 알아볼 HATEOAE에서 자세히 다루겠지만 지금은 링크를 통해 다음에 무엇을 할 수 있는지를 알려주는 것이라고만 이해해봅시다.</p>
<p> 이 <strong>HAL을 이용해 데이터의 구조와 의미를 설명하는 JSON Schema(OpenAPI와 비슷)를 링크</strong>할 수 있습니다.</p>
<pre><code class="language-json"> &quot;describedby&quot;: {
       &quot;href&quot;: &quot;https://api.example.com/schemas/user.json&quot;,
       &quot;type&quot;: &quot;application/schema+json&quot;
     }</code></pre>
<p> 이 부분에서 나는 의문이 있었다. 아니 왜 하필 JSON Schema를 연결하는가 OpenAPI는 왜 안되는가?</p>
<p> ⇒ 이는 <strong>OpenAPI는 전체 API의 명세를 담고 있어서 지나치게 복잡하기 때문</strong>입니다.</p>
<p>  <strong>JSON Schema는 하나의 리소스 단위로 스키마 문서를 쪼개서 배포</strong>할 수 있기 때문에 응답에 포함되어 있는 리소스의 명세만 링크가 가능합니다!</p>
</li>
</ol>
<br>


<hr>
<br>

<h3 id="hypermedia-as-the-engine-of-application-statehateoas"><strong>Hypermedia as the engine of application state(HATEOAS)</strong></h3>
<p>이걸 뭐라고 번역을 할까 고민하다가 <strong>하이퍼미디어 기반 애플리케이션 상태 제어</strong>쯤이면 괜찮지 않을까 한다..너무 길어서 이하 <strong>HATEOAS</strong>라고 하겠습니다.</p>
<p><strong>HATEOAS</strong>란 클라이언트 어플리케이션이 서버로 부터 받는 <strong>하이퍼미디어 표현(links, form등)을 통해 상태 전이</strong>되어야 함을 의미합니다. </p>
<p><strong>HATEOAS</strong>를 통해 클라이언트는 표현에 포함된 링크를 통해 다음 동작을 이해하고 진행할 수 있습니다. 즉 서버는 클라이언트가 어떤 상태에 있고, 어디로 이동(상태전이)가 가능한지를 <strong>하이퍼미디어 형태로 안내합니다.</strong></p>
<p>위에 자기서술적메세지에서 잠깐 나온 <strong>HAL</strong>은 <strong>HATEOAS을 가능케하기위해 응답 메시지에서 하이퍼링크를 구조화해서 표현하는 JSON기반 포맷</strong>입니다.</p>
<pre><code class="language-json">{
  &quot;id&quot;: &quot;123&quot;,
  &quot;name&quot;: &quot;Alex&quot;,
  &quot;email&quot;: &quot;alex@example.com&quot;,
  &quot;_links&quot;: {
    &quot;self&quot;: {
      &quot;href&quot;: &quot;/users/123&quot;
    },
    &quot;update&quot;: {
      &quot;href&quot;: &quot;/users/123&quot;,
      &quot;method&quot;: &quot;PUT&quot;
    },
    &quot;delete&quot;: {
      &quot;href&quot;: &quot;/users/123&quot;,
      &quot;method&quot;: &quot;DELETE&quot;
    },
    &quot;friends&quot;: {
      &quot;href&quot;: &quot;/users/123/friends&quot;
    }
  }
}</code></pre>
<p>위와 같이 HAL은 <code>_links</code>키워드를 통해 현재 리소스에 관련된 링크들을 나열합니다.</p>
<p>이를 통해 클라이언트는 응답메시지를 통해 현재상태를 알 수 있고, 다음 할 수 있는 행동(상태)을 파악하고 동작 할 수 있습니다.</p>
<p>이러한 <strong>HATEOAS이 중요한 이유는 서버가 바뀌어도 클라이언트는 이를 몰라도 된다는 것입니다.</strong> </p>
<p>예를들어 특정 동작에 대한 URI가 바뀌었다고 생각해봅시다. </p>
<p>HATEOAS가 만족되지 않는 API를 이용한다면 클라이언트는 URI가 바뀔때마다 이를 파악하고 클라이언트의 코드를 수정해야 합니다. </p>
<p>하지만 HATEOAS가 만족된다면 클라이언트는 서버가 준 링크만 따라가면 되므로 문제가 없습니다. </p>
<p>이러한 이점은 새기능 추가, 다양한 클라이언트의 상황에서도 마찬가지로 적용됩니다.</p>
<br>


<hr>
<br>

<h1 id="그래-그래서-이걸-다-지켜">그래 그래서 이걸 다 지켜?</h1>
<ul>
<li><p>CMIS : REST 바이딩이 있습니다!</p>
<p>  Roy Fielding : ㅋㅋㅋREST바이딩이래 그거 REST아님 (실제로 한말 : “This so-called REST binding is not REST at all.”)</p>
</li>
</ul>
<ul>
<li><p>마소 : API 스타일 가이드 보러오세요!</p>
<p>  Roy Fielding : 응 아니야 걍 HTTP임; (“It&#39;s not REST. It&#39;s just HTTP.”)</p>
</li>
</ul>
<ul>
<li>그 외 (Google APIs, 트위터 등) : RESTful호소자(대부분 RPC)</li>
</ul>
<p>이 글에서 계속 알아봤듯이 REST를 지켜 RESTful한 API를 구현하는 것은 쉬운 것이 아닙니다. 실제로 Roy아저씨도 이걸 느꼈는지 REST 원칙을 엄격하게 적용하는 것이 항상 최선은 아니며, 시스템의 전체를 통제할 수 있는 상황이라면 REST를 따지는 것이 시간 낭비일 수 있다고 언급한 바 있습니다.</p>
<p>그래서 우리도 RESTful에 너무 집착하지 말고 개발하는 시스템에 적합하고 이득이 될만한 부분을 차용하여 적용하는 것이 적합하다고 생각합니다.
<br>
<br></p>
<hr>
<h1 id="마치며">마치며</h1>
<p>사실 이글에 REST에 대한 간략한 개념을 정리할 예정이었습니다. 하지만 알아보면 알아볼 수록 블로그들마다 설명하는 것이 추상적이고 URI에 규칙? 같은걸 소개하고 있었다. 혼란스러워하다가 그냥 REST만든 사람의 설명을 보는게 정확할 거 같아서 Roy아저씨의 REST논문을 토대로 이 글을 작성하게 되었다.(물론 번역기를 십분 활용했다.) 영문으로 된 아주 긴 논문을 이해하는 것은 역시 생각보다 힘들었지만 이곳에 답이 있었다. RESTful하다는 것은 어떠한 정형화된 법칙을 따라야하는 것이 아니라 제시된 여러 제약사항(개념이라고 하고싶다)을 만족하는 것임을 알게 된거 같다.</p>
<p>이번 경험을 통해서 학습을 할때 특히 어떠한 개념을 학습할때는 공식문서가 느리지만 제일 빠른 길인 것 같다. 가장 정확하다는 확신이 서고, 의심이 생기지 않는다. 블로그같은 것을 통해 학습을 할땐 이게 맞는 말인지 항상 의심하고 다른거 찾아보다가 의심하고… 오히려 시간이 낭비되는거 같다.</p>
<p>데브코스 강사님이 항상 공식문서와 친해지라고 한 이유를 알 것 같다. 앞으론 영 읽는게 힘들지만 공식문서를 먼저 찾아야 겠다. 추가호 학습 과정에서 새롭게 알게된 OpenAPI(Swagger)를 학습을 해봐야 겠다.</p>
<hr>
<h1 id="참조">참조</h1>
<p><a href="https://velog.io/@guswlsapdlf/REST-API%EB%9E%80">REST-API란[hjkim]</a>
<a href="https://haesummy.tistory.com/entry/REST-API%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-feat-%EB%A1%9C%EC%9D%B4-%ED%95%84%EB%94%A9-%EB%85%BC%EB%AC%B8#%EA%B7%B8%EB%9F%BC%20%EC%9E%98%20%EC%A7%80%EC%BC%9C%EC%A7%84%20REST%20API%EB%8A%94%20%EB%AD%90%EA%B0%80%EC%9E%88%EC%9D%84%EA%B9%8C-1">REST API에 대하여 (feat. 로이 필딩 논문)[여행하는 개발자 해서미]</a>
<a href="https://ics.uci.edu/~fielding/pubs/dissertation/top.htm">Architectural Styles and the Design of Network-based Software Architectures[Roy Thomas Fielding]</a>
<a href="https://roy.gbiv.com/untangled/">Untangled(Roy Fielding 블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] DAO, DTO, VO, Entity 개념 간단 정리]]></title>
            <link>https://velog.io/@tech_bae/JAVA-DAO-DTO-VO-Entity-%EA%B0%9C%EB%85%90-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@tech_bae/JAVA-DAO-DTO-VO-Entity-%EA%B0%9C%EB%85%90-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 02 Apr 2025 14:17:30 GMT</pubDate>
            <description><![CDATA[<p>웹 공부를하면서 자주 접하는 단어인 DAO, DTO, VO, Entity가 들을 때마다 헷갈려서 개념을 잡아 놓으려 정리하려 한다.</p>
<h1 id="dao">DAO</h1>
<p>이름이 귀엽지 않은가
<strong>DAO</strong>는 <strong>Data Access Object</strong>로 말 그대로 <strong>DB의 데이터에 접근</strong>하는 객체이다.</p>
<p>DAO의 주요역할은 아래와 같다.</p>
<ul>
<li><strong>데이터베이스 연결 및 쿼리 실행</strong>
  SQL을 실행해서 데이터에 접근, 조회 및 조작. </li>
<li><strong>비즈니스 로직과 분리</strong>
  서비스 로직과 DB로직을 분리해서 보다 쉬운 유지보수를 가능케한다.</li>
<li><strong>객체와 관계형 DB사이의 매핑</strong>
  결과값을 객체로 변환하거나, 객체를 DB에 맞게 가공 및 저장한다.</li>
</ul>
<p>Java Spring기준으로 DAO는 여러 모양새를 가지고 있을 수 있다.</p>
<ul>
<li>DB Connection이 설정되어 있는 경우</li>
<li>그렇지 않은 경우</li>
</ul>
<p><strong>MyBatis</strong>는 DB Connection정보를 <code>root-context.xml</code>이라는 파일에 저장하고
<strong>JPA</strong>는 <code>application.yml(properties)</code>에 설정하여 사용한다.</p>
<hr>
<h1 id="dto">DTO</h1>
<p><strong>DTO</strong>는 <strong>Data Transfer Object</strong>로 <strong>데이터를 전달하는 객체</strong>이다.
DTO는 Controller, View 등 레이어 사이에서 데이터를 전달하는 역할을 한다.</p>
<p>데이터를 전달하는게 역할을 가지는 객체이므로 <strong>서비스로직을 가지지 않는다</strong>.
데이터 전달 용도로 사용하는 객체이므로 당연히 서비스로직이 필요없다.</p>
<p>순수한 데이터객체로써 <strong>getter와 setter만</strong> 가진다.</p>
<br>


<pre><code class="language-java">public class DtoEx {
    private String name;
    private int age;

    DtoEx(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
</code></pre>
<br>

<ul>
<li>롬복(Lombok)을 사용하여 가독성이 좋은 코드를 쓸 수 있다!<pre><code class="language-java">@Data   //Setter, Getter, toString
@Builder    //객체를 build 방식으로 생성할 수 있다.
@AllArgsConstructor //모든 필드 값을 매개변수로 받는 생성자
@NoArgsConstructor  //매개변수가 없는 생성자
public class DtoHasSetter {
  private String name;
  private int age;
}</code></pre>
</li>
</ul>
<hr>
<h1 id="vo">VO</h1>
<p><strong>VO</strong>는 <strong>Value Object</strong>로 <strong>값 그 자체의 객체</strong>이다.
그냥 쉽게 Setter가 없는 DTO라고 보면 될 것 같다.(Getter만 존재 할 수 있다.)</p>
<p>즉 VO는 한 번 생성하면 변경이 불가하다(<strong>read-Only</strong>)
그렇기에 새로운 값을 사용하려면 새로운 VO를 생성해야한다.</p>
<p>또한 <strong>VO는 모든 필드값이 같으면 두 객체는 같다</strong>는 정의를 가진다.
이를 위해 <code>equals()</code>와 <code>hashCode()</code>의 오버라이딩이 필요하다.</p>
<pre><code class="language-java">
@Getter
public class VoEx {
    private final String name;
    private final float height;

    public VoEx(String name, float height) {
        this.name = name;
        this.height = height;
    }


    @Override
    public int hashCode() {
        return Objects.hash(name, height);
    }


    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if(!(obj instanceof VoEx)) return false;
        VoEx o = (VoEx)obj;
        return Objects.equals(name, o.name) &amp;&amp; height == o.height;
    }
}</code></pre>
<pre><code class="language-java">public static void main(String[] args) {
        VoEx original = new VoEx(&quot;Max&quot;,182.2f);
        VoEx same = new VoEx(&quot;Max&quot;, 182.2f);

        boolean equals = original.equals(same);

        System.out.println(equals);
    }

// 출력 : true
// equals오버라이딩 안할 시 false가 출력된다)</code></pre>
<hr>
<h1 id="entity">Entity</h1>
<p><strong>Entity</strong>는 실제 <strong>DataBase의 테이블과 1:1 매핑되는 클래스</strong>이다.
고로 Entity는 <strong>DB 테이블의 존재하는 칼럼만을 필드</strong>로 가져야 한다.
테이블에 존재하지 않는 속성을 가져서도 안된다.</p>
<p>또한 <strong>Entity 기준으로 테이블이 형성되므로 Entity의 값이 바뀌면 DB에 반영</strong>된다.</p>
<p>Entity는 데이터를 전달하는 용도로 사용하면 안된다.
즉, <strong>DTO와 분리하여 사용</strong>하여야 한다.</p>
<p>추가로 Entity는 비즈니스 로직을 포함할 수도, setter() 메소드를 포함할 수도 있습니다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Order {
    private final int id;
    private final String customerId;
    private LocalDateTime orderDate;
    private int totalPrice;
    private Oder_Status status;

    //상태변경 비지니스 로직 가능
    public void setStatus(Oder_Status status) {
        this.status = status;
    }

    //id 기준으로 동일성 판단
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Order order)) return false;
        return id == order.id;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }
}</code></pre>
<h1 id="마치며">마치며</h1>
<p>공부하면서 자주 접하는 DAO, DTO, VO, Entity 개념을 그동안 다소 모호하게 알고 있었는데, 이번 글을 통해 가볍게나마 각 개념을 정리할 수 있었다.
단순히 개념을 읽고 이해하는 데 그치지 않고, 간단한 예제를 직접 만들어보며 코딩해 본 경험이 개념을 더 잘 체득하는 데 큰 도움이 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 웹 애플리케이션 아키텍처 이해: WEB, WAS, 서블릿, Spring MVC]]></title>
            <link>https://velog.io/@tech_bae/Spring-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%9D%B4%ED%95%B4-WEB-WAS-%EC%84%9C%EB%B8%94%EB%A6%BF-Spring-MVC</link>
            <guid>https://velog.io/@tech_bae/Spring-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%9D%B4%ED%95%B4-WEB-WAS-%EC%84%9C%EB%B8%94%EB%A6%BF-Spring-MVC</guid>
            <pubDate>Wed, 26 Mar 2025 16:19:50 GMT</pubDate>
            <description><![CDATA[<h1 id="wasweb-application-server">WAS(Web Application Server)</h1>
<h2 id="정적-페이지">정적 페이지</h2>
<ul>
<li>Web Server가 요청이 들어오면 해당 요청에 해당하는 파일을 반환한다.</li>
<li>이 파일은 항상 같은 페이지(밀키트 자판기라고 생각해보자. 밀키트 자판기는 밀키트를 조리해서 주지 않는다. 그냥 밀키트를 줄 뿐)<ul>
<li>WEB이 정적자원을 처리한다.</li>
</ul>
</li>
</ul>
<h2 id="동적-페이지">동적 페이지</h2>
<ul>
<li>클라이언트의 요청에 맞게 동적인 데이터를 반환한다.(원하는 대로 조리를 해서 준다 와 개이득)</li>
<li>Web Server에서 실행되는 프로그램의 연산을 통한 결과가 필요<ul>
<li>이 동적인 페이지 생성하기 위한 프로그램이 <strong>Servlet</strong></li>
<li>이러한 프로그램(Servlet)이 실행되는 서버가 <strong>WAS</strong></li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/1397f8df-35c6-45f8-be57-3bf177006d1f/image.png" alt=""></p>
<br/>

<hr>
<br/>

<h1 id="웹-서버web-server">웹 서버(Web Server)</h1>
<ul>
<li><p>클라이언트의 HTTP 요청을 해석하여 이에 맞는 데이터로 응답</p>
</li>
<li><p>이 HTTP를 해석하고 그에 맞는 데이터 형식으로 보내 준다.</p>
</li>
<li><p>정적데이터들을 다룬다.</p>
<p>  ⇒ 처리속도가 빠르고 트래픽 과부하를 잘 처리</p>
</li>
</ul>
<p><strong>왜 웹서버는 미들웨어가 아닌가?</strong></p>
<p>⇒ 웹서버는 WAS와는 다르게 비지니스 로직을 담당하지 않기 때문에</p>
<br/>

<hr>
<br/>


<h1 id="어플리케이션-서버application-server">어플리케이션 서버(Application Server)</h1>
<ul>
<li><p>데이터를 가공, 연산하여 처리하는 비지니스 로직을 처리하는 서버</p>
</li>
<li><p>HTTP를 통해 컴퓨터나 장치에 비지니스 로직을 수행해주는 미들웨어로 볼 수 있다.</p>
<p>  ⇒http는 요청과 응답에 규약이있음. 이 데이터를 자바(or 다른 언어)로 쓸수 있게 바꾸어줌.</p>
</li>
<li><p>동적데이터를 처리, 주로 DB서버와 같이 수행한다.</p>
</li>
</ul>
<br/>
<br/>

<h2 id="wasweb-application-server-1">WAS(Web Application Server)</h2>
<ul>
<li>WAS는 JAVA EE스펙을 구현하여, 서블렛이나 JSP로 작성된 어플리케이션을 실행하는 소프트웨어(어플리케이션 서버)</li>
</ul>
<p>어플리케이션 서버와 WAS는 같은 것 인가?</p>
<p>⇒ JAVA EE를 구현하여 서블릿으로 작성된 어플리케이션을 실행 할 수 있으면 WAS</p>
<p>즉, WAS는 어플리케이션 서버의 한 종류</p>
<h3 id="jakarta-ee">Jakarta EE</h3>
<p>분산 어플리케이션 개발 목적의 산업 표준 플랫 폼</p>
<p>Servlet(서블릿), JSP, EJB, JDBC, JNDI, JMX, JTA등의 기술을 포함</p>
<br/>

<hr>
<br/>


<h1 id="servelet-containerweb-container">Servelet Container(Web Container)</h1>
<h2 id="cgicommon-gateway-interface">CGI(Common Gateway Interface)</h2>
<p>웹 서버가 동적인 데이터를 반환하기 위해선 이를 위한 비지니스로직을 처리하는 프로그램이 필요하다. 이 데이터를 처리하는 프로그램에 <strong>요청을 적절히 전달해주는 중간자 역할을 하는 프로그램이 CGI프로그램</strong>이다.</p>
<p>(클라이언드 → CGI Process → Program)</p>
<p>하지만 <strong>CGI는 실체가 있는 도구가 아니라 웹 서버와 외부 프로그램 사이에서 정보를 주고 받는 방법, 즉 표준 스펙이자 인터페이스입니다.</strong> </p>
<p>⇒ 다양한 언어(PHP, Python …)로 CGI를 구현(적용) 할 수 있다.</p>
<p>JAVA에선 CGI와 유사한 방식으로 구현된 <strong>서블릿(Servlet)</strong>이라는 프로그램이 존재</p>
<h2 id="서블릿">서블릿</h2>
<p>서블릿은 JAVA를 사용하여 웹 요청(HTTP)을 처리하고 응답을 생성하는 서버 측 프로그램</p>
<ul>
<li>JAVA EE(Jakarta EE)에서 제공하는 웹 기술</li>
<li>클라이언트가 보낸 요청을 받고 서버에서 응답을 생성해서 돌려주는 역할</li>
</ul>
<p>💡 Jakarta EE의 스펙(인터페이스, 명세) 중 하나인 Servlet을 구현 → 이를 WAS로 배포</p>
<h3 id="서블릿은-싱글톤-패턴">서블릿은 싱글톤 패턴</h3>
<blockquote>
<p><strong>싱글톤이란?</strong></p>
<p>하나의 클래스에 대해 하나의 인스턴스만 존재하도록 보장하는 디자인 패턴</p>
</blockquote>
<ol>
<li><p>서블릿은 <strong>싱글톤으로으로 동작</strong>한다. → <strong>모든 요청에 대해서 하나의 같은 인스턴스를 공유</strong></p>
</li>
<li><p><strong>여러 요청이 동시에 들어온다면?</strong></p>
<ul>
<li>하나의 서블릿 인스턴스에 대해 여러 스레드가 동시에 접근</li>
<li>멀티스레드 환경이 자동으로 만들어진다(서블릿 컨테이너가 함)</li>
</ul>
</li>
<li><p>이 때 서블릿 내부에 멤버 변수가 있으면?</p>
<pre><code class="language-java"> private int count = 0;

 protected void doGet(...) {
     count++; // Thread-Unsafe!
 }
</code></pre>
<ul>
<li>경합(race condition)이 발생가능<ul>
<li>둘 이상의 스레드(또는 프로세스)가 동시에 같은 자원에 접근하거나 변경할 때, <strong>실행 순서에 따라 프로그램의 결과가 달라질 수 있는 상황</strong>(가능성이 현실이 됨..)</li>
</ul>
</li>
<li>Thread-Safe하지 않음<ul>
<li>Thread-unsafe라는 말은 여러 스레드가 동시에 어떤 <strong>공유 자원</strong>에 접근하거나 조작할 때, <strong>예상치 못한 결과나 오류가 발생할 수 있는 상황</strong>을 의미(가능성)</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>💡<strong>서블릿의 Thread-Safe를 보장하려면 다음과 같은 방법을 적용해야 한다.</strong></p>
<ul>
<li>무상태(stateless) : 멤버변수를 사용하지 않음(로컬 변수만 사용)</li>
<li>읽기 전용(read-only) : 상수처럼 변하지 않는 값만 사용</li>
<li>동기화 처리 : <code>synchronized</code>, <code>AtomicInteger</code> 와 같은 별도의 Thread-Safe구조를 사</li>
</ul>
<h2 id="서블릿-컨테이너">서블릿 컨테이너</h2>
<p>⚡웹 컨터이너와 서블릿 컨테이너는 같은 말</p>
<ul>
<li>서블릿 자체로 작동하는 것이 아니고 <strong>이를 생성, 소멸 및 관리해주는  것이 서블릿 컨테이너</strong></li>
<li>서블릿이 역할을 수행하는 정의서라면, 서블릿 컨테이너는 이 정의서를 보고 수행한다.</li>
</ul>
<p>💡<strong>Tomcat</strong>은 서블릿 컨테이너이면서도 WAS의 역할도 어느정도 가진다. 하지만 Tomcat은 JAVA EE의 스펙을 전부 구현하지 않기 때문에 → 서블릿 컨테이너라고 할 수 있다.</p>
<p>서블릿 컨테이너는 <strong>IoC(Inversion of Control: 제어의 역전)</strong>과 <strong>DI(Dependency Injection: 의존성주입)</strong></p>
<p>을 할 수 있으며 서블릿 컨테이너의 역할은 다음과 같다.</p>
<p>(IoC와 DI는 아래에서 자세히 알아보자)</p>
<h3 id="서블릿-컨테이너의-기능">서블릿 컨테이너의 기능</h3>
<ul>
<li><p><strong>웹 서버와 통신 지원</strong></p>
<ul>
<li>서블릿과 Web Server가 통신할 수 있도록 한다.</li>
<li>이는 서블릿 컨테이너가 알아서 하기 때문에 개발자는 통신에 신경을 쓸 필요가 없음</li>
</ul>
</li>
<li><p><strong>서블릿 생명주기 관리</strong></p>
<ul>
<li>서블릿의 생성/호출/소멸을 관리</li>
<li>요청이 들어오면 해당 서블릿을 찾고 없으면 초기화(web.xml을 읽는다.)</li>
<li>서블릿의 메소드를 호출 → 실제 기능을 하도록 한다</li>
<li>서블릿의 역할이 끝나면 GC가 관련 인스턴스를 소멸</li>
</ul>
</li>
<li><p><strong>멀티 쓰레딩 관리</strong></p>
<ul>
<li>서블릿 컨테이너는 요청이 들어올때마다 새로운 쓰레드를 생성하여 멀티 쓰레딩 방식으로 처리</li>
</ul>
</li>
<li><p><strong>JSP 지원</strong></p>
<ul>
<li>JSP를 서블릿으로 변환시켜 처리</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/60575dcf-e0d0-4e3a-9964-fb3d858a62f4/image.png" alt=""></p>
<h3 id="ioc와-di">IoC와 DI</h3>
<p>이 이름만 들어도 어지러운 두개는 도대체 무엇일까? 알아보자</p>
<ul>
<li><p><strong>IoC</strong></p>
<p>  <strong>프로그램의 제어 흐름을 개발자가 아닌, 외부(프레임워크)가 맡는 것</strong></p>
<p>  ⇒ 즉, 개발자가 객체 만들고 흐름을 제어하는 것이 아니고, 이를 외부 시스템이 대신 함 ㄱㅇㄷ.</p>
</li>
<li><p><strong>DI</strong></p>
<p>  어떤 객체가 필요로 하는 의존 객체를 직접 만들지 않고, 외부에서 전달받는 방식</p>
<p>  ⇒ <strong>필요한걸 내가 안만듦, 누가 대신 만들어서 내한테 줌.</strong></p>
<p>  ❌ DI없는 코드</p>
<pre><code class="language-java">  class Engine {
      void start() { System.out.println(&quot;엔진 시작&quot;); }
  }

  class Car {
      private Engine engine = new Engine();  // Car가 직접 Engine을 만듬

      void start() {
          engine.start();
      }
  }</code></pre>
<ul>
<li><p><code>Car</code>는 항상 <code>Engine</code>에 강하게 묶여있다. (강한 결합)</p>
<ul>
<li>테스트 어렵고, 의존하는 객체(<code>Engine</code> )을 바꾸기도 어렵다. (구현객체를 참조)</li>
</ul>
<p>⭕ DI 적용 코드</p>
<pre><code class="language-java">class Car {
  private Engine engine;

  // 외부에서 의존성(Engine)을 주입받음
  public Car(Engine engine) {
      this.engine = engine;
  }

  void start() {
      engine.start();
  }
}</code></pre>
</li>
<li><p>이제 <code>Car</code>는 어떤 <code>Engine</code>이든 의존이 가능(유연성 증가)(인터페이스를 참조 → 약한 결합)</p>
<ul>
<li>테스트, 유지보수, 확장성이 좋아짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<br/>

<hr>
<br/>

<h1 id="spring-mvc">Spring MVC</h1>
<p>MVC는 Model-View-Controller의 약자</p>
<p>Spring MVC는 이 구조를 기반으로 웹 어플리케이션을 구성하는 프레임워크이다.(Spring 프레임워크의 하위 모듈)</p>
<h2 id="mvc구조">MVC구조</h2>
<ul>
<li><strong>Model</strong> - 데이터와 비지니스 로직을 처리</li>
<li><strong>View</strong> - 사용자에게 보여지는 화면</li>
<li><strong>Controller</strong> - 사용자의 요청을 받아 처리, 결과를 모델과 뷰로 전달</li>
</ul>
<h2 id="spring-mvc의-동작-흐름">Spring MVC의 동작 흐름</h2>
<ol>
<li><strong>DispatcherServlet</strong>이 요청을 받는다. (스프링의 프론트 컨트롤러)</li>
<li>요청을 <strong>HandlerMapping</strong>에 넘겨준다</li>
<li><strong>HandlerMapping</strong>은 해당 요청을 처리할 <strong>Handler(Controller)</strong>를 탐색</li>
<li><strong>Handler</strong>실행 할 수 있는 <strong>HandlerAdapter</strong>를 탐색한다</li>
<li>찾은 <strong>HandlerAdapter</strong>를 사용하여 <strong>Handler</strong>의 메소드를 실행</li>
<li><strong>Handler</strong>의 반환값은 <strong>Model</strong>과 <strong>View</strong>임</li>
<li><strong>View</strong> 이름을 <strong>ViewResolver</strong>에게 전달, ViewResolver는 해당 View객체를 전달</li>
<li><strong>DispatcherServlet</strong>은 <strong>View</strong>에게 <strong>Model</strong>을 전달. (이때 Model이 null이면 그대로 View사용, 아니면 View에 Model데이터를 렌더링)</li>
<li><strong>DispatcherServlet</strong>은 <strong>View</strong>의 결과를 응답</li>
</ol>
<br/>

<hr>
<br/>

<h1 id="spring-containerioc-컨테이너-di-컨테이너">Spring Container(IoC 컨테이너, DI 컨테이너)</h1>
<p>일단 스프링컨테이너, IoC컨테이너, DI컨테이너 모두 같은 것을 지칭하는 용어다.</p>
<p><del>왜 용어가 통일이 안되는가 헷갈리게..</del></p>
<p>자 스프링 컨테이너를 이해하기 위해선 일단 <strong>Bean</strong>이 무엇인지를 알아야합니다.</p>
<p>Bean이란 간단히 <strong>Spring이 관리하는 객체</strong>입니다. 즉 앞으로 알아볼 스프링 컨테이너가 생성하고 관리하는 객체가 Bean입니다.</p>
<p>앞에서 IoC에 대해서 간단하게 소개했었는데 이 <strong>Bean을 스프링 컨테이너가 IoC를 적용해서 관리</strong>합니다.(알아서 관리한다는 소리, 개발자가 직접 생성하고 관리하지 않는다.)</p>
<p><strong>즉, 스프링컨테이너는 이 Bean의 생명 주기를 관리하는 역할을 합니다.</strong></p>
<ul>
<li>DispatcherServlet이라는 서블릿이 Spring Container을 사용하여 요청을 처리합니다.</li>
</ul>
<p>💡이 IoC컨테이너를 상속하여 부가 기능을 추가한 것이 <code>ApplicationContext</code>이다.</p>
<p>정리하자면 <strong>서블릿컨테이너</strong> 안에 <strong>Spring MVC의 DispatcherServlet</strong>이라는 서블릿이 등록되어 있고,</p>
<p>이 <strong>DispatcherServlet</strong>가 <strong>Spring컨테이너(ApplicationContext)</strong>을 통해 <strong>Bean들을(@Controller, @Service 등)</strong> 관리하여 요청에 의한 비지니스로직을 처리합니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/008dbf2a-f3a6-4aa7-863a-6b8d6b91ee95/image.png" alt=""></p>
<br/>

<hr>
<br/>

<h1 id="웹-어플리케이션-요청-응답-흐름-정리">웹 어플리케이션 요청-응답 흐름 정리</h1>
<ol>
<li><p>클라이언트가 HTTP요청을 보낸다.</p>
</li>
<li><p>이 요청을 <strong>WEB</strong>이 받는다.</p>
<p> 2-1. 정적 자원 요청이면 WEB이 직접 응답한다.</p>
<p> 2-2. 동적자원 요청이면 이른 <strong>WAS</strong>에 전달한다.</p>
</li>
<li><p>WAS(Servlet Container)가 요청을 받아 <strong>Spring MVC의 DispatcherServlet를 호출</strong></p>
</li>
<li><p><strong>DispatcherServlet</strong>이 요청에 따라 처리할 Controller Bean을 결정</p>
</li>
<li><p><strong>Spring Container(ApplicationContext</strong>)가 등록된 Bean들을 이용해 로직 처리</p>
</li>
<li><p>처리 결과를 View 또는 JSON의 형태로 응답</p>
</li>
</ol>
<br/>

<hr>
<br/>

<h1 id="느낀점">느낀점</h1>
<p>웹 애플리케이션 아키텍처를 정리하면서 처음 보는 용어도 많고, 같은 개념을 여러 방식으로 부르는 경우도 많아 많이 헷갈렸던 것 같습니다.
하지만 개념을 하나씩 정리해가다 보니, 각 구성 요소의 역할과 흐름이 조금씩 보이기 시작했고, 웹의 전반적인 구조를 이해하는 데 큰 도움이 되었습니다.</p>
<p>단순히 코드를 따라치는 것만으로는 구현은 가능하겠지만, 구조와 개념에 대한 이해가 없다면 그것은 진짜 &#39;이해한 것&#39;이 아니라고 느꼈습니다.
앞으로도 무언가 어렵거나 개념이 잡히지 않을 때는, 스스로에게 설명하듯 글로 정리하며 학습하는 습관을 가져야겠다는 생각이 들었습니다.</p>
<p>글이 다소 길고 두서없을 수 있지만, 저에게는 분명히 정리가 되는 소중한 시간이었습니다.
혹시 잘못된 내용이나 부족한 부분이 있다면 언제든지 알려주십쇼!
저에게 정말 큰 도움이 될 겁니다.</p>
<p><strong>참조</strong></p>
<blockquote>
<p><a href="https://velog.io/@choidongkuen/Spring-Spring-MVC-Servlet-Servlet-Container-%EB%9E%80#-servlet-container">[Spring] Spring MVC - Servlet, Servlet Container, Spring Container 에 대해</a>
<a href="https://steady-coding.tistory.com/599">[Spring] Servlet, Servlet Container, Spring MVC 정리</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] 자바의 자료구조(배열, Collections) (feat. 해시맵의 구조)]]></title>
            <link>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EC%9E%90%EB%B0%94%EC%9D%98-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EB%B0%B0%EC%97%B4-Collections-feat.-%ED%95%B4%EC%8B%9C%EB%A7%B5%EC%9D%98-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EC%9E%90%EB%B0%94%EC%9D%98-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EB%B0%B0%EC%97%B4-Collections-feat.-%ED%95%B4%EC%8B%9C%EB%A7%B5%EC%9D%98-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Wed, 19 Mar 2025 13:54:52 GMT</pubDate>
            <description><![CDATA[<h1 id="자료구조">자료구조</h1>
<p>자료구조란 데이터를 효율적으로 저장하고 관리하는 방식
<strong>Create</strong>, <strong>Read</strong>, <strong>Update</strong>, <strong>Delete</strong>가 되어야 한다 (CRUD)
이번 글에선 자바의 배열과 Collections 자료구조에 대해서 간단하게 정리해보았다.</p>
<h2 id="배열array">배열(Array)</h2>
<p>메모리에 연속적인 위치에 차례대로 저장된다.</p>
<p>크기 고정(선언시에 지정한 크기 변하지 않는다.)</p>
<pre><code class="language-java">int[] intArr = {1,2,3,4}
//임마는 크기가 4개로 고정</code></pre>
<h2 id="collections-framework">Collections Framework</h2>
<p>자바는 collections라는 라이브러리를 통해 다양한 자료구조를 제공한다.</p>
<p><code>java.util</code>패키지에 포함되어 있음.</p>
<hr>
<h3 id="배열리스트arrayliste">배열리스트(<code>ArrayList&lt;E&gt;</code>)</h3>
<p>배열과 다르게 크기가 동적이다.</p>
<p>일정크기의 배열을 만들었다가 일정 수준 해당 배열이 차면 내부적으로 크기를 늘린다.</p>
<p>자바의 ArraryList는 길이가 자동으로 확장되지만, 자동으로 축소되진 않는다.</p>
<p>인덱스 기반 접근이 빠름 - 배열의 크기 조절, 중간에 요소 삽입/삭제 느림</p>
<p>⇒ 줄이고 싶으면 <code>trimToSize()</code></p>
<pre><code class="language-java">ArrayList&lt;String&gt; arrList = new ArrayList&lt;&gt;();
arrList.add(&quot;Hello&quot;);
arrList.add(&quot;World&quot;); //값 추가
arrList.get(1); //1번 인덱스 값 가져옴
arrList.remove(0) //0번 인덱스 값 삭제
</code></pre>
<hr>
<h3 id="연결리스트linkedliste">연결리스트(<code>LinkedList&lt;E&gt;</code>)</h3>
<p>내부적으로 노드를 사용하여 값을 연결한다. (연속적 위치 ❌)</p>
<p>요소의 삽입과 삭제가 쉽다.</p>
<p>자바의 연결리스트는 이중연결리스트로 구현되어 있다.</p>
<p>⇒ 각 노드가 자기의 앞, 뒤의 노드를 알고 있음</p>
<pre><code class="language-java">LinkedList&lt;String&gt; linkedList = new LinkedList&lt;&gt;();
linkedList.add(&quot;Hello&quot;);
linkedList.add(&quot;World&quot;);
linkedList.add(&quot;!&quot;);
linkedList.removeFirst();
linkedList.remove(1);</code></pre>
<hr>
<h3 id="스택stacke">스택(<code>Stack&lt;E&gt;</code>)</h3>
<p>LIFO(Last In, First Out) 구조를 가진다</p>
<p>⇒ 마지막에 추가된 데이터가 먼저 나온다. (ex. 쌓여올려진 접시들을 생각해보자)</p>
<p>인덱스로 접근하지 않고 <code>push</code>, <code>pop</code>을 이용하여 데이터를 추가, 삭제한다.</p>
<pre><code class="language-java">Stack&lt;Integer&gt; intStack = new Stack&lt;&gt;();
for(int i = 1; i &lt;= 5; i++) {
    intStack.push(i); //1,2,3,4,5
}
intStack.pop(); //1,2,3,4
intStack.pop(); //1,2,3</code></pre>
<hr>
<h3 id="큐queuee">큐(Queue<E>)</h3>
<p>FIFO(First In, First Out) 구조</p>
<p>⇒ 추가된 순서대로 데이터가 나온다. (줄을 선 모습을 생각해보자)</p>
<p>스택과 비슷하게 인덱스로 접근하지 않고 <code>add</code>와 <code>poll</code>로 데이터를 추가, 삭제한다.</p>
<p>💡 <code>Queue</code>는 인터페이스이므로 <code>LinkedList</code> 또는 <code>PriorityQueue</code>로 객체 생성해야 한다.</p>
<pre><code class="language-java">    Queue&lt;Integer&gt; q = new LinkedList&lt;&gt;();
    for(int i = 1; i &lt;= 5; i++){
        q.add(i); // 1,2,3,4,5
    }

    q.poll(); // 2,3,4,5
    q.poll(); // 3,4,5

    System.out.println(q.peek()); //맨 앞 값 반환(제거X) 3
    System.out.println(q); // 3,4,5</code></pre>
<hr>
<h3 id="덱dequee">덱(<code>Deque&lt;E&gt;</code>)</h3>
<p>Deque(Double Ended Queue)는 스택과 큐를 합쳤다고 생각하면 편하다.</p>
<p>⇒ 양쪽(앞/뒤)에서 삽입과 삭제가 모두 가능하다.</p>
<p>💡 <code>Deque</code>는 인터페이스이므로 <code>LinkedList</code> 또는 ArrayDeque로 객체 생성해야 한다.</p>
<pre><code class="language-java">Deque&lt;Integer&gt; dq = new LinkedList&lt;&gt;();
dq.addFirst(10); //10
dq.addLast(20);  //10,20
dq.addFirst(30); //30,10,20

dq.pollLast(); //30, 10
dq.pollFirst(); // 10</code></pre>
<hr>
<h3 id="해쉬맵hashmapk-v">해쉬맵(<code>HashMap&lt;K, V&gt;</code>)</h3>
<p>키-값(Key, Value)쌍을 저장하는 자료구조</p>
<p>내부적으로 배열과 연결 리스트를 조합하여 구현된다.</p>
<p><strong>키가 해슁되어 저장되기 때문에 검색이 빠르다.</strong></p>
<p>💡해쉬(Hash)란? </p>
<p>본인은 해쉬를 정보보안으로 처음 알았다. 특히 포렌식할때 중요한 개념으로 배웠는데 어떠한 파일이나 자료들의 무결성을 증명하는 수단으로 쓰였다. 왜냐? 이 해시라는 것은 원칙적으로는 유일성을 보장하기 때문이다. </p>
<p>⇒ <strong>한마디로 해싱이란 어떠한 데이터를 해쉬알고리즘을 통해서 그 데이터으로만 만들 수 있는 값을 만드는 것</strong>이다.</p>
<p>위 말대로면 아주 완벽해 보이지만 이 또한 인간이 만든것이므로 간혹 다른 데이터가 같은 해쉬값을 만들어 낼 수 도 있다. 이를 <strong>해쉬충돌이라고(Hash Collsion)</strong>이라고 한다.</p>
<p>이러한 문제가 해시맵에서 일어났을 때는 어떻게 대처를 하는지 밑에서 알아보자</p>
<ol>
<li>키를 해싱하여 해시 값을 만든다.</li>
<li>이 해쉬값을 버킷이라는 배열의 길이로 나머지연산(%)하여 나온 값이 인덱스가 되어 해당 위치에 저장된다.(버킷은 고정길이 배열(<code>Node&lt;K,V&gt;[]</code>)가 들어있다. 2차원 배열과 비슷?)</li>
<li>여기서 만약 해쉬충돌이 일어난다면, 같은 버킷 내의 연결리스트로 값을 추가한다.(같은 키값 다른 벨류)</li>
<li>JAVA8부터는 연결리스트가 많아지면(해시충돌이 많이 일어나면) 트리(Red-Black Tree)로 변환되어 성능 최적화</li>
</ol>
<pre><code class="language-java">배열 (Bucket) 구조:
[0]  →  null
[1]  →  null
[2]  →  (&quot;apple&quot;, 100) → (&quot;grape&quot;, 300)  ⬅ (해시 충돌 발생, 연결 리스트로 저장)
[3]  →  null
[4]  →  (&quot;banana&quot;, 200)</code></pre>
<pre><code class="language-java">HashMap&lt;String, Integer&gt; map = new HashMap&lt;&gt;();

// 데이터 추가 (put)
map.put(&quot;Apple&quot;, 1000);
map.put(&quot;Banana&quot;, 2000);
map.put(&quot;Cherry&quot;, 3000);

// 데이터 가져오기 (get)
System.out.println(map.get(&quot;Apple&quot;));  

// 키 존재 여부 확인 (containsKey)
System.out.println(map.containsKey(&quot;Banana&quot;));  

// 값 존재 여부 확인 (containsValue)
System.out.println(map.containsValue(400)); 

// 크기 확인 (size)
System.out.println(map.size());  

// 데이터 삭제 (remove)
map.remove(&quot;Cherry&quot;);

// 전체 출력 (entrySet)
for (var entry : map.entrySet()) {
    System.out.println(entry.getKey() + &quot; -&gt; &quot; + entry.getValue());
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] 스트림(Stream)에 대해서 알아보자]]></title>
            <link>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%8A%B8%EB%A6%BCStream%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%8A%B8%EB%A6%BCStream%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 18 Mar 2025 15:11:16 GMT</pubDate>
            <description><![CDATA[<p>데브코스 수업에선 구현도 하고 그랬지만, 스트림이 뭔지도 몰랐던 나에겐 무리였다. 고로 이 글에서 스트림이 뭐고 어떻게 그리고 왜 쓰는지 정리해보겠다.</p>
<h1 id="스트림stream이란">스트림(Stream)이란</h1>
<ul>
<li>자료구조(컬렉션, 배열 등)을 쉽게 처리할 수 있도록 도와주는  기능</li>
<li><strong>데이터를 반복문 없이 연속적으로 처리</strong>할 수 있게 함</li>
</ul>
<p>기존의 <code>for</code>문 이나 iterator를 사용하면 코드가 길어짐 </p>
<p> 가독성 재사용성 떡락 또한 데이터 타입마다 다르게 다루어야한다 귀찮다.</p>
<p>⇒ 스트림은 <strong>데이터 소스에 상관없이(데이터 소스를 추상화) 모두 같은 방식</strong>으로 다룰 수 있다.</p>
<h2 id="스트림의-특징">스트림의 특징</h2>
<ul>
<li><p><strong>데이터를 한 줄씩 처리</strong> : 자료구조 전체를 메모리에 올리지 않고, <strong>하나씩 순차적으로 처리</strong></p>
</li>
<li><p><strong>불변성</strong> : 기존의 데이터 변경 ❌, 새로운 데이터 반환</p>
</li>
<li><p><strong>람다식 사용(<code>→</code>)</strong></p>
</li>
<li><p><strong>중간 연산과 최종 연산</strong> : 데이터를 가공 → <strong>중간 연산</strong>, 결과 출력 → <strong>최종연산</strong></p>
<ul>
<li><p><strong>지연 연산 사용</strong></p>
<p>  ⇒ 최종 연산이 호출되어야 중간 연산이 수행된다.</p>
</li>
</ul>
</li>
</ul>
<h2 id="스트림-사용하기">스트림 사용하기</h2>
<h3 id="스트림-만들기">스트림 만들기</h3>
<p>스트림을 사용하려면 스트림을 만들어야겠죠. </p>
<p>어떤 스트림을 만드느냐에 따라서 방법이 조금씩 다릅니다.</p>
<ul>
<li><strong>배열 스트림</strong> : <code>Arrays.stream(배열명)</code>으로 만들 수 있습니다.</li>
</ul>
<pre><code class="language-java">String[] strArr = {&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;g&quot;, &quot;h&quot;};
Stream&lt;String&gt; arrStream = Arrays.stream(strArr);</code></pre>
<ul>
<li><strong>컬렉션 스트림</strong>: <code>.stream()</code>으로 생성합니다.</li>
</ul>
<pre><code class="language-java">LinkedList&lt;String&gt; linkedList = new LinkedList&lt;&gt;();
Stream&lt;String&gt; linkedListStream = linkedList.stream();

ArrayList&lt;Integer&gt; arrayList = new ArrayList&lt;&gt;();
Stream&lt;Integer&gt; arrayListStream = arrayList.stream();</code></pre>
<ul>
<li><strong>직접 스트림 생성</strong> : 배열이나 컬렉션 없이 직접 스트림 생성(<code>builder()</code>, <code>Stream.of()</code>)</li>
</ul>
<pre><code class="language-java">//builder 사용
Stream&lt;String&gt; builderStream = Stream.&lt;String&gt;builder()
      .add(&quot;a&quot;).add(&quot;b&quot;)
      .build();

//Stream.of()사용
Stream&lt;String&gt; streamOf = Stream.of(&quot;Hello&quot;, &quot;World&quot;);</code></pre>
<ul>
<li><strong>람다사용</strong> : 람다를 사용하여 스트림을 생성 할 수도 있다. (<code>Stream.generate()</code>, <code>Stream.iterate()</code>)</li>
</ul>
<ol>
<li><p><code>Stream.generate()</code></p>
<pre><code class="language-java"> Stream&lt;Integer&gt; randomStream = Stream.generate(() -&gt; (int) (Math.random() * 100)).limit(3);
</code></pre>
<p> <code>Stream.generate()</code>는 <code>Supplier&lt;T&gt;</code>람다식을 이용하므로 값을 공급하는(단지 생성하는) 스트림을 만들때 사용한다. </p>
<p> <code>limit()</code>를 사용하지 않으면 무한으로 값이 생성된다 → <strong>무한스트림!</strong></p>
</li>
</ol>
<ol>
<li><p><code>Stream.iterate()</code></p>
<pre><code class="language-java"> Stream&lt;Integer&gt; iterateStream = Stream.iterate(0, n -&gt; n + 2).limit(3</code></pre>
<p> <code>Stream.iterate()</code>는 매개변수가 1개 있는 UnaryOperator<T>람다식을 사용해서 초기값으로 연산식을 통해 연산되는 값이 연속적으로 생성된다.</p>
<p> 또한, <code>generate()</code>와 동일하게 <code>limit()</code>를 사용하여 개수를 제한할 수 있다.</p>
</li>
</ol>
<ul>
<li><p><strong>기본 타입형 스트림 :</strong> 기본형 스트림도 만들 수 있다!!</p>
<p>  (<code>IntStream</code>, <code>LongStream</code>, <code>DoubleStream</code>)</p>
</li>
</ul>
<pre><code class="language-java">IntStream intStream = IntStream.range(1,5);
DoubleStream doubleStream = DoubleStream.of(1.2,15.8,2154.8487);</code></pre>
<h3 id="중간연산">중간연산</h3>
<p><strong>데이터를 가공하는 과정</strong>(변형, 필터링… 등)</p>
<p>중간 연산은 <strong>메서드체이닝</strong>이 된다. (연속적으로 수행)</p>
<ul>
<li><p><code>filter()</code> : 조건에 맞는 데이터 선택(필터링) , if문과 비슷한 역할</p>
<pre><code class="language-java">  List&lt;Integer&gt; list = Arrays.asList(1,2,3,4,5);
  Stream&lt;Integer&gt; result = list.stream().filter(n -&gt; n &gt; 3);
  result.forEach(System.out::println);

  /*출력
  4
  5
  */</code></pre>
</li>
<li><p><code>map()</code>: 데이터를 변환</p>
<pre><code class="language-java">  List&lt;String&gt; strList = Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;g&quot;, &quot;h&quot;);
  Stream&lt;String&gt; strStream = strList.stream().map(l -&gt; l.toUpperCase());

  strStream.forEach(l -&gt; System.out.print(l+&quot; &quot;));

  /*출력
  A B C D E F G H </code></pre>
</li>
</ul>
<ul>
<li><p><code>sorted()</code> : 데이터 정렬 (<code>Comparator</code>를 사용한다.)</p>
<pre><code class="language-java">  List&lt;String&gt; words = List.of(&quot;apple&quot;, &quot;banana&quot;, &quot;blueberry&quot;, &quot;oranges&quot;);
  Stream&lt;String&gt; wordsStream = words.stream()
          .sorted((s1, s2) -&gt;  Integer.compare(s1.length(), s2.length()));
  wordsStream.forEach(System.out::println);

  //Comparator.comparing()을 사용하면 더 간결
  Stream&lt;String&gt; sortedWords = words.stream()
          .sorted(Comparator.comparing(String::length));  // 문자열 길이 기준 정렬
  sortedWords.forEach(System.out::println);

  /*출력
  apple
  banana
  oranges
  blueberry
  */</code></pre>
</li>
</ul>
<ul>
<li><p><code>distinct()</code> : 중복제거</p>
</li>
<li><p><code>skip(n)</code> : 앞의 n개 건너뛰기</p>
</li>
<li><p><code>peek()</code> : 중간 상태 확인용(디버깅)</p>
<pre><code class="language-java">  List&lt;String&gt; listForRests = Arrays.asList(&quot;a&quot;, &quot;a&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;f&quot;, &quot;h&quot;);
  Stream&lt;String&gt; wordsStreamForRests = listForRests.stream().distinct()
          .limit(4)
          .peek(s -&gt; System.out.println(&quot;중간확인 : &quot; + s))
          .skip(2);
  wordsStreamForRests.forEach(System.out::println);</code></pre>
</li>
</ul>
<h3 id="최종-연산">최종 연산</h3>
<p>스트림을 처리하여 <strong>최종 결과 반환</strong></p>
<p>최종 연산 이 후엔 <strong>스트림이 소모(사용)되므로, 한번만 실행 가능</strong></p>
<ul>
<li><p><strong>통계내기(기본형 타입만 가능)</strong></p>
<pre><code class="language-java">  //count()로 개수 반환 long타입반환
  long count = IntStream.of(grades).count();

  //sum()으로 합계 구하기
  int summed = IntStream.of((grades)).sum();

  //min() 최솟값 : OptionalInt반환 (null값 대비)
  OptionalInt minimum = IntStream.of(grades).min();

  //max() 최댓값 : OptionalInt반환 (null값 대비)
  OptionalInt maximum = IntStream.of(grades).max();

  //average() 최솟값 : OptionalDouble반환 (null값 대비)
  OptionalDouble avg = IntStream.of(grades).average();
</code></pre>
</li>
</ul>
<ul>
<li><p><code>collect()</code>: 스트림 요소 원하는 자료형으로 변환</p>
<pre><code class="language-java">  List&lt;String&gt; list = List.of(&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;, &quot;apple&quot;);

  // List -&gt; Set 변환 (중복 제거)
  Set&lt;String&gt; set = list.stream()
                  .collect(Collectors.toSet());</code></pre>
</li>
</ul>
<ul>
<li><p><code>reduce()</code> : 스트림의 요소를 누적하여 하나로 합친다.</p>
<ul>
<li><p>초기값이 있으면 <code>T</code> 반환, 없으면 <code>Optional&lt;T&gt;</code>를 반환한다. (null값 대비)</p>
<pre><code class="language-java">List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4, 5);
//초기값이 없으므로 Optional&lt;T&gt;를 반환한다.
Optional&lt;Integer&gt; sum = numbers.stream()
               .reduce((a, b) -&gt; a + b); //15

//초기값 있으므로 Optional불필요(null일 수가 없음)                 
int sumWithInitial = numbers.stream()
              .reduce(5,(a, b) -&gt; a + b); //20                       </code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li><p>매칭(<code>anyMatch()</code>, <code>allMatch()</code>, <code>noneMatch()</code>)</p>
<ul>
<li><p>특정 조건을 만족하는 요소가 있는치 체크</p>
</li>
<li><p><code>boolean</code>반환</p>
<pre><code class="language-java">//길이가 4이상인 요소가 하나라도 있는지
boolean any = members.stream().anyMatch(name -&gt; name.length() &gt;= 4);
System.out.println(any); // true

//모든 요소가 &quot;e&quot;를 포함하는지
boolean all = members.stream().allMatch(name -&gt; name.contains(&quot;e&quot;));
System.out.println(all); // false

//q로 끝나는 요소가 하나도 없는지
boolean noneMatched = members.stream().noneMatch(name -&gt; name.endsWith(&quot;q&quot;));
System.out.println(noneMatched); // true</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="병렬-스트림parallel-stream">병렬 스트림(Parallel Stream)</h2>
<p>여러 개의 <strong>스레드를 사용하여 데이터를 병렬로 처리</strong>가 가능!</p>
<p><code>parallelStream()</code> 이나 <code>parallel()</code>을 이용해 병렬 스트림 사용</p>
<p><strong>순서가 보장되지 않는다.</strong> (순서가 보장되어야 한다면 주의해야 한다.)</p>
<p>⇒ 즉, 기본 스트림은 순차적으로 하나씩 데이터를 처리하지만 <strong>병렬스트림은 여러 스레드에서 동시에 데이터를 처리</strong>한다!</p>
<pre><code class="language-java">List&lt;String&gt; list = List.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;);

list.parallelStream()
        .forEach(s -&gt; System.out.println(Thread.currentThread().getName() + &quot; - &quot; + s));

/*
출력
ForkJoinPool.commonPool-worker-3 - A
ForkJoinPool.commonPool-worker-1 - B
main - C
ForkJoinPool.commonPool-worker-2 - E
ForkJoinPool.commonPool-worker-5 - D
*/</code></pre>
<p><code>parallel()</code>은 기존 스트림을 병렬스트림으로 변환해준다.</p>
<pre><code class="language-java">list.stream()  // 순차 스트림
    .parallel()  // 병렬 처리로 변환
    .forEach(s -&gt; System.out.println(Thread.currentThread().getName() + &quot; - &quot; + s));</code></pre>
<p>이번 정리를 통해서 대충 스트림이 어떤 역할을 하는지는 알 것 같다.</p>
<p>하지만 아직 코드로 직접 사용하는데는 서툰거 같아서 틈틈히 일부로 스트림을 사용해봐야겠다.</p>
<p>또한 스트림을 알아보면서(사실 람다 공부했을 때) 메서드 참조(<code>::</code>) 가 근근히 보이는데 이것도 나중에 정리를 해봐야겠다. (멋져보이지 않는가!)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] 람다와 메서드체이닝(feat. FunctionalInterface)]]></title>
            <link>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EB%9E%8C%EB%8B%A4%EC%99%80-%EB%A9%94%EC%84%9C%EB%93%9C%EC%B2%B4%EC%9D%B4%EB%8B%9D</link>
            <guid>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EB%9E%8C%EB%8B%A4%EC%99%80-%EB%A9%94%EC%84%9C%EB%93%9C%EC%B2%B4%EC%9D%B4%EB%8B%9D</guid>
            <pubDate>Mon, 17 Mar 2025 15:04:44 GMT</pubDate>
            <description><![CDATA[<h1 id="람다lambda">람다(Lambda)</h1>
<ul>
<li>람다는 익명 함수라고도 한다.</li>
<li>함수를 간결하게 표현하는 방법</li>
<li>코드의 가독성이 좋아짐</li>
</ul>
<p>한 번만 사용할 메소드를 일일히 정의하면 정의할 메소드가 너무 많아지지 않겠나!</p>
<p>그럴때 사용하기 좋은 것이 람다이다. 본인은 일회용 메소드 비슷하게 이해하고 있다.</p>
<h2 id="익명-클래스-사용">익명 클래스 사용</h2>
<ul>
<li>익명클래스란 이름이 없는 클래스</li>
<li>클래스를 정의함과 동시에 인스턴스를 생성할 때 사용</li>
</ul>
<p>람다가 없으면 있는 선택지는 익명클래스를 사용하는 겁니다.</p>
<pre><code class="language-java">Consumer&lt;String&gt; c = new Consumer&lt;String&gt;() {

    @Override
    public void accept(String s) {
        System.out.println(&quot;Hello, &quot; + s);
    }
};

c.accept(&quot;Lambda!&quot;);</code></pre>
<p>이렇게 일회용 메소드를 익명클래스를 이용해서도 사용할순 있다만, 너무 복잡하고 무엇보다 쓰기가 너무 귀찮습니다.</p>
<p>단, 익명클래스는 여러개의 메서드를 가질 수 있다는 장점?이 있습니다.(이럴 거면 그냥 클래스를 하나 만드는게..)</p>
<p>이런 귀찮음을 해결 할 수 있는게 람다입니다</p>
<h2 id="람다의-구성">람다의 구성</h2>
<p>일단 람다가 어떻게 생겨먹었는지부터 보자</p>
<pre><code class="language-java">
(매개변수,매개변수) → {
            표현식
        }

//매개변수 하나일때 () 생략가능        
매개변수 -&gt; {
    표현식
}

//한 줄이면 {}도 생략가능, return쓰면 안됌(암시적 반환)
매개변수 -&gt; 표현식</code></pre>
<p>람다는 <code>→</code> 연산자를 사용한다.</p>
<p><code>→</code> 좌측에 있는 매개변수가 <code>→</code> 우축에 있는 명령문에 들어가 연산수행 가능</p>
<p>이 람다는 메소드와 동일하게 <strong>반환값을 변수에 할당</strong>할 수 있다.</p>
<p>추가로 람다식이 한줄일때 반환값이 있더라도 <code>return</code>을 명시하면 문법 오류 발생함.</p>
<p>(여러줄 일땐 <code>{}</code>과 함께 <code>return</code> 필수)</p>
<p><code>Function&lt;Integer, Integer&gt; square = x -&gt; return x * x;</code>  - ❌  </p>
<p><code>Function&lt;Integer, Integer&gt; square = x -&gt; x * x;</code> - ⭕</p>
<h2 id="함수형-인터페이스functional-interface">함수형 인터페이스(Functional Interface)</h2>
<ul>
<li><p>함수형 인터페이스는 <strong>하나의 추상 메서드</strong>만을 가지는 인터페이스</p>
</li>
<li><p>람다는 함수형 인터페이스의 추상 메서드를 구현하면서 사용합니다.</p>
<p>  ⇒ <strong>람다 표현식으로 간단하게 이 하나의 추상 메서드를 구현!!</strong></p>
</li>
<li><p>함수형 인터페이스에는 <code>Consumer</code>, <code>Supplier</code>, <code>Function</code>, <code>Runnable</code> 등이 있다.</p>
<ul>
<li>물론 우리가 직접 함수형 인터페이스를 만들 수도 있다.(<code>@FunctionalInterface</code>)</li>
</ul>
</li>
</ul>
<h3 id="consumert">Consumer<T></h3>
<pre><code class="language-java">@FunctionalInterface
public interface Consumer&lt;T&gt; {

    void accept(T t);</code></pre>
<ul>
<li>함수형 인터페이스 <code>Consumer</code> 는 <code>accept()</code>라는 추상 메소드를 가진다.</li>
<li><code>accept()</code> 는 입력값만 있고 반환을 하지 않는다. <strong>즉, 소비만 한다.</strong></li>
</ul>
<pre><code class="language-java">Consumer&lt;String&gt; AnonymousConsumer  = new Consumer&lt;String&gt;() {
    @Override
    public void accept(String s) {
        System.out.println(&quot;Hello, &quot; + s);
    }
};
AnonymousConsumer.accept(&quot;익명클라스~!&quot;);

Consumer&lt;String&gt; MultiLambda = (s) -&gt; {
    System.out.println(&quot;Hello, &quot; + s);
};

MultiLambda.accept(&quot;여러줄 람다~&quot;);

Consumer&lt;String&gt; SingleLambda = s -&gt; System.out.println(&quot;Hello, &quot; + s);
SingleLambda.accept(&quot;한 줄 람다~&quot;);

/*
출력
Hello, 익명클라스~!
Hello, 여러줄 람다~
Hello, 한 줄 람다~
*/</code></pre>
<h3 id="suppliert">Supplier<T></h3>
<pre><code class="language-java">public interface Supplier&lt;T&gt; {

    T get();
}</code></pre>
<ul>
<li><code>Supplier</code>는 <code>get()</code>이라는 추상 메소드를 가짐</li>
<li><code>get()</code>은 매개변수를 받지 않고 반환만 한다. <strong>즉, 공급만 한다.</strong></li>
</ul>
<pre><code class="language-java">Supplier&lt;String&gt; justStringOut = () -&gt; &quot;Supplying..&quot;;
String str = justStringOut.get();
System.out.println(str);

Supplier&lt;Integer&gt; randomNumberSupplier = () -&gt; (int) (Math.random() * 100);
for (int i = 0; i &lt;= 5; i++){
    System.out.println(randomNumberSupplier.get());
}
/*
출력
Supplying..
31
84
64
16
50
43
*/</code></pre>
<h3 id="functiont-r">Function&lt;T, R&gt;</h3>
<pre><code class="language-java">@FunctionalInterface
public interface Function&lt;T, R&gt; {
    R apply(T t);</code></pre>
<ul>
<li><code>apply()</code> 라는 추상 메서드 가짐</li>
<li>이름처럼 <strong>매개변수를 받아서 적용시키고 반환</strong>함 → &lt;T, R&gt; 모두 지정해야 한다.</li>
</ul>
<pre><code class="language-java">Function&lt;Integer, Integer&gt; square = x -&gt; x * x;
Integer applied = square.apply(5);
System.out.println(applied);

Function&lt;String, String&gt; upperCase = x -&gt; x.toUpperCase();
String Uppercase = upperCase.apply(&quot;lambda&quot;);
System.out.println(Uppercase);

/*
출력
25
LAMBDA
*/</code></pre>
<h3 id="runnable">Runnable</h3>
<pre><code class="language-java">@FunctionalInterface
public interface Runnable {

    void run();</code></pre>
<ul>
<li><p>매개변수도 없고, 반환도 안함</p>
</li>
<li><p>멀티쓰레드를 구현할 수 있게함(<code>Thread</code>클래스가 <code>Runnable</code>구현)</p>
<ul>
<li><p>Thread의 start()를 호출 → 새로운 쓰레드가 생성 → run()이 백그라운드에서 실행</p>
</li>
<li><p>사용법: <code>Thread t = new Thread(runnable); t.start();</code></p>
<p>  ⇒ 멀티코어 CPU에서는 실제로 동시에 병렬 실행! (싱글코어에서는 그런 것처럼 보임)</p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">Runnable runnable = () -&gt; System.out.println(&quot;단지 실행할 뿐..&quot;);
runnable.run();

//출력 : 단지 실행할 뿐..</code></pre>
<pre><code class="language-java">Runnable task = () -&gt; {
    for (int i = 0; i &lt; 5; i++) {
        System.out.println(Thread.currentThread().getName() + &quot;: &quot; + i);
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
};

Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);

thread1.start();
thread2.start();

/*
출력
Thread-0: 0
Thread-1: 0
Thread-0: 1
Thread-1: 1
Thread-0: 2
Thread-1: 2
Thread-0: 3
Thread-1: 3
Thread-0: 4
Thread-1: 4
*/</code></pre>
<p>이처럼 <code>Runnable</code>을 <code>Thread</code>에 전달하여 멀티쓰레드가 실행된다.</p>
<h2 id="메서드-체이닝">메서드 체이닝</h2>
<ul>
<li>객체에서 여러 개의 <strong>메서드를 연속적으로 호출</strong> 하는 것</li>
<li>점(.)을 이용해 연속호출</li>
</ul>
<p>밑에서 대표적인 메서드 체이닝 방법 두개를 소개해 보겠다.</p>
<h3 id="객체-자신을-반환-this-반환"><strong>객체 자신을 반환</strong> (<code>this</code> 반환)</h3>
<pre><code class="language-java">public class MethodChaining {
    public String name;
    public int age;
    public boolean isGay;
    public String gay;

    public MethodChaining setName(String name) {
        this.name = name;
        return this;
    }

    public MethodChaining setAge(int age) {
        this.age = age;
        return this;
    }

    public MethodChaining setGay(boolean isGay) {
        this.isGay = isGay;
        gay = (isGay) ? &quot;게이&quot; : &quot;게이아님&quot;;
        return this;
    }

    public MethodChaining show() {
        System.out.println(&quot;이름: &quot; + name + &quot; 나이: &quot; + age + &quot; 게이?: &quot; + gay);
        return this;
    }
}</code></pre>
<pre><code class="language-java">public class LambdaTest {
    public static void main(String[] args) {

        new MethodChaining()
                .setName(&quot;홍석천&quot;)
                .setAge(54)
                .setGay(true)
                .show();
    }
}
/*
출력
이름: 홍석천 나이: 54 게이?: 게이
*/</code></pre>
<p>이런식으로 같은 객체에서 <code>this</code>를 반환하는 메소드들을 연속해서 호출할 수 있다.</p>
<h3 id="andthen"><code>andThen()</code></h3>
<p><code>Consumer</code>과 <code>Function</code>함수형 인테페이스에는 사실 <code>accept()</code>말고도 하나의 메소드가 더 있다.</p>
<pre><code class="language-java">public interface Consumer&lt;T&gt; {


    void accept(T t);

    default Consumer&lt;T&gt; andThen(Consumer&lt;? super T&gt; after) {
        Objects.requireNonNull(after);
        return (T t) -&gt; { accept(t); after.accept(t); };</code></pre>
<pre><code class="language-java">@FunctionalInterface
public interface Function&lt;T, R&gt; {

    R apply(T t);

    default &lt;V&gt; Function&lt;V, R&gt; compose(Function&lt;? super V, ? extends T&gt; before) {
        Objects.requireNonNull(before);
        return (V v) -&gt; apply(before.apply(v));
    }

    default &lt;V&gt; Function&lt;T, V&gt; andThen(Function&lt;? super R, ? extends V&gt; after) {
        Objects.requireNonNull(after);
        return (T t) -&gt; after.apply(apply(t));
    }</code></pre>
<p>두 함수형 인터페이스의 andThen을 잘보라!</p>
<p>모두 <strong>자기 자신을 반환</strong>하고 있다!(Function의 compose도 마찬가지다.)</p>
<p>즉, 이 두 함수형 인터페이스는 <code>andThen</code>과 <code>compose</code>(Function)를 통해서 메서드 체이닝이 가능하다.</p>
<pre><code class="language-java">//첫번째 방법
Consumer&lt;String&gt; consumer1 = (s) -&gt; System.out.println(&quot;첫 번째 람다!: &quot; + s);
Consumer&lt;String&gt; consumer2 = consumer1.andThen((s) -&gt; System.out.println(&quot;두 번째 람다!: &quot; + s));

consumer2.accept(&quot;메서드 체이닝!&quot;);

//두번때 방법
Consumer&lt;String&gt; consumer1 = (s) -&gt; System.out.println(&quot;첫 번째 람다!: &quot; + s);
Consumer&lt;String&gt; consumer2 = (s) -&gt; System.out.println(&quot;두 번째 람다!: &quot; + s);

Consumer&lt;String&gt; chaining = consumer1.andThen(consumer2);
chaining.accept(&quot;메서드 체이닝!&quot;);
/*출력
첫 번째 람다!: 메서드 체이닝!
두 번째 람다!: 메서드 체이닝!
*/</code></pre>
<pre><code class="language-java">Function&lt;Integer, Integer&gt; function1 = i -&gt; i * 2;
Function&lt;Integer, Integer&gt; function2 = i -&gt; i + 10;

Function&lt;Integer, Integer&gt; answer = function1.andThen(function2);
Function&lt;Integer, Integer&gt; composed = function1.compose(function2);

System.out.println(answer.apply(3));
System.out.println(composed.apply(3));
/*
출력
16
26
*/</code></pre>
<p>위와 같이 <code>Function</code>의 <code>compose()</code>는 <code>andThen()</code>과 <strong>실행순서가 반대</strong>이다.</p>
<p>⇒ <code>andThen</code> : 왼쪽실행 후 오른쪽실행</p>
<p>⇒ <code>compose</code>: 오른쪽 실행 후 왼쪽 실행</p>
<h2 id="번외-나의-착각">번외: 나의 착각</h2>
<p>사실 위에 <code>this</code>를 반환하는 메소드들이 연속으로 호출이 가능한건 <strong>점(.)왼쪽의 메소드의 반환타입과 점(.) 오른쪽의 메소드의 매개변수 타입이 같기 때문에</strong> 메소드 체이닝이 가능하다고 생각해서</p>
<p>같은 원리로 <code>this</code>를 반환하지 않아도 반환타입가 매개변수 타입만 만족한다면 메소드 체이닝은 가능한게 아닌가..? 라고 생각했는데 뭐.. 되긴되는데 이건 메서드체이닝이 아니었다. </p>
<p>⇒ 걍 메서드를 중첩 호출하는 거임</p>
<pre><code class="language-java">public class Main {
    public static String methodA() {
        return &quot;hello&quot;;
    }

    public static int methodB(String input) {
        return input.length();
    }

    public static void main(String[] args) {
        int result = methodB(methodA()); //이건 그냥 메서드 중첩 호출이다...
        System.out.println(result);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[예외/오류와 제네릭/와일드카드]]></title>
            <link>https://velog.io/@tech_bae/Temp-Title-m96zolzv</link>
            <guid>https://velog.io/@tech_bae/Temp-Title-m96zolzv</guid>
            <pubDate>Sun, 16 Mar 2025 14:38:59 GMT</pubDate>
            <description><![CDATA[<h1 id="예외exception와-오류error">예외(Exception)와 오류(Error)</h1>
<h2 id="예외exception">예외(Exception)</h2>
<ul>
<li>프로그램 실행 중 발생하는 비정상적인 상황</li>
<li>예외도 객체다..</li>
</ul>
<h3 id="checked-exception">Checked Exception</h3>
<ul>
<li>컴파일 타임에서 검사되는 예외 → 예외 처리 강제 → 무시하면? 컴파일 오류</li>
</ul>
<p>⇒ 고로 <code>try-catch</code> 나 <code>throws</code> / <code>throw</code> 를 사용해서 예외처리 해야함</p>
<ul>
<li><code>Exception</code> 을 상속</li>
</ul>
<h3 id="unchecked-exception">Unchecked Exception</h3>
<ul>
<li>컴파일러가 체크하지 않는 예외 → 예외처리 하지 않아도 컴파일됨 → 실행중에 예외가 발생 할 수 있다.</li>
<li><code>NullPointerException</code> 과 같은 예외 자주 발생 → null 여부 확인해야 한다. 등</li>
<li><code>RunTimeException</code> 을 상속(RunTimeException은 Exception을 상속)</li>
</ul>
<h3 id="예외처리">예외처리</h3>
<h3 id="throws">throws</h3>
<ul>
<li>메서드 선언부에 작성하여 메서드에서 예외가 발생할 수 있음을 알림</li>
<li>메소드를 호출하는 시점에서 예외처리가 되야함(예외를 던져서 호출한 놈한테 책임전가 ㄷㄷ;)</li>
</ul>
<h3 id="throw">throw</h3>
<ul>
<li>예외를 강제로 발생시킴</li>
</ul>
<h3 id="try-catch-fianlly">try-catch-fianlly</h3>
<ul>
<li><code>try</code> : 예외가 발생할 수 있는 코드 작성</li>
<li><code>catch</code>   : 예외가 발생할때의 대처 작성</li>
<li><code>finally</code> : 예외 발생여부 상관없이 반드시 실행(생략가능)</li>
</ul>
<pre><code class="language-java">public class ExceptionTest {
    public void ex() throws RuntimeException {
        System.out.println(&quot;예외 날라간드아~ㅋㅋ&quot;);
        throw new RuntimeException(&quot;쿠쿠루 삥뽕&quot;);
    }
}</code></pre>
<pre><code class="language-java">public class ExMain {
    public static void main(String[] args) {
        try {
            ExceptionTest ex = new ExceptionTest();
            ex.ex();
        }catch (RuntimeException e) {
            System.out.println(&quot;에러 발생!!&quot;+e);
        }finally {
            System.out.println(&quot;생략 안해주셔서 감사합니다..&quot;);
        }
    }
}

/*
출력
예외 날라간드아~ㅋㅋ
에러 발생!!java.lang.RuntimeException: 쿠쿠루 삥뽕
생략 안해주셔서 감사합니다..
*/
</code></pre>
<h2 id="사용자-정의-예외">사용자 정의 예외</h2>
<ul>
<li><p>새로운 예외 클래스를 정의하여 사용가능</p>
<p>  ⇒ <code>Exception</code> 이나 <code>RunTimeException</code>클래스를 상속받아 생성</p>
</li>
<li><p>예외의 원인을 좀 더 정확히 특정 할 수 있음</p>
</li>
</ul>
<pre><code class="language-java">catch(GotWeakerException e) {
    throw new FailLiftException(&quot;수행을 실패했습니다.&quot;,e); </code></pre>
<pre><code class="language-java">public class FailLiftException extends Exception {
    public FailLiftException (String message, Throwable cause) {
        super(message, cause);
    }
}</code></pre>
<p><code>cause</code>에 이 예외가 발생한 원인 예외를 넣어주면 연쇄적인 예외처리가 가능하다.</p>
<h2 id="오류error">오류(Error)</h2>
<ul>
<li><p>시스템 수준의 심각한 문제 → 프로그램에서 처리 못함</p>
</li>
<li><p><code>Throwable</code>을 상속 받기에 <code>try-catch</code> 사용가능하지만 치명적인 문제이기에 조취를 취해야함</p>
<p>  ⇒ 즉, 잡지마라</p>
</li>
<li><p>오류도 객체..</p>
</li>
</ul>
<h1 id="제네릭generic">제네릭(Generic)</h1>
<ul>
<li>자바에서 제네릭이란 참조 자료형의 타입을 일반화하는 것<ul>
<li>코드의 재사용성 높임</li>
<li>타입 안정성 보장</li>
</ul>
</li>
</ul>
<p>⇒ <strong>컴파일 시점에 타입이 결정되는 가변적인 타입 매개변수</strong></p>
<p>한마디로 어떤 참조 자료형(클래스 타입)이 들어올지 모르기에 일단 뭐든 들어올 수 있게 열어 놓고 있다가, 실제 객체가 특정 타입으로 생성될 때 그 타입으로 결정되는 것 <del>(양자역학인가..?)</del></p>
<h2 id="제네릭-기호-네이밍">제네릭 기호 네이밍</h2>
<p>기본적으론 제네릭의 이름에 제약사항은 따로 없다. 사실 변수 이름 지을 때처럼 마음대로 정해도 된다. 하지만 다른 사람을 배려하는 마음으로 통상적인 네이밍을 따르는 것도 바람직 할 것 같다. (보통 대문자 알파벳 문자를 사용한다.)</p>
<ul>
<li><strong><code>&lt;T&gt;</code></strong> : Type</li>
<li><strong><code>&lt;E&gt;</code></strong> : Element</li>
<li><strong><code>&lt;K&gt;</code></strong> : Key (Map)</li>
<li><strong><code>&lt;V&gt;</code></strong> : Value (Map)</li>
<li><strong><code>&lt;N&gt;</code></strong> : Number</li>
<li><strong><code>&lt;R&gt;</code></strong> : 리턴 타입(메서드에서 자주 사용)</li>
<li><strong><code>&lt;U&gt;</code> , <code>&lt;S&gt;</code></strong> : 제네릭 타입 여러개 일대 보조적 으로 사용</li>
</ul>
<h2 id="제네릭-사용">제네릭 사용</h2>
<pre><code class="language-java">public class GenericBox&lt;T&gt; {
    public T stuff;

    public GenericBox(T stuff) {
        this.stuff = stuff;
    }

    public T whatHave() {
        System.out.println(&quot;I have &quot; + stuff);
        return stuff;
    }
}</code></pre>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        GenericBox&lt;String&gt; strBox = new GenericBox&lt;String&gt;(&quot;Tools&quot;);
        GenericBox&lt;Integer&gt; intBox = new GenericBox&lt;&gt;(1234);
        GenericBox&lt;Boolean&gt; boolBox = new GenericBox&lt;&gt;(true);

        strBox.whatHave();
        intBox.whatHave();
        boolBox.whatHave();
    }
}

/*
출력
I have Tools
I have 1234
I have true
*/</code></pre>
<p>이런식으로 <code>GenericBox</code>의 필드타입을 제네릭을 사용하여 객체가 생성될때 필드의 타입이 결정된다.</p>
<p>참고로 제네릭 클래스의 인스턴스를 생성할 때</p>
<p><code>Box&lt;String&gt; myBox = new Box&lt;String&gt;();</code></p>
<p>이건 당연히 되겠지만</p>
<p><code>Box&lt;&gt; myBox = new Box&lt;String&gt;();</code>
이러면 오류발생 가능(컴파일러가 좌변의&lt;&gt;를 보고 우변의 &lt;&gt;을 추론하기 때문에)</p>
<p><code>Box&lt;String&gt; myBox = new Box&lt;&gt;();</code>
고로 이건 오류가 안남.</p>
<h2 id="제네릭과-static">제네릭과 Static</h2>
<h3 id="static필드에서의-제네릭">Static필드에서의 제네릭</h3>
<p>그렇다면 필드값이 Static이어도 제네릭이 사용가능 할까?</p>
<p><strong>답은 안된다.</strong></p>
<p>이유는 크게 두 가지다.</p>
<ul>
<li><p><code>static</code>변수는 객체 생성 없이 접근이 가능</p>
<ul>
<li><p><code>static</code> 변수는 <strong>클래스 로드 시점</strong>에 생성 된다. 제네릭은 <strong>객체가 생성될 때 결정</strong>된다 → 충돌발생</p>
<p>  ⇒ 한마디로 <code>static</code>은 모든 인스턴스가 공유하지만, 제네릭 타입은 각 객체마다 다를 수 있으므로!!</p>
</li>
</ul>
</li>
<li><p>타입소거</p>
<ul>
<li><p>제네릭은 컴파일 시에만 유효, 런타임에서 제네릭 타입 제거 → <code>Object</code> 또는 특정 상위 클래스로 변환</p>
<p>  ⇒ 클래스 수준에서 <code>static</code> 변수를 정의할 방법이 없다. (런타임에는 제네릭이 존재 X, )</p>
<p>  즉,</p>
<p>  <strong>static 변수는 클래스 로드 시점에 메로리에 로딩되어 런타임까지 유지되어야한다.</strong></p>
<p>  근데,</p>
<p>  제네릭은 타입 소거로 인해서 <strong>런타임에서 타입 정보가 사라짐(Object)</strong></p>
</li>
</ul>
</li>
</ul>
<h3 id="static메소드에서의-제네릭">Static메소드에서의 제네릭</h3>
<p>근데 또 static 메소드는 된다. 뭘까 이 일관되지 않은 규칙은..</p>
<p>이유를 알아보자.</p>
<p>한마디로 <strong>Static 메서드의 제네릭 타입은 메서드가 호출될 때 결정</strong>되기 때문이다.</p>
<p>static변수와 마찬가지로 <strong>static메소드의 정의</strong>가 클래스 로드 시점에 메서드 영역에 저장된다. </p>
<p>하지만 static변수와 다르게 static메소드는 이 시점에서 제네릭의 타입이 결정 되지 않고 (static변수는 저장될때 타입이 결정되어야 하지만, static메소드는 호출 될때 결정해도 된다.)</p>
<p>⇒ <strong>메소드가 호출될 때마다 다른 제네릭 타입을 사용</strong>할 수 있기때문에 제네릭 사용이 가능하다.</p>
<pre><code class="language-java">public class GenericBox&lt;T&gt; {

    public static &lt;T&gt; void whatHave(T t) {
        System.out.println(&quot;I have &quot; + t);
    }
}</code></pre>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        GenericBox.whatHave(&quot;Hello&quot;);
        GenericBox.whatHave(1234);
        GenericBox.whatHave(false);
    }
}

/*
출력
I have Hello
I have 1234
I have false
*/</code></pre>
<h2 id="제네릭-타입-제한">제네릭 타입 제한</h2>
<ul>
<li>제네릭에서 특정 타입만을 허용하도록 제한하는 기능</li>
<li><strong>상한제한</strong>과 <strong>하한제한</strong>으로 나눌 수 있다.</li>
</ul>
<p>앞으로의 설명을 위해 아래와 같은 상속(구현) 관계가 있다고 생각 해보자</p>
<p><strong>interface A
class B implements A
class C extends B
class D</strong></p>
<h3 id="상한제한">상한제한</h3>
<p><code>extends</code> 키워드를 사용하여 <strong>특정 클래스나 인터페이스를 상속하거나 구현한 타입만 허용</strong></p>
<p>예를 들어 </p>
<pre><code class="language-java">class UpperBoundedClass &lt;T extends B&gt;</code></pre>
<p>이렇게 제한한다면 <code>T</code>는  <code>B</code>를 상속하는 타입만이 올 수 있습니다.(B포함)</p>
<p>⇒ B, C가 올 수 있음.</p>
<h3 id="하한제한">하한제한</h3>
<p><code>super</code> 키워드를 사용하여 특정 클래스의 <strong>상위 타입</strong>만 가능</p>
<pre><code class="language-java">class LowerBoundedClass &lt;T super B&gt;</code></pre>
<p>⇒ B의 상위 타입인 B, A타입이 가능(B도 포함)</p>
<h3 id="다중제약다중-경계">다중제약(다중 경계)</h3>
<p><code>&amp;</code>를 사용하여 두개 이상의 제약을 걸 수 있다.(<code>|</code> 은 안됨!!)</p>
<pre><code class="language-java">class MultiBoundedClass &lt;T extends B &amp; A&gt;</code></pre>
<p><code>T</code> 에는 B를 상속함과 동시에 A를 구현한 타입만이 올 수 있음.</p>
<p>⇒ 클래스는 하나만 가능하다(자바에선 다중 상속이 안되기에..)</p>
<h2 id="와일드카드--">와일드카드 : ?</h2>
<ul>
<li>와일드카드 <code>?</code> 는 제네릭 타입을 사용할 때, 어떤 타입이 올지 모를 겅우 유연하게 처리하는 기능</li>
<li><strong>메서드</strong>에서만 사용 가능</li>
</ul>
<pre><code class="language-java">public class WildCard {
    public static void printList(List&lt;?&gt; list){
        for (Object o : list) {
            System.out.println(o);
        }
    }
}</code></pre>
<pre><code class="language-java">public class WildCardMain {
    public static void main(String[] args) {
        List&lt;String&gt; strList = new ArrayList&lt;&gt;();
        strList.add(&quot;Hello&quot;);
        strList.add(&quot;World&quot;);

        List&lt;Integer&gt; intList = new ArrayList&lt;&gt;();
        intList.add(1);
        intList.add(2);

        WildCard.printList(strList);
        WildCard.printList(intList);
    }
}

/*
출력
Hello
World
1
2
*/</code></pre>
<p>위처럼 <code>printList</code>의 매개변수를 <code>List&lt;?&gt;</code> 는 List의 타입이 무엇이든 받을 수 있다.</p>
<h3 id="와일드카드-메서드와-제네릭-메서드의-차이">와일드카드 메서드와 제네릭 메서드의 차이</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>제네릭</th>
<th>와일드카</th>
</tr>
</thead>
<tbody><tr>
<td>타입 결정</td>
<td>메서드 호출 시 타입을 지정해야 함</td>
<td>어떤 타입이든 받을 수 있음</td>
</tr>
<tr>
<td>사용 목적</td>
<td>특정 타입을 강제</td>
<td>여러 타입을 유연하게 처리</td>
</tr>
<tr>
<td>타입 안정성</td>
<td>타입이 정해져 있어서 안전</td>
<td>내부적으로 <code>Object</code>로 처리</td>
</tr>
</tbody></table>
<h3 id="와일드카드-타입제한">와일드카드 타입제한</h3>
<p>제네릭과 유사하게 와일드카드의 범위를 제한할 수 있다.</p>
<ol>
<li><p><code>?</code> (Unbounded Wildcard)</p>
<ul>
<li>어떤 타입의 제네릭이든 허용(모두 허용)</li>
</ul>
</li>
<li><p>상한제한(Upper Bounded Wildcard)</p>
<ul>
<li><p><code>extends</code> 키워드를 사용하여 제네릭과 동일하게 상속하는 하위 타입만 허용</p>
</li>
<li><p>ex) <code>&lt;? extends Number&gt;</code> : Number와 그 하위 타입만 허용(Number를 상속하는)</p>
</li>
<li><p>읽기만 가능, <strong>쓰기 불가</strong></p>
<p>  ⇒ 위 예에서 Number와 그 하위 타입중 하나이지만 정확히 어떤 타입인지 모르기에 쓰기가 불가능.(Integer, Double, Float 뭘 넣어야할지 모름..)</p>
</li>
</ul>
</li>
<li><p>하한제한(Lower Bounded Wildcard)</p>
<ul>
<li><p><code>super</code> 키워드를 사용하여 상위 타입만을 허용</p>
</li>
<li><p>ex) <code>&lt;? super Integer&gt;</code> : Integer와 그 상위 타입만 허용</p>
</li>
<li><p><strong>쓰기가 가능</strong></p>
<p>  ⇒ 위 예에서 Integer 또는 그 상위 타입(Number, Object) 중 하나지만, <strong>최소한 Integer 이상이 보장</strong>되기 때문에!</p>
</li>
</ul>
</li>
</ol>
<h1 id="번외-optional">번외: Optional</h1>
<p><code>Optional</code> 객체를 사용하면 <code>null</code>처리를 안전하고 우아하게 가능(정적 팩토리 메서드 사용하여 객체 생성)</p>
<ul>
<li><strong>정적 팩토리 메서드</strong> : <code>new</code> 키워드로 객체를 만드는 대신 <code>static</code>메서드로 객체를 생성하게 하는 메소드</li>
<li><code>null</code>을 감싸는 객체 제공 → <code>null</code>을 직접 다루지 않고 안전한 연산 가능!</li>
</ul>
<h2 id="optional-객체-생성-방법">Optional 객체 생성 방법</h2>
<ol>
<li><code>Optional&lt;String&gt; opt1 = Optional.of(&quot;Hello&quot;);</code> : 반드시 값이 있어야 함</li>
<li><code>Optional&lt;String&gt; opt2 = Optional.ofNullable(null);</code> : 값이 <strong>null</strong>일 수도 있다.</li>
<li><code>Optional&lt;String&gt; op3 = Optional.emtpy();</code> : 빈 객체 생성</li>
</ol>
<h2 id="optional-구현-실습">Optional 구현 실습</h2>
<pre><code class="language-java">import java.util.Objects;

public class Optional&lt;T&gt; {

    private final T data;

    private Optional(T data) { //private 생성자이므로 외부에서 접근 불가
        this.data = data;
    }

    public static &lt;T&gt; Optional&lt;T&gt; empty() { //빈 객체 반환
        return new Optional&lt;&gt;(null);
    }

    //값이 있는 객체를 반환 (requireNonNull : Null이면 예외 발생)
    public static &lt;T&gt; Optional&lt;T&gt; of(T value) { 
        return new Optional&lt;&gt;(Objects.requireNonNull(value));
    }

        // null이면 빈 객체 반환, 아니면 그 값 담긴 객체 반환
    public static &lt;T&gt; Optional&lt;T&gt; ofNullable(T value) {
        return value == null
                ? new Optional&lt;&gt;(null)
                : new Optional&lt;&gt;(value);
    }

        // 객체에 담긴 값 확인 -&gt; null이면 예외 발생(커스텀 가능!)
    public T get() {
        if ( data == null ){
            throw new NoSuchElementException(&quot;No value present&quot;);
        }
        return data;
    }
        // 값 있음?
    public boolean isPresent() {
        return data != null;
    }
        // 빔?
    public boolean isEmpty() {
        return data == null;
    }

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] Object클래스, Comparable / Comparator, Annotation, Reflection]]></title>
            <link>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-Object%ED%81%B4%EB%9E%98%EC%8A%A4-Comparable-Comparator-Annotation-Reflection</link>
            <guid>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-Object%ED%81%B4%EB%9E%98%EC%8A%A4-Comparable-Comparator-Annotation-Reflection</guid>
            <pubDate>Sun, 16 Mar 2025 14:32:21 GMT</pubDate>
            <description><![CDATA[<h2 id="object-클래스">Object 클래스</h2>
<p>모든 클래스의 최상위 부모 클래스</p>
<p>⇒ 모든 클래스는 <code>Object</code>클래스를 자동으로 상속</p>
<pre><code class="language-java">class Test /*extends Object 생략 */ {}</code></pre>
<p>Object클래스에는 모든 객체가 사용할 수 있는 <strong>기본 메서드</strong> 포함</p>
<ul>
<li><p><code>equals()</code></p>
<ul>
<li><p>기본은 <code>==</code> 연산자와 같이 <strong>메모리 주소를 비교</strong></p>
<p>  하지만 override를 통해 논리적비교 구현 가능(값 비교)</p>
</li>
</ul>
</li>
<li><p><code>hashCode()</code></p>
<ul>
<li>객체를 해시기반 자료구조에 저장할때 사용</li>
<li>같은 객체라면 동일한 <code>hashCode</code>값을 반환해야함 ⇒ <code>equals()</code> 오버라이딩할때 함꼐 오버라이딩해야 하는 이유</li>
</ul>
</li>
<li><p><code>toString()</code></p>
<ul>
<li>객체의 정보를 문자열로 표현</li>
<li>기본적으로 <code>클래스명@해시코드</code> 형태로 출력<ul>
<li>오버라이딩을 통해 커스텀가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="equals-오버라이딩"><code>equals()</code> 오버라이딩</h3>
<ul>
<li>동등성 : 객체가 가지고 있는 값을 비교(논리적 비교)</li>
<li>동일성 : 객체의 메모리 주소값을 비교(==, 물리적 비교)</li>
</ul>
<p><code>equals()</code>는 기본적으로 <code>==</code> 와 동일하게 객체의 실제 메모리 주소값을 비교</p>
<p>⇒ 같은 데이터값을 가진 객체라도 다르다고 판단</p>
<p>객체간의 동등성을 비교하기 위해선 <code>equals()</code>를 오버라이딩하여 사용해야한다.</p>
<pre><code class="language-java">public class Person {
    public String name;
    public int age;

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

    @Override
    public boolean equals(Object o) {
            if (this == o) return true; // 같은 객체인지 확인(두 객체의 메모리 주소가 완전히 같은 객체)
        if (o == null || getClass() != o.getClass()) return false;//null이면 false, getClass를 통해 클래스 타입확인
        Person person = (Person) o; //o가 Person타입임이 확인 -&gt; Person타입으로 다운캐스팅
        return age == person.age &amp;&amp; Objects.equals(name, person.name);
        }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}</code></pre>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        Person person1 = new Person(&quot;John Doe&quot;, 27);
        Person person2 = new Person(&quot;John Doe&quot;, 27);

        boolean equals = person1.equals(person2);
        System.out.println(equals);
    }
}

출력
Override 전 : false
Override 후 : true</code></pre>
<p><code>equals()</code>를 오버라이딩하는 과정에서 마지막에 실질적인 비교를 할때</p>
<p><code>a.equals(b)</code> 를 사용하면 <code>a</code>가 <code>null</code> 인 경우 <code>NullPointerException</code> 발생 가능</p>
<p>⇒ <code>Objects.equals(a, b)</code> 는 <code>null</code>를 자동으로 처리해 안전하게 비교가능</p>
<h3 id="왜-hashcode를-함께-오버라이딩-해야하는가">왜 hashCode()를 함께 오버라이딩 해야하는가</h3>
<ul>
<li><code>hashCode()</code>는 객체를 해시기반 컬렉션(HashMap, HashSet …)에 저장하거나 비교할 때 사용</li>
<li><code>equals()</code>만 오버라이딩하면 <strong>논리적으로 같은 객체가 다른 해시코드</strong>를 가질 수 있음.</li>
</ul>
<p>⇒ 결과적으로 논리적으로 같은 객체(모든 필드의 값이 같은 객체)를 컬렉션에 <strong>중복 저장될 수 있음</strong></p>
<h2 id="문자열-상수">문자열 상수</h2>
<p>사실 같은 값을 가지고 있는<code>String</code>타입들을 비교하면 <code>==</code> 연산자를 통해서도 <code>true</code> 가 반환된다.</p>
<p>그렇다면  <code>equals()</code> 를 오버라이딩해야하는 이유가 없지 않은가!</p>
<p>자바에서는 문자열(<code>String</code>)을 효율적으로 관리하기 위해서 <strong>문자열 상수 풀</strong>이라는 메모리 영역을 사용한다.</p>
<p>문자열 상수풀은 Heap영역에 일부로, 동일한 문자열 리터럴을 재사용하여 메모리 낭비를 방지한다.</p>
<p>⇒ 즉 같은 값을 가진 <code>String</code> 객체들은 그 하나의 객체만을 재사용하여 참조한다.</p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        String str1 = &quot;Hello&quot;;
        String str2 = &quot;Hello&quot;;

        boolean areTheySame = str1==str2;

        System.out.println(areTheySame); //ture
    }
}</code></pre>
<p>하지만 <code>new</code> 키워드로 String 객체를 생성하면 <strong>Heap 영역에 새로운 객체</strong>가 생성된다.</p>
<p>⇒ 다른 객체를 참조하게 되어 <code>==</code> 연산자로 비교하면 <code>false</code> 반환</p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        String str1 = new String(&quot;Hello&quot;);
        String str2 = new String(&quot;Hello&quot;);

        boolean areTheySame = str1==str2;
        boolean theValuesAreSame = str1.equals(str2);
        System.out.println(areTheySame); //false
        System.out.println(theValuesAreSame); //true
    }
}</code></pre>
<p>따라서 이럴 땐 <code>equals()</code>를 사용하여 객체의 값을 비교해야한다.</p>
<h2 id="comparable--comparator">Comparable / Comparator</h2>
<h3 id="comparable">Comparable</h3>
<ul>
<li>클래스 자체에서 정렬 기준을 지정(기본 정렬기준 지정)</li>
<li><code>compareTo()</code> 메소드를 오버라이딩하여 정의</li>
<li><code>Collections.sort()</code> , <code>Arrays.sort()</code>를 사용하면 자동으로 정용</li>
</ul>
<pre><code class="language-java">public class BodyBuilders implements Comparable&lt;BodyBuilders&gt; {
    protected String name;
    protected int age;
    protected int volume;

    public BodyBuilders(String name, int age, int volume) {
        this.name = name;
        this.age = age;
        this.volume = volume;
    }

    @Override
    public int compareTo(BodyBuilders o) {
        return Integer.compare(this.volume, o.volume);
    }

    @Override
    public String toString() {
        return
                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +
                &quot;, age=&quot; + age +
                &quot;, volume=&quot; + volume +
                &#39;}&#39;;
    }
}</code></pre>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        List&lt;BodyBuilders&gt; builders = new ArrayList&lt;&gt;();
        builders.add(new BodyBuilders(&quot;John&quot;, 20, 20));
        builders.add(new BodyBuilders(&quot;Tom&quot;, 23, 28));
        builders.add(new BodyBuilders(&quot;Jack&quot;, 28, 30));
        builders.add(new BodyBuilders(&quot;Jane&quot;, 31, 18));

        System.out.println(builders);
        builders.sort(null); //Collections.sort(builders);
        System.out.println(builders);
    }
}</code></pre>
<pre><code class="language-java">public int compareTo(BodyBuilders o) {
        return Integer.compare(this.volume, o.volume);
    }</code></pre>
<p> 이 부분을 내림차순으로 하고 싶다면</p>
<pre><code class="language-java">public int compareTo(BodyBuilders o) {
        return Integer.compare(o.volume, this.volume);
    }</code></pre>
<p>이렇게 둘의 순서를 바꿔주면 된다.</p>
<p>왜냐하면 </p>
<p><code>Integer.compare()</code> 은</p>
<pre><code class="language-java">public static int compare(int x, int y) {
    return (x &lt; y) ? -1 : ((x == y) ? 0 : 1);
}</code></pre>
<p>이렇게 생겨먹었다.</p>
<ul>
<li>o1이  o2 보다 작으면 <strong>음수</strong>(x와 y의 위치를 유지)  : o1 - o2 = 음수</li>
<li>두 값이 같으면 <strong>0</strong></li>
<li>o1이 o2보다 크면 <strong>양수</strong>(x와 y의 위치를 바꿈) : o1 - o2 = 양수</li>
</ul>
<p>그러므로 비교 순서를 바꾸면 내림차순으로 정렬 된다.</p>
<p>이는 <code>Comparator</code>의 <code>compare()</code>에도 동일하게 적용된다.</p>
<h3 id="comparator">Comparator</h3>
<ul>
<li>기본 정렬과 다른 정렬기준을 정립</li>
<li><code>compare()</code> 메소드를 오버라이딩하여 정의</li>
<li>클래스 자체의 변경없이 여러 정렬 기준을 만들 수 있음(클래스 외부에서 구현)</li>
</ul>
<pre><code class="language-java">public class BuilderComparator implements Comparator&lt;BodyBuilders&gt; {
    @Override
    public int compare(BodyBuilders o1, BodyBuilders o2) {
        return Integer.compare(o1.age,o2.age);
    }
}</code></pre>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {
        List&lt;BodyBuilders&gt; builders = new ArrayList&lt;&gt;();
        builders.add(new BodyBuilders(&quot;John&quot;, 45, 20));
        builders.add(new BodyBuilders(&quot;Tom&quot;, 23, 28));
        builders.add(new BodyBuilders(&quot;Jack&quot;, 28, 30));
        builders.add(new BodyBuilders(&quot;Jane&quot;, 31, 18));

        System.out.println(builders);
        builders.sort(new BuilderComparator()); //Collections.sort(builders, new BuilderComparator);
        System.out.println(builders);
    }
}</code></pre>
<h3 id="다중정렬">다중정렬</h3>
<pre><code class="language-java">builders.sort((o1, o2) -&gt; {
            if(o1.age == o2.age) {
                return Integer.compare(o2.volume, o1.volume);
            }
            return Integer.compare(o1.age, o2.age);
        });</code></pre>
<p>이 방법이 지금의 나에겐 가장 현실적인 방법 같다.</p>
<p>밑의 두 방법은 아직 나에겐 무리다.. 미래를 기약하겠다.</p>
<pre><code class="language-java">userGuild.sort(
        Comparator.comparing(
                (Character c) -&gt; c.getGold(), Comparator.reverseOrder()
        ).thenComparing(
                (Character c) -&gt; c.getLevel()
        )
);

userGuild.sort(
        Comparator.comparing(
                Character::getGold, Comparator.reverseOrder()
        ).thenComparing(
                Character::getLevel
        )
);</code></pre>
<h2 id="annotation">Annotation</h2>
<ul>
<li>컴파일러가 읽을 수 있는 주석이라고 말할 수 있음</li>
<li>자바파일에서 클래스파일로 바뀌어도(컴파일이 끝나도) 유지된다.</li>
<li>일반주석은 사람에게 코드를 설명하는 용도와 비슷하게 Annotation은 프로그램(컴파일러, 런타임)에게 코드를 설명하는 역할을 수행한다.</li>
<li>Annotation에 값을 넣어 줄 수 있다.</li>
</ul>
<h3 id="표준내장-annotation">표준(내장) Annotation</h3>
<p>자바가 기본적으로 제공하는 어노테이션이다.</p>
<ol>
<li><p><strong>@Override</strong></p>
<p> 오버라이딩을 올바르게 했는지 컴파일러가 체크하게 한다.</p>
</li>
<li><p><strong>@Deprected</strong></p>
<p> 앞으로 사용하지 않는 것을 권장함.</p>
<pre><code class="language-java"> @Deprecated
 public class TrashClass {

     @Deprecated
     public void trash() {
         System.out.println(&quot;Trash&quot;);
     }
 }</code></pre>
<p> <code>@Deprecated</code>를 붙은 클래스나 메소드를 사용하려고 하면 밑줄따위가 그어지며 사용하지 않을 것을 권장한다.</p>
</li>
<li><p><strong>@FunctionalInterface</strong></p>
<p> 함수형 인터페이스가 <strong>하나의 추상메서드만 가지고 있는지</strong>를 확인한다.</p>
<p> 이는 람다을 사용하기 위해서 필요한 개념이다.</p>
</li>
</ol>
<ol>
<li><p><strong>@SuppressWarnings(무시할 경고)</strong></p>
<p> 컴파일러의 경고메세지를 무시한다.</p>
<p> <code>&quot;unchecked&quot;</code>, <code>“deprected”</code> , <code>&quot;all&quot;</code> 등이 올 수 있다.</p>
</li>
</ol>
<h3 id="메타-annotation">메타 Annotation</h3>
<p>어노테이션을 위한 어노테이션</p>
<ol>
<li><p><strong>@Target</strong></p>
<p> 어노테이션을 정의할 때, 적용대상을 지정</p>
<p> <code>@Target({ElementType.TYPE, ElementType.METHOD})</code> 이런식으로 사용</p>
</li>
<li><p><strong>@Retention</strong></p>
<p> 어노테이션의 유지기간을 지정</p>
<ul>
<li><p><code>Runtime</code> 클래스 파일에도 존재, 실행시 사용가능(값 호출 가능)</p>
</li>
<li><p><code>Source</code> 컴파일되면 사라짐 (걍 주석)</p>
<p><code>@Rentention(RetentionPolicy.SOURCE)</code> 사용 예</p>
</li>
</ul>
</li>
</ol>
<p>그 외. @<strong>Documented, @Inherited, @Repeatable 등..</strong></p>
<h3 id="어노테이션-생성">어노테이션 생성</h3>
<pre><code class="language-java">@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetTest {
    String name() default &quot;Test&quot;;
    int testCnt();
}</code></pre>
<h2 id="reflection">Reflection</h2>
<ul>
<li>리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성</li>
<li>인스턴스의 시그니쳐에 접근 제어자와 상관없이 접근이 가능하도록 하는 API</li>
</ul>
<p><strong>런타임(실행 시간)에 클래스, 메서드, 필드 등의 정보를 조회하고 조작할 수 있는 기능</strong>을 의미합니다.</p>
<p>⇒ 실행 중에 객체의 구조를 분석하고 <strong>동적</strong>으로 변경</p>
<p>JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료 후 해당 클래스의 정보를 담은 <strong>Class 타입의 객체</strong>를 생성 후 힙 영역에 저장 <strong>→ <code>new</code>키워드로 만드는 객체 와는 다르다.</strong></p>
<h3 id="class-객체-로드-방법">Class 객체 로드 방법</h3>
<ol>
<li><p>클래스 이름으로 Class 객체 가져오기</p>
<p> <code>Class&lt;?&gt; cl1 = Example.class;</code></p>
</li>
<li><p>객체를 통한 Class 객체 가져오기</p>
<p> <code>Example example = new Example();</code></p>
<p> <code>Class&lt;?&gt; cls2 = example.getClass();</code></p>
</li>
</ol>
<ol>
<li><p>클래스 이름을 문자열로 가져오기</p>
<p> <code>Class&lt;?&gt; cls3 = Class.forName(&quot;Example&quot;);</code></p>
<ul>
<li>클래스 이름을 문자열로 다룰 수 있기 때문에 동적으로 로딩이 가능.</li>
</ul>
</li>
</ol>
<pre><code class="language-java">Example example = new Example();
Class&lt;?&gt; cls2 = example.getClass();</code></pre>
<p>여기서 나는 저 <code>example</code> 객체와 <code>cls2</code> 객체가 다른건가라는 생각을 했다.</p>
<p>결과적으로 다르다.</p>
<ul>
<li>example객체는 Example클래스의 실제 객체</li>
<li>cls2객체는 Example의 클래스 메타정보를 가지고 있는 Class&lt;?&gt;의 객체이다.</li>
</ul>
<p>여기까지 난 특정 클래스의 정보를 가지고 있는<code>Class</code> 객체를 생성?불러와서 이 객체를 통해서 특정 클래스의 정보를 가져올 수 있구나로 이해했다.</p>
<p>근데 왜 이게 되는거지가 궁금해서 좀 찾아보니 다음과 같은 과정으로 설명할 수 있을 거 같다.</p>
<ol>
<li>A라는 클래스를 생성하면 JVM이 A클래스의 정보를 메소드 영역에 저장한다</li>
<li>Class 객체를 생성하면 Class 객체가 Heap영역에 로드되고,  메모리 영역에 있는 A클래스의 정보를 참조한다.</li>
</ol>
<p><img src="attachment:f885b227-a22a-421b-a242-ea8021c9f18f:image.png" alt="image.png"></p>
<p>이제야 좀 이해가 되는 거 같다. 근데 의문점이 있다. 어째서 Class객체는 메타데이터의 접근제어자를 무시하는가..</p>
<p>⇒ 좀 찾아보니 JVM내부적으로 <code>setAccessible(true)</code>를 사용해서 접근 제한을 해제한단다.. 너무 딥해지니 이제 그만알아보자..</p>
<h3 id="주요-메서드">주요 메서드</h3>
<p><strong>클래스 관련</strong></p>
<ul>
<li><code>getName()</code> : 클래스 전체 이름 (패키지 포함)</li>
<li><code>getSimpleName()</code> : 클래스 이름 (패키지 제외)</li>
<li><code>getPackage()</code> : 패키지정보</li>
<li><code>getSuperclass()</code> : 부모클래스 정보</li>
<li><code>getInterfaces()</code> : 클래스가 구현한 인터페이스 정보</li>
</ul>
<p><strong>생성자 관련</strong></p>
<ul>
<li><code>Constructor&lt;?&gt;[] constructor = cls.getConstructors();</code> : public 생성자들을 배열의 형태로 가져옴</li>
<li><code>Constructor&lt;?&gt;[] constructor = cls.getConstructor(String.class);</code> : 특정 매개변수 생성자 가져오기</li>
<li><code>Constructor&lt;?&gt;[] constructor = cls.getDeclaredConstructors();</code> : private 생성자도 가져</li>
<li><code>Object obj = constructor.newInstance(&quot;어쩌고&quot;);</code> : 생성자를 이용해 객체 생성</li>
</ul>
<p><strong>필드 관련</strong></p>
<ul>
<li><p><code>Field[] fields = cls.getFields()</code> : 필드 배열 가져오기</p>
<ul>
<li><code>Field field = cls.getField(&quot;필드명&quot;)</code> : 특정 public 필드 가져오기</li>
</ul>
</li>
<li><p><code>getDeclaredFields()</code> : 모든 필드 가져오기</p>
</li>
<li><p><code>getDeclaredField(&quot;필드명&quot;)</code> : 특정 private필드 가져오기</p>
</li>
<li><p><code>get(obj)</code> : 객체의 필드 값을 가져옴(인스턴스 → 객체 필요, Static필드 → 객체 불필요)</p>
</li>
<li><p><code>set(obj, &quot;필드이름&quot;)</code> : 필드값 변경</p>
</li>
</ul>
<p><strong>메소드 관련</strong></p>
<ul>
<li><code>invoke(obj)</code> : 매개변수 없는 메서드 실행</li>
<li><code>invoke(obj, &quot;매개변수 명&quot;, 인자값)</code> : 매개변수 있는 메서드 실행</li>
</ul>
<p><strong>어노테이션 관련</strong></p>
<ul>
<li><code>isAnnotationPresent(MyAnnotation.class)</code> : 클래스의 모든 어노테이션 가져오기</li>
<li><code>getAnnotation(MyAnnotation.class)</code> : 특정 어노테이션 가져오기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] 인터페이스와 Inner Class]]></title>
            <link>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-Inner-Class</link>
            <guid>https://velog.io/@tech_bae/%EC%9E%90%EB%B0%94-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-Inner-Class</guid>
            <pubDate>Fri, 14 Mar 2025 00:05:12 GMT</pubDate>
            <description><![CDATA[<h2 id="인터페이스">인터페이스</h2>
<p>클래스들이 구현해야하는 메서드들의 집합</p>
<p>클래스를 만들기위한 기본 뼈대.</p>
<p><strong>추상 클래스</strong>  - 구현된 메소드와 필드를 가질수 있음, 단일 상속만 가능</p>
<p><strong>인터페이스</strong> - 메서드의 시그니쳐만 정의(이름, 변환형, 매개변수), 다중 상속 가능</p>
<p>인터페이스내의 메서드는 <code>public</code>과 <code>abstract</code>로 간주(생략가능)</p>
<p>인터페이스는 상수만 가질 수 있음(<code>public</code>, <code>static</code>, <code>final</code>로 자동으로 선언)</p>
<pre><code class="language-java">public interface TestInterface {
    int a = 0;  //자동으로 public static final

    public abstract void testMethod();
    void testMethod2(); //기본적으로 public abstract 간주
}</code></pre>
<p>인터페이스가 <code>public</code>과 <code>abstract</code> 만 가능한 건 아님</p>
<p><strong>default와 static은 가능</strong></p>
<ul>
<li><strong>default</strong> : 인터페이스 내에서 구현이 가능(override도 가능)</li>
<li><strong>static</strong> : 인터페이스 내에서 구현이 가능하나 <strong>재정의 불가능(override 불가)</strong></li>
</ul>
<pre><code class="language-java">public interface TestInterface {
    default void defaultTestMethod() {
        System.out.println(&quot;Default Method is available in Interface&quot;);
    }

    static void staticTestMethod() {
        System.out.println(&quot;Static Method is also available in Interface&quot;);
    }
}

package lec1.test.review;

public class TestClass implements TestInterface {

    @Override
    public void defaultTestMethod() {
        System.out.println(&quot;Default Method is available to override&quot;);

        TestInterface.super.defaultTestMethod(); //super키워드를 통해 인터페이스 내의 default메소드 호출 가능
    }

}

package lec1.test.review;

public class TestMain {
    public static void main(String[] args) {
        TestClass t = new TestClass();

        t.defaultTestMethod(); //Override된 메소드 호출
        TestInterface.staticTestMethod(); // Static이므로 인터페이스 이름을 통해 바로 접근 가능
    }
}
</code></pre>
<p> 클래스가 여러 인터페이스를 구현가능<strong>(like 다중상속)</strong></p>
<pre><code class="language-java">class A implements intesrfaceA, interfaceB</code></pre>
<pre><code class="language-java">interface C extends interfaceA, interfaceB</code></pre>
<p>인터페이스가 인터페이스를 다중상속가능</p>
<p>단, 이를 구현하는 class는 모든 메서드를 구현해야한다.</p>
<h3 id="인터페이스-구현과-클래스-상속을-동시에-받는-겨우">인터페이스 구현과 클래스 상속을 동시에 받는 겨우</h3>
<pre><code class="language-java">class a extends b implements c

//c : void sleep();, method1();
//b : public void sleep();</code></pre>
<p><strong>클래스의 메서드가 인터페이스의 메서드보다 우선</strong></p>
<p>c에 있는 <code>sleep()</code>을 b가 구현하고 a가 b를 상속받는다.</p>
<p>⇒ a는 <code>method1()</code>만 구현하면 된다. (부모클래스가 인터페이스를 이미 구현하고 상속한걸로 인식)</p>
<h2 id="캐스팅">캐스팅</h2>
<ul>
<li>다운캐스팅 - 부모 객체를 자식 객체로 변환( 문제가 자주 일어남)</li>
<li>업캐스팅 - 자식 객체를 부모 타입으로 변환 (문제 거의 없음)</li>
</ul>
<h2 id="inner-class">Inner Class</h2>
<p>inner class는 <strong>outer 클래스의 멤버로 정의</strong></p>
<p>inner class에서 outer변수는 사용이 가능하다</p>
<p>(변수명이 같다면 inner우선 ⇒ this.변수명이 자동으로 붙음)</p>
<p>outer변수를 사용하려면 <strong>outer클래스명.this.변수명</strong> 으로 사용</p>
<p>outer class에서 inner class의 변수에는 접근이 불가능(static 제외)</p>
<p>⇒ inner class의 객체를 생성후에 접근해야함</p>
<p>inner class의 인스턴스를 생성하려면 outer class의 인스턴스를 먼저 생성해야함</p>
<pre><code class="language-java">OuterClass out = new OuterClass();
OuterClass.InnerClass in = out.new InnerClass();

OuterClass.InnerClass in2 = new OuterClass().new InnerClass();</code></pre>
<h2 id="static-inner-class">Static Inner Class</h2>
<p>Outer class의 인스턴스와 독립적으로 존재</p>
<p>Outer class의 정적멤버에만 접근이 가능</p>
<p>Outer class의 인스턴스 없이 <strong>독립적으로 생성가능</strong></p>
<h3 id="inner-class의-메모리-누수">Inner Class의 메모리 누수</h3>
<p>static이 아닌 inner class는 메모리 누수를 일으킬 우려가 있음</p>
<p>일반 Inner Class는 Outer Class에 대한 참조를 유지하기 때문</p>
<p>Inner Class의 인스턴스가 생성되는 순간 Outer 클래스의 참조를 가진다.</p>
<p>⇒ Outer Class의 <code>this</code> 참조를 자동으로 포함하기 때문에 Outer class가 더이상 필요 없더라고 GC가 이를 해제하지 못함</p>
<p>⇒ Inner class의 객체가 Outer class의 객체보다 오래 유지되면 메모리 누수 발생 가능</p>
<pre><code class="language-java">public class OuterClass {
    private int ramdomNumber = 45678;

    public int getRamdomNumber() {
        return ramdomNumber;
    }

    public class InnerClass {
        public void trash() {
            System.out.println(ramdomNumber + &quot;is still available&quot;);
        }
    }

}

public class TestMain {
    public static void main(String[] args) {
        OuterClass outerInstance = new OuterClass();
        OuterClass.InnerClass innerInstance = outerInstance.new InnerClass();

        System.out.println(outerInstance.getRamdomNumber());
        innerInstance.trash();
        outerInstance = null;
        //System.out.println(outerInstance.getRamdomNumber());
        innerInstance.trash();
    }

}

출력: 
45678
45678is still available
45678is still available
</code></pre>
<p>Outer 인스턴스가 <code>null</code> 로 변하고 나서도 Inner 인스턴스는 값을 계속 참조하여 호출이 가능함 → 메모리누수!</p>
<p>💡</p>
<p>메모리 누수를 방지하기 위해 특별한 경우가 아니라면(외부클래스의 멤버 사용 등) <strong>static</strong>으로 생성 요망</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] 객체지향 프로그래밍과 자바]]></title>
            <link>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%ED%95%99%EC%8A%B5%EC%A0%95%EB%A6%AC-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%ED%95%99%EC%8A%B5%EC%A0%95%EB%A6%AC-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Fri, 07 Mar 2025 08:49:13 GMT</pubDate>
            <description><![CDATA[<h1 id="객체-지향object-oriented-programming">객체 지향(Object-Oriented Programming)</h1>
<h2 id="객체지향의-특징">객체지향의 특징</h2>
<ul>
<li><p><strong>캡슐화(Encapsulation)</strong></p>
<ul>
<li>객체의 데이터와 메서드를 하나로 묶어 외부로부터 정보를 은닉하는 원칙</li>
<li>객체가 제공하는 공용 메서드를 통해서만 조작가능</li>
<li><strong>접근제어자</strong>를 통해 구현될 수 있음</li>
</ul>
</li>
<li><p><strong>상속(Inheritance)</strong></p>
<ul>
<li>부모클래스의 속성과 메소드를 자식클래스가 물려받아 사용할 수 있도록 하는 원칙</li>
<li>코드의 재사용성 증가, 객체간의 관계가 명확하짐</li>
</ul>
</li>
<li><p><strong>다형성(Polymorphism)</strong></p>
<ul>
<li>동일한 이름의 메서드를 다양한 형태로 구현하는 기능(Overloading, Overriding)</li>
<li>오버로딩<ul>
<li>같은 이름 - 다른 매서드 시그니쳐(매개변수 타입, 개수 등)</li>
</ul>
</li>
<li>오버라이딩<ul>
<li><strong>상속받은 메서드</strong>를 자식 클래스에서 재정의</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>추상화(Abstraction)</strong></p>
<ul>
<li>복잡한 시스템내의 중요한 부분만 강조, 세부사항은 숨기는 것</li>
<li><strong>추상 클래스, 인터페이스</strong>로 추상화 구현</li>
</ul>
</li>
</ul>
<h2 id="객체">객체</h2>
<p>객체는 데이터와 메서드를 하나로 묶은 실체로 <strong>상태</strong>와 <strong>행동</strong>으로 구성</p>
<h2 id="클래스">클래스</h2>
<p>객체의 설계도로 생각할 수 있다.</p>
<p>객체의 상태를 나타낼 수 있는 <strong>필드</strong>와 행동을 나타내는 <strong>메서드</strong>로 구성</p>
<p>이름은 <strong>대문자로 시작</strong></p>
<ul>
<li><strong>정적변수(Static)</strong> : 클래스 자체에 속하여 모든 객체가 공유하는 변수<ul>
<li>클래스가 메모리에 올라갈때 같이 적재(클래스 멤버 변수) → 클래스에서 직접 참조 가능</li>
</ul>
</li>
<li>인스턴스변수 : 인스턴스별로 독립적으로 가지는 변수</li>
</ul>
<h2 id="인스턴스">인스턴스</h2>
<p>인스턴스는 클래스을 바탕으로 생성된 객체이다.</p>
<p><strong>new</strong> 키워드로 생성</p>
<h3 id="생성자">생성자</h3>
<p>객체를 초기화하는 메서드</p>
<p>객체의 초기 상태를 설정한다.</p>
<ul>
<li>생성자가 오버로딩되어 있지 않고 없는 상태면 컴파일러가 기본생성자를 자동 생성한다.<ul>
<li>사용자지정생성자가 하나 있으면 이 생성자가 기본생성자의 역할을 함(매개변수는 맞춰줘야 한다.)</li>
</ul>
</li>
<li>생성자의 이름은 클래스의 이름과 같아야한다.</li>
<li>변환타입X</li>
</ul>
<h3 id="this">this</h3>
<p>객체 자신을 참조하는 키워드</p>
<p>객체의 필드를 참조</p>
<pre><code class="language-java">public class Test{
    private int age;

    public howOld(int age){
        this.age = age;
    }
}</code></pre>
<p>위와 같은 예일때 메서드의 매개변수 <code>age</code>와 필드 <code>age</code>를 구분하기 위해 <code>this.age</code>를 사용하여 객체자신을 참조한다.</p>
<h3 id="생성자-체이닝constructor-chaining">생성자 체이닝(Constructor Chaining)</h3>
<p><strong>this</strong>로 같은 클래스의 다른 생성자를 호출 할 수 있다.</p>
<p>예를 들어 매개변수가 없는 생성자를 호출 했을때 </p>
<p><code>this.(0,0)</code> 이런식으로 다른 생성자를 호출하면 필드에 0값이 들어가게 된다.</p>
<h2 id="스코프scope">스코프(Scope)</h2>
<p>변수나 메서드에 접근할 수 있는 범위를 뜻한다.</p>
<ul>
<li>클래스 스코프</li>
<li>메서드 스코프</li>
<li>블록 스코프</li>
<li>매개변수 스코프</li>
<li>정적 영역 스코프<ul>
<li>클래스의 모든 인스턴스가 공유한다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] 제어문과 함수]]></title>
            <link>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%EC%9E%90%EB%B0%94%EC%A0%9C%EC%96%B4%EB%AC%B8%EA%B3%BC-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%EC%9E%90%EB%B0%94%EC%A0%9C%EC%96%B4%EB%AC%B8%EA%B3%BC-%ED%95%A8%EC%88%98</guid>
            <pubDate>Thu, 06 Mar 2025 08:38:17 GMT</pubDate>
            <description><![CDATA[<p>📋유용한 단축키(IntelliJ)</p>
<ul>
<li><strong>f2</strong> : 변수이름을 바꿨을때 오류가 발행하는 위치로 순간이동(바꿔야 하는 곳을 찾기가 쉽다.)</li>
<li><strong>shift + f6</strong> : 리팩토링(모든 관련 참조가 함께 변경 안전하게 변수이름을 바꿀 수 있다)</li>
<li><strong>ctrl + alt + v</strong> : 변수 추출 같은 원리로 클래스(<strong>c</strong>), 메소드(<strong>m</strong>)도 가능!</aside>

</li>
</ul>
<br>

<h1 id="제어문">제어문</h1>
<h2 id="선택문">선택문</h2>
<h3 id="if-else-if-else">if, else if, else</h3>
<ul>
<li><strong>if</strong>문은 주어진 조건이 <strong>참</strong>이면 해당 블록을 실행.</li>
</ul>
<pre><code class="language-java">if (조건식 or 값) {
    본문
}</code></pre>
<ul>
<li><strong>else if</strong>는 여러 개의 조건이 있을때 사용이 가능하다. 조건이 참이 블록을 실행하고 if문을 종료한다.(나머지 조건을 검사X)</li>
</ul>
<pre><code class="language-java">if (a &gt;= 6) {
    System.out.println(&quot;Too big&quot;);
    } else if(a &lt; 6 &amp;&amp; a &gt;= 3){
        System.out.println(&quot;Good Number&quot;);
   }

//출력 : Good Number
</code></pre>
<ul>
<li><strong>else</strong>는 ****어떠한 조건에도 만족하지 않을때 코드블록을 실행.</li>
</ul>
<pre><code class="language-java">boolean isClassFinished = false;

if (isClassFinished) {
    System.out.println(&quot;Let&#39;s go to workout!!&quot;);
    } else{
        System.out.println(&quot;Hang on...&quot;);
        }</code></pre>
<h3 id="switch">Switch</h3>
<ul>
<li>if문과 비슷하나 case에 부합하는 값이 들어오면 해당 case문을 실행하고 그 아래의 모든 case를 실행시킴<ul>
<li><strong>break</strong>키워드를 사용해 해당 case만 실행 시킬 수 있음</li>
</ul>
</li>
<li>모든 case에 부합하지 않을때 <strong>default</strong>키워드를 사용하여 실행시킴</li>
</ul>
<pre><code class="language-java">int number = 2;

switch (number){
  case 1:
      System.out.println(&quot;1&quot;);
  case 2:
      System.out.println(&quot;2&quot;);
  case 3:
      System.out.println(&quot;3&quot;);
  case 4:
      System.out.println(&quot;4&quot;);
}

//출력
2
3
4</code></pre>
<h1 id="반복문">반복문</h1>
<h2 id="while">while</h2>
<ul>
<li>조건이 참일 동안 반복적으로 실행되는 제어문</li>
</ul>
<h2 id="do-while">do while</h2>
<ul>
<li>while과 흡사하지만 <strong>do</strong>블록의 코드가 무조건 한번은 실행된다.</li>
<li><strong>do</strong> 블록(반복문)실행하고 조건식을 검사한다.</li>
</ul>
<h2 id="for">for</h2>
<ul>
<li>while과는 다르게 반복 수를 명시적으로 제어할 수 있음</li>
<li>for문의 외부에 있는 변수도 이용이 가능하다(이건 몰랐다..)</li>
</ul>
<pre><code class="language-java">int a = 5;
for(; a &lt;= 10; a++){
    System.out.println(a);
}

/* 출력
5
6
7
8
9
10
*/</code></pre>
<h1 id="함수">함수</h1>
<h2 id="함수와-메서드">함수와 메서드</h2>
<p>나는 함수와 메서드는 동일하고 용어의 차이인줄 알았는데 미묘한 차이가 있었다.</p>
<ul>
<li>함수<ul>
<li><strong>어떤 객체에 속하지 않고</strong>, 그자체로 호출된다.</li>
<li>객체와 무관하게 독립적으로 존재</li>
</ul>
</li>
<li>메서드<ul>
<li><strong>클래스나 객체에 소속</strong>되어 있다.</li>
</ul>
</li>
</ul>
<h2 id="메서드의-정의">메서드의 정의</h2>
<pre><code class="language-java">접근제어자 반환자료형 메서드이름 (매게변수 ...) {
        본문
}</code></pre>
<ul>
<li><strong>접근제어자</strong> : 메서드에 접근할 수 있는 범주를 정의 → 데이터 은닉화와 캡슐화 강화</li>
<li><strong>반환 자료형 :</strong> 메서드가 반환하는 자료형을 정의</li>
<li><strong>매게변수</strong> : 메서드가 사용할 수 있는 값, 매게변수는 호출 될 때 전달하는 <strong>인자값을 정의</strong></li>
</ul>
<h3 id="접근제어자">접근제어자</h3>
<table>
<thead>
<tr>
<th>접근자</th>
<th>클래스 내부</th>
<th>패키지</th>
<th>상속받은 클래스</th>
<th>이외의 영역</th>
</tr>
</thead>
<tbody><tr>
<td>default</td>
<td>O</td>
<td>O</td>
<td>X</td>
<td>X</td>
</tr>
<tr>
<td>public</td>
<td>O</td>
<td>O</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>private</td>
<td>O</td>
<td>X</td>
<td>X</td>
<td>X</td>
</tr>
<tr>
<td>protected</td>
<td>O</td>
<td>O</td>
<td>O</td>
<td>X</td>
</tr>
</tbody></table>
<h3 id="가변인자vararg">가변인자(Vararg)</h3>
<p>가변 인자를 사용하여 매개변수의 개수를 동적으로 설정할 수 있다!</p>
<pre><code class="language-java">public static void main(String[] args) {
        vararg(&quot;cat&quot;,&quot;dog&quot;,&quot;cow&quot;,&quot;rat&quot;,&quot;zebra&quot;);
    }

    public static void vararg(String...animals){
        for(String animal : animals){
            System.out.println(animal);
        }
    }</code></pre>
<p>이런게 될 줄은 몰랐다. 나만 신기했나.. 난 너무 신기했다.</p>
<h2 id="오버로딩">오버로딩</h2>
<p>같은 이름을 가지는 메소드들을 여러개 정의 하는 것</p>
<p>단, 각 매서드의 매개변수의 타입이나 개수, 순서가 다르게 정의해야 한다.</p>
<p>반환 타입만으로는 오버로딩X</p>
<ul>
<li><strong>메서드 시그니쳐</strong> : 메서드를 구별하는데 필요한 정보<ul>
<li>반환타입은 메서드 시그니쳐가 아니므로 메서드의 구별에 영향이 없다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바] 자료형, 메모리, 연산자]]></title>
            <link>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%EC%9E%90%EB%B0%94%EC%9E%90%EB%A3%8C%ED%98%95-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%97%B0%EC%82%B0%EC%9E%90</link>
            <guid>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-%EC%9E%90%EB%B0%94%EC%9E%90%EB%A3%8C%ED%98%95-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%97%B0%EC%82%B0%EC%9E%90</guid>
            <pubDate>Thu, 06 Mar 2025 08:35:50 GMT</pubDate>
            <description><![CDATA[<h1 id="자바">자바</h1>
<ul>
<li>플랫폼 독립성<ul>
<li>‘Write Onne, Run Anywhere’원칙을 기반으로 한번 작성된 코드는 다양한 플랫폼에서 실행될 수 있습니다.</li>
<li>JVM이라는 중간 계층에서 자바를 실행시키므로 다른 운영체제에 JVM만 설치되어있다면 자바가 실행 가능</li>
</ul>
</li>
<li>객체 지향 프로그램<ul>
<li>자바는 대표적인 객체 지향 프로그래밍 언어이다.</li>
<li>코드의 재사용성과 확정성이 뛰어남</li>
</ul>
</li>
</ul>
<h2 id="자바의-구성">자바의 구성</h2>
<ul>
<li><strong>JDK(Java Development Kit)</strong><ul>
<li>자바 애플리케이션을 개발하기 위한 도구의 모음</li>
<li>컴파일러, 디버거, 자바API등이 포함</li>
<li>자바 컴파일러(javac)는 자바 코드를 바이트코드로 컴파일하여 JVM에서 실행될 수 있도록 함</li>
<li>자바 표준라이브러리를 통해 내장된 클래스나 매서드를 활용할 수 있게 함</li>
</ul>
</li>
<li><strong>JRE(Java Runtime Environment)</strong><ul>
<li>자바 애플리케이션을 실행하기 위한 환경 제공</li>
<li>JRE는 JVM과 자바 표준 라이브러리로 구성</li>
<li>자바 실행을 위한 모든 요소를 포함(개발도구 X)</li>
</ul>
</li>
<li><strong>JVM(Java Virtual Machine)</strong><ul>
<li>자바 프로그램 실행 환경을 제공하는 핵심 구성요소</li>
<li>바이트코드(.class 파일)을 읽고 해석 → 기계어로 변환</li>
<li>메모리관리, 가비지 컬렉션, 쓰레드 관리 등의 기능</li>
</ul>
</li>
</ul>
<h1 id="자료형과-메모리">자료형과 메모리</h1>
<h2 id="원시-자료형primitive-data-types">원시 자료형(Primitive Data Types)</h2>
<p>직접적으로 메모리의 특정 위치에 값을 저장</p>
<p>값으로써 직접 사용이 가능하다. 객체가 아님</p>
<h3 id="정수-자료형">정수 자료형</h3>
<ul>
<li><p><strong>byte</strong></p>
<ul>
<li><strong>1byte</strong>의 크기</li>
<li>-128부터 127까지의 <strong>정수</strong> 표현가능</li>
</ul>
</li>
<li><p><strong>short</strong></p>
<ul>
<li><strong>2byte</strong>의 크기</li>
<li>-32,768부터 32767까지의 <strong>정수</strong> 표현가능</li>
</ul>
</li>
<li><p><strong>int</strong></p>
<ul>
<li><p><strong>4byte</strong>의 크기</p>
</li>
<li><p>--2,147,483,648부터 2,147,483,647까지의 <strong>정수</strong> 표현 가능</p>
</li>
<li><p>다양한 진수별 값 할당 가능</p>
<pre><code class="language-java">int a = 0;
int b = -1;
int c = 0b1110; // 14(2)
int d = 012; // 10(8) (앞에 0을 써줌으로써 8진수임을 표현)
int e = 10;  // 10
int f = 0xA; // 10(16)</code></pre>
</li>
</ul>
</li>
<li><p><strong>long</strong></p>
<ul>
<li><strong>8byte</strong>의 크기</li>
<li>-9,223,372,036,854,775,808부터 9,223,372,036,854,775,807까지의 <strong>정수</strong> 표현 가능;;</li>
<li>값 할당시 할당될 값 뒤 <strong>L</strong> 을 입력해야함 <code>long hugeNumber = 100_000_000_000L;</code></li>
</ul>
</li>
</ul>
<p>참고로 정수형 자료형에선 값에 <strong>언더바(_)을 사용하여 가독성을 높힐 수 있음</strong></p>
<pre><code class="language-java">int justNumber = 1_000_000_000;
short smallNumber = 10_000;</code></pre>
<h3 id="실수-자료형">실수 자료형</h3>
<ul>
<li><strong>float</strong><ul>
<li><strong>4byte</strong>의 크기</li>
<li>약 6~7자리의 십진수 정밀도</li>
<li>double보다 정밀도가 낮아 정밀한 계산에서는 부적절</li>
<li>long과 비슷하게 값을 할당시 <strong>f</strong>를 덧붙여야함 <code>float pi = 3.14f;</code></li>
</ul>
</li>
<li><strong>double</strong><ul>
<li><strong>8byte</strong>의 크기</li>
<li>약 15자리수의 정밀도 제공</li>
<li>float보다 정밀도가 큼</li>
</ul>
</li>
</ul>
<h3 id="문자-자료형">문자 자료형</h3>
<ul>
<li><p><strong>char</strong></p>
<ul>
<li><p><strong>2byte</strong>의 크기</p>
</li>
<li><p>유니코드를 기반으로 문자를 저장</p>
</li>
<li><p>작은 따옴표 사용(’)</p>
<pre><code class="language-java">char a = &#39;\uC548&#39;;
char b = &#39;\uB155&#39;;
char c = &#39;\uD558&#39;;
char d = &#39;\uC138&#39;;
char e = &#39;\uC694&#39;;

System.out.printf(&quot;%c%c%c%c%c&quot;,a,b,c,d,e); // 안녕하세요</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="논리-자료형">논리 자료형</h3>
<ul>
<li><strong>boolean</strong><ul>
<li>참 or 거짓을 표현</li>
<li>파이썬과 달리 소문자로 써야함.. 맘에 안듬</li>
</ul>
</li>
</ul>
<h2 id="참조-자료형">참조 자료형</h2>
<ul>
<li>참조 자료형은 객체를 참조하기 위해 사용된다.</li>
<li>기본자료형(원시자료형)과는 다르게 값이 저장된 메모리의 주소를 저장합니다.</li>
<li>힙(heap)영역에 저장된다.</li>
<li>참조변수는 힙 메모리의 객체를 가리키는 메모리 주소를 저장</li>
<li><strong>가비지 컬렉션</strong>이 더 이상 사용되지 않는 객체를 메모리에서 해제 → 메모리 누수 방지 및 효율적인 메모리 사용</li>
</ul>
<h3 id="wrapper-class">Wrapper Class</h3>
<p>기본자료형(원시자료형)을 객체로 다룰 수 있게 하는 클래스</p>
<p>자바에선 객체만이 메서드에 인수로 전달, 컬렉션 클래스와 같은 데이터 구조에 저장</p>
<blockquote>
<p><strong>byte → Byte</strong></p>
<p><strong>short → Short</strong></p>
<p><strong>int → Integer</strong></p>
<p><strong>long → Long</strong></p>
<p><strong>float → Float</strong></p>
<p><strong>double → Double</strong></p>
<p><strong>char → Character</strong></p>
<p><strong>boolean → Boolean</strong></p>
</blockquote>
<ul>
<li><strong>박싱(Boxing)</strong> : 원시자료형을 래퍼클래스로 변환</li>
<li><strong>언박싱(Unboxing)</strong> : 위 과정의 반대</li>
</ul>
<pre><code class="language-java">int basicInt = 27;
Integer wrapperInt = Integer.valueOf(basicInt);

int unboxedInt = wrapperInt.intValue();
</code></pre>
<p>위 처럼 boxing을 위해선 <code>valueOf</code>를 사용하고</p>
<p>unboxing은 <code>intValue</code>를 사용하여 가능</p>
<h1 id="jvm">JVM</h1>
<p>JVM은 메모리를 영역들로 나누어 관리</p>
<h2 id="메서드-영역method-area">메서드 영역(Method Area)</h2>
<ul>
<li>클래스의 메타데이터를 저장하는 공간(클래스 이름, 상위 클래스, 인터페이스 etc.)<ul>
<li>클래스와 메서드의 구조적 정보(클래스 정보, 메서드 코드, 정적 변수, 상수 풀) 저장</li>
</ul>
</li>
<li>모든 스레드가 공유</li>
<li>프로그램 실행 동안 클래스 정의와 메서드 정의 유지</li>
</ul>
<h2 id="힙-영역heap-area">힙 영역(Heap Area)</h2>
<ul>
<li>동적으로 생성된 객체와 배열을 저장하는 공간</li>
<li>가비지 컬렉션에 의해 관리 → 더 이상 참조되지 않는 객체를 해제</li>
<li>프로그램 메모리 사용량 조절 / 데이터 저장과 관리</li>
</ul>
<h2 id="스택-영역stack-area">스택 영역(Stack Area)</h2>
<ul>
<li>스레드마다 별도로 할당</li>
<li>메서드 호출과 관련되 지역변수, 매게변수 저장</li>
<li>메서드 호출 시 <strong>스택 프래임</strong>이 생성, 종료되면 스택 프레임 제거</li>
<li>LIFO방식으로 관리, 지역 변수와 메서드의 실행 정보 저장</li>
<li>메서드 호출의 효율적인 관리, 메모리 할당의 자동화</li>
</ul>
<h2 id="네이티브-메서드-스택-영역native-method-stack">네이티브 메서드 스택 영역(Native Method Stack)</h2>
<ul>
<li>자바 외부에서 작성된 네이티브 메서드 호출 시 사용</li>
<li>자바 네이티브 인터페이스를 통해 C나 C++로 작성된 네이티브 코드를 실행할 때 필요한 메모리 공간 제공</li>
<li>자바와 네이티브 코드 간의 상호작용을 지원</li>
<li>외부 시스템과 상호작용 필요시 사용</li>
</ul>
<h1 id="연산자">연산자</h1>
<h2 id="shift연산자">Shift연산자</h2>
<h3 id="좌측-shift">좌측 Shift</h3>
<p>비트열을 왼쪽으로 지정된 수 만큼 이동(오른쪽 빈 자리는 0으로 채운다) → 결과적으로 2의 제곱수로 곱하는 결과 도출</p>
<pre><code class="language-java">int a = 7; // 0111
int leftShifted = a &lt;&lt; 1; //1110(14)</code></pre>
<h3 id="우측-shift">우측 Shift</h3>
<p>좌측 Shift와 동일하게 비트열을 오른쪽으로 이동(왼쪽 빈 자리는 원래의 부호 비트로 채움) → 결과적으로 2의 제곱수로 나눗셈하는 결과</p>
<pre><code class="language-java">int a = 12; // 1100
int rightShifted = a &gt;&gt; 1; //0110(6)</code></pre>
<h3 id="부호-없는-우측-shift">부호 없는 우측 Shift</h3>
<p>우측 Shift와 흡사하나 왼쪽 빈자리를 <strong>0</strong>으로 채움 → 부호 비트를 무시</p>
<pre><code class="language-java">int a = -20;  // 1111 1111 1111 1111 1111 1111 1110 1100 
int result = a &gt;&gt;&gt; 2;  // 0011 1111 1111 1111 1111 1111 1111 1011 </code></pre>
<h2 id="연산자의-우선순위">연산자의 우선순위</h2>
<table>
<thead>
<tr>
<th><strong>우선순위</strong></th>
<th><strong>연산자</strong></th>
<th><strong>비고</strong></th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>( ) , [ ]</td>
<td>괄호 / 대괄호</td>
</tr>
<tr>
<td>2</td>
<td>!, ~, ++, --</td>
<td>부정/ 증감연산자 (단항연산자)</td>
</tr>
<tr>
<td>3</td>
<td>*, /, %</td>
<td>곱셈 / 나눗셈연산자</td>
</tr>
<tr>
<td>4</td>
<td>+, -</td>
<td>덧셈 / 뺄셈연산자</td>
</tr>
<tr>
<td>5</td>
<td>&lt;&lt; , &gt;&gt;, &gt;&gt;&gt;</td>
<td>비트 이동연산자</td>
</tr>
<tr>
<td>6</td>
<td>&lt;, ≤, &gt; , ≥</td>
<td>관계연산자</td>
</tr>
<tr>
<td>7</td>
<td>==, !=</td>
<td></td>
</tr>
<tr>
<td>8</td>
<td>&amp;</td>
<td>비트 논리연산자</td>
</tr>
<tr>
<td>9</td>
<td>^</td>
<td></td>
</tr>
<tr>
<td>10</td>
<td></td>
<td></td>
</tr>
<tr>
<td>11</td>
<td>&amp;&amp;</td>
<td>논리연산자</td>
</tr>
<tr>
<td>12</td>
<td></td>
<td></td>
</tr>
<tr>
<td>13</td>
<td>? :</td>
<td>조건연산자</td>
</tr>
<tr>
<td>14</td>
<td>=, +=, -=, *=, /=, %=, &lt;&lt;=, &gt;&gt;=, &amp;=, ^=, ~=</td>
<td>대입, 복합대입연산자</td>
</tr>
</tbody></table>
<p>외울 필욘 없을거 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데브코스 학습정리] Markdown, git]]></title>
            <link>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-1%EC%9D%BC%EC%B0%A8-%ED%95%99%EC%8A%B5%EC%A0%95%EB%A6%AC-mm89alto</link>
            <guid>https://velog.io/@tech_bae/%EB%8D%B0%EB%B8%8C%EC%BD%94%EC%8A%A4-1%EC%9D%BC%EC%B0%A8-%ED%95%99%EC%8A%B5%EC%A0%95%EB%A6%AC-mm89alto</guid>
            <pubDate>Wed, 05 Mar 2025 07:44:47 GMT</pubDate>
            <description><![CDATA[<h1 id="markdown">Markdown</h1>
<ul>
<li>readme.md: 프로젝트를 소개하거나 여러 안내사항을 기록할 수 있다.</li>
<li>HTML코드도 적용 가능</li>
<li>직관적이고 가독성이 뛰어나다 무엇보다 이쁜거 같다.</li>
</ul>
<hr>
<h2 id="markdown-문법">Markdown 문법</h2>
<h3 id="제목header">제목(Header)</h3>
<pre><code class="language-markdown"># 제목 1
## 제목 2
### 제목 3
#### 제목 4
##### 제목 5
###### 제목 6</code></pre>
<h1 id="제목-1">제목 1</h1>
<h2 id="제목-2">제목 2</h2>
<h3 id="제목-3">제목 3</h3>
<p>#의 개수가 많을수록 작아진다. 플랫폼마다 지원하는 header에 차이가 있다. 노션에서는 ###까지 지원.</p>
<h3 id="글자-서식">글자 서식</h3>
<pre><code class="language-markdown">**굵은**
*기운*</code></pre>
<p><strong>굵은</strong></p>
<p><em>기운</em></p>
<p><strong><em>굵고 기운</em></strong></p>
<h3 id="코드">코드</h3>
<pre><code class="language-markdown">`print(&quot;Hello, World!&quot;)`</code></pre>
<p><code>print(&quot;Hello, World!&quot;)</code></p>
<p>한줄짜리 코드를 나타낼 수 있음. 강조의 의미로써도 사용가능.</p>
<pre><code class="language-markdown"></code></pre>
<p>def solution():
    print(&quot;Good Day&quot;)</p>
<pre><code></code></pre><pre><code class="language-python">def solution():
    print(&quot;Good Day&quot;)
</code></pre>
<p>여러줄의 코드를 나타낼 수 있음.</p>
<h3 id="목록">목록</h3>
<pre><code class="language-markdown">- 목록 1
- 목록 2

 1. 숫자 목록 1
 2. 숫자 목록 2</code></pre>
<ul>
<li>목록 1</li>
<li>목록 2</li>
</ul>
<ol>
<li>숫자 목록 1</li>
<li>숫자 목록 2</li>
</ol>
<h3 id="체크-박스">체크 박스</h3>
<pre><code class="language-markdown">[x] checked
[] unchecked</code></pre>
<ul>
<li><input checked="" disabled="" type="checkbox"> checked</li>
<li><input disabled="" type="checkbox"> unchecked</li>
</ul>
<h3 id="링크">링크</h3>
<pre><code class="language-markdown">[표시 될 텍스트](url주소)</code></pre>
<p><a href="https://velog.io/@tech_bae/posts">내 블로그</a></p>
<h3 id="이미지">이미지</h3>
<pre><code class="language-markdown">![대체 텍스트](이미지 주소)</code></pre>
<h3 id="인용문">인용문</h3>
<pre><code class="language-markdown">&gt; 시간이 곧 금이다.</code></pre>
<blockquote>
<p>시간이 곧 금이다.</p>
</blockquote>
<p>참고로 노션에서 토글로 작동하는 것 같다. 노션에선 <code>/quote</code>를 사용하도록 하자.</p>
<h3 id="표">표</h3>
<pre><code class="language-markdown">|항목 1|항목 2|항목 3|
|---|---|---|
|뭐시기|거시기|요시기|</code></pre>
<table>
<thead>
<tr>
<th>항목 1</th>
<th>항목 2</th>
<th>항목 3</th>
</tr>
</thead>
<tbody><tr>
<td>뭐시기</td>
<td>거시기</td>
<td>요시기</td>
</tr>
</tbody></table>
<h3 id="개행">개행</h3>
<pre><code class="language-markdown">  (스페이스 두개) 
  or
  &lt;br/&gt;</code></pre>
<p>노션에선  안먹지만 <br/>이곳에선 먹는다.
스페이스  두 개는 벨로그에서도 작동이 안됩니다.</p>
<hr>
<h1 id="git--github">git &amp; github</h1>
<p><strong>git</strong> : 버전 관리 시스템으로 소스코드의 변경사항을 효율적으로 관리함으로써 효율적인 협업을 가능케하는 도구</p>
<p><strong>github</strong>: git의 로컬 저장소을 원격 저장소(remote repository)에 호스팅해주는 플랫폼</p>
<h2 id="명령어">명령어</h2>
<h3 id="기본-명령어">기본 명령어</h3>
<ul>
<li><code>git init</code> : 새로운 git 저장소 생성</li>
<li><code>git add [파일명]</code>   : 특정파일을 스테이징(git이 변경사항을 추적하는 것) -commit이 가능</li>
<li><code>git status</code> : 현재 저장소의 상태를 확인(스테이징 에어리어 확인 가능)</li>
<li><code>git commit -m &quot;메세지&quot;</code> : 변경사항을 커밋함.</li>
<li><code>git log</code> : 커밋 히스토리 확인</li>
</ul>
<h3 id="브랜치branch">브랜치(Branch)</h3>
<p><strong>브랜치(Branch)</strong> : 코드 변경을 독립적(병렬적)으로 진행할 수 있도록 하는 기능입니다.</p>
<ul>
<li><code>git branch</code> : 현재 브랜치 목록 확인</li>
<li><code>git branch [브랜치명]</code> : 새 브랜치 생성</li>
<li><code>git checkout [브랜치명]</code> : 해당 브랜치로 이동(HEAD 이동)</li>
</ul>
<h3 id="브랜치-합치기merge-rebase">브랜치 합치기(Merge, Rebase)</h3>
<pre><code class="language-css">A---B---C  (main)
        \
         D---E (feature-branch)</code></pre>
<p>위와 같은 커밋 히스토리를 가진 브랜치가 있을때</p>
<ul>
<li><p><strong>Merge</strong> : 두 개의 브랜치를 병합하여 기존 커밋 히스토리를 유지하면서 새로운 커밋을 생성</p>
<pre><code class="language-css">  A---B---C---M  (main)
       \       /
        D---E (feature-branch)
</code></pre>
</li>
<li><p><strong>Rebase</strong> : 커밋을 재배열하여 히스토리를 깔끔하게 만듬(기준이 되는 브랜치의 다른 브랜치의 커밋히스토리를 덮어씌움)</p>
<pre><code class="language-css">  A---B---C---D&#39;---E&#39;  (feature-branch)</code></pre>
</li>
</ul>
<blockquote>
<p>각 브랜치에서 같은 파일의 같은 부분을 수정했을때 충돌발생 가능</p>
<p>즉 git이 어느 변경 사항을 유지해야 할지 결정 불가 일 때.</p>
</blockquote>
<h3 id="원격-저장소">원격 저장소</h3>
<ul>
<li><p><code>git remout -v</code> : 원격 저장소 목록 확인</p>
</li>
<li><p><code>git remote add [별칭] [URL]</code> : 원격 저장소 추가</p>
</li>
<li><p><code>git remote remove [별칭]</code> : 원격 저장소 제거</p>
</li>
<li><p><code>git push [별칭] [브랜치명]</code> : 원격저장소에 로컬저장소를 업로드</p>
</li>
<li><p><code>git pull origin [브랜치명]</code> : 로컬저장소에 원격저장소를 다운로드</p>
</li>
<li><p><code>git fetch</code> : pull과 흡사하나 병합하지 않음</p>
</li>
</ul>
<h3 id="git-연습">git 연습</h3>
<p><a href="https://learngitbranching.js.org/?locale=ko">https://learngitbranching.js.org/?locale=ko</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘] KMP알고리즘을 그림으로 쉽게 이해 해보자]]></title>
            <link>https://velog.io/@tech_bae/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-KMP%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98feat.-LPS</link>
            <guid>https://velog.io/@tech_bae/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-KMP%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98feat.-LPS</guid>
            <pubDate>Sun, 19 Jan 2025 19:19:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>평소와 같이 백준문제를 풀던 중 &quot;IOIOI&quot;라는 강렬한 제목에 이끌려 도전했다. 긴 문자열에서 패턴 문자열이 총 몇 번 포함되어 있는지 구하는 문제였다. 단순히 브루트포스로 문제를 풀었더니 이런 세상에 50점 따리를 맞고야 말았다.. 100점을 맞고 싶었던 나는 질문게시판을 돌아다니며 힌트를 찾으려 했고 그렇게 해서 KMP알고리즘의 존재를 알았다. 하지만 안타깝게도 나의 감자대가리는 이를 이해하는데 오랜시간이 걸리고야 말았다. 이글은 KMP알고리즘을 본인 스스로 완전히 이해하려하는 몸부림이다.</p>
</blockquote>
<h1 id="패턴매칭">패턴매칭</h1>
<p>패턴매칭이란 전체 문자열에서 <strong>특정한 문자열(패턴)을 찾는 것</strong>을 말합니다.
예를 들어 &quot;ABCDAB&quot;라는 전체문자열에 &quot;AB&quot;라는 문자열 즉 패턴을 찾는 것을 말합니다.</p>
<p>예를들어, 우리는 인터넷을 돌아다니며 나오는 모든 긴 글들을 다 읽을 용기가 없을 때
우리는 <strong>Ctrl + F</strong>를 입력해서 찾고싶은 키워드를 찾곤합니다.</p>
<p>근데 이 작업을 단순하게 하나하나 비교하는 브루트포스를 사용하여 구현하한다면 논문같이 긴 문자열 등에는 시간이 정말 오래 걸릴겁니다. 정말 화가 나겠죠</p>
<p>그래서 타켓 문자열이 길거나 찾을 패턴의 길이가 길수록 빠르고 효율적인 패턴매칭 알고리즘이 요구됩니다.</p>
<p>여러 패턴매칭 알고리즘이 있지만, 오늘은 브루트포스와 KMP알고리즘에 대해서만 설명하겠습니다.</p>
<h1 id="브루트포스brute-force">브루트포스(Brute Force)</h1>
<p>Brute = 짐승같은, 난폭한  Force = 힘, 폭력
즉 브루트포스는 무식하게 하나하나 대입하여 원하는 것을 찾는 알고리즘? 입니다.
아주 직관적인 방법이죠. 저는 정보보호학과를 나와서 브루트포스를 암호해독으로 처음 배웠습니다.</p>
<p>짧게 예를 들면 만약에 영문 소문자로만 이루어진 &quot;iloveyou&quot;라는 8자리 비밀번호가 있다고 칩시다.
근데 해커가 이 사람의 비밀번호가 영문소문자로만 이루어져 있고 8자리라는 걸 알고있습니다.
그래서 이 해커는 생각하는게 귀찮은 나머지 그냥 영어소문자로 만들수 있는 모든 8글자의 조합을 전부 시도합니다. 아주 무식하지만 아주 오랜 시간이 지나면 비밀번호를 알 수 있겠죠. 
<del>그래서 비밀번호를 충분히 길고 여러 요소를 섞으라고 하는겁니다...</del></p>
<blockquote>
<p><strong>이렇게 모든 조합가능한 수를 모두 대입하는 걸 브루트포스라고 합니다.</strong></p>
</blockquote>
<p>브루트포스는 당연하게도 패턴매칭에도 사용할 수 있습니다.</p>
<ul>
<li>전체 문자열이 <strong>&quot;AAABABC&quot;</strong></li>
<li>찾을 패턴이 <strong>&quot;BC&quot;</strong>라고 해보겠습니다.</li>
</ul>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/7c410683-f223-4944-8be6-073a4cf4bcd7/image.png" alt="">
처음으로 패턴의 처음 문자와 전체 문자열의 처음을 비교합니다.
처음부터 다르므로 패턴의 첫 문자를 전체 문자열의 다음 인덱스로 이동합니다.
<img src="https://velog.velcdn.com/images/tech_bae/post/426cf68d-8006-4707-98e1-292ef83310be/image.png" alt=""></p>
<p>또 다르죠? 다르므로 위와 같은 방식으로 패턴의 첫 문자가 일치할때까지 전체 문자열의 다음 인덱스로 이동합니다.
<img src="https://velog.velcdn.com/images/tech_bae/post/5fb3a943-8298-4bba-8dd2-c2ff98bb39ed/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/bd39ec8e-c33d-4b08-a7a4-ffcff46e6d24/image.png" alt="">
드디어 패턴의 처음과 일치하는 인덱스로 도착했습니다. 이제 패턴의 두번째 문자열이 전체 문자열의 다음 인덱스와도 같은지 확인합니다.
<img src="https://velog.velcdn.com/images/tech_bae/post/caa3dfd0-b4ff-419a-93a7-66d5a5ce8063/image.png" alt="">
아쉽게도 패턴의 두번째 문자는 일치하지 않는군요.. 이제 패턴의 처음 문자를 또다시 비교합니다.
<img src="https://velog.velcdn.com/images/tech_bae/post/28ae58ed-f2f9-43f8-9d7e-1a4788c88dfd/image.png" alt="">
다르므로 반복합니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/f6a721ae-ad62-48d7-bbc9-f409a01f9afa/image.png" alt="">
다시 패턴의 첫 문자와 일치하는 곳을 찾았습니다. 이어서 패턴의 두번째 문자를 비교해야합니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/fc93dda3-78dd-41a4-ab3e-76715ce24bb6/image.png" alt="">
드디어 전체 문자열에서 패턴이 매칭되는 곳을 찾았습니다.</p>
<p>이렇게 전체문자열의 처음부터 끝까지 패턴의 한 문자씩 비교하며 패턴이 매칭되는 곳을 찾는 무식한 방법이 브루트포스입니다.</p>
<h2 id="브루트-포스-시간복잡도">브루트 포스: 시간복잡도</h2>
<p>전체문자열의 길이를 <strong>N</strong>, 패턴의 길이를 <strong>M</strong>이라고 할때 </p>
<p>최악에 경우 N만큼 패턴의 첫 문자와 일치하는지 확인하는 시간 복잡도가 <strong>O(N)</strong>,
위의 작업을 할 때마다 M만큼 매칭되는지 확인하는 시간 복잡도가 <strong>O(M)</strong>이기에
브루트 포스을 이용한 패턴매칭의 시간 복잡도는 <strong>O(N*M)</strong>입니다.</p>
<p>이는 문자열의 길이가 길면 길수록 패턴매칭에 소요되는 시간이 곱으로 늘어남을 뜻합니다.
<del>그래서 제가 &quot;IOIOI&quot;문제를 50따리를 맞았습니다.</del></p>
<h2 id="브루트-포스-python-구현">브루트 포스: Python 구현</h2>
<p>브루트포스를 이용해서 매칭된 횟수와 매칭이 된 위치를 반환하는 
패턴매칭 함수를 python으로 구현하면 아래와 같습니다.</p>
<pre><code class="language-python">def brute_force(target, pattern):

    t_idx, p_idx = 0,0  #전체 문자열의 인덱스, 패턴문자열의 인덱스 
    matched_idx = []    #패턴이 매칭된 전체문자열의 인덱스 값 저장하는 리스트
    cnt = 0             #매칭된 횟수 기록
    N, M = len(target), len(pattern)

    for idx in range(N-M+1):
        t_idx = idx

        #전체 문자열과 패턴이 일치하면 while문 실행행
        while target[t_idx] == pattern[p_idx]:
            if p_idx == M-1: 
                #패턴의 마지막 인덱스의 값까지 매칭되면 매칭이 시작된 전체문자열의 인덱스를 리스트에 저장
                matched_idx.append(t_idx)
                cnt += 1    #매칭된 횟수 +1
                break

            t_idx += 1  #전체문자열의 인덱스와 패턴의 인덱스 모두 +1
            p_idx += 1

        p_idx = 0   #매칭되지 않았다면 패턴의 인덱스값을 0으로 초기화화

    return cnt, matched_idx

print(brute_force(&quot;OOIOIOIOIIOII&quot;,&quot;IOI&quot;))
</code></pre>
<pre><code>#출력
(4, [2, 4, 6, 9])</code></pre><h1 id="kmp알고리즘">KMP알고리즘</h1>
<p><strong>KMP(Knuth–Morris–Pratt)</strong>는 별뜻이 있는건 아니고 단순히 만든 사람들 이름을 따온겁니다.  <del>like 장덕철</del></p>
<p>KMP는 패턴의 접두사(prefix)와 접미사(suffix)의 개념을 활용해서 패턴매칭이 이루어집니다.
패턴내에서 접두사와 접미사가 동일한 부분이 있다면 중간에 매칭이 실패했을 때 접미사를 접두사로 봄으로써 반복되는 연산을 접두사와 접미사가 같은 만큼 건너뛸 수 있게 됩니다.</p>
<blockquote>
<p>핵심은 매칭이 실패하였을 때 패턴의 처음으로 돌아가는 것이 아니라(브루트포스)
<strong>접미사를 접두사로 생각하고 이 둘이 같은 부분부터 다시 패턴매칭을 시작하는 것 입니다.</strong></p>
</blockquote>
<p>저도 그랬듯이 글로는 이게 무슨 소린가 싶을 수 있습니다. 정상입니다.
우선 KMP에서 핵심적인 개념인 <strong>접두사</strong>와 <strong>접미사</strong>에 관해 아래에서 설명하겠습니다.</p>
<h2 id="접두사prefix와-접미사suffix">접두사(prefix)와 접미사(suffix)</h2>
<p>접두사와 접미사의 개념을 어렵지 않습니다. 지금 여러분 머리 속에 떠오른 그것들 입니다.</p>
<blockquote>
<ul>
<li><strong>접두사 : 문자열의 앞 쪽 부분문자열</strong></li>
</ul>
</blockquote>
<ul>
<li><strong>접미사 : 문자열의 뒤 쪽 부분문자열</strong></li>
</ul>
<p><strong>&quot;banana&quot;</strong> 라는 문자열로 예를 들면</p>
<table>
<thead>
<tr>
<th align="center"><strong>길이</strong></th>
<th><strong>접두사</strong></th>
<th><strong>접미사</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>1</strong></td>
<td>b</td>
<td>a</td>
</tr>
<tr>
<td align="center"><strong>2</strong></td>
<td>ba</td>
<td>na</td>
</tr>
<tr>
<td align="center"><strong>3</strong></td>
<td>ban</td>
<td>ana</td>
</tr>
<tr>
<td align="center"><strong>4</strong></td>
<td>bana</td>
<td>nana</td>
</tr>
<tr>
<td align="center"><strong>5</strong></td>
<td>banan</td>
<td>anana</td>
</tr>
<tr>
<td align="center"><strong>6</strong></td>
<td>banana</td>
<td>banana</td>
</tr>
<tr>
<td align="center">참쉽죠?</td>
<td></td>
<td></td>
</tr>
<tr>
<td align="center"><br></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h2 id="lps">LPS</h2>
<p>LPS는 <strong>Longest Prefix which is also Suffix</strong> 입니다. 
가장 긴 접미사이기도 한 접두사?</p>
<p>말 참 어렵게 만들어 놨는데 이해하기 쉽게 말하면 아래와 같습니다.</p>
<blockquote>
<p><strong>문자열의 접두사와 접미사가 동일한 가장 긴 길이</strong></p>
</blockquote>
<p>KMP 알고리즘은 패턴의 <strong>0번째 인덱스부터 하나씩 길이를 늘려가며 만든 부분문자열의 LPS값을 활용</strong>합니다. LPS값을 구하는 과정에서 접두사와 접미사가 겹치는 경우(overlapping)도 포함합니다.</p>
<p>이를 <strong>&quot;ABCABABC&quot;</strong> 문자열로 예를 들어 설명 하겠습니다.
당연하게도 접두사나 접미사가 부분문자열 전체와 일치하는 경우는 포함하지 않습니다.</p>
<ul>
<li><p>첫 번째 부분 문자열: <strong>A</strong>
여기선 접두사가 A, 접미사도 A입니다. 
하지만 부분 문자열 전체와 일치하므로 <strong>LPS는 0</strong>입니다.</p>
</li>
<li><p>두 번째 부분 문자열: <strong>AB</strong>
접두사: A, AB
접미사: B, AB
전체와 일치하는 접두사와 접미사를 제외하기에 <strong>LPS는 0</strong>입니다.</p>
</li>
<li><p>세 번째 부분 문자열: <strong>ABC</strong>
접두사: A, AB, ABC
접미사: C, BC, ABC
위와 동일한 논리로 <strong>LPS는 0</strong>입니다.</p>
</li>
<li><p>네 번째 부분 문자열: <strong>ABCA</strong>
접두사: A, AB, ABC, ABCA
접미사: A, CA, BCA, ABCA
길이가 1인 접미사와 접두사가 일치함으로 <strong>LPS는 1</strong>입니다.</p>
</li>
</ul>
<p>위와 같은 단계를 패턴과 부분문자열이 같을때 까지 진행합니다.</p>
<table>
<thead>
<tr>
<th align="center"><strong>index</strong></th>
<th align="center"><strong>부분문자열</strong></th>
<th align="center"><strong>LPS</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>0</strong></td>
<td align="center">A</td>
<td align="center">0</td>
</tr>
<tr>
<td align="center"><strong>1</strong></td>
<td align="center">AB</td>
<td align="center">0</td>
</tr>
<tr>
<td align="center"><strong>2</strong></td>
<td align="center">ABC</td>
<td align="center">0</td>
</tr>
<tr>
<td align="center"><strong>3</strong></td>
<td align="center"><span style="color: Blue">A</span>BC<span style="color: Blue">A</span></td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>4</strong></td>
<td align="center"><span style="color: Blue">AB</span>C<span style="color: Blue">AB</span></td>
<td align="center">2</td>
</tr>
<tr>
<td align="center"><strong>5</strong></td>
<td align="center"><span style="color: Blue">A</span>BCAB<span style="color: Blue">A</span></td>
<td align="center">1</td>
</tr>
<tr>
<td align="center"><strong>6</strong></td>
<td align="center"><span style="color: Blue">AB</span>CAB<span style="color: Blue">AB</span></td>
<td align="center">2</td>
</tr>
<tr>
<td align="center"><strong>7</strong></td>
<td align="center"><span style="color: Blue">ABC</span>AB<span style="color: Blue">ABC</span></td>
<td align="center">3</td>
</tr>
</tbody></table>
<p>구한 LPS를 배열로 나타내면 아래와 같습니다.</p>
<p><strong>[0, 0, 0, 1, 2, 1, 2, 3]</strong></p>
<p>이렇게 구해진 LPS의 배열을 <strong>prefix table</strong>혹은 <strong>pi(파이)</strong>라고 합니다.
이글에선 pi라고 하겠습니다.</p>
<p>참고로 이런 LPS를 구하는 것을 <strong>Prefix Function</strong>이나 <strong>Failure Function</strong>이라고 합니다.</p>
<h3 id="lps-구현">LPS: 구현</h3>
<p>자 이제 LPS가 무엇인지 알았으니 구현을 해봐야겠죠. 저에게는 이것이 KMP고난의 시작이었습니다.</p>
<p>먼저 코드부터 보고 설명하겠습니다.</p>
<pre><code class="language-python">def lps(pattern):
    pi = [0 for _ in range(len(pattern))]

    j = 0   #if 접두사와 접미사가 같다면, 접두사의 끝 인덱스 =&gt; lps값

    for i in range(1, len(pattern)):    #i = 접미사의 첫 인덱스
        while j &gt; 0 and pattern[j] != pattern[i]:
            j = pi[j-1]

        if pattern[i] == pattern[j]:
            j += 1
            pi[i] = j

    return pi  

print(lps(&quot;ABCABABC&quot;))</code></pre>
<pre><code>#출력
[0, 0, 0, 1, 2, 1, 2, 3]</code></pre><p>위 코드의 큰 흐름은 다음과 같습니다.</p>
<blockquote>
<ul>
<li><code>j</code>는 0에서 시작, <code>i</code>는 1에서 시작하여 비교</li>
</ul>
</blockquote>
<ul>
<li><code>pattern[j]</code>와 <code>pattern[i]</code>가 같다면 <code>j</code>가 1증가하고 <code>pi[i]</code>에 <code>j</code>값 저장 후 <code>i</code> 1증가</li>
<li><code>j</code>가 0보다 크고 <code>pattern[j]</code>와 <code>pattern[i]</code>가 다르면 <code>j</code>를 <code>pi[j-1]</code>으로 이동
(<code>j</code>가 0이면 <code>i</code>만 1씩 증가)</li>
</ul>
<p>여기서 <code>j</code>는 접두사와 접미사가 같다는 가정하에 접두사의 맨 마지막 인덱스 값입니다.
즉 접두사와 접미사가 같다면 <code>j</code>은 곧 lps값이 됩니다.</p>
<p><code>i</code>는 접미사의 맨 처음 인덱스입니다.</p>
<p>아래서부터 단계별로 그림을 통해서 설명해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/70ff7605-bc88-4d35-87ed-a396c7b48751/image.png" alt=""></p>
<p><code>pattern[j]</code>와 <code>pattern[i]</code>가 일치하지 않으므로 해당 부분 문자열의 LPS는 0이다.
<code>j</code>가 0이므로 <code>i</code>만 1증가 시킵니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/645eba7b-8cfc-49d6-bfbb-8a2b6f06e760/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/96b5a3d9-eae4-4aa7-b25f-2a10ede80c75/image.png" alt=""></p>
<p><code>pattern[j]</code>와 <code>pattern[i]</code>가 일치하므로 <code>j</code>를 1증가시키고 <code>pi[i]</code>값이 <code>j</code>가 되어 LPS는 1이됩니다. 이후 <code>i</code>또한 1증가시켜 다음 부분문자열을 비교합니다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/tech_bae/post/a38f494b-35e7-4a48-aa36-da9eec1b7691/image.png" alt="">
<code>pattern[j]</code>와 <code>pattern[i]</code>가 다시 한 번 일치하므로 <code>j</code>를 1증가시키고 <code>pi[i]</code>값이 <code>j</code>가 되어 LPS는 2가 됩니다. 이후 <code>i</code>도 1증가하여 다음을 비교합니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/0073fff7-1d74-472b-b961-76736bb0d062/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/55caaf58-f5a8-4fe7-91b3-eaf79ce6e46b/image.png" alt=""></p>
<p>또 다시 일치하므로 위와 같은 단계를 거쳐 LPS는 3이 됩니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/d093064e-ec0b-4e4f-a805-1e822877a9bf/image.png" alt="">
다시 <code>i</code>를 1증가시켜 비교하니 이번엔 <code>pattern[j]</code>와 <code>pattern[i]</code>가 일치하지 않으므로 
<code>j</code>를 <code>pi[j-1]</code>인 <code>pi[2]</code> 즉 1로 이동시킵니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/32a273d1-674b-4f49-86f6-b1d15f342d7c/image.png" alt=""></p>
<p>이동시켰음에도 불구하고 또다시 일치하지 않으므로 <code>j</code>를 또다시 <code>pi[j-1]</code>로 이동시킵니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/e27cc6ce-91db-4d50-ba4a-1749fe1077f9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/6b875e51-2d66-4c4a-a419-34518371d4df/image.png" alt=""></p>
<p>이제는 일치하므로 앞에서와 같이 <code>j</code>를 1증가시키고 <code>j</code>가 LPS값이 되니 <code>pi[i]</code>에 <code>j</code>을 저장합니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/4deebe51-d5aa-48fe-9a95-a71f4873b6c9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/89c705f4-db23-43b9-abdf-db73c17e2bd9/image.png" alt=""></p>
<p>다음도 일치하므로 <code>j</code>값을 1증가시킨뒤 이 <code>j</code>가 LPS값이 되어 저장됩니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/e6ca94d5-83dc-4acf-bb32-36a647bf0d83/image.png" alt="">
값이 일치하지 않으므로 <code>j</code>가 <code>pi[j-1]</code>로 이동지만 마지막 인덱스에서의 비교가 끝났으므로 함수를 종료한다.</p>
<br>

<p>위에서 그림을 통해 LPS를 구하는 논리에 대해서 알아봤습니다. 전체적인 흐름은 파악하실 수 있으셨을 겁니다.</p>
<p>근데 저는 개인적으로 일치하지 않을 때 갑자기 <code>j</code>가 <code>pi[j-1]</code>이 되는게 이해가 잘 안되서 자괴감에 빠졌었습니다. 왜 그러한지 제가 더 자세히 설명을 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/ea742629-aef1-4142-a036-6133f060ec5a/image.png" alt=""></p>
<p>위 그림과 같이 이전 부분 문자열의 LPS가 N인 상태에서 ★과 ♥가 일치하지 않은 경우를 예로 들면</p>
<p><code>j</code>이전의 N길이의 접두사와 <code>i</code>이전에 N길이의 접미사가 같다는 것을 뜻하므로 <strong><code>pi[i-1]</code> = N</strong>입니다. 
동시에 <strong><code>j</code>의 인덱스 값은 N</strong>이기 때문에 (index는 0부터 시작이므로)
<strong>j = pi[i-1]</strong>이라는 논리가 성립합니다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/tech_bae/post/6a39654c-5dab-4145-96c7-607792e01b0a/image.png" alt=""></p>
<p>위 와 같은 논리로 <strong><code>pi[j-1]</code>이 M</strong>이라면 위 그림의 초록색 부분은 모두 같은 문자열입니다.
고로 위 그림에서 화살표시된 곳의 인덱스 값은 M이며 <strong><code>pi[j-1]</code></strong>입니다.</p>
<blockquote>
<p><strong><code>i</code>앞쪽의 M만큼의 문자열과 <code>pi[j-1]</code>앞의 M만큼의 문자열이 동일 하므로 <code>j</code>의 위치를 이곳으로 이동시켜 비교합니다.</strong></p>
</blockquote>
<p>정리하면 동일함이 보장되는 곳을 최대한 찾아 활용함으로써 쓸데없는 반복을 줄여 파이배열을 구할 수 있습니다.</p>
<p>Prefix function은 <code>i</code>가 1씩 전진만 하며 비교를 진행하기 때문에 시간 복잡도가 패턴의 길이를 N이라고 했을 때 O(N)입니다.</p>
<h3 id="pi배열의-특성">PI배열의 특성</h3>
<p>참고로 알아두면 좋을 거 같아서 소개하는 pi배열의 특성입니다.
소개를 안하기엔 중요한 정보인거 같아서 짧게 소개하겠습니다.</p>
<blockquote>
<p><strong>n - pi[n-1]은 반복패턴의 길이 입니다.(n = 문자열의 길이)</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/d4fd924e-6183-4a9c-b460-00eec7253f53/image.png" alt=""></p>
<p>위 그림의 n은 7입니다.
n - pi[n-1] = 7 - pi[6] = 2
그림에서 확인 할 수 있듯이 반복패턴의 길이가 2인것을 알 수 있습니다. (&#39;AB&#39;&#39;AB&#39;AB&#39;)</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/503cf2d8-9fdc-4419-a0be-42e348035ef4/image.png" alt=""></p>
<p>위의 예와 마찬가지로 n - pi[n-1]이 3이므로 반복패턴의 길이가 3인것을 알 수 있습니다.</p>
<p>원리를 간단히 그림으로 설명해보겠습니다.
<img src="https://velog.velcdn.com/images/tech_bae/post/3ab86487-2d86-468e-9464-6110e60aac84/image.png" alt=""></p>
<p>문자열의 길이인 n의 pi[n-1]값은 문자열의 마지막 LPS값을 뜻합니다.</p>
<p>이는 LPS만큼만 접두사와 접미사가 같다는 것을 의미하므로 길이인 n에서 이를 빼게 되면 전체문자열의 반복패턴의 길이가 나오는 것입니다.</p>
<p>이러한 특성은 KMP알고리즘과는 상관없는 내용이지만 유익한 정보인거 같아 간단히 소개 해보았습니다.</p>
<h2 id="kmp-알고리즘-구현">KMP 알고리즘: 구현</h2>
<p>위에서 KMP을 이해하기위해 접두사, 접미사의 개념과 LPS의 개념 마지막으로 pi배열의 원리과 구현까지 알아보았습니다.
이제 드디어 KMP를 구현해보고 자세히 뜯어볼 차례입니다. 참 오래 걸렸죠.
사실 KMP는 pi배열의 원리과 매우 비슷합니다.</p>
<p><strong>문자열이 패턴과 일치하지 않는다면 접두사와(prefix)와 접미사(suffix)가 같은 길이만큼 건너뛰어서 매칭을 진행</strong>시키기 때문입니다.</p>
<p>KMP알고리즘의 전체적인 흐름은 다음과 같습니다.</p>
<blockquote>
<ul>
<li><code>j</code>(패턴 index), <code>i</code>(전체문자열 index)를 0,1로 초기화</li>
</ul>
</blockquote>
<ul>
<li><code>pattern[j]</code> 와 <code>전체문자열[i]</code>를 비교하여 일치하면 <code>j</code>와 <code>i</code>를 1증가 
만약, <code>j</code>의 값이 <code>len(pattern)</code>과 같아지면 패턴을 찾은것이므로 이를 저장</li>
<li><code>pattern[j]</code>와 <code>pattern[i]</code>가 다르다면 <code>j</code>를 <code>pi[j-1]</code>로 이동</li>
</ul>
<p>pi배열의 구현과 매우 흡사하지 않습니까. 사실 pi배열 구현의 원리과 KMP는 같은 원리라해도 무방합니다.</p>
<p>이제 코드를 봅시다.</p>
<pre><code class="language-python">def lps(pattern):
    pi = [0 for _ in range(len(pattern))]

    j = 0   #if 접두사와 접미사가 같다면, 접두사의 끝 인덱스 =&gt; lps값

    for i in range(1, len(pattern)):    #i = 접미사의 첫 인덱스
        while j &gt; 0 and pattern[j] != pattern[i]:
            j = pi[j-1]

        if pattern[i] == pattern[j]:
            j += 1
            pi[i] = j

    return pi  

def KMP(target, pattern):
    pi = lps(pattern)   #lps함수로 pi배열 생성성

    match_point = []
    cnt = 0

    j = 0
    for i in range(len(target)):
        while j &gt; 0 and pattern[j] != target[i]:   #j가 0보다 크고 불일치하다면 j를 pi[j-1]로 이동
            j = pi[j-1]

        if pattern[j] == target[i]: #일치한다면 j와 i를 +1
            j += 1
            if j == len(pattern):   #j가 패턴의 길이가 된다면 패턴을 찾았다는 것을 의미하므로 이를 저장장
                cnt += 1
                match_point.append(i-len(pattern)+1)
                j = pi[j-1] #매칭되면 j를 pi[j-1]로 이동하여 계속 매칭시도도


    print(cnt)
    print(match_point)

KMP(&quot;OOIOIOIIOIOII&quot;,&quot;IOIOI&quot;)</code></pre>
<pre><code>#출력
2
[2, 7]</code></pre><p>자 이제 하나씩 뜯어봅시다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/3f713c2d-129f-4ea4-946e-049c2ed7592d/image.png" alt=""></p>
<p>처음부터 일치하지 않고 <code>j</code>가 0이므로 <code>i</code>만 1 증가시킵니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/623bcf89-00ae-4abf-b128-6dcf37a10ab3/image.png" alt=""></p>
<p>이번에도 일치하지 않는군요. 아직 <code>j</code>가 0이므로 마찬가지로 <code>i</code>만 1 증가시켜줍니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/444d425d-d816-4fcb-b68b-8a2b52c140fa/image.png" alt=""></p>
<p>드디어 일치하는군요! 일치하므로 <code>i</code>와 <code>j</code> 모두 1씩 증가시켜 계속 비교합니다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/tech_bae/post/8418a19b-03e9-43ff-a0f1-9da98a23faa3/image.png" alt=""></p>
<p>마찬가지로 일치하므로 위와 같이 <code>i</code>, <code>j</code> 1증가 시켜줍시다.
사실 컴퓨터는 아직 모르지만 우리는 그림으로 패턴이 계속 일치해서 target문자열 인덱스 2에서 매칭되는 걸 미리 알 수 있습니다. 그러므로 매칭이 될때까지는 별도의 설명없이 그림만 첨부하도록 하겠습니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/b58255aa-c2d4-4482-b8d3-f32e25aa9a85/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/52b9ca12-41b3-41a5-9174-cc5759a26684/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/907ff65f-c101-4faa-8765-912d6a576825/image.png" alt=""></p>
<p>계속 일치하면서 <code>j</code>가 패턴의 마지막 인덱스위치까지 왔으므로 패턴이 매칭이 되었음을 알 수 있습니다. 그럼 매칭이 되었다는 것을 기록하고 계속 비교를 해야겠죠?
그렇다면 <code>j</code>를 어디로 이동시키고 비교를 해야 시간을 아낄 수 있을까요
바로 우리가 앞서 만든 <strong>pi배열</strong>을 사용하면 됩니다.</p>
<p>지금 우리는 <code>i</code>가 6인 지점에서 매칭을 마쳤습니다. 다음엔 <code>i</code>가 7인 지점을 비교를 해야합니다. 이 상태에서 <code>j</code>를 0으로 이동시키고 비교를 하는 것은 브루트 포스입니다.</p>
<p>하지만 우리는 <strong>LPS을 기록한 pi배열</strong>이 있습니다. tartget[4<del>6]는 패턴의 접미사로 생각할 수 있습니다. (<code>i</code>가 6에서 매칭되었으므로) 현재 <code>j</code>위치의 LPS는 3입니다. 이는 길이가 3인 접두사와 접미사가 같다는 것을 뜻합니다. 그러므로 패턴의 접두사와 target[4</del>6]이 같기에 <strong>같은 부분은 생략</strong>하고 비교를 진행해야 합니다. (그림에서 연두색으로 칠해진 곳이 같다.)</p>
<p>그래서 <code>j</code>는 <code>pi[j]</code>로 이동합니다.
매칭이 되지 않는 경우에 <code>j</code>의 위치를 이동할때도 위와 완전히 똑같은 논리가 적용됩니다.
(pi배열을 만들때와도 매우 흡사합니다.)</p>
<p>이어서 진행해 봅시다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/5f0eb627-8582-4627-8498-18346cf209ec/image.png" alt=""></p>
<p><code>j</code>를 이동했음에도 일치하지 않습니다. 
현재 <code>j</code>는 3이고 <code>i</code>가 7인 상태에서 불일치가 발생했으므로, <code>pattern[0~2]</code>와 <code>target[4~6]</code>은 같았다는 것을 알 수 있습니다. 또한 <code>patter[0~2]</code>의 LPS는 1이기에 <code>pattern[0]</code>과 <code>target[6]</code>이 일치한다는 것을 내포합니다.(<code>target[6]</code> = 접미사, <code>pattern[0]</code> = 접두사)</p>
<p>그러므로 <code>j</code>를 <code>pi[j-1]</code>인 1로 이동시켜 비교를 계속합니다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/tech_bae/post/568c2ced-e0a2-4ef8-9c8a-388a06fc584d/image.png" alt=""></p>
<p>또 같지 않으므로 <code>j</code>를 <code>pi[j-1]</code>인 0으로 이동시킵니다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/tech_bae/post/f9eeba00-e86c-4483-99ec-56d4f0d0893e/image.png" alt=""></p>
<p>일치함으로 <code>j</code>와 <code>i</code>를 1씩 증가시킵니다.</p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/b4f137e7-dfba-4fb1-b7c1-2cc2b211d74a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/77582eb4-2238-461e-9504-b42aacdf9390/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/4478776d-f723-4ee1-a95f-6c41b6a0eb88/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/tech_bae/post/3e83865e-352d-405d-afeb-4e52d88b1418/image.png" alt=""></p>
<p><code>j</code>가 <code>pattern</code>의 마지막 인덱스 위치에 도달하고 문자열이 일치함으로, 패턴이 매칭되었음을 알 수 있습니다.
위에서 설명한대로 색깔칠해진 부분이 같기때문에 <code>j</code>를 <code>pi[j]</code>인 3으로 이동시켜 계속 진행합니다.</p>
<br> 

<p><img src="https://velog.velcdn.com/images/tech_bae/post/bbf1efd4-eb95-4aae-9305-d15cb37c4b55/image.png" alt=""></p>
<p>불일치 했지만 <code>i</code>가 문자열의 마지막에 도달했으므로 패턴매칭을 종료합니다.</p>
<h2 id="kmp-알고리즘-정리">KMP 알고리즘: 정리</h2>
<p>KMP는 접두사(prefix)와 접미사(suffix)가 같다면 이를 활용해서 의미없는 반복을 줄이는 패턴매칭 알고리즘입니다. 브루트포스의 시간 복잡도가 O(N<em>M)인거에 비해 *</em>KMP는 O(N+M)의 시간복잡도**를 가지고 있습니다. (pi배열 만드는 시간복잡도 O(N) + KMP패턴매칭의 시간복잡도 O(M))</p>
<p>하지만 KMP는 찾을 패턴의 접두사와 접미사가 같은 부분이 있어야 효율이 나는 알고리즘입니다. 이말은 접두사와 접미사가 같은게 없다면 사실상 브루트포스와 다를게 없다는 소리입니다. 하지만 KMP의 패턴매칭 알고리즘은 그 원리에 있어서 다소 신박한 부분이 있기 때문에 이해해두면 신박한 문제나 상황에선 유용할 거 같습니다.</p>
<h2 id="느낀점">느낀점</h2>
<p>패턴매칭에 대해서 아무것도 몰랐기에 KMP는 저의 첫 패턴매칭 알고리즘이었습니다. 그래서인지 이를 이해하는데 생각보다 많은 시간이 걸렸지만 결국 이해를 완전히 한거 같아서 너무나 기쁩니다. 이 과정에서 문자열을 이런식으로도 접근할 수 있구나를 느꼈고, 이 글을 쓰면서 pi배열을 만드는 과정과 KMP의 과정을 일일히 한 단계씩 그림을 그려봤던 것이 설명을 위한 것이었음에도 불구하고 누구보다 저에게 정말 큰 도움이 되었던 것 같습니다. 앞으로도 뭔가가 이해가 안된다면 그 과정을 성가시지만 일일히 그려보는 것도 좋을 것 같습니다. 마지막으로 이 긴글 읽으신 분들이 계시다면 정말 감사합니다. 혹시 오탈자다 글의 오류가 있다면 언제든지 알려주시면 저에게 큰 도움이 됩니다!</p>
<h1 id="references">References</h1>
<ul>
<li><p>[String] String searching #1 - prefix function &amp; KMP (재녹음)
<a href="https://www.youtube.com/watch?v=RHGfn52cHHw&amp;list=WL&amp;index=2&amp;t=1015s">https://www.youtube.com/watch?v=RHGfn52cHHw&amp;list=WL&amp;index=2&amp;t=1015s</a> [개발자 영맨(bluedawnstar)]</p>
</li>
<li><p>[알고리즘] 패턴 매칭 알고리즘,  KMP 알고리즘 ( Knuth–Morris–Pratt Algorithm )
<a href="https://ymg5218.tistory.com/152">https://ymg5218.tistory.com/152</a> [민규의 흔적:티스토리]</p>
</li>
<li><p>[알고리즘/ 파이썬] KMP 알고리즘? 그림으로 쉽게 이해하기!
<a href="https://yiyj1030.tistory.com/495">https://yiyj1030.tistory.com/495</a> [컴퓨터 지식, 이렇게 쉽게!(ggyongi)]</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] sort와 sorted]]></title>
            <link>https://velog.io/@tech_bae/Python-sort%EC%99%80-sorted</link>
            <guid>https://velog.io/@tech_bae/Python-sort%EC%99%80-sorted</guid>
            <pubDate>Wed, 25 Dec 2024 17:57:16 GMT</pubDate>
            <description><![CDATA[<p>백준문제를 풀때 정렬을 위해 sort()나 sorted() 사용할 일이 있다. 그 때마다 스스로 이 둘의 차이점과 사용법을 제대로 알지 못하는 거 같아서 이를 정리하여 게시해놓으려고 한다.</p>
<h2 id="sort">sort()</h2>
<p>sort()의 기본문법은 아래와 같습니다.</p>
<pre><code class="language-python">&lt;list&gt;.sort(key = &lt;function&gt;, reverse = &lt;bool&gt;</code></pre>
<blockquote>
<ul>
<li>sort()는 기존의 <code>list</code>를 정렬합니다.</li>
</ul>
</blockquote>
<ul>
<li><code>list</code>에만 사용 할 수 있습니다.</li>
<li><code>key</code>를 통해 정렬기준을 정합니다.</li>
<li><code>reverse</code>값이 <code>True</code>면 내림차순, <code>False</code>은 오름차순입니다.
(기본값은 <code>False</code>)</li>
</ul>
<pre><code class="language-python">nums = [13,2,11,5,4,7,1]
nums.sort()
print(&quot;오름차순&quot;,nums)

nums.sort(reverse = True)
print(&quot;내림차순&quot;,nums)</code></pre>
<pre><code>#출력
오름차순 [1, 2, 4, 5, 7, 11, 13]
내림차순 [13, 11, 7, 5, 4, 2, 1]</code></pre><p>위 코드에서 보이듯 sort()를 사용하면 원본 <code>list</code>가 정렬되어 수정됩니다.</p>
<br>

<h2 id="sorted">sorted()</h2>
<p>sorted()의 기본문법은 아래와 같습니다.</p>
<pre><code class="language-python">sorted(&lt;iterable&gt;, key = &lt;function&gt;, reverse = &lt;bool&gt;)</code></pre>
<blockquote>
<ul>
<li>어떠한 <code>iterable</code>을 정렬된 <code>list</code>로 새로 만듭니다. (새로 정의 or 출력 필요)</li>
</ul>
</blockquote>
<ul>
<li>모든 <code>iterable</code>에 사용이 가능합니다.</li>
<li><code>key</code>와 <code>reverse</code>의 기능은 sort()의 것과 동일합니다.</li>
</ul>
<pre><code class="language-python">nums = [13,2,11,5,4,7,1]
sorted_nums = sorted(nums)

print(&quot;sorted: &quot;,sorted_nums)
print(&quot;original: &quot;,nums)
</code></pre>
<pre><code>#출력
sorted:  [1, 2, 4, 5, 7, 11, 13]
original:  [13, 2, 11, 5, 4, 7, 1]</code></pre><p>위와 같이 sorted()를 사용하면 새로운 정렬된 <code>list</code>가 생성되는 것을 알 수 있습니다.
sorted()는 sort()와 다르게 <code>list</code>뿐만아니라 모든 <code>iterable</code>에 사용이 가능합니다.</p>
<pre><code class="language-python">_dict = {5:&#39;A&#39;, 13:&quot;E&quot;, 1:&quot;B&quot;, 3:&quot;G&quot;, 12:&quot;C&quot;}
_set = set([5,13,1,3,12])

sorted_dict = sorted(_dict)
sorted_set = sorted(_set)

print(&quot;dict: &quot;,sorted_dict)
print(&quot;set: &quot;,sorted_set)</code></pre>
<pre><code>#출력
dict:  [1, 3, 5, 12, 13]
set:  [1, 3, 5, 12, 13]</code></pre><br>

<h2 id="key-매개변수">key 매개변수</h2>
<p>sort()와 sorted()모두 <code>key</code>매개변수를 사용하여 정렬 기준을 세울 수 있습니다.
<code>key</code>매개 변수에는 함수가 입력되어아하며 이 함수에 따라 정렬의 기준이 달라집니다.
보통 <code>lambda</code>를 많이 이용합니다. </p>
<p><code>lambda</code>를 주로 많이 사용하지만 이를 사용하지 않는 예부터 살펴보겠습니다.</p>
<pre><code class="language-python">disorder = [&quot;i&#39;m&quot;,&quot;so&quot;,&quot;sorry&quot;,&quot;but&quot;,&quot;i&quot;,&quot;love&quot;,&quot;you&quot;]

disorder.sort(key = len)

print(disorder)</code></pre>
<pre><code>#출력
[&#39;i&#39;, &#39;so&#39;, &quot;i&#39;m&quot;, &#39;but&#39;, &#39;you&#39;, &#39;love&#39;, &#39;sorry&#39;]</code></pre><p>위와 같이 <code>key</code>에 <code>len</code>을 사용하면 글자 수를 기준으로 오름차순 되어 정렬됩니다.
<code>len</code>함수에 정렬할 리스트의 요소들이 하나씩 들어가 반환되는 값을 기준으로 정렬이 되는 것입니다.</p>
<h3 id="lambda-사용하기">lambda 사용하기</h3>
<p>key매개변수는 함수를 입력해야하므로 <code>lambda</code>를 사용해 정렬기준을 정할 수 있습니다.</p>
<pre><code class="language-python">student = {&quot;Kim&quot;:80, &quot;Lee&quot;: 70, &quot;Bae&quot;: 15, &quot;Park&quot;: 60, &quot;Oh&quot;: 0}

sorted_student_1 = sorted(student, key = lambda x : student[x])
sorted_student_2 = sorted(student.items(), key = lambda x : x[0]) #items는 (key,value)을 쌍으로 가져옵니다
sorted_student_3 = sorted(student.items(), key = lambda x : x[1])
reversed_student = sorted(student, key = lambda x : -student[x])

print(sorted_student_1)
print(sorted_student_2)
print(sorted_student_3)
print(reversed_student)</code></pre>
<pre><code>#출력
[&#39;Oh&#39;, &#39;Bae&#39;, &#39;Park&#39;, &#39;Lee&#39;, &#39;Kim&#39;]
[(&#39;Bae&#39;, 15), (&#39;Kim&#39;, 80), (&#39;Lee&#39;, 70), (&#39;Oh&#39;, 0), (&#39;Park&#39;, 60)]
[(&#39;Oh&#39;, 0), (&#39;Bae&#39;, 15), (&#39;Park&#39;, 60), (&#39;Lee&#39;, 70), (&#39;Kim&#39;, 80)]
[&#39;Kim&#39;, &#39;Lee&#39;, &#39;Park&#39;, &#39;Bae&#39;, &#39;Oh&#39;]</code></pre><ul>
<li><p>첫번째 예시는 <code>key = lambda x : student[x]</code>를 사용하여 딕셔너리의 value값을 비교하여 정렬된 key값을 가지는 리스트을 반환합니다.</p>
</li>
<li><p>두번째 예시는 <code>items()</code>를 사용하여 key와 value를 쌍으로 가져와 <code>key = lambda x : x[0]</code>을 통해 key값을 정렬기준으로 하고 이를 (key, value)리스트로 반환합니다.</p>
</li>
<li><p>세번째 예시는 두번째와 비슷하지만 <code>key = lambda x : x[1]</code>을 통해 value값을 정렬기준으로 하고 (key, value)리스트로 반환합니다.</p>
</li>
<li><p>마지막은 첫번째 예시에 <strong>마이너스 부호(-)</strong>를 사용하여 내림차순으로 정렬합니다.
(<code>reverse = True</code>와 동일)</p>
</li>
</ul>
<h3 id="다중조건-정렬">다중조건 정렬</h3>
<p>lambda를 이용해 정렬기준을 여러개로 설정할 수도 있습니다. </p>
<p>예를들어 나이와 이름이 기록된 명부 리스트가 있을때, 먼저 나이를 고려해서 정렬하고 만약 나이 같다면 이름 순대로 정렬해야 하는 경우 정렬기준을 튜플로 묶어 다음과 같이 설정하면 됩니다.</p>
<blockquote>
<p><strong><code>key = lambda x : (x[나이], x[이름])</code></strong></p>
</blockquote>
<pre><code class="language-python">people = {&quot;Kim&quot;:54, &quot;Lee&quot;: 25, &quot;Bae&quot;: 25, &quot;Park&quot;: 25, &quot;Oh&quot;: 15}

sorted_people = sorted(people.items() , key= lambda x : (x[1],x[0]))

print(sorted_people)

</code></pre>
<pre><code>#출력
[(&#39;Oh&#39;, 15), (&#39;Bae&#39;, 25), (&#39;Lee&#39;, 25), (&#39;Park&#39;, 25), (&#39;Kim&#39;, 54)]</code></pre><p>위 코드를 보면 나이를 기준으로 오름차순으로 정렬되어있고, 나이가 같은 <code>Bae</code>,<code>Lee</code>,<code>Park</code>는 이름을 기준으로 오름차순으로 정렬되었습니다.</p>
<h2 id="안정-정렬">안정 정렬</h2>
<blockquote>
<p><strong>안정정렬이란 같은 값을 정렬할때 기존 위치를 유지하여 정렬하는 것을 의미합니다.</strong></p>
</blockquote>
<img src="https://velog.velcdn.com/images%2Fgood159897%2Fpost%2Fda230c26-914f-4ae3-86d7-757a336d866d%2Fstable_sort.jpg" align='center'/>

<ul>
<li><a href="https://en.wikipedia.org/wiki/Sorting_algorithm#Stability">https://en.wikipedia.org/wiki/Sorting_algorithm#Stability</a></li>
</ul>
<p>위 그림처럼 정렬이 안정적(Stable)하면 정렬하기 전의 중복된 값들의 위치가 정렬 후에도 유지됩니다.
(불안정하다면 기존의 위치의 유지가 보장되지 않습니다.)</p>
<p><strong>sort()와 sorted()모두 정렬의 안정성(Stability)이 보장됩니다.</strong></p>
<p>마지막으로 위의 그림을 직접 코딩해보며 이 글을 마치겠습니다. 감사합니다.</p>
<pre><code class="language-python">cards = [(&quot;♠&quot;,7),[&quot;♥&quot;,5],[&quot;♥&quot;,2],[&quot;♠&quot;,5]]

cards.sort(key = lambda x : x[1])

print(cards)</code></pre>
<pre><code>#출력
[[&#39;♥&#39;, 2], [&#39;♥&#39;, 5], [&#39;♠&#39;, 5], (&#39;♠&#39;, 7)]</code></pre>]]></description>
        </item>
    </channel>
</rss>