<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>marha.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 15 Apr 2026 06:42:55 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>marha.log</title>
            <url>https://velog.velcdn.com/images/marha-hwang/profile/87855ef5-ae2f-412f-929e-c0090d962e6a/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. marha.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/marha-hwang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[API 호출을 넘어 블랙박스 열어보기: 트랜스포머 디코더 기반 GPT-2 아키텍처 밑바닥부터 구현하기]]></title>
            <link>https://velog.io/@marha-hwang/API-%ED%98%B8%EC%B6%9C%EC%9D%84-%EB%84%98%EC%96%B4-%EB%B8%94%EB%9E%99%EB%B0%95%EC%8A%A4-%EC%97%B4%EC%96%B4%EB%B3%B4%EA%B8%B0-%ED%8A%B8%EB%9E%9C%EC%8A%A4%ED%8F%AC%EB%A8%B8-%EB%94%94%EC%BD%94%EB%8D%94-%EA%B8%B0%EB%B0%98-GPT-2-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@marha-hwang/API-%ED%98%B8%EC%B6%9C%EC%9D%84-%EB%84%98%EC%96%B4-%EB%B8%94%EB%9E%99%EB%B0%95%EC%8A%A4-%EC%97%B4%EC%96%B4%EB%B3%B4%EA%B8%B0-%ED%8A%B8%EB%9E%9C%EC%8A%A4%ED%8F%AC%EB%A8%B8-%EB%94%94%EC%BD%94%EB%8D%94-%EA%B8%B0%EB%B0%98-GPT-2-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 15 Apr 2026 06:42:55 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="1-들어가며--왜-블랙박스를-열어보려-했는가">1. 들어가며 — 왜 블랙박스를 열어보려 했는가</h2>
<p>지난 기간 여러가지 스택의 개발을 경험해보았다. 그 과정에서 하나의 습관이 생겼는데, 서드파티 라이브러리가 기대와 다르게 동작하면 내부 소스를 직접 까보는 것이다. 네트워크 계층에서 원인 모를 타임아웃이 발생하면 소켓 레벨까지 내려가서 패킷을 확인했고, 메모리 누수가 의심되면 힙 덤프를 떠서 객체 참조 그래프를 추적했다. 문제를 해결하려면 결국 블랙박스를 열어야 했다.</p>
<p>AI 영역으로 넘어오면서도 같은 충동이 생겼다. OpenAI API를 호출하면 텍스트가 생성된다. 잘 된다. 그런데 <strong>안에서 무슨 일이 벌어지는지 모른다</strong>는 사실이 계속 걸렸다. <code>response = client.chat.completions.create(...)</code> 한 줄 뒤에서, 토큰이 어떻게 선택되고, 어텐션이 어떻게 계산되고, 왜 그 단어가 다음에 오는지. API 응답의 JSON만 보고 있으면 이건 그냥 HTTP 클라이언트를 쓰는 것과 다를 바 없다.</p>
<p>그래서 직접 구현해보기로 했다. 대상은 GPT-2 아키텍처, 트랜스포머 디코더 구조의 원형에 가까운 아키텍처이다. 이 글은 PyTorch로 GPT-2를 밑바닥부터 쌓아올리며 겪은 과정과 그 과정에서 얻은 인사이트를 정리한 기록이다.</p>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/047f3a44-a2ea-43cb-8c11-bf4ca4a16edb/image.png" alt="">
참조 : 위의 교재를 참고하여 학습하고 구현하였다.</p>
<hr>
<h2 id="2-트랜스포머-그리고-gpt-2아키텍처">2. 트랜스포머, 그리고 GPT-2아키텍처</h2>
<h3 id="21-트랜스포머의-핵심-셀프-어텐션">2.1 트랜스포머의 핵심: 셀프 어텐션</h3>
<p>구현에 들어가기 전에, 트랜스포머가 왜 등장했는지부터 짚고 넘어가야 한다.</p>
<p>RNN/LSTM은 시퀀스를 순서대로 처리한다. t번째 토큰을 처리하려면 t-1번째의 연산이 끝나야 한다. 이 구조는 두 가지 근본적인 문제를 안고 있다.</p>
<ul>
<li><strong>병렬화가 불가능하다.</strong> GPU의 수천 개 코어가 있어도, 순차적으로 밖에 처리하지 못한다.</li>
<li><strong>장기 의존성(Long-range Dependency)이 소실된다.</strong> 시퀀스가 길어질수록 앞쪽 정보가 희석된다.</li>
</ul>
<p>트랜스포머는 순환 구조를 버리고 <strong>셀프 어텐션(Self-Attention)</strong>으로 이 두 문제를 동시에 해결했다. 모든 토큰이 다른 모든 토큰과의 관계를 한 번에 계산하기 때문에, 병렬 처리가 가능하고 거리에 관계없이 직접적인 연결이 생긴다.</p>
<p>셀프 어텐션의 핵심 연산은 Q(Query), K(Key), V(Value) 세 벡터의 상호작용이다.</p>
<table>
<thead>
<tr>
<th>벡터</th>
<th>역할</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Q</strong> (Query)</td>
<td>&quot;나는 어떤 정보가 필요한가?&quot;</td>
<td>질문</td>
</tr>
<tr>
<td><strong>K</strong> (Key)</td>
<td>&quot;나는 어떤 정보를 갖고 있는가?&quot;</td>
<td>라벨, 분류표</td>
</tr>
<tr>
<td><strong>V</strong> (Value)</td>
<td>&quot;내가 실제로 전달할 정보&quot;</td>
<td>실제 내용</td>
</tr>
</tbody></table>
<p>Q와 K의 내적(Dot-Product)은 두 벡터 간의 유사도를 계산한다. 이 유사도가 곧 &quot;이 단어가 저 단어에 얼마나 주목해야 하는가&quot;를 나타내는 어텐션 스코어가 된다. 행렬 곱으로 모든 토큰 쌍의 유사도가 한 번에 계산되기 때문에 병렬 처리가 가능한 것이다.</p>
<p>여기서 하나 더. 어텐션 스코어를 √d_k로 나누는 <strong>스케일링(Scaling)</strong> 과정이 있다. 이걸 생략하면 키 벡터의 차원이 커질수록 내적 값의 분산이 같이 커지고, 소프트맥스 함수의 출력이 0 또는 1에 극단적으로 몰리는 &quot;소프트맥스 포화&quot; 현상이 발생한다. 포화된 영역에서는 그래디언트가 거의 0이 되어 학습이 멈춘다.</p>
<h3 id="22-인코더-디코더-vs-디코더-온리">2.2 인코더-디코더 vs. 디코더 온리</h3>
<p>2017년 &quot;Attention Is All You Need&quot; 논문의 오리지널 트랜스포머는 <strong>인코더-디코더</strong> 구조다. 인코더가 입력 문장의 문맥을 이해하고, 디코더가 그 문맥을 참조하면서 출력을 생성한다. 번역 태스크에 적합한 구조다.</p>
<p>이후 이 구조는 두 갈래로 분화된다.</p>
<pre><code>오리지널 트랜스포머 (인코더 + 디코더)
    ├── BERT 계열: 인코더만 사용 → 문맥 이해 특화
    └── GPT 계열:  디코더만 사용 → 텍스트 생성 특화</code></pre><p>GPT는 인코더를 사용하지 않으므로 <strong>크로스 어텐션 레이어가 존재하지 않는다.</strong> 질문과 답을 구분하지 않고, 입력으로 들어온 텍스트 뒤에 다음 단어를 이어 붙이는 방식으로 동작한다. &quot;Hello, I am&quot;이 들어오면 그 뒤에 올 단어를 하나씩 예측해 나가는 것이다. 단순하지만, 이 단순함이 스케일링에서 힘을 발휘했다.</p>
