<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>yoo97 개발 블로그</title>
    <link>https://yoo-dev.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 9 May 2026 19:38:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yoo97</managingEditor>
    <item>
      <title>[백준] 5464. 주차장</title>
      <link>https://yoo-dev.tistory.com/57</link>
      <description>&lt;p&gt;문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/5464&quot;&gt;https://www.acmicpc.net/problem/5464&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# N: 주차 공간 개수, M: 총 차량 댓수
N, M = map(int, input().split())

# 각 주차 공간의 단위 요금표
cost_table = []
for _ in range(N):
     cost = int(input())
     cost_table.append(cost)

# 차량 번호별 차량의 무게. 1번부터 시작
cars_weight = [0]
for __ in range(M):
    weight = int(input())
    cars_weight.append(weight)

# 출력할 정답. 총 금액
answer = 0

# 주차 공간 생성. 0은 비어있다는 의미
space = [0 for _ in range(N)]

# 주차 공간이 부족하면 대기할 대기열
queue = []

# count: 현재 주차된 총 댓수.
# for를 통해 매번 비어있는지 확인해도 되지만
# 매번 전체 주차 공간을 탐색하면 비효율적임.
# count와 N이 동일하면 빈 공간이 없다고 판단할 수 있음.
count = 0

for i in range(2 * M):
    # car: 양수이면, 해당 번호의 차량 주차. 음수이면 해당 번호의 차량 출차
    car = int(input())
    # 주차하는 경우
    if car &amp;gt; 0:
        # space가 만차 상태이면
        if count == N:
            # 주차 대기
            queue.append(car)
        # space에 빈 공간이 있으면
        else:
            # 앞에서부터 빈 공간 확인하기
            for j in range(N):
                # 현재 공간이 비어있으면
                if space[j] == 0:
                    # 현재 공간에 주차
                    space[j] = car
                    # 주차했으므로 현재 총 주차된 차량 수 증가
                    count += 1
                    break
    # 출차하는 경우
    else:
        # 음수를 양수로 변환. car 차량을 출차하면 된다.
        car = car * -1
        # 아래는 주차된 공간에서 car 차량을 찾는 로직
        for j in range(N):
            # 현재 확인중인 공간에 car가 주차되어 있으면
            if space[j] == car:
                # cost_table[j]: j번째 공간의 단위 무게당 비용
                # space[j]: j번째 공간에 주차된 차량 번호
                # cars_weight[space[j]]: 차량의 무게
                # car_cost: 해당 차량이 내야할 요금(공간의 무게당 비용 * 차량의 무게)
                car_cost = cost_table[j] * cars_weight[space[j]]
                answer += car_cost
                # 원래 주차된 차량이 나갔으므로
                # 해당 자리를 빈자리로 만들거나 대기 차량을 주차시켜야 한다.
                # 대기차량이 있으면
                if queue:
                    # 해당 자리에 대기 차량을 주차시킨다.
                    space[j] = queue.pop(0)
                # 대기 차량이 없으면
                else:
                    # 해당 자리를 빈자리로 만든다.
                    space[j] = 0
                    # 차량이 나가기만 했으니 총 주차된 차량 댓수를 1 감소시킨다.
                    count -= 1

print(answer)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>algorithm</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/57</guid>
      <comments>https://yoo-dev.tistory.com/57#entry57comment</comments>
      <pubDate>Fri, 24 Feb 2023 09:19:34 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Logback 설정</title>
      <link>https://yoo-dev.tistory.com/56</link>
      <description>&lt;h1&gt;Spring Boot Logback 설정&lt;/h1&gt;
&lt;h2&gt;Logback&lt;/h2&gt;
&lt;p&gt;로깅 프레임워크 중 하나로 SLF4J의 구현체이다.&lt;/p&gt;
&lt;p&gt;Logback은 &lt;code&gt;logback-core&lt;/code&gt;, &lt;code&gt;logback-classic&lt;/code&gt;, &lt;code&gt;logback-access&lt;/code&gt;의 3가지 모듈로 나뉜다.&lt;br&gt;&lt;code&gt;logback-core&lt;/code&gt;는 다른 두 모듈을 위한 기반 역할을 한다. &lt;code&gt;Appender&lt;/code&gt;와 &lt;code&gt;Encoder(Layout)&lt;/code&gt; 인터페이스가 속한다.&lt;br&gt;&lt;code&gt;logback-classic&lt;/code&gt;은 core에서 확장된 모듈로, core와 SLF4J API 라이브러리를 가진다. &lt;code&gt;Logger&lt;/code&gt; 클래스가 여기에 속한다.&lt;br&gt;&lt;code&gt;logback-access&lt;/code&gt;는 서블릿 컨테이너와 통합되어 HTTP 액세스에 대한 로깅 기능을 제공한다.&lt;/p&gt;
&lt;p&gt;Logback을 이용하여 로깅을 수행하기 위해 필요한 주요 설정 요소로는 Logger, Appender, Encoder 3가지가 있다.&lt;/p&gt;
&lt;h3&gt;Logger&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Logger&lt;/code&gt;는 실제 로깅을 수행하는 구성요소이다.&lt;br&gt;&lt;code&gt;ch.pos.logback.classic.Level&lt;/code&gt; 클래스에 &lt;code&gt;Logger&lt;/code&gt;가 사용 가능한 다섯 가지 레벨(&lt;code&gt;TRACE&lt;/code&gt;, &lt;code&gt;DEBUG&lt;/code&gt;, &lt;code&gt;INFO&lt;/code&gt;, &lt;code&gt;WARN&lt;/code&gt;, &lt;code&gt;ERROR&lt;/code&gt;)이 정의되어 있다. &lt;code&gt;Level&lt;/code&gt;을 통해 출력할 로그의 레벨을 조절할 수 있다. 각 &lt;code&gt;Level&lt;/code&gt;은 &lt;code&gt;TRACE &amp;lt; DEBUG &amp;lt; INFO &amp;lt; WARN &amp;lt; ERROR&lt;/code&gt;이며, 지정한 레벨보다 작은 레벨의 로그는 기록되지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;ERROR&lt;/code&gt; : 요청을 처리하는 중 오류가 발생한 경우 표시&lt;br&gt;&lt;code&gt;WARN&lt;/code&gt; : 처리 가능한 문제, 향후 시스템 에러 원인이 될 수 있는 경고성 메시지 표시&lt;br&gt;&lt;code&gt;INFO&lt;/code&gt; : 상태변경과 같은 정보성 로그 표시&lt;br&gt;&lt;code&gt;DEBUG&lt;/code&gt; : 프로그램을 디버깅하기 위한 정보를 표시&lt;br&gt;&lt;code&gt;TRACE&lt;/code&gt; : Debug보다 훨씬 상세한 정보를 표시&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Appender&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Appender&lt;/code&gt;는 로그 메시지가 출력될 대상을 결정하는 역할을 한다. &lt;code&gt;Appender&lt;/code&gt;는 다음과 같은 종류가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ConsoleAppender&lt;/code&gt; : 콘솔에 로그 출력&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FileAppender&lt;/code&gt; : 파일 단위로 로그 저장&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RollingFileAppender&lt;/code&gt; : 설정 옵션에 따라 로그를 여러 파일로 나누어 저장&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SMTPAppender&lt;/code&gt; : 로그를 메일로 전송하여 기록&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DBAppender&lt;/code&gt; : 로그를 DB에 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Encoder&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Encoder&lt;/code&gt;는 로그 이벤트를 바이트 배열로 변환하고, 해당 바이트 배열을 Output Stream에 쓰는 작업을 담당한다.&lt;br&gt;&lt;code&gt;Appender&lt;/code&gt;에 포함되어 사용자가 지정한 형식으로 표현 될 로그 메시지를 변환하는 역할을 담당한다.&lt;/p&gt;
&lt;h2&gt;Spring-Boot Logback Settings&lt;/h2&gt;
&lt;p&gt;스프링에서 Logback 설정을 위해 &lt;code&gt;logback-spring.xml&lt;/code&gt;을 작성할 수 있다.&lt;br&gt;&lt;code&gt;logback.xml&lt;/code&gt;을 사용하게 되면 스프링 설정 전에 Logback 설정이 먼저 완료되어 로그 제어가 어려워질 수 있다.&lt;/p&gt;
&lt;h3&gt;Dynamic Reloading&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dynamic reloading&lt;/code&gt; 기능을 지원한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;
&amp;lt;configuration scan=&amp;quot;true&amp;quot; scanPeriod=&amp;quot;60 seconds&amp;quot;&amp;gt;
    &amp;lt;!-- 60초 주기로 파일을 검사하여 바뀌었으면 프로그램을 갱신한다. --&amp;gt;
    &amp;lt;!-- ...logback settings... --&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Appender 설정&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;
&amp;lt;configuration scan=&amp;quot;true&amp;quot; scanPeriod=&amp;quot;60 seconds&amp;quot;&amp;gt;

    &amp;lt;!-- appender 설정 --&amp;gt;
    &amp;lt;!-- ConsoleAppender --&amp;gt;
    &amp;lt;appender name=&amp;quot;CONSOLE&amp;quot; class=&amp;quot;ch.qos.logback.core.ConsoleAppender&amp;quot;&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;!-- 로그 출력 패턴 설정 --&amp;gt;
            &amp;lt;pattern&amp;gt;
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- %logger: %-5line : %m%n
            &amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!-- FileAppender --&amp;gt;
    &amp;lt;appender name=&amp;quot;FILE-INFO&amp;quot; class=&amp;quot;ch.qos.logback.core.rolling.RollingFileAppender&amp;quot;&amp;gt;
        &amp;lt;!-- 로그를 기록할 파일명과 경로 설정 --&amp;gt;
        &amp;lt;file&amp;gt;./log/info/info-%d{yyyy-MM-dd}.log&amp;lt;/file&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- %logger: %-5line : %m%n
            &amp;lt;/pattern&amp;gt;
            &amp;lt;charset&amp;gt;UTF-8&amp;lt;/charset&amp;gt;
            &amp;lt;!-- 헤더에 패턴 출력 --&amp;gt;
            &amp;lt;outputPatternAsHeader&amp;gt;true&amp;lt;/outputPatternAsHeader&amp;gt;
        &amp;lt;/encoder&amp;gt;

        &amp;lt;!-- 로그 필터링 설정 --&amp;gt;
        &amp;lt;filter class=&amp;quot;ch.qos.logback.classic.filter.LevelFilter&amp;quot;&amp;gt;
            &amp;lt;!-- INFO 레벨의 로그만 파일에 기록 --&amp;gt;
            &amp;lt;level&amp;gt;INFO&amp;lt;/level&amp;gt;
            &amp;lt;onMatch&amp;gt;ACCEPT&amp;lt;/onMatch&amp;gt;
            &amp;lt;onMismatch&amp;gt;DENY&amp;lt;/onMismatch&amp;gt;
        &amp;lt;/filter&amp;gt;

        &amp;lt;!-- Rolling 정책 결정 --&amp;gt;
        &amp;lt;rollingPolicy class=&amp;quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&amp;quot;&amp;gt;
            &amp;lt;!-- 파일 쓰기가 종료된 log 파일명의 패턴 지정. &amp;#39;.gz&amp;#39;, &amp;#39;.zip&amp;#39;과 같이 지정하면 자동 압축 --&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;
                ./log/info/info-%d{yyyy-MM-dd}.%i.log
            &amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;!-- 파일당 최대 용량 --&amp;gt;
            &amp;lt;maxFileSize&amp;gt;10MB&amp;lt;/maxFileSize&amp;gt;
            &amp;lt;!-- 로그파일 최대 보관 주기(일). 해당 설정일 이상 지나면 로그파일 자동 삭제 --&amp;gt;
            &amp;lt;maxHistory&amp;gt;30&amp;lt;/maxHistory&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
    &amp;lt;/appender&amp;gt;

 &amp;lt;!-- 설정한 appender 적용 --&amp;gt;
    &amp;lt;root level=&amp;quot;info&amp;quot;&amp;gt;
        &amp;lt;appender-ref ref=&amp;quot;CONSOLE&amp;quot;/&amp;gt;
        &amp;lt;appender-ref ref=&amp;quot;FILE-INFO&amp;quot;/&amp;gt;
    &amp;lt;/root&amp;gt;

