<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>다 잘 될거야!</title>
        <link>https://velog.io/</link>
        <description>호기심이 많고, 문제를 끝까지 해결하려는 집념이 강한 개발자입니다.</description>
        <lastBuildDate>Fri, 12 Sep 2025 03:59:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>다 잘 될거야!</title>
            <url>https://velog.velcdn.com/images/developer-daily/profile/0fb7d907-cc81-4b63-a66d-34129c10a8c3/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 다 잘 될거야!. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/developer-daily" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Database Lock]]></title>
            <link>https://velog.io/@developer-daily/Database-Lock</link>
            <guid>https://velog.io/@developer-daily/Database-Lock</guid>
            <pubDate>Fri, 12 Sep 2025 03:59:18 GMT</pubDate>
            <description><![CDATA[<p>해당 글을 읽기 전 트랜잭션에 대해 잘 모른다면 <a href="https://velog.io/@developer-daily/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%98-%EA%B2%A9%EB%A6%AC%EC%84%B1%EA%B3%BC-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80">트랜잭션 포스트</a>를 읽고 와주세요!</p>
<p>데이터베이스는 항상 데이터의 일관성과 무결성이 보장되어야한다. 하지만 동시에 많은 세션이 DB에 접근한다면 이런 일관성과 무결성이 깨지느 <strong>동시성 문제</strong>가 발생할 수 있다.</p>
<p>예를 들어 고객 2명이 상품이 구매하는 경우를 가정해보자.
<strong>그러면 고객 1과 고객 2 모두 상품의 재고를 조회하고 상품의 재고를 1 차감하는 트랜잭션이 발생할 것이다.</strong>
여기서 고객 1의 트랜잭션을 T1, 고객 2의 트랜잭션을 T2라고 하겠다.</p>
<pre><code>1. (T1) 고객 1이 상품의 재고를 조회한다. 
조회 결과 5개로 확인되었다.

2. (T1) 고객 1이 상품을 구매하여 재고가 1 차감된다.
(현재 재고 4)

3. (T2) 고객 2가 상품의 개수를 조회한다.
조회 결과 5개로 확인되었다.

4. (T1) 트랜잭션 1이 커밋된다.

5. (T2) 고객 2가 상품을 구매하여 재고가 1 차감된다.
(현재 재고 4)

6. (T2) 트랜잭션 2이 커밋된다.</code></pre><p>결과적으로 두명의 고객이 상품을 구매했지만 재고는 3개가 아닌 4개로 수정되었다. 이유는 <strong>T2가 T1의 트랜잭션이 커밋되기 전에 실행되었기 때문이다.</strong>
이와 같이 트랜잭션끼리 서로 충돌이 발생하면 데이터의 불일치가 발생하여 무결성과 일관성이 깨지게 된다. </p>
<p>이러한 동시성 문제를 해결하기 위해 우리는 Lock을 사용할 수 있다.</p>
<h3 id="락lock">락(Lock)?</h3>
<p>데이터베이스의 일관성과 무결성을 유지하기 위해 트랜잭션 처리의 순차성을 보장하는 방법이다.
즉, 여러 개의 트랜잭션이 동시에 접근했을 때 일관성이 깨질 수 있으므로 잠금을 하여 다른 트랜잭션이 접근하지 못하도록 한다.</p>
<p>Lock은 크게 낙관적 락과 비관적 락으로 구분할 수 있다.</p>
<h3 id="낙관적-락optimistic-lock">낙관적 락(Optimistic Lock)</h3>
<p><strong>낙관적 락은 대부분의 트랜잭션이 충돌이 발생하지 않을 것이라고 낙관적으로 가정하는 방법</strong>이다. 따라서 서로 다른 트랜잭션이 동시에 접근하여 트랜잭션을 처리할 수 있다. </p>
<p>그렇다고 완전히 내버려두면 충돌이 발생하였을 때 데이터의 일관성이 무너질 수 있다.
그래서 <strong>낙관적 락에서는 DB 단이 아닌 어플리케이션 단에서 엔티티 버전을 통해 동시성을 제어하고 이때 version과 같은 구분 칼럼을 이용하여 충돌을 방지</strong>한다.
(hashCode, timestamp 같은 방법도 있다)</p>
<h3 id="jpa에서-낙관적-락을-적용하는-방법">JPA에서 낙관적 락을 적용하는 방법</h3>
<p>가장 간단한 방법은 <code>@Version</code> 어노테이션을 사용하는 것이다.</p>
<pre><code class="language-java">@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    private String name;

    private Integer price;

    @Version
    private Long version;

}</code></pre>
<p>위와 같이 <code>version</code> 필드를 엔티티에 추가하면 JPA에서 자동적으로 버전 값을 하나씩 증가시킨다. </p>
<pre><code>UPDATE PRODUCT
SET
  name = ?,
  version = ? # 버전 + 1 증가
WHERE
  id = ?,
  and version = ? # 버전 비교</code></pre><p>이렇게 하면 위와 같이 엔티티를 업데이트 할 때 엔티티 조회 시점의 version과 일치하는 엔티티를 찾고 많약 존재하지 않으면 예외를 발생한다. 그러면 개발자 직접 수동으로 롤백 처리를 하거나 이전 트랜잭션의 커밋이 완료되고 다시 예외가 발생한 트랜잭션을 재시도하는 방식으로 접근해야한다.
(어찌보면 좀 까다로운 방법이다😣)</p>
<p>주의할 점은 해당 version 필드는 JPA가 자동으로 관리하므로 개발자가 임의로 수정해서는 안된다.</p>
<p>이외에도 LockModeType 통해 락 옵션을 변경할 수 있는데 내용이 너무 길어지니 다음에 기회가 되면 포스팅 하겠다.</p>
<h3 id="비관적-락pessimistic-lock">비관적 락(Pessimistic Lock)</h3>
<p>비관적 락은 데이터를 수정하기 전에 해당 데이터에 대한 접근을 미리 제한하는 방식이다.
즉, 트랜잭션 작업 간에 충돌이 발생할 것이라고 미리 예상하고 락을 거는 방법이다.</p>
<p>비관적 락은 크게 공유 락(Shared Lock)과 배타 락(Exclusive Lock)이 있으며 트랜잭션이 시작될 때 해당 락을 걸고 시작한다.</p>
<p>공유 락은 읽기 작업 시, 배타락은 쓰기 작업 시에 사용된다고 일단은 알아두자.</p>
<h3 id="공유-락shared-lock">공유 락(Shared Lock)</h3>
<p>공유 락은 데이터를 변경하지 않은 읽기 작업을 할 때 사용한다. 그렇기 때문에 다음과 같은 특징을 가지고 있다.</p>
<ul>
<li>하나의 트랜잭션이 읽기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 읽더라도 문제가 발생하지 않으므로 다른 공유 락을 막을 필요가 없다.</li>
<li>하나의 트랜잭션이 읽기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 수정한다면 데이터 정합성이 지켜지지 않을 수 있으므로 막을 필요가 있다.</li>
</ul>
<p>즉, 공유 락이 걸어진 상황에서 다른 공유 락이 접근해도 되지만 배타 락은 접근하면 안된다.</p>
<pre><code>공유 락 - 공유 락 (O)
공유 락 - 배타 락 (X)</code></pre><h3 id="공유-락exclusive-lock">공유 락(Exclusive Lock)</h3>
<p>배타 락은 데이터를 변경하는 쓰기 작업을 할 때 사용된다. 해당 락도 특징을 살펴보자</p>
<ul>
<li>하나의 트랜잭션이 쓰기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 읽기 작업을 한다면 데이터 정합성 지켜지지 않을 수 있으므로 막을 필요가 있다.</li>
<li>하나의 트랜잭션이 쓰기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 쓰기 작업을 한다면 데이터 정합성이 지켜지지 않을 수 있으므로 막을 필요가 있다.</li>
</ul>
<p>즉. 배타 락이 걸어진 상황에서는 다른 공유 락이나 배타 락이 접근해서는 안된다.</p>
<pre><code>베타 락 - 공유 락 (X)
베타 락 - 배타 락 (X)</code></pre><h3 id="블로킹blocking">블로킹(Blocking)</h3>
<p>그렇다면 앞서 말한 접근하면 안되는 Lock들 끼리 서로 충돌이 발생하면 어떻게 될까?
이럴 경우 먼저 Lock을 설정한 트랜잭션이 끝날 때까지 다른 트랜잭션은 대기를 해야한다.</p>
<p>이런식으로 <strong>Lock간의 경합이 발생하여 특정 트랜잭션이 작업을 진행하지 못하고 대기하는 상황을 블로킹</strong>이라고 한다.
<img src="https://velog.velcdn.com/images/developer-daily/post/f52ed03d-b74c-42e1-adfa-03bac522b109/image.png" alt=""></p>
<p>위 그림은 보면 트랜잭션 A가 먼저 공유 락을 설정하였기 때문에 배타 락을 건 트랜잭션 B가 대기하며 블로킹 상태에 빠진 것을 알 수 있다.</p>
<h3 id="데드락dead-lock">데드락(Dead Lock)</h3>
<p>그렇다면 두 개의 트랜잭션에 모두 블로킹이 발생하면 어떻게 될까?
<img src="https://velog.velcdn.com/images/developer-daily/post/fb743168-c7a8-4186-85fa-e95537f4120d/image.png" alt=""></p>
<p>트랜잭션 A와 B 각각 Coupon 데이터에 X-L을 Member 데이터에 X-L을 걸었다. 그러면 이제 트랜잭선이 끝날 때까지 어떠한 S-L, X-L도 해당 데이터에 접근할 수 없다. 근데 트랜잭션 A와 B가 이미 락이 걸린 데이터에 S-L을 걸려고 한다. 이로 이로 인해 트랜잭션 A와 B 모두 블로킹에 빠져 무한대기가 발생한다. </p>
<p>이와 같이 A와 B 모두 블로킹이 발생하여 무한대기에 빠진 상태를 데드락이라고 한다.</p>
<p>보통 이런 경우 DBMS에서 자동으로 데드락을 감지하여 롤백을 진행하여 문제를 해결한다.</p>
<h3 id="낙관적-락-vs-비관적-락">낙관적 락 VS 비관적 락</h3>
<p>지금까지 낙관적 락, 비관적 락에 대해 알아보았다. 그러면 어떤 걸 쓰는 것이 좋을까?</p>
<p>낙관적 락은 한 트랜잭션이 데이터를 선점하고 있는 상황에서도 다른 트랜잭션이 접근할 수 있기 때문에 상황에 따라 조금 더 빠른 조회가 가능할 거 같다. 하지만 충돌이 발생했을 경우 트랜잭션을 재시도하는 로직을 작성해야하며 만약 재시도가 불가능한 로직이라면 개발자 직접 수동으로 롤백을 진행해야 한다는 불편한 점이 있다.</p>
<blockquote>
<p>낙관적 락은 충돌이 빈번하게 발생하지 않고 성능이 중요한 상황에서 사용하는 것이 좋을 거 같다.</p>
</blockquote>
<p>비관적 락은 한 트랜잭션에서 락을 건 상황에서 다른 트랜잭션이 락을 걸고자 할 때 블로킹이 발생하여 대기하는 시간이 존재한다. 또한 트랜잭션 모두가 블로킹이 발생한 경우 무한대기라는 데드락이 발생할 수 있다. 하지만 충돌이 발생할 경우 DB 단에서 블로킹을 통해 데이터의 정합성과 일관성을 유지해주기 때문에 개발자 입장에서는 편하다고 볼 수 있다.</p>
<blockquote>
<p>비관적 락은 충돌이 빈번하게 발생하는 상황에서 데이터의 일관성과 정합성이 중요한 상황에서 사용되는 것이 좋다고 생각된다.</p>
</blockquote>
<p>이 둘 중 뭐가 더 좋다기 보다는 상황에 따라 적절하게 쓰는 것이 좋다고 생각한다.</p>
<h3 id="마무리">마무리</h3>
<p>여담이지만 해당 포스트는 다른 사람들의 포트폴리오를 구경하던 중 동시성 이슈와 데드락에 대한 내용이 자주 등장해 궁금증을 해소하고자 데드락을 공부하고 정리하며 시작됐다.</p>
<p>데드락 하나를 제대로 이해하기 위해 트랜잭션부터 Lock 개념까지 다양한 내용을 습득하는 시간을 가졌다. 이틀이라는 시간동안 짬짬히 시간을 내어 공부하니 오랜만에 Deep하게 CS 공부를 한 거 같아 나름 뿌듯하다. </p>
<p>나의 공부기록이 다른 사람들에게도 도움이 되었으면 좋겠다!😊</p>
<p><strong>짧지만 어려운 내용 봐주셔서 감사합니다. 모두들 파이팅!💪🏻</strong></p>
<h3 id="출처">출처</h3>
<p><a href="https://gyeongsuuuu.tistory.com/68">https://gyeongsuuuu.tistory.com/68</a>
<a href="https://ksh-coding.tistory.com/121">https://ksh-coding.tistory.com/121</a>
<a href="https://chaewsscode.tistory.com/181">https://chaewsscode.tistory.com/181</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[트랜잭션의 격리성과 격리 수준]]></title>
            <link>https://velog.io/@developer-daily/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%98-%EA%B2%A9%EB%A6%AC%EC%84%B1%EA%B3%BC-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80</link>
            <guid>https://velog.io/@developer-daily/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%98-%EA%B2%A9%EB%A6%AC%EC%84%B1%EA%B3%BC-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80</guid>
            <pubDate>Thu, 11 Sep 2025 02:04:17 GMT</pubDate>
            <description><![CDATA[<p>이번에는 트랜잭션에 대한 개념을 정립하고 트랜잭션의 격리성으로 인해 발생할 수 있는 문제점과 해소 방법에 대해 알아본다.</p>
<h3 id="트랜잭션">트랜잭션</h3>
<p>데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 최소 단위이다.
즉, 데이터베이스에 여러 개의 작업(INSERT, UPDATE, DELETE 등)을 묶어서 <strong>모두 성공하면 반영하고, 하나라도 실패하면 전부 취소하는 것</strong>을 의미한다.</p>
<h3 id="트랜잭션은-왜-사용할까">트랜잭션은 왜 사용할까?</h3>
<pre><code class="language-sql">BEGIN;

-- 주문 테이블에 새로운 주문 추가
INSERT INTO orders(user_id, product_id, quantity) VALUES (1, 101, 2);

-- 상품 재고 차감
UPDATE products SET stock = stock - 2 WHERE product_id = 101;

-- 결제 내역 기록
INSERT INTO payments(order_id, amount, status) VALUES (LAST_INSERT_ID(), 50000, &#39;SUCCESS&#39;);

COMMIT;</code></pre>
<p>위와 같이 상품을 주문한 뒤 상품 재고를 차감하고 결제 내역을 기록하는 트랜잭션이 있다고 생각해보자.
만약 주문은 발생했지만 상품 재고의 차감하는 과정에서 오류가 발생한다면 돈만 결제되고 재고는 안 줄어드는 불상사가 발생하고 이 데이터는 믿을 수 없는 데이터가 된다.</p>
<p>이를 해결하기 위한 방법으로 DB는 <strong>All Or Nothing</strong> 전략을 취한다.
즉, 모든 작업을 성공적으로 수행하거나 작업을 수행하기 전으로 RollBack을 한다.</p>
<hr>
<h3 id="트랜잭션의-특징">트랜잭션의 특징</h3>
<ul>
<li>앞서 트랜잭션이 무엇인지 살펴보았으니 이번에는 특징을 살펴보자</li>
<li>트랜잭션은 총 4가지의 특징(ACID)을 가진다</li>
</ul>
<h4 id="원자성atomicity">원자성(<strong>Atomicity)</strong></h4>
<ul>
<li>트랜잭션은 더이상 분해가 불가능한 최소의 단위이므로, 전부 처리되거나 아예 하나도 처리되지 않아야한다.</li>
</ul>
<h4 id="일관성consistency"><strong>일관성(Consistency)</strong></h4>
<ul>
<li>데이터베이스가 일관된 상태에 있을때 새로운 하나의 트랜잭션이 성공하면, 성공 이후의 데이터베이스도 일관 된 상태를 유지해야한다.</li>
</ul>
<h4 id="격리성isolation"><strong>격리성(Isolation)</strong></h4>
<ul>
<li>트랜잭션의 실행 중간에 다른 트랜잭션이 접근할 수 없다.</li>
</ul>
<h4 id="durability-지속성">Durability (지속성)</h4>
<ul>
<li>트랜잭션이 성공적으로 완료되면, 그 시스템에 장애가 발생하여도 보장되어야한다.</li>
</ul>
<hr>
<h3 id="완전한-격리">완전한 격리?</h3>
<p>트랜잭션의 수행 중간에 다른 트랜잭션이 간섭을 할 수 없다면 당연히 모든 트랜잭션은 순차적으로 처리될 것이며 데이터의 정합성을 보장할 수 있을 것이다. 하지만 이럴 경우 트랜잭션을 처리하는 동안 다른 트랜잭션들은 데이터에 접근할 수 없으며 그만큼 대시시간이 증가하여 성능 저하로 이어질 것이다.</p>
<p>그렇기 때문에 사용자에게 불편함없이 처리 속도를 유지하기 위해서는 완전한 격리보다는 적당한 격리가 필요하다.
하지만 완전한 격리를 하지 않을 경우 다음과 같은 문제가 발생한다.</p>
<hr>
<h3 id="격리성으로-인해-발생할-수-있는-문제">격리성으로 인해 발생할 수 있는 문제</h3>
<p>완전한 격리가 아닌 어느 정도 허용된 격리를 하여 다른 트랜잭션이 간섭을 하게 될 경우 다음과 같은 격리성 문제(<strong>Dirty Read</strong>, <strong>Non-Repetable Read</strong>, <strong>Phantom Read</strong>)가 발생할 수 있다.</p>
<h4 id="dirty-read">Dirty Read</h4>
<p>다른 트랜잭션으로 데이터가 수정되었지만 커밋되지 않은 상태에서 데이터를 조회하는 경우를 말한다.
다른 트랜잭션에서 커밋을 할지 롤백을 할지 알 수 없기 때문에 문제가 발생한다.
<img src="https://velog.velcdn.com/images/developer-daily/post/ca9ffc19-c2a0-434d-acf6-d0402bfb3a16/image.png" alt=""></p>
<h4 id="non-repeatable-read">Non-Repeatable Read</h4>
<p>하나의 트랜잭션(T1)이 데이터를 두 번 읽었는데, 중간에 다른 트랜잭션(T2)이 그 데이터를 수정하거나 삭제해버리면 읽는 값이 달라지는 현상을 말한다.
이는 다른 트랙잭션에서 데이터의 값을 수정하였기 때문이다.
<img src="https://velog.velcdn.com/images/developer-daily/post/d21686d8-d207-46eb-a1ba-dd5e8089b849/image.png" alt=""></p>
<h4 id="phantom-read">Phantom Read</h4>
<p>한 트랜잭션 내 같은 조건으로 조회를 했을 때 행의 개수가 달라지는 현상을 말한다.
이는 다른 트랜잭션에서 데이터를 추가하거나 삭제하였기 때문이다.
<img src="https://velog.velcdn.com/images/developer-daily/post/0fc4d871-c71c-4d35-ba6c-1fdcfc4295f9/image.png" alt=""></p>
<hr>
<h3 id="격리수준">격리수준</h3>
<p>DBMS에서는 위 문제를 제어하기 위해 격리수준을 4단계로 나눈다.</p>
<h4 id="read-uncommitted">Read Uncommitted</h4>
<p>트랜잭션에서 처리중이지만 아직 커밋되지 않은 데이터를 다른 트랜잭션에서 읽는 것을 허용한다. 해당 수준에서는 Dirty Read, Non-Repeatable Read, Phantom Read가 일어날 수 있으며, 정합성 문제가 발생할 수 있다.</p>
<h4 id="read-committed">Read Committed</h4>
<p>트랜잭션의 커밋이 확정된 데이터만 다른 트랙잭션에서 읽을 수 있도록 허용한다. 커밋 되지 않은 데이터에 대해서는 실제 DB 데이터가 아닌 Undo 로그에 있는 이전 데이터를 가져온다. 해당 수준에서는 여전히 Non-Repeatable Read, Phantom Read가 발생할 수 있다.</p>
<h4 id="repeated-read">Repeated Read</h4>
<p>앞서 Non-Repeatable Read는 하나의 트랜잭션(T1)이 데이터를 두 번 읽었는데, 중간에 다른 트랜잭션(T2)이 그 데이터를 수정하거나 삭제해버리면 읽는 값이 달라지는 현상이라고 했다.
<strong>Repeated Read</strong>에서는 T2에서의 데이터 수정 혹은 삭제가 발생할 경우 Undo 로그에 기존 데이터를 저장해두고 T1에서 데이터를 읽을려고 할 때 Undo 로그에 저장된 백업 데이터를 가져와 사용한다.</p>
<p>이렇게하면 삭제와 수정에 대해서 트랜잭션내에서 불일치를 가져오던 Non-Reapeatable Read를 해소할 수 있습니다.</p>
<h4 id="serializable-read">Serializable Read</h4>
<p>트랜잭션 내에서 쿼리를 두 번 이상 수행할 때, 첫번째 쿼리에 있던 결과에 어떠한 변경이나 추가가 나타나지 않도록 한다.
즉, 항상 동일한 쿼리 결과를 보여준다.</p>
<h3 id="출처">출처</h3>
<p><a href="https://innovation123.tistory.com/166">https://innovation123.tistory.com/166</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] TCP와 UDP]]></title>
            <link>https://velog.io/@developer-daily/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCP%EC%99%80-UDP</link>
            <guid>https://velog.io/@developer-daily/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-TCP%EC%99%80-UDP</guid>
            <pubDate>Tue, 20 May 2025 07:21:05 GMT</pubDate>
            <description><![CDATA[<h3 id="tcp-transmission-control-protocol">TCP (Transmission Control Protocol)</h3>
<p>TCP는 데이터를 안전하게 전송하기 위한 전송 계층 프로토콜입니다.<br>HTTP, FTP, 이메일 등 <strong>신뢰성이 중요한 상황</strong>에서 주로 사용됩니다.</p>
<p>TCP의 주요 특성:</p>
<ol>
<li><p><strong>연결지향적 프로토콜</strong><br>데이터를 주고받기 전 송신자와 수신자 간 연결을 수립한 후 데이터를 전송합니다.<br>연결을 위해서는 3-way handshake, 연결 해제를 위해서는 4-way handshake를 사용합니다.</p>
</li>
<li><p><strong>재조립과 재전송</strong><br>데이터 전송 중 순서가 바뀌거나 유실될 경우 재조립 및 재전송을 통해 보완합니다.</p>
</li>
<li><p><strong>혼잡 제어와 흐름 제어</strong><br>네트워크 상태에 맞춰 전송 속도를 조절하여 효율적이고 안정적인 데이터 전달을 수행합니다.</p>
</li>
</ol>
<h4 id="연결지향적-프로토콜">연결지향적 프로토콜</h4>
<ul>
<li>데이터를 주고받기 전 <strong>3-way handshake</strong> 과정을 통해 통신 연결을 확립합니다.  </li>
<li>송신자와 수신자 간 안정적인 데이터 통신을 보장합니다.</li>
</ul>
<h4 id="흐름-제어-flow-control">흐름 제어 (Flow Control)</h4>
<ul>
<li>송신 측과 수신 측의 데이터 처리 속도 차이를 극복하는 기법입니다.</li>
<li>수신 측이 송신 측보다 데이터 처리 속도가 빠를 경우 문제가 없지만 반대의 경우 문제가 발생합니다.</li>
<li>수신 측이 감당가능한 용량을 초과한 경우 데이터가 유실될 수 있으며 이로 인해 재전송에 대한 오버헤드가 발생할 수 있습니다.</li>
<li>이러한 위험을 줄이기 위해 송신 측의 데이터 전송량을 수신측에 따라 조절해야합니다.</li>
<li>이에 대한 해결 방법으로 <strong>Stop and Wait, Sliding Window</strong> 등의 방법이 있다.</li>
</ul>
<h4 id="혼잡-제어-congestion-control">혼잡 제어 (Congestion Control)</h4>
<ul>
<li>송신측의 데이터 전달과 네트워크의 데이터 처리 속도 차이를 해결하기 위한 기법입니다.</li>
<li>송신 측에서 보내는 데이터는 인터넷을 통해 수신 측에 전달됩니다. 이 과정에서 특정 라우터에 데이터가 몰리게 될 경우 송신 측에서 보낸 데이터를 처리할 수 없으며 또 다시 재전송을 하게 되면서 혼잡만 가중됩니다. 이러한 상황을 막기 위해 송신측에서 데이터 전송을 강제로 줄이게 되는데 이를 혼잡 제어라고 합니다.</li>
</ul>
<hr>
<h3 id="udp-user-datagram-protocol">UDP (User Datagram Protocol)</h3>
<p>UDP는 <strong>비연결형 전송 계층 프로토콜</strong>입니다.</p>
<ul>
<li>데이터를 전송하기 전 <strong>연결을 수립하지 않기 때문에 빠르고 오버헤드가 적습니다.</strong></li>
<li>데이터의 <strong>순서 보장 및 복구 기능은 없습니다.</strong></li>
<li>신뢰성보다 속도가 중요한 경우에 적합합니다.</li>
<li>그렇기 때문에 스트리밍, VoIP와 같은 실시간적이고 빠른 전송이 중요한 경우에 사용됩니다.</li>
</ul>
<table>
<thead>
<tr>
<th>항목</th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody><tr>
<td>연결 방식</td>
<td>연결지향형</td>
<td>비연결형</td>
</tr>
<tr>
<td>신뢰성</td>
<td>높음</td>
<td>낮음</td>
</tr>
<tr>
<td>속도</td>
<td>느림</td>
<td>빠름</td>
</tr>
<tr>
<td>오버헤드</td>
<td>큼</td>
<td>작음</td>
</tr>
<tr>
<td>사용 예시</td>
<td>HTTP, FTP, 이메일</td>
<td>스트리밍, VoIP, 게임</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[네트워크] OSI 7계층]]></title>
            <link>https://velog.io/@developer-daily/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-OSI-7%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@developer-daily/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-OSI-7%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Mon, 19 May 2025 04:54:19 GMT</pubDate>
            <description><![CDATA[<h3 id="osi-7계층이란">OSI 7계층이란?</h3>
<p>OSI 7 계층은 네트워크 통신과정에서 일어나는 일을 7가지 단계로 나눈 것을 말합니다.</p>
<h4 id="왜-7단계로-나누었을까">왜 7단계로 나누었을까?</h4>
<ul>
<li>통신과정에서 일어나는 일을 단계별로 알 수 있으며,  </li>
<li>특정한 곳에 이상이 발생하면 해당 단계만 수정하면 되기 때문입니다.</li>
</ul>
<p>예를 들어,  </p>
<ul>
<li>회사 내 모든 컴퓨터에서 문제가 발생한 경우 → 1~3계층 문제일 가능성  </li>
<li>특정 컴퓨터에서만 문제가 발생한 경우 → 4~7계층 문제일 가능성</li>
</ul>
<h3 id="각-계층에-대해-알아보자">각 계층에 대해 알아보자</h3>
<h4 id="1-physical-layer-물리-계층-1계층">1. Physical Layer (물리 계층, 1계층)</h4>
<ul>
<li>데이터를 전기적인 신호로 변환하여 통신 케이블을 통해 주고받는 역할을 합니다.  </li>
<li>데이터 전달에만 신경 쓰며, 어떤 데이터인지, 오류 여부 등은 신경 쓰지 않습니다.  </li>
<li>케이블, 리피터, 허브 등을 통해 데이터 전송이 이루어집니다.</li>
</ul>
<h4 id="2-datalink-layer-데이터링크-계층-2계층">2. DataLink Layer (데이터링크 계층, 2계층)</h4>
<ul>
<li>물리 계층으로 전달되는 데이터에 대해 오류와 흐름을 관리하여 안정적으로 정보가 전달되도록 합니다.  </li>
<li>같은 네트워크에 포함된 두 장치 간의 데이터 전달을 담당합니다.  </li>
<li>데이터를 프레임(Frame) 단위로 캡슐화합니다.  </li>
<li>브릿지, 스위치를 통해 MAC 주소를 기반으로 통신합니다.  </li>
<li>오류 검출, 흐름 제어, 재전송 기능이 포함됩니다.</li>
</ul>
<p>관련 프로토콜: Ethernet, Wi-Fi(802.11), PPP, ARP</p>
<h4 id="3-network-layer-네트워크-계층-3계층">3. Network Layer (네트워크 계층, 3계층)</h4>
<ul>
<li>데이터를 다른 네트워크에 있는 목적지까지 안전하고 빠르게 전달합니다.  </li>
<li>라우터를 통해 목적지 IP 주소까지 경로를 탐색합니다.  </li>
<li>그리고 4계층에서 전달받은 데이터에 IP 헤더를 추가하여 패킷(Packet) 단위로 전달합니다.</li>
</ul>
<p><strong>전달 예시</strong></p>
<ol>
<li>&quot;안녕&quot;이라는 메시지를 보내려 할 때, A의 IP주소를 IP헤더로 붙여 스위치로 보냅니다.  </li>
<li>스위치에선 현재 같은 네트워크에 1.32.xx.xx 주소가 있는지 확인합니다.  </li>
<li>해당 주소를 가진 기기가 없을 경우 데이터를 라우터로 전송합니다.  </li>
<li>라우터에서 최적의 경로를 탐색하여 데이터를 전달합니다.  </li>
<li>목적지 라우터에서 스위치로 전달합니다.  </li>
<li>스위치가 1.32.xx.xx 주소를 가진 기기로 데이터 전달합니다.</li>
</ol>
<h4 id="4-transport-layer-전송-계층-4계층">4. Transport Layer (전송 계층, 4계층)</h4>
<ul>
<li>TCP와 UDP 프로토콜을 통해 통신을 활성화합니다.  </li>
<li>포트를 열어두고 프로그램들이 전송을 할 수 있도록 합니다.</li>
</ul>
<table>
<thead>
<tr>
<th>프로토콜</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>TCP</td>
<td>신뢰성 보장, 연결지향형</td>
</tr>
<tr>
<td>UDP</td>
<td>비신뢰성, 비연결형, 실시간</td>
</tr>
</tbody></table>
<h4 id="5-session-layer-세션-계층-5계층">5. Session Layer (세션 계층, 5계층)</h4>
<ul>
<li>응용 프로그램 간의 연결, 유지, 종료를 관리합니다.  </li>
<li>데이터 교환 중 에러가 발생하면 동기화 지점을 설정해 복원할 수 있도록 합니다.  </li>
<li>전송 계층에서도 연결을 맺고 종료할 수 있지만,<br>세션 계층은 응용 프로그램 관점에서 관리합니다.</li>
</ul>
<h4 id="6-presentation-layer-표현-계층-6계층">6. Presentation Layer (표현 계층, 6계층)</h4>
<ul>
<li>전송하는 데이터의 표현방식을 결정합니다.  </li>
<li>데이터 압축, 암호화, 인코딩/디코딩을 수행합니다.</li>
</ul>
<h4 id="7-application-layer-응용-계층-7계층">7. Application Layer (응용 계층, 7계층)</h4>
<ul>
<li>최종 목적지, 사용자와 가장 가까운 계층입니다.  </li>
<li>응용 프로세스와 직접 연결되며, 일반적인 응용 서비스를 수행합니다.</li>
</ul>
<p>대표 프로토콜: HTTP, FTP, SMTP 등</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring boot] Filter를 활용한 로그 관리]]></title>
            <link>https://velog.io/@developer-daily/Spring-boot-Filter%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@developer-daily/Spring-boot-Filter%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Fri, 25 Apr 2025 14:08:47 GMT</pubDate>
            <description><![CDATA[<p>앞서 <a href="https://velog.io/@developer-daily/Spring-boot-AOP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-Logging-%EA%B5%AC%ED%98%84feat.-%EB%AC%B8%EC%A0%9C%EC%A0%90">AOP를 사용한 로그 관리</a>를 살펴보았다.
하지만 본인이 느끼기에 몇가지 문제가 있었다. 그래서 이번에는 AOP가 아닌 <strong>Filter를 활용하여 Spring boot에서 로그 관리</strong>를 해볼려고 한다.</p>
<h3 id="1-필터-구현">1. 필터 구현</h3>
<p>우선 본인은 아래와 같이 <code>LogFilter</code>를 구현했다</p>
<pre><code class="language-java">import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.uni_bag.uni_bag_spring_boot_app.config.CustomHttpRequestWrapper;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Slf4j
public class LogFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        requestWrapper.getInputStream();

        boolean isShowingRequestLog = checkRequestLogShow(requestWrapper);

        if(isShowingRequestLog) {
            // 요청 로그 출력
            log.info(&quot;[REQUEST] {} {} | IP: {} | User-Agent: {} | RequestBody:{}&quot;,
                    request.getMethod(),
                    request.getRequestURI(),
                    request.getRemoteAddr(),
                    request.getHeader(&quot;User-Agent&quot;),
                    getRequestBody(requestWrapper)
            );
        }

        // 필터 체인 호출
        filterChain.doFilter(requestWrapper, responseWrapper);

        long duration = System.currentTimeMillis() - startTime;

        boolean isShowingResponseLog = checkResponseLogShow(requestWrapper);

        if(isShowingResponseLog) {
            // 응답 로그 출력
            log.info(&quot;[RESPONSE] {} {} | Status: {} | Time Taken: {}ms | ResponseBody:{}&quot;,
                    request.getMethod(),
                    request.getRequestURI(),
                    response.getStatus(),
                    duration,
                    new String(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8)
            );
        }

        responseWrapper.copyBodyToResponse(); // 요청을 전달
    }

    private String getRequestBody(CustomHttpRequestWrapper requestWrapper) {
        byte[] content = requestWrapper.getRequestBody();
        if (content.length == 0) {
            return &quot;&quot;;
        }

        try {
            ObjectMapper objectMapper = new ObjectMapper();
            Object json = objectMapper.readValue(content, Object.class);
            return objectMapper.writeValueAsString(json); // 한 줄 JSON 문자열 반환
        } catch (IOException e) {
            return new String(content, StandardCharsets.UTF_8);
        }
    }

    private boolean checkRequestLogShow(HttpServletRequest request) {
        return !request.getRequestURI().startsWith(&quot;/actuator&quot;);
    }

    private boolean checkResponseLogShow(HttpServletRequest request) {
        return !request.getRequestURI().startsWith(&quot;/actuator&quot;);
    }
}
</code></pre>
<p>이 <code>LogFilter</code> 클래스는 Spring Boot에서 HTTP 요청과 응답을 로깅하기 위한 커스텀 필터로 다음과 같은 기능을 한다.</p>
<h4 id="요청-및-응답-정보-캐싱">요청 및 응답 정보 캐싱</h4>
<pre><code class="language-java">CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
requestWrapper.getInputStream();</code></pre>
<p>Spring Boot에서는 특정 필터에서 한번 요청과 응답에 대한 객체를 읽을 경우 다른 필터에서 또 다시 요청과 응답 객체를 읽을 수 없다. 그래서 <strong>해당 정보들을 다른 필터(<code>logFilter</code>)에서 사용하기 위해 <code>CustomHttpRequestWrapper</code>와 <code>ContentCachingResponseWrapper</code>를 사용해 캐시를 진행했다.</strong></p>
<p>요청 캐싱 객체에 대해서는 아래에서 설명한다.</p>
<h4 id="요청request-정보-로깅">요청(Request) 정보 로깅</h4>
<pre><code class="language-java">long startTime = System.currentTimeMillis();

boolean isShowingRequestLog = checkRequestLogShow(requestWrapper);

if(isShowingRequestLog) {
    // 요청 로그 출력
    log.info(&quot;[REQUEST] {} {} | IP: {} | User-Agent: {} | RequestBody:{}&quot;,
            request.getMethod(),
            request.getRequestURI(),
            request.getRemoteAddr(),
            request.getHeader(&quot;User-Agent&quot;),
            getRequestBody(requestWrapper)
    );
}
</code></pre>
<p><code>LogFilter</code>는 먼저 요청 정보에 대한 로그를 작성한다. 여기서는 <strong>IP, User-Agent, HTTP 메서드, URI, 요청 본문</strong>을 로깅한다.</p>
<p>또한 특정 요청에 대해서는 로깅을 피하기 위해 요청을 로깅할지 말지 판단하는 <code>checkRequestLogShow()</code> 메서드를 거치게 된다. 본인은 <code>actuactor</code>에서 발생한 로그를 숨기기 위해 해당 메소드를 사용했다.</p>
<h4 id="응답response-정보-로깅">응답(Response) 정보 로깅</h4>
<pre><code class="language-java">// 필터 체인 호출
filterChain.doFilter(requestWrapper, responseWrapper); // 다음 필터를 호출하고 남은 비지니스 로직을 수행

long duration = System.currentTimeMillis() - startTime;

boolean isShowingResponseLog = checkResponseLogShow(requestWrapper);

if(isShowingResponseLog) {
    // 응답 로그 출력
    log.info(&quot;[RESPONSE] {} {} | Status: {} | Time Taken: {}ms | ResponseBody:{}&quot;,
            request.getMethod(),
            request.getRequestURI(),
            response.getStatus(),
            duration,
            new String(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8)
    );
}

responseWrapper.copyBodyToResponse(); </code></pre>
<p><code>LogFilter</code>는 앞서 요청 로그를 작성하고 요청에 대한 비지니스 로직을 실행한 뒤 응답 로그를 작성한다.
응답에서도 요청과 동일하게 <strong>로그 작성 여부를 확인하고 IP, User-Agent, HTTP 메서드, URI, 응답 본문을 로깅</strong> 한다.</p>
<p>여기서는 <code>responseWrapper.copyBodyToResponse();</code> 가 사용되었는데 이는 응답 본문을 다시 실제 response에 복사해서 클라이언트로 전달되도록 한다. ContentCachingResponseWrapper는 응답 내용을 캐시에 저장하므로, 이렇게 복사하지 않으면 응답이 사라진다.</p>
<h3 id="2-요청-캐싱">2. 요청 캐싱</h3>
<p>본인은 아래와 같이 코드를 구성하여 요청 객체에 대한 캐싱을 진행했다.</p>
<pre><code class="language-java">import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.Getter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

@Getter
public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] requestBody;

    public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // Request Body를 바이트 배열로 저장해 여러 번 읽을 수 있도록 캐싱
        InputStream is = request.getInputStream();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length;
        while ((length = is.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, length);
        }
        this.requestBody = byteArrayOutputStream.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.requestBody);
        return new ServletInputStream() {
            @Override
            public int read() {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }

}</code></pre>
<h4 id="inputstream을-캐싱">InputStream을 캐싱</h4>
<pre><code>InputStream is = request.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
    byteArrayOutputStream.write(buffer, 0, length);
}
this.requestBody = byteArrayOutputStream.toByteArray();</code></pre><p>여기서 요청 바디를 한 번 읽고, 메모리에 저장해둔다. 이로써 InputStream을 다시 만들 수 있게 된다.</p>
<h3 id="3-filterchain-적용">3. FilterChain 적용</h3>
<p>앞서 만든 <code>logFilter</code>가 제대로 동작하기 위해서는 <code>filterChain()</code> 메소드에 로그 필터가 언제 실행될지 명시를 해야한다.</p>
<pre><code class="language-java">@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            // 중략...
            .addFilterBefore(new LogFilter(), UsernamePasswordAuthenticationFilter.class);
    return http.build();
}</code></pre>
<p>본인은 유저 로그인 정보까지 로깅하기 위해 <code>UsernamePasswordAuthenticationFilter</code> 이전에 <code>logFilter</code>가 실행되도록 구현했다. 이 부분은 자신의 환경에 맞게 커스터마이징해도 괜찮을 거 같다.</p>
<h3 id="결론">결론</h3>
<p>AOP 방식에서 몇가지 불편한 점을 느껴 필터 방식으로 로그 관리를 진행했다. 확실히 AOP보다 깔끔하고 좋은 거 같다고 생각한다. 하짐나 아직 로그 관리에 대해 많은 것을 알고 있지 않기 때문에 배워야 할 점이 많다고 느끼며 해당 코드도 개선의 여지가 분명히 존재한다고 생각한다. 실제로 서비스르 배포하면서 개선할 점이 보인다면 더 공부해서 로그 관리 코드를 개선하여 다시 포스팅하도록 하겠다.</p>
<p>모두들 HappyCoding!😁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring boot] AOP를 이용하여 Logging 구현(feat. 문제점)]]></title>
            <link>https://velog.io/@developer-daily/Spring-boot-AOP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-Logging-%EA%B5%AC%ED%98%84feat.-%EB%AC%B8%EC%A0%9C%EC%A0%90</link>
            <guid>https://velog.io/@developer-daily/Spring-boot-AOP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-Logging-%EA%B5%AC%ED%98%84feat.-%EB%AC%B8%EC%A0%9C%EC%A0%90</guid>
            <pubDate>Thu, 27 Mar 2025 04:50:56 GMT</pubDate>
            <description><![CDATA[<p>앞서 로그를 관리하기 위한 여러가지 도구들을 살펴보았고 결과적으로 Logback을 사용하기로 하였다.
<a href="https://velog.io/@developer-daily/Logback%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%9E%90">Logback을 통한 로그 설정</a></p>
<p>이번에는 실제로 클라이언트가 서버로 API를 호출했을 때 어떻게 요청과 응답을 로깅할 수 있는지를 작성해볼려고 한다. 로깅은 보통 AOP, Interceptor, Fiter 단에서 구현되며 <strong>필자는 먼저 AOP를 통해 로깅을 구현해볼려고 한다</strong>. 여기서는 AOP에 대한 개념을 제외하고 로깅을 구현하는 것에 초점을 맞춰 설명한다.</p>
<h3 id="구현-방법">구현 방법</h3>
<h4 id="1-aop-의존성-추가">1. AOP 의존성 추가</h4>
<p>AOP를 사용하기위해 build.gradle 내 의존성을 추가한다.</p>
<pre><code class="language-shell">implementation &#39;org.springframework.boot:spring-boot-starter-aop&#39;</code></pre>
<h4 id="2-enableaspectjautoproxy-추가">2. @EnableAspectJAutoProxy 추가</h4>
<pre><code class="language-java">@SpringBootApplication
@EnableAspectJAutoProxy
public class LoggingTestSpringBootAppApplication {

    public static void main(String[] args) {
        SpringApplication.run(ToDoApplication.class, args);
    }

}</code></pre>
<h4 id="3loggingaspect-작성">3.LoggingAspect 작성</h4>
<pre><code class="language-java">import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Aspect
@Component
public class LoggingAspect {

    private final ObjectMapper objectMapper;
    private final HttpServletRequest request;

    public LoggingAspect(ObjectMapper objectMapper, HttpServletRequest request) {
        this.objectMapper = objectMapper;
        this.request = request;
    }

    @Pointcut(&quot;execution(* org.uni_bag.test_logging_spring_boot_app.controller..*(..))&quot;)
    public void controllerMethods() {
    }

    @Around(&quot;controllerMethods()&quot;)
    public Object logRequestResponse(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        long start = System.currentTimeMillis();

        String httpMethod = request.getMethod();
        Map&lt;String, Object&gt; params = getParameters(method, joinPoint.getArgs());

        log.info(&quot;[REQUEST] {} {} | IP: {} | User-Agent: {} | Method: {} | RequestBody: {}&quot;,
                request.getMethod(),
                request.getRequestURI(),
                request.getRemoteAddr(),
                request.getHeader(&quot;User-Agent&quot;),
                httpMethod,
                objectMapper.writeValueAsString(params)
        );


        Object proceed = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;
        log.info(&quot;[RESPONSE] {} {} | Status: {} | Time Taken: {}ms | ResponseBody: {}&quot;,
                request.getMethod(),
                request.getRequestURI(),
                ((ResponseEntity&lt;?&gt;) proceed).getStatusCode(),
                executionTime,
                objectMapper.writeValueAsString(proceed)
        );

        return proceed;
    }

    private Map&lt;String, Object&gt; getParameters(Method method, Object[] args) {
        Map&lt;String, Object&gt; paramMap = new HashMap&lt;&gt;();
        Annotation[][] annotations = method.getParameterAnnotations();

        for (int i = 0; i &lt; annotations.length; i++) {
            for (Annotation annotation : annotations[i]) {
                if (annotation instanceof RequestParam requestParam) {
                    paramMap.put(requestParam.value(), args[i]);
                } else if (annotation instanceof RequestBody) {
                    paramMap.put(&quot;body&quot;, args[i]);
                }
            }
        }
        return paramMap;
    }
}</code></pre>
<p>코드를 간략하게 설명하면 다음과 같다</p>
<pre><code class="language-java">@Pointcut(&quot;execution(* org.uni_bag.uni_bag_spring_boot_app.controller..*(..))&quot;)
    public void controllerMethods() {
}</code></pre>
<p>해당 AOP는 controller 패키지 내 모든 메소드에 대해 적용된다.
즉, API 요청으로 Controller단의 특정 메소드가 호출되면 해당 AOP가 실행된다.</p>
<pre><code class="language-java">@Around(&quot;controllerMethods()&quot;)
public Object logRequestResponse(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    long start = System.currentTimeMillis();

    String httpMethod = request.getMethod();
    Map&lt;String, Object&gt; params = getParameters(method, joinPoint.getArgs());

    log.info(&quot;[REQUEST] {} {} | IP: {} | User-Agent: {} | Method: {} | RequestBody: {}&quot;,
            request.getMethod(),
            request.getRequestURI(),
            request.getRemoteAddr(),
            request.getHeader(&quot;User-Agent&quot;),
            httpMethod,
            objectMapper.writeValueAsString(params)
    );


    Object proceed = joinPoint.proceed();

    long executionTime = System.currentTimeMillis() - start;
    log.info(&quot;[RESPONSE] {} {} | Status: {} | Time Taken: {}ms | ResponseBody: {}&quot;,
            request.getMethod(),
            request.getRequestURI(),
            ((ResponseEntity&lt;?&gt;) proceed).getStatusCode(),
            executionTime,
            objectMapper.writeValueAsString(proceed)
    );

    return proceed;
}</code></pre>
<p>Controller의 메소드가 호출되면 메소드가 실행되기 전후로 위의 로직이 실행된다.
먼저 Controller의 메소드가 실행되기전 요청에 대한 정보를 로깅한다.
이때 HttpServletRequest 객체를 활용하여 URL, Method, IP, User-Agent, Method, Request Body 정보를 추출하여 로깅에 사용한다.</p>
<p>요청에 대한 로깅이 끝나면 <code>Object proceed = joinPoint.proceed();</code>를 통해  Controller의 메소드를 실행하고 그 결과를 <code>proceed</code>에 저장한다. </p>
<p>이후 <code>proceed</code>를 활용하여 응답에 대한 로깅을 작성한다.</p>
<h3 id="aop-방식의-문제점">AOP 방식의 문제점</h3>
<p>간단하게 서버를 구성하여 AOP를 통해 로깅을 구현하면 상당히 쉽고 간단하게 구현할 수 있다. 하지만 필자가 느끼기에 몇가지 단점이 존재했다.</p>
<h4 id="요청과-응답-객체를-통해-얻을-수-있는-정보가-제한적이다">요청과 응답 객체를 통해 얻을 수 있는 정보가 제한적이다.</h4>
<ul>
<li>정확한 이유는 모르겠지만 AOP에서는 <code>HttpServletRequest</code> 객체의 일부 정보들을 가져올 수 없었다.</li>
<li>또한 <code>joinPoint.proceed()</code>가 반환하는 객체가 <code>ResponseEntity</code>이므로 <code>HttpServletResponse</code> 객체 대비 얻을 수 있는 정보가 제한적이었다.</li>
</ul>
<h4 id="service단에서-예외-발생-시-응답-객체에-대해-알-수-없다">Service단에서 예외 발생 시 응답 객체에 대해 알 수 없다.</h4>
<ul>
<li>위 코드와 같이 <code>@Around</code>를 통해 요청과 응답에 대한 로깅을 진행했다.</li>
<li>Service단에서 예외 발생 시 ControllerAdvice에 의해 예외가 처리된 후 컨트롤러로 돌아가는 것이 아니라 그 처리 결과를 직접 클라이언트에게 전달하게 되기 때문에 응답 로깅이 제대로 되지 않았다.</li>
<li>그래서 <code>@AfterThrowing</code>를 사용해볼려고 했지만 그렇게 될 경우 <code>Exception</code> 객체만 얻을 수 있어 원하는 응답로그를 작성할 수 없었다.</li>
</ul>
<h4 id="controller에-접근하지-않은-유저의-요청을-로깅-할-수-없다">Controller에 접근하지 않은 유저의 요청을 로깅 할 수 없다.</h4>
<ul>
<li>Spring Security를 이용하여 로그인을 하거나 Swagger를 통해 API 문서에 접근하는 등 Controller를 거치지 않은 유저의 요청에 대해 로깅을 할 수 없다.</li>
</ul>
<h3 id="결론">결론</h3>
<p>그래서 필자는 AOP 대신 Fiter를 사용하여 로깅을 구현하기로 결정하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 운영서버에서 Swagger 막는 법]]></title>
            <link>https://velog.io/@developer-daily/Spring-%EC%9A%B4%EC%98%81%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-Swagger-%EB%A7%89%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@developer-daily/Spring-%EC%9A%B4%EC%98%81%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-Swagger-%EB%A7%89%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Sun, 16 Mar 2025 13:33:02 GMT</pubDate>
            <description><![CDATA[<p>조간만 지금까지 만들어왔던 학점가방 서비스를 배포할 예정이다.
배포 준비를 하면서 보안적인 부분을 하니씩 점검하고 있다. 해당 서비스는 Swagger를 사용해서 개발하고 있으며 API문서를 관리하기 위해 Swagger를 사용했다. 
하지만 실제로 배포를 할려고 하니 운영서버에서 우리 팀 개발자뿐만 아니라 서비스를 이용하는 유저도 Swagger 엔드포인트가 노출 될 수도 있겠다는 우려가 생겼다. 그래서 운영서버에서 Swagger 접속을 막기 위한 방법이 필요했다.</p>
<h3 id="프로파일을-통한-swagger-노출-여부-결정">프로파일을 통한 Swagger 노출 여부 결정</h3>
<p>우리 팀은 다양한 방법들 중 프로파일별로 Swagger 노출 여부를 결정하기로 하였다.
우리는 로컬, 개발, 운영 서버마다 각기 다른 <code>application.yaml</code>을 생성하여 사용하고 있다.
<img src="https://velog.velcdn.com/images/developer-daily/post/a5da4b7b-c3a2-4024-9f7e-f10f0a2f0cea/image.png" alt="">
그렇기에 각 프로파일별로 필요한 Swagger 노출 여부를 결정하면 문제가 쉽게 해결될 것이라고 생각했다.
저 중에서 운영서버에 사용되는 <code>application-prod.yaml</code>에 아래 코드를 추가해서 운영서버에서는 Swagger에 접속할 수 없도록 막았다.</p>
<pre><code>springdoc:
  swagger-ui:
    enabled: false</code></pre><p><img src="https://velog.velcdn.com/images/developer-daily/post/e0c47c3e-e244-4a65-9f61-11e0b9ef3f40/image.png" alt="">
이렇게하면 Swagger UI가 비활성화 되면서 사용자가 Swagger에 접속하는 것을 막을 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] FCM 구현 방법]]></title>
            <link>https://velog.io/@developer-daily/Spring-Boot-FCM-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@developer-daily/Spring-Boot-FCM-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 12 Mar 2025 10:36:20 GMT</pubDate>
            <description><![CDATA[<p>Firebase Cloud Messaging(FCM)은 무료로 메시지를 안정적으로 전송할 수 있는 크로스 플랫폼 메시징 솔루션이다. 쉽게 이야기하면 서버에서 클라이언트로 메시지를 전달할 수 있는 서비스라고 생각하면 된다. </p>
<blockquote>
<p>💡 <strong>교차 플랫폼(Cross-Platform)</strong></p>
</blockquote>
<ul>
<li><strong>하나의 코드베이스로 여러 운영체제(OS)에서 실행될 수 있도록 개발하는 방식</strong></li>
<li>즉, <strong>한 번만 개발하면 여러 환경(Android, iOS, Windows, macOS 등)에서 실행 가능</strong>하도록 만드는 기술</li>
</ul>
<h3 id="특징">특징</h3>
<ul>
<li>무료로 메시지를 안정적으로 전송할 수 있다.</li>
<li>사용자에게 표시되는 알림 메시지를 전송한다. 혹은 데이터 메시지를 전송하고 애플리케이션 코드에서 임의로 처리할 수 있다.</li>
<li>단일 기기, 기기 그룹, 주제를 구독한 기기 등 3가지 방식으로 클라이언트 앱에 메시지를 배포할 수 있다.</li>
</ul>
<h3 id="구성요소">구성요소</h3>
<p>FCM 구현에는 송수신을 위한 몇가지 구성요소가 있다.</p>
<ol>
<li>메시지를 작성하고 이를 FCM 서버로 전송하기 위한 도구 혹은 환경<ul>
<li>Cloud Functions for Firebase</li>
<li><strong>Firebase Admin SDK 또는 FCM 서버 프로토콜을 지원하는 신뢰되는</strong> 앱 서버(Spring boot, Node.js)</li>
</ul>
</li>
<li>FCM Backend<ul>
<li>메시지 요청을 수락하는 등 여러 기능을 수행한다.</li>
<li>메시지 ID와 같은 메시지 메타데이터를 생성한다.</li>
</ul>
</li>
<li>알림을 수신 받을 클라이언트 앱<ul>
<li>Apple, Android 또는 웹(자바스크립트)</li>
</ul>
</li>
</ol>
<h3 id="동작-과정">동작 과정</h3>
<p>FCM의 동작 과정는 다음과 같다.</p>
<ul>
<li><p><strong>FCM에서 메시지를 수신하도록 기기를 등록</strong></p>
<ul>
<li>FCM을 사용하는 이유는 특정 클라이언트에게 메시지를 전송하기 위해서다. 이를 위해서는 특정 클라이언트를 식별할 수 있는 식별자가 필요하다.</li>
<li>클라이언트 앱이 메시지를 수신하도록 등록하여 앱을 고유하게 식별하는 등록 토큰(식별자)을 받는다.</li>
<li>앱 서버를 이용할 경우 해당 토큰을 앱 서버가 보관하고 있다가 앱서버가 메시지 알림을 전송할 때 해당 토큰을 꺼내 사용할 수 있다.</li>
</ul>
</li>
<li><p><strong>클라이언트 앱에 메시지 전송</strong>
  <img src="https://velog.velcdn.com/images/developer-daily/post/325780d5-2e56-4adb-a596-785d100bba94/image.png" alt=""></p>
<ol>
<li>메시지를 작성할 수 있는 도구 또는 환경(앱 서버)에서 메시지가 작성되며 메시지 요청이 FCM 백엔드로 전송된다.</li>
<li>FCM 백엔드는 메시지 요청을 수신하고 메시지 ID와 기타 메타데이터를 생성하여 플랫폼별 전송 레이어로 보낸다.</li>
<li>기기가 온라인 상태이면 메시지가 플랫폼별 전송 레이어를 통해 기기로 전송된다.</li>
<li>기기에서의 클라이언트 앱이 메시지 또는 알림을 수신한다.</li>
</ol>
</li>
</ul>
<h3 id="구현">구현</h3>
<p>Spring boot로 FCM을 통해 푸시 알림을 전달하는 방법은 다음과 같다.</p>
<h4 id="1-firebase-project-생성">1. Firebase Project 생성</h4>
<p>먼저 파이어베이스에 로그인하고 <a href="https://console.firebase.google.com/">콘솔</a>로 이동하여 프로젝트를 생성한다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/8ebd2b77-97cd-420c-9491-d00c1af57ecf/image.png" alt=""></p>
<h4 id="2-비공개-키-다운로드">2. 비공개 키 다운로드</h4>
<ol>
<li>왼쪽 상단의 톱니바퀴를 클릭하여 프로젝트 설정 페이지로 이동한다. </li>
<li>그리고 서비스 계정 탭을 클릭하고 아래 새 비공개 키 생성을 클릭하여 비공개 키인 <code>json</code> 파일을 다운받는다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/35a6778b-4e48-472f-b44b-962b9d896392/image.png" alt=""></p>
<h4 id="3-스프링-부트-프로젝트-내-비공개-키-저장">3. 스프링 부트 프로젝트 내 비공개 키 저장</h4>
<p>다운 받은 <code>json</code> 파일을 <code>resources/firebase</code> 디렉토리 아래에 <code>service-account.json</code> 이라는 이름으로 저장한다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/a1174ba3-0c47-42e5-a1d3-e45301e7e727/image.png" alt=""></p>
<h4 id="4-파이어베이스-의존성-추가">4. 파이어베이스 의존성 추가</h4>
<p><code>build.gradle</code> 파일에 firebase 의존성을 추가해준다.</p>
<pre><code class="language-java">dependencies {
    implementation &#39;com.google.firebase:firebase-admin:9.2.0&#39;
}</code></pre>
<h4 id="5-firebase-환경-설정-파일-추가">5. Firebase 환경 설정 파일 추가</h4>
<p>Spring Boot에서 firebase를 사용하기 위해서는 환경 설정이 필요하다.</p>
<p>먼저 나는 application.yaml에 비공개 키의 경로를 속성으로 추가하여 사용하였다.</p>
<pre><code class="language-yaml">firebase:
  service-account:
    path: &quot;/firebase/service-account.json&quot;</code></pre>
<p>이후 <code>FirebaseConfig</code>라는 환경 설정 파일을 만들고 위 경로를 통해 비공개 키를 불러와 Firebase를 초기화하는 코드를 작성하였다.</p>
<pre><code class="language-java">// FirebaseConfig.java

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;

@Slf4j
@Configuration
public class FirebaseConfig {
    @Value(&quot;${firebase.service-account.path}&quot;)
    private String SERVICE_ACCOUNT_PATH;

    @Bean
    public FirebaseApp firebaseApp() {
        try {
            FirebaseOptions options = FirebaseOptions.builder()
                    .setCredentials(
                            GoogleCredentials.fromStream(new ClassPathResource(SERVICE_ACCOUNT_PATH).getInputStream())
                    )
                    .build();

            log.info(&quot;Successfully initialized firebase app&quot;);
            return FirebaseApp.initializeApp(options);

        } catch (IOException exception) {
            log.error(&quot;Fail to initialize firebase app{}&quot;, exception.getMessage());
            return null;
        }
    }

    @Bean
    public FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) {
        return FirebaseMessaging.getInstance(firebaseApp);
    }
}</code></pre>
<h4 id="6-push-알림-전송">6. Push 알림 전송</h4>
<p><code>FcmService</code>라는 서비스 클래스를 만들고 <code>sendNotification()</code> 메서드로 특정 FCM토큰을 가진 유저에게 지정된 <code>title</code>과 <code>body</code>를 포함하여 푸시 알림을 전송하도록 구현하였다.</p>
<pre><code class="language-java">// FcmService.java

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class FcmService {
    private final FirebaseMessaging firebaseMessaging;

    public void sendNotification(String title, String body, String fcmToken) {
        log.info(&quot;Attempting to send Notification (title: {}, body: {}, fcmToken: {})&quot;, title, body, fcmToken);
        send(createMessage(title, body, fcmToken));
    }

    private void send(Message message) {
        try {
            String response = firebaseMessaging.send(message);
            log.info(&quot;Successfully send Notification: {}&quot;, response);
        } catch (FirebaseMessagingException e) {
            log.error(&quot;Fail to send Notification : {}&quot;, e.getMessage());
        }
    }

    private Message createMessage(String title, String body, String fcmToken) {
        return Message.builder()
                .putData(&quot;title&quot;, title)
                .putData(&quot;body&quot;, body)
                .setToken(fcmToken)
                .build();
    }
}</code></pre>
<h4 id="7-실행-결과">7. 실행 결과</h4>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/f5c295a0-f775-474a-864b-a5c8f4922c6b/image.png" alt=""></p>
<h3 id="추가적인-활용">추가적인 활용</h3>
<p>지금까지 간단하게 Spring boot에서 Firebase를 설정하고 간단하게 알림을 보내는 코드를 작성해보았다. </p>
<p>여기서 더 나아가 회원가입 시 유저의 FCM 토큰을 등록하고 특정 이벤트 발생하면 특정 유저의 FCM 토큰을 조회하여 알림을 보내는 로직을 구상하는 듯 해당 코드를 무궁무진하게 활용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Server] 어떻게 푸시 알림을 구현할까?]]></title>
            <link>https://velog.io/@developer-daily/%ED%91%B8%EC%8B%9C-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-FCM-SSE-WebSocket</link>
            <guid>https://velog.io/@developer-daily/%ED%91%B8%EC%8B%9C-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-FCM-SSE-WebSocket</guid>
            <pubDate>Tue, 11 Mar 2025 10:34:58 GMT</pubDate>
            <description><![CDATA[<p>요즘 학점가방 서비스를 제작하면서 바쁜 나날을 보내고 있다.
해당 서비스에는 학교 과제를 관리하는 기능이 있으며 팀장으로부터 과제 마감 전 푸시 알림을 전송하는 기능을 구현해달라는 부탁을 받았다.
처음에는 단순히 FCM으로 구현해야지 생각했다가 다른 방식도 있지 않을까라는 생각에 여러 기술들을 찾아보고 정리해보았다.</p>
<h3 id="fcmfirebase-cloud-message">FCM(Firebase Cloud Message)</h3>
<p>Firebase Cloud Messaging(FCM)은 무료로 메시지를 안정적으로 전송할 수 있는 크로스 플랫폼 메시징 솔루션이다. 쉽게 이야기하면 서버에서 클라이언트로 메시지를 전달할 수 있는 서비스라고 생각하면 된다. </p>
<h4 id="동작방식">동작방식</h4>
<p>FCM의 동작 방식을 간단하게 설명하면 다음과 같다.</p>
<ol>
<li>FCM에서 클라이언트 기기에 대한 토큰을 발급 받는다.</li>
<li>해당 토큰을 앱 서버(예: Spring Boot, Node.js 등)로 전송한다. </li>
<li>앱 서버에서 전달할 메시지와 토큰을 FCM 서버로 전송한다.</li>
<li>FCM 서버에서 해당 메시지를 특정 클라이언트의 기기로 전송한다.</li>
</ol>
<h4 id="장점">장점</h4>
<ul>
<li>FCM은 교차 플랫폼이기 때문에 , 특정 플랫폼에 종속되지 않고 메시지를 전송할 수 있다.</li>
<li>클라우드 메시징 서버를 중간에 둠으로써, 클라이언트 어플리케이션은 <strong>적은 배터리와 네트워크</strong>만을 사용하며 메세지를 실시간으로 수신할 수 있다.</li>
<li>Firebase API를 이용하기 때문에 구현이 쉽고 이로 인해 기능 구축에 대한 오버헤드를 줄일 수 있다.</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>Real-time 서비스이긴 하지만 장치 연결 상태, 메시지의 크기와 포맷, 그리고 네트워크 상태 등 전송 시간이 지연될 수 있는 요소들이 존재한다.</li>
<li>Firebase API에 의존하기 때문에 커스텀마이징이 불가능하다.</li>
</ul>
<h3 id="sseserver-send-event">SSE(Server Send Event)</h3>
<p>SSE(Server Send Event)는 서버의 데이터를 실시간으로, 지속적으로 Streaming 하는 기술이다.</p>
<p>SSE는 <strong>단방향 통신</strong>이며 클라이언트의 별도 추가요청 없이 서버에서 업데이트를 스트리밍할 수 있다는 특징을 가진다.</p>
<h4 id="장점-1"><strong>장점</strong></h4>
<ul>
<li>HTTP를 통해 통신하므로 다른 프로토콜은 필요가 없다.</li>
<li>HTML과 JavaScript만으로 쉽게 구현할 수 있다.</li>
<li>네트워크 연결이 끊겼을 때 자동으로 재연결을 시도한다.</li>
<li>실시간으로 서버에서 클라이언트로 데이터를 전송할 수 있다.</li>
<li>전송하고자 하는 데이터를 자유롭게 커스텀마이징이 가능하다.</li>
</ul>
<h4 id="단점-1"><strong>단점</strong></h4>
<ul>
<li>구현에 대한 오버헤드가 존재한다.</li>
<li>GET 메소드만 지원하고, 파라미터를 보내는데 한계가 있다.</li>
<li>WebSocket과 달리 단방향 통신이다.</li>
<li>클라이언트가 페이지를 닫아도 서버에서 감지하기가 어렵다</li>
<li>SSE는 지속적인 연결을 유지해야 하므로, 많은 클라이언트가 동시에 연결을 유지할 경우 서버 부담이 커질 수 있다.</li>
<li>지원되지 않는 브라우저를 사용하는 사용자의 경우에는 중요한 알림을 아예 받지 못할 가능성이 존재한다.</li>
</ul>
<h3 id="websocket">WebSocket</h3>
<p>WebSocket은 클라이언트(웹 브라우저)와 서버 간에 양방향(full-duplex) 통신을 할 수 있도록 해주는 프로토콜이다.</p>
<p>서버는 응답을 보낸 뒤 연결을 종료하는 &#39;비연결성&#39;을 갖는 것이 HTTP의 특징인데, WebSocket은 HTTP의 비연결성으로 데이터 전송 시 발생하는 오버헤드를 극복하기 위해 고안된 기술이다.</p>
<h4 id="특징">특징</h4>
<ul>
<li>웹소켓은 HTTP 통신과 달리 TCP/IP 응용 계층에서 동작한다.</li>
<li>HTTP는 클라이언트가 요청해야 서버가 응답하지만, 웹소켓은 요청과 응답 없이 서버와 클라이언트가 자유롭게 데이터를 주고받을 수 있다.<ul>
<li>그렇기 때문에 클라이언트 끼리도 자유롭게 데이터를 전달하고 전송 받을 수 있다.</li>
</ul>
</li>
<li>서버와 클라이언트가 처음 연결 될 때 HTTP를 통해 연결되며 연결이 완료되면 Websoket 프로토콜을 사용한다.</li>
<li>HTTP는 요청이 끝나면 연결을 끊지만, 웹소켓은 한 번 연결되면 계속 유지된다.<ul>
<li>즉, 새로운 연결을 생성할 필요가 없다.</li>
</ul>
</li>
</ul>
<p>이러한 특징들 덕분에 웹 소켓은 채팅, 게임, 실시간 알림, 주식 데이터 등에 사용된다.</p>
<h4 id="동작-방식">동작 방식</h4>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/ecb30324-fd3e-424b-bfad-d86a2ad7ad13/image.png" alt=""></p>
<ol>
<li><strong>웹 소켓 핸드쉐이크</strong><ul>
<li>클라이언트가 웹소켓 연결을 요청하면, HTTP 요청을 이용해 서버와 초기 연결을 설정한다.</li>
<li>HTTP 요청 헤더에 <strong><code>Upgrade: websocket</code></strong> 이라는 값을 포함하여 웹소켓을 사용하겠다고 선언한다.</li>
<li>이 과정을 <strong>웹소켓 핸드셰이크(WebSocket Handshake)</strong> 라고 한다.</li>
<li>즉, 웹소켓 연결도 HTTP를 사용해 시작하지만, 연결이 성공하면 <strong>HTTP에서 웹소켓 프로토콜로 변경된다.</strong></li>
</ul>
</li>
<li><strong>서버가 웹소켓 연결 수락</strong><ul>
<li>서버는 클라이언트의 요청을 받고 웹소켓 통신을 허용할지 결정한다.</li>
<li>만약 웹소켓을 지원하고 연결을 허용한다면, <strong>HTTP 101 Switching Protocols 응답</strong>을 보낸다.<ul>
<li>여기서 101 응답 코드는 <strong>&quot;프로토콜을 HTTP에서 웹소켓으로 변경한다&quot;</strong> 는 의미한다.</li>
</ul>
</li>
<li>이제부터 클라이언트와 서버는 <strong>웹소켓 프로토콜을 사용하여 실시간으로 데이터를 주고받을 수 있다.</strong></li>
</ul>
</li>
<li><strong>데이터 송수신</strong><ul>
<li>연결이 설정된 후에는 클라이언트와 서버가 자유롭게 데이터를 주고받을 수 있다.</li>
<li>HTTP와 다르게, 새로운 요청을 하지 않아도 서버에서 클라이언트로 직접 메시지를 보낼 수 있다.<ul>
<li>이를 <strong>서버 푸시(Server Push)</strong> 라고 한다.</li>
</ul>
</li>
<li>즉, 클라이언트가 서버로 데이터를 보낼 수도 있고, 서버가 클라이언트에게 데이터를 보낼 수 있는 양방향(Full-Duplex) 통신이 가능해진다.</li>
</ul>
</li>
<li><strong>웹소켓 연결 종료</strong><ul>
<li>클라이언트 또는 서버 중 한쪽에서 연결을 종료할 수 있다.</li>
<li>연결을 종료하면, 웹소켓 프로토콜이 &quot;CLOSE 프레임&quot; 을 전송하며, 서버가 연결을 닫았는지, 클라이언트가 닫았는지를 서로 확인한다.</li>
<li>정상적으로 종료되면, 더 이상 데이터를 주고받을 수 없다.</li>
</ul>
</li>
</ol>
<h4 id="장점-2">장점</h4>
<ul>
<li>양방향 통신이 가능하다.</li>
<li>대부분 브라우저에서 지원한다.</li>
<li>전송하고자 하는 데이터를 자유롭게 커스텀마이징이 가능하다.</li>
</ul>
<h4 id="단점-2">단점</h4>
<ul>
<li>구현에 대한 오버헤드가 존재한다.</li>
<li>배터리 소모량이 클 수도 있다.</li>
<li>FCM과 달리 서버에서 모든 부하를 감당 해야한다.</li>
</ul>
<h3 id="결론">결론</h3>
<p>지금까지 푸시 알림 구현이 가능한 3가지 방식에 대해 알아보았다. 지금 현재 중요한 것은 시간과 비용이다. SSE와 Websocket을 구현하기 위해서는 프론트와 벡엔드 모두 구현을 위한 오버헤드가 존재한다. 또한 우리는 간단한 푸시알림을 각 유저에게 전달하면 된다. 이러한 점들을 따져보았을 때 현재 가장 적합한 방식은 FCM이라고 생각한다. 그래서 우리 팀은 FCM을 통해 푸시 알림을 구현하기로 결정하였다.</p>
<h3 id="참고-자료">참고 자료</h3>
<ul>
<li><a href="https://velog.io/@bjo6300/SSE-vs-FCM-vs-WebSocket#fcm%EC%9D%98-%EC%9E%A5%EC%A0%90">https://velog.io/@bjo6300/SSE-vs-FCM-vs-WebSocket#fcm의-장점</a></li>
<li><a href="https://doozi0316.tistory.com/entry/WebSocket%EC%9D%B4%EB%9E%80-%EA%B0%9C%EB%85%90%EA%B3%BC-%EB%8F%99%EC%9E%91-%EA%B3%BC%EC%A0%95-socketio-Polling-Streaming">https://doozi0316.tistory.com/entry/WebSocket이란-개념과-동작-과정-socketio-Polling-Streaming</a></li>
<li><a href="https://velog.io/@yeonjoo7/Web-Socket">https://velog.io/@yeonjoo7/Web-Socket</a></li>
<li><a href="https://velog.io/@black_han26/SSE-Server-Sent-Events">https://velog.io/@black_han26/SSE-Server-Sent-Events</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[부하테스트는 무엇이며 왜 하는걸까?]]></title>
            <link>https://velog.io/@developer-daily/%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EB%A9%B0-%EC%99%9C-%ED%95%98%EB%8A%94%EA%B1%B8%EA%B9%8C</link>
            <guid>https://velog.io/@developer-daily/%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EB%A9%B0-%EC%99%9C-%ED%95%98%EB%8A%94%EA%B1%B8%EA%B9%8C</guid>
            <pubDate>Mon, 24 Feb 2025 06:47:31 GMT</pubDate>
            <description><![CDATA[<p>팀원들과 함께 개발하고 있는 학점가방 서비스를 조만간 배포할 예정이다.
그리고 배포 전에 문득 운영서버가 얼마나 많은 트래픽을 감당할 수 있을지 의문이 들었다.
이러한 의문을 해결하기 위해 <a href="https://inf.run/jBYVR">인프런의 대규모 트래픽 처리를 위한 부하테스트 입문/실전</a> 강의를 듣고 공부한 내용을 기록으로 남기고자 한다.</p>
<h3 id="부하테스트란">부하테스트란?</h3>
<p>부하테스트는 시스템이 <strong>어느 정도의 부하(= 트래픽, 요청)를 견딜 수 있는 지 테스트</strong>하는 것을 의미한다. 보통 백엔드 시스템을 구성하면 웹서버, WAS, DB 등으로 구성된다. 이러한 시스템에 수많은 클라이언트가 요청을 보낼 경우 어느 정도까지 요청을 수용하며 서버가 견딜 수 있는지 해당 테스트를 통해 알 수 있다. </p>
<h3 id="부하테스트는-왜-하는걸까">부하테스트는 왜 하는걸까?</h3>
<p>서비스를 배포하기 전 문득 이런 생각이 들 수 있다. </p>
<blockquote>
<p>&quot;우리 서버는 얼마나 많은 사용자를 감당할 수 있을까?&quot;
&quot;얼마 만큼의 서버 사양이 준비되어야 할까?&quot;</p>
</blockquote>
<p>이런 생각이 드는 이유는 우리의 서버 자원이 무한정하지 않기 때문이다. 만약 컴퓨터 성능 너무 넉넉할 경우 자원을 모두 사용하지 못한채 불필요하게 많을 지출을 만들어 낼 것이다. 하지만 반대로 성능이 낮을 경우 많은 서비스 사용자가 몰리게 되면 서버가 죽어버리고 말 것이다. 서비스를 운영하는 사람이라면 모두 최소한의 비용으로 최대한의 효율을 끌어내고 싶어 할 것이다. 그러기 위해서는 부하테스트를 통해 딱 필요한 만큼의 서버 사양을 선택하는 것이 중요하다.</p>
<h3 id="부하테스트를-위한-툴">부하테스트를 위한 툴</h3>
<p>부하테스트를 위해서는 ngrinder, jmeter, ab, locust, k6등과 같은 유명한 툴이 있다. 나는 그 중 메모리를 적게 사용하면서 비교적 많은 요청 수를 보낼 수 있는 부하 테스트 툴인 k6를 사용하고자 한다. 사용 방법도 간단해서 쉽게 테스트 해볼 수 있다.</p>
<h3 id="다음-시리즈">다음 시리즈</h3>
<p>해당 포스트는 시리즈 형태로 계속 이야기를 이어나가고자 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Logback을 통한 Spring boot 로그 관리]]></title>
            <link>https://velog.io/@developer-daily/Logback%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@developer-daily/Logback%EC%9C%BC%EB%A1%9C-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC%EB%A5%BC-%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 20 Feb 2025 06:06:03 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-인식">문제 인식</h2>
<p>현재 학점가방이라는 앱 서비스를 제작 중이며 나는 스프링 부트로 백엔드 개발을 맡고 있다. 지금까지 프론트엔드 개발자들이 API 연결과정에서 로그를 확인해야 할 때 직접 서버에 접속하여 <code>docker logs</code> 명령을 통해 콘솔로만 확인해왔다. 하지만 이 방식은 다음과 같은 문제점이 있었다. </p>
<ul>
<li>콘솔을 통해서만 확인이 가능하다.</li>
<li>이전의 로그를 파악하기 힘들다.</li>
<li>어플리케이션이 재시작되면 기존의 로그들이 삭제된다.</li>
<li>프론트엔드 팀원이 api 연결하는 과정에서 발생하는 에러를 파악하기 힘들었다. </li>
</ul>
<p>이러한 이유들로 우리 팀은 좀 더 효율적인 방식으로 로그를 관리하기로 결정하였다.</p>
<h2 id="로그">로그?</h2>
<p>로그(log)란 프로그램 또는 시스템에서 발생하는 이벤트, 정보, 상태, 오류 등을 기록한 것을 말하고, 이러한 로그를 생성하고 저장하는 것을 로깅(logging)이라 한다.</p>
<p>로그는 프로그램의 동작을 추적해 문제를 해결하거나 성능을 분석하기 위한 목적으로 활용된다. 이 때문에 로그 관리는 프로그램 개발과 운영에서 아주 중요한 역할을 한다.</p>
<h2 id="사용할-프레임워크">사용할 프레임워크</h2>
<p>로그를 관리하기 위한 여러 도구들을 찾아보았고 <code>Logback</code>, <code>java.tuil.logging</code>, <code>Log4j2</code>와 같은 같은 선택지가 있었다. 그리고 챗지피티를 활용하여 이들의 장단점을 비교해보았다.
<img src="https://velog.velcdn.com/images/developer-daily/post/1181fb43-2272-43df-a073-aeed6b174cfd/image.png" alt="">
나는 이 중에서는 스프링 부트에서 기본적으로 제공하고 있으며 권장하고 있는 Logback을 사용하기로 결정하였다.</p>
<h2 id="logback">Logback?</h2>
<p><strong>Logback</strong>은 Java에서 가장 널리 사용되는 로깅 프레임워크 중 하나로, <strong>Log4j의 창시자인 Ceki Gülcü</strong>가 개발했다. 또한 오랫동안 검증된 <strong>Log4j</strong>의 아키텍쳐 기반으로 작성되었다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>Spring Boot에서 기본 로깅 프레임워크로 사용된다.</li>
<li><code>SLF4J</code>와 함께 동작한다.</li>
<li><strong>로깅을 비동기적으로 처리</strong>할 수 있다.</li>
<li>다양한 출력 대상(Appender) 지원한다.<ul>
<li>콘솔(Console)</li>
<li>파일(File)</li>
<li>데이터베이스(DB)</li>
<li>SMTP(이메일)</li>
<li>Syslog(시스템 로그)</li>
</ul>
</li>
</ul>
<h2 id="로그-레벨">로그 레벨</h2>
<p>로그라고 해서 모두 다 같은 로그는 아니다. API 요청을 했다는 정보성 로그가 있고, 예상치 못한 오류로 인해 발생한 로그도 있을 것이다. Logback에서는 이러한 로그들을 5단계로 분류하여 출력한다.</p>
<h3 id="분류">분류</h3>
<p>Logback에서는 다음과 같은 방식으로 로그 레벨을 분류한다.</p>
<p><code>Error</code> : 애플리케이션 오류(예외 발생, 장애 감지)</p>
<p><code>Warn</code> : 예외는 아니지만 문제 가능성이 있는 경우(오래 걸리는 요청, 메모리 부족 경고)</p>
<p><code>Info</code> : 정상적인 시스템 동작 로그(API 요청, 중요한 비즈니스 이벤트)</p>
<p><code>Debug</code> : 개발용 상세 로그(서비스 내부 로직)</p>
<p><code>Trace</code> : 상세한 디버깅용(DB 쿼리 상세 로그)</p>
<h3 id="심각도">심각도</h3>
<p>심각도는 오른쪽으로 갈수록 높아진다.</p>
<p>** Trace &gt; Debug &gt; Info &gt; Warn &gt; Error **</p>
<h3 id="출력">출력</h3>
<p>모든 로그를 출력하면 로그 파일의 용량이 빠르게 증가하고 서버의 저장공간 부족문제가 발생할 수도 있다. 그래서 일반적으로 특정 레벨 이상의 로그만 출력하도록 설정한다.
예를 들어 로그 레벨을 Info로 설정 시, Info 레벨 이상의 로그레벨(Info, Warn, Error)이 출력된다.</p>
<h2 id="로그-설정-관리">로그 설정 관리</h2>
<p>스프링 부트에서는 <code>logback-spring.xml</code> 혹은 <code>application.properties</code> 을 사용하여 로그를 관리할 수 있다. 나는 이 중에서 profile에 따라 다양한 출력 방식을 지정할 수 있는 <code>logback-spring.xml</code> 방식을 사용하기로 결정했다.</p>
<p>먼저 resoures폴더 아래에 <code>logback-spring.xml</code>을 만들어준다.</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;configuration&gt;
    &lt;!-- base.xml default.xml 에 존재하는 Log 메시지의 Color 설정 --&gt;
    &lt;conversionRule conversionWord=&quot;clr&quot; converterClass=&quot;org.springframework.boot.logging.logback.ColorConverter&quot;/&gt;

    &lt;!-- 콘솔 로그 패턴 --&gt;
    &lt;property name=&quot;CONSOLE_LOG_PATTERN&quot;
              value=&quot;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %clr(%5level) %cyan(%logger) - %msg%n&quot;/&gt;
    &lt;!-- 파일 로그 패턴 --&gt;
    &lt;property name=&quot;FILE_LOG_PATTERN&quot; value=&quot;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %5level %logger - %msg%n&quot;/&gt;

    &lt;!-- 콘솔 로그 Appender --&gt;
    &lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
        &lt;encoder&gt;
            &lt;pattern&gt;${CONSOLE_LOG_PATTERN}&lt;/pattern&gt;
        &lt;/encoder&gt;
    &lt;/appender&gt;

    &lt;!-- 파일 로그 Appender --&gt;
    &lt;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;
        &lt;encoder&gt;
            &lt;pattern&gt;${FILE_LOG_PATTERN}&lt;/pattern&gt;
        &lt;/encoder&gt;
        &lt;!-- RollingPocliy: 로그가 길어지면 가독성이 떨어지므로 로그를 나눠서 기록하기위한 규칙 --&gt;
        &lt;!-- 로그파일을 크기, 시간 기반으로 관리하기 위한 SizeAndTimeBasedRollingPolicy --&gt;
        &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&gt;
            &lt;!-- 로그파일명 패턴 --&gt;
            &lt;!-- 날짜별로 기록되며 maxFileSize를 넘기면 인덱스(i)를 증가시켜 새로운 이름의 로그파일에 기록을 이어간다 --&gt;
            &lt;fileNamePattern&gt;./log/%d{yyyy-MM-dd}.%i.log&lt;/fileNamePattern&gt;
            &lt;!-- 로그파일 최대사이즈 --&gt;
            &lt;maxFileSize&gt;100MB&lt;/maxFileSize&gt;
            &lt;!-- 생성한 로그파일 관리 일수 --&gt;
            &lt;maxHistory&gt;30&lt;/maxHistory&gt;
        &lt;/rollingPolicy&gt;
    &lt;/appender&gt;

    &lt;!-- local Profile에서의 로그 설정 --&gt;
    &lt;springProfile name=&quot;local&quot;&gt;
        &lt;!-- 전체적인 로그는 INFO 레벨 부터 출력 --&gt;
        &lt;root level=&quot;INFO&quot;&gt;
            &lt;!-- CONSOLE 로그 Appender를 로그 Appender로 등록 --&gt;
            &lt;appender-ref ref=&quot;CONSOLE&quot; /&gt;
            &lt;appender-ref ref=&quot;FILE&quot; /&gt;
        &lt;/root&gt;
    &lt;/springProfile&gt;
    &lt;!-- dev Profile에서의 로그 설정 --&gt;
    &lt;springProfile name=&quot;dev&quot;&gt;
        &lt;root level=&quot;INFO&quot;&gt;
            &lt;appender-ref ref=&quot;CONSOLE&quot; /&gt;
            &lt;appender-ref ref=&quot;FILE&quot; /&gt;
        &lt;/root&gt;
    &lt;/springProfile&gt;
    &lt;!-- prod Profile에서의 로그 설정 --&gt;
    &lt;springProfile name=&quot;prod&quot;&gt;
        &lt;root level=&quot;INFO&quot;&gt;
            &lt;appender-ref ref=&quot;CONSOLE&quot; /&gt;
            &lt;appender-ref ref=&quot;FILE&quot; /&gt;
        &lt;/root&gt;
    &lt;/springProfile&gt;

&lt;/configuration&gt;</code></pre><h3 id="xml-파일-설명">xml 파일 설명</h3>
<h4 id="color-설정">Color 설정</h4>
<pre><code>&lt;conversionRule conversionWord=&quot;clr&quot; converterClass=&quot;org.springframework.boot.logging.logback.ColorConverter&quot;/&gt;</code></pre><ul>
<li>로그 메시지에서 특정 부분을 색상으로 강조하기 위한 설정이다.</li>
<li><code>%clr(...)</code> 패턴을 사용할 때 색상을 지정할 수 있도록 해준다.</li>
<li>콘솔 로그에서 색상을 지원하는 터미널에서만 동작한다.</li>
</ul>
<h4 id="로그-패턴">로그 패턴</h4>
<pre><code>&lt;property name=&quot;CONSOLE_LOG_PATTERN&quot;
          value=&quot;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %clr(%5level) %cyan(%logger) - %msg%n&quot;/&gt;
&lt;property name=&quot;FILE_LOG_PATTERN&quot; 
          value=&quot;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %5level %logger - %msg%n&quot;/&gt;</code></pre><p><code>${CONSOLE_LOG_PATTERN}</code></p>
<ul>
<li>콘솔에 출력될 로그 형식을 지정한다.</li>
<li><code>%d{yyyy-MM-dd HH:mm:ss.SSS}</code> → 로그의 날짜 및 시간 (밀리초 단위 포함)</li>
<li><code>[%thread]</code> → 해당 로그를 출력한 스레드명</li>
<li><code>%clr(%5level)</code> → 로그 레벨 (INFO, DEBUG, ERROR 등), 색상을 적용</li>
<li><code>%cyan(%logger)</code> → 로그를 출력한 클래스명을 파란색으로 표시</li>
<li><code>%msg%n</code> → 로그 메시지를 출력하고 개행</li>
</ul>
<p><code>${FILE_LOG_PATTERN}</code></p>
<ul>
<li>로그 파일에 기록될 형식을 지정한다.</li>
<li>콘솔과 동일하지만 색상 적용이 없다.</li>
</ul>
<h4 id="콘솔-로그-appender">콘솔 로그 Appender</h4>
<pre><code>&lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
    &lt;encoder&gt;
        &lt;pattern&gt;${CONSOLE_LOG_PATTERN}&lt;/pattern&gt;
    &lt;/encoder&gt;
&lt;/appender&gt;</code></pre><ul>
<li>콘솔(<code>stdout</code>)에 로그를 출력하는 <code>ConsoleAppender</code>를 설정한다.</li>
<li>로그 패턴으로 <code>${CONSOLE_LOG_PATTERN}</code>을 사용한다.</li>
</ul>
<h4 id="파일-로그-appender">파일 로그 Appender</h4>
<pre><code>&lt;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;
    &lt;encoder&gt;
        &lt;pattern&gt;${FILE_LOG_PATTERN}&lt;/pattern&gt;
    &lt;/encoder&gt;
    &lt;!-- RollingPocliy: 로그가 길어지면 가독성이 떨어지므로 로그를 나눠서 기록하기위한 규칙 --&gt;
    &lt;!-- 로그파일을 크기, 시간 기반으로 관리하기 위한 SizeAndTimeBasedRollingPolicy --&gt;
    &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&gt;
        &lt;!-- 로그파일명 패턴 --&gt;
        &lt;!-- 날짜별로 기록되며 maxFileSize를 넘기면 인덱스(i)를 증가시켜 새로운 이름의 로그파일에 기록을 이어간다 --&gt;
        &lt;fileNamePattern&gt;./log/%d{yyyy-MM-dd}.%i.log&lt;/fileNamePattern&gt;
        &lt;!-- 로그파일 최대사이즈 --&gt;
        &lt;maxFileSize&gt;100MB&lt;/maxFileSize&gt;
        &lt;!-- 생성한 로그파일 관리 일수 --&gt;
        &lt;maxHistory&gt;30&lt;/maxHistory&gt;
    &lt;/rollingPolicy&gt;
&lt;/appender&gt;
</code></pre><ul>
<li>로그를 파일로 저장하는 RollingFileAppender를 설정한다.</li>
<li>로그 패턴으로 <code>${FILE_LOG_PATTERN}</code>을 사용한다.</li>
<li>로그 파일을 자동으로 관리하는 <strong>Rolling Policy</strong>를 설정한다.</li>
<li><strong><code>fileNamePattern</code></strong><ul>
<li><code>./log/%d{yyyy-MM-dd}.%i.log</code></li>
<li>날짜별로 로그 파일을 생성하고, 파일 크기가 <code>maxFileSize</code>를 초과하면 인덱스(<code>%i</code>)를 증가시켜 새 파일을 만든다.</li>
</ul>
</li>
<li><strong><code>maxFileSize</code></strong><ul>
<li>하나의 로그 파일 크기가 <code>100MB</code>를 초과하면 새 파일을 생성한다.</li>
</ul>
</li>
<li><strong><code>maxHistory</code></strong><ul>
<li>로그 파일을 <strong>최대 30일간 유지</strong>한다.</li>
</ul>
</li>
</ul>
<h4 id="환경별-로그-관리">환경별 로그 관리</h4>
<pre><code>&lt;springProfile name=&quot;dev&quot;&gt;
    &lt;root level=&quot;INFO&quot;&gt;
        &lt;appender-ref ref=&quot;CONSOLE&quot; /&gt;
        &lt;appender-ref ref=&quot;FILE&quot; /&gt;
    &lt;/root&gt;
&lt;/springProfile&gt;</code></pre><ul>
<li>dev 프로파일을 사용할 때 로그 설정이다.</li>
<li><code>INFO</code> 이상의 레벨에 대한 로그를 출력한다.</li>
<li>콘솔과 파일에 로그를 출력한다.</li>
</ul>
<h2 id="실제-사용">실제 사용</h2>
<p>스프링부트 앱 메인함수를 다음과 같이 작성하고 실행해보았다.</p>
<pre><code class="language-Java">@SpringBootApplication
public class UniBagSpringBootAppApplication {
    private static final Logger logger = LoggerFactory.getLogger(UniBagSpringBootAppApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(UniBagSpringBootAppApplication.class, args);


        logger.info(&quot;애플리케이션이 시작되었습니다.&quot;);
        logger.debug(&quot;디버그 로그입니다.&quot;);
        logger.error(&quot;에러 로그입니다.&quot;);
    }

}
</code></pre>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/ace2d26e-cfd4-4868-854f-ac1406acc876/image.png" alt="">
로컬 환경에서 실행을 하였기 때문에 INFO 이상의 레벨에 해당하는 로그들이 잘 출력되는 것을 알 수 있다.</p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><a href="https://wikidocs.net/163118">https://wikidocs.net/163118</a></li>
<li><a href="https://velog.io/@pgmjun/LogBack%EC%9D%84-%ED%86%B5%ED%95%9C-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC">https://velog.io/@pgmjun/LogBack을-통한-효율적인-로그-관리</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Gradle 버전 오류]]></title>
            <link>https://velog.io/@developer-daily/Flutter-Gradle-%EB%B2%84%EC%A0%84-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@developer-daily/Flutter-Gradle-%EB%B2%84%EC%A0%84-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Sat, 16 Nov 2024 15:54:07 GMT</pubDate>
            <description><![CDATA[<p>요즘 졸업작품으로 대학생들과 함께 배달음식을 시켜먹는 앱을 플러터를 통해 만들고 있다.
원래 올해 초부터 개발을 진행하고 있었지만 이러저러한 사정으로 인해 7개월정도 개발이 중단되었다.
그래서 오랜만에 플러터를 설치하고 실행하니 다음과 같은 오류가 발생하였다.</p>
<pre><code class="language-bash">Your project&#39;s Gradle version is incompatible with the Java version that Flutter is using for Gradle.</code></pre>
<h2 id="문제가-발생한-이유">문제가 발생한 이유</h2>
<p>해당 오류를 구글링 해보았고 Gradle과 Java 버전이 서로 호환되지 않아 발생하지 않은 오류라는 것을 알 수 있있다.</p>
<p>Java JDK 별 Gradle 지원 버전은 아래와 같다.
<img src="https://velog.velcdn.com/images/developer-daily/post/b28d88b6-1273-4e4f-b808-25d3209e0a2c/image.png" alt=""></p>
<p>해당 문제를 해결하기 위해 <code>android/gradle/wrapper</code> 안에 있는 <code>gradle-wrapper.properties</code> 파일을 통해 gradle 버전을 확인하였다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/8c0194ca-f685-414d-835e-d2211bd5c17c/image.png" alt=""></p>
<p><code>distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip</code>을 통해 현재 gradle 버전이 7.6.3이라는 걸 알 수 있었다.</p>
<h2 id="해결방법-고민">해결방법 고민</h2>
<p>현재 나는 JAVA 17버전을 사용하고 있기 때문에 정상적으로 동작해야한다. 하지만 현재 오류가 발생하고 있다. 대부분의 블로그에서 gradle 버전 때문에 발생한다고 설명하고 있지만 나는 조금 다르게 생각했다.</p>
<blockquote>
<p>현재 플러터에서 자바 17버전을 사용하고 있지 않는 것은 아닐까?</p>
</blockquote>
<p>그래서 다음 명령어를 통해 해당 플러터 프로젝트 정보를 확인해보았다.</p>
<pre><code class="language-bash">flutter analyze --suggestions</code></pre>
<p>예상대로 현재 자바 버전이 17이 아닌 21로 잡혀있었다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/64ec858d-f0ba-4f80-bb21-b8d91b3d419e/image.png" alt=""></p>
<p><code>JAVA_HOME</code> 제대로 설정 되어있고 자바 버전도 제대로 17로 잡히는데 왜 이런 오류가 발생하는지 정확히는 모르겠다.
안드로이드 스튜디오를 깔면 자동으로 자바가 설치되는데 그것 때문일지도 모르겠다(그저 단순한 추측이다).</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/efc49208-0f2e-4836-aa10-72c6328cf1d4/image.png" alt=""> <img src="https://velog.velcdn.com/images/developer-daily/post/a7d650b5-7305-41f0-b682-1c09863674b7/image.png" alt=""></p>
<h2 id="해결방법">해결방법</h2>
<p>이런 상황에서 문제를 해결할 수 있는 방법은 다음 명령을 통해 플러터에서 사용할 자바 버전을 직접 명시해주는 것이다.</p>
<pre><code class="language-bash">flutter config --jdk-dir /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/</code></pre>
<p>나의 경우 자바 17버전의 위치가 <code>/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/</code>이기 때문에 위와 같이 작성해지만 이와 다를 경우 자신에게 맞는 경로를 찾아 넣어주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/c03ed8dd-978f-431e-be88-1a400289b2e5/image.png" alt="">
이후 <code>flutter analyze --suggestions</code>를 명령을 통해 프로젝트 정보를 확인하였더니 Gradle과 Java의 버전이 서로 호환되었고 문제가 해결되었다.</p>
<h2 id="마무리">마무리</h2>
<p>앞으로 이런 상황이 발생하지 않도록 Gradle과 Java를 잘 확인해야겠다.
모두들 Happy Coding!😁</p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://ffuny.tistory.com/146">https://ffuny.tistory.com/146</a>
<a href="https://adjh54.tistory.com/214">https://adjh54.tistory.com/214</a>
<a href="https://stackoverflow.com/questions/76123807/my-projects-gradle-version-is-incompatible-with-the-java-version-that-flutter-i">https://stackoverflow.com/questions/76123807/my-projects-gradle-version-is-incompatible-with-the-java-version-that-flutter-i</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring과 Spring Boot를 비교해보자]]></title>
            <link>https://velog.io/@developer-daily/Spring%EA%B3%BC-Spring-Boot%EB%A5%BC-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@developer-daily/Spring%EA%B3%BC-Spring-Boot%EB%A5%BC-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 22 Sep 2024 05:38:10 GMT</pubDate>
            <description><![CDATA[<p>Spring과 Spring Boot는 둘 다 Java 기반의 애플리케이션을 개발할 때 사용하는 프레임워크지만, 목적과 편의성 면에서 차이가 있습니다.</p>
<h2 id="spring-framework">Spring Framework</h2>
<p>Spring은 Java 기반 애플리케이션을 개발하기 위한 오픈소스 프레임워크입니다. 
의존성 주입(DI), AOP(Aspect-Oriented Programming) 같은 핵심 기능을 제공하여 복잡한 Java 애플리케이션의 구조를 단순화합니다.</p>
<p><strong>XML 설정이나 자바 설정 파일을 통해 각 컴포넌트에 대한 설정을 세밀하게 조절할 수 있습니다.</strong> 하지만 개발자가 직접 대부분의 설정을 해야되기 때문에 설정 작업이 번거로울 수 있습니다.</p>
<h2 id="spring-boot">Spring Boot</h2>
<p>Spring Boot는 Spring의 복잡한 설정 작업을 자동화하여 빠르게 애플리케이션을 개발할 수 있도록 도와주는 프레임워크이며 이를 통해 Spring을 더욱 쉽게 사용할 수 있습니다.</p>
<p><strong>Spring Boot는 Spring과 비교하였을 때 다음과 같은 장점들을 가지고 있습니다.</strong></p>
<ul>
<li>미리 정의된 기본 설정과 자동 설정 기능을 통해 복잡한 XML 설정이나 Java 설정 파일을 크게 줄였습니다. </li>
<li>기본적으로 Spring Boot 스타터 패키지를 통해 필요한 의존성을 쉽게 추가할 수 있습니다.</li>
<li>내장 서버(Tomcat, Jetty 등)를 제공하여 애플리케이션 실행이 간편합니다.</li>
</ul>
<p>그럼 각각의 기능에 대해 한번 살펴보도록 하겠습니다.</p>
<h2 id="spring-boot의-장점">Spring Boot의 장점</h2>
<h3 id="자동-설정auto-configuration">자동 설정(Auto-configuration)</h3>
<p>Spring Boot는 어플리케이션의 실행 환경이나 코드에 따라 필요한 빈이나 설정을 자동으로 적용합니다.
개발자가 따로 명시하지 않으면 스프링부트가 기본적을 제공하는 설정을 자동적으로 사용합니다.</p>
<p>예를 들어 만약 Spring Boot 프로젝트에서 데이터베이스 의존성을 추가하면, Spring Boot는 기본적으로 H2 같은 인메모리 데이터베이스를 자동으로 설정합니다. 만약 MySQL을 사용하고자 할 경우, 의존성만 추가하고 약간의 설정만 하면 됩니다. Spring Boot는 자동으로 데이터베이스 연결을 설정하고 필요한 빈을 구성합니다.</p>
<p>반대로 Spring 프로젝트에서 데이터베이스를 설정하려면, 별도로 데이터 소스, 트랜잭션 매니저 등을 명시적으로 설정해야하는 번거로움이 존재합니다.</p>
<p>Auto-configuration을 사용하고 싶다면 <code>@EnableAutoConfiguration</code> 또는 <code>@SpringBootApplication</code> 어노테이션을 추가하면 됩니다.</p>
<pre><code class="language-java">import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

}</code></pre>
<h3 id="스타터-패키지를-통한-간단한-의존성-추가">스타터 패키지를 통한 간단한 의존성 추가</h3>
<p>Spring Boot는 특정 기능을 사용할 때 필요한 의존성을 간단하게 추가할 수 있도록 <strong>스타터 패키지</strong>라는 것을 제공합니다. 스타터 패키지는 특정 기능을 위해 필요한 라이브러리와 설정을 미리 정의한 패키지로, 이를 사용하면 개발자가 각 라이브러리를 일일이 찾아 추가할 필요가 없습니다.</p>
<p>다음은 주로 사용하는 의존성 중 일부입니다.</p>
<ul>
<li><code>spring-boot-starter-web</code>: 이 스타터 패키지를 추가하면 Spring MVC, 내장 Tomcat 서버, Jackson 등의 라이브러리가 함께 포함됩니다. 개발자는 웹 애플리케이션 개발에 필요한 여러 라이브러리를 별도로 설정할 필요 없이 이 스타터 패키지를 추가하는 것만으로 쉽게 개발할 수 있습니다.<ul>
<li><code>spring-boot-starter-data-jpa</code>: JPA와 관련된 의존성과 설정이 포함되어 있어, 데이터베이스 관련 설정 작업이 대폭 줄어듭니다.</li>
</ul>
</li>
</ul>
<h3 id="yamlproperties-파일-사용">YAML/Properties 파일 사용</h3>
<p>Spring Boot는 전통적인 XML 설정 대신, 더 간결하고 가독성이 좋은 <code>application.properties</code>나 <code>application.yml</code> 파일을 통해 설정할 수 있습니다. 이 파일들은 환경 설정이나 애플리케이션의 동작 방식을 간단하게 정의할 수 있게 합니다.</p>
<p>예를 들어 데이터베이스 설정을 할 때, 다음과 같이 <code>application.properties</code> 파일에 간단한 설정만 추가하면 Spring Boot가 나머지 설정을 자동으로 처리합니다.</p>
<pre><code>spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update</code></pre><p>이러한 설정 파일을 사용하면 XML보다 훨씬 적은 설정으로 원하는 기능을 쉽게 구현할 수 있습니다.</p>
<h3 id="내장-서버-embedded-server">내장 서버 (Embedded Server)</h3>
<p>Spring Boot는 Tomcat, Jetty, Undertow 등의 웹 서버를 애플리케이션에 내장하여 제공합니다.
즉, 따로 서버 설정을 할 필요 없이 애플리케이션을 독립적으로 실행할 수 있습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 컨테이너인 ApplicationContext에 대해 알아보자!]]></title>
            <link>https://velog.io/@developer-daily/Spring-ApplicationContext</link>
            <guid>https://velog.io/@developer-daily/Spring-ApplicationContext</guid>
            <pubDate>Thu, 19 Sep 2024 11:37:19 GMT</pubDate>
            <description><![CDATA[<p>스프링은 스프링 컨테이너를 이용하여 자바 객체의 생명주기를 관리합니다.</p>
<p>스프링은 스프링 컨테이너로서 <code>BeanFactory</code> 라는 인터페이스르 제공하며 해당 인터페이스는  Bean을 관리하고 검색하는 기능을 제공합니다.</p>
<p><code>ApplicationContext</code>는 <code>BeanFactory</code>를 상속받은 인터페이스로 Bean의 관리 및 검색 기능 뿐만 아니라 다국화, 이벤트 기능 등 추가적인 기능들을 제공합니다.</p>
<p>그렇기 때문에 대부분 <code>ApplicationContext</code> 인터페이스의 구현체를 사용하여 개발을 진행합니다.</p>
<p>저는 이번에 <code>ApplicationContext</code>에 초첨을 맞춰 글을 써보고자 합니다.</p>
<h2 id="applicationcontext"><strong>ApplicationContext</strong></h2>
<p>Spring에서는 <code>ApplicationContext</code>의 다양한 구현체를 제공합니다.</p>
<p><code>ApplicationContext</code>의 사용 목적 중 하는 Bean의 관리 및 검색 기능을 제공하는 것입니다. </p>
<p>구현체에 따라 XML, Java 클래스 파일 등 다양한 형식의 설정 파일을 통해 스프링에서 관리할 자바 객체(Bean)을 추가할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/ada06efd-85cb-43f0-b2fa-b125d5ee22a0/image.png" alt=""></p>
<p>스프링에는 위와 같이 몇가지의 ApplicationContext 구현체가 존재한다.</p>
<p><code>AnnotationConfigApplicationContext</code>: Java 클래스 설정 파일을 기반으로 빈을 관리한다.</p>
<p><code>GenericXmlApplicationContext</code>: xml 설정 파일을 기반으로 빈을 관리한다.</p>
<p><code>xxxApplicationContext</code>: Java 혹은 xml 설정 파일 이외에 다른 형식의 설정파일로 빈을 관리한다.</p>
<p>이것이 가능한 이유는 Bean 등록을 <code>BeanDefinition</code>으로 추상화하여 생성하기 때문입니다.</p>
<p>다양한 형식의 설정 파일에 상관없이 <code>BeanDefinition</code> 객체가 생성됩니다.</p>
<h2 id="스프링-컨테이너의-동작">스프링 컨테이너의 동작</h2>
<h3 id="1-스프링-컨테이너-생성과정">1. 스프링 컨테이너 생성과정</h3>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/2ead5319-d4d0-4631-8652-bc573b5e3f60/image.png" alt=""></p>
<p>스프링 컨테이너는 다음과 같은 순서로 생성된다.</p>
<ol>
<li>new를 하여 스프링 컨테이너를 만든다.</li>
<li>스프링 컨테이너 안에는 <strong>스프링 빈 저장소</strong> 라는 것이 있는데 스프링 빈이 여기에 저장된다. 
이때 빈 이름이 Key, 빈 객체가 Value가 된다.</li>
<li>스프링 컨테이너를 생성할 때는 구성 정보(설정 정보)를 지정해주어야 하는데 여기서는 AppConfig.class이다.</li>
<li>이 AppConfig.class를 보고 스프링 빈 저장소에 등록한다.ㅌㅂ</li>
</ol>
<p>만약 동일한 이름의 빈 객체가 존재할 경우 오류가 발생한다.</p>
<h3 id="2-빈-등록-및-의존성-주입">2. 빈 등록 및 의존성 주입</h3>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/0d05565e-7be7-40d1-91d0-b6a29bcb79ee/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/fbc7c7dd-be64-4605-9a62-c056d4976373/image.png" alt=""></p>
<ol>
<li>설정 정보에 있는 정보를 스프링 빈 저장소에 등록한다. 이때 <strong>@Bean</strong>이 붙은 객체를 등록한다.</li>
<li>이때 빈 이름은 메서드 이름을 사용하며 빈 이름을 직접 부여할 수 도 있다.<ul>
<li><strong>빈 이름은 항상 다른 이름을 부여해야 한다.</strong></li>
</ul>
</li>
<li>Bean 생성 후, 스프링 컨테이너는 의존성 주입(Dependency Injection)을 수행합니다.<ul>
<li>의존성 주입은 Bean이 필요로 하는 다른 Bean을 찾아서 해당 Bean을 주입하는 작업을 의미합니다.</li>
<li>@Bean으로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.</li>
</ul>
</li>
</ol>
<h3 id="3-빈-초기화">3. 빈 초기화</h3>
<p>Bean이 생성되고 의존성이 주입된 후, 스프링 컨테이너는 Bean의 초기화 작업을 수행합니다.
초기화 작업은 커스텀한 초기화 메서드를 호출하거나 특정 인터페이스를 구현한 메서드를 실행하는 등의 방식으로 진행됩니다.</p>
<h3 id="4-빈-사용">4. 빈 사용</h3>
<ul>
<li>ac.getBean(빈 이름, 타입) 혹은 ac.getBean(타입)를 이용하여 빈 저장소에 등록된 빈 객체를 가져올 수 있다.</li>
</ul>
<pre><code class="language-java">AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

// ac.getBean(빈 이름, 타입)
MemberServiceImpl memberService = ac.getBean(&quot;memberService&quot;, MemberServiceImpl.class);

// ac.getBean(타입)
MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);</code></pre>
<h3 id="5-소멸">5. 소멸</h3>
<p>스프링 컨테이너가 종료될 때 또는 필요한 경우에 Bean의 소멸 작업을 수행합니다.
소멸 작업은 커스텀한 소멸 메서드를 호출하거나 특정 인터페이스를 구현한 메서드를 실행하는 등의 방식으로 진행됩니다.</p>
<h2 id="beandefinition">BeanDefinition</h2>
<p><code>BeanDefinition</code>은 빈 설정 메타정보라고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/aed8d657-a4c5-4d2a-8382-85f27b077897/image.png" alt=""></p>
<p><code>BeanDefinition</code> 라는 인터페이스 덕분에 스프링 컨테이너는 설정 파일이 자바 코드인지, xml인지 몰라도 빈을 관리할 수 있습니다.</p>
<p>BeanDefinition은 역할(interface)이 되고, AppConfig.class, appConfig.xml, appConfig.xxx 등과 같은 것들이 구현이 되어 역할과 구현으로 서로 나눠졌다고 볼 수 있습니다.</p>
<p><code>BeanDefinition</code> 에 대해 조금 더 자세히 알아보겠습니다.</p>
<pre><code class="language-java">public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
    private final AnnotatedBeanDefinitionReader reader;
...
}</code></pre>
<p>앞서 자바 설정 구현체 중에는 <code>AnnotationConfigApplicationContext</code> 가 있고 해당 코드를 보면 <code>AnnotatedBeanDefinitionReader</code> 라는 객체가 존재하며 이는 자바 설정 파일을 읽어 <code>BeanDefinition</code>를 생성하는 역할을 합니다.</p>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/9843d1aa-689b-4359-8c52-263b647a99dd/image.png" alt=""></p>
<p>이 뿐만 아니라 다른 <code>ApplicationContext</code> 의 구현체들도 각각 Reader객체가 존재하고 해당 객체가 설정파일을 읽어 <code>BeanDefinition</code>를 생성합니다.</p>
<p>BeanDefinition에는 다음과 같은 정보들이 포함되어 있습니다.</p>
<ul>
<li>BeanClassName : 생성할 빈의 클래스 명(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)</li>
<li>factoryBeanName : 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig</li>
<li>factoryMethodName : 빈을 생성할 팩토리 메서드 지정, 예) memberService</li>
<li>Scope : 싱글톤(기본값)</li>
<li>lazyInit : 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때까지 최대한 생성을 지연처리 하는지 여부</li>
<li>InitMethodName : 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명</li>
<li>DestoryMethodName : 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명</li>
<li>Constructor arguments, Properties : 의존관계 주입에서 사용한다. (자바 설정처럼 팩터리 역할의 빈을 사용하면 없음)</li>
</ul>
<p>실제로 BeanDefinition의 메타정보를 출력해보겠습니다.</p>
<p>아래코드는 Java로 작성된 설정파일입니다.</p>
<pre><code class="language-java">@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}</code></pre>
<p>해당 Java 설정파일을 의존성으로 하여 AnnotationConfigApplicationContext를 통해 의존성 컨테이너를 생성하고 개발자가 생성한 빈만(<code>beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION</code>) 추려 출력하도록 코드를 작성하였습니다.</p>
<pre><code class="language-java">class BeanDefinitionTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @DisplayName(&quot;빈 설정 메타정보 확인&quot;)
    @Test
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println(&quot;beanDefinitionName = &quot; + beanDefinitionName);
                System.out.println(&quot;beanDefinition = &quot; + beanDefinition);
            }
        }
}</code></pre>
<p>factoryBeanName는 AppConfig의 명이 되고, factoryMethodName는 빈 저장소에 저장된 빈의 이름(key)이 출력됩니다.</p>
<pre><code class="language-java">beanDefinitionName = memberService
beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false;
                               factoryBeanName=appConfig; factoryMethodName=memberService; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig

beanDefinitionName = memberRepository
beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; 
                             factoryBeanName=appConfig; factoryMethodName=memberRepository; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig</code></pre>
<h2 id="마무리">마무리</h2>
<p>해당 포스팅에서 스프링의 ApplicationContext과 이것의 동작과정을 알아보았고, 빈의 메타 정보를 가지고 있는 객체인 BeanDefinition에 대해서 알아보았습니다.</p>
<p>여러가지 형식의 설정 파일을 이용하여 빈을 스프링 컨테이너에 주입할 수 있지만 등록해야 할 빈이 수십개, 수백개가 되면 일일이 등록하기 귀찮고, 설정정보 자체가 커져 누락하는 문제까지 발생할 수 있다.</p>
<p>그래서 스프링은 설정정보 없이 자동으로 스프링 빈을 등록하는 <code>@ComponentScan</code>이라는 기능과 의존관계도 자동으로 주입하는 <code>@Autowired</code>라는 기능을 제공합니다. </p>
<p>해당 어노테이션을 사용하면 특정 범위 내에 있는 자바 클래스들 중 @Component라는 어노테이션이 붙은 클래스에 대해 스프링 컨테이너에서 자동으로 빈을 생성하여 생명주기를 관리해주기 때문에 개발자가 비지니스 로직에 더욱 집중할 수 있습니다. 해당 기능에 대해서는 기회가 되면 포스팅 할 수 있도록 하겠습니다. </p>
<p>긴 글 읽어주셔서 감사합니다.</p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://drcode-devblog.tistory.com/334">https://drcode-devblog.tistory.com/334</a>
<a href="https://velog.io/@max9106/Spring-ApplicationContext">https://velog.io/@max9106/Spring-ApplicationContext</a>
<a href="https://sorryday.tistory.com/21">https://sorryday.tistory.com/21</a>
<a href="https://devwarriorjungi.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88Spring-Container-%EB%B9%88Bean">https://devwarriorjungi.tistory.com/entry/스프링-컨테이너Spring-Container-빈Bean</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 컨테이너에 대해 알아보자(feat. IoC, DI)]]></title>
            <link>https://velog.io/@developer-daily/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90feat.-IoC-DI</link>
            <guid>https://velog.io/@developer-daily/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90feat.-IoC-DI</guid>
            <pubDate>Wed, 18 Sep 2024 13:09:36 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서 스프링이 뭔지에 대해 간단하게 알아보았습니다.