<h3 id="23-gpt-2에서-달라진-것-pre-layer-normalization">2.3 GPT-2에서 달라진 것: Pre-Layer Normalization</h3>
<p>오리지널 트랜스포머는 <strong>Post-LN</strong> 구조다. 어텐션이나 FFN 연산을 수행한 <strong>후에</strong> 잔차 연결과 정규화를 적용한다.</p>
<pre><code>Post-LN: x → Attention → Add(x) → LayerNorm</code></pre><p>GPT-2는 이를 <strong>Pre-LN</strong>으로 바꿨다. 정규화를 연산 <strong>전에</strong> 적용한다.</p>
<pre><code>Pre-LN:  x → LayerNorm → Attention → Add(x)</code></pre><p>차이는 미묘해 보이지만 실질적인 효과가 있다. Pre-LN은 잔차 경로에 정규화되지 않은 원본 값이 그대로 흐르기 때문에, 깊은 레이어에서도 그래디언트가 안정적으로 전파된다. 이 구조적 차이는 이후 <code>TransformerBlock</code>을 구현할 때 <code>forward</code> 메서드의 연산 순서에 직접 반영된다.</p>
<hr>
<h2 id="3-밑바닥부터-쌓아올린-gpt-2-구현기">3. 밑바닥부터 쌓아올린 GPT-2 구현기</h2>
<p>이제부터 실제 코드다. 텍스트가 입력되어 다음 단어가 출력되기까지, 데이터가 거치는 모든 단계를 하나씩 구현해 나간다.</p>
<h3 id="31-토크나이저--텍스트를-숫자로-바꾸는-첫-관문">3.1 토크나이저 — 텍스트를 숫자로 바꾸는 첫 관문</h3>
<p>모델은 문자열을 직접 처리하지 못한다. 텍스트를 정수 ID의 시퀀스로 변환하는 과정이 필요하고, 이것이 토크나이저의 역할이다.</p>
<p>가장 단순한 접근은 <strong>단어 기반 토큰화</strong>다. 공백과 구두점 기준으로 텍스트를 분리하고, 각 단어에 고유한 정수 ID를 부여한다.</p>
<pre><code class="language-python">class SimpleTokenizerV2:
    def __init__(self, vocab):
        self.str_to_int = vocab
        self.int_to_str = {i: s for s, i in vocab.items()}

    def encode(self, text):
        preprocessed = re.split(r&#39;([,.:;?_!&quot;()\&#39;]|--|\s)&#39;, text)
        preprocessed = [item.strip() for item in preprocessed if item.strip()]
        preprocessed = [
            item if item in self.str_to_int
            else &quot;&lt;|unk|&gt;&quot; for item in preprocessed
        ]
        ids = [self.str_to_int[s] for s in preprocessed]
        return ids

    def decode(self, ids):
        text = &quot; &quot;.join([self.int_to_str[i] for i in ids])
        text = re.sub(r&#39;\s+([,.:;?!&quot;()\&#39;])&#39;, r&#39;\1&#39;, text)
        return text</code></pre>
<p>이 방식은 직관적이지만 한계가 명확하다. 어휘 사전에 없는 단어는 전부 <code>&lt;|unk|&gt;</code>로 처리된다. 희귀한 단어, 오타, 신조어 — 전부 같은 토큰으로 뭉개진다.</p>
<p>실제 GPT-2는 <strong>BPE(Byte Pair Encoding)</strong>를 사용한다. 모든 단어를 문자 단위로 쪼갠 뒤, 가장 자주 등장하는 문자 쌍을 반복적으로 병합하여 서브워드 토큰을 만드는 알고리즘이다. 원하는 어휘 크기(vocab size)에 도달할 때까지 병합을 반복한다. 이렇게 하면 희귀한 단어도 서브워드 조합으로 표현할 수 있고, 어휘 사전의 크기도 통제 가능하다.</p>
<p>구현에서는 OpenAI의 <a href="https://github.com/openai/tiktoken">tiktoken</a> 라이브러리를 사용했다.</p>
<pre><code class="language-python">import tiktoken
tokenizer = tiktoken.get_encoding(&quot;gpt2&quot;)
# vocab_size: 50257</code></pre>
<h3 id="32-토큰-임베딩--포지셔널-임베딩">3.2 토큰 임베딩 + 포지셔널 임베딩</h3>
<p>토큰 ID는 단순한 정수일 뿐이다. 모델이 의미적 연산을 수행하려면, 이 정수를 고차원 벡터 공간에 매핑해야 한다. <code>nn.Embedding</code>은 결국 조회 테이블(Lookup Table)이다. 토큰 ID를 인덱스로 사용해서, 해당 행의 벡터를 꺼내오는 것이 전부다.</p>
<pre><code class="language-python"># 토큰 임베딩: ID → 벡터
token_embedding_layer = torch.nn.Embedding(vocab_size, output_dim)  # (50257, 256)
token_embeddings = token_embedding_layer(inputs)
# inputs: (8, 4) → token_embeddings: (8, 4, 256)</code></pre>
<p>그런데 여기서 문제가 하나 있다. 셀프 어텐션은 모든 토큰을 동시에 처리하기 때문에, <strong>순서 정보가 없다.</strong> &quot;John is taller than Tom&quot;과 &quot;Tom is taller than John&quot;이 동일한 어텐션 결과를 만들어낸다. 의미가 정반대인데도.</p>
<p>이 문제를 해결하기 위해 <strong>포지셔널 임베딩</strong>을 추가한다. 각 위치마다 고유한 벡터를 생성하고, 이를 토큰 임베딩에 더한다.
학습이 진행되면서 모델은 포지셔널 임베딩의 패턴을 통해 상대적 위치의 개념을 스스로 학습한다.</p>
<pre><code class="language-python"># 포지셔널 임베딩: 위치 → 벡터
pos_embedding_layer = torch.nn.Embedding(context_length, output_dim)  # (4, 256)
pos_embeddings = pos_embedding_layer(torch.arange(max_length))

# 최종 입력 = 토큰 임베딩 + 포지셔널 임베딩
input_embeddings = token_embeddings + pos_embeddings
# (8, 4, 256) — shape는 그대로, 위치 정보가 벡터에 녹아들었다</code></pre>
<h3 id="33-멀티헤드-어텐션--구현의-핵심이자-가장-까다로운-부분">3.3 멀티헤드 어텐션 — 구현의 핵심이자 가장 까다로운 부분</h3>
<blockquote>
<p>&quot;문장 속 단어의 진짜 의미는 주변 단어들이 알려준다.&quot;</p>
</blockquote>
<p>셀프 어텐션은 이 직관을 수학으로 옮긴 것이다. 입력된 임베딩 벡터를 가중치 행렬과 연산하여, 문맥이 반영된 새로운 벡터로 변환한다.</p>
<p><strong>왜 &quot;멀티&quot;헤드인가?</strong> 하나의 큰 어텐션 헤드는 여러 종류의 관계를 하나의 가중합으로 뭉개버린다. CNN에서 필터 하나만 쓰면 색상 하나의 특징만 잡아내는 것과 같다. 여러 헤드를 병렬로 두면, 각 헤드가 구문적 관계, 의미적 유사성, 지시 관계 등 서로 다른 문맥적 특징을 독립적으로 포착할 수 있다.</p>
<p><strong>마스킹은 왜 필요한가?</strong> GPT는 디코더 모델이다. 추론 시에는 미래의 단어를 알 수 없다. 그런데 훈련 시에는 전체 문장이 한 번에 입력된다. 이 불일치를 해결하기 위해, 어텐션 스코어 계산 후 소프트맥스 적용 전에 미래 위치의 값을 -∞로 채운다. 소프트맥스를 거치면 -∞는 0이 되어, 미래 토큰에 대한 어텐션이 완전히 차단된다.</p>
<p>전체 구현은 다음과 같다. 각 단계의 텐서 shape 변화를 주석으로 남겼다.</p>
<pre><code class="language-python">class MultiHeadAttention(nn.Module):
    def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
        super().__init__()
        assert d_out % num_heads == 0, &quot;d_out must be divisible by num_heads&quot;
        self.d_out = d_out
        self.num_heads = num_heads
        self.head_dim = d_out // num_heads  # 768 // 12 = 64

        # 가중치 행렬: 각각 (d_in, d_out) 크기의 Linear 레이어
        self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
        self.out_proj = nn.Linear(d_out, d_out)  # 헤드 병합 후 최종 프로젝션
        self.dropout = nn.Dropout(dropout)

        # 상삼각 행렬로 미래 토큰 마스킹 준비
        self.register_buffer(
            &quot;mask&quot;,
            torch.triu(torch.ones(context_length, context_length), diagonal=1)
        )

    def forward(self, x):
        b, num_tokens, d_in = x.shape
        # x: (b, num_tokens, d_in) 예: (2, 6, 768)

        # Step 1: Q, K, V 생성
        keys    = self.W_key(x)    # (b, num_tokens, d_out) → (2, 6, 768)
        queries = self.W_query(x)  # (b, num_tokens, d_out) → (2, 6, 768)
        values  = self.W_value(x)  # (b, num_tokens, d_out) → (2, 6, 768)

        # Step 2: 헤드 분리 — 마지막 차원을 (num_heads, head_dim)으로 나눈다
        # (b, num_tokens, d_out) → (b, num_tokens, num_heads, head_dim)
        keys    = keys.view(b, num_tokens, self.num_heads, self.head_dim)
        queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)
        values  = values.view(b, num_tokens, self.num_heads, self.head_dim)
        # (2, 6, 768) → (2, 6, 12, 64)

        # Step 3: 차원 전치 — 각 헤드를 독립적인 배치처럼 처리하기 위해
        # (b, num_tokens, num_heads, head_dim) → (b, num_heads, num_tokens, head_dim)
        keys    = keys.transpose(1, 2)     # (2, 12, 6, 64)
        queries = queries.transpose(1, 2)  # (2, 12, 6, 64)
        values  = values.transpose(1, 2)   # (2, 12, 6, 64)

        # Step 4: 어텐션 스코어 계산
        # (b, num_heads, num_tokens, head_dim) @ (b, num_heads, head_dim, num_tokens)
        # → (b, num_heads, num_tokens, num_tokens)
        attn_scores = queries @ keys.transpose(2, 3)  # (2, 12, 6, 6)

        # Step 5: 마스킹 — 미래 토큰에 -∞ 적용
        mask_bool = self.mask.bool()[:num_tokens, :num_tokens]
        attn_scores.masked_fill_(mask_bool, -torch.inf)

        # Step 6: 스케일링 + 소프트맥스 + 드롭아웃
        attn_weights = torch.softmax(
            attn_scores / keys.shape[-1]**0.5, dim=-1
        )  # (2, 12, 6, 6)
        attn_weights = self.dropout(attn_weights)

        # Step 7: 가중합으로 context vector 생성
        # (b, num_heads, num_tokens, num_tokens) @ (b, num_heads, num_tokens, head_dim)
        # → (b, num_heads, num_tokens, head_dim)
        context_vec = (attn_weights @ values).transpose(1, 2)
        # transpose: (2, 12, 6, 64) → (2, 6, 12, 64)

        # Step 8: 헤드 결합 — 분리했던 헤드를 다시 하나로
        context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
        # (2, 6, 12, 64) → (2, 6, 768)

        # Step 9: 최종 프로젝션
        context_vec = self.out_proj(context_vec)  # (2, 6, 768)
        return context_vec</code></pre>
<p>이 코드에서 <code>view</code>와 <code>transpose</code>가 번갈아 등장하는 이유는, 하나의 큰 행렬을 여러 헤드로 <strong>논리적으로</strong> 분리한 뒤 각 헤드를 독립적인 배치 차원처럼 처리하기 위해서다. 물리적으로 별도의 행렬을 만들지 않으면서도, 차원 재배치만으로 병렬 연산을 가능하게 하는 것이다.</p>
<h3 id="34-피드포워드-네트워크--비선형성의-주입">3.4 피드포워드 네트워크 — 비선형성의 주입</h3>
<p>어텐션 연산의 본질은 가중합(Weighted Sum)이다. 선형 연산의 조합은 아무리 쌓아도 여전히 선형이다. 어텐션 레이어를 12개 쌓아봐야, 하나의 거대한 선형 변환과 수학적으로 동치가 될 뿐이다. 복잡한 패턴을 학습하려면 <strong>비선형성</strong>이 필요하고, 그 역할을 피드포워드 네트워크(FFN)가 담당한다.</p>
<p>어텐션이 &quot;단어들을 섞는 과정&quot;이라면, FFN은 &quot;각 단어의 뜻을 더 정교하게 변환하는 과정&quot;이다.</p>
<p>구조는 단순하다. 차원을 4배로 확장하고, GELU 활성함수를 통과시킨 뒤, 다시 원래 차원으로 복귀한다.</p>
<pre><code class="language-python">class GELU(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, x):
        return 0.5 * x * (1 + torch.tanh(
            torch.sqrt(torch.tensor(2.0 / torch.pi)) *
            (x + 0.044715 * torch.pow(x, 3))
        ))

class FeedForward(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(cfg[&quot;emb_dim&quot;], 4 * cfg[&quot;emb_dim&quot;]),  # 768 → 3072
            GELU(),
            nn.Linear(4 * cfg[&quot;emb_dim&quot;], cfg[&quot;emb_dim&quot;]),  # 3072 → 768
        )

    def forward(self, x):
        return self.layers(x)</code></pre>
<p>GPT-2가 ReLU 대신 GELU를 사용하는 이유는, GELU가 0 근방에서 부드러운 곡선을 그려 그래디언트 흐름이 더 안정적이기 때문이다. ReLU의 &quot;죽은 뉴런(Dead Neuron)&quot; 문제를 완화하는 효과도 있다.</p>
<h3 id="35-레이어-정규화와-잔차-연결">3.5 레이어 정규화와 잔차 연결</h3>
<p>트랜스포머 블록에는 두 가지 안전장치가 있다.</p>
<p><strong>잔차 연결(Residual Connection)</strong>: ResNet에서 가져온 개념이다. 입력을 서브 레이어의 출력에 더해준다. 층이 깊어질수록 역전파 과정에서 그래디언트가 소실되는 문제를 방지한다. 수학적으로, 잔차 경로를 통해 그래디언트가 변형 없이 직접 전파될 수 있는 지름길이 생기는 것이다.</p>
<p><strong>레이어 정규화(Layer Normalization)</strong>: 레이어가 깊어지면 값의 분포가 극단적으로 커지거나 작아질 수 있다. 정규화는 평균을 0, 분산을 1로 맞춰서 값의 분포를 안정시킨다.</p>
<p>여기서 BatchNorm이 아닌 LayerNorm을 쓰는 이유가 있다. BatchNorm은 배치 내의 같은 위치에 있는 특징들을 모아서 정규화하는데, 시퀀스 길이가 가변적인 NLP 태스크에서는 적합하지 않다. LayerNorm은 <strong>하나의 샘플 내에서</strong> 정규화를 수행하므로 배치 크기나 시퀀스 길이에 독립적이다.</p>
<pre><code class="language-python">class LayerNorm(nn.Module):
    def __init__(self, emb_dim):
        super().__init__()
        self.eps = 1e-5  # 분모가 0이 되는 것을 방지
        self.scale = nn.Parameter(torch.ones(emb_dim))   # 학습 가능한 스케일
        self.shift = nn.Parameter(torch.zeros(emb_dim))  # 학습 가능한 시프트

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        norm_x = (x - mean) / torch.sqrt(var + self.eps)
        return self.scale * norm_x + self.shift</code></pre>
<p><code>scale</code>과 <code>shift</code>는 학습 가능한 파라미터다. 정규화로 분포를 통일한 뒤, 모델이 필요에 따라 다시 분포를 조정할 수 있도록 자유도를 남겨두는 것이다.</p>
<h3 id="36-트랜스포머-블록--모든-것의-조립">3.6 트랜스포머 블록 — 모든 것의 조립</h3>
<p>지금까지 만든 컴포넌트들을 하나의 블록으로 조립한다. GPT-2의 <strong>Pre-LN</strong> 구조를 반영하여, 정규화가 어텐션과 FFN 연산 <strong>전에</strong> 적용된다.</p>
<pre><code class="language-python">class TransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.att = MultiHeadAttention(
            d_in=cfg[&quot;emb_dim&quot;],
            d_out=cfg[&quot;emb_dim&quot;],
            context_length=cfg[&quot;context_length&quot;],
            num_heads=cfg[&quot;n_heads&quot;],
            dropout=cfg[&quot;drop_rate&quot;],
            qkv_bias=cfg[&quot;qkv_bias&quot;])
        self.ff = FeedForward(cfg)
        self.norm1 = LayerNorm(cfg[&quot;emb_dim&quot;])
        self.norm2 = LayerNorm(cfg[&quot;emb_dim&quot;])
        self.drop_shortcut = nn.Dropout(cfg[&quot;drop_rate&quot;])

    def forward(self, x):
        # ── 멀티헤드 어텐션 ──
        shortcut = x              # 잔차 경로 저장
        x = self.norm1(x)         # Pre-LN: 먼저 정규화
        x = self.att(x)           # 어텐션 연산
        x = self.drop_shortcut(x) # 드롭아웃
        x = x + shortcut          # 잔차 연결

        # ── 피드포워드 ──
        shortcut = x
        x = self.norm2(x)         # Pre-LN: 먼저 정규화
        x = self.ff(x)            # FFN 연산
        x = self.drop_shortcut(x) # 드롭아웃
        x = x + shortcut          # 잔차 연결

        return x
        # 입출력 shape 동일: (b, num_tokens, emb_dim)</code></pre>
<p>입력과 출력의 shape가 동일하다는 점이 핵심이다. 이 덕분에 같은 블록을 N번 반복해서 쌓을 수 있다.</p>
<h3 id="37-gpt-2-모델-전체-조립">3.7 GPT-2 모델 전체 조립</h3>
<p>이제 모든 부품이 준비됐다. GPT-2 124M의 설정값과 함께 전체 모델을 조립한다.</p>
<pre><code class="language-python">GPT_CONFIG_124M = {
    &quot;vocab_size&quot;: 50257,      # BPE 어휘 사전 크기
    &quot;context_length&quot;: 1024,   # 최대 문맥 길이
    &quot;emb_dim&quot;: 768,           # 임베딩 차원
    &quot;n_heads&quot;: 12,            # 어텐션 헤드 수
    &quot;n_layers&quot;: 12,           # 트랜스포머 블록 수
    &quot;drop_rate&quot;: 0.1,         # 드롭아웃 비율
    &quot;qkv_bias&quot;: False         # QKV 편향 미사용
}</code></pre>
<pre><code class="language-python">class GPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg[&quot;vocab_size&quot;], cfg[&quot;emb_dim&quot;])
        self.pos_emb = nn.Embedding(cfg[&quot;context_length&quot;], cfg[&quot;emb_dim&quot;])
        self.drop_emb = nn.Dropout(cfg[&quot;drop_rate&quot;])

        self.trf_blocks = nn.Sequential(
            *[TransformerBlock(cfg) for _ in range(cfg[&quot;n_layers&quot;])]
        )

        self.final_norm = LayerNorm(cfg[&quot;emb_dim&quot;])
        self.out_head = nn.Linear(
            cfg[&quot;emb_dim&quot;], cfg[&quot;vocab_size&quot;], bias=False
        )

    def forward(self, in_idx):
        batch_size, seq_len = in_idx.shape
        # in_idx: (batch_size, seq_len) 예: (2, 4)

        # 토큰 임베딩 + 포지셔널 임베딩
        tok_embeds = self.tok_emb(in_idx)
        # (2, 4) → (2, 4, 768)
        pos_embeds = self.pos_emb(
            torch.arange(seq_len, device=in_idx.device)
        )
        # (4,) → (4, 768)
        x = tok_embeds + pos_embeds  # (2, 4, 768) — 브로드캐스팅
        x = self.drop_emb(x)

        # 트랜스포머 블록 x 12
        x = self.trf_blocks(x)      # (2, 4, 768)

        # 최종 정규화
        x = self.final_norm(x)      # (2, 4, 768)

        # 어휘 사전 크기로 프로젝션 → 각 토큰 위치에서의 다음 단어 확률 분포
        logits = self.out_head(x)    # (2, 4, 50257)
        return logits</code></pre>
