<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>틀려도 일단 기록하자</title>
        <link>https://velog.io/</link>
        <description>틀려도 일단 기록하자</description>
        <lastBuildDate>Sun, 26 Nov 2023 09:19:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>틀려도 일단 기록하자</title>
            <url>https://velog.velcdn.com/images/xxx-sj/profile/8f1d547f-5edc-4942-aabd-c5bc7124a48a/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 틀려도 일단 기록하자. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/xxx-sj" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[docker 공부 시작하기 [spring-boot] run 과 start의 차이와 attach, detach에 대해서- (3)]]></title>
            <link>https://velog.io/@xxx-sj/docker-%EA%B3%B5%EB%B6%80-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-spring-boot-3</link>
            <guid>https://velog.io/@xxx-sj/docker-%EA%B3%B5%EB%B6%80-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-spring-boot-3</guid>
            <pubDate>Sun, 26 Nov 2023 09:19:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 전에는 <code>docker run</code>만 이용해서 컨테이너를 생성하고 실행했지만, <code>docker start</code>를 통해 종료된 컨테이너를 다시 <code>실행</code>시킬 수 있습니다.</p>
</blockquote>
<h2 id="📙종료된-컨테이너-다시-실행하기">📙종료된 컨테이너 다시 실행하기</h2>
<blockquote>
<p>종료시킨 컨테이너를 다시 실행하기 위해서는 <code>컨테이너 ID</code>를 갖고 다시 실행할 수 있습니다.
컨테이너 list를 찾는 명령어는 다음과 같다.</p>
<blockquote>
<p><code>docker ps -a</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/bc628470-d11b-4583-a12e-7ed8abf9180b/image.png" alt=""></p>
</blockquote>
</blockquote>
<blockquote>
<p>컨테이너를 다시 실행시키는 명령어는 다음과 같다.</p>
<blockquote>
<p><code>docker start [container-id / container-name]</code></p>
</blockquote>
</blockquote>
<blockquote>
<p>컨테이너를 처음 <code>run</code>을 통해 실행시켰을 때 주었던 옵션이 <code>start</code>를 통해 다시 실행했을 때 동일하게 주어진다.
<code>start</code>를 통해 컨테이너를 실행하게 되면, <code>run</code>때와는 다르게 터미널을 차단하지 않는다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/54d1b4f4-4740-44f8-aa7c-c0f3679569c7/image.png" alt=""></p>
<h3 id="📗-attach-detach">📗 attach, detach</h3>
<blockquote>
<p>여기서 말하는 <code>attach</code> 와 <code>detach</code>의 차이는 간단하게 생각하면 현재 실행중인 <code>컨테이너</code>에서의 출력 내용을 볼 수 있고 없고의 차이이다. <code>attach</code>모드는 볼 수 있고 반대로 <code>detach</code>는 볼 수 없다. </p>
</blockquote>
<blockquote>
<p><code>run</code>명령어를 통해 컨테이너를 생성하고 실행할 때는 <code>attach</code> 모드가 기본이 되며, <code>포그라운드</code>로 실행하게 된다. 따라서 이때는 <code>터미널</code>이 차단되게 된다. 
<img src="https://velog.velcdn.com/images/xxx-sj/post/0a9776ce-a97b-45d6-925d-9182458a792a/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>만약 <code>run</code>사용 시 <code>detach</code>모드로 실행하고 싶다면 <code>run</code> 명령어 사용 시 추가적으로 <code>-d</code> 옵션을 주면 된다.
<code>detach</code>로 실행하게 되면 <code>터미널</code>이 차단되지 않고 해당 <code>컨테이너</code>는 <code>백그라운드</code>로 실행되게 된다.</p>
<blockquote>
<p><code>docker run -d -p 8081:8081 [image-name]</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/f89cfd7e-ab08-404e-8a75-9c5efe88518f/image.png" alt=""></p>
</blockquote>
</blockquote>
<blockquote>
<p><code>start</code>명령어로 컨테이너를 실행하게 되면 기본적으로 <code>백그라운드</code> <code>detach</code>모드로 실행되게 된다. 이 때 만약 <code>attch</code>모드로 실행하고 싶다면 <code>-a</code>옵션을 추가하면 된다.</p>
<blockquote>
<p><code>docker start -a [container-name / container-id]</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/1b949f67-06f2-41ef-99bc-6f78e7b4b1b0/image.png" alt=""></p>
</blockquote>
</blockquote>
<blockquote>
<p>만약, <code>start</code>명령어로 컨테이너를 실행하였을 때 <code>attach</code>모드로 들어가고 싶다면 다음과 같이 사용할 수 있다.</p>
<blockquote>
<p><code>docker (container) attach [container-name / container-id]</code></p>
</blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/9f26d76d-e883-4714-900b-eea935d68884/image.png" alt=""></p>
<blockquote>
<p>추가적으로 <code>detach</code>모드로 실행되었지만 <code>로그</code>를 보고 싶다면 다음과 같이 사용하면 된다.</p>
<blockquote>
<p><code>docker logs (-f) [container-name / container-id]</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/73ac4f6a-5e72-48bb-a683-703b8bca0f46/image.png" alt="">
<code>-f</code> 옵션은 <code>fllow</code> 옵션이다.</p>
</blockquote>
</blockquote>
<blockquote>
<p>주의점은 이 전에 컨테이너를 종료하고 실행한 로그들이 다 보인다는 것..</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[docker 공부 시작하기 [spring-boot] 도커파일에 작성되는 명령어 알아보기- (2)]]></title>
            <link>https://velog.io/@xxx-sj/docker-%EA%B3%B5%EB%B6%80-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-spring-boot-2</link>
            <guid>https://velog.io/@xxx-sj/docker-%EA%B3%B5%EB%B6%80-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-spring-boot-2</guid>
            <pubDate>Wed, 22 Nov 2023 13:02:50 GMT</pubDate>
            <description><![CDATA[<h1 id="📕명령어-비교하기">📕명령어 비교하기</h1>
<blockquote>
<p>이 전 글을 작성하면서 비슷한 역할을 하는 명령어들이 있는 것 같아서 이 글에서는 해당 명령어들을 정리해보려 합니다.</p>
</blockquote>
<h2 id="📙-run-cmd-entrypoint">📙 RUN, CMD, ENTRYPOINT</h2>
<h3 id="📒run">📒RUN</h3>
<blockquote>
<p><code>RUN</code>은 <code>Dockerfile</code>에서 <code>image</code>로 <code>build</code>하는 순간 실행되는 명령어 입니다.
<code>RUN</code> 명령어는 라이브러리 설치 시 주로 사용됩니다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile
FROM ...
RUN npm install</code></pre>
<h3 id="📒cmd">📒CMD</h3>
<blockquote>
<p><code>CMD</code>는 <code>image</code>에서 <code>container</code>를 생성하여 실행할 시 수행됩니다. </p>
</blockquote>
<pre><code class="language-javascript">FROM openjdk:11-jdk
CMD [&quot;echo&quot;, &quot;CMD test&quot;]</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/3c04f5c1-0d27-4d96-a27b-44ae2442ae5d/image.png" alt=""></p>
<h3 id="📒entrypoint">📒ENTRYPOINT</h3>
<blockquote>
<p><code>CMD</code>와 비슷합니다. <code>container</code>가 생성되고 최초로 실행할 때 수행되는 명령어 입니다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
CMD [&quot;echo&quot;, &quot;CMD test&quot;]
ENTRYPOINT [&quot;echo&quot;, &quot;entry point test&quot;]</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/95ded3bb-8d82-48ce-aacd-a2fb017f1546/image.png" alt=""></p>
<blockquote>
<p>해당 파일을 실행해보면 실행순서는 <code>ENTRYPOINT</code> 다음 <code>CMD</code>가 실행되는 것 같다..</p>
</blockquote>
<h3 id="📒-cmd-entrypoint-차이">📒 CMD, ENTRYPOINT 차이</h3>
<blockquote>
<p>동일하게 명령어를 실행하는 것 같은데 차이점은 무엇일까?
<code>ENTRYPOINT</code>는 항상 실행이 되고, <code>CMD</code>는 <code>docker run</code> 할때 변경이 가능합니다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
CMD [&quot;echo&quot;, &quot;CMD test&quot;]
ENTRYPOINT [&quot;echo&quot;, &quot;entry point test&quot;]</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/5382e2ea-68e6-449a-9ab7-3506bc11b8e5/image.png" alt=""></p>
<blockquote>
<p>Dockerfile에 CMD를 정의했음에도 <code>docker run</code> 할 때 명령어를 추가적으로 추가해서 실행하니 <code>Dockerfile</code> 에 정의한 <code>CMD</code> 명령어가 override되는 것을 볼 수 있다 물론 <code>ENTRYPOINT</code> 도 <code>run</code> 할때 옵션으로 <code>override</code> 가능하다.</p>
</blockquote>
<h2 id="📙-copy-add">📙 COPY, ADD</h2>
<blockquote>
<p>둘 다 <code>Host OS</code>에서 <code>파일</code> 또는 <code>디렉토리</code>를 컨테이너 안으로 <code>복사</code>하는 것이다.</p>
</blockquote>
<blockquote>
<p><code>COPY</code>의 경우 <code>Host OS</code>에서 <code>컨테이너</code> 안으로 <code>복사만 가능</code>하지만, <code>ADD</code>의 경우 <code>원격 파일 다운로드</code> 또는 <code>압축 헤제</code> 등과 같은 기능을 갖고 있습니다. <code>Host OS</code>에서 <code>컨테이너</code>로 간단히 복사 시 <code>COPY</code>를 사용합니다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

COPY test.sh /app/copy/test.sh
ADD test.sh /app/add/test.sh</code></pre>
<blockquote>
<p>ADD 의 경우 URL을 통해 다운로드가 가능합니다.</p>
</blockquote>
<pre><code class="language-javascript">ADD http://.... /app/add/url/index.html</code></pre>
<h2 id="📙env-arg">📙ENV, ARG</h2>
<blockquote>
<p><code>ARG</code>는 <code>Dockerfile</code>에서만 사용이 가능하고, <code>ENV</code>는 <code>Dockerfile</code> 이외에도 <code>컨테이너</code>에서도 <code>환경변수</code>로 사용이 가능하다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

# ENV [key] [value]
ENV test test
ENV test2 test2

#ENV [key]=[value] 
ENV test=test
ENV test2=test2

#ARG
ARG test=test
ARG test2=test2</code></pre>
<blockquote>
<p>다음으로 작성한 <code>Dockerfile</code>로 빌드된 <code>이미지</code>를 가지고 <code>컨테이너</code>를 실행하면 다음과 같다.</p>
</blockquote>
<pre><code class="language-javascript">FROM openjdk:11-jdk
ARG test=&quot;test1111&quot;
ENV test2=&quot;test2222&quot;
CMD echo &quot;&gt;&gt;&gt; $test&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/e498b318-0c7d-4b51-bda6-bd99ea86bb98/image.png" alt=""></p>
<blockquote>
<p><code>CMD</code> 를 통해 <code>ARG</code> 변수를 출력해보면 아무것도 찍히지 않는것을 볼 수 있다. 즉, <code>ARG</code>는 <code>Dockerfile</code> 내에서만 사용되고 그 이후에는 사용되지 않는다.</p>
</blockquote>
<blockquote>
<p>다음으로는 동일하게 하되, <code>ENV</code> 변수의 값을 사용해서 출력해보겠습니다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
ARG test=&quot;test1111&quot;
ENV test2=&quot;test2222&quot;
CMD echo &quot;&gt;&gt;&gt;&gt; $test2&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/ba89bd7b-1d7e-497f-8403-ca1ca25ffebc/image.png" alt=""></p>
<blockquote>
<p><code>ARG</code>와는 반대로 출력이 잘 되는것을 볼 수 있습니다. 즉, <code>ENV</code>는 환경변수로 <code>컨테이너</code> 내에 <code>환경변수</code>로 저장되어있는 것을 볼 수 있습니다.</p>
</blockquote>
<h2 id="📙expose">📙EXPOSE</h2>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
ARG PATH=./build/libs
WORKDIR /app
EXPOSE 8081
COPY ${PATH}/docker-example-0.0.1-SNAPSHOT.jar ${PATH}/docker-example-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;./build/libs/docker-example-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<blockquote>
<p><code>EXPOSE</code>는 해당 <code>이미지</code>를 가지고 만든<code>컨테이너</code>는 <code>EXPOSE</code>에 명시된 포트를 <code>수신</code>할 것 이라는 것과 같습니다.
하지만 아무런 옵션을 주지 않고 <code>컨테이너</code>를 실행하게 되면 해당<code>포트</code>만 열려있을 뿐 <code>Host OS</code>와 바인딩되어있거나 하지 않습니다. 
<img src="https://velog.velcdn.com/images/xxx-sj/post/de0f76ab-f1ab-418a-a688-d40ee036b7ed/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>docker run</code> 명령어 실행 시 옵션으로 <code>-P</code>를 주게되면 <code>Host OS</code>에서 <code>랜덤 포트</code>가 <code>EXPOSE</code>에 명시한 <code>포트</code>와 <code>바인딩</code> 됩니다. </p>
<blockquote>
<p><code>docker run -P [image-name]</code></p>
</blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/2dc12686-1208-47c5-80ed-8cf81d8af3ea/image.png" alt=""></p>
<blockquote>
<p>기존에 알고있던 옵션 <code>-p [host-port]:[container-port]</code> 를 통해서도 <code>포트</code>를 <code>바인딩</code>할 수 있습니다. </p>
</blockquote>
<blockquote>
<p>두 개의 다른점은 <code>EXPOSE</code>로 명시한 <code>포트</code>같은 경우는 <code>-P</code> 옵션을 통해 <code>랜덤 포트</code>가 <code>바인딩</code> 된다는 점 이고, <code>-p [host-port]:[container-port]</code>를 통해 <code>바인딩</code>하는 경우는 <code>명시적</code>으로 <code>바인딩</code>할 수 있다는 점입니다.</p>
</blockquote>
<h2 id="📙추가적인-테스트">📙추가적인 테스트</h2>
<blockquote>
<p>만약 다음과 같은 경우에는 어떻게 될까?</p>
</blockquote>
<ul>
<li><code>EXPOSE</code>에 명시한 <code>포트</code>를 <code>-p []:[]</code>를 통해 바인딩하면 어떻게 될까?</li>
<li><code>EXPOSE</code>에 <code>포트</code>를 명시한 후 <code>-P</code> 옵션과 <code>-p []:[]</code>을 같이 주면 어떻게 될까?</li>
<li><code>EXPOSE</code>에 <code>포트</code>를 명시한 후 <code>-P</code>옵션과 <code>-p []:[]</code>옵션을 같이 사용하되, <code>-p []:[]</code> 옵션에서 다른 <code>포트</code>를 바인딩하면 어떻게 될까?</li>
</ul>
<h3 id="📒-expose와-같은-포트-바인딩해보기">📒 EXPOSE와 같은 포트 바인딩해보기</h3>
<blockquote>
<p><code>EXPOSE</code>에 명시한 <code>포트</code>와 같은 포트를 <code>-p</code> 옵션을 통해 바인딩하면 다음과 같습니다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile 

FROM openjdk:11-jdk
ARG PATH=./build/libs
WORKDIR /app
EXPOSE 8081
COPY ${PATH}/docker-example-0.0.1-SNAPSHOT.jar ${PATH}/docker-example-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;./build/libs/docker-example-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<blockquote>
<p><code>docker run -p 8081:8081 as</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/986578c9-919f-4182-9b80-768c7de28726/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>Host OS</code> <code>8081</code> 과 <code>컨테이너</code> <code>8081</code>과 바인딩 된 것을 볼 수 있다. </p>
</blockquote>
<h3 id="📒--p-옵션과--p-같이-사용하기">📒 -P 옵션과 -p 같이 사용하기</h3>
<blockquote>
<p><code>-P</code>옵션과 <code>-p []:[]</code>를 같이 사용하면 다음과 같습니다.</p>
</blockquote>
<blockquote>
<p><code>docker run -P -p 8081:8081 as</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/d1190a6c-0e59-4a3c-b1a9-5ad646789b44/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>위와 동일한 것을 볼 수 있습니다.</p>
</blockquote>
<h3 id="📒-그렇다면--p-옵션에서-다른-포트-바인딩-시엔">📒 그렇다면 -p 옵션에서 다른 포트 바인딩 시엔??</h3>
<blockquote>
<p>이번엔 <code>-P</code>와 <code>-p</code>를 같이 쓰되, <code>-p</code>옵션에서 다른 <code>컨테이너</code> <code>포트</code>를 바인딩 해보겠습니다.</p>
</blockquote>
<blockquote>
<p><code>docker run -P -p 10888:8088 as</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/a5a57bcd-8fcb-4894-896f-c98fb0754351/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>컨테이너</code> <code>포트</code>를 변경해서 바인딩할 경우, <code>-P</code> 옵션에 의해 <code>랜덤 포트</code>와 바인딩 되고, <code>-p</code> 옵션은 따로 <code>10888</code> 과 <code>8088</code>이 바인딩 된 것을 볼 수 있습니다.</p>
</blockquote>
<h2 id="📙volume-bind-mount">📙VOLUME, BIND MOUNT</h2>
<blockquote>
<p>Docker에서 <code>컨테이너</code>의<code>생명주기</code>와 관계없이 데이터를 영속적으로 저장을 해야합니다. 또는 많은 경우 여러 개의 <code>컨테이너</code>가 하나의 <code>저장 공간</code>을 공유해서 데이터를 읽거나 써야 합니다.
Docker는 이에 대해 2가지 옵션인<code>VOLUME</code> 과 <code>BIND MOUNT</code>를 제공합니다.</p>
</blockquote>
<h3 id="📒volume">📒VOLUME</h3>
<blockquote>
<p><code>docker volume ls</code>를 통해 생성된 <code>volume</code>의 리스트를 볼수 있습니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/7dc6e8c3-8f89-46cc-95a8-eddddf058a17/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>docker inspect [volume]</code> 을 통해 <code>volume</code>의 내용을 자세히 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/08da25f7-5b26-431c-b6cf-86ae0294bf37/image.png" alt=""></p>
</blockquote>
<h3 id="📒volume-생성하기">📒VOLUME 생성하기</h3>
<blockquote>
<p><code>VOLUME</code>생성 할 때는 <code>docker volume create [volume-name]</code> 으로 생성한다.</p>
<blockquote>
<p><code>docker volume create vol-t</code></p>
</blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/783babe4-1c1a-48e6-a036-2cdad6374584/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/c8dfae2b-a7d6-45c0-9cf8-9b0814bd5f72/image.png" alt=""></p>
<h3 id="📒컨테이너에-volume-마운트하기">📒컨테이너에 VOLUME 마운트하기</h3>
<blockquote>
<p>컨테이너가 볼륨을 사용하기 위해서는 <code>볼륨</code>을 <code>컨테이너</code>에 <code>마운트</code>해줘야 합니다. 
<code>마운트</code>하기 위해서는 <code>-v [volume-name]:[container-location]</code> 옵션을 추가합니다.</p>
<blockquote>
<p><code>docker run -p 8081:8081 -v vol-t:/app as</code></p>
</blockquote>
</blockquote>
<blockquote>
<p><code>docker inspect [container-name]</code> 을 통해 <code>Mounts</code>부분을 보면 다음과 되어있는 것을 볼 수 있습니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/b6cdbe32-997e-4071-ae7f-397093110227/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>-v</code> 옵션이 없는 <code>컨테이너</code>는 다음과 같이 <code>Mounts</code> 부분이 비어있습니다.</p>
<blockquote>
<p><code>docker run -p 8081:8081 as</code>
<img src="https://velog.velcdn.com/images/xxx-sj/post/0b5ffac5-1120-4115-9d7c-4f119a715398/image.png" alt=""></p>
</blockquote>
</blockquote>
<blockquote>
<p><code>volume</code>은 <code>다른 컨테이너</code>에도 <code>마운트</code>가 가능하기 때문에 <code>컨테이너</code> 간 <code>데이터 공유</code>가 가능해집니다.</p>
</blockquote>
<h3 id="📒volume-삭제">📒VOLUME 삭제</h3>
<blockquote>
<p><code>VOLUME</code> 삭제 시에는 마운트 되어있는 <code>컨테이너</code>를 <code>삭제</code> 한 후 <code>VOLUME</code>을 삭제해야 합니다.</p>
</blockquote>
<h3 id="📒bind-mount">📒BIND MOUNT</h3>
<blockquote>
<p>바인드 마운트는 <code>Host OS</code> 파일 시스템의 특정 경로를 <code>컨테이너</code>로 바로 <code>마운트</code> 할 수 있습니다.
컨테이너 실행 시 옵션으로 <code>-v [host location(file or directory)]:[container-location]</code> 추가해주면 됩니다. </p>
</blockquote>
<blockquote>
<blockquote>
<p><code>docker run -p 8081:8081 -v $(pwd):/app as</code></p>
</blockquote>
</blockquote>
<h4 id="출처">출처</h4>
<blockquote>
<p><a href="https://nirsa.tistory.com/69">https://nirsa.tistory.com/69</a>
<a href="https://nirsa.tistory.com/70?category=868315">https://nirsa.tistory.com/70?category=868315</a>
<a href="https://soft.plusblog.co.kr/139#google_vignette">https://soft.plusblog.co.kr/139#google_vignette</a>
<a href="https://tech.cloudmt.co.kr/2022/06/29/%EB%8F%84%EC%BB%A4%EC%99%80-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%9D%98-%EC%9D%B4%ED%95%B4-3-3-docker-image-dockerfile-docker-compose/">https://tech.cloudmt.co.kr/2022/06/29/%EB%8F%84%EC%BB%A4%EC%99%80-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%9D%98-%EC%9D%B4%ED%95%B4-3-3-docker-image-dockerfile-docker-compose/</a>
<a href="https://www.daleseo.com/docker-volumes-bind-mounts/#google_vignette">https://www.daleseo.com/docker-volumes-bind-mounts/#google_vignette</a>
<a href="https://seosh817.tistory.com/374">https://seosh817.tistory.com/374</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[docker 공부 시작하기 [spring-boot] 도커 파일로 컨테이너 실행하기- (1)]]></title>
            <link>https://velog.io/@xxx-sj/docker-%EA%B3%B5%EB%B6%80-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@xxx-sj/docker-%EA%B3%B5%EB%B6%80-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Tue, 21 Nov 2023 14:22:51 GMT</pubDate>
            <description><![CDATA[<h1 id="📕-시작">📕 시작</h1>
<blockquote>
<p>이제 docker를 공부를 해야 될 것 같아서 정리도 하고, 제가 겪은 것들을 공유하기 위해 이 글을 시작합니다. 
 잘못된 부분이 있거나 궁금하신 부분이 있으시다면 언제든지 말씀해주세요</p>
</blockquote>
<h1 id="📕프로젝트-구성">📕프로젝트 구성</h1>
<blockquote>
<p>프로젝트는 간단하게 <code>start.spring.io</code> 에서 spring web 만 dependency로 갖도록 하여 구성하였습니다. </p>
</blockquote>
<p> <img src="https://velog.velcdn.com/images/xxx-sj/post/94a2a766-75de-44ea-bd87-1b22ee943af0/image.png" alt=""></p>
<blockquote>
<p>이 후에 도커로 띄운 서버 테스트를 위해 간단한 controller를 만들어 주었습니다.</p>
</blockquote>
<pre><code class="language-java"> @RestController
public class HelloController {

    @GetMapping(&quot;/&quot;)
    public String hello() {
        return &quot;hello&quot;;
    }
}</code></pre>
<blockquote>
<p>잘 실행되는지 run 실행해 본 후 <code>./gradle build</code> 빌드를 통해 <code>jar</code> 파일을 만들어 줍니다.</p>
</blockquote>
<h2 id="📙실행과정">📙실행과정</h2>
<blockquote>
<p>도커 실행 과정은 크게 보면 다음과 같습니다.</p>
<blockquote>
<p>도커 파일 작성 -&gt; <code>build</code> 를 통해 <code>이미지</code> 생성 -&gt; <code>run</code>을 통해 <code>이미지</code>를 가진<code>container</code> 실행 </p>
</blockquote>
</blockquote>
<blockquote>
<p>먼저 도커 파일 작성을 알아보도록 하겠습니다.</p>
</blockquote>
<h1 id="📕dockerfile">📕Dockerfile</h1>
<blockquote>
<p>Dokcerfile은 도커 이미지를 생성하는 데 사용되는 파일입니다. Dockerfile에 정의한 명령어들은 이미지 레이어를 형ㅇ성하며, 각 레이어는 이전 레이어를 기반으로 합니다. 
처음 적는 명령어 <code>FROM</code>은 베이스 이미지로, 매우 중요합니다.  </p>
</blockquote>
<h3 id="📒dockerfile-말고-다른이름은-안되나요">📒Dockerfile 말고 다른이름은 안되나요??</h3>
<blockquote>
<p>Docker에서는 Dockerfile이라는 특정한 파일명을 사용하도록 규정하고 있고 있습니다.
<code>대소문자 주의!</code></p>
</blockquote>
<h3 id="📒dockerfile의-위치는">📒Dockerfile의 위치는??</h3>
<blockquote>
<p>Dockerfile의 위치는 project root에 위치하는게 좋습니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/4105c81e-fdb7-4c70-b80f-3bed1d780d66/image.png" alt=""></p>
</blockquote>
<h2 id="📙작성된-dockerfile">📙작성된 Dockerfile</h2>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk

WORKDIR /app

ARG PATH=./build/libs

COPY ${PATH}/docker-example-0.0.1-SNAPSHOT.jar ${PATH}/docker-example-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;./build/libs/docker-example-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<h2 id="📙하나씩-알아보기">📙하나씩 알아보기</h2>
<h3 id="📒from">📒FROM</h3>
<blockquote>
<p>베이스 이미지를 결정하는 명령어 입니다. 해당 프로젝트는 java11 을 사용하기 때문에 <code>FROM openjdk:11-jdk</code> 를 사용하였습니다.</p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk</code></pre>
<blockquote>
<p>Docker 파일에 <code>FROM</code> 명령어만 적어서 이미지를 만들고 <code>run</code> 해보도록 하겠습니다.</p>
<blockquote>
<p><code>docker</code> 명령어를 사용할 때는 Dockerfile이 있는 <code>root</code>에서 명령어를 실행하였습니다.
이미지 생성 명령어는 다음과 같습니다
<code>docker build . -t [image-name] -f [dockerfile-name]</code>
<code>docker build . -t section01-example</code></p>
</blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/68b9c81e-4716-408e-b92f-a5da15db7452/image.png" alt=""></p>
<blockquote>
<p>도커 이미지는 <code>docker images</code> 명령어를 통해 생성된 이미지를 확인하실 수 있습니다.</p>
</blockquote>
<h3 id="📗실행해보기">📗실행해보기</h3>
<blockquote>
<p>생성된 이미지로 컨테이너를 실행해보도록 하겠습니다. 명령어는 다음과 같습니다</p>
<blockquote>
<p><code>docker run [image-name]</code>
<code>docker run section01-example</code></p>
</blockquote>
</blockquote>
<blockquote>
<p><code>FROM</code>만 작성된 Dockerfile을 이용하여 컨테이너를 실행했을 때 아래와 같습니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/6feb5ab4-87d6-46fe-b98a-98bb9fc16202/image.png" alt=""></p>
<blockquote>
<p>간단하게 생각해서 <code>실행하는 명령이나 서비스를 실행하는 내용이 없기</code> 때문입니다.</p>
</blockquote>
</blockquote>
<blockquote>
<p>다음으로 실행하는 명령어를 추가해보겠습니다.</p>
</blockquote>
<h3 id="📒cmd">📒CMD</h3>
<blockquote>
<p><code>CMD</code>는 <code>docker run</code> 명령어를 실행 할 때 <code>실행되는 명령어</code> 입니다.
CMD로 실행할 명령어는 다음과 같습니다</p>
<blockquote>
<p><code>java -jar /build/libs/docker-example-0.0.1-SNAPSHOT.jar</code>
<code>gradle build</code>를 통해 빌드된 jar 파일을 실행하는 것입니다.</p>
</blockquote>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/build/libs/docker-example-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<blockquote>
<p>이제 다시 이미지를 만들어서 컨테이너를 생성하여 실행해보겠습니다. 이 전과 같습니다.</p>
<blockquote>
<p><code>docker build . -t section01-example</code> , <code>docker run section01-example</code></p>
</blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/0b961625-7c87-420f-b32d-0f9191ac99ce/image.png" alt=""></p>
<blockquote>
<p>에러 내용을 보면  <code>/build/libs/docker-example-0.0.1-SNAPSHOT.jar</code> 해당 jarfile을 access 할 수 없다는 내용입니다.
❗️<code>왜 이런 오류가 났을까요??</code>❗️</p>
</blockquote>
<blockquote>
<p>간단히 생각해보면 다음과 같습니다. 이미지로 생성된 <code>container</code> 도 하나의 새로운 <code>공간[컴퓨터]</code> 입니다. 
즉, <code>/build/libs/docker-example-0.0.1-SNAPSHOT.jar</code> 위치의 jar 파일은 <code>local[Host OS]</code> 에만 있을 뿐 <code>container</code> 안에는 없기 때문입니다. </p>
</blockquote>
<blockquote>
<p><code>container</code> 내 <code>Files</code> 를 <code>docker desktop</code>으로 보면 <code>/build/libs/docker-example-0.0.1-SNAPSHOT.jar</code> 가 없는 것을 볼 수 있습니다. 
<img src="https://velog.velcdn.com/images/xxx-sj/post/04b647a8-1abb-42cd-8ed7-dfd834dd960f/image.png" alt="section01-example image"></p>
</blockquote>
<h3 id="📗어떻게-해야할까">📗어떻게 해야할까?</h3>
<blockquote>
<p><code>local</code>에 있는 <code>jar파일</code>을 해당 위치로 <code>복사</code> 해주면 됩니다. </p>
</blockquote>
<h3 id="📒copy">📒COPY</h3>
<blockquote>
<p>여기에서는 <code>local [Host OS]</code>에 위치한 <code>jar file</code>을 복사해서 <code>container</code>로 <code>이동시키는 명령어</code> 입니다.</p>
<blockquote>
<p><code>COPY [local(Host OS) file] [container location]</code></p>
</blockquote>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
COPY ./build/libs/docker-example-0.0.1-SNAPSHOT.jar /build/libs/docker-example-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;/build/libs/docker-example-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<blockquote>
<p>Dockerfile을 수정하였으니 동일하게 이미지를 만들고 컨테이너를 실행해보겠습니다.</p>
</blockquote>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/c1cad042-229a-4b9c-b38f-8816e098770d/image.png" alt=""></p>
<blockquote>
<p><code>COPY</code> 명령어를 통해 해당 위치에<code>jarfile</code>이 복사된 것을 확인할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/fa976a2d-90b7-4a9d-a0bc-f26a7a85e8bc/image.png" alt=""></p>
<blockquote>
<p>tomcat이 뜨는것 까지 해결되었습니다. 톰캣 서버도 떴으니 이제 다음으로 <code>port 8081</code> 로 떠있는 서버에 브라우저에서 접근해보도록 하겠습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/0be1bb9f-6e59-421d-8cad-37c9dd084de9/image.png" alt=""></p>
<blockquote>
<p>❗️하지만 사이트에 연결할 수 없다고 나오네요.. 왜 일까요??❗️
위에서 말한 것처럼 <code>이미지</code>로 생성된 <code>container</code>도 하나의 새로운 <code>공간[컴퓨터]</code>이기 때문에 
<code>local[Host OS]</code> 과 <code>container</code>를 <code>연결</code>해 주어야 합니다. </p>
</blockquote>
<h3 id="📗그러면-어떻게-해야-할까">📗그러면 어떻게 해야 할까??</h3>
<blockquote>
<p>간단합니다. <code>container</code>를 실행할 때 옵션으로 <code>local[Host OS]</code>과 <code>container</code>의<code>port</code>를 바인딩 해주면 됩니다. 저는 tomcat을 8081로 띄웠기 때문에 8081로 바인딩하겠습니다.</p>
</blockquote>
<blockquote>
<blockquote>
<p>docker run -p <code>[local-port(Host-port)]</code>:<code>[container port]</code>  <code>[image-name]</code> 
docker run -p 8081:8081 section01-example</p>
</blockquote>
</blockquote>
<blockquote>
<p>옵션을 추가해서 컨테이너를 실행해보겠습니다.</p>
</blockquote>
<blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/2f43e168-a3ec-4d8f-805c-c75c923b1492/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/53556055-a624-4df4-b248-c7881b4759ff/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/75445f83-803b-4316-b01f-d048ed7026fe/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>접근이 잘 되는것을 확인할 수 있습니다.</p>
</blockquote>
<h3 id="📒추가적으로">📒추가적으로</h3>
<blockquote>
<p>가장 처음 봤던 <code>Dockerfile</code>에서 사용했던 <code>WORKDIR</code>과 <code>ARG</code> 에 대해 설명을 적어보려 합니다. </p>
</blockquote>
<h3 id="📒workdir">📒WORKDIR</h3>
<blockquote>
<p><code>WORKDIR</code> 명령어는 linux 명령어 <code>cd</code>와 같다고 생각하면 쉽다. 말 그대로 <code>working directory</code>이다. <code>그렇다면 왜 사용하는가?</code> </p>
<blockquote>
<p>사용이유는 다음과 같다. 처음 베이스이미지를 이용해 만든 파일들이 있을텐데, 해당 파일들과 <code>구분하기 위해서</code> 라고 생각이 들며, 만약 같은<code>파일이름</code> 으로 COPY할 경우 베이스 이미지에서 만든 파일이 <code>덮어쓰기</code>가 되어버린다. </p>
</blockquote>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
WORKDIR /app
COPY ./build/libs/docker-example-0.0.1-SNAPSHOT.jar ./build/libs/docker-example-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;./build/libs/docker-example-0.0.1-SNAPSHOT.jar&quot;]</code></pre>
<blockquote>
<p>여기서는 조금 달라지는데, <code>WORKDIR</code>를 이용하여 
<code>디렉토리를 이동</code> 하였기 때문에 <code>COPY</code> 부분과 <code>CMD</code> 부분이 달라진다. </p>
<blockquote>
<p>기본에는 <code>COPY</code>가 <code>./build/libs/docker-example-0.0.1-SNAPSHOT.jar /build/libs/docker-example-0.0.1-SNAPSHOT.jar</code> 에서 <code>./build/libs/docker-example-0.0.1-SNAPSHOT.jar ./build/libs/docker-example-0.0.1-SNAPSHOT.jar</code> 로 변경되었다.</p>
<blockquote>
<p>이유는 다음과 같다. <code>WORKDIR</code>로 <code>/app</code>으로 이동하였기 때문에 <code>/build/libs</code>에서 <code>./build/libs</code>로 변경된 것이다. <code>/build/libs</code>를 하게되면 절대경로 <code>/build</code> 부터 시작하게 되어버리는 것이고, <code>./build</code>를 하게 되면 현재 디렉토리인 <code>/app</code>에서 시작하는 것. 다시말해, <code>./build/libs</code> -&gt; <code>/app/build/libs</code> 가 되는 것이다.</p>
</blockquote>
</blockquote>
</blockquote>
<blockquote>
<p>동일하게 CMD도 마지막에 <code>./build</code>로 변경된 이유도 같은 이유이다. 현재 <code>working directory</code>는 <code>/app</code>이기 때문에 현재 디렉토리에서 이동하는 것으로 <code>./build/libs</code>로 시작하는 것이다.</p>
</blockquote>
<blockquote>
<p>동일하게 도커 컨테이너 실행 명령어를 실행한 다음 <code>container</code> 의 <code>Files</code>를 보면 
<img src="https://velog.velcdn.com/images/xxx-sj/post/9fa3a039-07a4-405e-8fca-cc9da62ce470/image.png" alt="">
해당 디렉토리에 <code>COPY</code>된 것을 볼 수 있다.</p>
</blockquote>
<h3 id="📒arg">📒ARG</h3>
<blockquote>
<p>ARG는 말그대로 <code>argument</code>, <code>인자</code>이다. 해당 Dockerfile을 실행할 때 사용되는 <code>변수</code>를 <code>선언할 때</code> 사용한다. </p>
</blockquote>
<pre><code class="language-javascript">#Dockerfile

FROM openjdk:11-jdk
ARG PATH=./build/libs
WORKDIR /app
COPY ${PATH}/docker-example-0.0.1-SNAPSHOT.jar ${PATH}/docker-example-0.0.1-SNAPSHOT.jar
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;./build/libs/docker-example-0.0.1-SNAPSHOT.jar&quot;]
</code></pre>
<blockquote>
<p>여기에서는 <code>./build/libs</code>가 중복되어서 따로 ARG로 빼서 사용한 것이다.</p>
</blockquote>
<h1 id="📕시행착오">📕시행착오</h1>
<h3 id="📒error-failed-to-solve-no-build-stage-in-current-context">📒ERROR: failed to solve: no build stage in current context</h3>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/963ddc1b-c2fd-4cf7-9065-844ad26167fc/image.png" alt="">
위 사진처럼 Dockerfile을 만들고 명령어<code>docker build . -t [image-name]</code> 을 실행하던 중 발생한 문제입니다. </p>
<h3 id="📗해결법">📗해결법</h3>
<p>해결법은 간단합니다. Dockerfile 시작이 LABEL author=&quot;loky1&quot; 로 시작하기 때문에 발생한 문제입니다. <code>FROM ...</code> 명령어를 가장 위로 올려주면 됩니다.</p>
<h3 id="📒starting-무한로딩">📒starting 무한로딩</h3>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/fda2d3c8-1236-4575-acf1-7b9c7e6c7599/image.png" alt=""></p>
<h3 id="📗해결법-1">📗해결법</h3>
<blockquote>
<p>OS가 <code>window</code>라면 실행을 검색하신 후 <code>%AppData%</code>를 입력한 후 확인을 눌러줍니다. 
<img src="https://velog.velcdn.com/images/xxx-sj/post/e385025e-81f1-401e-9ca2-614b28157c7d/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>도커를 프로그램을 삭제 후<code>[user]/[AppData]/[Local]/[Docker]</code> 파일을 완전히 삭제해준 후 다시 설치합니다.</p>
</blockquote>
<blockquote>
<p>다음으로 <code>Docker Desktop</code>을 실행하면 오류가 날 수 있습니다. 그 때는 <code>CMD</code>창을 열어 <code>--unregister docker-desktop-data</code> 명령어 입력 후 
<code>등록 취소 중입니다, / 작업을 완료했습니다</code> 가 뜬 후 다시 <code>Docker Desktop</code>을 실행하면 됩니다.</p>
</blockquote>
<h3 id="참고">참고</h3>
<blockquote>
<p><a href="https://ttl-blog.tistory.com/761#%EC%96%B4%EB%96%BB%EA%B2%8C%20%ED%95%B4%EA%B2%B0%ED%95%98%EB%82%98%EC%9A%94%3F%20%F0%9F%A7%90-1">https://ttl-blog.tistory.com/761#%EC%96%B4%EB%96%BB%EA%B2%8C%20%ED%95%B4%EA%B2%B0%ED%95%98%EB%82%98%EC%9A%94%3F%20%F0%9F%A7%90-1</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring-vue] 웹소켓으로 채팅 구현하기 (5) -  vue 화면 설명]]></title>
            <link>https://velog.io/@xxx-sj/spring-vue-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-4-vue-%ED%99%94%EB%A9%B4-%EC%84%A4%EB%AA%85</link>
            <guid>https://velog.io/@xxx-sj/spring-vue-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-4-vue-%ED%99%94%EB%A9%B4-%EC%84%A4%EB%AA%85</guid>
            <pubDate>Sun, 12 Nov 2023 09:40:31 GMT</pubDate>
            <description><![CDATA[<p>화면을 개발하면서 생각보다 기록해두어야 할 부분이 많다고 생각하여 추가로 글을 작성합니다.</p>
<p>프로젝트 생성부분은 <a href="https://velog.io/@xxx-sj/vue-build-%EB%B0%8F-dev-proxy-%EC%84%A4%EC%A0%95-3">프로젝트 생성</a> 부분을 참고해주세요</p>
<h2 id="📘vueconfigjs">📘vue.config.js</h2>
<p>먼저 vue.config 부분입니다. spring과 vue를 같이 개발시 npm run serve로 front server를 띄울 때 back-end server와 통신을 하기위해 설정합니다.</p>
<pre><code class="language-js">const { defineConfig } = require(&#39;@vue/cli-service&#39;)

const host = &quot;localhost&quot;; //back-end host
const port = &quot;8081&quot;; //back-end port
module.exports = defineConfig({
  transpileDependencies: true,

  outputDir: &quot;../back-end/src/main/resources/static&quot;, //webpack build 시 결과물 위치


  devServer: {
    hot: true,
    proxy: {
      //http://localhost:8080/api/ -&gt; http://localhost:8081/api/
      &#39;/api/&#39;: {
        target: `http://${host}:${port}`,
        changeOrigin: true,
      },

      &#39;/ws/&#39;: {
        target: `ws://${host}:${port}`,
        changeOrigin: false,
        ws: true,
      }
    }
  }
})</code></pre>
<p><del>&gt; outputDir path 설정은 resources/static 이거나, classpath 내 public 폴더도 가능합니다 <a href="https://bottom-to-top.tistory.com/38">참고</a></del></p>
<p>여기서 주의해서 봐야하는 부분은 proxy 부분입니다. 작성한 부분을 자세히 보자면</p>
<blockquote>
<p>vue에서 /api/로 보내는 요청은  target 으로 변경되어 요청됩니다. 
즉, 현재 front-server가 8080으로 떠있다면 axios를 이용해 localhost:8080/api/** 로의 요청은 localhost:8081/api/** 로 변경됩니다.</p>
</blockquote>
<blockquote>
<p>다음 /ws/또한 같은 의미로 사용됩니다. /ws/는 vue에서 웹소켓을 이용 할 때 사용합니다.</p>
</blockquote>
<h2 id="📘axios-사용-시">📘AXIOS 사용 시</h2>
<p>front에서 back-end와 통신 시 axios를 많이 사용하실텐데요,여기서 주의해야할 점은 fetch할 때 input 부분입니다. 처음 input 부분에 다음과 같이 입력했는데 오류가 발생했었습니다.</p>
<blockquote>
<p>fetch(&quot;<a href="http://localhost:8081/api/v1/room">http://localhost:8081/api/v1/room</a>, ...)
<img src="https://velog.velcdn.com/images/xxx-sj/post/2527189a-62ff-431a-bafa-d4e27ba9b94f/image.png" alt=""></p>
</blockquote>
<p>개발환경에서 개발 시 위에서 설정한 대로</p>
<blockquote>
<p>fetch(&quot;<a href="http://localhost:8080/api/v1/room">http://localhost:8080/api/v1/room</a>,) 또는
fetch(&quot;/api/v1/room&quot;, )</p>
</blockquote>
<p>으로 사용하셔야 합니다.</p>
<blockquote>
<blockquote>
<p><strong>배포 후 빌드 시에는 localhost:8081로 설정되어있어야 합니다</strong></p>
</blockquote>
</blockquote>
<h2 id="📘stomp">📘STOMP</h2>
<h3 id="📗client-객체-생성">📗Client 객체 생성</h3>
<p>해당 프로젝트에서 사용한 <strong>@stomp/stompjs</strong> 사용법은 다음과 같습니다.</p>
<blockquote>
<p>const client = new Client({//config});
config 부분에는 어떤 속성들이 있는지 확인해봅시다. Client를 따라 들어가보면
<img src="https://velog.velcdn.com/images/xxx-sj/post/677ad116-8bea-4124-aec2-b9acc18f88b5/image.png" alt=""> 와 같은 생성자가 있는 것을 확인할 수 있습니다.
저희가 여기서 봐야하는 부분은 생성자의 인자에 있는 StompConfig 부분입니다.</p>
</blockquote>
<ul>
<li><p>StompConfig</p>
<pre><code class="language-java">export class StompConfig {
/**
 * See [Client#brokerURL]{@link Client#brokerURL}.
 */
public brokerURL?: string;

/**
 * See [Client#stompVersions]{@link Client#stompVersions}.
 */
public stompVersions?: Versions;

/**
 * See [Client#webSocketFactory]{@link Client#webSocketFactory}.
 */
public webSocketFactory?: () =&gt; any;

/**
 * See [Client#connectionTimeout]{@link Client#connectionTimeout}.
 */
public connectionTimeout?: number;

/**
 * See [Client#reconnectDelay]{@link Client#reconnectDelay}.
 */
public reconnectDelay?: number;

/**
 * See [Client#heartbeatIncoming]{@link Client#heartbeatIncoming}.
 */
public heartbeatIncoming?: number;

/**
 * See [Client#heartbeatOutgoing]{@link Client#heartbeatOutgoing}.
 */
public heartbeatOutgoing?: number;

/**
 * See [Client#splitLargeFrames]{@link Client#splitLargeFrames}.
 */
public splitLargeFrames?: boolean;

/**
 * See [Client#forceBinaryWSFrames]{@link Client#forceBinaryWSFrames}.
 */
public forceBinaryWSFrames?: boolean;

/**
 * See [Client#appendMissingNULLonIncoming]{@link Client#appendMissingNULLonIncoming}.
 */
public appendMissingNULLonIncoming?: boolean;

/**
 * See [Client#maxWebSocketChunkSize]{@link Client#maxWebSocketChunkSize}.
 */
public maxWebSocketChunkSize?: number;

/**
 * See [Client#connectHeaders]{@link Client#connectHeaders}.
 */
public connectHeaders?: StompHeaders;

/**
 * See [Client#disconnectHeaders]{@link Client#disconnectHeaders}.
 */
public disconnectHeaders?: StompHeaders;

/**
 * See [Client#onUnhandledMessage]{@link Client#onUnhandledMessage}.
 */
public onUnhandledMessage?: messageCallbackType;

/**
 * See [Client#onUnhandledReceipt]{@link Client#onUnhandledReceipt}.
 */
public onUnhandledReceipt?: frameCallbackType;

/**
 * See [Client#onUnhandledFrame]{@link Client#onUnhandledFrame}.
 */
public onUnhandledFrame?: frameCallbackType;

/**
 * See [Client#beforeConnect]{@link Client#beforeConnect}.
 */
public beforeConnect?: () =&gt; void | Promise&lt;void&gt;;

/**
 * See [Client#onConnect]{@link Client#onConnect}.
 */
public onConnect?: frameCallbackType;

/**
 * See [Client#onDisconnect]{@link Client#onDisconnect}.
 */
public onDisconnect?: frameCallbackType;

/**
 * See [Client#onStompError]{@link Client#onStompError}.
 */
public onStompError?: frameCallbackType;

/**
 * See [Client#onWebSocketClose]{@link Client#onWebSocketClose}.
 */
public onWebSocketClose?: closeEventCallbackType;

/**
 * See [Client#onWebSocketError]{@link Client#onWebSocketError}.
 */
public onWebSocketError?: wsErrorCallbackType;

/**
 * See [Client#logRawCommunication]{@link Client#logRawCommunication}.
 */
public logRawCommunication?: boolean;

/**
 * See [Client#debug]{@link Client#debug}.
 */
public debug?: debugFnType;

/**
 * See [Client#discardWebsocketOnCommFailure]{@link Client#discardWebsocketOnCommFailure}.
 */
public discardWebsocketOnCommFailure?: boolean;

/**
 * See [Client#onChangeState]{@link Client#onChangeState}.
 */
public onChangeState?: (state: ActivationState) =&gt; void;
}</code></pre>
</li>
</ul>
<p>간단하게 저희가 사용하는 것은 brokerURL, onConnect, onDisconnect, onStompError, onWebSocketClose, onWebSocketError 등이 있을 것 같습니다.<br>정의해야 하는 속성과 함께 client config를 완성시키면 다음과 같습니다.</p>
<pre><code class="language-java"> this.websocketClient = new Client({
    brokerURL: &quot;ws://localhost:8080/ws/init&quot;,
    onConnect: () =&gt; {
    this.websocketClient.subscribe(`/sub/room/${this.currentRoom.id}`, msg =&gt; {
            this.messages.push(msg.body);
          });

          this.websocketClient.publish({
            destination: `/pub/room/${this.currentRoom.id}/entered`,
            body: JSON.stringify({message: `${this.textMessage}`, writer: &quot;user1&quot;}),
          });

    },
    onDisconnect: (frame) =&gt; {},
    onStompError: (frame) =&gt; {},
    onWebSocketClose: (_) =&gt; {}
    onWebSocketError: (_) =&gt; {},
});</code></pre>
<blockquote>
<p>추가로 마지막에 client.activate()를 호출해주셔야 합니다.</p>
</blockquote>
<h3 id="📗subscribe-하기">📗subscribe 하기</h3>
<p>클라이언트를 생성했다면 다음으로는 subscribe입니다. subscribe은 다음과 같습니다.</p>
<pre><code class="language-js">client.subscribe(destination, callback=(message:IMessage) =&gt; {}, headers=[key:String])</code></pre>
<p>프로젝트에서 사용한 코드는 다음과 같습니다.</p>
<pre><code class="language-js">this.websocketClient.subscribe(`/sub/room/${this.currentRoom.id}`, msg =&gt; {
            this.messages.push(msg.body);
          })</code></pre>
<blockquote>
<p>destination = <code>/sub/room/${this.currentRoom.id}</code>
 callback =  msg =&gt; {  this.messages.push(msg.body); }</p>
</blockquote>
<h3 id="📗publish-하기">📗publish 하기</h3>
<p>sub가 비슷합니다.</p>
<pre><code class="language-js">
client.publish(params: IPublishParams) == 
client.publish({destination: &quot;/queue/test&quot;, body: &quot;Hello, STOMP&quot;});
---
export interface IPublishParams {
  /**
   * destination end point
   */
  destination: string;
  /**
   * headers (optional)
   */
  headers?: StompHeaders;
  /**
   * body (optional)
   */
  body?: string;
  /**
   * binary body (optional)
   */
  binaryBody?: Uint8Array;
  /**
   * By default, a `content-length` header will be added in the Frame to the broker.
   * Set it to `true` for the header to be skipped.
   */
  skipContentLengthHeader?: boolean;
}</code></pre>
<p>프로젝트에서 사용한 코드는 다음과 같습니다.</p>
<pre><code class="language-js">const body = {message: `${this.textMessage}`, writer: &quot;user1&quot;}

this.websocketClient.publish({
        destination: `/pub/room/${this.currentRoom.id}`,
        body: JSON.stringify(body),
      });</code></pre>
<h3 id="📗disconnect하기">📗disconnect하기</h3>
<blockquote>
<p>disconnect는 activate 와 반대로 deactivate 를 사용하면 됩니다.</p>
</blockquote>
<pre><code class="language-js">this.websocketClient.deactivate();</code></pre>
<p>전체 코드는 <a href="https://github.com/xxx-sj/spring-exmaple-projects">github</a>에서 확인 가능합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring-vue] 웹소켓으로 채팅 구현하기 (4) - 
DB 추가 및 엔티티 추가]]></title>
            <link>https://velog.io/@xxx-sj/spring-vue-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-3-DB-%EC%B6%94%EA%B0%80-%EB%B0%8F-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%B6%94%EA%B0%80</link>
            <guid>https://velog.io/@xxx-sj/spring-vue-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-3-DB-%EC%B6%94%EA%B0%80-%EB%B0%8F-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%B6%94%EA%B0%80</guid>
            <pubDate>Sat, 11 Nov 2023 17:05:54 GMT</pubDate>
            <description><![CDATA[<p>이 전에 이어서 엔티티와  H2디비를 구현하여 개발해보겠습니다.</p>
<h2 id="📗구현">📗구현</h2>
<h3 id="📘엔티티-설정">📘엔티티 설정</h3>
<p>관계는 필요하니, 간단하게 User 와 Room 엔티티와 User와 Room이 다대다 관계이기 때문에 중간에 EnteredRoom 이라는 다대다 해소 엔티티를 넣도록 하겠습니다. </p>
<ul>
<li><p>USER</p>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

  @Id @GeneratedValue
  private Long id;
  private String name;

  @OneToMany(mappedBy = &quot;user&quot;)
  private List&lt;EnteredRoom&gt; enteredRoom = new ArrayList&lt;&gt;();

  public User(String name) {
      this.name = name;
  }
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>
- ROOM
```java
@Entity
@Getter
@NoArgsConstructor
public class Room {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = &quot;room&quot;)
    private List&lt;EnteredRoom&gt; enteredRoom = new ArrayList&lt;&gt;();

    public Room(String name) {
        this.name = name;
    }
}</code></pre><ul>
<li><p>ENTEREDROOM</p>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class EnteredRoom {

  @Id @GeneratedValue
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = &quot;USER_ID&quot;)
  private User user;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = &quot;ROOM_ID&quot;)
  private Room room;

  @Enumerated(value = EnumType.STRING)
  private RoomStatus roomStatus = RoomStatus.ENTER;

  public EnteredRoom(User user, Room room) {
      this.user = user;
      this.room = room;
  }
}</code></pre>
</li>
</ul>
<h3 id="📘테스트-데이터-추가">📘테스트 데이터 추가</h3>
<p>엔티티도 만들었으니, 테스트 할 수 있도록 데이터를 어플리케이션이 구동될 때 넣도록 하겠습니다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketBrokerConfiguration implements WebSocketMessageBrokerConfigurer {


    private final UserService userService;
    private final RoomService roomService;
    private final EnteredRoomService enteredRoomService;

     @EventListener(value = ApplicationReadyEvent.class)
    public void addTestData() {

        User user1 = new User(&quot;user1&quot;);
        User user2 = new User(&quot;user2&quot;);
        User user3 = new User(&quot;user3&quot;);
        User user4 = new User(&quot;user4&quot;);

        Room room1 = new Room(&quot;채팅방1&quot;);
        Room room2 = new Room(&quot;채팅방2&quot;);

        Long savedUserId1 = userService.save(user1);
        Long savedUserId2 = userService.save(user2);
        Long savedUserId3 = userService.save(user3);
        Long savedUserId4 = userService.save(user4);


        Long savedRoomId1 = roomService.save(room1);
        Long savedRoomId2 = roomService.save(room2);

        enteredRoomService.save(savedUserId1, savedRoomId1);
        enteredRoomService.save(savedUserId2, savedRoomId1);
        enteredRoomService.save(savedUserId3, savedRoomId1);

        enteredRoomService.save(savedUserId3, savedRoomId2);
        enteredRoomService.save(savedUserId4, savedRoomId2);
    }
}</code></pre>
<p>service, repository는 spring-data-jpa를 이용해서 간단하게 만들었습니다.</p>
<h3 id="📘api-추가">📘API 추가</h3>
<blockquote>
<p>몇 개의 API를 추가하도록 하겠습니다.</p>
</blockquote>
<ul>
<li>유저가 포함되어있는 ROOM LIST</li>
<li>ROOM 전체 LIST</li>
<li><del>유저가 ROOM을 나갈 경우 삭제</del></li>
<li><del>유저가 ROOM을 새로 들어갈 경우 등록</del></li>
</ul>
<p>등등.. 일단 테스트에 맞춰 필요한 API만 개발하도록 하겠습니다.</p>
<ul>
<li>room 전체 List를 가져오는 API는 단순하게 findAll()로 가져옵니다.</li>
</ul>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/v1/room&quot;)
public class RoomController {

    private final RoomService service;

    @GetMapping(&quot;&quot;)
    public Result&lt;List&lt;RoomResponseDto&gt;&gt; list() {

        return service.findAll();
    }
}
</code></pre>
<ul>
<li>유저가 포함되어있는 ROOM List
해당 API같은 경우 userID를 넘겨 해당 유저가 속해있는 방의 정보들을 가져옵니다.</li>
</ul>
<pre><code class="language-java">- controller
    @GetMapping(&quot;/joined&quot;)
    public Result&lt;List&lt;EnteredRoomResponseDto&gt;&gt; joinedList( RoomJoinedRequestDto requestDto) {
        return service.findAll(requestDto.getUserId());
    }

-------

- service
    public Result&lt;List&lt;EnteredRoomResponseDto&gt;&gt; findAll(Long userId) {
        User user = userRepository.findById(userId).get();
        List&lt;EnteredRoom&gt; findRooms = enteredRoomRepository.findAll(RoomStatus.ENTER, user);

        List&lt;EnteredRoomResponseDto&gt; collect = findRooms.stream().map(EnteredRoomResponseDto::new).collect(Collectors.toList());

        return new Result&lt;&gt;(collect);

    }

--------

- repository

@Query(&quot;SELECT er FROM EnteredRoom er JOIN FETCH er.room r WHERE er.user = :user AND er.roomStatus = :roomStatus&quot;)
    List&lt;EnteredRoom&gt; findAll(@Param(&quot;roomStatus&quot;) RoomStatus roomStatus, @Param(&quot;user&quot;) User user);
</code></pre>
<p>해당 API들은 맨 처음 화면에서 전체리스트와 유저가 속한 리스트를 가져올 때 사용됩니다.</p>
<h3 id="📘시나리오1">📘시나리오1</h3>
<blockquote>
<p>사용자가 채팅방을 클릭했을 때 [ /sub/room/{roomId}] 와 함께 [ /pub/room/{roomId}/entered ] 웹소켓 요청을 보낸다. </p>
</blockquote>
<p>front 화면은 따로 설명하지않고 <a href="https://github.com/xxx-sj/spring-exmaple-projects">github</a>을 보고 참고해주시기 바랍니다.</p>
<blockquote>
<blockquote>
<p>client 부분을 만들다보니 설명할 부분이나 제가 기록용으로 사용하기 위해 따로 블로그 글을 쓰겠습니다.</p>
</blockquote>
</blockquote>
<blockquote>
<p>vue에서 stomp를 사용하기 위해 두 개의 모듈을 설치합니다
stompjs - stomp <a href="https://github.com/stomp-js/stompjs">npm</a> //  <a href="https://stomp-js.github.io/guide/stompjs/rx-stomp/upgrading-to-stompjs-6-rx-stomp-1.html#deactivate---async">참고</a>
ws - websocket</p>
<blockquote>
<p>npm i --save @stomp/stompjs ws</p>
</blockquote>
</blockquote>
<blockquote>
<p>부가적인 기능은 현재 구현하지 않았다는 점.. 참고 부탁드립니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/ff036eab-c140-4cfd-abb9-b900e79cf65c/image.png" alt=""></p>
<p>화면에서 사용자가 채팅방을 클릭하면 웹소켓 요청을 보내게 됩니다.</p>
<pre><code class="language-javascript">connect() {
      //vue.config.js 참고
      const url = &quot;ws://localhost:8080/ws/init&quot;;
      this.websocketClient = new Client({
        brokerURL: url,
        onConnect: () =&gt; {
          this.websocketClient.subscribe(`/sub/room/${this.currentRoom.id}`, msg =&gt; {
            this.messages.push(msg.body);

          })

          this.websocketClient.publish({
            destination: `/pub/room/${this.currentRoom.id}/entered`,
            body: JSON.stringify({message: `${this.textMessage}`, writer: &quot;user1&quot;}),
          });

          this.isLoading = false;
        },

        onWebSocketError: () =&gt; {
          this.isLoading = false;
        },
      })

      this.websocketClient.activate();
    },</code></pre>
<p>server에서는 다음 메서드가 실행되며, 브로커를 통해 메시지를 전달하게 됩니다.</p>
<pre><code class="language-java">    @MessageMapping(&quot;/room/{roomId}/entered&quot;)
    public void entered(@DestinationVariable(value = &quot;roomId&quot;) String roomId, MessageDto message){
        log.info(&quot;# roomId = {}&quot;, roomId);
        log.info(&quot;# message = {}&quot;, message);
        final String payload = message.getWriter() + &quot;님이 입장하셨습니다.&quot;;
        template.convertAndSend(&quot;/sub/room/&quot; + roomId, payload);
    }</code></pre>
<h3 id="📘시나리오2">📘시나리오2</h3>
<blockquote>
<p>사용자가 메시지를 입력하고 전송 클릭 시 메시지가 다른 사용자들에게 전송된다. </p>
</blockquote>
<p>이번 시나리오는 간단하게 연결되어있는 웹소켓을 통해 메시지를 서버에 전달하고, 서버에서는 해당 메시지를 브로커를 통해 다른 사용자들에게 전달하면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/3a50dbfa-aa25-4804-8f11-49aa80b63f4d/image.png" alt=""></p>
<p>화면과 같이 왼쪽에서 메시지를 보내기를 누르게 되면, 서버에서는 아래 핸들러가호출되고, template를 통해 room에 속한 유저들에게 메시지를 보내게 됩니다.</p>
<pre><code class="language-java">    @MessageMapping(&quot;/room/{roomId}&quot;)
    public void sendMessage(@DestinationVariable(value = &quot;roomId&quot;) String roomId, MessageDto message) {
        log.info(&quot;# roomId = {}&quot;, roomId);
        log.info(&quot;# message = {}&quot;, message);

        template.convertAndSend(&quot;/sub/room/&quot; + roomId, message.getMessage());
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/156094b5-7d8f-4d63-8f9c-65146349097a/image.png" alt=""></p>
<p>대략 흐름은 다음과 같습니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/6fd3f6d3-11af-467a-98fc-1226862f0a97/image.png" alt=""></p>
<blockquote>
<ol>
<li>방에 접속한 클라이언트는 message를 보낸다.</li>
<li>sever에서는 해당 destination에 맞는 핸들러로 요청을 전달하고</li>
<li>핸들러에서는 로직을 실행 후 SimpMessagingTemplate 을 통해 sub 하고 있는 유저들에게
메시지를 전달한다.</li>
</ol>
</blockquote>
<h2 id="정리">정리</h2>
<p>처음 구현을 목표로 했던 <strong>방에 접속하고, 접속한 유저들끼리 채팅을 할 수 있다</strong> 까지 구현이 완료되었기 때문에 추가적인 개발 부분에 대해서는 추후 개발해보도록 하겠습니다. 채팅기능에 의의를 두었기 때문에 다른분들이 이 글을 읽고 spring-vue를 이용해 채팅을 구현함에 있어 큰 어려움을 없으실꺼라 생각합니다 .</p>
<p>지금까지 한 코드는 <a href="https://github.com/xxx-sj/spring-exmaple-projects">github 레포지토리</a> 에서 확인 가능하십니다.</p>
<p>개발하시면서 궁금하신 점이나, 안되는 부분이 있으시다면 저도 많이 못하지만.. 제가 힘이 닿는 부분까지 최대한 도와드리도록 하겠습니다. 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring-vue] 웹소켓으로 채팅 구현하기 (3) - STOMP]]></title>
            <link>https://velog.io/@xxx-sj/spring-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-3-STOMP</link>
            <guid>https://velog.io/@xxx-sj/spring-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-3-STOMP</guid>
            <pubDate>Sat, 11 Nov 2023 11:07:22 GMT</pubDate>
            <description><![CDATA[<h1 id="📕stomp">📕STOMP</h1>
<p>이번에는 stomp 프로토콜을 사용해 채팅을 구현해보도록 하자. 자세한 내용은 <a href="https://docs.spring.io/spring-framework/reference/web/websocket/stomp.html">spring doc</a> 에 나와있습니다.</p>
<h2 id="📗개요">📗개요</h2>
<p>STOMP란? <strong>Simple Text Oriented Messaged Protocol</strong> 말 그대로 텍스트 지향 프로토콜이다. STOMP는 텍스트 지향 프로토콜 이지만 페이로드는 텍스트 이거나 바이너리 일 수 있는 점..
또한 STOMP는 <strong>pub/sub</strong> 구조로 메시지를 공급받는 쪽 [pub] 과 메시지를 소비하는 쪽 [sub]이 분리되어 있으며
이 후에 개발할 서버가 <strong>broker</strong> 역할을 하게된다.</p>
<p>또한 STOMP는 HTTP를 모델로 하는 프레임 기반 프로토콜이다. 이 프레임에 대해 이해하면 이 후에 구현할 때 도움이 된다. 
프레임은 다음과 같다.</p>
<pre><code>COMMAND
header1:value1
header2:value2

Body^@</code></pre><blockquote>
<p>클라이언트는 메시지 내용[message]과 수신 대상[sub]을 설명하는 대상 헤더와 함께 SEND 또는 SUBSCRIBE 명령을 사용하여 메시지를 보내거나 구독할 수 있다.</p>
</blockquote>
<p>다음으로 간단하게 client가 구독할 때 서버에 보내는 헤더 내용이다.</p>
<pre><code>SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@</code></pre><p>Command 로 SUBSCRIBE을 보내면서 destination 정보를 전달한다. destination은 브로커에게 자신이 수신할[sub] topic을 알려주는 정보라고 이해하면 쉽다. 이 후에 해당 topic으로 pub이 오면 해당 topic을 수신하고 있던 클라이언트 들에게 pub 메시지가 전송된다.  </p>
<p>자세한 프로토콜 사양은 <a href="https://stomp.github.io/stomp-specification-1.2.html">프로토콜 사양</a>에서 볼 수 있습니다.</p>
<h2 id="📗사용이유">📗사용이유</h2>
<p>STOMP를 사용하는 이유는 다음과 같다.</p>
<blockquote>
<ul>
<li>이 전에 했던 코드같은 경우, 웹소켓 서버가 한 대인 경우 적용이 가능하나, 여러 대 일 경우 웹소켓 세션 정보를 서로 알 수 없다.</li>
</ul>
</blockquote>
<ul>
<li>spring으로 구현할 경우 메모리 브로커를 사용하게 되는데, 이 외에 외부 브로커를 사용할 경우 
등등..</li>
</ul>
<h2 id="📗구현">📗구현</h2>
<p>STOMP에 대한 자세한 내용은 <a href="https://docs.spring.io/spring-framework/reference/web/websocket/stomp.html">spring doc</a>를 보면 알 수 있기 때문에 이 글에서는 더 이상 설명치않고 구현을 해보도록 하겠습니다.</p>
<blockquote>
<p>이 전에 만들던 프로젝트에서 브로커를 사용할 수 있도록 어노테이션을 추가하고, [WebSocketMessageBrokerConfigurer] 를 구현합니다. 구현할 메서드로는 총 3가지로 
[registerStompEndpoints, configureClientInboundChannel, configureMessageBroker]를 구현합니다.</p>
</blockquote>
<pre><code class="language-java">@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketBrokerConfiguration implements WebSocketMessageBrokerConfigurer {


    private final WebsocketBrokerInterceptor interceptor;
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(&quot;/ws/init&quot;) //1
                .setAllowedOrigins(&quot;*&quot;); 
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(interceptor); //2
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker(&quot;/sub&quot;); //3
        registry.setApplicationDestinationPrefixes(&quot;/pub&quot;); //4
    }
}
</code></pre>
<blockquote>
<ol>
<li>최초 websocket을 연결할 때 보내는 endPoint입니다.</li>
<li>websocket이 연결되거나, sub/pub/send 등 client에서 메시지를 보내게 될 때 interceptor를 통해 핸들링 하게 됩니다.</li>
<li>client는 /sub/** 의 형태로 topic을 구독하게 됩니다.</li>
<li>반대로 메시지를 보낼때는 /pub 형식으로 보내게 됩니다.</li>
</ol>
</blockquote>
<p>다음 그림은 spring에서 제공하는 메모리 브로커를 사용할 때의 구성요소를 보여줍니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/f8b1b12e-f235-466d-b36b-19880f5666db/image.png" alt=""></p>
<blockquote>
<p>위 사진에서 /app은 /pub에 매칭되고, /topic은 /sub에 매칭됩니다. </p>
</blockquote>
<p>/pub 형식으로 보내는 메시지에 대해서는 일반적인 controller처럼 핸들링이 가능한데 예를 들어
/pub/test 와 같이 destination 정보를 넣어 요청을 할 경우, @MessageMapping(&quot;/test&quot;) 어노테이션을 가진 컨트롤러에서 핸들링이 가능해집니다.</p>
<pre><code class="language-java">@Slf4j
@RestController
@RequiredArgsConstructor
public class BrokerController {

    private final SimpMessagingTemplate template;


    @MessageMapping(&quot;/test&quot;)
    public void test(SimpMessageHeaderAccessor accessor) {
        log.info(&quot;### test method {}&quot;, accessor);
    }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/c80fa044-3cc2-43f4-82a8-e9c4f15daa19/image.png" alt=""></p>
<p>STOMP 테스트를 진행할 때는 <a href="https://www.apic.app/">APIC</a> 을 이용해야 한다는 점..
여기까지가 큰 흐름이고 이제 자세히 구현을 해보도록 하겠습니다.</p>
<p>다음으로는 간단하게 사용자가 방으로 들어오는 시나리오를 만들어서 구현해보록 하겠습니다.</p>
<h3 id="📘시나리오1">📘시나리오1</h3>
<blockquote>
<p>유저가 채팅방을 클릭해서 들어오는 경우</p>
</blockquote>
<p>사용자가 방을 클릭 했을 때 방 번호와 함께 /sub 을 요청하게 됩니다.</p>
<blockquote>
<p>[ destination = /sub/room/{roomId} ] </p>
</blockquote>
<p>서버로의 웹소켓 모든 요청의 경우 위에서 추가한 인터셉터를 타게 됩니다. 해당 인터셉터에서 CommndType을 구별하여 로직을 작성합니다.</p>
<pre><code class="language-java">@Component
public class WebsocketBrokerInterceptor implements ChannelInterceptor {

    @Override
    public Message&lt;?&gt; preSend(Message&lt;?&gt; message, MessageChannel channel) {
        final StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
        final StompCommand commandType = headerAccessor.getCommand();

        if(StompCommand.CONNECT == commandType) {
            //웹소켓 연결 요청 시 유저 인증

        } else if (StompCommand.SEND == commandType) {
            //pub 시 메시지 처리할 경우
        } else if (StompCommand.SUBSCRIBE == commandType) {
            //sub 시 처리할 코드를 여기서 작성
        } 
        return message;
    }
}
</code></pre>
<blockquote>
<p>여기에서는 따로 인증 또는 PUB,SUB 시 따로 로직을 추가하지 않겠습니다.</p>
</blockquote>
<p>추가로 방에 있는 유저에게 입장 메시지를 전송하기 위해 다음과 같이 요청합니다. </p>
<blockquote>
<p>[ destination = /pub/room/{roomId}/entered]</p>
</blockquote>
<p>pub 메시지는 MessageMapping 어노테이션이 있는 컨트롤러에서 처리하게 됩니다. 
해당 핸들러에서는 입장 메시지를 생성하여 해당 room에 있는 유저에게 메시지를 보냅니다.</p>
<pre><code class="language-java">@Slf4j
@RestController
@RequiredArgsConstructor
public class BrokerController {

    private final SimpMessagingTemplate template;

    @MessageMapping(&quot;/room/{roomId}/entered&quot;)
    public void entered(@DestinationVariable(value = &quot;roomId&quot;) String roomId, MessageDto message){
        log.info(&quot;# roomId = {}&quot;, roomId);
        log.info(&quot;# message = {}&quot;, message);
        final String payload = message.getWriter() + &quot;님이 입장하셨습니다.&quot;;
        template.convertAndSend(&quot;/sub/room/&quot; + roomId, payload);
    }
}</code></pre>
<ul>
<li>MessageDto</li>
</ul>
<pre><code class="language-java">@Getter
@NoArgsConstructor
public class MessageDto {

    private String message;
    private String writer;
}
</code></pre>
<h3 id="📘시나리오2">📘시나리오2</h3>
<blockquote>
<p>방에 들어온 유저가 메시지를 보낼경우</p>
</blockquote>
<p>사용자가 방에 들어온 후 메시지를 보낼때는</p>
<blockquote>
<p>[ destination = /pub/room/{roomId} ] </p>
</blockquote>
<p>과 함께 메시지를 서버에 요청하게 됩니다.<br>여기에서는 /pub 하는 데이터를 @MemssageMapping한 controller에서 처리하게 됩니다.</p>
<pre><code class="language-java">    @MessageMapping(&quot;/room/{roomId}&quot;)
    public void sendMessage(@DestinationVariable(value = &quot;roomId&quot;) String roomId, MessageDto message) {
        log.info(&quot;# roomId = {}&quot;, roomId);
        log.info(&quot;# message = {}&quot;, message);

        template.convertAndSend(&quot;/sub/room/&quot; + roomId, message.getMessage());
    }</code></pre>
<p>이제 APic 으로 테스트를 진행해보도록 하겠습니다.</p>
<h3 id="📘시나리오-테스트">📘시나리오 테스트</h3>
<p>APIC에서 탭 2개를 열고 두 개 모두 연결을 해둔 상태에서 입장 메시지가 잘 오는지, 
메시지 전송 시 잘 도착하는지 테스트 해보겠습니다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/ce0cf42e-b71f-4c2b-ac4d-0efd57564e7e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/168cf91f-d26a-4da9-9083-7286b58d30b2/image.png" alt=""></p>
<p>두 개의 웹소켓을 연결해둔 상태에서 첫 탭에서 /pub/room/{roomId}/entered를 보내게되면, </p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/5ae6f7bc-bc89-49f1-b673-62b284c02dcd/image.png" alt="">
반대편 탭에 도착한 것을 볼 수 있다.<br>다음으로는 메시지 전송을 테스트 해보자. 메시지 전송 시에는 /pub/room/{roomId} 로 보내며 payload로 message를 같이 보낸다. 
<img src="https://velog.velcdn.com/images/xxx-sj/post/edafd2f1-ad42-426f-85a8-72c449cc792e/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/573bc227-213b-48d7-b46c-9442a40e160e/image.png" alt=""></p>
<p>잘 전송되는 것을 확인 할 수 있다.</p>
<h2 id="정리">정리</h2>
<p>지금까지 STOMP를 이용해 기본적인 구현과 큰 흐름을 알아봤습니다. 다음에는 엔티티를 추가하고 메모리로 디비를 만들어서 좀더 살을 붙여보겠습니다.</p>
<p>전체 코드는 <a href="https://github.com/xxx-sj/spring-exmaple-projects">xxx-sj</a>에서 보실 수 있습니다.</p>
<h5 id="출처">출처:</h5>
<p><a href="https://docs.spring.io/spring-framework/reference/web/websocket/stomp/handle-annotations.html">https://docs.spring.io/spring-framework/reference/web/websocket/stomp/handle-annotations.html</a>
<a href="https://brunch.co.kr/@springboot/695">https://brunch.co.kr/@springboot/695</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring-vue]웹소켓으로 채팅 구현하기 (2) - 프로젝트 생성 및 스프링웹소켓]]></title>
            <link>https://velog.io/@xxx-sj/spring%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-2-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9B%B9%EC%86%8C%EC%BC%93</link>
            <guid>https://velog.io/@xxx-sj/spring%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-2-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9B%B9%EC%86%8C%EC%BC%93</guid>
            <pubDate>Fri, 10 Nov 2023 16:28:58 GMT</pubDate>
            <description><![CDATA[<h2 id="📗프로젝트-생성">📗프로젝트 생성</h2>
<p>이 부분은 모두 하실 줄 안다고 생각하여 빠르게 넘기겠습니다. 간단하게 제가 프로젝트를 만들면서 추가한 의존성만 보여드리고 넘어가도록 하겠습니다</p>
<pre><code class="language-gradle">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-websocket&#39;
    compileOnly &#39;org.projectlombok:lombok&#39;
    runtimeOnly &#39;com.h2database:h2&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
}</code></pre>
<p>웹소켓을 사용하기 위해서는 하위 의존성을 추가하자.</p>
<pre><code class="language-gradle">implementation &#39;org.springframework.boot:spring-boot-starter-websocket&#39;</code></pre>
<p>프로젝트 생성 후 잘 생성되었는지 반드시 WebsocketApplication으로 실행해보자.</p>
<h2 id="📗spring-websocket">📗spring-websocket</h2>
<p>아래코드는 <a href="https://docs.spring.io/spring-framework/reference/web/websocket/server.html">스프링 doc</a>을 참고하여 구현합니다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), &quot;/myHandler&quot;).setAllowedOriginPatterns(&quot;*&quot;);
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
}</code></pre>
<blockquote>
<ul>
<li>@EnableWebSocket : 웹소켓 서버를 사용하도록 추가합니다.</li>
<li>/myHandler : 웹소켓 서버의 endPoint를 &quot;/myHandler&quot; 로 설정합니다.</li>
<li>setAllowedOriginPatterns(&quot;*&quot;) : 웹소켓 서버로의 요청을 모두 수용한다. (실제로는 제한할 것)</li>
<li>myHandler() : 웹소켓 핸들러로 MyHandler 클래스를 설정한다. </li>
</ul>
</blockquote>
<p><strong>MyHandler의 클래스는 다음과 같다. 간단히 TextWebSocketHandler extends하여 정의한다.</strong></p>
<pre><code class="language-java">public class MyHandler extends TextWebSocketHandler {


    //최초 연결 시
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {

    }

    //양방향 데이터 통신할 떄 해당 메서드가 call 된다.
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //do something
    }

    //웹소켓 종료
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

    }

    //통신 에러 발생 시
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {


    }
}
</code></pre>
<p>MyHandler에서는 4개의 메서드를 구현하는데</p>
<blockquote>
<ul>
<li>afterConnectionEstablished: 최초 연결시 call 된다.</li>
<li>handleTextMessage: 연결 후 메세지를 주고 받을 때 call 된다. </li>
<li>afterConnectionClosed: 웹소켓이 끊키면 call 된다.</li>
<li>handleTransportError: 통신 에러 발생시 call 된다.</li>
</ul>
</blockquote>
<hr>
<p>여기까지 기초적인 설정은 끝났으니 postman으로 테스트를 진행하면서 메서드에 어떠한 데이터들이 들어오는지 확인해보자.
postman으로 websocket 테스트 하는 방법은 많은 블로그에서 소개하고 있으니 따로 적진 않겠습니다.</p>
<h3 id="📘postman으로-테스트-하기">📘postman으로 테스트 하기</h3>
<p>postman에서 websocket로 연결 요청을 보내면 다음과 같이 afterConnectionEstalished메서드에 break point가 걸리는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/2b5810d1-42cf-4da2-a463-f69e8a845c8e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/bfad631e-f8bc-4554-8442-8d6ce96b3c8c/image.png" alt=""></p>
<p>해당 메서드의 session 인자의 데이터를 확인해보면 다음과 같다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/debdc5f2-55aa-4f70-a8f9-2de218aab209/image.png" alt=""></p>
<p>session 객체에 id가 보이는데, 이 session id를 key로 갖는 map을 통해 session을 관리해보도록 하자. </p>
<p>인스턴스 필드로 map을 선언하고, afterConnectionEstablished 메서드가 호출될 때 id 와 session을 map에 put 한다. 코드로 보면 다음과 같다.</p>
<h3 id="📘session객체-저장하기">📘session객체 저장하기</h3>
<pre><code class="language-java">    private final Map&lt;String, WebSocketSession&gt; sessions = new HashMap&lt;&gt;();

    //최초 연결 시
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        final String sessionId = session.getId();

        sessions.put(sessionId, session);

    }</code></pre>
<p>다음으로는 최초 연결 시 session객체를 저장하면서, 저장되어있는 다른 session 객체들에게 알림을 보내는 코드를 추가해보자.</p>
<pre><code class="language-java">    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        final String sessionId = session.getId();
        final String enteredMessage = sessionId + &quot;님이 입장하셨습니다.&quot;;
        sessions.put(sessionId, session);

        sessions.values().forEach((s) -&gt; {

            try {
                if(!s.getId().equals(sessionId) &amp;&amp; s.isOpen()) {

                    s.sendMessage(new TextMessage(enteredMessage));
                }
            } catch (IOException e) {}
        });


    }</code></pre>
<p>여기까지 완료했다면 postman을 통해 tab을 2개를 열어서 코드 테스트를 진행해보자.</p>
<h3 id="📘postman-테스트">📘postman 테스트</h3>
<p>시나리오는 다음과 같다.</p>
<blockquote>
<ul>
<li>한쪽을 연결해둔 상태에서 다른 한 탭에서 웹소켓 연결 시 최초 연결했던 탭에 연결되었다는 메시지를 받아야 한다.</li>
</ul>
</blockquote>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/653a06af-e256-4918-909b-8466c7818c54/image.png" alt=""></p>
<p>이제 다른 탭에서 새로 연결을 해보면~</p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/9938fcea-28f8-4731-8c82-cbf60ec31211/image.png" alt=""></p>
<p>위 화면과 같이 입장 메시지를 받을것을 확인할 수 있다.</p>
<h3 id="📘handletextmessage-메서드-구현">📘handleTextMessage 메서드 구현</h3>
<p>다음으로는 연결된 웹소켓들이 메시지를 주고받을 때 call되는 메서드를 구현한다. 
이것도 별 다르지 않게 연결되어있는 모든 session에게 메시지를 보낸다.</p>
<pre><code class="language-java">    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //do something
        final String sessionId = session.getId();
        sessions.values().forEach((s) -&gt; {

            if (!s.getId().equals(sessionId) &amp;&amp; s.isOpen()) {
                try {
                    s.sendMessage(message);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }</code></pre>
<h3 id="📘handletextmessage-메서드-테스트">📘handleTextMessage 메서드 테스트</h3>
<p>동일하게 postman으로 테스트를 진행해보자. 이번에는 두 탭 모두 연결을 한 후, 한 탭에서 메시지를 보내 메시지가 오는지 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/8db7be98-761a-490e-9608-d309ae23cfad/image.png" alt=""></p>
<p>일반적인 text를 입력한 후 send를 보내면 다른 쪽에 메시지가 도착하는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/43031489-0dd5-4ffb-9c59-87fc07a67f4a/image.png" alt=""></p>
<h3 id="📘afterconnectionclosed-메서드-구현">📘afterConnectionClosed 메서드 구현</h3>
<p>해당 메서드에서는 session이 끊기게 되면 map에 저장되어있는 객체를 remove 하고, 다른 접속자들에게 leave message를 보낸다. </p>
<blockquote>
<p>해당 메서드의 인자 중 CloseStatus status에는 종료상태에 대해 정의되어 있으니 필요에 따라 분기를 통해 정의할 수 있다.</p>
</blockquote>
<pre><code class="language-java">//웹소켓 종료
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        final String sessionId = session.getId();
        final String leaveMessage = sessionId + &quot;님이 떠났습니다.&quot;;
        sessions.remove(sessionId); // 삭제

        //메시지 전송
        sessions.values().forEach((s) -&gt; {

            if (!s.getId().equals(sessionId) &amp;&amp; s.isOpen()) {
                try {
                    s.sendMessage(new TextMessage(leaveMessage));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/cbdeecf1-490a-4823-b37a-25decd39edc2/image.png" alt=""></p>
<p>테스트로 잘 되는 것도 확인하였다.</p>
<h3 id="📕리팩토링">📕리팩토링</h3>
<p>간단하게 구현하긴 했지만 각각의 메서드에서 메시지를 전송하는 부분은 message를 제외하면 모두 같기 때문에 하나의 메서드를 정의하고 메서드를 call하도록 수정한다. 전체코드는 다음과 같다.</p>
<pre><code class="language-java">
package com.sj.websocket.handler;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MyHandler extends TextWebSocketHandler {

    private final Map&lt;String, WebSocketSession&gt; sessions = new HashMap&lt;&gt;();

    //최초 연결 시
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        final String sessionId = session.getId();
        final String enteredMessage = sessionId + &quot;님이 입장하셨습니다.&quot;;

        sessions.put(sessionId, session);

        sendMessage(sessionId, new TextMessage(enteredMessage));

    }

    //양방향 데이터 통신할 떄 해당 메서드가 call 된다.
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //do something
        final String sessionId = session.getId();
        sendMessage(sessionId, message);
    }

    //웹소켓 종료
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        final String sessionId = session.getId();
        final String leaveMessage = sessionId + &quot;님이 떠났습니다.&quot;;
        sessions.remove(sessionId); // 삭제

        sendMessage(sessionId, new TextMessage(leaveMessage));

    }

    //통신 에러 발생 시
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}

    private void sendMessage(String sessionId, WebSocketMessage&lt;?&gt; message) {
        sessions.values().forEach(s -&gt; {
            if(!s.getId().equals(sessionId) &amp;&amp; s.isOpen()) {
                try {
                    s.sendMessage(message);
                } catch (IOException e) {}
            }
        });
    }
}
</code></pre>
<h2 id="정리">정리</h2>
<p>websocket을 가지고 간단하게 구현하였는데, 위 코드는 입장 시, 퇴장 시, 메시지 보낼 때 모두 다른 세션에 메시지를 보내고 있다. 만약, 특정 사람에게 보내거나, 그룹에 보내야 한다면 최초 웹소켓 연결 시 서버에 object 형식의 데이터를 보내어 그룹을 짓거나 특정 사용자에게 메시지를 보낼 수 있다.</p>
<p>위의 코드는 <a href="https://github.com/xxx-sj/spring-exmaple-projects">websocket repository</a> 에서 볼 수 있습니다.</p>
<h4 id="참고">참고</h4>
<p><a href="https://brunch.co.kr/@springboot/695">https://brunch.co.kr/@springboot/695</a>
<a href="https://docs.spring.io/spring-framework/reference/web/websocket/server.html">https://docs.spring.io/spring-framework/reference/web/websocket/server.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring-vue] 웹소켓으로 채팅 구현하기(1) - 웹소켓이란]]></title>
            <link>https://velog.io/@xxx-sj/Spring-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B01-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@xxx-sj/Spring-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B01-%EC%9B%B9%EC%86%8C%EC%BC%93%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Fri, 10 Nov 2023 13:28:16 GMT</pubDate>
            <description><![CDATA[<h1 id="계기">계기</h1>
<p>회사에서 웹소켓으로 모바일과 통신하는 프로젝트를 맡게되면서 공부도 할겸 간단한 채팅 프로젝트를 만들어 보고자 한다. 필자도 웹소켓에 대해 모른다.. 앞으로 공부하며 알아가 보도록 하자.</p>
<h1 id="웹소켓-이전">웹소켓 이전</h1>
<p>웹소켓 이전에는 다음과 같은 방법으로 통신을 했었다.</p>
<h3 id="⏳polling">⏳polling</h3>
<p>polling 같은 경우는 client 측에서 매번 서버에 요청을 보내면서 이벤트가 발생했는지에 대한 여부를 응답으로 받는 형식이다.</p>
<p><img src="https://velog.velcdn.com/images/xxx-sj/post/be3d8179-7f7e-42a1-bd0a-30d2eb6a1b2d/image.png" alt=""></p>
<h3 id="⏳long-polling">⏳Long polling</h3>
<p>long polling은 문자 그대로 좀 더 길게  polling하는 것이다. 클라이언트 측에서 요청을 보내면 이벤트가 발생하여 응답을 줄 때 까지 기다리는 것이다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/742463d7-5539-4585-a845-6e6f2c38c384/image.png" alt=""></p>
<p>위와 같은 통신은 real-time에는 거리가 멀고, 매번 요청을 해야 하거나, 요청을 보내고 이벤트가 발생할 때 까지 기다리는 등의 낭비가 발생한다.</p>
<h1 id="웹소켓이란">웹소켓이란?</h1>
<p>먼저 쉽게 알 수 있는 ChatGPT에 웹소켓에 대해 물어보면 다음과 같이 답하는데, 그 중 몇가지 맛만 보자.</p>
<blockquote>
<p>웹소켓(WebSocket)은 웹 어플리케이션에서 양방향 통신을 가능케 하는 통신 프로토콜입니다. 웹소켓은 기존의 HTTP 프로토콜과 함께 사용되며, 클라이언트와 서버 간에 실시간으로 데이터를 주고받을 수 있도록 해줍니다.</p>
</blockquote>
<blockquote>
<p>기본적인 특징과 개념은 다음과 같습니다:
양방향 통신: 웹소켓은 양방향 통신을 지원하며, 클라이언트와 서버 간에 데이터를 실시간으로 전송할 수 있습니다. 이는 클라이언트가 서버로 메시지를 보내고, 서버가 클라이언트에게 메시지를 보낼 수 있음을 의미합니다.</p>
</blockquote>
<blockquote>
<p>실시간성: 일반적인 HTTP 요청과 달리 웹소켓은 계속해서 연결을 유지하므로 실시간으로 데이터를 전송하는 데 효과적입니다. 이는 채팅 애플리케이션, 주식 시세 업데이트, 온라인 게임 등 실시간 업데이트가 필요한 시나리오에서 특히 유용합니다.</p>
</blockquote>
<blockquote>
<p>프로토콜: 웹소켓 프로토콜은 RFC 6455에 정의되어 있습니다. 기본적으로는 80번 포트를 사용하며, 보안 연결인 경우에는 443번 포트를 사용합니다.</p>
</blockquote>
<blockquote>
<p>웹소켓은 웹 어플리케이션에서 실시간 통신이 필요한 많은 경우에 사용됩니다. 브라우저에서는 JavaScript를 통해 웹소켓을 사용할 수 있고, 서버 측에서는 여러 언어 및 프레임워크를 통해 구현할 수 있습니다.</p>
</blockquote>
<p>부족한 내용은 <a href="https://ko.wikipedia.org/wiki/%EC%9B%B9%EC%86%8C%EC%BC%93">위키백과</a> 에서 더 많이 내용을 볼 수 있다.</p>
<p>위의 설명에서도 볼 수 있듯이 웹소켓은 양방향으로 통신이 가능하며, 실시간으로 데이터를 주고 받을 수 있도록 해준다는 것이 포인트이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 2579번: 계단 오르기 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-2579%EB%B2%88-%EA%B3%84%EB%8B%A8-%EC%98%A4%EB%A5%B4%EA%B8%B0-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-2579%EB%B2%88-%EA%B3%84%EB%8B%A8-%EC%98%A4%EB%A5%B4%EA%B8%B0-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 14:44:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/2d0ee506-d3f1-4a55-ab28-44d9f67451be/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/b2624136-3163-4434-9989-511e4bec2699/image.png" alt=""></p>
<h3 id="문제-접근">문제 접근</h3>
<p>이 문제의 키 포인트는 <strong>마지막 계단을 밟는다</strong>에 있다.<br>마지막에 밟는 계단이 N 이라고 했을 때, N을 밟을 수 있는 조건은 2가지가 있다.   </p>
<ol>
<li>N 과 N - 1 계단을 밟는다면 N - 2는 밟을 수 없기 때문에 N -3 까지 누적된 값을 더한다.<ul>
<li>즉, (dp[N - 3] (N -3 까지의 누적합) + number[N] + number[N - 1]) 이 될 것이다.</li>
</ul>
</li>
<li>다음으로는 N - 1을 밟지 않을 경우이다. 이때는, N 과 N - 2까지의 누적된 값을 더하게 된다.<ul>
<li>dp[N - 2] + number[N] </li>
</ul>
</li>
</ol>
<p>이렇게 보고 나면 쉽게 문제를 해결할 수 있다.   </p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

    static int[] stairs;
    static Integer[] dp;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        stairs = new int[N + 1];
        dp = new Integer[N + 1];

        for(int i = 1;i &lt;= N; i++) {
            stairs[i] = Integer.parseInt(br.readLine());
        }

        dp[0] = 0;
        dp[1] = stairs[1];
        //조건을 추가하지않으면 N = 1일 때, dp[2], stairs[2]에 접근하려다 오류 발생함.
        if (N &gt;= 2) {
            dp[2] = stairs[1] + stairs[2];
        }

        System.out.println(recur(N));
    }

    private static int recur(int N) {

        if(dp[N] == null) {
            dp[N] = Math.max(recur(N - 2), recur(N - 3) + stairs[N - 1]) + stairs[N];
        }

        return dp[N];
    }
}</code></pre>
<h3 id="코드-상세">코드 상세</h3>
<h4 id="top-down">top-down</h4>
<p>top-down 문제는 위에서 아래로 진행하며 재귀를 통해 dp를 초기화한다는 것을 기억하자.   </p>
<ul>
<li>N을 입력받는다.</li>
<li>입력받은 N으로 계단의 배열과 dp배열을 초기화한다.</li>
<li>dp배열에는 각각의 계단에 누적합이 저장된다.<pre><code class="language-java">static int[] stairs;
static Integer[] dp;
</code></pre>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</p>
<p>int N = Integer.parseInt(br.readLine());</p>
<p>stairs = new int[N + 1];
dp = new Integer[N + 1];</p>
<pre><code>- 계단을 입력받아 stairs 배열을 초기화한다.
```java
for(int i = 1;i &lt;= N; i++) {
    stairs[i] = Integer.parseInt(br.readLine());
}</code></pre><ul>
<li>초기값을 설정한다.<ul>
<li>초기값으로는 dp[0]= 0; </li>
<li>dp[1] 의 값으로는 이 전값이 없기 때문에 dp[1] = stairs[1] 이 된다.</li>
<li>dp[2] 또한 초기화를 해주는데, 이 때는 N 이 1보다 클 때 즉, 2부터 초기화가 가능하게 한다.<ul>
<li>이유는 N = 1 을 입력받았을 때 dp[2]에 접근하려고 할 때 오류가 발생하기 때문이다.</li>
</ul>
</li>
<li>dp[2] 까지 초기화하는 이유는 잘 생각해보면 이후에 나올 재귀함수에서 dp[N - 3] 까지 접근하기 때문이다.  까지<pre><code class="language-java">dp[0] = 0;
dp[1] = stairs[1];
</code></pre>
</li>
</ul>
</li>
</ul>
<p>//조건을 추가하지않으면 N = 1일 때, dp[2], stairs[2]에 접근하려다 오류 발생함.
if (N &gt;= 2) {
    dp[2] = stairs[1] + stairs[2];
}</p>
<pre><code>- 재귀함수를 통해 dp를 초기화한다.
  - 재귀함수 내에서는 dp[N]이 초기화 되지 않았을 때 dp[N] 값을 초기화한다.,
  - 위에서 설명한 dp[N - 2]까지 밟고 stairs[N]을 밟는 조건과
  - dp[N - 3] 까지 밟고 stairs[N - 1] + stairs[N] 두개를 밟는 조건 중 더 큰 값을 dp[N]에 저장한다.
```java
private static int recur(int N) {

    if(dp[N] == null) {
        dp[N] = Math.max(recur(N - 2), recur(N - 3) + stairs[N - 1]) + stairs[N];
    }

    return dp[N];
}</code></pre><h3 id="정리">정리</h3>
<p>이 문제는 N번쨰 계단을 밟는다.가 핵심이었다. N에 도달하기 위한 조건을 서로 비교하여 가장 큰 값을 dp배열에 넣는 문제였다. 문제에서도 조건을 주었듯이 해당 조건을 가지고 N에 도달할 수 있는 조건을 만드는 것이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 1699번: 제곱수의 합 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-1699%EB%B2%88-%EC%A0%9C%EA%B3%B1%EC%88%98%EC%9D%98-%ED%95%A9-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-1699%EB%B2%88-%EC%A0%9C%EA%B3%B1%EC%88%98%EC%9D%98-%ED%95%A9-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 14:34:05 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/974a67e4-8c4a-49e5-a2e9-9331c689e8d6/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/fc1a0639-12af-4b72-826f-ee38af2ea615/image.png" alt=""></p>
<h3 id="문제-접근">문제 접근</h3>
<p>이 문제는 설명과 코드 상세를 보며 먼저 이해해보자.</p>
<p>이 문제는 손으로 직접 써보고 나서 이해하기 쉬웠다.<br>여기서도 dp[N]의 값은 제곱수의 최소 갯수가 저장된다.<br>N = 11을 기준으로 보자면
쉽게 표시를 하기위해 지수는 생략하고 밑 수만 표기하면   </p>
<ul>
<li><p>dp     제곱수   갯수  </p>
</li>
<li><p>dp[1] = 1 =     1; </p>
</li>
<li><p>dp[2] = 1 + 1 = 2;</p>
</li>
<li><p>dp[3] = 1 + 1 + 1 = 3;</p>
</li>
<li><p>dp[4] = 2 = 1;</p>
</li>
<li><p>dp[5] = 2 + 1 = 2;</p>
</li>
<li><p>dp[6] = 2 + 1 + 1 = 3;</p>
</li>
<li><p>dp[7] = 2 + 1 + 1 + 1 = 4;</p>
</li>
<li><p>dp[8] = 2 + 2 = 2;</p>
</li>
<li><p>dp[9] = 3 = 1;</p>
</li>
<li><p>dp[10] = 3 + 1 = 2;</p>
</li>
<li><p>dp[11] = 3 + 1 + 1 = 3;
와 같다.<br>예전에 배운 가우스법칙을 떠올리면 11을 나타내기 위해서</p>
</li>
<li><p>10 + 1</p>
</li>
<li><p>9 + 2</p>
</li>
<li><p>8 + 3</p>
</li>
<li><p>7 + 4</p>
</li>
<li><p>6 + 5
와 같이 나타낼 수 있다. 
위 값을 잘 살펴보면 N = 11 일 때<br>[N - 1] + [1]
[N - 2] + [2]
...
[N - 5] + [5]
즉, [N - i] + [i] 가 된다.<br>이 문제를 bottom-up 방식으로 풀어낸다면 아래와 같을 것이다.</p>
</li>
<li><p>아래 for 문을 살펴보면 먼저 </p>
</li>
<li><p>min값을 초기값을 MAX_VALUE로 설정하고 </p>
</li>
<li><p>이중 for문에서는 i의 반만큼 for문을 순회한다.  </p>
</li>
<li><p>만약, j xj 가 i 와 같다면 즉, 3 x 3 = 9와 같이 곱하여 같다면</p>
</li>
<li><p>min 값은 1로 초기하고 다음으로 넘어간다.</p>
</li>
<li><p>그게 아니라면 min값과 dp[i - j] + dp[j] 중에 큰 값을 min에 저장한다.</p>
</li>
<li><p>마지막에 min값을 dp[i]에 저장한다.</p>
<pre><code class="language-java">for(int i = 2; i &lt;= N; i++) {
  int min = Integer.MAX_VALUE;

  for(int j = 1; j &lt;= i / 2; j++) {

      if(j * j == i) {
          min = 1;
          break;
      } else {
          min = Math.min(min, dp[i - j]  + dp[j]);
      }
  }

  dp[i] = min;
}</code></pre>
</li>
<li><p>위의 for문을 이해했다면 다음으로 불 필요한 계산을 줄이는 방법이 있을 것이다.</p>
</li>
<li><p>여기서는 <strong>제곱 수</strong>가 포인트인데, 제곱 수로 나타낼 수 있는 수가 있다면 최소 갯수 1이된다.</p>
</li>
<li><p>1, 4, 9, 16 과 같이 = 1^1, 2^2, 3^2, 4^2와 같은 값들이다.    </p>
</li>
<li><p>다시말해,</p>
</li>
<li><p>10 + 1</p>
</li>
<li><p>9 + 2</p>
</li>
<li><p>8 + 3</p>
</li>
<li><p>7 + 4</p>
</li>
<li><p>6 + 5</p>
</li>
<li><p>와 같이 다 계산할 필요없이 11아래에 존재하는 제곱 수 끼리만 비교를 하면 된다는 말이다.</p>
</li>
<li><p>11아래에 있는 제곱수는 1, 4, 9 가 있다.   </p>
</li>
<li><p>즉, </p>
</li>
<li><p>10 + 1 [1^1]</p>
</li>
<li><p>7 + 4 [2^2]</p>
</li>
<li><p>2 + 9 [3^2]
값만 비교하면 된다. 이 조건을 가지고 다시 for문을 나타내면 아래와 같다.</p>
</li>
<li><p>바깥 for문은 2 ~ N까지 순회한다.</p>
<ul>
<li>1을 하지않는 이유는 1로 고정이기때문에. </li>
<li>1은 1^2 값 즉, dp[1] = 1 이 초기값으로 들어가게된다.</li>
</ul>
</li>
<li><p>for문에서는 기본적으로 1^2만으로 나타낸 수를 dp[i]에 초기화한다.</p>
<ul>
<li>1^2로 나타내는 수는 1 + 1+ 1.... = i 즉, i개만큼 1이 더해진다.</li>
</ul>
</li>
<li><p>내부 for문에서는 위에서 말했던 숫자 N 아래에 있는 제곱만을 계산할 수 있도록 조건으로 </p>
</li>
<li><p>j*j &lt;= i 로 설정하였다.</p>
</li>
<li><p>for문 내부에서는 dp[i]값과 dp[i - j*j] + 1 중 작은 값을 dp[i]에 저장한다</p>
</li>
<li><p>예를들어 i = 11 일때, </p>
</li>
<li><p>dp[11] = Math.min(dp[11], dp[11 - 1(10)] + 1);</p>
</li>
<li><p>dp[11] = Math.min(dp[11], dp[11 - 4(7)] + 1);</p>
</li>
<li><p>dp[11] = Math.min(dp[11], dp[11 - 9(2)] + 1);</p>
</li>
<li><p>중 작은 값을 dp[11]에 저장한다.</p>
</li>
<li><p>여기서 + 1을 하는 이유는 제곱 수의 갯수이다.</p>
</li>
<li><p>헷갈리지 말아야 할 것은 구하려는 값은 숫자가 아닌 <strong>제곱의 갯수</strong>이다.</p>
</li>
<li><p>dp[10] + 1 [1^1] = dp[10] + 1</p>
</li>
<li><p>dp[7] + 4 [2^2] = dp[7] + 1</p>
</li>
<li><p>dp[2] + 9 [3^2] = dp[2] + 1</p>
<pre><code class="language-java">private static int bottom_up2(int N) {

  for(int i = 2; i &lt;= N; i++) {
      dp[i] = i;
      for(int j = 1; j * j&lt;= i; j++) {
          dp[i] = Math.min(dp[i], dp[i - j*j] + 1);
      }
  }

  return dp[N];
}</code></pre>
</li>
<li><p>N을 입력받아 dp배열을 초기화한다</p>
</li>
<li><p>이 때 초기화는 N + 1로 한다.</p>
</li>
<li><p>이유는 인덱스가 0부터 시작하기 때문이고, 문제에서 1부터 시작하기 때문에</p>
<pre><code class="language-java">static Integer[] dp;
</code></pre>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</p>
<p>int N = Integer.parseInt(br.readLine());</p>
<p>dp = new Integer[N + 1];</p>
<pre><code>- 초기값을 설정한다
```java
dp[0] = 0;
dp[1] = 1;</code></pre><ul>
<li><p>bottom-up으로 dp값을 초기화한다.</p>
<pre><code class="language-java">private static int bottom_up2(int N) {

  for(int i = 2; i &lt;= N; i++) {
      dp[i] = i;
      for(int j = 1; j * j&lt;= i; j++) {
          dp[i] = Math.min(dp[i], dp[i - j*j] + 1);
      }
  }

  return dp[N];
}</code></pre>
</li>
<li><p>bottom-up 메서드를 호출하여 출력한다.</p>
<pre><code class="language-java">System.out.println(bottom_up2(N));</code></pre>
</li>
</ul>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static Integer[] dp;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        dp = new Integer[N + 1];

        dp[0] = 0;
        dp[1] = 1;

        System.out.println(bottom_up2(N));
    }

    private static int bottom_up(int N) {

        for(int i = 2; i &lt;= N; i++) {
            int min = Integer.MAX_VALUE;

            for(int j = 1; j &lt;= i / 2; j++) {

                if(j * j == i) {
                    min = 1;
                    break;
                } else {
                    min = Math.min(min, dp[i - j]  + dp[j]);
                }
            }

            dp[i] = min;
        }

        return dp[N];
    }

    private static int bottom_up2(int N) {

        for(int i = 2; i &lt;= N; i++) {
            dp[i] = i;
            for(int j = 1; j * j&lt;= i; j++) {
                dp[i] = Math.min(dp[i], dp[i - j*j] + 1);
            }
        }

        return dp[N];
    }


}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 11054번: 가장 긴 바이토닉 수열 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11054%EB%B2%88-%EA%B0%80%EC%9E%A5-%EA%B8%B4-%EB%B0%94%EC%9D%B4%ED%86%A0%EB%8B%89-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11054%EB%B2%88-%EA%B0%80%EC%9E%A5-%EA%B8%B4-%EB%B0%94%EC%9D%B4%ED%86%A0%EB%8B%89-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 13:59:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/ccc52b51-21cc-4687-a4b0-dd1d2505f30b/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>이 문제는 어떤한 값 기분으로 왼쪽은 증가하는 수열 [LIS] 오른쪽으로는 감소하는 수열[LDS]이란걸 알 수 있다.<br>그렇다면 이 문제 같은경우는 어떻게 풀면될까.. 하고 생각해보면<br>어떠한 수열의 값 N을 기준으로 왼쪽으로는 LIS를 오른쪽으로는 LDS를 진행해준 후 두 개의 배열의 값을 합친 값 중<br>가장 큰 값을 출력하면 되지 않을까란 생각이 들 수 있다.<br>말 그대로 N을 기준으로 N - 1 부터 시작하여 0까지는 LIS를 통해 LIS_DP를 초기화하고,<br>N + 1 부터 시작하여 length - 1까지는 LDS를 통해 LDS_DP를 초기화하여 두 개의 배열의 합을 구하면 된다.<br>여기서 하나더, 이전에 LDS문제는 비교할 때 (number[N] &lt; number[j]) 가 성립할 때 였는데 이때는 0 부터 시작하여 N까지 였기 때문에 N 기준 앞 쪽의 수가 더 컸어야 했지만, 여기서는 반대로 N부터 시작하여 Length - 1 까지 순회하기 때문에 <strong>N이 더 커야 한다</strong>는 것을 명심하자.
(number[N] &gt; number[j])
여기서 조심해야 하는 것은 LIS,LDS 2번을 하게 되면 자기자신 N이 2번 들어가기 때문에 <strong>두 개의 합한 값에 -1</strong>을 반드시 해주어야한다.   </p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">
import java.awt.im.InputContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static int[] numbers;

    static Integer[] lis_dp;
    static Integer[] lds_dp;
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        numbers = new int[N];

        lis_dp = new Integer[N];
        lds_dp = new Integer[N];

        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

        for(int i = 0; i &lt; N; i++) {
            numbers[i] = Integer.parseInt(st.nextToken());
        }

        for(int i = 0; i &lt; N; i++) {
            recur_lis(i);
        }

        for(int i = N - 1; i &gt;= 0; i--) {
            recur_lds(i);
        }

        int max = Integer.MIN_VALUE;

        for(int i = 0; i &lt; N; i++) {

            max = Math.max(max, recur_lds(i) + recur_lis(i) - 1);
        }

        System.out.println(max);
    }


    static int recur_lis(int N) {

        if(lis_dp[N] == null) {
            lis_dp[N] = 1;

            for(int i = N - 1; i &gt;= 0; i--) {

                if (numbers[N] &gt; numbers[i]) {
                    lis_dp[N] = Math.max(lis_dp[N], recur_lis(i) + 1);
                }
            }
        }


        return lis_dp[N];
    }


    static int recur_lds(int N) {

        if(lds_dp[N] == null) {
            lds_dp[N] = 1;

            for(int i = N + 1; i &lt; lds_dp.length; i++) {
                if(numbers[N] &gt; numbers[i])  {
                    lds_dp[N] = Math.max(lds_dp[N], recur_lds(i) + 1);
                }
            }
        }

        return lds_dp[N];
    }
}
</code></pre>
<h3 id="정리">정리</h3>
<p>LIS 와 LDS를 알고 조합 할 수 있었다면 쉽게 해결할 수 있었을 것이다. LIS, LDS를 하되, 모두 N에서 시작하기 때문에 N의 값(1)이 중복되기 때문에 답에서 -1을 해주는 것이 중요했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 1912번: 연속합 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-1912%EB%B2%88-%EC%97%B0%EC%86%8D%ED%95%A9-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-1912%EB%B2%88-%EC%97%B0%EC%86%8D%ED%95%A9-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 13:49:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/f1c95aee-4aff-4a51-8954-0c6bf6669b7c/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>이 문제는 손으로 써보면 쉽게 해결할 수 있는 문제이다.<br>이 문제의 핵심은 연속된 몇 개의 수를 합하여 가장 큰 합을 구하는 것이다.<br>맨 처음 이 문제를 풀 떄 수열로 헷갈려서 문제를 틀렸는데, 잘 읽어보면 연속된 몇 개의 수에 답이 있다.<br>0부터 N까지 수를 합하면서 현재 [자기자신] 과 [이 전까지 합한 수 + 자기자신]을 비교하여 큰 값을 dp에 저장하면 된다.<br>예를들어서 예제 입력<br>10 -4 3 1 5 6 -35 12 21 -1   </p>
<ul>
<li>idx[0] = 10;<ul>
<li>idx[0] 이전 값을 비교할 것이 없기 때문에 자기자신 10이 가장 크다.</li>
<li>dp[0] = 10;</li>
</ul>
</li>
<li>idx[1] = -4; [이전 값 = 10]+ [자기자신 = -4] = [6];<ul>
<li>idx[1]은 -4 이지만 이전 값 10을 합치면 6으로 자기자신보다 크다.</li>
<li>dp[1] = dp[0] + idx[1] = 6;</li>
</ul>
</li>
<li>idx[2] = 3; <ul>
<li>idx[2]는 비교할 것 없이 이전 값과 자신을 합한 값이 크다.</li>
<li>dp[2] = dp[1] + idx[2] = 9;</li>
</ul>
</li>
<li>idx[3] = 1;<ul>
<li>idx[3]도 동일하다</li>
<li>dp[3] = dp[2] + idx[3] = 10;</li>
</ul>
</li>
<li>idx[4] = 5;<ul>
<li>idx[4]도 동일하다</li>
<li>dp[4] = dp[3] + idx[4] = 15;</li>
</ul>
</li>
<li>idx[5] = 6;<ul>
<li>idx[5] 도 동일하다</li>
<li>dp[5] = dp[4] + idx[5] = 21;</li>
</ul>
</li>
<li>idx[6] = -35;<ul>
<li>idx[6]은 음수인데, 자기자신 보다는 자기자신의 값 + 이전 합한 값을 합친 값이 더 크다.</li>
<li>즉, dp[6] = dp[5] + idx[6] = -14;</li>
</ul>
</li>
<li>idx[7] = 12;<ul>
<li>idx[7]은 이전값 + 자기자신 보다 자기자신의 수가 더 크다는 것을 한눈에 알 수 있다.  </li>
<li>여기에서는 이 전까지 합한 값을 버리게 되며, dp[7]에는 idx[7] 자기자신만 들어가게 된다.</li>
<li>dp[7] = idx[7] = 12;</li>
</ul>
</li>
<li>idx[8] = 21;<ul>
<li>idx[8]에서는 이전 값 dp[7]과 자기자신을 합한 값이 크다는것을 알 수 있다.</li>
<li>dp[8] = dp[7] + idx[8] = 33;</li>
</ul>
</li>
<li>idx[9] = -1;<ul>
<li>idx[9] 도 dp[8]과 자기자신을 합한 값이 더 크다.</li>
<li>dp[9] = dp[8] + idx[9] = 32;</li>
</ul>
</li>
</ul>
<p>이제 dp[]의 값을 모아보면 아래와 같다.<br>[10, 6, 9, 10, 15, 21, -14, 12, 33, 32]<br>여기서 가장 큰 수는 33이다. 33 = dp[8] 은 idx[7], idx[8]을 합친 값이란걸 알 수 있다.<br>이것을 코드로 보자면   </p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static Integer[] dp;
    static int[] numbers;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

        dp = new Integer[N];
        numbers = new int[N];

        for(int i = 0; i &lt; N; i++) {
            numbers[i] = Integer.parseInt(st.nextToken());
        }

        dp[0] = numbers[0];


        recur(N - 1);



        int max = Integer.MIN_VALUE;

        for(int i = 0; i &lt; N; i++) {
            max = Math.max(max, dp[i]);
        }

        System.out.println(max);


    }


    static int recur(int N) {

        if(dp[N] == null) {
            dp[N] = Math.max(recur(N - 1) + numbers[N], numbers[N]);
        }

        return dp[N];
    }
}
</code></pre>
<h3 id="코드-상세">코드 상세</h3>
<h3 id="top-down">top-down</h3>
<p>위에서 아래로 진행하며 자기자신의 값과 |이전 dp[]의 값 + 자기자신| 을 비교하여 더 큰 값을 dp[]에 넣는다.   </p>
<pre><code class="language-java">recur(N - 1);