그래서 이번에는 스프링의 핵심 아키텍쳐인 스프링 컨테이너에 대해 알아보고자 합니다.
그전에 몇가지 집고 넘어가야 할 개념들이 있어 빠르게 한번 알아보겠습니다.</p>
<h2 id="iocinverse-of-control">IoC(Inverse of Control)</h2>
<blockquote>
<p>제어의 역전(Inverse of Control, IoC)는 사용할 객체를 직접 생성하지 않고, <strong>객체의 생명주기 관리를 외부(스프링 컨테이너)에 위임</strong>하는 설계 방식입니다.</p>
</blockquote>
<p>IoC는 프레임워크를 설계하는 방식 중 하나입니다. IoC을 사용하게 되면 객체의 생명주기를 개발자가 아닌 외부에서 관리하기 때문에 개발자는 비지니스 로직에 더욱 집중할 수 있다는 장점이 있습니다.</p>
<p>그렇다면 개발자가 작성한 클래스로 만들어진 객체를 스프링 프레임워크가 관리할 수 있도록 해야되는데 어떻게 그게 가능할까요?
스프링에서는 DI(Dependency Injection)라는 개념을 사용하여 해결합니다.</p>
<h2 id="didependency-injection">DI(Dependency Injection)</h2>
<blockquote>
<p>의존성 주입(DI, Dependency Injection)은 제어의 역전을 구현하는 방법 중의 하나로, 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴 입니다.</p>
</blockquote>
<p>스프링에서는 특정 객체가 사용할 또 다른 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용합니다. DI를 사용하지 않은 코드와 사용한 코드를 서로 비교하는 예제를 통해 DI가 뭔지 그리고 왜 사용해야 하는지 자세히 알아보겠습니다.</p>
<h3 id="di를-사용해야-하는-이유">DI를 사용해야 하는 이유</h3>
<p>알림을 보낼 때 메시지 서비스에서 제공하는 전송 메시지 문구를 사용하여 알림을 보내는 기능을 구현한다고 가정해보겠습니다.</p>
<pre><code class="language-java">// 메일 발송 서비스 클래스
class EmailService {
    public void sendEmail(String message) {
        System.out.println(&quot;Sending email: &quot; + message);
    }
}

