<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>성장하는 개발 블로그</title>
    <link>https://mirrorofcode.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 8 Apr 2026 19:29:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>후;</managingEditor>
    <item>
      <title>Skip lock을 아시나요? (feat: PostgreSQL)</title>
      <link>https://mirrorofcode.tistory.com/433</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Context&lt;/b&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;어떻게 하면 여러 인스턴스에서 데이터를 race condition을 방지하면서 데이터를 읽어서 처리할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-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;pre id=&quot;code_1748528984330&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;type A struct {
  publisher EventPublisher
}

func (a *A) DoSomething(ctx context.Context) {
   // 어떤 비즈니스 로직 수행

   a.publisher.publishEvent(ctx, &amp;amp;Event{}) // event 발행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직에서 DoSomething메서드를 호출했을 때, 이 이벤트를 구독해서 Outbox 테이블에 기록하는 로직이 있다고 가정해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748529083837&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type EventListener struct {
  outboxRepo OutboxRepository
}

func (e *EventListener) OnEvent(ctx context.Context, event Event) {
   e.outboxRepo.Save(ctx, event)
}&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;그럼 이때 Outbox 테이블에는 많은 이벤트 들이 쌓이게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 Outbox의 이벤트를 읽는 poller가 이를 주기적으로 읽어서 데이터를 처리할 것입니다.&lt;/p&gt;
&lt;p data-ke-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;Problem&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 poller 인스턴스가 하나라면 다음과 같이 코드를 작성할 수 있을 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748529828956&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type poller struct {
   repo OutboxRepository
}


func (p *Poller) poll() {
  records, err := repo.Exec(
    &quot;select * from outbox where processed_at is null limit 10&quot;
  )
  
  //error handling and do something
}&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;이렇게 하면 poll을 호출하는 go루틴 혹은 스레드가 주기적으로 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;인스턴스가 2개 이상이라면 같은 row에 대한 처리를 2개의 인스턴스에서 동시에 처리하게 되기 때문에 &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;br /&gt;&lt;br /&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;b&gt;Solution&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 MySQL/PostgreSQL에서는 Skip 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;skip lock은 MySQL 8.0.1 부터 지원했고, psql은 9.5 버전부터 skip 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;skip lock이란 여러 프로세스가 같은 테이블의 같은 행을 경합 없이 안전하게 처리하도록 해주는 lock입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처리 중인 행(락이 걸린 행)은 건너뛰고, 아직 락이 걸리지 않은 행만 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;그럼 위의 skip lock을 적용해서 다시 살펴봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1748537654579&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;type poller struct {
   repo OutboxRepository
}


func (p *Poller) poll() {
  repo.Exec(&quot;BEGIN&quot;) // start transaction
  
  records, err := repo.Exec(
    &quot;select * from outbox where processed_at is null order by id limit 10 for update skip locked&quot;
  )
  
  
  //error handling and do something
  
  // update records processed_at = now()
  repo.Exec(&quot;COMMIT&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;먼저 트랜잭션을 시작하고, skip lock으로 lock을 걸어 첫 번째 인스턴스에서 10개의 레코드를 가져온다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 인스턴스에서는 lock이 걸리지 않은 남은 최대 10개의 레코드에 대해서 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;그리고 processed_at을 현재 시각으로 업데이트하면 완성됩니다.&lt;/p&gt;
&lt;p data-ke-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;b&gt;Addition&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SKIP LOCK을 사용하면 서로 다른 인스턴스에서 다른 레코드에 대해 접근을 할 수 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 상황에서 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748538238272&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 1) 트랜잭션 시작
BEGIN;

-- 2) 아직 처리되지 않고, 다른 세션이 잡지 않은 행만 최대 10개 가져오기
SELECT *
  FROM outbox
 WHERE processed is NULL
 ORDER BY id
 LIMIT 10
 FOR UPDATE SKIP LOCKED;

-- 3) 애플리케이션에서 결과를 꺼내 처리(processing)&amp;hellip;  (ERROR)

-- 4) 처리 완료 표시
UPDATE outbox
   SET processed   = TRUE,
       processed_at = NOW()
 WHERE id IN (&amp;hellip;가져온 id 목록&amp;hellip;);

