<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev-hr2.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 개발자의 로그</description>
        <lastBuildDate>Sun, 15 Mar 2026 02:56:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dev-hr2.log</title>
            <url>https://velog.velcdn.com/images/dev-hr2/profile/4603f1ee-8b89-4c5f-9647-e1fc6b738990/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dev-hr2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev-hr2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Go를 Java와 비교해보자]]></title>
            <link>https://velog.io/@dev-hr2/Go%EB%A5%BC-Java%EC%99%80-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@dev-hr2/Go%EB%A5%BC-Java%EC%99%80-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 15 Mar 2026 02:56:30 GMT</pubDate>
            <description><![CDATA[<p>Go를 처음 익히게 됐다.
당장 Go 관련 책을 사거나 영상을 보거나, 다른 사람의 글을 읽어도 되겠지만 우선은 가장 익숙한 Java랑 비교해서 아주 러프하게라도 흐름은 따라가보도록 해보기 위해 정리했다.
공부한다고 작성한 것이라 틀린 것이 있으면 학습하면서 수정해 나가려고 한다.</p>
<blockquote>
<p>Go 언어를 Java 관점에서 이해하기 위한 비교 문서</p>
</blockquote>
<h2 id="기본-개념-비교">기본 개념 비교</h2>
<table>
<thead>
<tr>
<th>Go 개념</th>
<th>Java 비교</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>package</code></td>
<td><code>package</code></td>
<td>동일 - 코드 그룹화</td>
</tr>
<tr>
<td><code>func</code></td>
<td><code>method</code></td>
<td>함수 정의</td>
</tr>
<tr>
<td><code>struct</code></td>
<td><code>class</code></td>
<td>데이터를 담는 구조체 (클래스와 유사하나 상속 없음)</td>
</tr>
<tr>
<td><code>interface</code></td>
<td><code>interface</code></td>
<td>동일 - 메서드 시그니처 정의</td>
</tr>
<tr>
<td><code>go.mod</code></td>
<td><code>pom.xml</code> / <code>build.gradle</code></td>
<td>의존성 관리 파일</td>
</tr>
<tr>
<td><code>import</code></td>
<td><code>import</code></td>
<td>동일</td>
</tr>
<tr>
<td><code>:=</code></td>
<td>변수 선언</td>
<td><code>var name = value</code>의 축약형</td>
</tr>
<tr>
<td><code>ctx context.Context</code></td>
<td><code>ThreadLocal</code></td>
<td>요청 컨텍스트 전달</td>
</tr>
</tbody></table>
<h2 id="프로젝트-구조-비교">프로젝트 구조 비교</h2>
<table>
<thead>
<tr>
<th>Java Spring</th>
<th>Go</th>
</tr>
</thead>
<tbody><tr>
<td><code>SpringApplication.run()</code></td>
<td><code>main.go: main()</code></td>
</tr>
<tr>
<td><code>@Configuration</code> 클래스</td>
<td><code>config.go: LoadConfig()</code></td>
</tr>
<tr>
<td>Application 초기화</td>
<td><code>app.go: NewApp()</code></td>
</tr>
<tr>
<td>Tomcat 서버 시작</td>
<td><code>app.go: Run()</code></td>
</tr>
</tbody></table>
<h2 id="계층-구조-비교">계층 구조 비교</h2>
<table>
<thead>
<tr>
<th>계층</th>
<th>Java Spring</th>
<th>Go eva-vsc</th>
</tr>
</thead>
<tbody><tr>
<td>진입점</td>
<td><code>SpringApplication.run()</code></td>
<td><code>main.go</code></td>
</tr>
<tr>
<td>설정</td>
<td><code>@Configuration</code></td>
<td><code>config.go</code></td>
</tr>
<tr>
<td>라우팅</td>
<td><code>@RequestMapping</code></td>
<td><code>router.go</code></td>
</tr>
<tr>
<td>필터</td>
<td><code>Filter/Interceptor</code></td>
<td><code>middleware/</code></td>
</tr>
<tr>
<td>컨트롤러</td>
<td><code>@Controller</code></td>
<td><code>handler/</code></td>
</tr>
<tr>
<td>DTO</td>
<td><code>@RequestBody</code> 클래스</td>
<td><code>message/</code></td>
</tr>
<tr>
<td>모델</td>
<td><code>Entity/VO</code></td>
<td><code>model/</code></td>
</tr>
<tr>
<td>서비스</td>
<td><code>@Service</code></td>
<td><code>pkg/</code> 내 클라이언트들</td>
</tr>
</tbody></table>
<h2 id="에러-처리-비교">에러 처리 비교</h2>
<h3 id="java-예외-기반">Java (예외 기반)</h3>
<pre><code class="language-java">try {
    result = service.call();
} catch (Exception e) {
    // 에러 처리
}</code></pre>
<h3 id="go-반환값-기반">Go (반환값 기반)</h3>
<pre><code class="language-go">result, err := service.Call()
if err != nil {
    // 에러 처리
    return err
}
// 정상 처리</code></pre>
<h2 id="인터페이스-비교">인터페이스 비교</h2>
<h3 id="java-명시적-implements">Java (명시적 implements)</h3>
<pre><code class="language-java">public class MyService implements Service {
    // 메서드 구현
}</code></pre>
<h3 id="go-덕-타이핑---암시적-구현">Go (덕 타이핑 - 암시적 구현)</h3>
<pre><code class="language-go">// 메서드만 구현하면 자동으로 인터페이스 충족
type PubSub interface {
    Publish(subject string, data []byte) error
    Subscribe(subject string, cb MsgHandler) error
}

// natsConnWrapper는 위 메서드들을 구현했으므로
// 자동으로 PubSub 인터페이스를 충족
type natsConnWrapper struct { ... }
func (n *natsConnWrapper) Publish(...) error { ... }
func (n *natsConnWrapper) Subscribe(...) error { ... }</code></pre>
<h2 id="비동기-처리-비교">비동기 처리 비교</h2>
<h3 id="java-thread">Java (Thread)</h3>
<pre><code class="language-java">new Thread(() -&gt; doSomething()).start();</code></pre>
<h3 id="go-goroutine">Go (Goroutine)</h3>
<pre><code class="language-go">go doSomething()  // 새 고루틴에서 비동기 실행</code></pre>
<h2 id="고루틴-간-통신-channel">고루틴 간 통신 (Channel)</h2>
<p>Go의 채널은 Java의 <code>BlockingQueue</code>와 유사하다.</p>
<pre><code class="language-go">ch := make(chan Message)

// 송신
go func() {
    ch &lt;- message
}()

// 수신
msg := &lt;-ch</code></pre>
<h2 id="미들웨어-vs-filter">미들웨어 vs Filter</h2>
<h3 id="java-servlet-filter">Java Servlet Filter</h3>
<pre><code class="language-java">public void doFilter(req, res, chain) {
    // 전처리
    chain.doFilter(req, res);
    // 후처리
}</code></pre>
<h3 id="go-middleware">Go Middleware</h3>
<pre><code class="language-go">func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 전처리
        next.ServeHTTP(w, r)
        // 후처리
    })
}</code></pre>
<h2 id="컨트롤러-비교">컨트롤러 비교</h2>
<h3 id="java-spring-controller">Java Spring Controller</h3>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api&quot;)
public class ApiController {

    @Autowired
    private Service service;

    @PostMapping(&quot;/events&quot;)
    public ResponseEntity&lt;Response&gt; events(@RequestBody EventDTO dto) {
        // 비즈니스 로직
        return ResponseEntity.ok(result);
    }
}</code></pre>
<h3 id="go-handler">Go Handler</h3>
<pre><code class="language-go">type Handler struct {
    service Service
}

func (h *Handler) Events(w http.ResponseWriter, r *http.Request) {
    var dto EventDTO
    json.NewDecoder(r.Body).Decode(&amp;dto)

    // 비즈니스 로직

    json.NewEncoder(w).Encode(result)
}</code></pre>
<h3 id="포인터-리시버">포인터 리시버</h3>
<p>Go에서 <code>func (a *App) Run()</code>의 <code>*</code>는 <strong>포인터(Pointer)</strong>를 의미</p>
<h4 id="java와-비교">Java와 비교</h4>
<pre><code class="language-java">// Java - 클래스 안에 메서드 정의
public class App {
    public void run() {
        this.doSomething();  // this로 자기 자신 참조
    }
}</code></pre>
<pre><code class="language-go">
// Go 메소드 구조

func (m *AuthMiddleware) Handler(next http.Handler) http.Handler
//    └──────┬───────┘  └──┬──┘  └───────┬───────┘ └─────┬─────┘
//        리시버          메소드명        파라미터           반환타입


// Go - 구조체 밖에서 메서드 정의
type App struct {
    config Config
}