// 알림 서비스 클래스
class NotificationService {
    private EmailService emailService = new EmailService(); // 직접 객체를 생성

    public void sendNotification(String message) {
        emailService.sendEmail(message);
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        NotificationService notificationService = new NotificationService();
        notificationService.sendNotification(&quot;Hello, DI without!&quot;);
    }
}</code></pre>
<p>위의 코드는 의존성 주입 없이 <code>NotificationService</code> 객체에 <code>EmailService</code> 객체를 직접 넣은 예시 코드입니다. 해당 코드의 단점은 무엇일까요?</p>
<p><strong>1. 객체가 서로 강한 결합을 가지고 있다.</strong></p>
<ul>
<li><code>NotificationService</code>는 <code>EmailService</code> 객체를 직성 생성하였기 때문에 가장 의존 관계를 가지고 있습니다. 그렇기 때문에 <code>EmailService</code>의 구현이 변경되면 <code>NotificationService</code>도 수정해야 합니다.</li>
<li>지금은 <code>EmailService</code> 의 <code>sendEmail</code> 메소드만 사용하고 있어 복잡하지 않지만 코드가 커지고 <code>EmailService</code> 에서 다양한 메소드를 사용할 경우 수정해야할 코드는 기하급수적으로 늘어날 것입니다.</li>
</ul>
<p><strong>2. 객체들 간의 관계가 아니라 클래스들 간의 관계가 맺어져 있다.</strong></p>
<ul>
<li>또한 위의 <code>NotificationService</code>와 <code>EmailService</code>는 객체들 간의 관계가 아니라 클래스들 간의 관계가 맺어져 있다는 문제가 있습니다.</li>
<li>올바른 객체지향적 설계라면 객체들 간에 관계가 맺어져야 합니다. </li>
</ul>
<p>위의 코드를 의존성 주입 방식을 활용하여 아래 코드와 같이 개선해보았습니다.</p>
<pre><code class="language-java">// 메일 발송 서비스 인터페이스
interface MessageService {
    void sendMessage(String message);
}

