<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Lifealong</title>
    <link>https://0soo.tistory.com/</link>
    <description>백엔드 개발을 좋아합니다.
java kotlin spring, infra 에 관심이 많습니다.

email : kim206gh@naver.com

github : https://github.com/devysk</description>
    <language>ko</language>
    <pubDate>Wed, 10 Jun 2026 09:00:55 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ysk(0soo)</managingEditor>
    <image>
      <title>Lifealong</title>
      <url>https://tistory1.daumcdn.net/tistory/5390034/attach/567b47a0efe5417fb9ffae25de7e338d</url>
      <link>https://0soo.tistory.com</link>
    </image>
    <item>
      <title>nginx를 이용한 Node exporter basic auth (feat.prometheus)</title>
      <link>https://0soo.tistory.com/262</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Node-exporter는 Prometheus 모니터링에 쓰이는 컴포넌트중 하나입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 서버를 운영하다 보면, 하드웨어및 OS 리소스 등을 모니터링 해야 할 필요가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그라파나-프로메테우스로 모니터링 하는 환경이라면 Node-Exporter를 사용하여 시스템 메트릭을 수집할 수 있습니다.&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;그런데, VPC등으로 네트워크 보안 등을 제한하지 않은 상태에서 그라파나나 프로메테우스로 메트릭을 수집하게 되면 이 경우, 누구나 접근 가능한 엔드포인트를 통해 중요한 서버 메트릭을 조회할 수 있게 되어, 보안에 취약점이 발생할 수 있습니다.&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;VPC나 방화벽 등을 사용할 수 없을 때, 다른 네트워크에 있는 Node-Exporter 등과 같은 컴포넌트의 정보를 가져와야 한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;nginx basic auth로 프록시를 이용하여 NodeExporter의 메트릭 요청에 basic auth를 설정하여 메트릭을 수집할 수 있는 방법입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node-exporter와 nginx를 docker-compose로 컨테이너로 같이 실행시킨다는 상황을 가정합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선, Nginx를 설치하고, Node-Exporter를 위한 프록시 서버를 설정합니다. 이 과정에서 Nginx에 Basic Auth 인증을 추가하여, 메트릭 데이터에 접근을 제한할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;htpasswd&lt;/code&gt; 명령어를 사용하여 인증에 필요한 사용자명과 비밀번호를 담은 파일을 생성하여 nginx basic auth에 이용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Nginx 프록시 설정을 통해, Node-Exporter 엔드포인트로의 모든 요청이 Basic Auth 인증을 거치도록 합니다. 이렇게 하면, 인증되지 않은 접근을 차단할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법을 통해, VPC나 방화벽을 사용할 수 없는 상황에서도 Node-Exporter와 같은 모니터링 컴포넌트의 보안을 강화할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;1. node-exporter와 nginx docker-compose 작성&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    ports:
      - 9101:9100 # 포트 충돌 방지 위해 9101로 설정합니다.
  nginx:
    image: nginx:latest
    container_name: nginx-proxy
    restart: unless-stopped
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;&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;command 명령은 리눅스 시스템 디렉토리를 지정한것이기 때문에 절대 수정하면 안됩니다. 시스템 메트릭을 수집 못할수도 있습니다.&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/&quot;&gt;https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-prometheus/prometheus-config-examples/docker-compose-linux/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. prometheus 설정 config.yml&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prometheus.config를 다음과 같이 지정합니다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;global:
  scrape_interval: 15s     # scrap target의 기본 interval을 15초로 변경 / default = 1m
  scrape_timeout: 15s      # scrap request 가 timeout waite/ default = 10s

  external_labels:
    monitor: 'monitor'       # 기본적으로 붙여줄 라벨
  query_log_file: query_log_file.log # prometheus의 쿼리 로그들을 기록. 설정되지않으면 기록하지않는다.

# 매트릭을 수집할 엔드포인드로 여기선 Prometheus 서버 자신을 가리킨다.
scrape_configs:

 # 이 설정에서 수집한 타임시리즈에 `job=&amp;lt;job_name&amp;gt;`으로 잡의 이름을 설정.
 # metrics_path의 기본 경로는 '/metrics'이고 scheme의 기본값은 `http`다

  - job_name: 'my-node-exporter'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['ip:9100']
    basic_auth:
      username: '유저명'
      password: '비밀번호'&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;메트릭을 수집하기 위해 node-exporter가 띄워져있는 서버의 주소와 basicauth를 사용할 유저명:패스워드를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. nginx 암호 설정을 위한 basic-auth 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 httpd-tools를 다운로드 받습니다&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;htpasswd&lt;/code&gt;는 basic auth를 위한 패스워드 파일을 생성하고 관리하는 데 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;sudo yum install httpd-tools&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로, .htpasswd 파일을 생성하고 사용자를 추가합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 basic auth에 사용할 유저명, 비밀번호를 지정합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;sudo htpasswd -c /etc/nginx/.htpasswd '유저명' # '빼도 된다
~이후 비밀번호 지정 &lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4. nginx 설정파일 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;htpasswd로 생성한 아이디 패스워드 파일을 nginx 설정파일을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
    listen 9100; # Basic Auth를 적용할 포트

    location / {
        auth_basic &quot;Protected Node Exporter&quot;;
        auth_basic_user_file /etc/nginx/.htpasswd; # 위에서 htpasswd로 생성한 .htpasswd 파일 

        # Node Exporter로 프록시
        proxy_pass http://localhost:9101; # node_exporter가 실행되고 있는 포트 
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}&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;node-exporter를 9101에 띄워서 nginx를 통해 baiscauth를 진행하고 우회시키기 위한 설정입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx 재실행&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;sudo systemctl restart nginx
# 또는
sudo systemctl reload nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx를 재실행하게 되면 지정한 password와 basic auth 적용이 완료됩니다.&lt;/p&gt;
&lt;h1&gt;5. nginx에 인증 설정 완료 후 프로메테우스 재실행.&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;global:
  scrape_interval:     5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: 'my-node-exporter'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['ip:9100']
    basic_auth:
      username: '유저명'
      password: '비밀번호'&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;위 promerthues.config에서 설정하지 않았다면 지금 지정합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prometheus 재시작&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker-compose restart prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 promethues를 재실행하게되면 9100 포트를 통해 nginx에서 basic auth를 진행하게 되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 통과된다면 9101 포트의 node-exporter로 접근하여 메트릭을 가져올 수 있게 됩니다.&lt;/p&gt;
&lt;div id=&quot;mttContainer&quot; class=&quot;notranslate&quot; style=&quot;transform: translate(862px, 447px);&quot; aria-expanded=&quot;false&quot;&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>Infra</category>
      <category>nginx basic auth</category>
      <category>node exporter basic auth</category>
      <category>promethues basic auth</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/262</guid>
      <comments>https://0soo.tistory.com/262#entry262comment</comments>
      <pubDate>Sat, 30 Mar 2024 00:34:58 +0900</pubDate>
    </item>
    <item>
      <title>Java 가상 스레드(Virtual Thread) :  SpringBoot에서 사용하기 -3</title>
      <link>https://0soo.tistory.com/261</link>
      <description>&lt;h1&gt;SpringBoot With Virtual Thread&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;springboot 3.2 + 자바 21 버전부터 virtual thread를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;property 설정으로 AutoCofngiruation을 통해 활성화 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# virtual thread enabled/disabled
spring.threads.virtual.enabled=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 조건을 활성화하게되면, SpringBoot Webserver AutoConfiguration에서 기본 스레드풀 대신 가상 스레드 풀을 이용한 톰캣, 제티 등이 활성화가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@AutoConfiguration
@ConditionalOnNotWarDeployment
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
    public static class TomcatWebServerFactoryCustomizerConfiguration {

        @Bean
        public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
            ServerProperties serverProperties) {
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
        }

        @Bean
        @ConditionalOnThreading(Threading.VIRTUAL)
        TomcatVirtualThreadsWebServerFactoryCustomizer tomcatVirtualThreadsProtocolHandlerCustomizer() {
            return new TomcatVirtualThreadsWebServerFactoryCustomizer();
        }

    }

}&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;Redis, Kafka등 많은 AutoConfigurationClass도 활성화 여부에 따라 가상 스레드를 풀로 사용해요.&lt;/li&gt;
&lt;li&gt;LettuceConnectionConfiguration, KafkaAnnotationDrivenConfiguration&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 모델 조건에 따라 빈을 다르게 생성하기 위한 Condition 어노테이션과 구현체가 추가되어서, 해당 Condition으로 확인하고 스레드풀을 다르게 등록합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;/**
 * {@link Conditional @Conditional} that matches when the specified threading is active.
 *
 * @author Moritz Halbritter
 * @since 3.2.0
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnThreadingCondition.class)
public @interface ConditionalOnThreading {

    /**
     * The {@link Threading threading} that must be active.
     * @return the expected threading
     */
    Threading value();

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 다음처럼 Virutal Thread Executor 전용 Bean을 선언할 수 있게되었습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
public class ExecutorServiceConfig {

    @Bean
    @ConditionalOnThreading(Threading.VIRTUAL)
    public ExecutorService virtualThreadExecutor(){
        return Executors.newVirtualThreadPerTaskExecutor();
    }