func (a *App) Run() {
    a.doSomething()  // a가 Java의 this 역할
}</code></pre>
<h4 id="-유무의-차이"><code>*</code> 유무의 차이</h4>
<table>
<thead>
<tr>
<th>문법</th>
<th>이름</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>func (a App)</code></td>
<td>값 리시버</td>
<td>복사본 전달 (원본 수정 불가)</td>
</tr>
<tr>
<td><code>func (a *App)</code></td>
<td>포인터 리시버</td>
<td>원본 참조 전달 (원본 수정 가능)</td>
</tr>
</tbody></table>
<h4 id="예시">예시</h4>
<pre><code class="language-go">type Counter struct {
    count int
}

// 값 리시버 - 원본 변경 안 됨
func (c Counter) IncrementWrong() {
    c.count++  // 복사본만 증가, 원본은 그대로
}

// 포인터 리시버 - 원본 변경됨
func (c *Counter) Increment() {
    c.count++  // 원본이 증가
}</code></pre>
<h4 id="java로-비유하면">Java로 비유하면</h4>
<pre><code class="language-java">// 값 리시버 = 이런 느낌 (실제론 불가능)
void increment(Counter copy) {  // 복사본 전달
    copy.count++;  // 원본 영향 없음
}

// 포인터 리시버 = 일반적인 Java 메서드
void increment() {
    this.count++;  // 원본 수정
}</code></pre>
<h4 id="결론">결론</h4>
<p>Go에서 <code>*</code>가 붙은 포인터 리시버를 쓰는 이유:</p>
<ol>
<li><strong>원본 수정이 필요할 때</strong> (상태 변경)</li>
<li><strong>구조체가 클 때</strong> (복사 비용 절약)</li>
</ol>
<p>Java에서는 객체가 항상 참조로 전달되므로 이런 구분이 없지만, Go는 명시적으로 지정해야 한다. <strong>대부분의 경우 <code>*</code>를 붙인 포인터 리시버를 사용</strong></p>
<pre><code class="language-go">
x := 10
p := &amp;x   // &amp; : x의 주소를 얻어 p에 저장 (p는 *int)
*p = 20   // * : p가 가리키는 실제 값을 수정

// &amp; 는 주소를 만들기
// * 는 포인터를 선언하거나, 포인터를 따라가 실제 값을 읽고, 쓰기
// 포인터는 필요한 시점에 한 번 따라가서 값 읽기/쓰기를 진행함

</code></pre>
<pre><code class="language-go">
// **결과값 + 에러를 같이 반환**하는 이 패턴이 Go 코드 어디서나 보임
// Java의 try-catch 대신 이걸 쓰는 것

func findUser(id int) (User, error) {
    user, err := db.Query(id)
    if err != nil {
        return User{}, err  // 빈 User와 에러 반환
    }
    return user, nil  // 유저와 nil(에러 없음) 반환
}

// 사용할 때
user, err := findUser(123)
if err != nil {
    // 에러 처리
}</code></pre>
<h2 id="타입">타입</h2>
<pre><code class="language-go">bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