// 메일 발송 서비스 구현 클래스
class EmailService implements MessageService {
    public void sendMessage(String message) {
        System.out.println(&quot;Sending email: &quot; + message);
    }
}

// 알림 서비스 클래스
class NotificationService {
    private MessageService messageService;

    // 의존성 주입: 외부에서 객체를 주입받음
    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void sendNotification(String message) {
        messageService.sendMessage(message);
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        MessageService emailService = new EmailService();
        NotificationService notificationService = new NotificationService(emailService); // 의존성 주입
        notificationService.sendNotification(&quot;Hello, DI with!&quot;);
    }
}</code></pre>
<p><code>MessageService</code> 라는 인터페이스를 하나 추가했고 <code>NotificationService</code> 클래스에는 이전과 달리 객체를 직접 생성하는 것이 아닌 messageService 인터페이스를 매개변수로 받고 있습니다. 이후 <code>main()</code> 함수에 messageService의 구현체(<code>EmailService</code>)를 생성하고 주입받고 있습니다.</p>
<p><strong>인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해줍니다.</strong></p>
<p><strong>스프링에서도 특정 객체가 사용할 또 다른 객체를 직접 생성하지 않고 외부 컨테이너가 생성한 객체를 주입받아 사용합니다.</strong></p>
<h2 id="스프링에서-의존성을-주입하는-방법">스프링에서 의존성을 주입하는 방법</h2>
<p>스프링에서는 다양한 의존성 주입 방식을 지원합니다.</p>
<h3 id="생성자-주입constructor-injection"><strong>생성자 주입(Constructor Injection)</strong></h3>
<ul>
<li>생성자를 통해 의존 관계를 주입하는 방법입니다.</li>
<li>생성자 주입은 생성자의 호출 시점에 1회 호출 되는 것이 보장합니다. 그렇기 때문에 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용할 수 있습니다.</li>
<li>또한 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능하도록 편의성을 제공합니다.</li>
</ul>
<pre><code class="language-java">@Service
public class UserService {