-- 5) 트랜잭션 커밋
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;&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;그러다 select 를 한 시점에서 skip 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;만약 이 시점에 애플리케이션에 장애가 발생해서 쿼리가 롤백되게 된다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같은 쿼리를 실행하고 있는 다른 인스턴스는 lock이 걸린 데이터를 건너뛰고 데이터를 처리하게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;그래서 순서가 중요한 경우에는 skip 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;</description>
      <category>개발 지식</category>
      <category>mysql</category>
      <category>posgresql</category>
      <category>psql</category>
      <category>skip lock</category>
      <category>skip locked</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/433</guid>
      <comments>https://mirrorofcode.tistory.com/433#entry433comment</comments>
      <pubDate>Fri, 30 May 2025 02:10:51 +0900</pubDate>
    </item>
    <item>
      <title>(kafka) debezium을 사용한 exactly once producing</title>
      <link>https://mirrorofcode.tistory.com/432</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Abstract&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;마이크로서비스 아키텍처(MSA) 환경에서 서비스 간 메시지 전송의 정합성을 확보하는 일은 매우 중요한 과제입니다. &lt;/span&gt;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;일반적인 Kafka acks 설정만으로는 메시지 중복 전송 방지나 전송과 오프셋 커밋의 원자성을 동시에 만족시키기 어렵습니다. &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;이 글에서는 enable.idempotence와 transactional.id 기능을 활용해 브로커 단계에서 exactly‑once producing을 완전하게 확보하는 방법을 설명합니다. 아울러 Debezium을 통한 DB CDC(Change Data Capture)와 Kafka Connect/Connector 구조를 결합하여, 외부 데이터베이스 트랜잭션과 Kafka 메시지 전송 간의 일관성을 유지하면서도 높은 확장성과 운영 편의성을 제공하는 방안을 살펴봅니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;1. 서론&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;MSA 환경에서는 각기 다른 서비스 간에 전달되는 메시지가 한 번만 정확히 처리되어야만 전체 시스템의 정합성을 유지할 수 경우가 많습니다. 하지만&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;메시지를 수신한 쪽에서 부수효과가 발생하는 경우에는 트랜잭션 롤백이 불가능해 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;exactly‑once processing&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;을 보장한다는 것은 현실적으로 거의 달성하기 어려운 문제입니다. 반면,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;exactly‑once producing&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;은 브로커가 제공하는 멱등성(idempotence)과 트랜잭션(transaction) 기능을 적절히 결합하면 완전하게 확보할 수 있습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;이 글에서는 먼저 Kafka 프로듀서-브로커 간의 기본 acks 설정이 지닌 내구성과 가용성 사이의 트레이드오프를 살펴보고, 각각의 한계를 짚어봅니다. 이어서 enable.idempotence=true를 통해 파티션 경계 내 중복 전송을 제거하고, transactional.id=&amp;lt;고유ID&amp;gt;를 통해 메시지 전송과 오프셋 커밋을 하나의 트랜잭션으로 묶어 처리하는 과정을 상세히 기술합니다. 마지막으로 Debezium 기반 CDC와 Kafka Connect/Connector 조합을 통해 외부 RDBMS 트랜잭션과의 일관성을 유지하면서도 무중단 수평 확장과 운영 편의성을 확보하는 구조를 소개합니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;2. 기본 Producer·Broker 설정의 한계&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;Apache&amp;nbsp;Kafka는 acks 설정(acks=0, acks=1, acks=all)으로 프로듀서가 브로커로부터 얼마나 많은 확인을 기다릴지를 제어합니다.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; 하지만 이들 옵션만으로는 메시지를 반드시 한 번만 기록하고, 전송과 오프셋 커밋을 원자적으로 묶는 동작을 모두 보장할 수 없습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;acks=0 모드&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;는 브로커로부터 ACK를 전혀 기다리지 않고 즉시 성공으로 간주하므로, 초저지연을 얻을 수 있으나 네트워크 분리나 브로커 장애 시 메시지가 한 번도 기록되지 않을 수 있습니다. &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;acks=1 모드&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;는 리더 파티션에 메시지가 기록된 후에만 ACK를 반환하므로 리더 도달을 보장하지만, 팔로워 복제 지연이나 리더 장애로 인해 ISR(In‑Sync Replicas)에 복제되지 않은 메시지가 유실될 위험이 남습니다. &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;acks=all에 min.insync.replicas ≥ 2를 결합&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;하면 복제본이 반드시 충분히 살아 있어야만 쓰기를 허용해 데이터 유실 가능성을 사실상 제거하지만, 복제본 하나만 빠져도 서비스 전체의 쓰기가 중단될 수 있어 가용성이 크게 저하될 수 있습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;또한 프로듀서-브로커 단계의 내구성이 아무리 높아도, 컨슈머가 메시지를 처리한 뒤 오프셋을 커밋하는 구조에서는 처리 도중 장애가 발생하면 동일 메시지를 재처리하거나 아예 처리되지 않은 채 넘어가는 상황이 남아 있습니다. 이처럼 기본 acks 설정만으로는 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;중복 전송 방지&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;와 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;전송·커밋 원자성&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;을 동시에 달성하지 못합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;3. Kafka의 Exactly‑Once Producing 메커니즘&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;Kafka 브로커 단계에서 exactly‑once producing을 달성하기 위해서는 멱등성(idempotence)과 트랜잭션(transaction) 기능을 결합해야 합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;첫째, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Idempotent Producer&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;는 enable.idempotence=true 설정을 통해 활성화됩니다. 프로듀서 인스턴스마다 부여된 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Producer&amp;nbsp;ID&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;와 각 파티션 메시지에 할당된 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Sequence Number&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;를 조합하여, 브로커는 (Producer ID, Partition, Sequence Number)가 이미 처리된 메시지를 중복으로 식별하고 폐기(drop)합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;이로써 파티션 경계 내에서 네트워크 재시도나 클라이언트 중복 호출로 인한 중복 전송을 전면 차단할 수 있습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;둘째, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Transactional Producer&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;는 여기에 transactional.id=&amp;lt;고유ID&amp;gt;를 추가하여 구현됩니다. 트랜잭션 프로듀서는 다음과 같은 순서로 동작합니다.&lt;/span&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;initTransactions() 호출을 통해 과거 트랜잭션 상태를 복원하고,&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;beginTransaction()으로 트랜잭션을 시작한 뒤,&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;send(...) 메서드로 여러 파티션·토픽에 메시지를 전송하고,&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;sendOffsetsToTransaction(...)를 통해 컨슈머 오프셋을 동일 트랜잭션에 포함시킨 다음,&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;commitTransaction() 또는 장애 시 abortTransaction()을 호출합니다.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;이 과정에서 Transaction Coordinator는 __transaction_state 내부 토픽에 트랜잭션 상태를 내구성 있게 기록하며, 네트워크 타임아웃이나 GC pause 같은 장애가 발생해도 정확히 복원하여 중복 커밋을 방지합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;특징&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;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;파티션 단위 네트워크 중복 방지&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;메시지 전송과 오프셋 커밋의 원자적 묶음&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;장애 복구 시 이전 트랜잭션 상태 안전 재개&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;이 두 기능을 결합하면 Kafka 브로커 단계에서 중복 없이 단 한 번만 메시지를 기록하고, 컨슈머 재시작 시에도 전송과 오프셋 커밋이 함께 보장되는 exactly‑once producing이 완전하게 확보됩니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;4. Debezium&amp;nbsp;+&amp;nbsp;Kafka Connect/Connector를 통한 확장&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;외부 RDBMS의 트랜잭션 커밋 시점과 Kafka 메시지 전송 시점을 일치시키려면, Debezium 기반 CDC와 Kafka&amp;nbsp;Connect/Connector 구조가 유용합니다. &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;Debezium은 데이터베이스의 &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Write‑Ahead Log(WAL)&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; 를 직접 추출(tailing)하여, 먼저 전체 테이블을 스냅샷으로 덤프한 뒤 이후 변경 로그를 연속 전송합니다. &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;이때 Kafka Connect는 Debezium Connector를 통해 poll() 메서드로 변경 로그를 읽어 enable.idempotence와 transactional.id가 활성화된 Producer로 exactly‑once producing을 수행하며, &lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;WAL LSN(Log Sequence Number)&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt; 을 Kafka Connect offset에 기록합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;장애가 발생해도 커밋되지 않은 LSN(Log Sequence Number)부터 재시작하기 때문에 메시지가 중복 전송되지 않습니다. Kafka Connect 플랫폼은 Distributed Mode로 여러 Worker에 Task를 자동 분배·재할당하므로, 무중단 수평 확장과 고가용성을 확보할 수 있습니다. Connector 설정과 offset이 Kafka 토픽에 보관되므로 Worker를 교체하거나 재배포해도 별도 상태 이관 없이 즉시 복구됩니다.&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;5. 결론&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;MSA 환경에서 exactly‑once producing을 달성하려면, Kafka 브로커의 기본 acks 설정만으로는 부족하며, 멱등성(idempotence)과 트랜잭션(transaction) 기능을 결합해야 합니다. enable.idempotence=true로 중복 전송을 차단하고, transactional.id=&amp;lt;고유ID&amp;gt;로 메시지 전송과 오프셋 커밋을 원자적으로 묶으면 브로커 단계에서 정확히 한 번만 메시지를 기록할 수 있습니다. &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, Malgun Gothic, 맑은 고딕, dotum, 돋움, sans-serif;&quot;&gt;더 나아가 Debezium CDC와 Kafka Connect/Connector 구조를 적용하면 외부 데이터베이스 트랜잭션과 Kafka 메시지 전송 간의 일관성을 유지하면서도 높은 확장성과 운영 편의성을 누릴 수 있습니다. 이와 같은 구조는 MSA 기반 서비스에서 메시지 생산의 중복·유실 문제를 근본적으로 해소하고, 서비스 안정성 및 사용자 경험을 크게 향상시킵니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>개발 지식</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/432</guid>
      <comments>https://mirrorofcode.tistory.com/432#entry432comment</comments>
      <pubDate>Mon, 21 Apr 2025 22:37:04 +0900</pubDate>
    </item>
    <item>
      <title>Go 애플리케이션에서 테스트 커버리지 시각화하기</title>
      <link>https://mirrorofcode.tistory.com/431</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Context&lt;/b&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;&amp;nbsp;&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;커버리지는 단순히 &quot;작성된 코드 중 테스트가 실제로 실행한 부분&quot;으로, 예외 상황이나 경계 조건까지 테스트를 하지 않으면 수치가 높아도 의미가 없습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼에도 불구하고 커버리지가 중요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버리지는 BMI에 비유한다면&lt;br /&gt;&amp;nbsp;&lt;br /&gt;BMI가 건강의 전부는 아니지만, 기초 지표로는 유용한 것처럼, 커버리지도 그런 신호등 역할을 해줍니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;그럼 어떻게 커버리지를 활용해야할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버리지에서 실제로 확인해야하는 것은 &quot;실제로 중요한 로직이 테스트 되었는가&quot;입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;func Grade(score int) string {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if score &amp;gt;= 90 {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;A&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if score &amp;gt;= 80 {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;B&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return &quot;C&quot;