<p><code>out_head</code>의 Linear 레이어가 마지막 차원을 768에서 50257로 확장한다. 이 50257차원의 벡터가 곧 어휘 사전의 각 토큰에 대한 로짓(logit)이 되고, 소프트맥스를 거치면 확률 분포가 된다.</p>
<h3 id="38-데이터-로더와-텍스트-생성">3.8 데이터 로더와 텍스트 생성</h3>
<p>GPT는 다음 단어를 예측하는 과정을 반복하면서 문장을 완성해 나간다. 훈련 데이터를 만들기 위해 <strong>슬라이딩 윈도우</strong> 방식을 사용한다. 입력 시퀀스를 오른쪽으로 1칸 shift한 것이 곧 정답 라벨이 된다.</p>
<pre><code>입력(input):  [토큰1, 토큰2, 토큰3, 토큰4]
정답(target): [토큰2, 토큰3, 토큰4, 토큰5]</code></pre><p>이 방식으로 하나의 시퀀스에서 여러 개의 (입력, 정답) 쌍을 효율적으로 생성할 수 있다.</p>
<p>텍스트 생성은 자기회귀(Autoregressive) 방식으로 이루어진다. 현재까지의 시퀀스를 모델에 넣고, 마지막 토큰 위치의 로짓에서 가장 확률이 높은 토큰을 선택해 이어 붙인다. 이 과정을 원하는 길이만큼 반복한다.</p>
<pre><code class="language-python">def generate_text_simple(model, idx, max_new_tokens, context_size):
    for _ in range(max_new_tokens):
        # 문맥 크기를 초과하면 앞부분을 잘라낸다
        idx_cond = idx[:, -context_size:]

        with torch.no_grad():
            logits = model(idx_cond)  # (batch, seq_len, vocab_size)

        # 마지막 토큰 위치의 확률 분포만 사용
        logits = logits[:, -1, :]     # (batch, vocab_size)

        # 가장 높은 확률의 토큰 선택 (greedy decoding)
        idx_next = torch.argmax(logits, dim=-1, keepdim=True)  # (batch, 1)

        # 기존 시퀀스에 이어 붙이기
        idx = torch.cat((idx, idx_next), dim=1)  # (batch, seq_len+1)

    return idx</code></pre>
<p>여기서 <code>idx[:, -context_size:]</code>로 자르는 부분이 중요하다. GPT-2의 context_length는 1024이므로, 생성이 진행되면서 시퀀스가 이 길이를 넘기면 앞쪽 토큰을 버려야 한다. 앱 개발에서 링 버퍼(Ring Buffer)로 고정 크기 로그를 관리하는 것과 비슷한 패턴이다.</p>
<hr>
<h2 id="4-텐서-shape과의-전쟁--엔지니어링-디버깅-노하우">4. 텐서 Shape과의 전쟁 — 엔지니어링 디버깅 노하우</h2>
<h3 id="41-가장-빈번한-에러-차원-불일치">4.1 가장 빈번한 에러: 차원 불일치</h3>
<p>앱 개발에서 가장 많은 시간을 잡아먹었던 건 UI 렌더링 버그가 아니라, 네트워크 레이어에서 터지는 원인 불명의 크래시였다. 콜 스택을 한 프레임씩 올라가면서 어디서 상태가 오염됐는지 추적해야 했다.</p>
<p>딥러닝 구현에서의 디버깅도 본질은 같다. 다만 대상이 메모리 주소 대신 <strong>텐서의 shape</strong>으로 바뀌었을 뿐이다. 가장 자주 마주치는 에러는 이것이다.</p>
<pre><code>RuntimeError: mat1 and mat2 shapes cannot be multiplied (6x768 and 256x768)</code></pre><p>행렬 곱에서 내부 차원이 맞지 않으면 즉시 터진다. 문제는 여러 레이어를 거치면서 어느 시점에 shape이 틀어졌는지 추적하기 어렵다는 것이다. 앱 개발에서 크래시 로그의 콜 스택을 타고 올라가는 것처럼, 각 레이어 사이에 <code>print(tensor.shape)</code>를 심어놓고 한 단계씩 좁혀나가는 수밖에 없다.</p>
<h3 id="42-멀티헤드-어텐션에서의-차원-변환-추적">4.2 멀티헤드 어텐션에서의 차원 변환 추적</h3>
<p>구현 과정에서 가장 shape 에러가 많이 발생한 곳은 멀티헤드 어텐션이다. <code>view</code>와 <code>transpose</code>가 연쇄적으로 등장하면서, 각 단계에서 어떤 차원이 어떤 의미를 갖는지 놓치기 쉽다.</p>
<p>이 변환의 흐름을 한 번 정리하면 이렇다.</p>
<pre><code>Q, K, V 생성 후:
  (b, num_tokens, d_out)                    → (2, 6, 768)

view로 헤드 분리:
  (b, num_tokens, num_heads, head_dim)      → (2, 6, 12, 64)

transpose로 헤드를 배치 차원으로:
  (b, num_heads, num_tokens, head_dim)      → (2, 12, 6, 64)

Q @ K^T (어텐션 스코어):
  (b, num_heads, num_tokens, num_tokens)    → (2, 12, 6, 6)

score @ V (가중합):
  (b, num_heads, num_tokens, head_dim)      → (2, 12, 6, 64)

transpose + contiguous + view (헤드 병합):
  (b, num_tokens, d_out)                    → (2, 6, 768)</code></pre><p>여기서 실수하기 쉬운 지점이 두 가지 있다.</p>
<p><strong>첫째, <code>d_out % num_heads != 0</code>인 경우.</strong> <code>view</code>는 텐서의 원소 수가 정확히 맞아야 reshape이 가능하다. d_out이 num_heads로 나누어 떨어지지 않으면, <code>view(b, num_tokens, num_heads, head_dim)</code>에서 바로 에러가 발생한다. 이것을 방지하기 위해 <code>__init__</code>에서 <code>assert</code> 검증을 넣어둔 것이다.</p>
<p><strong>둘째, <code>contiguous()</code>의 누락.</strong> <code>transpose</code>는 실제 메모리 레이아웃을 바꾸지 않고 스트라이드(stride)만 변경한다. 이 상태에서 <code>view</code>를 호출하면 메모리가 연속적이지 않아 에러가 발생한다. <code>contiguous()</code>는 텐서를 메모리상에서 실제로 재배치하여 <code>view</code>가 동작할 수 있도록 만든다.</p>
<pre><code class="language-python"># 이렇게 하면 에러:
context_vec = (attn_weights @ values).transpose(1, 2).view(b, num_tokens, self.d_out)
# RuntimeError: view size is not compatible with input tensor&#39;s size and stride