&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Property&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;property&lt;/code&gt; : 설정 파일에서 사용될 변수값을 선언한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;
&amp;lt;configuration&amp;gt;

    &amp;lt;!-- property 선언 --&amp;gt;
    &amp;lt;property name=&amp;quot;LOG_PATTERN&amp;quot; value=&amp;quot;%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- %logger: %-5line : %m%n&amp;quot;/&amp;gt;
    &amp;lt;property name=&amp;quot;LOG_CHARSET&amp;quot; value=&amp;quot;UTF-8&amp;quot;/&amp;gt;
    &amp;lt;property name=&amp;quot;LOG_FILE_PATH&amp;quot; value=&amp;quot;./log&amp;quot;/&amp;gt;
    &amp;lt;timestamp key=&amp;quot;BY_DATE&amp;quot; datePattern=&amp;quot;yyyy-MM-dd&amp;quot;/&amp;gt;

    &amp;lt;!-- appender 설정 --&amp;gt;
    &amp;lt;appender name=&amp;quot;CONSOLE&amp;quot; class=&amp;quot;ch.qos.logback.core.ConsoleAppender&amp;quot;&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;!-- property 사용 --&amp;gt;
            &amp;lt;pattern&amp;gt;${LOG_PATTERN}&amp;lt;/pattern&amp;gt;
            &amp;lt;charset&amp;gt;${LOG_CHARSET}&amp;lt;/charset&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;appender name=&amp;quot;FILE-INFO&amp;quot; class=&amp;quot;ch.qos.logback.core.rolling.RollingFileAppender&amp;quot;&amp;gt;
        &amp;lt;!-- property 사용 --&amp;gt;
        &amp;lt;file&amp;gt;${LOG_FILE_PATH}/info-${BY_DATE}.log&amp;lt;/file&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;!-- property 사용 --&amp;gt;
            &amp;lt;pattern&amp;gt;${LOG_PATTERN}&amp;lt;/pattern&amp;gt;
            &amp;lt;charset&amp;gt;${LOG_CHARSET}&amp;lt;/charset&amp;gt;
            &amp;lt;outputPatternAsHeader&amp;gt;true&amp;lt;/outputPatternAsHeader&amp;gt;
        &amp;lt;/encoder&amp;gt;

        &amp;lt;filter class=&amp;quot;ch.qos.logback.classic.filter.LevelFilter&amp;quot;&amp;gt;
            &amp;lt;level&amp;gt;INFO&amp;lt;/level&amp;gt;
            &amp;lt;onMatch&amp;gt;ACCEPT&amp;lt;/onMatch&amp;gt;
            &amp;lt;onMismatch&amp;gt;DENY&amp;lt;/onMismatch&amp;gt;
        &amp;lt;/filter&amp;gt;

        &amp;lt;rollingPolicy class=&amp;quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&amp;quot;&amp;gt;
            &amp;lt;!-- property 사용 --&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${LOG_FILE_PATH}/info-${BY_DATE}.%i.log&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;maxFileSize&amp;gt;10MB&amp;lt;/maxFileSize&amp;gt;
            &amp;lt;maxHistory&amp;gt;30&amp;lt;/maxHistory&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
    &amp;lt;/appender&amp;gt;


    &amp;lt;root level=&amp;quot;info&amp;quot;&amp;gt;
        &amp;lt;appender-ref ref=&amp;quot;CONSOLE&amp;quot;/&amp;gt;
        &amp;lt;appender-ref ref=&amp;quot;FILE-INFO&amp;quot;/&amp;gt;
    &amp;lt;/root&amp;gt;

&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Logger 설정&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;root&lt;/code&gt;와 &lt;code&gt;logger&lt;/code&gt;를 통해 설정할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;root&lt;/code&gt; : 전역 설정. 지역 설정인 &lt;code&gt;logger&lt;/code&gt;가 선언되면 해당 &lt;code&gt;logger&lt;/code&gt; 설정이 default로 적용된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logger&lt;/code&gt; : 지역 설정. 특정 패키지의 &lt;code&gt;logger&lt;/code&gt;를 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;!-- ...other settings... --&amp;gt;

    &amp;lt;!-- root level 설정 --&amp;gt;
    &amp;lt;root level=&amp;quot;info&amp;quot;&amp;gt;
    &amp;lt;appender-ref ref=&amp;quot;CONSOLE&amp;quot;/&amp;gt;
    &amp;lt;/root&amp;gt;

    &amp;lt;!-- 특정 패키지 logging level 설정 --&amp;gt;
    &amp;lt;!-- additivity 값은 root 설정 상속 유무를 결정하는 속성이다. default 값은 true --&amp;gt;
    &amp;lt;logger name=&amp;quot;com.example.demo.service&amp;quot; level=&amp;quot;debug&amp;quot; additivity=&amp;quot;false&amp;quot;&amp;gt;
        &amp;lt;appender-ref ref=&amp;quot;CONSOLE&amp;quot;/&amp;gt;
        &amp;lt;appender-ref ref=&amp;quot;FILE-INFO&amp;quot;/&amp;gt;
    &amp;lt;/logger&amp;gt;

&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Profile별 설정 분리&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;srpingProfile&lt;/code&gt;을 통해 Profile별로 &lt;code&gt;Logger&lt;/code&gt;, &lt;code&gt;Appender&lt;/code&gt; 설정을 분리할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; ?&amp;gt;
&amp;lt;configuration scan=&amp;quot;true&amp;quot; scanPeriod=&amp;quot;60 seconds&amp;quot;&amp;gt;

    &amp;lt;!-- property 선언 --&amp;gt;
    &amp;lt;property name=&amp;quot;LOG_PATTERN&amp;quot; value=&amp;quot;%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- %logger: %-5line : %m%n&amp;quot;/&amp;gt;
    &amp;lt;property name=&amp;quot;LOG_CHARSET&amp;quot; value=&amp;quot;UTF-8&amp;quot;/&amp;gt;
    &amp;lt;property name=&amp;quot;LOG_FILE_PATH&amp;quot; value=&amp;quot;./log&amp;quot;/&amp;gt;
    &amp;lt;timestamp key=&amp;quot;BY_DATE&amp;quot; datePattern=&amp;quot;yyyy-MM-dd&amp;quot;/&amp;gt;

    &amp;lt;!-- 로컬 환경 logger settings --&amp;gt;
    &amp;lt;springProfile name=&amp;quot;local&amp;quot;&amp;gt;
        &amp;lt;appender name=&amp;quot;CONSOLE&amp;quot; class=&amp;quot;ch.qos.logback.core.ConsoleAppender&amp;quot;&amp;gt;
            &amp;lt;encoder&amp;gt;
                &amp;lt;pattern&amp;gt;${LOG_PATTERN}&amp;lt;/pattern&amp;gt;
                &amp;lt;charset&amp;gt;${LOG_CHARSET}&amp;lt;/charset&amp;gt;
                &amp;lt;outputPatternAsHeader&amp;gt;true&amp;lt;/outputPatternAsHeader&amp;gt;
            &amp;lt;/encoder&amp;gt;
        &amp;lt;/appender&amp;gt;

        &amp;lt;root level=&amp;quot;info&amp;quot;&amp;gt;
            &amp;lt;appender-ref ref=&amp;quot;CONSOLE&amp;quot;/&amp;gt;
        &amp;lt;/root&amp;gt;
    &amp;lt;/springProfile&amp;gt;

    &amp;lt;!-- 운영 환경 logger settings --&amp;gt;
    &amp;lt;springProfile name=&amp;quot;prod&amp;quot;&amp;gt;
        &amp;lt;appender name=&amp;quot;FILE&amp;quot; class=&amp;quot;ch.qos.logback.core.rolling.RollingFileAppender&amp;quot;&amp;gt;
            &amp;lt;file&amp;gt;${LOG_FILE_PATH}/info-${BY_DATE}.log&amp;lt;/file&amp;gt;
            &amp;lt;encoder&amp;gt;
                &amp;lt;pattern&amp;gt;${LOG_PATTERN}&amp;lt;/pattern&amp;gt;
                &amp;lt;charset&amp;gt;${LOG_CHARSET}&amp;lt;/charset&amp;gt;
                &amp;lt;outputPatternAsHeader&amp;gt;true&amp;lt;/outputPatternAsHeader&amp;gt;
            &amp;lt;/encoder&amp;gt;
            &amp;lt;rollingPolicy class=&amp;quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&amp;quot;&amp;gt;
                &amp;lt;!-- property 사용 --&amp;gt;
                &amp;lt;fileNamePattern&amp;gt;${LOG_FILE_PATH}/info-${BY_DATE}.%i.log&amp;lt;/fileNamePattern&amp;gt;
                &amp;lt;maxFileSize&amp;gt;10MB&amp;lt;/maxFileSize&amp;gt;
                &amp;lt;maxHistory&amp;gt;30&amp;lt;/maxHistory&amp;gt;
            &amp;lt;/rollingPolicy&amp;gt;
        &amp;lt;/appender&amp;gt;

        &amp;lt;root level=&amp;quot;info&amp;quot;&amp;gt;
            &amp;lt;appender-ref ref=&amp;quot;FILE&amp;quot;/&amp;gt;
        &amp;lt;/root&amp;gt;
    &amp;lt;/springProfile&amp;gt;

&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Logback 출력 로그 색상 변경&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;conversionRule conversionWord=&amp;quot;clr&amp;quot; converterClass=&amp;quot;org.springframework.boot.logging.logback.ColorConverter&amp;quot; /&amp;gt;
&amp;lt;!-- %clr(...){cyan} 과 같이 사용할 수 있다. %clr() 내부의 내용이 지정한 색상으로 출력된다. --&amp;gt;

&amp;lt;!-- 사용 예 --&amp;gt;
&amp;lt;property name=&amp;quot;LOG_PATTERN&amp;quot; value=&amp;quot;${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}: %-5line){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}&amp;quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;참고 사이트 : &lt;a href=&quot;https://oingdaddy.tistory.com/257&quot;&gt;https://oingdaddy.tistory.com/257&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>framework/spring</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/56</guid>
      <comments>https://yoo-dev.tistory.com/56#entry56comment</comments>
      <pubDate>Sun, 5 Feb 2023 16:28:11 +0900</pubDate>
    </item>
    <item>
      <title>[Github] Github Action 기초 및 Spring Boot CI/CD Workflow 구성하기</title>
      <link>https://yoo-dev.tistory.com/55</link>
      <description>&lt;h1&gt;Github Action을 이용한 CI/CD 구축&lt;/h1&gt;
