<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev_init_ung.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 30 Apr 2025 08:22:55 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev_init_ung.log</title>
            <url>https://velog.velcdn.com/images/dev_init_ung/profile/f15ffce8-69e8-4ac7-89f5-84e84b66e8c0/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev_init_ung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_init_ung" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[LangGraph로 조건 분기형 GPT 에이전트 만들기(1)]]></title>
            <link>https://velog.io/@dev_init_ung/LangGraph</link>
            <guid>https://velog.io/@dev_init_ung/LangGraph</guid>
            <pubDate>Wed, 30 Apr 2025 08:22:55 GMT</pubDate>
            <description><![CDATA[<p>최근 다양한 AI 제품들이 단순한 응답 생성에서 나아가 외부 도구를 직접 호출하고, 판단하고, 행동하는 구조로 진화하고 있습니다.
이런 구조를 코드로 실현하기 위해서는 GPT와 도구 사이의 흐름을 정교하게 제어할 수 있는 프레임워크가 필요합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/efa5605d-3f6a-439f-9f35-0a49a10e4a7e/image.png" alt=""></p>
<p>그래서 이번 글에서는 LangGraph를 활용해 GPT가 웹 검색 도구를 직접 요청하고, 결과를 바탕으로 다시 사고하고 응답하는 일련의 흐름을 구현했습니다.</p>
<p>이 과정을 통해 단순한 챗봇을 넘어서, 도구 기반 Agent 구조를 설계하고,
LLM의 사고와 행동을 그래프 기반으로 제어하는 실전 워크플로우를 학습할 수 있습니다.</p>
<h2 id="langgraph란">LangGraph란?</h2>
<p>LangGraph는 LangChain 위에서 동작하는 상태 기반 워크플로우 프레임워크입니다.
즉, 에이전트의 상태와 흐름을 노드 그래프 형태로 시각적으로 설계하고 실행할 수 있게 해주는 프레임워크라고 할 수 있습니다.</p>
<p>기존의 LangChain으로 체인, 에이전트를 구성하였고, 
동작시켰지만 다음과 같은 문제가 발생합니다. 그리고 해당 문제들을 LangGraph로 해결합니다.</p>
<p>흐름 제어가 어렵다 -&gt; 명시적 상태 기반 전이로 해결
에이전트 간 협업이 불투명 -&gt; 각 노드를 정의하고 연결함으로써 해결
복잡한 워크플로우의 디버깅 -&gt; 시각적 흐름으로 디버깅 가능</p>
<h3 id="사전-개념">사전 개념</h3>
<p>코드 리뷰를 진행하기 전에 알아야할 기본 개념들입니다.</p>
<ul>
<li><p>LangChain
: LLM을 위한 체인/에이전트 구성 프레임워크</p>
</li>
<li><p>State 
: 현재 에이전트나 워크플로우가 어떤 정보를 갖고 있는지 나타내는 상태값</p>
</li>
<li><p>Node 
: 그래프에서 하나의 처리 단계를 의미(LLM 호출, 도구 실행)</p>
</li>
<li><p>Edge 
: 상태 간의 전이(어떤 조건이 만족되었을 때 어디로 이동하는가?)</p>
</li>
<li><p>memory 
: 노드간 상태(state)를 공유하고 업데이트하는 역할</p>
</li>
<li><p>Thread id 
: 각 그래프 실행 인스턴스를 고유하게 식별하는 ID
: 같은 그래프라도 서로 다른 유저가 동시에 실행할 수 있다
: 따라서 어떤 사용자의 흐름인지 식별해야 한다</p>
</li>
<li><p>checkpointer 
: LangGraph의 실행 흐름 중간 상태를 디스크나 데이터베이스 등에 저장하는 기능이다. 
: 중간에 끊겨도 상태를 저장해두었다가 이어서 실행이 가능하다
: 복잡한 워크플로우에서 각 단계별 상태 스냅샷의 보관이 가능하다.</p>
</li>
<li><p>graph_builder 
: LangGraph 워크플로우를 정의할 때 사용되는 빌더 객체</p>
</li>
</ul>
<h1 id="코드-설명">코드 설명</h1>
<h2 id="langgraph---basic">LangGraph - Basic</h2>
<p>Dict : 일반적인 key-value 딕셔너리이며 모든 값이 str,int 등 값임을 지정합니다.
TypeDict : Python 3.8에서 등장한 구조로, 딕셔너리를 클래스처럼 정의할 수 있게 해줍니다.</p>
<pre><code># TypedDict
class Person(TypedDict):
    name: str
    age: int
    job: str


typed_dict: Person = {
    &quot;name&quot;: &quot;김영희&quot;,
    &quot;age&quot;: 25,
    &quot;job&quot;: &quot;ai engineer&quot;,
}</code></pre><p>typedDict는 LangGraph의 상태정의(State)를 표현할 때 자주 사용됩나다.
그래프의 상태가 어떤 필드를 갖고, 어떤 타입인지 명시해줘야 그래프 흐름이 더 명확해지기 때문</p>
<p>뿐만 아니라 dict는 아무키나 추가가 가능하지만 typeddict는 사전에 정의된 키만 허용합니다
그래프 상태를 구조화하고, 예상치 못한 값이 흘러가지 않게 하기 위해서입니다.</p>
<hr>
<pre><code>from typing import Annotated, List
from pydantic import Field, BaseModel, ValidationError</code></pre><p>LangGraph의 상태를 정의할 떄 고급 타입과 검증을 지원하기 위해 추가 모듈을 가져옵니다.
Annotated : 타입에 추가 메타데이터를 부여한다. 필드에 설명이나 제약조건을 추가
List : 리스트 타입 지정. 복수 값 필드 타입화
Field : 필드에 기본값, 검증 조건 설정. Pydantic 모델 필드 설정
BaseModel : Pydantic의 기본 모델로 데이터 검증 및 직렬화에 사용</p>
<p>이에 따라 Pydantic 모델을 활용한 데이터 검증을 본격적으로 진행할 수 있습니다.</p>
<pre><code>class Student(BaseModel):
    id: Annotated[str, Field(..., description=&quot;학생ID&quot;)]
    name: Annotated[str, Field(..., min_length=3, max_length=50, description=&quot;이름&quot;)]
    age: Annotated[int, Field(gt=23, lt=31, description=&quot;나이(24~30세)&quot;)]
    skills: Annotated[
        List[str], Field(min_items=1, max_items=10, description=&quot;보유기술(1~10개)&quot;)
    ]</code></pre><p>각 필드는 Annotated를 통해 타입과 추가 검증 조건이 설정되어있다.
따라서 모든 조건이 올바르게 만족되어야 에러가 발생하지 않고 인스턴스가 생성될 수 있다.</p>
<hr>
<pre><code>from langchain_core.messages import AIMessage, HumanMessage
from langgraph.graph import add_messages</code></pre><p>LangGraph에서 메시지 관리는 굉장히 중요한데, 특히 대화형 워크플로우를 구축할 때
HumanMessage와 AIMessage를 구분해서 관리합니다.
AIMessage : AI모델의 응답 메시지를 표현 -&gt; 메시지 흐름에서 사용
HumanMessage : 사용자의 입력 메시지를 표현 -&gt; 사용자-에이전트 대화 관리
add_messages : 두 메시지 리스트를 합치는 함수 -&gt; 상태(State) 업데이트 시 사용</p>
<pre><code>message1 = [HumanMessage(content=&quot;안녕하세요?&quot;, id=&quot;ID-001&quot;)]
message2 = [AIMessage(content=&quot;반갑습니다.&quot;, id=&quot;ID-002&quot;)]

result = add_messages(message1, message2)
print(result)
</code></pre><p>서로 다른 아이디의 메시지를 합친다면 단순히 두 리스트를 이어 붙이는 역할을 한다.
문제없이 병합된다는 의미입니다.</p>
<pre><code>message1 = [HumanMessage(content=&quot;안녕하세요?&quot;, id=&quot;ID-001&quot;)]
message2 = [AIMessage(content=&quot;반갑습니다.&quot;, id=&quot;ID-001&quot;)]</code></pre><p>하지만 같은 ID의 메시지라면 보통은 마지막 메시지가 우선시 됩니다.</p>
<blockquote>
<p>정리
TypedDict와 BaseModel을 이용해 상태(State) 구조를 엄격하게 정의한다.
Pydantic을 이용해 입력 데이터의 타입과 조건을 검증할 수 있도록 준비한다.
HumanMessage와 AIMessage를 이용해 대화 메시지를 객체로 관리한다.
add_messages를 사용하여 여러 메시지를 안전하게 합치고, 상태를 갱신한다.</p>
</blockquote>
<h2 id="langgraph---chatbot">LangGraph - Chatbot</h2>
<p>앞으로 진행될 코드리뷰에서 계속해서 이 코드가 반복됩니다.</p>
<pre><code># API KEY Loading
from dotenv import load_dotenv

load_dotenv()</code></pre><p>이는 API_KEY나 TAVILY_KEY같은 개인키를 불러오는 역할을 합니다.
또한 </p>
<pre><code>from langchain_teddynote import logging

logging.langsmith(&quot;CH21-LangGraph&quot;)
</code></pre><p>해당 코드를 통해 CH21-LangGraph라는 이름으로 LangSmith상에서 로그를 남길 수 있습니다.
<a href="https://smith.langchain.com">https://smith.langchain.com</a> 사이트에서 워크플로우 실행을 추적할 수 있습니다.</p>
<pre><code>from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages</code></pre><p>StartGraph : LangGraph 그래프 빌더. 워크플로우를 설계할 객체
START,END : 그래프 특수 노드. 그래프의 시작점과 종료점을 지정</p>
<h3 id="state-정의">State 정의</h3>
<pre><code>class State(TypedDict):
    messages: Annotated[list, add_messages]</code></pre><p>State라는 클래스를 정의합니다.
이는 LangGraph 워크 플로우에서 사용할 상태 구조를 정의한것으로
TypeDict를 사용해서 messages라는 키만 가진 딕셔너리 형태로 상태를 정의합니다.
이때 messages는 단순 리스트가 아니라 Annotated를 통해 LangGraph가 상태 업데이트 시
자동으로 add_messages 함수를 적용하도록 설정한 것입니다.</p>
<p>즉, 각각의 노드가 출력한 메시지를 messages 필드에 계속 누적하면서 대화 흐름을 관리할 수 있게 되는 구조입니다.</p>
<hr>
<h3 id="node-정의">Node 정의</h3>
<pre><code># 챗봇 함수 정의 
def chatbot(state: State):
    return {
        &quot;messages&quot;: [llm.invoke(state[&#39;messages&#39;])]
    }</code></pre><p>LangGraph 노드에서 사용할 chatbot 동기 호출 함수입니다
입력받은 state(이전 대화 메시지들)를 기반으로 LLM을 호출하고,
그 결과 메시지를 다시 messages에 추가합니다.</p>
<p>반환값은 [] 딕셔너리로 되어있고, messages 키에 AI 응답 메시지가 리스트로 포함됩니다.
이 값은 State의 messages에 자동으로 add_messages를 통해 누적됩니다.</p>
<h3 id="graph-정의">Graph 정의</h3>
<pre><code># 그래프 정의 
graph_builder = StateGraph(State)

# 노드 추가 
graph_builder.add_node(&quot;chatbot&quot;, chatbot)</code></pre><p>StateGraph(State)를 사용해 워크플로우에서 사용할 상태 타입을 설정하고, chatbot이라는 이름의 노드를 정의합니다.
&quot;chatbot&quot;이라는 노드는 위에서 정의한 def chatbot 함수를 수행합니다.</p>
<h3 id="edge-추가">Edge 추가</h3>
<pre><code># 시작점 정의 
graph_builder.add_edge(START, &quot;chatbot&quot;)

## 종료지점 정의 
graph_builder.add_edge(&quot;chatbot&quot;, END)</code></pre><p>START -&gt; &quot;chatbot&quot;노드로 흐름을 연결
즉, 그래프 실행이 시작되면 가장 먼저 &quot;chatbot&quot;노드를 실행하게 됩니다.
&quot;chatbot&quot; -&gt; END 지점으로 흐름을 이어줌
즉, 이 그래프는 &quot;START&quot; → &quot;chatbot&quot; → &quot;END&quot; 순서로 한 번만 실행되고 종료됩니다.</p>
<hr>
<pre><code>graph = graph_builder.compile()</code></pre><p>위에서 설계한 graph_builder의 상태와 노드들을 실행가능한 그래프 객체로 컴파일 합니다.
이후 이 graph 객체를 사용하여 .invoke()등으로 실행이 가능합니다.</p>
<pre><code>from langchain_teddynote.graphs import visualize_graph</code></pre><p>LangChain과 LangGraph 흐름을 시각화 할 수 있는 도구입니다.</p>
<pre><code>question = &quot;서울 맛집 TOP 10 알려주세요&quot;

for event in graph.stream({&quot;messages&quot;: [(&quot;user&quot;, question)]}):
    for value in event.values():
        print(&quot;Assistant: &quot;, value[&quot;messages&quot;][-1].content)</code></pre><p>graph.stream()은 LangGraph 그래프를 단계별로 실행하며 이벤트를 스트리밍으로 전달합니다.
입력으로 {&quot;messages&quot;: [(&quot;user&quot;, question)]} 구조의 상태(state)를 넘깁니다.
각 단계에서 반환된 메시지중 마지막[-1] 메시지를 꺼내어 출력합니다.</p>
<blockquote>
</blockquote>
<p>GPT 기반 챗봇을 LangGraph 워크플로우로 구성하는 예제
한 번의 질문에 GPT가 응답하고 종료하는 단일 턴 챗봇 구조</p>
<h2 id="langgraph-agent">LangGraph-Agent</h2>
<pre><code>from langchain_teddynote.tools.tavily import TavilySearch</code></pre><p>TavilySearch는 웹 검색을 수행하는 LangChain Tool입니다.
에이전트가 외부 정보를 실시간으로 가져오기 위해 사용합니다.
이번 코드는 이 TavilySearch 도구를 에이전트에 연결하여
LangGraph 내에서 Tool 기반 행동을 수행하는 에이전트를 구성하려는 흐름으로 진행됩니다.</p>
<pre><code># 도구 정의 
tool = TavilySearch(max_results=5)

# 도구 목록 반영
tools = [tool]</code></pre><p>TavilySearch의 검색결과를 최대 5개를 가져와 도구로 정의합니다.
이 도구를 tools 리스트에 담아 나중에 에이전트나 LangGraph 그래프에 연결할 수 있게 준비합니.</p>
<pre><code># 도구 실행
print(tool.invoke(&quot;LangGraph Tutorial&quot;))</code></pre><p>TavilySearch 도구에 LangGraph Tutorial이라는 쿼리를 전달하여 웹 검색을 수행합니다.
결과물 출력
<img src="https://velog.velcdn.com/images/dev_init_ung/post/7f571512-3df7-4fdd-a23b-2fef79419bc1/image.png" alt=""></p>
<hr>
<pre><code>from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI</code></pre><p>위에서 정의한대로 상태정의와 메시지 병합 그리고 LLM 준비를 위한 import 구성입니다.
상태를 TypeDict로 정의하고 GPT기반 응답 시스템을 ChatOpenAI로 만들게됩니다.</p>
<pre><code># State 정의 
class State(TypedDict):
    messages: Annotated[list, add_messages]</code></pre><p>LangGraph에서 사용할 상태 클래스를 TypeDict로 정의합니다.
사용자와 AI가 주고받은 메시지를 messages에 저장하고, 
단순히 리스트가 아니라 새로운 메시지가 추가될때마다 병합됩니다.</p>
<pre><code># LLM 정의 
llm = ChatOpenAI(model = &#39;gpt-4o-mini&#39;, temperature=0)

# LLM + Tools 
llm_with_tools = llm.bind_tools(tools)</code></pre><p>앞서 정의한 tools목록을 GPT의 도구로 연결(bind)합니다.
이를 통해 LLM은 일반 텍스트 응답 뿐만 아니라 필요한 경우 TavilySearch 도구를 직접 호출합니다.</p>
<pre><code>def chatbot(state: State):
    answer = llm_with_tools.invoke(state[&quot;messages&quot;])
    return {&quot;messages&quot;: [answer]}</code></pre><p>에이전트 노드 함수를 정의합니다.</p>
<pre><code>from langgraph.graph import StateGraph

# 그래프 초기화 
graph_builder = StateGraph(State)

# 노드 연결
graph_builder.add_node(&quot;chatbot&quot;, chatbot)</code></pre><p>앞서 정의한 State클래스를 기반으로 LangGraph그래프를 초기화합니다.
이 객체에 노드추가, 시작/종료 연결등을 합니다.</p>
<pre><code>def __init__(self, tools: list) -&gt; None:
        # 도구 리스트
        # 주어진 도구 리스트를 이름(name)을 기준으로 딕셔너리 형태로 변환
        self.tools_list = {tool.name: tool for tool in tools}

    def __call__(self, inputs: dict):
        # 메시지가 존재할 경우 가장 최근 메시지 1개 추출
        # inputs 딕셔너리에서 &quot;messages&quot; 키의 값을 가져옴 (없으면 빈 리스트 반환)
        if messages := inputs.get(&quot;messages&quot;, []):
            message = messages[-1]
        else:
            raise ValueError(&quot;No message found in input&quot;)

        # 도구 실행 결과를 저장할 리스트
        outputs = []

        # message 객체 안의 tool_calls 속성에는 LLM이 호출 요청한 도구 정보가 리스트로 저장되어 있음
        for tool_call in message.tool_calls:
            # 도구 이름으로 실제 도구 인스턴스를 가져와서, 전달된 인자(args)를 사용해 실행
            tool_result = self.tools_list[tool_call[&quot;name&quot;]].invoke(tool_call[&quot;args&quot;])

            # 도구 호출 후 결과 저장
            # 도구 실행 결과를 문자열(JSON 형식)로 변환하여 ToolMessage 객체로 저장
            outputs.append(
                # 도구 호출 결과를 메시지로 저장
                ToolMessage(
                    content=json.dumps(
                        tool_result, ensure_ascii=False
                    ),  # 도구 호출 결과를 문자열로 변환
                    name=tool_call[&quot;name&quot;],
                    tool_call_id=tool_call[&quot;id&quot;],
                )
            )

        return {&quot;messages&quot;: outputs}</code></pre><p>GPT가 tool_call을 요청하면 해당 요청을 처리하는 Tool 전용 실행노드입니다.
GPT 응답 안의 .tool_calls 속성을 확인해 
tool_result = self.tools_list[].... 문장에서
도구 이름에 해당하는 실제 Tool객체를 찾아 전달된 args를 넘겨 invoke를 실행합니다.
실제 도구를 실행하고 그 결과글을 ToolMessage로 포장해서 반환합니다.</p>
<p>self.tools.list 문장을 통해 GPT가 &quot;name&quot;:&quot;TavilySearch&quot;로 요청했을 때,
이름으로 바로 찾아 실행할 수 있습니다.</p>
<pre><code># 도구 노드 생성
tool_node = BasicToolNode(tools=[tool])

# 그래프에 도구 노드 추가
graph_builder.add_node(&quot;tools&quot;, tool_node)</code></pre><p>위에서 정의한 BasicToolNode 인스턴스를 생성하고
tools라는 이름으로 그래프에 연결합니다.</p>
<hr>
<h3 id="conditional-edge">Conditional Edge</h3>
<p>조건 분기 노드 연결 이라는 뜻으로 그래프 실행 중 상태(state)의 내용에 따라
다음 노드를 동적으로 결정할 수 있게 해주는 기능입니다.
일반적인 흐름은 다음과 같습니다.</p>
<pre><code>graph_builder.add_edge(&quot;nodeA&quot;, &quot;nodeB&quot;)</code></pre><p>무조건 nodeA 다음에 nodeB
하지만 조건 분기 흐름을 사용하면 다음과 같습니다.</p>
<pre><code>graph_builder.add_conditional_edges(
    source=&quot;nodeA&quot;,
    path=route_function,
    path_map={&quot;yes&quot;: &quot;nodeB&quot;, &quot;no&quot;: &quot;nodeC&quot;}
)
</code></pre><p>nodeA가 끝난 다음, route_function(state)함수의 반환값에 따라
nodeB, nodeC 경로를 지정합니다.
라는 의미입니다.</p>
<p>그리고 조건 분기 함수의 구성은 다음과 같습니다.</p>
<pre><code>def route_function(state):
    ai_message = state[&quot;messages&quot;][-1]  # 가장 최근 메시지
    if hasattr(ai_message, &quot;tool_calls&quot;) and len(ai_message.tool_calls) &gt; 0:
        return &quot;tools&quot;
    return END</code></pre><p>현재 상태(state)안에 들어있는 AIMessage객체를 확인합니다
해당 메시지 안에 tool_calls 속성이 있고, 실제로 호출 요청이 존재하면 tools를 반환하고
그렇지 않으면 END를 반환합니다.</p>
<p>코드의 조건분기 연결설정에서는</p>
<pre><code>graph_builder.add_conditional_edges(
    source=&quot;chatbot&quot;,
    path=route_tools,
    path_map={
        &quot;tools&quot;: &quot;tools&quot;,  # route_tools가 &quot;tools&quot; 반환 시 실행할 노드
        END: END,          # route_tools가 END 반환 시 종료
    }
)</code></pre><p>이렇게 존재하는데 그렇다면 이는
route_tools가 &quot;tools&quot; 반환 시 tools 노드를 다음 노드로 실행하고
route_tools가 END 반환 시 종료한다는 의미인 것입니다.</p>
<pre><code>graph_builder.add_edge(START, &quot;chatbot&quot;)
graph_builder.add_edge(&quot;tools&quot;, &quot;chatbot&quot;)
</code></pre><p>이제 전체 흐름을 마무리 합니다
LangGraph의 시작점에서 &quot;chatbot&quot;으로 흐름에 진입하고
도구 실행을 완료한 tools노드에서 다시 chatbot으로 되돌아가
도구 결과를 기반으로 후속 응답을 생성합니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/ec500fd4-bf7b-48fb-b0fc-107a2ea893f7/image.png" alt=""></p>
<p>점선은 조건부, 실선은 기본을 의미합니다.</p>
<h3 id="실행">실행</h3>
<pre><code>inputs = {&quot;messages&quot;: &quot;SKC&amp;C에서 진행하는 SKALA에 대해서 검색해 주세요&quot;}

for event in graph.stream(inputs, stream_mode=&quot;values&quot;):
    for key, value in event.items():
        print(f&quot;\n==============\nSTEP: {key}\n==============\n&quot;)
        print(value[-1])</code></pre><p>실제로 실행을 한다면
<strong>START -&gt; chatbot 노드</strong>
GPT는 사용자의 질문을 입력으로 받고, 아직 정보가 부족하다고 판단하면
도구 호출(tool_call)을 생성합니다.</p>
<pre><code>tool_calls: [{&quot;name&quot;: &quot;tavily_web_search&quot;, &quot;args&quot;: {&quot;query&quot;: &quot;SKC&amp;C SKALA&quot;}}]</code></pre><p>GPT 응답은 비어있지만 tool_calls에 tavily_web_search 검색 명령이 들어가있음을 확인할 수 있습니다.</p>
<p><strong>조건 분기 route_tools</strong>
route_tools()가 실행되고, tool_call가 있음을 확인 후 tools가 선택됩니다.</p>
<p><strong>tools 노드 실행</strong>
BasicToolNode가 실행되어 실제로 TavilySearch(&quot;SKC&amp;C SKALA&quot;)를 호출
웹에서 정보를 검색한 결과를 ToolMessage형식으로 LangGraph에 저장합니다.</p>
<p><strong>tools -&gt; chatbot</strong>
chatbot노드는 gpt에게 ToolMessage 결과를 넘깁니다.
GPT는 검색 결과를 바탕으로 최종 응답을 생성합니다.</p>
<p><strong>다시 route_tools실행</strong>
이번에는 tool_calls가 없습니다.</p>
<blockquote>
<p>정리
사용자의 질문에 대해 GPT가 도구를 활용해 직접 정보 검색하고,
검색 결과를 바탕으로 최종 응답을 생성하는 지능형 워크플로우 구현</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[에이전트 시대의 시작: MCP와 A2A가 바꿀 비즈니스]]></title>
            <link>https://velog.io/@dev_init_ung/%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%8B%9C%EB%8C%80%EC%9D%98-%EC%8B%9C%EC%9E%91-MCP%EC%99%80-A2A%EA%B0%80-%EB%B0%94%EA%BF%80-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4</link>
            <guid>https://velog.io/@dev_init_ung/%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%8B%9C%EB%8C%80%EC%9D%98-%EC%8B%9C%EC%9E%91-MCP%EC%99%80-A2A%EA%B0%80-%EB%B0%94%EA%BF%80-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4</guid>
            <pubDate>Mon, 21 Apr 2025 23:30:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/ec430d60-aaca-483d-a185-e38cea299712/image.png" alt="">
OpenAI와 마이크로소프트가 오픈소스 AI 프로토콜 &#39;MCP&#39;를 채택하며, AI 에이전트의 웹 자동화와 상호운용성 표준화에 본격 시동을 걸었고, 
AI 에이전트 간 직접 협업을 가능케 하는 &#39;A2A&#39; 프로토콜과 생태계의 등장으로, 인간의 개입 없이도 자동화된 비즈니스 운영이 현실화 되고 있다.</p>
<p>이번 글을 통해 MCP와 A2A의 개념에 대해서 간단하게 살펴보도록 하자.</p>
<p>먼저 결론부터 말하자면,
MCP는 AI와 도구를 연결하는 표준, A2A는 AI끼리 소통하고 협업하는 방식이다.</p>
<h2 id="mcp-model-context-protocol">MCP (Model Context Protocol)</h2>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/c862000a-b030-48ac-8662-3fa19e0a568d/image.png" alt=""></p>
<p>AI 모델이 다양한 도구 및 데이터와 안정적으로 연결되도록 돕는 오픈소스 프로토콜이다.</p>
<p>예를들어 ChatGPT가 웹에서 무언가를 검색하고자 자동화된 행동을 하려면 이런 표준이 필요하다.
GTP나 Claude 같은 LLM은 텍스트를 잘 생성하긴 하지만 실제 비즈니스에서는 실시간 데이터 접근, 인증된 API 사용, 외부 도구 실행등이 필요하다.</p>
<p>당연히 각 도구는 인증방식, 응답 형식이 모두 다르다. 이 문제를 해결하기 위해 등장한것이 MCP라고 이해하면 편하다.</p>
<p>기술적 의의</p>
<ul>
<li>MCP는 LLM이 단순 텍스트 생성기에서 인터랙티브 에이전트로 진화할 수 있는 관문이다</li>
<li>다양한 기업의 API, 도구를 LLM과 연결 가능한 공통 규격으로 묶는다</li>
<li>OAuth 인증 + Streaming 전송을 통해 기업용으로도 적합하다.</li>
</ul>
<hr>
<h2 id="a2a-agent-to-agent">A2A (Agent-to-Agent)</h2>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/3a07c8bc-ef5b-4b46-a0c0-be5d9dc6971f/image.jpg" alt=""></p>
<p>AI 에이전트들이 서로 협업하고 대화하며 업무를 처리하는 구조로 사람의 개입 없이 자동으로 진행된다.</p>
<p>기존에는 AI가 인간의 요청에 반응하는 1:1 관계가 일반적이었다.
하지만 점점 더 많은 AI기능이 필요해지면서 AI끼리 역할을 나누어 협업하는 방식이 등장했다.</p>
<p>실제로 구글에서는 오픈 A2A 프로토콜을 발표함과 동시에 50개의 기업과 협업을 발표했고,
센드버그에서는 AI 고객상담 봇들끼리 협업해 영업-&gt;상담-&gt;결제를 자동으로 처리하는 방식을 보여주기도 하였다.</p>
<p>기술적 의의</p>
<ul>
<li>사용자가 버튼 하나만 눌러도 여러 AI가 협업하여 결과를 생성한다</li>
<li>1개의 거대 모델이 모든걸 하는 시대가 도래한다(작고 효율적인 모델들이 역할 분담하는 구조)</li>
</ul>
<hr>
<h2 id="정리">정리</h2>
<p>MCP는 도구 연결의 표준화, A2A는 에이전트 협업의 자동화이다.
두 기술은 LLM의 활용을 넘어, AI가 실제 비즈니스 프로세스에 깊이 관여할 수 있는 핵심 인프라로 부상 중이다.
특히 OAuth 기반 보안, 실시간 처리, 상호운용성 등은 기업 시스템 전반에 걸쳐 AI 자동화의 기준을 재정의하게 될 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[

[코드리뷰]조인 형태에 따른 SQL 실행]]></title>
            <link>https://velog.io/@dev_init_ung/%EC%A1%B0%EC%9D%B8-%ED%98%95%ED%83%9C%EC%97%90-%EB%94%B0%EB%A5%B8-SQL-%EC%8B%A4%ED%96%89-17</link>
            <guid>https://velog.io/@dev_init_ung/%EC%A1%B0%EC%9D%B8-%ED%98%95%ED%83%9C%EC%97%90-%EB%94%B0%EB%A5%B8-SQL-%EC%8B%A4%ED%96%89-17</guid>
            <pubDate>Sun, 20 Apr 2025 23:24:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/fe9d25d5-35f6-45a4-af7b-17869ca5fbed/image.png" alt="">
패밀리 레스토랑 신규 매출을 분석하기 위한 Query문으로
주문, 예약 데이터 분석을 통해 기본적인 집계함수, 조건벌 집계, 날짜 파싱, 조인 등
SQL문에 대해 이해하기 쉬운 예제들로 구성되어 있다.</p>
<h3 id="사전-지식">사전 지식</h3>
<p>JOIN</p>
<ul>
<li>두 개 이상의 테이블을 특정 조건을 기준으로 연결하는 연산</li>
</ul>
<p>GROUP BY </p>
<ul>
<li>특정 컬럼 단위로 그룹화해서 집계 수행</li>
</ul>
<p>집계함수 (SUM, AVG, COUNT, MAX, MIN)</p>
<ul>
<li>그룹 혹은 전체 행에 대해 계산을 수행</li>
</ul>
<p>DECODE / CASE WHEN</p>
<ul>
<li>조건문 로직을 SQL에서 처리할 수 있게 함</li>
</ul>
<p>SUBSTR()</p>
<ul>
<li>문자열 자르기. 날짜에서 월만 추출할 때 주로 사용</li>
</ul>
<p>TO_CHAR, TO_DATE</p>
<ul>
<li>날짜 포맷을 변경하거나 파싱할 때 사용</li>
</ul>
<p>RANK() OVER(PARTITION BY ... ORDER BY ...)</p>
<ul>
<li>그룹별 순위를 매기는 윈도우 함수</li>
</ul>
<p>UNION</p>
<ul>
<li>두 쿼리의 결과를 합치는 집합 연산자</li>
</ul>
<p>ROUND()</p>
<ul>
<li>반올림 함수</li>
</ul>
<h3 id="🔍-sql1-전체-주문-건수-총매출-평균최고최저-매출">🔍 [SQL#1] 전체 주문 건수, 총매출, 평균/최고/최저 매출</h3>
<pre><code>SELECT COUNT(*)             AS 주문건수,
       SUM(B.sales)         AS 총매출,
       ROUND(AVG(B.sales))  AS 평균매출,
       MAX(B.sales)         AS 최고매출,
       MIN(B.sales)         AS 최저매출
FROM reservation A
JOIN order_info B ON A.reserv_no = B.reserv_no;
</code></pre><p>INNER JOIN
-&gt; 예약 테이블과 주분 테이블을 예약 번호를 기준으로 조인합니다
COUNT(*)
-&gt; 총 주문 건수를 계산합니다
SUM(B.sales)
-&gt; 주문 매출 합계
ROUND(AVG(B.sales)
-&gt; 평균 매출을 반올림해서 출력
MAX(B.sales) / MIN(B.sales)
-&gt; 가장 높은 매출, 가장 낮은 매출</p>
<h3 id="🔍-sql2-전체-판매건수-전용상품m0001-집계">🔍 [SQL#2] 전체 판매건수, 전용상품(M0001) 집계</h3>
<pre><code>SELECT COUNT(*) AS 전체판매건수,
       ROUND(SUM(B.sales), 2) AS 총매출액,
       SUM(CASE WHEN B.item_id = &#39;M0001&#39; THEN 1 ELSE 0 END) AS 전용상품판매건수,
       ROUND(SUM(CASE WHEN B.item_id = &#39;M0001&#39; THEN B.sales ELSE 0 END), 2) AS 전용상품매출
FROM reservation A
JOIN order_info B ON A.reserv_no = B.reserv_no
WHERE A.cancel = &#39;N&#39;;</code></pre><p>CASE WHEN
-&gt; 특정 조건을 만족할 때만 값을 계산
SUM(CASE WHEN ... THEN 1 ...)
-&gt; 전용 상품 판매 횟수 (M0001)
SUM(CASE WHEN ... THEN B.sales ...)
-&gt; 전용 상품 매출
ROUND(..., 2)
-&gt; 소수점 둘째 자리까지 반올림
WHERE A.cancel = &#39;N&#39;
-&gt; 예약취소 건은 포함시키지 않아야 하니</p>
<h3 id="🔍-sql3-상품별-매출-내림차순-정렬">🔍 [SQL#3] 상품별 매출 내림차순 정렬</h3>
<pre><code>SELECT C.item_id,
       C.product_name,
       SUM(B.sales) AS 상품매출
FROM reservation A
JOIN order_info B ON A.reserv_no = B.reserv_no
JOIN item C ON B.item_id = C.item_id
WHERE A.cancel = &#39;N&#39;
GROUP BY C.item_id, C.product_name
ORDER BY 상품매출 DESC;</code></pre><p>3개 테이블 조인
-&gt; 예약 → 주문 → 상품
GROUP BY
-&gt; 상품 ID, 이름으로 묶어 집계
ORDER BY ... DESC
-&gt; 매출이 높은 순으로 정렬</p>
<h3 id="🔍-sql4-월별-상품-매출-집계">🔍 [SQL#4] 월별 상품 매출 집계</h3>
<pre><code># 기존 코드
SELECT SUBSTR(A.reserv_date,1,6) AS 월,
       SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS SPECIAL_SET,
       SUM(DECODE(B.item_id,&#39;M0002&#39;,B.sales,0)) AS PASTA,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS PIZZA,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS SEA_FOOD,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS STEAK,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS SALAD_BAR,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS SALAD,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS SANDWICH,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS WINE,
       SUM(DECODE(B.item_id,&#39;M0003&#39;,B.sales,0)) AS JUICE
FROM reservation A, order_info B
WHERE A.reserv_no = B.reserv_no
AND   A.cancel = &#39;N&#39;
GROUP BY SUBSTR(A.reserv_date,1,6)
ORDER BY SUBSTR(A.reserv_date,1,6);</code></pre><p>SUBSTR(A.reserv_date,1,6)
-&gt; YYYYMM 형태로 월 추출
DECODE(item_id, &#39;M0001&#39;, sales, 0)
-&gt; item_id가 M0001일 경우에만 매출을 더함
GROUP BY 월
-&gt; 월별로 집계</p>
<h3 id="🔍-sql5-월별-총매출과-전용상품-매출">🔍 [SQL#5] 월별 총매출과 전용상품 매출</h3>
<pre><code>SELECT SUBSTR(A.reserv_date,1,6) AS 월,
       SUM(B.sales) AS 총매출,
       SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 전용상품매출
FROM reservation A, order_info B
WHERE A.reserv_no = B.reserv_no
AND   A.cancel = &#39;N&#39;
GROUP BY SUBSTR(A.reserv_date,1,6)
ORDER BY SUBSTR(A.reserv_date,1,6);</code></pre><p>SUBSTR(A.reserv_date,1,6) AS 월
-&gt; 예약일자에서 연월(YYYYMM) 형태 추출 (예: 202501)
SUM(B.sales)
-&gt; 월 전체 매출 합계
SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0))
-&gt; 전용 상품(&#39;M0001&#39;)에 해당하는 매출만 합산
WHERE A.cancel = &#39;N&#39; 
-&gt; 취소되지 않은 예약만 집계
GROUP BY / ORDER BY SUBSTR(...)
-&gt; 연월 기준으로 그룹화하고 오름차순 정렬</p>
<h3 id="🔍-sql6-전용상품-매출-기여율-추가">🔍 [SQL#6] 전용상품 매출 기여율 추가</h3>
<pre><code>SELECT SUBSTR(A.reserv_date,1,6) AS 월,
       SUM(B.sales) - SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 기타매출,
       SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 전용매출,
       ROUND(SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0))/SUM(B.sales)*100,1) AS 기여율
FROM reservation A, order_info B
WHERE A.reserv_no = B.reserv_no
AND   A.cancel = &#39;N&#39;
GROUP BY SUBSTR(A.reserv_date,1,6)
ORDER BY SUBSTR(A.reserv_date,1,6);
</code></pre><p>SUM(B.sales) - SUM(DECODE(...))
-&gt; 기타 상품 매출 = 총매출 - 전용상품 매출
ROUND(...,1)
-&gt; 기여율을 소수 첫째 자리까지 반올림</p>
<h3 id="🔍-sql7-예약취소-건수-포함-월별-매출-요약">🔍 [SQL#7] 예약/취소 건수 포함 월별 매출 요약</h3>
<pre><code>SELECT SUBSTR(A.reserv_date,1,6) AS 월,
       SUM(B.sales) AS 총매출,
       SUM(B.sales) - SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 기타상품매출,
       SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 전용상품매출,
       ROUND(SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0))/SUM(B.sales)*100,1) AS 기여율,
       COUNT(A.reserv_no) AS 예약건수,
       SUM(DECODE(A.cancel,&#39;N&#39;,1,0)) AS 완료건,
       SUM(DECODE(A.cancel,&#39;Y&#39;,1,0)) AS 취소건
FROM reservation A, order_info B
WHERE A.reserv_no = B.reserv_no(+)
GROUP BY SUBSTR(A.reserv_date,1,6)
ORDER BY SUBSTR(A.reserv_date,1,6);
</code></pre><p>A.reserv_no = B.reserv_no(+)
-&gt; LEFT OUTER JOIN: 예약은 존재하지만 주문이 없을 수도 있음 (예: 예약만 했다가 취소된 경우 포함)
SUM(DECODE(A.cancel, &#39;N&#39;, 1, 0))
→ 예약 상태(N/Y)에 따라 완료건, 취소건 카운팅</p>
<h3 id="🔍-sql8-퍼센트-표시-추가-기여율--취소율">🔍 [SQL#8] 퍼센트 표시 추가 (기여율 + 취소율)</h3>
<pre><code>SELECT SUBSTR(A.reserv_date,1,6) AS 월,
       SUM(B.sales) AS 총매출,
       SUM(B.sales) - SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 기타상품매출,
       SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 전용상품매출,
       ROUND(SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0))/SUM(B.sales)*100,1)||&#39;%&#39; AS 전용상품기여율,
       COUNT(A.reserv_no) AS 예약건수,
       SUM(DECODE(A.cancel,&#39;N&#39;,1,0)) AS 완료건,
       SUM(DECODE(A.cancel,&#39;Y&#39;,1,0)) AS 취소건,
       ROUND(SUM(DECODE(A.cancel,&#39;Y&#39;,1,0))/COUNT(A.reserv_no)*100,1)||&#39;%&#39; AS 취소율
FROM reservation A, order_info B
WHERE A.reserv_no = B.reserv_no(+)
GROUP BY SUBSTR(A.reserv_date,1,6)
ORDER BY SUBSTR(A.reserv_date,1,6);
</code></pre><p>% 기호를 붙이기 위해 || &#39;%&#39; 문자열 연결 사용</p>
<h3 id="🔍-sql9-월별-전용상품-매출을-요일별로-출력">🔍 [SQL#9] 월별 전용상품 매출을 요일별로 출력</h3>
<pre><code>SELECT SUBSTR(reserv_date,1,6) AS 월,
       A.product_name AS 상품명,
       SUM(DECODE(A.WEEK,&#39;1&#39;,A.sales,0)) AS 일,
       SUM(DECODE(A.WEEK,&#39;2&#39;,A.sales,0)) AS 월,
       SUM(DECODE(A.WEEK,&#39;3&#39;,A.sales,0)) AS 화,
       SUM(DECODE(A.WEEK,&#39;4&#39;,A.sales,0)) AS 수,
       SUM(DECODE(A.WEEK,&#39;5&#39;,A.sales,0)) AS 목,
       SUM(DECODE(A.WEEK,&#39;6&#39;,A.sales,0)) AS 금,
       SUM(DECODE(A.WEEK,&#39;7&#39;,A.sales,0)) AS 토
FROM (
    SELECT A.reserv_date,
           C.product_name,
           TO_CHAR(TO_DATE(A.reserv_date, &#39;YYYYMMDD&#39;),&#39;D&#39;) AS WEEK,
           B.sales
    FROM reservation A, order_info B, item C
    WHERE A.reserv_no = B.reserv_no
    AND   B.item_id = C.item_id
    AND   B.item_id = &#39;M0001&#39;
) A
GROUP BY SUBSTR(reserv_date,1,6), A.product_name
ORDER BY SUBSTR(reserv_date,1,6);</code></pre><p>서브쿼리 내부
TO_DATE(A.reserv_date, &#39;YYYYMMDD&#39;)
-&gt; 문자열 날짜 → 날짜 형식 변환
TO_CHAR(..., &#39;D&#39;)
-&gt; 요일 숫자 추출 (&#39;1&#39;: 일요일, &#39;7&#39;: 토요일 / DB 설정에 따라 다름)
&#39;M0001&#39;
-&gt; 전용상품만 필터링
상품명, 매출, 요일 추출</p>
<p>바깥 쿼리
SUBSTR(reserv_date, 1, 6)
-&gt; 연월 추출
DECODE(WEEK, &#39;1&#39;, ..., &#39;2&#39;, ..., ...)
-&gt; 요일별 매출 분리 집계
GROUP BY 월, 상품명</p>
<h3 id="🔍-sql10-월별-전용상품-매출-13위-지점-출력">🔍 [SQL#10] 월별 전용상품 매출 1~3위 지점 출력</h3>
<pre><code>SELECT *
FROM (
    SELECT SUBSTR(A.reserv_date,1,6) AS 월,
           A.branch AS 지점,
           SUM(B.sales) AS 전용상품매출,
           RANK() OVER(
               PARTITION BY SUBSTR(A.reserv_date,1,6)
               ORDER BY SUM(B.sales) DESC
           ) AS 순위
    FROM reservation A, order_info B
    WHERE A.reserv_no = B.reserv_no
      AND A.cancel = &#39;N&#39;
      AND B.item_id = &#39;M0001&#39;
    GROUP BY SUBSTR(A.reserv_date,1,6), A.branch
)
WHERE 순위 &lt;= 3;</code></pre><p>RANK() OVER(PARTITION BY 월 ORDER BY 매출 DESC)
→ 월별 지점 매출 순위 계산
PARTITION BY SUBSTR(A.reserv_date,1,6)
→ 각 월별로 RANK 따로 계산
WHERE 순위 &lt;= 3
→ 1~3위만 필터링</p>
<h3 id="🔍-sql11-월별-요약--전용상품-1위-지점-리포트">🔍 [SQL#11] 월별 요약 + 전용상품 1위 지점 리포트</h3>
<pre><code>SELECT A.월, MAX(총매출), MAX(전용매출), MAX(기여율), MAX(예약건수),
       MAX(완료건수), MAX(취소건수), MAX(취소율), MAX(상위지점), MAX(지점매출)
FROM (
    SELECT SUBSTR(A.reserv_date,1,6) AS 월,
           SUM(B.sales) AS 총매출,
           SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0)) AS 전용매출,
           ROUND(SUM(DECODE(B.item_id,&#39;M0001&#39;,B.sales,0))/SUM(B.sales)*100,1)||&#39;%&#39; AS 기여율,
           COUNT(A.reserv_no) AS 예약건수,
           SUM(DECODE(A.cancel,&#39;N&#39;,1,0)) AS 완료건수,
           SUM(DECODE(A.cancel,&#39;Y&#39;,1,0)) AS 취소건수,
           ROUND(SUM(DECODE(A.cancel,&#39;Y&#39;,1,0))/COUNT(A.reserv_no)*100,1)||&#39;%&#39; AS 취소율,
           &#39;&#39; AS 상위지점,
           0 AS 지점매출
    FROM reservation A, order_info B
    WHERE A.reserv_no = B.reserv_no(+)
    GROUP BY SUBSTR(A.reserv_date,1,6)
UNION
    SELECT 월, 0, 0, &#39;&#39;, 0, 0, 0, &#39;&#39;, A.branch, A.지점매출
    FROM (
        SELECT SUBSTR(A.reserv_date,1,6) AS 월,
               A.branch,
               SUM(B.sales) AS 지점매출,
               ROW_NUMBER() OVER(
                   PARTITION BY SUBSTR(A.reserv_date,1,6)
                   ORDER BY SUM(B.sales) DESC
               ) AS 순위
        FROM reservation A, order_info B
        WHERE A.reserv_no = B.reserv_no
          AND A.cancel = &#39;N&#39;
          AND B.item_id = &#39;M0001&#39;
        GROUP BY SUBSTR(A.reserv_date,1,6), A.branch
    ) A
    WHERE A.순위 = 1
) A
GROUP BY A.월
ORDER BY A.월;
</code></pre><p>두 가지 데이터를 UNION으로 합친 후, 월별로 MAX()를 통해 통합 출력</p>
<ol>
<li>매출 요약 정보 (총매출, 예약건수 등)</li>
<li>전용상품 매출 1위 지점과 매출</li>
</ol>
<p>MAX()는 UNION 결과를 월별 하나로 합치기 위해 사용된 집계 함수</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSA에서 Kafka, Elasticsearch, Redis 쉽게 이해하기]]></title>
            <link>https://velog.io/@dev_init_ung/%EA%B4%80%EA%B3%84%ED%98%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@dev_init_ung/%EA%B4%80%EA%B3%84%ED%98%95-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Sun, 20 Apr 2025 07:48:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/8ee36f19-8350-4e5b-804b-a7ff22fee3ef/image.png" alt=""></p>
<p>관계형 데이터 베이스 기반의 MSA에서 kafka, elasticsearch, redis가 어떤 역할을 할까?
각 구성 요소에 대한 이해는 MSA에서 데이터를 어떻게 효율적으로 전달하고 처리하며 저장하는지를 이해하는데 핵심요소이다.</p>
<h2 id="사전-개념">사전 개념</h2>
<h3 id="msamicroservices-architecture">MSA(Microservices Architecture)</h3>
<p>하나의 큰 시스템을 여러 개의 독립적인 작은 서비스 단위로 나누어 개발, 배포, 운영하는 아키텍처 스타일이다.
이때 서비스 간 통신은 REST API, 메시지 브로커(Kafka)등을 통해 이루어진다.
서비스 간 DB를 공유하지 않고 <strong>독립적인 DB</strong>를 사용하는 것이 원칙이다.</p>
<h3 id="rdbrelational-database">RDB(Relational Database)</h3>
<p>각 MSA는 다제 데이터베이스를 가지고 있으며, RDB를 사용하는 경우가 많다
그러나 RDB많으로는 로그 수집, 메시지 큐, 캐싱 등 다양한 기능을 효율적으로 처리하기 어렵다
따라서 Kafka, Elasticsearch, Redis같은 비관계형 도구들이 보완적으로 사용되는 것</p>
<hr>
<h2 id="kafka---비동기-메시지-전달이벤트-브로커">Kafka - 비동기 메시지 전달(이벤트 브로커)</h2>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/3426ed8f-d137-4a35-a0ae-ae40638a81e7/image.png" alt=""></p>
<p>대용량의 데이터를 비동기적으로 전달하는 분산 메시지 스트리밍 플랫폼이다.
MSA간의 비동기 통신을 가능하게 만들어준다
서비스 간의 강한 결합을 피하면서 메시지의 유실 없이 저장이 가능하다.</p>
<p>핵심 고려사항은 다음과 같다.</p>
<h3 id="exactly-once">Exactly once</h3>
<p>Kafka는 기본적으로 메시지를 한번 이상(at least once) 보낸다.
이에 따라 DB에 같은 메시지를 두번 저장할 수 도 있다.
이 때 idempotent key(중복 방지 키)를 써서 이미 처리한 메시지인지 확인한다
또는 트랜잭션 기능으로 한 번만 저장하도록 묶는다.</p>
<h3 id="오프셋-관리">오프셋 관리</h3>
<p>Kafka에서 메시지를 읽을때 책갈피처럼 offset을 저장한다
오프셋을 commit 하느냐에 따라 메시지가 누락되거나 중복 처리 될 수 있다.
따라서 적절한 commmit &amp; rollback 전략이 필요하다</p>
<h3 id="schema-관리">Schema 관리</h3>
<p>메시지의 형식이 바뀌면 데이터를 읽는 쪽에서 해석할 수 없게 된다.
따라서 Schema Registry를 사용해서 데이터 형식을 등록하고 버전을 관리한다.</p>
<h3 id="back-pressure처리">Back-pressure처리</h3>
<p>Consumer(받는 쪽)가 너무 느리면 Kafka에 메시지가 쌓인다
이럴 때는 batch로 묶어서 처리하거나, 느린 속도에 맞춰서 조절한다.</p>
<h3 id="메시지-순서-보장">메시지 순서 보장</h3>
<p>Kafka는 Partition을 기준으로 메시지를 나눈다.
같은 키(ex: user_id)를 묶으면 순서가 유지된다</p>
<h3 id="장애-복구-전략">장애 복구 전략</h3>
<p>오류난 메시지는 메시지를 다시 보내거나 DLA(Dead Letter Queue)에 따로 저장해서 나중에 처리한다.</p>
<hr>
<h2 id="elasticsearch---빠른-검색-및-로그-분석">Elasticsearch - 빠른 검색 및 로그 분석</h2>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/806cec9f-340f-43d1-94c3-b8ced561bf96/image.png" alt=""></p>
<p>분산 검색 엔진으로, 대용량 데이터를 실시간으로 검색, 분석할 수 있다.
JSON 기반으로 문서를 저장하며, NoSQL의 일종이다.
RDB보다 검색 성능이 뛰어나서 사용하는 것으로 복잡한 텍스트를 검색하고 필터링하는데 뛰어나다.
뿐만 아니라 실시간 로그 분석 및 에러 추적에 효과적이다.</p>
<p>핵심 고려사항은 다음과 같다.</p>
<h3 id="데이터-동기화-전략">데이터 동기화 전략</h3>
<p>DB와 ES의 데이터는 동기화 되어야 한다
DB의 데이터가 바뀌면 ES에서도 반영되어야 한다.
Kafka,Logstash,CDC 도구를 써서 자동으로 동기화시킨다</p>
<h3 id="index-설계">Index 설계</h3>
<p>index는 검색을 위한 디렉토리이다.
따라서 어떤 구조로 데이터를 쌓을지 어떻게 계획하냐에 따라 검색 속도가 달라진다.</p>
<h3 id="실시간성-이슈">실시간성 이슈</h3>
<p>ES는 DB처럼 데이터가 완전히 최신 상태이지 않을 수 있다.
이를 eventual consistency라고 한다</p>
<h3 id="bulk-처리">Bulk 처리</h3>
<p>하나하나 넣는것보다 여러개를 묶어서 처리해야 빠르다</p>
<h3 id="데이터-중복-방지">데이터 중복 방지</h3>
<p>문서에 고유 ID를 넣어주지 않으면 중복 저장이 된다.</p>
<h3 id="검색-성능-최신화">검색 성능 최신화</h3>
<p>검색어를 잘게 쪼개면(ngram) 검색어 가중치 조절 등으로 성능을 높일 수 있다
하지만 너무 많은 와일드카드는 느려진다는 위험성이 있다.</p>
<hr>
<h2 id="redis---초고속-데이터-캐싱">Redis - 초고속 데이터 캐싱</h2>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/50b1037a-9bcb-4636-8efb-5b2dbcb1d40d/image.png" alt=""></p>
<p>인메모리 기반의 Key-Value 캐시 저장소이다.
데이터를 디스크가 아닌 RAM에 저장하므로 속도가 매우 빠르다
DB나 외부 API 요청 결과를 캐싱하여 속도를 높인다
이를 통해 DB의 부하가 감소된다.</p>
<p>핵심 고려사항은 다음과 같다.</p>
<h3 id="캐시-무효화-전략">캐시 무효화 전략</h3>
<p>캐시는 오래된 데이터를 가지고 있을 수 있다
이때 DB와 캐시를 어떻게 동기화할지 전략이 필요하다
(Write-through, write-behind, cache aside)</p>
<h3 id="ttl-설정">TTL 설정</h3>
<p>Time To Live를 정해서 자동으로 캐시를 삭제하게 한다
그렇지 않다면 캐시에 오래된 데이터가 남게 된다.</p>
<h3 id="key-설계">Key 설계</h3>
<p>데이터를 구분하기 쉬운 명확한 key를 쓰고, TTL도 잘 지정해야 한다</p>
<h3 id="데이터-동기화">데이터 동기화</h3>
<p>Redis는 임시 저장소이다. 따라서 항상 DB와 같이 쓰는게 원칙이다.</p>
<h3 id="메모리-제한">메모리 제한</h3>
<p>Redis는 메모리 기반이라 꽉 차면 데이터를 버리는 정책이 필요하다
(LRU, LFU)</p>
<h3 id="장애-복구-대비">장애 복구 대비</h3>
<p>Redis가 장애가 발생한다면 Sentinel이나 Cluster 모드를 통해 
Redis를 여러 개로 구성해서 고장에도 대비한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[최단 거리 알고리즘 완전 정복]]></title>
            <link>https://velog.io/@dev_init_ung/%EC%B5%9C%EB%8B%A8-%EA%B1%B0%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5</link>
            <guid>https://velog.io/@dev_init_ung/%EC%B5%9C%EB%8B%A8-%EA%B1%B0%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5</guid>
            <pubDate>Fri, 18 Apr 2025 01:53:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/cf1d2079-9532-4dbf-a01a-2f6d7f5fb730/image.png" alt="">
알고리즘 문제를 풀이하면서 가장 많이 만나는 문제 유형인 최단 거리 알고리즘에 대해 정리해보도록 하겠다.
보통 최단거리 알고리즘은 간선(edge)의 가중치 유무, 음수 가중치 존재 여부, 모든 정점 간 vs 단일 출발점 문제에 따라 선택이 달라진다.
먼저 결론부터 말하자면,</p>
<ul>
<li>가중치가 없거나 모두 1이면 BFS</li>
<li>양수 가중치일 경우 다익스트라</li>
<li>음수 가중치를 포함할 경우 bellman ford</li>
<li>모든 정점간의 경로를 구하고자 한다면 플로이드 워셜
알고리즘을 활용한다.</li>
</ul>
<h1 id="다익스트라-알고리즘-dijkstra">다익스트라 알고리즘 (Dijkstra)</h1>
<p>두 노드를 잇는 가장 짧은 경로 즉 최단경로를 찾아야 할 때 주로 사용한다.<br>또한 가중치가 존재하는 그래프에서 주로 사용한다.   </p>
<p>첫 노드를 기준으로 연결되어 있는 노드들을 추가하며 최단 거리를 갱신하는 알고리즘이다.<br>A에서 출발하여 F까지 가는 최단 거리를 찾는다.<br>라는 의미로 해석하면 이해하기 쉽다.   </p>
<blockquote>
</blockquote>
<p>한줄요약 : 가중치가 양수일 때, 하나의 출발점에서 모든 정점까지의 최단거리를 구하는 알고리즘이다.
핵심 : Greedy방식으로 현재 가까운 노드를 우선적으로 방문하며 거리를 갱신한다. Priority Queue를 사용하면 효율적이다.
시간 복잡도 : O((N + E) log N)</p>
<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/66858b93-1afd-4a78-bcc8-a547ff400bc4/image.png" width=70%, height = 70%></p>

<p>그래프를 보며 쉽게 이해해보도록 하자
각 상황에서 본인을 제외한 최단노드를 뽑아야 하기 때문에 우선순위 큐가 필요한것이다.<br>노드의 갯수만큼 배열을 생성, 출발노드의 값은 0, 나머지 값들은 INF(무한)으로 초기화한다.<br>우선순위 큐를 생성하고, 첫 노드의 가중치를 0으로 한다.   </p>
<p>우선 순위 큐에서 값을 추출하고 인접한 노드들과 거리를 계산한다.<br>위 그림을 예로 든다면<br>A -&gt; B : 9 + 0(A)<br>A -&gt; C : 1 + 0(A)<br>A -&gt; D : 15 + 0(A)<br>해당 값들이 INF로 저장된 값들보다 작기 때문에 update<br>그리고 방문한 노드들을 우선순위 큐에 저장한다.<br>해당 단계 후 변화된 배열<br>A B C D E F<br>0 9 1 15 INF INF<br>우선순위 큐(거리가 짧은 순으로)<br>C B D   </p>
<hr>
<p>이제 해당 단계를 우선순위 큐가 빌 때까지 진행한다
이해를 위해 몇단계만 더 진행해보도록 하겠다 
C -&gt; B : 5 + 1(C)
C -&gt; E : 3 + 1(C)
변화된 배열
A B C D E F
0 6 1 15 4 INF
우선순위 큐
E B(6) B(9) D</p>
<hr>
<p>E -&gt; F : 7 + 4(E)
변화된 배열
A B C D E F
0 6 1 15 4 11
우선순위 큐
B(6) B(9) F D</p>
<hr>
<p>그렇게 최종 결과는 다음과 같이 종료된다
A B C D E F
0 6 1 15 4 11</p>
<h1 id="플로이드-워셜-알고리즘floyd-warshall">플로이드-워셜 알고리즘(Floyd-Warshall)</h1>
<p>그래프에서 자주 출제되는 문제 유형이다.
크루스칼 알고리즘과 플루이드 와샬이 대표적인 그리디 알고리즘이다.
그 중 여기서는 플로이드 워셜 알고리즘에 대해 알아보자.</p>
<p>&#39;모든 지점&#39;에서 &#39;모든 지점&#39;까지의 최단 경로를 모두 구해야할 때 사용할 수 있는 알고리즘이다.</p>
<p>매번 방문하지 않은 노드중에서 최단 거리를 갖는 노드를 찾
을 필요가 없다는 점에서 다익스트라와 다른점이라고 할 수 있다.</p>
<blockquote>
</blockquote>
<p>한줄요약 : 모든 정점 간의 최단거리를 구하는 DP 기반 알고리즘
핵심 : 각 정점이 경유지가 될 수 있다는 점에 착안해 3중 루프로 모든 경로를 갱신
시간 복잡도 : O(N^3)
주의 : N이 500 이하일때만 가능</p>
<p>총 N번의 단계를 수행하며, 수행하는 단계마다 O(N^2)의 연산을 통해 계산한다 
따라서 N * O(N^2) -&gt; O(N^3)의 시간복잡도를 가진다.</p>
<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/28bc8395-8a0a-4a2b-bb5d-63d4616ed1ff/image.png" width=50%, height = 50%></p>

<p>그래프를 보며 쉽게 이해해보도록 하자
위의 그림을 예로 들 때, 1에서 5까지 가는 최소 비용은
1 - 2 - 5 -&gt; 비용 3이다.</p>
<pre><code>func Floyd_Warshall() {
  for i in 0..&lt;N {
    for j in 0..&lt;N {
      for k in 0..&lt;N {
        node[j][k] = min(node[j][i] + node[i][j], node[j][k])
      }
    }
  }
}</code></pre><p>코드가 복잡하지 않아 이해하는데는 어려움이 없을것이다.
node[j][k]값과 node[j][i] + node[i][k]중 작은값을 결과값으로 지정한다.
즉, j에서 k까지 갈때, i라는 경유지를 거쳐서 가는 경우를 비교하는 것이다.</p>
<p>해당 경우를 이용해 알고리즘 문제를 풀이한 경우가 있다
<a href="https://github.com/ww5702/Swift_Coding_Test/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/Level%203/%ED%95%A9%EC%8A%B9%20%ED%83%9D%EC%8B%9C%20%EC%9A%94%EA%B8%88">2021 KAKAO BLIND RECURITMENT - 합승 택시 요금</a></p>
<blockquote>
</blockquote>
<p>이쯤에서 궁금한 점이 있다.
시간복잡도가 O(N^3)인 알고리즘을 왜 알아야할까?
다익스트라 알고리즘은 그래프에서 최단 거리를 찾는 가장 대표적인 알고리즘이다.
특정시작지점에서 모든 노드까지의 최소 거리를 알 수 있는데
그냥 N번 반복하여 각각의 노드를 전부 시작지점으로 설정해서 모든 노드로의 거리를 구하면 끝 아닌가?
복잡도 또한 다익스트라 O(VlogV + E)에 N번 반복 = O(N(VlogV + E))이 될텐데 말이다.
이유는 단순하다.
플로이드 워셜 알고리즘은 다익스트라와 달리 가중치가 &#39;음수&#39;여도 가능하기 때문이다.
하지만 3중 반복문의 크기는 상당히 크기 때문에 노드의 크기가 500 이상이라면 이용하기 쉽지 않다.</p>
<h1 id="깊이-우선-탐색dfs">깊이 우선 탐색(DFS)</h1>
<p>말 그대로 깊이 우선 탐색, 너비 우선 탐색이다.
DFS는 가중치와 무관하게 깊이 우선 탐색을 수행하며, 최단 거리 보장은 하지 않는다.</p>
<blockquote>
</blockquote>
<p>한줄 요약 : 한 방향으로 깊게 탐색하는 방식으로, 최단거리를 보장하지는 않지만 경로 탐색에 활용
핵심 : 스택 또는 재귀 호출을 통해 깊이 우선 탐색
시간복잡도 : O(V + E)</p>
<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/41ec3b05-8ee7-4726-8cad-f62bf6a3e73d/image.png" width=70%, height = 70%></p>


<p>위 그림대로 탐색노드의 인접 노드의 자식 노드들을 모두 탐색하고, 다시 돌아가서 탐색노드의 다른 인접노드로 향하는 방식이다.
보통 큐하나와 스택 하나로 구성이 된다.
방문한 VisitedQueue as Q(FIFO)
방문해야 할 needVisitStack as S(LIFO)이다.
위 사진을 그래프로 표기하면 이와 같다.</p>
<pre><code>1 - 2 8
2 - 3 6 7
8 - 9
3 - 4 5
6 - 2
7 - 2
9 - 8
4 - 3
5 - 4</code></pre><p>스택의 마지막 값을 추출해서 visiedQueu에 해당 값이 있는지 확인한다.
만약 이미 방문했던 노드라면 추출값을 버리고 Stack에서 다음값을 뽑는다.
위 과정을 visiedQueue에 없는 값이 나올때 까지 반복하는데
이러다 stack이 비게 된다면 탐색이 끝나게 된다.</p>
<p>이해를 위해 몇번의 과정을 보여주도록 하겠다</p>
<pre><code>이제 1부터 탐색을 시작한다고 하자
Q
S - 1
방문했던 노드가 아니기에 visitedQueue에 넣는다.
그리고 연결된 간선들을 stack에 넣는다.
Q - 1
S - 2 8
다시 위의 과정을 반복
8을 추출하던 2를 추출하던 사실 상관없다.
어떤 인접 노드부터 탐색할지는 순서가 없기 때문이다.
Q - 1 8
S - 2 9
8을 뽑고 방문한 사실이 없기에 8을 Q을 넣고 8의 자식노드를 stack에 넣는다.
Q - 1 8 9
S - 2 8
9를 뽑고 방문한 사실이 없기에 9를 Q에 넣고 9의 선택노드인 8을 넣는다.
Q - 1 8 9
S - 2 1</code></pre><p>위의 과정을 반복한 결과
최종 : Q - 1 8 9 2 7 6 3 5 4 의 결과가 나온다
사실 최단 거리 계산보다는 경로 탐색에 유용하다.
dfs와 bfs의 차이를 위해 작성한다.</p>
<h1 id="너비-우선-탐색bfs">너비 우선 탐색(BFS)</h1>
<p>말 그대로 너비 우선 탐색이다.</p>
<blockquote>
</blockquote>
<p>한줄 요약 : 모든 간선의 가중치가 동일(또는 1)일 때, 최단 경로를 구할 수 있는 탐색 알고리즘
핵심 : 가까운 정점부터 차례대로 방문하므로, 처음 도착한 경로가 곧 최단거리
시간복잡도 : O(V + E)</p>
<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/41ec3b05-8ee7-4726-8cad-f62bf6a3e73d/image.png" width=70%, height = 70%></p>

<p>방식은 DFS과 똑같다고 할 수 있다.</p>
<pre><code>Q - 1
S - 2 3
---
Q - 1 2
S - 3 4 5 6
---
Q - 1 2 3
S - 4 5 6 7

등등 이 반복되다
Q - 1 2 3 4 5 6 7 8 9가 되는 것이다.</code></pre><p>중요한 점은 큐를 사용하고, 가까운 곳부터 탐색한다는 것이다.</p>
<h1 id="벨만-포드-알고리즘bellman-ford">벨만-포드 알고리즘(Bellman-Ford)</h1>
<p>벨만포드 알고리즘은 다익스트라 알고리즘의 단점을 해결한 알고리즘이다.
다익스트라 알고리즘은 간선들의 거리가 음수라면 최소거리를 찾을 때도 있고 못찾을 때도 있다.
또한 벨만 포드 알고리즘은 Greedy 해결법이 아닌 dp를 사용한다.
물론 플로이드 워셜 알고리즘을 통해 음수의 거리들을 속에서 최단거리를 찾을 수도 있다.
하지만 플로이드 워셜 알고리즘은 O(n^3)이기에 노드의 크기가 500 이상이라면 시간초과가 나게된다.</p>
<blockquote>
</blockquote>
<p>한줄 요약 : 음수 가중치까지 포함된 최단거리를 구할 수 있는 알고리즘
핵심 : 모든 간선을 최대 V-1번 반복하며 Relaxation 수행, 음수 사이클까지 탐지할 수 있음
시간복잡도 : O(V × E)</p>
<h3 id="해결법">해결법</h3>
<p>최단거리를 dist[]배열에 저장할 때, 항상 dist[cur] + cost[next] &lt; dist[next]이라면
최단거리는 dist[cur]이다.
dist[v] &lt;= dist[cur] + w(current,v)이라는 것이다.
시작점 s-v까지 가는 최단거리는 s-next까지 가는 최단거리에 current-v까지의 가중치를 더한 값보다 클 수 없다.
n - 1 번 반복하여, 모든 간선에 대해 경로를 최소값으로 갱신이 가능한지 확인해준다면
음의 간선이 있어도 최단 경로를 구할 수 있다.</p>
<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/254c1818-bf9b-45e3-af7a-dd8eed92bca9/image.png" width=50%, height = 50%></p>


<p>그림을 예시로 더 쉽게 이해해보자</p>
<p>우리는 V-1번 반복하게 된다.
즉, 3번의 간선정보를 통해 최단거리를 찾을 수 있다.
1에서부터 시작한다고 가정하고 시작값에는 최단거리 0, 그외의 값들에는 INF를 넣는다.</p>
<pre><code>초기값
1 2 3 4
0 I I I

1번 실행
1 2 3 4
0 4 I 5

2번 실행
1 2 3 4
0 4 -6 5

3번 실행
1 2 3 4
0 4 -6 -3</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[회귀 vs 분류, 모델 성능평가 지표 한눈에 보기]]></title>
            <link>https://velog.io/@dev_init_ung/%EB%AA%A8%EB%8D%B8%EB%A7%81-%EC%84%B1%EB%8A%A5-%ED%8F%89%EA%B0%80</link>
            <guid>https://velog.io/@dev_init_ung/%EB%AA%A8%EB%8D%B8%EB%A7%81-%EC%84%B1%EB%8A%A5-%ED%8F%89%EA%B0%80</guid>
            <pubDate>Mon, 14 Apr 2025 08:47:11 GMT</pubDate>
            <description><![CDATA[<p>모델링 성능평가는 머신러닝 모델이 얼마나 잘 작동하는지를 수치적으로 평가하는 과정이다.
여러 알고리즘을 테스트할 때 어떤 모델이 더 나은지 비교가 가능하고, 모델이 과적합 또는 과소적합되었는지 판단할 수 있는 근거가 된다.</p>
<p>모델의 문제 유형은 보통 회귀, 분류로 구분된다.</p>
<h2 id="회귀-regression">회귀 (Regression)</h2>
<p>예측값이 연속적인 숫자일 때 (주가, 온도, 집값)
독립변수(X)를 통해 답(Y)과 가장 유사하도록 예측하는 것이다.</p>
<ol>
<li><p>MAE(Mean Ansolute Error)
예측값과 실제값의 차이의 절댓값의 평균
장점 : 직관적이고 이상치에 덜 민감하다
단점 : 오차의 방향(음수,양수)를 고려하지 않았다.</p>
</li>
<li><p>MSE(Mean Squared Error)
오차의 제곱값의 평균
장점 : 큰 오차를 강조한다(제곱을 진행). -&gt; 금융과 같이 작은 오차에도 민감한 분야일 때 사용
단점 : 단위가 실제값보다 제곱되어 해석이 어려울 수 있다.</p>
</li>
<li><p>RMSE(Root Mean Squared Error)
MSE의 제곱근
장점 : 제곱근을 취하여 해석이 용이하고 큰 오차에 민감하다
단점 : 이상치에 취약하다</p>
</li>
<li><p>MAPE(Mean Absolute Percentage Error)
실제값 대비 오차의 비율 평균
장점 : 직관적인 %의 표현으로 이해당사자들이 좋아한다
단점 : 실제값이 0에 가까우면 무한대로 발산
유의 : zero-division(분모에 0이 들어경우)</p>
</li>
</ol>
<ol start="5">
<li>R^2(R-Squared)
결정계수로 전체 변동성 중 모델이 설명하는 비율이다
장점 : 예측 성능 전체 평가 가능(1에 가까울수록 모델이 잘 설명함)
단점 : 과적합 모델도 성능이 좋게 평가될 수 있다.</li>
</ol>
<h2 id="분류-classification">분류 (Classification)</h2>
<p>예측값이 카테고리인 경우 (스팸 메일/비스팸 메일, 질병 있음/없음)
독립변수(X)로 답(Y)을 구분하기 위한 경계를 찾는 것이다.</p>
<ol>
<li>Confusion Matrix
예측 결과를 TP,FP,TN,FN 4가지로 나눈 행렬이다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/e34604a6-f998-4c64-b33d-aeca0dd3b6a6/image.png" alt=""></p>
<p>모델에서 뱉는 신호가 있다(Positive, Negative)
예측과 결과가 맞으면 T를 뱉고, 예측과 결과가 다르면 F를 뱉는다.
TP : 실제도 Positive - 예측도 Positive 
TN : 실제도 Negaitive - 예측도 Negative
이런 방식이다.
해당 행렬을 통해 아래의 정확도나 정밀도를 계산한다.</p>
<ol start="2">
<li>Accuracy(정확도)
TP + TN / (TP+TN+FP+FN)
전체 예측 중 맞춘 비율이다.
장점 : 이해하기 쉽다.
단점 : 클래스의 불균형에 취약하다 (99%가 Negative라면?)</li>
</ol>
<ol start="3">
<li><p>Precision (정밀도)
TP / TP+FP
모델이 출력한 양성(Positive) 신호의 정확도를 말한다.
장점 : False Positive를 줄이는데 집중한다(스팸을 필터)</p>
</li>
<li><p>Recall(재현율) = Sensitivtiy(민감도)
TP / TP+FN
실제 Positive중 예측에 잡힌 확률
ex) 질병예측 → 암을 에측해야 하는데 암 환자 중 진짜를 맞춘 확률</p>
</li>
<li><p>Specificity(특이도)
TN / TN+FP 
실제 정상인 사람들(암이 아닌) 중에 정상을 맞춘 배율
Actual Negative 중 Negative로 분류한 비율</p>
</li>
<li><p>Fallout(위양성률) = 1-Specificity = FPR
FP / TN+FP
실제 정상인 애들 중에서 암이라고 잘못 판정한 비율
Actual Negative 중 양성(Positive)로 잘못 분류한 비율</p>
</li>
<li><p>F1 Score
Precision과 Recall의 조화평균
불균형한 데이터에서 잘 동작한다는 장점이 있다.</p>
</li>
</ol>
<blockquote>
<p>조화평균(harmonic mean)
Precision, Recall 둘중 하나가 극도로 낮을 때에도 지표에 잘 반영되도록 할때
혹은 두 지표 모두를 균형 있게 반영하기 위해 사용한다.
Precision 1 , Recall 0.01 일때
산술평균은 0.505 → 그럼 반이나 맞추는 모형일까?
조화평균은 이를 0.019로 맞춰주는 과정</p>
</blockquote>
<ol start="8">
<li><p>ROC Curve (Receiver Operating Characteristic)
다양한 임계값 설정에서 분류 모델의 성능을 평가하기 위해 사용한다.
ROC커브가 좌 상단에 붙을 수록 더 좋은 분류기를 의미한다.
임계치(Threshold)를 높은 수준에서 낮은 수준으로 이동하면서 각 임계치마다 점(x:FPR,y:TPR)을 그려주고 이어준다.</p>
</li>
<li><p>AUROC (Area Under ROC Curve)
ROC 곡선 아래 면적이다.
해석: 1에 가까울수록 분류 성능이 좋음
샘플의 분포에 변화가 생기더라도 급격한 변화를 보이지 않는다.</p>
</li>
<li><p>Log Loss(로그 손실)
모델의 확률 예측값과 실제값 간의 차이를 로그로 측정한다.
ex) 55%의 확률로 맞춘 모델과 99%의 확률로 맞춘 모델이 같은 모델로 취급해야할까?
운이 좋아서 맞춘경우와 99%의 확률로 맞춘 경우는 다르게 취급해야한다.
얼마나 예측값에 확신하는지에 대한 평가가 필요했고, 예측의 확신을 반영하여 평가하자가 Log Loss이다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[FastAPI]Migrating from Async to Sync]]></title>
            <link>https://velog.io/@dev_init_ung/Migrating-from-Async-to-Sync</link>
            <guid>https://velog.io/@dev_init_ung/Migrating-from-Async-to-Sync</guid>
            <pubDate>Sat, 12 Apr 2025 08:17:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/b1cade52-25e5-4dd6-81a5-6c4c175a9fd7/image.png" alt=""></p>
<p>FastAPI는 async/await 기반의 비동기 처리 덕분에 고성능 API 서버 구축에 최적화되어 있다. 그렇다면 반대로, 동일한 기능을 동기 방식으로 구현한다면 어떤 차이가 생길지 알아보자.</p>
<p>이 글에서는 FastAPI 비동기 코드를 동기 방식으로 전환하여 직접 비교해보고, 그 과정을 통해 비동기 방식의 이점이 단순한 속도 이상의 의미임을 검증해보고자 한다.</p>
<h2 id="동기-방식으로-전환하며-바뀌는-핵심-요소">동기 방식으로 전환하며 바뀌는 핵심 요소</h2>
<p>FastAPI의 비동기 구조를 동기 방식으로 전환하기 위해서는 몇 가지 명확한 수정이 필요하다. 아래는 개발하며 주로 변경되는 핵심 요소들이다.</p>
<h3 id="db-드라이버의-변경">DB 드라이버의 변경</h3>
<pre><code># 동기
ASYNC_DB_URL = &quot;mysql+aiomysql://user:pass@localhost:3306/dbname&quot;
# 비동기
DB_URL = &quot;mysql+pymysql://user:pass@localhost:3306/dbname&quot;</code></pre><p>aiomysql은 비동기 전용 드라이버이고, pymysql은 동기 전용이다.</p>
<p>ORM(SessionLocal, SQLAlchemy 등)이 에러 없이 작동하려면 DB 드라이버와 ORM 방식이 일치해야 한다.</p>
<h3 id="라우터-함수-선언의-차이">라우터 함수 선언의 차이</h3>
<pre><code># 동기
@router.post(&quot;/tasks&quot;)
def create_task(...):
    ...

# 비동기
@router.post(&quot;/tasks&quot;)
async def create_task(...):
    ...</code></pre><p>async def 대신 일반적인 def로 선언</p>
<p>FastAPI는 자동으로 동기/비동기 방식을 구분하여 처리하므로, 내부 코드 실행 방식에 맞춰 명확하게 선언해야 한다.</p>
<h3 id="db-세션-주입-함수의-차이">DB 세션 주입 함수의 차이</h3>
<pre><code># 동기
def get_db() -&gt; Session:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
# 비동기
async def get_db() -&gt; AsyncSession:
    async with AsyncSessionLocal() as session:
        yield session</code></pre><p>비동기에서는 async with, 동기에서는 try-finally를 사용해 세션을 관리한다.</p>
<p>비동기 컨텍스트 매니저를 통해 처리한다.</p>
<h3 id="orm-dbms-함수-호출">ORM DBMS 함수 호출</h3>
<pre><code># 동기 방식
return task_crud.create_task(db, task_body)

# 비동기 방식
return await task_crud.create_task(db, task_body)</code></pre><p>await가 없으면 비동기 함수는 단순히 coroutine 객체만 반환하고, 실행되지 않는다.</p>
<p>동기 방식에서는 await 없이 바로 리턴한다.</p>
<h2 id="trouble-shooting">Trouble Shooting</h2>
<h3 id="⚠️-fastapi-다중-진입점-충돌-문제-해결">⚠️ FastAPI 다중 진입점 충돌 문제 해결</h3>
<pre><code>python3 -m uvicorn api.main2:app --host 0.0.0.0 --port 8004 --reload</code></pre><p>위 명령어로 <code>main2.py</code>를 실행하려 했지만 실제로는 <strong><code>main.py</code>의 app 객체가 실행</strong></p>
<p><code>--reload</code> 옵션은 <code>uvicorn</code>이 파일 변경을 감지해 재시작하는 데몬을 띄웁니다. 이 때 최초 실행 지점의 모듈 경로 또는 import 로직이 꼬일 수 있다.</p>
<p>✅ main.py 와 main2.py에 app=FastAPI()가 동일하게 있게되므로 이전 main 삭제 조치 후 해결</p>
<h3 id="⚠️-fastapi-오류-방지-get_task-사전조회의-필요성">⚠️ FastAPI 오류 방지 get_task() 사전조회의 필요성</h3>
<p>router의 API에서 update_task() 혹은 delete_task()함수만 호출했더니 
ORM(SQLAlchemy)오류 발생</p>
<p>original에서 실제 DB객체가 없으므로 NoneType 에러 유발</p>
<pre><code>500 Internal Server Error
AttributeError: &#39;NoneType&#39; object has no attribute &#39;title&#39;</code></pre><p>✅ 해결 </p>
<pre><code>@router.put(&quot;/tasks/{task_id}&quot;, response_model=TaskCreateResponse)
def update_task(task_id: int, task_body: TaskCreate, db: Session = Depends(get_db)):
    db_task = task_crud.get_task(db, task_id)
    if db_task is None:
        raise HTTPException(status_code=404, detail=&quot;Task not found&quot;)
    return task_crud.update_task(db=db, task_create=task_body, original=db_task)</code></pre><p>먼저 get_task()로 DB에서 해당 task가 존재하는지 확인 후 전달</p>
<h2 id="🤔-회고">🤔 회고</h2>
<p>동기 방식은 구현이 단순하고, 구조가 직관적이라는 장점이 있다. 특히 대부분의 CRUD 중심 API 서버에서는 동기 처리만으로도 충분히 빠른 응답 속도를 제공할 수 있음을 확인했다. 또한, 요청 흐름이 순차적으로 처리되기 때문에 예외 상황에 대한 디버깅이 비교적 용이하다는 점도 큰 장점으로 느껴졌다.</p>
<p>반면, 비동기 방식은 수천 개 이상의 요청을 병렬로 처리해야 하는 환경에서 매우 유리하다. 외부 API 호출이나 데이터베이스 접근처럼 I/O 작업이 많은 경우에도 응답이 블로킹되지 않고, 전체 시스템의 처리 효율을 향상시킬 수 있었다. 특히 실시간성과 응답 속도가 중요한 서비스에서는 비동기 구조가 필수적이라는 점을 체감할 수 있었다.</p>
<p>ORM은 단순히 SQL을 대체하는 수준을 넘어, Python 객체를 통해 테이블과 레코드를 표현할 수 있는 강력한 도구로 활용하였다. 이를 통해 쿼리 작성이 훨씬 안전하고 명확해졌으며, 복잡한 조인이나 조건문도 Python 문법으로 처리할 수 있어 생산성과 유지보수성이 크게 향상되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 기반 비동기 처리 전략: 고성능 API 설계의 출발점]]></title>
            <link>https://velog.io/@dev_init_ung/FastAPI</link>
            <guid>https://velog.io/@dev_init_ung/FastAPI</guid>
            <pubDate>Fri, 11 Apr 2025 00:02:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/90fdcca7-46b5-4214-a487-a8e5b3e1abcc/image.png" alt="">
FastAPI는 Python 기반의 고성능 웹 프레임워크로, 비동기 처리를 지원하며 RESTful API나 GraphQL API를 빠르게 개발할 수 있도록 도와준다. 
Python 3.6 이상에서 작동하며, Pydantic의 데이터 검증 기능과 타입 힌팅 기반의 자동 문서화 기능 덕분에 코드의 안정성과 생산성을 동시에 확보할 수 있다.</p>
<h2 id="왜-fastapi인가">왜 FastAPI인가?</h2>
<p>말 그대로 &#39;Fast&#39;API라서 빠르게 API서버 개발을 진행하며 매우 높은 성능을 자랑한다.</p>
<ol>
<li><p>성능 (Speed)
FastAPI는 Starlette과 Uvicorn 기반으로 동작하며, 비동기 I/O를 기본 지원하여 Node.js 수준의 속도를 자랑한다. 따라서 수천 개의 요청을 효율적으로 처리할 수 있다.</p>
</li>
<li><p>자동 문서화
코드만 작성하면 Swagger UI와 ReDoc으로 API 문서가 자동 생성되며, 별도 설정 없이 테스트까지 가능하다.</p>
</li>
<li><p>간결한 코드
타입 힌팅을 통해 코드 가독성이 뛰어나며, IDE의 자동완성 기능과 정적 분석 도구까지 활용 가능하다.</p>
</li>
<li><p>비동기 처리
비동기 처리(async/await)를 기본적으로 지원하여, 외부 API 호출, 파일 입출력, DB 연산 등에서 논블로킹 처리가 가능하다.</p>
</li>
</ol>
<h2 id="🔄-동기-vs-비동기-처리">🔄 동기 vs 비동기 처리</h2>
<p>우선 비동기(asynchronous processing)과 동기(synchronous processing)의 차이점에 대해 알아보자
결론부터 말하자면 동기처리는 요청이 끝날때까지 기다리고, 비동기처리는 기다리지 않고 다음 작업을 병렬로 처리한다.</p>
<h3 id="동기-처리">동기 처리</h3>
<p>요청 -&gt; 작업 완료될 때까지 기다림 -&gt; 다음 작업
순차적 실행방식이다.</p>
<pre><code>✅ 동기 (sync) 적합 상황
간단한 CRUD 작업
I/O가 적고, 순차 처리가 필요한 로직
테스트 코드 작성 등 빠른 실행이 필요한 경우</code></pre><h3 id="비동기-처리">비동기 처리</h3>
<p>요청 -&gt; 기다리지 않고 다음 작업 수행 -&gt; 결과가 준비되면 코랙
따라서 병렬적이고 논블로킹 방식으로 작업수행이 가능하다.</p>
<pre><code>✅ 비동기 (async) 적합 상황
간단한 CRUD 작업
I/O가 적고, 순차 처리가 필요한 로직
테스트 코드 작성 등 빠른 실행이 필요한 경우</code></pre><h2 id="📊-주요-프레임워크-비교">📊 주요 프레임워크 비교</h2>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/f603e3b1-12f9-41d7-a1a5-e5986710da9d/image.png" alt=""></p>
<h2 id="🌊-서비스-흐름">🌊 서비스 흐름</h2>
<pre><code>[클라이언트 요청]
     ↓
[routers/task.py] - FastAPI 라우터에서 요청 수신
     ↓
[schemas/task.py] - 응답 데이터 구조화 후 반환
         ↓
[cruds/task.py] - 실제 DB 연산 처리
     ↓
[models/task.py] - SQLAlchemy로 DB 조작
     ↓
[클라이언트 응답 전송]</code></pre><h3 id="1-routers---실제-api-라우팅-처리">1. routers - 실제 API 라우팅 처리</h3>
<p>클라이언트의 요청을 어떤 curd함수에 보낼지 지정
@router.get, @router.post 같은 데코레이터는 HTTP 요청에 대한 엔드포인트 정의 → FastAPI에서 실제로 외부 요청을 받는다.</p>
<h3 id="2-schemas---데이터-검증-및-직렬화">2. schemas - 데이터 검증 및 직렬화</h3>
<p>입력값 및 응답값 정의 (요청, 응답)
FastAPI의 입력과 출력을 책임지는 Data Schema들을 정의하는 공간</p>
<h3 id="3-cruds---비즈니스-로직-및-db-조작-함수">3. cruds - 비즈니스 로직 및 DB 조작 함수</h3>
<p>실제 DB와 상호작용하는 로직 정의
SQLAlchemy 세션을 받아 쿼리를 실행
라우터는 요청, 응답 처리만 cruds는 실제 동작만 맡게 되어 유지보수성과 테스트 효율성의 향상</p>
<h3 id="4-models---데이터베이스-모델-정의">4. models - 데이터베이스 모델 정의</h3>
<p>SQLAlchemy를 사용한 데이터베이스 모델 정의
Task와 Done이라는 두개의 테이블을 SQLAlchemy ORM 클래스로 정의</p>
<h3 id="5-tests---테스트-코드-작성">5. tests - 테스트 코드 작성</h3>
<p>각 API 동작이 의도한 대로 동작하는지 확인
단위 테스트 / 통합 테스트 수행</p>
<h2 id="🔥-trouble-shooting">🔥 Trouble Shooting</h2>
<h3 id="🔗-dbms-연동">🔗 DBMS 연동</h3>
<p>데모 앱을 동작시킬 때, 접근할 DBMS 연결을 설정
컨테이너 환경에서 기본적으로 필요한 설정이다
단순하게 <code>manager@&#39;%&#39;</code> 하나만 생성해 외부 접속만 처리</p>
<p>→ FastAPI나 DBeaver에서 DB 접속은 되었지만 테이블이 안보이는 문제 발생</p>
<p><a href="http://localhost">localhost</a> 접속전용 계정을 별도로 만들어 권한을 부여하고
MariaDB에서는 <code>manager@&#39;%&#39; &amp; manager@&#39;localhost&#39;</code>를 다르게 취급하므로
모두 만들어야 다양한 환경에서 문제 없이 접속 가능하게 변경</p>
<pre><code>-- demo 데이터베이스에 대한 권한 부여
GRANT ALL PRIVILEGES ON demo.* TO &#39;manager&#39;@&#39;%&#39;;
GRANT ALL PRIVILEGES ON demo.* TO &#39;manager&#39;@&#39;localhost&#39;;</code></pre><h3 id="🚨-asyncawait">🚨 async/await</h3>
<pre><code>❌ 오류 발생
done = done_crud.get_done(db, task_id=task_id)   
return done_crud.create_done(db, task_id)       </code></pre><p>이렇게 작성하면 다음과 같은 오류 발생</p>
<pre><code>RuntimeWarning: coroutine &#39;get_done&#39; was never awaited</code></pre><p>done_crud.get_done()과 create_done()는 비동기 함수이다.
비동기 함수는 호출만 하면 실행되지 않고, coroutine 객체만 반환한다.
따라서 <code>await</code>가 필요하다.</p>
<pre><code>done = await done_crud.get_done(db, task_id=task_id)
return await done_crud.create_done(db, task_id)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[DTO&DAO 스프링 계층 구조 속 역할 이해하기(feat. Lombok)]]></title>
            <link>https://velog.io/@dev_init_ung/DTODAO-%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0-%EC%86%8D-%EC%97%AD%ED%95%A0-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0feat.-Lombok</link>
            <guid>https://velog.io/@dev_init_ung/DTODAO-%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0-%EC%86%8D-%EC%97%AD%ED%95%A0-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0feat.-Lombok</guid>
            <pubDate>Sun, 06 Apr 2025 23:40:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/058d6d87-2fc5-4a95-bb7a-b9b128969d50/image.png" alt="">
스프링 기반 애플리케이션은 주로 3 계층 구조로 나뉜다.
Controller : 클라이언트 요청을 받고 응답을 처리
Service : 핵심 로직 처리, 트랜잭션 관리
Respository : DB와 직접 통식</p>
<p>앞서서 배운 MVC패턴을 확장한 구조라고 생각하면 편하다.
Model의 부분을 좀 더 세분화 해서 Service + Repository 계층으로 나뉘었다고 보면 된다.
Model의 비즈니스 로직을 Service가 데이터 처리를 Repository가 한다.</p>
<blockquote>
</blockquote>
<p>왜 나눌까?
역할을 분리하면 유지보수와 테스트가 쉬워지고, 협업에도 유리한게 당연하다
Service, Repository로 분리되면
각 구조에서 비즈니스로직, DB처리만 집중하게 된다</p>
<h1 id="dao-data-access-object">DAO (Data Access Object)</h1>
<p>DB와 통신하는 객체로 DB에 접근해서 데이터를 조회하거나 저장하는 로직을 담는 클래스이다.
Service나 Controller에 직접 DB를 접근하는것보다 안정적인 로직을 구현할 수 있다.</p>
<pre><code>@Repository
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
    Optional&lt;User&gt; findByUsername(String username);
}</code></pre><p>위의 코드 처럼 SQL 쿼리를 실행(JPA, MyBatis등)하거나
DB에서 데이터를 가져오거나 삽입, 수정, 삭제를 진행한다
보통 @Repository로 마킹되어 DB접근을 담당하게 된다.</p>
<h1 id="dto-data-transfer-object">DTO (Data Transfer Object)</h1>
<p>데이터를 전달하는데 사용되는 객체이다.
주로 Controller &lt;-&gt; Service // Service &lt;-&gt; Client 사이에서의 데이터 전달이 필요할 때 사용한다.
Entity에는 DB 관련 설정이 많다. 따라서 직접 노출되면 보안상 위험하므로 이 또한 안정적인 서비스를 구현하기 위한 단계이다.</p>
<pre><code>@Getter
@Setter
// 클라이언트 요청 시 받는 DTO
public class UserRequestDto {
    private String username;
    private String password;
}

// 클라이언트에게 응답 시 전달하는 DTO
public class UserResponseDto {
    private Long id;
    private String username;
}</code></pre><p>위처럼 데이터 전달 전용 객체로 사용하며, Entity와는 별도의 클래스이다.
Entity는 DB 테이블과 직접 매핑되는 클래스이고
DTO는 데이터를 전달하기 위한 객체이다.
클라이언트의 요청 -&gt; UserRequestDto -&gt; Service계층(Entity로 변환) -&gt; 처리결과를 DTO로 변환 -&gt; 클라이언트 응답
이러한 구조로 흘러간다고 생각하면 된다.</p>
<h2 id="lombok-라이브러리">Lombok 라이브러리</h2>
<p>스프링 자체 기능은 아니지만, 스프링 프로젝트에서 거의 필수처럼 쓰이는 Lombok 라이브러리에 대해서도
같이 설명해보도록 하겠다.
반복적인 작성해야 하는 메서드들을 자동으로 생성해주는 도구라고 생각하면 된다.</p>
<p>자바에서는 필드의 값을 직접 접근하지 않고, 보통 밑에와 같이 다룬다.</p>
<pre><code>public class User {
    private String username;
    private String password;

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    @Override
    public String toString() {
        return &quot;User{username=&#39;&quot; + username + &quot;&#39;, password=&#39;&quot; + password + &quot;&#39;}&quot;;
    }
}</code></pre><p>딱봐도 복잡한 위의 코드를 Lombok를 쓰면 몇줄로 대체가 가능하다.</p>
<pre><code>import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class User {
    private String username;
    private String password;
}</code></pre><h3 id="주요-어노테이션">주요 어노테이션</h3>
<p>@Getter</p>
<ul>
<li>모든 필드의 getter 생성    </li>
<li>DTO, Entity에서 사용</li>
</ul>
<p>@Setter    </p>
<ul>
<li>모든 필드의 setter 생성    </li>
<li>주로 DTO (Entity는 신중하게 사용)</li>
</ul>
<p>@ToString    </p>
<ul>
<li>toString() 메서드 생성    </li>
<li>디버깅용</li>
</ul>
<p>@NoArgsConstructor    </p>
<ul>
<li>기본 생성자 생성    </li>
<li>JPA Entity 필요 조건</li>
</ul>
<p>@AllArgsConstructor    </p>
<ul>
<li>모든 필드 포함 생성자 생성    </li>
<li>DTO, 테스트에서 사용</li>
</ul>
<p>@RequiredArgsConstructor    </p>
<ul>
<li>final 또는 @NonNull 필드만 포함한 생성자    </li>
<li>의존성 주입 시 사용</li>
</ul>
<p>@Builder    </p>
<ul>
<li>빌더 패턴 생성    </li>
<li>객체 생성 시 유연성 향상됨</li>
</ul>
<p>@Data    </p>
<ul>
<li>@Getter, @Setter, @ToString, 
@EqualsAndHashCode, @RequiredArgsConstructor 
  포함 통합 어노테이션    </li>
<li>DTO에 자주 사용</li>
</ul>
<hr>
<p><a href="https://projectlombok.org/">Project Lombok Site</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC 디자인패턴]]></title>
            <link>https://velog.io/@dev_init_ung/MVC-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@dev_init_ung/MVC-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Sat, 05 Apr 2025 12:01:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/016e49f9-7e48-49a7-b8ac-89444503ec66/image.png" alt=""></p>
<p>MVC패턴은 애플리케이션을 Model, View, Controller의 세가지 역할로 나누어 설계하는 구조 이다.</p>
<p>스프링에서 가장 기본이 되는 구조로 웹 애플리케이션을 개발할 때 유지보수성과 확장성을 높이기 위해 사용하는 중요한 아키텍처라고 할 수 있다.
즉, 사용자(Client)가 API를 기반으로 요청을 처리하고 반환하는 전체적인 동작 방식을 스프링 웹 MVC 또는 스프링 MVC라고 한다.</p>
<p>지금부터 MVC 디자인 패턴에 대해 알아보고자 한다.</p>
<h2 id="model모델">Model(모델)</h2>
<p>데이터와 비즈니스 로직을 담당하는 부분으로
DB와 연결되어 데이터를 저장하거나 불러오는 역할을 담당한다.</p>
<pre><code>@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String email;
}</code></pre><h2 id="view뷰">View(뷰)</h2>
<p>사용자에게 보여지는 화면(UI)를 담당한다
HTML,JSP,JSON등으로 표현하며, Model의 데이터를 기반으로 화면을 구성합니다.</p>
<pre><code>&lt;!-- Thymeleaf 예시 --&gt;
&lt;p&gt;회원 이름: &lt;span th:text=&quot;${member.name}&quot;&gt;&lt;/span&gt;&lt;/p&gt;
</code></pre><h2 id="controller컨트롤러">Controller(컨트롤러)</h2>
<p>사용자의 요청을 받아 처리하고, 적절한 응답을 반환한다.
Model과 View 사이를 연결하며
주로 사용자의 입력을 받고 비즈니스 로직 호출 후 결과를 전달한다(Mapping)</p>
<pre><code>@Controller
public class MemberController {

    @GetMapping(&quot;/members/new&quot;)
    public String createForm(Model model) {
        model.addAttribute(&quot;memberForm&quot;, new MemberForm());
        return &quot;members/createMemberForm&quot;;
    }
}</code></pre><h2 id="흐름-구조-요청---응답">흐름 구조 (요청 -&gt; 응답)</h2>
<pre><code>[사용자 브라우저]
     |
     | 요청 (예: /members/new)
     ↓
[Controller]
     |
     | 비즈니스 로직 호출
     ↓
  [Model]
     |
     | 처리된 데이터 전달
     ↓
  [View]
     |
     | HTML 생성
     ↓
[사용자에게 응답]
</code></pre><p>스프링으로 백엔드를 구현하다 보면 이보다 더 복잡한 흐름도 있지만, 
이 기본 구조는 대부분의 웹 애플리케이션에서 유지된다.</p>
<ol>
<li>사용자에게 회원가입 폼을 보여주는 화면 요청을 실시한다 (/members/new)</li>
</ol>
<ul>
<li>회원가입 폼 조회(GET)</li>
</ul>
<ol start="2">
<li>MemberController가 요청을 받아 처리한다</li>
</ol>
<ul>
<li>회원가입 폼을 화면에 출력한다(@GetMapping(&quot;/members/new&quot;)</li>
<li>이 단계에서는 아직 아무 데이터도 저장되지 않는다</li>
</ul>
<ol start="3">
<li>사용자가 폼을 작성하고 &quot;회원가입&quot; 버튼을 클릭한다</li>
</ol>
<ul>
<li>이름,이메일,비밀번호 등을 입력하고 제출한다(HTTP POST 요청 발생)</li>
</ul>
<ol start="4">
<li>MemberController가 POST 요청을 처리한다</li>
</ol>
<ul>
<li>@PostMapping(&quot;/members&quot;) 메서드가 호출된다.</li>
<li>폼 데이터가 @ModelAttribute를 통해 자동으로 MemberForm 객체에 바인딩된다.</li>
</ul>
<ol start="5">
<li>MemberService에서 저장 로직을 처리한다.</li>
</ol>
<ul>
<li>비즈니스 로직을 처리하는 서비스 계층이다.</li>
<li>DB 중복 회우너 처리 체크 등의 비즈니스 로직을 수행한다.</li>
</ul>
<ol start="6">
<li>MemberRepository가 DB에 저장</li>
</ol>
<ul>
<li>데이터 접근을 담당하는 계층이다(JPA, JDBC)</li>
<li>실제로 데이터를 DB 또는 메모리에 저장하는 역할이다.</li>
</ul>
<ol start="7">
<li>저장 완료 후 View로 응답</li>
</ol>
<ul>
<li>회원가입 성공 시, 홈 화면이나 회원목록페이지 등으로 연결해준다.</li>
</ul>
<h2 id="그럼-무슨-기능이-mvc">그럼 무슨 기능이 MVC?</h2>
<h3 id="model">Model</h3>
<p>핵심 데이터 및 비즈니스 로직을 담당
구성 요소:</p>
<ul>
<li>Member (도메인 객체)</li>
<li>MemberForm (DTO)</li>
<li>MemberService (중복 검사 및 저장 로직)</li>
<li>MemberRepository (DB 저장 및 조회)</li>
</ul>
<p>즉, 회원 데이터를 다루는 모든 코드, 저장, 검증 로직을 모델이라고 할 수 있다.</p>
<h3 id="view">View</h3>
<p>사용자에게 보여지는 화면(UI)을 담당
구성 요소:</p>
<ul>
<li>회원가입 폼 페이지 (createMemberForm.html)</li>
<li>회원가입 성공 후 이동할 HTML 페이지 등</li>
</ul>
<p>즉, 사용자가 실제로 보는 웹페이지를 뷰라고 할 수 있다.</p>
<h3 id="controller">Controller</h3>
<p>사용자 요청을 받고 처리 결과를 View에 전달
구성 요소:</p>
<ul>
<li>MemberController 클래스</li>
<li>@GetMapping, @PostMapping 등 요청 매핑 메서드</li>
</ul>
<p>즉, 요청을 받고 모델을 호출하고, 결과를 뷰로 넘겨주는 연결 역할을 담당한다고 할 수 있다.</p>
<h2 id="장점">장점</h2>
<p>MVC가 각각의 기능이 명확하게 나눠지므로 유지보수성과 가독성이 향상된다.
-&gt; 수정할 때 해당 역할만 수정하면 되므로!
개발 분업에서 유리하다.
-&gt; MVC로 나누어서 개발을 할 수 있기에 여러 개발자가 역할을 분담해서 진행할 수 있다.
코드의 재사용성 향상
-&gt; View만 바꿔서 같은 Model 데이터를 다양한 방식으로 출력이 가능하다
유연한 확장성
-&gt; 새로운 기능 추가 시 구조 변경 없이 Controller만 추가하면 된다.</p>
<h2 id="단점">단점</h2>
<p>복잡성
-&gt; 기능이 많아지고 많은 URL요청이 한 컨트롤러에 몰릴 경우, 코드가 복잡해진다.
흐름을 따라가기 어려움
-&gt; 작은 규모의 프로젝트에서도 많은 흐름이 존재하므로 이해하기 어려울 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체 지향 설계의 5가지 원칙 - S.O.L.I.D]]></title>
            <link>https://velog.io/@dev_init_ung/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-S.O.L.I.D</link>
            <guid>https://velog.io/@dev_init_ung/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84%EC%9D%98-5%EA%B0%80%EC%A7%80-%EC%9B%90%EC%B9%99-S.O.L.I.D</guid>
            <pubDate>Fri, 04 Apr 2025 02:33:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/20540ce2-2517-4080-a7eb-480d77a051a4/image.png" alt=""></p>
<h2 id="5가지-개발-원칙">5가지 개발 원칙</h2>
<ul>
<li>SRP(Single Responsibility Principle): 단일 책임 원칙</li>
<li>OCP(Open Closed Priciple): 개방 폐쇄 원칙</li>
<li>LSP(Listov Substitution Priciple): 리스코프 치환 원칙</li>
<li>ISP(Interface Segregation Principle): 인터페이스 분리 원칙</li>
<li>DIP(Dependency Inversion Principle): 의존 역전 원칙</li>
</ul>
<p>좋은 소프트웨어란 변화에 대응을 잘하는 것을 말한다. 
변화에 대응을 잘하려면 시스템에 새로운 변경 사항이 있거나, 고객으로부터 새로운 요구사항이 생겼을때 소프트웨어에 적용을 시켜야하고, 그 때 영향을 받는 범위가 적은 설계를 짜야 한다.
따라서 SOLID 5개의 원칙을 적용하면 코드를 확장하고, 유지 보수 관리가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발 생산성을 높일 수 있다.</p>
<h2 id="srp-single-responsibility-principle">SRP (Single Responsibility Principle)</h2>
<p>단일 책임 원칙
말 그대로 클래스(객체)는 단 하나의 책임만 가져야 한다는 원칙이다.
책임이란 쉽게 말해서 &#39;기능&#39;이라고 이해하면 된다.
기능이 많아질수록 유지보수가 어려워지고, 다른 책임이 영향을 줄 수 있어 결합도가 증가한다.</p>
<h3 id="🙅-나쁜-예시">🙅 나쁜 예시</h3>
<pre><code>class UserManager:
    def create_user(self, name, email):
        # 사용자 생성
        print(f&quot;{name} 생성 완료!&quot;)

    def send_welcome_email(self, email):
        # 환영 이메일 전송
        print(f&quot;{email} 로 환영 메일 전송!&quot;)

    def save_to_database(self, user):
        # DB 저장
        print(&quot;DB에 사용자 저장 완료!&quot;)</code></pre><p>UserManager가 사용자 생성, 환영 이메일 전송, DB 저장 기능까지 담당하고 있다고 가정하자.
만약 이메일 시스템이나 DB방식이 바뀌면 해당 클래스도 수정해야 하므로 유지보수가 어렵다.
<strong>한 기능의 변경으로 부터 다른 기능의 변경으로의 연쇄작용</strong>을 막을 수 있다는 뜻이다.</p>
<h3 id="🙆-좋은-예시">🙆 좋은 예시</h3>
<pre><code>class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save(self, user):
        print(&quot;DB에 사용자 저장 완료!&quot;)

class EmailService:
    def send_welcome_email(self, email):
        print(f&quot;{email} 로 환영 메일 전송!&quot;)

class UserService:
    def __init__(self, repository, email_service):
        self.repository = repository
        self.email_service = email_service

    def register_user(self, name, email):
        user = User(name, email)
        self.repository.save(user)
        self.email_service.send_welcome_email(email)</code></pre><p>각각의 클래스가 딱 하나의 기능만을 가진다.
각각의 변경은 자기 class에서만 일어나므로 유지보수가 쉽다.</p>
<h2 id="ocp-open-closed-priciple">OCP (Open Closed Priciple)</h2>
<p>개방-폐쇄 원칙
소프트웨어 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
기존 코드를 수정하지 않고도 기능을 확장할 수 있어야 한다는 의미이다. 
기능 추가 요청이 오면 클래스를 확장을 통해 쉽게 구현하면서, 확장에 따른 클래스의 수정은 최소화해야 한다고 이해하면 된다.</p>
<h3 id="🙅-나쁜-예시-1">🙅 나쁜 예시</h3>
<pre><code>class HelloAnimal:
    void hello(Animal animal) {
        if (animal.type.equals(&quot;Cat&quot;)) {
            System.out.println(&quot;냐옹&quot;);
        } else if (animal.type.equals(&quot;Dog&quot;)) {
            System.out.println(&quot;멍멍&quot;);
        }</code></pre><p>동물 울음소리가 추가될때마다 elif를 직접 수정해야한다.</p>
<h3 id="🙆-좋은-예시-1">🙆 좋은 예시</h3>
<pre><code>// 추상화
abstract class Animal {
    abstract void speak();
}

class Cat extends Animal { // 상속
    void speak() {
        System.out.println(&quot;냐옹&quot;);
    }
}

class Dog extends Animal { // 상속
    void speak() {
        System.out.println(&quot;멍멍&quot;);
    }
}

class HelloAnimal {
    void hello(Animal animal) {
        animal.speak();
    }
}</code></pre><p>그리고 만약 사자와 같이 새로운 동물이 추가된다면
새로운 클래스를 선언만 해주면 된다.</p>
<h2 id="lsp-liskov-substitution-principle">LSP (Liskov Substitution Principle)</h2>
<p>영어 그대로 리스코프 치환 원칙이다.
LSP 원칙은 서브타입은 언제나 부모 타입으로 교체할 수 있어야 한다는 원칙이다.
상위 클래스 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면,
업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 &#39;의도&#39;대로 흘러가야 한다는 것을 의미한다.</p>
<h3 id="🙅-나쁜-예시-2">🙅 나쁜 예시</h3>
<p>Ostrich는 Bird를 상속받았지만, 타조는 fly()를 하지 못한다.
따라서 자기 멋대로 날지 못한다고 정의내렸지만,
Bird를 기대하고 Ostrich를 넣었던 main에서 기능이 깨진다.
이는 행동규약을 어겼고, 애초에 다형성 코드가 동작 자체가 되지 않는다</p>
<pre><code>class Bird {
    public void fly() {
        System.out.println(&quot;하늘을 납니다!&quot;);
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException(&quot;타조는 날 수 없습니다!&quot;);
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Ostrich();  // 부모 타입으로 자식 객체 사용
        bird.fly();  // 런타임 에러 발생 LSP 위반
    }
}
</code></pre><h3 id="🙆-좋은-예시-2">🙆 좋은 예시</h3>
<p>이렇게 조류는 알을 모두 낳는다 따라서 Bird에는 날을 낳는 기능만 정의하고
나는 새들만 따로 분류하여 FlyingBird로 정의한다.</p>
<pre><code>// 새는 알을 낳는 기능만 정의
class Bird {
    public void layEggs() {
        System.out.println(&quot;알을 낳습니다.&quot;);
    }
}

// 날 수 있는 새를 별도 클래스로 분리
class FlyingBird extends Bird {
    public void fly() {
        System.out.println(&quot;하늘을 납니다!&quot;);
    }
}

class Sparrow extends FlyingBird {
    // 모든 기능 문제없이 수행
}

class Ostrich extends Bird {
    // fly() 없음 → 구조적으로 날지 못함
}</code></pre><h2 id="isp-interface-segregation-principle">ISP (Interface Segregation Principle)</h2>
<p>인터페이스 분리 원칙으로 인터페이스를 각각 사용에 맞게 끔 잘게 분리해야한다는 설계 원칙이다.
SRP가 클래스의 단일 책임을 강조한다면 ISP는 인터페이스의 단일 책임을 강조한다고 이해하면 편하다
목적과 용도에 적합한 인터페이스를 제공해야하는것이 목표이다
(주의 : 인터페이스를 분리하여 구성 후 나중에 수정사항이 생겨서 또 분리하는 행위 ❌)</p>
<h3 id="🙅-나쁜-예시-3">🙅 나쁜 예시</h3>
<p>어떤 기계는 프린트만되고, 어떤기계에는 스캔 기능이 빠져있을수도 있다
하지만 machine 인터페이스에 모든 기능을 넣으면 없는 기능도 구현해야한다
-&gt; ISP 위반</p>
<pre><code>interface Machine {
    void print();
    void scan();
    void fax();
}

class OldPrinter implements Machine {
    @Override
    public void print() {
        System.out.println(&quot;문서 출력&quot;);
    }

    @Override
    public void scan() {
        throw new UnsupportedOperationException(&quot;스캔 기능 없음&quot;);
    }

    @Override
    public void fax() {
        throw new UnsupportedOperationException(&quot;팩스 기능 없음&quot;);
    }
}</code></pre><h3 id="🙆-좋은-예시-3">🙆 좋은 예시</h3>
<p>각각의 기계가 필요한 기능들만 상속받아서 사용하는 모습을 볼 수 있다.</p>
<pre><code>// 작은 인터페이스로 분리
interface Printer {
    void print();
}

interface Scanner {
    void scan();
}

interface Fax {
    void fax();
}

// 필요한 인터페이스만 구현
class SimplePrinter implements Printer {
    @Override
    public void print() {
        System.out.println(&quot;문서 출력&quot;);
    }
}

class OfficeMultiMachine implements Printer, Scanner, Fax {
    @Override
    public void print() {
        System.out.println(&quot;문서 출력&quot;);
    }

    @Override
    public void scan() {
        System.out.println(&quot;문서 스캔&quot;);
    }

    @Override
    public void fax() {
        System.out.println(&quot;팩스 전송&quot;);
    }
}
</code></pre><h2 id="dip-dependency-inversion-principle">DIP (Dependency Inversion Principle)</h2>
<p>의존 역전 원칙으로 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 
그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙이다.
의존 관계를 맺을 때 변화가 자주 일어나는 것보다는 변화하기 어려운것, 거의 변하지 않는 것에 의존하라는 원칙이다.</p>
<h3 id="🙅-나쁜-예시-4">🙅 나쁜 예시</h3>
<p>OrderService는 결제를 처리해야 한다
KaKaoPay를 new로 생성해서 쓰다가 결제수단을 바꾸게 된다면 아예 클래스 필드의
변수 타입을 교체해주거나 해야한다
즉 이미 완전하게 구현된 하위 모듈을 의존하고 있다.</p>
<pre><code>class KakaoPay {
    public void pay(int amount) {
        System.out.println(&quot;KakaoPay로 &quot; + amount + &quot;원 결제&quot;);
    }
}

class OrderService {
    private KakaoPay kakaoPay = new KakaoPay(); // 구체 클래스에 직접 의존

    public void order(int amount) {
        kakaoPay.pay(amount);
    }
}
</code></pre><h3 id="🙆-좋은-예시-4">🙆 좋은 예시</h3>
<p>따라서 더 상위 모듈인 PaymentService 인터페이스를 생성해서
모든 결제시스템들이 PaymentService 인터페이스를 implementgkdu 
결제 변경에 따라 코드를 변경할 필요가 없게 구현한다.</p>
<pre><code>// 추상화: 결제 인터페이스 정의
interface PaymentService {
    void pay(int amount);
}

// 하위 모듈: 구현체
class KakaoPay implements PaymentService {
    public void pay(int amount) {
        System.out.println(&quot;KakaoPay로 &quot; + amount + &quot;원 결제&quot;);
    }
}

class TossPay implements PaymentService {
    public void pay(int amount) {
        System.out.println(&quot;TossPay로 &quot; + amount + &quot;원 결제&quot;);
    }
}

// 상위 모듈: 인터페이스에만 의존
class OrderService {
    private final PaymentService paymentService;

    // 생성자 주입 (Constructor Injection)
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void order(int amount) {
        paymentService.pay(amount);
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring+Vue] Stock-Application (4)]]></title>
            <link>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-4</link>
            <guid>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-4</guid>
            <pubDate>Wed, 02 Apr 2025 23:12:21 GMT</pubDate>
            <description><![CDATA[<p>이번에 배우게 된 Spring + Vue를 활용해 간단한 주식 시장 웹서비스를 구현하고자 한다.
이 글은 구현한 애플리케이션의 구조 설명 및 시연 화면등으로 구성될 예정이다.</p>
<p>그 중 BackEnd의 Spring과 FrontEnd Vue의 도커파일 생성에 대한 내용이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/b5d75a64-c921-40da-b3d6-21a7092a5a82/image.png" alt=""></p>
<h2 id="backend">Backend</h2>
<pre><code>cd stock

./gradlew bootJar</code></pre><p>stock-0.0.1-SNAPSHOT.jar 파일 생성
파일이 2개가 있다.
<img src="https://velog.velcdn.com/images/dev_init_ung/post/0dc002ea-f963-443c-bc6a-33f2bddbe2a1/image.png" alt=""></p>
<p>Dockerfile 이라는 파일 만들기(확장자 ❌)
파일 안에 아래 내용 작성</p>
<pre><code># Java 21 기반의 Spring Boot 애플리케이션용 Dockerfile
FROM openjdk:21

# 작업 디렉토리 설정
WORKDIR /app

# 빌드된 jar 파일을 컨테이너로 복사
# COPY &lt;복사할_파일_이름&gt; &lt;컨테이너_내_위치와_이름&gt;
COPY build/libs/stock-0.0.1-SNAPSHOT.jar app.jar

# 외부에서 접근할 포트 지정
EXPOSE 8080

# 환경 변수 설정 (선택 사항)
# ENV SPRING_PROFILES_ACTIVE=prod

# jar 파일 실행
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre><p>stock 폴더 터미널에서 명령어 실행</p>
<pre><code># Docker 이미지 빌드
docker build -t stock-backend .

# 컨테이너 실행
docker run -d -p 8080:8080 --name backend stock-backend</code></pre><p>확인용 터미널</p>
<pre><code>docker image ls

REPOSITORY      TAG       IMAGE ID       CREATED          SIZE
stock-backend   latest    2d3f782c6dc6   15 minutes ago   905MB</code></pre><pre><code>docker ps

CONTAINER ID   IMAGE           COMMAND               CREATED          STATUS          PORTS                    NAMES
59905eb558c2   stock-backend   &quot;java -jar app.jar&quot;   25 seconds ago   Up 25 seconds   0.0.0.0:8080-&gt;8080/tcp   backend</code></pre><h2 id="frontend">Frontend</h2>
<pre><code>cd stock-front

npm install
npm run build</code></pre><p>똑같이 Dockerfile 생성자 없이 생성</p>
<pre><code># 1단계: 빌드용
FROM node:20-alpine AS builder

WORKDIR /app

# package.json과 lock 파일 복사
COPY package*.json ./

# 의존성 설치
RUN npm install

# 전체 프로젝트 복사
COPY . .

# Vite 빌드
RUN npm run build

# 2단계: Nginx 사용해 정적 파일 서빙
FROM nginx:stable-alpine

# 빌드된 파일을 Nginx HTML 경로로 복사
COPY --from=builder /app/dist /usr/share/nginx/html

# 포트 오픈 (기본: 80)
EXPOSE 80

# 컨테이너 시작 시 Nginx 실행
CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</code></pre><p>빌드 &amp; 실행 명령어</p>
<pre><code># Docker 이미지 빌드
docker build -t stock-frontend .

# 컨테이너 실행 (포트는 5173 → 80으로 매핑)
docker run -d -p 5173:80 --name frontend stock-frontend</code></pre><blockquote>
</blockquote>
<h3 id="trouble-shooting">Trouble Shooting</h3>
<p>로컬 개발환경에서는</p>
<pre><code>const res = await.get(&#39;http://localhost:8080/api/stocks)</code></pre><p>이런식으로 되어있지만
도커로 프론트를 띄웠을 때는 Vue 앱이 localhost:8080이 아닌 
Nginx 컨테이너에서 실행중이라 실패
따라서 코드를 변경해줘야 한다</p>
<pre><code>axios.post(&#39;/api/players/login&#39;, { ... })
axios.get(&#39;/api/players&#39;)
-&gt;
axios.post(`${import.meta.env.VITE_API_URL}/api/players/login`, { ... })
axios.get(`${import.meta.env.VITE_API_URL}/api/players`)</code></pre><p>위와 같은 형식으로 전부 수정해주고
.env.production을 만들어
Vite가 도커 빌드시 자동으로 인식해서 <code>import.meta.env.VITE_API_URL</code>에 주입해주게 한다.</p>
<pre><code>VITE_API_URL=http://localhost:8080</code></pre><p>앱에서도 확인이 가능하다
<img src="https://velog.velcdn.com/images/dev_init_ung/post/09c2512d-7e1b-4f0a-b29e-5b670f95692e/image.png" alt=""></p>
<h2 id="front--back">Front + Back</h2>
<p>Vue(front)와 Spring Boot(back)의 Dockerfile은 분리되어 있고
그 둘을 함께 실행하고 관리하려면 docker-compose.yml 을 사용하는 것이 정석</p>
<h3 id="구조">구조</h3>
<pre><code>stock-project/
├── stock-front/       # Vue 프론트 (Dockerfile 존재)
├── stock/             # Spring Boot 백엔드 (Dockerfile 존재)
└── docker-compose.yml </code></pre><p>docker-compose.yml 작성</p>
<pre><code>version: &#39;3.8&#39;

services:
  backend:
    build: ./stock
    container_name: stock-backend
    ports:
      - &quot;8080:8080&quot;
    networks:
      - stock-net

  frontend:
    build: ./stock-front
    container_name: stock-frontend
    ports:
      - &quot;5173:80&quot;
    depends_on:
      - backend
    networks:
      - stock-net

networks:
  stock-net:
    driver: bridge</code></pre><p>build : 각각의 Dockerfile이 있는 디렉토리 경로
ports : 외부 → 내부 포트 매핑 (브라우저 접속용)
depends_on : 프론트가 백엔드보다 나중에 실행되도록 보장
networks : 두 컨테이너가 서로 통신할 수 있도록 구성</p>
<blockquote>
</blockquote>
<h3 id="trouble-shooting-1">Trouble Shooting</h3>
<p>front의 .env.production의 API 주소 수정</p>
<pre><code>VITE_API_URL=http://backend:8080</code></pre><p>backend는 docker-compose 내부에서 자동으로 DNS로 연결한다.
외부에서 보면 8080이지만, 컨테이너 안에서는 <a href="http://backend:8080%EC%9C%BC%EB%A1%9C">http://backend:8080으로</a> 통신한다.</p>
<p>포트 충돌 가능성과 네트워크 혼란 관리의 편의성등을 위해
기존 컨테이너 정지</p>
<pre><code>docker stop frontend
docker rm frontend

docker stop backend
docker rm backend</code></pre><p>빌드 및 실행</p>
<pre><code>docker-compose build
docker-compose up -d</code></pre><p>docker ps 할 경우</p>
<pre><code>CONTAINER ID   IMAGE           COMMAND                   CREATED          STATUS          PORTS                    NAMES
4798fbc4f487   java-frontend   &quot;/docker-entrypoint.…&quot;   27 seconds ago   Up 26 seconds   0.0.0.0:5173-&gt;80/tcp     stock-frontend
ac76e9fd4e78   java-backend    &quot;java -jar app.jar&quot;       27 seconds ago   Up 26 seconds   0.0.0.0:8080-&gt;8080/tcp   stock-backend</code></pre><h1 id="시연-사진">시연 사진</h1>
<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/a8bae497-e7c7-44dc-9dd8-8c1148812404/image.png"></p>

<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/e9ff8ccc-d2d6-4ee7-9d4c-ccc6a9390356/image.png"></p>

<p><img src ="https://velog.velcdn.com/images/dev_init_ung/post/f1259bcb-d993-4603-89d0-b1c1065b61d0/image.png"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring+Vue] Stock-Application (3)]]></title>
            <link>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-3</link>
            <guid>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-3</guid>
            <pubDate>Tue, 01 Apr 2025 23:10:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/20e2c7ef-db2c-4f19-acc6-d1c0ddb3fa91/image.png" alt="">
이번에 배우게 된 Spring + Vue를 활용해 간단한 주식 시장 웹서비스를 구현하고자 한다.
이 글은 구현한 애플리케이션의 구조 설명 및 시연 화면등으로 구성될 예정이다.</p>
<p>그 중 BackEnd의 Spring과 FrontEnd Vue의 연동에 대한 설명이다.</p>
<h1 id="spring--vue-연동">Spring + Vue 연동</h1>
<pre><code>[ Vue ] &lt;---- axios ----&gt; [ Spring Boot REST API ] &lt;----&gt; [ DB ]</code></pre><ul>
<li>Vue에서 로그인, 주식 매수 등 요청 발생</li>
<li>axios를 통해 Spring API 호출 (<code>/api/player</code>, <code>/api/trade</code>)</li>
<li>Spring에서는 Controller → Service → Repository 순으로 처리 후 JSON 응답</li>
<li>응답 받은 데이터를 Vue가 렌더링<pre><code>Controller → Service → Repository → DB
              ↓
응답(JSON) ← DTO 변환 ←</code></pre><h2 id="1-vueclient-사용자가-버튼-클릭으로-이벤트발생주식추가">1. vue(client) 사용자가 버튼 클릭으로 이벤트발생(주식추가)</h2>
<pre><code>POST /api/stock
{
&quot;name&quot;: &quot;SK하이닉스&quot;,
&quot;price&quot;: 80000
}</code></pre><h2 id="2-controller-http-요청을-받아-postmapping-getmapping-으로-처리">2. Controller: HTTP 요청을 받아 @PostMapping, @GetMapping 으로 처리</h2>
<pre><code>@PostMapping(&quot;/api/stock&quot;)
public ResponseEntity&lt;StockResponse&gt; createStock(@RequestBody CreateStockRequest request) {
  return ResponseEntity.ok(stockService.createStock(request));
}
</code></pre></li>
</ul>
<pre><code>## 3. Service : 비즈니스 로직 수행(잔액확인, 거래처리 등)</code></pre><p>public StockResponse createStock(CreateStockRequest request) {
    Stock stock = new Stock(request.getName(), request.getPrice());
    stockRepository.save(stock);
    return new StockResponse(stock);  // Entity → DTO
}</p>
<pre><code>## 4. Entity(JPA) : DB 저장/조회 도메인객체</code></pre><p>@Entity
public class Stock {
    @Id @GeneratedValue
    private Long id;</p>
<pre><code>private String name;
private int price;</code></pre><p>}</p>
<pre><code>## 5. DTO : Entity를 클라이언트 응답용으로 변환</code></pre><p>public class StockResponse {
    private Long id;
    private String name;
    private int price;</p>
<pre><code>public StockResponse(Stock stock) {
    this.id = stock.getId();
    this.name = stock.getName();
    this.price = stock.getPrice();
}</code></pre><p>}</p>
<pre><code>## 6. 응답 : JSON응답 (화면 반영)</code></pre><p>{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;SK하이닉스&quot;,
  &quot;price&quot;: 80000
}</p>
<p>```</p>
<h2 id="vue-의-장점">Vue 의 장점?</h2>
<p>상대적으로 쉽습니다. (HTML,JS,CSS의 조합) → SFC
구조적으로 깔끔하고 직관적이며, 빠른 개발에 적합합니다
(특정 기능을 수행하는 컴포넌트를 찾고 수정할때 편리하여 유지보수의 원활)
<strong>axios + router 통합 구조</strong>: 스파(SPA) 환경에 최적화</p>
<p>따라서 규모가 작고 가벼운 프로젝트에는 매우 유용하다고 생각합니다.
너무 유연해서 유지보수시 통일된 컨벤션이 필요 → 팀마다 코드 스타일이 다름</p>
<h2 id="프론트엔드의-공부-및-접근">프론트엔드의 공부 및 접근</h2>
<ol>
<li>전체적인 구조와 흐름 → 필요한 기능들을 단계적으로 구현</li>
</ol>
<ul>
<li>각 기능들이 어떻게 연결되고, 어떤 역할을 해야 하는지가 명확해지고 나중에 코드를 정리하거나 확장할 때도 훨씬 수월</li>
</ul>
<ol start="2">
<li>공통 요소의 재사용성</li>
</ol>
<ul>
<li>많은 UI 요소나 로직들이 반복 → 공통화하면 유지보수도 편하고, 팀 프로젝트에서도 협업 효율 상승</li>
</ul>
<ol start="3">
<li><code>예외 상황에 대한 고민</code> </li>
</ol>
<ul>
<li>정상 흐름이 아닐 때 어떻게 처리할 것인가 → 이런 상황들을 어떻게 막고, 사용자에게 어떻게 알려줄지를 고민(디테일)</li>
</ul>
<ol start="4">
<li>조금씩 만들고 자주 테스트</li>
</ol>
<ul>
<li>뼈대 → 기능  → 테스트 → 수정 → 반복</li>
</ul>
<hr>
<p>[깃허브 소스코드]</p>
<p><a href="https://github.com/ww5702/Skala-Stock-Server">Skala-Stock-Server</a>
<a href="https://github.com/ww5702/Skala-Stock-Front">Skala-Stock-Front</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring+Vue] Stock-Application (2)]]></title>
            <link>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-2</link>
            <guid>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-2</guid>
            <pubDate>Mon, 31 Mar 2025 23:12:40 GMT</pubDate>
            <description><![CDATA[<p>이번에 배우게 된 Spring + Vue를 활용해 간단한 주식 시장 웹서비스를 구현하고자 한다.
이 글은 구현한 애플리케이션의 구조 설명 및 시연 화면등으로 구성될 예정이다.</p>
<p>그 중 FrontEnd의 Vue에 대한 설명이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/47aa64f1-8e3d-487b-8c0a-ea97168e4152/image.png" alt=""></p>
<h1 id="기본-프로젝트-정보">기본 프로젝트 정보</h1>
<pre><code>npm create vue@latest frontend</code></pre><p>프롬프트에서 설정:
✔ Project name: … frontend             ← 그대로 Enter
✔ Add TypeScript? › No                ← ❌ (기본 JavaScript로)
✔ Add JSX Support? › No               ← ❌
✔ Add Vue Router for Single Page Application development? › Yes ← ✅
✔ Add Pinia for state management? › No   ← ❌ (필요하면 나중에 추가 가능)
✔ Add Vitest for Unit Testing? › No      ← ❌
✔ Add Cypress for both Unit and End-to-End testing? › No ← ❌
✔ Add ESLint for code quality? › No      ← ❌
✔ Add Prettier for code formatting? › No ← ❌</p>
<h1 id="vuefrontend">Vue(FrontEnd)</h1>
<p>파일 구조</p>
<pre><code>src/
  main.js                    ← 진입점: 앱 초기화 및 마운트
  App.vue                    ← 루트 컴포넌트
  components/                ← 재사용 가능한 UI 컴포넌트
    StockForm.vue           ← 주식 등록용 폼
    PlayerForm.vue          ← 사용자 등록용 폼
  views/                     ← 페이지 단위 컴포넌트 (라우터에 연결됨)
    HomeView.vue            ← 메인 홈 페이지
    LoginView.vue           ← 로그인 화면
    PlayerView.vue          ← 사용자 정보 및 주식 보유 현황
    StockView.vue           ← 주식 목록/등록 페이지
    AboutView.vue           ← 예시용 뷰 (삭제 가능)
  api/                       ← 백엔드와 통신하는 axios API 모듈
    axios.js                ← 공통 axios 인스턴스 설정
    stock.js                ← 주식 관련 API 모듈
  router/                    ← Vue Router 설정
    index.js                ← URL 경로별 페이지 연결 정의
  assets/
    main.css                ← 전역 스타일 정의</code></pre><h3 id="mainjs">main.js</h3>
<ul>
<li>Vue 앱 생성 및 <code>App.vue</code>를 기반으로 렌더링</li>
<li>Vue Router를 전역으로 사용하도록 등록</li>
</ul>
<h3 id="routerindexjs">router/index.js</h3>
<ul>
<li>URL 경로 ↔ 화면 컴포넌트 매핑</li>
<li>SPA 방식의 페이지 전환 지원</li>
</ul>
<h3 id="components">components/</h3>
<ul>
<li><code>PlayerForm.vue</code>: 사용자 등록용 입력 폼 (아이디, 비밀번호, 초기 자산)</li>
<li><code>StockForm.vue</code>: 주식 등록용 폼 (이름, 가격 등)</li>
</ul>
<h3 id="api">api</h3>
<ul>
<li><code>axios.js</code>: axios 인스턴스를 생성해 기본 URL 및 공통 설정 정의</li>
<li><code>stock.js</code>:  <code>registerStock</code>, <code>getStocks</code> 등 주식 관련 API 호출 함수 정의</li>
</ul>
<h3 id="views">views/</h3>
<ul>
<li><code>HomeView.vue</code>: 로그인 창, 주식 정보 요약 등 메인 페이지 역할</li>
<li><code>LoginView.vue</code>: 로그인 기능 UI</li>
<li><code>PlayerView.vue</code>: 사용자 정보, 보유 주식, 자산 상태 표시</li>
<li><code>StockView.vue</code>: 전체 주식 목록 및 주식 추가 폼</li>
</ul>
<h3 id="appvue">App.vue</h3>
<ul>
<li>실제 모든 페이지가 렌더링되는 <strong>루트 컴포넌트</strong></li>
<li><code>&lt;router-view /&gt;</code>로 현재 URL에 따른 페이지를 자동 출력</li>
</ul>
<hr>
<p>[깃허브 소스코드]</p>
<p><a href="https://github.com/ww5702/Skala-Stock-Server">Skala-Stock-Server</a>
<a href="https://github.com/ww5702/Skala-Stock-Front">Skala-Stock-Front</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring+Vue] Stock-Application (1)]]></title>
            <link>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-1</link>
            <guid>https://velog.io/@dev_init_ung/SpringVue-Stock-Application-1</guid>
            <pubDate>Mon, 31 Mar 2025 09:47:09 GMT</pubDate>
            <description><![CDATA[<p>이번에 배우게 된 Spring + Vue를 활용해 간단한 주식 시장 웹서비스를 구현하고자 한다.
이 글은 구현한 애플리케이션의 구조 설명 및 시연 화면등으로 구성될 예정이다.</p>
<p>그 중 BackEnd의 Spring에 대한 설명이다.<img src="https://velog.velcdn.com/images/dev_init_ung/post/775a3ddb-b85b-4523-8a97-ce2af9c65506/image.png" alt=""></p>
<h1 id="spring-boot-프로젝트-생성">Spring Boot 프로젝트 생성</h1>
<h2 id="기본-프로젝트-정보">기본 프로젝트 정보</h2>
<p>Group - com. skala
Artifact - stock
Name - skala-stock
Type - Gradle (혹은 Maven 가능)
Java Version - 21
Packaging - Jar
Spring Boot - 3.x 버전</p>
<h2 id="필수-dependencies">필수 Dependencies</h2>
<p>• Spring Web (REST API)
• Spring Data JPA
• H2 Database -&gt; 파일저장 (또는 MySQL로 나중에 변경 가능)
• Lombok
• Validation</p>
<h1 id="springbackend">Spring(BackEnd)</h1>
<p>파일 구조</p>
<pre><code>project_extracted/
  stock/                  ← Spring Boot 백엔드
    build.gradle
    settings.gradle
    README.md
    application.properties
    controller/
      PlayerController.class
      StockController.class
      TradeController.class
    service/
      PlayerService.class
      StockService.class
      TradeService.class
    repository/
      PlayerRepository.class
      StockRepository.class
      PlayerStockRepository.class
    dto/
      CreatePlayerRequest.class
      CreateStockRequest.class
      TradeRequest.class
      PlayerResponse.class
      StockResponse.class
    domain/
      Player.class
      Stock.class
      PlayerStock.class
    config/
      WebConfig.class
      DataInitializer.class
      GlobalExceptionHandler.class
    templates/
    ...</code></pre><h3 id="controller">controller</h3>
<ul>
<li>클라이언트(Vue)에서 요청을 받는 <strong>엔드포인트(API)</strong> 정의</li>
<li><code>PlayerController</code>: 사용자 관련 요청 처리 (회원가입, 조회 등)</li>
<li><code>StockController</code>: 주식 관련 요청 처리 (주식 등록, 목록 조회 등)</li>
<li><code>TradeController</code>: 매수/매도 등 트랜잭션 처리</li>
</ul>
<h3 id="service">service</h3>
<ul>
<li>비즈니스 로직 담당 (실제 매수/매도 수행, 검증 등)</li>
<li><code>PlayerService</code>: 유저 생성/잔액 관리 등</li>
<li><code>TradeService</code>: 주식 거래 로직</li>
<li><code>StockService</code>: 주식 등록/조회</li>
</ul>
<h3 id="repository">repository</h3>
<ul>
<li>DB 접근 담당 (JPA 기반)</li>
<li><code>PlayerRepository</code>, <code>StockRepository</code>: 엔티티에 맞는 CRUD 기능</li>
<li><code>PlayerStockRepository</code>: 다대다 관계 관리</li>
</ul>
<h3 id="dto">dto</h3>
<ul>
<li>API 요청/응답 객체</li>
<li><code>CreatePlayerRequest</code>, <code>TradeRequest</code>: POST 요청 바디 매핑</li>
<li><code>PlayerResponse</code>, <code>StockResponse</code>: 클라이언트에게 응답 전용 구조</li>
</ul>
<h3 id="domain">domain</h3>
<ul>
<li>JPA Entity 정의 (DB 테이블과 매핑)</li>
<li><code>Player</code>: 유저 테이블</li>
<li><code>Stock</code>: 주식 테이블</li>
<li><code>PlayerStock</code>: 보유 주식 매핑 테이블 (다대다 관계)</li>
</ul>
<p>(한명의 유저는 여러 개의 주식을 사고, 한 주식은 여러 유저가 동시에 보유할 수 있다)
중간 엔티티 PlayerStock을 둬서 @ManyToMany를 사용하지 않고 1:N + N:1로 구현</p>
<pre><code>@OneToMany(mappedBy = &quot;player&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
private List&lt;PlayerStock&gt; stocks = new ArrayList&lt;&gt;();
→ 한 명의 유저가 여러 개의 PlayerStock 보유 가능

@ManyToOne
private Stock stock;
-&gt; 여러개의 playStock -&gt; 하나의 player / stock</code></pre><h3 id="config">config</h3>
<ul>
<li>설정 관련 클래스</li>
<li><code>WebConfig</code>: CORS 설정 포함</li>
</ul>
<p>프론트엔드와 백엔드가 분리되어 있을때 거의 등장하는 개념
브라우저가 서로 다른 출처 간의 HTTP 요청을 제한하는 보안 정책</p>
<pre><code>http://localhost:5173   (Vue 개발 서버)
http://localhost:8080   (Spring 서버)</code></pre><pre><code> @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping(&quot;/**&quot;) // 모든 경로에 대해
                        .allowedOrigins(&quot;http://localhost:5173&quot;) 
                        // Vue 개발 서버(http://localhost:5173)에서 들어오는 요청만 허용
                        .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;)
                        // CORS 허용 메서드 지정
                        .allowedHeaders(&quot;*&quot;)
                        // 요청 헤더 종류를 제한하지 않음
                        .allowCredentials(true);
                        // 쿠키, 세션 정보, 인증 토큰 등을 포함한 요청 허용(인증처리가 필요한 경우)
            }
        };
    }</code></pre><ul>
<li><code>DataInitializer</code>: 초기 데이터 자동 삽입</li>
<li><code>GlobalExceptionHandler</code>: 에러를 JSON으로 반환 (Swagger 사용 시 주의)</li>
</ul>
<p>일관된 JSON 에러 메시지를 보내기 위해 사용</p>
<hr>
<p>[깃허브 소스코드]</p>
<p><a href="https://github.com/ww5702/Skala-Stock-Server">Skala-Stock-Server</a>
<a href="https://github.com/ww5702/Skala-Stock-Front">Skala-Stock-Front</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java 클래스 & 인터페이스]]></title>
            <link>https://velog.io/@dev_init_ung/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@dev_init_ung/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Thu, 27 Mar 2025 23:31:50 GMT</pubDate>
            <description><![CDATA[<p>Java를 사용하면 객체지향 프로그래밍(Object-Oriented Programming, OOP)을 지향하게 되며, 그 과정에서 <strong>클래스(class)</strong>와 <strong>인터페이스(interface)</strong>를 자연스럽게 자주 사용하게 된다.</p>
<hr>
<h2 id="클래스class">클래스(Class)</h2>
<p>클래스는 객체를 생서하기 위한 설계도 이다.
필드(속성)와 메서드(행동)을 정의하고, 이를 바탕으로 객체(인스턴스)를 생성한다.
이처럼 현실 세계의 개념을 코드로 추상화하여 구조화하는 방식을 <strong>객체지향 프로그래밍(OOP)</strong>이라고 하며, 클래스는 그 중심에 있는 핵심 요소이다.</p>
<pre><code>// 사람(Person)을 표현하는 클래스
public class Person {
    // 필드: 객체의 속성(상태)을 나타냄
    String name;
    int age;

    // 생성자: 객체 생성 시 호출되는 특별한 메서드
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 메서드: 객체의 동작(행위)을 나타냄
    public void introduce() {
        System.out.println(&quot;안녕하세요, 저는 &quot; + name + &quot;이고, 나이는 &quot; + age + &quot;살입니다.&quot;);
    }
}</code></pre><p>Person 클래스는 name과 age라는 두개의 속성을 가진다.
생성자 Person(String, int)는 객체를 만들 때의 초기값을 설정하기 위해 존재한다.
introduce()는 객체가 수행할 수 있는 행동(메서드)이다.
아래는 main() 메서드에서 객체를 생성하고 사용하는 예시이다.</p>
<pre><code>// 메인 메서드: 프로그램 실행 진입점
    public static void main(String[] args) {
        // Person 객체 생성 (인스턴스화)
        Person student = new Person(&quot;홍길동&quot;, 20);

        // 메서드 호출
        student.introduce(); // 출력: 안녕하세요, 저는 홍길동이고, 나이는 20살입니다.
    }</code></pre><p>new 키워드를 사용하여 student 객체를 생성하고, 홍길동이라는 이름과 20이라는 나이를 부여한 모습을 확인할 수 있다.</p>
<hr>
<h2 id="인터페이스interface">인터페이스(Interface)</h2>
<p>인터페이스는 클래스가 구현해야 할 <strong>공통된 동작(메서드)</strong>을 정의하는 틀이다.
구체적인 구현은 하지 않으며,</p>
<p>“이 메서드를 반드시 구현해야 한다”
는 <strong>약속(계약)</strong>의 역할을 한다.</p>
<p>자바는 다중 상속을 허용하지 않지만, 인터페이스는 여러 개를 구현할 수 있으므로 구조적으로 유연한 설계가 가능하다.</p>
<h3 id="왜-클래스만으로는-부족하고-인터페이스가-필요할까">왜 클래스만으로는 부족하고, 인터페이스가 필요할까?</h3>
<p>인터페이스는 공통된 규약을 정의해서 여러 클래스들이 동일한 방식으로 동작하도록 강제하고, 코드의 유연성과 확장성을 보장한다.
예를 들어보자.
고양이, 강아지, 사자 등의 동물들은 자신만의 울음소리를 가지고 있다
그렇다면, 공통된 작업인 &#39;울음소리&#39;의 소리가 각기 달라서 묶어서 다룰수가 없는 문제가 발생한다
이런 문제를 인터페이스로 해결이 가능하다.</p>
<pre><code>interface Animal {
    void sound(); // 모든 동물은 sound()라는 울음소리를 내야 함
}
// 각 동물이 인터페이스를 구현

class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println(&quot;멍멍&quot;);
    }
}

class Cat implements Animal {
    @Override
    public void sound() {
        System.out.println(&quot;냐옹&quot;);
    }
}

class Lion implements Animal {
    @Override
    public void sound() {
        System.out.println(&quot;으르렁&quot;);
    }
}</code></pre><p>각기 다른 동물들을 하나의 타입(Animal)로 다를 수 있다
코드가 확장 가능, 유지보수가 쉽고, 다형성이 가능하다.</p>
<p>즉, 클래스만 있으면 중복되고, 통일되지 않고 바꾸기 힘든 메서드들을 
인터페이스라는 공통 동작의 표준을 정의하여 설계의 유연성과 확장성을 확보한다.</p>
<blockquote>
</blockquote>
<p>@Override ?
이 메서드는 상위 클래스 또는 인터페이스의 메서드를 정확히 오버라이딩 한것이라고 컴파일러에게 알려주는 안전장치이다.
만약 위의 코드에서</p>
<pre><code>public void souund() {
}</code></pre><p>와 같이 오타가 발생했을때 Override가 없으면 아무일도 일어나지 않는다
souund라는 새로운 메서드가 생성되었다고 판단할 수 있기 때문이다
하지만 @Override를 붙이면 오타가 있거나 메서드 시그니처가 다르면 에러가 발생한다
즉, 실수를 즉시 방지할 수 있어 버그 사전 예방</p>
<h3 id="예제-코드">예제 코드</h3>
<p>Vehicle.java</p>
<pre><code>public interface Vehicle {
    void drive();
    void stop();
    void speedUp();
}</code></pre><p>Car.java</p>
<pre><code>public class Car implements Vehicle {
    private int speed = 0;

    @Override
    public void drive() {
        System.out.println(&quot; 차가 출발합니다. 현재 속도: &quot; + speed + &quot;km/h&quot;);
    }

    @Override
    public void stop() {
        speed = 0;
        System.out.println(&quot;차가 멈췄습니다.&quot;);
    }

    @Override
    public void speedUp() {
        speed += 10;
        System.out.println(&quot;속도를 올립니다! 현재 속도: &quot; + speed + &quot;km/h&quot;);
    }
}</code></pre><p>CarMain.java</p>
<pre><code>public class CarMain {
    public static void main(String[] args) {
        Vehicle myCar = new Car();

        myCar.drive();
        myCar.speedUp();
        myCar.speedUp();
        myCar.drive();
        myCar.stop();

    }
}</code></pre><p>Vehicle 인터페이스는 명확하게 차량의 기본 동작들을 정의 
-&gt; 차량이라면 이런 기능은 공통적으로 들어가야해 라는 행동 규약
Car 클래스는 그 동작을 충실하게 구현
-&gt; 실제 자동차 객체의 동작을 구현
CarMain 클래스는 인터페이스 타입으로 다형성을 활용
-&gt; 프로그램을 실행하고, 인터페이스를 통해 객체를 사용</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클래스, 인스턴스, 객체 완전 정복!]]></title>
            <link>https://velog.io/@dev_init_ung/%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-feat.-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@dev_init_ung/%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-feat.-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Tue, 25 Mar 2025 23:42:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/31a6725a-84d1-4214-aae5-fd2eb3621b53/image.webp" width = 70%, height = 70%></p>


<h2 id="클래스class">클래스(Class)</h2>
<p>클래스는 객체(Object)를 생성하기 위한 설계도 또는 틀이다.
쉽게 말해, 붕어빵을 만들기 위한 ‘틀’처럼 이해하면 좋다.</p>
<pre><code>class Car:
    def __init__(self, brand, model):  # 생성자 (Constructor)
        self.brand = brand
        self.model = model

    def drive(self):
        print(f&quot;{self.brand} {self.model} is driving.&quot;)</code></pre><p>init() = 객체가 생성될때 자동으로 실행되는 생성자
self = 자기자신을 가리키는 변수</p>
<h2 id="인스턴스instance">인스턴스(Instance)</h2>
<p>인스턴스는 클래스를 기반으로 실제 생성된 객체를 의미한다.</p>
<pre><code>car1 = Car(&quot;Tesla&quot;, &quot;Model S&quot;)  # 인스턴스 생성
car2 = Car(&quot;BMW&quot;, &quot;X5&quot;)         # 또 다른 인스턴스 생성

car1.drive()  # Tesla Model S is driving.
car2.drive()  # BMW X5 is driving.
</code></pre><h2 id="객체object">객체(Object)</h2>
<p>객체는 <strong>속성(데이터)과 동작(메서드)</strong>을 함께 가지는 모든 것을 의미한다.
즉, 프로그래밍에서 자료형이 있는 값은 모두 객체이다.</p>
<pre><code>num = 10
text = &quot;Hello&quot;</code></pre><p>일때 num은 int클래스의 객체이고,
text는 str클래스의 객체인것이다.</p>
<h3 id="인스턴스와-객체의-차이는">인스턴스와 객체의 차이는?</h3>
<p>아까 위에서 정의한 car1,car2은 Car 클래스의 인스턴스라고 정의했었다.
하지만 객체라고 불러도 틀린말은 아니다.</p>
<p>즉,
모든 인스턴스는 객체이지만,
모든 객체는 인스턴스가 아니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/6ea0e630-d66a-4d51-9a42-35b220d4feb9/image.png" alt=""></p>
<h2 id="객체지향-프로그래밍">객체지향 프로그래밍</h2>
<p>그렇다면 객체지향 프로그래밍이란 무엇일까?
말 그대로 &#39;객체&#39;를 중심으로 프로그램을 설계하고 개발하는 방식이다.
객체는 (데이터 + 동작)을 함께 가진다고 앞서서 설명했다.
즉, 데이터와 기능을 하나의 단위로 묶어 관리하는 방식이다.
이렇게 구성하면 유지보수성과 확장성이 뛰어나기 때문에 소프트웨어 개발에서 매우 중요하게 사용된다.</p>
<pre><code>class Car:
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year

    def drive(self):
        print(f&quot;{self.brand} {self.model} is driving.&quot;)

class ElectricCar(Car):  # 상속 (Inheritance)
    def __init__(self, brand, model, year, battery_size):
        super().__init__(brand, model, year)
        self.battery_size = battery_size

    def charge(self):
        print(f&quot;{self.brand} {self.model} is charging.&quot;)

# 객체 생성
car1 = Car(&quot;Toyota&quot;, &quot;Corolla&quot;, 2022)
car2 = ElectricCar(&quot;Tesla&quot;, &quot;Model S&quot;, 2023, &quot;100kWh&quot;)

car1.drive()  # Toyota Corolla is driving.
car2.drive()  # Tesla Model S is driving.
car2.charge() # Tesla Model S is charging.</code></pre><p>위 예시에서 ElectricCar는 Car 클래스를 상속받아 추가 기능을 확장하고 있다.</p>
<p>즉, car2 객체는 Car 클래스의 drive() 메서드도 사용할 수 있고,
ElectricCar에서 새롭게 정의한 charge() 메서드도 사용할 수 있다.
물론 Car에 명시된 함수들을 무조건적으로 가지고 있어야한다.</p>
<h3 id="객체지향-프로그래밍의-4가지-특징">객체지향 프로그래밍의 4가지 특징</h3>
<ol>
<li>캡슐화(Encapsulation)</li>
</ol>
<ul>
<li>내부 데이터(예: brand, model, year)는 외부에서 직접 접근할 수 없으며, 메서드를 통해서만 접근 가능하다.</li>
<li>데이터 보호 및 구조화에 효과적이다.</li>
</ul>
<ol start="2">
<li>상속(Inheritance)</li>
</ol>
<ul>
<li>ElecticCar에서 Car에 들어간 모든 기능을 사용할 수 있다.</li>
<li>기존 클래스를 확장하여 새로운 기능을 추가할 수 있다.</li>
<li>코드의 재사용성과 유연성이 향상된다.</li>
</ul>
<ol start="3">
<li>다형성(Polymorphism)</li>
</ol>
<ul>
<li>같은 이름의 메서드가 클래스에 따라 다르게 동작할 수 있다.</li>
</ul>
<ol start="4">
<li>추상화(Abstraction)</li>
</ol>
<ul>
<li>내부 동작은 숨기고, 꼭 필요한 인터페이스만 제공한다.</li>
<li>복잡성을 줄이고 사용성을 높인다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git Fork & Commit & Sync]]></title>
            <link>https://velog.io/@dev_init_ung/Git-Fork-Commit-Sync</link>
            <guid>https://velog.io/@dev_init_ung/Git-Fork-Commit-Sync</guid>
            <pubDate>Tue, 25 Mar 2025 10:51:05 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/817e08c5-22f0-4afb-b9c7-2a26a7dc041c/image.png" alt="">
오픈소스나 팀 프로젝트를 진행함에 따라 <strong>git fork</strong>를 자주 사용하게 된다.
협업 효율성과 안전성때문에 git fork를 사용하는데
보통 fork 작업 -&gt; PR(Pull Request)의 흐름으로 진행된다.</p>
<p>원본 저장소를 복사해서 나만의 git 공간에서 안전하게 작업후 
변경 내용을 commit 그리고 pull request로 변경사항을 제안한다.</p>
<h2 id="fork-진행-과정">Fork 진행 과정</h2>
<h3 id="1-저장소-fork">1. 저장소 Fork</h3>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/58a35131-4959-46d3-a0c1-b204c9248102/image.png" alt="">
우측 상단의 Fork 버튼을 눌러 본인 계정의 repository로 fork한다.</p>
<h3 id="2-로컬에-clone">2. 로컬에 clone</h3>
<pre><code>git clone https://github.com/내계정/저장소.git
</code></pre><p>내 로컬에 clone하여 fork한 파일들을 그대로 불러온다.</p>
<h3 id="3-브랜치-생성--작업">3. 브랜치 생성 &amp; 작업</h3>
<pre><code>브랜치 생성 + 이동
git checkout -b feature/my-feature 
브랜치 생성
git branch feature/my-feature
해당 브랜치로 이동
git checkout feature/my-feature</code></pre><p>원하는 브랜치를 생성하고 작업하거나
그럴 생각이 없을 경우 해당 과정은 생략하고 main에서 작업한다
(하지만 브랜치를 통해 메인 브랜치에 영향을 주지 않으면서 새로운 기능 기발이나 수정 작업을 안전하게 진행하는게 좋다)</p>
<h3 id="4-커밋--push">4. 커밋 &amp; push</h3>
<pre><code>git add .
git commit -m &quot;feat: 새로운 기능 추가&quot;
git push origin feature/my-feature
혹은
git push origin main</code></pre><p>변경사항을 내 git에 추가하고 commit 후 push한다</p>
<pre><code>git add index.html -&gt; index.html만 스테이징
git add . -&gt; 현재 디렉토리 기준 모든 변경 사항을 스테이징</code></pre><p>변경 사항에 대한 commit 메시지는 사람들이 정해놓은 규칙이 있다.
이를 따라주면 commit 메시지만으로 변경사항에 대한 추측이 가능하다.</p>
<blockquote>
</blockquote>
<p>일관된 commit 메시지를 작성하면 협업, 릴리즈, 추적 등에 유리하다.
주요 commit 타입
feat- 새로운 기능 추가
fix - 버그 수정
docs - 문서 수정 (README 등)
style - 코드 포맷팅 (세미콜론, 들여쓰기 등)
refactor - 리팩토링 (기능 변화 없이 코드 개선)
test - 테스트 코드 추가/수정
chore - 기타 변경사항 (빌드 설정, 패키지 매니저 등)</p>
<h3 id="5-pull-request-생성">5. Pull Request 생성</h3>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/6d583b10-2a97-4cb2-87b0-ebf6b75a308f/image.png" alt="">
내 레파지토리 안의 좌측 상단 Contribute를 통해 pull request를 생성한다.
contribute 클릭 -&gt; open pull request 클릭
<img src="https://velog.velcdn.com/images/dev_init_ung/post/2f7da8f9-15f2-4f60-b4d8-289f69fcb00c/image.png" alt="">
commit 메시지 작성 후 create pull request를 작성하면
담당자에게 pull request 요청이 간다.</p>
<h2 id="sync-fork">Sync fork</h2>
<p>fork한 저장소는 시간이 지나면서 많은 공동 개발자들의 pull request로 인해 원본 저장소(upstream)와 내용이 달라질 수 있다. 
따라서 정기적으로 동기화(sync)를 진행 해줘야 한다. 그렇지 않으면 오래된 코드로 작업하게 되어 충돌이나 오류가 발생할 수 있다.</p>
<p>두가지 방법이 있는데
깃 허브 내에서 버튼 클릭을 통해 쉽게 하는 방법
로컬의 터미널을 통해 귀찮게 하는 방법
둘다 똑같지만 알아두는게 좋다.</p>
<h3 id="깃-허브-내에서-하는-방법">깃 허브 내에서 하는 방법</h3>
<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/6d583b10-2a97-4cb2-87b0-ebf6b75a308f/image.png" alt="">
아까 위의 사진에서 Sync fork 버튼을 클릭후
git pull origin main으로 가져올 수 있다.</p>
<h3 id="로컬의-터미널을-통하는-방법">로컬의 터미널을 통하는 방법</h3>
<pre><code>1. upstream 추가
git remote add upstream https://github.com/원본계정/저장소이름.git
2. upstream 최신 내용 가져오기
git fetch upstream
3. 내 로컬 main 브랜치로 이동
git checkout main
4. upstream의 main 브랜치 내용 병합
git merge upstream/main</code></pre><p>4번을 입력하면 git은 자동으로 변합할 수 없는 부분이 없을때는 바로 커밋이 되지만
병합 메시지를 직접 작성하라고 요구할 수 도 있다.
메시지를 그대로 두고 저장하려면 ESC -&gt; :wq 입력후 Enter하면 merge 커밋이 완료된다.</p>
<pre><code>5. 내 fork 원격 저장소에 push
git push origin main</code></pre><p>이제 열심히 sync된 코드들을 통해 개발을 다시 진행하면된다.👊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블 슈팅] HTTP Status Code]]></title>
            <link>https://velog.io/@dev_init_ung/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-HTTP-Status-Code</link>
            <guid>https://velog.io/@dev_init_ung/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-HTTP-Status-Code</guid>
            <pubDate>Mon, 24 Mar 2025 08:25:07 GMT</pubDate>
            <description><![CDATA[<p><img src = "https://velog.velcdn.com/images/dev_init_ung/post/27c8c905-c094-4ca7-b541-3252c2f6db6a/image.png" width = "70%" height = "70%"></p>
같은 조 끼리 각자만의 로그인 페이지를 구현 하던 와중 발생할 HTTP Status Code에 대한 트러블 슈팅이다.
<a href="https://github.com/happy6team/login-frontend">로그인 서비스 깃허브</a>
## HTTP Status Code
HTTP Status Code는 클라이언트 요청에 대해 서버가 응답한 결과 상태를 숫자로 나타낸 것이다.

<p>주요 코드는 다음과 같다.
<img src="https://velog.velcdn.com/images/dev_init_ung/post/b1c6241a-0284-48fa-b9c9-c2fac3f98123/image.png" alt=""></p>
<h3 id="405-오류">405 오류</h3>
<p>프론트: /api/users/signup 으로 POST 요청 전송
서버: /api/users/signup 이라는 @PostMapping(&quot;/signup&quot;) 없음 → 405 Method Not Allowed
따라서 프론트 코드 고치기</p>
<h3 id="회원가입-실패--undefined">회원가입 실패 : undefined</h3>
<p>response.data.success가 undefined이기 때문
즉, 서버가 응답은 보냈지만, 그 응답(JSON)에 &quot;success&quot;라는 필드가 없어서 생기는 현상
방법 : 프론트에서 응답 구조에 맞게 처리</p>
<pre><code>const signUp = async () =&gt; {
  try {
    const response = await axios.post(&#39;/api/users&#39;, {
      userId: userId.value,
      userName: name.value,
      nickname: nickname.value,
      password: password.value,
      phoneNumber: phone.value,
    })

    // 응답 구조에 따라 분기 처리
    if (response.status === 201) {
      alert(&#39;회원가입 성공!&#39;)
      router.push(&#39;/jaeung&#39;)
    } else {
      alert(&#39;회원가입 실패: 알 수 없는 상태&#39;)
    }
  } catch (error) {
    if (error.response) {
      console.log(&#39;서버 응답 에러:&#39;, error.response.data)
      alert(&#39;회원가입 실패: &#39; + (error.response.data.message || &#39;서버 오류&#39;))
    } else {
      alert(&#39;서버 연결 실패: &#39; + error.message)
    }
  }
}</code></pre><p>201 반환 -&gt; Created
요청이 성공적이었으며 그 결과로 새로운 리소스가 생성되었다. 이 응답은 일반적으로 POST 요청 또는 일부 PUT 요청 이후에 따라온다 라는 의미.</p>
<h3 id="500-internal-server-error">500 Internal Server Error</h3>
<p>DataIntegrityViolationException
constraint [PUBLIC.CONSTRAINT_INDEX_4]
-&gt; 중복된 DB의 Unique 제약조건을 위반해서 삽입 실패</p>
<hr>
<h1 id="회원가입-성공">회원가입 성공</h1>
<p>그리고 그 이후 발생한 오류들...</p>
<hr>
<h3 id="400-request-failed-with-status-code">400 Request failed with status code</h3>
<p>잘못된 문법으로 서버가 요청을 이해할 수 없음을 의미
로그인 페이지 이동 후 아이디 비번 입력시 서버 오류 발생
로그인 페이지에서</p>
<pre><code>const response = await axios.get(&#39;/api/users/login&#39;, {
      params: {
        id: userId.value,
        Pw : password.value,
      },
    })</code></pre><p>파라미터가 제대로 적혀있지 않음을 확인</p>
<pre><code>const response = await axios.get(&#39;/api/users/login&#39;, {
      params: {
        userId: userId.value,
        password: password.value,
      },
    })</code></pre><p>수정했지만 여전히 400 오류 발생
콘솔을 찍어 브라우저 url은 정확히 보내졌음을 확인</p>
<blockquote>
</blockquote>
<p>GET <a href="http://localhost:8080/api/users/login?userId=a1&amp;password=a1">http://localhost:8080/api/users/login?userId=a1&amp;password=a1</a> </p>
<p>핵심 오류 메시지 확인</p>
<pre><code>message: &#39;Required request body is missing: public ...
loginUsers(com.practice.loginServer.dto.LoginRequest)&#39;</code></pre><p>백엔드에서 get 요청시 @requestBody를 사용할 수 없음
따라서 get-&gt;post로 프론트 변경</p>
<h3 id="서버오류">서버오류</h3>
<pre><code>서버 오류: Cannot invoke &quot;com.practice.loginServer.domain.Users.getPassword()&quot; because &quot;users&quot; is null</code></pre><p>서버 내부 로직에서 null pointer 예외가 발생
users.getPassword() 를 호출하려 했는데
users 자체가 null (즉, 없는 사용자임)</p>
<p>params는 GET 요청용 파라미터에 쓰는 키이다
axios.post(..., { params: ... })라고 쓰면 → body가 아니라 이상한 형식이 들어간다.
서버의 @RequestBody는 body 안의 userId, password를 기대하는데,
지금은 body에 { params: { ... } }가 들어가니까 null이 들어가는 것</p>
<pre><code>await axios.post(&#39;/api/users/login&#39;, {
  userId: userId.value,
  password: password.value,
})
</code></pre><p>이에 따라 params 키 삭제</p>
<p>정상적으로 회원가입 - 로그인 - 메인페이지 구현 성공</p>
<hr>
<p>참고 페이지 (HTTP 상태 코드) : <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Status">https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Status</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vue-template (3)]]></title>
            <link>https://velog.io/@dev_init_ung/template-%EB%AC%B8%EB%B2%953</link>
            <guid>https://velog.io/@dev_init_ung/template-%EB%AC%B8%EB%B2%953</guid>
            <pubDate>Sun, 23 Mar 2025 05:46:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_init_ung/post/762998f6-d207-4d6e-81bb-9b8805a23b48/image.png" alt=""></p>
<h3 id="선언적-렌더링">선언적 렌더링</h3>
<p>일단 선언적 렌더링이란 무엇일까?
Vue의 template는 HTML과 거의 동일한 구조를 갖고 있으면서, 데이터 상태에 따라 UI를 선언적으로 기술할 수 있다. 
&#39;무엇을 보여줄까&#39;를 목표중심으로 작성하면, 프레임워크가 그에 맞게 DOM을 알아서 업데이트한다.
명령적 프로그래밍은 document.getElementByID().innerText와 같은 형식으로 DOM을 직접 조작해야했다. 하지만 선언형 방식에서는 단순히 이렇게 보여줘 라고 선언만 하면 내부적으로 알아서 조작해준다.</p>
<p>이제 선언적 렌더링을 구현하는 도구인 template문법에 대해 알아보자
template 안에서 다음과 같은 문법을 사용한다는 것은
&#39;이 상태일때 화면에서는 이렇게 보여줘&#39; 라는 목표 상태를 선언해주고, Vue가 그에 맞게 DOM을 알아서 구성해준다.</p>
<h3 id="----mustache-문법-interpolation">{{ }} - Mustache 문법 (Interpolation)</h3>
<p>보간법(Interpolation)이라고도 한다.
데이터의 값을 HTML에 출력해주는 문법이다.
script안 data()안에 작성한 key를 template에 데이터 바인딩을 할 수 있는 가장 기본 형태</p>
<pre><code>&lt;template&gt;
    &lt;h1&gt;Hello {{ message }} &lt;/h1&gt;
&lt;/template&gt;
&lt;script&gt;
export default {
    data() {
        return {
            message: &quot;Hello&quot;
        }
    }
}</code></pre><hr>
<p>디렉티브란
v-접두사가 있는 특수 속성이다. </p>
<h3 id="v-text">v-text</h3>
<p>보간법과 동일하게 template에 데이터바인딩을 하는 방법이다</p>
<pre><code>&lt;template&gt;
    &lt;h1 v-text=&quot;animal&quot; /&gt;
&lt;/template&gt;</code></pre><blockquote>
</blockquote>
<p>{{ }} 와 v-text의 차이점</p>
<ol>
<li>닫는 태그가 없어도 된다 ( 코드 간결성 )<pre><code>&lt;h1&gt;Hello {{ message }}&lt;/h1&gt;
&lt;h1 v-text=&quot;message&quot; /&gt;</code></pre></li>
<li>{{ }} 는 엘리먼트 안에 다른 text가 있어도 되지만, v-text는 다른 text가 있을시 오류<pre><code>&lt;h1 v-text=&quot;message&quot;&gt;&lt;/h1&gt; O
&lt;h1 v-text=&quot;message&quot;&gt;123&lt;/h1&gt; X</code></pre></li>
</ol>
<h3 id="v-bind-속성-바인딩">v-bind 속성 바인딩</h3>
<p>HTML 속성에 데이터를 바인딩 한다.
: 으로 줄여서 사용이 가능하다.</p>
<pre><code>&lt;template&gt;
    &lt;img v-bind:src=&quot;imgUrl&quot; /&gt;
    &lt;!-- 축약형 --&gt;
    &lt;img :src=&quot;imgUrl&quot; /&gt;
&lt;/template&gt;</code></pre><p>imgUrl의 값이 img 태그 안의 src 속성에 동적으로 바인딩된다.
값이 변경되면 이미지도 자동으로 갱신된다.</p>
<h3 id="v-model-양방향-바인딩">v-model 양방향 바인딩</h3>
<p>사용자 입력과 데이터 상태를 동기화한다.</p>
<pre><code>&lt;template&gt;
  &lt;input v-model=&quot;username&quot; /&gt;
  &lt;p&gt;입력한 이름: {{ username }}&lt;/p&gt;
&lt;/template&gt;</code></pre><p>input에 입력한 값이 username 변수에 자동으로 반영된다
그리고 이에 따라 값이 바뀌면 p태그에 출력되는 내용도 실시간으로 바뀐다.
양방향 바인딩이기 때문에, 데이터와 뷰가 항상 동기화된다.</p>
<h3 id="v-if--v-else-if--v-else-조건부-렌더링">v-if / v-else-if / v-else 조건부 렌더링</h3>
<p>지정한 뷰 데이터의 참,거짓에 따라 표시 여부를 선택한다.</p>
<pre><code>&lt;template&gt;
  &lt;p v-if=&quot;isLoggedIn&quot;&gt;환영합니다!&lt;/p&gt;
  &lt;p v-else&gt;로그인 해주세요.&lt;/p&gt;
&lt;/template&gt;</code></pre><p>isLoggedIn이 true면 환영합니다 출력
false면 로그인 해주세요가 출력
해당 조건이 바뀔 때 마다 동적으로 표기</p>
<h3 id="v-show-조건부-표시">v-show 조건부 표시</h3>
<p>DOM을 항상 렌더링하되, CSS display 속성으로 보이기/숨기기를 제어한다</p>
<pre><code>&lt;template&gt;
  &lt;p v-show=&quot;isVisible&quot;&gt;이 문장은 조건에 따라 표시됩니다.&lt;/p&gt;
&lt;/template&gt;</code></pre><p>isVissible이 true면 p태그가 보이고, false면 숨겨진다.</p>
<blockquote>
</blockquote>
<p>v-if와 v-show의 차이점
false인 경우 v-if는 아예 엘리멘트조차 생성되지않고, v-show의 경우 엘리멘트는 생성되어있지만 display:none;인 상태가 된다.</p>
<h3 id="v-for-반복-렌더링">v-for 반복 렌더링</h3>
<p>배열이나 객체를 기반으로 반복적으로 DOM을 생성</p>
<pre><code>&lt;template&gt;
  &lt;ul&gt;
    &lt;li v-for=&quot;(item, index) in items&quot; :key=&quot;index&quot;&gt;
      {{ index }} - {{ item }}
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/template&gt;</code></pre><p>items 배열을 순회하면서 각 요소마다 li태그를 생성한다
key를 안적어줄 경우 오류가 난다.</p>
<h3 id="v-on-이벤트-핸들러-등록">v-on 이벤트 핸들러 등록</h3>
<p>사용자 이벤트(클릭,입력)에 대한 함수를 연결한다
@ 로 생략 가능</p>
<pre><code>&lt;template&gt;
  &lt;button @click=&quot;increment&quot;&gt;클릭 수: {{ count }}&lt;/button&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count += 1
    }
  }
}
&lt;/script&gt;</code></pre><p>버튼을 클릭하면 increment함수가 실행되고 count값이 1씩 증가한다.
count가 변경됨에 따라 text도 자동으로 갱신된다.</p>
<h3 id="v-slot-컴포넌트-슬록">v-slot 컴포넌트 슬록</h3>
<p>부모 컴포넌트가 자식 컴포넌트의 특정 위치에 콘텐츠를 삽입할 수 있도록 한다</p>
<pre><code>&lt;!-- 부모 컴포넌트 --&gt;
&lt;MyCard&gt;
  &lt;template v-slot:header&gt;
    &lt;h3&gt;나의 헤더&lt;/h3&gt;
  &lt;/template&gt;
  &lt;p&gt;본문입니다.&lt;/p&gt;
&lt;/MyCard&gt;

&lt;!-- MyCard.vue --&gt;
&lt;template&gt;
  &lt;div&gt;
    &lt;header&gt;&lt;slot name=&quot;header&quot;&gt;&lt;/slot&gt;&lt;/header&gt;
    &lt;main&gt;&lt;slot&gt;&lt;/slot&gt;&lt;/main&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><p>v-slot:header는 slot name &quot;header&quot;에 삽입된다.</p>
<h3 id="v-html-html-문자열-렌더링">v-html HTML 문자열 렌더링</h3>
<p>데이터 안의 HTML 문자열을 실제 HTML로 출력한다</p>
<pre><code>&lt;template&gt;
  &lt;div v-html=&quot;rawHtml&quot;&gt;&lt;/div&gt;
&lt;/template&gt;</code></pre><p>반드시 신뢰할 수 있는 데이터에서만 사용해야 한다는 위험이 있다. (XSS)
따라서 잘 사용하지 않는다.</p>
]]></description>
        </item>
    </channel>
</rss>