# contiguous()를 추가해야 동작:
context_vec = (attn_weights @ values).transpose(1, 2).contiguous().view(b, num_tokens, self.d_out)</code></pre>
<p>앱 개발에서 포인터 연산 후 메모리 정렬(alignment) 문제를 겪었던 경험이 떠올랐다. 레벨은 다르지만, &quot;논리적 구조와 물리적 메모리 배치가 일치하지 않을 때 문제가 생긴다&quot;는 본질은 같다.</p>
<h3 id="43-마스킹-행렬의-크기-매칭">4.3 마스킹 행렬의 크기 매칭</h3>
<p>마스크 행렬은 <code>__init__</code>에서 <code>context_length × context_length</code> 크기로 미리 생성해둔다.</p>
<pre><code class="language-python">self.register_buffer(&quot;mask&quot;, torch.triu(torch.ones(1024, 1024), diagonal=1))</code></pre>
<p>하지만 실제 입력 시퀀스의 길이는 이보다 짧을 수 있다. <code>forward</code>에서 실제 토큰 수에 맞게 슬라이싱해야 한다.</p>
<pre><code class="language-python">mask_bool = self.mask.bool()[:num_tokens, :num_tokens]</code></pre>
<p>이 슬라이싱을 빠뜨리면 마스크의 shape <code>(1024, 1024)</code>과 어텐션 스코어의 shape <code>(num_tokens, num_tokens)</code>이 맞지 않아 브로드캐스팅 에러가 발생한다. 에러 메시지만 보면 원인이 직관적이지 않아서, 처음에는 한참 헤맸던 부분이다.</p>
<hr>
<h2 id="5-돌아보며--블랙박스를-열어본-뒤-달라진-시야">5. 돌아보며 — 블랙박스를 열어본 뒤 달라진 시야</h2>
<h3 id="51-앱-개발자에서-ai-엔지니어로">5.1 앱 개발자에서 AI 엔지니어로</h3>
<p>3년간 앱 개발과 백엔드를 다루며 쌓은 습관들이 이 과정에서 그대로 유효했다. &quot;에러가 나면 콜 스택을 추적한다&quot;, &quot;추상화 레이어를 한 꺼풀 벗겨본다&quot;, &quot;입출력의 스펙을 먼저 확인한다&quot; — 이런 기본적인 디버깅 원칙은 텐서 연산 디버깅에서도 그대로 통한다.</p>
<p>다만 달라진 것이 있다면, 논문의 수식이 코드로 변환되는 과정을 직접 경험했다는 점이다. Attention(Q, K, V) = softmax(QK^T / √d_k)V 라는 수식이, 실제로는 <code>view</code>, <code>transpose</code>, <code>@</code>, <code>masked_fill_</code>, <code>softmax</code>, <code>contiguous</code> 같은 PyTorch 연산의 연쇄로 구현된다. 수식의 간결함 뒤에 숨겨진 구현의 복잡성을 체감한 것이 가장 큰 수확이다.</p>
<h3 id="52-밑바닥-구현이-실무에-주는-가치">5.2 밑바닥 구현이 실무에 주는 가치</h3>
<p>모델의 내부 구조를 이해하고 있으면, 실무에서 마주치는 문제들의 성격이 달라진다.</p>
<ul>
<li><strong>서빙 최적화</strong>: vLLM 같은 추론 프레임워크가 KV 캐시를 활용한다고 할 때, 그 &quot;KV 캐시&quot;가 정확히 어떤 텐서인지 안다. <code>keys.transpose(1, 2)</code> 이후의 <code>(b, num_heads, num_tokens, head_dim)</code> 형태의 텐서가 매 생성 스텝마다 축적되는 것이다. 이걸 알면 메모리 사용량 추정이 가능하다.</li>
<li><strong>양자화(Quantization)</strong>: INT8이나 INT4로 양자화한다고 할 때, <code>W_query</code>, <code>W_key</code>, <code>W_value</code>의 가중치 행렬이 대상이라는 것을 안다. 이 행렬들의 크기가 <code>(d_in, d_out)</code> = <code>(768, 768)</code>이고, 이것이 12개 레이어에 각 3개씩 존재한다는 것을 파악하면, 양자화의 메모리 절감 효과를 구체적으로 계산할 수 있다.</li>
<li><strong>파인튜닝 전략</strong>: LoRA가 어텐션 레이어의 가중치 행렬에 저랭크 행렬을 끼워넣는다고 할 때, 그 &quot;가중치 행렬&quot;이 구체적으로 어디에 위치하는지 코드 레벨에서 짚을 수 있다.</li>
</ul>
<p>단순히 &quot;이 라이브러리를 쓰면 된다&quot;가 아니라, <strong>&quot;왜 이 최적화가 효과적인지&quot;</strong>를 설명할 수 있게 된다. 이것이 블랙박스를 열어본 사람과 그렇지 않은 사람의 차이라고 생각한다.</p>
<hr>
<p><em>텍스트를 토큰으로 쪼개고, 벡터로 바꾸고, 어텐션으로 섞고, 다시 단어로 복원하는 과정. 그 전체를 코드로 직접 타이핑해본 뒤에야, &quot;다음 단어를 예측한다&quot;는 한 문장의 무게가 느껴졌다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] @State와 @Binding 이해하기]]></title>
            <link>https://velog.io/@marha-hwang/SwiftUI-State%EC%99%80-Binding-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@marha-hwang/SwiftUI-State%EC%99%80-Binding-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Feb 2025 12:25:24 GMT</pubDate>
            <description><![CDATA[<h2 id="state-와-binding을-사용하는-이유는-무엇일까">@State 와 @Binding을 사용하는 이유는 무엇일까?</h2>
<ul>
<li>데이터의 변화에 따른 뷰 업데이트를 위해 사용한다.</li>
<li>아래 코드를 보면 BindingView라는 커스텀뷰에서는 Counter라는 인스턴스를 초기화 과정에서 주입받는다
그리고 부모뷰에서 버튼을 통해 counter의 값을 증가 시켰을때 출력값은 정상적으로 증가하지만 뷰는 업데이트 되지 않는 현상이 발생한다.</li>
</ul>
<aside>

<p>부모뷰에서 Counter클래스를 사용한 이유는?</p>
<ul>
<li>struct는 값타입이므로 Immutable하다. 따라서 View구조체도 Immutable하므로,
값을 증가시키는 로직을 @State를 사용하지 않고 구현하기 위해 참조타입인 Counter을 통해 내부의 값을 증가시키는 방법을 사용하였다.</li>
</ul>
</aside>

<pre><code class="language-swift">import Foundation
import SwiftUI

class Counter{
    var count = 1
}

struct StateBinding:View {

    private var counter = Counter()

    var body: some View {

        BindingView(counter: counter)

        Button(&quot;Add Value&quot;){
            counter.count += 1
            print(counter.count)
        }
    }
}

struct BindingView:View {

    private var counter:Counter
    init(counter: Counter) {
        self.counter = counter
    }

    var body: some View {
        Text(String(counter.count))
    }
}
</code></pre>
<h2 id="데이터-변화에-따른-뷰의-업데이트를-어떻게-구현해야-할까">데이터 변화에 따른 뷰의 업데이트를 어떻게 구현해야 할까?</h2>
<ul>
<li>일반적인 변수로는 데이터의 변화에 따른 뷰의 업데이트를 구현할 수 없다. 그렇기 때문에 사용하는 것이 @State와 @Binding이다.</li>
<li>상태 변화에 따라 UI에 반영하기 위한 데이터는 @State로 선언한다.</li>
<li>자식뷰에서 부모뷰의 @State변수를 전달받아 사용하고 싶다면 @Binding 변수를 선언하여 사용한다.
@Binding변수에는 자동으로 이니셜라이저가 구현된다.</li>
</ul>
<pre><code class="language-swift">import Foundation
import SwiftUI

struct StateBinding:View {

    @State private var count = 1

    var body: some View {

        BindingView(count: $count)

        Button(&quot;Add Value&quot;){
            count += 1
            print(count)
        }
    }
}

struct BindingView:View {

    @Binding var count:Int

    var body: some View {
        Text(String(count))
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Combine] sink, assign 의 차이점]]></title>
            <link>https://velog.io/@marha-hwang/Combine-sink-assign-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@marha-hwang/Combine-sink-assign-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Sat, 01 Feb 2025 08:10:24 GMT</pubDate>
            <description><![CDATA[<p>공통점 :</p>
<ul>
<li>sink와 assign모두 값을 구독하고 이를 처리하기 위한 방법이다.</li>
</ul>
<p>차이점 : </p>
<ul>
<li>sink : 값을 받아 처리하는 클로저를 전달받아 해당 값을 통한 로직 수행</li>
<li>assign : 값을 전달받아 특정 객체의 프로퍼티에 할당시킴</li>
</ul>
<h2 id="sink의-정의">sink의 정의</h2>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/8e80a4c4-638a-40fd-8770-d0cf0eb2baa8/image.png" alt=""></p>
<ul>
<li>subscriber을 직접 구현 할 필요없이 해당 publisher의 타입을 준수하는 subscriber을 반환해준다.</li>
<li>AnyCancellable프로토콜을 준수하는 subscriber를 반환하기 때문에 구독 취소를 할 수 있다.</li>
</ul>
<h3 id="sink예제">sink예제</h3>
<pre><code class="language-swift">let myRange = (0...3)
cancellable = myRange.publisher
    .sink(receiveCompletion: { print (&quot;completion: \($0)&quot;) },
          receiveValue: { print (&quot;value: \($0)&quot;) })

// Prints:
//  value: 0
//  value: 1
//  value: 2
//  value: 3
//  completion: finished</code></pre>
<h2 id="assignto의-정의">assign(to:)의 정의</h2>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/25c003c4-51b0-4f84-b888-ab431e0a9bd1/image.png" alt=""></p>
<ul>
<li>publisher로 부터 방출받은 값을 @Published가 붙은 인스턴스에서 재방출하기 위해 사용하는 연산자이다.</li>
<li>@Published가 붙은 인스턴스의 생명주기에 따라 subscription이 관리 되기 때문에 AnyCancellable인스턴스를 반환하지 않는다.</li>
</ul>
<h3 id="assignto예제">assign(to:)예제</h3>
<ul>
<li>Just를 통해 100이란 값을 방출하면 MyModel인스턴스 id에 값을 전달하는 코드</li>
</ul>
<pre><code class="language-swift">    class MyModel: ObservableObject {
        @Published var id: Int = 0
    }
    let model2 = MyModel()
    Just(100).assign(to: &amp;model2.$id)</code></pre>
<h2 id="assigntoon">assign(to:on:)</h2>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/41d2197f-ba01-463c-bc6f-5ce99db8a01f/image.png" alt=""></p>
<ul>
<li>publisher로 부터 방출받은 값을 특정 객체의 프로터티에 할당한다.</li>
<li>keyPath는 on의 객체에서 프로퍼티를 특정시키기 위한 인풋값이다.</li>
<li>AnyCancellable인스턴스를 반환하므로 프로퍼티에 값이 자동할당 되는 것을 중지하고 싶다면 cancell()함수를 호출하면 된다.</li>
</ul>
<h3 id="assigntoon-예제">assign(to:on:) 예제</h3>
<pre><code class="language-swift">class MyClass {
    var anInt: Int = 0 {
        didSet {
            print(&quot;anInt was set to: \(anInt)&quot;, terminator: &quot;; &quot;)
        }
    }
}

var myObject = MyClass()
let myRange = (0...2)
cancellable = myRange.publisher
    .assign(to: \.anInt, on: myObject)

// Prints: &quot;anInt was set to: 0; anInt was set to: 1; anInt was set to: 2&quot;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Combine] Subject에 대해 알아보기]]></title>
            <link>https://velog.io/@marha-hwang/Combine-Subject%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@marha-hwang/Combine-Subject%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 01 Feb 2025 07:59:43 GMT</pubDate>
            <description><![CDATA[<h2 id="subject를-사용하는-이유">Subject를 사용하는 이유</h2>
<p>내가 처음 Combine을 공부할 때 Publisher은 무조건 값을 소유 해야 하고 해당 값이 어떤 이벤트에 의해서 변화 할 때 subscriber에 값을 방출하는 것인 줄 잘못 알고 있었다.</p>
<p>하지만 combine의 값 방출 시점은 값의 변화가 아닌 이벤트 발생 시점과 더 가깝다는 사실을 알게 되었다.</p>
<p>만약 이벤트 발생시점을 외부에서 호출을 통해 제어하고 싶다면 어떻게 해야할까?</p>
<p>이런 경우에 사용 할 수 있는 것이 subject이다.</p>
<h2 id="subject프로토콜의-정의">Subject프로토콜의 정의</h2>
<aside>

<p><strong>A publisher that exposes a method for outside callers to publish elements.</strong>
외부 호출자를 통해 값을 방출시키기 위한 함수를 노출시키는 puslisher이다.</p>
<p><strong>A subject is a publisher that you can use to ”inject” values into a stream, by calling its <a href="https://developer.apple.com/documentation/combine/subject/send(_:)"><code>send(_:)</code></a> method. This can be useful for adapting existing imperative code to the Combine model.</strong>
subject는 send메서드를 호출함으로써  스트림에 값을 삽입할 수 있는 publisher이다. 
이것은 기존 명령형 코드를 combine모델에 적용하는데 유용할 수 있다.</p>
</aside>

<h2 id="subject프로토콜-구현체">Subject프로토콜 구현체</h2>
<ul>
<li><p>PassthroughSubject
하위 subscriber에게 브로트캐스트 방식으로 값을 전파하는 publisher이다.</p>
</li>
<li><p>CurrentValueSubject
내부에 값을 가지고 있고 내부 값이 변화하면 하위 subscriber들에게 값을 전파한다.</p>
</li>
</ul>
<h2 id="두-구현체의-차이는-무엇일까">두 구현체의 차이는 무엇일까?</h2>
<p>PassthroughSubject는 send메서드를 통해 들어온 값은 단순히 방출시키는 역활만 한다면</p>
<p>CurrentValueSubject는 send메서드를 통해 들어온 값을 내부에 저장하고 저장된 값을 방출시키는 과정을 거친다.</p>
<h2 id="구현예제">구현예제</h2>
<ul>
<li><p>출력결과를 보면 두가지 Subject모두 sub1을 생성하고 .finished값을 방출한다.
이때 CurrentValueSubject는 값을 소유하기 때문에 sub1을 생성한 직후 subject에 저장된 초기값을 바로 방출하지만 PassThroughSubject는 completion결과만 출력되는 것을 볼수있다.</p>
</li>
<li><p>PassThroughSubject예제</p>
</li>
</ul>
<pre><code class="language-swift">    func callPassthrough(){
        let passthroughSubject = PassthroughSubject&lt;String, Never&gt;()

        let sub1 = passthroughSubject
            .sink(receiveCompletion: { print(&quot;1 번째 sink completion: \($0)&quot;) },
                  receiveValue: { print(&quot;1 번째 sink value: \($0)&quot;) })

        passthroughSubject.send(completion: .finished)

        let sub2 = passthroughSubject
            .sink(receiveCompletion: { print(&quot;2 번째 sink completion: \($0)&quot;) },
                  receiveValue: { print(&quot;2 번째 sink value: \($0)&quot;) })

        // 현재 Subscriber들에게 모두 보냄
        passthroughSubject.send(&quot;두번째 값&quot;)

    }</code></pre>
<aside>

<p>출력결과</p>
<p>1 번째 sink completion: finished
2 번째 sink completion: finished</p>
</aside>

<ul>
<li>CurrentValueSubject</li>
</ul>
<pre><code class="language-swift">    func callCurrentValue(){
        let currentValueSubject = CurrentValueSubject&lt;String, Never&gt;(&quot;첫번째 값&quot;)

        let sub1 = currentValueSubject
            .sink(receiveCompletion: { print(&quot;1 번째 sink completion: \($0)&quot;) },
                  receiveValue: { print(&quot;1 번째 sink value: \($0)&quot;) })

        currentValueSubject.send(completion: .finished)

        let sub2 = currentValueSubject
            .sink(receiveCompletion: { print(&quot;2 번째 sink completion: \($0)&quot;) },
                  receiveValue: { print(&quot;2 번째 sink value: \($0)&quot;) })

        // 현재 Subscriber들에게 모두 보냄
        currentValueSubject.send(&quot;두번째 값&quot;)

        print(currentValueSubject.value)
    }</code></pre>
<aside>

<p>출력값</p>
<p>1 번째 sink value: 첫번째 값
1 번째 sink completion: finished
2 번째 sink completion: finished
첫번째 값</p>
</aside>]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] ForEach]]></title>
            <link>https://velog.io/@marha-hwang/SwiftUI-ForEach</link>
            <guid>https://velog.io/@marha-hwang/SwiftUI-ForEach</guid>
            <pubDate>Sat, 18 Jan 2025 09:50:21 GMT</pubDate>
            <description><![CDATA[<h2 id="foreach를-사용하는-이유는-무엇일까">ForEach를 사용하는 이유는 무엇일까?</h2>
<p>UIKit같은 경우는 swift문법에서 지원하는 for문을 통해 반복되는 View를 구현할 수 있었다.</p>
<p>하지만 SwiftUI를 통해 View를 구현할 때는 for문을 사용할 수 없기 때문에 ForEach를 사용해야 한다.</p>
<h2 id="swiftui는-왜-for문을-사용할-수-없을까">SwiftUI는 왜 for문을 사용할 수 없을까?</h2>
<p><strong>SwiftUI를 한마디로 정의하면 선언적 UI를 구현하기 위한 프레임워크이다.</strong></p>
<p>자세히 풀어 설명하면 View를 선언하여 생성하면 View는  상태에 따라 알아서 변화해야 한다는 것을 의미한다.</p>
<h2 id="그렇다면-상태에-따라-변화하는-것과-foreach사이에는-무슨-관계가-있을까">그렇다면 상태에 따라 변화하는 것과 ForEach사이에는 무슨 관계가 있을까?</h2>
<p>만약 어떤 View의 상태가 변화한다면 이에 대해 View를 변화시켜야 한다. 그리고 이를 위해서는 SwiftUI의 뷰 시스템에서 상태변화에 따른 처리를 하기 위해 뷰를 고유하게 식별할 수 있는 방법이 필요하다.</p>
<p>정리하자면 ForEach는 View들이 고유 식별자(id)를 가지도록 돕는 역할을 함으로써 상태 변화에 따라 View가 자동으로 변화될 수 있도록 한다.</p>
<h2 id="foreach의-정의">ForEach의 정의</h2>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/bca8426d-c6bb-4e52-8c80-0530e14a15cf/image.png" alt=""></p>
<p><code>RandomAccessCollection</code>: <strong>Swift의 컬렉션 타입</strong> 중 하나로, <strong>인덱스를 통해 임의로 접근 가능</strong>한 컬렉션을 정의하는 프로토콜. </p>
<p><code>Hashable</code> : Swift에서 중요한 프로토콜 중 하나로, <strong>객체를 고유하게 식별</strong>할 수 있는 <strong>해시 값</strong>을 생성할 수 있도록 함. 이 프로토콜은 주로 Set, Dictionary와 같은 컬렉션에서 <strong>객체를 고유하게 식별하고 저장</strong>하기 위해 사용</p>
<h2 id="foreach구현예제">ForEach구현예제</h2>
<ul>
<li>categoryList배열을 사용하여 List의 각 Section을 구성하고, menuList를 사용하여 각 Section내부에서 리스트 요소를 추가하는 예제이다.</li>
<li>forEach를 사용할 때는 데이터를 구분하기 위한 id값이 필요하다. 따라서 Menu구조체 같은 경우 이를 구현하기 위해 Identifiable프로토콜을 준수하였다.</li>
<li>ForEach(menuList[i], id: .id)  에서 .id의 의미는 Menu구조체의 id프로퍼티를 식별자로 사용한다는 것을 의미한다.</li>
</ul>
<pre><code class="language-swift">struct Menu:Identifiable{
    var id:String{ name }
    let name:String
    let price:String
    let description:String
    let imageUrl:String
}

let categoryList = [&quot;인기메뉴&quot;, &quot;양념치킨&quot;, &quot;후라이드 치킨&quot;, &quot;간장치킨&quot;, &quot;파닭&quot;, &quot;스페셜 치킨&quot;]
let menuList:[[Menu]] = [ ... ]

List{
    ForEach(0..&lt;categoryList.count, id: \.self) { i in
        Section(header: Text(categoryList[i])){
            ForEach(menuList[i], id: \.id) { menu in
                MenuCardView(menu: menu, action: nil)
                    .frame(height:100)
            }
        }
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] GeometryReader]]></title>
            <link>https://velog.io/@marha-hwang/SwiftUI-GeometryReader</link>
            <guid>https://velog.io/@marha-hwang/SwiftUI-GeometryReader</guid>
            <pubDate>Thu, 09 Jan 2025 14:49:06 GMT</pubDate>
            <description><![CDATA[<h2 id="어떤상황에서-쓰일까">어떤상황에서 쓰일까?</h2>
<p>SwiftUI는 선언형 UI라고 한다. 따라서 뷰의 위치를 하나하나 잡아줄 필요 없이 뷰를 생성함으로써 부모뷰는 자식뷰에게 위치와 크기를 제안하고 이를 통해 자식뷰는 알아서 제자리를 찾고 크기를 결정한다.</p>
<p>하지만 자식뷰는 부모뷰가 제안한 위치를 기준으로 원하는 위치를 알아서 찾아야 하는 경우가 존재한다.</p>
<p>이때 사용하는 것이 GeometryReader이다.</p>
<h2 id="geometryreader의-정의">GeometryReader의 정의</h2>
<blockquote>
<p><strong>A container view that defines its content as a function of its own size and coordinate space.
콘텐츠를 자체 크기와 좌표 공간의 함수로 정의하는 컨테이너 뷰입니다.</strong></p>
</blockquote>
<p>애플 공식문서의 설명에 따르면 Geometry는 컨테이너뷰이고 내부의 콘텐츠를 자체크기와 좌표공간을 포함하는 클로저 함수를 통해 정의한다고 한다.</p>
<p>여기서 크기와 좌표공간을 제공하기 위해 GeometryProxy라는 구조체를 사용하는데 해당 구조체의 정의는 다음과 같다</p>
<p><a href="https://developer.apple.com/documentation/swiftui/geometryproxy">GeometryProxy | Apple Developer Documentation</a></p>
<blockquote>
<p><strong>A proxy for access to the size and coordinate space (for anchor resolution) of the container view.
컨테이너 뷰(GeometryReader)의 크기와 좌표 공간(앵커 해상도)에 대한 접근을 위한 프록시입니다.</strong></p>
</blockquote>
<h2 id="예제">예제</h2>
<ul>
<li>GeometryReader은 할당가능한 모든 영역을 차지한다. 아래 예제를 보면 최상위 View는 VStack이고 Stack는 기본적으로 자식뷰의 크기만큼의 공간만 차지한다.
하지만 GeometryReader(파랑)가 모든 영역을 차지한 것을 확인 할 수 있다.</li>
</ul>
<pre><code class="language-swift">struct GeometryView: View {

    var body: some View {
        VStack{
            GeometryReader{ proxy in
                Text(&quot;Hello World~~&quot;).background(Color.yellow)
            }
            .background(Color.blue)
        }
        .background(Color.red)
        }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/972c74cd-603d-49c5-b521-d91215d4dace/image.png" alt=""></p>
<h2 id="참조한-것">참조한 것</h2>
<p><a href="https://medium.com/hcleedev/swift-geometryreader%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C-564896c6d6e0">Swift: GeometryReader는 무엇일까?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[디자인패턴] Adapter패턴]]></title>
            <link>https://velog.io/@marha-hwang/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-Adapter%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@marha-hwang/%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-Adapter%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Tue, 27 Jun 2023 01:34:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/marha-hwang/post/e8883dbf-571a-4bd5-bae2-22d5797df56a/image.png" alt="">
클라이언트가 A클래스의 기능을 사용하기 위해서는 인터페이스A를 이용한다.
B클래스 기능을 사용하기 위해서는 인터페이스B를 이용한다.
하지만 인터페이스A를 이용하여 B클래스를 사용할 수는 없다.
따라서 클라이언트가 인터페이스A를 이용하여 B클래스를 사용하기 위해서는 별도의 어댑터 클래스가 필요하고 인터페이스A의 구현 메서드를 B클래스와 매핑시키는 방식으로 구현한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring 클라이언트 데이터 처리]]></title>
            <link>https://velog.io/@marha-hwang/Spring-Spring-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@marha-hwang/Spring-Spring-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 21 Jun 2023 11:17:55 GMT</pubDate>
            <description><![CDATA[<h2 id="url로-들어오는-데이터-처리">URL로 들어오는 데이터 처리</h2>
<h3 id="pathvariable">@PathVariable</h3>
<blockquote>
<p>url경로에 변수를 사용하는 경우에 사용함</p>
</blockquote>
<ul>
<li>코드예시<pre><code>@RequestMapping(&quot;/{name}&quot;)
public String printName(@PathVariable String name, Model model ) {
  model.addAttribute(&quot;name&quot;, name);
  return &quot;home&quot;;
}
</code></pre></li>
</ul>
<pre><code>
### @MatrixVariable 
&gt; “/…/name=gildong;age=19/…/ 과 같이 url에 포함된 다중 피라미터를 사용하는 경우에 사용

- dispatcher-servlet.xml에 아래와 같이 설정 추가</code></pre><annotation-driven enable-matrix-variables="true"/>
```

<ul>
<li>코드예시<pre><code>//url에 다중피라미터를 사용하는 경우 url=”/name=gilding;age=19”
@RequestMapping(&quot;/{user}&quot;)
public String printUser(
                      @MatrixVariable(value=&quot;name&quot;, pathVar=&quot;user&quot;) String name,
                      @MatrixVariable(value=&quot;age&quot;, pathVar=&quot;user&quot;) String age,
                      Model model) {
  System.out.println(name);
  System.out.println(age);
  return &quot;home&quot;;
}
</code></pre></li>
</ul>
<p>// Map과 함께 사용하여 value를 따로 지정하지 않고 한번에 받기 가능
@RequestMapping(&quot;/{user}&quot;)
public String printUser(
                        @MatrixVariable(pathVar=&quot;user&quot;) Map&lt;String,String&gt; param,
                        Model model) {
    System.out.println(param.get(&quot;name&quot;));
    System.out.println(param.get(&quot;age&quot;));
    return &quot;home&quot;;
}</p>
<pre><code>
### @RequestParam 
&gt; “/user?name=gildong” 와 같이 url경로에 “?”로 구분된 피라미터를 처리하기 위해 사용하는 방식

- 코드예시</code></pre><p>//url에 포함된 피라미터를 처리하는 가장 일반적인 방식 url= &quot;/user?name=gildong&quot;
@RequestMapping(&quot;/user&quot;)
public String Home(@RequestParam String name, Model model) {
    System.out.println(name);
    return &quot;home&quot;;
}</p>
<pre><code>### @ModelAttribute 
&gt; DTO객체를 생성하여 url쿼리로 넘어온 피라미터를 생성된 DTO객체에 자동으로 바인딩후 model에 추가함, jsp에서는 생성된 객체를 getter를 이용하여 사용

- 코드예시</code></pre><p>//url피라미터를 model에 DTO객체로 전달 url=&quot;/user?name=gildong&amp;age=19&quot;
@RequestMapping(&quot;/user&quot;)
// @ModelAttribute의 &quot;user&quot;속성은 model에 전달된 객체명임 따라서 jsp에서 사용시 ${user.getName()}과 같이 사용한다
public String Home(@ModelAttribute(&quot;user&quot;) HomeDTO user, Model model) {
        System.out.println(user.getName());
        System.out.println(user.getAge());
        return &quot;home&quot;;
}</p>
<pre><code>
## HTTP body로 들어오는 데이터 처리
&gt; http body데이터의 변환은 HttpMessageConverter을 이용하고, HttpMessageConverter은 변환할 데이터가 json, xml, string인지에 따라 여러 구현된 클래스가 존재한다.
http 헤더의 content-type 설정에 따라 변환을 진행할 HttpMessageConverter종류가 달라진다 이를 통하여 변환될 데이터의 종류를 지정 가능하다
예를들어 body의 내용을 json으로 변환하기 위해서는 content-type를 application/json으로 설정하면 MappingJacksonHttpMessageConverter클래스를 이용하여 json형태로 데이터를 변환한다.

### @RequestBody 
&gt; http request의 body내용을 HttpMessageConverter을 이용하여 자바 객체로 변환
- MappingJacksonHttpMessageConverter을 이용하면 json데이터를 변환가능

### @ResponseBody 
&gt; http response의 body에 HttpMessageConverter을 이용하여 데이터 세팅 
- MappingJacksonHttpMessageConverter을 이용하면 json데이터를 변환가능

- json형태의 데이터를 Map또는 DTO객체로 변환하기 위해 jackson라이브러리 pom.xml에 추가
&gt; MappingJacksonHttpMessageConverter클래스 추가됨
</code></pre><!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<pre><code>    &lt;dependency&gt;
        &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt;
        &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt;
        &lt;version&gt;2.14.2&lt;/version&gt;
    &lt;/dependency&gt;    
    &lt;!-- https://mvnrepository.com/artifact/org.codehaus.jackson/jackson-mapper-asl --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.codehaus.jackson&lt;/groupId&gt;
        &lt;artifactId&gt;jackson-mapper-asl&lt;/artifactId&gt;
        &lt;version&gt;1.9.13&lt;/version&gt;
    &lt;/dependency&gt;</code></pre><pre><code>
- 코드예시</code></pre><p>//http body내용을 객체로 받고 body 내용 세팅하기
@RequestMapping(&quot;/user&quot;)
public @ResponseBody HomeDTO Home(@RequestBody HomeDTO user) {
    HomeDTO dto = new HomeDTO();
    dto.setName(user.getName());
    dto.setAge(user.getAge());
    return dto;
}</p>
<p>```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] 예외처리]]></title>
            <link>https://velog.io/@marha-hwang/JAVA-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@marha-hwang/JAVA-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Thu, 15 Jun 2023 11:12:18 GMT</pubDate>
            <description><![CDATA[<h2 id="예외를-처리하는-두가지-추상-클래스">예외를 처리하는 두가지 추상 클래스</h2>
<ul>
<li>Exception : 컴파일시에 발생하는 에러를 처리할 때 사용</li>
<li>RuntimeException : 런타임시에 발생하는 에러를 처리할 때 사용<blockquote>
<p>Exception을 구현한 경우에는 예외 처리를 try catch 또는 throws를 통해 필수적으로 해야 하고,
RuntimeException을 구현한 경우에는 예외 처리코드를 작성하지 않아도 되지만 이러한 경우 &quot;예외 발생 함수를 호출한 최상위 함수(main함수)&quot;까지 바로 종료되고 예외처리가 진행된다 </p>
<br>
</blockquote>
</li>
</ul>
<h2 id="예외처리-구현방법">예외처리 구현방법</h2>
<h3 id="try-catch구문을-이용한-예외-처리와-함수-옆-throws키워드를-사용한-예외처리">try catch구문을 이용한 예외 처리와 함수 옆 throws키워드를 사용한 예외처리</h3>
<p><strong>차이점 : 예외 처리를 실행 하는 함수가 달라진다.</strong></p>
<blockquote>
<p><strong>try catch : 예외가 발생한 함수에서 예외처리</strong></p>
</blockquote>
<ul>
<li>예외 발생시 cahch문으로 흐름이 넘어가고 해당 함수의 나머지 코드도 모두 실행된다. 따라서 아래 코드에서 divde함수는 int형을 return해야 하기 때문에 int형을 return하지 않으면 에러가 발생한다.</li>
</ul>
<blockquote>
<p><strong>함수옆 throws : 예외가 발생한 함수를 호출한 함수에서 예외 처리</strong> </p>
</blockquote>
<ul>
<li>예외가 발생한 함수를 바로 종료시키고 해당 함수를 호출한 대상에게 예외 처리를 넘긴다. </li>
</ul>
<h2 id="예제-코드">예제 코드</h2>
<blockquote>
<p> 0으로 나누면 DivideException예외처리를 하는 코드</p>
</blockquote>
<ul>
<li>Exception을 구현한 클래스(기본형)<pre><code>public class DivideException extends Exception{
}</code></pre></li>
<li>try catch로 예외처리를 하는 코드</li>
</ul>
<pre><code>public class Main {

    public static void main(String[] args) {
        divide(5,0);
    }

    public static int divide(int a, int b){
        try {
            if(b == 0) {
                throw new DivideException();
            }
        }catch(DivideException e) {
            e.printStackTrace();
            return -1;
        }
        return a/b;
    }
}</code></pre><ul>
<li><p>throws로 예외처리를 하는 코드</p>
<pre><code>public class Main {

  public static void main(String[] args) {
      try {
          divide(5,0);
      } catch (DivideException e) {
          e.printStackTrace();
      }
  }

  public static int divide(int a, int b) throws DivideException{
      if(b == 0) {
          throw new DivideException();
      }

      return a/b;
  }
}</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring 인터셉터]]></title>
            <link>https://velog.io/@marha-hwang/Spring-Spring-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0</link>
            <guid>https://velog.io/@marha-hwang/Spring-Spring-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0</guid>
            <pubDate>Wed, 14 Jun 2023 14:13:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="인터셉터란">인터셉터란?</h3>