static int recur(int N) {

    if(dp[N] == null) {
        dp[N] = Math.max(recur(N - 1) + numbers[N], numbers[N]);
    }

    return dp[N];
}</code></pre>
<ul>
<li>N을 입력받아 dp[], numbers[]을 초기화한다.</li>
<li>다음으로 입력받는 수열을 numbers 배열에 저장한다.<pre><code class="language-java">BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
</code></pre>
</li>
</ul>
<p>int N = Integer.parseInt(br.readLine());</p>
<p>StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);</p>
<p>dp = new Integer[N];
numbers = new int[N];</p>
<p>for(int i = 0; i &lt; N; i++) {
    numbers[i] = Integer.parseInt(st.nextToken());
}</p>
<pre><code>- dp[0]의 값을 초기화해준다.
  - dp[0]의 값은 이전 값과 비교할 값이 없기 때문에 number[0]의 값을 갖는다.
```java
dp[0] = numbers[0];</code></pre><ul>
<li>N - 1 부터 재귀를 호출하여 dp배열을 초기화한다.<pre><code class="language-java">recur(N - 1);
</code></pre>
</li>
</ul>
<p>static int recur(int N) {</p>
<pre><code>if(dp[N] == null) {
    dp[N] = Math.max(recur(N - 1) + numbers[N], numbers[N]);
}   

return dp[N];</code></pre><p>}</p>
<pre><code>- dp배열 중 가장 큰 값을 출력한다
```java
int max = Integer.MIN_VALUE;