&lt;h2&gt;Github Action&lt;/h2&gt;
&lt;p&gt;Github Action은 미리 정의된 특정 작업의 수행을 자동화할 수 있게 해주는 기능이다.&lt;br&gt;빌드/배포 워크플로를 자동화하거나, Pull Request 전 테스트를 자동으로 수행하여 테스트 통과시에만 PR을 open하도록 설정하거나, 특정 시간대에 크롤링 스크립트 등을 반복 수행하도록 하는 등, 여러 작업을 자동화 할 수 있다.&lt;/p&gt;
&lt;h2&gt;Github Action 구성 요소&lt;/h2&gt;
&lt;p&gt;Github Action을 구성하는 요소는 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Workflow&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;가장 최상위에 위치한 개념으로, 모든 명령어의 집합을 말한다.&lt;/li&gt;
&lt;li&gt;하나 이상의 &lt;code&gt;Job&lt;/code&gt;으로 구성되어 있으며 특정 &lt;code&gt;Event&lt;/code&gt;(Push, Pull Request)에 의해서 실행될 수도 있고 특정 시간대에 실행될 수도 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.github/workflows&lt;/code&gt; 디렉토리에 &lt;code&gt;.yml&lt;/code&gt; 형태로 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Event&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Workflow&lt;/code&gt;를 실행시키는 특정 행동을 의미한다.&lt;/li&gt;
&lt;li&gt;Push, Pull Request, Commit과 같은 Github 내의 특정 행동이나, Github 외부에서 발생한 이벤트 등이 될 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows&quot;&gt;Github Action 공식 문서&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Job&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;동일한 &lt;code&gt;Runner&lt;/code&gt;에서 실행되는 여러 &lt;code&gt;Step&lt;/code&gt;의 집합을 말한다.&lt;/li&gt;
&lt;li&gt;기본적으로 하나의 &lt;code&gt;Workflow&lt;/code&gt; 내의 여러 &lt;code&gt;Job&lt;/code&gt;은 독립적으로 실행된다. 독립적으로 실행된다는 말은 병렬로 실행된다는 의미이다.&lt;/li&gt;
&lt;li&gt;필요에 따라 의존관계를 설정하여 &lt;code&gt;Job&lt;/code&gt;들의 실행 순서를 지정해줄 수 있다. 이 경우, 의존하는 작업이 실패하면 이후의 작업들은 취소된다.&lt;/li&gt;
&lt;li&gt;하나의 &lt;code&gt;Job&lt;/code&gt; 내에서 각각의 &lt;code&gt;Step&lt;/code&gt;은 다양한 Task로 인해 생성된 데이터를 공유할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Step&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;Github Action에서 가장 작은 실행 단위로, 명령을 실행할 수 있는 각각의 Task를 의미한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Step&lt;/code&gt;은 &lt;code&gt;shell command&lt;/code&gt;가 될 수도 있고, 하나의 &lt;code&gt;Action&lt;/code&gt;이 될 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Action&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Job&lt;/code&gt;을 구성하기 위해 여러 &lt;code&gt;Step&lt;/code&gt;을 결합한 독립적인 &lt;code&gt;command&lt;/code&gt;를 말한다.&lt;/li&gt;
&lt;li&gt;Github Action에서 재사용이 가능한 &lt;code&gt;Workflow&lt;/code&gt;의 가장 작은 단위이다.&lt;/li&gt;
&lt;li&gt;직접 만든 &lt;code&gt;Action&lt;/code&gt;을 사용하거나 타인이 작성한 &lt;code&gt;Action&lt;/code&gt;을 불러와 사용할 수 있으며, 자신이 작성한 &lt;code&gt;Action&lt;/code&gt;을 다른 사람들에게 배포할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Runner&lt;/code&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Job&lt;/code&gt;을 실행시키기 위한 애플리케이션을 말한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Runner&lt;/code&gt; 애플리케이션은 Github에서 호스팅하는 가상 환경 또는 직접 호스팅하는 가상 환경에서 실행 가능하며, Github에서 호스팅하는 가상 인스턴스의 경우, 메모리 및 용량 제한이 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;참고 : Github Action의 실행 환경&lt;/strong&gt;&lt;br&gt;Github Action은 가상기계 위에 도커 컨테이너를 띄워 수행된다. 따라서 가상기계의 실행 환경과 도커 컨테이너로 띄울 실행 환경을 명시해야 한다. 이때, &lt;strong&gt;가상기계의 실행 환경&lt;/strong&gt;은 &lt;strong&gt;필수&lt;/strong&gt;로 지정해야 하며, &lt;strong&gt;컨테이너로 띄울 도커 이미지&lt;/strong&gt;는 &lt;strong&gt;선택사항&lt;/strong&gt;이다.&lt;br&gt;컨테이너로 띄울 도커 이미지는 여러개가 주어질 수 있다. 도커 이미지를 여러개 선택하게 되면, 선택한 이미지를 띄운 컨테이너들을 생성하고, 각 컨테이너에서 동일한 작업을 수행하게 된다. 이를 이용하면 서로 다른 운영체제에서 작업을 실행시킬 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;CI/CD Workflow 구성하기&lt;/h2&gt;
&lt;p&gt;먼저 Github Repository에서 &lt;code&gt;Actions&lt;/code&gt; 탭을 선택하여 Github Action을 구성할 수 있는 페이지로 들어간다.&lt;/p&gt;
&lt;p&gt;이후, 상단의 &lt;code&gt;set up a workflow yourself&lt;/code&gt;를 클릭하여 &lt;code&gt;Workflow&lt;/code&gt;를 구성할 수 있는 페이지로 접근한다. 그러면 &lt;code&gt;Workflow&lt;/code&gt;를 작성할 수 있는 에디터가 보이는데, 여기에 구성할 &lt;code&gt;Workflow&lt;/code&gt;의 내용을 작성하면 된다.&lt;/p&gt;
&lt;p&gt;아래와 같이 SpringBoot 애플리케이션의 CI/CD &lt;code&gt;Workflow&lt;/code&gt;를 구성한 내용을 작성했다.&lt;br&gt;&lt;code&gt;Workflow&lt;/code&gt; 파일의 각 위치에 설명을 달아뒀다. &lt;code&gt;#&lt;/code&gt;은 해당 코드가 의미하는 바를, &lt;code&gt;###&lt;/code&gt;은 해당 &lt;code&gt;Step&lt;/code&gt;이 어떤 작업을 하는지에 대한 설명이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# Workflow name
name: ComeOn App Dev CI/CD
# Event Trigger
on:
  push:
    branches: [ &amp;quot;dev&amp;quot; ]
# 현재 Workflow를 구성하는 Job들을 정의
jobs:
  # 하나의 Job에 대한 식별자를 설정. test_and_build는 하나의 Job이 된다.
  test_and_build:
    # 실행환경 설정
    runs-on: ubuntu-latest
    # 현재 Job을 구성하는 Step들을 정의
    steps:
    # `-`으로 Step을 구분한다. name은 Step의 식별자를 정의한다.
    - name: Checkout
      # Action을 사용하여 Step을 구성한다.
      uses: actions/checkout@v3
      # Action에 사용될 변수값 지정
      with:
        # repo settings에서 설정한 secrets 값을 사용한다.
        token: ${{ secrets.ACCESS_TOKEN }}
        submodules: true
    ### runner application java 설정
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: &amp;#39;11&amp;#39;
        distribution: &amp;#39;temurin&amp;#39;
    ### gradlew 실행 권한 부여
    - name: Grant execute permisstion for gradlew
      # shell 명령어로 Step의 Task를 지정한다.
      run: chmod +x gradlew
    ### project test
    - name: Test
      run: ./gradlew clean test -Pprofile=test
    ### project build
    - name: Build
      run: ./gradlew bootJar
    ### 도커 buildx 설정
    - name: Setup Docker Buildx
      uses: docker/setup-buildx-action@v2.2.1
    ### 도커 이미지 build &amp;amp; push
    - name: Docker Image build and push
      # 다음과 같이 작성하여 한 Task에서 여러 shell 명령을 수행하게 할 수 있다.
      run: |
        docker login --username=${{ secrets.DOCKERHUB_USERNAME }} --password=${{ secrets.DOCKERHUB_PASSWORD }}
        docker buildx build --platform linux/arm64/v8,linux/amd64 --tag ${{ secrets.DOCKERHUB_USERNAME }}/image-name:latest --push .

  # 하나의 Job에 대한 식별자를 설정. deploy는 하나의 Job이 된다.
  deploy:
    # 현재 Job의 의존관계 설정. test_and_build 식별자를 갖는 Job이 성공해야 현재 Job이 수행된다.
    needs: test_and_build
    runs-on: ubuntu-latest
    steps:
      ### ssh로 배포 서버에 접근하여 도커 이미지 pull &amp;amp; run
      - name: Docker image pull and run
        uses: appleboy/ssh-action@v0.1.7
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          port: ${{ secrets.PORT }}
          script: |
            docker compose down
            docker image rm ${{ secrets.DOCKERHUB_USERNAME }}/image-name
            docker compose up -d --no-recreate&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중간중간 &lt;code&gt;${{ secrets.XXXX }}&lt;/code&gt;와 같은 형식으로 작성된 부분이 보이는데, 이는 민감한 정보를 &lt;code&gt;Secrets&lt;/code&gt; 변수로 등록하여 사용하는 방법이다. 이를 이용하면 비밀번호나 엑세스 토큰과 같은 민감한 정보들을 다른 사람들에게 노출하지 않고 안전하게 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Secrets&lt;/code&gt; 변수를 등록하는 방법은 간단하다.&lt;br&gt;프로젝트 Repository의 &lt;strong&gt;Settings&lt;/strong&gt; 탭으로 접근하여, 왼쪽 네비게이션의 &lt;strong&gt;Secrets and variables&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Actions&lt;/strong&gt; 버튼을 클릭한다.&lt;br&gt;페이지가 전환되면 우측의 &lt;strong&gt;New repository secret&lt;/strong&gt; 버튼을 클릭해서 원하는 변수의 이름과 변수 호출시 사용할 값을 입력하고 &lt;strong&gt;Add secret&lt;/strong&gt; 버튼을 눌러 등록하면 된다.&lt;br&gt;이렇게 등록된 &lt;code&gt;Secrets&lt;/code&gt; 변수는 Github Action에서 사용시 &lt;code&gt;${{ secrets.variable-name }}&lt;/code&gt;와 같은 형태로 사용하면 된다.&lt;br&gt;&lt;code&gt;Secrets&lt;/code&gt; 변수는 사용시에는 정확한 값이 사용되지만, &lt;code&gt;Workflow&lt;/code&gt;의 로그에서는 변수 이름으로 출력되어 로그에 해당 값을 노출시키지 않는다.&lt;/p&gt;</description>
      <category>git</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/55</guid>
      <comments>https://yoo-dev.tistory.com/55#entry55comment</comments>
      <pubDate>Sun, 22 Jan 2023 02:25:28 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] MySQL 컨테이너 띄우고 원격 접속 허용하기</title>
      <link>https://yoo-dev.tistory.com/54</link>
      <description>&lt;h1&gt;MySQL 컨테이너 띄우고 원격 접속 허용하기&lt;/h1&gt;
&lt;h2&gt;docker-compose 파일 작성&lt;/h2&gt;
&lt;p&gt;docker-compose 를 통해 MySQL 컨테이너를 띄우기 위한 파일을 작성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &amp;#39;3.8&amp;#39;
volumes:
        [volume-name]:
                external: true
                name: [volume-name]
services:
        [service-name]:
                container_name: [container-name]
                image: mysql
                ports:
                        - &amp;quot;3306:3306&amp;quot;
                volumes:
                        - [volume-name]:/var/lib/mysql
                env_file: .env
                environment:
                        - TZ=Asia/Seoul
                restart: always
                command:
                        - --character-set-server=utf8mb4
                        - --collation-server=utf8mb4_unicode_ci
                        - --character-set-client-handshake=FALSE&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;환경변수 파일 작성&lt;/h2&gt;
&lt;p&gt;환경변수를 분리한 &lt;code&gt;.env&lt;/code&gt; 파일을 작성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=[root-password 입력]

MYSQL_USER=[생성할 유저명 입력]
MYSQL_PASSWORD=[생성할 유저 패스워드 입력]&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;컨테이너 띄우고 MySQL 접속&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;docker compose&lt;/code&gt; 명령어를 통해 MySQL 컨테이너를 띄운다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ docker compose up -d&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;아래 명령을 통해 컨테이너에 접속한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ docker exec -it [container-name] bin/bash&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;아래 명령을 통해 MySQL에 접속한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mysql -u root -p&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Database 생성&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; show databases;
+------------------------+
| Database               |
+------------------------+
| information_schema     |
| mysql                  |
| performance_schema     |
| sys                    |
+------------------------+
5 rows in set (0.01 sec)

mysql&amp;gt; create database testdb;
Query OK, 1 row affected (0.01 sec)

mysql&amp;gt; show databases;
+------------------------+
| Database               |
+------------------------+
| testdb                 |
| information_schema     |
| mysql                  |
| performance_schema     |
| sys                    |
+------------------------+
5 rows in set (0.01 sec)&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;유저 생성 및 데이터베이스 권한 부여&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mysql&amp;gt; use mysql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

mysql&amp;gt; select host, user from user;
+-----------+------------------+
| host      | user             |
+-----------+------------------+
| %         | root             |
| localhost | mysql.infoschema |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+
5 rows in set (0.01 sec)

mysql&amp;gt; create user &amp;#39;test_user&amp;#39;@&amp;#39;%&amp;#39; identified by &amp;#39;password1111&amp;#39;;
Query OK, 0 rows affected (0.02 sec)

mysql&amp;gt; select host, user from user;
+-----------+------------------+
| host      | user             |
+-----------+------------------+
| %         | test_user        |
| %         | root             |
| localhost | mysql.infoschema |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+
6 rows in set (0.00 sec)

mysql&amp;gt;  flush privileges;
Query OK, 0 rows affected (0.01 sec)

mysql&amp;gt; show grants for &amp;#39;test_user&amp;#39;@&amp;#39;%&amp;#39;;
+----------------------------------------------+
| Grants for test_user@%                       |
+----------------------------------------------+
| GRANT USAGE ON *.* TO `test_user`@`%`        |
+----------------------------------------------+
1 row in set (0.01 sec)

mysql&amp;gt; grant all privileges on testdb.* to test_user@&amp;#39;%&amp;#39;;
Query OK, 0 rows affected (0.03 sec)