    private UserRepository userRepository;
    private MemberService memberService;

        @Autowired // 생략 가능
    public UserService(UserRepository userRepository, MemberService memberService) {
        this.userRepository = userRepository;
        this.memberService = memberService;
    }

}</code></pre>
<h3 id="수정자-주입setter-injection">수정자 주입(Setter Injection)</h3>
<ul>
<li>필드 값을 변경하는 Setter를 통해서 의존 관계를 주입하는 방법입니다.</li>
<li>Setter 주입은 생성자 주입과 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용합니다.</li>
<li>@Autowired로 주입할 대상이 없는 경우에는 오류가 발생하며 주입할 대상이 없어도 동작하도록 하려면 @Autowired(required = false)를 통해 설정할 수 있습니다.</li>
<li>스프링 초기에는 수정자 주입이 자주 사용되었는데, 그 이유는 바로 getX, setX 등 프로퍼티를 기반으로 하는 자바 기본 스펙 때문이였습니다. 하지만 시간이 지나면서 점차 수정자 주입이 아닌 다른 방식이 주목받게 되었습니다.</li>
</ul>
<pre><code class="language-java">@Service
public class UserService {

    private UserRepository userRepository;
    private MemberService memberService;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Autowired
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }
} </code></pre>
<h3 id="필드-주입field-injection">필드 주입(Field Injection)</h3>
<ul>
<li>필드에 바로 의존 관계를 주입하는 방법입니다.</li>
<li>필드 주입을 이용하면 코드가 간결해져서 과거에 상당히 많이 이용되었던 주입 방법이빈다.</li>
<li>하지만 필드 주입은 외부에서 접근이 불가능하다는 단점이 존재하는데, 테스트 코드의 중요성이 부각됨에 따라 필드의 객체를 수정할 수 없는 필드 주입은 거의 사용되지 않게 되었다.</li>
<li>그렇기에 애플리케이션의 실제 코드와 무관한 테스트 코드나 설정을 위해 불가피한 경우에만 이용하도록 해야합니다.</li>
</ul>
<pre><code class="language-java">@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;

}</code></pre>
<h2 id="spring-container">Spring Container</h2>
<p>스프링 컨테이너는 스프링 프레임워크의 핵심 컴포넌트입니다.
스프링 컨테이너는 주로 객체의 생명주기를 관리하고, 생성된 객체들의 추가적인 기능들을 제공합니다.
앞서 설명한 IoC의 실체라고 볼 수 있습니다.
스프링에서는 자바 객체를 빈(Bean)이라고 부릅니다.</p>
<ul>
<li>스프링 컨테이너의 역할<ul>
<li><strong>의존성 주입</strong>: 객체 간의 의존성을 코드가 아닌 외부 설정 파일이나 애노테이션을 통해 설정할 수 있습니다. Spring Container가 이러한 의존성을 자동으로 주입하여 객체를 구성합니다.</li>
<li><strong>객체 생성 및 관리</strong>: Spring Container는 애플리케이션에서 필요한 빈(bean, 객체)을 생성하고, 해당 빈들의 라이프사이클을 관리합니다.</li>
</ul>
</li>
<li>장점<ol>
<li>프로그램의 진행 흐름과 구체적인 구현을 분리시킬 수 있습니다.</li>
<li>개발자는 <strong>비즈니스 로직에 집중할 수 있습니다.</strong></li>
<li><strong>구현체 사이의 변경이 용이</strong>합니다.</li>
<li>객체 간 <strong>의존성이 낮아집니다.</strong></li>
</ol>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/6b8e82d5-383d-445c-bee4-6f7be025cdcd/image.png" alt=""></p>
<h3 id="application-context">Application Context</h3>
<p>일반적으로 Spring Container는 <code>ApplicationContext</code>를 의미합니다.
<code>ApplicationContext</code>는 <code>BeanFactory</code>를 비롯한 여러가지 인터페이스를 다중 상속한 인터페이스 입닌다.</p>
<p>상속을 받은 각각의 부모 인터페이스들의 역할은 다음과 같습니다.</p>
<ul>
<li><code>BeanFactory</code> : Bean을 관리하고 검색하는 기능을 제공하는 인터페이스</li>
<li><code>MessageSource</code> : 메세지 다국화를 위한 인터페이스</li>
<li><code>EnvironmentCapable</code> : 개발, 운영 등 환경을 분리해서 처리하고, 애플리케이션 구동에 필요한 정보들을 관리하기 위한 인터페이스</li>
<li><code>ApplicationEventPublisher</code> : 이벤트를 발행하고 구독하는 모델을 편리하게 지원하는 인터페이스</li>
<li><code>ResourceLoader</code> : 파일, class path 등 리소스를 읽어오기 위한 인터페이스        </li>
</ul>
<p><strong>Spring 공식문서 상 컨테이너를 사용해야 할때 특별한 이유가 없다면, ApplicationContext 를 사용하라고 권장합니다. 그 이유는 BeanFactory를 포함한 여러 인터페이스의 모든 기능을 ApplicationContext가 포함하고 있기 때문입니다.</strong></p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://hudi.blog/inversion-of-control/">https://hudi.blog/inversion-of-control/</a>
<a href="https://mangkyu.tistory.com/125">https://mangkyu.tistory.com/125</a>
<a href="https://lucas-owner.tistory.com/39">https://lucas-owner.tistory.com/39</a>
<a href="https://innovation123.tistory.com/167">https://innovation123.tistory.com/167</a>
<a href="https://junhkang.tistory.com/43">https://junhkang.tistory.com/43</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링(Spring)에 대해 알아보자!]]></title>
            <link>https://velog.io/@developer-daily/%EC%8A%A4%ED%94%84%EB%A7%81Spring-%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@developer-daily/%EC%8A%A4%ED%94%84%EB%A7%81Spring-%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Fri, 13 Sep 2024 12:09:48 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>저는 올해 초 스프링에 입문하고 혼자 혹은 팀으로 여러 프로젝트를 진행했습니다. 지금 시점에서 저 자신을 돌아보니 코딩은 어느정도(?) 하는거 같은데 스프링에 대한 이론적인 지식은 많이 부족하다는 것을 알게되었습니다. 이번에 확실히 스프링 부트에 대한 이론은 제대로 알고 넘어가야 스프링 부트 개발자로서 더 성장할 수 있다고 생각되어 글을 쓰게 되었습니다. 그럼 함께 스프링에 대해 공부해볼까요?🥹</p>