<p>디스패처 서블릿과 컨트롤러 사이에 위치하여 컨트롤러로 가는 요청, 컨트롤러에서 오는 응답을 가로채어 특정 작업을 처리한다.
여기서 특정 작업이란 “url요청에 대한 log처리”, “권한체크” 와 같이 컨트롤러에서 중복하여 작성해야 하는 코드를 통해 처리되는 작업을 말한다.</p>
</blockquote>
<h3 id="handlerintercepter인터페이스">HandlerIntercepter인터페이스</h3>
<ul>
<li>인터셉터는 HandlerIntercepter인터페이스를 구현하여 사용한다
아래 3가지 함수중 필요한 함수를 작성한다.</li>
</ul>
<blockquote>
<p><strong>preHandle</strong>(HttpServletRequest request,  HttpServletResponse response, Object handler) 
  실행시점 : 디스패처 서블릿에서 컨트롤러로 요청이 전달되기 이전 실행</p>
</blockquote>
<blockquote>
<p><strong>postHandle</strong>(HttpServletRequest arg0, HttpServletResponse response, Object handler, ModelAndView modelAndView) 
  실행시점 : 컨트롤러를 호출하여 요청을 처리한 이후 실행</p>
</blockquote>
<ul>
<li>view를 생성하기전에 호출되어 model의 데이터를 참조하거나 조작가능</li>
</ul>
<blockquote>
<p><strong>afterCompletion</strong>(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception)
  실행시점 : View생성까지의 모든 작업을 처리한 이후 실행</p>