for(int i = 0; i &lt; N; i++) {
    max = Math.max(max, dp[i]);
}

System.out.println(max);</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 11722번: 가장 긴 감소하는 부분 수열 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11722%EB%B2%88-%EA%B0%80%EC%9E%A5-%EA%B8%B4-%EA%B0%90%EC%86%8C%ED%95%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11722%EB%B2%88-%EA%B0%80%EC%9E%A5-%EA%B8%B4-%EA%B0%90%EC%86%8C%ED%95%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 13:46:33 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/a80ea759-4fb9-4b0a-99b6-8cbac4066497/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>LIS와 반대로 LDS 의 문제이다. 이 문제 또한 DP로 풀 수 있다.<br>LIS와는 반대로 감소하는 수열이기 떄문에 N에 대한 값을 비교할 때는 비교하는 N의 값이 이 전 값보다 작을 때 
조건을 만족하게 된다.<br>LIS를 풀었었다면 쉽게 LDS문제도 풀 수 있다.<br>코드로 보자면 아래와 같다.</p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static Integer[] dp;
    static int[] number;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        dp = new Integer[N + 1];
        number = new int[N + 1];

        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

        for(int i = 1; i &lt;= N;i ++) {
            number[i] = Integer.parseInt(st.nextToken());
        }

//        for(int i = N; i &gt;= 0; i--) {
//            recur_lds(i);
//        }
        bottom_up(N);

        int max = Integer.MIN_VALUE;

        for(int i = 1; i &lt;= N; i++) {
            max = Math.max(dp[i], max);
        }

        System.out.println(max);
    }

    static int recur_lds(int N) {

        if(dp[N] == null) {

            dp[N] = 1;
            for(int i = N - 1; i &gt;= 0; i--) {

                if(number[N] &lt; number[i]) {
                    dp[N] = Math.max(dp[N], recur_lds(i) + 1);
                }
            }
        }


        return dp[N];
    }

    static void bottom_up(int N) {
        for(int i = 1; i &lt;= N; i++) {
            dp[i] = 1;

            for(int j = 0; j &lt; i; j++) {

                if(number[i] &lt; number[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
    }
}</code></pre>
<h2 id="코드상세">코드상세</h2>
<h3 id="top-down">top-down</h3>
<ul>
<li>N을 입력받고 입력받은 N으로 배열을 초기화한다.<ul>
<li>dp 배열은 수열의 길이를 저장하는 배열이고</li>
<li>number 배열은 다음으로 입력받는 수열의 정보를 저장한다.<ul>
<li>N + 1로 초기화한 이유는 (1 &lt;= N &lt;= 1000)에 이해를 돕기위해 </li>
<li>인덱스0이 아닌 시작을 1로 잡았기 때문에<pre><code class="language-java">static Integer[] dp;
static int[] number;
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</p>
<p>int N = Integer.parseInt(br.readLine());</p>
<p>dp = new Integer[N + 1];
number = new int[N + 1];</p>
<pre><code>- 다음으로 입력받은 수열을 number 배열에 저장한다.
```java
StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

for(int i = 1; i &lt;= N;i ++) {
    number[i] = Integer.parseInt(st.nextToken());
}</code></pre><ul>
<li>재귀함수를 호출하여 dp배열을 초기화 한다.</li>
</ul>
<pre><code class="language-java">for(int i = N; i &gt;= 0; i--) {
   recur_lds(i);
}</code></pre>
<ul>
<li><p>재귀함수에서는 dp[N]의 값이 아직 초기화 되지 않았다면 먼저 dp[N]의 값을 1로 초기화한다. </p>
<ul>
<li>1로 초기화하는 이유는 수열의 길이를 저장하는 dp배열에 기본적으로 자기자신이 포함되기 때문에 1로 초기화한다.</li>
</ul>
</li>
<li><p>다음으로는 위에서 아래로 for-loop를 돌면서 값을 비교한다.</p>
<ul>
<li><p>이 떄 입력받은 number[N]의 값이 비교하려는 for-loop의 number[i]의 값보다 작다면 </p>
</li>
<li><p>dp[N]에 dp[N]과 dp[i] + 1의 값을 비교하여더 큰값을 저장한다.</p>
<ul>
<li>dp[i] + 1의 값은 다른말로 recur_lds(i) + 1 과 같다.<ul>
<li>그 이유는 dp[i]에 대하여 for-loop는 위에서 아래로 진행 중인데 아직 dp[i]에 해당하는 값은   </li>
<li>초기화 되지 않았기 때문에 재귀를 통해 초기화를 해준다.<pre><code class="language-java">static int recur_lds(int N) {
</code></pre>
</li>
</ul>
</li>
</ul>
<p>if(dp[N] == null) {</p>
<pre><code>dp[N] = 1;
for(int i = N - 1; i &gt;= 0; i--) {

    if(number[N] &lt; number[i]) {
        dp[N] = Math.max(dp[N], recur_lds(i) + 1);
    }
}</code></pre><p>}</p>
</li>
</ul>
</li>
</ul>
<pre><code>return dp[N];</code></pre><p>}</p>
<pre><code>- dp 배열을 순회하며 큰 값을 출력한다.
```java
int max = Integer.MIN_VALUE;

for(int i = 1; i &lt;= N; i++) {
    max = Math.max(dp[i], max);
}

System.out.println(max);</code></pre><h3 id="bottom-up">bottom-up</h3>
<ul>
<li>Bottom-up도 top-down과 비슷하지만 다른게 있다면 아래에서 부터 순회를 시작하여 dp 배열을 채우고 </li>
<li>2중 for-loop를 사용한다는 점이다.</li>
<li>헷갈릴수도 있는데 비교의 기준이 되는 number[N]의 값이 비교하려는 number[j]의 값 보다 작을 때 조건이 성립한다.<br>예를들어,  N = 4 일때 0,1,2,3을 비교하게 되는데 N = 4일때의 값은 20이고, 0,1,2,3 의 값은 각각 [10, 30, 10, 20]이다. 이 문제는 감소하는 수열로써 그대로 보이는 것 기준으로 왼쪽으로 갈 수록 수가 커야하고, 오른쪽으로 갈 수록 수가 작아져야 하기 때문에 number[N]의 값이 비교하려는 number[j]보다 작아야하는 것이다. </li>
<li>N을 입력받아 배열을 초기화한다.<pre><code class="language-java">BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
</code></pre>
</li>
</ul>
<p>int N = Integer.parseInt(br.readLine());</p>
<p>dp = new Integer[N + 1];
number = new int[N + 1];</p>
<pre><code>- 다음으로 입력받는 값으로 number 배열을 초기화한다.
```java
StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

for(int i = 1; i &lt;= N;i ++) {
    number[i] = Integer.parseInt(st.nextToken());
}</code></pre><ul>
<li><p>dp 배열의 값을 초기화한다.</p>
</li>
<li><p>이때는 아래서 부터 위로 값을 채워나간다.</p>
<ul>
<li><p>2중 for-loop를 사용하는데, 기본적으로 자기자신이 수열에 포함되기 때문에 </p>
</li>
<li><p>dp[i]의 값을 1로 초기화한다.</p>
</li>
<li><p>2번째 for-loop에서는 i보다 작을 때 까지 loop를 돌면서</p>
<ul>
<li>i보다 작은 이유는 예를들어 i= 3 일때 1,2; 즉, 이 전값과 비교하여</li>
<li>더 큰 값이 있을경우 조건에 있는 로직을 실행하여 dp[i]를 초기화한다.</li>
</ul>
</li>
<li><p>비교하려는 number[j]보다 작다면 dp[i]의 값과 dp[j] + 1값을 비교하여 더 큰 값을 저장한다.</p>
<ul>
<li><p>+1인 이유는 dp[j]에 대한 값이 i에 속해지기 때문에 i값을 카운팅하기 위해 +1을 해주는 것이다.</p>
<pre><code class="language-java">static void bottom_up(int N) {
for(int i = 1; i &lt;= N; i++) {
 dp[i] = 1;

 for(int j = 0; j &lt; i; j++) {

     if(number[i] &lt; number[j]) {
         dp[i] = Math.max(dp[i], dp[j] + 1);
     }
 }
}
}</code></pre>
</li>
</ul>
</li>
</ul>
</li>
<li><p>dp배열을 순회하며 큰 값을 출력한다.</p>
<pre><code class="language-java">int max = Integer.MIN_VALUE;
</code></pre>
</li>
</ul>
<p>for(int i = 1; i &lt;= N; i++) {
    max = Math.max(dp[i], max);
}</p>
<p>System.out.println(max);</p>
<p>```</p>
<h3 id="정리">정리</h3>
<p>이 문제는 그대로 보이는 것 기준으로 왼쪽으로 갈 수록 수가 커져야 하고, 오른쪽으로 갈 수록 수가 작아지는 것만 인식한다면 생각보다 쉽게 풀 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 11055번: 가장 큰 증가하는 부분 수열 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11055%EB%B2%88-%EA%B0%80%EC%9E%A5-%ED%81%B0-%EC%A6%9D%EA%B0%80%ED%95%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11055%EB%B2%88-%EA%B0%80%EC%9E%A5-%ED%81%B0-%EC%A6%9D%EA%B0%80%ED%95%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 13:12:09 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/8a35e4b6-40f9-4f54-b0df-d6f55d7b36c5/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>이 문제에서는 dp의 값으로는 해당 배열의 부분 수열의 합이 들어간다.<br>비교하려는 arr[N] 에 대하여 N - 1부터 arr[N]과 비교하여 값이 더 작을경우<br>dp[N] 에 dp[N] 과 dp[N - 1] + arr[N] 를 비교하여 더 큰 값을 dp[N] 에 할당한다.
예를들어, 처음 dp배열의 초기값은 입력받은 값들이 들어갈 것이다. 왜냐하면 수열의 합이 기본적으로 자기자신을 포함하기 때문이다.<br>다음으로는 for문을 돌면서 N 보다 작은 N-1 ~ 0 까지 순회하며 입력받은 arr값들을 비교한다. 만약 N과 비교하여 이전값들이 더 작다면 dp[N]에 dp[N]과 dp[해당 루프 값 (j)] + arr[N] 을 더해 더 큰 값을 dp[N]에 저장한다.   </p>
<p>예제입력을 보면 dp배열의 초기값으로 
dp[0] = 1;
dp[1] = 100;
dp[2] = 2;
dp[3] = 50;
dp[4] = 60;
dp[5] = 3;
dp[6] = 5;
dp[7] = 6;
dp[8] = 7;
dp[9] = 8;
이 들어가게 된다.  </p>
<p>다음으로 한 예시로 만약 N = 5 일 때를 기준으로  bottom-up으로 for문을 돌면서 값을 구해보자.   </p>
<pre><code class="language-java">for(int i = 0; i &lt; N(5); i++) {
    dp[i] = number[i];
    for(int j = 0; j &lt; i; j++) {
        dp[i] = Math.max(dp[i], dp[j] + number[i]);
    }
}</code></pre>
<ul>
<li><p>i=0 (1)</p>
<ul>
<li>i가 0일때는 처음 초기값 설정 이후 내부 for문은 타지않는다. 따라서</li>
<li>dp[0] = 1;</li>
</ul>
</li>
<li><p>i=1 (100)</p>
<ul>
<li>i=1 일때는 내부 for문을 타게되고, dp[0] + number[1] 과 dp[1] 중에 더 큰값을 저장하게 된다. 각각의 값은 101 과 100이다.</li>
<li>dp[1] = 101;</li>
</ul>
</li>
<li><p>i=2 (2)</p>
<ul>
<li>i=2 일때는 내부 for문으로는 0,1을 순회하며, number[1]은 조건에 맞지 않아 패스하게 되며,  dp[0] + number[2] 과 dp[2]를 비교하게 된다.</li>
<li>dp[2] = 3;</li>
</ul>
</li>
<li><p>i=3 (50)</p>
<ul>
<li>i=3 일때는 0~2를 순회하며, 조건에 맞는 0과 2를 비교하게 된다. 각각 dp[0] + number[3] 과, dp[2] + number[3]을 비교하게 되며 각각의 값은 51과 53이다.</li>
<li>dp[3] = 53;</li>
</ul>
</li>
<li><p>i=4 (60)</p>
<ul>
<li>i=4 일때는 0~3를 순회하며, 조건에 맞는 0,2,3 을 비교하게 된다. 각각 dp[0] + number[4] 과, dp[2] + number[4], dp[3] + number[4]을 비교하게 되며 각각의 값은 51과 53, 113이다.</li>
<li>dp[4] = 113;</li>
</ul>
</li>
</ul>
<p>dp[0] = 1;
dp[1] = 101;
dp[2] = 3;
dp[3] = 53;
dp[4] = 113;
dp[5] = 6;
dp[6] = 11;
dp[7] = 17;
dp[8] = 24;
dp[9] = 32;</p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static int[] number;
    static Integer[] dp;
    static int N;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        N = Integer.parseInt(br.readLine());
        number = new int[N];
        dp = new Integer[N];

        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

        for(int i = 0; i &lt; N; i++) {
            number[i] = Integer.parseInt(st.nextToken());
        }

        dp[0] = number[0];
        bottom_up_lis();

//        for(int i = 1; i &lt; N; i++) {
//            recur_lis(i);
//        }

        int max = Integer.MIN_VALUE;

        for(int i = 0; i &lt; N; i++) {

            if(max &lt; dp[i]) {
                max = dp[i];
            }
        }

        System.out.println(max);
    }

    private static int recur_lis(int N) {

        if(dp[N] == null) {
            dp[N] = number[N];

            for(int i = N - 1; i &gt;= 0; i--) {

                if(number[N] &gt; number[i]) {
                    dp[N] = Math.max(dp[N], number[N] + recur_lis(i));;
                }
            }
        }

        return dp[N];
    }


    private static void bottom_up_lis() {

        for(int i = 1; i &lt; N; i++) {
            dp[i] = number[i];

            for(int j = 0; j &lt; i; j++) {
                if(number[i] &gt; number[j]) {
                    dp[i] = Math.max(dp[j] + number[i], dp[i]);
                }
            }
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 11053번: 가장 긴 증가하는 부분 수열 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11053%EB%B2%88-%EA%B0%80%EC%9E%A5-%EA%B8%B4-%EC%A6%9D%EA%B0%80%ED%95%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11053%EB%B2%88-%EA%B0%80%EC%9E%A5-%EA%B8%B4-%EC%A6%9D%EA%B0%80%ED%95%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%88%98%EC%97%B4-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 12:53:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/c8e5c69e-9002-4831-bdd2-628d543ac7c4/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>이 문제는 다른 말로는 LIS(최장 증가 부분 수열)라고 부른다고 한다.<br>주어진 수열에서 오름차순으로 구성 가능한 원소들을 선택하여 최대 길이를 찾아 내는 것.<br>아래에서 풀 문제는 시간 복잡도가 O(N^2) 이다.   </p>
<p>먼저 예제 출력을 갖고 살펴보면<br>기본적으로 수열을 갖고 있지 않아도 1이 할당된다.[자기 자신]<br>idx[0] = 10; 일때는 수열이 없기 때문에 값이 1이된다.
idx[1] = 20; 일때는 앞에 값 {10, 20}으로 수열의 값이 2가 된다.<br>idx[2] = 10; 일 때는 앞에 값은 10보다 큰 20이기 때문에 수열의 값이 1이된다.<br>idx[3] = 30; 일 때는 idx[0]의 값 10, idx[1]의 값 20, 그리고 자신까지 해서 총 3이된다.  {10, 20, 30}<br>idx[4] = 20; 일 때는 idx[0]의 값 그리고 자기자신 까지 해서 총 2의 값을 가진다. {10, 20}<br>idx[5] = 50; 일 때는 idx[0], idx[1], idx[3]과 자기 자신까지 해서 총 4가 된다. {10, 20, 30, 50}<br>그렇다면 어떻게 풀면 될까?<br>각 값에 대하여 이전 값과 비교하여 작다면 수열의 값을 늘려주면 된다.<br>즉, for loop에서 i가 3일 때 3 아래 값 0,1,2 와 비교하여 값이 수열에 해당한다면 값을 증가시켜주면 된다.   </p>
<h3 id="top-down">top-down</h3>
<p>dp[] 은 기본적으로 idx의 해당하는 수열의 갯수를 저장하고 있다.<br>dp[]에서 기본적으로 모든 값은 1[자기자신]값을 갖고있다.<br>N의 값에 대하여 for loop를 도는데, 초기값은 N, i를 감소시키면서 N-1,N-2 ... 0 까지<br>N값과 비교하며 N보다 작다면 dp[N] 에 dp[N]과 재귀함수(i) + 1과 비교하여 큰 값을 dp[N]에 저장한다.<br>여기서 +1을 해주는 이유는 예를들어서,<br>N의 값이 비교하는 값보다 큰데, 비교하는 값 dp[i]에 대하여 dp[i]의 값은 dp[N]에 합쳐지는것 뿐만 아니라 arr[i]의 값은<br>arr[N]에 속해지는 수열이기에 + 1을 해주는 것이다.   </p>
<ul>
<li>N을 입력받는다.  <ul>
<li>N을 입력받고 dp 배열과 number 배열을 초기화 해준다.  <ul>
<li>여기서 dp배열은 수열의 값을 저장한다.<pre><code class="language-java">static Integer dp[];
static int[] number;
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());</p>
<p>dp = new Integer[N];
number = new int[N];</p>
<pre><code>- 다음으로 입력받는 수열을 number 배열에 저장한다.
```java
for(int i = 0; i &lt; N; i++) {
    number[i] = Integer.parseInt(st.nextToken());
}</code></pre><ul>
<li>초기값을 설정한다<pre><code class="language-java">dp[0] = 1;</code></pre>
</li>
<li>for loop를 돌면서 dp[] 값을 초기화한다.<pre><code class="language-java">for(int i = 0; i &lt; N; i++) {
  recur(i);
}</code></pre>
</li>
<li>재귀함수에서는 dp[N]이 초기화 되지 않았을 경우 dp값을 초기화 해준다.<ul>
<li>여기서는 dp[N]의 값의 초기값은 1로 설정해준다.</li>
<li>for loop를 돌면서 N - 1 부터 시작하여 0까지 number 값을 비교한다.</li>
<li>dp[N] 에는 dp[N] 값과 재귀함수(i) + 1 과 비교하여 큰 값을 dp[N]에 저장한다.   </li>
</ul>
</li>
</ul>
<pre><code class="language-java">private static int recur(int N) {

    if(dp[N] == null) {

        dp[N] = 1;

        for(int i = N - 1; i &gt;= 0; i--) {

            if(number[N] &gt; number[i]) {
                dp[N] = Math.max(dp[N], recur(i) + 1);
            }
        }
    }

    return dp[N];
}</code></pre>
<ul>
<li>dp[] loop를 돌면서 max값을 출력한다.<pre><code class="language-java">int max = Integer.MIN_VALUE;
</code></pre>
</li>
</ul>
<p>for(int i = 0; i &lt; N; i++) {
    if (max &lt; dp[i]) {
        max = dp[i];
    }
}</p>
<p>System.out.println(max);</p>
<pre><code>

### bottom-up
top-down과 동일한데 값을 설정할 때 for loop를 통해 dp[]을 초기화한다.
top-down과는 다른건 dp값을 초기화 할 때 조건이 하나 더 붙는다.   
조건이라기보다 top-down에서 Math.max로 비교하는 값이 조건으로 추가되는 것이다.   
기본적으로 number[i] 값이 비교하려는 number[j] 보다 커야한다. 그래야만 수열이 성립하기 때문이다.   
다음으로는 Math.max에서 dp[i] 값과 recur(N) + 1 값을 비교하는것을 여기에서는    
number[i] 에 해당하는 dp[i] 와 number[j]에 해당하는 dp[j] 에서 만약 number[i]가 더 큼에도 불구하고    
dp[i]가 dp[j] 보다 작을 경우 dp[i]에 dp[j] + 1을 초기화한다.   
여기서 + 1을 하는 이유는 number[i]가 number[j]보다 크기 때문에 number[j]가 number[i]의 수열에 포함되고, 자기자신 number[j] 도    
number[i]의 수열에 포함되기 때문에 + 1을 해주는 것이다.   


- N을 입력받는다.
```java
static Integer dp[];
static int[] number;

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

int N = Integer.parseInt(br.readLine());

dp = new Integer[N];
number = new int[N];</code></pre><ul>
<li>수열을 입력받아 number[] 을 초기화한다.<pre><code class="language-java">StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);
</code></pre>
</li>
</ul>
<p>for(int i = 0; i &lt; N; i++) {
    number[i] = Integer.parseInt(st.nextToken());
}</p>
<pre><code>- dp 초기값을 설정해준다.
```java
dp[0] = 1;</code></pre><ul>
<li><p>초기값 다음 1 부터 시작하여 N-1 까지 순회하며 dp값을 초기화한다.</p>
<ul>
<li><p>top-down과 동일하게 초기값은 dp[i]의 값은 1로 초기화해준다.</p>
</li>
<li><p>2중 for loop에서는 0부터 시작하여 i전 까지 돌면서 number[i]와 비교해준다.</p>
</li>
<li><p>여기서는 조건이 number[i]가 number[j] 보다 큰데도, dp[]의 값이 dp[j] 보다 작을경우</p>
</li>
<li><p>dp[i]에 dp[j] + 1로 초기화 해준다.</p>
<pre><code class="language-java">for(int i = 1; i &lt; N; i++) {

dp[i] = 1;

for(int j = 0; j &lt; i; j++) {

  if(number[i] &gt; number[j] &amp;&amp; dp[i] &lt;= dp[j]) {
      dp[i] = dp[j] + 1;
  }
}
}</code></pre>
</li>
</ul>
</li>
<li><p>dp를 순회하며 max값을 출력한다.</p>
<pre><code class="language-java">int max = Integer.MIN_VALUE;
</code></pre>
</li>
</ul>
<p>for(int i = 0; i &lt; N; i++) {
    if(max &lt; dp[i]) {
        max = dp[i];
    }
}</p>
<p>System.out.println(max);</p>
<pre><code>
### 전체코드
```java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    static Integer dp [];
    static int number [];
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

        dp = new Integer[N];
        number = new int[N];

        for(int i = 0; i &lt; N; i++) {
            number[i] = Integer.parseInt(st.nextToken());
        }

        //초기값
        dp[0] = 1;

        for(int i = 0; i &lt; N; i++) {
            recur(i);
        }

        int max = Integer.MIN_VALUE;

        for(int i = 0; i &lt; N; i++) {
            if (max &lt; dp[i]) {
                max = dp[i];
            }
        }

        System.out.println(max);

    }

    private static int recur(int N) {

        if(dp[N] == null) {

            dp[N] = 1;

            for(int i = N - 1; i &gt;= 0; i--) {

                if(number[N] &gt; number[i]) {
                    dp[N] = Math.max(dp[N], recur(i) + 1);
                }
            }
        }

        return dp[N];
    }
}</code></pre><h3 id="전체코드-bottom-up">전체코드 (bottom-up)</h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main2 {

    static Integer dp[];
    static int[] number;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        dp = new Integer[N];
        number = new int[N];


        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);

        for(int i = 0; i &lt; N; i++) {
            number[i] = Integer.parseInt(st.nextToken());
        }

        dp[0] = 1;

        for(int i = 1; i &lt; N; i++) {

            dp[i] = 1;

            for(int j = 0; j &lt; i; j++) {

                if(number[i] &gt; number[j] &amp;&amp; dp[i] &lt;= dp[j]) {
                    dp[i] = dp[j] + 1;
                }
            }
        }

        int max = Integer.MIN_VALUE;

        for(int i = 0; i &lt; N; i++) {
            if(max &lt; dp[i]) {
                max = dp[i];
            }
        }

        System.out.println(max);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 9465번: 스티커 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-9465%EB%B2%88-%EC%8A%A4%ED%8B%B0%EC%BB%A4-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-9465%EB%B2%88-%EC%8A%A4%ED%8B%B0%EC%BB%A4-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 11:42:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/8e010cb5-f15b-4ecd-87f0-c88b95cb57ce/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/baf915a9-8381-4118-a14e-01f43dcc5f78/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>2가지 방법으로 풀어보려고 한다. </p>