&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;pre class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;Expect(Grade(95)).To(Equal(&quot;A&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;이렇게 한 줄만 테스트 해도 커버리지 수치는 100%가 나올 수 있습니다.&lt;br /&gt;하지만 경계값과 다른 분기는 테스트 되지 않았으니 의미있는 커버리지가 아니죠.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그래서 이런 경계값이나 분기가 잘 테스트 되었는지를 확인하기 위해서는 &lt;b&gt;시각화도구&lt;/b&gt;가 도움이 됩니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;커버리지는 단순 수치보단&lt;br /&gt;&lt;b&gt;&quot;중요한 분기와 경계값을 제대로 테스트했는가?&quot;를 눈으로 직접 확인하는 것&lt;/b&gt;이 핵심이고,&lt;br /&gt;그걸 도와주는 게 바로 &lt;b&gt;시각화 도구&lt;/b&gt;입니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Go에서의 테스트 시각화 도구&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go에서 테스트를 실행할 때 다음과 같은 명령어로 실행할 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;go test ${fileName}&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;그리고 여기에서 `-cover` 옵션을 주게되면 coverage를 함께 출력해주죠.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;go test ${fileName} -cover&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;하지만 이는 단순히 라인 커버리지만을 출력하기 때문에 원하는 옵션이 아닙니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Go에는 다른 옵션인 &lt;b&gt;`-coverprofile`&lt;/b&gt;옵션이 있습니다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;go test -coverprofile=cover.out ./...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&lt;b&gt;`-coverprofile`&lt;/b&gt;옵션을 통해 측정된 커버리지 정보를 cover.out 파일에 저장할 수 있습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그리고 Go은 Go tool에서 이를 html로의 시각화를 지원합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;go tool cover -html=cover.out&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;그러면 이렇게 어디가 테스트 되었는지 알 수 있는 html 파일이 완성되게 됩니다.&lt;br /&gt;&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;1334&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dXJsog/btsNlYsLsHb/jbP0Bq3Y96IqeKFQkkUXu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dXJsog/btsNlYsLsHb/jbP0Bq3Y96IqeKFQkkUXu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dXJsog/btsNlYsLsHb/jbP0Bq3Y96IqeKFQkkUXu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdXJsog%2FbtsNlYsLsHb%2FjbP0Bq3Y96IqeKFQkkUXu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1334&quot; height=&quot;644&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;644&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;pre class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;package main

import &quot;testing&quot;

func TestGrade(t *testing.T) {
	tests := []struct {
		score&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;int
		expected string
	}{
		{95, &quot;A&quot;},
		{85, &quot;B&quot;},
		{75, &quot;C&quot;},
		{65, &quot;C&quot;},
		{55, &quot;C&quot;},
	}

	for _, test := range tests {
		result := Grade(test.score)
		if result != test.expected {
			t.Errorf(&quot;Grade(%d) = %s; expected %s&quot;, test.score, result, test.expected)
		}
	}
}&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;Conclusion&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문서에서 Go에서 테스트 커버리지를 시각화하는 것을 알아보았습니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;위 기능을 조금 더 발전시킨다면 codecov처럼 &lt;b&gt;&quot;현재 브랜치에서 변경된 파일들에 대한 커버리지&quot;&lt;/b&gt;도 측정이 가능합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;다만 아쉬운 점은 `go test`가 Java의 JaCoCo처럼 라인 커버리지, 브랜치 커버리지 등 다양한 커버리지를 측정할 수 없다는 부분입니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그렇기에 더더욱 시각화를 해서 경계값에 대한 테스트가 필요합니다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 지식</category>
      <category>coverprofile</category>
      <category>go cover</category>
      <category>go test</category>
      <category>html 시각화</category>
      <category>테스트 시각화</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/431</guid>
      <comments>https://mirrorofcode.tistory.com/431#entry431comment</comments>
      <pubDate>Tue, 15 Apr 2025 03:50:31 +0900</pubDate>
    </item>
    <item>
      <title>멱등성 있는 이벤트 발행하기</title>
      <link>https://mirrorofcode.tistory.com/430</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;EDA에 많은 관심을 가지고 있어서 이벤트에 대해 많은 공부를 하게 되었고,&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;b&gt;# 이벤트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EDA에서 이벤트는 단순한 기록으로 처리되는 것이 아니라 시스템 구성요소 간의 동기/비동기 통신 및 비즈니스 로직 실행의 핵심 메시지로 취급합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 layered architecture를 생각해 보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 엔티티의 변경사항이 발생하고, 이 이벤트를 A와 관련된 엔티티에 전파해야 한다면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A service &amp;lt;- B Service, C Service, D Service... 등과 같이 하나의 layer에 다른 도메인의 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;이런 패턴은, 응집도를 떨어뜨리고, 결합을 강하게 만들어 좋은 패턴이라고 볼 수는 없죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740919194542&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class Aservice {

    private final ARepository aRepository;
    private final BService bService;
    private final CService cService;
    private final DService dService;
    
    public void foo() {
    	A a = new A();
        aRepository.delete(a);
        
        bService.someMethod(a);
        cService.someMethod(a);
        dService.someMethod(a);
    }
 }&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;p data-ke-size=&quot;size16&quot;&gt;1. A 엔티티의 변경사항 이벤트를 publish 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. A 엔티티의 변경사항 이벤트를 구독하고 있던 subscriber/listener가 이를 인지하고 처리합니다.&lt;/p&gt;
&lt;p data-ke-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;A Service는 publisher에 대한 참조만 가지고 있게 되므로 &lt;b&gt;결합도가 줄어들게 되고, 응집도도 높아지게&lt;/b&gt; 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740919318445&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class AService {
    private final ARepository aRepository;
    private final EventPublisher publisher;

    public void foo() {
       A a = new A();
       aRepository.delete(a);

       ADeleteEvent event = new ADeleteEvent();
       publisher.publish(event);
    }
}&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;b&gt;## 비즈니스적 요구사항 (트랜잭션 aware, async)&lt;/b&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;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;- A라는 엔티티는 B라는 엔티티에 contraint가 걸려있습니다.&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;&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;또한 DB접근이기 때문에 RDB의 경우 트랜잭션에서 엔티티 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;그래서 이런 비즈니스 요구사항이 있을 때는 동기/트랜잭션 이벤트 발행이 필요합니다.&lt;/p&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에서는 이를 직접적으로 제공하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @TransactionalEventListener&lt;/p&gt;
&lt;figure id=&quot;og_1740919631044&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;TransactionalEventListener (Spring Framework 6.2.3 API)&quot; data-og-description=&quot;The event classes that this listener handles. If this attribute is specified with a single value, the annotated method may optionally accept a single parameter. However, if this attribute is specified with multiple values, the annotated method must not dec&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html&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;TransactionalEventListener (Spring Framework 6.2.3 API)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The event classes that this listener handles. If this attribute is specified with a single value, the annotated method may optionally accept a single parameter. However, if this attribute is specified with multiple values, the annotated method must not dec&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;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-filename=&quot;스크린샷 2025-03-02 오후 9.47.51.png&quot; data-origin-width=&quot;2238&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CbQac/btsMzOyRrO2/6jNlRyWm3mTJCANqMDwRSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CbQac/btsMzOyRrO2/6jNlRyWm3mTJCANqMDwRSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CbQac/btsMzOyRrO2/6jNlRyWm3mTJCANqMDwRSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCbQac%2FbtsMzOyRrO2%2F6jNlRyWm3mTJCANqMDwRSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;2238&quot; height=&quot;572&quot; data-filename=&quot;스크린샷 2025-03-02 오후 9.47.51.png&quot; data-origin-width=&quot;2238&quot; data-origin-height=&quot;572&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;- Classes는 어떤 클래스에 대해서 이벤트 처리를 할 것인지 설정할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- phase 값을 통해 어떤 트랜잭션 페이즈에서 동작할지를 설정할 수 있습니다. (BeforeCommit, AfterCommit, AfterRollback, AfterCompletion)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 실행 시에는 @Async 어노테이션만 붙이면 사용가능합니다.(비동기 설정이 되어 있다는 가정하에)&lt;/p&gt;
&lt;p data-ke-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;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;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;# 외부로 이벤트 전송하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA환경에서의 이벤트 기반 통신은 매우 흔한 일입니다.&lt;/p&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가 삭제되었을 때 User와 관련된 다른 리소스를 지우는 이벤트를 외부로 발행하는 상황을 가정해 봅시다.&lt;/p&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;1. client의 User 삭제 요청이 들어옴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 내부 리소스를 삭제(User를 삭제)함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 외부로 UserDeletedEvent를 보냄.&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;&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;1. client의 User 삭제 요청이 들어옴.&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. 외부로 UserDeletedEvent를 보냄.&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;5. 서버 측에서 재시도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 이벤트를 2번 수신.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. ??&lt;/p&gt;
&lt;p data-ke-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;이렇게 네트워크 분단 문제뿐만 아니라 외부에서 응답이 처리가 늦어졌을 경우 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;어떻게 이를 극복할 수 있을까요?&lt;/p&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;b&gt;# 멱등성&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-03-03 오전 12.40.52.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2z8ze/btsMy3QG2ps/cSFNV2MzAxKj9tkdHpNaO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2z8ze/btsMy3QG2ps/cSFNV2MzAxKj9tkdHpNaO0/img.png&quot; data-alt=&quot;by gemini&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2z8ze/btsMy3QG2ps/cSFNV2MzAxKj9tkdHpNaO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2z8ze%2FbtsMy3QG2ps%2FcSFNV2MzAxKj9tkdHpNaO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1308&quot; height=&quot;190&quot; data-filename=&quot;스크린샷 2025-03-03 오전 12.40.52.png&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;by gemini&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;멱등성&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;p data-ke-size=&quot;size16&quot;&gt;유저 삭제를 한다고 합시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 요청에서는 유저를 실제로 삭제하고 200을 응답했습니다.&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;/p&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;유저의 잔고에 1000원을 더하는 작업이라면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 요청에서는 r(기존 값) + 1000이 될 것이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 요청에서는 r + 2000,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 요청에서는 r + 3000이 될 것입니다.&lt;/p&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;1. 이벤트를 받았을 때 r + 1000이 되었습니다.&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. 클라이언트가 재시도해서 다시 이벤트를 수신하게 되었고, r + 2000이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 원하는 것은 r + 1000인데, r + 2000이 되었습니다.&lt;/p&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;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;## 멱등적인 이벤트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;X-Request-Event-Id&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;만약 처리된 메시지라면 에러를 발생시키고,&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;간략하게 sudo code를 작성해 보자면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741770656328&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func handle(messageKey string, event Event) error {
	_, ok := store.get(messageKey)
    if ok {
    	return errors.new(&quot;The event was already procesed&quot;)
    }
    _  = store.put(messageKey, event)
	return processEvent(event)
}&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;여기서 event 저장소는 redis 같은 캐시를 사용할 수도 있고, RDBMS나 NoSQL을 사용할 수도 있고, 그 외의 다른 방법으로 사용할 수도 있습니다.&lt;/p&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 같은 저장소에 timeout을 함께 명시해서 저장하는 방법이 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 경우 redis의 데이터가 재부팅 등으로 인해 휘발된 경우 문제가 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(비동기 작업으로 FileSystem에 추가적으로 저장하는 패턴을 사용해 휘발되는 상황을 막을 수 있습니다.)&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;# wrap-up&lt;/b&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 지식</category>
      <category>@EventListener</category>
      <category>이벤트</category>
      <category>이벤트 멱등성</category>
      <category>이벤트 스트리밍</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/430</guid>
      <comments>https://mirrorofcode.tistory.com/430#entry430comment</comments>
      <pubDate>Wed, 12 Mar 2025 18:42:29 +0900</pubDate>
    </item>
    <item>
      <title>[도메인 주도 설계 철저 입문] 2장. 시스템 특유의 값을 나타내기 위한 '값 객체'</title>
      <link>https://mirrorofcode.tistory.com/418</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 장에서는 '값 객체'에 대해서 다루고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'값 객체'란 무엇일까요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 주도 설계에서 말하는 값 객체는 이렇듯 시스템 특유의 값을 나타내는 객체다.&lt;/p&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;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;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;변하지 않는다. -&amp;gt; final&lt;/li&gt;
