<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>R+7</title>
    <link>https://prgmd.tistory.com/</link>
    <description>prgmd 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 9 Oct 2026 04:34:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>prgmd</managingEditor>
    <image>
      <title>R+7</title>
      <url>https://tistory1.daumcdn.net/tistory/8897984/attach/88f6e7b6eced4794ba95bca862ae5283</url>
      <link>https://prgmd.tistory.com</link>
    </image>
    <item>
      <title>[Programmers] 기둥과 보 설치</title>
      <link>https://prgmd.tistory.com/37</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n x n 격자 벽면에 기둥과 보를 설치하거나 삭제하면서, 매 작업 후 남아 있는 모든 구조물이 조건을 만족해야 하는 문제. 기둥은 바닥 위에 있거나, 아래에 기둥이 있거나, 보의 한쪽 끝 위에 있어야 한다. 보는 한쪽 끝이 기둥 위에 있거나, 양쪽 끝이 다른 보와 연결되어 있어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령은 &lt;code&gt;[x, y, a, b]&lt;/code&gt; 형태로 주어지고, &lt;code&gt;a&lt;/code&gt;는 기둥/보, &lt;code&gt;b&lt;/code&gt;는 설치/삭제를 의미한다. 최종적으로 남은 구조물을 x 오름차순, y 오름차순, 기둥 먼저 정렬해서 반환해야 한다. 조건 자체가 복잡해서, 설치와 삭제 각각의 경우를 미리 전부 따지려 하면 코드가 쉽게 꼬이는 구현 문제다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 명령을 그대로 따라가면서 모든 경우의 수를 직접 나누려고 했는데 (먼저 &lt;code&gt;b&lt;/code&gt;가 1이면 설치, 0이면 삭제로 나누고, 그 안에서 다시 &lt;code&gt;a&lt;/code&gt;가 1이면 보, 0이면 기둥으로 나누는 식...) 그러다 보니 조건이 괴랄할 정도로 많아진데다, 특히 삭제 쪽 구현은 주변 구조물이 같이 무너지는 상황을 고려해야 해서 고민하다 포기 선언을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한 조건 상, &lt;code&gt;build_frame&lt;/code&gt;의 길이가 최대 1000이고, 구조물 개수도 그 정도를 크게 벗어나지 않는데다, 그렇다면 매 명령마다 전체 구조물을 한 번씩 검사해도 충분히 감당 가능한 크기라는 점이 문제의 핵심. 따라서 명령을 일단 적용하고, 현재 남아 있는 모든 구조물이 유효한지만 검사해도 충분히 풀 수 있는 조건이고, 하나라도 조건을 어기면 방금 한 작업을 되돌리는 식으로 풀었다. 핵심은 &lt;b&gt;적용 -&amp;gt; 전체 검사 -&amp;gt; 실패하면 원복&lt;/b&gt;이다. 백트래킹에서 상태를 바꿨다가 되돌리는 방식과 거의 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 중요한 부분은 자료구조였다. 정답 코드에는 리스트에 구조물을 넣고 &lt;code&gt;[x, y, a] in answer&lt;/code&gt; 형태로 존재 여부를 확인했는데, AI가 추천해주기를 리스트에서 &lt;code&gt;in&lt;/code&gt;은 선형 탐색이기 때문에 (명령 수가 최대 1000이라 통과는 가능하지만) &lt;code&gt;set&lt;/code&gt;로 하는 편이 훨씬 자연스럽다고. 대신&lt;b&gt; 리스트는 set에 넣을 수 없으므로&lt;/b&gt; 구조물을 &lt;code&gt;(x, y, a)&lt;/code&gt; 튜플로 관리해야 한다는 점은 기억할 것.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def solution(n, build_frame):
    structures = set()

    def is_valid_one(x, y, a):
        if a == 0:
            return (
                y == 0
                or (x, y - 1, 0) in structures
                or (x - 1, y, 1) in structures
                or (x, y, 1) in structures
            )

        return (
            (x, y - 1, 0) in structures
            or (x + 1, y - 1, 0) in structures
            or ((x - 1, y, 1) in structures and (x + 1, y, 1) in structures)
        )

    def is_valid():
        return all(is_valid_one(x, y, a) for x, y, a in structures)

    for x, y, a, b in build_frame:
        item = (x, y, a)

        if b == 1:
            structures.add(item)
            if not is_valid():
                structures.remove(item)
        else:
            structures.remove(item)
            if not is_valid():
                structures.add(item)

    return sorted([list(item) for item in structures])&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/문제</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/37</guid>
      <comments>https://prgmd.tistory.com/37#entry37comment</comments>
      <pubDate>Mon, 31 Aug 2026 17:28:46 +0900</pubDate>
    </item>
    <item>
      <title>[Programmers] n^2 배열 자르기</title>
      <link>https://prgmd.tistory.com/30</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/87390&quot;&gt;링크 (문제 설명이 말로 하기가 까다로워서, 직접 문제에 들어가 GIF를 보는 것이 이해가 더 빠르다)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n x n 크기의 2차원 배열을 특정 규칙에 따라 채운 뒤, 이를 1차원 배열로 펼쳤을 때 &lt;code&gt;left&lt;/code&gt;부터 &lt;code&gt;right&lt;/code&gt;까지의 구간만 반환하는 문제. n은 최대 10^7까지 주어지고, &lt;code&gt;right - left&lt;/code&gt;는 10^5 미만으로 제한된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 2차원 배열을 직접 만들어야 할 것 같지만, n^2 크기의 배열을 만드는 순간 시간과 메모리 양쪽에서 바로 터지는 문제로, 결국 전체 배열을 만드는 것이 아니라 특정 인덱스가 원래 2차원 배열의 몇 행 몇 열인지 계산해서 값을 바로 구해야 하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 처음에는 '설마 바로 값을 찍어야 하는 문제는 아니겠지'라고 생각했는데, 그게 정답이었다. 제한 조건을 보면 애초에 리스트를 만든 다음에 자르는 행위가 불가능한데, n이 최대 10^7이기 때문에 n x n 배열은 최대 10^14칸이기 때문. 핵심은 1차원 인덱스를 다시 2차원 좌표로 바꾸는 것으로, &lt;b&gt;어떤 인덱스 &lt;code&gt;i&lt;/code&gt;가 있을 때, 행은 &lt;code&gt;i // n&lt;/code&gt;, 열은 &lt;code&gt;i % n&lt;/code&gt;으로 구할 수 있고, &lt;/b&gt;문제에서 만들어지는 배열을 직접 찍다보면 어떻게든 규칙이 나오기는 나오는데, 그저 행과 열 값 중에 더 큰 값을 넣으면 된다는 조건. (다만 이 문제에서는 0, 0부터 시작하는 게 아니다 보니 각각 row와 col에 1을 더해줬다) 제한 조건만 보고 빠르게 전체를 만들 생각은 저버리고 필요한 부분만 수식 계산이 되는지 의심해 봐야 했던 문제.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;def solution(n, left, right):
    answer = []

    for i in range(left, right + 1):
        row = i // n + 1
        col = i % n + 1
        answer.append(max(row, col))

    return answer&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/문제</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/30</guid>
      <comments>https://prgmd.tistory.com/30#entry30comment</comments>
      <pubDate>Fri, 14 Aug 2026 12:44:34 +0900</pubDate>
    </item>
    <item>
      <title>[Programmers] 고고학 최고의 발견</title>
      <link>https://prgmd.tistory.com/24</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N x N 격자의 시계들(0:12시, 1:3시, 2:6시, 3:9시)이 상하좌우 연동되어 돌 때, 모든 시계를 12시(0)로 맞추기 위한 최소 조작 횟수를 구하는 문제. 시계 하나를 돌리면 본인 및 상하좌우 시계가 함께 90도(1칸)씩 돌아가고, 0(12시), 1(3시), 2(6시), 3(9시) 이며, 4번에 1바퀴 회전. 제한 조건은 N이 2 이상 8 이하.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀다가 막혔다. 처음에는 주변 시계가 0이 아닌 곳을 찾아 돌리는 탐욕적(Greedy) 방식을 사용했지만, 너무 많은 경우의 수가 존재하는데다 애초에 돌려서 상하좌우가 0이 아닌 상태가 되더라도 전체를 맞추기 위해 돌려야 하는 경우가 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 푸는데는 정석적인 방법으로 라이츠 아웃(Lights Out) 퍼즐의 해결 방법을 도입해야 한다. &lt;b&gt;첫 번째 행(Row 0)이 결정되면 나머지 행의 조작이 강제되는 로직&lt;/b&gt;이 중요한 키. 첫 번째 행을 완전탐색해서 돌린 다음 2~N까지 행은 윗 행에 맞춰 빠르게 계산한 다음 마지막 행이 0이 되는지만 체크하고, 만약 조건을 만족한다면 결과값과 비교해 더 작은 값으로 갱신하는 방식으로 해결이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;from itertools import product