<h2 id="스프링">스프링</h2>
<p>스프링이란 무엇일까요? 
해당 의문에 대해 정답을 찾기위해 일부 블로그를 찾아보았고 저에게 가장 와닿는 정의는 다음과 같았습니다.</p>
<blockquote>
<p><strong>엔터프라이즈용 Java 애플리케이션 개발을 위한 오픈소스 경량 애플리케이션 프레임워크</strong></p>
</blockquote>
<p>즉, 기업에서 사용할 수 있을 정도의 대규모의 어플리케이션을 만들기에 적합한 오픈소스 프레임워크입니다. 
해당 정의에서 생소할수도 있는 부분을 정리하고 넘어가겠습니다.</p>
<h3 id="경량">경량</h3>
<p>대규모의 어플리케이션을 만들기에 적합하다고 했는데 왜 경량일까요?
여기서 말하는 경량은 개발자가 작성해야 할 코드의 길이가 적음을 의미합니다.
스프링을 사용하기 이전에 EJB라는 기술을 기업에서 많이 사용하였습니다. 하지만 해당 기술을 복잡하고 EJB에서 기술이나 패턴을 강제하였기 때문에 개발자가 비지니스 로직에 집중하기 힘들었스빈다.</p>
<p>스프링은 기존 기술(EJB)에서 불가피하게 사용되어야만 했던 복잡한 코드들을 모두 제거하고 코드의 복잡성을 낮춰 개발자가 비지니스 로직에 더 집중할 수 있도록 설계되었습니다.</p>
<h3 id="애플리케이션-프레임워크">애플리케이션 프레임워크</h3>
<p>개발을 조금 해보신 분들이라면 프레임워크라는 단어를 접해본 적이 있을 겁니다.
프레임워크는 라이브러리와 서로 반대되는 개념이라고 볼 수 있습니다.</p>
<h3 id="libary-vs-framework">Libary VS Framework</h3>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/41ed2c1b-5b1f-4917-86f3-d758bba014e1/image.png" alt=""></p>
<p><strong>라이브러리와 프레임워크 차이는 누가 제어권을 가지냐에 따라 다르다고 볼 수 있습니다.</strong></p>
<p><strong>라이브러리</strong>는 개발자가 필요한 기능을 선택하여 임포트하고, 언제, 어디서 호출할지 결정할 수 있어 제어권은 개발자가 가진다고 볼 수 있습니다.</p>
<p>반대로 <strong>프레임워크</strong>은 이미 전체 어플리케이션의 구조와 흐름이 프레임워크를 제작한 사람에 의해 결정되어 있고 개발자는 그 구조에 맞춰 코드를 작성해야 합니다. 그렇기에 제어권은 프레임워크에 있으며, 프레임워크가 개발자의 코드를 호출하게 됩니다.
이처럼 개발자가 작성한 객체나 메서드의 제어를 개발자가 아니라 외부에 위임하는 설계 원칙을 <strong>제어의 역전(Inversion Of Control, IoC)</strong> 이라고 합니다. 즉, 프레임워크는 제어의 역전 개념이 적용된 대표적인 기술이라고 할 수 있습니다.</p>
<pre><code class="language-java">import org.apache.commons.lang3.StringUtils;

public class LibraryExample {
    public static void main(String[] args) {
        String original = &quot;   Hello, Java!   &quot;;

        // 라이브러리의 trim 기능 호출
        String trimmed = StringUtils.trim(original);

        System.out.println(&quot;Original: \&quot;&quot; + original + &quot;\&quot;&quot;);
        System.out.println(&quot;Trimmed: \&quot;&quot; + trimmed + &quot;\&quot;&quot;);
    }
}
</code></pre>
<p>위의 예시코드는 라이브러리를 사용한 예제로 <code>StringUtils</code> 라이브러리의 <code>trim()</code> 메소드를 사용하여 <code>original</code>의 앞, 뒤 공백을 제거하는 코드입니다.
해당 코드에서 제어권은 개발자에게 있습니다. 개발자가 StringUtils.trim()을 언제 호출할지 결정합니다.</p>
<pre><code class="language-java">// 프레임워크 예시 (Spring Framework 사용)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class FrameworkExample {
    public static void main(String[] args) {
        SpringApplication.run(FrameworkExample.class, args);
    }
}