<ol>
<li>top-down 방식 [Main.java]
이렇게 불러야 할지 모르겠지만 top-down 방식으로 풀어보도록 하겠다.<br>먼저 어떠한 값 (row, col)에 마지막에 도달하기 위해서는 아래 그림과 같은 접근만 가능하다.
<img src="https://velog.velcdn.com/images/xxx-sj/post/6db547b1-674f-4b02-87bd-f3f772753e2a/image.png" alt=""></li>
</ol>
<p>그림에서 같이 값(row, col)에 대하여 접근할 수 있는 방법은 row = 0 일 때는 (row + 1, col - 2)와 (row + 1, col -1)에서 접근이 가능하다.<br>다음으로 row = 1일때는 (row - 1, col -2), (row - 1, col - 1)에서 접근이 가능하다.<br>문제에서는 최대값을 구하는 것이기에 접근할 수 있는 두 값 중 큰 것에서 마지막 값 (row, col)에 접근하면 되는 것이다. Math.max((row + 1, col - 2), (row + 1, col - 1)) [규칙 부분]<br>top-down 방식은 재귀를 통해 마지막까지 접근해 다시 역으로 올라오는 것이다. 다시말해 재귀의 종료시점은 배열이라면 첫 번째 값 [0]에 해당할 것이다. [초기화 부분]<br>다음으로 중요한 것은 예외이다. 위에서 보인 규칙에 따르면 현재 값의 (col -1)과 (col - 2) 값을 비교한다. 해당 문제를 풀기위해 배열을 사용할텐데, col &lt;=1 이라면 배열에 벗어나 버려 오류가 발생한다 [예외 부분]<br>위의 조건들을 합치면 문제를 해결할 수 있다.</p>
<ul>
<li>케이스 T를 입력받는다. 이 후에 입력받는 배열값을 나누기 위해 StringTokenizer를 선언한다.<ul>
<li>static 영역에는 메모이제이션에 사용할 배열과, 입력받는 스티커 정보를 저장할 배열을 선언한다.<pre><code class="language-java">static int[][] stickers;
static Integer[][] dp;
</code></pre>
</li>
</ul>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;</p>
<p>int T = Integer.parseInt(br.readLine());</p>
<pre><code>- 테스트 케이스만큼 돌면서 for문 안에서는 배열의 가로 길이 N을 입력받는다. 
  - 입력받은 N 으로 크기 [2][N + 1] 배열을 dp, stickers 배열을 초기화한다.  
    - 여기서 N + 1인 이유는 인덱스는 0 부터 시작하기 때문에 쉽게 이해하기 위해 1크기를 더 더했다.
  - 입력받은 스티커 정보에서 첫 번째 값 [0][1] 과 [1][1]의 값을 가지고 dp 배열의 초기값을 넣어준다.