def solution(clockHands):
    answer = 1e9
    n = len(clockHands)
    d = [(-1,0), (1,0), (0,0), (0,-1), (0,1)]

    for first in product(range(4), repeat = n):
        clock = []
        cnt = sum(first)

        for clock_row in clockHands:
            one_row = []
            for clock_num in clock_row:
                one_row.append(clock_num)
            clock.append(one_row)

        for i, f in enumerate(first):
            y, x = 0, i
            for dy, dx in d:
                ny, nx = y+dy, x+dx
                if 0 &amp;lt;= ny &amp;lt; n and 0 &amp;lt;= nx &amp;lt; n:
                    clock[ny][nx] = (clock[ny][nx]+f)%4

        for i in range(1, n):
            ex_row = clock[i-1]
            for j, k in enumerate(ex_row):
                if k != 0:
                    m = 4-clock[i-1][j]
                    cnt += m
                    for dy, dx in d:
                        ny, nx = i+dy, j+dx
                        if 0 &amp;lt;= ny &amp;lt; n and 0 &amp;lt;= nx &amp;lt; n:
                            clock[ny][nx] = (clock[ny][nx]+m)%4

        if set(clock[-1]) == {0}:
            answer = min(answer, cnt)
            print(answer, cnt)

    return answer&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/문제</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/24</guid>
      <comments>https://prgmd.tistory.com/24#entry24comment</comments>
      <pubDate>Tue, 4 Aug 2026 09:07:19 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 차근차근 CS (10) - Cookie, HttpOnly, LocalStorage</title>
      <link>https://prgmd.tistory.com/18</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKWqh9%2FdJMcabZfp3g%2Ff4kTkwLeV7kmlnFfiSjJlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;442&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Learned&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;user-select: auto !important;&quot; data-path-to-node=&quot;0&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Cookie&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;웹 브라우저가 사용자 컴퓨터에 저장하는 최대 4KB 크기의 작은 텍스트 데이터 파일. 서버가 HTTP 응답 헤더(Set-Cookie)를 통해 브라우저에 데이터를 전달하면 브라우저는 이를 저장해 뒀다 이후 해당 도메인으로 보내는 모든 HTTP 요청 헤더에 자동 포함해 서버로 다시 전송.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;대체로 사용자 로그인 상태 유지(세션 관리), 웹사이트 개인화 설정 저장, 사용자 트래킹 등에 사용한다. 그래서 '쿠키 수집 및 사용 동의' 팝업창이 뜨는 사이트가 많은 것도 이러한 이유. (주로 어떤 페이지에 오래 머무는지, 어떤 상품을 클릭했는지 기록을 수집한다) 다만 매 요청마다 서버로 전송되기 때문에 불필요한 네트워크 트래픽을 유발할 수 있고, JavaScript로 접근이 가능해 XSS 공격 등에 취약할 수 있다. 이러한 이유에서 HttpOnly 옵션을 설정한다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HttpOnly&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;쿠키를 서버에서 생성할 때 설정 가능한 보안 옵션(Flag). 적용 시 JavaScript의 document.cookie API를 통한 접근 및 차단이 원천 봉쇄된다. 이 쿠키는 오직 브라우저와 서버 간 HTTP(S) 통신에만 자동 첨부되어 전송되며, 만약 해커가 XSS 공격을 성공시키더라도 JavaScript로 쿠키를 읽을 수 없기 때문에 민감한 데이터가 탈취되는 것을 방지한다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;user-select: auto !important;&quot; data-path-to-node=&quot;8&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;LocalStorage&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;HTML5에 추가된 Web Storage API의 한 종류. 브라우저 내 데이터를 키-값(Key-Value) 쌍으로 영구 저장하는 메커니즘이다. JavaScript를 통해서만 데이터를 읽고 쓸 수 있고, 쿠키와 달리 HTTP 요청 시 서버로 자동 전송되지 않는다. 도메인당 약 5MB~10MB 까지 저장이 가능하기 때문에 쿠키보다 훨씬 넉넉하고, 만료 기한 역시 존재하지 않는다. 사용자가 브라우저 캐시나 데이터를 직접 지우거나 JavaScript 코드로 삭제하지 않는 한 재부팅해도 데이터가 유지된다. 서버로 전송할 필요가 없는 UI 설정(다크 모드 여부), 임시 저장 데이터, 자동 완성 히스토리 등을 보관할 때 활용된다.&lt;/p&gt;