</code></pre>
<h2 id="mux">Mux</h2>
<ul>
<li>Spring DispatcherServlet과 비슷한 역할</li>
<li>@RequestMapping, @GetMapping 같은게 Go에서는 Mux가 하는 일</li>
<li>URL 보고 알맞은 핸들러(컨트롤러)로 연결해주는 것</li>
</ul>
<h2 id="bufio-잘-안쓰겠지만">Bufio (잘 안쓰겠지만)</h2>
<p>Java의 <code>BufferedReader</code>와 개념적으로 유사 — <strong>I/O 횟수를 줄이기 위한 버퍼링</strong>이 핵심</p>
<h3 id="공통점">공통점</h3>
<table>
<thead>
<tr>
<th></th>
<th>Go <code>bufio</code></th>
<th>Java <code>BufferedReader</code></th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>버퍼를 두어 I/O 횟수 감소</td>
<td>동일</td>
</tr>
<tr>
<td>래핑 방식</td>
<td>기존 Reader를 감쌈</td>
<td>기존 Reader를 감쌈</td>
</tr>
<tr>
<td>줄 읽기</td>
<td><code>ReadString(&#39;\n&#39;)</code></td>
<td><code>readLine()</code></td>
</tr>
</tbody></table>
<h3 id="비교">비교</h3>
<pre><code class="language-java">// Java
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();</code></pre>
<pre><code class="language-go">// Go
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString(&#39;\n&#39;)  // &#39;\n&#39;까지 읽음</code></pre>
<h3 id="주요-차이점">주요 차이점</h3>
<ul>
<li>Go의 <code>ReadString(&#39;\n&#39;)</code>은 <strong>구분자 문자(&#39;\n&#39;)를 결과에 포함</strong>시킴</li>
<li>Java의 <code>readLine()</code>은 구분자를 제외하고 반환</li>
<li>그래서 Go에서는 읽은 후 <code>strings.TrimSpace(line)</code>으로 개행 문자를 직접 제거해야 함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AI 서비스 도입 (1) - 배포 자동화, CI/CD 구축기]]></title>
            <link>https://velog.io/@dev-hr2/AI-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%8F%84%EC%9E%85-1</link>
            <guid>https://velog.io/@dev-hr2/AI-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%8F%84%EC%9E%85-1</guid>
            <pubDate>Tue, 02 Jan 2024 11:25:05 GMT</pubDate>
            <description><![CDATA[<p>AI 서비스를 신규로 도입하려는데 AI 부서의 코드 배포를 자동화하고 지속적인 통합, 지속적인 배포가 가능하게 배포라인을 잡아달라는 요구를 받았다.</p>
<p>당시 요청 부서의 상황은</p>
<ul>
<li>깃헙 repository에서 브랜치 관리를 안 하고 있었음<ul>
<li>단일 브랜치로 작업했고 개발 테스트용 브랜치의 필요성을 절감하지 못했음</li>
</ul>
</li>
<li>배포 툴과 방식에 대해 익숙하지 않아 내가 알아보고 몇 가지 안을 준비해야하는 상황</li>
<li>Docker와 Elastic Beanstalk를 사용했으면 좋겠다는 정도의 요구사항이 있었음</li>
</ul>
<p>현황과 부서의 요구사항에 맞춰 설계 작업에 들어갔다.</p>
<h2 id="0-사전-작업">0. 사전 작업</h2>
<h3 id="브랜치-분리-요청">브랜치 분리 요청</h3>
<p>신규 서비스는 백엔드에서 AI 서비스 (ML 작업을 직접 하는 코드인지는 아직 확인이 안 됐다. 나에게 코드를 확인할 권한도 없기도 했고.)를 호출하고, 응답받는 과정이었고 이를 위한 서버도 띄워야 했다.
회사는 개발 테스트용 환경이 별도 구축돼 있었고, 모든 개발자들이 환경별 테스트를 했기 때문에 AI 서비스도 이를 따라야 테스트가 원활했다.
그래서 담당자에게 브랜치 분리의 필요성을 설명하고, 참고를 위해 백엔드에서 관리하는 방법을 설명했다. 
실제 코드 관리는 해당 부서에서 해주시겠지만 브랜치 분리의 필요성에 공감해줬고, 환경별 마더브랜치를 알려줘서 깃헙쪽 작업은 정리가 됐다.</p>
<h3 id="배포-방식-선택-요청">배포 방식 선택 요청</h3>
<p>두 가지 안을 제안했다.
현재 백엔드, 프론트엔드에서 사용중인 Jenkins를 이용하는 방법과 Github action을 통한 방식을 각각 설명하며 원하는 것을 알려달라고 했다.</p>
<p>Jenkins 배포 방식</p>
<ul>
<li>사내 개발팀 모두 jenkins로 배포 중이기에 회사 내 통일성을 가져갈 수 있음</li>
<li>배포 시점과 브랜치 머지 시점을 다르게 가져갈 수 있음 (장점이자 단점)<ul>
<li>환경별 마더 브랜치로 머지 후에도 추가 수정 작업 후 배포가 가능 </li>
<li>다만 배포를 위한 스텝이 하나 더 있는 것처럼 느낄 수 있음</li>
<li>Jenkins를 배포하려면 별도 ip로 접속 후, 로그인을 거쳐 배포하는 과정이 번거로울 수도 있기 때문</li>
</ul>
</li>
</ul>
<p>Github action 배포 방식</p>
<ul>
<li>환경별 마더 브랜치에 머지하면 그 즉시 배포가 되기 때문에 편리하게 여길 수 있음</li>
<li>다만 현 담당자의 퇴사 후, 인수인계가 잘 되지 않으면 배포방식에 대해 유지보수성이 떨어질 수 있다는 우려도 있음</li>
</ul>
<p>각 방식의 장단을 들은 AI 부서에서는 통일성을 최우선으로 생각해 Jenkins로 배포할 수 있게 작업을 요청했다.</p>
<h2 id="1-설계">1. 설계</h2>
<p>각 단계별 인프라 서비스는 아래처럼 정해졌다.</p>
<ul>
<li>코드 관리 : github</li>
<li>Docker image build : Jenkins</li>
<li>Docker image push : AWS ECR</li>
<li>Docker.aws.json 저장 : AWS S3</li>
<li>Deploy : AWS Elastic Beanstalk</li>
</ul>
<p>이에 기반한 배포 플로우는</p>
<p>1) Jenkins에서 빌드할 소스 코드를 Github Repo에서 가져온다 
2) 이미지 빌드가 완료되면 AWS ECR에 push 한다
3) S3에 Docker관련 Json 파일을 업로드하고
4) Elastic Beanstalk에 배포를 한다
5) 그 과정에서 beanstalk은 S3에 업로드된 Docker.aws.json 파일을 다운로드 받고
6) 다운받은 파일을 기반으로 배포가 진행된다.</p>
<p>이렇게 설계까지 짜는 것은 어렵지 않았다.</p>
<h2 id="2-구축">2. 구축</h2>
<p>작업은 AWS쪽과 Jenkins 스크립트 작성으로 분리해서 작업했다.
AWS는 콘솔에서 필요한 데이터만 입력해주면 끝나지만 Jenkins는 스크립트를 짜야했기 때문.</p>
<h3 id="1-aws">(1) AWS</h3>
<h4 id="1-elastic-beanstalk">1) Elastic Beanstalk</h4>
<p>배포 한 번으로 관련 인프라(로드 밸런싱, 오토 스케일링, 어플리케이션 상태 모니터링 등)를 관리해주는 기능이다.
<img src="https://velog.velcdn.com/images/dev-hr2/post/3e11123a-ddd6-4712-bb14-f05ed5669cf2/image.png" alt=""></p>
<p>어플리케이션 하나에 두 환경을 구축할 수 있었기에 운영과 개발 테스트 환경, 두 가지를 구성했다.
<img src="https://velog.velcdn.com/images/dev-hr2/post/f40f845d-07e8-463b-98ac-dfce0d612ba7/image.png" alt="">
이런 식으로 버전에 넘버링은 자동으로 채번되고, 생성일자도 모두 로그가 남아서 관리가 다소 용이하다.
캡쳐에는 없지만 소스도 모두 남아있어 배포된 버전의 json 파일도 다운로드가 가능했다.</p>
<h4 id="2-ecr-elastic-container-registry">2) ECR (Elastic Container Registry)</h4>
<p>별도의 도커 repo를 구성하는 방법도 있었지만 최소한의 노력과 설정으로 빠르게 구성하기 위해, 그리고 요청 부서의 요구사항으로 ECR을 사용했다.
IAM으로 권한 관리하기도 편했다.
<img src="https://velog.velcdn.com/images/dev-hr2/post/4d90ae05-d154-430a-b64b-d02d75de9683/image.png" alt="">
운영과 개발 테스트 환경용은 분리했다.</p>
<p><img src="https://velog.velcdn.com/images/dev-hr2/post/5b5c0316-bff8-497f-9db6-41b96d628f83/image.png" alt="">
역시 최신 이미지는 자동으로 표기해주고, 푸시 로그도 모두 남기 때문에 히스토리 관리가 편리했다.</p>
<h4 id="3-로드-밸런서">3) 로드 밸런서</h4>
<p>beanstalk에서 로드 밸런서까지 자동으로 만들어서 연결해주지만, 정책상 도메인 매핑이 필요해 관리 부서에 DNS NAME을 전달했다.
AWS 로드밸런서와 연결된 IP주소는 변경될 수 있어서 DNS Name을 이용해서 CNAME 레코드를 생성하라고 권장한다.
다행히 이 작업은 금방 끝났다.</p>
<h3 id="2-jenkins">(2) Jenkins</h3>
<p>기존에 스크립트를 짜 본 적은 없었기 때문에 찾아가며 짜야했다.
주된 예시는 파이프라인이 많았다.
Freestyle project로 만들까 잠시 고민도 했지만, 파이프라인 형식이 전체 플로우를 알고 싶어하는 해당 부서의 요구사항에도 잘 맞다 생각했다.
또 처음 해 본 작업이라 실패나는 구간을 확인하기 편리하기 때문에 최종적으로 파이프라인 방식으로 설정했다.</p>
<p><img src="https://velog.velcdn.com/images/dev-hr2/post/eddaa02d-cdca-48a6-a6e6-04614ad92b32/image.png" alt=""></p>
<p>과정은 총 3가지로 구성했다. 
깃헙에서 코드 가져오기 -&gt; 도커 이미지 빌드하기, 빌드한 이미지 ECR에 푸시하기 -&gt; Elastic beanstalk에 배포하기</p>
<p>각 스텝별로 참고할만한 자료는 많았었기 때문에 차근차근 작성했고, 오탈자에 유의하며 작성을 완료했다.</p>
<h2 id="3문제-상황-발생">3.문제 상황 발생</h2>
<p>스크립트도 완료했고, AWS쪽 작업도 잘 돼 있는데 자꾸 젠킨스 instance에서 Docker image build 할 때 실패가 났다.
원인을 못 찾아서 한참을 헤맸다.</p>
<h4 id="첫번째-의심--스크립트-오류">첫번째 의심 : 스크립트 오류</h4>
<p>처음 스크립트를 짜보다 보니, 스크립트 내 오류가 있는 건 아닌가 싶었다.
ChatGPT에게도 물어보고, 구글 내 검색을 해보며 문법 오류를 다시 점검했으나 이상 없었다.</p>
<h4 id="두-번째-의심--젠킨스-ec2의-권한-문제">두 번째 의심 : 젠킨스 EC2의 권한 문제</h4>
<p>AWS에 떠 있는 젠킨스 서버가 github에서 코드를 복사해오지 못하는 것 아닌가, 혹은 이미지 빌드 후 ECR에 push할 때 권한의 문제가 있는 것은 아닌가 의심했다.
하지만 AWS IAM에서 FullAccess 권한을 줬고, github에서도 정상적으로 복사해 오는 것을 젠킨스 서버 내에서 확인할 수 있었다. </p>
<h4 id="세-번째-의심--docker-image-오타">세 번째 의심 : docker image 오타</h4>
<p>스크립트와 다른 파일 내 오타가 있어서 이미지명 불일치가 실패 원인일까 의심돼 모든 파일과 스크립트를 다시 훑어보고, cmd+f 로 확인했으나 역시 이 때문도 아니었다. </p>
<h3 id="결국-원인은">결국 원인은...</h3>
<p>정말 한참을 헤매다가 젠킨스 EC2 서버에 들어가서 다시 확인했더니 서버 내에 docker 설치가 안 돼 있었다. 그러니 docker 관련 명령어가 안 먹혔던 것이다.
해당 인스턴스 내 docker가 설치 돼 있다고 착각했던 이유는, 과거 devOps의 흔적인 듯 하나 빈 docker 디렉토리가 있었기 때문이었다.</p>
<pre><code class="language-bash">docker --version</code></pre>
<p>이라도 한번만 쳐봤으면 빨리 알았을텐데 짐작만으로 작업했던 것이 느린 이슈 해결의 원인이었던 것이다.</p>
<h2 id="4작업-종료">4.작업 종료</h2>
<p>중간에 docker 설치 체크가 안 되면서 아쉽게 시간을 허비했지만, 최종 예정 일정보다 3일 일찍 구축을 완료했다.
이후 배포부터 서비스 호출까지 모두 이슈없이 됐고, 운영 환경 배포일에도 정상 작동되는 것 확인했다.
현재까지 서버 이슈 없이 정상 작동 잘 되는 중이다.</p>
<h2 id="5마무리하며">5.마무리하며</h2>
<p>이번 작업은 처음 해보는 작업이 많아, 검색과 약간의 공부가 필수적이었다.
그리고 짐작으로 다음 스텝을 진행한다는 것이 얼마나 돌아가는 길인지도 깨달았다.
어떠한 작업을 할 떄, 짐작과 추측만으로 작업하지 말고 꼭 확인하는 절차를 거칠 필요성을 체감한 프로젝트였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[메시지 발송 내역 조회 및 엑셀 다운로드 성능 개선]]></title>
            <link>https://velog.io/@dev-hr2/%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B0%9C%EC%86%A1-%EB%82%B4%EC%97%AD-%EC%A1%B0%ED%9A%8C-%EB%B0%8F-%EC%97%91%EC%85%80-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@dev-hr2/%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B0%9C%EC%86%A1-%EB%82%B4%EC%97%AD-%EC%A1%B0%ED%9A%8C-%EB%B0%8F-%EC%97%91%EC%85%80-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Wed, 06 Sep 2023 11:40:56 GMT</pubDate>
            <description><![CDATA[<h3 id="간략한-설명">간략한 설명</h3>
<ul>
<li>메시지 대량 발송 시, 발송 내역에 대한 세부 조회 및 조회 건에 대한 엑셀 다운로드 성능 개선</li>
<li>기준 : 메시지 발송 건수 <code>약 6만 5천 건</code></li>
<li>성과<ul>
<li>발송 내역 상세 조회 속도 <em>4900% 개선 (100초 -&gt; 2초)</em></li>
<li>상세 조회 내역 엑셀 다운로드 속도 <em>2100% 개선 (155초 -&gt; 7초)</em></li>
<li>속도 측정 방법 : 해당 로직 전,후에 <code>System.currentTimeMillis()</code>를 삽입한 뒤 산출</li>
</ul>
</li>
</ul>
<pre><code>// 개선 전
INFO i.c.g.a.a.m.MassMessageFacade@findMessageHistoryDetailsExcel(130) - 메세지 로그 조회 소요시간 : 100.337초
INFO i.c.g.a.a.m.MassMessageFacade@findMessageHistoryDetailsExcel(135) - 엑셀 파싱 작업 소요시간 : 154.885초

// 개선 후
INFO i.c.g.a.a.m.MassMessageFacade@findMessageHistoryDetailsExcel(130) - 메세지 로그 조회 소요시간 : 1.504초
INFO i.c.g.a.a.m.MassMessageFacade@findMessageHistoryDetailsExcel(136) - 엑셀 파싱 작업 소요시간 : 6.817초</code></pre><br>

<h3 id="성능-저하-원인">성능 저하 원인</h3>
<ul>
<li>상세 내역 조회<ul>
<li>발송 상태에 대한 code와 description이 별도 테이블에 정의 돼 있음</li>
<li>발송 상태에 대한 description을 내려주기 위해, 발송 건마다 이 테이블에 select 쿼리를 날림</li>
<li>즉, 메시지 6만 5천 건을 조회한다면 6만 5천번 select 쿼리를 날리고 있는 상황</li>
</ul>
</li>
<li>메시지 발송 내역 상세 조회 엑셀 다운로드<ul>
<li>엑셀 파일 생성용 공통 Util이 존재했고, As-is는 이 util을 사용 중</li>
<li>공통 Util은 쿼리해온 결과를 <code>Map&lt;String, Object&gt;</code>에 파일명, head, body를 각각 담아서 row별로 출력하는 방식</li>
<li>각 cell에 적힐 데이터들은 모두 String으로 변환</li>
<li>쿼리 결과를 두 번 변환하다보니(Map에 담고, 다시 꺼내서 엑셀로 출력) 대량 건을 작업할 때는 부적합<br>

</li>
</ul>
</li>
</ul>
<h3 id="해결-방법">해결 방법</h3>
<ul>
<li>상세 내역 조회<ul>
<li>발송 상태에 대한 정의 테이블은 DML이 드문 테이블이며 데이터가 많지 않음</li>
<li>따라서, 발송 상태 코드에 맞는 description을 찾기 전 모든 값을 가져와(findAll()) Java단에서 일치하는 값을 찾아서 매칭</li>
<li>그 결과 100초 -&gt; 2초로 개선</li>
</ul>
</li>
<li>메시지 발송 내역 상세 조회 엑셀 다운로드<ul>
<li>공통 Util이 아닌, 쿼리해온 결과를 for-each 문을 사용해 바로 찍어주는 방식으로 변경</li>
<li>그 결과 155초 -&gt; 7초로 개선</li>
<li>단, 해당 건처럼 대용량 데이터를 엑셀로 변환하는 경우를 제외하고는 엑셀 공통 Util을 쓰는 것이 바람직함</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mysql 5.7 -> 8.0 Version Upgrade]]></title>
            <link>https://velog.io/@dev-hr2/Mysql-5.7-8.0-Version-Upgrade</link>
            <guid>https://velog.io/@dev-hr2/Mysql-5.7-8.0-Version-Upgrade</guid>
            <pubDate>Mon, 28 Aug 2023 12:17:39 GMT</pubDate>
            <description><![CDATA[<h2 id="작업-배경">작업 배경</h2>
<ul>
<li>AWS RDS 엔진버전이 2.10.3 지원 중단</li>
<li>2.11 버전으로 자동 업그레이드 가능하나, 운영 서버의 다운타임 우려</li>
<li>다운타임은 불가피 하므로, 이 기회에 <b>MySQL 8.x 버전으로 업그레이드 진행 결정</b></li>
</ul>
<h2 id="작업대상">작업대상</h2>
<p>: 클라우드게이트 RDS, 셀러게이트 RDS 전체
<img src="https://velog.velcdn.com/images/dev-hr2/post/0f86e226-bf79-456c-ab9e-9e46dec6821c/image.png" alt="">
-&gt; 지원중단 관련 <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.Updates.2103.html">공고</a></p>
<h2 id="사전-준비">사전 준비</h2>
<h3 id="1-업그레이드-테스트">1) 업그레이드 테스트</h3>
<ul>
<li><p>테스트를 위한 DB 세팅</p>
<ol>
<li>기존 DB 스냅샷을 뜬다</li>
<li>그 스냅샷으로 test용 DB를 띄운다</li>
<li>test용 DB를 버전 업그레이드 해본다</li>
<li>이슈가 있다면 모두 수정 및 이슈 해결 한다</li>
<li>모든 이슈가 해결되면 본 DB를 업그레이드 진행한다</li>
</ol>
</li>
<li><p>환경별 테스트 순서</p>
<ul>
<li>develop DB에서 1차 테스트</li>
<li>실 운영 DB의 스탭샷으로 2차 테스트 진행</li>
</ul>
</li>
</ul>
<h4 id="실서버-db-스냅샷-테스트">실서버 DB 스냅샷 테스트</h4>
<ul>
<li>작업 방식은 크게 두 가지</li>
<li>블루/그린 배포 방식 : down time을 최소화 하면서 RDS를 교체하는 방식</li>
<li>인플레이스 방식 : 기존 RDS를 바로 버전 업 하는 방식. 작업하는 동안 서비스 운영 불가</li>
<li>AWS 에서는 블루그린 배포 방식을 권장하기에 두 방식 모두 테스트 해보기로 함</li>
</ul>
<h4 id="실서버-db-테스트-작업-계획">실서버 DB 테스트 작업 계획</h4>
<ul>
<li><p>운영 DB 스냅샷을 두 개 복원한 뒤, 하나는 블루그린 테스트용 다른 하나는 인플레이스용으로 세팅
<img src="https://velog.velcdn.com/images/dev-hr2/post/8836127b-078f-4101-9b02-3241b8dac556/image.png" alt=""></p>
</li>
<li><p>각 버전의 공통 체크 리스트</p>
<ol>
<li>파라미터 그룹 값들 확인하기</li>
<li>api 연결 체크</li>
<li>버전 업 총 소요 시간 체크</li>
</ol>
</li>
<li><p>블루/그린 버전일 때 추가 체크</p>
<ul>
<li>블루 -&gt; 그린으로 전환될 때 엔드포인트도 변경되는지 확인</li>
</ul>
</li>
</ul>
<h4 id="테스트-결과">테스트 결과</h4>
<ul>
<li><p>블루그린 배포 방식</p>
<ul>
<li>총 6시간 소요</li>
<li>기존 SQL mode 설정값 때문에 버전업이 원활하지 않은 이슈가 있었으나 해결</li>
<li>엔드포인트 변함 없음<br></li>
</ul>
</li>
<li><p>인플레이스 배포 방식</p>
<ul>
<li>파라미터 그룹 값 : 기존과 동일하게 세팅 및 확인 완료</li>
<li>api 연결 체크 : 로컬에서 직접 접근으로 확인 완료</li>
<li>버전 업 총 소요 시간 : 21분 소요 (16:57 ~ 17:18)</li>
</ul>
<br> 
#### 테스트 중 만난 이슈</li>
<li><p><em>1. 파라미터 그룹에서 charset 설정을 안 해준 때에 데이터가 ?로 저장*</em></p>
</li>
<li><p>파라미터그룹이 default 일 때 저장된 값
<img src="https://velog.velcdn.com/images/dev-hr2/post/e8df1a4e-fb0f-4fd6-be0c-2e3775964ca1/image.png" alt=""></p>
</li>
</ul>
<p>-&gt; 파라미터 그룹 내, charset 설정을 모두 진행 후 테스트했더니 정상 노출 돼었다.</p>
<p><strong>2. 셀러게이트 RDS 업그레이드 시, upgrade-precheck 실패로 업그레이드 실패 이슈</strong></p>
<ul>
<li>이 역시 파라미터 그룹의 차이 때문에 발생한 이슈<ul>
<li>pre-check를 통과한 RDS와의 차이는 <code>sql_mode가 지정 값이 없느냐, TRADITIONAL로 세팅돼 있냐 차이</code>였음</li>
<li><code>최종적으로는 sql_mode에 지정값을 없애고 진행</code>해 정상 작동함</li>
</ul>
</li>
<li>AWS에서 메이저 업그레이드할 때 precheck를 해주는데 에러 발생 시, 로그를 확인할 수 있음<ul>
<li>Warning은 업그레이드 가능하지만, Error가 발생하면 업그레이드가 불가</li>
<li>당시 에러 로그는 아래와 같았음<pre><code class="language-json">{
  &quot;id&quot;: &quot;auroraUpgradeCheckForSpecialCharactersInProcedures&quot;,
  &quot;title&quot;: &quot;Check for inconsistency related to special characters in stored procedures.&quot;,
  &quot;status&quot;: &quot;ERROR&quot;,
  &quot;description&quot;: &quot;Invalid default value for &#39;modified&#39;&quot;
}</code></pre>
</li>
</ul>
</li>
</ul>
<ul>
<li>해결 방법을 찾기 위한 여정<ol>
<li>첫 번째 검색 키워드 : <code>procedure</code><pre><code>- ‘호환되지 않아 사전에 주의해야할 부분’ 중에 프로시저에 대한 설명이 있었음
- `SHOW PROCEDURE STATUS;` 실행 후 확인해봤으나 직접 설정한 프로시저는 없었음
- 설정하지도 않은 부분에서 에러가 날 수 없기 때문에 이 부분은 아닐 것이라 생각하고 다음 키워드 진행&lt;br&gt;</code></pre></li>
</ol>
</li>
</ul>
<pre><code>   2. 두번째 검색 키워드 : `character, character set`
        - ‘호환되지 않아 사전에 주의해야할 부분’ 중에 character set에 대한 설명 또한 있었음
        - 역시 확인해봤으나 설정에 걸리는 부분이 없음</code></pre><ul>
<li><p>결론</p>
<ol>
<li><p>함부로 DB 설정을 바꾸면 안되지만 시니어들과 상의 후, sql_mode를 껐음</p>
<pre><code>- DB 세팅의 base로 삼는 클라우드게이트에는 sql_mode가 설정돼 있지 않다는 점
- TRADITIONAL로 세팅한 히스토리 및 사유를 아는 사람이 없다는 점
- 버전 업그레이드 진행이 최우선 과제라는 점</code></pre><p>  위 세 가지 주된 이유로 설정을 껐음</p>
<ol start="2">
<li>다만 현재까지 sql_mode와 로그 상 에러 메시지의 상관 관계를 몰라서 미해결 과제로 남겨두고 있다는 부분이 아쉬움</li>
</ol>
</li>
</ol>
</li>
</ul>
 <br>

<h2 id="업그레이드-진행-326일-오전-100">업그레이드 진행 (3/26(일) 오전 1:00)</h2>
<h3 id="작업-순서">작업 순서</h3>
<ol>
<li>기존 서버들 binlog_format mixed로 변경, 재부팅</li>
<li>블루그린 생성</li>
<li>그린(버전업)을 prod로 스위칭</li>
<li>기존 버전 삭제<br>

</li>
</ol>
<h3 id="작업-결과">작업 결과</h3>
<ul>
<li>현재까지 특별한 이슈 없이, 정상 작동해 프로젝트 종료</li>
</ul>
<hr>
<h5 id="관련-내용">관련 내용</h5>
<p><a href="https://velog.io/@dev-hr2/Mysql-5.7%EA%B3%BC-8.0%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80">MySQL5.7 -&gt; 8.0의 차이점은?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[paging total Elements가 뻥튀기 됐는데요?!]]></title>
            <link>https://velog.io/@dev-hr2/paging-total-Elements%EA%B0%80-%EB%BB%A5%ED%8A%80%EA%B8%B0-%EB%90%90%EB%8A%94%EB%8D%B0%EC%9A%94</link>
            <guid>https://velog.io/@dev-hr2/paging-total-Elements%EA%B0%80-%EB%BB%A5%ED%8A%80%EA%B8%B0-%EB%90%90%EB%8A%94%EB%8D%B0%EC%9A%94</guid>
            <pubDate>Tue, 22 Aug 2023 12:48:48 GMT</pubDate>
            <description><![CDATA[<p>버그 원인 파악 중 만난 이슈로 이틀을 고민하다 해결한 게 있어 기록으로 남기려고 한다.</p>
<h2 id="버그-발생-상황">버그 발생 상황</h2>
<ul>
<li><p>쿼리에서 distinct, paging 작업을 하던 것을 java 단에서 처리하려고 수정</p>
</li>
<li><p>쿼리에서 데이터를 가져온 뒤, stream().distinct()까지만 진행된 채로 배포</p>
<pre><code class="language-java">@Transactional(readOnly = true)
public Page&lt;Response&gt; getHistories(List&lt;Long&gt; brandIds, String startDate, String endDate, PageRequest pageRequest) {
      List&lt;MassMessageType&gt; messageTypes = MassMessageType.getMassMessageTypeList(channelName);
      LocalDateTime startTime = DateUtils.convertKstToUtcStartLocalDateTime(startDate);
      LocalDateTime endTime = DateUtils.convertKstToUtcEndLocalDateTime(endDate);

      if (startTime.isAfter(endTime)) {
          throw BaseException.of(ApiErrorCode.BAD_REQUEST);
      }

      List&lt;Response&gt; histories = historyRepository.findHistory(brandIds, startTime, endTime, pageRequest)
              .stream()
              .distinct()
              .collect(Collectors.toList());

      Long count = historyRepository.findCountHistories(brandIds, startTime, endTime)
              .orElse(0L);
      return new PageImpl&lt;&gt;(histories, pageRequest, count);
  }</code></pre>
</li>
</ul>
<h2 id="해결-방법을-찾던-중-내가-만난-이슈">해결 방법을 찾던 중 내가 만난 이슈</h2>
<ul>
<li>java단에서 페이징 처리를 추가하면 되는 것으로 판단</li>
<li>distinct().limit(pageRequest.getPageSize()) 까지만 추가함</li>
<li>그러고 return new PageImpl&lt;&gt;(histories, pageRequest, count) 로 넘김</li>
<li><blockquote>
<p>그랬더니 <strong>total Elements가 달라졌다!!</strong></p>
</blockquote>
</li>
</ul>
<pre><code class="language-java">// 위는 생략
List&lt;Response&gt; histories = historyRepository.findHistory(brandIds, startTime, endTime, pageRequest)
                .stream()
                .distinct()
                .limit(pageRequest.getPageSize()) //여기만 추가
                .collect(Collectors.toList());

        Long count = historyRepository.findCountHistories(brandIds, startTime, endTime)
                .orElse(0L);

         // histories.size() = 15, count = 23
        return new PageImpl&lt;&gt;(histories, pageRequest, count);
        // return 되는 결과 값은 total Elements = 30으로 나왔다!!
    }</code></pre>
<h2 id="무엇이-total-elements를-부풀렸나">무엇이 total Elements를 부풀렸나?</h2>
<p>내가 잘못한 것은 크게 두 가지였다.</p>
<h3 id="1-pagerequest를-넘길-때-잘못-넘겼다">1) PageRequest를 넘길 때 잘못 넘겼다.</h3>
<ul>
<li>포스트맨으로 디버깅하며, param 설정을 잘못 했다.<ul>
<li>1 page를 확인하려면 -1 처리해서 0으로 PageSize()를 세팅해줬어야 했다.</li>
<li>param을 1로 보냈으면 2 페이지를 조회하겠다는 뜻이 됐다.<h3 id="2-stream에서-skippagerequestgetoffset을-빼먹었다">2) stream에서 skip(pageRequest.getOffSet())을 빼먹었다.</h3>
</li>
</ul>
</li>
<li>페이징 처리의 필수값인 skip()을 뺀 채로 stream이 돌다 보니, total 산출하는 로직에서 기대값과 다르게 나왔다.</li>
</ul>
<h4 id="skip과-limit">skip()과 limit()</h4>
<ul>
<li>skip() : 지정한 갯수만큼 &#39;건너 뛰겠다&#39;.</li>
<li>limit() : 지정한 갯수만큼만 받겠다.
즉, 페이징할 때는 skip()은 이번 페이지에서 보여주기 위해 앞에서 몇 개를 건너뛸 것인지를 나타내고
limit()은 한 페이지에 표기할 데이터 개수를 뜻한다.</li>
</ul>
<h3 id="그래서-내가-작성했던-값과-코드의-의미는">그래서 내가 작성했던 값과 코드의 의미는..</h3>
<p>2 페이지(page = 1)에 표기될 값을 알고 싶지만 건너뛸 데이터는 없음. 처음부터 15개로 보여줘.</p>
<p>-&gt; 그렇게 뽑힌 데이터가 total을 산출할 때 아래 로직을 타게 된다.</p>
<pre><code class="language-java">// content.size() = 15, total = 23
public PageImpl(List&lt;T&gt; content, Pageable pageable, long total) {

        super(content, pageable);

        this.total = pageable.toOptional().filter(it -&gt; !content.isEmpty())//
                //            15 x 1 + 15 &gt; 23 (true)
                .filter(it -&gt; it.getOffset() + it.getPageSize() &gt; total)// 
                //            15 + 15 = 30
                .map(it -&gt; it.getOffset() + content.size())//
                .orElse(total);
    }// this.total = 30</code></pre>