@RestController
class HelloController {
    @GetMapping(&quot;/hello&quot;)
    public String hello() {
        return &quot;Hello, Spring Framework!&quot;;
    }
}
</code></pre>
<p>반대로 위의 예시코드는 스프링이 처음이시라면 이해가 어려울 수 있지만 프레임워크를 사용한 예제로 Spring Boot 프레임워크를 사용해 간단한 웹 서버를 실행하고, /hello라는 경로로 요청이 들어오면 &quot;Hello, Spring Framework!&quot;를 반환하는 REST API를 만들었습니다.</p>
<p>해당 코드에서 제어권은 프레임워크에 있습니다. SpringApplication.run()을 호출하면 Spring이 애플리케이션을 제어하고, 요청이 들어왔을 때 hello() 메소드를 호출합니다.</p>
<p>해당 프레임워크 섹션의 개념들이 좀 많이 낮설고 왜 쓰는지 이해가 안될 수 있다고 생각합니다. 뒷부분에서 조금 더 상세하게 다뤄보겠습니다.</p>
<h2 id="스프링의-특징">스프링의 특징</h2>
<p>지금까지 스프링의 정의에 대해 알아보았습니다.
그렇다면 스프링 프레임워크의 특징에 대해 알아보도록 하겠습니다.</p>
<h3 id="pojo-프로그래밍">POJO 프로그래밍</h3>
<p>스프링은 POJO 프로그래밍을 채택하였습니다.</p>
<blockquote>
<p>POJO 프로그래밍은 다른 기술을 사용하지 않고 오로지 Java 기술만을 사용하여 객체를 만드는 방식입니다.</p>
</blockquote>
<p>POJO(Plain Old Java Object)는 오래된 방식의 간단한 자바 오브젝트라는 말로서 Java EE 등의 중량 프레임워크들을 사용하게 되면서 해당 프레임워크에 종속된 무거운 객체를 만들게 된 것에 반발해서 사용되게 된 용어이다. POJO는 2000년 9월에 마틴 파울러, 레베카 파슨, 조쉬 맥킨지 등이 사용하기 시작한 용어이다.</p>
<pre><code class="language-java">public class Burger {

    private String bread; // 빵
    private String patty; // 패티

    // getter
    public long getBread() {
        return bread;
    }
    public String getPatty() {
        return patty;
    }

    // setter
    public void setBread(String bread) {
        this.bread = bread;
    }
    public void setPatty(String patty) {
        this.patty = patty;
    }
}</code></pre>
<p>위의 코드는 field, getter, setter만을 이용하여 만들어진 POJO 프로그래밍의 조건을 만족하는 코드입니다.
그렇다면 스프링은 왜 POJO 프로그래밍을 채택하였을까요?</p>
<p>스프링이 나오기 전 기업에서는 EJB(Enterprise Java Bean)이라고 불리는 기술을 사용하여 서비스를 구현하였습니다. </p>
<blockquote>
<p><strong>EJB(Enterprise Java Bean)</strong>
기업환경의 시스템을 구현하기 위한 서버측 컴포넌트 모델이다. 
자바를 이용해 비즈니스 서비스를 개발할 때 비즈니스 로직 뿐만 아니라 트랜잭션, 보안 등 로우레벨의 로직까지 작성해야하는 부담감을 없애고자 EJB(Enterprise Java Beans)를 만들게 되었다.</p>
</blockquote>
<p>당시에 EJB는 강력한 기능들을 지원했지만 그에 따라 아래와 같은 문제점이 존재했습니다.</p>
<ul>
<li><p><strong>EJB의 복잡성</strong>: EJB는 트랜잭션 관리, 원격 호출, 보안 등 엔터프라이즈 기능을 제공했지만, 이를 사용하려면 많은 설정과 복잡한 구조를 요구했습니다. 개발자는 EJB 스펙에 맞춰 작성해야 했고, 이는 학습 곡선을 높이며 생산성을 떨어뜨렸습니다.</p>
</li>
<li><p><strong>프레임워크 종속성</strong>: EJB에서 작성된 코드는 특정 컨테이너에 종속적이었으며, 이를 다른 환경에서 재사용하기 어려웠습니다. 비즈니스 로직을 단순하게 구현하더라도 EJB 스펙을 따르다 보니 코드가 무거워지고 유지보수성이 떨어졌습니다.</p>
</li>
<li><p><strong>유연성 부족</strong>: EJB는 특정 기술이나 패턴을 강제했기 때문에, 개발자가 더 유연한 설계를 적용하기 어려웠습니다. 또한 테스트 환경 설정이 복잡해 단위 테스트를 효율적으로 수행하기 어려웠습니다.</p>
</li>
</ul>
<p>이러한 문제를 해결하고자 Spring에서는 POJO 프로그래밍을 지향하게 되었고 다음과 같은 장점을 누릴 수 있었습니다.</p>
<ul>
<li><p><strong>단순한 비즈니스 로직 작성</strong>: POJO는 단순한 자바 객체를 의미합니다. EJB처럼 복잡한 인터페이스를 구현하거나 특정 규칙을 따를 필요가 없기 때문에 개발자는 자유롭게 비즈니스 로직을 구현할 수 있습니다. 이는 코드의 가독성과 유지보수성을 크게 향상시켰습니다.</p>
</li>
<li><p><strong>테스트 용이성</strong>: POJO 기반의 코드는 프레임워크에 종속적이지 않으므로, 독립적으로 테스트할 수 있습니다. 스프링은 객체 생성과 의존성 관리를 IoC 컨테이너를 통해 처리하기 때문에, 테스트 시 의존성을 쉽게 대체할 수 있어 단위 테스트가 매우 용이해졌습니다.</p>
</li>
<li><p><strong>경량화</strong>: 스프링은 EJB처럼 무겁고 복잡한 설정 파일 없이도 핵심 엔터프라이즈 기능을 제공했습니다. POJO 객체를 관리하면서도, 트랜잭션 관리, AOP(Aspect-Oriented Programming), 의존성 주입 등 다양한 기능을 유연하게 제공했습니다.</p>
</li>
<li><p><strong>프레임워크 종속성 탈피</strong>: 스프링의 POJO 프로그래밍 방식은 특정 API나 기술 스택에 종속되지 않습니다. 스프링에서 관리하는 빈(Bean)은 단순한 자바 객체이므로, 다른 환경에서도 쉽게 사용 가능하며 재사용성도 높습니다.</p>
</li>
</ul>
<h3 id="pojo-프로그래밍를-지원하는-spring">POJO 프로그래밍를 지원하는 Spring</h3>
<p><img src="https://velog.velcdn.com/images/developer-daily/post/614783b8-d79f-4693-ad30-aeba1dd4d522/image.png" alt=""></p>
<p>스프링에서 이러한 POJO 프로그래밍을 지원하기 위해 다음과 같은 대표적인 3가지 기술들을 사용합니다. 각각 알아보도록 하겠습니다.</p>
<h3 id="iocinverse-of-control">IoC(Inverse of Control)</h3>
<p>먄약 우리가 스프링 프레임워크에 우리가 만들고자 하는 비지니스 로직을 추가할려면 어떻게 해야 될까요? </p>
<p>스프링 프레임워크는 IoC라는 설계 원칙을 따라 구현된 프레임워크이기 때문에 <strong>객체의 생성과 의존 관계를 개발자가 직접 관리하는 것이 아니라, Spring 컨테이너가 대신 관리하도록 위임합니다.</strong></p>
<p>우리가 추가하고자 하는 비지니스 로직은 객체로 이루어져 있습니다. 이러한 객체를 스프링이 스스로 관리하도록 위임하기 위해서는 우리의 비지니스 로직 객체를 스프링 프레임워크에게 전달해야 합니다.
이러한 전달을 위해 스프링에서는 DI(Dependency Injection)이라고 불리는 개념을 적용하였습니다. </p>
<p><strong>의존성 주입(DI)은 객체가 다른 객체에 의존할 때, 그 의존성을 객체 내부에서 직접 생성하지 않고 외부에서 주입하는 방식입니다.</strong> 즉, 우리의 비지니스 로직을 의존성이라는 형태로 스프링 프레임워크에 주입하게 되면 비지니스 로직이 동작하게 되는 것이죠.</p>
<p><strong>스프링에서 IoC (Inversion of Control) 개념을 구현하는 중요한 역할을 하는 것이며
Spring Container는 Spring 프레임워크에서 객체의 생성, 관리, 소멸을 담당하는 핵심 컴포넌트입니다.</strong>
Spring Container까지 다루기에는 글이 너무 길어져 다음 번에 기회가 된다면 따로 다룰 수 있도록 하겠습니다.</p>
<h3 id="aopaspect-oriented-programming">AOP(Aspect-Oriented Programming)</h3>
<p>Spring AOP(Aspect-Oriented Programming)는 관점 지향 프로그래밍을 의미하며, 코드에서 공통적으로 사용되는 기능을 분리하여 관리하는 방법입니다. 핵심 로직 외에 공통적으로 적용되는 기능(예: 로깅, 트랜잭션 관리, 보안 등)을 분리하여 모듈화함으로써 코드의 가독성과 유지보수성을 높일 수 있습니다.</p>
<p>즉, 비지니스 로직이라는 핵심적인 관심 사항과 로깅, 트랜잭션 관리, 보안 등과 같은 공통적으로 사용되는 관심 사항을 서로 분리시켜 개발자가 비지니스 로직에 더 집중하고 유지보수성을 높일 수 있습니다.</p>
<h3 id="psaportable-service-abstraction">PSA(Portable Service Abstraction)</h3>
<p>Spring PSA(Portable Service Abstraction)는 스프링 프레임워크의 핵심 개념 중 하나로, 서비스 추상화 계층을 제공합니다. 말이 어려운데 쉽게 설명하면 PSA는 다양한 서비스의 구현을 추상화하고, 현재 필요한 구현 클래스로 바꿔끼는 방식으로 쉽게 다른 기술로 전환할 수 있다는 의미입니다. </p>
<p>예를 들어 API로 요청이 들어오면 쿼리문을 통해 MySQL 데이터베이스에 요청받은 데이터를 저장해야 된다고 가정해보겠습니다. 
이때 갑자기 MySQL에서 MariaDB로 데이터베이스를 바꾼다고 생각해보겠습니다. 아마 MySQL에 맞춰 짠 쿼리문을 모두 변경해야될 수도 있습니다. (저 같으면 아마 멘붕이 올 거 같네요.🤯)</p>
<p>이런 경우 스프링에서 제공하는 JDBC(Java DataBase Connectivity)나 JPA를 사용하면 추상화된 코드를 작성하고 필요한 데이터베이스 벤더에 맞춰 드라이버만 설정해주면 앞선 상황을 방지할 수 있다. 또한 중간에 데이터베이스의 벤더가 바뀌더라도 일부 코드만 수정하면 되기 때문에 유지보수에도 도움이 된다.</p>
<h2 id="마무리">마무리</h2>
<p>정말 오랜만에 쓰는 글이라서 글에 두서가 없는 거 같습니다.😭
혹시라도 잘못된 부분이 있다면 댓글을 남겨주시면 확인하고 반영하겠습니다.
오늘도 즐거운 코딩 되세요!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[표준 스트림]]></title>
            <link>https://velog.io/@developer-daily/%ED%91%9C%EC%A4%80-%EC%8A%A4%ED%8A%B8%EB%A6%BC</link>
            <guid>https://velog.io/@developer-daily/%ED%91%9C%EC%A4%80-%EC%8A%A4%ED%8A%B8%EB%A6%BC</guid>
            <pubDate>Mon, 24 Jan 2022 09:39:05 GMT</pubDate>
            <description><![CDATA[<p>우리가 키보드, 마우스같은 단말기를 통해 어떠한 데이터(문자, 파일)를 입력하게 되면 컴퓨터는 프로그램을 통해 그에 알맞는 데이터를 출력시켜준다.</p>
<p>이러한 상황에서 사용되는 것이 표준스트림이다.</p>
<p>표준스트림의 사전적 정의는 아래와 같다(출처 - 위키피디아)</p>
<blockquote>
<p><strong>표준스트림(standard stream)</strong>이란 유닉스(unix)와 유닉스계열의 OS(linux, mac ...)에서 컴퓨터 프로그램과 단말기 사이에 존재하는 통로를 의미한다.</p>
</blockquote>
<p>즉, 표준스트림은 단말기와 프로그램 사이에서 일어나는 데이터 입출력을 추상화한 것이다</p>
<p>이러한 표준 스트림에는 아래의 3가지 스트림이 존재한다.</p>
<ol>
<li>표준입력 스트림(standard input stream)</li>
<li>표준출력 스트림(standard output stream)</li>
<li>표준오류 스트림(stdard error stream)</li>
</ol>
<p><img src="https://images.velog.io/images/developer-daily/post/8f1b9214-56c1-4c1d-a954-cc10f5536c13/Stdstreams-notitle.svg.png" alt=""></p>
<h2 id="표준-입력-스트림">표준 입력 스트림</h2>
<hr>
<p><strong>표준 입력 스트림</strong>이란 컴퓨터 프로그램에 입력되는 데이터 스트림(data stream)이다. </p>
<p>standard input(stdio), 표준입력이라고도 한다.</p>
<p>입력스트림은 기본적으로 키보드로 문자열을 입력을 받으나 <strong>리다이렉션(redirection)</strong>을 통해 파일을 입력받을 수 있다.</p>
<p>리다이렉션에 관한 내용은 따로 다루었다.
<a href="https://velog.io/@developer-daily/%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%EC%85%98%EA%B3%BC-%ED%8C%8C%EC%9D%B4%ED%94%84%EC%B2%98%EB%A6%AC">https://velog.io/@developer-daily/리다이렉션과-파이프처리</a></p>
<p>하지만 항상 입력이 존재해야되는 것은 아니다.</p>
<p>아래 ls 명령을 보면 인자(commad line argument)만 존재하고 따로 키보드나 파일을 통한 입력은 없는 것을 알 수 있다.</p>
<pre><code class="language-bash">$ ls -l //인자는 l</code></pre>
<h2 id="표준-출력-스트림">표준 출력 스트림</h2>
<hr>
<p>표준 출력 스트림이란 컴퓨터 프로그램에서 출력되는 데이터 스트림이다.</p>
<p>기본적으로 텍스트 터미널을 통해 모니터에 출력 되지만, 리다이렉션을 통해 파일에 출력값을 저장 할 수 있다.</p>
<p>예를 들어 유닉스의 ls 명령을 수행하게 되면 결과 값으로 현재 디렉토리에 있는 파일들의 목록을 출력된다.</p>
<pre><code class="language-bash">$ ls -l
//출력 결과
total 64
lrwxrwxrwx   1 root root    7 Jan  5 16:54 bin -&gt; usr/bin
drwxr-xr-x   2 root root 4096 Apr 15  2020 boot
drwxr-xr-x   5 root root  360 Jan 16 12:55 dev
drwxr-xr-x   1 root root 4096 Jan 16 08:28 etc
drwxr-xr-x   2 root root 4096 Apr 15  2020 home
lrwxrwxrwx   1 root root    7 Jan  5 16:54 lib -&gt; usr/lib
.
.
.</code></pre>
<p>하지만 항상 출력이 존재하는 것은 아니다.</p>
<p> 아래와 같이 명령의 수행이 성공 했을 경우 아무런 출력이 나타나지 않는다.</p>
<pre><code class="language-bash">$ rm -r newfolder</code></pre>
<h2 id="표준-오류-스트림">표준 오류 스트림</h2>
<hr>
<p>표준 오류 스트림은 프로그램이 오류메시지나 진단을 출력하기 위해 일반적으로 쓰이는 또다른 출력 스트림이다.</p>
<p>즉, 명령 수행에 실패하였을 때 출력되는 오류들을 말한다.</p>
<pre><code class="language-bash">$ mv hello.txt
mv: missing destination file operand after &#39;hello&#39; //오류 출력
Try &#39;mv --help&#39; for more information.</code></pre>
<h2 id="파일-서술자">파일 서술자</h2>
<hr>
<p>파일 서술자(file descriptor, 파일 디스크립터)는 리눅스에서 프로세스가 파일을 사용할 때 다루는 고유한 값이다.</p>
<p>이 고유한 값은 각각의 스트림마다 하나씩 가지고 있다.
0은 표준 입력스트림, 1은 표준 출력스트림, 2는 표준 오류스트림이다.</p>
<p>파일에 사용되기 때문에 <strong>리다이렉션</strong>이 필요한 상황에서 사용된다.</p>
<hr>
<p>참고자료</p>
<ul>
<li>위키피디아 - 표준스트림(standard stream)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[보수와 뺄셈의 관계]]></title>
            <link>https://velog.io/@developer-daily/%EB%B3%B4%EC%88%98%EC%99%80-%EB%BA%84%EC%85%88%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@developer-daily/%EB%B3%B4%EC%88%98%EC%99%80-%EB%BA%84%EC%85%88%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Fri, 21 Jan 2022 15:42:01 GMT</pubDate>
            <description><![CDATA[<p>보수란 각 자리의 숫자의 합이 어느 일정한 수가 되게 하는 수를 의미한다.</p>
<p>여기서 말하는 일정한 수가 10이라면 4에 대한 10의 보수는 6이 되고 2에 대한 10의 보수는 8이 된다</p>
<p>R진법에 대한 보수는 R-1의 보수와 R의 보수로 구분할 수 있다.</p>
<h2 id="r-1의-보수">R-1의 보수</h2>
<hr>
<p>R-1의 보수를 구하는 공식은 아래와 같다.</p>
<p>$$
(R^n-1) - N
$$</p>
<p>(여기서 N은 구하려는 보수, n은 N의 자리수이다)</p>
<p>예를 들어 10진수 N이 57이면 9의 보수는 100-1-57 ⇒ 42가 된다.</p>
<p>그럼 이진수는 어떨까?</p>
<p>이진수 N이 1010이라면 1의 보수는 1111-1010 =  0101이 된다. </p>
<p>1의 보수를 자세히 관찰하면 아래와 같은 규칙이 있다.</p>
<blockquote>
<p>📚 각 자리의 숫자가 <strong>0→1 , 1→0</strong>으로 바뀐다. 즉, <strong>토글(toggle)</strong>이 된다.</p>
</blockquote>
<p>컴퓨터에서는 토글을 이용하여 1의 보수를 구하게 된다.</p>
<h2 id="r의-보수">R의 보수</h2>
<hr>
<p>R의 보수를 구하는 공식은 아래와 같다</p>
<p>$$
(R^n-1) - N +1
$$</p>
<p>(여기서 N은 구하려는 보수, n은 N의 자리수이다)</p>
<p>2의 보수의 공식을 보면 1의 보수의 공식에 1을 더한 값인 것을 알 수 있다 (물론 $R^n - N$으로 구해도 무방하다)</p>
<p>그럼 10진수 N이 57이면 10의 보수는 42가 되고, 이진수 N이 1010이면 2의 보수는 0110이 된다.</p>
<h2 id="보수를-사용하는-이유">보수를 사용하는 이유</h2>
<hr>
<p>보수는 컴퓨터에서 뺄셈을 할 때 사용된다. 그럼 왜 굳이 보수를 이용해서 뺄셈을 하는 것일까?</p>
<p>컴퓨터는 기본적으로 덧셈 연산을 하는 장치만 존재한다. 왜냐하면 덧셈과 보수을 통해 충분히 뺄셈이 가능하기 때</p>
<p>문이다. 이런 상황에서 CPU에 뺄셈 연산 장치를 넣는 것은 쓸데없이 단가만 올리는 일이다.  </p>
<p>그럼 보수로 뺄셈을 하는 법을 알아보자.</p>
<h2 id="정수-계산에서의-비트구조">정수 계산에서의 비트구조</h2>
<hr>
<p> 그 전에 비트구조에 대해 알아보자 4비트의 구조를 예로 들겠다. </p>
<p>4비트는 총 4개의 비트로 이루어져 있기때문에 <strong>자연수(unsigned)만을 넣게 된다면 0~15까지의 숫자</strong>를 넣을 수 있다. </p>
<p>하지만 정수는 다르다. 정수에는 부호가 있다. </p>
<p>그렇기 때문에 <strong>정수계산에서는 가장 왼쪽 비트(MSB)는 부호비트(0이면 양수, 1이면 음수)</strong>로 사용된다. </p>
<p>결론적으로 아래와 같은 구조가 된다.</p>
<blockquote>
<p>📚 <strong>MSB는 부호비트, 나머지 비트들은 정수의 값</strong></p>
</blockquote>
<p>정수형 4비트는 -8~7(2의 보수 기준)까지의 숫자를 나타낼 수 있다.</p>
<h2 id="2의-보수를-이용한-뺄셈-연산">2의 보수를 이용한 뺄셈 연산</h2>
<hr>
<p>지금부터 아래 2가지의 뺄셈을 이진법으로 계산 해볼 것이다.</p>
<ol>
<li>9 - 3</li>
<li>4 - 6</li>
</ol>
<h3 id="1번-풀이"><strong>1번 풀이</strong></h3>
<p>9-3 =  9+(-3)으로 나타낼 수 있기 때문에 9에 -3을 합한 값을 구하면 된다. 그럼 -3은 어떻게 구할까?</p>
<p>2의 보수를 이용하면 된다. 3은 0011(2)인데 2의 보수를 구하면 1101(2)이 된다.</p>
<p>이 1101(2)은 10진수로 -3이 된다. </p>
<p>이 말이 사실인지 확인 해볼려면 3과 -3 즉, 0011과 1101을 더해보면 된다.</p>
<blockquote>
<p>📚 0011(2) + 1101(2) = 10000 ⇒ 가장 왼쪽의 올림수(carry) 삭제 ⇒ 0000</p>
</blockquote>
<p>결론적으로 0이 되므로 <strong>N에 대한 2의 보수 = -N</strong>이 성립함을 알 수 있다.</p>
<p>이에 따라 9+(-3)은 1001(2) + 1101(2) = 10110(2)이 된다. 여기서 가장 왼쪽의 올림수(carry)를 지워준다</p>
<p>그럼 0110(2) 10진수로 6이 된다.</p>
<h3 id="2번-풀이"><strong>2번 풀이</strong></h3>
<p>4-6 =  4+(-6)이므로 0100(2) + 1010(2) = 1110(2)이 된다.</p>
<h3 id="두-가지로-나눈-이유"><strong>두 가지로 나눈 이유</strong></h3>
<p>1번의 경우 큰 수에서 작은 수를 뺀 것인데 이때는 항상 <strong>가장 왼쪽의 올림수(carry)</strong>가 생기게 된다.</p>
<p>여기서 carry를 버리게 되면 <strong>부호비트가 0인 양수 값</strong>이 나오게 된다.</p>
<p>2번의 경우 작은 수에서 큰 수를 뺀 것이기 때문에 <strong>부호비트가 1인 음수</strong>가 나오게 된다.</p>
<p>여기서 결과값에 대한 2의 보수를 구한 다음 -부호를 붙여도 똑같은 결과가 된다</p>
<p>(EX. 1110(2) → -0010(2) = -2(10))</p>
<h2 id="마무리">마무리</h2>
<hr>
<p>2의 보수말고도 부호의 절댓값, 1의 보수를 이용해도 이진수의 뺄셈이 가능하지만 대부분의 컴퓨터가 2의 보수를 이용하여 뺄셈을 하기 때문에 2의 보수만 다루었다.</p>
]]></description>
        </item>
    </channel>
</rss>