&lt;hr style=&quot;user-select: auto !important;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Listened to&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/UpzrxsFCpVA?si=_86n9ddmnOnW6Oyv&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>HTTP</category>
      <category>httpOnly</category>
      <category>til</category>
      <category>XSS</category>
      <category>세션</category>
      <category>쿠키</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/18</guid>
      <comments>https://prgmd.tistory.com/18#entry18comment</comments>
      <pubDate>Mon, 27 Jul 2026 08:02:35 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 차근차근 CS (9) - Auth와 OAuth</title>
      <link>https://prgmd.tistory.com/17</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKWqh9%2FdJMcabZfp3g%2Ff4kTkwLeV7kmlnFfiSjJlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;442&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Learned&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size20&quot;&gt;인증(Authentication)과 인가(Authorization)&lt;/h4&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;개발에서 'Auth'는 철자가 비슷한 인증(Authentication)과 인가(Authorization) 두 가지 개념을 통칭. 시스템 보안을 구성하는 가장 기초적인 두 축이다. 인증은 '사용자가 누구인지 증명하고 확인하는 절차'고, 인가는 그 인증된 사용자가 '특정 자원에 접근, 조작 가능한지 검증하는 절차'다. 쉽게 말하면 &quot;너 누구..?&quot;랑 &quot;너 뭐 돼?&quot;다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;인증은 항상 먼저 진행되고, 인가는 인증이 완료된 후 진행된다. 실패 시 HTTP 상태 코드는 각각 401 Unauthorized (미인증 상태)와 403 Forbidden&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;user-select: auto !important;&quot;&gt;&amp;nbsp;&lt;/span&gt;(인증은 되었으나 권한 없음)이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size20&quot;&gt;OAuth(Open Authorization)&lt;/h4&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;OAuth는 사용자가 자신의 비밀번호를 직접 제공하지 않고도 외부 애플리케이션(클라이언트)이 사용자를 대신해 특정 서비스(구글, 카카오, 네이버 등)에 저장된 데이터나 기능에 안전하게 접근할 수 있도록 허용하는 표준 인가 프로토콜이다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;과거 특정 앱에서 사용자의 구글 캘린더 일정을 불러오려면 사용자가 해당 앱에 직접 구글 아이디와 비밀번호를 알려줘야 했다. 매우 위험한 방식. 다만 OAuth는 사용자 비밀번호를 외부 앱에 절대 공유하지 않는다. 대신 인증 서버(구글 등)가 사용자에게 동의를 받은 후 외부 앱에 제한된 권한만 가진 액세스 토큰(Access Token)을 발급해 준다.&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size18&quot;&gt;핵심 구성 요소&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;Resource Owner (자원 소유자):&lt;span style=&quot;user-select: auto !important;&quot;&gt;&amp;nbsp;&lt;/span&gt;사용자 본인&lt;/li&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;Client (클라이언트):&lt;span style=&quot;user-select: auto !important;&quot;&gt; &lt;/span&gt;제3자 애플리케이션 (개발 중인 웹/앱 서비스)&lt;/li&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;Authorization Server (인증 서버):&lt;span style=&quot;user-select: auto !important;&quot;&gt;&amp;nbsp;&lt;/span&gt;사용자 신원을 확인하고 동의를 받아 클라이언트에게 토큰을 발급해 주는 서버 (예: 구글 로그인 서버)&lt;/li&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;Resource Server (자원 서버):&lt;span style=&quot;user-select: auto !important;&quot;&gt;&amp;nbsp;&lt;/span&gt;사용자의 실제 데이터가 저장되어 있고, 토큰을 검증해 데이터를 제공하는 API 서버. (예: 구글 캘린더 API 서버)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size18&quot;&gt;동작 핵심 원리&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;사용자가 우리 서비스(Client)에서 '구글로 로그인하기' 버튼 클릭&lt;/li&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;구글(Authorization Server)로 이동해 로그인 &amp;rarr; &quot;이 앱이 귀하 프로필 정보에 접근하도록 허용하시겠습니까?&quot; 동의 창 승인&lt;/li&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;&lt;span style=&quot;user-select: auto !important;&quot;&gt;&lt;/span&gt;구글이 우리 서비스 서버로 임시 암호인 &lt;b&gt;'인가&lt;/b&gt; &lt;b&gt;코드&lt;/b&gt;' 전달.&lt;/li&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;우리 서비스의 서버가 이 인가 코드를 구글 인증 서버로 보내고, 실제 통신에 쓸&amp;nbsp;&lt;b&gt;AccessToken&lt;/b&gt;과&amp;nbsp;&lt;b&gt;RefreshToken&lt;/b&gt;을 발급&lt;/li&gt;
&lt;li style=&quot;user-select: auto !important;&quot;&gt;발급받은 AccessToken을 활용해 구글 자원 서버에서 사용자의 기본 정보(이메일, 이름)를 가져와 우리 서비스의 회원으로 인증을 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size14&quot;&gt;※ RefreshToken은 보안상 유효기간이 짧을 수 밖에 없는 AccessToken을 사용자가 일일이 다시 로그인할 필요 없이, 백그라운드에서 조용히 새로 재발급해주는 토큰. 오직 새로운 AccessToken 발급용으로만 쓰이며 (인증 API) AccessToken의 수명을 15분 정도로 설정하더라도 2주~1달 정도 수명을 가진 RefreshToken이 존재하기 때문에 사용자는 로그아웃을 겪지 않음. 그 기간 동안은 만료되더라도 조용히 재발급을 해준다.&amp;nbsp;&lt;/p&gt;
&lt;hr style=&quot;user-select: auto !important;&quot; contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Listened to&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;user-select: auto !important;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/5wth9BScmGY?si=0jfTmjiMLuTyt0lM&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Auth</category>
      <category>HTTP</category>
      <category>OAuth</category>
      <category>til</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/17</guid>
      <comments>https://prgmd.tistory.com/17#entry17comment</comments>
      <pubDate>Fri, 24 Jul 2026 14:25:57 +0900</pubDate>
    </item>
    <item>
      <title>[Programmers] 징검다리</title>
      <link>https://prgmd.tistory.com/16</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출발 지점부터 도착 지점까지 놓인 바위 중 n개를 제거해 각 지점 사이 거리 최솟값 중 가장 큰 값을 구하는 문제. 도착 지점까지의 거리 (distance)는 1부터 10억. 바위 개수는 5만 개로 조건이 주어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 바위가 [2, 14, 11, 21, 17] 로 주어지고 n이 2라고 가정하면, [2, 14]를 제거했을 때 각 바위 사이 거리는 [11, 6, 4, 4]가 되고, 거리의 최솟값은 4가 된다. 다른 값과 비교했을 때 이 값이 제일 크므로 답은 4.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 이분 탐색인가..? 생각이 가장 먼저 들었고, 어떻게 접근해야 문제를 풀 수 있을지 한참 고민. 도착 지점까지의 거리가 10억이라는 부분에서 힌트를 얻었어야 하는 문제였다. 애초에 바위 개수가 5만 개인 상황에서 n개를 전부 빼보는 조합을 쓴다면 무조건 시간 초과가 발생하는 상황이고, 이 때 탐색 대상을 '바위를 n개 제거했을 때 최소 거리가 얼마인지'가 아닌 '최소 거리가 x일 때 제거해야 하는 바위 수가 n개 이하인지'로 접근해야 풀 수 있던 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이분 탐색 문제는 가장 까다로운 부분이 조건 설정으로 보이는데... 개인적으로 난항을 겪었던 부분을 몇 가지 꼽자면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오랜만에 풀다 보니 &lt;code&gt;high&lt;/code&gt;와 &lt;code&gt;low&lt;/code&gt; 값을 갱신할 때 각각 &lt;code&gt;mid-1&lt;/code&gt;, &lt;code&gt;mid+1&lt;/code&gt;로 설정해줘야 값이 유의미하게 변한다는 점을 까먹었고...&lt;/li&gt;