</blockquote>
<h3 id="인터셉터의-구현단계">인터셉터의 구현단계</h3>
<ol>
<li>HandlerIntercepter를 구현한 클래스를 작성한다.</li>
</ol>
<ul>
<li>아래 예제 코드에서는 각 함수를 호출하여 로그 찍는 과정을 구현했다</li>
</ul>
<pre><code>import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class ExampleIntercepter implements HandlerInterceptor{
    public Logger logger = LoggerFactory.getLogger(ExampleIntercepter.class);


    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception{
        logger.info(&quot;인터셉터 preHandle함수 실행&quot;);
        return true;
    }

    public void postHandle(HttpServletRequest arg0,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception{
        logger.info(&quot;인터셉터 postHandle함수 실행&quot;);
    }

    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception exception) throws Exception{
        logger.info(&quot;인터셉터 afterCompletion함수 실행&quot;);
    }
}</code></pre><ol start="2">
<li>selvlet-context.xml에 &lt; intercepters&gt;태그안에 작성한 인터페이스 클래스를 bean으로 등록한다.</li>
</ol>
<pre><code>&lt;interceptors&gt;
        &lt;beans:bean class=&quot;intercepter.ExampleIntercepter&quot;/&gt;
&lt;/interceptors&gt;</code></pre><h3 id="실행결과">실행결과</h3>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/6d482162-4b8e-4172-be2d-8f315edaa63a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring과 Log]]></title>
            <link>https://velog.io/@marha-hwang/Spring-Spring%EA%B3%BC-Log</link>
            <guid>https://velog.io/@marha-hwang/Spring-Spring%EA%B3%BC-Log</guid>
            <pubDate>Wed, 14 Jun 2023 13:59:09 GMT</pubDate>
            <description><![CDATA[<h3 id="systemoutprint-대신-log를-사용하는-이유는-무엇일까">System.out.print() 대신 Log를 사용하는 이유는 무엇일까?</h3>
<ul>
<li>System.out.print()에 비해 오버헤드가 적다.</li>
<li>시간, 로그레벨, 발생한 클래스등 더 자세한 정보를 알 수 있다.</li>
<li>로그 내용을 파일에 남기기 간편하다.</li>
<li>원하는 레벨의 로그만 확인가능하다.<br>

</li>
</ul>
<h3 id="로그를-구성하는-요소">로그를 구성하는 요소</h3>
<ul>
<li>logger클래스 : 로그 출력여부를 설정된 로그 레벨에 따라 결정하고 appender에 로그 정보를 전달</li>
<li>appender클래스 : 로그정보를 출력할 위치를 결정하는 클래스(파일, 콘솔, DB등등)</li>
<li>layout클래스 : 로그정보의 출력 형식을 결정(HTML형식, XML형식, 사용자 정의 형식 등등)<br>

</li>
</ul>
<h3 id="로그-레벨아래로-갈수록-로그의-레벨이-높음">로그 레벨(아래로 갈수록 로그의 레벨이 높음)</h3>
<p>  TRACE : 가장 하위레벨의 로그
DEBUG : 디버그 용도로 사용
INFO : 런타임시에 발생하는 동작을 나타낼 때 사용
WARN : 오류가 발생할 가능성이 존재하는 경우 사용
ERROR : 오류가 발생한 경우 사용
FATAL : 프로그램이 강제로 종료될 만한 오류인 경우 사용
<br></p>
<blockquote>
<p>logger의 계층구조 </p>
</blockquote>
<ul>
<li>logger는 계층구조를 가지고 모든 logger의 최상위 logger는 root logger이다. logger가 따로 지정되지 않은 경우 root logger에서 설정된 방식으로 로그를 출력한다</li>
</ul>
<h3 id="spring에서-log4j-적용">spring에서 log4j 적용</h3>
<ol start="0">
<li>pom.xml에 log4j의존성 설정이 되었는지 확인</li>
<li>src/main/resources 폴더 아래 log4j.xml 파일 생성<ol start="2">
<li>&lt; appender&gt;태그안에 &lt; layout&gt;태그를 작성하여 로그를 출력할 위치와 방식을 설정한다</li>
<li>&lt; logger&gt;태그에 logger를 추가할 패키지와 출력할 로그레벨을 설정한다</li>
<li>&lt; root&gt;태그를 통해 root logger을 설정한다.</li>
</ol>
</li>
</ol>
<blockquote>
<p>로그가 설정된 대로 출력되지 않았던 이유</p>
</blockquote>
<ul>
<li>Src/test/resource에 같은 같은 이름의 log4j.xml파일이 존재 했는데 해당 파일에서 로그레벨을 수정하니 정상적으로 출력했다. 따라서 Src/main/resource의 log4j.xml의 설정을 사용하기 위해 src/test/resource의 파일을 삭제했다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring 프로젝트 생성]]></title>
            <link>https://velog.io/@marha-hwang/Spring-Spring-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@marha-hwang/Spring-Spring-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Wed, 07 Jun 2023 11:32:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>문제 : STS 플러그인 설치 오류 발생 – 기존에 사용하던 이클립스에서 마켓플레이스를 통해 STS를 설치하려고 하였으나 플러그인이 기존 이클립스의 버전을 지원하지 않아 설치 오류 발생</p>
</blockquote>
<blockquote>
<p>해결 : STS설치또는 이클립스 버전 변경후 플러그인 설치(STS는 이클립스 기반의 스프링에 최적화된 IDE)
(스프링 홈페이지에서 STS3다운 최신 버전인 STS4는 legacy project생성이 불가함)
STS3다운로드 링크 - <a href="https://github.com/spring-attic/toolsuite-distribution/wiki/Spring-Tool-Suite-3">https://github.com/spring-attic/toolsuite-distribution/wiki/Spring-Tool-Suite-3</a> //자바 11이상</p>
</blockquote>
<blockquote>
<p>다음부터 주의 할 점 :  똑같이 했는데도 실패하면 버전을 의심해야 하고 그래도 안되면 해당 기능을 더 이상 지원하지 않기 때문에 오류가 뜨는 것인지 확인해볼 필요가 존재</p>
</blockquote>
<ul>
<li>ex)이클립스에 sts plug-in버전이 일치하지 않아 생긴 오류, spring버전과 tomcat버전이 일치하지 않아 생긴 오류</li>
</ul>
<h4 id="이번-포스팅에서는-spring-legacy-project를-생성하는-법을-정리하고자-함-sts3를-통해-간단하게-생성-가능하지만-spring프로젝트의-구조를-이해하고자-dynamic-web-project를-통해-spring프로젝트를-생성-해-봄">이번 포스팅에서는 spring legacy project를 생성하는 법을 정리하고자 함 STS3를 통해 간단하게 생성 가능하지만 spring프로젝트의 구조를 이해하고자 Dynamic Web Project를 통해 spring프로젝트를 생성 해 봄</h4>
<h3 id="spring-project-생성과정">Spring project 생성과정</h3>
<ol>
<li>다이나믹 웹프로젝트 생성</li>
<li>메이븐 프로젝트로 변환</li>
<li>pom.xml설정 : <project>태그 안에 dependencies작성 spring-web, spring-webmvc를 기본적으로 설정(mvnrepository에서 복사해오기)</li>
<li>pom.xml 저장 후 update project</li>
<li>web.xml 설정 : 1. ApplicationContext설정 2. 리스너 등록 3. 디스패처 서블릿 생성 및 매핑 4. UTF-8필터 설정</li>
<li>WEB-INF폴더아래 spring폴더를 새로 만들고 root-context.xml, servlet-context.xml 생성</li>
<li>실행 테스트를 위해 src폴더아래 com.spring.test패키지 생성 후 TestController, TestService클래스 작성</li>
<li>WEB-INF폴더 아래 views폴더 생성 후 hello.jsp파일 작성</li>
</ol>
<h3 id="참고사항">참고사항</h3>
<blockquote>
<ul>
<li>xml 문서의 태그 구조를 정의하기 위해서는 xsd파일이 필요한데 servlet-context.xml에서는 beans태그를 사용하고 beans태그의 xsd파일을 가져오기 위해 “xmlns:xsi”, “xsi:schemaLocation”을 사용한다  </li>
</ul>
</blockquote>
<ul>
<li>한번에 여러 태그를 사용하는 경우 중복되는 태그가 존재 할 가능성이 있는데 이런 경우를 해결하기 위해 “xmlns:context”와 같이 태그에 접두사를 생성한 후 “<a href="context:component-scan">context:component-scan</a>”와 같은 방식으로 사용한다</li>
</ul>
<p>Xml스키마 관련 참조 - <a href="https://linuxism.ustd.ip.or.kr/911">https://linuxism.ustd.ip.or.kr/911</a></p>
<h3 id="코드">코드</h3>
<pre><code>===============================================================
web.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;web-app xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot; xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd&quot; id=&quot;WebApp_ID&quot; version=&quot;4.0&quot;&gt;

    &lt;!-- ApplicationContext설정 --&gt;
    &lt;context-param&gt;
        &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
        &lt;param-value&gt;/WEB-INF/spring/root-context.xml&lt;/param-value&gt;
    &lt;/context-param&gt;

    &lt;!-- 리스너 등록 --&gt;
    &lt;listener&gt;
        &lt;listener-class&gt;org.springframework.web.context.ContextLoaderListener&lt;/listener-class&gt;
    &lt;/listener&gt;

    &lt;!-- 디스패처 서블릿 생성 및 매핑 --&gt;
    &lt;servlet&gt;
        &lt;servlet-name&gt;appServlet&lt;/servlet-name&gt;
        &lt;servlet-class&gt;org.springframework.web.servlet.DispatcherServlet&lt;/servlet-class&gt;
        &lt;init-param&gt;
            &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
            &lt;param-value&gt;/WEB-INF/spring/appServlet/servlet-context.xml&lt;/param-value&gt;
        &lt;/init-param&gt;
        &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;
    &lt;/servlet&gt;

    &lt;servlet-mapping&gt;
        &lt;servlet-name&gt;appServlet&lt;/servlet-name&gt;
        &lt;url-pattern&gt;/&lt;/url-pattern&gt;
    &lt;/servlet-mapping&gt;

    &lt;!-- UTF-8필터 설정 --&gt;
    &lt;filter&gt;
        &lt;filter-name&gt;encodingFilter&lt;/filter-name&gt;
        &lt;filter-class&gt;org.springframework.web.filter.CharacterEncodingFilter&lt;/filter-class&gt;
        &lt;init-param&gt;
            &lt;param-name&gt;encoding&lt;/param-name&gt;
            &lt;param-value&gt;UTF-8&lt;/param-value&gt;
        &lt;/init-param&gt;
        &lt;init-param&gt;
            &lt;param-name&gt;forceEncoding&lt;/param-name&gt;
            &lt;param-value&gt;true&lt;/param-value&gt;
        &lt;/init-param&gt;
    &lt;/filter&gt;

    &lt;filter-mapping&gt;
        &lt;filter-name&gt;encodingFilter&lt;/filter-name&gt;
        &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
    &lt;/filter-mapping&gt;

&lt;/web-app&gt;
===============================================================
root-context.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
    xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;
&lt;/beans&gt;
===============================================================
servlet-context.xml

&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
    xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
    xmlns:mvc=&quot;http://www.springframework.org/schema/mvc&quot;
    xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                        http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd&quot;&gt;

    &lt;!-- 어노테이션 활성화 --&gt;
    &lt;mvc:annotation-driven&gt;&lt;/mvc:annotation-driven&gt;

    &lt;!-- view 경로 설정 --&gt;
    &lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;&gt;
        &lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/views/&quot; /&gt;
        &lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot; /&gt;
    &lt;/bean&gt;

    &lt;!-- java공통패키지 스캔 --&gt;
    &lt;context:component-scan base-package=&quot;com.spring.test&quot;/&gt;
&lt;/beans&gt;
===============================================================
TestController.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class TestController {

    @Autowired
    private TestService testservice;

    @RequestMapping(&quot;/hello&quot;)
    public String sysHello(Model model) {
        model.addAttribute(&quot;value&quot;, testservice.sayHello());
        return &quot;hello&quot;;
    }
}
===============================================================
TestService.java

import org.springframework.stereotype.Service;