mysql&amp;gt; show grants for &amp;#39;test_user&amp;#39;@&amp;#39;%&amp;#39;;
+------------------------------------------------------------------------------+
| Grants for test_user@%                                                       |
+------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `test_user`@`%`                                        |
| GRANT ALL PRIVILEGES ON `testdb`.* TO `test_user`@`%`                        |
+------------------------------------------------------------------------------+
2 rows in set (0.00 sec)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 “test_user”로 “testdb” 데이터베이스에 원격 접속이 가능하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;&lt;br&gt;DBeaver에 데이터베이스 연결을 생성할 때, Dirver properties에 다음과 같이 옵션을 선택해야 한다.&lt;br&gt;&lt;code&gt;autoReconnect=true&lt;/code&gt;&lt;br&gt;&lt;code&gt;useSSL=false&lt;/code&gt;&lt;br&gt;&lt;code&gt;allowPublicKeyRetrieval=true&lt;/code&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>docker</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/54</guid>
      <comments>https://yoo-dev.tistory.com/54#entry54comment</comments>
      <pubDate>Sun, 15 Jan 2023 00:47:59 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring REST Docs, Swagger 조합. restdocs-api-spec</title>
      <link>https://yoo-dev.tistory.com/53</link>
      <description>&lt;h1&gt;Spring REST Docs + Swagger&lt;/h1&gt;
&lt;h2&gt;Swagger, Spring REST Docs 간단한 설명&lt;/h2&gt;
&lt;p&gt;자바로 API를 개발하게 되면 일반적으로 Spring REST Docs와 Swagger 중 하나를 사용하여 API 문서화를 진행하게 된다.&lt;br&gt;Spring Rest Docs와 Swagger의 특징은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[Swagger]&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 테스트가 가능하다.&lt;/li&gt;
&lt;li&gt;API 문서 생성이 자동으로 이루어진다.&lt;/li&gt;
&lt;li&gt;프로덕션 코드에 Swagger 문서화를 위한 어노테이션이 추가된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;[Spring REST Docs]&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트 코드 작성을 강제하여 API 문서가 신뢰성이 있다.&lt;/li&gt;
&lt;li&gt;테스트 성공 이후 생성된 스니펫으로 직접 문서를 작성해야 한다.&lt;/li&gt;
&lt;li&gt;API 테스트 불가.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;프로덕션 코드에 API 문서 관련 코드가 추가되면 컨트롤러 단이 상당히 지저분해질 것 같아서 Spring REST Docs를 자주 사용했다.&lt;br&gt;하지만 테스트 코드 변경시마다 직접 문서를 수정하는 과정이 반복되다 보니 불편함을 느껴서 방법을 찾아보게 되었고 &lt;code&gt;restdocs-api-spec&lt;/code&gt; 라이브러리를 알게 되었다.&lt;/p&gt;
&lt;h3&gt;restdocs-api-spec&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;restdocs-api-spec&lt;/code&gt; 라이브러리를 사용하면 Spring REST Docs와 같이 테스트 코드가 통과하면 OpenAPI 스펙을 얻게되고, Swagger-UI를 통해 OpenAPI 스펙을 문서로 띄우고 테스트할 수 있는 환경을 제공할 수 있다.&lt;/p&gt;
&lt;p&gt;이 포스팅은 Spring REST Docs와 Swagger의 장점을 동시에 누릴 수 있도록 &lt;code&gt;restdocs-api-spec&lt;/code&gt; 라이브러리를 사용하여 문서를 작성하는 방법을 기록한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec&quot;&gt;restdocs-api-spec 깃허브 바로가기&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;적용 방법&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;작성한 모든 코드는 필자의 &lt;a href=&quot;https://github.com/YooHayoung/ex-swagger-restdocs&quot;&gt;깃허브&lt;/a&gt;에서 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;build.gradle 설정&lt;/h3&gt;
&lt;p&gt;restdocs-api-spec 라이브러리를 사용하기 위해 &lt;code&gt;build.gradle&lt;/code&gt;에 의존성을 추가한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// restdocs-api-spec: version 변수 설정
buildscript {
    ext {
        restdocsApiSpecVersion = &amp;#39;0.16.2&amp;#39;
    }
}

plugins {
    id &amp;#39;java&amp;#39;
    id &amp;#39;org.springframework.boot&amp;#39; version &amp;#39;2.7.7&amp;#39;
    id &amp;#39;io.spring.dependency-management&amp;#39; version &amp;#39;1.0.15.RELEASE&amp;#39;
    // restdocs-api-spec: plugin 추가
    id &amp;#39;com.epages.restdocs-api-spec&amp;#39; version &amp;quot;${restdocsApiSpecVersion}&amp;quot;
}

group = &amp;#39;com.example&amp;#39;
version = &amp;#39;0.0.1-SNAPSHOT&amp;#39;
sourceCompatibility = &amp;#39;11&amp;#39;

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

// restdocs-api-spec: OpenAPI 스펙 생성 설정 명시
openapi3 {
    server = &amp;quot;http://localhost:8081&amp;quot;
    title = &amp;quot;restdocs-swagger Test API Documentation&amp;quot;
    description = &amp;quot;Spring REST Docs with SwaggerUI&amp;quot;
    version = &amp;quot;0.0.1-SNAPSHOT&amp;quot;
    format = &amp;quot;json&amp;quot;
    outputDirectory = &amp;quot;src/main/resources/static&amp;quot;
    outputFileNamePrefix = &amp;quot;swagger&amp;quot;
}

dependencies {
    implementation &amp;#39;org.springframework.boot:spring-boot-starter-validation&amp;#39;
    implementation &amp;#39;org.springframework.boot:spring-boot-starter-data-jpa&amp;#39;
    implementation &amp;#39;org.springframework.boot:spring-boot-starter-web&amp;#39;

    compileOnly &amp;#39;org.projectlombok:lombok&amp;#39;
    runtimeOnly &amp;#39;com.h2database:h2&amp;#39;
    annotationProcessor &amp;#39;org.projectlombok:lombok&amp;#39;

    testImplementation &amp;#39;org.springframework.boot:spring-boot-starter-test&amp;#39;
    // restdocs-api-spec: restdocs-api-spec-mockmvc 의존성 추가
    testImplementation &amp;quot;com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}&amp;quot;
}

