<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>vinyl-bin.log</title>
        <link>https://velog.io/</link>
        <description>하루살이</description>
        <lastBuildDate>Fri, 31 Jul 2026 04:38:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>vinyl-bin.log</title>
            <url>https://velog.velcdn.com/images/vinyl-bin/profile/955d5df0-e2d8-4891-bfd3-ee411744aa5c/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. vinyl-bin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/vinyl-bin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Golang 정리(7)- 타입 정의와 타입 별칭]]></title>
            <link>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC7-%ED%83%80%EC%9E%85-%EC%A0%95%EC%9D%98%EC%99%80-%ED%83%80%EC%9E%85-%EB%B3%84%EC%B9%AD</link>
            <guid>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC7-%ED%83%80%EC%9E%85-%EC%A0%95%EC%9D%98%EC%99%80-%ED%83%80%EC%9E%85-%EB%B3%84%EC%B9%AD</guid>
            <pubDate>Fri, 31 Jul 2026 04:38:37 GMT</pubDate>
            <description><![CDATA[<h2 id="1-타입-정의-type-definition">1. 타입 정의 (Type Definition)</h2>
<p><code>type A B</code> 와 같은 형식으로 커스텀 타입(완전히 새로운 타입)을 만들 때 사용</p>
<ul>
<li><p>완전히 새로운 타입 생성: 기존 타입(B)의 형태를 빌려오지만, 컴파일러는 이를 완전히 다른 새로운 타입(A)으로 취급.</p>
</li>
<li><p>형변환(Casting) 필수: 근본이 같은 타입이더라도, 다른 타입으로 간주되므로 섞어 쓰려면 명시적으로 형변환을 해야함.</p>
</li>
<li><p>고유한 메서드 정의 가능: 새로운 타입이므로, 해당 타입만을 위한 전용 메서드를 만들어 붙일 수 있음.</p>
</li>
</ul>
<pre><code>package main

import &quot;fmt&quot;

// int를 기반으로 새로운 커스텀 타입 &#39;MyInt&#39; 정의
type MyInt int 

func main() {
    var a int = 10
    var b MyInt = 20

    // a = b // ❌ 에러 발생: int와 MyInt는 다른 타입!

    a = int(b) // ✅ 명시적 형변환을 해야만 대입 가능
    fmt.Println(a) 
}</code></pre><h2 id="2-타입-별칭-type-alias">2. 타입 별칭 (Type Alias)</h2>
<p><code>type A = B</code> 와 같은 형식으로 타입에 별칭을 붙일 때 사용</p>
<ul>
<li><p>단순한 별명 부여: 새로운 타입을 만드는 것이 아니라, 기존 타입(B)에 새로운 이름(A)표를 하나 더 붙여준 것임.</p>
</li>
<li><p>형변환 불필요: 완전히 동일한 타입이므로, 명시적인 형변환 없이 자유롭게 섞어 쓸 수 있음.</p>
</li>
<li><p>새로운 메서드 정의 불가: 별칭을 통해 새로운 메서드를 추가할 수는 없음. (단, 원본 타입의 메서드는 그대로 사용 가능)</p>
</li>
<li><p>주요 사용처: 대규모 리팩토링 시 패키지 이동에 따른 호환성 유지, 또는 길고 복잡한 타입명(interface{})을 짧게(any) 줄여 쓸 때 사용.</p>
</li>
</ul>
<pre><code>package main

import &quot;fmt&quot;

// int와 완전히 동일한 &#39;MyAlias&#39;라는 별명 생성
type MyAlias = int 

func main() {
    var a int = 10
    var b MyAlias = 20

    a = b // ✅ 에러 없음! 둘은 완전히 똑같은 타입이기 때문
    fmt.Println(a) 
}</code></pre><p>golang에서 <code>any</code>라는 타입은 go 내장 패키지(builtin.go)에서 <code>type any = interface{}</code>로 선언되어 있음.</p>
<p>이때 빈 인터페이스<code>interface{}</code>는 어떠한 메서드도 요구되지 않음. 즉, 조건이 아예 없기 때문에 모든 타입들(int, string, bool 등등)이 들어갈 수 있음.</p>
<pre><code>package main

import &quot;fmt&quot;

// Go 내부에 실제로 선언되어 있는 타입 별칭 예시
// type any = interface{} 

// 1. 기존 방식: interface{}를 인자로 받는 함수
func printWithInterface(val interface{}) {
    fmt.Printf(&quot;[interface{}] 값: %v\n&quot;, val)
}

// 2. 새로운 방식: any를 인자로 받는 함수
func printWithAny(val any) {
    fmt.Printf(&quot;[any] 값: %v\n&quot;, val)
}

func main() {
    var a interface{} = &quot;Hello&quot;
    var b any = 123

    // ✅ 호환성 1: interface{} 변수를 any를 받는 함수에 전달
    // (둘은 완벽히 같은 타입이므로 형변환 없이 작동)
    printWithAny(a)

    // ✅ 호환성 2: any 변수를 interface{}를 받는 함수에 전달
    printWithInterface(b)

    // ✅ 호환성 3: 별다른 형변환 없이 서로 대입도 가능
    a = b
    fmt.Printf(&quot;대입 후 값: %v\n&quot;, a)
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Golang 정리(6)-인터페이스]]></title>
            <link>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC6-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC6-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Thu, 30 Jul 2026 09:24:23 GMT</pubDate>
            <description><![CDATA[<h2 id="0-용어-정리">0. 용어 정리</h2>
<ul>
<li><p><strong>타입(type)</strong>: Go에서 type 키워드를 사용하여 기본 타입 재정의 또는 구조체 정의를 할 수 있음.</p>
<ul>
<li>기본 타입 재정의(커스텀 타입): <code>type Won int</code> 처럼 재정의 할 수 있음. 다른 타입과 대입이나 연산이 금지(int와 Won 타입을 가진 변수끼리 연산X), 메서드 연결이 가능함.</li>
<li>구조체 정의: <code>type Person struct { Name String }</code>처럼 정의할 수 있음, 메서드 연결 가능.</li>
</ul>
</li>
<li><p><strong>함수</strong>: 독립적으로 기능을 수행. </p>
<pre><code>type Person struct { Name string }
</code></pre></li>
</ul>
<p>// 일반 함수: Person 구조체를 매개변수로 받음
func PrintNameFunc(p Person) {
    fmt.Println(p.Name)
}</p>
<p>// 호출 방식
PrintNameFunc(p)</p>
<pre><code>
- **메서드**: 리시버를 붙여서 특정 타입의 기능으로 귀속시킴. 이때 리시버 선언 방식에 따라 내부 값 변경 여부가 결정됨.
    - 값 리시버 (p Person): 구조체의 값을 복사해서 가져옴, 메서드 내부에서 값을 변경해도 원본 구조체의 값은 변하지 않음.
    - 포인터 리시버 (p *Person): 구조체의 메모리 주소를 가져옴, 메서드 내부에서 값을 변경하면 원본 구조체의 값이 실제로 변경됨.</code></pre><p>package main</p>
<p>import &quot;fmt&quot;</p>
<p>type Person struct {
    Name string
}</p>
<p>func (p Person) PrintNameMethod() {
    fmt.Println(p.Name)
}</p>
<p>// 값 리시버: 원본 변경 불가
func (p Person) ChangeNameValue(newName string) {
    p.Name = newName
}</p>
<p>// 포인터 리시버: 원본 변경 가능
func (p *Person) ChangeNamePointer(newName string) {
    p.Name = newName
}</p>
<p>func main() { </p>
<pre><code>p := Person{Name: &quot;kdb&quot;} 

// 1. 값 리시버 호출 (복사본을 수정하므로 원본 변화 없음)
p.ChangeNameValue(&quot;kdv&quot;)
p.PrintNameMethod() // 출력: kdb

// 2. 포인터 리시버 호출 (Go 컴파일러가 &amp;p로 자동 해석하여 원본 수정)
p.ChangeNamePointer(&quot;kdv&quot;)
p.PrintNameMethod() // 출력: kdv</code></pre><p>}</p>
<pre><code>

- **인터페이스**: 특정 타입(구조체, 커스텀타입)이 가져야하는 메서드들의 집합,
    - 암묵적 구현: implements 키워드 없이 특정 타입이 인터페이스에 정의된 메서드를 모두 구현하기만 하면 자동으로 인터페이스를 만족함.
    - 다형성: 하나의 객체나 메서드가 여러가지 타입을 가질 수 있는 성질, 명령은 하나인데 객체마다 다르게 행동 </code></pre><p>type Speaker interface {
    Speak() string
}</p>
<pre><code>
## 1. 인터페이스 다형성 코드 예시
특정 타입이 인터페이스에 정의된 메서드를 모두 구현하면 명시적 선언 없이 암묵적으로 인터페이스를 구현한 것으로 인정함.(이를 Duck Typing이라 함)

아래 코드는 구조체와 커스텀 타입이 모두 인터페이스의 메서드를 구현해서 암묵적으로 인터페이스를 만족하므로 ```MakeSounds(s Speaker)``` 함수의 매개변수인 인터페이스 자리에 구조체와 커스텀 타입을 넣을 수 있음. 
</code></pre><p>package main</p>
<p>import &quot;fmt&quot;</p>
<p>// 1. 인터페이스 정의 (자격 요건)
// &quot;Speak() 메서드를 가진 모든 타입은 Speaker로 인정한다.&quot;
type Speaker interface {
    Speak() string
}</p>
<p>// 2. 일반 구조체 생성 및 메서드 구현
type Dog struct{}</p>
<p>func (d Dog) Speak() string {
    return &quot;멍멍!&quot;
}</p>
<p>// 3. 커스텀 타입 생성 및 메서드 구현 (기본형 int 기반)
type hotdog int</p>
<p>func (h hotdog) Speak() string {
    return &quot;나는 뜨거운 개다!&quot;
}</p>
<p>// 4. 인터페이스를 매개변수로 받는 함수
func MakeSound(s Speaker) {
    fmt.Println(s.Speak())
}</p>
<p>func main() {
    myDog := Dog{}
    myFood := hotdog(1)</p>
<pre><code>// Dog와 hotdog는 형태가 완전히 다르지만,
// 둘 다 Speak()를 할 줄 알기 때문에 Speaker로 취급됩니다.
MakeSound(myDog)  
MakeSound(myFood) </code></pre><p>}</p>
<pre><code>
## 2. 인터페이스 변수 사용 예시
변수 자체를 인터페이스로 정의하고 값을 할당하면 새로운 기술이 도입될 때 기존 코드를 많이 바꾸지 않고 변수만 다른 것으로 바꿀 수 있음.

아래 코드는 db를 인터페이스로 정의하여 MySql 구조체 사용에서 PostgreSQL 구조체 사용으로 바뀌어도 다른 메소드 사용 코드를 수정하지 않고 db 변수 수정만 하면 됨.

</code></pre><p>package main</p>
<p>import &quot;fmt&quot;</p>
<p>// 1. 인터페이스 정의 (자격 요건 상자)
// &quot;Save()와 Connect() 메서드를 가진 타입은 모두 Database로 취급한다&quot;
type Database interface {
    Connect()
    Save(data string)
}</p>
<p>// ---------------------------------------------------
// 2-A. 첫 번째 구체적인 타입: MySQL
type MySQL struct {
    host string
}</p>
<p>func (m MySQL) Connect() {
    fmt.Printf(&quot;[MySQL] %s 서버에 성공적으로 연결되었습니다.\n&quot;, m.host)
}</p>
<p>func (m MySQL) Save(data string) {
    fmt.Printf(&quot;[MySQL] 데이터 저장 완료: &#39;%s&#39;\n&quot;, data)
}</p>
<p>// ---------------------------------------------------
// 2-B. 두 번째 구체적인 타입: PostgreSQL (나중에 새로 추가됨)
type PostgreSQL struct {
    host string
    port int
}</p>
<p>func (p PostgreSQL) Connect() {
    fmt.Printf(&quot;[PostgreSQL] %s:%d 서버에 성공적으로 연결되었습니다.\n&quot;, p.host, p.port)
}</p>
<p>func (p PostgreSQL) Save(data string) {
    fmt.Printf(&quot;[PostgreSQL] 데이터 저장 완료: &#39;%s&#39;\n&quot;, data)
}</p>
<p>// ---------------------------------------------------</p>
<p>func main() {
    // 3. 변수를 인터페이스 타입으로 선언! (다용도 상자 준비)
    // 아직 이 안에는 아무것도 없습니다 (nil).
    var db Database</p>
<pre><code>fmt.Println(&quot;--- 서비스 초기 (MySQL 사용) ---&quot;)

// 인터페이스 변수에 MySQL 구조체를 할당
db = MySQL{host: &quot;192.168.1.100&quot;}

// 여기서부터는 db가 MySQL인지 신경 쓰지 않고 인터페이스의 메서드만 호출합니다.
db.Connect()
db.Save(&quot;user_001&quot;)


fmt.Println(&quot;\n--- 6개월 뒤 (PostgreSQL로 교체) ---&quot;)

// db 변수가 &#39;인터페이스&#39;이기 때문에, 코드를 엎을 필요 없이 알맹이만 스윽 갈아 끼웁니다.
db = PostgreSQL{host: &quot;10.0.0.5&quot;, port: 5432}

// 아래 코드는 위와 동일하지만, 이제는 PostgreSQL의 메서드가 실행됩니다!
db.Connect()
db.Save(&quot;user_001&quot;)</code></pre><p>}</p>
<p>```</p>
<blockquote>
<p>참고: <a href="https://velog.io/@chappi/Go%EB%A5%BC-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-10%EC%9D%BC%EC%B0%A8-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%9E%80-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EC%A0%95%EC%9D%98">https://velog.io/@chappi/Go%EB%A5%BC-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-10%EC%9D%BC%EC%B0%A8-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EB%9E%80-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EC%A0%95%EC%9D%98</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Golang 정리(5)-채널(channel)]]></title>
            <link>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC5-%EC%B1%84%EB%84%90channel</link>
            <guid>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC5-%EC%B1%84%EB%84%90channel</guid>
            <pubDate>Fri, 17 Jul 2026 18:25:15 GMT</pubDate>
            <description><![CDATA[<p>채널은 데이터를 주고 받을 수 있는 통로임. 
이 포스팅에서는 버퍼 없는 채널의 작동 방시에 대해 작성함.</p>
<h2 id="1-채널--바통터치">1. 채널 = 바통터치</h2>
<p>버퍼 없는 채널의 경우 데이터 주는 것과 데이터를 받는 것이 정확히 같은 시간에 이루어져야 함. 
이 특성으로 쌍방향 무한 대기(blocking) 상태가 나타남.</p>
<ul>
<li>ch &lt;- 1 (데이터를 줄 때): 데이터를 받을 때까지 그 줄에서 무한 대기</li>
<li>result := &lt;- ch (데이터 받을 때): 데이터를 줄 때까지 그 줄에서 무한 대기</li>
</ul>
<h2 id="2-데드락-발생">2. 데드락 발생</h2>
<ul>
<li>블로킹(blocking): 특정 작업이 끝날 때까지 잠시 멈춰서서 대기하는 &#39;정상적인 상태&#39;</li>
<li>데드락(deadlock): 두 개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다리며 영원히 멈춰버리는 &#39;에러 상태&#39;</li>
</ul>
<p>고루틴 없이 메인 함수 안에서 채널로 데이터를 주고 받으면 데드락 발생함.</p>
<pre><code>package main

import &quot;fmt&quot;

func main() {
    ch := make(chan int) // 버퍼 없는 채널 생성

    ch &lt;- 1 // [1] 데이터를 보냄 (여기서 멈춤!)

    result := &lt;-ch // [2] 데이터를 받음
    fmt.Println(result)
}</code></pre><p>[1]에서 무한 대기가 발생하고 영원히 아래 라인으로 넘어갈 수 없음. 혼자서 데이터를 주지도 받지도 못하고 영원히 멈춰있는 데드락 상태에 빠짐. </p>
<h2 id="3-고루틴-사용">3. 고루틴 사용</h2>
<p>위의 데드락을 해결하기 위해서 데이터는 동시에 주고 받아야 함.</p>
<pre><code>package main

import &quot;fmt&quot;

func main() {
    ch := make(chan int)

    // 새로운 심부름꾼(고루틴) 생성
    go func() {
        ch &lt;- 1 // 고루틴이 데이터를 보내고 대기함
    }()

    result := &lt;-ch // 메인 함수는 여기서 대기하다가, 고루틴이 1을 주면 받음
    fmt.Println(result)
}</code></pre><p>고루틴을 사용하면 데이터를 동시에 주고 받을 수 있으므로 데드락 없이 다음 작업이 진행됨.</p>
<h2 id="4-무한-블로킹">4. 무한 블로킹</h2>
<p>채널의 블로킹 성질을 활용하여 백그라운드 서버를 만들 때 프로그램 강제 종료를 막을 수 있음.</p>
<pre><code>func main() {
    // 1. 메시지를 처리하는 백그라운드 고루틴들을 실행
    go catalogManager.MenuHandler()
    go catalogManager.SubMessage(ch)

    fmt.Println(&quot;Catalog Manager가 실행 중입니다...&quot;)

    // 2. 종료 방지용 &#39;forever&#39; 채널 생성
    forever := make(chan bool)

    // 3. 아무도 값을 넣어주지 않는 채널을 대기함
    &lt;-forever 
}</code></pre><p>채널에 아무도 데이터를 주지 않기 때문에 영원기 대기하게 되어 main 함수가 종료되지 않고 백그라운드의 고루틴들이 종료되지 않고 계속 돌아가게 됨.</p>
<p>이때 백그라운드에서 다른 고루틴이 실행되고 있기 때문에 데드락이 발생하지 않음.</p>
<blockquote>
<p>참고: <a href="https://wikidocs.net/164596">https://wikidocs.net/164596</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Golang으로 SQLite 동시성 처리]]></title>
            <link>https://velog.io/@vinyl-bin/Golang%EC%9C%BC%EB%A1%9C-SQLite-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@vinyl-bin/Golang%EC%9C%BC%EB%A1%9C-SQLite-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 15 Jul 2026 07:32:24 GMT</pubDate>
            <description><![CDATA[<h2 id="1-sqlite-한계">1. SQLite 한계</h2>
<p>SQLite는 애플리케이션에 내장되어 직접 파일을 읽고 쓰는 방식으로 동작. 이로 인해 아래와 같은 한계 발생.</p>
<ul>
<li>File Lock 기반 동시성 제어: 기본 저널링 모드(Rollback Journal)에서는 쓰기 작업이 시작되면 파일 전체에 Lock이 걸림</li>
<li>Read/Write 상호 블로킹: Lock이 걸린 동안 다른 프로세스나 스레드도 데이터를 읽거나 쓸 수 없음, 읽기와 쓰기 작업을 동시에 수행할 수 없음</li>
<li>Go 언어와 충돌: Go에서 기본적으로 Connection Pool을 사용하여 여러 고루틴이 동시에 데이터베이스에 접근하면 Lock이 걸리고 대기 시간을 초과하여 &#39;database is locked&#39;(SQLITE_BUSY) 에러 발생</li>
</ul>
<h2 id="2-동시성-처리-방법">2. 동시성 처리 방법</h2>
<ul>
<li>WAL 모드: 읽기와 쓰기 동시 수행</li>
<li>쓰기 직렬화: 단일 고루틴 사용하여 쓰기와 쓰기 간의 충돌 방지</li>
<li>읽기 병렬화: 커넥션 풀과 여러 개의 고루틴을 사용하여 빠르게 읽기 수행</li>
</ul>
<h3 id="1-wal-모드">(1) WAL 모드</h3>
<h5 id="wal-모드란">WAL 모드란</h5>
<p>데이터베이스를 수정할 때 원본 .db 파일을 직접 덮어쓰는 대신, 변경 사항을 .db-wal (Write-Ahead Log) 이라는 별도의 로그 파일 끝에 순차적으로 추가(Append)하는 방식</p>
<ul>
<li>.db-wal 파일: 쓰기 트랜잭션이 기록되는 곳으로 데이터가 이곳에 먼저 쌓인 뒤, 나중에 적절한 시점(Checkpoint)에 원본 .db 파일로 일괄 병합됨</li>
<li>.db-shm(Shared Memory) 파일: .db-wal 파일의 인덱스 역할을 하는 공유 메모리 파일로 여러 커넥션이 WAL 파일 내의 최신 데이터를 매우 빠르게 찾을 수 있도록 함. 데이터베이스가 닫히면 자동으로 지워짐</li>
</ul>
<p>WAL 모드를 사용하면 쓰기와 읽기 작업 간의 동시성을 확보할 수 있음(다른 곳에서 데이터 쓰기 작업을 하고 있어도 다른 곳에서 데이터 읽기 작업을 수행할 수 있음), 또한 무작위 쓰기가 아닌 순차적 쓰기를 수행하므로 디스크 I/O가 최적화 됨.</p>
<pre><code>dsn := fmt.Sprintf(&quot;file:%s?_pragma=journal_mode(WAL)&amp;_pragma=busy_timeout(5000)&amp;_pragma=foreign_keys(ON)&amp;_pragma=synchronous(NORMAL)&quot;, path)
db, err := sql.Open(&quot;sqlite&quot;, dsn)</code></pre><p>sql.Open을 호출할 때 DSN(Data Source Name) 문자열에서 WAL 활성화하여 파라미터로 넘겨줌</p>
<ul>
<li>_pragma=journal_mode(WAL): 앞서 설명한 WAL 모드를 활성화함. 이 설정 덕분에 커넥션이 맺어질 때 .db-wal과 .db-shm 파일이 자동으로 생성됨.</li>
</ul>
<h3 id="2-쓰기-직렬화">(2) 쓰기 직렬화</h3>
<p>쓰기와 읽기 간의 동시성은 해결했지만, SQLite 구조상 단 하나의 쓰기 트랜잭션(최소 작업 단위)만 허용하므로 여전히 쓰기와 쓰기의 충돌은 해결할 수 없음. </p>
<p>이를 해결하기 위해 단일 고루틴으로 단일 쓰기를 보장함.</p>
<pre><code>func NewStore(dbPath string) (*Store, error) {
    // WAL 모드로 SQLite 열기
    dsn := fmt.Sprintf(&quot;file:%s?_pragma=journal_mode(WAL)&amp;_pragma=busy_timeout(5000)&quot;, dbPath)
    db, err := sql.Open(&quot;sqlite&quot;, dsn)
    if err != nil {
        return nil, err
    }
    db.SetMaxOpenConns(4) // 읽기(Read)는 병렬로 4개까지 허용
    s := &amp;Store{
        db:     db,
        writes: make(chan writeJob, 100), // 최대 100개의 쓰기 작업이 대기할 수 있는 채널 생성
        done:   make(chan struct{}),      // 종료 신호 채널 생성
    }
    // ★ 핵심: 딱 하나뿐인 &#39;쓰기 전담 고루틴(Worker)&#39;을 백그라운드에서 출근시킵니다.
    s.wg.Add(1) 
    go s.writer() 
    return s, nil
}
// 단일 쓰기 고루틴 본체 (오직 얘 혼자서만 DB에 글을 씁니다)
func (s *Store) writer() {
    defer s.wg.Done() // 퇴근할 때 WaitGroup에 &quot;저 일 다 끝내고 갑니다&quot;라고 알림
    for {
        select {
        case job := &lt;-s.writes: 
            // 채널(writes)에 누군가 작업을 던지면, 쏙 꺼내서 순서대로 처리합니다.
            // 오직 하나의 고루틴 안에서 실행되므로 절대 다른 쓰기 작업과 충돌(병렬 실행)하지 않습니다.
            _, err := s.db.ExecContext(job.ctx, job.query, job.args...)

            // 처리가 끝났으면, 작업을 부탁한 사람의 result 채널로 결과를 배달해줍니다.
            job.result &lt;- err 
        case &lt;-s.done:
            // &quot;이제 영업 끝났어(Close)&quot;라는 done 신호를 받으면 루프를 탈출합니다.
            // 단, 아직 채널에 줄 서 있는 남은 손님(작업)들이 있다면 마저 처리해 줍니다.
            for len(s.writes) &gt; 0 {
                job := &lt;-s.writes
                _, err := s.db.ExecContext(job.ctx, job.query, job.args...)
                job.result &lt;- err
            }
            return // 완전히 퇴근(루프 종료)
        }
    }
}</code></pre><h3 id="3-읽기-병렬화">(3) 읽기 병렬화</h3>
<p>WAL 모드의 가장 큰 장점은 누군가 쓰기 작업을 하고 있어도 읽기 작업이 전혀 차단(Blocking)되지 않는다는 점임.
따라서 쓰기 작업과 달리, 읽기 작업은 여러 개의 고루틴이 동시에 데이터에 접근하도록 설계할 수 있음.</p>
<pre><code>db.SetMaxOpenConns(4)</code></pre><ul>
<li>Go 언어<code>database/sql</code>은 내부적으로 커넥션 풀(Connection Pool)을 알아서 관리 </li>
<li>위 코드는 &quot;데이터베이스와 연결되는 물리적인 선을 최대 4개까지만 열어두겠다&quot;는 커넥션 풀 설정 </li>
<li>이를 통해 수십 개의 고루틴이 동시에 읽기 요청을 보내더라도, 커넥션 풀이 최대 4개의 쿼리를 병렬(Parallel)로 안전하게 분산 처리</li>
<li>5번째 요청부터는 큐에 대기하다가 연결이 반환되면 즉시 재사용하므로, DB에 과도한 부하를 주지 않으면서도 읽기 속도를 극대화할 수 있음</li>
</ul>
<h2 id="3-전체-코드">3. 전체 코드</h2>
<pre><code>package main

import (
    &quot;context&quot;
    &quot;database/sql&quot;
    &quot;fmt&quot;
    &quot;log&quot;
    &quot;sync&quot;

    _ &quot;modernc.org/sqlite&quot; // 순수 Go SQLite 드라이버
)

// 1. 쓰기 작업을 포장하는 상자 (Job 구조체)
type writeJob struct {
    ctx    context.Context
    query  string       // 실행할 쿼리 (예: INSERT INTO ...)
    args   []any        // 쿼리에 들어갈 데이터
    result chan error   // 작업이 끝난 후 결과를 돌려받을 우체통(채널)
}

// 2. 데이터베이스와 단일 고루틴을 관리하는 관리자 (Store 구조체)
type Store struct {
    db     *sql.DB
    writes chan writeJob  // 쓰기 작업들이 줄을 서서 대기할 큐(버퍼 채널)
    done   chan struct{}  // 종료 신호를 알리는 방송용 채널
    wg     sync.WaitGroup // 고루틴이 안전하게 종료될 때까지 기다려주는 역할
}

// 3. Store 초기화 및 고루틴 시작
func NewStore(dbPath string) (*Store, error) {
    // WAL 모드로 SQLite 열기
    dsn := fmt.Sprintf(&quot;file:%s?_pragma=journal_mode(WAL)&amp;_pragma=busy_timeout(5000)&quot;, dbPath)
    db, err := sql.Open(&quot;sqlite&quot;, dsn)
    if err != nil {
        return nil, err
    }
    db.SetMaxOpenConns(4) // 읽기(Read)는 병렬로 4개까지 허용

    s := &amp;Store{
        db:     db,
        writes: make(chan writeJob, 100), // 최대 100개의 쓰기 작업이 대기할 수 있는 채널 생성
        done:   make(chan struct{}),      // 종료 신호 채널 생성
    }

    // ★ 핵심: 딱 하나뿐인 &#39;쓰기 전담 고루틴(Worker)&#39;을 백그라운드에서 출근시킵니다.
    s.wg.Add(1) 
    go s.writer() 

    return s, nil
}

// 4. 단일 쓰기 고루틴 본체 (오직 얘 혼자서만 DB에 글을 씁니다)
func (s *Store) writer() {
    defer s.wg.Done() // 퇴근할 때 WaitGroup에 &quot;저 일 다 끝내고 갑니다&quot;라고 알림

    for {
        select {
        case job := &lt;-s.writes: 
            // 채널(writes)에 누군가 작업을 던지면, 쏙 꺼내서 순서대로 처리합니다.
            // 오직 하나의 고루틴 안에서 실행되므로 절대 다른 쓰기 작업과 충돌(병렬 실행)하지 않습니다.
            _, err := s.db.ExecContext(job.ctx, job.query, job.args...)

            // 처리가 끝났으면, 작업을 부탁한 사람의 result 채널로 결과를 배달해줍니다.
            job.result &lt;- err 

        case &lt;-s.done:
            // &quot;이제 영업 끝났어(Close)&quot;라는 done 신호를 받으면 루프를 탈출합니다.
            // 단, 아직 채널에 줄 서 있는 남은 손님(작업)들이 있다면 마저 처리해 줍니다.
            for len(s.writes) &gt; 0 {
                job := &lt;-s.writes
                _, err := s.db.ExecContext(job.ctx, job.query, job.args...)
                job.result &lt;- err
            }
            return // 완전히 퇴근(루프 종료)
        }
    }
}

// 5. 외부에서 쓰기 작업을 요청하는 함수 (직접 쓰지 않고 채널에 던짐)
func (s *Store) WriteData(ctx context.Context, query string, args ...any) error {
    // 1회용 결과 반환 채널을 만듭니다. (버퍼를 1로 두어 블로킹 방지)
    resultCh := make(chan error, 1)

    // 작업을 상자에 예쁘게 포장해서 writes 채널(컨베이어 벨트)에 올려둡니다.
    s.writes &lt;- writeJob{
        ctx:    ctx,
        query:  query,
        args:   args,
        result: resultCh,
    }

    // 백그라운드에 있는 writer 고루틴이 내 작업을 꺼내서 처리해줄 때까지 기다렸다가, 결과를 받습니다.
    return &lt;-resultCh
}

// 6. 안전한 종료 처리 (Graceful Shutdown)
func (s *Store) Close() {
    close(s.done) // 1. writer 고루틴에게 &quot;이제 더 이상 작업 안 받아, 퇴근 준비해&quot;라고 방송함
    s.wg.Wait()   // 2. writer 고루틴이 남은 일을 다 마치고 완전히 꺼질 때까지 대기
    s.db.Close()  // 3. 최종적으로 DB 연결 종료
}

// --- 실행 테스트 ---
func main() {
    store, _ := NewStore(&quot;test.db&quot;)
    defer store.Close()

    var wg sync.WaitGroup

    // 10명의 사람(고루틴)이 동시에 DB에 글을 쓰려고 달려드는 상황 가정
    for i := 1; i &lt;= 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            // 이 코드는 10번 동시에 실행되지만, 
            // 내부는 채널에 줄을 서게 되므로 DB 입장에서는 완벽히 순서대로 처리됩니다.
            err := store.WriteData(context.Background(), &quot;INSERT INTO users (name) VALUES (?)&quot;, fmt.Sprintf(&quot;User%d&quot;, id))
            if err != nil {
                log.Println(&quot;에러:&quot;, err)
            }
        }(i)
    }

    wg.Wait() // 10명이 쓰기 요청을 다 마칠 때까지 대기
    fmt.Println(&quot;모든 쓰기 작업 완료! 락(Lock) 충돌은 전혀 없었습니다.&quot;)
}
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[관계형 데이터베이스 핵심 내용 정리]]></title>
            <link>https://velog.io/@vinyl-bin/%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-%ED%95%B5%EC%8B%AC-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@vinyl-bin/%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-%ED%95%B5%EC%8B%AC-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 11 Jul 2026 19:01:51 GMT</pubDate>
            <description><![CDATA[<h2 id="1-관계형-데이터베이스">1. 관계형 데이터베이스</h2>
<ul>
<li>관계형 데이터베이스 RDBMS(Relational Database Management System)는 행과 열로 구성된 표 형태로 데이터를 저장하는 시스템</li>
<li>데이터 중복을 최소화(정규화)하고 데이터의 무결성을 보장</li>
<li>테이블을 의미 단위로 쪼개고 키를 통해 연결</li>
</ul>
<p>핵심 키로는 기본키와 외래키 존재</p>
<ul>
<li>기본키(PK, Primary key): 테이블 내 각 행을 유일하게 식별하는 고유번호, 중복될 수 없고 비어있으면 안됨</li>
<li>외래키(FK, Foreign Key): 쪼개진 다른 테이블의 PK를 참고하여 두 테이블을 연결하는 키</li>
</ul>
<pre><code>-- 1. 부모 테이블 (사용자) 생성
CREATE TABLE Users (
    -- id를 기본키(PK)로 지정. 
    -- AUTO_INCREMENT: 데이터가 추가될 때마다 1부터 자동 증가
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL
);

-- 2. 자식 테이블 (게시글) 생성
CREATE TABLE Posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    -- 외래키로 사용할 컬럼 선언 (참조할 Users.id와 타입이 같아야 함)
    user_id INT, 

    -- [외래키 제약조건 설정]
    -- &quot;이 테이블의 user_id는 Users 테이블의 id를 참조한다&quot;
    -- fk_post_user: 나중에 수정/삭제나 에러 추적을 위해 개발자가 지어준 제약조건 이름표
    CONSTRAINT fk_post_user FOREIGN KEY (user_id) REFERENCES Users(id)

    -- ON DELETE CASCADE: 만약 부모(Users) 데이터가 삭제되면, 
    -- 그 유저가 쓴 게시글(자식 데이터)도 연쇄적으로 모두 삭제하라는 안전장치
    ON DELETE CASCADE 
);</code></pre><h2 id="2-관계차수">2. 관계차수</h2>
<p>테이블을 쪼갠 후 연결 방식에 따라 1:N, 1:1, N:M 3가지 관계로 나뉨 </p>
<h3 id="2-1-1n-일대다-관계">2-1. 1:N (일대다) 관계</h3>
<p>예를 들면 1명의 사용자가 N개의 게시글을 작성하는 관계
<strong>이때 N쪽 테이블에 FK를 배치</strong></p>
<p>아래 코드의 경우 Users가 1이고 Posts가 N 관계
따라서 Posts 테이블에 Users의 PK를 FK로 등록함</p>
<pre><code>-- 사용자 Users(1) - 게시글 Posts(N) 관계

CREATE TABLE Users (
    -- id를 기본키(PK)로 지정. 
    -- AUTO_INCREMENT: 데이터가 추가될 때마다 1부터 자동 증가
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL
);


CREATE TABLE Posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    user_id INT, -- &#39;N&#39;쪽인 게시글 테이블에 FK를 둠
    CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES Users(id)
);</code></pre><h3 id="2-2-11-일대일-관계">2-2. 1:1 (일대일) 관계</h3>
<p>하나의 데이터가 딱 하나의 데이터와 매칭됨. 보통 덩치가 큰 데이터를 분리해 DB 성능을 높이거나 보안 민감도를 나눌 때 사용.
예를 들면 사용자 기본 정보와 사용자 상세 프로필 관계
<strong>이때 논리적으로 종속적인 테이블에 FK 배치하며 FK에 UNIQUE 제약조건을 걸어 1:N이 되는 것을 방지</strong></p>
<pre><code>CREATE TABLE Users (
    -- id를 기본키(PK)로 지정. 
    -- AUTO_INCREMENT: 데이터가 추가될 때마다 1부터 자동 증가
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL
);

CREATE TABLE UserProfiles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    bio TEXT,
    -- UNIQUE 제약조건을 걸어 한 유저당 하나의 프로필만 강제함
    user_id INT UNIQUE, 
    CONSTRAINT fk_profile_user FOREIGN KEY (user_id) REFERENCES Users(id)
);</code></pre><h3 id="2-3-nm-다대다-관계">2-3. N:M (다대다) 관계</h3>
<p>관계형 DB는 N:M을 직접 연결할 수 없으므로 항상 매핑 테이블(중간 테이블)을 두어 두 개의 1:N 관계로 쪼개어 해결함.
예를 들면 학생 N명과 수강과목 M개 관계</p>
<p><strong>이때 A와 B가 N:M 관계이고 이를 해결하기 위한 매핑테이블이 C일 때, A:C과 B:C는 1:N이 됨. 즉, 매핑 테이블이 다(N) 관계가 됨.</strong></p>
<pre><code>-- 학생 테이블(A)과 강의 테이블(B)이 있다고 가정

-- 매핑 테이블 (C: 수강내역)
CREATE TABLE Student_Class_Mapping (
    student_id INT,
    class_id INT,

    -- [복합키 설정]
    -- PK는 테이블당 1개만 가능하지만, 여러 컬럼을 묶어서 1개의 PK로 만들 수 있음
    -- 1번 학생이 1번 강의를 듣는 내역이 중복 저장되는 것을 완벽히 차단함
    PRIMARY KEY (student_id, class_id),

    -- 양쪽 테이블을 바라보는 2개의 외래키(FK) 설정
    CONSTRAINT fk_mapping_student FOREIGN KEY (student_id) REFERENCES Students(id),
    CONSTRAINT fk_mapping_class FOREIGN KEY (class_id) REFERENCES Classes(id)
);</code></pre><h2 id="3-join">3. JOIN</h2>
<p>정규화를 통해 쪼개둔 테이블을 합쳐 조회하는 slq문</p>
<ul>
<li>JOIN 실행 전 각 테이블 데이터 (dept_id가 두 테이블의 공통 컬럼)<pre><code>========================================
[테이블 1] Employees (직원 테이블) - 왼쪽
========================================
emp_id  |  emp_name  |  dept_id  |  비고</code></pre></li>
</ul>
<hr>
<p>1       |  손흥민    |  10       |
2       |  이강인    |  20       |
3       |  김민재    |  NULL     |  부서 배정 대기</p>
<p>========================================
[테이블 2] Departments (부서 테이블) - 오른쪽
========================================
dept_id |  dept_name |  비고</p>
<hr>
<p>10      |  개발팀    |
20      |  영업팀    |
30      |  인사팀    |  소속 직원 없음</p>
<pre><code>
### 3-1. INNER JOIN
양쪽 테이블 모두에 매칭되는 데이터가 있는 경우만 가져옴

- INNER JOIN 코드</code></pre><p>-- 직원 이름과 그 직원이 속한 부서 이름을 조회
SELECT E.emp_name, D.dept_name
FROM Employees E
INNER JOIN Departments D   -- (INNER는 생략하고 그냥 JOIN이라고 써도 됩니다)
ON E.dept_id = D.dept_id;  -- 연결 기준: 직원의 부서번호와 부서테이블의 부서번호가 같은 것만!</p>
<pre><code>
- 출력</code></pre><p>[실행 후 결과: INNER JOIN]</p>
<ul>
<li>설명: ON 절에 명시한 dept_id 값이 양쪽에 모두 존재하는 데이터만 출력됩니다. </li>
<li>버려지는 데이터: 부서가 NULL인 &#39;김민재&#39;, 매칭되는 직원이 없는 &#39;인사팀&#39;은 통째로 탈락합니다.</li>
</ul>
<h2 id="emp_name----dept_name">emp_name  |  dept_name</h2>
<p>손흥민    |  개발팀
이강인    |  영업팀</p>
<pre><code>
### 3-2. LEFT OUTER JOIN
왼쪽 테이블 데이터를 모두 보여주고 오른쪽 테이블에 없는 내용은 NULL로 채움

- LEFT OUTER JOIN 코드</code></pre><p>-- 기준(FROM 절)이 Employees(직원) 테이블입니다.
SELECT E.emp_name, D.dept_name
FROM Employees E
LEFT OUTER JOIN Departments D 
ON E.dept_id = D.dept_id;</p>
<pre><code>
- 출력</code></pre><p>[실행 후 결과: LEFT JOIN]</p>
<ul>
<li>설명: 왼쪽(Employees) 데이터는 무조건 100% 보존됩니다. </li>
<li>결과: 짝을 찾지 못한 &#39;김민재&#39;도 살아남으며, 부서명 칸만 NULL로 채워집니다.</li>
</ul>
<h2 id="emp_name----dept_name-1">emp_name  |  dept_name</h2>
<p>손흥민    |  개발팀
이강인    |  영업팀
김민재    |  NULL</p>
<pre><code>### 3-3. RIGHT OUTER JOIN
오른쪽 테이블 데이터를 모두 보여주고 왼쪽 테이블에 없는 내용은 NULL로 채움

- RIGHT OUTER JOIN 코드</code></pre><p>-- 기준(JOIN 절)이 Departments(부서) 테이블입니다.
SELECT E.emp_name, D.dept_name
FROM Employees E
RIGHT OUTER JOIN Departments D 
ON E.dept_id = D.dept_id;</p>
<pre><code>
- 출력</code></pre><p>[실행 후 결과: RIGHT JOIN]</p>
<ul>
<li>설명: 오른쪽(Departments) 데이터는 무조건 100% 보존됩니다. </li>
<li>결과: 소속 직원이 없는 &#39;인사팀&#39;도 살아남으며, 직원이름 칸만 NULL로 채워집니다.</li>
</ul>
<h2 id="emp_name----dept_name-2">emp_name  |  dept_name</h2>
<p>손흥민    |  개발팀
이강인    |  영업팀
NULL      |  인사팀</p>
<pre><code>### 3-4. FULL OUTER JOIN
양쪽 테이블의 모든 데이터를 가져옴

- FULL OUTER JOIN 코드</code></pre><p>SELECT E.emp_name, D.dept_name
FROM Employees E
FULL OUTER JOIN Departments D 
ON E.dept_id = D.dept_id;</p>
<pre><code>
- 출력</code></pre><p>[실행 후 결과: FULL OUTER JOIN]</p>
<ul>
<li>설명: 양쪽 데이터 100% 보존. 짝이 없는 &#39;김민재&#39;와 &#39;인사팀&#39;이 모두 출력됩니다.</li>
</ul>
<h2 id="emp_name----dept_name-3">emp_name  |  dept_name</h2>
<p>손흥민    |  개발팀
이강인    |  영업팀
김민재    |  NULL
NULL      |  인사팀</p>
<pre><code>### 3-5. CROSS JOIN
양쪽 테이블 데이터의 모든 가능한 조합을 가져옴

- CROSS JOIN 코드</code></pre><p>SELECT E.emp_name, D.dept_name
FROM Employees E
CROSS JOIN Departments D; -- ON 조건절이 없습니다!</p>
<pre><code>
- 출력</code></pre><p>[실행 후 결과: CROSS JOIN]</p>
<ul>
<li>설명: 왼쪽 3행 × 오른쪽 3행 = 총 9행의 모든 경우의 수가 생성됨.</li>
</ul>
<h2 id="emp_name----dept_name-4">emp_name  |  dept_name</h2>
<p>손흥민    |  개발팀
손흥민    |  영업팀
손흥민    |  인사팀
이강인    |  개발팀
이강인    |  영업팀
이강인    |  인사팀
김민재    |  개발팀
김민재    |  영업팀
김민재    |  인사팀
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래밍 패러다임 종류]]></title>
            <link>https://velog.io/@vinyl-bin/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84-%EC%A2%85%EB%A5%98</link>
            <guid>https://velog.io/@vinyl-bin/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84-%EC%A2%85%EB%A5%98</guid>
            <pubDate>Sat, 11 Jul 2026 10:07:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고
<a href="https://adjh54.tistory.com/97">https://adjh54.tistory.com/97</a>
<a href="https://velog.io/@yyjjvv/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84">https://velog.io/@yyjjvv/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84</a></p>
</blockquote>
<h2 id="프로그래밍-패러다임programming-paradigm">프로그래밍 패러다임(Programming Paradigm)</h2>
<p>프로그래머가 코드를 작성하고 문제를 해결할 때 취하는 관점이자 접근방식</p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/5eae54da-9d1e-4ad7-a8c9-28a6916eaf65/image.png" alt=""></p>
<p>프로그래밍 패러다임은 위 그림과 같이 나뉨.</p>
<h2 id="1-명령형">1. 명령형</h2>
<ul>
<li>단계적인 절차와 상태변경의 순서를 상세히 기술하는 방식을 의미</li>
<li><strong>어떻게(HOW)</strong>에 초점을 맞춤</li>
<li>변수값을 수정하며 상태변경하거나 조건문 반복문을 사용해 코드 실행흐름을 직접 제어</li>
<li>절차지향과 객체지향이 명령형에 해당</li>
</ul>
<h3 id="1-1-절차지향형pp-procedural-programming">1-1. 절차지향형(PP, Procedural Programming)</h3>
<p>어떠한 일을 해결하기 위해 일련의 작업 순서대로 프로그램이 유기적으로 연결되도록 구성하는 프로그래밍 기법</p>
<ul>
<li>대표언어: C언어<pre><code>#include &lt;stdio.h&gt;
</code></pre></li>
</ul>
<p>int main() {
    int sum = 0; // 상태(데이터)</p>
<pre><code>// 순서대로 명령을 실행 (어떻게 더할지 단계별로 명시)
for(int i = 1; i &lt;= 5; i++) {
    sum = sum + i; 
}

printf(&quot;%d&quot;, sum);
return 0;</code></pre><p>}</p>
<pre><code>
### 1-2. 객체지향형 (OOP, Object-Oriented Programming)
어떠한 일을 해결하기 위해 객체들(Object)를 기본단위로 나누고 서로 간의 상호작용으로 개발하는 프로그래밍 기법
- 대표언어: Java</code></pre><p>// 설계도(클래스) 정의
class Car {
    int speed = 0; // 데이터(상태)</p>
<pre><code>// 행동(메서드)
void accelerate() {
    speed += 10;
    System.out.println(&quot;현재 속도: &quot; + speed);
}</code></pre><p>}</p>
<p>public class Main {
    public static void main(String[] args) {
        // 객체 생성 및 상호작용
        Car myCar = new Car(); 
        myCar.accelerate(); 
    }
}</p>
<pre><code>

## 2. 선언형
- 최종적인 결과를 명시하는 방식을 의미
- **무엇을(WHAT)**에 초점을 맞춤
- 내부적인 순서나 제어흐름을 지시하지 않음(무엇에만 초점을 맞추고 어떻게는 신경쓰지 않음)
- 함수형과 논리형이 선언형에 해당

### 2-1. 함수형 (FP, Functional Programming)
상태 및 데이터 변경에는 사용하지 않으며(함수는 외부의 상태를 참조하거나 변경하지 않음) 작은 문제를 해결하기 위한 &#39;함수&#39; 단위로 개발하는 프로그래밍 기법
- 언어: Golang에서 함수형 일부 지원
(아래 코드를 보면 main 함수 내부에서 반복문이나 제어문 없이 함수로 무엇을 할지 명시함)</code></pre><p>package main</p>
<p>import &quot;fmt&quot;</p>
<p>// 1. Filter: 조건(predicate)을 만족하는 요소만 걸러내는 고차 함수
// 함수(func(int) bool)를 매개변수로 받습니다.
func Filter(numbers []int, predicate func(int) bool) []int {
    var result []int
    for _, n := range numbers {
        if predicate(n) {
            result = append(result, n) 
        }
    }
    // 원본 데이터를 수정하지 않고 새로운 슬라이스를 반환합니다. (불변성 유지)
    return result
}</p>
<p>// 2. Map: 각 요소에 변환 연산(transform)을 적용하는 고차 함수
// 함수(func(int) int)를 매개변수로 받습니다.
func Map(numbers []int, transform func(int) int) []int {
    var result []int
    for _, n := range numbers {
        result = append(result, transform(n))
    }
    return result
}</p>
<p>func main() {
    // 원본 데이터
    numbers := []int{1, 2, 3, 4, 5, 6}</p>
<pre><code>// [함수형 패러다임 적용]

// 순수 함수들을 일급 함수 특성을 이용해 변수에 할당합니다.
isEven := func(n int) bool { return n%2 == 0 }
double := func(n int) int { return n * 2 }

// &#39;어떻게&#39; 걸러낼지 반복문을 쓰지 않고, 
// Filter와 Map을 이용해 &#39;무엇을&#39; 할지 선언적으로 작성합니다.
evens := Filter(numbers, isEven)
doubledEvens := Map(evens, double)

// 결과 확인
fmt.Println(&quot;원본 데이터:&quot;, numbers)         // 출력: [1 2 3 4 5 6] (원본이 훼손되지 않음)
fmt.Println(&quot;짝수만 필터링:&quot;, evens)         // 출력: [2 4 6]
fmt.Println(&quot;짝수를 두 배로:&quot;, doubledEvens) // 출력: [4 8 12]</code></pre><p>}</p>
<p>```</p>
<h3 id="2-2-논리형-logic-programming">2-2. 논리형 (Logic Programming)</h3>
<p>컴퓨터에게 fact와 rule을 알려주면 추론하여 질문에 대한 답을 스스로 찾아내는 프로그래밍 기법</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Golang 정리(4)-익명함수, 고차함수, 일급함수]]></title>
            <link>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC4-%EC%9D%B5%EB%AA%85%ED%95%A8%EC%88%98-%EA%B3%A0%EC%B0%A8%ED%95%A8%EC%88%98-%EC%9D%BC%EA%B8%89%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC4-%EC%9D%B5%EB%AA%85%ED%95%A8%EC%88%98-%EA%B3%A0%EC%B0%A8%ED%95%A8%EC%88%98-%EC%9D%BC%EA%B8%89%ED%95%A8%EC%88%98</guid>
            <pubDate>Sat, 11 Jul 2026 09:11:10 GMT</pubDate>
            <description><![CDATA[<h2 id="1-익명함수anonymous-function">1. 익명함수(Anonymous Function)</h2>
<p>익명함수는 단어 뜻 그대로 이름을 적지 않고 정의한 함수를 의미함.
일회성으로 사용하거나 특정 로직을 변수에 담아두고 싶을 때 사용</p>
<pre><code>// 일반적인 함수 선언 (이름 있음)
func sum(x int, y int) int { return x + y }

// 익명 함수 선언 (이름 없음)
func(x int, y int) int { return x + y }</code></pre><h2 id="2-고차함수higher-order-function">2. 고차함수(Higher-Order Function)</h2>
<p>고차함수는 함수 자체를 일반 데이터처럼 주고받는 함수를 의미함.
아래 두가지 조건 중 하나라도 만족하면 고차함수라고 부름.</p>
<ul>
<li>하나 이상의 함수를 파라미터로 받는 함수</li>
<li>함수를 결과로 반환하는 함수<pre><code>func calc(f func(int, int) int, a int, b int) int {
  result := f(a, b) // 파라미터인 함수 f를 내부에서 실행
  return result
}</code></pre></li>
</ul>
<h2 id="3-일급함수first-class-function---언어의-특성">3. 일급함수(First-Class Function) -&gt; 언어의 특성</h2>
<p>일급함수는 프로그래밍 언어의 특성이나 규칙을 의미함.
일급함수란 어떤 데이터(함수)가 정수나 문자열 같은 기본 데이터 타입과 완벽하게 동일한 대우를 받는 특성을 의미함.(이때 어떤 데이터는 일급객체라고 함)</p>
<p>일급함수 특성을 가진 언어는 함수를 다음과 같이 다룰 수 있음.</p>
<ul>
<li>변수에 함수를 할당할 수 있음</li>
<li>다른 함수에 함수를 파라미터로 전달할 수 있음</li>
<li>다른 함수의 반환값으로 함수를 사용할 수 있음</li>
</ul>
<p>즉, 위의 고차함수는 일급함수 특성을 보여줌.</p>
<h2 id="4-일급함수-특성을-보여주는-익명함수와-고차함수의-예시-코드">4. 일급함수 특성을 보여주는 익명함수와 고차함수의 예시 코드</h2>
<pre><code>package main

import (
    &quot;fmt&quot;
    &quot;strings&quot;
)

func changeValue(message *string) {
    *message = fmt.Sprintf(&quot;your name is %s&quot;, *message)
}

func addTen(value int) int {
    return value + 10
}

// ... 은 slice 로 값을 받음
func sum(values ...int) int {
    result := 0
    for _, value := range values {
        result = result + value
    }

    return result
}

func divideStrings(value string) (string, string) {
    values := strings.Split(value, &quot;_&quot;)
    return values[0], values[1]
}

// 고차함수 -&gt; 함수를 파라미터로 받으므로써 일급함수 특성을 보여줌
func calc(f func(int, int) int, a int, b int) int {
    result := f(a, b)
    return result
}

func main() {
    // 함수 호출
    fmt.Println(addTen(100))

    // slice 변수 호출
    fmt.Println(sum(1, 2, 3, 4, 5))

    // 다중 변수 반환
    prefix, suffix := divideStrings(&quot;Your_Name&quot;)
    fmt.Println(prefix, suffix)

    // 포인터 전달
    message := &quot;Ronaldo&quot;
    changeValue(&amp;message)
    fmt.Println(message)

    // 익명 함수 -&gt; 함수를 변수에 할당하므로써 일급함수 특성을 보여줌
    sum := func(x int, y int) int {
        return x + y
    }
    fmt.Println(sum(5, 10))


    add := func(i int, j int) int {
        return i + j
    }

    minus := func(i int, j int) int {
        return i - j
    }

    fmt.Println(calc(add, 10, 5))
    fmt.Println(calc(minus, 10, 5))
}
</code></pre><blockquote>
<p>코드 참고: <a href="https://wikidocs.net/164464">https://wikidocs.net/164464</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Golang 정리(3)-포인터]]></title>
            <link>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC3-%ED%8F%AC%EC%9D%B8%ED%84%B0</link>
            <guid>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC3-%ED%8F%AC%EC%9D%B8%ED%84%B0</guid>
            <pubDate>Fri, 10 Jul 2026 06:52:47 GMT</pubDate>
            <description><![CDATA[<ul>
<li>포인터: 데이터가 저장된 메모리 주소를 담는 변수를 의미</li>
<li>메모리 주소: 데이터가 저장된 메모리의 주소</li>
<li>인스턴스: 구조체에 실제 데이터 값을 담은 데이터 덩어리<pre><code># 구조체
type Obj struct { Name string, Age int }
</code></pre></li>
</ul>
<h1 id="인스턴스">인스턴스</h1>
<p>{&quot;Beckham&quot;, 11}</p>
<pre><code>
## 1. 포인터 타입 (*int)</code></pre><p>var ptr *int</p>
<pre><code>*이 int, string 같은 타입 이름 앞에 붙을 때 포인터 타입을 의미함.
이는 해당 타입의 데이터의 메모리 주소를 담는 변수임.

## 2. 주소 반환 (&amp;age)</code></pre><p>age := 11</p>
<p>// age라는 값이 저장된 메모리 주소(예: 0xc0000a4000)를 구해서 ptr에 넣음
ptr := &amp;age</p>
<pre><code>11이라는 데이터가 담긴 age 변수의 메모리 주소를 &amp;age로 가지고 올 수 있음

## 3. 역참조 (*ptr)</code></pre><p>age := 11</p>
<p>var ptr *int
ptr = &amp;age</p>
<p>fmt.Println(ptr)  // 출력: 0xc0000a4000
fmt.Println(*ptr) // 출력: 11</p>
<pre><code>*이 포인터 변수에 붙을 때 역참조를 의미함.
포인터 변수의 주소의 인스턴스를 가져옴.



전체 코드</code></pre><p>age := 11</p>
<p>var ptr *int
ptr = &amp;age</p>
<p>fmt.Println(ptr) // 출력: 0xc0000a4000
fmt.Println(*ptr) // 출력: 11</p>
<p><em>prt = 200
fmt.Println(</em>ptr) // 출력: 200</p>
<p>```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Golang 정리(2)-명명규칙]]></title>
            <link>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC2-%EB%AA%85%EB%AA%85%EA%B7%9C%EC%B9%99</link>
            <guid>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC2-%EB%AA%85%EB%AA%85%EA%B7%9C%EC%B9%99</guid>
            <pubDate>Fri, 10 Jul 2026 05:57:09 GMT</pubDate>
            <description><![CDATA[<h2 id="1-파일명-규칙">1. 파일명 규칙</h2>
<p>기본적으로 모두 소문자 사용하거나 snake_case 사용함.</p>
<ul>
<li>일반 파일명: ex) server.go, user_manager.go</li>
<li>테스트 파일명: 테스트하려는 원본 파일명 뒤에 <code>_test.go</code>를 붙여야함. ex) server_test.go</li>
</ul>
<h2 id="2-변수명-및-상수명-규칙">2. 변수명 및 상수명 규칙</h2>
<p> 변수명은 기본적으로 camelCase 사용함.</p>
<ul>
<li>scope: 스코프가 좁을수록 짧은 이름 사용. ex) 인덱스를 i로 사용</li>
<li>접근제어: 외부 패키지에서 접근 가능해야하면(public) 첫글자를 대문자로, 패키지 내부에서만 사용한다면(private) 첫글자를 소문자로 작성해야함.</li>
<li>약어: ID, URL, API 등 약어는 모두 대문자로하거나 소문자로 해야함. <ul>
<li>좋은 예) userID, apiURL, ServeHTTP</li>
<li>나쁜 예) userId, apiUrl, ServeHttp</li>
</ul>
</li>
<li>상수명도 일반 변수와 동일함. (camelCase와 외부공개여부에 따른 대소문자)</li>
</ul>
<h2 id="3-함수명-및-메서드명-규칙">3. 함수명 및 메서드명 규칙</h2>
<p>함수와 메서드 차이는 다음 링크에서 확인 가능</p>
<blockquote>
<p><a href="https://peterica.tistory.com/881">https://peterica.tistory.com/881</a></p>
</blockquote>
<ul>
<li>camelCase사용</li>
<li>첫글자 대소문자에 따른 접근제어 규칙 따름<ul>
<li>공개(Public): CalculateTotal(), NewUser()</li>
<li>비공개(Private): calculateTotal(), parseData()</li>
</ul>
</li>
</ul>
<h2 id="4-패키지명-규칙">4. 패키지명 규칙</h2>
<ul>
<li>모두 소문자 한단어로 작성 (snake_case나 camelCase 사용X)</li>
<li>패키지가 위치한 디렉토리 이름과 동일해야함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[macOS 환경에서 Golang 설치]]></title>
            <link>https://velog.io/@vinyl-bin/macOS-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Golang-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@vinyl-bin/macOS-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Golang-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Fri, 10 Jul 2026 01:46:08 GMT</pubDate>
            <description><![CDATA[<h2 id="1-golang-설치">1. Golang 설치</h2>
<pre><code>brew cleanup
brew update
brew install go</code></pre><p>아래 명령어로 설치 및 버전 확인</p>
<pre><code>go version</code></pre><h2 id="2-환경변수-설정">2. 환경변수 설정</h2>
<p>go 1.11 버전부터 모듈이 도입되어서 간결한 개발 환경 제공함
따라서 GOPATH나 GOBIN를 직접 지정하지 않아도 기본적으로 ~/go 폴더를 사용하도록 설계됨</p>
<pre><code>go env | grep -E &#39;GOPATH|GOBIN&#39;

##예시##
GOBIN=&#39;&#39;
GOPATH=&#39;/Users/ooo/go&#39;</code></pre><p>GOBIN의 경우 비어있지만 자동으로 GOPATH 뒤에 /bin을 붙여서 작동됨</p>
<p>따라서 한가지 환경변수 설정만 하면 됨.
go install로 설치된 외부 라이브러리나 개발 도구들은 $HOME/go/bin에 저장되는데 터미널에서 해당 도구가 PATH에 등록되지 않으면 command not found 에러가 발생하므로 아래와 같은 명령어로 $HOME/go/bin을 환경변수로 등록해줘야 함.</p>
<pre><code>vi ~/.zshrc

##----- 파일 맨 아래에 다음 한줄 붙여넣기
export PATH=$PATH:$HOME/go/bin
##-----


source ~/.zshrc</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Golang 정리(1)-개념]]></title>
            <link>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC1-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@vinyl-bin/Golang-%EC%A0%95%EB%A6%AC1-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Wed, 08 Jul 2026 10:01:05 GMT</pubDate>
            <description><![CDATA[<h2 id="1-컴파일-언어">1. 컴파일 언어</h2>
<p>golang은 소스코드를 기계어로 번역하여 실행파일을 만들어내는 언어임, 빠른 컴파일 속도를 가짐</p>
<h2 id="2-런타임-포함">2. 런타임 포함</h2>
<ul>
<li>런타임은 &#39;프로그램이 실행되고 있는 환경&#39; 또는 &#39;프로그램 실행을 도와주는 백그라운드 시스템&#39;을 의미</li>
<li>일반적인 Java나 JavaScript 같은 언어는 PC에 런타임을 설치해야하는 단점 존재</li>
<li>golang의 경우 런타임을 실행파일 안에 포함시킴</li>
<li>내장된 런타임 기능(가비지 컬렉션, 고루틴 스케줄링)</li>
</ul>
<h2 id="3-goroutine고루틴">3. Goroutine(고루틴)</h2>
<ul>
<li>기존 멀티스레딩: os가 제공하는 스레드를 여러 개 생성하여 동시에 여러 작업을 처리하는 방식, 하지만 context switching(컨텍스트 스위칭) 비용이 비쌈(스레드 생성 및 전환하는 것이 무거워서 서버 부하가 많음)</li>
<li>기존 비동기: 하나의 작업이 끝날 때까지 기다리지 않고 다른 작업을 먼저 처리하는 방식으로 코드가 복잡해지는 단점 있음(javascript의 경우 콜백 지옥)</li>
<li><em>Goroutine*</em></li>
<li>os 스레드 쓰지 않고 &#39;자체 경량 가상 스레드&#39;인 고루틴 사용</li>
<li>엄청 가볍고 사용이 쉬워 코드 깔끔</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[cloudflare R2 객체 스토리지]]></title>
            <link>https://velog.io/@vinyl-bin/cloudflare-R2-%EA%B0%9D%EC%B2%B4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80</link>
            <guid>https://velog.io/@vinyl-bin/cloudflare-R2-%EA%B0%9D%EC%B2%B4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80</guid>
            <pubDate>Tue, 16 Jun 2026 05:54:49 GMT</pubDate>
            <description><![CDATA[<p>cloudflare R2는 aws의 s3 기능 또는 minIO와 비슷함.</p>
<p>clouflare에서 무료로 홈페이지를 올릴 때 25mb 이상의 미디어를 올릴 수 없으므로 cloudflare R2를 사용하여 그 이상의 미디어를 올림.</p>
<h2 id="1-r2-버킷-생성">1. R2 버킷 생성</h2>
<p>cloudflare홈 - 빌드 - 스토리지 및 데이터베이스 - R2 객체 스토리지 - 개요</p>
<p>0원 구독 확인 후 파란색 버튼 &#39;버킷생성&#39; 클릭하여 버킷 생성</p>
<h2 id="2-버킷-공개-설정">2. 버킷 공개 설정</h2>
<p>생성한 버킷 클릭 - 설정 - 사용자 설정 도메인 - 추가</p>
<p>기존 cloudflare에 등록된 홈페이지 도메인에 media를 붙여 서브도메인 활용</p>
<blockquote>
<p>만약 a.co.kr이 홈페이지 도메인이라면 사용자 설정 도메인은 media.a.co.kr로 설정</p>
</blockquote>
<p>이후 <a href="https://media.a.co.kr/">https://media.a.co.kr/</a> 을 붙여 해당 미디어 사용 가능</p>
<blockquote>
<p>만약 temp라는 버킷을 생성 후 해당 버킷에 도메인을 media.a.co.kr로 설정했고 temp라는 버킷에 /(루트)에 video라는 폴더를 생성하고 그 아래에 a.png를 저장했다면
-&gt; <a href="https://media.a.co.kr/video/a.png">https://media.a.co.kr/video/a.png</a> 로 해당 미디어 사용 가능</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[cloudflare 정적페이지 github 연동 배포 및 도메인 이전]]></title>
            <link>https://velog.io/@vinyl-bin/cloudflare-%EC%A0%95%EC%A0%81%ED%8E%98%EC%9D%B4%EC%A7%80-github-%EC%97%B0%EB%8F%99-%EB%B0%B0%ED%8F%AC-%EB%B0%8F-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%9D%B4%EC%A0%84</link>
            <guid>https://velog.io/@vinyl-bin/cloudflare-%EC%A0%95%EC%A0%81%ED%8E%98%EC%9D%B4%EC%A7%80-github-%EC%97%B0%EB%8F%99-%EB%B0%B0%ED%8F%AC-%EB%B0%8F-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%9D%B4%EC%A0%84</guid>
            <pubDate>Tue, 09 Jun 2026 06:15:11 GMT</pubDate>
            <description><![CDATA[<h2 id="1-직접-업로드-방식에서-github-연동-배포로-페이지-새로-생성">1. 직접 업로드 방식에서 github 연동 배포로 페이지 새로 생성</h2>
<h4 id="1-1-github-repository-생성">1-1. github repository 생성</h4>
<p>정적 페이지를 github repository에 저장
이때 메인 페이지는 index.html로 설정</p>
<h4 id="1-2-cloudflare에서-페이지-설정">1-2. cloudflare에서 페이지 설정</h4>
<ul>
<li>계정 홈 - 컴퓨트 - workers 및 pages - 응용프로그램 생성</li>
<li>정적 페이지이므로 아래 &#39;Pages를 배포하려고 하십니까? 시작&#39; 선택
<img src="https://velog.velcdn.com/images/vinyl-bin/post/7dd04ddd-9261-4abd-88dd-6ad03f64281b/image.png" alt=""></li>
<li>기존 git 레포지토리 가져오기 선택</li>
</ul>
<h2 id="2-도메인-이전">2. 도메인 이전</h2>
<h4 id="2-1-기존-도메인-연결된-페이지-연결-해제">2-1. 기존 도메인 연결된 페이지 연결 해제</h4>
<ul>
<li>계정 홈 - 컴퓨트 - workers 및 pages - 해당 응용프로그램 페이지 선택 - 사용자 설정 도메인 - 해당 도메인 오른쪽 점 세개 - 도메인 제거
<img src="https://velog.velcdn.com/images/vinyl-bin/post/dc15f36b-e4b4-41cb-8394-a22369ce8100/image.png" alt=""></li>
</ul>
<h4 id="2-2-새로운-페이지에-기존-도메인-연결">2-2. 새로운 페이지에 기존 도메인 연결</h4>
<ul>
<li>계정 홈 - 컴퓨트 - workers 및 pages - 새로운 응용프로그램 페이지 선택 - 사용자 설정 도메인 - 사용자 설정 도메인 설정 버튼 클릭</li>
<li>기존 cloudflare에서 설정한 도메인 입력
<img src="https://velog.velcdn.com/images/vinyl-bin/post/3ecc475e-cfaf-4645-8f61-c220963de4cc/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[commenthat CI/CD] 3. k3s 서버에 ghcr secret과 logrotate 설정]]></title>
            <link>https://velog.io/@vinyl-bin/commenthat-CICD-3.-k3s-%EC%84%9C%EB%B2%84%EC%97%90-ghcr-secret%EA%B3%BC-logrotate-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@vinyl-bin/commenthat-CICD-3.-k3s-%EC%84%9C%EB%B2%84%EC%97%90-ghcr-secret%EA%B3%BC-logrotate-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Fri, 05 Jun 2026 06:44:31 GMT</pubDate>
            <description><![CDATA[<h2 id="1-ghcr-secret-설정">1. ghcr secret 설정</h2>
<p>ghcr private image를 가져오기 위해 설정</p>
<p>k3s 서버에서 아래 명령어 입력</p>
<pre><code>kubectl create secret docker-registry ghcr-org-secret --docker-server=ghcr.io --docker-username=&lt;본인의_깃허브_아이디&gt; --docker-password=&lt;발급받은_토큰값&gt; --docker-email=&lt;본인의_깃허브_이메일&gt;</code></pre><p>확인 명령어</p>
<pre><code>kubectl get secret ghcr-org-secret</code></pre><p>이때 private 이미지를 pull하는 디플로이먼트 yaml 파일에서 imagePullSecrets.name를 꼭 위에서 설정한 ghcr-org-secret 으로 설정해야 함</p>
<p>** 예시 **</p>
<pre><code>apiVersion: apps/v1
kind: Deployment
metadata:
 name: auto-processor 
spec:
 replicas: 1
 selector:
   matchLabels:
     app: auto-processor 
 template:
   metadata:
     labels:
       app: auto-processor 
   spec:
     imagePullSecrets:
       - name: ghcr-org-secret  # -&gt; 이 부분
     containers:
       - name: auto-processor
         image: ghcr.io/cookie-k3/server-autoprocessor:latest 
         imagePullPolicy: Always
         resources:
           limits:
             memory: &quot;2Gi&quot;
         env:
           - name: SPRING_PROFILES_ACTIVE
             value: &quot;prod&quot;
           - name: TZ
             value: Asia/Seoul
         ports:
           - containerPort: 8080</code></pre><blockquote>
<p>깃허브 인증 토큰은 개인 또는 조직 레포지토리 상관없이 아래와 같이 설정
개인 github 계정 settings - Developer settings - Personal access tokens (classic)</p>
</blockquote>
<h2 id="2-logrotate-설정">2. logrotate 설정</h2>
<p>나중에 여러 파드를 띄워두게 될 때 컨테이너 로그 용량이 문제가 될 수 있으므로 로그를 주기적으로 지우는 logrotate 설정하기</p>
<pre><code>sudo vim /etc/logrotate.d/k3s-container-logs

# 아래 내용 입력 후 저장
/var/lib/docker/containers/*/*.log {
    size 100M
    rotate 1
    missingok
    notifempty
    nocompress
    copytruncate
}</code></pre><ul>
<li>로그 파일이 100MB를 넘으면 즉시 회전</li>
<li>최대 1개의 백업 파일만 보관 (log.1만 유지, 그 이상은 삭제)</li>
<li>로그 파일이 없어도 에러 없이 무시</li>
<li>로그 파일이 비어있으면 건너뜀</li>
<li>로그 압축 안 함 (압축 원하면 compress로 변경)</li>
<li>로그 잘라내되, 컨테이너는 그대로 계속 쓰게 함 (필수)</li>
</ul>
<p>아래 명령어로 테스트 후 오류없으면 정상작동</p>
<pre><code>sudo logrotate -f /etc/logrotate.d/k3s-container-logs</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[commenthat CI/CD] 2. k3s 및 ArgoCD, Grafana, Prometheus 설치]]></title>
            <link>https://velog.io/@vinyl-bin/commenthat-CICD-2.-k3s-%EB%B0%8F-ArgoCD-Grafana-Prometheus-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@vinyl-bin/commenthat-CICD-2.-k3s-%EB%B0%8F-ArgoCD-Grafana-Prometheus-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Thu, 04 Jun 2026 09:07:13 GMT</pubDate>
            <description><![CDATA[<h2 id="1-k3s-설치">1. k3s 설치</h2>
<p>앞서 설정한 vm에 원격접속 후 진행</p>
<pre><code># 1. 방화벽 확인
sudo ufw status 
#inactive라고 나오는 경우 바로 3번으로, active라고 나오면 2번 방화벽 설정하기

# 2. 방화벽 설정
sudo ufw allow 6443/tcp
sudo ufw allow from 10.42.0.0/16
sudo ufw allow from 10.43.0.0/16

# 3. 스왑 메모리 끄기 (쿠버네티스 필수 작업)
sudo swapoff -a
sudo sed -i &#39;/swap/s/^/#/&#39; /etc/fstab

# 4. K3s 올인원 설치
curl -sfL https://get.k3s.io | sh -
</code></pre><h2 id="2argocd">2.ArgoCD</h2>
<h3 id="2-1-설치">2-1. 설치</h3>
<pre><code>### 1. 설치  ###
# ArgoCD 네임스페이스 생성
kubectl create namespace argocd

# namespace 확인
kubectl get namespaces

# ArgoCD 설치
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# 확인
kubectl get all -n argocd


### 2. 초기 비밀번호 확인 -&gt; 나중에 비밀번호 바꾸기 위해 기록해두기 ###
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=&quot;{.data.password}&quot; | base64 -d


### 3.argocd 접속을 위한 포트포워딩  ###
nohup kubectl port-forward --address 0.0.0.0 svc/argocd-server -n argocd 8089:443 &gt; /dev/null 2&gt;&amp;1 &amp;
</code></pre><h3 id="2-2-접속">2-2. 접속</h3>
<h4 id="1-k3s-서버와-같은-네트워크인-경우">(1) k3s 서버와 같은 네트워크인 경우</h4>
<p>아래 url를 통해 브라우저로 argocd 페이지 접속
https://{k3s서버ip}:8089</p>
<h4 id="2-k3s-서버와-다른-네트워크인-경우">(2) k3s 서버와 다른 네트워크인 경우</h4>
<p>현재 저는 아래와 같은 환경으로 이루어져 있으므로</p>
<pre><code>[외부]
   └─ 내 노트북 (https://localhost:8089)
         │
         ▼ (SSH Tunneling-&gt; 공인IP:30022)
[집 내부망 - Gateway]
   └─ 외부 접속용 서버 (A 네트워크 공인 IP)
         │
         ▼ (내부 사설망 통신-&gt; 사설IP:8089)
[Proxmox VM]
   └─ k3s 서버 (A 네트워크 사설 IP)
         │
         ▼ (kubectl port-forward 8089 -&gt; 443)
[k3s Cluster]
   └─ ArgoCD Pod (최종 목적지)</code></pre><p>아래 명령어로 외부 네트워크에서 ssh 터널링을 유지하여</p>
<pre><code>ssh -p 30022 root@{게이트웨이ip} -L 8089:{k3s서버ip}:8089</code></pre><p><a href="https://localhost:8089">https://localhost:8089</a> url를 통해 argocd 페이지 접속</p>
<h3 id="2-3-비밀번호-수정">2-3. 비밀번호 수정</h3>
<h4 id="1-초기-로그인">(1) 초기 로그인</h4>
<p>username: admin
password: 앞서 2-1에서 확인한 비밀번호</p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/6f92e15f-5bc8-41b1-9456-57287918abe3/image.png" alt=""></p>
<p>User Info - Update Password에서 비밀번호 수정
<img src="https://velog.velcdn.com/images/vinyl-bin/post/49966f9e-d1ee-4898-99a7-f78ba0b45dd4/image.png" alt=""></p>
<h2 id="3grafana-prometheus">3.grafana, prometheus</h2>
<h3 id="3-1-helm으로-grafana-prometheus-설치">3-1. helm으로 grafana, prometheus 설치</h3>
<pre><code># 1. helm 설치
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# 2. grafana, prometheus 레포지토리 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# 3. 네임스페이스 및 환경변수 추가
kubectl create namespace monitoring

# root 사용자의 bash 설정 파일 끝에 환경 변수 추가
echo &#39;export KUBECONFIG=/etc/rancher/k3s/k3s.yaml&#39; &gt;&gt; ~/.bashrc

# 변경사항 바로 새로고침
source ~/.bashrc

# 4. 설치 (kube-prometheus-stack이 prometheus와 grafana가 묶여있는 세트)
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --set grafana.adminPassword=패스워드 \
  --set grafana.service.type=NodePort \
  --set grafana.service.nodePort=30000 \
  --set prometheus.service.type=NodePort \
  --set prometheus.service.nodePort=30001
</code></pre><h3 id="3-2-접속">3-2. 접속</h3>
<h4 id="1-k3s-서버와-같은-네트워크인-경우-1">(1) k3s 서버와 같은 네트워크인 경우</h4>
<p>아래 url를 통해 브라우저로 페이지 접속
grafana -&gt; https://{k3s서버ip}:30000
prometheus -&gt; https://{k3s서버ip}:30001</p>
<h4 id="2-k3s-서버와-다른-네트워크인-경우-1">(2) k3s 서버와 다른 네트워크인 경우</h4>
<p>현재 저는 아래와 같은 환경으로 이루어져 있으므로</p>
<pre><code>[외부]
   └─ 내 노트북 
         │
         ▼ (SSH Tunneling-&gt; 공인IP:30022)
[집 내부망 - Gateway]
   └─ 외부 접속용 서버 (A 네트워크 공인 IP)
         │
         ▼ 
[Proxmox VM]
   └─ k3s 서버 (A 네트워크 사설 IP)
         │
         ▼ 
[k3s Cluster]
   └─ (최종 목적지)</code></pre><p>아래 명령어로 외부 네트워크에서 ssh 터널링을 유지하여</p>
<pre><code>ssh -p 30022 root@{게이트웨이ip} -L 30000:{k3s서버ip}:30000 -L 30001:{k3s서버ip}:30001</code></pre><p><a href="https://localhost:30000%EA%B3%BC">https://localhost:30000과</a> <a href="https://localhost:30001">https://localhost:30001</a> url를 통해 grafana와 prometheus 페이지 접속
<img src="https://velog.velcdn.com/images/vinyl-bin/post/31a47296-9201-4d9c-8b12-e7a98d8d4f94/image.png" alt="">
<img src="https://velog.velcdn.com/images/vinyl-bin/post/58c429be-647e-451b-92fe-6c450de5af48/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[commenthat CI/CD] 1. Github Actions 설정]]></title>
            <link>https://velog.io/@vinyl-bin/commenthat-CICD-1.-Github-Actions-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@vinyl-bin/commenthat-CICD-1.-Github-Actions-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 01 Jun 2026 08:04:27 GMT</pubDate>
            <description><![CDATA[<h2 id="1-commenthat-프로젝트-구조">1. commenthat 프로젝트 구조</h2>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/a1e97624-6635-4fbe-918c-78b35f3870ad/image.png" alt=""></p>
<p>MSA 구조로 설계되어 4가지 모듈이 있으며, 이 4가지 모듈을 CICD 파이프라인을 통해 자동으로 k3s로 배포하도록 할 예정</p>
<p>예상 파이프라인은 아래와 같음
<img src="https://velog.velcdn.com/images/vinyl-bin/post/32b7dcd5-4ef4-4912-afea-5395a2d4205f/image.png" alt=""></p>
<p>더 자세한 설명: <a href="https://github.com/cookie-k3/commentHAT-showcase">https://github.com/cookie-k3/commentHAT-showcase</a></p>
<h2 id="2-github-actions-설정">2. Github Actions 설정</h2>
<p>github 해당 레포지토리 - Actions - New workflow - set up a workflow yourself 클릭</p>
<h3 id="viewservicefront">ViewServiceFront</h3>
<p>.github/workflows/ci-cd.yml</p>
<pre><code>name: React CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  NODE_VERSION: &#39;18&#39;

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    # main 브랜치 푸시 시에만 실행
    if: github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;
    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: &#39;npm&#39;

    - name: Install dependencies
      run: npm ci  # package.json의 모든 의존성이 자동으로 설치됨

    - name: Build React app
      run: npm run build
      env:
        CI: false  # Warning을 에러로 처리하지 않음

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=sha,prefix=main-,format=short
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        build-args: |
          BUILDKIT_INLINE_CACHE=1

  update-manifest:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == &#39;refs/heads/main&#39;

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        fetch-depth: 0

    - name: Update Kubernetes manifests
      run: |
        # 새로운 이미지 태그 추출
        NEW_TAG=$(echo &quot;${{ needs.build-and-push.outputs.image-tag }}&quot; | grep -E &quot;main-[a-f0-9]{7}&quot; | head -1 | cut -d&#39;:&#39; -f2)
        echo &quot;New tag: $NEW_TAG&quot;

        # kustomization.yaml에서 이미지 태그 업데이트
        sed -i &quot;s|newTag: .*|newTag: ${NEW_TAG}|g&quot; k8s/kustomization.yaml

        # 변경사항 확인
        git diff k8s/kustomization.yaml

        # 변경사항 커밋 및 푸시
        git config --local user.email &quot;action@github.com&quot;
        git config --local user.name &quot;GitHub Action&quot;
        git add k8s/kustomization.yaml

        if git diff --staged --quiet; then
          echo &quot;No changes to commit&quot;
        else
          git commit -m &quot;Deploy: Update image tag to ${NEW_TAG}&quot;
          git push
        fi</code></pre><h3 id="viewserviceback">ViewServiceBack</h3>
<p>.github/workflows/ci-cd.yml</p>
<pre><code>name: Spring Boot CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: cookie-k3/server-webBack  # 실제 이미지명과 일치
  JAVA_VERSION: &#39;17&#39;

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    # main 브랜치 푸시/머지 시에만 실행
    if: github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;
    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: ${{ env.JAVA_VERSION }}
        distribution: &#39;temurin&#39;

    - name: Cache Gradle packages
      uses: actions/cache@v4
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles(&#39;**/*.gradle*&#39;, &#39;**/gradle-wrapper.properties&#39;) }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    - name: Make gradlew executable
      run: chmod +x gradlew

    - name: Build with Gradle (JAR 파일 생성)
      run: |
        ./gradlew build -x test --no-daemon

        # JAR 파일이 올바른 위치에 있는지 확인
        ls -la build/libs/

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=sha,prefix=main-,format=short
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64  # ARM64 제외 (안정성)
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        build-args: |
          BUILDKIT_INLINE_CACHE=1

  update-manifest:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == &#39;refs/heads/main&#39;

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        fetch-depth: 0

    - name: Update Kubernetes manifests
      run: |
        # 새로운 이미지 태그 추출
        NEW_TAG=$(echo &quot;${{ needs.build-and-push.outputs.image-tag }}&quot; | grep -E &quot;main-[a-f0-9]{7}&quot; | head -1 | cut -d&#39;:&#39; -f2)
        echo &quot;New tag: $NEW_TAG&quot;

        # kustomization.yaml에서 이미지 태그 업데이트
        sed -i &quot;s|newTag: .*|newTag: ${NEW_TAG}|g&quot; k8s/kustomization.yaml

        # 변경사항 확인
        git diff k8s/kustomization.yaml

        # Git 설정 및 커밋
        git config --local user.email &quot;action@github.com&quot;
        git config --local user.name &quot;GitHub Action&quot;
        git add k8s/kustomization.yaml

        if git diff --staged --quiet; then
          echo &quot;No changes to commit&quot;
        else
          git commit -m &quot;Deploy: Update Spring Boot image tag to ${NEW_TAG}&quot;
          git push
        fi</code></pre><h3 id="analysisservice">AnalysisService</h3>
<p>.github/workflows/ci-cd.yml</p>
<pre><code>name: Python Analysis Service CI/CD

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: cookie-k3/server-analysis
  PYTHON_VERSION: &#39;3.9&#39;

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    # main 브랜치 푸시 시에만 실행
    if: github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;
    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Verify required files
      run: |
        echo &quot;프로젝트 파일 확인...&quot;
        ls -la
        echo &quot;requirements.txt 확인:&quot;
        head -10 requirements.txt || echo &quot;requirements.txt 없음&quot;
        echo &quot;run_project.sh 확인:&quot;
        ls -la run_project.sh || echo &quot;run_project.sh 없음&quot;

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=sha,prefix=main-,format=short
          type=raw,value=latest,enable={{is_default_branch}}
          type=raw,value=miniserver  # 기존 태그 유지

    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64  # Python + PyTorch는 ARM64 복잡함
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        build-args: |
          BUILDKIT_INLINE_CACHE=1

  update-manifest:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == &#39;refs/heads/main&#39;
    permissions:
      contents: write
      actions: read

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        fetch-depth: 0

    - name: Update Kubernetes manifests
      run: |
        # 새로운 이미지 태그 추출
        NEW_TAG=$(echo &quot;${{ needs.build-and-push.outputs.image-tag }}&quot; | grep -E &quot;main-[a-f0-9]{7}&quot; | head -1 | cut -d&#39;:&#39; -f2)
        echo &quot;New tag: $NEW_TAG&quot;

        # kustomization.yaml에서 이미지 태그 업데이트
        sed -i &quot;s|newTag: .*|newTag: ${NEW_TAG}|g&quot; k8s/kustomization.yaml

        # 변경사항 확인
        git diff k8s/kustomization.yaml

        # Git 설정 및 커밋
        git config --local user.email &quot;action@github.com&quot;
        git config --local user.name &quot;GitHub Action&quot;
        git add k8s/kustomization.yaml

        if git diff --staged --quiet; then
          echo &quot;No changes to commit&quot;
        else
          git commit -m &quot;Deploy: Update Python analysis service image tag to ${NEW_TAG}&quot;
          git push
        fi</code></pre><h3 id="autoprocessor">AutoProcessor</h3>
<p>.github/workflows/ci-cd.yml</p>
<pre><code>name: Spring Boot CI/CD Pipeline (No Test)

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  JAVA_VERSION: &#39;17&#39;

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    # 테스트 job 제거로 인해 조건 단순화
    if: github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;
    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: ${{ env.JAVA_VERSION }}
        distribution: &#39;temurin&#39;

    - name: Cache Gradle packages
      uses: actions/cache@v4
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles(&#39;**/*.gradle*&#39;, &#39;**/gradle-wrapper.properties&#39;) }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=sha,prefix=main-,format=short
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v5
      with:
        context: .
        platforms: linux/amd64
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        build-args: |
          BUILDKIT_INLINE_CACHE=1

  update-manifest:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == &#39;refs/heads/main&#39;

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        fetch-depth: 0

    - name: Update Kubernetes manifests
      run: |
        # 새로운 이미지 태그 추출 (main-short-sha 형태)
        NEW_TAG=$(echo &quot;${{ needs.build-and-push.outputs.image-tag }}&quot; | grep -E &quot;main-[a-f0-9]{7}&quot; | head -1 | cut -d&#39;:&#39; -f2)
        echo &quot;New tag: $NEW_TAG&quot;

        # kustomization.yaml에서 이미지 태그 업데이트
        sed -i &quot;s|newTag: .*|newTag: ${NEW_TAG}|g&quot; k8s/kustomization.yaml

        # 변경사항 확인
        git diff k8s/kustomization.yaml

        # 변경사항 커밋 및 푸시
        git config --local user.email &quot;action@github.com&quot;
        git config --local user.name &quot;GitHub Action&quot;
        git add k8s/kustomization.yaml

        if git diff --staged --quiet; then
          echo &quot;No changes to commit&quot;
        else
          git commit -m &quot;Deploy: Update image tag to ${NEW_TAG}&quot;
          git push
        fi</code></pre><p>도커이미지를 만드는 build-and-push와 argocd에서 감지하는 manifest 파일을 수정하는 update-manifest job으로 나뉨 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Proxmox VM 생성]]></title>
            <link>https://velog.io/@vinyl-bin/Proxmox-VM-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@vinyl-bin/Proxmox-VM-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Wed, 13 May 2026 07:12:51 GMT</pubDate>
            <description><![CDATA[<h2 id="리눅스-iso-이미지-등록">리눅스 iso 이미지 등록</h2>
<p><a href="https://ubuntu.com/download/server/thank-you?version=26.04&amp;architecture=amd64&amp;lts=true">https://ubuntu.com/download/server/thank-you?version=26.04&amp;architecture=amd64&amp;lts=true</a></p>
<ol>
<li>위 링크로 &#39;download now&#39; 하이퍼링크를 클릭하여 server용 ubuntu 26.04 LTS Download 링크 복사
<img src="https://velog.velcdn.com/images/vinyl-bin/post/10ad85df-8c3d-4635-82d5-d4d17246a69e/image.png" alt=""></li>
</ol>
<ol start="2">
<li>Proxmox 페이지에서 &#39;Datacenter-proxmox-local(proxmox)-ISO Images-Download from URL&#39; 클릭</li>
</ol>
<p>URL: 위에서 복사한 링크 붙여넣기
Filename: 식별 가능하도록 직접 지정</p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/a1988f7e-bcf4-456c-bf58-ffc2496deeea/image.png" alt=""></p>
<p>다운로드 버튼 클릭</p>
<p>로그에서 &#39;TASK OK&#39;가 뜨면 창 닫고 종료</p>
<h2 id="vm-생성">VM 생성</h2>
<p>상단 Create VM 버튼 클릭</p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/a488a8e8-d96d-4d52-b09d-e643c25d862e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/7712bbd4-1bba-4cc5-bf00-aed723804faf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/3557ef03-07f2-4932-8d74-388a75b00f8e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/c1a252c9-e53a-4a3d-b168-e3142c3a5bd0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/cd6d58e5-1054-48a1-96d6-4a2d0eb74e80/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/b75c11ac-b931-4984-b29b-62f79694e143/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/d5b233fd-b6b6-461d-a2bc-03144987420c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/1cf59c73-a97b-4523-b4d7-5af1d3698ed2/image.png" alt=""></p>
<h2 id="우분투-설정">우분투 설정</h2>
<p>생성된 vm를 더블클릭하여 콘솔을 연 후 우분투 설정</p>
<p>네트워크의 경우 vmbr2(10.0.12.1/24)를 사용했으므로 아래 사진과 같이 설정함
<img src="https://velog.velcdn.com/images/vinyl-bin/post/77bfce0d-bcec-45a8-98c3-6204592f0854/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/10c94f7e-7a89-4069-8823-a09e8219d1a3/image.png" alt=""></p>
<p>proxy configuration은 비워두고 넘어가기</p>
<p>마지막에 아래와 같은 화면 나오면 &#39;Reboot Now&#39; 클릭하기
<img src="https://velog.velcdn.com/images/vinyl-bin/post/270856f4-a9b6-46ae-b779-bea69a473fcb/image.png" alt=""></p>
<p>reboot 후 &#39;Please remove the installation medium, then press ENTER&#39;가 뜨면 잘 설치된 것으로 재설치 방지를 위해 엔터를 누르면 됨</p>
<pre><code>만약 &#39;Please remove the installation medium, then press ENTER&#39;가 안뜨고 다시 재설치 화면이 나온다면 수동으로 ISO 미디어 제거해야함

- vm 강제 종료
- proxmox 홈화면에서 해당 vm 아이콘 클릭
- Hardware 탭 클릭
- CD/DVD Drive (ide2) 항목을 더블 클릭하거나 선택 후 Edit 누름
- Do not use any media를 선택하고 OK 누름

그 후 콘솔로 돌아와 엔터를 누르거나 VM을 Reboot 시킴</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Proxmox 가상 네트워크 구축 (Bridge와 NAT)]]></title>
            <link>https://velog.io/@vinyl-bin/Proxmox-%EA%B0%80%EC%83%81-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%B6%95-Bridge%EC%99%80-NAT</link>
            <guid>https://velog.io/@vinyl-bin/Proxmox-%EA%B0%80%EC%83%81-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B5%AC%EC%B6%95-Bridge%EC%99%80-NAT</guid>
            <pubDate>Wed, 13 May 2026 06:39:00 GMT</pubDate>
            <description><![CDATA[<h2 id="가상머신에서-네트워크-종류">가상머신에서 네트워크 종류</h2>
<ul>
<li>Host-Only:외부 인터넷과 완전히 단절된 폐쇄적 네트워크로, 가상머신 내에 존재하는 머신들과 호스트PC끼리만 통신 가능</li>
<li>Bridge: 가상머신이 호스트PC의 물리 랜카드에 직접 매달려 있는 형태로, 공유기(라우터)로부터 독립적인 IP 주소를 할당받음. 공유기를 통해 외부 네트워크 통신 가능</li>
<li>NAT: 가상머신은 호스트PC로부터 사설 ip를 할당받고, 외부로 나갈 때는 호스트PC의 IP로 통신. 호스트PC가 가상 공유기 역할함. 외부에서 vm으로 들어오려면 포트포워딩 필수 </li>
</ul>
<h2 id="osi-7계층과-l2스위치-l3라우터">OSI 7계층과 L2(스위치), L3(라우터)</h2>
<h4 id="1-osi-7계층">1. OSI 7계층</h4>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/5c73bd5a-8036-4e20-babb-c8436575d76b/image.png" alt=""></p>
<h4 id="2-l2-2layer-datalink">2. L2 (2Layer, DataLink)</h4>
<ul>
<li>데이터링크 계층, 전송 단위: 프레임</li>
<li>대표 장비: 브릿지(옛날 장비), 스위치(요즘 장비)<pre><code>인터넷
  │
 [WAN 포트]
 라우터
 [LAN 포트]
  │
 [업링크 포트]
 스위치
 [포트 1~24]
  │ │ │ │
    랜선
  │ │ │ │
 PC들</code></pre></li>
<li>식별자: MAC 주소 -&gt; 네트워크 카드(NIC)에 MAC 주소가 정해짐</li>
<li>같은 네트워크 대역 안에서 장비들끼리 데이터 주고 받음, 같은 대역일때는 라우터를 거치지 않고 MAC 주소만으로 스위치를 통해 데이터를 주고 받음(그게 훨씬 빠름, 또한 스위치가 라우터에 비해 상대적으로 포트 개수가 많으므로 라우터에 스위치를 연결해서 사용하면 비용이 절감됨)</li>
</ul>
<h4 id="3-l3-3layer-network">3. L3 (3Layer, Network)</h4>
<ul>
<li>네트워크 계층, 전송단위: 패킷</li>
<li>대표 장비: 라우터, L3 스위치</li>
<li>식별자: IP 주소</li>
<li>서로 다른 네트워크에서 데이터 주소 받음</li>
</ul>
<h2 id="proxmox-설정">Proxmox 설정</h2>
<h4 id="1-bridge-방식으로-가상-스위치인-vmbr0-설정">1. bridge 방식으로 가상 스위치인 vmbr0 설정</h4>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/b615806b-d692-4f22-9a55-09e39ff160f8/image.png" alt=""></p>
<h4 id="2-nat-방식으로-가상-스위치인-vmbr2-설정">2. NAT 방식으로 가상 스위치인 vmbr2 설정</h4>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/e23ef40a-7710-4f7c-89d8-e9a5fa778662/image.png" alt=""></p>
<p>이후 Apply Configuration 클릭해서 적용하기</p>
<h4 id="3-nat-방식-적용을-위해-interfaces-파일-수정">3. NAT 방식 적용을 위해 interfaces 파일 수정</h4>
<ul>
<li><p>&#39;Datacenter-proxmox-System-Network-shell&#39; 클릭하여 호스트pc 접속</p>
</li>
<li><p>interfaces 파일 수정 </p>
<pre><code>vi /etc/network/interfaces</code></pre><pre><code># vmbr2 부분을 다음과 같이 수정
</code></pre></li>
</ul>
<p>auto vmbr2
iface vmbr2 inet static
        address 10.0.12.1/24
        bridge-ports none
        bridge-stp off
        bridge-fd 0
#k8s 전용 NAT 설정
        # 이 줄이 반드시 들어가야 인터넷이 됨
        post-up echo 1 &gt; /proc/sys/net/ipv4/ip_forward
        post-up iptables -t nat -A POSTROUTING -s &#39;10.0.12.0/24&#39; -o vmbr0 -j MASQUERADE
        post-down iptables -t nat -D POSTROUTING -s &#39;10.0.12.0/24&#39; -o vmbr0 -j MASQUERADE</p>
<pre><code>
- 네트워크 재시작하여 변경사항 반영하기</code></pre><p>systemctl restart networking</p>
<p>```</p>
<blockquote>
<p>proxmox vm 생성하는 게시물에 이번에 만든 가상 네트워크를 활용하는 방법 나와있습니다.<br><a href="https://velog.io/@vinyl-bin/Proxmox-VM-%EC%83%9D%EC%84%B1">https://velog.io/@vinyl-bin/Proxmox-VM-%EC%83%9D%EC%84%B1</a></p>
</blockquote>
<hr>
<p><strong>참고</strong></p>
<ul>
<li><a href="https://hg2lee.tistory.com/entry/Network-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0%EC%97%90%EC%84%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EC%9D%98-%EC%A2%85%EB%A5%98-Bridged-NAT-Host-onl">https://hg2lee.tistory.com/entry/Network-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0%EC%97%90%EC%84%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EC%9D%98-%EC%A2%85%EB%A5%98-Bridged-NAT-Host-onl</a></li>
<li><a href="https://shlee0882.tistory.com/110">https://shlee0882.tistory.com/110</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSH Key 설정]]></title>
            <link>https://velog.io/@vinyl-bin/SSH-Key-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@vinyl-bin/SSH-Key-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 06 May 2026 07:16:38 GMT</pubDate>
            <description><![CDATA[<h2 id="ssh-key-설명">SSH Key 설명</h2>
<p>SSH 패스키 설정은 원격서버 ssh 접속 시 패스워드 로그인을 차단하고 등록된 key로만 접속하도록 설정하는 것입니다.</p>
<p>ssh key는 private key와 public key로 나누어져 있습니다.</p>
<ul>
<li>private key: 로컬pc 위치, 절대 타인에게 노출X, 확장자X<ul>
<li>단 private key 권한은 600이어야 함.(chmod 600 ~/.ssh/id_ed25519)</li>
</ul>
</li>
<li>public key: 원격서버 ~/.ssh/authorized_keys 파일에 한 줄로 위치, 공개되어도 상관없음, .pub확장자</li>
</ul>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/fb6a370e-59d2-432b-a15f-5ff34affa439/image.png" alt=""></p>
<h2 id="ssh-key-설정">SSH Key 설정</h2>
<h3 id="로컬-pc-설정">로컬 PC 설정</h3>
<h4 id="1-key-생성-private-public-모두-생성됨">1. key 생성 (private, public 모두 생성됨)</h4>
<p>보통 기본경로인 ~/.ssh/id_ed25519 에 생성됨</p>
<pre><code>ssh-keygen -t ed25519 -C &quot;내이름_또는_이메일&quot;</code></pre><ul>
<li>-t ed25519 : 알고리즘 타입</li>
<li>-C &quot;주석&quot; : 식별용 주석 </li>
</ul>
<h4 id="2-원격서버로-public-key-전송">2. 원격서버로 public key 전송</h4>
<p>아래 방법 중 하나 선택해서 사용</p>
<p><strong>2-1. 직접 복사해서 원격서버 ~/.ssh/authorized_keys 파일 다음 줄에 붙여넣기 후 원격서버에서 권한 설정</strong></p>
<pre><code>chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys</code></pre><p><strong>2-2. 로컬pc에서 ssh-copy-id 명령어 사용</strong></p>
<pre><code>ssh-copy-id -i ~/.ssh/id_ed25519.pub 사용자계정@서버IP</code></pre><h3 id="원격-서버-설정">원격 서버 설정</h3>
<h4 id="1-ssh-설정-변경">1. ssh 설정 변경</h4>
<pre><code>sudo vi /etc/ssh/sshd_config</code></pre><pre><code>PubkeyAuthentication yes       # 공개키 인증 허용
PasswordAuthentication no      # 패스워드 인증 비활성화
ChallengeResponseAuthentication no  # 시도-응답 인증 비활성화</code></pre><pre><code>sudo sshd -t              # 문법 검사 (아무 출력 없으면 정상)
sudo systemctl restart ssh</code></pre><h2 id="원격접속-테스트">원격접속 테스트</h2>
<pre><code>ssh -i ./id_ed25519 사용자계정@서버IP</code></pre><p>또는</p>
<pre><code>ssh -i ./id_ed25519 -p 포트 사용자계정@서버IP  #포트포워딩 했을 경우</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[딥러닝 가중치 초기화 (Xavier, He 초기화)]]></title>
            <link>https://velog.io/@vinyl-bin/%EB%94%A5%EB%9F%AC%EB%8B%9D-%EA%B0%80%EC%A4%91%EC%B9%98-%EC%B4%88%EA%B8%B0%ED%99%94-Xavier-He-%EC%B4%88%EA%B8%B0%ED%99%94</link>
            <guid>https://velog.io/@vinyl-bin/%EB%94%A5%EB%9F%AC%EB%8B%9D-%EA%B0%80%EC%A4%91%EC%B9%98-%EC%B4%88%EA%B8%B0%ED%99%94-Xavier-He-%EC%B4%88%EA%B8%B0%ED%99%94</guid>
            <pubDate>Thu, 08 Jan 2026 07:35:26 GMT</pubDate>
            <description><![CDATA[<h2 id="1-가중치-초기화-필요성">1. 가중치 초기화 필요성</h2>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/1460690d-dfb6-4b66-9660-62973d4d9b72/image.png" alt=""></p>
<ul>
<li>위 그림과 같이 가중치를 어떻게 초기화 하냐에 따라 많은 성능 차이가 나므로 가중치 초기화는 중요</li>
<li>초기 가중치를 잘못 설정하면 layer(층)이 깊어질수록 문제 발생<ul>
<li>기울기 소멸: 역전파 과정에서 가중치의 작은 값들이 계속 곱해지면서 입력층으로 갈수록 기울기가 0에 수렴하게 되어 학습이 제대로 안됨</li>
<li>기울기 폭발: 역전파 과정에서 가중치의 큰 값들이 계속 곱해지면서 입력층으로 갈수록 기울기가 기하급수적으로 커져 학습이 제대로 안됨 </li>
</ul>
</li>
<li>따라서 기울기가 0에 수렴하거나 발산하지 않기 위해 가중치의 분산 정도를 적절히 조절하는 것이 필요<h2 id="2-사전-기본-개념">2. 사전 기본 개념</h2>
<h3 id="1-표준편차">(1) 표준편차</h3>
</li>
<li>데이터가 평균으로부터 얼마나 퍼져있는지 나타내는 수치</li>
<li>표준편차가 작으면 데이터가 평균 근처에 모여있음 </li>
<li>표준편차가 크면 데이터가 평균과 멀리 퍼져있음</li>
</ul>
<h3 id="2-68-95-997-규칙">(2) 68-95-99.7 규칙</h3>
<ul>
<li>정규분포의 특징 중 하나</li>
</ul>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/e90eacbb-2bbd-4883-8697-c7909ede3db8/image.png" alt=""></p>
<p>=&gt; 68%의 관측값이 평균으로부터 양쪽으로 표준편차가 떨어져 있는 곳 내에 분포
=&gt; 95%의 관측값이 평균으로부터 양쪽으로 2<em>표준편차가 떨어져있는곳 내에 분포 
=&gt; 99.7%의 관측값이 평균으로부터 양쪽으로 3</em>표준편차가 떨어져있는곳 내에 분포</p>
<ul>
<li>예시: 평균이 0이고 표준편차가 0.1인 분포는 랜덤하게 숫자를 뽑으면 95% 확률로 -0.2 ~ 0.2 사이의 값이 나옴</li>
</ul>
<h3 id="3-여러-활성화-함수">(3) 여러 활성화 함수</h3>
<p><img src="https://velog.velcdn.com/images/vinyl-bin/post/9115204b-0aa1-4b24-be9e-17893dd704c3/image.png" alt=""></p>
<h2 id="3-xavier-초기화">3. Xavier 초기화</h2>
<ul>
<li>각 층의 가중치를 그 층의 입력 노드 수와 출력 노드 수에 기반하여 조절된 분산을 가진 분포에서 무작위로 선택하여 초기화하는 방법</li>
<li>주로 tanh(탄젠트) 또는 sigmoid(시그모이드) 같은 선형적 활성화 함수에 적합<ul>
<li>실제로 탄젠트와 시그모이드 함수는 비선형 함수이지만 초기화 범위가 0 근처이므로 초기화 시점에서는 이 함수들이 선형과 비슷하게 행동한다고 가정함 (0 근처에서는 선형과 비슷하다는 성질 활용)</li>
</ul>
</li>
</ul>
<h3 id="1-xavier-normal정규분포-초기화">(1) Xavier Normal(정규분포 초기화)</h3>
<ul>
<li>평균이 0이고 표준편차(σ)가 아래와 같은 정규분포를 따름
<img src="https://velog.velcdn.com/images/vinyl-bin/post/5a8264f2-89e2-4776-8f37-6be6e22ab64f/image.png" alt=""></li>
</ul>
<h3 id="2-xavier-uniform균일분포-초기화">(2) Xavier Uniform(균일분포 초기화)</h3>
<ul>
<li>아래 범위 내에서 균일하게 값 추출함
<img src="https://velog.velcdn.com/images/vinyl-bin/post/af806af2-ce27-40d4-a7d2-850ef4a188a4/image.png" alt=""></li>
</ul>
<h2 id="4-he-초기화">4. He 초기화</h2>
<ul>
<li>xavier 초기화 한계점<ul>
<li>Xavier 초기화는 활성화 함수가 선형(Linear)이라는 가정하에 유도됨</li>
<li>하지만 ReLU 함수는 입력값이 음수일 때 0을 출력하는 비선형성을 가짐 </li>
<li>이로 인해 ReLU 층을 통과할 때마다 출력값의 분산이 입력값 분산의 약 절반으로 줄어드는 현상이 발생하며, 층이 깊어질수록 활성화 값의 분포가 0으로 수렴함</li>
</ul>
</li>
<li>He 초기화<ul>
<li>ReLU 함수를 통과하면 음수 영역이 0이 되어 출력의 분산이 입력 분산의 절반(1/2)이 된다는 점에 착안</li>
<li>이를 상쇄하기 위해 가중치의 분산을 인위적으로 2배로 설정</li>
<li>주로 ReLU 및 그 변형들 (Leaky ReLU 등) 같은 비선형 활성화 함수에 적합</li>
<li>출력 노드는 분산 보존에 영향을 주지 않는다고 가정하여 무시하고, 입력 노드 수만을 변수로 사용</li>
</ul>
</li>
</ul>
<h3 id="1-he-normal정규분포-초기화">(1) He Normal(정규분포 초기화)</h3>
<ul>
<li>평균이 0이고 표준편차(σ)가 아래와 같은 정규분포를 따름
<img src="https://velog.velcdn.com/images/vinyl-bin/post/4e57c199-2050-431c-a238-53d23ea8395a/image.png" alt=""></li>
</ul>
<h3 id="2-he-uniform균일분포-초기화">(2) He Uniform(균일분포 초기화)</h3>
<ul>
<li>아래 범위 내에서 균일하게 값 추출함
<img src="https://velog.velcdn.com/images/vinyl-bin/post/2564b34c-a7a6-4859-b9d8-af35ba9894bd/image.png" alt=""></li>
</ul>
<p>** 참고</p>
<ul>
<li><a href="https://guru.tistory.com/69">https://guru.tistory.com/69</a></li>
<li><a href="https://jangpiano-science.tistory.com/126">https://jangpiano-science.tistory.com/126</a></li>
<li><a href="https://resultofeffort.tistory.com/114">https://resultofeffort.tistory.com/114</a></li>
<li><a href="https://heeya-stupidbutstudying.tistory.com/entry/ML-%ED%99%9C%EC%84%B1%ED%99%94-%ED%95%A8%EC%88%98Activation-Function">https://heeya-stupidbutstudying.tistory.com/entry/ML-%ED%99%9C%EC%84%B1%ED%99%94-%ED%95%A8%EC%88%98Activation-Function</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>