&lt;li&gt;&lt;code&gt;while&lt;/code&gt; 문 탈출 조건 부분에서 거듭 헷갈린 점 (등호를 붙일지 말지)&lt;/li&gt;
&lt;li&gt;또한 이 문제의 경우 &lt;code&gt;answer&lt;/code&gt;를 계속해서 값을 갱신해줘야 했는데, &lt;code&gt;cnt&lt;/code&gt;가 &lt;code&gt;n&lt;/code&gt;보다 작을 때마다 잡아주면서 최대값을 구했어야 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분 탐색은 계속 풀어보면서 감을 익히는 게 중요하다. 아자아자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;def solution(distance, rocks, n):
    answer = 0    
    low, high = 0, distance
    rocks.sort()

    while low &amp;lt;= high:
        mid = (low + high)//2
        curr, cnt = 0, 0

        for rock in rocks:
            if rock - curr &amp;gt;= mid:
                curr = rock
                continue
            cnt += 1

        if distance - curr &amp;lt; mid:
            cnt += 1

        if cnt &amp;gt; n:
            high = mid-1
        elif cnt &amp;lt;= n:
            answer = mid
            low = mid+1

    return answer&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/문제</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/16</guid>
      <comments>https://prgmd.tistory.com/16#entry16comment</comments>
      <pubDate>Thu, 23 Jul 2026 17:04:32 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 차근차근 Java (1) - Java와 Spring</title>
      <link>https://prgmd.tistory.com/15</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKWqh9%2FdJMcabZfp3g%2Ff4kTkwLeV7kmlnFfiSjJlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;442&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Learned&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Java와 JVM&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Java&lt;/b&gt;는 프로그래밍 언어. _한 번 작성하면 어디서나 실행된다(Write Once, Run Anywhere)_는 철학을 기반으로 만들어짐. 코드를 컴파일하면 기계어가 아니라 &lt;b&gt;바이트코드&lt;/b&gt;로 변환되고, 이 바이트코드를 &lt;b&gt;JVM(Java Virtual Machine)&lt;/b&gt;이 해석해서 실행하는 구조다. 그래서 Windows에서 짠 Java 코드가 Linux 서버에서도 그대로 돌아간다. 이 부분은 상당히 편리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프레임워크&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크는 애플리케이션의 뼈대(구조)를 미리 짜놓고, 개발자에게 그 빈칸(비즈니스 로직)만 채우게 하는 방식의 도구다. 내가 필요할 때 불러다 쓰는 라이브러리와 다르다. 프레임워크는 본인이 흐름을 쥐고 있고 내 코드를 필요할 때만 불러주는 식이다. 전자는 내가 주인이라면 후자는 프레임워크가 주인. 이는 Java에서 매우 중요한 개념인 &lt;b&gt;제어의 역전(IoC, Inversion of Control)&lt;/b&gt; 이라고 부르는데, 스프링의 가장 핵심적인 설계 철학이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IoC&lt;/b&gt;란, 객체를 누가 생성하고 관리하느냐의 주도권이 '내 코드'에서 '프레임워크'로 넘어가는 것을 의미한다. 그중 &lt;b&gt;DI(Dependency Injection, 의존성 주입)&lt;/b&gt;는 IoC를 구현하는 구체적인 방법 중 하나다. 객체가 필요한 다른 객체(의존성)를 자기가 직접 new로 만들지 않고, 외부(스프링)로부터 주입받는 것을 말한다. 비유하자면 DI 없이 만든다면 내가 직접 특정 원두를 사와서 커피를 내리는 방식(new)인 거고, DI를 적용한다면 나는 '원두'라는 규격만 요청해 두고 오늘 브라질산이 올지 케냐산이 올지 결정하는 것은 스프링에게 맡겨 공급받는 방식이라 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// DI 없이 &amp;mdash; 내가 직접 만듦, BlockService가 BlockRepository 구현체를 강하게 알아야 함
public class BlockService {
    private BlockRepository repository = new BlockRepositoryImpl();
}

// DI 적용 &amp;mdash; 스프링이 만들어서 넣어줌, BlockService는 &quot;어떤 구현체인지&quot; 몰라도 됨
@Service
public class BlockService {
    private final BlockRepository repository;

