<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>minji's engineering note</title>
    <link>https://sinclairstudio.tistory.com/</link>
    <description>Data Engineering과 Cloud Native 기술에 대해 Dive Deep 하는 플랫폼 엔지니어가 되는 것을 목표로 하고 있습니다. 
경험과 공부한 내용을 기록하며 지속가능한 엔지니어가 되는 것이 꿈입니다. 
</description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 07:29:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>minjiwoo</managingEditor>
    <image>
      <title>minji's engineering note</title>
      <url>https://tistory1.daumcdn.net/tistory/3175925/attach/72e9270aace84c65b8c95d48318116ef</url>
      <link>https://sinclairstudio.tistory.com</link>
    </image>
    <item>
      <title>이벤트 기반의 데이터 마이그레이션을 위한 Kafka Outbox Pattern 적용기</title>
      <link>https://sinclairstudio.tistory.com/662</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qr0KK/dJMcahYxMtI/TvydA0IedXVhMwBoFOZTZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qr0KK/dJMcahYxMtI/TvydA0IedXVhMwBoFOZTZk/img.png&quot; data-alt=&quot;generated by chatGPT&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qr0KK/dJMcahYxMtI/TvydA0IedXVhMwBoFOZTZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqr0KK%2FdJMcahYxMtI%2FTvydA0IedXVhMwBoFOZTZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;generated by chatGPT&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 배경&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 서비스에서 신규 서비스로의 회원 데이터 이관을 위해, 이벤트 기반 아키텍처를 도입하였다. 회원이 처음 신규 시스템에 접근하고 이관에 동의하면, 회원 서비스는 MemberMigrated 이벤트를 발행한다. 이 이벤트를 트리거로 데이터 이관이 비동기적으로 수행되는 구조를 설계하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 초기 설계&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 다음과 같은 구조를 사용하였다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Kafka Consumer가 MemberMigrated 이벤트를 수신&lt;/li&gt;