@Service
public class TestService {

    public String sayHello() {
        return &quot;Spring_MVC&quot;;
    }
}
===============================================================

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] mybatis사용법]]></title>
            <link>https://velog.io/@marha-hwang/Spring-mybatis%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@marha-hwang/Spring-mybatis%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Wed, 07 Jun 2023 06:39:45 GMT</pubDate>
            <description><![CDATA[<h4 id="스프링에서-db를-연결하기-위해-다음과-같은-방법-사용">스프링에서 DB를 연결하기 위해 다음과 같은 방법 사용</h4>
<ol>
<li>SqlMapper/Mybatis</li>
</ol>
<ul>
<li>sql문과 자바코드를 설정 파일로 분리하여 코드의 수정이 일어나지 않게 함</li>
</ul>
<ol start="2">
<li>JPA/Hibernate </li>
</ol>
<ul>
<li>DB테이블을 객체로 구성하여 sql문 작성없이 DB쿼리 가능</li>
</ul>
<blockquote>
<p>이번 포스팅에서는 spring에서 mybatis를 사용하여 mysql과 연결하는 과정을 작성한다.</p>
</blockquote>
<blockquote>
<p>Spring에서 파일을 읽어오지 못하는 경우</p>
</blockquote>
<ul>
<li>Spring에서 Resources파일(ex: Java, xml, jsp등등)을 찾기 위해서는 해당 파일이 속하는 경로가 등록 되어 있어야 함</li>
<li>등록 방법 : Build Path -&gt; configure Build Path -&gt; 추가를 원하는 파일 경로 add</li>
<li>확인 : Java Resources에 추가한 파일 경로가 추가되어 있어야 함</li>
</ul>
<h3 id="1-pomxml에-모듈-추가">1. pom.xml에 모듈 추가</h3>
<ul>
<li><p>mybatis-spring : SqlSessionFactory, SqlSession 빈을 사용하기 위한 모듈</p>
<ul>
<li>mysql-connector-java : mysql Driver를 사용하기 위한 모듈<blockquote>
<p>해당 모듈 버전과 mysql버전을 일치 시켜야 함</p>
</blockquote>
</li>
<li>spring-jdbc : DriverManagerDataSource를 사용하기 위한 모듈<pre><code></code></pre></li>
</ul>
<!-- DB연결 -->
 <dependency> 
     <groupId>org.mybatis</groupId> 
     <artifactId>mybatis-spring</artifactId> 
     <version>1.2.4</version> 
 </dependency> 

 <dependency> 
     <groupId>org.springframework</groupId> 
     <artifactId>spring-jdbc</artifactId> 
     <version>5.3.22</version> 
 </dependency> 

 <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <version>8.0.30</version>
 </dependency>
```
### 2. root-context.xml설정
-  dataSource, SqlSessionFactory, SqlSession 빈 등록
- component-scan을 통해 dao에 SqlSession주입
>  driverClassName : "com.mysql.jdbc.Driver"는 이전 버전에서 사용하던 드라이버명 최신 버전은 "com.mysql.cj.jdbc.Driver"와 같이 사용함
url :  “& amp;useSSL=false”를 사용하여야 SSL인증 오류를 막을 수 있음
```
 <context:component-scan base-package="com.spring.test"/>

 <!-- mysql접속 정보객체 -->
 <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
     <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
     <property name="url" value="jdbc:mysql://localhost:3306/test_db?serverTimezone=Asia/Seoul&amp;useSSL=false"></property> 
     <property name="username" value="root"/>
     <property name="password" value="1111"/>
 </bean>

 <!-- mysql세션생성 객체 -->
 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
     <property name="dataSource" ref="dataSource"/>
     <property name="configLocation" value="WEB-INF/resources/mybatis-config.xml" /> <!-- mybatis 설정파일 등록 -->
 </bean>

 <!-- mysql세션 객체 -->
 <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
     <constructor-arg ref="sqlSessionFactory" />
 </bean>
```

</li>
</ul>
<h3 id="3-mybatis-configxml-설정">3. mybatis-config.xml 설정</h3>
<ul>
<li><p>typeAlias태그를 통해 사용할 DTO에 alias이름을 붙임</p>
</li>
<li><p>mapper태그를 통해 sql문이 작성된 xml파일을 등록함</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;!DOCTYPE configuration
PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;
&quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&gt;
&lt;configuration&gt;
&lt;typeAliases&gt;
    &lt;typeAlias type=&quot;com.spring.test.TestDTO&quot; alias=&quot;user&quot;/&gt; &lt;!-- DTO객체에 alias이름 설정 --&gt;
&lt;/typeAliases&gt;

&lt;mappers&gt;
    &lt;mapper resource=&quot;WEB-INF/resources/mapper/testmapper.xml&quot; /&gt; &lt;!-- User관련 쿼리 파일 등록--&gt;
&lt;/mappers&gt;
&lt;/configuration&gt;</code></pre><h3 id="4-mapperxml설정---sql문-작성-파일">4. mapper.xml설정 - sql문 작성 파일</h3>
<ul>
<li>Namespace를 이용하여 해당 mapper파일에 이름을 붙임</li>
<li>select태그안에 sql문을 작성하고 해당 sql문을 지칭 할 id와 결과를 받을 Type을 설정<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;!DOCTYPE mapper
PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
&quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;
&lt;mapper namespace=&quot;UserMapper&quot;&gt; &lt;!-- Repository와 연동을 위해 사용될 namespace --&gt;
</code></pre></li>
</ul>
<select id="SelectUser" resultType="user"> 
    SELECT *
    FROM user
</select>
</mapper>
```
### 5. dao에서 SqlSession객체를 사용한 db접근
> mapper.xml에 지정된 namespace와 sql문의 id를 통해 쿼리를 수행한다 

</li>
</ul>
<pre><code>import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class TestDAO {

    @Autowired
    SqlSession session;

    public List&lt;TestDTO&gt; selectUser(){
        return session.selectList(&quot;UserMapper.SelectUser&quot;);
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] 어노테이션]]></title>
            <link>https://velog.io/@marha-hwang/JAVA-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@marha-hwang/JAVA-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98</guid>
            <pubDate>Tue, 30 May 2023 10:14:55 GMT</pubDate>
            <description><![CDATA[<h3 id="어노테이션이란">어노테이션이란?</h3>
<ul>
<li>프로그램에게 코드에 대한 정보를 전달하기 위한 메타데이터</li>
<li>사람에게는 주석을 이용하여 정보를 전달한다면, 프로그램에게는 어노테이션을 이용하여 정보를 전달한다</li>
<li>컴파일 또는 런타임시 코드를 어떻게 컴파일하고 처리 할 것인지 알려주는 정보</li>
</ul>
<h3 id="어노테이션의-사용이유">어노테이션의 사용이유?</h3>
<blockquote>
<p><strong>어노테이션이 아래 기능을 수행하는 것이 아닌 어노테이션과 리플렉션을 이용하여 수행</strong></p>
</blockquote>
<ul>
<li>컴파일시에 코드문법 체크 ex) @override</li>
<li>빌드시 코드를 자동으로 생성 ex) lombok라이브러리의 @getter, @setter</li>
<li>런타임시에 특정기능 수행 ex) Spring 프레임워크의 @Component, @Controller등등</li>
</ul>
<h3 id="리플렉션이란">리플렉션이란?</h3>
<blockquote>
<p>클래스의 구체적인 타입을 알지 못해도 그 클래스의 정보(타입, 메소드, 변수, 어노테이션 등)에 접근할 수 있도록 해주는 API</p>
</blockquote>
<ul>
<li>자바의 Class클래스를 이용하여 리플렉션 기능을 이용가능</li>
<li>예를들어 Spring에서 @Component가 붙은 클래스를 Bean으로 등록 하는 경우 어느 클래스에 @Component 어노테이션이 붙어 있는지 알지 못한다. 따라서 모든 클래스를 리플렉션을 이용하여 Scan후에 해당 어노테이션이 붙은 클래스만 bean으로 등록 가능하다</li>
</ul>
<h3 id="spring의-component-구현">Spring의 @Component 구현</h3>
<ul>
<li>@Component가 붙은 클래스를 bean으로 등록 해야한다. 하지만 어느 클래스에 @Component가 붙어 있는지는 바로 알 수 없다. 따라서 리플렉션(Class객체)을 이용하여 모든 클래스를 scan(for문과 if문)하면서 @Component가  붙은 클래스에 한해 bean객체를 생성하고 컨테이너에 저장한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] Hash자료구조]]></title>
            <link>https://velog.io/@marha-hwang/JAVA-Hash%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@marha-hwang/JAVA-Hash%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Tue, 30 May 2023 06:36:44 GMT</pubDate>
            <description><![CDATA[<h3 id="hash자료구조-정리">hash자료구조 정리</h3>
<ul>
<li>Hash자료구조를 생성하면 내부적으로 배열이 생긴다</li>
<li>객체의 hashcode( )함수를 이용하여 객체의 해쉬값을 구한다.</li>
<li>객체의 해쉬값을 이용하여 인덱스를 구하고 배열에 객체를 저장한다
ex)해쉬값이 1235이면 1235%16 = 해당 인덱스</li>
<li>hashMap은 key의 해쉬값을 구해 알맞은 인덱스에 value를 저장하고, 검색시에는 key의 해쉬값을 구해 value가 저장된 인덱스를 바로 구한다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[완전탐색]오목 (백준2615)]]></title>
            <link>https://velog.io/@marha-hwang/%EC%99%84%EC%A0%84%ED%83%90%EC%83%89%EC%98%A4%EB%AA%A9-%EB%B0%B1%EC%A4%802615</link>
            <guid>https://velog.io/@marha-hwang/%EC%99%84%EC%A0%84%ED%83%90%EC%83%89%EC%98%A4%EB%AA%A9-%EB%B0%B1%EC%A4%802615</guid>
            <pubDate>Tue, 30 May 2023 06:27:45 GMT</pubDate>
            <description><![CDATA[<h3 id="문제요약">문제요약</h3>
<ul>
<li>흑돌과 백돌중 누가 이겼는지 판별하고 이긴 돌의 맨 왼쪽과 위의 좌표를 출력하는 문제</li>
</ul>
<h3 id="풀이-방법">풀이 방법</h3>
<ul>
<li>모든 위치를 순차적으로 탐색해 나가면서 승부가 결정 났는지 완전탐색으로 판별해나감</li>
</ul>
<h3 id="어려웠던-점">어려웠던 점</h3>
<ul>
<li>처음에는 완전탐색으로 풀릴 만한 간단한 문제인줄 알고 풀었지만 생각보다 처리해야 하는 경우의 수가 많아서 반례를 찾기 어려웠다</li>
<li>비효율적인 flag 사용으로 코드의 복잡도를 증가 시켰다<blockquote>
<p>flag의잘못된 사용
어느 상태를 만족시킬때 flag값을 변화시킴으로써 기록할때 사용해야 하는데 &quot;오목&quot;문제에서 나는 &#39;flag값이 변화하지 않았으면 어떤 상태를 만족시킨다&#39;라는 방식으로 사용하였다. 이때문에 flag값을 변화시키는 5목을 만족하지 않는 모든 경우에 대한 처리를 해주어야 했고 여기서 논리적인 빈틈이 생겨 문제를 풀기 어려웠었다</p>
</blockquote>
</li>
</ul>
<h3 id="풀이코드">풀이코드</h3>
<pre><code>/*
 * 문제 : 오목을 누가 이겼는지 판별하기 -&gt; 상,하,좌,우,대각선에서 5개가 연속으로 나타나는 경우 찾기
 * 생각해볼 것 : 
 * 반례 : 6목 확인을 위해 이전돌을 확인하는 과정에서 잘못된 if문과 flag값 세팅으로 반례발생
 * 
 * 문제풀이 : 완전탐색
 * 무엇을 탐색해야 하나? 모든 돌을 차례대로 탐색해나가야 한다.
 * 어떻게 탐색하나? 이긴돌의 가장 왼쪽 위의 돌을 출력 해야 하므로 가장 왼쪽부터 아래방향으로 탐색을 진행한다.
 * 탐색 해야 할 경우는? 모든 돌에 대하여 탐색
 * 각 돌을 탐색하는 방법은? 현재돌의 오른쪽, 아래, 대각선 탐색
 *  현재돌을 포함해 5번째 까지는 같은 돌이어야함, 6번째는 같지 않아야함
 *  또한 현재돌의 이전돌이 현재돌과 같지 않아야함
 * 
 */

 import java.util.*;
 import java.io.*;
public class Main {
    static int[][] arr;
    static int[][] Case = {{0,1}, {1,0}, {1,1}, {1,-1}};
    public static void main(String[] args) throws Exception{
        //입력받기
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        arr = new int[19][19];
        for(int i = 0; i&lt;19; i++){
            st = new StringTokenizer(in.readLine());
            for(int j=0; j&lt;19; j++){
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        //반복문 : 모든 돌에 대한 탐색
        boolean result = false;
        Loop1:
        for(int i=0; i&lt;19; i++){
            for(int j=0; j&lt;19; j++){
                //만약 현재 놓여진 돌이 없거나 승부가 났다면 break
                if(arr[j][i] == 0) continue;

                //결과 출력
                result = check(i, j);
                if(result){
                    System.out.println(arr[j][i]);
                    System.out.println(++j + &quot; &quot; + ++i);
                    break Loop1;
                }
            }
         }
         if(!result) System.out.print(0);

    }
     // 현재돌이 조건을 만족하는지 확인
    public static boolean check(int x, int y){
        boolean flag = false;

        //반복문 : 3가지 경우에 대한 탐색 - 가로, 세로, 대각선
        Loop1:
        for(int i = 0; i&lt;4; i++){
            int a = x-Case[i][0];
            int b = y-Case[i][1];
            //육목 확인을 위해 이전돌 확인
            if(a&gt;=0 &amp;&amp; b&gt;=0 &amp;&amp; a&lt;19 &amp;&amp; b&lt;19 &amp;&amp; arr[b][a] == arr[y][x]) {
                continue; 
            }
            //반복문 : 6번째 돌까지 확인
            int count = 5;
            for(int j=1; j&lt;6; j++){
                //확인 할 돌의 인덱스 구하기
                int nowX = x + j*Case[i][0];
                int nowY = y + j*Case[i][1];

                //인덱스를 초과하는 경우
                if(nowX&gt;=19 || nowY&gt;=19 || nowX&lt;0 || nowY&lt;0) {
                    if(j==5) break;
                    count-=1;
                    break;
                }
                //if : 5번째 미만의 돌이고, 다른 돌이면
                if(j &lt; 5 &amp;&amp; arr[y][x] != arr[nowY][nowX]) count-=1;
                //else if : 5번째 이상의 돌이고, 같은 돌이면
                else if(j==5 &amp;&amp; arr[y][x] == arr[nowY][nowX]) count-=1;
            }
            //조건을 만족하는 경우 종료
            if(count == 5){
                flag = true;
                break Loop1; 
            } 
        }
        return flag;

    }
}


</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] IP주소체계]]></title>
            <link>https://velog.io/@marha-hwang/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP%EC%A3%BC%EC%86%8C%EC%B2%B4%EA%B3%84</link>
            <guid>https://velog.io/@marha-hwang/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP%EC%A3%BC%EC%86%8C%EC%B2%B4%EA%B3%84</guid>
            <pubDate>Mon, 22 May 2023 05:18:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번 포스팅에서는 IPv4를 기준으로 IP주소체계를 정리한다
참고 - <a href="https://better-together.tistory.com/118">https://better-together.tistory.com/118</a></p>
</blockquote>
<h2 id="ip주소의-구성방식">IP주소의 구성방식</h2>
<ul>
<li>IP주소는 32비트로 나타내어지고 8비트씩 4부분으로 구분되어진다.</li>
<li>네트워크부와 호스트부로 구분되어진다.<blockquote>
<p>네트워크부와 호스트부 : 인터넷상에서 상대 host와 동일한 네트워크상에 위치하면 라우터 없이 호스트부를 이용해서 찾아갈 수 있지만 다른 네트워크상에 위치하면 라우터에서 네트워크부의 주소를 통하여 다른 네트워크로 라우팅 시켜야만 찾아갈 수 있다.</p>
</blockquote>
</li>
</ul>
<h2 id="ip주소의-클래스">IP주소의 클래스</h2>
<ul>
<li>IP주소에서 네트워크부를 구성하는 비트가 몇 비트인지에 따라 클래스가 구분된다</li>
<li>네트워크부를 구성하는 비트수가 적은 경우(ex A클래스) 그만큼 호스트부의 비트수도 많아지기 때문에 할당 가능한 호스트의 수도 많아진다.<blockquote>
<p>A클래스 : 처음 8비트 (첫 1비트는 클래스 식별비트)
B클래스 : 처음 16비트(첫 2비트는 클래스 식별비트)
C클래스 : 처음 24비트(첫 2비트는 클래스 식별비트)</p>
</blockquote>
</li>
</ul>
<h2 id="서브넷팅">서브넷팅</h2>
<ul>
<li>어느 네트워크가 클래스C방식을 사용하는 경우 256개의 IP를 생성 가능하지만, 
만약 56개의 host만 필요하다면 200개의 IP주소가 낭비되어지게 된다. 
따라서 이를 효율적으로 사용하기 위해 좀 더 작은 네트워크로 분할 하는 방식을 서브넷팅이라고 한다.</li>
</ul>
<h3 id="서브넷-분할-방법">서브넷 분할 방법</h3>
<blockquote>
<p>첫24비트가 네트워크부, 나머지 8비트가 호스트부인 C클래스의 경우</p>
</blockquote>
<ul>
<li>호스트부의 2비트를 서브넷부(네트워크부)로 할당하게 된다면 4개의 서로다른 네트워크를 가질 수 있는 서브넷이 생성가능하다.</li>
<li>이제 각 서브넷에서 호스트부의 길이는 6비트이기 때문에 각 서브넷마다 64(2^6)의 IP주소를 사용 가능하다.</li>
</ul>
<h2 id="서브넷-마스크">서브넷 마스크</h2>
<blockquote>
<p>서브넷을 사용하여 네트워크를 구성하는 경우 네트워크부와 호스트부를 구분하는 새로운 방식이 필요한데 이를 서브넷 마스크라고 한다</p>
</blockquote>
<p>서브넷 마스크는 32비트로 이루어져 있고, 각 비트는 IP 32비트에 대응한다</p>
<ul>
<li>IP주소의 네트워크부에 해당하는 서브넷 마스크 비트는 1로 설정</li>
<li>IP주소의 호스트부에 해당하는 서브넷 마스크 비트는 0으로 설정</li>
</ul>
<blockquote>
<p>서브넷 마스크 표기법</p>
</blockquote>
<ul>
<li>네트워크부의 비트가 24개인 경우</li>
</ul>
<p>11111111.11111111.11111111.00000000 = 255.255.255.0
십진수 표기법 - xxx.xxx.xxx.xxx /24
<br></p>
<ul>
<li>네트워크부의 비트가 26개인 경우</li>
</ul>
<p>11111111.11111111.11111111.11000000 = 255.255.255.192
십진수 표기법 - xxx.xxx.xxx.xxx /26</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[완전탐색] 꽃길(백준14620)]]></title>
            <link>https://velog.io/@marha-hwang/%EC%99%84%EC%A0%84%ED%83%90%EC%83%89-%EA%BD%83%EA%B8%B8%EB%B0%B1%EC%A4%8014620</link>
            <guid>https://velog.io/@marha-hwang/%EC%99%84%EC%A0%84%ED%83%90%EC%83%89-%EA%BD%83%EA%B8%B8%EB%B0%B1%EC%A4%8014620</guid>
            <pubDate>Fri, 19 May 2023 01:18:57 GMT</pubDate>
            <description><![CDATA[<h2 id="문제요약">문제요약</h2>
<ul>
<li>n*n크기의 마당에 3개의 꽃을 심는 최소비용 찾기, 단 마당을 벗어나거나 꽃이 겹치면 안됨</li>
</ul>
<h2 id="풀이-과정">풀이 과정</h2>
<p>완전탐색을 이용하여 풀이 </p>
<ul>
<li><p>무엇을 탐색해야 함? 꽃을 배치하는 모든 경우찾기</p>
</li>
<li><p>어떻게 모든 경우를 찾음? 가능한 자리에 하나씩 차례대로 배치</p>
</li>
<li><p>배치가 가능한 조건은? 마당 안에 있어야함, 겹치면 안됨</p>
</li>
<li><p>마당안에 있는지 확인하는 법은? 마당의 크기를 n+1*n+1로 표현하고 미리 체크해놓기</p>
</li>
<li><p>겹치는지 확인하는 법은? 마당을 배열로 표현하고 꽃이 위치할 지점이 체크 되었는지 확인 </p>
</li>
<li><p>DFS or BFS 무엇을 사용할지? 중복된 경우에서 우선순위가 존재하지 않기 때문에 그냥 BFS사용</p>
</li>
<li><p>depth기록을 위해 배열의 [0,0]인덱스에는 depth기록, 1번 인덱스부터 모양저장</p>
</li>
</ul>
<h2 id="개선할-점">개선할 점</h2>
<blockquote>
<ol>
<li>DFS를 사용할 경우 현재 상황을 기록하는 배열을 DFS함수의 인자로 넘기지 않고 구현하는 법</li>
</ol>
</blockquote>
<ul>
<li>DFS는 1가지의 경우를 완전히 탐색 후 다음 경우를 처음부터 완전히 탐색하는 성질을 이용한다</li>
<li><em>DFS함수가 끝난 후 바뀐 값을 원래대로 되돌리는 작업을 진행*</em>한다.<pre><code>arrCheck(i, j, true);
DFS(depth + 1, sum + tmpCost);
arrCheck(i, j, false);</code></pre></li>
</ul>
<blockquote>
<ol start="2">
<li>배열복사시 주의 해야 함!! 1차원 배열을 복사 하려면 값이 아닌 주소를 복사하기 때문에 clone()을 사용하여 복사함,<pre><code>             하지만 2차원 배열에서 clone사용시 배열의 값이 주소이기 때문에 clone를 사용하더라도 주소값을 복사하게 됨</code></pre></li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[sync/async 와 blocking/non-blocking개념]]></title>
            <link>https://velog.io/@marha-hwang/syncasync-%EC%99%80-blockingnon-blocking%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@marha-hwang/syncasync-%EC%99%80-blockingnon-blocking%EA%B0%9C%EB%85%90</guid>
            <pubDate>Wed, 10 May 2023 04:26:10 GMT</pubDate>
            <description><![CDATA[<p>** Sync/Async와 Blocking/Non-Blocking의 차이 : A함수가 B함수를 호출하는 경우를 가정**</p>
<h3 id="syncasync">Sync/Async</h3>
<p>B함수의 결과를 A함수에서 처리하는지에 대한 구분, Sync인 경우에는 B함수가 끝날 때까지 기다린 후 B의 결과를 A가 처리하지만, Async인 경우에는 B의 결과는 callback함수를 통해 처리한다</p>
<h3 id="blockingnon-blocking">Blocking/Non-Blocking</h3>
<p>실행의 제어권을 누가 가지느냐에 대한 구분, Blocking인 경우에는 A가 B를 호출한 경우에 B가 종료되기 전까지 B가 제어권을 가지지만, Non-Blocking인 경우에는 B가 실행된 직후 바로 리턴하여 제어권을 A에게 넘겨준다.</p>
<blockquote>
<p>소켓은 기본적으로 blocking모드이기 때문에 단일 스레드에서는 소켓통신을 하는 중에는 다른 작업을 진행하지 못한다. 하지만 소켓을 non-blocking모드로 전환하면 소켓함수가 실행되는 즉시 반환되어 다른 작업을 진행 할 수 있다. 이 때 지속적으로 통신이 완료되었는지를 확인 함으로써 완료처리를 한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[비트마스킹]]></title>
            <link>https://velog.io/@marha-hwang/%EB%B9%84%ED%8A%B8%EB%A7%88%EC%8A%A4%ED%82%B9</link>
            <guid>https://velog.io/@marha-hwang/%EB%B9%84%ED%8A%B8%EB%A7%88%EC%8A%A4%ED%82%B9</guid>
            <pubDate>Fri, 24 Feb 2023 07:23:13 GMT</pubDate>
            <description><![CDATA[<h2 id="비트-마스킹">비트 마스킹</h2>
<blockquote>
<p>각 요소들의 true/false여부를 저장 할 때 사용한다. 비트이기 때문에 오버헤드가 작다는 장점이 있다.</p>
</blockquote>
<ul>
<li>비트 연산(<a href="http://www.tcpschool.com/c/c_operator_bitwise">http://www.tcpschool.com/c/c_operator_bitwise</a>)</li>
</ul>
<h3 id="시프트-연산">시프트 연산</h3>
<p><img src="https://velog.velcdn.com/images/marha-hwang/post/3a546618-7ece-41a3-9c0d-640a0a282de4/image.png" alt=""></p>
<p>int num01 = 15; int num02 = 8;  </p>
<ul>
<li>~num01    // 1의 보수 – num01의 비트를 반전</li>
<li>num02 &lt;&lt; 1 // 곱하기 2 – num01의 비트를 왼쪽으로 1만큼 이동</li>
<li>num02 &gt;&gt; 1 // 나누기 2 – num01의 비트를 오른쪽으로 1만큼 이동</li>
</ul>
<h3 id="비트-마스킹-활용법">비트 마스킹 활용법</h3>
<blockquote>
<p>배열의 인덱스와 같이 0번부터 시작한다. 단 오른쪽부터 인덱스를 매긴다.</p>
</blockquote>
<ul>
<li>삽입 (false -&gt; true)
1010 에서 2번째 비트를 1로 바꾸기</li>
</ul>
<ol>
<li>1 &lt;&lt; 2 시프트 연산을 통해, 두번째 비트만 1인 0100 구하기</li>
<li>1010 | 0100 OR연산을 통해, 2번째 비트를 1로 바꾸기<br>결과 : 1110</li>
</ol>
<ul>
<li>삭제 (true -&gt; false)
1110 에서 2번째 비트를 0으로 바꾸기</li>
</ul>
<ol>
<li>~(1 &lt;&lt; 2) 시프트 연산을 통해, 두번째 비트만 0인 1011 구하기</li>
<li>1110 &amp; 1011 AND 연산을 통해 두번째 비트만 0으로 바꾸기
결과 : 1010</li>
</ol>
<ul>
<li>조회
1010 에서 3번째 비트가 true인지 확인</li>
</ul>
<ol>
<li>1 &lt;&lt; 3 시프트 연산을 통해, 3번째 비트만 1인 1000구하기</li>
<li>1010 &amp; 1000 AND연산을 통해 결과값이 1이상이면 true, 0이면 false</li>
</ol>
<p>int a = 5; // 0101
a |= (1&lt;&lt;3); //비트 켜기 1101
a &amp;= ~(1&lt;&lt;2); //비트 끄기 1001</p>
<p>System.out.print(a); //1001 = 9
//비트확인
System.out.print(a &amp; 1&lt;&lt;3); //3번째 비트 확인 1001이므로 true상태</p>
]]></description>
        </item>
    </channel>
</rss>