<p>이래서 total Elements가 자꾸 30이 나왔던 것이다!</p>
<h2 id="해결방법">해결방법</h2>
<p>위의 무엇이 부풀렸는지를 고치면 해결 방법이 된다!</p>
<p>1) param 값을 내가 원하는 페이지에 -1 해준다 (1페이지 조회 = 0, 2페이지 조회 = 1 ...)
2) skip()을 추가해준다!</p>
<p>그래서 최종 수정한 값은..</p>
<pre><code class="language-java">// 코드 일부 생략
    List&lt;Response&gt; histories = historyRepository.findHistory(brandIds, startTime, endTime)
                    .stream()
                    .distinct()
                    .skip(pageRequest.getOffset())
                    .limit(pageRequest.getPageSize())
                    .collect(Collectors.toList());

    Long count = historyRepository.findCountHistories(brandIds, startTime, endTime)
                .orElse(0L);

    return new PageImpl&lt;&gt;(histories, pageRequest, count);
}</code></pre>
<p>total이 왜! 뻥튀기 되는지 몰랐는데 드디어 이유를 알게 되서 속이 시원했다ㅎㅎ
그리고 이 기회에 total 산출하는 식을 좀 더 들여다 봐서 재밌었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[??? : 주차별 조회할 때, ISO 8601를 베이스로 삼으면서 시작 요일을 일요일로 만들어주세요. ]]></title>
            <link>https://velog.io/@dev-hr2/%EC%A3%BC%EC%B0%A8%EB%B3%84-%EC%A1%B0%ED%9A%8C</link>
            <guid>https://velog.io/@dev-hr2/%EC%A3%BC%EC%B0%A8%EB%B3%84-%EC%A1%B0%ED%9A%8C</guid>
            <pubDate>Mon, 07 Aug 2023 14:06:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev-hr2/post/eb2ccf3a-a166-426f-bdd8-62f5f01843a5/image.png" alt=""></p>
