<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>MYVELOP 마이벨롭</title>
    <link>https://myvelop.tistory.com/</link>
    <description>좌충우돌 개발기</description>
    <language>ko</language>
    <pubDate>Tue, 12 May 2026 10:44:07 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>gakko</managingEditor>
    <image>
      <title>MYVELOP 마이벨롭</title>
      <url>https://tistory1.daumcdn.net/tistory/5136617/attach/ee9fa9e51dc84012b2b2ca2ecfb4840b</url>
      <link>https://myvelop.tistory.com</link>
    </image>
    <item>
      <title>Kubernetes가 내 Pod를 죽이는 이유: Health Probe</title>
      <link>https://myvelop.tistory.com/269</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rxjgV/dJMcadVyt0m/1yXolthPUvVKHj9l7mWyB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rxjgV/dJMcadVyt0m/1yXolthPUvVKHj9l7mWyB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rxjgV/dJMcadVyt0m/1yXolthPUvVKHj9l7mWyB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrxjgV%2FdJMcadVyt0m%2F1yXolthPUvVKHj9l7mWyB1%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;500&quot; height=&quot;281&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;281&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;h2 data-ke-size=&quot;size26&quot;&gt;Liveness와 Readiness Probe&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 Pod의 상태를 판단하기 위해 두 종류의 probe를 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LivenessProbe&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;&quot;이 컨테이너가 살아있는가&quot;를 확인한다. &lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet이 주기적으로 지정된 엔드포인트를 호출하고, 응답이 없거나 실패하면 컨테이너를 재시작한다. 앱이 데드락에 빠지거나, 메모리 릭으로 응답 불능 상태가 됐을 때 자동으로 복구시키는 역할이다. 단, liveness가 실패하면 컨테이너를 아예 죽이고 다시 만든다는 점을 기억해야 한다. &quot;느리다&quot;와 &quot;죽었다&quot;는 다른 문제인데, liveness는 이 둘을 구분하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ReadinessProbe&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;&quot;이 컨테이너가 트래픽을 받을 준비가 됐는가&quot;를 확인한다. &lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패하면 컨테이너를 재시작하지 않고, Service의 endpoints 목록에서 해당 Pod를 빼서 트래픽 라우팅을 중단한다. DB 연결이 일시적으로 끊어졌거나, 앱이 기동 중이거나, 일시적 과부하 상태일 때 트래픽만 차단하고 Pod 자체는 살려두는 것이다. 상태가 회복되면 다시 endpoints에 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Rolling Update를 진행할 때&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;health check가 없다면 어떻게 될까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LivenessProbe가 설정되지 않으면&lt;/b&gt; Kubernetes는 해당 probe의 결과를 항상 성공으로 간주한다. 즉, kubelet이 컨테이너의 health check를 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 컨테이너가 재시작되는 건 오직 &lt;b&gt;프로세스 자체가 종료(exit)됐을 때&lt;/b&gt;뿐이다. restartPolicy에 따라 프로세스가 크래시하면 재시작하지만, 프로세스가 죽지 않고 응답 불능인 상태(데드락, 무한 루프, 메모리 릭으로 인한 가비지 컬렉션 문제 발생)는 감지하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ReadinessProbe가 없으면&lt;/b&gt; Kubernetes는 컨테이너가 Running 상태가 되는 즉시 해당 Pod를 &quot;Ready&quot;로 간주한다 Rolling Update를 하면 &lt;u&gt;&lt;b&gt;새 Pod의 컨테이너 프로세스가 뜨기만 하면 바로 트래픽이 넘어오고&lt;/b&gt;&lt;/u&gt;, 이전 Pod는 종료되기 시작한다. &lt;u&gt;&lt;b&gt;마치 Recreate처럼 동작하게 된다.&lt;/b&gt;&lt;/u&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;LivenessProbe가 없으면 비정상 컨테이너가 재시작 없이 계속 떠 있고, ReadinessProbe가 없으면 준비되지 않은 Pod에 트래픽이 라우팅된다. 둘 다 없으면 Kubernetes의 자가 복구 메커니즘이 사실상 작동하지 않으므로, 2개에 대한 설정은 필수라고 볼 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애플리케이션과 k8s 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k8s 노드의 제어자인 kubelet은 애플리케이션 API를 호출하여 health check를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 애플리케이션에서 규격화된 health check API를 만들어야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 Actuator가 /actuator/health 엔드포인트에 /liveness와 /readiness를 기본 제공하고, DB와 Redis health indicator도 내장되어 있어서 별도 구현 없이 설정만으로 동작한다.&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;build.gradle&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773757416866&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
}&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;application.yaml&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773757846113&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management:
  endpoint:
    health:
      show-details: always
      group:
        liveness:
          include: livenessState
        readiness:
          include: readinessState, db, redis
  # default
  endpoints:
    web:
      exposure:
        include: health&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 링크로 호출하면 liveness와 readiness를 확인할 수 있다.&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;http://localhost:8080/actuator/health/liveness&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773757534816&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;status&quot;: &quot;UP&quot;,
  &quot;components&quot;: {
    &quot;livenessstate&quot;: {
      &quot;status&quot;: &quot;UP&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;b&gt;http://localhost:8080/actuator/health/readiness&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773757605802&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;status&quot;: &quot;UP&quot;,
  &quot;components&quot;: {
    &quot;readinessstate&quot;: {
      &quot;status&quot;: &quot;UP&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NestJS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 NestJS에서는 @nestjs/terminus를 사용해 API를 직접 구현해줘야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773758252010&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller('health')
export class HealthController {
  constructor(
    private readonly health: HealthCheckService,
    private readonly db: TypeOrmHealthIndicator,
    private readonly redis: RedisHealthIndicator,
  ) {}

  @Get('liveness')
  @HealthCheck()
  liveness() {
    return this.health.check([]);
  }

  @Get('readiness')
  @HealthCheck()
  readiness() {
    return this.health.check([
      this.db.pingCheck('database', { timeout: 3000 }),
      this.redis.isHealthy('redis'),
    ]);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 경우 기본 제공하지 않아 아래와 같이 직접 만들거나 서드파티 라이브러리를 사용해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1773758088424&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Injectable()
export class RedisHealthIndicator {
  constructor(
    private readonly healthIndicatorService: HealthIndicatorService,
    @InjectRedis()
    private readonly redis: Redis,
  ) {}

  async isHealthy(key: string) {
    const indicator = this.healthIndicatorService.check(key);

    try {
      const res = await this.redis.ping();
      if (res === 'PONG') {
        return indicator.up();
      }
    } catch (error) {
      // ping 실패 시 아래에서 down 반환
      // 필요에 따라 로깅을 넣어도 된다.
    }

    return indicator.down();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;k8s Probe&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;k8s 공식 문서에서는 health check를 할 때 /healthz 네이밍을 사용한다. 참고만 하자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k8s 설정은 그리 어렵지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 &lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;를 참고하길 바란다.&lt;/p&gt;
&lt;pre id=&quot;code_1773757987900&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-api
  template:
    metadata:
      labels:
        app: my-api
    spec:
      containers:
        - name: my-api
          image: my-api:latest
          ports:
            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 1
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 애플리케이션 배포시간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prod에서 배포 직후 Pod가 재시작되는 현상이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 확인해보면 애플리케이션의 부트스트랩 과정에서 DB 마이그레이션 체크, Redis 연결, 외부 서비스 초기화 등이 initialDelaySeconds 안에 끝나지 않는 경우를 발견하게 될 것이다! kubelet은 앱이 아직 뜨고 있음에도 불구하고 liveness probe로 체크해버리고 응답이 없으면 &quot;죽었다&quot;고 판단해서 컨테이너를 재시작시킬 것이다.&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;그렇다고 initialDelaySeconds를 넉넉하게 잡자니 다른 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 금방 기동을 끝내도 Kubernetes는 설정된 시간이 지날 때까지 liveness/readiness 체크를 시작하지 않는다. 새 Pod가 이미 준비됐는데도 트래픽 전환이 늦어진다.&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;이때 사용할 수 있는 것이 StartupProbe다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StartupProbe는 Liveness와 거의 비슷하지만, 최초에 애플리케이션이 실행됐는지 확인하는 용도로만 사용된다는 점에서 다르다. 이를 설정하면 StartupProbe가 성공하기 전까지 Liveness와 Readiness probe는 아예 실행되지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;0초:&amp;nbsp; &amp;nbsp; 컨테이너 시작, startupProbe 체크 시작 &lt;br /&gt;10초:&amp;nbsp; startupProbe 실패, 기다림 &lt;br /&gt;20초:&amp;nbsp; startupProbe 실패, 기다림&lt;br /&gt;30초:&amp;nbsp; startupProbe 실패, 기다림 &lt;br /&gt;40초:&amp;nbsp; startupProbe 성공! &amp;rarr; 이때부터 liveness/readiness 시작&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정은 아래와 Liveness나 Readiness와 비슷하다.&lt;/p&gt;
&lt;pre id=&quot;code_1773759290879&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: hiddenmoney-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hiddenmoney-api
  template:
    metadata:
      labels:
        app: hiddenmoney-api
    spec:
      containers:
        - name: hiddenmoney-api
          image: hiddenmoney-api:latest
          ports:
            - containerPort: 4000
          startupProbe:
            httpGet:
              path: /health/liveness
              port: 4000
            periodSeconds: 10     # 10초마다
            timeoutSeconds: 3
            failureThreshold: 30  # 30번의 시도 (10초 * 30번 = 총 300초)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 로그 노이즈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;health check probe는 liveness + readiness 합산 10초마다 2건씩 호출된다.&amp;nbsp; 하루로 치면 애플리케이션 하나에 약 17,280건의 액세스 로그가 찍힌다. 장애 상황에서 로그를 검색할 때 GET /health/liveness 200이 끝없이 반복되면서 실제 의미 있는 로그를 찾기 어렵다.&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;만약 Fluent Bit, Logstash, OpenTelemetry Collector 등의 로그 필터링 기능을 활용할 수 있는 인프라가 존재한다면 아래와 같이 필터링을 넣어줘 &lt;u&gt;&lt;b&gt;health check 관련 로그가 로깅 시스템에 찍히지 않게 방지하자.&lt;/b&gt;&lt;/u&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 id=&quot;code_1773759958657&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{                                                                  
    &quot;timestamp&quot;: &quot;2026-03-18T09:00:02.456Z&quot;,
    &quot;level&quot;: &quot;info&quot;,                                                                      
    &quot;method&quot;: &quot;GET&quot;,
    &quot;path&quot;: &quot;/health/readiness&quot;,                                                          
    &quot;status&quot;: 200,                                                   
    &quot;response_time_ms&quot;: 5                                                                 
}&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;fluent-bit.conf&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1773759535438&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[FILTER]
    Name    grep
    Match   *
    Exclude path /health/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra/k8s</category>
      <category>Health Check</category>
      <category>k8s</category>
      <category>kubernetes</category>
      <category>terminus</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/269</guid>
      <comments>https://myvelop.tistory.com/269#entry269comment</comments>
      <pubDate>Wed, 18 Mar 2026 00:08:15 +0900</pubDate>
    </item>
    <item>
      <title>2025년 회고</title>
      <link>https://myvelop.tistory.com/268</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발자로서 성장&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;임팩트&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;기술만 좋으면 된다고 믿었다.&lt;br&gt;클린 코드, 리팩토링, TDD, 도메인 주도 설계.&lt;br&gt;좋은 코드를 짜는 것이 개발자의 최고 덕목이라 생각하며 끊임없이 수련했다.&lt;br&gt;그런데 AI 시대가 오면서 그 전제가 흔들렸다.&lt;br&gt;코드를 잘 작성하는 것만으로는 대체 불가능한 가치를 만들 수 없다는 걸 체감했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면 개발자가 진짜 집중해야 할 것은 무엇인가.&lt;br&gt;나는 &lt;b&gt;도메인에 깊이 들어가는 것&lt;/b&gt;이라고 결론 내렸다.&lt;br&gt;어떤 문제를 먼저 풀어야 하는지 판단하고, 가장 빠르게 해결하는 방법을 설계하는 능력이 가장 중요하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;토스 러너스 하이를 기점으로 이 관점을 의식적으로 연습했다.&lt;br&gt;어떻게 하면 임팩트를 키울 수 있을지, 무엇을 먼저 풀어야 할지 계속 고민했다.&lt;br&gt;그 과정에서 내가 먼저 바꾼 건 “바로 구현에 들어가는 습관”이었다.&lt;br&gt;요구사항을 받으면 곧장 설계부터 하던 내가 이제는&lt;br&gt;&quot;&lt;b&gt;누구의 어떤 불편을 줄이려는지&quot;&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &quot;&lt;/span&gt;&lt;b&gt;성공을 무엇으로 측정할지&quot;&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; &quot;&lt;/span&gt;&lt;b&gt;지금 풀어야 하는 이유가 뭔지&quot;&lt;/b&gt;&amp;nbsp;&lt;br&gt;질문부터 던지게 됐다.&lt;br&gt;&amp;nbsp;&lt;br&gt;가능한 한 빨리 ‘정답’이 아니라 &lt;b&gt;검증&lt;/b&gt;에 도달하려고 한다.&lt;br&gt;처음부터 완벽한 구조를 만드는 대신, 작은 범위로 가설을 시험하고 결과를 보고 다음 결정을 내린다.&lt;br&gt;이 과정에서 “더 많은 코드”를 쓰기보다, &lt;b&gt;덜 만들고 더 빨리 배우는 선택&lt;/b&gt;이 임팩트를 키우는 경우가 많았다.&lt;br&gt;솔직히 아직 확신은 없다.&lt;br&gt;다만 요구사항을 파악하고 사람들과 대화하며 설계하는 데 더 많은 시간을 쓰고 있다.&lt;br&gt;코드는 AI가 대부분 작성하고, 나는 리뷰어로서 방향과 품질을 지키는 역할에 가까워졌다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;공유&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;링크드인에 생각을 글로 올렸는데, 예상보다 큰 반응을 받았다.&lt;br&gt;감사한 마음과 동시에, &lt;b&gt;내가 쓰는 한 문장이 다른 개발자의 선택과 시간에 영향을 줄 수 있다는 책임감&lt;/b&gt;을 더 크게 느꼈다.&lt;br&gt;&amp;nbsp;&lt;br&gt;“그렇다더라”가 아니라 &lt;b&gt;근거와 맥락이 남는 글&lt;/b&gt;을 꾸준히 쓰려고 한다.&lt;br&gt;독자와&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;함께&lt;/span&gt; 고민할 수 있는 글을 쓰고 싶다.&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;더 나은 선택을 돕고,&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;좋은 영향을 주는 개발자&lt;/b&gt;가 되고 싶다.&lt;/p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;1424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAmUYR/dJMcabpDGv5/3odNdRBMHkDRrNPH3uHBRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAmUYR/dJMcabpDGv5/3odNdRBMHkDRrNPH3uHBRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAmUYR/dJMcabpDGv5/3odNdRBMHkDRrNPH3uHBRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAmUYR%2FdJMcabpDGv5%2F3odNdRBMHkDRrNPH3uHBRk%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;1100&quot; height=&quot;1424&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;1424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;넥스터즈&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;넥스터즈에서 두 번의 프로젝트를 경험했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;첫 번째는 PM으로 참여했다.&lt;br&gt;처음에는 정말 &lt;b&gt;“될 때까지 밀어붙이는”&lt;/b&gt; 방식으로 했다.&lt;br&gt;매일 새벽 2~3시까지 개발하고, 일정에 쫓기면서 어떻게든 결과물을 만들어내는 데 집중했다.&lt;br&gt;그때는 그게 성실함이고 책임감이라고 생각했다.&lt;br&gt;그렇게 몇 주를 태우고 나니 3월쯤 번아웃이 왔다.&lt;br&gt;그리고 솔직히 말하면, 그렇게 고생한 것에 비해 결과도 만족스럽지 못했다.&lt;br&gt;“이 정도로 갈아 넣었는데 이 정도라면, 내가 뭔가 잘못하고 있는 거 아닌가?”라는 생각이 들었다.&lt;br&gt;노력의 양이 임팩트를 보장하지 않는다는 걸 몸으로 배운 시간이었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그 뒤 여름에는 방향을 바꿨다.&lt;br&gt;이번에는 ‘잘 만들어야 한다’는 압박보다 &lt;b&gt;제품을 만드는 즐거움&lt;/b&gt;을 더 크게 두기로 했다.&lt;br&gt;무리해서 시간을 늘리기보다, 여유를 확보하고 중요한 것부터 차근차근 만들었다.&lt;br&gt;완성도도 중요했지만, 무엇보다 &lt;b&gt;만드는 과정 자체를 즐기는 상태&lt;/b&gt;를 유지하려고 했다.&lt;br&gt;아이러니하게도 그때 결과가 더 좋았다.&lt;br&gt;즐기면서 하다 보니 팀의 호흡도 좋아졌고, 판단도 더 명확해졌고, 제품의 디테일도 더 살아났다.&lt;br&gt;결국 대상을 받았다.&lt;/p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UGYMO/dJMcadnn7w0/RAU0KKfSCOQb0n3A59TyCk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UGYMO/dJMcadnn7w0/RAU0KKfSCOQb0n3A59TyCk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UGYMO/dJMcadnn7w0/RAU0KKfSCOQb0n3A59TyCk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUGYMO%2FdJMcadnn7w0%2FRAU0KKfSCOQb0n3A59TyCk%2Fimg.jpg&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; alt=&quot;넥스터즈 27기 대상 수상&quot; loading=&quot;lazy&quot; width=&quot;1382&quot; height=&quot;1258&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이 경험들을 통해 배운 건 단순했다.&lt;br&gt;임팩트는 얼마나 열심히 했느냐가 아니라, 어떻게 지속 가능한 방식으로 만들어 갔느냐에서 나왔다.&lt;br&gt;앞으로도 무작정 갈아 넣는 대신, &lt;b&gt;즐길 수 있는 속도로 꾸준히&lt;/b&gt; 만들어가려 한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;첫 이직&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;토스 면접 기회를 얻었던 적이 있다.&lt;br&gt;하지만 넥스터즈 활동 이후 번아웃이 크게 왔고, 면접을 제대로 준비하지 못한 채 탈락했다.&lt;br&gt;애초에 준비가 안 되어 있었다.&lt;br&gt;내가 왜 그 기술을 선택했는지조차 설명하지 못했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;탈락하고 나서 생각보다 오래 멍했다.&lt;br&gt;“조금만 더 준비했으면” 하는 아쉬움이 남았다.&lt;br&gt;그때 결심했다. 다시는 준비 부족으로 기회를 흘려보내지 말자.&lt;br&gt;조급하게 다음을 보지 말고, 최소 3년은 내 일을 제대로 쌓고 정리한 다음 움직이자.&lt;br&gt;그래서 이력서와 포트폴리오를 손보고,&lt;br&gt;스터디에 들어가 꾸준히 피드백을 받고,&lt;br&gt;매주 블로그와 링크드인에 기록을 남기며 루틴을 만들기 시작했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그러던 중 부스트캠프 동기에게 연락이 왔다.&lt;br&gt;“우리 회사 와볼 생각 있냐?”&lt;br&gt;처음엔 망설였다. 지금까지 해온 것과 기술 스택이 완전히 달랐기 때문이다.&lt;br&gt;그런데 이야기를 나눌수록, 다들 일에 몰입하는 분위기와 문제를 대하는 태도가 마음에 들어 결국 도전해보기로 했다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금은 TypeScript와 NestJS라는 새로운 스택 위에서 일하고 있다.&lt;br&gt;코드베이스도 전통적인 객체지향 스타일이라기보다는 &lt;b&gt;함수형 기반에 가까운 방식&lt;/b&gt;이 많다.&lt;br&gt;익숙하지 않은 만큼 『이펙티브 타입스크립트』를 다시 읽고,&lt;br&gt;도메인 주도 개발 관점에서 함수형 프로그래밍을 다루는 책과 자료들을 찾아보며 차근차근 따라가는 중이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;기술적으로 새로 배우는 것도 많다.&lt;br&gt;Graphite, Linear 같은 협업 도구부터 Pulumi, Fluent Bit, Lambda까지, 손대볼 만한 요소들이 정말 다양하다.&lt;br&gt;&amp;nbsp;&lt;br&gt;무엇보다 좋은 건 사람이다.&lt;br&gt;열심히 일하는 사람이 많고, 배울 수 있는 사람이 많다. 다들 개발에 진심이다.&lt;br&gt;회사에 남아서 몰입하는 사람도 있고,&lt;br&gt;Tailscale로 망을 구성해 휴대폰으로 로컬 컴퓨터에 원격 접속한 뒤 Claude를 활용해 생산성을 끌어올리는 등&lt;br&gt;각자만의 방식으로 더 잘 일하는 방법을 실험한다.&lt;br&gt;주말에도 이것저것 만들어보며 시간을 보내는 모습을 보면서 자극을 많이 받고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이제 “다음 기회”를 기다리기보다, &lt;b&gt;매일 준비된 상태에 가까워지자.&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;여행&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;성인이 되고 나서 첫 해외여행을 다녀왔다.&lt;br&gt;&amp;nbsp;&lt;br&gt;원래 갈 생각이 없었다. 엄청 지쳐 있었고, 그냥 아무것도 안 하고 싶었다.&lt;br&gt;그런데 회사를 추천해준 친구가 링크를 하나 던져왔다.&lt;br&gt;&quot;쉬는데 아무것도 안 하면 시간이 너무 아깝잖아.&quot;&lt;br&gt;마침 이직하는 회사에서 입사가 일주일 미뤄져 시간이 비었다. 그래서 그냥 떠났다.&lt;br&gt;출발 전까지는 기대가 없었는데, 도착하니 모든 게 새로웠다.&lt;br&gt;예전엔 여행을 돈과 시간 낭비라고 생각했다.&lt;br&gt;그 시간에 책 한 장 더 읽고, 뭔가 하나라도 더 하는 게 더 낫다고 믿었다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그런데 꼭 그렇진 않았다.&lt;br&gt;잘 쉬고 돌아오니 다시 달릴 힘이 생겼다.&lt;br&gt;룸메이트였던 변리사 형은 37년 만에 처음 해외에 나온 거라고 했다.&lt;br&gt;&lt;b&gt;&quot;이제야 온 게 후회된다. 한 살이라도 어릴 때 이것저것 경험해볼 걸.&quot; &lt;/b&gt;&lt;br&gt;그 말이 꽤 오래 남았다.&lt;br&gt;가끔 공백이 생기면, 이렇게 한번 다녀오는 것도 꽤 괜찮겠다.&lt;/p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUwYKy/dJMcagRZ37t/CET7sebxSduWGMndELkvr0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUwYKy/dJMcagRZ37t/CET7sebxSduWGMndELkvr0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUwYKy/dJMcagRZ37t/CET7sebxSduWGMndELkvr0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUwYKy%2FdJMcagRZ37t%2FCET7sebxSduWGMndELkvr0%2Fimg.jpg&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;1080&quot; height=&quot;1440&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;독서&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 다양한 분야의 책을 여러 권 읽었다.&lt;br&gt;아무래도 작년보다 더 많은 책을 읽을 수 있었던 이유는 강의보는 시간이 줄었고, 그만큼 책에 투자할 수 있게 되었기 때문일 것이다. 평일 저녁에는 회사에 남아 개발 관련 서적을 읽었고, 주말에는 집 앞에 있는 스타벅스에 가서 책을 봤다.&lt;br&gt;&amp;nbsp;&lt;br&gt;책을 읽는 방식도 약간 바뀌었다.&lt;br&gt;예전엔 이해가 되지 않았을 때 그냥 넘어가버렸다면 이제는 이해가 될 때까지 다시 읽어보고, 스스로 고민해보며, 정 안되면 Gemini나 GPT에게 물어봐 이해했다. 여기에 더해, 그 책에 대한 내 생각을 마크다운 파일로 정리했다. 확실히, 사고력이 좋아진 걸 느꼈다. &lt;s&gt;여전히 말은 잘 못하지만..&lt;/s&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;클린 아키텍처, 로버트 C 마틴&lt;/li&gt;&lt;li&gt;실용주의 프로그래머, 앤드류 헌트, 데이비드 토머스&lt;/li&gt;&lt;li&gt;대규모 시스템 설계 기초1 , 알렉스 쉬&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;[재독]&lt;/b&gt;&lt;/li&gt;&lt;li&gt;대규모 시스템 설계 기초2, 알렉스 싀, 산 람&lt;/li&gt;&lt;li&gt;Real MySQL1&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;[재독]&lt;/b&gt;&lt;/li&gt;&lt;li&gt;토비의 스프링1, 이일민&lt;/li&gt;&lt;li&gt;개발자를 위한 레디스, 김가람&lt;/li&gt;&lt;li&gt;실전 레디스, 하야시 쇼고&lt;/li&gt;&lt;li&gt;엘라스틱 스택 개발부터 운영까지, 김준영 &amp;amp; 정상운 지음&lt;/li&gt;&lt;li&gt;DDD 시작하기, 최범균 지음&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;[재독]&lt;/b&gt;&lt;/li&gt;&lt;li&gt;데이터베이스 인터널스, 알렉스 페트로프&lt;/li&gt;&lt;li&gt;이펙티브 타입스크립트&lt;/li&gt;&lt;li&gt;코틀린 인 액션, 세바스티안 아이그너&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;[...읽는 중...]&lt;/b&gt;&lt;/li&gt;&lt;li&gt;이펙티브 코틀린, 마르친 모스칼라&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;[...읽는 중...]&lt;/b&gt;&lt;/li&gt;&lt;li&gt;단위테스트, 블라디미르 코리코프&lt;/li&gt;&lt;li&gt;운영체제 10판 (공룡책)&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;[...읽는 중...]&lt;/b&gt;&lt;/li&gt;&lt;li&gt;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식, 최범균&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 분석&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;고객을 끌어오는 구글 애널리틱스4, 문준영 지음&lt;/li&gt;&lt;li&gt;데이터 과학자의 가설 사고&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;디자인&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;UX 리서치 교과서&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;과학&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;이기적 유전자, 리처드 도킨스&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;철학&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;의무론, 키케로&lt;/li&gt;&lt;li&gt;인생철학이야기, 세네카 지음&lt;/li&gt;&lt;li&gt;순수이성비판, 임마누엘 칸트 1편 =&amp;gt; 정명오 번역본 &amp;amp; 코디정 번역본&lt;/li&gt;&lt;li&gt;자유론, 존 스튜어트 밀&lt;/li&gt;&lt;li&gt;이성의 기능, 알프레드 노스 화이트헤드 &lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;[...읽는 중...]&lt;/b&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;경제/비즈니스&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;일의 감각, 조수용&lt;/li&gt;&lt;li&gt;변화하는 세계질서, 레이 달리오&lt;/li&gt;&lt;li&gt;The Lean Startup, 에릭 리스&lt;/li&gt;&lt;li&gt;작은 브랜드를 위한 판매전략 지침서, 스몰브랜더&lt;/li&gt;&lt;li&gt;본질의 발견, 최장순&lt;/li&gt;&lt;li&gt;컨테이저스, 조나 버거&lt;/li&gt;&lt;li&gt;유난한 도전, 정경화&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;문학&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;페스트, 알베르 카뮈&lt;/li&gt;&lt;li&gt;에세2, 미셀 드 몽테뉴&lt;/li&gt;&lt;li&gt;무의미의 축제, 밀란 쿤데라&lt;/li&gt;&lt;li&gt;위대한 개츠비, F. 스콧 피츠제럴드&lt;/li&gt;&lt;li&gt;왜 나는 너를 사랑하는가, 알랭 드 보통&lt;/li&gt;&lt;li&gt;오뒷세이아, 호메로스&lt;/li&gt;&lt;li&gt;너와 세상 사이의 싸움에서, 프란츠 카프카&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;영문책&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;Sapiens, Yuval Noah Harari &lt;b&gt;[...읽는 중...]&lt;/b&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;기타&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;린치핀, 세스 고딘&lt;/li&gt;&lt;li&gt;팩트풀니스, 한스 로슬링&lt;/li&gt;&lt;li&gt;원칙, 레이 달리오&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;체력 관리&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;운동&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;운동은 꾸준히 하고 있다. 체력과 집중력의 원천이다.&lt;br&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;수면 시간이 충분하다고 생각하는데, 피로감이 그에 비해 심하다.&lt;br&gt;예전에 군대에서 코골이가 엄청 심하지는 않는데 갑자기 코골이를 멈춘다거나, 컥컥대는걸 본 적이 있다고 들었다. &lt;span style=&quot;color: #333333;&quot;&gt;수면 다원 검사의 필요성을 느끼고 있다.&lt;/span&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;술자리를 많이 줄였다. 요즘 술을 마시면 더 빠르게 취하고, 피로감이 예전보다 더 심하다는 걸 느낀다. 점점 술을 멀리하고 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&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;/li&gt;&lt;li&gt;오픈소스 기여 혹은 만들어보기&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>끄적끄적</category>
      <category>2025년</category>
      <category>개발자</category>
      <category>목표</category>
      <category>회고</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/268</guid>
      <comments>https://myvelop.tistory.com/268#entry268comment</comments>
      <pubDate>Tue, 17 Feb 2026 14:20:34 +0900</pubDate>
    </item>
    <item>
      <title>CDN 비용 줄이기 - 브라우저 캐시</title>
      <link>https://myvelop.tistory.com/267</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN을 도입했는데 매달 나가는 비용이 부담스럽다고 생각한 적이 있는가? 우리 팀은 브라우저 캐시 설정만으로 연간 100만원 이상의 CDN 비용을 절감했다. 단순히 Cache-Control 헤더를 적절히 설정하는 것만으로도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 CDN의 기본 원리부터 시작해서, Cache-Control 헤더에 대한 설명과 브라우저 캐시를 활용해 어떻게 비용을 절감할 수 있는지 실제 경험을 바탕으로 공유하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CDN&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CDN의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국에 있는 사용자가 미국 서버에 있는 이미지를 요청한다고 생각해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckTcO4/dJMcafLMqxH/J7Y8GvZxh5PYLPrnz1dnTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckTcO4/dJMcafLMqxH/J7Y8GvZxh5PYLPrnz1dnTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckTcO4/dJMcafLMqxH/J7Y8GvZxh5PYLPrnz1dnTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckTcO4%2FdJMcafLMqxH%2FJ7Y8GvZxh5PYLPrnz1dnTK%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; alt=&quot;한국과 미국 사이의 거리&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;600&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 컴퓨터 &amp;rarr; 공유기 &amp;rarr; 한국 통신사(ISP) &amp;rarr; 해저 케이블 게이트웨이 &amp;rarr; 태평양 해저 케이블 &amp;rarr; 미국 통신사 &amp;rarr; 데이터 센터 라우터 &amp;rarr; 최종 서버 등 수십 개의 장비(Router/Switch)를 거쳐야 하므로 오랜 시간이 걸릴 수밖에 없다.&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;이때, CDN을 사용하면 이 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CDN이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN(Content Delivery Network)은 전 세계에 분산된 캐시 서버 네트워크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 서버(Origin)의 콘텐츠를 각 지역의 엣지 서버에 복사해두고, 사용자와 가장 가까운 서버에서 콘텐츠를 전송한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjNRR6/dJMcai2Lf3j/GYnYwe16QJhB4ZWtgt7igK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjNRR6/dJMcai2Lf3j/GYnYwe16QJhB4ZWtgt7igK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjNRR6/dJMcai2Lf3j/GYnYwe16QJhB4ZWtgt7igK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjNRR6%2FdJMcai2Lf3j%2FGYnYwe16QJhB4ZWtgt7igK%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; alt=&quot;CDN의 원리&quot; loading=&quot;lazy&quot; width=&quot;941&quot; height=&quot;832&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 물리적 거리가 줄어들어 속도가 빨라지고, 원본 서버의 부하도 줄어든다. 특히 이미지, 동영상, CSS, JavaScript 같은 정적 파일 제공에 사용하기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CDN의 비용 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN 서비스의 비용은 보통 요청량에 따라 결정된다. 아래는 NCloud Global Edge 서비스의 요금 정보이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNFeOH/dJMcaiocq6I/rWuPgB1ayGPrREurMapKH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNFeOH/dJMcaiocq6I/rWuPgB1ayGPrREurMapKH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNFeOH/dJMcaiocq6I/rWuPgB1ayGPrREurMapKH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNFeOH%2FdJMcaiocq6I%2FrWuPgB1ayGPrREurMapKH1%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; alt=&quot;NCloud Global Edge 요금 정보&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;741&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보는 것처럼 GB당 금액이 청구된다. 여기서 이미지 파일을 기준으로 금액을 계산해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 1장 크기: 평균 1MB&lt;/li&gt;
&lt;li&gt;홈페이지에서 사용되는 이미지 개수: 50개&lt;/li&gt;
&lt;li&gt;일일 페이지뷰: 100,000회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 캐시 없이 매번 CDN에서 홈페이지의 정적 파일을 서빙하면 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일일 전송량: 1MB &amp;times; 50 &amp;times; 100,000 = 5TB&lt;/li&gt;
&lt;li&gt;월간 전송량: 5TB &amp;times; 30 = 150TB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;월 예상 비용은 약 900만원&lt;/b&gt;이나 된다. 심지어 유저가 홈페이지를 나갔다가 다시 들어오거나, 다른 페이지에 살펴 보는 등의 행동을 하면 더 많은 비용이 청구될 것이다. 특히 이미지가 많은 커머스나 미디어 서비스는 더욱 부담이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브라우저 캐시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 어떻게 해결하면 될까? 한 번 받은 파일은 브라우저에 저장해두고 재사용하게 만들면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 한 번 받은 정적 파일을 CDN에 매번 요청하지 않게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache-Control 헤더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 캐시를 제어하는 핵심은 Cache-Control 헤더다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답값에 Cache-Control 헤더가 있으면 브라우저는 해당 값을 브라우저 캐시에 저장한다.&lt;/p&gt;
&lt;pre id=&quot;code_1763800862700&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP 200 OK
Content-Type: image/jpeg
Cache-Control: max-age=84600
Content-Length:32512

~~~~&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache-Control에서 설정할 수 있는 값들은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;max-age: &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;초 단위로&amp;nbsp;&lt;/span&gt;캐시 유효 시간 설정&lt;/li&gt;
&lt;li&gt;no-cache: &lt;b&gt;데이터는 캐시&lt;/b&gt;해도 되지만 &lt;b&gt;항상 Origin Server에 검증&lt;/b&gt; (캐시를 안 하는 것이 아니다! 주의!)&lt;/li&gt;
&lt;li&gt;no-store: 데이터에 민감한 정보가 포함되어 있어 저장 X&lt;/li&gt;
&lt;li&gt;public: public 캐시에 저장 가능 &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;(프록시 캐시 서버 개념)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;private: public 캐시에 저장 불가&lt;/li&gt;
&lt;li&gt;s-maxage: 프록시 캐시 서버에 적용되는 max-age&lt;/li&gt;
&lt;li&gt;Age: Origin Server 의 응답이 프록시 캐시 서버에 머문 시간 (초 단위)&lt;/li&gt;
&lt;li&gt;must-revalidate: 캐시 만료 후 조회시 Origin Server에 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloud 서비스를 사용한다면 해당 서비스에서 Cache-Control 헤더에 대한 설정을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;브라우저 캐시 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache-Control 헤더가 있는 경우 브라우저는 어떤 식으로 작동하는지 설명해보겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 최초 요청&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 서버로부터 응답을 받았을 때, Cache-Control가 있다면 해당 응답 자체를 캐싱한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mLgbx/dJMcaaqazpq/baKuzo3T9HuwYkmCkgw4tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mLgbx/dJMcaaqazpq/baKuzo3T9HuwYkmCkgw4tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mLgbx/dJMcaaqazpq/baKuzo3T9HuwYkmCkgw4tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmLgbx%2FdJMcaaqazpq%2FbaKuzo3T9HuwYkmCkgw4tK%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; alt=&quot;응답 결과 캐싱&quot; loading=&quot;lazy&quot; width=&quot;840&quot; height=&quot;537&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 재차 요청: 캐시가 유효한 경우&lt;/h4&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xv9OW/dJMcahbK6Mz/InbK6Tp9Px66SATaflcEcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xv9OW/dJMcahbK6Mz/InbK6Tp9Px66SATaflcEcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xv9OW/dJMcahbK6Mz/InbK6Tp9Px66SATaflcEcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxv9OW%2FdJMcahbK6Mz%2FInbK6Tp9Px66SATaflcEcK%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; alt=&quot;브라우저 캐시 유효 검증&quot; loading=&quot;lazy&quot; width=&quot;831&quot; height=&quot;539&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;539&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M6KIr/dJMcacIicNB/i8qgvSTajgox61kv8gkTb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M6KIr/dJMcacIicNB/i8qgvSTajgox61kv8gkTb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M6KIr/dJMcacIicNB/i8qgvSTajgox61kv8gkTb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM6KIr%2FdJMcacIicNB%2Fi8qgvSTajgox61kv8gkTb1%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; alt=&quot;캐시에서 조회&quot; loading=&quot;lazy&quot; width=&quot;829&quot; height=&quot;528&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 재차 요청: 캐시가 유효하지 않은 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 재차 요청을 했고 캐시를 검증했는데 시간이 초과하여 유효하지 않다면 어떻게 될까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ETC4z/dJMcagjCrGH/ZiDcKGR57m1ClMpwotX5h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ETC4z/dJMcagjCrGH/ZiDcKGR57m1ClMpwotX5h0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ETC4z/dJMcagjCrGH/ZiDcKGR57m1ClMpwotX5h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FETC4z%2FdJMcagjCrGH%2FZiDcKGR57m1ClMpwotX5h0%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; alt=&quot;캐시 유효성 검증 실패&quot; loading=&quot;lazy&quot; width=&quot;846&quot; height=&quot;538&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;538&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mLgbx/dJMcaaqazpq/baKuzo3T9HuwYkmCkgw4tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mLgbx/dJMcaaqazpq/baKuzo3T9HuwYkmCkgw4tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mLgbx/dJMcaaqazpq/baKuzo3T9HuwYkmCkgw4tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmLgbx%2FdJMcaaqazpq%2FbaKuzo3T9HuwYkmCkgw4tK%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; alt=&quot;서버로 재요청 및 캐시 재저장&quot; loading=&quot;lazy&quot; width=&quot;840&quot; height=&quot;537&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;537&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NCloud 설정해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Global Edge의 Management 설정으로 들어가 원하는 서비스의 룰빌더를 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 상세 룰 메뉴에서 Add cache rules 버튼을 클릭해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYXUvz/dJMcahbK6zP/Vtb919g63eKdnvoefghGA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYXUvz/dJMcahbK6zP/Vtb919g63eKdnvoefghGA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYXUvz/dJMcahbK6zP/Vtb919g63eKdnvoefghGA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYXUvz%2FdJMcahbK6zP%2FVtb919g63eKdnvoefghGA1%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; alt=&quot;ncloud 캐시 상세 룰&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;116&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 정적 리소스의 Directory, File Extension 등을 설정하고, 원본 Cache-Control 헤더 우선으로 설정해준다. 그리고 Advanced settings에서 브라우저 캐시를 허용해주자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcxck2/dJMcaiBGRs4/XKYPsBm6sBxhgGKI6qPark/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcxck2/dJMcaiBGRs4/XKYPsBm6sBxhgGKI6qPark/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcxck2/dJMcaiBGRs4/XKYPsBm6sBxhgGKI6qPark/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdcxck2%2FdJMcaiBGRs4%2FXKYPsBm6sBxhgGKI6qPark%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; alt=&quot;ncloud 캐시 룰 설정&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;824&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 정적 리소스를 호출해보자. Cache-Control 헤더가 붙은 걸 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bru6tk/dJMcag4ZDlj/zRHs3N90NAB0m3xszqVWo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bru6tk/dJMcag4ZDlj/zRHs3N90NAB0m3xszqVWo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bru6tk/dJMcag4ZDlj/zRHs3N90NAB0m3xszqVWo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbru6tk%2FdJMcag4ZDlj%2FzRHs3N90NAB0m3xszqVWo0%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; alt=&quot;응답값에 Cache-Control 헤더 확인&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;109&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;109&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDHHPF/dJMcabo4IMv/VTyKBIVXbIXVaYi40DEvQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDHHPF/dJMcabo4IMv/VTyKBIVXbIXVaYi40DEvQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDHHPF/dJMcabo4IMv/VTyKBIVXbIXVaYi40DEvQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDHHPF%2FdJMcabo4IMv%2FVTyKBIVXbIXVaYi40DEvQ1%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; alt=&quot;요청이 캐싱된 것을 확인&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;138&quot; data-origin-width=&quot;580&quot; data-origin-height=&quot;138&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;간단한 설정이 생각보다 큰 임팩트를 낼 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cloud 서비스에서 딸깍.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정만 하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;753&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lco2a/dJMcabijFZ9/xosdbnhCII3zqbjIr6x3u0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lco2a/dJMcabijFZ9/xosdbnhCII3zqbjIr6x3u0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lco2a/dJMcabijFZ9/xosdbnhCII3zqbjIr6x3u0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLco2a%2FdJMcabijFZ9%2FxosdbnhCII3zqbjIr6x3u0%2Fimg.webp&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; alt=&quot;딸깍&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;753&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;753&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다 보면 복잡한 솔루션에 매몰되기 쉽다. 예를 들어, 브라우저 캐시를 사용하기 위해 프론트엔드 코드 레벨의 fetch에 캐시 설정을 하거나, 여기서 좀 더 나아가 tanstack-query를 사용하는 것이다.&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;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;&amp;nbsp;&lt;/p&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://velog.io/@heka1024/CDN%EA%B3%BC-%EC%BA%90%EC%8B%9C-%EC%84%A4%EC%A0%95%EC%9C%BC%EB%A1%9C-%EC%A0%95%EC%A0%81%ED%8C%8C%EC%9D%BC%EC%9D%84-%EB%B9%A0%EB%A5%B4%EA%B2%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CDN과 캐시 설정으로 정적 파일을 빠르게&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/HTTP-%F0%9F%8C%90-%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%9D%98-%EC%BA%90%EC%8B%9C-%EC%A0%84%EB%9E%B5-Cache-Headers-%EB%8B%A4%EB%A3%A8%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;웹 브라우저의 캐시 전략 Cache Headers 다루기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.ncloud.com/product/contentDelivery/globalEdge#pricing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;NCloud Global Edge&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트</category>
      <category>Cache-Control</category>
      <category>CDN</category>
      <category>ncloud</category>
      <category>브라우저 캐시</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/267</guid>
      <comments>https://myvelop.tistory.com/267#entry267comment</comments>
      <pubDate>Sat, 22 Nov 2025 18:05:57 +0900</pubDate>
    </item>
    <item>
      <title>이론상 10만 QPS를 처리할 수 있는 티켓팅 시스템</title>
      <link>https://myvelop.tistory.com/266</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;티켓팅 시스템&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요구사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유명인의 내한으로 이벤트를 열게 되었다. 선착순 1,000명에게만 주어지는 기회다.&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;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;: 선착순 1,000장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제한&lt;/b&gt;: 1인 1티켓. 중복 불가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예상 트래픽&lt;/b&gt;: 5만 명 동시 접속&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인프라 제약&lt;/b&gt;: 애플리케이션 서버는 필요한 만큼 scale-out 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;목표 응답시간&lt;/b&gt;: 1초 이내&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 처리? 락?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락을 통해 락을 걸었다고 해보자. 혹시 완벽하게 동시성을 제어했다고 안심하고 있는가?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;티켓 오픈 시간, 수만 명이 몰려올 때 서버에서는 어떤 일이 벌어지게 될까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대규모 트래픽이 하나의 자원을 획득하기 위해 경쟁하는 상황이 벌어지면, 이는 치명적 병목이 될 수 있다. 아래는 실제로 5만 명이 동시 요청을 보냈을 때의 시나리오다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;739&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/If3Zz/dJMcaksGigz/X8kLgRZ25kkKrkETMbvIE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/If3Zz/dJMcaksGigz/X8kLgRZ25kkKrkETMbvIE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/If3Zz/dJMcaksGigz/X8kLgRZ25kkKrkETMbvIE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIf3Zz%2FdJMcaksGigz%2FX8kLgRZ25kkKrkETMbvIE1%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; alt=&quot;5만 명의 동시 요청 시나리오&quot; loading=&quot;lazy&quot; width=&quot;975&quot; height=&quot;739&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;739&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;락을 한 명씩 순차 처리하기 때문에 2500초, 약 42분의 시간이 소요될 것으로 예상된다. 이에 따라, 대부분의 사용자는 타임아웃(5초)으로 실패하게 될 것이다. 엄청나게 많은 요청이 애플리케이션에서 대기하게 될 것이며, 애플리케이션에서 장애가 발생할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락을 걸어서 느려진다면, 락을 걸지 않으면 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇가면 위 문제를 해결할 수 있을까?&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;락을 안 걸면 된다고? Redis가 그걸 가능하게 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;락 없이 어떻게 동시성을 제어하지?&quot;라고 생각할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 Redis의 독특한 아키텍처에 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis는 싱글 스레드다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 명령어는 &lt;b&gt;싱글 스레드 기반의&amp;nbsp;이벤트 루프&lt;/b&gt;에서 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lqX3S/dJMcake87Sk/2Fm2OJGmSAaPprbIy5cJqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lqX3S/dJMcake87Sk/2Fm2OJGmSAaPprbIy5cJqk/img.png&quot; data-alt=&quot;출처: Getting started with Redis&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lqX3S/dJMcake87Sk/2Fm2OJGmSAaPprbIy5cJqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlqX3S%2FdJMcake87Sk%2F2Fm2OJGmSAaPprbIy5cJqk%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; alt=&quot;레디스의 이벤트 루프&quot; loading=&quot;lazy&quot; width=&quot;991&quot; height=&quot;780&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: Getting started with Redis&lt;/figcaption&gt;
&lt;/figure&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;5만 명이 동시에 요청을 보내도, Redis는 이를 큐에 담아 한 번에 하나씩 순차적으로 처리한다. 이것이 바로 락 없이도 자연스러운 동시성 제어가 가능한 이유다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sO7tw/dJMcah3NjKX/51fMjH4HqWApNX6km0u7W1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sO7tw/dJMcah3NjKX/51fMjH4HqWApNX6km0u7W1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sO7tw/dJMcah3NjKX/51fMjH4HqWApNX6km0u7W1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsO7tw%2FdJMcah3NjKX%2F51fMjH4HqWApNX6km0u7W1%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; alt=&quot;레디스의 명령어 순차 처리 플로우&quot; loading=&quot;lazy&quot; width=&quot;1164&quot; height=&quot;776&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;776&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;h3 data-ke-size=&quot;size23&quot;&gt;Redis가 싱글 스레드인데도 빠른 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 명령어 처리는 싱글 스레드로 하지만, 나머지는 철저히 병렬 처리한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Multiplexing I/O&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Redis는 다수의 연결과 요청을 비동기적으로 처리할 수&lt;span&gt; 있는 기술을 활용한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/np4HV/dJMcaklUKsE/wU63UUd3QCuaXRGAr7jAr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/np4HV/dJMcaklUKsE/wU63UUd3QCuaXRGAr7jAr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/np4HV/dJMcaklUKsE/wU63UUd3QCuaXRGAr7jAr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnp4HV%2FdJMcaklUKsE%2FwU63UUd3QCuaXRGAr7jAr0%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; alt=&quot;Multiplexing I/O&quot; loading=&quot;lazy&quot; width=&quot;1123&quot; height=&quot;374&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;epoll()을 사용하여 소켓 연결의 상태 변화를 논블로킹 방식으로 감지한다. &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이벤트 기반으로 변화를 감지하며,&lt;/span&gt; 덕분에 다수의 클라이언트 연결(소켓)을 관리할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;연결마다 프로세스나 스레드가 할당되는 것이 아니라 컨텍스트 스위칭을 최소화하고 리소스 측면에서도 더 효율적이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 자식 프로세스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 여러 무거운 작업을 자식 프로세스에게 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;BGSAVE&lt;/b&gt;: RDB 스냅샷을 자식 프로세스에서 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BGREWRITEAOF&lt;/b&gt;: AOF 파일 압축/재작성도 자식 프로세스에서 진행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Copy-on-Write&lt;/b&gt;: fork() 직후 메모리 공유, 변경 시에만 복사&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;h4 data-ke-size=&quot;size20&quot;&gt;3. 백그라운드 스레드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O 스레드를 통해 네트워크 I/O를 멀티 스레드로 처리한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;멀티플렉싱 모듈에서 epoll()을 통해 이벤트를 발행하면 백그라운드 스레드가 클라이언트 요청을 메인 스레드로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메인 스레드는 명령어를 처리에만 집중&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;처리된 결과를 백그라운드 스레드가 받아 클라이언트에게 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, BIO 스레드를 통해 fsync나 unlink와 같은 느린 시스템 콜을 처리하여 메인 스레드의 부하를 줄여준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 섬세한 최적화&lt;/h4&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;Zero-Copy: 데이터를 전송할 때 커널 내부에서 직접 전송함으로써 데이터 복사를 최소화&lt;/li&gt;
&lt;li&gt;I/O 최적화: 연속된 메모리 블록에 데이터를 배치하여 Random Access를 최소화하고 Sequential Access를 최대화&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;깨알 상식&lt;br /&gt;디스크와 마찬가지로 메모리에서도 Random Access가 발생하며, CPU가 메모리에 어떻게 접근하느냐에 따라 성능 차이가 많이 발생할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 벤치마크&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://redis.io/docs/latest/operate/oss_and_stack/management/optimization/benchmarks/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redis benchmark&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스 공식문서에서 확인할 수 있다시피, 10만 QPS의 연산을 처리할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DECR, 단 한 줄의 마법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis의 strings 자료구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Redis의 Strings 자료구조를 이해해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 문자열은 아니다.&lt;/p&gt;
&lt;pre id=&quot;code_1762343618942&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 숫자로도 사용이 가능하다.
SET counter 100
INCR counter  # 101
DECR counter  # 100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 숫자 문자열의 정수 &amp;lt;-&amp;gt; 문자열 파싱을 지원하며,&lt;b&gt; 증감 명령어&lt;/b&gt;(INCR, DECR, INCRBY, DECRBY)을 통해 &lt;b&gt;원자적&lt;b&gt;연산&lt;/b&gt;&lt;/b&gt;이 가능하다. 또한 증감 명령어의 결과값을 즉시 반환해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 아이디어: 재고를 &quot;카운터&quot;로 관리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 분산 락을 사용했을 때의 사고 방식은 아래와 같았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(1)재고를 확인하고 (2) 확인한 재고를 차감한다. &lt;br /&gt;두 단계를 원자적으로 처리하려면 락이 필요하다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://myvelop.tistory.com/258&quot;&gt;MySQL의 X-Lock을 활용한 처리 방법&lt;/a&gt;과 동일하게 발상을 전환해보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;재고를 차감하고 &amp;rarr; 결과를 확인한다.&quot; 한 단계로 통합한다.&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Redis에 총 티켓 수를 세팅해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1762344394585&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET ticket:count 1000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 확인한 것과 같이 재고를 차감하고 확인하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1762344321891&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TicketService {
    private final StringRedisTemplate redisTemplate;
    
    public TicketResponse purchaseWithDECR(Long userId) {
        // 재고 차감
        Long remaining = redisTemplate.opsForValue()
            .decrement(&quot;ticket:count&quot;);
        
        if (Objects.isNull(remaining) || remaining &amp;lt; 0) {
            throw new SoldOutException(&quot;매진&quot;);
        }
        
        // 티켓팅 성공!!
        saveTicket(userId, remaining);
        return new TicketResponse(remaining);
    }
}&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;h3 data-ke-size=&quot;size23&quot;&gt;중복 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 한 명의 유저가 중복으로 티켓팅을 할 수 있다는 문제가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 문제를 해결하기 위해 아래와 같은 접근 방법을 사용해볼 수 있을 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;userId에 분산 락을 걸어 발급을 확인.&lt;/li&gt;
&lt;li&gt;Redis의 Set 자료구조를 통해 중복을 확인.&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;이미 -40,000이 되어 재고를 다시 더해주는 것이 무의미해질 수도 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lua Script를 통한 원자성 확보&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Lua 스크립트를 &lt;b&gt;단일 명령어처럼&lt;/b&gt; 실행한다. 스크립트 실행 중에는 다른 명령어가 끼어들 수 없다는 얘기다. 이를 통해 원자성을 확보할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lua 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; background-color: #dddddd;&quot;&gt;&lt;b&gt;redis.call('SADD', KEYS[2], ARGV[1]) &lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #383a42;&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #dddddd;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #383a42; text-align: start;&quot;&gt;SADD&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #383a42; text-align: start;&quot;&gt;: &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Set에 멤버를 추가하는 명령이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #383a42;&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #dddddd;&quot;&gt;&lt;b&gt;KEYS[2]&lt;/b&gt;&lt;/span&gt;: &lt;/span&gt;구매자 목록 Set Key 전달&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #383a42;&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #dddddd;&quot;&gt;&lt;b&gt;ARGV[1]&lt;/b&gt;&lt;/span&gt;: 현재 사용자 ID&lt;/span&gt;&lt;span style=&quot;color: #383a42;&quot;&gt;&amp;nbsp;전달&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #383a42;&quot;&gt;반환값이 1이면 새로 추가된 것이고, 반환값이 0이면 이미 값이 존재한다는 의미다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1762345045455&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- DECR + 중복 체크를 원자적으로
local remaining = redis.call('DECR', KEYS[1])
if remaining &amp;lt; 0 then
    return -1  -- 매진
end

local purchased = redis.call('SADD', KEYS[2], ARGV[1])
if purchased == 0 then
    redis.call('INCR', KEYS[1])  -- 롤백
    return -2  -- 중복 구매
end

return remaining  -- 성공&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 스프링에서 위의 루아 스크립트를 실행하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1762345634038&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RedisScript&amp;lt;Long&amp;gt; ticketPurchaseScript = ...;

List&amp;lt;Long&amp;gt; result = redisTemplate.execute(
    ticketPurchaseScript,
    Arrays.asList(&quot;ticket:count&quot;, &quot;ticket:users&quot;),
    userId.toString()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점은 아래와 같다.&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;원자성 보장&lt;/b&gt;: 중간 상태에 대한 동시성 문제가 발생하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 효율&lt;/b&gt;: 왕복 1회로 모든 것이 처리된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 단점은 없을까? 사실 Lua도 만능은 아니다.&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;관리 포인트 증가&lt;/b&gt;: 애플리케이션 로직이 루아 스크립트로 분산된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호환성 이슈&lt;/b&gt;: Lua 사용자들은 지독한 하위 호환성 문제에 시달리고 있다고 한다. 마이너 버전을 올렸는데 안 되는 기능이 있다는 얘기가 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터 제약&lt;/b&gt;: 만약 Redis 클러스터를 사용하고 있다면 문제가 발생할 수 있다. Lua는 크로스 슬롯 연산이 불가능하기 때문이다. (모든 키가 같은 해시 슬롯에 있어야만 연산이 가능하다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블로킹 문제&lt;/b&gt;: 위에서 언급했다시피 Redis는 Lua를 단일 명령어 취급하기 때문에, Lua를 실행하는 동안 다른 명령어를 전부 블로킹한다. 따라서 너무 오래 걸리는 작업을 Lua로 실행하지 않도록 주의하자.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증 과정이 추가되어 5만 QPS 정도의 연산을 처리할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목에서처럼 10만 QPS를 처리하진 못하지만, 이 정도면 요구사항을 충족하는 빠른 성능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 고려사항&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Redis의 처리량을 DB가 따라오지 못한다면?&lt;/h4&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;대기열을 구성하여 Server-Sent Event로 티켓팅 처리&lt;/li&gt;
&lt;li&gt;이벤트 큐를 통한 비동기 처리로 사용자 응답 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 현재 QPS로 부족하다면?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster를 고려하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 티켓 키를 분산하자. ex) 100개 단위로 분할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 싱글 스레드 특성을 이해하고 활용하면, 복잡한 분산 락 없이도 동시성을 제어할 수 있음을 봤다.&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;사실 10만 QPS라는 숫자는 다소 과장되어 보일 수 있다.&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;&lt;b&gt;하지만 이론적 한계를 알고 있다면, 현실적인 목표도 더 명확히 세울 수 있다고 믿는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 이론상 10만 QPS가 가능하니, 우리 시스템에서 1만 QPS가 안 나온다면 Redis가 아닌 다른 곳에 병목이 있다는 판단을 할 수 있게 된다.&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;최근 유튜브에서 &quot;개발의 본질은 문제 해결&quot;이라는 메시지를 담은&amp;nbsp;&lt;a href=&quot;https://www.youtube.com/watch?si=hgEcPzjbem5BWBOh&amp;amp;v=Du1aeNElueA&amp;amp;feature=youtu.be&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;개발감각 있는지 확인하는 법&lt;/a&gt;&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;&lt;b&gt;깊은 이론적 이해가 있을 때, 비로소 단순하고 우아한 해결책이 보인다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 &quot;그냥 되네?&quot;에서 멈추지 않고, 원리를 이해할 수 있는 개발자로 성장해나가고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://subscription.packtpub.com/book/data/9781783988167/1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Getting started with Redis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;개발자를 위한 레디스&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chapakook.tistory.com/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://chapakook.tistory.com/6&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Redis</category>
      <category>java</category>
      <category>redis</category>
      <category>Spring</category>
      <category>동시성 처리</category>
      <category>레디스</category>
      <category>스프링</category>
      <category>자바</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/266</guid>
      <comments>https://myvelop.tistory.com/266#entry266comment</comments>
      <pubDate>Wed, 5 Nov 2025 22:04:44 +0900</pubDate>
    </item>
    <item>
      <title>자기 입맛에 맞는 NestJS 초기 세팅하기</title>
      <link>https://myvelop.tistory.com/265</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSsh6R/dJMb88MEOoW/ctzUURLkmeQ4bvbWuWQxzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSsh6R/dJMb88MEOoW/ctzUURLkmeQ4bvbWuWQxzK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSsh6R/dJMb88MEOoW/ctzUURLkmeQ4bvbWuWQxzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSsh6R%2FdJMb88MEOoW%2FctzUURLkmeQ4bvbWuWQxzK%2Fimg.jpg&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; alt=&quot;NestJS 이미지&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;966&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;966&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;h2 data-ke-size=&quot;size26&quot;&gt;NestJS 시작하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;NestJS boilerplate&quot;&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;NestJS를 처음 시작한 사람들은 한 번씩 검색해봤을 법한 키워드일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 막상 코드를 확인해보면 뭐가 뭔지, 다 필요한 건지 헷갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 `nest new`만 치고 시작하자니, 허전하다.&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;span&gt;선배 개발자가 옆에서 &quot;이것만 세팅하면 돼&quot; 라고 알려주는 느낌으로, &lt;/span&gt;&lt;span&gt;정말 필요한 설정들만 정리해봤다.&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;기본 설정&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. NVM 자동 설정하기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 노드 버전을 합의하지 않고 프로젝트를 시작했다고 해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여러 의존성을 설치하다보면 서로의 노드 버전이 맞지 않아 에러가 발생하게 될 수 있다.&lt;/span&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;이 문제는 NVM(Node Version Manager)으로 해결할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설치하기 (Mac 기준)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;homebrew를 통해 nvm을 설치하자.&lt;/p&gt;
&lt;pre id=&quot;code_1761132698648&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ brew install nvm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.zshrc 파일에 nvm 관련 설정을 추가해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1761132727524&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ vim ~/.zshrc&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1761132746565&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export NVM_DIR=&quot;$HOME/.nvm&quot;
[ -s &quot;/opt/homebrew/opt/nvm/nvm.sh&quot; ] &amp;amp;&amp;amp; \. &quot;/opt/homebrew/opt/nvm/nvm.sh&quot;
[ -s &quot;/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm&quot; ] &amp;amp;&amp;amp; \. &quot;/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 스크립트는 nvmrc 파일을 로드해 nvm을 자동으로 설정해주는 코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1761132841203&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Auto NVM
autoload -U add-zsh-hook
load-nvmrc() {
  [[ -a .nvmrc ]] || return
  local node_version=&quot;$(nvm version)&quot;
  local nvmrc_path=&quot;$(nvm_find_nvmrc)&quot;

  if [ -n &quot;$nvmrc_path&quot; ]; then
    local nvmrc_node_version=$(nvm version &quot;$(cat &quot;${nvmrc_path}&quot;)&quot;)

    if [ &quot;$nvmrc_node_version&quot; = &quot;N/A&quot; ]; then
      nvm install
    elif [ &quot;$nvmrc_node_version&quot; != &quot;$node_version&quot; ]; then
      nvm use
    fi
  elif [ &quot;$node_version&quot; != &quot;$(nvm version default)&quot; ]; then
    echo &quot;Reverting to nvm default version&quot;
    nvm use default
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 .zshrc 파일을 적용해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1761132880262&quot; class=&quot;shell&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;$ source ~/.zshrc&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.nvmrc&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프로젝트에 .nvmrc 파일을 생성하자.&lt;/p&gt;
&lt;pre id=&quot;code_1761132991512&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ echo 'v22.18.0' &amp;gt; .nvmrc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프로젝트 경로에서 터미널을 열면 자동으로 노드 버전이 v22.18.0로 고정될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Tip!&lt;/b&gt;&lt;br /&gt;asdf 도구를 쓰시는 분은 &lt;b&gt;.tool-versions&lt;/b&gt; 파일 설정을 찾아보세요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Type 설정 (tsconfig.json 기본 설정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeScript는 타입 안정성을 위해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 strict mode를 안 켜놓으면 TypeScript를 사용하는 의미가 반감된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 나는 tsconfig.json에서 strict mode를 키는 걸 적극 추천한다.&lt;/p&gt;
&lt;pre id=&quot;code_1761133207440&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;strict&quot;: true,
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;strict mode를 키면 아래 설정들이 활성화된다.&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;noImplicitAny&lt;/b&gt;: any 타입을 명시하지 않으면 에러&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;alwaysStrict&lt;/b&gt;: 모든 파일에 'use strict' 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;strictFunctionTypes&lt;/b&gt;: 함수 타입을 더 엄격하게 검사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;strictBindCallApply&lt;/b&gt;: bind, call, apply &lt;span&gt;타입&lt;/span&gt; &lt;span&gt;검사&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;strictPropertyInitialization&lt;/b&gt;: 클래스 속성 초기화 검사 (모든 프로퍼티를 생성자에서 선언/non-null 단언 연산자&amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; ! &lt;/span&gt;&lt;/b&gt;&lt;/span&gt;를 사용/strictPropertyInitialization를 false 둔다)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;strictNullChecks&lt;/b&gt;: null, undefined를 명시적으로 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;noImplicitThis&lt;/b&gt;: this 타입이 any면 에러&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;나중에 &quot;아 이거 지워야 하는데 깜빡했네&quot; 하는 일이 없어질 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1761133917392&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;noUnusedLocals&quot;: true,              // 안 쓰는 변수 있으면 에러
    &quot;noUnusedParameters&quot;: true,          // 안 쓰는 매개변수 있으면 에러  
    &quot;noFallthroughCasesInSwitch&quot;: true,  // switch문에 return이나 break 빠뜨리면 에러
    &quot;noImplicitReturns&quot;: true            // 모든 경로에서 return 하도록 강제
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 절대 경로 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로 경로 설정을 해주지 않았다면 곧 만나게 될 상대 경로 불지옥을 만나게 될 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1761133996826&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { UserService } from '../../../modules/user/user.service';
import { AuthGuard } from '../../../../common/guards/auth.guard';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 아래와 같이 절대 경로로 보이도록 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1761134059883&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { UserService } from '@app/user/user.service';
import { AuthGuard } from '@shared/guards/auth.guard';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정은 간단하다. tsconfig.json을 수정해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1761134094114&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;./&quot;,
    &quot;paths&quot;: {
      &quot;@app/*&quot;: [&quot;src/*&quot;],
      &quot;@shared/*&quot;: [&quot;src/shared/*&quot;],
      &quot;@config/*&quot;: [&quot;src/config/*&quot;]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Jest를 사용한다면 package.json나 jest.config.js에 상대 경로에 대한 설정을 추가해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1761134294342&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;moduleNameMapper&quot;: {
  &quot;^@/(.*)$&quot;: &quot;&amp;lt;rootDir&amp;gt;/$1&quot;,
  &quot;^@config/(.*)$&quot;: &quot;&amp;lt;rootDir&amp;gt;/config/$1&quot;,
  &quot;^@shared/(.*)$&quot;: &quot;&amp;lt;rootDir&amp;gt;/shared/$1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 패키지 매니저 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 패키지 매니저 3가지를 비교해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;npm&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도 설치 불필요&lt;/li&gt;
&lt;li&gt;모든 환경에서 작동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;속도가 느림&lt;/li&gt;
&lt;li&gt;디스크 공간 많이 차지&lt;/li&gt;
&lt;li&gt;유령 의존성 발생 가능성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;yarn&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;npm보다 빠름&lt;/li&gt;
&lt;li&gt;안정적인 lock 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도 설치 필요&lt;/li&gt;
&lt;li&gt;yarn berry는 러닝커브 있음&lt;/li&gt;
&lt;li&gt;유령 의존성 발생 가능성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;pnpm&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠르다&lt;/li&gt;
&lt;li&gt;디스크 공간 효율적 활용&lt;/li&gt;
&lt;li&gt;심볼릭 링크를 사용해 유령 의존성을 차단한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도 설치 필요&lt;/li&gt;
&lt;li&gt;가끔 호환성 이슈 발생&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;h3 data-ke-size=&quot;size23&quot;&gt;5. 코드 스타일 통일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;javascript를 사용해봤다면 Prettier와 Lint는 한 번씩 들어봤을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 설정을 어떻게 하든 상관없다. &lt;b&gt;팀에서 통일만 되면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Prettier 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 .prettierrc 파일부터 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1761135162240&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;singleQuote&quot;: true,           // 작은 따옴표 사용
  &quot;trailingComma&quot;: &quot;all&quot;,        // 마지막 콤마 항상
  &quot;semi&quot;: true,                  // 세미콜론 사용
  &quot;printWidth&quot;: 80,              // 한 줄 최대 길이
  &quot;tabWidth&quot;: 2,                 // 탭 너비
  &quot;useTabs&quot;: false,              // 스페이스 사용
  &quot;arrowParens&quot;: &quot;always&quot;,       // 화살표 함수 괄호 항상
  &quot;endOfLine&quot;: &quot;lf&quot;              // 개행문자 LF로 통일
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.prettierignore를 통해 굳이 코드 스타일이 적용될 필요가 없는 곳은 제외해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1761135235830&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dist
node_modules
등등등&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ESLint 룰 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prettier가 스타일을 담당한다면, ESLint는 코드 품질을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드 예제는 최신 자바스크립트 모듈, ES Module 전용 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;eslint.config.mjs &lt;/span&gt;&lt;/b&gt;를 기준으로 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;import js from '@eslint/js'

export default tseslint.config(
  {
     rules: {
       '@typescript-eslint/no-explicit-any': 'error',
       '@typescript-eslint/no-floating-promises': 'error',
       '@typescript-eslint/no-unsafe-argument': 'error',
       '@typescript-eslint/no-unsafe-assignment': 'error',
       '@typescript-eslint/no-unsafe-member-access': 'error',
       '@typescript-eslint/no-unsafe-return': 'error',
       '@typescript-eslint/explicit-function-return-type': 'off',
       'prettier/prettier': ['error', { endOfLine: 'auto' }],
     },
  },
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 룰에 대한 내용은 eslint 공식문서에서 찾아서 설정하자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ESLint 플러그인 설치하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 룰이 세팅되어 있는 설정이 있다. 가장 유명한 설정 중 하나가 airbnb 설정이다.&lt;/p&gt;
&lt;pre id=&quot;code_1761136467705&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -D eslint-config-airbnb-typescript&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일에 해당 플러그인을 전달하기만 하면 airbnb 룰을 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1761136505480&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default tseslint.config(
  // ...
  airbnbTypescript,
  // ...
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Prettier와 ESLint 충돌 해결하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eslint-config-prettier는 eslint에서 prettier와 충돌하는 eslint 규칙을 전부 꺼준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;eslint와 prettier의 역할을 정확히 구분하기 위해 사용하는 것이 좋다.&lt;/p&gt;
&lt;pre id=&quot;code_1761135386976&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -D eslint-config-prettier&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd; color: #000000;&quot;&gt;&amp;nbsp;eslint.config.mjs &lt;/span&gt;&lt;/b&gt;에서 아래 설정을 추가해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1761135809577&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default tseslint.config(
  // ...
  eslint.configs.recommended,
  // ...
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Prettier를 린터로 사용하기&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;eslint-plugin-prettier 의존성은 prettier를 린터인 것처럼 실행할 수 있는 플러그인이다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #080808; text-align: start;&quot;&gt;eslint-config-prettier를 통해 포맷팅에 관련된 eslint 규칙을 전부 제거해야 에러가 발생하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre id=&quot;code_1761135567733&quot; class=&quot;shell&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;$ npm i -D eslint-plugin-prettier&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;그리고 &lt;span style=&quot;background-color: #dddddd; color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp;eslint.config.mjs&lt;/b&gt; &lt;/span&gt;에서 설정을 추가해주자.&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre id=&quot;code_1761135823960&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;export default tseslint.config(
  // ...
  eslintPluginPrettierRecommended,
  // ...
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Tip!&lt;/b&gt;&lt;br /&gt;타이핑할 때, IDE에서 전역으로 규칙을 적용해주는 설정도 있다.&lt;br /&gt;.editorconfig를 찾아보라!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Husky 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;린트 돌리는 거 깜빡하고 커밋하고 푸시했다가 CI/CD 파이프라인에서 봉변(?)을 당할 수도 있다.&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;이때 Husky를 사용하면 이 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git hook을 활용해 커밋하기 전에 Husky가 알아서 린트를 실행하도록 설정할 수 있다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre id=&quot;code_1761136721552&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i -D husky
$ npm exec husky init
$ npm i -D lint-staged&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Tip!&lt;/b&gt;&lt;br /&gt;commitlint를 사용하면 커밋 메시지에 대해서도 제약을 걸 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NestJS 필수 코드 만들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. ConfigModule 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;a href=&quot;https://docs.nestjs.com/techniques/configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nest 문서&lt;/a&gt;를 보고선 만들면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 configService에서 문자열 키 값으로 환경변수를 꺼내올 때 실수가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 블로그를 보면 TypeConfigService를 구성해서 문자열 자동 완성과 컴파일 에러까지 잡아주는 시스템을 만들 수 있다.&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.to/hantaihe/typed-configservice-in-nestjs-1nml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Typed ConfigService in NestJS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 로그 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약&lt;span&gt; &lt;/span&gt;예기치&lt;span&gt; &lt;/span&gt;못하게&lt;span&gt; &lt;/span&gt;애플리케이션이&lt;span&gt; &lt;/span&gt;종료되었을&lt;span&gt; &lt;/span&gt;때&lt;span&gt;, &lt;/span&gt;로그&lt;span&gt; &lt;/span&gt;파일을&lt;span&gt; &lt;/span&gt;저장하지&lt;span&gt; &lt;/span&gt;않으면&lt;span&gt; &lt;/span&gt;문제&lt;span&gt; &lt;/span&gt;원인을&lt;span&gt; &lt;/span&gt;파악하기&lt;span&gt; &lt;/span&gt;어렵다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Winston을 사용하면 손 쉽게 파일 로그를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761455817688&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install --save nest-winston winston winston-daily-rotate-file&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;winston-daily-rotate-file 설정을 활용해 로그 파일 저장 설정을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Winston은&amp;nbsp;파일&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;로그를 쌓을 때,&amp;nbsp;기본적으로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;비동기로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;동작하기&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;때문에&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;성능&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761455918305&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import DailyRotateFile from 'winston-daily-rotate-file';

export enum LogLevel {
	ERROR = 'error',
	WARN = 'warn',
	INFO = 'info',
	HTTP = 'http',
	VERBOSE = 'verbose',
	DEBUG = 'debug',
	SILLY = 'silly',
}

const createFileTransports = (type: string, level?: LogLevel): winston.transport =&amp;gt; {
	return new DailyRotateFile({
		level,
		datePattern: 'YYYY-MM-DD',
		dirname: `${process.cwd()}/logs`,  // 저장 경로
		filename: `%DATE%.${type}.log`,    // 파일 이름
		maxFiles: MAX_FILES,               // 최대 일수
		maxSize: MAX_SIZE,                 // 로그 파일 최대 크기
		zippedArchive: true,               // 압축 여부
	});
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 아래와 같이 로그 포맷을 구체적으로 정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1761456648765&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import * as winston from 'winston';

const { combine, timestamp, label, printf, colorize } = winston.format;

const logFormat = printf(({ level, message, label, timestamp }) =&amp;gt; {
	return `${timestamp as string} [${label as string}] ${level}: ${message as string}`;
});

const createConsoleFormat = (applicationName: string) =&amp;gt;
	combine(timestamp({ format: TIMESTAMP }), label({ label: applicationName }), colorize({ all: true }), logFormat);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 로그 포맷과 파일 설정을 LoggerOptions 객체로 엮어 WinstonModule을 생성할 때 전달하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1761456605493&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export function createWinstonConfig(configService: TypedConfigService): LoggerOptions {
	const applicationName = configService.get('applicationName');
	const nodeEnv = configService.get('nodeEnv');
	const logLevel = configService.get('logLevel');

	const consoleTransport = new winston.transports.Console({ format: createConsoleFormat(applicationName) });
	const fileTransports = [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO].map(level =&amp;gt;
		createFileTransports(level, level),
	);

	return {
		level: logLevel,
		format: createFileFormat(applicationName),
		defaultMeta: { environment: nodeEnv },
		transports: [consoleTransport, ...fileTransports],
		exceptionHandlers: [createFileTransports(FILE_NAME_EXCEPTION)],
		exitOnError: false,
	};
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 추가적으로 LoggingInterceptor까지 구현해주면 더 좋다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;logging.interceptor.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761456174009&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Injectable()
export class LoggingInterceptor implements NestInterceptor {
	constructor(
		@Inject(WINSTON_MODULE_NEST_PROVIDER)
		private readonly logger: LoggerService,
	) {}

	intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;unknown&amp;gt; {
		const ctx = context.switchToHttp();
		const request = ctx.getRequest&amp;lt;Request&amp;gt;();
		const response = ctx.getResponse&amp;lt;Response&amp;gt;();

		const { method, url, ip } = request;
		const requestBody = JSON.stringify(request.body);

		this.logger.log({
			context: 'HTTP',
			message: `[Request] method=${method}, url=${url}, ip=${ip}, body=${requestBody}`,
		});

		return next.handle().pipe(
			tap({
				next: () =&amp;gt; {
					const { statusCode } = response;
					this.logger.log({
						context: 'HTTP',
						message: `[Response] method=${method}, url=${url}: statusCode=${statusCode}`,
					});
				},
				error: (error: Error) =&amp;gt; {
					this.logger.error({
						context: 'HTTP',
						message: `[Error] ${method} ${url}: ${error.message}`,
					});
				},
			}),
		);
	}
}&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;shared.module.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761456573700&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module({
	imports: [
		WinstonModule.forRootAsync({
			inject: [TypedConfigService],
			useFactory: createWinstonConfig,
		}),
	],
	providers: [
		{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
	],
})
export class SharedModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 전역 에러 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/exception-filters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nest의 Exception Filters 문서&lt;/a&gt;를 보면 쉽게 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 AllExceptionFilter를 만들어 그 안에서 if-else문으로 분기 처리하는 것보다 여러 개의 ExceptionFilter를 만들어 구체적인 에러가 먼저 잡히도록 구성하는 게 더 좋은 구조라고 생각한다. 그 이유는 아래와 같다.&lt;/p&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;에러를 다룰 때 타입 캐스팅을 할 필요가 사라진다.&lt;/li&gt;
&lt;/ol&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;custom-exception.filter.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761456356725&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Catch(CustomException)
export class CustomExceptionFilter implements ExceptionFilter {
	catch(exception: CustomException, host: ArgumentsHost) {
		const ctx = host.switchToHttp();
		const response = ctx.getResponse&amp;lt;Response&amp;gt;();
		const request = ctx.getRequest&amp;lt;Request&amp;gt;();
		const status = exception.getStatus();

		const errorResponse: ErrorResponse = {
			statusCode: status,
			errorCode: exception.errorCode,
			message: exception.message,
			timestamp: dayjs().toISOString(),
			path: request.url,
		};

		response.status(status).json(errorResponse);
	}
}&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;http-exception.filter.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761456348856&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
	catch(exception: HttpException, host: ArgumentsHost) {
		const ctx = host.switchToHttp();
		const response = ctx.getResponse&amp;lt;Response&amp;gt;();
		const request = ctx.getRequest&amp;lt;Request&amp;gt;();
		const status = exception.getStatus();

		const errorResponse: ErrorResponse = {
			statusCode: status,
			errorCode: status,
			message: exception.message,
			timestamp: dayjs().toISOString(),
			path: request.url,
		};

		response.status(status).json(errorResponse);
	}
}&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;all-exception.filter.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761456398426&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Catch()
export class AllExceptionFilter implements ExceptionFilter {
	catch(exception: unknown, host: ArgumentsHost) {
		const ctx = host.switchToHttp();
		const response = ctx.getResponse&amp;lt;Response&amp;gt;();
		const request = ctx.getRequest&amp;lt;Request&amp;gt;();

		const statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
		const message = exception instanceof Error ? exception.message : 'Internal server error';

		const errorResponse: ErrorResponse = {
			statusCode,
			errorCode: statusCode,
			message,
			timestamp: dayjs().toISOString(),
			path: request.url,
		};

		response.status(statusCode).json(errorResponse);
	}
}&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;shared.module.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761456486163&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Module({  // 구체적인 것이 마지막에 오도록
	providers: [
		{ provide: APP_FILTER, useClass: AllExceptionFilter },
		{ provide: APP_FILTER, useClass: HttpExceptionFilter },
		{ provide: APP_FILTER, useClass: CustomExceptionFilter },
	],
})
export class SharedModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 헬스 체크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS에서 제공해주는 기능이 있다!&lt;/p&gt;
&lt;pre id=&quot;code_1761137139848&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm i @nestjs/terminus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.nestjs.com/recipes/terminus#http-healthcheck&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nest 문서의 Health checks&lt;/a&gt;에 잘 나와 있으니 이걸 참고해서 구성하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;준비 끝!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 협업을 위한 기본기부터 시작해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node 버전은 nvm으로 통일했고, 코드 스타일은 Prettier와 ESLint가 알아서 맞춰준다. 누가 커밋을 하든 Husky가 검사하고, Commitlint가 커밋 규칙을 지키게 만들 것이다.&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;p data-ke-size=&quot;size16&quot;&gt;에러가 발생하면 일관된 형식으로 로그가 남고, Winston이 차곡차곡 정리해준다. 헬스 체크를 통해 로드밸런서나 쿠버네티스와 연동하기도 쉬워졌다.&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;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;&amp;nbsp;&lt;/p&gt;</description>
      <category>NestJS</category>
      <category>Backend</category>
      <category>javascript</category>
      <category>nest</category>
      <category>nestjs</category>
      <category>nvm</category>
      <category>tsconfig</category>
      <category>typescript</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/265</guid>
      <comments>https://myvelop.tistory.com/265#entry265comment</comments>
      <pubDate>Sun, 26 Oct 2025 15:22:53 +0900</pubDate>
    </item>
    <item>
      <title>AOP로 동시성 처리 코드 분리하기</title>
      <link>https://myvelop.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Spring AOP를 활용해 분산 락 처리 코드를 비즈니스 로직에서 완전히 분리하는 방법을 소개한다. 동시성을 제어를 위해 Lock 인터페이스를 사용할 때, try-finally 블록과 락 관리 코드가 비즈니스 로직을 압도하는 문제를 겪게 된다.&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;Spring의 @Transactional이 트랜잭션 관리를 단순화한 것처럼, DistributedLock Aspect를 만들어 이 문제를 해결할 것이다. 또한, Spring Cache의 검증된 패턴을 참고하여 SpEL 평가기를 만들어 @DistributedLock의 사용성을 확장해볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lock 인터페이스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java/Spring에서 Lock 인터페이스를 직접 사용하면 필연적으로 try-finally 블록을 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러가 발생했을 때 락을 해제하지 않으면 데드락이 발생하기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1760246699029&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class PostService {
    private final LockRegistry lockRegistry;  // RedisLockRegistry, JdbcLockRegistry 등 사용 가능
    private final PostRepository postRepository;
    
    public void likePost(Long postId, Long userId) {
        String lockKey = &quot;lock:post:&quot; + postId + &quot;:like:&quot; + userId;
        Lock lock = lockRegistry.obtain(lockKey);
        
        try {
            if (!lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
                throw new LockAcquisitionException(&quot;Failed to acquire lock&quot;);
            }
            
            // 비즈니스 로직 //
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new LockException(&quot;Interrupted while acquiring lock&quot;, e);
        } finally {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 가독성 저하&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드 예시에서 락 처리를 위한 코드가 12줄이나 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, try-finally 구문으로 인해 코드 depth가 생긴다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 코드 중복&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 곳에서도 Lock 인터페이스를 사용할 때, tryLock()에 대한 예외 처리, 인터럽트에 대한 처리, 락 자원 회수 등의 코드 중복이 발생할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 에러 처리 일관성&lt;/h4&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;h3 data-ke-size=&quot;size23&quot;&gt;비즈니스 로직에 집중하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 @Transactional처럼 어노테이션으로 단순하게 처리할 수 있으면 어떨까?&lt;/p&gt;
&lt;pre id=&quot;code_1760247155958&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class PostService {
    private final PostRepository postRepository;
    
    @DistributedLock(key = &quot;'lock:post:' + #postId + ':like:' + #userId&quot;)
    public void likePost(Long postId, Long userId) {
        // 이제 비즈니스 로직만 남는다.
    }
}&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;@DistributedLock 어노테이션 하나만으로 락 처리를 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 처리하기 전에 먼저 알아둬야 하는 개념이 있다. AOP와 SpEL이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring AOP&lt;/h2&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;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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C5Rcj/btsQ5iiCsap/2W7HltLh6QSBk54GxCguW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C5Rcj/btsQ5iiCsap/2W7HltLh6QSBk54GxCguW1/img.png&quot; data-alt=&quot;공통 기능이 여러 기능을 횡단하는 것처럼 보인다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C5Rcj/btsQ5iiCsap/2W7HltLh6QSBk54GxCguW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC5Rcj%2FbtsQ5iiCsap%2F2W7HltLh6QSBk54GxCguW1%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; alt=&quot;횡단 관심사에 대해 설명 이미지&quot; loading=&quot;lazy&quot; width=&quot;1224&quot; height=&quot;808&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;808&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;&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;하나의 클래스나 메소드가 너무 많은 책임을 갖게 된다. 응집도가 떨어진다는 말이다. 결합도도 높아진다.&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;Spring AOP는 Aspect를 통해 이 문제를 해결한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;참고! Spring AOP 핵심 용어 정리!&lt;br /&gt;1. Aspect: 횡단 관심사를 모듈화한 것&lt;br /&gt;2. Advice: 언제 무엇을 할지 정의 (@Around, @Before, @After 등)&lt;br /&gt;3. Pointcut: 어디에 적용할지 정의 (@Transactional이 붙은 메서드)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Aspect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;횡단 관심사를 분리하지 않으면 코드는 아래와 같은 형태를 띄게 될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diu0VC/btsQ7M92oX0/cXKHUqcuXMl4pN0klkaOEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diu0VC/btsQ7M92oX0/cXKHUqcuXMl4pN0klkaOEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diu0VC/btsQ7M92oX0/cXKHUqcuXMl4pN0klkaOEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdiu0VC%2FbtsQ7M92oX0%2FcXKHUqcuXMl4pN0klkaOEk%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; alt=&quot;비즈니스 로직에서 횡단 관심사를 분리하지 않았을 때&quot; loading=&quot;lazy&quot; width=&quot;1296&quot; height=&quot;920&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;920&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;Spring AOP에서 그 모듈을 Asepct라고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coKmuz/btsQ4mrZnwX/zDl463gfHn5KIyBYokVpR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coKmuz/btsQ4mrZnwX/zDl463gfHn5KIyBYokVpR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coKmuz/btsQ4mrZnwX/zDl463gfHn5KIyBYokVpR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoKmuz%2FbtsQ4mrZnwX%2FzDl463gfHn5KIyBYokVpR1%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; alt=&quot;Aspect 적용&quot; loading=&quot;lazy&quot; width=&quot;1338&quot; height=&quot;958&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aspect를 통해 각 클래스와 메소드는 부가 기능을 가져다 사용할 수 있으며, 핵심 비즈니스 로직에 집중할 수 있게 된다. 이를 통해 위에서 제시된 문제점들은 말끔히 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP는 &lt;b&gt;프록시 패턴&lt;/b&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해, 우리가 호출하는 객체를 Spring이 감싸는 래퍼(Wrapper) 객체를 만든다.&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;@Distributed 어노테이션의 예시를 살펴 보면, 대충 아래와 프록시 객체가 만들어지게 될 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1760248913945&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PostServiceProxy extends PostService {
    private PostService target;
    
    public void likePost(Long postId, Long userId) {
        // 전처리: 락 획득
        acquireLock(...);
        try {
            // 실제 메서드 호출
            target.likePost(postId, userId);
        } finally {
            // 후처리: 락 해제
            releaseLock(...);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링은 빈을 주입할 때,&lt;/b&gt; PostService를 주입하지 않고 &lt;b&gt;PostServiceProxy를 주입해준다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로, likePost를 호출하는 객체들은 PostService가 아닌 PostServiceProxy의 likePost를 호출하게 되고 락 처리가 자동으로 이뤄지게 되는 것이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SpEL(Spring Expression Language)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문자열로 코드 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpEL은 Spring에서 제공하는 표현식 언어다. &lt;b&gt;문자열 안에 동적인 로직&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;을 작성할 수 있게 해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현식은 보통 #{...} 형태로 작성하고, SpelExpressionParser 구현체를 통해 평가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1760249873927&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 문법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 기본적인 표현식&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리터럴(Literal)&lt;/b&gt;: 문자열, 숫자, boolean, null과 같은 기본적인 값을 직접 표현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760249831056&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(&quot;'Hello World'&quot;); 
String message = (String) exp.getValue();&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;연산자(Operators)&lt;/b&gt;: 산술, 관계, 논리 연산자도 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760250000308&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;boolean falseValue = parser.parseExpression(&quot;2 &amp;lt; -5.0&quot;).getValue(Boolean.class);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 클래스 표현식&lt;/h4&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 id=&quot;code_1760250141624&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String city = (String) parser.parseExpression(&quot;placeOfBirth.city&quot;).getValue(context);
String bc = parser.parseExpression(&quot;'abc'.substring(1, 3)&quot;).getValue(String.class);&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760250211921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Object bean = parser.parseExpression(&quot;@someBean&quot;).getValue(context);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 컬렉션 표현식&lt;/h4&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 id=&quot;code_1760250289723&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String invention = parser.parseExpression(&quot;inventions[3]&quot;).getValue(context, tesla, String.class);
String name = parser.parseExpression(&quot;members[0].name&quot;).getValue(context, ieee, String.class);&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;h3 data-ke-size=&quot;size23&quot;&gt;SpEL 어디에서 사용하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 정의된 어노테이션 곳곳에서 이미 SpEL을 사용하고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1760249387979&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Value(&quot;${server.port}&quot;) // 프로퍼티 값 주입&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1760249406044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable(key = &quot;'userId:' + #userId&quot;) // 캐시 키 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DistributedLock 컴포넌트 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위에서 배운 개념을 바탕으로 분산 락을 Aspect로 처리하는 코드를 작성해볼 것이다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;락의 키와 획득 대기 시간, 조건부 락 등의 기능이 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1760250460212&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    /**
     * SpEL expression for lock key
     * 예: &quot;'user:' + #userId&quot;, &quot;'order-' + #order.id&quot;
     */
    String key();
    
    /**
     * 락 획득 대기 시간 (밀리초)
     */
    long waitTimeMillis() default 5000;
    
    /**
     * 조건부 락 - true일 때만 락 획득
     * 예: &quot;#amount &amp;gt; 1000&quot;
     */
    String condition() default &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SpEL 평가기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 파라미터를 SpEL 변수로 변환하고, 표현식을 평가하는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 클래스는 Spring Cache의 기능을 모방해 만들어 봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1760250726888&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DistributedLockExpressionEvaluator extends CachedExpressionEvaluator {
    
    private final SpelExpressionParser parser = new SpelExpressionParser();
    
    public String getLockKey(String keyExpression, Method method, Object[] args, Object target) {
        
        // 평가 컨텍스트 생성
        EvaluationContext context = createEvaluationContext(method, args, target);
        
        // SpEL 표현식 파싱 및 평가
        Expression expression = parser.parseExpression(keyExpression);
        Object value = expression.getValue(context);
        
        return value != null ? value.toString() : &quot;&quot;;
    }
    
    public boolean checkCondition(String condition, Method method, Object[] args, Object target) {
        if (condition.isEmpty()) {
            return true;  // 조건이 없으면 항상 true
        }
        
        EvaluationContext context = createEvaluationContext(method, args, target);
        Expression expression = parser.parseExpression(condition);
        
        return Boolean.TRUE.equals(expression.getValue(context, Boolean.class));
    }
    
    private EvaluationContext createEvaluationContext(Method method, Object[] args, Object target) {
        // 메서드 파라미터를 변수로 등록
        StandardEvaluationContext context = new StandardEvaluationContext(target);
        
        // 파라미터 이름 추출
        ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
        String[] paramNames = discoverer.getParameterNames(method);
        
        // 파라미터를 SpEL 변수로 등록
        if (paramNames != null) {
            for (int i = 0; i &amp;lt; paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
        }
        
        return context;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DistributedLockAspect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aspect를 구현한 클래스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP를 통해 @DistributedLock이 붙은 메서드를 가로채고, 락 처리를 수행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1760250640571&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
@Slf4j
public class DistributedLockAspect {
    
    private final LockRegistry lockRegistry;
    private final DistributedLockExpressionEvaluator evaluator;
    
    public DistributedLockAspect(LockRegistry lockRegistry) {
        this.lockRegistry = lockRegistry;
        this.evaluator = new DistributedLockExpressionEvaluator();
    }
    
    @Around(&quot;@annotation(distributedLock)&quot;)
    public Object handleDistributedLock(ProceedingJoinPoint joinPoint, 
                                        DistributedLock distributedLock) throws Throwable {
        
        // 메서드 정보 추출
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();
        Object target = joinPoint.getTarget();
        
        // SpEL로 락 키 생성
        String lockKey = evaluator.getLockKey(
            distributedLock.key(), 
            method, 
            args, 
            target);
        
        // 조건 확인 (condition이 있고 false면 락 없이 실행)
        if (!evaluator.checkCondition(distributedLock.condition(), method, args, target)) {
            return joinPoint.proceed();
        }
        
        // 락 획득 및 메서드 실행
        Lock lock = lockRegistry.obtain(lockKey);
        boolean acquired = false;
        
        try {
            acquired = lock.tryLock(distributedLock.waitTimeMillis(), TimeUnit.MILLISECONDS);
            
            if (!acquired) {
                throw new LockAcquisitionException(
                    String.format(&quot;Failed to acquire lock for key: %s&quot;, lockKey)
                );
            }
            
            return joinPoint.proceed();
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new LockException(&quot;Thread interrupted&quot;, e);
        } finally {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@DistributedLock 사용 예시&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;클래스 파라미터에 직접 접근해서 키를 생성하는 예시이다.&lt;/p&gt;
&lt;pre id=&quot;code_1760251411967&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DistributedLock(key = &quot;'lock:post:' + #command.postId + ':like:' + #command.userId&quot;)
public void likePost(LikePostCommand command) {
    // 비즈니스 로직 //
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건부 락 걸기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 상품 타임 핫딜에 대한 예제이다.&lt;/p&gt;
&lt;pre id=&quot;code_1760251846416&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class FlashSaleService {

    private final ItemInventoryRepository inventoryRepository;
    private final FlashSaleScheduleManager scheduleManager; // 핫딜 시간 관리 Bean

    @DistributedLock(
        key = &quot;'item:' + #request.itemId&quot;,
        // SpEL을 사용하여 'flashSaleScheduleManager' Bean의 isActive 메서드를 호출
        condition = &quot;@flashSaleScheduleManager.isActive(#request.itemId)&quot;)
    public boolean purchase(PurchaseCommand command) {
        // 비즈니스 로직 //
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;따라서 임계영역을 최소화하고자 하는 요구가 생길 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1760252078873&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class ReportService {

    // Bad: 전체 메서드를 락으로 보호
    @DistributedLock(key = &quot;'report:' + #reportId&quot;)
    public ReportResult generateReport(Long reportId) {
        Report report = loadReport(reportId);   // 락 불필요
        FileData file = generateFile(report);   // 락 불필요  
        updateReportComplete(reportId);         // 락 필요!
        return new ReportResult(file);
    }
    
    private void updateReportComplete(Long reportId) {
        // 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 updateReportComplete에&amp;nbsp; AOP는 기본적으로 &lt;b&gt;내부 메소드에 @DistributedLock 어노테이션을 달면 어떻게 될까? 의도한대로 동작하지 않는다. 락이 걸리지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760252120547&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class ReportService {

    public ReportResult generateReport(Long reportId) {
        Report report = loadReport(reportId);   // 락 불필요
        FileData file = generateFile(report);   // 락 불필요  
        updateReportComplete(reportId);         // 락 동작하지 않는다.
        return new ReportResult(file);
    }

    @DistributedLock(key = &quot;'report:' + #reportId&quot;)
    public void updateReportComplete(Long reportId) {
        // 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내부 메소드 호출은 프록시(Proxy)를 거치지 않고, 객체 내부에서 직접 일어나기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lo8FC/btsQ5iQvF8A/TepzpRdHobTNNZSB1Dkbnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lo8FC/btsQ5iQvF8A/TepzpRdHobTNNZSB1Dkbnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lo8FC/btsQ5iQvF8A/TepzpRdHobTNNZSB1Dkbnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flo8FC%2FbtsQ5iQvF8A%2FTepzpRdHobTNNZSB1Dkbnk%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; alt=&quot;내부 메소드를 호출하면 프록시가 아닌 원본을 호출한다.&quot; loading=&quot;lazy&quot; width=&quot;1288&quot; height=&quot;1244&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;updateReportComplete 메소드가 실행되는 시점은 이미 프록시를 통과하여 &lt;b&gt;원본 ReportService 객체 내부로 들어온 때이다. 이 원본 객체 내부에서 사용하는 this는 &lt;b&gt;프록시를 가리키는 것이 아니라, 원본 객체 자기 자신&lt;/b&gt;을 가리키므로, 당연히 Lock이 적용되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 문제를 해결하고 싶다면 클래스를 분리하여 updateReportComplete를 구현해줘야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1760252383812&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class ReportService {

    private final ReportUpdateService reportUpdateService;

    public ReportResult generateReport(Long reportId) {
        Report report = loadReport(reportId);
        FileData file = generateFile(report);
        reportUpdateService.updateReportComplete(reportId); // 락 동작!!!
        return new ReportResult(file);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1760252450306&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class ReportUpdaterService {

    @DistributedLock(key = &quot;'report:' + #reportId&quot;)
    public void updateReportComplete(Long reportId) {
        // 로직 //
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프록시를 통과하므로 락이 정상적으로 걸리게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8zFEJ/btsQ50ognDt/1uyUUMiJnWXkUdBtxQkJtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8zFEJ/btsQ50ognDt/1uyUUMiJnWXkUdBtxQkJtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8zFEJ/btsQ50ognDt/1uyUUMiJnWXkUdBtxQkJtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8zFEJ%2FbtsQ50ognDt%2F1uyUUMiJnWXkUdBtxQkJtK%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; alt=&quot;클래스를 나눠 외부에서 호출해야 프록시가 정상적으로 동작한다.&quot; loading=&quot;lazy&quot; width=&quot;2082&quot; height=&quot;1074&quot; data-origin-width=&quot;2082&quot; data-origin-height=&quot;1074&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;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락 처리 코드가 비즈니스 로직을 압도하는 문제를 AOP와 SpEL을 활용해 해결했다. Spring이 @Transactional로 트랜잭션 관리를 단순화했듯이, 위 예제에서는 @DistributedLock으로 분산 락을 단순화했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드는 우리의 의도를 표현하는 수단이다.&lt;/b&gt; try-finally 블록과 보일러플레이트 코드로 가득한 메서드는 우리가 정말로 해결하려는 문제를 흐리게 만든다. 반면 @DistributedLock을 사용한 메서드는 &quot;이 작업은 동시에 실행되면 안 돼&quot;라는 의도를 명확히 드러낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;좋은 추상화는 복잡함을 적절한 곳에 격리시키는 거라고 생각한다.&lt;/b&gt; 우리가 만든 @DistributedLock이 바로 그런 역할을 한다. 락의 복잡성은 여전히 존재하지만, 이제 그것은 인프라 레이어에 깔끔하게 격리되어 있다.&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;&amp;nbsp;&lt;/p&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.spring.io/spring-framework/reference/core/expressions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 공식문서, Spring Expression Language&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%EB%8F%99%EC%8B%9C%EC%84%B1%20%EC%B2%98%EB%A6%AC%20%EC%8B%9C%EB%A6%AC%EC%A6%88-1&quot; style=&quot;color: #000000; text-align: start;&quot; 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://myvelop.tistory.com/262&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;처음부터&amp;nbsp;다시&amp;nbsp;배우는&amp;nbsp;Java&amp;nbsp;동시성&amp;nbsp;제어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/258&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UPDATE 한 줄로 끝내는 동시성 처리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/263&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;좋아요 기능으로 알아보는 넥스트 키 락&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lettuce&amp;nbsp;분산&amp;nbsp;락의&amp;nbsp;오해와&amp;nbsp;진실&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AOP로&amp;nbsp;동시성&amp;nbsp;처리&amp;nbsp;코드&amp;nbsp;분리하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>AOP</category>
      <category>Aspect</category>
      <category>spel</category>
      <category>Spring</category>
      <category>분산 락</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/264</guid>
      <comments>https://myvelop.tistory.com/264#entry264comment</comments>
      <pubDate>Sun, 12 Oct 2025 16:23:49 +0900</pubDate>
    </item>
    <item>
      <title>Lettuce 분산 락의 오해와 진실 (feat. RedisLockRegistry)</title>
      <link>https://myvelop.tistory.com/260</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yHW4k/btsQ18tBnqA/Yp68M8nxOpHCXDt0IwvOn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yHW4k/btsQ18tBnqA/Yp68M8nxOpHCXDt0IwvOn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yHW4k/btsQ18tBnqA/Yp68M8nxOpHCXDt0IwvOn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyHW4k%2FbtsQ18tBnqA%2FYp68M8nxOpHCXDt0IwvOn0%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; alt=&quot;Lettuce 분산락의 오해와 진실&quot; loading=&quot;lazy&quot; width=&quot;952&quot; height=&quot;902&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글인 &lt;a href=&quot;https://myvelop.tistory.com/263&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;좋아요 기능으로 알아보는 비관적 락&quot;&lt;/a&gt;에서 이어지는 글이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Lettuce는 SpinLock만 지원한다&quot;는 잘못된 정보를 바로잡고, Spring RedisLockRegistry의 PubSub Lock 설정으로 Redisson 없이도 효율적인 분산 락을 구현하는 방법을 소개한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Lettuce에 대한 오해, 당신도 믿고 있는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글에 &quot;Redis 분산 락&quot;를 검색하면 수십 개의 블로그가 같은 내용을 반복한다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;Lettuce는 SpinLock 방식이라 Redis에 부하를 준다&quot;&lt;/li&gt;
&lt;li&gt;&quot;그래서 Redisson을 써야 한다&quot;&lt;/li&gt;
&lt;li&gt;&quot;Lettuce로는 PubSub 방식을 구현할 수 없다&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;이런 주장들이 마치 정설처럼 반복되고 있다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7GeKO/btsQR8tfTdE/3hggvBB1I9sqfXIKgjZ40K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7GeKO/btsQR8tfTdE/3hggvBB1I9sqfXIKgjZ40K/img.png&quot; data-alt=&quot;lettuce가 스핀락을 사용한다는 오해1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7GeKO/btsQR8tfTdE/3hggvBB1I9sqfXIKgjZ40K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7GeKO%2FbtsQR8tfTdE%2F3hggvBB1I9sqfXIKgjZ40K%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; alt=&quot;오해1&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;61&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;lettuce가 스핀락을 사용한다는 오해1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0pRvy/btsQR19kHNu/BNKishOyFjHPjOcv3AP1fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0pRvy/btsQR19kHNu/BNKishOyFjHPjOcv3AP1fK/img.png&quot; data-alt=&quot;lettuce가 스핀락을 사용한다는 오해2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0pRvy/btsQR19kHNu/BNKishOyFjHPjOcv3AP1fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0pRvy%2FbtsQR19kHNu%2FBNKishOyFjHPjOcv3AP1fK%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; alt=&quot;오해2&quot; loading=&quot;lazy&quot; width=&quot;786&quot; height=&quot;72&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;72&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;lettuce가 스핀락을 사용한다는 오해2&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;그리고 이러한 논리를 바탕으로 Redisson 구현체를 추천한다.&amp;nbsp;&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCOoIo/btsQ3jOnlCM/NAYLVBHAP95rMMkI0k8on0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCOoIo/btsQ3jOnlCM/NAYLVBHAP95rMMkI0k8on0/img.png&quot; data-alt=&quot;Lettuce가 spin lock이라는 근거를 통해 Redisson을 사용을 추천하는 글&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCOoIo/btsQ3jOnlCM/NAYLVBHAP95rMMkI0k8on0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCOoIo%2FbtsQ3jOnlCM%2FNAYLVBHAP95rMMkI0k8on0%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; alt=&quot;redisson 추천 글1&quot; loading=&quot;lazy&quot; width=&quot;1522&quot; height=&quot;178&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Lettuce가 spin lock이라는 근거를 통해 Redisson을 사용을 추천하는 글&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHaWKc/btsQ2dHZX4t/s9rY50wg0N3SP5ZY8X03CK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHaWKc/btsQ2dHZX4t/s9rY50wg0N3SP5ZY8X03CK/img.png&quot; data-alt=&quot;Lettuce가 spin lock이라 CPU 사용량이 많기 때문에 Redisson을 사용을 추천하는 글&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHaWKc/btsQ2dHZX4t/s9rY50wg0N3SP5ZY8X03CK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHaWKc%2FbtsQ2dHZX4t%2Fs9rY50wg0N3SP5ZY8X03CK%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; alt=&quot;redisson 추천 글2&quot; loading=&quot;lazy&quot; width=&quot;1546&quot; height=&quot;142&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Lettuce가 spin lock이라 CPU 사용량이 많기 때문에 Redisson을 사용을 추천하는 글&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만, 이는 전부 잘못된 정보이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;첫 번째 오해! Lettuce는 무조건 Spin Lock 방식이다?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;Lettuce는 spin lock 방식으로 구현되어 Redis에 부하를 준다&quot;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 블로그에서 이렇게 단정하지만, 이는 Lettuce 자체의 특성이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발자가 While 문으로 반복해서 락 획득을 시도하도록 구현했기 때문에&lt;/b&gt; Spin Lock이 된 것일 뿐이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce를 사용하더라도 &lt;b&gt;RedisMessageListenerContainer를 통해 특정 채널을 구독하는 방식&lt;/b&gt;으로 구현하면 &lt;b&gt;Pub/Sub 기반 락을 충분히 구현할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;Lettuce는 단지 Redis 클라이언트 라이브러리일 뿐, 락 구현 방식을 강제하지 않는다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;두 번째 오해! Lettuce에서 분산 락을 직접 구현해야 한다.&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;Lettuce는 분산 락 구현을 제공하지 않으니 Redisson을 쓰는 게 낫지 않을까?&quot;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람들이 이를 근거로 Redisson 사용을 제안한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce 자체가 분산 락 기능을 제공하지 않는 것은 사실이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring Integration이 제공하는 RedisLockRegistry를 활용하면 직접 구현하지 않고도 손쉽게 분산 락을 사용할 수 있다. Lettuce 기반으로도 충분히 프로덕션 레벨의 분산 락 구현이 가능하다는 의미다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;이 잘못된 정보는 어떻게 퍼졌을까?&lt;/h3&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;누군가 Lettuce로 While 루프 기반 락을 구현한 예제 코드 작성&lt;/li&gt;
&lt;li&gt;이를 &quot;Lettuce = Spin Lock&quot;이라고 잘못 일반화&lt;/li&gt;
&lt;li&gt;후속 글들이 검증 없이 이 내용을 인용하며 확산&lt;/li&gt;
&lt;li&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;RedisLockRegistry 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring 공식 문서를 읽다가 LockRegistry라는 Spring의 락을 통합하는 인터페이스&lt;/b&gt;를 알게 되었고, 덕분에 RedisLockRegistry라는 구현체를 알게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Lua 스크립트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lua 스크립트를 통해 Redis 분산 락을 구현했다.&lt;/p&gt;
&lt;pre id=&quot;code_1759120632999&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private abstract class RedisLock implements Lock {

    private static final String OBTAIN_LOCK_SCRIPT = &quot;&quot;&quot;
            local lockClientId = redis.call('GET', KEYS[1])
            if lockClientId == ARGV[1] then
                redis.call('PEXPIRE', KEYS[1], ARGV[2])
                return true
            elseif not lockClientId then
                redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
                return true
            end
            return false
            &quot;&quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lua 스크립트 통해 다음과 같은 효과를 얻을 수 있다.&amp;nbsp;&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;GET과 SET 사이의 Race Condition 원천 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SpinLock과 PubSubLock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisLockRegistry는 SpinLock 방식과 PubSubLock 방식을 동시에 제공한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759477484336&quot; class=&quot;haxe&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Function&amp;lt;String, RedisLock&amp;gt; getRedisLockConstructor(RedisLockType redisLockType) {
    return switch (redisLockType) {
        case SPIN_LOCK -&amp;gt; RedisSpinLock::new;
        case PUB_SUB_LOCK -&amp;gt; RedisPubSubLock::new;
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpinLock은 위에서 얘기한 것과 같이 반복문을 통해 락 획득을 지속적으로 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisLockRegistry의 RedisSpinLock 구현체도 while 문을 통해 락을 시도한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759477316120&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (!obtainLock()) {
    Thread.sleep(RedisLockRegistry.this.idleBetweenTries.toMillis()); //NOSONAR
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpinLock 방식은 CPU 자원을 낭비하게 되는 문제가 있다.&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;반면 RedisPubSubLock 구현체는 Redis Pub/Sub을 활용해 CPU 자원을 낭비하는 문제를 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구현체는 Lock을 획득하지 못했을 때, lockKey에 대해 구독한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759477559190&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Future&amp;lt;String&amp;gt; future =
    RedisLockRegistry.this.unlockNotifyMessageListener.subscribeLock(this.lockKey);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 락을 획득한 스레드가 락을 해체하면 아래 Lua 스크립트를 호출하고, 해당 채널에 이벤트를 발행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759477460766&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final class RedisPubSubLock extends RedisLock {

    private static final String UNLINK_UNLOCK_SCRIPT = &quot;&quot;&quot;
            local lockClientId = redis.call('GET', KEYS[1])
            if (lockClientId == ARGV[1] and redis.call('UNLINK', KEYS[1]) == 1) then
                redis.call('PUBLISH', ARGV[2], KEYS[1])
                return true
            end
            return false
            &quot;&quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisLockRegistry를 PubSubLock 모드로 설정하면 RedisMessageListenerContainer를 실행하고, lockKey에 대한 topic을 구독하고 있다. 이를 구독하고 있던 애플리케이션은 이벤트가 발행되면 락을 획득하여 임계 영역으로 진입하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1759478572001&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {

  private volatile RedisMessageListenerContainer redisMessageListenerContainer; 
  
  private void setupUnlockMessageListener(RedisConnectionFactory connectionFactory) {
		Assert.isNull(RedisLockRegistry.this.redisMessageListenerContainer,
				&quot;'redisMessageListenerContainer' must not have been re-initialized.&quot;);
		Assert.isNull(RedisLockRegistry.this.unlockNotifyMessageListener,
				&quot;'unlockNotifyMessageListener' must not have been re-initialized.&quot;);
		RedisLockRegistry.this.redisMessageListenerContainer = new RedisMessageListenerContainer();
		RedisLockRegistry.this.unlockNotifyMessageListener = new RedisPubSubLock.RedisUnLockNotifyMessageListener();
		final Topic topic = new ChannelTopic(this.unLockChannelKey);
		this.redisMessageListenerContainer.setConnectionFactory(connectionFactory);
		this.redisMessageListenerContainer.setTaskExecutor(this.executor);
		this.redisMessageListenerContainer.setSubscriptionExecutor(this.executor);
		this.redisMessageListenerContainer.addMessageListener(this.unlockNotifyMessageListener, topic);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisPubSubLock의 동작을 다이어그램으로 정리하면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;1364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mnoho/btsQ0tLxz7e/8Ntrrk9uAU6RDoc0qHN4n0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mnoho/btsQ0tLxz7e/8Ntrrk9uAU6RDoc0qHN4n0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mnoho/btsQ0tLxz7e/8Ntrrk9uAU6RDoc0qHN4n0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmnoho%2FbtsQ0tLxz7e%2F8Ntrrk9uAU6RDoc0qHN4n0%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; alt=&quot;RedisPubSubLock 다이어그램&quot; loading=&quot;lazy&quot; width=&quot;1268&quot; height=&quot;1364&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;1364&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;RedisLockRegistry 적용해보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisLockRegistry를 사용하기 위해선 아래 의존성을 추가해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759120633000&quot; class=&quot;isbl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;dependencies {
    implementation(&quot;org.springframework.integration:spring-integration-redis&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 RedisLockRegistry를 생성하고, 어떤 락을 설정한 뒤 빈으로 등록하면 사용 준비 완료다.&lt;/p&gt;
&lt;pre id=&quot;code_1759120633000&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class RedisLockConfig {

  public static final String REDIS_LOCK_REGISTRY = &quot;redisLockRegistry&quot;;
  private static final Duration DEFAULT_LOCK_EXPIRE = Duration.ofSeconds(10);

  @Primary
  @Bean
  public LockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
    RedisLockRegistry redisLockRegistry =
        new RedisLockRegistry(
            redisConnectionFactory, REDIS_LOCK_REGISTRY, DEFAULT_LOCK_EXPIRE.toMillis());
    redisLockRegistry.setRedisLockType(RedisLockType.PUB_SUB_LOCK);
    return redisLockRegistry;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;LockRegistry 주입받기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 LockRegistry를 주입받아 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 비관적 락을 통해 처리했던 로직을 분산 락을 이용해 처리해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1759120633001&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ReviewLikeService {

  // 기타 의존성들
  // ....

  // RedisLockRegistry
  private final LockRegistry RedisLockRegistry;

  @Transactional
  public ReviewLikeResult likeReview(final ReviewLikeCommand command) {
    final String key =
        String.format(
            &quot;review-like:reviewId:%d:memberId:%d&quot;, command.reviewId(), command.memberId());
    final Lock lock = RedisLockRegistry.obtain(key);
    if (lock.tryLock(5000, TimeUnit.MILLISECONDS)) {
      try {
        // 비즈니스 로직
      } finally {
        lock.unlock();
      }
    }
    return ReviewLikeResult.empty();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;결론: 개발자로서의 자세&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;Lettuce는 SpinLock만 지원한다&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 한 문장이 검증 없이 수십 개의 블로그에서 반복되고 있다는 사실에 놀랐다.&lt;/b&gt; 심지어 주변 동료들도 &quot;Lettuce가 Spin Lock 방식이라서 Redisson을 선택했다&quot;고 말하는 것을 듣고 충격을 받았다.&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;분산 락 하나만 필요한데, Redisson의 수십 가지 기능을 모두 가져올 필요가 있을까? 이 글을 읽었다면, &lt;b&gt;Spring Integration이 제공하는 가벼운 솔루션으로 충분히 해결 가능하다&lt;/b&gt;는 사실을 알게 되었을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; 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;블로그 글만이 아니라 &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;/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;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;기술 선택의 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redisson이 나쁘다는 게 아니다. Redisson은 분명 강력하고 편리한 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 &quot;모두가 그렇게 하니까&quot;가 기술 선택의 이유가 되어서는 안 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제로 필요한 기능&lt;/b&gt;이 무엇인지, &lt;b&gt;트레이드오프&lt;/b&gt;는 무엇인지, &lt;b&gt;우리 팀의 상황&lt;/b&gt;에 맞는 선택인지를 고민해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 분산 락이 필요하다면 Spring Integration의 RedisLockRegistry로 충분할 수 있다. 복잡한 분산 자료구조와 고급 기능이 필요하다면 그때 Redisson을 선택해도 늦지 않다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;%EB%8F%99%EC%8B%9C%EC%84%B1%20%EC%B2%98%EB%A6%AC%20%EC%8B%9C%EB%A6%AC%EC%A6%88-1&quot; 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://myvelop.tistory.com/262&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;처음부터&amp;nbsp;다시&amp;nbsp;배우는&amp;nbsp;Java&amp;nbsp;동시성&amp;nbsp;제어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/258&quot;&gt;UPDATE 한 줄로 끝내는 동시성 처리&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/263&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;좋아요 기능으로 알아보는 넥스트 키 락&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Lettuce&amp;nbsp;분산&amp;nbsp;락의&amp;nbsp;오해와&amp;nbsp;진실&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/264&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AOP로 동시성 처리 코드 분리하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Redis</category>
      <category>java</category>
      <category>lettuce</category>
      <category>LockRegistry</category>
      <category>redis</category>
      <category>RedisLockRegistry</category>
      <category>Spring</category>
      <category>분산 락</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/260</guid>
      <comments>https://myvelop.tistory.com/260#entry260comment</comments>
      <pubDate>Mon, 29 Sep 2025 13:41:16 +0900</pubDate>
    </item>
    <item>
      <title>좋아요 기능으로 알아보는 비관적 락</title>
      <link>https://myvelop.tistory.com/263</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 좋아요 기능을 구현하면서 발생하는 동시성 문제를 DB 락으로 해결하는 과정을 다룬다. 비관적 락의 동작 원리와 함께, 넥스트 키 락(Next-Key Lock)으로 인한 성능 저하 문제까지 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋아요 기능 요구사항 정리&lt;/h2&gt;
&lt;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;/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;1) 좋아요 추가&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;한 사용자는 하나의 리뷰에 한 번만 좋아요 가능 (중복 방지)&lt;/li&gt;
&lt;li&gt;동시다발적인 요청도 안전하게 처리해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 좋아요 취소&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;물리적 삭제가 아닌 논리적 삭제(Soft Delete) 방식 사용&lt;/li&gt;
&lt;li&gt;취소 후 다시 좋아요를 누를 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 좋아요 조회&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;좋아요를 누른 상태: 활성화 표시&lt;/li&gt;
&lt;li&gt;좋아요를 취소한 상태: 비활성화 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엔티티 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰에 좋아요를 눌렀는지 여부를 판단하기 위해 ReviewLike 엔티티를 설계해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1758804798653&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class ReviewLike {

  @Id
  private Long id;
  
  private Long reviewId;
  private Long memberId;
  
  private LocalDateTime deletedAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋아요를 추가할 때는 ReviewLike를 저장하고, 취소할 때는 deletedAt에 시간을 기록하는 방식으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 처리를 하지 않은 좋아요 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 동시성 처리를 하지 않은 코드를 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1758805129634&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ReviewLikeService {
    private final ReviewLikeRepository repository;
    
    public void addLike(Long reviewId, Long memberId) {
        // 이미 좋아요 했는지 체크
        Optional&amp;lt;ReviewLike&amp;gt; existing = repository.findByReviewIdAndMemberId(reviewId, memberId);
        
        if (existing.isEmpty()) {
            ReviewLike newLike = ReviewLike.builder()
                .reviewId(reviewId)
                .memberId(memberId)
                .build();
            repository.save(newLike);
        }
    }
}&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;하지만 &quot;따닥...&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 더블클릭으로 동시에 두 번의 요청을 보낸다면 어떻게 될까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;749&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DhcJp/btsQQbCK2jB/qkdkkApEHaahHIrAKQcxX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DhcJp/btsQQbCK2jB/qkdkkApEHaahHIrAKQcxX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DhcJp/btsQQbCK2jB/qkdkkApEHaahHIrAKQcxX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDhcJp%2FbtsQQbCK2jB%2FqkdkkApEHaahHIrAKQcxX0%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; alt=&quot;동시성 제어를 하지 않으면 중복 요청이 전부 적용된다.&quot; loading=&quot;lazy&quot; width=&quot;651&quot; height=&quot;749&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;749&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Race Condition이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 스레드가 동시에 조회하면 둘 다 &quot;없음&quot;을 받고, 둘 다 저장을 진행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좋아요를 취소해도 하나가 남아 있는 문제&lt;/li&gt;
&lt;li&gt;좋아요 카운트가 2번 늘어나는 문제&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;동시성 제어 방법 고민하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템 환경에서는 Java API가 제공하는 동시성 제어만으로는 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 서버에서 동시에 요청이 들어올 수 있기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Unique Key 제약 조건은?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;(member_id, review_id) 조합으로 Unique Key를 걸면 되는 거 아니야?&quot; 라고 생각할 수 있다. 하지만 Soft Delete를 사용하기 때문에 같은 조합이 여러 번 들어갈 수 있어 유일성을 보장할 수 없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;X-Lock으로 해결해볼 수 있을까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 &lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://myvelop.tistory.com/258&quot;&gt;&quot;UPDATE 한 줄로 끝내는 동시성 문제&quot;&lt;/a&gt; 글에서는 명시적인 읽기 락 없이 동시성을 제어하는 방법을 다뤘다. 하지만 이번 요구사항에서는 &lt;b&gt;존재하지 않는 데이터를 새로 만들어야 하기 때문에&lt;/b&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;member_id와 review_id를 키 값으로 Lock을 거는 방법을 고려해볼 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적 락 (Pessimistic Lock)&lt;/li&gt;
&lt;li&gt;Named Lock&lt;/li&gt;
&lt;li&gt;분산 락 (Distributed Lock)&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비관적 락&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock을 거는 방식이다. &lt;b&gt;&quot;동시에 같은 데이터를 수정할 것이다&quot;라고 비관적으로 가정하고 미리 락을 거는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL로 표현하면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1759054676346&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 트랜잭션 시작
BEGIN;

-- WHERE 조건에 따라 범위 락을 건다.
SELECT * FROM review_like 
WHERE review_id = :review_id AND member_id = :member_id
FOR UPDATE;

-- 결과가 없으면 INSERT
INSERT INTO review_like (review_id, member_id) 
VALUES (1, 100);

COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 Row Lock과 Gap Lock을 조합한 &lt;b&gt;Next-Key Lock&lt;/b&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;&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;각 케이스별로 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case01. 인덱스가 없는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 없으면 MySQL은 데이터가 어디에 있는지, 또는 어디에 들어가야 할지 전혀 예측할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 &lt;b&gt;테이블 전체를 스캔(Full Table Scan)&lt;/b&gt; 하면서 모든 레코드에 락을 걸어버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 리뷰와 관련 없는 다른 모든 요청이 모두 대기하게 되고, 결과적으로 엄청난 성능 저하가 발생하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HPwV4/btsQRBJvTIG/o3YdIvwTQ7VQit0YYlVHa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HPwV4/btsQRBJvTIG/o3YdIvwTQ7VQit0YYlVHa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HPwV4/btsQRBJvTIG/o3YdIvwTQ7VQit0YYlVHa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHPwV4%2FbtsQRBJvTIG%2Fo3YdIvwTQ7VQit0YYlVHa1%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; alt=&quot;인덱스가 없을 때의 Next Key Lock&quot; loading=&quot;lazy&quot; width=&quot;1167&quot; height=&quot;784&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 Lock을 사용한다면 반드시 인덱스를 고려해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case02. (review_id) 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;review_id에만 인덱스를 건 경우를 생각해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GWqzB/btsQUkF0lD6/ANLYRiFPgjod05rOj2cdxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GWqzB/btsQUkF0lD6/ANLYRiFPgjod05rOj2cdxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GWqzB/btsQUkF0lD6/ANLYRiFPgjod05rOj2cdxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGWqzB%2FbtsQUkF0lD6%2FANLYRiFPgjod05rOj2cdxk%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;982&quot; height=&quot;763&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;763&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;user_100이 review_id=1 조건으로 SELECT FOR UPDATE를 실행했다고 가정해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZuLDr/btsQ2WlwfwH/aK8R20XYfnFwhL8whbY0QK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZuLDr/btsQ2WlwfwH/aK8R20XYfnFwhL8whbY0QK/img.png&quot; data-alt=&quot;(review_id) 인덱스 샘플 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZuLDr/btsQ2WlwfwH/aK8R20XYfnFwhL8whbY0QK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZuLDr%2FbtsQ2WlwfwH%2FaK8R20XYfnFwhL8whbY0QK%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; alt=&quot;(review_id) index 샘플 데이터&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;596&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(review_id) 인덱스 샘플 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 review_id=1 인덱스 범위에 Next-Key Lock을 건다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;932&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bveQwa/btsQ3yxMKme/kztOlJMcjXEUpGVz1H70ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bveQwa/btsQ3yxMKme/kztOlJMcjXEUpGVz1H70ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bveQwa/btsQ3yxMKme/kztOlJMcjXEUpGVz1H70ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbveQwa%2FbtsQ3yxMKme%2FkztOlJMcjXEUpGVz1H70ok%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; alt=&quot;(review_id) index Locking&quot; loading=&quot;lazy&quot; width=&quot;1040&quot; height=&quot;932&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;932&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;이는 아래와 같은 결과를 초래하게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 명의 유저가 review_id=1로 동시에 접근하면 모두 Gap Lock을 기다려야 함&lt;/li&gt;
&lt;li&gt;인기 리뷰일수록 병목 현상 심화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Case03. (member_id) 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member_id를 기준으로 Next-Key Lock이 잡힌다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VMLKM/btsQSIVpJmC/AzlyfyYPPRwNnXOkwKCSl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VMLKM/btsQSIVpJmC/AzlyfyYPPRwNnXOkwKCSl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VMLKM/btsQSIVpJmC/AzlyfyYPPRwNnXOkwKCSl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVMLKM%2FbtsQSIVpJmC%2FAzlyfyYPPRwNnXOkwKCSl0%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; alt=&quot;member_id에만 인덱스를 건 경우&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;776&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;710&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH5PCH/btsQ2O8ZGR5/ofrVDwHlkscrDY7Kcm3751/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH5PCH/btsQ2O8ZGR5/ofrVDwHlkscrDY7Kcm3751/img.png&quot; data-alt=&quot;(member_id) 인덱스 샘플 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH5PCH/btsQ2O8ZGR5/ofrVDwHlkscrDY7Kcm3751/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH5PCH%2FbtsQ2O8ZGR5%2FofrVDwHlkscrDY7Kcm3751%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; alt=&quot;(member_id) index 데이터 샘플&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;710&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;710&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(member_id) 인덱스 샘플 데이터&lt;/figcaption&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;하나의 멤버가 여러 리뷰에 대해 좋아요를 동시에 누르면 병목 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUME1Y/btsQ2ioWsQ0/S3dYiGasXBOP9kTI74m4s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUME1Y/btsQ2ioWsQ0/S3dYiGasXBOP9kTI74m4s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUME1Y/btsQ2ioWsQ0/S3dYiGasXBOP9kTI74m4s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUME1Y%2FbtsQ2ioWsQ0%2FS3dYiGasXBOP9kTI74m4s1%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; alt=&quot;(member_id) index Locking&quot; loading=&quot;lazy&quot; width=&quot;906&quot; height=&quot;912&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;912&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;h3 data-ke-size=&quot;size23&quot;&gt;Case04. (review_id, member_id) 인덱스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 작은 락 범위를 가질 가능성이 높은 방법이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cChbOi/btsQSyFdA03/yrg4sy2KZz8DINSpSk57i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cChbOi/btsQSyFdA03/yrg4sy2KZz8DINSpSk57i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cChbOi/btsQSyFdA03/yrg4sy2KZz8DINSpSk57i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcChbOi%2FbtsQSyFdA03%2Fyrg4sy2KZz8DINSpSk57i1%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; alt=&quot;(review_id, member_id) INDEX 시나리오&quot; loading=&quot;lazy&quot; width=&quot;1164&quot; height=&quot;614&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;614&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&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzjpQ/btsQ2yE86ts/UaV2gbBpH7ouEeEdfGtnPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzjpQ/btsQ2yE86ts/UaV2gbBpH7ouEeEdfGtnPk/img.png&quot; data-alt=&quot;(review_id, member_id) 인덱스 샘플 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzjpQ/btsQ2yE86ts/UaV2gbBpH7ouEeEdfGtnPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzjpQ%2FbtsQ2yE86ts%2FUaV2gbBpH7ouEeEdfGtnPk%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; alt=&quot;(review_id, member_id) index 데이터 샘플&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;690&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(review_id, member_id) 인덱스 샘플 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;review_id=1, member_id=150 조건으로 조회 시, member_id가 93~234 범위에 Gap Lock이 걸린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZFILm/btsQ3lrQnyt/9W3G0wKeMkCfAkxpUb0z4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZFILm/btsQ3lrQnyt/9W3G0wKeMkCfAkxpUb0z4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZFILm/btsQ3lrQnyt/9W3G0wKeMkCfAkxpUb0z4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZFILm%2FbtsQ3lrQnyt%2F9W3G0wKeMkCfAkxpUb0z4k%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; alt=&quot;(review_id, member_id) index Locking&quot; loading=&quot;lazy&quot; width=&quot;1186&quot; height=&quot;798&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&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;가장 좁은 범위지만 여전히 주변 영역에 Gap Lock이 걸림&lt;/li&gt;
&lt;li&gt;다른 회원의 좋아요 요청에도 영향을 미칠 수밖에 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비관적 락의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락을 사용해 동시성 문제는 해결했지만, 처리량이 급감할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기 리뷰 하나에 100명이 동시에 좋아요를 누르면 지연 시간이 크게 증가하게 된다.&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;DB 부하 증가&lt;/b&gt; - 모든 동시성 제어를 DB가 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;넥스트 키 락의 넓은 범위&lt;/b&gt; - 필요 이상의 영역까지 락을 걸게 됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 커넥션 점유&lt;/b&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;이런 단점을 고려해봤을 때, 비관적 락이 적합한 경우는 아래와 같다.&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;/li&gt;
&lt;li&gt;구현 복잡도를 낮추고 싶을 때 (SELECT FOR UPDATE로 간단한 구현 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더 나은 해결책은?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;어떤 락이 최고다&quot;라는 정답은 없다.&lt;/b&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;&amp;nbsp;&lt;/p&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;&lt;b&gt;Named Lock&lt;/b&gt; (MySQL)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 락&lt;/b&gt; (Redis, Zookeeper 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 분산 락에 대해 알아보고 싶다면,&amp;nbsp;&lt;a href=&quot;https://myvelop.tistory.com/260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;Lettuce 분산 락의 오해와 진실&quot;&lt;/a&gt; 포스트를 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동시성 처리 시리즈&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/262&quot;&gt;처음부터&amp;nbsp;다시&amp;nbsp;배우는&amp;nbsp;Java&amp;nbsp;동시성&amp;nbsp;제어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/258&quot;&gt;UPDATE 한 줄로 끝내는 동시성 처리&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;좋아요 기능으로 알아보는 넥스트 키 락&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lettuce&amp;nbsp;분산&amp;nbsp;락의&amp;nbsp;오해와&amp;nbsp;진실&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/264&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AOP로 동시성 처리 코드 분리하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>java</category>
      <category>redis</category>
      <category>RedisLockRegistry</category>
      <category>Spring</category>
      <category>동시성 제어</category>
      <category>비관적 락</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/263</guid>
      <comments>https://myvelop.tistory.com/263#entry263comment</comments>
      <pubDate>Sun, 28 Sep 2025 20:36:03 +0900</pubDate>
    </item>
    <item>
      <title>처음부터 다시 배우는 Java 동시성 제어</title>
      <link>https://myvelop.tistory.com/262</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 synchronized 키워드의 JVM 모니터 락이 실제로 어떻게 동작하는지 살펴보고, ReentrantLock이 제공하는 동시성 처리 기능을 알아본 뒤, 동시성 프로그래밍을 할 때 발생할 수 있는 생산자-소비자 문제를 해결해본다. 그리고 사용자별 키 값을 활용한 락 분리 전략으로 성능을 개선하는 방법을 다룰 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;synchronized만으로 충분할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'synchronized를 쓰면 동시성 문제가 해결된다'라고만 이해하고 있을 수도 있다. 실제로 동시성 문제를 해결하기 위해 단순히 코드 블록에 synchronized만 붙이면 문제는 해결되지만 성능이 급격히 떨어지는 경우를 자주 볼 수 있다.&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;예를 들어, 모든 사용자의 포인트 처리가 하나의 공유된 락으로 처리하면, 사용자 A의 포인트 적립이 사용자 B의 포인트 적립을 대기하게 만드는 불필요한 병목이 발생한다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JVM 모니터 락의 비밀&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;간단한 계좌 차감 로직 예시 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계좌에서 돈을 인출하는 메서드에 synchronized를 붙여 동시성을 제어하는 코드 예시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronized 키워드가 붙는 순간, JVM 내부에서는 꽤 복잡한 일들이 벌어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1758435282708&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public synchronized boolean withdraw(long amount) {
        if (balance &amp;lt; amount) {
            return false;
        }
        Thread.sleep(1000); // 외부 시스템 연동 시뮬레이션
        balance -= amount;
        return true;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모니터 락(Monitor Lock)이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 모든 객체는 모니터(monitor)라는 내재 락을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronized 키워드를 만나면, 스레드는 해당 객체의 모니터 락을 획득해야만 &lt;b&gt;임계 영역&lt;/b&gt;에 진입할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;임계 영역(Critical Section)이란?&lt;/b&gt;&lt;br /&gt;여러 스레드(작업 단위)가 동시에 접근해서는 안 되는 공유 자원(데이터나 변수)에 접근하는 코드 영역 을 말한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드에서는 메소드 레벨에 synchronized가 걸렸기 때문에 this의 모니터 락을 획득하는 과정을 거친다.&lt;/p&gt;
&lt;pre id=&quot;code_1758437051384&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    public synchronized boolean withdraw(long amount) {
        if (balance &amp;lt; amount) {
            return false;
        }
        Thread.sleep(1000); // 외부 시스템 연동 시뮬레이션
        balance -= amount;
        return true;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 아래와 같이 sychronized에서 사용할 객체를 명시하면 해당 객체의 모니터 락을 획득하도록 만들 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1758437186852&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public boolean logic(String key) {
        synchronized (key) {
            // ...
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락 획득 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 스레드(t1, t2)가 동시에 계좌에서 출금을 시도하는 상황을 살펴보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 초기 상태&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BankAccount 객체는 모니터 락을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;t1과 t2 두 스레드가 동시에 withdraw() 메서드를 호출하려고 대기 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계좌의 잔액은 1000원이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1FX3S/btsQGGRUMxD/7IrMQjN3jUb0f6smHkLaZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1FX3S/btsQGGRUMxD/7IrMQjN3jUb0f6smHkLaZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1FX3S/btsQGGRUMxD/7IrMQjN3jUb0f6smHkLaZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1FX3S%2FbtsQGGRUMxD%2F7IrMQjN3jUb0f6smHkLaZ0%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; alt=&quot;락 획득 과정1&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;569&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;569&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;h4 data-ke-size=&quot;size20&quot;&gt;2. t1이 락을 획득&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;t1이 먼저 모니터 락을 획득했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;b&gt;t1은 synchronized 블록 내부로 진입&lt;/b&gt;할 수 있다. (이것을 &quot;임계 영역에 진입&quot;한다고 표현)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6eckr/btsQJ8S2n0b/56qGrfELbkdojKhi6HlMk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6eckr/btsQJ8S2n0b/56qGrfELbkdojKhi6HlMk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6eckr/btsQJ8S2n0b/56qGrfELbkdojKhi6HlMk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6eckr%2FbtsQJ8S2n0b%2F56qGrfELbkdojKhi6HlMk0%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; alt=&quot;락 획득 과정2&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;582&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. t2는 BLOCKED 상태로 대기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;여기서 핵심은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;t2는 락을 획득하지 못해 BLOCKED 상태가 된다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;자바 스레드의 상태값 종류&lt;/b&gt;&lt;br /&gt;총 6가지의 상태가 있다. &lt;br /&gt;NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;t2가 락을 획득하려 시도하지만, 이미 t1이 보유 중이므로 BLOCKED 상태로 대기한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 t2는 CPU를 전혀 사용하지 않고 대기만 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ3I6d/btsQH0Iw8wF/i10z9dRo45iYFB6cD6BnSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ3I6d/btsQH0Iw8wF/i10z9dRo45iYFB6cD6BnSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ3I6d/btsQH0Iw8wF/i10z9dRo45iYFB6cD6BnSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ3I6d%2FbtsQH0Iw8wF%2Fi10z9dRo45iYFB6cD6BnSk%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; alt=&quot;락 획득 과정3&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;625&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. t1 작업 완료 및 락 반납&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;t1은 800원을 차감하고 synchronized 블록을 벗어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잔액은 200원이 되고, 락이 해제된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvQPJx/btsQJ5hIHI3/IuuNlbhBokK1mjEYv0k9p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvQPJx/btsQJ5hIHI3/IuuNlbhBokK1mjEYv0k9p1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvQPJx/btsQJ5hIHI3/IuuNlbhBokK1mjEYv0k9p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvQPJx%2FbtsQJ5hIHI3%2FIuuNlbhBokK1mjEYv0k9p1%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; alt=&quot;락 획득 과정4&quot; loading=&quot;lazy&quot; width=&quot;1446&quot; height=&quot;621&quot; data-origin-width=&quot;1446&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. t2 락 획득 및 실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;t1이 락을 반납하면 BLOCKED 상태였던 t2가 RUNNABLE 상태로 전환되며 락을 획득한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 t2가 synchronized 블록 안의 코드를 실행할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lJ54y/btsQGQmqoJc/0VUf88JuVCuhfrQVQMxyC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lJ54y/btsQGQmqoJc/0VUf88JuVCuhfrQVQMxyC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lJ54y/btsQGQmqoJc/0VUf88JuVCuhfrQVQMxyC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlJ54y%2FbtsQGQmqoJc%2F0VUf88JuVCuhfrQVQMxyC0%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; alt=&quot;락 획득 과정5&quot; loading=&quot;lazy&quot; width=&quot;1495&quot; height=&quot;668&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;668&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;h3 data-ke-size=&quot;size23&quot;&gt;Virtual Thread와 Thread Pinning&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 21에서 Virtual Thread가 도입되면서 synchronized 사용 시 주의할 점이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronized 블록 내에서 Virtual Thread는 캐리어 스레드에 &quot;고정(pinned)&quot;된다.&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;가상 스레드는 I/O 작업을 만났을 때, Unmount를 통해 스레드가 다른 작업을 할 수 있게 해주는 것이 장점인데,&amp;nbsp;JVM이 synchronized의 잠금(Monitor Lock) 소유자를 플랫폼 스레드로 기록하기 때문에 가상 스레드가 그 안에 갇혀 버리는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Virtual Thread의 주요 이점인 스레드 전환 효율성을 무효화시킨다.&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;다행히 JDK 24에서는 이 문제가 해결되어, synchronized 블록에서도 Virtual Thread가 자유롭게 스케줄링될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sychronized의 한계&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 락 획득 시도를 중단할 수 없다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 사용자가 동시성 처리와 관련된 버튼을 눌렀다가 로딩이 길어져 페이지를 떠났어도, 서버의 스레드는 계속 synchronized 블록 앞에서 대기한다. 타임아웃 설정도, 중간에 포기하는 것도 불가능하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 락 획득 순서를 보장하지 않는다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 상품 구매처럼 순서가 중요한 경우, 먼저 온 사용자가 계속 밀리는 불공정한 상황이 발생할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 락 상태를 확인할 수 없다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Lock이 사용 중인지 확인하고 싶어도, synchronized로는 확인할 방법이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 임계 영역으로 진입해보는 수밖에 없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 여러 조건 변수를 사용할 수 없다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;synchronized에선느 wait()/notify()만 다루고 이 메소드들은 구분 없이 모든 대기 스레드를 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요하게 깨어났다가 다시 잠드는 스레드가 많아지면 성능에 영향을 미칠 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 Thundering Herd Problem라고 부른다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모니터 락을 넘어서: ReentrantLock&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 본 synchronized의 한계들을 해결할 수 있는 대안이 바로 ReentrantLock이다. java.util.concurrent 패키지에서 제공하는 이 락은 synchronized와 같은 상호 배제를 보장하면서도 훨씬 더 유연한 제어가 가능하다.&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;위의 차감 예시를 ReentrantLock으로 다시 작성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1758510546110&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private final ReentrantLock lock = new ReentrantLock();

    public boolean withdraw(long amount) {
        lock.lock();
        try {
            if (balance &amp;lt; amount) {
                return false;
            }
            Thread.sleep(1000); // 외부 시스템 연동 시뮬레이션
            balance -= amount;
            return true;
        } finally {
            lock.unlock();
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AQS(AbstractQueuedSynchronizer)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReentrantLock이 synchronized보다 유연한 이유는 AQS라는 프레임워크를 기반으로 동작하기 때문이다. JVM 모니터 락이 네이티브 레벨에서 동작한다면, AQS는 순수 Java로 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;state는 락 획득 숫자를 의미한다. &lt;b&gt;ReentrantLock은 단어의 뜻 그대로 재진입이 가능한 락&lt;/b&gt;인데, &lt;b&gt;같은 스레드가 중첩해서 락을 획득할 수 있다.&lt;/b&gt; 이 기능을 통해 &lt;b&gt;스스로를 블로킹하는 모순적인 상황을 방지할 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758530393362&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    /**
     * Head of the wait queue, lazily initialized.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue. After initialization, modified only via casTail.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;
    
    abstract static class Node {
        volatile Node prev;       // initially attached via casTail
        volatile Node next;       // visibly nonnull when signallable
        Thread waiter;            // visibly nonnull when enqueued
        volatile int status;      // written by owner, atomic bit ops by others
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AQS가 락을 획득하고 해제하는 과정은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;133&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/carZoe/dJMb8VsWtbj/DBWHIrm3v3lIcd5kANJENk/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/carZoe/dJMb8VsWtbj/DBWHIrm3v3lIcd5kANJENk/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/carZoe/dJMb8VsWtbj/DBWHIrm3v3lIcd5kANJENk/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcarZoe%2FdJMb8VsWtbj%2FDBWHIrm3v3lIcd5kANJENk%2Ftfile.svg&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;133&quot; height=&quot;150&quot; data-origin-width=&quot;133&quot; data-origin-height=&quot;150&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;h3 data-ke-size=&quot;size23&quot;&gt;ReentrantLock의 핵심 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReentrantLock은 Lock 인터페이스를 구현한다. JVM이 아닌 자바 코드 레벨에서 Lock을 걸기 때문에 synchronized에서는 불가능했던 다양한 기능을 제공할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758536676948&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Lock {
  
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. lock() / unlock()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock()은 락을 획득하는 메소드다. 다른 스레드가 이미 락을 획득했다면 락이 풀릴 때까지 WAITING한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unlock()은 락을 해제한다. 락이 해제되면 다른 스레드가 락을 획득할 수 있게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. tryLock()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락을 시도하는 메소드이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 획득에 성공하면 true를 반환하고, 실패하면 false를 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758536993092&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Lock {
    // 1회만 획득 요청
    boolean tryLock();

    // 시간만큼 대기
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
}&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;3. lockInterruptibly()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기하는 동안 다른 스레드가 Thread.interrupt()를 호출하면, 즉시 InterruptedException을 던지며 대기를 포기하는 락이다. lockInterruptibly()를 사용하면 종료 시점에 대기 중인 모든 스레드에 인터럽트를 걸어 깔끔하게 정리할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1758536983138&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Lock {
  
    void lockInterruptibly() throws InterruptedException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 공정 모드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReentrantLock을 생성할 때 boolean을 전달하면 공정 모드를 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1758537339599&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 공정한 락: 먼저 온 스레드가 먼저 획득
private final ReentrantLock fairLock = new ReentrantLock(true);

// 불공정한 락: 성능 우선 (기본값)
private final ReentrantLock unfairLock = new ReentrantLock(false);&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생산자-소비자 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생산자-소비자 문제는 여러 스레드가 데이터를 공유할 때 발생할 수 있는 가장 대표적인 동시성 문제이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생산자-소비자 문제는 3가지 핵심요소로 구성된다.&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;생산자 (Producer)&lt;/b&gt;:&amp;nbsp;데이터나 작업을 생성하는 역할을 한다. 생성한 결과물을 공유 자원인 '버퍼'에 저장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;소비자 (Consumer): &lt;/b&gt;'버퍼'에 저장된 데이터나 작업을 가져와서 처리(소비)하는 역할을 한다. 버퍼에서 데이터를 가져와 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유한 버퍼 (Bounded Buffer)&lt;/b&gt;:&amp;nbsp;생산자와 소비자 사이의 공유 자원입니다. 저장할 수 있는 데이터의 개수가 &lt;b&gt;제한&lt;/b&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;b&gt;버퍼가 가득 차면, 생산자는 기다려야 한다&lt;/b&gt;: 버퍼에 더 이상 데이터를 넣을 공간이 없으면, 생산자는 소비자가 데이터를 빼서 공간을 만들 때까지 대기해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버퍼가 비어 있으면, 소비자는 기다려야 한다&lt;/b&gt;:&amp;nbsp;버퍼에 가져갈 데이터가 하나도 없으면, 소비자는 생산자가 데이터를 채워 넣을 때까지 대기해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 ReentrantLock을 활용하면 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ReentrantLock의 Condition을 통한 세밀한 제어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Condition은 락과 결합되어 사용되며, 스레드가 특정 조건을 기다리거나 신호를 받을 수 있도록 한다. Object 클래스의 wait, notify, notifyAll과 유사한 역할을 한다고 보면 된다.&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;Lock에서 newCondition() 메소드를 호출하면 Condition 객체를 얻을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1758536972224&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface Lock {
  
    Condition newCondition();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하기 2개의 메소드만 알아보자.&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;await()&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: &lt;/span&gt;현재 스레드가 가진 Lock을 해제하고, 다른 스레드가 signal()이나 signalAll()을 호출해 자신을 깨울 때까지 대기한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;signal()&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;: &lt;/span&gt;해당 Condition에서 대기 중인(awaiting) 스레드 중 &lt;b&gt;하나를 무작위로 선택하여 깨운다.&lt;/b&gt; 깨어난 스레드는 다시 Lock을 획득하기 위해 시도한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758537179680&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Condition {

  void await() throws InterruptedException;
  void signal();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 Condition을 활용하면 생산자-소비자 패턴을 효율적으로 구현할 수 있다. 큐가 비었을 때 생산할 때까지 소비자가 기다리고, 큐가 가득찼을 땐 소비할 때까지 생산자가 기다린다.&lt;/p&gt;
&lt;pre id=&quot;code_1758536967668&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BoundedQueue implements BoundedQueue {

  private final Lock lock = new ReentrantLock();
  private final Condition producerCond = lock.newCondition(); // 생산자
  private final Condition consumerCond = lock.newCondition(); // 소비자

  private final Queue&amp;lt;String&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
  private final int max;

  public BoundedQueue(final int max) {
    this.max = max;
  }

  @Override
  public void produce(final String data) {
    lock.lock();
    try {
      while (queue.size() == max) {
        try {
          // 버퍼가 가득 차면 생산자를 대기시킨다.
          producerCond.await();
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      }
      // 대기 상태가 풀리면 데이터를 저장한다. (소비 메소드에서 생산자의 signal()을 호출)
      queue.offer(data);
      // 생산자가 데이터를 저장하면 소비자의 signal() 호출한다.
      consumerCond.signal();
    } finally {
      lock.unlock();
    }
  }

  @Override
  public String consume() {
    lock.lock();
    try {
      while (queue.isEmpty()) {
        try {
          // 버퍼에 아무것도 없으면 소비자를 대기시킨다.
          consumerCond.await();
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      }
      // 생산자가 signal()을 호출하면 데이터가 들어왔을 것이므로, 데이터를 소비한다.
      String data = queue.poll();
      // 소비자 데이터를 처리하면 생산자의 signal() 호출한다.
      producerCond.signal();
      return data;
    } finally {
      lock.unlock();
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lock Striping: 동시성 성능 높이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 본 synchronized와 ReentrantLock은 모두 하나의 락으로 전체 리소스를 보호한다. 하지만 이는 병목을 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1758626653961&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class PointService {

    private final MemberRepository memberRepository;
    private final PointRepository pointRepository;
    
    private final ReentrantLock lock = new ReentrantLock();
    
    public Point usePoint(long memberId, long pointAmount) {
        lock.lock();  // 모든 사용자가 이 하나의 락을 기다린다
        try {
        	Member member = memberRepository.getById(memberId);
			Point point = pointRepository.getByMemberId(memberId);
            return point.use(pointAmount);
        } finally {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드의 문제는 뭘까?&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lock Striping&lt;/h3&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;synchronized 키워드 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758627212428&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class PointService {

    private final MemberRepository memberRepository;
    private final PointRepository pointRepository;
    
    private final Map&amp;lt;Long, Object&amp;gt; memberLocks = new ConcurrentHashMap&amp;lt;&amp;gt;();
    
    private Object getLockForMember(long memberId) {
        return userLocks.computeIfAbsent(memberId, key -&amp;gt; new Object());
    }
    
    @Transactional
    public Point usePoint(long memberId, long pointAmount) {
        synchronized (getLockForMember(memberId)) {
            Member member = memberRepository.getById(memberId);
            Point point = pointRepository.getByMemberId(memberId);
            return point.use(pointAmount);
        }
    }
}&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;ReentrantLock 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758626826611&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class PointService {

    private final MemberRepository memberRepository;
    private final PointRepository pointRepository;
    
    private final Map&amp;lt;Long, ReentrantLock&amp;gt; memberLocks = new ConcurrentHashMap&amp;lt;&amp;gt;();
    
    private ReentrantLock getLockForMember(long memberId) {
        return userLocks.computeIfAbsent(memberId, key -&amp;gt; new ReentrantLock());
    }
    
    @Transactional
    public Point usePoint(long memberId, long pointAmount) {
        Lock memberLock = getLockForMember(long memberId);
        memberLock.lock();
        try {
            Member member = memberRepository.getById(memberId);
            Point point = pointRepository.getByMemberId(memberId);
            return point.use(pointAmount);
        } finally {
            memberLock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다룬 내용은 단일 JVM 내에서의 동시성 제어다. 하지만 현실은 더 복잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 분산 시스템을 운영하고 있다면, 여러 서버에 걸친 공유 자원에 대한 동시성 처리가 필요할 것이다. 이때는 데이터베이스 레벨의 락이나 Redis, Zookeeper 같은 분산 락이 필요하다.&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;하지만 모든 동시성 문제를 분산 락으로 해결할 필요는 없다. 서버 인스턴스 내부의 자원을 보호해야 한다면, Java의 락을 먼저 고려해보자.&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;&amp;nbsp;&lt;/p&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://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%9E%90%EB%B0%94-%EA%B3%A0%EA%B8%89-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;김영한 실전 자바 - 고급 1편, 멀티스레드와 동시성&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동시성 처리 시리즈&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음부터&amp;nbsp;다시&amp;nbsp;배우는&amp;nbsp;Java&amp;nbsp;동시성&amp;nbsp;제어&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/258&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;UPDATE 한 줄로 끝내는 동시성 처리 &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/263&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;좋아요 기능으로 알아보는 넥스트 키 락&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;a href=&quot;https://myvelop.tistory.com/260&quot;&gt;Lettuce&amp;nbsp;&lt;span&gt;분산&lt;/span&gt;&amp;nbsp;&lt;span&gt;락의&lt;/span&gt;&amp;nbsp;&lt;span&gt;오해와&lt;/span&gt;&amp;nbsp;&lt;span&gt;진실&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/264&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AOP로 동시성 처리 코드 분리하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>java</category>
      <category>JVM</category>
      <category>lock striping</category>
      <category>ReentrantLock</category>
      <category>Spring</category>
      <category>Synchronized</category>
      <category>동시성</category>
      <category>모니터 락</category>
      <category>생산자 소비자 문제</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/262</guid>
      <comments>https://myvelop.tistory.com/262#entry262comment</comments>
      <pubDate>Tue, 23 Sep 2025 20:49:00 +0900</pubDate>
    </item>
    <item>
      <title>외부 API가 서비스를 마비시킬 뻔한 이야기: 가상스레드 도입기</title>
      <link>https://myvelop.tistory.com/261</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Network I/O로 인한 스레드 블로킹 문제를 진단하고, JDK 21의 가상 스레드(Virtual Thread)를 활용해 근본적인 해결책을 찾아가는 과정을 담고 있다. 단순히 문제를 해결하는 것을 넘어, 가상 스레드 도입 과정에서 마주친 Thread Pinning과 Overwhelming 같은 예상치 못한 도전들과 그 해결책까지 상세히 다룬다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드 풀이 왜 고갈되었을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월요일 오전 10시, 커피를 마시며 한 주를 시작하려는 순간 슬랙에서 지연시간 알림이 오기 시작한다.&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;잔여 스레드 수: 0&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;마케팅 팀과 협업해 만든 서버 사이드 이벤트 추적(Meta Conversion API 연동)의 영향이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHmzMm/btsQIWepdDD/BD9cuh9vJLZy7oviuqx5e1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHmzMm/btsQIWepdDD/BD9cuh9vJLZy7oviuqx5e1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHmzMm/btsQIWepdDD/BD9cuh9vJLZy7oviuqx5e1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHmzMm%2FbtsQIWepdDD%2FBD9cuh9vJLZy7oviuqx5e1%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; alt=&quot;서버 사이드 이벤트 추적&quot; loading=&quot;lazy&quot; width=&quot;1199&quot; height=&quot;608&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 수집하기 위해 사용자의 모든 주요 행동을 Meta에 전송하는 시스템이다. 페이지 이동, 버튼 클릭, 구매 완료 등의 행동 유형을 파악하고, 마케팅 성과를 추적하고 있다. 하지만 &lt;b&gt;각 API 호출마다 200~500ms의 Network I/O가 발생&lt;/b&gt;했고, 무료체험 오픈 시간대의 트래픽과 만나자 문제가 발생한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Network I/O가 스레드 풀 고갈에 미치는 치명적인 영향을 알려면 플랫폼 스레드의 한계에 대해서 이해하고 있어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술적인 문제 분석&lt;/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 접근, API 호출 등 I/O 작업을 수행할 때 &lt;b&gt;스레드가&amp;nbsp;Blocking된다.&lt;/b&gt;&amp;nbsp;이때 Blocking된 스레드는 다른 작업을 할 수 없다.&lt;/li&gt;
&lt;li&gt;컨텍스트 스위칭 비용이 낮은 가상스레드, 스레드 스위칭 비용이 없는 코루틴 등에 비해 &lt;b&gt;상대적으로 컨텍스트 스위칭 비용이 높다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스레드 생성하는 데 시간이 오래 걸린다.&lt;/b&gt; 그렇다고 미리 만들어두기엔 스레드 1개당 MB 단위의 메모리를 필요로 하기 때문에 &lt;b&gt;자원을 낭비하게 될 가능성이 크다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;1170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzDnmy/btsQGkuCjk8/nQQtKn516r4uT1ph9XMgV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzDnmy/btsQGkuCjk8/nQQtKn516r4uT1ph9XMgV0/img.png&quot; data-alt=&quot;I/O 작업이 발생할 때 Blocking이 일어나기 때문에 스레드가 다른 작업을 할 수 없다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzDnmy/btsQGkuCjk8/nQQtKn516r4uT1ph9XMgV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzDnmy%2FbtsQGkuCjk8%2FnQQtKn516r4uT1ph9XMgV0%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; alt=&quot;플랫폼 스레드의 블로킹&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;668&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;1170&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;I/O 작업이 발생할 때 Blocking이 일어나기 때문에 스레드가 다른 작업을 할 수 없다.&lt;/figcaption&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;문제상황을 정리해보자면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 스레드 풀에서 가용할 수 있는 스레드를 100개를 설정했다고 해보자. Network I/O 200ms가 발생한다고 했을 때, 해당 요청이 1초에 500번만 들어와도 가용 가능한 스레드가 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;물론 스레드의 수를 늘리면 위 문제를 간단하게 해결할 수 있겠지만, 근본적인 문제인 Blocking I/O를 해결하면 리소스를 아낄 수 있다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;근본적인 문제 해결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blocking이 문제라면 Non-Blocking 도구를 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 생태계에는 여러 선택지가 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring Reactor&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring 생태계의 대표적인 비동기 프로그래밍 도구다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public Mono&amp;lt;Void&amp;gt; trackEvent(EventData eventData) {
    return Mono.fromCallable(() -&amp;gt; eventData)
        .flatMap(this::validateEvent)
        .flatMap(this::enrichEventData)
        .flatMap(this::sendToMetaAPI)
        .then();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존의 간단한 trackEvent() 메서드가 Mono 체인으로 변해야 했고, 이를 호출하는 모든 코드까지 연쇄적으로 수정해야 했다. DB 접근은? R2DBC를 공부하고 도입해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;h3 data-ke-size=&quot;size23&quot;&gt;2. 코루틴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor보다는 적용 난이도가 낮아 보였지만 자바 프로젝트에 Kotlin을 섞어야 하고, 여전히 상당한 코드 변경이 필요했다. 그때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/coroutine_virtual_thread_wayne/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;카카오페이 기술블로그&lt;/a&gt;에서 코루틴보다 가상스레드의 성능이 더 좋다는 글이 눈에 띄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 가상 스레드&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JDK 21부터 제공하기 시작한 기술로, 경량 스레드 기술이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가상 스레드의 구성도를 대충 그려보자면 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Carrier Thread는 기존의 Platform Thread라고 이며, Kernel Thread와 1대 1로 매핑되는 관계라고 생각하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFFQ9y/btsQGpCyPMh/zi9eQVfF2rxWq0Yome7rrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFFQ9y/btsQGpCyPMh/zi9eQVfF2rxWq0Yome7rrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFFQ9y/btsQGpCyPMh/zi9eQVfF2rxWq0Yome7rrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFFQ9y%2FbtsQGpCyPMh%2Fzi9eQVfF2rxWq0Yome7rrk%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; alt=&quot;가상스레드 풀 구성도&quot; loading=&quot;lazy&quot; width=&quot;994&quot; height=&quot;523&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청을 처리하기 위해 스레드를 할당할 때, 가상 스레드는 Mount 과정을 통해 Carrier Thread와 연결된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKWdYh/btsQGEzBIK3/IsZxVgejjM4Qel3PU6S3V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKWdYh/btsQGEzBIK3/IsZxVgejjM4Qel3PU6S3V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKWdYh/btsQGEzBIK3/IsZxVgejjM4Qel3PU6S3V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKWdYh%2FbtsQGEzBIK3%2FIsZxVgejjM4Qel3PU6S3V1%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; alt=&quot;가상 스레드의 Mount&quot; loading=&quot;lazy&quot; width=&quot;825&quot; height=&quot;403&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 가상스레드가 I/O 작업을 만나면 Unmount를 통해 연결을 끊고, 다른 가상스레드가 Carrier Thread를 사용할 수 있는 상태로 만든다. 이게 가상 스레드가 Non-Blocking으로 동작하는 방식이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0LhHi/btsQGUWurWQ/pBRI8Scezwr5IKTu7FxtO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0LhHi/btsQGUWurWQ/pBRI8Scezwr5IKTu7FxtO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0LhHi/btsQGUWurWQ/pBRI8Scezwr5IKTu7FxtO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0LhHi%2FbtsQGUWurWQ%2FpBRI8Scezwr5IKTu7FxtO1%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; alt=&quot;가상 스레드의 Unmount&quot; loading=&quot;lazy&quot; width=&quot;1374&quot; height=&quot;673&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;673&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;I/O 작업이 끝나면 다시 Carrier Thread와 연결하여 작업을 마무리하고 응답을 진행하게 될 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 적은 Carrier Thread 개수만으로도 많은 요청을 처리할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 가상 스레드는 적은 메모리, 짧은 생성시간 등의 장점을 가지고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 표는 플랫폼 스레드와 가상 스레드를 비교한 내용이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;Platform Thread&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;Virtual Thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;Stack 사이즈&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;~2MB&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;~10KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;생성시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;~1ms&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;~1&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;컨텍스트 스위칭&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;~100&amp;mu;s&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;~10&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;가상 스레드 적용해보기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;적용 방법은 간단했다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    
    private static final String PREFIX = &quot;virtual-thread-&quot;;
    private static final String TOMCAT_PREFIX = &quot;tomcat-virtual-thread-&quot;;
    
    @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    public AsyncTaskExecutor executor() {
        return createAsyncTaskExecutor(true, PREFIX, 5000);
    }
    
    @Bean
    public TomcatProtocolHandlerCustomizer&amp;lt;?&amp;gt; protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -&amp;gt; 
            protocolHandler.setExecutor(
                createAsyncTaskExecutor(true, TOMCAT_PREFIX, 5000)
            );
    }
    
    private SimpleAsyncTaskExecutor createAsyncTaskExecutor(
            final boolean isVirtualThread, final String prefix, final int timeout) {
        SimpleAsyncTaskExecutor asyncTaskExecutor = new SimpleAsyncTaskExecutor();
        asyncTaskExecutor.setVirtualThreads(isVirtualThread);
        asyncTaskExecutor.setThreadFactory(Thread.ofVirtual().name(prefix, 0).factory());
        asyncTaskExecutor.setTaskTerminationTimeout(timeout);
        return asyncTaskExecutor;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 설정 하나로 애플리케이션 전체가 가상스레드로 전환된다. Tomcat이 요청을 받는 것부터 비동기 작업까지 모두 가상스레드가 처리한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존의 코드도 전혀 수정할 필요가 없다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void trackEvent(TrackedEvent event) {
    // ...
    sendToMeta(event);  // 200~500ms blocking
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가상스레드를 적용하는 것만으로도 다음과 같은 성과를 얻을 수 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피크 시간 P95 Latency:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1.3초 &amp;rarr; 80ms&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;메모리 사용량 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;끝나지 않은 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;가상스레드를 적용하고 끝!&quot;... 이었으면 좋았겠지만&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;h3 data-ke-size=&quot;size23&quot;&gt;1. Thread Pinning - 가상스레드가 synchronized를 만났을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 테스트에서 DB 접근 작업 시 처리량이 전혀 개선되지 않았다. 원인은 &lt;b&gt;MySQL Connector/J 8.x가 내부적으로 사용하는 synchronized 키워드&lt;/b&gt;였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가상스레드는 synchronized를 만나면 플랫폼 스레드를 반납하지 못한다. JVM이 synchronized의 잠금(Monitor Lock) 소유자를 플랫폼 스레드로 기록하기 때문에 가상 스레드가 그 안에 갇혀 버린다. 가상 스레드의 장점이 사라지는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;만약 synchronized의 Blocking 문제를 해결하고 싶다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;ReentrantLock으로의 마이그레이션이 필요하다. 다행히 Connector/J는 가상 스레드 지원을 위해 발 빠르게 움직였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Connector/J 9.0.0 릴리스 노트를 확인하면 synchronized 대신 ReentrantLock을 사용하여 가상 스레드 친화적인 방향으로 코드를 개선했다고 한다. 실제로 Connector/J 9.x 버전으로 업데이트하니, 처리량이 개선된 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OhNWc/btsQHqm6K94/kX0P1tS8D68qO1CEzg0AmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OhNWc/btsQHqm6K94/kX0P1tS8D68qO1CEzg0AmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OhNWc/btsQHqm6K94/kX0P1tS8D68qO1CEzg0AmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOhNWc%2FbtsQHqm6K94%2FkX0P1tS8D68qO1CEzg0AmK%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; alt=&quot;Connector/J 9.x의 가상 스레드 지원&quot; loading=&quot;lazy&quot; width=&quot;1190&quot; height=&quot;154&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;모든 라이브러리가 Connector/J처럼 가상 스레드 호환에 적극적인 것은 아니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아직 많은 JVM 생태계의 라이브러리들이 synchronized를 그대로 사용하고 있다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 Thread Pinning 문제는 역사 속으로 사라지게 될 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://openjdk.org/jeps/491&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JEP 491&lt;/a&gt;에 따르면 JDK 24에서 획기적인 개선이 이뤄졌다.&lt;/p&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;따라서 synchronized 키워드를 만다더라도 플랫폼 스레드로부터 unmount를 진행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Overwhelming - 사라진 유량 제어&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;HikariPool-1 - Connection is not available, request timed out after ~~~~~ms&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스파이크 테스트를 진행하니, DB Connection Timeout이 발생한다. 왜 갑자기 DB 연결에서 문제가 발생할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플랫폼 스레드의 blocking은 의외의 역할을 하고 있었다. 바로 &lt;b&gt;자연스러운 스로틀링&lt;/b&gt;이었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;플랫폼 스레드 시절: 100개 스레드 = 최대 100개 동시 DB 접근 &lt;br /&gt;가상스레드 적용 후: 무제한 스레드 = DB 커넥션 풀 순식간에 고갈&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;만약 너무 많은 요청량이 그대로 DB에 전달되면 DB에서도 장애가 발생할 수 있는 위험한 상황인 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 &lt;a href=&quot;https://www.youtube.com/watch?v=vQP6Rs-ywlQ&amp;amp;t=1544s&quot;&gt;카카오 테크 밋&lt;/a&gt;에서 똑같은 사례를 발견했다. 해결책은 세마포어를 활용한 의도적인 유량 제어였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 코드는 제공해주지 않아 유량 제어를 직접 설계해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데코레이터 패턴을 활용하여 DataSource와 Connection 객체를 감싸 설정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;663&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A8GqL/btsQIK6jI0f/ZNMF1s5QT4QFJCkMs1c6HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A8GqL/btsQIK6jI0f/ZNMF1s5QT4QFJCkMs1c6HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A8GqL/btsQIK6jI0f/ZNMF1s5QT4QFJCkMs1c6HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA8GqL%2FbtsQIK6jI0f%2FZNMF1s5QT4QFJCkMs1c6HK%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; alt=&quot;데코레이터 세마포어를 활용한 데이터베이스 유량 제어&quot; loading=&quot;lazy&quot; width=&quot;1108&quot; height=&quot;663&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;663&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;DB 커넥션 풀 크기만큼만 동시 접근을 허용하도록 세마포어를 구현했다. 핵심은 DataSource.getConnection() 호출 시 세마포어를 획득하고, Connection.close() 시 반환하는 것이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class ThrottledDataSource implements DataSource {
    
    private final DataSource dataSource;
    private final Semaphore semaphore;
    
    public ThrottledDataSource(final DataSource dataSource, final int maximumPoolSize) {
        this.dataSource = dataSource;
        this.semaphore = new Semaphore(maximumPoolSize);
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        try {
            semaphore.acquire();
            return new DbConnectionReleaser(dataSource.getConnection(), semaphore);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    
    // 나머지는 delegate pattern으로 처리
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자원 반환을 보장하기 위해 Connection도 감쌌다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class DbConnectionReleaser implements Connection {
    
    private final Connection delegate;
    private final Semaphore semaphore;
    
    public DbConnectionReleaser(final Connection delegate, final Semaphore semaphore) {
        this.delegate = delegate;
        this.semaphore = semaphore;
    }
    
    @Override
    public void close() throws SQLException {
        try {
            delegate.close();
        } finally {
            semaphore.release();  // 반드시 세마포어 반환
        }
    }
    
    // 나머지는 delegate로 위임
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSourceConfig에서 ThrottledDataSource로 감싸기만 하면 된다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = &quot;spring.datasource.hikari&quot;)
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }
    
    @Bean
    public DataSource dataSource(final HikariConfig hikariConfig) {
        HikariDataSource dataSource = new HikariDataSource(hikariConfig);
        return new ThrottledDataSource(dataSource, hikariConfig.getMaximumPoolSize());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가상 스레드가 가져온 변화&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 풀 고갈 문제는 단순히 스레드 수를 늘리는 것으로 해결할 수 있지만, 이는 근본적인 해결책이 아닐 수 있다. 이번 사례를 통해 가상 스레드가 기존의 블로킹 I/O 문제를 얼마나 간단하게 해결할 수 있는지 확인할 수 있었다. 피크 시간 P95 레이턴시를 1.3초에서 80ms로 대폭 개선하고, 메모리 사용량까지 줄이는 성과를 거두었다.&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;특히 주목할 점은 가상 스레드의 도입 용이성이다. Spring Reactor나 코루틴처럼 코드베이스를 수정할 필요 없이, 설정 변경만으로도 즉시 적용할 수 있다는 것은 큰 장점이다. 물론 Thread Pinning과 같은 함정들이 있었지만, 라이브러리 버전 업그레이드 혹은 JDK 24 업그레이드 등으로 해결 가능했다.&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;가상 스레드는 단순한 기술적 개선을 넘어, JVM 생태계가 현대적인 동시성 패러다임으로 전환하는 중요한 전환점이 되고 있다. 최소한의 코드 변경으로 블로킹 I/O 문제를 해결하고 싶다면, 가상 스레드 도입을 적극 고려해보길 권한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;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/15398/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java의 미래 Virtual Thread&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;카카오 테크 밋, &lt;a href=&quot;https://www.youtube.com/watch?v=vQP6Rs-ywlQ&amp;amp;t=1544s&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JDK 21&lt;span&gt;의&lt;/span&gt; &lt;span&gt;신기능&lt;/span&gt; Virtual Thread &lt;span&gt;알아보기&lt;/span&gt; (&lt;span&gt;안정수&lt;/span&gt; James)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/491&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JEP 491: Synchronize Virtual Threads without Pinning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;카카오 페이 기술 블로그, &lt;a href=&quot;https://tech.kakaopay.com/post/coroutine_virtual_thread_wayne/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;코루틴과&lt;/span&gt; Virtual Thread &lt;span&gt;비교와&lt;/span&gt; &lt;span&gt;사용&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Blocking IO</category>
      <category>java</category>
      <category>JVM</category>
      <category>non-blocking io</category>
      <category>Platform Thread</category>
      <category>Spring</category>
      <category>virtual thread</category>
      <category>가상 스레드</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/261</guid>
      <comments>https://myvelop.tistory.com/261#entry261comment</comments>
      <pubDate>Sat, 20 Sep 2025 16:05:37 +0900</pubDate>
    </item>
    <item>
      <title>넥스터즈 27기 후기: 이론보다 실험, 정답은 유저에게 있었다</title>
      <link>https://myvelop.tistory.com/259</link>
      <description>&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;26기에서의 아쉬움&lt;/h2&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Nexters 26기 때 나는 유저가 아닌 내 머릿속 시나리오에 집착하고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;PM으로서, 아래의 슬로건을 걸고 프로젝트를 시작했다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;주변 사람들이 각자의 삶에서 의미를 발견하고, 그 의미를 통해 행복을 찾길 바랍니다.&lt;br /&gt;그러기 위해선 자기 자신의 가치관을 알아가는 것이 중요하다고 생각합니다.&lt;/blockquote&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;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;우리 팀은 Schwartz&lt;/span&gt;의 가치 이론에 기반한 자기 발견 앱인 Loopy를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UT에서 크게 불편하다는 얘기를 듣지 못했고, 나는 문제가 없다고 착각했다.&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;결과적으로 잘못 잡은 컨셉으로 인해 사용하기 불편한 UI/UX가 만들어졌고, 결과를 좋지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉬운 점이 많았지만, 배운 게 있었다. 27기에는 다르게 해보고 싶었다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;27기 재도전: 감정 탐색 AI, 이모티아&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;27기 프로젝트 소개를 봤다. 감정 탐색 AI 프로젝트가 눈에 띄었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가치관에서 감정으로 초점은 바뀌었지만 '자기 이해'라는 본질은 같았다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 팀에 1순위로 지원했고, 백엔드 개발자로서 합류할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;팀은 디자이너 1명, 앱 개발자 3명, 백엔드 2명으로 구성되었다. 7월 5일부터 8월 23일까지 8주간 진행하는 이번 프로젝트의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;개인적 목표는 &quot;유저의 사용성을 생각하는 개발&quot;을 하는 것&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2274&quot; data-origin-height=&quot;1480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BIiOx/btsQwdPHHAv/9CE4J8IfKRdW9IdGcyRnL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BIiOx/btsQwdPHHAv/9CE4J8IfKRdW9IdGcyRnL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BIiOx/btsQwdPHHAv/9CE4J8IfKRdW9IdGcyRnL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBIiOx%2FbtsQwdPHHAv%2F9CE4J8IfKRdW9IdGcyRnL0%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;2274&quot; height=&quot;1480&quot; data-origin-width=&quot;2274&quot; data-origin-height=&quot;1480&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;h3 data-ke-size=&quot;size23&quot;&gt;심리상담사의 자문과 프롬프트 초안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내가 믿었던 것: &quot;AI 모델에게 도메인 전문 지식을 많이 전달할수록 높은 품질의 결과물이 나올 것이다.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하기 전에 도메인 지식이 필요했다. 팀에 심리상담 지식을 가진 사람이 없어 외부에서 찾아야 했다. &lt;b&gt;다행히 회사에 심리상담사 출신 PM이 있었다. 그와의 대화에서 중요한 인사이트를 얻었다:&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;일상생활이 가능한 사람들이 자신의 감정을 더 잘 이해하도록 돕는 것이 목표&lt;/li&gt;
&lt;li&gt;감정 뒤에 숨은 욕망을 찾아 구체적인 액션 아이템을 제공하면 더 좋다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조언을 전부  프롬프트에 담았다. 감정 단계별 탐색 패턴, 공감 방법, 질문 순서 등 few-shot 예시를 잔뜩 넣었다. 답변의 질을 높이기 위해 chain-of-thought도 활용했다. 프롬프트 엔지니어링 가이드에서 본대로 했으니 잘 작동할 거라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UT의 충격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과: &quot;그냥 GPT랑 대화하는 게 나을 것 같아요.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전문성 있는 프롬프트로 감정을 정확히 파악하여, 유저에게 도움이 될 거라고 생각했다.&lt;/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;&quot;공감 &amp;rarr; 내 경험 공유 &amp;rarr; 추가 질문, 계속 이 패턴만 반복돼요.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;대화가 너무 템플릿 같아요. 기계적이에요.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;직전 답변에만 반응하는 것 같아요. 제가 앞서 얘기한 내용은 기억 못하네요.&quot;&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;few-shot 예시가 족쇄가 되었다. 내가 넣은 예시대로만 대화하니, 모든 대화가 똑같은 패턴으로 흘러갔다. Chain-of-thought로 정교하게 설계한 사고 과정도 마찬가지였다. 매번 같은 단계를 거치니 예측 가능하고 지루했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 아픈 피드백은 이거였다: &lt;b&gt;&quot;그냥 GPT랑 대화하는 게 나을 것 같아요.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 만든 건 '자연스러운 대화'가 아니라 '정해진 시나리오'였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;26기의 실수를 똑같이 반복하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;zero-shot으로 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UT 피드백을 곱씹으며 생각했다. 유저들이 예측 불가능성을 원한다면, 최대한 적은 예시를 주면 어떨까? 실험해볼 가치가 있었다.&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;모든 few-shot 예시를 삭제했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chain-of-thought도 뺐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심리상담 기법을 세세하게 지시하는 대신, '요정 여왕'의 페르소나와 맥락만 충실하게 정의했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;Persona&amp;gt;
정체: 이모티아(Emotia)라는 요정 세계의 여왕
나이: 1,000 (인간 나이로는 20대 후반처럼 보임)
성격: 상냥하고 포근하지만, 가끔 장난스러운 면도 있음, 상대방에 대한 호기심이 많음.
말투: 항상 반말을 사용하며, 친구처럼 편안하게 대화함

...

요정여왕은 이모티아(Emotia)라는 요정 세계를 돌보는 수호자다.
이모티아는 모든 감정이 살아 숨 쉬는 신비로운 왕국으로,
기쁨의 초원, 슬픔의 호수, 분노의 화산, 평온의 숲이 공존한다.

천 년 동안 이모티아를 지키며
인간 세계와 요정 세계를 오가는 마음의 다리를 돌봐왔다.
여왕이라 불리지만 누구보다 평등을 중요시하며,

...

&amp;lt;/Persona&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화 패턴이나 질문 순서 같은 건 일절 지정하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 자유롭게 대화하기 시작했다. 패턴에 갇히지 않고, 사용자의 맥락에 맞춰 유연하게 반응했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술적 고민들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자로서 프롬프트 엔지니어링 외에도 해결해야 할 기술적 과제들이 있었다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;도메인 주도 개발&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring AI를 활용하는 모듈을 만들 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;ai&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;라는 패키지를 만들어서 사용했었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 코드를 짜다 보니 든 생각이 있다. 결국 내가 만들려고 하는 것은 AI가 아니라 '대화' 기능이지 않나?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기술이 아닌 도메인 중심으로 생각을 바꿨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;ai&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패키지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;conversation&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;이 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드를 읽는 사람이 &quot;이 모듈이 뭘 하는지&quot; 바로 알 수 있게 하고 싶었기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;코드의 의도가 명확해졌다고 생각한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AI는 단지 구현 수단일 뿐, 비즈니스 로직의 핵심을 표현할 수 있게 되었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;또 기술이 발전하면 AI가 아닌 다른 기술을 사용할 수도 있지 않겠는가?&lt;/s&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI 모델 추상화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 고민은 모델 변경의 유연성이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT, Gemini, Clova 등 여러 모델를 바꿔 사용할 수 있는 유연성을 제공하고 싶었다.&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;Spring AI의 ChatModel과 ChatClient를 추상화했다. Spring AI에서 제공하는 인터페이스를 사용하되, 각 모델별로 구현체를 만들어서 사용할 수 있도록 설정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;1810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLEGZc/btsQxcCJcdw/ghp8GcNxKjWYtNsffstK0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLEGZc/btsQxcCJcdw/ghp8GcNxKjWYtNsffstK0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLEGZc/btsQxcCJcdw/ghp8GcNxKjWYtNsffstK0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLEGZc%2FbtsQxcCJcdw%2Fghp8GcNxKjWYtNsffstK0K%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; alt=&quot;Spring AI 모델 추상화 - 클래스다이어그램&quot; loading=&quot;lazy&quot; width=&quot;1388&quot; height=&quot;1810&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;1810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini VertexAI를 사용하는 것이 아니고, AI studio의 무료 토큰을 사용하는 것이었기 때문에 Gemini Google SDK를 사용해 GeminiChatModel를 직접 구현했다. (Spring AI에서 지원하는 툴을 찾지 못함.)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 이모티아는 Nexters 27기 대상을 받았다. 하지만 더 중요한 건 과정에서 얻은 교훈이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQH5xt/btsQx4kbVeL/eRVFNNlsD6RfHbi6QiTFwK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQH5xt/btsQx4kbVeL/eRVFNNlsD6RfHbi6QiTFwK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQH5xt/btsQx4kbVeL/eRVFNNlsD6RfHbi6QiTFwK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQH5xt%2FbtsQx4kbVeL%2FeRVFNNlsD6RfHbi6QiTFwK%2Fimg.jpg&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; alt=&quot;넥스터즈 대상 수상&quot; loading=&quot;lazy&quot; width=&quot;1382&quot; height=&quot;1258&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;26기 때는 유저의 목소리를 듣지 못했다.&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;27기는 달랐다. &lt;b&gt;핵심 기능을 빠르게 만들고, UT를 통해 피드백을 받고, 과감하게 수정했다.&lt;/b&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;&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;few-shot이 항상 답은 아니었고, chain-of-thought가 만능은 아니었다. 오히려 경우에 따라서 독이 될 수 있었다.&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;8주라는 짧은 기간, 회사 야근과 병행하는 빡센 일정이었지만 완주할 수 있었다.&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;사이드 프로젝트든 실무든, 실패를 두려워하지 말고 빠르게 만들고 피드백받기를 반복하면 좋겠다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동/IT커뮤니티</category>
      <category>Nexters</category>
      <category>Nexters 27기</category>
      <category>개발</category>
      <category>백엔드</category>
      <category>프롬프트 엔지니어링</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/259</guid>
      <comments>https://myvelop.tistory.com/259#entry259comment</comments>
      <pubDate>Sun, 14 Sep 2025 12:22:06 +0900</pubDate>
    </item>
    <item>
      <title>UPDATE 한 줄로 끝내는 동시성 문제</title>
      <link>https://myvelop.tistory.com/258</link>
      <description>&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;요구사항:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매일 제한된 수량의 무료 체험 제공&lt;/li&gt;
&lt;li&gt;선착순 N명 정확히 선정&lt;/li&gt;
&lt;li&gt;1명만 1개만 주문 가능&lt;/li&gt;
&lt;li&gt;동시 요청 처리 필수&lt;/li&gt;
&lt;li&gt;일별 재고 수량 기록 필요&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;p data-ke-size=&quot;size16&quot;&gt;100개의 재고가 있을 때 동시에 1,000명이 요청하면 어떻게 정확히 100명에게만 제공할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 일반적으로 제시되는 해결책들은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적 락(SELECT FOR UPDATE)&lt;/li&gt;
&lt;li&gt;낙관적 락(JPA @Version)&lt;/li&gt;
&lt;li&gt;데이터베이스 네임드 락&lt;/li&gt;
&lt;li&gt;Redis 분산 락&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;b&gt;MySQL의 UPDATE 쿼리와 배타락을 활용한 간단하면서도 효과적인 방법&lt;/b&gt;을 소개하고, 각 방법들의 트레이드오프를 분석해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황 구체화&lt;/h2&gt;
&lt;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 id=&quot;code_1755082118002&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class FreeItemStock {
    private String itemType;         // 상품 종류
    private LocalDate date;          // 날짜
    private Integer stock;           // 제공 개수 (비즈니스 분석용)
    private Integer orderedCount;    // 주문된 수량 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일별로 무료 상품을 제공하기 때문에 date 필드가 포함되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항에서 &quot;일별 재고 수량 기록 필요&quot;가 있었기 때문에, stock은 초기 설정 후 변경하지 않고 유지한다. 대신 주문 요청이 들어올 때마다 orderedCount를 증가시켜 실제 주문된 수량을 추적하는 방식으로 구현할 것이다.&lt;/p&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 id=&quot;code_1755082330849&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void claimFreeItem(String itemType, LocalDate date) {
    // 1. 현재 재고 조회
    FreeItemStock stock = repository.findByItemTypeAndDate(itemType, date);
    
    // 2. 재고 확인 (초기 재고 - 주문된 수량)
    if (stock.getStock() &amp;gt; stock.getOrderedCount()) {
        // 3. 주문 수량만 증가
        stock.setOrderedCount(stock.getOrderedCount() + 1);
        repository.save(stock);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7zRXq/btsPRysNPpc/N6tdeRRlKUVSyKL6xKhSqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7zRXq/btsPRysNPpc/N6tdeRRlKUVSyKL6xKhSqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7zRXq/btsPRysNPpc/N6tdeRRlKUVSyKL6xKhSqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7zRXq%2FbtsPRysNPpc%2FN6tdeRRlKUVSyKL6xKhSqK%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; alt=&quot;동시성 제어를 하지 않았을 때&quot; loading=&quot;lazy&quot; width=&quot;744&quot; height=&quot;728&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 요청이 거의 동시에 들어오면 둘 다 orderedCount를 99로 읽고, 둘 다 주문에 성공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;재고는 1개였지만 2명이 상품을 받아가는&lt;/b&gt; 심각한 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일반적인 해결책들과 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 비관적 락 (SELECT ... FOR UPDATE)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 데이터를 읽을 때부터 락을 걸어 다른 트랜잭션이 접근하지 못하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서 비관적 락을 구현하는 방법은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Lock 어노테이션과 LockModeType을 활용하면 JPA가 자동으로 SELECT ... FOR UPDATE 쿼리를 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1755083075380&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface FreeItemStockRepository extends JpaRepository&amp;lt;FreeItemStock, Long&amp;gt; {
    
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;SELECT s FROM FreeItemStock s WHERE s.itemType = :itemType AND s.date = :date&quot;)
    FreeItemStock findByItemTypeAndDateForUpdate(
        @Param(&quot;itemType&quot;) String itemType, @Param(&quot;date&quot;) LocalDate date);
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 시퀀스를 보면 비관적 락의 특징이 명확히 드러난다. 요청 A가 먼저 SELECT FOR UPDATE로 데이터를 조회하면서 락을 획득하면, 요청 B는 같은 데이터에 접근하려 해도 &lt;b&gt;락이 해제될 때까지 대기&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 A가 트랜잭션을 커밋하고 락을 해제한 후에야 요청 B가 데이터에 접근할 수 있다. 이때 이미 orderedCount가 100이 되어 재고가 소진되었으므로 요청 B는 주문에 실패한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg8gnL/btsPSZQNn9I/MjH7kydBukbYbaxnFHSOsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg8gnL/btsPSZQNn9I/MjH7kydBukbYbaxnFHSOsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg8gnL/btsPSZQNn9I/MjH7kydBukbYbaxnFHSOsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg8gnL%2FbtsPSZQNn9I%2FMjH7kydBukbYbaxnFHSOsK%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; alt=&quot;비관적 락의 동시성 제어&quot; loading=&quot;lazy&quot; width=&quot;821&quot; height=&quot;821&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;821&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;이 방식은 동시성 제어가 확실하지만, &lt;b&gt;모든 요청이 순차적으로 처리&lt;/b&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;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;❌ &lt;b&gt;데드락 위험&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 낙관적 락 (Versioning)&lt;/h3&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;JPA의 @Version 어노테이션을 사용하여 구현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1755083565304&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class FreeItemStock {
    private String itemType;
    private LocalDate date;
    private Integer stock;
    private Integer orderedCount;
    
    @Version
    private Long version;  // 낙관적 락을 위한 버전 필드
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은 재시도 로직이 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1755083848065&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class FreeItemService {
    private final FreeItemStockRepository repository;
    
    @Retryable(
        value = {OptimisticLockException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 50)
    )
    @Transactional
    public FreeItemStock claimFreeItemWithOptimisticLock(String itemType, LocalDate date) {
        FreeItemStock stock = repository.findByItemTypeAndDate(itemType, date);
        
        if (stock.getStock() &amp;lt;= stock.getOrderedCount()) {
            throw new OutOfStockException(&quot;재고가 소진되었습니다&quot;);
        } 
        stock.setOrderedCount(stock.getOrderedCount() + 1);
        repository.save(stock);  // 버전 불일치 시 OptimisticLockException 발생
    }
    
    // 재시도 실패 시 처리
    @Recover
    public void recover(OptimisticLockException e, String itemType, LocalDate date) {
        throw new RuntimeException(&quot;동시 요청이 많아 처리할 수 없습니다. 잠시 후 다시 시도해주세요.&quot;, e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락의 동작 과정을 살펴보면, 두 요청이 &lt;b&gt;동시에 데이터를 조회&lt;/b&gt;할 수 있다는 점이 비관적 락과의 큰 차이이다. 하지만 UPDATE 시점에 버전 체크를 통해 충돌을 감지한다.&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;요청 A가 먼저 UPDATE에 성공하면 version이 1에서 2로 증가한다. 요청 B가 뒤이어 UPDATE를 시도하지만, WHERE 조건의 &lt;b&gt;version이 더 이상 맞지 않아 업데이트가 실패&lt;/b&gt;하고 OptimisticLockException이 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;849&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckOKKp/btsPQxuBf0R/Ack64GX5IuCxVHDb0HysK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckOKKp/btsPQxuBf0R/Ack64GX5IuCxVHDb0HysK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckOKKp/btsPQxuBf0R/Ack64GX5IuCxVHDb0HysK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckOKKp%2FbtsPQxuBf0R%2FAck64GX5IuCxVHDb0HysK0%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; alt=&quot;낙관적 락의 동시성 제어&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;849&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;충돌이 드물게 발생하는 환경 (읽기가 많고 쓰기가 적은 경우)&lt;/li&gt;
&lt;li&gt;사용자가 폼을 작성하는 동안 다른 사용자의 수정을 방지해야 할 때&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;p data-ke-size=&quot;size16&quot;&gt;100명 한정 이벤트에 수백 명이 동시 접속하면 대부분의 요청이 충돌하게 되고, &lt;b&gt;재시도가 반복되면서 오히려 성능이 저하&lt;/b&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;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;❌ &lt;b&gt;충돌이 잦으면 비효율적&lt;/b&gt;: 재시도가 반복되면 오히려 성능 저하&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;버전 관리 부담&lt;/b&gt;: 엔티티에 version 필드 추가 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;불필요한 쿼리 반복&lt;/b&gt;: 재시도마다 SELECT 쿼리 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌&lt;span&gt;&amp;nbsp;&lt;b&gt;순서 보장 어려움&lt;/b&gt;: 재시도가 뒤엉켜 순서가 보장되지 않을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 네임드 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네임드 락은 MySQL이 제공하는 사용자 정의 락으로, 임의의 문자열을 기준으로 락을 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 특정 로우에만 락을 걸 수 있지만, 네임드 락은 더 유연한 범위 설정이 가능하다. 예를 들어 &quot;특정 사용자의 모든 주문 처리&quot;처럼 여러 테이블에 걸친 작업을 하나의 락으로 제어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 2개의 Function을 기반으로 네임드 락 동시성 제어를 구성할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET_LOCK('lockname', timeout): 네임드 락 획득&lt;/li&gt;
&lt;li&gt;RELEASE_LOCK('lockname'): 네임드 락 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;네임드 락의 특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;트랜잭션과 독립적&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;: 락은 MySQL 세션(커넥션) 단위로 관리되며, 세션이 종료되면 자동 해제된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전역 범위&lt;/b&gt;: 데이터베이스 전체에서 동일한 이름의 락은 하나만 존재할 수 있다.&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 A가 GET_LOCK()으로 특정 이름의 락을 획득합니다. 이때 &lt;b&gt;타임아웃을 5초로 설정&lt;/b&gt;했으므로, 락 획득을 위해 최대 5초까지 기다린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 B가 동일한 이름의 락을 요청하면, 요청 A가 락을 해제할 때까지 대기한다. 만약 5초가 지나도 락을 획득하지 못하면 0을 반환하며 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 &lt;b&gt;트랜잭션 커밋과 락 해제가 별개&lt;/b&gt;라는 것이다. 요청 A가 트랜잭션을 커밋해도 락은 유지되며, RELEASE_LOCK()을 명시적으로 호출해야 락이 해제된다. 이후 대기 중이던 요청 B가 락을 획득하지만, 이미 재고가 소진되어 주문에 실패한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;805&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjtIax/btsPSNiD0vx/L5tnf6zYgCT0defxiAwrJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjtIax/btsPSNiD0vx/L5tnf6zYgCT0defxiAwrJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjtIax/btsPSNiD0vx/L5tnf6zYgCT0defxiAwrJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjtIax%2FbtsPSNiD0vx%2FL5tnf6zYgCT0defxiAwrJ0%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; alt=&quot;네임드 락의 동시성 제어&quot; loading=&quot;lazy&quot; width=&quot;970&quot; height=&quot;805&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;805&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;✅ 트랜잭션과 독립적으로 동작&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;❌ &lt;b&gt;MySQL 종속적&lt;/b&gt;: 다른 DB로 전환 시 코드 수정 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;별도 커넥션 필요&lt;/b&gt;: 락 관리용 추가 DB 커넥션 필요&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;❌&lt;b&gt;&lt;span&gt; 커넥션 풀 고갈 위험&lt;/span&gt;&lt;/b&gt;: 락을 기다리는 동안 커넥션을 점유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Redis 분산 락&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Redis 분산 락은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;SET NX(Not eXists)&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어를 사용하여 원자적으로 락을 획득한다. NX 옵션은 키가 존재하지 않을 때만 설정하고, PX 1000은 1초(1000ms) 후 자동 만료를 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TTL이 설정되어 있어&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로세스가 비정상 종료되어도 지정된 시간 후 자동으로 락이 해제된다. 서로 다른 애플리케이션 서버에서 실행되는 요청들도 Redis를 통해 동시성을 제어할 수 있다는 것이 가장 큰 장점이다.&lt;/span&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;Redis를 통한 분산 락 동작도 편의상 구현은 생략하고 (Redisson을 사용했다고 가정하고) 동작 원리만 설명하도록 하겠다.&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;요청 A가 락을 획득하면, 요청 B는 락 획득에 실패하고 재시도 로직을 실행한다. DB 작업이 완료되면 Redisson은&amp;nbsp;&lt;b&gt;Lua 스크립트를 사용하여 원자적으로&lt;/b&gt; 락을 해제한다. 이는 자신이 설정한 락만 해제하도록 보장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCI1Az/btsPR7BWYNX/Ix5bDkQXu0RSE6i1hOwkBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCI1Az/btsPR7BWYNX/Ix5bDkQXu0RSE6i1hOwkBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCI1Az/btsPR7BWYNX/Ix5bDkQXu0RSE6i1hOwkBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCI1Az%2FbtsPR7BWYNX%2FIx5bDkQXu0RSE6i1hOwkBK%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; alt=&quot;Redis 분산 락의 동시성 제어&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;797&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;797&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;여기서 주의할 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SET NX를 사용할 경우 선착순이 보장되지 않아, 선착순 요구사항을 만족하지 못하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Fair Lock을 사용해야 한다. 대기 큐를 구현하여 순서를 보장해야 하고, 그로 인해 오버헤드가 발생할 수 있다.&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;&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;✅ 메모리 기반의 높은 성능&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;: Redis 클러스터 구축/운영&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;네트워크 오버헤드&lt;/b&gt;: 매 요청마다 Redis 통신&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;복잡한 에러 처리&lt;/b&gt;: Redis 장애, 네트워크 단절 대응&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;트랜잭션 문제&lt;/b&gt;: Redis와 DB 간 트랜잭션 보장 어려움&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UPDATE WHERE 배타락&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순하지만 강력한 해결책&lt;/h3&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 살펴본 방법들은 각각 장단점이 있었다. 비관적 락은 대기 시간이 길고, 낙관적 락은 재시도 로직이 복잡하며, 네임드 락은 관리가 번거롭고, Redis는 추가 인프라가 필요하다.&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그런데 &lt;/span&gt;&lt;b&gt;MySQL의 UPDATE 쿼리 하나로&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 이 모든 문제를 해결할 수 있다면 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1755089992119&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE free_item_stock 
SET ordered_count = ordered_count + 1
WHERE item_type = :itemType 
  AND date = :date 
  AND stock &amp;gt; ordered_count;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절의 조건이 만족할 때만 UPDATE가 실행되고, &lt;b&gt;UPDATE 자체가 원자적 연산&lt;/b&gt;이므로 동시성 문제가 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 구현도 훨씬 간단해진다.&lt;/p&gt;
&lt;pre id=&quot;code_1755090049854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class FreeItemService {
    private final FreeItemStockRepository repository;
    
    @Transactional
    public void claimFreeItem(String itemType, LocalDate date) {
        int updatedCount = repository.incrementOrderedCount(itemType, date);
        
        if (updatedCount == 0) {
            throw new OutOfStockException(&quot;재고가 소진되었습니다&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1755090212385&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface FreeItemStockRepository extends JpaRepository&amp;lt;FreeItemStock, Long&amp;gt; {
    
    @Modifying
    @Query(&quot;&quot;&quot;
        UPDATE FreeItemStock s 
        SET s.orderedCount = s.orderedCount + 1 
        WHERE s.itemType = :itemType 
          AND s.date = :date 
          AND s.stock &amp;gt; s.orderedCount
    &quot;&quot;&quot;)
    int incrementOrderedCount(
    	@Param(&quot;itemType&quot;) String itemType, @Param(&quot;date&quot;) LocalDate date);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 UPDATE 문을 실행할 때 다음과 같은 과정을 거친다.&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;WHERE 절 평가&lt;/b&gt;: 조건에 맞는 로우를 찾는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배타락 획득&lt;/b&gt;: 해당 로우에 자동으로 배타락(X Lock)을 건다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UPDATE 실행&lt;/b&gt;: 값을 변경한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커밋 시 락 해제&lt;/b&gt;: 트랜잭션 커밋 시 락이 해제된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 두 요청이 동시에 들어와도 MySQL이 내부적으로 순서를 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 UPDATE가 성공하면 orderedCount가 100이 되고, 두 번째 UPDATE는 WHERE 조건(stock &amp;gt; orderedCount, 즉 100 &amp;gt; 100)을 만족하지 못해 0 rows를 반환하게 된다.&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FVs0G/btsPRwBPW8X/o1dNoUtvwRMX4gyKUGKwx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FVs0G/btsPRwBPW8X/o1dNoUtvwRMX4gyKUGKwx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FVs0G/btsPRwBPW8X/o1dNoUtvwRMX4gyKUGKwx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFVs0G%2FbtsPRwBPW8X%2Fo1dNoUtvwRMX4gyKUGKwx0%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; alt=&quot;배타 락의 동시성 제어&quot; loading=&quot;lazy&quot; width=&quot;1057&quot; height=&quot;644&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&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;✅ &lt;b&gt;성능 최적화&lt;/b&gt;: SELECT 없이 UPDATE 한 번으로 끝&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;자동 동시성 제어&lt;/b&gt;: MySQL이 알아서 처리&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;✅ &lt;b&gt;추가 인프라 불필요&lt;/b&gt;: Redis, 별도 커넥션 불필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ 복잡한 비즈니스 로직에는 부적합할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ UPDATE 결과만으로 판단 (상세한 실패 이유 파악 어려움)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Redis 분산 락과 비교&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;DB 배타락이 더 나은 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 일관성 (ACID 보장)&lt;/li&gt;
&lt;li&gt;네트워크 오버헤드 최소화 (1번의 쿼리로 해결)&lt;/li&gt;
&lt;li&gt;기존 인프라 활용하여 운영 복잡도를 줄이고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Redis가 더 나은 경우&lt;/h4&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;대규모 동시 접근 (수만 TPS)&lt;/li&gt;
&lt;li&gt;분산 락이 필요한 마이크로서비스 환경, 여러 데이터베이스 간 동시성 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 설계 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE WHERE 방식의 성능을 극대화하려면 &lt;b&gt;인덱스 설계가 필수&lt;/b&gt;다. MySQL의 InnoDB 스토리지 엔진은 데이터의 일관성과 무결성을 지키기 위해 인덱스를 기준으로 넥스트 키 락을 거는데, &lt;b&gt;만약 인덱스가 없다면 테이블 전체에 락을 걸어버리기 때문에 성능이 크게 저하될 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 인덱스 설정은 락의 범위를 최소화하기에 바람직하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. WHERE 절 분석&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;item_type = :itemType (등호 조건)&lt;/li&gt;
&lt;li&gt;date = :date (등호 조건)&lt;/li&gt;
&lt;li&gt;stock &amp;gt; ordered_count (범위 조건)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 인덱스 컬럼 순서 결정 원칙&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합 인덱스에서 컬럼 순서는 성능에 절대적인 영향을 미친다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&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;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;item_type, date가 여기에 해당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;2순위: 범위(&amp;lt;, &amp;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;stock &amp;gt; ordered_count 조건에서 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 카디널리티(Cardinality) 고려&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&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;아래와 같이 조회했는데 상품의 종류가 10, 날짜가 1,000이라고 해보자. 그렇다면 date가 먼저 오게 해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1755090712897&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 
    COUNT(DISTINCT item_type) as item_type_cardinality,
    COUNT(DISTINCT date) as date_cardinality
FROM free_item_stock;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 적합해 보이는 복합 인덱스는 (date, item_type, stock) 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커버링 인덱스 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능을 높여 보겠다고 커버링 인덱스를 고려할 수도 있다. (date, item_type, stock, ordered_count)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 경우에 따라 UPDATE 문의 성능이 오히려 나빠질 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755091512812&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN UPDATE free_item_stock 
SET ordered_count = ordered_count + 1
WHERE item_type = 'ITEM1' 
  AND date = '2025-08-13' 
  AND stock &amp;gt; ordered_count;&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 88.721%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.0775%;&quot;&gt;id&lt;/td&gt;
&lt;td style=&quot;width: 24.4566%;&quot;&gt;select_type&lt;/td&gt;
&lt;td style=&quot;width: 16.8057%;&quot;&gt;...&lt;/td&gt;
&lt;td style=&quot;width: 45.9002%;&quot;&gt;Extra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.0775%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 24.4566%;&quot;&gt;UPDATE&lt;/td&gt;
&lt;td style=&quot;width: 16.8057%;&quot;&gt;...&lt;/td&gt;
&lt;td style=&quot;width: 45.9002%;&quot;&gt;Using&amp;nbsp;where;&amp;nbsp;Using&amp;nbsp;temporary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Using temporary. 임시 테이블을 만들었다는 얘기다. 왜 일까?&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;커버링 인덱스는 SELECT 쿼리에서는 탁월한 성능을 보인다. 조회할 필드와 검색 조건이 모두 인덱스에 포함되어 있다면, 실제 테이블에 접근하지 않고 인덱스만으로 결과를 반환할 수 있기 때문이다.&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;하지만 UPDATE 문에서는 상황이 다르다. 인덱스를 통해 조건에 맞는 로우를 찾았더라도, 결국 실제 테이블의 데이터를 수정해야 한다. 이 과정에서 MySQL은 인덱스에서 찾은 정보를 임시 테이블에 저장한 후, 이를 기반으로 실제 테이블을 업데이트한다.&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;이러한 임시 테이블 생성은 추가적인 메모리와 CPU를 사용하므로 오버헤드가 발생한다. 특히 예제처럼 카디널리티가 높은 경우, 즉 업데이트할 로우를 정확히 특정할 수 있는 경우, 임시 테이블 생성이 오히려 성능을 악화시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Transaction 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPDATE WHERE 방식이 아무리 효율적이어도, 트랜잭션 관리를 잘못하면 모든 장점이 무너진다.&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;UPDATE 쿼리가 실행되는 순간 해당 로우에 배타락이 걸린다. 하지만 트랜잭션이 끝날 때까지 락이 유지되므로 만약 시간이 오래 걸리는 작업과 같은 트랜잭션에 묶여 있다면 다른 요청들이 모두 대기하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k8zQ0/btsPSoQPCGf/V7Fvwxat9Imr9ryzGkjXXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k8zQ0/btsPSoQPCGf/V7Fvwxat9Imr9ryzGkjXXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k8zQ0/btsPSoQPCGf/V7Fvwxat9Imr9ryzGkjXXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk8zQ0%2FbtsPSoQPCGf%2FV7Fvwxat9Imr9ryzGkjXXk%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; alt=&quot;배타 락: 트랜잭션 문제 유의&quot; loading=&quot;lazy&quot; width=&quot;882&quot; height=&quot;646&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;646&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 선택 기준&lt;/h3&gt;
&lt;div&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;분산 환경인가?&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;/li&gt;
&lt;li&gt;&lt;b&gt;팀의 기술 스택은 무엇인가?&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오버엔지니어링을 피하자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 소개한 UPDATE WHERE 방식이 모든 상황에 적합한 것은 아니다.&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;하지만 많은 경우, 특히 단순한 요구사항에서는 데이터베이스가 제공하는 기본 기능만으로도 충분하다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;간단한 동시성 문제. UPDATE 한 줄로 끝내자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/262&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;처음부터&amp;nbsp;다시&amp;nbsp;배우는&amp;nbsp;Java&amp;nbsp;동시성&amp;nbsp;제어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;UPDATE 한 줄로 끝내는 동시성 문제&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/263&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;좋아요 기능으로 알아보는 넥스트 키 락&lt;/a&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Lettuce 분산 락의 오해와 진실&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/264&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;AOP로 동시성 처리 코드 분리하기&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>java</category>
      <category>MySQL</category>
      <category>redis</category>
      <category>Spring</category>
      <category>낙관적 락</category>
      <category>동시성</category>
      <category>동시성 제어</category>
      <category>배타락</category>
      <category>분산락</category>
      <category>비관적 락</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/258</guid>
      <comments>https://myvelop.tistory.com/258#entry258comment</comments>
      <pubDate>Wed, 13 Aug 2025 22:40:09 +0900</pubDate>
    </item>
    <item>
      <title>분산 캐시 동기화 문제, Redis Pub/Sub으로 해결하기</title>
      <link>https://myvelop.tistory.com/257</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;너무 느린 외부 API&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 외부 시스템과의 연동 프로젝트를 진행하게 되었다. 요구사항은 간단해 보였다. &quot;해당 일자에 주문이 가능한지 외부 API를 통해 확인할 수 있어야 한다.&quot; 하지만 실제로 구현해보니, 고객에게 정확한 정보를 전달하기 위해선 한 화면에서 40~60건의 날짜별 배송 계획을 한 번에 조회해야 했다.&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;병렬 처리를 적용했음에도 불구하고 API 응답 시간은 &lt;b&gt;500ms에서 1초&lt;/b&gt;, 심지어 요청이 여러 번 겹치면 그 이상 소요되었다. 연동사에서 제공해준 bulk API를 사용했는데 오히려 더 느려졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 주문 가능 일자를 확인할 때마다 1초 이상을 기다려야 한다니, 이건 명백히 사용성에 심각한 문제였다.&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;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;h3 data-ke-size=&quot;size23&quot;&gt;Redis 캐시 도입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 캐시를 적용한 후, 성능은 확실히 개선되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초가 걸리던 API 호출이 50~100ms로 줄어들었으니 약 10배 정도 빨라진 셈이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여전히 뭔가 아쉬웠다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Redis 캐시 적용&lt;/b&gt;:&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;50 ~ 100ms&lt;br /&gt;&lt;b&gt;외부 API 직접 호&lt;/b&gt;출:&amp;nbsp; &amp;nbsp;500ms ~ 1초&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;왜 캐시를 적용했는데도 여전히 수십 밀리초가 걸릴까?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 간단했다. &lt;b&gt;아무리 빨라도 네트워크는 네트워크&lt;/b&gt;였던 것이다. Redis가 아무리 빠르다고 해도, 네트워크를 통해 데이터를 가져오고, 수십 건의 데이터를 직렬화/역직렬화하는 과정은 피할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 캐시의 등장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그럼 애플리케이션 메모리에 캐시를 두면 어떨까?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 캐시(Caffeine)를 Redis 캐시 앞단에 추가했다. 결과는 놀라웠다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;L1 Cache (로컬 메모리)&lt;/b&gt;:&amp;nbsp; &amp;nbsp;~10ms ⚡ &lt;br /&gt;&lt;b&gt;L2 Cache (Redis)&lt;/b&gt;:&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;50~100ms &lt;br /&gt;&lt;b&gt;외부 API 직접 호출&lt;/b&gt;:&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;500ms ~ 1초&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 캐시는 자바 Object를 메모리에 그대로 저장하기 때문에 네트워크 왕복과 JSON 파싱이 과정이 생략된다. 따라서 눈에 띄게 속도가 빨라진 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로운 문제: 분산 환경의 딜레마&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 놓친 게 있다. 우리 서비스는 여러 대의 서버에서 동작하는 &lt;b&gt;분산 시스템&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 상황을 그려보면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2YV7k/btsPMZp4yWc/RKiChItwZW43pYOfgWWwG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2YV7k/btsPMZp4yWc/RKiChItwZW43pYOfgWWwG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2YV7k/btsPMZp4yWc/RKiChItwZW43pYOfgWWwG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2YV7k%2FbtsPMZp4yWc%2FRKiChItwZW43pYOfgWWwG0%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; alt=&quot;분산 시스템에서의 캐시&quot; loading=&quot;lazy&quot; width=&quot;1349&quot; height=&quot;799&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ServerA에서 리뷰를 수정했다고 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1351&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/euqdaJ/btsPNbYcX2T/4uNKj7tgyLBcORmMpuaPYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/euqdaJ/btsPNbYcX2T/4uNKj7tgyLBcORmMpuaPYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/euqdaJ/btsPNbYcX2T/4uNKj7tgyLBcORmMpuaPYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeuqdaJ%2FbtsPNbYcX2T%2F4uNKj7tgyLBcORmMpuaPYK%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; alt=&quot;분산 시스템에서의 캐시 - 캐시 업데이트로 인한 데이터 부정합 발생&quot; loading=&quot;lazy&quot; width=&quot;1351&quot; height=&quot;754&quot; data-origin-width=&quot;1351&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ServerA는 자신의 로컬 캐시를 비우고 새로운 값을 넣었지만, ServerB는 여전히 옛날 데이터를 가지고 있게 된다. 사용자가 어느 서버에 정보를 요청하느냐에 따라 다른 가격 정보를 보게 되는 것이다!&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 무효화 이벤트를 모든 서버에 전파할 방법이 필요했다. 그리고 우리가 선택한 해결책은 &lt;b&gt;Redis Pub/Sub&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Redis Pub/Sub은 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub을 라디오 방송에 비유해보겠다.&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;DJ가 사연을 읽으면, 그 주파수에 맞춰놓은 모든 청취자가 동시에 같은 내용을 듣게 된다. 심지어 사연을 보낸 본인도 라디오를 켜놓았다면 자신의 사연을 듣게 된다.&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;Redis Pub/Sub도 똑같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;Redis(&lt;b&gt;방송국):&lt;/b&gt;&lt;/b&gt; 메시지를 중간에서 받아 전달해주는 &lt;b&gt;메시지 브로커(Message Broker)&lt;/b&gt; 역할&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Publisher(사연자)&lt;/b&gt;: 메시지를 보내는 주체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Subscriber(청취자)&lt;/b&gt;: 특정 채널을 구독하고 메시지를 받는 주체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Channel(주파수)&lt;/b&gt;: 메시지가 전달되는 통로&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Redis Pub/Sub을 선택했을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub을 사용하게된 이유는 아래와 같다.&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;이미 Redis를 쓰고 있었다&lt;/b&gt;: 추가 인프라 없이 바로 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 전파&lt;/b&gt;: 메시지 전달 지연이 거의 없음 (수십 ms 이내)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;간단한 구현&lt;/b&gt;: 복잡한 설정 없이 바로 적용 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fire-and-Forget&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현해보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring CompositeCacheManager의 한계와 커스텀 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring이 제공하는 CompositeCacheManager은 여러모로 아쉬운 점이 많다.&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;CompositeCacheManager는 캐시 매니저를 List로 가지고 있으며, 캐시를 사용할 때 캐시 매니저 리스트를 순회하며 첫 번째로 발견되는 캐시만 반환한다. 그래서 아래와 같은 문제가 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;: 각 캐시 매니저가 독립적으로 동작한다. Put이나 Evict을 해도 선택된 캐시에만 적용된다. L1/L2 캐시의 일관성이 깨진다는 말이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백필(Backfill) 미지원&lt;/b&gt;: L1 미스 &amp;rarr; L2 히트 시, L2 데이터를 L1에 자동 저장하지 않음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주의사항: Backfill은 캐시 TTL 문제 존재&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Github 이슈에서도 한 번 언급된 적이 있었다. 스프링 측의 답변은 명확했다. &quot;CompositeCacheManager는 fallback 용이지 multi-level용이 아닙니다.&quot;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gihub Issue: &lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/23531&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring cache multiple level cache support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;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;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보면 캐시를 반환할 때, CompositeCache라는 Cache 구현체로 감싸서 반환하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754731766079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CustomCompositeCacheManager implements CacheManager {
    private final List&amp;lt;CacheManager&amp;gt; cacheManagers;
    
    public CustomCompositeCacheManager(
        CacheManager l1CacheManager,
        CacheManager l2CacheManager) {
        // 순서 중요! L1 &amp;rarr; L2 순으로 조회됨
        this.cacheManagers = List.of(l1CacheManager, l2CacheManager);
    }
    
    @Override
    public Cache getCache(String name) {
        List&amp;lt;Cache&amp;gt; caches = cacheManagers.stream()
            .map(manager -&amp;gt; manager.getCache(name))
            .filter(Objects::nonNull)
            .toList();
            
        return caches.isEmpty() ? null : new CompositeCache(caches);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1754731924218&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// CompositeCache.java - 실제 Multi-Level 로직
public class CompositeCache implements Cache {
    private final List&amp;lt;Cache&amp;gt; caches;
    
    @Override
    public ValueWrapper get(Object key) {
        List&amp;lt;Cache&amp;gt; missedCaches = new ArrayList&amp;lt;&amp;gt;();
        
        for (Cache cache : caches) {
            ValueWrapper value = cache.get(key);
            if (value != null) {
                // 핵심! 상위 캐시들에 백필
                backfillCaches(key, value.get(), missedCaches);
                return value;
            }
            missedCaches.add(cache);  // 미스된 캐시 추적
        }
        return null;
    }
    
    private void backfillCaches(Object key, Object value, List&amp;lt;Cache&amp;gt; missedCaches) {
        // L1 미스 &amp;rarr; L2 히트 시, L1에 자동 저장
        missedCaches.forEach(cache -&amp;gt; cache.put(key, value));
    }
    
    @Override
    public void put(Object key, Object value) {
        // Write-Through: 모든 레벨에 동시 저장
        caches.forEach(cache -&amp;gt; cache.put(key, value));
    }
    
    @Override
    public void evict(Object key) {
        // 모든 레벨에서 동시 제거
        caches.forEach(cache -&amp;gt; cache.evict(key));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 커스텀 캐시 매니저로 L1/L2 캐시를 주입해서 빈으로 등록하면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L1을 로컬 캐시, L2를 글로벌 캐시로 설정해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1754732024166&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class CacheConfig {
    
    @Bean
    @Primary
    public CacheManager cacheManager(
        @Qualifier(&quot;l1CacheManager&quot;) CacheManager l1,
        @Qualifier(&quot;l2CacheManager&quot;) CacheManager l2) {
        
        return new CustomCompositeCacheManager(l1, l2);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 그림으로 그려본다면 아래와 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GET을 시도했을 때, L1/L2 캐시가 모두 존재하는 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0g7fZ/btsPNPN5zGI/RQiAMuJ2weK7JHWn7Ys141/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0g7fZ/btsPNPN5zGI/RQiAMuJ2weK7JHWn7Ys141/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0g7fZ/btsPNPN5zGI/RQiAMuJ2weK7JHWn7Ys141/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0g7fZ%2FbtsPNPN5zGI%2FRQiAMuJ2weK7JHWn7Ys141%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; alt=&quot;L1/L2 캐시가 모두 성공하면 로컬 캐시를 조회한다.&quot; loading=&quot;lazy&quot; width=&quot;1275&quot; height=&quot;656&quot; data-origin-width=&quot;1275&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GET을 시도했을 때, L2 캐시만 존재하는 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciiTL7/btsPL2AYBJh/xixAKRQnLnP1JiSCcs2qSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciiTL7/btsPL2AYBJh/xixAKRQnLnP1JiSCcs2qSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciiTL7/btsPL2AYBJh/xixAKRQnLnP1JiSCcs2qSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciiTL7%2FbtsPL2AYBJh%2FxixAKRQnLnP1JiSCcs2qSK%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; alt=&quot;L2 캐시만 존재하면 레디스를 조회한다. 대신 backfill을 통해 로컬 캐시를 채워준다.&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;480&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EVICT를 시도하면 모든 캐시 계층에 접근하여 삭제&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UhoDe/btsPMBC6cq8/C1kcaOpKy0SqwSNYjou4Fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UhoDe/btsPMBC6cq8/C1kcaOpKy0SqwSNYjou4Fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UhoDe/btsPMBC6cq8/C1kcaOpKy0SqwSNYjou4Fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUhoDe%2FbtsPMBC6cq8%2FC1kcaOpKy0SqwSNYjou4Fk%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; alt=&quot;커스텀 캐시 매니저는 2개의 캐시를 모두 반환하기 때문에 Eviction이 일어날 때 두 계층의 캐시를 모두 삭제한다.&quot; loading=&quot;lazy&quot; width=&quot;1265&quot; height=&quot;485&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;485&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;h3 data-ke-size=&quot;size23&quot;&gt;2. Redis Pub/Sub을 통한 캐시 정합성 보장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Redis 설정이 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1754734751036&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {
    
    @Bean
    public RedisConnectionFactory pubSubConnectionFactory(RedisProperties properties) {
        LettuceConnectionFactory factory = new LettuceConnectionFactory(
            new RedisStandaloneConfiguration(properties.host(), properties.port()));
            
        // PubSub 전용 연결 풀 설정
        factory.setShareNativeConnection(false);
        return factory;
    }
    
    @Bean
    public RedisTemplate&amp;lt;String, String&amp;gt; pubSubRedisTemplate(
        RedisConnectionFactory connectionFactory) {
        RedisTemplate&amp;lt;String, String&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);
        template.setDefaultSerializer(new StringRedisSerializer());
        return template;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 메세지를 전송할 Publisher를 선언한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754734640818&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class RedisPublisher implements MessagePublisher {
    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;
    private final ObjectMapper objectMapper;
    
    @Override
    public void publish(String channel, Object message) {
        try {
            String json = objectMapper.writeValueAsString(message);
            redisTemplate.convertAndSend(channel, json);
            log.debug(&quot;Published message to channel: {}&quot;, channel);
        } catch (JsonProcessingException e) {
            // 발행 실패해도 로컬 캐시는 이미 무효화됨
            log.error(&quot;Failed to publish message&quot;, e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 메시지를 구독하는 부분을 만들어야 하는데 MessageListenerAdapter와 RedisMessageListenerContainer 이렇게 2개의 구현체를 활용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisMessageListenerContainer의 내부 동작은 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;: Spring 컨텍스트 시작 시 Redis SUBSCRIBE 명령 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블로킹 리스닝&lt;/b&gt;: 별도 스레드에서 Redis 연결을 통해 메시지 대기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 수신&lt;/b&gt;: Redis로부터 메시지 수신 시 TaskExecutor로 처리 위임&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754734904828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class MessageConfig {

    @Bean
    public MessageListenerAdapter messageListenerAdapter(
        CacheMessageDelegate delegate) {
        // handleMessage 메서드로 메시지 라우팅
        return new MessageListenerAdapter(delegate, &quot;handleMessage&quot;);
    }
    
    @Bean
    public RedisMessageListenerContainer messageListenerContainer(
        RedisConnectionFactory connectionFactory,
        MessageListenerAdapter listenerAdapter) {
        
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        
        // 에러 핸들러 설정
        container.setErrorHandler(throwable -&amp;gt; log.error(&quot;Error in message listener&quot;, throwable));
        
        // 구독할 채널 등록
        container.addMessageListener(
            listenerAdapter, 
            new ChannelTopic(&quot;cache:evict&quot;)
        );        
        return container;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CacheMessageDelegate를 통해 구독한 메시지를 수신하여 Eviction 동작을 실행하게 만들 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754735107319&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CacheMessageDelegate {
    private final CacheManager l1CacheManager;
    private final ObjectMapper objectMapper;
    private final String serverId = UUID.randomUUID().toString();
    
    public void handleMessage(String message, String channel) {
        try {
            // 1. JSON 파싱
            CacheMessage cacheMessage = objectMapper.readValue(
                message, CacheMessage.class);
            
            // 2. 자기 메시지 필터링 (무한 루프 방지!)
            if (serverId.equals(cacheMessage.getSenderId())) {
                log.debug(&quot;Ignoring self message&quot;);
                return;
            }
            
            // 3. L1 캐시에서만 무효화 (L2는 이미 최신 상태)
            Cache l1Cache = l1CacheManager.getCache(cacheMessage.getCacheName());
            if (l1Cache != null) {
                l1Cache.evict(cacheMessage.getKey());
            }
            
        } catch (Exception e) {
            // 메시지 처리 실패해도 다음 메시지는 계속 처리
            log.error(&quot;Failed to handle cache message&quot;, e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1754735159364&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record CacheMessage(
	String senderId, 
    String cacheName, 
    String key, 
    Object value) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub을 사용하기 위한 준비는 다 끝났다.&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;메시지 퍼블리싱은 다양한 방법으로 구현해볼 수 있겠지만, 여기서는 데코레이터 패턴을 통해 구현할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PubSubCache라는 Cache구현체로 캐시를 감싸서 메시지를 퍼블리싱하도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1754735549639&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class PubSubCache implements Cache {

    private final Cache delegate;  // 실제 캐시
    private final MessagePublisher publisher;
    
    @Override
    public void evict(Object key) {
        // 1. PubSub 메시지 발행
        publishEvictMessage(key);
        
        // 2. 실제 캐시에서 제거
        delegate.evict(key);
    }
    
    private void publishEvictMessage(Object key) {
        publisher.publish(
        	&quot;cache:evict&quot;, // 채널 이름
            new CacheMessage(MessageConfig.getSenderId(), delegate.getName(), key.toString()));
    }
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 매니저 또한 PubSubCacheManager로 감싸, 캐시 매니저가 PubSubCache를 통해 메시지를 발행할 수 있도록 구성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754735734707&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PubSubCacheManager implements CacheManager {

  private final CacheManager cacheManager;
  private final MessagePublisher messagePublisher;

  public PubSubCacheManager(
      final CacheManager cacheManager, final MessagePublisher messagePublisher) {
    this.cacheManager = cacheManager;
    this.messagePublisher = messagePublisher;
  }

  @Override
  public Cache getCache(final String name) {
    return new PubSubCache(cacheManager.getCache(name), messagePublisher);
  }

  @Override
  public Collection&amp;lt;String&amp;gt; getCacheNames() {
    return cacheManager.getCacheNames();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 CacheConfig에서 데코레이터로 감싸진 캐시 매니저를 사용하도록 설정하면 끝이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754735688910&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class CacheConfig {
    
    @Bean
    @Primary
    public CacheManager cacheManager(
        @Qualifier(&quot;l1CacheManager&quot;) CacheManager l1,
        @Qualifier(&quot;l2CacheManager&quot;) CacheManager l2,
        CircuitBreaker circuitBreaker,
        MessagePublisher publisher) {
        
        // 1. 기본 Multi-Level 캐시
        CacheManager composite = new CustomCompositeCacheManager(l1, l2);
        
        // 2. PubSub 기능 추가
        CacheManager withPubSub = new PubSubCacheManager(composite, publisher);
        
        return withPubSub;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지로 도식화하면 아래와 같은 플로우처럼 메시지 발행 및 구독이 이뤄질 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메시지 발행&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1481&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IJLpY/btsPNInV7Jc/Ku1qhdd2csP9eah9Jxk6tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IJLpY/btsPNInV7Jc/Ku1qhdd2csP9eah9Jxk6tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IJLpY/btsPNInV7Jc/Ku1qhdd2csP9eah9Jxk6tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIJLpY%2FbtsPNInV7Jc%2FKu1qhdd2csP9eah9Jxk6tk%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; alt=&quot;메시지 발행 플로우&quot; loading=&quot;lazy&quot; width=&quot;1481&quot; height=&quot;560&quot; data-origin-width=&quot;1481&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메시지 구독&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTRxdI/btsPMO3sjpk/b15OKNA8AMYSTsHbNQxCm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTRxdI/btsPMO3sjpk/b15OKNA8AMYSTsHbNQxCm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTRxdI/btsPMO3sjpk/b15OKNA8AMYSTsHbNQxCm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTRxdI%2FbtsPMO3sjpk%2Fb15OKNA8AMYSTsHbNQxCm0%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; alt=&quot;메시지 구독자는 메시지를 받아 캐시를 제거하게 된다.&quot; loading=&quot;lazy&quot; width=&quot;1639&quot; height=&quot;625&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;625&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성과와 교훈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 성과&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;초기 상태&lt;/b&gt;:&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 500ms ~ 1초 &lt;br /&gt;&lt;b&gt;Redis 캐시 적용&lt;/b&gt;:&amp;nbsp; &amp;nbsp; &amp;nbsp;50 ~ 100ms (10배 개선) &lt;br /&gt;&lt;b&gt;로컬 캐시 추가&lt;/b&gt;:&amp;nbsp; &amp;nbsp; &amp;nbsp; ~10ms (추가 5~10배 개선)&lt;br /&gt;&lt;br /&gt;&lt;b&gt;최종 개선율&lt;/b&gt;:&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;50~100배  &lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Trade-off&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Pub/Sub은 메시지 순서를 보장하지 않고, 메시지가 유실될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 캐시의 순서가 바뀔 정도로 업데이트가 잦지 않았고, TTL로 최종 일관성이 보장할 수 있었기 때문에 Redis Pub/Sub으로 충분하다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며: 여러분의 상황에 맞는 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 Redis Pub/Sub을 선택한 이유는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 Redis를 사용 중이었고&lt;/li&gt;
&lt;li&gt;구현이 간단해 빠르게 도입할 수 있었으며,&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kafka&lt;/b&gt;: 내구성/재처리/순서 중요, 소비 지연 허용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;: 복잡한 라우팅이 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hazelcast&lt;/b&gt;: JVM 통합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질리도록 듣는 얘기겠지만 기술 선택에 정답은 없다. 현재 상황과 제약사항을 잘 파악하고, Trade-off를 고려해서 &lt;b&gt;&quot;지금 우리에게 가장 적합한&quot;&lt;/b&gt; 기술을 선택하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Redis</category>
      <category>java</category>
      <category>pub/sub</category>
      <category>redis</category>
      <category>Spring</category>
      <category>로컬 캐시</category>
      <category>캐시 계층</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/257</guid>
      <comments>https://myvelop.tistory.com/257#entry257comment</comments>
      <pubDate>Sat, 9 Aug 2025 19:55:20 +0900</pubDate>
    </item>
    <item>
      <title>실무에서 @Transactional을 제거했더니 성능이 2배 향상된 이유</title>
      <link>https://myvelop.tistory.com/256</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 @Transactional(readOnly = true)는 DirtyChecking 모드를 Manual 모드로 바꿔줘 성능 최적화를 위해 사용된다고 알려져 있지만, 오히려 불필요한 JDBC 호출로 인해 성능이 저하될 수 있다. 이 글은 Elastic APM을 통해 확인한 실제 호출 로그를 바탕으로 readOnly 트랜잭션이 성능에 어떤 영향을 주는지 분석하고, 실무적으로 더 나은 대안을 제시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아는 사람만 아는 @Transactional의 비밀&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 API는 @Transactional(readOnly = true)로 선언된 Service 메소드를 호출한다. Elastic APM을 통해 추적한 결과, 해당 API는 단 2개의 SELECT 쿼리만 실행함에도 불구하고, 여러 번 JDBC 호출이 발생하는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/plYSZ/btsNQEVfeHD/jdpULK0mYb46QShNXcmY1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/plYSZ/btsNQEVfeHD/jdpULK0mYb46QShNXcmY1K/img.png&quot; data-alt=&quot;쿼리는 분명 2개만 실행했지만 여러 기능들이 호출되는 것을 확인할 수 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/plYSZ/btsNQEVfeHD/jdpULK0mYb46QShNXcmY1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FplYSZ%2FbtsNQEVfeHD%2FjdpULK0mYb46QShNXcmY1K%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; alt=&quot;@Transactional 위해 호출되는 여러 JDBC 기능들을 Elastic APM으로 확인&quot; loading=&quot;lazy&quot; width=&quot;1180&quot; height=&quot;544&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쿼리는 분명 2개만 실행했지만 여러 기능들이 호출되는 것을 확인할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;set_option&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 JPA에서 @Transactional을 사용하면서 &lt;b&gt;트랜잭션을 관리하기 위해 내부적으로 실행되는 설정(set_option)&lt;/b&gt; 때문이다. 이 과정에서 다음과 같은 JDBC 호출이 자동으로 발생하며, 이들은 성능에도 영향을 줄 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Set&lt;span&gt;&amp;nbsp;&lt;/span&gt;transaction access mode 'read-only'&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional(readOnly = true)는 JDBC 레벨에서 &lt;b&gt;Connection을 read-only로 설정&lt;/b&gt;하도록 만든다. 실제 구현 레벨에서는 아래와 같은 코드를 호출한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746707718802&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Connection.setReadOnly(true)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 MySQL, PostgreSQL 등 일부 DB에서 실행 계획 최적화에 도움이 될 수 있지만 대부분의 JDBC 드라이버는 이 설정이 필수는 아니며, &lt;b&gt;추가적인 네트워크 왕복을 유발&lt;/b&gt;할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. autocommit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 트랜잭션을 시작할 때 JPA의 FlushMode도 함께 설정된다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기본값이 FlushModeType.AUTO로 지정되어 있으며, 이 설정은 트랜잭션 commit 시점이나 JPQL 실행 시 flush를 자동으로 발생시키는 설정이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746707637965&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Session extends SharedSessionContract, EntityManager {

	/**
	 * Set the current {@link FlushModeType JPA flush mode} for this session.
	 * &amp;lt;p&amp;gt;
	 * &amp;lt;em&amp;gt;Flushing&amp;lt;/em&amp;gt; is the process of synchronizing the underlying persistent
	 * store with persistable state held in memory. The current flush mode determines
	 * when the session is automatically flushed.
	 *
	 * @param flushMode the new {@link FlushModeType}
	 *
	 * @see #setHibernateFlushMode(FlushMode) for additional options
	 */
	@Override
	void setFlushMode(FlushModeType flushMode);
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1746707463508&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Session.setHibernateFlushMode(FlushMode.AUTO)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Rollback&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Rollback은 예외 발생 시 실행된다고 생각하기 쉽지만, DB 트랜잭션은 &lt;b&gt;명시적인 commit 또는 rollback으로만 종료&lt;/b&gt;되기 때문에 예외가 발생하지 않아도 Rollback을 호출할 수도 있다. 예를 들어, @Transactional(readOnly = true)이 적용된 경우, Spring은 변경사항이 없다고 판단하고 &lt;b&gt;명시적으로 rollback()을 호출&lt;/b&gt;하여 트랜잭션을 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 실제 DB에는 아무런 변경이 없더라도, JDBC 드라이버 수준에서 rollback 요청이 발생한다는 의미이며, 역시 &lt;b&gt;불필요한 비용&lt;/b&gt;이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Transactional 제거 시의 변화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 소개한 API에서 @Transactional(readOnly = true)을 제거하고 실행해봤다. &lt;b&gt;&lt;b&gt;Set&lt;span&gt;&amp;nbsp;&lt;/span&gt;transaction access mode 'read-only',&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;autocommit,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Rollback&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 호출이 발생하지 않게 되었다. 그 결과, API 응답 속도가 절반 가까이 줄어든 것을 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmOK5O/btsNPCqp7J4/sDCGLicgwUKMscKwXkIok1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmOK5O/btsNPCqp7J4/sDCGLicgwUKMscKwXkIok1/img.png&quot; data-alt=&quot;@Transactional을 뺀 것만으로도 속도가 빨라진다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmOK5O/btsNPCqp7J4/sDCGLicgwUKMscKwXkIok1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmOK5O%2FbtsNPCqp7J4%2FsDCGLicgwUKMscKwXkIok1%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; alt=&quot;@Transactional을 제거했더니 속도가 2배가 되었다.&quot; loading=&quot;lazy&quot; width=&quot;1180&quot; height=&quot;336&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@Transactional을 뺀 것만으로도 속도가 빨라진다.&lt;/figcaption&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;&lt;b&gt;흔히 @Transactional(readOnly = true)가 &lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;dirtyChecking 모드를 Manual모드로 바꿔 줄 수 있기 때문에 application 성능 개선에 도움이 된다고 알려져 있지만, 실제로는 아예 트랜잭션 자체를 제거하는 것이 오히려 더 큰 성능 향상을 가져왔다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Replication 환경에서는 주의!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 성능을 위해 @Transactional을 제거하는 접근은 DB Replication을 환경에서는 문제가 될 수 있다. DB의 처리량을 증가시키고, 고가용성을 확보하기 위해 Replication을 구성한 시스템에서는 보통 AbstractRoutingDataSource을 사용하여 트랜잭션이 읽기 전용인지 여부를 따져 Primary 또는 Replica DB로 요청을 분기한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746709321462&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;final AbstractRoutingDataSource dataSourceRouter =
        new AbstractRoutingDataSource() {
          @Override
          protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                ? REPLICA_DATASOURCE_KEY
                : PRIMARY_DATASOURCE_KEY;
          }
        };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 TransactionSynchronizationManager.isCurrentTransactionReadOnly()를 통해 분기를 처리하기 때문에, &lt;b&gt;트랜잭션 자체를 제거하면 읽기 요청이 모두 Master로 몰리는 부작용&lt;/b&gt;이 발생할 수 있다. 이는 Replica 부하 분산이 깨지고, 전체 시스템 성능에 악영향을 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해, &lt;b&gt;카카오페이 테크 블로그&lt;/b&gt;에서는 다음과 같은 형태의 커스텀 어노테이션을 제안했다.&lt;/p&gt;
&lt;pre id=&quot;code_1746709743494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public @interface ReadOnlyTransactional {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;SUPPORTS 전파 수준은 메서드가 트랜잭션 없이 단독으로 호출되면 &lt;b&gt;트랜잭션을 생성하지 않으며&lt;/b&gt;, 상위에 트랜잭션이 있을 경우에는 &lt;b&gt;그 트랜잭션에 참여&lt;/b&gt;한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #060b11; text-align: start;&quot;&gt;즉, @ReadOnlyTransactional만 적용된 메소드에서는 JPA 트랜잭션이 시작되지 않아 &lt;b&gt;setReadOnly, rollback 등 불필요한 JDBC 호출을 피할 수 있고&lt;/b&gt;, 동시에 &lt;b&gt;AbstractRoutingDataSource 기반의 Replica 라우팅 로직도 정상 동작&lt;/b&gt;하게 된다.&lt;/span&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;반면, 상위 계층에서 이미 @Transactional(REQUIRED) 트랜잭션이 시작된 경우에는 @ReadOnlyTransactional은 영향을 주지 않기 때문에, &lt;b&gt;기존 트랜잭션 구조에 유연하게 섞어 사용할 수 있는 장점&lt;/b&gt;도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lazy Loading을 사용할 때도 주의!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 API에서 Lazy Loading을 사용하고 있는데, @Transactional(readOnly = true)를 제거한다면 무슨 일이 벌어질까? API를 호출할 때마다 아래 에러가 발생할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1746751772592&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.hibernate.LazyInitializationException: could not initialize proxy - no Session&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 트랜잭션을 시작하지 않으면 JPA는 영속성 컨텍스트를 사용할 수 없기 때문에 Lazy Loading 또한 사용할 수 없게 된다. 기술적인 이유로 어쩔 수 없이 Lazy Loading을 사용해야 하는 API라면 @Transactional을 붙여 영속성 컨텍스트를 사용할 수 있는 환경을 만들어야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional(readOnly = true)는 흔히 &lt;b&gt;읽기 성능 최적화&lt;/b&gt;를 위해 사용하는 설정이라고 알려져 있지만, 실제로는 JDBC 수준에서 Set readOnly, AutoCommit, Rollback 등 &lt;b&gt;불필요한 호출이 추가적으로 발생&lt;/b&gt;할 수 있다는 사실을 파악했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 @ReadOnlyTransactional과 같이 propagation을 SUPPORTS로 설정한 어노테이션을 도입하는 방법을 제시하여 트랜잭션의 오버헤드는 줄이되, Replica 라우팅 로직은 유지하는 방법을 알아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은, &lt;b&gt;서비스의 트랜잭션 전략을 일률적으로 가져가지 않고, API의 목적과 시스템 구조에 따라 유연하게 적용하는 것&lt;/b&gt;이다. 불필요한 @Transactional 사용이 오히려 성능 병목이 될 수 있다는 점을 기억하고, 필요에 따라 readOnly 분리 전략을 고민해보는 것이 실무에서의 핵심이다.&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;&amp;nbsp;&lt;/p&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://tech.kakaopay.com/post/jpa-transactional-bri/#%EC%8B%A4%EC%A0%9C%EB%A1%9C-set_option%EA%B3%BC-commit%EC%9D%B4-%EC%84%B1%EB%8A%A5%EC%97%90-%EC%98%81%ED%96%A5%EC%9D%84-%EB%AF%B8%EC%B9%A0%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JPA Transactional 잘 알고 쓰고 계신가요?&lt;/a&gt;, 카카오 pay tech 블로그&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/43742617/spring-transactional-read-only-mode-rollback-behaviour&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring @Transactional read-only mode rollback behavior&lt;/a&gt;, stack overflow&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@Transactional</category>
      <category>jdbc</category>
      <category>JPA</category>
      <category>MySQL</category>
      <category>set_option</category>
      <category>Spring</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/256</guid>
      <comments>https://myvelop.tistory.com/256#entry256comment</comments>
      <pubDate>Thu, 8 May 2025 22:38:02 +0900</pubDate>
    </item>
    <item>
      <title>마지막 글또</title>
      <link>https://myvelop.tistory.com/255</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;시작할 때의 마음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 9기가 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니티에 속해 다른 사람들과 교류하고, 글을 쓰며 발전해나가는 내 모습이 마음에 들었다. 그래서 10기에도 참여했다.&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;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;한 달에 한 명씩 알아가기&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;일주일 앞서서 글 제출하기&lt;/span&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;글또 9기 때처럼 매일 야근에 시달리고 있는 시기도 아니었기에, 이번엔 좀 더 여유롭게 할 수 있으리라 생각했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;게다가, 이번엔 회사 동료분과 함께 하는 활동이었기에 심리적 부담도 덜햇다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;'동료가 커뮤니티 활동에 참여하면 나도 껴야지~'&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;'다른 활동에 참여할 때, 회사 분들에게 권유도 해봐야지~'&lt;/span&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;&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;&quot;지금도 벌여 놓은 일이 많은데, 글또 커뮤니티 활동에 제대로 참여할 수 있을까?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원 전에 잠깐 스쳐 지나갔던 이 생각이, 현실이 되어버린 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lewYw/btsM0tuWhW3/mPtCZKns8CxGnSqKyFVJF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lewYw/btsM0tuWhW3/mPtCZKns8CxGnSqKyFVJF0/img.png&quot; data-alt=&quot;그 걱정은 현실이 되어버렸고...&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lewYw/btsM0tuWhW3/mPtCZKns8CxGnSqKyFVJF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlewYw%2FbtsM0tuWhW3%2FmPtCZKns8CxGnSqKyFVJF0%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;745&quot; height=&quot;119&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;119&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; 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;그 와중에 스터디 2개를 운영했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7월부터 12월까지는 오픈소스 컨트리뷰션,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12월부터 1월까지는 토스 러너스 하이,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1월부터 2월까지는 Nexters에서 PM으로 활동을 했다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 하반기부터 올해 초는 내게&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;피버 타임&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biPe8J/btsM0pF1a3X/bPNUdZO2JRkJp3iB3kjYN0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biPe8J/btsM0pF1a3X/bPNUdZO2JRkJp3iB3kjYN0/img.jpg&quot; data-alt=&quot;콤보가 연속되면 피버타임이 온다 (출처: https://biz.heraldcorp.com/article/10300285)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biPe8J/btsM0pF1a3X/bPNUdZO2JRkJp3iB3kjYN0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiPe8J%2FbtsM0pF1a3X%2FbPNUdZO2JRkJp3iB3kjYN0%2Fimg.jpg&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;350&quot; height=&quot;622&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;콤보가 연속되면 피버타임이 온다 (출처: https://biz.heraldcorp.com/article/10300285)&lt;/figcaption&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;나에게 주어진 거의 모든 시간을 개발에 쏟았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머릿속에 뒤엉켜 있던 개발 지식들이 하나씩 퍼즐처럼 맞춰지며, 개발 지식에 한해 내 머리가 스펀지와 같은 흡수력을 가지는 시기였다. &lt;b&gt;성장 경험치 2배 이벤트&lt;/b&gt;. 그 기회를 놓치기 싫어서, 할 수 있는 활동에 전부 도전해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공교롭게도&lt;/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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;'이 활동만 끝나면 글또 커뮤니티에 집중해보자'는 생각만 반복하며 일을 계속 벌였 놓았고, 결국&amp;nbsp;&lt;/span&gt;글또 커뮤니티 활동은 자연스럽게 우선순위에서 밀려났다. 심지어 이번 기수에서는 커피챗 한 번조차 하지 못했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, &quot;글 제출 일주일 앞서기&quot; &amp;amp; &quot;한달에 한 명씩 알아가기&quot; 라는 목표들은 결국 실패로 남았다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;잘 즐기다 갑니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 첫번째 목표였던 &quot;즐기기&quot;는, 이건 확실히 달성했다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nexters 활동 당시 너무 바쁘던 시기엔 패스 2회를 쓰기도 했지만, &lt;b&gt;한 번도 글을 미제출한 적은 없었다.&lt;/b&gt; 자투리 시간을 쪼개 글을 쓸 땐 &lt;b&gt;부담&lt;/b&gt;보다는 되려 즐거움을 느꼈다. 내가 생각하는 &quot;좋은 글&quot;을 쓰기 위해 고민하고, 단어 하나하나 써내려가는 시간이 즐거웠다.&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅ 1회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/244&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;글또 10기 시작!&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 2&lt;/span&gt;회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/239&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarQube로 코드 품질 관리하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 3&lt;/span&gt;회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| &lt;a href=&quot;https://myvelop.tistory.com/246&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;REST API 문서 자동화로 업무 효율 극대화하는 방법&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 4&lt;/span&gt;회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| &lt;a href=&quot;https://myvelop.tistory.com/247&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;나만의 Swagger UI 서버, 쿠버네티스에서 운영하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 5&lt;/span&gt;회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/248&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Nexters 26기 지원 및 면접 후기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;⏩&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 6&lt;/span&gt;회차 패스&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 7&lt;/span&gt;회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/250&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HikariCP를 이해하면 풀 사이즈 설정이 보인다&lt;/a&gt; | &lt;b&gt;큐레이션&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;8&lt;/span&gt;회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/251&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024년 회고&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;⏩&lt;/span&gt;&amp;nbsp;9&lt;/span&gt;회차 패스&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;10회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;기술과 관리 사이에서 (넥스터즈 26기 후기)&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;11회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;|&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;a href=&quot;https://myvelop.tistory.com/253&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;당신의 메모리는 안녕하십니까?&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅ 12회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| 마지막 글또&lt;/span&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고마운 커뮤니티, 감사합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(글을 다 썼다는 가정하에) 돈 한 푼 내지 않고 이렇게 좋은 경험을 할 수 있는 커뮤니티가 또 있을까?&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;바쁜 와중에도 글을 꾸준히 쓸 수 있었던 이유는 글또 덕분이었다. 글쓰기가 나에게 변화를 만들어준다는 걸 느꼈기 때문일 것이다. 처음 9기를 시작할 땐 블로그에서 완전히 손을 놓고 있었는데, 이제는 좋은 글 소재가 떠오르면 블로그부터 열게 된다. (물론 작성하다 말고 쌓인 글도 많지만&amp;hellip;)&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;특히 큐레이션 좋은 동기부여였다. 동료 개발자들에게 내가 생각하는 &quot;좋은 글&quot;을 보여줄 수 있다는 게 좋았다. 큐레이션에 선정됐을 땐 그렇게 뿌듯할 수가 없었다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1447&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Tqeb/btsM2f93cqd/7abUzs5Pe53Lvs0oV0coZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Tqeb/btsM2f93cqd/7abUzs5Pe53Lvs0oV0coZ0/img.png&quot; data-alt=&quot;큐레이션은 못 참지~&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Tqeb/btsM2f93cqd/7abUzs5Pe53Lvs0oV0coZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2Tqeb%2FbtsM2f93cqd%2F7abUzs5Pe53Lvs0oV0coZ0%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; alt=&quot;큐레이션 선정&quot; loading=&quot;lazy&quot; width=&quot;1447&quot; height=&quot;670&quot; data-origin-width=&quot;1447&quot; data-origin-height=&quot;670&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스 러너스 하이를 통해 나의 커리어를 성장시킬 수 있는 기회를 얻은 것도 글또 덕분이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzjk6a/btsM2W3cweK/kO2BQYON3KfQsTVRdkAepK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzjk6a/btsM2W3cweK/kO2BQYON3KfQsTVRdkAepK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzjk6a/btsM2W3cweK/kO2BQYON3KfQsTVRdkAepK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdzjk6a%2FbtsM2W3cweK%2FkO2BQYON3KfQsTVRdkAepK%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; alt=&quot;글또 - 토스 러너스 하이 홍보&quot; loading=&quot;lazy&quot; width=&quot;745&quot; height=&quot;591&quot; data-origin-width=&quot;745&quot; data-origin-height=&quot;591&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;넥스터즈에서 PM 활동을 할 때는 글또 자유 홍보 채널에서 유저 리서치 설문을 올린 적이 있었는데, 정말 많은 분들이 참여해주신 덕분에 이틀 만에 100명 이상의 응답을 받았고 빠르게 기획을 진행할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vlMTd/btsMZ6NvnJV/b5e5ONa8OSvGFgasxJHjWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vlMTd/btsMZ6NvnJV/b5e5ONa8OSvGFgasxJHjWk/img.png&quot; data-alt=&quot;유저 리서치&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vlMTd/btsMZ6NvnJV/b5e5ONa8OSvGFgasxJHjWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvlMTd%2FbtsMZ6NvnJV%2Fb5e5ONa8OSvGFgasxJHjWk%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; alt=&quot;유저 리서치&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;446&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;446&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;&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;운영진 분들, 그리고 변성윤님!&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;</description>
      <category>대외활동/글또</category>
      <category>개발자</category>
      <category>글또10기</category>
      <category>마지막 글또</category>
      <category>블로그</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/255</guid>
      <comments>https://myvelop.tistory.com/255#entry255comment</comments>
      <pubDate>Sun, 30 Mar 2025 15:14:21 +0900</pubDate>
    </item>
    <item>
      <title>당신의 메모리는 안녕하십니까?</title>
      <link>https://myvelop.tistory.com/253</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;당신의 애플리케이션은 안전한가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&quot;혹시 서비스 운영 중 아무런 이상이 없어 보이던 시스템이 갑자기 느려지고, 응답 시간이 증가하며, 결국 장애로 이어진 경험이 있지는 않나요? 우리가 흔히 간과하는 작은 코드 한 줄이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;애플리케이션 성능 저하와 서버 장애를 초래할 수 있다는 사실&lt;/b&gt;, 알고 계셨나요?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제는 &lt;b&gt;대부분 메모리 관리의 작은 실수에서 시작된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 실행되는 동안 메모리는 끊임없이 할당되고 해제된다. 특히 &lt;b&gt;Java의 JVM 메모리 관리 방식&lt;/b&gt;을 제대로 이해하지 못하면, 시스템 성능 저하와 장애를 초래하는 &lt;b&gt;메모리 누수(memory leak)&lt;/b&gt;가 발생하여 시스템 성능이 점점 저하될 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3sA7i/btsNzV3FIPo/9Igb0I5NphKUtL00KiePZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3sA7i/btsNzV3FIPo/9Igb0I5NphKUtL00KiePZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3sA7i/btsNzV3FIPo/9Igb0I5NphKUtL00KiePZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3sA7i%2FbtsNzV3FIPo%2F9Igb0I5NphKUtL00KiePZK%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;682&quot; height=&quot;266&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그래프는 JVM 메모리 사용량을 보여준다. 특정 시점에&amp;nbsp;&lt;b&gt;메모리 사용량이 급격히 증가&lt;/b&gt;하고 있다. 많은 객체가 생성되었고, 그 객체들이&amp;nbsp; 메모리에서 해제되지 않고 계속 남아있어 메모리 점유하고 있다&lt;b&gt;.&lt;/b&gt;&amp;nbsp;이는 성능 저하와 장애로 이어질 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;720&quot; data-start=&quot;575&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 &lt;b&gt;JVM 메모리 구조, 메모리 누수가 발생하는 원인, 그리고 이를 방지하는 방법&lt;/b&gt;에 대해 다뤄보려고 한다. JVM 메모리를 이해하고 최적화하면 성능 저하와 장애를 예방할 수 있다. 지금부터 하나씩 살펴보자!&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JVM의 메모리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM의 메모리는 크게 Heap, Stack, PC Register, Native Method Stack 등이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyHMUC/btsMKfWTeha/rEksrVboiKkHT4rkyKq8sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyHMUC/btsMKfWTeha/rEksrVboiKkHT4rkyKq8sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyHMUC/btsMKfWTeha/rEksrVboiKkHT4rkyKq8sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdyHMUC%2FbtsMKfWTeha%2FrEksrVboiKkHT4rkyKq8sK%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; alt=&quot;Runtime Data Area&quot; loading=&quot;lazy&quot; width=&quot;885&quot; height=&quot;646&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 Heap 영역은 JVM 메모리 중 가장 크고, GC의 주요 대상이기 때문에 최적화 효과가 가장 크다. Heap을 잘 튜닝하면 GC 오버헤드를 줄이고 애플리케이션의 응답 속도를 개선할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 설정 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap의 크기를 적절히 설정해줘야 Minor GC가 너무 빈번하게 발생하지 않는다. 반면, Heap을 너무 크게 설정하면 GC 시 애플리케이션이 멈추는 시간이 길어질 수 있으므로, Heap의 크기를 적절히 설정해줘야 한다.&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;JVM에서 사용할 수 있는 메모리 설정 옵션을 몇 가지만 소개하자면 아래와 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 91.0465%; height: 239px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 20px;&quot;&gt;-Xms{size}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 20px;&quot;&gt;JVM의 최소 메모리 크기를 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 20px;&quot;&gt;-Xmx{size}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 20px;&quot;&gt;JVM의 최대 메모리 크기를 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 20px;&quot;&gt;-XX:InitialRAMPercentage={size}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 20px;&quot;&gt;전체 RAM의 {size}%를 Heap 크기로 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 19px;&quot;&gt;-XX:MaxRAMPercentage={size}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 19px;&quot;&gt;전체 RAM의 {size}%를 최대 Heap 크기로 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 20px;&quot;&gt;-XX:NewRatio={ratio}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 20px;&quot;&gt;New 영역과 Old 영역의 비율&lt;br /&gt;-XX:NewRatio=1로 설정하면 {New}:{Old} = 1:1&lt;br /&gt;-XX:NewRatio=2로 설정하면 {New}:{Old} = 1:2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 20px;&quot;&gt;-XX:NewSize={size}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 20px;&quot;&gt;New 영역의 크기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 20px;&quot;&gt;-XX:SurvivorRatio={ratio}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 20px;&quot;&gt;Eden 영역과 Survivor 영역의 비율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 60px;&quot;&gt;-XX:PermSize={size}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 60px;&quot;&gt;JVM이 사용하는 초기 메모리 크기 지정. 영구적으로 사용.&lt;br /&gt;JDK 8부터 PermGen이 제거되고 Meatspace로 대체되었기 때문에 무시된다. 주의!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 45.5892%; height: 40px;&quot;&gt;-XX:MaxPermSize={size}&lt;/td&gt;
&lt;td style=&quot;width: 56.9933%; height: 40px;&quot;&gt;JVM이 사용하는 최대 메모리 크기 지정. 영구적으로 사용&lt;br /&gt;-XX:PermSize={size}와 마찬가지로 무시된다. 주의!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXL9Li/btsMLPRLezS/kHWNRn8mCU4vCukfkuMCL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXL9Li/btsMLPRLezS/kHWNRn8mCU4vCukfkuMCL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXL9Li/btsMLPRLezS/kHWNRn8mCU4vCukfkuMCL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXL9Li%2FbtsMLPRLezS%2FkHWNRn8mCU4vCukfkuMCL0%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; alt=&quot;메모리 설정 옵션에 따른 메모리 할당 영역&quot; loading=&quot;lazy&quot; width=&quot;1302&quot; height=&quot;770&quot; data-origin-width=&quot;1302&quot; data-origin-height=&quot;770&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;아래와 같이 java 명령어에 옵션으로 실행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1741951101216&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ java -Xms1024m -Xmx1024m -jar myapp.jar&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1741950971965&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ java -XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=50.0 -jar myapp.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 위와 같이 메모리의 초기 값과 Max 값을 동일하게 맞춰주는 것이 좋다. 이유는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 메모리 사이즈와 최대 메모리 사이즈가 다르게 설정하면, Heap 메모리가 부족해질 때마다 추가적으로 메모리를 할당받게 된다. Heap 메모리 크기를 동적으로 조정하는 과정에서 GC가 발생하며, 이는 성능에 악영향을 미친다.&lt;/li&gt;
&lt;li&gt;또한, 초기 메모리 사이즈와 최대 메모리 사이즈를 동일하게 해야 메모리 할당과 해제에 따른 변동성이 줄어 애플리케이션의 성능이 더 예측 가능해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Heap의 사이즈를 어떻게 설정하느냐에 따라 GC의 발생 횟수와 GC의 수행 시간이 달라지므로, 신중하게 설정해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 크기가 클 때, GC 발생 횟수는 감소하나 GC 수행 시간이 늘어난다.&lt;/li&gt;
&lt;li&gt;메모리 크기가 작을 때, GC 수행 시간이 감소하나 GC 발생 횟수가 증가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;b&gt;Minor GC가 빈번하게 발생&lt;/b&gt;하는 상황이라고 가정해보자. Minor GC가 너무 빠르게 발생하면, Young 영역에서 수명이 짧은 객체를 충분히 정리하지 못하고 &lt;b&gt;Old Generation으로 더 많은 객체가 승격&lt;/b&gt;된다. 이로 인해 Old 영역이 빠르게 채워지고, 결국 Full GC가 자주 실행되어 애플리케이션 성능 저하를 초래한다. 따라서, 이런 경우 Young 영역을 확장하여 Minor GC 빈도를 줄이는 것이 필요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1742022092082&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;$ java -Xms1024m -Xmx1024m -XX:NewSize=256m -jar myapp.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GC 설정 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM에서는 여러 가지 GC 알고리즘을 제공한다. 각각의 GC는 메모리 관리 방식과 성능 특성이 다르므로, 프로젝트의 특성에 맞는 GC를 선택하는 것이 중요하다. 예를 들어, 낮은 지연 시간이 중요한 실시간 애플리케이션에서는 Shenandoah GC가 적합할 수 있으며, 대규모 서비스에서는 ZGC나 G1GC가 효율적일 수 있다. 따라서, 단순히 기본 GC를 사용하는 것이 아니라 애플리케이션의 성능 요구 사항과 시스템 환경을 고려하여 최적의 GC 알고리즘을 적용해야 한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 90px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.8682%; height: 18px;&quot;&gt;Serial GC&lt;/td&gt;
&lt;td style=&quot;width: 31.0077%; height: 18px;&quot;&gt;-XX:+UesSerialGC&lt;/td&gt;
&lt;td style=&quot;width: 46.124%; height: 18px;&quot;&gt;단일 스레드, 작은 메모리 환경.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.8682%; height: 18px;&quot;&gt;Parallel GC&lt;/td&gt;
&lt;td style=&quot;width: 31.0077%; height: 18px;&quot;&gt;-XX:+UseParallelGC&lt;/td&gt;
&lt;td style=&quot;width: 46.124%; height: 18px;&quot;&gt;멀티 스레드, 높은 처리량.&lt;br /&gt;JDK8에서 default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.8682%; height: 18px;&quot;&gt;G1GC&lt;/td&gt;
&lt;td style=&quot;width: 31.0077%; height: 18px;&quot;&gt;-XX:UseG1GC&lt;/td&gt;
&lt;td style=&quot;width: 46.124%; height: 18px;&quot;&gt;Region 단위의 관리로 지연 시간을 줄임.&lt;br /&gt;Full GC의 시간을 수백 밀리초 이하로 관리하고 싶을 때 사용.&lt;br /&gt;JDK11에서 default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.8682%; height: 18px;&quot;&gt;ZGC&lt;/td&gt;
&lt;td style=&quot;width: 31.0077%; height: 18px;&quot;&gt;-XX:+UseZGC&lt;/td&gt;
&lt;td style=&quot;width: 46.124%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #4d5256; text-align: start;&quot;&gt;초저지연 GC. 대용량 메모리에서 Stop-The-World 시간을 최대한 줄이는 것이 목표.&lt;br /&gt;10GB 이하의 메모리에서 비효율적일 수 있음. 50GB 이상일 경우 매우 적합하다고 함.&lt;br /&gt;&lt;/span&gt;JDK11+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.8682%; height: 18px;&quot;&gt;Shenandoah GC&lt;/td&gt;
&lt;td style=&quot;width: 31.0077%; height: 18px;&quot;&gt;-XX:UseShenandoahGC&lt;/td&gt;
&lt;td style=&quot;width: 46.124%; height: 18px;&quot;&gt;작은 GC를 여러 번 실행하여 낮은 지연시간을 보장. JDK12+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Memory Metrics&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 사용량이 급격히 증가하지 않는지, GC가 과도하게 실행되고 있지는 않는지, 로컬 캐시 사용이 지나쳐 메모리가 부족해지지는 않는지 판단하려면, 관련 지표를 지속적으로 모니터링해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Actuator에서 Memory Metrics 확인해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트에서 actutator를 통해 jvm 메모리 관련 메트릭을 확인해볼 수 있다. 아래와 같이 의존성을 추가해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1742175235482&quot; class=&quot;stylus&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-actuator&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yaml 설정 파일에서 metrics를 활성화해주자.&lt;/p&gt;
&lt;pre id=&quot;code_1742175293932&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: metrics&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 애플리케이션을 실행하고 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;/actuator/metrics&lt;/span&gt;&lt;/b&gt; 경로에 접속하면 아래의 메모리 관련 메트릭과 함께 다양한 메트릭을 확인해볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1741951660045&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;names&quot;: [
    ...
    &quot;jvm.memory.committed&quot;,
    &quot;jvm.memory.max&quot;,
    &quot;jvm.memory.usage.after.gc&quot;,
    &quot;jvm.memory.used&quot;,
    ...
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jvm.memory.used&lt;/span&gt;&lt;/b&gt;를 더 상세하고 확인하고 싶다면 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;/actuator/metrics/jvm.memory.used&lt;/span&gt;&lt;/b&gt; 경로에 접속해보자. 여기서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;availableTags&lt;/b&gt;&lt;/span&gt;를 확인해보면 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;area&lt;/span&gt;&lt;/b&gt; 태그의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;heap&lt;/span&gt;&lt;/b&gt;과 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;nonheap&lt;/span&gt;&lt;/b&gt;. &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;id&lt;/span&gt;&lt;/b&gt; 태그의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;G1 Eden Space&lt;/span&gt;&lt;/b&gt;, &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;G1 Old Gen&lt;/span&gt;&lt;/b&gt; 등 메모리 영역이 분류되어 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1741951914005&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;jvm.memory.used&quot;,
  &quot;description&quot;: &quot;The amount of used memory&quot;,
  &quot;baseUnit&quot;: &quot;bytes&quot;,
  &quot;measurements&quot;: [
    {
      &quot;statistic&quot;: &quot;VALUE&quot;,
      &quot;value&quot;: {number}
    }
  ],
  &quot;availableTags&quot;: [
    {
      &quot;tag&quot;: &quot;area&quot;,
      &quot;values&quot;: [
        &quot;heap&quot;,
        &quot;nonheap&quot;
      ]
    },
    {
      &quot;tag&quot;: &quot;id&quot;,
      &quot;values&quot;: [
        &quot;G1 Survivor Space&quot;,
        &quot;Compressed Class Space&quot;,
        &quot;Metaspace&quot;,
        &quot;CodeCache&quot;,
        &quot;G1 Old Gen&quot;,
        &quot;G1 Eden Space&quot;
      ]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시각화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM의 Heap 메모리 사용량은 Kibana를 활용해 직관적으로 시각화할 수 있다. 아래는 그래프는 Heap 메모리 영역의 Max, Used, Commited 값과 사용량 최대값을 시각화해 놓은 예시다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOtLqw/btsMKB1aFXA/eXHcqizHny3eaBa6pKVi0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOtLqw/btsMKB1aFXA/eXHcqizHny3eaBa6pKVi0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOtLqw/btsMKB1aFXA/eXHcqizHny3eaBa6pKVi0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOtLqw%2FbtsMKB1aFXA%2FeXHcqizHny3eaBa6pKVi0K%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; alt=&quot;메모리 사용량 시각화&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;401&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;401&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap 메모리 사용량 자체를 시각화하는 작업은 비교적 간단하다. Kibana에서 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jvm.memory.max&lt;/span&gt;&lt;/b&gt;, &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jvm.memory.used&lt;/span&gt;&lt;/b&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;jvm.memory.commited&lt;/b&gt;&lt;/span&gt; 메트릭을 사용해 시각화에 넣어주기만 하면, 전체 Heap 사용량을 쉽게 확인할 수 있다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 GC 사이클을 분석하기 위해 메모리 시각화하는 작업은 조금 더 복잡하다.&lt;/span&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;Kibana APM에서 JVM 메모리 사용량을 추적하는 메트릭이 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jvm_memory_used&lt;/span&gt;&lt;/b&gt;로 기록된다. 위의&amp;nbsp;actuator에서 확인해봤던 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;area&lt;/span&gt;&lt;/b&gt; 태그와 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;id&lt;/span&gt;&lt;/b&gt; 태그를 활용해 각 메모리 영역별(heap &amp;amp; nonheap, GC 관련) 데이터 필터링이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;451&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBp63f/btsMK9ct0pt/ihjdR7pENkrodxYaDlkfXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBp63f/btsMK9ct0pt/ihjdR7pENkrodxYaDlkfXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBp63f/btsMK9ct0pt/ihjdR7pENkrodxYaDlkfXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBp63f%2FbtsMK9ct0pt%2FihjdR7pENkrodxYaDlkfXk%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; alt=&quot;jvm_memory_used apm 데이터 확인해보기&quot; loading=&quot;lazy&quot; width=&quot;451&quot; height=&quot;70&quot; data-origin-width=&quot;451&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Minor GC가 발생하는 Eden 영역의 메모리 사용량을 추적하기 위해 필터링 조건을 적용해보자.&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Eden Space&lt;/span&gt;&lt;/b&gt;의 데이터를 확인하기 위해 필터링 조건을 아래와 같이 적용하면 된다. 추가적으로&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;max&lt;/span&gt;&lt;/b&gt;와 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;commited&lt;/span&gt;&lt;/b&gt; 값을 확인하는 축도 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1742110385232&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;average(jvm_memory_used, kql='labels.id:&quot;Eden Space&quot;  and labels.area:&quot;heap&quot;')&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742111172822&quot; class=&quot;shell&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;average(jvm_memory_max, kql='labels.id:&quot;Eden Space&quot;  and labels.area:&quot;heap&quot;')&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742111173691&quot; class=&quot;shell&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;average(jvm_memory_committed, kql='labels.id:&quot;Eden Space&quot;  and labels.area:&quot;heap&quot;')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프를 확인해보면, Eden 영역의 사용량이 약 140MB 수준이며, 몇 십 분 주기로 Minor GC가 발생하는 것을 확인할 수 있다. 위에서 설명했다시피 Minor GC가 너무 빈번하게 발생하면 문제가 발생할 수 있기 때문에 빈도를 잘 체크해줘야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bskRht/btsMKn9REON/Y4gTvnvaUCk7RvBxUH8wUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bskRht/btsMKn9REON/Y4gTvnvaUCk7RvBxUH8wUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bskRht/btsMKn9REON/Y4gTvnvaUCk7RvBxUH8wUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbskRht%2FbtsMKn9REON%2FY4gTvnvaUCk7RvBxUH8wUk%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; alt=&quot;Eden 시각화&quot; loading=&quot;lazy&quot; width=&quot;952&quot; height=&quot;341&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;341&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;다음으로 Old Generation의 메모리 사용량을 분석해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;labels.id&lt;/span&gt;&lt;/b&gt;를 Old 영역에 맞게 필터링하는 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;vertical axis&lt;/span&gt;&lt;/b&gt;들을 추가해주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZn4v1/btsMNiFtOMZ/OAwwZas8kQYIH007ErStt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZn4v1/btsMNiFtOMZ/OAwwZas8kQYIH007ErStt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZn4v1/btsMNiFtOMZ/OAwwZas8kQYIH007ErStt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZn4v1%2FbtsMNiFtOMZ%2FOAwwZas8kQYIH007ErStt1%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; alt=&quot;그래프 설정&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;400&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1742111237538&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;average(jvm_memory_used, kql='labels.id:&quot;Tenured Gen&quot;  and labels.area:&quot;heap&quot;')&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742111249202&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;average(jvm_memory_max, kql='labels.id:&quot;Tenured Gen&quot; and labels.area:&quot;heap&quot;')&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742111257629&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;average(jvm_memory_committed, kql='labels.id:&quot;Tenured Gen&quot;  and labels.area:&quot;heap&quot;')&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vUPPH/btsMLLV99H7/9WlTC1mqvBKCgKVqkHEILk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vUPPH/btsMLLV99H7/9WlTC1mqvBKCgKVqkHEILk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vUPPH/btsMLLV99H7/9WlTC1mqvBKCgKVqkHEILk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvUPPH%2FbtsMLLV99H7%2F9WlTC1mqvBKCgKVqkHEILk%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; alt=&quot;Old Gen 시각화&quot; loading=&quot;lazy&quot; width=&quot;929&quot; height=&quot;326&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;326&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;Old Generation은 Eden에서 승격된 객체들이 쌓이는 공간으로 Old Generation이 지속적으로 증가하면 Full GC가 발생할 가능성이 높다. 만약 Major GC나 Full GC 이후에도 Old Generation의 사용량이 줄어들지 않고 계속해서 증가한다면 메모리 누수 가능성이 높은 상태다.&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;이 때 메모리 누수의 원인을 파악할 수 있는 방법이 있는데, 자바 애플리케이션에서 Heap Dump를 추출하고 Memory Analyzer Tool로 분석하는 방법이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Heap Dump&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Heap Dump 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap Dump를 뜨는 명령어는 간단하다.&lt;/p&gt;
&lt;pre id=&quot;code_1741956473673&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ jmap -dump:format=b,file=/tmp/&amp;lt;filename&amp;gt;.hprof &amp;lt;JAVA_PROCESS_ID&amp;gt;&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;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;jamp&lt;/b&gt;&lt;/span&gt; 명령어의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-dump&lt;/span&gt;&lt;/b&gt; 옵션으로 Heap Dump를 뜰 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;format=b&lt;/span&gt;&lt;/b&gt;는 바이너리 형식을 의미한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;file={경로}/{파일명}.hprof&lt;/span&gt;&lt;/b&gt;는 저장할 파일이름을 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;lt;JAVA_PROCESS_ID&amp;gt;는 애플리케이션이 실행되고 있는 프로세스 ID를 의미하며, 컨테이너 환경에서는 보통 PID &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;1&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 값을 가진다. 이는 컨테이너가 &quot;애플리케이션 실행&quot;에 최적화된 경량 환경으로 설계되었기 때문이다. 컨테이너는 호스트 OS의 커널을 공유하여 불필요한 시스템 프로세스를 실행할 필요가 없고, 덕분에 내부에서 실행되는 애플리케이션이 자연스럽게 PID &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;1 &lt;/span&gt;&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Heap Dump를 생성할 때&lt;/b&gt;, JVM은 전체 힙 메모리를 스냅샷처럼 저장해야 한다. 이를 위해 &lt;b&gt;GC가 모든 애플리케이션 스레드를 중단(Stop-the-World)시킨다.&lt;/b&gt; 또한, Heap Dump 파일을 생성하는 과정에서 대량의 메모리 데이터를 디스크에 기록해야 하므로, 디스크 I/O가 발생한다. 디스크 쓰기 속도는 상대적으로 느린 편에 속하며, 이 과정에서 &lt;b&gt;수십 초 이상의 시간이 소요&lt;/b&gt;될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 환경에서의 Heap Dump&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 설명한 것처럼, &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jmap&lt;/span&gt;&lt;/b&gt; 명령어를 통해 Heap Dump를 추출하면 Java 애플리케이션이 수십 초 동안 중단될 수 있다. 따라서 실제 운영환경에서 무작정 Heap Dump를 수행하면 시스템 장애로 이어질 위험이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개발(Dev) 또는 스테이징(Staging) 환경에서 확인하려고 해도, 네트워크 요청량이 적어 Old Generation에 충분한 객체가 적재되지 않는 경우가 많다. 그렇다면 이 때는 어떻게 해야 할까?&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;이 경우, 로드밸런서를 활용해 Dump 대상 컨테이너로의 API 유입을 차단한 후 Heap Dump를 추출하면 된다. Kubenetes 환경이라면, Service와 연결된 Pod의 Endpoint를 일시적으로 해제하는 방식으로 이를 구현할 수 있다.&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;Kubernetes에서는 Service의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;spec.selector.app&lt;/span&gt;&lt;/b&gt; 설정을제거하면 기존 Endpoint가 유지된 상태에서 새로운 Pod를 찾지 않도록 설정할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1741957013241&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl describe pod -n my-namespace
# 연결을 끊고자하는 Pod의 Endpoint 확인

$ kubectl edit endpoints -n my-namespace my-svc
# 제거하고자 하는 Endpoint를 지우고 저장

$ kubectl get ep -n my-namespace
# 해당 Endpoint가 제거된 것을 확인할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 Pod로는 API 요청이 유입되지 않으므로 Heap Dump를 안전하게 추출할 수 있는 상태가 된다. Dump 파일이 생성되었다면,&amp;nbsp; 해당 파일을 내가 사용하는 디바이스로 꺼내오자. 그리고 Pod 내부의 Heap Dump 파일을 제거한다.&lt;/p&gt;
&lt;pre id=&quot;code_1742113697582&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl cp \
  my-namespace/my-api-deploy-59b79c4795-q8l6p:/tmp/my-api-20250226.hprof \ 
  /Users/myroot/dumps/my-api-20250226.hprof
  
$ kubectl exec -it my-api -n my-namespace -- rm -f /tmp/my-api-20250226.hprof
# Pod 내부에 남아있는 Heap Dump 파일 삭제 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 작업이 완료되었다면, 서비스 트래픽을 다시 원래대로 복구해야 한다. Service의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;spec.selector.app&lt;/span&gt;&lt;/b&gt; 설정을 다시 추가하고 적용해준다. 이제 Kubernetes가 자동으로 Endpoints를 복구할 것이다. 아래 명령어를 동해 정상적으로 연결되었는지 확인해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1742113591924&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl get ep -n my-namespace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Eclipse&amp;nbsp;Memory&amp;nbsp;Analyzer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap Dump를 추출한 후, Eclipse Memory Analyzer라는 MAT(Memory Analyzer Tool)을 활용하면&lt;br /&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://eclipse.dev/mat/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Eclipse Memory Analyzer 링크&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eclipse MAT을 설치했다면 설정에서 Keep unreachable objects를 활성화해줘야 한다. 이 설정은 heap dump 도중에 parsing하는 과정에서 객체와의 연결을 잃어버린 객체들을 확인할 수 있도록 해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6IRDG/btsMMzHl9SQ/T6rglfFqV9X1Nmxc4xoET0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6IRDG/btsMMzHl9SQ/T6rglfFqV9X1Nmxc4xoET0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6IRDG/btsMMzHl9SQ/T6rglfFqV9X1Nmxc4xoET0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6IRDG%2FbtsMMzHl9SQ%2FT6rglfFqV9X1Nmxc4xoET0%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; alt=&quot;Eclipse Memory Analyzer&quot; loading=&quot;lazy&quot; width=&quot;927&quot; height=&quot;542&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;542&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;이제 방금 전에 생성했던 Heap dump 파일(.hprof)을 프로그램에서 열고, 메모리 누수 분석 버튼을 클릭하면 아래와 같이 문제를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/42PCf/btsMKN1uPdt/zBW7VFtMJVtztEW67ZnGi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/42PCf/btsMKN1uPdt/zBW7VFtMJVtztEW67ZnGi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/42PCf/btsMKN1uPdt/zBW7VFtMJVtztEW67ZnGi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F42PCf%2FbtsMKN1uPdt%2FzBW7VFtMJVtztEW67ZnGi0%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; alt=&quot;Leak 확인&quot; loading=&quot;lazy&quot; width=&quot;925&quot; height=&quot;774&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;774&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;Leak Suspects Report에서 주의 깊게 볼 항목은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;734&quot; data-start=&quot;510&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;591&quot; data-start=&quot;510&quot;&gt;&lt;b&gt;Biggest Objects by Retained Size&lt;/b&gt;: 가장 많은 메모리를 차지하는 객체 (문제 원인일 가능성 높음)&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;651&quot; data-start=&quot;592&quot;&gt;&lt;b&gt;Class Histogram&lt;/b&gt;: 특정 클래스에서 생성된 객체 수 및 점유 메모리 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap Dump를 통해 누수 원인을 확인했다면, 이제 코드를 수정하거나 메모리 관리 전략을 최적화할 차례다. 구체적인 해결 방법은 발생한 문제에 따라 다르므로, 분석한 데이터를 기반으로 적절한 조치를 취해야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;무심코 작성한 코드 한 줄, 메모리 누수의 원인?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. inner class를 static으로 선언하지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이펙티브 자바&quot;의 아이템 24 &quot;멤버 클래스는 되도록 static으로 만들라&quot;를 보면 아래와 같은 문구가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메소드에서 정규화된 this를 사용해 바깥 인스턴스의 메소드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.&quot; 라는 문구가 요점이다. static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 같게 된다는 의미다. 이때 심각한 문제가 발생하게 된다. 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 것이다.&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;내부 클래스를 선언할 때는 static을 꼭 붙여주도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. ThreadLocal&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal&lt;/span&gt;&lt;/b&gt;은 기본적으로 자바의 스레드와 생명주기를 같이 한다. 때문에&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal&lt;/span&gt;&lt;/b&gt;을 적절히 관리하지 않으면 메모리 누수가 발생할 수 있다. 이를 이해하려면 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal&lt;/span&gt;&lt;/b&gt;의 동작 원리를 알아야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ThreadLocal의 동작 원리?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Thread&lt;/span&gt;&lt;/b&gt; 클래스를 살펴보면, 내부에 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocalMap&lt;/span&gt;&lt;/b&gt;으로 선언된 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;threadLocals&lt;/span&gt;&lt;/b&gt;라는 필드를 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1742019118408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Thread implements Runnable {
  //...
  ThreadLocal.ThreadLocalMap threadLocals;
  //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocalMap&lt;/span&gt;&lt;/b&gt;을 확인해보면 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Entry&lt;/span&gt;&lt;/b&gt; 배열로 관리됨을 알 수 있다. 즉, 각각의&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Thread&lt;/span&gt;&lt;/b&gt;가 독립적인&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal&lt;/span&gt;&lt;/b&gt; 저장소를 갖는다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;static class ThreadLocalMap {
  //...
  private Entry[] table;
  //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Thread Pool과 ThreadLocal&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(Platform Thread 시스템을 사용한다고 가정했을 때)&lt;span&gt; 서버에서 단순히 스레드를 생성하고 버린다면 ThreadLocal도 함께 사라지므로 문제가 되지 않는다. 하지만 &lt;b&gt;스레드 생성 비용이 크기 때문에, 대부분의 서버는 스레드 풀링(Thread Pooling) 시스템을 사용&lt;/b&gt;한다.&lt;/span&gt;&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 스프링 서버에 요청을 보내면 Thread Pooling 시스템이 스레드를 할당한다. 해당 스레드가 작업을 마치고 응답을 반환하면, 스레드는 삭제되지 않고 스레드 풀로 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 사용한 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal&lt;/span&gt;&lt;/b&gt;의 데이터를 명시적으로 삭제하지 않으면 그대로 남아 있게 된다. 만약 비싼 리소스를 풀링해서 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal&lt;/span&gt;&lt;/b&gt;에 저장했다면, &lt;b&gt;스레드가 풀로 반환될 때도 값이 유지&lt;/b&gt;되어 메모리가 낭비될 수 있다. 따라서 작업이 끝난 후&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal.remove()&lt;/span&gt;&lt;/b&gt;를 반드시 호출해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1742019817230&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;threadLocal.remove();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ScopedValue&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 한 가지 방법은 &lt;b&gt;실행 컨텍스트에서만 값을 저장&lt;/b&gt;하는 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ScopedValue&lt;/span&gt;&lt;/b&gt;를 활용하는 것이다. &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ScopedValue&lt;/span&gt;&lt;/b&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;&quot;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ThreadLocal&lt;/span&gt;&lt;/b&gt;이 스레드와 생명주기를 함께 한다면, 작업이 끝나면 삭제되는 &lt;b&gt;가상 스레드(Virtual Thread)&lt;/b&gt; 를 사용하면 문제를 해결할 수 있지 않을까?&quot;라고 생각할 수도 있다. 하지만, 가상 스레드를 사용하면 추가적인 고려가 필요하다. 이론상으로 가상 스레드는 무한정 생성될 수 있으므로, 각 스레드가 ThreadLocal을 사용한다면 메모리가 무한정 증가할 수 있다. 순간적으로 많은 메모리가 점유된다면 OOM(Out of Memory)이 발생해 서버가 다운될 위험이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. String&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;String&lt;/b&gt;&lt;/span&gt;은 불변 객체이기 때문에, &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;+&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 연산자로 문자열을 연결할 때마다 새로운 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;String&lt;/span&gt;&lt;/b&gt; 객체가 생성된다. 이로 인해 작은 문자열이 많이 생성되면 GC 부담이 증가하여 성능 저하와 메모리 낭비로 이어질 수 있다.&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;하지만, Java는 이러한 문제를 방지하기 위해 일부 문자열 연산을 자동으로 최적화한다. 특히, 단순한 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;+&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 연산자는 컴파일러 내부적으로 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;StringBuilder&lt;/span&gt;&lt;/b&gt;로 변환하여 성능을 개선한다.&lt;/p&gt;
&lt;pre id=&quot;code_1742114486057&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public String getName() {
  retrun firstName + &quot; &quot; + lastName;
  // 내부적으로는 아래와 같이 동작
  // return new StringBuilder().append(firstName).append(&quot; &quot;).append(lastName).toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;+&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 연산자가 여러 줄에 걸쳐 사용되거나 반복문 내에서 사용될 경우, 최적화가 적용되지 않는다. 이때는 매번 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;String&lt;/span&gt;&lt;/b&gt; 객체가 새로 생성되면서 메모리 사용량이 급격히 증가할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1742114255140&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String badStringConcat() {
    String result = &quot;&quot;;
    for (int i = 0; i &amp;lt; 10000; i++) {
        result += i;
    }
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 반복적인 문자열 연결이 필요할 경우&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;StringBuilder&lt;/span&gt;&lt;/b&gt;나 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;StringBuffer&lt;/span&gt;&lt;/b&gt;를 직접 사용하는 것이 가장 좋은 해결책이다.&lt;/p&gt;
&lt;pre id=&quot;code_1742114283262&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String goodStringConcat() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i &amp;lt; 10000; i++) {
        sb.append(i);
    }
    return sb.toString();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB Connection, 보이지 않는 폭탄&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB Connection은 애플리케이션이 정상적으로 실행될 때는 큰 문제가 없어 보이지만, &lt;b&gt;적절히 관리되지 않으면 보이지 않는 폭탄이 되어 시스템을 위협할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 JDBC를 이용해 직접 DB Connection을 열고 닫아야 했기 때문에, 연결을 명시적으로 해제하지 않으면 누수가 발생할 가능성이 높았다. 하지만 최근에는 MyBatis, JPA 같은 ORM 프레임워크가 Connection을 관리해주기 때문에, 많은 개발자가 더 이상 Connection 누수를 걱정할 필요가 없다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, &lt;b&gt;보이지 않는 문제들이 여전히 존재한다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. AbandonedConnectionCleanupThread&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AbandonedConnectionCleanupThread&lt;/span&gt;&lt;/b&gt;는 개발자가 DB 작업을 완료한 후 명시적으로 DB Connection이 닫지 않았을 때, 이를 자동으로 정리하기 위해 MySQL JDBC 드라이버(Connector/J)에서 내부적으로 실행되는 백그라운드 스레드다.&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;사실 Spring Boot 2.0부터 기본 데이터베이스 커넥션 풀링 시스템이 HikariCP로 변경되면서, HikariCP의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;HouesKeeper&lt;/span&gt;&lt;/b&gt;가 커넥션 정리를 자동으로 수행하게 되었다.이로 인해&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AbandonedConnectionCleanupThread&lt;/span&gt;&lt;/b&gt;가 필요하지 않게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 커넥션 누수 문제를 다룬&amp;nbsp;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: left;&quot; href=&quot;https://helloworld.kurly.com/blog/connection-leak/&quot;&gt;99%가 모른다는 DB Connection 누수 문제&lt;/a&gt; 글을 보면 max-lifetime이 짧을 경우&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AbandonedConnectionCleanupThread&lt;/span&gt;&lt;/b&gt;가 폭발적으로 늘어나 문제가 되는 현상을 볼 수 있다. 몇몇 개발자는 이 글을 읽고 max-lifetime이 충분히 길면 문제가 없는 것이 아닌가? 라고 생각할 수 있다. 하지만, MAT로 메모리 누수를 분석해보면 max-lifetime가 충분히 길더라도&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AbandonedConnectionCleanupThread&lt;/span&gt;&lt;/b&gt;로 인해 누수가 서서히 발생하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qe3jE/btsMK2jZBx0/42j2ZeEOck0P9LrpM9JxZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qe3jE/btsMK2jZBx0/42j2ZeEOck0P9LrpM9JxZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qe3jE/btsMK2jZBx0/42j2ZeEOck0P9LrpM9JxZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqe3jE%2FbtsMK2jZBx0%2F42j2ZeEOck0P9LrpM9JxZ1%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; alt=&quot;AbandonedConnectionCleanupThread로 인한 누수&quot; loading=&quot;lazy&quot; width=&quot;953&quot; height=&quot;530&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 문제를 완전히 해결하려면 AbandonedConnectionCleanup을 사용하지 않는 것이 필요하다.&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;다행히도 mysql-connector-j 8.0.22 버전 이상부터는 AbandonedConnectionCleanup을 비활성화 할 수 있는 옵션이 생겼다. 아래와 같이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;java&lt;/span&gt; 명령어에 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true&lt;/span&gt;&lt;/b&gt; 옵션을 추가해주면 문제를 해결할 수 있다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;$ java -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true -jar myapp.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. EntityManager 주입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityManager를 아래와 같이 스프링 프레임워크의 의존성 주입으로 받게 되면 동시성 문제가 발생할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1742003303955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public class MyRepository {

  private final EntityManager em;
  
  public MyRepository(EntityManager em) {
    this.em = em;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용하면 애플리케이션 내에서 여러 개의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt; 인스턴스가 생성되고, Bean으로 관리된다. 스프링 프레임워크의 의존성 주입(&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Autowired&lt;/span&gt;&lt;/b&gt;, 생성자 주입 등)을 통해 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;를 받을 경우, 여러 개의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt; 중 하나의 인스턴스에 의존하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 스프링은 기본적으로 멀티스레드로 실행되며, 여러 요청을 동시에 수행할 수 있다. 이때 동일한 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt; 인스턴스가 여러 스레드에서 공유될 경우, 동시성 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;는 Thread-Safe하지 않다. &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;를 스레드 간에 공유하게 된다면, 한 스레드가 DB Connection을 닫으려는 순간, 다른 스레드가 동일한 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;를 통해 쿼리르 실행할 수도 있다. 이로 인해 커넥션 반환이 제대로 이루어지지 않는 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@PersistenceContext&lt;/span&gt;&lt;/b&gt;를 통해 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;를 주입하는 방식을 권장한다. &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@PersistenceContext&lt;/span&gt;&lt;/b&gt;를 사용하면 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;가 아닌 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;SharedEntityManagerBean&lt;/span&gt;&lt;/b&gt;이라는 공유 프록시를 반환한다. 이 프록시 인스턴스는 스레드 간 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;를 공유하는 것을 방지해준다. 해당 프록시는 현재 트랙잭션과 연결된&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;를 반환하여 동기화 시켜준다. (없다면 생성해준다.) 따라서 트랙잭션별로 독립적인 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EntityManager&lt;/span&gt;&lt;/b&gt;를 사용하게 되고 이를 통해 Thread-Safe한 동작을 보장하여, &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Connection.close()&lt;/span&gt;&lt;/b&gt;도 문제 없이 실행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 환경에서는 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@PersistenceContext&lt;/span&gt;&lt;/b&gt;를 사용하는 것이 가장 안전한 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(최신 스프링 부트 버전에서는 스프링의 의존성 주입으로 받아도 프록시 객체를 반환하는 것으로 보인다.)&lt;/p&gt;
&lt;pre id=&quot;code_1742003448588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public class MyRepository {

  @PersistenceContext
  private EntityManager em; 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 간과했던 작은 코드 한 줄이 예상치 못한 메모리 누수, GC 과부하, 성능 저하로 이어질 수 있다. JVM 메모리는 자동으로 관리되지만, &lt;b&gt;적절한 코드 최적화와 메모리 사용 패턴을 이해하지 않으면 &lt;/b&gt;애플리케이션의 성능 저하, 심한 경우 OOM(Out of Memory)이 발생해 서비스 중단될 수도 있다.&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;이번 글에서 살펴본 것처럼, JVM의 메모리 구조를 이해하고, 메모리 누수를 방지하는 방법을 적용하면 불필요한 객체 유지, 과도한 GC 실행, 메모리 부족 등의 문제를 효과적으로 예방할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 관리는 애플리케이션 성능 최적화에서 &lt;b&gt;가장 중요한 요소 중 하나&lt;/b&gt;다.&lt;br /&gt;눈에 보이지 않는 문제라고 해서 무시하면 결국 &lt;b&gt;서비스 장애와 성능 저하라는 대가&lt;/b&gt;를 치르게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 메모리 누수를 예방하는 작은 실천이 &lt;b&gt;더 빠르고 안정적인 시스템을 만드는 첫걸음&lt;/b&gt;이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/JAVA-%E2%98%95-JVM-%EB%82%B4%EB%B6%80-%EA%B5%AC%EC%A1%B0-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%81%EC%97%AD-%EC%8B%AC%ED%99%94%ED%8E%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JVM 내부 구조 &amp;amp; 메모리 영역 총정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/java-permgen-metaspace&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Permgen&amp;nbsp;vs&amp;nbsp;Metaspace&amp;nbsp;in&amp;nbsp;Java&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.voidmainvoid.net/184&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java 8에서 사라진 MaxPermSize, PermSize을 대체하는 옵션?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98-GC-%ED%8A%9C%EB%8B%9D-%EB%A7%9B%EB%B3%B4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;가비지 컬렉션 GC 튜닝 절차 맛보기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/connection-leak/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;99%가 모른다는 DB Connection 누수 문제&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;이펙티브 자바, 조슈아 블로크, 프로그래밍 인사이트&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://devel-repository.tistory.com/68&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java21 - scoped value에 대해서 알아보자&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@semi-cloud/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B3%A0%EA%B8%89%ED%8E%B81-ThreadLocal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ThreadLocal의 개념과 동작 방식&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://do-study.tistory.com/97&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HikariCP와 커넥션 누수(Connection Leak) 관련 트러블슈팅&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@biddan606/EntityManager%EB%A5%BC-%EC%A3%BC%EC%9E%85%ED%95%A0-%EB%95%8C-Autowired-vs-PersistContext&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;EntityManager를 주입할 때 @PersistenceContext 대신 @Autowired를 사용하면 안될까?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>java</category>
      <category>memory</category>
      <category>Spring</category>
      <category>메모리</category>
      <category>메모리 누수</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/253</guid>
      <comments>https://myvelop.tistory.com/253#entry253comment</comments>
      <pubDate>Sun, 16 Mar 2025 17:55:50 +0900</pubDate>
    </item>
    <item>
      <title>기술과 관리 사이에서 (넥스터즈 26기 후기)</title>
      <link>https://myvelop.tistory.com/252</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PM에 도전하다.&lt;/h2&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;PM의 역할을 맡으면 이것저것 챙길 일이 많아질 테지만, 제대로 고생 한 번 해보면 &lt;b&gt;스스로 더 성장할 수 있을 거라고 믿었다. &lt;/b&gt;나는 &lt;b&gt;협업의 중심점 역할을 맡아, 팀을 이끌고 문제를 해결하는 경험&lt;/b&gt;을 해보고 싶었다. 단순히 맡은 업무를 수행하는 것이 아니라, 팀이 효율적으로 움직일 수 있도록 방향을 제시하는 사람이 되고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나는 어떤 PM이었나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;PM이란 &quot;프로덕트를 성공시키기 위해 무엇이든 할 수 있는 역할&quot;이라고 생각했다. 하지만 구체적인 역할을 정의하지 못했던 나는 프로젝트 초기에 약간 방황하며 어려움을 겪었다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;이 과정에서 회사의 PM 분과 커피챗을 요청해 이야기를 나눴다. 그분 역시 &quot;PM의 존재 이유는 프로덕트를 성공시키는 것&quot;이라는 점에 동의했지만, 이를 달성하기 위한 본인의 포지셔닝을 정하는 것이 중요하다는 조언을 해주셨다.&lt;/span&gt;&amp;nbsp;&lt;/span&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;span&gt;기획자 베이스의 PM, 디자이너 베이스의 PM이 각자의 강점을 갖추었듯이, 백엔드 개발자로서의 강점을 활용하는 방법을 고민해보라는 말이었다. 이 대화를 계기로 PM으로서의 역할을 보다 구체적으로 정의하고 수행할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;일정 계획자&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;PM에게 가장 중요한 역할 중 하나는 프로젝트 일정을 맞추는 것이라고 생각했다.&lt;/span&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;나는 1차, 2차, 3차에 나누어 중간 목표를 세워 최종 목표에 도달할 수 있도록 계획했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;계획을 100% 이행하지는 못했지만, 각 단계에서 진행 상황을 점검하며 일정 조율에 집중했다. 특히, 예상보다 일정이 밀리는 경우, 팀원들과 논의하여 유연하게 조정하는 방식으로 대응했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;병목 파괴자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥스터즈는 2달이라는 짧은 시간 안에 결과물을 만들어내야 했기 때문에, &lt;b&gt;병목은 곧 실패로 이어진다&lt;/b&gt;고 생각했다. &lt;span&gt;병목을 최소화하는 것이 가장 중요한 임무 중 하나였다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;직무 간 협업이 필요한 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;각 팀과의 의사소통을 빠르게 조율&lt;/b&gt;&lt;/span&gt;했다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;팀 내 논의가 필요할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;회의를 주도적으로 개최&lt;/b&gt;&lt;/span&gt;하여 지연이 발생하지 않도록 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백엔드 리딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 백엔드 팀원이 다른 팀에 비해 많았다. &lt;span&gt;세부적인 구현은 팀원들에게 맡기고, 큰 틀에서 방향성을 잡아주며 프로젝트가 원활히 진행될 수 있도록 했다.&lt;/span&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;span&gt;앱 팀과 프론트엔드 팀이 API를 기다리는 시간을 최소화하는 것이 목표였고, 백엔드 팀원들이 바로 작업을 시작할 수 있도록 작업 계획을 세워 Github에서 마일스톤 및 이슈로 제공했다. &lt;/span&gt;&lt;span&gt;또한, 비즈니스 정책이 결정되자마자 도메인 모델링을 진행했고, 다이어그램을 제시해 백엔드 팀과 함께 논의하며 구조를 빠르게 확정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;1486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJCQsb/btsMzeK9CxM/SJioqcgBPoUdPA5mW8ztq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJCQsb/btsMzeK9CxM/SJioqcgBPoUdPA5mW8ztq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJCQsb/btsMzeK9CxM/SJioqcgBPoUdPA5mW8ztq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJCQsb%2FbtsMzeK9CxM%2FSJioqcgBPoUdPA5mW8ztq0%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; alt=&quot;도메인 설계&quot; loading=&quot;lazy&quot; width=&quot;1102&quot; height=&quot;1486&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;1486&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;h3 data-ke-size=&quot;size23&quot;&gt;함께 하는 파트너&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&quot;가치관 탐색&quot;이라는 추상적인 개념을 서비스로 구현하려다 보니 기획 단계에서 예상보다 많은 시간이 소요되었다. 디자인이 확정되었을 때는 최종 발표까지 한 달도 남지 않은 상태였고, 작업해야 할 페이지가 많았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;PM의 역할이 마감 기한을 맞추기 위해 업무량을 조정하는 것이지만, 앱의 완성도를 고려했을 때 &lt;/span&gt;&lt;span&gt;뺄 수 있는 기능이 하나도 없었다.&lt;/span&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;span&gt;당연히 우리 팀원들의 능력이라면 충분히 해낼 수 있다고 믿었지만, 문제는 사기였다. 프론트엔드 팀이 작업량에 압도된 듯 보였고, 기한 내에 완성할 수 있을지에 대한 의문이 커지고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 상황에서 내가 할 수 있는 선택지는 하나였다. &lt;/span&gt;&lt;span&gt;&lt;b&gt;프론트엔드 개발을 직접 지원하는 것.&lt;/b&gt;&lt;/span&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;span&gt;가능한 모든 시간을 활용해 프론트엔드 개발에 전념했고, 일주일 만에 1차 MVP 페이지를 완료했다. 덕분에 계획했던 UT(사용성 테스트)를 성공적으로 진행할 수 있었고, 작은 성취를 경험한 프론트엔드 팀은 자신감을 되찾아 빠르게 개발을 진행했다. 결국 &lt;/span&gt;&lt;span&gt;3주 만에 모든 페이지를 완성하는 기염을 토해냈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잡일 담당자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;PM으로서 팀원들이 본인의 업무에 집중할 수 있도록 돕는 것도 중요한 역할이었다. &lt;/span&gt;&lt;span&gt;그래서 나는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;플러터 개발, iOS 심사, 백엔드 병목 해결, 자잘한 프론트 이슈 해결, QA 등 다양한 업무를 직접 수행했다. &lt;/b&gt;&lt;/span&gt;&lt;span&gt;그 외에도 &lt;/span&gt;&lt;span&gt;&lt;b&gt;광고 작업, 발표 준비, 회의 장소 예약&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 등 크고 작은 업무들도 도맡아 처리했다. &lt;/span&gt;&lt;span&gt;백엔드를 리딩하면서도 다양한 실무를 병행하느라 정신없이 움직였다. &lt;s&gt;그리고 사업가의 삶이 얼마나 고달픈지 체감할 수 있었다.&lt;/s&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리더에게 필요한 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;리더의 실수는 팀 전체에 영향을 미친다. 하지만 중요한 것은 실수를 통해 배우고, 같은 실수를 반복하지 않는 것이다. 나는 이번 경험을 통해 리더가 가져야 할 핵심 역량에 대해 고민해봤다.&lt;/span&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;&lt;span&gt;리더의 판단이 팀의 방향을 결정한다. 리더가 방향을 잘못 설정하면, 팀원들은 불필요한 시행착오를 겪고 고생하게 된다. 따라서 리더는 자신의 프로덕트에 대한 깊은 이해를 갖추고, 많은 정보를 확보하며, 트렌드에도 민감해야 한다. 무엇보다도 각 직무에 대한 높은 이해도를 바탕으로 매끄러운 협업을 이끌어낼 수 있어야 한다.&lt;/span&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;span&gt;하지만 나는 이 부분에서 부족했다. &lt;b&gt;트렌드에 둔감했고, 내 결정에 대한 확신이 없었다.&lt;/b&gt; &amp;nbsp;&lt;/span&gt;특히, UI/UX나 사용자 경험 설계에 대한 이해가 부족해 디자인 팀과 논의할 때 논리적인 피드백을 주지 못했다. 특히,&amp;nbsp;&lt;b&gt;'사용자 중심의 인터랙션'&lt;/b&gt;을 간과했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확신이 부족한 상태에서 결정을 내리는 것이 두려웠다.&lt;/b&gt; 디자인을 수정해야 한다는 직감을 느꼈지만, &quot;리소스가 낭비될까 봐&quot;, &quot;내가 디자인에 대해 뭘 안다고 판단을 해&quot;라는 생각에 쉽게 결정을 내리지 못했다. 중요한 사안에서도 망설였고, 때로는 &lt;b&gt;명확한 결정을 내리지 못한 채 팀원들에게 판단을 유보했다.&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 &lt;b&gt;팀이 혼란스러워졌고, 기획이 길어졌다.&lt;/b&gt; 기획의 방향이 명확하지 않다 보니 &lt;b&gt;디자인 팀은 계속해서 수정을 반복해야 했고, 개발 팀도 불확실한 상태에서 일을 진행해야 했다.&lt;/b&gt; 결과적으로 프로젝트 전체 일정이 지연되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더는 완벽한 답을 내놓는 사람이 아니다. 중요한 것은 &lt;b&gt;불확실한 상황에서도 팀이 나아갈 방향을 명확히 제시하는 능력이 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계획과 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;리더에게는 명확한 계획과 전략이 있어야 한다. 전략 없이 실행부터 하면, 팀원들은 쓸데없는 작업을 반복하게 되고 사기가 떨어진다. 몇 수 앞을 내다보고 고민하는 것이 리더의 역할이다&lt;/span&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;span&gt;우리 팀은 서비스 아이템을 결정한 후, 유저 리서치를 통해 인사이트를 도출했고, 바로 와이어프레임 제작을 진행했다. 그러나 와이어프레임을 기반으로 논의를 시작하면서 큰 문제가 발생했다. &lt;/span&gt;&lt;span&gt;&lt;b&gt;핵심 정책이 결정되지 않은 상태에서 디자인 작업이 먼저 진행되었기 때문&lt;/b&gt;&lt;/span&gt;&lt;span&gt;이다. 결국 디자인을 다시 해야 했고, 불필요한 수정 작업이 발생하면서 많은 시간이 낭비되었다.&lt;/span&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;span&gt;다음과 같은 순서로 진행했더라면 훨씬 효율적이었을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;핵심 정책 논의 및 결정 -&amp;gt; 와이어프레임 작업 -&amp;gt; 디테일한 플로우 피드백 -&amp;gt; 디자인 작업&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;&lt;b&gt;일의 중요도와 순서를 고려한 전략적인 접근이 필요하다.&lt;/b&gt;&lt;/b&gt; 리더가 명확한 방향을 제시하지 않으면 팀은 시행착오를 반복하게 된다. 전략 없는 실행은 리소스 낭비로 이어지고, 결국 목표 달성도 어려워진다. &lt;b&gt;좋은 리더는 단순히 실행을 독려하는 사람이 아니다.&lt;/b&gt; 팀이 &lt;b&gt;옳은 방향으로 움직이도록 전략을 세우고, 최적의 경로를 설계하는 사람&lt;/b&gt;이어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위기 대응 능력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀이 위기에 잘 대응할 수 있도록 돕는 것도 중요한 능력이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 중간에 팀원 중 한 명에게 갑작스러운 회사 일정이 생겨, 더 이상 프로젝트에 참여하기 어려운 상황이 온 적이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0jT8i/btsMA7483rk/pyZR0dumKuVBk03W1MqNH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0jT8i/btsMA7483rk/pyZR0dumKuVBk03W1MqNH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0jT8i/btsMA7483rk/pyZR0dumKuVBk03W1MqNH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0jT8i%2FbtsMA7483rk%2FpyZR0dumKuVBk03W1MqNH0%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; alt=&quot;팀원의 참여가 어려워진 상황&quot; loading=&quot;lazy&quot; width=&quot;1482&quot; height=&quot;244&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;244&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;특히, &lt;b&gt;일러스트레이션이 핵심 요소였던 우리 팀에서 디자이너의 이탈은 큰 위기였다.&lt;/b&gt; 순간적으로 멘탈이 흔들렸지만, 팀원들이 패닉 상태에 빠지지 않도록 &lt;b&gt;긍정적인 분위기를 유지&lt;/b&gt;하려고 노력했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmA6bf/btsMA8pskZX/DnAvpXptDK7AcjqZlSGSI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmA6bf/btsMA8pskZX/DnAvpXptDK7AcjqZlSGSI0/img.png&quot; data-alt=&quot;프로젝트가 끝난 후, 사실 멘탈이 많이 흔들렸었다고 고백했는데 전혀 안 그래보였다고 해서 다행..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmA6bf/btsMA8pskZX/DnAvpXptDK7AcjqZlSGSI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmA6bf%2FbtsMA8pskZX%2FDnAvpXptDK7AcjqZlSGSI0%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; alt=&quot;멘탈 잡아보기&quot; loading=&quot;lazy&quot; width=&quot;1740&quot; height=&quot;612&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;612&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 디자인 팀의 부담을 줄여주기 위해 캐릭터 소개를 위한 UX 라이팅을 직접 맡기로 결정했다. 회사에 있던 &quot;UX 라이팅 교과서&quot;를 찾아 읽으며, GPT와 한동안 씨름하며 최적의 표현을 고민했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 팀원들도 컨텐츠 제작과 QA에 적극적으로 참여하며 앱의 완성도를 높여나갔다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;나에게 생긴 변화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리더의 관점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM의 역할을 하다보니, 회사에서도 리더의 관점으로 생각해보는 일이 잦아졌다. 내가 말하는 리더의 관점이란 우리 팀의 리소스를 파악하고 그에 따라 현실적인 일정을 계획하며 프로덕트가 더 좋은 방향으로 나아갈 수 있게 고민할 수 있는 것이다.&amp;nbsp;&lt;b&gt;단순히 업무를 하는 사람이 아니라, 팀이 가진 역량을 최적화하고 최선의 결과를 도출하는 사람&lt;/b&gt;이어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀의 계획이 틀어질 것 같다면, &lt;b&gt;역할을 가리지 않고 선뜻 나서서 문제를 해결&lt;/b&gt;하려 했다. 또한, &lt;b&gt;팀원이 더 성장할 수 있는 방향을 고민하며 돕는 것이 자연스러워졌다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;광고에 대한 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 회사에서 광고 담당자와 긴밀히 협업하며, &lt;b&gt;Google Tag Manager, Google Ads, Meta Tag Manager&lt;/b&gt; 등을 효과적으로 활용할 수 있도록 &lt;b&gt;코드를 추가하는 역할&lt;/b&gt;을 하고 있다. 하지만 이 과정에서 나는 광고 시스템을 &lt;b&gt;기술적인 관점에서만 바라보고 있었다.&lt;/b&gt; 광고가 어떻게 기획되고 운영되는지, 어떤 전략이 필요한지에 대한 고민은 상대적으로 적었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중, &lt;b&gt;PM 역할을 맡으며 직접 광고를 운영해볼 기회가 생겼다.&lt;/b&gt;&lt;br /&gt;회사에서는 광고 관련 요청이 오면 &lt;b&gt;트래킹 코드 추가, 이벤트 태깅, 픽셀 설정&lt;/b&gt; 등을 담당했지만, 이번에는 &lt;b&gt;광고의 성과를 직접 측정하고 분석해야 하는 입장&lt;/b&gt;이 되었다. 단순히 &quot;어떤 데이터를 수집할 것인가&quot;를 고민하는 것이 아니라, &lt;b&gt;&quot;광고 전략을 어떻게 최적화할 것인가&quot;라는 새로운 시각이 필요&lt;/b&gt;해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 &lt;b&gt;Meta 광고 관리자&lt;/b&gt;를 활용해 직접 광고를 진행해보았다.&lt;br /&gt;큰 금액은 아니었지만, &lt;b&gt;내 사비를 들여 광고를 운영해보는 경험을 했다.&lt;/b&gt; 광고를 운영해보면서 회사의 담당자분과도 대화를 나눌 기회가 생겨 이것저것 배울 수 있었다. 특히, 광고 타겟 설정이 성과에 얼마나 큰 영향을 미치는지, 앱 광고에서 유저의 관심을 끄는 최적의 단계별 전략은 무엇인지 등 구체적인 인사이트를 얻을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 통해, 내가 수집해왔던 데이터들이 단순한 숫자가 아니라, &lt;b&gt;실제 비즈니스 성과를 좌우하는 핵심 자료임을 깨달았다.&lt;/b&gt;&amp;nbsp;작은 변화지만, 이는 나에게 &lt;b&gt;단순한 개발자가 아닌, 비즈니스를 이해하는 개발자로 성장하는 과정&lt;/b&gt;이었다. 앞으로도 &lt;b&gt;기술과 비즈니스 사이에서 더 넓은 시각을 갖춘 개발자가 되고 싶다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu2taF/btsMz7x6xBD/GjVNb2zEDWqTHrn5t5mlU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu2taF/btsMz7x6xBD/GjVNb2zEDWqTHrn5t5mlU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu2taF/btsMz7x6xBD/GjVNb2zEDWqTHrn5t5mlU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu2taF%2FbtsMz7x6xBD%2FGjVNb2zEDWqTHrn5t5mlU1%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;934&quot; height=&quot;686&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;686&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EYwGB/btsMzZ7Qqz9/y7drbYaU2y28YrxiYcopJK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EYwGB/btsMzZ7Qqz9/y7drbYaU2y28YrxiYcopJK/img.jpg&quot; data-alt=&quot;발표는 정말 질리도록 했다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EYwGB/btsMzZ7Qqz9/y7drbYaU2y28YrxiYcopJK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEYwGB%2FbtsMzZ7Qqz9%2Fy7drbYaU2y28YrxiYcopJK%2Fimg.jpg&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;4032&quot; height=&quot;3024&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&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;PM 역할을 맡으면서, 나는 단순히 개발자가 아니라 &lt;b&gt;팀을 이끌고 방향을 고민하는 역할&lt;/b&gt;을 경험하게 되었다.&lt;br /&gt;처음에는 막막했고, &quot;PM이란 무엇을 해야 하는 역할인가?&quot;라는 질문부터 시작했다. 하지만 프로젝트를 진행하면서 점점 &lt;b&gt;나만의 방식으로 PM의 역할을 정의&lt;/b&gt;할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단순히 기술을 익히는 것을 넘어, 더 넓은 시각을 가진 사람이 되고 싶다.&lt;/b&gt;&lt;br /&gt;어떤 역할을 맡든, &lt;b&gt;문제를 해결하고 팀을 성장시킬 수 있는 사람이 되는 것.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동/IT커뮤니티</category>
      <category>Nexters</category>
      <category>PM</category>
      <category>개발자</category>
      <category>넥스터즈</category>
      <category>넥스터즈 26기</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/252</guid>
      <comments>https://myvelop.tistory.com/252#entry252comment</comments>
      <pubDate>Sun, 2 Mar 2025 14:04:47 +0900</pubDate>
    </item>
    <item>
      <title>2024년 회고</title>
      <link>https://myvelop.tistory.com/251</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 1일 1커밋&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;올해도 어김없이 1일 1커밋을 성공적으로 이어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 &quot;1일 1커밋을 해야 하니까 개발을 한다&quot;는 생각이 아니라, &quot;매일 개발을 해야 하니까 그 과정에서 1일 1커밋을 자연스럽게 기록한다&quot;는 느낌이다. 마치 개발을 위한 다이어리를 쓰는 것처럼 말이다. 이는 분명 긍정적인 변화라고 생각한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oBgGN/btsL2TzUASZ/D03BxGuEKErjyZxnPjFWqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oBgGN/btsL2TzUASZ/D03BxGuEKErjyZxnPjFWqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oBgGN/btsL2TzUASZ/D03BxGuEKErjyZxnPjFWqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoBgGN%2FbtsL2TzUASZ%2FD03BxGuEKErjyZxnPjFWqK%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; alt=&quot;1일 1커밋&quot; loading=&quot;lazy&quot; width=&quot;1540&quot; height=&quot;392&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 블로그&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글또 9기의 경험이 좋았기에, 글또 10기에도 참여하게 되었다.&lt;/p&gt;
&lt;figure id=&quot;og_1738032591465&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;글또 10기 시작!&quot; data-og-description=&quot;글또 지원9기에 이어 10기에도 지원했다.&amp;nbsp;이것저것 벌여 놓은 일들(스터디 2개 운영, 오픈소스 컨트리뷰션 활동 등)이 많아 글또를 잘 할 수 있을까? 라는 생각이 있었다. 그래도 하고 싶었다. 저&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/244&quot; data-og-url=&quot;https://myvelop.tistory.com/244&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/MDgGE/hyX4qUEHRi/OUESPJNSVVopU0daRZBYX1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bJxHVq/hyX4qmLi9Y/tzy0VmcGsCWnjHpPqA5hi1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/244&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/244&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/MDgGE/hyX4qUEHRi/OUESPJNSVVopU0daRZBYX1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bJxHVq/hyX4qmLi9Y/tzy0VmcGsCWnjHpPqA5hi1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;글또 10기 시작!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;글또 지원9기에 이어 10기에도 지원했다.&amp;nbsp;이것저것 벌여 놓은 일들(스터디 2개 운영, 오픈소스 컨트리뷰션 활동 등)이 많아 글또를 잘 할 수 있을까? 라는 생각이 있었다. 그래도 하고 싶었다. 저&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글또에 참여한 이후, 더 좋은 글을 쓰기 위해 고민하는 시간이 많아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 글의 퀄리티는 높아졌다고 생각하지만, 예상과는 달리 블로그 방문자는 오히려 절반으로 줄었다. AI의 발전으로 직접 검색을 통해 자료를 찾는 일이 줄어든 영향도 있겠지만, 가장 큰 원인은 결국 나에게 있다고 본다.&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;3. 오픈소스 컨트리뷰션&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;b&gt;오픈소스 컨트리뷰션&lt;/b&gt;이었다.&lt;/p&gt;
&lt;figure id=&quot;og_1738032113848&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;2024 오픈소스 컨트리뷰션 아카데미 시작!&quot; data-og-description=&quot;오픈소스 컨트리뷰션인프콘 발표자를 지원했으나 떨어졌고, 넥스터즈는 서류 광탈.이제 해볼 만한 게 없을까 이것저것 찾아보다가 2024 오픈소스 컨트리뷰션 아카데미가 눈에 들어왔다.링크:&amp;nbsp;20&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/232&quot; data-og-url=&quot;https://myvelop.tistory.com/232&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KpjzI/hyX4vPaU9e/uXncs6BIERXBkxCkMMToak/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/bnK9sj/hyX4q8a8qT/aTaYdpUe0aKlk0nUYNc22K/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/KRQNA/hyX4uo8OBu/MtYz7uZ0WkAfLC8RlxsORk/img.jpg?width=1411&amp;amp;height=1058&amp;amp;face=0_0_1411_1058&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/232&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/232&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KpjzI/hyX4vPaU9e/uXncs6BIERXBkxCkMMToak/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/bnK9sj/hyX4q8a8qT/aTaYdpUe0aKlk0nUYNc22K/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/KRQNA/hyX4uo8OBu/MtYz7uZ0WkAfLC8RlxsORk/img.jpg?width=1411&amp;amp;height=1058&amp;amp;face=0_0_1411_1058');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2024 오픈소스 컨트리뷰션 아카데미 시작!&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오픈소스 컨트리뷰션인프콘 발표자를 지원했으나 떨어졌고, 넥스터즈는 서류 광탈.이제 해볼 만한 게 없을까 이것저것 찾아보다가 2024 오픈소스 컨트리뷰션 아카데미가 눈에 들어왔다.링크:&amp;nbsp;20&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 Zeppelin의 의존성 관리 도구인 &lt;b&gt;Helium&lt;/b&gt;의 업데이트 자동화를 주로 담당했다. 기존에는 S3 + AWS Lambda로 관리되고 있었지만, AWS 서버 지원이 중단되면서 새로운 방식으로의 전환이 필요했다. 이를 해결하기 위해 &lt;b&gt;GitHub Actions를 활용한 자동화 작업&lt;/b&gt;을 진행했다. 그 과정에서 Helium의 버그를 찾아 수정하고, Kubernetes(k8s) 지원을 위한 Docker화 작업도 도왔다.&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;하나의 기여가 이루어지기까지는 생각보다 오랜 시간이 걸렸다. Maintainer분들도 전업 기여자가 아닌 현업 개발자였기 때문에, PR을 올리면 리뷰를 받는 데 &lt;b&gt;일주일 이상&lt;/b&gt; 걸리는 경우도 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 스터디 일정 등으로 인해 모든 시간을 오픈소스 기여에 쏟을 수는 없었지만, 매주 주말에 열리는 &lt;b&gt;오프라인 모임에는 꼭 참석&lt;/b&gt;했다. 함께 식사하며 개발 이야기를 나누는 시간이 즐거웠고, 다양한 책도 추천받으며 많은 배움을 얻을 수 있었다.&lt;/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;그리고 의미 있는 성과도 있었다. 우리 팀이 기여한 이후, Helium이 포함된 &lt;b&gt;Apache Zeppelin이 Kafka, Spark를 포함한 Apache TLP(Top-Level Project) 중 가장 높은 커뮤니티 건강 지수를 달성&lt;/b&gt;했다. 무려 &lt;b&gt;10점 만점에 10점!&lt;/b&gt;  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UDVYl/btsL3mIIMDt/05EelKko0DKK0Y8jrl7FT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UDVYl/btsL3mIIMDt/05EelKko0DKK0Y8jrl7FT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UDVYl/btsL3mIIMDt/05EelKko0DKK0Y8jrl7FT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUDVYl%2FbtsL3mIIMDt%2F05EelKko0DKK0Y8jrl7FT1%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; alt=&quot;아파치 재단 커뮤니티 건강 지수&quot; loading=&quot;lazy&quot; width=&quot;1534&quot; height=&quot;620&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;620&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;&lt;span style=&quot;color: #2c2d2d;&quot;&gt;또한 User Discussion 활성화 트래픽이 &lt;/span&gt;&lt;span style=&quot;color: #2c2d2d;&quot;&gt;246% 상승했고 &lt;span style=&quot;color: #2c2d2d;&quot;&gt;PR Review 활성화 트래픽이&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #2c2d2d;&quot;&gt;858% 증가했다고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdiL0C/btsL2Royvby/9GS8z9bKnDz5PZlil90sZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdiL0C/btsL2Royvby/9GS8z9bKnDz5PZlil90sZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdiL0C/btsL2Royvby/9GS8z9bKnDz5PZlil90sZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdiL0C%2FbtsL2Royvby%2F9GS8z9bKnDz5PZlil90sZ1%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; alt=&quot;User Discussion Graph&quot; loading=&quot;lazy&quot; width=&quot;1599&quot; height=&quot;593&quot; data-origin-width=&quot;1599&quot; data-origin-height=&quot;593&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1595&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIWq9y/btsL1UzU2wG/4GqeDYhsyNAGLkJULPf4Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIWq9y/btsL1UzU2wG/4GqeDYhsyNAGLkJULPf4Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIWq9y/btsL1UzU2wG/4GqeDYhsyNAGLkJULPf4Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIWq9y%2FbtsL1UzU2wG%2F4GqeDYhsyNAGLkJULPf4Ck%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; alt=&quot;PR Review 활성화 트래픽 Graph&quot; loading=&quot;lazy&quot; width=&quot;1595&quot; height=&quot;582&quot; data-origin-width=&quot;1595&quot; data-origin-height=&quot;582&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;이런 성과를 인정받아 최우수상을 수여받게 되었다. 사실 이렇게 큰 상을 받게될 거라고는 상상도 하지 못하고 있었기 때문에 굉장히 놀랐고, 기뻤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4KXMZ/btsL3hHoMAH/8pUMQxWK6pRAAGFLy7rNhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4KXMZ/btsL3hHoMAH/8pUMQxWK6pRAAGFLy7rNhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4KXMZ/btsL3hHoMAH/8pUMQxWK6pRAAGFLy7rNhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4KXMZ%2FbtsL3hHoMAH%2F8pUMQxWK6pRAAGFLy7rNhK%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; alt=&quot;오픈소스 컨트리뷰션 최우수상&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 토스 러너스하이 1기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;업무의 본질은 무엇일까?&lt;/b&gt; 결국, &lt;b&gt;제품을 성공시키는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 제품의 성공은 단순히 좋은 코드를 작성하는 것만으로 이루어지지 않는다. 올바른 방향으로 나아가기 위해서는 &lt;b&gt;데이터 기반의 소통이 필요&lt;/b&gt;하며, 실패를 두려워하기보다 &lt;b&gt;과감하게 시도하고, 그 과정에서 성과를 만들어내야 한다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스 멘토링을 통해 변화의 필요성을 논리적으로 설명하는 방법을 배우고, 개선된 성과가 조직에 미치는 영향을 보다 명확하게 전달할 수 있게 되었다. 예전에는 &quot;아마 이게 더 낫지 않을까요?&quot;라고 말하는 사람이었다면, 이제는 &lt;b&gt;구체적인 근거를 바탕으로 팀원을 설득할 수 있는 사람&lt;/b&gt;이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;5. 넥스터즈 합격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여름에 25기에 지원했지만, 지원서 제출일 저녁에 부랴부랴 작성한 것으로는 당연히 합격할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;26기에는 다르게 접근했다. &lt;b&gt;일주일 동안 지원서를 준비&lt;/b&gt;했고, 제출 당일에는 &lt;b&gt;반차까지 내며 포트폴리오를 정리&lt;/b&gt;했다. 그 결과 &lt;b&gt;서류에 합격&lt;/b&gt;할 수 있었고, 면접을 거쳐 마침내 &lt;b&gt;넥스터즈 26기에 합류&lt;/b&gt;하게 되었다.&lt;/p&gt;
&lt;figure id=&quot;og_1738031880713&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Nexters 26기 지원 및 면접 후기&quot; data-og-description=&quot;지원을 시작하며Nexters 25기당시 나는 Nexters 25기에 지원했지만, 면접조차 보지 못하고 서류에서 탈락했다.&amp;nbsp;지원 문항들은 나에게 여러 질문을 던졌지만, 그 질문에 답하는 과정이 쉽지 않았다. &quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/248&quot; data-og-url=&quot;https://myvelop.tistory.com/248&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2MZW7/hyX71lmba2/PRG76I0ksF31iqJrIhAWmK/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bowkcE/hyX7YoCVBB/QWPEqIxTeGcR7XrPdTo9K0/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bg9tGf/hyX72dtgLD/UhkwxKQuyEO7QkmP31sUmK/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=0_0_1440_1440&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/248&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/248&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2MZW7/hyX71lmba2/PRG76I0ksF31iqJrIhAWmK/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bowkcE/hyX7YoCVBB/QWPEqIxTeGcR7XrPdTo9K0/img.jpg?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bg9tGf/hyX72dtgLD/UhkwxKQuyEO7QkmP31sUmK/img.jpg?width=1440&amp;amp;height=1440&amp;amp;face=0_0_1440_1440');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Nexters 26기 지원 및 면접 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;지원을 시작하며Nexters 25기당시 나는 Nexters 25기에 지원했지만, 면접조차 보지 못하고 서류에서 탈락했다.&amp;nbsp;지원 문항들은 나에게 여러 질문을 던졌지만, 그 질문에 답하는 과정이 쉽지 않았다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새로운 도전. PM&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥스터즈에 처음 합류함과 동시에, &lt;b&gt;PM 역할&lt;/b&gt;에 도전하는 무모한 도전을 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백엔드 개발자로서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 노력의 결실을 확인할 수 있는 시간이었다. &lt;b&gt;2년간의 현업 경험이 결코 헛되지 않았음을 실감했다. &lt;/b&gt;&lt;b&gt;&lt;/b&gt;이제는 개발 프로젝트의 흐름을 파악하고, &lt;b&gt;백엔드와 프론트엔드 팀의 일정을 조율하며 이끌어갈 수 있는 사람&lt;/b&gt;이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, &lt;b&gt;백엔드 개발자로서 직접 코드를 작성하는 시간은 거의 없었다.&lt;/b&gt; 대신, 프로젝트 전체를 설계하고, 도메인 모델링을 정의해 팀원들에게 제공하며, 코드 리뷰를 통해 개발의 방향성을 잡아주는 역할을 했다. &lt;s&gt;이거 완전 CTO 아잉교&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 팀원들의 &lt;b&gt;열정은 대단했다.&lt;/b&gt; 궁금한 것도 많고, 소통에도 누구보다 적극적이었다. PR에는 수십 개의 코멘트가 달렸고, 나 역시 이 과정을 통해 배운 것이 많았다. &lt;b&gt;내가 몰랐던 부분을 알아가고, 다른 사람의 코드를 읽으며 토론하는 과정이 무척 즐거웠다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk6Sgv/btsL37FKsRX/tgJwJh4wflcUdmRtF1kyt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk6Sgv/btsL37FKsRX/tgJwJh4wflcUdmRtF1kyt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk6Sgv/btsL37FKsRX/tgJwJh4wflcUdmRtF1kyt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk6Sgv%2FbtsL37FKsRX%2FtgJwJh4wflcUdmRtF1kyt1%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; alt=&quot;코드리뷰1&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;460&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u0toc/btsL4q6ggR3/k2PMYOgZTI53iKBLJSaqg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u0toc/btsL4q6ggR3/k2PMYOgZTI53iKBLJSaqg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u0toc/btsL4q6ggR3/k2PMYOgZTI53iKBLJSaqg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu0toc%2FbtsL4q6ggR3%2Fk2PMYOgZTI53iKBLJSaqg1%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; alt=&quot;코드리뷰2&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;460&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;460&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;h3 data-ke-size=&quot;size23&quot;&gt;PM으로서&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;b&gt;최소 50명에게 응답을 받아내겠다고 선언&lt;/b&gt;했다. 이를 위해 회사 사람들에게 일일이 찾아가 설문을 부탁하고, 친구들에게도 요청했다. 하지만 그것만으로는 부족하다고 느껴, 글또 커뮤니티의 자유 홍보 채널에 아래와 같은 글을 올려 추가로 홍보를 진행했다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cp79DO/btsL352iKQ2/tY9gs87yioL6ONluZ8HdZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cp79DO/btsL352iKQ2/tY9gs87yioL6ONluZ8HdZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cp79DO/btsL352iKQ2/tY9gs87yioL6ONluZ8HdZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcp79DO%2FbtsL352iKQ2%2FtY9gs87yioL6ONluZ8HdZ1%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; alt=&quot;글또에 자유 홍보&quot; loading=&quot;lazy&quot; width=&quot;2040&quot; height=&quot;712&quot; data-origin-width=&quot;2040&quot; data-origin-height=&quot;712&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;결과적으로 &lt;b&gt;48시간만에 103명의 설문&lt;/b&gt;을 받아, 리서치로부터 유의미한 데이터를 뽑아낼 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uiNej/btsL37eNzzh/yNQPKoKWS02dXVWlk3Sjk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uiNej/btsL37eNzzh/yNQPKoKWS02dXVWlk3Sjk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uiNej/btsL37eNzzh/yNQPKoKWS02dXVWlk3Sjk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuiNej%2FbtsL37eNzzh%2FyNQPKoKWS02dXVWlk3Sjk1%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; alt=&quot; 유저 리서치 결과&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;600&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;600&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;회사에서도 단순히 개발 업무만 하지 않고 기획자로서의 업무도 겸하고 있는데 PM의 역할을 할 때 큰 도움이 됐다. 덕분에 어떤 팀원에게는 백엔드 개발자가 아니라 기획자같다는 얘기를 듣기도 했다. &lt;s&gt;디자이너들 생각은 다를텐데&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 성공적으로 마치기 위해 최선을 다하고 있다. &lt;b&gt;일정을 산정하여 제시&lt;/b&gt;하고, 팀원들과 적극적으로 소통하며 현재의 &lt;b&gt;병목을 파악하고 해결하는 역할&lt;/b&gt;을 맡고 있다. 지금은 &lt;b&gt;일정을 최대한 맞추기 위해 백엔드 개발뿐만 아니라, 프론트엔드와 Flutter 개발&lt;/b&gt;까지 동시에 진행하며 바쁘게 지내고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리더로서&lt;/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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 리더란 최고의 업무 효율을 만들어내는 사람이라고 생각한다. 여기서 말하는 업무 효율이란, 팀원들의 희생을 강요하는 것이 아니다. 올바른 선택을 통해 &lt;b&gt;기회 비용을 최소화&lt;/b&gt;하고, 적절한 일정 조율로 &lt;b&gt;직군별 병목을 방지&lt;/b&gt;하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해선 &lt;b&gt;전략과 계획이 필수적&lt;/b&gt;이다. &lt;b&gt;팀원보다 몇 수 앞을 내다보고 미리 고민&lt;/b&gt;해야 더 효과적인 방향을 제시할 수 있으며, 팀원들의 헛수고를 줄일 수 있다. 특히, 시간이 부족할 때는 이러한 역량이 더욱 중요해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삶&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 내 나이만큼 책 읽기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;27권의 책 읽기라는 목표를 달성했다. 나에게 깊게 각인 책도 있고, 그저그런 책들도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나에게 가장 큰 도움이 되었던 &quot;오브젝트&quot;였고, 가장 좋았던 책은 몽테뉴의 &quot;에세&quot;였다.&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;2024년 내가 읽은 책은 아래와 같다.&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;랩걸, 호프 자런&lt;/li&gt;
&lt;li&gt;참을 수 없는 존재의 가벼움, 밀란 쿤데라&lt;/li&gt;
&lt;li&gt;반지의 제왕 1, J. R. R. 톨킨&lt;/li&gt;
&lt;li&gt;반지의 제왕 2, J. R. R. 톨킨&lt;/li&gt;
&lt;li&gt;반지의 제왕 3, J. R. R. 톨킨&lt;/li&gt;
&lt;li&gt;반지의 제왕 4, J. R. R. 톨킨&lt;/li&gt;
&lt;li&gt;이방인, 알베르 카뮈&lt;/li&gt;
&lt;li&gt;에세1, 몽테뉴&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;철학&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인간 불평등 기원론, 루소&lt;/li&gt;
&lt;li&gt;소크라테스 익스프레스, 에릭 와이너&lt;/li&gt;
&lt;li&gt;운명론, 키케로&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개발 관련 서적&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DDD 시작하기, 최범균&lt;/li&gt;
&lt;li&gt;프로그래머의 길 멘토에게 묻다, 데이브 후버&lt;/li&gt;
&lt;li&gt;리팩토링, 마틴 파울러&lt;/li&gt;
&lt;li&gt;SQL Anti-Patterns, 빌카윈&lt;/li&gt;
&lt;li&gt;구글 개발자는 이렇게 일한다, 타이터스 윈터스 외 2인&lt;/li&gt;
&lt;li&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초1, 알렉스 쉬&lt;/li&gt;
&lt;li&gt;클린 코드, 로버트 C. 마틴&lt;/li&gt;
&lt;li&gt;오브젝트, 조영호&lt;/li&gt;
&lt;li&gt;자바 ORM 표준 JPA 프로그래밍, 김영한&lt;/li&gt;
&lt;li&gt;Real MySQL1, 백은빈, 이성욱&lt;/li&gt;
&lt;li&gt;함께 자라기, 이창준&lt;/li&gt;
&lt;li&gt;성공과 실패를 결정하는 1%의 네트워크 원리, 츠토무 톤&lt;/li&gt;
&lt;li&gt;마이크로서비스 아키텍처 구축, 샘 뉴먼&lt;/li&gt;
&lt;li&gt;이펙티브 자바, 조슈아 블로크&lt;/li&gt;
&lt;li&gt;만들면서 배우는 클린 아키텍처, 톰 홈버그&lt;/li&gt;
&lt;li&gt;헤드퍼스트 디자인패턴, 에릭 프리먼 외 3인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 꾸준히 운동하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 주 5일정도 헬스장에 나갔고, 여유가 있을 땐 주 7일 운동을 하기도 했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 절주&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;올해 계획&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 1일 1커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해도 1일 1일 커밋! 꾸준히 해볼 생각이다!&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;예전에는 방문자 수에 엄청나게 큰 의미를 부여했었는데, 지금은 생각이 바뀌었다. 글을 작성함으로써, 내 생각이 정연해지고 그걸로 내가 성장할 수 있다면 충분하다는 생각이 들었다. 남들에게 좋은 글보다는 나에게 좋은 글을 위주로 작성해볼 생각이다. 나에게도 좋은 글이라면 누군가에게도 좋은 영향이 될 수 있지 않을까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>끄적끄적</category>
      <category>개발자</category>
      <category>회고</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/251</guid>
      <comments>https://myvelop.tistory.com/251#entry251comment</comments>
      <pubDate>Sun, 2 Feb 2025 14:50:03 +0900</pubDate>
    </item>
    <item>
      <title>HikariCP를 이해하면 풀 사이즈 설정이 보인다</title>
      <link>https://myvelop.tistory.com/250</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 데이터베이스 성능은 커넥션 풀의 효율성과 직결된다. 커넥션 풀은 데이터베이스 연결을 효율적으로 관리하는 핵심 요소다. 하지만 이를 제대로 이해하지 못한 채 기본 설정에 의존하는 경우도 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring은 HikariCP라는 뛰어난 성능과 신뢰성을 가진 커넥션 풀을 기본적으로 제공한다. 그러나 기본 설정만으로는 모든 상황에서 최적화된 성능과 안정성을 보장하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 HikariCP의 주요 개념과 작동 원리를 살펴보고, 적절한 풀 사이즈를 설정하는 방법에 대해 논의한다. 또한, 커넥션 풀과 관련된 대표적인 문제와 이를 방지하는 설정 전략도 함께 다룬다. HikariCP를 최적화하여 애플리케이션의 성능과 안정성을 극대화하는 방법을 알아보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. DB Connection&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB Conneciton이란 애플리케이션과 데이터베이스 서버가 통신할 수 있도록 연결하는 것을 의미한다. Pool을 사용하지 않고 DB를 연결하고 사용할 때 아래와 같은 과정을 거치게 된다. (Java 기준)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB 연결 정보를 담은 Connection 객체를 생성한다.&lt;/li&gt;
&lt;li&gt;Connection 객체를 사용해 DB 관련 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;DB 작업을 마치면 Connection 객체를 명시적으로 닫는다. &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;connection.close()&lt;/span&gt;&lt;/li&gt;
&lt;/ol&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;새로운 Connection을 생성할 때마다 DB 서버와 네트워크 연결을 설정해야 한다.&lt;/li&gt;
&lt;li&gt;Connection 객체를 생성하기 위해 서버의 리소스를 소모해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해 Connection Pool이라는 기술이 나오게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Connection Pool&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB Connection Pool이란 데이터베이스와의 연결을 효율적으로 관리하기 위해 미리 일정 수의 연결을 생성해 두고, 필요할 때 가져다 사용한 뒤 반환하도록 설계된 기술이다. 이를 통해 연결 생성 및 해제에 드는 오버헤드를 줄이고, 애플리케이션의 성능을 향상시키고, 자원을 아낄 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;561&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x8WbB/btsLwniNpRB/O4QHV4pyUYxDKgWd88B6T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x8WbB/btsLwniNpRB/O4QHV4pyUYxDKgWd88B6T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x8WbB/btsLwniNpRB/O4QHV4pyUYxDKgWd88B6T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx8WbB%2FbtsLwniNpRB%2FO4QHV4pyUYxDKgWd88B6T1%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; alt=&quot;Connection Pool&quot; loading=&quot;lazy&quot; width=&quot;905&quot; height=&quot;561&quot; data-origin-width=&quot;905&quot; data-origin-height=&quot;561&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HikariCP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP는 위에서 설명한 DB Connection Pool을 지원하는 라이브러리 중 하나로, 가볍고 빠른 성능을 제공하기에 많은 개발자들이 사용했고, Spring Boot 2.0부터는 기본 데이터베이스 pooling 시스템이 Tomcat Pool에서 HikariCP로 변경되었다. 아래는 Spring Boot&amp;nbsp; 2.0 Release Notes 중 HikariCP에 관련된 내용이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;HikariCP&lt;br /&gt;The default database pooling technology in Spring Boot 2.0 has been switched from Tomcat Pool to HikariCP. We&amp;rsquo;ve found that Hakari offers superior performance, and many of our users prefer it over Tomcat Pool.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이말인즉슨, Spring Boot 2.0 이상에서는 따로 설정을 해주지 않아도 자동으로 HikariCP Connection Pool을 사용한다는 얘기이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HikariCP Configuration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP에 대한 설정은 HikariCP Github에 상세하게 설명되어 있으니, 중요한 몇 가지 설정만 살펴보도록 하자.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; jdbcUrl&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DriverManager 기반의 JDBC 드라이버를 구성하기 위해 사용하는 설정이다. DB의 주소값을 넣어준다.&lt;/li&gt;
&lt;li&gt;만약 &quot;구식&quot; 드라이버를 함께 사용한다면 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;driverClassName&amp;nbsp;&lt;/span&gt;을 설정해줘야 한다. 일단 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;driverClassName&amp;nbsp;&lt;/span&gt;을 지정하지 않고 시도해본 다음, 에러가 발생한다면 그 때 찾아서 넣는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기초 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; username&amp;nbsp;&lt;/span&gt;&lt;/span&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의 사용자의 아이디&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; password&amp;nbsp;&lt;/span&gt;&lt;/span&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 사용자의 패스워드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚙️ &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;&amp;nbsp;driverClassName&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 HikariCP는 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;jdbcUrl&amp;nbsp;&lt;/span&gt;만으로 DriverManager를 통해 드라이버를 해결하려 시도하지만, 일부 오래된 드라이버는 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;driverClassName&amp;nbsp;&lt;/span&gt;를 명시해줘야 한다.&lt;/li&gt;
&lt;li&gt;따라서 드라이버를 찾을 수 없다는 명확한 오류 메시지가 표시되지 않는 한 이 속성은 생략해도 괜찮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; autoCommit&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection을 반환할 때 auto commit을 할 것인지에 대한 설정하는 요소다.&lt;/li&gt;
&lt;li&gt;Default는 true이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; connectionTimeout&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 Connection Pool로부터 Connection을 받기 위해 기다리는 시간을 설정하는 요소다.&lt;/li&gt;
&lt;li&gt;단위는 milliseconds이며, 실제 런타임에서 기다리는 시간이 초과되면 &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;SQLException이 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;Default는 30,000ms(30초)다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; idleTimeout&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection Pool에서 Connection이 유휴 상태로 존재할 수 있는 최대 시간을 설정한다.&lt;/li&gt;
&lt;li&gt;이 설정은 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;minimumIdle&amp;nbsp;&lt;/span&gt;이 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;maximumPoolSize&amp;nbsp;&lt;/span&gt;보다 작은 값으로 정의될 때만 적용된다.&lt;/li&gt;
&lt;li&gt;Connection Pool의 사이즈가 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&amp;nbsp;minimumIdle &lt;/span&gt;에 도달하면 유휴 커넥션은 더 이상 제거되지 않는다.&lt;/li&gt;
&lt;li&gt;Default는 600,000ms(10분)이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; keepaliveTime&amp;nbsp;&lt;/span&gt;&lt;/span&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;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;maxLifetime&amp;nbsp;&lt;/span&gt;보다 작아야 하며, 이 작업은 유휴 커넥션에서만 발생한다.&lt;/li&gt;
&lt;li&gt;주어진 커넥션에 대해 'keepalive'를 실행할 때가 되면, 해당 커넥션은 풀에서 제거된 후 'ping'을 실행하고, 다시 풀로 반환된다. 여기서 'ping'이란 JDBC4의 isValid() 메소드를 호출하거나, connectionTestQuery를 실행하는 것 중 하나다.&lt;/li&gt;
&lt;li&gt;최소 허용 값은 30,000ms(30초)이며, 몇 분 단위의 값이 적합하다고 하다.&lt;/li&gt;
&lt;li&gt;Default는 120,000ms로 12분이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt; maxLifetime&amp;nbsp;&lt;/span&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;이 값은 데이터베이스나 인프라에서 설정한 연결 제한 시간보다 몇 초 짧게 설정하는 것이 좋다. 참고로 MySQL에서는 &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;&amp;nbsp;SHOW VARIALBES LIKE 'wait_timeout'&amp;nbsp;&lt;/span&gt; 을 통해 연결 시간을 확인할 수 있다. (기본 8시간으로 설정되어 있다.)&lt;/li&gt;
&lt;li&gt;최소 허용 값은 30,000ms(30초)이며, 기본값은 1,800,000ms(30분)이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; connectionTestQuery&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connection.isValid()가 없는 레거시 드라이버를 위한 설정으로 특정 쿼리를 설정해 'ping'을 할 때 해당 쿼리를 사용하도록 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; minimumIdle&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&lt;/span&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;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;maximumPoolSize&amp;nbsp;&lt;/span&gt;보다 작아야 한다.&lt;/li&gt;
&lt;li&gt;HikariCP는 기본적으로 가능한 한 빠르고 효율적으로 추가 커넥션을 생성하려고 시도하지만, 이를 위한 시간과 리소스가 발생할 수밖에 없으므로 최대 성능을 보장하기 위해선 적정한 고정 크기 커넥션 풀로 작동하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;기본값은 &amp;nbsp;&lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;maximumPoolSize &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;과 동일하다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; maximumPoolSize&amp;nbsp;&lt;/span&gt;&lt;/span&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;기본값은 10이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚙️ &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;&amp;nbsp;poolName&amp;nbsp;&lt;/span&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 Pool에 사용자 정의 이름을 넣고 싶을 때 사용하는 설정이다.&lt;/li&gt;
&lt;li&gt;기본 값은 auto-generated이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java Bean 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hikari Connection Pool을 Spring에서 코드를 통해 Bean으로 등록하는 방법은 두 가지가 있다. HikariDataSource 객체를 생성하고 setter를 활용하는 방법과 HikariConfig 객체를 활용하는 방법이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HikariDataSource Setter 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1735105941821&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class DatabaseConfig {

  @Bean
  public DataSoucre dataSource() {
    HikariDataSource ds = new HikariDataSource();
    ds.setJdbcUrl(&quot;jdbc:mysql://localhost:3306/mydb&quot;);
    ds.setUsername(&quot;bell&quot;);
    ds.setPassword(&quot;1234&quot;);
    ds.setMaximumPoolSize(20);
    ds.setMinimumIdle(15);
    ds.setConnectionTimeout(5000);
    return ds;
  }
}&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;HikariConfig 객체 활용 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1735106087216&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class DatabaseConfig {

  @Bean
  public DataSoucre dataSource() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(&quot;jdbc:mysql://localhost:3306/mydb&quot;);
    config.setUsername(&quot;bell&quot;);
    config.setPassword(&quot;1234&quot;);
    config.setMaximumPoolSize(20);
    config.setMinimumIdle(15);
    config.setConnectionTimeout(5000);
    return new HikariDataSource(config);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Application.yaml Configuration 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 코드를 통해 Config 객체를 만들어 HikariConfig를 명시적으로 선언할 수도 있지만, 다른 방법을 사용할 수도 있다. 스프링 부트에서는 기본적으로 application.yaml에서 HikariCP 풀을 설정할 수 있게 해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1735106270809&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: bell
    password: 1234
    hikari:
      minimum-idle: 5           # 최소 유지할 유휴 커넥션 수
      maximum-pool-size: 10     # 최대 커넥션 풀 크기
      idle-timeout: 30000       # 유휴 커넥션을 유지할 시간 (ms)
      max-lifetime: 1800000     # 커넥션의 최대 수명 (ms)
      connection-timeout: 30000 # 커넥션 요청 대기 시간 (ms)
      pool-name: HikariPool     # 풀 이름 설정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. HikariCP 내부 동작 원리 맛보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP Github Wiki 중 &lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Down the Rabbit Hole(토끼굴로 들어가기)&lt;/a&gt;라는 글을 보면 HikariCP가 어떤 식으로 최적화를 진행했는지 자세히 살펴볼 수 있다.&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;성능과 Connection Pool을 생각할 때, 많은 사람들은 풀 자체가 성능 공식에서 가장 중요한 부분이라고 착각할 수도 있다. 하지만 반드시 그렇다고 말할 수는 없는게,&lt;span&gt;&amp;nbsp;&lt;/span&gt;getConnection()&lt;span&gt;&amp;nbsp;&lt;/span&gt;호출의 횟수는 다른 JDBC 작업에 비해 적은 편이다. 성능 향상의 상당 부분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Conneciton,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Statement&lt;span&gt;&amp;nbsp;&lt;/span&gt;등을 감싸는&lt;span&gt;&amp;nbsp;&lt;/span&gt;위임 객체 delegates의 최적화에서 이뤄진다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;거의 측정되지 않을 정도의 미세한 최적화가 많이 포함되어 있지만, 이러한 최적화가 결합되면 전체적인 성능이 크게 향상시키고 있다고 한다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;바이트코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP는 지금처럼 빨라지기 위해 바이트코드 수준의 엔지니어링을 넘어섰다고 한다.&amp;nbsp;Just-In-Time Compiler가 개발자를 도울 수 있도록 모든 트릭을 동원했다.&amp;nbsp;컴파일러의 바이트코드 출력과 JIT의 어셈블리 출력까지 연구해 주요 루틴을 JIT의 인라인 임계값 이하로 제한한다.&amp;nbsp;상속 계층 구조를 단순화하고, 멤버 볌수를 섀도잉하며, 형 변환을 제거했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ArrayList&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 최적화 중 하나는&amp;nbsp;ProxyConnection에 의해 열린&amp;nbsp;Statement&amp;nbsp;인스턴스를 추적하기 위해 사용되던&amp;nbsp;ArrayList&amp;lt;Statement&amp;gt;를 제거한 것이다.&amp;nbsp;기존에는 이 컬렉션에서 해야할 작업이 많았다.&amp;nbsp;Statement가 닫힐 때 해당&amp;nbsp;Statement&amp;nbsp;제거해야 한다.&amp;nbsp;Connection이 닫히면 반복을 통해 열려 있는 모든&amp;nbsp;Statement를 닫야하고, 컬렉션을 비워야 했다.&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;ArrayList의&amp;nbsp;get(int index)는 호출될 때마다 범위 검사를 하는데 이는 불필요한 오버헤드다.&amp;nbsp;remove(Object)는 컬렉션의 head부터 tail까지 스캔을 수행해야 하는데, 일반적인 JDBC 프로그래밍의 일반적인 패턴은 사용 후&amp;nbsp;Statement를 즉시 닫거나, 열린 순서의 역순으로 닫는 경우가 많다.&amp;nbsp;이런 경우 뒤에서부터 하는 스캔이 유리하다.&amp;nbsp;따라서 범위 검사를 제거하고, 스캔을 뒤에서부터 하는&amp;nbsp;&lt;b&gt;FastList&lt;/b&gt;라는 커스텀 클래스를 만들어 사용하고 있다고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConcurrentBag&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP는 고성능 Connection Pool 관리를 위해 ConcurrentBag이라는 구조체를 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠금 없는 컬렉션 lock-free collection이다.&amp;nbsp;C# .NET의&amp;nbsp;ConcurrentBag&amp;nbsp;클래스에서 착안했지만, 내부 구현은 상당히 다르다고 한다.&amp;nbsp;ConcurrentBag은 아래와 같은 기능을 제공한다.&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;ThreadLocal 캐싱&lt;/li&gt;
&lt;li&gt;Queue-stealing&lt;/li&gt;
&lt;li&gt;Direct hand-off 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이&lt;span&gt; &lt;/span&gt;기능을&lt;span&gt; &lt;/span&gt;통해&lt;span&gt; &lt;/span&gt;높은&lt;span&gt; &lt;/span&gt;수준의&lt;span&gt; &lt;/span&gt;동시성과&lt;span&gt; &lt;/span&gt;극도로&lt;span&gt; &lt;/span&gt;낮은&lt;span&gt; &lt;/span&gt;대기&lt;span&gt; &lt;/span&gt;시간&lt;span&gt;,&amp;nbsp;false-sharing&amp;nbsp;&lt;/span&gt;발생을&lt;span&gt; &lt;/span&gt;최소화한다&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;참고: False sharing&lt;/b&gt;&lt;br /&gt;분산된 일관성 캐시를 사용하는 시스템에서 가장 작은 리소스 블록 크기 단위로 캐시 메커니즘이 데이터를 관리할 때 발생할 수 있는 성능 저하 패턴&lt;br /&gt;시스템의 한 참여자가 다른 참여자에 의해 변경되지 않은 데이터를 주기적으로 접근하려고 할 때, 해당 데이터가 변경되고 있는 데이터와 동일한 캐시 블록을 공유하고 있다면, 캐싱 프로토콜은 논리적으로는 불필요함에도 불구하고 첫 번째 참여자가 전체 캐시 블록을 다시 로드하도록 강제할 수 있다.&lt;br /&gt;캐싱 시스템은 이 블록 내에서 이루어지는 활동에 대해 알지 못하며, 실제로 리소스를 공유 접근할 때 발생하는 오버헤드와 동일한 캐싱 시스템의 오버헤드를 첫 번째 참여자가 감당하도록 만든다.&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;invokevirtual vs invokestatic&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;HikariCP는&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;Connection,&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;Statement,&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;ResultSet&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;인스턴스를 위한 프록시를 생성하기 위해 처음에는&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;싱글턴 팩토리 singleton factory를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ProxyConnection의 경우, 정적 필드에 저장되어 있었다.(PROXY_FACTORY)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1736682554116&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
    return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1736682572101&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
  flags: ACC_PRIVATE, ACC_FINAL
  Code:
    stack=5, locals=3, args_size=3
       0: getstatic     #59                 // Field PROXY_FACTORY:Lcom/zaxxer/hikari/proxy/ProxyFactory;
       3: aload_0
       4: aload_0
       5: getfield      #3                  // Field delegate:Ljava/sql/Connection;
       8: aload_1
       9: aload_2
      10: invokeinterface #74,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
      15: invokevirtual #69                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
      18: return&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 정적 필드&lt;span&gt;&amp;nbsp;&lt;/span&gt;PROXY_FACTORY의 값을 가져오기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;getstatic&lt;span&gt;&amp;nbsp;&lt;/span&gt;호출이 있으며, 마지막으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;ProxyFactory&lt;span&gt;&amp;nbsp;&lt;/span&gt;인스턴스에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;getProxyPreparedStatement()를 호출하기 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;invokevirtual&lt;span&gt;&amp;nbsp;&lt;/span&gt;호출이 이뤄지는 것을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;HikariCP는 Javassist가 생성한 싱글턴 팩토리를 제거하고, Javassist가 메소드 본문을 생성하는 정적 메소드를 가진 final 클래스로 대체했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Javassist&lt;/b&gt;&lt;br /&gt;Java 애플리케이션에서 바이트코드 조작(Bytecode Manipulation)을 쉽게 수행할 수 있도록 도와주는 라이브러리&lt;/blockquote&gt;
&lt;pre id=&quot;code_1736682741367&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
    return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1736682752766&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
  flags: ACC_PRIVATE, ACC_FINAL
  Code:
    stack=4, locals=3, args_size=3
       0: aload_0
       1: aload_0
       2: getfield      #3                  // Field delegate:Ljava/sql/Connection;
       5: aload_1
       6: aload_2
       7: invokeinterface #72,  3           // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
      12: invokestatic  #67                 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
      15: areturn&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;getstatic 호출이 사라졌고,&lt;/li&gt;
&lt;li&gt;invokevirtual&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;호출이&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;invokestatic&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;호출로 대체되어 JVM에서 더 쉽게 최적화할 수 있게 되었으며,&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;스택 크기가 5개에서 4개로 줄었다. 이는&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;invokevirtual&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;호출의 경우, 스택에&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;ProxyFactory&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;인스턴스(this)가 암묵적으로 전달되기 때문이다. 또한&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;getProxyPreparedStatement()가 호출될 때 해당 값이 스택에서 추가적으로&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;pop되는 동작이 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 정적 필드 접근, 스택의 push와 pop 동작을 제거했으며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;호출 지점 callsite이 변경되지 않음을 보장하여 JIT 컴파일러가 호출을 더 쉽게 최적화할 수 있게 만들었다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;기타&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;그 외에도 코드의 모든 중요한 경로에서 명령어 수를 줄여, 각 메소드가 OS 스케줄러의 실행 퀀텀 내에 맞도록 최적화하여 캐시 라인 무효화로 인한 성능 저하를 피할 수 있었다고 한다.&lt;/span&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 최적의 Connection Pool Size를 찾아서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP의 내부 동작 원리를 대충 알아봤으니, 핵심 주제인 Pool Size에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP Github의 Wiki를 확인해보면 &lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;About Pool Sizing&lt;/a&gt;이라는 글이 존재한다. 여기서 제공하는 공식은 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;connections = ((core_count * 2) + effective_spindle_count)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 하나의 하드 디스크를 가진 4-Core i7 서버에서 가장 최적의 커넥션 풀 사이즈는 아래와 같이 도출해낼 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;connections = (4 * 2) + 1 = 9&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자를 반올림해서 10으로 설정하는 게 깔끔할 것이다. HikariCP 팀은 이 설정으로 3000명의 frontend 유저가 요청하는 6000TPS를 손쉽게 처리했다고 한다.&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;여기서 Spindle이란 전통적인 하드 디스크 드라이브(HDD)의 물리적 디스크 회전 장치를 가리키는 용어다. 정확히 말하자면 Spindle이란 플래터를 회전시키는 구성요소이고, HDD를 작동하게 만드는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dM2vXa/btsLx2j13SK/1FI4qw9HaKsCG1Crl3EEzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dM2vXa/btsLx2j13SK/1FI4qw9HaKsCG1Crl3EEzk/img.png&quot; data-alt=&quot;이미지 출처: wikipedia&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dM2vXa/btsLx2j13SK/1FI4qw9HaKsCG1Crl3EEzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdM2vXa%2FbtsLx2j13SK%2F1FI4qw9HaKsCG1Crl3EEzk%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; alt=&quot;Hard Disk&quot; loading=&quot;lazy&quot; width=&quot;767&quot; height=&quot;657&quot; data-origin-width=&quot;767&quot; data-origin-height=&quot;657&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미지 출처: wikipedia&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식의 effective_spindle_count가 의미하는 것은 병렬 처리 가능한 디스크의 I/O 경로 수를 추정한 값이다. 예를 들어, 단일 HDD라면 이 값이 1이 될 것이다. 반면 RAID 구성을 통해 병렬 처리 가능한 디스크의 수가 5개라면 이 수치가 5가 될 것이다. HDD를 동작 시킬 수 있는 Spindle 하나당 +1을 해주면 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pool Locking&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위에서 제시한 공식이 항상 정답인 것은 아니다. 단일 요청이 여러 커넥션을 획득해야 하는 경우 데드락의 가능성은 존재한다. 이런 경우  풀 크기를 늘려서 문제를 해결해야할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hikari CP 팀에서 데드락을 피하기 위해 제안하는 공식은 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;pool size = Tn * (Cm - 1) + 1&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Tn은 스레드의 최대 개수를 의미하고, Cm은 하나의 스레드에서 동시에 발생하는 커넥션의 수를 의미한다.&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;예를 들어, 하나의 작업을 진행할 때, 5개의 비동기 작업이 스레드에 의해 실행되고, 하나의 스레드당 4개의 커넥션이 필요하다고 가정해보자. 그렇다면 풀 사이즈는 아래와 같이 도출될 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;pool size = 5 * (4 - 1) + 1 = 16&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DBMS&amp;nbsp;최대 커넥션의 수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 DBMS는 최대 커넥션 사이즈를 지정할 수 있으며, 관련된 환경변수는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL: &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;&amp;nbsp;max_connections&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;PostgreSQL: &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: left;&quot;&gt;&amp;nbsp;max_connections&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Oracle: &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;&amp;nbsp;sessions&amp;nbsp;&lt;/span&gt; &amp;amp; &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;&amp;nbsp;processes&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;MSSQL: &lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;&amp;nbsp;user connections&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 DB를 사용하는 애플리케이션의 총 커넥션 수는 DBMS에 설정된 최대 커넥션 수를 넘지 않는 선에서 설정되어야만 한다. 만약 MySQL을 사용할 경우, &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: left;&quot;&gt;&amp;nbsp;max_connections &lt;/span&gt;보다 많은 양의 연결을 시도하려고 하면 아래와 같은 에러가 발생하며 문제가 생길 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ERROR 1040 (HY000): Too many connections&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서비스 트래픽을 감당하기 위해 DBMS의 기본 설정 값보다 많은 커넥션이 필요하다면 설정값을 변경할 수도 있다. 하지만 너무 높은 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: left;&quot;&gt;&amp;nbsp;max_connections&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 값은 운영 중 불필요한 연결을 방치할 가능성을 높이게 된다. 또한 connection은 생각보다 자원을 많이 사용하기 때문에 connection 수의 증가에 따라 CPU나 RAM을 얼마나 사용하는지 확인하고, 성능에는 문제가 없는지 따로 확인해야 한다. 따라서 적절한 값을 설정할 수 있도록 해야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Hikari CP와 Metrics&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;지표 수집, 추적, 감사 등의 모니터링을 쉽게 할 수 있는 다양한 편의 기능을 제공하는&lt;/span&gt; Actuator라는 툴을 기본으로 제공한다. 이는 Micrometer라는 벤더 독립적인 인터페이스를 내장하여 사용한 것으로 HikariCP에 대한 수치도 Actuator를 통해 수집되게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메트릭 상세 보기&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 74.8837%; height: 202px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;메트릭 이름&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;현재 Connection Pool 사이즈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.active&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;현재 사용 중인 활성 Conneciton 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.idle&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;현재 유휴 상태의 Connection 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.pending&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;현재 대기 중인 Connection 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.creation&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;생성된 Connection 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.timeout&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;Connection 요청 시간 초과 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.acquire&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;Connection 획득하는 데 걸린 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.usage&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;Connection 사용 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.max&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;설정된 최대 Connection Pool 사이즈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;hikaricp.connections.min&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 18px;&quot;&gt;설정된 최소 유휴 Connection Pool 사이즈&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다시 HikariCP Configuration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 HikariCP의 자주 사용하는 Configuration 중 설명하지 않은 두 가지 요소가 있다. 둘 설정 값은 메트릭과 깊은 관계가 있기 때문에 현재 목차에서 설명하는 것이 맞다고 생각해 따로 빼두었다.&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;metricRegistry&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Codahale/Dropwizard MetricRegistry의 인스턴스를 지정하여 풀에서 다양한 메트릭을 기록하도록 하는 옵션이다.&lt;/li&gt;
&lt;li&gt;기본 값은 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;⚙️&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;healthCheckRegistry&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Codahale/Dropwizard HealthCheckRegistry의 인스턴스를 지정하여 풀에서 현재 상태 정보를 보고하도록 한다.&lt;/li&gt;
&lt;li&gt;기본 값은 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 위의 설정을 따로 해줄 필요가 없다고 봐도 무방하다. Spring Boot Actuator와 HikariCP가 통합되었기 때문에 별도의 MetricRegistry를 설정하지 않아도 자동화된 메트릭 수집 및 관리가 가능하다! 만약 설정을 커스터마이징하고 싶다면 Dropwizard 등을 활용해볼 수 있을 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Metrics 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 actuator 의존성이 필요하다. SpringBoot에서 제공하는 starter-actuator 의존성을 추가하자.&lt;/p&gt;
&lt;pre id=&quot;code_1735180043520&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-actuator&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP의 메트릭을 직접 확인하고 싶다면 아래와 같이 application.yaml에서 /actuator/metrics의 주소를 열어주고, 보여줄 metrics를 enable 상태로 만들어줘야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1735179586151&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management:
  metrics:
    enable:
      hikari: true
  endpoints:
    web:
      exposure:
        include: metrics&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 서버를 시작해보자. 주소(localhost:8080 기준)로 접근할 때, localhost:8080/actuator/metrics로 접근하면 전체 메트릭을 확인해볼 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;527&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ847f/btsLxjmxXRv/TW46B3JSNaKDiLB38wLlx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ847f/btsLxjmxXRv/TW46B3JSNaKDiLB38wLlx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ847f/btsLxjmxXRv/TW46B3JSNaKDiLB38wLlx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ847f%2FbtsLxjmxXRv%2FTW46B3JSNaKDiLB38wLlx0%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; alt=&quot;actuator metrics&quot; loading=&quot;lazy&quot; width=&quot;527&quot; height=&quot;393&quot; data-origin-width=&quot;527&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 names 중 하나를 선택에 현재 path의 뒤에 넣어주면 상세 메트릭 정보를 확인할 수 있다. 예를 들어 localhost:8080/actuator/metrics/hikaricp.connections.acquire에 접근한다고 해보자. 그러면 아래와 같은 JSON 파일을 응답받게 될 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1735179972544&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;hikaricp.connections.acquire&quot;,
  &quot;description&quot;: &quot;Connection acquire time&quot;,
  &quot;baseUnit&quot;: &quot;seconds&quot;,
  &quot;measurements&quot;: [
    {
      &quot;statistic&quot;: &quot;COUNT&quot;,
      &quot;value&quot;: 10
    },
    {
      &quot;statistic&quot;: &quot;TOTAL_TIME&quot;,
      &quot;value&quot;: 0.019
    },
    {
      &quot;statistic&quot;: &quot;MAX&quot;,
      &quot;value&quot;: 0.01
    }
  ],
  &quot;availableTags&quot;: [
    {
      &quot;tag&quot;: &quot;pool&quot;,
      &quot;values&quot;: [
        &quot;write&quot;,
        &quot;read&quot;
      ]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Metrics 시각화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집된 Metrics를 활용해 애플리케이션의 리소스를 모니터링할 수 있으며, 아래와 같은 지표를 시각적으로 확인할 수 있게 된다. &lt;b&gt;커넥션 요청 시간 초과 횟수&lt;/b&gt;, &lt;b&gt;커넥션을 획득하는 데 걸린 시간&lt;/b&gt; 등을 확인하여 DB Connection 수를 조정하는 등 내 프로젝트에 유의미한 결과를 가져올 수 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDX5SI/btsLzcUen7k/i31k5O1MkJuDewFwzjfjqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDX5SI/btsLzcUen7k/i31k5O1MkJuDewFwzjfjqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDX5SI/btsLzcUen7k/i31k5O1MkJuDewFwzjfjqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDX5SI%2FbtsLzcUen7k%2Fi31k5O1MkJuDewFwzjfjqk%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; alt=&quot;Metric 시각화&quot; loading=&quot;lazy&quot; width=&quot;957&quot; height=&quot;399&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;399&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;유휴 커넥션이 존재한다고 가정했을 때, 커넥션을 획득하는 데 걸리는 시간은 보통 1ms 미만이라고 한다. 만약 1ms 이상의 시간이 걸린다면 어떤 문제가 있는 것일 수도 있으니, DB 관련된 지표들을 면밀히 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. HikariCP와 DB Replication&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB Replication이란 하나의 데이터베이스에 대하여 여러 개의 복제본을 만들어, 데이터의 고가용성과 성능 최적화를 도모하는 데이터베이스 관리 기술이다.&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;여기 DB의 분류는 크게 Primary(Master) DB와 Replica(Secondary/Slave) DB 두 가지 나뉘게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Primary DB: INSERT, UPDATE ,DELETE 등 데이터베이스의 데이터에 변화를 일으키는 작업을 수행한다.&lt;/li&gt;
&lt;li&gt;Replica DB: Primary DB를 복제한 DB로 SELECT와 같이 조회하는 작업을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통의 웹 서비스는 쓰기:읽기 작업의 비율이 1:9 혹은 2:8 정도로 차이가 많이 나며, 또한 쓰기에 비해 읽기의 시간이 더 오래 걸리므로 읽기 전용의 DB를 분리하여 처리를 분산하는 것이 효율적이기 때문에 위와 같이 분류된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Read/Write Database 분리 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hikari DB에서 분리된 DB를 사용하려면 각각의 DB에 대한 커넥션 풀 설정과 읽기/쓰기 작업에 대한 분기 처리를 추가해줘야 한다. 아래 예시를 확인해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DataSourceConfig.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1735125209428&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class DataSourceConfig {

  private static final String PRIMARY_DATASOURCE_KEY = &quot;READ&quot;;
  private static final String REPLICA_DATASOURCE_KEY = &quot;WRITE&quot;;

  @Bean
  @ConfigurationProperties(prefix = &quot;spring.datasource.primary&quot;)
  public HikariConfig writeHikariConfig() {
    return new HikariConfig();
  }

  @Bean
  public DataSource writeDataSource(final HikariConfig writeHikariConfig) {
    HikariDataSource dataSource = new HikariDataSource(writeHikariConfig);
    return dataSource;
  }

  @Bean
  @ConfigurationProperties(prefix = &quot;spring.datasource.replica&quot;)
  public HikariConfig readHikariConfig() {
    return new HikariConfig();
  }

  @Bean
  public DataSource readDataSource(final HikariConfig readHikariConfig) {
    HikariDataSource dataSource = new HikariDataSource(readHikariConfig);
    return dataSource;
  }

  @Bean
  @DependsOn({&quot;writeDataSource&quot;, &quot;readDataSource&quot;})
  public DataSource routeDataSource() {
    final AbstractRoutingDataSource dataSourceRouter =
        new AbstractRoutingDataSource() {
          @Override
          protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                ? REPLICA_DATASOURCE_KEY
                : PRIMARY_DATASOURCE_KEY;
          }
        };

    final DataSource writeDataSource = writeDataSource(writeHikariConfig());
    final DataSource readDataSource = readDataSource(readHikariConfig());
    final Map&amp;lt;Object, Object&amp;gt; dataSources =
        Map.ofEntries(
            entry(WRITE_DATASOURCE_KEY, writeDataSource),
            entry(READ_DATASOURCE_KEY, readDataSource));
    dataSourceRouter.setTargetDataSources(dataSources);
    dataSourceRouter.setDefaultTargetDataSource(writeDataSource);

    return dataSourceRouter;
  }

  @Bean
  @Primary
  @DependsOn({&quot;routeDataSource&quot;})
  public DataSource dataSource() {
    return new LazyConnectionDataSourceProxy(routeDataSource());
  }
}&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.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1735125629507&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    read:
      username: bell
      password: 1234
      jdbc-url: jdbc:mysql://mydb-host/mydb
      maximum-pool-size: 10
      minimum-idle: 8
      connection-timeout: 5000
      keepalive-time: 60000
      idle-timeout: 300000
      pool-name: &quot;READ&quot;
      read-only: true
    write:
      username: bell
      password: 1234
      jdbc-url: jdbc:mysql://mydb-host/mydb
      maximum-pool-size: 10
      minimum-idle: 8
      connection-timeout: 5000
      keepalive-time: 60000
      idle-timeout: 300000
      pool-name: &quot;WRITE&quot;
      read-only: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd; color: #333333;&quot;&gt;@ConfigurationProperties&lt;/span&gt;라는 어노테이션을 활용함으로써 application.yaml의 spring.datasource.primary의 요소를 가져와 HikariConfig 객체에 자동으로 주입해주게 된다. HikariDataSource는 Config를 주입받아 그 값 그대로 객체를 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1735125690264&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@ConfigurationProperties(prefix = &quot;spring.datasource.primary&quot;)
public HikariConfig writeHikariConfig() {
  return new HikariConfig();
}
  
@Bean
public DataSource writeDataSource(final HikariConfig writeHikariConfig) {
  return new HikariDataSource(writeHikariConfig);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 AbstractRoutingDataSource라는 클래스가 등장하게 된다. AbstractRoutingDataSource는 spring-jdbc 모듈에 포함되어 있는 클래스로 DataSource 인터페이스를 구현한다. determineCurrentLookupKey라는 메소드를 오버라이드해 구현하면 되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionSynchronizationManager의 isCurrentTransactionReadOnly메소드를 사용해 현재 트랜잭션이 readOnly로 설정되어 있다면 Replica DB로, readOnly가 아니라면 Primary DB로 라우팅하게 만들 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1735125833524&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@DependsOn({&quot;writeDataSource&quot;, &quot;readDataSource&quot;})
public DataSource routeDataSource() {
  final AbstractRoutingDataSource dataSourceRouter =
      new AbstractRoutingDataSource() {
        @Override
        protected Object determineCurrentLookupKey() {
          return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
              ? REPLICA_DATASOURCE_KEY
              : PRIMARY_DATASOURCE_KEY;
        }
      };

  final DataSource writeDataSource = writeDataSource(writeHikariConfig());
  final DataSource readDataSource = readDataSource(readHikariConfig());
  final Map&amp;lt;Object, Object&amp;gt; dataSources =
      Map.ofEntries(
          entry(WRITE_DATASOURCE_KEY, writeDataSource),
          entry(READ_DATASOURCE_KEY, readDataSource));
  dataSourceRouter.setTargetDataSources(dataSources);
  dataSourceRouter.setDefaultTargetDataSource(writeDataSource);

  return dataSourceRouter;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 RouterDataSource를 기본 dataSource로 사용하도록 IoC 컨테이너에 등록해주면 모든 준비는 완료된다.&lt;/p&gt;
&lt;pre id=&quot;code_1735125969938&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@Primary
@DependsOn({&quot;routeDataSource&quot;})
public DataSource dataSource() {
  return new LazyConnectionDataSourceProxy(routeDataSource());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 트랜잭션을 사용할 때 조회를 하는 서비스에서 @Transactional 어노테이션을 달아주고, readOnly 옵션을 사용함으로써 DB Routing이 자동으로 이뤄지게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1735126264324&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MemberService {

  @Transactional(readOnly = true)
  public Member getMember(Long id) {...}
  
  @Transactional
  public Member saveMember(Member member) {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분리된 DB의 Metrics 수집&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 PoolName이 중요해진다. 먼저 HikariCP Metrics가 어떻게 수집되고 있는지 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;availableTags 배열의 values 배열에 &quot;write&quot;와 &quot;read&quot;로 수집되고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1735110059566&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;hikaricp.connections&quot;,
  &quot;description&quot;: &quot;Total connections&quot;,
  &quot;measurements&quot;: [
    {
      &quot;statistic&quot;: &quot;VALUE&quot;,
      &quot;value&quot;: 10
    }
  ],
  &quot;availableTags&quot;: [
    {
      &quot;tag&quot;: &quot;pool&quot;,
      &quot;values&quot;: [
        &quot;write&quot;,
        &quot;read&quot;
      ]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 수집된 PoolName은 시각화를 할 때 요긴하게 사용된다. 아래는 ElasticSearch의 Kibana Dashboard이다. 해당 메트릭의 PoolName은 labels.pool이라는 Field 값으로 선택할 수 있고, 일치하는 PoolName을 골라 필터링을 할 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QezKB/btsLxjmg7Hz/DsnfAWE3kK4YZZfVCBnPT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QezKB/btsLxjmg7Hz/DsnfAWE3kK4YZZfVCBnPT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QezKB/btsLxjmg7Hz/DsnfAWE3kK4YZZfVCBnPT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQezKB%2FbtsLxjmg7Hz%2FDsnfAWE3kK4YZZfVCBnPT1%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; alt=&quot;Kibana Dashboard Visualization&quot; loading=&quot;lazy&quot; width=&quot;957&quot; height=&quot;314&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 PoolName 조건을 통해 유의미한 값을 넣어 그래프를 만들어낼 수도 있을 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bev1fv/btsLzDKCWcX/FkYNtx6LG0klaDly9rK000/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bev1fv/btsLzDKCWcX/FkYNtx6LG0klaDly9rK000/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bev1fv/btsLzDKCWcX/FkYNtx6LG0klaDly9rK000/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbev1fv%2FbtsLzDKCWcX%2FFkYNtx6LG0klaDly9rK000%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; alt=&quot;Kibana Dashboard visualization formula&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;540&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;540&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;결과적으로 Read/Write DB 각각의 Connection Pool을 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot Actucator에서 제공하는 다양한 메트릭을 구분된 값으로 확인해 Primary와 Replica DB 중 필요한 DB의 커넥션의 사이즈를 늘리는 식으로 개선이 가능해진다는 점에서 중요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;455&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DEZUQ/btsLwuvyPCC/WXxk6qkp5Hj8nOsF6KJYc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DEZUQ/btsLwuvyPCC/WXxk6qkp5Hj8nOsF6KJYc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DEZUQ/btsLwuvyPCC/WXxk6qkp5Hj8nOsF6KJYc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDEZUQ%2FbtsLwuvyPCC%2FWXxk6qkp5Hj8nOsF6KJYc0%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; alt=&quot;Metric 시각화 결과&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;455&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;455&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP는 뛰어난 성능, 효율성, 그리고 신뢰성을 바탕으로 커넥션 풀의 표준으로 자리 잡았다. 특히 낮은 대기 시간과 최소한의 리소스 소비로 다른 커넥션 풀과 차별화된 성능을 제공한다.&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;Spring Boot는 HikariCP를 기본 커넥션 풀로 채택하며, 설정의 대부분을 자동으로 구성해주는 편리함을 제공한다. 이러한 기본 설정만으로도 강력한 성능을 발휘할 수 있지만, 설정을 깊이 이해하지 못한다면 커넥션 풀로 인한 장애를 경험할 가능성이 크다. 예를 들어, 풀 크기를 적절히 설정하지 않으면 'Too many connections' 또는 'Connection timeout'과 같은 문제가 발생할 수 있다.&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;HikariCP의 작동 원리와 최적화 방법을 이해하고, 환경에 맞는 설정을 적용하면 애플리케이션의 성능과 안정성을 극대화할 수 있다. 이는 단순한 성능 개선을 넘어 서비스의 신뢰성을 확보하는 핵심 요소이다. 실무에서 HikariCP를 제대로 이해하고 활용하는 것은 필수다.&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;&amp;nbsp;&lt;/p&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://github.com/brettwooldridge/HikariCP&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HikariCP Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HikariCP Github Wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/spring-boot-2.0-release-notes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Boot 2.0 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Wikipedia&lt;/li&gt;
&lt;li&gt;Real MySQL | 백은빈, 이성욱 지음&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://saramin.github.io/2023-04-27-order-error/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[장애회고] ORM(JPA) 사용 시 예상치 못한 쿼리로 인한 HikariCP 이슈&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>db replication</category>
      <category>hikari cp</category>
      <category>Spring</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/250</guid>
      <comments>https://myvelop.tistory.com/250#entry250comment</comments>
      <pubDate>Sun, 12 Jan 2025 21:20:49 +0900</pubDate>
    </item>
    <item>
      <title>Nexters 26기 지원 및 면접 후기</title>
      <link>https://myvelop.tistory.com/248</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfDOGj/btsLkLJ1Aiw/gX5NtwO77GSqpgzKkAScV1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfDOGj/btsLkLJ1Aiw/gX5NtwO77GSqpgzKkAScV1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfDOGj/btsLkLJ1Aiw/gX5NtwO77GSqpgzKkAScV1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfDOGj%2FbtsLkLJ1Aiw%2FgX5NtwO77GSqpgzKkAScV1%2Fimg.jpg&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;1440&quot; height=&quot;1440&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1440&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;h2 data-ke-size=&quot;size26&quot;&gt;지원을 시작하며&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nexters 25기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 나는 Nexters 25기에 지원했지만, 면접조차 보지 못하고 서류에서 탈락했다.&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재도전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 아쉬움과 부끄러움이 교차했다. 내가 어떤 점이 부족했는지 진지하게 돌아보게 되었다. 그동안 미뤄왔던 나 자신에 대한 정리를 시작하며, 생각을 체계적으로 정리하기 시작했다.&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;여기서 멈추지 않고, 정리한 소재들을 활용해 새로운 도전에 나섰다. 오픈소스 컨트리뷰션 아카데미에 지원한 것이다. 운이 좋게도 10대 1이 넘는 경쟁률을 뚫고 선발되어, 하반기에는 오픈소스 기여 활동에 몰두할 수 있었다. 모든 것은 Nexters에서의 실패 덕분이었다는 생각이 들었다.&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;그렇게 오픈소스 활동이 마무리될 즈음, Nexters 26기 모집 소식이 들려왔다. 망설임 없이 재도전하기로 마음먹었다. 이전의 실패가 있었기에 이번에는 더욱 확신을 가지고 도전할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서류 지원&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지원 문항 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 지원서를 작성하면서 모든 문항을 꽉 채우려 노력했다. 글자수 제한 때문에 아쉬움을 남긴 부분도 있었다. 특히,&amp;nbsp;&quot;함께 하고 싶은 동료&quot;로 보이기 위해 어떤 이야기를 담아야 할지 고민이 깊었다.&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;a href=&quot;https://imksh.com/108&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;넥스터즈 지원자 서류 검토 후기&lt;/a&gt;라는 글을 읽으며, &quot;장기적인 목표&quot;와 &quot;열정&quot;. 그리고 &quot;가독성&quot;을 중요하게 본다는 점을 알게 되었고, 이를 중심으로 작성하려 애썼다.&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;하지만 내가 가진 &quot;열정&quot;을 글로 보여주는 건 쉽지 않았다. 표현력이 부족하다고 느꼈고, 그 때 문해일님의 이야기가 떠올랐다. 최근 그의 영상&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(출처:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://www.youtube.com/watch?v=7VNymOuWRwo&amp;amp;t=643s&quot;&gt;링크&lt;/a&gt;)을 보며, 진정한 열정이 무엇인지 깨달은 순간이 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;ldquo;내가 사랑하는 일을 하더라도 그것 하나를 제대로 이해하는 과정은 정말 어렵고 힘들고 고통스럽습니다. 사랑하는 일이라서 더 피눈물 나요. 하지만 몰입의 시간을 경험해 봐야 이해했다고 느끼는 순간이 옵니다. 그렇게 탄생한 그 사람의 견해는 본질에 다가선 새로운 가치를 가지고 있어요.&amp;rdquo;&lt;br /&gt;(유튜버 문해일)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문장은 나의 상황과 맞닿아 있었다. 나 역시 &quot;개발&quot;이라는 분야를 깊이 이해하기 위해 대부분의 시간을 사용하며, 어렵지만 몰입의 과정을 즐기고 있다. 문해일 님의 말처럼 이 과정은 힘들지만, 이내 나만의 견해와 새로운 가치를 만들어내는 밑거름이 된다고 믿는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지원서에도 이 문장을 인용하며 내가 가진 열정을 드러내고자 했다. 내가 사랑하는 일을 향한 진심 어린 고민과 몰입이 Nexters에서 함께할 동료들에게도 공감되길 바라는 마음이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이력서 및 포트폴리오 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넥스터즈를 준비하면서 이력서를 정리하고, 주변 지인들에게 피드백도 받아 부족한 점을 보완했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이력서와 서류 지원 질문에 너무 많은 시간을 쏟다 보니 포트폴리오 준비 시간이 촉박했다. &lt;b&gt;제출일 당일에는 반차를 내고 작업&lt;/b&gt;했지만, 완성도 면에서 아쉬움이 남았다. 준비를 한 번에 몰아서 하다 보니 제대로 된 결과물을 내기 어려웠다.&lt;/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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 이번엔 서류에 통과했고 면접 기회를 얻을 수 있었다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;227&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBuBhG/btsLc2SfrVb/YKz1BPMQNTBzY3CPP8kDH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBuBhG/btsLc2SfrVb/YKz1BPMQNTBzY3CPP8kDH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBuBhG/btsLc2SfrVb/YKz1BPMQNTBzY3CPP8kDH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBuBhG%2FbtsLc2SfrVb%2FYKz1BPMQNTBzY3CPP8kDH1%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; alt=&quot;서류 합격&quot; loading=&quot;lazy&quot; width=&quot;614&quot; height=&quot;227&quot; data-origin-width=&quot;614&quot; data-origin-height=&quot;227&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;25기를 지원했던 경험 덕분에 질문 리스트를 미리 알고 있었고, 그에 대해 고민을 해 온 시간이 있었기에 지원서를 더 수월하게 작성할 수 있었던 점이 합격에 큰 도움이 되었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;면접&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 전부터 긴장이 심했다. 이전에 YAPP에서 백엔드 면접을 보며 실수했던 장면들이 스쳐 지나가면서, 긴장이 갑자기 확 올라왔다.&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;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;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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인성 면접&lt;/h3&gt;
&lt;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;&quot;안 됩니다&quot;라는 얘기를 하지 않는 개발자가 되었다고 작성해주셨는데, 만약 기술적인 한계로 인해 구현할 수 없는 상황이라면 어떻게 할 것인지&lt;/b&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; =&amp;gt; 개인적으로 이 부분의 답변이 굉장히 아쉬웠다고 생각한다. 복잡한 비즈니스 로직을 간단하게 설명하려다보니 전혀 전달력 없게 얘기를 했던 것 같다. 차라리 최근에 만들었던 성장곡선을 주제로 얘기를 했다면 더 쉽게 설명할 수 있었을 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;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;b&gt;Future와 같은 것을 사용할 수도 있었는데, 왜 ThreadPool과 @Async 방식을 사용했는지&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이그레이션 하기 전의 기술은 어떤 기술을 사용하고 있었는지&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Slow Query 감지는 어떤 방식으로 이뤄지고, 그 쿼리를 개선하기 위해 어떤 것을 했는지&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 코드를 2000건 이상 작성하는 게 쉽지 않은 일이라고 생각. 테스트를 작성하다보면 비즈니스 경계가 모호해지는 문제 등이 발생할 수도 있음. 테스트를 작성할 때 어떤 점을 중요하게 생각하시는지 설명해주세요.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 그 어떤 것을 지원할 때보다 이번에는 더 열심히 준비했다. 서류부터 면접까지 전력을 다했다.&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;면접을 본 경험만으로도 나에게는 큰 의미가 있었다. 당연히 준비했어야 할 질문조차 떠올리지 못했다는 사실이 충격적이었다. 물론, 모든 것을 일주일 만에 준비하기에는 시간이 부족했던 것도 사실이다. 하지만, 평소에 이슈가 발생할 때마다 꾸준히 정리해 두었다면 큰 어려움 없이 대응할 수 있었을 것이다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;쉽게 설명할 수 없으면 제대로 아는 것이 아니다.&amp;rdquo;라는 말처럼, 나는 과연 지금 내가 배운 것들을 쉽게 설명할 수 있는가? 스스로에게 이 질문을 던졌을 때, 확신을 가지고 &amp;ldquo;그렇다&amp;rdquo;고 대답하기는 어려웠다.&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;&quot;내가 정말 이해했을까?&quot;, &quot;이해한 내용을 구체적인 예시로 들 수 있을까?&quot;와 같은 질문을 끊임없이 스스로에게 던지며, 지금까지 배운 지식을 되짚고 정리해야겠다는 다짐을 하게 되었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6pg4s/btsLl9JJ98J/5RRPEcTpM8YnDPKe9NM611/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6pg4s/btsLl9JJ98J/5RRPEcTpM8YnDPKe9NM611/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6pg4s/btsLl9JJ98J/5RRPEcTpM8YnDPKe9NM611/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6pg4s%2FbtsLl9JJ98J%2F5RRPEcTpM8YnDPKe9NM611%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; alt=&quot;합격메일&quot; loading=&quot;lazy&quot; width=&quot;875&quot; height=&quot;261&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;261&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;그렇게 기대하지 않았는데 합격했다!!&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;</description>
      <category>대외활동/IT커뮤니티</category>
      <category>Nexters</category>
      <category>nexters 26기</category>
      <category>면접</category>
      <category>백엔드 개발자</category>
      <category>지원서 작성</category>
      <category>합격</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/248</guid>
      <comments>https://myvelop.tistory.com/248#entry248comment</comments>
      <pubDate>Sat, 14 Dec 2024 15:28:20 +0900</pubDate>
    </item>
    <item>
      <title>나만의 Swagger UI 서버, 쿠버네티스에서 운영하기</title>
      <link>https://myvelop.tistory.com/247</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI에서 여러 서비스에 대한 문서를 효율적으로 관리하려면, 문서 파일의 저장소와 접근 방식에 대한 고민이 필요하다. 이 글에서는 Swagger UI를 쿠버네티스 환경에서 구성하는 방법과, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Object Storage를 활용해&lt;/span&gt; Swagger 문서를 제공하는 방법을 다룬다. 이를 통해 자동화된 문서 관리, 안정적인 시스템 구축을 목표로 한다. 또한, Swagger UI를 조직의 필요에 맞게 커스터마이징하여 제공하는 방법도 함께 살펴볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Swagger UI&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. Swagger UI란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI는 swagger-api Organization의 핵심 프로젝트이다. 사용자에게 직관적인 UI를 제공하여 API 제공자와 사용자 간의 협업에 큰 도움을 주는 툴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Organization: &lt;a href=&quot;https://github.com/swagger-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/swagger-api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Swagger UI Project: &lt;a href=&quot;https://github.com/swagger-api/swagger-ui&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/swagger-api/swagger-ui&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. Swagger UI의 기능 간단 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI는 &lt;b&gt;HTTP URL 경로에 접근해 문서를 로드하도록 설계&lt;/b&gt;되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rdvc6/btsKT7Nr6GU/8ht6HmEz10jIUbObibowEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rdvc6/btsKT7Nr6GU/8ht6HmEz10jIUbObibowEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rdvc6/btsKT7Nr6GU/8ht6HmEz10jIUbObibowEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRdvc6%2FbtsKT7Nr6GU%2F8ht6HmEz10jIUbObibowEk%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; alt=&quot;Swagger UI - URL&quot; loading=&quot;lazy&quot; width=&quot;1320&quot; height=&quot;784&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI에 입력한 경로로 들어가보면 스웨거 문서가 호스팅되고 있다는 사실을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/onGll/btsKUABDruJ/MnLXAszGHXIQKNQ4hYu8a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/onGll/btsKUABDruJ/MnLXAszGHXIQKNQ4hYu8a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/onGll/btsKUABDruJ/MnLXAszGHXIQKNQ4hYu8a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FonGll%2FbtsKUABDruJ%2FMnLXAszGHXIQKNQ4hYu8a1%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; alt=&quot;openapi3.json documents&quot; loading=&quot;lazy&quot; width=&quot;1330&quot; height=&quot;796&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI는 위와 같은 문서를 기반으로, 자체 로직을 통해 UI를 구성하여 사용자에게 아래와 같은 예쁜 화면을 보여주는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgHfHa/btsKS4j0XZg/UsrEkjr9EQKfs5ADNPYeGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgHfHa/btsKS4j0XZg/UsrEkjr9EQKfs5ADNPYeGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgHfHa/btsKS4j0XZg/UsrEkjr9EQKfs5ADNPYeGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgHfHa%2FbtsKS4j0XZg%2FUsrEkjr9EQKfs5ADNPYeGK%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; alt=&quot;Swagger UI - API&quot; loading=&quot;lazy&quot; width=&quot;1217&quot; height=&quot;599&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;599&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;또한, Swagger UI는 단지 API의 스펙만을 보여주는 것이 아니라 API를 직접 호출해볼 수 있다. API 스펙을 열어 Try it out 버튼을 클릭하고 Execute를 클릭하면 실제 Response 값을 받아볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w6QQk/btsKVqFmulh/MKog7CY2qicsW339gqH7D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w6QQk/btsKVqFmulh/MKog7CY2qicsW339gqH7D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w6QQk/btsKVqFmulh/MKog7CY2qicsW339gqH7D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw6QQk%2FbtsKVqFmulh%2FMKog7CY2qicsW339gqH7D0%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; alt=&quot;Swagger Execute&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;884&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 Security를 설정해 API-KEY 방식이나 토큰을 넣어 인증을 테스트해볼 수 있는 기능 등 다양한 기능을 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. Swagger UI를 활용하려면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, 팀에서 사용하는 인프라에 Swagger UI 서버를 배포해야 한다. 또한, 위에서 설명한 것처럼 스웨거 문서를 호스팅하여 Swagger UI가 이를 참조할 수 있도록 설정해야 한다.&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;epages의 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;restdocs-api-spec와 같은 써드파티 라이브러리를 사용하는 경우,&lt;/span&gt; 서버의 코드만으로 수정할 수 없는 부분들이 존재할 수 있다. 필요한 기능이 있다면 Swagger UI 프로젝트를 직접 내려받아 코드를 수정해 사용하는 방법도 고려해야 한다.&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;이처럼 Swagger UI를 사용하는 여정은 쉽지 않다. 하지만 지금부터 차근차근 알아가 보도록 하자.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 나만의 Swagger UI 만들기 전에&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1.  자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 Swagger UI를 사용하려면&amp;nbsp;&lt;u&gt;&lt;b&gt;1.Swagger 문서 생성하고&lt;/b&gt;&lt;/u&gt;, &lt;b&gt;2.&lt;/b&gt;&lt;u&gt;&lt;b&gt;Swagger UI가 참조할 수 있는 저장소에 그 문서를 업로드&lt;/b&gt;&lt;/u&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;하지만 1번과 2번의 과정을 CI/CD 파이프라인에 통합할 수 있다면 장기적으로 리소스를 절약할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 업데이트 시점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Swagger 문서를 업로드하는 적절한&amp;nbsp;&lt;b&gt;&quot;시점&quot;&lt;/b&gt;은 언제일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;이는 그 서버를 사용하는 대상에 따라  달라질 수 있다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 내부의 프론트엔드 개발자에게 API 스펙을 제공해야 한다면, DEV 서버를 배포하는 시점에 Swagger가 업데이트하는 것이 적합할 것이다. 반면 외부 사용자에게 API를 제공한다면 운영 환경이 배포될 때 Swagger 문서를 업데이트해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 회사나 팀의 상황에 따라 적절한 &lt;b&gt;&quot;업데이트 시점&quot;&lt;/b&gt;을 선택하는 것이 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3.  스웨거 문서 업로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger 문서를 생성하고, Swagger UI가 참조할 수 있는 문서를 업로드하는 과정을 Jenkins 파이프라인에 통합할 것이다. 기존&amp;nbsp; Dev 서버나 Production 배포 파이프라인에 이 작업을 추가하면, &lt;b&gt;서버가 업데이트됨과 동시에 Swagger UI도 자동으로 최신 상태를 유지할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger 문서를 Object Storage에 업로드하고 Swagger UI에서 Object Storage를 참조하게 할 것이다. 내부에서만 사용되는 API 문서는 공개하지 않는 것이 좋다고 생각한다. 문서가 공개되어 있으면 악의적인 사용자에 의해 서버가 공격을 당할 가능성이 높아지기 때문이다. &lt;b&gt;따라서 Object Storage를 정적 호스팅하지 않고 숨긴 상태로 둘 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고!&lt;br /&gt;이미 외부에  공개된 API라면 openapi3 파일을 공개적으로 호스팅하는 방법도 좋다고 생각한다. 구현이 훨씬 간단해지기 때문이다.&lt;/blockquote&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;사용자가 Jenkins에서 Build를 실행한다.&lt;/li&gt;
&lt;li&gt;Jenkins가 Git Repository에서 프로젝트를 가져온다.&lt;/li&gt;
&lt;li&gt;Jenkins는 프로젝트에서 openapi 명령어를 통해 스웨거 문서를 생성한다.&lt;/li&gt;
&lt;li&gt;생성한 문서를 Object Storage에 업로드한다.&lt;/li&gt;
&lt;li&gt;스웨거 UI는 업로드된 스웨거 문서를 통해 새로운 UI를 그려준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zfoY0/btsKUxyGL2n/3xYNlv0EHNsEkEtpvEMdu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zfoY0/btsKUxyGL2n/3xYNlv0EHNsEkEtpvEMdu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zfoY0/btsKUxyGL2n/3xYNlv0EHNsEkEtpvEMdu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzfoY0%2FbtsKUxyGL2n%2F3xYNlv0EHNsEkEtpvEMdu0%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; alt=&quot;전체 Flow&quot; loading=&quot;lazy&quot; width=&quot;1263&quot; height=&quot;656&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;656&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Swagger 문서 업로드 파이프라인 추가하기&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-1. 먼저 문서 자동화&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;1.Swagger 문서 만들기&lt;/b&gt;&lt;/u&gt;를 CI/CD 파이프라인에서 자동화하려면, 애플리케이션 코드 수준에서 Swagger 문서가 자동으로 생성되도록 설정해야 한다. &lt;br /&gt;아래 글에서 Spring REST Docs와 Epages의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;restdocs-api-spec를 활용하여 Swagger API 문서 자동화하는 방법을 소개하고 있다. 이를 참고하면 손쉽게 Swagger 문서 자동화를 적용해볼 수 있을 것이다.&lt;/span&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://myvelop.tistory.com/246&quot;&gt;https://myvelop.tistory.com/246&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Jenkins AWS Steps 플러그인 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins를 사용해 Swagger 문서 형식인 openapi3.yaml 혹은 openapi3.json 파일을 Object Storage에 업로드할 것이다. 그런데 파일을 업로드하려면 어떻게 해야 할까? Jenkins 의 플러그인을 활용하면 효율적으로 작업할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Jenkins의 가장 큰 장점 중 하나는 강력한 플러그인 생태계가 있다는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Steps라는 플러그인은 S3 업로드 기능을 제공하며 아래 단계를 통해 Jenkins 파이프라인에 적용해볼 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS Steps 플러그인을 설치.&lt;/li&gt;
&lt;li&gt;인프라 비밀키를 등록. NCloud를 사용하고 있다면 Username에 access_key를 넣고, Password에 secret_key를 입력해주면 된다.&lt;/li&gt;
&lt;li&gt;Jenkins 파이프라인에서 AWS Steps의 기능을 사용할 때 비밀키를 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r0FrA/btsKTSpvsrm/ImKGzi5q1mHmo8itYHPIC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r0FrA/btsKTSpvsrm/ImKGzi5q1mHmo8itYHPIC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r0FrA/btsKTSpvsrm/ImKGzi5q1mHmo8itYHPIC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr0FrA%2FbtsKTSpvsrm%2FImKGzi5q1mHmo8itYHPIC1%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; alt=&quot;AWS Key 설정&quot; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;715&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;715&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 스크립트 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기존 CI/CD 파이프라인에 &quot;Swagger Docs Upload&quot;라는 Stage를 추가할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;swagger&lt;/span&gt;&lt;/b&gt;라는 버킷의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;openapi&lt;/span&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;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;SOURCE_BRANCH&lt;/b&gt;&lt;/span&gt;와 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;PROJECT_NAME&lt;/b&gt;&lt;/span&gt;&amp;nbsp;환경변수는 파이프라인을 실행 전에 사용자 입력을 통해 받아오도록 설정하자.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;TARGET_FILE_PATH&lt;/span&gt;&lt;/b&gt;는 API 문서 자동화를 통해 생성된 스웨거 문서의 경로를 지정한다.&lt;/li&gt;
&lt;li&gt;등록했던 비밀키 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;NCLOUD_KEY&lt;/span&gt;&lt;/b&gt;를 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;withAWS&lt;/span&gt;&lt;/b&gt;라는 함수에 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;credentials&lt;/span&gt;&lt;/b&gt;로 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732350948870&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any

    environment {
        TARGET_FILE_PATH = '/build/resources/main/static/docs/'
    }
    
    stages {
        stage('Checkout') {
            steps {
                script {
                    GIT_VALUE = git branch: '${SOURCE_BRANCH}', credentialsId: 'GIT', url: '${GIT_URL}'
                    env.GIT_COMMIT = GIT_VALUE.GIT_COMMIT
                }
            }
        }
        
        stage('Swagger Upload') {
            when {
                anyOf {
                    expression { env.PROJECT_NAME == 'shop' }
                    expression { env.PROJECT_NAME == 'admin' }
                }
            }
            steps {
                sh './gradlew clean :${PROJECT_NAME}:openapi3'
                
                withAWS(region: 'kr-standard', endpointUrl: 'https://kr.object.ncloudstorage.com', credentials: 'NCLOUD_S3') {
                    s3Upload(
                        bucket: 'swagger', 
                        file: PROJECT_NAME + TARGET_FILE_PATH + PROJECT_NAME + '-openapi3.yaml',
                        path: 'openapi/')
                }
            }
        }
        
        // ... Build ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Swagger UI 커스터마이징하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 기본 컨셉&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;Swagger UI 프로젝트의 코드를 직접 수정해서 Docker Image로 빌드해, 나만의 Swagger UI를 만들 것이다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI 프로젝트를 조금만 살펴보면, docker라는 폴더에 인프라 커스터마이징을 위한 핵심 요소들이 모여 있다. 이를 통해 필요에 따라 코드를 수정하여 자신만의 환경에 맞게 활용할 수 있다. (개인적으로 굉장히 Customizable-friendly하다고 느꼈다.)&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;또한,&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;docs/customization&lt;/b&gt;&lt;/span&gt; 디렉토리에는 Swagger UI 커스터마이징에 대한 문서가 잘 정리되어 있으니, 참고하면 도움이 될 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Swagger UI 프로젝트 받아오기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Swagger UI 프로젝트를 clone 받아보자.&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/swagger-api/swagger-ui&quot;&gt;https://github.com/swagger-api/swagger-ui&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 버전을 선택하면 된다. 예시에서는 v4.11.1 Tag로 checkout했다. 이제 Swagger UI를 커스터마이징할 준비를 마쳤다.&lt;/p&gt;
&lt;pre id=&quot;code_1732348323568&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ git checkout v4.11.1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 커스터마이징 예시 - URL 추가하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 커스터마이징 예시 하나를 준비했다. Swagger UI에 여러 개의 URL을 추가하여, 유저가 드롭다운을 통해 다양한 API 문서를 접근할 수 있도록 만들 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLHNFS/btsKTmdD9h4/hq4uWQaqXKmCmsbkfiNKF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLHNFS/btsKTmdD9h4/hq4uWQaqXKmCmsbkfiNKF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLHNFS/btsKTmdD9h4/hq4uWQaqXKmCmsbkfiNKF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLHNFS%2FbtsKTmdD9h4%2Fhq4uWQaqXKmCmsbkfiNKF0%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; alt=&quot;Select a definition&quot; loading=&quot;lazy&quot; width=&quot;1047&quot; height=&quot;83&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;83&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;우리는 이 작업을 수정하기 위해 swagger-ui 프로젝트의 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;dist/swagger-initializer.js&lt;/span&gt;&lt;/b&gt; 파일을 수정할 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEzhhg/btsKSS4YA5u/zdEh1KVwHNgcR0YP7IMhk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEzhhg/btsKSS4YA5u/zdEh1KVwHNgcR0YP7IMhk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEzhhg/btsKSS4YA5u/zdEh1KVwHNgcR0YP7IMhk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEzhhg%2FbtsKSS4YA5u%2FzdEh1KVwHNgcR0YP7IMhk1%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; alt=&quot;swagger-ui project directory - swagger-initializer.js&quot; loading=&quot;lazy&quot; width=&quot;479&quot; height=&quot;310&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1732351511721&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;window.onload = function() {
  //&amp;lt;editor-fold desc=&quot;Changeable Configuration Block&quot;&amp;gt;

  // the following lines will be replaced by docker/configurator, when it runs in a docker-container
  window.ui = SwaggerUIBundle({
    url: &quot;https://petstore.swagger.io/v2/swagger.json&quot;,
    dom_id: '#swagger-ui',
    deepLinking: true,
    presets: [
      SwaggerUIBundle.presets.apis,
      SwaggerUIStandalonePreset
    ],
    plugins: [
      SwaggerUIBundle.plugins.DownloadUrl
    ],
    layout: &quot;StandaloneLayout&quot;
  });

  //&amp;lt;/editor-fold&amp;gt;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 url을 urls로 수정하고 url 객체를 담은 배열 형태로 수정하면 끝이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;window.ui = SwaggerUIBundle({
  urls: [
    {url: &quot;/openapi3/admin-openapi3.yaml&quot;, name: 'Admin'},
    {url: &quot;/openapi3/shop-openapi3.yaml&quot;, name: 'Shop'}
  ],
  //..
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 커스터마이징을 마친 Swagger UI를 Docker 이미지로 빌드해보자.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Docker 이미지 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 아래와 같은 구성을 만들 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bazNjV/btsKTmLxWsv/kP3gh9EKMNxuKvLkSVYhJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bazNjV/btsKTmLxWsv/kP3gh9EKMNxuKvLkSVYhJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bazNjV/btsKTmLxWsv/kP3gh9EKMNxuKvLkSVYhJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbazNjV%2FbtsKTmLxWsv%2FkP3gh9EKMNxuKvLkSVYhJ1%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; alt=&quot;Docker를 통한 Swagger UI 구성&quot; loading=&quot;lazy&quot; width=&quot;1041&quot; height=&quot;608&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. S3 마운트 준비하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 이미지처럼 우리는 Swagger UI 서버가 Object Storage에 접근할 수 있도록 설정할 것이다. 서버가 안정적으로 Object Storage에 접근하려면&amp;nbsp;&lt;b&gt;스토리지를 마운트&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI 프로젝트는 root 디렉토리에 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Dockerfile&lt;/span&gt;&lt;/b&gt;을 구성해 두었으며, 우리는 Swagger UI의 실행 시점에 S3 Mount를 할 수 있도록 Dockerfile에 수정할 것이다. S3 파일 시스템에 접근하기 위해 s3fs라는 라이브러리를 사용할 것이다.&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; swagger-ui/Dockerfile&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732353652427&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM nginx:1.21.6-alpine
ARG S3_ACCESS_KEY
ARG S3_SECRET_KEY

RUN apk add automake autoconf fuse-dev libtool build-base git curl-dev libxml2-dev make openssl-dev &amp;amp;&amp;amp; \
    git clone https://github.com/s3fs-fuse/s3fs-fuse.git &amp;amp;&amp;amp; \
    cd s3fs-fuse &amp;amp;&amp;amp; \
    chmod +x autogen.sh &amp;amp;&amp;amp; \
    chmod +x configure.ac &amp;amp;&amp;amp; \
    ./autogen.sh &amp;amp;&amp;amp; \
    ./configure &amp;amp;&amp;amp; \
    make  &amp;amp;&amp;amp; \
    make install &amp;amp;&amp;amp; \
    echo &quot;${S3_ACCESS_KEY}:${S3_SECRET_KEY}&quot; &amp;gt; /etc/passwd-s3fs &amp;amp;&amp;amp; \
    chmod 600 /etc/passwd-s3fs &amp;amp;&amp;amp; \
    mkdir /swagger

RUN apk update &amp;amp;&amp;amp; \
    apk add --no-cache &quot;nodejs&amp;gt;=14.17.6-r0&quot;

LABEL maintainer=&quot;fehguy&quot;
ENV API_KEY &quot;**None**&quot;
ENV SWAGGER_JSON &quot;/openapi/openapi3.yaml&quot;
ENV PORT 8080
ENV BASE_URL &quot;&quot;
ENV SWAGGER_JSON_URL &quot;&quot;

COPY --chown=nginx:nginx --chmod=0666 docker/nginx.conf ./docker/cors.conf /etc/nginx/

# copy swagger files to the `/js` folder
COPY --chmod=0666 ./dist/* /usr/share/nginx/html/
COPY --chmod=0555 ./docker/docker-entrypoint.d/ /docker-entrypoint.d/
COPY --chmod=0666 ./docker/configurator /usr/share/nginx/configurator

COPY start.sh /start.sh
RUN chmod +x /start.sh

RUN chmod 777 /usr/share/nginx/html/ /etc/nginx/ /var/cache/nginx/ /var/run/
EXPOSE 8080

ENTRYPOINT [&quot;/start.sh&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;b&gt;swagger-ui/start.sh&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732353693563&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/sh

# s3fs 명령어 실행
s3fs swagger /swagger -o allow_other -o url=https://kr.object.ncloudstorage.com -o umask=0000

# 권한 부여
chmod -R 755 /swagger/openapi

# Nginx 실행
nginx -g 'daemon off;'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;여기서 주의해야 할 점이 있다.&lt;/b&gt;&lt;/u&gt; 마운트한 폴더 자체에 권한을 부여하지 않으면 스웨거 문서가 업데이트될 때마다 파일이 권한이 리셋되어 Swagger UI 서버가 문서에 접근하려고 할 때 &lt;b&gt;403에러가 발생&lt;/b&gt;할 수 있다. 이를 방지하려면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;s3fs&lt;/b&gt;&lt;/span&gt; 명령어를 실행할 때,&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;umask&lt;/span&gt;&lt;/b&gt; 옵션을 사용하여 권한을 설정해야 한다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5-2. NGINX&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Object Storage에 저장된 문서를 가져올 수 있다. 도커를 실행하면 나만의 Swagger UI를 사용할 수 있게 되지만, 사실 작업이 하나 더 필요하다. 바로 NGINX다.&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;Swagger UI는 기본적으로 HTTP URL 경로에서 문서를 로드하도록 설계되어 있다. 따라서 파일 경로(file://)로 직접 접근하려고 하면 아래와 같이 CORS 오류를 터트려버린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8xFIK/btsKTZ9KfpB/mnYWBcyzROiJIb37IkKhmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8xFIK/btsKTZ9KfpB/mnYWBcyzROiJIb37IkKhmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8xFIK/btsKTZ9KfpB/mnYWBcyzROiJIb37IkKhmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8xFIK%2FbtsKTZ9KfpB%2FmnYWBcyzROiJIb37IkKhmK%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; alt=&quot;swagger cors error&quot; loading=&quot;lazy&quot; width=&quot;1032&quot; height=&quot;1006&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;1006&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;&lt;b&gt;이 문제를 해결하기 위해 NGINX를 사용하는 것이다. &lt;/b&gt;swagger-ui 프로젝트의 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;docker&lt;/b&gt;&lt;/span&gt; 디렉토리를 살펴보면, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;nginx.conf&lt;/b&gt;&lt;/span&gt; 가 존재한다. &lt;s&gt;마치 &quot;나 커스터마이징해주세요.&quot;라는 외치는 것 같다.&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;263&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czYm7S/btsKTW6kqXr/Kuqspb6C9ZGQnbWVLEhtTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czYm7S/btsKTW6kqXr/Kuqspb6C9ZGQnbWVLEhtTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czYm7S/btsKTW6kqXr/Kuqspb6C9ZGQnbWVLEhtTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczYm7S%2FbtsKTW6kqXr%2FKuqspb6C9ZGQnbWVLEhtTK%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; alt=&quot;swagger-ui project docker directory&quot; loading=&quot;lazy&quot; width=&quot;386&quot; height=&quot;154&quot; data-origin-width=&quot;263&quot; data-origin-height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 이 파일을 수정하여 마운트된 폴더의 &lt;b&gt;스웨거 파일을 Swagger UI 서버와 동일한 호스트에서 호스팅하도록 설정&lt;/b&gt;할 것이다. 아래와 같이 코드 단 4줄만 추가하면 된다. Swagger UI의 호스트에 접근할 때 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;/openapi&lt;/span&gt;&lt;/b&gt;라는 경로로 요청을 보내면, NGINX가 그 요청을 마운트된 폴더 경로인&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; /swagger/openapi&lt;/span&gt;&lt;/b&gt; 로 연결해줄 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1732352042660&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http {
  // 기본설정 ...

  server {
    // 기본설정 ...

    location /openapi {
        alias /swagger/openapi;  # S3 버킷이 마운트된 경로
        autoindex on;  # 디렉터리 목록 표시를 위한 설정
        include cors.conf;  # CORS 설정 포함
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. 도커 이미지 생성 및 업로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래 명령어를 통해 이미지를 생성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1732354002739&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker buildx build --platform linux/amd64 \
    --build-arg S3_ACCESS_KEY=${엑세스키} \
    --build-arg S3_SECRET_KEY=${시크릿키} \
    -t my-sever/swagger-ui:v1.0.0 .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 해당 이미지를 Docker Hub에 업로드하여 k8s의 Deployment 오브젝트에서 이미지를 사용할 수 있게 할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1732354149745&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker login
$ docker image push my-server/swagger-ui:v1.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 쿠버네티스에서 Swagger UI 관리하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 코드형 인프라 활용&lt;/h3&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;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;swagger-ui&lt;/b&gt;&lt;/span&gt;라는 디렉토리는 실제 swagger-ui 프로젝트의 커스터마이징한 부분만 옮겨 조금씩 수정하며 버저닝할 수 있게 한 것이다. (아니면 Swagger UI 프로젝트를 통째로 레포지토리에 넣어도 괜찮다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbzK8o/btsKTJswXIT/b3U55WXLpHXm9e7zUE4quk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbzK8o/btsKTJswXIT/b3U55WXLpHXm9e7zUE4quk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbzK8o/btsKTJswXIT/b3U55WXLpHXm9e7zUE4quk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbzK8o%2FbtsKTJswXIT%2Fb3U55WXLpHXm9e7zUE4quk%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; alt=&quot;k8s swagger directory&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;295&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 디렉토리에서는 커스터마이징 과정을 설명하는&amp;nbsp;&lt;b&gt;문서를 꼼꼼하게 작성&lt;/b&gt;해두는 것이 좋다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. Service 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 통해 Swagger UI가 8080 포트에서 실행되므로 , targetPort를 8080 포트로 설정하고 외부에서는 80 포트를 통해 접근할 수 있도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1732354496580&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: swagger-svc
  namespace: swagger
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 8080
  selector:
    app: swagger-ui&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6-3. Deployment 스크립트 구성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deployment 스크립트를 구성하는 것은 다소 복잡할 수 있다.&amp;nbsp;&lt;b&gt;Swagger UI가 Object Storage에 마운트할 수 있도록 설정을 추가&lt;/b&gt;해줘야 하기 때문이다. 사실 Docker 컨테이너의 파일 시스템에서는 직접 S3를 마운트해올 수 없으므로, 호스트 시스템의 힘을 빌려야 한다.&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;a href=&quot;https://myvelop.tistory.com/247#5.%20Docker%20%EC%9D%B4%EB%AF%B8%EC%A7%80%20%EB%A7%8C%EB%93%A4%EA%B8%B0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;5. Docker 이미지 만들기&lt;/a&gt;에서는 마치 도커 컨테이너에서 직접 마운트하는 것처럼 그려졌지만 실제로는 아래와 같은 구조로 S3를 마운트하게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1065&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3DGB2/btsKSQ0k0vk/hJPP5166UD4JkL1Db2NRa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3DGB2/btsKSQ0k0vk/hJPP5166UD4JkL1Db2NRa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3DGB2/btsKSQ0k0vk/hJPP5166UD4JkL1Db2NRa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3DGB2%2FbtsKSQ0k0vk%2FhJPP5166UD4JkL1Db2NRa1%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; alt=&quot;Docker bind mount&quot; loading=&quot;lazy&quot; width=&quot;1065&quot; height=&quot;524&quot; data-origin-width=&quot;1065&quot; data-origin-height=&quot;524&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;따라서 s3fs를 사용하는 Docker Image를 실행하려면 명령어를 아래와 같이 구성해줘야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;--rm&lt;/span&gt;&lt;/b&gt;: 컨테이너가 종료될 때, 연결된 파일시스템을 삭제해주는 옵션이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;--device /dev/fuse&lt;/span&gt;&lt;/b&gt;: Docker 컨테이너가 실행되는 호스트에 FUSE 기반 마운트를 수행할 수 있도록 디바이스 디렉토리를 만든다. 실제로&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #1d1c1d; text-align: left;&quot;&gt;S3를&amp;nbsp;&lt;/span&gt;마운트할 곳을 지정.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;--cap-add SYS_ADMIN&lt;/span&gt;&lt;/b&gt;: 파일 시스템 마운트를 컨테이너 내부에서 수행하려면 어드민 권한이 필요한데, 그 권한을 부여하는 옵션이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732354704348&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker run \
    --rm --device /dev/fuse \
    --cap-add SYS_ADMIN \ 
    -p 8080:8080 my-server/swagger-ui:v1.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위  설정을 Deployment에 담으면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;volumeMounts와 volumes를 설정하여 /dev/fuse에 마운트하도록 설정했다.&lt;/li&gt;
&lt;li&gt;securityContext를 통해 SYS_ADMIN 권한을 부여했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732354562659&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: swagger-ui-deploy
  namespace: swagger
  labels:
    app: swagger-ui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: swagger-ui
  template:
    metadata:
      labels:
        app: swagger-ui
    spec:
      nodeSelector:
        type: production
      imagePullSecrets:
        - name: my-docker-registry             # Docker Hub Secret 등록 필요
      containers:
        - name: swagger-ui
          image: my-server/swagger-ui:v1.0.0
          securityContext:
            privileged: true
            capabilities:
              add:
                - SYS_ADMIN
          volumeMounts:
            - mountPath: /dev/fuse
              name: fuse-device
          ports:
            - containerPort: 8080
      volumes:
        - name: fuse-device
          hostPath:
            path: /dev/fuse&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-4. Swagger UI 주소 은닉하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네트스의 Ingress 오브젝트를 사용하여&amp;nbsp;&lt;b&gt;Swagger UI 서버에 대한 접근을 은닉&lt;/b&gt;할 수 있다. 예를 들어, 팀에서 VPN을 사용한다면 해당 VPN에 접속한 유저만 Swagger UI에 접근할 수 있도록 설정할 수 있다.&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;span style=&quot;background-color: #dddddd;&quot;&gt;nginx.ingress.kubernetes.io/whitelist-source-range&lt;/span&gt;&lt;/b&gt;라는 어노테이션을 사용하면 해당 인그레스에 접근 가능한 IP를 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732354461069&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: swagger-ingress
  namespace: swagger
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: &quot;true&quot;
    nginx.ingress.kubernetes.io/proxy-body-size: 20m
    nginx.ingress.kubernetes.io/whitelist-source-range: &quot;허용할IP주소1, 허용할IP주소2&quot;
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - swagger.my-server.com
      secretName: my-tls
  rules:
    - host: swagger.my-server.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: swagger-svc
                port:
                  number: 80&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-5. 스크립트 실행하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 네임스페이스를 스웨거 자원을 담을 네임스페이스를 생성하자.&lt;/p&gt;
&lt;pre id=&quot;code_1732438547330&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl create ns swagger&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, Docker Hub에 접근하기 위한 Secret을 등록하고, TLS가 있다면 TLS Secret도 등록해준다.&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;모든 준비가 끝났다면, Deployment로 Pod를 띄우고 Service와 Ingress를 구성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1732438373462&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl apply -f swagger/swagger-ui-deploy.yaml
$ kubectl apply -f swagger/swagger-ui-service.yaml
$ kubectl apply -f swagger/swagger-ui-ingress.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 ingress로 설정한 주소(위의 예시에서는 swagger.my-server.com)에 접근하면 Swagger UI가 정상적으로 실행되는 것을 확인할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI를 효과적으로 활용하기 위해 S3 마운트 및 쿠버네티스 환경 구축 등 여러 가지 사안을 고려해야 하지만, 적절한 설정을 통해 안정적이고 확장 가능한 시스템을 구축할 수 있다. Swagger UI 프로젝트를 커스터마이징하고, S3 마운트 작업을 통해 문서를 안전하게 관리할 수 있는 환경을 구축했다. 또한, NGINX를 통해 문서를 호스팅하여 CORS 문제를 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI와 관련된 인프라가 코드형 인프라로 구축되었기 때문에 추후 시스템 변경이나 업데이트가 용이하며, 팀원들이 더 쉽게 유지보수할 수 있게 되었다.&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;이제 우리가 Swagger UI를 배포하기 위해 거쳐야 할 일은 3가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Swagger UI 코드 수정하기&lt;/li&gt;
&lt;li&gt;도커 이미지 빌드하고 Push하기&lt;/li&gt;
&lt;li&gt;쿠버네티스 Pod 새로운 버전으로 교체하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정이 번거롭게 느껴진다면! Jenkins의 파이프라인으로 사용하여 Swagger UI를 배포하는 과정 또한 자동화할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더 나은 방법을 찾기 위해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 위에서 설명한 시스템은 내 상황에 맞게 설계된 해결책이다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Object Storage를 사용하고 마운트를 활용한 접근 방식이&lt;/span&gt; 회사의 특정 제약을 해결하는 데 효과적이었지만, &lt;b&gt;모든 상황에 적합한 정답은 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자의 기술 스택과 제약 조건에 따라 Swagger UI와 문서를 어떻게 배포하고 관리할지 고민할 때, &lt;b&gt;가장 중요한 것은 자신의 상황에 맞는 최선의 선택&lt;/b&gt;을 하는 거라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 글이 다양한 환경에 맞춰 API 문서를 구축하고 배포하는 데 도움이 되는 참고 자료가 되길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <category>docker</category>
      <category>jenkins</category>
      <category>s3fs</category>
      <category>Spring</category>
      <category>swagger</category>
      <category>swagger ui</category>
      <category>스웨거</category>
      <category>스프링</category>
      <category>젠킨스</category>
      <category>쿠버네티스</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/247</guid>
      <comments>https://myvelop.tistory.com/247#entry247comment</comments>
      <pubDate>Sun, 24 Nov 2024 20:06:26 +0900</pubDate>
    </item>
    <item>
      <title>REST API 문서 자동화로 업무 효율 극대화하는 방법</title>
      <link>https://myvelop.tistory.com/246</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Spring REST Docs를 통해 Swagger 문서화를 자동화하는 방법과 이를 통해 업무 효율을 극대화할 수 있는 여러 가지 도구들에 대해 소개한다.&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;Swagger는 가장 유명한 API 문서화 툴 중 하나이다. HTTP 통신을 통해 정보를 교환하는 REST API를 사용한다면 협업을 진행하기 위해 Swagger를 사용하고 있는 팀이 많을 것이다. 테스트 코드로 자동화했을 수도 있고, 주석을 다는 방법과 기타 방법을 사용해 자동화한 팀도 존재할 것이며, openapi 문서를 한땀한땀 직접 작성하는 분들도 있을 것이다.&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;몇몇 분들은 굳이 &quot;Swagger가 굳이 필요하냐?&quot;, &quot;테스트 코드를 통해 Swagger 문서를 자동화할 필요가 있냐?&quot;고 의문을 가질 수도 있다. 하지만 내 대답은 &lt;b&gt;&quot;&lt;b&gt;테스트 코드로 Swagger 문서 자동화? &lt;/b&gt;무조건 해야 한다.&quot;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 사용하여 Swagger 문서 자동화했을 때 얻을 수 있는 이점은 많다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;REST Docs 테스트 코드에서 실수로 넣지 않은 PathVariable, QueryParameter, RequestBody Field, Response Field 등이 있다면 에러를 발생시킨다. 이는 문서의 유지보수에 도움이 된다.&lt;/li&gt;
&lt;li&gt;Swagger를 업데이트하는 파이프라인을 배포 사이클에 추가하여 문서 업데이트를 자동화할 수 있다.&lt;/li&gt;
&lt;li&gt;신속하게 업데이트되는 Swagger UI는 프론트엔드 개발자(혹은 해당 마이크로서비스를 사용하는 백엔드/인프라 개발자)들의 병목을 줄여주고, 업무 효율성 증대로 이어진다.&lt;/li&gt;
&lt;li&gt;문서 자동화를 통해 생성된 openapi3 파일을 활용해 API와 관련된 객체 생성을 자동화할 수 있고, 이를 통해 업무 생산성을 확보할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 API 문서 자동화를 활용하는 방법에 대해 하나씩 알아가보자.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Swagger 살펴보기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring의 REST Docs와는 다르게 예쁜 UI를 가진 것이 특징이다. 요청과 응답 등의 스키마 등이 잘 구성되어 있고, API를 직접 호출해보는 것도 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lNvUN/btsKvGwq2T4/OPp0vfS8BX3nXi7Q2YJUKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lNvUN/btsKvGwq2T4/OPp0vfS8BX3nXi7Q2YJUKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lNvUN/btsKvGwq2T4/OPp0vfS8BX3nXi7Q2YJUKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlNvUN%2FbtsKvGwq2T4%2FOPp0vfS8BX3nXi7Q2YJUKK%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; alt=&quot;Swagger UI&quot; loading=&quot;lazy&quot; width=&quot;1217&quot; height=&quot;599&quot; data-origin-width=&quot;1217&quot; data-origin-height=&quot;599&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;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 어노테이션을 통한 문서화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger를 사용해서 API를 문서화하면 아래와 같이 컨트롤러 코드에 Swagger 관련 어노테이션과 코드가 작성되어야 한다. 컨트롤러 단의 코드가 어노테이션으로 뒤덮이기 때문에 코드의 가독성이 떨어진다는 문제점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691497715828&quot; class=&quot;scala&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/v1/categories&quot;)
@RequiredArgsConstructor
public class CategoryController {

	private final CategoryService categoryService;
  
    @Operation(summary = &quot;find category&quot;, description = &quot;카테고리 리스트 조회 API&quot;)
    @ApiResponses({@ApiResponse(responseCode = &quot;200&quot;, content = {
      @Content(schema = @Schema(implementation = FindCategoryResponseSwagger.class))}),
      @ApiResponse(responseCode = &quot;400&quot;, description = ExceptionMessage.INVALID_PAGE_REQUEST, content = {
          @Content(schema = @Schema(implementation = InvalidPageRequestExceptionSwagger.class))}),
      @ApiResponse(responseCode = &quot;403&quot;, description = ExceptionMessage.FORBIDDEN, content = {
          @Content(schema = @Schema(implementation = AccessForbiddenSwagger.class))})})
    @PageableAsQueryParam
    @GetMapping
    public ResponseEntity&amp;lt;ResultDTO&amp;lt;PageResponse&amp;lt;FindCategoryResponse&amp;gt;&amp;gt;&amp;gt; findCategories(
          @Valid @ParameterObject @ModelAttribute FindCategoryRequest request,
          @Parameter(hidden = true) Pageable pageable) {
        Page&amp;lt;FindCategoryResponse&amp;gt; categoryPage = categoryService.findCategories(request.toService(),
                pageable)
            .map(FindCategoryServiceResponse::toResponse);
        PageResponse&amp;lt;FindCategoryResponse&amp;gt; response = new PageResponse&amp;lt;&amp;gt;(categoryPage);
        return ResponseEntity.ok(new ResultDTO&amp;lt;&amp;gt;(ResponseStatus.OK, &quot;&quot;, response));
    }
 	
    ...
}&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;또한, 실제 API에서 사용하는 Request와 Response 객체에 Swagger 의존성을 침투시키지 않기 위해서는 아래와 같이 Swagger 전용 파일을 분리해야 하며, 이는 코드량이 증가하는 원인이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691498525973&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public class FindCategoryResponseSwagger {

  @Schema(description = &quot;Result Code&quot;, example = ResponseStatus.OK)
  private String status;

  @Schema(description = &quot;Message&quot;, example = ResponseMessage.FIND_CATEGORY)
  private String message;

  private PageResponse&amp;lt;FindCategoryResponse&amp;gt; data; 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. Swagger의 대척점: Spring REST Docs&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://myvelop.tistory.com/214&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring REST Docs 설정하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spring REST Docs는 테스트 코드를 문서 생성을 자동화하는 기능을 제공한다.&lt;/li&gt;
&lt;li&gt;따라서 Swagger와 다르게 기능 코드에 침투적이지 않고, 테스트를 통해 문서 생성을 자동화할 수 있다는 장점이 있다.&lt;/li&gt;
&lt;li&gt;또한 문서화해야 하는 필드를 놓쳤을 때, 에러를 발생시키기 때문에 문서 유지보수에도 유리하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730726150896&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void testFindUser() throws Exception {
    // given
    UserResponse response = UserResponse.builder().id(1).username(&quot;bell&quot;).age(26).build();
    given(userService.findUser(anyLong())).willReturn(response);

    // when
    ResultActions actions = mvc.perform(MockMvcRequestBuilders.get(&quot;/v1/users/1&quot;)
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON));    

    // then

    actions.andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath(&quot;$.data&quot;, equalTo(asParsedJson(response))))
        .andDo(MockMvcRestDocumentation.document(&quot;user/findUser&quot;, responseFields(
            fieldWithPath(&quot;data&quot;).type(JsonFieldType.OBJECT).description(&quot;data&quot;),
            fieldWithPath(&quot;message&quot;).type(JsonFieldType.STRING).description(&quot;message&quot;),
            fieldWithPath(&quot;data.id&quot;).type(JsonFieldType.NUMBER).description(&quot;The user's primary key&quot;),
            fieldWithPath(&quot;data.username&quot;).type(JsonFieldType.STRING).description(&quot;The user's name&quot;),
            fieldWithPath(&quot;data.age&quot;).type(JsonFieldType.NUMBER).description(&quot;The user's age&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 REST Docs가 제공하는 UI는 예쁜 UI와는 굉장히 거리가 멀다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brVQeV/btsKwq0Hlbn/v1m0rK1ciXdg4malvIbnf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brVQeV/btsKwq0Hlbn/v1m0rK1ciXdg4malvIbnf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brVQeV/btsKwq0Hlbn/v1m0rK1ciXdg4malvIbnf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrVQeV%2FbtsKwq0Hlbn%2Fv1m0rK1ciXdg4malvIbnf0%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; alt=&quot;REST Docs UI&quot; loading=&quot;lazy&quot; width=&quot;995&quot; height=&quot;930&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;930&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;h3 data-ke-size=&quot;size23&quot;&gt;1-3. 이 둘의 장점을 함께 사용할 순 없을까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇다면 Swagger의 뛰어난 UI와 Spring REST Docs의 편리한 자동화 기능을 같이 사용할 순 없을까?&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;com.epages.restdocs-api-spec&lt;/b&gt;&lt;/a&gt;라는 써드파티 플러그인을 사용하면 된다.&lt;/li&gt;
&lt;li&gt;epages라는 독일 기업에서 제공하는 오픈소스이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. REST Docs + Swagger 구성하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Gradle 설정 - 의존성 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 epages의 restdocs-api-spec 플러그인 설정해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730726332830&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id(&quot;com.epages.restdocs-api-spec&quot;) version &quot;0.18.2&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730726696731&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    testImplementation(&quot;com.epages:restdocs-api-spec-mockmvc:0.18.2&quot;) // epages
    testImplementation(&quot;org.springframework.restdocs:spring-restdocs-mockmvc&quot;) // restdocs
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 openapi3 문서를 생성하는 설정을 해준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;setServers&lt;/span&gt; 메소드를 통해 여러 개의 서버 주소를 세팅할 수 있다.&lt;/li&gt;
&lt;li&gt;실제로 생성하게 될 파일 이름은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;outputFileNamePrefix&lt;/span&gt;를 통해 정할 수 있으며, file의 형태는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;format&lt;/span&gt;을 통해 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730726793151&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import io.swagger.v3.oas.models.servers.Server

openapi3 {
    val stagServer: Closure&amp;lt;Server&amp;gt; = closureOf&amp;lt;Server&amp;gt; {
        this.url = &quot;https://my-server.com&quot;
    } as Closure&amp;lt;Server&amp;gt;
    val localServer: Closure&amp;lt;Server&amp;gt; = closureOf&amp;lt;Server&amp;gt; {
        this.url = &quot;http://localhost:8080&quot;
    } as Closure&amp;lt;Server&amp;gt;
    setServers(listOf(stagServer, localServer))
    title = &quot;Spring Rest Docs + Swagger-UI + Open-API 3&quot;
    description = &quot;Swagger&quot;
    version = &quot;0.0.1&quot;
    format = &quot;yaml&quot;
    outputFileNamePrefix = &quot;my-server-openapi3&quot;
    outputDirectory = &quot;${layout.buildDirectory.get()}/resources/main/static/docs&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 기반 코드 작성하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시 코드에서는 User라는 자원에 요청을 보내기 위해 UserController를 구성하고 API를 주고 받을 것이다.&lt;/li&gt;
&lt;li&gt;먼저 API 스펙이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730898592750&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
public class ApiResponse&amp;lt;T&amp;gt; {

  private final String code;
  private final String message;
  private final T data;

  private ApiResponse(String code, String message, T data) {
    this.code = code;
    this.message = message;
    this.data = data;
  }

  public static ApiResponse&amp;lt;EmptyResponse&amp;gt; NO_CONTENT() {
    return new ApiResponse&amp;lt;&amp;gt;(&quot;&quot;, &quot;&quot;, new EmptyResponse());
  }

  public static &amp;lt;T&amp;gt; ApiResponse&amp;lt;T&amp;gt; OK(T data) {
    return new ApiResponse&amp;lt;&amp;gt;(&quot;&quot;, &quot;OK&quot;, data);
  }
}&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;User의 RequestBody와 Response 객체를 정의하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730898619613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record UserRequest(String name, String email) {}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1730898630508&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record UserResponse(Long userId, String name, String email) {}&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;UserController는 아래와 같이 구성되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730898704100&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class UserController {

  private final UserService userService;

  @PostMapping(&quot;/api/v1/users&quot;)
  public ApiResponse&amp;lt;UserResponse&amp;gt; registerUser(@RequestBody UserRequest request) {
    UserResponse response = userService.registerUser(request);
    return ApiResponse.OK(response);
  }

  @GetMapping(&quot;/api/v1/users/{userId}&quot;)
  public ApiResponse&amp;lt;UserResponse&amp;gt; findUser(@PathVariable long userId) {
    UserResponse response = userService.findUser(userId);
    return ApiResponse.OK(response);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. REST Docs 코드 작성하기 - GET API&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서를 작성할 때는 com.epages.restdocs.apispec 의존성의 객체들을 사용해야 한다.&lt;/li&gt;
&lt;li&gt;아래는 코드 예시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730728094926&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@WebMvcTest
@AutoConfigureRestDocs
class UserControllerTest {

  @Autowired MockMvc mockMvc;
  @Autowired ObjectMapper objectMapper;

  @MockBean UserService userService;

  @Test
  @DisplayName(&quot;회원 조회&quot;)
  void findUser() throws Exception {
    // given
    long userId = 111L;
    UserResponse response = new UserResponse(userId, &quot;홍길동&quot;, &quot;xxxxx@xxxxx.com&quot;);

    BDDMockito.given(userService.findUser(anyLong())).willReturn(response);

    // when
    ResultActions resultActions =
        mockMvc.perform(
            RestDocumentationRequestBuilders.get(&quot;/api/v1/users/{userId}&quot;, userId)
                .contentType(MediaType.APPLICATION_JSON));

    // then
    resultActions
        .andExpect(status().isOk())
        .andExpect(jsonPath(&quot;$.message&quot;).value(&quot;OK&quot;))
        .andExpect(jsonPath(&quot;$.data&quot;, equalTo(asParsedJson(response))))
        .andDo(
            MockMvcRestDocumentationWrapper.document(
                &quot;{class_name}/{method_name}&quot;,
                preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint()),
                ResourceDocumentation.resource(
                    ResourceSnippetParameters.builder()
                        .tag(&quot;Users&quot;)
                        .description(&quot;회원 조회&quot;)
                        .pathParameters(
                            ResourceDocumentation.parameterWithName(&quot;userId&quot;).description(&quot;회원 ID&quot;))
                        .responseFields(
                            fieldWithPath(&quot;code&quot;).type(JsonFieldType.STRING).description(&quot;응답 코드&quot;),
                            fieldWithPath(&quot;message&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;응답 메시지&quot;),
                            fieldWithPath(&quot;data.userId&quot;)
                                .type(JsonFieldType.NUMBER)
                                .description(&quot;회원 ID&quot;),
                            fieldWithPath(&quot;data.name&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;회원 이름&quot;)
                            fieldWithPath(&quot;data.email&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;회원 이메일&quot;))
                        .responseSchema(Schema.schema(&quot;UserResponse&quot;))
                        .build())));
  }
  
  private Object asParsedJson(Object obj) throws JsonProcessingException {
    String json = objectMapper.writeValueAsString(obj);
    return JsonPath.read(json, &quot;$&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;span style=&quot;background-color: #dddddd;&quot;&gt;@WebMvcTest&lt;/span&gt;는 @Autowired, @MockBean 등을 사용하여 컨트롤러 테스트에 필요한 환경을 설정하도록 해주고, MVC 테스트를 위해 필요한 객체인 mockMvc를 주입해준다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@AutoConfigureRestDocs&lt;/span&gt; 어노테이션을 통해 REST Docs를 자동 설정해줄 수 있다.&lt;/li&gt;
&lt;li&gt;mockMvc에서 요청을 구성할 때 &lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;org.springframework.&lt;/span&gt;restdocs.mockmvc의 &lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;RestDocumentationRequestBuilders&lt;/span&gt;를 사용하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;&lt;br /&gt;import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;&lt;br /&gt;import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;&lt;br /&gt;import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;&lt;/blockquote&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;u&gt;Epages에서 제공해주는 객체를 사용해야 한다.&lt;/u&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;import com.epages.restdocs.apispec.ResourceDocumentation;&lt;br /&gt;import com.epages.restdocs.apispec.Schema;&lt;br /&gt;import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;&lt;br /&gt;import com.epages.restdocs.apispec.ResourceSnippetParameters;&lt;/blockquote&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;span style=&quot;background-color: #dddddd;&quot;&gt;ResourceSnippetParameters&lt;/span&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;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;tag&lt;/span&gt;&lt;/b&gt; 메소드로 해당 자원의 이름을 명시할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;pathParamters&lt;/span&gt;&lt;/b&gt;를 통해 path parameter를 표현할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;responseField&lt;/span&gt;&lt;/b&gt; 메소드는 response 스키마의 필드값을 설명하는 주석이 된다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;responseSchema&lt;/b&gt;&lt;/span&gt; 메소드를 통해 response 스키마의 이름을 직접 작성할 수 있다. (이 기능을 사용하지 않으면 객체 이름에 이상한 해시 값 같은 것이 들어가게 된다. 이 기능은 뒤에서 소개할 openapi generator에서 유용하게 사용된다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5ETFN/btsKAf43WIV/r6SYxcrbKfHYiKkOcMfC1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5ETFN/btsKAf43WIV/r6SYxcrbKfHYiKkOcMfC1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5ETFN/btsKAf43WIV/r6SYxcrbKfHYiKkOcMfC1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5ETFN%2FbtsKAf43WIV%2Fr6SYxcrbKfHYiKkOcMfC1K%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; alt=&quot;UserResponse Schema&quot; loading=&quot;lazy&quot; width=&quot;942&quot; height=&quot;564&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;564&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 테스트 코드를 통해 생성된 openapi3 파일을 Swagger UI에 전달하면 아래와 같은 형태를 보이게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFIws2/btsKzxFfvh0/Wa9Kv45dZUbmnQm4G8myrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFIws2/btsKzxFfvh0/Wa9Kv45dZUbmnQm4G8myrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFIws2/btsKzxFfvh0/Wa9Kv45dZUbmnQm4G8myrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFIws2%2FbtsKzxFfvh0%2FWa9Kv45dZUbmnQm4G8myrk%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; alt=&quot;REST Docs로 만들어진 Swagger UI - GET&quot; loading=&quot;lazy&quot; width=&quot;1418&quot; height=&quot;869&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;869&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;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2-4. 코드 작성하기 - POST API&lt;/h3&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;/div&gt;
&lt;pre id=&quot;code_1730729401803&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@WebMvcTest
@AutoConfigureRestDocs
class UserControllerTest {

  @Autowired MockMvc mockMvc;
  @Autowired ObjectMapper objectMapper;

  @MockBean UserService userService;
  
  @Test
  @DisplayName(&quot;유저 등록&quot;)
  void registerUser() throws Exception {
    // given
    UserRequest request = new UserRequest(&quot;홍길동&quot;, &quot;xxxx@xxxxx.com&quot;);
    UserResponse response = new UserResponse(1L, &quot;홍길동&quot;, &quot;xxxx@xxxxx.com&quot;);

    given(userService.registerUser(any())).willReturn(response);

    // when
    ResultActions resultActions =
        mockMvc.perform(
            post(&quot;/api/v1/users&quot;)
                .content(objectMapper.writeValueAsString(request))
                .contentType(MediaType.APPLICATION_JSON));

    // then
    resultActions
        .andExpect(status().isOk())
        .andExpect(jsonPath(&quot;$.data&quot;, equalTo(asParsedJson(response))))
        .andDo(
            MockMvcRestDocumentationWrapper.document(
                &quot;{class_name}/{method_name}&quot;,
                preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint()),
                resource(
                    ResourceSnippetParameters.builder()
                        .tag(&quot;Users&quot;)
                        .description(&quot;유저 등록&quot;)
                        .requestFields(
                            fieldWithPath(&quot;name&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;회원 이름&quot;),
                            fieldWithPath(&quot;email&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;이메일&quot;))
                        .responseFields(
                            fieldWithPath(&quot;code&quot;).type(JsonFieldType.STRING).description(&quot;응답 코드&quot;),
                            fieldWithPath(&quot;message&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;응답 메시지&quot;),
                            fieldWithPath(&quot;data.userId&quot;)
                                .type(JsonFieldType.NUMBER)
                                .description(&quot;회원 ID&quot;),
                            fieldWithPath(&quot;data.name&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;회원 이름&quot;),
                            fieldWithPath(&quot;data.email&quot;)
                                .type(JsonFieldType.STRING)
                                .description(&quot;회원 이메일&quot;))
                        .requestSchema(schema(&quot;UserRequest&quot;))
                        .responseSchema(schema(&quot;UserResponse&quot;))
                        .build())));
  }
}&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;GET과는 다르게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;RequestField&lt;/b&gt;&lt;/span&gt;가 추가되었다.&lt;/li&gt;
&lt;li&gt;Request Field를 통해 request 스키마를 구성하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;requestSchema&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드를 통해 스키마의 이름을 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cody5j/btsKAhBNPUx/kUa9RaTk4VebpGURPmFoH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cody5j/btsKAhBNPUx/kUa9RaTk4VebpGURPmFoH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cody5j/btsKAhBNPUx/kUa9RaTk4VebpGURPmFoH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcody5j%2FbtsKAhBNPUx%2FkUa9RaTk4VebpGURPmFoH1%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; alt=&quot;UserRequest 스키마&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;286&quot; data-origin-width=&quot;568&quot; data-origin-height=&quot;286&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;위 테스트 코드를 통해 구성된 openapi3를 Swagger UI에 제공하면 아래와 같은 UI를 보여준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET 예시와는 다르게 Request Body가 추가된 것을 확인할 수 있따.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X7JU6/btsKzVMwNOe/nkjZHKQSsNAkqgsAKqupu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X7JU6/btsKzVMwNOe/nkjZHKQSsNAkqgsAKqupu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X7JU6/btsKzVMwNOe/nkjZHKQSsNAkqgsAKqupu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX7JU6%2FbtsKzVMwNOe%2FnkjZHKQSsNAkqgsAKqupu0%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; alt=&quot;REST Docs로 만들어진 Swagger UI - POST&quot; loading=&quot;lazy&quot; width=&quot;1281&quot; height=&quot;900&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;900&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;h3 data-ke-size=&quot;size23&quot;&gt;2-5. openapi3 실행해보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gradle의 openapi3 명령어를 실행해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8LLcD/btsKwDlbwkX/VfhMF5UylslnBmBZK8P5ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8LLcD/btsKwDlbwkX/VfhMF5UylslnBmBZK8P5ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8LLcD/btsKwDlbwkX/VfhMF5UylslnBmBZK8P5ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8LLcD%2FbtsKwDlbwkX%2FVfhMF5UylslnBmBZK8P5ik%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; alt=&quot;gradle 명령어 - openapi3&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;255&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;163&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;테스트가 먼저 실행되고 실행된 테스트를 바탕으로 openapi3 문서를 만든다. 명령어 실행이 완료되면 내가 outputDirectory로 지정해둔 path에 my-sever-openapi3.yaml이 만들어져 있는 것을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;243&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ownEW/btsKw52F5CS/IikKBVnBwVzjxewiKhGEIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ownEW/btsKw52F5CS/IikKBVnBwVzjxewiKhGEIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ownEW/btsKw52F5CS/IikKBVnBwVzjxewiKhGEIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FownEW%2FbtsKw52F5CS%2FIikKBVnBwVzjxewiKhGEIK%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; alt=&quot;build 디렉토리에 openapi3 파일이 구성된다.&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;374&quot; data-origin-width=&quot;343&quot; data-origin-height=&quot;243&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;h3 data-ke-size=&quot;size23&quot;&gt;2-6. 이렇게 만들어진 openapi3 파일은 어떻게 사용해야 할까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같은 과정을 거치면 openapi3 파일은 만들어지나, Swagger UI를 따로 생성해주진 않기 때문에 따로 띄워진 Swagger UI 서버에서 해당 파일을 참조하도록 만들어야 한다.&lt;/li&gt;
&lt;li&gt;여기에 대한 내용은 추후 2편을 통해 자세히 다뤄볼 예정이다. (&lt;b&gt;Swagger UI 커스터마이징을 위한&amp;nbsp;docker 활용과 k8s에서 서비스하는 방법&lt;/b&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Swagger UI를 백엔드 서버에 붙이고 싶다면?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 Swagger 서버를 따로 구축하지 않았다면 어떻게 해야 할까?&lt;/li&gt;
&lt;li&gt;백엔드 서버에서 Swagger UI를 생성해서 호스팅하는 방법이 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아래의 예시는 2번의 openapi3 문서 생성기가 설정되어 있는 것을 가정한다. 2번의 과정을 먼저 진행하자.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. SwaggerUI Generator 스크립트 작성하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;org.hidetake.swagger.generator 라는 플러그인을 추가해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730814776710&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id(&quot;com.epages.restdocs-api-spec&quot;) version &quot;0.18.2&quot;
    id(&quot;org.hidetake.swagger.generator&quot;) version &quot;2.18.2&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그리고 해당 플러그인에서 제공하는 swaggerUI 메소드에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;org.webjars:swagger-ui&lt;/b&gt;&lt;/span&gt; 의존성을 추가해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730813841474&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;dependencies {
    testImplementation(&quot;com.epages:restdocs-api-spec-mockmvc:0.18.2&quot;) // epages
    testImplementation(&quot;org.springframework.restdocs:spring-restdocs-mockmvc&quot;) // restdocs
    swaggerUI(&quot;org.webjars:swagger-ui:4.11.1&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;그리고 swagger UI를 생성하기 위해&amp;nbsp;Gradle 설정에서 GenerateSwaggerUI라는 객체를 사용할 것이다.&lt;/li&gt;
&lt;li&gt;Swagger UI에서 사용할 openapi3 파일을 만들어야 하기 때문에 dependsOn을 걸어주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730816677077&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
import io.swagger.v3.oas.models.servers.Server

openapi3 {
    val stagServer: Closure&amp;lt;Server&amp;gt; = closureOf&amp;lt;Server&amp;gt; {
        this.url = &quot;https://my-server.com&quot;
    } as Closure&amp;lt;Server&amp;gt;
    val localServer: Closure&amp;lt;Server&amp;gt; = closureOf&amp;lt;Server&amp;gt; {
        this.url = &quot;http://localhost:8080&quot;
    } as Closure&amp;lt;Server&amp;gt;
    setServers(listOf(stagServer, localServer))
    title = &quot;Spring Rest Docs + Swagger-UI + Open-API 3&quot;
    description = &quot;Swagger&quot;
    version = &quot;0.0.1&quot;
    format = &quot;yaml&quot;
    outputFileNamePrefix = &quot;my-server-openapi3&quot;
    outputDirectory = &quot;${layout.buildDirectory.get()}/resources/main/static/docs&quot;
}

tasks.withType&amp;lt;GenerateSwaggerUI&amp;gt; {
    dependsOn(&quot;openapi3&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 스프링 설정하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.yaml에서 스웨거 설정을 추가해준다.&lt;/li&gt;
&lt;li&gt;Spring은 resources/static 경로의 정적 파일을 그대로 호스팅하는 특징이 있다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;springdoc.swagger-ui.url&lt;/b&gt;&lt;/span&gt;은 스웨거 UI 서버에서 스프링에서 호스팅하는 docs/openapi3.yaml을 사용하게 설정한 것이다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;springdoc.swagger-ui.path&lt;/b&gt;&lt;/span&gt;는 해당 path로 접근했을 때 자동으로 swagger UI 주소로 매핑해주는 역할을 한다. 아래와 같이 설정할 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;http://localhost:8080/docs/swagger&lt;/b&gt;&lt;/span&gt;로 접근하면 자동으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;/docs/swagger-ui/index.html&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일로 리다이렉트될 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730815157552&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;springdoc:
  default-consumes-media-type: application/json;charset=UTF-8
  default-produces-media-type: application/json;charset=UTF-8
  swagger-ui:
    url: /docs/my-server-openapi3.yaml
    path: /docs/swagger&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730815157552&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {

  // ...
    
  @Bean
  public WebSecurityCustomizer webSecurityCustomizer() {
    return web -&amp;gt;
        web.ignoring()
            .requestMatchers(&quot;/images/**&quot;, &quot;/js/**&quot;, &quot;/webjars/**&quot;)
            .requestMatchers(
                // -- Swagger UI v3 (OpenAPI)
                &quot;/v3/api-docs/**&quot;,
                &quot;/swagger-ui/**&quot;,
                &quot;/docs/my-server-openapi3.yaml&quot;,
                &quot;/docs/swagger&quot;,
                &quot;/docs/swagger-ui/**&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 로컬 환경에서 사용하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래에 작성한 build만 해도 바로 Swagger UI를 사용할 수 있게 자동화한 스크립트이다.&lt;/li&gt;
&lt;li&gt;generagteSwaggerUI에서 openapi3 파일을 먼저 생성하고 createSwaggerDirectory task를 통해 openapi3 파일을 담을 디렉토리를 만들어줬다.&lt;/li&gt;
&lt;li&gt;delete를 통해 기존의 swagger 파일을 삭제하고, build 디렉토리의 resources/main/static/docs로부 openapi3 파일을 복사하여 프로젝트의 src/mainresources/static/docs 폴더로 넣어준다. (로컬에서 Swagger UI를 확인하려면 메인 소스로 파일을 가져와야 한다.)&lt;/li&gt;
&lt;li&gt;이 과정을 build에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;dependsOn(&quot;generateSwaggerUI&quot;)&lt;/b&gt;&lt;/span&gt;으로 걸어줬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730813979345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tasks.withType&amp;lt;GenerateSwaggerUI&amp;gt; {
    dependsOn(&quot;openapi3&quot;)
    dependsOn(&quot;createSwaggerDirectory&quot;)

    delete(file(&quot;src/main/resources/static/docs/my-server-openapi3.yaml&quot;))
    copy {
        from(&quot;${layout.buildDirectory.get()}/resources/main/static/docs&quot;)
        into(&quot;src/main/resources/static/docs/&quot;)
    }
}

tasks {
    // ...
    build {
        dependsOn(&quot;generateSwaggerUI&quot;)
    }
    
    register(&quot;createSwaggerDirectory&quot;) {
        doLast {
            val directory = file(&quot;${layout.buildDirectory.get()}/resources/main/static/docs&quot;)
            if (!directory.exists()) {
                directory.mkdirs()
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. BootJar로 배포를 해야 한다면?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 BootJar 명령어를 통해 Jar 파일을 생성하여 서버를 운영하게 된다.&lt;/li&gt;
&lt;li&gt;로컬에서 사용할 때와는 다르게 openapi3 파일을 굳이 가져올 필요가 없기 때문에 generateSwaggerUI만 실행해주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730897531658&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tasks.withType&amp;lt;GenerateSwaggerUI&amp;gt; {
    dependsOn(&quot;openapi3&quot;)
}

tasks {
    // ...
    bootJar {
        dependsOn(&quot;generateSwaggerUI&quot;)
    }
    
    register(&quot;createSwaggerDirectory&quot;) {
        doLast {
            val directory = file(&quot;${layout.buildDirectory.get()}/resources/main/static/docs&quot;)
            if (!directory.exists()) {
                directory.mkdirs()
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5. 인증 간단하게 추가해보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger의 yaml이나 json 작성 문법에 따라 문자열을 작성하여 openapi3 파일에 추가해줄 수 있다.&lt;/li&gt;
&lt;li&gt;아래와 같이 간단한 API Key 인증 절차를 추가해줄 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730812990266&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tasks.withType&amp;lt;GenerateSwaggerUI&amp;gt; {
    dependsOn(&quot;openapi3&quot;)

    doFirst {
        val swaggerUIFile = file(&quot;${layout.buildDirectory.get()}/resources/main/static/docs/openapi3.yaml&quot;)

        val securitySchemesContent =
            &quot;  securitySchemes:\n&quot; +
                    &quot;    partnerCode:\n&quot; +
                    &quot;      type: apiKey\n&quot; +
                    &quot;      name: apiKey\n&quot; +
                    &quot;      in: header\n&quot; +
                    &quot;    APISecret:\n&quot; +
                    &quot;      type: apiKey\n&quot; +
                    &quot;      name: apiSecret\n&quot; +
                    &quot;      in: header\n&quot; +
                    &quot;security:\n&quot; +
                    &quot;  - partnerCode: []\n&quot; +
                    &quot;  - APISecret: []\n&quot;


        swaggerUIFile.appendText(securitySchemesContent)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 같이 사용하면 좋은 툴!&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger 문서 자동화를 통해 얻는 것은 Swagger UI만이 아니다.&lt;/li&gt;
&lt;li&gt;해당 API를 사용하는 클라이언트들의 업무 프로세스 개선에 사용해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. OpenAPI Generator 사용해서 TypeScript 코드 생성하기&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://github.com/OpenAPITools/openapi-generator&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/OpenAPITools/openapi-generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;npm 의존성을 설치해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730900250566&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ npm install @openapitools/openapi-generator-cli -g
$ openapi-generator-cli version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 명령어를 통해 백엔드에서 만들어진 openapi3.yaml 파일을 사용하면 프론트엔드 코드가 자동으로 생성되는 마법같은 일이 일어난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730900360164&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ openapi-generator-cli generate -g typescript -i ./docs/openapi3.yaml&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;UserRequest가 TypeScript로 자동 변환되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/75stm/btsKyGXwLQ3/wszYVbEJukYM0gW6rPGIjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/75stm/btsKyGXwLQ3/wszYVbEJukYM0gW6rPGIjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/75stm/btsKyGXwLQ3/wszYVbEJukYM0gW6rPGIjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F75stm%2FbtsKyGXwLQ3%2FwszYVbEJukYM0gW6rPGIjK%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; alt=&quot;openapi-generator&quot; loading=&quot;lazy&quot; width=&quot;952&quot; height=&quot;735&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;735&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;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Swagger Codegen 사용해서 Java 객체 생성 자동화하기&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://github.com/swagger-api/swagger-codegen&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/swagger-api/swagger-codegen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;아쉽게도 JSON만 지원되는 것으로 보인다. YAML로 실행하려고 하면 에러가 발생한다. Gradle 스크립트에서 문서를 생성할 때 YAML이 아닌 JSON으로 생성되게 수정해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730902236207&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;openapi3 {
    // ...
    format = &quot;json&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;Swagger Codegen을 설치하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730901283155&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ git clone https://github.com/swagger-api/swagger-codegen
$ cd swagger-codegen
$ ./mvnw clean package&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;i&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 옵션: 사용할 openapi3 파일&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;l&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 옵션: 프로그래밍 언어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;o&amp;nbsp;&lt;/span&gt;&lt;/b&gt; 옵션: 객체가 생성될 디렉토리를 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1730901324935&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ java -jar modules/swagger-codegen-cli/target/swagger-codegen-cli.jar generate \
   -i {openapi3 파일의 경로} \
   -l java \
   -o ./output&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;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;685&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daA7rG/btsKyGwwRux/xJANvhpfkwD5Hh5My68011/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daA7rG/btsKyGwwRux/xJANvhpfkwD5Hh5My68011/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daA7rG/btsKyGwwRux/xJANvhpfkwD5Hh5My68011/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaA7rG%2FbtsKyGwwRux%2FxJANvhpfkwD5Hh5My68011%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; alt=&quot;Java 코드 자동 생성&quot; loading=&quot;lazy&quot; width=&quot;439&quot; height=&quot;685&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;685&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에 처음 입사했을 때 Swagger가 구축되어 있지 않아서 프론트엔드 개발자와 협업을 진행할 때면 API 문서를 일일이 작성하여 건네주거나, 직접 프론트엔드 코드에 API 스펙에 맞는 객체를 생성하고 API를 정의해줘야 했다.&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;API가 완성되어 배포가 된 상태임에도 문서 자동화가 없었기 때문에 프론트엔드 팀에 업데이트가 즉각적으로 이뤄지지 않아 업무에 병목이 생긴 적도 있었다.&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;결국 문제를 개선하기 위해 문서 자동화를 시작했다. 처음 Swagger 문서 자동화를 시작할 때 아래와 같은 질문을 받은 적도 있었다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&quot;괜히 Swagger 문서 자동화 때문에 개발 속도 느려지는 거 아니에요?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문서 자동화를 도입한 이후, 오히려 작업 속도가 빨라졌다고 자부할 수 있다. 백엔드 개발자들은 API 스펙을 더 쉽게 더 빨리 공유할 수 있게 되었고, API를 사용하는 클라이언트들은 API 스펙을 알아내기 위해 닦달할 필요가 사라졌다. 이제는 다른 팀에서도 Swagger 문서 자동화에 관심을 가지고 시작했고, 나에게 자동화 세팅 방법을 물어본다.&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;REST API를 사용하고 있지만 여러 가지 이유 때문에 문서 자동화를 적용하지 않은 팀도 있을 것이다. 만약, 자동화되지 않은 API 문서 때문에 병목이 일어나고 있다는 생각이 든다면! Swagger 문서 자동화를 우선적으로 고려해봤으면 좋겠다!&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>api 문서 자동화</category>
      <category>OpenAPI</category>
      <category>Spring</category>
      <category>spring docs</category>
      <category>swagger</category>
      <category>자동화</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/246</guid>
      <comments>https://myvelop.tistory.com/246#entry246comment</comments>
      <pubDate>Wed, 6 Nov 2024 23:42:23 +0900</pubDate>
    </item>
    <item>
      <title>SonarQube로 코드 품질 관리하기 (feat. gitlab, jenkins, docker)</title>
      <link>https://myvelop.tistory.com/239</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjfDte/btsKblfAFYM/T5tH2G8mkc2kymK4cgnm6k/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjfDte/btsKblfAFYM/T5tH2G8mkc2kymK4cgnm6k/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjfDte/btsKblfAFYM/T5tH2G8mkc2kymK4cgnm6k/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjfDte%2FbtsKblfAFYM%2FT5tH2G8mkc2kymK4cgnm6k%2Fimg.webp&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; alt=&quot;sonarqube&quot; loading=&quot;lazy&quot; width=&quot;375&quot; height=&quot;375&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. SonarQube란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SonarQube는 정적 분석 도구 중 하나로 20가지 이상의 언어와 프레임워크를 지원한다. 코드 품질을 관리하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 코드를 작성하거나, 코드 리뷰를 할 때 놓치기 쉬운 기본적인 실수들을 하지 않게 도와준다. PullRequest와 연동하면 분석 결과를 리뷰도 달아주기까지 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 어떤 것을 우리에게 제공해주는가?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SonarQube를 활용하면 문제에 대한 분석을 자동화할 수 있다. 아래와 같은 지표를 한 눈에 파악할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Code Smell&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;: 해커들에게 잠재적 약점이 될 수 있는 보안상 이슈. (ex. SQL Injection, XSS 공격)&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;/li&gt;
&lt;li&gt;&lt;b&gt;사이즈&lt;/b&gt;: 코드 라인 전체 라인 수 구문, 함수 클래스 파일, 디렉터리 주석 수, 코멘트 비율 등&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 예시&lt;/h3&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;불필요한 주석 (ex. 코드 자체를 주석 처리, Todo 주석)&lt;/li&gt;
&lt;li&gt;테스트에서 isEqualTo(0) 사용 (대신 isZero() 사용할 수 있다.)&lt;/li&gt;
&lt;li&gt;테스트에서 너무 많은 단언문을 남발&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. SonarQube 환경 구축하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SonarQube 환경을 구축하기 위해 Docker Compsoe를 활용했다. (Docker와 Docker Compose 설치를 해야 아래 과정대로 따라올 수 있다.)&lt;/li&gt;
&lt;li&gt;소나큐브는 정적 분석 결과를 postgres DB를 사용해 저장하기 때문에 기본적으로 2개의 컨테이너(서버와 DB)가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Docker Compose&lt;/h3&gt;
&lt;pre id=&quot;code_1726368134106&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.9&quot;

services:
    sonarqube:
        image: sonarqube:lts-community
        depends_on:
            - sonar_db
        environment:
            SONAR_JDBC_URL: jdbc:postgresql://sonar_db:5432/sonar
            SONAR_JDBC_USERNAME: username
            SONAR_JDBC_PASSWORD: password
        ports:
            - &quot;9000:9000&quot;
        volumes:
            - sonarqube_conf:/opt/sonarqube/conf
            - sonarqube_data:/opt/sonarqube/data
            - sonarqube_extensions:/opt/sonarqube/extensions
            - sonarqube_logs:/opt/sonarqube/logs
            - sonarqube_temp:/opt/sonarqube/temp

    sonar_db:
        image: postgres:13
        environment:
            POSTGRES_USER: username
            POSTGRES_PASSWORD: password
            POSTGRES_DB: sonar
        volumes:
            - sonar_db:/var/lib/postgresql
            - sonar_db_data:/var/lib/postgresql/data

volumes:
    sonarqube_conf:
    sonarqube_data:
    sonarqube_extensions:
    sonarqube_logs:
    sonarqube_temp:
    sonar_db:
    sonar_db_data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. SonarQube 실행하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 docker compose 스크립트를 실행하고 9090 포트에 접속해보자. 처음 홈페이지에 들어가면 로그인을 해야하는데 SonarQube의 기본 아이디, 비밀번호를 사용하면 된다.&lt;/li&gt;
&lt;li&gt;아이디: admin, 비밀번호: admin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/butDaz/btsJDRyhGkE/cnMt0xnHAqnNCFBxPQ1Zwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/butDaz/btsJDRyhGkE/cnMt0xnHAqnNCFBxPQ1Zwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/butDaz/btsJDRyhGkE/cnMt0xnHAqnNCFBxPQ1Zwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbutDaz%2FbtsJDRyhGkE%2FcnMt0xnHAqnNCFBxPQ1Zwk%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; alt=&quot;SonarQube Login&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;395&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;530&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;로그인을 하면 패스워드를 새롭게 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3YOfR/btsJES33R7A/uYcl01jnUMcCeu4U8rRJkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3YOfR/btsJES33R7A/uYcl01jnUMcCeu4U8rRJkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3YOfR/btsJES33R7A/uYcl01jnUMcCeu4U8rRJkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3YOfR%2FbtsJES33R7A%2FuYcl01jnUMcCeu4U8rRJkk%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; alt=&quot;Update SonarQube Password&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;980&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;980&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;접속이 완료되면 아래와 같이 프로젝트 생성창이 뜬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2684&quot; data-origin-height=&quot;1340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqSoQ1/btsJDFrfFHZ/PL14nsa9ZBl7d4PkgJL9Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqSoQ1/btsJDFrfFHZ/PL14nsa9ZBl7d4PkgJL9Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqSoQ1/btsJDFrfFHZ/PL14nsa9ZBl7d4PkgJL9Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqSoQ1%2FbtsJDFrfFHZ%2FPL14nsa9ZBl7d4PkgJL9Z1%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; alt=&quot;SonarQube Homepage&quot; loading=&quot;lazy&quot; width=&quot;2684&quot; height=&quot;1340&quot; data-origin-width=&quot;2684&quot; data-origin-height=&quot;1340&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. SonarQube 연동하기 (Jenkins, GitLab)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 설정을 하기 전에 &lt;u&gt;&lt;b&gt;Jenkins에서 형상 관리 저장소(GitHub, GitLab 등) 설정이 되어 있다고 가정하고 진행한다.&lt;/b&gt;&lt;/u&gt; (만약 설정되어 있지 않다면 플러그인을 설치하고 필요한 Secret 설정을 해줘야 한다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. SonarQube Token 발급하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jenkins에서 사용할 SonarQube의 토큰을 먼저 생성해야 한다.&lt;/li&gt;
&lt;li&gt;프로젝트를 생성하기 위해 &lt;b&gt;Manually&lt;/b&gt;를 클릭해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IWgRI/btsJCYymC7U/3QaqREYbMtxKR8E3sp8R00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IWgRI/btsJCYymC7U/3QaqREYbMtxKR8E3sp8R00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IWgRI/btsJCYymC7U/3QaqREYbMtxKR8E3sp8R00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIWgRI%2FbtsJCYymC7U%2F3QaqREYbMtxKR8E3sp8R00%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; alt=&quot;SonarQube Homepage&quot; loading=&quot;lazy&quot; width=&quot;1382&quot; height=&quot;880&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;880&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;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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;994&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t6QQQ/btsJCU3QFgE/O18PQdHif5kLZEWPFaBScK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t6QQQ/btsJCU3QFgE/O18PQdHif5kLZEWPFaBScK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t6QQQ/btsJCU3QFgE/O18PQdHif5kLZEWPFaBScK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft6QQQ%2FbtsJCU3QFgE%2FO18PQdHif5kLZEWPFaBScK%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; alt=&quot;Create a project&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;594&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;994&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇게 새로 생성한 프로젝트에는 아래와 같이 &lt;b&gt;Locally&lt;/b&gt; 메뉴가 있을 것이다. 클릭하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGjro2/btsJD7U5eAj/O9OtZZLNnTfeVIl1TyrWF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGjro2/btsJD7U5eAj/O9OtZZLNnTfeVIl1TyrWF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGjro2/btsJD7U5eAj/O9OtZZLNnTfeVIl1TyrWF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGjro2%2FbtsJD7U5eAj%2FO9OtZZLNnTfeVIl1TyrWF1%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; alt=&quot;Click the Locally button&quot; loading=&quot;lazy&quot; width=&quot;1908&quot; height=&quot;1034&quot; data-origin-width=&quot;1908&quot; data-origin-height=&quot;1034&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;여기서 토큰 이름과 기한을 설정해 토큰을 생성하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/umHjD/btsJDyFMEH9/slcqk1lkhKr0jpgfeKCOhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/umHjD/btsJDyFMEH9/slcqk1lkhKr0jpgfeKCOhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/umHjD/btsJDyFMEH9/slcqk1lkhKr0jpgfeKCOhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FumHjD%2FbtsJDyFMEH9%2Fslcqk1lkhKr0jpgfeKCOhK%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; alt=&quot;Analyze your project&quot; loading=&quot;lazy&quot; width=&quot;1488&quot; height=&quot;802&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;802&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;&lt;b&gt;Generate&lt;/b&gt; 버튼을 클릭하면 아래와 같이 토큰이 생성된다. 토큰을 메모장에 따로 저장해둔 다음 &lt;b&gt;Contiune&lt;/b&gt; 버튼을 클릭하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LZksu/btsJDOByQs3/2tUBLxPjS5TOZpaIAmMKe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LZksu/btsJDOByQs3/2tUBLxPjS5TOZpaIAmMKe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LZksu/btsJDOByQs3/2tUBLxPjS5TOZpaIAmMKe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLZksu%2FbtsJDOByQs3%2F2tUBLxPjS5TOZpaIAmMKe1%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; alt=&quot;Provide a token&quot; loading=&quot;lazy&quot; width=&quot;1458&quot; height=&quot;646&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;646&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;SonarQube를 실행하기 위한 정보를 보여준다.&lt;/li&gt;
&lt;li&gt;plugins는 Spring Project에 설정해줘야 한다.
&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://myvelop.tistory.com/239#5.%20%EC%97%B0%EB%8F%99%20%EC%8B%9C%20%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;5. 연동 시 주의사항&lt;/b&gt;&lt;/a&gt;에서 plugins 버전 설정을 참고 바람.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그리고 &quot;and run the following command:&quot; 에 작성된 명령어는 Jenkins 파이프라인에서 직접 실행할 명령어다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSvpcv/btsJEZ9RLCr/U3uyWPNa10kSK4VTt9SIzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSvpcv/btsJEZ9RLCr/U3uyWPNa10kSK4VTt9SIzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSvpcv/btsJEZ9RLCr/U3uyWPNa10kSK4VTt9SIzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSvpcv%2FbtsJEZ9RLCr%2FU3uyWPNa10kSK4VTt9SIzK%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; alt=&quot;Run analysis on your project&quot; loading=&quot;lazy&quot; width=&quot;1732&quot; height=&quot;864&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;864&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;h3 data-ke-size=&quot;size23&quot;&gt;3-2. SonarQube Jenkins Webhook 설정하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최상단에 있는 메뉴인 &lt;b&gt;Administration&lt;/b&gt;을 클릭하고 &lt;b&gt;Configuration &amp;gt; Webhooks&lt;/b&gt;를 클릭하자.&lt;/li&gt;
&lt;li&gt;우측 상단에 있는 &lt;b&gt;Create&lt;/b&gt; 버튼을 클릭한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2722&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by8ssf/btsJENPlB6r/Q35S0rDZKAQlXflOHhY8f0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by8ssf/btsJENPlB6r/Q35S0rDZKAQlXflOHhY8f0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by8ssf/btsJENPlB6r/Q35S0rDZKAQlXflOHhY8f0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby8ssf%2FbtsJENPlB6r%2FQ35S0rDZKAQlXflOHhY8f0%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; alt=&quot;SonarQube Webhook&quot; loading=&quot;lazy&quot; width=&quot;2722&quot; height=&quot;684&quot; data-origin-width=&quot;2722&quot; data-origin-height=&quot;684&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;URL은 Jenkins 서버의 endpoint()를 작성해주면 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;endpoint 형식: &lt;b&gt;http://{jenkins_url}:{port}/sonarqube-webhook/&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;1084&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qnl4D/btsJDdPCNNx/0UF7uITFn1hC77nenRBNBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qnl4D/btsJDdPCNNx/0UF7uITFn1hC77nenRBNBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qnl4D/btsJDdPCNNx/0UF7uITFn1hC77nenRBNBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQnl4D%2FbtsJDdPCNNx%2F0UF7uITFn1hC77nenRBNBK%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; alt=&quot;Create SonarQube Webhook&quot; loading=&quot;lazy&quot; width=&quot;497&quot; height=&quot;601&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;1084&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;h3 data-ke-size=&quot;size23&quot;&gt;3-3. Jenkins 설정하기 - SecretKey&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본격적으로 SonarQube를 설정하기 전에 SecretKey를 설정해보도록 하자.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Jenkins 관리 &amp;gt; Credentials&lt;/b&gt;를 클릭한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2134&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZrPZU/btsJDTW6txD/BpxWqjk4agLTyRc3x6JaY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZrPZU/btsJDTW6txD/BpxWqjk4agLTyRc3x6JaY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZrPZU/btsJDTW6txD/BpxWqjk4agLTyRc3x6JaY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZrPZU%2FbtsJDTW6txD%2FBpxWqjk4agLTyRc3x6JaY0%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; alt=&quot;Jenkins Security Setting&quot; loading=&quot;lazy&quot; width=&quot;2134&quot; height=&quot;782&quot; data-origin-width=&quot;2134&quot; data-origin-height=&quot;782&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;&lt;b&gt;Stores scoped to Jenkins&lt;/b&gt;의 &lt;b&gt;System&lt;/b&gt;을 클릭한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwpi96/btsJDssckDQ/rBoUlfrh1iryhQephSBvy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwpi96/btsJDssckDQ/rBoUlfrh1iryhQephSBvy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwpi96/btsJDssckDQ/rBoUlfrh1iryhQephSBvy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwpi96%2FbtsJDssckDQ%2FrBoUlfrh1iryhQephSBvy1%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; alt=&quot;Store scoped to Jenkins&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;231&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;358&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;&lt;b&gt;Global credentials&lt;/b&gt;를 클릭한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2436&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cW57kh/btsJE2FwI1G/qO85Cp5AbvhtI0Kmknt7Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cW57kh/btsJE2FwI1G/qO85Cp5AbvhtI0Kmknt7Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cW57kh/btsJE2FwI1G/qO85Cp5AbvhtI0Kmknt7Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcW57kh%2FbtsJE2FwI1G%2FqO85Cp5AbvhtI0Kmknt7Mk%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; alt=&quot;Global credentials&quot; loading=&quot;lazy&quot; width=&quot;586&quot; height=&quot;137&quot; data-origin-width=&quot;2436&quot; data-origin-height=&quot;570&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;&lt;b&gt;Add Credentials&lt;/b&gt; 버튼을 클릭한다.&lt;/li&gt;
&lt;li&gt;종류를 Secret text로 설정한 뒤, SonarQube에서 발급받은 토큰을 등록해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2748&quot; data-origin-height=&quot;1342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rQbIs/btsJDu4Cjpa/bw9HSMeiTsqlkSYhjKmyek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rQbIs/btsJDu4Cjpa/bw9HSMeiTsqlkSYhjKmyek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rQbIs/btsJDu4Cjpa/bw9HSMeiTsqlkSYhjKmyek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrQbIs%2FbtsJDu4Cjpa%2Fbw9HSMeiTsqlkSYhjKmyek%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; alt=&quot;New credentials&quot; loading=&quot;lazy&quot; width=&quot;2748&quot; height=&quot;1342&quot; data-origin-width=&quot;2748&quot; data-origin-height=&quot;1342&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. Jenkins 설정하기 - SonarQube Server&lt;/h3&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;먼저 SonarQube 플러그인이 설치되어 있어야 한다. &lt;b&gt;SonarQube Scanner&lt;/b&gt;와 &lt;b&gt;Sonar Quality Gates&lt;/b&gt;를 설치해주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2388&quot; data-origin-height=&quot;1198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmgqVG/btsJEnXHTSp/WoVnUu13OkCCKuuzkcmGqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmgqVG/btsJEnXHTSp/WoVnUu13OkCCKuuzkcmGqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmgqVG/btsJEnXHTSp/WoVnUu13OkCCKuuzkcmGqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmgqVG%2FbtsJEnXHTSp%2FWoVnUu13OkCCKuuzkcmGqk%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; alt=&quot;Jenkins Plugins&quot; loading=&quot;lazy&quot; width=&quot;2388&quot; height=&quot;1198&quot; data-origin-width=&quot;2388&quot; data-origin-height=&quot;1198&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;설치가 완료되었다면 &lt;b&gt;Jenkins 관리 &amp;gt; System&lt;/b&gt;의 &lt;b&gt;SonarQube servers&lt;/b&gt; 설정을 확인하자.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Server URL&lt;/b&gt;은 SonarQube의 서버 URL을 넣어주면 된다. 만약 Jenkins와 SonarQube가 같은 가상 머신 안에 있다면 Default 설정인 localhost:9000으로 두어도 상관없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Server authentication token&lt;/b&gt;에는 위에서 입력한 credentials의 토큰을 드롭다운으로 선택할 수 있다. sonarqube token을 선택해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2716&quot; data-origin-height=&quot;1542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vYIhq/btsJC0Qu2YV/KO7plx5Ezzq3KfuIZ1vC6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vYIhq/btsJC0Qu2YV/KO7plx5Ezzq3KfuIZ1vC6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vYIhq/btsJC0Qu2YV/KO7plx5Ezzq3KfuIZ1vC6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvYIhq%2FbtsJC0Qu2YV%2FKO7plx5Ezzq3KfuIZ1vC6K%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; alt=&quot;register SonarQube servers&quot; loading=&quot;lazy&quot; width=&quot;2716&quot; height=&quot;1542&quot; data-origin-width=&quot;2716&quot; data-origin-height=&quot;1542&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;SonarQube Scanner Server를 따로 설치하지 않았다면 Jenkins 내에서 &lt;b&gt;Install automatically&lt;/b&gt;를 체크하여 자동으로 설치하도록 구성해야 한다. (Jenkins 관리 &amp;gt; Tools &amp;gt; SonarQube Scanner installations)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2744&quot; data-origin-height=&quot;1372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6ad4M/btsJDtxNA7t/IQGe9gwISAARcgGR80Uvp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6ad4M/btsJDtxNA7t/IQGe9gwISAARcgGR80Uvp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6ad4M/btsJDtxNA7t/IQGe9gwISAARcgGR80Uvp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6ad4M%2FbtsJDtxNA7t%2FIQGe9gwISAARcgGR80Uvp1%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; alt=&quot;SonarQube Scanner installations&quot; loading=&quot;lazy&quot; width=&quot;2744&quot; height=&quot;1372&quot; data-origin-width=&quot;2744&quot; data-origin-height=&quot;1372&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5. Jenkins 파이프라인 스크립트 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1726380260477&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any

    stages {
        stage('checkout') {
            steps {
                script {
                    SCM_VARS = git branch: '${gitlabSourceBranch}', credentialsId: '${CREDENTIAL}', url: '${GIT_URL}'
                    env.GIT_COMMIT = SCM_VARS.GIT_COMMIT
                }
            }
        }
        
        stage('Build') {
            steps {
                sh './gradlew clean build'
            }
        }
        
        stage('SonarQube analysis') {
            steps{
                withSonarQubeEnv('sonarqube'){
                    sh &quot;&quot;&quot;
                    ./gradlew clean sonar \
                    -Dsonar.projectKey={SONAR_PROJECT} \
                    -Dsonar.host.url=${SONAR_URL} \ 
                    -Dsonar.login=${SONAR_TOKEN}
                    &quot;&quot;&quot;
                }
            }
        }
        
        stage('SonarQube Quality Gate'){
            steps{
                timeout(time: 1, unit: 'MINUTES') {
                    script{
                        echo &quot;Start&quot;
                        def qg = waitForQualityGate()
                        echo &quot;Status: ${qg.status}&quot;
                        if(qg.status != 'OK') {
                            echo &quot;NOT OK Status: ${qg.status}&quot;
                            updateGitlabCommitStatus(name: &quot;SonarQube Quality Gate&quot;, state: &quot;failed&quot;)
                            error &quot;Pipeline aborted due to quality gate failure: ${qg.status}&quot;
                        } else{
                            echo &quot;status: ${qg.status}&quot;
                            updateGitlabCommitStatus(name: &quot;SonarQube Quality Gate&quot;, state: &quot;success&quot;)
                        }
                        echo &quot;End&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;&quot;SonarQube Analysis&quot; stage는 대상 SonarQube Server를 지정하여 분석을 진행한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;withSonarQubeEnv&lt;/b&gt;는 &lt;b&gt;Jenkins 관리 &amp;gt; System&lt;/b&gt;에 등록한 &lt;b&gt;SonarQube Servers&lt;/b&gt;의 &lt;b&gt;Name&lt;/b&gt;과 매핑된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&quot;SonarQube Quality Gate&quot; stage는 SonarQube Servers에서 분석 결과를 응답하기까지 대기한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대기 시간을 지정하여 무한정 대기하는 상태를 방지할 수 있다. &lt;b&gt;waitForQualityGate&lt;/b&gt;를 사용해 Server에서 분석을 완료하고 상태를 반환할때까지 파이프라인을 중단시키는 시간을 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. GitLab으로 리포팅하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;이 기능은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Developer Edition&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이상에서만 가능하다.&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;아래 블로그 링크에서 특정 플러그인을 사용해 무료로 Github 리포팅 사용하는 방법을 알아볼 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/229&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Server]&amp;nbsp;소나큐브(SonarQube)&amp;nbsp;커뮤니티&amp;nbsp;무료&amp;nbsp;버전에서&amp;nbsp;PR&amp;nbsp;데코레이션(Pull&amp;nbsp;Request&amp;nbsp;Decoration)&amp;nbsp;설정&amp;nbsp;적용하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. GitLab OAuth2 로그인 애플리케이션 구성 및 SonarQube 등록&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 문서를 참고해 OAuth2 애플리케이션을 구성해주자&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.sonarsource.com/sonarqube/9.9/instance-administration/authentication/gitlab/&quot;&gt;SonarQube Docs: GitLab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitLab에서 애플리케이션을 만들면 Application ID와 Secret을 알려준다. 그 정보를 SonarQube에 입력해줘야 한다.&lt;/li&gt;
&lt;li&gt;Administration &amp;gt; Configuration &amp;gt; General Setting &amp;gt; Authentication&lt;/li&gt;
&lt;li&gt;GitLab 주소 및 Application ID와 Secret을 입력해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2124&quot; data-origin-height=&quot;1354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n0ntS/btsJE9xWGWY/e4KpEO8cPadMLpG0fdmwlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n0ntS/btsJE9xWGWY/e4KpEO8cPadMLpG0fdmwlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n0ntS/btsJE9xWGWY/e4KpEO8cPadMLpG0fdmwlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn0ntS%2FbtsJE9xWGWY%2Fe4KpEO8cPadMLpG0fdmwlK%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; alt=&quot;SonarQube &amp;amp;gt; Configuration &amp;amp;gt; General Setting Page&quot; loading=&quot;lazy&quot; width=&quot;2124&quot; height=&quot;1354&quot; data-origin-width=&quot;2124&quot; data-origin-height=&quot;1354&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;4-2. SonarQube에 GitLab 연동하기&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 style=&quot;color: #0070d1;&quot; href=&quot;https://docs.sonarsource.com/sonarqube/latest/devops-platform-integration/gitlab-integration/global-setup/&quot;&gt;SonarQube Docs: Setting up the GitLab integration at the global level&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitLab에서 Access Token을 발급받는다. scope는 api와 read_api 두 가지를 선택하도록 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2278&quot; data-origin-height=&quot;1460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwMO0G/btsJDXyqIlK/2Q3pOzBxhT91guUlTaQtV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwMO0G/btsJDXyqIlK/2Q3pOzBxhT91guUlTaQtV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwMO0G/btsJDXyqIlK/2Q3pOzBxhT91guUlTaQtV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwMO0G%2FbtsJDXyqIlK%2F2Q3pOzBxhT91guUlTaQtV1%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; alt=&quot;Add a personal access token page&quot; loading=&quot;lazy&quot; width=&quot;2278&quot; height=&quot;1460&quot; data-origin-width=&quot;2278&quot; data-origin-height=&quot;1460&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;Administration &amp;gt; Configuration &amp;gt; General Settings &amp;gt; DevOps Platform Integrations&lt;/li&gt;
&lt;li&gt;Create configuration 버튼을 클릭해 연동 정보를 입력해주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6Wm4I/btsJDY5eVg8/0xDaBdM67unpBQAPnl2afk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6Wm4I/btsJDY5eVg8/0xDaBdM67unpBQAPnl2afk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6Wm4I/btsJDY5eVg8/0xDaBdM67unpBQAPnl2afk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6Wm4I%2FbtsJDY5eVg8%2F0xDaBdM67unpBQAPnl2afk%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; alt=&quot;devops platform&quot; loading=&quot;lazy&quot; width=&quot;2554&quot; height=&quot;1090&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;1090&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;API URL을 입력할 때 뒤에 /api/v4 라는 path를 붙여줘야 한다.&lt;/li&gt;
&lt;li&gt;도메인 주소는 현재 사용하고 있는 gitlab의 주소를 넣어주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KtwWz/btsJE9SflOj/So37aEFLwpsrOkhul3zsq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KtwWz/btsJE9SflOj/So37aEFLwpsrOkhul3zsq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KtwWz/btsJE9SflOj/So37aEFLwpsrOkhul3zsq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKtwWz%2FbtsJE9SflOj%2FSo37aEFLwpsrOkhul3zsq0%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; alt=&quot;add gitlab api&quot; loading=&quot;lazy&quot; width=&quot;1176&quot; height=&quot;856&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;856&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4-3. GitLab Webhook 등록하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 Jenkins의 파이프라인에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Build when a change is pushed to GitLab~~&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정을 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxdpGx/btsJDicq59j/KtZuTdR6q7ciXQKjjAkByK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxdpGx/btsJDicq59j/KtZuTdR6q7ciXQKjjAkByK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxdpGx/btsJDicq59j/KtZuTdR6q7ciXQKjjAkByK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxdpGx%2FbtsJDicq59j%2FKtZuTdR6q7ciXQKjjAkByK%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; alt=&quot;jenkins pipeline&quot; loading=&quot;lazy&quot; width=&quot;1314&quot; height=&quot;904&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;904&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;만약 merge request에 새로운 commit이 들어갔을 때도 파이프라인이 재실행되길 원한다면 설정을 아래와 같이 수정해주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVPtqE/btsJDYqzUFG/FTH0ajcG9fi6WdEmfKd7SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVPtqE/btsJDYqzUFG/FTH0ajcG9fi6WdEmfKd7SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVPtqE/btsJDYqzUFG/FTH0ajcG9fi6WdEmfKd7SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVPtqE%2FbtsJDYqzUFG%2FFTH0ajcG9fi6WdEmfKd7SK%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; alt=&quot;jenkins pipeline setting&quot; loading=&quot;lazy&quot; width=&quot;850&quot; height=&quot;370&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;370&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;고급 설정에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Secret token&lt;/b&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Generate&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;버튼을 클릭하여 토큰을 만들어주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4pVg/btsJDS47g20/zmk2RJ9AQU7Nnv7MA0JPXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4pVg/btsJDS47g20/zmk2RJ9AQU7Nnv7MA0JPXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4pVg/btsJDS47g20/zmk2RJ9AQU7Nnv7MA0JPXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4pVg%2FbtsJDS47g20%2Fzmk2RJ9AQU7Nnv7MA0JPXk%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; alt=&quot;Jenkins Secret Token&quot; loading=&quot;lazy&quot; width=&quot;1818&quot; height=&quot;428&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;428&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;GitLab의 Webhook 설정에서 Jenkins의 위 설정에서 나타난 URL 정보와 Secret Token을 입력해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blG8wG/btsJDuqgpK0/0un8AM13x7eALoS172u6wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blG8wG/btsJDuqgpK0/0un8AM13x7eALoS172u6wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blG8wG/btsJDuqgpK0/0un8AM13x7eALoS172u6wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblG8wG%2FbtsJDuqgpK0%2F0un8AM13x7eALoS172u6wK%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; alt=&quot;Webhooks&quot; loading=&quot;lazy&quot; width=&quot;1016&quot; height=&quot;982&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;982&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;&lt;b&gt;Trigger&lt;/b&gt;에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Merge Request Event&lt;/b&gt;를 체크해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;1446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U1tlN/btsJDy63Eor/9ZPAmRAcDN2SbLL82yz2V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U1tlN/btsJDy63Eor/9ZPAmRAcDN2SbLL82yz2V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U1tlN/btsJDy63Eor/9ZPAmRAcDN2SbLL82yz2V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU1tlN%2FbtsJDy63Eor%2F9ZPAmRAcDN2SbLL82yz2V0%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; alt=&quot;Webhooks&quot; loading=&quot;lazy&quot; width=&quot;544&quot; height=&quot;673&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;1446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. GitLab으로부터 Merge Request 정보 수신하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;앞에서 설명했다시피 이 기능은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Developer Edition&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이상에서만 가능하다.&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;아래의 공식문서를 참고하면 Pull Request(GitLab에서는 Merge Request)를 분석할 수 있는 기능에 대해 설명되어 있다.&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/pull-request-analysis/setting-up-the-pull-request-analysis/&quot;&gt;SonarQube Docs: Setting up the pull request analysis&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇다면 pull request 정보를 어디로부터 받아 넘겨줘야 할까? Jenkins의 GitLab 플러그인 문서를 확인해보면 여러가지 정보를 환경변수로 넣어준다는 사실을 알 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://plugins.jenkins.io/gitlab-plugin/&quot;&gt;Jenkins GitLab Plugin Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;실제로 Jenkins에서 printenv를 찍어보면 GitLab 정보가 환경변수에 저장되어 있는 것을 확인할 수 있다. 여기서 필요한 정보는 아래와 같이 세 가지다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726470032004&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gitlabMergeRequestIid=28
gitlabTargetBranch=merge
gitlabBranch=feat/sonar&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;위 내용을 pull request에 매칭해주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726475062513&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stage('SonarQube analysis') {
    steps{
        withSonarQubeEnv('lohasmeal-sonar'){
            sh &quot;&quot;&quot;
            ./gradlew sonar 
            
            ... args ...
            
            -Dsonar.pullrequest.key=${gitlabMergeRequestIid} \
            -Dsonar.pullrequest.branch=${gitlabSourceBranch} \
            -Dsonar.pullrequest.base=${gitlabTargetBranch}
            &quot;&quot;&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 연동 시 주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. SonarQube Scanner 버전 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gradlew의 sonar 명령어를 사용할 때 아래와 같은 에러와 함께 명령어가 실패하는 경우가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726382783973&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* What went wrong:
'org.gradle.api.provider.Provider org.gradle.api.reporting.Report.getOutputLocation()'&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;아래 질문의 답변에 의하면 SonarQube 버전을 5.x.x으로 설정하면 문제를 해결할 수 있다고 한다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://community.sonarsource.com/t/sonarqube-gradle-task-with-gradle-8-produces-a-crash-on-report-getoutputlocation/81252/12&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarQube QnA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sonarsource.com/sonarqube/9.8/analyzing-source-code/scanners/sonarscanner-for-gradle/?_gl=1*jf76fq*_gcl_au*MjA5ODc2NDA5MS4xNzI2MjMzMDU4*_ga*MTc3MTgxMDgzMS4xNzI2MjMzMDU5*_ga_9JZ0GZ5TC6*MTcyNjM4MjY3Ny40LjAuMTcyNjM4MjY3Ny42MC4wLjA.&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarQube Docs: SonarScanner for Gradle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spring Application의 SonarQube 플러그인 버전을 업그레이드했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726383578099&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.sonarqube&quot;) version(&quot;5.1.0.4882&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;5-2. &lt;/span&gt;Please provide compiled classes of your project with sonar.java.binaries property&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sonar.java.binaries를 설정해주지 않으면 아래와 같은 에러가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726384308111&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;* What went wrong:
Execution failed for task ':sonar'.
&amp;gt; Your project contains .java files, please provide compiled classes with sonar.java.binaries property, or exclude them from the analysis with sonar.exclusions property.&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;gradlew sonar를 실행할 때 Spring Application에서 SonarQube의 property를 설정해줄 수 있다. sonar.java.binaries 옵션을 추가해주도록 하자.&lt;/li&gt;
&lt;li&gt;build.gradle.kts 코드 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726385091731&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; sonarqube {
    properties {
        property(&quot;sonar.host.url&quot;, &quot;http://${SONARQUBE_URL}:9000&quot;)
        property(&quot;sonar.login&quot;, &quot;{SONARQUBE_TOKEN}&quot;)
        property(&quot;sonar.sources&quot;, &quot;src&quot;)
        property(&quot;sonar.language&quot;, &quot;java&quot;)
        property(&quot;sonar.sourceEncoding&quot;, &quot;UTF-8&quot;)
        property(&quot;sonar.coverage.jacoco.xmlReportPaths&quot;, &quot;${layout.buildDirectory.get()}/reports/jacoco/test/jacocoTestReport.xml&quot;)
        property(&quot;sonar.java.binaries&quot;, &quot;${layout.buildDirectory.get()}/classes&quot;)
        property(&quot;sonar.test.inclusions&quot;, &quot;**/*Test.java&quot;)
        property(&quot;sonar.exclusions&quot;, &quot;**/test/**, **/Q*.java, **/*Doc*.java, **/resources/**&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. SonarQube 정적 분석 결과 확인하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 Overview에서 SonarQube의 실행결과를 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2690&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buacnN/btsJEO1P0kF/v93kONZ8U23IyKxVrYEtc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buacnN/btsJEO1P0kF/v93kONZ8U23IyKxVrYEtc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buacnN/btsJEO1P0kF/v93kONZ8U23IyKxVrYEtc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuacnN%2FbtsJEO1P0kF%2Fv93kONZ8U23IyKxVrYEtc0%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; alt=&quot;overview report&quot; loading=&quot;lazy&quot; width=&quot;2690&quot; height=&quot;1368&quot; data-origin-width=&quot;2690&quot; data-origin-height=&quot;1368&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;이슈에서는 Code Smell과 Bug를 확인해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2662&quot; data-origin-height=&quot;1364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kXgzy/btsJDSjAXIU/KpZ0krb5UqKxNrQgs2wqaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kXgzy/btsJDSjAXIU/KpZ0krb5UqKxNrQgs2wqaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kXgzy/btsJDSjAXIU/KpZ0krb5UqKxNrQgs2wqaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkXgzy%2FbtsJDSjAXIU%2FKpZ0krb5UqKxNrQgs2wqaK%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; alt=&quot;SonarQube Issues&quot; loading=&quot;lazy&quot; width=&quot;2662&quot; height=&quot;1364&quot; data-origin-width=&quot;2662&quot; data-origin-height=&quot;1364&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 SonarQube 서버를 구성하고 사용한 지 딱 한 달이 되었다. SonarQube를 사용하고 나서 엄청난 변화가 있진 않았지만, 그냥 지나칠 수도 있는 실수에 대해 피드백 바로 받을 수 있기 때문에 코드 품질이 나빠지는 것을 방지할 수 있었다. 코드의 상태가 조금씩 좋아지는 중이다.&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;SonarQube를 더 잘 활용하기 위해 아래와 같은 방식을 고려해볼 수 있다.&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;SonarLint&lt;/b&gt; IDE 플러그인 설치하여 SonarQube와 연동하기&lt;/li&gt;
&lt;li&gt;SonarQube에서 Custom Ruleset 설정하고 SonarLint로 설정 가져오기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SonarQube를 사용하는 것은 인프라 리소스를 꽤나 사용해야 하기 때문에 사용하기 망설여질 수 있다. 만약 리소스가 걱정이라면 &lt;b&gt;SonarLint&lt;/b&gt;라는 플러그인만 설치해 사용하는 것만으로도 코드의 품질을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;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.sonarsource.com/sonarqube/latest/devops-platform-integration/gitlab-integration/introduction/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarQube Docs: GitLab Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@denis.verkhovsky/sonarqube-with-docker-compose-complete-tutorial-2aaa8d0771d4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarQube with Docker compose: complete tutorial&lt;b&gt;&lt;/b&gt;&lt;/a&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.opendocs.co.kr/?p=721&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Setting | DevOps] jenkins, gitlab, sonarqube 연동설정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://waspro.tistory.com/596&quot;&gt;SonarQube &lt;span&gt;정적분석&lt;/span&gt; &lt;span&gt;및&lt;/span&gt; Jenkins CI/CD &lt;span&gt;통합&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://brunch.co.kr/@joypinkgom/45&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SonarQube + Jenkins + GitLab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@lxxjn0/%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D-%EB%8F%84%EA%B5%AC-%EC%A0%81%EC%9A%A9%EA%B8%B0-3%ED%8E%B8-SonarQube-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코드분석 도구 적용기 - 3편, SonarQube 적용하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://taetaetae.github.io/2018/02/08/jenkins-sonar-github-integration/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;소나큐브 이용 코드 정적분석 자동화&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/229&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;소나큐브 커뮤니티 버전에서 Pull Request Decoration 설정 적용하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <category>Sonar</category>
      <category>sonarqube</category>
      <category>정적분석툴</category>
      <category>코드분석</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/239</guid>
      <comments>https://myvelop.tistory.com/239#entry239comment</comments>
      <pubDate>Sat, 19 Oct 2024 15:16:43 +0900</pubDate>
    </item>
    <item>
      <title>글또 10기 시작!</title>
      <link>https://myvelop.tistory.com/244</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또 지원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9기에 이어 10기에도 지원했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것저것 벌여 놓은 일들(스터디 2개 운영, 오픈소스 컨트리뷰션 활동 등)이 많아 글또를 잘 할 수 있을까? 라는 생각이 있었다. 그래도 하고 싶었다. 저번 기수에서 다양한 활동을 하진 못했으나, 나의 글쓰기 방식이 바뀌고 작문 능력이 향상된 것만으로도 좋았다. 또 반상회 행사, 커피챗도 좋은 경험이었기에 글또 지원을 결정하게 되었다.&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;s&gt;이제 반상회 외롭게 혼자 가지 않아도..&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마지막 글또&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또는 10기를 끝으로 막을 내린다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 OT에서 성윤님이 &quot;끝이 없으면 지친다&quot;라는 말씀을 하셨다.&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;(비록 두 기수밖에 참여하지 않았지만) 글또라는 커뮤니티가 끝나는 것은 나에게도 큰 의미가 있는 일이라고 생각한다. 글또가 끝나고 나면 내가 몸담을 새로운 커뮤니티를 찾게 될 것이고, 내가 글또에서 배운 긍정적인 영향력을 새로운 커뮤니티에 전파하는 경험을 하게 될지도 모른다.  마지막이 아쉽지 않게 내가 해볼 수 있는 것들은 다 해보고 싶다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예전의 목표?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 글또를 시작할 때 잡았던 목표가 과연 나에게 도움이 되었을까? 라는 질문으로 스스로 던져봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미제출 없이 글 작성하기.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 3월에 직장 생활을 시작했다. 일이 바쁘다는 걸 핑계로 블로그 글 작성을 중단했었다. 회사에서 많은 일을 했지만 정리가 부족한 탓에 실제로 나에게 남은 것이 없다는 사실을 깨닫게 됐다. 따라서 글쓰는 습관을 만드는 것이 나에게 가장 중요한 목표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스스로 결핍을 느꼈기 때문에 목표에 대한 집착도 강했다. 덕분에 3달 동안 야근에 치여 살던 내가 미제출 없이 글을 작성할 수 있었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하루 방문자 1000명&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그의 성장세를 봤을 땐 달성할 수 있을 거라고 생각했다. 그리고 가까워 보였었다. 처음 글또 활동을 시작할 때는 매일 500명 정도의 방문자가 들어왔었고, 700명까지도 올라왔던 적이 있었다.&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;내가 읽기에도 다른 사람이 보기에도 글또 활동할 때 작성했던 글의 퀄리티가 다른 글에 비해 높다고 판단하는데, 오히려 방문자는 줄어드는 기현상(?)을 보면서 많은 생각이 들었던 것 같다. '내가 글 제목을 잘못 지어서 방문자가 적어진 걸까?' '내 글이 다른 사람들이 읽기에 쉽지가 않은가?' 내가 &quot;방문자 수&quot;라는 명목에 집착하고 있는 것은 아닐까? 라는 의문도 들기 시작했다.&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;&quot;방문자 수&quot;라는 수치는 블로그가 잘 운영되고 있음을 증명할 수 있는 요인 중 하나다.&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;/li&gt;
&lt;li&gt;'내 글을 정기적으로 소비자하는 구독자가 있는가'&lt;/li&gt;
&lt;li&gt;'검색엔진 최적화가 되어 있는가'&lt;/li&gt;
&lt;li&gt;'키워드를 잘 설정했는가'&lt;/li&gt;
&lt;li&gt;'소셜 미디어 유입이 있는가'&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 다양한 요인으로 인해 결정되는 수치기 때문에 &quot;방문자 수가 많지 않다&quot;라는 사실이 &quot;내가 좋지 않은 글을 쓰고 있다&quot;라는 논리로 바로 이어지진 않는다.&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;방문자 수가 늘어난다는 것은 &quot;눈에 보이는&quot; 수치였기 때문에 블로그 운영하면서 몇 안되는 도파민이 터지는 일이었다. 그렇기 때문에 수치에 집착했던 것 같다. 글또 9기가 좋았던 이유는 내가 공부하고 깨달은 내용을 정리하면서 &lt;b&gt;내 것으로 체화하는 과정&lt;/b&gt;이 나에게 큰 의미가 있는 과정이라는 것을 알았기 때문이다. 따라서 &quot;방문자 수&quot;보다는 나에게 더 유의미한 목표를 고민해보는 것이 좋다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;링크드인에 글 3번 공유하기&lt;/b&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;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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새로운 목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 기수의 목표. 부담을 많이 덜어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즐기고 싶다.&lt;/b&gt;&lt;/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;'하루 방문자 수를 늘리기 위한 방법을 찾아야 해. 언제 1000명 달성하지?'&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 달에 한 명씩 알아가자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;비현실적인 목표보단 작은 목표더라도 지금 당장 할 수 있는 걸 목표로 잡아보세요.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 OT에서 들었던 말 중 가장 인상적이었던 말이었다.&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;&lt;b&gt;일주일 앞서가기&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;소소하게(?) 일주일 빨리 글을 제출해 다른 활동들도 둘러볼 수 있는 여유를 가져보고 싶다. &lt;s&gt;1주차부터 실패..&lt;/s&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 글을 쓰고 싶은가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;좋은 글을 쓰고 싶다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 기준 &lt;b&gt;좋은 글이란 문제 해결하는 데 도움이 되는 글&lt;/b&gt;이다. 직접 문제와 부딪혀 보며 고민한 &lt;b&gt;여러 대안과 트레이드오프&lt;/b&gt;. 그리고 그 해결 방법을 채택해야 하는 &lt;b&gt;이유&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;독자들이 읽기 쉬운 글을 작성하고 싶다.&lt;/b&gt; 이는 &lt;b&gt;내 글의 독자층을 넓히는 것으로 이어진다.&lt;/b&gt;&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;저번 기수에는 좋은 글에 대한 정의를 탐구하고, 또 그런 글을 적기 위해 노력했다면, 이번 기수에서는 한 단계 더 나아가 다른 사람들이 보기에 술술 읽히는 글을 작성해보려고 한다. 독자가 아예 모르는 개념에 대해 설명하고 있더라도 추가자료 없이 내 글만 보고도 이해할 수 있는 수준으로 글을 작성하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제해결 과정을 잘 정리하고 싶다. 그리고 그 과정에서 배운 것을 잘 간직하고 싶다.&lt;/b&gt;&lt;/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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동/글또</category>
      <category>글또</category>
      <category>글또 10기</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/244</guid>
      <comments>https://myvelop.tistory.com/244#entry244comment</comments>
      <pubDate>Sun, 13 Oct 2024 20:20:47 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Free Tier EC2 인스턴스 생성하는 법</title>
      <link>https://myvelop.tistory.com/242</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDxdd5/btsJE1fXFH8/Q4owHXxhDI1uRE9jUv0KKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDxdd5/btsJE1fXFH8/Q4owHXxhDI1uRE9jUv0KKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDxdd5/btsJE1fXFH8/Q4owHXxhDI1uRE9jUv0KKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDxdd5%2FbtsJE1fXFH8%2FQ4owHXxhDI1uRE9jUv0KKk%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; alt=&quot;aws&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 프리 티어(Free Tier)?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS는 신규 고객에게 1년간 Free Tier라는 명목으로 무료 서비스를 제공한다.&lt;/li&gt;
&lt;li&gt;덕분에 AWS의 제품들을 부담없이 사용해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 사용량 제한&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boAneO/btsJDHprSWR/QQzxaoS7tmUiEDuXI7qu01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boAneO/btsJDHprSWR/QQzxaoS7tmUiEDuXI7qu01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boAneO/btsJDHprSWR/QQzxaoS7tmUiEDuXI7qu01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboAneO%2FbtsJDHprSWR%2FQQzxaoS7tmUiEDuXI7qu01%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; alt=&quot;AWS Free Tier&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;383&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;1066&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;하지만 위에 표시된 것처럼 사용량 제한이 있으며, 그 이상을 사용하게 되면 비용을 청구한다.&lt;/li&gt;
&lt;li&gt;예를 들어 EC2 서버 하나를 사용하면 750시간(31.25일)을 사용할 수 있으므로 한 달을 무료로 사용할 수 있지만 EC2 인스턴스를 2개 사용하면 각각 375시간 사용할 수 있으므로 한 달을 꼬박 사용하면 1500시간을 사용하는 것이므로 750시간에 대한 비용이 청구되는 것이다.&lt;/li&gt;
&lt;li&gt;또한 EC2의 스펙 또한 &lt;b&gt;t2.micro(t2.micro를 사용할 수 없는 Region은&lt;/b&gt;&amp;nbsp;&lt;b&gt;t3.micro로 대체 가능)&lt;/b&gt;으로 제한된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;t3가 최신 버전&lt;/u&gt;으로 t2에 비해 네트워크 성능이 대폭 향상되었으나 일부 region에서 사용하지 못하고 높은 비용이 발생할 수 있다는 단점이 있다고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. EC2 만들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. EC2 서비스 검색하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상단의 검색 창에서 EC2 서비스를 검색한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LtmfY/btsJDeOQUei/KtL6VYJadKQ9tyPq7mOPxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LtmfY/btsJDeOQUei/KtL6VYJadKQ9tyPq7mOPxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LtmfY/btsJDeOQUei/KtL6VYJadKQ9tyPq7mOPxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLtmfY%2FbtsJDeOQUei%2FKtL6VYJadKQ9tyPq7mOPxk%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; alt=&quot;search ec2&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;321&quot; data-origin-width=&quot;2180&quot; data-origin-height=&quot;992&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;EC2 서비스 페이지로 진입하면 아래와 같은 페이지가 뜬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;1316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PSztm/btsJDnSpoRa/ILF6tV0EwaluoAf9Zkb82k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PSztm/btsJDnSpoRa/ILF6tV0EwaluoAf9Zkb82k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PSztm/btsJDnSpoRa/ILF6tV0EwaluoAf9Zkb82k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPSztm%2FbtsJDnSpoRa%2FILF6tV0EwaluoAf9Zkb82k%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; alt=&quot;ec2 page&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;535&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;1316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. EC2 인스턴스 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인스턴스 시작&lt;/b&gt; 버튼을 클릭하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Lnde/btsJDH34tNW/u6jlRtIFkXUiwQHe1KN6Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Lnde/btsJDH34tNW/u6jlRtIFkXUiwQHe1KN6Uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Lnde/btsJDH34tNW/u6jlRtIFkXUiwQHe1KN6Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2Lnde%2FbtsJDH34tNW%2Fu6jlRtIFkXUiwQHe1KN6Uk%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; alt=&quot;start instance&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;454&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 서버 이름 지정&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R5QaQ/btsJEGJVr42/oVhmEqYN6wfOczBko0CHyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R5QaQ/btsJEGJVr42/oVhmEqYN6wfOczBko0CHyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R5QaQ/btsJEGJVr42/oVhmEqYN6wfOczBko0CHyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR5QaQ%2FbtsJEGJVr42%2FoVhmEqYN6wfOczBko0CHyK%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; alt=&quot;server name&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;129&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. OS 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운영체제는 가장 무난한 Ubuntu를 선택!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1Fvku/btsJDgMFcL6/utxtVlZVkRpQUNmj8rB4C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1Fvku/btsJDgMFcL6/utxtVlZVkRpQUNmj8rB4C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1Fvku/btsJDgMFcL6/utxtVlZVkRpQUNmj8rB4C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1Fvku%2FbtsJDgMFcL6%2FutxtVlZVkRpQUNmj8rB4C1%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; alt=&quot;os setting&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;626&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-5. 인스턴스 유형 선택&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스턴스 유형은 &lt;b&gt;t2.micro&lt;/b&gt;를 선택해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQOPjF/btsJDeg8iuT/wlLy5w1SUSUWmbFNy3arQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQOPjF/btsJDeg8iuT/wlLy5w1SUSUWmbFNy3arQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQOPjF/btsJDeg8iuT/wlLy5w1SUSUWmbFNy3arQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQOPjF%2FbtsJDeg8iuT%2FwlLy5w1SUSUWmbFNy3arQk%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; alt=&quot;instance type setting&quot; loading=&quot;lazy&quot; width=&quot;644&quot; height=&quot;194&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-6. 키 페어 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음은 키 페어 설정이다. 보안을 위해 &lt;b&gt;새 키 페어 생성&lt;/b&gt; 버튼을 클릭해 키 페어를 생성해주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIVVCX/btsJDPHAIkb/Ilm4DwNiZx2xuQJh5JYTY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIVVCX/btsJDPHAIkb/Ilm4DwNiZx2xuQJh5JYTY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIVVCX/btsJDPHAIkb/Ilm4DwNiZx2xuQJh5JYTY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIVVCX%2FbtsJDPHAIkb%2FIlm4DwNiZx2xuQJh5JYTY0%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; alt=&quot;key pair setting&quot; loading=&quot;lazy&quot; width=&quot;608&quot; height=&quot;171&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;392&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;OpenSSH를 사용할 것이기 때문에 pem파일로 선택했다. 여기서 생성되는 pem 파일은 바로 Device로 다운로드 된다. 잘 보관해두어야 한다. (잃어버리면 인스턴스에 접근할 수 없게 되어버린다..)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;1070&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LdYkg/btsJD5XEH2u/3B3KHe6oKOR2S9dDN2k3w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LdYkg/btsJD5XEH2u/3B3KHe6oKOR2S9dDN2k3w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LdYkg/btsJD5XEH2u/3B3KHe6oKOR2S9dDN2k3w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLdYkg%2FbtsJD5XEH2u%2F3B3KHe6oKOR2S9dDN2k3w0%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; alt=&quot;create key pair&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;573&quot; data-origin-width=&quot;1062&quot; data-origin-height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-7. 네트워크 설정&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;1206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTDg95/btsJDwn0hXb/BLuSzeWQjOnc7m7vCgoQMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTDg95/btsJDwn0hXb/BLuSzeWQjOnc7m7vCgoQMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTDg95/btsJDwn0hXb/BLuSzeWQjOnc7m7vCgoQMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTDg95%2FbtsJDwn0hXb%2FBLuSzeWQjOnc7m7vCgoQMK%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; alt=&quot;network setting&quot; loading=&quot;lazy&quot; width=&quot;664&quot; height=&quot;579&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;1206&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;만약 인스턴스에 접속할 IP를 제한하고 싶다면 아래 설정을 해주자. 현재 접속된 IP로 설정할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1lb30/btsJEUup4QY/koi1vkWSbZKAqOM4JPv2rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1lb30/btsJEUup4QY/koi1vkWSbZKAqOM4JPv2rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1lb30/btsJEUup4QY/koi1vkWSbZKAqOM4JPv2rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1lb30%2FbtsJEUup4QY%2Fkoi1vkWSbZKAqOM4JPv2rK%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; alt=&quot;traffic setting&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;231&quot; data-origin-width=&quot;974&quot; data-origin-height=&quot;368&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;HTTPS(443 port)나 HTTP(80 port)의 접근을 허용하고 싶다면 위의 &lt;b&gt;인터넷에서 HTTPS 트래픽 허용&lt;/b&gt;과 &lt;b&gt;인터넷에서 HTTP 트래픽 허용&lt;/b&gt;을 체크해주도록 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-8. 스토리지 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프리 티어에서는 최대 30GIB까지 설정 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4PS1B/btsJFqmgCR4/UF7Ul3xQeXcDBhluf0PYDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4PS1B/btsJFqmgCR4/UF7Ul3xQeXcDBhluf0PYDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4PS1B/btsJFqmgCR4/UF7Ul3xQeXcDBhluf0PYDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4PS1B%2FbtsJFqmgCR4%2FUF7Ul3xQeXcDBhluf0PYDk%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; alt=&quot;storage setting&quot; loading=&quot;lazy&quot; width=&quot;635&quot; height=&quot;350&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;764&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 인스턴스 시작하고 연결해보기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 인스턴스 시작&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 설정은 마쳤다. &lt;b&gt;인스턴스 시작&lt;/b&gt;&amp;nbsp;버튼을 클릭하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0VYzI/btsJDSdfx4W/sLK8Nw5Bg8WGsjOBrIvxw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0VYzI/btsJDSdfx4W/sLK8Nw5Bg8WGsjOBrIvxw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0VYzI/btsJDSdfx4W/sLK8Nw5Bg8WGsjOBrIvxw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0VYzI%2FbtsJDSdfx4W%2FsLK8Nw5Bg8WGsjOBrIvxw1%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; alt=&quot;start instance&quot; loading=&quot;lazy&quot; width=&quot;595&quot; height=&quot;141&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;172&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;잠깐의 시간이 지나면 인스턴스가 실행된다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;822&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cph1OO/btsJDH34R72/TzKSZra384z9rmbBMcwg9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cph1OO/btsJDH34R72/TzKSZra384z9rmbBMcwg9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cph1OO/btsJDH34R72/TzKSZra384z9rmbBMcwg9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcph1OO%2FbtsJDH34R72%2FTzKSZra384z9rmbBMcwg9K%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; alt=&quot;complete page&quot; loading=&quot;lazy&quot; width=&quot;721&quot; height=&quot;362&quot; data-origin-width=&quot;1636&quot; data-origin-height=&quot;822&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 인스턴스 연결해보기&lt;/h3&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;b&gt;인스턴스에 연결&lt;/b&gt; 버튼을 클릭하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cU1MA7/btsJDGD3wcD/TWyCuslxc2ZBGsunTkrLzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cU1MA7/btsJDGD3wcD/TWyCuslxc2ZBGsunTkrLzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cU1MA7/btsJDGD3wcD/TWyCuslxc2ZBGsunTkrLzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcU1MA7%2FbtsJDGD3wcD%2FTWyCuslxc2ZBGsunTkrLzK%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; alt=&quot;connect instance&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;446&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;446&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;&lt;b&gt;연결&lt;/b&gt; 버튼을 클릭한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEMLvY/btsJDW7yRmj/kCFqp6KDKB82yKZcE18CP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEMLvY/btsJDW7yRmj/kCFqp6KDKB82yKZcE18CP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEMLvY/btsJDW7yRmj/kCFqp6KDKB82yKZcE18CP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEMLvY%2FbtsJDW7yRmj%2FkCFqp6KDKB82yKZcE18CP0%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; alt=&quot;connect instance&quot; loading=&quot;lazy&quot; width=&quot;565&quot; height=&quot;551&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;998&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;AWS Console과 연결되는 것을 확인해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/re4Oq/btsJENvtInA/GX4jCGqRpsacdCpMczxRd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/re4Oq/btsJENvtInA/GX4jCGqRpsacdCpMczxRd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/re4Oq/btsJENvtInA/GX4jCGqRpsacdCpMczxRd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fre4Oq%2FbtsJENvtInA%2FGX4jCGqRpsacdCpMczxRd1%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; alt=&quot;aws console&quot; loading=&quot;lazy&quot; width=&quot;636&quot; height=&quot;499&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;1074&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;</description>
      <category>Infra/AWS</category>
      <category>AWS</category>
      <category>AWS EC2</category>
      <category>AWS 비용</category>
      <category>ec2</category>
      <category>free tier</category>
      <category>프리 티어</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/242</guid>
      <comments>https://myvelop.tistory.com/242#entry242comment</comments>
      <pubDate>Tue, 17 Sep 2024 17:07:14 +0900</pubDate>
    </item>
    <item>
      <title>[Jenkins] Jenkins에 Jacoco 연동하기 (Spring 환경)</title>
      <link>https://myvelop.tistory.com/237</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLpZif/btsJzQewZqt/Vn5fLS0flSM3ORPNK6aezK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLpZif/btsJzQewZqt/Vn5fLS0flSM3ORPNK6aezK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLpZif/btsJzQewZqt/Vn5fLS0flSM3ORPNK6aezK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLpZif%2FbtsJzQewZqt%2FVn5fLS0flSM3ORPNK6aezK%2Fimg.webp&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; alt=&quot;Jenkins signature&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;459&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;570&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;정적 분석 툴인 Jacoco를 CI/CD 과정에서 활용할 수 있습니다. 만약 Jenkins를 사용한다면 Jenkins 플러그인을 설치하고 간단한 스크립트만 작성하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring 애플리케이션 Jacoco 설정&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://myvelop.tistory.com/215&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://myvelop.tistory.com/215&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1726056928367&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Jacoco 설정하기 (build.gradle &amp;amp; .kts)&quot; data-og-description=&quot;Jacoco 자바코드의 커버리지를 체크할 때 사용하는 오픈소스 라이브러리이다. CI/CD와 연계해 테스트 커버리지를 충분히 채우지 못하면 배포가 되지 못하게 하는 등 구성원들에게 테스트 코드를 &quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/215&quot; data-og-url=&quot;https://myvelop.tistory.com/215&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Fr5tM/hyWZiDkIdi/Zn0M1I6SE2bBMcdcrFpglK/img.png?width=800&amp;amp;height=892&amp;amp;face=0_0_800_892,https://scrap.kakaocdn.net/dn/bZetVW/hyW2PlWMA2/4dAO61cK2OS9mAvmfYbF1k/img.png?width=800&amp;amp;height=892&amp;amp;face=0_0_800_892,https://scrap.kakaocdn.net/dn/JPEOe/hyW2RYn5jK/Ao7mSjd68sfVWPPNIsk1HK/img.png?width=750&amp;amp;height=836&amp;amp;face=0_0_750_836&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/215&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/215&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Fr5tM/hyWZiDkIdi/Zn0M1I6SE2bBMcdcrFpglK/img.png?width=800&amp;amp;height=892&amp;amp;face=0_0_800_892,https://scrap.kakaocdn.net/dn/bZetVW/hyW2PlWMA2/4dAO61cK2OS9mAvmfYbF1k/img.png?width=800&amp;amp;height=892&amp;amp;face=0_0_800_892,https://scrap.kakaocdn.net/dn/JPEOe/hyW2RYn5jK/Ao7mSjd68sfVWPPNIsk1HK/img.png?width=750&amp;amp;height=836&amp;amp;face=0_0_750_836');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Jacoco 설정하기 (build.gradle &amp;amp; .kts)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Jacoco 자바코드의 커버리지를 체크할 때 사용하는 오픈소스 라이브러리이다. CI/CD와 연계해 테스트 커버리지를 충분히 채우지 못하면 배포가 되지 못하게 하는 등 구성원들에게 테스트 코드를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 링크를 참고하면 Spring Application에서 Jacoco를 설정하는데 도움이 될 것입니다.&lt;/li&gt;
&lt;li&gt;참고로 Jenkins에서는 Jacoco를 해석할 때 xml 파일을 사용하기 때문에 아래와 같이 &lt;b&gt;jacocoTestReport Task&lt;/b&gt;에서 xml.enabled를 true로 설정해줘야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726057700245&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jacocoTestReport {
    dependsOn test
    reports {
        xml.enabled true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Jenkins 플러그인 설치&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jenkins UI에서 플러그인을 설치하는 방법에 대해 설명하겠습니다.&lt;/li&gt;
&lt;li&gt;먼저 Jenkins UI 홈페이지에서 Jenkins 관리 메뉴를 클릭합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccnYb8/btsJyV144Yw/wBsH08jFnJKj9CSvmgzuF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccnYb8/btsJyV144Yw/wBsH08jFnJKj9CSvmgzuF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccnYb8/btsJyV144Yw/wBsH08jFnJKj9CSvmgzuF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccnYb8%2FbtsJyV144Yw%2FwBsH08jFnJKj9CSvmgzuF0%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; alt=&quot;jenkins menu&quot; loading=&quot;lazy&quot; width=&quot;477&quot; height=&quot;598&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;966&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;System Configuration&lt;/b&gt; &amp;gt; &lt;b&gt;Plugins&lt;/b&gt; 를 클릭합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIu8NP/btsJAjgeIQj/mukItjSEdIYQNR5u9SoBm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIu8NP/btsJAjgeIQj/mukItjSEdIYQNR5u9SoBm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIu8NP/btsJAjgeIQj/mukItjSEdIYQNR5u9SoBm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIu8NP%2FbtsJAjgeIQj%2FmukItjSEdIYQNR5u9SoBm0%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; alt=&quot;jenkins system configuration&quot; loading=&quot;lazy&quot; width=&quot;679&quot; height=&quot;506&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;506&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좌측 메뉴에서 &lt;b&gt;Available plugins&lt;/b&gt;를 선택합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WhsLq/btsJxXzxeMB/sOKmggc8zgnOr4nb0PV4pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WhsLq/btsJxXzxeMB/sOKmggc8zgnOr4nb0PV4pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WhsLq/btsJxXzxeMB/sOKmggc8zgnOr4nb0PV4pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWhsLq%2FbtsJxXzxeMB%2FsOKmggc8zgnOr4nb0PV4pk%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; alt=&quot;Jenkins available plugins&quot; loading=&quot;lazy&quot; width=&quot;531&quot; height=&quot;501&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;700&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;&lt;b&gt;Jacoco&lt;/b&gt;를 검색하고, 선택해 &lt;b&gt;Install&lt;/b&gt; 버튼을 클릭해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxLznT/btsJzhKLvjc/cF1Fklp1MdGEkFA2iSXPI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxLznT/btsJzhKLvjc/cF1Fklp1MdGEkFA2iSXPI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxLznT/btsJzhKLvjc/cF1Fklp1MdGEkFA2iSXPI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxLznT%2FbtsJzhKLvjc%2FcF1Fklp1MdGEkFA2iSXPI0%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; alt=&quot;jacoco plugins&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;214&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;734&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;위의 과정을 거치면 일단 플러그인 설치는 완료입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Jenkins Script 작성&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;jacoco-plugin docs: &lt;a href=&quot;https://plugins.jenkins.io/jacoco/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://plugins.jenkins.io/jacoco/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1726057802878&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;JaCoCo&quot; data-og-description=&quot;This plugin integrates &amp;lt;a href=&amp;quot;http://www.eclemma.org/jacoco/trunk/index.html&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;nofollow noopener noreferrer&amp;quot;&amp;gt;JaCoCo code coverage reports&amp;lt;/a&amp;gt; to Jenkins.&quot; data-og-host=&quot;plugins.jenkins.io&quot; data-og-source-url=&quot;https://plugins.jenkins.io/jacoco/&quot; data-og-url=&quot;https://plugins.jenkins.io/jacoco&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bLhnJc/hyWZdPyM25/20cSXiUOKhsbeWCk85MWV1/img.png?width=1200&amp;amp;height=630&amp;amp;face=581_198_680_306,https://scrap.kakaocdn.net/dn/tkYpQ/hyW23dra2K/JefPsGRO8fRSqRuHR2wev0/img.png?width=1200&amp;amp;height=630&amp;amp;face=581_198_680_306&quot;&gt;&lt;a href=&quot;https://plugins.jenkins.io/jacoco/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://plugins.jenkins.io/jacoco/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bLhnJc/hyWZdPyM25/20cSXiUOKhsbeWCk85MWV1/img.png?width=1200&amp;amp;height=630&amp;amp;face=581_198_680_306,https://scrap.kakaocdn.net/dn/tkYpQ/hyW23dra2K/JefPsGRO8fRSqRuHR2wev0/img.png?width=1200&amp;amp;height=630&amp;amp;face=581_198_680_306');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;JaCoCo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This plugin integrates &amp;lt;a href=&quot;http://www.eclemma.org/jacoco/trunk/index.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener noreferrer&quot;&amp;gt;JaCoCo code coverage reports&amp;lt;/a&amp;gt; to Jenkins.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;plugins.jenkins.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 문서에 서술된 스크립트는 아래와 같습니다. 그러면 테스트를 위한 파이프라인을 하나 만들고, 이 스크립트를 사용해 jacoco 분석 툴을 적용해보도록 하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726057978282&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;post {
    success {
        jacoco(
            execPattern: '**/build/jacoco/*.exec',
            classPattern: '**/build/classes/java/main',
            sourcePattern: '**/src/main'
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;git 주소로부터 프로젝트를 가져옵니다. (Jenkins의 Git 플러그인 사용)&lt;/li&gt;
&lt;li&gt;그리고 Gradle 명령어를 사용해 &lt;b&gt;jacocoTestReport Task&lt;/b&gt;를 실행해줍니다. 위에 설명된 jacoco의 애플리케이션 설정을 보면 &lt;b&gt;dependsOn test&lt;/b&gt;가 걸려 있기 때문에 테스트가 된 후, jacoco의 리포트가 생성됩니다.&lt;/li&gt;
&lt;li&gt;테스트가 실패했을 때도 분석 결과를 보기 위해&amp;nbsp;always로 적어줬습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726058241739&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
  agent any

  stages {
    stage('Test') {
      steps {
        git branch: '${SOURCE_BRANCH}', credentialsId: 'credentialsId', url: 'git주소'
        sh './gradlew jacocoTestReport'
      }
    }

    post {
      always {
        jacoco(
          execPattern: '**/build/jacoco/*.exec',
          classPattern: '**/build/classes/java/main',
          sourcePattern: '**/src/main'
        )
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xixtg/btsJzeAq3Wb/hsTpGcg84tSa8L8q8pxuK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xixtg/btsJzeAq3Wb/hsTpGcg84tSa8L8q8pxuK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xixtg/btsJzeAq3Wb/hsTpGcg84tSa8L8q8pxuK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXixtg%2FbtsJzeAq3Wb%2FhsTpGcg84tSa8L8q8pxuK0%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; alt=&quot;jenkins jacoco graph&quot; loading=&quot;lazy&quot; width=&quot;478&quot; height=&quot;511&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;1164&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 상세 페이지에 들어가면 &lt;b&gt;Overall Coverage Summary&lt;/b&gt;를 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Test Result&lt;/b&gt;를 클릭하면 각 테스트에 대한 내용을 볼 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8uskM/btsJy4rli8A/uZUXTGtJwK6UooPQZUdlG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8uskM/btsJy4rli8A/uZUXTGtJwK6UooPQZUdlG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8uskM/btsJy4rli8A/uZUXTGtJwK6UooPQZUdlG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8uskM%2FbtsJy4rli8A%2FuZUXTGtJwK6UooPQZUdlG0%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; alt=&quot;build page&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;1102&quot; data-origin-width=&quot;1944&quot; data-origin-height=&quot;1102&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Blue Ocean 연동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins의 플러그인 중 하나인 Blue Ocean은 강력한 Visualization을 제공합니다. Jenkins 스크립트에 한 줄만 추가해줘도, Jacoco로 만든 테스트 리포트를 Blue Ocean에서 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 jacoco 리포트(XML 파일)을 전달하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726059061832&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;post {
  always {
    junit '**/build/test-results/test/*.xml'
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파이프라인의 테스트를 확인해보면 아래와 같이 Blue Ocean UI에서도 Jacoco 테스트 리포트가 적용된 것을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJzdyP/btsJz3Sh3SJ/hQHZuxM551bKuqABLXUm1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJzdyP/btsJz3Sh3SJ/hQHZuxM551bKuqABLXUm1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJzdyP/btsJz3Sh3SJ/hQHZuxM551bKuqABLXUm1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJzdyP%2FbtsJz3Sh3SJ%2FhQHZuxM551bKuqABLXUm1k%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; alt=&quot;blue ocean pipeline page&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;277&quot; data-origin-width=&quot;2012&quot; data-origin-height=&quot;772&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra/Jenkins</category>
      <category>Blue Ocean</category>
      <category>CI/CD</category>
      <category>jacoco</category>
      <category>jacoco 연동</category>
      <category>jenkins</category>
      <category>jenkins plugin</category>
      <category>jenkins 연동</category>
      <category>정적 분석 툴</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/237</guid>
      <comments>https://myvelop.tistory.com/237#entry237comment</comments>
      <pubDate>Wed, 11 Sep 2024 22:02:26 +0900</pubDate>
    </item>
    <item>
      <title>인프콘 2024 후기</title>
      <link>https://myvelop.tistory.com/236</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2380&quot; data-origin-height=&quot;1548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUPtAB/btsI6TQpLrm/cBV7FMPJBZMzox7A0VgUAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUPtAB/btsI6TQpLrm/cBV7FMPJBZMzox7A0VgUAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUPtAB/btsI6TQpLrm/cBV7FMPJBZMzox7A0VgUAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUPtAB%2FbtsI6TQpLrm%2FcBV7FMPJBZMzox7A0VgUAk%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; alt=&quot;infcon 2024&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;1548&quot; data-origin-width=&quot;2380&quot; data-origin-height=&quot;1548&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;올해도 작년과 마찬가지로 인프콘 티켓팅에 실패했다. 하지만!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mgBQZ/btsI4Yr6P03/xaEHSTMAVZFbI9rrYVdxZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mgBQZ/btsI4Yr6P03/xaEHSTMAVZFbI9rrYVdxZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mgBQZ/btsI4Yr6P03/xaEHSTMAVZFbI9rrYVdxZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmgBQZ%2FbtsI4Yr6P03%2FxaEHSTMAVZFbI9rrYVdxZK%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; alt=&quot;인프콘 티켓팅 실패&quot; loading=&quot;lazy&quot; width=&quot;627&quot; height=&quot;502&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;960&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;&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;&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.&amp;nbsp; 부스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 기업 부스와 인프런의 자체 부스가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 인프콘의 목적은 발표세션 최대한 많이 듣기였기 때문에 세션이 시작되기 전, 잠깐 부스 2~3곳에 들러 티셔츠나 스티커, 손풍기 등의 선물을 받았다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 딥다이브 세션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2층의 강의실에서 딥다이브 세션이 진행되었다. 원래 이런 세션이 있는 줄도 몰랐는데 인프콘에 같이 온 회사 동료가 소개해줘 알게 되었다. 마침 우리 회사 내에서 제일 큰 화두가 기존 레거시 프로젝트의 개선이었는데 고민에 도움이 될 것 같아 참여를 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 12:20~13:40로 진행되었기 때문에 간단히 요기를 할 수 있는 샌드위치를 나눠주셨다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 10년 된 레거시 PHP 모노리스 갈아엎은 후기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트원의 장두호 CTO님이 3년 간의 레거시 프로젝트 개선 과정을 발표해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;레거시라고 해서 무조건 나쁜 것은 아니다. 하지만 레거시가 성장의 병목이 될 때는 근본적인 개선이 필요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 레거시를 나쁜 것이라고 동일시하여 봤던 적이 있었다. 내가 처음 입사하고 1년 동안 해왔던 작업들은 레거시를 유지보수하는 일이었다. 그래도 3년 정도밖에 진행하지 않은 프로젝트(기술 스택은 Spring Boot v2.3에 MyBatis)였기 때문에 그렇게 엄청난 레거시는 아니었다. 하지만 'JPA를 쓰지 않기 때문에', '스프링을 제대로 사용하지 않아서' 등의 이유로 그냥 왠지 모르게 마음에 들지 않았던 것 같다.&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;그러다가 2013년부터 유지보수되지 않은 레거시 끝판왕 쇼핑몰 프로그램을 맡았던 적이 있었다. 스트레스가 이만저만이 아니었다. 외주 업체에서 일정을 맞추기 위해 아무런 고민 없이 작성한 코드 + 내가 알지 못하는 언어 + 버전 관리 툴 미사용 등등의 요인 때문에 더 힘들었다. 이런 극단적인 상황에서는 당연히 근본적인 개선이 요구되었다. 2개월에 걸쳐 새로운 기술 스택을 사용한 프로젝트로 갈아 엎는 작업을 하면서 느낀 점은 기존 레거시를 갈아 엎는다는 게 쉬운 일은 아니라는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 비즈니스 로직이 잘 돌아가는 것은 물론 앞으로의 기능 추가를 위해 코드의 확장성 또한 고려해야 했다. 그 길 위에는 수많은 버그가 깔려 있었다. 결국 2개월 동안 내리 야근을 한 끝에 결국 프로젝트를 성공적으로 끝마쳤지만, 그 과정이 너무 힘들었기 때문에 &quot;정말 필요한 게 아니라면 레거시를 뒤엎는 모험은 함부로 하는 게 아니구나&quot;라는 걸 느꼈다.&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;그런데 여기서 &quot;근본적인 개선이 정말 필요하다&quot;라는 시점이 구체적으로 어떤 때일까? 생각해봤을 때 한마디로 표현하기 어려웠는데 딥다이브 세션에서 &quot;레거시가 성장의 병목이 될 때&quot;라는 얘기가 와닿았다. 그렇다. 프로젝트가 더 이상 앞으로 나아가기 판단되면 그 때는 근본적인 개선을 하는 게 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;our temporary solution to a temporary problem has become a permanent problem. &lt;br /&gt;- Naval Ravikant&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 회사의 초점은 &quot;속도&quot;에 맞춰져 있었다. 물론 스타트업에서 속도는 굉장히 중요하다. 짧은 시간 안에 더 많은 시도를 하고 더 많은 피드백을 받아야 비즈니스 완성도가 높아진다.(애자일의 철학과 상통하는 부분) 하지만 그렇게 &quot;속도&quot;만 바라보고 기능이 추가되다 보니 문제가 발생하기 시작했다. 어느 순간부터 버그가 양산되었고, 오히려 나아가는 속도가 느려지기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동료들은 배포에 더 많은 피로감을 느끼게 되었다. &quot;어? 이거 레거시가 성장의 병목이 될 때라고 표현한 게 이런 걸 얘기하는 게 아닐까?&quot;라는 생각이 들었다.&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;팀에서 현재 코드베이스를 개선하는 방법이 논의되고 있다. 하지만 &quot;임시적인 해결책&quot;이 될 가능성이 높다고 생각한다. &quot;최악의 코드를 차악으로 변경하는&quot; 작업이기 때문이다. 그런 개선으로 잠시 좋아질 수는 있겠지만 결국 &quot;임시 해결책&quot;은 또 다른 문제를 만들어내게 된다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;모놀리스에서 점차 MSA로 발전해나가는 것이 좋다고 생각합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스를 도입하기 전에는 다양한 요인을 고려하고, 도입한다면 왜 그래야 하는지 이유가 명확해야 한다. 엔지니어의 수는 어떻게 되는지, 기술적인 제약이 있는지, 데이터 등 많은 조건을 고려해봐야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v2를 한다고 해서 꼭! 마이크로서비스를 도입하는 방향으로 개선하지 않아도 된다. 오히려 도입이 'microservice tax'로 이어질 수도 있다. 따라서 모놀리스로 시작해 점차 MSA로 발전해나가는 방향도 염두에 두자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;레거시는 어떻게 유지보수하셨나요? =&amp;gt; 구버전과 신버전 팀.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 고객의 요구사항은 계속 들어온다. 결국 레거시를 유지보수해야 하고, v2는 v2대로 앞으로 나아가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;외부에서 바라볼 때는 시스템 개선 없이 계류되는 것으로 보일 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 개선은 상당히 긴 호흡으로 진행되는 작업이다. 포트원에서도 3년이라는 시간이 지났음에도 레거시가 아직 남아있다고 한다. &quot;끝이 없는 작업&quot;이라는 말씀까지 하셨다. 결국 레거시를 개선하는 문제는 이해당사자를 잘 설득하는 문제로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;결국 어필을 잘 해야 한다. 생산성이 얼마나 올랐는지, 장애 지표가 얼마나 좋아졌는지 등의 수치를 통해 증명해야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 발표 세션&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmAK4/btsI4ZqZlRM/uoyhzdEopXMnXiK8k5Snm1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmAK4/btsI4ZqZlRM/uoyhzdEopXMnXiK8k5Snm1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmAK4/btsI4ZqZlRM/uoyhzdEopXMnXiK8k5Snm1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlmAK4%2FbtsI4ZqZlRM%2FuoyhzdEopXMnXiK8k5Snm1%2Fimg.jpg&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; alt=&quot; 발표 세션&quot; loading=&quot;lazy&quot; width=&quot;654&quot; height=&quot;490&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&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;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 지속 성장 가능한 설계를 만들어가는 법&lt;/h3&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;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;설계를 잘하는 법은 설계를 하지 않는 것이다.&quot;&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 기능을 추가할 때 설계가 빈약한 것이 약점이라고 생각했다. 옆에서 동료는 도메인 모델링도 휙휙 하고 코드를 작성하기 전에 고민을 잘하고 구현에 돌입하는 것 같았다. 최근에서야 &quot;오브젝트&quot;라는 책을 읽고 간단하게 메시지 중심의 객체 설계라도 조금씩 하기 시작했다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구현할 때 2가지만 사용하면 됩니다. 개념과 격벽&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나와 다른 점이 있다면 나는 그냥 구현에 들어가는 반면, 발표자님은 &quot;개념&quot;과 &quot;격벽&quot;이라는 도구를 사용했다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 말하는 &quot;개념&quot; 도메인 객체와 동일하다고 봐도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념을 생각하다보면 자연스럽게 그룹화를 하게 되는데 구분 없이 그룹화를 하면 오히려 장황해보일 수 있기 때문에 &quot;격벽&quot;이라는 것을 개념 사이에 세워 개념 간의 1차적 분리를 시도해볼 수 있다. &quot;격벽&quot;을 통해 개념의 흐름을 통제할 수 있게 된다.&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;패키지와 접근 제한자를 잘 활용해보면 &quot;격벽&quot;이라는 것을 효과적으로 만들 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;요구사항은 계속 변합니다. 그래서 항상 준비해야 합니다. &lt;br /&gt;요구사항은 계속 변하기 때문에 완벽한 설계란 환상입니다. 완벽한 설계가 있다면 그건 발전 없고 죽어가는 서비스나 가능한 얘기입니다.&lt;br /&gt;소프트웨어는 soft해야 합니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그러니 이런 말은 하지 맙시다.&lt;br /&gt;1. 요구사항이 완벽해야 설계 가능해요. &lt;br /&gt;2. 우리 설계에서 그건 개발 못해요. &lt;br /&gt;3. 설계해 봐야 개발 일정이 나옵니다.&lt;br /&gt;이런 말들은 설계를 잘못 해놓고 하는 말입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그 요구사항 수용할 수 없어요.&quot; 회사에서 일을 하다보면 이런 얘기가 종종 들려온다.&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;소프트웨어는 문제를 해결하기 위해 존재하는 것이고 어떻게든 방법이 있다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;설계를 성급하게 하면 변화에 취약해집니다. 과도한 설계는 모든 것을 망가트려요. 설계는 필요한 만큼만 하는 게 좋습니다.&lt;/blockquote&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;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;blockquote data-ke-style=&quot;style2&quot;&gt;그냥 즉시 구현을 했으면 좋겠습니다. 증명하고 피드백 하세요. 테스트 코드로 반복할 수 있는 환경을 만들어 놓고 계속 반복하세요. 테스트 코드와 코드 작성을 반복하다보면 설계가 자연스럽게 만들어지게 됩니다. 결국 검증이 완벽한 설계를 위한 길입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 떠오른 것은 TDD다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;테스트로 요구사항을 세팅해놓고 요구사항에 맞게 돌아가는 코드를 만든다&quot;라는 철학은 같았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 혹시 당신은 데이터를 모르는 백엔드 개발자 인가요?&lt;/h3&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;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;blockquote data-ke-style=&quot;style2&quot;&gt;백엔드 엔지니어는 행을 잘 읽고 쓰는 것이 중요합니다. 반면, 데이터 엔지니어는 어떨까요? 열을 잘 읽고 쓰는 것이 중요합니다.&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;예를 들어 테이블의 column에 json이나 array로 들어가 있다고 해보자. 백엔드 개발자 입장에서는 행 단위로 처리될 때 json과 array 타입의 처리가 간편하다. 하지만 해당 컬럼에 데이터 엔지니어링 요구사항이 들어오게 되면 어떻게 될까? 1억 7천만 row의 json과 배열을 모두 스캔해야 되는 상황인데 그게 가능할까? 열 단위 분석 요청의 비용이 너무 많이 들기 때문에 불가능할 것이다.&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;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;blockquote data-ke-style=&quot;style2&quot;&gt;설계 맥락은 항상 중요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제방식에 따라 테이블에 컬럼을 추가하는 식으로 개발할 수도 있다. 그런데 테이블에 comment가 없다? 설계 맥락을 파악하고 싶어도 이 테이블을 만든 장본인은 이미 퇴사하고 없을 수도 있다. 이는 생산성을 떨어뜨리는 주범이다.&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;따라서 문서가 파편화되지 않도록 관리 필요하다. 데이터 카탈로그를 활용해 볼 수 있다.(dataHub라는 라이브러리) 카탈로그 시스템이 구축되어 있지 않다면 최소한 스키마에 코멘트라도 자세히 달아주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;백엔드 엔지니어는 장애 없이 빠르게 대용량 트래픽을 처리하느냐가 중요하지만, 데이터 엔지니어는 대용량 데이터 관리가 중요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 인프라와 마찬가지로 데이터도 분산처리가 필수다. 1개의 테이블에 수십억의 데이터가 존재할 수도 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터는 DB 데이터 관리가 전부가 아니다. Spark나 Hadoop과 같은 시스템을 사용하면 거기에 있는 데이터도 관리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그냥 모두 저장하는 것이 비즈니스를 지키는 가장 좋은 방법이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쇼핑몰 레거시에서 deleted_at을 찍는 게 아니라 아예 물리 삭제하는 로직으로 도배되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 남아있지 않았기 때문에 CS 대응할 때 굉장히 불편했던 기억이 있다. DB 데이터로 확인할 수 없었기 때문에 일일이 로그를 찾아 헤맸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;하나의 컬럼에 하나의 맥락이 들어가 있어야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부끄러운 과거지만, 하나의 컬럼(시간 관련 컬럼)을 2개의 맥락(예약 신청에 관한 2개의 맥락)에서 사용하는 테이블을 설계한 적이 있다. 이렇게 설계된 테이블은 해당 데이터의 맥락을 정확히 파악할 수 없게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;좋은 서비스를 만들기 위해서라도 데이터에 대해 잘 알아야 합니다. 회사는 데이터를 가지고 의사결정을 진행합니다. 잘못된 데이터로 의사결정하면 손해가 발생하게 되니 데이터를 잘 관리하는 것은 필수입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 클린 스프링: 스프링 개발자를 위한 클린 코드 전략&lt;/h3&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;blockquote data-ke-style=&quot;style2&quot;&gt;경력이 쌓이면서 개발자는 점점 더 많은 고민거리를 가지게 되어 구현 속도가 느려진다?&lt;/blockquote&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;클린 코드를 추구해서 주석은 작성하지 않습니다. &lt;br /&gt;코드가 클린하면 리팩토링할 필요가 없죠. &lt;br /&gt;클린 코드는 버그가 적어서 테스트 코드가 없어도 되지 않나요? &lt;br /&gt;클린 코드를 작성해야 해서 일정을 지킬 수 없습니다. &lt;br /&gt;클린 코드 원칙에 위배되어서 리뷰를 승인할 수 없습니다.&lt;br /&gt;&lt;br /&gt;'사람들이 클린 코드에 대해 오해하고 있구나', '사람들이 원칙만 추구하는구나'라는 생각이 들었습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; 익숙한 기술&lt;/b&gt;로 &lt;br /&gt;&lt;b&gt;핵심 기능&lt;/b&gt;이 &lt;br /&gt;&lt;b&gt;동작&lt;/b&gt;하는 &lt;br /&gt;&lt;b&gt;가장 단순한 코드&lt;/b&gt;를 &lt;br /&gt;&lt;b&gt;리팩터링 하기 좋게&lt;/b&gt; 작성&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시간이 없어서 테스트를 못한다?&lt;br /&gt;회사에서 테스트를 하지 말라고 하면 어떻게 해야 하나요?&lt;/blockquote&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;span style=&quot;color: #333333; text-align: left;&quot;&gt;토비 님은 위와 같은 질문에 이렇게 답변하셨다고 한다. &quot;그럼 테스트를 빨리 만들면 되지 않아요??&quot; &lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;코드를 다루는 문제는 &lt;b&gt;유지보수성&lt;/b&gt;, &lt;b&gt;생산성&lt;/b&gt;, &lt;b&gt;팀워크&lt;/b&gt; 삼체의 문제입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;교양 있는 개발자.&lt;br /&gt;자신의 말을 하더라도 다른 사람의 기분을 나쁘지 않게 하는 것 말하지 않는 것이 교양있는 것이 아닙니다. 부정적인 피드백이라도 필요하다면 해야 합니다. 교양이 저절로 생기는 것은 아니기 때문에 훈련이 필요한 영역이에요. 항상 친절하세요. 동료에게, 자신에게요. &lt;br /&gt;예를 들어, 클린 코드는 내가 행할 수 있는 친절 중 하나입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. 객체지향은 여전히 유용한가?&lt;/h3&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;실제 코드 예시를 보여주시면서 발표 세션을 진행하셨다. 예시의 내용과 그 내용을 분석한 내용은 조영호 님이 직접 쓰신 책인 &quot;오브젝트&quot;와 거의 비슷했다. 나는 책의 내용을 복습하는 느낌으로 강의를 들었던 것 같다. (만약 객체지향에 입문하는 분이라면 꼭 한 번 들었으면 좋겠다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;사실 강의 제목은 &lt;b&gt;객체지향은 여전히 유용한가?&lt;/b&gt;로 지었지만 사실 올바른 질문은 &quot;&lt;b&gt;객체 지향은 언제 유용한가요?&lt;/b&gt;&quot;라고 생각합니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;만일 객체지향이 유용해 보이지 않는 개발자 분들이 있다면 그건 현재 하는 일의 맥락 때문일 겁니다. 나중에 시스템이 굉장히 복잡해졌을 때 객체지향이 도움이 될 수 있기 때문에 미리 공부해두는 것이 좋다고 생각합니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;언제 유용한 지 항상 고민합시다.&lt;br /&gt;그냥 도구 하나 배운다고 생각하는 것이 좋습니다. 이 툴이 언제 유용한지를 머리 속에 넣어두고 나중에 사용하면 됩니다. 코드를 만들 때 어떤 기술이나 패러다임이 유용한지를 질문하고 고민합시다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;코드의 목적과 변경의 방향성에 따라 언제 어떤 기술을 사용할지 결정하세요 주니어 시기에 배워야 할 게 정말 많을 겁니다. 취사선택하고 싶은 마음도 알지만 그냥 배우고 고민하면 좋을 것 같습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 질의응답&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강연자 님들과 질의 응답을 할 수 있는 장소가 2층에 마련되어 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조영호 님의 &quot;오브젝트&quot;를 보면서 스터디를 진행하는 중이라 조영호 님에 대한 호기심이 증폭되어 있는 상태였고, 질의응답에 참여하면서 객체지향에 대한 인사이트를 좀 더 알아가고 싶었다. 질의 응답은 40분 동안 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 다형성에 대한 질문&lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 타입이 확장된다는 전제가 있어야 합니다. 다형성이 if을 객체로 바꾸는 것이 아닙니다. (코드를 변경함으로써) 고통스러운 순간이 올 때 바꾸는 게 맞습니다. 절차적인 설계의 단점은 룰이 없다는 것입니다. 그런 단점에도 불구하고 다형성이 있는 게 무조건 좋은 설계가 아닙니다. 추상화는 비용이 매우 비싸기 때문에 그 선택을 하는 타당한 이유가 있어야 합니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 함수형 프로그래밍에 대해 어떻게 생각하시는지&lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 함수형은 좋습니다. 유연합니다. 객체지향보다 조합하기 훨씬 좋다고 생각해요. 함수형은 좋지만 훨씬 복잡해요. 큰 구조는 객체지향으로 가져가고, 객체의 행위를 바꾸는 작은 단위들. 유연성이 필요한 부분들. 그 부분들을 함수형으로 짜는 걸 추천합니다. &lt;br /&gt;함수형은 어렵다는 게 단점입니다. 함수의 단위가 작기 때문에 컨트롤하기가 어렵습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 외부 연동사를 사용할 때, 인터페이스를 사용한 추상화에 대하여 (현재 외부 연동사가 1개 있고, 새롭게 추가될 가능성이 있어서 추상화를 진행해야 하는지가 질문이었다.)&lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 될지 안될지 모른다면 그럼 안하는 게 맞습니다. 런타임에 기대되는 동작이 하나만 있는데 추상화되었는 코드를 보면 굉장히 당황스럽습니다. 변경하기 쉬운 설계는 인터페이스를 사용하는 게 아닙니다. 단순한 코드가 변경하기 쉬운 코드가 좋은 설계입니다.&lt;br /&gt;A, B, C가 들어왔더니 중복이 있다? 그러면 그 때 추상화하는 것이 맞습니다. 처음부터 PG사의 요구사항을 알 수 없는데 그걸 공통화한다는 게 쉽지 않습니다. 추상화는 최대한 미루는 것이 맞습니다. 바뀌지 않는데 다형성을 사용하는 건 낭비입니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 토비님은 계층 간에 인터페이스를 무조건 두는 것을 얘기하셨습니다. 여기에 대해 조영호 님은 어떻게 생각하시는지?&lt;br /&gt;&lt;b&gt;A.&lt;/b&gt;  제가 발표에서 진행한 내용은 유연성을 부여하기 위함이고 토비 님이 말씀하신 스프링의 인터페이스는 (강결합을 끊고 싶기 때문에) 디커플링을 하는 것이 목적입니다. 레포지토리 쪽은 인터페이스를 도입하는 것이 맞습니다. 외부의 요소를 사용하고 싶다면 인터페이스로 끊어주는 것이 맞습니다. 런타임에 뒤에 있는 모든 의존성을 다 가져오기 때문입니다. 내가 의존하고 있는 게 시스템 밖에 있는 것이라면 인터페이스를 사용해야 testability가 높아집니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q.&lt;/b&gt; 그렇다면 서비스 레이어의 디펜던시를 끊는 게 맞나요? &lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 지금까지도 논쟁이 있는 부분 설계적으로 이상적으로 보이지만 그냥 통합테스트 하더라도 인터페이스 정의하지 않는 게 나은 것 같습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 팀원들이 잘 이해할 수 있는 설계를 위해 객체지향을 사용해야 하는데? 현재 절차지향적인 코드가 만연해서 팀원들에게 익숙합니다. 현재 딜레마에 빠져 있는데요. 어떻게 하는 게 좋을까요?&lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 절차적으로 짜는 게 좋을 것 같습니다. 절차적이던 객체지향적이던 스타일이 다 다르면 조직에게 어렵습니다. 동료들이 같은 컨벤션으로 코드를 짜는 게 좋습니다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;Q.&lt;/b&gt; 설득을 하고 싶다면? &lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 굉장히 힘든 길이 될 거에요. 객체지향으로 짜는 게 왜 좋은지를 설명해야 합니다. 그런데 이것이 설득되려면 듣는 분들도 학습이 되어 있어야 합니다. 회사의 상황은 책과는 다릅니다. &amp;ldquo;책에서 이게 좋대요&amp;rdquo;보다는 현재 코드와 도메인을 잘 파악하고 독립적으로 할 수 있는 부분을 병합하는 것이 좋지 않을까? 생각합니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 조영호님은 새로운 패러다임을 학습할 때 어떻게 하시나요?&lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 패러다임을 공부할 때, 저는 패러다임 자체를 공부하지 않고 언어나 프레임워크를 공부합니다. 그냥 코드를 만들어봅니다. 예전에 만들어놓은 코드를 그대로 가져와서 새로운 언어나 프레임워크로 새롭게 만들어봅니다. 학습할 때 가장 좋은 방법은 동료들과 얘기하는 것입니다. (지금은 주변에 연차가 비슷한 동료들이 얼마 안 남았지만) 비슷한 연차의 동료 서비스 코드에 대해 얘기하면서 배우는 것이 많습니다. 그런 것이 어렵다면 책이나 강의, 자료 같은 것을 보고 습득을 하는 것이 좋지 않을까요? 맞는지 안 맞는지 모르더라도 학습을 해놓는 것이 좋습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 도메인을 나누는 기준은 무엇인가요?&lt;br /&gt;&lt;b&gt;A.&lt;/b&gt; 도메인은 논리적인 단위입니다. 시스템 단위가 아니라 풀어야 하는 문제를 풀 수 있는 논리적인 단위로 나뉘어져야 합니다. 주문과 결제는 다른 도메인이라는 것을 직관적으로 알 수 있을 겁니다. 주문과 결제가 해결하려는 문제가 각자 다르기 때문입니다. 상품도 도메인이 나뉩니다. 상품 등록 상품 대행 상품 전시 등등등 하지만 시스템의 사이즈가 작으면 다른 도메인이어도 같은 시스템 안으로 묶을 수도 있습니다. 결국 트레이드오프의 문제입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 네트워킹 세션&lt;/h2&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;내가 갔을 때는 이미 시간이 20분 밖에 남지 않아 스탠딩 테이블에 참여하긴 어려웠다. 대신 회사에서 받은 개발자 인터뷰하기 과제를 수행했다. 현재 회사에서 프론트엔드 개발자를 채용하고 있었기 때문에 총 3분의 프론트엔드 개발자를 인터뷰했다. 인터뷰를 통해 개발자들은 어떤 회사에 가고 싶어하는지, 어떤 문화를 원하는지 파악해볼 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz6mpd/btsI6RkMfgv/5CNZBnGzFzBNogmdrtu5Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz6mpd/btsI6RkMfgv/5CNZBnGzFzBNogmdrtu5Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz6mpd/btsI6RkMfgv/5CNZBnGzFzBNogmdrtu5Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz6mpd%2FbtsI6RkMfgv%2F5CNZBnGzFzBNogmdrtu5Lk%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; alt=&quot;네트워킹 세션 사진&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;1058&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&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;</description>
      <category>대외활동/컨퍼런스</category>
      <category>infcon</category>
      <category>infcon 2024</category>
      <category>발표 세션</category>
      <category>백엔드</category>
      <category>인프콘</category>
      <category>인프콘 2024</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/236</guid>
      <comments>https://myvelop.tistory.com/236#entry236comment</comments>
      <pubDate>Fri, 16 Aug 2024 17:21:21 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] 서로 다른 Step끼리 데이터 공유하기</title>
      <link>https://myvelop.tistory.com/235</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch에서 로직을 처리하다보면 첫 번째 Step에서 처리한 작업을 다음 Step에서 사용하고 싶은 요구가 생기게 된다. 보통 chunk-oriented 처리를 할 때 이런 요구가 생긴다. 쓰기 작업에서는 CompositeItemWriter와 같이 2개의 쓰기를 할 수 있는 객체가 존재하지만 Reader에는 그런 기능이 있는 객체가 없기 때문이다.&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;그런데 Job에서는 Step끼리 직접 호출하여 데이터를 주고 받을 수 있는 기능을 제공하지 않기 때문에 데이터를 공유하기 위해 우회하여 전달하는 방법을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법1. 스프링 공식 문서에서 추천하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 소개할 방법은 스프링 공식 문서에서 추천해주고 있는 방식이다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;ExecutionContextPromotionListener&lt;/b&gt;&lt;/span&gt; 객체를 사용해 StepExecutionContext를 JobExecutionContext로 승격시키는 방법이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ExecutionContext&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch에서 ExecutionContext의 종류는 크게 2가지다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;StepExecutionContext와 JobExecutionContext다. Step의&lt;span&gt;&amp;nbsp;&lt;/span&gt;ExecutionContext는 하나의&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step에만 종속된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step이 끝나면 같이 사라진다는 얘기다. Job의&lt;span&gt;&amp;nbsp;&lt;/span&gt;ExecutionContext&lt;span&gt;&amp;nbsp;&lt;/span&gt;또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;Job과 수명을 함께 한다.&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;span&gt;&amp;nbsp;&lt;/span&gt;Step이 다음 진행할&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step에게 전달하고 싶다면 어떻게 하면 될까?&lt;span&gt;&amp;nbsp;&lt;/span&gt;Job ExecutionContext에 값을 저장하면 된다. Job ExecutionContext는 모든&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step이 실행될 때까지 살아있으니깐? (그러면 ExecutionContextPromotionListener을 사용할 게 아니라 JobExecutionContext를 사용하는 게 낫지 않냐고 얘기할 수도 있지만 그러지 않는 것이 좋다. 그 이유는 뒤에서 설명하겠다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;ExecutionContextPromotionListener&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Batch가 제공하는 객체로 Step이 완료되는 시점에&lt;span&gt;&amp;nbsp;&lt;/span&gt;StepExecutionContext 중 ExecutionContextPromotionListener 설정한 키 값에 대해 Job ExecutionContext로 승격(promotion)시켜주는 구현체다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723038301759&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ExecutionContextPromotionListener promotionListener() {
  ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
  listener.setKeys(new String[] {&quot;member&quot;});
  return listener;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 먼저 Step ExecutionContext에 필요한 값을 저장하자.&lt;/h4&gt;
&lt;a id=&quot;user-content-1-먼저-step-executioncontext에-필요한-값을-저장하자&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/pythonstrup/TIL/blob/main/Backend/Spring/batch/step/execution-context-promotion-listener.md#1-%EB%A8%BC%EC%A0%80-step-executioncontext%EC%97%90-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B0%92%EC%9D%84-%EC%A0%80%EC%9E%A5%ED%95%98%EC%9E%90&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chunk-oriented로&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step을 구성하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;itemReader와&lt;span&gt;&amp;nbsp;&lt;/span&gt;itemWriter를 활용해 저장할 수도 있지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;Tasklet을 사용해 간단하게 구성해보겠다.&lt;/li&gt;
&lt;li&gt;@BeforeStep을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;Tasklet이 실행되기 전,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step Execution을 가져온다.&lt;/li&gt;
&lt;li&gt;Tasklet이 실행될 때, 필요한 값을 구성해&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step Execution에 저장하도록 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723038514779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(&quot;memberTasklet&quot;)
public Tasklet memberTasklet() {
  return new Tasklet() {

    private StepExecution stepExecution;

    @Override
    public RepeatStatus execute(
        StepContribution contribution, ChunkContext chunkContext) throws Exception {
      Member member = memberService.find();
      ExecutionContext stepContext = this.stepExecution.getExecutionContext();
      stepContext.put(&quot;member&quot;, member);
      return RepeatStatus.FINISHED;
    }

    @BeforeStep
    public void saveStepExecution(StepExecution stepExecution) {
      this.stepExecution = stepExecution;
    }
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Step을 등록할 때 주의할 점!&lt;/li&gt;
&lt;li&gt;@BeforeStep이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;@AfterStep&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션을 사용하려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step의&lt;span&gt;&amp;nbsp;&lt;/span&gt;listener()&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드를 사용해 리스너로 등록해줘야 한다!&lt;/li&gt;
&lt;li&gt;자세한 내용은&amp;nbsp;&lt;a href=&quot;https://myvelop.tistory.com/234&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;해당 링크&lt;/a&gt; 참조&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723038578795&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(&quot;memberStep&quot;)
public Step memberStep(
    final JobRepository jobRepository, final PlatformTransactionManager transactionManager) {
  return new StepBuilder(&quot;memberStep&quot;, jobRepository)
      .tasklet(memberTasklet(), transactionManager)
      .listener(memberTasklet())
      .listener(promotionListener())
      .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. ExecutionContextPromotionListener의 역할&lt;/h4&gt;
&lt;a id=&quot;user-content-2-executioncontextpromotionlistener의-역할&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/pythonstrup/TIL/blob/main/Backend/Spring/batch/step/execution-context-promotion-listener.md#2-executioncontextpromotionlistener%EC%9D%98-%EC%97%AD%ED%95%A0&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ExecutionContextPromotionListener는 다음 Step으로 전달하고 싶은 키 값을 저장해 넘겨주면 된다.&lt;/li&gt;
&lt;li&gt;Step의 listener로&lt;span&gt;&amp;nbsp;&lt;/span&gt;ExecutionContextPromotionListener를 등록해주면 된다.&lt;/li&gt;
&lt;li&gt;이제 Step이 종료되면 자동으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;Job Execution에 구성한 값이 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723038565868&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ExecutionContextPromotionListener promotionListener() {
  ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
  listener.setKeys(new String[] {&quot;member&quot;});
  return listener;
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 값 꺼내서 사용하기&lt;/h4&gt;
&lt;a id=&quot;user-content-3-값-꺼내서-사용하기&quot; style=&quot;color: #000000;&quot; href=&quot;https://github.com/pythonstrup/TIL/blob/main/Backend/Spring/batch/step/execution-context-promotion-listener.md#3-%EA%B0%92-%EA%BA%BC%EB%82%B4%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot;&gt;&lt;/a&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 다음&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step의 ItemWriter에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;JobExecution을 사용해 컨텍스트 값을 가져올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723038546257&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(&quot;orderWriter&quot;)
public ItemWriter&amp;lt;Order&amp;gt; orderWriter() {

  return new ItemWriter&amp;lt;Order&amp;gt;() {
    private Member member;

    @Override
    public void write(final Chunk&amp;lt;? extends Order&amp;gt; chunk) throws Exception {
      orderService.registerOrder(this.member, chunk);
    }

    @BeforeStep
    public void retrieveInterStepData(StepExecution stepExecution) {
      final JobExecution jobExecution = stepExecution.getJobExecution();
      final ExecutionContext jobContext = jobExecution.getExecutionContext();
      this.member = (Member) jobContext.get(&quot;member&quot;);
    }
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법2. JobExecution에 저장하기&lt;/h2&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;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;Step 실행 중 JobExecutionContext에 값을 저장하더라도 Step이 실패하는 경우 데이터가 유실될 수 있다.&lt;/li&gt;
&lt;li&gt;이 방법은 Step이 JobExecutionContext에 강결합하게 된다. 따라서 다른 Job에서 Step 구현체를 재사용하기 어려워진다.&lt;/li&gt;
&lt;/ol&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;간단하다. 데이터를 저장할 Step에서 JobExecutionContext에 데이터를 저장하고 다음 스텝에서 JobExecutionContext에서 데이터를 가져와 사용하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723039266915&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Step memberStep() {
  return stepBuilderFactory.get(&quot;memberStep&quot;)
    .tasklet((contribution, chunkContext) -&amp;gt; {
      StepContext stepContext = chunkContext.getStepContext();
      JobExecution jobExecution = stepContext.getStepExecution().getJobExecution();
      Member member = memberService.getMember();
      jobExecution.getExecutionContext().put(&quot;key&quot;, member);  
      return RepeatStatus.FINISHED;
    })
    .build();
}

@Bean
public Step orderStep() {
  return stepBuilderFactory.get(&quot;orderStep&quot;)
      .tasklet((contribution, chunkContext) -&amp;gt; {
        StepContext stepContext = chunkContext.getStepContext();
        JobExecution jobExecution = stepContext.getStepExecution().getJobExecution();
        Member member = (Member) jobExecution.getExecutionContext().getString(&quot;dataKey&quot;);
        
        ...
        
        return RepeatStatus.FINISHED;
      })
      .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;방법3. Singleton Bean 사용하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ExecutionContext와 Serialize의 압박&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step 간에 데이터를 공유할 때 ExecutionContext를 사용하면 발생하는 치명적인 문제가 하나있다. 바로 ExecutionContext에 저장할&amp;nbsp; 객체는 직렬화된다는 것이다. 이게 왜 치명적인 문제냐? ExecutionContext에 데이터를 저장하면서 json string 형태로 변환하는데 이 비용이 생각보다 크다고 한다.&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;ExecutionContext에 담을 객체가 크지 않다면 별로 문제가 되지 않지만 크기가 너무 크다면 성능 이슈가 발생할 수 있다. 이 때 사용할 수 있는 것이 스레드 세이프한 자료구조를 가진 Singleton Bean이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 예시&lt;/h3&gt;
&lt;pre id=&quot;code_1723041065567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class SharedData&amp;lt;T&amp;gt; {

  private final Map&amp;lt;String, T&amp;gt; values = new ConcurrentHashMap&amp;lt;&amp;gt;();

  public void putData(String key, T data) {
    values.put(key, data);
  }

  public T getData (String key) {
    return values.get(key);
  }

  public int getSize () {
    return values.size();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스레드 세이프한 자료구조를 사용하지 않을 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk-Oriented 지향 처리 중 Singleton Bean의 데이터가 오염되어 작업이 망가질 가능성이 있다.&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;예를 들어 단체 문자를 발송하는 Schduler Job이 있다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업이 실행되면 20분 이상 소요되지만 단체 문자를 발송하는 일이 자주 있지는 않다. 그래도 단체 문자를 등록하면 최대한 빨리 발송하는 것이 좋기 때문에 5분마다 작업이 있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 요청이 들어왔다. Job은 단체 발송 작업이 들어온 것을 캐치해 발송 내용을 Singleton Bean에 담고 다음 스텝에서 chunk-oriented 지향 처리를 통해 순차적으로 문자를 발송하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 작업이 끝나지 않았는데 다음 단체 발송 문제 요청이 연달아 들어왔다. 두 번째 Job은 첫 번째 Job이 사용하고 있던 Singleton Bean에 새로운 발송 내용을 담게될 것이다. Singleton Bean이 오염되었기 때문에 첫 번째 작업은 그 뒤부터는 두 번째 작업과 동일한 문자를 보내게 된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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.spring.io/spring-batch/reference/common-patterns.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 공식문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wckhg89.github.io/archivers/springbatch3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링 배치(스프링 Boot 기반) 삽질기 3탄 - 싱글톤 빈을 이용한 step간 데이터 공유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@umutaskin/how-to-pass-data-among-steps-in-a-spring-batch-application-3568ae6f63d6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;How to Pass Data Among Steps in a Spring Batch Application&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/235</guid>
      <comments>https://myvelop.tistory.com/235#entry235comment</comments>
      <pubDate>Wed, 7 Aug 2024 23:32:23 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] Tasklet에서 왜 @BeforeStep과 @AfterStep이 동작하지 않을까?</title>
      <link>https://myvelop.tistory.com/234</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Tasklet만으로는 beforeStep이나 afterStep을 트리거하지 못한다.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tasklet을 구현하고 StepBuilder에서 tasklet() 메소드를 등록해주는 것만으로는 @BeforeStep이나 @AfterStep을 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;Tasklet에서 Step의 생명주기에 관여하고 싶다면 추가적인 작업이 필요하다는 말이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722519463930&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class MemberJobConfig {

  @Bean(&quot;memberJob&quot;)
  public Job memberJob(
      JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new JobBuilder(&quot;memberJob&quot;, jobRepository)
        .start(memberStep(jobRepository, transactionManager))
        .build();
  }

  @Bean(&quot;memberStep&quot;)
  public Step memberStep(
      JobRepository jobRepository, PlatformTransactionManager transactionManager) {
    return new StepBuilder(&quot;memberStep&quot;, jobRepository)
        .tasklet(sendingItemScheduleAlarmTasklet(), transactionManager)
        .build();
  }

  @Bean(&quot;memberTasklet&quot;)
  public Tasklet memberTasklet() {
    return new Tasklet() {
      @Override
      public RepeatStatus execute(
          StepContribution contribution, ChunkContext chunkContext) throws Exception {/** 로직 */}

      @BeforeStep
      public void beforeStep(StepExecution stepExecution) {/** 실행되지 않는다. */}

      @AfterStep
      public ExitStatus afterStep() {/** 실행되지 않는다. */}
    };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Tasklet이 Step의 생명주기에 관여하는 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. StepExecutionListener를 구현한다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tasklet과 함께 StepExecutionListener를 구현해주면 된다.&lt;/li&gt;
&lt;li&gt;beforeStep() 메소드와 afterStep() 메소드를 오버라이드를 해서 작성하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;pre id=&quot;code_1722519669603&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class MemberTasklet implements Tasklet, StepExecutionListener {

  @Override
  public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext)
      throws Exception {
    return RepeatStatus.FINISHED;
  }

  @Override
  public void beforeStep(final StepExecution stepExecution) {
    // 실행
  }

  @Override
  public ExitStatus afterStep() {
    // 실행
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 tasklet() 메소드를 사용해 등록만 해줘도 Spring Batch 내부적으로 Tasklet으로 등록한 StepExecutionListener 구현체의 beforeStep(), afterStep() 메소드를 실행해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722519746483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(&quot;memberStep&quot;)
public Step memberStep(
    final JobRepository jobRepository, final PlatformTransactionManager transactionManager) {
  return new StepBuilder(&quot;myStep&quot;, jobRepository)
      .tasklet(memberTasklet, transactionManager)
      .build();
}&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;&lt;span&gt;단&lt;/span&gt;, StepExecutionListener&lt;span&gt;을&lt;/span&gt; &lt;span&gt;구현해도&lt;/span&gt;&amp;nbsp;@BeforeStep, @AfterStep&lt;span&gt;이&lt;/span&gt; &lt;span&gt;트리거되지&lt;/span&gt; &lt;span&gt;않는다는&lt;/span&gt; &lt;span&gt;사실은&lt;/span&gt; &lt;span&gt;주의하자&lt;/span&gt;.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722519792178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class MemberTasklet implements Tasklet, StepExecutionListener {

  @Override
  public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext)
      throws Exception {/** 로직 */}

  @BeforeStep
  public void findMember(final StepExecution stepExecution) {/** 실행되지 않는다! */}

  @AfterStep
  public ExitStatus writeMember() {/** 실행되지 않는다! */}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Tasklet을 StepListener로 등록한다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 StepBuilder의 listener() 메소드를 사용해 Tasklet를 리스너로 등록해주면 @BeforeStep과 @AfterStep이 트리거된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722519960533&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(&quot;memberStep&quot;)
public Step memberStep(
    JobRepository jobRepository, PlatformTransactionManager transactionManager) {
  return new StepBuilder(&quot;memberStep&quot;, jobRepository)
      .tasklet(memberTasklet(), transactionManager)
      .listener(memberTasklet()) // 리스너로 등록해줘도 트리거된다!!
      .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 어떻게 동작하길래 리스너로 등록해도 사용이 가능한 걸까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이 StepBuilder에서 listener() 메소드를 호출하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;AbstractTaskletStepBuilder에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;super.listener(listener)를 호출해 리스너를 추가해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722520084154&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractTaskletStepBuilder&amp;lt;B extends AbstractTaskletStepBuilder&amp;lt;B&amp;gt;&amp;gt; extends StepBuilderHelper&amp;lt;B&amp;gt; {
  ...
  
  public B listener(Object listener) {
	super.listener(listener);

	Set&amp;lt;Method&amp;gt; chunkListenerMethods = new HashSet&amp;lt;&amp;gt;();
	chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeChunk.class));
	chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunk.class));
	chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunkError.class));

	if (chunkListenerMethods.size() &amp;gt; 0) {
		StepListenerFactoryBean factory = new StepListenerFactoryBean();
		factory.setDelegate(listener);
		this.listener((ChunkListener) factory.getObject());
	}

	return self();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StepBuilderHelper에서는 @BeforeStep과 @AfterStep 어노테이션이 달려있는 메소드를 탐색해 StepListenerFactoryBean이라는 객체를 사용해 프록시로 감싸준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722520238344&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class StepBuilderHelper&amp;lt;B extends StepBuilderHelper&amp;lt;B&amp;gt;&amp;gt; {

  ...

  public B listener(Object listener) {
    Set&amp;lt;Method&amp;gt; stepExecutionListenerMethods = new HashSet&amp;lt;&amp;gt;();
    stepExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), BeforeStep.class));
    stepExecutionListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterStep.class));

    if (stepExecutionListenerMethods.size() &amp;gt; 0) {
        StepListenerFactoryBean factory = new StepListenerFactoryBean();
        factory.setDelegate(listener);
        properties.addStepExecutionListener((StepExecutionListener) factory.getObject());
    }

    return self();
  }
  
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getObject()를 실행할 때 nvokers에는 현재 리스너를 실행시키고 있는 컨텍스트에 대한 정보와 내가&lt;span&gt;&amp;nbsp;&lt;/span&gt;@BeforeStep나&lt;span&gt;&amp;nbsp;&lt;/span&gt;@AfterStep을 달았던 메소드 이름이 담긴다.&lt;/li&gt;
&lt;li&gt;나중에 StepListener의&lt;span&gt;&amp;nbsp;&lt;/span&gt;beforeStep이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;afterStep이 실행될 때, 프록시를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;@BeforeStep나&lt;span&gt;&amp;nbsp;&lt;/span&gt;@AfterStep가 달려있는 메소드를 실행해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1722520434539&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractListenerFactoryBean&amp;lt;T&amp;gt; implements FactoryBean&amp;lt;Object&amp;gt;, InitializingBean {

  @Override
  public Object getObject() {
    ...

    Set&amp;lt;Class&amp;lt;?&amp;gt;&amp;gt; listenerInterfaces = new HashSet&amp;lt;&amp;gt;();

    Map&amp;lt;String, Set&amp;lt;MethodInvoker&amp;gt;&amp;gt; invokerMap = new HashMap&amp;lt;&amp;gt;();
    boolean synthetic = false;
    for (Entry&amp;lt;String, String&amp;gt; entry : metaDataMap.entrySet()) {
        ...

      if (metaData.getAnnotation() != null) {
        invoker = getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate, metaData.getParamTypes());
        if (invoker != null) {
            invokers.add(invoker);
            synthetic = true;
        }
      }

      if (!invokers.isEmpty()) {
        invokerMap.put(metaData.getMethodName(), invokers);
        listenerInterfaces.add(metaData.getListenerInterface());
      }

    }

    ...
    proxyFactory.setInterfaces(listenerInterfaces.toArray(a));
    proxyFactory.addAdvisor(new DefaultPointcutAdvisor(new MethodInvokerMethodInterceptor(invokerMap, ordered)));
    return proxyFactory.getProxy();
  }
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AbstractStep에서 CompositeStepExecutionListener를 사용해 &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;리스너를 등록해준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722520507353&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CompositeStepExecutionListener implements StepExecutionListener {

	private final OrderedComposite&amp;lt;StepExecutionListener&amp;gt; list = new OrderedComposite&amp;lt;&amp;gt;();

	public void register(StepExecutionListener stepExecutionListener) {
		list.add(stepExecutionListener);
	}

    protected StepExecutionListener getCompositeListener() {
      return stepExecutionListener;
    }
	
	...	
}&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;이후 Step이 실행될 때, AbstractStep에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;CompositeStepExecutionListener을 가져와&lt;span&gt;&amp;nbsp;&lt;/span&gt;beforeStep과&lt;span&gt;&amp;nbsp;&lt;/span&gt;afterStep을 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722520558216&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractStep implements Step, InitializingBean, BeanNameAware {

	@Override
	public final void execute(StepExecution stepExecution)
			throws JobInterruptedException, UnexpectedJobExecutionException {
		...
		
		getCompositeListener().beforeStep(stepExecution);
		
		...
		
		exitStatus = exitStatus.and(getCompositeListener().afterStep(stepExecution));
		
		...
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CompositeStepExecutionListener는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Step에 등록된 모든&lt;span&gt;&amp;nbsp;&lt;/span&gt;StepExecutionListener의&lt;span&gt;&amp;nbsp;&lt;/span&gt;beforeStep와&lt;span&gt;&amp;nbsp;&lt;/span&gt;afterStep을 실행하게 된다. 위에서 설명했다시피 프록시 객체를 통해 @BeforeStep과 @AfterStep이 트리거되어 Tasklet에 구현한 메소드가 실행되는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ItemReader, ItemWriter, ItemProcessor&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ItemReader, ItemProcessor, ItemWriter는 따로 리스너로 등록하지 않아도 @BeforeStep나 @AfterStep을 사용할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722520660377&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(&quot;memberStep&quot;)
public Step memberStep(
    JobRepository jobRepository, PlatformTransactionManager transactionManager) {
  return new StepBuilder(&quot;memberStep&quot;, jobRepository)
      .&amp;lt;Member, Member&amp;gt;chunk(chunkSize, transactionManager)
      .reader(memberReader())
      .writer(memberWriter())
      .build();
}&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;StepBuilder(정확히는 SimpleStepBuilder)에서 build()를 실행할 때 registerAsStreamsAndListeners()&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드를 사용해 ItemReader, ItemProcessor, ItemWriter를 리스너로 자동 등록하기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722520790367&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public TaskletStep build() {

	registerStepListenerAsItemListener();
	registerAsStreamsAndListeners(reader, processor, writer);
	return super.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@afterstep</category>
      <category>@beforestep</category>
      <category>itemreader</category>
      <category>itemwriter</category>
      <category>Spring</category>
      <category>Spring Batch</category>
      <category>Step</category>
      <category>tasklet</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/234</guid>
      <comments>https://myvelop.tistory.com/234#entry234comment</comments>
      <pubDate>Thu, 1 Aug 2024 23:00:14 +0900</pubDate>
    </item>
    <item>
      <title>[Github Actions] 원격 레포지토리에 파일 저장하기</title>
      <link>https://myvelop.tistory.com/233</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRMcoh/btsJyAK2WQj/aAHKtwQwEZBAbKiMYguK5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRMcoh/btsJyAK2WQj/aAHKtwQwEZBAbKiMYguK5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRMcoh/btsJyAK2WQj/aAHKtwQwEZBAbKiMYguK5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRMcoh%2FbtsJyAK2WQj%2FaAHKtwQwEZBAbKiMYguK5k%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; alt=&quot;github actions signature img&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;363&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;363&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;Github Actions을 사용해 파일을 생성하고 그 파일을 레포지토리에 저장하고 싶다면 어떻게 해야할까?&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;Github Actions는 가상 머신에서 라이브러리의 기능이나 명령어를 통해 동작한다. 가상 머신에서 레포지토리의 코드를 가져와, 내가 원하는 파일을 생성하고 그것을 원격 저장소에 저장하면 된다. 레포지토리에&amp;nbsp; 파일을 저장하려면 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;git push&lt;/span&gt;&lt;/b&gt;를 해야하기 때문에 권한이 필요하다. 토큰을 발급받아야 하고 그 토큰을 사용해 레포지토리의 원격 저장소에 접근하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;먼저 Developer Settings에서 Access Token을 발급받아야 한다.&lt;/li&gt;
&lt;li&gt;아래와 같이 repo 권한을 가진 Access Token 하나를 발급해준다. 토큰 값은 레포지토리의 Secrets에 설정해줘야 하니 메모장에 잘 저장해두자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DXuy1/btsIRge5HSX/933KZ5GtEfEhLW5aq7iBx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DXuy1/btsIRge5HSX/933KZ5GtEfEhLW5aq7iBx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DXuy1/btsIRge5HSX/933KZ5GtEfEhLW5aq7iBx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDXuy1%2FbtsIRge5HSX%2F933KZ5GtEfEhLW5aq7iBx0%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; alt=&quot;personal access tokens&quot; loading=&quot;lazy&quot; width=&quot;1107&quot; height=&quot;197&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;197&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레포지토리 Secrets 지정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Github Actions를 설정할 레포지토리를 선택하고 상단의 Settings 메뉴로 들어가자.&lt;/li&gt;
&lt;li&gt;왼쪽 메뉴의 Security - Secrets and variables - Actions를 선택한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;231&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3sUTY/btsIRZ46i2j/dN2X6KnGvOGxakalycMRZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3sUTY/btsIRZ46i2j/dN2X6KnGvOGxakalycMRZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3sUTY/btsIRZ46i2j/dN2X6KnGvOGxakalycMRZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3sUTY%2FbtsIRZ46i2j%2FdN2X6KnGvOGxakalycMRZK%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; alt=&quot;secrets and variables&quot; loading=&quot;lazy&quot; width=&quot;381&quot; height=&quot;281&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;231&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;위에서 만들어둔 Access Token을 Secrets에 저장해준다.&lt;/li&gt;
&lt;li&gt;NAME은 나중에 Github Actions에서 env의 변수로 사용할 이름을 넣어주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ty5Y2/btsITllKEQo/kAAlrxVNzw3uiFRY3OCahK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ty5Y2/btsITllKEQo/kAAlrxVNzw3uiFRY3OCahK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ty5Y2/btsITllKEQo/kAAlrxVNzw3uiFRY3OCahK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fty5Y2%2FbtsITllKEQo%2FkAAlrxVNzw3uiFRY3OCahK%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; alt=&quot;configure new secret&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;334&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;425&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;저장하면 아래와 같이 값이 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2MHam/btsIRVO7GZS/Y2Q2NvpTdt2x7AGsBN7xP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2MHam/btsIRVO7GZS/Y2Q2NvpTdt2x7AGsBN7xP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2MHam/btsIRVO7GZS/Y2Q2NvpTdt2x7AGsBN7xP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2MHam%2FbtsIRVO7GZS%2FY2Q2NvpTdt2x7AGsBN7xP0%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; alt=&quot;Secrets complete&quot; loading=&quot;lazy&quot; width=&quot;628&quot; height=&quot;411&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;411&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;yaml 작성하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래의 코드 예시는 create.js 파일을 실행해 현재 레포지토리에서 저장하길 원하는 파일을 생성하고 그것을 add, commit하고 push해 원격 레포지토리에 파일이 자동으로 저장되게 하는 workflow이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722433115373&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: my push flow

on:
  push:
    branches: [main]
  workflow_dispatch:


jobs:
  build:
    runs-on: ubuntu-latest
    permissions: write-all
    steps:
      - name: checkout code
        uses: actions/checkout@v4
        
      - name: setup node
        uses: actions/setup-node@v3
        with:
          node-version: 16
          
      - name: run node
        run: |
          npm ci
          node create.js
          
      - name: push json
        env:
          GITHUB_TOKEN: ${{secrets.SECRET_TOKEN}}
        run: |
          git config --global user.email &quot;{당신의 이메일}&quot;
          git config --global user.name &quot;{당신의 깃헙 아이디}&quot;
          git add .
          git commit -m &quot;update my file&quot;
          git push origin main&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;workflow를 수동으로 실행하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;on&lt;/b&gt;&lt;/span&gt;에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;workflow_dispatch&lt;/b&gt;&lt;/span&gt;를 추가해줬다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jobs.build.permissions&lt;/span&gt;&lt;/b&gt;를 꼭&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;write-all&lt;/span&gt;&lt;/b&gt;로 둬야 한다. 참고로 이 설정을 넣지 않을 경우 권한이 없기 때문에 &quot;&lt;b&gt;remote: Write access to repository not granted. fatal: unable to access '': The requested URL returned error: 403&lt;/b&gt;&quot; 에러가 발생한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/b&gt;를 통해 레포지토리에 있는 코드 환경을 세팅해준다.&lt;/li&gt;
&lt;li&gt;자바스크립트를 실행하기 위해 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;actions/setup-node@v3&lt;/b&gt;&lt;/span&gt;를 통해 자바스크립트 런타임 환경인 노드를 설정해줬다.&lt;/li&gt;
&lt;li&gt;npm 의존성을 노드를 실행해 파일을 생성한다.&lt;/li&gt;
&lt;li&gt;마지막으로 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;git add&lt;/b&gt;&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;commit&lt;/b&gt;&lt;/span&gt;, &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;push&lt;/b&gt;&lt;/span&gt; 명령어를 통해 원격 저장소에 파일이 저장되도록 한다. 이 때 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;env&lt;/span&gt;&lt;/b&gt;에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;GITHUB_TOKEN&lt;/b&gt;&lt;/span&gt;으로 방금 전에 발급받았던 토큰의 값을 넣어줘야 push가 정상적으로 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주의사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nothing to commit, working tree clean&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dK67NE/btsJvtX60OW/WDM2K67EIK5VRDKoKOgG21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dK67NE/btsJvtX60OW/WDM2K67EIK5VRDKoKOgG21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dK67NE/btsJvtX60OW/WDM2K67EIK5VRDKoKOgG21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdK67NE%2FbtsJvtX60OW%2FWDM2K67EIK5VRDKoKOgG21%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; alt=&quot;push error&quot; loading=&quot;lazy&quot; width=&quot;662&quot; height=&quot;366&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;366&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;위와 같은 에러가 발생하는 이유는 실제로 추가할 파일이 없어서 그런 것이다. 따라서 커밋 및 푸시 작업이 진행되지 않도록 처리해야 한다.&lt;/li&gt;
&lt;li&gt;git status의 --porcelain 옵션을 사용하면 된다. 해당 옵션을 통해 변경사항이 있는지 파악할 수 있다. 아래와 같이 쉘 스크립트로 분기처리를 해주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1725782441841&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;name: my push flow

on:
  push:
    branches: [main]
  workflow_dispatch:


jobs:
  build:
    runs-on: ubuntu-latest
    permissions: write-all
    steps:
      ...
          
      - name: push json
        env:
          GITHUB_TOKEN: ${{secrets.SECRET_TOKEN}}
        run: |
          git config --global user.email &quot;{당신의 이메일}&quot;
          git config --global user.name &quot;{당신의 깃헙 아이디}&quot;
          
          if [[ -n $(git staus --porcelain) ]]; then
            git add .
            git commit -m &quot;update my file&quot;
            git push origin main
          else
            echo &quot;No changes to commit&quot;
          fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git &amp;amp; GitHub/Github Actions</category>
      <category>Git</category>
      <category>GitHub</category>
      <category>GitHub Actions</category>
      <category>nothing to commit working tree clean</category>
      <category>remote: write access to repository not granted</category>
      <category>yaml</category>
      <category>프로젝트에 파일 저장하기</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/233</guid>
      <comments>https://myvelop.tistory.com/233#entry233comment</comments>
      <pubDate>Wed, 31 Jul 2024 23:05:05 +0900</pubDate>
    </item>
    <item>
      <title>2024 오픈소스 컨트리뷰션 아카데미 시작!</title>
      <link>https://myvelop.tistory.com/232</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;256&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/loo69/btsIzrOZF1t/hVs3SJHls2LKDSfKRXmo4k/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/loo69/btsIzrOZF1t/hVs3SJHls2LKDSfKRXmo4k/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/loo69/btsIzrOZF1t/hVs3SJHls2LKDSfKRXmo4k/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Floo69%2FbtsIzrOZF1t%2FhVs3SJHls2LKDSfKRXmo4k%2Fimg.webp&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; alt=&quot;2024 오픈소스 컨트리뷰션 아카데미&quot; loading=&quot;lazy&quot; width=&quot;256&quot; height=&quot;256&quot; data-origin-width=&quot;256&quot; data-origin-height=&quot;256&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;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;이제 해볼 만한 게 없을까 이것저것 찾아보다가 2024 오픈소스 컨트리뷰션 아카데미가 눈에 들어왔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;링크:&amp;nbsp;&lt;a href=&quot;https://www.oss.kr/notice/show/2d116973-98e2-47a9-96fb-baaca19291fa&quot;&gt;2024 오픈소트 컨트리뷰션 아카데미 [참여형] 멘티 모집&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;다시 현 시점. 회사에서 큰 프로젝트도 마쳤고 생활에 여유가 생겼다. 오픈소스에 언제가는 꼭 기여해보고 싶다는 목표가 있었고, 스터디 모임 말고는 할 게 없었기 때문에 오픈소스 컨트리뷰션에 지원해보기로 했다. &lt;s&gt;되면 좋고~ 안되면 말고~&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트&lt;/h3&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;/li&gt;
&lt;li&gt;프로젝트에 기여한 내용이 나의 개발 실력, 회사 업무에 도움이 되는가&lt;/li&gt;
&lt;li&gt;내가 관심있던 기술인가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 2개를 기준으로 선택한 프로젝트는 Apache Zeppelin과 ArgoCD 였다. Apache Zeppelin은 회사에서 사용하는 기술 스택이었고, 백엔드와 인프라 과제가 명확해보여서 마음에 들었다. ArgoCD는 회사에서 사용하진 않았지만 나중에 사용해볼 가능성이 크다고 여겨졌고 k8s라는 기술에 관심이 많았기 때문에 선택했다.&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;어떤 것을 1지망으로 할지 고민이었다. 기술적인 관심도는 ArgoCD 높았지만 백엔드 과제가 명확하다는 점에서는 Apache Zeppelin이 끌렸다. 결국 회사 동료, 사수와 CTO 님한테까지 조언을 구했다. 결국 1지망을 Apache Zeppelin으로 했다. 제플린에 대해 잘 알게 되었을 때 회사에서 활용해볼 수 있는 많을 것 같다는 생각이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지원서 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 인적 사항과 1지망, 2지망 프로젝트를 선택하고 자기소개를 적는 부분이 있었다. 자기 소개(관심 분야 등)와 지원 동기, 프로젝트 개발 경험을 적어야 했다.&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;s&gt;드럽게 못써서 팩폭을 당했다.&lt;/s&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;첫 번째 자기소개 항목에서는 1,300자를 적었고 두 번째 지원 동기는 800자의 글을 작성했다. 프로젝트 개발 경험 부분은 200자밖에 적을 수 없었기 때문에 간략하게 작성하고 아래 이력서 링크를 첨부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀염뽀짝한 이력서 링크: &lt;a href=&quot;https://www.rallit.com/hub/resumes/131893/%EB%B0%95%EC%A2%85%ED%98%81?isExpanded=true&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;백엔드 개발자 - 박종혁 이력서&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;합격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격했다는 내용을 7월 8일 문자와 메일로 전달받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 받은 합격 메일이라 좋았다. 설레는 느낌.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Zeppelin 팀은 총 13명의 멘티가 선정되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1872&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uhx0b/btsIBfGzfML/OWxvahtbZpHiw8csmLNJsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uhx0b/btsIBfGzfML/OWxvahtbZpHiw8csmLNJsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uhx0b/btsIBfGzfML/OWxvahtbZpHiw8csmLNJsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuhx0b%2FbtsIBfGzfML%2FOWxvahtbZpHiw8csmLNJsK%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; alt=&quot;합격 메일&quot; loading=&quot;lazy&quot; width=&quot;680&quot; height=&quot;337&quot; data-origin-width=&quot;1872&quot; data-origin-height=&quot;928&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;나중에 알게 된 사실이지만 &lt;b&gt;Apache Zeppelin의 경쟁률은 1지망이 10대 1 이상&lt;/b&gt;이었다고 한다. (오픈소스 컨트리뷰션의 전체 경쟁률이 3대 1 정도라고 함) 멘토님도 지원자가 너무 많아 멘티 선정할 때 심혈을 기울였다고 했다. 잘할 것 같은 사람만 뽑았다고 말씀하셨는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;살짝 걱정이 된달까..? &lt;s&gt;나 사실 감자 개발자인데&lt;/s&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;발대식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발대식은 합격 발표가 있었던 주의 토요일에 진행되었다. 행사는 12시 30분부터 시작되었고 한국과학기술회관 지하 1층에서 열렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행사장에 도착해서 QR 코드를 통해 발대식 출석 체크를 하고 Apache Zeppelin 팀의 명찰을 받았다. 옆에 커피 부스가 있었고 커피와 물을 받아 정해진 팀별 자리에 착석했다.&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;같이 부스트캠프를 했던 분이 Raftify 팀의 멘토로 계셔서 잠깐 얼굴을 뵈었는데 반가웠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4DFIW/btsIBmrVjzw/Mf7XEV5J1kP15kxOWLbgx0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4DFIW/btsIBmrVjzw/Mf7XEV5J1kP15kxOWLbgx0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4DFIW/btsIBmrVjzw/Mf7XEV5J1kP15kxOWLbgx0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4DFIW%2FbtsIBmrVjzw%2FMf7XEV5J1kP15kxOWLbgx0%2Fimg.jpg&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; alt=&quot;명찰 이미지&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;504&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cncLU8/btsIAXMVDam/cAMGWj8Gbnixk6CukJNQYk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cncLU8/btsIAXMVDam/cAMGWj8Gbnixk6CukJNQYk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cncLU8/btsIAXMVDam/cAMGWj8Gbnixk6CukJNQYk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcncLU8%2FbtsIAXMVDam%2FcAMGWj8Gbnixk6CukJNQYk%2Fimg.jpg&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;630&quot; height=&quot;472&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&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;기념품으로 스티커와 보조배터리도 선물 받았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;1411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxqlLV/btsIAzZVSCc/NPyFaM7MKFa9tEt1lNb4KK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxqlLV/btsIAzZVSCc/NPyFaM7MKFa9tEt1lNb4KK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxqlLV/btsIAzZVSCc/NPyFaM7MKFa9tEt1lNb4KK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxqlLV%2FbtsIAzZVSCc%2FNPyFaM7MKFa9tEt1lNb4KK%2Fimg.jpg&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;409&quot; height=&quot;545&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;1411&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;h3 data-ke-size=&quot;size23&quot;&gt;특별 강연&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;세상을 바꾸는 시간&quot;이라는 제목으로 강연이 진행되었다. Linux Kernel Networking Stack의 유태희 멘토님이 발표를 해주셨다. 한국에서의 오픈소스 활동과 진입장벽. 그리고 본인의 일화를 소개하셨다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;한국에서는 소수의 개발자만 하드하게 오픈소스에 기여하고 나머지는 거의 참여하지 않는다. 그렇기에 오픈소스 컨트리뷰터는 굉장히 희소성이 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;오픈소스 컨트리뷰션을 하기 위해서는 알아야 할 것이 너무 많다. 오픈소스 기여를 혼자서 할 게 아니라면, 진입장벽을 허물 수 있는 가장 좋은 방법 중 하나는 오픈소스 컨트리뷰션 아카데미다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;문화나 프로세스 같은 것이 명시적인 룰이 존재하는 좋은 오픈소스인데, 대부분은 그렇지 않다. 문서적으로는 나태한 경우가 많다. 작은 프로젝트라면 더 그렇다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;오픈소스 컨트리뷰터는 자신의 의무를 다해야 한다. 의무를 소홀히 하여 문제가 있는 코드를 오픈소스에 남기는 일을 겪게될 수 있다. (제출한 패치에 취약점이 발견되었다는 메일을 받았던 일화)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;자신을 위해 한 오픈소스 기여 활동이 본인의 이익 뿐만 아니라 우리가 소속된 조직, 공동체, 그리고 국가에 유익한 영향을 미친다. (Linux ARIA 알고리즘 적용 일화)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아카데미 경험 공유 발표1 - &quot;두 배로 즐기기&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 발표는 RustPython의 정윤원 멘토님이 진행하셨다. 제목 그대로 오픈소스 컨트리뷰션을 잘 활용할 수 있는 방법에 대해 발표를 해주셨다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;멘티는 보통 해결하고 싶은 문제가 있어서 왔기보다는 프로젝트가 궁금해서 온 경우가 많다. (해결하고 싶은 걸 미리 찾아왔다면 너무 훌륭한 분이지만!) 멘토는 강사가 아니라 평범한 직장인이다. 프로젝트의 모든 것을 잘 설명하는 게 쉽지 않다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;가급적 기술적인 난이도가 낮은 이슈를 해결해보는 것이 좋다. 코드 작성 외에도 빌드, 테스트 실행, 코드 찾기, 변경 커밋하기, 패치 제출하기 등 다방면에서 다양한 문제가 발생하기 때문이다. (문제 공간 줄이기!) 첫이슈를 빠르게 해결해보는 경험이 진입 장벽을 낮추는 데 도움이 된다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;오픈소스 컨트리뷰션은 내가 참여하는 프로젝트의 주도적인 개발자로써 활동하는 과정이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;질문의 목적은 답을 듣기 위한 것이 아니라 이 문제를 이해하는 방식을 전달하는 커뮤니케이션의 일부이다. 때로는 엉뚱한 질문을 한다는 것이 어떤 이유로 잘못된 탐색을 하고 있는지를 설명하는 빠른 방법이 되기도 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;아카데미 경험 공유 발표1 - &quot;2024 오픈소스 컨트리뷰션 아카데미 미리보기&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python-mysql-replication의 장동욱 멘토님이 진행하셨다. 오픈소스 컨트리뷰션에서 python-mysql-replication 프로젝트 기여 활동을 진행해오면서 배운 것과 즐거웠던 경험을 공유해주셨다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;원티드에서 python-mysql-replication을 헤비하게 사용하고 있습니다. 한 번은 하위호환이 안되는 버전을 패치한 적이 있었습니다. 원티드에서 버전업을 원했고 정말 잘 되는 것인지 문제는 없는 것인지 의문이 많으셨는데요. 원티드 사옥에 가서 컨설팅까지 하고 맛있는 걸 얻어먹었던 적이 있습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;꿀팁 #1 Be Proactive. &lt;br /&gt;일을 벌이자. (스터디 조직하기!)&lt;br /&gt;과제 만들기. (멘토 분에게 과제를 역제안하기)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;꿀팁 #2 Be Open&amp;nbsp;&lt;br /&gt;모르는 것이 있다면 나서서 말하기 (총대매기) - 오픈된 채널에서 공유를 해주는 것이 좋다. &lt;br /&gt;막혀있기엔 4개월은 짧다. (혼자 생각하는 것보다 집단 지성이 더 낫다.)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;꿀팁 #3 Be Confident&lt;br /&gt;모두 다 제로베이스&lt;br /&gt;능력자는 내 아군&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멘토님과의 만남&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발대식 마지막엔 팀별 소통 시간이 진행되었다. 멘토님이 Apache Zeppelin이 어떤 프로젝트인지 소개해주셨다. 멘토님은 Apache Zeppelin을 만드는 회사에 있었는데 제플린을 Apache 재단에 기부하면서 제플린이 Apache의 후원을 받는 오픈소스가 되었다고 한다.&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;멘토님은 미리 생각해둔 이슈들이 있었고 그걸 염두에 두고 선정했다고 하셨다. 프론트엔드 UI 과제, 백엔드 기술 스택을 스프링으로 바꾸는 과제, 제플린 서비스들을 k8s의 오브젝트로 잘 감싸서 배포하는 과제 등을 생각해두셨다고 했다.&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;마지막으로 질문 시간이 진행되었다. 질문 하나를 드렸다. &quot;혹시 멘티들에게 바라는 점이 있으신가요?&quot;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;좋은 질문이네요. 저는 멘티님들한테 바라는 게 2가지 있습니다. 첫 번째는, 오픈소스 컨트리뷰션을 포기하지 않았으면 좋겠어요. 어려운 게 있으면 동료나 저에게 언제든지 도움을 요청해주셨으면 합니다. 두 번째는 오픈소스 컨트리뷰션 아카데미가 끝나더라도 기여 활동을 꾸준히 했으면 좋겠어요. 긴 호흡을 가져가시면 좋을 것 같아요. 취미 생활이나 자기 계발하듯 시간날 때마다 하는 것이 좋습니다. 저는 &quot;Apache Zeppelin에 1년 반을 갈아넣었다&quot;고 표현할 정도로 열심히 했었거든요. 그게 커리어에 언젠가 정말 큰 도움이 될 수 있어요. 제가 라인에 들어갔을 때 &quot;우리 팀에서 Zeppelin을 사용해야 하니깐 저 사람 데려와야 해&quot; 라는 식으로 뽑혔거든요. 아마 정석대로 들어가려고 했으면 면접이 너무 어려워서 못 들어갔을 수도 있어요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 Apache Zeppelin 팀과 기념 촬영을 하고 행사를 끝마쳤다!! 끝끝~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동/IT커뮤니티</category>
      <category>apache zeppelin</category>
      <category>오픈소스 컨트리뷰션 2024</category>
      <category>오픈소스 컨트리뷰션 합격</category>
      <category>제플린</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/232</guid>
      <comments>https://myvelop.tistory.com/232#entry232comment</comments>
      <pubDate>Tue, 16 Jul 2024 13:33:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 이벤트로 유연한 설계 만들기</title>
      <link>https://myvelop.tistory.com/231</link>
      <description>&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;스프링 이벤트는 Observer Pattern으로 구현된 기술로 객체 간 강결합 의존성을 떼어내기 위해 사용합니다. 해당 객체의 주 관심사가 아닌 로직과의 결합을 느슨하게 만들어 주 관심사에 집중할 수 있게 해줍니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spring Event를 사용하기 전 알아두면 좋은 것들!&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. Observer Pattern&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;옵저버 패턴의 구성 요소는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Observable&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Observer&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;로 나눌 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observable은 의존 대상이 되는 피관찰자입니다.&lt;/li&gt;
&lt;li&gt;Observer는 의존하고 있는 관찰자입니다.&lt;/li&gt;
&lt;li&gt;Observable은 여러&lt;span&gt;&amp;nbsp;&lt;/span&gt;Observer를 등록할 수 있습니다. (registry) (Observable:Observer&lt;span&gt;&amp;nbsp;&lt;/span&gt;= 1:N)&lt;/li&gt;
&lt;li&gt;Observable에서 이벤트가 발생하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Observable은 자신에게 등록된 모든&lt;span&gt;&amp;nbsp;&lt;/span&gt;Observer에게 이를 알립니다. (notify)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CW4PM/btsIg0YxncK/OYKrBZ3X8QlT2luxTptUH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CW4PM/btsIg0YxncK/OYKrBZ3X8QlT2luxTptUH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CW4PM/btsIg0YxncK/OYKrBZ3X8QlT2luxTptUH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCW4PM%2FbtsIg0YxncK%2FOYKrBZ3X8QlT2luxTptUH1%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; alt=&quot;옵저버 패턴&quot; loading=&quot;lazy&quot; width=&quot;1404&quot; height=&quot;528&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;528&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;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 비즈니스 로직의 예를 들어 설명해보도록 하겠습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;OrderEvent&lt;/span&gt;&lt;/b&gt;라는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Observable이 존재하고 그것을 관찰하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Observer인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;PointService&lt;/span&gt;&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;CouponService&lt;/b&gt;&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;SupporterService&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등이 있다고 가정해봅시다.&lt;/li&gt;
&lt;li&gt;Observer들은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;OrderEvent&lt;/b&gt;&lt;/span&gt;라는 이벤트를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;@EventListener&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 기능을 통해 등록해놓습니다. (registry)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;OrderService&lt;/span&gt;&lt;/b&gt;에서 주문을 진행하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;OrderEvent&lt;/span&gt;&lt;/b&gt;를 라는 이벤트가 발생합니다. (notify) 이제&lt;span&gt;&amp;nbsp;&lt;/span&gt;Observer들은 이벤트를 수신해 각자의 로직을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9FYOU/btsIg40L4qm/ecnCaPyuGIAtR797VOqSn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9FYOU/btsIg40L4qm/ecnCaPyuGIAtR797VOqSn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9FYOU/btsIg40L4qm/ecnCaPyuGIAtR797VOqSn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9FYOU%2FbtsIg40L4qm%2FecnCaPyuGIAtR797VOqSn0%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; alt=&quot;스프링 이벤트 옵저버 패턴 예시&quot; loading=&quot;lazy&quot; width=&quot;1568&quot; height=&quot;550&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;550&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;h3 data-ke-size=&quot;size23&quot;&gt;1-2. Modular Monolithic Architecture&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 이벤트가 속한 프로젝트인 Spring Modulith는 Modular Monolithic Architecture를 지원하는 라이브러리입니다. 해당 기능이 어떤 철학을 바탕으로 만들어졌는지 공부해보면 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크: &lt;a href=&quot;https://medium.com/design-microservices-architecture-with-patterns/microservices-killer-modular-monolithic-architecture-ac83814f6862&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Modular Monolithic Architecture를 설명한 블로그&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring Event&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Spring Event란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Event는 Spring Modulith 프로젝트의 일부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Modulith는 마이크로서비스 아키텍처의 장점을 활용하면서도 단일 모놀리식 애플리케이션 구조 내에서 모듈화된 개발을 하자는 Modular Monolithic Architecture에서 시작된 프로젝트입니다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스프링 이벤트는 멀티캐스팅입니다. 이벤트 하나가 발생하면 다수의 사용자가 수신할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 기본적으로 동기 방식으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 트랜잭션을 결합할 수 있습니다. 트랜잭션을 하나의 범위로 묶어서 사용할 수 있습니다. 이벤트를 구독하는 곳과 동일한 트랜잭션을 공유할 수 있다는 얘깁니다. 위에서 설명한 TransactionPhase 설정을 통해 트랜잭션에 관여할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #1f2328; text-align: -webkit-center;&quot;&gt;의존성을 분리하여 느슨하게 결합할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #1f2328; text-align: -webkit-center;&quot;&gt;코드를 파악하기 어렵습니다. 이벤트를 파악하기 위해 이벤트를 구독하는 모든 메소드를 찾아다녀야할 수도 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;서비스 로직을 분리하기 쉽습니다.&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;로직의 순서를 고려해야할 경우 오히려 처리하기 어려워질 수 있습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;단위 테스트가 쉬워집니다.&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;color: #1f2328; text-align: -webkit-center;&quot;&gt;전체적인 이벤트 발행/구독 과정을 정확히 파악하기 어렵기 때문에 통합 테스트가 어려워집니다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3.언제 사용해야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문 도메인이 있다고 해봅시다. 주문은 회원, 포인트, 쿠폰, 결제, 알림, 서포터즈, 제품 등등 너무 많은 의존성이 엮어 있어 관리가 힘든 상태입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719726370349&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Server
@RequiredArgsConstructor
public class OrderService {
  private final MemberService memberService;
  private final ProductService productService;
  private final PaymentService paymentService;
  private final PointService pointService;
  private final CouponService couponService;
  private final NotifiactionService notifiactionService;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 의존성을 떼어내고 주문이라는 관심사에 집중하기 위해 스프링 이벤트를 사용해볼 수 있습니다. &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;주문은 회원, 제품, 결제 정보 없이는 로직 진행이 안되기 때문에 의존성을 남겨두기로 하고 나머지 포인트 및 쿠폰 사용 및 적립, 알림 등을 이벤트로 떼어내기로 결정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 수많은 의존성이 엮어있던 주문의 서비스 의존성이 줄어든 것을 확인할 수 있습니다. 굳이 쿠폰, 포인트, 알림 서비스의 메소드를 호출할 필요 없이 이벤트만 발행하면 로직이 자동으로 실행되는 마법같은 일이 벌어집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719726445149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Server
@RequiredArgsConstructor
public class OrderService {
  private final MemberService memberService;
  private final ProductService productService;
  private final PaymentService paymentService;
  private final OrderEventPublisher orderEventPublisher;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 스프링 이벤트로 인해 코드 복잡성이 올라갈 수 있습니다. 이벤트 발행 시 사용되는 로직을 이해하기 위해 모든 이벤트 사용처를 일일이 찾아다니면서 확인해야 한다는 불편함이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 세부 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. ApplicationEventPublisher&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;ApplicationEventPublisher&lt;/b&gt;&lt;/span&gt;는 Spring에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ApplicationContext&lt;/span&gt;&lt;/b&gt;로 구현됩니다. &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ApplicationContext&lt;/span&gt;&lt;/b&gt;는 빈 탐색과 등록, 리소스 처리 등의 역할을 수행하는 구현체입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719727922856&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ApplicationContext extends 
    EnvironmentCapable, 
    ListableBeanFactory, 
    HierarchicalBeanFactory,
    MessageSource, 
    ApplicationEventPublisher, 
    ResourcePatternResolver {...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;ApplicationEventPublisher&lt;/b&gt;&lt;/span&gt;는 인터페이스 분리 원칙(Interface Segregation Principle)에 따라 이벤트 발행 책임만 처리하기 때문에 이벤트 발행 기능을 사용할 객체는 의존성 주입을 받을 때 굳이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;ApplicationContext&lt;/b&gt;&lt;/span&gt; 인터페이스를 받지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;ApplicationEventPublisher&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인터페이스만 주입받아도 됩니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. @EventListener&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 수신할 때 사용하는 어노테이션입니다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;@EventListener&lt;/b&gt;&lt;/span&gt;가 달려 있는 메소드는 이벤트 객체 파라미터를 필수로 넣어야 합니다. 이벤트 객체 파라미터를 넣지 않으면 실행 과정에서 에러가 발생하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;@EventListener&lt;/b&gt;&lt;/span&gt;는 호출 시점에 바로 실행된다는 특징이 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719725478896&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {
  
  @EventListener
  public void onOrderEvent(final OrderEvent orderEvent) {
    ...
  } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-3. @TransactionalEventListener&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션과 연관된 이벤트 리스너입니다. 트랜잭션을 사용하지 않으면 이벤트를 수신하지 않는 특징이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Transactional(propagation = Propagation.NOT_SUPPORTED)&lt;/span&gt;&lt;/b&gt;에 감싸인 메소드에서 이벤트를 발행하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@TransactionalEventListner&lt;/span&gt;&lt;/b&gt;는 이벤트를 수신하지 않습니다. 왜 그럴까요?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;@TransactionalEventListner&lt;/b&gt;&lt;/span&gt;는 트랜잭션 커밋, 롤백, 종료 시점 등이 트리거가 되는데, 트랜잭션이 실행되지 않은 상태면 트리거가 작동하지 않기 때문에 이벤트가 당연히 수신되지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@TransactionalEventListner&lt;/span&gt;&lt;/b&gt;에는 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;phase&lt;/span&gt;&lt;/b&gt;라는 속성이 있습니다. 트랙잭션 페이즈의 종류는 4가지가 있고, 사용자는 원하는 시점을 선택해 설정하면 됩니다. 기본값은 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;TransactionPhase.AFTER_COMMIT&lt;/b&gt;&lt;/span&gt; 입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TransactionPhase&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 87px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;TransactionPhase&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;BEFORE_COMMIT&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;트랜잭션 커밋이 되기 전에 실행된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;AFTER_COMMIT&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;트랜잭션 커밋이 되면 실행된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;AFTER_ROLLBACK&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;트랜잭션 롤백이 되면 실행된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;AFTER_COMPLETION&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;트랜잭션이 끝난 이후에 실행된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 코드 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;주문을 진행하는 코드입니다.&lt;/span&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;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;orderEventPublisher&lt;/b&gt;&lt;/span&gt; 객체를 따로 생성해 주입받고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;orderEventPublisher&lt;/span&gt;&lt;/b&gt;는 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ApplicationEventPulisher&lt;/span&gt;&lt;/b&gt;를 주입받습니다. 이벤트 객체를 만들고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;ApplicationEventPulisher&lt;/span&gt;&lt;/b&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;publishEvent()&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드를 사용해 이벤트를 발행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1719726125771&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OrderService {
  
  ...
  private final OrderEventPublisher orderEventPublisher;
  
  @Transactional
  public Order order(final OrderRequest request) {
    ...
    orderEventPublisher.publishOrderEvent(order);
    return order;
  }
  
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719726135338&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class OrderEventPublisher {
  
  private final ApplicationEventPulisher eventPublisher;
  
  public void publishOrderEvent(final Order order) {
    final OrderEvent orderEvent = OrderEvent.from(order);
    eventPublisher.publishEvent(orderEvent);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719726143804&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {
  
  @EventListener
  public void onOrderEvent(final OrderEvent orderEvent) {
    ...
  } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 트랜잭션 시점을 조정하고 싶다면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;트랜잭션의 시점을 조절하여 이벤트를 수신하고 싶다면 위에서 설명했던&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@TransactionalEventListener&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;phase&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;속성을 사용하시면 됩니다!&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719726158637&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {
  
  @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
  public void onOrderEvent(final OrderEvent orderEvent) {
    ...
  } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 비동기 이벤트 수신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;AsyncConfig&lt;/span&gt;&lt;/b&gt;를 만들고 아래와 같이 비동기로 처리하고 싶은 이벤트 리스너에 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Async&lt;/span&gt;&lt;/b&gt;만 붙이면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1719726169603&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {
  
  @Async
  @TransactionalEventListener
  public void onOrderEvent(final OrderEvent orderEvent) {
    ...
  } 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async를 사용하면 트랜잭션이 자동으로 분리되기 때문에 비동기 작업의 실패로 인해 부모 트랜잭션이 롤백되지는 않는다. 다만, 트랜잭션이 분리된다는 것을 명시적으로 표현하고 싶다면 아래와 같이 트랜잭션 전파 레벨을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;REQUIRES_NEW&lt;/span&gt;&lt;/b&gt;로 할 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1719726179569&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {

  @Async
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  @TransactionalEventListener
  public void onOrderEvent(final OrderEvent orderEvent) {
    ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 여기서 이러시면 안 됩니다&lt;/h2&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5-1. 트랜잭션을 사용하지 않는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;@TransactionalEventListener로 이벤트를 수신하려는 경우&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모종의 이유로 주문 로직에서 트랜잭션을 사용하지 않는다고 가정해봅시다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f6f8fa; color: #1f2328; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OrderService {
  
  ...
  private final OrderEventPublisher orderEventPublisher;

  @Transactional(propagation = Propagation.NOT_SUPPORTED)
  public Order order(final OrderRequest request) {
    ...
    orderEventPublisher.publishOrderEvent(order);
    return order;
  }
  
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 사용하지 않는 메소드로부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@TransactionalEventListener&lt;/span&gt;&lt;/b&gt;을 사용해 이벤트를 수신하려고 하면 아무런 일도 발생하지 않습니다. &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@TransactionalEventListener&lt;/span&gt;&lt;/b&gt;는 &lt;u&gt;트랜잭션이 롤백되거나 커밋 혹은 종료되는 시점을 기준으로 이벤트가 실행되는데 트랜잭션 자체가 없으면 이벤트 트리거가 발동되지 않기 때문&lt;/u&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f6f8fa; color: #1f2328; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {

  @Async
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  @TransactionalEventListener
  public void onOrderEvent(final OrderEvent orderEvent) {
    ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 트랜잭션을 사용하지 않는다면 아래와 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;@EventListener를 사용합시다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f6f8fa; color: #1f2328; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {

  @Async
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  @EventListener
  public void onOrderEvent(final OrderEvent orderEvent) {
    ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 이벤트 발행이 소비자 종속적인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 아래 그림과 같이 주문, 회원이 서포터즈 이벤트 발행자에게 의존하고 있는 모양이라면 어떻게 보이시나요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFYOZw/btsIigMCaU0/enH3vvKViiax07rVTCpR9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFYOZw/btsIigMCaU0/enH3vvKViiax07rVTCpR9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFYOZw/btsIigMCaU0/enH3vvKViiax07rVTCpR9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFYOZw%2FbtsIigMCaU0%2FenH3vvKViiax07rVTCpR9k%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; alt=&quot;잘못된 의존성 연결1&quot; loading=&quot;lazy&quot; width=&quot;668&quot; height=&quot;706&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;706&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;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;나중에 이벤트가 늘어나면 늘어날 수록 그만큼 이벤트 발행자를 의존해야 하기 때문에 &lt;/span&gt;사실상 Service를 의존하는 것과 다를 바 없습니다.&amp;nbsp; 이렇게 되면 도메인 간 강결합 의존성을 떼어낸다는 취지에 완전히 어긋나버리게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjg8Ld/btsIhL0DwhN/4iyWd2tvJPy5h7C4jWdKg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjg8Ld/btsIhL0DwhN/4iyWd2tvJPy5h7C4jWdKg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjg8Ld/btsIhL0DwhN/4iyWd2tvJPy5h7C4jWdKg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjg8Ld%2FbtsIhL0DwhN%2F4iyWd2tvJPy5h7C4jWdKg1%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; alt=&quot;잘못된 의존성 연결2&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;272&quot; data-origin-width=&quot;1710&quot; data-origin-height=&quot;730&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;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 id=&quot;code_1719724821550&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OrderService {
  private final SupporterEventPublisher supporterEventPublisher;
  
  public void order() {
    ...
    supporterEventPublisher.publishOrderEvent(order);
  }
}

@Service
@RequiredArgsConstructor
public class MemberService {
  private final SupporterEventPublisher supporterEventPublisher;
  
  public void signUp() {
    ...
    supporterEventPublisher.publishSignUpEvent(member);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719724834824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SupporterService {

  @EventListener
  public void onSignUp(final SignUpEvent signUpEvent) {...}
  
  @EventListener
  public void onOrderEvent(final OrderEvent orderEvent) {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;이벤트 소비자는 이벤트를 발행시켜 가져오는 역할을 하기보다는 그저 이벤트가 발행되기만을 기다리는 것이 좋습니다. 소비자는 이벤트를 수신받을 뿐인 불특정 다수라는 사실을 인지합시다.&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;&amp;nbsp;&lt;/p&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://medium.com/dtevangelist/event-driven-microservice-%EB%9E%80-54b4eaf7cc4a&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Event Driven Architecture?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-modulith/reference/events.html&quot;&gt;공식 문서: Working with Application Event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/292&quot;&gt;[Spring] 스프링에서 이벤트의 발행과 구독 방법과 주의사항, 이벤트 사용의 장/단점과 사용 예시&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/231</guid>
      <comments>https://myvelop.tistory.com/231#entry231comment</comments>
      <pubDate>Sun, 30 Jun 2024 15:52:49 +0900</pubDate>
    </item>
    <item>
      <title>Remote JVM Debug (feat. IntelliJ, k8s)</title>
      <link>https://myvelop.tistory.com/230</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플 로그인 만드는 작업을 했는데, 애플 로그인은 다른 로그인과는 다른 특징이 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http나 로컬호스트에서 사용 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 Dev나 Stag 서버에 배포해 테스트해보는 수밖에 없었는데 디버거 없이 개발하는 과정이 굉장히 불편하게 느껴졌습니다. 하나하나 로그 찍고 배포하고, 또 확인할 거 생겼을 때 다시 로그 찍고 배포해서 확인할 수도 없고...&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 하면 원격에서도 디버거를 사용할 수 있을까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Debugger&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 애플리케이션을 만들어 보신 분이라면 대부분 IDE에 내장된 디버거를 사용해보신 경험이 있으실 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eclipse, IntelliJ IDEA와 같은 IDE는 디버거를 가지고 있는데요. 개발자들은 이 디버거를 사용해 코드의 플로우나 변수의 값을 확인해 버그를 쉽게 잡을 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java platform debugger architecture&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 자바에서 디버그는 어떤 원리로 실행될까요? Java Platform debugger architecture에 대해 알아봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아키텍처는 디버깅을 당하는 대상인 Debuggee(서버)와 디버깅을 하는 Debugger로 구성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Debuggee는 Debugger는 JDWP라는 프로토콜로 메시지를 주고 받는데요. 이 때 중요한 역할을 하는 것이 Debuggee의 JMVTI 인터페이스, Debugger의 JDI 인터페이스 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIV3Rb/btsH2URFcJn/Za2ZD7JxAGkJ5nG9dyTHX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIV3Rb/btsH2URFcJn/Za2ZD7JxAGkJ5nG9dyTHX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIV3Rb/btsH2URFcJn/Za2ZD7JxAGkJ5nG9dyTHX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIV3Rb%2FbtsH2URFcJn%2FZa2ZD7JxAGkJ5nG9dyTHX0%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; alt=&quot;relationship of debuggee and debugger&quot; loading=&quot;lazy&quot; width=&quot;1482&quot; height=&quot;696&quot; data-origin-width=&quot;1482&quot; data-origin-height=&quot;696&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;간단하게 설명하자면, &lt;b&gt;JMVTI&lt;/b&gt;(JVM Tools Interface)는 JVM에서 실행되는 애플리케이션의 상태를 검사하고 실행을 제어해주는 네이티브 인터페이스입니다. &lt;b&gt;JDI&lt;/b&gt;(Java Debug Interface)는 애플리케이션이 디버깅되는 동안 개발자가 디버거를 통해 애플리케이션과 상호 작용할 수 있도록 해주는 인터페이스입니다. &lt;b&gt;JDI&lt;/b&gt;를 통해 중단점 설정, Stepping, 스레드 처리, 변수 검사 등의 작업을 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JDWP&lt;/b&gt;(Java Debugging Wire Protocol)는 Debuggee와 Debugger가 소통할 때 사용하는 프로토콜입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그런데 로컬에서 디버거를 사용할 수 없다면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 서론에 적혀 있는 것처럼 로컬에서 개발해야하는 핵심 로직을 실행할 수 없다면? 그래서 디버그를 사용할 수 없는 상황이라면 어떻게 해야할까요? 그럴 때 사용할 수 있는 것이 Remote JVM Debug 기능입니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Remote Debug&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버는 Debuggee, IDE는 Debugger.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 간단하게 나누고 보면, 원격으로 디버깅을 하나 로컬로 디버깅을 하나 원리는 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버와 IDE를 JWDP로 연결해주면 로컬에서 사용하는 IDE에서 디버깅이 가능해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬에서 디버거를 사용할 때는 별도의 설정 없이 IDE가 Debuggee를 제어할 수 있기에 연결이 자유자재였습니다만 원격 서버는 아무런 설정 없이 IDE가 제어하기란 불가능하니 디버거를 연결하고 싶으면 별도의 설정이 필요합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Java 옵션 명령어 사용&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Java 명령어로 Application을 실행할 때 agentlib 옵션을 설정하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718720386624&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5556 -jar application.jar&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;agentlib&lt;/span&gt;&lt;/b&gt; 옵션
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jdwp=transport=dt_socket&lt;/span&gt;&lt;/b&gt;: JVM에 JDWP를 에이전트로 등록하여 디버거가 JVM에 소켓 방식으로 연결할 수 있도록 설정. 실행 중인 두 애플리케이션을 연결할 때, 소켓이 표준으로 사용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;server=y&lt;/span&gt;&lt;/b&gt;: y로 설정할 경우, 서버 역할을 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;suspend=n&lt;/span&gt;&lt;/b&gt;: 디버거를 기다리지 않고, 즉시 프로그램을 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;address=*:5556&lt;/span&gt;&lt;/b&gt;: 5556 포트에 디버거가 연결될 수 있도록 Listening 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;도커파일에서 명령어 적용해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 실제 배포 환경에서는 도커 이미지를 많이 사용하실텐데요. 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커에서 Application을 실행하는 명령어를 동작시킬 때 위의 옵션을 그대로 적용하면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1718720291363&quot; class=&quot;shell&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;FROM gradle:8.4.0-jdk21-alpine AS builder

ARG name=my-server

WORKDIR /app
COPY gradle ./gradle
COPY build.gradle.kts ./
COPY settings.gradle.kts ./
COPY ${name} ./${name}

RUN gradle :${name}:bootJar

FROM openjdk:21
ARG name=my-server
ENV jar_name=${name}

WORKDIR /home/my-server
COPY --from=builder app/${name}/build/libs/${name}.jar ./${name}.jar

ENTRYPOINT exec java \
  -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5556 \
  -jar ${jar_name}.jar&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;원격 연결하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 이미지로 배포에 성공했다면 이제 IDE의 Remote JVM Debug 기능을 사용해 원격의 Debuggee에 연결을 시도해봅시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Edit Configurations로 들어갑시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQdczH/btsH4pv9qxG/eksKdtGr3YI0wPCDbkAnZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQdczH/btsH4pv9qxG/eksKdtGr3YI0wPCDbkAnZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQdczH/btsH4pv9qxG/eksKdtGr3YI0wPCDbkAnZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQdczH%2FbtsH4pv9qxG%2FeksKdtGr3YI0wPCDbkAnZ1%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; alt=&quot;edit configurations&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;202&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Add New Configuration을 클릭하고 Remote JVM Debug를 선택합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;1182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyiKCP/btsH44kHKKX/YHy0b1hZWYWLm4XWU13wK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyiKCP/btsH44kHKKX/YHy0b1hZWYWLm4XWU13wK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyiKCP/btsH44kHKKX/YHy0b1hZWYWLm4XWU13wK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyiKCP%2FbtsH44kHKKX%2FYHy0b1hZWYWLm4XWU13wK1%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; alt=&quot;Remote JVM Debug&quot; loading=&quot;lazy&quot; width=&quot;457&quot; height=&quot;814&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;1182&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;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Attach to remote JVM을 선택합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2068&quot; data-origin-height=&quot;714&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5Z5n4/btsH4EfwkaE/rtuQxLIpOwK2HPB7TDpI4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5Z5n4/btsH4EfwkaE/rtuQxLIpOwK2HPB7TDpI4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5Z5n4/btsH4EfwkaE/rtuQxLIpOwK2HPB7TDpI4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5Z5n4%2FbtsH4EfwkaE%2FrtuQxLIpOwK2HPB7TDpI4K%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; alt=&quot;configure remote jvm&quot; loading=&quot;lazy&quot; width=&quot;2068&quot; height=&quot;714&quot; data-origin-width=&quot;2068&quot; data-origin-height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트 주소와 Listening 포트를 적어주고 Apply -&amp;gt; OK!&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Listening 포트는 위의 명령에서 작성한 5556입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;1476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S4AVT/btsH3kicTVz/GHmXZElt4XWafH94tf9M7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S4AVT/btsH3kicTVz/GHmXZElt4XWafH94tf9M7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S4AVT/btsH3kicTVz/GHmXZElt4XWafH94tf9M7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS4AVT%2FbtsH3kicTVz%2FGHmXZElt4XWafH94tf9M7K%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; alt=&quot;configure remote jvm&quot; loading=&quot;lazy&quot; width=&quot;2084&quot; height=&quot;1476&quot; data-origin-width=&quot;2084&quot; data-origin-height=&quot;1476&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;이제 위 Configuration으로 디버그를 실행하면 원격 서버에서 실행되는 로직이 로컬에서 디버그 가능해집니다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(대신 원격 서버의 5556가 열려 있어야 합니다!!)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보너스! k8s에서 Remote JVM Debug 쉽게 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubernetes 환경에서 Remote JVM Debug를 쉽게 사용할 수 있는 방법 2가지를 소개해드리도록 하겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pod가 여러 개 띄워져 있을 때 사용이 불가능합니다. API 요청 시 로드밸런싱에 의해 요청이 나눠지게 되는데, 그 요청이 포트포워딩된 파드나 노드 포트로 연결된 파드로 무조건 전달된다는 보장이 없기 때문입니다.&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;Application Pod를 로컬호스트로 포트포워딩하여 IDE의 Remote JVM Debug 설정 시 호스트 주소를 로컬호스트로 두는 방식입니다. 가장 간단한 방법입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 명령어에 대한 설명
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;{로컬에서 사용할 포트}:{서버에서 리스닝하고 있는 포트}&lt;/li&gt;
&lt;li&gt;서버에서 리스닝하고 있는 포트는 도커 파일에서 설정해준 5556이 들어가야합니다.&lt;/li&gt;
&lt;li&gt;로컬에서 사용할 포트는 IntelliJ에서 설정할 포트 값으로 해주시면 됩니다. (현재 로컬에서 사용하고 있지 않은 포트 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1718721360976&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl port-forward -n my-namespace pods/my-server-deploy-69d75789cf-psfcw 5556:5556
Forwarding from 127.0.0.1:5556 -&amp;gt; 5556
Forwarding from [::1]:5556 -&amp;gt; 5556&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;아래와 같이 호스트를 localhost로 둬도 연결이 됩니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjcyxo/btsH4ZcGgSY/WdWgBHAnbR3OvRLxpxdrg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjcyxo/btsH4ZcGgSY/WdWgBHAnbR3OvRLxpxdrg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjcyxo/btsH4ZcGgSY/WdWgBHAnbR3OvRLxpxdrg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjcyxo%2FbtsH4ZcGgSY%2FWdWgBHAnbR3OvRLxpxdrg1%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; alt=&quot;port forward configuration&quot; loading=&quot;lazy&quot; width=&quot;1352&quot; height=&quot;426&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;426&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 노드포트 사용하기&lt;/h3&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;서비스의 Type을 NodePort 두는 것이 방법이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 확인해봅시다. Remote JVM Debug에 사용할 포트를 고정하기 위해 debug 포트를 32000번으로 고정해줬습니다. (주의! deployment에서도 5556번 포트를 열어줘야함.)&lt;/p&gt;
&lt;pre id=&quot;code_1718721650370&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: my-server-svc
  namespace: my-namespace
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
      name: server
    - port: 5556
      targetPort: 5556
      protocol: TCP
      nodePort: 32000
      name: debug
  type: NodePort
  selector:
    app: my-server&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;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;파드가 어떤 노드에 속하는지 알아봅시다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1718721778385&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl get pod -o wide -n lohasmeal-dev
NAME                               READY   STATUS    RESTARTS        AGE     IP             NODE                    NOMINATED NODE   READINESS GATES
my-server-deploy-795888d8c5-jcp27  1/1     Running   0               3d23h   xxx.xx.x.xx    my-nodepool-AAAAAAA     &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;&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;노드의 INTERNAL-IP를 호스트주소 삼아 원격 연결을 시도하면 됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;xx.x.x.xx:32000 형식으로 연결!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1718721818728&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl get node -o wide
NAME                   STATUS   ROLES    AGE    VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
my-nodepool-AAAAAAA    Ready    &amp;lt;none&amp;gt;   88d    v1.23.9   xx.x.x.xx     &amp;lt;none&amp;gt;        Ubuntu 20.04.3 LTS   5.4.0-99-generic   containerd://1.6.16&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://velog.io/@roycewon/Java-Remote-Debugging&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java Remote Debugging&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>IntelliJ</category>
      <category>java remote debug</category>
      <category>jpda</category>
      <category>remote debug</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/230</guid>
      <comments>https://myvelop.tistory.com/230#entry230comment</comments>
      <pubDate>Tue, 18 Jun 2024 23:45:23 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] RestClient URI Encoding 문제 (feat. 퍼센트 인코딩)</title>
      <link>https://myvelop.tistory.com/228</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RestClient의 URI 인코딩&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DefaultUriBuilderFactory&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RestClient를 생성할 때 보통 Builder를 사용해 만들게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716217120849&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface RestClient {

    ...

    static RestClient.Builder builder() {
        return new DefaultRestClientBuilder();
    }
    
    ....
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RestClient.Bulider가 build() 하는 시점에 아래와 같이 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;DefaultUriBuilderFactory&lt;/span&gt;&lt;/b&gt;를 기본으로 생성해 가지고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716214721544&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DefaultRestClientBuilder {

    ...

    @Override
    public RestClient build() {
        ClientHttpRequestFactory requestFactory = initRequestFactory();
        UriBuilderFactory uriBuilderFactory = initUriBuilderFactory();
        HttpHeaders defaultHeaders = copyDefaultHeaders();
        List&amp;lt;HttpMessageConverter&amp;lt;?&amp;gt;&amp;gt; messageConverters = (this.messageConverters != null ?
                this.messageConverters : initMessageConverters());
        return new DefaultRestClient(requestFactory,
                this.interceptors, this.initializers, uriBuilderFactory,
                defaultHeaders,
                this.statusHandlers,
                messageConverters,
                this.observationRegistry,
                this.observationConvention,
                new DefaultRestClientBuilder(this)
                );
    }
    
    private UriBuilderFactory initUriBuilderFactory() {
	    if (this.uriBuilderFactory != null) {
		    return this.uriBuilderFactory;
	    }
	    DefaultUriBuilderFactory factory = (this.baseUrl != null ?
			    new DefaultUriBuilderFactory(this.baseUrl) : new DefaultUriBuilderFactory());
	    factory.setDefaultUriVariables(this.defaultUriVariables);
	    return factory;
    }
    
    ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;DefaultUriBuilderFactory&lt;/span&gt;&lt;/b&gt;의 EncodingMode 기본값을 확인해보면 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;EncodingMode.TEMPLATE_AND_VALUES&lt;/span&gt;&lt;/b&gt;입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716214798025&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class DefaultUriBuilderFactory implements UriBuilderFactory {

    ...
    
    private EncodingMode encodingMode = EncodingMode.TEMPLATE_AND_VALUES;
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;DefaultUriBuilderFactory &lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;EncodingMode&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EncodingMode를 이해하기 전에 먼저 &lt;b&gt;URI 변수&lt;/b&gt;와 &lt;b&gt;URI 템플릿&lt;/b&gt;에 대해 알고 있어야 합니다. 아래와 같은 형식을 URI 템플릿이라고 부릅니다. {} 중괄호를 사용해 변수를 넣을 수 있는 부분(URI 변수)이며 나머지는 고정값입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://api.example.com/{version}/users/{userId}/details&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URI 변수에 해당하는 중괄호 값인 {version}과 {userId}는 어떤 값이든 들어갈 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://api.example.com/v1/users/1/details&lt;br /&gt;https://api.example.com/v2/users/33/details&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이와 같이 URI를 특정 형식에 맞춰 동적으로 구성 가능한 기술을 URI 템플릿이라고 이해하시면 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 EncodingMode에 대해 알아봅시다. 종류는 아래와 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;TEMPLATE_AND_VALUES&lt;/b&gt;&lt;/span&gt;: URI 템플릿과 URI 변수 모두 인코딩 합니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;VALUES_ONLY&lt;/b&gt;&lt;/span&gt;: URI 템플릿을 인코딩하지 않고 URI 변수를 인코딩합니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;URI_COMPONENT&lt;/b&gt;&lt;/span&gt;: URI 변수를 적용한 후 URI 컴포넌트를 인코딩합니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;NONE&lt;/b&gt;&lt;/span&gt;: 인코딩을 적용하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestClient는 기본적 설정이&amp;nbsp; &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;TEMPLATE_AND_VALUES &lt;/b&gt;&lt;/span&gt;였으므로, URI 템플릿과 URI 변수 모두 인코딩한다고 보시면 될 것 같습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인코딩 문제?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜론을 PathVariable로 전달&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;종종 외부 API의 Path를 보면 중간에 비밀키를 넣어야 하는 경우가 있습니다. 영문과 숫자만 들어있다면 다행이지만, 특수문자가 들어가는 때도 있습니다.&lt;/li&gt;
&lt;li&gt;메시지 키 은닉을 위해 PathVariable로 messageKey를 전달해봅시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716214909882&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MessageClient {
  @PostExchange(&quot;/v1/services/{messageKey}/messages&quot;)
  MessageResponse sendMessage(
      @PathVariable String messageKey,
      @RequestBody MessageRequest request);
}&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;messageKey에 할당된 값을 &lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;example:sms:XXXXXXXXXX:myApp&lt;/span&gt;&lt;/b&gt; 라고 해봅시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716215154752&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional
public class MessageService {

    private final String MESSAGE_KEY;
    private final MessageClient messageClient;
    
    public MessageService(
        @Value(&quot;${external-api.message.key}&quot;) final String MESSAGE_KEY,
        final MessageClient messageClient) {
        this.MESSAGE_KEY = MESSAGE_KEY;
        this.messageClient = messageClient;
    }
    
    public MessageResponse sendMessage(final Message Request) {
        return messageClient(MESSAGE_KEY, request);
    }
}&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;% 인코딩&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문제는 인코딩에 있습니다. 위의 경로로 실제 URI가 인코딩된 모습을 보면 아래와 같습니다.&lt;/li&gt;
&lt;li&gt;콜론(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt; :&lt;/b&gt; &lt;/span&gt;)이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;b&gt;%3A&lt;/b&gt;&lt;/span&gt;로 인코딩되면서 SecretKey 값이 변형되어 UNAUTHORIZED 에러가 발생했기 때문에 제대로 동작하지 않았던 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://api.example.com/v1/services/example%3Asms%3AXXXXXXXXXX%3AmyApp/messages&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인코딩 문제를 해결하려면?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DefaultUriBuilderFactory의 &lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;EncodingMode를 변경&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래가 기존의 RestClient를 생성하는 로직이었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716216559561&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private RestClient createRestClient(String baseUrl) {
    return RestClient.builder()
        .baseUrl(baseUrl)
        .build();
}&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;DefaultUriBuilderFactory를 선언하고 setEncodingMode 메소드를 사용해 EncodingMode.NONE을 설정한 뒤, uriBuilderFactory 메소드를 통해 RsetClient에 추가해줬습니다. 이제 URI가 인코딩되지 않기 때문에 SecretKey가 변형되지 않고 잘 보내질 겁니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716216630670&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private RestClient createRestClient(String baseUrl) {
    DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
    uriBuilderFactory.setEncodingMode(EncodingMode.NONE);
    
    return RestClient.builder()
        .uriBuilderFactory(uriBuilderFactory)
        .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;EncodingMode를 None으로 두면 문제가 없을까요?&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;QueryParam으로 한글이나 특수문자가 넘어오면 문제가 생길 수 있습니다.&lt;/b&gt; (하지만 제가 경험한 바로는 외부 API에서 params 값으로 한글이나 특수문자를 넘겨받기를 원하는 스펙이 보편적인 경우는 아닌 것 같습니다.)&lt;/li&gt;
&lt;li&gt;이 때는 RestClient로 전달하기 전에 미리 Encoding을 해주고 변수로 넘겨주면 됩니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716216399465&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface PaymentClient {

    @PostExchange(&quot;/payment&quot;)
    ExampleResponse confirmPayment(@RequestParam String param);
}&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;UriEncoder를 통해 한글을 인코딩해줘서 RestClient에게 넘기면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1716216417686&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ExternalPaymentService {

    private final PaymentClient paymentClient;

    public PaymentExample example(final ExampleRequest request) {
      String encodedKorean = UriEncoder.encode(request.koreanStr()); // 한글 및 특수문자 인코딩
      return paymentClient.confirmPayment(encodedKorean);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관련 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a title=&quot;restclient 글&quot; href=&quot;https://myvelop.tistory.com/217&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링의 외부 API 호출, 그리고 RestClient&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Encoding</category>
      <category>RESTClient</category>
      <category>Spring</category>
      <category>URI</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/228</guid>
      <comments>https://myvelop.tistory.com/228#entry228comment</comments>
      <pubDate>Tue, 21 May 2024 00:01:13 +0900</pubDate>
    </item>
    <item>
      <title>[글또] 글또 9기 회고</title>
      <link>https://myvelop.tistory.com/227</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또를 시작했던 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집-회사만 반복하던 일상. 우물 안 개구리가 되어 간다는 생각을 떨칠 수 없었다.&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;글또 지원은 그 때 상황을 타개할 수 있는 가장 좋은 방법이었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글쓰기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 9기를 시작하기 앞서 목표로 설정했던 &quot;미제출하지 않기&quot;는 달성했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제출한 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 12회차 중 2번 패스했지만 나머지 10회차의 글은 제출했다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅ 1회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/218&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;글또 9기 발돋움&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 2&lt;/span&gt;회차 제출 | &lt;a href=&quot;https://myvelop.tistory.com/217&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링의 외부 API 호출, 그리고 RestClient&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 3&lt;/span&gt;회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| &lt;a href=&quot;https://myvelop.tistory.com/216&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;신입 개발자! 회사와 함께 성장하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 4&lt;/span&gt;회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| &lt;a href=&quot;https://myvelop.tistory.com/220&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Security] LogoutFilter를 구현할 때 생길 수 있는 문제&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 5&lt;/span&gt;회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;|&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://myvelop.tistory.com/221&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[글또] 1차 글쓰기 세미나, 그리고 프로세스 1.0&lt;/a&gt;&lt;/span&gt;&lt;a href=&quot;https://myvelop.tistory.com/221&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&amp;nbsp;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;⏩&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 6&lt;/span&gt;회차 패스&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 7&lt;/span&gt;회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| &lt;a href=&quot;https://myvelop.tistory.com/222&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Udemy - Java 멀티스레딩, 병행성 및 최적화&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;⏩&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 8&lt;/span&gt;회차 패스&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 9&lt;/span&gt;회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| &lt;a href=&quot;https://myvelop.tistory.com/223&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Test] Persisence Layer Test와 테스트에 대한 고찰&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;10회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;|&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://myvelop.tistory.com/224&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Test] 비즈니스 로직 테스트: 읽기 쉽고 효율적인 단위테스트&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;11회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;|&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://myvelop.tistory.com/226&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Test] Testcontainers를 사용한 DB 테스트&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;✅ 12회차 제출&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;| [글또] 글또 9기 회고&lt;/span&gt;&lt;/span&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;글또에 제출하진 않았지만 개인적으로 2023년 회고를 작성했다. 글또에서 활동하는 &lt;b&gt;6개월 동안 총 11개의 글을 작성&lt;/b&gt;했다.&lt;/p&gt;
&lt;figure id=&quot;og_1715416061983&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;2023년 신입개발자의 회고&quot; data-og-description=&quot;조금 늦었지만 2023년 회고를 올려봅니다 작년 계획 책 읽기 To acquire the habit of reading is to construct for yourself a refuge from almost all the miseries of life. 독서하는 습관은 인생의 거의 모든 불행으로부터 자&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/219&quot; data-og-url=&quot;https://myvelop.tistory.com/219&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AAgRZ/hyV2yM4Tkk/WzFR3sRqJOiF4C1ADcDn21/img.png?width=800&amp;amp;height=187&amp;amp;face=0_0_800_187,https://scrap.kakaocdn.net/dn/D3BwA/hyV2ucPVEO/j04k2ugWSU2ahaFGsKA1H1/img.png?width=800&amp;amp;height=187&amp;amp;face=0_0_800_187,https://scrap.kakaocdn.net/dn/bz4B1t/hyV2q9jyCb/ITuml1Pv877ku1ZZlEloD1/img.png?width=1652&amp;amp;height=1326&amp;amp;face=0_0_1652_1326&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/219&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/219&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AAgRZ/hyV2yM4Tkk/WzFR3sRqJOiF4C1ADcDn21/img.png?width=800&amp;amp;height=187&amp;amp;face=0_0_800_187,https://scrap.kakaocdn.net/dn/D3BwA/hyV2ucPVEO/j04k2ugWSU2ahaFGsKA1H1/img.png?width=800&amp;amp;height=187&amp;amp;face=0_0_800_187,https://scrap.kakaocdn.net/dn/bz4B1t/hyV2q9jyCb/ITuml1Pv877ku1ZZlEloD1/img.png?width=1652&amp;amp;height=1326&amp;amp;face=0_0_1652_1326');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2023년 신입개발자의 회고&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;조금 늦었지만 2023년 회고를 올려봅니다 작년 계획 책 읽기 To acquire the habit of reading is to construct for yourself a refuge from almost all the miseries of life. 독서하는 습관은 인생의 거의 모든 불행으로부터 자&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큐레이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐레이션에는 총 2번 뽑혔다. 처음 글또 시작할 때는 큐레이션에 뽑힐 거라는 기대가 전혀 없었다. 글또에 참여하시는 분의 수는 무려 400명 이상이었고 글을 잘 쓰시는 분들도 많았다. 내 글을 잘 써서 큐레이션에 선정되어 보자는 욕심보다는 다른 분들의 글을 보고 배워야겠다는 생각이 강했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 잘 쓰고자 하는 욕심은 있었기 때문에 &lt;b&gt;글쓰기 세미나에 참여하고 큐레이션에 뽑힌 분들의 글을 보면서 좋은 글이 무엇인지에 대한 고민&lt;/b&gt;을 시작했다. 또 그런 글을 적기 위해 글을 쓰는 내내 셀프 피드백을 진행하고 글을 제출한 이후에도 퇴고했다. 덕분에 예전보다 글이 좋아진 게 스스로도 느껴졌고, 그 결과 큐레이션에도 뽑힐 수 있었던 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWe5I2/btsHmFfO9q7/BlpW8XGNApKbnqMfP5aPLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWe5I2/btsHmFfO9q7/BlpW8XGNApKbnqMfP5aPLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWe5I2/btsHmFfO9q7/BlpW8XGNApKbnqMfP5aPLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWe5I2%2FbtsHmFfO9q7%2FBlpW8XGNApKbnqMfP5aPLK%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;604&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;604&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/38QYG/btsHmrib6P4/1hlRj5BVdMO7ZQ4qxFFYA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/38QYG/btsHmrib6P4/1hlRj5BVdMO7ZQ4qxFFYA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/38QYG/btsHmrib6P4/1hlRj5BVdMO7ZQ4qxFFYA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F38QYG%2FbtsHmrib6P4%2F1hlRj5BVdMO7ZQ4qxFFYA1%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;388&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;388&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또 커뮤니티 활동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1차 글쓰기 세미나&lt;/h3&gt;
&lt;figure id=&quot;og_1715411508133&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[글또] 1차 글쓰기 세미나, 그리고 프로세스 1.0&quot; data-og-description=&quot;**글또 9기의 활동 내용입니다. 1차 글쓰기 세미나에 참여 및 과제를 수행했습니다. 세미나 후기은 대체로 성윤님의 발표자료를 바탕으로 작성되었으며, 글의 중간중간 제 생각을 더해봤습니다.*&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/221&quot; data-og-url=&quot;https://myvelop.tistory.com/221&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/DkSKa/hyV2ssxsau/yIUccMVr37Qq9d0YT0VwK1/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/bAJ7wr/hyV2wuVjLe/6oPwYwrDRk9lhzET4WVdHK/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/YEEOE/hyV2sTAGuQ/tHRXWVLeNtPOOumPYwHhM1/img.png?width=1429&amp;amp;height=784&amp;amp;face=0_0_1429_784&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/221&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/221&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/DkSKa/hyV2ssxsau/yIUccMVr37Qq9d0YT0VwK1/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/bAJ7wr/hyV2wuVjLe/6oPwYwrDRk9lhzET4WVdHK/img.png?width=800&amp;amp;height=438&amp;amp;face=0_0_800_438,https://scrap.kakaocdn.net/dn/YEEOE/hyV2sTAGuQ/tHRXWVLeNtPOOumPYwHhM1/img.png?width=1429&amp;amp;height=784&amp;amp;face=0_0_1429_784');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[글또] 1차 글쓰기 세미나, 그리고 프로세스 1.0&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;**글또 9기의 활동 내용입니다. 1차 글쓰기 세미나에 참여 및 과제를 수행했습니다. 세미나 후기은 대체로 성윤님의 발표자료를 바탕으로 작성되었으며, 글의 중간중간 제 생각을 더해봤습니다.*&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1월 14일에 온라인으로 진행된 1차 글쓰기 세미나가 진행됐다.&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;글쓰기 프로세스&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;/li&gt;
&lt;li&gt;&lt;b&gt;글의 핵심 주제&lt;/b&gt;를 정하고, 그 주제를 잘 표현하기 위해 고민하기 시작했다.&lt;/li&gt;
&lt;li&gt;블로그 글을 종종 확인하며 &lt;b&gt;꾸준히 퇴고&lt;/b&gt;하기 시작했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽게도 2차 글쓰기 세미나와 개인 일정이 겹쳐 참여하지 못했다. 2차 글쓰기 세미나에 참여하면 블로그 피드백을 받을 수 있는 기회가 있었는데 그걸 놓친 건 조금 아쉬웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백엔드/인프라 빌리지 반상회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3월 14일에 열렸던 백엔드/인프라 빌리지 반상회에 참여했다. 무려 96분의 개발자가 행사에 참여하셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC와 Kafka를 통한 데이터 처리에 대한 발표를 들을 수 있었고, 발표가 끝난 후에는 네트워킹 세션이 진행됐다. 8명으로 구성된 팀에서 대화를 나눴다. 시간이 너무 짧아 아쉬웠지만 비슷한 고민을 가지고 있는 사람들과 만나 얘기를 나눌 수 있어 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커피챗&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커피챗은 1회 참여했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변명을 해보자면, 글또를 시작할 때쯤 회사에서 쇼핑몰 오픈을 목표로 프로젝트를 시작했으며 한창 바쁜 2달 동안은 매일 오후 10~12시에 퇴근하고 주말에도 회사에 나갔었기 때문에 시간이 부족했고 정신적으로도 지쳐있었다.&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;3월 24일에 백엔드_j 팀원 분들과 만나뵐 수 있었다. 다들 개발을 좋아하는 분들이었고 자기 삶을 열심히 사는 분들이었다. 대화를 나누면서 자극도 많이 받았고 기술적으로도 많이 배웠다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;글또 9기를 마치며&lt;/h2&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;/li&gt;
&lt;li&gt;회사 실무에 적용하고 깨달은 점들을 따로 기록해둔다.&lt;/li&gt;
&lt;li&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;&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;글쓰기의 관점에서 얻은 것이 많지만 커뮤니티 활동은 거의 참여하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적극적으로 활동하지 못했고 그 때문에 후회가 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 10기를 시작할 때, 새로운 목표와 새로운 마음가짐으로 돌아오고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동/글또</category>
      <category>글또</category>
      <category>글쓰기</category>
      <category>블로그</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/227</guid>
      <comments>https://myvelop.tistory.com/227#entry227comment</comments>
      <pubDate>Sat, 11 May 2024 17:40:59 +0900</pubDate>
    </item>
    <item>
      <title>[Test] Testcontainers를 사용한 DB 테스트</title>
      <link>https://myvelop.tistory.com/226</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UkIbO/btsGZU45tlF/h4Sar9yZwKiXCWkFvUnFF1/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UkIbO/btsGZU45tlF/h4Sar9yZwKiXCWkFvUnFF1/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UkIbO/btsGZU45tlF/h4Sar9yZwKiXCWkFvUnFF1/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUkIbO%2FbtsGZU45tlF%2Fh4Sar9yZwKiXCWkFvUnFF1%2Ftfile.svg&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;778&quot; height=&quot;204&quot; data-origin-width=&quot;778&quot; data-origin-height=&quot;204&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;h2 data-ke-size=&quot;size26&quot;&gt;1. TestContainers란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 프로젝트를 할 때 멱등성 있는 테스트를 구성하기 위해 테스트 DB를 따로 띄워 테스트를 실행했던 적이 있습니다. 그 때는 Testcontainers의 존재를 몰랐기에 Docker Compose로 테스트 DB를 띄워 테스트를 실행해줬습니다. 테스트 DB 컨테이너를 계속 띄워 놓기엔 때문에 컴퓨터 리소스 낭비도 심했기 때문에 통합 테스트를 실행해야할 때마다 테스트 DB 컨테이너를 띄워주고 테스트가 종료되면 컨테이너를 내리는 식으로 진행되었는데 정말 귀찮은 작업이었습니다.&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;이런 작업을 자동화해주는 Testcontainers입니다. 똑같이 Docker 환경을 사용하며 테스트가 실행될 때 실제 DB와 같이 돌아가는 DB 컨테이너를 띄워주고, 테스트가 종료되면 자동으로 컨테이너를 내립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;멱등성이란?&lt;/b&gt;&lt;br /&gt;컴퓨터 과학에서 멱등성이 보장된다는 것은 연산을 여러 차례 적용해도 똑같은 결과가 보장된다는 뜻입니다. 그런데 테스트 DB와 실제 운영 DB가 다르다면 멱등성이 보장되는 테스트가 아닙니다. 환경이 다르기에 테스트가 실패할 가능성이 존재합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했다시피 Testcontainers는 도커 환경에서 실행되기 때문에 로컬에서 실행하려면 Docker Deamon이 필요하기에 Docker를 설치해줘야 합니다.Testcontainers를 실행시키기 위해 Docker Desktop을 설치하는 하고 Testcontainers 추가적인 기능을 사용하기 위해 Docker Compose를 설치해주겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Docker Desktop&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Docker Desktop이라는 프로그램을 아래 링크에서 설치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크: &lt;a href=&quot;https://www.docker.com/products/docker-desktop/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.docker.com/products/docker-desktop/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop을 설치하면 자동으로 도커 명령어도 설치됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Docker Compose&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컴포즈의 설치는&amp;nbsp;&lt;a href=&quot;https://docs.docker.com/compose/install/standalone/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docker.docs&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; 도커 컴포즈를 설치해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714200645821&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ curl -SL https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714200837982&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo chmod +x /usr/local/bin/docker-compose&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;span style=&quot;background-color: #e1e2e6; color: #000000; text-align: left;&quot;&gt;docker-compose&lt;/span&gt; 커맨드가 실행되지 않는다면 심볼릭 링크를 지정해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714200800996&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose&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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714200829638&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ docker-compose --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Testcontainers 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Testcontainers의 공식문서의 &lt;a href=&quot;https://testcontainers.com/guides/getting-started-with-testcontainers-for-java/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;guide 부분&lt;/a&gt;을 보면 의존성 설정에 대한 상세 설명이 나와 있습니다. 아래와 같이 gradle 의존성을 추가해주면&amp;nbsp; 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714201847068&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    ...

    // Testcontainers
    testImplementation &quot;org.testcontainers:junit-jupiter:1.19.7&quot;
    testImplementation &quot;org.testcontainers:postgresql:1.19.7&quot;
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 데이터베이스 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Testcontainers에서는 수 많은 &lt;a href=&quot;https://testcontainers.com/modules/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모듈&lt;/a&gt;을 지원합니다. 위의 예시에는 postgresql을 예시로 들었지만, 다른 데이터베이스를 사용할 수 있습니다. 예를 들어 MySQL 모듈을 사용하고 싶다면  공식문서에서 찾아 MySQL Testcontainers 의존성을 추가해줄 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714202059707&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;testImplementation &quot;org.testcontainers:mysql:1.19.7&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Testcontainers를 사용하는 다양한 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Testcontainers를 사용하기에 앞서 로컬 환경이라면 Docker Desktop을 실행해줍니다. 도커 컨테이너를 실행할 수 있는 환경이 구성되어 있어야 Testcontainers가 정상적으로 동작할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 준비가 됐다면 아래 코드 예시를 통해 Testcontainers를 어떻게 사용할 수 있는지 살펴봅시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 특정 모듈 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Testcontainers의 의존성과 데이터베이스 모듈 의존성을 추가했다면 Testcontainers에서 지원하는 어노테이션과 DB 모듈 컨테이너를 사용할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;src/test/resources&lt;/b&gt; 디렉토리의 &lt;b&gt;application.yaml 파일&lt;/b&gt;을 아래와 같이 구성해보겠습니다. 구성하는 방법은 &lt;a href=&quot;https://java.testcontainers.org/modules/databases/jdbc/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에 설명되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;jdbc&lt;/span&gt;라는 키워드 뒤에 tc라는 키워드를 넣으면 호스트 이름, 포트 및 데이터베이스 이름이 무시되고 테스트 컨테이너를 알아서 찾아가게 만들 수 있습니다. 따라서 특이하게 host-less URI 라고 지칭되는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;///&lt;/span&gt; 를 사용합니다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;tc&lt;/span&gt; 키워드를 가진 URL을 JDBC 드라이버가 사용할 수 있도록 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;org.testcontainers.jdbc.ContainerDatabaseDriver&lt;/span&gt;를 설정해줬습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/test/resources/application.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714202841489&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
    datasource:
        url: jdbc:tc:postgresql:///testdb
        driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Testcontainer&lt;/span&gt;와 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Container&lt;/span&gt;는 Testcontainers의 junit-jupiter 의존성으로 추가된 기능으로 DB 컨테이너의 생성을 도와줍니다. 반면 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;PostgreSQLContainer&lt;/span&gt;는 Testcontainer의 Postgre Module 의존성으로 추가된 기능입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;test code&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714201574660&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
@Testcontainers // JUnit5 확장팩으로 테스트 클래스에 @Container를 사용한 필드를 찾아서 컨테이너 라이프사이클 관련 메소드를 실행해준다.
public class OrderServiceIntegrationTest {

    @Autowired
    OrderRepository orderRepository;
    
    @Container // 인스턴스 필드에 사용하면 모든 테스트마다 컨테이너를 재시작하고, 스태틱 필드에 사용하면 클래스 내부 모든 테스트에서 동일한 컨테이너를 재사용한다.
    private static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer().withDatabaseName(&quot;testdb&quot;);
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Testcontainers에서 지원하지 않는 DB 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Testcontainers에서는 많은 데이터베이스 모듈을 지원하지만 지원하지 않는 모듈도 존재합니다. 여기서 &lt;b&gt;Testcontainers는 도커 환경을 사용한다는 점을 주목해봅시다.&lt;/b&gt; 도커 환경을 사용하기 때문에 &lt;b&gt;데이터베이스의 이미지 또한 DockerHub에서 가져와&lt;/b&gt; 컨테이너를 만듭니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GenericContainer 클래스를 사용하면 원하는 이미지를 가져와 커스터마이징된 테스트 컨테이너를 구성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1714203247276&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
@Testcontainers
public class OrderServiceIntegrationTest {

    @Autowired
    OrderRepository orderRepository;
    
    @Container
    private static GenericContainer postgreSQLContainer = new GenericContainer(&quot;postgres&quot;)
        .withExposedPorts(5432);
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 도커 컴포즈를 사용해 Testcontainers 띄우기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose 파일을 사용해 테스트 컨테이너를 띄울 수 있습니다. 위의 방식보다 시간이 조금 더 걸립니다.&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;먼저 도커 컴포즈 yaml 파일을 만듭니다. 여기서 주의할 점이 있습니다. 원래 docker-compose.yaml 파일을 구성할 때 port 매핑을 설정해주지만 &lt;b&gt;Testcontinaers에서 사용할 docker-compose 파일에서는 port를 매핑해주지 않는 것&lt;/b&gt;이 좋습니다. 해당 포트를 사용하는 프로그램이 존재하면 테스트가 실행되지 않기 때문입니다. &lt;b&gt;Testcontainers에서는 사용 가능한 포트를 자동으로 매핑&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;src/test/resources/docker-compose.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714206325130&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3.9&quot;

services:
  test-db:
    image: postgres
    ports:
      - 5432
    environment:
      POSTGRES_PASSWORD: 1234
      POSTGRES_USER: test
      POSTGRES_DB: testdb&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;test code&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1714206838637&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
@Testcontainers
public class OrderServiceIntegrationTest {

    @Autowired
    OrderRepository orderRepository;
    
    @Container
    private static DockerComposeContainer postgreSQLContainer =
        new DockerComposeContainer(new File(&quot;src/test/resources/docker-compose.yaml&quot;));
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컴포즈를 사용하면 속도가 더 느린데 그 이유는 아래와 같이 추정됩니다. 도커 컴포즈를 사용하지 않는 방법(4-1, 4-2 방식)으로 테스트를 실행하면 testcontainers 컨테이너와 postgres 컨테이너만 있으면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ziy4R/btsGYMN0pb5/NdkIA4gXh2RKfBNEGkkU2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ziy4R/btsGYMN0pb5/NdkIA4gXh2RKfBNEGkkU2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ziy4R/btsGYMN0pb5/NdkIA4gXh2RKfBNEGkkU2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZiy4R%2FbtsGYMN0pb5%2FNdkIA4gXh2RKfBNEGkkU2K%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;2028&quot; height=&quot;812&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;812&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;반면 도커 컴포즈를 사용해 테스트 컨테이너를 띄우는 경우 &lt;b&gt;도커 컴포즈 컨테이너가 추가적으로 실행&lt;/b&gt;되기 때문에 다른 방식보다 느립니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/usLmE/btsG0Tkf8w0/1GEJWnXywbx13taQBCGFk1/img.png&quot; data-origin-width=&quot;2072&quot; data-origin-height=&quot;1054&quot; data-is-animation=&quot;false&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Testcontainers는 확실히 느립니다. 메모리를 사용하는 H2 In-Memory DB와는 다르게 일반 DB는 디스크를 거쳐야하기에 느릴 수 밖에 없습니다. 게다가 Testcontainers는 컨테이너를 띄우는 시간도 필요하기에 시간이 더 걸립니다.&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;하지만 빠르다는 이유만으로 섣불리 H2 In-memory DB를 사용했다가 실제 운영 환경에서는 동작하지 않거나 SQL 함수 호환 문제로 인해 테스트가 실패해버리는 경우가 발생할 수 있습니다.&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;Testcontainers를 더 빠르게 사용하는 방법을 사용하면 충분히 개선이 가능하다고 합니다. Testcontainers를 사용하는 것이 멱등성&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;있는 테스트를 보장하기&lt;/span&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;&amp;nbsp;&lt;/p&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://myvelop.tistory.com/223&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Test] Persistence Layer Test와 테스트에 대한 고찰&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://testcontainers.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Testcontainers 공식문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/course/the-java-application-test&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런 강의&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@zxshinxz/Testcontainers-%EC%82%AC%EC%9A%A9%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Testcontainers 사용 기초&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/riiid-teamblog-kr/testcontainer-%EB%A1%9C-%EB%A9%B1%EB%93%B1%EC%84%B1%EC%9E%88%EB%8A%94-integration-test-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-4a6287551a31&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TestContainer로 멱등성있는 integration test 환경 구축하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Test</category>
      <category>MySQL</category>
      <category>Spring</category>
      <category>TEST</category>
      <category>Testcontainers</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/226</guid>
      <comments>https://myvelop.tistory.com/226#entry226comment</comments>
      <pubDate>Sat, 27 Apr 2024 18:03:15 +0900</pubDate>
    </item>
    <item>
      <title>[Test] 비즈니스 로직 테스트: 읽기 쉽고 효율적인 단위테스트</title>
      <link>https://myvelop.tistory.com/224</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 비즈니스 로직을 테스트하기 전에 알면 좋은 지식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. Layered Architecture와 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 보기 전에 아래 글을 먼저 보고 오는 것을 추천한다. &lt;b&gt;1-1.Layered Architecture&lt;/b&gt;와 &lt;b&gt;1-2.테스트의 분류&lt;/b&gt;만 읽고 와도 충분하다.&lt;br /&gt;&lt;a href=&quot;https://myvelop.tistory.com/223&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://myvelop.tistory.com/223&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[Spring] Persistence Layer Test와 테스트에 대한 고찰&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;단순히 Persistence Layer를 테스트하는 방법만을 서술하는 것이 아닌, 영속 계층을 테스트해야 하는 이유에 대해 정리하고 어떤 방식으로 테스트하는 것이 더 좋은 방법인지 고민한 내용을 정리해&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/223&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/NpdVD/hyVMMR3zVz/cUIs1UK0iARDkfUhOpwxQ0/img.png?width=800&amp;amp;height=595&amp;amp;face=0_0_800_595,https://scrap.kakaocdn.net/dn/dUPtuT/hyVMVO0FiR/wtaoAi2ti1CaQhkGOsenk1/img.png?width=800&amp;amp;height=595&amp;amp;face=0_0_800_595,https://scrap.kakaocdn.net/dn/cj1JOr/hyVMQUtqau/TYyXEcaBKPKS8zPHQMgUp0/img.png?width=750&amp;amp;height=558&amp;amp;face=0_0_750_558&quot; data-og-url=&quot;https://myvelop.tistory.com/223&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/223&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/223&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/NpdVD/hyVMMR3zVz/cUIs1UK0iARDkfUhOpwxQ0/img.png?width=800&amp;amp;height=595&amp;amp;face=0_0_800_595,https://scrap.kakaocdn.net/dn/dUPtuT/hyVMVO0FiR/wtaoAi2ti1CaQhkGOsenk1/img.png?width=800&amp;amp;height=595&amp;amp;face=0_0_800_595,https://scrap.kakaocdn.net/dn/cj1JOr/hyVMQUtqau/TYyXEcaBKPKS8zPHQMgUp0/img.png?width=750&amp;amp;height=558&amp;amp;face=0_0_750_558');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] Persistence Layer Test와 테스트에 대한 고찰&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;단순히 Persistence Layer를 테스트하는 방법만을 서술하는 것이 아닌, 영속 계층을 테스트해야 하는 이유에 대해 정리하고 어떤 방식으로 테스트하는 것이 더 좋은 방법인지 고민한 내용을 정리해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;간단히 요약하자면, Business Layer는 비즈니스 로직을 수행하는 계층으로 Business Layer 테스트는 로직이 잘 수행되는지를 중점에 두고 수행하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. BDD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BDD란 Behavior-Driven Development의 약자로 행위 주도 개발을 의미한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;테스트할 때 상태의 변화에 집중하며 Given, When, Then으로 구조를 가진 시나리오를 만들어 테스트하는 것을 권장한다. 테스트 케이스의 시나리오는 개발자가 아닌 사람이 봐도 이해할 수 있을 정도로 만드는 것이 좋다.&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;Feature&lt;/b&gt; : 테스트에 대상의 기능/책임을 명시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Scenario&lt;/b&gt; : 테스트 목적에 대한 상황을 설명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Given&lt;/b&gt;: 시나리오 진행에 필요한 값을 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;When&lt;/b&gt;: 시나리오를 진행하는데 필요한 조건을 명시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Then&lt;/b&gt;: 시나리오가 끝났을 때의 상태 변화(결과)를 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void order() { // Feature &amp;amp; Scenario
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3. Classicist vs Mockist&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위테스트의&amp;nbsp;&lt;b&gt;격리(Solitary)는 무엇보다 중요하기 때문에 연관된 모든 객체에 Test Double을 사용해야 한다고 주장하는 사람들을 Mockist&lt;/b&gt;라고 부른다. 반면, 최대한 실제 객체를 사용하되 사용이 어려운 경우에만 Test Double을 사용해&amp;nbsp;&lt;b&gt;협동(Sociable) 테스트를 만드는 것이 좋다고 생각하는 사람들을 Classicist&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfbq20/btsGCpYBgZY/gzC5i6lbiyW4MIHidifHKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfbq20/btsGCpYBgZY/gzC5i6lbiyW4MIHidifHKk/img.png&quot; data-alt=&quot;출처:&amp;amp;amp;amp;amp;nbsp;https://martinfowler.com/bliki/UnitTest.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfbq20/btsGCpYBgZY/gzC5i6lbiyW4MIHidifHKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfbq20%2FbtsGCpYBgZY%2FgzC5i6lbiyW4MIHidifHKk%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; alt=&quot;classicist vs mockist&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;196&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처:&amp;amp;amp;amp;nbsp;https://martinfowler.com/bliki/UnitTest.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;예를 들어 OrderService가 PointService, PaymentService에 의존하는 코드의 테스트를 작성한다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final OrderRepository orderRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final PointService pointService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final PaymentService paymentService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Order order(OrderRequest request) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Point point = pointService.usePoint(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Payment payment = paymentService.saveOf(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return orderRepository.save(Order.of(request, point, payment));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Classicist의 코드 작성 예시이다. OrderService와 의존관계인 PointService와 PaymentService의 객체는 그대로 사용하고 그대로 사용하기 어려운 Repository만 Fake로 구현해 테스트를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderService orderService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FakeOrderRepository orderRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PointService pointService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FakePointRepository pointRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PaymentService paymentService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FakePaymentRepository paymentRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@BeforeEach
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void setUp() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderRepository = new FakeOrderRepository();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pointRepository = new FakePointRepository();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;paymentRepository = new FakePaymentRepository();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pointService = new PointService(pointRepository);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;paymentService = new PaymentService(paymentRepository);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderService = new OrderService(orderRepository, pointService, paymentService);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 테스트 작성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;반면 Mockist라면 아래와 같이 코드를 만들 것이다. 테스트 과정에서 발생하는 요청의 모든 반환 값은 Test Double을 통해 만든다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@extendwith(mockitoextension.class)
class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@InjectMocks
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderService orderService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Mock
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PointService pointService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Mock
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;PaymentService paymentService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 테스트 작성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;위의 2가지 방법은 정답은 없다. 본인이 맞다고 생각하는 방식을 선택해 테스트를 작성하면 된다. (cf. 마틴 파울러 옹은 2가지 방식 모두 존중한다고 했고, 본인은 classicist라고 함.)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Test Double&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test Double. 테스트 대역. 테스트하려고 하는 객체와 의존관계가 있는 객체의 모조품을 만들어 대역을 세우는 것을 의미한다. 보통 Test Double은 아래와 같은 경우에 사용된다.&lt;/p&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;테스트가 어려운 외부 API를 다른 것으로 대체하고 싶은 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 대역의 종류에는 Dummy, Fake, Stub, Mock, Spy 등이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Dummy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법이다. Test Double로 작성될 객체의 내부 기능이 필요하지 않을 때 사용하는데, 보통 void 반환값을 지닌 함수를 텅빈 함수로 만들어 사용하게 한다. 서비스가 인터페이스를 의존하고 있어야 사용할 수 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;아래는 메일을 전송하는 객체를 Dummy로 만든 예시이다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface MailSender {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void sendMail(String subject, String content);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class DummyMailSender implements MailSender {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void sendMail(String subject, String content) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Fake&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dummy와는 다르게 실제 동작하는 코드를 가지고 있는 Test Double이다. 실제 운영환경처럼 정교하게 동작하지는 않지만 흉내내는 정도로 만들어 사용한다. Dummy와 마찬가지로 서비스가 인터페이스를 의존하고 있어야 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface OrderRepository {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order save(Order order);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class FakeOrderRepository implements OrderRepository {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public long autoIncrement = 1;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public List&amp;lt;Order&amp;gt; orders = new ArrayList&amp;lt;&amp;gt;();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Order save(Order order) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ReflectionTestUtils.setFiled(order, &quot;id&quot;, autoIncrement++);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orders.add(order);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return order;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. Stub&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 준비된 반환값을 전달하는 객체이다. Dummy와 Fake의 중간이라고 생각하면 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class StubOrderRepository implements OrderRepository {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Order save(Order order) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	return new Order(....);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4. Mock&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 예시로 보여준 것들과는 다르게 서비스가 인터페이스를 의존하지 않고 있더라도 사용할 수 있다. Mockito 객체를 사용해 손쉽게 모킹을 사용할 수 있다. 어노테이션을 사용해 모킹 객체를 만드는 방식은 뒤에서 설명하도록 하겠다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredConstructor
public class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final OrderRepository orderRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Order order(OrderRequest request) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = Order.of(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return orderRepository.save(order);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class OrderServiceTest {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderService orderService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRepository orderRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문 요청을 받아 주문을 저장할 수 있다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderRepository = Mockito.mock(OrderRepository.class);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderService = new OrderService(orderRepository);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRequest request = createOrderRequest();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order returnOrder = createOrder(1L);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Mockito.when(orderRepository.save(any())).thenReturn(order);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = orderService.order(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assertThat(result.getId()).isEqualTo(1L);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OrderRequest createOrderRequest() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	return OrderRequest.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Order createOrder(final Long id) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	return Order.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;대충 보면 Mock과 Stub이 다를 게 없어보이지만 엄연히 다른 개념이다. Stub은 상태 검증(State Verification)을 하기 위한 수단으로 어떤 기능을 요청했을 때 내부적인 상태가 어떻게 바뀌었는지에 집중한다. 반면 Mock은 행위 검증(Behavior Verification)에 사용되는데, 어떤 메소드가 실행했을 때 어떤 결과가 Return되는지가 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-5. Spy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stub의 역할을 하면서 호출될 때마다 기록을 하는 객체이다. 특정 메소드가 호출되었는지 여부를 확인할 수 있는 Test Double이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class SpyMailSender implements MailSender {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private int sendMailCallCount = 0;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void sendMail(String subject, String content) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callSendMailCount++;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public int getSendMailCallCount() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return this.sendMailCallCount;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Test Double을 사용해 단위테스트 작성해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Test Double을 사용해 단위테스트(소형테스트)를 작성하는 방법에 대해 살펴보자.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. @ExtendWith로 더 쉬운 Mock 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 Mockito를 사용해 간단하게 Mock 객체 만드는 방법을 알아봤다. 확장 모듈을 가져오는 @ExtendWith을 사용하면 Mockito를 간편하게 사용할 수 있다.&amp;nbsp; &lt;span style=&quot;color: #0c0d0e;&quot;&gt;@ExtendWith(MockitoExtension.class)&lt;/span&gt; 라고 테스트 상단에 선언하면 테스트를 실행할 때 Mockito 확장 모듈을 가져와 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전에는 Mockito.mock이라는 메소드로 정의 사용했던 Mock 객체를 아래와 같이 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Mock&lt;/span&gt; 하나로 만들 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@InjectMocks&lt;/span&gt; 어노테이션을 사용해 자동으로 테스트 대상이 될 객체를 생성하고 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Mock&lt;/span&gt;으로 생성된 객체를 자동으로 주입받게 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Mock&lt;/span&gt;으로 생성된 객체는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Mockito.when()&lt;/span&gt; 메소드를 사용해 모킹할 수 있다. 특정 함수가 호출됐을 때 반환할 값을 지정해주면 테스트가 실행될 때&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@InjectMocks // 생성된 Mock 객체를 자동으로 주입받아 생성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderService orderService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Mock // Mock 객체 자동 생성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRepository orderRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문 요청을 받아 주문을 저장할 수 있다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRequest request = createOrderRequest();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order returnOrder = createOrder(1L);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Mockito.when(orderRepository.save(any())).thenReturn(order);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order result = orderService.order(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assertThat(result.getId()).isEqualTo(1L);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OrderRequest createOrderRequest() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	return OrderRequest.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Order createOrder(final Long id) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	return Order.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .id(id)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. FakeRepository&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서 주입받을 Repository를 한 번쯤 Fake로 구현해볼 것을 권장한다. 다른 Repository를 Fake로 만들면 다른 Test Double을 사용하는 것보다 더 장점이 많다고 생각한다. 내가 생각하는 FakeRepository의 강점은 아래와 같다.&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; (문서로서의 테스트! 자세한 것은 예시에서 설명) Mocking의 경우 프로젝션 반환 값만 작성하면 되기 때문에 테스트 케이스의 문맥 파악이 제대로 되지 않는 경우가 발생할 수 있다. (대신 Fake를 사용할 시 테스트 작성의 난이도가 올라가고 유지보수를 지속적으로 해줘야 한다는 단점이 생길 수 있다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Stub으로 만들 경우 정해진 반환 값만 받을 수 있지만, Fake는 테스트 케이스마다 다른 값을 넣어 조건에 따라 테스트하기 유리&lt;/b&gt;하다. 따라서 실제 환경과 같은 테스트를 구성하는데 도움이 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 Repository 구현체를 의존하지 않고 인터페이스를 의존한다. (의존성 역전)&amp;nbsp; 운영에서 사용할 구현체와 FakeRepository 구현체는 Repository Interface를 구현하며, 실제 운영환경에서는 빈으로 등록된 구현체를 서비스에서 주입받게 될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;585&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vb0mE/btsGCr9TDzS/Ke5NWllaVhZjhqmJvmSHN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vb0mE/btsGCr9TDzS/Ke5NWllaVhZjhqmJvmSHN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vb0mE/btsGCr9TDzS/Ke5NWllaVhZjhqmJvmSHN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvb0mE%2FbtsGCr9TDzS%2FKe5NWllaVhZjhqmJvmSHN1%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; alt=&quot;Dependency Inversion for test&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;442&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;585&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;반면, 단위테스트에서는 Test Double을 사용하기 위해 Service를 생성할 때&amp;nbsp; FakeRepository를 주입해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVg8x8/btsGASVqB9y/EkedMmoXF2HcSPdk1RKxNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVg8x8/btsGASVqB9y/EkedMmoXF2HcSPdk1RKxNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVg8x8/btsGASVqB9y/EkedMmoXF2HcSPdk1RKxNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVg8x8%2FbtsGASVqB9y%2FEkedMmoXF2HcSPdk1RKxNK%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; alt=&quot;Dependency Inversion for test&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;432&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&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;아래와 같이 여러 도메인 객체와 연관된 객체 OrderLine과 이 객체를 사용하기 위한 레포지토리, 서비스가 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class OrderLine {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Long id;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Order order; // N:1 관계
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Item item; // N:1 관계
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private List&amp;lt;OrderLineCompose&amp;gt; composes = new ArrayList&amp;lt;&amp;gt;(); // 1:N 관계
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private long price;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface OrderLineRepository {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderLine save(OrderLine orderLine);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Optional&amp;lt;OrderLine&amp;gt; findById(long id);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;List&amp;lt;OrderLineProjection&amp;gt; findByOrderNumber(String orderNumber);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class OrderLineService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final OrderLineRepository orderLineRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pubilc OrderLineService(OrderLineRepository orderLineRepository) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderLineRepository = orderLineRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public OrderLine order(final OrderRequest request) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderLine orderLine = OrderLine.of(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return orderLineRepository.save(orderLine);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Optional&amp;lt;OrderLine&amp;gt; findById(final Long id) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return orderLineRepository.findById(id);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public List&amp;lt;OrderLineProjection&amp;gt; findByOrderNumber(final String orderNumber) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return orderLineRepository.findByOrderNumber(orderNumber);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;Fake 레포지토리는 OrderLineRepository Interface를 구현했고, 실제 레포지토리와 비슷하게 동작하도록 로직을 작성했다. List 자료구조로 데이터도 저장하고 조건에 따라 값을 반환할 수 있다.&lt;br /&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;save()&lt;/span&gt; 메소드로 값을 저장할 때 리플렉션을 활용했다. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;private&lt;/span&gt;으로 지정되어 있는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;id&lt;/span&gt;에 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;autoIncrement&lt;/span&gt; 값을 넣어주기 위해 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;ReflectionTestUtils.setField()&lt;/span&gt; 메소드를 사용했다. (실제 JPA 환경에서도 Entity 필드에 값을 넣기 위해 리플렉션을 사용)&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class FakeOrderLineRepository implements OrderLineRepository {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public long autoIncrement = 1;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public List&amp;lt;OrderLine&amp;gt; data = new ArrayList&amp;lt;&amp;gt;();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public OrderLine save(final OrderLine orderLine) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ReflectionTestUtils.setField(orderLine, &quot;id&quot;, autoIncrement++);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.data.add(orderLine);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return orderLine;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Optional&amp;lt;OrderLine&amp;gt; findById(final Long id) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return data.stream()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.filter(orderLine -&amp;gt; id.equals(orderLine.getId()))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.findAny();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public List&amp;lt;OrderLineProjection&amp;gt; findByOrderNumber(final String orderNumber) {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;프로젝션을 사용한다면 아래와 같이 설정된 관계들을 가져와 값을 넣어주자. Mocking을 했다면 프로젝션 그 자체를 작성했을 것이고 그대로 반환받았겠지만 &lt;b&gt;Fake를 사용하면&lt;/b&gt;&lt;b&gt; 테스트 케이스에서 관계 설정을 해주는 것이 강제되기 때문에 해당 케이스의 상황을 파악하기 더 수월해지고 이는 문서로서의 테스트를 작성하기 위한 길&lt;/b&gt;이 될 수 있다고 생각한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class OrderLineProjection {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String orderNumber;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final Long orderLineId;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String thumbnail;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final String itemName;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final Integer quantity;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final Long price;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// constructor
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// getter
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class FakeOrderLineRepository implements OrderLineRepository {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private long autoIncrement = 1;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public List&amp;lt;OrderLine&amp;gt; data = new ArrayList&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public OrderLine save(final OrderLine orderLine) {...}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Optional&amp;lt;OrderLine&amp;gt; findById(final Long id) {...}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public List&amp;lt;OrderLineProjection&amp;gt; findByOrderNo(final String orderNumber) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return data.stream()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.filter(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine -&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderNumber.equals(orderLine.getOrder().getOrderNumber()))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.map(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine -&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;new OrderLineProjection(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine.getOrder().getOrderNumber(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine.getId(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine.getItem().getThumbnail(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine.getItem().getName(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine.getOrderLineComposes().stream()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.mapToInt(OrderLineCompose::getQuantity)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.sum(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLine.getPrice()))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.collect(Collectors.toList());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이렇게 구현한 FakeRepository를 서비스 테스트 코드에 주입해 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderLineServiceTest {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderLineService orderLineService;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FakeOrderLineRepository orderLineRepository;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@BeforeEach
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void setUp() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderLineRepository = new FakeOrderLineRepository();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;this.orderLineService = new OrderLineService(this.orderLineRepository);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문번호로 주문 아이템 목록을 조회할 수 있다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void findByOrderNumber() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Item item = createItem();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = createOrder();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderLineCompose compose = createOrderLineCompose();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderLine orderLine = createOrderLine(item, order, List.of(compose));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;orderLineRepository.save(orderLine);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderLineProjection result = orderLineService.findByOrderNumber(&quot;주문번호&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assertThat(result.getOrderNumber()).isEqualTo(&quot;주문번호&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Item createItem() {...}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Order createOrder() {...}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OrderLineCompose createOrderLineCompose() {...}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OrderLine createOrderLine(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final Item, final Order order, final List&amp;lt;OrderLineCompose&amp;gt; composes) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return OrderLine.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.item(item)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.order(order)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.composes(composes)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 읽기 쉽고 효율적인 단위테스트를 만들기 위해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. Mockito보다는 BDDMockito&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;Mockito.when()&lt;/span&gt;을 보면 BDD스럽지 않다는 느낌을 받을 수 있다. BDD에 따르면 반환값을 설정하는 단계인 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;Mockito.when()&lt;/span&gt;은 Given절에 들어 가는 것이 마땅한데, when이라는 메소드명을 가지고 있기 때문이다. Mockito를 BDD스럽게 만든 BDDMockito를 사용하면 이런 문제를 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SuppressWarnings(&quot;unchecked&quot;)
public class BDDMockito extends Mockito {
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;BDDMockito는 Mockito를 상속받아 만든 클래스로 사용방법은 Mockito와 거의 동일하다. 대신 BDD의 시나리오에 맞게 테스트 코드를 볼 수 있도록 메소드 명만 변경했다. 아래의 예시를 살펴보자.&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;when()&lt;/span&gt; 메소드가 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;given()&lt;/span&gt;으로 네이밍이 바뀌었고, 체이닝 메소드인 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;thenReturn()&lt;/span&gt;에서 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;willReturn()&lt;/span&gt;로 변경&lt;/b&gt;된 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

&amp;nbsp;&amp;nbsp;@InjectMocks
&amp;nbsp;&amp;nbsp;OrderService orderService;

&amp;nbsp;&amp;nbsp;@Mock
&amp;nbsp;&amp;nbsp;OrderRepository orderRepository;

&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문 요청을 받아 주문을 저장할 수 있다.&quot;)
&amp;nbsp;&amp;nbsp;void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRequest request = createOrderRequest();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order returnOrder = createOrder(1L);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// BDDMockito의 given 메소드를 사용 가독성 좋은 테스트 코드를 만들 수 있다.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;BDDMockito.given(orderRepository.save(any())).willReturn(returnOrder);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = orderService.order(request);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assertThat(order.getId()).isEqualTo(1L);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;private OrderRequest createOrderRequest() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new OrderRequest(...);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;private Order createOrder(final Long id) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Order.builder()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.id(id)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Nested Class를 적절하게 섞자.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Feature에서 여러 개의 케이스가 존재할 때 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@Nested&lt;/span&gt; 어노테이션을 사용해 엮어서 보여주는 것이 더 깔끔해보일 수 있다. Nested Class는 BDD의 Feature, Scenario의 구분에도 도움이 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderServiceTest {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Nested
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문할 때, &quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class order { // Feature 단위
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;case1이 발생할 수 있다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case1() {...} // Scenario1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;case2가 발생할 수 있다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case2() {...} // Scenario2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3.&amp;nbsp; 테스트는 빠르게&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도 단위테스트는 속도가 가장 중요하다고 생각한다. (좋은 테스트는 FIRST 원칙을 따른다고 하는데, 이 중 F(Fast)가 빠른 속도를 의미하는 원칙) 따라서, 통합테스트보다는 단위테스트 위주로 테스트를 작성하는 것이 좋다고 생각한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;가장 지양해야 할 테스트 코드는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@SpringBootTest&lt;/span&gt;를 달아놓고 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@MockBean&lt;/span&gt;을 사용해 의존관계를 전부 모킹해 단위테스트를 만드는 것이다. 단위테스트는 가장 가벼운 방식으로 테스트해야 한다는 걸 명심하자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 모든 클래스의 테스트 코드를 작성&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;클래스마다 테스트 코드를 갖춰야 한다.&amp;rdquo; &lt;br /&gt;David A. Thomas&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;- 리팩토링, 마틴 파울러 -&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;(만약 도메인 주도 설계를 했다면) 비즈니스 로직은 서비스 계층에만 한정되지 않는다. 오히려 도메인 계층에 중요한 비즈니스 로직이 담겨 있는 경우가 더 많을 것이다. (서비스 계층은 도메인 객체에게 일을 시키는 동작만 하도록 기능을 한정하는 게 좋다.)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;서비스 계층 테스트만 작성했을 때 도메인 객체에 결함이 있음에도 불구하고 얼렁뚱땅(?) 테스트가 잘 통과하는 경우가 생길 수 있다. 각각의 도메인 객체의 테스트를 생성해 단위테스트를 만들자. 도메인 단위테스트는 격리된 테스트를 작성하기 가장 좋은 단위이다. 각 도메인이 요구사항에 맞게 잘 동작하는지 확인하는 것은 무엇보다 중요하다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;다른 계층의 테스트에서도 통용되는 법칙이다. Presentation Layer와 Persistence Layer의 DTO에 변환 로직이 있다면 그 로직에 대해서도 테스트를 작성해주는 것이 좋다고 생각한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public record OrderSearchParam(...) { // from Controller DTO

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public OrderQueryCondition toCondition() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return new OrderQueryCondition(...) // to Repository DTO
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5. 중요한 것은 테스트 커버리지가 아니다. (feat. 경계값 테스트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버리지가 100% 채워졌다는 사실이 그 코드가 완벽하다는 것을 보장하진 않는다. 우리가 테스트를 작성할 때 고려해야할 것은 커버리지가 아니라 로직이 얼마나 빈틈 없이 테스트되었는지다. 요구사항과 그에 따른 예외 상황을 모두 정리해놓고 테스트 코드에 그대로 작성하자.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;여기서 경계값 테스트를  활용하면 좋다. 경계값 테스트란 입력 값이 특정 범위의 경계에 위치할 때 프로그램이 잘 동작하는지 확인할 때 사용하는 테스트이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderTest {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Nest
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문할 때, &quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class order {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;10000원 이상 주문하면 100원이 할인된다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case1() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRequest request = createOrderRequest(15_000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = Order.order(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assertThat(order.getDiscountAmount()).isEqualTo(100);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;10000원 주문하면 100원이 할인된다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case2() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRequest request = createOrderRequest(10_000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = Order.order(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assertThat(order.getDiscountAmount()).isEqualTo(100);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;9999원 주문하면 할인되지 않는다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case3() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// given
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;OrderRequest request = createOrderRequest(9_999);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// when
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Order order = Order.order(request);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// then
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assertThat(order.getDiscountAmount()).isEqualTo(0);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private OrderRequest createOrderRequest(final long amount) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return OrderRequest
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.amount(amount)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.build();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-6. 테스트 하나에는 하나의 케이스만 사용하자.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 시나리오에서 2가지 이상의 상황을 이해해야 하는 경우 동료들이 테스트의 의도를 파악하는데 어려움을 겪을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderLineServiceTest {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Nested
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문 아이템을 주문할 때,&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class order{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;아이템 개수가 2개 이하면 배송비가 3000원이고, 3개 이상이면 배송비가 무료다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case() {...}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 아래와 같이 테스트 케이스를 2개로 분리해서 작성하자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderLineServiceTest {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Nested
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;주문 아이템을 주문할 때,&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;class order{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;아이템 개수가 2개 이하면 배송비가 3000원이다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case1() {...}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Test
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@DisplayName(&quot;아이템 개수가 3개 이상이면 배송비가 무료다.&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;void case2() {...}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-7. DAMP 원칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 읽기 좋아야 한다. &lt;b&gt;읽기 좋으려면 문맥이 제공되어야 한다.&lt;/b&gt; 따라서 테스트 코드를 작성할 때, DRY(Don't Repeat Yourself) 원칙이 아닌 DAMP(Descriptive And Meaningful Phrase) 원칙을 따르는 것이 좋다. 오히려 코드가 중복되고 코드 라인이 늘어나더라도 테스트 코드 이해에 도움이 된다면 그렇게 작성하는 것이 좋다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이전 글인 &lt;a href=&quot;https://myvelop.tistory.com/223#4-1.%20given%20%EC%A0%88%EC%97%90%EC%84%9C%20%EC%9E%91%EC%84%B1%2C%20%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8A%94%20%EB%AC%B8%EC%84%9C%EB%8B%A4.-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[Test] Persistence Layer Test와 테스트에 대한 고찰&lt;/span&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;모든 &lt;b&gt;Fixture는 given에 작성&lt;/b&gt;하자. (&lt;span style=&quot;background-color: #dddddd;&quot;&gt;@BeforeAll&lt;/span&gt;이나 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;@BeforeEach&lt;/span&gt;에서 Fixture 작성을 지양)&lt;/li&gt;
&lt;li&gt;생성 로직(&lt;span style=&quot;background-color: #dddddd;&quot;&gt;of&lt;/span&gt; 등의 도메인 객체 메소드)을 사용하지 말고 &lt;b&gt;생성자나 빌더를 사용해 객체를 생성&lt;/b&gt;하자.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fixture 함수를 분리하고 싶다면 &lt;/b&gt;한 곳에 모아서 사용하기보다는&lt;b&gt; 각 테스트 클래스에 필요한 &lt;/b&gt;&lt;b&gt;함수&lt;/b&gt;를 두자. 각 테스트 케이스마다 전달해야 하는 파라미터가 달라질 수 있기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 필요 없는 Fixture를 given에 넣으면 오히려 혼란을 야기할 수 있으니 필요한 데이터만 선별해서 작성하도록 하자.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 글을 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직 테스트 가장 중요한 테스트다. 비즈니스 요구사항이 충족되었는지, 요구사항 자체에 결함은 없는지, 의도하지 않은 사이드 이펙트가 발생하지 않는지 확인할 수 있는 과정이기 때문이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그 뿐만 아니라 테스트 코드는 내가 작성한 코드에 자체 피드백을 줄 수 있다. 간혹가다 테스트 작성이 어려운 코드를 만나곤 하는데, 이는 내 코드에 문제가 있을 수 있음을 암시한다. 코드의 책임과 역할이 적절하게 할당하지 않았기 때문일 수 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;만약 작성한 테스트가 문서의 역할까지 할 수 있다면, 개발자들은 테스트 코드만 보고도 비즈니스의 핵심적인 규칙이나 프로세스를 파악할 수 있을 것이다. 새로운 팀원이 합류했을 때 그 팀원이 비즈니스 규칙을 이해하는 데 큰 도움을 줄 것이기 때문에 코드의 유지보수 측면에도 큰 가치가 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&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://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Test Double을 알아보자&lt;/span&gt;&lt;/a&gt; - Tecoble&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2020-09-29-compare-mockito-bddmockito/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Mockito와 BDDMockito는 뭐가 다를까?&lt;/span&gt;&lt;/a&gt; - Tecoble&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.popit.kr/bdd-behaviour-driven-development%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%84%EB%9E%B5%ED%95%9C-%EC%A0%95%EB%A6%AC/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;BDD에 대한 간략한 정리&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/daangn/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%9C%84%ED%95%9C-stub-%EA%B0%9D%EC%B2%B4-%ED%99%9C%EC%9A%A9%EB%B2%95-5c52a447dfb7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;효율적인 테스트를 위한 Stub 객체 사용법&lt;/span&gt;&lt;/a&gt; - 당근 테크 블로그&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/bliki/UnitTest.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;Unit Test&lt;/span&gt;&lt;/a&gt; - 마틴 파울러&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/mock-test-code-part-2/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;실무에서 적용하는 테스트 코드 작성 방법과 노하우 Part 2: 테스트 코드로부터 피드백받기&lt;/span&gt;&lt;/a&gt; - 카카오페이 기술 블로그&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&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://myvelop.tistory.com/223&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[Test] Persistence Layer Test와 테스트에 대한 고찰&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Test</category>
      <category>JUnit</category>
      <category>Spring</category>
      <category>TEST</category>
      <category>test double</category>
      <category>단위테스트</category>
      <category>소형테스트</category>
      <category>중형테스트</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/224</guid>
      <comments>https://myvelop.tistory.com/224#entry224comment</comments>
      <pubDate>Sun, 14 Apr 2024 12:26:51 +0900</pubDate>
    </item>
    <item>
      <title>[Test] Persistence Layer Test와 테스트에 대한 고찰</title>
      <link>https://myvelop.tistory.com/223</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 Persistence Layer를 테스트하는 방법만을 서술하는 것이 아닌, 영속 계층을 테스트해야 하는 이유에 대해 정리하고 어떤 방식으로 테스트하는 것이 더 좋은 방법인지 고민한 내용을 정리해보려 합니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Persistence Layer (혹은 Repository Layer)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Persistence Layer의 테스트를 하기 전에 Layered Architecture와 테스트의 분류에 대해 먼저 숙지해두면 각 레이어 별 테스트가 어떤 것을 목적으로 하는지 파악할 수 있고, 그 목적에 맞는 테스트를 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-1. Layered Architecture&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layered Architecture에서 각 계층의 역할은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Presentation Layer: 사용자의 요청과 응답을 처리하는 계층&lt;/li&gt;
&lt;li&gt;Business Layer (Application Layer): 비즈니스 로직을 수행하는 계층&lt;/li&gt;
&lt;li&gt;Persistence Layer (Repository Layer): 데이터베이스로부터 데이터를 조회해 보관 및 사용하고 저장하는 계층&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;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bplq1m/btsF5GfsQWA/nKQNM3kFH4Ep8itk0kN030/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bplq1m/btsF5GfsQWA/nKQNM3kFH4Ep8itk0kN030/img.png&quot; data-alt=&quot;Layered Architecture (출처: https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bplq1m/btsF5GfsQWA/nKQNM3kFH4Ep8itk0kN030/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbplq1m%2FbtsF5GfsQWA%2FnKQNM3kFH4Ep8itk0kN030%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; alt=&quot;Layered Architecture&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;474&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Layered Architecture (출처: https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 각 계층에 역할에 맞는 단위테스트를 작성하면 된다. Presentation Layer는 요청과 응답을 잘 처리하는지 테스트 하면 되고, Business Layer는 비즈니스 로직이 잘 실행되는지를 염두에 두고 테스트하면 된다. Persistence Layer에서는 데이터를 잘 조회하고 저장하는지 테스트해보면 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2. 테스트의 분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Persistence Layer Test는 테스트의 분류 중 어디에 속할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 구글의 테스트 3분류이다. 보편적으로 테스트의 3분류를 API 테스트, 통합 테스트, 단위 테스트로 나눈다. 하지만, 구글에서는 소중대로 구분한 새로운 분류체계를 사용한다.&amp;nbsp;&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;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ5gYx/btsGcHUaUhV/h9gHzkNNwfdz5jGgDjre3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ5gYx/btsGcHUaUhV/h9gHzkNNwfdz5jGgDjre3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ5gYx/btsGcHUaUhV/h9gHzkNNwfdz5jGgDjre3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ5gYx%2FbtsGcHUaUhV%2Fh9gHzkNNwfdz5jGgDjre3K%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;759&quot; height=&quot;377&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;소형 테스트&lt;/b&gt;: 단일 서버, 단일 프로세스, 단일 스레드, 디스크 I/O 허용 X, Blocking Call 허용 X&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중형 테스트&lt;/b&gt;: 단일 서버, 멀티 프로세스, 멀티 스레드, H2와 같은 DB 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대형 테스트&lt;/b&gt;: 멀티 서버, End to End 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Persistence Layer Test는 전통적인 관점에서 봤을 때 Repository 컴포넌트만 테스트하는 단위테스트이다. 구글의 테스트 3분류에 따르면 데이터베이스를 달고 테스트를 진행하는 중형테스트라고 볼 수 있다. Persistence Layer Test는 DB를 달고 테스트하기 때문에 소형테스트보다 시간이 오래 걸린다.&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;반면, Presentation Layer Test와 Business Layer Test는 Test Double(Fake, Stub, Mock 등)을 만들어 단위테스트를 할 수 있기 때문에 스프링부트를 띄우지 않고 단위테스트를 진행한다면 소형테스트라고 볼 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 데이터베이스 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Persistence Layer를 테스트하려면 데이터베이스 설정이 선행되어야 한다. 운영 환경에서 사용하는 MySQL, Oracle, PostgreSQL과 같은 데이터베이스를 사용해 테스트를 진행하거나 In-memory H2 DB를 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. H2 데이터베이스를 사용해야 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H2 In-Memory DB를 사용해 테스트하는 것이 더 빠르다. 아래 블로그를 확인해보면 평균적으로 24.18%가 더 빠르다는 사실을 확인해볼 수 있다.&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 title=&quot;[Test Code] 테스트 DB의 In-Memory H2와 로컬 MySQL 속도 차이 비교하기&quot; href=&quot;https://velog.io/@da_na/Test-Code-%ED%85%8C%EC%8A%A4%ED%8A%B8-DB%EC%9D%98-In-Memory-H2%EC%99%80-%EB%A1%9C%EC%BB%AC-MySQL-%EC%86%8D%EB%8F%84-%EC%B0%A8%EC%9D%B4-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0-vfzyjhy5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Test Code] &lt;span&gt;테스트&lt;/span&gt; DB&lt;span&gt;의&lt;/span&gt; In-Memory H2&lt;span&gt;와&lt;/span&gt; &lt;span&gt;로컬&lt;/span&gt; MySQL &lt;span&gt;속도&lt;/span&gt; &lt;span&gt;차이&lt;/span&gt; &lt;span&gt;비교하기&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 H2 DB를 사용하면 데이터베이스 구성이 훨씬 쉽다. 테스트를 위한 DB를 따로 구성하지 않아도 된다. 로컬에서야 도커를 사용해 테스트 DB를 띄우면 된다지만, CI/CD 환경에서 테스트를 실행하기 위해 따로 연결해 사용할 수 있는 DB가 필요하다. 하지만 H2 In-Memory DB를 사용하면 따로 DB 환경을 구성할 필요가 없다.&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;하지만 단점도 존재한다. 실제 운영 환경에서는 MySQL이나 PostgreSQL를 사용하고 테스트에서 H2 DB를 사용하면 운영 환경과 괴리로 인해 생기는 문제가 있다. 예를 들어, MySQL에서 사용하는 모든 함수를 H2에서 지원하지는 않기 때문에 만약 실제 환경에서 H2가 지원하지 않는 SQL Function을 사용하면 해당 메소드에 대해 테스트를 실행할 때 H2에는 존재하지 않는 SQL Function이기 때문에 에러가 발생하고 테스트가 실패할 것이다.&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;이런 단점에도 불구하고 이를 상쇄할만큼 H2 DB가 빠르고 간편하기 때문에 사용하는 것이 나쁘지 않다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;H2 데이터베이스 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 Profile 기능을 사용해 테스트 환경에서는 운영환경과는 다르게 H2의 In-Memory DB를 사용하게 설정할 수 있다.&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;H2 DB를 In-Memory로 생성하려면 spring.datasource.url에 &lt;b&gt;jdbc:h2:mem&amp;nbsp;&lt;/b&gt;와 같은 형식으로 값을 넣어주면 된다. ddl-auto를 create로 설정해놓으면 Entity만 만들어도 테이블을 자동으로 생성해주기 때문에 따로 스키마에 대한 SQL문을 작성할 필요가 없다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/test/resources/application.yaml&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1711631524559&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  profiles:
    active: test

  datasource:
    url: jdbc:h2:mem:~/myApplication
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    defer-datasource-initialization: true

  h2:
    console:
      enabled: true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Testcontainers&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 바와 같이 H2 In-Memory DB를 사용한 테스트는 실제 운영환경과 테스트 환경의 멱등성을 보장할 수 없다. 이 부분에서 아쉬움을 느끼는 개발자라면 Testcontainers라는 기술을 사용해볼 수 있다. 자세한 내용은 아래 글에 담겨 있다.&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://myvelop.tistory.com/226&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Test] Testcontainers를 사용한 DB 테스트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;3. 테스트 환경 구축&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Persistence Layer Test는 다른 레이어의 단위 테스트와는 다르게 애플리케이션 환경이 구축되어야 DB와 연결해 테스트를 실행할 수 있다. 이 때 사용되는 것이 @DataJpaTest와 @SpringBootTest이다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;3-1. @DataJpaTest&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data JPA 컴포넌트들(JPA에 의해 자동 생성되는 Proxy 객체)를 테스트할 수 있는 환경을 만들어준다. Data Jpa 컴포넌트만 불러오기 때문에 다른 객체를 가져오려고 하면 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@DataJpaTest 어노테이션에는 기본적으로 @Transactional이 들어가 있기 때문에 모든 테스트가 롤백된다.&lt;/p&gt;
&lt;pre id=&quot;code_1711891615876&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;3-2. @SpringBootTest&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했다시피 @DataJpaTest는 Data Jpa 컴포넌트만 지원하기 때문에 그 외의 다른 컴포넌트를 테스트하려면 다른 방법을 사용해야 한다. 이때 사용할 수 있는 게 @SpringBootTest 어노테이션이다. 예를 들어, QueryDsl을 사용해 만들어진 Repository 구현체는 Data Jpa 컴포넌트가 아니기 때문에 @DataJpaTest 어노테이션으로는 주입 받을 수 없는 대신 @SpringBootTest를 사용할 수 있다.&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;@SpringBootTest는 모든 빈을 스캔해 애플리케이션 컨텍스트를 생성하는 등 통합 테스트를 위한 환경을 만들어준다. 모든 빈을 다 가져오기 때문에 @DataJpaTest 보다 느리다. &lt;b&gt;하지만 @SpringBootTest를 Persistence Layer 테스트에서 쓰면 좋은 이유가 있는데 3-3을 확인해보자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;3-3. 테스트 환경을 통합해서 더 빠른 테스트 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 테스트 환경을 통합하지 않았다면 전체 테스트를 돌렸을 때 Test Results에서 Spring Boot가 여러 번 실행된 것을 확인할 수 있다.(전체 테스트를 돌린 후, &quot;Spring Boot&quot;라고 검색해보자.)&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;서비스 테스트에서 서로 각기 다른 MockBean을 가지고 있거나, Persistence Layer에서 @DataJpa와 @SpringBootTest 또는 커스텀 환경을 섞어 썼다면 새로운 환경의 Spring Boot가 더 많이 실행됐을 것이다. 스프링 테스트에서는 컨테이너 환경(의존성 설정)이 다르다고 판단하면 Spring Boot를 새롭게 실행하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 테스트 속도를 개선하고 싶다면 Persistence Layer에서도 @SpringBootTest를 사용하고 Service 테스트와 환경을 통합할 필요가 있다. (&lt;/b&gt;Presentation Layer의 테스트는 Service만 모킹해서 요청과 응답을 테스트하는 단위테스트 환경을 구축하기 때문에 성격이 약간 다르다. 그렇기 때문에 Controller의 경우는 따로 환경을 구성해주는 것이 좋아 보인다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;코드예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 통합 테스트 환경을 abstract class로 생성해준다. Service 통합테스트 환경과 Persistence Layer의 환경을 합칠 것이기 때문에 Service Test 단에 있는 모든 Mock들을 IntegrationTestSupport 추상 클래스로 가지고 온다. 이 때 Mocking 객체들을 protected로 선언해줘야 각 테스트에서 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1711891615878&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ActiveProfiles(&quot;test&quot;)
@SpringBootTest
public abstract class IntegrationTestSupport {

  @MockBean
  protected MessageSendClient messageSendClient;

  @MockBean
  protected MailSendClient mailSendClient;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Service 테스트와 Persistence Layer 테스트 클래스에서 추상클래스를 상속받아 테스트를 구현하자. 그리고 전체 테스트를 돌려보면 테스트 환경이 통합되어 스프링 부트가 실행 횟수가 줄어들고, 테스트 속도도 빨라진 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1711891615879&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderServiceIntegrationTest extends IntegrationTestSupport {

	// Mocking이 IntegrationTestSupport 쪽으로 다 넘어가야 한다.
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1711891615879&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class OrderRepository extends IntegrationTestSupport {

	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 테스트를 위한 데이터 준비&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 로직과 업데이트 로직을 테스트하려면 DB에 미리 데이터를 넣어야 하는데 이 때 사용할 수 있는 방법 몇 가지와 각각의 장단점에 대해 소개하려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. given 절에서 작성, 테스트는 문서다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 테스트 케이스를 실행할 때마다 데이터를 넣는 방식이다. 각 테스트 케이스마다 필요한 데이터만 삽입할 수 있지만 중복되는 데이터들이 모든 케이스마다 작성되기 때문에 테스트 코드가 길어질 수 있다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 단점에도 불구하고, given 절에서 데이터들이 들어가야 하는 이유가 있다. &lt;b&gt;문서로서의 역할을 하는 테스트 코드&lt;/b&gt;를 작성하기 위해서이다. given에 테스트에 필요한 모든 데이터가 들어있어야 나를 포함해 팀원들이 이해하기 훨씬 편하다. 테스트 케이스의 상황이 한눈에 들어오기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작성자 입장에서는 given에 모든 데이터를 작성하는 게 귀찮고 불편한데다가 시간도 오래 걸린다. 하지만 그 귀찮음 때문에 Fixture들을 파편화하기 시작하고 그것이 계속 이어져 Fixture를 관리되기 힘든 수준까지 이르면 문서로서의 역할을 하는 테스트 코드 작성이 어려워진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 문서로서의 테스트를 만들기 위해 아래의 원칙을 따르는 것이 좋다.&lt;/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;모든 Fixture는 given에 작성하자.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;생성 로직을 사용하지 말고 &lt;b&gt;생성자나 빌더를 사용해 객체를 생성하자.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fixture 함수를 분리하고 싶다면 &lt;/b&gt;한 곳에 모아서 사용하기 보다는 &lt;b&gt;각 테스트 클래스에 필요한 함수를 두자.&lt;/b&gt; 각 테스트 케이스마다 전달해야 하는 파라미터가 달라질 수 있기 때문이다.&lt;/li&gt;
&lt;/ul&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;테스트 코드에 레포지토리로 데이터를 삽입하는 코드가 있다면, @Transactional을 사용해 데이터를 롤백해줄 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1711631964680&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class OrderRepositoryTest {

    @Autowired
    OrderRepository orderRepository;

    @Test
    @Transactional
    public void orderTest() {
       // given
       Order order1 = createOrderFixture(...); 
       Order order2 = createOrderFixture(...);
       Order order3 = createOrderFixture(...);
       orderRepository.saveAll(List.of(order1, order2, order3));

       // when
       ...

       // then
       ...
    }

    ...
    
    private Order createOrderFixuture(...) {
    	...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. @BeforeEach나 @Before를 사용한 setup&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;케이스마다 데이터를 넣어주는 것이 현실적으로 불가능할 때 사용할 수 있는 방식이다. 공통적인 데이터를 넣어주고, 특정 데이터가 필요할 때만 3-1의 방식을 사용해 데이터를 넣어주는 방식으로 사용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 @BeforeEach를 사용해 모든 메소드마다 데이터 삽입을 실행하면 어떤 케이스에서는 필요 없는 데이터가 들어갈 수 있기 때문에 테스트 성능에 영향을 줄 수 있다. 또한 특정 케이스를 위해 setup에 있는 Fixture 변경했을 때 다른 케이스들에 영향을 줄 수 있다는 단점도 존재한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@AfterEach나 @After를 통해 데이터를 지워주는 로직을 넣어줘야 다른 테스트에 영향을 주지 않을 수 있다. 이 때 deleteAll()은 findAll()로 찾은 리스트를 순회하며 하나씩 데이터를 삭제하기 때문에 속도가 느릴 수 있다. 한 번의 쿼리로 모든 데이터를 지우는 것이 훨씬 빠르기 때문에 &lt;b&gt;deleteAllInBatch()를 사용하는 것을 추천&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre id=&quot;code_1711638977927&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class OrderRepositoryTest {

    @Autowired
    OrderRepository orderRepository;
    
    @BeforeEach()
    void setup() {
       Order order1 = createOrderFixture(...); 
       Order order2 = createOrderFixture(...);
       Order order3 = createOrderFixture(...);
       orderRepository.saveAll(List.of(order1, order2, order3));
    }
    
    @AfterEach
    void tearDown() {
    	orderRepository.deleteAllInBatch();
    }

    @Test
    public void orderTest() {
       // given
       ...

       // when
       ...

       // then
       ...
    }

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. @Sql이나 @SqlGroup과 SQL 쿼리 파일을 사용한 데이터 삽입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-2와 마찬가지로 given에 모든 데이터를 작성할 여력이 없을 때 사용할 수 있는 방법이다. value에 원하는 SQL 파일(이 때 루트 디렉토리는 resources)을 넣어주고 executionPhase로 원하는 시점을 지정할 수 있다. 지정할 수 있는 시점은 org.springframework.test.context.jdbc.Sql 어노테이션에 ExecutionPhase Enum으로&amp;nbsp; 정의되어 있는데 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BEFORE_TEST_CLASS: 클래스 시작 전 동작. @Before와 같은 역할&lt;/li&gt;
&lt;li&gt;BEFORE_TEST_MTEHOD: 메소드 시작 전 동작. @BeforeEach와 같은 역할&lt;/li&gt;
&lt;li&gt;AFTER_TEST_CLASS: 클래스 종료 후 동작. @After와 같은 역할&lt;/li&gt;
&lt;li&gt;AFTER_TEST_MTEHOD: 메소드 종료 후 동작. @AfterEach와 같은 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 관리자 페이지와 쇼핑몰 페이지 2개를 각 모듈로 나눠 구현해 운영한다고 가정해보자. 관리자 페이지에서는 상품을 등록해야 하기 때문에 ProductRepostory에 save가 필요하다. 하지만 실제 유저에게 서비스하는 쇼핑몰에서는 상품을 저장할 필요가 없기 때문에 &lt;b&gt;ProductRepository에 save나 saveAll이 필요하지 않다. 이런 경우 테스트만을 위해 repository의 save를 구현해야 하는 것이 싫을 때 사용하면 좋은 방식이 @Sql과 @SqlGroup이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 연관관계를 같이 테스트하려면 연관된 다른 레포지토리를 주입받아 save를 실행해줬어야 하지만, 이 방식은 데이터를 넣기 위해 레포지토리의 save를 사용하지 않기 때문에 굳이 다른 레포지토리를 주입받지 않아도 된다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 3-2와 마찬가지로 테스트 속도 이슈와 Fixture 수정 시 테스트 케이스에 대한 영향 등의 문제가 있다. 또한, SQL문에서 각 row의 i&lt;b&gt;d 값을 지정해서&lt;/b&gt; 데이터를 넣어 놓고 테스트 코드 단에서 repository save를 실행하려고 하면 채번을 &quot;1&quot;부터 하기 때문에 &lt;span style=&quot;background-color: #ffffff; color: #0c0d0e; text-align: left;&quot;&gt;Unique index or primary key violation 에러가 발생할 수 있다는 단점이 있다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SqlGroup을 사용해 데이터를 삽입하는 sql문과 데이터를 지워주는 sql문을 같이 작성해줘야 다른 테스트에 영향을 주지 않을 수 있다. 예시는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1711639146560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@SqlGroup({
    @Sql(value = &quot;/sql/order-repository-test.sql&quot;, executionPhase = ExecutionPhase.BEFORE_TEST_METHOD),
    @Sql(value = &quot;/sql/delete-all-data.sql&quot;, executionPhase = ExecutionPhase.AFTER_TEST_METHOD),
})
class OrderRepositoryTest {

    @Autowired
    OrderRepository orderRepository;

    @Test
    public void orderTest() {
       // given
       ...

       // when
       ...

       // then
       ...
    }

    ...
}&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;test/resources/sql/order-repository-test.sql&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1711639401456&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insert into `member`(...)
values (...), (...), (...);

insert into `order`(...)
values (...), (...), (...);

insert into `order_line`(...)
value (...), (...), (...);&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;test/resources/sql/delete-all-data.sql&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1711639460468&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;delete `order_line` where 1;
delete `order` where 1;
delete `member` where 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Persistence Layer는 어떻게 테스트해야할까..?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. Repository의 의존성 역전&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;테스트를 할 때 구현(implementation)이 아니라 설계(interface)에 맞춰야 한다&lt;br /&gt;당신의 TDD가 항상 실패하는 이유, 이규원 - 2018OKKYCON&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 어떤 JPA와 QueryDSL을 사용하는 프로젝트를 진행하고 있다고 해보자. 미래에 JPA를 뛰어넘을 차세대 기술이 생겨 진행하고 있는 프로젝트의 기술 스택을 다른 ORM이나 QueryMapper로 변경하게 되어 마이그레이션을 하게 되었다. 그러면 구현체가 바뀌게 될 것이다. 만약 구현체를 대상으로 테스트를 작성했다면 테스트를 전체적으로 변경해야하는 불상사가 생길 수 있다.&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;h4 data-ke-size=&quot;size20&quot;&gt;구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OrderRepository를 중심으로 관계를 살펴보자.&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;OrderRepositoryImpl은 OrderRepository를 구현한 구현체이다. DataJpa 컴포넌트나 QueryDSL, MyBatis, JdbcTemplate 등으로 구현된 구현체를 주입받아 인터페이스를 구현한다.&lt;/li&gt;
&lt;li&gt;OrderService는 OrderRepository에 의존하는데 실제로 OrderService는 OrderRepositoryImpl 구현체를 주입받게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JUQwJ/btsGd0yVFOp/0598kBpctb5luGTFQUKwFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JUQwJ/btsGd0yVFOp/0598kBpctb5luGTFQUKwFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JUQwJ/btsGd0yVFOp/0598kBpctb5luGTFQUKwFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJUQwJ%2FbtsGd0yVFOp%2F0598kBpctb5luGTFQUKwFk%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;738&quot; height=&quot;480&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Persistence Layer Test를 진행할 때, 테스트하는 대상은 구현체가 아닌 OrderRepository 인터페이스가 되어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2276&quot; data-origin-height=&quot;1694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vwSL2/btsGexbPVxL/XB2EKWgoCKwY3YrEGZGKj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vwSL2/btsGexbPVxL/XB2EKWgoCKwY3YrEGZGKj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vwSL2/btsGexbPVxL/XB2EKWgoCKwY3YrEGZGKj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvwSL2%2FbtsGexbPVxL%2FXB2EKWgoCKwY3YrEGZGKj1%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;691&quot; height=&quot;514&quot; data-origin-width=&quot;2276&quot; data-origin-height=&quot;1694&quot;/&gt;&lt;/span&gt;&lt;/figure&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;아래와 같이 Repository 인터페이스가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1711721248694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface OrderRepository {

    Order save(Order order);

    List&amp;lt;Order&amp;gt; search(OrderQueryCondition condition);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그리고 OrderRepository를 구현한 구현체(OrderRepositoryImpl)는 위의 인터페이스를 구현한다. 이 객체는 QueryDsl, DataJpa, MyBatis, Jdbc 등으로 만든 구현체를 주입받아 로직을 구현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1711721382733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredConstructor
public class OrderRepositoryImpl implements OrderRepository {

    private final OrderQueryRepository orderQueryRepository;
    private final OrderJpaRepository orderJpaRepository;

    @Override
    public Order save(Order order) {
        return orderJpaRepository.save(order);
    }

    @Override
    public List&amp;lt;Order&amp;gt; search(OrderQueryCondition condition) {
        return orderQueryRepository.search(condition);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1711721040913&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredConstructor
public class OrderQueryRepository {

    private final JPAQueryFactory queryFactory;

    public List&amp;lt;Order&amp;gt; search(OrderQueryCondition condition) {...}
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1711721145243&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface OrderJpaRepository extends JpaRepository&amp;lt;Order, OrderNo&amp;gt; {

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;여기서 주의할 점이 있는데 이렇게 만들어진 인터페이스는 Data Jpa의 컴포넌트가 아니기 때문에 @DataJpaTest를 사용하면 인터페이스를 주입받을 수 없다. @SpringBootTest를 사용해야 테스트 환경이 정상적으로 구성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1711721611103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class OrderRepositoryTest {

    @AutoWired
    OrderRepository orderRepository;

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 과하지 않게&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했다시피 Persistence Layer 테스트는 다른 레이어의 단위테스트보다 무겁다. 따라서 정말 필요한 메소드만 테스트하는 것이 좋다. 예를 들어 복잡한 동적 쿼리가 있는 조회 로직이라면 테스트해보는 것이 좋을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 단순 조회(findAll, findById)와 저장 로직만 있는 Persistence Layer를 굳이 테스트할 필요는 없다고 생각한다. 그 시간에 차라리 비즈니스 로직 통합테스트를 만드는 것이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-3. @Transactional&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서 @Transactional을 사용하는 것에 대해 많은 개발자들의 생각이 갈리는 것으로 알고 있다.&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://jojoldu.tistory.com/761&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테스트 데이터 초기화에 @Transactional 사용하는 것에 대한 생각&lt;/a&gt; - 향로님 블로그&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test에 @Transactional이 있으면 트랜잭션이 Test로 묶여버리기 때문에 발생하는 여러 문제들로 인해 통합테스트에서 @Transactional을 사용하는 것에 대해서는 굉장히 조심스러운 입장이다. 비즈니스 로직은 비동기, 트랜잭션 전파 레벨, 스프링 이벤트 등으로 인해 테스트 단의 @Transactional에 영향을 받을 가능성이 크기 때문이다.&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;하지만, Persistence Layer에서는 @Transactional을 사용하는 것은 괜찮다고 생각한다. Persistence Layer가 테스트 단의 @Transacional에 의해 트랜잭션이 묶여 발생할 수 있는 문제를 따져보면 비즈니스 로직을 통합테스트를 실행할 때와는 다르게 크게 신경쓰일만한 것이 없다고 생각한다. 트랜잭션 전파 레벨을 레포지토리 단에서 제어하거나 레포지토리 메소드를 비동기로 처리하는 것이 아니라면 큰 문제가 없어보인다. 그래도 @Transactional의 사이드이펙트를 잘 알고 사용하면 좋을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, @Transactional을 사용하기 꺼려진다면 @AfterEach에서 레포지토리의 deleteAllInBatch() 메소드를 사용하는 것을 권장한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 Persistence Layer의 테스트보다 더 중요한 것은 비즈니스 로직의 소형 테스트(혹은 단위테스트)라고 생각한다. 중형 테스트가 많아지면 테스트 속도가 느려질 수 있고, 이렇게 느려진 테스트는 CI/CD와 리팩토링 등 프로젝트 전반에 큰 영향을 줄 수 있다. 너무 오랜 시간이 걸리면 개발자들이 테스트를 돌려보기 무서워지는 시점이 올 수도 있다.&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관련 글&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://myvelop.tistory.com/226&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Test] Testcontainers를 사용한 DB 테스트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그 외 참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://mangkyu.tistory.com/242&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스프링부트 테스트를 위한 의존성과 어노테이션&lt;/a&gt; - 망나니개발자님 블로그&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/course/practical-testing-%EC%8B%A4%EC%9A%A9%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%80%EC%9D%B4%EB%93%9C/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Practical Testing: 실용적인 테스트 가이드&lt;/a&gt; - 인프런 강의&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%A4%EB%8B%B5%EB%85%B8%ED%8A%B8/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트&lt;/a&gt; - 인프런 강의&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/david-learner/java-study/tree/master/2018OKKYCON#%EC%84%A4%EA%B3%84&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2018OKKYCON&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Test</category>
      <category>H2</category>
      <category>Persistence</category>
      <category>Repository</category>
      <category>Spring</category>
      <category>단위테스트</category>
      <category>스프링</category>
      <category>영속계층</category>
      <category>중형테스트</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/223</guid>
      <comments>https://myvelop.tistory.com/223#entry223comment</comments>
      <pubDate>Sat, 30 Mar 2024 16:07:50 +0900</pubDate>
    </item>
    <item>
      <title>Udemy - Java 멀티스레딩, 병행성 및 성능 최적화</title>
      <link>https://myvelop.tistory.com/222</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9IYEI/btsGBZTDBof/Zah2BNgKdxKKZdiJ9FRvDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9IYEI/btsGBZTDBof/Zah2BNgKdxKKZdiJ9FRvDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9IYEI/btsGBZTDBof/Zah2BNgKdxKKZdiJ9FRvDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9IYEI%2FbtsGBZTDBof%2FZah2BNgKdxKKZdiJ9FRvDk%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;657&quot; height=&quot;284&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;730&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;글또 9기에서 지원받은 Udemy 강의 리뷰입니다.&lt;/li&gt;
&lt;li&gt;강의를 지원받는 대신! 리뷰를 작성하기로 했다는 점!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NXH9U/btsFoDLbr83/GPUFmDCZa3y6kLl7vM0pH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NXH9U/btsFoDLbr83/GPUFmDCZa3y6kLl7vM0pH0/img.png&quot; data-alt=&quot;사진출처: udemy&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NXH9U/btsFoDLbr83/GPUFmDCZa3y6kLl7vM0pH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNXH9U%2FbtsFoDLbr83%2FGPUFmDCZa3y6kLl7vM0pH0%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;651&quot; height=&quot;389&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사진출처: udemy&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; 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;/li&gt;
&lt;li&gt;운영체제를 미리 공부해둬야 강의를 수월하게 이해할 수 있다. 개인적으로는 운영체제를 공부하지 않았다면 강의 내용을 이해하기 어려웠을 거라고 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;강의 구성&lt;/h2&gt;
&lt;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.udemy.com/course/java-multi-threading/?couponCode=8B14F706D33BF228ABBC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;강의는 먼저 이론을 설명한 뒤 실습에 들어가는 형식으로 진행되었다.&lt;/li&gt;
&lt;li&gt;중간중간 이론 퀴즈와 실습 문제가 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 스레드의 개념, Runnable과 Thread&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의를 시작하기 전 가장 많이 사용하게 될 Runnable과 Thread의 기본적인 사용 방법을 알려준다. 스택 및 힙 메모리 영역에 대해 공부하며&amp;nbsp;스레드 간의 조율을 위해 사용하는 데몬 스레드와 Join 등의 개념을 배운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 처리량과 지연시간 - 성능 최적화&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;3.&amp;nbsp; 병렬 처리 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임계 영역와 동기화, 경쟁 상태에 대한 개념에 대해 배운다. 임계 영역을 동기화하기 위해 volatile과 synchronized 키워드를 사용하는 방법을 실습한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;임계 영역 혹은 공유 변수 영역은 병렬 컴퓨팅에서 둘 이상의 스레드가 동시에 접근해서는 안되는 공유 자원을 접근하는 코드의 일부를 의미한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Locking&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Locking과 Deadlock의 개념을 익히고, ReentrantLock 객체 사용법을 공부한다. 읽기/쓰기 락을 사용해 효율적으로 Locking하는 기법을 배울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaFx를 사용해 ReentrantLock을 실습하는 내용이 있는데, 설정 때문에 꽤나 애를 먹었던 기억이 남는다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Thread 간의 정보 교환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세마포어와 조건변수에 대한 개념을 공부하고 wait(), notify(), notifyAll() 등의 메소드 사용법을 공부한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Blocking I/O와 Non-Blocking I/O&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blocking I/O에 대해 공부하고 Blocking이 일어날 때 운영체제와 자바의 플랫폼 스레드가 어떤 식으로 동작하는지 배울 수 있다. 또한 실습을 통해 직접 스레드를 작동시켜보고 문맥교환 비용이 얼마나 되는지 직접 눈으로 확인해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 Blocking I/O 처리 방식과 Non-Blocking I/O 처리 방식에 대한 비교를 통해 장단점을 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Virtual Thread&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java21에 새롭게 추가된 경량스레드인 가상스레드의 개념에 대해 공부한다. 가상스레드에 대한 개념 설명이 꽤나 상세하다. 이전에 가상스레드를 따로 공부한 적이 있었는데 내가 잘못 이해하고 있던 내용을 강의를 통해 교정할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM에서 Virtual Thread와 Carrier Thread가 어떻게 동작하는지에 대해 이해하고, 가상스레드와 연관된 여러 개념과 오해에 대해 배울 수 있다. (처리량과 지연시간, Blocking Call의 빈도에 따른 가상스레드 효율성 등)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;강의 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mooc에서 반효경 교수님의 강의를 통해 운영체제를 공부한 적이 있었는데 한 번 들으면서 정리한 것이 다였기 때문에 기억이 희미해졌었다. Java 멀티스레딩, 병행성 및 성능 최적화 강의 덕분에 희미했던 운영체제에 대한 지식을 복습할 수 있어서 좋았다.&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;회사에서 이미지 업로드 성능 개선을 위해 스레드를 통한 병렬 처리를 사용하면서 Java와 Spring의 스레드 구현체와 사용법에 대해 공부한 적이 있었다. 그 당시에는 사용법 정도만 알고 있었는데, 강의를 통해 동작 원리와 병행성 문제에 대한 대처 방법 등을 추가로 공부해볼 수 있었다. 개인 프로젝트나 회사에서 성능 개선을 위해 스레드를 사용해본 경험이 있는 개발자라면 해당 강의를 강력 추천하고 싶다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>후기/Udemy 리뷰</category>
      <category>java</category>
      <category>Thread</category>
      <category>udemy</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/222</guid>
      <comments>https://myvelop.tistory.com/222#entry222comment</comments>
      <pubDate>Sat, 2 Mar 2024 23:24:05 +0900</pubDate>
    </item>
    <item>
      <title>[글또] 1차 글쓰기 세미나, 그리고 프로세스 1.0</title>
      <link>https://myvelop.tistory.com/221</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;**글또 9기의 활동 내용입니다. 1차 글쓰기 세미나에 참여 및 과제를 수행했습니다. 세미나 후기은 대체로 성윤님의 발표자료를 바탕으로 작성되었으며, 글의 중간중간 제 생각을 더해봤습니다.**&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1차 글쓰기 세미나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또의 1차 글쓰기 세미나는 1월 14일(일) 오후 9시에 열렸고 1시간 30분 정도 진행됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 글쓰기 세미나는 선택 참여 행사였다. 세미나 지원하려면 구글 폼 설문을 작성해야 했는데 그 내용을 바탕으로 발표자료를 준비해주신 것으로 보인다. 참여하지 않았으면 후회했을 거라고 느낄 정도로 좋았고, 글쓰기에 대한 나의 관점이 뒤바뀐 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글쓰기의 어려움&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글쓰기를 하기 전 항상 저항에 부딪힌다.&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;&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;자신의 문제를 스스로 정의하고 자신만의 방법으로 해결하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 글이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 좋은 글을 판단하는 근거는 주관적이다. 내가 좋다고 정의하는 글이 좋은 글이 될 수 있다.&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;최근 EP9의 한 칼럼을 읽었다. 케빈 켈리를 인터뷰한 내용이었다. 와이어드의 콘텐츠 만드는 기준 중 2가지가 눈에 띄었다. &lt;b&gt;&quot;스스로에게 설명하듯이 적어라&quot;&lt;/b&gt;, &lt;b&gt;&quot;편집자와 독자의 흥미는 정비례&quot;&lt;/b&gt;. 결국 편집자 본인이 이해할 수 있는 글, 좋다고 생각한 글이 남들도 보편적으로 좋은 콘텐츠라고 생각하는 글일 것이라는 내용이다.&lt;/p&gt;
&lt;figure id=&quot;og_1706427030995&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;케빈 켈리 : &amp;ldquo;기술을 낙관할 때, 우린 나아간다&amp;rdquo; 세계적인 IT 구루의 조언&quot; data-og-description=&quot;안녕, Ep9 피플! 우리의 첫 레코드는 케빈 켈리와의 대화로 시작할게. 그는 실리콘밸리의 &amp;lsquo;테크 구루&amp;rsquo;로 불려. 미국의 기술문화 잡지 「와이어드」를 공동 창간했고, 7년 동안 초대 편집장을 맡&quot; data-og-host=&quot;www.ep9.co&quot; data-og-source-url=&quot;https://www.ep9.co/record/1?seq=1&amp;amp;format=cover&quot; data-og-url=&quot;https://www.ep9.co/record/1?format=cover&amp;amp;seq=1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/4R9do/hyVb64i3ka/FHYtYZLLEd3zqeGnbNCoVK/img.png?width=1147&amp;amp;height=1147&amp;amp;face=399_265_691_584,https://scrap.kakaocdn.net/dn/vzigq/hyVcfGW3Cg/uEuLOwLF0hK28NWk99nYak/img.png?width=1147&amp;amp;height=1147&amp;amp;face=399_265_691_584&quot;&gt;&lt;a href=&quot;https://www.ep9.co/record/1?seq=1&amp;amp;format=cover&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.ep9.co/record/1?seq=1&amp;amp;format=cover&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/4R9do/hyVb64i3ka/FHYtYZLLEd3zqeGnbNCoVK/img.png?width=1147&amp;amp;height=1147&amp;amp;face=399_265_691_584,https://scrap.kakaocdn.net/dn/vzigq/hyVcfGW3Cg/uEuLOwLF0hK28NWk99nYak/img.png?width=1147&amp;amp;height=1147&amp;amp;face=399_265_691_584');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;케빈 켈리 : &amp;ldquo;기술을 낙관할 때, 우린 나아간다&amp;rdquo; 세계적인 IT 구루의 조언&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕, Ep9 피플! 우리의 첫 레코드는 케빈 켈리와의 대화로 시작할게. 그는 실리콘밸리의 &amp;lsquo;테크 구루&amp;rsquo;로 불려. 미국의 기술문화 잡지 「와이어드」를 공동 창간했고, 7년 동안 초대 편집장을 맡&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.ep9.co&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;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;p data-ke-size=&quot;size16&quot;&gt;'독자를 상정하고 그들이 잘 이해할 수 있는 글을 적어야하는데, 어떻게 해야 그런 글을 적을 수 있는건데?'&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 위에서 얘기했다시피 내가 잘 이해할 수 있는 글을 적는 것이 우선이고, 그걸로 글쓰기를 1차적으로 끝내는 것이 좋다. 처음부터 완벽한 글을 적을 수 있다고 생각하는 것은 오만이다. 처음엔 나를 위한 글을 작성하고 이후에 글을 발전시키자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글을 잘 쓰려면 어떻게 해야할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점진적 목표를 가지고 있는 것이 도움이 될 수 있다. 글쓰는 습관을 만들기, 글의 특정 부분 개선하기 등의 작은 목표로 시작해 조금씩 목표를 키워나가면 된다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, 글을 작성하고 회고하지 않으면 발전이 없다. 나의 글을 지속적으로 퇴고하고 보완할 수 있는 방법을 강구해야 한다. 다른 사람들에게 글을 피드백해달라고 부탁해보거나, GPT같은 툴을 사용해 피드백을 진행해볼 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글의 깊이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;깊이감? 굉장히 추상적인 단어다.&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;/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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글쓰기 파이프라인과 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미나 중 이 부분이 가장 인상적이었다. 글쓰기 과정을 도식화하거나 체계화해봐야 겠다는 생각을 가질 수 있다는 것이 놀라웠고, 전략 하나 없이 글을 써왔다는 사실을 반성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 성윤 님이 글쓰기 파이프라인과 전략을 만들 때 고려한 내용이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글쓰기 뿐만 아니라 글을 쓰기 위한 준비 과정 또한 문서화&lt;/li&gt;
&lt;li&gt;소재를 저장하고 공부하는 파트와 글쓰기 파트를 분리해 구성 (다만 글을 바로 적고 싶으면 바로 적었다고 하셨다.)&lt;/li&gt;
&lt;li&gt;글쓰기 집중을 위한 분석&lt;/li&gt;
&lt;li&gt;글쓰기 목표 설정(어떤 변화를 만들고 싶은지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세미나를 듣고 달라진 생각&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글쓰기에 대한 나의 관점에 변화가 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;글을 적기 전 개요 정도만 적었지, 한 번도 글을 잘쓰기 위한 고민을 해본 적이 없었다. &lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;글은 그냥 쓰기만 하면 된다고 생각했었던 것 같다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;글을 잘 쓰기 위해 이렇게까지 체계적으로 접근해볼 수 있다는 생각조차 못했고 더 나아지고자 하는 마음이 없었기 때문에 같은 레벨에서 맴돌고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #202124;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;나도 앞으로는 글을 잘 쓰기 위해 체계적으로 접근해볼 생각이다. 프로세스를 수립하고 회차마다 글쓰기 패턴을 분석해보려 한다.&lt;/span&gt;&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나만의 글쓰기 프로세스 1.0&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;프로세스를 생각한 적이 없었기에 글을 적을 때 체계가 없었다. 내가 적고 싶은 방식대로 적었다. 때때로 개요 작성을 건너뛰거나 자료수집과 동시에 글을 적은 적도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tZF4P/btsEkRYQ1vH/h4R81MCbgKL9vSI9wJvQo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tZF4P/btsEkRYQ1vH/h4R81MCbgKL9vSI9wJvQo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tZF4P/btsEkRYQ1vH/h4R81MCbgKL9vSI9wJvQo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtZF4P%2FbtsEkRYQ1vH%2Fh4R81MCbgKL9vSI9wJvQo0%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; alt=&quot;기존의 글쓰기 프로세스 도식화&quot; loading=&quot;lazy&quot; width=&quot;1328&quot; height=&quot;450&quot; data-origin-width=&quot;1328&quot; data-origin-height=&quot;450&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;h3 data-ke-size=&quot;size23&quot;&gt;새로운 글쓰기 파이프라인 도식화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개요에서 독자를 상정하고 글의 핵심 메시지를 정리하는 부분과 회고와 퇴고의 시간이 새롭게 추가되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 쓰다보면 핵심 주제를 놓칠 때가 잦다. 글을 본격적으로 작성하기 전에 글의 핵심 메시지를 미리 정리해놓는다면 큰 도움이 될 것 같아 추가했다. (성윤님이 추천해주신 방식) 또 글을 쓰고 나서 제대로 피드백을 했던 적이 없었다. 때때로 다시 읽어보고 문맥이 이상하면 고치는 정도로 마무리했었다. 앞으로는 글 쓸 때의 습관과 문제점을 파악하고 개선하기 위해 회고와 퇴고의 시간을 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1429&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bENtmi/btsEmDls8F7/LGyKkBGikz7m4wVACiDfuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bENtmi/btsEmDls8F7/LGyKkBGikz7m4wVACiDfuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bENtmi/btsEmDls8F7/LGyKkBGikz7m4wVACiDfuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbENtmi%2FbtsEmDls8F7%2FLGyKkBGikz7m4wVACiDfuk%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; alt=&quot;새로운 글쓰기 프로세스 도식화&quot; loading=&quot;lazy&quot; width=&quot;1429&quot; height=&quot;784&quot; data-origin-width=&quot;1429&quot; data-origin-height=&quot;784&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;h3 data-ke-size=&quot;size23&quot;&gt;글쓰기 및 준비과정(루틴)&lt;/h3&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;매주 일요일 19:00~22:00을 글쓰기 고정 시간으로 둔다.&lt;/li&gt;
&lt;li&gt;시간이 부족하면 주말에 추가 시간을 할당한다.&lt;/li&gt;
&lt;li&gt;시작하기 전, 핸드폰은 장롱 안에 넣어둔다.&lt;/li&gt;
&lt;li&gt;듣고 싶은 노래 하나를 선정해 한곡 재생으로 반복한다. (개인적으로 집중력 향상에 도움이 됐다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존의 글 퇴고하기!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셀프 피드백을 통해 글을 퇴고해보고자 한다. 아래는 수정된 글의 링크이다.&lt;/p&gt;
&lt;figure id=&quot;og_1707051398371&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Security] Securiry Logout&quot; data-og-description=&quot;Spring Securiry Logout 스프링 시큐리티는 로그아웃 기능을 제공한다. Config 파일에서 아무런 설정을 하지 않아도 기본적으로 제공되는 로그아웃 기능이 동작한다. 기본 로그아웃의 특징은 아래와 같&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/220&quot; data-og-url=&quot;https://myvelop.tistory.com/220&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/waU7M/hyVf97K3z4/76atctyS3gkpKovUCtVYRK/img.png?width=618&amp;amp;height=583&amp;amp;face=0_0_618_583,https://scrap.kakaocdn.net/dn/gcD74/hyVf5qKuu2/5dIW3fkrzFkKVvAS9gei00/img.png?width=618&amp;amp;height=583&amp;amp;face=0_0_618_583,https://scrap.kakaocdn.net/dn/bTzL03/hyVf1hzuXX/ivaR8g6hqcDZ9mhdBjALs1/img.png?width=1056&amp;amp;height=442&amp;amp;face=0_0_1056_442&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/220&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/220&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/waU7M/hyVf97K3z4/76atctyS3gkpKovUCtVYRK/img.png?width=618&amp;amp;height=583&amp;amp;face=0_0_618_583,https://scrap.kakaocdn.net/dn/gcD74/hyVf5qKuu2/5dIW3fkrzFkKVvAS9gei00/img.png?width=618&amp;amp;height=583&amp;amp;face=0_0_618_583,https://scrap.kakaocdn.net/dn/bTzL03/hyVf1hzuXX/ivaR8g6hqcDZ9mhdBjALs1/img.png?width=1056&amp;amp;height=442&amp;amp;face=0_0_1056_442');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Security] Securiry Logout&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Securiry Logout 스프링 시큐리티는 로그아웃 기능을 제공한다. Config 파일에서 아무런 설정을 하지 않아도 기본적으로 제공되는 로그아웃 기능이 동작한다. 기본 로그아웃의 특징은 아래와 같&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셀프 피드백&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글쓰기 세미나에서 들은 내용을 바탕으로 셀프 피드백을 진행해봤다.&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;/li&gt;
&lt;li&gt;내가 독자에게 전달하고 싶었던 핵심 내용은 Spring Security의 Logout 기능을 구현할 때 겪을 수 있는 문제들과 이를 해결하는 방법이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 제목 변경&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;수정 전&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목을 &quot;Security Logout&quot;라고만 해놓으니 이게 뭘 위한 글인지 알 수 없었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYpjO/btsEmD6QRVx/KlKd6bwacHZzyd3rk3IFj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYpjO/btsEmD6QRVx/KlKd6bwacHZzyd3rk3IFj0/img.png&quot; data-alt=&quot;제목 수정 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYpjO/btsEmD6QRVx/KlKd6bwacHZzyd3rk3IFj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYpjO%2FbtsEmD6QRVx%2FKlKd6bwacHZzyd3rk3IFj0%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;565&quot; height=&quot;164&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제목 수정 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;수정 후&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;독자들이 글의 내용이 무엇인지 제목에서 바로 알 수 있도록 수정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1648&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tRhFR/btsJvfy69SI/FU7dkCBVpAyV0SnMRBKtDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tRhFR/btsJvfy69SI/FU7dkCBVpAyV0SnMRBKtDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tRhFR/btsJvfy69SI/FU7dkCBVpAyV0SnMRBKtDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtRhFR%2FbtsJvfy69SI%2FFU7dkCBVpAyV0SnMRBKtDK%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;721&quot; height=&quot;161&quot; data-origin-width=&quot;1648&quot; data-origin-height=&quot;368&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;2. 목차 수정&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;수정 전&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;주의해야할 점&quot;은 두루뭉술해 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQ5nlE/btsElItQc9f/9uxDBh2HSAXP2kpMkDVKOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQ5nlE/btsElItQc9f/9uxDBh2HSAXP2kpMkDVKOK/img.png&quot; data-alt=&quot;목차 수정 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQ5nlE/btsElItQc9f/9uxDBh2HSAXP2kpMkDVKOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQ5nlE%2FbtsElItQc9f%2F9uxDBh2HSAXP2kpMkDVKOK%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;454&quot; height=&quot;122&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;목차 수정 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;수정 후&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 주제에 맞게 목차의 이름을 변경해줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;413&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3caC/btsEm95iuRA/SUga05TR6kwv4dhJiT3Wqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3caC/btsEm95iuRA/SUga05TR6kwv4dhJiT3Wqk/img.png&quot; data-alt=&quot;목차 수정 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3caC/btsEm95iuRA/SUga05TR6kwv4dhJiT3Wqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3caC%2FbtsEm95iuRA%2FSUga05TR6kwv4dhJiT3Wqk%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; alt=&quot;목차 수정 후&quot; loading=&quot;lazy&quot; width=&quot;413&quot; height=&quot;102&quot; data-origin-width=&quot;413&quot; data-origin-height=&quot;102&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 서론 추가&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y8Dh7/btsEkSDuWvX/JKm6PMZU2GPIENYjRmMLwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y8Dh7/btsEkSDuWvX/JKm6PMZU2GPIENYjRmMLwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y8Dh7/btsEkSDuWvX/JKm6PMZU2GPIENYjRmMLwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy8Dh7%2FbtsEkSDuWvX%2FJKm6PMZU2GPIENYjRmMLwK%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; alt=&quot;서론부 수정 전&quot; loading=&quot;lazy&quot; width=&quot;804&quot; height=&quot;462&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dN9MDs/btsEj5W9A8z/cy8fViHNHE9NUkd85i8tg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dN9MDs/btsEj5W9A8z/cy8fViHNHE9NUkd85i8tg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dN9MDs/btsEj5W9A8z/cy8fViHNHE9NUkd85i8tg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdN9MDs%2FbtsEj5W9A8z%2Fcy8fViHNHE9NUkd85i8tg1%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; alt=&quot;서론 추가&quot; loading=&quot;lazy&quot; width=&quot;804&quot; height=&quot;261&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;261&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;h3 data-ke-size=&quot;size23&quot;&gt;4. 결론 추가&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;수정 전&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글의 결론부가 없어 코드 예시에서 글이 뚝 끊겨버리는데 글의 맺음이 부자연스러워 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;863&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nsfgL/btsEnfxmXuT/vEKIwC3KKHkVDuwv9iYTkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nsfgL/btsEnfxmXuT/vEKIwC3KKHkVDuwv9iYTkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nsfgL/btsEnfxmXuT/vEKIwC3KKHkVDuwv9iYTkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnsfgL%2FbtsEnfxmXuT%2FvEKIwC3KKHkVDuwv9iYTkK%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; alt=&quot;결론부 수정 전&quot; loading=&quot;lazy&quot; width=&quot;824&quot; height=&quot;863&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;863&quot;/&gt;&lt;/span&gt;&lt;/figure&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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvflah/btsEm7fm4C2/zmLT8os5APnZpFBbQOQ45k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvflah/btsEm7fm4C2/zmLT8os5APnZpFBbQOQ45k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvflah/btsEm7fm4C2/zmLT8os5APnZpFBbQOQ45k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcvflah%2FbtsEm7fm4C2%2FzmLT8os5APnZpFBbQOQ45k%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; alt=&quot;결론 추가&quot; loading=&quot;lazy&quot; width=&quot;762&quot; height=&quot;238&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;238&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글을 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;세미나를 듣고 정리하며, 그 내용을 직접 실천에 옮겨보면서&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;내가 어떤 습관을 가지고 글을 작성하고 있었는지 파악할 수 있었다.&lt;/span&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;/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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;저작권 표시&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt; flaticon: &lt;a href=&quot;https://www.flaticon.com/kr/free-icons/&quot;&gt;https://www.flaticon.com/kr/free-icons/&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생각 아이콘&amp;nbsp;&amp;nbsp;제작자: BomSymbols&lt;/li&gt;
&lt;li&gt;글 머리 기호 목록 아이콘&amp;nbsp;&amp;nbsp;제작자: lutfix&lt;/li&gt;
&lt;li&gt;학생 아이콘&amp;nbsp;&amp;nbsp;제작자: Freepik&lt;/li&gt;
&lt;li&gt;프로그램 제작자 아이콘&amp;nbsp;&amp;nbsp;제작자: Freepik&lt;/li&gt;
&lt;li&gt;제출 아이콘&amp;nbsp;&amp;nbsp;제작자: Freepik&lt;/li&gt;
&lt;li&gt;회고 아이콘&amp;nbsp;&amp;nbsp;제작자: Freepik&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>대외활동/글또</category>
      <category>글또</category>
      <category>글또 9기</category>
      <category>글쓰기</category>
      <category>글쓰기 프로세스</category>
      <category>세미나</category>
      <category>퇴고</category>
      <category>회고</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/221</guid>
      <comments>https://myvelop.tistory.com/221#entry221comment</comments>
      <pubDate>Sun, 4 Feb 2024 22:27:27 +0900</pubDate>
    </item>
    <item>
      <title>2023년 신입개발자의 회고</title>
      <link>https://myvelop.tistory.com/219</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;조금 늦었지만 2023년 회고를 올려봅니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;작년 계획&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;책 읽기&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;To acquire the habit of reading is to construct for yourself a refuge from almost all the miseries of life.&lt;br /&gt;독서하는 습관은 인생의 거의 모든 불행으로부터 자신을 위한 은신처를 만드는 것이다.&lt;br /&gt;- 윌리엄 서머싯 몸&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 나이만큼 책읽기라는 계획이 있었지만 실패했다. 목표했던 26권보다 적은 12권의 책을 읽었다.&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;힘든 직장 생활, 외로운 삶 속에서 나의 정신을 온전히 유지하는 일이 쉽지 않았다. 직장에서의 적응을 핑계로 책을 잠시 손에서 놓았던 적이 있었다. 시간이 갈수록 시각은 편협해졌고 삶은 의미를 잃어갔다. &quot;생각하는대로 살자&quot;를 인생의 모토로 삼은 내가 어느 순간부터 사는대로 생각하고 있었다.&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;이대로는 안되겠다고 생각했고 자기 전에 한 페이지라도 읽기 시작했다. 추천을 받아&amp;nbsp;부캠 커뮤니티인 '&lt;span data-channel-id=&quot;C04K89TTGDR&quot; data-qa=&quot;inline_channel_entity&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span data-qa=&quot;inline_channel_entity__name&quot;&gt;부각코-1day-1page-miracle'에도 참여했었다. &lt;s&gt;이제는&amp;nbsp;인증을 거의 안 올리고 있지만..&lt;/s&gt; 매일매일 책을 읽고 인증을 하는 모임이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MYtpU/btsEdDrBKw4/M8NWBEbHJUDdwlce1ROeO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MYtpU/btsEdDrBKw4/M8NWBEbHJUDdwlce1ROeO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MYtpU/btsEdDrBKw4/M8NWBEbHJUDdwlce1ROeO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMYtpU%2FbtsEdDrBKw4%2FM8NWBEbHJUDdwlce1ROeO0%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; alt=&quot;글읽기 인증&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;800&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;800&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;과도기를 거쳐 다행히 책 읽는 습관이 다시 정착되었다. 긴 시간을 책에 쏟진 못하지만 적어도 매일 30분 정도의 시간을 내서 책을 읽고 있다. 우연찮게도 회사에 책을 좋아하는 분과 함께 책 얘기를 나누고 서로 추천해주고 있는데 나의 책읽기 습관에 큰 도움이 되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 1일 평균 방문자 &lt;b&gt;500명 달성이 목표였고, 얼떨결에 성공&lt;/b&gt;했다. 직장 생활을 시작한 후로 블로그 글을 자주 작성하진 않았지만 스킨을 재적용하면서 블로그의 퍼포먼스가 올라갔다. 그 영향인지는 몰라도 갑자기 방문자수가 급증해서 목표를 달성할 수 있었다. 평일 평균 500~600명의 방문자가 블로그에 방문하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;취업&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 초, 취업에 성공해 3월에 입사했고 잘 다니고 있다.&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;h3 data-ke-size=&quot;size23&quot;&gt;1일 1커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 성공했다. 때때로 야근하고 집에 돌아오면 바로 쉬지 못하고 무엇이라도 커밋을 해야 한다는 점이 힘들고 고통스러웠다. 주변 사람들 중 1일 1커밋을 하던 사람들도 취업하고 나서는 커밋이 뜸해지는 것을 지켜보면서 나도 포기하고 싶다는 유혹에 시달렸다. 그래도 2023년만큼은 꼭 해내고 싶었고 결국 해냈다.&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;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BRyt3/btsD4H8jnFp/CL1dfzyXhryWxt1F54K50K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BRyt3/btsD4H8jnFp/CL1dfzyXhryWxt1F54K50K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BRyt3/btsD4H8jnFp/CL1dfzyXhryWxt1F54K50K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBRyt3%2FbtsD4H8jnFp%2FCL1dfzyXhryWxt1F54K50K%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; alt=&quot;1일 1커밋&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;224&quot; data-origin-width=&quot;958&quot; data-origin-height=&quot;224&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 일이 나의 일인 것처럼 했다. 찾아서 일을 벌렸다. 덕분에 인생이 많이 고달파졌지만 내가 해보고 싶은 것들을 다 해볼 수 있었다. 그 과정에서 개발뿐만 아니라 비즈니스와 기획, 조직에 대한 이해도가 높아지고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;고민이 깊어졌다&lt;/span&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;figure id=&quot;og_1706340582348&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;신입 개발자! 회사와 함께 성장하기&quot; data-og-description=&quot;작년 3월 신입 개발자로 입사한 나는 부푼 꿈을 안고 개발자로서의 커리어를 시작했다. 작은 스타트업이었다. 코드리뷰는 당연히 없었고, 업무를 위해 참고할 수 있는 문서가 단 한 장도 없었다.&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/216&quot; data-og-url=&quot;https://myvelop.tistory.com/216&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/SL6GM/hyU81QXw0Z/975vhC3fKyo31cpFrGikOK/img.png?width=800&amp;amp;height=228&amp;amp;face=0_0_800_228,https://scrap.kakaocdn.net/dn/4LVWG/hyVb2Od6Fp/oJkXa7KMMJQkTpSiEyK751/img.png?width=800&amp;amp;height=228&amp;amp;face=0_0_800_228,https://scrap.kakaocdn.net/dn/5hsdw/hyU8VJZCM5/iYkNmMetiD0Iftrk8BkRS0/img.png?width=1474&amp;amp;height=1064&amp;amp;face=0_0_1474_1064&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/216&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/216&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/SL6GM/hyU81QXw0Z/975vhC3fKyo31cpFrGikOK/img.png?width=800&amp;amp;height=228&amp;amp;face=0_0_800_228,https://scrap.kakaocdn.net/dn/4LVWG/hyVb2Od6Fp/oJkXa7KMMJQkTpSiEyK751/img.png?width=800&amp;amp;height=228&amp;amp;face=0_0_800_228,https://scrap.kakaocdn.net/dn/5hsdw/hyU8VJZCM5/iYkNmMetiD0Iftrk8BkRS0/img.png?width=1474&amp;amp;height=1064&amp;amp;face=0_0_1474_1064');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;신입 개발자! 회사와 함께 성장하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;작년 3월 신입 개발자로 입사한 나는 부푼 꿈을 안고 개발자로서의 커리어를 시작했다. 작은 스타트업이었다. 코드리뷰는 당연히 없었고, 업무를 위해 참고할 수 있는 문서가 단 한 장도 없었다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 힘들었던 것?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년 동안 회사에서 가장 힘든 일이라면 지금 하고 있는 신규 프로젝트(커머스)이다. &lt;s&gt;1년차도 안된 풋내기&lt;/s&gt; 책임자로 해당 프로젝트를 리드하게 되었다. 오래된 레거시 프로젝트(ASP + javascript + 스토어드 프로시저)를 Spring, JPA, QueryDsl, Nuxt 환경으로 마이그레이션하는 작업이다. 300개의 테이블과 699개의 프로시저가 존재했는데 사용하지 않는 것이 많아 테이블을 가려내는 작업만 해도 며칠이 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테이블은 하나 같이 불합리했다. 예를 들어, primary key를 지칭하는 값이 id, idx, 테이블명_idx, idx_number, 테이블명_number와 같이 제각각이라 BaseEntity를 사용할 수 없었다. 결국, 테이블도 아예 다 뜯어고쳐 마이그레이션 했다. 이상한 관계로 엮어 있는 테이블들의 관계를 다시 정립하고, 불필요한 컬럼을 덜어냈다. 그리고 테이블을 정규화를 진행했다.&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;p data-ke-size=&quot;size16&quot;&gt;로직에 대한 고민, 데드라인 압박, 나의 부족함으로 인한 자괴감 때문에 밤에 잠이 안 올 지경이었다. 회사에서 신입한테 너무 많은 것을 바라는 것 아닌가하는 원망이 아예 없었다면 거짓말일 것이다.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;솔직히 이 일은 나에게 버거운 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;s&gt;야근 수당도 주어지지 않는&lt;/s&gt; 고생길을 내가 자처한 이유는 나의 성장에 도움이 되고 있다고 느꼈기 때문이었다. 1년차 신입이 도대체 어떤 회사에서 프로젝트의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;전체적인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;업무 일지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 업무를 할 때, 업무 일지를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈가 있으면 업무 정리에 따로 시도해본 방법과 공부한 내용을 정리해두는데 이 습관이 여러모로 도움이 된다. 업무 일지를 바탕으로 블로그 글 소재도 쏙쏙 뽑아낼 수 있고, 일할 때 참고자료도 된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lsDvE/btsEbzpHoY8/VJN1LdfyNb5qSknQgCcuKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lsDvE/btsEbzpHoY8/VJN1LdfyNb5qSknQgCcuKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lsDvE/btsEbzpHoY8/VJN1LdfyNb5qSknQgCcuKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlsDvE%2FbtsEbzpHoY8%2FVJN1LdfyNb5qSknQgCcuKk%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; alt=&quot;업무일지&quot; loading=&quot;lazy&quot; width=&quot;546&quot; height=&quot;1326&quot; data-origin-width=&quot;1652&quot; data-origin-height=&quot;1326&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;학습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;강의 및 공부&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프런 강의를 총 10편 완강했다. 스프링 MVC, 스프링 JPA 기본편 및 활용 2개편 , 스프링 데이터 JPA, QueryDSL, 스프링 부트 핵심 원리, 스프링 고급편, 스프링 JDBC 2개편 등 영한님 강의를 들었다.&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;h3 data-ke-size=&quot;size23&quot;&gt;사이드 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부스트캠프 동기들과 함께 원바이트라는 팀을 만들어 사이드 프로젝트를 시작했다. 직장인 8명으로 구성된 팀이었다. 회사에서 적용해볼 기술을 충분히 연습할 수 있어 회사 일에 굉장히 큰 도움이 됐다. 마감기한이 촉박한 프로젝트에서 RestDocs + Swagger UI 적용이나 Spring Security 작업이 필요했는데, 사이드 프로젝트의 경험을 살려 해당 작업을 빠르게 끝낼 수 있었다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스터디&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10월부터 프론트엔드 스터디인 차가운 스터디를 시작했다. 백엔드 개발자지만 회사에서 프론트엔드를 직접 개발해야할 일이 꽤 있었기에 스터디에 참여해 FE 개발자 분들의 인사이트를 쏙쏙 빼오고 싶었다. 그런데 아는 게 있는만큼 보인다고 스터디원들이 하는 얘기를 대체로 이해할 수 없었다. 그래도 개발을 한다는 본질은 같았기 때문에 배운 점이 많았다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외부 활동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글또&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023년 12월부터 글또 활동을 시작했다. 회사에서 사용한 기술에 대해서 깊게 공부해보고 정리하는 시간이 되고 있다. 직장 생활을 시작하고 나서 블로그에 소홀했었는데 글또 덕분에 글에 대한 열정에 다시 불이 지펴졌다.&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;일단 현재(4회차)까지는 패스 없이 블로그 글을 제출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3회차에는 큐레이션에도 뽑혔다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3X5SD/btsD2vVzbM5/Vm5dvrEKFzjtG5IakycqRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3X5SD/btsD2vVzbM5/Vm5dvrEKFzjtG5IakycqRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3X5SD/btsD2vVzbM5/Vm5dvrEKFzjtG5IakycqRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3X5SD%2FbtsD2vVzbM5%2FVm5dvrEKFzjtG5IakycqRK%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; alt=&quot;3회차 큐레이션&quot; loading=&quot;lazy&quot; width=&quot;754&quot; height=&quot;618&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;618&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;h3 data-ke-size=&quot;size23&quot;&gt;개발자 컨퍼런스 참여&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023년 총 3번의 컨퍼런스(네이버 Deview, 스프링 캠프, 인프콘)에 참여했다. Deview와 스프링 캠프는 티켓팅에 성공했고, 인프콘은 인프런에 다니는 지인에게 초대권을 받아 다녀왔다. 컨퍼런스에서 내가 모르는 내용이 너무 많아 당황스러웠다. 내용을 정말 집중해서 듣지 않는 이상 알아듣기 어려웠다.&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;그런데 시간의 텀을 가지고 컨퍼런스에 다녀오면서 느낀 게 있는데 내가 점점 성장하고 있다는 사실이었다. Deview의 내용은 정말 하나도 알아들을 수 없었는데, Spring Camp에서는 조금 덜해졌다. 8월에 열린 인프콘에서는 기술적인 얘기를 들으면 대체로 이해할 정도의 수준이 되었다.&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;2024년에도 기회가 된다면 컨퍼런스에 참여해 회사에 적용해볼 수 있는 내용을 터득해가고 싶다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 컨퍼런스 후기이다. &lt;s&gt;아쉽게도 인프콘 후기는 없다.&lt;/s&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1706364401864&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;조금 늦은 Deview 2023 후기&quot; data-og-description=&quot;Deveiw 2023에 참가하게된 계기? 2월 초, 여러 회사를 기웃거리며 이력서를 넣고 코딩테스트와 면접을 전전하고 있었다. 계속해서 탈락의 고배를 마시며 자신감을 조금씩 잃어갔고, 점점 기술적으&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/207&quot; data-og-url=&quot;https://myvelop.tistory.com/207&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fbZPM/hyU8TyHHa2/ivHfqdCA4ExkSdpMdBKkK1/img.jpg?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/pYxir/hyVb8npyEJ/nbsEaZhbJAOMXrISXZTFuK/img.jpg?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/EoAGH/hyVcf7Vsdq/exzRzaV5Rq6Kn9lRUPuHz0/img.png?width=600&amp;amp;height=449&amp;amp;face=0_0_600_449&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/207&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/207&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fbZPM/hyU8TyHHa2/ivHfqdCA4ExkSdpMdBKkK1/img.jpg?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/pYxir/hyVb8npyEJ/nbsEaZhbJAOMXrISXZTFuK/img.jpg?width=800&amp;amp;height=420&amp;amp;face=0_0_800_420,https://scrap.kakaocdn.net/dn/EoAGH/hyVcf7Vsdq/exzRzaV5Rq6Kn9lRUPuHz0/img.png?width=600&amp;amp;height=449&amp;amp;face=0_0_600_449');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;조금 늦은 Deview 2023 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Deveiw 2023에 참가하게된 계기? 2월 초, 여러 회사를 기웃거리며 이력서를 넣고 코딩테스트와 면접을 전전하고 있었다. 계속해서 탈락의 고배를 마시며 자신감을 조금씩 잃어갔고, 점점 기술적으&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1706364413947&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;2023 Spring Camp 근데 이제 현업 경험을 곁들인&quot; data-og-description=&quot;스프링 캠프? 3월에 막 입사하여 신입 개발자로 근근이 살아던 중 인프런에 재직 중인 부스트캠프 동기 김00 군에게 연락이 왔다. 아래 링크를 던져주면서 참가할 생각이 있냐고 물어봤다. [오프&quot; data-og-host=&quot;myvelop.tistory.com&quot; data-og-source-url=&quot;https://myvelop.tistory.com/211&quot; data-og-url=&quot;https://myvelop.tistory.com/211&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TAwYA/hyU81jbD0U/dF78zKlUwM7q14icfLFMg1/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599,https://scrap.kakaocdn.net/dn/hl4El/hyVb9mk0mP/2UElP3gkjKle4XkmPPFKuK/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599,https://scrap.kakaocdn.net/dn/morFD/hyVcd3kham/MGwcWzcEuKTdNSnu107FN1/img.jpg?width=1411&amp;amp;height=1058&amp;amp;face=0_0_1411_1058&quot;&gt;&lt;a href=&quot;https://myvelop.tistory.com/211&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://myvelop.tistory.com/211&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TAwYA/hyU81jbD0U/dF78zKlUwM7q14icfLFMg1/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599,https://scrap.kakaocdn.net/dn/hl4El/hyVb9mk0mP/2UElP3gkjKle4XkmPPFKuK/img.jpg?width=800&amp;amp;height=599&amp;amp;face=0_0_800_599,https://scrap.kakaocdn.net/dn/morFD/hyVcd3kham/MGwcWzcEuKTdNSnu107FN1/img.jpg?width=1411&amp;amp;height=1058&amp;amp;face=0_0_1411_1058');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2023 Spring Camp 근데 이제 현업 경험을 곁들인&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스프링 캠프? 3월에 막 입사하여 신입 개발자로 근근이 살아던 중 인프런에 재직 중인 부스트캠프 동기 김00 군에게 연락이 왔다. 아래 링크를 던져주면서 참가할 생각이 있냐고 물어봤다. [오프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;myvelop.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삶&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생활&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3월에 회사 생활을 시작하면서 본가로부터 독립했다.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;나는 물리적, 경제적인 독립을 해야 어른답게 살 수 있다고 생각한다.&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;취업준비를 할 때, 오랜만에 본가에 얹혀 살았다. 부모의 품 안에 있다는 편안함과 안정감 때문인지 날이 갈수록 나약해진다는 느낌을 떨쳐낼 수 없었다. 그래서인지 재빨리 집을 뛰쳐나오고 싶었다. 취업이 됐을 때 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;부모님은 보증금이 모일 때까지 출퇴근하길 원하셨지만 나는 고민 없이 대출을 받아 &lt;/span&gt;자취방을 구했다. 의존하지 않는 삶은 나를 강하게 만들었고, 그 덕에 개인시간에 무엇을 할 지 &quot;선택&quot;할 수 있는 힘이 생겼다.&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;건강과 운동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 앉아서 생활하다보니 자세도 안 좋아지고, 몸상태가 안 좋아지고 있다고 느껴지기 시작했다. 그래서 6월부터 헬스를 시작했다. 처음 시작할 땐 점심시간에 30분 동안 잠깐 하는 정도였는데 욕심이 생겨서 아침과 점심에 나눠 1시간 30분씩 매일 운동을 하고 있다. &lt;s&gt;운동을 하니 데드라인에 치여 줄야근을 하더라도 버틸만 하다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고민&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 정신없이 바쁘다. 회사와 집이 반복되는 삶은 쉽지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업을 준비할 때까지만 해도 회사에 들어가기만 하면 소원이 없겠다, 매일 격무에 시달려도 상관없다고 생각했다. 주중에 회사 일을 하고 돌아와서 부족한 공부를 하고, 주말에도 회사에 나가 일을 하거나 공부를 하고 블로그를 쓰고 있으면 문득 머리 속에 물음표가 그려진다. 이런 삶이 지속 가능할까?&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;이렇게 사는 게 내가 원하는 삶이었는지 말이다.&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;하지만 뭔가에 쫓기듯 사는 삶이 나를 지치게 만든다. 여유가 사라지니 사람이 상당히 쪼잔해지고 시야가 좁아졌다. 내가 힘들더라도 다른 사람을 먼저 살펴볼 수 있는 사람이었는데, 지금은 나 하나 보살피는 것조차 버겁다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 목표에 대해서 다시 생각해봐야할 것 같다. 하루하루 열심히 살아가다보면 길을 잃은 것 같은 느낌이 든다. 내가 진정 원하는 목표가 맞을까? 목표를 이루면 내가 정말 행복해질까? 나는 그냥 행복하게 살고 싶었을 뿐인데, 주객이 전도된 상태일지도 모르겠다.&lt;/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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2024년 계획&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;나이만큼 책읽기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나이만큼 책읽기&quot;에 재도전하려고 한다. 총 27권이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작이 좋다. 1월 한달만 해도 벌써 3권의 책을 읽었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 페이스대로면 1년에 36권으로 초과 달성 가능할 수도..?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1일 1커밋&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년에도 1일 1커밋에 도전해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 한 해 동안 1일 1커밋을 명분으로 꾸준히 공부할 수 있었다.&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; 지금 진행하고 있는 프로젝트의 마감기한이 얼마 남지 않아 야근을 거의 매일 같이 하고 있다. 육체적, 정신적으로 지친 상태라 다른 친구들처럼 1일 1커밋에 연연하지 않고 내가 하고 싶을 때만 코딩을 하는 방식으로의 변경을 고민도 했다. 하지만 그렇게 했을 때, 공부의 끈을 놓게 될 수 있을 것 같아 일단 1일 1커밋을 지속하기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;블로그 하루 방문자 1,000명에 도전&lt;/b&gt;할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 9기에 지원할 때도 작성했던 목표였다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;겸손해지기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 드는 생각. 열심히 사는 척을 너무 한다. &lt;s&gt;내가 생각해도 나는 정말 재수가 없다.&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입으로 하지 말고 행동으로만 하자.&lt;/p&gt;</description>
      <category>끄적끄적</category>
      <category>개발자</category>
      <category>계획</category>
      <category>회고</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/219</guid>
      <comments>https://myvelop.tistory.com/219#entry219comment</comments>
      <pubDate>Sun, 28 Jan 2024 00:42:21 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] LogoutFilter 개념부터 사용법까지</title>
      <link>https://myvelop.tistory.com/220</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 손쉽게 사용할 수 있는 기본 LogoutFilter를 제공한다. 기능을 기본 LogoutFilter 스펙에 맞춰서 구현했다면 아무런 문제가 없겠지만 그렇지 않을 경우(예를 들어 JWT로 인증/인가를 구현) 로그아웃을 했을 때 알 수 없는 오류가 터지기 시작한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 땐 내가 원하는 기능에 맞춰 로그아웃 기능을 커스터마이징이 필요한데,&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그 기능을 정확히 이해하지 않으면 또 다른 문제가 발생할 수 있다. 지금부터 LogoutFilter의 개념과 구현 과정 중 문제 어떻게 해결할 수 있는지 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Securiry Logout&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티는 로그아웃 기능을 제공한다. Config 파일에서 아무런 설정을 하지 않아도 기본적으로 제공되는 로그아웃 기능이 동작한다. 기본 로그아웃의 특징은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;/logout&amp;nbsp;&lt;/span&gt; Path로 GET이나 POST 요청&lt;/li&gt;
&lt;li&gt;ServerCsrfTokenRepository&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;ServerSecurityContextRepository&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;를 비워준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;RememberMe Authentication을 지운다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;저장되어 있는 모든 CSRF token을 지운다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;LogoutSuccessEventPublishingLogoutHandler를 통해 LogoutSuccessEvent라는 이벤트를 발행한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 로직이 수행되기 위해서는 Spring Security의 Filter Chain 중 LogoutFilter를 거쳐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LogoutFilter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 Filter Chain을 사용해 애플리케이션의 엔드 포인트 요청에 도달하기 전에 요청을 가로채 인증/인가 로직을 수행한다. 특정 요건(URI)이 충족되면 로직을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security의 Filter Chain은 아래 순서대로 진행된다. Logout Filter에 설정되어 있는 URI, Method와 일치하는 요청이 들어오면 LogoutFilter에서 해당 요청을 채간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T4vi8/btsDGnRI8ug/zW2KVXARtmCQqpEBEiogPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T4vi8/btsDGnRI8ug/zW2KVXARtmCQqpEBEiogPk/img.png&quot; data-alt=&quot;Spring Seuciry Filter Chain&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T4vi8/btsDGnRI8ug/zW2KVXARtmCQqpEBEiogPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT4vi8%2FbtsDGnRI8ug%2FzW2KVXARtmCQqpEBEiogPk%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; alt=&quot;security filter chain&quot; loading=&quot;lazy&quot; width=&quot;618&quot; height=&quot;583&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring Seuciry Filter Chain&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogoutFilter에 요청이 도달했을 때의 실행과정은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LogoutFilter에 요청이 도착하면 AntPathRequestMatcher를 통해 URI가 일치하는지 확인한다. 만약 일치하면 LogoutFilter의 로직이 실행된다.&lt;/li&gt;
&lt;li&gt;Security Context에서 Authentication 객체를 찾고 그것을 LogoutHandler로 넘겨준다.&lt;/li&gt;
&lt;li&gt;LogoutHandler에서 세션 무효화, 쿠키 삭제, SecurityContext 삭제, 로그인 페이지 리다이렉트 등의 필요한 작업을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Logout 커스터마이징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring Security에서 기본적으로 제공하는 로그아웃 기능만으로는 요구사항을 충족하지 못하는 경우가 생길 수 있다. 예를 들어, 인증/인가를 위해 세션이 아닌 JWT를 사용한다거나, 로그아웃 Path를 다르게 하고 싶을 수 있다. 이런 경우 Logout을 커스터마이징하면 된다. Security에서 HttpSecurity 객체의 체이닝 메소드 중 logout 메소드를 사용해 로그아웃을 커스터마이징할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1705819959742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class HttpSecurity extends AbstractConfiguredSecurityBuilder&amp;lt;DefaultSecurityFilterChain, HttpSecurity&amp;gt;
    implements SecurityBuilder&amp;lt;DefaultSecurityFilterChain&amp;gt;, HttpSecurityBuilder&amp;lt;HttpSecurity&amp;gt; {
  
  ...
  
  public HttpSecurity logout(Customizer&amp;lt;LogoutConfigurer&amp;lt;HttpSecurity&amp;gt;&amp;gt; logoutCustomizer) throws Exception {
    logoutCustomizer.customize(getOrApply(new LogoutConfigurer&amp;lt;&amp;gt;()));
    return HttpSecurity.this;
  } 
  
  ...
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logout 체이닝 메소드는 LogoutConfigurer를 전달 받는데, LogoutConfigurer를 통해 로그아웃 기능을 핸들링할 수 있다. 아래는 LogoutConfigurer에서 제공하는 핵심 메소드이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;logoutUrl(String logoutUrl)&lt;/li&gt;
&lt;li&gt;logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler)&lt;/li&gt;
&lt;li&gt;addLogoutHandler(LogoutHandler logoutHandler)&lt;/li&gt;
&lt;li&gt;deleteCookies(String... cookieNamesToClear)&lt;/li&gt;
&lt;li&gt;logoutRequestMatcher(RequestMatcher logoutRequestMatcher)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. URI 커스텀&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LogoutConfigurer의&lt;span&gt;&amp;nbsp;&lt;/span&gt;logoutUrl(String logoutUrl)를 사용하여 로그아웃 시 사용할 URI를 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;아래와 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;logoutUrl&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드에 스트링 형식의 path를 전달하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1705820157695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
  
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .logout((logout) -&amp;gt; logout.logoutUrl(&quot;/api/v1/logout&quot;));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Clean Up Handler Custom&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 아닌 쿠키를 사용해 인증/인가 작업을 진행하고 로그아웃 로직을 만들고 싶다면 LogoutConfigurer의 addLogoutHandler를 사용해 Clean Up Handler를 커스터마이징해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시는 스프링 시큐리티에서 기본적으로 제공하는 쿠키 로그아웃 핸들러인 CookieClearingLogoutHandler를 활용한 코드 예시이다. 생성자는&lt;span&gt;&amp;nbsp;&lt;/span&gt;CookieClearingLogoutHandler(String... cookiesToClear)이다. 제거하고 싶은 쿠키를 전달하고 싶은만큼 전달하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1705820290073&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  ...
  
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .logout((logout) -&amp;gt; logout.addLogoutHandler(new CookieClearingLogoutHandler(&quot;accessToken&quot;, &quot;refreshToken&quot;)));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CookieClearingLogoutHandler 대신 LogoutConfigurer의 deleteCookies(String... cookieNamesToClear) 메소드를 활용해도 동일하게 동작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1705820371004&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  ...
  
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .logout((logout) -&amp;gt; logout.deleteCookies(&quot;accessToken&quot;, &quot;refreshToken&quot;));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Success Handler Custom&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LogoutConfigurer의 logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) 메소드를 사용해 로그아웃이 성공했을 때의 동작을 제어할 수 있는 핸들러를 지정할 수 있다. 아래는 코드 예시이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705820560581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  ...
  
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .logout((logout) -&amp;gt; logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LogoutFilter를 사용할 때 발생하는 문제들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 로그아웃 Success Handling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그아웃 성공 핸들러를 따로 설정하지 않으면 로그아웃이 성공했을 때, LogoutConfigurer는 createDefaultSuccessHandler()를 사용해 기본 LogoutSuccessHandler를 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1705821085510&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class LogoutConfigurer&amp;lt;H extends HttpSecurityBuilder&amp;lt;H&amp;gt;&amp;gt;
		extends AbstractHttpConfigurer&amp;lt;LogoutConfigurer&amp;lt;H&amp;gt;, H&amp;gt; {
        
	...
    
	public LogoutSuccessHandler getLogoutSuccessHandler() {
		LogoutSuccessHandler handler = this.logoutSuccessHandler;
		if (handler == null) {
			handler = createDefaultSuccessHandler();
			this.logoutSuccessHandler = handler;
		}
		return handler;
	}

	private LogoutSuccessHandler createDefaultSuccessHandler() {
		SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler();
		urlLogoutHandler.setDefaultTargetUrl(this.logoutSuccessUrl);
		if (this.defaultLogoutSuccessHandlerMappings.isEmpty()) {
			return urlLogoutHandler;
		}
		DelegatingLogoutSuccessHandler successHandler = new DelegatingLogoutSuccessHandler(
				this.defaultLogoutSuccessHandlerMappings);
		successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler);
		return successHandler;
	}
   	
	...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 핸들러인 SimpleUrlLogoutSuccessHandler는 /login으로 리다이렉트를하는 동작을 수행한다. 이 때 문제가 발생할 수 있다. /login 자원에 아무것도 할당되어 있지 않다면 로그아웃을 성공했는데도 아래와 같이 404 에러가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/punm7/btsDJlrR1rJ/AVBzfsVl2cfZGZIhpOQzp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/punm7/btsDJlrR1rJ/AVBzfsVl2cfZGZIhpOQzp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/punm7/btsDJlrR1rJ/AVBzfsVl2cfZGZIhpOQzp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpunm7%2FbtsDJlrR1rJ%2FAVBzfsVl2cfZGZIhpOQzp1%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; alt=&quot;/login not found error&quot; loading=&quot;lazy&quot; width=&quot;1056&quot; height=&quot;442&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;442&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 쿠키 삭제의 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 확인했다시피 스프링에서&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; deleteCookies &lt;/span&gt;와 같은 메소드나&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; CookieClearingLogoutHandler &lt;/span&gt;와 같은 클래스를 사용해 쿠키를 비워주는 로직을 별다른 코드 작성 없이 실행할 수 있었다. 하지만 여기에는 치명적인 단점이 존재하는데, Cookie 객체를 사용해 HttpServletResponse에 addCookie 메소드로 담은 쿠키가 아니면 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Spring Security에서 제공하는 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;CookieClearingLogoutHandler &lt;/span&gt;의 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1705819419056&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class CookieClearingLogoutHandler implements LogoutHandler {

	private final List&amp;lt;Function&amp;lt;HttpServletRequest, Cookie&amp;gt;&amp;gt; cookiesToClear;

	public CookieClearingLogoutHandler(String... cookiesToClear) {
		Assert.notNull(cookiesToClear, &quot;List of cookies cannot be null&quot;);
		List&amp;lt;Function&amp;lt;HttpServletRequest, Cookie&amp;gt;&amp;gt; cookieList = new ArrayList&amp;lt;&amp;gt;();
		for (String cookieName : cookiesToClear) {
			cookieList.add((request) -&amp;gt; {
				Cookie cookie = new Cookie(cookieName, null);
				String contextPath = request.getContextPath();
				String cookiePath = StringUtils.hasText(contextPath) ? contextPath : &quot;/&quot;;
				cookie.setPath(cookiePath);
				cookie.setMaxAge(0);
				cookie.setSecure(request.isSecure());
				return cookie;
			});
		}
		this.cookiesToClear = cookieList;
	}

	/**
	 * @param cookiesToClear - One or more Cookie objects that must have maxAge of 0
	 * @since 5.2
	 */
	public CookieClearingLogoutHandler(Cookie... cookiesToClear) {
		Assert.notNull(cookiesToClear, &quot;List of cookies cannot be null&quot;);
		List&amp;lt;Function&amp;lt;HttpServletRequest, Cookie&amp;gt;&amp;gt; cookieList = new ArrayList&amp;lt;&amp;gt;();
		for (Cookie cookie : cookiesToClear) {
			Assert.isTrue(cookie.getMaxAge() == 0, &quot;Cookie maxAge must be 0&quot;);
			cookieList.add((request) -&amp;gt; cookie);
		}
		this.cookiesToClear = cookieList;
	}

	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		this.cookiesToClear.forEach((f) -&amp;gt; response.addCookie(f.apply(request)));
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseCookie 객체를 사용해 쿠키를 발행하는 방식은 Cookie 객체를 사용하는 것과는 다르게 same site 설정을 할 수 있다는 장점이 있는데 same site 설정을 활용하기 위해 ResponseCookie를 활용했다고 가정해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1705822826734&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

  private final TokenService tokenService;

	@Override
    public void onAuthenticationSuccess(
      HttpServletRequest request, HttpServletResponse response, Authentication authentication)
      throws IOException, ServletException {

        String accessToken = tokenService.generateAccessToken(authentication);
        response.setHeader(HttpHeaders.SET_COOKIE, makeTokenCookie(ACCESS_TOKEN, accessToken).toString());
        sendResponse(authentication, response);
    }

	private ResponseCookie makeTokenCookie(String key, String token) {
        return ResponseCookie.from(key, token)
            .httpOnly(true)
            .sameSite(&quot;None&quot;)
            .secure(true)
            .maxAge(TOKEN_EXPIRE_PERIOD)
            .path(&quot;/&quot;)
            .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseCookie 객체를 사용해 쿠키를 생성하고 HttpServletResponse의 setHeader 메소드를 사용해 쿠키를 설정해주면 Cookie 객체가 Response에 들어가지 않는다. 이 상태에서 Logout 로직을 실행하면 &lt;span style=&quot;background-color: #dddddd; color: #333333; text-align: start;&quot;&gt;CookieClearingLogoutHandler&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&amp;nbsp;에서는 아래의 에러를 터트린다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1705819550348&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;11:23:31.752 [http-nio-5555-exec-10] ERROR org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Filter execution threw an exception] with root cause
java.lang.NoSuchMethodError: 'java.lang.String jakarta.servlet.http.Cookie.getAttribute(java.lang.String)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 에러를 해결하기 위해서는 시큐리티에서 기본 제공하는 쿠키 제거 방식이 아닌 헤더에서 쿠키를 제거해주는 Custom Clean Up Handler를 만들어 적용해줘야 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Logout 로직 만들어보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 전제를 바탕으로 Logout 로직을 작성해봤다. 위에서 제시한 문제점을 어떻게 해결할 수 있는지 살펴보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 방식의 인증/인가를 사용하며 ResponseCookie를 통해 쿠키를 구성하고 HttpServletResponse의 setHeader 메소드를 사용해 쿠키를 할당한다.&lt;/li&gt;
&lt;li&gt;로그아웃를 하기 위한 경로는 /api/v1/logout 이다.&lt;/li&gt;
&lt;li&gt;로그아웃이 성공했을 때, /login으로 리다이렉트되는 것이 아닌 200 OK를 주길 원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. LogoutSuccessHandler 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서의 예시처럼 HttpStatusReturningLogoutSuccessHandler 객체를 할당해주면 /login으로 강제로 리다이렉트되는 문제는 해결할 수 있지만 다른 로직을 추가할 수 없다는 단점이 있다. 로직을 추가하고 싶다면 LogoutSuccessHandler 인터페이스의 구현체를 만들어 코드를 작성하면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1705821769305&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

  @Override
  public void onLogoutSuccess(
      HttpServletRequest request, HttpServletResponse response, Authentication authentication)
      throws IOException, ServletException {
	
    ...
    추가로직 작성
    ...
    
    response.setStatus(HttpStatus.OK.value());
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Clean Up Handler 작성하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseCookie를 사용해 생성과 동시에 만료되는 쿠키를 만들고 HttpServletResponse의 setHeader를 사용해 할당해주면 쿠키가 만료 처리되어 제거된다.&lt;/p&gt;
&lt;pre id=&quot;code_1705821990307&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class CustomCookieLogoutHandler implements LogoutHandler {

  @Override
  public void logout(
      HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    ResponseCookie access = makeExpiredTokenCookie(TokenProvider.ACCESS_TOKEN, null);
    ResponseCookie refresh = makeExpiredTokenCookie(TokenProvider.REFRESH_TOKEN, null);
    response.addHeader(HttpHeaders.SET_COOKIE, access.toString());
    response.addHeader(HttpHeaders.SET_COOKIE, refresh.toString());
  }
  
  private ResponseCookie makeExpiredTokenCookie(String key, String token) {
    return ResponseCookie.from(key, token)
        .httpOnly(true)
        .sameSite(&quot;None&quot;)
        .secure(true)
        .maxAge(0)
        .path(&quot;/&quot;)
        .build();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. SecurityConfig에서 LogoutFilter 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현한 LogoutHandler와 LogoutSuccessHandler를 주입받아 LogoutFilter에 설정해주면 핸들러가 적용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1705822066393&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  private final LogoutHandler logoutHandler;
  private final LogoutSuccessHandler logoutSuccessHandler;
  
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  
  	return http.
    	...
        .logout(
            logout -&amp;gt;
                logout
                    .logoutUrl(&quot;/api/v1/logout&quot;)
                    .logoutSuccessHandler(logoutSuccessHandler)
                    .addLogoutHandler(logoutHandler))
        ...
        .build();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글맺음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 기능을 사용한 인증/인가에서 로그아웃을 할 수 있는 간단한 예제를 작성해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인을 어떻게 구현했느냐에 따라서 로그아웃의 커스터마이징도 완전히 달라질 수 있다는 것을 명심해야 한다. 본인이 만든 로그인과 LogoutFilter의 정확한 스펙을 확인해 로그아웃 기능을 만들 수 있다면 좋을 것이다.&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;&amp;nbsp;&lt;/p&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.spring.io/spring-security/reference/reactive/authentication/logout.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Security 공식문서: Logout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Security 공식문서: Handling Logouts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-security-custom-logout-handler&quot;&gt;https://www.baeldung.com/spring-security-custom-logout-handler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>logout</category>
      <category>LogoutFilter</category>
      <category>Security</category>
      <category>Spring</category>
      <category>유의사항</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/220</guid>
      <comments>https://myvelop.tistory.com/220#entry220comment</comments>
      <pubDate>Sun, 21 Jan 2024 16:44:29 +0900</pubDate>
    </item>
    <item>
      <title>신입 개발자! 회사와 함께 성장하기</title>
      <link>https://myvelop.tistory.com/216</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;작년 3월 신입 개발자로 입사한 나는 부푼 꿈을 안고 개발자로서의 커리어를 시작했다. 작은 스타트업이었다. 코드리뷰는 당연히 없었고, 업무를 위해 참고할 수 있는 문서가 단 한 장도 없었다. 나에게 &quot;개발의 왕도&quot;를 제시해줄 수 있는 시니어 개발자도 없었다. 거기다 회사일이 많았기 때문에 CTO님이나 사수가 신입 개발자들을 케어해줄 수 있는 시간이 부족했다. 결국 내가 성장할 수 있는 방법을 스스로 찾아야 하는 상황이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에 입사하기 전 &lt;a title=&quot;체대 출신 개발자의 회고&quot; href=&quot;https://ryan-han.com/post/memoirs/memoirs2018/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;체대 출신 개발자의 회고&lt;/a&gt;라는 글을 읽었다. 정수님의 발자취를 보며 나도 어떤 회사를 가더라도 내 성장 환경을 만들기 위해 행동하면 무엇이든 될 거라는 자신감을 얻었다. 스타트업 회사에 입사하는 것을 지망했기에 회사에 입사하면 어떤 것을 실천해볼지 고민해봤다. 실제로 입사해서 내가 생각했던 것들을 하나씩 실천했고 조금씩 변화해나가는 회사를 지난 9개월 동안 지켜봤다. 그런 과정을 통해 성장해가는 나의 모습을 보면서 느낀 점이 많았다. 이제 그 내용을 정리해보려 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개발문화 변화시키기&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;1-1. 문서화 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 입사하자마자 든 생각이었다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(지금은 많이 개선됐지만)&lt;/span&gt;&amp;nbsp;사수가 없으면 회사가 아예 멈춰버릴 것 같다는 생각이 들었다. 비즈니스, 기술적인 정보가 모두 사수에게 집중되어 있었다. 사수가 너무 바쁜 탓에 그 내용을 정리할 시간은 없었기에 필요할 때마다 다른 개발자들에게 알려주는 식으로 일이 진행됐다. 문서로 정리되지 않았기 때문에 개발자들은 사수에게 지속적으로 질문을 해야 했고, 사수는 그만큼 시간을 허비할 수 밖에 없었다.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 비효율적이라고 생각했고 문서화가 필요하다고 생각했다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;문서가 코드만큼 많다는 얘기, 문서화를 통해 얻는 이점 등 문서화의&lt;/span&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;결국, CTO님에게 문서화를 시작하자고 적극적으로 의견을 개진했다. CTO님도 문서화의 필요성을 느끼고 있었기 때문에 Jira Confluence를 사용해 문서화를 시작하자고 선뜻 답변해주셨다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;문서화는 6월에 처음으로 시작했다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;마침 할 일을 다 끝내놓은 상태였기 때문에 본격적으로 문서화 작업을 시작할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1067&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRGU52/btsCN3Nkvf6/UaQxW4zkfRpzShqgH48G2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRGU52/btsCN3Nkvf6/UaQxW4zkfRpzShqgH48G2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRGU52/btsCN3Nkvf6/UaQxW4zkfRpzShqgH48G2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRGU52%2FbtsCN3Nkvf6%2FUaQxW4zkfRpzShqgH48G2k%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; alt=&quot;문서화&quot; loading=&quot;lazy&quot; width=&quot;657&quot; height=&quot;305&quot; data-origin-width=&quot;1067&quot; data-origin-height=&quot;305&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;물론 처음부터 문서화가 잘되진 않았다. 사수가 종종 같이 작성해주긴 했으나 처음엔 거의 나 혼자 작성했다. 현재 &lt;b&gt;187건 중 114건(약 61%)&lt;/b&gt;의 문서가 내 손에서 작성된 만큼 다들 문서화에 큰 관심을 가지고 있지 않았던 것이 사실이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서화를 통해 몇몇 작업들에서 효과를 보고 있기 때문인지 몰라도 변화가 생기기 시작했다. CTO님이 문서화를 인사 평가 때 넣을 거라고 말씀하셔 문서화에 힘이 실렸다. 신입 개발자 분들은 누구보다 열정적으로 문서화에 참여하고 있고, 몇몇 동료 분들이 문서를 작성하고 있는 것이 눈에 보이기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;문서화를 통해 생긴 변화&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) 불필요한 질문이 줄어들었다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 인수인계를 하기 위해 담당자에게 직접 물어봐야만 문맥 파악이 가능했지만 문서로 대체할 수 있게 됐다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) 신입 교육에 문서를 활용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 사수가 신입의 개발 환경에 대한 교육을 진행하면 평균적으로 2시간 정도의 시간이 소요됐다. 개발환경 세팅부터 시작해 Gitlab, Jira 등의 툴까지 전부 일일이 설명해줘야 했기 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 신입 교육에 필요한 거의 모든 것을 문서로 정리해뒀고, 사수가 이것을 신입 교육에 활용하기 시작했다. 그 이후로 3명의 신입을 교육했는데 30분~1시간 정도의 시간만 들여도 될 정도로 교육을 문서로 대체할 수 있게 되었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(3) 인사이트 공유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 기술공유가 잘 이뤄지지 않고 있었는데 문서화를 통해 해당 문제를 어느정도 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 쿠버네티스와 RabbitMQ를 다루면서 프로덕트의 구조에 대한 이해도가 생겨 아키텍처를 도식화해 문서화했다. 다른 개발자들이 해당 문서를 확인하고 프로덕트의 구조를 이해하는데 도움되었다는 얘기를 들었다. 또, 동료 개발자 분이 Java8의 Functional Interface 기능을 사용해 하드 코딩되어 있던 코드를 리팩토링하고 문서화해 남겨두었는데 덕분에  다들 해당 기능을 확인하고 배울 수 있는 기회가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;코드리뷰 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 입사했을 때는 코드리뷰는 커녕 Pull Request 조차 사용하지 않고 있었다. 입사 1주차부터 회사에 코드리뷰와 PR 사용을 건의했었다. 신입인 나로서는 내가 코드를 잘 작성하고 있는지 확인받고 싶었다. 하지만 CTO님과 사수는 코드리뷰에 회의적이었다. 할 일도 많은데 언제 PR 등록하고 코드리뷰까지 하겠냐는 답변이 돌아왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드리뷰에 대한 설득은 6개월이라는 시간이 걸렸다. PR과 코드리뷰에 대한 문서를 만들어 정리해두고 리뷰에 대한 얘기를 기회가 될 때마다 나눴다. 결국, 신입 및 경력 개발자들이 새로 들어오면서 개발팀의 인원이 많아졌고 CTO님도 이제는 코드에 대한 체계적인 관리가 필요하다는 것을 인정하셔 PR을 사용한 코드리뷰를 도입하게 되었다.&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;s&gt;나도 신입이지만&lt;/s&gt; 신입 개발자들의 온보딩에 쏠쏠하게 사용되었다. 요구사항에 맞게 구현되었는지, 코드 컨벤션, 코드에 일관성이 있는지, 클래스 및 메소드 네이밍, 중복되는 코드를 확인해줬다. 그정도만 확인해줘도 리뷰 이전의 코드와 이후를 코드를 비교해봤을 때 확연히 좋아보였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lVtMV/btsC4HieMgd/uMwzYljJBKukiRR8inhAk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lVtMV/btsC4HieMgd/uMwzYljJBKukiRR8inhAk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lVtMV/btsC4HieMgd/uMwzYljJBKukiRR8inhAk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlVtMV%2FbtsC4HieMgd%2FuMwzYljJBKukiRR8inhAk0%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; alt=&quot;코드리뷰 내용&quot; loading=&quot;lazy&quot; width=&quot;677&quot; height=&quot;720&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;976&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HK5sT/btsC4GXZHbT/k3BIZkkUXYKQR37xfVSxlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HK5sT/btsC4GXZHbT/k3BIZkkUXYKQR37xfVSxlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HK5sT/btsC4GXZHbT/k3BIZkkUXYKQR37xfVSxlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHK5sT%2FbtsC4GXZHbT%2Fk3BIZkkUXYKQR37xfVSxlK%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; alt=&quot;코드리뷰 내용&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;382&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;976&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7XlyA/btsDbeMpIjK/WMI6s2NlibM6DRMrmkPHG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7XlyA/btsDbeMpIjK/WMI6s2NlibM6DRMrmkPHG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7XlyA/btsDbeMpIjK/WMI6s2NlibM6DRMrmkPHG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7XlyA%2FbtsDbeMpIjK%2FWMI6s2NlibM6DRMrmkPHG1%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; alt=&quot;코드리뷰 내용&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;286&quot; data-origin-width=&quot;1736&quot; data-origin-height=&quot;724&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 회사와 함께 성장하기&lt;/h2&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2-1. 기술 면접에 기여&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나와 회사가 성장하려면 좋은 동료들이 주변에 있어야한다. 좋은 동료들은 채용을 통해 들어온다. 다시 말해, 회사의 성장에 채용만큼 큰 영향을 끼칠 수 있는 프로세스는 없다고 생각한다. 개발자들은 좋은 면접 경험을 제공하는 회사를 선호하기 때문에 면접을 잘 준비하는 것이 중요하다고 생각한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;신입 개발자를 뽑기 위한 면접 일정이 잡혀 CTO님이 사수와 함께 면접에 들어가고 싶은 팀원을 지원받았다. 내가 면접을 준비한다면 지금 회사에서 면접을 보면서 아쉬웠던 점들을 개선하고 채용에 기여할 수 있다고 생각했기 때문에 지원했다. 그렇게 총 20번의 기술 면접에 참여했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;시간이 날 때마다 이력서와 프로젝트를 살펴봤다. 그리고 내가 판단할 수 있는 부분(기술적인 내용, 기여도, 기록을 얼마나 잘 남겼는지, 컨벤션을 정립했는지 등)들을 추려 CTO님한테 보고를 드렸다. 시간이 부족할 때면 퇴근을 하고 집에서 면접 질문을 준비한 적도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2vOhy/btsDbcVsZHe/LuplyXV1Lfe3WINue9vtmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2vOhy/btsDbcVsZHe/LuplyXV1Lfe3WINue9vtmk/img.png&quot; data-alt=&quot;지원자의 프로젝트를 보고 준비한 면접 질문&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2vOhy/btsDbcVsZHe/LuplyXV1Lfe3WINue9vtmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2vOhy%2FbtsDbcVsZHe%2FLuplyXV1Lfe3WINue9vtmk%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;704&quot; height=&quot;508&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;지원자의 프로젝트를 보고 준비한 면접 질문&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;최근에 입사한 신입 개발자 분께 다른 회사를 붙었음에도 우리 회사를 입사한 이유에 대해서 물어봤다. 처음 나온 답변이 생각지도 못하게 기술 면접에 관한 얘기였다. 면접자의 프로젝트를 꼼꼼히 읽어와 질문해준 것이 느껴졌고 질문 내용도 면접을 봤던 다른 회사에 비해 좋다고 생각해 입사를 결정했다고 하셨다. 내가 기술면접을 위해 했던 노력이 확실히 도움이 되었다는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;면접을 준비하는 과정이 회사 뿐만 아니라 나에게도 도움이 되었다. 면접 질문 목록을 자주 봤었는데 내가 몰랐던 내용이 굉장히 많았다. 모르는 내용을 면접자에게 질문할 수는 없었기 때문에 그 내용들을 공부했고 나에게 부족한 것들을 채워나갈 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 기술 공유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 공유는 내가 직접 만들자고 제안하진 않았지만 우연한 계기로 시작됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 개선을 진행하고 난 후, 다른 개발자들도 해당 방법을 적용하면 좋겠다는 마음에서 CTO님과 사수한테 공유한 적이 있었다. 이 얘기를 들은 CTO님이 기술 공유 시간을 만들어 해당 내용을 발표해보라고 제안하셨고 나는 당연히 하겠다고 답변드렸다. 20분 정도의 짧은 발표를 준비했고 기술적인 내용과 그 성과에 대해 발표했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/durEao/btsC880lUEL/Plhwo6bXtZxjyvuEoNiq3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/durEao/btsC880lUEL/Plhwo6bXtZxjyvuEoNiq3k/img.png&quot; data-alt=&quot;기술공유 때 사용했던 발표 자료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/durEao/btsC880lUEL/Plhwo6bXtZxjyvuEoNiq3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdurEao%2FbtsC880lUEL%2FPlhwo6bXtZxjyvuEoNiq3k%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; alt=&quot;기술공유 때 사용했던 발표 자료&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;391&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;816&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. 시스템 구축하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 나에게 주어진 버그 이슈가 주어졌을 때 간단하게 고칠 수 있는 문제지만, 이대로 두면 결국 다시 문제가 생길 것을 인지하는 때가 있다. 나는 그럴 때마다 근본적으로 문제를 해결할 수 있는 방법을 고민했다. &lt;i&gt;&quot;어떤 실수가 한 번 발생하면 그 사람의 잘못이지만, 그 실수가 반복되면 시스템의 잘못이다.&quot;&lt;/i&gt; 라는 말이 있다. &lt;b&gt;실수가 발생했을 때 그 실수가 다시 발생하지 않도록 시스템을 만들어 놓는 경험이 나와 회사의 성장에 도움이 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 입사 초반 내가 만든 버그로 인해 큰 손해를 본 적이 있었다. 예약자에게 문자를 보내는 배치 잡이 있다. 연관된 코드에 문제가 발생해 배치에서 문자가 발송되지 않는 버그가 발생했다. 그런데 문자가 발송되지 않아도 확인할 방법이 없었다. 기어코 5일 연속된 연휴에 문제가 발생했다. 2천 건 가까이 되는 문자가 발송되지 않았는데 아무도 알아채지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 버그를 만들어낸 내 잘못도 있지만 더 STAG 환경에 7일을 테스트하고 프로덕트로 배포된 지 5일이 지났는데도 이 문제를 아무도 알아채지 못했다는 것이 더 큰 문제라고 생각했다. 문제가 있는지 바로 확인할 수 있는 경로만 있다면 STAG 환경에서도 충분히 잡을 수 있는 버그였다. 나는 이 문제를 근본적으로 해결하기 위해 슬랙을 사용해 모니터링 알림을 구축했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wBfIy/btsC5XegA4g/VrDABMICGjqfiPriZC7ny1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wBfIy/btsC5XegA4g/VrDABMICGjqfiPriZC7ny1/img.png&quot; data-alt=&quot;문자가 발송되지 않으면 슬랙 알림이 발송된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wBfIy/btsC5XegA4g/VrDABMICGjqfiPriZC7ny1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwBfIy%2FbtsC5XegA4g%2FVrDABMICGjqfiPriZC7ny1%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; alt=&quot;슬랙 알림&quot; loading=&quot;lazy&quot; width=&quot;573&quot; height=&quot;96&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;210&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;해당 알림은 최근까지도 유용하게 쓰이고 있다. 슬랙 시스템이 생긴 이후로 다른 개발자들도 이를 활용해 Batch 서버의 버그를 확인하는 등 런타임에 발생하는 여러 문제에 대해 즉각적으로 피드백받을 수 있게 되었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 회사 밖에서 영감을 얻기&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 개인 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 알고 지내는 지인들과 프로젝트 진행했다. 새로운 기술을 공부해보고자 하는 마음도 있었고, 다른 개발자 친구들의 코드 스타일을 분석해 내 코드를 개선해보고 싶었다. 회사에서는 사용해보지 못한 기술을 연마했고 프로젝트 커뮤니티를 통해 모각코, 스터디 등을 진행해 부족한 부분을 함께 채워나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 진행했던 프로젝트에서는 특히 REST Docs + Swagger UI와 Spring Security를 집중적으로 공부했었다. 개인 프로젝트에서 구현할 때는 굉장히 오랜 시간을 들여 공부하고 구현했다. 그런데 우연찮게도 신규 프로젝트를 맡아 진행하게 되었다. 기한이 촉박했고 해당 기술을 사용해야만 하는 상황이었기 때문에 그 기술을 알고 있는 나에게 기회가 온 것이었다. 개인 프로젝트에서 공부해둔 덕분에 굉장히 빠르게 Swagger와 인증/인가 기능을 구현할 수 있었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 기술 블로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 회사나 개발자의 기술 블로그가 프로덕트의 비즈니스 로직의 개선과 나의 성장에 도움이 됐다. 회사에 시니어 개발자가 없는 상황이기 때문에 웬만한 문제해결을 스스로 해야하는 상황이다. 때때로 내가 감당하기 어려울 정도의 복잡한 로직을 구현하라는 요구 사항이 주어지기도 하는데 그럴 때마다 도움이 된 것은 다른 회사들의 기술 블로그에 작성된 Best Practice였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 개발하고 있는 쇼핑몰 프로젝트의 포인트 구조에 불합리한 점이 많았다. 포인트를 만료시키기 위해 모든 로우를 계산하는 방법을 사용해야 했기에 때문에 속도가 확연히 느렸다. 그러다 우아한형제들 기술블로그의 &lt;a title=&quot;신규 포인트 시스템 전환기&quot; href=&quot;https://techblog.woowahan.com/2587/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;신규 포인트 시스템 전환기&lt;/a&gt;라는 글에서 힌트를 얻어 포인트의 테이블 스키마를 변경하고 마이그레이션하여 로직을 새로 만들었다. 해당 로직을 적용하고 나니 만료일이 된 포인트만 체크해 만료처리할 수 있었고 성능을 20배 개선했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 강의와 책&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pkFyC/btsC7qNJ7sW/kg6JkYgHpAaRGKqgu4bpd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pkFyC/btsC7qNJ7sW/kg6JkYgHpAaRGKqgu4bpd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pkFyC/btsC7qNJ7sW/kg6JkYgHpAaRGKqgu4bpd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpkFyC%2FbtsC7qNJ7sW%2Fkg6JkYgHpAaRGKqgu4bpd0%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;673&quot; height=&quot;304&quot; data-origin-width=&quot;934&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 업무와 관련된 강의나 책이라면 필요할 때마다 지원해주겠다고 했기 때문에 마음 편히(?) 강의를 구매할 수 있었고, 지금까지 입사 이래로 총 10편의 강의를 수강했다. 회사에서 사용하는 스택인 자바나 스프링에 익숙하지 않았기 때문에 영한님의 강의를 위주로 들었고 그 외 필요한 강의와 책을 곁들였다. 강의를 통해 배운 기술을 신규 프로젝트 적용해 도움이 됐다. 회사에서 사용하는 기술과는 다른 기술을 공부하더라도 강의에서 배운 내용을 응용해 회사 프로젝트에 적용해볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영한님의 JPA 로드맵을 들으면서 N+1 문제를 해결하기 위한 방법들을 공부할 때였다. 문제를 해결하기 위해 @BatchSize를 사용할 수 있다는 내용이었다. @BatchSize의 동작 원리에 대한 설명을 들었는데, 거기서 영감을 얻어 다른 DB 접근 기술인 MyBatis 쿼리를 개선했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글을 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기 할 일을 묵묵히 하고 따로 시간내서&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;&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>끄적끄적</category>
      <category>it</category>
      <category>개발자</category>
      <category>개발자 성장</category>
      <category>면접</category>
      <category>문서화</category>
      <category>코드리뷰</category>
      <category>프로젝트</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/216</guid>
      <comments>https://myvelop.tistory.com/216#entry216comment</comments>
      <pubDate>Sun, 7 Jan 2024 19:38:52 +0900</pubDate>
    </item>
    <item>
      <title>스프링의 외부 API 호출, 그리고 RestClient</title>
      <link>https://myvelop.tistory.com/217</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;애플리케이션 외부 API 호출&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현업에서 외부 API를 호출해야하는 일이 많다. 다른 회사의 서비스(휴대전화 인증, 결제시스템)를 이용할 때 필수적이다. 물론 클라이언트 단에서 외부 API를 호출한다면 스프링 서버에서 API를 호출할 일이 없겠지만, CORS 오류를 회피하기 위해 프록시 서버가 필요한 경우 스프링 서버가 프록시 서버의 역할을 해줘야 하기 때문에 스프링에서 외부 API를 대신 호출해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 서버를 서비스 단위로 나눠 배포하는 경우, 내부 서버 컨테이너끼리 데이터를 교환해야할 경우가 생기는데 이럴 때 RabbitMQ와 같은 메시지 브로커를 사용해 데이터를 전달할 수 있지만, 컨테이너끼리 API를 호출을 하는 방식을 사용할 수 있다.&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;자바나 스프링에서는 HTTP 요청을 보내기 위한 다양한 도구를 지원한다. 자바에서 기본적으로 HttpURLConnection/URLConnection라는 Http Connection을 맺고 끊는 방식의 API를 제공하고 있지만 여러 단점(추상화 레벨이 낮다는 점, 동기적이라는 점)을 가지고 있기 때문에 개발자들은 더 나은 방식을 찾고자 했다. 자연스럽게 자바와 스프링이 버전이 올라감과 동시에 외부 API를 호출하는 방법도 발전했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외부 API를 호출하는 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. HttpURLConnection/URLConnection&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 통신을 가능하게 해주는 클래스로 자바에서 제공하는 기본적인 API이다.&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(순수 자바로 HTTP 통신)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;URL을 이용해 외부 API에 연결하고 데이터를 전송한다.&lt;/li&gt;
&lt;li&gt;데이터의 타입/길이에 거의 제한이 없다.&lt;/li&gt;
&lt;li&gt;오래된 자바 버전에서 사용하는 클래스다. 즉, 동기적 통신을 기본으로 사용한다. 동기적이므로 요청을 보내고 응답을 받을 때까지 스레드가 대기 상태라는 점에서 성능에 안 좋은 영향을 미칠 수 있다.&lt;/li&gt;
&lt;li&gt;URLConnection은 상대적으로 저수준 API에 해당하기 때문에 기본적인 요청/응답 기능이 있지만 추가적인 기능들을 직접 구현해야 한다는 불편함도 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HttpURLConnection 사용 예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바에서 기본으로 제공하는 API이기 때문에 따로 의존성을 추가할 필요가 없다.&lt;/li&gt;
&lt;li&gt;POST를 호출하는 코드 예시이다. http &amp;amp; https부터 Header 및 Method 설정과 외부 API와의 통신과 결과값 받기 그리고 그 결과값에 따른 반환값 및 예외 처리까지 전부 사용자가 처리해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1702799925006&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class HttpURLConnectionEx {

  private final ObjectMapper objectMapper;

  public String post(
      String requestUrl,
      Map&amp;lt;String, String&amp;gt; headers,
      Object body) {

    try {
      URL obj = new URL(requestUrl);
      HttpURLConnection con = requestUrl.startsWith(&quot;https&quot;) ? (HttpsURLConnection) obj.openConnection() : (HttpURLConnection) obj.openConnection();
      con.setRequestProperty(&quot;charset&quot;, &quot;utf-8&quot;);
      for(Map.Entry&amp;lt;String, String&amp;gt; header : headers.entrySet()) {
        con.setRequestProperty(header.getKey(), header.getValue());
      }

      con.setRequestMethod(HttpMethod.POST.name());
      con.setDoOutput(true);
      String bodyStr = objectMapper.writeValueAsString(body);
      con.getOutputStream().write(bodyStr.getBytes(StandardCharsets.UTF_8));
      con.connect();
      int resCode = con.getResponseCode();

      if (resCode != 200) {
        con.disconnect();
        throw new MyHttpFailRuntimeException();
      }

      BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), &quot;euc-kr&quot;));
      StringBuffer response = new StringBuffer();
      String inputLine;
      while ((inputLine = br.readLine()) != null) {
        response.append(inputLine);
      }
      return response.toString();
    } catch(IOException e) {
      throw new MyException();
    } catch(MyHttpFailRuntimeException e) {
      throw e;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. Apache HttpClient&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 프로토콜을 쉽게 사용할 수 있게 도와주는 Apache HTTP 컴포넌트이다. 객체 생성이 쉽다는 장점이 있다.&lt;/li&gt;
&lt;li&gt;URLConnection 방식보다 코드가 간결해 졌지만, 반복적이고 코드가 길고 응답의 컨텐츠 타입에 따라 별도의 로직이 필요하다는 단점이 존재한다.&lt;/li&gt;
&lt;li&gt;HttpClient5부터는 CloseableHttpAsyncClient를 사용해 비동기 통신이 가능해졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 의존성을 추가해주자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703404831525&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.apache.httpcomponents.client5:httpclient5:5.3'&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;HttpURLConnection/URLConnection보다 추상화 레벨이 높기 때문에 보다 다루기 쉬운 편이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703403757405&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class ApacheHttpClientEx {

  private final ObjectMapper objectMapper;

  public MyResponse post(
      String requestUri,
      MyRequest requestBody) {

    try {
      HttpPost httpPost = new HttpPost(requestUri);
      httpPost.setHeader(HttpHeaders.CONTENT_TYPE, &quot;application/json;charset=UTF-8&quot;);

      HttpEntity entity = new StringEntity(objectMapper.writeValueAsString(requestBody), StandardCharsets.UTF_8);
      httpPost.setEntity(entity);

      CloseableHttpClient httpClient = HttpClientBuilder.create().build();
      CloseableHttpResponse response = httpClient.execute(httpPost);

      if (response.getCode() != 200) {
        throw new MyException();
      }

      BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler();
      String body = handler.handleResponse(response);
      MyResponse myResponse = objectMapper.readValue(body, MyResponse.class);
      return myResponse;

    } catch (IOException e) {
      throw new MyException(e);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;3. Java11 버전부터 출시된 Java.net.http의 HttpClient&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 소개한 apache의 HttpClient와는 다른 객체이다.&lt;/li&gt;
&lt;li&gt;java.net.http 패키지의 HttpClient는 자바 11 버전에 포함되어 있기 때문에 따로 의존성이 필요하지 않다.&lt;/li&gt;
&lt;li&gt;비동기 통신을 사용할 수 있다는 장점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;HttpClient 사용 예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 11에서 포함하고 있는 API이기 때문에 따로 의존성을 추가할 필요가 없다.&lt;/li&gt;
&lt;li&gt;아래 예시는 send 메소드를 사용해 동기적 통신을 하고 있지만 sendAsync 메소드를 사용해 비동기 통신을 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703403784588&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class HttpClientEx {

  private final ObjectMapper objectMapper;

  public MyResponse post(
      String requestUri,
      MyRequest requestBody) {

    try {
      HttpClient client = HttpClient.newBuilder().build();
      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(requestUri))
          .timeout(Duration.ofSeconds(30))
          .setHeader(&quot;Content-Type&quot;, &quot;application/json;charset=UTF-8&quot;)
          .POST(BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody)))
          .build();
      HttpResponse&amp;lt;String&amp;gt; responseStr = client.send(request, BodyHandlers.ofString());
      return objectMapper.convertValue(responseStr.body(), MyResponse.class);
    } catch (IOException e) {
      throw new MyException(e);
    } catch (InterruptedException e) {
      throw new MyException(e);
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. RestTemplate&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링에서 제공하는 HTTP 통신 템플릿으로 스프링 3.0에서 추가됐다.&lt;/li&gt;
&lt;li&gt;Apache의 HttpClient를 추상화해서 제공한다. &lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;ClientHttpRequestFactory에 HttpClient를 넘겨서 활용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;다른 API를 호출할 때 RestTemplate를 사용해 호출한다.&lt;/li&gt;
&lt;li&gt;JSON, XML, String과 같은 응답을 받을 수 있다.&lt;/li&gt;
&lt;li&gt;Blocking 기반의 동기 방식 사용한다.&lt;/li&gt;
&lt;li&gt;HTTP 서버와의 통신을 단순화하고 RESTful 원칙 고수&lt;/li&gt;
&lt;li&gt;header, content-type등을 설정하며 외부 API를 호출&lt;/li&gt;
&lt;li&gt;사용하기 편하고 직관적이라는 장점이 있다.&lt;/li&gt;
&lt;li&gt;동기적인 HTTP 요청을 하기 때문에 성능에 영향을 미칠 수 있다는 점, Connection Pool을 사용하지 않기 때문에 연결할 때마다 로컬 포트를 열고 TCP Connection을 시도한다는 특징으로 인해 해당 로직을 따로 설정해줘야한다는 불편함 등의 단점이 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RestTemplate의 동작 원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RestTemplate의 동작 원리는 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4GeYa/btsCayebhFL/WdfFX2LxkvyQYurSL4zRcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4GeYa/btsCayebhFL/WdfFX2LxkvyQYurSL4zRcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4GeYa/btsCayebhFL/WdfFX2LxkvyQYurSL4zRcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4GeYa%2FbtsCayebhFL%2FWdfFX2LxkvyQYurSL4zRcK%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;728&quot; height=&quot;409&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션이 에서 API를 호출하기 위해 RestTemplate을 호출한다.&lt;/li&gt;
&lt;li&gt;HttpMessageConverter를 사용해 Object를 RequestBody에 담을 수 있는 형태로 변환한다.&lt;/li&gt;
&lt;li&gt;ClientHttpRequestFactory를 통해 ClientHttpRequest를 받아와 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;ClientHttpRequest가 요청메시지를 만들어 HTTP 프로토콜을 통해 REST API를 호출한다.&lt;/li&gt;
&lt;li&gt;ResponseErrorHandler를 사용해 오류가 확인되면 RestTemplate에서 오류 로직을 실행한다.&lt;/li&gt;
&lt;li&gt;ResponseErrorHandler에 오류가 확인되면 ClientHttpResponse에서 응답데이터를 가져와 처리한다.&lt;/li&gt;
&lt;li&gt;HttpMessageConverter가 문자열로 되어 있는 응답메시지를 Object로 변환해준다.&lt;/li&gt;
&lt;li&gt;애플리케이션은 자바 Object를 반환받는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RestTemplate 사용 예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RestTemplate을 사용하려면 스프링 의존성이 필요하다. (RestTemplate에서 Apache의 HttpClient의 기능을 활용하고 싶다면 Apache HttpClient 의존성을 따로 추가해주자.)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703404731702&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-web'&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;RestTemplate를 사용하면&amp;nbsp; ObjectMapper를 주입받아 요청과 응답에 사용될 Java Object를 직접 변환해줄 필요가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1702803197342&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RestTemplateEx {

  public MyResponse post(
      String requestUrl,
      MyRequest requestBody) {

    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    MediaType mediaType = new MediaType(&quot;application&quot;, &quot;json&quot;, Charset.forName(&quot;UTF-8&quot;));
    headers.setContentType(mediaType);
    HttpEntity&amp;lt;MyRequest&amp;gt; requestHttpEntity = new HttpEntity&amp;lt;&amp;gt;(requestBody, headers);
    ResponseEntity&amp;lt;MyResponse&amp;gt; response = restTemplate.postForEntity(requestUrl, requestHttpEntity, MyResponse.class);
    return response.getBody();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. WebClient&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 5.0부터 도입된 라이브러리이다.&lt;/li&gt;
&lt;li&gt;비동기/논블로킹 방식으로 외부 API 호출 가능하다. 무엇보다 WebClient의 장점은 HttpInterface라는 강력한 도구와 함께 사용할 수 있다는 점이다.&lt;/li&gt;
&lt;li&gt;리액티브 프로그래밍이 가능하며 데이터 스트림을 효과적으로 처리 가능하기 때문에 높은 처리량과 확장성을 확보할 수 있다.&lt;/li&gt;
&lt;li&gt;대신 WebFlux 의존성을 설치해야하고, WebClient를 잘 사용하려면 WebFlux에 대한 이해도가 필요(진입 장벽이 높다)하다는 단점이 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 WebFlux 의존성을 추가해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703405429107&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-webflux'&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;메소드 체이닝만으로도 WebClient를 생성하거나 요청을 보낼 수 있다. 또한 WebClient의 Exception Handling도 훨씬 간편하게 할 수 있었다. 이와 같은 장점 때문에 이전의 다른 API 호출 방식보다 사용하기 수월했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class WebClientEx {

  public MyResponse post(
      String requestUri,
      MyRequest requestBody) {

    WebClient webClient = WebClient.builder()
        .baseUrl(requestUri)
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .defaultStatusHandler(HttpStatusCode::is4xxClientError, response -&amp;gt; {
          throw new MyException();
        }).build();

    ResponseEntity&amp;lt;MyResponse&amp;gt; response = webClient.post()
        .uri(&quot;/request&quot;)
        .bodyValue(requestBody)
        .retrieve()
        .toEntity(MyResponse.class)
        .block();

    return response.getBody();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. HttpInterface&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 6.0 버전 이후부터 사용할 수 있는 HTTP 통신 기능이다.&lt;/li&gt;
&lt;li&gt;각 API 자원에 대한 인터페이스를 작성하고 ProxyFactory를 사용하면 인터페이스와 WebClient 혹은 RestClient를 통해 동적 프록시를 생성한다. 이를 Bean으로 등록해주면 서비스 단에서 인터페이스를 주입받아 메소드 하나로 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;API 호출을&lt;/span&gt; 쉽게 할 수 있다.&lt;/li&gt;
&lt;li&gt;@RequestHeader, @RequestBody 등의 어노테이션을 사용해 요청에 필요한 정보를 매개변수로 전달할 수 있다.&lt;/li&gt;
&lt;li&gt;WebClient가 있어야만 사용할 수 있어서 WebFlux 의존성이 필요했지만 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;스프링 6.1.2 이후로&lt;/span&gt;&amp;nbsp;RestClient라는 대안이 생겼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코드 예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 WebFlux 의존성을 추가해주자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703406595747&quot; class=&quot;clean&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-webflux'&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;외부 REST API의 스펙에 맞게 인터페이스를 작성해준다.&lt;/li&gt;
&lt;li&gt;주목할 점은 Controller처럼 @PathVariable, @RequestBody 등의 어노테이션을 활용해 필요한 정보를 담아 보낼 수 있다는 점이다. RequestBody의 경우, Java Object를 그대로 내보내도 직렬화를 자동으로 해준다. 정말 편리하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703406679740&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface MyHttpInterface {

  @PostExchange(&quot;/request&quot;)
  MyResponse request(
      @RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType,
      @RequestBody MyRequest request);
}&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;HttpInterface를 동적 프록시를 생성해주는 Config를 작성하자. HttpInterface를 Bean으로 등록해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703406580986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class HttpInterfaceConfig {

  @Bean
  public MyHttpInterface myHttpInterface() {
    WebClient webClient = WebClient.builder()
        .baseUrl(&quot;uri&quot;)
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .defaultStatusHandler(HttpStatusCode::is4xxClientError, response -&amp;gt; {
          throw new MyException();
        }).build();

    HttpServiceProxyFactory factory = HttpServiceProxyFactory
        .builderFor(WebClientAdapter.create(webClient)).build();
    return factory.createClient(MyHttpInterface.class);
  }
}&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;이제 서비스 단에 HttpInterface를 주입받으면 동적 프록시로 생성된 객체를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703406866848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MyService {

  private final MyHttpInterface myHttpInterface;

  public MyResponse request(MyRequest request) {
    MyResponse response = myHttpInterface.request(MediaType.APPLICATION_JSON_VALUE, request);
    return response;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring 6.1.2 버전에 출시된 RestClient&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;RestClient란?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring 6.1.2 버전부터 WebFlux 의존성 없이 사용할 수 있는 RestClient 기능이 출시되었다.&lt;/li&gt;
&lt;li&gt;WebClient와 유사한 HTTP 요청을 위한 객체이다. WebClient와는 다르게 동기식으로 동작되지만 사용방식은 WebClient와 거의 유사하다.&lt;/li&gt;
&lt;li&gt;스프링 6.0 버전에 출시된 HttpInterface는 강력한 도구지만 해당 기능을 사용하기 위해 WebClient를 사용해야했고, WebFlux 의존성을 설치해야만 했지만 RestClient로 대체할 수 있다.&lt;/li&gt;
&lt;/ul&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;아래 코드는 토스페이먼츠의 환불 REST API를 호출하는 예시이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HttpInterface&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TossPayments HttpInterface이다. HttpServiceProxyFactory를 통해 RestClient를 달아주고 IoC 컨테이너에 Bean으로 등록해주면 동적 프록시를 통해 구현체를 자동으로 만들어준다.&lt;/li&gt;
&lt;li&gt;@RequestHeader, @PathVariable, @RequestBody 등의 어노테이션을 통해 요청에 필요한 정보를 담는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703079963968&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface TossPaymentsClient {

  @PostExchange(&quot;/v1/payments/{paymentsKey}/cancel&quot;)
  RefundPaymentResponse refund(
      @RequestHeader(HttpHeaders.AUTHORIZATION) String secretKey,
      @RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType,
      @PathVariable String paymentsKey,
      @RequestBody RefundPaymentRequest request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HttpConfig&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Factory Interface&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703080142003&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface HttpInterfaceFactory {
  &amp;lt;S&amp;gt; S create(Class&amp;lt;S&amp;gt; clientClass, RestClient restClient);
}&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;Factory 구현체이다. Config에서 HttpInterface를 쉽게 선언하기 위해 만든 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703080142004&quot; class=&quot;monkey&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class SimpleHttpInterfaceFactory implements HttpInterfaceFactory {

  public &amp;lt;S&amp;gt; S create(Class&amp;lt;S&amp;gt; clientClass, RestClient restClient) {
    return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(restClient))
        .build()
        .createClient(clientClass);
  }
}&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;HttpConfig 코드이다. RestClient를 만들고 HttpInterface에 부여해 Bean으로 등록하는 코드이다.&lt;/li&gt;
&lt;li&gt;RestClient의 builder를 사용해 RestClient를 구성할 수 있다. RestClient에 TossPayments의 REST API URL을 baseUrl로 부여한다. 또한, defaultStatusHandler를 사용해 에러 핸들링을 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703080367657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
public class HttpInterfaceConfig {

  private final String tossPaymentsUrl;
  private final HttpInterfaceFactory httpInterfaceFactory;

  public HttpInterfaceConfig(@Value(&quot;${external-api.toss.url}&quot;) String tossPaymentsUrl) {
    this.httpInterfaceFactory = new SimpleHttpInterfaceFactory();
    this.tossPaymentsUrl = tossPaymentsUrl;
  }

  @Bean
  public TossPaymentsClient tossPaymentsClient() {
    return httpInterfaceFactory.create(TossPaymentsClient.class, createRestClient(tossPaymentsUrl));
  }

  private RestClient createRestClient(String baseUrl) {
    return RestClient.builder()
        .baseUrl(baseUrl)
        .defaultStatusHandler(
            HttpStatusCode::is4xxClientError,
            (request, response) -&amp;gt; {
              log.error(&quot;Client Error Code={}&quot;, response.getStatusCode());
              log.error(&quot;Client Error Message={}&quot;, new String(response.getBody().readAllBytes()));
            })
        .defaultStatusHandler(
            HttpStatusCode::is5xxServerError,
            (request, response) -&amp;gt; {
              log.error(&quot;Server Error Code={}&quot;, response.getStatusCode());
              log.error(&quot;Server Error Message={}&quot;, new String(response.getBody().readAllBytes()));
            })
        .build();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Service&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동적 프록시로 생성된 HttpInterface를 서비스 코드에서 주입받는다. 이제 비즈니스 로직에서 해당 서비스 메소드를 호출하면 REST API가 호출된다.&lt;/li&gt;
&lt;li&gt;참고로 TossPaymentsUtils의 encodeAuthorizationByBase64 메소드는 시크릿키를 Base64로 변환해주는 로직이다. 자세한 내용은 토스 페이먼츠 연동 공식 문서를 참고하길 바란다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1703080022391&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class TossPaymentsService {

  private final String clientKey;
  private final String secretKey;
  private final TossPaymentsClient tossPaymentsClient;

  public TossPaymentsService(
      @Value(&quot;${external-api.toss.client-key}&quot;) String clientKey,
      @Value(&quot;${external-api.toss.secret-key}&quot;) String secretKey,
      TossPaymentsClient tossPaymentsClient) {
    this.clientKey = clientKey;
    this.secretKey = secretKey + &quot;:&quot;;
    this.tossPaymentsClient = tossPaymentsClient;
  }

  public void refundPayment(OrderPayment orderPayment) {

    tossPaymentsClient.refund(
        TossPaymentsUtils.encodeAuthorizationByBase64(secretKey),
        TossPaymentsUtils.applicationJsonAndUtf8Set(),
        orderPayment.getPaymentsKey(),
        RefundPaymentRequest.of(orderPayment.getPayAmount().abs()));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient 또는 RestClient와 HttpInterface를 결합해 사용하면 RestTemplate, HttpClient, HttpURLConnection, URLConnection 등의 예전 기술과 비교해봤을 때 추상화 수준이 높기 때문에 매우 편리하다고 느꼈다. 코드 가독성도 훨씬 좋고 생산성도 높아진다. 외부 API를 호출하는 로직이 있다면 HttpInterface를 사용하는 것을 강력 추천하고 싶다.&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;RestClient와 WebClient의 기능이 비슷하지만 서버를 리액티브 프로그래밍으로 만들지 않았다면 아래의 이유 때문에 RestClient를 사용하는 것이 좋다고 생각한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RestClient를 사용하면 WebFlux 의존성을 제거할 수 있다는 점에서 좋다. (막상 젠킨스에서 빌드를 여러차례 돌려봤으나 속도의 차이를 못 느낄 정도였다. WebFlux 의존성이 그렇게 무거운 의존성은 아니기 때문이다.)&lt;/li&gt;
&lt;li&gt;WebClient를 사용할 때는 Mono, Flux 등의 Reactor 객체에 대한 이해도가 있어야 하지만 RestClient는 동기식 Http 호출 도구이기 때문에 WebClient에 비해 사용하기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 프로젝트를 구성하거나 오래되지 않은 프로젝트를 진행하고 있다면, RestClient를 사용하기 위해 Spring Boot 버전을 올리는 것도 괜찮다고 생각한다. 하지만 기존 프로젝트에서 RestClient를 사용하기 위해 버전을 올려야 하고, 버전을 올렸을 때 프로젝트에 영향이 가는 상황이라면 WebClient + HttpInterface나 RestTemplate을 사용하거나 원래 사용하던 기술을 그대로 가져가는 것이 오히려 더 좋은 방법이라고 생각한다.&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;&amp;nbsp;&lt;/p&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 title=&quot;restclient uri encoding 문제 블로그 링크&quot; href=&quot;https://myvelop.tistory.com/228&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RestClient URI Encoding 문제 (feat. 퍼센트 인코딩)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HttpURLConnection 통신: &lt;a href=&quot;https://chwan.tistory.com/entry/HttpURLConnection-POST-%ED%86%B5%EC%8B%A0&quot;&gt;https://chwan.tistory.com/entry/HttpURLConnection-POST-%ED%86%B5%EC%8B%A0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Apache HttpClient: &lt;a href=&quot;https://linked2ev.github.io/java/2020/03/09/JAVA-3.-Apache-httpclient-Http-API-Request/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://linked2ev.github.io/java/2020/03/09/JAVA-3.-Apache-httpclient-Http-API-Request/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;nGrinder&lt;span&gt;에&lt;/span&gt; &lt;span&gt;적용한&lt;/span&gt; HttpCore5&lt;span&gt;와&lt;/span&gt; HttpClient5 &lt;span&gt;살펴보기: &lt;a href=&quot;https://d2.naver.com/helloworld/0881672&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://d2.naver.com/helloworld/0881672&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Java11에 정식으로 추가된 HttpClient: &lt;a href=&quot;https://brush-up.github.io/java/java11-http-client/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://brush-up.github.io/java/java11-http-client/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;HttpClient를 사용해 웹사이트 요청보내기: &lt;a href=&quot;https://colinch4.github.io/2023-11-16/13-02-56-953484-httpclient%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8%EC%97%90-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%9A%94%EC%B2%AD%EC%9D%84-%EB%8F%99%EC%8B%9C%EC%97%90-%EB%B3%B4%EB%82%B4%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%80/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;RestTemplate 정의, 특징, 동작원리: &lt;a href=&quot;https://sjh836.tistory.com/141&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sjh836.tistory.com/141&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spring RestTemplate: &lt;a href=&quot;https://dejavuhyo.github.io/posts/spring-resttemplate/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dejavuhyo.github.io/posts/spring-resttemplate/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;HttpInterface:&amp;nbsp;&lt;a href=&quot;https://mangkyu.tistory.com/291&quot;&gt;https://mangkyu.tistory.com/291&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spring 6.0의 HTTPInterface 끄적이기: &lt;a href=&quot;https://medium.com/@auburn0820/spring-6-0%EC%9D%98-httpinterface-%EB%81%84%EC%A0%81%EC%9D%B4%EA%B8%B0-f3653143a373&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@auburn0820/spring-6-0%EC%9D%98-httpinterface-%EB%81%84%EC%A0%81%EC%9D%B4%EA%B8%B0-f3653143a373&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;RestClient 공식문서: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/rest-clients.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/integration/rest-clients.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;RestClient: &lt;a href=&quot;https://www.baeldung.com/spring-boot-restclient&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/spring-boot-restclient&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[Spring]&amp;nbsp;Spring&amp;nbsp;Boot3.2에&amp;nbsp;새롭게&amp;nbsp;추가될&amp;nbsp;RestClient: &lt;a href=&quot;https://mangkyu.tistory.com/303&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/303&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>API</category>
      <category>RESTClient</category>
      <category>restTemplate</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>webclient</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/217</guid>
      <comments>https://myvelop.tistory.com/217#entry217comment</comments>
      <pubDate>Sun, 24 Dec 2023 18:26:23 +0900</pubDate>
    </item>
    <item>
      <title>글또 9기 발돋움</title>
      <link>https://myvelop.tistory.com/218</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또를 지원하게 되기까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업을 준비를 시작하고 취업이 된 후에도 한동안 눈앞에 보이는 것에만 정신이 팔려 살았다. 꾸준히 작성해왔던 블로그도 소홀해졌고 외부 활동도 거의 하지 않았다. 오로지 삶의 밸런스를 잡아가는 것에 집중했다. 시간이 지나 대충 직장에 적응했고 여유가 생겨 자신을 돌아볼 시간이 생겼다. 글쓰기라는 것이 생략되었던 탓일까, 회사에서 한 것은 많은데 머리 속에 정리가 하나도 안 되어 있다는 사실을 알게 됐다. 여기에 더해 내 주변 사람들은 다들 커뮤니티, 스터디 등에 열심히 참여하며 성장하고 있는데 나는 집과 직장이 서울과 멀다는 이유로 커뮤니티 활동을 미뤄오고 있었다. 그렇게&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 회사와 집만 왔다갔다 하다보니 우물 안 개구리가 되어가고 있었고 내 성장이 더디다는 생각을 떨칠 수가 없었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 와중에 부스트캠프 동기 분께 글또를 강력 추천받았다. 글을 작성하고 커뮤니티를 통해 서로 좋은 영향력을 주고 받는 곳이라는 점에서 큰 매력을 느꼈다. 글또 페이지에 들어가 운영 방식이나 가이드라인 등을 확인해봤는데 철학이 너무 마음에 들었다. 10기까지만 운영한다는 글을 확인한 나는 망설이지 않고 이번에 꼭 지원하기로 결정했다.&lt;/p&gt;
&lt;figure id=&quot;og_1701499742401&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;글 쓰는 또라이가 세상을 바꾼다 - 글또 페이지&quot; data-og-description=&quot;  안녕하세요 :)&quot; data-og-host=&quot;www.notion.so&quot; data-og-source-url=&quot;https://www.notion.so/zzsza/ac5b18a482fb4df497d4e8257ad4d516&quot; data-og-url=&quot;https://www.notion.so/ac5b18a482fb4df497d4e8257ad4d516&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bIZncl/hyUFdw3KmZ/uTYkCjm9wszF2ymK5tJDD0/img.png?width=2000&amp;amp;height=1957&amp;amp;face=0_0_2000_1957,https://scrap.kakaocdn.net/dn/bN0Hm4/hyUE2CiNYW/00z44kkbYbSFwdu7ItpgeK/img.png?width=2000&amp;amp;height=1957&amp;amp;face=0_0_2000_1957&quot;&gt;&lt;a href=&quot;https://www.notion.so/zzsza/ac5b18a482fb4df497d4e8257ad4d516&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.notion.so/zzsza/ac5b18a482fb4df497d4e8257ad4d516&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bIZncl/hyUFdw3KmZ/uTYkCjm9wszF2ymK5tJDD0/img.png?width=2000&amp;amp;height=1957&amp;amp;face=0_0_2000_1957,https://scrap.kakaocdn.net/dn/bN0Hm4/hyUE2CiNYW/00z44kkbYbSFwdu7ItpgeK/img.png?width=2000&amp;amp;height=1957&amp;amp;face=0_0_2000_1957');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;글 쓰는 또라이가 세상을 바꾼다 - 글또 페이지&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  안녕하세요 :)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.notion.so&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또 지원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또 지원기간 전에 미리 알림 메일을 신청했다. 지원 기간이 되자 메일이 왔고 바로 글또 지원서를 작성하기 시작했다. 총 10문항의 질문이 있었다. 글또를 시작하기 전에 참가자 본인이 더 의미있는 활동을 만들어갈 수 있도록 유도하는 질문으로 구성되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중 삶의 지도를 작성해 링크를 제출하라는 문항이 있었다. 과거와 현재의 삶에 대해서 지도를 그리듯이 회고하는 글을 작성하는 것이 주제였다. 글을 작성하다보니 나도 몰랐던 나를 알아간다는 생각이 들었다. 내가 과거에 어떤 가치관을 가지고 살아왔는지, 또 현재는 어떤 가치관을 가지고 있는지 고민해볼 수 있었고, 순간순간의 선택으로 생긴 기회와 상처들에 대해 돌아보는 시간이 되었다. 꼭 합류하고 싶다는 생각에 부끄러움을 무릅쓰고 글을 작성하고 읽는 것에 일가견이 있는 분에게 리뷰를 요청해 피드백도 받았다. 최종적으로 삶의 지도는 공백 포함해서 3,318자의 글을 적었다. 나머지 문항도 성실히 작성해 제출했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QhwBN/btsBlaE4F81/CHR0Xg8YnTp73U0L1pq4P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QhwBN/btsBlaE4F81/CHR0Xg8YnTp73U0L1pq4P0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QhwBN/btsBlaE4F81/CHR0Xg8YnTp73U0L1pq4P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQhwBN%2FbtsBlaE4F81%2FCHR0Xg8YnTp73U0L1pq4P0%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;457&quot; height=&quot;97&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 약 500명이라는 많은 수의 개발자 분들이 글또에 지원했다. 운이 좋겠도 합격했다는 메일을 받았고 글또 9기에 참여할 기회를 얻었다. 초대된 슬랙에서 자기소개를 남기고 다른 분들의 소개를 보는 것으로 글또 활동이 시작되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHaMWa/btsBfK2zVSj/tmJapFJ4WjZFK5HLoLtcMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHaMWa/btsBfK2zVSj/tmJapFJ4WjZFK5HLoLtcMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHaMWa/btsBfK2zVSj/tmJapFJ4WjZFK5HLoLtcMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHaMWa%2FbtsBfK2zVSj%2FtmJapFJ4WjZFK5HLoLtcMK%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;579&quot; height=&quot;209&quot; data-origin-width=&quot;579&quot; data-origin-height=&quot;209&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;글또 OT&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11월 26일 일요일 오후 10시에 글또 OT 일정이 잡혔다. 오리엔테이션은 글또 커뮤니티 창시자인 변성윤 님의 발표 형식으로 진행되었다. 글또의 글 작성, 제출 활동과 커피챗, 그 외 다양한 활동(빌리지 반상회, 유데미와 협업, 고민 상담소 등)에 대한 설명을 해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글또에 참여하는 개발자 분들에게 글쓰기 팁이나, 여러 액션 아이템 등 가이드라인을 제공해주셨다. 제시해주신 글쓰기 팁은 아래와 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;글쓰기 Tip&lt;/h4&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 글의 핵심 메시지를 2~3줄로 정리하면?&lt;/li&gt;
&lt;li&gt;이 글을 읽은 사람이 어떻게 되기를 바라는지?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일단 글을 다 작성하고 난 뒤에 지속적으로 수정하는 것이 좋다. (스프린트 진행하듯이)&lt;/li&gt;
&lt;li&gt;다른 사람과 비교하지 말고 과거의 나와 비교하기&lt;/li&gt;
&lt;/ul&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;OT를 듣고 나서 최근 회고글 위주로 과거의 글 몇 개를 훑어 봤다. 예전에 작성했던 글을 읽어보니 고칠 것들이 조금씩 보였다. 글을 수정하고 나니 전보다는 확실히 보기 좋아졌다. 글을 처음부터 완벽하게 쓰자는 욕심은 버리고 시간날 때마다 셀프 피드백하는 전략을 취해야겠다고 다짐했다.&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;&amp;nbsp;&lt;/p&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;/li&gt;
&lt;li&gt;(현재 평균적으로 500명 정도의 방문자가 블로그 방문) 글또가 끝날 때쯤엔 1000명의 방문자가 찾아오는 블로그로 성장시키고 싶다.&lt;/li&gt;
&lt;li&gt;3번 이상 링크드인 같은 커뮤니티에 글을 공유하고 싶다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5개월의 과정을 돌아봤을 때 긍정적인 변화가 생겼으면 좋겠고 후회가 없었으면 한다. 최근 기한에 쫓기는 일을 많이 담당하고 있는데 일정 관리를 신경써야할 것 같다. 회사 일도 잡고, 글또 활동도 잘 해내고 싶다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작성하고 싶은 글의 주제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 해왔던 것들을 잘 정리하고 싶다. 처음 입사했을 때 문서나 컨벤션도 없고, PR을 통한 코드리뷰도 하지 않는 회사였는데 나의 작은 실천 하나하나가 만들어낸 회사의 문서화, 코드리뷰 문화에 대해 글을 작성해보고 싶다.&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;Spring 3.2에 새롭게 출시된 RestClient 활용&lt;/li&gt;
&lt;li&gt;스프링과 자바의 스레드, 병렬 처리를 통한 성능 개선&lt;/li&gt;
&lt;li&gt;신입 개발자로서 회사의 개발 문화를 개선한 경험&lt;/li&gt;
&lt;li&gt;RestDocs + Swagger 작성 시 고려할 점&lt;/li&gt;
&lt;li&gt;스프링에서의 RabbitMQ 활용&lt;/li&gt;
&lt;li&gt;도메인, 네임서버, 호스팅의 개념. 인프라 이전 하기 전 고려해야할 것(더 안전하게 이전하는 방법)들은 무엇인 있는지.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그 외 하고 싶은 것&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 개발자 분들을 만나고 보고 싶어 지원한 이유도 있기 때문에 커피챗이나 여러 활동들에 참여해보고 싶다. 모임에 최소 3회 이상 참여하고 싶다. 그리고 공부하고 싶은 주제의 스터디가 있다면 참여해보고 싶다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동/글또</category>
      <category>9기</category>
      <category>글또</category>
      <category>기술블로그</category>
      <category>블로그</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/218</guid>
      <comments>https://myvelop.tistory.com/218#entry218comment</comments>
      <pubDate>Sun, 3 Dec 2023 17:47:25 +0900</pubDate>
    </item>
    <item>
      <title>Spring REST Docs 설정하기 (build.gradle &amp;amp; .kts)</title>
      <link>https://myvelop.tistory.com/214</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REST Docs&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Rest Docs는 Spring MVC를 사용하는 REST API를 문서화할 때 사용하는 툴이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다양한 API 문서화 도구&lt;/h3&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;노션이나 깃허브 Wiki 등 문서 툴을 사용해 직접 API 문서를 작성하는 방식이다.&lt;/li&gt;
&lt;li&gt;서비스 코드의 변경, 작성 실수 등의 이유로 인해 내가 작성한 API 스펙과 실제 코드의 API 스펙이 달라질 수 있다는 문제가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Swagger&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대중적인 API 문서화 툴이다.&lt;/li&gt;
&lt;li&gt;Swagger를 사용해서 API를 문서화하면 아래와 같이 서비스 코드에 Swagger 관련 어노테이션과 코드가 작성되어야 한다. 때문에 코드의 양이 방대해져 서비스 가독성이 떨어뜨릴 수 있는 문제점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691497715828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/v1/categories&quot;)
@RequiredArgsConstructor
public class CategoryController {

	private final CategoryService categoryService;
  
    @Operation(summary = &quot;find category&quot;, description = &quot;카테고리 리스트 조회 API&quot;)
    @ApiResponses({@ApiResponse(responseCode = &quot;200&quot;, content = {
      @Content(schema = @Schema(implementation = FindCategoryResponseSwagger.class))}),
      @ApiResponse(responseCode = &quot;400&quot;, description = ExceptionMessage.INVALID_PAGE_REQUEST, content = {
          @Content(schema = @Schema(implementation = InvalidPageRequestExceptionSwagger.class))}),
      @ApiResponse(responseCode = &quot;403&quot;, description = ExceptionMessage.FORBIDDEN, content = {
          @Content(schema = @Schema(implementation = AccessForbiddenSwagger.class))})})
    @PageableAsQueryParam
    @GetMapping
    public ResponseEntity&amp;lt;ResultDTO&amp;lt;PageResponse&amp;lt;FindCategoryResponse&amp;gt;&amp;gt;&amp;gt; findCategories(
          @Valid @ParameterObject @ModelAttribute FindCategoryRequest request,
          @Parameter(hidden = true) Pageable pageable) {
        Page&amp;lt;FindCategoryResponse&amp;gt; categoryPage = categoryService.findCategories(request.toService(),
                pageable)
            .map(FindCategoryServiceResponse::toResponse);
        PageResponse&amp;lt;FindCategoryResponse&amp;gt; response = new PageResponse&amp;lt;&amp;gt;(categoryPage);
        return ResponseEntity.ok(new ResultDTO&amp;lt;&amp;gt;(ResponseStatus.OK, &quot;&quot;, response));
    }
 	
    ...
}&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;Swagger 또한 일반 문서 작성과 마찬가지로 설명을 직접 적어야하기 때문에 실수가 발생할 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691498525973&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
public class FindCategoryResponseSwagger {

  @Schema(description = &quot;Result Code&quot;, example = ResponseStatus.OK)
  private String status;

  @Schema(description = &quot;Message&quot;, example = ResponseMessage.FIND_CATEGORY)
  private String message;

  private PageResponse&amp;lt;FindCategoryResponse&amp;gt; data;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Rest Docs를 사용해야 하는 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Rest Docs를 사용하면 테스트 코드가 성공해야만 문서가 생성되기 때문에 서비스 코드가 변경되어서 생기는 실제 API와 문서상 API 스펙의 괴리가 자동으로 체크해줄 수 있다.&lt;/li&gt;
&lt;li&gt;또한, 프로덕션 코드가 아닌 테스트코드를 작성하면서 API 명세를 작성할 수 있어 Swagger가 가진 문제를 해결할 수 있다.&lt;/li&gt;
&lt;li&gt;단순 Asciidoc으로 작성해 만든 html 파일은 UI가 그닥 예쁘지 않지만, Rest Docs와 Swagger를 연동해 Swagger-UI를 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gradle 설정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 17 / Spring Boot 3.1.2 / Gradle 8.2.1 기준&lt;/li&gt;
&lt;li&gt;아래 링크의 공식 문서를 참고해 설정을 진행했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1690892932554&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring REST Docs&quot; data-og-description=&quot;Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot; data-og-url=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring REST Docs&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Build.gradle (Groovy)&lt;/h3&gt;
&lt;pre id=&quot;code_1690893616174&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.2'
    id 'io.spring.dependency-management' version '1.1.2'
    
    // 1. asciidoctor 플러그인 추가
    id 'org.asciidoctor.jvm.convert' version '3.3.2' 
}

group = 'com.onebyte'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    
    // 2. configuration 추가
    asciidoctorExt 
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'

    /**
     * Test Implementation
     */
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // 3. REST Docs Implementation 추가
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}

// 4. sinppetsDir 추가
ext {
    snippetsDir = file('build/genertated-snippets')
}

// 5. test Task snippetsDir 추가
tasks.named('test') {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

// 6. asciidoctor Task 추가
tasks.named('asciidoctor') {
    inputs.dir snippetsDir
    configurations 'asciidoctorExt'
    dependsOn test
}

// 7. bootJar Settings
bootJar {
    dependsOn asciidoctor
    from (&quot;${asciidoctor.outputDir}&quot;) {
        into 'static/docs'
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Build.gradle.kts (Kotlin)&lt;/h3&gt;
&lt;pre id=&quot;code_1690844967943&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    java
    id(&quot;org.springframework.boot&quot;) version &quot;3.1.2&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.2&quot;

    // 1. asciidoctor 플러그인 추가
    id(&quot;org.asciidoctor.jvm.convert&quot;) version &quot;3.3.2&quot;
}

group = &quot;com.onebyte&quot;
version = &quot;0.0.1-SNAPSHOT&quot;
val asciidoctorExt: Configuration by configurations.creating // 2. configuration 추가

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    compileOnly(&quot;org.projectlombok:lombok&quot;)
    annotationProcessor(&quot;org.projectlombok:lombok&quot;)
    runtimeOnly(&quot;com.mysql:mysql-connector-j&quot;)


    /**
     * Test
     */
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)

    // 3. RestDoc Implementation
    asciidoctorExt(&quot;org.springframework.restdocs:spring-restdocs-asciidoctor&quot;)
    testImplementation(&quot;org.springframework.restdocs:spring-restdocs-mockmvc&quot;)
}

// 4. sinppetsDir 추가
val snippetsDir by extra { file(&quot;build/generated-snippets&quot;) }

tasks {

    // 5. test Task snippetsDir 추가
    test {
        outputs.dir(snippetsDir)
        useJUnitPlatform()
    }

    // 6. asciidoctor Task 추가
    asciidoctor {
        inputs.dir(snippetsDir)
        configurations(&quot;asciidoctorExt&quot;)
        dependsOn(test)
    }

    // 7. bootJar Settings
    bootJar {
        dependsOn(asciidoctor)
        from (&quot;build/docs/asciidoc&quot;) {
            into(&quot;static/docs&quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;REST Docs 작성 및 사용 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TestConfiguration&lt;/h3&gt;
&lt;pre id=&quot;code_1690845373803&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.onebyte.springboilerplate.config;

import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;

import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@TestConfiguration
public class RestDocsConfiguration {

  @Bean
  public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
    return configurer -&amp;gt; configurer.operationPreprocessors()
        .withRequestDefaults(prettyPrint())
        .withResponseDefaults(prettyPrint());
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Test 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1690894123394&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AutoConfigureRestDocs
@WebMvcTest(UserController.class)
@Import(RestDocsConfiguration.class)
class UserControllerTest {

  @Autowired
  MockMvc mvc;

  @MockBean
  UserService userService;

  @Test
  void test() throws Exception {
    FieldDescriptor[] reviews = getReviewFieldDescriptors();

    List&amp;lt;UserDto&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
    UserDto user = UserDto.builder().id(1).username(&quot;bell&quot;).age(26).build();
    list.add(user);

    // when
    Mockito.when(userService.findUserAll())
        .thenReturn(list);

    ResultActions actions = mvc.perform(MockMvcRequestBuilders.get(&quot;/v1/users&quot;)
        .accept(MediaType.APPLICATION_JSON));
    actions.andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath(&quot;$[0].username&quot;).value(&quot;bell&quot;))
        .andDo(MockMvcRestDocumentation.document(&quot;user&quot;));
  }

  private FieldDescriptor[] getReviewFieldDescriptors() {
    return new FieldDescriptor[]{
        fieldWithPath(&quot;username&quot;).description(&quot;이름&quot;),
        fieldWithPath(&quot;age&quot;).description(&quot;나이&quot;)
    };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Test 실행&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;test를 실행하면 generated-snippets에 Test 코드에서 지정한 디렉토리로 adoc 확장자 파일이 생성된다. 공식문서에도 나와있다시피 기본적으로 생성되는 adoc파일은 아래 6가지이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boJSaL/btspK2ZYLnN/cZCgM2FZ0CWtGadVIjKGW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boJSaL/btspK2ZYLnN/cZCgM2FZ0CWtGadVIjKGW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boJSaL/btspK2ZYLnN/cZCgM2FZ0CWtGadVIjKGW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboJSaL%2FbtspK2ZYLnN%2FcZCgM2FZ0CWtGadVIjKGW0%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; alt=&quot;generated-snippets&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;440&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;curl-request.adoc&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서 작성한 MockMvc 호출과 동일한 curl 명령어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690894498829&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[source,bash]
----
$ curl 'http://localhost:8080/v1/users' -i -X GET \
    -H 'Accept: application/json'
----&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;http-request.adoc&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서 작성한 MockMvc 호출과 동일한 HTTP 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690894430777&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[source,bash]
----
$ http GET 'http://localhost:8080/v1/users' \
    'Accept:application/json'
----&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;http-response.adoc&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서 MockMvc 호출했을 때 반환된 HTTP Response&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690894480778&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[source,http,options=&quot;nowrap&quot;]
----
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 55

[ {
  &quot;id&quot; : 1,
  &quot;username&quot; : &quot;bell&quot;,
  &quot;age&quot; : 26
} ]
----&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;httpie-request.adoc&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드에서 작성한 MockMvc 호출과 동일한 HTTPie 명령어&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690894524021&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[source,bash]
----
$ http GET 'http://localhost:8080/v1/users' \
    'Accept:application/json'
----&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;request-body.adoc&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MockMvc를 호출할 때 보냈던 RequestBody&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690894542482&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[source,options=&quot;nowrap&quot;]
----

----&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;response-body.adoc&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MockMvc를 호출하고 반환받은 ResponseBody&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690894558392&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[source,json,options=&quot;nowrap&quot;]
----
[ {
  &quot;id&quot; : 1,
  &quot;username&quot; : &quot;bell&quot;,
  &quot;age&quot; : 26
} ]
----&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;asciidoctor로 index.html 추출하기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;adoc 작성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래는 Spring REST Docs 공식 문서에 적혀있는 내용이다. Asciidoc을 사용할 때 파일 위치를 어디에 둬야하는지 친절하게 설명해줬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baXloJ/btspK27LeVy/MLsMEUOmKVLwMd88Auml2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baXloJ/btspK27LeVy/MLsMEUOmKVLwMd88Auml2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baXloJ/btspK27LeVy/MLsMEUOmKVLwMd88Auml2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaXloJ%2FbtspK27LeVy%2FMLsMEUOmKVLwMd88Auml2k%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;644&quot; height=&quot;514&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;514&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;아래와 같이 src/docs/asciidoc 디렉토리를 만들고 index.adoc 파일을 생성하자. 해당 파일을 정확한 위치에 만들어주지 않으면 index.html을 생성하지 않으므로 주의하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfjxNi/btspMkTFneq/gPtooXJEjhmEZkvuBSEPL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfjxNi/btspMkTFneq/gPtooXJEjhmEZkvuBSEPL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfjxNi/btspMkTFneq/gPtooXJEjhmEZkvuBSEPL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfjxNi%2FbtspMkTFneq%2FgPtooXJEjhmEZkvuBSEPL1%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; alt=&quot;docs directory&quot; loading=&quot;lazy&quot; width=&quot;536&quot; height=&quot;367&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;516&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;index.adoc 내용의 형식은 아래와 같이 작성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690984886902&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;= Title

== User API

=== User Find All
- http request
include::{snippets}/user/http-request.adoc[]

- http response
include::{snippets}/user/http-response.adoc[]

- response body
include::{snippets}/user/response-body.adoc[]&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;IntelliJ Settings에서 Plugins에 들어와 AsciiDoc 플러그인을 설치하면 adoc 파일을 편집하면서 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k0ph6/btspOBUR5d9/8aRKkKd64v4QuYoxeKWKj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k0ph6/btspOBUR5d9/8aRKkKd64v4QuYoxeKWKj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k0ph6/btspOBUR5d9/8aRKkKd64v4QuYoxeKWKj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk0ph6%2FbtspOBUR5d9%2F8aRKkKd64v4QuYoxeKWKj1%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; alt=&quot;AsciiDoc Plugins&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;429&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;1122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cns35q/btspUonOYiw/pJKHNskdG1wmaUjiW7598K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cns35q/btspUonOYiw/pJKHNskdG1wmaUjiW7598K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cns35q/btspUonOYiw/pJKHNskdG1wmaUjiW7598K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcns35q%2FbtspUonOYiw%2FpJKHNskdG1wmaUjiW7598K%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; alt=&quot;asciidoc plugins&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;1122&quot; data-origin-width=&quot;1988&quot; data-origin-height=&quot;1122&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;상단 메뉴의 HTML 버튼을 클릭하면 해당 adoc 폴더가 위치하는 디렉토리에 index.html을 자동으로 생성해주니 참고하자. Asciidoc의 상세한 문법은 아래 링크에서 확인해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1690896062370&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Asciidoc 기본 사용법&quot; data-og-description=&quot;Asciidoc의 기본 문법을 설명한다&quot; data-og-host=&quot;narusas.github.io&quot; data-og-source-url=&quot;https://narusas.github.io/2018/03/21/Asciidoc-basic.html&quot; data-og-url=&quot;https://narusas.github.io/2018/03/21/Asciidoc-basic.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/l3lLU/hyTvjZIyte/x0gy1KJE0kZD3ppiFDcmnK/img.png?width=200&amp;amp;height=64&amp;amp;face=0_0_200_64&quot;&gt;&lt;a href=&quot;https://narusas.github.io/2018/03/21/Asciidoc-basic.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://narusas.github.io/2018/03/21/Asciidoc-basic.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/l3lLU/hyTvjZIyte/x0gy1KJE0kZD3ppiFDcmnK/img.png?width=200&amp;amp;height=64&amp;amp;face=0_0_200_64');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Asciidoc 기본 사용법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Asciidoc의 기본 문법을 설명한다&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;narusas.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;index.html 생성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gradle build를 실행하면 bootJar가 실행될 때 아래와 같이 docs/asciidoc 경로에 index.html을 생성해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7HWBR/btspNPNAPPo/trKvhZkWGQQpa0WugLtxfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7HWBR/btspNPNAPPo/trKvhZkWGQQpa0WugLtxfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7HWBR/btspNPNAPPo/trKvhZkWGQQpa0WugLtxfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7HWBR%2FbtspNPNAPPo%2FtrKvhZkWGQQpa0WugLtxfK%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; alt=&quot;index.html&quot; loading=&quot;lazy&quot; width=&quot;694&quot; height=&quot;262&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;262&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;index.html을 확인해보면 아래와 같이 문서가 잘 생성되었음을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpY6Ik/btspRJ0pOh2/HgsMBwlyDztz6nRyh7Uu4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpY6Ik/btspRJ0pOh2/HgsMBwlyDztz6nRyh7Uu4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpY6Ik/btspRJ0pOh2/HgsMBwlyDztz6nRyh7Uu4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpY6Ik%2FbtspRJ0pOh2%2FHgsMBwlyDztz6nRyh7Uu4k%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; alt=&quot;asciidoc api specification&quot; loading=&quot;lazy&quot; width=&quot;692&quot; height=&quot;930&quot; data-origin-width=&quot;995&quot; data-origin-height=&quot;930&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;h4 data-ke-size=&quot;size20&quot;&gt;index.html 파일 이동시키기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 만들어진 index.html을 main/resources로 옮겨보자.&lt;/li&gt;
&lt;li&gt;copyAsciidoc 이라는 task를 하나 생성했다. asciidoctor가 실행될 때 같이 실행되도록 설정했고 위 디렉토리에 생성된 index.html을 src/main/resource/static/docs 디렉토리에 옮겼다.&lt;/li&gt;
&lt;li&gt;그리고 build가 실행될 때 copyAsciidoc이 실행되도록 했다.&lt;/li&gt;
&lt;li&gt;build.gradle 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690984249993&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;task copyAsciidoc(type: Copy) {
    dependsOn asciidoctor
    from file(&quot;$buildDir/docs/asciidoc&quot;)
    into file(&quot;src/main/resources/static/docs&quot;)
}

build {
    dependsOn copyAsciidoc
}&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;build.gradle.kts 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691017011072&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;task {
	
    ...
	
    register&amp;lt;Copy&amp;gt;(&quot;copyAsciidoctor&quot;) {
        dependsOn(asciidoctor)
        from(file(&quot;$buildDir/docs/asciidoc&quot;))
        into(file(&quot;src/main/resources/static/docs&quot;))
    }

    build {
        dependsOn(&quot;copyAsciidoctor&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;이제 gradle build를 실행해보자. 아래와 같이 index.html이 복사되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cl9uqn/btspRhJv5Gv/ZvkZZOkV8s1lJ7MiTGQLI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cl9uqn/btspRhJv5Gv/ZvkZZOkV8s1lJ7MiTGQLI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cl9uqn/btspRhJv5Gv/ZvkZZOkV8s1lJ7MiTGQLI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcl9uqn%2FbtspRhJv5Gv%2FZvkZZOkV8s1lJ7MiTGQLI1%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; alt=&quot;docs index.html&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;367&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;464&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Response-Fields&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;org.springframework.restdocs.payload.PayloadDocumentation 패키지의 responseFields 메소드와 fieldWithPath 메소드를 사용해 작성할 수 있다.&lt;/li&gt;
&lt;li&gt;주의사항으로는 Response Fields를 적을 땐 모든 필드에 대해 적어줘야 한다는 점이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691496824589&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void testFindUser() throws Exception {
    // given
    UserDto response = UserDto.builder().id(1).username(&quot;bell&quot;).age(26).build();

    // when
    Mockito.when(userService.findUser(1))
        .thenReturn(response);

    // then
    ResultActions actions = mvc.perform(MockMvcRequestBuilders.get(&quot;/v1/users/1&quot;)
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON));

    // responseFields를 작성할 땐 모든 필드를 작성해야한다.
    actions.andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.jsonPath(&quot;$.data&quot;, equalTo(asParsedJson(response))))
        .andDo(MockMvcRestDocumentation.document(&quot;user/findUser&quot;, responseFields(
            fieldWithPath(&quot;data&quot;).type(JsonFieldType.OBJECT).description(&quot;data&quot;),
            fieldWithPath(&quot;message&quot;).type(JsonFieldType.STRING).description(&quot;message&quot;),
            fieldWithPath(&quot;data.id&quot;).type(JsonFieldType.NUMBER).description(&quot;The user's primary key&quot;),
            fieldWithPath(&quot;data.username&quot;).type(JsonFieldType.STRING).description(&quot;The user's name&quot;),
            fieldWithPath(&quot;data.age&quot;).type(JsonFieldType.NUMBER).description(&quot;The user's age&quot;)
        )))
        .andDo(print());
}&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;아래와 같은 약식으로 Fields 설명이 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fk4Ck/btsqwAbFaJ0/YVeaTDG64G2pU4fqcRmEV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fk4Ck/btsqwAbFaJ0/YVeaTDG64G2pU4fqcRmEV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fk4Ck/btsqwAbFaJ0/YVeaTDG64G2pU4fqcRmEV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFk4Ck%2FbtsqwAbFaJ0%2FYVeaTDG64G2pU4fqcRmEV1%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; alt=&quot;response-fields&quot; loading=&quot;lazy&quot; width=&quot;1276&quot; height=&quot;598&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;598&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;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;아래와 같이 :toc: 을 넣어주면 자동으로 목차를 생성해준다.&lt;/li&gt;
&lt;li&gt;= 바로 밑에 넣어줘야 목차가 생성된다. 만약 줄바꿈을 하거나 다른 위치에서 사용하려고 하면 목차가 생성되지 않으니 주의하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691497136408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;= User API
:toc:&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;942&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnddSs/btsqBvnjXRP/kPBuzBkoXoiuTKn9r2bUw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnddSs/btsqBvnjXRP/kPBuzBkoXoiuTKn9r2bUw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnddSs/btsqBvnjXRP/kPBuzBkoXoiuTKn9r2bUw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnddSs%2FbtsqBvnjXRP%2FkPBuzBkoXoiuTKn9r2bUw0%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; alt=&quot;자동으로 작성된 목차&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;470&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;942&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;아래와 같이 :toc: left 와 같이 방향을 넣어주면 해당 방향에 고정된 목차를 생성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k7rBR/btsqBqfhMKz/Zk8k2SC2mQbxuEL62KMi70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k7rBR/btsqBqfhMKz/Zk8k2SC2mQbxuEL62KMi70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k7rBR/btsqBqfhMKz/Zk8k2SC2mQbxuEL62KMi70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk7rBR%2FbtsqBqfhMKz%2FZk8k2SC2mQbxuEL62KMi70%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; alt=&quot;toc left 목차 생성&quot; loading=&quot;lazy&quot; width=&quot;748&quot; height=&quot;386&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;427&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;https://velog.io/@backtony/Spring-REST-Docs-%EC%A0%81%EC%9A%A9-%EB%B0%8F-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%95%98%EA%B8%B0&lt;/li&gt;
&lt;li&gt;https://chordplaylist.tistory.com/300&lt;/li&gt;
&lt;li&gt;https://woo-chang.tistory.com/62&lt;/li&gt;
&lt;li&gt;https://kth990303.tistory.com/347&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>asciidoc</category>
      <category>mockmvc</category>
      <category>RestDocs</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>TEST</category>
      <category>스프링부트</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/214</guid>
      <comments>https://myvelop.tistory.com/214#entry214comment</comments>
      <pubDate>Tue, 8 Aug 2023 21:46:49 +0900</pubDate>
    </item>
    <item>
      <title>Jacoco 설정하기 (build.gradle &amp;amp; .kts)</title>
      <link>https://myvelop.tistory.com/215</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Jacoco&lt;/h2&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;CI/CD와 연계해 테스트 커버리지를 충분히 채우지 못하면 배포가 되지 못하게 하는 등 구성원들에게 테스트 코드를 강제할 때 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;여기서 커버리지란 Test를 실행했을 때 Code가 얼마나 빈틈없이 실행됐는지 측정한 수치이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gradle 설정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Java 17 / Spring Boot 3.1.2 / Gradle 8.2.1 기준&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle (groovy)&lt;/h3&gt;
&lt;pre id=&quot;code_1691070410774&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.2'
    id 'io.spring.dependency-management' version '1.1.2'

    // 1. Jacoco 플러그인 추가
    id 'jacoco'
}

group = 'com.onebyte'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'

    /**
     * Test Implementation
     */
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    ...
}

/**
 * Jacoco
 */
tasks.named('test') {
    useJUnitPlatform()
    // 2. test 종료 후 jacocoTestReport 실행
    finalizedBy jacocoTestReport
}

// 3. 커버리지 결과를 html 파일로 가공
jacocoTestReport {
    dependsOn test
}

// 4. 커버리지 기준을 만족하는지 확인해 주는 task
jacocoTestCoverageVerification {
    violationRules {
        rule {
            enabled = true
        
            element = 'CLASS'

            limit {
                counter = 'BRANCH'
                value = 'COVERDRATIO'
                minimum = 0.80
            }
            
            excludes = listOf(
                    &quot;*.Config.*&quot;,
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;build.gradle.kts&lt;/h3&gt;
&lt;pre id=&quot;code_1691071427944&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    java
    id(&quot;org.springframework.boot&quot;) version &quot;3.1.2&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.2&quot;
    
    // 1. Jacoco 플러그인 추가
    id(&quot;jacoco&quot;)
}

group = &quot;com.onebyte&quot;
version = &quot;0.0.1-SNAPSHOT&quot;

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    compileOnly(&quot;org.projectlombok:lombok&quot;)
    annotationProcessor(&quot;org.projectlombok:lombok&quot;)
    runtimeOnly(&quot;com.mysql:mysql-connector-j&quot;)

    /**
     * Test
     */
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
}


/**
 * Jacoco
 */
tasks {

    test {
        useJUnitPlatform()
        // 2. test 종료 후 jacocoTestReport 실행
        finalizedBy(jacocoTestReport)
    }

    // 3. 커버리지 결과를 html 파일로 가공
    jacocoTestReport {
        dependsOn(test)
    }

    // 4. 커버리지 기준을 만족하는지 확인해주는 task
    jacocoTestCoverageVerification {
        violationRules {
            rule {
                enabled = true

                element = &quot;CLASS&quot;

                limit {
                    counter = &quot;BRANCH&quot;
                    value = &quot;COVEREDRATIO&quot;
                    minimum = &quot;0.80&quot;.toBigDecimal()
                }

                // 커버리지 체크 제외 클래스 지정
                excludes = listOf(
                    &quot;*.Config.*&quot;,
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설명&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;test task에서 finalizedBy jacocoTestReport를 이용해 테스트가 종료된 후 jacocoTestReport가 실행되도록 설정했다.&lt;/li&gt;
&lt;li&gt;jacocoTestReport는 커버리지 결과는 xml, csv, html과 같이 보기 쉬운 파일로 생성해주는 task이다.&lt;/li&gt;
&lt;li&gt;jacocoTestCoverageCerification는 커버리지 기준, 커버리지 대상 등에 대한 기준을 정할 수 있는 task이다. 자세한 설정에 대한 내용은 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1691071017440&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tasks {

	...

    jacocoTestCoverageVerification {
        violationRules {
            rule {
            	// 커버리지를 아래 옵션으로 키고 끌 수 있다.
                enabled = true
            
            	// 룰을 체크할 단위는 클래스 단위로 설정한 것
                element = &quot;CLASS&quot;

                limit {
                    counter = &quot;BRANCH&quot; // 브랜치 대상
                    value = &quot;COVEREDRATIO&quot; // 커버리지 비율
                    minimum = &quot;0.80&quot;.toBigDecimal() // 최소 80퍼센트
                }
                
                // 커버리지 체크 제외 클래스 지정
                excludes = listOf(
                    &quot;*.Config.*&quot;,
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gradle test 실행&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이 jacoco를 설정하고 test를 실행해보자.&lt;/li&gt;
&lt;li&gt;build/reports/jacoco/test/html 디렉토리에 index.html 파일이 생성되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq3zrZ/btspUibqKfe/rHCEtdBu7apfKT42Oc58Y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq3zrZ/btspUibqKfe/rHCEtdBu7apfKT42Oc58Y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq3zrZ/btspUibqKfe/rHCEtdBu7apfKT42Oc58Y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq3zrZ%2FbtspUibqKfe%2FrHCEtdBu7apfKT42Oc58Y1%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; alt=&quot;jacoco report&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;493&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;1006&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;index.html을 열면 아래와 같이 커버리지 페이지를 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buRI18/btspVNWdTms/6X7ciayXsd5ZgnI2Oulhpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buRI18/btspVNWdTms/6X7ciayXsd5ZgnI2Oulhpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buRI18/btspVNWdTms/6X7ciayXsd5ZgnI2Oulhpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuRI18%2FbtspVNWdTms%2F6X7ciayXsd5ZgnI2Oulhpk%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;625&quot; height=&quot;268&quot; data-origin-width=&quot;625&quot; data-origin-height=&quot;268&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;어떤 부분이 테스트되었고 테스트되지 않았는지 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bma92o/btsp343rJEw/W4wpRPfajzf04adk2ZY4LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bma92o/btsp343rJEw/W4wpRPfajzf04adk2ZY4LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bma92o/btsp343rJEw/W4wpRPfajzf04adk2ZY4LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbma92o%2Fbtsp343rJEw%2FW4wpRPfajzf04adk2ZY4LK%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; alt=&quot;jacoco report&quot; loading=&quot;lazy&quot; width=&quot;497&quot; height=&quot;409&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;409&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;우형 블로그: https://techblog.woowahan.com/2661/&lt;/li&gt;
&lt;li&gt;Jacoco 버전: https://www.eclemma.org/jacoco/&lt;/li&gt;
&lt;li&gt;Gradle JacocoPluginExtension: https://docs.gradle.org/current/dsl/org.gradle.testing.jacoco.plugins.JacocoPluginExtension.html&lt;/li&gt;
&lt;li&gt;build.gradle.kts: https://github.com/th-deng/jacoco-on-gradle-sample/blob/master/build.gradle.kts&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>build.gradle</category>
      <category>build.gradle.kts</category>
      <category>jacoco</category>
      <category>Spring</category>
      <category>Spring Boot</category>
      <category>test coverage</category>
      <category>커버리지</category>
      <category>테스트</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/215</guid>
      <comments>https://myvelop.tistory.com/215#entry215comment</comments>
      <pubDate>Thu, 3 Aug 2023 23:10:48 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot 환경 QueryDSL 설정 (build.gradle &amp;amp; .kts)</title>
      <link>https://myvelop.tistory.com/213</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;QueryDSL&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA에서 제공하는 객체지향쿼리인 JPQL(Java Persistence Query Language)을 통해 동적 쿼리를 구성하면 코드가 굉장히 난잡해진다는 것을 느낄 수 있다.&lt;/li&gt;
&lt;li&gt;JPQL은 문자열을 사용한다. 문자열을 조건에 따라 이어붙이는 형식으로 구성하기 때문에 생기는 문제가 있다. 문자열이기에&lt;b&gt; 오타가 발생해도 컴파일 단계에서 에러를 잡아주지 못한다.&lt;/b&gt;(다만, NamedQuery를 사용하면 가능하다) 또한 동적쿼리를 구성할 때, 중간중간 if문에 의해 문자열이 추가되기 때문에 &lt;b&gt;가독성이 떨어진다.&lt;/b&gt; 따라서 쿼리를 체계적으로 관리하기 어렵다.&lt;/li&gt;
&lt;li&gt;QueryDSL은 위와 같은 문제를 해결하기 위해 만들어졌다. Type-Safe한 쿼리를 사용하기 위해 엔티티와 매핑되는 정적 타입 QClass를 생성해 쿼리를 생성할 수 있게 만들었다. 컴파일 단계에서 오류를 잡아낼 수 있고 메소드 체이닝을 통해 조건을 보다 쉽게 추가할 수 있다. 즉, 동적 쿼리를 작성할 때 용이해진다는 말이다.&lt;/li&gt;
&lt;li&gt;QueryDSL도 내부적으로는 JPQL를 구성해 쿼리를 만들어낸다. 다만 사용자들이 편하게 사용할 수 있게 추상화해놓은 JPQL 빌더라고 생각하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Gradle 설정&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 3.x 버전 기준&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QueryDSL Implementation&lt;/b&gt; 부분과 &lt;b&gt;QueryDSL Build Options&lt;/b&gt; 부분을 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;build.gradle (groovy)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1690610296940&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.2'
    id 'io.spring.dependency-management' version '1.1.2'
}

group = 'com.pythonstrup'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // QueryDSL Implementation
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor &quot;com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta&quot;
    annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot;
    annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot;
}

test {
    useJUnitPlatform()
}

/**
 * QueryDSL Build Options
 */
def querydslDir = &quot;src/main/generated&quot;

sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean.doLast {
    file(querydslDir).deleteDir()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;build.gradle.kts (kotlin)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코틀린의 경우, QeuryDSL Version Setting 주석 부분도 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1690614434244&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    java
    id(&quot;org.springframework.boot&quot;) version &quot;3.1.2&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.2&quot;
    id(&quot;org.asciidoctor.jvm.convert&quot;) version &quot;3.3.2&quot;
}

group = &quot;com.pythonstrup&quot;
version = &quot;0.0.1-SNAPSHOT&quot;
val queryDslVersion = &quot;5.0.0&quot; // QueryDSL Version Setting

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

repositories {
    mavenCentral()
}

extra[&quot;snippetsDir&quot;] = file(&quot;build/generated-snippets&quot;)

dependencies {
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-jpa&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)
    compileOnly(&quot;org.projectlombok:lombok&quot;)
    runtimeOnly(&quot;com.mysql:mysql-connector-j&quot;)
    annotationProcessor(&quot;org.projectlombok:lombok&quot;)
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)

    // QueryDSL Implementation
    implementation (&quot;com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta&quot;)
    annotationProcessor(&quot;com.querydsl:querydsl-apt:${queryDslVersion}:jakarta&quot;)
    annotationProcessor(&quot;jakarta.annotation:jakarta.annotation-api&quot;)
    annotationProcessor(&quot;jakarta.persistence:jakarta.persistence-api&quot;)
}

tasks.withType&amp;lt;Test&amp;gt; {
    useJUnitPlatform()
}

/**
 * QueryDSL Build Options
 */
val querydslDir = &quot;src/main/generated&quot;

sourceSets {
    getByName(&quot;main&quot;).java.srcDirs(querydslDir)
}

tasks.withType&amp;lt;JavaCompile&amp;gt; {
    options.generatedSourceOutputDirectory = file(querydslDir) 

    // 위의 설정이 안되면 아래 설정 사용
    // options.generatedSourceOutputDirectory.set(file(querydslDir))
}

tasks.named(&quot;clean&quot;) {
    doLast {
        file(querydslDir).deleteRecursively()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Gradle 동작 원리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;의존성 설정&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Querydsl JPA Support 의존성을 추가해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yx988/btsplpIKwms/h9KeMkmkZwljj1wQ8tVgV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yx988/btsplpIKwms/h9KeMkmkZwljj1wQ8tVgV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yx988/btsplpIKwms/h9KeMkmkZwljj1wQ8tVgV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyx988%2FbtsplpIKwms%2Fh9KeMkmkZwljj1wQ8tVgV0%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; alt=&quot;maven repository querydsl support&quot; loading=&quot;lazy&quot; width=&quot;1092&quot; height=&quot;470&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;470&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;maven repository: &lt;a href=&quot;https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Annotation Processor&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저, Annotation Processor는 컴파일 단계에서 Annotation에 정의된 일렬의 프로세스를 동작하게 하는 것이다. 컴파일 단계에서 실행되기 때문에 빌드 단계에서 에러를 출력하게 하거나 소스코드 및 바이트 코드를 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;b&gt;querydsl-apt&lt;/b&gt;는 소스 코드 레벨에서 Querydsl 관련 어노테이션들을 처리하고 쿼리 타입 클래스들을 생성해주는 역할을 한다. 이를 통해 쿼리를 직접 작성하는 대신 Java 코드를 활용하여 컴파일러가 검사하는 안전한 방법으로 쿼리를 작성할 수 있다. sourceSets에 QClass를 저장할 경로를 설정해주면 해당 경로에 쿼리 타입 클래스를 생성해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;b&gt;annotation-api&lt;/b&gt;를 추가하지 않으면 아래와 같은 에러가 발생한다. 해당 api에는 @Generated, @Deprecated&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;, &lt;/span&gt;@SuppressWarnings&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;, &lt;/span&gt;@Override와 같은 자바 표준 어노테이션이 해당 API에 포함되어 있다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Unable to load class 'jakarta.annotation.Generated'&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;&lt;b&gt;persistence-api&lt;/b&gt;를 추가하지 않으면 아래와 같은 에러가 발생한다. &lt;span style=&quot;color: #374151; text-align: start;&quot;&gt;persistence-api는 @Entity&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;, &lt;/span&gt;@Id&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;, &lt;/span&gt;@GeneratedValue&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;, &lt;/span&gt;@Column 등의 어노테이션을 지원한다.&lt;span style=&quot;background-color: #f7f7f8; color: #374151; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Unable to load class 'jakarta.persistence.Entity'.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;빌드옵션&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;querydslDir&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;querydslDir은 변수다. QClass를 저장하길 원하는 경로를 설정해줬다. 대체로 &quot;src/main/generated&quot; 경로로 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;sourceSets&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Gradle 빌드 스크립트에서 정의한 소스코드와 리소스 디렉토리를 구성하고 관리할 때 사용한다.&lt;/li&gt;
&lt;li&gt;java&amp;nbsp;source&amp;nbsp;set에&amp;nbsp;querydsl&amp;nbsp;QClass&amp;nbsp;위치를 추가했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JavaCompile 명령어&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JavaCompile 작업이 일어날 때, querydsl QClass 파일 생성 위치를 지정할 때 사용한다.&lt;/li&gt;
&lt;li&gt;Groovy =&amp;gt;&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; tasks.withType(JavaCompile) {}&amp;nbsp;&lt;/span&gt; 로 추가&lt;/li&gt;
&lt;li&gt;Kotlin =&amp;gt;&amp;nbsp;&lt;span style=&quot;background-color: #dddddd;&quot;&gt; tasks.withType&amp;lt;JavaCompile&amp;gt; {}&amp;nbsp;&lt;/span&gt; 로 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Clean 명령어&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gradle clean 명령어를 실행할 때, 동작시킬 작업을 추가할 수 있다.&lt;/li&gt;
&lt;li&gt;clean 명령어가 실행되면 QClass Directory 삭제하도록 설정했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실행 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Qclass 생성 및 삭제&lt;/b&gt;&lt;/h3&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;Gradle - Task - other - compileJava를 실행하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;1012&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uUAlY/btspgnx2dQq/81oNW1UBTiN7AIYe2WdKpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uUAlY/btspgnx2dQq/81oNW1UBTiN7AIYe2WdKpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uUAlY/btspgnx2dQq/81oNW1UBTiN7AIYe2WdKpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuUAlY%2Fbtspgnx2dQq%2F81oNW1UBTiN7AIYe2WdKpk%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; alt=&quot;QClass 생성&quot; loading=&quot;lazy&quot; width=&quot;992&quot; height=&quot;1012&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;1012&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;아래와 같이 src/main/generated 경로에 QClass가 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d65p4s/btspfPOGDzS/uKFIeueAD6ULi7O2ZJdw6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d65p4s/btspfPOGDzS/uKFIeueAD6ULi7O2ZJdw6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d65p4s/btspfPOGDzS/uKFIeueAD6ULi7O2ZJdw6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd65p4s%2FbtspfPOGDzS%2FuKFIeueAD6ULi7O2ZJdw6k%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; alt=&quot;QClass 생성&quot; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;282&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;282&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;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;Gradle - Task - build - clean 을 실행하면 된다.&lt;/li&gt;
&lt;li&gt;src/main/generated 디렉토리와 파일이 전부 삭제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AQyMd/btspolE0Rrn/D2rXn5IVHoiQuLsXbrZL51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AQyMd/btspolE0Rrn/D2rXn5IVHoiQuLsXbrZL51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AQyMd/btspolE0Rrn/D2rXn5IVHoiQuLsXbrZL51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAQyMd%2FbtspolE0Rrn%2FD2rXn5IVHoiQuLsXbrZL51%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;570&quot; height=&quot;457&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주의사항&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/main/generated 디렉토리는 개발할 때 편의를 위해 사용하는 것이다. 실제 배포를 위해 build를 명령어를 실행할 때, 코드 실행을 위해 필요한 QClass들이 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;build 디렉토리의 entity와 같은 경로에&lt;/span&gt;&amp;nbsp;전부 포함되기 때문에 Docker와 같은 컨테이너에 generated 디렉토리를 COPY해서 넣을 이유가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OXa4i/btsppIG5mDj/1TTFHx4kuMlKhgX4QqzXX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OXa4i/btsppIG5mDj/1TTFHx4kuMlKhgX4QqzXX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OXa4i/btsppIG5mDj/1TTFHx4kuMlKhgX4QqzXX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOXa4i%2FbtsppIG5mDj%2F1TTFHx4kuMlKhgX4QqzXX0%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; alt=&quot;build 디렉토리에 포함되는 QClass&quot; loading=&quot;lazy&quot; width=&quot;412&quot; height=&quot;362&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;608&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;Git과 같은 버전관리시스템을 사용하고 있다면, generated를 굳이 저장할 필요가 없기 때문에 ignore 파일에 src/main/generated 디렉토리를 추가하도록 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QueryDSL: &lt;a href=&quot;https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@juhyeon1114/Spring-QueryDsl-gradle-%EC%84%A4%EC%A0%95-Spring-boot-3.0-%EC%9D%B4%EC%83%81&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;sourceSets: &lt;a href=&quot;https://kkang-joo.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kkang-joo.tistory.com/3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;AnnotationProcessor와 QueryDSL: &lt;a href=&quot;http://honeymon.io/tech/2020/07/09/gradle-annotation-processor-with-querydsl.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http://honeymon.io/tech/2020/07/09/gradle-annotation-processor-with-querydsl.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>build.gradle</category>
      <category>build.gradle.kts</category>
      <category>kotlin gradle</category>
      <category>kotlin-gradle</category>
      <category>querydsl</category>
      <category>Spring Boot</category>
      <category>스프링부트</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/213</guid>
      <comments>https://myvelop.tistory.com/213#entry213comment</comments>
      <pubDate>Sun, 30 Jul 2023 00:45:54 +0900</pubDate>
    </item>
    <item>
      <title>데이터베이스 인덱스</title>
      <link>https://myvelop.tistory.com/212</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스를 사용해야하는 근본적인 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하는 이유는 무엇일까? 효율적으로 조회하기 위해서 일 것이다. 그럼 왜 풀텍스트 스캔을 하면 시간이 오래 걸리고, 인덱스를 사용하는 것이 더 효율적일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스의 스토리지 엔진은 컴퓨터와 마찬가지로 하드디스크에 정보를 저장하고 읽어온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tdPdO/btsoULlux4c/nrwcKfR2DIZPmnhZOYIMak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tdPdO/btsoULlux4c/nrwcKfR2DIZPmnhZOYIMak/img.png&quot; data-alt=&quot;스토리지 엔진과 하드디스크&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tdPdO/btsoULlux4c/nrwcKfR2DIZPmnhZOYIMak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtdPdO%2FbtsoULlux4c%2FnrwcKfR2DIZPmnhZOYIMak%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; alt=&quot;database storage engine&quot; loading=&quot;lazy&quot; width=&quot;812&quot; height=&quot;603&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;603&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크는 아래 그림과 같이 구성되어 있다. 여기서 섹터는 하드 드라이브의 최소 기억 단위로 트랙의 일부이다. 만약 정보를 읽으라는 명령이 떨어지면 헤드와 암을 열심히 움직여 섹터를 탐색한다. 내용을 읽으려면 암과 헤드가 원하는 track으로 이동해야하고 (빙글빙글 돌고 있는 플래터의) 타이밍이 맞아 헤드와 섹터가 맞닿아야 한다. 이 과정 자체가 굉장히 느리기 때문에 효율적으로 디스크를 조회하려면 최소한의 섹터 범위 안에서 정보를 탐색하는 것이 가장 중요하다고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;928&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SJWEX/btsoZcP0vgn/1Nq6GKKkSKbJEYMdjCeR0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SJWEX/btsoZcP0vgn/1Nq6GKKkSKbJEYMdjCeR0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SJWEX/btsoZcP0vgn/1Nq6GKKkSKbJEYMdjCeR0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSJWEX%2FbtsoZcP0vgn%2F1Nq6GKKkSKbJEYMdjCeR0k%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; alt=&quot;hard disk&quot; loading=&quot;lazy&quot; width=&quot;1332&quot; height=&quot;928&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;928&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;섹터는 디스크의 종류에 따라 512바이트 섹터, 2048바이트 섹터, 4096바이트 섹터 등으로 용량을 고정되어 있다. 오래된 하드디스크의 경우 512바이트 섹터를 사용하는데 데이터베이스에서 해당 스펙을 사용한다고 가정하고 데이터베이스가 테이블의 내용과 인덱스를 저장하는 방식을 설명하도록 하겠다. 아래와 같은 테이블이 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 90.4655%; height: 58px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 6.36201%; height: 18px;&quot;&gt;id(int)&lt;/td&gt;
&lt;td style=&quot;width: 13.5594%; height: 18px;&quot;&gt;name(varchar(8))&lt;/td&gt;
&lt;td style=&quot;width: 13.9705%; height: 18px;&quot;&gt;age(int)&lt;/td&gt;
&lt;td style=&quot;width: 41.8342%; height: 18px;&quot;&gt;intro(varchar(240))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 6.36201%; height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 13.5594%; height: 20px;&quot;&gt;park&lt;/td&gt;
&lt;td style=&quot;width: 13.9705%; height: 20px;&quot;&gt;26&lt;/td&gt;
&lt;td style=&quot;width: 41.8342%; height: 20px;&quot;&gt;안녕하세요~&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 6.36201%; height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 13.5594%; height: 20px;&quot;&gt;kim&lt;/td&gt;
&lt;td style=&quot;width: 13.9705%; height: 20px;&quot;&gt;21&lt;/td&gt;
&lt;td style=&quot;width: 41.8342%; height: 20px;&quot;&gt;반갑습니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id는 4바이트/name은 8바이트/age는 4바이트/intro는 240바이트로 하나의 row당 256바이트를 사용해 저장한다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 약 1,000개 정도 저장되어있다고 가정한다면 1,000(개) * 256(byte) = 256,000 byte이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;256,000 바이트를 섹터에 저장하려면 256,000(byte)/ 512(byte) = 500(섹터)이므로 데이터 1000개를 저장하기 위해 &lt;b&gt;500개의 섹터가 필요&lt;/b&gt;하다. 해당 조건에서 풀스캔을 하면 최악의 경우 500개의 섹터를 모두 조회해야 정보를 찾을 수 있다.&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;p data-ke-size=&quot;size16&quot;&gt;primary key인 id는 자동으로 인덱스가 적용된다. 인덱스는 해당 값을 기준으로 정렬되고 해당 row의 주소값을 가리키는 pointer를 가지고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSON4Q/btsoZnqpJmD/eoV6P3eRFNkZMeKSYOBAD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSON4Q/btsoZnqpJmD/eoV6P3eRFNkZMeKSYOBAD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSON4Q/btsoZnqpJmD/eoV6P3eRFNkZMeKSYOBAD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSON4Q%2FbtsoZnqpJmD%2FeoV6P3eRFNkZMeKSYOBAD1%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; alt=&quot;인덱스와 테이블&quot; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;742&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;64비트 포인터(8 byte)라고 생각해보자. 그러면 id와 pointer를 합쳐서 12 byte가 된다. 총 1,000개의 row를 가지므로 12,000 byte의 값이 섹터에 저장될 것이다. 12,000(byte)/512(byte) = 23.4375(섹터) 이므로 &lt;b&gt;총 24개의 섹터&lt;/b&gt;만으로 1,000개의 데이터를 저장할 수 있다. &lt;b&gt;테이블의 데이터가 저장된 500개의 섹터&lt;/b&gt;와 비교하면 &lt;b&gt;20배의 차이&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 인덱스의 힘이다. id를 통해 조회하면 탐색해야할 범위가 500개의 섹터에서 24개의 섹터로 줄어드는 마법을 경험할 수 있다. 그런데 여기서 &lt;b&gt;B트리&lt;/b&gt;나 &lt;b&gt;해시테이블&lt;/b&gt;과 같은 자료구조를 사용하면 24개의 섹터보다 더 적은 개수의 섹터만 조회해도 정보를 찾을 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;B트리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이진 트리(Binary Tree)의 진화된 버전으로 데이터가 삽입되거나 삭제되어도 지속적으로 균형(Balance)을 유지하는 트리이다. 덕분에 log(O)의 효율성으로 탐색을 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B트리에 대한 영상을 하나 추천하고 싶다. 너무 잘 설명해주셔서 한 번만 봐도 B트리가 무엇인지 바로 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크: &lt;a href=&quot;https://youtu.be/bqkcoSm_rCs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://youtu.be/bqkcoSm_rCs&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=bqkcoSm_rCs&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bvopTQ/hyTqAtjhek/9r5XLrq7cxQOo2Vdtk7IH1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-original-url=&quot;&quot; data-video-title=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/bqkcoSm_rCs&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+트리는 아래 블로그에서 정말 잘 정리해주셨다. 강력 추천!&lt;/p&gt;
&lt;figure id=&quot;og_1690378783144&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[자료구조] 그림으로 알아보는 B+Tree&quot; data-og-description=&quot;정렬된 순서를 보장하고, 멀티레벨 인덱싱을 통한 빠른 검색과 선형탐색까지 가능한 실전형 자료구조 B+ 트리입니다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@emplam27/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-B-Plus-Tree&quot; data-og-url=&quot;https://velog.io/@emplam27/자료구조-그림으로-알아보는-B-Plus-Tree&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/caHiym/hyTqofVGqk/Yh6mOPkNBk2FfIRUpflTb1/img.jpg?width=756&amp;amp;height=394&amp;amp;face=0_0_756_394,https://scrap.kakaocdn.net/dn/jmc0V/hyTrST0FzV/knIk88lakSm4hi1LJojsKk/img.jpg?width=756&amp;amp;height=394&amp;amp;face=0_0_756_394,https://scrap.kakaocdn.net/dn/mi7rQ/hyTqtIj8wM/kiI4Skprk3bHUcKe9OdzBk/img.jpg?width=1125&amp;amp;height=1547&amp;amp;face=0_0_1125_1547&quot;&gt;&lt;a href=&quot;https://velog.io/@emplam27/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-B-Plus-Tree&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@emplam27/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-B-Plus-Tree&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/caHiym/hyTqofVGqk/Yh6mOPkNBk2FfIRUpflTb1/img.jpg?width=756&amp;amp;height=394&amp;amp;face=0_0_756_394,https://scrap.kakaocdn.net/dn/jmc0V/hyTrST0FzV/knIk88lakSm4hi1LJojsKk/img.jpg?width=756&amp;amp;height=394&amp;amp;face=0_0_756_394,https://scrap.kakaocdn.net/dn/mi7rQ/hyTqtIj8wM/kiI4Skprk3bHUcKe9OdzBk/img.jpg?width=1125&amp;amp;height=1547&amp;amp;face=0_0_1125_1547');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[자료구조] 그림으로 알아보는 B+Tree&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;정렬된 순서를 보장하고, 멀티레벨 인덱싱을 통한 빠른 검색과 선형탐색까지 가능한 실전형 자료구조 B+ 트리입니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS/데이터베이스</category>
      <category>MySQL</category>
      <category>인덱스</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/212</guid>
      <comments>https://myvelop.tistory.com/212#entry212comment</comments>
      <pubDate>Wed, 19 Jul 2023 09:25:42 +0900</pubDate>
    </item>
    <item>
      <title>2023 Spring Camp 근데 이제 현업 경험을 곁들인</title>
      <link>https://myvelop.tistory.com/211</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프링 캠프?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3월에 막 입사하여 신입 개발자로 근근이 살아던 중 인프런에 재직 중인 부스트캠프 동기 김00 군에게 연락이 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 링크를 던져주면서 참가할 생각이 있냐고 물어봤다.&lt;/p&gt;
&lt;figure id=&quot;og_1687875309576&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[오프라인] 스프링캠프 2023 - 인프런 | 강의&quot; data-og-description=&quot;애플리케이션 서버 개발자들과 함께 가치있는 기술에 관한 정보과 경험을 '공유'하고, 참가한 사람들과 함께 '인연'을 만들고, 시끌벅적하게 즐길 수 있는 개발자들을 위한 '축제'를 목표로 하는&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/springcamp2023&quot; data-og-url=&quot;https://www.inflearn.com/course/springcamp2023&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bsqxjX/hyS8qR3AMc/oJJAwFHXwG4WH9jjmYQyL1/img.png?width=720&amp;amp;height=406&amp;amp;face=0_0_720_406,https://scrap.kakaocdn.net/dn/beeWhG/hyS8yvMXPM/kJCI8KIuJLCaJkaRcq43v1/img.png?width=720&amp;amp;height=406&amp;amp;face=0_0_720_406&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/springcamp2023&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/springcamp2023&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bsqxjX/hyS8qR3AMc/oJJAwFHXwG4WH9jjmYQyL1/img.png?width=720&amp;amp;height=406&amp;amp;face=0_0_720_406,https://scrap.kakaocdn.net/dn/beeWhG/hyS8yvMXPM/kJCI8KIuJLCaJkaRcq43v1/img.png?width=720&amp;amp;height=406&amp;amp;face=0_0_720_406');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[오프라인] 스프링캠프 2023 - 인프런 | 강의&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버 개발자들과 함께 가치있는 기술에 관한 정보과 경험을 '공유'하고, 참가한 사람들과 함께 '인연'을 만들고, 시끌벅적하게 즐길 수 있는 개발자들을 위한 '축제'를 목표로 하는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;스프링 캠프는 한국 스프링 사용자 모임(KSUG)에서 매년 진행하는 비영리 컨퍼런스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격은 33,000원이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티켓팅은 선착순이었고 소문에 의하면 10초만에 매진되었다고 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;강연&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GhcRT/btslxJR6qwX/VY8KfT1BUPoqwqUxlpLbmK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GhcRT/btslxJR6qwX/VY8KfT1BUPoqwqUxlpLbmK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GhcRT/btslxJR6qwX/VY8KfT1BUPoqwqUxlpLbmK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGhcRT%2FbtslxJR6qwX%2FVY8KfT1BUPoqwqUxlpLbmK%2Fimg.jpg&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; alt=&quot;강연1&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;1058&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&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;어느 #월급쟁이 개발의 스프링 부트 따라잡기 ver.3&lt;/li&gt;
&lt;li&gt;글로벌 서비스를 위한 Timezone/DST&lt;/li&gt;
&lt;li&gt;대규모 엔터프라이즈 시스템 개선 경험기 1부 - 달리는 기차의 바퀴 갈아 끼우기&lt;/li&gt;
&lt;li&gt;대규모 엔터프라이즈 시스템 개선 경험기 2부 - 새 술을 담을 새 부대 마련하기&lt;/li&gt;
&lt;li&gt;실무에서 적용하는 테스트 코드 작성 방법과 노하우&lt;/li&gt;
&lt;li&gt;구현부터 테스트까지 - 대용량 트래픽 처리 시스템&lt;/li&gt;
&lt;li&gt;Journey to Modern Spring (클라우드 시대를 맞이하는 스프링의 자세)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어느 #월급쟁이 개발의 스프링 부트 따라잡기 ver.3&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬리 개발자이신 김지헌 님의 발표로 스프링가 업데이트됨에 따라 프로젝트에서 스프링을 업그레이드 하는 전략에 대해 설명해주셨다. Gradle과 Maven, Yaml과 Properties를 선택 이유에 대한 내용으로 시작해 업그레이드에 대한 다양한 팁과 스프링부트 3.0, 스프링 6.0으로 업데이트되면서 생긴 변화들을 설명해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 기억에 남는 것은 &lt;u&gt;&lt;b&gt;주부수&lt;/b&gt;&lt;/u&gt;이다. 주부수란 버전, {Major}.{Minor}.{Patch}를 의미한다. 예를 들어 스프링부트 3.1.1의 주=3, 부=1, 수=1이 된다. 업데이트 전략은 다음과 같다. Minor 버전 기준으로&amp;nbsp; 최신 패치버전을 적용한다. 그 다음 Minor 버전을 1 올리고, 해당 Minor 기준으로 최신 패치버전으로 변경한다. Minor 버전으로 끝에 도달하면 Major를 바꾸면 되는데 Major를 바꾸는 것은 추천하지 않으셨다. (정신건강에 매우 해롭다고..) 예시는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2.6.2 -&amp;gt; 2.6.15 -&amp;gt; 2.7.13 -&amp;gt; 3.0.x&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 우리 회사에서도 스프링 업데이트에 대한 고민이 많았는데 해당 내용이 큰 도움이 될 것 같다. 서버로 스프링부트 2.2 버전을 사용하고 있는데, 최근 ELK 스택을 최신으로 업데이트하면서 스프링부트 버전으로 인한 제약사항이 많이 생겼다. 심지어 지원중단된 지 한참 지난 버전이기 때문에 애플리케이션 기능이나 확장성 면에서도 불리한 위치이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfhbH3/btslEVcceFh/v53ybHTOtke2MpNuhwFbg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfhbH3/btslEVcceFh/v53ybHTOtke2MpNuhwFbg0/img.png&quot; data-alt=&quot;Spring Support (출처: https://spring.io/projects/spring-boot#support)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfhbH3/btslEVcceFh/v53ybHTOtke2MpNuhwFbg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfhbH3%2FbtslEVcceFh%2Fv53ybHTOtke2MpNuhwFbg0%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; alt=&quot;Spring Support&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;280&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring Support (출처: https://spring.io/projects/spring-boot#support)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 홈페이지에서 스프링에 대한 다양한 소식을 접할 수 있다고 하니 참고하자!&lt;/p&gt;
&lt;figure id=&quot;og_1687876608467&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring | Blog&quot; data-og-description=&quot;On behalf of the team and everyone who has contributed, I am pleased to announce that Spring Security 6.1.1, 6.0.4, 5.8.4, 5.7.9, and 5.6.11 are available now. The releases are mostly composed of bug fixes, dependency upgrades, and documentation improvemen&quot; data-og-host=&quot;spring.io&quot; data-og-source-url=&quot;https://spring.io/blog/&quot; data-og-url=&quot;https://spring.io/blog/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kiKGx/hyS8s3p0Oq/u3asDiEkzYddVJYnbdH7P0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/uXx3l/hyS8pr52E7/XnaQ3evVNA2ickOAhyWsLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://spring.io/blog/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://spring.io/blog/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kiKGx/hyS8s3p0Oq/u3asDiEkzYddVJYnbdH7P0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/uXx3l/hyS8pr52E7/XnaQ3evVNA2ickOAhyWsLK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring | Blog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;On behalf of the team and everyone who has contributed, I am pleased to announce that Spring Security 6.1.1, 6.0.4, 5.8.4, 5.7.9, and 5.6.11 are available now. The releases are mostly composed of bug fixes, dependency upgrades, and documentation improvemen&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글로벌 서비스를 위한 Timezone/DST&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환화 솔루션의 백엔드 개발자이신 김대겸 님이 발표를 담당했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜 및 시간 관련 기본 지식이 주요한 내용이었는데, 이번 기회를 통해 써머타임이 정확히 무엇인지 알게 되었다. 그 외에도 Time과 관련된 Java Class인 java.util.date, java.util.Calendar, Joda-Time 라이브러리, java.time, ZonedDateTime, OffsetDateTime에 대해서 배웠다.&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;h3 data-ke-size=&quot;size23&quot;&gt;대규모 엔터프라이즈 시스템 개선 경험기 1부 - 달리는 기차의 바퀴 갈아 끼우기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 쇼핑카탈로그를 개발하시는 임형태 님의 발표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strangler Pattern이 주요 내용이었다. 아래 사진은&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: left;&quot;&gt;Strangler fig라는 나무에 기생하는 나무이다. 이 나무는 숙주의 양분을 빨아먹으며 살다가 결국 속에 있는 나무를 말려죽이게 되는데 이런 과정을 레거시 애플리케이션에서 새로운 시스템(Fig Application)으로 옮겨가는 프로젝트에 비유해 설명해주셨다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dslnaO/btslKjrKr2K/JivVd1UJKasfHHnGPPah90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dslnaO/btslKjrKr2K/JivVd1UJKasfHHnGPPah90/img.png&quot; data-alt=&quot;기생하는 나무, 결국엔 기생당하는 나무를 말라죽인다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dslnaO/btslKjrKr2K/JivVd1UJKasfHHnGPPah90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdslnaO%2FbtslKjrKr2K%2FJivVd1UJKasfHHnGPPah90%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; alt=&quot;Strangler fig&quot; loading=&quot;lazy&quot; width=&quot;354&quot; height=&quot;531&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;450&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;마이그레이션부터 Pub-Sub 패턴 적용, CRUD 기능 이관, 마지막으로 레거시 시스템이 말라죽을 때까지 반복하는 것까지 쉽지 않아보였다. 작업을 시작하기 위한 설계 단계의 난이도가 굉장히 높을 것이다. 기존 시스템보다 효율적인 구조를 고민함과 더불어 레거시의 변화를 예상하고 그에 대응할 수 있는 유연한 프로젝트를 설계해야할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 시스템을 사용하지 않고 처음부터 다시 만드는 것이 좋을지, 레거시와 공존하는 프로젝트를 만들어 개발해나가는 것이 좋을지 고민해봤다. A to Z를 하면 레거시 서비스에 생기는 변화에 유기적으로 대응하기 쉬울까? 어떤 것이 더 많은 인력과 시간을 필요로 할까? Strangler Pattern을 사용했을 때, 코드에 혁신을 이뤄내는 것이 힘들지 않을까? 오히려 레거시에 잠식되어 이도저도 아닌 코드가 되는 것은 아닐까하는 생각이 들었다. 이것도 프로젝트마다 다를 것 같다. 규모, 서비스 특성 등 여러 조건을 따져봐야할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대규모 엔터프라이즈 시스템 개선 경험기 2부 - 새 술을 담을 새 부대 마련하기&lt;/h3&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;우리 팀이 작성한 코드로 인해 버그가 발생해 확인해보거나 코드를 리뷰하다보면&amp;nbsp;&lt;b&gt;객체의 의존성 문제&lt;/b&gt;, &lt;b&gt;모호한 책임과 역할&lt;/b&gt;로 인해 생긴 버그를 발견하곤 한다. 예를 들어, 팀동료가 작성한 코드로 인해 전체 코드가 영향을 받는 경우가 생겼었다. 동료는 새로운 기능을 만들기 귀찮았는지(?) 병원 프로필을 조회하기 위해 사용하는 API를 다른 서비스 로직에서 사용하고 있었다. 그러다보니 해당 API에 기능을 추가하게 되었는데, 그것이 병원 프로필 기능에 영향을 주는 문제가 발생했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근, 내 실수로 인해 Batch Server에서 5일간 안내 문자 약 1200통이 발송되지 않았던 적이 있었다. 선철 님과 마찬가지로 &lt;b&gt;테스트와 검증 절차의 부족&lt;/b&gt;으로 인해 생긴 문제였다. 당시가 연휴였기 때문에 CS팀이 확인할 수도 없었고, 그렇다고 테스트 코드나 모니터링이 잘 되어 있는 것도 아니라 문제에 대한 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;피드백이 바로 되지 않았다. 결국,&lt;span&gt;&amp;nbsp;연휴가 끝날 때까지 아무도 알아채지 못했었다.&lt;/span&gt;&lt;/span&gt; 나는 해당 문제가 다시는 발생하지 않도록 일정기간동안 문자메시지가 발송되지 않으면 슬랙 알림을 보내는 코드를 작성하고, Batch-Server에 추가로 모니터링 시스템을 구축했다. 또한, 팀에서 최초로 단위테스트와 통합테스트를 도입한 사람이 되었다.&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;h3 data-ke-size=&quot;size23&quot;&gt;실무에서 적용하는 테스트 코드 작성 방법과 노하우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오페이 개발자 김남윤 님이 발표를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;효율적인 Mock Test와 객체의 책임 분리가 테스트 코드에 얼마나 큰 도움이 되는지 설명해주셨다.&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;현재 회사에서 나 혼자만 테스트 코드를 작성하고 있다. 혼자 작성하면 공부도 되고 좋지만 개발팀의 효율을 극대화하기 위해서라면 팀 전체가 테스트 코드를 작성하는 것이 좋다고 생각했다. 회사가 큰 투자를 받은 것도 아니고, 규모가 큰 것도 아니라 새로운 기능 만드는 것만 해도 바쁜데 개발팀 전체가 테스트 코드를 작성하게 하려면 어떻게 해야할 지 고민이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사수와도 논의한 적이 있었는데, 내가 테스트 코드 짜놓은 것을 보니, 난이도가 높아서 다른 팀원들의 진입장벽이 높을 것 같다며 다른 사람들이 테스트를 쉽게 작성할 수 있는 방법을 강구해보라고 조언해주셨다. (예를 들면 쉘 스크립트로 테스트 코드 작성 시작을 자동화) 대체로 동의되는 내용이었지만 몇몇 내용에 대해선 전혀 동의할 수 없었는데, 그 중 하나는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;DB에 값을 미리 넣어놓으면 일일이 값을 넣을 필요가 없어 팀원들이 쉽게 접근할 수 있지 않겠느냐는 주장이었다.&lt;/span&gt; 테스트 코드마다 DB값을 A to Z로 넣고 @Transactional로 롤백하는 코드를 보더니 너무 난잡해서 팀원들이 사용하기 어려울 것이라는 것이 이유였다. 여기에 대해선 동의할 수 없어 다음날 그렇게 해서는 안되는 이유를 정리해가고 대신 Fixture 라이브러리를 사용해 편리하게 데이터를 넣을 수 있는 방법을 제안한 것으로 마무리됐다.&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;h3 data-ke-size=&quot;size23&quot;&gt;구현부터 테스트까지 - 대용량 트래픽 처리 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 Cell TF에서 근무 중이신 이경일 님의 발표였다.&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;s&gt;해보고 싶다.. 대용량 트래픽..&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Journey to Modern Spring (클라우드 시대를 맞이하는 스프링의 자세)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;박용권님의 발표였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드와 스프링에 초점을 맞춰 웹개발의 역사를 훑어주셨다. 2000~2010년에는 클라우드 시스템도 대중화되지 않고 스프링 부트도 없었던 세상이라 사람들이 어떻게 개발을 했을까 싶었다. 직접 서버 컴퓨터 구매해서, 톰캣 서버에 war 파일을 넣어 배포했을텐데 쉽지 않았을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2010년 이후부터 뭔가 본격적인 변화가 시작됐던 것 같다. 2011년에 마이크로서비스라는 키워드가 처음으로 등장했고, 2012년에는 &lt;s&gt;현재는 유료서비스만 하는&lt;/s&gt; 헤로쿠에서 클라우드 12 원칙을 발표했다. AWS Lambda(serverless), Kubernetes 공개되었고 그 외에도 많은 발전이 진행되어, 이제는 클라우드 시스템을 사용하지 않고 배포하는 사람을 신기하게 취급하는 시대가 되어버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브나 강의를 듣다보면 자바를 소위 &quot;틀딱&quot; 언어라고 표현하는 사람들이 있다. &lt;s&gt;2018년까지만 해도 자바는 컨테이너 환경을 잘 이해하지 못했다고 한다.&lt;/s&gt; 하지만 자바와 스프링이 새로운 시대를 맞아 끊임없이 변화해왔다. 스프링은 프레임워크의 사용성을 개선하기 위해 스프링 부트를 개발했고, 약점이라고 평가받던 비동기에 특화된 Spring Webflux도 공개했다. 자바 더 좋은 JVM을 만들기 위해 여러 시도들을 해왔고, 2019년에는 GraalVM을 통해 성능을 10% 이상 개선했다고 한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소소한 재미&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링캠프는 단순 강연만 진행되지 않고 부스도 운영되었는데, 여러 이벤트와 취업 상담 등 소소하게 즐길 거리들이 많은 행사였다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;부스 운영&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 캠프에는 회사 부스가 배치되어있었는데, 인프런과 데보션에서 경품 추천 이벤트를 준비해주셨다. 각 추첨당 200대 10~20의 경쟁률을 자랑했는데 정말 운이 좋게도 두곳에서 모두 경품을 선물을 받았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXAAEM/btslEW9ZKWG/g84piPNCuoN4wasKq0Rzv1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXAAEM/btslEW9ZKWG/g84piPNCuoN4wasKq0Rzv1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXAAEM/btslEW9ZKWG/g84piPNCuoN4wasKq0Rzv1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXAAEM%2FbtslEW9ZKWG%2Fg84piPNCuoN4wasKq0Rzv1%2Fimg.jpg&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; alt=&quot;인프런 슬리퍼 선물&quot; loading=&quot;lazy&quot; width=&quot;495&quot; height=&quot;1058&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&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;인프런에서는 슬리퍼를 선물받았고, 데보션에서는 보조배터리를 선물받았다! &lt;s&gt;안그래도 사려고 했었는데&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 기본선물인 손풍기, 스티커와 인프런 설문조사만 진행해도 받을 수 있는 인프런 30% 할인권을 챙겼다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lcLfC/btslFkiAm6A/quOVI9wPiPcWlRi8jRQr70/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lcLfC/btslFkiAm6A/quOVI9wPiPcWlRi8jRQr70/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lcLfC/btslFkiAm6A/quOVI9wPiPcWlRi8jRQr70/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlcLfC%2FbtslFkiAm6A%2FquOVI9wPiPcWlRi8jRQr70%2Fimg.jpg&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; alt=&quot;스프링캠프 아이템&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;1058&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&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;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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbdvn6/btslwpTQZAc/LKluQ26jf2ZK64Oy8x1ajk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbdvn6/btslwpTQZAc/LKluQ26jf2ZK64Oy8x1ajk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbdvn6/btslwpTQZAc/LKluQ26jf2ZK64Oy8x1ajk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbbdvn6%2FbtslwpTQZAc%2FLKluQ26jf2ZK64Oy8x1ajk%2Fimg.jpg&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; alt=&quot;간식&quot; loading=&quot;lazy&quot; width=&quot;485&quot; height=&quot;364&quot; data-origin-width=&quot;1411&quot; data-origin-height=&quot;1058&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;</description>
      <category>대외활동/컨퍼런스</category>
      <category>java</category>
      <category>Spring</category>
      <category>Spring Camp</category>
      <category>개발자</category>
      <category>스프링 캠프</category>
      <category>컨퍼런스</category>
      <author>gakko</author>
      <guid isPermaLink="true">https://myvelop.tistory.com/211</guid>
      <comments>https://myvelop.tistory.com/211#entry211comment</comments>
      <pubDate>Fri, 30 Jun 2023 01:04:11 +0900</pubDate>
    </item>
  </channel>
</rss>