<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>byhy0 님의 블로그</title>
    <link>https://byhy0.tistory.com/</link>
    <description>byhy0 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 14 Jul 2026 12:56:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>byhy0</managingEditor>
    <image>
      <title>byhy0 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/7469509/attach/948c165146b14fd6b4587cf99f0468fd</url>
      <link>https://byhy0.tistory.com</link>
    </image>
    <item>
      <title>[Python] 성능 병목을 찾는 방법 '프로파일링'</title>
      <link>https://byhy0.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 기반 서비스를 개발하다 보면 한 번쯤 이런 상황을 만납니다. 사용자가 질문을 입력했는데 응답까지 15초가 걸립니다. 느리다는 건 알겠는데 왜 느린지는 모르는 상황입니다. LLM이 문제일까요, 벡터 DB 검색이 문제일까요, 아니면 다른 곳일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 필요한 것이 &lt;b&gt;프로파일링(Profiling)&lt;/b&gt; 입니다. 이 글에서는 프로파일링이 무엇인지, 그리고 실무에서 자주 사용하는 두 도구인 py-spy와 pyinstrument를 어떤 상황에서 어떻게 활용하는지 정리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로파일링이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로파일링은 프로그램이 실행되는 동안 시간과 자원이 어디에서 얼마나 소비되는지 측정하는 과정입니다. 단순히 전체 실행 시간을 재는 것이 아니라, 어느 함수가 얼마나 오래 실행됐는지, CPU를 얼마나 사용했는지, 어디서 대기가 발생하는지를 함수 단위로 파악합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅과 혼동하기 쉬운데 둘은 목적이 다릅니다. 디버깅은 프로그램이 올바르게 동작하지 않는 원인을 찾는 과정이고, 프로파일링은 프로그램은 정상적으로 동작하지만 왜 느린지, 어디가 성능 병목(Bottleneck)인지를 찾는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병원에 비유하면 이렇습니다. 프로젝트는 사람의 몸이고, FastAPI나 LangGraph 같은 각 컴포넌트는 심장이나 폐 같은 장기입니다. 프로파일링 도구는 그 장기들을 들여다보는 CT나 MRI 장비입니다. CT가 몸의 일부가 아닌 것처럼, 프로파일링 도구도 서비스를 구성하는 컴포넌트가 아니라 전체를 관찰하는 진단 도구입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 cProfile 대신 다른 도구를 쓰는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에는 표준 프로파일러인 cProfile이 내장되어 있습니다. 그런데 실무에서는 py-spy나 pyinstrument를 더 많이 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cProfile의 가장 큰 한계는 운영 서버에서 사용하기 어렵다는 점입니다. 코드를 수정해야 하고, 프로그램을 재실행해야 하며, 이미 돌아가고 있는 프로세스를 분석할 수 없습니다. 반면 py-spy는 실행 중인 프로세스에 외부에서 붙어서 분석할 수 있어 운영 서버에서도 사용이 가능합니다. pyinstrument는 결과를 함수 호출 트리(Call Tree) 형태로 보여줘서 병목을 한눈에 파악하기 쉽습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;py-spy: 운영 서버의 상태를 실시간으로 들여다보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;py-spy는 실행 중인 Python 프로세스를 외부에서 관찰하는 프로파일러입니다. 코드를 수정하거나 서비스를 중단하지 않아도 되기 때문에 운영 환경에서 장애가 발생했을 때 특히 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;py-spy top 명령어는 현재 각 함수가 CPU를 얼마나 사용하고 있는지를 실시간으로 보여줍니다. 아래처럼 프로세스 ID(PID)를 지정하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# 실행 중인 Python 프로세스의 PID 확인
ps aux | grep python

# py-spy로 해당 프로세스 분석 (PID: 12345 예시)
py-spy top --pid 12345
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 다음과 같은 결과를 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Function              CPU 사용률
call_openai()         70%
embedding()           12%
json.dumps()           5%
redis.get()            3%
others                10%
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;py-spy dump 명령어를 사용하면 현재 프로그램이 어느 함수에서 멈춰 있는지 호출 스택을 확인할 수 있습니다. waiting... 상태가 보이면 그 지점이 대기 구간입니다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;py-spy dump --pid 12345
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Thread 1
graph.invoke()
  └── agent()
        └── tool()
              └── call_openai()
                    └── waiting...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;py-spy record 명령어는 Flame Graph(불꽃 그래프)를 생성합니다. 가로 폭이 넓을수록 그 함수가 실행 시간을 많이 차지했다는 의미입니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;call_openai()   ████████████████████
embedding()     █████████
json.dumps()    ███
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;py-spy는 응급실 모니터와 비슷합니다. 이미 움직이고 있는 환자를 멈추지 않고 실시간으로 상태를 확인하는 것처럼, 운영 중인 서비스를 중단하지 않고 어디에서 시간이 소비되는지 관찰할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pyinstrument: 개발 중 함수별 실행 시간을 트리로 확인하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyinstrument는 Python 코드 내부에서 함수가 얼마나 오래 실행됐는지를 트리 형태로 보여주는 프로파일러입니다. 코드에 몇 줄을 추가해야 하지만, 그 결과가 사람이 읽기 쉬운 형태로 출력된다는 것이 가장 큰 장점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 LangGraph 그래프 실행을 프로파일링하는 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;from pyinstrument import Profiler

profiler = Profiler()

# 프로파일링 시작
profiler.start()

result = graph.invoke(question)

# 프로파일링 종료 및 결과 출력
profiler.stop()
print(profiler.output_text())
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 결과는 이런 형태입니다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;Duration: 8.41 seconds