```java
for(int i = 0; i &lt; T; i++) {
    int N = Integer.parseInt(br.readLine());

    stickers = new int[2][N + 1];

    for(int j = 0; j &lt; 2; j++) {
        st = new StringTokenizer(br.readLine(), &quot; &quot;);
        for(int k = 1; k &lt;= N; k++) {
            stickers[j][k] = Integer.parseInt(st.nextToken());
        }
    }

    dp = new Integer[2][N + 1];

    dp[0][1] = stickers[0][1];
    dp[1][1] = stickers[1][1];

    System.out.println(Math.max(recur(0, N), recur(1, N)));
}</code></pre><ul>
<li><p>다음으로 재귀로 호출할 메서드이다.</p>
<ul>
<li>메서드의 인자로는 2개의 정보를 받는다. <ul>
<li>하나는 row, 다른 하나는 col이다.</li>
</ul>
</li>
<li>재귀의 종료 조건으로 col == 1 일때 stickers[row][col]을 리턴한다.<ul>
<li>col == 1 인 이유는 col가 1에 도달했다는 건 오른쪽에서 시작하여 왼쪽 끝에 도달하였다는 말이기 때문이다.</li>
</ul>
</li>
<li>다음으로는 메모이제이션 배열 dp가 초기화되지 않았다면 [dp[row][col] == null] 초기화를 해준다.</li>
<li>(여기서 dp 배열의 값은 큰 값으로 갈 수록 이전 값의 누적 합이 저장된다.)<ul>
<li>이 때 조건으로는 col == 2 일 때는 뒤로 한칸밖에 없기 때문에 따로 최대값을 비교하지않고 (col -1) 값만 호출하여 현재 stickers[row][col] 값에 더해서 초기화 해준다.<ul>
<li>쉽게 생각하여 현재 (row, col) 값에 도달하기 위해서는 (row - 1, col -1), (row -1, col - 2) 둘 중에서 와야하는데, col == 2 인 상태에서 (col -2)를 배열을 넘어가기 때문이다.   </li>
</ul>
</li>
<li>col &gt; 2 라면 그림에서 봤듯이 col -2, col - 1 중에 큰 값을 찾은 후 현재 값 stickers[row][col]과 더하여 dp[row][col]를 초기화해준다.<ul>
<li>여기서 1 - row 인 이유는 row 값은 0 또는 1 인데, 만약 현재 값이 0이라면 1에서 도달한 것이기에 1 - 0 =&gt; 1 이 되고, 현재 값이 1이라면 0 에서 도달한 것이기에 1 - 1 =&gt; 0 이 된다.<ul>
<li>즉, 쉽게 표현하기 위하여 1 - row로 짧게 쓴 것이다.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>마지막에는 모두 초기화 된 메모이제이션에서 값만 꺼내어 return 한다.</li>
</ul>
</li>
<li><p>메서드를 호출했던 부분에서는 마지막 값 dp[0][N] 과 dp[1][N] 중에서 큰 값을 출력한다.</p>
<pre><code class="language-java">private static int recur(int row, int col) {

  if (col == 1) {
      return stickers[row][col];
  }

  if (dp[row][col] == null) {
      if (col == 2) {
          dp[row][col] = recur(1 - row, col - 1) + stickers[row][col];
      } else {
          dp[row][col] = Math.max(recur(1 - row, col - 1), recur(1 - row, col - 2)) + stickers[row][col];
      }
  }

  return dp[row][col];
}</code></pre>
</li>
</ul>
<ol start="2">
<li>bottom-up 방식
위와는 다르게 for문을 통해 모든 값을 설정하고 마지막에 Math.max를 통해 dp[0][N], dp[1][N] 중 큰 값을 출력하는 것은 동일하다.<br>규칙은 위에서 설명했기에 생략한다.  </li>
</ol>
<ul>
<li>T를 입력받는다.</li>
<li>여기서는 dp 배열을 Integer가 아닌 int 배열로 선언한다.<pre><code class="language-java">static int dp[][], stickers[][];
</code></pre>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;</p>
<p>int T = Integer.parseInt(br.readLine());</p>
<pre><code>- test case t 만큼 for문을 돈다.
  - 여기서도 동일하게 n을 입력받고, 그 크기만큼 stickers, dp 배열을 초기화한다.
  - 다음으로 스티커 배열을 입력받아 초기화 시켜준다.
  - 초기값 dp[0][1], dp[1][1] 도 stickers 배열에서 가져와 초기화한다.
  - 여기서는 재귀를 사용하지않고, col == 2 부터 시작해 dp 배열의 값을 초기화한다.  
    - 여기서도 col == 2 일때 위에처럼 범위를 벗어나는게 아닌가 라고 생각할 수 있는데, 그래서 dp배열을 Integer가 아닌, int로 한 이유도 있다.
      - 원래라면 [0][0], [0][1] 에 값을  0으로 초기화 시켜줬어야 했지만, int 배열 초기값이 0 이기 때문에 따로 초기화 하지 않은 것이다.
  - 동일한 규칙을 이용해 (col - 2), (col - 1) 중 큰 값과 현재 값(stickers[0][j] or stickers[1][j])을 더하여 dp값을 초기화한다.
  - 마지막도 동일하게 dp[0][N], dp[1][N] 중 큰 값을 출력한다.

```java
for(int i = 0; i &lt; T; i++) {
    int N = Integer.parseInt(br.readLine());

    stickers = new int[2][N + 1];
    dp = new int[2][N + 1];


    for(int j = 0; j &lt; 2; j++) {
        st = new StringTokenizer(br.readLine(), &quot; &quot;);
        for(int k = 1; k &lt;= N; k++) {
            stickers[j][k] = Integer.parseInt(st.nextToken());
        }
    }

    dp[0][1] = stickers[0][1];
    dp[1][1] = stickers[1][1];

    for(int j = 2; j &lt;= N; j++) {
        dp[0][j] = Math.max(dp[1][j - 1], dp[1][j - 2]) + stickers[0][j];
        dp[1][j] = Math.max(dp[0][j - 1], dp[0][j - 2]) + stickers[1][j];
    }

    System.out.println(Math.max(dp[0][N], dp[1][N]));
}</code></pre><h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static int[][] stickers;
    static Integer[][] dp;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        int T = Integer.parseInt(br.readLine());

        for(int i = 0; i &lt; T; i++) {
            int N = Integer.parseInt(br.readLine());

            stickers = new int[2][N + 1];

            for(int j = 0; j &lt; 2; j++) {
                st = new StringTokenizer(br.readLine(), &quot; &quot;);
                for(int k = 1; k &lt;= N; k++) {
                    stickers[j][k] = Integer.parseInt(st.nextToken());
                }
            }

            dp = new Integer[2][N + 1];

            dp[0][1] = stickers[0][1];
            dp[1][1] = stickers[1][1];

            System.out.println(Math.max(recur(0, N), recur(1, N)));
        }
    }

    private static int recur(int row, int col) {

        if (col == 1) {
            return stickers[row][col];
        }

        if (dp[row][col] == null) {
            if (col == 2) {
                dp[row][col] = recur(1 - row, col - 1) + stickers[row][col];
            } else {
                dp[row][col] = Math.max(recur(1 - row, col - 1), recur(1 - row, col - 2)) + stickers[row][col];
            }
        }

        return dp[row][col];
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 2156번: 포도주 시식 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-2156%EB%B2%88-%ED%8F%AC%EB%8F%84%EC%A3%BC-%EC%8B%9C%EC%8B%9D-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-2156%EB%B2%88-%ED%8F%AC%EB%8F%84%EC%A3%BC-%EC%8B%9C%EC%8B%9D-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 11:15:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/32e0adbc-afc4-42b0-9d86-2b6be7f60984/image.png" alt="">
<img src="https://velog.velcdn.com/images/xxx-sj/post/89049598-6d23-430b-bab0-c84a3d35e669/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>이 문제는 간단하게 보자면, N에 도달하기 위한 몇 가지 조건들 중에서 가장 큰 수를 뽑는 것 이다. 여기서는 몇가지 규칙이 있는데</p>
<ol>
<li>마지막 N번 째 포도주를 마실 필요가 없다. </li>
<li>연속으로 놓여진 3잔을 모두 마실 수 없다.  </li>
</ol>
<p>마지막 N번 째 포도주를 마실 필요가 없다는 이유는, 문제의 마지막 부분에 가각의 잔에 순서대로 놓여진 포도주를 마시는 방법을 소개할 때 알 수 있다.</p>
<p>이 규칙들을 가지고 N에 도달하기 위한 몇 가지 조건들을 세워보자면 아래와 같다.
여기서 말하는 &quot;N에 도달하기 위한&quot;이 문제에서는 반드시 N에 도달할 필요는 없다. 
[N - 1 까지만 도달해도 된다.] </p>
<ol>
<li>N번 째 포도주와, N-1번 째 포도주를 마시고, N - 3 까지의 누적 최대합을 더하는 것</li>
<li>N번 째 포도주를 마시고, N - 2 까지의 누적 최대합을 더하는 것</li>
<li>N번 째 포도주를 마시지 않고, N -1 까지의 누적 최대합을 더하는 것</li>
</ol>
<p>규칙들을 코드로 나타내 보자면 </p>
<ol>
<li>wines[N] + wines[N -1] + dp[N -3]</li>
<li>wines[N] + dp[N -2]</li>
<li>dp[N - 1]</li>
</ol>
<p>이렇게 나눈 이유는 첫 번째로는 연속으로 놓여진 3잔을 모두 마실수 없다. 그렇다면 최대한으로 마실 수 있는 잔의 수는 2잔인데, 만약 N을 마신다면 N - 1을 마신 후, N - 2는 못 마실것이다. 
그말은 N - 3 잔 부터는 다시 마실 수 있다는 뜻이되고, N을 마신 후 N - 1을 마시지 않는다면 N - 2부터 다시 마실 수 있다는 것이다.  위의 말대로 만들어진 규칙이 1번 2번이 된다.<br>마지막으로 N을 마시지 않은경우 N - 1 까지의 최대값을 구한게 규칙이 될 것이다.  </p>
<p>여기서 의문이 들 수 있는데, 그렇다면 N을 마시지 않고, N -1 을 마시고 한번 건너 뛴 N - 3 의 최대값을 더하면 되지 않을까? [wines[N - 1] + dp[N - 3]] 라는 생각이 들 수 있다.<br>여기서 생각을 해보면 dp에는 N - 3 까지 오면서 마실 수 있는 최대값을 선택하면서 왔을 것이다. 그런데 이 선택에 있어서 wines[N - 3]을 마셨을 수도, 마시지 않았을 수도 있다. 그렇기 때문에 N -2 를 건너뛰고 N -1 을 마시는 것은 최대가 되지 않을수도 있는 것이다. 따라서 dp[N - 1] 이란 규칙이 나온것이다. dp[N - 1]는 N - 1까지 오면서 선택할 수 있는 최대값을 선택하면서 왔기 때문이다. </p>
<h3 id="전체코드">전체코드</h3>
<pre><code class="language-java">package date_2023_08_18.DP_2156;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {


    static Integer dp[];
    static int glass[];
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        dp = new Integer[N + 1];
        glass = new int[N + 1];

        for(int i = 1; i &lt;= N; i++) {
            glass[i] = Integer.parseInt(br.readLine());
        }

        //초기값 세팅
        dp[0] = 0;
        dp[1] = glass[1];


    }

    private static int recur(int N) {

        if(N == 1) {
            return dp[N];
        }

        if(dp[N] == null) {
            dp[N] = Math.max(Math.max(recur(N - 2), recur(N - 3) + glass[N - 1]) + glass[N], recur(N - 1));
        }

        return dp[N];
    }
}
</code></pre>
<h3 id="정리">정리</h3>
<p>이 문제도 생각해보면 N에 도달하기 위한 규칙을 찾고, 그 규칙들 중 가장 큰 값을 찾는 문제였다. 간단하게 생각하자면, 규칙 중 최대값을 찾아 dp배열에 넣으면 끝.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 11057번: 오르막 수 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11057%EB%B2%88-%EC%98%A4%EB%A5%B4%EB%A7%89-%EC%88%98-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-11057%EB%B2%88-%EC%98%A4%EB%A5%B4%EB%A7%89-%EC%88%98-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 30 Aug 2023 10:42:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/1a9cb5ac-14e2-4050-8c65-d8917b7178f0/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>이 문제는 이 전 쉬운 계단수와 비슷한 문제이다. 다른점이 있다면 이 문제에서는 0으로 시작할 수 있다는 것이다.
<a href="https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-10844%EB%B2%88-%EC%89%AC%EC%9A%B4-%EA%B3%84%EB%8B%A8%EC%88%98-Java-%EC%9E%90%EB%B0%94">쉬운계단 수</a></p>
<p>이 문제는 손으로 직접 적어보면 쉽게 이해가 가능하다.<br>이 문제에서는 0으로 시작하는 숫자 또한 수로 인정하고 있다.<br>1자리를 예를 들면 0 ~ 9 까지 총 10가지가 가능 할 것이다.  </p>
<p>다음으로 2자리를 예시로 들면, 2자리는 2번째 자리 즉 10의 자리가 0 일때 첫 번째 자리 1의 자리는 0 ~ 9 까지 가능하다. [인접한 수가 같아도 오름차순으로 쳐주기 때문에]<br>10의 자리 | 1의자리<br>0        | 0 ~ 9  </p>
<p>이어서 2번 째 자리가 1일 때는 첫 번쨰 자리는 1 ~ 9 까지,<br>10의 자리 | 1의자리<br>1       | 1 ~ 9  </p>
<p>2번 째 자리가 2일때는 첫 번째 자리는 2 ~9 까지....
2번 째 자리가 8일 때는 첫 번째 자리는 8 ~9 까지
2번 째 자리가 9일 때는 첫 번째 자리는 9 까지 가능하다.<br>10의 자리 | 1의자리<br>9          | 9 ~ 9  </p>
<p>위의 규칙을 보자면 2번 째 자리의 값이 T 라면 첫 번째 자리의 값은  T ~ 9 까지 인것을 확인할 수 있다.<br>즉, 입력받은 N 번째 자리의 값이 T 일 때 N - 1번째 자리 값은 T ~ 9 까지이다.  </p>
<h3 id="전체코드-top-down">전체코드 (top-down)</h3>
<pre><code class="language-java">public class Main {

    static Long dp[][];
    static int mod = 10_007;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        dp = new Long[N + 1][10];


        //초기값 설정
        for(int i = 0; i &lt;= 9; i++) {
            dp[1][i] = 1L;
        }


        long result = 0;

        for(int i = 0; i &lt;= 9; i++) {
            result += recur(N, i);

        }
            System.out.println(result % mod);

    }

    static long recur(int digit, int val) {

        if (digit == 1) {
            return dp[digit][val];
        }

        if(dp[digit][val] == null) {
//            if (val == 9) {
//                dp[digit][val] = recur(digit - 1, 9) % mod;
//            } else {
//                long result = 0;
//                for(int i = val; i &lt;= 9; i++) {
//                    result += recur(digit - 1, i);
//                }
//                dp[digit][val] = result  % mod;
//            }
            long result = 0;
            for(int i = val; i &lt;= 9; i++) {
                result += recur(digit - 1, i);
            }

            dp[digit][val] = result % mod;
        }

        return dp[digit][val];
    }
}
</code></pre>
<h3 id="코드-상세">코드 상세</h3>
<ul>
<li>N을 입력받는다.<ul>
<li>입력받은 N + 1 크기의 이차원배열 생성</li>
<li>N + 1인 이유는 인덱스가 0 부터 시작하기 때문에</li>
<li>두 번째 값이 10인 이유는 0~9까지 이기때문에<pre><code class="language-java">static Long dp[][];
static int mod = 10007;
</code></pre>
</li>
</ul>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</p>
<p>int N = Integer.parseInt(br.readLine());</p>
<p>dp = new Long[N + 1][10];</p>
<pre><code>- 배열의 초기값을 설정한다.
  - 1자리 일 때 각각의 수는 1개만 가능하기 때문에