    @Bean
    @ConditionalOnThreading(Threading.PLATFORM)
    public ExecutorService platformThreadExecutor(){
        return Executors.newCachedThreadPool();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 가상 스레드 이름을 지정하고 싶다면?&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; @Bean
 @ConditionalOnThreading(Threading.VIRTUAL)
 public ExecutorService virtualThreadExecutor() {
     ThreadFactory factory = Thread.ofVirtual().name(&quot;my-virtual&quot;).factory();

     return Executors.newThreadPerTaskExecutor(factory);
 }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Thread.ofVirtual().start(() -&amp;gt; {
    try {
        예외가 발생할 수 있는 로직 
    } catch (Exception e) {
        // 예외 처리 로직
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. UncaughtExceptionHandler 이용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UncaughtExceptionHandler 객체 인자를 받는 메소드를 제공해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 객체는 아래 인터페이스에요&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;/**
 * {@code Thread}가 잡히지 않은 예외로 인해 갑자기 종료될 때 호출되는 핸들러를 정의합니다.
 * 스레드가 잡히지 않은 예외로 인해 종료될 때, 자바 가상 머신은 스레드에 대한 {@code UncaughtExceptionHandler}를
 * 를 사용하여 조회하고 핸들러의 {@code uncaughtException} 메소드를 호출하며, 스레드와 예외를
 * 인자로 전달합니다.
 * 스레드가 {@code UncaughtExceptionHandler}를
 * 명시적으로 설정하지 않은 경우, 해당 스레드의 {@code ThreadGroup} 객체가 그 역할을 합니다. 
 * {@code ThreadGroup} 객체가 예외를 다루는 특별한 요구사항이 없으면, getDefaultUncaughtExceptionHandler로 호출을 전달할 수 있습니다.
 */

@FunctionalInterface
public interface UncaughtExceptionHandler {
  /**
  * 주어진 스레드가 주어진 잡히지 않은 예외로 인해 종료될 때 호출되는 메소드입니다.
  * @param t 스레드
  * @param e 예외
  */
  void uncaughtException(Thread t, Throwable e);  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 핸들링하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Thread.ofVirtual().unstarted(() -&amp;gt; System.out.println(&quot;Virtual thread&quot;))
        .setUncaughtExceptionHandler((t, e) -&amp;gt; System.err.println(&quot;Uncaught exception in thread &quot; + t.getName() + &quot;: &quot; + e.getMessage()));

// or

Thread virtualThread = Thread.ofVirtual().unstarted(() -&amp;gt; {
    // 예외가 발생할 수 있는 경우 
});

virtualThread.setUncaughtExceptionHandler((t, e) -&amp;gt; {
    // 예외 처리 로직
});

virtualThread.start();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. CustomThreadFactory 이용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ExecutorService&lt;/code&gt;에 가상 스레드를 생성하면서 각 스레드에 대해 공통적인 UncaughtExceptionHandler를 사용하므로 일관되게 구현할 수 있어요.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class VirtualThreadWithExceptionHandler {

    public static void main(String[] args) {
        // 커스텀 ThreadFactory 구현
        ThreadFactory customThreadFactory = task -&amp;gt; {
            Thread thread = Thread.ofVirtual().start(task); // 가상 스레드 생성
            thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println(&quot;Uncaught exception in thread: &quot; + t.getName() + &quot;, error: &quot; + e.getMessage());
                }
            });
            return thread;
        };

        // 커스텀 ThreadFactory를 사용하여 ExecutorService 생성
        ExecutorService executor = Executors.newThreadPerTaskExecutor(customThreadFactory);

        // 예외를 발생시키는 작업 제출
        executor.submit(() -&amp;gt; {
            System.out.println(&quot;This will throw a runtime exception&quot;);
            throw new RuntimeException(&quot;Example exception&quot;);
        });

        // ExecutorService 종료
        executor.shutdown();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Async 어노테이션을 이용한 비동기 작업에 가상 스레드 사용하기 - AsyncConfig&lt;/h2&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = &quot;virtualThreadExecutor&quot;)
    public Executor getAsyncExecutor() {
        ThreadFactory factory = Thread.ofVirtual().name(&quot;virtual-thread&quot;, 1)
            .uncaughtExceptionHandler(
                (t, e) -&amp;gt; System.err.println(&quot;Uncaught exception in thread &quot; + t.getName() + &quot;: &quot; + e.getMessage()))
            .factory(); // 1은 시작 넘버

        return Executors.newThreadPerTaskExecutor(factory);
    }

    @Bean
    public AsyncTaskExecutor applicationTaskExecutor() {
        return new TaskExecutorAdapter(getAsyncExecutor());
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }

    public static class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

        @Override
        public void handleUncaughtException(Throwable throwable, Method method, Object... params) {
            System.err.println(&quot;Exception Name - &quot; + throwable.getClass().getName());
            System.err.println(&quot;Exception message - &quot; + throwable.getMessage());
            System.err.println(&quot;Method name - &quot; + method.getName());

            for (Object param : params) {
                System.err.println(&quot;Parameter value - &quot; + param);
            }

            try {
                throw (Exception) throwable;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            //  추가적인 예외 처리 로직을 구현할 수 있습니다. 예를 들어, 애플리케이션 이벤트를 발행하거나, 알림을 전송할 수 있습니다.
            // eventPublisher.publishEvent(new AsyncErrorEvent(throwable));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RestClient에 가상 스레드 팩토리 지정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring 3.2에 나온 RestClient에도 다음처럼 가상 스레드를 지정하여 사용 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;@Value(&quot;${spring.threads.virtual.enabled}&quot;)
private boolean isVirtualThreadEnabled;

private RestClient buildRestClient(String baseUrl) {
    log.info(&quot;base url: {}&quot;, baseUrl);
    var builder = RestClient.builder()
        .baseUrl(baseUrl);

    if (isVirtualThreadEnabled) {
        builder = builder.requestFactory(new JdkClientHttpRequestFactory(
            HttpClient.newBuilder()
                .executor(Executors.newVirtualThreadPerTaskExecutor())
                .build()
        ));
    }
    return builder.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여러 외부 api를 동시에 호출하기&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class HealthCheckService {

    private static final Logger log = LoggerFactory.getLogger(HealthCheckService.class);
    private final PatientRecordServiceClient patientRecordServiceClient;
    private final AppointmentServiceClient appointmentServiceClient;
    private final MedicationServiceClient medicationServiceClient;

    @Qualifier(&quot;virtualThreadExecutor&quot;)
    private final ExecutorService executor;

    public HealthCheckReport getHealthCheckReport(String patientId){
        var patientRecords = this.executor.submit(() -&amp;gt; this.patientRecordServiceClient.getPatientRecords(patientId));
        var appointments = this.executor.submit(() -&amp;gt; this.appointmentServiceClient.getAppointments(patientId));
        var medications = this.executor.submit(() -&amp;gt; this.medicationServiceClient.getMedications(patientId));

        return new HealthCheckReport(
                patientId,
                getOrElse(patientRecords, Collections.emptyList()),
                getOrElse(appointments, Collections.emptyList()),
                getOrElse(medications, Collections.emptyList())
        );
    }

    private &amp;lt;T&amp;gt; T getOrElse(Future&amp;lt;T&amp;gt; future, T defaultValue){
        try {
            return future.get();
        } catch (Exception e) {
            log.error(&quot;error&quot;, e);
        }
        return defaultValue;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;건강 관리 시스템에서 여러 외부 api를 호출하는 예제에요.&lt;br /&gt;가상스레드는 기존 플랫폼스레드보다 가볍기때문에, 이렇게 동시 여러 I/O작업을 하는데 용이합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getOrElse라는 서포트 메소드를 이용해서 예외를 핸들링 할 수도 있고, ThreadFactory를 이용해서 공통된 ExceptionHandler를 적용할수도 있어요.&lt;/p&gt;
&lt;h1&gt;결론&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 가상 스레드를 도입함으로써,멀티 스레드 프로그래밍에 더 유연해지고 처리량이 높은 애플리케이션을 구현할 수 있게 되었습니다.&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;CPU Bound 작업에서는 동일하다고 볼 수 있습니다.&lt;br /&gt;주의할점을 지켜가면서 개발한다면, 처리량이 높은 애플리케이션을 개발하는것에 도움이 된다고 생각합니다.&lt;/li&gt;
&lt;li&gt;syncronized, 가상스레드풀링, 스레드로컬 마구 사용 등등&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&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://docs.oracle.com/en/java/javase/21&quot;&gt;https://docs.oracle.com/en/java/javase/21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog&quot;&gt;https://spring.io/blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-6-virtual-threads&quot;&gt;https://www.baeldung.com/spring-6-virtual-threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&quot;&gt;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/1203723&quot;&gt;https://d2.naver.com/helloworld/1203723&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/java-virtual-thread/?couponCode=KEEPLEARNING&quot;&gt;https://www.udemy.com/course/java-virtual-thread/?couponCode=KEEPLEARNING&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://0soo.tistory.com/259&quot;&gt;Java 가상 스레드(Virtual Thread)의 이해: 종류, 설정, 사용법 - 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://0soo.tistory.com/260&quot;&gt;Java 가상 스레드(Virtual Thread)의 이해: 주의할점, Scope Value, 구조화된 동시성 - 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/Java</category>
      <category>VirtualThread Springboot</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/261</guid>
      <comments>https://0soo.tistory.com/261#entry261comment</comments>
      <pubDate>Fri, 29 Mar 2024 23:49:50 +0900</pubDate>
    </item>
    <item>
      <title>Java 가상 스레드(Virtual Thread)의 이해:  주의할점, Scope Value, 구조화된 동시성 -2</title>
      <link>https://0soo.tistory.com/260</link>
      <description>&lt;h1&gt;가상 스레드 풀 사용시 주의할점.&lt;/h1&gt;
&lt;p&gt;가상 스레드가 가볍다고 해서 무조건 좋은것은 아닙니다. 다음과 같은 내용을 주의해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가상 스레드의 스레드풀을 사용할때에는 고정 풀 을 사용하면 안된다.&lt;/li&gt;
&lt;li&gt;동시성을 제어하기 위해서 synchronized 키워드 대신 Lock을 사용하자&lt;/li&gt;
&lt;li&gt;스레드 로컬에 용량이 큰 객체를 저장하지 말자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. 가상 스레드의 스레드풀을 사용할때에는 고정 풀을 사용하면 안된다.&lt;/h2&gt;
&lt;p&gt;가상 스레드는 고정된 풀 (newFixedThreadPool)을 사용하면 안됩니다.&lt;/p&gt;
&lt;p&gt;지정된 한도 이내에 더 많은 가상 스레드를 만들지 못하기 때문입니다.&lt;/p&gt;
&lt;p&gt;또한 1회용이기 때문에 풀에 담아 사용하지 말고, 그냥 생성해서 사용하는게 좋습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-7F5DA570-4B24-4CF6-899C-0424464B6032&quot;&gt;https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-7F5DA570-4B24-4CF6-899C-0424464B6032&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스레드 실행 수를 제한하고 싶으면, semaphore를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.concurrent.Semaphore;

public class ConcurrencyLimiter implements AutoCloseable {

    private static final Logger log = LoggerFactory.getLogger(ConcurrencyLimiter.class);

    private final ExecutorService executor;
    private final Semaphore semaphore;
    private final Queue&amp;lt;Callable&amp;lt;?&amp;gt;&amp;gt; queue;

    public ConcurrencyLimiter(ExecutorService executor, int limit) {
        this.executor = executor;
        this.semaphore = new Semaphore(limit);
        this.queue = new ConcurrentLinkedQueue&amp;lt;&amp;gt;();
    }

    public &amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(Callable&amp;lt;T&amp;gt; callable) {
        this.queue.add(callable);
        return executor.submit(() -&amp;gt; executeTask());
    }

    private &amp;lt;T&amp;gt; T executeTask() {
        try {
            semaphore.acquire();

            return (T)this.queue
                .poll()
                .call();

        } catch (Exception e) {
            log.error(&amp;quot;error&amp;quot;, e);
        } finally {
            semaphore.release();
        }
        return null;
    }

    @Override
    public void close() throws Exception {
        this.executor.close();
    }
}


public class ConcurrencyLimitWithSemaphore {

    private static final Logger log = LoggerFactory.getLogger(ConcurrencyLimitWithSemaphore.class);

    public static void main(String[] args) throws Exception {
        var factory = Thread.ofVirtual().name(&amp;quot;vins&amp;quot;, 1).factory();
        var limiter = new ConcurrencyLimiter(Executors.newThreadPerTaskExecutor(factory), 3);
      // 3회로 제한한다. 
        execute(limiter, 200);
    }

    private static void execute(ConcurrencyLimiter concurrencyLimiter, int taskCount) throws Exception {
        try(concurrencyLimiter){
            for (int i = 1; i &amp;lt;= taskCount; i++) {
                int j = i;
                concurrencyLimiter.submit(() -&amp;gt; printProductInfo(j));
            }
            log.info(&amp;quot;submitted&amp;quot;);
        }
    }

    // 3rd party service
    // contract: 3 concurrent calls are allowed
    private static String printProductInfo(int id){
        var product = Client.getProduct(id);
        log.info(&amp;quot;{} =&amp;gt; {}&amp;quot;, id, product);
        return product;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;var limiter = new ConcurrencyLimiter(Executors.newThreadPerTaskExecutor(factory), 3);&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동시 호출 수를 3회로 제한한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 동시성을 제어하기 위해서 synchronized 키워드 대신 Lock을 사용하자&lt;/h2&gt;
&lt;h3&gt;Pinning Thread(고정된 스레드) 문제&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Pinning&lt;/code&gt; 쓰레드는 가상 스레드가 I/O 작업이나 기타 블로킹 연산을 수행할 때 발생하는 현상을 가리킵니다.&lt;/p&gt;
&lt;p&gt;Virtual Thread가 플랫폼 스레드에 고정되어 &lt;strong&gt;장점을 활용할 수 없는 경우가 있습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;바로 Virtual Thread 내에서 synchronized block을 사용하거나, JNI를 통해 네이티브 메서드를 사용하는 경우입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-704A716D-0662-4BC7-8C7F-66EE74B1EDAD&quot;&gt;https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-704A716D-0662-4BC7-8C7F-66EE74B1EDAD&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일반적으로 가상 스레드는 실행 중인 작업이 블로킹 상태가 되면, 그 스레드를 실행 중인 캐리어 스레드에서 분리하여 다른 작업을 수행할 수 있게 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;synchronized&lt;/code&gt; 블록 내부에서 블로킹 I/O 작업을 수행하면, 해당 작업이 완료될 때까지 가상 스레드가 캐리어 스레드에서 분리되지 않고 고정되어 처리 능력에 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 가상 스레드를 사용할 때는 블로킹 I/O 작업이나 &lt;code&gt;synchronized&lt;/code&gt; 블록의 사용을 신중하게 고려해야 하며, 가능한 경우 논블로킹 I/O 작업을 수행하거나, 동시성을 관리하기 위해 &lt;code&gt;java.util.concurrent&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;SpringBoot와 자바 21을 이용한다면, 실제 내가 끌어다 쓰는 프레임 워크 내부 구현이 Synchronized로 선언되어있는지 확인하고 사용하는것이 좋습니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;가상 스레드 사용시 타 라이브러리가 이렇게 막혀있다면 어떻게 미리 탐지하여 pinning을 막을 수 있을까?&lt;/h3&gt;
&lt;p&gt;리액터의 BlockHound처럼 가상스레드에서도 미리 pinning을 방지할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Synchronization {

    // Use this to check if virtual threads are getting pinned in your application
    static {
        System.setProperty(&amp;quot;jdk.tracePinnedThreads&amp;quot;, &amp;quot;short&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;아래와같이 로깅이 되서 추적해서 잡을 수 있게됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread[#80,ForkJoinPool-1-worker-10,5,CarrierThreads]
    com.ys.example.ioTask(Synchronization.java:47) &amp;lt;== monitors:1&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;syncronized 대신 동시 접근을 방지하는법&lt;/h3&gt;
&lt;p&gt;synchronized보다는 ReentrantLock을 권장합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ReentrantLock&lt;/code&gt;과 달리, &lt;code&gt;synchronized&lt;/code&gt; 키워드는 내부적으로 스레드를 OS 레벨의 락에 묶어버리는데, 이렇게되면 pinning이 발생하기 때문에 캐리어 스레드가 묶여 스케줄링 하지 못하기 때문입니다.&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;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;ReentrantLock의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;synchronized&lt;/code&gt;보다 유연성을 제공합니다.&lt;/li&gt;
&lt;li&gt;공정성을 지원합니다.&lt;ul&gt;
&lt;li&gt;더 오래 기다린 스레드가 잠금을 획득할 기회를 얻습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;타임아웃이 있는 tryLock 을 지원합니다.&lt;ul&gt;
&lt;li&gt;스레드가 잠금을 획득하기 위해 대기할 수 있는 최대 시간을 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래처럼 Lock을 사용하면 동시 접근에 대한 정합성이 보장됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ReentrantLock {

    private static final Logger log = LoggerFactory.getLogger(ReentrantLock.class);
    private static final Lock lock = new ReentrantLock();
    private static final List&amp;lt;Integer&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();

    public static void main(String[] args) {

        demo(Thread.ofVirtual());

        CommonUtils.sleep(Duration.ofSeconds(2));

        log.info(&amp;quot;list size: {}&amp;quot;, list.size());
    }

    private static void demo(Thread.Builder builder){
        for (int i = 0; i &amp;lt; 50; i++) {
            builder.start(() -&amp;gt; {
                log.info(&amp;quot;Task started. {}&amp;quot;, Thread.currentThread());
                for (int j = 0; j &amp;lt; 200; j++) {
                    inMemoryTask();
                }
                log.info(&amp;quot;Task ended. {}&amp;quot;, Thread.currentThread());
            });
        }
    }

    private static void inMemoryTask(){
        try{
            lock.lock();
            list.add(1);
        }catch (Exception e){
            log.error(&amp;quot;error&amp;quot;, e);
        }finally {
            lock.unlock();
        }
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;3. 스레드 로컬에 용량이 큰 객체를 저장하지 말자.&lt;/h2&gt;
&lt;h3&gt;가상 스레드와 스레드 로컬 그리고 Scope Value&lt;/h3&gt;
&lt;p&gt;가상 스레드도 스레드 로컬을 사용할 수 있으며 가상 스레드의 자식 가상 스레드도 스레드 로컬이 전파됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ThreadLocal {

    private static final Logger log = LoggerFactory.getLogger(ThreadLocal.class);
    private static final ThreadLocal&amp;lt;String&amp;gt; SESSION_TOKEN = new ThreadLocal&amp;lt;&amp;gt;();

    public static void main(String[] args) {

        Thread.ofVirtual().name(&amp;quot;virtual-1&amp;quot;).start( () -&amp;gt; processIncomingRequest());
        Thread.ofVirtual().name(&amp;quot;virtual-2&amp;quot;).start( () -&amp;gt; processIncomingRequest());

        CommonUtils.sleep(Duration.ofSeconds(1));
    }

    private static void processIncomingRequest(){
        authenticate();
        controller();
    }

    private static void authenticate(){
        var token = UUID.randomUUID().toString();
        log.info(&amp;quot;token={}&amp;quot;, token);
        SESSION_TOKEN.set(token);
    }

    private static void controller(){
        log.info(&amp;quot;controller: {}&amp;quot;, SESSION_TOKEN.get());
        service();
    }

    private static void service(){
        log.info(&amp;quot;service: {}&amp;quot;, SESSION_TOKEN.get());
        var threadName = &amp;quot;child-of-&amp;quot; + Thread.currentThread().getName();
        Thread.ofVirtual().name(threadName).start(ThreadLocal::callExternalService);
    }

    // This is a client to call external service
    private static void callExternalService(){
        log.info(&amp;quot;preparing HTTP request with token: {}&amp;quot;, SESSION_TOKEN.get());
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;결과&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// here
00:11:01.874 [virtual-1] INFO com.ys.ThreadLocal -- token=03dca3e7-9c5e-4b8c-a535-0cf4b7a8655f

00:11:01.874 [virtual-2] INFO com.ys.ThreadLocal -- token=b56e4b9a-3569-4525-a5fc-bf1d1cf11807
00:11:01.876 [virtual-2] INFO com.ys.ThreadLocal -- controller: b56e4b9a-3569-4525-a5fc-bf1d1cf11807

// here
00:11:01.876 [virtual-1] INFO com.ys.ThreadLocal -- controller: 03dca3e7-9c5e-4b8c-a535-0cf4b7a8655f
00:11:01.876 [virtual-2] INFO com.ys.ThreadLocal -- service: b56e4b9a-3569-4525-a5fc-bf1d1cf11807

//here
00:11:01.876 [virtual-1] INFO com.ys.ThreadLocal -- service: 03dca3e7-9c5e-4b8c-a535-0cf4b7a8655f

//here
00:11:01.877 [child-of-virtual-1] INFO com.ys.ThreadLocal -- preparing HTTP request with token: 03dca3e7-9c5e-4b8c-a535-0cf4b7a8655f
00:11:01.877 [child-of-virtual-2] INFO com.ys.ThreadLocal -- preparing HTTP request with token: b56e4b9a-3569-4525-a5fc-bf1d1cf11807&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;자식스레드에서 스레드 로컬에 무슨 짓을 하더라도 부모 스레드로는 전파되지 않습니다. 자식스레드로 사본을 제공하기 때문입니다.&lt;/p&gt;
&lt;p&gt;그런데, 문제는 너무 복제가 많아지면 메모리나 관리적에서 힘들 수 있어 가상 스레드를 사용한 코드에서 스레드 로컬은 지양하는것이 좋습니다.&lt;/p&gt;
&lt;p&gt;이 문제를 자바에서는 Scope Value란것을 도입해서 해결하려고 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Scope Value&lt;/h2&gt;
&lt;p&gt;스코프 벨류란, 범위가 지정된 값을 사용하는 것입니다.&lt;/p&gt;
&lt;p&gt;스레드로컬의 문제에서 보면, 복제된 스레드가 상위 스레드의 스레드 로컬 정보를 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;서로 다른 스레드에는 서로 다른 데이터가 필요할 수 있으며 다른 스레드가 소유한 데이터에 액세스하거나 재정의할 수 없어야 하는데, 스레드 로컬은 가능합니다.&lt;/p&gt;
&lt;p&gt;이로인한 스레드로컬의 문제는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;첫째, 모든 스레드-로컬 변수는 변경 가능하며, 어떤 코드에서든 언제든지 setter 메소드를 호출할 수 있습니다. 따라서, 데이터는 컴포넌트 사이에서 어떤 방향으로든 흐를 수 있어&lt;strong&gt;, 어떤 컴포넌트가 공유 상태를 업데이트하는지와 그 순서를 이해하기 어렵게 만듭니다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;둘째, set 메소드를 사용하여 스레드의 인스턴스를 작성할 때, 데이터는 스레드의 전체 수명 동안 혹은 스레드가 remove 메소드를 호출할 때까지 유지됩니다. 사용을 다 하고, remove 메소드를 호출하는 것을 잊어버리면, &lt;strong&gt;데이터는 필요 이상으로 메모리에 유지됩니다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;마지막으로, 부모 스레드의 스레드로컬 변수는 자식 스레드에 의해 상속될 수 있습니다. 부모 스레드로컬 변수를 상속하는 자식 스레드를 생성할 때, 새 스레드는 모든 부모 스레드로컬 변수에 대한 추가 저장 공간을 할당해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Scope Value를 사용하면 메서드 인수를 사용하지 않고도 &lt;strong&gt;변경할 수 없는 데이터를 안전하고 효율적으로 공유 할 수 있습니다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;사용법 측면에서는 스레드 로컬과 비슷합니다.&lt;/p&gt;
&lt;p&gt;scope value는 스레드당 하나씩 여러 형태로 사용합니다. 스레드로컬 변수와 유사하게, Scope Values은 스레드마다 하나씩 여러 incarnation을 사용합니다.&lt;/p&gt;
&lt;p&gt;그리고 보통 public static 필드로 선언되어 많은 컴포넌트에서 쉽게 접근할 수 있습니다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final static ScopedValue&amp;lt;User&amp;gt; LOGGED_IN_USER = ScopedValue.newInstance();&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;반면에, 스코프 값은 한 번 작성되면 변경할 수 없습니다. 스코프 값은 스레드 실행의 제한된 기간 동안만 사용할 수 있습니다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ScopedValue.where(LOGGED_IN_USER, user.get()).run(
  () -&amp;gt; service.getData()
);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;where&lt;/code&gt; 메소드는 Scope Value값을 받아 바인딩될 객체를 필요로 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;run&lt;/code&gt; 메소드를 호출할 때, 스코프 값은 바인딩되어 현재 스레드에 고유한 인카네이션을 생성한 다음, 람다 함수가 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;run&lt;/code&gt; 메소드의 수명 동안, 표현식에서 직접적으로나 간접적으로 호출된 어떤 메소드라도 스코프 값을 읽을 수 있게되는데, , &lt;code&gt;run&lt;/code&gt; 메소드가 끝나면 바인딩은 해제됩니다.&lt;/p&gt;
&lt;p&gt;즉 Scope Value의 제한된 수명과 불변성은 스레드 동작에 대한 추론을 단순화하는 데 도움을 줍니다.&lt;/p&gt;
&lt;p&gt;결국 여러 컴포넌트에서 이리저리 데이터가 흐르는 것이 아닌, 데이터는 한 방향으로만 전달되어 관리 및 추적을 용이하게 해요.&lt;/p&gt;
&lt;h3&gt;scope value의 바인딩 문제&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ScopedValue&lt;/code&gt;의 바인딩은 &lt;code&gt;runWhere&lt;/code&gt;에 전달된 람다 표현식의 실행 컨텍스트 내에서만 유효하며, 이 컨텍스트를 벗어난 후에는 더 이상 유효하지 않습니다. 범위 밖에서 사용하면 예외가 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;아래 예제를 봅시다&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ScopedValues {

    private static final Logger log = LoggerFactory.getLogger(ScopedValues.class);
    private static final ScopedValue&amp;lt;String&amp;gt; SESSION_TOKEN = ScopedValue.newInstance();

    public static void main(String[] args) {

        log.info(&amp;quot;isBound={}&amp;quot;, SESSION_TOKEN.isBound());
        log.info(&amp;quot;value={}&amp;quot;, SESSION_TOKEN.orElse(&amp;quot;default value&amp;quot;));

        Thread.ofVirtual().name(&amp;quot;1&amp;quot;).start( () -&amp;gt; processIncomingRequest());
        // Thread.ofVirtual().name(&amp;quot;2&amp;quot;).start( () -&amp;gt; processIncomingRequest());

        CommonUtils.sleep(Duration.ofSeconds(1));
    }

    private static void processIncomingRequest(){
        var token = authenticate();

        ScopedValue.runWhere(SESSION_TOKEN, token, () -&amp;gt; controller());

        System.out.println(&amp;quot;good : &amp;quot; + SESSION_TOKEN.get()); // 예외 발생
         //controller(); // 여기서도 예외 발생 
    }

    private static String authenticate(){
        var token = UUID.randomUUID().toString();
        log.info(&amp;quot;token={}&amp;quot;, token);
        return token;
    }

    // @Principal
    private static void controller(){
        log.info(&amp;quot;controller: {}&amp;quot;, SESSION_TOKEN.get());
        service();
    }

    private static void service(){
        log.info(&amp;quot;service: {}&amp;quot;, SESSION_TOKEN.get());
        ScopedValue.runWhere(SESSION_TOKEN, &amp;quot;new-token-&amp;quot; + Thread.currentThread().getName(), () -&amp;gt; callExternalService());
        System.out.println(&amp;quot;service end&amp;quot;);
    }

    // This is a client to call external service
    private static void callExternalService(){
        log.info(&amp;quot;preparing HTTP request with token: {}&amp;quot;, SESSION_TOKEN.get());
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;ScopedValue&lt;/code&gt;는 특정한 스레드에서만 잠시 동안 사용할 수 있는 변수입니다. 이 변수는 설정한 스레드 내에서만 값을 가지고 있고, 그 스레드의 작업이 끝나면 그 값은 사라지게 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여기서 &lt;code&gt;ScopedValue.runWhere&lt;/code&gt;라는 메서드를 사용하면, 그 메서드 안에서만 &lt;code&gt;SESSION_TOKEN&lt;/code&gt; 변수에 특정한 값을 &amp;quot;임시로&amp;quot; 할당할 수 있습니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ScopedValue.runWhere&lt;/code&gt; 메서드를 사용하여 &lt;code&gt;SESSION_TOKEN&lt;/code&gt;의 범위를 지정된 람다 표현식(&lt;code&gt;() -&amp;gt; controller()&lt;/code&gt;) 실행 동안에만 바인딩합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ScopedValue.runWhere(SESSION_TOKEN, token, () -&amp;gt; controller());&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 바인딩은 &lt;code&gt;runWhere&lt;/code&gt; 메서드에 의해 생성된 람다 표현식의 실행 컨텍스트 내에서만 유효합니다.&lt;/p&gt;
&lt;p&gt;즉, &lt;code&gt;controller&lt;/code&gt;와 &lt;code&gt;service&lt;/code&gt; 메서드 내에서 &lt;code&gt;SESSION_TOKEN&lt;/code&gt;에 접근할 수 있으나, &lt;code&gt;runWhere&lt;/code&gt; 메서드 호출이 완료되고 나면 해당 바인딩은 해제됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;runWhere&lt;/code&gt; 메서드 내부에서 &lt;code&gt;SESSION_TOKEN&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;유효한 범위 내에서 사용하지 않으면 noSuchElementException이 발생하게 됩니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;이런 문제를 두고, 이 값을 상속시키기 위해 구조화된 동시성(StructuredConcorrency)라는 개념이 나오게 됩니다.&lt;/p&gt;
&lt;h3&gt;Inheriting Scoped Value (스코프 벨류 상속 )&lt;/h3&gt;
&lt;p&gt;범위 지정된 값은 &lt;code&gt;StructuredTaskScope&lt;/code&gt;를 사용하여 생성된 모든 자식 스레드에 자동으로 상속됩니다. 자식 스레드는 부모 스레드에서 설정된 범위 지정된 값에 대한 바인딩을 사용할 수 있습니다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future&amp;lt;Optional&amp;lt;Data&amp;gt;&amp;gt; internalData = scope.fork(
      () -&amp;gt; internalService.getData(request)
    );
    Future&amp;lt;String&amp;gt; externalData = scope.fork(externalService::getData);
    try {
        scope.join();
        scope.throwIfFailed();

        Optional&amp;lt;Data&amp;gt; data = internalData.resultNow();
        // 응답에서 데이터를 반환하고 적절한 HTTP 상태를 설정
    } catch (InterruptedException | ExecutionException | IOException e) {
        response.setStatus(500);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 경우, &lt;code&gt;fork&lt;/code&gt; 메소드를 통해 생성된 자식 스레드에서 실행 중인 서비스에서도 범위 지정된 값을 여전히 접근할 수 있습니다. 하지만, 스레드로컬 변수와 달리 부모 스레드에서 자식 스레드로 범위 지정된 값이 복사되지는 않습니다.&lt;/p&gt;
&lt;h3&gt;3.3. 범위 지정된 값의 재바인딩&lt;/h3&gt;
&lt;p&gt;범위 지정된 값은 변경 불가능하기 때문에 저장된 값을 변경하기 위한 set 메소드를 지원하지 않습니다. 하지만, 제한된 코드 섹션의 호출에 대해 범위 지정된 값을 재바인딩할 수 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들어, &lt;code&gt;run&lt;/code&gt;에서 호출된 메소드로부터 범위 지정된 값을 숨기기 위해 &lt;code&gt;where&lt;/code&gt; 메소드를 사용하여 null로 설정할 수 있습니다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ScopedValue.where(Server.LOGGED_IN_USER, null).run(service::extractData);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;하지만, 해당 코드가 종료되는 즉시 원래 값이 다시 사용 가능해집니다. &lt;code&gt;run&lt;/code&gt; 메소드의 반환 타입이 void인 이유를 잘 봐야합니다. 만약 값을 반환해야하는 경우, 반환된 값들을 처리할 수 있도록 &lt;code&gt;call&lt;/code&gt; 메소드를 사용할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;구조화된 동시성(StructuredConcorrency)&lt;/h1&gt;
&lt;p&gt;구조화된 동시성은 동시성 프로그래밍에서의 한 패턴으로, 코드의 복잡성을 줄이고, 버그를 쉽게 찾을 수 있도록 돕는 방식입니다. 동시에 실행되는 작업들을 더 잘 관리하고, 코드의 흐름을 이해하기 쉽게 만드는 데 중점을 둡니다. 주요 목표 중 하나는 프로그램에서 생성된 모든 병렬 작업이 명확하게 구조화되고, 제어될 수 있도록 하는 것입니다.&lt;/p&gt;
&lt;h3&gt;구조화된 동시성의 핵심 원칙:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;범위 지정&lt;/strong&gt;: 구조화된 동시성에서는 모든 작업이 명시적인 scope 안에서 생성되어야 합니다. 이는 작업이 시작되고 종료되는 생명주기가 명확히 정의되어 있음을 의미합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자원 관리&lt;/strong&gt;: 구조화된 동시성을 사용하면 생성된 자원(예: 스레드, 핸들 등)이 적절히 관리되고 해제됩니다. 메모리 누수나 리소스 고갈같은 문제를 방지하는 데 도움이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오류 처리&lt;/strong&gt;: 오류를 효과적으로 캐치하고 처리할 수 있게합니다. 구조화된 동시성에서는 범위 내에서 발생한 오류를 범위를 관리하는 상위 코드 블록으로 전파하여 적절히 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가독성과 유지보수성 향상&lt;/strong&gt;: 코드의 동시적 부분이 명확하게 구조화되어 있으면, 프로그램의 흐름을 더 쉽게 이해할 수 있습니다. 즉 가독성을 향상시키고, 버그를 더 쉽게 찾아내고 수정할 수 있게 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;테스크가 여러 작은 작업으로 나뉠때, 스레드마다 서로 다른 결과가 나올 수 있습니다.&lt;/p&gt;
&lt;p&gt;아래 케이스에 따라 동시성을 성공과 실패로 나누어 예외를 관리할 수 있게됩니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&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;/td&gt;
&lt;td&gt;서브태스크들이 다른 스레드에서 실행됩니다. 각각의 성공 또는 실패 상태가 될 수 있습니다. (Executor Service 사용과 유사)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모두 성공&lt;/td&gt;
&lt;td&gt;모든 서브태스크가 성공해야 합니다. 어느 하나라도 실패하면, 다른 실행 중인 서브태스크들을 취소합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;첫번째만 성공&lt;/td&gt;
&lt;td&gt;첫 번째로 성공한 응답을 얻고 나머지는 취소합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;구조적 동시성이 이러한 다양한 실행 시나리오를 보다 명확하게 관리하고, 효과적으로 실행하기 위한 메커니즘을 제공하기 때문입니다.&lt;/p&gt;
&lt;p&gt;구조적 동시성이 없는 경우, 동시성 프로그래밍에서 다음과 같은 여러 문제가 발생할 수 있습니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;자원 관리의 어려움&lt;/strong&gt;: 개별 스레드나 작업을 수동으로 관리해야 하기 때문에, 사용한 자원을 적절히 해제하지 않으면 메모리 누수나 자원 고갈과 같은 문제가 발생할 수 있습니다. 구조적 동시성은 자동으로 자원을 관리하고 해제하여 이러한 문제를 예방합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오류 처리 복잡성&lt;/strong&gt;: 복수의 스레드나 태스크에서 발생하는 오류를 효율적으로 관리하고 처리하는 것이 어렵습니다. 오류가 발생했을 때, 모든 관련 태스크를 적절히 취소하거나 오류를 상위로 전파하는 로직을 수동으로 구현해야 합니다. 구조적 동시성은 이를 단순화하여 오류 처리를 더 용이하게 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;코드 복잡성 증가&lt;/strong&gt;: 개별 스레드의 생명주기를 수동으로 관리하면 코드가 복잡해지고, 이해하기 어려워집니다. 이로 인해 버그가 발생하기 쉬워지고, 유지보수가 어려워집니다. 구조적 동시성은 코드의 구조를 명확하게 하여 이러한 문제를 줄입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;동기화 문제&lt;/strong&gt;: 여러 스레드가 공유 자원에 접근할 때 동기화를 적절히 관리하지 못하면, 데이터 무결성 문제나 경쟁 상태(race condition)가 발생할 수 있습니다. 구조적 동시성을 사용하면, 이러한 동기화 문제를 더 쉽게 관리할 수 있는 패턴을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;작업 취소 및 종료의 어려움&lt;/strong&gt;: 복수의 스레드나 태스크가 실행 중일 때, 특정 조건에서 모든 작업을 취소하거나 안전하게 종료시키는 것이 어려울 수 있습니다. 구조적 동시성은 작업의 범위를 명확하게 정의하고, 범위 내의 모든 작업을 쉽게 제어할 수 있는 메커니즘을 제공합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예시코드&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;구조적 동시성 작업 범위 객체를 정의하고 외부 api 호출을 2개의 가상스레드로 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/*
    구조화된 태스크 스코프를 사용한 스코프드 값 상속
 */
public class StructuredTaskScopeWithValue {

    private static final Logger log = LoggerFactory.getLogger(StructuredTaskScopeWithValue.class);
    private static final ScopedValue&amp;lt;String&amp;gt; SESSION_TOKEN = ScopedValue.newInstance(); // 세션 토큰을 위한 스코프드 값 생성

    public static void main(String[] args) {

        // 세션 토큰에 &amp;quot;token-123&amp;quot; 값을 할당하고 task 메서드를 실행
        ScopedValue.runWhere(SESSION_TOKEN, &amp;quot;token-123&amp;quot;, StructuredTaskScopeWithValue::task);

    }

    private static void task() {
        try (var taskScope = new StructuredTaskScope&amp;lt;&amp;gt;()) { // 가상 스레드를 생성할 수 있는 스코프 생성.

            log.info(&amp;quot;token: {}&amp;quot;, SESSION_TOKEN.get()); // 현재 세션 토큰 값 로깅

            // 하위 작업 생성
            var subtask1 = taskScope.fork(StructuredTaskScopeWithValue::getDeltaAirfare); // 델타 항공 운임 조회 작업
            var subtask2 = taskScope.fork(StructuredTaskScopeWithValue::getFrontierAirfare); // 프론티어 항공 운임 조회 작업

            taskScope.join(); // 모든 하위 작업이 완료될 때까지 대기

            log.info(&amp;quot;subtask1 state: {}&amp;quot;, subtask1.state()); // 하위 작업의 상태 (UNAVAILABLE, SUCCESS, FAIL) 반환
            log.info(&amp;quot;subtask2 state: {}&amp;quot;, subtask2.state());

            log.info(&amp;quot;subtask1 result: {}&amp;quot;, subtask1.get()); // 하위 작업의 결과 출력
            log.info(&amp;quot;subtask2 result: {}&amp;quot;, subtask2.get());

        } catch (Exception e) {
            throw new RuntimeException(e); // 예외 발생 시 RuntimeException을 던짐
        }
    }

    private static String getDeltaAirfare() {
        var random = ThreadLocalRandom.current()
                .nextInt(100, 1000); // 100에서 1000 사이의 임의의 값 생성
        log.info(&amp;quot;delta: {}&amp;quot;, random); // 생성된 무작위 값 로깅
        log.info(&amp;quot;token: {}&amp;quot;, SESSION_TOKEN.get()); // 현재 세션 토큰 값 로깅
        CommonUtils.sleep(&amp;quot;delta&amp;quot;, Duration.ofSeconds(1)); // 1초간 대기
        return &amp;quot;Delta-$&amp;quot; + random; // 델타 항공 운임 반환
    }

    private static String getFrontierAirfare() {
        var random = ThreadLocalRandom.current()
                .nextInt(100, 1000); // 100에서 1000 사이의 임의의 값 생성
        log.info(&amp;quot;frontier: {}&amp;quot;, random); // 생성된 무작위 값 로깅
        log.info(&amp;quot;token: {}&amp;quot;, SESSION_TOKEN.get()); // 현재 세션 토큰 값 로깅
        CommonUtils.sleep(&amp;quot;frontier&amp;quot;, Duration.ofSeconds(2)); // 2초간 대기
        failingTask(); // 예외를 발생시키는 작업 실행
        return &amp;quot;Frontier-$&amp;quot; + random; // 프론티어 항공 운임 반환 (이 코드는 실행되지 않음)
    }

    private static String failingTask() {
        throw new RuntimeException(&amp;quot;oops&amp;quot;); // RuntimeException 발생
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;UNAVIABLE: 아직 시작되지 않았거나 시작상태가 결정되지 않은상태&lt;/li&gt;
&lt;li&gt;SUCCESS: 테스크 성공,&lt;/li&gt;
&lt;li&gt;FAIL : 실패. 호출시 예외 발생 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결과&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;22:25:48.904 [main] INFO com.ys.ScopeValueWithStructedScope -- token: token-123
22:25:48.910 [] INFO com.ys.ScopeValueWithStructedScope -- delta: 949
22:25:48.910 [] INFO com.ys.ScopeValueWithStructedScope -- frontier: 853
22:25:48.910 [] INFO com.ys.ScopeValueWithStructedScope -- detal token: token-123
22:25:48.910 [] INFO com.ys.ScopeValueWithStructedScope -- frontier token: token-123
22:25:50.918 [main] INFO com.ys.ScopeValueWithStructedScope -- subtask1 state: SUCCESS
22:25:50.919 [main] INFO com.ys.ScopeValueWithStructedScope -- subtask1 result: Delta-$949
Exception in thread &amp;quot;main&amp;quot; java.lang.RuntimeException: java.lang.IllegalStateException: Subtask not completed or did not complete successfully&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Frontier 호출시 일부러 예외를 발생시켰습니다.&lt;/li&gt;
&lt;li&gt;그렇게되면, subtask2의 state는 FAIL이 나오며, 하위작업의 결과를 get하는 과정에서 예외가 상위 스콮으로 번져 전체 테스크는 실패하게 됩니다.&lt;/li&gt;
&lt;li&gt;그러나 subTask1에는 영향을 미치지 않았습니다. SUCCESS&lt;/li&gt;
&lt;li&gt;결과를 보면, 서브테스크로는 ScopeValue가 상속되는것을 볼 수 있습니다. 즉 StructuredTaskScope 내에서는 같은 변수를 공유하는 것이지요.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StructuredTaskScope&lt;/code&gt;를 사용하면, 개발자는 여러 작업을 동시에 실행하고, 그들이 모두 완료될 때까지 기다릴 수 있는 명확한 구조를 갖게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;만약 한 처리가 실패시, 다른 나머지 처리도 실패하고 싶다면?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;taskScope.throwIfFailed&lt;/code&gt; 메소드는 태스크 스코프 내에서 실패한 태스크가 있는 경우, 사용자 정의 예외(&lt;code&gt;RuntimeException(&amp;quot;something went wrong&amp;quot;)&lt;/code&gt;)를 발생시키는 방법입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try (var taskScope = new StructuredTaskScope.ShutdownOnFailure()) {
    var subtask1 = taskScope.fork(CancelOnFailure::getDeltaAirfare);
    var subtask2 = taskScope.fork(CancelOnFailure::failingTask);

  taskScope.join();
    taskScope.throwIfFailed(ex -&amp;gt; new RuntimeException(&amp;quot;something went wrong&amp;quot;, ex));

  log.info(&amp;quot;subtask1 state: {}&amp;quot;, subtask1.state());
    log.info(&amp;quot;subtask2 state: {}&amp;quot;, subtask2.state());
} catch (Exception e) {
    throw new RuntimeException(e);
}

// 결과
-- delta: 661
-- delta is cancelled
Exception in thread &amp;quot;main&amp;quot; java.lang.RuntimeException: java.lang.RuntimeException: something went wrong&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 구조는 한 태스크가 실패하면 모든 태스크가 취소되고 종료되는 방식으로 작동합니다.&lt;/p&gt;
&lt;h3&gt;만약 한 처리가 실패하더라도 예외가 상위 스콮으로 던지지 않고, 나머지 테스크를 종료 시키고 성공적으로 끝내게 하려면?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;StructuredTaskScope.ShutdownOnSuccess&lt;/code&gt; 사용&lt;/strong&gt;: 이 클래스는 여러 태스크를 동시에 실행할 때, 첫 번째 성공적으로 완료된 태스크가 나타나면 나머지 태스크를 자동으로 셧다운하는 기능을 제공합니다. 이는 여러 대안적인 실행 경로가 있고, 그 중 하나만 성공하면 충분한 경우에 유용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try (var taskScope = new StructuredTaskScope.ShutdownOnSuccess&amp;lt;&amp;gt;()) {
    var subtask1 = taskScope.fork(FirstSuccess::failingTask);
    var subtask2 = taskScope.fork(FirstSuccess::getFrontierAirfare);
    taskScope.join();
    log.info(&amp;quot;subtask1 state: {}&amp;quot;, subtask1.state());
    log.info(&amp;quot;subtask2 state: {}&amp;quot;, subtask2.state());
    log.info(&amp;quot;subtask result: {}&amp;quot;, taskScope.result(ex -&amp;gt; new RuntimeException(&amp;quot;all failed&amp;quot;, ex)));
} catch (Exception e) {
    throw new RuntimeException(e);
}

// 결과
-- frontier: 753
-- subtask1 state: FAILED
-- subtask2 state: SUCCESS
-- subtask result: Frontier-$753&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;두 개의 비동기 태스크를 실행하고, 첫 번째로 성공적으로 완료되는 태스크가 있을 때 나머지 태스크를 셧다운(종료)하는 패턴입니다.&lt;/p&gt;
&lt;p&gt;만약 모든 태스크가 실패하면, 사용자 정의 예외를 던집니다&lt;/p&gt;
&lt;p&gt;다음으로는, SpringBoot에서는 어떻게 사용하는지 알아보겠습니다.&lt;/p&gt;
&lt;h2&gt;참조&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21&quot;&gt;https://docs.oracle.com/en/java/javase/21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog&quot;&gt;https://spring.io/blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-6-virtual-threads&quot;&gt;https://www.baeldung.com/spring-6-virtual-threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&quot;&gt;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/1203723&quot;&gt;https://d2.naver.com/helloworld/1203723&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/java-virtual-thread/?couponCode=KEEPLEARNING&quot;&gt;https://www.udemy.com/course/java-virtual-thread/?couponCode=KEEPLEARNING&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;관련 포스팅&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://0soo.tistory.com/259&quot;&gt;Java 가상 스레드(Virtual Thread)의 이해: 종류, 설정, 사용법 - 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://0soo.tistory.com/261&quot;&gt;Java 가상 스레드(Virtual Thread) : SpringBoot에서 사용하기- 3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/Java</category>
      <category>scope value</category>
      <category>StructuredConcorrency</category>
      <category>virtual thread</category>
      <category>구조화된 동시성</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/260</guid>
      <comments>https://0soo.tistory.com/260#entry260comment</comments>
      <pubDate>Fri, 29 Mar 2024 23:38:05 +0900</pubDate>
    </item>
    <item>
      <title>Java 가상 스레드(Virtual Thread)의 이해: 종류, 설정, 사용법 - 1</title>
      <link>https://0soo.tistory.com/259</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 21부터 (자바 21) 기존 플랫폼 스레드의 단점을 보완하고 동시 처리량을 높이기 위한 새로운 방식의 스레드가 도입됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 스레드가 무엇인지, 기존 스레드와는 무엇이 다르고 어떻게 사용해야 하며 어떤점을 주의해서 사용하는지 정리해보았습니다.&lt;/p&gt;
&lt;h1&gt;1. Java 가상 스레드(Virtual Thread)와 기존 자바 스레드&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드의 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 유형: KLT vs. ULT&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드는 크게 커널 수준 스레드(Kernel-Level Threads, KLT)와 사용자 수준 스레드(User-Level Threads, ULT)로 분류될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;커널 수준 스레드(KLT)&lt;/b&gt;: 스레드의 생성, 스케줄링 및 관리를 직접 OS 커널이 담당하며, 이러한 스레드는 OS에 의존적입니다. KLT는 자원 관리 및 멀티프로세싱 환경에서의 스케줄링 측면에서 장점이 있으나, 스레드 생성 및 컨텍스트 스위칭에 높은 오버헤드가 있을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 수준 스레드(ULT)&lt;/b&gt;: ULT는 사용자 영역의 라이브러리나 애플리케이션에 의해 관리되는 운영 체제의 커널로부터 독립적으로 스케줄링되며, 스레드 관리에 필요한 모든 작업을 사용자 영역에서 처리하는 스레드입니다. ULT의 장점은 스레드 생성 및 컨텍스트 스위칭이 빠르다는 점입니다. 그러나, 일부 리소스를 공유하는 작업에서는 커널의 도움이 필요할 수 있으며, 자바 같은 경우 1:1로 매핑이 되기떄문에 이점이 줄어들 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 스레드 풀은 &lt;code&gt;ExecutorService&lt;/code&gt; 인터페이스를 통해 제공됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 힙 메모리에 여러 유저 레벨 스레드를 구현 및 생성하여 풀에 담아두고, 커널 레벨 스레드(KLT)를 JVM 이 유저 레벨 스레드(ULT)로 1:1로 매핑하여 사용합니다. Thread 객체는 JNI을 호출하여 커널 레벨 스레드에 1:1로 매핑하여 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 스레드의 스케줄링은 JVM을 통해 운영 체제의 스레드 스케줄러에 위임(스케쥴 알고리즘!)되어 관리되며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스케줄러가 스레드의 실행 타이밍과 프로세서 할당을 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 실제 하드웨어의 CPU와 스레드는 무한정 할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 운영체제의 스케줄러는 아주 빠르게 각 스레드를 돌아가면서 실행하며 이로 인해 비싼 컨텍스트 스위칭이 발생합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 자바 기존 스레드가 문제가 될 수 있을까요?&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;오버헤드 : 스레드의 생성과 종료 과정이 커널을 통해 이루어지기 때문에, 사용자 수준에서 발생하는 것보다 더 큰 오버헤드가 발생합니다. 때문에 애플리케이션 시작시 미리 만들어두고 사용하는것입니다. 부족하면 새로 생성하는것에 대해 매우 비싼 비용이 발생합니다.&lt;/li&gt;
&lt;li&gt;비싼 메모리 : 기존 유저 레벨 스레드는 매우 무겁습니다. (보통 1~2MB, 운영체제에 따라 다름 )&lt;/li&gt;
&lt;li&gt;블로킹 : 애플리케이션에서 I/O 작업(네트워크 요청, 파일 입출력 등)을 만나면 해당 스레드는 작업이 완료될 때까지 블로킹(대기) 상태가 됩니다. 이때, 스레드는 CPU 자원을 사용할 수 없으므로 OS에 CPU 자원을 반환하고, 실행할 수 없는 상태가 됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 연산과 입출력은 컴퓨터구조상 담당하는 하드웨어가 다릅니다. 데이터연산은 CPU, 입출력은 I/O장치(NIC, 마우스, 키보드 등)을 담당합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU는 I/O 장치와 직접 상호작용하지 않습니다. 대신, 운영 체제는 I/O 요청을 관리하며, I/O 작업이 필요할 때 DMA(Direct Memory Access)와 같은 메커니즘을 사용하여 CPU의 개입 없이 데이터를 메모리와 I/O 장치 사이에서 직접 전송할 수 있도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CPU가 수행하는 작업에 비해 I/O (네트워크, 파일 쓰기 및 읽기 요청)은 상대적으로 매우 느리기 때문에 I/O가 발생하면 그시간동안 스레드가 놀고있으면 아까우니, 제어권을 CPU에 반환해서 다른 스레드가 동작하게 되어 실행할 수 없게 됩니다.&lt;/li&gt;
&lt;li&gt;그러다 I/O 작업이 끝나면 스케쥴러에의해 제어권을 받아 남은 작업을 이어가고, 작업이 끝나고 스레드를 반환합니다 .&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제들로 인해, 자바 애플리케이션에서 처리량을 올리는것에 한계가 생기게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이런 단점들을 해결 하기 위해 자바에서는 다른방안을 고민, 버츄얼 스레드를 도입하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경량화와 높은 확장성(수만 수백만개 동시 스레드 사용 가능)을 갖게하며 컨텍스트 스위칭과 메모리 사용량을 최소화하면서도 높은 수준의 동시성과 병렬성을 더 쉽게 관리할 수 있게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 대안으로, 스레드를 공유하고 비동기 - 논 블로킹 방식을 사용하여 처리량을 매우 높이는 반응형 리액티브 기술도 있으나 개발과 디버깅의 어려움이 있다는 단점이 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 스레드와 가상 스레드의 차이점 - 가상 스레드와 캐리어 스레드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 자바 스레드는 플랫폼 스레드라고도 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;플랫폼 스레드 (Platform Thread)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플랫폼 스레드는 OS가 관리하는 전통적인 자바 스레드 모델에서 사용되는 스레드로, Java 가상 머신(JVM)이 운영 체제의 기능을 활용하여 생성합니다.&lt;/li&gt;
&lt;li&gt;높은 연산량을 요구하는 계산 작업등에 작업에 주로 사용되며 상대적으로 많은 리소스를 소비하며, 스레드의 수는 시스템의 리소스에 의해 제한됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버츄얼 스레드가 나오게 되면서, 새로운 캐리어 스레드라는 개념이 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버츄얼 스레드와 캐리어 스레드에 대한 정의를 보겠습니다.&lt;/p&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;경량 스레드로 JVM 위에서 생성 및 실행되며플랫폼 스레드보다 훨신 가볍고 더 적은 리소스를 사용합니다.&lt;/li&gt;
&lt;li&gt;캐리어 스레드 위에서 캐리어 스레드에 의해 관리 및 실행됩니다.&lt;/li&gt;
&lt;li&gt;플랫폼 스레드의 크기는 1MB ~ 2MB 이고 스택사이즈가 고정되어있지만,
&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://blog.ycrash.io/is-java-virtual-threads-lightweight/&quot;&gt;https://blog.ycrash.io/is-java-virtual-threads-lightweight/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;버츄얼 스레드는 상대적으로 훨씬 작으며, 고정된 스택 사이즈가 없습니다. 즉 사용량에 따라 크기가 커질수도 작을수도 있습니다. (Stack Chunk Object)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;캐리어 스레드 (Carrier Thread, 플랫폼 스레드라고도 할 수 있다.)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐리어 스레드는 Project Loom의 일부로 도입된 개념으로, Virtual Thread를 실행하기 위한 운반체(Carrier) 역할을 합니다. 기존의 OS 수준의 스레드(플랫폼 스레드)를 기반으로 합니다. 버츄얼 스레드가 수행되는 동안 실제로 CPU의 실행 시간을 제공하는 스레드입니다.&lt;/li&gt;
&lt;li&gt;여러 버츄얼 스레드를 효율적으로 관리하고 실행하기 위해 사용되며, 한 캐리어 스레드는 동시에 여러 버츄얼 스레드의 작업을 처리할 수 있습니다. 즉 여러 버츄얼 스레드를 캐리어 스레드 위에서 시분할 방식으로 실행시킵니다.&lt;/li&gt;
&lt;li&gt;이 캐리어 스레드의 모적은 다수의 경량 모델인 버추얼 스레드를 효율저으로 스케줄링하고 실행하는 목적입니다.&lt;/li&gt;
&lt;li&gt;캐리어스레드로 인해 애플리케이션은 OS 스레드의 제한(리소스 등)없이 사용할 수 있게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 하나의 캐리어 스레드가, 여러 버추얼 스레드를 돌아가면서 실행, 관리하는 1:N 관계라고 볼 수 있고, 여러 캐리어 스레드가 존재해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 캐리어 스레드의 갯수를 조절할 수 있는데, 이건 뒤에서 살펴볼게요 (웬만해서는 건드릴 필요 없는 설정이에요.)&lt;/p&gt;
&lt;/blockquote&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;scala&quot;&gt;&lt;code&gt;/**
 * A thread that is scheduled by the Java virtual machine rather than the operating
 * system.
 */
final class VirtualThread extends BaseVirtualThread {

  ...// 버추얼 스레드의 실행상태들  
        // carrier thread when mounted, accessed by VM
    private volatile Thread carrierThread;
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버추얼 스레드의 실행 상태가 있는데, 상태에 따라 Virtual Thread의 상태에 따라 플랫폼 스레드에 마운트/언마운트해 실행을 관리합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'마운트'된다는 것은 버추얼 스레드가 플랫폼 스레드에 할당되어 실행되기 시작했다는 것을 의미해요. 즉 버추얼 스레드의 실행 상태가 실제 CPU에서 처리될 수 있도록 플랫폼 스레드가 이를 &quot;운반(캐리어)&quot;하죠.&lt;/li&gt;
&lt;li&gt;'언마운트'란 반대로, 버추얼 스레드가 실행을 마치거나 대기 상태로 전환될 때 플랫폼 스레드에서 언마운트하게 해서, 현재 CPU 할당을 중지하고 다른 버추얼 스레드가 해당 플랫폼 스레드에 의해 실행될 수 있게끔 되는것을 의미해요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐리어 스레드가 버추얼 스레드의 실행과 스케줄링을 담당하고 있는데, 실제 코드로도 그렇게 되어있어요.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;private void mount() {
...
  carrier.setCurrentThread(this); // 플랫폼(캐리어) 스레드에 실행할 Virtual Thread 객체 this 할당 
... 
}

private void unmount() {

  Thread carrier = this.carrierThread;
  carrier.setCurrentThread(carrier);

  synchronized (interruptLock) {
    setCarrierThread(null); // 플랫폼(캐리어) 스레드에서 Virtual Thread 제거
   }
   carrier.clearInterrupt();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메소드들은 JVM 내부의 스레드 스케줄러에 의해 자동으로 호출됩니다.&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;a href=&quot;https://d2.naver.com/helloworld/1203723&quot;&gt;네이버 D2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.woowahan.com/15398/&quot;&gt;우아한 형제들 기술블로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;가상 스레드의 사용&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual Thread를 사용하려면 인텔리제이와 gradle 프로젝트에서의 설정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 인텔리제이에서 project structr의 sdk와 gradle jvm을 java 21로 맞춰 주셔야해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 builg.gradle에서 다음 설정을 추가해주셔야 해요&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;tasks.withType(JavaCompile).configureEach {
    options.encoding = 'UTF-8'
    options.compilerArgs += '--enable-preview' // 프리뷰 해야 structured concurrency 사용 가능
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으론, 버추얼 스레드를 생성 및 사용할 수 있는 다양한 API가 나왔어요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread.startVirtualThread(Runnable task);&lt;/li&gt;
&lt;li&gt;Thread.ofVirtual.start(Runnable task);&lt;/li&gt;
&lt;li&gt;ExecutorService.submit(() -&amp;gt; { })&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class SimpleVirtualThreadExample {
  /**
  기본적인 버추얼 스레드 생성
  버추얼 스레드를 생성하고 시작하는 기본적인 방법입니다.
  */
  void createVirtualThreadWithLambda() {
    Thread.startVirtualThread(() -&amp;gt; { // public static Thread startVirtualThread(Runnable task) {}
        System.out.println(&quot;Hello, Virtual Thread!&quot;);
        });
  }

  void createVirtualThreadWithRunnable() {        
    Runnable runnable = () -&amp;gt; log.info(&quot;Hello&quot;);

    Thread virtualThread = Thread.ofVirtual()     
      .name(&quot;my-virtual1&quot;, 1) 
      .unstarted(runnable);

    virtualThread.start();
  }

  /*
        Thread.Builder를 사용하여 가상 스레드를 생성하기
        - 가상 스레드는 기본적으로 데몬 스레드입니다.
        - 가상 스레드는 기본적으로 이름이 지정되어 있지 않지만, name으로 지정이 가능하며, name 다음인수로 넘버링 해요
    */
    void virtualThreadDemo() throws InterruptedException {
        Thread virtualBuilder = Thread.ofVirtual().name(&quot;virtual-&quot;, 1);
        virtualBuilder.unstarted(() -&amp;gt; { 실행시킬내용 }); // Thread unstarted(Runnable task);
        virtualBuilder.start():
    }

  /**
       ExecutorService를 사용하여 버추얼 스레드 생성하고 작업
    */

      void startVirtualThread() {
            ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
                    executor.submit(() -&amp;gt; {
                    System.out.println(&quot;Task running in virtual thread&quot;);
                    });
                executor.shutdown();    
      }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Future와 CompletableFuture와도 같이 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Future와 CompletableFuture와도 같이 사용할 수 있어요.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;String futureWithVirtual() {
  ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    Future&amp;lt;String&amp;gt; future = executor.submit(() -&amp;gt; {
    Thread.sleep(100); // 비동기 작업 시뮬레이션
    return &quot;Result from Future&quot;;
    });

  return future.get();
}

//

String completableFutureWithVirtual() {

  var cf = CompletableFuture
    .supplyAsync(() -&amp;gt; &quot;Hello&quot;, Executors.newVirtualThreadPerTaskExecutor());

  try {
    return cf.get();
  } catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);  
  } 
}
// thenAccept, thenRun, exceptionally와도 사용 가능. 
String completableFutureWithVirtual() {
    var cf = CompletableFuture
        .supplyAsync(() -&amp;gt; &quot;Hello&quot;, Executors.newVirtualThreadPerTaskExecutor())
        .thenApply((s) -&amp;gt; s + &quot; World&quot;)
        .exceptionally(ex -&amp;gt; {
            log.info(&quot;error - {}&quot;, ex.getMessage());
            return null;
        });
    try {
        return cf.get();
    } catch (InterruptedException | ExecutionException e) {
        throw new RuntimeException(e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 스레드가 가상 스레드인지 확인하는 메서드도 제공해요.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;boolean isVirtualThread = Thread.isVirtual();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐리어 스레드 설정 - JDK 가상 스레드 스케줄러를 구성하기 위해 사용할 수 있는 시스템 속성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버추얼 스레드는 캐리어 스레드에 의해 관리 및 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 캐리어 스레드는 기본적으로 우리가 알던 플랫폼 스레드와 동일합니다.&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;아래 Java &lt;code&gt;java.lang.Thread&lt;/code&gt;의 공식 문서도 읽어보세요&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&quot;&gt;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시스템 속성&lt;/h3&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jdk.virtualThreadScheduler.parallelism&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가상 스레드를 스케줄링하기 위해 사용할 수 있는 플랫폼 스레드의 수입니다. 기본값은 사용 가능한 프로세서의 수입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jdk.virtualThreadScheduler.maxPoolSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스케줄러에 사용할 수 있는 플랫폼 스레드의 최대 수입니다. 기본값은 256입니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 프로그램을 시작할 때 최대 풀 크기를 변경할 수 있음을 의미해요.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;java -Djdk.virtualThreadScheduler.maxPoolSize=512 &amp;lt;다른 인수들...&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 실행되는 캐리어 스레드의 수를 제한하고 싶은 경우 다음 두 속성을 이용할 수 있어요.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;기본적으로 캐리어 스레드의 수는 사용 가능한 cpu 코어의 수와 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성되는 캐리어 스레드의 수를 설정하려면 다음 속성을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt; jdk.virtualThreadScheduler.parallelism=5&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐리어 스레드의 최대 수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한 병렬성 값에 의해 설정된 수를 초과하는 캐리어 스레드 수는 가상 스레드가 차단될 때 발생할 수 있습니다. 이러한 새로운 캐리어 스레드는 차단된 가상 스레드와 캐리어 스레드를 수용하기 위해 일시적으로 생성됩니다. 생성될 수 있는 캐리어 스레드의 최대 양을 설정하려면 다음 시스템 속성을 사용하십시오:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;jdk.virtualThreadScheduler.maxPoolSize=10 &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;기본값은 256입니다.&lt;/li&gt;
&lt;/ul&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;그러나 다시말하지만 일반적으로, 우리가 따로 관리해야 할 필요는 없어요.&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 Java 애플리케이션을 시작할 때 JVM에 전달하는 인수를 통해 설정할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 커맨드 라인을 통한 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 시작할 때, JVM에 전달하는 커맨드 라인 인수에 &lt;code&gt;-D키=값&lt;/code&gt; 형식을 사용하여 속성을 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 캐리어 스레드의 수를 5로 제한하고, 최대 캐리어 스레드 수를 10으로 설정하려면 다음과 같이 할 수 있습니다&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;java -Djdk.virtualThreadScheduler.parallelism=5 -Djdk.virtualThreadScheduler.maxPoolSize=10 -jar xxx.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;xxx.jar&lt;/code&gt;라는 Java 애플리케이션을 시작하면서 캐리어 스레드의 병렬성을 5로, 최대 풀 크기를 10으로 설정합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 프로그램 내에서 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 속성은 Java 코드 내에서도 &lt;code&gt;System.setProperty()&lt;/code&gt; 메서드를 사용하여 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 초기화 단계나 설정이 필요한 특정 시점에서 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        System.setProperty(&quot;jdk.virtualThreadScheduler.parallelism&quot;, &quot;5&quot;);
        System.setProperty(&quot;jdk.virtualThreadScheduler.maxPoolSize&quot;, &quot;10&quot;);
    }
}

//or

static {
    System.setProperty(&quot;jdk.virtualThreadScheduler.parallelism&quot;, &quot;5&quot;);
    System.setProperty(&quot;jdk.virtualThreadScheduler.maxPoolSize&quot;, &quot;10&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;다양한 threadpool executor와 가상 스레드 Executor (스레드풀)&lt;/h1&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class ExecutorType {

    private static final Logger log = LoggerFactory.getLogger(Lec02ExecutorServiceTypes.class);

    public static void main(String[] args) {
        // main 메소드에서 각 메소드를 호출하여 ExecutorService 타입을 시험해 볼 수 있습니다.
    }

    // 단일 스레드 실행자 - 작업을 순차적으로 실행하기 위함
    private static void single(){
        execute(Executors.newSingleThreadExecutor(), 3);
        // 사용 사례: 작업 실행 순서가 중요할 때 사용.
        // 생성 비용: 낮음. 단일 스레드만 유지하므로 오버헤드가 적습니다.
    }

    // 고정 스레드 풀
    private static void fixed(){
        execute(Executors.newFixedThreadPool(5), 20);
        // 사용 사례: 동시에 실행할 작업의 최대 수가 정해져 있을 때 사용.
        // 생성 비용: 중간. 고정된 수의 스레드를 미리 생성하고 관리해야 하므로 적당한 오버헤드가 존재.
    }

    // 탄력적 스레드 풀
    private static void cached(){
        execute(Executors.newCachedThreadPool(), 200);
        // 사용 사례: 실행해야 할 작업의 수가 불규칙하거나 예측 불가능할 때 사용.
        // 생성 비용: 높음. 필요에 따라 스레드 수가 자동으로 조절되므로 관리 오버헤드가 증가할 수 있음.
    }

    // 작업 당 가상 스레드를 생성하는 ExecutorService
    private static void virtual(){
        execute(Executors.newVirtualThreadPerTaskExecutor(), 10_000);
        // 사용 사례: 매우 많은 수의 짧은 작업을 처리해야 할 때 사용.
        // 생성 비용 낮음. 가상 스레드는 경량이며, 생성과 소멸 비용이 매우 낮음.
    }

    // 주기적인 작업을 스케줄링
    private static void scheduled(){
        try(var executorService = Executors.newSingleThreadScheduledExecutor()){
            executorService.scheduleAtFixedRate(() -&amp;gt; {
                log.info(&quot;실행 중인 작업&quot;);
            }, 0, 1, TimeUnit.SECONDS);

            CommonUtils.sleep(Duration.ofSeconds(5));
        }
        // 사용 사례: 주기적으로 반복해야 하는 작업을 스케줄링할 때 사용.
        // 생성 비용 낮음. 주기적인 작업을 관리하는 데 필요한 리소스가 적음.
    }

    private static void execute(ExecutorService executorService, int taskCount){
        try(executorService){
            for (int i = 0; i &amp;lt; taskCount; i++) {
                int j = i;
                executorService.submit(() -&amp;gt; ioTask(j));
            }
            log.info(&quot;작업 제출 완료&quot;);
        }
    }

    private static void ioTask(int i){
        log.info(&quot;작업 시작: {}. 스레드 정보 {}&quot;, i, Thread.currentThread());
        CommonUtils.sleep(Duration.ofSeconds(5));
        log.info(&quot;작업 종료: {}. 스레드 정보 {}&quot;, i, Thread.currentThread());
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;threadpool executor에 다음과 같이 virtualThreadFactory를 전달하여 이름을 지정할 수 있어요.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;var factory = Thread.ofVirtual().name(&quot;vins&quot;, 1).factory(); // 이름 지정 
execute(Executors.newFixedThreadPool(3), factory);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Thread.ofVirtual().start(() -&amp;gt; {
    try {
        예외가 발생할 수 있는 로직 
    } catch (Exception e) {
        // 예외 처리 로직
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. UncaughtExceptionHandler 이용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UncaughtExceptionHandler 객체 인자를 받는 메소드를 제공해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 객체는 아래 인터페이스에요&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;/**
 * {@code Thread}가 잡히지 않은 예외로 인해 갑자기 종료될 때 호출되는 핸들러를 정의합니다.
 * 스레드가 잡히지 않은 예외로 인해 종료될 때, 자바 가상 머신은 스레드에 대한 {@code UncaughtExceptionHandler}를
 * 를 사용하여 조회하고 핸들러의 {@code uncaughtException} 메소드를 호출하며, 스레드와 예외를
 * 인자로 전달합니다.
 * 스레드가 {@code UncaughtExceptionHandler}를
 * 명시적으로 설정하지 않은 경우, 해당 스레드의 {@code ThreadGroup} 객체가 그 역할을 합니다. 
 * {@code ThreadGroup} 객체가 예외를 다루는 특별한 요구사항이 없으면, getDefaultUncaughtExceptionHandler로 호출을 전달할 수 있습니다.
 */

@FunctionalInterface
public interface UncaughtExceptionHandler {
  /**
  * 주어진 스레드가 주어진 잡히지 않은 예외로 인해 종료될 때 호출되는 메소드입니다.
  * @param t 스레드
  * @param e 예외
  */
  void uncaughtException(Thread t, Throwable e);  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 핸들링하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Thread.ofVirtual().unstarted(() -&amp;gt; System.out.println(&quot;Virtual thread&quot;))
        .setUncaughtExceptionHandler((t, e) -&amp;gt; System.err.println(&quot;Uncaught exception in thread &quot; + t.getName() + &quot;: &quot; + e.getMessage()));

// or

Thread virtualThread = Thread.ofVirtual().unstarted(() -&amp;gt; {
    // 예외가 발생할 수 있는 경우 
});

virtualThread.setUncaughtExceptionHandler((t, e) -&amp;gt; {
    // 예외 처리 로직
});

virtualThread.start();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서 가상 스레드를 사용할때의 주의점, SpringBoot에서는 어떻게 사용하는지 알아보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가상 스레드의 스레드풀을 사용할때에는 고정 풀 을 사용하면 안되고 필요할때마다 생성해야 한다.&lt;/li&gt;
&lt;li&gt;동시성을 제어하기 위해서 synchronized 키워드 대신 Lock을 사용하자(Reentrant)&lt;/li&gt;
&lt;li&gt;스레드 로컬에 용량이 큰 객체를 저장하지 말자. 스레드 로컬의 특징이 무엇일까?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&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://docs.oracle.com/en/java/javase/21&quot;&gt;https://docs.oracle.com/en/java/javase/21&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog&quot;&gt;https://spring.io/blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-6-virtual-threads&quot;&gt;https://www.baeldung.com/spring-6-virtual-threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&quot;&gt;https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/1203723&quot;&gt;https://d2.naver.com/helloworld/1203723&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.udemy.com/course/java-virtual-thread/?couponCode=KEEPLEARNING&quot;&gt;https://www.udemy.com/course/java-virtual-thread/?couponCode=KEEPLEARNING&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://0soo.tistory.com/260&quot;&gt;Java 가상 스레드(Virtual Thread)의 이해: 주의할점, Scope Value, 구조화된 동시성 - 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://0soo.tistory.com/261&quot;&gt;Java 가상 스레드(Virtual Thread)SpringBoot에서 사용하기 - 3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/Java</category>
      <category>가상 스레드</category>
      <category>가상 스레드 예외 핸들링</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/259</guid>
      <comments>https://0soo.tistory.com/259#entry259comment</comments>
      <pubDate>Fri, 29 Mar 2024 23:35:10 +0900</pubDate>
    </item>
    <item>
      <title>Postgresql 외래키 제약조건 없애고 데이터 초기화하는 방법</title>
      <link>https://0soo.tistory.com/258</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Postgresql을 사용할 때&amp;nbsp; 마이그레이션, 리플리케이션 할 때나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트시 테이블을 수정하거나 데이터를 초기화할 때 외래키 제약조건때문에 복잡한 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 &lt;code&gt;SET FOREIGN_KEY_CHECKS&lt;/code&gt; 를 바꿈으로써 제약조건을 해제할 수 있는데요,&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;-- foreign key 제약 체크(기본값) - 제약조건 체크함
SET FOREIGN_KEY_CHECKS = 1;

-- foreign key 제약 미체크 - 제약조건 관계없이 데이터 조작 가능
SET FOREIGN_KEY_CHECKS = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postgresql에서는 session_replication_role 명령어를 이용해서 제약조건을 해제할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;session_replication_role&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;session_replication_role&lt;/code&gt;는 현재 세션에서 복제(replication)와 관련된 트리거와 규칙의 실행 여부를 제어하는 설정입니다.&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;b&gt;origin&lt;/b&gt;: 모든 트리거와 규칙이 정상적으로 동작. PostgreSQL 세션 설정의 default 값&lt;/li&gt;
&lt;li&gt;&lt;b&gt;replica&lt;/b&gt;: 이 모드가 활성화되면, 모든 트리거와 규칙의 실행이 일시적으로 비활성화됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;replication 중인 데이터가 원본 데이터베이스에서 이미 실행된 트리거에 의해 변경되었을 가능성이 있기 때문에 사용합니다.&lt;/li&gt;
&lt;li&gt;이 모드를 사용하면 데이터가 변경되지 않고 복제될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;local&lt;/b&gt;: 로그에 기록된 데이터 변경은 적용되지 않으나, 로컬로 정의된 트리거나 규칙은 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;always&lt;/b&gt;: 모든 트리거와 규칙이 항상 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postgresql에서 외래키 제약 조건은 트리거로 구현됩니다.&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;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;)가 발생할 때 자동으로 실행되도록 정의된 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 replica로 특정 세션의 role을 바꾸면 외래키 제약조건 트리거가 비활성화 되어서, 데이터 조작어 실행시 외래키 제약조건을 검사하지 않습니다.&lt;br /&gt;이렇게 하면 외래 키 제약 조건을 위반하는 레코드를 일시적으로 삽입하거나 수정할 수 있게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정의 원래 용도는 replication 시스템이 복제된 변경 사항을 적용할 때 이 설정을 복제하도록 설정하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 복제나 로딩 시 트리거에 의한 부작용을 방지하기 위한 기능이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들면 replication, 데이터 로딩 중 종종 데이터 무결성 문제나 불필요한 트리거 실행방지를 위해 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 아무렇지 않게 남발하면 안됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외래키 제약조건 해제하고 테이블 데이터 초기화&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 트리거 정지. 외래키 제약조건을 비활성 시키고 여러 테이블의 데이터 삭제
SET session_replication_role = 'replica'

-- 테이블 데이터 초기화
TRUNCATE table1, table2, table3 CASCADE; -- delete문도 대체가능

-- 데이터 초기화 후에 트리거 및 규칙 실행 재개
SET session_replication_role = 'origin';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;session_replication_role&lt;/code&gt; 설정은 세션 별로 유효하며, 그 세션 내에서만 적용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 세션에는 영향을 미치지 않으며, 세션이 종료되면 해당 설정도 초기 상태 (기본값은 &lt;code&gt;origin&lt;/code&gt;)로 복원되므로, 초기화시에 사용하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 다음과 같이 사용도 가능


-- 트리거 정지. 외래키 제약조건을 비활성 시키고 여러 테이블의 데이터 삭제
SET session_replication_role = 'replica'

-- 테이블 데이터 초기화 주어진 스키마명 내의 모든 테이블에 대해 DELETE문을 생성하고 이를 출력하므로 내용을 복사해서 지우면된다. 
SELECT 'delete from ' || tablename || ';' as de
FROM pg_catalog.pg_tables
WHERE schemaname = '스키마명';


-- 데이터 초기화 후에 트리거 및 규칙 실행 재개
SET session_replication_role = 'origin';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;a href=&quot;https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE&quot;&gt;https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SESSION-REPLICATION-ROLE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://postgresqlco.nf/doc/en/param/session_replication_role/&quot;&gt;https://postgresqlco.nf/doc/en/param/session_replication_role/&lt;/a&gt;*&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/postgresql/flexible-server/how-to-bulk-load-data&quot;&gt;https://learn.microsoft.com/ko-kr/azure/postgresql/flexible-server/how-to-bulk-load-data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://repost.aws/ko/knowledge-center/rds-postgresql-foreign-keys&quot;&gt;https://repost.aws/ko/knowledge-center/rds-postgresql-foreign-keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database/PostgreSQL</category>
      <category>postgresql 외래키</category>
      <category>postgresql 초기화</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/258</guid>
      <comments>https://0soo.tistory.com/258#entry258comment</comments>
      <pubDate>Sat, 16 Sep 2023 18:37:03 +0900</pubDate>
    </item>
    <item>
      <title>Springboot PSQLException: Unterminated dollar quote started at position $$ 해결방법</title>
      <link>https://0soo.tistory.com/257</link>
      <description>&lt;h1&gt;Springboot PSQLException: Unterminated dollar quote started at position $$&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot Junit5 환경에서, 일부 테스트 에만 sql 파일을 실행시켜 테스트 해야하는 경우가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 sql 스크립트를 이용하여 테스트 하던 중 다음과 같은 오류를 만나 테스트가 실행되지 않는 문제를 해결한 방법입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Caused by: org.postgresql.util.PSQLException: Unterminated dollar quote started at position 62 in SQL CREATE FUNCTION random_age(age_in INTEGER) RETURNS INTEGER AS $$ DECLARE random_offset INTEGER. Expected terminating $$
// 스크립트 내용은 재현하기 위한 가제. 예외명이랑 메시지는 겪은 문제와 같다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;환경&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpringBoot 2.7.x&lt;/li&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Postgresql 14&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot Junit5 에서는 테스트 실행 전후로 @Sql 어노테이션을 이용해서 특정 SQL 스크립트를 실행시킬 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;classpath:디렉토리명/sql파일명&lt;/li&gt;
&lt;li&gt;&lt;i&gt;@Sql&lt;/i&gt; 어노테이션 의 속성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;config&lt;/b&gt; : SQL 스크립트에 대한 설정. - &lt;code&gt;@SqlConfig&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;executionPhase&lt;/b&gt; &amp;ndash; BEFORE_TEST_METHOD 또는 &lt;i&gt;AFTER_TEST_METHOD&lt;/i&gt; 중 스크립트를 실행 시기 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구문(statements)&lt;/b&gt; - 실행할 인라인 SQL 문을 선언.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;script(스크립트) &amp;ndash; 값&lt;/b&gt; 실행할 SQL 스크립트 파일의 경로를 선언 .&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class SomeTest {

  @Autowired
    private SomeService service;

    @Sql(scripts = &quot;classpath:sql/age_function.sql&quot;,
        executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD
    )
    @Sql(scripts = {&quot;classpath:sql/drop_age_function.sql&quot;},
        executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    @Test
    void test() {
        int age = 10;
        service.logic(&quot;bob&quot;, age);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgresql 등에서 함수를 선언할때 &lt;code&gt;$$(달러 쿼트)&lt;/code&gt; 를 많이 이용하는데요&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE FUNCTION random_age(age_in INTEGER)
    RETURNS INTEGER AS $$
DECLARE
    random_offset INTEGER;
BEGIN
    random_offset := CAST(FLOOR(RANDOM() * 21) - 10 AS INTEGER);
    RETURN age_in + random_offset;
END;
$$ LANGUAGE plpgsql;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수를 age_function.sql에 작성하고, 위 자바 테스트 코드 예제와 같이 작성하면 다음과 같은 오류를 만납니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Caused by: org.postgresql.util.PSQLException: Unterminated dollar quote started at position xxx...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 원인 분석&lt;/h2&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;@Sql&lt;/code&gt; 어노테이션이 지정된 테스트 메서드나 테스트 클래스가 실행될 때, 해당 SQL 스크립트나 문장은 &lt;b&gt;&lt;code&gt;TestExecutionListener&lt;/code&gt;&lt;/b&gt;에 의해 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;org.springframework.test.context.jdbc.&lt;code&gt;SqlScriptsTestExecutionListener&lt;/code&gt;에 의해 처리되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TestExecutionListner&lt;/code&gt;가 처리하는 과정에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DatabasePopulator&lt;/code&gt; 를 구현한 &lt;code&gt;ResourceDatabasePopulator&lt;/code&gt;가 populate(Connection connection)메소드로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ScriptUtils&lt;/code&gt;를 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;org.springframework.jdbc.datasource.init.ScriptUtils&lt;/code&gt;에서 executeSqlScript()메소드가 구문을 분석하고 SQL을 파싱하는 과정에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResourceDatabasePopulator의 separator(구분자)를 같이 넘겨주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디폴트로는 &lt;code&gt;;&lt;/code&gt; 라는 기호 기준으로, 한 파일내에서 SQL을 분리합니다&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class ResourceDatabasePopulator {

  private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR; // 구분자.  변경 가능하다.

    @Override
    public void populate(Connection connection) throws ScriptException {
        Assert.notNull(connection, &quot;'connection' must not be null&quot;);
        for (Resource script : this.scripts) {
            EncodedResource encodedScript = new EncodedResource(script, this.sqlScriptEncoding);
            ScriptUtils.executeSqlScript(connection, encodedScript, this.continueOnError, this.ignoreFailedDrops,
                    this.commentPrefixes, this.separator, this.blockCommentStartDelimiter, this.blockCommentEndDelimiter);
        }
    }
}

// Script Utils의 DEFAULT_SEPARATOR
public abstract class ScriptUtils {

    /**
     * Default statement separator within SQL scripts: {@code &quot;;&quot;}.
     */
    public static final String DEFAULT_STATEMENT_SEPARATOR = &quot;;&quot;; // here

  public static final String FALLBACK_STATEMENT_SEPARATOR = &quot;\n&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다음 예제 스크립트라면&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;create table users(
    id int,
    name text
);

create table sellers(
    id int,
    name text
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 적혀있는 스크립트 내에서 ; 구분자를 이용해 두 구문을 분리하게 되면, 다음과 같이 2개의 스크립트가 생기는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0sJd3/btstxkioNEY/UnKRN8KqIcIcaDkcPCoPMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0sJd3/btstxkioNEY/UnKRN8KqIcIcaDkcPCoPMK/img.png&quot; data-alt=&quot;디버깅 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0sJd3/btstxkioNEY/UnKRN8KqIcIcaDkcPCoPMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0sJd3%2FbtstxkioNEY%2FUnKRN8KqIcIcaDkcPCoPMK%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;1280&quot; height=&quot;1199&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1199&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디버깅 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 처음으로 돌아가, 우리의 기존 예제 함수인&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE FUNCTION random_age(age_in INTEGER)
    RETURNS INTEGER AS $$
DECLARE
    random_offset INTEGER;
BEGIN
    random_offset := CAST(FLOOR(RANDOM() * 21) - 10 AS INTEGER);
    RETURN age_in + random_offset;
END;
$$ LANGUAGE plpgsql;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 보면 '';'' 구분자에 의해 6개로 나뉜것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvgLOB/btstwhGtDzX/XNGLSrKUW1wAKNhctCBs0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvgLOB/btstwhGtDzX/XNGLSrKUW1wAKNhctCBs0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvgLOB/btstwhGtDzX/XNGLSrKUW1wAKNhctCBs0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvgLOB%2FbtstwhGtDzX%2FXNGLSrKUW1wAKNhctCBs0K%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;1280&quot; height=&quot;1226&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 구문이 문법에 맞지않게 분리되어 함수가 제대로 실행이 되지 못하는것입니다.&lt;/p&gt;
&lt;h1&gt;해결방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결방법은 2가지가 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@Sql 어노테이션 지정시 구문을 분리할 적당한 separtor 지정&lt;/li&gt;
&lt;li&gt;SQL Script에서 함수 정의 본문을 &lt;code&gt;'&lt;/code&gt;로 감싸주기&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. @Sql 어노테이션 지정시 구문을 분리할 적당한 separtor 지정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 테스트 어노테이션에서 지정합니다&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Sql(scripts = {&quot;classpath:sql/age_function.sql&quot;},
        executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
        config = @SqlConfig(separator = &quot;;;&quot;) // separator 지정
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 지정하면, ResourceDatabasePopulator에서 주입받아서 ScriptUtils에게 구문 분석시 지정한 separator를 넘겨주게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 원문 스크립트도 구분할 수 있게 지정한 separator로 바꿔줘야 합니다&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// test/resources/sql 아래에 있는 age_function.sql

DROP FUNCTION if exists random_age(age_in INTEGER);; // 지정한 separator

CREATE FUNCTION random_age(age_in INTEGER)
    RETURNS INTEGER AS $$
DECLARE
    random_offset INTEGER;
BEGIN
    random_offset := CAST(FLOOR(RANDOM() * 21) - 10 AS INTEGER);
    RETURN age_in + random_offset;
END;
$$ LANGUAGE plpgsql;; // 지정한  separator&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Le2JH/btstrPKNc2L/ZBaZsGgjujTTjUljcndcf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Le2JH/btstrPKNc2L/ZBaZsGgjujTTjUljcndcf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Le2JH/btstrPKNc2L/ZBaZsGgjujTTjUljcndcf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLe2JH%2FbtstrPKNc2L%2FZBaZsGgjujTTjUljcndcf1%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;1280&quot; height=&quot;1344&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하면 정상 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( 물론 스크립트 내용에 따라 다르겠지만, 상황에 맞게 적절한 구분자(separator)를 지정해주고 스크립트를 수정하면 됩니다)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.SQL Script에서 함수 정의 본문을 &lt;code&gt;'&lt;/code&gt;로 감싸주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 사용했던 $$ 대신 '로 함수 정의 본문을 감싸주면 해결됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;바꾸기 전 $$를 이용한 스크립트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;DROP FUNCTION if exists random_age(age_in INTEGER);

CREATE FUNCTION random_age(age_in INTEGER)
    RETURNS INTEGER AS $$
DECLARE
    random_offset INTEGER;
BEGIN
    random_offset := CAST(FLOOR(RANDOM() * 21) - 10 AS INTEGER);
    RETURN age_in + random_offset;
END;
$$ LANGUAGE plpgsql;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바꾼 후 ''를 이용한 스크립트&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DROP FUNCTION if exists random_age(age_in INTEGER);

CREATE FUNCTION random_age(age_in INTEGER)
    RETURNS INTEGER AS '

DECLARE
    random_offset INTEGER;
BEGIN
    random_offset := CAST(FLOOR(RANDOM() * 21) - 10 AS INTEGER);
    RETURN age_in + random_offset;
END;

' LANGUAGE plpgsql;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 다음과 같이 SQl 스크립트 내부에 ''가 나 %같은 다른 특수문자가 들어간다면, 그때는 더블 쿼트를주면 됩니다&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;'~~~' -&amp;gt; ''~~~''
'%' -&amp;gt; ''%''&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 만약 다음과 같이 예외를 발생시킨다면&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;DROP FUNCTION if exists random_age(age_in INTEGER);

CREATE OR REPLACE FUNCTION random_age(age_in INTEGER)
    RETURNS INTEGER AS $$
DECLARE
    random_offset INTEGER;
BEGIN
    -- 입력값 검사
    IF age_in &amp;lt; 0 OR age_in &amp;gt; 150 THEN
        RAISE EXCEPTION 'Invalid age value: %', age_in; // ''가 들어가있음 
    END IF;

    random_offset := CAST(FLOOR(RANDOM() * 21) - 10 AS INTEGER);
    RETURN age_in + random_offset;
END;
$$ LANGUAGE plpgsql;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 싱글 쿼트를 더블 쿼트로 지정해줍니다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;DROP FUNCTION if exists random_age(age_in INTEGER);

CREATE OR REPLACE FUNCTION random_age(age_in INTEGER)
    RETURNS INTEGER AS '
DECLARE
    random_offset INTEGER;
BEGIN
    -- 입력값 검사
    IF age_in &amp;lt; 0 OR age_in &amp;gt; 150 THEN
        RAISE EXCEPTION ''Invalid age value: %'', age_in;
    END IF;

    random_offset := CAST(FLOOR(RANDOM() * 21) - 10 AS INTEGER);
    RETURN age_in + random_offset;
END;
' LANGUAGE plpgsql;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이외 다른 해결방법&lt;/h2&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;a href=&quot;https://stackoverflow.com/questions/52228470/exception-in-jpa-when-using-seed-file-for-postgresql/52230382#52230382&quot;&gt;https://stackoverflow.com/questions/52228470/exception-in-jpa-when-using-seed-file-for-postgresql/52230382#52230382&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>테스트</category>
      <category>Unterminated dollar quote</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/257</guid>
      <comments>https://0soo.tistory.com/257#entry257comment</comments>
      <pubDate>Sat, 9 Sep 2023 17:40:50 +0900</pubDate>
    </item>
    <item>
      <title>레디스를 활용한 분산 락(Distrubuted Lock) feat lettuce, redisson</title>
      <link>https://0soo.tistory.com/256</link>
      <description>&lt;h1&gt;레디스를 활용한 분산 락(Distrubuted Lock)&lt;/h1&gt;
&lt;h1&gt;분산 락이란(Distributed Lock)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은 분산 환경에서 여러대의 서버와 여러 DB간의 동시성을 관리하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 분산환경이 아닌 DB 등과 같은 곳에서는 비관적 락 등을 이용하여 동시성을 제어할 수 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 대의 DB가 존재하는 분산 DB 환경에서는 동시성 문제를 해결할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분산 DB에서 비관적 락으로 해결할 수 없는 이유&lt;/b&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;b&gt;성능 저하&lt;/b&gt;: 분산 환경에서의 비관적 락은 네트워크 지연, 노드 간의 통신 오버헤드 등으로 인해 더 큰 성능 저하를 초래할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데드락 문제&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 파티션 문제&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 노드 사이의 네트워크 연결이 끊긴다.&lt;/li&gt;
&lt;li&gt;한 노드에서 데이터의 락을 설정했지만, 연결이 끊어진 노드에서는 이 락의 정보를 알 수 없다.&lt;/li&gt;
&lt;li&gt;결과적으로 두 노드에서 동시에 동일한 데이터를 변경할 수 있으며, 이는 데이터 불일치를 초래할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 복제본과 일관성의 문제&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 노드에서 데이터를 업데이트하고 락을 해제한 후, 변경 사항을 다른 노드에 복제한다.&lt;/li&gt;
&lt;li&gt;복제하는 동안, 다른 사용자가 이전 버전의 데이터를 다른 노드에서 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;A 노드에서 데이터 X에 대한 락을 설정했다고 하면, B 노드에서는 그 정보를 모르기 때문에 B 노드의 사용자가 동일한 데이터 X에 접근 가능하기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;MySQL의 네임드락&lt;/b&gt;: MySQL에서 락이름을 명시하여 관리할 수 있는 네임드락으로 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ZooKeeper&lt;/b&gt;: Apache ZooKeeper는 분산 시스템을 위한 일관된 서비스를 제공하는 오픈 소스 프로젝트로, 분산 락 구현에 자주 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;etcd 또는 Consul&lt;/b&gt;: 이러한 도구는 분산 설정 관리와 서비스 발견에 사용되며, 분산 락을 구현하기 위한 원자적 연산을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레디스 (Redis)&lt;/b&gt;: 레디스는 &lt;code&gt;SETNX&lt;/code&gt; (set if not exists) 명령어와 같은 원자적 연산을 사용하여 분산 락을 구현하는 데 사용될 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은, 반드시 분산환경에서만 사용할 수 있는 것은 아닙니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;락 (Lock)&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;/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;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스핀락 (Spin Lock)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티스레딩 환경에서 공유 자원에 대한 동시 접근을 방지하기 위한 락(Lock) 중 하나.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 락과는 다르게, 락을 획득할때까지 계속해서 락 획득을 시도하고 조건을 확인하면서 대기하는 기법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식때문에 스핀이라는 이름이 붙었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스핀락의 동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. A스레드가 락을 획득하려고 시도
2. 락이 이미 다른 스레드가 획득했다면 A스레드가 반복적으로 요청하면서 락 획득 시도
3. 락이 해제되면 다음으로 먼저 요청한 스레드중 하나가 랜덤으로 락을 획득  &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 락을 얻을때가지 계속 요청을 보내며 대기하므로 서버에 많은 부하를 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스핀락의 장점:&lt;/b&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;b&gt;컨텍스트 스위치(Context Switch) 발생하지 않음&lt;/b&gt;: 스핀락을 대기하는 동안 스레드는 활성 상태로 유지되기 때문에 컨텍스트 스위치가 발생하지 않습니다. 따라서 짧은 시간 동안 락을 획득하려는 경우 스핀락이 효율적일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스핀락의 단점:&lt;/b&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;b&gt;CPU 시간 낭비&lt;/b&gt;: 락이 해제되길 기다리며 CPU 시간을 계속 소비합니다. 따라서 락이 오랜 시간 동안 보유될 것으로 예상되는 경우에는 스핀락이 비효율적일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;우선순위 역전(Priority Inversion) 문제 발생 가능&lt;/b&gt;: 높은 우선순위의 스레드가 낮은 우선순위의 스레드로 인해 블록되는 현상을 발생시킬 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스핀락은 해당 락을 대기하는 동안 스레드를 유휴 상태로 만들지 않고 계속 실행 상태로 둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 짧은 시간 락 대기에는 효율적일 수 있지만, 긴 시간 대기에는 다른 락 메커니즘이 더 적합할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL의 네임드락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 Named Lock은 주어진 이름의 락을 사용하여 여러 세션 사이에서 동기화를 수행할 수 있는 기능입니다.&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;GET_LOCK()&lt;/code&gt;, &lt;code&gt;RELEASE_LOCK()&lt;/code&gt;, &lt;code&gt;IS_USED_LOCK()&lt;/code&gt;, &lt;code&gt;IS_FREE_LOCK()&lt;/code&gt;와 같은 함수를 통해 락을 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 이름으로 락의 이름을 지정할 수 있어 애플리케이션에서 명시적으로 동시성 제어 관리가 가능합니다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 애플리케이션에서 동시성을 관리하기에 더 편할 수 있죠.&lt;/p&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;또한 동시성을 위해 Zookeeper, Redis 등의 추가 인프라 관리가 필요하지 않아 비용과 관리포인트를 아낄 수 있다는 장점이 있습니다&lt;/p&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://techblog.woowahan.com/2631/&quot;&gt;우아한형제들 기술블로그 네임드락 사용&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네임드락 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;불필요한 부하&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락에 대한 정보가 DB에 저장되고, 락을 획득하고 제거하는 쿼리가 매번 발생하여 DB에 불필요한 부하를 줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;그러나 대부분의 상황에서는 미미한 편입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제한적이다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MySQL에서만 사용가능하다는 단점&lt;/li&gt;
&lt;li&gt;JPA에서는 nativeQuery를 사용해야 함.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데드락 위험&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Named Locks와 테이블 레벨 락, 행 레벨 락을 혼용하여 사용할 경우 데드락 상황이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션이 종료되면 잠금이 해제되는 문제 - &lt;a href=&quot;https://techblog.woowahan.com/2631/&quot;&gt;우아한형제들 기술블로그&lt;/a&gt;, &lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html&quot;&gt;MySQL 공식 문서&lt;/a&gt;에 적힌 내용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Named Locks는 세션 범위를 가지며, 다른 세션에서는 해당 락의 상태를 변경할 수 없기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션풀이 부족&lt;/b&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;a href=&quot;https://kwonnam.pe.kr/wiki/database/mysql/user_lock&quot;&gt;참고&lt;/a&gt;) 주의할 점은, Named Lock을 활용할 때 데이터소스를 분리하지않고 하나로 사용하게되면 커넥션풀이 부족해질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Named Lock은 한 MySQL 서버 인스턴스에서만 유효합니다. 따라서, 여러 서버 인스턴스가 있는 분산 환경에서는 한 서버에서 설정된 Named Lock이 다른 서버에는 적용되지 않으므로 분산 시스템에는 적합하지 않을 수도 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제점들로 인해, 분산 시스템에서는 주로 분산 락 전용 솔루션(예: Apache ZooKeeper, etcd, Redis의 RedLock 등)을 사용하여 관리하는 것이 좋을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Redis를 이용한 분산락&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis를 이용한 분산락 구현 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 사용할 수 있는 Redis Client로는 Jedis, Lettuce, Redisson 등이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 시나리오&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시나리오&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서 유저가 gather에 가입을 한다.&lt;br /&gt;이때 gather에는 인원 제한이 있다.&lt;br /&gt;100명의 유저가 5명의 인원 제한이 있는 gather에 가입 요청을 하였을 때, 5명만 가입되어야 한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 엔티티&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// user
@Entity
@Table(name = &quot;users&quot;)
class User(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long?,

    val name: String,

    @ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
    @JoinColumn(name = &quot;group_id&quot;)
    var gather: Gather?

) {

    fun join(gather: Gather) {
        this.gather = gather
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게더 엔티티&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;gathers&quot;)
class Gather(

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long?,

    @OneToMany(fetch = FetchType.LAZY, mappedBy = &quot;gather&quot;, cascade = [CascadeType.ALL])
    val members: MutableList&amp;lt;User&amp;gt;,

    var currentMemberCount: Int,

    val limitsCount: Int,

    ) {

    constructor(limitsCount: Int, user: User) : this(null, mutableListOf(user), 1, limitsCount)

    fun join(user: User) {
        require(currentMemberCount &amp;lt; limitsCount) { &quot;가입 불가&quot; }

        if (!this.members.contains(user)) {
            this.members.add(user)
            user.join(this)

            this.currentMemberCount += 1
            println(&quot;currentMemberCount : $currentMemberCount&quot;)
        }
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가입 비지니스 로직&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class GatherService(
    private val gatherRepository: GatherRepository,
    private val userRepository: UserRepository,

) {

    @Transactional
    fun join(groupId: Long, userId: Long) {
        val gather = gatherRepository.findById(groupId).orElseThrow()
        val user = userRepository.findById(userId).orElseThrow()

        gather.join(user)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lettuce를 이용한 분산 락 구현&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce는 공식적으로 분산락을 제공하지 않기 때문에 직접 구현해서 사용해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 : 코틀린, spring boot 3.1.3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 추가&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.springframework.boot&quot;) version &quot;3.1.3&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.3&quot;
    kotlin(&quot;jvm&quot;) version &quot;1.8.22&quot;
    kotlin(&quot;plugin.spring&quot;) version &quot;1.8.22&quot;
    kotlin(&quot;plugin.jpa&quot;) version &quot;1.8.22&quot;
    kotlin(&quot;plugin.allopen&quot;) version &quot;1.8.22&quot;
}

implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis&quot;)&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;Spring Data Redis는 기본 클라이언트로 Lettuce와 Jedis를 지원합니다&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis:requirements&quot;&gt;https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis:requirements&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lettuce 클라이언트 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@ConfigurationProperties(prefix = &quot;spring.data.redis&quot;)
class RedisProperties @ConstructorBinding constructor(
    val host: String,
    val port: Int,
    val password: String,
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class LettuceConfig(
    private val redisProperties: RedisProperties
) {
    // TCP 통신
    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory {
        val redisStandaloneConfiguration = RedisStandaloneConfiguration(redisProperties.host, redisProperties.port)

        redisStandaloneConfiguration.password = RedisPassword.of(redisProperties.password)

        return LettuceConnectionFactory(redisStandaloneConfiguration)
    }

    // 커넥션 위에서 조작 가능한 메소드 제공
    // 공식 문서에서는 &amp;lt;String, String&amp;gt;으로 되어 있다
    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate&amp;lt;Any, Any&amp;gt; {
        val redisTemplate = RedisTemplate&amp;lt;Any, Any&amp;gt;()

        redisTemplate.apply {
            keySerializer = StringRedisSerializer()
            valueSerializer = GenericJackson2JsonRedisSerializer() // JSON 포맷으로 저장
        }

        redisTemplate.connectionFactory = connectionFactory

        return redisTemplate
    }

    // 문자열에 특화한 메소드 제공. 위에서 선언한 redisTemplate만으로도 사용가능하다
    @Bean
    fun stringRedisTemplate(redisConnectionFactory: RedisConnectionFactory): StringRedisTemplate {
        val redisTemplate = StringRedisTemplate()

        redisTemplate.apply {
            connectionFactory = redisConnectionFactory
            keySerializer = StringRedisSerializer()
            valueSerializer = StringRedisSerializer()
        }

        return redisTemplate
    }

}&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;application. yml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: 1234&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;redisTemplate 를 활용해서 락을 관리하는 LockManager를 구현&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예제에서는 Repository라는 이름를 사용했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class RedisRepository(
    private val redisTemplate: RedisTemplate&amp;lt;String, String&amp;gt;
) {

    // setIfAbsent() 를 활용해서 SETNX를 실행
    fun lock(key: String, timeoutMills: Long): Boolean {
        return redisTemplate
            .opsForValue()
            .setIfAbsent(key, &quot;lock&quot;, Duration.ofMillis(timeoutMills)) ?: true
    }

    fun unlock(key: String): Boolean {
        return redisTemplate.delete(key)
    }

}&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;key값과 timeout을 받아 Lock을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비즈니스 로직에 Lock 구현&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class GroupLettuceService(
    private val redisRepository: RedisRepository,
    private val gatherService: GatherService,
    ) {

    fun join(groupId: Long, userId: Long) {
        val key = LOCK_PREFIX + groupId.toString()

        while (!redisRepository.lock(key, 3000)) {
            Thread.sleep(100)
        }

        try {
            gatherService.join(groupId, userId)
        } finally {
            redisRepository.unlock(key)
        }
    }

    companion object {
        private const val LOCK_PREFIX = &quot;LOCK:&quot;
    }

}&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;락을 획득할때까지 계속 재시도 (spin) 해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
internal class GatherLettuceServiceTest {
        ...생략

    @DisplayName(&quot;그룹 가입- lettuce 스핀 lock&quot;)
    @Test
    fun join() {
        // given
        val threadCount = 100
        val executorService = Executors.newFixedThreadPool(threadCount)
        val countDownLatch = CountDownLatch(threadCount)

        val limit = 5
        val user = User.create(&quot;그룹장&quot;)
        val gather = Gather(limit, user)

        gatherRepository.save(gather)
        val createUsers = createUser(100) // 100명 유저 생성 

        // when
        IntStream.range(0, threadCount)
            .forEach {
                executorService.submit {
                    try {
                        gatherLettuceService.join(gather.id!!, createUsers[it].id!!)
                    } catch (ex: InterruptedException) {
                        throw RuntimeException(ex)
                    } finally {
                        countDownLatch.countDown()
                    }
                }

            }

        countDownLatch.await()
        executorService.shutdown()

        // then
        val findGroup = gatherRepository.findById(gather.id!!).get()

        println(&quot;### findGroup.count=${findGroup.currentMemberCount}&quot;)

        Assertions.assertThat(findGroup.currentMemberCount).isEqualTo(limit)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;그러나 Lettuce를 이용한 문제점은 위에서 이야기 했던 스핀락의 문제점을 갖고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 획득하지 못한 경우 락을 획득하기 위해 redis에 계속해서 while로 요청을 보내야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 스레드도 계속 일을 하는 상태가 되며 redis에 부하가 생길 수 있단 단점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부하를 낮추기 위해 락 획득을 재시도 시간을 길게 설정하게 되면, 락을 획득할 수 있음에도 불구하고 무조건 설정된 시간만큼 기다려야 하는 비효율적인 경우가 발생할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 Redisson에서는 다르게 해결할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redisson을 이용한 분산 락 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson은 Lettuce,Jedis와는 달리 &lt;b&gt;RLock&lt;/b&gt; 이라는 Lock 전용 객체를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson은 Lock에 타임아웃을 명시하여 무한정 대기상태로 빠질 수 있는 위험이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 스핀락(Spin Lock)을 사용하지 않고 &lt;b&gt;pub sub&lt;/b&gt; 기능을 사용합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락이 해제되면 락을 subscribe(구독)하는 클라이언트들에게 &lt;b&gt;채널로&lt;/b&gt; 락이 해제되었다는 신호를 보내게 됩니다.&lt;/li&gt;
&lt;li&gt;그렇기에 락을 subscribe하는 클라이언트들은 더 이상 락을 획득해도 되냐고 redis로 요청을 보내지 않고 해제를 공지받아 락을 시도합니다. 따라서, 별도의 retry 로직이 필요없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis에서 채널을 사용해보고 싶으시다면 &lt;code&gt;redis-cli&lt;/code&gt;를 열어 다음과 같이 사용하면됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;(Session 1) $ docker exec -it 6c7c0a47dd34 redis-cli
(Session 2) $ docker exec -it 6c7c0a47dd34 redis-cli

(Session 1) $ subscribe ch1
// Reading messages... (press Ctrl-C to quit)
// 1) &quot;subscribe&quot;
// 2) &quot;ch1&quot;
// 3) (integer) 1

(Session 2) $ publish ch1 hello
// (integer) 1

(Session 1) $
// 1) &quot;message&quot;
// 2) &quot;ch1&quot;
// 3) &quot;hello&quot;&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;a href=&quot;https://sigridjin.medium.com/weekly-java-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%ED%95%99%EC%8A%B5%ED%95%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-9daa85155f66&quot;&gt;https://sigridjin.medium.com/weekly-java-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%ED%95%99%EC%8A%B5%ED%95%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-9daa85155f66&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;leaseTime을 잘못 잡으면 작업 도중 Lock이 해제될 수도 있습니다. 이를 &lt;code&gt;IllegalMonitorStateException&lt;/code&gt; 이라고 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 추가&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpringBoot 2.x / 3.x 버전별로 라이브러리 호환이 다르므로 다음 문서를 참조하세요.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redisson/redisson/blob/master/redisson-spring-boot-starter/README.md&quot;&gt;https://github.com/redisson/redisson/blob/master/redisson-spring-boot-starter/README.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redisson/redisson/blob/master/redisson-spring-data/README.md&quot;&gt;https://github.com/redisson/redisson/blob/master/redisson-spring-data/README.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;//redisson
implementation(&quot;org.redisson:redisson-spring-boot-starter:3.23.3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RedissonClient 빈 등록&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class RedissonConfig(
    private val redisProperties: RedisProperties,
) {

    @Bean
    fun redissonClient(): RedissonClient {
        val config = Config()
        val codec: Codec = StringCodec() // redis-cli에서 보기 위함

        config.codec = codec

        config.useSingleServer().apply {
            //https로 접근시에는 rediss://로 접근해야 한다.
            address = &quot;$REDISSON_HOST_PREFIX${redisProperties.host}:${redisProperties.port}&quot;
            password = redisProperties.password
            isSslEnableEndpointIdentification = false
            timeout = 3000 // 주입받아 설정도 가능 
        }

        return Redisson.create(config)
    }

    companion object {
        const val REDISSON_LOCK_PREFIX = &quot;LOCK:&quot;
        const val REDISSON_HOST_PREFIX = &quot;redis://&quot;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비즈니스 로직&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class GatherRedissonService(
    private val gatherService: GatherService,
    private val redissonClient: RedissonClient,
) {

    fun join(groupId: Long, userId: Long) {
        val key = LOCK_PREFIX + groupId.toString()
        val lock: RLock = redissonClient.getLock(key)

        try {
            // 락 획득. (락 획득을 대기할 타임아웃, 락이 만료되는 시간)
            val isAvailable = lock.tryLock(5, 3, TimeUnit.SECONDS)

            if (!isAvailable) {
                log.info(&quot;redisson getLock fail.&quot;)
                return
            }

            gatherService.join(groupId, userId)

        } finally {
            // 락 해제
            lock.unlock()
        }
    }

    companion object {
        private val log: Logger = logger()
        private const val LOCK_PREFIX = &quot;LOCK:&quot;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson을 통한 락에는 RLock 인터페이스 객체가 사용됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락 획득 실패 시 false를 반환, 락 획득 시 true를 반환하는데 unlock을 하지 않고 leaseTime 만큼 잠금을 획득하는 방식&lt;/li&gt;
&lt;li&gt;redisson의 경우 leaseTime 설정을 통하여 만료를 지정할 수 있으므로, 락을 해제 안해주더라도 시간이 지나면 락이 해제가 됩니다. 때문에 프로세스에서 해당 락을 획득하기 위해 무한정 대기해야 하는 상황이 발생하지 않게 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;public interface RedissonClient {
  ...
    /**
     * 이름으로 Lock 인스턴스를 반환합니다.
         * non-fair 락킹을 구현하므로 스레드에 의한 획득 순서를 보장하지 않습니다.
         *  장애 복구 동안의 신뢰성을 높이기 위해, 모든 연산은 모든 Redis 슬레이브로의 전파를 기다립니다.
     * @param name - Lock 객체 이름
     * @return Lock 객체
     */
    RLock getLock(String name);
}&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;비공정(non-fair) 락킹은 스레드가 잠금(lock)을 요청하는 순서와 관계없이 잠금을 획득할 수 있다는 것을 의미합니다. 즉, 스레드가 잠금을 요청하는 순서대로 잠금을 획득하지 않는다는 것이죠.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이와 대조적으로 공정(fair) 락킹은 스레드가 잠금을 요청하는 순서대로 잠금을 획득합니다. 따라서 첫 번째로 잠금을 요청한 스레드가 첫 번째로 잠금을 획득하게 됩니다.&lt;/li&gt;
&lt;li&gt;non-fair 락킹은 스레드가 잠금(lock)을 요청하는 순서와 관계없이 잠금을 획득할 수 있다는 것을 의미합니다. 즉, 스레드가 잠금을 요청하는 순서대로 잠금을 획득하지 않는다는 것이죠.&lt;/li&gt;
&lt;li&gt;non-fair은 일반적으로 faire 락보다 성능이 좋을 수 있습니다. 그 이유는 공정성을 유지하기 위한 추가적인 비용이 없기 때문입니다.&lt;/li&gt;
&lt;li&gt;하지만 non-fair 락의 경우, 특정 스레드가 잠금을 획득하는데 긴 시간이 걸릴 수 있거나 기아 상태(starvation)에 빠질 위험이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public interface RLock {
  ...
  /**
  * 정의된 &amp;lt;code&amp;gt;leaseTime&amp;lt;/code&amp;gt;으로 잠금을 획득하려고 시도합니다.
  * 필요한 경우 잠금이 사용 가능해질 때까지 정의된 &amp;lt;code&amp;gt;waitTime&amp;lt;/code&amp;gt;까지 기다립니다.
  * 정의된 &amp;lt;code&amp;gt;leaseTime&amp;lt;/code&amp;gt; 간격 후에 잠금은 자동으로 해제됩니다.
  * 
  * @param waitTime 잠금을 획득하기 위한 최대 시간
  * @param leaseTime 점유(타임아웃) 시간
  * @param unit 시간 단위
  * @return 잠금이 성공적으로 획득된 경우 true, 그렇지 않고 잠금이 이미 설정된 경우 false.
  * @throws InterruptedException 스레드가 중단된 경우
  */
  boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트도 마찬가지로 통과합니다.&lt;/p&gt;
&lt;h1&gt;Redisson tryLock()이 락을 pub/sub 방식 으로 획득하는 과정&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;999&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cba53Z/btssk2o40SS/B1itfKAsbmgypzk8W7wxuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cba53Z/btssk2o40SS/B1itfKAsbmgypzk8W7wxuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cba53Z/btssk2o40SS/B1itfKAsbmgypzk8W7wxuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcba53Z%2Fbtssk2o40SS%2FB1itfKAsbmgypzk8W7wxuk%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;1280&quot; height=&quot;999&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;999&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getMultiLock()이나 getSpinLock()을 사용하지 않을 경우, RedissonLock 구현체를 사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chJfup/btssc85G0vf/RWQlNZVMZtAVtlGRXtCtn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chJfup/btssc85G0vf/RWQlNZVMZtAVtlGRXtCtn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chJfup/btssc85G0vf/RWQlNZVMZtAVtlGRXtCtn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchJfup%2Fbtssc85G0vf%2FRWQlNZVMZtAVtlGRXtCtn1%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;1280&quot; height=&quot;920&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 쓰레드의 ID로 구독하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class RedissonLock {

  String getChannelName() {
    return prefixName(&quot;redisson_lock__channel&quot;, getRawName());  
  }
  ...

  protected CompletableFuture&amp;lt;RedissonLockEntry&amp;gt; subscribe(long threadId) {  
    return pubSub.subscribe(getEntryName(), getChannelName());
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C5bTT/btssgHlG9Av/8kiHVJtP0onK3JcBxJZ2V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C5bTT/btssgHlG9Av/8kiHVJtP0onK3JcBxJZ2V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C5bTT/btssgHlG9Av/8kiHVJtP0onK3JcBxJZ2V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC5bTT%2FbtssgHlG9Av%2F8kiHVJtP0onK3JcBxJZ2V0%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;1280&quot; height=&quot;357&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&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;구독시 내부적으로 PublishSubscribeService을 호출하여 세마포어를 가지고 옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 tryAcquire()를 호출해서 락 획득에 성공하면 true를 반환합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간을 계산하여 지정한 waitTime이나 LeaseTime이 지난다면 false를 반환합니다.&lt;/li&gt;
&lt;li&gt;이후 락 획득에 성공해서 true를 반환하든, 실패해서 false를 반환하든 구독을 해지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;tryLock(long waitTime, long leaseTime, TimeUnit unit)&lt;/code&gt; 메서드는 락을 획득하려 할 때, 최대 &lt;code&gt;waitTime&lt;/code&gt; 시간 동안 대기하게 됩니다. &lt;code&gt;leaseTime&lt;/code&gt;은 락의 최대 유지 시간입니다.&lt;/li&gt;
&lt;li&gt;처음에 tryAcquire()을 이용하여 락을 획득하려 시도하고, 락이 성공적으로 획득되면 true를 반환합니다.&lt;/li&gt;
&lt;li&gt;락 획득에 실패한 경우, ttl이 남아있고, 대기시간(waitTime)이 남아있다면 현재 스레드는 락이 사용 가능해질 때까지 알림을 받기 위해 특정 채널을 구독합니다. 이 구독 과정도 일정 시간 내에 완료되어야 합니다.&lt;/li&gt;
&lt;li&gt;구독한 후, 현재 스레드는 다시 락 획득을 시도합니다. 이 때, 락 획득에 성공하면 true를 반환하며, 실패하면 락이 풀릴 때까지 대기합니다.&lt;/li&gt;
&lt;li&gt;락이 아직 사용 가능하지 않다면, 현재 스레드는 락이 사용가다는 메시지가 도착할 때까지 대기합니다.&lt;/li&gt;
&lt;li&gt;다시 ttl 내로 락을 획득하지 못하면 false를 반환합니다&lt;/li&gt;
&lt;li&gt;성공적으로 락을 획득했든, 시간 초과로 실패했든 구독을 해지합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Redisson과 Lettuce의 분산락 구현 방식, 차이를 알아보았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock 획득이 실패하고 재시도가 반드시 필요하지 않은 경우에는 Lettuce를 사용하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도가 반드시 필요한 경우에는 Redisson을 활용하면 좋습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 해당 락을 사용하는 코드들을 보면&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;getLock(); // 락 획득

try {
  businessLogic()
} finally {
  releaseLock() // 락 해제  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같이 락 획득 - 비지니스 로직 - 락 반납이 반복되는데요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이부분은 AOP 등으로 해결할 수 있습니다.&lt;/p&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://helloworld.kurly.com/blog/distributed-redisson-lock/#3-%EB%B6%84%EC%82%B0%EB%9D%BD%EC%9D%84-%EB%B3%B4%EB%8B%A4-%EC%86%90%EC%89%BD%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98%EB%8A%94-%EC%97%86%EC%9D%84%EA%B9%8C&quot;&gt;컬리 기술 블로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 다음 Redisson의 Wiki를 보면 더 다양한 상황에 따른 Redisson Lock을 사용할 수 있습니다.&lt;/p&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://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers&quot;&gt;https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redisson/redisson/wiki/11.-Redis-commands-mapping&quot;&gt;https://github.com/redisson/redisson/wiki/11.-Redis-commands-mapping&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;참조&lt;/h1&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://redis.io/docs/manual/patterns/distributed-locks/#disclaimer-about-consistency&quot;&gt;https://redis.io/docs/manual/patterns/distributed-locks/#disclaimer-about-consistency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html&quot;&gt;https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/distributed-redisson-lock/#3-%EB%B6%84%EC%82%B0%EB%9D%BD%EC%9D%84-%EB%B3%B4%EB%8B%A4-%EC%86%90%EC%89%BD%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98%EB%8A%94-%EC%97%86%EC%9D%84%EA%B9%8C&quot;&gt;https://helloworld.kurly.com/blog/distributed-redisson-lock/#3-%EB%B6%84%EC%82%B0%EB%9D%BD%EC%9D%84-%EB%B3%B4%EB%8B%A4-%EC%86%90%EC%89%BD%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98%EB%8A%94-%EC%97%86%EC%9D%84%EA%B9%8C&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sigridjin.medium.com/weekly-java-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%ED%95%99%EC%8A%B5%ED%95%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-9daa85155f66&quot;&gt;https://sigridjin.medium.com/weekly-java-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9C%BC%EB%A1%9C-%ED%95%99%EC%8A%B5%ED%95%98%EB%8A%94-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-9daa85155f66&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devfunny.tistory.com/888&quot;&gt;https://devfunny.tistory.com/888&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;redis transaction - &lt;a href=&quot;https://ronaldocfg.tistory.com/12&quot;&gt;https://ronaldocfg.tistory.com/12&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database/Redis</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/256</guid>
      <comments>https://0soo.tistory.com/256#entry256comment</comments>
      <pubDate>Sun, 27 Aug 2023 21:01:49 +0900</pubDate>
    </item>
    <item>
      <title>MySQL Named Lock 사용법 with Spring Boot</title>
      <link>https://0soo.tistory.com/255</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693122577829&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;목차 
1. MySQL Named Lock
2. 사용법 - Named Lock Functions
2.1. GET_LOCK(lock_name, timeout) - 락 획득 시도
2.2. RELEASE_LOCK(lock_name) - lock_name으로 락 해제 시도
2.3. IS_FREE_LOCK(lock_name) - 락 획득 가능(사용 가능) 여부확인
2.4. IS_USED_LOCK(lock_name) - 락 사용중인지 여부
2.5. 네임드 락 중첩과 해제
2.5.1 네임드락이 관리되는 테이블 - performance_schema의 metadata_locks Table
2.6. 네임드락(Named Lock의) 단점
3. Spring에서의 구현
3.1. JdbcTemplate으로 구현 
3.2. EntityManager로 구현
3.3. JpaRepository로 구현
3.4. 사용
4. Name Lock 구현하여 사용시 주의할점
4.1. 반복하여 중첩된 락을 해제할때는 동일 커넥션에서
4.2. 같은 커넥션을 이용해서 Lock을 설정하고 로직을 수행한 후 Lock을 해제해야 한다 
5. metadata_locks로 어떤 세션이 네임드 락을 걸었는지 확인하는법
5.1. 세션이 종료되었는데, Named Lock이 해제되지 않을 경우 해결방법
5.2. 참조&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;1. MySQL Named Lock&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Named Lock은 MySQL에서 제공하는 사용자 수준의 잠금입니다.&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;b&gt;GET_LOCK&lt;/b&gt;: &lt;code&gt;GET_LOCK(lock_name, timeout)&lt;/code&gt; 함수를 사용하여 특정 문자열에 대해 락을 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 및 메타 데이터 기반&lt;/b&gt;: 실제 데이터 레코드나 테이블에 락을 걸지 않습니다. 대신 메모리와 메타 데이터 테이블(&lt;code&gt;metadata_locks&lt;/code&gt;)을 사용하여 락을 관리합니다. 따라서, 전체 시스템의 처리량이 &lt;code&gt;비관적 락&lt;/code&gt;에 비해 증가할 수 있습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상이 테이블이나 레코드 같은 객체가 아니므로 테이블을 잠그는 테이블락과 레코드를 잠그는 레코드 락과는 다릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테이블/레코드 액세스 제한 없음&lt;/b&gt;: Named Lock은 단순히 사용자가 지정한 문자열에 대한 락만을 제공합니다. 따라서, 특정 테이블이나 레코드에 대한 접근은 제한되지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 인프라 불필요: Zookeeper, Redis와 같은 추가적인 인프라를 사용하지 않기 때문에, 비용과 관리 측면에서의 부담을 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;2. 사용법 - Named Lock Functions&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL Named Lock은 다음의 함수들로 이용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1. GET_LOCK(lock_name, timeout) - 락 획득 시도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock_name으로 Named Lock을 획득하려고 시도합니다. timeout 매개변수는 잠금을 획득할 수 없는 경우 오류를 반환하기 전 함수가 기다리는 시간을 지정합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- // &quot;mylock&quot;이라는 문자열에 대해 잠금을 획득한다.
-- // 이미 잠금을 사용 중이면 2초 동안만 대기한다. (2초 이후 자동 잠금 해제됨)

SELECT GET_LOCK('mylock', 2);&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;한 session에서 잠금을 유지하고 있는동안에는 다른 모든 session에서 동일한 이름의 잠금을 획득 할 수 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET_LOCK() 을 이용하여 획득한 잠금은 Transaction 이 commit 되거나 rollback 되어도 해제되지 않습니다.&lt;/code&gt;&lt;/li&gt;
&lt;li&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;MySQL 5.7 이전에서는,동시에 1개의 Lock만 획득 가능하고 LOCK 이름 글자수 무제한 이였지만.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.7 이후에는 동시에 여러개의 Lock 획득이 가능하며 LOCK 이름 글자수가 60자로 제한됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;주의&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 커넥션에서 GET_LOCK을 여러번하면 여러번 락이 잡힌다. 따라서 RELEASE_LOCK을 그 횟수만큼 동일 커넥션에서 반복해서 해주지 않으면 lock이 풀리지 않게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 문제에 따라 &lt;b&gt;애플리케이션에서 Connection Pool 사용시 항상 동일 Lock 문자열에 대한 GET_LOCK과 RELEASE_LOCK 이 동일 커넥션에서 동일 횟수만큼 이뤄짐을 보장해줘야 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2. RELEASE_LOCK(lock_name) - lock_name으로 락 해제 시도&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- // &quot;mylock&quot;이라는 문자열에 대해 획득했던 잠금을 반납(해제) 한다.

SELECT RELEASE_LOCK('mylock');&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;b&gt;성공하는 경우 1을 반환&lt;/b&gt;하지만, 락이 없는 경우 등에 대해서는 &lt;b&gt;NULL이나 0을 반환&lt;/b&gt;합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3. IS_FREE_LOCK(lock_name) - 락 획득 가능(사용 가능) 여부확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- // &quot;mylock&quot;이라는 문자열에 대해 잠금이 설정돼 있는지 확인한다.

SELECT IS_FREE_LOCK('mylock');&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;락이 걸려있지 않은 경우 1, 락이 걸려있는경우 0을 반환합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4. IS_USED_LOCK(lock_name) - 락 사용중인지 여부&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 세션에서 특정 Named Lock이 현재 사용 중인지 확인하는 데 유용하며, 락을 보유하고 있는 세션을 파악합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lock_name의 Named Lock이 사용 중인지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- // lock_name의 Named Lock이 사용 중인지 확인

SELECT IS_USED_LOCK('mylock');&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;락 이름이 존재하지 않거나 설정되지 않았다면 NULL을 반환합니다.&lt;/li&gt;
&lt;li&gt;락을 보유하고 있다면, 락을 보유하고 있는 세션 ID( ex. 932) 를 반홥합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 8.0 이전에는 Named Lock을 중첩해서 걸 수 없엇지만, 8.0 이후부터는 중첩해서 걸 수 있게 되면서 조금 더 복잡한 로직을 처리할 수 있게 됐습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.2. 네임드 락 중첩과 해제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 버전 부터는 네임드 락을 중첩해서 사용할 수 있으며, 현재 세션에서 획득한 네임드 락을 한 번에 모두 해제할 수도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;mysq1&amp;gt; SELECT GET_LOCK('mylock_1',10);
-- // mylock_1에 대한 작업 실행

mysql&amp;gt; SELECT GET_LOCK('mylock_2',10);

-- // mylock_1과 mylock_2에 대한 작업 실행

mysql&amp;gt; SELECT RELEASE LOCK('mylock_2');
mysql&amp;gt; SELECT RELEASE LOCK('mylock_1');

-- // mylock_1과 mylock_2를 동시에 모두 해제하고자 한다면 RELEASE_ALL_LOCKS() 함수 사용
mysql&amp;gt; SELECT RELEASE_ALL_LOCKS();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 하나의 세션에서 연속으로 동일 Lock 이름으로 Lock을 잡으면 그 횟수만큼 중복으로 Lock이 잡히므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;중복으로 건 횟수만큼 풀어줘야 합니다.&lt;/code&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.1. 네임드락이 관리되는 테이블 - performance_schema의 metadata_locks Table&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/information-schema-innodb-locks-table.html&quot;&gt;&lt;code&gt;INNODB_LOCKS&lt;/code&gt;&lt;/a&gt;테이블은 각 잠금에 대한 정보를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 INNODB_LOCKS 테이블은 MySQL 5.7.14부터 더 이상 사용되지 않으며 MySQL 8.0에서 제거되었습니다.&lt;/p&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://dev.mysql.com/doc/refman/5.7/en/information-schema-innodb-locks-table.html&quot;&gt;https://dev.mysql.com/doc/refman/5.7/en/information-schema-innodb-locks-table.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;대신 performance_schema의 metadata_locks Table에서 관리됩니다.&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;mysql&amp;gt; select get_lock('mylock_1', 10);

mysql&amp;gt; select * from performance_schema.metadata_locks&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cf2v97/btssqz073Ft/4vSG892v10E0wf9yUSsWq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cf2v97/btssqz073Ft/4vSG892v10E0wf9yUSsWq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cf2v97/btssqz073Ft/4vSG892v10E0wf9yUSsWq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcf2v97%2Fbtssqz073Ft%2F4vSG892v10E0wf9yUSsWq0%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;1280&quot; height=&quot;101&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&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;metadata_locks 테이블 중 알면 좋겠는 부분만 설명하겠습니다&lt;/p&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://dev.mysql.com/doc/refman/8.0/en/performance-schema-metadata-locks-table.html&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/performance-schema-metadata-locks-table.html&lt;/a&gt; 에서 더 자세하게 확인 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OBJECT_TYPE&lt;/b&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;/li&gt;
&lt;li&gt;값은 GLOBAL, SCHEMA, TABLE, FUNCTION, PROCEDURE, TRIGGER (현재 사용되지 않음), EVENT, COMMIT, USER LEVEL LOCK, TABLESPACE, BACKUP LOCK, 또는 LOCKING SERVICE 중 하나입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USER LEVEL LOCK 값&lt;/code&gt;은 GET_LOCK()로 획득한 잠금을 나타냅니다.&lt;/li&gt;
&lt;li&gt;LOCKING SERVICE 값은 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/locking-service.html&quot;&gt;Section 5.6.9.1, &amp;ldquo;The Locking Service&amp;rdquo;&lt;/a&gt; 에 설명된 잠금 서비스로 획득한 잠금을 나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OBJECT_NAME&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LOCK_TYPE&lt;/b&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;/li&gt;
&lt;li&gt;값은 INTENTION_EXCLUSIVE, SHARED, SHARED_HIGH_PRIO, SHARED_READ, SHARED_WRITE, SHARED_UPGRADABLE, SHARED_NO_WRITE, SHARED_NO_READ_WRITE, 또는 EXCLUSIVE 중 하나입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;네임드 락 같은경우 EXCLUSIVE가 됩니다.&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LOCK_DURATION&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메타데이터 잠금 하위 시스템에서의 잠금 기간입니다. 값은 STATEMENT, TRANSACTION, 또는 EXPLICIT 중 하나입니다.&lt;/li&gt;
&lt;li&gt;EXPLICIT 값은 문 또는 트랜잭션 종료 후에도 유지되고, FLUSH TABLES WITH READ LOCK과 같은 명시적 작업으로 해제되는 전역 잠금을 나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OWNER_THREAD_ID&lt;/b&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;/li&gt;
&lt;li&gt;네임드 락의 경우 스레드 Id가 들어가며, performance_schema.threads에서 세션 id를 얻을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/performance-schema-metadata-locks-table.html&quot;&gt;&lt;code&gt;metadata_locks&lt;/code&gt;&lt;/a&gt; 테이블에는 다음과 같은 인덱스가 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 키 on (&lt;code&gt;OBJECT_INSTANCE_BEGIN&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;인덱스 on (&lt;code&gt;OBJECT_TYPE&lt;/code&gt;, &lt;code&gt;OBJECT_SCHEMA&lt;/code&gt;, &lt;code&gt;OBJECT_NAME&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;인덱스 on (&lt;code&gt;OWNER_THREAD_ID&lt;/code&gt;, &lt;code&gt;OWNER_EVENT_ID&lt;/code&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;metadata_locks 테이블에 대해서는 TRUNCATE TABLE이 허용되지 않습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.3. 네임드락(Named Lock의) 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;불필요한 부하&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;락에 대한 정보가 DB에 저장되고, 락을 획득하고 제거하는 쿼리가 매번 발생하여 DB에 불필요한 부하를 줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;그러나 대부분의 상황에서는 미미한 편입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제한적이다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MySQL에서만 사용가능하다는 단점&lt;/li&gt;
&lt;li&gt;JPA에서는 nativeQuery를 사용해야 함.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데드락 위험&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Named Locks와 테이블 레벨 락, 행 레벨 락을 혼용하여 사용할 경우 데드락 상황이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션이 종료되면 잠금이 해제되는 문제 - &lt;a href=&quot;https://techblog.woowahan.com/2631/&quot;&gt;우아한형제들 기술블로그&lt;/a&gt;, &lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html&quot;&gt;MySQL 공식 문서&lt;/a&gt;에 적힌 내용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Named Locks는 세션 범위를 가지며, 다른 세션에서는 해당 락의 상태를 변경할 수 없기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션에서 같은 커넥션으로 관리해야하는 문제&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션풀이 부족&lt;/b&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;a href=&quot;https://kwonnam.pe.kr/wiki/database/mysql/user_lock&quot;&gt;참고&lt;/a&gt;) 주의할 점은, Named Lock을 활용할 때 데이터소스를 분리하지않고 하나로 사용하게되면 커넥션풀이 부족해질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Named Lock은 한 MySQL 서버 인스턴스에서만 유효합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 여러 서버 인스턴스(클러스터 등)가 있는 분산 환경에서는 한 서버에서 설정된 Named Lock이 다른 서버에는 적용되지 않으므로 분산 시스템에는 적합하지 않을수도 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제점들로 인해, 분산 시스템에서는 주로 분산 락 전용 솔루션(예: Apache ZooKeeper, etcd, Redis의 RedLock 등)을 사용하여 관리하는 것이 좋을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;3. Spring에서의 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JdbcTemplate, EntityManager, JpaRepository를 이용해 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통점으로는 JPA에서는 지원하지 않으므로, native query로 구성해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. JdbcTemplate으로 구현&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class NamedLockWithJdbcTemplate(
    private val jdbcTemplate: NamedParameterJdbcTemplate
) {

    private val log = LoggerFactory.getLogger(this::class.java)

    fun getLock(userLockName: String, timeoutSeconds: Int): Boolean {
        val sql = &quot;SELECT GET_LOCK(:userLockName, :timeoutSeconds)&quot;
        val params = mapOf(&quot;userLockName&quot; to userLockName, &quot;timeoutSeconds&quot; to timeoutSeconds)

        return convertResult(jdbcTemplate.queryForObject(sql, params, Int::class.java))
    }

    fun releaseLock(userLockName: String): Boolean {
        // Release the named lock
        val sql = &quot;SELECT RELEASE_LOCK(:userLockName)&quot;
        val params = mapOf(&quot;userLockName&quot; to userLockName)

        return convertResult(jdbcTemplate.queryForObject(sql, params, Int::class.java))
    }

    private fun convertResult(result: Int?): Boolean {
        return when (result) {
            1 -&amp;gt; {
                log.info(&quot;lock 획득&quot;)
                true
            }
            0, null -&amp;gt; {
                log.info(&quot;lock 획득 실패&quot;)
                false
            }
            else -&amp;gt; {
                log.error(&quot;예상치 못한 결과: $result&quot;)
                false
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. EntityManager로 구현&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class NamedLockWithEntityManager(
    @PersistenceContext
    private val entityManager: EntityManager
) {

    fun getLock(userLockName: String, timeoutSeconds: Int): Boolean {
        val query: Query = entityManager.createNativeQuery(&quot;SELECT GET_LOCK(:userLockName, :timeoutSeconds)&quot;)
            .setParameter(&quot;userLockName&quot;, userLockName)
            .setParameter(&quot;timeoutSeconds&quot;, timeoutSeconds)

        return convertResult(query.singleResult as Int?)
    }

    fun releaseLock(userLockName: String): Boolean {
        // Release the named lock
        val query: Query = entityManager.createNativeQuery(&quot;SELECT RELEASE_LOCK(:userLockName)&quot;)
            .setParameter(&quot;userLockName&quot;, userLockName)

        return convertResult(query.singleResult as Int?)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3. JpaRepository로 구현&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;interface NamedLockWithJpaRepository : JpaRepository&amp;lt;Item, Long&amp;gt; {

    @Query(value = &quot;select GET_LOCK(:key, :timeoutSeconds)&quot;, nativeQuery = true)
    fun getLock(key: String, timeoutSeconds: Int): Long?

    @Query(value = &quot;select RELEASE_LOCK(:key)&quot;, nativeQuery = true)
    fun releaseLock(key: String) : Long?
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4. 사용&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class ItemNamedLockService(
    private val itemService: ItemService,
    private val namedLockWithJdbcTemplate: NamedLockWithJdbcTemplate
) {

    fun decreaseStock(itemId: Long, quantity: Long) {
        val key = LOCK_PREFIX + itemId.toString()

        try {
            // 락 획득. (락 획득을 대기할 타임아웃, 락이 만료되는 시간)
            namedLockWithJdbcTemplate.getLock(key, 3000)

            // 재고 감소
            itemService.decrease(itemId, quantity)

        } finally {
            // 락 해제
            namedLockWithJdbcTemplate.releaseLock(key)
        }
    }

    companion object {
        private const val LOCK_PREFIX = &quot;LOCK:&quot;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복적인 try-finally 문은 AOP 또는 함수를 전달해줌으로써 해결해줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun &amp;lt;T&amp;gt; executeWithLock(
    userLockName: String,
    timeoutSeconds: Int,
    action: () -&amp;gt; T
): T = try {

    getLock(userLockName, timeoutSeconds)

    action()

} finally {
    releaseLock(userLockName)
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;fun decreaseStock2(itemId: Long, quantity: Long) {
  val key = LOCK_PREFIX + itemId.toString()

  val result = namedLockWithJdbcTemplate.executeWithLock(&quot;key&quot;, 3000) 
  { itemService.decrease(itemId, quantity) }    
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4. Name Lock 구현하여 사용시 주의할점&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.1. 반복하여 중첩된 락을 해제할때는 동일 커넥션에서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 커넥션에서 GET_LOCK을 여러번하면 여러번 락이 잡히므로 RELEASE_LOCK을 동일 커넥션에서 해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 lock이 풀리지 않게 됩니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try-finally 문으로 application에서 Connection Pool 사용시 항상 동일 Lock 문자열에 대한 GET_LOCK과 RELEASE_LOCK 이 동일 커넥션에서 동일 횟수만큼 이뤄짐을 보장해줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Lock이 중첩된 경우 한번에 해제하기 어렵다면 &lt;code&gt;SELECT RELEASE_ALL_LOCKS();&lt;/code&gt; 를 이용하여 모든 락을 해제할 수 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Lock을 이용하여 진행중인 프로세스가 있을수 있으니 주의해야 합니다.&lt;/p&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;#%EC%84%B8%EC%85%98%EC%9D%B4-%EC%A2%85%EB%A3%8C%EB%90%98%EC%97%88%EB%8A%94%EB%8D%B0-named-lock%EC%9D%B4-%ED%95%B4%EC%A0%9C%EB%90%98%EC%A7%80-%EC%95%8A%EC%9D%84-%EA%B2%BD%EC%9A%B0-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95&quot;&gt;세션이 종료되었는데, Named Lock이 해제되지 않을 경우 해결방법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.2. 같은 커넥션을 이용해서 Lock을 설정하고 로직을 수행한 후 Lock을 해제해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Named락의 특성을 잘 고려해야합니다.&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;GET_LOCK() 을 이용하여 획득한 잠금은 Transaction 이 commit 되거나 rollback 되어도 해제되지 않습니다.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;세션(커넥션)이 종료될때에는 암시적으로 해제됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나아가 동시에 동일한 Lock 이름으로 요청한 다른 스레드에서 &lt;code&gt;GET_LOCK()&lt;/code&gt; 에서 반환했던 Connection 을 획득하여 락을 풀어버릴 위험도 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션 풀을 공유하기 때문에 우연치 않게 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 같은 커넥션을 이용해서 Lock을 설정하고, Logic을 수행한 후에 Lock을 해제하도록 구현해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면 &lt;a href=&quot;https://kwonnam.pe.kr/wiki/springframework&quot;&gt;Spring Framework&lt;/a&gt;의 트랜잭션에 의존하지 말고, &lt;b&gt;별도의 DataSource를 생성하여 직접 Lock 용 커넥션과 트랜잭션을 컨트롤&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Repository
class NamedLockWithDataSource(
    private val dataSource: DataSource
) {

    fun &amp;lt;T&amp;gt; executeWithLock(
        userLockName: String,
        timeoutSeconds: Int,
        action: () -&amp;gt; T
    ): T = dataSource.connection.use { connection -&amp;gt;
        getLock(connection, userLockName, timeoutSeconds)

        val result = action()

        releaseLock(connection, userLockName)

        return result
    }

    private fun getLock(
        connection: Connection,
        userLockName: String,
        timeoutSeconds: Int
    ) = connection.prepareStatement(GET_LOCK).use { preparedStatement -&amp;gt;

        preparedStatement.setString(1, userLockName)
        preparedStatement.setInt(2, timeoutSeconds)

        checkResultSet(userLockName, preparedStatement, LockFunction.GET_LOCK)
    }

    private fun releaseLock(
        connection: Connection,
        userLockName: String
    ) = connection.prepareStatement(RELEASE_LOCK).use { preparedStatement -&amp;gt;

        preparedStatement.setString(1, userLockName)

        checkResultSet(userLockName, preparedStatement, LockFunction.RELEASE_LOCK)
    }

    private fun checkResultSet(
        lockName: String,
        preparedStatement: PreparedStatement,
        type: LockFunction
    ) = preparedStatement.executeQuery().use { resultSet -&amp;gt;

        if (!resultSet.next()) {
            log.error(&quot;Named Lock 결과 값이 없습니다. type = ${type.name}, lockName $lockName&quot;)

            throw RuntimeException(EXCEPTION_MESSAGE)
        }

        val result = resultSet.getInt(1)

        if (result != 1) {
            log.error(&quot;Named Lock 획득 실패  type = ${type.name}, lockName $lockName&quot;)

            throw RuntimeException(EXCEPTION_MESSAGE)
        }

    }

    companion object {
        private val log = LoggerFactory.getLogger(NamedLockWithDataSource::class.java)
        private const val GET_LOCK = &quot;SELECT GET_LOCK(?, ?)&quot;
        private const val RELEASE_LOCK = &quot;SELECT RELEASE_LOCK(?)&quot;
        private const val EXCEPTION_MESSAGE = &quot;NAMED LOCK 을 수행하는 중에 오류가 발생하였습니다.&quot;
    }

    enum class LockFunction {
        GET_LOCK,
        RELEASE_LOCK
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5. metadata_locks로 어떤 세션이 네임드 락을 걸었는지 확인하는법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Named Lock을 걸게 되면 다음과 같은 정보가 &lt;code&gt;metadata_locks 테이블&lt;/code&gt;에 저장됩니다&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;mysql&amp;gt; select get_lock('mylock_1', 10);

mysql&amp;gt; select * from performance_schema.metadata_locks&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QVk0g/btssfVkpDjv/jL7BwcZEG7qjSDCXhfcwZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QVk0g/btssfVkpDjv/jL7BwcZEG7qjSDCXhfcwZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QVk0g/btssfVkpDjv/jL7BwcZEG7qjSDCXhfcwZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQVk0g%2FbtssfVkpDjv%2FjL7BwcZEG7qjSDCXhfcwZ1%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;1280&quot; height=&quot;101&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이때, owner_thread_id로 현재 커넥션이 누군지 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 활성 커넥션 확인 방법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;mysql&amp;gt; SHOW PROCESSLIST;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션 종료 방법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;mysql&amp;gt; KILL connection_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 세션(커넥션) id 보는법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT CONNECTION_ID();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 세션의 OWNER_THREAD_ID 확인법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT THREAD_ID as owner_thread_id
FROM performance_schema.threads
WHERE PROCESSLIST_ID = CONNECTION_ID();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OWNER_THREAD_ID로 connection_id를 얻는법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT PROCESSLIST_ID as connection_id
FROM performance_schema.threads
WHERE THREAD_ID = ?;  -- 여기에 owner_thread_id 값을 대입합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.1. 세션이 종료되었는데, Named Lock이 해제되지 않을 경우 해결방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 종료되었다고 생각했지만, Named Lock이 해제되지 않을 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, metadata_locks은 root 유저로도 truncate를 할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 강제로 세션을 종료 시켜 Lock을 해제할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물론 주의해서 사용해야 합니다. 실제 LOCK을 이용하고 있는지, 아닌지 알 수 없기 때문입니다.&lt;/li&gt;
&lt;li&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번과 2번 세션이 있다.&lt;br /&gt;1번 세션의 세션 ID는 733이며 2번 세션의 ID는 631이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 세션에서 'test' 라는 named Lock을 걸었고 그냥 종료했는데, 2번 세션에서 'test'라는 named Lock을 사용하고 싶어도 사용하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;named_lock 특성상 lock을 건 세션이 종료되면 lock도 해제되기 때문에 강제 해제해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번 세션(id 733)에서 'test' named lock을 건다&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;mysql(1번, id: 733)&amp;gt; select get_lock('test', 3);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;metadata_locks확인&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;select * from performance_schema.metadata_locks;

// 출력 간소화
+-----------------+----------------+-----------------+
| OBJECT_TYPE     | OBJECT_NAME    | OWNER_THREAD_ID |
+-----------------+----------------+-----------------+
| USER LEVEL LOCK | test           |             772 | // 772번 스레드가 test 라는 락을 건것을 확인
| TABLE           | metadata_locks |             772 |
+-----------------+----------------+-----------------+&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;OWNER_THREAD_ID는 세션 ID가 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세션 ID 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;mysql&amp;gt; SELECT PROCESSLIST_ID as connection_id
    -&amp;gt; FROM performance_schema.threads
    -&amp;gt; WHERE THREAD_ID =772;
+---------------+
| connection_id |
+---------------+
|           733 |
+---------------+&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;OWNER_THREAD_ID가 772번 인 커넥션의 id는 733이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번 세션 종료. 해당 콘솔로 다시 접근 불가 상태.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2번 세션(id 631) 에서 해당 'test' lock을 걸라고 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;mysql&amp;gt; select get_lock('test', 3);
+---------------------+
| get_lock('test', 3) |
+---------------------+
|                   0 |
+---------------------+
1 row in set (3.01 sec)&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;3초를 기다렸지만 0을 반납.&lt;/li&gt;
&lt;li&gt;락을 획득할 수 없다. 왜냐하면 733번 션에서 해제하지 않았기 때문에 다른 세션(커넥션) 에서는 쓸수없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 때, 'test' 락을 갖고 있는 세션을 확인해서 종료시키면 됩니다&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;select * from performance_schema.metadata_locks;&lt;/code&gt; 를 이용해서 조회한 OWNER_THREAD_ID로&lt;/li&gt;
&lt;li&gt;connection_id를 획득 후,&lt;/li&gt;
&lt;li&gt;kill 프로세스ID로 종료하면 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;-- 1. lock을 갖고 있는 스레드 확인
mysql&amp;gt; select * from performance_schema.metadata_locks; // 772번 쓰레드가 test 락을 갖고있는것을 확인

+-----------------+----------------+-----------------+
| OBJECT_TYPE     | OBJECT_NAME    | OWNER_THREAD_ID |
+-----------------+----------------+-----------------+
| USER LEVEL LOCK | test           |             772 | // 772번 스레드가 test 라는 락을 건것을 확인
| TABLE           | metadata_locks |             772 |
+-----------------+----------------+-----------------+

-- 2. 세션 ID 확인

mysql&amp;gt; SELECT PROCESSLIST_ID as connection_id
    -&amp;gt; FROM performance_schema.threads
    -&amp;gt; WHERE THREAD_ID =772;
+---------------+
| connection_id |
+---------------+
|           733 |
+---------------+

-- 3. kill process 733번 세션 
mysql&amp;gt; kill 733
Query OK, 0 rows affected (0.01 sec)

-- 4. metadata_locks를 확인

mysql&amp;gt; select * from performance_schema.metadata_locks;

+-------------+----------------+-----------------+
| OBJECT_TYPE | OBJECT_NAME    | OWNER_THREAD_ID |
+-------------+----------------+-----------------+
| TABLE       | metadata_locks |             670 |
+-------------+----------------+-----------------+&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.2. 참조&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Real MySQL&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://blog.saltfactory.net/introduce-mysql-lock/&quot;&gt;MySQL에서 사용하는 Lock 이해&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.woowahan.com/2631/&quot;&gt;MySQL을 이용한 분산락으로 여러 서버에 걸친 동시성 관리 | 우아한형제들 기술블로그&lt;/a&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://velog.io/@this-is-spear/MySQL-Named-Lock#%EB%B6%84%EC%82%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C%EB%8A%94-%EC%A0%81%ED%95%A9%ED%95%98%EC%A7%80-%EC%95%8A%EB%8B%A4&quot;&gt;https://velog.io/@this-is-spear/MySQL-Named-Lock#%EB%B6%84%EC%82%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C%EB%8A%94-%EC%A0%81%ED%95%A9%ED%95%98%EC%A7%80-%EC%95%8A%EB%8B%A4&lt;/a&gt;*&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/performance-schema-metadata-locks-table.html&quot;&gt;https://dev.mysql.com/doc/refman/8.0/en/performance-schema-metadata-locks-table.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database/MySQL</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/255</guid>
      <comments>https://0soo.tistory.com/255#entry255comment</comments>
      <pubDate>Sun, 27 Aug 2023 16:46:56 +0900</pubDate>
    </item>
    <item>
      <title>프로메테우스, 그라파나, springboot 이용한 모니터링 with , security, docker</title>
      <link>https://0soo.tistory.com/251</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dT8oSS/btsozj897v6/wJXUkuYLySaOOUippOJ110/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dT8oSS/btsozj897v6/wJXUkuYLySaOOUippOJ110/img.png&quot; data-alt=&quot;출처 : https://refactorfirst.com/spring-boot-prometheus-grafana&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dT8oSS/btsozj897v6/wJXUkuYLySaOOUippOJ110/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdT8oSS%2Fbtsozj897v6%2FwJXUkuYLySaOOUippOJ110%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;478&quot; height=&quot;268&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://refactorfirst.com/spring-boot-prometheus-grafana&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영중인 서버의 모니터링을 위해, 프로메테우스 + 그라파나를 이용해서 메트릭을 수집하고 모니터링 하기 위한 설정 방법에 대해 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;환경&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kotlin&lt;/li&gt;
&lt;li&gt;SpringBoot 2.7.4&lt;/li&gt;
&lt;li&gt;Spring actuator, Security with Jwt Auth &amp;amp; Basic Auth&lt;/li&gt;
&lt;li&gt;docker, docker-compose&lt;/li&gt;
&lt;li&gt;NCP ubuntu 18 linux&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존성 추가&lt;/h2&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.springframework.boot&quot;) version &quot;2.7.4&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.0.14.RELEASE&quot;
    kotlin(&quot;jvm&quot;) version &quot;1.6.21&quot;
    kotlin(&quot;plugin.spring&quot;) version &quot;1.6.21&quot;
    kotlin(&quot;kapt&quot;) version &quot;1.7.10&quot;
    idea
}


dependencies {
    ...
  implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation(&quot;org.springframework.boot:spring-boot-starter-actuator&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-security&quot;)
  implementation(&quot;io.micrometer:micrometer-registry-prometheus&quot;) 
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  config:
    activate:
      on-profile: develop # develop profile

management:
  endpoints:
    web:
      exposure:
        include: &quot;*&quot; #  이 설정은 모든 Actuator 엔드포인트를 외부에 노출
      base-path: system/actuator # Actuator 엔드포인트의 기본 경로를 system/actuator로 지정
  server:
    port: 8090 # Actuator 엔드포인트가 서비스될 서버의 포트를 8090으로 지정
  endpoint:
    health:
      show-details: always # health 엔드포인트가 상세 정보를 항상 표시하도록 함

server:
  tomcat: #  tomcat 메트릭 설정 on. 톰캣 메트릭은 tomcat. 으로 시작
    mbeanregistry: # 톰캣 메트릭을 모두 사용하려면 다음 옵션을 켜야하며, 옵션을 켜지 않으면 tomcat.session. 관련정보만 노출됩니다
      enabled: true

actuator: # basic 인증에 사용하기 위한 커스텀 Properties
  user: ysk
  password: yskgood
  role-name: ACTUATOR&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로메테우스에서 scrape 할 때 basic auth를 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jwt 인증을 할 수도 있지만 설정의 간소화를 위해 basic auth를 사용했습니다.&lt;/li&gt;
&lt;li&gt;프로메테우스의 다양한 scrape 인증 방법은 아래에 정리하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Basic auth란?- 기본 인증&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP가 액세스 제어와 인증을 위한 프레임워크 중 가장 일반적인 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 사용자가 누구인지 식별 할 수 있어야 함으로 authentication를 통하여 식별하여 접근 권한을 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 호출 시 &lt;b&gt;Authorization 헤더&lt;/b&gt;에 user id와 password를 &lt;b&gt;base64&lt;/b&gt;로 인코딩한 문자열을 추가하여 인증하는 형식&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Security&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 api 서버의 기본 인증방식은 jwt 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 basic auth를 위한 추가적인 security 설정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 여러 FilterChain을 지원합니다. 여러 FilterChain을 사용하면 각기 다른 보안 정책을 다른 URL 패턴에 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 설정을 위해 basic auth를 위해 추가적인 설정을 합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
@Configuration
@EnableWebSecurity
class WebSecurityConfig(
    private val actuatorProperties: ActuatorProperties,
      private val passwordEncoder: PasswordEncoder
) {

    @Bean
    @Order(0) // jwt 인증을 위한 Security FilterChain 설정
    fun apiSecurity(http: HttpSecurity): SecurityFilterChain {
        http.httpBasic().disable()
            .cors().and()
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()

            .authorizeRequests()
            .antMatchers(&quot;/api/**&quot;).hasAnyAuthority(...)) // here
            .anyRequest().authenticated()

            .and()
            .addFilterBefore(
                JwtAuthenticationFilter(authService),
                UsernamePasswordAuthenticationFilter::class.java
            )

                  ...

        return http.build()
    }

    @Bean
    @Order(1) // basic auth를 위한 Security Filter chain 설정
    fun actuatorSecurity(http: HttpSecurity, passwordEncoder: PasswordEncoder) : SecurityFilterChain {
        http
            .requestMatchers().antMatchers(&quot;/system/actuator/**&quot;)
            .and()
            .httpBasic()
            .and().userDetailsService(userDetailsService(passwordEncoder))
            .cors().and()
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers(&quot;/api/system/actuator/**&quot;).hasRole(actuatorProperties.roleName)

            .anyRequest().denyAll()

        return http.build()
    }

    fun userDetailsService(): UserDetailsService { // basic auth를 위한 UserDetailsService
        val user = User.withUsername(actuatorProperties.user)
            .password(passwordEncoder.encode(actuatorProperties.password))
            .roles(actuatorProperties.roleName)
            .build()

        return InMemoryUserDetailsManager(user)
    }

  ...
}

//
@ConstructorBinding
@ConfigurationProperties(prefix = &quot;actuator&quot;) // application.yml에 설정된 값을 읽어 바인딩
data class ActuatorProperties(
    val user: String,
    val password : String,
    val roleName: String,
) {
}&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;Spring의 @Order 어노테이션은 특정 타입의 빈들이 여러 개 있을 경우 어떤 순서로 처리될지를 정의합니다. 이 어노테이션은 빈들 간의 실행 순서를 지정하는데 사용됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Order(number)는 . 숫자가 작을 수록 우선순위가 높고, 필터 체인 리스트에 들어가는 순서가 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@Order(0) - apiSecurity: 제일 높은 우선순위이며 jwt 인증을 위한 filterchain입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;, /api/** endpoint로 들어오는 요청을 해당 filterchain이 인증 / 인가처리합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;@Order(1) - actuatorSecurity : 다음 우선순위이며 basic auth 인증을 위한 filterchain입니다,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/system/actuator/** 로 들어오는 요청을 해당 filterchain이 인증/ 인가처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Order(0) apiSecurity : .antMatchers(&quot;/api/**&quot;)
@Order(1) actuatorSecurity : .antMatchers(&quot;/system/actuator/**&quot;)

1. `/api/**`으로 접속 -&amp;gt; apiSecurity FilterChain 실행
2. `/system/actuator/**`으로 접속 -&amp;gt; actuatorSecurity FilterChain 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml에 설정한 값으로 basic auth를 처리하도록 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 property는 ActuatorProperties class에 바인딩됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보안을 위해 다른 방법이 많으나, basic auth 간소화를 위함입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;actuator: # basic 인증에 사용하기 위한 커스텀 Properties
  user: ysk
  password: yskgood
  role-name: ACTUATOR&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 /api/** 로 오는 요청들은 해당 filterchain이 jwt 인증을 처리하며, basic auth로는 인증이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/system/actuator로 오는 요청들은 해당 filterchain이 basic auth 인증을 처리하며, jwt auth로는 인증이 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 클라우드 (Naver Cloud Platform)의 Server instance 에서 도커를 설치하고, docker-compose 파일 작성입니다.&lt;/p&gt;
&lt;h1&gt;Docker-compose 파일 작성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus, grafana를 Docker에 설치하기 위해 docker-compose.yml, 파일 및 디렉토리를 구성해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;version: '3.3'  # 파일 규격 버전
services:       # 이 항목 밑에 실행하려는 컨테이너 들을 정의
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus/config:/etc/prometheus
      - ./prometheus/volume:/prometheus
    ports:
      - 9090:9090 # 접근 포트 설정 (컨테이너 외부:컨테이너 내부)
    command: # web.enalbe-lifecycle은 api 재시작없이 설정파일들을 reload 할 수 있게 해줌
      - '--web.enable-lifecycle'
      - '--config.file=/etc/prometheus/prometheus.yml'
    restart: always
    networks:
      - promnet

  grafana:
    image: grafana/grafana
    container_name: grafana
    # user: &quot;$GRA_UID:$GRA_GID&quot;
    ports:
      - 3000:3000 # 접근 포트 설정 (컨테이너 외부:컨테이너 내부)
    volumes:
      - ./grafana/volume:/var/lib/grafana
      - ./grafana/provisioning/:/etc/grafana/provisioning/
    restart: always
    networks:
      - promnet
    depends_on:
      - prometheus

networks:
  promnet:
    driver: bridge&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;Prometheus DockerHub image
&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://hub.docker.com/r/prom/prometheus&quot;&gt;https://hub.docker.com/r/prom/prometheus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Prometheus의 저장 디렉토리는 ./prometheus/volume으로 지정하였습니다.&lt;/li&gt;
&lt;li&gt;Prometheus의 설정 디렉토리는 ./prometheus/config으로 지정하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 파일을 다 생성하면 아래와 같은 구조가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;.
├── docker-compose.yml
└── prometheus
    └── config
        ├── prometheus.yml
        └── rule.yml&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;/prometheus 디렉토리의 권한을 docker에서 수정할 수 있도록 변경합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sudo chmod -R 777 ./prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 로컬에서 디렉토리와 파일을 만들어 전송하였습니다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt; scp -P [포트번호] -r [디렉토리이름] 사용자@IP:서버의 디렉토리 위치&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 프로메테우스 설정 파일을 작성해보겠습니다.&lt;/p&gt;
&lt;h1&gt;프로메테우스 설정파일 작성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로메테우스는 prometheus/config/prometheus.yml 설정 파일을 읽어 동작합니다.&lt;/p&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://prometheus.io/docs/prometheus/latest/configuration/configuration&quot;&gt;https://prometheus.io/docs/prometheus/latest/configuration/configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석때문에 복잡해보이지만 어렵지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prometheus.yml&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;global:
  scrape_interval: 15s     # scrap target의 기본 interval을 15초로 변경 / default = 1m
  scrape_timeout: 15s      # scrap request 가 timeout waite/ default = 10s

  external_labels:
    monitor: 'ysk-monitor'       # 기본적으로 붙여줄 라벨
  query_log_file: query_log_file.log # prometheus의 쿼리 로그들을 기록. 설정되지않으면 기록하지않는다.

# 매트릭을 수집할 엔드포인드로 여기선 Prometheus 서버 자신을 가리킨다.
scrape_configs:

 # 이 설정에서 수집한 타임시리즈에 `job=&amp;lt;job_name&amp;gt;`으로 잡의 이름을 설정.
 # metrics_path의 기본 경로는 '/metrics'이고 scheme의 기본값은 `http`다

  # 여기를 추가! 
  - job_name: &quot;spring-actuator&quot; # job_name 은 모든 scrap 내에서 고유해야한다
    metrics_path: '/system/actuator/prometheus' # 스프링부트에서 설정한 endpoint
    scrape_interval: 15s # global에서 default 값을 정의해주었기 떄문에 안써도 된다. 
    scheme: 'http'            # request를 보낼 scheme 설정 | default = http
    static_configs:
      - targets: ['서버주소:8090'] # request를 보낼 server ip 그리고 actuator port를 적어주면 된다.
    basic_auth:
      username: 'ysk'
      password: 'yskgood'&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;job_name : 수집하는 이름이다. 임의의 이름을 사용하면 됩니다.&lt;/li&gt;
&lt;li&gt;metrics_path : 수집할 경로를 지정.&lt;/li&gt;
&lt;li&gt;scrape_interval : 수집할 주기를 설정.&lt;/li&gt;
&lt;li&gt;targets : 수집할 서버의 IP, PORT를 지정.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외에도 알림, rule 등 다양한 설정을 할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로메테우스의 수집 요청시 다양한 인증방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus에서 원격 시스템 (ex actuator)에서 scrape 하는 방식에 대한 인증 설정은 다음과 같이 다양합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus의 HTTP 인증 설정 : &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config&quot;&gt;https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Prometheus의 Configuration 문서: &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/configuration/configuration/&quot;&gt;https://prometheus.io/docs/prometheus/latest/configuration/configuration/&lt;/a&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;VPN, VPC을 이용한 내부망으로 호출할 수도 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;none auth : 인증방식 없이 호출합니다. 이러면 endpoint가 노출되서 위험합니다.&lt;/li&gt;
&lt;li&gt;basic auth&lt;/li&gt;
&lt;li&gt;bearer token&lt;/li&gt;
&lt;li&gt;authorization&lt;/li&gt;
&lt;li&gt;oauth2&lt;/li&gt;
&lt;li&gt;tlsconfig&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;basic auth&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;basic_auth: 기본 인증을 설정하는 방법으로, username과 password 또는 비밀번호가 저장된 파일인 password_file을 제공하여 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;basic_auth:
  [ username: &amp;lt;string&amp;gt; ]
  [ password: &amp;lt;secret&amp;gt; ]
  [ password_file: &amp;lt;filename&amp;gt; ] // 또는 이렇게 설정&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;bearer token - with jwt&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bearer_token 또는 bearer_token_file: Bearer 토큰을 설정하는 방법으로, 일반적으로 JWT와 같은 토큰 기반 인증에 사용됩니다. bearer_token에 직접 토큰을 제공하거나 bearer_token_file를 통해 토큰이 저장된 파일을 지정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[ bearer_token: &amp;lt;secret&amp;gt; ]
[ bearer_token_file: &amp;lt;filename&amp;gt; ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex)&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;...

scrape_configs:
  - job_name: 'test'
      metrics_path: &quot;/metrics&quot;
      scheme: &quot;http&quot;
      bearer_token_file: /var/run/secrets/    OR   bearer_token: token_here
    static_configs:
              - targets: ['host.com']&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;authorization&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;authorization: 모든 스크랩 요청에 Authorization 헤더를 설정합니다. 토큰 유형 및 크리덴셜(토큰 값 또는 토큰 파일)을 설정할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;authorization:
  [ type: &amp;lt;string&amp;gt; | default: Bearer ]
  [ credentials: &amp;lt;secret&amp;gt; ]
  [ credentials_file: &amp;lt;filename&amp;gt; ]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;oauth&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;oauth2: OAuth 2.0을 사용하여 인증을 설정하는 경우에 사용됩니다. 이는 client_id, client_secret, token_url 등 다양한 OAuth 2.0 관련 설정을 제공해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;oauth2:
  client_id: &amp;lt;string&amp;gt;
  client_secret: &amp;lt;string&amp;gt;
  token_url: &amp;lt;string&amp;gt;
  [ scope: &amp;lt;string&amp;gt; ]
  [ endpoint_params: { [&amp;lt;string&amp;gt;: &amp;lt;string&amp;gt;] } ]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;tls_config&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tls_config: 이 섹션은 TLS를 사용하여 인증을 수행하는 경우에 사용됩니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;tls_config:
  [ ca_file: &amp;lt;filename&amp;gt; ]
  [ cert_file: &amp;lt;filename&amp;gt; ]
  [ key_file: &amp;lt;filename&amp;gt; ]
  # ... 추가 설정은 가능합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할점은 위의 방법들 중 일부는 서로 동시에 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, basic_auth와 authorization 설정은 동시에 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정하는 방식은 사용하려는 인증 방법에 따라 다르며, 각 설정에 대한 자세한 사항은 Prometheus의 공식 문서를 참고해서 볼 수 있습니다.&lt;/p&gt;
&lt;h1&gt;docker-compose 프로메테우스 그라파나 실행&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버로 전송이 완료되면, docker-compose.yml 파일이 있는곳으로 이동하여 docker-compose 명령어로 실행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행하기 전에 아래의 sudo chmod -r 777 명령어로 미리 권한을 변경해두면 좋습니다.&lt;/li&gt;
&lt;li&gt;만약, 실행하기 전에 권한을 주지 않았다면, 권한을 주고 prometheus랑 grafana를 재시작해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker-compose up -d &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의사항은 반드시 /prometheus 디렉토리의 권한을 docker에서 수정할 수 있도록 변경해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미리 권한을 주는 다른 방법이 있지만, 현재는 이렇게 해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sudo chmod -R 777 ./prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안그러면 권한때문에 오류가 나서 실행 안될수도 있습니다&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;docker ps
docker logs -f prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어로 확인 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그라파나도 마찬가지입니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행이 안된다면 /grafana 디렉토리의 권한을 docker에서 수정할 수 있도록 변경해야합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;sudo chmod -R 777 ./grafana&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안그러면 다음과 같은 오류가 나서 실행 안될수도 있습니다&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;&amp;gt; docker logs -f grafana

mkdir: can't create directory '/var/lib/grafana/plugins': Permission denied
GF_PATHS_DATA='/var/lib/grafana' is not writable.
You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migrate-to-v51-or-later&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한을 주고나서 prometheus랑 grafana를 재시작해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;docker stop prometheus
docker start promethues

//

docker stop grafana
docker start grafana&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;서버 접속&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://서버주소:9090&lt;/li&gt;
&lt;li&gt;그라파나랑 연결해두고, 그라파나로만 모니터링 하며 프로메테우스 포트는 접속 못하게 닫아두는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;인증은 다르게 처리해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/guides/basic-auth/&quot;&gt;https://prometheus.io/docs/guides/basic-auth/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Grafana
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://서버주소:3000
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 계정 ID/PW: admin/admin&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비밀번호를 변경할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로메테우스 접속&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://서버주소:9090/&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로메테우스 메뉴 -&amp;gt; Status Configuration 에 들어가서 prometheus.yml 에 입력한 부분이 추가되어 있는지 확인하면 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://서버주소:9090/config&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;1966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buA9ga/btsoxbdjbiC/lGXLi9ekmKD4gK7hgZrkw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buA9ga/btsoxbdjbiC/lGXLi9ekmKD4gK7hgZrkw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buA9ga/btsoxbdjbiC/lGXLi9ekmKD4gK7hgZrkw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuA9ga%2FbtsoxbdjbiC%2FlGXLi9ekmKD4gK7hgZrkw0%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;235&quot; height=&quot;530&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;1966&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보안을 위해 민감한 정보는 다 가렸으며, prometheus.yml에 작성한 내용들입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로메테우스 메뉴 Status Targets 에 들어가서 연동이 잘 되었는지 확인해봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://서버주소:포트번호/targets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c49bQm/btsoyVmVXEe/KmKyRl1tXKxHPEqs3VOQw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c49bQm/btsoyVmVXEe/KmKyRl1tXKxHPEqs3VOQw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c49bQm/btsoyVmVXEe/KmKyRl1tXKxHPEqs3VOQw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc49bQm%2FbtsoyVmVXEe%2FKmKyRl1tXKxHPEqs3VOQw0%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;548&quot; height=&quot;237&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;prometheus : 프로메테우스 자체에서 제공하는 메트릭 정보. (프로메테우스가 프로메테우스 자신의&lt;br /&gt;메트릭을 확인하는 것)&lt;/li&gt;
&lt;li&gt;spring-actuator : 우리가 연동한 애플리케이션의 메트릭 정보.&lt;/li&gt;
&lt;li&gt;State 가 UP 으로 되어 있으면 정상이고, DOWN 으로 되어 있으면 연동이 안된 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그라파나 접속&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://서버주소:3000&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 계정 ID/PW: admin/admin&lt;/li&gt;
&lt;/ul&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;http://서버Ip:3000/profile/password 에 접속&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&lt;i&gt;그라파나 데이터소스 추가 *&lt;/i&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;a href=&quot;http://localhost:3000/datasources&quot;&gt;http://localhost:3000/datasources&lt;/a&gt; 로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elgrnm/btsov5kFXu0/7D9TLOjcrgOl0mA9AqkXmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elgrnm/btsov5kFXu0/7D9TLOjcrgOl0mA9AqkXmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elgrnm/btsov5kFXu0/7D9TLOjcrgOl0mA9AqkXmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Felgrnm%2Fbtsov5kFXu0%2F7D9TLOjcrgOl0mA9AqkXmk%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;685&quot; height=&quot;200&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왼쪽 하단에 있는 설정(Configuration) 버튼에서 Data sources를 선택.&lt;/li&gt;
&lt;li&gt;Add data source 를 선택.&lt;/li&gt;
&lt;li&gt;Prometheus를 선택.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Prometheus 데이터 소스 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정한 url을 입력합니다. 현재 설정은 http://서버Ip:9090&lt;/li&gt;
&lt;li&gt;특별히 고칠 부분이 없다면 그대로 두고 Save &amp;amp; test 를 선택&lt;/li&gt;
&lt;li&gt;Data source is working 이라는 문구가 나오면 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http 설정을 &lt;a href=&quot;http://prometheus:9090&quot;&gt;http://prometheus:9090&lt;/a&gt; 로 해야합니다. 도커로 실행했기 때문에 컨테이너가 호스트명으로 통신할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://prometheus:9090&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b92mFK/btsozToRFBN/MXHKb1aXvb1HNTayFUJD11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b92mFK/btsozToRFBN/MXHKb1aXvb1HNTayFUJD11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b92mFK/btsozToRFBN/MXHKb1aXvb1HNTayFUJD11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb92mFK%2FbtsozToRFBN%2FMXHKb1aXvb1HNTayFUJD11%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;541&quot; height=&quot;648&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 프로메테우스 &amp;amp; 그라파나 설정을 마치겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 메트릭 수집이 필요하다면 actuator와 promethues, grafana로 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드를 사용해서 시각화를 할 수 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대시보드 -&amp;gt; new dashboard 선택후&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;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grafana.com/grafana/dashboards&quot;&gt;https://grafana.com/grafana/dashboards&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 프로메테우스, 그라파나는 각 그래프마다 경보(Alert)을 설정할 수 있으며 이메일, 슬랙을 포함한 다양한 알림 방법을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추후 다른 포스팅으로 정리할 예정입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로써, 엔지니어로써, 프로메테우스와 그라파나와 같은 도구를 이용해 실시간으로 시스템을 모니터링 하며, 문제가 발생하면 즉시 대응할 수 있도록 대비해야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전투에서 실패한 지휘관은 용서할 수 있지만 경계에서 실패하는 지휘관은 용서할 수 없다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;a href=&quot;https://www.devkuma.com/docs/prometheus/docker-compose-install/#google_vignette&quot;&gt;https://www.devkuma.com/docs/prometheus/docker-compose-install/#google_vignette&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/introduction/overview/&quot;&gt;https://prometheus.io/docs/introduction/overview/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://grafana.com/&quot;&gt;https://grafana.com/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;인프런 김영한님 강의 - Spring Boot&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>system</category>
      <category>docker compose prometheus</category>
      <category>Grafana</category>
      <category>spring boot</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/251</guid>
      <comments>https://0soo.tistory.com/251#entry251comment</comments>
      <pubDate>Fri, 21 Jul 2023 23:44:57 +0900</pubDate>
    </item>
    <item>
      <title>ubuntu, amazon linux2, centos7 도커 설치 명령어</title>
      <link>https://0soo.tistory.com/249</link>
      <description>&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ubuntu&lt;/li&gt;
&lt;li&gt;amazon linux2&lt;/li&gt;
&lt;li&gt;cent os7&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;ubuntu 도커 설치&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버전 Ubuntu 18.04&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 확인 방법&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;lsb_release -a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;No LSB modules are available.
Distributor ID:    Ubuntu
Description:    Ubuntu 18.04.5 LTS
Release:    18.04
Codename:    bionic&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;Ubuntu 22.04&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패키지 업데이트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;sudo apt-get update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패키지 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker의 공식 GPG키를 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker의 공식 apt 저장소를 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;sudo add-apt-repository &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패키지 업데이트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;sudo apt-get update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;sudo apt-get install docker-ce docker-ce-cli containerd.io&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도커 실행상태 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl status docker&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker-compose 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo apt-get install docker-compose&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Permission 에러 발생할 시 &lt;code&gt;sudo usermod -aG docker $USER&lt;/code&gt; 하고, 터미널을 나갔다가 들어와야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Amazon linux2 도커 설치&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 업데이트&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo yum update -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;yum으로 Docker 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;sudo yum install docker -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker-compose 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;sudo yum install docker-compose&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 데몬 소켓 파일 권한 변경&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sudo chmod 666 /var/run/docker.sock&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;or Docker 그룹에 인스턴스 접속 후 도커 바로 제어할 수 있도록 sudo 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sudo usermod -aG docker ec2-user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker 서비스 시작&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo service docker start&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;도커 실행상태 확인&lt;/h4&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl status docker&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Centos7 도커 설치&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패키지 업데이트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo yum update -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요한 패키지 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;sudo yum install -y yum-utils device-mapper-persistent-data lvm2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker 저장소 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;sudo yum install docker-ce docker-ce-cli containerd.io&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;docker-compose 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;sudo yum install docker-compose&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;or&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;yum update -y

yum install -y yum-utils

yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Docker</category>
      <author>ysk(0soo)</author>
      <guid isPermaLink="true">https://0soo.tistory.com/249</guid>
      <comments>https://0soo.tistory.com/249#entry249comment</comments>
      <pubDate>Fri, 21 Jul 2023 01:08:11 +0900</pubDate>
    </item>
  </channel>
</rss>