graph.invoke()                     8.41s
├── retrieve_documents()           0.15s
├── search_vector_db()             0.10s
├── call_openai()                  6.90s
├── parse_response()               0.90s
└── make_json()                    0.36s
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;call_openai()가 전체 8.41초 중 6.90초를 차지하고 있다는 것을 한눈에 알 수 있습니다. pyinstrument는 건강검진을 예약해서 차분히 받는 정밀검사와 비슷합니다. 개발 환경에서 코드를 실행하며 어떤 함수가 얼마나 시간을 사용하는지 상세하게 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로파일링 결과를 어떻게 해석할 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은, 가장 오래 걸린 부분을 무조건 최적화하는 것이 아니라는 점입니다. 그 시간이 정상 범위인지 먼저 판단해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 위 결과에서 call_openai()가 6.90초 걸렸다고 해서 바로 &quot;LLM이 느리니까 바꾸자&quot;가 아닙니다. LLM API 호출은 원래 몇 초가 걸리는 것이 정상일 수 있습니다. 대신 이런 질문들을 던져야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프롬프트에 검색 결과를 너무 많이 넣고 있지는 않은가?&lt;/li&gt;
&lt;li&gt;더 작은 모델로 대체할 수 있는가?&lt;/li&gt;
&lt;li&gt;스트리밍(Streaming, LLM이 생성하는 즉시 조금씩 응답을 보내는 방식)을 적용해 체감 속도를 개선할 수 있는가?&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;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&gt;병목 구간&lt;/td&gt;
&lt;td&gt;적용 가능한 개선 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector DB 검색&lt;/td&gt;
&lt;td&gt;인덱스 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외부 API 호출&lt;/td&gt;
&lt;td&gt;비동기 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동일한 요청 반복&lt;/td&gt;
&lt;td&gt;캐싱 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB 조회&lt;/td&gt;
&lt;td&gt;쿼리 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU 연산 집중&lt;/td&gt;
&lt;td&gt;멀티프로세싱&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로파일링은 병목이 어디에 있는지 알려주고, 최적화는 그 원인에 따라 다르게 적용하는 것입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 도구의 차이와 사용 시점&lt;/h2&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&gt;항목&lt;/td&gt;
&lt;td&gt;py-spy&lt;/td&gt;
&lt;td&gt;pyindustrument&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 방식&lt;/td&gt;
&lt;td&gt;외부에서 프로세스 관찰&lt;/td&gt;
&lt;td&gt;코드 내부에서 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 수정 필요 여부&lt;/td&gt;
&lt;td&gt;필요 없음&lt;/td&gt;
&lt;td&gt;몇 줄 추가 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 서버 사용&lt;/td&gt;
&lt;td&gt;매우 적합&lt;/td&gt;
&lt;td&gt;개발 환경 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flame Graph 지원&lt;/td&gt;
&lt;td&gt;지원&lt;/td&gt;
&lt;td&gt;미지원 (HTML 트리 중심)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 중 프로세스 분석&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추천 상황&lt;/td&gt;
&lt;td&gt;운영 장애 분석&lt;/td&gt;
&lt;td&gt;개발 중 성능 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 두 도구는 각 단계에 맞게 나눠서 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 단계에서 기능을 완성한 후 응답 속도를 확인할 때는 pyinstrument로 함수별 실행 시간을 파악하고 병목을 수정합니다. 배포 후 운영 중 사용자 증가나 장애가 발생했을 때는 서비스를 중단하지 않고 py-spy로 원인을 파악합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로파일링은 프로그램이 정상 동작하는 상황에서 시간과 자원이 어디에서 소비되는지 측정해 성능 병목을 찾아내는 과정이며, 기능 오류를 찾는 디버깅과는 목적이 다릅니다.&lt;/li&gt;
&lt;li&gt;py-spy는 코드 수정 없이 실행 중인 프로세스를 분석할 수 있어 운영 서버 장애 대응에 적합하고, pyinstrument는 함수 호출 트리를 통해 개발 중 병목을 빠르게 파악하는 데 적합합니다.&lt;/li&gt;
&lt;li&gt;프로파일링 결과에서 가장 오래 걸린 구간을 무조건 최적화하는 것이 아니라, 그 시간이 정상 범위인지 판단한 후 원인에 맞는 개선 방법(캐싱, 비동기 처리, 쿼리 최적화 등)을 선택하는 것이 실무적인 접근입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Python</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/76</guid>
      <comments>https://byhy0.tistory.com/76#entry76comment</comments>
      <pubDate>Sun, 28 Jun 2026 17:54:37 +0900</pubDate>
    </item>
    <item>
      <title>[Date Engineer] Data Firehose란?</title>
      <link>https://byhy0.tistory.com/64</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJKBTL/dJMb9967yKV/xjkbuNWdpaxfCyB54IXUak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJKBTL/dJMb9967yKV/xjkbuNWdpaxfCyB54IXUak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJKBTL/dJMb9967yKV/xjkbuNWdpaxfCyB54IXUak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJKBTL%2FdJMb9967yKV%2FxjkbuNWdpaxfCyB54IXUak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1990&quot; height=&quot;816&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;데이터 파이어호스란 데이터를 안전하게 목적지(타겟, s3, redshift등)에 전달하는 역할을 해주는 서비스이다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;[데이터 발생(로그)] -&amp;gt; [Firehose] -&amp;gt; [S3, ...]&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;와 같은 순서로 진행된다고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징으로는 &lt;b&gt;최소 60초&lt;/b&gt; 단위 혹은 &lt;b&gt;1MB&lt;/b&gt; 단위로 버퍼링하여 데이터를 적제한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 역할을 하는 것이 firehose밖에 없냐고 물어본다면, 그것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 Fluentd와 Logstash가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Amazon Data Firehose&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Fluentd / Logstash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;관리 주체&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;AWS (완전 관리)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;사용자 (서버 운영 필)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;주목적&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;데이터 적재 (Delivery)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;로그 수집 및 필터링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;난이도&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;쉬움&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;비용&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;데이터 처리량에 비례&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;서버 운영비&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Airflow</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/64</guid>
      <comments>https://byhy0.tistory.com/64#entry64comment</comments>
      <pubDate>Sat, 18 Apr 2026 13:36:40 +0900</pubDate>
    </item>
    <item>
      <title>[Airflow] ec2를 사용해 클라우드에 airflow 띄우기 - (2. EC2 인스턴스에 docker 환경 구축하기)</title>
      <link>https://byhy0.tistory.com/63</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 실습에서는 ec2 인스턴스를 생성하고 puTTY를 사용하여 터미널로 접속하는 것을 해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 ec2 인스턴스에 docker를 설치하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 시스템 최신 상태로 업데이트&lt;/p&gt;