```jpaql
for(int i = 0; i &lt;= 9; i++) {
    dp[1][i] = 1L;
}</code></pre><ul>
<li><p>재귀함수를 만든다.</p>
<ul>
<li><p>해당 함수에서는 digit [자릿수] 가 1 일 경우 dp[digit][val]를 리턴한다.</p>
</li>
<li><p>배열 값이 초기화가 되어있지않다면 [== null] 값을 초기화 한다.   </p>
</li>
<li><p>위에서 얘기하였듯이 N번째 자리 val에 대하여 N - 1 자리 값은 val ~ 9 까지 이다.</p>
<ul>
<li>따라서 인자로 받은 val를 for의 초기값으로 설정하고 9 까지 for문을 돌면서 값을 저장한다.</li>
</ul>
</li>
<li><p>값을 저장할 때는 mod만큼 나누어 저장한다. 여기서는 모듈러 연산이 사용된다.   </p>
<pre><code class="language-java">static long recur(int digit, int val) {
if (digit == 1) {
    return dp[digit][val];
}

if(dp[digit][val] == null) {
    long result = 0;
    for(int i = val; i &lt;= 9; i++) {
        result += recur(digit - 1, i);
    }

    dp[digit][val] = result % mod;
}

return dp[digit][val];
}</code></pre>
</li>
</ul>
</li>
<li><p>마지막으로 N번쨰 자리 0~ 9 까지 for문을 돌면서 결과를 구한다.</p>
<ul>
<li>0 ~ 9 까지 인 이유는 조건에 나와있다. <pre><code class="language-java">long result = 0;
</code></pre>
</li>
</ul>
</li>
</ul>
<p>for(int i = 0; i &lt;= 9; i++) {
    result += recur(N, i);</p>
<p>}
    System.out.println(result% mod);</p>
<p>```</p>
<h3 id="정리">정리</h3>
<p>이 문제에서는 손으로 적어보면서 쉽게 풀 수 있었을 것이다. 별 다른 예외없이 가장 앞부터 0~9를 채우고 자릿수를 하나씩 줄여가며 범위를 줄여나가면 쉽게 해결 할 수 있었을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 2193번: 이친수 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-2193%EB%B2%88-%EC%9D%B4%EC%B9%9C%EC%88%98-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-2193%EB%B2%88-%EC%9D%B4%EC%B9%9C%EC%88%98-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Tue, 29 Aug 2023 07:44:28 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/f294a56c-5b81-42c0-a231-e67c82df72fa/image.png" alt=""></p>
<h3 id="문제접근">문제접근</h3>
<p>먼저 자릿수에 대한 개념을 잡기 위해서 이전 글을 참고하고 오면 이해하기 쉽습니다. 
<a href="https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-10844%EB%B2%88-%EC%89%AC%EC%9A%B4-%EA%B3%84%EB%8B%A8%EC%88%98-Java-%EC%9E%90%EB%B0%94">이전 글</a></p>
<p>이 전에 풀던DP와 같은 풀이로 푼다.  자릿수에 해당하는 자릿값으로 문제를 푸는데 dp[digit][val]<br>여기서 자릿값은 0 or 1,  2개만 가능하다.   </p>
<ol>
<li>다음 조건으로는 시작이 0 으로 시작하지 않는다는 점.</li>
<li>1이 연속으로 두 번 나타나지 않는다는 점.<br>이 조건들을 조합해보면 아래와같은 재귀함수를 만들 수 있다.</li>
</ol>
<ul>
<li>digit[자릿수] 가 1일때는 digit[digit][val]를 반환한다. [종료시점]</li>
<li>dp[digit][val] == null 일 때 <ul>
<li>val이 1일때는 뒤에 올 수 있는 수는 0밖에 없으므로 recur(digit - 1, 0);</li>
<li>val이 0일때는 뒤에 0 or 1이 올수 있기 때문에 recur(digit - 1, 0) + recur(digit -1 , 1) 이 된다.<br>코드로 보면 아래와 같다.</li>
</ul>
</li>
</ul>
<h3 id="전체코드-top-down">전체코드 (top-down)</h3>
<pre><code class="language-java">package date_2023_08_16.DP_2193;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static Long dp[][];
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        dp = new Long[N + 1][2];

        dp[1][0] = dp[1][1] = 1L;

        System.out.println(recur(N, 1));
    }

    static Long recur(int digit, int val) {

        if(digit == 1) {
            return dp[digit][val];
        }

        if(dp[digit][val] == null) {
            if(val == 1) {
                dp[digit][val] = recur(digit - 1, 0);
            } else {
                dp[digit][val] = recur(digit - 1, 0) + recur(digit - 1, 1);
            }
        }

        return dp[digit][val];
    }
}
</code></pre>
<h3 id="코드-상세">코드 상세</h3>
<ul>
<li>N을 입력받는다.<ul>
<li>입력받은 N으로 배열을 생성한다.</li>
<li>이 때 이차원 배열은 Long을 사용한다. 2^90이어 값이 넘어갈 수 있기 때문에..?</li>
<li>N + 1 인 이유는 인덱스가 0 부터 시작하기 때문이고, 두 번째 크기가 2인 이유는 0 or 1 두개 만 들어가기 때문에.<pre><code class="language-java">static Long dp[][];
</code></pre>
</li>
</ul>
</li>
</ul>
<p>BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
dp = new Long[N + 1][2];</p>
<pre><code>- 초기값을 설정한다.
```java
dp[1][0] = dp[1][1] = 1L;</code></pre><ul>
<li><p>재귀함수를 선언한다.</p>
<ul>
<li><p>종료조건은 digit == 1 일때</p>
</li>
<li><p>아직 배열의 값이 초기화되지 않았다면</p>
<ul>
<li>val == 1 일때는 뒷 값으로 0 밖에 오지 못함.</li>
<li>val == 0 일때는 뒷 값으로 0, 1 둘다 가능.<pre><code class="language-java">static Long recur(int digit, int val) {
</code></pre>
</li>
</ul>
<p>if(digit == 1) {</p>
<pre><code>return dp[digit][val];</code></pre><p>}</p>
<p>if(dp[digit][val] == null) {</p>
<pre><code>if(val == 1) {
    dp[digit][val] = recur(digit - 1, 0);
} else {
    dp[digit][val] = recur(digit - 1, 0) + recur(digit - 1, 1);
}</code></pre><p>}</p>
<p>return dp[digit][val];
}
```</p>
</li>
</ul>
</li>
<li><p>재귀함수를 호출한다.</p>
<ul>
<li>N,1 만 호출하는 이유는 첫번 째 값이 1밖에 오지 못하기 때문이다.<pre><code class="language-java">System.out.println(recur(N, 1));</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="bottom-up">bottom-up</h3>
<p>이제는 bottom-up으로 풀어보도록 하자. 
어떠한 자릿수 N에 올 수 있는 수는 0 or 1 일 것이다. 그렇다면 N - 1에는 어떻게 와야하는가?
N에서 0 일때는 1 or 0 둘 다 올 수 있지만 1일 경우는 0밖에 오지 못한다는 것을 알 수 있다.</p>
<pre><code class="language-java">if (val == 0) {
    dp[digit][val] = dp[digit - 1][0] + dp[digit -1][1];
} else {
//val == 1
    dp[digit][val] = dp[digit - 1][0];
}</code></pre>
<h3 id="bottom-up-전체코드">bottom-up 전체코드</h3>
<pre><code class="language-java">
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static Long dp[][];
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        dp = new Long[N + 1][2];

        dp[1][0] = dp[1][1] = 1L;

        bottom_up(N);

            //1만 하는 이유는 N 자리 즉, 가장 앞자리에는 0이 올수 없기 때문에.
        System.out.println(dp[N][1]);
    }


    static void bottom_up(int N) {

        for(int i = 2; i &lt;= N; i++) {
            for(int j = 0; j &lt;= 1; j++) {

                if (j == 0) {
                    dp[i][j] = dp[i - 1][0] + dp[i - 1][1];
                } else if (j == 1){
                    dp[i][j] = dp[i - 1][0];
                }
            }
        }
    }

}
</code></pre>
<h3 id="여담">여담</h3>
<p>이친수를 적다보면 규칙이 하나보이는데,</p>
<p>dp[1] = 1;
dp[2] = 10;
dp[3] = 100,   101;
dp[4] = 1000,  1010,  1001;
dp[5] = 10000, 10100, 10010, 10001, 10101;</p>
<p>dp[N]에 대하여 dp[N -1] 수 뒤에 0을 붙인 값 + 
dp[N]에 대하여 dp[N - 2] 수 뒤에 01 을 붙인 값을 더한게 dp[N]에 된다는 것이다..</p>
<p>dp[3]을 먼저 보면 dp[1] = 1에 01 을 붙인 [101]과 dp[2]에 0을 붙인 [100]이 곧 dp[3]이 된다.
다시,
dp[4] 에서 dp[2]에 01을 붙인 [1001], dp[3]에 0을 붙인 [1000, 1010] 이 곧 dp[4]가 된다.
즉, 
dp[N] = dp[N -1] + dp[N -2]; 가 성립한다.<br>이렇게 1차원배열로도 문제를 풀 수 있다.   </p>
<h3 id="정리">정리</h3>
<p>이 문제는 풀 수 있는 방법이 여러가지였다. 저는 이전에 자릿수에 대해 문제를 풀었기에 2차원배열로 문제를 해결하였습니다. N자리에 들어갈 수 있는 값 [0,1]을 갖고 조건을 기준으로 이 전 dp[]값을 가지고 현재 dp[]을 구하는 것이었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 10844번: 쉬운 계단수 - Java, 자바]]></title>
            <link>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-10844%EB%B2%88-%EC%89%AC%EC%9A%B4-%EA%B3%84%EB%8B%A8%EC%88%98-Java-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@xxx-sj/%EB%B0%B1%EC%A4%80-10844%EB%B2%88-%EC%89%AC%EC%9A%B4-%EA%B3%84%EB%8B%A8%EC%88%98-Java-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Tue, 29 Aug 2023 07:08:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/xxx-sj/post/e15cb811-e024-4d95-b4e8-40bb854ac52e/image.png" alt=""></p>
