<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Jung_In_Lee.log</title>
        <link>https://velog.io/</link>
        <description>Software Developer</description>
        <lastBuildDate>Thu, 04 Sep 2025 10:12:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Jung_In_Lee.log</title>
            <url>https://velog.velcdn.com/images/sil_tr/profile/c593a45a-88cf-4f78-ad8c-f4011b52169b/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Jung_In_Lee.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sil_tr" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[삼성기출 코드트리] 미지의 공간 탈출]]></title>
            <link>https://velog.io/@sil_tr/%EC%82%BC%EC%84%B1%EA%B8%B0%EC%B6%9C-%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EB%AF%B8%EC%A7%80%EC%9D%98-%EA%B3%B5%EA%B0%84-%ED%83%88%EC%B6%9C</link>
            <guid>https://velog.io/@sil_tr/%EC%82%BC%EC%84%B1%EA%B8%B0%EC%B6%9C-%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EB%AF%B8%EC%A7%80%EC%9D%98-%EA%B3%B5%EA%B0%84-%ED%83%88%EC%B6%9C</guid>
            <pubDate>Thu, 04 Sep 2025 10:12:33 GMT</pubDate>
            <description><![CDATA[<h3 id="어려웠던점">어려웠던점</h3>
<p>1차 미통과 : 윗면일때 미지의 공간으로 가는 좌표 수정
2차 미통과 : WeirdThing 배열의 WeirdThing 좌표 미수정
3차 통과</p>
<h3 id="그전에-어려웠던점">그전에 어려웠던점</h3>
<blockquote>
<ul>
<li>답이 안나왔던 과정</li>
</ul>
</blockquote>
<ol>
<li>윗면에서 동,서,남,북으로 가는것뿐만아니라 동서남북에서 자유롭게 다른 측면을 왔다갔다 할수있다는점</li>
</ol>
<ul>
<li>어떻게 보면 당연함.</li>
</ul>
<ol start="2">
<li>예제의 동,서,남,북,윗면이 모두 정면을 봤을때 기준이라는것.</li>
</ol>
<ul>
<li><p>문제의 예제 파악 미숙</p>
</li>
<li><p>문제풀다가 나중에 알아차렸던점</p>
</li>
</ul>
<ol>
<li>시간의 방 탈출구를 구하는게 끝이아니라, 미지의 방 탈출구(4)로 이동해야한다는 점.<ul>
<li>bfs를 두번 돌려야한다는점.</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[env.yml vs env.properties]]></title>
            <link>https://velog.io/@sil_tr/env.properties</link>
            <guid>https://velog.io/@sil_tr/env.properties</guid>
            <pubDate>Sun, 31 Aug 2025 10:45:24 GMT</pubDate>
            <description><![CDATA[<h3 id="envyml-vs-envproperties">env.yml vs env.properties</h3>
<ul>
<li><p>env.yml
application.yml</p>
<pre><code>spring:
    config:
        import: env.yml
---
spring:
    mail:
        username: ${USERNAME}</code></pre><p>env.yml</p>
<pre><code>USERNAME=~~</code></pre></li>
<li><p>env.properties :  application.properties에 선언후 비밀키 내용을 env.properties에 통째로 입력</p>
<p>application.properties</p>
<pre><code>spring.config.import=env.properties</code></pre><p>env.properties</p>
<pre><code>spring.mail.username=~~</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Node.js는 싱글스레드인가?]]></title>
            <link>https://velog.io/@sil_tr/JS-Node.js%EB%8A%94-%EC%8B%B1%EA%B8%80%EC%8A%A4%EB%A0%88%EB%93%9C%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@sil_tr/JS-Node.js%EB%8A%94-%EC%8B%B1%EA%B8%80%EC%8A%A4%EB%A0%88%EB%93%9C%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Tue, 31 Dec 2024 13:12:01 GMT</pubDate>
            <description><![CDATA[<p>※ 해당 글은 기록용입니다. 부족한 부분이 있을수도있습니다. 고칠점이있다면 댓글로 알려주세요.</p>
<br>

<p>무료하게 앉아있는 연말... 새로운 주제를 접하게되어 정리해보려고한다.</p>
<p>시작은 멀티쓰레드와 비동기를 동일시하는 주제에서 시작이다.</p>
<h2 id="멀티-쓰레드--비동기">멀티 쓰레드 = 비동기?</h2>
<ul>
<li><p>결론부터 말하자면 당연히 아니다. 워낙 스레드를 이용해서 비동기/동기개념이 다뤄지고있다보니 처음 접하는 사람은 스레드를 사용해서 해당개념을 이해하려고한다. 이래서 생기는 오류가 아닌가 싶다.</p>
</li>
<li><p>멀티쓰레드는 알다시피 하나 또는 여러개의 프로세스에서 여러 스레드를 사용해서 작업을 처리하는 방식이다.</p>
</li>
<li><p>비동기방식은 일이 들어온 순서에따라 결과도 순서대로 처리되는것이 아니라, 일을 들어온 요청대로 다 받아서, 그냥 완료되면 완료되었다고 보고하는것이다.</p>
</li>
<li><p>잘못된 개념으로는 비동기방식으로 일을 처리하던중, 다른 작업 요청이 오면 그 요청을 실행하면서, Timeout된 요청은 다른 스레드가 받아서 실행한다는 것인데, 이 비유에서 이런 오류가 많이 발생하는것같다.</p>
</li>
<li><p>싱글 스레드 상황으로 가정해보자.</p>
</li>
<li><p>일례로, Node.js는 싱글스레드이면서 비동기작업을 진행한다고한다.</p>
</li>
<li><p>Node.js에서 작업을 실행중에 Timeout요청이 발생했다고 하자.</p>
</li>
<li><p>그럼 이 작업은 libuv(Node.js의 I/O작업 담당 스레드)가 백그라운드로 실행한다.</p>
</li>
<li><p>Node.js는 싱글스레드가 맞긴하다. 이는 근데 관점의 차이다.</p>
</li>
</ul>
<h2 id="nodejs는-싱글스레드인가">Node.js는 싱글스레드인가?</h2>
<ul>
<li><p>Node.js = Node.js + Libuv</p>
</li>
<li><p>Node.js는 이벤트 루프, 자바스크립트 메인 코드 실행을 담당하는 메인스레드가 하나 존재한다. 이 관점으로 보면 맞다.</p>
</li>
<li><p>Node.js는 메인 스레드외에 I/O요청을 담당하는 스레드 풀이 존재한다. 이게 Libuv 라이브러리다.</p>
</li>
<li><p>Libuv 라이브러리는 메인스레드로부터 떨어져나온 작업을 백그라운드로 실행한다. 스레드 4개를 사용한다.</p>
</li>
<li><p>크게 보면 총 5개의 스레드를 사용하기때문에, 엄밀히 말하면 싱글스레드라고 말할수없다.</p>
</li>
<li><p>흔히 싱글스레드라고 말하는건 이벤트루프를 처리하는 메인스레드 하나만 말하는 경우이다. </p>
</li>
<li><p>보통 이렇게 말하는 사람은 다 알고 쓴다... 하지만 나같은 JS가 주언어가 아닌사람은 당연히 모른다.</p>
</li>
</ul>
<h2 id="그럼-진짜-스레드가-하나일때는-어떻게-비동기-작업을-하지">그럼 진짜 스레드가 하나일때는 어떻게 비동기 작업을 하지?</h2>
<ul>
<li><p>돌아가서 Node.js에대한 이야기말고, 진짜 스레드가 하나만 있다고 가정하자.</p>
</li>
<li><p>작업 A를 실행중에 Timeout요청을 받고, 작업 B를 실행한다. 남은 작업A는 담당해줄 스레드가 없다. 그럼 이 작업은 그냥 작업B가 끝날때까지 유지되는것인가?</p>
</li>
<li><p>내생각에는 그냥 놔둬도 비동기라는 개념이 맞다고 생각한다. 비동기는 그냥 일을 요청하면 다 받기때문이다. (A -&gt; B -&gt; B완료 -&gt; A완료) 그냥 이렇게 실행해도 비동기기때문.</p>
</li>
<li><p>그래도 진짜 맞는지 조사를 해봤다.
<img src="https://velog.velcdn.com/images/sil_tr/post/855236a4-466b-4adc-82f2-7d14a7eaf168/image.png" alt=""></p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sil_tr/post/41a4e293-e46b-4a53-a2bf-85706987c00d/image.png" alt="">
<img src="https://velog.velcdn.com/images/sil_tr/post/61c125c4-1f88-49dd-a434-2a3659ab291a/image.png" alt=""></p>
<ul>
<li><p>여기서 고려하는 사항은 백그라운드 작업이나, 외부 api를 지원하지않는 상황이다. 이때는 블로킹/논블로킹 관점으로 들어가야할것같다.</p>
</li>
<li><p>여기서부터는 다음에...</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] HashMap]]></title>
            <link>https://velog.io/@sil_tr/Java-HashMap</link>
            <guid>https://velog.io/@sil_tr/Java-HashMap</guid>
            <pubDate>Thu, 26 Dec 2024 05:25:39 GMT</pubDate>
            <description><![CDATA[<h2 id="해시맵의-키가-객체일때">해시맵의 키가 객체일때</h2>
<ul>
<li>문제를 풀던 도중 객체에 equals, hashcode를 왜 오버라이드하는지 알수있게하는 문제를 발견했다.<pre><code class="language-java">import java.io.*;
import java.util.*;
</code></pre>
</li>
</ul>
<p>public class Template {
    static class Point{
        int x;
        int y;</p>
<pre><code>    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj){
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Point point = (Point)obj;
        return x == point.x &amp;&amp; y == point.y;
    }

    @Override
    public int hashCode(){
        return Objects.hash(x,y);
    }
}



public static void main(String[] args){
    Map&lt;Point, Integer&gt; pointMap = new HashMap&lt;&gt;();

    Point point1 = new Point (3,4);
    Point point2 = new Point (3,4);
    Point point3 = null;
    Point point4 = null;

    pointMap.put(point1, pointMap.getOrDefault(point1, 0) + 1);
    pointMap.put(point2, pointMap.getOrDefault(point2, 0) + 1);
    pointMap.put(point3, pointMap.getOrDefault(point3, 0) + 1);
    pointMap.put(point4, pointMap.getOrDefault(point3, 0) + 1);

    // equals, hash 오버라이드 하지않을경우
    // 예상 출력 : 1 1 1 1
    // 실제 : 1 1 2 2
    // 같은 좌표만 묶지 못함.
    System.out.println(pointMap.get(point1));
    System.out.println(pointMap.get(point2));
    System.out.println(pointMap.get(point3));
    System.out.println(pointMap.get(point4));

    // equals, hash 오버라이드 했을경우
    // 예상 출력 : 2 2 2 2
    // 실제 출력 : 2 2 2 2
}</code></pre><p>}</p>
<pre><code>
- 해시맵의 put 작동방식은 다음과같다.
1. 먼저 같은값이 있는지 체크
2. 있으면 그곳에 값을 대입

- 위에서는 Map&lt;Point, Integer&gt;으로 객체의 카운팅을 하는 맵이다.
- 요구사항은 같은 좌표를 동등시하여 카운트를 올리는 방식이다.

- 해시맵에서는 그대로 사용하면 위의 (3,4) 좌표는 서로 다르게 계산된다.

- 좀더 구체적으로 해시맵의 put방식을 살펴보면,
- hashmap은 키의 동등성을 비교할때 hashcode를 먼저 사용하고, equals를 통해 추가적으로 확인한다.
1. hashcode()를 사용해서 저장할 버킷을 결정하고
2. 같은 공간안에 동일한 key가있는지 equals로 비교한다.

- 즉, equals를 오버라이딩하지않으면 객체 메모리 주소 기반으로 hashcode를 반환하고, equals는 주소값을 동등비교밖에 하지않는다.
![](https://velog.velcdn.com/images/sil_tr/post/3aeeba4f-7c07-4996-bac4-a47dd3bd6398/image.png)
- 상위 메소드를 타고 올라가면
![](https://velog.velcdn.com/images/sil_tr/post/c52f081f-e43e-4020-8bf6-c0ed2effe7d6/image.png)
- 주소 동등비교 밖에 없다.
- 값을 비교하려면 따로 커스텀해서 지정해줘야한다.

- 따라서 원하는 연산인 두 좌표가 같음에도 불구하고 같은 좌표로 카운트되지않는다.

- 문제로 돌아가서 커스텀하고 싶은것은 hashcode 함수도 포함이다.
```java
        @Override
        public int hashCode(){
            return Objects.hash(x,y);
        }</code></pre><ul>
<li><p>해당 메소드는 x,y 좌표를 매개변수로 Objects의 클래스의 도움을 받아 해시코드를 생성한다.</p>
</li>
<li><p>그리고 equals에 오버라이딩한 설명은 다음과같다.</p>
<pre><code class="language-java">      @Override
      public boolean equals(Object obj){
          if (this == obj) return true;
          if (obj == null || getClass() != obj.getClass()) return false;
          Point point = (Point)obj;
          return x == point.x &amp;&amp; y == point.y;
      }</code></pre>
</li>
<li><p>일단 기본적으로 주소값이 같을때 -&gt; true</p>
</li>
<li><p>객체가 null이거나 클래스 타입이 다를때 -&gt; false</p>
</li>
<li><p>obj타입이 point인데, 각 변수가 같을때 -&gt; true</p>
</li>
<li><p>출처</p>
<ul>
<li>해당문제
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/340211">프로그래머스 충돌위험 찾기</a>
<a href="https://acg6138.tistory.com/entry/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-PCCP-%EA%B8%B0%EC%B6%9C-3%EB%B2%88-%EC%B6%A9%EB%8F%8C%EC%9C%84%ED%97%98-%EC%B0%BE%EA%B8%B0-Java">정답 블로그</a>
<a href="https://chatgpt.com/c/676cd751-236c-8013-878d-3b65a805ac93">chatgpt</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] DATE_ADD 등 날짜 관련 SQL]]></title>
            <link>https://velog.io/@sil_tr/SQL-DATEADD-%EB%93%B1-%EB%82%A0%EC%A7%9C-%EA%B4%80%EB%A0%A8-SQL</link>
            <guid>https://velog.io/@sil_tr/SQL-DATEADD-%EB%93%B1-%EB%82%A0%EC%A7%9C-%EA%B4%80%EB%A0%A8-SQL</guid>
            <pubDate>Sat, 21 Dec 2024 07:33:45 GMT</pubDate>
            <description><![CDATA[<h1 id="날짜">날짜</h1>
<h2 id="date_add">DATE_ADD</h2>
<ul>
<li>날짜 관련해서 일수를 더하고 빼는 함수이다<pre><code class="language-sql">DATE_ADD(날짜, INTERVAL 더하거나 뺄 값 단위) AS 별칭
</code></pre>
</li>
</ul>
<p>DATE_ADD(&#39;2024-12-21&#39; INTERVAL 1 DAY) AS DATE</p>
<pre><code>
위 함수를 실행하면
```sql
2024-12-22 00:00:00</code></pre><p>이 나오게된다.</p>
<p>이는 DateFormat함수로 변환해주면 이쁘게 포장된다.</p>
<pre><code class="language-sql">DATE_FORMAT(DATE, &#39;%Y-%m-%d&#39;)</code></pre>
<pre><code class="language-sql">2024-12-22</code></pre>
<h2 id="date_sub">DATE_SUB</h2>
<ul>
<li>자매품 DATE_SUB함수도 있다. 양식은 같다.<pre><code class="language-sql">DATE_SUB(&#39;2024-12-21&#39;, INTERVAL 1 DAY)</code></pre>
<pre><code class="language-sql">2024-12-20</code></pre>
</li>
</ul>
<h2 id="datediff">DATEDIFF</h2>
<ul>
<li>각 날짜의 차이를 구하는 함수이다.<pre><code class="language-sql">DATEDIFF(날짜, 날짜)</code></pre>
</li>
</ul>
<h2 id="window-함수">Window 함수</h2>
<ul>
<li>좀 더 정교하게 추출할수있는 함수다.</li>
<li>현재 열을 포함한 이전 6일 사이의 범위에 대해서 합을 구하는 쿼리이다.</li>
<li>over 이후 조건을 적는다.<pre><code class="language-sql">select sum(sum_amount) over w as amount
from cte_sum
window w as (order by visited_on range between interval 6 day preceding and current row)</code></pre>
</li>
</ul>
<h2 id="offset">offset</h2>
<ul>
<li>데이터의 어디부터 어디까지 범위를 나타냄.</li>
<li>0부터 시작하는듯.</li>
</ul>
<h2 id="distinct">distinct</h2>
<ul>
<li>함수가 아님.</li>
</ul>
<h2 id="with-table_name-as-subquery">with (table_name) as (subquery)</h2>
<ul>
<li>여러 테이블을 표시할경우 &#39;,&#39; 로 나열한다.<pre><code class="language-sql">with cte_sum as(
  select visited_on,
  sum(amount) as sum_amount
  from customer
  group by visited_on
),
cte_all as(
  select visited_on,
  sum(sum_amount) over w as amount,
  round(sum(sum_amount) over w / 7, 2) as average_amount
  from cte_sum
  window w as (order by visited_on range between interval 6 day preceding and current row)
)</code></pre>
</li>
</ul>
<h2 id="출처">출처</h2>
<p><a href="https://devjhs.tistory.com/89">[MYSQL] DATE_FORMAT</a>
<a href="https://extbrain.tistory.com/58">[MYSQL] DATE_ADD, DATE_SUB</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Novelit] Aws 요금 이슈]]></title>
            <link>https://velog.io/@sil_tr/Novelit-Aws</link>
            <guid>https://velog.io/@sil_tr/Novelit-Aws</guid>
            <pubDate>Fri, 20 Dec 2024 10:19:42 GMT</pubDate>
            <description><![CDATA[<h2 id="aws-예산-초과">Aws 예산 초과..</h2>
<p><img src="https://velog.velcdn.com/images/sil_tr/post/b07d2c5d-59e0-4f26-9e5d-40763238aa5f/image.png" alt="">
AWS 프리티어 사용량 초과를 대비해 예산 알림을 0.01$에 설정을 해놓았었다.
그라파나 세팅후 3일만에 이메일 알림이 오게 되는데...
<img src="https://velog.velcdn.com/images/sil_tr/post/8cbb4ebc-627b-447a-9669-2d1c705a8746/image.png" alt="">
바로 그만쓰라는 무언의 압박 메세지...</p>
<p>받자마자 든 생각이 용량 초과는 아니었을것이라는 생각이다.
AWS EC2용량은 한개만 굴리기때문에 넘는 기억이 없기때문이다. (역시 맞음)
<img src="https://velog.velcdn.com/images/sil_tr/post/bf190756-8033-4754-90d8-e0a1f92ae534/image.png" alt=""></p>
<p>일단 뭐 메일을 자세히 보니 AWS Data Transfer의 한달 사용량이 초과했다. 청구서에 들어가봤다.
<img src="https://velog.velcdn.com/images/sil_tr/post/862facd2-53c9-4897-ae62-6994fd36d39f/image.png" alt="">
탄력적IP를 사용한 ec2간의 인바운드 아웃바운드 통신에서 요금이 발생한거같다.</p>
<p>Practice 서버와 Monitoring 서버를 따로쓰고있던거에서 패킷을 분석해봤다.
<img src="https://velog.velcdn.com/images/sil_tr/post/7581725b-461a-46b2-a303-cf1d78f45bcf/image.png" alt="">
하... 위는 네트워크 패킷 입력개수이다. 12/17일경 33,000개의 패킷이 확 오른것을 볼수있다. 누가 많은 양의 패킷을 시도하는 경우는 퍼블릭IP를 사용하면 흔하게 볼수있는 것이다.(저번에 모니터링 서버를 구축하지않았을때도 그랬다.)</p>
<p>근데 그 이후 보면 패킷이 2,200대를 유지하는데, 이는 Node-exporter를 통해 나가는 패킷들이다. Prometheus에서 Practice서버 패킷을 요청하기때문에 꾸준히 요청이 들어오게된다. 당시 scrap-interval을 10s로 설정을 해놨는데 프리티어에서는 감당이 안되는 요청량이였나보다.</p>
<p>3일만에 1.7gb의 data transfer를 사용했다면, 한달 제한이 1gb니까 20s로 제한하면 3일만에 0.85gb, 이를 넉넉하게 32일동안사용하려면 220s에 한번 요청을 하는것으로 바꾸는것이 좋을거같다. 220s = 3분 40초에 한번 패킷 요청...</p>
<p>사실 아예 저 피뢰침을 막는게 근본적인 해결법인데, 그것도 조사해봐야겠다.</p>
<p>일단 1달 사용량을 모두 사용해서 로컬로 프로메테우스를 전환해야겠다.</p>
<p>+CPU 사용률
<img src="https://velog.velcdn.com/images/sil_tr/post/15b12796-301c-4e2c-b16c-779b92167df0/image.png" alt="">
+네트워크 패킷 출력개수
<img src="https://velog.velcdn.com/images/sil_tr/post/906acbe7-73d0-457f-b446-e7f58dd32511/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Novelit] Grafana 관측 이슈]]></title>
            <link>https://velog.io/@sil_tr/Novelit-Grafana-%EA%B4%80%EC%B8%A1-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@sil_tr/Novelit-Grafana-%EA%B4%80%EC%B8%A1-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Tue, 17 Dec 2024 17:25:15 GMT</pubDate>
            <description><![CDATA[<p>grafana를 구축하고 둘러보는데, 좀 큰 이슈같아 보이는것을 발견했다.
<img src="https://velog.velcdn.com/images/sil_tr/post/82ce1cbf-5f17-437f-8ef6-c24936462c1f/image.png" alt=""></p>
<ul>
<li>job을 변경해보는데  위와같이 novelit-aws(production 서버) 에서 RAM 사용량이 94퍼가 관측된것...
<img src="https://velog.velcdn.com/images/sil_tr/post/0ac4f74e-e95a-41dc-9bd2-aa5d20d25958/image.png" alt=""></li>
<li>문제가있는지 찾아보고, 조치를 취해야겠다. 모니터링 환경을 구성하니, 확실히 로컬로 돌릴때 알수없었던 메모리 사용량을 직관적으로 알수있어서, 서버가 어떤 상황인지 파악하기 너무 좋은것같다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 초기화]]></title>
            <link>https://velog.io/@sil_tr/Java-%EC%B4%88%EA%B8%B0%ED%99%94</link>
            <guid>https://velog.io/@sil_tr/Java-%EC%B4%88%EA%B8%B0%ED%99%94</guid>
            <pubDate>Mon, 16 Dec 2024 08:12:11 GMT</pubDate>
            <description><![CDATA[<p>14500 테트로미노 문제를 풀던중 한가지 문제를 마주하였다.</p>
<p>바로 배열의 초기화에대한것.</p>
<p>자바에서 초기화는 다음과 같다.</p>
<pre><code class="language-java">boolean[][] visited = new boolean[N][M];</code></pre>
<p>이 함수를 보고 여태 한 생각은 처음 메모리공간이 O(1)시간 만에 디폴드값으로 할당되는줄알았다. (부끄럽게도)</p>
<p>하지만 테트로미노를 풀던 중,</p>
<ol>
<li>전역으로 정의된 visited<pre><code class="language-java">static boolean[][] visited;
</code></pre>
</li>
</ol>
<p>visited = new boolean[N][M];
for (int i = 0; i &lt; N; i++) { // 500
    for (int j = 0; j &lt; M; j++) { // 500
        visited[i][j] = true;
        dfs(i, j, 1, map[i][j]);
        visited[i][j] = false;
    }
}</p>
<pre><code>2. 지역함수의 파라미터로 정의된 visited
```java
        for (int i = 0; i &lt; N; i++) { // 500
            for (int j = 0; j &lt; M; j++) { // 500
                boolean[][] visited = new boolean[N][M];
                visited[i][j] = true;
                dfs(i, j, 1, map[i][j], visited);
            }
        }

    private static void dfs(int x, int y, int cnt, int sum, boolean[][] visited) {
        if (cnt == 4){
            max = Math.max(max, sum);
            return;
        }

        for (int d = 0; d &lt; 4; d++) { // 4
            int nx = x + dx[d];
            int ny = y + dy[d];

            if (!isRange(nx, ny)) continue;
            if (!visited[nx][ny]) {
                visited[nx][ny] = true;
                dfs(nx, ny, cnt + 1, sum + map[nx][ny], visited);
                visited[nx][ny] = false;
            }
        }
    }</code></pre><p>2번 코드에서 시간초과가 나는것을 마주했다.</p>
<p>그래서 두 차이점을 chatgpt로 알아본 결과..</p>
<blockquote>
</blockquote>
<p>네, 맞습니다. <code>boolean[][] visited = new boolean[N][M]</code>을 선언하면 내부적으로 다음과 같은 초기화 작업이 이루어집니다:</p>
<h3 id="1-초기화-동작"><strong>1. 초기화 동작</strong></h3>
<ul>
<li><strong>Java에서 배열은 선언과 동시에 기본값으로 초기화</strong>됩니다.</li>
<li><code>boolean[][] visited = new boolean[N][M];</code>의 경우:<ul>
<li><code>visited</code>는 <strong>2차원 배열</strong>이므로, 내부적으로 <code>N * M</code>개의 <code>boolean</code> 값을 생성하고, <strong>모두 기본값 <code>false</code>로 초기화</strong>합니다.<h4 id="내부-동작">내부 동작:</h4>
</li>
</ul>
</li>
<li>메모리 공간 할당:<ul>
<li>O(N×M)의 메모리 공간이 할당됩니다.</li>
</ul>
</li>
<li>초기화:<ul>
<li>각 배열 요소를 기본값 <code>false</code>로 설정하며 O(N×M) 시간이 소요됩니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="2-왜-초기화가-필요한가"><strong>2. 왜 초기화가 필요한가?</strong></h3>
<p>Java는 <strong>안전한 초기값</strong>을 보장하기 위해 배열 선언 시 초기화를 자동으로 수행합니다.<br>이는:</p>
<ul>
<li>선언된 변수에 <strong>알 수 없는 값(garbage value)</strong>가 남아있는 것을 방지합니다.</li>
<li><code>boolean</code>은 기본값이 <code>false</code>, <code>int</code>는 <code>0</code>, <code>Object</code>는 <code>null</code> 등으로 초기화됩니다.</li>
</ul>
<hr>
<h3 id="3-초기화-비용의-영향"><strong>3. 초기화 비용의 영향</strong></h3>
<ul>
<li>배열 크기가 클수록 초기화 작업에 소요되는 시간이 늘어납니다.<ul>
<li>예를 들어, (N = 500), (M = 500)이라면 (250,000)번의 기본값 설정이 이루어집니다.</li>
</ul>
</li>
<li><strong>반복적으로 선언</strong>한다면 이 초기화 작업이 누적되어 성능에 영향을 미칠 수 있습니다.</li>
</ul>
<hr>
<h3 id="4-최적화-방법"><strong>4. 최적화 방법</strong></h3>
<ul>
<li><strong>전역 변수 사용</strong>:<ul>
<li><code>visited[][]</code>를 한 번 선언하여 모든 탐색에 재사용하면, 배열 초기화 작업을 반복적으로 수행할 필요가 없습니다.</li>
<li>탐색마다 <code>visited[x][y]</code> 값을 <code>true</code>/<code>false</code>로 변경하면서 활용합니다.</li>
</ul>
</li>
<li><strong>배열 초기화 최소화</strong>:<ul>
<li>필요할 때만 특정 영역을 초기화하거나, <code>Arrays.fill()</code>을 이용해 특정 행만 초기화합니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="5-결론"><strong>5. 결론</strong></h3>
<ul>
<li>선언만으로도 O(N×M)번의 초기화 작업이 이루어지며, 이는 코드 실행 시간에 영향을 미칠 수 있습니다.</li>
<li>배열을 반복적으로 생성하기보다는 <strong>전역으로 선언해 재사용</strong>하거나, 필요에 따라 <strong>부분 초기화</strong>를 수행하면 효율성을 높일 수 있습니다.</li>
</ul>
<p>결론적으로 초기화를 할때 배열의 크기만큼 작업이 이루어진다는것이다.
따라서 O(1)이 아닌 O(N * M)만큼의 실행시간이 걸린다. 알고리즘을 풀때는 전역변수를 주로 사용하는것이 이와같은 이유인것같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot JPA] Audit]]></title>
            <link>https://velog.io/@sil_tr/Spring-Boot-JPA</link>
            <guid>https://velog.io/@sil_tr/Spring-Boot-JPA</guid>
            <pubDate>Sun, 15 Dec 2024 10:47:36 GMT</pubDate>
            <description><![CDATA[<h2 id="audit">Audit</h2>
<ul>
<li>자동으로 시간을 넣어주는 기능.</li>
<li>몰랐는데 spring boot에서 지원하는 기능이아니라, jpa에서 지원하는 기능이다.
이걸 redis에 적어서 오류가 난듯?</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Novelit] Docker-compose에 Grafana, Node-exporter, Prometheus 설치]]></title>
            <link>https://velog.io/@sil_tr/Novelit-Docker-compose%EC%97%90Grafana-Node-exporter-Prometheus-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@sil_tr/Novelit-Docker-compose%EC%97%90Grafana-Node-exporter-Prometheus-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Wed, 11 Dec 2024 13:05:56 GMT</pubDate>
            <description><![CDATA[<h2 id="전제">전제</h2>
<ul>
<li>먼저 모니터링 시스템을 구축하기위해 알아야할 조건이있다.
같은 서버안에 모니터링 컨테이너를 구축하면, 메모리를 같이 사용하기때문에 정확한 결과를 이끌어내기 어렵다. 따라서 모니터링 서버를 따로 구축하는것을 추천한다.</li>
<li>헷갈리니까 monitoring, production으로 나누어서 표시하겠다.</li>
<li>이 글은 monitoring 서버와 production 서버를 나누어서 구축했다.</li>
<li>먼저, monitoring 서버에는 다음과 같은 프로그램을 설치한다.<ul>
<li>prom/prometheus</li>
<li>grafana</li>
</ul>
</li>
<li>production 서버에는 다음을 설치한다<ul>
<li>node-exporter<h2 id="monitoring-서버">monitoring 서버</h2>
<h2 id="1-docker-compose">1. Docker-compose</h2>
</li>
</ul>
</li>
<li>docker-compose.yml<pre><code>services:
prometheus:
  image: prom/prometheus
  container_name: prometheus
  restart: always
  env_file:
  - .env
  ports:
    - ${PROMETHEUS_BINDING_PORT}:${PROMETHEUS_PORT}
  volumes:
    - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
  user: root
  networks:
    - promnet

</code></pre></li>
</ul>
<p>  grafana:
    image: grafana/grafana
    container_name: grafana
    restart: always
    env_file:
    - .env
    ports:
      - ${GRAFANA_BINDING_PORT}:${GRAFANA_PORT}
    user: root
    networks:
      - promnet</p>
<p>networks:
  promnet:
    driver: bridge</p>
<pre><code>

## 2. Prometheus 설정
- 우선 docker-compose를 업데이트하기 전에 ec2내에 prometheus 폴더를 생성하고, prometheus.yml을 작성한다.
- 프로메테우스는 스프링부트 메트릭을 수집하기때문에, 스프링부트에서도 설정이 필요하다.
### applicaiton.yml
- 여기서 monitoring 서버를 구축한다고, application-monitoring.yml을 따로 설정하는경우가있는데...
- 해당 서버의 메트릭을 수집하는것이기 때문에 같은 yml에 넣어줘야한다. 아니면 같이 적용하거나. 그런 방법이있는지는 아직 안찾아봐서 모르겠다. 난 같이 설정해줬다.
- 혹시 spring boot security를 적용중인경우 url을 열어놔야한다.</code></pre><p>/actuator/**</p>
<pre><code>- application.yml</code></pre><hr>
<h3 id="monitoring">monitoring</h3>
<p>management:
  server:
    port: ${MANAGEMENT_SERVER_PORT}
  endpoint:
    metrics:
      enabled: true
    prometheus:
      enabled: true
  endpoints:
    web:
      exposure:
        include: prometheus</p>
<pre><code>
- 당연히 gradle도 설정해줘야한다.</code></pre><pre><code>// string boot actuator, prometheus
implementation &quot;org.springframework.boot:spring-boot-starter-actuator&quot;
implementation &quot;io.micrometer:micrometer-registry-prometheus&quot;</code></pre><pre><code>- 최종 확인은 ${monitoring IP} : 9090으로 나중에 접속해서 확인한다.
- 중간 확인은 ${production IP}/actuator하고 ${production IP}/actuator/prometheus로 확인할수있다. 여러 매트릭 목록이 쫘악 출력될것이다.
### Prometheus.yml
- 파일 위치를 잘 결정해야한다.
  - 리눅스 파일위치
    - . : 현재 디렉토리
    - . / prometheus.yml : 현재 디렉토리의 파일
    - ~ : 루트 디렉토리였나...
- volumn : 원격에서 위치 : docker 컨테이너 안에 마운트 시킬 위치
- .env 파일 사용을 지원하지않는다.
- 주소는 배열안에 문자열 형식으로 넣어주어야한다.</code></pre><p>global:
  scrape_interval: 10s
  evaluation_interval: 10s
scrape_configs:</p>
<ul>
<li><p>job_name: &#39;novelit&#39;
metrics_path: /actuator/prometheus
static_configs:</p>
<ul>
<li>targets: [&#39;${production 서버 IP}:${spring boot 서버포트}&#39;]</li>
</ul>
</li>
<li><p>job_name: &#39;novelit-aws&#39;
metrics_path: /metrics
static_configs:</p>
<ul>
<li>targets: [&#39;${production 서버 IP}:${node-exporter port}&#39;]</li>
</ul>
</li>
</ul>
<pre><code>- global의 스크랩 주기는 알아서 설정하자.
- 여기서 스크랩할것은, 2가지이다.
  - spring boot 서버
  - ec2 서버
- prometheus.yml을 생성했으면, 업데이트를 먼저 해줘도 된다.</code></pre><p>docker pull prom/prometheus</p>
<pre><code></code></pre><p>docker-compose up -d</p>
<pre><code>- 프로메테우스는 해당 monitoring 서버의 포트로 들어가서
![](https://velog.velcdn.com/images/sil_tr/post/f1492377-eab6-473f-9863-0485076d8d95/image.png)
- 위와같이 표시되어야 정상이다.



## production 서버
- grafana를 설정하기전에, production 서버에 node-exporter를 설치해준다. 이건 대시보드 형태로 다른 다양한 형태가 많다. 그중에서도 node-exporter를 사용하는 것이다.
- docker-compose.yml</code></pre><p>  node-exporter:
    image: prom/node-exporter
    container_name: node-exporter
    env_file:
    - .env
    ports:
      - ${NODE_EXPORTER_BINDING_PORT}:${NODE_EXPORTER_PORT}
    networks:
      - backend</p>
<pre><code>- node-exporter 기본포트는 9100이긴하다.
- 설치해준다.</code></pre><p>docker pull prom/node-exporter</p>
<pre><code></code></pre><p>docker-compose up -d</p>
<p>```</p>
<ul>
<li>node-exporter확인은 해당 production IP의 node-exporter포트에서 확인하면된다.</li>
<li>여기도 메트릭이 쫘악 출력된다.</li>
</ul>
<h2 id="3-grafana-설정">3. Grafana 설정</h2>
<ul>
<li><p>docker-compose에 잘 적어놨다면 추가 할것이 딱히 없다.</p>
</li>
<li><p>그라파나는 접속해서 비밀번호를 재설정해주고, node-exporter를 사용하고 prometheus를 import해서 사용하기만하면된다.</p>
</li>
<li><p>granfana의 기본 포트는 3000이다.</p>
</li>
<li><p>monitoring server ip의 grafana 포트로 접속한다.</p>
</li>
<li><p>아이디는 docker-compose.yml에 설정한대로이고, 초기 비밀번호는 admin이다. 접속하고 비밀번호를 변경하면된다.</p>
</li>
<li><p>접속한후 대시보드로 들어가서 New-import로 들어가자
<img src="https://velog.velcdn.com/images/sil_tr/post/e62bc68f-1a1e-4d9f-99f5-49128e9c5ea2/image.png" alt=""></p>
</li>
<li><p>node-exporter의 대시보드 ID는 1860이다. load하자.
<img src="https://velog.velcdn.com/images/sil_tr/post/ce64b068-15b3-478a-aa7c-06e82b77045e/image.png" alt=""></p>
</li>
<li><p>로드한뒤에는 네이밍을 해주고,
<img src="https://velog.velcdn.com/images/sil_tr/post/3bc42824-97ec-4af4-9fb2-5b43126b6910/image.png" alt=""></p>
</li>
<li><p>프로메테우스 데이터 소스를 설정한다.
<img src="https://velog.velcdn.com/images/sil_tr/post/895b5bef-d116-4d31-b204-075a2f20e940/image.png" alt=""></p>
</li>
<li><p>이부분이 중요하다. 처음에 할때 이부분에서 헤멧다. 첫번째 블로그에 모니터링IP:프로메테우스포트 로 되어있는데, 모니터링 IP가 production IP가 아니라 Monitoring IP를 의미하는것이다.
<img src="https://velog.velcdn.com/images/sil_tr/post/43475d77-2999-4379-b53e-e01fc128a38c/image.png" alt=""></p>
</li>
<li><p>save&amp;test를 눌러주면 대시보드가 생성된다.</p>
</li>
<li><p>들어가서, 적용한 데이터소스로 전환해주면
<img src="https://velog.velcdn.com/images/sil_tr/post/bfd9222b-ec61-467d-bfbb-9b3e58268def/image.png" alt=""></p>
</li>
<li><p>그에따른 대시보드 현황이 표시된다.
<img src="https://velog.velcdn.com/images/sil_tr/post/183b445d-2679-420d-9bb6-8a183de64d1b/image.png" alt=""></p>
</li>
<li><p>너무 많이 생성된 데이터 소스는 connection - datasource에서 목록을 볼수있는데, 들어가서 삭제하면된다.
<img src="https://velog.velcdn.com/images/sil_tr/post/65e5ef4b-9aa0-4b50-8e67-a54b885dec37/image.png" alt=""></p>
</li>
<li><p>설정 끝.</p>
</li>
</ul>
<h3 id="참고블로그">참고블로그</h3>
<ul>
<li>기본틀은 여기서 참고했지만
<a href="https://dy-coding.tistory.com/entry/Prometheus-Grafana%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-EC2-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95">Prometheus-Grafana를-이용한-EC2-모니터링-환경-구축</a></li>
<li>이 블로그로 정확도를 잡는것이 좋다.
<a href="https://velog.io/@roycewon/Spring-boot-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81Prometheus-Grafana-docker">Spring boot 모니터링(Prometheus, Grafana, docker)</a></li>
</ul>
<h3 id="추신">추신</h3>
<ul>
<li>일단 느낀점은, ci/cd를 구축해놓지않으니 서버를 빌드하고 도커에 올리고 내려받고 다시 올리고 하는 과정이 너무 반복되다보니, 특히 도커 허브에 푸시할때 시간이 너무 오래걸린다. 아마 다음과정은 ci/cd를 구축하거나 간단히 github action등을 사용해봐야할것같다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] Stringbuilder]]></title>
            <link>https://velog.io/@sil_tr/java-Stringbuilder</link>
            <guid>https://velog.io/@sil_tr/java-Stringbuilder</guid>
            <pubDate>Sun, 24 Nov 2024 04:01:44 GMT</pubDate>
            <description><![CDATA[<h2 id="stringbuilderdeletestartindex-endindex--1">StringBuilder.delete(startIndex, endIndex + 1)</h2>
<ul>
<li>부분 문자열을 삭제하는 메소드다.</li>
<li>startIndex를 포함하며, endIndex를 포함하지않기때문에, +1한 인덱스를 넣어준다.</li>
<li>여기서 시험삼아서 돌려봤는데, endIndex는 문자열 길이를 초과해도, 에러를 발생시키지않는다. 이미 끝부분이 문자열의 length()까지로 저장되어있는거같다.<pre><code class="language-java">import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
</code></pre>
</li>
</ul>
<p>public class Main {
    static int R, C;
    static StringBuilder sb = new StringBuilder();</p>
<pre><code>public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    String str = &quot;1234567&quot;;

    for (int i = 0; i &lt; str.length(); i++) {
        sb.append(str.charAt(i));
    }

    sb.delete(6, 100000);

    System.out.println(sb);


    bw.flush();
    bw.close();
    br.close();
}</code></pre><p>}</p>
<pre><code>
- 결과
![](https://velog.velcdn.com/images/sil_tr/post/9209ae2e-a3d6-4ab6-b024-7bdbd8fa364c/image.png)

## +추가

- 그래서 혹시 상위메소드에 단서가있을까봐, 타고올라가봤다.
![](https://velog.velcdn.com/images/sil_tr/post/0271ca7e-fa0c-4807-b881-314ebc54a68f/image.png)
- StringBuilder는 AbstractStringBuilder를 상속받는것같다.
- AbstractStringBuilder의 delete메소드는 위와같이 구현되어있는데,
endIndex가 길이보다 클경우 이를 길이로 맞춰주고있다. 따라서 따로 신경쓸필요가 없었다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] AssertionFailedError : Actual Null]]></title>
            <link>https://velog.io/@sil_tr/Spring-AssertionFailedError-Actual-Null</link>
            <guid>https://velog.io/@sil_tr/Spring-AssertionFailedError-Actual-Null</guid>
            <pubDate>Sat, 16 Nov 2024 19:23:09 GMT</pubDate>
            <description><![CDATA[<p>서비스 테스트했을때 발생한 문제다.</p>
<ul>
<li><p>ProductServiceTest.class</p>
<pre><code class="language-java">@Test
  @DisplayName(&quot;제품 등록: 회원만&quot;)
  void addProductTest(){

      given(memberRepository.getReferenceById(sellerId)).willReturn(member);
      given(productRepository.save(any(Product.class))).willReturn(product);

      // when
      Long actual = productService.addProduct(name,price,quantity,sellerId);

      // then
      assertThat(actual).isEqualTo(product.getId());
  }</code></pre>
</li>
<li><p>ProductService.addProduct</p>
<pre><code class="language-java">@Transactional
  public Long addProduct(String name, Price price, Quantity quantity, Long sellerId) {
      Member seller = memberRepository.getReferenceById(sellerId);

      Product product = new Product(seller,name,price,quantity);

      productRepository.save(product);

      return product.getId();
  }</code></pre>
</li>
<li><p>다음과같은 상황에서 테스트를 실행하면 이런 에러가 뜬다</p>
<pre><code>org.opentest4j.AssertionFailedError: 
expected: 1L
but was: null
Expected :1L
Actual   :null</code></pre></li>
<li><p>이는 stub도 잘되어있고 뭐가 문제일까하며 찾아봤는데, 객체가 저장되었는데 null값이 반환된다면 id값이 없는게 문제였던걸로 가정되었다.</p>
<pre><code class="language-java">  @Id
  @Column(name = &quot;product_id&quot;)
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;</code></pre>
</li>
<li><p>@GeneratedValue는 테스트코드에서 실행되지않는다. 이는 Mock 객체는 실제 데이터베이스와 상호작용없이 동작하기 때문이다.</p>
</li>
<li><p>나는 이전에 ProductRepository를 @Mock로 등록해주었고, 실제 데이터베이스에 접근하지않기때문에 @GeneratedValue가 실행되지않는것이다.</p>
<pre><code class="language-java">Product product = new Product(1L, seller,name,price,quantity);</code></pre>
</li>
<li><p>따라서 그냥 테스트용으로 1L을 대입해주었다. 일치
<img src="https://velog.velcdn.com/images/sil_tr/post/c0ef30de-3334-41be-9ec5-3542b8bdd1d2/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BE] Java Spring]]></title>
            <link>https://velog.io/@sil_tr/JPA-Embedded</link>
            <guid>https://velog.io/@sil_tr/JPA-Embedded</guid>
            <pubDate>Sun, 03 Nov 2024 16:59:57 GMT</pubDate>
            <description><![CDATA[<h3 id="1-embedded-embeddable">1. @Embedded, @Embeddable</h3>
<ul>
<li>새로운 클래스 생성할필요 x<h3 id="2-record">2. record</h3>
</li>
<li>자동으로 getter 생성<h3 id="3-noargsconstructoraccess--accesslevelprotected">3. @NoArgsConstructor(access = AccessLevel.PROTECTED)</h3>
<h3 id="4-repository">4. @Repository</h3>
</li>
<li>JpaRepository를 extends할때는 굳이 적을 필요없긴함.<h3 id="5-patchmapping-vs-putmapping">5. @PatchMapping vs @PutMapping</h3>
</li>
<li>@PatchMapping : 원하는 정보만 수정</li>
<li>@PutMapping : 모두 수정<h3 id="6-mockmvc--controller-test">6. MockMvc : Controller test</h3>
</li>
<li>@WebMvcTest(AuthController.class)</li>
<li>404 error : @WebMvcTest(AuthControllerTest.class)<ul>
<li>테스트 클래스로 테스트를 돌림....</li>
</ul>
</li>
</ul>
<h3 id="7-test-code">7. Test code</h3>
<ul>
<li>AuthControllerTest.class<ul>
<li><a href="https://github.com/JungInLee0130/shopping_backend/blob/develop/marketapi/src/test/java/com/example/marketapi/auth/controller/AuthControllerTest.java">코드</a></li>
</ul>
</li>
</ul>
<ol>
<li>외부 API 테스트</li>
</ol>
<ul>
<li>MockMVC는 스프링부트 내에서 제작한 API를 제작하는 빈이다.
따라서 RestTemplate를 가져와서 테스트를 하였다.<pre><code class="language-java">MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);</code></pre>
</li>
<li>카카오 로그인의 url은 이렇다.
kauth~로 시작하는 authorization uri을 기본으로,</li>
</ul>
<ol>
<li>redirecturi</li>
<li>client_id</li>
<li>responsetype
세게의 파라미터를 쿼리스트링에 입력하여 auth/login~로 리다이렉트 시킨다.</li>
</ol>
<p>실제로 실행하면서 기능을 점검해도되는데, 나는 매번 실행시키는게 귀찮아서 테스트 코드로 작성하였다.
302 FOUND를 반환하고 redirecturi로 파싱되는지만 확인한다.</p>
<ol start="2">
<li><p>엑세스 토큰을 입력하면 클라이언트로 반환하는 코드다.
확인은 jsonpath를 사용해서 확인하였다.</p>
</li>
<li><p>로그아웃은 refreshtoken이 삭제되는지 확인하였다.
원격 레디스와 연결이 되어있고 이를 성공적으로 수행하였다.</p>
</li>
</ol>
<h2 id="jpa">JPA</h2>
<h3 id="fetchjoinfetchtypelazy">FetchJoin(FetchType.LAZY)</h3>
<ul>
<li>지연로딩 : 실제로 실행될때 데이터가 로딩.</li>
</ul>
<h2 id="converter">Converter</h2>
<ul>
<li>DB -&gt; spring</li>
<li>spring -&gt; DB</li>
<li>데이터 변환</li>
<li>AttributeConverter implements</li>
</ul>
<h3 id="영속성-컨텍스트">영속성 컨텍스트</h3>
<ul>
<li>querydsl로 추출한값을 수정하면 왜 저장을 하지않아도 반영이 되는가?</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[힙(heap)]]></title>
            <link>https://velog.io/@sil_tr/%ED%9E%99heap</link>
            <guid>https://velog.io/@sil_tr/%ED%9E%99heap</guid>
            <pubDate>Wed, 28 Aug 2024 06:38:24 GMT</pubDate>
            <description><![CDATA[<h2 id="힙">힙</h2>
<blockquote>
</blockquote>
<ul>
<li>기본 배열로 구성</li>
<li>편의를 위해 인덱스를 1부터 시작</li>
<li>기본 완전 이진 트리 형태를 사용</li>
<li>최대힙을 기준으로 설명</li>
</ul>
<h3 id="삽입">삽입</h3>
<ul>
<li><p>가장 마지막 인덱스 n + 1에 삽입</p>
</li>
<li><p>n + 1의 부모와 계속 비교해나가며, 값이 크면 (부모의 값이 작으면) 교환한다.</p>
<pre><code class="language-java">void insert(){
  maxHeap[++heapSize] = x;

  for (int i = heapSize; i &gt; 1; i /= 2){
      if (maxHeap[i / 2] &lt; maxHeap[i]){
          swap(i/2, i);
      }
      else{
          break;
      }
  }
}</code></pre>
</li>
</ul>
<h3 id="삭제">삭제</h3>
<ul>
<li><pre><code class="language-java">int deleteMaxHeap(){
  if (heapSize == 0){
      return 0;
  }

  // 1번째 인덱스가 root임.
  // 1. 루트 노드를 poll한다.
  int root = maxHeap[1]; 
  // 2. 마지막 노드의 요소를 루트에 삽입한다.
  maxHeap[1] = maxHeap[heapSize];
  // 3. 힙을 재배치한다.
  // 자식들과 대소관계를 비교하며 내려간다. (O(logN))
  for (int i = 1; i * 2 &lt;= heapSize;){
      // 3-1. 마지막 노드의 값이 두자식보다 모두 큰 경우 : 그냥 break;
      if (maxHeap[i] &gt; maxHeap[i * 2] &amp;&amp; maxHeap[i] &gt; maxHEap[i * 2 + 1]){
          break;
      }
      // 3-2. 왼쪽노드가 더 큰경우 : swap
      else if (maxHeap[i] &lt; maxHeap[i * 2]){
          swap(i, i * 2);
          i = i * 2; // 인덱스 왼쪽 자식으로
      }
      else if (maxHeap[i] &lt; maxHeap[i * 2 + 1]){
          swap(i, i * 2 + 1);
          i = i * 2 + 1;
      }
  }
}</code></pre>
<h3 id="참고-출처">참고, 출처</h3>
</li>
<li><p>github
<a href="https://github.com/gyoogle/tech-interview-for-developer/blob/master/Computer%20Science/Data%20Structure/Heap.md">tech-interview-for-developer : 자료구조 - 힙</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 9466. 텀프로젝트]]></title>
            <link>https://velog.io/@sil_tr/%EB%B0%B1%EC%A4%80-9466.-%ED%85%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@sil_tr/%EB%B0%B1%EC%A4%80-9466.-%ED%85%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Sat, 13 Apr 2024 15:51:22 GMT</pubDate>
            <description><![CDATA[<h2 id="1-재귀-체크">(1) 재귀 체크</h2>
<ul>
<li>그래프 문제는 아니다.</li>
<li>사이클을 찾는 문제다.</li>
<li>단독 사이클이 가능하므로, 이 원소와 연결되는 애들은 같은 팀이아니다.<pre><code class="language-java">import java.awt.*;
import java.io.*;
import java.math.BigInteger;
import java.util.*;
</code></pre>
</li>
</ul>
<p>public class Main {
    static int T;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));</p>
<pre><code>    T = Integer.parseInt(br.readLine());

    for (int t = 0; t &lt; T; t++) {
        int n = Integer.parseInt(br.readLine()); // 10만

        StringTokenizer st = new StringTokenizer(br.readLine()); // 선택한 번호, 1부터 시작

        selected = new int[n + 1]; // 정
        visited = new boolean[n + 1];
        result = 0;
        isChecked = new boolean[n + 1];

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


        for (int i = 1; i &lt;= n; i++) {
            if (!isChecked[i]) {
                dfs(i);
            }
        }

        System.out.println(n - result);
    }

    bw.flush();
    bw.close();
    br.close();
}
static int[] selected;
static int result;
private static void dfs(int x) {
    // 이미 체크함.
    if (isChecked[x]) return;

    // visited의 목적은 재방문 찾기. 사이클 찾기임.
    // 이문제의 특징은 무조건 사이클이 탄생한다는것.
    // 이미 방문한 곳을 만나면 -&gt; 사이클임.
    if (visited[x]){ // 이 조건문 덕분.
        isChecked[x] = true;
        result++; // 사이클일때만 다시돌아와서 ++.
    }
    // 방문하지않았으면
    visited[x] = true;
    dfs(selected[x]);
    // 여기다 result++ 넣으면 사이클 아닌것들도 다 걸림.
    isChecked[x] = true; // 사이클 아닌것들도 근데 검사 끝났으니까 ischecked.
    visited[x] = false; // 매번 초기화 해주면 시간초과 난다고함.
}
static boolean[] visited;
static boolean[] isChecked;</code></pre><p>}</p>
<p>```</p>
<ul>
<li><p>일단 처음접근은 모든 점마다 dfs를 돌렸는데, 당연히 시간초과가 났다.</p>
</li>
<li><p>기본틀은 dfs가 맞는데, 시간초과가 안나도록 조건문, 체크 배열을 추가할 필요가 있다.</p>
<blockquote>
<ul>
<li>체크배열 (isChecked)</li>
</ul>
</blockquote>
<ol>
<li>이미 검증이 끝난 원소들은 다시 반복해서 들어가지않는다.</li>
<li>즉, 예제의 2 -&gt; 1 -&gt; 3 -&gt; 3 으로 이루어진 것들도 단독사이클을 포함한 사이클(?) 이므로 체크해둔다.
(dfs 아래 isChecked문이 존재하는 이유이다. 이게 빠지면 시간초과난다.)</li>
<li>이 배열은 마지막에 true만 골라다가 count하게 셀수없다.</li>
<li>따라서 dfs안에 count문을 따로 두어 체크한다.<blockquote>
<ul>
<li>방문배열(visited)</li>
</ul>
</blockquote>
</li>
<li>평소에 쓰던 방문처리용보다 약간 특별한 의미를 가진다.</li>
<li>4-&gt;7-&gt;6-&gt;4 로 순환하는 사이클에서
(visited) -&gt; (visited) -&gt; (visited) -&gt; (4번째 visited)
4번째 visited를 통해 사이클을 판별하는 기능을 한다.
이후, return문 처리를 하지않고, 다음 dfs로 넘어가, isChecked처리를 하지않았으므로, 모두 isChecked처리를 해주면서 같은 팀 원소들을 count한다.</li>
</ol>
</li>
<li><p>즉, 시간을 줄이기위한 방법을 정리하면</p>
</li>
</ul>
<ol>
<li>visited</li>
<li>isChecked 
두가지이다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] String.equals와 "=="에 대해서]]></title>
            <link>https://velog.io/@sil_tr/Java-String.equals%EC%99%80-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@sil_tr/Java-String.equals%EC%99%80-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Thu, 11 Apr 2024 06:56:55 GMT</pubDate>
            <description><![CDATA[<h2 id="1-펠린드롬">(1) 펠린드롬?</h2>
<p><a href="https://www.acmicpc.net/problem/10942">https://www.acmicpc.net/problem/10942</a></p>
<p>String.equals()는 두 문자열의 <span style= "color:blue">내용이 같은지</span>를 확인하는 함수다.</p>
<pre><code class="language-java">str[j].equals(str[j + i])

System.out.println(str[1].equals(str[3])); // -&gt; True</code></pre>
<p>&quot;==&quot;는 두 문자열의 <span style= "color:red">주소가 같은지</span>를 확인하는 함수이다.</p>
<pre><code>str[i] == str[i + 1]

System.out.println(str[1] == str[3]); // 0 -&gt; False</code></pre><p>이 개념은 문자열 배열을 조건문에 사용할때 실수가 발생할수있다.</p>
<p>특히, <span style= "color:red">코드가 길어질경우 진짜 못찾는다.</span></p>
<p>올바른 예)</p>
<pre><code class="language-java">import java.awt.*;
import java.io.*;
import java.math.BigInteger;
import java.util.*;

public class Main {
    static int N, M;
    static boolean[][] dp;
    static String[] str;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        N = Integer.parseInt(br.readLine()); // 2000

        StringTokenizer st = new StringTokenizer(br.readLine());
        str = new String[N + 1]; // 1 ~ N
        str[0] = &quot;0&quot;;
        for (int i = 1; i &lt;= N; i++) {
            str[i] = st.nextToken();
        }

        M = Integer.parseInt(br.readLine()); // 100만개

        dp = new boolean[N + 1][N + 1];

        solve();

        for (int i = 0; i &lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());

            if (dp[start][end]) {
                bw.write(&quot;1\n&quot;);
            }
            else{
                bw.write(&quot;0\n&quot;);
            }
        }

        // 이게 dp지
        //System.out.println(str[1] == str[3]);
        //System.out.println(str[1].equals(str[3]));

        bw.flush();
        bw.close();
        br.close();
    }

    private static void solve() {
        // 1
        for (int i = 1; i &lt;= N; i++) {
            dp[i][i] = true;
        }

        // 2
        for (int i = 1; i &lt;= N - 1; i++) {
            if (str[i].equals(str[i + 1])) dp[i][i + 1] = true;
        }

        // 3
        for (int i = 2; i &lt; N; i++) { // i + 1 ~ n 길이 (인덱스 기준)
            for (int j = 1; j &lt;= (N - i); j++) { // 길이에따른 범위
                // 처음 == 끝 (양 끝점 비교)
                // 처음 + 1 ~ == 끝 - 1 ~ (저장된 dp 사용)
                if (str[j].equals(str[j + i]) &amp;&amp; dp[(j) + 1][(j + i) - 1]) {
                    dp[j][j + i] = true;
                }
            }
        }
    }
}</code></pre>
<p>잘못된 예)</p>
<pre><code class="language-java">import java.awt.*;
import java.io.*;
import java.math.BigInteger;
import java.util.*;

public class Main {
    static int N, M;
    static boolean[][] dp;
    static String[] str;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        N = Integer.parseInt(br.readLine()); // 2000

        StringTokenizer st = new StringTokenizer(br.readLine());
        str = new String[N + 1]; // 1 ~ N
        str[0] = &quot;0&quot;;
        for (int i = 1; i &lt;= N; i++) {
            str[i] = st.nextToken();
        }

        M = Integer.parseInt(br.readLine()); // 100만개

        dp = new boolean[N + 1][N + 1];

        solve();

        for (int i = 0; i &lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());

            if (dp[start][end]) {
                bw.write(&quot;1\n&quot;);
            }
            else{
                bw.write(&quot;0\n&quot;);
            }
        }

        // 이게 dp지
        //System.out.println(str[1] == str[3]);
        //System.out.println(str[1].equals(str[3]));

        bw.flush();
        bw.close();
        br.close();
    }

    private static void solve() {
        // 1
        for (int i = 1; i &lt;= N; i++) {
            dp[i][i] = true;
        }

        // 2
        for (int i = 1; i &lt;= N - 1; i++) {
            if (str[i] == str[i + 1]) dp[i][i + 1] = true;
        }

        // 3
        for (int i = 2; i &lt; N; i++) { // i + 1 ~ n 길이 (인덱스 기준)
            for (int j = 1; j &lt;= N - i; j++) { // 길이에따른 범위
                // 처음 == 끝 (양 끝점 비교)
                // 처음 + 1 ~ == 끝 - 1 ~ (저장된 dp 사용)
                if (str[j] == str[j + i] &amp;&amp; dp[j + 1][j + i - 1]) {
                    dp[j][j + i] = true;
                }
            }
        }
    }
}</code></pre>
<p>두 코드를 돌려보면 틀린 답이 나온다.</p>
<p>1) 답</p>
<pre><code>1
0
1
1</code></pre><p>2) 답</p>
<pre><code>0
0
1
0</code></pre><p>문제는 답이 맞았을때다.
아래 조건문중 하나만 이렇게 바꿔보자.</p>
<pre><code class="language-java">        // 2
        for (int i = 1; i &lt;= N - 1; i++) {
            if (str[i] == str[i + 1]) dp[i][i + 1] = true;
        }

        // 3
        for (int i = 2; i &lt; N; i++) { // i + 1 ~ n 길이 (인덱스 기준)
            for (int j = 1; j &lt;= N - i; j++) { // 길이에따른 범위
                // 처음 == 끝 (양 끝점 비교)
                // 처음 + 1 ~ == 끝 - 1 ~ (저장된 dp 사용)
                if (str[j].equals(str[j + i]) &amp;&amp; dp[j + 1][j + i - 1]) {
                    dp[j][j + i] = true;
                }
            }
        }</code></pre>
<p>차이점을 찾을 수 있겠는가?</p>
<p>이 코드는 심지어 답이 맞았는데도 oj에 돌려보면 틀렸습니다가 나온다. 이런경우 답찾기가 난감해진다.</p>
<p>결론은 뭐... 알고 쓰자이다.</p>
<h3 id="읽어보면-좋은-블로그">읽어보면 좋은 블로그</h3>
<p><a href="https://coding-factory.tistory.com/536">https://coding-factory.tistory.com/536</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[docker-compose에 mariadb 올리고 spring boot 연동하기]]></title>
            <link>https://velog.io/@sil_tr/docker-compose%EC%97%90-mariadb-%EC%98%AC%EB%A6%AC</link>
            <guid>https://velog.io/@sil_tr/docker-compose%EC%97%90-mariadb-%EC%98%AC%EB%A6%AC</guid>
            <pubDate>Tue, 09 Apr 2024 03:40:28 GMT</pubDate>
            <description><![CDATA[<h2 id="1-mariadb-설치">(1) mariadb 설치</h2>
<ul>
<li><p>포트만 3306으로 설정해주고 설치한다.</p>
</li>
<li><p>mysql이랑 겹치므로 삭제하고 설치한다.</p>
</li>
<li><p>aws 포트경로랑도 일치해서 다른 포트로 하면 어디까지 바꿔야할지 감이안온다. 그냥 mysql을 사용하면 테이블을 백업하고 삭제하자.
<img src="https://velog.velcdn.com/images/sil_tr/post/d76c973a-9d83-4fc0-8c9a-f192a940111f/image.png" alt=""></p>
</li>
<li><p>docker-compose.yml 설정을 해준다.</p>
<pre><code class="language-docker">version: &quot;3&quot;
</code></pre>
</li>
</ul>
<p>services:
  mariadb:
    container_name: mariadb
    image: mariadb:10
    ports:
      - ${MARIA_BINDING_PORT}:${MARIA_PORT}
    volumes:
      - ${MARIA_DEFAULT_CONFIG_PATH}:/etc/maria/conf.d
      - ${MARIA_DATA_PATH}:/var/lib/maria
      - ${MARIA_ENTRYPOINT_PATH}:/docker-entrypoint-initdb.d
    env_file:
    - .env
    environment:
      - TZ= &quot;Asia/Seoul&quot;
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_USER=${MARIADB_USER}
      - MARIADB_DATABASE=${MARIADB_DATABASE}
      - MARIADB_PASSWORD=${MARIADB_PASSWORD}
    networks:
      - backend
    restart: always</p>
<p>networks:
  backend:</p>
<pre><code>
- conf.d -&gt; my.cnf 만들고 붙여넣기</code></pre><p>[client]
default-character-set = utf8mb4</p>
<p>[mysql]
default-character-set = utf8mb4</p>
<p>[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci</p>
<pre><code>
- 초기설정 : initdb.d -&gt; create_table.sql과 load_data.sql 파일을 생성

- .env</code></pre><p>#####MARIADB
MARIA_BINDING_PORT=${인바운드 포트}
MARIA_PORT=${아웃바운드 포트}
MARIA_DEFAULT_CONFIG_PATH=./db/conf.d
MARIA_DATA_PATH=./db/data
MARIA_ENTRYPOINT_PATH=./db/initdb.d</p>
<p>MARIADB_HOST=${aws 탄력적IP}
MARIADB_PORT=${인바운드 포트}
MARIADB_ROOT_PASSWORD=${root password}
MARIADB_DATABASE=${database 이름}
MARIADB_USER=${사용자}
MARIADB_PASSWORD=${사용자 비밀번호}</p>
<pre><code>- 도커 업데이트</code></pre><p>docker-compose up -d</p>
<pre><code>

## spring boot mariadb 연결
- build.gradle</code></pre><p>implementation group: &#39;org.mariadb.jdbc&#39;, name: &#39;mariadb-java-client&#39;, version: &#39;2.4.1&#39;</p>
<pre><code>- application.yml
    - jpa 설정 고쳐줌.
    - database-platform, ddl-auto
    - 원격 DB에 올라가니까 update로 변경
    - create: 테이블이 있을시 지우고 다시 create
    - update: 변경분만 업데이트</code></pre><p>spring:
  jpa:
    database-platform: org.hibernate.dialect.MariaDBDialect 
    hibernate:
      ddl-auto: update  </p>
<pre><code>- mariadb 설정 추가</code></pre><p>###mariadb
spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: ${MARIA_URL}
    username: ${MARIA_USERNAME}
    password: ${MARIA_PASSWORD}</p>
<pre><code>- env.yml</code></pre><p>#MARIADB
MARIA_URL: jdbc:mariadb://${탄력적 IP주소}:${인바운드 포트}/${데이터베이스 이름}
MARIA_USERNAME: ${사용자}
MARIA_PASSWORD: ${비밀번호}</p>
<pre><code>
- import.sql 있으면 넣으면된다.

- 완성. 헤이디로 원격IP로 접속하면 잘된다.

### 참고 블로그
- docker-compose mariadb 설치
- 가장 잘되어있는 블로그
- https://int-i.github.io/sql/2020-12-31/mysql-docker-compose/



</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1699. 제곱수의 합]]></title>
            <link>https://velog.io/@sil_tr/%EB%B0%B1%EC%A4%80-1699.-%EC%A0%9C%EA%B3%B1%EC%88%98%EC%9D%98-%ED%95%A9</link>
            <guid>https://velog.io/@sil_tr/%EB%B0%B1%EC%A4%80-1699.-%EC%A0%9C%EA%B3%B1%EC%88%98%EC%9D%98-%ED%95%A9</guid>
            <pubDate>Mon, 08 Apr 2024 05:36:30 GMT</pubDate>
            <description><![CDATA[<h2 id="1-1699-제곱수의-합">(1) 1699. 제곱수의 합.</h2>
<ul>
<li><p>자연수 N은 그보다 작거나 같은 수들의 합으로 표현이 가능하다. 그중에서 최소 갯수를 사용해 표시하는 경우의 수를 구하시오.</p>
</li>
<li><p>네. 당연히 반복문 사용하면 풀릴줄알았습니다.</p>
<pre><code class="language-java">import java.awt.*;
import java.io.*;
import java.math.BigInteger;
import java.util.*;
</code></pre>
</li>
</ul>
<p>public class Main {
    static int N;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));</p>
<pre><code>    N = Integer.parseInt(br.readLine());

    int count = 0;
    while (N &gt; 0) { // 10만 이하
        // N보다 작거나 같은 제곱수 찾기
        int sqrtN = (int) Math.sqrt(N);
        //System.out.println(sqrtN);
        int powN = (int) Math.pow(sqrtN, 2);
        N -= powN;
        count++;
    }

    System.out.println(count);
}</code></pre><p>}</p>
<pre><code>
- 근데 생각해보니까, 꼭 자신보다 작은것중에 최대 제곱으로 빼는식으로, 내림차순으로 빼도,
- 중간에 그것보다 작은것들의 합으로 표현이 되는 경우가 있을거라고 생각했습니다.
- 예를 들어, 17 = 9^2 + 4^2 + 1^2 인경우가 된다고 해도,
6^2 + 5^2 가 있을지도 모른다고 생각했습니다. (두경우가 같은수라고 한다면)
- 근데 이런경우는, 반복문으로 풀수가 없습니다.
- 따라서 방법을 생각하고있는데, DP라고 하네요.
- 방법은 이렇습니다.</code></pre><p>dp[1] = 1    (1^2)
dp[4] = 1    (2^2)
dp[9] = 1     (3^2)</p>
<pre><code>- 제곱수들의 수들은 모두 1입니다.
따라서, dp[17]같은수의 경우</code></pre><p>dp[17] = dp[16] + 1 or dp[13] + 1 or dp[8] + 1
or dp[1] + 1</p>
<pre><code>- 로 표현이 가능합니다.
- 각각의 경우의 수를 구하면,</code></pre><ol>
<li>dp[17] = 2 (1^1 + dp[16])</li>
<li>dp[17] = 3 (2^2 + dp[13]) </li>
<li>dp[17] = 4 (3^3 + dp[8])</li>
<li>dp[17] = 2 (4^4 + 1)<pre><code>이중에서 최소 경우의수는 2입니다.
따라서 이걸 조건식으로 놔주면됩니다.
```java
for (int i = 1; i &lt;= N; i++){
 dp[i] = i;
 for (int j = 1; j * j &lt;= N; j++){
     if (dp[i] &gt; dp[i - j * j] + 1){ 
     // 더 작은 경우가 있다면 최신화한다.
         dp[i] = dp[i - j * j] + 1;
     }
 }
}</code></pre></li>
</ol>
<h3 id="최종-코드">최종 코드</h3>
<pre><code class="language-java">import java.awt.*;
import java.io.*;
import java.math.BigInteger;
import java.util.*;

public class Main {
    static int N;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

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

        int count = 0;
        // 또 dp야
        int[] dp = new int[N + 1];
        for (int i = 1; i &lt;= N; i++) {
            dp[i] = i;
            for (int j = 1; j * j &lt;= i; j++) {
                if (dp[i] &gt; dp[i - j * j] + 1) {
                    dp[i] = dp[i - j * j] + 1;
                }
            }
        }

        // dp[1] = 1;
        // dp[4] = 1;
        // dp[9] = 1; -&gt; 제곱수들은 모두 1개이다.
        // dp[8] = dp[7] + 1 or dp[4] + 1 중에 하나일 것이다.
        // N보다 작은 제곱수들로 빼준것 + 1 중에 더 작은게 있으면 그걸로 대체한다.


        System.out.println(dp[N]);
    }
}</code></pre>
<ul>
<li>네, 또 dp 입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[redis pub/sub로 알림구현하기]]></title>
            <link>https://velog.io/@sil_tr/redis-pubsub%EB%A1%9C-%EC%95%8C%EB%A6%BC%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sil_tr/redis-pubsub%EB%A1%9C-%EC%95%8C%EB%A6%BC%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 06 Apr 2024 07:01:06 GMT</pubDate>
            <description><![CDATA[<h2 id="novelit-복구-고도화">Novelit 복구, 고도화</h2>
<h2 id="1-redis-pubsub">(1) redis pub/sub</h2>
<p><img src="https://velog.velcdn.com/images/sil_tr/post/3de2c338-e6f0-4392-adaa-ef3164fa9d0f/image.png" alt=""></p>
<ul>
<li><p>기존에 SSE방식으로 전달하는 알림방식은, username에 해당하는 사람에게만 전송하는 방식으로 처리를 한다.</p>
</li>
<li><p>댓글을 작성하면 해당 구독자에게만 알림을 보낸다.</p>
</li>
<li><p>다만 둘이 양방향 소통은 아니고, publisher(Editor) -&gt; subscriber(Writer), 혹은 그 반대로 삼아 전달한다.</p>
</li>
<li><p>뭔가 진행방식은 양방향인데, 양방향으로 계속 소켓을 열어두기에는 리소스 소모가 조금 크다고 보았다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sil_tr/post/2a0d348b-1490-49d2-9eef-780f217bd8c9/image.png" alt=""></p>
<ul>
<li><p>처음엔 리소스 절감과 일방향 소통이라고 생각해 sse 하나만으로 상관이없겠다고 생각했다.</p>
</li>
<li><p>하지만 Editor가 댓글을쓰면 Writer에게 알림이 가야하고, Writer가 답글을 쓰면 Editor에게 알림이 가야한다.</p>
</li>
<li><p>양방향 소통 방식의 그림은 띄지만, 실시간 양방향은 아니다.</p>
</li>
<li><p>따라서 둘이 소통할수있는 하나의 channel을 둬서, 아키텍처를 생성하려고한다.</p>
</li>
</ul>
<h2 id="spring-boot에서-redis-pubsub-구현">spring boot에서 redis pub/sub 구현</h2>
<ul>
<li><p>Redis 설정은 이전장에서 설명을 했으므로, 이번엔 spring 쪽을 다루려고한다.</p>
</li>
<li><p>RedisConfig</p>
<pre><code class="language-java">package com.example.practice.redis.config;
</code></pre>
</li>
</ul>
<p>import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;</p>
<p>@Slf4j
@Configuration
@EnableRedisRepositories
public class RedisConfig { // spring &lt;-&gt; redis 연결설정</p>
<pre><code>@Value(&quot;${spring.data.redis.host}&quot;)
private String redisHost;
@Value(&quot;${spring.data.redis.port}&quot;)
private int redisPort;
@Value(&quot;${spring.data.redis.password}&quot;)
private String redisPassword;

//lettuce
@Bean
public RedisConnectionFactory redisConnectionFactory(){
    RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost,redisPort);
    config.setPassword(redisPassword);
    return new LettuceConnectionFactory(config);
}

// redisTemplate: default Value Type = JSON
/* If you want to use String Type, you can change ValueSerializer
/ to StringRedisSerializer or Use StringRedisTemplate*/
@Bean
public RedisTemplate&lt;?, ?&gt; redisTemplate(){
    RedisTemplate&lt;?, ?&gt; redisTemplate = new RedisTemplate&lt;&gt;();
    redisTemplate.setConnectionFactory(redisConnectionFactory()); // connection
    redisTemplate.setKeySerializer(new StringRedisSerializer()); // key
    // Java Object &lt;-&gt; JSON &lt;-&gt; String value
    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer&lt;&gt;(String.class));

    return redisTemplate;
}

/*
* Redis pub/sub 메시지 처리 Listener
* */
@Bean
public RedisMessageListenerContainer redisMessageListener(){
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(redisConnectionFactory());
    return container;
}</code></pre><p>}</p>
<pre><code>- RedisConfig에는 연결관련 설정을 해준다.
- RedisTemplate가 거의 핵심역할이라고 생각한다. 여기서 Redis의 &lt;key, value&gt; 형태를 java Object로 변환할수있게하고, 반대도 가능하게 해주는 설정을 가진다.

- RedisController
```java
package com.example.practice.redis.controller;

import com.example.practice.redis.dto.MessageDto;
import com.example.practice.redis.service.RedisPubService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping(&quot;/api/v1/redis/pubsub&quot;)
public class RedisPubSubController {
    private final RedisPubService redisSubscribeService;

    @PostMapping(&quot;/send&quot;)
    public void sendMessage(@RequestParam(required = true) String channel,
                            @RequestBody MessageDto message) {
        log.info(&quot;Redis Pub MSG Channel = {}&quot;, channel);
        redisSubscribeService.pubMsgChannel(channel, message);
    }

    @PostMapping(&quot;/cancel&quot;)
    public void cancelSubChannel(@RequestParam String channel) {
        redisSubscribeService.cancelSubChannel(channel);
    }
}
</code></pre><ul>
<li><p>실험할 api다. /send api를 사용하면 해당 채널에 메시지를 전송할수있다.</p>
</li>
<li><p>MessageDto</p>
<pre><code class="language-java">package com.example.practice.redis.dto;
</code></pre>
</li>
</ul>
<p>import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;</p>
<p>import java.io.Serializable;</p>
<p>@Getter
@NoArgsConstructor
public class MessageDto implements Serializable {
    private static final long serialVersionUID = 1L;</p>
<pre><code>private String message;
private String sender;
private String roomId; // 타겟 channel

@Builder
public MessageDto(String message, String sender, String roomId) {
    this.message = message;
    this.sender = sender;
    this.roomId = roomId;
}</code></pre><p>}</p>
<pre><code>- 이건 MessageDto의 형태.

### Publisher

- RedisPublisher
```java
package com.example.practice.redis.service;

import com.example.practice.redis.dto.MessageDto;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;

@Service
public class RedisPublisher {
    private final RedisTemplate&lt;String, Object&gt; template;

    public RedisPublisher(RedisTemplate&lt;String, Object&gt; template) {
        this.template = template;
    }

    /*
    *  특정 채널에 메시지를 전송한다.
    * */

    /*
     * Object Publish
     * */
    public void publish(ChannelTopic topic, MessageDto messageDto) {
        template.convertAndSend(topic.getTopic(), messageDto);
    }

    /*
     * String Publish
     * */
    public void publish(ChannelTopic topic, String data) {
        template.convertAndSend(topic.getTopic(), data);
    }
}</code></pre><ul>
<li><p>RedisTemplate를 사용해 메시지를 전송한다. 파라미터는 채널과 메세지이다.</p>
</li>
<li><p>RedisPubService</p>
<pre><code class="language-java">package com.example.practice.redis.service;
</code></pre>
</li>
</ul>
<p>import com.example.practice.redis.dto.MessageDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Service;</p>
<p>@Service
@RequiredArgsConstructor
@Slf4j
public class RedisPubService {
    private final RedisMessageListenerContainer redisMessageListenerContainer;
    private final RedisPublisher redisPublisher;</p>
<pre><code>// 각 Channel 별 Listener
private final RedisSubscribeListener redisSubscribeListener;

/*
 * Channel 별 Message 전송
 * @Param
 * */
public void pubMsgChannel(String channel, MessageDto messageDto) {
    // 1. 요청한 Channel을 구독
    redisMessageListenerContainer.addMessageListener(redisSubscribeListener,
            new ChannelTopic(channel));

    // 2. Message 전송
    redisPublisher.publish(new ChannelTopic(channel), messageDto);
}

/*
 * Channel 구독 취소
 * @Param channel
 * */
public void cancelSubChannel(String channel) {
    redisMessageListenerContainer.removeMessageListener(redisSubscribeListener);
}</code></pre><p>}</p>
<pre><code>- 서비스 단이다. 메세지 구독자들에게 모든 알림을 전송한다.

### Subscibe
- RedisSubscribeListener
```java
package com.example.practice.redis.service;

import com.example.practice.redis.dto.MessageDto;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class RedisSubscribeListener implements MessageListener {

    private final RedisTemplate&lt;String, Object&gt; template;
    private final ObjectMapper objectMapper;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        try {
            String publishMessage = template
                    .getStringSerializer().deserialize(message.getBody());

            MessageDto messageDto = objectMapper.readValue(publishMessage, MessageDto.class);

            log.info(&quot;Redis Subscribe Channel : &quot;, messageDto.getRoomId());
            log.info(&quot;Redis SUB Message : {}&quot;, publishMessage);

            // Return || Another Method Call (etc.sae to DB)
            // TODO
            /*
            * 여기 알림이 들어갈 예정이다.
            * 
            * */
        } catch (JsonProcessingException e) {
            log.error(e.getMessage());
        }
    }
}</code></pre><ul>
<li>publisher로 부터 받은 메세지를 String 형태로 deserialize 시킨다. </li>
<li>이후, DTO 형태에 맡게 넣어주고, log를 찍어 이를 확인한다.</li>
<li>추후에 여기는 알림을 send해줘야하기때문에 알림 메소드가 대신들어간다.</li>
</ul>
<h3 id="결과">결과</h3>
<ul>
<li>postman으로 api를 쏘면
<img src="https://velog.velcdn.com/images/sil_tr/post/89ed380e-6dee-4bef-b926-b175a1ce0f37/image.png" alt=""></li>
<li>spring에서는 submessage가 정상적으로 받아짐을 볼수있고,
<img src="https://velog.velcdn.com/images/sil_tr/post/118e991f-b67d-4fad-9665-13a276b41b69/image.png" alt=""></li>
<li>movaxterm에서 redis 채널 모니터링도 가능하다.
<img src="https://velog.velcdn.com/images/sil_tr/post/e56dc556-fecc-4666-b8f8-c98367d26db3/image.png" alt=""></li>
</ul>
<h3 id="이후">이후</h3>
<ul>
<li><p>redis를 일단 하나만 사용했는데, message가 많이 발생하는걸 대비해 clustering을 먼저 할 생각이다.</p>
</li>
<li><p>redis pub/sub는 데이터를 저장하지않는다. 하지만, 전체 알림확인을 위해 db에 데이터를 저장하긴해야하는데,</p>
</li>
<li><p>kafka를 사용해서 두개의 consumer를 만들생각이다. 하나는 db에 연결하고, 하나는 위와같은 알림 consumer.</p>
</li>
</ul>
<h3 id="참고-및-출처">참고 및 출처</h3>
<ul>
<li>spring boot에서 redis pub/sub 구현<ul>
<li><a href="https://lucas-owner.tistory.com/68">https://lucas-owner.tistory.com/68</a></li>
</ul>
</li>
<li>sse, redis pub/sub, kafka를 사용한 알림 고도화<ul>
<li><a href="https://velog.io/@xogml951/Server-Sent-EventsSSE-Redis-pubsub-Kafka%EB%A1%9C-%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0#3-redis-pubsub%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-sse-scale-out%EC%8B%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0">https://velog.io/@xogml951/Server-Sent-EventsSSE-Redis-pubsub-Kafka%EB%A1%9C-%EC%95%8C%EB%A6%BC-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0#3-redis-pubsub%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-sse-scale-out%EC%8B%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</a></li>
</ul>
</li>
</ul>
<p>좋은 코드 알려주시는 분들 항상 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로컬 spring boot에서 aws docker-compose redis 접속]]></title>
            <link>https://velog.io/@sil_tr/2024.04.06-TIL</link>
            <guid>https://velog.io/@sil_tr/2024.04.06-TIL</guid>
            <pubDate>Fri, 05 Apr 2024 18:44:55 GMT</pubDate>
            <description><![CDATA[<h2 id="1-로컬-spring-boot에서-aws-docker-compose-redis-접속">(1) 로컬 spring boot에서 aws docker-compose redis 접속</h2>
<ul>
<li><p>sse -&gt; redis pub/sub로의 전환기인데, 구현자체는 어렵지않은데 redis 설정이 되게 오래걸렸다.</p>
</li>
<li><p>단순히 따라하는것이 아닌, 어느정도 이해가 필요하다 보니 오래걸렸던것같다.</p>
</li>
<li><p>우선 설명하기전에, redis.conf 파일에 대해 알아보자.</p>
</li>
<li><p>redis.conf 파일은 로컬에서 redis를 다운 할경우 자동으로 생성되는 파일이라고 한다. </p>
</li>
<li><p>하지만 원격(aws)의 docker-compose에 굴리려면 redis.conf 파일이 존재하지않는다. 따라서, 직접 다운을 받아 넣어주도록하자.</p>
</li>
<li><p>나는 여기를 참고했다.
<a href="https://github.com/gingaminga/docker-redis/blob/main/redis.conf">https://github.com/gingaminga/docker-redis/blob/main/redis.conf</a></p>
</li>
<li><p>자신이 참고할 aws의 경로에 redis.conf 파일을 그대로 드래그해서 넣어주자.</p>
</li>
<li><p>그리고 경로를 정해 주어야한다.</p>
</li>
<li><p>docker-compose.yml을 수정하자.</p>
<pre><code class="language-docker">redis:
  container_name: redis
  image: redis:latest
  hostname: redis
  env_file:
  - .env
  ports:
    - ${REDIS_BINDING_PORT}:${REDIS_PORT}
  volumes:
    - ${REDIS_DATA_PATH}:/data
    - ${REDIS_DEFAULT_CONFIG_FILE}:/etc/redis/redis.conf
  command: redis-server /etc/redis/redis.conf
  restart: always</code></pre>
</li>
<li><p>volumes 부분에서 redis config file path를 추가해주었다.</p>
</li>
<li><p>이제 좌측 경로로 redis.conf에 접근하면된다.</p>
</li>
<li><p>redis.conf 파일을 넣어주지않을경우 폴더로 생성이된다. 드래그해서 넣어주자.</p>
</li>
<li><p>도커 컴포즈 업데이트 후, redis.conf 파일을 찾아 연다.
우리가 봐야할 부분은 2가지이다.</p>
</li>
</ul>
<ol>
<li>크롤링 봇들이 돌아다니기 때문에 먼저 password를 설정해야한다.</li>
</ol>
<ul>
<li>vi 편집기를 연후, &quot;/requirepass&quot;를 통해 password를 설정하는 부분을 찾아 비밀번호를 입력한다.
<img src="https://velog.velcdn.com/images/sil_tr/post/9106f0a3-e0b2-4eda-9ba7-07f4509fd2fe/image.png" alt=""><pre><code>  requirepass ${PASSWORD}</code></pre></li>
</ul>
<ol start="2">
<li>포트를 직접 바인딩 시켜야한다 &quot;/bind &quot;를 검색해 바인딩 부분을 찾아낸다.
<img src="https://velog.velcdn.com/images/sil_tr/post/b437deaf-1a03-4512-bedf-645db18bb8cc/image.png" alt=""></li>
</ol>
<ul>
<li><p>친절하게 설명도 나와있다. aws 보안규칙에서는 0.0.0.0/0으로 외부접근을 열어놨으니 여기도 열어야한다.</p>
</li>
<li><p>여기서 redis의 바인드 규칙은</p>
</li>
<li><p>127.0.0.1 ::1 -&gt; 이건 로컬 테스트 용이다. 즉 원격 네트워크에서 내부 테스트 용이라는 소리다. 외부에서 reids 접근이 불가능하다.</p>
</li>
<li><p>0.0.0.0 :: 1 -&gt; 모든 포트에서 접근이 가능하다. 이걸로 바꿔주자.</p>
</li>
<li><p>변경내용을 저장한후, 도커컴포즈를 업데이트 해주자.</p>
</li>
<li><p>이제 외부에서 접근이 가능하다. 스프링을 실행하고 관련 api를 돌려보면
<img src="https://velog.velcdn.com/images/sil_tr/post/4b51474c-7df4-41f9-81e3-b74102d5b778/image.png" alt=""></p>
</li>
<li><p>성공적으로 실행된다.</p>
</li>
</ul>
<h3 id="스프링-redis-설정">스프링 redis 설정</h3>
<ul>
<li>스프링 레디스 설정은 이렇게 하면된다.</li>
<li>application.yml
<img src="https://velog.velcdn.com/images/sil_tr/post/8bb80e31-0020-4d1f-b640-c64aab886759/image.png" alt=""></li>
<li>여기서 host에는 탄력적 ip주소가 들어간다. 나도 정확하게 썼다고 생각했는데, 틀렸었고, 이거랑 bind 만 계속 바꾸고있었다.....</li>
<li>이거 제대로 입력해주고, bind 설정만 제대로해도 접속이 될것이다.</li>
<li>redis pub/sub 구현은 다음장에서 다루도록 하겠다.</li>
</ul>
<h3 id="느낀점">느낀점</h3>
<ul>
<li>novelit 프로젝트에서도 느낀거지만, 설정이 가장 어렵다. 설정은 단순히 블로그 참고하기만해서 할수가 없었다. 찾아보는 과정에서 필요한걸 계속 찾아보고, 개념을 찾아보게된다. 시간은 많이 들어도, 꼭 해결해서 배워가는게 중요한것같다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>