<h2 id="기존-문제-상황">기존 문제 상황</h2>
<ul>
<li>주차별 조회 ( = 주간 조회)를 했을 때의 표기에 대한 정확한 기준이 없었다.</li>
<li>기준이 없다보니 조회 방식에 따라 결과값이 다른, 즉 데이터 정합성의 문제가 있었다.</li>
</ul>
<p>날짜와 시간을 다루는 부분이 의외로(?) 까다롭다는 얘길 들어서 도전해보고 싶었다.
자원해서 수정 작업에 들어갔다.</p>
<h2 id="요구사항">요구사항</h2>
<ul>
<li><p>ISO 8601에 따라, N월 1일이 <code>목요일보다 이전 요일에 있으면</code> 그 주는 N월의 1주차 이다.</p>
<ul>
<li>위 이미지에서 22주차는 6월 1주차이다. (5월 5주차가 아니다)</li>
</ul>
</li>
</ul>
<ul>
<li><p>하지만 대한민국의 달력은 주로 일요일부터 시작한다는 점에 따라, 한 주의 시작 요일은 <code>일요일</code> 이다.</p>
<ul>
<li>ISO 8601은 월요일부터 시작이지만, 우리는 일요일을 시작 요일로 본다.</li>
</ul>
</li>
</ul>
<p>즉, 일요일부터 한 주를 시작하지만 특정 월의 첫 주를 결정짓는 것은 1일이 일 ~ 목 중에 있는지 여부이다.</p>
<hr>
<h3 id="-iso-8601과-월요일-시작의-차이">+) ISO 8601과 월요일 시작의 차이</h3>
<h4 id="iso-8601-국제-표준법">ISO 8601 국제 표준법</h4>
<ul>
<li>주수는 목요일을 카운트 해서 기술한다.</li>
<li>한 주의 시작 요일은 <code>월요일</code>이다.</li>
<li>월요일 ~ 일요일까지 중에서 1일이 목요일보다 이전 요일 (월 ~ 목)에 있으면, 한 주의 과반 이상이 N월에 속하기 때문에 N월 첫번째 주로 본다.<ul>
<li>즉, 6월 1일이 목요일이면 한 주 중 총 4일(목,금,토,일)이 6월이므로 6월의 1주차로 보는 것</li>
</ul>
</li>
</ul>
<h4 id="미국식-표기법">미국식 표기법</h4>
<ul>
<li>한 주의 시작은 <code>일요일</code>이다.</li>
<li>첫 째주로 보기 위해서는 최소 하루만 있어도 된다.<ul>
<li>월보다는 Year로 바꿔서 생각하면 쉽다.</li>
<li>2023년 1월 1일이 무슨 요일에 있던, 그 주는 2023년 1월 1주차로 카운트 한다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="description">description</h3>
<ul>
<li><p>주차별 조회할 때, WeekFields를 우리 기준에 맞게 재정의 했다.</p>
<pre><code class="language-java">case WEEKLY:
  LocalDateTime dateTime = getWeekStartDay(dateDescription);
  // 한 주의 시작일을 일요일로 설정
  // 1일이 일 ~ 목요일 일 때 1일이 포함된 월의 첫 주로 설정하기 위해 최소 필요한 일수는 3일로 설정함
  TemporalField weekOfMonth = WeekFields.of(DayOfWeek.SUNDAY, 3).weekOfMonth();

  LocalDateTime thursday = dateTime.plusDays(4);
  int dayOfMonth = thursday.getDayOfMonth();
  int year = thursday.getYear();
  formatter = DateTimeFormatter.ofPattern(&quot;MM월 &quot;);
  String thursdayMonth = thursday.format(formatter);

  int week;
  if(dayOfMonth &lt; 6) {
      week = 1;
  } else {
      week = thursday.get(weekOfMonth);
  }

  return new StringBuilder().append(year)
      .append(&quot;년 &quot;)
      .append(thursdayMonth)
      .append(week)
      .append(&quot;주차&quot;)
      .toString();</code></pre>