    public BlockService(BlockRepository repository) {  // 생성자로 주입받음
        this.repository = repository;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ final: 한 번 값이 대입되면 다시는 바꿀 수 없는 변수 선언.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스프링 컨테이너 (IoC 컨테이너)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 구성하는 객체(Bean)의 생성, 구성(설정), 조립, 생명주기 관리를 애플리케이션 코드 대신 수행하는 소프트웨어 컴포넌트다. 스프링 프레임워크의 핵심이며, &lt;code&gt;org.springframework.context.ApplicationContext&lt;/code&gt; 인터페이스(및 그 상위인 &lt;code&gt;BeanFactory&lt;/code&gt;)로 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;빈 (Bean)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이 직접 생성하고 생명주기를 관리하는 객체. &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, &lt;code&gt;@Controller&lt;/code&gt;, &lt;code&gt;@Component&lt;/code&gt; 같은 어노테이션이 붙은 클래스는 스프링이 알아서 객체로 만들어 자기 창고(&lt;b&gt;IoC 컨테이너&lt;/b&gt;)에 등록한다. 이후 다른 곳에서 그 타입이 필요하면 스프링이 창고에서 꺼내 주입해주는 시스템.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들자면 &lt;code&gt;BackendApplication.java&lt;/code&gt;의 &lt;code&gt;@SpringBootApplication&lt;/code&gt;에 포함된 &lt;code&gt;@ComponentScan&lt;/code&gt;이 바로 &quot;이 패키지 밑을 다 뒤져서 빈 후보(어노테이션 붙은 클래스)를 찾아 등록해&quot;라는 지시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring과 Spring Boot의 차이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 Java로 웹 애플리케이션을 만들기 위한 기반 프레임워크로, IoC 컨테이너, DI, AOP 등을 제공한다. 원래는 설정(XML)이 굉장히 많고 복잡했다고. Spring Boot는 Spring 위에 얹은 설정 자동화 도구다. 이런 의존성이 있으면 이런 설정이 필요하겠지... 를 추론해 자동으로 세팅해주는 역할이다. Spring Boot 덕분에 톰캣 서버 설정이나 DB 커넥션풀 설정 같은 걸 손으로 일일이 작업하지 않고 의존성만 추가하면 알아서 동작한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;※&amp;nbsp;Tomcat: 아파치에서 개발한 오픈 소스 자바 서블릿 컨테이너이자, WAS. Java로 작성된 웹 애플리케이션을 서버 상에서 실행하고 관리해 주는 소프트웨어다. 일반 웹 서버가 정적 콘텐츠를 주로 다룬다면 Tomcat은 동적 콘텐츠 처리가 주 목적이며, 자체적으로 정적 콘텐츠를 처리하는 웹 서버 기능도 내장하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;어노테이션(Annotation)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@으로 시작하는 표식. 코드에 이 클래스, 메서드는 이런 역할이다라고 메타데이터를 붙이는 Java 문법이다. 어노테이션 자체는 로직이 없고, 스프링(또는 다른 프레임워크)이 실행 시점에 이 표식을 읽고 동작을 결정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@SpringBootApplication&lt;/code&gt; = 이 클래스는 앱의 시작점이다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RestController&lt;/code&gt; = 이 클래스는 HTTP 요청을 받아 JSON으로 응답하는 클래스다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Service&lt;/code&gt; = 이 클래스는 비즈니스 로직 담당이다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Repository&lt;/code&gt; = 이 클래스는 DB 접근 담당이다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Entity&lt;/code&gt; = 이 클래스는 DB 테이블 한 줄(row)과 매핑된다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt; = 이 필드에 스프링이 Bean을 넣는다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@GetMapping&lt;/code&gt;, &lt;code&gt;@PostMapping&lt;/code&gt; = 이 메서드가 어떤 HTTP 요청(GET/POST)에 응답할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Gradle&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 소스코드를 컴파일하고, 필요한 외부 라이브러리(의존성)를 내려받고, 테스트를 돌리고, 실행 가능한 파일(jar)로 묶는 과정을 자동화하는 도구. npm의 &lt;code&gt;package.json&lt;/code&gt;, Python의 &lt;code&gt;requirements.txt&lt;/code&gt;와 같은 역할이라 보면 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradlew(Gradle Wrapper)는 팀원마다 빌드 도구인 Gradle 자체의 버전이 달라서 생기는 문제를 막기 위해 올바른 빌드 도구 버전을 찾아주는 부트스트래퍼(Bootstrapper).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;계층형 아키텍처 (MVC)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 웹 앱은 보통 역할을 층으로 나눈다. 이렇게 나누는 이유는 &lt;b&gt;관심사 분리&lt;/b&gt; 때문. 각 부분이 자기 역할만 책임지게 해서, 한 부분을 고쳐도 다른 부분에 영향이 안 가게 하기 위함이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[클라이언트] &amp;rarr; Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; Entity &amp;harr; DB
                  &amp;uarr;            &amp;uarr;           &amp;uarr;           &amp;uarr;
              요청/응답만    비즈니스     DB 접근만    테이블과
              담당, 로직X    로직 담당    (JPA가 대부분  1:1 매핑
                                          자동 구현)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 각 계층이 무엇을 의미하는지는 (당연히) 알아둬야 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;계층&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HTTP 요청의 진입점. URL과 메서드를 매핑하고, 요청/응답 형식만 다룸&lt;/td&gt;
&lt;td&gt;식당의 홀 직원. 주문만 받고 요리는 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Service&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;실제 비즈니스 로직(&quot;이 예산이 팀 총액인지 1인 금액인지 계산&quot; 같은 것)&lt;/td&gt;
&lt;td&gt;주방장. 실제 요리(로직) 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Repository&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DB에 데이터를 저장/조회하는 인터페이스. Spring Data JPA를 쓰면 인터페이스만 선언해도 구현체를 스프링이 자동 생성&lt;/td&gt;
&lt;td&gt;창고 관리인. 재료(데이터) 입출고만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Entity&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DB 테이블 하나에 대응하는 Java 클래스. @Entity 붙이면 JPA가 테이블과 매핑&lt;/td&gt;
&lt;td&gt;재료 명세서 양식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DTO&lt;/b&gt; (Data Transfer Object)&lt;/td&gt;
&lt;td&gt;계층 간, 또는 클라이언트-서버 간 주고받는 데이터 형태를 정의한 클래스. Entity를 그대로 노출하지 않기 위해 씀&lt;/td&gt;
&lt;td&gt;포장 용기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ORM / JPA&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM은 Object-Relational Mapping이라는 뜻으로 SQL문을 직접 안 쓰고, Java 객체를 다루는 것만으로 DB를 조작하게 해주는 기술이다. &lt;b&gt;JPA&lt;/b&gt;는 Java의 ORM 표준 규격(인터페이스)이고, &lt;b&gt;Hibernate&lt;/b&gt;가 그 규격의 실제 구현체. JPA라는 표준 명세를 실제로 구현한 자바 ORM 프레임워크라고 보면된다. 살짝 새로운 용어가 많이 등장해서 헷갈릴 수 있는데, Java의 객체지향 프로그래밍과 RDB 사이 패러다임 불일치를 해결하기 위해 객체와 DB 테이블을 자동으로 매핑해 주는 기술이자, 객체-관계 매핑 프레임워크라 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr style=&quot;user-select: auto !important;&quot; contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Listened to&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/twLhSqabby0?si=RAfbVV1FaR9evbSd&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/15</guid>
      <comments>https://prgmd.tistory.com/15#entry15comment</comments>
      <pubDate>Thu, 23 Jul 2026 09:03:15 +0900</pubDate>
    </item>
    <item>
      <title>알고리즘 트레이닝 목차</title>
      <link>https://prgmd.tistory.com/14</link>
      <description>&lt;p&gt;앞으로 알고리즘 관련 문제 풀이 모음 + 개념 포스팅을 한눈에 찾기 위한 메인 인덱스 페이지. 새 글 작성할 때마다 이 곳에 링크와 간단 팁을 업데이트할 예정이다.&lt;/p&gt;
&lt;h3&gt;1. 개념 정리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://prgmd.tistory.com/11&quot;&gt;그래프 문제 해결 방안&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 문제 풀이&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;No.&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;문제명&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;주요 알고리즘&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;바로가기&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;001&lt;/td&gt;
&lt;td&gt;징검다리&lt;/td&gt;
&lt;td&gt;이분 탐색&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://prgmd.tistory.com/16&quot;&gt;보러가기&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;002&lt;/td&gt;
&lt;td&gt;고고학 최고의 발견&lt;/td&gt;
&lt;td&gt;완전 탐색&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://prgmd.tistory.com/24&quot;&gt;보러가기&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;</description>
      <category>Algorithm</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/14</guid>
      <comments>https://prgmd.tistory.com/14#entry14comment</comments>
      <pubDate>Wed, 22 Jul 2026 13:50:03 +0900</pubDate>
    </item>
    <item>
      <title>[Python] 그래프 문제, 어떻게 접근할 것인가</title>
      <link>https://prgmd.tistory.com/11</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;분기 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 문제 중 그래프가 나왔을 때, 우리가 생각해 볼 접근법을 설정한다면 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 이동: &lt;u&gt;최단 거리&lt;/u&gt;나 &lt;u&gt;최소 비용&lt;/u&gt;을 구하는가?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간선 비용이 모두 같음(또는 비용 없음): &lt;b&gt;BFS&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;간선 비용이 다름
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모두 양수: &lt;b&gt;다익스트라&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;음수 포함됨: &lt;b&gt;벨만-포드&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;한 지점이 아니라 '모든 지점 간'의 거리: &lt;b&gt;플로이드-워셜&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 연결: 모든 노드를 &lt;u&gt;하나로 묶거나 그룹화&lt;/u&gt;하는가?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최소 비용으로 전체를 하나로 연결: &lt;b&gt;MST (크루스칼, 프림)&lt;/b&gt; &lt;i&gt;필수 조건:&lt;/i&gt; 무방향 그래프이어야 하며, 사이클이 없어야 함&lt;/li&gt;
&lt;li&gt;같은 그룹/네트워크인지 확인하거나 두 그룹을 합침: &lt;b&gt;유니온-파인드 &lt;/b&gt;&lt;i&gt;(얘는 여기 뿐 아니라 여러 군데 사용됨)&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 작업의 &lt;u&gt;선후 관계&lt;/u&gt;가 있거나 &lt;u&gt;사이클&lt;/u&gt;을 찾아야 하는가?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순서 정하기, 선수 과목, 선행 조건: &lt;b&gt;위상 정렬&lt;/b&gt; &lt;i&gt;필수 조건:&lt;/i&gt; 방향 그래프이어야 하며, 사이클이 절대 없어야 함 (DAG)&lt;/li&gt;
&lt;li&gt;사이클(순환) 존재 여부 판별:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무방향 그래프: &lt;b&gt;유니온-파인드&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;방향 그래프: &lt;b&gt;DFS&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;알고리즘 별 설명&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DFS&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;graph = []
visited = []
def dfs(now):
    # 1. 현재 노드 방문 처리
    visited[now] = True

    # 2. 현재 노드와 인접한 노드를 확인
    for nxt in graph[now]:
        # 3. 아직 방문하지 않은 노드라면 더 깊이 들어감 (재귀)
        if not visited[nxt]:
            dfs(nxt)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;백트래킹&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def backtracking(depth, path):
    # 1. 종료 조건 (정답을 찾은 경우)
    if depth == target_depth:
        print(path)
        return

    # 2. 현재 단계에서 선택 가능한 후보들 탐색
    for candidate in candidates:
        # 3. 유망성 검사 (Constraint Check)
        if is_promising(candidate):
            # 4. 상태 변경 (선택)
            visited[candidate] = True
            path.append(candidate)

            # 5. 다음 단계로 재귀 호출
            backtracking(depth + 1, path)

            # 6. 복구 (이게 백트래킹의 핵심!)
            path.pop()
            visited[candidate] = False&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;플로이드 워셜&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 정점에서 모든 정점으로의 최단 경로를 구하는 알고리즘. 거쳐가는 정점을 k로 설정하고, 이를 기준으로 기존 최단 거리와 비교해 갱신하는 것이 핵심이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;V = 4
INF = int(1e9)

# 2차원 리스트(최단 거리 테이블)를 만들고, 모든 값을 무한으로 초기화
# 인덱스 편의를 위해 V + 1 크기로 할당
graph = [[INF] * (V + 1) for _ in range(V + 1)]

# 1. 자기 자신에서 자기 자신으로 가는 비용은 0으로 초기화
for a in range(1, V + 1):
    for b in range(1, V + 1):
        if a == b:
            graph[a][b] = 0

# 2. 주어지는 간선 정보(가중치)로 초기화
# (중략)

# 3. 플로이드-워셜 알고리즘 수행 (핵심 로직)
# k: 거쳐가는 노드, i: 출발 노드, j: 도착 노드
for k in range(1, V + 1):
    for i in range(1, V + 1):
        for j in range(1, V + 1):
            graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j])&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;BFS&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 정점으로부터 가까운 정점을 먼저 방문하는 순회. 가중치가 없는 그래프에서 최단 경로 찾을 경우 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;deque&lt;/code&gt; 사용과 방문 처리가 필수. 시간 복잡도는 모든 정점과 간선을 한 번씩 확인하므로 O(V+E)&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;from collections import deque