&lt;li&gt;주고 받을 수 있다. -&amp;gt; 대입으로 밖에 변경이 안 된다.&lt;/li&gt;
&lt;li&gt;등가성을 비교할 수 있다.(equals @override)&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;변하지 않는다.&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불변하는 값의 장점을 책에서는 다음과 같이 서술하고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 생성하고 메서드에 인자로 넘기니 자기도 모르는 사이에 상태가 수정되어 의도하지 않은 동작을 보이거나 버그를 일으켰다는 이야기는 ... 모르는 사이에 상태가 바뀌는 것이 문제라면 처음부터 상태가 변화하지 않게 하면 된다. 단순하지만 강력한 방어책이다.&lt;/p&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;&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;b&gt;교환 가능하다.&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;pre id=&quot;code_1724676498292&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var fullName = Fullname(&quot;hoo&quot;, &quot;kang&quot;)
fullName = Fullname(&quot;hoo2&quot;, &quot;kang&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;b&gt;등가성 비교 가능&lt;/b&gt;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서는 equality를 등가성으로 번역한 것으로 보인다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 VO의 특징을 얘기할때 '동등성'을 많이 사용한다.&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;pre id=&quot;code_1724676855475&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var nameA = FullName(&quot;hoo&quot;, &quot;kang&quot;)
var nameB = FullName(&quot;hoo&quot;, &quot;kang&quot;)

println(nameA == nameB) // 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;이것이 가능하게 하려면 Java에서는 equals, HashCode를 override 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kotlin에서는 data class를 사용해서 ==(동등성 비교)를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;값 객체가 되기 위한 기준&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서는 어떤 것을 값 객체로 사용해야하는 지에 대해서 간략하게 설명합니다.&lt;/p&gt;
&lt;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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724678611673&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class FullName(
    private val firstName: String,
    private val lastName: String) {

    init {
        require(validateName(firstName)) { &quot;First name must contain only letters&quot; }
        require(validateName(lastName)) { &quot;Last name must contain only letters&quot; }
    }

    private fun validateName(name: String): Boolean {
        return name.matches(&quot;^[a-zA-Z]+$&quot;.toRegex())
    }
}&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;행동이 정의된 값 객체&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VO가 method를 통해서 새로운 객체를 생성하는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724678848628&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
public class Money {
    @NotNull
    private final BigDecimal amount;
    @NotNull
    private final Currency currency;
    
    public Money add(@NotNull Money arg) {
       if(!arg.currency.equals(this.currency)) {
          throw new IllegalArgumentException(&quot;화폐 단위가 다릅니다.&quot;);
        }
        
        return new Money(amount.add(arg.amount), currency);
     }
 }&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;값 객체를 도입했을 때의 장점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서는 총 4가지의 장점을 말한다.&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;2. 무결성이 유지된다.&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;4. 로직이 코드 이곳저곳에 흩어지는 것을 방지한다.&lt;/p&gt;
&lt;p data-ke-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>VO</category>
      <category>값 객체</category>
      <category>도메인 주도 설계</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/418</guid>
      <comments>https://mirrorofcode.tistory.com/418#entry418comment</comments>
      <pubDate>Mon, 26 Aug 2024 21:44:09 +0900</pubDate>
    </item>
    <item>
      <title>flyway로 DB 관리하기(with spring boot &amp;amp; gradle)</title>
      <link>https://mirrorofcode.tistory.com/417</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 flyway를 어떻게 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;Flyway는 DB 버전 관리 툴로, 다양한 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;의존성 추가 및 기본 설정&lt;/h2&gt;
&lt;pre id=&quot;code_1723791581739&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
   ...
   id(&quot;org.flywaydb.flyway&quot;) version &quot;9.0.0&quot;
}