&lt;pre id=&quot;code_1776271008265&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get upgrade -y&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업데이트를 안 하면 변경사항이 있을 수 있으니 모두 업데이트 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 도커 자동 설치 스크립트 실행하기&lt;/p&gt;
&lt;pre id=&quot;code_1776271105831&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 공식 홈페이지에서 설치 파일을 가져와서 실행시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 설치 확인하기&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ISjUV/dJMcaaSvAkW/ueVOPEgycQ2KwVJRHT4ej0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ISjUV/dJMcaaSvAkW/ueVOPEgycQ2KwVJRHT4ej0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ISjUV/dJMcaaSvAkW/ueVOPEgycQ2KwVJRHT4ej0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FISjUV%2FdJMcaaSvAkW%2FueVOPEgycQ2KwVJRHT4ej0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;952&quot; height=&quot;431&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 화면이 나온 후 도커가 잘 설치 됐는지 확인하기 위해&lt;/p&gt;
&lt;pre id=&quot;code_1776271225856&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 프롬프트에 작성하여 확인합니다.&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;336&quot; data-origin-height=&quot;29&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eIvCbe/dJMcagd64qc/uKUzXxD0oFjkIkY3rOpSzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eIvCbe/dJMcagd64qc/uKUzXxD0oFjkIkY3rOpSzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eIvCbe/dJMcagd64qc/uKUzXxD0oFjkIkY3rOpSzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeIvCbe%2FdJMcagd64qc%2FuKUzXxD0oFjkIkY3rOpSzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;336&quot; height=&quot;29&quot; data-origin-width=&quot;336&quot; data-origin-height=&quot;29&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 나왔다면 잘 설치가 된 것 입니다.&lt;/p&gt;</description>
      <category>Airflow</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/63</guid>
      <comments>https://byhy0.tistory.com/63#entry63comment</comments>
      <pubDate>Fri, 17 Apr 2026 02:44:16 +0900</pubDate>
    </item>
    <item>
      <title>[Airflow] ec2를 사용해 클라우드에 airflow 띄우기 - (1. EC2 인스턴스 만들기)</title>
      <link>https://byhy0.tistory.com/62</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;노트북에 airflow를 docker에 올려 로컬 환경에서 돌리며 실습하던 중 노트북이 램, CPU 사용량이 너무 높아져 실습이 어려운 상황이 자주 발생하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하고자 AWS에 EC2로 우분투 인스턴스를 만들어 그 안에 docker를 설치하고 airflow를 돌려야겠다고 생각했다.&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;1492&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZarpo/dJMcahc0dqi/w41iuw3burhAFt6pAnb4hK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZarpo/dJMcahc0dqi/w41iuw3burhAFt6pAnb4hK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZarpo/dJMcahc0dqi/w41iuw3burhAFt6pAnb4hK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZarpo%2FdJMcahc0dqi%2Fw41iuw3burhAFt6pAnb4hK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1492&quot; height=&quot;271&quot; data-origin-width=&quot;1492&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 비용이 부족해 t3.micro 모델을 사용하려고 했지만, docker에 컨테이너로 airflow를 돌릴것이기 때문에 메모리가 1GB 더 많은 t3.small를 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습은 mac 환경에서 하기때문에 key는 pem키로 받았어야 하지만, 집에 있는 컴퓨터는 윈도우이기 때문에 ppk로 받고 puTTYgen을 이용해 pem키로 바꿔줄 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. putty로 서버 접속하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KlnBn/dJMcabqlLx0/OI9g0CUZwCGFCqsxGuzlm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KlnBn/dJMcabqlLx0/OI9g0CUZwCGFCqsxGuzlm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KlnBn/dJMcabqlLx0/OI9g0CUZwCGFCqsxGuzlm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKlnBn%2FdJMcabqlLx0%2FOI9g0CUZwCGFCqsxGuzlm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;416&quot; height=&quot;328&quot; data-origin-width=&quot;843&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. puTTY에 접속하고 Host Name에 생성한 인스턴스의 퍼블릭 주소를 넣는다.&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;673&quot; data-origin-height=&quot;669&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mEcdh/dJMcabcLo1F/Ioh3eAcAlLXpChpobBX4Nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mEcdh/dJMcabcLo1F/Ioh3eAcAlLXpChpobBX4Nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mEcdh/dJMcabcLo1F/Ioh3eAcAlLXpChpobBX4Nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmEcdh%2FdJMcabcLo1F%2FIoh3eAcAlLXpChpobBX4Nk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;373&quot; height=&quot;371&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;669&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Connection &amp;gt; SSH &amp;gt; Auth &amp;gt; Credentials 에서 Private Key file에 아까 발급 받은 ppk 파일을 넣어준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;36&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bslSNS/dJMcacW3upG/FBgeqJUlt7LaAvcgeZconK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bslSNS/dJMcacW3upG/FBgeqJUlt7LaAvcgeZconK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bslSNS/dJMcacW3upG/FBgeqJUlt7LaAvcgeZconK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbslSNS%2FdJMcacW3upG%2FFBgeqJUlt7LaAvcgeZconK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;274&quot; height=&quot;36&quot; data-origin-width=&quot;274&quot; data-origin-height=&quot;36&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;3. Open을 눌러 프롬프트 창을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ubuntu@ip-172-xx-xx-xx:~$ 모양의 프롬프트가 떴다면 성공.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후의 과정은 다음 포스팅에서 다루겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>Airflow</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/62</guid>
      <comments>https://byhy0.tistory.com/62#entry62comment</comments>
      <pubDate>Thu, 16 Apr 2026 01:34:37 +0900</pubDate>
    </item>
    <item>
      <title>[Airflow] DAG 처음 짜면서 했던 실수들</title>
      <link>https://byhy0.tistory.com/61</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Airflow를 처음 접했을 때 솔직히 이런 생각이 들었다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #000000; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이거 그냥 cron이랑 뭐가 달라?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Python으로 스케줄 짜고, 태스크 실행하고&amp;hellip; 겉으로 보면 비슷해 보인다. 그런데 실제로 DAG를 짜기 시작하자마자 예상치 못한 곳에서 계속 막혔다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 그 과정에서 내가 직접 겪었던 실수들을 정리한 기록이다. Airflow를 처음 시작하는 분들이 같은 곳에서 시간을 낭비하지 않았으면 하는 마음으로 썼다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. start_date를&lt;span&gt;&amp;nbsp;&lt;/span&gt;datetime.now()로 설정했다&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;어떤 실수였나&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DAG를 처음 만들 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;start_date를 이렇게 설정했다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from datetime import datetime
from airflow import DAG

with DAG(
    dag_id=&quot;my_first_dag&quot;,
    start_date=datetime.now(),  # 이렇게 하면 안 된다
    schedule_interval=&quot;@daily&quot;,
) as dag:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;언뜻 보면 &quot;지금부터 시작&quot;이라는 뜻이니까 자연스러워 보인다. 그런데 이걸 실행하면 DAG가 제대로 트리거되지 않거나, 매번 실행할 때마다&lt;span&gt;&amp;nbsp;&lt;/span&gt;start_date가 달라지는 문제가 생긴다. 그렇게 되면 과거 시점이 존재하지 않게되어 catchup이 불가능해지는 문제도 있다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;왜 안 되는 걸까&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Airflow는 DAG 파일을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주기적으로 파싱&lt;/b&gt;한다. 스케줄러가 DAG 파일을 읽을 때마다&lt;span&gt;&amp;nbsp;&lt;/span&gt;datetime.now()가 새로 계산되기 때문에,&lt;span&gt;&amp;nbsp;&lt;/span&gt;start_date가 계속 바뀐다. Airflow 입장에서는 &quot;언제부터 시작해야 하는지&quot;를 일관되게 알 수 없게 되는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;올바른 방법&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;start_date는 항상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;고정된 과거 날짜&lt;/b&gt;로 설정한다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;from datetime import datetime
from airflow import DAG