def bfs(start):
    q = deque()
    q.append(start)
    visited = set()
    visited.add(start_node)

    while q:
        now = queue.popleft()        
        for nxt in graph[now]:
            if nxt not in visited:
                q.append(nxt)
                visited.add(nxt)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;플러드 필&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최단 거리가 목적이 아니라 영역의 개수나 크기를 구하는 유형 (아래는 개수 구하는 유형)&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;    count = 0
    while q:
        now = queue.popleft()        
        count += 1 # 한 칸씩 갈 때마다 카운트 증가
        for nxt in graph[now]:
            if nxt not in visited:
                q.append(nxt)
                visited.add(nxt)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;차원 확장&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;층을 구분하는 경우도 존재 - &lt;a href=&quot;https://www.acmicpc.net/problem/2206&quot;&gt;2206 벽 부수고 이동하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;층을 나누다 못해 아예 차원을 비트마스킹으로 다루는 문제도 있다 - &lt;a href=&quot;https://www.acmicpc.net/problem/1194&quot;&gt;1194 달이 차오른다, 가자&lt;/a&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 비트마스킹 비교
if not (keys &amp;amp; (1 &amp;lt;&amp;lt; bit)):&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가중치&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가중치가 0 또는 1인 경우 구분이 필요하다면 &amp;rarr; 가중치가 0인 노드는 앞으로, 가중치가 1인 노드는 뒤로 보내는 식. &lt;code&gt;popleft()&lt;/code&gt;와 &lt;code&gt;pop()&lt;/code&gt;이 동시에 가능하다는 점을 이용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;여러 지점에서 동시에 번져나가는 상황&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BFS를 시작하기 전, 모든 시작점 좌표를 미리 큐에 전부 넣고 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시작점과 도착점이 정해져 있을 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시작점과 도착점에서 동시에 출발하는 &lt;b&gt;양방향 BFS&lt;/b&gt; - &lt;a href=&quot;https://www.acmicpc.net/problem/1525&quot;&gt;1525 퍼즐&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;경우의 수가 무지하게 많을 때 연산량을 줄이기 위해 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;경로 복원이 필요할 경우 (&lt;b&gt;역추적&lt;/b&gt;)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나를 여기로 보낸 직전 노드(parent)가 누군지 기록하는 배열을 만듦 (경로 리스트를 통째로 보내면 리스트 복사 비용 때문에 메모리와 시간이 초과될 가능성이 높음) - &lt;a href=&quot;https://www.acmicpc.net/problem/13913&quot;&gt;13913 숨바꼭질 4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변환&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2차원 배열을 문자열이나 정수 하나로 압축해 관리하는 기법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2차원 좌표 &lt;code&gt;y, x&lt;/code&gt; &amp;rarr; &lt;code&gt;i = y*cols + x&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;다익스트라&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 출발 정점에서 다른 모든 정점까지의 최단 거리를 구하는 알고리즘. 매번 가장 비용이 적은 노드를 선택해 인접 노드의 최단 거리를 갱신하는 Greedy 방식을 사용한다. 우선순위 큐(&lt;code&gt;heapq&lt;/code&gt;)를 활용하고, 모든 간선 가중치가 양수일 때 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import heapq