dependencies {
	implementation(&quot;org.flywaydb:flyway-core&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;build.gradle에 이렇게 두 개의 문장을 추가해 의존성을 추가해줍니다.&lt;/p&gt;
&lt;p data-ke-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_1723791695025&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flyway {
	url = db_url
	user = db_user
	password = db_password
	locations = arrayOf(db_migration_file_loc) # 기본 위치는 filesystem:src/main/resources/db/migration
	cleanDisabled = false # clean 가능하게 할지 여부
}&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;그리고 flyway설정을 추가해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 후 gradle sync를 실행해주면,&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-filename=&quot;스크린샷 2024-08-16 오후 4.02.11.png&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bifzj5/btsI5w22TjN/7CQgBjIA0q373fQVWqVQBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bifzj5/btsI5w22TjN/7CQgBjIA0q373fQVWqVQBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bifzj5/btsI5w22TjN/7CQgBjIA0q373fQVWqVQBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbifzj5%2FbtsI5w22TjN%2F7CQgBjIA0q373fQVWqVQBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;425&quot; height=&quot;334&quot; data-filename=&quot;스크린샷 2024-08-16 오후 4.02.11.png&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;398&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;flyway 관련 명령어가 생성된 것을 확인할 수 있습니다.&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;기본 위치는 /src/main/resources/db/migration입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;해당 위치에 실행하고자 하는 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;이떄 규칙이 있는데,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-16 오후 4.05.18.png&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p4Cd0/btsI6zx00qk/dR4SwluXh3l8WoK5p269Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p4Cd0/btsI6zx00qk/dR4SwluXh3l8WoK5p269Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p4Cd0/btsI6zx00qk/dR4SwluXh3l8WoK5p269Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp4Cd0%2FbtsI6zx00qk%2FdR4SwluXh3l8WoK5p269Mk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1380&quot; height=&quot;296&quot; data-filename=&quot;스크린샷 2024-08-16 오후 4.05.18.png&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;296&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;V1__Create_Account.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;undo의 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;U2__Undoe_account.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;repeatable migration의 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R1__Repeat.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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* repeatable migration은 매번 실행될 때마다 동일한 이름으로 재실행 가능한 마이그레이션 스크립트를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 마이그레이션과 달리, Repeatable Migration은 고유한 버전 번호가 없으며, 스크립트 내용이 변경될 때마다 다시 실행됩니다.&lt;/p&gt;
&lt;p data-ke-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;저는 db/migration위치에 V1__Create_account.sql을 만들었고,&lt;/p&gt;
&lt;pre id=&quot;code_1723792120159&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE accounts
(
    id              BIGSERIAL PRIMARY KEY,
    email           CHARACTER VARYING NOT NULL,
    encoded_password CHARACTER VARYING NOT NULL,
    default_username CHARACTER VARYING NOT NULL
);&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;flyway-migrate를 실행하면&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-16 오후 4.11.16.png&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0rDcE/btsI5SStUXP/yDqKoGNmCg3EQPmz9MZbPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0rDcE/btsI5SStUXP/yDqKoGNmCg3EQPmz9MZbPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0rDcE/btsI5SStUXP/yDqKoGNmCg3EQPmz9MZbPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0rDcE%2FbtsI5SStUXP%2FyDqKoGNmCg3EQPmz9MZbPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1010&quot; height=&quot;282&quot; data-filename=&quot;스크린샷 2024-08-16 오후 4.11.16.png&quot; data-origin-width=&quot;1010&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;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;figure id=&quot;og_1723792312377&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;Flyway - Automate database deployments across teams and technologies&quot; data-og-description=&quot;&quot; data-og-host=&quot;www.red-gate.com&quot; data-og-source-url=&quot;https://www.red-gate.com/products/flyway/get-started&quot; data-og-url=&quot;https://www.red-gate.com/products/flyway/get-started&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BWELM/hyWOcbLWdT/Y3jwBlKRKSqtHw9fZnThPk/img.png?width=1202&amp;amp;height=1202&amp;amp;face=0_0_1202_1202&quot;&gt;&lt;a href=&quot;https://www.red-gate.com/products/flyway/get-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.red-gate.com/products/flyway/get-started&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BWELM/hyWOcbLWdT/Y3jwBlKRKSqtHw9fZnThPk/img.png?width=1202&amp;amp;height=1202&amp;amp;face=0_0_1202_1202');&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;Flyway - Automate database deployments across teams and technologies&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.red-gate.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;</description>
      <category>개발 지식</category>
      <category>flyway</category>
      <category>스프링부트 db 관리</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/417</guid>
      <comments>https://mirrorofcode.tistory.com/417#entry417comment</comments>
      <pubDate>Fri, 16 Aug 2024 16:12:12 +0900</pubDate>
    </item>
    <item>
      <title>(데이터베이스) Lost update와 serializable</title>
      <link>https://mirrorofcode.tistory.com/416</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션에는 4가지 격리수준이 있습니다.&lt;br&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;read uncomitted&lt;/li&gt;&lt;li&gt;read comitted&lt;/li&gt;&lt;li&gt;repeatable read&lt;/li&gt;&lt;li&gt;serializable&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;dirty read&lt;/li&gt;&lt;li&gt;non-repeatable read&lt;/li&gt;&lt;li&gt;phantom read&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;(phantom read는 MVCC덕에 발생하지 않습니다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 이와 별개로 serialiable을 제외한 트랜잭션 격리 레벨에서 발생할 수 있는 lost update 문제가 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lost update&lt;/h2&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Lost Update는 동시성 제어 문제 중 하나로, 두 개 이상의 트랜잭션이 동시에 같은 데이터를 수정하려고 할 때 발생할 수 있습니다. 이는 하나의 트랜잭션이 수행한 업데이트가 다른 트랜잭션에 의해 덮어쓰여지는 상황을 말합니다. 즉, 최종적으로 한 트랜잭션의 변경 내용이 손실됩니다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;예를 들어 봅시다.&lt;br&gt;격리 수준 repeatable read에서 두 개의 트랜잭션이 수행됩니다.&lt;br&gt;&amp;nbsp;&lt;br&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;account 테이블에 balance 컬럼이 있는 하나의 레코드가 존재한다고 가정합니다.&lt;/li&gt;&lt;li&gt;초기 balance 값은 100입니다.&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;T1: balance 값을 100 증가시키려고 합니다.&lt;/li&gt;&lt;li&gt;T2: balance 값을 50 감소시키려고 합니다.&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;1. 먼저 트랜잭션 T1이 balance 값을 읽습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T1
BEGIN;
SELECT balance FROM account WHERE id = 1;&amp;nbsp;&amp;nbsp;-- 결과: 100&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;2. 트랜잭션 T2가 balacne 값을 읽습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T2
BEGIN;
SELECT balance FROM account WHERE id = 1;&amp;nbsp;&amp;nbsp;-- 결과: 100&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;3. 트랜잭션 T1이 값을 증가시킵니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T1
UPDATE account SET balance = balance + 100 WHERE id = 1;
-- balance = 200&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;4. 트랜잭션 T1이 커밋됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T1
COMMIT;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;5. 트랜잭션 T2가 값을 감소시킵니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T2
UPDATE account SET balance = balance - 50 WHERE id = 1;
-- balance = 50 (T1의 업데이트가 덮어써짐)&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;6. 트랜잭션 T2가 커밋됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T2
COMMIT;&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;이렇게 되었을 때, 최종 결과는 아래가 된다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;balance = 50&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;하지만 PSQL에서는 다릅니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;PSQL에서의 Lost update&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;PSQL이든MySQL이든&amp;nbsp; 모두 MVCC를 사용합니다.&lt;br&gt;(MySQL의 MVCC는 repeatable read부터이고, PSQL의 MVCC는 read comitted부터 적용됩니다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;MVCC는 Multi Version Concurrency Control의 약자로 동시성 제어 메커니즘입니다.&lt;br&gt;스냅샷을 격리시키기 때문에 여러 개의 트랜잭션의 간섭을 최소화해줍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그럼 MVCC를 가지고 위의 예시를 살펴봅시다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;1. 먼저 트랜잭션 T1이 balance 값을 읽습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T1
BEGIN;
SELECT balance FROM account WHERE id = 1;&amp;nbsp;&amp;nbsp;-- 결과: 100&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;2. 트랜잭션 T2가 balacne 값을 읽습니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T2
BEGIN;
SELECT balance FROM account WHERE id = 1;&amp;nbsp;&amp;nbsp;-- 결과: 100&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;3. 트랜잭션 T1이 값을 증가시킵니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T1
UPDATE account SET balance = balance + 100 WHERE id = 1;
-- balance = 200&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;4. 트랜잭션 T1이 커밋됩니다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- T1
COMMIT;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그리고 5번에서 트랜잭션 T2가 UPDATE를 시도합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 UPDATE 쿼리가 동작하지 않고 충돌로 롤백되게 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그 이유는 MVCC때문인데요,&lt;br&gt;&amp;nbsp;&lt;br&gt;update 쿼리는 내부적으로 실행하기 전에 내부적으로 select가 발생합니다.&lt;br&gt;select를 하면 balance는 T1에 의해서 이미 변경된 200이 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이때 MVCC가 관여하는데, 가장 처음에 스냅샷에서 가져온 값과 조회한 값이 다르기 때문에&lt;br&gt;트랜잭션이 실패하게 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;주의할 점은 Repeatable read 격리 수준에서만 `First-committer wins`원칙에 따라 나중 커밋이 롤백이 되게 됩니다.&lt;br&gt;Read-Comitted의 경우 첫 번째 트랜잭션의 커밋을 대기하고, 커밋이 정상적으로 이루어지면 t2도 커밋이 됩니다.&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;이를 해결하기 위해서는 3가지 방법이 있습니다.&lt;br&gt;&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;select for udpate로 lock을 사용한다.&lt;/li&gt;&lt;li&gt;롤백 되어 실패할 경우 재시도 과정을 넣는다.&lt;/li&gt;&lt;li&gt;격리 수준을 serializable로 올린다.&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;lock을 걸면 잠금 경합이 발생할 수 있고, 이로 인해 데드락이 발생할 수 있습니다.&lt;br&gt;serializable로 격리수준을 올리면 동시성 문제가 발생할 가능성은 없어지지만, 성능 저하가 발생합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;br&gt;&lt;/p&gt;</description>
      <category>개발 지식</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/416</guid>
      <comments>https://mirrorofcode.tistory.com/416#entry416comment</comments>
      <pubDate>Sun, 14 Jul 2024 20:56:10 +0900</pubDate>
    </item>
    <item>
      <title>당신의 Go 코드의 nil 체크가 실패하는 이유(feat. interface)</title>
      <link>https://mirrorofcode.tistory.com/415</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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저에게 하루 약 120만 건 이상의 요청이 들어오는 서버에 관한 태스크가 주어졌습니다.&lt;/p&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;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;p data-ke-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;hr data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;로그를 보던 중, 이런 로그를 보게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715441631887&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{0xc00000c030, 0x0}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;중괄호로 된 것은 struct 같은데,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;왜 하나는 주소값이 있고, 하나는 주소값이 없는 것인가...&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;마침 에러가 발생한 곳은, 모두 인터페이스 nil check를 거친 곳이어서 더 수상했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그러던 중, 특이한 사실을 알아냈습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;바로 인터페이스의 nil이 진짜 nil이 아닐 수도 있다는 사실을요.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715611027121&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type A struct {
	ID string
}

func makeInt() interface{} {
	var a *A
	return a
}

func main() {
	var a = makeInt()

	if a == nil {
		fmt.Println(&quot;a is nil&quot;)
	} else {
		fmt.Println(&quot;a is not nil&quot;)
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;여기서 어떤 것이 출력될까요?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;당연하게 저는 `a is nil`이 출력될 것으로 예상했지만&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결과는 `a is not nil`이 출력되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;왜 그럴까요?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&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;786&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLz8dr/btsHor20zHn/lo4xVwQRNZejGHMKKp4uw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLz8dr/btsHor20zHn/lo4xVwQRNZejGHMKKp4uw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLz8dr/btsHor20zHn/lo4xVwQRNZejGHMKKp4uw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLz8dr%2FbtsHor20zHn%2Flo4xVwQRNZejGHMKKp4uw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;786&quot; height=&quot;494&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;494&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;interface는 내부적으로 type과 value를 가지고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그리고 value만 nil이라면 nil check에 실패하게 되는 것이죠.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;에러가 난 코드를 간단하게 재현해 보자면,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715611382074&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type A struct {
	ID string
}

func makeInt() interface{} {
	var a *A
	return a
}

func main() {
	var a = makeInt()
	var ok bool
	var b *A

	if b, ok = a.(*A); ok {
		fmt.Println(&quot;ok&quot;)
		return
	}

	fmt.Println(b.ID)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이렇게 type 변환을 시도했고, nil이라면 실패했을 type 변환에 성공한 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이렇게 interface가 nillable 하게 되면, 이런 문제를 야기할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그렇기 때문에 반드시 inteface는 nil이 되어서는 안 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;라이브러리 선택과 제거의 중요성&lt;/b&gt;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;저는 서버를 수정하는 과정에서 chebyrash/promise라는 라이브러리를 사용하고 있는 코드를 보고,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;일반 go 루틴을 사용하는 것과 차이가 없다고 느껴 이를 제거했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1715440480217&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignLeft&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - chebyrash/promise: Promise / Future library for Go&quot; data-og-description=&quot;Promise / Future library for Go. Contribute to chebyrash/promise development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/chebyrash/promise&quot; data-og-url=&quot;https://github.com/chebyrash/promise&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b1JfR8/hyV2ujJaxB/7qFTEUkok5muh6MuptEUAk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://github.com/chebyrash/promise&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/chebyrash/promise&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b1JfR8/hyV2ujJaxB/7qFTEUkok5muh6MuptEUAk/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&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;GitHub - chebyrash/promise: Promise / Future library for Go&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Promise / Future library for Go. Contribute to chebyrash/promise development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이것이 문제를 드러나게 했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;promise의 내부 코드를 살펴보면 다음과 같은 코드가 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1715440534756&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func (p *Promise[T]) handlePanic() {
	err := recover()
	if err == nil {
		return
	}

	switch v := err.(type) {
	case error:
		p.reject(v)
	default:
		p.reject(fmt.Errorf(&quot;%+v&quot;, v))
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;promise 실행 중, 에러가 나면 recover()를 통해서 panic을 잡아주는 로직이 존재했던 것입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;더구나 아무런 로그를 남기지 않으니 알 도리가 없었죠.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 문제가 발생한 이유를 알아냈습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;- 원래 코드에 문제가 있어서 panic을 발생하고 있었으며,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;- promise의 recover 때문에 에러가 발생하지 않고 있었고,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;- 로그를 남기지 않았기 때문에 문제가 발생하는 줄도 몰랐던 것입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;라이브러리를 제거할 때, 내부에 있는 구현부까지 확인했어야 했는데,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;제 불찰로 인해 문제가 발생했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;교훈&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 통해서 얻은 교훈은 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;- 언어의 특성을 잘 이해하고 코드를 작성해야한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;- nillable한 인터페이스를 만들지 말자.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;- 로그를 잘 찍자.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 지식</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/415</guid>
      <comments>https://mirrorofcode.tistory.com/415#entry415comment</comments>
      <pubDate>Mon, 13 May 2024 23:47:24 +0900</pubDate>
    </item>
    <item>
      <title>(회고)2023년 회고</title>
      <link>https://mirrorofcode.tistory.com/413</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 무엇을 했고, 어떤 것을 얻었는지 나열함으로써 한 해를 돌아보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;23년 역시 많은 일들이 있었고, 많이 성장했으며, 많은 어려움이 있었던 시간이었습니다.&lt;/p&gt;
&lt;p data-ke-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;우연히 예전에 활동했던 개발 동아리를 통해서 멘토링을 지원했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩 자율학습 &amp;lt;나도 코딩의 파이썬 입문&amp;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;이때의 인연으로 지금까지 파이썬뿐만 아니라, C, HTML+JS, 스프링 부트까지&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;3월, 채널톡 입사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호감이 있었던 회사였는데, 우연히 채용공고를 보게 되었고&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1월 말 지원&lt;/li&gt;
&lt;li&gt;2월 중순 코딩 테스트&lt;/li&gt;
&lt;li&gt;2월 말 1차 인터뷰&lt;/li&gt;
&lt;li&gt;3월 초 합격&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;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;4월, 첫 TF 진행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 내에서 쓰는 서버 하나를 혼자 리팩토링 했어요. (물론 리뷰어는 여러 분 계셨습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go로 작성된 서버였는데,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Echo &amp;rarr; Gin&lt;/li&gt;
&lt;li&gt;DI 라이브러리 fx 사용&lt;/li&gt;
&lt;li&gt;요청 모델, 응답 모델 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 작업을 약 3개월 간 진행했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;diff는 대략 4200줄 정도였네요!&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;6월, 팀 이동&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;lsquo;주문&amp;rsquo;이 제 주요 도메인이 된 거 같아요.&lt;/p&gt;
&lt;p data-ke-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;10월, 두 번째 TF 진행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 TF는 사내에서만 사용하는 거라 상대적으로 부담감이 적었다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 TF는 큰 고객사들의 니즈로 진행되는 거라 부담감이 꽤 컸어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 커머스 연동이었고, 10월에 시작해서 12월 초에 정상적으로 릴리즈 되었어요.&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;23년에 더 나아진 것.&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;22년보다 코딩 잘 해짐.&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;23년에 더 못해진 것.&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;24년엔 더 나아진 것에 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;figure id=&quot;og_1703823532956&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;mikekang47 - Overview&quot; data-og-description=&quot;BE Developer TDD lover. mikekang47 has 88 repositories available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/mikekang47&quot; data-og-url=&quot;https://github.com/mikekang47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bEhZpb/hyUTCi5cAL/RUXsz6xgIGKG0HBs8JXRT0/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420&quot;&gt;&lt;a href=&quot;https://github.com/mikekang47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/mikekang47&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bEhZpb/hyUTCi5cAL/RUXsz6xgIGKG0HBs8JXRT0/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420');&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;mikekang47 - Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;BE Developer TDD lover. mikekang47 has 88 repositories available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;figure id=&quot;og_1703823558229&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;hoo47 - Overview&quot; data-og-description=&quot;Hoo, Back-end engineer, enjoy to test . hoo47 has 5 repositories available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/hoo47&quot; data-og-url=&quot;https://github.com/hoo47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/8kBRd/hyUXKmeeRB/xIcaXjx1qkP7gEFGeMxjqK/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420&quot;&gt;&lt;a href=&quot;https://github.com/hoo47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/hoo47&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/8kBRd/hyUXKmeeRB/xIcaXjx1qkP7gEFGeMxjqK/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420');&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;hoo47 - Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Hoo, Back-end engineer, enjoy to test . hoo47 has 5 repositories available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.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;</description>
      <category>회고</category>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/413</guid>
      <comments>https://mirrorofcode.tistory.com/413#entry413comment</comments>
      <pubDate>Fri, 29 Dec 2023 13:24:32 +0900</pubDate>
    </item>
    <item>
      <title>(Spring) JpaRepository는 인터페이스인데 어떻게 동작할까?</title>
      <link>https://mirrorofcode.tistory.com/412</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Java의 인터페이스&lt;/h2&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;pre id=&quot;code_1693414508298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Person {
	
    void walk();
    
    void run();
 
 }&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;이제는 너무나 잘 아시겠지만, Java 8버전 이후로는 기본 메서드를 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1693414606601&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Person {
	
    void walk();
    
    void run();
 	
    default void sayHello() {
    	System.out.println(&quot;Hello&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;또한 인터페이스의 추상메서드를 사용하기 위해서는 반드시 구현이 필요합니다.&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;pre id=&quot;code_1693414698987&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class User implements Person {
	
    void walk() {
    	System.out.println(&quot;User can walk&quot;);
    }
    
    void run() {
    	System.out.println(&quot;User can not run&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;그리고 JpaRepository를 봅시다.&lt;/p&gt;
&lt;p data-ke-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;JpaRepository&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaRepo를 상속하는 class를 하나 만들었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1693414820060&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 UserRepository extends JpaRepository&amp;lt;User, UUID&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;인터페이스에 어떠한 메서드도 정의되어 있지 않습니다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.01.10.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/paDR8/btssvXpaca1/34k63XDE4pR5l95OfNeYkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/paDR8/btssvXpaca1/34k63XDE4pR5l95OfNeYkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/paDR8/btssvXpaca1/34k63XDE4pR5l95OfNeYkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpaDR8%2FbtssvXpaca1%2F34k63XDE4pR5l95OfNeYkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1310&quot; height=&quot;364&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.01.10.png&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;364&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;저 save() 메서드는 JpaRepository에 정의되어 있지 않습니다.&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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.02.50.png&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/suZoZ/btssHFtucxP/TJki3amksZX8vUG5M9lhy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/suZoZ/btssHFtucxP/TJki3amksZX8vUG5M9lhy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/suZoZ/btssHFtucxP/TJki3amksZX8vUG5M9lhy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsuZoZ%2FbtssHFtucxP%2FTJki3amksZX8vUG5M9lhy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;2026&quot; height=&quot;228&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.02.50.png&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;228&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.02.57.png&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbNGkA/btssBXV9Cji/dKv8suo5xewv9r5fpBiIcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbNGkA/btssBXV9Cji/dKv8suo5xewv9r5fpBiIcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbNGkA/btssBXV9Cji/dKv8suo5xewv9r5fpBiIcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbNGkA%2FbtssBXV9Cji%2FdKv8suo5xewv9r5fpBiIcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1808&quot; height=&quot;196&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.02.57.png&quot; data-origin-width=&quot;1808&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.03.05.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMjQwC/btssGjxHpeE/9kkXWZBJiB3jfwJO0K9EaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMjQwC/btssGjxHpeE/9kkXWZBJiB3jfwJO0K9EaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMjQwC/btssGjxHpeE/9kkXWZBJiB3jfwJO0K9EaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMjQwC%2FbtssGjxHpeE%2F9kkXWZBJiB3jfwJO0K9EaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1546&quot; height=&quot;602&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.03.05.png&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;602&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;이렇게 상속의 트리를 타고 올라가면 CrudRepository가 존재하고, 여기에 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;/p&gt;
&lt;p data-ke-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;그럼 인터페이스로 돌아와서, 인터페이스의 메서드는 추상 메서드이기 때문에 구현이 필요하다고 앞서 말했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 JpaRepo를 상속한 인터페이스에는 그 어디에도 메서드가 정의되어 있지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 JpaRepo, 상위 인터페이스인 PagingAndSoringRepo, CrudRepo 모두 인터페이스입니다.&lt;/p&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;SimpleJpaRepository&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.07.26.png&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcGUj9/btssvg3FiGG/YRWFj7rtA8bc26DbevPkbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcGUj9/btssvg3FiGG/YRWFj7rtA8bc26DbevPkbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcGUj9/btssvg3FiGG/YRWFj7rtA8bc26DbevPkbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcGUj9%2Fbtssvg3FiGG%2FYRWFj7rtA8bc26DbevPkbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;199&quot; height=&quot;106&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.07.26.png&quot; data-origin-width=&quot;466&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 Fx 버튼을 눌러 우리는 구현체를 찾을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.06.54.png&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kE7xz/btssGAsp6iQ/Opl0k7y8gbIkQRdIbjmm91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kE7xz/btssGAsp6iQ/Opl0k7y8gbIkQRdIbjmm91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kE7xz/btssGAsp6iQ/Opl0k7y8gbIkQRdIbjmm91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkE7xz%2FbtssGAsp6iQ%2FOpl0k7y8gbIkQRdIbjmm91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1700&quot; height=&quot;680&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.06.54.png&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;680&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;바로 SimpleJpaRepo를요.&lt;/p&gt;
&lt;p data-ke-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;여기에 JpaRepo의 상위 인터페이스에 대한 추상메서드의 구현체가 담겨 있습니다.&lt;/p&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()는 어떻게 동작할까요?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.09.13.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsPB4o/btssGjxHpxm/AF5e32U0IK90LrwLWcqKt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsPB4o/btssGjxHpxm/AF5e32U0IK90LrwLWcqKt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsPB4o/btssGjxHpxm/AF5e32U0IK90LrwLWcqKt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsPB4o%2FbtssGjxHpxm%2FAF5e32U0IK90LrwLWcqKt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1316&quot; height=&quot;708&quot; data-filename=&quot;스크린샷 2023-08-31 오전 2.09.13.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;708&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;EntityManager를 통해서 Persist한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 날것의 코드와 차이가 없습니다.&lt;/p&gt;
&lt;p data-ke-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;여기서 혼동하지 말아야하는 것은 JPA와 SpringDataJpa, Hibernate의 관계입니다.&lt;/p&gt;
&lt;p data-ke-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;JPA는 인터페이스이고, SpringDataJPA는 이 인터페이스를 쉽게 사용하기 위해 존재하는 모듈이며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 인터페이스의 구현을 Hibernate가 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-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;SpringDataJpa의 구현은 Hibernate로 작성되어 있다.&lt;/li&gt;
&lt;li&gt;상속관계를 잘 확인하자&lt;/li&gt;
&lt;li&gt;JPA, SpringDataJpa, Hibernate 관계를 잘 알자.&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>
      <author>후;</author>
      <guid isPermaLink="true">https://mirrorofcode.tistory.com/412</guid>
      <comments>https://mirrorofcode.tistory.com/412#entry412comment</comments>
      <pubDate>Thu, 31 Aug 2023 02:15:41 +0900</pubDate>
    </item>
  </channel>
</rss>