with DAG(
    dag_id=&quot;my_first_dag&quot;,
    start_date=datetime(2024, 1, 1),  # 고정된 과거 날짜
    schedule_interval=&quot;@daily&quot;,
) as dag:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. catchup=True인 줄 몰랐다&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;어떤 실수였나&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DAG를 처음 활성화했더니 갑자기 태스크가 수십 개씩 한꺼번에 실행되기 시작했다. &quot;왜 이렇게 많이 돌아가지?&quot; 하고 보니, Airflow가&lt;span&gt;&amp;nbsp;&lt;/span&gt;start_date부터 현재까지 밀린 실행을 모두 메꾸려고 한 것이었다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;start_date를 6개월 전으로 설정하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;schedule_interval=&quot;@daily&quot;로 하면, DAG를 활성화하는 순간 180개의 DAG Run이 한꺼번에 생성된다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;왜 이런 일이 생기나&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Airflow에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;catchup이라는 설정이 있는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본값이&lt;span&gt;&amp;nbsp;&lt;/span&gt;True&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;catchup=True는 &quot;과거에 실행됐어야 했는데 못 한 것들을 지금 다 채워라&quot;라는 의미다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 이 기본값의 존재 자체를 몰랐다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;올바른 방법&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 경우, 특히 처음 개발할 때는&lt;span&gt;&amp;nbsp;&lt;/span&gt;catchup=False로 설정하는 것이 안전하다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;with DAG(
    dag_id=&quot;my_first_dag&quot;,
    start_date=datetime(2024, 1, 1),
    schedule_interval=&quot;@daily&quot;,
    catchup=False,  # 과거 실행 채우지 않음
) as dag:
    ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론&lt;span&gt;&amp;nbsp;&lt;/span&gt;catchup=True가 유용한 경우도 있다. 데이터 파이프라인에서 특정 날짜 범위의 데이터를 재처리해야 할 때는 의도적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;True로 설정하기도 한다. 중요한 건&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본값이 True라는 것을 알고 명시적으로 선택&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. Task가 실패해도 데이터가 중복됐다&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;어떤 실수였나&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Task가 중간에 실패해서 재실행했더니 DB에 데이터가 두 번 들어가 있었다. &quot;재실행하면 이전 결과가 덮어씌워지는 거 아닌가?&quot; 하고 생각했는데, 전혀 그렇지 않았다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;def insert_data():
    # 단순 INSERT는 재실행 시 중복 발생
    db.execute(&quot;INSERT INTO orders SELECT * FROM raw_orders WHERE date = '2024-01-01'&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Task가 실패한 후 재실행하면, 이전에 이미 실행된 부분이 또 실행되면서 데이터가 쌓였다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;핵심 개념 - 멱등성(Idempotency)&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 처음 알게 된 개념이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;멱등성(Idempotency)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;멱등성이란,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;같은 작업을 몇 번 반복해도 결과가 동일한 성질&lt;/b&gt;을 말한다. Airflow에서 Task는 실패 시 재실행될 수 있기 때문에, Task 자체가 멱등해야 한다. 즉, &quot;몇 번 실행해도 결과가 같아야 한다&quot;는 원칙이다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;올바른 방법&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;단순 INSERT 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;UPSERT&lt;/b&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;삭제 후 재삽입&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식을 사용한다.&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;python&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;def insert_data():
    # 방법 1: UPSERT (PostgreSQL 기준)
    db.execute(&quot;&quot;&quot;
        INSERT INTO orders SELECT * FROM raw_orders WHERE date = '2024-01-01'
        ON CONFLICT (id) DO UPDATE SET ...
    &quot;&quot;&quot;)

def insert_data():
    # 방법 2: 해당 날짜 데이터 삭제 후 재삽입
    db.execute(&quot;DELETE FROM orders WHERE date = '2024-01-01'&quot;)
    db.execute(&quot;INSERT INTO orders SELECT * FROM raw_orders WHERE date = '2024-01-01'&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;그냥 재실행 안 되게 잘 짜면 되지 않나?&quot;라고 생각할 수 있다. 하지만 분산 환경에서 Task는 언제든 실패할 수 있고, Airflow는 그 상황에서도 파이프라인이 안전하게 재실행될 수 있도록 설계되어야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5. 마치며&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금 돌이켜보면 이 실수들은 다 &quot;당연히 이렇게 되겠지&quot;라는 막연한 가정에서 비롯됐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;datetime.now()가 직관적으로 보였고,&lt;/li&gt;
&lt;li&gt;catchup 같은 기본값의 존재를 몰랐고,&lt;/li&gt;
&lt;li&gt;재실행 시 멱등성이 보장돼야 한다는 개념 자체가 낯설었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Airflow는 단순히 스크립트를 스케줄링하는 도구가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;분산 환경에서 신뢰할 수 있는 데이터 파이프라인을 만들기 위한 도구&lt;/b&gt;다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 관점에서 다시 보면 위의 개념들이 왜 그렇게 설계됐는지 자연스럽게 이해된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음엔 Airflow를 조금 더 써보면서 느낀 것들을 정리해볼 예정이다.&lt;/p&gt;</description>
      <category>Airflow</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/61</guid>
      <comments>https://byhy0.tistory.com/61#entry61comment</comments>
      <pubDate>Tue, 14 Apr 2026 16:28:06 +0900</pubDate>
    </item>
    <item>
      <title>[Airflow] Apache Airflow 개념 정리</title>
      <link>https://byhy0.tistory.com/59</link>
      <description>&lt;h1&gt;Apache Airflow 개념 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;카테고리&lt;/b&gt;: Data Engineering &amp;middot; Airflow &amp;middot; Workflow&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Airflow란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Airflow는 워크플로우 오케스트레이션 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 작업을 수행하지 않고,&lt;br /&gt;작업들이 &lt;b&gt;언제, 어떤 순서로 실행될지를 관리&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실행 엔진이라기보다는&lt;br /&gt;&lt;b&gt;작업 흐름을 제어하는 관리자&lt;/b&gt;에 가깝다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 스케줄러와의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에도 작업 자동화는 가능하다. (cron, 윈도우 작업 스케줄러 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다음과 같은 한계가 존재한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한계&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 간 &lt;b&gt;의존성 관리 어려움&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;성공/실패에 따른 &lt;b&gt;분기 처리 어려움&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재시도 로직 구성의 제한&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Airflow의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow는 Python 기반으로 워크플로우를 정의한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;의존성 제어&lt;/td&gt;
&lt;td&gt;작업 순서를 코드로 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재시도&lt;/td&gt;
&lt;td&gt;실패 시 자동 재실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분기 처리&lt;/td&gt;
&lt;td&gt;조건에 따라 다른 흐름 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 단순 스케줄러가 아니라 &lt;b&gt;조건 기반 자동화 시스템&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Airflow를 사용하는 이유&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task A &amp;rarr; 성공 &amp;rarr; Task B 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 간 실행 순서를 명확하게 정의할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재시도 및 장애 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실패 시 일정 시간 후 재시도&lt;/li&gt;
&lt;li&gt;반복 실패 시 알림 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서 필수적인 기능이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백필 (Backfill)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 수집 중 장애로 과거 데이터가 누락된 경우:&lt;br /&gt;과거 시점부터 현재까지 재실행 &amp;rarr; 데이터 복구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 파이프라인에서 중요한 기능이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Airflow 아키텍처&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성 요소&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler&lt;/td&gt;
&lt;td&gt;실행 대상 작업을 지속적으로 탐지하고 실행 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web Server&lt;/td&gt;
&lt;td&gt;UI 제공 (모니터링, 로그 확인)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata DB&lt;/td&gt;
&lt;td&gt;DAG, 실행 상태, 로그 등 모든 정보 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executor / Worker&lt;/td&gt;
&lt;td&gt;실제 작업 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구성 요소 설명&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Scheduler&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항상 실행 중&lt;/li&gt;
&lt;li&gt;실행 조건이 맞는 작업을 탐지하여 Executor에 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Web Server&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 UI 제공 (&lt;code&gt;localhost:8080&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;DAG 상태 및 로그 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Metadata Database&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 관리 중심 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;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;DAG 스케줄&lt;/li&gt;
&lt;li&gt;실행 결과&lt;/li&gt;
&lt;li&gt;로그&lt;/li&gt;
&lt;li&gt;시스템 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Executor / Worker&lt;/h4&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LocalExecutor&lt;/td&gt;
&lt;td&gt;단일 머신 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CeleryExecutor&lt;/td&gt;
&lt;td&gt;분산 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KubernetesExecutor&lt;/td&gt;
&lt;td&gt;컨테이너 기반 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DAG (Directed Acyclic Graph)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 워크플로우&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;방향성&lt;/td&gt;
&lt;td&gt;작업 순서 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비순환&lt;/td&gt;
&lt;td&gt;루프 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A &amp;rarr; B &amp;rarr; C (O)&lt;br /&gt;A &amp;rarr; B &amp;rarr; A (X)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Operator&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 정의하는 템플릿&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BashOperator&lt;/td&gt;
&lt;td&gt;쉘 명령 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PythonOperator&lt;/td&gt;
&lt;td&gt;Python 함수 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SqlOperator&lt;/td&gt;
&lt;td&gt;DB 작업 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Task&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Operator의 실행 단위&lt;/li&gt;
&lt;li&gt;DAG 내 하나의 노드&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Task Instance&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Task가 실행된 실제 객체&lt;/li&gt;
&lt;li&gt;실행 상태 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;성공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;실패&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;XCom&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task 간 데이터 전달 기능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task A &amp;rarr; 결과 저장 &amp;rarr; Task B에서 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;동일 결과 보장 (Idempotency)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 DAG를 여러 번 실행해도&lt;br /&gt;&lt;b&gt;결과는 항상 동일해야 한다&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 중복 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재실행 시 데이터가 중복 저장되지 않도록 설계 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 airflow는 단순한 스케줄러가 아니라 작업 흐름을 정의하고 의존성을 관리해주며, 재시도 및 장애 처리까지 포함한 데이터 파이프라인 관리 도구이다.&lt;/p&gt;</description>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/59</guid>
      <comments>https://byhy0.tistory.com/59#entry59comment</comments>
      <pubDate>Wed, 8 Apr 2026 22:40:40 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] AWS 3-Tier 아키텍처 실습기</title>
      <link>https://byhy0.tistory.com/57</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 AWS 3-Tier 아키텍처를 만들어보는 실습에 대한 후기를 남기려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 3-Tier 아키텍처는 다음과 같이 설계해보았다.&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;2027&quot; data-origin-height=&quot;1346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EOVz6/dJMcahcP92G/dSdKglGeiEMIAX5j67ddI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EOVz6/dJMcahcP92G/dSdKglGeiEMIAX5j67ddI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EOVz6/dJMcahcP92G/dSdKglGeiEMIAX5j67ddI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEOVz6%2FdJMcahcP92G%2FdSdKglGeiEMIAX5j67ddI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;670&quot; height=&quot;445&quot; data-origin-width=&quot;2027&quot; data-origin-height=&quot;1346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 아키텍처 구조 잡기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 VPC 서브넷을 총 6개의 서브넷으로 구성했다.&lt;br /&gt;&lt;br /&gt;1. public Subnet 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. private subnet 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. DB subnet 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개씩 만든 이유는 고가용성(HA)을 위해 설계하였다. 여기서 말한 고가용성은 한쪽 서버에 완전히 장애가 생겨도 물리적으로 먼 거리에 있는 서버 2개를 사용하기 때문에 서비스는 정상적으로 작동하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private subvet에는 중요한 정보가 있기 때문에 외부 사용자는 private subvet에 바로 접속 못하고 로드밸런서를 통해서만 접속하게 했다. public subnet에는 &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;Bastion Host를 만들어 외부에서 내부에 접속할 수 있는 유일한 통로를 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시말해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서비스 정문 (ALB, 80번)&lt;/b&gt;: 일반 사용자가 드나드는 곳이다. 누구든 HTTP 요청은 환영이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관리자 쪽문 (Bastion, 22번)&lt;/b&gt;: 오직 나(관리자)만 아는 문이다. 내 PC의 IP를 가진 사람만 열 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버(Private EC2)는 이 두 문을 통해서만 접근이 가능하고, 데이터베이스는 그 서버에서만 접근이 가능하다. 마치 왕궁처럼 성벽(VPC) &amp;rarr; 정문&amp;middot;쪽문(Public Subnet) &amp;rarr; 내실(Private Subnet) &amp;rarr; 금고(DB Subnet) 순서로 겹겹이 둘러싸인 구조라고 생각하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 보안 그룹 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 그룹을 설정하다 보면 이런 순간이 온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Private EC2의 80번 포트를 ALB한테 열어줘야 하는데, ALB의 IP를 넣으면 되나?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 안 된다. 이유는 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라우드 서버의 IP는 휘발성이기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALB를 만들면 지금 이 순간 IP는 3.34.12.56일 수 있다. 그런데 트래픽 폭주로 스케일 아웃되면 IP가 바뀔 수 있고, ALB를 지웠다가 다시 만들면 당연히 IP가 바뀐다. 그 순간 보안 그룹 규칙에 써있는 3.34.12.56은 유령 IP가 되고, 서비스는 다운된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 보안 그룹 ID(sg-xxxxxxxx)는 논리적 식별자라서 IP가 바뀌든 인스턴스가 교체되든 상관없다. AWS가 알아서 해당 보안 그룹에 속한 리소스의 IP를 실시간으로 추적해주기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 비유하면 이렇다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ &lt;b&gt;IP 방식&lt;/b&gt;: 얼굴만 아는 친구 &amp;mdash; 성형수술 하면 못 알아본다&lt;br /&gt;✅ &lt;b&gt;보안 그룹 ID 방식&lt;/b&gt;: 사원증으로 입장 &amp;mdash; 사람이 바뀌어도 사원증만 있으면 OK&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 정리는 아래와 같다.&lt;/p&gt;
&lt;div&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&gt;Bastion Host&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;내 PC 공인 IP (x.x.x.x/32)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALB&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;0.0.0.0/0 (전체 공개)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private EC2&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;Bastion의 &lt;b&gt;보안 그룹 ID&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private EC2&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;ALB의 &lt;b&gt;보안 그룹 ID&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Bastion Host로 징검다리 SSH 접속하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배스천 호스트는 관리자의 징검다리 역할을 한다. 내 PC &amp;rarr; 배스천 &amp;rarr; Private EC2, 이렇게 두 번 SSH를 타야 하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 처음 떠오르는 자연스러운 생각이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;배스천에서 Private EC2로 접속할 때도 키가 필요하겠네? 그럼 배스천 서버에 .pem 파일 올려놓으면 되겠다!&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 하면 안 됐다. 이렇게 한다면 배스천은 의미가 없어질 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배스천에는 public ip가 있기 때문에 인터넷에 노출된 서버라 항상 공격 대상이 될 수 있다. 만약 누군가 배스천을 뚫었을 때를 생각해보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배스천에 키 파일이 &lt;b&gt;없다면&lt;/b&gt;: &quot;배스천은 뚫었는데 더 이상 못 들어가겠네.&quot; ✅&lt;/li&gt;
&lt;li&gt;배스천에 키 파일이 &lt;b&gt;있다면&lt;/b&gt;: &quot;어, Private EC2 키까지 여기 있네? 감사합니다.&quot; ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배스천에 키를 두는 건 금고 열쇠를 금고 위에 올려두는 것과 같다. 쪽문을 뚫으면 내실까지 한 번에 털리는 구조가 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하는 방법이 바로 &lt;b&gt;SSH 에이전트 포워딩&lt;/b&gt;이다. 키 파일 자체를 전달하는 게 아니라, &lt;b&gt;'이 키로 서명할 수 있는 능력'을 임시로 빌려주는 것&lt;/b&gt;이라고 생각하면 된다. 실제로 키 파일은 내 PC를 절대 떠나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어 순서는 아래와 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;# Step 1: 내 PC에서 SSH 키 관리자(에이전트) 실행
eval $(ssh-agent -s)

# Step 2: 에이전트에게 내 키 등록
ssh-add [키이름].pem

# Step 3: 배스천 접속 시 -A 옵션으로 에이전트 권한 전달
ssh -A ec2-user@[배스천_공인IP]

# Step 4: 배스천 안에서 추가 설정 없이 Private EC2 접속
ssh ec2-user@[프라이빗_사설IP]
```

`-A` 옵션 하나가 전부다. 이 한 글자가 &quot;키 복사&quot;와 &quot;에이전트 포워딩&quot;의 차이를 만들어낸다.

---

##   트러블슈팅

### `Permission denied (publickey)` 에러가 난다면?

배스천까지는 접속이 됐는데 Private EC2로 넘어가려니 아래 에러가 뜨는 경우가 있다.
```
Permission denied (publickey,gssapi-keyex,gssapi-with-mic).&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범인은 거의 항상 셋 중 하나다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;ssh-agent를 실행하지 않았다&lt;/b&gt; &amp;rarr; eval $(ssh-agent -s) 먼저 했는지 확인하자.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ssh-add를 빠뜨렸다&lt;/b&gt; &amp;rarr; 에이전트를 켰어도 키를 등록하지 않으면 빈 에이전트이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;-A 옵션을 빠뜨렸다&lt;/b&gt; &amp;rarr; 배스천 접속 시 -A가 없으면 포워딩이 일어나지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지만 체크하면 거의 해결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 보안 설정은 처음엔 그냥 외우는 것처럼 느껴졌다. 그런데 &quot;왜 이렇게 해야 하는가&quot;를 이해하고 나면 설정 하나하나가 의도를 가진 결정처럼 보이기 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 최소한의 권한을 줘서 만약 해킹을 당하더라도 피해를 최소화 할 수 있을 것 같다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이걸 보안 용어로는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;최소 권한 원칙(Principle of Least Privilege)&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; 이번 3-Tier 실습은 그 원칙을 아키텍처 전체에 적용해본 좋은 연습이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 실습에서는 NAT Gateway를 붙여서 Private EC2가 외부 패키지를 설치할 수 있게 만들거나, HTTPS를 위한 ACM 인증서 설정으로 이어가 볼 것이다.&lt;/p&gt;</description>
      <category>클라우드</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/57</guid>
      <comments>https://byhy0.tistory.com/57#entry57comment</comments>
      <pubDate>Thu, 2 Apr 2026 17:38:52 +0900</pubDate>
    </item>
    <item>
      <title>[Project] 카카오 API 사용을 위한 카카오 개발자계정 만드는법</title>
      <link>https://byhy0.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 어플리케이션에 카카오 API를 넣으려면 필요한 카카오 개발자 계정을 만드는 법을 소개하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1775006570097&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kakao Developers&quot; data-og-description=&quot;카카오 API를 활용하여 다양한 어플리케이션을 개발해 보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&quot; data-og-host=&quot;developers.kakao.com&quot; data-og-source-url=&quot;https://developers.kakao.com/&quot; data-og-url=&quot;https://developers.kakao.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/PfxmK/dJMb8VNvALu/qNwirCKKU65LqEuq9UMF1k/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/YXQEd/dJMb9iIHh4Z/JUXOyvtHtjGf1nFFzh0bg0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/bp63Rn/dJMb9c9x4hC/Hy7iZhym443rfVeHyzDSG1/img.png?width=1200&amp;amp;height=800&amp;amp;face=0_0_1200_800&quot;&gt;&lt;a href=&quot;https://developers.kakao.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.kakao.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/PfxmK/dJMb8VNvALu/qNwirCKKU65LqEuq9UMF1k/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/YXQEd/dJMb9iIHh4Z/JUXOyvtHtjGf1nFFzh0bg0/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/bp63Rn/dJMb9c9x4hC/Hy7iZhym443rfVeHyzDSG1/img.png?width=1200&amp;amp;height=800&amp;amp;face=0_0_1200_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Kakao Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카카오 API를 활용하여 다양한 어플리케이션을 개발해 보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.kakao.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 가입을 하기 위해 KaKao Developers 사이트에 접속합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-01 오전 10.23.22.png&quot; data-origin-width=&quot;2922&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKTlBV/dJMcafF5yR6/eAKqkyXq7RfaWk1qK3lvb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKTlBV/dJMcafF5yR6/eAKqkyXq7RfaWk1qK3lvb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKTlBV/dJMcafF5yR6/eAKqkyXq7RfaWk1qK3lvb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKTlBV%2FdJMcafF5yR6%2FeAKqkyXq7RfaWk1qK3lvb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;825&quot; height=&quot;374&quot; data-filename=&quot;스크린샷 2026-04-01 오전 10.23.22.png&quot; data-origin-width=&quot;2922&quot; data-origin-height=&quot;1326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우측 상단에 로그인을 누르시면 회원가입 창으로 바로 넘어가집니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-01 오전 10.26.41.png&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SOJN2/dJMcahjCZkk/LZsSHGHdkDXnPTttfdcvyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SOJN2/dJMcahjCZkk/LZsSHGHdkDXnPTttfdcvyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SOJN2/dJMcahjCZkk/LZsSHGHdkDXnPTttfdcvyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSOJN2%2FdJMcahjCZkk%2FLZsSHGHdkDXnPTttfdcvyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1966&quot; height=&quot;504&quot; data-filename=&quot;스크린샷 2026-04-01 오전 10.26.41.png&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-01 오전 10.24.09.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ouuUu/dJMcagrrx1L/kTYXdzwUp8Ie87FN7pjhzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ouuUu/dJMcagrrx1L/kTYXdzwUp8Ie87FN7pjhzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ouuUu/dJMcagrrx1L/kTYXdzwUp8Ie87FN7pjhzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FouuUu%2FdJMcagrrx1L%2FkTYXdzwUp8Ie87FN7pjhzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;1782&quot; height=&quot;896&quot; data-filename=&quot;스크린샷 2026-04-01 오전 10.24.09.png&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;896&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 이용 동의 체크해주시고, 카카오 계정 정보에는 본인이 사용하시는 계정 ID와 사용하실 닉네임을 입력하시면 됩니다.&lt;br /&gt;저는 닉네임으로 제 본명을 사용하였습니다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 아래에 회원가입 버튼을 누르시면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 계정 회원가입이 완료됩니다.&lt;/p&gt;</description>
      <category>Project</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/56</guid>
      <comments>https://byhy0.tistory.com/56#entry56comment</comments>
      <pubDate>Wed, 1 Apr 2026 10:30:14 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스_SQL] Lv.2 진료과별 총 예약 횟수 출력하기</title>
      <link>https://byhy0.tistory.com/55</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Lv2.진료과별 총 예약 횟수 출력하기.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9sgB/dJMcabjogp6/zkBjKsPKqkKeesMu6AdRy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9sgB/dJMcabjogp6/zkBjKsPKqkKeesMu6AdRy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9sgB/dJMcabjogp6/zkBjKsPKqkKeesMu6AdRy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9sgB%2FdJMcabjogp6%2FzkBjKsPKqkKeesMu6AdRy0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;400&quot; height=&quot;400&quot; data-filename=&quot;Lv2.진료과별 총 예약 횟수 출력하기.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;카테고리&lt;/strong&gt;: SQL · GROUP BY · DATE_FORMAT · ORDER BY&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;플랫폼&lt;/strong&gt;: 프로그래머스&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;난이도&lt;/strong&gt;: Level 2&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;문제 설명&lt;/h2&gt;
&lt;p&gt;다음은 종합병원의 진료 예약정보를 담은 &lt;code&gt;APPOINTMENT&lt;/code&gt; 테이블입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| Column name | Type | Nullable |
| --- | --- | --- |
| APNT\_YMD | TIMESTAMP | FALSE |
| APNT\_NO | NUMBER(5) | FALSE |
| PT\_NO | VARCHAR(10) | FALSE |
| MCDP\_CD | VARCHAR(6) | FALSE |
| MDDR\_ID | VARCHAR(10) | FALSE |
| APNT\_CNCL\_YN | VARCHAR(1) | TRUE |
| APNT\_CNCL\_YMD | DATE | TRUE |&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;APPOINTMENT&lt;/code&gt; 테이블에서 &lt;strong&gt;2022년 5월에 예약한 환자 수를 진료과코드 별로&lt;/strong&gt; 조회하는 SQL문을 작성해주세요.&lt;/p&gt;
&lt;p&gt;컬럼명은 &lt;code&gt;&amp;#39;진료과 코드&amp;#39;&lt;/code&gt;, &lt;code&gt;&amp;#39;5월예약건수&amp;#39;&lt;/code&gt;로 지정하고, 진료과별 예약한 환자 수를 기준으로 오름차순 정렬하되, 예약한 환자 수가 같다면 진료과 코드를 기준으로 오름차순 정렬해주세요.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;예시&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;입력&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| APNT\_YMD | APNT\_NO | PT\_NO | MCDP\_CD | MDDR\_ID | APNT\_CNCL\_YN | APNT\_CNCL\_YMD |
| --- | --- | --- | --- | --- | --- | --- |
| 2022-04-14 09:30:00 | 47 | PT22000064 | GS | DR20170123 | N | NULL |
| 2022-04-15 10:30:00 | 48 | PT22000065 | OB | DR20100231 | N | NULL |
| 2022-05-15 17:30:00 | 49 | PT22000086 | OB | DR20100231 | N | NULL |
| 2022-05-18 10:30:00 | 52 | PT22000019 | GS | DR20100039 | N | NULL |
| 2022-05-19 12:00:00 | 53 | PT22000020 | FM | DR20010112 | N | NULL |
| 2022-05-22 08:30:00 | 54 | PT22000021 | GS | DR20100039 | N | NULL |
| 2022-05-04 10:30:00 | 56 | PT22000023 | FM | DR20090112 | N | NULL |
| 2022-05-14 15:30:00 | 57 | PT22000074 | CS | DR20200012 | N | NULL |
| 2022-05-24 15:30:00 | 58 | PT22000085 | CS | DR20200012 | N | NULL |
| 2022-05-28 10:00:00 | 60 | PT22000092 | OS | DR20100031 | N | NULL |&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;출력&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| 진료과코드 | 5월예약건수 |
| --- | --- |
| OB | 1 |
| OS | 1 |
| CS | 2 |
| FM | 2 |
| GS | 2 |&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;내 풀이&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SELECT
    MCDP_CD AS &amp;#39;진료과 코드&amp;#39;,
    COUNT(*) AS &amp;#39;5월예약건수&amp;#39;
FROM APPOINTMENT
WHERE DATE_FORMAT(APNT_YMD, &amp;#39;%Y-%m&amp;#39;) = &amp;#39;2022-05&amp;#39;
GROUP BY MCDP_CD
ORDER BY COUNT(*) ASC, MCDP_CD ASC;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;공부 포인트&lt;/h2&gt;
&lt;h3&gt;1. DATE_FORMAT() 으로 날짜 필터링&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;APNT_YMD&lt;/code&gt; 는 TIMESTAMP 타입이므로 연월 단위로 비교하려면 &lt;code&gt;DATE_FORMAT()&lt;/code&gt; 으로 포맷을 맞춰줘야 함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DATE_FORMAT(APNT_YMD, &amp;#39;%Y-%m&amp;#39;) = &amp;#39;2022-05&amp;#39;&lt;/code&gt; 로 2022년 5월 데이터만 필터링&lt;/li&gt;
&lt;li&gt;&lt;code&gt;YEAR(APNT_YMD) = 2022 AND MONTH(APNT_YMD) = 5&lt;/code&gt; 로 작성해도 동일한 결과를 얻을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 다중 정렬 (ORDER BY)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ORDER BY COUNT(*) ASC, MCDP_CD ASC&lt;/code&gt; 처럼 콤마로 구분하여 정렬 기준을 여러 개 지정할 수 있음&lt;/li&gt;
&lt;li&gt;앞에 있는 기준이 같을 경우 다음 기준으로 정렬함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;추가 팁&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;COUNT()&lt;/code&gt; 는 &lt;code&gt;COUNT(*)&lt;/code&gt; 로 쓰는 것이 일반적입니다. &lt;code&gt;COUNT(컬럼명)&lt;/code&gt; 은 해당 컬럼의 NULL을 제외하고 세지만, &lt;code&gt;COUNT(*)&lt;/code&gt; 는 행 전체를 세기 때문에 예약 건수를 셀 때는 &lt;code&gt;COUNT(*)&lt;/code&gt; 가 더 명확합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DATE_FORMAT&lt;/code&gt; 대신 &lt;code&gt;BETWEEN&lt;/code&gt; 을 활용하는 방법도 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE APNT_YMD BETWEEN &amp;#39;2022-05-01&amp;#39; AND &amp;#39;2022-05-31 23:59:59&amp;#39;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;마무리 요약&lt;/h2&gt;
&lt;p&gt;이번 문제는 &lt;code&gt;DATE_FORMAT()&lt;/code&gt; 으로 날짜를 필터링하고 &lt;code&gt;GROUP BY&lt;/code&gt; 와 다중 &lt;code&gt;ORDER BY&lt;/code&gt; 를 조합하는 실무형 SQL 예제다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;날짜 타입 필터링 방법&lt;/strong&gt;과 &lt;strong&gt;다중 정렬 기준 설정&lt;/strong&gt;은 데이터 분석 시 자주 쓰이는 핵심 패턴이므로 꼭 익혀두는 것이 좋다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;이상입니다. 혹시나 제가 잘못된 내용을 작성했거나 더 좋은 방법이 있다면 댓글 남겨주세요.&lt;/p&gt;
&lt;p&gt;지금까지 이 글을 봐주셔서 감사합니다.&lt;/p&gt;</description>
      <category>코딩테스트/SQL</category>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/55</guid>
      <comments>https://byhy0.tistory.com/55#entry55comment</comments>
      <pubDate>Tue, 31 Mar 2026 12:49:04 +0900</pubDate>
    </item>
    <item>
      <title>[AI] Claude에게 원하는 답을 얻어내는 질문법</title>
      <link>https://byhy0.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Claude를 쓰다 보면 같은 도구인데 사람마다 결과물 차이가 꽤 크다는 걸 느끼게 됩니다. 그 차이는 대부분 모델이 아니라 질문에서 나옵니다. Claude는 여러분이 건넨 정보만 가지고 답을 만들기 때문에, 질문에 무엇을 담느냐가 답변의 질을 거의 결정합니다. 이 글에서는 더 쓸모 있는 답을 받기 위한 여섯 가지 질문 방법과, 그것들을 하나로 묶는 공식을 정리해봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;맥락을 충분히 주기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c04FSd/dJMcaciZTKv/UIgEZdjEoLbT0B17vKwJHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c04FSd/dJMcaciZTKv/UIgEZdjEoLbT0B17vKwJHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c04FSd/dJMcaciZTKv/UIgEZdjEoLbT0B17vKwJHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc04FSd%2FdJMcaciZTKv%2FUIgEZdjEoLbT0B17vKwJHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;720&quot; height=&quot;344&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본이면서 효과가 큰 부분입니다. 배경 정보가 없으면 Claude는 가장 일반적인 답을 내놓을 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &quot;마케팅 전략 알려줘&quot;라고만 하면 교과서적인 답이 돌아옵니다. 반면 &quot;스타트업 SaaS 회사에서 B2B 영업을 담당하고 있고, 월 예산은 200만 원, 타겟은 중소기업 HR 담당자야. 6개월 안에 리드 50개를 만들어야 해. 어떤 전략이 효과적일까?&quot;라고 물으면 상황에 맞는 구체적인 답이 나옵니다. 회사 규모, 예산, 기간, 목표처럼 판단에 영향을 주는 조건은 아낌없이 적는 편이 좋습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;역할을 지정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude에게 특정 전문가 역할을 맡기면 그 역할에 맞는 깊이와 관점으로 답합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 코드 리뷰해줘&quot;보다 &quot;시니어 백엔드 엔지니어로서 이 코드의 성능 문제를 리뷰해줘&quot;가 더 날카로운 답을 줍니다. 이력서를 봐달라고 할 때도 &quot;10년 경력의 커리어 코치처럼 평가해줘&quot;라고 하면 두루뭉술한 칭찬 대신 실질적인 지적을 받을 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원하는 출력 형식을 명시하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;형식을 정해주면 받자마자 바로 쓸 수 있는 결과물이 나옵니다. 무엇을 지정할 수 있는지 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;height: 96px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;지정 방법&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;분량&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&quot;3문장으로 요약해줘&quot; / &quot;1,000자 이내로&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;형식&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&quot;표로 비교해줘&quot; / &quot;글머리 기호 없이 산문으로&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;구조&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&quot;장점/단점/결론 순서로 써줘&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;대상&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&quot;비전공자도 이해할 수 있게 설명해줘&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 표로 비교해달라거나 특정 순서로 써달라는 요청은 결과물을 다시 손보는 수고를 줄여줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단계적으로 생각하게 하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 문제일수록 답만 받기보다 추론 과정을 보여달라고 하는 편이 정확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;단계별로 생각하면서 풀어줘&quot;라고 하면 중간 과정이 드러나기 때문에 어디서 판단이 어긋났는지 확인할 수 있습니다. 찬반이 갈리는 주제라면 &quot;결론을 내리기 전에 찬반 논거를 먼저 나열해줘&quot;처럼 요청하면 한쪽으로 치우치지 않은 답을 받기 쉽습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예시를 함께 주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 스타일이나 패턴이 있는데 말로 설명하기 애매할 때는 예시를 직접 보여주는 게 가장 빠릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 &quot;아래 예시처럼 캐주얼한 톤으로 제품 설명을 써줘&quot;라고 한 뒤 &quot;이 가방, 진짜 어디든 들고 다녀도 안 촌스러워요. 가볍고 튼튼해서 믿음직하고.&quot;처럼 본보기를 붙이면, Claude가 그 말투와 분위기를 그대로 맞춰줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;제약 조건을 명시하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지 말아야 할 것을 알려주면 원하지 않는 답을 미리 걸러낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;영어 단어는 쓰지 말고, 전문 용어 설명 없이 써줘&quot;라거나 &quot;이미 아는 내용이니 기초 설명은 건너뛰어도 돼&quot;처럼 선을 그어주면 불필요한 부분이 줄고 원하는 곳에만 집중한 답이 돌아옵니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 번에 끝내려 하지 않기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY9Zxy/dJMcai4y0zG/T5Ik0IhYqx8fPtuMY75ISk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY9Zxy/dJMcai4y0zG/T5Ik0IhYqx8fPtuMY75ISk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY9Zxy/dJMcai4y0zG/T5Ik0IhYqx8fPtuMY75ISk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY9Zxy%2FdJMcai4y0zG%2FT5Ik0IhYqx8fPtuMY75ISk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;700&quot; height=&quot;189&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 한 번의 질문으로 완벽한 답을 기대하기보다 대화를 이어가며 다듬는 편이 효과적입니다. 1차로 초안을 받고, 2차에 &quot;더 간결하게 줄여줘&quot;, 3차에 &quot;두 번째 단락을 좀 더 설득력 있게 바꿔줘&quot; 식으로 좁혀나가면 됩니다. Claude는 대화 흐름 안에서 앞 내용을 기억하므로 매번 처음부터 설명하지 않아도 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 방법은 하나의 공식으로 묶을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;prompt_formula.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RM53o/dJMcajbi4Gw/NikMFs4pTWPm4QNJm2kT8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RM53o/dJMcajbi4Gw/NikMFs4pTWPm4QNJm2kT8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RM53o/dJMcajbi4Gw/NikMFs4pTWPm4QNJm2kT8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRM53o%2FdJMcajbi4Gw%2FNikMFs4pTWPm4QNJm2kT8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.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;720&quot; height=&quot;400&quot; data-filename=&quot;prompt_formula.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[역할] + [맥락] + [구체적인 요청] + [출력 형식] + [제약 조건]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 의식하면서 질문하면 답변의 질이 눈에 띄게 달라집니다. 핵심만 추리면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Claude는 여러분이 준 정보로만 답하므로, 맥락과 조건을 구체적으로 적을수록 답이 좋아집니다.&lt;/li&gt;
&lt;li&gt;역할, 출력 형식, 제약 조건을 지정하면 바로 쓸 수 있는 결과물에 가까워집니다.&lt;/li&gt;
&lt;li&gt;한 번에 끝내려 하지 말고 대화를 이어가며 다듬는 것이 가장 빠른 길입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>byhy0</author>
      <guid isPermaLink="true">https://byhy0.tistory.com/54</guid>
      <comments>https://byhy0.tistory.com/54#entry54comment</comments>
      <pubDate>Mon, 30 Mar 2026 23:53:29 +0900</pubDate>
    </item>
  </channel>
</rss>