# 노드의 개수(V)와 간선의 개수(E)
V = 6
INF = int(1e9) # 무한을 의미하는 값으로 10억 설정

# 각 노드에 연결되어 있는 노드에 대한 정보를 담는 리스트 만들기
graph = [[] for i in range(V + 1)]

# 최단 거리 테이블을 모두 무한으로 초기화
distance = [INF] * (V + 1)

# 간선 정보 입력 예시 (출발 노드, 도착 노드, 가중치)
# graph[1].append((2, 2))  # 1번 노드에서 2번 노드로 가는 비용이 2
# graph[1].append((3, 5))
# graph[1].append((4, 1))

def dijkstra(start):
    q = []
    # 시작 노드로 가기 위한 최단 경로는 0으로 설정하여 큐에 삽입
    # heapq는 튜플의 첫 번째 원소를 기준으로 최소 힙을 구성하므로 (거리, 노드) 순서로 삽입
    heapq.heappush(q, (0, start))
    distance[start] = 0

    while q:
        # 가장 최단 거리가 짧은 노드에 대한 정보 꺼내기
        dist, now = heapq.heappop(q)

        # 현재 큐에서 꺼낸 거리값이 테이블에 기록된 값보다 크다면 이미 처리된 적이 있는 노드이므로 무시
        if distance[now] &amp;lt; dist:
            continue

        # 현재 노드와 연결된 다른 인접한 노드들을 확인
        for i in graph[now]:
            cost = dist + i[1]

            # 현재 노드를 거쳐서, 다른 노드로 이동하는 거리가 더 짧은 경우
            if cost &amp;lt; distance[i[0]]:
                distance[i[0]] = cost
                heapq.heappush(q, (cost, i[0]))

# 1번 노드에서 시작
# dijkstra(1)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;벨만-포드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다익스트라와 동일하지만 간선 가중치가 음수일 때도 사용이 가능하다. 매 단계마다 그래프의 모든 간선을 전부 확인해 최단 거리를 갱신하는 특징이 있으며, 이를 통해 음수 사이클 존재 여부를 판별할 수 있다. (이 경우 예외 처리) 아무래도 매 라운드마다 모든 간선을 확인하기 때문에 시간 복잡도가 높은 편.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 노드의 개수(V)와 간선의 개수(E)
V = 5
E = 6
INF = int(1e9) # 무한을 의미하는 값으로 10억 설정

# 모든 간선에 대한 정보를 담는 리스트 만들기
edges = []
# 최단 거리 테이블을 모두 무한으로 초기화
distance = [INF] * (V + 1)

# 간선 정보 입력 예시 (출발 노드, 도착 노드, 가중치)
# edges.append((1, 2, -1)) 
# edges.append((1, 3, 4))
# ... (문제 조건에 맞게 입력받아 세팅)

def bellman_ford(start):
    # 시작 노드 초기화
    distance[start] = 0

    # 전체 V - 1번의 라운드(round)를 반복, 마지막 V번째는 음수 사이클 확인용
    for i in range(V):
        # 매 반복마다 모든 간선을 확인
        for j in range(E):
            cur_node = edges[j][0]
            next_node = edges[j][1]
            edge_cost = edges[j][2]

            # 현재 간선을 거쳐서 다른 노드로 이동하는 거리가 더 짧은 경우
            if distance[cur_node] != INF and distance[next_node] &amp;gt; distance[cur_node] + edge_cost:
                distance[next_node] = distance[cur_node] + edge_cost

                # V번째 라운드에서도 값이 갱신된다면 음수 사이클이 존재한다는 의미
                if i == V - 1:
                    return True # 음수 사이클 존재

    return False # 정상 종료 (음수 사이클 없음)

# 1번 노드에서 시작
# has_negative_cycle = bellman_ford(1)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;유니온 파인드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 노드가 존재할 경우 두 노드가 서로 같은 집합에 속해 있는지 판별하거나, 두 그래프를 하나로 병합하는 서로소 집합 알고리즘. 특정 노드가 속한 집합의 대표 노드를 찾고(find), 이를 합함(union). 경로 압축이 필수적이다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;# 노드의 개수(V)
V = 6

# 부모 테이블 초기화 (처음에는 모든 노드가 자기 자신을 부모로 가짐)
parent = [0] * (V + 1)
for i in range(1, V + 1):
        parent[i] = i

# Find 연산: 특정 원소가 속한 집합(루트 노드)을 찾기
def find_parent(parent, x):
        # 자기 자신이 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
        if parent[x] != x:
                # 경로 압축(Path Compression) 기법: 찾은 루트 노드를 현재 노드의 부모로 바로 갱신
                parent[x] = find_parent(parent, parent[x])
        return parent[x]

# Union 연산: 두 원소가 속한 집합을 합치기
def union_parent(parent, a, b):
        a = find_parent(parent, a)
        b = find_parent(parent, b)

        # 번호가 더 작은 노드를 부모로 설정 (일반적인 갱신 규칙)
        if a &amp;lt; b:
                parent[b] = a
        else:
                parent[a] = b

# 연산 수행 예시
# union_parent(parent, 1, 4)
# union_parent(parent, 2, 3)
# union_parent(parent, 2, 4)

# 1번 노드와 3번 노드가 같은 집합인지 확인
# if find_parent(parent, 1) == find_parent(parent, 3):
#     print(&quot;같은 집합입니다.&quot;)
# else:
#     print(&quot;다른 집합입니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MST (최소 신장 트리)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 내 모든 정점을 연결하는 부분 그래프 중 사용된 간선 가중치 합이 가장 작은 트리. 조건 상 &lt;b&gt;모든 노드가 연결되어 있어야 하며, 사이클이 없어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간선이 적은 희소 그래프의 경우 전체 간선을 비용 순 오름차순 정리 후 유니온-파인드로 사이클을 피해가며 간선을 선택하는 &lt;b&gt;크루스칼&lt;/b&gt;이 있고, 간선이 많은 밀집 그래프의 경우 임의 시작 정점에서 출발해 현재 트리와 인접한 간선 중 우선순위 큐를 이용해 최소 비용 간선을 선택 확장하는 &lt;b&gt;프림&lt;/b&gt; 기법이 존재.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;크루스칼&lt;/h4&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;import sys

# 유니온 파인드: 경로 압축(Path Compression) 적용
def find(x):
    if parent[x] != x:
        parent[x] = find(parent[x])
    return parent[x]

def union(a, b):
    root_a = find(parent, a)
    root_b = find(parent, b)
    if root_a != root_b:
        parent[root_b] = root_a
        return True
    return False