&lt;li&gt;내부 API 호출&lt;/li&gt;
&lt;li&gt;&lt;span&gt;API는 즉시 &lt;/span&gt;202 Accepted&lt;span&gt; 응답 반환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;실제 데이터 migration은 비동기로 수행&lt;/li&gt;
&lt;li&gt;migration 완료 후 migrationDone 이벤트 발행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 기반이며, 데이터 마이그레이션이 필요하므로 API 응답을 항상 빠르게 처리하고 비동기적으로 데이터 마이그레이션을 처리하는 것이 좋다고 판단하여 이렇게 설계하였다. 이 구조는 API 응답을 빠르게 처리하고, 비동기 방식으로 시스템 부하를 분산할 수 있다는 장점이 있었다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위의 초기 설계에서는 문제점이 있다.&amp;nbsp;&lt;b&gt;Kafka Consumer 는 API 호출이 성공하여 202응답을 받으면 해당 이벤트가 정상적으로 처리되었다고 판단하고 offset 을 commit한다. 그러나 실제로 데이터 migration 처리는 비동기로 이루어지기 때문에, 이후 단계에서 실패하더라도 이를 Consumer가 인지할 수 없다. 결과적으로 다음과 같은 문제가 발생한다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 Migration 실패 여부를 추적할 수 없음&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이벤트 재처리 불가능&lt;/li&gt;
&lt;li&gt;DLQ(Dead Letter Queue) 로의 전송 불가능&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. 개선 시도 : 동기 처리&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;API 내부에서 데이터 Migration 을 동기적으로 수행한다.&lt;/li&gt;
&lt;li&gt;migration 성공 이후에 이벤트를 발행한다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 실제 데이터 처리 결과를 기준으로 이벤트가 발행되기 때문에 Consumer 는 처리 결과를 신뢰할 수 있다는 장점이 있다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Dual Write Problem&amp;nbsp; (이중 쓰기 문제) 의 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 위의 개선시도에서, 또 다른 문제가 여전히 존재한다. 데이터를 적재하는 target Database 와 Kafka는 서로 다른 시스템이므로, 다음과 같은 불일치 상황이 발생할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Case 1&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 저장 성공&lt;/li&gt;
&lt;li&gt;Kafka publish 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;u&gt;이벤트를 consume 하는 다른 서비스들은 이 결과를 받지 못한다.&amp;nbsp;&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;Case 2&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;Kafka publish 성공&lt;/li&gt;
&lt;li&gt;DB 저장 실패&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;u&gt;실제로는 존재하지 않는 데이터에 대한 이벤트가 발생할 수 있다.&amp;nbsp;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 Dual Write Problem 이라고 하며, 단순히 재시도 로직으로 이 문제를 해결하기 어렵다. 또한 데이터 정합성이 깨지는 원인이된다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;6. Outbox Pattern 으로 Dual Write Problem 해결하기&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Dual Write Problem 문제를 해결하기 위한 방안으로, Outbox Pattern 을 도입했다. Outbox Pattern의 핵심 아이디어는,&lt;b&gt; 이벤트를 Kafka 에 직접 발행하지 않고, 데이터베이스에 함께 상태를 저장&lt;/b&gt;한다는 것이다. 즉 데이터 마이그레이션과 kafka event 발행을 하나의 트랜잭션으로 묶는 효과를 얻을 수 있다. 이렇게 하면 Database 데이터 적재와 이벤트 생성이 보장된다는 일관성을 확보할 수 있다. Outbox table 을 Database 에 생성하면, 이후 CDC 또는 polling을 통해 outbox table 의 레코드를 읽어 kafka 로 이벤트를 발행한다. 이러한&lt;/span&gt;&lt;span&gt; Outbox Pattern 에도 단점은 존재한다. CDC 또는 polling 구조가 필요하므로 아키텍처와 인프라의 복잡도가 증가한다. 또한 이벤트가 중복으로 발행될 수 있기 때문에 Idempotent 처리가 필요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Outbox Pattern 도입하기&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;7.1 Outbox Table 설계 (PostgreSQL 기준)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox Pattern의 핵심은 &lt;span&gt;&lt;b&gt;이벤트를 DB에 저장하는 것&lt;/b&gt;&lt;/span&gt;이다.PostgreSQL 기준으로 Outbox Table 은 다음과 같이 설계할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777504321777&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE outbox (
    id BIGSERIAL PRIMARY KEY,
    aggregate_type VARCHAR(100) NOT NULL,
    aggregate_id VARCHAR(100) NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    payload JSONB NOT NULL,
    status VARCHAR(20) DEFAULT 'READY',
    created_at TIMESTAMP DEFAULT NOW()
);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;aggregate_type&lt;/b&gt;&lt;/span&gt;: 도메인 타입 (예: Member)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;aggregate_id&lt;/b&gt;&lt;/span&gt;: 이벤트의 대상 식별자 (예: memberId). partition key로 활용할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;event_type&lt;/b&gt;&lt;/span&gt;: 이벤트 종류 (예: MemberMigrated)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;payload&lt;/b&gt;&lt;/span&gt;: 실제 이벤트 데이터 (JSON 형태)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;status&lt;/b&gt;&lt;/span&gt;: 처리 상태 (READY, SENT 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;outbox table 에는 cdc connector 에서 접근하게 된다. 따라서 주로 kafka_user 를 새로 만들고 권한을 부여한다. CDC 를 활용하는 경우 &lt;b&gt;Replication 권한 또는 WAL 접근 권한&lt;/b&gt;&lt;span&gt;이 추가로 필요할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;7.2 Polling vs CDC 방식의 비교&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox 테이블에 저장된 이벤트를 Kafka로 전달하는 방식은 크게 두 가지가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Polling 방식&lt;/b&gt; : 애플리케이션 또는 별도 worker가 주기적으로 outbox 테이블을 조회한다.&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;별도의 인프라가 필요하지 않다&amp;nbsp;&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;지연 발생&amp;nbsp;&lt;/li&gt;
&lt;li&gt;DB 부하 증가&amp;nbsp;&lt;/li&gt;
&lt;li&gt;실시간성 감소&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1777505340573&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
class OutboxPollingScheduler(
    private val outboxRepository: OutboxRepository,
    private val producer: OutboxKafkaProducer
) {

    @Scheduled(fixedDelay = 1000) // 1초마다 polling
    @Transactional
    fun publishOutboxEvents() {
        val events = outboxRepository.findTop100ByStatusOrderByCreatedAt()

        events.forEach { event -&amp;gt;
            try {
                val topic = mapToTopic(event.eventType)
				
                // application 에서 직접 topic 으로 이벤트를 발행한다. 
          
                producer.send(
                    topic = topic,
                    key = event.aggregateId,
                    payload = event.payload
                )

                event.status = OutboxStatus.SENT

            } catch (e: Exception) {
                event.status = OutboxStatus.FAILED
            }
        }
    }

    private fun mapToTopic(eventType: String): String {
        return when (eventType) {
            &quot;MemberMigrated&quot; -&amp;gt; &quot;member.migrated&quot;
            &quot;OrderCreated&quot; -&amp;gt; &quot;order.created&quot;
            else -&amp;gt; &quot;unknown.event&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;2. CDC (Change Data Capture) 방식&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OhIPD/dJMcaaynjk9/4QQkX9Z6ZKJnjfxKKbNly1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OhIPD/dJMcaaynjk9/4QQkX9Z6ZKJnjfxKKbNly1/img.png&quot; data-alt=&quot;created by chatgpt&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OhIPD/dJMcaaynjk9/4QQkX9Z6ZKJnjfxKKbNly1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOhIPD%2FdJMcaaynjk9%2F4QQkX9Z6ZKJnjfxKKbNly1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1754&quot; height=&quot;896&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;created by chatgpt&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 변경 로그(WAL)를 기반으로 이벤트를 감지하여 Kafka로 전달한다. 대표적으로 Debezium 을 사용한다. CDC란 데이터베이스의 변경 사항 (Insert/Update/Delete) 을 감지하여 외부 시스템으로 전달하는 기술이다. 즉, polling 에서는 어플리케이션이 직접 DB 를 주기적으로 조회 (polling) 해야 했지만, CDC 를 사용하면 DB 변경 자체를 이벤트로 활용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;장점&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간에 가까운 처리&lt;/li&gt;
&lt;li&gt;DB 부하 최소화&lt;/li&gt;
&lt;li&gt;확장성 우수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 설정 복잡&lt;/li&gt;
&lt;li&gt;Kafka Connector 운영 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자의 경우, 우선적으로 Debezium과 kafka connector 가 구축이 되어 있는 상황이었으며 data migration 특성상 db의 부하가 클 것이므로 부하를 최소화 하는 CDC 방식을 선택하게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Q. CDC는 어떻게 동작할까 ?&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDC는 직접 table을 조회하지 않고, DB의 내부 로그 (WAL, binlog 등) 를 읽는다. PostgreSQL 기준으로 보면 INSERT 가 발생하면 WAL (Write Ahead Log)에 기록한다. CDC는 WAL 를 읽고 변경 이벤트를 Kafka로 전달한다. 따라서 DB 성능에 영향을 거의 주지 않게 되는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;7.3 Outbox Topic -&amp;gt; 실제 Topic 라우팅&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 outbox 에서 하나의 공통 outbox topic 으로 이벤트가 적재된다. 그 이후 서비스 별로 관심있는 이벤트가 다를 수 있으므로, topic 단위로 구독 구조를 분리하여 라우팅한다. &lt;/span&gt;&lt;span&gt;&lt;/span&gt;Debezium 설정을 통해 event_type 기반으로 topic 자동 분기를 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/662</guid>
      <comments>https://sinclairstudio.tistory.com/662#entry662comment</comments>
      <pubDate>Thu, 30 Apr 2026 07:52:30 +0900</pubDate>
    </item>
    <item>
      <title>Keycloak SSO 연동 과정에서 이해한 인증과 인가</title>
      <link>https://sinclairstudio.tistory.com/661</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;외부 SaaS와 우리 서비스를 연동하는 개발을 하면서 가장 많이 마주친 개념은 인증과 인가였다. 이번 글에서는 SSO, JWT, 그리고 인증/인가가 실제 요청 흐름에서 어떻게 연결되는지 정리해보려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.인증 (Authentication) 이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;너 누구야?&amp;rdquo; 라고 사용자가 누군지 확인하는 단계이다. 즉, 요청을 보낸 주체가 누구인지 식별하고, 그 신원이 유효한지 검증하는 단계다.&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;Google, Kakao 등을 통한 소셜 로그인&lt;/li&gt;
&lt;li&gt;회사 SSO 를 통한 로그인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 끝나면 서버는 &amp;ldquo;이 사용자가 누구인지&amp;rdquo;는 알 수 있다. 하지만 그 사용자가 어떤 기능까지 수행할 수 있는지는 아직 알 수 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.인가 (Authorization)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인가 라는 것은 &amp;ldquo;너 이거 해도 돼?&amp;rdquo; 를 확인하는 과정이다. 인증이 사용자의 신원을 확인하는 단계라면, 인가는 그 사용자가 특정 자원이나 기능에 접근할 권한이 있는지 판단하는 단계다. 예를 들어 이런 것들이 인가의 문제다.&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;사용자 A 가 사용자 B, C, D의 주문 정보를 조회할 수 있는가&lt;/li&gt;
&lt;li&gt;이 문서를 읽는 것이 가능한가 수정이 가능한가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인가의 기준은 단순한 역할(Role)만으로 결정되지 않는다.&lt;br /&gt;실제 서비스에서는 권한(Permission), 소유권(Ownership), 조직 범위(Tenant), 정책(Policy) 등 다양한 기준이 함께 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 인증과 인가를 분리해야 하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증과 인가는 함께 등장하지만, 서로 다른 문제를 해결한다. 특히 외부 SaaS를 우리 서비스와 연동할 때 이 차이가 더 분명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 커머스 서비스에서 고객 상담용 SaaS인 &lt;b&gt;Zendesk&lt;/b&gt;를 연동한다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Zendesk는 현재 접속한 사용자가 누구인지 확인해야 한다. 즉, 이 사용자가 실제로 우리 커머스 서비스의 유효한 사용자인지 검증하는 &lt;b&gt;인증(Authentication)&lt;/b&gt; 이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 인증만으로는 충분하지 않다. 어떤 사용자인지 확인되었다고 해서, 그 사용자가 모든 상담 내용에 접근할 수 있어서는 안 되기 때문이다. 예를 들어 사용자 A가 로그인했다고 해서 사용자 B나 C의 문의 내역, 주문 관련 상담 내용, 개인정보가 포함된 상담 기록까지 조회할 수 있다면 큰 보안 문제가 된다. 그래서 인가가 필요하다. 인가는 사용자에게 허용된 범위를 제한하는 단계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증과 인가를 분리하지 않으면, 로그인만 하면 다른 사용자들의 모든 상담내역을 볼 수 있는 위험한 구조가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. JWT는 무엇일까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 로그인 페이지에서 아이디와 비밀번호를 입력하면, 인증 시스템은 해당 사용자의 정보를 검증한다. 검증이 성공하면 Access Token 을 발급하는데, 이 토큰 형식으로 자주 사용되는 것이 JWT (JSON Web Token) 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 사용자와 토큰에 대한 정보를 담아 전달하는 형식이다. 클라이언트는 로그인 성공 이후 API를 호출할 때 이 JWT를 Authorization 헤더에 담아 보낸다.&amp;nbsp;JWT는 보통 다음과 같은 정보를 담을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2143&quot; data-start=&quot;2091&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2100&quot; data-start=&quot;2091&quot; data-section-id=&quot;10sqcfo&quot;&gt;사용자 식별자&lt;/li&gt;
&lt;li data-end=&quot;2108&quot; data-start=&quot;2101&quot; data-section-id=&quot;s4bf4g&quot;&gt;만료 시간&lt;/li&gt;
&lt;li data-end=&quot;2131&quot; data-start=&quot;2109&quot; data-section-id=&quot;1sbb4gd&quot;&gt;역할(Role) 또는 권한 관련 정보&lt;/li&gt;
&lt;li data-end=&quot;2143&quot; data-start=&quot;2132&quot; data-section-id=&quot;4kfem4&quot;&gt;토큰 발급자 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;Authorization: Bearer &amp;lt;access-token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 는 점으로 구분된 세 부분으로 구성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1774663429226&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;header.payload.signature&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 부분의 의미는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2343&quot; data-start=&quot;2234&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2261&quot; data-start=&quot;2234&quot; data-section-id=&quot;16kawas&quot;&gt;header: 서명에 사용한 알고리즘 정보&lt;/li&gt;
&lt;li data-end=&quot;2309&quot; data-start=&quot;2262&quot; data-section-id=&quot;xj95cb&quot;&gt;payload: sub, role, exp 같은 클레임(Claim)&lt;/li&gt;
&lt;li data-end=&quot;2343&quot; data-start=&quot;2310&quot; data-section-id=&quot;4ii3im&quot;&gt;signature: 토큰 위변조를 방지하기 위한 서명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 예시&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;123&quot;, // 사용자 식별자 
  &quot;preferred_username&quot;: &quot;minjiwoo&quot;, 
  &quot;role&quot;: &quot;ADMIN&quot;, // 권한 
  &quot;exp&quot;: 300 // 만료 시각 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 정보를 담는 형식일 뿐이다. 이 토큰이 신뢰할 수 있는지, 그리고 이 사용자가 실제로 무엇을 할 수 있는지는 서버가 별도로 판단해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. SSO 는 무엇일까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSO(Single Sign-On)는 한 번 로그인하면 여러 시스템을 다시 로그인하지 않고 사용할 수 있게 해주는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 회사에서 &lt;b&gt;Okta&lt;/b&gt; 같은 중앙 인증 시스템에 한 번 로그인하면, 이후 AWS나 GitHub 같은 다른 서비스에도 다시 로그인하지 않고 접근할 수 있는 경우가 있다.&lt;br /&gt;즉, 서비스마다 아이디와 비밀번호를 반복해서 입력하는 대신, &lt;b&gt;한 번 중앙 인증 시스템에 로그인하면 그 결과를 여러 서비스가 함께 신뢰하는 구조&lt;/b&gt;가 바로 SSO다. 즉, SSO의 핵심은&amp;nbsp;&lt;b&gt;여러 시스템이 동일한 사용자 신원 체계를 공유하도록 만드는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 그렇다면 Keycloak 은 무슨 역할을 할까&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xFjcW/dJMcagdS6Qa/B7sCB5BdBjloDRJQKZMvi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xFjcW/dJMcagdS6Qa/B7sCB5BdBjloDRJQKZMvi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xFjcW/dJMcagdS6Qa/B7sCB5BdBjloDRJQKZMvi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxFjcW%2FdJMcagdS6Qa%2FB7sCB5BdBjloDRJQKZMvi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;342&quot; height=&quot;228&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 등장하는 것이 &lt;b&gt;Keycloak&lt;/b&gt; 이다. Keycloak은 SSO, 인증, 인가를 중앙에서 관리할 수 있게 해주는 &lt;b&gt;오픈소스 IAM(Identity and Access Management) 솔루션&lt;/b&gt;이다.&lt;br /&gt;쉽게 말하면, 각 서비스가 로그인 기능을 제각각 구현하지 않고, 로그인과 사용자 신원 검증을 &lt;b&gt;Keycloak에 맡기는 구조&lt;/b&gt;를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keycloak 의 주요 기능은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 인증의 중앙화 &lt;br /&gt;사용자의 로그인 처리를 한 곳에서 담당한다.서비스마다 로그인 로직을 따로 만들지 않아도 되기 때문에 인증 방식이 일관되어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) SSO 제공&lt;br /&gt;한 번 로그인한 사용자가 여러 시스템을 다시 로그인하지 않고 이용할 수 있게 한다.즉, Keycloak이 여러 애플리케이션 사이의 로그인 세션을 연결해주는 중심 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 토큰 발급&lt;br /&gt;인증이 끝난 뒤 Access Token, Refresh Token, ID Token 같은 토큰을 발급한다.애플리케이션은 이 토큰을 바탕으로 사용자를 식별하고 요청을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 사용자 및 권한 관리&lt;br /&gt;사용자, 그룹, 역할(Role) 등을 중앙에서 관리할 수 있다. 즉, &amp;ldquo;누구인지&amp;rdquo;뿐 아니라 &amp;ldquo;어떤 권한을 가질 수 있는지&amp;rdquo;를 함께 다룰 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) 외부 인증 시스템 연동&lt;br /&gt;Google 같은 소셜 로그인이나 다른 인증 시스템과도 연동할 수 있다.즉, Keycloak은 직접 사용자를 관리할 수도 있고, 다른 인증 시스템을 중간에서 연결하는 허브 역할도 할 수 있다.&lt;/p&gt;
&lt;h3 data-end=&quot;88&quot; data-start=&quot;71&quot; data-section-id=&quot;xy0q66&quot; data-ke-size=&quot;size23&quot;&gt;7. Keycloak의 Realm은 어떤 단위일까&lt;/h3&gt;
&lt;p data-end=&quot;138&quot; data-start=&quot;90&quot; data-ke-size=&quot;size16&quot;&gt;Keycloak을 처음 보면 가장 먼저 나오는 개념 중 하나가 &lt;b&gt;Realm&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-end=&quot;420&quot; data-start=&quot;140&quot; data-ke-size=&quot;size16&quot;&gt;Realm은 쉽게 말해 &lt;b&gt;인증과 인가가 독립적으로 관리되는 하나의 보안 영역&lt;/b&gt;이다. 각 Realm은 자기만의 사용자(User), 클라이언트(Client), 역할(Role), 그룹(Group), 로그인 설정을 가진다. 즉, Realm이 다르면 사용자 목록도 다르고, 토큰을 발급하는 기준도 다르고, 권한 체계도 분리된다. 예를 들어서 서비스별로 권한 체계를 나눌 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;420&quot; data-start=&quot;140&quot;&gt;고객 전용 서비스 Realm&lt;/li&gt;
&lt;li data-end=&quot;420&quot; data-start=&quot;140&quot;&gt;백오피스 전용 Realm&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-section-id=&quot;xy0q66&quot; data-start=&quot;71&quot; data-end=&quot;88&quot;&gt;8. Keycloak에서 JWT 를 어떻게 발급 / 관리할 수 있을까&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 Keycloak을 통해 로그인하면, Keycloak은 인증에 성공한 사용자에 대해 토큰을 발급한다.&lt;br /&gt;애플리케이션은 이 토큰을 받아 이후 요청을 처리하게 된다. 또한 토큰의 수명이나 세션 유지 정책은 Keycloak의 &lt;b&gt;realm&lt;/b&gt; 또는 &lt;b&gt;client&lt;/b&gt; 설정에서 조정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 단순하게 보면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1635&quot; data-start=&quot;1276&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1299&quot; data-start=&quot;1276&quot; data-section-id=&quot;1yv89w9&quot;&gt;사용자가 애플리케이션에 접속한다.&lt;/li&gt;
&lt;li data-end=&quot;1340&quot; data-start=&quot;1300&quot; data-section-id=&quot;105px3f&quot;&gt;애플리케이션은 사용자를 Keycloak 로그인 페이지로 보낸다.&lt;/li&gt;
&lt;li data-end=&quot;1361&quot; data-start=&quot;1341&quot; data-section-id=&quot;2u8qfn&quot;&gt;사용자가 로그인에 성공한다.&lt;/li&gt;
&lt;li data-end=&quot;1410&quot; data-start=&quot;1362&quot; data-section-id=&quot;h34pxm&quot;&gt;Keycloak은 애플리케이션에 authorization code를 전달한다.&lt;/li&gt;
&lt;li data-end=&quot;1512&quot; data-start=&quot;1411&quot; data-section-id=&quot;1g99051&quot;&gt;애플리케이션은 이 code를 이용해 Keycloak의 토큰 엔드포인트에 요청하고, Access Token, ID Token, 필요하면 Refresh Token을 발급받는다.&lt;/li&gt;
&lt;li data-end=&quot;1557&quot; data-start=&quot;1513&quot; data-section-id=&quot;1dp3p7d&quot;&gt;클라이언트는 이후 API 요청에 Access Token을 담아 보낸다.&lt;/li&gt;
&lt;li data-end=&quot;1557&quot; data-start=&quot;1513&quot; data-section-id=&quot;1dp3p7d&quot;&gt;서버는 토큰을 검증하고, 사용자 정보를 바탕으로 요청을 처리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Access Token은 주로 API 호출에 사용되고, ID Token은 로그인한 사용자가 누구인지 식별하는 데 사용된다.&lt;br /&gt;그리고 Refresh Token은 Access Token이 만료되었을 때, 사용자가 다시 로그인하지 않아도 새로운 토큰을 발급받을 수 있게 해준다.&lt;/p&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;866&quot; data-ke-size=&quot;size16&quot;&gt;Keycloak은 이렇게 토큰을 발급하는 것에서 끝나지 않고, &lt;b&gt;토큰을 검증하고 갱신할 수 있는 기준&lt;/b&gt;도 함께 제공한다.&lt;br /&gt;예를 들어 API 서버는 클라이언트가 보낸 Access Token을 그대로 신뢰하는 것이 아니라, 이 토큰이 정말 Keycloak이 발급한 것인지 검증해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1165&quot; data-start=&quot;1030&quot; data-ke-size=&quot;size16&quot;&gt;이때 Keycloak은 각 realm의 공개키를 &lt;b&gt;JWKS(JSON Web Key Set)&lt;/b&gt; 형태로 제공한다.&lt;br /&gt;애플리케이션이나 API 서버는 이 공개키를 사용해 JWT의 서명을 검증하고, 해당 토큰이 위조되지 않았는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-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>Backend</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/661</guid>
      <comments>https://sinclairstudio.tistory.com/661#entry661comment</comments>
      <pubDate>Sat, 28 Mar 2026 11:17:28 +0900</pubDate>
    </item>
    <item>
      <title>2025년 개발자 회고</title>
      <link>https://sinclairstudio.tistory.com/660</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;2025. Q1&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSP 프로젝트 &amp;gt; Databricks MLOps 운영 프로젝트&amp;nbsp;&lt;/h3&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 data-end=&quot;254&quot; data-start=&quot;178&quot;&gt;MSP 프로젝트 특성상 클라우드 벤더(Azure) 서비스 의존도가 높았고 신규 개발보다는 &lt;b&gt;운영/지원 중심 역할&lt;/b&gt;로 투입되었다.&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;313&quot; data-start=&quot;255&quot;&gt;실제 업무는 운영이라기보다 &lt;b&gt;고객사 엔지니어 대상 Databricks 교육&lt;/b&gt;이 주가 되었음&lt;/li&gt;
&lt;li data-end=&quot;399&quot; data-start=&quot;314&quot;&gt;기존 MLOps 환경은 아래의 문제점을 가지고 있었다. 사실 MLOps 라고 말할 수가 없을 정도였다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;399&quot; data-start=&quot;333&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;350&quot; data-start=&quot;333&quot;&gt;ML 엔지니어 수동으로 실행해야 하는 부분이 많았음&lt;/li&gt;
&lt;li data-end=&quot;378&quot; data-start=&quot;353&quot;&gt;exe + Windows 스케줄러 기반&lt;/li&gt;
&lt;li data-end=&quot;399&quot; data-start=&quot;381&quot;&gt;파이프라인 가시성/운영성 부족&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;498&quot; data-start=&quot;400&quot;&gt;&lt;b&gt;MLOps 현대화(Azure Data Pipeline + Databricks)&lt;/b&gt; 개선
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;498&quot; data-start=&quot;400&quot;&gt;우선 기존의 방식이 너무 비효율적이라고 생각.&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;498&quot; data-start=&quot;400&quot;&gt;처음으로 운영 환경을 경험해보는 건데, 나의 주된 role은 교육/지원이었지만 운영 환경에서 문제를 정의하고 성취감을 느낄 수 있을 만한 엔지니어링 업무를 도전해보고 싶었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아쉬운 점: 고객사와의 커뮤니케이션이 미숙했던 것 같다. 그래도&amp;nbsp;&lt;/li&gt;
&lt;li&gt;좋았던 점: 중간에 그만두고 싶었지만 책임감을 지고 끝까지 마쳤다는 것이다. &amp;nbsp;다른 동료 / PM 없이 혼자 그 먼 거리 (송도)를 출퇴근 하면서 프로젝트를 하면서&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2025. Q2&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;조직 이동과 이직&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 조직 이동&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직한 마음으로는 SI 성 업무에 지쳤다. 비슷한 프로젝트가 반복되서 성장한다는 느낌이 적었고, 내가 되고 싶어하는 엔지니어보다는 customer engineer 업무를 하는 부서여서 한번 진득하게 개발해보고 싶었다.&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;2.&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>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/660</guid>
      <comments>https://sinclairstudio.tistory.com/660#entry660comment</comments>
      <pubDate>Wed, 31 Dec 2025 23:46:55 +0900</pubDate>
    </item>
    <item>
      <title>ChatGPT와의 대화를 기반으로 한 2025년 회고</title>
      <link>https://sinclairstudio.tistory.com/659</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKxq0q/dJMcagc5Wy9/Gx50LUfKN0okACrTisDD9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKxq0q/dJMcagc5Wy9/Gx50LUfKN0okACrTisDD9K/img.png&quot; data-alt=&quot;GPT와의 회고 시작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKxq0q/dJMcagc5Wy9/Gx50LUfKN0okACrTisDD9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKxq0q%2FdJMcagc5Wy9%2FGx50LUfKN0okACrTisDD9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;552&quot; height=&quot;143&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GPT와의 회고 시작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 12월 31일, 벌써 올해도 두시간 채 남지 않았다. 오랜만에 이런 특별한 날에 약속이 없어서 혼자 회고 하는 시간을 가지며 올 한해를 마무리해보려고 한다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. GPT 에게 부탁한 나의 회고&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-end=&quot;110&quot; data-start=&quot;78&quot; data-ke-size=&quot;size23&quot;&gt;2025년 회고 &amp;mdash; 흔들리면서도 분명히 앞으로 간 해&lt;/h3&gt;
&lt;p data-end=&quot;179&quot; data-start=&quot;112&quot; data-ke-size=&quot;size16&quot;&gt;2025년의 나는, 한 문장으로 말하면&lt;br /&gt;**&amp;ldquo;불안해도 멈추지 않았고, 확신이 없어도 계속 쌓아 올린 사람&amp;rdquo;**이었다.&lt;/p&gt;
&lt;h4 data-end=&quot;204&quot; data-start=&quot;181&quot; data-ke-size=&quot;size20&quot;&gt;1. 커리어: 방향을 다시 잡은 해&lt;/h4&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;206&quot; data-ke-size=&quot;size16&quot;&gt;올해의 가장 큰 키워드는 단연 &lt;b&gt;전환&lt;/b&gt;이었다.&lt;br /&gt;SI 중심의 경험, Databricks 위주의 커리어, &amp;ldquo;이게 과연 서비스 회사에서 의미가 있을까?&amp;rdquo;라는 질문.&lt;br /&gt;그리고 실제 면접 탈락.&lt;/p&gt;
&lt;p data-end=&quot;393&quot; data-start=&quot;316&quot; data-ke-size=&quot;size16&quot;&gt;솔직히 말하면, 흔들렸다.&lt;br /&gt;&amp;ldquo;내가 쌓아온 것들이 잘못된 건가?&amp;rdquo;&lt;br /&gt;&amp;ldquo;다시 처음부터 배워야 하나?&amp;rdquo;&lt;br /&gt;이 질문들이 꽤 오래 머물렀다.&lt;/p&gt;
&lt;p data-end=&quot;427&quot; data-start=&quot;395&quot; data-ke-size=&quot;size16&quot;&gt;하지만 인상 깊었던 건, &lt;b&gt;포기하지 않았다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;569&quot; data-start=&quot;429&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;469&quot; data-start=&quot;429&quot;&gt;Kafka, Streaming, 이벤트 파이프라인을 다시 공부했고&lt;/li&gt;
&lt;li data-end=&quot;516&quot; data-start=&quot;470&quot;&gt;Airflow, EKS, Terraform 같은 인프라 쪽으로 시야를 넓혔고&lt;/li&gt;
&lt;li data-end=&quot;569&quot; data-start=&quot;517&quot;&gt;단순히 &amp;ldquo;툴을 쓴다&amp;rdquo;가 아니라&lt;br /&gt;&lt;b&gt;왜 이런 구조가 필요한지&lt;/b&gt;를 설명하려고 애썼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;584&quot; data-start=&quot;571&quot; data-ke-size=&quot;size16&quot;&gt;이 해를 지나며 나는&lt;/p&gt;
&lt;blockquote data-end=&quot;651&quot; data-start=&quot;585&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;651&quot; data-start=&quot;587&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;Databricks 엔지니어&amp;rdquo;가 아니라&lt;br /&gt;&lt;b&gt;&amp;ldquo;서비스를 이해하는 데이터 엔지니어&amp;rdquo;&lt;/b&gt; 쪽으로 방향을 틀었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;653&quot; data-ke-size=&quot;size16&quot;&gt;이건 꽤 큰 전환이었다.&lt;/p&gt;
&lt;h4 data-end=&quot;698&quot; data-start=&quot;673&quot; data-ke-size=&quot;size20&quot;&gt;2. 일의 밀도: 실무에서 더 깊어지다&lt;/h4&gt;
&lt;p data-end=&quot;744&quot; data-start=&quot;700&quot; data-ke-size=&quot;size16&quot;&gt;2025년의 나는 코드도 많이 썼지만,&lt;br /&gt;그보다 더 많이 &lt;b&gt;결정&lt;/b&gt;을 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;796&quot; data-start=&quot;746&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;759&quot; data-start=&quot;746&quot;&gt;세금/관세 도메인&lt;/li&gt;
&lt;li data-end=&quot;772&quot; data-start=&quot;760&quot;&gt;글로벌 이커머스&lt;/li&gt;
&lt;li data-end=&quot;796&quot; data-start=&quot;773&quot;&gt;번역, 세율, 국가별 규칙, 예외 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;850&quot; data-start=&quot;798&quot; data-ke-size=&quot;size16&quot;&gt;단순 구현이 아니라&lt;br /&gt;**&amp;ldquo;이 로직이 비즈니스에서 어떤 의미를 갖는지&amp;rdquo;**를 계속 고민했다.&lt;/p&gt;
&lt;p data-end=&quot;866&quot; data-start=&quot;852&quot; data-ke-size=&quot;size16&quot;&gt;이때부터 바뀐 점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;955&quot; data-start=&quot;868&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;888&quot; data-start=&quot;868&quot;&gt;&amp;ldquo;동작한다&amp;rdquo;보다 &amp;ldquo;설명 가능하다&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;919&quot; data-start=&quot;889&quot;&gt;&amp;ldquo;지금 편한 구조&amp;rdquo;보다 &amp;ldquo;내일 고칠 수 있는 구조&amp;rdquo;&lt;/li&gt;
&lt;li data-end=&quot;955&quot; data-start=&quot;920&quot;&gt;&amp;ldquo;내가 이해한 코드&amp;rdquo;보다 &amp;ldquo;다른 사람이 읽을 수 있는 코드&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1002&quot; data-start=&quot;957&quot; data-ke-size=&quot;size16&quot;&gt;이건 기술 성장이라기보다&lt;br /&gt;&lt;b&gt;엔지니어로서의 태도가 바뀐 해&lt;/b&gt;였다고 생각한다.&lt;/p&gt;
&lt;h4 data-end=&quot;1037&quot; data-start=&quot;1009&quot; data-ke-size=&quot;size20&quot;&gt;3. 마음 상태: 목표에서 잠시 내려온 시기&lt;/h4&gt;
&lt;p data-end=&quot;1061&quot; data-start=&quot;1039&quot; data-ke-size=&quot;size16&quot;&gt;연말로 갈수록, 조금 다른 변화가 왔다.&lt;/p&gt;
&lt;p data-end=&quot;1078&quot; data-start=&quot;1063&quot; data-ke-size=&quot;size16&quot;&gt;예전의 나는 늘 이랬다.&lt;/p&gt;
&lt;blockquote data-end=&quot;1122&quot; data-start=&quot;1079&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1122&quot; data-start=&quot;1081&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;반드시 이직해야 해&amp;rdquo;&lt;br /&gt;&amp;ldquo;더 잘해야 해&amp;rdquo;&lt;br /&gt;&amp;ldquo;뒤처지면 안 돼&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1158&quot; data-start=&quot;1124&quot; data-ke-size=&quot;size16&quot;&gt;그런데 2025년 후반의 나는&lt;br /&gt;잠시 그 자리에서 내려왔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1221&quot; data-start=&quot;1160&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1177&quot; data-start=&quot;1160&quot;&gt;목표에서 한 발 물러났고&lt;/li&gt;
&lt;li data-end=&quot;1195&quot; data-start=&quot;1178&quot;&gt;쉬는 걸 스스로 허락했고&lt;/li&gt;
&lt;li data-end=&quot;1221&quot; data-start=&quot;1196&quot;&gt;콘텐츠를 소비하며 느슨해지는 시간도 가졌다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1251&quot; data-start=&quot;1223&quot; data-ke-size=&quot;size16&quot;&gt;이건 퇴보가 아니었다.&lt;br /&gt;&lt;b&gt;회복&lt;/b&gt;에 가까웠다.&lt;/p&gt;
&lt;p data-end=&quot;1284&quot; data-start=&quot;1253&quot; data-ke-size=&quot;size16&quot;&gt;계속 달리기만 하던 사람이&lt;br /&gt;호흡을 다시 고르는 시간.&lt;/p&gt;
&lt;p data-end=&quot;1347&quot; data-start=&quot;1286&quot; data-ke-size=&quot;size16&quot;&gt;그리고 그 과정에서&lt;br /&gt;&amp;ldquo;나는 성취가 없으면 무가치한 사람인가?&amp;rdquo;라는 질문에도&lt;br /&gt;조금은 솔직해질 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1357&quot; data-start=&quot;1349&quot; data-ke-size=&quot;size16&quot;&gt;답은 아니었다.&lt;/p&gt;
&lt;h4 data-end=&quot;1399&quot; data-start=&quot;1364&quot; data-ke-size=&quot;size20&quot;&gt;4. 나라는 사람: 여전히 성실하고, 여전히 호기심 많은&lt;/h4&gt;
&lt;p data-end=&quot;1419&quot; data-start=&quot;1401&quot; data-ke-size=&quot;size16&quot;&gt;대화를 돌아보면 확실히 느껴진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1532&quot; data-start=&quot;1421&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1441&quot; data-start=&quot;1421&quot;&gt;이해 안 되면 끝까지 물어본다&lt;/li&gt;
&lt;li data-end=&quot;1465&quot; data-start=&quot;1442&quot;&gt;한 번 시작한 건 구조까지 고민한다&lt;/li&gt;
&lt;li data-end=&quot;1532&quot; data-start=&quot;1466&quot;&gt;기술 얘기와 동시에&lt;br /&gt;옷, 향, 캐릭터, 햄스터 세계관 같은&lt;br /&gt;&lt;b&gt;전혀 다른 영역도 진심으로 좋아한다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1575&quot; data-start=&quot;1534&quot; data-ke-size=&quot;size16&quot;&gt;이건 흩어진 성향이 아니라&lt;br /&gt;&lt;b&gt;넓은 감각을 가진 사람의 모습&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-end=&quot;1622&quot; data-start=&quot;1577&quot; data-ke-size=&quot;size16&quot;&gt;논리와 감성, 실무와 상상력.&lt;br /&gt;2025년의 나는 이 둘을 동시에 놓지 않았다.&lt;/p&gt;
&lt;h4 data-end=&quot;1652&quot; data-start=&quot;1629&quot; data-ke-size=&quot;size20&quot;&gt;5. 2025년을 한 줄로 정리하면&lt;/h4&gt;
&lt;p data-end=&quot;1739&quot; data-start=&quot;1697&quot; data-ke-size=&quot;size16&quot;&gt;불안했고, 흔들렸고, 때로는 느슨했지만&lt;br /&gt;그럼에도 방향은 계속 앞으로였다.&lt;/p&gt;
&lt;h4 data-end=&quot;1763&quot; data-start=&quot;1746&quot; data-ke-size=&quot;size20&quot;&gt;6. 2026년의 나에게&lt;/h4&gt;
&lt;p data-end=&quot;1795&quot; data-start=&quot;1765&quot; data-ke-size=&quot;size16&quot;&gt;조급해하지 않아도 된다.&lt;br /&gt;이미 충분히 많이 쌓았다.&lt;/p&gt;
&lt;p data-end=&quot;1813&quot; data-start=&quot;1797&quot; data-ke-size=&quot;size16&quot;&gt;다만 하나만 기억했으면 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1880&quot; data-start=&quot;1815&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1841&quot; data-start=&quot;1815&quot;&gt;너는 &amp;ldquo;운 좋게 여기 온 사람&amp;rdquo;이 아니라&lt;/li&gt;
&lt;li data-end=&quot;1880&quot; data-start=&quot;1842&quot;&gt;&lt;b&gt;생각하고, 고민하고, 책임지며 여기까지 온 엔지니어&lt;/b&gt;라는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1911&quot; data-start=&quot;1882&quot; data-ke-size=&quot;size16&quot;&gt;그리고 가끔은&lt;br /&gt;햄스터처럼 굴러가도 괜찮다는 것도.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-end=&quot;1357&quot; data-start=&quot;1349&quot; data-ke-size=&quot;size16&quot;&gt;대체로 맞는 말도 있고 조금 과장되거나 드라마틱하게 쓰여진 부분들이 있다. 기술 블로그는 조금 드라이하게 쓰자고 생각하여 내가 좋아하는 KPT 방식으로 회고를 해보려고 한다.&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. GPT 와 KPT 방식으로 회고 하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpxp51/dJMcaaDWfPL/ErUn8ErmAX2cCSguJSPim1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpxp51/dJMcaaDWfPL/ErUn8ErmAX2cCSguJSPim1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpxp51/dJMcaaDWfPL/ErUn8ErmAX2cCSguJSPim1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpxp51%2FdJMcaaDWfPL%2FErUn8ErmAX2cCSguJSPim1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;547&quot; height=&quot;314&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  2025년 KPT 종합 회고 (업데이트)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  커리어&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.3256%;&quot;&gt;&lt;b&gt;Keep&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 87.5581%;&quot;&gt;&amp;bull; 2024년 이직 탈락 이후에도 2025년 초까지 도전을 멈추지 않았다.&lt;br /&gt;&amp;bull;글또 커뮤니티를 중심으로 개발자 네트워킹을 꾸준히 유지했고, 실제 면접&amp;middot;연봉 협상&amp;middot;커리어 선택에 도움을 받았다.&lt;br /&gt;&amp;bull; 데이터 엔지니어에서 백엔드 엔지니어로의 커리어 전환을 고민에 그치지 않고 직접 실행했다.&lt;br /&gt;&amp;bull; 안정적인 대기업에 재직하면서도 스터디와 기술 블로그를 꾸준히 이어갔다.&lt;br /&gt;&amp;bull; 최근에는 이직 불안보다 현재 회사 일에 집중하며 업무 몰입도가 높아졌다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.3256%;&quot;&gt;&lt;b&gt;Problem&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 87.5581%;&quot;&gt;&amp;bull; 이직 전까지 이직 생각에 과도하게 매여 타인과의 비교로 에너지를 많이 소모했다.&lt;br /&gt;&amp;bull; 의욕에 비해 시간이 한정적인데, 너무 많은 것을 동시에 하려는 경향이 있었다.&lt;br /&gt;&amp;bull; 해외 취업에 대한 관심은 컸지만 실제 지원&amp;middot;면접으로 이어지지 못했다.&lt;br /&gt;  &lt;b&gt;여전히 해외 생활&amp;middot;유학을 하는 타인을 보며 부러움과 조급함을 느꼈다.&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.3256%;&quot;&gt;&lt;b&gt;Try&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 87.5581%;&quot;&gt;&amp;bull; 백엔드 및 인프라 지식을 더 깊고 날카롭게 다듬는다.&lt;br /&gt;&amp;bull; 개인 업무를 넘어 팀과 프로젝트 전체에 +&amp;alpha;의 영향을 줄 수 있는 기여를 시도한다.&lt;br /&gt;&amp;bull; 호주, 미국, 유럽 회사들에 실제로 이력서를 제출하고 면접을 경험해본다.&lt;br /&gt;  &lt;b&gt;타인과의 비교보다 나에게 집중하고, 해외 생활&amp;middot;해외 취업이라는 목표가 분명하다면 그에 맞는 노력과 시간을 의도적으로 투자한다.&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  생활 (마음 &amp;middot; 일상 &amp;middot; 리듬)&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.6047%;&quot;&gt;&lt;b&gt;Keep&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.2791%;&quot;&gt;&amp;bull; 헬스장을 주 2~3회 꾸준히 다니며 운동 루틴을 유지했다.&lt;br /&gt;&amp;bull; 플라잉 요가라는 새로운 운동에 도전했다.&lt;br /&gt;&amp;bull; 인생 처음으로 10km 마라톤에 도전하며 러닝이라는 새로운 취미를 만들었다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.6047%;&quot;&gt;&lt;b&gt;Problem&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.2791%;&quot;&gt;&amp;bull; 잦은 야근으로 기상 시간이 늦어지며 생활 리듬이 흐트러졌다.&lt;br /&gt;&amp;bull; 과자 등 간식 섭취가 늘어나 식습관 관리가 어려웠다.&lt;br /&gt;&amp;bull; 자투리 시간을 의식적으로 활용하지 못한 점이 아쉬웠다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.6047%;&quot;&gt;&lt;b&gt;Try&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.2791%;&quot;&gt;&amp;bull; 운동 루틴을 계속 유지한다.&lt;br /&gt;&amp;bull; 단 음식과 탄수화물 섭취를 줄이고 체지방 감량을 목표로 &lt;b&gt;-3kg&lt;/b&gt;에 도전한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  돈 (소비 &amp;middot; 저축 &amp;middot; 태도)&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.1163%;&quot;&gt;&lt;b&gt;Keep&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 89.7674%;&quot;&gt;&amp;bull; ISA 계좌와 IRP 계좌를 개설했다.&lt;br /&gt;&amp;bull; 처음으로 주식 투자를 시작했고, ETF와 빅테크 위주로 투자 경험을 쌓았다.&lt;br /&gt;&amp;bull; 경제 유튜브를 꾸준히 시청하며 금융 지식에 대한 관심을 키웠다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.1163%;&quot;&gt;&lt;b&gt;Problem&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 89.7674%;&quot;&gt;&amp;bull; 쇼핑몰 앱을 쉬는 시간에 자주 보며 충동 소비가 늘어났다.&lt;br /&gt;&amp;bull; 예쁜 옷을 보면 자연스럽게 구매로 이어지는 소비 패턴이 반복되었다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.1163%;&quot;&gt;&lt;b&gt;Try&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 89.7674%;&quot;&gt;&amp;bull; 옷 구매 빈도를 &lt;b&gt;월 1~2회 &amp;rarr; 2개월~분기 1회&lt;/b&gt;로 줄인다.&lt;br /&gt;&amp;bull; 투자 포트폴리오를 보다 구체적으로 설계한다.&lt;br /&gt;&amp;bull; 자취를 시작하기 위한 재정적 준비를 본격적으로 시작한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-end=&quot;1736&quot; data-start=&quot;1715&quot; data-ke-size=&quot;size20&quot;&gt;최종 한 줄 회고 (with GPT)&lt;/h4&gt;
&lt;blockquote data-end=&quot;1808&quot; data-start=&quot;1738&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1808&quot; data-start=&quot;1740&quot; data-ke-size=&quot;size16&quot;&gt;흔들리고 부러워하는 순간도 있었지만,&lt;br /&gt;비교에 머무르기보다&lt;br /&gt;이직에 성공하며 성취를 확인했고,&lt;br /&gt;내가 원하는 삶에 시간을 쓰기로 결심한 해.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발일기</category>
      <category>chatgpt회고</category>
      <category>개발자이직</category>
      <category>주니어개발자이직</category>
      <category>회고</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/659</guid>
      <comments>https://sinclairstudio.tistory.com/659#entry659comment</comments>
      <pubDate>Wed, 31 Dec 2025 23:15:58 +0900</pubDate>
    </item>
    <item>
      <title>가상 면접 사례로 배우는 대규모 시스템 설계 기초 2 - 분산 메시지 큐</title>
      <link>https://sinclairstudio.tistory.com/658</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl3pCq/dJMcagc4I8g/jsUp0TkWA2xBvzG4fOJYk0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl3pCq/dJMcagc4I8g/jsUp0TkWA2xBvzG4fOJYk0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl3pCq/dJMcagc4I8g/jsUp0TkWA2xBvzG4fOJYk0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl3pCq%2FdJMcagc4I8g%2FjsUp0TkWA2xBvzG4fOJYk0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;694&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분산 메시지 큐&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 메시지 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. point-to-point model : 전통적인 메시지 큐.&lt;u&gt; 큐에 전송된 메시지는 한 소비자만&lt;/u&gt; 가져갈 수 있음. 메시지를 가져갔다는 뜻으로 ACK 를 보내면, 큐에서 해당 메시지가 삭제됨.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. publish-subscribe model : 토픽에 메세지를 보내고 토픽으로부터 메세지를 받음. 메세지는 &lt;u&gt;해당 토픽을 구독하는 모든 소비자들&lt;/u&gt;에게 전달됨. 메세지는 토픽에 보관됨. 토픽을 여러 파티션으로 나눠서, 메시지를 균등하게 각각의 파티션에 보내어 분산 배치함.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 문제의 설계 요구 조건&amp;nbsp;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메세지 큐의 기본 조건 : 생산자는 메시지를 큐에 보내고, 소비자는 큐에서 메시지를 꺼낼 수 있어야 한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;기본 기능 외에도 성능, 메시지 전달 방식, 데이터 보관 기간 등을 고려해야 한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 개략적 설계안&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 클라이언트&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생산자&lt;/li&gt;
&lt;li&gt;소비자&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 핵심 서비스 및 저장소&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브로커 : 파티션들을 유지한다. 하나의 파티션은 토픽에 대한 부분 집합.&lt;/li&gt;
&lt;li&gt;저장소 :
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 저장소 : 메시지를 보관한다.&lt;/li&gt;
&lt;li&gt;상태 저장소 : 소비자의 상태를 저장한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;메타데이터 저장소 : 토픽 설정, 속성 등을 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;조정 서비스
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서비스 탐색 : 어떤 브로커가 살아있는지 알려준다.&lt;/li&gt;
&lt;li&gt;리더 선출 : 컨트롤러 역할. 파티션 배치를 책임진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 상세 설계&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 데이터 저장소&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선택지 1) 데이터 베이스 : 저장 요구사항을 맞출 수는 있지만, 메시지 큐 데이터 사용 패턴에 적합하지 않음. 읽기 연산과 쓰기 연산이 동시에 대규모로 빈번하게 일어나는 메시지 큐에 적합하지 않음 -&amp;gt; 오히려 병목이 됨.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;선택지 2) 쓰기 우선 로그 WAL : Write Ahead Log 는 새로운 항목이 추가되면 append-only 만 하는 방식. MySQL 의 복구 로그가 WAL 로 구현되어 있음. WAL 에 대한 접근 패턴은 읽기 / 쓰기 모두 순차적이고, &lt;u&gt;접근 패턴이 순차적일 때 디스크는 좋은 성능&lt;/u&gt;을 보인다.&amp;nbsp;&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;&lt;b&gt;디스크가 접근 패턴이 순차적일 때 효과적인 이유&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 랜덤 접근 시에, 디스크는 헤드를 이동하고, 원판이 돌아서 위치를 맞추기를 기다린 후에 데이터 접근 (읽기 / 쓰기) 작업을 한다. 실제 데이터 처리보다 이동 및 대기 시간이 더 커질 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- WAL 은 순차적으로 write 작업은 계속 뒤에 append 만 시키고, read 작업은 앞에서부터 읽도록 시키기 때문에 디스크에서 성능이 효과적이다.&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. push model vs pull model&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;push model : 브로커가 소비자에게 메시지를 보내는 방식&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : 브로커는 메시지를 받는 즉시 소비자에게 보낼 수 있음.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;단점 : 소비자가 메세지를 처리하는 속도가 생산자가 메시지를 생성하는 속도보다 느린 경우, 소비자에게 큰 부하가 걸릴 가능성이 있음.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pull model : 소비자가 메시지를 땡겨와서 가져가는 방식&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : 메시지를 소비하는 속도를 소비자가 결정 가능함. 소비하는 속도가 생산 속도보다 느려지면, 소비자를 늘려서 해결할 수 있음. 혹은 기다릴 수 있음. 배치 처리에 적합함.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;단점 : 브로커에 메시지가 없어도 소비자가 데이터를 끌어가려고 하는 시도를 할 것임. -&amp;gt; 대부분 메세지 큐는 롱 폴링 모드를 지원해서, 일정 시간은 기다리도록 하게 함. 메시지가 올 때까지 서버가 잠깐 붙잡고 기다려주는 방식. 숏 폴링은 계속 요청을 보내서 네트워크를 낭비하는데, 롱 폴링은 서버 부담이 줄어든다는 장점이 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 메시지 큐는 pull model 을 지원한다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Architecture</category>
      <category>시스템디자인</category>
      <category>시스템디자인인터뷰</category>
      <category>시스템아키텍처</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/658</guid>
      <comments>https://sinclairstudio.tistory.com/658#entry658comment</comments>
      <pubDate>Sun, 28 Dec 2025 22:13:25 +0900</pubDate>
    </item>
    <item>
      <title>[Architecture] Usecase 중심 백엔드 아키텍처 : 비즈니스 행동을 코드로 드러내기</title>
      <link>https://sinclairstudio.tistory.com/657</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Intro&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비즈니스는 오래 가고, 기술은 자주 바뀐다&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chn4ua/dJMcabCLvKK/b9AunPH1e03K9dFXQlvUg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chn4ua/dJMcabCLvKK/b9AunPH1e03K9dFXQlvUg0/img.png&quot; data-alt=&quot;image generated by Dall-E&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chn4ua/dJMcabCLvKK/b9AunPH1e03K9dFXQlvUg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchn4ua%2FdJMcabCLvKK%2Fb9AunPH1e03K9dFXQlvUg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;320&quot; height=&quot;480&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;image generated by Dall-E&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 엔지니어에서 백엔드 엔지니어로 전향하면서, 코드로 해결해야 하는 문제가 훨씬 더 많아졌다. 지금은 MAU가 꽤 나오는 글로벌 이커머스 백엔드를 만들고 있다. 트래픽도, 요구사항도, 의존하는 외부 시스템도 많다 보니 코드베이스는 빠르게 커지고 복잡도도 함께 증가한다.&lt;br /&gt;그래서 단순히 &amp;ldquo;기능을 구현하는 방법&amp;rdquo;이 아니라, &lt;b&gt;유지보수와 확장에 강한 구조&lt;/b&gt;를 더 진지하게 공부해야겠다고 느꼈다. 이 글에서는 현재 실무에서 백엔드 개발에서 자주 사용중인 &lt;b&gt;Usecase 패턴&lt;/b&gt;을 중심으로, 실제 이커머스 도메인에서 어떻게 적용할 수 있는지 정리해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전통적인&amp;nbsp;Controller&amp;ndash;Service&amp;ndash;Repository&amp;nbsp;구조의&amp;nbsp;한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 Backend pattern 은 다음과 같다. Controller, Service, Repository, Entity 로 이루어진다. 이해하기 쉽고 직관적이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766049993961&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Controller :  Handles HTTP requests and responses (Presentation layer)
 └─ Service : Contains the business logic and orchestrates data flow (Business layer/Service Layer)
     └─ Repository : Manages database interactions (Persistence/Data Access layer)
         └─ Entity : Represents the data model in the database&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;그렇지만 이 구조의 문제는 점점 서비스가 발전함에 따라서 Service Layer 가 비대해질 수 있다는 것이다. Service는 이런 역할들을 모두 떠 안게 된다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 규칙 : 도메인 규칙, 정책 판단, 상태 변경 가능 여부&lt;/li&gt;
&lt;li&gt;유즈케이스 흐름 제어 : A-&amp;gt;B-&amp;gt;C 의 흐름, 실패시 분기&lt;/li&gt;
&lt;li&gt;트랜잭션 : 어디서부터 트랜잭션을 시작하고 끝내는지의 여부&lt;/li&gt;
&lt;li&gt;외부 시스템 호출 : 결제 시스템 호출, 재고 시스템 호출, 배송 시스템 호출 등&lt;/li&gt;
&lt;li&gt;검증 : 요청 값 검증, 상태 검증, 중복 요청 방지&lt;/li&gt;
&lt;li&gt;로깅 / 메트릭 : 이벤트 발행, 모니터링 로그 수집&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;즉, Service Layer는 시간이 지날수록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;비즈니스 규칙과 기술 관심사가 뒤섞인 거대한 실행 클래스&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;가 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Service Layer가 커지면 이게 어떤 비즈니스 로직인지 바로 이해하기가 어려워진다. 또한 새로운 요구 사항이 들어왔을 때 사이드 이펙트 파악하기도 쉽지 않아서 유지보수가 쉽지 않다. 주문 기능이 있다고 할 때 OrderService 라는 이름만 봐서는 어떤 행동을 하고, 시나리오를 가지고 있고, 변경 범위가 어디인지 파악하기 어렵다. 반면, &lt;b&gt;Usecase&lt;/b&gt; 구조는 이러한 책임을 &amp;ldquo;&lt;b&gt;비즈니스 행동 단위&lt;/b&gt;&amp;rdquo;로 분리하여 코드의 의도를 다시 드러낼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Usecase&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Usecase란 행위자(Actor)에게 시스템이 제공하는 하나의 행동(Action)을 정의한 것이다.기존 Service Layer에 뒤섞여 있던 책임을 &lt;b&gt;비즈니스 행동 단위로 재배치&lt;/b&gt;하는 역할을 한다.&lt;br /&gt;Usecase는 &lt;b&gt;상태가 아니라 행동&lt;/b&gt;을 표현한다. 그래서 클래스 이름은 항상 &lt;b&gt;동사 형태&lt;/b&gt;를 사용한다. 클래스 이름만 봐도 이 usecase가 어떤 행동을 제공하는지 직관적으로 알 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766052107935&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PlaceOrder
CancelOrder
CalculateOrderPrice
ApplyPromotion&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 usecase 는 하나의 시나리오이다. 시나리오 내부에서 여러 도메인 객체들을 조합하고, 실행 순서를 정의내리고 실행한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766052373603&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class PlaceOrder(
    private val loadProductsFromCart: LoadProductsFromCart, // Port Layer
    private val saveOrderResult: SaveOrderResult // Port Layer
) : PlaceOrderUseCase {

    @Transactional
    override fun execute(command: PlaceOrderCommand): PlaceOrderResult {
        val cart = loadProductsFromCart.load(command.cartId)

        val order = Order.place(
            customerId = command.customerId,
            cart = cart,
            shippingAddressId = command.shippingAddressId,
            paymentMethod = command.paymentMethod,
            couponId = command.couponId
        )

        val orderId = saveOrderResult.save(order, command.idempotencyKey)

        return PlaceOrderResult(orderId.value)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;669&quot; data-start=&quot;634&quot; data-ke-size=&quot;size23&quot;&gt;Ports (input / output ports)&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Usecase는 Port(인터페이스)를 통해서만 바깥과 소통한다. 또한 Usecase 는 행위를 요청만 하고, 어떤식으로 만들어지는 알지 못한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;783&quot; data-start=&quot;670&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;715&quot; data-start=&quot;670&quot;&gt;&lt;b&gt;Input Port&lt;/b&gt;: Usecase가 외부에서 행위자에 의해 어떻게 호출되는지 정의한다. Controller, Scheduler, Consumer 등 모든 호출자는 이 인터페이스만 의존한다. 아래의 인터페이스는 PlaceOrder 라는 Usecase를 외부에 노출하는 Input Port 이다 .&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1766052527680&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface PlaceOrder {
    fun execute(command: PlaceOrderCommand): PlaceOrderResult
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;783&quot; data-start=&quot;670&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;716&quot;&gt;&lt;b&gt;Output Port&lt;/b&gt;:Usecase가 필요로 하는 기능을 정의한다. 주문 시스템에서, 주문이 이루어지면 주문 결과를 db 에 저장해야 한다고 하자. 그러기 위해서는 Order Id 와 같은 새로운 주문에 대한 식별자 값이 필요할 것이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1766052494590&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface SaveOrderResult {
    fun save(order: Order): OrderId
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Usecase 입력 모델: Command&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Usecase는 보통 execute() 하나로 호출 규칙을 통일하고 있다. 이때 Usecase 실행에 필요한 입력값은 Command 객체로 캡슐화한다.Command 는 Usecase 를 실행하기 위한 입력값을 하나의 요청 객체로 묶어놓은 것이다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;아래와 같은 형태로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Usecase 실행에 필요한 데이터만 담는 DTO&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;이다. 파라미터가 늘어나는 것을 막고, usecase의 입력을 안정적으로 만든다. 또한 &amp;ldquo;이 행동을 실행한다&amp;rdquo;는 의도를 코드에 명확히 드러내는 효과가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;199&quot; data-start=&quot;178&quot;&gt;PlaceOrderCommand&lt;/li&gt;
&lt;li data-end=&quot;222&quot; data-start=&quot;200&quot;&gt;CancelOrderCommand&lt;/li&gt;
&lt;li data-end=&quot;248&quot; data-start=&quot;223&quot;&gt;ApplyPromotionCommand&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1766054113037&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class PlaceOrderCommand(
    val customerId: String,
    val cartId: String,
    val shippingAddressId: String,
    val paymentMethod: PaymentMethod,
    val couponId: String? = null,
    val idempotencyKey: String
)

enum class PaymentMethod {
    CARD,
    PAYPAL,
    APPLE_PAY
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Adapter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Port 는 내부에서 (Usecase) 바깥에 요구하는 &amp;ldquo;계약(Interface)&amp;rdquo; 에 대해서 정의한다. 그리고 Adapter 는 계약에 대해서 실제로 구현하는 클래스이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Port 는 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766054675740&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.company.ecommerce.application.order.port

interface LoadProductsFromCart {
    fun load(cartId: String): List&amp;lt;CartProduct&amp;gt;
}

data class CartProduct(
    val productId: String,
    val name: String,
    val unitPrice: Long,
    val quantity: Int
)&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;Adapter는&lt;b&gt; Usecase가 정의한 요구 조건(Port)&lt;/b&gt;을 실제 기술로 구현한 클래스다. 예를 들어서, &quot;장바구니에서 물품 목록을 조회해야 한다&quot;는 요구를 JPA, SQL, HTTP와 같은 외부 인프라를 이용해 충족시킨다. 따라서 Adapter에는 데이터베이스 접근, 외부 API 호출 등 &lt;b&gt;기술 의존적인 코드가 포함된다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1766054667014&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class CartProductQueryAdapter(
    private val cartItemRepository: CartItemJpaRepository,
    private val productRepository: ProductJpaRepository
) : LoadProductsFromCart {

    @Transactional(readOnly = true)
    override fun load(cartId: String): List&amp;lt;CartProduct&amp;gt; {
        val cartItems = cartItemRepository.findAllByCartId(cartId)
        if (cartItems.isEmpty()) return emptyList()

        val productIds = cartItems.map { it.productId }.distinct()
        val products = productRepository.findAllById(productIds)
            .associateBy { it.id }

        return cartItems.map { item -&amp;gt;
            val p = products[item.productId]
                ?: throw IllegalStateException(&quot;Product not found. productId=${item.productId}&quot;)

            CartProduct(
                productId = p.id,
                name = p.name,
                unitPrice = p.unitPrice,
                quantity = item.quantity
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2495&quot; data-start=&quot;2474&quot; data-ke-size=&quot;size23&quot;&gt;Domain Service&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순수 비즈니스 규칙에 해당하는 로직이다. 여러 엔티티에 걸친 정책이다. 정책 이기 때문에 상태가 없다. (Stateless)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Adapter와 다르게 특정 외부 기술 (SaaS, Database 등)에 의존적이지 않다. 언뜻보기에 비슷해보여서 헷갈리지만, 둘은 완전히 다른 목적의 layer이다. &lt;/span&gt;Adapter는 Usecase가 정의한 요구 조건을 기술(JPA, HTTP 등)을 통해 구현하는 계층이며, Domain Service는 특정 엔티티에 속하지 않는 순수 비즈니스 규칙을 표현한다.&lt;br /&gt;Domain Service는 기술을 모르고, Adapter는 비즈니스를 판단하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멤버 등급에 따라서 할인을 해주는 비즈니스 규칙이 있다고하자. 아래와 같이 구현할 수 있을 것이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766055519862&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DiscountPolicy {
    fun applyMembershipDiscount(order: Order, memberGrade: MemberGrade): Money {
        return when (memberGrade) {
            MemberGrade.VIP -&amp;gt; order.totalAmount().multiply(0.9)
            MemberGrade.NORMAL -&amp;gt; order.totalAmount()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain Service는 도메인 내부의 비즈니스 규칙을 표현하는 객체이기 때문에, Port처럼 구현 교체를 전제로 한 인터페이스를 반드시 둘 필요는 없다.&lt;br /&gt;정책이 여러 개로 분기되거나 전략 교체가 필요한 경우에만 인터페이스 도입을 고려하는 것이 적절하다. 만약 할인 정책이 여러개이고, 런타임에 바뀌는 경우에는 interface를 둘 수도 있을 것이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766055825475&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface DiscountPolicy {
    fun apply(order: Order, memberGrade: MemberGrade): Money
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1766055791569&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class VipDiscountPolicy : DiscountPolicy
class NormalDiscountPolicy : DiscountPolicy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Usecase 패턴은 코드를 &amp;ldquo;기능&amp;rdquo;이 아니라 &amp;ldquo;행동&amp;rdquo;으로 바라보는 하나의 관점이다. Usecase 구조는 Service가 무거워지고 복잡해지는 문제를 해결하기 위해 &amp;ldquo;주문한다&amp;rdquo;, &amp;ldquo;취소한다&amp;rdquo;, &amp;ldquo;할인을 적용한다&amp;rdquo;와 같은 비즈니스 행동 단위로 책임을 분리한다.&lt;br /&gt;Usecase는 시나리오의 흐름을 담당하고, Domain은 규칙을 표현하며, Adapter는 외부 기술과의 연결만을 책임진다. &lt;br /&gt;Usecase 패턴은 복잡한 서비스를 직관적으로 풀어내기에 현실적인 선택지인 것 같다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/Architecture</category>
      <category>Kotlin</category>
      <category>Spring</category>
      <category>개발자</category>
      <category>백엔드</category>
      <category>아키텍처</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/657</guid>
      <comments>https://sinclairstudio.tistory.com/657#entry657comment</comments>
      <pubDate>Thu, 18 Dec 2025 19:10:06 +0900</pubDate>
    </item>
    <item>
      <title>꽁꽁 얼어붙은 취업시장, 주니어 데이터 엔지니어의 이직 성공기</title>
      <link>https://sinclairstudio.tistory.com/656</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SI(시스템 통합) 경력을 만 2년 채우고 운 좋게 인하우스 개발 부서로 이동한 지 한 달 만에, 결국 이직을 결심하게 되었습니다. 사실 저는 정말 많이 떨어져 봤고, 이번에 처음으로 최종 합격이라는 결과를 받았습니다. 그동안 스스로를 의심하고, 초조하고 불안해하기도 했지만, 꾸준히 준비해서 원하는 결과를 만들어냈습니다. 이 글은 저의 이직 준비 과정을 회고하며, 같은 고민을 하는 누군가에게 조금이나마 도움이 되었으면 하는 마음으로 작성합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 이직을 결심한 사유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) (상대적으로) 더 보상체계가 훌륭한 곳을 가고 싶어서&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;2)&amp;nbsp;서비스&amp;nbsp;회사에서의&amp;nbsp;경험을&amp;nbsp;쌓고&amp;nbsp;싶어서&lt;/b&gt;&lt;br /&gt;SI&amp;nbsp;특성상&amp;nbsp;너무&amp;nbsp;빠르게&amp;nbsp;바뀌는&amp;nbsp;프로젝트&amp;nbsp;환경,&amp;nbsp;사람,&amp;nbsp;그리고&amp;nbsp;운영&amp;nbsp;및&amp;nbsp;유지보수&amp;nbsp;경험이&amp;nbsp;적었습니다.&lt;br /&gt;&lt;br /&gt;SI&amp;nbsp;회사에서&amp;nbsp;서비스&amp;nbsp;회사로&amp;nbsp;이직하고&amp;nbsp;싶다는&amp;nbsp;고민은&amp;nbsp;커뮤니티에서도&amp;nbsp;자주&amp;nbsp;보입니다.&amp;nbsp;저&amp;nbsp;역시&amp;nbsp;기술적으로&amp;nbsp;욕심이&amp;nbsp;생기면서&amp;nbsp;자연스럽게&amp;nbsp;이런&amp;nbsp;고민을&amp;nbsp;하게&amp;nbsp;됐습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;하지만&amp;nbsp;SI가&amp;nbsp;모두에게&amp;nbsp;나쁜&amp;nbsp;선택은&amp;nbsp;아닙니다.&lt;/b&gt;&lt;br /&gt;컨설팅에&amp;nbsp;관심이&amp;nbsp;있고,&amp;nbsp;외근을&amp;nbsp;즐기며&amp;nbsp;고객을&amp;nbsp;만나는&amp;nbsp;것을&amp;nbsp;좋아한다면&amp;nbsp;SI도&amp;nbsp;충분히&amp;nbsp;좋은&amp;nbsp;커리어가&amp;nbsp;될&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;실제로&amp;nbsp;SI에서&amp;nbsp;유명&amp;nbsp;외국계&amp;nbsp;빅테크&amp;nbsp;pre-sales&amp;nbsp;포지션으로&amp;nbsp;이직하는&amp;nbsp;사례도&amp;nbsp;종종&amp;nbsp;봤습니다.&lt;br /&gt;&lt;br /&gt;또한 SI에서도 기술적으로 성장할 수 있습니다. 저 역시 SI에서의 프로젝트 경험을 인정받아, 경력직 2~3년차 포지션에 합격할 수 있었습니다. 다만, 서비스 회사가 더 자율성이 보장되고, 엔지니어로 성장하기에 더 적합하다고 느꼈기에 이직을 결심했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.&amp;nbsp;이직&amp;nbsp;준비&amp;nbsp;과정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 무작위 지원의 실패&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 원티드 등에서 무작위로 이력서를 뿌렸지만, 결과는 좋지 않았습니다.&lt;br /&gt;커리어에&amp;nbsp;대한&amp;nbsp;고민&amp;nbsp;없이&amp;nbsp;회사&amp;nbsp;타이틀만&amp;nbsp;보고&amp;nbsp;외국계&amp;nbsp;빅테크&amp;nbsp;면접을&amp;nbsp;보기도&amp;nbsp;했으나,&amp;nbsp;중도에&amp;nbsp;그만뒀습니다.&amp;nbsp;아직&amp;nbsp;엔지니어라는&amp;nbsp;롤을&amp;nbsp;포기하고&amp;nbsp;싶지&amp;nbsp;않았던&amp;nbsp;것&amp;nbsp;같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. 이력서 정비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2년차쯤, 기존 노션 이력서를 블로그 글을 참고해 Google Docs로 재정비했습니다.&lt;br /&gt;포트폴리오와 이력서를 따로 만들었으나, 나중에는 조언을 받아 하나로 합쳤습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. 면접 전형 대비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 코딩테스트 준비&lt;br /&gt;2년 동안 리트코드(LeetCode)를 주 2문제씩 풀었습니다.&lt;br /&gt;알고리즘 스터디도 주 1회 진행하며 풀이를 공유했습니다.&lt;br /&gt;경력직&amp;nbsp;코딩테스트는&amp;nbsp;리트코드&amp;nbsp;이지~미디움&amp;nbsp;정도면&amp;nbsp;충분했습니다.&lt;br /&gt;&lt;br /&gt;(2)&amp;nbsp;기술&amp;nbsp;면접&amp;nbsp;준비&lt;br /&gt;CS 스터디를 2개월간 꾸준히 했습니다.&lt;br /&gt;지원 회사의 기술 스택(airflow, kafka 등)을 미리 공부하고, 직접 만들어보기도 했습니다.&lt;br /&gt;실시간&amp;nbsp;데이터&amp;nbsp;처리&amp;nbsp;등도&amp;nbsp;혼자&amp;nbsp;공부하며,&amp;nbsp;면접에서&amp;nbsp;대답할&amp;nbsp;수&amp;nbsp;있을&amp;nbsp;정도로&amp;nbsp;준비했습니다.&lt;br /&gt;&lt;br /&gt;(3)&amp;nbsp;실제&amp;nbsp;면접&amp;nbsp;경험&lt;br /&gt;여러 회사를 면접 보며 경험을 쌓았습니다.&lt;br /&gt;면접을 많이 보다 보면 긴장도 덜하게 되고, 실전 감각이 생깁니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-4. 맞는 회사에만 지원하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 이직에서는 무작위 지원 대신, 총 6~7곳만 신중하게 지원했습니다.&lt;br /&gt;지원&amp;nbsp;기준은&amp;nbsp;다음과&amp;nbsp;같습니다.&lt;br /&gt;&lt;br /&gt;- 붙었을 때 정말 가고 싶은 곳 (시리즈 B~D 스타트업, 유니콘, 대기업 계열사)&lt;br /&gt;- 클라우드 기반 데이터 플랫폼을 사용하는 회사&lt;br /&gt;- 내 경험(글로벌 시장, 일본 유학 등)이 어필될 수 있는 곳&lt;br /&gt;- 2~3년차 경력 포지션만 지원&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.&amp;nbsp;나만의&amp;nbsp;이직&amp;nbsp;준비&amp;nbsp;팁&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1.&amp;nbsp;자존감&amp;nbsp;관리가&amp;nbsp;가장&amp;nbsp;중요&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속&amp;nbsp;떨어지다&amp;nbsp;보면&amp;nbsp;자존감이&amp;nbsp;낮아지기&amp;nbsp;쉽습니다.&lt;br /&gt;하지만,&amp;nbsp;내가&amp;nbsp;한&amp;nbsp;일에&amp;nbsp;자부심을&amp;nbsp;갖는&amp;nbsp;것이&amp;nbsp;정말&amp;nbsp;중요합니다.&lt;br /&gt;자신감이&amp;nbsp;있어야&amp;nbsp;이력서에도,&amp;nbsp;면접에서도&amp;nbsp;긍정적인&amp;nbsp;인상을&amp;nbsp;줄&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&lt;br /&gt;예를 들어, &amp;lsquo;나는&amp;nbsp;SI에서&amp;nbsp;별거&amp;nbsp;아닌&amp;nbsp;걸&amp;nbsp;개발하는&amp;nbsp;것&amp;nbsp;같아&amp;hellip;&amp;rsquo;&amp;nbsp;보다는&lt;br /&gt;&amp;lsquo;나는&amp;nbsp;고객사에게&amp;nbsp;감사&amp;nbsp;메일을&amp;nbsp;받을&amp;nbsp;정도로&amp;nbsp;기여했고,&amp;nbsp;누군가에게&amp;nbsp;꼭&amp;nbsp;필요한&amp;nbsp;전문가야!&amp;nbsp;가치를&amp;nbsp;만들고&amp;nbsp;있어!&amp;rsquo;&lt;br /&gt;이런 긍정적인 마인드셋이 무의식적으로 이력서나 면접 태도에 녹아 드는 것 같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2. 타협하지 않는 자세&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순간순간 '쉬운 길'의 유혹이 찾아올 때, 타협하지 않았던 것이 결국 원하는 결과를 만들어냈습니다. 앞으로도 행복한 엔지니어 커리어를 이어가고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;Disclaimer&lt;/b&gt;&lt;br /&gt;이 글은 특정 회사를 비판하거나 폄하하려는 의도가 전혀 없습니다.&lt;br /&gt;오직 저의 개인적인 커리어 고민과 성장 과정에 대한 회고이며, 비슷한 길을 걷고 있는 분들에게 도움이 되기를 바라는 마음으로 작성되었습니다.&lt;br /&gt;모든 선택은 사람마다 다르며, 각자의 환경과 가치관에 따라 최고의 길은 달라질 수 있다고 생각합니다.&lt;/p&gt;</description>
      <category>개발일기/개발자 취준생</category>
      <category>SI</category>
      <category>개발자</category>
      <category>개발자이력서</category>
      <category>개발자이직</category>
      <category>개발자커리어</category>
      <category>데이터엔지니어</category>
      <category>데이터엔지니어링</category>
      <category>데이터엔지니어이직</category>
      <category>데이터엔지니어취업</category>
      <category>서비스회사</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/656</guid>
      <comments>https://sinclairstudio.tistory.com/656#entry656comment</comments>
      <pubDate>Thu, 22 May 2025 00:06:06 +0900</pubDate>
    </item>
    <item>
      <title>[Algorithm] 정렬 알고리즘 정리하기 (1)</title>
      <link>https://sinclairstudio.tistory.com/655</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;정렬 알고리즘이 중요한 이유&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 알고리즘에서 정렬은 필수 전처리 단계로 사용된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;정렬된 데이터는 이진 탐색처럼 빠른 탐색 알고리즘을 사용할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;사람이나 시스템이 데이터를 해석하기 더 쉬워진다. ex. 시간순 , 크기 순, 알파벳 순&lt;/li&gt;
&lt;li&gt;정렬을 통해서 중복된 값들을 모아 놓을 수 있으므로, 효율적으로 중복 제거를 할 수 있으며 그룹 처리에 유리하다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버블 정렬 Bubble Sort&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버블 정렬은 한번 순회할때 정렬되지 않은 값들중에서 가장 큰 값을 찾아서 맨 뒤로 보낸다. 맨 첫번째 정렬 시도에서는 가장 큰 값을 찾아서 배열의 맨 뒤로 보내고, 두번째 정렬시도에서는 두번째로 큰 값을 찾아서 맨 뒤에서 두번째로 보낸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 공간 복잡도 : O(1)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도의 추가 공간 없이 주어진 배열 안에서 크기 비교와 swap 이 일어난다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 시간 복잡도 : O(N**2)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 원소에 접근해야 하므로 O(N)이 걸리며, 한번의 루프에서 이웃 값들을 대소 비교하며 swap해 나가야 하므로 여기서 또 O(N)의 시간 복잡도가 걸리게 된다. 따라서 최종적으로는 O(N**2)의 시간복잡도가 걸리게 된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746113183292&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;arr = [6, 5, 1, 7, 2, 3, 9, 8, 4]

def bubble_sort(arr):
    for i in range(len(arr) - 1, 0, -1):
        for j in range(i): # 0, 1, 2, 3 | 0, 1, 2 | 0, 1 | 0
            if arr[j] &amp;gt; arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
    
print(&quot;Unsorted numbers:&quot;, arr) # Unsorted numbers: [6, 5, 1, 7, 2, 3, 9, 8, 4]
bubble_sort(arr)
print(&quot;Sorted numbers&quot;, arr) # Sorted numbers [1, 2, 3, 4, 5, 6, 7, 8, 9]&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/python-program-for-bubble-sort/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.geeksforgeeks.org/python-program-for-bubble-sort/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Programming Languages/Python</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/655</guid>
      <comments>https://sinclairstudio.tistory.com/655#entry655comment</comments>
      <pubDate>Fri, 2 May 2025 00:30:57 +0900</pubDate>
    </item>
    <item>
      <title>[MLOps] MLOps 첫 경험기 - ADF, Databricks, MLflow를 이용한 파이프라인 구축</title>
      <link>https://sinclairstudio.tistory.com/654</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Intro&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 ETL 파이프라인 구축 중심의 프로젝트를 해왔는데, 이번에는 처음으로 &lt;b&gt;MLOps 파이프라인&lt;/b&gt;을 다뤄보게 되었다. 약 &lt;b&gt;2개월간 진행된 프로젝트&lt;/b&gt;를 마치며, 그 과정에서 얻은 기술적인 인사이트와 소프트 스킬에 대한 회고를 남겨본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MLOps 오케스트레이션 - Data Factory&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zez3z/btsMZNHaDOx/3PmlZUNRYOZU03LkK0cqPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zez3z/btsMZNHaDOx/3PmlZUNRYOZU03LkK0cqPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zez3z/btsMZNHaDOx/3PmlZUNRYOZU03LkK0cqPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzez3z%2FbtsMZNHaDOx%2F3PmlZUNRYOZU03LkK0cqPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;311&quot; height=&quot;162&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 MLOps 파이프라인을 오케스트레이션하기 위해 &lt;b&gt;Azure Data Factory (ADF)&lt;/b&gt; 를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure 공식 문서에 따르면 ADF는 &lt;b&gt;복잡한 하이브리드 ETL, ELT 및 데이터 통합 작업을 위한 완전 관리형 클라우드 서비스&lt;/b&gt;다. 일반적으로 &lt;b&gt;Apache Airflow&lt;/b&gt;와 비교되곤 하는데, 두 도구 모두 워크플로우 오케스트레이션 도구로서 데이터 이동 및 처리 파이프라인 구축, 배치 파이프라인 스케줄링, 의존성 관리, 작업 실패에 대한 알림 및 재시도 설정 등의 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 도구의 주요 차이점은 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;720&quot; data-start=&quot;590&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;659&quot; data-start=&quot;590&quot;&gt;&lt;b&gt;ADF&lt;/b&gt;: 완전한 Azure 관리형 서비스로, GUI 기반의 드래그 앤 드롭 방식으로 파이프라인을 구성할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;720&quot; data-start=&quot;660&quot;&gt;&lt;b&gt;Airflow&lt;/b&gt;: 코드 기반 오케스트레이션 도구로, Python으로 유연한 커스터마이징이 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EkHgS/btsM01ZcMxi/kBh6a9Km3RGk95s4dBi58K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EkHgS/btsM01ZcMxi/kBh6a9Km3RGk95s4dBi58K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EkHgS/btsM01ZcMxi/kBh6a9Km3RGk95s4dBi58K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEkHgS%2FbtsM01ZcMxi%2FkBh6a9Km3RGk95s4dBi58K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;708&quot; height=&quot;219&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터 플랫폼이 Azure 기반(Azure SQL, Storage, Databricks 등)이었기 때문에 &lt;b&gt;Azure 생태계와의 통합이 쉬운 ADF를 선택&lt;/b&gt;하게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;1027&quot; data-start=&quot;823&quot; data-ke-size=&quot;size16&quot;&gt;직접 사용해본 결과, &lt;b&gt;Airflow처럼 100% 코드 기반은 아니기 때문에 유연성이 다소 떨어지는 점&lt;/b&gt;이 아쉬웠다. 원하는 흐름을 만들기 위해 다양한 Activity를 조합하며 &lt;b&gt;타협점을 찾아야 했다는 점&lt;/b&gt;도 도전이었다. 반면, ADF는 &lt;b&gt;중앙집중식으로 파이프라인을 관리&lt;/b&gt;할 수 있으며, &lt;b&gt;MS 기반 서비스 간의 통합&lt;/b&gt;이 자연스럽다는 장점도 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온프레미스에서 클라우드 환경으로 마이그레이션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 마이그레이션을 하게 된 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온프레미스에서 잘 운영되던 프로그램을 &lt;b&gt;클라우드 환경으로 이전&lt;/b&gt;한 이유는, &lt;b&gt;중앙집중식 관리&lt;/b&gt;를 위한 선택이었다. 특히 &lt;b&gt;Databricks라는 데이터 플랫폼을 도입&lt;/b&gt;하면서, 전체 데이터 파이프라인과 MLOps, 분석 실험을 &lt;b&gt;ADF와 Databricks 기반으로 한 곳에서 관리&lt;/b&gt;하는 방향으로 전환하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로그램은 &lt;b&gt;AWS Forecast&lt;/b&gt; 서비스를 활용해 API 호출 방식으로 데이터를 학습시키고 예측 결과를 결과 테이블에 저장하는 방식이었다. 자체적인 모델 학습 및 배포가 필요한 구조가 아니었기 때문에, 클라우드 환경으로 이전하더라도 &lt;b&gt;리소스 부담이 크지 않았다&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 마이그레이션 시 고려한 사항&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Windows .exe 파일을 Databricks에서 실행하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 시스템은 &lt;b&gt;Windows 환경&lt;/b&gt;에서 .exe 형태로 만들어진 Python 프로그램을 &lt;b&gt;분석가가 수동 실행&lt;/b&gt;하는 방식이었다. 그러나 Databricks는 &lt;b&gt;리눅스 기반&lt;/b&gt;이므로 .exe 파일은 사용할 수 없다. 이를 해결하기 위해, python -m 명령어로 main 모듈을 실행하는 방식으로 전환하여 &lt;b&gt;패키지 구조를 유지하면서도 실행 가능&lt;/b&gt;하도록 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 패키지 구조를 보존하면서 모듈을 실행시킬 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1743212411642&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import subprocess

subprocess.run([&quot;python&quot;, &quot;-m&quot;, &quot;mypackage&quot;])&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Python 패키지 버전 호환 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제 변경에 따라 &lt;b&gt;패키지 버전 호환성&lt;/b&gt; 문제가 발생했다. 이에 따라 필요한 패키지 버전을 재정비하여 &lt;b&gt;Linux 환경에 맞게 설정&lt;/b&gt;해야 했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;반복 작업 자동화 및 모니터링 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Windows 시스템에서 스케줄링을 설정했지만, &lt;b&gt;실제 작동 여부를 수동으로 모니터링&lt;/b&gt;해야 했다. 주말에는 &lt;b&gt;약 10시간 동안 모니터링&lt;/b&gt;을 해야 하는 불편함도 있었다.&lt;/p&gt;
&lt;h4 data-end=&quot;1992&quot; data-start=&quot;1970&quot; data-ke-size=&quot;size20&quot;&gt;이벤트 기반 처리 고려&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Forecast의 상태값이 업데이트될 때 이를 감지하여 &lt;b&gt;이벤트 기반 파이프라인&lt;/b&gt;으로 구성하면 효율적이었겠지만, 운영 인력의 한계와 새로운 기술에 대한 거부감으로 인해 &lt;b&gt;도입이 어려웠다&lt;/b&gt;.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작업 상태 조회 로직 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 기반 처리가 어려운 상황이었기에, &lt;b&gt;MLOps 로그 테이블을 활용&lt;/b&gt;하여 파이프라인 실행 전 &lt;b&gt;작업 상태를 조회하고 분기처리&lt;/b&gt;하는 SQL 로직을 도입했다.&lt;/p&gt;
&lt;h4 data-end=&quot;2249&quot; data-start=&quot;2228&quot; data-ke-size=&quot;size20&quot;&gt;클러스터 비용 최적화&lt;/h4&gt;
&lt;p data-end=&quot;2393&quot; data-start=&quot;2250&quot; data-ke-size=&quot;size16&quot;&gt;클러스터 사용 시 비용 효율성도 중요한 고려사항이었다. 처음엔 작업을 10번 반복하고 1시간 대기하는 방식으로 설계했으나, &lt;b&gt;불필요한 리소스 낭비가 발생&lt;/b&gt;했다. 이후 작업 상태에 따라 &lt;b&gt;유연하게 분기 처리&lt;/b&gt;하는 방식으로 개선하여 비용을 절감했다.&lt;/p&gt;
&lt;p data-end=&quot;2393&quot; data-start=&quot;2250&quot; data-ke-size=&quot;size16&quot;&gt;이러한 고민과 실험을 통해 &lt;b&gt;ADF의 Activity 블록&lt;/b&gt; 기반으로 파이프라인을 아래와 같이 설계하게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boJGjW/btsM2FmPDwE/tHHTGLgQzIWxwpqE2TIvY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boJGjW/btsM2FmPDwE/tHHTGLgQzIWxwpqE2TIvY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boJGjW/btsM2FmPDwE/tHHTGLgQzIWxwpqE2TIvY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboJGjW%2FbtsM2FmPDwE%2FtHHTGLgQzIWxwpqE2TIvY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;283&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MLflow 의 적용&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5Rltb/btsMZ3QwBhz/HAsaf9Qm2etrqkjmBA2251/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5Rltb/btsMZ3QwBhz/HAsaf9Qm2etrqkjmBA2251/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5Rltb/btsMZ3QwBhz/HAsaf9Qm2etrqkjmBA2251/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5Rltb%2FbtsMZ3QwBhz%2FHAsaf9Qm2etrqkjmBA2251%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;383&quot; height=&quot;148&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이번 프로젝트에서는 모든 파이프라인에 MLflow를 바로 적용한 것은 아니었다.&lt;br /&gt;&lt;b&gt;모델 학습 자체를 AWS Forecast API를 통해 수행하는 파이프라인의 경우&lt;/b&gt;, 내부에서 학습 로직이 수행되지 않기 때문에 MLflow를 적용하지 않았다. 하지만 또 다른 파이프라인에서는 &lt;b&gt;Databricks 노트북 환경에서 직접 모델을 생성하고 학습 및 추론까지 진행&lt;/b&gt;하는 방식이었고, 이에 따라 &lt;b&gt;MLflow&lt;/b&gt;를 효과적으로 적용해볼 수 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MLflow 의 주요 기능&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Experiments Tracking&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 학습 과정에서 발생하는 다양한 &lt;b&gt;지표(metrics)&lt;/b&gt; 와 &lt;b&gt;하이퍼파라미터(params)&lt;/b&gt; 를 기록하여, 실험을 비교하고 재현할 수 있게 해준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Model Registry&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습된 모델을 &lt;b&gt;등록, 버전 관리, 배포 상태 관리(Staging, Production 등)&lt;/b&gt; 할 수 있는 기능으로, 협업 시나 운영 환경과의 연계를 보다 체계적으로 구성할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파이프라인에 MLflow 적용한 방식&amp;nbsp;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Experiments 로깅&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모델 학습 후, 정확도 등의 성능 지표(metrics) 를 MLflow에 기록하여 실험별 성능 비교가 가능하도록 구성했다.&lt;/li&gt;
&lt;li&gt;또한, 학습에 사용된 하이퍼파라미터도 함께 로깅함으로써, 추후 파리미터에 대한 성능 비교가 가능하도록 구성했다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MLflow 공식 도큐먼트에서 가져온 예시 코드는 아래와 같다. 실제로도 log_params, log_metric, log_model 함수를 사용해서 실험을 로깅했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743216071928&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Start an MLflow run
with mlflow.start_run():
    # Log the hyperparameters -&amp;gt; 하이퍼 파라미터 로깅
    mlflow.log_params(params)

    # Log the loss metric -&amp;gt; 메트릭 로깅
    mlflow.log_metric(&quot;accuracy&quot;, accuracy)

    # Set a tag that we can use to remind ourselves what this run was for
    mlflow.set_tag(&quot;Training Info&quot;, &quot;Basic LR model for iris data&quot;)

    # Infer the model signature
    signature = infer_signature(X_train, lr.predict(X_train))

    # Log the model
    model_info = mlflow.sklearn.log_model(
        sk_model=lr,
        artifact_path=&quot;iris_model&quot;,
        signature=signature,
        input_example=X_train,
        registered_model_name=&quot;tracking-quickstart&quot;,
    )&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Artifact 저장&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습된 모델에 대한 pickle 파일, output 파일을 artifact로 저장했다. 실험별로 모델 파일을 관리할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이후 필요하다면 Model Registry 를 활용하여, 배포 관리를 (Staging, Production) 가능하도록 확장할 수 있을 것이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보완할 수 있는 점&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 현재는 &lt;b&gt;Random Forest 모델을 새로운 데이터로 매번 재학습&lt;/b&gt;시키고 있다. 리소스 부담이 크지 않아 실용적인 선택이지만, 데이터가 커질 경우에는 &lt;b&gt;모델을 재사용하거나 부분 학습 방식(Incremental Learning)&lt;/b&gt; 으로 변경할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 아직은 &lt;b&gt;Model Registry를 통한 운영 배포 흐름은 적용하지 않았지만&lt;/b&gt;, 향후에 모델 검증, 승인을 추가하여 운영 환경에 자동 반영하는 구조로 확장할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트를 마무리하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 &lt;b&gt;처음으로 MLOps 파이프라인을 실제로 배포&lt;/b&gt;해보며, 모델 학습과 추론 등 MLOps에 필요한 여러 단계들을 직접 경험할 수 있었던 값진 시간이었다. 특히 &lt;b&gt;MLflow를 실무에 적용&lt;/b&gt;해보면서, 모델의 버전 관리나 배포 단계를 체계적으로 관리할 수 있는 도구라는 것을 체감할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다 이번 프로젝트는 다른 프로젝트들과 달리 &lt;b&gt;혼자서 투입되어 고객과 직접 소통하며 프로젝트를 리드&lt;/b&gt;해야 했다는 점에서 많은 도전이 있었다.&lt;br /&gt;분석가와 협업하면서 &lt;b&gt;ML 파이프라인의 전체 흐름을 이해&lt;/b&gt;하고, 관련된 테이블을 확인하며 요구사항을 하나씩 정리해나갔다. 또, 클라우드 환경에서는 비용 이슈가 민감하다 보니, &lt;b&gt;작업 효율성과 비용 절감을 동시에 고려&lt;/b&gt;하며 설계에 대한 고민도 많이 하게 되었다.개인적으로는, 이번 기회를 통해 &lt;b&gt;자율성과 책임을 동시에 경험&lt;/b&gt;했다는 점에서 큰 의미가 있었다. 이전에는 &amp;ldquo;내가 이 정도까지 의견을 내도 될까?&amp;rdquo; 하는 고민이 많았지만, 이번 경험을 통해 &lt;b&gt;앞으로는 기술적으로 현재 상황에 타당하다고 생각이 된다면 더 주도적으로 방향을 제시하고 의견을 낼 수 있겠다는 자신감&lt;/b&gt;이 생겼다. 혼자 리딩하는 것이 힘들었던 만큼 소프트 스킬적으로 많이 성장할 수 있었다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Data Engineering/MLOps</category>
      <category>MLflow</category>
      <category>mlops</category>
      <category>데이터엔지니어</category>
      <category>데이터엔지니어링</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/654</guid>
      <comments>https://sinclairstudio.tistory.com/654#entry654comment</comments>
      <pubDate>Sat, 29 Mar 2025 10:56:19 +0900</pubDate>
    </item>
    <item>
      <title>[Spark] Spark Data Skew의 발생 원인과 해결방법</title>
      <link>https://sinclairstudio.tistory.com/653</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pOXWZ/btsMMfW2B4d/wNhKNIS8r8ADwlPvtXBvN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pOXWZ/btsMMfW2B4d/wNhKNIS8r8ADwlPvtXBvN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pOXWZ/btsMMfW2B4d/wNhKNIS8r8ADwlPvtXBvN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpOXWZ%2FbtsMMfW2B4d%2FwNhKNIS8r8ADwlPvtXBvN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;448&quot; height=&quot;233&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spark Data Skew 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark 클러스터에서, &lt;b&gt;Data Skew 는 특정 키 또는 파티션에 데이터가 쏠려서 불균형&lt;/b&gt;이 일어나는 현상이다. 여기서 특정 키 (Key) 라는 의미는 주로 Join, GroupBy, Aggregation 같은 연산에서 특정 키에 과도한 데이터가 집중되는 것을 의미한다. 또한 파티션 (Partition) 이란, Spark 가 데이터를 나누어 저장하고 처리하는 &lt;u&gt;최소 단위&lt;/u&gt;이다. Spark 는 각 파티션을 개별 태스크에서 처리하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data Skew가 발생하면 다음과 같은 문제가 발생할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OOM (Out of Memory)&lt;/b&gt; : 특정 파티션에 과도하게 데이터가 몰리게 되면, 해당 파티션을 처리하는 태스크(Task) 가 많은 메모리를 소비하게 된다. Spark 는 기본적으로 JVM 메모리를 사용하여 연산을 수행하는데, Data Skew가 발생하게 되면 Spark 가 모든 메모리에 데이터를 유지하지 못하고 JVM Heap 메모리가 부족해져서, OOM 이 발생하여 &lt;u&gt;Spark 어플리케이션이 비정상적으로 종료&lt;/u&gt;된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전체 작업 지연&lt;/b&gt; : 특정 태스크 (Task)에 불균형한 데이터가 할당되어 다른 태스크보다 훨씬 비정상적으로 오래 실행되게 된다. Spark 는 기본적으로 모든 태스크가 끝나야 다음 스테이지로 넘어가게 된다. 따라서 이런 태스크가 병목이 되어 &lt;u&gt;전체 작업이 지연&lt;/u&gt;되는 원인이 된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Disk Spilling&lt;/b&gt; : Spark 는 기본적으로는 메모리 내에서 연산을 수행한다. 그렇지만, 메모리가 부족하면 데이터의 일부를 디스크에 저장하게 된다. 즉 데이터가 넘쳐서 메모리에서 디스크로 엎질러지게 (Spilling) 된다. 디스크에 데이터를 저장하게 되면 디스크에서 다시 읽어와야 하므로 IO 오버헤드가 증가하여 성능이 저하된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xYy6s/btsMMCdn6Og/bky7gRYCNxaft0qzLEq8B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xYy6s/btsMMCdn6Og/bky7gRYCNxaft0qzLEq8B1/img.png&quot; data-alt=&quot;JVM 에서의 메모리 영역은 위와 같다. 메모리 영역이 제대로 관리되지 않는 경우, Spark 어플리케이션이 비정상 종료될 수 있다. 출처 :&amp;amp;amp;nbsp;https://www.devkuma.com/docs/jvm/memory-structure/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xYy6s/btsMMCdn6Og/bky7gRYCNxaft0qzLEq8B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxYy6s%2FbtsMMCdn6Og%2Fbky7gRYCNxaft0qzLEq8B1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;607&quot; height=&quot;331&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JVM 에서의 메모리 영역은 위와 같다. 메모리 영역이 제대로 관리되지 않는 경우, Spark 어플리케이션이 비정상 종료될 수 있다. 출처 :&amp;amp;nbsp;https://www.devkuma.com/docs/jvm/memory-structure/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. Data Skew 의 원인&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터의 불균형한 분산 : 실제 real-world 에서는, 데이터가 항상 고르게 분포하지 않은 경우가 더 많다. 데이터가 편향된 외부 소스에서 유입되는 경우 이미 데이터가 불균형한 상태일 수 있다. 2.1 의 예시와 비슷한 경우이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Join 연산 /&lt;span&gt;&amp;nbsp;&lt;/span&gt;GroupBy 연산&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Join 연산이 발생할 때, 특정 key 에 데이터가 집중되면, 해당 key를 처리하는 태스크에 과부하가 발생할 수 있다. 특히 &lt;b&gt;Shuffle 이 발생하는 Join 연산&lt;/b&gt;에서 문제가 될 수 있으며, 반면 Broadcast Join 을 사용하는 경우에는 Data Skew 문제를 완화할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;기본 파티셔닝 전략 문제 : Spark 는 기본적으로 Hash Partitioning 을 사용하는데, 일부 키가 해시 충돌을 일으키거나 데이터가 균등하지 않으면 특정 파티션이 커질 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Data Skew 가 일어났는지 확인하기&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 특정 컬럼 (Key) 값의 데이터 개수 분포를 확인&lt;/h3&gt;
&lt;pre id=&quot;code_1742089774994&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pyspark.sql.functions import count, col


# 샘플 데이터 생성
data = [
    (&quot;A&quot;, 10), (&quot;A&quot;, 20), (&quot;A&quot;, 30), (&quot;A&quot;, 40), (&quot;A&quot;, 50),  # 'A' 값이 많음 (Data Skew)
    (&quot;B&quot;, 60), (&quot;C&quot;, 70), (&quot;D&quot;, 80), (&quot;E&quot;, 90)  # 다른 키들은 데이터가 적음
]

df = spark.createDataFrame(data, [&quot;skewed_column&quot;, &quot;value&quot;])

# 특정 키별 데이터 개수 확인
df.groupBy(&quot;skewed_column&quot;).agg(count(&quot;*&quot;).alias(&quot;count&quot;)).orderBy(col(&quot;count&quot;).desc()).show()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 라는 key 값이 다른 값들보다 많으므로 이런 경우 Data Skew 발생 가능성이 높은것으로 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742089808717&quot; class=&quot;asciidoc&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;+-------------+-----+
| skewed_column |count|
+-------------+-----+
|           A |   5 |
|           B |   1 |
|           C |   1 |
|           D |   1 |
|           E |   1 |
+-------------+-----+&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 파티션 별 데이터 개수를 확인하기&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1742089879488&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df = df.repartition(4, &quot;skewed_column&quot;)  # 4개 파티션으로 분할

# 각 파티션의 데이터 개수 확인
df.rdd.mapPartitions(lambda partition: [len(list(partition))]).collect()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 파티션에만 데이터가 집중되어 있으므로, 과부하 발생 가능성이 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742089898634&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[1, 1, 1, 5]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Data Skew 의 원인&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터의 불균형한 분산 : 실제 real-world 에서는, 데이터가 항상 고르게 분포하지 않은 경우가 더 많다. 데이터가 편향된 외부 소스에서 유입되는 경우 이미 데이터가 불균형한 상태일 수 있다. 2.1 의 예시와 비슷한 경우이다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Join 연산 / GroupBy 연산 : Join 연산이 발생할 때, 특정 key 에 데이터가 집중되면, 해당 key를 처리하는 태스크에 과부하가 발생할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Data Skew 핸들링하기&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 AQE (Adaptive Query Execution) 의 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AQE 는 Adaptive Query Execution 으로, Spark 3.0 버전 이후부터 동적으로 최적화 작업을 해주는 프레임 워크이다. Spark 3.2부터는 SQE enabled 설정이 디폴트로 True가 되었다. AQE 는 shuffle 이 끝난 이후 partition 을 적절하게 병합해주는 기능을 한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742090329455&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spark.conf.set(&quot;spark.sql.adaptive.enabled&quot;, True)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AQE는 다음과 같은 기능을 제공한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Skewed Join Optimization&lt;/b&gt; : &lt;u&gt;최적화된 Join 을 적용하는 기능이다.&lt;/u&gt; Join 연산에서 특정 키에 데이터가 집중되면, AQE가 자동으로 작은 파티션으로 분리하여 최적화한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dynamic Coalescing&lt;/b&gt; : Partition의 수를 줄여주는 기능이다. 너무 많은 Partition 은 많은 Task 를 필요하거나 I/O 를 발생시킨다. (1 Partition = 1 Task). Spark 연산 실행 중 작은 파티션을 합쳐서 병렬처리를 최적화한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 Repartitioning&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark 의 디폴트 파티셔닝 값에서 직접 repartitioning 을 통해서, 파티션을 조정할 수 있다. 비교적 고르게 분산된 numeric한 key 값 컬럼을 이용하여 repartitioning 하는 전략을 세울 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742090853466&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df = df.repartition(100, &quot;column_a&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정한 key (column) 을 기준으로 데이터를 다시 파티셔닝해서 특정 노드에 부하가 집중되는 현상을 완화할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 Join 시 salting 기법&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Join 연산 시에 특정 key 에 데이터가 몰리는 경우, Salting 기법을 사용해서 데이터 분포를 인위적으로 균등하게 만드는 방법이다. 새로운 salt 작업용 컬럼을 추가하고, 균등하게 분포되도록 값을 지정해준다. 그리고 join 연산에 join 할 대상 컬럼과 salting 된 컬럼을 포함하여 조인한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742090996544&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pyspark.sql.functions import monotonically_increasing_id

# Salting 기법 적용 - salt 라는 컬럼을 새로 추가한다. 
# salt 컬럼에 들어가는 값은 골고루 분포하도록 인위적으로 조정한다. 
def add_salt(df, column, salt_range=10):
    return df.withColumn(&quot;salt&quot;, (monotonically_increasing_id() % salt_range))

fact_df = add_salt(fact_df, &quot;join_key&quot;)
dim_df = add_salt(dim_df, &quot;join_key&quot;)

# Salting된 컬럼을 포함하여 조인
joined_df = fact_df.join(dim_df, [&quot;join_key&quot;, &quot;salt&quot;], &quot;inner&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 Join 시 Broadcasting Join 기법 사용하기&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spark broadcasting join 을 사용하는 경우, 작은 테이블을 모든 spark worker node에 복제하게 된다. 따라서 shuffle 현상을 방지해서 data skew를 방지할 수 있다. 단, 대상 테이블이 작을 경우에 효과적이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742091097005&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from pyspark.sql.functions import broadcast

joined_df = fact_df.join(broadcast(dim_df), &quot;join_key&quot;, &quot;inner&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SpOoA/btsMLsQuMfU/q9sKuwzcGjzg7OQRdeoc30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SpOoA/btsMLsQuMfU/q9sKuwzcGjzg7OQRdeoc30/img.png&quot; data-alt=&quot;broadcast join&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SpOoA/btsMLsQuMfU/q9sKuwzcGjzg7OQRdeoc30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSpOoA%2FbtsMLsQuMfU%2Fq9sKuwzcGjzg7OQRdeoc30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;250&quot; height=&quot;258&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;broadcast join&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 결론&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Data Skew 는 Spark 작업 성능을 저하시키는 중요한 문제 중 하나여서, 관련된 Spark 튜닝 기법에 대해 정리해보았다. Data Skew 현상을 잘 해결해야 Spark 분산처리의 이점을 극대화시킬 수 있다.&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;size18&quot;&gt;&lt;b&gt;Reference&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@diehardankush/how-to-understanding-data-skewness-in-apache-spark-9e93b9a68f46&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/@diehardankush/how-to-understanding-data-skewness-in-apache-spark-9e93b9a68f46&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.kakao.com/posts/489&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tech.kakao.com/posts/489&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Data Engineering/ Apache Spark</category>
      <category>data skew</category>
      <category>SPARK</category>
      <category>spark aqe</category>
      <category>spark data skew</category>
      <category>spark join</category>
      <category>spark 조인</category>
      <category>데이터 쏠림</category>
      <category>분산처리</category>
      <category>스파크</category>
      <category>스파크 데이터 스큐</category>
      <author>minjiwoo</author>
      <guid isPermaLink="true">https://sinclairstudio.tistory.com/653</guid>
      <comments>https://sinclairstudio.tistory.com/653#entry653comment</comments>
      <pubDate>Sun, 16 Mar 2025 11:02:01 +0900</pubDate>
    </item>
  </channel>
</rss>