</li>
</ul>
<p>+) 참고로 ISO 8601은 <code>WeekFields.of(DayOfWeek.MONDAY, 4)</code> 이고 미국식은 <code>WeekFields.of(DayOfWeek.SUNDAY, 1)</code>이었다.</p>
<h3 id="startdate-enddate-추출">startDate, endDate 추출</h3>
<ul>
<li>프론트에서는 해당 연도의 주차를 String으로 넘기는 중이었다. (ex. 202322)</li>
<li>주차에서 startDate와 endDate를 아래 메소드를 통해서 뽑아 냈다.<pre><code class="language-java">// KST 시작일자 뽑기
public static LocalDateTime getWeekStartDay(String yyyyuu) {
  int year = Integer.parseInt(yyyyuu.substring(0,4));
  // 그 해의 몇 일째인지 (2023년 56일째 || 2023년 300일째)
  int day = Integer.parseInt(yyyyuu.substring(4)) == 0 ? 1 : Integer.parseInt(yyyyuu.substring(4)) * 7;
  LocalDate ld = LocalDate.ofYearDay(year, day);
  // 월요일 == 1, 일요일 == 7
  int turn = ld.getDayOfWeek().getValue() == 7 ? 0 : ld.getDayOfWeek().getValue();
  // DayofWeek는 월요일이 한 주의 시작이므로 뒤집어 줌(일요일을 시작일이 되게)
  return ld.minusDays(turn).atTime(0,0,0);
}
</code></pre>
</li>
</ul>
<p>// KST 종료일자 뽑기
public static LocalDateTime getWeekEndDay(String yyyyuu) {
    int year = Integer.parseInt(yyyyuu.substring(0,4));
    int day = Integer.parseInt(yyyyuu.substring(4)) == 0 ? 1 : Integer.parseInt(yyyyuu.substring(4)) * 7;
    LocalDate ld = LocalDate.ofYearDay(year, day);
    int turn = ld.getDayOfWeek().getValue() == 7 ? 0 : ld.getDayOfWeek().getValue();
    // 종료일자는 토요일이 되도록 뽑아야 하므로 plus 해줌
    return ld.plusDays(6-turn).atTime(23,59, 59);
}</p>
<pre><code>
* 202322 (2023년 22주차)를 startDate Param으로 받았을 때
    - LocalDate ld = 2023-05-29 (월)
    -&gt; `ld.minusDays(1).atTime(0,0,0) == 2023 05 28 00:00:00`


* 202322 (2023년 22주차)를 endDate Param으로 받았을 때
    - LocalDate ld = 2023-05-29 (월)
    -&gt; `ld.plusDate(6-1).atTime(23,59,59) == 2023 06 03 23:59:59`