<h3 id="문제-접근">문제 접근</h3>
<p>이 문제는 풀지못하여 좋아하는 이방인님 블로그를 참고하여 제 생각을 정리하였습니다.</p>
<h5 id="출처-httpsst-labtistorycom134">출처: <a href="https://st-lab.tistory.com/134">https://st-lab.tistory.com/134</a></h5>
<p>몇 가지 조건을 알아보도록 하자.
길이가 3인 계단 수에 대해 구한다면 123, 345 등등이 사용될 것이다.<br>여기서 123을 살펴보면 3번째 자리의 값은 1, 두 번째 자리는 2, 첫번 째 자리는 3이 될것이다.<br>다른 조건으로는 인접한 모든 자리의 차이가 1이라는 것인데, 이것을 살펴보면 만약 0일 경우<br>다음으로는 1 밖에 오지못할 것이다. [-1이 올 수 없기 때문에] 다음으로는 9 다음으로는 8밖에 못 올것이다. [10이 못오기 때문에]<br>예를 들어 10 다음으로는 101, 89 다음으로는 898 밖에 올 수 없다는 말이다.<br>나머지 수 2~8 까지는 +1,-1 값을 가질 수 있다.<br><img src="https://velog.velcdn.com/images/xxx-sj/post/98db6e2e-055a-4d07-ae73-cd19ee719526/image.png" alt="">
윗 그림은 제가 기억하기 위해 남겨놓습니다...</p>
<p>여기서는 2차원배열을 사용해야 하는데, 2차원 배열을 사용하는 이유는 [N번쨰 자리]의 [자릿값]을 표현하기 위해서 이다.<br>여기서 N번째 자리는 입력을 받을 것이고 해당 N번 째 자리에 들어가는 값들은 0 ~ 9 까지 일 것이다.<br>따라서 배열로 표현하면 [Long[N + 1][10]]이다.<br>여기서 N + 1 인 이유는 인덱스는 0부터 시작이고, 자연수는 1부터 시작이기 떄문이다.<br>10또한 같은 이유로 9가 아닌 10의 크기를 갖는 것이다.   </p>
<p>여기서 헷갈릴 수 있는데, 2차원 배열에서 첫 번째 배열은 자릿수에 해당한다.  아래에서 예를 들어보자<br>만약 10123 이란 숫자가 있다고 가정할 때<br>숫자:     1         0        1   2   3<br>자리:     5번째자리  4번째자리  3.. 2.. 1<br>이렇게 표현이 가능할텐데, 배열같은 경우 왼쪽에서 오른쪽으로 읽는데, 여기서는 다르게<br>인덱스를 마치 N번째 자리에 해당하는 값이라고 생각하면 쉽게 이해할 수 있다. 다시말해,<br>10123일 때<br>숫자:    1   0   1   2   3<br>인덱스:  5   4   3   2   1<br>자리:    5   4   3   2   1<br>이런식으로 값이 들어가게 된다.   </p>
<p>다음으로 1자리만 주어진다면 1 ~9 가 가능하며, 계단 수는 각각 1이 된다.<br>[1][1] ~ [1][9] = 1;   </p>
<p>이제 조건을 가지고 재귀함수를 만든다면 아래와 같을 것이다.<br>재귀함수는 2가지 인수를 갖는데, 하나는 자릿수에 해당하는 digit과 해당 자리에 할당되어있는 자릿값 val이다.
위에 보았듯이 1자리만 주어졌을 때 1이 모두 할당되었으니 재귀의 종료시점은 digit이 1일 때이다.   </p>
<pre><code class="language-java">private static long recur(int digit, int val) {

    if(digit == 1) {
        return dp[digit][val];
    }
}</code></pre>
<p>다음으로는 0과 9에 대한 조건이다. 자리수 N에 대하여 0일 때 뒤에는 1밖에 오지못한다는 점과<br>9일 때 뒤로는 8밖에 못오는 점이다.<br>위에서 설명했듯이 이차원배열의 첫 번째 배열은 N번 째 자리를 나타내는 것이다.<br>예를들어서<br>숫자:   1   0   1<br>자리:   3   2   1<br>인덱스: 3   2   1<br>따라서 digit - 1를 이해하자면 2째 자리가 0 일때 다음 첫 째 자리로는 1 만 올 수 있기 때문에<br>digit - 1, 1이 오게되는 것이다.</p>
<pre><code class="language-java">if(val == 0) {
    dp[digit][val] = recur(digit - 1, 1) % mod;
} else if (val == 9){
    dp[digit][val]=recur(digit-1, 8) % mod;
}</code></pre>
<p>0,9를 제외한 수는 +1, -1를 가질 수 있다.</p>
<pre><code class="language-java">} else {
    dp[digit][val] = (recur(digit - 1, val - 1) + recur(digit - 1, val + 1)) % mod;
}</code></pre>
<p>참고한 블로그에서 모듈러연산을 사용하였는데, 모듈러연산에 대해 짧게 설명하자면<br>% 연산에 대한 나머지 값이 같다는 것이다.<br>3 % 10 과 13 % 10의 결과 값이 같다는 것이다.<br>%연산에 대해서 몫은 중요하지 않고, 나머지가 같은 값이 나온다는 것을 설명한다.<br>A % B 에 대해서 A에 B를 여러번 더해도 같은 결과가 나온다는 것이다.<br>3 % 10, 13 % 10, 23 % 10 ...,<br>이것을 가지고 값이 long이 넘어갈 수도 있기 때문에 dp[][]에 저장을 할 때 % mod 만큼하여 저장한다.<br>결과에서도 % mod한 값을 출력하기 때문이다.<br>mod값이 10,000일 경우 10,007 % 10,000 에서의 결과는 7이고<br>7 % 10,000 에서의 결과도 7로 동일하기 때문이다.   </p>
<h3 id="전체코드-top-down-재귀">전체코드 top-down [재귀]</h3>
<pre><code class="language-java">
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static Long[][] dp;

    static long mod = 1_000_000_000;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        //배열 생성
        dp = new Long[N + 1][10];

        //초기값 설정
        for(int i = 0; i &lt; 10; i++) {
            dp[1][i] = 1L;
        }

        long result = 0;
        // N에 도달하기 위한 1 ~9 까지의 수를 구한다.
        for(int i = 1; i &lt;= 9; i++) {
            result += recur(N, i);
        }

        System.out.println(result % mod);

    }

    private static long recur(int digit, int val) {

        if(digit == 1) {
            return dp[digit][val];
        }

        if(dp[digit][val] == null) {

            if(val == 0) {
                dp[digit][val] = recur(digit - 1, 1) % mod;
            } else if (val == 9) {
                dp[digit][val] = recur(digit - 1, 8) % mod;
            } else {
                dp[digit][val] = (recur(digit - 1, val - 1) + recur(digit - 1, val + 1)) % mod;
            }
        }

        return dp[digit][val];
    }
}
</code></pre>
<h3 id="bottom-up">bottom-up</h3>
<p>다른 문제로 top-down으로  해결했던 문제를 bottom-up으로 다시 풀어보려 한다.<br>bottom-up 방식은 아래에서 부터 위로 올라가는 방식으로 초기값 이후 ~ N까지 진행하며 값을 채운다. 따라서, bottom-up 같은경우 이 전 값을 이용하여 현재 dp[] 을 채운다.
다시말해,<br>N = 3 일때 N &lt; 3 일때의 값을 가지고 N = 3을 채운 다는 것이다.<br>생각해보면 위의 값은 채워지지 않았기 때문에 이 전값을 사용하는것은 당연한걸지도..?</p>
<p>위에서 볼 수 있듯이 초기값은 1자리 일 경우 1 ~ 9 의 값은 1만 갖는다. </p>
<pre><code class="language-java">for(int i = 1; i &lt; 10; i++) {
    dp[1][i] = 1;
}</code></pre>
<p>그리고 조건으로는 만약 현재 자리값의 수가 0 일 때는 1에서 밖에 오지 못 한것이고, 9일때는 8에서 밖에 오지 못한것이다.
그 이외의 숫자 2~8은 다음 자릿수의 +1, -1에서 온 것이다.</p>
<pre><code class="language-java">if(val == 0) {
    dp[digit][val] = dp[digit - 1][1] % mod;
} else if (val == 9) {
    dp[digit][val] = dp[digit - 1][8] % mod;
} else {
    dp[i][j] = (dp[i - 1][j + 1] + dp[i -1][j - 1]) % mod;
}</code></pre>
<h3 id="전체코드-bottom-up">전체코드 bottom-up</h3>
<pre><code class="language-java">package date_2023_08_15.DP_10844;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static Long[][] dp;

    static long mod = 1_000_000_000;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        //배열 생성
        dp = new Long[N + 1][10];

        //초기값 설정
        for(int i = 0; i &lt; 10; i++) {
            dp[1][i] = 1L;
        }

        long result = 0;
        bottom_up(N);

        for(int i = 1; i &lt;= 9; i++) {
            result += dp[N][i];
        }

        System.out.println(result % mod);

    }


    static void bottom_up(int N) {

        for(int i = 2; i &lt;= N; i++) {

            for(int j = 0; j &lt; 10; j++) {
                if(j == 0) {
                    dp[i][j] = dp[i - 1][1] % mod;
                } else if (j == 9) {
                    dp[i][j] = dp[i - 1][8] % mod;
                } else {
                    dp[i][j] = (dp[i - 1][j + 1] + dp[i -1][j - 1]) % mod;
                }
            }
        }
    }
}</code></pre>
<h3 id="정리">정리</h3>
<p>이 문제는 풀면서 처음으로 나온 이차원 배열이다. 여기서 이차원 배열의 개념은 어떠한 N의 자리에 대하여 1~9까지의 값 이었다.   어떠한 N의 자리 / 1 ~ 9 까지의 경우의 수  아직 어떨 때 2차원 배열을 써야하는지 개념이 잡히지 않았지만, 대략 이렇게 이해하고 있다.<br>이 문제의 경우 예외 조건을 찾는것 또한 중요하다. 곰곰히 생각해보면 어떠한 수 X에 대하여 도달할 수 있는 경우의 수를 찾는 문제였다. 예를 들어, 2자리 3 [2][3] 에 도달하려면 [1][2] 또는 [1][4] 에서 
[2][3]에 도달할 수 있는 그런 느낌.. 이랄까.. 거기에 몇 가지 예외를 추가하여 푼 문제 같다.  </p>
]]></description>
        </item>
    </channel>
</rss>