def kruskal(v, edges):
    # 1. 간선을 가중치 기준으로 오름차순 정렬
    edges.sort(key=lambda x: x[2])

    parent = [i for i in range(v + 1)]
    mst_weight = 0
    edges_count = 0

    for u, v, weight in edges:
        # 2. 사이클이 생기지 않는다면 간선 선택
        if union(u, v):
            mst_weight += weight
            edges_count += 1
            # 정점 - 1개의 간선을 찾으면 종료 (최적화)
            if edges_count == v - 1:
                break

    return mst_weight&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프림&lt;/h4&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;import heapq

def prim(start_node, v, adj):
    visited = [False] * (v + 1)
    # (가중치, 정점) 형태로 우선순위 큐 관리
    pq = [(0, start_node)]
    mst_weight = 0
    count = 0

    while pq:
        weight, curr = heapq.heappop(pq)

        # 이미 방문한 정점이면 무시 (사이클 방지)
        if visited[curr]:
            continue

        visited[curr] = True
        mst_weight += weight
        count += 1

        if count == v: # 모든 정점을 방문했다면 종료
            break

        # 인접한 정점 중 방문하지 않은 곳을 큐에 삽입
        for next_node, next_weight in adj[curr]:
            if not visited[next_node]:
                heapq.heappush(pq, (next_weight, next_node))

    return mst_weight&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/개념</category>
      <category>BFS</category>
      <category>dfs</category>
      <category>MST</category>
      <category>그래프</category>
      <category>다익스트라</category>
      <category>백트래킹</category>
      <category>알고리즘</category>
      <category>크루스칼</category>
      <category>프림</category>
      <category>플로이드워셜</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/11</guid>
      <comments>https://prgmd.tistory.com/11#entry11comment</comments>
      <pubDate>Fri, 17 Jul 2026 23:30:33 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 차근차근 CS (7) - 웹소켓</title>
      <link>https://prgmd.tistory.com/10</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKWqh9/dJMcabZfp3g/f4kTkwLeV7kmlnFfiSjJlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKWqh9%2FdJMcabZfp3g%2Ff4kTkwLeV7kmlnFfiSjJlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;442&quot; data-filename=&quot;Gemini_Generated_Image_n3rykkn3rykkn3ry.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Learned&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹소켓(WebSocket)이란 하나의 TCP 접속 위에서 클라이언트와 서버 간 지속적인 양방향(Full Duplex) 실시간 통신을 가능케 하는 네트워크 기술이다. 양방향이기 때문에 클라이언트 측의 요청(request)이 따로 없어도 서버가 데이터를 보낼 수 있고, 한 번 연결이 성립되면 명시적으로 연결을 끊기 전까지 실시간으로 연결이 유지된다(stateful). 연결을 설정하는 초기 단계에만 HTTP 통신을 사용하고 그 후로는 데이터를 프레임 단위로 교환하기 때문에 오버헤드를 방지할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;※ 왜 HTTP는 무거운가?: 무상태성(stateless)을 지향하기 때문에 매 요청마다 필수 메타 데이터를 헤더에 통째로 담아 보내야 하기 때문에 담기는 정보가 매우 많고, 이를 매 통신마다 TCP 핸드셰이크를 반복적으로 해가며 보내야 하기 때문(단방향). 따라서 웹 페이지 로딩이나 이미지 요청 같이 단발성 작업에 최적화된 모델의 경우에 사용. 또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;Cache-Control,&lt;span&gt;&amp;nbsp;&lt;/span&gt;ETag&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 헤더를 통해 CDN 레벨에서 캐싱 인프라를 제공하기 때문에 로딩 속도를 극적으로 단축시킬 수도 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동작 원리는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;오프닝 핸드셰이크&lt;/b&gt;: 클라이언트가 표준 HTTP 요청을 서버로 보냄. 헤더에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Upgrade: websocket,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Connection: Upgrade&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 포함해 서버에게 웹소켓 통신 전환을 요청.&lt;/li&gt;
&lt;li&gt;서버가 해당 요청을 지원하고 수락시 HTTP&lt;span&gt;&amp;nbsp;&lt;/span&gt;101 Switching Protocols&lt;span&gt;&amp;nbsp;&lt;/span&gt;상태 코드로 응답.&lt;/li&gt;
&lt;li&gt;HTTP 프로토콜이 웹소켓 프로토콜로 전환.&lt;span&gt;&amp;nbsp;&lt;/span&gt;ws://&lt;span&gt;&amp;nbsp;&lt;/span&gt;(암호화된 버전은&lt;span&gt;&amp;nbsp;&lt;/span&gt;wss://) 스키마를 사용해 양방향 통신이 시작됨.&lt;/li&gt;
&lt;li&gt;통신 완료시 클라이언트나 서버 둘 중 하나가 종료(close) 프레임 전송. 연결 닫음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹소켓의 용도는 주로 실시간 채팅, 라이브 스트리밍, 실시간 데이터 피드 등이 있다. 실시간 채팅의 경우, 각 사용자가 채팅 서버와 단일 웹소켓 연결을 맺고 이를 유지하면서 이용하는 시스템. 만약 10,000명이 채팅창에 접속해 있다면 10,000개의 세션(웹소켓 통로)이 동시에 열린 상태가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자 A가 메시지를 입력해 서버로 전송(push)하면, 서버가 해당 채팅방에 연결되어 있는 다른 사용자 B, C들의 웹소켓 세션을 식별한다. 이후 바로 사용자 A의 요청 없이도 메시지를 브로드캐스팅. 이 때 중요한 건, 상대방이 메시지를 보냈는지 확인을 위해 주기적으로 서버에 묻는 Polling 과정을 없애고, 상대방이 입력 중, 읽음 상태까지 지연 없이 양방향으로 교환할 수 있게 해준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;라이브 스트리밍의 경우 UDP 기반 WebRTC를 주로 사용하는 편이고, 실시간 상호작용 처리를 위해 웹소켓을 병행 연결하는 편이다. 사용자가 입력한 라이브 채팅, 실시간 시청자수 증감, 후원 알림, 좋아요 버튼 등 이벤트를 전체 시청자 화면에 동기화한다. WebRTC 시그널링의 경우 WebRTC 환경에서 P2P 연결을 맺기 전, Peer 간 통신 규격이나 네트워크 경로 정보(SDP, ICE Candidate 등)를 교환하는 초기 통로로 사용한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Today I Listend to&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/A_51KAhJ-c8?si=1iz3vPW1161FMgsu&quot; width=&quot;560&quot; height=&quot;315&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>HTTP</category>
      <category>til</category>
      <category>UDP</category>
      <category>websocket</category>
      <author>prgmd</author>
      <guid isPermaLink="true">https://prgmd.tistory.com/10</guid>
      <comments>https://prgmd.tistory.com/10#entry10comment</comments>
      <pubDate>Thu, 16 Jul 2026 11:13:15 +0900</pubDate>
    </item>
  </channel>
</rss>