## 또 다른 이슈, Elastic Search
우리는 ES에서 조회해오고, 네트워크 이슈 등으로 ES 조회 실패할 때만 DB에서 쿼리하도록 돼 있다.
ES에서 조회할 때 시작일/종료일에 맞게 결과가 안 나오는 상태였다.

Date histogram aggregation이 `Calendar intervals week (1w)로 설정`돼 있었고,
`ES는 별도 설정이 없다면 월요일이 한 주의 시작`이었기 때문에 start,endDate는 의미가 없었다.

### 해결 해야할 이슈 
* 지정한 startDate, endDate에 맞게 주 단위의 데이터를 조회해오기
* 한 주의 시작은 일요일이고, 종료 요일은 토요일일 것

### 해결 방법
* `offSet : -1d` 를 주니 해결됐다.
출처 : https://discuss.elastic.co/t/how-to-make-weekday-start-on-sunday-in-date-histogram-aggregation/85621

다만 offSet은 주차별 조회할 때만 적용돼야 하기 때문에 아래처럼 분기문을 태웠다.
```java
private String getOffSetData(StatisticsDateType dateType) {
    // ES 조회 시, 일요일을 주의 시작점으로 잡기 위해 설정
    if(StatisticsDateType.WEEKLY.equals(dateType)) {
        return DateHistogramInterval.days(-1).toString();
    } else {
        return &quot;0&quot;;
    }
}</code></pre><p>offSet을 적용하기 전에 calendar intervals 를 fixed intervals로 변경해보기도 하고, 주 단위를 -7d로 검색하도록 해보기도 했으나 방법이 아니었다.
+) fixed intervals 관련 공식 문서 : <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-aggregations-bucket-datehistogram-aggregation.html#fixed_intervals">https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-aggregations-bucket-datehistogram-aggregation.html#fixed_intervals</a></p>
<p>ES에서 Date histogram aggregation 포함 aggregation 쪽도 공부해볼 부분이 많은 곳이라 기회가 된다면 포스팅 해보려고 한다.</p>
<h2 id="배포가-나간-뒤">배포가 나간 뒤..</h2>
<p>2022년 12월 마지막 주로도 검색해보고, 과거 일자로 조회 해보고 다양하게 조회해봐도 이슈 없이 현재까지 정상 조회 돼서 뿌듯했다.</p>
<p>날짜, 특히 주차별 조회 이슈를 해결하면서 공식 문서들을 뒤져보고 공부한 게 든든한 자산이 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring boot] JavaMailSender 설정하며 알게된 것들]]></title>
            <link>https://velog.io/@dev-hr2/spring-boot-JavaMailSender-%EC%84%A4%EC%A0%95%ED%95%98%EB%A9%B0-%EC%95%8C%EA%B2%8C%EB%90%9C-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@dev-hr2/spring-boot-JavaMailSender-%EC%84%A4%EC%A0%95%ED%95%98%EB%A9%B0-%EC%95%8C%EA%B2%8C%EB%90%9C-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Mon, 24 Jul 2023 12:24:26 GMT</pubDate>
            <description><![CDATA[<p>사이드프로젝트를 진행하면서 &#39;메일 발송&#39; 기능을 담당하게 됐다.
회사 소스에 이미 세팅된 걸 파악하기 보단, 내가 처음부터 세팅하면서 알아가고 싶어서 자원했다.</p>
<p>하지만 뜻대로 되지 않아 의외로 시간을 많이 쏟았고 원인은 다음 세 가지였다.</p>
<ol>
<li>yml 혹은 properties에 작성된 값을 어떻게 바인딩해야 할 지 몰랐음</li>
<li>각 설정 단계에 대한 이해 부족</li>
<li>구글의 보안 관련 정책 변경</li>
</ol>
<p>실질적으로 세팅하는 방법은 다른 포스팅에 적어두려고 한다.
이번엔 시간을 많이 쓴 원인과 알게된 것들을 정리하고자 한다.</p>
<hr>
<h2 id="1-yml-혹은-properties의-바인딩">1. yml 혹은 properties의 바인딩</h2>
<p>가장 근본적인 원인이었다.
application.properties에 smtp 관련 값을 추가해두고 바인딩이 안 되는 것을 모른 채 테스트를 진행했다.</p>
<p>우선 yaml과 properties는 다음과 같은 차이점이 있다.</p>
<h4 id="properties">properties</h4>
<ul>
<li>key-value 구조</li>
</ul>
<h4 id="yaml">yaml</h4>
<ul>
<li>계층적 구조</li>
<li>prefix 중복 제거 가능</li>
</ul>
<p>어떤 형식을 쓸 것인지는 취향 차이인 것 같다.</p>
<h3 id="작성된-데이터-바인딩-방법">작성된 데이터 바인딩 방법</h3>
<p>부끄럽게도 아래 이미지처럼 세팅해두면 자동으로(?) MailProperties에 바인딩되는 줄 알았다...!</p>
<p><img src="https://velog.velcdn.com/images/dev-hr2/post/7f92ffcc-d0b2-46a8-a79f-1a81a70f7a29/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev-hr2/post/699d5f59-9bda-435f-bdde-6c7b25d17d26/image.png" alt=""></p>
<p>모든 과정을 디버깅 찍어가면서 확인하다가 알게된 것이 저렇게만 둔다고 알아서 주입되는 것이 아니라는 점이었다.</p>
<p>방법은 아래와 같다.</p>
<h4 id="1-value">1) @Value</h4>
<pre><code class="language-java">@Getter
@Setter
@Component
public final class MailProperties {

    // SMTP 서버
    @Value(&quot;${spring.mail.host}&quot;)
    private String host;

    // 계정
    @Value(&quot;${spring.mail.username}&quot;)
    private String username;

}</code></pre>
<p>이렇게 필드값마다 어노테이션을 달아주고, Placeholder나 SpEL을 명시해주는 방식이다.
다만, 객체가 생성된 후에 주입이 되기 때문에 객체 생성자가 실행되는 시점에선 @Value 값이 null이 되므로 주의할 필요가 있다.</p>
<h4 id="2-configurationproperties">2) @ConfigurationProperties</h4>
<pre><code class="language-java">@Getter
@Setter
@Configuration
@ConfigurationProperties(&quot;mail&quot;)
public final class MailProperties {

    // SMTP 서버
    private String host;

    // 계정
    private String username;

    //smtp
    private Properties properties;

    @Getter
    @Setter
    public static class Properties {
        private Smtp smtp;

        @Getter
        @Setter
        public static class Smtp {
            private boolean auth;

        }

    }

}</code></pre>
<p>이런 방식으로 정의한다.
prefix를 적어줘야하며, inner class를 사용하게 되는 경우 이름을 똑같이 일치 시켜야 한다.
또, setter를 반드시 정의해줘야 한다.
@Value보다 간결하고, inner class 사용 시 좀 더 간편하다는 장점이 있다.</p>
<h4 id="3-constructorbinding">3) @ConstructorBinding</h4>
<p>spring boot 2.2부터는 @ConfigurationProperties가 달린 클래스를 변경 불가능하도록 @ConstructorBinding을 쓸 수 있게 됐다.
그래서 final 필드에 대해 값을 주입해준다.</p>
<pre><code class="language-java">@Getter
@RequiredArgsConstructor
@ConfigurationProperties(&quot;mail&quot;)
public final class MailProperties {

    // SMTP 서버
    private final String host;

    // 계정
    private final String username;

    //smtp
    private final Properties properties;

    @Getter
    @RequiredArgsConstructor
    public static final class Properties {
        private final Smtp smtp;

        @Getter
        @RequiredArgsConstructor
        public static final class Smtp {
            private final boolean auth;

        }

    }

}
</code></pre>
<p>이 방식은 MailProperties 클래스에 직접적으로 bean을 만들어주지 않는다.
따라서 @EnableConfigurartionProperties를 이용해서 클래스 타입을 명시해줘야한다.</p>
<hr>
<p>프로젝트에는 @Value 방식을 썼다.</p>
<p>1) 대규모 프로젝트가 아니기에 yml 파일이 여러 개 생성되지 않는다는 점
2) 메일 관련 설정을 MailProperties 외에 다른 클래스에서 산발적으로 호출하지 않는다는 점
3) 세팅에 좀 더 많은 시간을 쓸 수 없었다는 점</p>
<p>을 이유로 그렇게 사용했다.</p>
<p>프로젝트 마무리 시점에 시간적 여유가 된다면 @Value 외에 다른 방식으로 바인딩하게 변경해보고 싶다.</p>
<p>+) 3번 시간 이슈에 대해 좀 더 설명하자면 @ConfigurationProperties를 쓰려고 설정했지만 주입이 안 됐다. @EnableConfigurartionProperties 도 달아줬으나 안 돼 해결하고 싶었다.
하지만 혼자하는 프로젝트가 아니어서 다른 팀원들이 merge를 기다리고 있는 상황이라 우선 @Value로 마무리 지었다.</p>
<h2 id="2-각-설정-단계에-대한-이해-부족">2. 각 설정 단계에 대한 이해 부족</h2>
<p>참고한 블로그 글들, 회사 소스들 모두 방식은 각자에 맞게 했지만 원리는 동일했다.
하지만 각 단계별로 왜 이렇게 세팅 하는지를 이해하지 못하고 베끼다보니 원인 파악도 느렸다.</p>
<p>결국 각 단계별로 필요한 설정 순서는 아래와 같다.</p>
<p>1) .yml 혹은 .properties에 필요한 설정 값을 넣어준다
2) MailProperties에 값을 매핑해준다
3) MailConfig에 javaMailService 메소드를 @Bean 등록 해준다
3-1) javaMailService 메소드 내 JavaMailSender 객체를 생성하고, 설정값을 set해준다
3-2) Properties 객체에 smtp.auth, starttls 등도 put 해준다
3-3) javaMailSender에 session도 세팅해준다
4) password 보안 때문에 Authenticator를 상속받아 MailAuthenticator를 만들어준다
5) PasswordAuthentication 객체를 재정의 해준다</p>
<p>각 순서에 대해 전체적인 그림을 그리고, 현재 어떤 단계에 있는지를 생각하면서 코드를 작성했으면 문제가 발생하는 위치를 빠르게 잡을 수 있지 않았을까 싶다.</p>
<h2 id="3-구글-정책-변경">3. 구글 정책 변경</h2>
<p>대다수의 블로그를 보면, 메일 발송이 안되면 &#39;보안수준이 낮은 앱의 액세스 허용&#39;을 해주라고 나온다.
하지만 구글 정책상 이 허용값 설정 자체가 불가능하게 바뀌었다.</p>
<p>그러다가 발견한 stackOverflow 글.
친절하게 캡쳐를 첨부해 설명해줘서 이해하고 해결했다.</p>
<p>링크 : <a href="https://stackoverflow.com/questions/72930539/javax-mail-authenticationfailedexception-535-5-7-8-username-and-password-not-ac">https://stackoverflow.com/questions/72930539/javax-mail-authenticationfailedexception-535-5-7-8-username-and-password-not-ac</a></p>
<hr>
<h2 id="작업을-해보며">작업을 해보며..</h2>
<p>회고 아닌 회고글이 됐지만 이번 작업으로 느낀 점이 많았다.</p>
<p>첫째는 이미 선배 개발자들이 세팅해둔 회사 소스를 눈으로 보는 것보다 내가 직접 설정하면서 겪어야 부족한 부분을 뼈저리게 느낀다는 것이다.
두 번째는 기본기를 확실하게 다져야 하고, 구멍이 숭숭 나 있다는 것이었다.</p>
<p>이렇게 느끼기 위해서 사이드 프로젝트를 시작했는데 역시나 하길 잘 했다.
앞으로 배우고 느낄 게 많아서 행복하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Querydsl 에서 동적 sorting을 위해 OrderSpecifier 클래스 구현]]></title>
            <link>https://velog.io/@dev-hr2/Querydsl-%EC%97%90%EC%84%9C-%EB%8F%99%EC%A0%81-sorting%EC%9D%84-%EC%9C%84%ED%95%B4-OrderSpecifier-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@dev-hr2/Querydsl-%EC%97%90%EC%84%9C-%EB%8F%99%EC%A0%81-sorting%EC%9D%84-%EC%9C%84%ED%95%B4-OrderSpecifier-%ED%81%B4%EB%9E%98%EC%8A%A4-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 23 Apr 2023 10:00:15 GMT</pubDate>
            <description><![CDATA[<h3 id="querydsl-에서-동적-sorting을-위해-orderspecifier-클래스-구현">Querydsl 에서 동적 sorting을 위해 OrderSpecifier 클래스 구현</h3>
<h4 id="필요했던-케이스">필요했던 케이스</h4>
<pre><code>- paging 조회 시, 클라이언트가 설정한 정렬 조건 대로 내려줘야 했다.
- 정렬 조건은 가입일 (id가 순서를 보장하지 않는 케이스), 이름 (국영숫), 나이 등의 오름 || 내림차순</code></pre><h4 id="orderspecifier란">OrderSpecifier란?</h4>
<ul>
<li>공식 문서 : <a href="http://querydsl.com/static/querydsl/4.0.7/apidocs/com/querydsl/core/types/OrderSpecifier.html">http://querydsl.com/static/querydsl/4.0.7/apidocs/com/querydsl/core/types/OrderSpecifier.html</a></li>
<li>Querydsl 에서 동적 sorting을 위해 필요한 클래스</li>
</ul>
<h4 id="활용-예제">활용 예제</h4>
<pre><code class="language-java">    private OrderSpecifier getOrderCondition(AccountsSortType accountsSortType,  
      boolean ascending, QAccounts accounts) {  
       switch (accountsSortType) {  
          case JOIN_DATE:  
             return ascending ? accounts.joinDate.asc(): accounts.joinDate.desc();  
          case NAME:  
             return ascending ? accounts.name.asc() : accounts.name.desc();  
          case LAST_LOGIN:  
             return ascending ? accounts.lastLoginDate.asc()  
                   : accounts.lastLoginDate.desc();  
          default:  
             return null;  
       }  
}</code></pre>
<ul>
<li>케이스별로 sorting을 진행할 수 있게 구현함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mysql 5.7과 8.0의 차이점은?]]></title>
            <link>https://velog.io/@dev-hr2/Mysql-5.7%EA%B3%BC-8.0%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80</link>
            <guid>https://velog.io/@dev-hr2/Mysql-5.7%EA%B3%BC-8.0%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80</guid>
            <pubDate>Sun, 23 Apr 2023 09:47:12 GMT</pubDate>
            <description><![CDATA[<p>회사에서 쓰던 MySQL 5.7 버전을 8.x 버전으로 업그레이드 할 필요가 생겼다.
그래서 두 버전의 차이점과 업그레이드 작업 시 주의할 점을 간단하게 알아봤다.</p>
<h3 id="57-버전에-비해-개선된-점">5.7 버전에 비해 개선된 점</h3>
<p><strong>1. IO 바운드 읽기 전용</strong>
<img src="https://velog.velcdn.com/images/dev-hr2/post/5d27acf2-05d0-42f8-8129-1eca8beaaa29/image.png" alt="">
-&gt; 내림차순 인덱스를 지원하면서 읽기 성능에서 크게 개선됨</p>
<p><strong>2. 읽기, 쓰기의 개선(update No key)</strong>
<img src="https://velog.velcdn.com/images/dev-hr2/post/5e82dfbe-df93-4a9e-b3f4-43dbed14ca93/image.png" alt=""></p>
<p><strong>3. 이중 쓰기 버퍼, IO 바인딩 읽기 쓰기</strong>
<img src="https://velog.velcdn.com/images/dev-hr2/post/8210bee2-5a66-4f27-9a83-6f640a7b456f/image.png" alt=""></p>
<h3 id="80-버전의-변경-사항">8.0 버전의 변경 사항</h3>
<ul>
<li>기본 인증 플러그인으로 &#39;caching_sha2_password&#39; 사용<ul>
<li>sha256_password 사용 불가</li>
</ul>
</li>
<li>자체 파티셔닝 핸들러 제공</li>
</ul>
<h4 id="호환되지-않아-사전에-점검해야할-부분">호환되지 않아 사전에 점검해야할 부분</h4>
<ul>
<li><p>사용되지 않는 데이터 형식이나 함수를 사용하는 테이블이 없어야 함</p>
</li>
<li><p>orphan ~.frm파일이 없어야 함</p>
</li>
<li><p>기본 파티셔닝 지원이 없는 스토리지 엔진을 사용하는 분할된 테이블이 없어야 함</p>
</li>
<li><p>255자 또는 1020바이트 길이를 초과하는 개별 <code>ENUM</code> 또는 <code>SET</code> 열 요소가 있는 테이블이나 저장 프로시저가 없어야 함</p>
</li>
<li><p>MySQL 8.0.13 이상으로 업그레이드하기 전에 공유 InnoDB 테이블스페이스에 있는 테이블 파티션이 없어야 함</p>
</li>
<li><p><code>ASC</code> 절에 사용하는 <code>DESC</code> 또는 <code>GROUP BY</code> 한정자를 사용하는 MySQL 8.0.12 이하의 쿼리 및 저장 프로그램 정의가 없어야 함</p>
</li>
<li><p>64자를 초과하는 외래 키 제약 조건 이름이 없어야 함</p>
</li>
<li><p><code>utf8mb3</code> -&gt; <code>utf8mb4</code> charset을 사용하도록 변환 필요</p>
</li>
</ul>
<p>출처 (MySQL 공식 문서)</p>
<ul>
<li><a href="https://www.mysql.com/why-mysql/benchmarks/mysql/">5.7 버전에 비해 개선된 점</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html">8.0의 변경 사항</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html">MySQL 8.0의 새 기능</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>