tasks.named(&amp;#39;test&amp;#39;) {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;openapi3 테스크에서 생성될 OpenAPI 스펙 관련 설정을 추가하는 것을 확인할 수 있다.&lt;br&gt;관련 설정은 &lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec#gradle-plugin-configuration&quot;&gt;여기&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;프로젝트 생성시 Spring REST Docs 의존성을 추가하여 생성했다면 asciidoctor 관련 설정이 적용되어 있을 텐데, restdocs-api-spec 라이브러리를 사용하게 되면 해당 설정이 필요없다. asciidoctor 관련 설정을 지운다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;’org.springframework.restdocs:spring-restdocs-mockmvc’ 의존성도 필요없다. restdocs-api-spec-mockmvc 라이브러리가 restdocs의 기능을 지원한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;API 작성&lt;/h3&gt;
&lt;p&gt;다음으로 API를 작성한다.&lt;br&gt;예제를 위해 간단하게 다음과 같이 작성했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(&amp;quot;/api/v1/users&amp;quot;)
public class UserController {
    private final UserService userService;

    @PostMapping
    public ApiResponse&amp;lt;UserAddResponse&amp;gt; userAdd(@Validated @RequestBody UserAddRequest request, BindingResult bindingResult) {
        // 요청 데이터 검증 실패시 ControllerAdvice가 Catch -&amp;gt; Exception Controll 함.
        if (bindingResult.hasErrors()) {
            throw new ValidationException(bindingResult);
        }

        Long userId = userService.saveUser(
                new User(
                        request.getName(),
                        request.getAge(),
                        request.getGender()
                )
        );

        return ApiResponse.createSuccess(new UserAddResponse(userId));
    }
}

// 공통 응답 Spec
@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiResponse&amp;lt;T&amp;gt; {
    private LocalDateTime responseTime;
    private String errorMessage;
    private T data;

    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; createSuccess(T data) {
        return ApiResponse.&amp;lt;T&amp;gt;builder()
                .responseTime(LocalDateTime.now())
                .data(data)
                .build();
    }

    public static ApiResponse&amp;lt;?&amp;gt; createError(String errorMessage) {
        return ApiResponse.builder()
                .responseTime(LocalDateTime.now())
                .errorMessage(errorMessage)
                .build();
    }

    // 요청 데이터 검증 실패시 data 필드에 MultiValueMap으로 필드 오류 메시지 넣음
    public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; createValidationError(T data) {
        return ApiResponse.&amp;lt;T&amp;gt;builder()
                .responseTime(LocalDateTime.now())
                .errorMessage(&amp;quot;요청 데이터 검증 오류 발생&amp;quot;)
                .data(data)
                .build();
    }
}

// 요청 성공시 ApiResponse.data에 들어갈 응답 데이터
@Data
@AllArgsConstructor
public class UserAddResponse {
    private Long userId;
}

// 요청 Body 데이터
@Data
@AllArgsConstructor
public class UserAddRequest {
    @NotBlank
    private String name;

    @Range(min = 19, max = 30)
    private int age;

    private Gender gender;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이제 테스트 코드를 작성하자.&lt;/p&gt;
&lt;h3&gt;테스트 코드 작성&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs //
@ExtendWith(RestDocumentationExtension.class) //
public class UserControllerTest {
    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    // 요청 성공시 공통 응답 Spec 부분 추출
    FieldDescriptor[] successResponseFields = {PayloadDocumentation.fieldWithPath(&amp;quot;responseTime&amp;quot;).type(JsonFieldType.STRING).description(&amp;quot;응답 시간&amp;quot;),
            PayloadDocumentation.subsectionWithPath(&amp;quot;errorMessage&amp;quot;).type(JsonFieldType.STRING).description(&amp;quot;오류 메시지&amp;quot;).optional(),
            PayloadDocumentation.subsectionWithPath(&amp;quot;data&amp;quot;).type(JsonFieldType.OBJECT).description(&amp;quot;응답 데이터&amp;quot;).optional()};

    // 요청 데이터 검증 실패시 공통 응답 Spec 부분 추출
    FieldDescriptor[] validFailResponseFields = {PayloadDocumentation.fieldWithPath(&amp;quot;responseTime&amp;quot;).type(JsonFieldType.STRING).description(&amp;quot;응답 시간&amp;quot;),
            PayloadDocumentation.subsectionWithPath(&amp;quot;errorMessage&amp;quot;).type(JsonFieldType.STRING).description(&amp;quot;오류 메시지&amp;quot;),
            PayloadDocumentation.subsectionWithPath(&amp;quot;data&amp;quot;).type(JsonFieldType.OBJECT).description(&amp;quot;오류 필드&amp;quot;)};

    @Nested
    @DisplayName(&amp;quot;유저 생성&amp;quot;)
    class userAdd {

        @Test
        @DisplayName(&amp;quot;유저 생성 성공&amp;quot;)
        public void success() throws Exception {
            // given
            UserAddRequest userAddRequest = new UserAddRequest(&amp;quot;user1&amp;quot;, 25, Gender.MEN);

            // when
            ResultActions perform = mockMvc.perform(
                    RestDocumentationRequestBuilders.post(&amp;quot;/api/v1/users&amp;quot;)
                            .contentType(MediaType.APPLICATION_JSON)
                            .characterEncoding(StandardCharsets.UTF_8)
                            .content(objectMapper.writeValueAsString(userAddRequest))
            );

            // then
            perform.andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(MockMvcResultMatchers.jsonPath(&amp;quot;$.data.userId&amp;quot;).isNotEmpty());

            // docs
            perform.andDo(
                    MockMvcRestDocumentationWrapper.document(
                            &amp;quot;{class-name}/{method-name}&amp;quot;,
                            Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
                            Preprocessors.preprocessResponse(Preprocessors.prettyPrint()),
                            ResourceDocumentation.resource(
                                    ResourceSnippetParameters.builder()
                                            .description(&amp;quot;유저 생성&amp;quot;)
                                            .requestFields(
                                                    PayloadDocumentation.fieldWithPath(&amp;quot;name&amp;quot;).description(&amp;quot;이름&amp;quot;),
                                                    PayloadDocumentation.fieldWithPath(&amp;quot;age&amp;quot;).description(&amp;quot;나이&amp;quot;),
                                                    PayloadDocumentation.fieldWithPath(&amp;quot;gender&amp;quot;).description(&amp;quot;성별&amp;quot;)
                                            )
                                            .responseFields(
                                                    new FieldDescriptors(successResponseFields).and(
                                                            PayloadDocumentation.fieldWithPath(&amp;quot;data.userId&amp;quot;).description(&amp;quot;생성된 유저 식별값&amp;quot;)
                                                    )
                                            )
                                            .build()
                            )
                    )
            );
        }

        @Test
        @DisplayName(&amp;quot;요청 데이터 검증 실패&amp;quot;)
        public void validFail() throws Exception {
            // given
            UserAddRequest userAddRequest = new UserAddRequest(null, 10, null);

            // when
            ResultActions perform = mockMvc.perform(
                    RestDocumentationRequestBuilders.post(&amp;quot;/api/v1/users&amp;quot;)
                            .contentType(MediaType.APPLICATION_JSON)
                            .characterEncoding(StandardCharsets.UTF_8)
                            .content(objectMapper.writeValueAsString(userAddRequest))
            );

            // then
            perform.andExpect(MockMvcResultMatchers.status().isBadRequest());

            // docs
            perform.andDo(
                    MockMvcRestDocumentationWrapper.document(
                            &amp;quot;{class-name}/{method-name}&amp;quot;,
                            Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
                            Preprocessors.preprocessResponse(Preprocessors.prettyPrint()),
                            ResourceDocumentation.resource(
                                    ResourceSnippetParameters.builder()
                                            .responseFields(
                                                    new FieldDescriptors(validFailResponseFields).and(
                                                            PayloadDocumentation.subsectionWithPath(&amp;quot;data.name&amp;quot;).description(&amp;quot;name 필드 검증 오류 메시지 리스트&amp;quot;).optional(),
                                                            PayloadDocumentation.subsectionWithPath(&amp;quot;data.age&amp;quot;).description(&amp;quot;age 필드 검증 오류 메시지 리스트&amp;quot;).optional()
                                                    )
                                            )
                                            .build()
                            )
                    )
            );
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;기본적으로 Spring REST Docs 사용법과 같다.&lt;br&gt;주의할 점은 docs 작성 시에 &lt;code&gt;MockMvcRestDocumentationWrapper.document()&lt;/code&gt;를 사용해야 한다는 점이다.&lt;/p&gt;
&lt;p&gt;또한 &lt;code&gt;@AutoConfigureRestDocs&lt;/code&gt; 어노테이션이 필요하다.&lt;br&gt;기존에 Spring REST Docs를 사용할 때에는 해당 어노테이션이 없어도 MockMvc에 커스터마이징한 설정을 넣어서 사용했다.&lt;br&gt;내가 방법을 못찾은 것일 수도 있지만.. 이번 시도에서는 커스터마이징이 어려웠다.&lt;/p&gt;
&lt;p&gt;한 가지 주의할 점이 또 있다.&lt;br&gt;이 역시 Spring REST Docs를 사용할 때에는 &lt;code&gt;PayloadDocumentation.beneathPath()&lt;/code&gt;를 통해 응답의 특정 필드부터만 문서 작성을 명시해도 테스트가 통과되었다.&lt;br&gt;이 때에는 &lt;code&gt;@AutoConfigureRestDocs&lt;/code&gt; 어노테이션을 사용하지 않았고 직접 설정을 추가했었는데, &lt;code&gt;restdocs-api-spec&lt;/code&gt; 라이브러리를 사용할 때에는 &lt;code&gt;@AutoConfigureRestDocs&lt;/code&gt; 어노테이션 때문인지 몰라도, &lt;code&gt;beneathPath()&lt;/code&gt;를 사용하면 테스트가 실패했다(스니펫은 정상적으로 생성된다).&lt;br&gt;이 때문에 공통 API 스펙같은 경우 따로 추출하여 &lt;code&gt;FieldDescriptors(...)&lt;/code&gt;로 묶어주고, 추가로 &lt;code&gt;FieldDescriptor&lt;/code&gt;가 필요한 경우, &lt;code&gt;FieldDescriptors.and(...)&lt;/code&gt; API를 통해 붙여주었다.&lt;/p&gt;
&lt;h3&gt;Open API Spec 생성&lt;/h3&gt;
&lt;p&gt;테스트 코드 작성이 완료되고 테스트를 수행하여 통과되면 &lt;code&gt;/build/generated-snippets/&lt;/code&gt; 아래에 &lt;code&gt;.adoc&lt;/code&gt; 파일과 &lt;code&gt;resource.json&lt;/code&gt; 파일이 생성되었을 것이다.&lt;/p&gt;
&lt;p&gt;이제 gradle의 &lt;code&gt;openapi3&lt;/code&gt; 태스크를 수행하면 &lt;code&gt;.adoc&lt;/code&gt; 스니펫 파일들과 &lt;code&gt;resource.json&lt;/code&gt; 파일을 이용해서 Open API 스펙을 생성한다.&lt;br&gt;&lt;code&gt;./gradlew openapi3&lt;/code&gt; 명령을 입력하거나 인텔리제이 기준 Gradle 탭을 열어 &lt;code&gt;Tasks/documentation/openapi3&lt;/code&gt; 태스크를 수행하자.&lt;/p&gt;
&lt;p&gt;태스크 수행이 성공적으로 완료되면, &lt;code&gt;build.gradle&lt;/code&gt;에서 &lt;code&gt;openapi3&lt;/code&gt; 태스크 설정시 지정한 &lt;code&gt;outputDirectory&lt;/code&gt; 경로에 &lt;code&gt;{outputFileNamePrefix}.{format}&lt;/code&gt;으로 Open API 스펙이 생성된다.&lt;br&gt;따로 지정하지 않았다면 디폴트 값인 &lt;code&gt;build/api-spec/openapi3.json&lt;/code&gt;으로 생성된다.&lt;/p&gt;
&lt;h3&gt;Swagger-UI로 Open API Spec 띄우기&lt;/h3&gt;
&lt;p&gt;프로젝트에서 Swagger-UI 의존성을 추가하여 Swagger 페이지를 생성할 수도 있지만, 별도 Swagger-UI 서버를 띄워 여기에 Open API 스펙들을 모아서 관리하도록 하였다.&lt;br&gt;요즘 MSA 프로젝트를 많이 구성하는데 여러 서버의 문서를 한 곳에서 모아서 보고 싶었기 때문이다.&lt;/p&gt;
&lt;p&gt;Swagger-UI 서버를 띄우기 위해 Docker Compose를 이용했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# swaggerui-docker-compose.yml
version: &amp;quot;3&amp;quot;
services:
  swagger:
    container_name: local-swagger
    image: swaggerapi/swagger-ui
    ports:
      - &amp;quot;8080:8080&amp;quot;
    env_file:
      - .env&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;컨테이너에 필요한 환경변수를 별도 파일로 작성했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// .env
URLS=[{url:&amp;#39;http://localhost:8081/swagger.json&amp;#39;,name:&amp;#39;Test&amp;#39;},]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;URLS에 &lt;code&gt;{url:&amp;#39;url1&amp;#39;,name:&amp;#39;name1&amp;#39;}&lt;/code&gt; 형식으로 &lt;code&gt;[]&lt;/code&gt; 내부에 값을 집어넣으면 Swagger-UI에서 &lt;code&gt;name&lt;/code&gt; 값을 선택하면 해당 &lt;code&gt;name&lt;/code&gt;에 매핑된 &lt;code&gt;url&lt;/code&gt;을 불러온다. 따라서 &lt;code&gt;name&lt;/code&gt;에 특정 서버의 문서 이름을 기입하고 &lt;code&gt;url&lt;/code&gt;에 해당 서버의 Open API Spec을 불러올 경로를 입력하면, Swagger-UI를 통해 여러 서버의 Open API Spec들을 편리하게 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;이제 우리가 작성한 애플리케이션 서버를 띄우고, Swagger-UI 서버 컨테이너를 실행해서 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/81250857/211193097-d12e8c4a-2b7f-4e4d-9bb8-06cb735dcef8.png&quot; alt=&quot;Swagger-UI에서 서버의 Open API Spec을 불러와 띄운 모습&quot;&gt;&lt;/p&gt;
&lt;p&gt;구동에 성공했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;Swagger-UI에 접속해서 애플리케이션 서버의 Open API Spec을 불러오는데 Fetch error가 발생한다면?&lt;/strong&gt;&lt;br&gt;Failed to fetch &lt;a href=&quot;http://localhost:8081/swagger.json**Fetch&quot;&gt;http://localhost:8081/swagger.json**Fetch&lt;/a&gt; error**&lt;br&gt;Possible cross-origin (CORS) issue? The URL origin (&lt;a href=&quot;http://localhost:8081&quot;&gt;http://localhost:8081&lt;/a&gt;) does not match the page (&lt;a href=&quot;http://localhost:8080&quot;&gt;http://localhost:8080&lt;/a&gt;). Check the server returns the correct ‘Access-Control-Allow-*’ headers.&lt;/p&gt;
&lt;p&gt;Open API Spec을 불러올 서버에 CORS 설정이 필요하다. CORS 설정을 추가해주자.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;고찰&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;restdocs-api-spec&lt;/code&gt; 라이브러리가 아직 완벽하게 Spring REST Docs를 지원하지는 못하는 것 같다.&lt;br&gt;&lt;code&gt;MultiValueMap&lt;/code&gt; 같은 경우 &lt;code&gt;&amp;lt;String, String&amp;gt;&lt;/code&gt; 타입인데, &lt;code&gt;Map&lt;/code&gt;의 &lt;code&gt;value&lt;/code&gt; 타입을 &lt;code&gt;String&lt;/code&gt;으로 명시하고 테스트를 수행하니 테스트가 실패한다.&lt;br&gt;타입 명시를 지우고 테스트를 성공시킨 후, &lt;code&gt;resource.json&lt;/code&gt;을 확인해보니 &lt;code&gt;Array&lt;/code&gt; 타입으로 명시되어있다. &lt;code&gt;MultiValueMap&amp;lt;String, String&amp;gt;&lt;/code&gt;의 특정 키의 &lt;code&gt;value[0]&lt;/code&gt;은 &lt;code&gt;String&lt;/code&gt; 타입인데 말이다..&lt;/p&gt;
&lt;p&gt;또한 Swagger-UI에서 이를 확인하면 아래와 같이 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/81250857/211193287-158acb22-8202-4b1d-8299-516abcf9a11f.png&quot; alt=&quot;MultiValueMap의 문제..?&quot;&gt;&lt;/p&gt;
&lt;p&gt;API를 테스트 할 수 있다는 점은 협업하는 입장에서 매우 좋은 것 같다.&lt;br&gt;하지만 API 필드 타입과 설명을 명시하는 부분에서는 Spring REST Docs만을 사용해서 직접 문서로 만드는 것이 더 확실하게 명시할 수 있는 것 같다(내 개인적인 생각이다).&lt;/p&gt;
&lt;p&gt;어쨋든 Spring으로 API 문서를 작성할 때 Swagger, Spring REST Docs 2개의 선택지에서 restdocs-api-spec라는 선택지가 하나 더 생기게 되었다.&lt;br&gt;협업하는 팀원들간에 상의를 해보고 더 적절한 것을 선택하면 좋을 것 같다.&lt;/p&gt;</description>
      <category>framework/spring</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/53</guid>
      <comments>https://yoo-dev.tistory.com/53#entry53comment</comments>
      <pubDate>Sun, 8 Jan 2023 20:30:15 +0900</pubDate>
    </item>
    <item>
      <title>[Git] 깃 서브모듈(Submodule)</title>
      <link>https://yoo-dev.tistory.com/52</link>
      <description>&lt;h1&gt;Submodule&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git Submodule은 하나의 저장소(부모)에 다른 저장소(자식)를 두고 관리하기 위한 도구로, 하나의 프로젝트에서 다른 프로젝트를 함께 사용해야 하는 경우에 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 프로젝트는 public repo로 관리하고, 애플리케이션의 중요한 정보들이 담기는 &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.properties&lt;/code&gt;는 private repo에 따로 관리하고 싶어서 이를 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단해보였지만 의외로 주의해야 할 점이 많아서 기록해두려 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트에 Submodule 적용하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Public, Private Repository 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Public Repository 하나와 Private Repository 하나를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Public Repository에는 프로젝트를, Private Repository에는 프로젝트에서 숨겨야하는 중요한 설정 파일들(application.yml 등)을 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, Private Repository에서 관리할 파일들은 Public Repository에서 제거해준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Public Repository에 Submodule 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Public Repository에 다음과 같이 서브 모듈을 등록해준다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ git submodule add [submodule로 등록할 repo 주소] [submodule 디렉토리를 둘 경로]
Cloning into ...
remote: Enumerating objects: 11, done.
remote: Counting objects: 100% (11/11), done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 11 (delta 0), reused 11 (delta 0), pack-reused 0
Receiving objects: 100% (11/11), done.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 등록이 완료되면 서브 모듈을 등록한 repository 최상위에 &lt;code&gt;.gitmodules&lt;/code&gt; 파일이 생성되고, 지정한 경로에 submodule repository 디렉토리가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitmodules&lt;/code&gt; 파일 내부는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;$ cat .gitmodules
[submodule &quot;submodule 디렉토리 경로&quot;]
    path = submodule 디렉토리 경로
    url = submodule git repository 주소&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Public Repository Push&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submodule 등록을 완료하였으면 이를 commit/push 해야 리모트에 반영된다. Public Repository를 push 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;push 한 뒤에 깃허브를 확인해보면 &lt;code&gt;config @ 102f51b&lt;/code&gt;와 같이 디렉토리 이름 뒤에 &lt;code&gt;@ commit-id&lt;/code&gt;가 붙어있고, 링크로 표시되는 것을 확인할 수 있다.&lt;br /&gt;해당 링크로 접근하면 submodule로 등록된 repository에 접근할 수 있는데, 해당 submodule repository가 private이라면 권한이 없는 유저는 접근할 수 없다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 표시되는 &lt;code&gt;commit-id&lt;/code&gt;는 해당 repository가 참조하는 submodule repository의 &lt;code&gt;commit-id&lt;/code&gt;이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Submodule이 적용된 프로젝트 clone 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 submodule이 적용된 프로젝트를 clone 하면, submodule 내부의 파일이 존재하지 않는다.&lt;br /&gt;터미널에서 다음 명령을 통해 submodule을 초기화하고 업데이트한다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;# submodule 초기화
$ git submodule init
Submodule '프로젝트에 적용된 submodule path' (submodule repository url) registered for path '프로젝트에 적용된 submodule path'

# submodule 업데이트
$ git submodule update
Cloning into 'local project의 submodule path'...
Submodule path 'project submodule path': checked out 'submodule commit-id'&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submodule이 적용된 프로젝트를 처음 clone 할 때 submodule까지 한번에 clone 할 수 있다. 다음 명령을 이용한다.&lt;br /&gt;&lt;code&gt;$ git clone --recurse-submodules [main-project-url]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;update된 submodule repository를 main repository로 가져오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submodule repository가 업데이트 되었는데 메인 프로젝트 repository에 아직 반영이 안되었을 경우, submodule repository의 변경 사항을 가져와서 작업해야 하는 경우에는 다음 명령을 이용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;$ git submodule update --remote --merge&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;--remote&lt;/code&gt; 옵션은 &lt;code&gt;.gitmodules&lt;/code&gt; 파일에 정의되어 있는 브랜치의 최신 버전으로 업데이트한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--merge&lt;/code&gt; 옵션은 로컬에서 작업 중인 부분과 원격에 작업된 부분이 다르면, &lt;code&gt;merge&lt;/code&gt;까지 진행해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령을 수행하고 나면 메인 프로젝트에 submodule repository의 변경 사항이 적용되었다.&lt;br /&gt;메인 프로젝트 commit/push 시점에 해당 변경 사항을 함께 commit/push 해주면 된다.&lt;br /&gt;로컬에 업데이트한 submodule의 변경 사항을 commit/push 해주지 않으면 remote에는 변경 사항이 적용되지 않은 이전 커밋의 submodule을 참조하고 있을 것이다..&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로컬의 submodule repository 변경 내역을 commit/push 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submodule repository의 파일에 변경 사항을 커밋하고 싶다면, 먼저 메인 프로젝트에 등록된 submodule repository 디렉토리로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메인 프로젝트에서 해당 디렉토리로 이동하면, 메인 프로젝트가 참조하고 있는 submodule repository의 &lt;code&gt;commit&lt;/code&gt;으로 체크아웃 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이, 변경 내역을 commit/push 할 submodule repository의 &lt;code&gt;branch&lt;/code&gt;로 &lt;code&gt;checkout&lt;/code&gt;하고, &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;push&lt;/code&gt; 한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# project submodule path로 이동
~/project $ cd [project submodule path]

# main project가 참조하고 있는 submodule의 commit으로 체크아웃 됨.
# commit/push 할 branch로 checkout
~/project/submodule-path[➦ commit-id] $ git checkout main

# add, commit, push 수행하면 된다.
~/project/submodule-path[ main] $ &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submodule repository를 업데이트하고 나면, 메인 프로젝트의 submodule이 참조하고 있는 submodule repository의 commit을 업데이트 해줘야 한다.&lt;br /&gt;메인 프로젝트에서 &lt;code&gt;git submodule update&lt;/code&gt; 명령을 통해 submodule이 참조하는 &lt;code&gt;commit&lt;/code&gt;을 업데이트 해주자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;submodule repository에 새로운 변경 내역을 업데이트 했지만, 메인 프로젝트에서 &lt;code&gt;git submodule update&lt;/code&gt; 명령을 수행하지 않으면, 메인 프로젝트는 여전히 변경 내역이 적용되지 않은 이전 커밋의 버전을 참조한다.&lt;br /&gt;메인 프로젝트에서도 변경 사항을 적용하고 싶다면 꼭 submodule을 업데이트 해주자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Jenkins 같은 CI/CD 툴을 이용할 때 서브 모듈에 있는 파일이 필요할 수도 있다. 이 경우 서브 모듈에 접근할 수 있는 권한을 줘야한다. (ex: Jenkins의 경우 credentials를 추가해주면 간단하게 해결된다.)&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>git</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/52</guid>
      <comments>https://yoo-dev.tistory.com/52#entry52comment</comments>
      <pubDate>Sat, 7 Jan 2023 11:57:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Web-Socket, SockJS, STOMP 이론</title>
      <link>https://yoo-dev.tistory.com/51</link>
      <description>&lt;h1&gt;WebSocket, SockJS, STOMP 소개&lt;/h1&gt;
&lt;h2&gt;WebSocket&lt;/h2&gt;
&lt;p&gt;WebSocket은 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜이다.&lt;/p&gt;
&lt;p&gt;웹 소켓은 HTTP를 사용하는 네트워크 데이터 통신의 단점을 보완하는데 그 목적이 있다.&lt;br&gt;웹 소켓을 설명하기 이전에, 웹 소켓의 등장 이전에는 HTTP 통신의 단점을 어떻게 해결하려고 했는지 알아보겠다.&lt;/p&gt;
&lt;h3&gt;WebSocket 등장 이전&lt;/h3&gt;
&lt;p&gt;모든 HTTP를 사용한 통신은 클라이언트가 먼저 요청을 보내고, 그 요청에 따라 웹 서버가 응답하는 형태이며 웹 서버는 응답을 보낸 후 웹 브라우저와의 연결을 끊는다.&lt;br&gt;이러한 통신 방식을 반이중 통신(Half Duplex)라고 한다.&lt;/p&gt;
&lt;p&gt;실시간 검색어와 같이 서버에서 제공하는 데이터를 항상 최신으로 유지하고 싶다고 하자.&lt;br&gt;반이중 통식 방식을 지원하는 기존 HTTP 통신으로는 어떻게 해결할 수 있었을까?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Polling&lt;/strong&gt;&lt;br&gt;클라이언트가 일반적인 HTTP 요청을 주기적으로 보내서 변경된 데이터를 확인하는 방식을 Polling이라고 한다.&lt;br&gt;가장 간단하고 쉬운 방식이지만 클라이언트가 주기적으로 요청을 보내기 때문에 클라이언트의 수가 많아지면 서버의 부담이 많아지게 된다. HTTP 커넥션을 맺고 끊는 것 또한 리소스를 많이 잡아먹는다는 문제가 있었다.&lt;/p&gt;
&lt;p&gt;Polling의 문제를 해결하기 위해 Long-Polling이 등장한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Long-Polling&lt;/strong&gt;&lt;br&gt;Long-Polling은 클라이언트에서 서버로 일단 HTTP 요청을 보내고, 서버는 요청을 받은 상태를 유지하다가 클라이언트가 원하는 이벤트가 서버에 발생하면 그 때 응답 메시지를 보낸다. 응답을 받은 클라이언트는 다시 서버로 요청을 보내 다음 이벤트를 기다리게 된다.&lt;br&gt;Polling 방식보다는 서버의 부담이 줄지만, 이 방식 역시 클라이언트로 보내는 이벤트의 발생 주기가 짧다면 Polling 방식과 별반 다를게 없다. 또한 동시에 여러 이벤트가 발생하게 되어 다수의 클라이언트에게 보내야 할 경우, 서버의 부담이 급증하게 된다는 문제가 있다.&lt;/p&gt;
&lt;p&gt;이 때, SSE가 등장한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSE(Server-Sent Event)&lt;/strong&gt;&lt;br&gt;클라이언트가 서버로 HTTP 요청을 보내면 서버는 이벤트에 대한 응답을 할 때 이 HTTP 요청에 대한 연결을 끊지 않고 계속 유지한다. 이렇게 되면 서버는 이벤트가 발생하여 변경이 필요한 데이터만 계속 응답하면 된다. 이러한 방법을 SSE라고 한다.&lt;br&gt;클라이언트는 한 번만 요청을 보내놓으면 커넥션이 끊기지 않는 한 계속해서 변경된 데이터를 받을 수 있고, 서버 입장에서도 이벤트가 발생할 때 마다 HTTP 커넥션을 생성하지 않아도 되어 부담이 줄어들게 된다.&lt;/p&gt;
&lt;p&gt;Polling, Long-Polling, SSE와 같은 방식은 실시간으로 변경된 데이터를 전달받을 수 있지만, 여전히 서버에서 클라이언트 방향으로, 단방향으로 데이터를 전송한다.&lt;br&gt;실시간 검색어나 주식, 날씨 정보와 같은 데이터는 양방향 통신이 필요없다. 서버의 데이터가 변동되면 단방향으로 데이터를 전달받으면 된다.&lt;/p&gt;
&lt;p&gt;하지만 채팅의 경우에는 어떤가?&lt;br&gt;상대에게 실시간으로 메시지를 전송해야 하고, 상대의 메시지를 실시간으로 전달받아야 한다. 즉, 양방향 통신이 필요하다.&lt;br&gt;이렇게 서버를 통해 여러 클라이언트가 실시간으로 데이터를 주고 받기 위해 WebSocket 기술이 등장하게 된 것이다.&lt;/p&gt;
&lt;h3&gt;웹 소켓&lt;/h3&gt;
&lt;p&gt;위에서 언급했듯이, WebSocket은 기존의 단방향 HTTP 프로토콜과 호환되어 양방향 통신을 제공하기 위해 개발된 프로토콜로, 서버와 클라이언트 사이에 Socket 커넥션을 유지하면서 양방향 통신을 가능하게 하는 기술이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;웹 소켓의 동작 과정&lt;/strong&gt;은 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;클라이언트가 HTTP 프로토콜로 Handshake 요청을 한다.&lt;/li&gt;
&lt;li&gt;이에 대해 서버는 HTTP 상태 코드 101을 응답해준다. 이때 HTTP Upgrade 헤더를 사용하여 웹 소켓 프로토콜로 변경할 수 있도록 명시한다.&lt;/li&gt;
&lt;li&gt;이후 통신 프로토콜을 WebSocket 프로토콜로 변환하여 데이터를 전송할 수 있게끔 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;WebSocket은 일반 Socket 통신과는 달리 HTTP 80, HTTPS 443 포트 위에서 동작하도록 설계되었다. 따라서 별도의 포트를 열지 않아도 된다.&lt;/p&gt;
&lt;p&gt;HTTP 통신은 HTTP와 HTTPS로 통신하는데, HTTPS는 통신의 보안을 위해 HTTP에 보안을 적용한 것이다.&lt;br&gt;웹 소켓은 통신을 위해 &lt;strong&gt;ws&lt;/strong&gt;를 사용하는데 ws는 HTTP에 대응하는 것이며, 보안을 위해서는 &lt;strong&gt;wss&lt;/strong&gt;를 사용하여 통신해야 한다.&lt;/p&gt;
&lt;h2&gt;SockJS&lt;/h2&gt;
&lt;p&gt;웹소켓은 HTML5 이후에 나왔기 때문에 HTML5 이전의 기술로 구현된 서비스에서는 웹소켓 기술을 사용할 수 없다.&lt;br&gt;HTML5 이전의 기술로 구현된 서비스에서 웹소켓처럼 동작할 수 있도록 도와주는 것이 &lt;strong&gt;SockJS&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;SockJS는 네이티브 웹소켓을 사용하려고 하는 WebSocket 클라이언트이며, 웹소켓을 지원하지 않는 구형 브라우저에 대체 옵션을 제공한다.&lt;br&gt;우선 WebSocket 연결을 시도하고 실패할 경우 SSE, Long-Polling과 같은 HTTP 기반의 다른 기술로 전환하여 다시 연결을 시도한다.&lt;/p&gt;
&lt;p&gt;SockJS를 사용하면 애플리케이션이 웹소켓을 사용하도록 허용하고 브라우저 등에서 웹소켓을 지원하지 않는 경우에 웹소켓을 대체할 대안 기술을 사용하도록 한다. 런타임에 사용 기술을 변경하기 때문에 애플리케이션 코드의 변경이 필요없어 유연하다는 장점이 있다.&lt;/p&gt;
&lt;p&gt;SockJS는 서버에서 주기적으로 Heartbeat 메시지를 전송하도록 하여, 프록시가 커넥션이 끊겼다는 결론을 내리지 않도록 한다. 주기적으로 보내는 시간을 &lt;code&gt;heartbeat interval&lt;/code&gt;이라고 한다.&lt;br&gt;서버가 Heartbeat 메시지를 전송하고 클라이언트가 이에 대한 응답을 하지 않으면, 서버는 클라이언트가 죽은 것으로 판단한다.&lt;br&gt;반면 클라이언트는 마지막 Hearbeat 메시지를 전송받고 특정 시간(&lt;code&gt;heartbeat timeout&lt;/code&gt;)동안 Heartbeat 메시지를 받지 못하면 서버가 죽은 것으로 판단하고 접속 종료와 재접속 흐름을 진행한다.&lt;br&gt;기본값으로 &lt;code&gt;heartbeat interval&lt;/code&gt;은 25초, &lt;code&gt;heartbeat timeout&lt;/code&gt;은 60초로 설정되며 설정을 변경할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;(참고)&lt;/strong&gt; STOMP를 이용해 Heartbeat를 주고 받게되면, SockJS의 Heartbeat 설정은 비활성화된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;STOMP&lt;/h2&gt;
&lt;p&gt;웹소켓만을 이용하게 되면 해당 메시지가 어떤 요청인지, 어떻게 처리해줘야 하는지 직접 구현해야 한다. 세션을 관리하는 방법 또한 서버에서 직접 구현하게 된다.&lt;/p&gt;
&lt;p&gt;STOMP는 Simple Text Oriented Message Protocol의 약자로, 메시지 전송을 효율적으로 하기 위한 프로토콜이다.&lt;br&gt;클라이언트와 서버가 전송할 메시지의 유형, 형식, 내용들을 정의하는 매커니즘으로, 메시지의 헤더에 값을 줘서 인증 처리를 구현하는 것도 가능하다.&lt;/p&gt;
&lt;p&gt;STOMP는 PUB/SUB 구조로 동작한다.&lt;br&gt;PUB/SUB 구조는 메시지를 공급하는 주체와, 소비하는 주체를 분리하여 제공하는 메시징 방법이다.&lt;br&gt;따라서 메시지 송신, 수신에 대한 처리를 명확하게 정의할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://stomp.github.io/stomp-specification-1.2.html&quot;&gt;STOMP 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;PUB/SUB 구조&lt;/h3&gt;
&lt;p&gt;PUB/SUB 구조에 대해 조금 더 설명해 보겠다.&lt;/p&gt;
&lt;p&gt;PUB은 클라이언트의 메시지 송신과 관련된다.&lt;br&gt;서버가 &lt;code&gt;/pub/chat-room/{chat-room-id}&lt;/code&gt;과 같은 메시지 요청 경로를 오픈했다고 하자.&lt;br&gt;클라이언트들은 서버에서 오픈한 메시지 요청 경로로 메시지를 전송하고, 서버는 메시지를 전달받아 적절한 처리를 한다.&lt;/p&gt;
&lt;p&gt;SUB은 클라이언트의 메시지 수신과 관련된다.&lt;br&gt;클라이언트가 &lt;code&gt;/sub/chat-room/{chat-room-id}&lt;/code&gt;와 같이 &lt;strong&gt;구독&lt;/strong&gt;요청을 보내면, 서버는 해당 경로를 &lt;code&gt;topic&lt;/code&gt;으로 관리하고 특정 이벤트가 발생했을 때 특정 &lt;code&gt;topic&lt;/code&gt;을 구독중인 클라이언트들에게 메시지를 전달할 수 있다.&lt;br&gt;간단하게 말하면 &lt;code&gt;topic&lt;/code&gt;을 기준으로 그룹을 나누고, 서버는 &lt;code&gt;topic&lt;/code&gt; 그룹으로 세션을 관리, 클라이언트는 구독한 &lt;code&gt;topic&lt;/code&gt;에 대한 메시지를 수신할 수 있도록 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;RabbitMQ, Kafka와 같은 외부 메시지 큐를 사용하는 이유&lt;/strong&gt;&lt;br&gt;WebSocket만을 이용하게 되면, 세션 단위의 메시지 전달만 가능하다. 따라서 채팅방을 여러개 만들지 못한다는 문제가 있다.&lt;br&gt;이를 해결하기 위해 STOMP를 이용하여 pub/sub 구조로 여러 방을 만들고 STOMP Broker를 통해 특정 Topic을 구독중인 클라이언트들에게 메시지를 전달할 수 있다.&lt;br&gt;하지만 STOMP는 서버 메모리를 Broker로 사용하기 때문에 서버가 2개 이상일 경우, 한 서버에서 발생한 메시지를 다른 서버에 전달하기가 어렵다. 클러스터링을 통해 해결해야 한다.&lt;br&gt;이러한 문제를 해결하기 위해 외부 메시지 큐를 사용할 수 있다. 한 서버에서 발생한 메시지는 외부 메시지 큐에 적재되고, 외부 메시지 큐를 구독중인 모든 서버는 적재된 메시지를 가져와 메시지가 해당되는 토픽을 구독중인 모든 클라이언트들에게 전달할 수 있게 된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <category>framework/spring</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/51</guid>
      <comments>https://yoo-dev.tistory.com/51#entry51comment</comments>
      <pubDate>Fri, 6 Jan 2023 13:56:17 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 혼자 놀기의 달인 (Kotlin)</title>
      <link>https://yoo-dev.tistory.com/50</link>
      <description>&lt;h1&gt;프로그래머스 : 혼자 놀기의 달인 (Kotlin)&lt;/h1&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/131130&quot;&gt;문제 확인하기&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cards&lt;/code&gt;의 길이는 총 상자의 개수이며, &lt;code&gt;cards&lt;/code&gt;의 값에는 1부터 &lt;code&gt;cards&lt;/code&gt;의 길이까지의 숫자 카드가 존재한다.&lt;/p&gt;
&lt;p&gt;그리고 문제를 다시 확인해보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그 다음 임의의 상자를 하나 선택하여 선택한 상자 안의 숫자 카드를 확인합니다. 다음으로 확인한 카드에 적힌 번호에 해당하는 상자를 열어 안에 담긴 카드에 적힌 숫자를 확인합니다. 마찬가지로 숫자에 해당하는 번호를 가진 상자를 계속해서 열어가며, 열어야 하는 상자가 이미 열려있을 때까지 반복합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 부분은 DFS 알고리즘을 통해 구현할 수 있다.&lt;br&gt;상자의 번호를 &lt;code&gt;cards&lt;/code&gt; 배열의 인덱스로, 상자안의 값을 &lt;code&gt;cards&lt;/code&gt; 배열의 값으로, 상자가 열렸는지 여부는 방문 여부를 체크하는 새로운 배열 &lt;code&gt;visited&lt;/code&gt;를 선언하여 최상위 DFS 알고리즘 호출 한번에 하나의 그룹을 얻을 수 있다.&lt;br&gt;이때, DFS 호출의 결과로 몇 개의 상자를 방문했는지 반환하도록 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;visited&lt;/code&gt; 배열을 순차적으로 탐색하면서 아직 방문되지 않은 상자에 대해 DFS 알고리즘을 계속해서 호출하도록 한다.&lt;/p&gt;
&lt;p&gt;모든 상자에 대해 방문을 마쳤다면, 최상위 DFS 호출이 반환한 방문한 상자의 갯수 중 가장 큰 2개를 곱셈하면 문제가 요구하는 답을 얻을 수 있다.&lt;br&gt;DFS 호출 한번에 모든 상자를 열었다면 0을 반환하면 된다.&lt;/p&gt;
&lt;p&gt;이때 주의할 점은 상자 번호를 가리키는 &lt;code&gt;cards&lt;/code&gt; 배열의 인덱스와 카드 숫자를 가리키는 &lt;code&gt;cards&lt;/code&gt; 배열의 값이 다르다는 점이다.&lt;br&gt;배열의 인덱스는 0부터 시작하고, &lt;code&gt;cards&lt;/code&gt; 배열의 값은 카드 숫자를 가리키므로 1부터 시작한다.&lt;br&gt;따라서 카드 번호가 &lt;code&gt;k&lt;/code&gt;이면, 다음 선택할 상자 번호는 &lt;code&gt;k-1&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;p&gt;아래는 코틀린으로 작성한 코드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class Solution {
    lateinit var visited: BooleanArray

    fun solution(cards: IntArray): Int {
        visited = BooleanArray(cards.size)

        var group1 = 0
        var group2 = 0

        for (i in cards.indices) {
            // 현재 상자를 열지 않았으면
            if (!visited[i]) {
                // 현재 상자를 연다.
                visited[i] = true
                // DFS 수행, 이 그룹의 개수를 얻는다.
                val count = dfs(i, cards)
                // 현재 그룹의 개수 저장
                if (count &amp;gt; group1) {
                    group2 = group1
                    group1 = count
                } else if (count &amp;gt; group2) group2 = count
            }
        }

        // 가장 개수가 많은 그룹끼리 곱셉
        // 그룹이 하나뿐이면 group2는 0이 되므로 문제의 요구사항 만족
        return group1 * group2
    }

    fun dfs(idx: Int, cards: IntArray): Int {
        var count = 1
        // cards[idx]는 현재 박스의 카드 숫자
        // 인덱스는 0부터 시작, 카드 번호는 1부터 시작
        // 따라서, 다음 박스의 인덱스는 현재 박스의 카드 숫자 - 1 
        val nextBoxIdx = cards[idx] - 1
        // 다음 박스륿 오픈하지 않았으면
        if (!visited[nextBoxIdx]) {
            // 다음 상자를 연다
            visited[nextBoxIdx] = true
            count = dfs(nextBoxIdx, cards) + 1
        }
        // 현재 박스의 카드 숫자를 박스의 번호로 한다면(가장 깊은 DFS) 1을 반환
        // 그렇지 않으면 DFS가 중첩된 횟수만큼(최상위 DFS 한 번에 방문한 박스 개수만큼) 반환
        return count
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>algorithm/문제풀이</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/50</guid>
      <comments>https://yoo-dev.tistory.com/50#entry50comment</comments>
      <pubDate>Sat, 31 Dec 2022 15:38:28 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 마법의 엘리베이터 (Kotlin)</title>
      <link>https://yoo-dev.tistory.com/49</link>
      <description>&lt;h1&gt;프로그래머스 : 마법의 엘리베이터 (Kotlin)&lt;/h1&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/148653&quot;&gt;문제 확인하기&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;storey&lt;/code&gt; 층에서 0층으로 내려가야 한다.&lt;br&gt;내려가는 방법은 &lt;code&gt;-10^c or +10^c (c &amp;gt;= 0)&lt;/code&gt; 중에서 선택해야 한다.&lt;/p&gt;
&lt;p&gt;우선 &lt;code&gt;storey&lt;/code&gt;로 한 자리 숫자가 주어졌을 때를 생각해보자.&lt;/p&gt;
&lt;p&gt;3이 주어졌다고 할 때 단순히 -1 버튼을 3번 눌러서 0층으로 가는 것이 가장 빠르다.&lt;/p&gt;
&lt;p&gt;7이 주어졌다고 해보자.&lt;br&gt;이 때는 -1을 7번 누르는 것 보다 +1을 3번 눌러서 10층으로 이동하고 -10 버튼을 눌러 0층으로 가는 것이 가장 빠르다.&lt;/p&gt;
&lt;p&gt;5가 주어지면 어떨까?&lt;br&gt;5가 +1을 5번 누르고 -10을 한 번 누르는 것 보다는 단순히 -1을 5번 누르는 것이 더 빠르게 이동할 수 있다.&lt;/p&gt;
&lt;p&gt;이번에는 &lt;code&gt;storey&lt;/code&gt;가 두 자리 숫자가 주어졌을 때를 생각해보겠다.&lt;/p&gt;
&lt;p&gt;13이 주어진다면 -1을 3번, -10을 한번 누르는 것이 가장 빠르다.&lt;/p&gt;
&lt;p&gt;17이 주어진다면 -1을 7번 누르는 경우보다 +1을 3번, -10을 2번 누르는 경우가 더 빠르다.&lt;/p&gt;
&lt;p&gt;각각 45와 55, 65가 주어진 경우를 생각해보자.&lt;br&gt;45가 주어진다면 -1을 5번, -10을 4번 누르는 경우가 총 9번 누르게 되어 가장 빠르다.&lt;br&gt;55가 주어진다면 -1을 5번, -10을 5번, 총 10번으로 이동 가능하고 또 다른 방법으로 +1을 5번, +10을 4번, -100을 1번 눌러 총 10번으로 이동하는 경우가 있다.&lt;br&gt;65가 주어진다면 +1을 5번, +10을 3번, -100을 1번 눌러 총 9번만 눌러서 0층으로 갈 수 있다.&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;i&lt;/code&gt;번째 자릿수가 5이면 &lt;code&gt;i+1&lt;/code&gt;번째 자릿수의 값에 따라 올라갈지 내려갈지 정해야 한다.&lt;br&gt;위 내용을 확인해보면 &lt;code&gt;i+1&lt;/code&gt;번째 자릿수가 4 이하이면 - 버튼으로 내려가면 되고, 6 이상이면 + 버튼을 통해 올라가는 것이 빠르다.&lt;br&gt;그럼 &lt;code&gt;i+1&lt;/code&gt;번째 자릿수가 5일 경우가 남는데, 이 때는 올라가든 내려가든 동일했기 때문에 아무 경우나 하나를 골라 선택하면 된다.&lt;/p&gt;
&lt;p&gt;여기서 알고리즘을 도출할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;1의 자릿수부터 마지막 자릿수까지 차례로 확인하여 아래 내용을 반복한다.&lt;ul&gt;
&lt;li&gt;현재 확인중인 자릿수를 &lt;code&gt;i&lt;/code&gt;라고 한다. &lt;code&gt;i&lt;/code&gt;번째 자릿수의 값(&lt;code&gt;k&lt;/code&gt;)에 따라 아래 내용을 수행한다.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;k&lt;/code&gt;가 5보다 작으면, &lt;code&gt;-10^(i-1)&lt;/code&gt; 버튼을 &lt;code&gt;k&lt;/code&gt;번 눌러 내려간다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;k&lt;/code&gt;가 5보다 크면, &lt;code&gt;+10^(i-1)&lt;/code&gt; 버튼을 &lt;code&gt;10 - k&lt;/code&gt;번 눌러 올라간다. 이 때, 다음 자릿수의 값이 1 증가한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;k&lt;/code&gt;가 5이면, 다시 아래 내용 중 하나를 선택해서 수행한다. &lt;ul&gt;
&lt;li&gt;&lt;code&gt;i+1&lt;/code&gt;번째 자릿수의 값(&lt;code&gt;j&lt;/code&gt;)이 4 이하이면, &lt;code&gt;-10^(i-1)&lt;/code&gt; 버튼을 &lt;code&gt;k&lt;/code&gt;번 눌러 내려간다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i+1&lt;/code&gt;번째 자릿수의 값(&lt;code&gt;j&lt;/code&gt;)이 5 이상이면, &lt;code&gt;+10^(i-1)&lt;/code&gt; 버튼을 &lt;code&gt;10 - k&lt;/code&gt;번 눌러 올라간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 주의할 점은 &lt;code&gt;storey&lt;/code&gt;의 자릿수 만큼 반복을 수행했다면, &lt;code&gt;storey&lt;/code&gt;의 마지막 자릿수에서 자릿수 올림이 발생했는지의 여부이다.&lt;/p&gt;
&lt;p&gt;만약 98765가 &lt;code&gt;storey&lt;/code&gt;로 주어졌을 때, 총 자릿수의 갯수는 5개이다.&lt;br&gt;위 알고리즘을 통해 5번 반복수행 했다고 하자.&lt;br&gt;위 알고리즘을 이용하면 마지막 자릿수 9에서 자릿수 올림이 발생한다.&lt;br&gt;그럼 알고리즘 수행을 마쳤을 때 현재 위치한 층은 100000가 되어 0층으로 이동하지 않은 상태가 된다.&lt;/p&gt;
&lt;p&gt;이 경우 -100000 버튼을 한번 눌러 0층으로 갈 수 있기 때문에 알고리즘 수행을 마쳤을 당시 총 버튼을 누른 횟수 + 1을 결과로 반환하면 된다.&lt;/p&gt;
&lt;p&gt;아래는 해당 내용을 코틀린을 사용해서 작성한 코드이다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class MagicElevator {
    fun solution(storey: Int): Int {
        // 총 버튼을 누른 횟수
        var answer = 0

        var remainStorey = storey
        // 자릿수
        val digitCount = remainStorey.toString().length

        // 자릿수 만큼 진행
        for (i in 1 .. digitCount) {
            // 10^(i-1)의 자릿수 값
            val digitNum = getDigitNumberFrom(digitAt = i, num = remainStorey)
            // 자릿수의 값이 5보다 크면 +10^(i-1) 버튼을 통해 올라간다.
            if (digitNum &amp;gt; 5) {
                remainStorey += makeDigitNumber(digit = i, num = 10 - digitNum)
                answer += (10 - digitNum)
            }
            // 자릿수의 값이 5보다 작으면 -10^(i-1) 버튼을 통해 내려간다.
            else if (digitNum &amp;lt; 5) {
                remainStorey -= makeDigitNumber(digit = i, num = digitNum)
                answer += digitNum
            }
            // 자릿수의 값이 5이면 다음 자릿수를 확인한다.
            else {
                // 다음 자릿수가 없으면 -10^(i-1) 버튼을 통해 내려간다.
                if (i + 1 &amp;gt; digitCount) {
                    remainStorey -= makeDigitNumber(digit = i, num = digitNum)
                    answer += digitNum
                } else {
                    // 다음 자릿수
                    val nextDigitNumber = getDigitNumberFrom(num = remainStorey, digitAt = i + 1)
                    // 다음 자릿수가 4 이하이면 -10^(i-1) 버튼을 통해 내려간다.
                    if (nextDigitNumber &amp;lt;= 4) {
                        remainStorey -= makeDigitNumber(digit = i, num = digitNum)
                        answer += digitNum
                    }
                    // 다음 자릿수가 5 이상이면 +10^(i-1) 버튼을 통해 내려간다.
                    else {
                        remainStorey += makeDigitNumber(digit = i, num = 10 - digitNum)
                        answer += (10 - digitNum)
                    }
                }
            }
        }

        // 기존 storey 의 가장 큰 자릿수에서 자릿수가 하나 더 생겼을 경우
        // ex) 9 -&amp;gt; 10이 된 경우, -10 버튼을 눌러줘야 한다.
        if (remainStorey != 0 &amp;amp;&amp;amp; remainStorey % 10 == 0) answer++;

        return answer
    }

    // num * 10^(digit-1) 값 생성
    private fun makeDigitNumber(digit: Int, num: Int) = ((10).toDouble().pow(digit - 1) * num).toInt()

    // num 에서 digitAt 번째 자릿수 값 꺼냄
    private fun getDigitNumberFrom(num: Int, digitAt: Int): Int = ((num / (10).toDouble().pow(digitAt - 1)) % 10).toInt()
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>algorithm/문제풀이</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/49</guid>
      <comments>https://yoo-dev.tistory.com/49#entry49comment</comments>
      <pubDate>Fri, 30 Dec 2022 18:15:26 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 멀쩡한 사각형 (Kotlin)</title>
      <link>https://yoo-dev.tistory.com/48</link>
      <description>&lt;h1&gt;프로그래머스 : 멀쩡한 사각형 (Kotlin)&lt;/h1&gt;
&lt;h2&gt;문제&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/62048?language=kotlin&quot;&gt;문제 확인하기&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;풀이&lt;/h2&gt;
&lt;p&gt;가로 3, 세로 4 크기의 직사각형이 주어졌다고 하자.&lt;/p&gt;
&lt;p&gt;이 직사각형을 대각선 양 끝 꼭짓점을 기준으로 자르는 것을 좌표상에 표시하면 아래와 같이 표현할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/81250857/209919873-c1b3862a-26fd-40a6-891b-7a8cd66752c1.jpeg&quot; alt=&quot;좌표상에 나타낸 사각형을 자른 선&quot;&gt;&lt;/p&gt;
&lt;p&gt;양 끝 꼭짓점([0, 0]과 [3, 4])을 서로 이었을 때 나오는 대각선은 &lt;code&gt;y = (4/3) * x&lt;/code&gt; 방정식으로 표현할 수 있다.&lt;br&gt;우리는 이 대각선을 기준으로 우측에 있는 삼각형에 몇 개의 1 * 1 크기의 정사각형이 존재하는지 세고, 이 값의 2배를 정답으로 반환하면 된다.&lt;/p&gt;
&lt;p&gt;여기서 1 * 1 크기의 정사각형이 몇 개가 존재하는지 어떻게 셀 수 있을까?&lt;/p&gt;
&lt;p&gt;먼저 &lt;code&gt;x&lt;/code&gt; 좌표의 범위가 0 ~ 1인 부분을 보자.&lt;br&gt;여기는 정사각형이 존재하지 않는다.&lt;/p&gt;
&lt;p&gt;다음 &lt;code&gt;x&lt;/code&gt; 좌표의 범위가 1 ~ 2인 부분을 보자.&lt;br&gt;&lt;code&gt;x&lt;/code&gt; 좌표가 1일 때 &lt;code&gt;y&lt;/code&gt; 좌표는 4/3 이다.&lt;br&gt;&lt;code&gt;x&lt;/code&gt; 좌표가 2일 때는 확인할 필요가 없다.&lt;br&gt;&lt;code&gt;x&lt;/code&gt; 좌표가 1일 때의 &lt;code&gt;y&lt;/code&gt; 좌표 4/3 위로는, 대각선이 1 * 1 정사각형을 모두 잘라버려 더 이상 정사각형이 아니게 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/81250857/209921823-351be859-8e9e-4852-b3b1-e8cd2e05ff28.jpeg&quot; alt=&quot;x좌표 1부터 2까지 확인&quot;&gt;&lt;/p&gt;
&lt;p&gt;따라서 &lt;code&gt;x&lt;/code&gt; 좌표가 1일 때의 &lt;code&gt;y&lt;/code&gt; 좌표값을 자연수로 내림한 값 만큼의 1 * 1 크기의 정사각형이 존재하게 된다.&lt;br&gt;여기서 4/3을 자연수로 내림하면 1이 되고, 1 * 1 크기의 정사각형이 1개 존재한다.&lt;/p&gt;
&lt;p&gt;위 내용을 통해 &lt;code&gt;x&lt;/code&gt; 좌표의 범위가 2 ~ 3인 부분도 확인해보자.&lt;br&gt;&lt;code&gt;x&lt;/code&gt;좌표가 2일 때 y 좌표는 8/3 이다.&lt;br&gt;8/3 위로는 &lt;code&gt;x&lt;/code&gt; 좌표 3 이내에 정사각형이 존재하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/81250857/209921977-bd872606-0438-4b2c-8904-e0c5e3dbe2ab.jpeg&quot; alt=&quot;x좌표 2부터 3까지 확인&quot;&gt;&lt;/p&gt;
&lt;p&gt;따라서 &lt;code&gt;x&lt;/code&gt; 좌표가 2일 때의 &lt;code&gt;y&lt;/code&gt; 좌표값을 자연수로 내림한 값이 &lt;code&gt;x&lt;/code&gt; 좌표값 2에서 3 이내에 존재하는 정사각형의 개수이다.&lt;br&gt;여기서 8/3을 자연수로 내림하면 2이 되고, 1 * 1 크기의 정사각형이 2개 존재하는 것이다.&lt;/p&gt;
&lt;p&gt;이제 위 과정을 통해 구한 정사각형의 값을 모두 더하면 큰 직각삼각형 하나에 존재하는 1 * 1 정사각형의 개수(&lt;code&gt;sum&lt;/code&gt;)가 된다.&lt;/p&gt;
&lt;p&gt;3 * 4 크기의 직사각형을 대각선으로 잘랐기 때문에 이 직각삼각형이 2개 존재하게 되므로, &lt;code&gt;sum * 2&lt;/code&gt;가 문제가 요구하는 정답이 된다.&lt;/p&gt;
&lt;p&gt;아래는 코틀린으로 작성한 코드이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Solution {
    fun solution(w: Int, h: Int): Long {
        return (0L until w.toLong()).reduce { total, i -&amp;gt; total + (h.toLong() * i) / w } * 2
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>algorithm/문제풀이</category>
      <author>yoo97</author>
      <guid isPermaLink="true">https://yoo-dev.tistory.com/48</guid>
      <comments>https://yoo-dev.tistory.com/48#entry48comment</comments>
      <pubDate>Thu, 29 Dec 2022 17:15:16 +0900</pubDate>
    </item>
  </channel>
</rss>