<?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>Thu, 03 Apr 2025 06:59:42 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/_inho/profile/120e261b-3e40-41ae-ab96-ad7e7f47e3b2/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 개념 정리하기. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/_inho" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[📌 TODO 백엔드 API 개발하기]]></title>
            <link>https://velog.io/@_inho/TODO-%EB%B0%B1%EC%97%94%EB%93%9C-API-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_inho/TODO-%EB%B0%B1%EC%97%94%EB%93%9C-API-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 03 Apr 2025 06:59:42 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@_inho/TODO-API-%EB%AC%B8%EC%84%9C">API 문서</a></p>
<h2 id="1-프로젝트-구조">1. 프로젝트 구조</h2>
<pre><code>src/main/java
  ├── com.nhnacademy.todo
      ├── controller       // REST API 엔드포인트 정의
      ├── domain           // JPA 엔티티 클래스
      ├── dto              // 데이터 전송 객체 (DTO)
      ├── repository       // JPA 리포지토리 인터페이스
      ├── service          // 비즈니스 로직 구현
      ├── exception        // 사용자 정의 예외 처리
      └── config           // 설정 파일 (예: CORS, 데이터베이스 등)
src/main/resources
  ├── application.properties // 환경 설정 파일</code></pre><h2 id="2-엔티티">2. 엔티티</h2>
<pre><code class="language-java">package inho.domain;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.time.LocalDateTime;

@Entity
@NoArgsConstructor
@Data
@Table(name = &quot;todos&quot;)
@Slf4j
public class Todo {

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

    @Column(name = &quot;subject&quot;, nullable = false)
    private String subject;

    @Column(name = &quot;event_at&quot;, nullable = false)
    private String eventAt;

    @Column(name = &quot;created_at&quot;, nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @JsonCreator
    public Todo(
            @JsonProperty(&quot;subject&quot;) String subject,
            @JsonProperty(&quot;eventAt&quot;) String eventAt) {
        this.subject = subject;
        this.eventAt = eventAt;
        log.debug(&quot;eventAt 테스트 {}&quot;, eventAt);
        this.createdAt = LocalDateTime.now(); // createdAt을 항상 설정
    }

    @PrePersist
    protected void onCreate() {
        if (this.eventAt == null || this.eventAt.isEmpty()) {
            throw new IllegalArgumentException(&quot;eventAt 필드는 필수입니다&quot;);
        }
        if (this.createdAt == null) {
            this.createdAt = LocalDateTime.now();
        }
    }
}</code></pre>
<p>subject: 할 일 제목</p>
<p>eventAt: 할 일 날짜</p>
<p>createdAt: 생성 날짜 (자동 설정)</p>
<p>@PrePersist: 데이터 저장 전 필수 값 검증</p>
<h2 id="3-레포지토리">3. 레포지토리</h2>
<pre><code class="language-java">package inho.repository;

import inho.domain.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface TodoRepository extends JpaRepository&lt;Todo, Long&gt; {

    List&lt;Todo&gt; findByEventAt(String eventAt);

    long countByEventAt(String eventAt);

    boolean existsById(Long id);

    @Query(&quot;SELECT t from Todo t where t.eventAt LIKE :eventMonth%&quot;)
    List&lt;Todo&gt; findEventAtStartingWith(String eventMonth);
}</code></pre>
<ul>
<li>특정 날짜 또는 월별 조회 기능 제공</li>
<li>@Query를 활용하여 월별 데이터 조회</li>
</ul>
<h2 id="4-서비스">4. 서비스</h2>
<h3 id="서비스-구현-service-implementation">서비스 구현 (Service Implementation)</h3>
<pre><code class="language-java">package inho.service.impl;

import inho.domain.Todo;
import inho.exception.MaxTodoLimitExceededException;
import inho.exception.TodoNotFoundException;
import inho.repository.TodoRepository;
import inho.service.TodoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
public class TodoServiceImpl implements TodoService {
    private final TodoRepository todoRepository;
    private static final int DAILY_MAX_TODO_COUNT = 8;

    /**
     * 새로운 TODO를 저장
     */
    public Todo saveTodo(String subject, String eventAt) {
        long count = todoRepository.countByEventAt(eventAt);
        if (count &gt;= DAILY_MAX_TODO_COUNT) {
            throw new MaxTodoLimitExceededException(&quot;하루 최대 8개의 TODO만 등록 가능합니다.&quot;);
        }
        Todo todo = new Todo(subject, eventAt);
        return todoRepository.save(todo);
    }

    /**
     * 특정 날짜의 모든 TODO 삭제
     */
    public void deleteTodosByDate(String eventAt) {
        List&lt;Todo&gt; todos = todoRepository.findByEventAt(eventAt);
        if (todos.isEmpty()) {
            throw new TodoNotFoundException(&quot;해당 날짜(&quot; + eventAt + &quot;)에 등록된 TODO가 없습니다.&quot;);
        }
        todoRepository.deleteAll(todos);
    }

    /**
     * 특정 ID의 TODO 삭제
     */
    public void deleteTodoById(Long id) {
        if (!todoRepository.existsById(id)) {
            throw new TodoNotFoundException(&quot;ID &quot; + id + &quot;에 해당하는 TODO가 존재하지 않습니다.&quot;);
        }
        todoRepository.deleteById(id);
    }

    /**
     * 특정 날짜의 TODO 리스트 조회
     */
    public List&lt;Todo&gt; getTodosByDate(String eventAt) {
       return findTodos(eventAt, false);
    }

    /**
     * 특정 월의 TODO 리스트 조회
     */
    @Transactional(readOnly = true)
    public List&lt;Todo&gt; getTodosByMonth(String eventMonth) {
        return findTodos(eventMonth, true);
    }

    /**
     * 특정 날짜의 TODO 개수 반환
     */
    public long countTodosByDate(String date) {
        return todoRepository.countByEventAt(date);
    }

    /**
     * 특정 ID의 TODO 조회
     */
    public Todo getTodoById(Long id) {
        return todoRepository.findById(id)
                .orElseThrow(() -&gt; new TodoNotFoundException(&quot;ID &quot; + id + &quot;에 해당하는 TODO가 존재하지 않습니다.&quot;));
    }

    /**
     * 특정 날짜 또는 월의 TODO 리스트 조회 (공통 로직)
     */
    private List&lt;Todo&gt; findTodos(String queryParam, boolean isMonthly){
        return isMonthly ? todoRepository.findEventAtStartingWith(queryParam) : todoRepository.findByEventAt(queryParam);
    }
}</code></pre>
<h3 id="서비스-인터페이스">서비스 인터페이스</h3>
<pre><code class="language-java">package inho.service;

import inho.domain.Todo;
import java.util.List;

public interface TodoService {
    Todo saveTodo(String subject, String eventAt);
    void deleteTodosByDate(String eventAt);
    void deleteTodoById(Long id);
    List&lt;Todo&gt; getTodosByDate(String eventAt);
    List&lt;Todo&gt; getTodosByMonth(String eventMonth);
    long countTodosByDate(String date);
    Todo getTodoById(Long id);
}</code></pre>
<p>🔹 핵심 정리</p>
<ul>
<li>비즈니스 로직은 TodoServiceImpl에서 관리</li>
<li>인터페이스를 통해 서비스의 계약을 정의</li>
<li>트랜잭션을 관리하여 데이터 일관성 유지</li>
</ul>
<h2 id="5-컨트롤러">5. 컨트롤러</h2>
<blockquote>
<p>컨트롤러 계층은 클라이언트로부터 HTTP 요청을 받아 서비스 계층을 호출하고, 그 결과를 적절한 HTTP 응답으로 변환하는 역할을 한다.</p>
</blockquote>
<p>주요 기능</p>
<ul>
<li>TODO 생성: @PostMapping(&quot;/events&quot;)를 통해 새로운 할 일을 생성한다.</li>
<li>TODO 삭제: 특정 ID 또는 날짜의 TODO를 삭제한다.</li>
<li>TODO 조회: 특정 날짜 또는 월의 TODO 리스트를 조회할 수 있다.</li>
<li>TODO 개수 확인: 특정 날짜의 TODO 개수를 반환한다.</li>
</ul>
<pre><code class="language-java">package inho.controller;

import inho.domain.Todo;
import inho.service.TodoService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/calendar&quot;)
public class TodoController {

    private final TodoService todoService;

    /**
     * 새로운 TODO 생성
     */
    @PostMapping(&quot;/events&quot;)
    public ResponseEntity&lt;Todo&gt; createTodo(@RequestBody Todo todo) {
        Todo savedTodo = todoService.saveTodo(todo.getSubject(), todo.getEventAt());
        return ResponseEntity.status(201).body(savedTodo);
    }

    /**
     * 특정 ID의 TODO 삭제
     */
    @DeleteMapping(&quot;/events/{id}&quot;)
    public ResponseEntity&lt;Void&gt; deleteTodoById(@PathVariable Long id) {
        todoService.deleteTodoById(id);
        return ResponseEntity.noContent().build();
    }

    /**
     * 특정 날짜의 모든 TODO 삭제
     */
    @DeleteMapping(&quot;/events/daily/{eventAt}&quot;)
    public ResponseEntity&lt;Void&gt; deleteTodosByDate(@PathVariable String eventAt) {
        todoService.deleteTodosByDate(eventAt);
        return ResponseEntity.noContent().build();
    }

    /**
     * 특정 날짜의 TODO 리스트 조회
     */
    @GetMapping(value = &quot;/events/&quot;, params = {&quot;year&quot;, &quot;month&quot;, &quot;day&quot;})
    public ResponseEntity&lt;List&lt;Todo&gt;&gt; getTodosByDate(
            @RequestParam int year,
            @RequestParam int month,
            @RequestParam int day
    ){
        String eventAt = String.format(&quot;%04d-%02d-%02d&quot;, year, month, day);
        List&lt;Todo&gt; todos = todoService.getTodosByDate(eventAt);
        return ResponseEntity.ok(todos);
    }

    /**
     * 특정 월의 TODO 리스트 조회
     */
    @GetMapping(value = &quot;/events/&quot;, params = {&quot;year&quot;,&quot;month&quot;})
    public ResponseEntity&lt;List&lt;Todo&gt;&gt; getTodosByMonth(
            @RequestParam int year,
            @RequestParam int month
    ){
        String eventMonth = String.format(&quot;%04d-%02d&quot;, year, month);
        List&lt;Todo&gt; todos = todoService.getTodosByMonth(eventMonth);
        return ResponseEntity.ok(todos);
    }
}</code></pre>
<h4 id="🔹-핵심-정리">🔹 핵심 정리</h4>
<ul>
<li>컨트롤러 계층은 클라이언트 요청을 처리하고 응답 반환</li>
<li>@RequestParam을 활용하여 날짜 및 월별 조회 지원</li>
</ul>
<hr>
<h1 id="전반적인-흐름">전반적인 흐름</h1>
<h2 id="1-백엔드-계층-구조">1. 백엔드 계층 구조</h2>
<h3 id="🔹-주요-계층">🔹 주요 계층</h3>
<ul>
<li>컨트롤러 (Controller): 클라이언트 요청을 받아 처리하고 응답을 반환</li>
<li>서비스 (Service): 비즈니스 로직을 수행</li>
<li>리포지토리 (Repository): 데이터베이스와 직접적으로 상호작용</li>
<li>도메인 (Domain/Entity): 데이터 모델 정의</li>
<li>예외 처리 (Exception Handling): 발생하는 예외를 관리하고 적절한 에러 응답을 반환</li>
</ul>
<h2 id="2-주요-기능-흐름">2. 주요 기능 흐름</h2>
<h3 id="🔹-todo-생성">🔹 TODO 생성</h3>
<p>클라이언트가 POST /api/calendar/events 요청을 보냄</p>
<p>TodoController에서 @RequestBody로 요청 데이터를 받음</p>
<p>TodoService에서 비즈니스 로직 수행 (최대 8개 제한 체크 등)</p>
<p>TodoRepository를 통해 데이터베이스에 저장</p>
<p>저장된 데이터를 클라이언트에 반환 (201 Created 응답)</p>
<h3 id="🔹-todo-조회">🔹 TODO 조회</h3>
<h4 id="특정-날짜-조회">특정 날짜 조회</h4>
<p>클라이언트가 GET /api/calendar/events/?year=2024&amp;month=04&amp;day=01 요청</p>
<p>TodoController에서 @RequestParam을 활용해 파라미터를 받음</p>
<p>TodoService에서 findTodos(eventAt, false) 호출</p>
<p>TodoRepository에서 해당 날짜의 데이터를 조회 후 반환</p>
<p>클라이언트에 200 OK 응답과 함께 리스트 반환</p>
<h4 id="특정-월-조회">특정 월 조회</h4>
<p>클라이언트가 GET /api/calendar/events/?year=2024&amp;month=04 요청</p>
<p>TodoService에서 findTodos(eventMonth, true) 호출</p>
<p>TodoRepository에서 해당 월의 데이터를 조회 후 반환</p>
<p>클라이언트에 200 OK 응답과 함께 리스트 반환</p>
<h3 id="🔹-todo-삭제">🔹 TODO 삭제</h3>
<h4 id="특정-id-삭제">특정 ID 삭제</h4>
<p>클라이언트가 DELETE /api/calendar/events/{id} 요청</p>
<p>TodoController에서 @PathVariable Long id로 ID 받음</p>
<p>TodoService에서 deleteTodoById(id) 실행</p>
<p>데이터베이스에서 해당 ID 삭제 후 204 No Content 반환</p>
<h4 id="특정-날짜의-모든-todo-삭제">특정 날짜의 모든 TODO 삭제</h4>
<p>클라이언트가 DELETE /api/calendar/events/daily/{eventAt} 요청</p>
<p>TodoService에서 deleteTodosByDate(eventAt) 실행</p>
<p>데이터베이스에서 해당 날짜의 TODO 삭제 후 204 No Content 반환</p>
<h2 id="3-예외-처리-흐름">3. 예외 처리 흐름</h2>
<h3 id="🔹-예외-발생-시-처리-과정">🔹 예외 발생 시 처리 과정</h3>
<p>MaxTodoLimitExceededException: 하루 최대 8개 제한 초과 시 발생</p>
<p>TodoNotFoundException: 삭제 또는 조회 시 해당 데이터가 없을 경우 발생</p>
<p>예외 발생 시 @ControllerAdvice를 통해 글로벌 예외 처리</p>
<p>적절한 HTTP 상태 코드와 메시지를 포함한 JSON 응답 반환</p>
<h2 id="4-데이터베이스-연동">4. 데이터베이스 연동</h2>
<h3 id="🔹-todorepository">🔹 TodoRepository</h3>
<p>findByEventAt(String eventAt): 특정 날짜의 TODO 조회</p>
<p>findEventAtStartingWith(String eventMonth): 특정 월의 TODO 조회</p>
<p>countByEventAt(String eventAt): 특정 날짜의 TODO 개수 확인</p>
<p>existsById(Long id): 특정 ID 존재 여부 확인</p>
<p>deleteById(Long id): 특정 ID의 TODO 삭제</p>
<h2 id="5-전체적인-요청--응답-흐름">5. 전체적인 요청 &amp; 응답 흐름</h2>
<pre><code>클라이언트 → 컨트롤러 → 서비스 → 리포지토리 → 데이터베이스
           ← 응답 반환 ←</code></pre><p>Spring Boot의 핵심 계층을 활용하여 모듈화된 구조로 개발되었으며, 클린 아키텍처를 유지하면서 유지보수성이 높은 구조를 가진다! </p>
<h2 id="데이터-넣기">데이터 넣기</h2>
<h3 id="front">Front</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/682382a7-13e1-4ff1-a2ed-e42209f8f202/image.png" alt=""></p>
<h3 id="db">DB</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/66d308bd-9a2b-40ef-9c5a-45510af81abf/image.png" alt=""></p>
<h2 id="데이터-삭제">데이터 삭제<img src="https://velog.velcdn.com/images/_inho/post/559d1dd8-4f16-4e9a-be66-0e4e69ee5a48/image.png" alt=""></h2>
<h3 id="front-1">Front</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/3a7e472b-0246-4542-a617-3c5cd58212ef/image.png" alt=""></p>
<h3 id="db-1">DB</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/7fa3d7e9-6b76-407a-be47-6edbb0511edb/image.png" alt=""></p>
<h3 id="날짜별-삭제">날짜별 삭제</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/ed3efda5-ea90-4be9-9bfa-86268c8fe7b4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[📌 TODO API 문서]]></title>
            <link>https://velog.io/@_inho/TODO-API-%EB%AC%B8%EC%84%9C</link>
            <guid>https://velog.io/@_inho/TODO-API-%EB%AC%B8%EC%84%9C</guid>
            <pubDate>Wed, 02 Apr 2025 12:13:54 GMT</pubDate>
            <description><![CDATA[<h2 id="🔐-인증-authentication">🔐 인증 (Authentication)</h2>
<p>모든 요청에는 반드시 아래 헤더를 포함해야 합니다:</p>
<pre><code class="language-http">X-USER-ID: 사용자 식별자 (예: &quot;dusen0528&quot;)
Content-Type: application/json</code></pre>
<hr>
<h2 id="🌍-엔드포인트-endpoints">🌍 엔드포인트 (Endpoints)</h2>
<table>
<thead>
<tr>
<th>HTTP Method</th>
<th>Endpoint</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>POST</strong></td>
<td><code>/api/calendar/events</code></td>
<td>새로운 TODO 생성</td>
</tr>
<tr>
<td><strong>DELETE</strong></td>
<td><code>/api/calendar/events/{id}</code></td>
<td>특정 ID의 TODO 삭제</td>
</tr>
<tr>
<td><strong>DELETE</strong></td>
<td><code>/api/calendar/events/daily/{todoDate}</code></td>
<td>특정 날짜의 모든 TODO 삭제</td>
</tr>
<tr>
<td><strong>GET</strong></td>
<td><code>/api/calendar/events/?year={year}&amp;month={month}&amp;day={day}</code></td>
<td>특정 날짜의 TODO 조회</td>
</tr>
<tr>
<td><strong>GET</strong></td>
<td><code>/api/calendar/daily-register-count?date={todoDate}</code></td>
<td>특정 날짜의 TODO 개수 조회</td>
</tr>
<tr>
<td><strong>GET</strong></td>
<td><code>/api/calendar/events/{id}</code></td>
<td>특정 ID의 TODO 조회</td>
</tr>
<tr>
<td><strong>GET</strong></td>
<td><code>/api/calendar/events/?year={year}&amp;month={month}</code></td>
<td>특정 월의 TODO 리스트 조회</td>
</tr>
</tbody></table>
<hr>
<h2 id="📌-엔드포인트-상세">📌 엔드포인트 상세</h2>
<h3 id="✅-1-새로운-todo-생성">✅ 1. 새로운 TODO 생성</h3>
<ul>
<li><strong>설명</strong>: 새로운 TODO를 생성합니다.</li>
<li><strong>HTTP Method</strong>: <code>POST</code></li>
<li><strong>URL</strong>: <code>/api/calendar/events</code></li>
</ul>
<p><strong>헤더:</strong></p>
<pre><code class="language-http">Content-Type: application/json
X-USER-ID: 사용자 ID</code></pre>
<p><strong>요청 본문 (Request Body):</strong></p>
<pre><code class="language-json">{
  &quot;subject&quot;: &quot;todo 제목&quot;,
  &quot;eventAt&quot;: &quot;2025-04-02&quot;
}</code></pre>
<p><strong>응답 코드:</strong></p>
<ul>
<li><code>201 Created</code>: 성공적으로 생성됨 ✅</li>
<li><code>400 Bad Request</code>: 요청 데이터가 잘못됨 ⚠️</li>
<li><code>403 Forbidden</code>: 하루 최대 TODO 개수 초과 ❌</li>
</ul>
<p><strong>응답 예시:</strong></p>
<pre><code class="language-json">{
  &quot;id&quot;: 123,
  &quot;subject&quot;: &quot;todo 제목&quot;,
  &quot;eventAt&quot;: &quot;2025-04-02&quot;,
  &quot;createdAt&quot;: &quot;2025-04-02T12:00:00Z&quot;
}</code></pre>
<hr>
<h3 id="❌-2-특정-id의-todo-삭제">❌ 2. 특정 ID의 TODO 삭제</h3>
<ul>
<li><strong>설명</strong>: 특정 ID에 해당하는 TODO를 삭제합니다.</li>
<li><strong>HTTP Method</strong>: <code>DELETE</code></li>
<li><strong>URL</strong>: <code>/api/calendar/events/{id}</code></li>
</ul>
<p><strong>헤더:</strong></p>
<pre><code class="language-http">X-USER-ID: 사용자 ID</code></pre>
<p><strong>응답 코드:</strong></p>
<ul>
<li><code>204 No Content</code>: 성공적으로 삭제됨 ✅</li>
<li><code>404 Not Found</code>: 해당 ID가 존재하지 않음 ⚠️</li>
</ul>
<p><strong>응답 예시:</strong></p>
<pre><code class="language-json">{
  &quot;message&quot;: &quot;삭제 완료&quot;
}</code></pre>
<hr>
<h3 id="🗑-3-특정-날짜의-모든-todo-삭제">🗑 3. 특정 날짜의 모든 TODO 삭제</h3>
<ul>
<li><strong>설명</strong>: 지정된 날짜의 모든 TODO를 삭제합니다.</li>
<li><strong>HTTP Method</strong>: <code>DELETE</code></li>
<li><strong>URL</strong>: <code>/api/calendar/events/daily/{todoDate}</code></li>
</ul>
<p><strong>헤더:</strong></p>
<pre><code class="language-http">X-USER-ID: 사용자 ID</code></pre>
<p><strong>응답 코드:</strong></p>
<ul>
<li><code>204 No Content</code>: 성공적으로 삭제됨 ✅</li>
<li><code>404 Not Found</code>: 해당 날짜에 등록된 TODO가 없음 ⚠️</li>
</ul>
<hr>
<h3 id="📅-4-특정-날짜의-todo-조회">📅 4. 특정 날짜의 TODO 조회</h3>
<ul>
<li><strong>설명</strong>: 지정된 날짜에 등록된 모든 TODO를 조회합니다.</li>
<li><strong>HTTP Method</strong>: <code>GET</code></li>
<li><strong>URL</strong>: <code>/api/calendar/events/?year={year}&amp;month={month}&amp;day={day}</code></li>
</ul>
<p><strong>헤더:</strong></p>
<pre><code class="language-http">X-USER-ID: 사용자 ID</code></pre>
<p><strong>응답 코드:</strong></p>
<ul>
<li><code>200 OK</code>: 요청 성공 ✅</li>
<li><code>404 Not Found</code>: 해당 날짜에 등록된 TODO가 없음 ⚠️</li>
</ul>
<p><strong>응답 예시:</strong></p>
<pre><code class="language-json">[
  {
    &quot;id&quot;: 123,
    &quot;subject&quot;: &quot;HTML 공부하기&quot;,
    &quot;eventAt&quot;: &quot;2025-04-02&quot;,
    &quot;createdAt&quot;: &quot;2025-04-01T12:00:00Z&quot;
  },
  {
    &quot;id&quot;: 124,
    &quot;subject&quot;: &quot;JavaScript 복습하기&quot;,
    &quot;eventAt&quot;: &quot;2025-04-02&quot;,
    &quot;createdAt&quot;: &quot;2025-04-01T13:00:00Z&quot;
  }
]</code></pre>
<hr>
<h3 id="🔢-5-특정-날짜의-todo-개수-조회">🔢 5. 특정 날짜의 TODO 개수 조회</h3>
<ul>
<li><strong>설명</strong>: 지정된 날짜에 등록된 TODO 개수를 반환합니다.</li>
<li><strong>HTTP Method</strong>: <code>GET</code></li>
<li><strong>URL</strong>: <code>/api/calendar/daily-register-count?date={todoDate}</code></li>
</ul>
<p><strong>헤더:</strong></p>
<pre><code class="language-http">X-USER-ID: 사용자 ID</code></pre>
<p><strong>응답 코드:</strong></p>
<ul>
<li><code>200 OK</code>: 요청 성공 ✅</li>
<li><code>404 Not Found</code>: 해당 날짜에 등록된 TODO가 없음 ⚠️</li>
</ul>
<p><strong>응답 예시:</strong></p>
<pre><code class="language-json">{
  &quot;count&quot;: 5
}</code></pre>
<hr>
<h3 id="🔍-6-특정-id의-todo-조회">🔍 6. 특정 ID의 TODO 조회</h3>
<ul>
<li><strong>설명</strong>: 특정 ID에 해당하는 단일 TODO를 조회합니다.</li>
<li><strong>HTTP Method</strong>: <code>GET</code></li>
<li><strong>URL</strong>: <code>/api/calendar/events/{id}</code></li>
</ul>
<p><strong>헤더:</strong></p>
<pre><code class="language-http">X-USER-ID: 사용자 ID</code></pre>
<p><strong>응답 코드:</strong></p>
<ul>
<li><code>200 OK</code>: 요청 성공 ✅</li>
<li><code>404 Not Found</code>: 해당 ID가 존재하지 않음 ⚠️</li>
</ul>
<p><strong>응답 예시:</strong></p>
<pre><code class="language-json">{
  &quot;id&quot;: 123,
  &quot;subject&quot;: &quot;HTML 공부하기&quot;,
  &quot;eventAt&quot;: &quot;2025-04-02&quot;,
  &quot;createdAt&quot;: &quot;2025-04-01T12:00:00Z&quot;
}</code></pre>
<hr>
<h3 id="📆-7-월-단위로-todo-리스트-조회">📆 7. 월 단위로 TODO 리스트 조회</h3>
<ul>
<li><strong>설명</strong>: 지정된 연도와 월에 등록된 모든 TODO를 반환합니다.</li>
<li><strong>HTTP Method</strong>: <code>GET</code></li>
<li><strong>URL</strong>: <code>/api/calendar/events/?year={year}&amp;month={month}</code></li>
</ul>
<p><strong>헤더:</strong></p>
<pre><code class="language-http">X-USER-ID: 사용자 ID</code></pre>
<p><strong>응답 코드:</strong></p>
<ul>
<li><code>200 OK</code>: 요청 성공 ✅</li>
<li><code>404 Not Found</code>: 해당 월에 등록된 TODO가 없음 ⚠️</li>
</ul>
<p><strong>응답 예시:</strong></p>
<pre><code class="language-json">[
  {
    &quot;id&quot;: 123,
    &quot;subject&quot;: &quot;HTML 공부하기&quot;,
    &quot;eventAt&quot;: &quot;2025-04-02&quot;,
    &quot;createdAt&quot;: &quot;2025-04-01T12:00:00Z&quot;
  },
  {
    &quot;id&quot;: 124,
    &quot;subject&quot;: &quot;JavaScript 복습하기&quot;,
    &quot;eventAt&quot;: &quot;2025-04-03&quot;,
    &quot;createdAt&quot;: &quot;2025-04-01T13:00:00Z&quot;
  }
]</code></pre>
<hr>
<h2 id="🚨-에러-처리-error-handling">🚨 에러 처리 (Error Handling)</h2>
<table>
<thead>
<tr>
<th>HTTP 상태 코드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>200 OK</code></td>
<td>요청이 성공적으로 처리됨</td>
</tr>
<tr>
<td><code>201 Created</code></td>
<td>리소스가 성공적으로 생성됨</td>
</tr>
<tr>
<td><code>204 No Content</code></td>
<td>요청 성공, 응답 본문 없음</td>
</tr>
<tr>
<td><code>400 Bad Request</code></td>
<td>잘못된 요청 데이터 ⚠️</td>
</tr>
<tr>
<td><code>403 Forbidden</code></td>
<td>하루 최대 할 일 개수 초과 ❌</td>
</tr>
<tr>
<td><code>404 Not Found</code></td>
<td>리소스를 찾을 수 없음 🚫</td>
</tr>
</tbody></table>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis&RabbitMQ 통합 JWT 인증 시스템]]></title>
            <link>https://velog.io/@_inho/RedisRabbitMQ-%ED%86%B5%ED%95%A9-JWT-%EC%9D%B8%EC%A6%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@_inho/RedisRabbitMQ-%ED%86%B5%ED%95%A9-JWT-%EC%9D%B8%EC%A6%9D-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Mon, 31 Mar 2025 07:58:34 GMT</pubDate>
            <description><![CDATA[<h1 id="jwt-인증-시스템">JWT 인증 시스템?</h1>
<blockquote>
<p>사용자가 로그인할 때 발급되는 토큰
이 토큰은 사용자의 정보를 담고 있어서, 사용자가 API 호출할 때 매번 &quot;나는 이 사람이 맞습니다!&quot;라고 증명하는데 사용됨
이 JWT에는 짧은 시간 동안만 유효하고, 만료시 다시 로그인하거나 Refresh Token을 이용해 새로운 AccessToken이 필요</p>
</blockquote>
<h2 id="🚨-jwt의-문제점">🚨 JWT의 문제점?</h2>
<h3 id="로그아웃한-사용자의-jwt를-어떻게-무효화할까">로그아웃한 사용자의 JWT를 어떻게 무효화할까?</h3>
<p>-&gt; 기본적으로 JWT는 자체적으로 만료되기 전까지 유효함
근데 사용자가 로그아웃했거나, 보안상의 이유로 강제 로그아웃을 해야한다면 어떻게 처리할까?</p>
<blockquote>
<h2 id="그냥-토큰-지워버리면-되는거-아닌가요">그냥 토큰 지워버리면 되는거 아닌가요?</h2>
<p>JWT는 클라이언트 측에 저장되기 때문에 서버가 JWT를 직접 삭제하거나 만료시킬 방법은 존재하지 않음</p>
</blockquote>
<h4 id="💡-세션기반-인증방식과-비교해본다면">💡 세션기반 인증방식과 비교해본다면?</h4>
<ul>
<li>세션은 서버에서 관리되므로 사용자가 로그아웃하면 서버에서 세션을 삭제할 수 있음. 하지만 서버 메모리 혹은 DB에 작업해야하므로 서버마다 세션을 저장해야해서 확장성이 떨어짐</li>
</ul>
<h3 id="토큰을-분산-시스템에서-공유해야-할-때-어떻게-동기화-하지">토큰을 분산 시스템에서 공유해야 할 때 어떻게 동기화 하지?</h3>
<p>-&gt; 여러 서비스에서 인증을 공유할 때, 한 서비스에서 로그아웃 시 다른 서비스에서도 즉시 반영되어야 함</p>
<hr>
<h1 id="1-redis란">1. Redis란?</h1>
<p><img src="https://velog.velcdn.com/images/_inho/post/ca997145-0453-4156-8322-f697903857f0/image.png" alt=""></p>
<h2 id="11-redis의-역할">1.1 Redis의 역할</h2>
<h3 id="토큰-블랙리스트와-refresh-token-저장">토큰 블랙리스트와 Refresh Token 저장</h3>
<ul>
<li>Redis는 데이터를 메모리에 저장해서 엄청 빠르게 읽고 쓸 수 있다.</li>
<li>로그아웃된 JWT를 기억하고 RefreshToken을 안전하게 저장하기 위해서 사용</li>
</ul>
<h3 id="블랙리스트-관리로그아웃된-jwt저장">블랙리스트 관리(로그아웃된 JWT저장)</h3>
<ul>
<li>사용자가 로그아웃하면 JWT를 Redis에 넣고 &quot;이 토큰은 더 이상 사용 불가!&quot;로 저장</li>
<li>API 호출시마다 Redis를 확인해서, 블랙리스트에 있는 토큰이면 거부</li>
</ul>
<h3 id="refresh-token-저장">Refresh Token 저장</h3>
<ul>
<li>Access Token은 짧은 시간이 지나면 만료되는데</li>
<li>우리가 사용하는 SNS 인스타를 예시로 보면 1시간마다 로그아웃되면 사용자 경험이 현저히 낮아질 것</li>
<li>이때 활용되는게 Refresh Token인데, 사용자가 다시 로그인 하지 않도록 이 토큰을 발급해서 새로운 Access Token을 발급해준다.</li>
<li>단, 금융 서비스와 같이 보안에 민감한 부분에는 짧은 토큰 만료기간을 설정한 후 만료시, 다시 로그인 하도록 하며 SNS와 같이 사용자 경험이 중요한 서비스의 경우 리프레쉬 토큰 만료 기간을 길게 잡아서 편리한 서비스로 만듬</li>
</ul>
<h3 id="확장성">확장성</h3>
<ul>
<li>서버가 여러개여도 Redis가 중앙에서 JWT 블랙리스트를 관리하면 문제 없음</li>
</ul>
<h3 id="휘발성">휘발성</h3>
<ul>
<li>다만 휘발성이 있기에, 서버가 꺼지면 데이터가 사라질 수 있음 </li>
<li>따라서 캐시로 많이 사용됨</li>
</ul>
<h2 id="12-redis-대표적인-기능">1.2 Redis 대표적인 기능</h2>
<ul>
<li>Key-Value 저장소 : &quot;user:123&quot; → {&quot;name&quot;: &quot;Alice&quot;, &quot;age&quot;: 25} 처럼 저장 가능</li>
<li>캐싱 : 자주 쓰는 데이터를 저장해서 DB 부하를 줄임</li>
<li>토큰 저장 : RefreshToken 저장, JWT 블랙리스트 관리</li>
</ul>
<hr>
<h1 id="2-rabbitmq란">2. RabbitMQ란?</h1>
<p><img src="https://velog.velcdn.com/images/_inho/post/ca45103b-675e-49c6-9ff2-84fa8df5f165/image.png" alt=""></p>
<h2 id="2-1-rabbitmq의-역할">2-1. RabbitMQ의 역할</h2>
<blockquote>
<p>각각의 서비스가 독립적으로 동작하면서도, 서로 데이터를 주고 받을 수 있도록 도와주는 메시지 중계소</p>
</blockquote>
<h4 id="💡-쉽게-말하면">💡 쉽게 말하면?</h4>
<ul>
<li>auth 서비스가 blog 서비스로 회원가입 할거야! 라고 메세지를 보내고 싶을 때 RabbitMQ를 거쳐 전달</li>
<li>두 서비스가 동시에 연결될 필요 없이 비동기적으로 통신 가능</li>
</ul>
<h2 id="msa-구조에서-서비스들끼리-데이터-교환시-사용">MSA 구조에서 서비스들끼리 데이터 교환시 사용</h2>
<hr>
<h1 id="🚀-결론적으로">🚀 결론적으로</h1>
<ul>
<li>JWT 인증시 Redis를 통해 빠른 데이터 조회(JWT 블랙리스트, Refresh Token 저장)</li>
<li>분산 시스템에서도 중앙화된 Redis 메모리를 통해 인증 정보 공유 가능</li>
<li>로그아웃 같은 이벤트를 여러 서비스에 동기화 하기 위해 Rabbit MQ</li>
<li>서비스간 독립적인 메세지 전달을 지원하는 장점들을 활용해서</li>
</ul>
<p>JWT 인증을 구현한다! </p>
<hr>
<h1 id="step-1--기술-스택-선정">Step 1 : 기술 스택 선정</h1>
<ul>
<li><p>JWT: 인증 토큰 발급/검증</p>
</li>
<li><p>Redis: 토큰 블랙리스트 관리, Refresh Token 저장</p>
</li>
<li><p>RabbitMQ: 분산 시스템 간 이벤트 통신 (로그아웃 알림 등)</p>
</li>
<li><p>Spring Cloud Gateway: API Gateway 역할</p>
</li>
<li><p>Spring Security: 인증/인가 처리</p>
</li>
<li><p>Eureka: 서비스 디스커버리</p>
</li>
</ul>
<h1 id="step-2--아키텍처">Step 2 : 아키텍처</h1>
<pre><code>[Client]
  │
  ▼ HTTPS
[API Gateway] ───▶ [Auth Service] ↔ Redis (Token Storage)
  │                   │
  │                   ▼
  └──▶ [Blog Service] ←─ RabbitMQ (Logout Events)</code></pre><h1 id="step-3--redis-통합-토큰-관리">Step 3 : Redis 통합 토큰 관리</h1>
<ul>
<li>Gateway에서 모든 클라이언트 요청을 필터링하며 JWT 검증을 수행</li>
<li>블랙리스트에 등록된 토큰은 요청을 차단!</li>
<li>TokenRepository 사용해서 블랙리스트와  Refresh Token 관리</li>
</ul>
<pre><code>brew install redis
redis-server --version</code></pre><p><img src="https://velog.velcdn.com/images/_inho/post/53d5f5ea-4189-4771-a1b6-f6948cfe7102/image.png" alt="">
위와 같이 설치하기 </p>
<h2 id="redis-명령어">Redis 명령어</h2>
<h3 id="1-foreground로-실행">1. Foreground로 실행</h3>
<pre><code>redis-server</code></pre><h3 id="2-background로-실행">2. Background로 실행</h3>
<pre><code>brew services start redis
brew services stop redis</code></pre><ul>
<li>redis 테스트<pre><code>redis-cli ping</code></pre><h4 id="redis는-기본적으로-포트-6379에서-작동">Redis는 기본적으로 포트 6379에서 작동</h4>
</li>
</ul>
<h2 id="step-3-1--redis-설정">Step 3-1 : Redis 설정</h2>
<h3 id="pomxml">pom.xml</h3>
<pre><code>&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre><ul>
<li>redis 의존성 추가<h3 id="applicationproperties">application.properties</h3>
<pre><code>spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=1234 </code></pre></li>
<li>레디스 정보 설정</li>
</ul>
<h2 id="step-3-2--redisconfigclass">Step 3-2 : RedisConfig.class</h2>
<pre><code class="language-java">@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(); // Lettuce는 비동기 방식으로 높은 성능 제공
    }

    @Bean
    public RedisTemplate&lt;String, String&gt; redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate&lt;String, String&gt; redisTemplate = new RedisTemplate&lt;&gt;();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // Key를 문자열로 직렬화
        redisTemplate.setValueSerializer(new StringRedisSerializer()); // Value를 문자열로 직렬화
        return redisTemplate;
    }
}</code></pre>
<ul>
<li><strong>RedisConnectionFactory</strong>는 레디스 서버와 연결을 관리하는 객체</li>
<li>Lettuce는 레디스와 통신할 때 비동기 방식으로 동작해 높은 성능을 제공</li>
</ul>
<ul>
<li><strong>RedisTemplate</strong>는 레디스에 데이터를 넣고 가져오는 데 사용하는 주요 도구<ol>
<li>connectionFactory: 레디스 서버와 연결을 관리하는 객체를 연결</li>
<li>KeySerializer: 키를 문자열로 변환(직렬화)</li>
<li>ValueSerializer: 값을 문자열로 변환(직렬화)</li>
</ol>
</li>
</ul>
<blockquote>
<h3 id="💡-왜-직렬화가-필요할까">💡 왜 직렬화가 필요할까?</h3>
<p>레디스는 데이터를 네트워크를 통해 주고 받는데, 네트워크에서 데이터를 보내려면 객체를 문자열 혹은 바이트 형태로 바꿔야한다.
이 과정을 <strong>직렬화</strong>라고 한다.</p>
</blockquote>
<ul>
<li>RdisConfig는 실행 시 레디스 서버와 연결을 설정</li>
<li>RedisTemplate를 통해 레디스에 데이터를 저장하거나 가져올 수 있음<ul>
<li>&quot;user:1&quot;라는 키에 &quot;Inho&quot;라는 값 저장하거나 가져오기</li>
</ul>
</li>
</ul>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-java">@Autowired
private RedisTemplate&lt;String, String&gt; redisTemplate;

public void useRedis() {
    // 데이터 저장
    redisTemplate.opsForValue().set(&quot;user:1&quot;, &quot;John&quot;);

    // 데이터 가져오기
    String value = redisTemplate.opsForValue().get(&quot;user:1&quot;);
    System.out.println(&quot;user:1의 값은 &quot; + value); // 출력: user:1의 값은 Inho
}</code></pre>
<ul>
<li>데이터를 넣고 가져오는 간단한 예시</li>
</ul>
<h2 id="step-3-3--토큰-블랙리스트--리프레쉬-토큰-관리-클래스">Step 3-3 : 토큰 블랙리스트 &amp; 리프레쉬 토큰 관리 클래스</h2>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class TokenRepository {

    private final RedisTemplate&lt;String, String&gt; redisTemplate;

    // 블랙리스트에 토큰 추가 (만료 시간 설정)
    public void addToBlacklist(String token, long expiration) {
        redisTemplate.opsForValue().set(token, &quot;blacklisted&quot;, expiration, TimeUnit.MILLISECONDS);
    }

    // 블랙리스트 여부 확인
    public boolean isBlacklisted(String token) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(token));
    }

    // Refresh Token 저장 (유저 ID 기준)
    public void storeRefreshToken(String userId, String refreshToken) {
        redisTemplate.opsForValue().set(userId, refreshToken, 7, TimeUnit.DAYS);
    }

    // Refresh Token 조회 (유저 ID 기준)
    public String getRefreshToken(String userId) {
        return redisTemplate.opsForValue().get(userId);
    }
}</code></pre>
<h3 id="메소드-1--블랙리스트에-토큰-추가">메소드 1 : 블랙리스트에 토큰 추가</h3>
<pre><code class="language-java"> // 블랙리스트에 토큰 추가 (만료 시간 설정)
    public void addToBlacklist(String token, long expiration) {
        redisTemplate.opsForValue().set(token, &quot;blacklisted&quot;, expiration, TimeUnit.MILLISECONDS);
    }</code></pre>
<ul>
<li>특정 JWT 토큰을 블랙리스트에 추가</li>
<li>Redis에 토큰을 키로 저장 후 값은 blacklisted로 설정</li>
<li>만료시간을 설정해서 밀리초 단위로 설정해서 자동으로 삭제</li>
</ul>
<blockquote>
<h3 id="opsforvalue-메소드">opsForValue() 메소드</h3>
<p><strong>1. 값 저장</strong></p>
</blockquote>
<pre><code class="language-java">redisTemplate.opsForValue().set(&quot;key&quot;, &quot;value&quot;);</code></pre>
<p>Redis에 &quot;key&quot;라는 이름으로 &quot;value&quot;를 저장
기본적으로 만료 시간이 설정되지 않으며, 값은 영구적으로 저장
<strong>2. 값 조회</strong></p>
<pre><code class="language-java">String value = redisTemplate.opsForValue().get(&quot;key&quot;);</code></pre>
<p>Redis에서 &quot;key&quot;에 해당하는 값을 가져옴
값이 없으면 null을 반환
<strong>3. 값 삭제</strong></p>
<pre><code class="language-java">redisTemplate.delete(&quot;key&quot;);</code></pre>
<p>특정 키에 저장된 데이터를 삭제
<strong>4. 값 저장 (만료 시간 설정)</strong></p>
<pre><code class="language-java">redisTemplate.opsForValue().set(&quot;key&quot;, &quot;value&quot;, 60, TimeUnit.SECONDS);</code></pre>
<p>&quot;key&quot;에 &quot;value&quot;를 저장하고, 만료 시간을 60초로 설정
만료 시간이 지나면 Redis는 해당 데이터를 자동으로 삭제
<strong>5. 키 존재 여부 확인</strong></p>
<pre><code class="language-java">boolean exists = redisTemplate.hasKey(&quot;key&quot;);</code></pre>
<p>특정 키가 Redis에 존재하는지 확인
존재하면 true, 없으면 false를 반환</p>
<h3 id="메소드-2--블랙리스트-여부-확인">메소드 2 : 블랙리스트 여부 확인</h3>
<pre><code class="language-java"> // 블랙리스트 여부 확인 
    public boolean isBlacklisted(String token){
        return Boolean.TRUE.equals(redisTemplate.hasKey(token));
    }
</code></pre>
<ul>
<li>특정 토큰이 블랙리스트에 등록되어 있는지 확인</li>
<li>해당 토큰이 Redis에서 키로 존재하는지 검사</li>
</ul>
<h3 id="메소드-3--리프레쉬-토큰-저장">메소드 3 : 리프레쉬 토큰 저장</h3>
<pre><code class="language-java">    // 리프레쉬 토큰 저장 
    public void storeRefreshToken(String userId, String refreshToken){
        redisTemplate.opsForValue().set(userId, refreshToken, 7, TimeUnit.DAYS);
    }</code></pre>
<ul>
<li>특정 사용자ID를 기준으로 RefreshToken을 저장</li>
<li>리프레쉬 토큰은 7일동안 유지되며, Redis TTL 기능으로 자동 삭제 됨</li>
</ul>
<h3 id="메소드-4--리프레쉬-토큰-조회">메소드 4 : 리프레쉬 토큰 조회</h3>
<pre><code class="language-java">// 리프레쉬 토큰 조회 
    public String getRefreshToken(String userId){
        return redisTemplate.opsForValue().get(userId);
    }</code></pre>
<ul>
<li>특정 사용자 ID를 기준으로 리프레쉬 토큰 조회</li>
</ul>
<h2 id="전체-동작-흐름">전체 동작 흐름</h2>
<h3 id="1-로그아웃-처리블랙리스트">1. 로그아웃 처리(블랙리스트)</h3>
<ul>
<li>서버가 로그아웃 요청을 받으면 해당 JWT를 addToBlacklist 메소드로 블랙리스트에 추가</li>
<li>이후 인증 요청 시 isBlackliste 메소드로 JWT가 블랙리스트에 있는지 확인하고 요청 차단</li>
</ul>
<h3 id="2-리프레쉬-토큰-관리">2. 리프레쉬 토큰 관리</h3>
<ul>
<li>사용자가 로그인시 storeRefreshToekn 메소드로 리프레쉬 토큰 저장</li>
<li>클라이언트가 새로운 액세스 토큰 요청 시 리프레쉬 토큰 조회 후 검증 </li>
</ul>
<hr>
<p>JWT 검증 필터 구현은 다음 게시글로 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Eureka & Gateway 설정]]></title>
            <link>https://velog.io/@_inho/Eureka-Gateway-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@_inho/Eureka-Gateway-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 31 Mar 2025 05:45:09 GMT</pubDate>
            <description><![CDATA[<h1 id="1-eureka란">1. Eureka란?</h1>
<blockquote>
<p>&quot;모든 서비스가 자신의 위치(IP, Port)를 등록하고 다른 서비스가 찾을 수 있게 해주는 중앙 등록소&quot;</p>
</blockquote>
<ul>
<li>넷플릭스가 개발하고 Spring Cloud에 기여한 오픈소스 서비스 디스커버리 도구</li>
<li>마이크로서비스 환경에서 서비스 인스턴스의 위치 정보를 중앙 집중식으로 관리하며, 동적 서비스 탐색과 상태 모니터링 기능을 제공</li>
</ul>
<h2 id="11-아키텍처-구성">1.1 아키텍처 구성</h2>
<pre><code>[Eureka Server Cluster]
      ⇅      ⇅
[Service A Instance]  
      ⇅      ⇅
[Service B Instance]</code></pre><h2 id="12-설정-파일">1.2 설정 파일</h2>
<h4 id="applicationproperties">application.properties</h4>
<pre><code>server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=10s

spring.application.name=eureka-server

# 스프링 시큐리티 기본 인증 설정
spring.security.user.name=admin
spring.security.user.password=1234


# 활성화할 프로필 설정
spring.profiles.active=peer1

# 유레카 클라이언트 설정
# 자신을 유레카 서버에 등록하지 않음 (서버 모드)
eureka.client.register-with-eureka=false

# 레지스트리 정보를 로컬에 캐싱하지 않음 (서버 모드)
eureka.client.fetch-registry=false


# 유레카 서버 설정
# 서비스 등록 정보 캐시 갱신 주기 (밀리초)
eureka.server.response-cache-update-interval-ms=5000
# 등록된 서비스 중 비정상 서비스 제거 주기 (밀리초)
eureka.server.eviction-interval-timer-in-ms=5000</code></pre><ul>
<li>graceful : 서버 종료 시 진행 중인 요청을 처리한 후 종료하는 그레이스풀 셧다운 방식을 사용</li>
<li>그레이스풀 셧다운시 최대 10초까지 대기</li>
<li>서버 실행될 포트</li>
</ul>
<h4 id="application-peer1properties--peer2">application-peer1.properties &amp; peer2</h4>
<p>peer 1</p>
<pre><code># 서버 포트 설정
server.port=8761
# 유레카 인스턴스 호스트명
eureka.instance.hostname=peer1
# 유레카 서버 URL 설정 (클러스터링)
eureka.client.service-url.defaultZone=http://admin:1234@peer2:8762/eureka/
# 자신을 유레카 서버에 등록 (클러스터링 모드)
eureka.client.register-with-eureka=true
# 레지스트리 정보를 로컬에 캐싱 (클러스터링 모드)
eureka.client.fetch-registry=true</code></pre><p>peer2</p>
<pre><code># 서버 포트 설정
server.port=8762
# 유레카 인스턴스 호스트명
eureka.instance.hostname=peer2
# 유레카 서버 URL 설정 (클러스터링)
eureka.client.service-url.defaultZone=http://admin:1234@peer1:8761/eureka/
# 자신을 유레카 서버에 등록 (클러스터링 모드)
eureka.client.register-with-eureka=true
# 레지스트리 정보를 로컬에 캐싱 (클러스터링 모드)
eureka.client.fetch-registry=true</code></pre><ul>
<li>peer1 &amp; peer2 2개의 유레카 서버가 서로 연결되어 서비스 레지스트리 정보를 동기화</li>
<li>이를 통해 peer1이 다운되더라도 peer2가 서비스 디스커버리 기능을 계속해서 제공</li>
<li>30초 간격으로 서로 레지스트리 정보를 복제</li>
<li>모든 유레카 서버는 동일한 서비스 인스턴스 정보를 유지</li>
</ul>
<h3 id="클라이언트-측-로드-밸런싱">클라이언트 측 로드 밸런싱</h3>
<ul>
<li>클라이언는 두개의 유레카 서버를 설정 파일에서 지정<pre><code>eureka.client.service-url.defaultZone=http://admin:1234@localhost:8761/eureka,http://admin:1234@localhost:8762/eureka</code></pre>클라이언트는 이 목록에서 하나의 서버를 선택해서 요청을 보냄</li>
<li>기본적으로 라운드 로빈 방식으로 유레카 서버를 선택</li>
</ul>
<pre><code>첫 번째 요청 → Peer1 (http://localhost:8761)

두 번째 요청 → Peer2 (http://localhost:8762)

세 번째 요청 → 다시 Peer1

---
Peer1이 다운된 경우 → Peer2로 요청 전환

Peer2도 다운된 경우 → 서비스 디스커버리 실패</code></pre><p>이 방식은 클라이언트와 유레카 서버 간 트래픽을 분산시켜 부하를 줄이는 데 도움을 줌</p>
<h2 id="13-eureka---securityconfig">1.3 Eureka - SecurityConfig</h2>
<pre><code class="language-java">@EnableWebSecurity(debug = false)
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);
        http.authorizeHttpRequests(authorizeRequests -&gt;
            authorizeRequests.anyRequest().authenticated()
        );

        http.httpBasic(Customizer.withDefaults());
        return http.build();
    }
}</code></pre>
<ul>
<li>유레카 서버는 서버-서버간 통신에 사용되기 때문에 불필요한 보안 계층인 CSRF 보호를 비활성화</li>
<li>Basic Auth로 최소한의 보안 유지</li>
</ul>
<hr>
<h1 id="2-gateway란">2. Gateway란?</h1>
<pre><code>모든 요청의 단일 입구. 적절한 서비스로 안내하고, 보안 검사도 담당</code></pre><h2 id="2-1-아키텍처-구성">2-1. 아키텍처 구성</h2>
<pre><code>[Client] 
  ↓ HTTPS
[API Gateway] : 모든 요청의 단일 진입점
  ⇅ 
[Eureka Server]
  ⇅ 
[Microservices]</code></pre><h2 id="2-2-설정-파일">2-2. 설정 파일</h2>
<pre><code># 서버 포트 설정
server.port=8080

# 애플리케이션 이름 설정 (유레카에 등록될 서비스 이름)
spring.application.name=api-gateway

# 서비스 디스커버리 활성화 (유레카를 통한 서비스 자동 발견)
spring.cloud.gateway.discovery.locator.enabled=true
# 서비스 ID를 소문자로 변환 (URL 경로에서 대소문자 구분 없이 사용)
spring.cloud.gateway.discovery.locator.lower-case-service-id=true

# 유레카 서버 주소 설정 (클러스터링된 두 서버 모두 등록)
eureka.client.service-url.defaultZone=http://admin:1234@localhost:8761/eureka,http://admin:1234@localhost:8762/eureka
# IP 주소 사용 (호스트명 대신)
eureka.instance.prefer-ip-address=true

# 로깅 레벨 설정 (게이트웨이 디버깅용)
logging.level.org.springframework.cloud.gateway=DEBUG
logging.level.reactor.netty=DEBUG</code></pre><ul>
<li>서비스 이름 소문자로 통일</li>
<li>커넥션 풀 유휴 시간 기본값 45초</li>
</ul>
<h2 id="2-3-routelocatorconfig">2-3. RouteLocatorConfig</h2>
<pre><code class="language-java">@Configuration
public class RouteLocatorConfig {

    @Bean
    public RouteLocator myRoute(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(&quot;auth-service&quot;, r -&gt; r
                        .path(&quot;/auth/**&quot;)
                        .uri(&quot;lb://AUTH-SERVICE&quot;))
                .route(&quot;blog-service&quot;, r -&gt; r
                        .path(&quot;/blog/**&quot;)
                        .uri(&quot;lb://BLOG-SERVICE&quot;))
                .build();

    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/_inho/post/0ddaea2f-ed7e-4185-abb1-163e9796d6ea/image.png" alt=""></p>
<ul>
<li>/auth로 시작하는 모든 경로에 대한 요청은 AUTH-SERVICE를 유레카 서비스 목록에서 찾아 로드밸런싱을 통해 하나의 인스턴스로 요청 전달</li>
</ul>
<p><strong>예시</strong></p>
<blockquote>
<p>GET /auth/login → AUTH-SERVICE/auth/login
POST /blog/posts → BLOG-SERVICE/blog/posts</p>
</blockquote>
<h2 id="2-4-동작흐름">2-4. 동작흐름</h2>
<blockquote>
<ol>
<li><strong>클라이언트 요청</strong> : GET <a href="http://localhost:8080/auth/login">http://localhost:8080/auth/login</a></li>
<li><strong>게이트웨이 라우팅</strong> : /auth/** 경로 매칭 -&gt; AUTH-SERVICE로 라우팅 결정</li>
<li><strong>Eureka 조회</strong> : 유레카 서버에서 AUTH-SERVICE의 등록된 인스턴스 목록을 가져옴</li>
<li><strong>로드 밸런싱</strong> : 인스턴스중 하나를 선택 (예: <a href="http://auth-service-instance:8081">http://auth-service-instance:8081</a>)</li>
<li>** 요청 전달** : 선택된 인스턴스의 /auth/login 엔드포인트로 요청 전달</li>
</ol>
</blockquote>
<h2 id="2-5-대시보드">2-5. 대시보드</h2>
<p>유레카 서버 <a href="http://localhost:8761">http://localhost:8761</a> 에 접속하면 등록된 서비스들이 확인할 수 있다.</p>
<hr>
<h1 id="전체-동작-아키텍처">전체 동작 아키텍처</h1>
<pre><code>[Client] 
  │
  ▼ 
[API Gateway] 
  ├──▶ [Auth Service] /auth/** (JWT 검증 X)
  └──▶ [Blog Service] /blog/** (JWT 검증 O)
        ▲
        │
[Service-to-Service Communication]
        │
        ▼
    [Eureka Server]</code></pre><h3 id="1️⃣-서비스-간-통신-방식">1️⃣ 서비스 간 통신 방식</h3>
<h4 id="1-클라이언트-서버-통신">(1) 클라이언트&lt;-&gt;서버 통신</h4>
<p>모든 클라이언트 요청은 반드시 API Gateway를 통과 
<img src="https://velog.velcdn.com/images/_inho/post/1215f431-b8bf-4b66-b691-bf7ab03fc0e3/image.png" alt=""></p>
<h4 id="2-서버-서버-통신">(2) 서버&lt;-&gt;서버 통신</h4>
<p>Eureka를 통한 직접 통신 (Gateway 경유하지 않음)
<img src="https://velog.velcdn.com/images/_inho/post/306a2037-dcff-4f29-8637-72f2eadce991/image.png" alt=""></p>
<h4 id="컴포넌트간-역할-분리">컴포넌트간 역할 분리</h4>
<p><img src="https://velog.velcdn.com/images/_inho/post/2f929caf-2976-4eb9-8c57-342377a9679d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프론트엔드에서 JWT 토큰 인증을 구현하기]]></title>
            <link>https://velog.io/@_inho/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-JWT-%ED%86%A0%ED%81%B0-%EC%9D%B8%EC%A6%9D%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_inho/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-JWT-%ED%86%A0%ED%81%B0-%EC%9D%B8%EC%A6%9D%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 18 Mar 2025 08:05:00 GMT</pubDate>
            <description><![CDATA[<h1 id="feign-client를-통한-백엔드-통신-방식">Feign Client를 통한 백엔드 통신 방식</h1>
<p><img src="https://velog.velcdn.com/images/_inho/post/cf2ed8b0-2dd0-4ec4-857c-4a177464fe42/image.png" alt=""></p>
<h3 id="1-인터페이스-정의">1. 인터페이스 정의</h3>
<pre><code class="language-java">java
@FeignClient(name = &quot;authAdaptor&quot;, url = &quot;${api.blog.url}&quot;, path = &quot;/api/auth&quot;)
public interface AuthAdaptor {
    @PostMapping(&quot;/login&quot;)
    ResponseEntity&lt;String&gt; login(@RequestBody Map&lt;String, String&gt; loginRequest);
}</code></pre>
<ul>
<li>백엔드 API에게 부탁할 일의 목록과 같음<ul>
<li>&quot;나는 /api/auth/login으로 POST 요청을 보낼 거야&quot;</li>
<li>&quot;이메일과 비밀번호를 JSON으로 보낼 거야&quot;</li>
<li>&quot;그리고 문자열(JWT 토큰) 응답을 받을 거야&quot;</li>
</ul>
</li>
</ul>
<h3 id="2-spring-자동-구현">2. Spring (자동 구현)</h3>
<ul>
<li>스프링은 위 인터페이스를 보고 실제 HTTP 요청을 보내는 코드를 자동으로 만듬</li>
</ul>
<h3 id="3-컨트롤러에서-사용하기">3. 컨트롤러에서 사용하기</h3>
<pre><code class="language-java">
@Controller
public class AuthController {
    // Spring이 자동으로 만든 구현체를 여기에 넣어줌
    private final AuthAdaptor authAdaptor;

    @PostMapping(&quot;/api/auth/login&quot;)
    public String login(...) {
        // 사용자가 입력한 이메일과 비밀번호를 Map에 담기
        Map&lt;String, String&gt; requestBody = new HashMap&lt;&gt;();
        requestBody.put(&quot;email&quot;, email);
        requestBody.put(&quot;password&quot;, password);

        // 백엔드 API 호출하기 !!🌟
        ResponseEntity&lt;String&gt; responseEntity = authAdaptor.login(requestBody);

        // 응답 처리하기
        // ...
    }
}</code></pre>
<h3 id="4-실제-http-요청">4. 실제 HTTP 요청</h3>
<pre><code>POST /api/auth/login HTTP/1.1
Host: api.blog.url
Content-Type: application/json

{
  &quot;email&quot;: &quot;user@example.com&quot;,
  &quot;password&quot;: &quot;userPassword123&quot;
}
</code></pre><h3 id="5-백엔드-api-응답">5. 백엔드 API 응답</h3>
<pre><code>HTTP/1.1 200 OK
Content-Type: text/plain

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...</code></pre><h2 id="1인증-관련-adaptor-생성">1.인증 관련 Adaptor 생성</h2>
<pre><code class="language-java">import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.Map;

@FeignClient(name = &quot;authAdaptor&quot;, url = &quot;${api.blog.url&quot;, path = &quot;/api/auth&quot;)
public interface AuthAdaptor {
    // &quot;/api/auth/login&quot; 경로로 POST 요청을 보내는 메서드 선언
    @PostMapping(&quot;/login&quot;)
    ResponseEntity&lt;String&gt; login(@RequestBody Map&lt;String, String&gt; loginRequest);
}</code></pre>
<ul>
<li><p>Feign Client 선언: 백엔드 API와 통신하기 위한 인터페이스</p>
</li>
<li><p>name은 스프링에서 빈으로 등록할 때 사용</p>
</li>
<li><p>url : 요청을 보낼 서버의 기본 url(application.properties)</p>
</li>
<li><p>path : API 기본 경로</p>
</li>
<li><p>요청 본문에서 Map형태로 이메일 비밀번호를 담아서 전송</p>
</li>
<li><p>응답으로 JWT 토큰을 받음</p>
</li>
</ul>
<h2 id="2-인증-컨트롤러-생성">2. 인증 컨트롤러 생성</h2>
<pre><code class="language-java">
@Controller
@RequiredArgsConstructor
public class AuthController {
    private final AuthAdaptor authAdaptor;

     @PostMapping(&quot;/api/auth/login&quot;)
    public String login(@RequestParam String email, @RequestParam String password,
                        HttpServletResponse response, RedirectAttributes redirectAttributes){
    }
}
</code></pre>
<ul>
<li>웹 요청을 처리하는 컨트롤러임을 어노테이션으로 명시</li>
<li>백엔드 API와 통신하기 위해 AuthAdaptor 주입 받음</li>
<li>폼에서 전송된 email, password 파라미터를 받음</li>
<li>또한 파라미터로 리다이렉트 시 데이터 전달을 위한 객체를 받아줌</li>
</ul>
<pre><code>// 응답 본문에서 JWT 토큰을 추출
String token = responseEntity.getBody();</code></pre><ul>
<li>만약 백엔드에서 응답이</li>
</ul>
<pre><code>HTTP/1.1 200 OK
Content-Type: text/plain

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</code></pre><p>이렇게 온다면 getBody는 </p>
<pre><code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</code></pre><p>반환 하는데 이것이 JWT 토큰</p>
<h3 id="2-1-login-메소드-구현">2-1. Login 메소드 구현</h3>
<p>메소드가 해야하는 역할</p>
<ol>
<li>/api/auth/login 경로로 POST 요청이 도착</li>
<li>@RequestParam으로 이메일과 비밀번호를 받음</li>
<li>이메일과 비밀번호를 Map에 담아 JSON 형태로 변환<pre><code class="language-java">// 백엔드 API에 보낼 JSON 데이터를 준비 (이메일과 비밀번호)
         Map&lt;String, String&gt; requestBody = new HashMap&lt;&gt;();
         requestBody.put(&quot;email&quot;, email);
         requestBody.put(&quot;password&quot;, password);
</code></pre>
</li>
</ol>
<pre><code>4. AuthAdaptor를 통해 백엔드 API 호출
```java
        ResponseEntity&lt;String&gt; responseEntity = authAdaptor.login(requestBody);</code></pre><ol start="5">
<li><p>백엔드 API에서 인증 처리 (이메일, 비밀번호 검증 후 성공시 JWT 토큰 생성 후 응답)</p>
</li>
<li><p>응답 코드가 200인지, 응답 본문에서 JWT 토큰 추출</p>
<pre><code class="language-java">if(responseEntity.getStatusCode().is2xxSuccessful(){
 // 응답 본문에서 토큰 추출
 String token = responseEntity.getBody();

 // jwtToken 이라는 이름의 쿠키 생성
 Cookie cookie = new Cookie(&quot;jwtToken&quot;, token);

 // 쿠키 경로 &quot;/&quot;로 설정(모든 페이지에서 사용이 가능하게)
 cookie.setPath(&quot;/&quot;);

 // 자바스크립트에서 쿠키 접근 할 수 없게 설정(XSS 공격방지)
 cookie.setHttpOnly(true);

 // 7. 쿠키를 HTTP 응답에 추가
 response.addCookie(cookie);
}</code></pre>
</li>
<li><p>성공시 홈페이지로 리다이렉트, 실패시 오류메세지와 함께 로그인 페이지로 이동</p>
<pre><code class="language-java">             // 홈페이지로 리다이렉트
             return &quot;redirect:/&quot;;
         }else{
              // 로그인 실패시 로그인 페이지로 리다이렉트
             redirectAttributes.addAttribute(&quot;error&quot;, &quot;true&quot;);
             return &quot;redirect:/login&quot;;
         }
     }catch(Exception e){
         redirectAttributes.addAttribute(&quot;error&quot;, &quot;true&quot;);
         return &quot;redirect:/login&quot;;
     }</code></pre>
</li>
</ol>
<h3 id="2-2-logout-메소드">2-2. Logout 메소드</h3>
<pre><code class="language-java">    @GetMapping(&quot;/logout&quot;)
    public String logout(HttpServletResponse response){
        //JWT 토큰 쿠키 삭제
        Cookie cookie = new Cookie(&quot;jwtToken&quot;, null);
        cookie.setPath(&quot;/&quot;);
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        return &quot;redirect:/login?logout=true&quot;;
    }</code></pre>
<ul>
<li>쿠키값 null 설정</li>
<li>모든 경로에서 삭제되도록 설정 후</li>
<li>쿠키 즉시 만료 : setMaxAge(0)</li>
<li>응답에 쿠키 추가 후</li>
<li>로그아웃 메세지와 함께 로그인 페이지로 이동<img src="https://velog.velcdn.com/images/_inho/post/5cb82ef8-94e5-4451-a1fc-842b9bfd7129/image.png" alt=""></li>
</ul>
<hr>
<h2 id="현재까지-구현된-사항">현재까지 구현된 사항</h2>
<h3 id="게이트웨이에서-jwt-인증-필터-구현">게이트웨이에서 JWT 인증 필터 구현</h3>
<ul>
<li>JwtProvider 클래스: 토큰 검증 및 사용자 정보 추출</li>
<li>JwtAuthenticationFilter 클래스: 요청 필터링 및 인증 처리<h3 id="프론트엔드에서-인증-관련-기능-구현">프론트엔드에서 인증 관련 기능 구현</h3>
</li>
<li>AuthAdaptor 인터페이스: 백엔드 API와의 통신</li>
<li>AuthController 클래스: 로그인 및 로그아웃 처리</li>
<li>쿠키 기반 JWT 토큰 저장 및 관리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Gateway에서 JWT 인증하기]]></title>
            <link>https://velog.io/@_inho/Gateway%EC%97%90%EC%84%9C-JWT-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_inho/Gateway%EC%97%90%EC%84%9C-JWT-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 18 Mar 2025 05:07:45 GMT</pubDate>
            <description><![CDATA[<h1 id="게이트웨이에-securityconfig를-추가">게이트웨이에 SecurityConfig를 추가</h1>
<p><a href="https://velog.io/@_inho/JWT-%ED%86%A0%ED%81%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84">이전게시물</a></p>
<p>MSA 아키텍처에서는 각 마이크로서비스마다 인증 로직을 반복 구현해줘야 하는데, 이러한 단점을 보완하며 모든 서비스에 동일한 인증 및 권한 부여 정책을 적용하기위해 게이트웨이 패턴을 적용하기로 했다.</p>
<ul>
<li>이점으로는 각 서비스는 인증 로직 없이 비즈니스 로직에만 집중 가능</li>
<li>서비스 확장시에 용이</li>
</ul>
<h2 id="1-securityconfig">1. SecurityConfig</h2>
<h4 id="pomxml-의존성-추가하기">pom.xml 의존성 추가하기</h4>
<pre><code>  &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-netflix-eureka-client&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
            &lt;artifactId&gt;jjwt-api&lt;/artifactId&gt;
            &lt;version&gt;0.11.5&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
            &lt;artifactId&gt;jjwt-impl&lt;/artifactId&gt;
            &lt;version&gt;0.11.5&lt;/version&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
            &lt;artifactId&gt;jjwt-jackson&lt;/artifactId&gt;
            &lt;version&gt;0.11.5&lt;/version&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
        &lt;/dependency&gt;</code></pre><h4 id="securityconfig">SecurityConfig</h4>
<pre><code class="language-java">package com.nhnacademy.gateway.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http){
        return http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(exchanges -&gt; exchanges
                        .pathMatchers(&quot;/api/auth/**&quot;, &quot;/login&quot;, &quot;/register&quot;, &quot;/css/**&quot;, &quot;/js/**&quot;)
                        .permitAll()
                        .anyExchange().authenticated()
                )
                .build();
    }
}</code></pre>
<ul>
<li>@EnableWebFluxSecurity: Spring WebFlux 환경에서 Spring Security를 활성화하는 어노테이션</li>
<li>리액티브 환경에 맞게 설계됨</li>
</ul>
<blockquote>
<h3 id="💡-리액티브-환경">💡 리액티브 환경</h3>
<p>비동기&amp;논블로킹 방식으로 동작하는 환경
요청이 들어와도 쓰레드를 계속 점유하지 않고, 다른 요청을 처리할 수 있음</p>
</blockquote>
<h3 id="🛠-블로킹-vs-논블로킹-비교">🛠 블로킹 vs 논블로킹 비교</h3>
<p>1️⃣ 블로킹 I/O (Spring MVC 환경)</p>
<pre><code>@GetMapping(&quot;/user&quot;)
public String getUser() {
    return userService.getUser(); // DB에서 데이터를 가져올 때까지 스레드 대기 (Blocking)
}</code></pre><p>요청이 들어오면 getUser() 실행 후 완료될 때까지 스레드가 블로킹됨.
동시 요청이 많아지면 스레드가 부족해지고 성능 저하 발생.
2️⃣ 논블로킹 I/O (Spring WebFlux 환경)</p>
<pre><code>@GetMapping(&quot;/user&quot;)
public Mono&lt;String&gt; getUser() {
    return userService.getUser(); // DB에서 데이터 가져오는 동안 스레드는 다른 작업 수행 (Non-blocking)
}</code></pre><p>요청을 받으면 바로 Mono<String>을 반환하고 스레드는 다른 요청을 처리.
DB 응답이 오면 그때 결과를 전달.</p>
<h2 id="2-jwtauthenticationfilter">2. JwtAuthenticationFilter</h2>
<pre><code class="language-java">@Component
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {

    private final JwtProvider jwtProvider;
    private final List&lt;String&gt; allowedPaths;

    public JwtAuthenticationFilter(JwtProvider jwtProvider) {
        this.jwtProvider = jwtProvider;
        this.allowedPaths = List.of(&quot;/api/auth/login&quot;, &quot;/api/auth/register&quot;, &quot;/login&quot;, &quot;/register&quot;);
    }

    @Override
    public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return null;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
</code></pre>
<ul>
<li>import org.springframework.beans.factory.annotation.Value;</li>
<li>Lombok의 Value ❌</li>
<li>key: JWT 토큰 서명을 검증하는 데 사용되는 키</li>
<li>allowedPaths: 인증 없이 접근 가능한 경로 목록</li>
</ul>
<p><strong>생성자</strong></p>
<ul>
<li>@Value(&quot;${jwt.secret}&quot;): application.properties 또는 application.yml 파일에서 jwt.secret 값을 주입받음</li>
<li>Keys.hmacShaKeyFor(): 바이트 배열로부터 HMAC-SHA 키를 생성, JWT 토큰 검증에 사용됨</li>
<li>allowedPaths: 인증이 필요 없는 경로 목록을 초기화</li>
</ul>
<h3 id="2-1-filter-메소드-구현">2-1. filter 메소드 구현</h3>
<ul>
<li><p>import org.springframework.http.server.reactive.ServerHttpRequest;</p>
</li>
<li><p>Spring-Cloud-Gateway는 리액티브 기반이기에 패키지 클래스 임포트</p>
<h3 id="2-2-jwtprovider">2-2 JwtProvider</h3>
<ul>
<li>게이트웨이에서는 토큰 생성할 필요 없이 검증만 하고</li>
<li>백엔드에서는 validateTokenOrThrow(String token) 메서드가 있어 토큰이 유효하지 않을 경우 예외를 발생 시키는데, 게이트웨이는 필터 내에서 직접 토큰 유효성을 확인하고 401 응답을 반환</li>
<li>위의 사항을 고려 최소한의 기능으로 만든 JwtProvider도 Gateway에 만들어줘야함<pre><code class="language-java">package com.nhnacademy.gateway.security;
</code></pre>
</li>
</ul>
<p>import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;</p>
<p>import java.security.Key;</p>
<p>@Component
public class JwtProvider {</p>
<pre><code>private final Key key;

public JwtProvider(@Value(&quot;${jwt.secret}&quot;) String secretKey) {
    byte[] keyBytes = secretKey.getBytes();
    this.key = Keys.hmacShaKeyFor(keyBytes);
}

// 토큰 검증
public boolean validateToken(String token) {
    try {
        Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token);
        return true;
    } catch (Exception e) {
        return false;
    }
}

// 토큰에서 이메일 추출
public String getEmailFromToken(String token) {
    Claims claims = Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody();
    return claims.getSubject();
}</code></pre><p>}</p>
<p>```</p>
</li>
<li><p>filter 메서드는 Spring Cloud Gateway에서 모든 요청이 통과하는 필터 로직을 구현</p>
</li>
<li><p>public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) </p>
</li>
<li><p>파라미터로 웹 요청과 응답에 대한 컨테이너인 SeverWebExchange, 필터 체인을 위한 파라미터</p>
<pre><code class="language-java">@Override
  public Mono&lt;Void&gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      // 1. 요청 정보 추출
      ServerHttpRequest request = exchange.getRequest(); // Spring WebFlux에서 HTTP 요청 정보를 담고 있는 객체
      String path = request.getURI().getPath(); // 요청 URL에서 경로 부분만 추출 (&quot;/api/auth/login&quot;)

      // 2. 인증 예외 경로 확인
      if(isAllowedPath(path)){
          return chain.filter(exchange); // 인증 필요없는 경로면 다음 필터로 요청 전달
      }

      // 3. 정적 리소스 확인
      if (path.startsWith(&quot;/css/&quot;) || path.startsWith(&quot;/js/&quot;)) {
          return chain.filter(exchange);
      }

      // 4. JWT 토큰 추출
      String token = extractToken(request); // Authorization&quot; 헤더에서 &quot;Bearer &quot; 다음에 오는 문자열을 토큰으로 추출

      // 5. 토큰 검증 및 오류 처리
      if (token == null || !jwtProvider.validateToken(token)) {
          ServerHttpResponse response = exchange.getResponse();
          response.setStatusCode(HttpStatus.UNAUTHORIZED);
          return response.setComplete();
      }

      // 6. 사용자 정보 추가
      String email = jwtProvider.getEmailFromToken(token);

      ServerHttpRequest modifiedRequest = request.mutate()
              .header(&quot;X-User-Email&quot;, email)
              .build();

    // 7. 수정된 요청 전달
      return chain.filter(exchange.mutate().request(modifiedRequest).build());
  }  </code></pre>
</li>
</ul>
<ol>
<li>요청 경로를 확인하여 인증이 필요한지 판단</li>
<li>인증이 필요없는 경로인지 판단 후 필요없을 시 다음 필터로 요청 그대로 전달</li>
<li>경로가 정적 리소스인지 확인, 정적 리소스는 인증 없이 접근 가능 해야하기 때문</li>
<li>Authorization 헤더에서 Bearer 토큰 추출</li>
<li>토큰이 없거나, 유효성 검증 실패시 ,401 상태코드 설정 후 응답 완료 후 클라이언트에게 반환</li>
</ol>
<p>-&gt; 즉 유효하지 않은 토큰 접근 시 인증 오류 반환
6. 토큰에서 사용자 이메일 추출 후, mutate() 메소드를 통해 기존 요청 객체를 수정하기 위한 빌더 생성
-&gt; header로 사용자 이메일 정보 추가
-&gt; .build() 수정된 요청 객체 생성
7. 수정된 요청을 전달하는데, 기존 교환 객체를 수정하기 위해 빌더 생성 후 수정된 요청 객체를 교환 객체에 설정 후 생성! -&gt; 수정된 교환 객체를 다음 필터로 전달</p>
<h3 id="2-2-getorder-메소드">2-2. getOrder 메소드</h3>
<pre><code class="language-java">  @Override
    public int getOrder() {
        return -1;
    }</code></pre>
<ul>
<li>JWT 인증 필터는 일반적으로 다른 필터보다 먼저 실행되어야 하므로 getOrder 값을 음수로 설정</li>
</ul>
<p>이제 JWT 인증필터가 게이트웨이에 구현되었기에</p>
<ul>
<li>인증되지 않은 요청은 게이트웨이에서 차단</li>
<li>각 마이크로서비스마다 JWT 인증 로직을 구현할 필요 없음</li>
<li>모든 서비스에 동일한 인증 정책 적용됨</li>
</ul>
<h2 id="게이트웨이는-다음과-같은-역할을-수행">게이트웨이는 다음과 같은 역할을 수행</h2>
<ul>
<li>JWT 토큰 검증</li>
<li>인증되지 않은 요청 차단 (401 응답 반환)</li>
<li>인증된 요청에 사용자 정보 추가 (X-User-Email 헤더)</li>
<li>인증된 요청을 적절한 백엔드 서비스로 라우팅</li>
</ul>
<p>백엔드 서비스는 게이트웨이가 추가한 X-User-Email 헤더를 통해 사용자를 식별할 수 있으며, 추가적인 권한 검사나 비즈니스 로직에 집중할 수 있게 됨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT 토큰 & 로그인 기능 구현]]></title>
            <link>https://velog.io/@_inho/JWT-%ED%86%A0%ED%81%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@_inho/JWT-%ED%86%A0%ED%81%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 17 Mar 2025 13:00:25 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-jwt란">📌 JWT란?</h1>
<blockquote>
<p>JWT(JSON Web Token)는 사용자가 로그인에 성공한 후, 서버가 발급해주는 인증 토큰
이 토큰은 클라이언트가 서버에 요청할 때마다 자신이 인증된 사용자라는 것을 증명하는 데 사용됨</p>
</blockquote>
<h2 id="jwt의-구성요소">JWT의 구성요소</h2>
<ul>
<li>Header : 토큰의 타입(JWT)와 서명 알고리즘 포함</li>
<li>Payload : 사용자 정보 포함</li>
<li>Signature : 토큰이 변조되지 않았음을 보장하기 위해 생성된 서명</li>
</ul>
<h2 id="jwt-특징">JWT 특징</h2>
<ul>
<li>REST API와 같은 상태 없는 통신에서 사용자 인증을 처리하는데 적합하다</li>
<li>서버가 클라이언트의 상태(Session)를 저장할 필요가 없다.</li>
<li>대부분의 인증 정보는 JWT(민감한 정보는 제외)에 포함되어 클라이언트와 함께 전송됨
<img src="https://velog.velcdn.com/images/_inho/post/5dbdeaf2-d507-4982-ab80-820067853b78/image.png" alt=""></li>
<li>이러한 사이트에서 바로 복호화가 가능함</li>
</ul>
<blockquote>
<p>사용자가 로그인 시 서버는 JWT 발급
-&gt; 이후 요청시마다 이 토큰을 통해 인증처리</p>
</blockquote>
<ul>
<li><p>JWT는 사용자 정보를 페이로드에 포함가능하기에 추가적인 데이터 전달이 가능 (Role, userID, 만료 시간 등..)</p>
</li>
<li><p>서버가 상태를 저장하지 않기에 서버 부하 감소</p>
</li>
<li><p>서명을 통해 토큰이 변조되지 않았음을 보장</p>
</li>
<li><p>클라이언트가 서버로부터 받은 JWT만 있다면 서버는 추가적인 DB조회없이 사용자 인증 가능</p>
</li>
<li><p>토큰에 만료시간 설정 가능함, 하지만 한번 발급되면 만료시간까지 유효하기 때문에 서버에서 강제로 만료시키기 어려움</p>
</li>
<li><blockquote>
<p>*<em>사용자가 로그아웃해도 기존 토큰 유효 *</em></p>
</blockquote>
</li>
<li><p>JWT는 모든 데이터를 자체적으로 포함하기에 크기가 커질 수 있음</p>
</li>
<li><p>비밀키 유출시 모든 JWT가 위조될 수 있음</p>
</li>
</ul>
<p><strong>즉 REST API에서 인증 정보를 안전하게 받고 변조 방지를 위해 JWT</strong></p>
<p>우선 로그인 요청 DTO를 만들어준다.
로그인 시에는 이메일, 패스워드만 있으면 되기 때문에 </p>
<pre><code class="language-java">package com.nhnacademy.blog.member.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class MemberLoginRequest {
    private String mbEmail;
    private String mbPassword;
}</code></pre>
<p>위와 같이 만들었음</p>
<h2 id="1-jwt-토큰-생성-컴포넌트">1. JWT 토큰 생성 컴포넌트</h2>
<p>이제 JWT 토큰을 생성하고 검증하는 컴포넌트가 필요하다.
<strong>JwtTokenProvider.java</strong></p>
<pre><code class="language-java">package com.nhnacademy.blog.common.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JwtProvider {
    private final Key key;

    // @Value 어노테이션을 사용해 설정 파일에서 값을 주입받음
    public JwtProvider(@Value(&quot;${jwt.secret}&quot;) String secretKey) {
        // Base64로 인코딩된 비밀키를 디코딩하여 Key 객체 생성
        byte[] keyBytes = secretKey.getBytes();
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }


}</code></pre>
<ul>
<li>스프링 빈으로 등록해서 다른 클래스에서 주입 받아 사용할 수 있도록 @Component</li>
<li>application.properties에 설정된 값 가져오기<ul>
<li>secretKey : JWT 암호화시 사용하는 비밀키</li>
<li>validityInMilliseconds : JWT 토큰 만료될 때까지 유효시간 설정</li>
</ul>
</li>
</ul>
<h2 id="2-jwt-토큰-생성-메소드">2. JWT 토큰 생성 메소드</h2>
<pre><code class="language-java">   public String generateToken(String email) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + 3600000); // 1시간 유효

        return Jwts.builder()
                .setSubject(email) // 사용자 이메일을 subject로 설정
                .setIssuedAt(now) // 발급 시간 설정
                .setExpiration(validity) // 만료 시간 설정
                .signWith(key, SignatureAlgorithm.HS256) // Key 객체와 알고리즘을 사용해 서명
                .compact(); // 최종적으로 문자열 형태의 JWT 반환
    }
    }</code></pre>
<ul>
<li><p>subject는 JWT의 페이로드에 포함될 주요 데이터(사용자 식별 정보)</p>
</li>
<li><p>로그인 성공시 사용자 이메일을 JWT subject 필드에 저장해서 이후 요청에서 사용자 식별 가능</p>
</li>
<li><p>토큰이 일정 시간 후 만료되게 설정</p>
</li>
<li><p>HS256 알고리즘 사용</p>
</li>
</ul>
<h2 id="3-토큰-검증-및-사용">3. 토큰 검증 및 사용</h2>
<pre><code class="language-java"> public boolean validateToken(String token){
        try{
            Jwts.parserBuilder()
                    .setSigningKey(key) // 서명을 검증하기 위해 키를 설정
                    .build()   // 파서를 빌드하여 실제 검증 작업을 수행할 준비를 완료
                    .parseClaimsJws(token); // 전달받은 토큰을 파싱하여 서명 및 구조가 올바른지 확인

            return true;
        }catch(Exception e){
            return false;
        }
    }

 // 유효하지 않은 토큰 예외

public void validateTokenOrThrow(String token) {
        //validateToken 메소드를 호출하여 토큰의 유효성을 확인
        if (!validateToken(token)) {
            throw new TokenIsNotValidException(&quot;유효하지 않은 토큰입니다&quot;);
        }
    }</code></pre>
<ul>
<li>Jwts.parserBuilder() : 서명을 검증하기 위해 키를 설정하는데 이때 이 키는 <strong>토큰 생성시 사용된 비밀키와 동일</strong>해야함</li>
<li>이를 통해 클라이언트가 제공한 토큰이 변조되지 않았고, 서버에서 발급된 것인지 확인 가능</li>
<li>setSigningKey(key)는 토큰의 서명이 비밀키로 생성되었는지 확인하는 중요한 단계</li>
</ul>
<h2 id="3-1-jwt-이메일-추출">3-1. JWT 이메일 추출</h2>
<pre><code class="language-java">  public String getEmailFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key) // 서명을 검증하기 위해 키를 설정
                .build() // 파서를 빌드하여 실제 검증 작업을 수행할 준비 완료
                .parseClaimsJws(token) // 전달받은 토큰을 파싱하여 Claims 객체 반환
                .getBody() // Claims 객체에서 페이로드(body)를 가져옴
                .getSubject(); // 페이로드에서 subject 필드(사용자 이메일)를 가져옴
    }</code></pre>
<ul>
<li>사용자가 로그인하면 이 메서드를 호출하여 JWT를 생성하고 클라이언트에게 반환</li>
</ul>
<h2 id="4-jwt를-활용하여-인증-및-권한-부여를-시스템에-통합하기">4. JWT를 활용하여 인증 및 권한 부여를 시스템에 통합하기</h2>
<h3 id="4-1-spring-security와-jwt-통합">4-1. Spring Security와 JWT 통합</h3>
<p>SecurityConfig 설정</p>
<pre><code class="language-java">
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtProvider jwtProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(csrf -&gt; csrf.disable())
                .authorizeHttpRequests(auth -&gt;
                        auth.requestMatchers(&quot;/api/auth/**&quot;).permitAll()
                                .anyRequest().authenticated());

        return httpSecurity.build();
    }
}
</code></pre>
<ul>
<li>REST API는 클라이언트가 매번 인증 정보를 포함하여 요청하므로 CSRF 보호가 필요❌</li>
<li>요청별로 보안 정책 설정</li>
<li>/api/auth/** 경로로 들어오는 요청은 인증 없이 접근가능하게 허용</li>
<li>위에 명시된 경로를 제외한 모든 요청은 인증 필요 </li>
</ul>
<h3 id="4-2-jwtauthenticationfilter">4-2. JwtAuthenticationFilter</h3>
<ul>
<li>클라이언트가 요청시 전달한 JWT를 검증, 인증 정보를 SecurityContextHolder에 저장하는 역할</li>
<li>Spring Security Filter Chain에 등록되어 모든 요청 처리전 실행됨</li>
</ul>
<pre><code class="language-java">@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    }
}</code></pre>
<ul>
<li>OncePerRequestFilter : Spring Security의 기본 필터를 확장하여 요청마다 한 번만 실행되는 필터</li>
</ul>
<p>JWT는 Authorization 헤더에 포함된다.</p>
<ul>
<li>클라이언트는 서버에 요청시 JWT를 HTTP 헤더에 포함<pre><code>Authorization: Bearer &lt;JWT&gt;</code></pre></li>
<li>Bearer는 단순히 토큰임을 나타내는 접두사</li>
</ul>
<p><strong>실제 JWT는 Bearer뒤에 있는 값이므로 잘라내는 메소드</strong></p>
<pre><code class="language-java">  // Authorization 헤더에서 Bearer 토큰 추출
    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader(&quot;Authorization&quot;);
        if (header != null &amp;&amp; header.startsWith(&quot;Bearer &quot;)) {
            return header.substring(7); // &quot;Bearer &quot; 이후의 토큰만 추출
        }
        return null; // 헤더가 없거나 Bearer로 시작하지 않으면 null 반환
    }</code></pre>
<p><strong>doFilterInternal</strong>
요청이 들어올 때마다 실행되는 메소드</p>
<pre><code class="language-java">    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 요청 헤더에서 JWT 추출
        String token = extractToken(request);

        if(token!=null &amp;&amp; jwtProvider.validateToken(token)){
            String email = jwtProvider.getEmailFromToken(token);
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(email, null, null);

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
</code></pre>
<ul>
<li>extractToken으로 실제 토큰 값 추출</li>
<li>클라이언트가 JWT를 보내는지 확인 후 보냈다면 JWT가 유효한지 확인 + 서명이 올바른가, 만료되지 않았는가 등</li>
<li>getEmailFromToken을 통해 토큰에서 이메일 꺼낸 후
SpringSecurity에서 사용하는 인증 객체 <strong>UsernamePasswordAuthenticationToken</strong>에다가 이메일만 인증정보로 설정</li>
<li>이후 SpringSecurtiy 컨텍스트에 인증 정보 저장</li>
<li>이를 이용해 추후 컨트롤러에서 인증된 사용자 정보 사용 가능</li>
<li>필터 작업이 끝나면 요청을 다음 필터로 전달</li>
</ul>
<h3 id="4-3만든-필터를-securityconfig에-추가하기">4-3.만든 필터를 SecurityConfig에 추가하기</h3>
<pre><code class="language-java">  @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(csrf -&gt; csrf.disable())
                .authorizeHttpRequests(auth -&gt;
                        auth.requestMatchers(&quot;/api/auth/**&quot;).permitAll()
                                .anyRequest().authenticated())  
                .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }</code></pre>
<ul>
<li>Spring Security의 기본 인증 필터(UsernamePasswordAuthenticationFilter) 앞에 JwtAuthenticationFilter를 추가</li>
</ul>
<h3 id="4-4-refreshtoken-고민">4-4. RefreshToken 고민</h3>
<blockquote>
<p>Access Token은 보안상의 이유로 짧은 유효기간(예: 15분<del>1시간)을 설정함
만료된 Access Token은 더 이상 사용할 수 없으므로 클라이언트는 새로운 Access Token을 발급받아야 함
Refresh Token은 Access Token이 만료되었을 때 새로운 Access Token을 발급받기 위해 사용됨 
Refresh Token은 상대적으로 긴 유효기간(예: 7일</del>30일)을 가지며, 서버에서 안전하게 관리됨</p>
</blockquote>
<p>추후에 필요시 구현하기 
지금 당장은 꼭 필요한지 모르겠다</p>
<h2 id="5-authcontroller에-추가하기">5. AuthController에 추가하기</h2>
<h3 id="5-1-authservice-인터페이스에-로그인-메소드-추가">5-1. AuthService 인터페이스에 로그인 메소드 추가</h3>
<pre><code class="language-java">
    @Override
    public String login(MemberLoginRequest memberLoginRequest) {
        Member member = memberRepository.findByMbEmail(memberLoginRequest.getMbEmail())
                .orElseThrow(()-&gt;new NotFoundException(&quot;이메일 또는 비밀번호가 일치하지 않습니다&quot;));

        if (!passwordEncoder.matches(memberLoginRequest.getMbPassword(), member.getMbPassword())) {
            throw new UnauthorizedException(&quot;이메일 또는 비밀번호가 일치하지 않습니다.&quot;);
        }

        return jwtProvider.generateToken(member.getMbEmail()); // JWT 생성 후 반환 
    }</code></pre>
<ul>
<li>이메일로 멤버 조회후 존재하지 않을시 예외 발생</li>
<li>만약 입력된 비밀번호와 DB에 저장된 암호화된 비밀번호 비교 후 일치하지 않을 시 예외 발생</li>
<li>인증 성공시 JWT 토큰 생성 후 반환</li>
</ul>
<h3 id="5-2-authcontroller에-로그인-엔드-포인트-추가">5-2. AuthController에 로그인 엔드 포인트 추가</h3>
<pre><code class="language-java">   @PostMapping(&quot;/login&quot;)
    public ResponseEntity&lt;String&gt; login(@RequestBody MemberLoginRequest memberLoginRequest){
        String token = authService.login(memberLoginRequest);
        return ResponseEntity.ok(token);
    }</code></pre>
<ul>
<li>/api/auth/login 주소로 POST 요청시<pre><code>POST /api/auth/login
Content-Type: application/json
</code></pre></li>
</ul>
<p>{
    &quot;mbEmail&quot;: &quot;<a href="mailto:test@example.com">test@example.com</a>&quot;,
    &quot;mbPassword&quot;: &quot;password123&quot;
}</p>
<pre><code>이와 같은 요청이 오는데
</code></pre><p>public ResponseEntity<String> login(@RequestBody MemberLoginRequest memberLoginRequest){</p>
<pre><code>- 클라이언트가 보낸 JSON 형식의 요청 데이터를 자바 객체(MemberLoginRequest)로 변환하여 받음
- authService.login 메소드로 생성된 JWT를 클라이언트에게 반환
- 클라이언트는 이 JWT를 사용해서 인증이 필요한 API 호출

🌟 **전체 흐름**
![](https://velog.velcdn.com/images/_inho/post/cc8556a0-2e77-4c48-b4c8-1bfb2951f7c1/image.png)

## 6. 추가된 기능 테스트하기 by Mockito

### 6-1. 로그인 성공 테스트
```java
 @Test
    @DisplayName(&quot;로그인 성공 테스트&quot;)
    void loginSuccess() {
        // Given: 로그인 요청 데이터 준비
        MemberLoginRequest loginRequest = new MemberLoginRequest(&quot;test@nhnacademy.com&quot;, &quot;password123123!&quot;);

        // Mock 설정: 데이터베이스에서 사용자 조회
        Member member = Member.ofNewMember(
                &quot;test@nhnacademy.com&quot;,
                &quot;TestUser&quot;,
                passwordEncoder.encode(&quot;password123123!&quot;),
                &quot;01012345678&quot;
        );

        Mockito.when(memberRepository.findByMbEmail(Mockito.anyString())).thenReturn(Optional.of(member));
        // Mock 설정: JWT 생성 동작 정의 (Mock)
        Mockito.when(jwtProvider.generateToken(Mockito.anyString())).thenReturn(&quot;mock-jwt-token&quot;);

        // When: 로그인 로직 실행
        String token = authService.login(loginRequest);

        // Then: 결과 검증
        assertNotNull(token);
        assertEquals(&quot;mock-jwt-token&quot;, token);
    }</code></pre><ul>
<li><img src="https://velog.velcdn.com/images/_inho/post/c6f59327-18cc-47f7-832c-d5bed3ec9450/image.png" alt=""></li>
<li>@Spy 어노테이션은 내가 모킹해주는 메소드에 한해서만 모킹하고 나머지는 실제 메소드가 동작</li>
<li>이와 같이 설정해준 후 테스트 진행</li>
</ul>
<h3 id="6-2-로그인-실패-테스트---비밀번호-불일치">6-2. 로그인 실패 테스트 - 비밀번호 불일치</h3>
<pre><code class="language-java">
    @Test
    @DisplayName(&quot;로그인 실패 테스트 - 비밀번호 불일치&quot;)
    void loginFail() {
        // Given: 로그인 요청 데이터 준비
        MemberLoginRequest loginRequest = new MemberLoginRequest(&quot;test@nhnacademy.com&quot;, &quot;temp&quot;);

        // Mock 설정: 데이터베이스에서 사용자 조회
        Member member = Member.ofNewMember(
                &quot;test@nhnacademy.com&quot;,
                &quot;TestUser&quot;,
                passwordEncoder.encode(&quot;password123123!&quot;),
                &quot;01012345678&quot;
        );

        Mockito.when(memberRepository.findByMbEmail(Mockito.anyString())).thenReturn(Optional.of(member));

        // When &amp; Then: 로그인 로직 실행 시 UnauthorizedException 발생 확인
        assertThrows(UnauthorizedException.class, () -&gt; authService.login(loginRequest));

    }</code></pre>
<ul>
<li>로그인 실패 테스트 </li>
<li>요청 비밀번호, 저장된 비밀번호 일부러 실패하게 만든후 예외 확인</li>
</ul>
<h3 id="6-3-로그인-실패-테스트---존재하지-않는-이메일">6-3. 로그인 실패 테스트 - 존재하지 않는 이메일</h3>
<pre><code class="language-java">  @Test
    @DisplayName(&quot;로그인 실패 테스트 - 존재하지 않는 이메일&quot;)
    void notFoundEmail(){
        Mockito.when(memberRepository.findByMbEmail(Mockito.anyString())).thenReturn(Optional.empty());

        assertThrows(NotFoundException.class, ()-&gt;{
            authService.login(new MemberLoginRequest(&quot;testmail&quot;, &quot;testpass&quot;));
        });
    }
</code></pre>
<ul>
<li>메일로 멤버 조회시 일부러 Optional.empty 리턴</li>
<li>이후 로그인 시 NotFound 예외 발생하는지 검증</li>
</ul>
<hr>
<h2 id="7-jwtprovider-검증">7. JwtProvider 검증</h2>
<h3 id="7-1-환경-설정">7-1. 환경 설정</h3>
<pre><code class="language-java">   private JwtProvider jwtProvider;
    private final String secretKey = &quot;7c83e8ed501e28981f123d88ca87fa8a61277945c39b18a14ec8816986f96399&quot;; // 테스트용 시크릿 키
    private final String testEmail = &quot;test@example.com&quot;;

    @BeforeEach
    void setUp() {
        jwtProvider = Mockito.spy(new JwtProvider(secretKey));
    }
</code></pre>
<ul>
<li>테스트용 시크릿키 및 테스트 데이터</li>
<li>Mockito.spy로 JwtProvider 초기화 </li>
</ul>
<h3 id="7-2-토큰-생성-테스트">7-2. 토큰 생성 테스트</h3>
<pre><code class="language-java">  @Test
    @DisplayName(&quot;토큰 생성 테스트&quot;)
    void generateToken() {

        // jwt 토큰이 mock-jwt-token 반환하도록 설정
        doReturn(&quot;mock-jwt-token&quot;).when(jwtProvider).generateToken(testEmail);

        // 테스트 이메일로 토큰 생성
        String token = jwtProvider.generateToken(testEmail);

        // 생성된 토큰이 NULL이 아닌지
        assertNotNull(token);

        // 생성된 토큰이 비어있지 않은지
        assertFalse(token.isEmpty());

        // mock-jwt-token인지
        assertEquals(&quot;mock-jwt-token&quot;, token);
    }</code></pre>
<ul>
<li>generateToken() 메소드가 정상적으로 실행되며, mock-jwt-token이 반환되는지 확인</li>
</ul>
<h3 id="7-3-유효한-토큰-검증">7-3. 유효한 토큰 검증</h3>
<pre><code class="language-java"> @Test
    @DisplayName(&quot;유효한 토큰 검증 성공&quot;)
    void validateToken_validToken_returnsTrue() {
        String token = &quot;valid-token&quot;;
        // validateToken시 true 반환
        doReturn(true).when(jwtProvider).validateToken(token);

        // 생성된 토큰 검증 (성공 예상)
        boolean isValid = jwtProvider.validateToken(token);

        //  검증 결과가 true인지 확인
        assertTrue(isValid);
    }</code></pre>
<ul>
<li>validateToken()이 true를 반환하는지 테스트</li>
</ul>
<h3 id="7-4-유효하지-않은-토큰-검증-실패">7-4. 유효하지 않은 토큰 검증 (실패)</h3>
<pre><code class="language-java">    @Test
    @DisplayName(&quot;유효하지 않은 토큰 검증 실패&quot;)
    void validateToken_invalidToken_returnsFalse() {
        // 유효하지 않은 토큰 생성
        String invalidToken = &quot;wrong-token&quot;;
        doReturn(false).when(jwtProvider).validateToken(invalidToken);

        // 유효하지 않은 토큰 검증 (실패 예상)
        boolean isValid = jwtProvider.validateToken(invalidToken);

        // 검증 결과가 false인지 확인
        assertFalse(isValid);
    }
</code></pre>
<ul>
<li>validateToken()이 false를 반환하는지 테스트</li>
</ul>
<h3 id="7-5-유효한-토큰-검증-시-예외-발생-x">7-5. 유효한 토큰 검증 시 예외 발생 X</h3>
<pre><code class="language-java">    @Test
    @DisplayName(&quot;유효한 토큰 검증 시 예외 발생하지 않음&quot;)
    void validateTokenOrThrow_validToken_noException() {
        //  테스트 이메일로 토큰 생성
        String token = &quot;valid-token&quot;;
        doNothing().when(jwtProvider).validateTokenOrThrow(token);

        //  예외가 발생하지 않는지 확인
        assertDoesNotThrow(() -&gt; jwtProvider.validateTokenOrThrow(token));
    }</code></pre>
<ul>
<li>validateTokenOrThrow()가 정상적으로 실행되는지 확인.</li>
</ul>
<h3 id="7-6-유효하지-않은-토큰-검증-시-예외-발생">7-6. 유효하지 않은 토큰 검증 시 예외 발생</h3>
<pre><code class="language-java">    @Test
    @DisplayName(&quot;유효하지 않은 토큰 검증 시 예외 발생&quot;)
    void validateTokenOrThrow_invalidToken_throwsException() {
        // 유효하지 않은 토큰 생성
        String invalidToken = &quot;inValid-token&quot;;
        doThrow(new TokenIsNotValidException(&quot;유효하지 않은 토큰입니다.&quot;)).when(jwtProvider).validateTokenOrThrow(invalidToken);

        //  TokenIsNotValidException 예외가 발생하는지 확인
        assertThrows(TokenIsNotValidException.class, () -&gt; jwtProvider.validateTokenOrThrow(invalidToken));
    }</code></pre>
<ul>
<li>TokenIsNotValidException이 발생하는지 검증.</li>
</ul>
<h3 id="7-7-토큰에서-이메일-추출-테스트">7-7. 토큰에서 이메일 추출 테스트</h3>
<pre><code class="language-java">  @Test
    @DisplayName(&quot;토큰에서 이메일 추출 테스트&quot;)
    void getEmailFromToken() {
        // 테스트 이메일로 토큰 생성
        String token = &quot;valid-token&quot;;
        doReturn(testEmail).when(jwtProvider).getEmailFromToken(token);

        // 토큰에서 이메일 추출
        String email = jwtProvider.getEmailFromToken(token);

        // 추출한 이메일, 테스트 이메일 비교
        assertEquals(email, testEmail);
    }</code></pre>
<ul>
<li>getEmailFromToken()이 예상한 이메일 값을 반환하는지 확인.</li>
</ul>
<h2 id="추후에-공부할-내용">추후에 공부할 내용</h2>
<ul>
<li><p>Refresh Token 관리 방식</p>
<ul>
<li>DB저장? Redis?</li>
<li>Refresh Token 탈취방지 보안 이슈</li>
</ul>
</li>
<li><p>JWT Payload</p>
<ul>
<li>Payload 암호화 되지 않으므로 민감한 정보 포함 X</li>
<li>추가 정보 필요시 DB 조회</li>
</ul>
</li>
<li><p>JWT 발급 후 클라이언트에서 저장 방식</p>
<ul>
<li>LocalStorage는 보안상 위험 -&gt; HTTP ONLY 쿠키 사용 권장</li>
<li>쿠키 사용지 CORS, Secure 설정 고려</li>
</ul>
</li>
<li><p>로그아웃 처리 </p>
<ul>
<li>Access Token은 클라이언트가 갖고 있기 때문에 서버에서 강제 만료 어렵</li>
<li>보통 Refresh Token을 서버에서 삭제하는 방식으로 처리</li>
</ul>
</li>
</ul>
<h2 id="내용-정리">내용 정리</h2>
<ol>
<li>JWT 인증 시스템 구현
JwtProvider: JWT 토큰 생성, 검증, 이메일 추출 기능 구현</li>
</ol>
<p>generateToken(String email): 사용자 이메일로 JWT 토큰 생성</p>
<p>validateToken(String token): 토큰 유효성 검증</p>
<p>getEmailFromToken(String token): 토큰에서 이메일 추출</p>
<p>JwtAuthenticationFilter: 요청 헤더에서 JWT 토큰을 추출하고 검증하는 필터</p>
<p>모든 요청에 대해 Authorization 헤더에서 Bearer 토큰을 추출</p>
<p>토큰 검증 후 인증 정보를 SecurityContext에 설정</p>
<p>PasswordEncoder: 비밀번호 암호화 및 검증</p>
<p>BCrypt 알고리즘을 사용한 비밀번호 암호화</p>
<p>로그인 시 비밀번호 일치 여부 검증</p>
<ol start="2">
<li>인증 관련 API 구현
회원가입 API: /api/auth/register</li>
</ol>
<p>사용자 정보(이메일, 이름, 비밀번호, 연락처 등) 저장</p>
<p>블로그 생성 및 카테고리 설정</p>
<p>로그인 API: /api/auth/login</p>
<p>이메일과 비밀번호로 사용자 인증</p>
<p>인증 성공 시 JWT 토큰 발급</p>
<ol start="3">
<li>보안 설정
SecurityConfig: Spring Security 설정</li>
</ol>
<p>CSRF 보호 비활성화 (REST API이므로)</p>
<p>세션 관리 정책을 STATELESS로 설정</p>
<p>인증이 필요한 경로와 필요하지 않은 경로 설정</p>
<p>JwtAuthenticationFilter 등록</p>
<ol start="4">
<li>예외 처리
NotFoundException: 리소스를 찾을 수 없을 때 발생</li>
</ol>
<p>UnauthorizedException: 인증 실패 시 발생</p>
<p>TokenIsNotValidException: 유효하지 않은 토큰일 때 발생</p>
<ol start="5">
<li>테스트 코드
AuthServiceImplTest: 회원가입 및 로그인 기능 테스트</li>
</ol>
<p>JwtProviderTest: JWT 토큰 생성, 검증, 이메일 추출 기능 테스트</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring boot 단위 테스트 vs 통합테스트]]></title>
            <link>https://velog.io/@_inho/Spring-boot-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-vs-%ED%86%B5%ED%95%A9%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@_inho/Spring-boot-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-vs-%ED%86%B5%ED%95%A9%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Tue, 11 Mar 2025 04:24:07 GMT</pubDate>
            <description><![CDATA[<h2 id="블로그-멤버-매핑-테스트-방식">블로그 멤버 매핑 테스트 방식</h2>
<pre><code class="language-java">@Entity
@Table(name = &quot;blog_member_mappings&quot;,
    indexes = {
        @Index(name = &quot;uk_blog_member_mapping&quot;,columnList = &quot;mb_no, blog_id, role_id&quot;, unique = true)
    }
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Getter
public class BlogMemberMapping {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;blog_member_id&quot;)
    private Long blogMemberId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;mb_no&quot;, referencedColumnName = &quot;mb_no&quot;, nullable = false)
    private Member member;

    @Setter
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name=&quot;blog_id&quot;, referencedColumnName = &quot;blog_id&quot;, nullable = false)
    private Blog blog;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name=&quot;role_id&quot;, referencedColumnName = &quot;role_id&quot;, nullable = false)
    private Role role;

    private BlogMemberMapping(Member member, Blog blog, Role role) {
        this.member = member;
        this.blog = blog;
        this.role = role;
    }

    public static BlogMemberMapping ofNewBlogMemberMapping(Member member, Blog blog, Role role) {
        return new BlogMemberMapping(member,blog,role);
    }

}
</code></pre>
<p>블로그 멤버 매핑은 Role, Blog, Member 세 가지 엔티티를 연결하는 중요한 관계 엔티티이다.
이러한 복잡한 관계를 테스트할 때 단위 테스트와 통합 테스트 각각의 적합성에 대해서 고민한 내용에 대해 얘기해보려한다.</p>
<p>이전에 Blog, Member 등에 대해서는 테스트 코드를 작성할 때 당연히 단위 테스트로 진행했다.</p>
<h4 id="blogrepositorytest">BlogRepositoryTest</h4>
<pre><code class="language-java">@SpringBootTest
@ActiveProfiles(&quot;test&quot;)

class BlogRepositoryTest {

    @Autowired
    BlogRepository blogRepository;


    Blog blog = null;


    @BeforeEach
    void setUp(){
        blog = Blog.ofNewBlog(
                &quot;testFid&quot;,
                true,
                &quot;testBlogName&quot;,
                &quot;testNickName&quot;,
                &quot;testDescription&quot;
        );

        blogRepository.save(blog);
    }

    @AfterEach
    void tearDown(){
        blogRepository.delete(blog);
    }

    @Test
    @DisplayName(&quot;블로그 아이디 - 블로그 찾기 &quot;)
    void findBlogsByBlogId() {

        Optional&lt;Blog&gt; testBlog = blogRepository.findBlogsByBlogId(blog.getBlogId());

        assertNotNull(testBlog);

        assertAll(
                ()-&gt; assertEquals(true, testBlog.get().isBlogMain()),
                ()-&gt; assertEquals(&quot;testFid&quot;, testBlog.get().getBlogFid()),
                ()-&gt; assertEquals(&quot;testBlogName&quot;,testBlog.get().getBlogName()),
                ()-&gt; assertEquals(&quot;testNickName&quot;, testBlog.get().getBlogMbNickname()),
                ()-&gt; assertEquals(&quot;testDescription&quot;, testBlog.get().getBlogDescription())
        );
    }
   ...
  }</code></pre>
<h4 id="blogservicetest">BlogServiceTest</h4>
<pre><code class="language-java">@ActiveProfiles(&quot;test&quot;)
@SpringBootTest
@Transactional
class BlogServiceImplTest {

    @Mock // 블로그레포지토리는 테스트가 끝났으므로 (검증 완료) 실제 객체가 아닌 Mock 객체
    BlogRepository blogRepository;

    @InjectMocks // Mock 객체 자동 주입 후 AutoWired
    BlogServiceImpl blogService;



    @Test
    @DisplayName(&quot;블로그 저장&quot;)
    void saveBlog() {
        BlogRequest blogRequest = new BlogRequest(
                &quot;testFid&quot;,
                true,
                &quot;testName&quot;,
                &quot;testNickName&quot;,
                &quot;testDescription&quot;
        );

        Mockito.doAnswer(invocationOnMock -&gt; {
                  Blog blog = invocationOnMock.getArgument(0);
                  Field blogId = Blog.class.getDeclaredField(&quot;blogId&quot;);
                  Field createdAt = Blog.class.getDeclaredField(&quot;createdAt&quot;);
                  blogId.setAccessible(true);
                  blogId.set(blog, 1L);

                  createdAt.setAccessible(true);
                  createdAt.set(blog, LocalDateTime.now());
                  return null;
                })
                .when(blogRepository)
                .save(Mockito.any(Blog.class));

        BlogResponse blogResponse =  blogService.saveBlog(blogRequest);

        assertNotNull(blogResponse);

        assertAll(
                ()-&gt; assertNotNull(blogResponse.getBlogId()),
                ()-&gt; assertTrue(blogResponse.getCategories().isEmpty()),
                ()-&gt; assertTrue(blogResponse.getBlogMemberMappings().isEmpty()),
                ()-&gt; assertEquals(&quot;testFid&quot;,blogResponse.getBlogFid()),
                ()-&gt; assertTrue(blogResponse.isBlogMain()),
                ()-&gt; assertEquals(&quot;testName&quot;, blogResponse.getBlogName()),
                ()-&gt; assertEquals(&quot;testNickName&quot;, blogResponse.getBlogMbNickName()),
                ()-&gt; assertEquals(&quot;testDescription&quot;, blogResponse.getBlogDescription()),
                ()-&gt; assertNotNull(blogResponse.getCreatedAt()),
                ()-&gt; assertNull(blogResponse.getUpdatedAt()),
                ()-&gt; assertTrue(blogResponse.getBlogIsPublic())
        );

    }
    ...
}</code></pre>
<p>단위 테스트는 개별 컴포넌트나 메서드를 격리된 환경에서 테스트하는 방식으로 위의 예시로 서비스 테스트의 경우 Repository는 이미 검증이 끝났기 때문에 Mockito를 사용해 가짜 객체를 만들어 Service 비즈니스로직에만 집중해서 테스트를 할 수 있다.</p>
<p>따라서 블로그 멤버 매핑도 단위 테스트로 진행하려 했다.</p>
<p><strong>통합테스트</strong>
여러 컴포넌트간의 상호작용을 테스트 </p>
<h3 id="하지만-블로그-멤버-매핑에서는-blog-member-role-등-여러-모듈이-합쳐지기-때문에-이를-통합테스트로-진행해야-하는-것이-아닌가">하지만 블로그 멤버 매핑에서는 Blog, Member, Role 등 여러 모듈이 합쳐지기 때문에 이를 통합테스트로 진행해야 하는 것이 아닌가?</h3>
<pre><code class="language-java">@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
@Transactional
class BlogMemberMappingServiceTest {
    @Autowired
    private BlogMemberMappingService blogMemberMappingService;

    @Autowired
    private BlogMemberMappingRepository blogMemberMappingRepository;
    ...
 }</code></pre>
<p>즉 실제 ID가 생성되고 관계가 맺어지는 것을 테스트해야 하므로 통합 테스트로 변경해서 진행했다.</p>
<p>테스트 목표인 블로그 멤버 매핑 서비스 코드를 다시 한번 보자</p>
<pre><code class="language-java">public class BlogMemberMappingServiceImpl implements BlogMemberMappingService {

    private final BlogMemberMappingRepository blogMemberMappingRepository;

    @Override
    @Transactional
    public void createBlogMemberMapping(Member member, Blog blog, Role role) {
        Optional&lt;BlogMemberMapping&gt; existingMapping =
            blogMemberMappingRepository.findBlogMemberMappingByMember_MbNoAndBlog_BlogId(
            member.getMbNo(), blog.getBlogId()
        );

        if(existingMapping.isPresent()){
            return;
        }

        BlogMemberMapping memberMapping = 
            BlogMemberMapping.ofNewBlogMemberMapping(
            member, blog, role
        );
        blogMemberMappingRepository.save(memberMapping);
    }
}</code></pre>
<p>이거 근데 보이드면 검증이 불가능하기 때문에 dto만들어서 BlogMemberMappingResponse를 반환형을 바꿔준다면?</p>
<p>확인해보니 void로 지정할 경우 </p>
<ol>
<li>단순성 : 구현이 간단하고 명확하고 메서드는 작업을 수행하고 끝난다</li>
<li>또한 오직 작업 수행에만 집중하고 결과 정보 생성에 대한 책임이 없다</li>
<li>CQS(명령 쿼리 분리 원칙) 디자인 원칙
이와 같은 장점이 있지만</li>
</ol>
<ul>
<li>메소드 실행 결과를 직접 검증할 수 없어 DB 조회 등 간접적인 방법으로 검증해야한다(Repository)</li>
<li>확장성이 제한되고 추가 조회 작업이 필요</li>
</ul>
<p>따라서 단순한 작업일 경우엔 void, 복잡한 비즈니스 로직이나 API에서는 DTO 반환 방식이 좀 더 유연하다고 할 수 있다. </p>
<h3 id="하지만-이-테스트만을-위해서-dto를-만들어주고-메소드-반환형을-수정하는게-과연-올바른가">하지만 이 테스트만을 위해서 dto를 만들어주고, 메소드 반환형을 수정하는게 과연 올바른가?</h3>
<p>테스트가 설계를 주도하는 것과 같은 상황에 다음과 같이 생각해보자</p>
<ul>
<li>API 사용성: 호출자에게 작업 결과에 대한 정보 제공이 필요한가?</li>
<li>비즈니스 요구사항: 실제 비즈니스 로직에서 반환값이 필요한 상황이 있는가?</li>
<li>확장성: 향후 추가 정보가 필요할 가능성이 있는가?</li>
<li>일관성: 다른 유사한 API들은 어떤 패턴을 따르고 있는가?</li>
</ul>
<p>등을 생각하다...가
블로그 멤버 매핑 서비스의 필요성에 대해서 생각하게 됐다.</p>
<p>당연히 모든 엔티티는 레포지토리, 서비스가 있을 것이다 라고 깊은 고민을 해보지 않은채 단정을 지었기 때문에 미쳐 놓쳐버린 것이다.</p>
<p>단순 CRUD vs 비즈니스 로직: <strong>서비스 = 비즈니스 로직, 레포지토리 = 데이터 액세스</strong>
만약 블로그 멤버 매핑이 단순한 데이터 저장/조회를 넘어 권한 검증, 비즈니스 규칙 적용 등이 필요하다면 서비스 계층이 적합하지만 지금 블로그 멤버 매핑은 간단한 데이터 저장.조회 뿐이기 때문에 레포지토리 계층에 끝내는게 맞는 것 같다. </p>
<h2 id="결론">결론</h2>
<ul>
<li>모든 엔티티에 동일한 계층 구조가 필요한 것은 아니며, 각 엔티티의 역할과 복잡성에 따라 적절한 구조를 선택해야함</li>
<li>블로그 멤버 매핑과 같은 단순 관계 매핑 엔티티의 경우, 복잡한 비즈니스 로직이 필요하기 전까지는 레포지토리 계층만으로도 충분</li>
<li>테스트 코드를 작성하며 기능 검증뿐만이 아닌 설계 자체에 대해 고민하는 시간이 되었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Web Front-end (HTML/CSS)]]></title>
            <link>https://velog.io/@_inho/Web-Front-end-HTMLCSS</link>
            <guid>https://velog.io/@_inho/Web-Front-end-HTMLCSS</guid>
            <pubDate>Tue, 07 Jan 2025 04:56:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/_inho/post/9bd4d97c-0f51-4e70-9aea-201af3ca137c/image.png" alt=""></p>
<pre><code>&lt;head&gt; 요소의 내용은 브라우저에 표시되지 않음
페이지에 대한 metadata를 포함

제목을 표시하는 &lt;title&gt;

&lt;title&gt;Datamotion Movie Database&lt;/title&gt;
파일 링크와 스크립트

CSS : &lt;style&gt; 태그로 사입

&lt;style&gt;
    p {
        font-family: helvetica, sans-serif;
        letter-spacing: 1px;
        text-transform: uppercase;
        border: 2px solid rgba(0,0,200,0.6);
        display: inline-block;
        cursor:pointer;
    }
&lt;/style&gt;
&lt;link&gt; 태그로 파일 참조

&lt;link rel=&quot;stylesheet&quot; href=&quot;sample.css&quot;&gt;
JavaScript : &lt;script&gt; 태그로 삽입

&lt;script&gt;
    const para = document.querySelector(&#39;p&#39;);
    para.addEventListener(&#39;click&#39;, updateName);
    function updateName() {
        let name = prompt(&#39;Enter a new name&#39;);
        para.textContent = &#39;Player 1: &#39; + name;
    }
&lt;/script&gt;
script src = 로 파일 참조

&lt;script src=&quot;sample.js&quot;&gt;&lt;/script&gt;
페이지에 대한 메타 데이터를 포함
인코딩 설정

&lt;meta charset=&quot;UTF-8&quot;&gt;
IE 호환성

&lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
페이지 설명

&lt;meta name=&quot;keywords&quot; content=&quot;movie&quot;&gt;
&lt;meta name=&quot;description&quot; content=&quot;Simple Movie Database&quot;&gt;
&lt;meta name=&quot;author&quot; content=&quot;Randy&quot;&gt;</code></pre><h2 id="open-graph-protocol-페이지에-대한-요약-정보">Open Graph Protocol 페이지에 대한 요약 정보</h2>
<p>웹사이트가 OGP 를 지원한다면, 웹사이트를 들어가기도 전에 뭐하는 사이트인지 미리 알 수 있습니다.
payco.com의 url을 카카오톡 or dooray 메신저에 붙여 넣으면 다음과 같이 확인 할 수 있습니다.
<img src="https://velog.velcdn.com/images/_inho/post/ca0f6f79-01e9-4ea8-b6dd-258dae89f843/image.png" alt=""></p>
<pre><code class="language-&lt;meta">&lt;meta name=&quot;og:image&quot; content=“http://image.toast.com/aaaaac/paycoNoti/payco_com.jpg&quot;&gt;
&lt;meta name=&quot;og:title&quot; content=&quot;PAYCO.COM 사는게 니나노PAYCO&quot;&gt;
&lt;meta name=&quot;og:description&quot; content=&quot;NHN 페이코의 간편결제 서비스, 착한소비 제로페이, 송금수수료 없는 제휴계좌, 매달 PAYCO포인트 리워드 혜택, 실적 조건 없이 적립되는 제휴카드, 실속있는금융 생활의 중심, PAYCO&quot;&gt;</code></pre>
<br>
<br> 


<hr>
<h1 id="character-sets-and-encodings">Character Sets and Encodings</h1>
<ul>
<li>글자나 기호들의 집합을 숫자로 정의한 것 </li>
</ul>
<h3 id="ascii">ASCII</h3>
<ul>
<li>영문 알파벳을 사용하는 대표적인 문자 인코딩</li>
<li>7bit</li>
<li>한글 표현 불가능<br>
### EUC-KR</li>
<li>한글 완성형 인코딩</li>
<li>8bit 문자 인코딩</li>
<li>한글은 2byte 사용하는 문자 집합</li>
</ul>
<h3 id="utf-8-유니코드">UTF-8 (유니코드)</h3>
<ul>
<li>전세계 모든 문자열을 하나의 코드표로 통합</li>
<li>한문자를 저장하기 위해 최소 1byte ~ 최대 4byte까지 동적으로 사용</li>
<li>조합형, 완성형 등이 있다</li>
</ul>
<h4 id="charset이-잘못되면-생기는-일">charset이 잘못되면 생기는 일</h4>
<p>charset = euc-kr, 파일은 UTF-8 인코딩이면..</p>
<p>또는 charset=utf-8 파일은 euc-kr 이면 ..</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
    &lt;head&gt;
        &lt;meta charset=&quot;euc-kr&quot; /&gt;
        &lt;title&gt;실습-02&lt;/title&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h1&gt;euc-kr 한글 인코딩 테스트&lt;/h1&gt;
    &lt;/body&gt;

&lt;/html&gt;</code></pre><p><img src="https://velog.velcdn.com/images/_inho/post/d876fa6e-aeb4-427b-bc37-34db3fe3df16/image.png" alt=""></p>
<hr>
<h2 id="html-문서를-표현하는-모든-tag는-두-분류중-하나에-속합니다">HTML 문서를 표현하는 모든 TAG는 두 분류중 하나에 속합니다.</h2>
<h4 id="inline-tag">Inline Tag</h4>
<p>자신의 내용과 앞뒤 태그의 내용을 같은 라인에 출력하는 태그</p>
<pre><code>대표적인 tag는 &lt;span&gt;&lt;/span&gt;

반드시 알아야할 테그

&lt;span&gt;, &lt;a&gt;, &lt;br&gt;
,&lt;button&gt;,&lt;img&gt;,&lt;input&gt;,&lt;select&gt;,&lt;textarea&gt;,&lt;label&gt;,&lt;strong&gt;
&lt;abbr&gt;, &lt;acronym&gt;, &lt;b&gt;, &lt;bdo&gt;, &lt;big&gt;, &lt;cite&gt;, &lt;code&gt;, &lt;dfn&gt;, &lt;em&gt;, &lt;i&gt;, &lt;kbd&gt;,&lt;map&gt;, &lt;object&gt;, &lt;q&gt;, &lt;samp&gt;, &lt;small&gt;, &lt;script&gt;,&lt;sub&gt;, &lt;sup&gt;,&lt;tt&gt;, &lt;var&gt;</code></pre><h4 id="block-tag">Block Tag</h4>
<p>자신의 내용과 앞뒤 태그의 내용을 다른 라인에 출력하는 태그(즉 좌우 너비가 100%)</p>
<pre><code>주로 구조를 만들 때 사용

대표적인 tag는 &lt;div&gt;&lt;/div&gt;

다만 &lt;p&gt; 태그는 내부에는 인라인 요소만 표현할 수 있습니다.

반드시 알아야할 테그

&lt;form&gt;, &lt;ul&gt;, &lt;p&gt;, &lt;table&gt;, &lt;div&gt;,&lt;address&gt;
&lt;h1&gt;,&lt;h2&gt;, &lt;h3&gt;, &lt;h4&gt;, &lt;h5&gt;, &lt;h6&gt;
&lt;article&gt;, &lt;aside&gt;, &lt;audio&gt;, &lt;blockquote&gt;, &lt;canvas&gt;, &lt;dd&gt;, &lt;dl&gt;, &lt;fieldset&gt;, &lt;figcaption&gt;, &lt;figure&gt;, &lt;footer&gt;,
&lt;header&gt;, &lt;hgroup&gt;, &lt;hr&gt;, &lt;noscript&gt;, &lt;ol&gt;, &lt;output&gt;, &lt;pre&gt;, &lt;section&gt;, &lt;video&gt;
</code></pre><hr>
<h2 id="html-text">HTML Text</h2>
<pre><code>&lt;p&gt; : 문단
&lt;br&gt; : new line
List
계층구조(목록)을 표현

순서 없는 목록 : &lt;ul&gt;, &lt;li&gt;

&lt;ul&gt;
    &lt;li&gt;우유&lt;/li&gt;
    &lt;li&gt;계란&lt;/li&gt;
    &lt;li&gt;빵&lt;/li&gt;
    &lt;li&gt;후무스(중동의 김치)&lt;/li&gt;
    &lt;li&gt;베이컨&lt;/li&gt;
&lt;/ul&gt;

순서 있는 목록(Ordered) : &lt;ol&gt;, &lt;li&gt;

&lt;ol&gt;
    &lt;li&gt;Avatar&lt;/li&gt;
    &lt;li&gt;Avengers: Endgame&lt;/li&gt;
    &lt;li&gt;Titanic&lt;/li&gt;
    &lt;li&gt;Starwars: Force Awaken&lt;/li&gt;
    &lt;li&gt;Avengers: Infinity War&lt;/li&gt;
&lt;/ol&gt;</code></pre><p><strong>중요(Emphasis)와 강조(Strong importance)</strong>
중요한 글자를 강조하기 위해 글자를 두껍게 표현하거나 기울여서 표현</p>
<pre><code>&lt;p&gt;&lt;em&gt;스래시 메탈&lt;/em&gt; 밴드로는 &lt;strong&gt;메탈리카&lt;/strong&gt;가 있습니다&lt;/p&gt; &lt;p&gt;그리고 &lt;strong&gt;메가데스&lt;/strong&gt;또한 말하지 않을 수 없죠.&lt;/p&gt;</code></pre><p><img src="https://velog.velcdn.com/images/_inho/post/99a98055-b59f-4364-b34f-d4e9f099745b/image.png" alt=""></p>
<p><strong>a tag</strong></p>
<pre><code>문법
&lt;a href=&quot;링크할 주소&quot;&gt;텍스트 또는 이미지&lt;/a&gt;

예제1
&lt;p&gt;영화 데이터베이스&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.imdb.com&quot;&gt;IMDB&lt;/a&gt;로 연결&lt;/p&gt;
&lt;p&gt;영화 데이터베이스 &lt;a href=&quot;http://www.imdb.com&quot; title=&quot;세계에서 가장 큰 영화 데이터베이스&quot;&gt;IMDB&lt;/a&gt;로 연결
&lt;/p&gt;</code></pre><hr>
<h3 id="url-과-path">URL 과 Path</h3>
<ul>
<li>URL(Unified Resource Locator) : 웹 상의 어디에 위치하는지 결정하는 텍스트 문자열</li>
<li>Path은 내부 파일을 찾기 위해 Path 사용
<img src="https://velog.velcdn.com/images/_inho/post/5c7ab020-7da3-4779-9126-8a6b300a2bf4/image.png" alt=""></li>
</ul>
<h3 id="table--테이블-">table ( 테이블 )</h3>
<p>웹 문서에서 자료를 정리할 때 가장 많이 사용하는 태그</p>
<pre><code>&lt;table&gt; 태그로 테이블을 시작

&lt;tr&gt; 태그로 테이블을 시작

&lt;td&gt; 태그로 행을 만듦

&lt;th&gt; 태그는 셀의 문자를 가운데 굵게 표시(제목에 사용)
</code></pre><table border="1">
    <tr>
        <td>아바타</td> <td>2009</td> <td>제임스 카메론</td>
    </tr>
    <tr>
        <td>어벤저스: 엔드게임</td> <td>2019</td>
        <td>루소 형제</td>
    </tr>
</table>

<pre><code>&lt;table border=&quot;1&quot;&gt;
    &lt;tr&gt;
        &lt;td&gt;아바타&lt;/td&gt; &lt;td&gt;2009&lt;/td&gt; &lt;td&gt;제임스 카메론&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;어벤저스: 엔드게임&lt;/td&gt; &lt;td&gt;2019&lt;/td&gt;
        &lt;td&gt;루소 형제&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;</code></pre><h4 id="행-합치기--colspan-열-합치기--rowspan">행 합치기 : colspan, 열 합치기 : rowspan</h4>
<pre><code>&lt;style&gt;
    *{
        font-size:20pt;
    }
    table,th,td {
        border: 1px double black;
        width: 800px;
    }

    .border-red{
        border: 1px double red;
        color:red;
    }
    .border-blue{
        border:1px double blue;
        color:blue;
    }
&lt;/style&gt;

&lt;table&gt;
    &lt;catpion&gt;전 세계 박스 오피스&lt;/catpion&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;제목&lt;/th&gt;
            &lt;th&gt;연도&lt;/th&gt;
            &lt;th&gt;감독&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;
            &lt;td&gt;아바타&lt;/td&gt;
            &lt;td&gt;2009&lt;/td&gt;
            &lt;td rowspan=&quot;2&quot; class=&quot;border-blue&quot;&gt;제임스 카메론&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;타이타닉&lt;/td&gt;
            &lt;td&gt;2002&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
            &lt;td&gt;어벤저스: 엔드게임&lt;/td&gt;
            &lt;td&gt;2019&lt;/td&gt;
            &lt;td&gt;루소 형제&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
        &lt;tr&gt;
            &lt;td colspan=&quot;3&quot; class=&quot;border-red&quot;&gt;www.boxoffice.com&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tfoot&gt;
&lt;/table&gt;</code></pre><p><img src="https://velog.velcdn.com/images/_inho/post/72af2c2a-c1a0-4529-a850-deb291f5668b/image.png" alt=""></p>
<hr>
<h2 id="element">Element</h2>
<h3 id="opening-tag--closing-tag">Opening tag , Closing tag</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/5f502db7-3d44-425f-9565-06ad22c2bca2/image.png" alt=""></p>
<h3 id="영역을-나누는-태그">영역을 나누는 태그</h3>
<ul>
<li><p>div</p>
<ul>
<li>Division의 약자, 웹 사이트의 레이아웃을 만들 때 사용하는 태그
웹 페이지에서 논리적 구분을 정의
각각의 블록(공간)을 알맞게 배치하고 CSS  스타일을 적용
Block level element
<img src="https://velog.velcdn.com/images/_inho/post/ecda754e-6423-4490-84bd-d0fb23c02850/image.png" alt=""></li>
</ul>
</li>
<li><p>span</p>
<ul>
<li>자체만으로는 어떠한 의미도 가지지 않음
class, id의 전역 속성으로 스타일링을 위해 요소들을 그룹화
Inline level element</li>
</ul>
</li>
</ul>
<ul>
<li>form
<img src="https://velog.velcdn.com/images/_inho/post/951550c8-f990-4009-9a8e-c5ecf8a33e03/image.png" alt=""></li>
</ul>
<h3 id="내용을-표현하는-태그">내용을 표현하는 태그</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/6c9b6858-7c0e-465f-96f3-7778bc006b56/image.png" alt=""></p>
<h3 id="semantic-tags">Semantic tags</h3>
<ul>
<li>의미 없는 div 태그의 사용보다 문서의 내용을 쉽게 이해할 수 있도록 의미를 가지는 새로운 태그요소
<img src="https://velog.velcdn.com/images/_inho/post/68dde18a-d126-4fe2-a5cd-3b5f8b125834/image.png" alt=""></li>
</ul>
<table border="1">
  <thead>
    <tr>
      <th>Tag명</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>&lt;main&gt;</td>
      <td>문서의 주요 콘텐츠를 포함, 문서 내에 단 하나만 존재</td>
    </tr>
    <tr>
      <td>&lt;header&gt;</td>
      <td>문서 소개나 탐색을 돕는 요소들의 그룹</td>
    </tr>
    <tr>
      <td>&lt;nav&gt;</td>
      <td>현재 페이지 내, 또는 다른 페이지로의 링크</td>
    </tr>
    <tr>
      <td>&lt;aside&gt;</td>
      <td>주요 내용과 간접적으로만 연관된 부분</td>
    </tr>
    <tr>
      <td>&lt;section&gt;</td>
      <td>문서의 일반적인 구획, 여러 줌심 내용을 감싸는 공간</td>
    </tr>
    <tr>
      <td>&lt;footer&gt;</td>
      <td>문서의 아래쪽 작성자 구획, 저작권 데이터, 관련된 문서의 링크에 대한 정보</td>
    </tr>
    <tr>
      <td>&lt;figure&gt;</td>
      <td>문서의 멀티미디어 요소</td>
    </tr>
    <tr>
      <td>&lt;article&gt;</td>
      <td>글자가 많이 들어가는 부분(그 자체로 독립적으로 구분되거나 재사용 가능한 영역)</td>
    </tr>
  </tbody>
</table>

<p><img src="https://velog.velcdn.com/images/_inho/post/8ab28a7c-68c1-4a94-876a-3c6ecf2b7078/image.png" alt=""></p>
<hr>
<h1 id="http-method">HTTP Method</h1>
<table border="1">
  <thead>
    <tr>
      <th>HTTP 메서드</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GET</td>
      <td>특정 리소스의 표시를 요청합니다. GET을 사용하는 요청은 오직 데이터를 받기만 합니다.</td>
    </tr>
    <tr>
      <td>HEAD</td>
      <td>GET 메서드의 요청과 동일한 응답을 요구하지만, 응답 본문을 포함하지 않습니다.</td>
    </tr>
    <tr>
      <td>POST</td>
      <td>특정 리소스에 엔티티를 제출할 때 쓰입니다. 이는 종종 서버의 상태의 변화나 부작용을 일으킵니다.</td>
    </tr>
    <tr>
      <td>PUT</td>
      <td>목적 리소스 모든 현재 표시를 요청 payload로 바꿉니다.</td>
    </tr>
    <tr>
      <td>DELETE</td>
      <td>특정 리소스를 삭제합니다.</td>
    </tr>
    <tr>
      <td>OPTIONS</td>
      <td>목적 리소스의 통신을 설정하는 데 쓰입니다.</td>
    </tr>
    <tr>
      <td>PATCH</td>
      <td>리소스의 부분만을 수정하는 데 쓰입니다.</td>
    </tr>
    <tr>
      <td>CONNECT</td>
      <td>목적 리소스로 식별되는 서버로의 터널을 맺습니다.</td>
    </tr>
    <tr>
      <td>TRACE</td>
      <td>목적 리소스의 경로를 따라 메시지 loop-back 테스트를 합니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="get-방식의-특징">GET 방식의 특징</h3>
<p>데이터가 URL에 노출됩니다</p>
<p>예시: <a href="https://httpbin.org/get?no=1&amp;id=marco&amp;age=30">https://httpbin.org/get?no=1&amp;id=marco&amp;age=30</a>
URL에 파라미터가 ?key=value 형태로 전달됨</p>
<p>데이터는 args 객체 안에 저장됩니다
URL에 데이터가 포함되어 있어 북마크가 가능합니다
데이터 크기에 제한이 있습니다 (브라우저마다 다르지만 보통 2048자)</p>
<h3 id="post-방식의-특징">POST 방식의 특징</h3>
<p>데이터가 URL에 노출되지 않습니다</p>
<p>예시: <a href="https://httpbin.org/post">https://httpbin.org/post</a>
데이터는 HTTP 요청 본문(body)에 포함됨</p>
<p>데이터는 form 객체 안에 저장됩니다
보안성이 상대적으로 높습니다 (URL에 데이터가 노출되지 않음)
대용량 데이터 전송이 가능합니다</p>
<h3 id="사용-용도">사용 용도</h3>
<p>GET: 데이터 조회(검색, 읽기)와 같은 단순 요청
POST: 데이터 생성/수정/삭제와 같이 서버의 상태나 데이터를 변경하는 요청</p>
<hr>
<h1 id="http-status-code">HTTP Status Code</h1>
<div class="container mx-auto p-4">
    <table class="w-full border-collapse border border-gray-300">
        <thead class="bg-gray-100">
            <tr>
                <th class="border border-gray-300 p-2">상태 코드</th>
                <th class="border border-gray-300 p-2">분류</th>
                <th class="border border-gray-300 p-2">설명</th>
                <th class="border border-gray-300 p-2">세부 코드</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td class="border border-gray-300 p-2 font-bold">1XX</td>
                <td class="border border-gray-300 p-2">정보 전달</td>
                <td class="border border-gray-300 p-2">요청을 받았고, 작업을 진행 중</td>
                <td class="border border-gray-300 p-2">웹 소켓에서 주로 사용</td>
            </tr>
            <tr>
                <td class="border border-gray-300 p-2 font-bold">2XX</td>
                <td class="border border-gray-300 p-2">성공</td>
                <td class="border border-gray-300 p-2">요청이 성공적으로 처리됨</td>
                <td class="border border-gray-300 p-2">
                    <ul class="list-disc pl-4">
                        <li><strong>200</strong> - OK (성공)</li>
                        <li><strong>201</strong> - Created (리소스 생성 성공)</li>
                        <li>202 - Accepted (요청 수락, 미처리)</li>
                        <li>203 - Non-Authoritative Information</li>
                        <li>204 - No Content (성공, 컨텐츠 없음)</li>
                    </ul>
                </td>
            </tr>
            <tr>
                <td class="border border-gray-300 p-2 font-bold">3XX</td>
                <td class="border border-gray-300 p-2">리다이렉션</td>
                <td class="border border-gray-300 p-2">요청 완료를 위해 추가 동작 필요</td>
                <td class="border border-gray-300 p-2">
                    <ul class="list-disc pl-4">
                        <li>301 - Moved Permanently (영구 이동)</li>
                        <li>302 - Found (일시적 이동)</li>
                    </ul>
                </td>
            </tr>
            <tr>
                <td class="border border-gray-300 p-2 font-bold">4XX</td>
                <td class="border border-gray-300 p-2">클라이언트 오류</td>
                <td class="border border-gray-300 p-2">잘못된 요청</td>
                <td class="border border-gray-300 p-2">
                    <ul class="list-disc pl-4">
                        <li><strong>400</strong> - Bad Request (잘못된 요청)</li>
                        <li><strong>401</strong> - Unauthorized (인증 필요)</li>
                        <li><strong>403</strong> - Forbidden (접근 거부)</li>
                        <li><strong>404</strong> - Not Found (리소스 없음)</li>
                        <li><strong>405</strong> - Method Not Allowed (허용되지 않은 메소드)</li>
                    </ul>
                </td>
            </tr>
            <tr>
                <td class="border border-gray-300 p-2 font-bold">5XX</td>
                <td class="border border-gray-300 p-2">서버 오류</td>
                <td class="border border-gray-300 p-2">서버 응답 불가</td>
                <td class="border border-gray-300 p-2">
                    <ul class="list-disc pl-4">
                        <li><strong>500</strong> - Internal Server Error (서버 내부 오류)</li>
                        <li>501 - Not Implemented (미구현 기능)</li>
                        <li>502 - Bad Gateway (게이트웨이 오류)</li>
                        <li>503 - Service Unavailable (서비스 이용 불가)</li>
                        <li>504 - Gateway Timeout (게이트웨이 시간 초과)</li>
                    </ul>
                </td>
            </tr>
        </tbody>
    </table>
</div>]]></description>
        </item>
        <item>
            <title><![CDATA[Maven Project 구조 파악하기]]></title>
            <link>https://velog.io/@_inho/Maven-Project-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_inho/Maven-Project-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 06 Jan 2025 01:56:42 GMT</pubDate>
            <description><![CDATA[<h2 id="빌드-도구-maven">빌드 도구 Maven</h2>
<ul>
<li>소프트웨어 개발에 있어 소스 코드를 실행할 수 있는 애플리케이션으로 만들어주는 도구</li>
<li>자동화, 일관성, CI/CD 통합, 코드 컴파일, 테스팅, 호환성, 배포, 패키징 등.. 많은 기능들을 제공</li>
<li>Maven은 Java의 대표적인 빌드 툴 중 하나이며 <strong>XML</strong>을 사용해 빌드 파일을 기술하며, 중앙 저장소를 이용해 편리한 의존 관계 라이브러리를 관리한다.</li>
<li>또한 일관된 디렉토리 구조를 제공하지만 기본적으로 maven에서 제공하지 않는 빌드 과정 추가가 복잡하다</li>
<li>플러그인 설정이 상이하거나 장황해질 경우 재사용성 및 확장성이 떨어지는 문제가 있다<br>

</li>
</ul>
<h3 id="설치">설치</h3>
<pre><code>brew install mvn</code></pre><p>Mac 환경을 사용하고 있기에 Homebrew를 통해 간단히 설치 했다.</p>
<p>또한 Java 11 / Java 21 버전을 기반으로 학습할 예정이다. <br><br></p>
<h3 id="프로젝트-생성">프로젝트 생성</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/173e3541-58d2-4e1e-a94c-dc4003133f3c/image.png" alt=""></p>
<p>이전 vscode에서 생성할 때와 달리 IntelliJ 툴을 이용하니 직관적으로 바로 Maven 프로젝트를 생성할 수 있었다.</p>
<p><br><br></p>
<h2 id="maven-프로젝트-구조">Maven 프로젝트 구조</h2>
<p><img src="https://velog.velcdn.com/images/_inho/post/d84f9d6b-c7d0-4e6c-9d34-ab41eeea39c2/image.png" alt=""></p>
<p>기본적으로 위와 같은 구조를 하는데 각 디렉토리는 다음과 같은 목적을 가지고 있다.</p>
<h3 id="src-디렉토리">src 디렉토리</h3>
<h4 id="srcmain">src/main</h4>
<ul>
<li>이 디렉토리는 프로젝트의 주요 소스 코드와 리소스를 포함합니다.<h4 id="srcmainjava">src/main/java</h4>
  •    프로젝트의 주요 Java 소스 코드가 위치합니다.
  •    여기에는 com/nhnacademy 패키지 구조가 있어, 해당 패키지 내에 Java 클래스 파일들이 위치하게 됩니다.<h4 id="srcmainresources">src/main/resources</h4>
  •    애플리케이션에서 사용되는 리소스 파일들이 위치합니다.
  •    설정 파일, 프로퍼티 파일, XML 파일 등이 이 디렉토리에 저장됩니다.<h4 id="srctest">src/test</h4>
  • 이 디렉토리는 테스트 관련 코드와 리소스를 포함합니다.<h4 id="srctestjava">src/test/java</h4>
  •    단위 테스트를 위한 Java 소스 코드가 위치합니다.
  •    JUnit 테스트 클래스 등이 이 디렉토리에 저장됩니다.<h3 id="target-디렉토리">target 디렉토리</h3>
  • 빌드 결과물이 저장되는 디렉토리입니다.<h4 id="targetclasses">target/classes</h4>
  •    컴파일된 클래스 파일들이 저장됩니다.
  •    com/nhnacademy 구조는 소스 코드의 패키지 구조를 반영합니다.<h4 id="targetgenerated-sources">target/generated-sources</h4>
  •    빌드 프로세스 중에 생성된 소스 코드가 저장됩니다.<h4 id="targetmaven-archiver">target/maven-archiver</h4>
  •    Maven 아카이브 관련 정보가 저장됩니다.<h4 id="targetmaven-status">target/maven-status</h4>
  •    Maven 컴파일 상태 정보가 저장됩니다.</li>
</ul>
<br>

<h3 id="pomxml">Pom.xml</h3>
<p>Maven의 기본 작업 단위.</p>
<p>Maven이 프로젝트를 빌드하는 데 사용하는 프로젝트 및 구성에 대한 세부 정보가 포함된 XML 파일 </p>
<p>Java 11 환경을 구성하기에 </p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;

    &lt;groupId&gt;com.nhnacademy&lt;/groupId&gt;
    &lt;artifactId&gt;Web&lt;/artifactId&gt;
    &lt;version&gt;1.0-SNAPSHOT&lt;/version&gt;

    &lt;properties&gt;
        &lt;maven.compiler.source&gt;11&lt;/maven.compiler.source&gt;
        &lt;maven.compiler.target&gt;11&lt;/maven.compiler.target&gt;
        &lt;mavn.compiler.release&gt;11&lt;/mavn.compiler.release&gt;
        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;/properties&gt;
    &lt;build&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
                &lt;version&gt;3.8.0&lt;/version&gt;
            &lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;
&lt;/project&gt;</code></pre><p>이와 같은 의존성을 추가해줬다.</p>
<h3 id="pomproject-object-model-주요-tag">POM(Project Object Model) 주요 Tag</h3>
<ul>
<li><p>project</p>
<p>  Maven의 XML Namespace를 지정합니다.</p>
</li>
<li><p>modelVersion</p>
<p>  Maven의 model Version</p>
</li>
<li><p>groupId</p>
</li>
</ul>
<pre><code>    group id</code></pre><ul>
<li><p>artifactId</p>
<pre><code>  artifact id</code></pre></li>
<li><p>name</p>
<pre><code>  project 이름</code></pre></li>
<li><p>url</p>
</li>
</ul>
<pre><code>프로젝트 정보를 다른 사람들이 프로젝트에 대한 자세한 정보를 찾을 수 있도록 안내

프로젝트 관련된 문서, 소스코드(git)등 관련된 정보를 제공</code></pre><ul>
<li><p>properties</p>
<p>  프로젝트에서 사용할 공통 속성</p>
</li>
<li><p>dependencies</p>
<p>  프로젝트가 참조하고 있는 라이브러리</p>
</li>
</ul>
<br>
<br>

<hr>
<h2 id="주요-maven-명령어">주요 Maven 명령어</h2>
<p><strong>mvn clean install</strong> : 이전 빌드 결과를 정리하고, 프로젝트를 새로 컴파일, 테스트, 패키징한 후 로컬 저장소에 설치합니다.
<strong>mvn clean package</strong> : 이전 빌드 결과를 정리하고, 프로젝트를 새로 컴파일, 테스트한 후 패키징합니다.
<strong>mvn clean test</strong> : 이전 빌드 결과를 정리하고, 프로젝트를 컴파일한 후 테스트를 실행합니다.</p>
<p><br><br></p>
<p><strong>mvn validate</strong> : 프로젝트 상태 점검 및 필요한 정보와 존재 여부 확인</p>
<p><strong>mvn compile</strong> : 프로젝트 소스 코드 컴파일</p>
<p><strong>mvn test</strong> : 컴파일된 소스코드에 대한 단위 테스트 실행</p>
<p><strong>mvn package</strong> : 컴파일된 코드를 JAR(Java ARchive), WAR(Web Archive) 파일로 패키징</p>
<p><strong>mvn verify</strong> : 패키지가 품질 기준에 적합한지 검사</p>
<p><strong>mvn install</strong> : 패키징된 JAR/WAR 파일을 로컬 Maven 저장소에 설치</p>
<p><strong>mvn site</strong> : 프로젝트에 대한 문서 사이트 생성</p>
<p><strong>mvn deploy</strong> : 패키징된 JAR/WAR 파일을 원격 저장소에 배포</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024년 회고]]></title>
            <link>https://velog.io/@_inho/2024%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@_inho/2024%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 31 Dec 2024 06:21:27 GMT</pubDate>
            <description><![CDATA[<h2 id="2024년-회고">2024년 회고</h2>
<p>내가 벌써 4학년이야 !! 했던 1월 1일의 기억이 선명한데 정신차리니 12월 31일이네요
아마 이번년도에 가장 많은 경험을 하지 않았을까 생각이 듭니다
좋은 사람들도 많이 만나고 꽤나 의미있게 보낸거 같아요 !</p>
<h3 id="2024---상반기">2024 - 상반기</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/2e6b991f-bcab-4a31-a180-14c1f2459074/image.png" alt=""></p>
<p>당시에는 꽤나 알차게 보냈던 기억이 남는 학기인데.. 특히 캡스톤을 2과목이나 수강하는 바람에 프로젝트 폭탄이였습니다.</p>
<p>가장 힘들었던게 USG 캡스톤 종합설계 과목이 참 ... </p>
<p><strong>시각 장애인을 위한 점자 해석 어플리케이션</strong> 이라는 주제인데 당시에는 막막한 이미지 처리 기술부터.. 매번 2명이서 팀플하다가 5명이라는 인원으로 진행하려니 꽤 막막했었습니다.</p>
<p>특히 이미지 회전 !!!! 이건 챗GPT, google, github 다 뒤져보면서 오픈소스도 찾아보고 이것저것 했지만.. 영문으로 했을 땐 쉽게 끝났을텐데 한글로 하려고 하니 점자 변환 부분에서도 꽤 애를 먹었네요
<em><del>피눈물의 흔적</del></em>
<img src="https://velog.velcdn.com/images/_inho/post/42487eb0-2188-4e31-bd1b-e5e6f2eb2c21/image.png" alt=""></p>
<p>*<em>발표날 *</em>
<img src="https://velog.velcdn.com/images/_inho/post/7432987e-5c6f-48ce-b556-92f6cb96307b/image.JPG" alt=""></p>
<p>비록 수상은 못했지만 공익성 있는 주제 선정부터 중간에 학교 캠퍼스 관련 문제로 ..usg 자격 상실 등 여러 이슈를 겪은 것 치고는 우리팀들 모두 고생 많았습니다 ^^</p>
<h3 id="2024---하반기">2024 - 하반기</h3>
<p>이후 2학기를 혼자 공부할지.. 아니면 다른 부캠을 갈지 고민 하던 중 2학년 때부터 고민하던 NHN Academy IoT/Backend 과정이 보여 바로 지원했습니다.</p>
<p>코테하고 제주도 가서 놀던 중 결과가 나오자마자 너무 기뻤고 가서 많은 경험을 할 수 있단 생각에 양산에 집도 정리하고 바로 김해로 이사 왔네요 !!</p>
<p>우선 여기 오자마자 느낀건 생각보다 제가 제대로 된 공부를 하고 있었던게 아니구나..와 깃허브 사용법을 제대로 숙지 하지 않아 기록을 안하고 있었는데.. 막상 딱 앉아서 끝까지 파보니까 어려운게 아니더라구요
<img src="https://velog.velcdn.com/images/_inho/post/a10ffa70-1552-465a-b0b7-2e7245e2333c/image.png" alt=""></p>
<p>이런식으로 코드 리뷰도 받고 내가 짠 코드의 목적성과 구조 등을 팀원들이랑 같이 검토하는 과정에서 되게 재미를 느꼈습니다.</p>
<p>지금까지 Java Basic / Thread / Network / DB / Python / IoT / ML 등을 배우는데 <img src="https://velog.velcdn.com/images/_inho/post/9992d49c-6832-4b08-9fa1-12d8280f59b4/image.png" alt=""></p>
<p>조금씩 기록하면서 12월 달에는 2주동안 8인 팀플도 했네요 !!</p>
<p>Node-Red라는 플랫폼을 카피한 Node-Blue 라는 자바 라이브러리를 만들어보자! 를 목표로 열심히 달렸습니다.</p>
<p>그래도 대학생 때 기록하는 습관들이 있어 Notion, Miro, Git 등을 활용할 때 도움이 됐네요
<img src="https://velog.velcdn.com/images/_inho/post/6b3c2a98-72bf-4f46-a21e-6d88596ccaaf/image.png" alt="">
간단히 팀 페이지 만들어두고 ..
<img src="https://velog.velcdn.com/images/_inho/post/43b1ad35-cc43-4c52-a061-a5d5ee815368/image.png" alt=""></p>
<p>매일 오전엔 스크럼을 진행하면서 어제 했던 것과 오늘 해야할 목표등을 설정해서 최대한 기간내에 각자 맡은 부분을 개발하는 것을 제일 중요하게 생각했네요 !!</p>
<p><img src="https://velog.velcdn.com/images/_inho/post/cf9db917-6d07-40d8-b798-7b9d5d3029f9/image.png" alt="">
그리고.. 설계 부분을 진행하면서 굉장히 어려움이 많았는데 처음엔 완벽할거 같아서 이젠 코드로 구현을 해보자 !! 였지만.. 바로 논리적인 문제가 생겨서 수정에 수정, 수정, 수정 ..... 찐찐찐찐막 설계도를 토대로 우선 구현부터 해나가자 !! (시간이 매우 부족했음)
한 결과로 위에 시스템 아키텍처와</p>
<p><img src="https://velog.velcdn.com/images/_inho/post/e16f45ca-f96b-416e-bdad-c390a11cc61a/image.png" alt=""></p>
<p>메인 프로젝트에서 Node-Blue를 사용한 흐름도를 구성했습니다.
<img src="https://velog.velcdn.com/images/_inho/post/32fe03fe-b507-4b82-8193-711e882a11dd/image.png" alt=""></p>
<p>그 중 상세히 제가 담당했던 부분이네요 !!
이 과목에서 저에겐 멀게만 느껴졌던 라즈베리 파이 모듈과.. 낯선 Flow-Based Programming 등 .. 자세히 공부해볼 수 있었네요 !! 가장 재밌었고 열정적이였던 과목 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/_inho/post/8f0cc71d-fa71-40c1-a084-c62c2d4e324a/image.png" alt=""></p>
<p>팀원간 전반적인 회고록인데 다음 팀플을 하게 되면 이 아쉬운 점을 개선한 팀이 되도록 좀 더 노력해봐야겠네요..</p>
<h2 id="결론">결론</h2>
<p>그동안 대학울타리 안에 갇혀 학점만 챙기며 진짜 노력이란 것을 해보지 않았던 저에겐 부트캠프에 와서 한계도 깨닫고 열정도 태우는 좋은 경험이 된거 같습니다 ! 내년 초부턴 Spring 본과정에 들어가는데 그땐 Velog, Git에 더 성실히 복습해서 기록하는 습관을 들이고 싶네요!</p>
<p>그리고 틈틈히 알고리즘 공부까지 !!</p>
<p>이렇게 마무리 하자니 대학생활 때 뭐가 남았나 할거 같은데 그래도 알차게 보냈습니다 ^^
<img src="https://velog.velcdn.com/images/_inho/post/ef8f8ded-ac5d-4794-913c-87bd589ec511/image.jpg" alt=""></p>
<p>2019.03 ~ 2020.02 컴퓨터 공학과 1학년 대표
2020.02 ~ 2021.10 대한민국 해군 작전사령부 독도함 전산병 만기 전역
2022.03 ~ 2023.02 컴퓨터 공학과 2학년 대표
2023.03 ~ 2023.12 22대 자치회장
<img src="https://velog.velcdn.com/images/_inho/post/5d4cd62f-c1d5-4ee5-a709-46802a918485/image.png" alt=""></p>
<p>2023.03 ~ 2024.08 AI 컴퓨터공학과 학회장 
2024.08 ~ ing NHN Academy A/IOT 2기</p>
<p>20대의 초반을 좋은 사람들, 좋은 경험으로 가득 쌓은거 같네요 
다들 2024년 고생 많으셨고 2025년에는 성실한 Velog로 돌아오겠습니다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Disk & File]]></title>
            <link>https://velog.io/@_inho/Disk-File</link>
            <guid>https://velog.io/@_inho/Disk-File</guid>
            <pubDate>Thu, 31 Oct 2024 07:56:17 GMT</pubDate>
            <description><![CDATA[<h2 id="db에서-query를-처리하는-과정"><strong>DB에서 Query를 처리하는 과정</strong></h2>
<p><img src="https://velog.velcdn.com/images/_inho/post/149d4add-b3c1-4f5b-9651-6434964c86a6/image.png" alt=""></p>
<pre><code>**Query**
⬇︎⬇︎
**Query Optimizer and Execution** : 쿼리 최적화기를 실행함으로써 데이터 구조&amp;통계를 이용해 효율적인 수행 계획을 수립 후 실행
⬇︎⬇︎
**Relation Operators** : 관계 연산자를 통한 데이터에 대한 질의 생성
⬇︎⬇︎
**Buffer Manager** : 버퍼관리자를 통해서 메모리의 버퍼에 적재된 주기억 장치 공간(HDD, SDD)의 데이터를 프레임 단위로 구분해서 관리
⬇︎⬇︎
**File and Access Manager** : 파일 관리자를 통해 여러 형태의 파일 내의 페이지를 추적 감시하여 한 패이지내에 정보들을 조직하는 방법을 수행
⬇︎⬇︎
**Disk Manager** : 데이터가 저장될 디스크 공간을 관리
⬇︎⬇︎
**DataBase**</code></pre><p>의 과정을 거친다.</p>
<p>레코드를 삽입 또는 삭제함에 따라 데이터베이스 역시 확장되고 축소되는데, 디스크 관리자는 어떤 디스크 블록들이 사용중이고, 어떤 페이지가 어느 디스크 블록에 있는지를 추적 감시하는데 DB를 사용하는 과정에 처음에는 빈 공간에 디스크에 블록들이 할당되지만 <strong>할당, 반환 등의 과정이 반복되며 순차적인 구조는 무너지고, 중간에 빈 공간이 생기게 된다</strong></p>
<p>이러한 빈 공간을 효율적으로 관리해야만 새로운 데이터를 잘 저장 할 수 있는데, 두가지 방법이 있다.</p>
<blockquote>
</blockquote>
<ol>
<li><p>블록 반환시마다 비어있는 공간의 포인터를 리스트에 저장해 나중에 사용할 수 있도록 한다.</p>
<ul>
<li>빈공간을 쉽게 찾을 수 있으며, 관리가 간단</li>
<li>연속된 큰 공간을 찾기 어려울 수 있다.</li>
</ul>
</li>
<li><p>각 디스크 블록마다 1bit 블록 사용 여부를 나타내는 비트맵을 유지</p>
<ul>
<li>연속된 빈 공간을 빨리 찾을 수 있으며, 전체 공간 상태를 한눈에 파악할 수 있다.</li>
<li>단, 데이터 구조에 따라 관리가 복잡할 수 있다.</li>
</ul>
<hr>
</li>
</ol>
<h2 id="버퍼-관리">버퍼 관리</h2>
<p>대부분의 데이터베이스는 주기억장치보다 큰 용량을 가지는데, CPU는 메모리에 적재된 데이터만 처리할 수 있다.
따라서 DBMS는 필요시 데이터를 주 기억장치에 적재하여, 언제 페이지를 교체해야하는지 결정해야 한다.
이 때 버퍼 관리자는 하드디스크에서 필요한 데이터를 가져와 메모리에 임시 저장해두는 역할을 한다.
예시로, 책장(하드디스크)에서 자주 보는 책을 책상(메모리)에 올려두는 과정과 유사하다.</p>
<h3 id="버퍼-풀">버퍼 풀</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/0d7f16eb-64bb-4332-b397-38498c238813/image.png" alt=""></p>
<p>메모리에서 데이터를 임시로 보관하는 공간이며, 여러개의 <strong>프레임</strong>으로 나누어져있다.
책상 위에 책을 올려놓을 수 있는 여러개의 공간을 나눈 것과 같은데, 각 칸은 두가지 정보를 가지고 있다.</p>
<blockquote>
<p><strong>프레임이 가지고 있는 정보</strong></p>
</blockquote>
<ul>
<li>pin_count : 특정 페이지가 사용중인 상태를 나타내며, 여러 트랜잭션이 동시 접근 가능함 (pin_count가 0이 아닌 경우 해당 페이지는 교체 대상에서 제외)</li>
<li>dirty : 데이터가 수정되었는지 표시하는 flag (수정시 1, 메모리의 페이지를 디스크에 기록 하기 전 dirty 상태의 페이지는 디스크에 반영해야 함)</li>
</ul>
<p><strong>간단한 버퍼 관리의 동작 과정</strong></p>
<ol>
<li>우선 서버로 데이터 요청이 들어오면 해당 데이터가 있는지 버퍼 풀에서 먼저 탐색한다.
2-1. 만약 빈공간이 있다면 바로 사용
2-2. 버퍼 풀에 없다면 새로운 공간을 할당해줘야 하는데 이때 pin_count가 0인 곳을 찾아 수정된 데이터(dirty == true)인 경우 하드디스크에 저장하고 새로운 데이터를 가져온다.</li>
</ol>
<p>자세히 정리하자면 버퍼 풀의 역할은
<strong>디스크 I/O를 줄이기 위해 메모리에 데이터를 캐싱해두는 핵심 공간이며, DBMS는 버퍼 풀에 데이터를 적재함으로써 디스크 접근을 최소화하여 성능을 최적화 시킨다. 즉, 자주 사용하는 데이터에 대해 디스크 대신 메모리에서 읽고 쓰는 작업을 수행</strong></p>
<p>또한 버퍼 풀의 크기는 DB 성능에 중요한 영향을 미치기에 버퍼 풀이 너무 작을 경우 페이지 교체가 빈번해지며(속도 저하?), 너무 크면 메모리 낭비가 발생하므로 최적의 크기를 찾는 것이 DB 성능 최적화를 위한 중요한 요소 중 하나이다.</p>
<h3 id="페이지">페이지</h3>
<p><img src="https://velog.velcdn.com/images/_inho/post/94028652-f94d-437d-a729-e80839bb41fc/image.png" alt=""></p>
<p>페이지는 버퍼 풀에 저장되는 레코드의 집합이며, <strong>디스크 -&gt; 메모리로 데이터가 전송되는 최소 단위</strong>이다.</p>
<p>페이지 역시 각 레코드가 저장되는 슬롯을 가지며, 각 슬롯에 레코드가 저장된다.</p>
<p>레코드는 &lt;페이지 번호&gt;, &lt;슬롯 번호&gt;의 쌍으로 식별되며 이 쌍을 RID(Record ID)라고 하며 레코드의 포인터 역할을 수행한다.</p>
<hr>
<h2 id="데이터-저장-및-관리">데이터 저장 및 관리</h2>
<p>대부분 자료구조에서는 저장된 데이터의 rid를 직접 알 수 없는데, 정렬되지 않은 자료 구조에서 동등 검색을 수행할 경우, 전체 자료 구조를 스캔 해야한다.</p>
<p>이때 <strong>index</strong>는 선택(Selection) 조건에 맞는 rid를 구할 수 있도록 만든 보조 자료구조인데 이를 활용한다.</p>
<h3 id="스토리지-엔진">스토리지 엔진</h3>
<p><strong>MyISAM(ISAM 기반)</strong></p>
<ul>
<li>색인 순차 접근 방식 파일</li>
<li>인덱스를 순차적으로 구성하여 큰 인덱스의 성능 문제를 해결<pre><code>파일 구조
.frm : 테이블 구조
.MYD : 실제 데이터
.MYI : 파일에 인덱스</code></pre>기본적인 연산인 삽입, 삭제, 검색은 매우 간단한데, 이중 동등 셀렉션 탐색의 경우 루트 노드부터 시작하여 크 기밧에 해당하는 레코드가 어떤 서브트리에 있을지 판단한다.</li>
</ul>
<p>ISAM에서 삽입, 삭제는 단말 페이지의 내용에만 영향을 주는데, 이 경우 하나의 단말 노드에 수많은 삽입이 수행될 때 오버 플로우 체인을 만들 수 있으며, 탐색시 그만큼 탐색해야 하므로 레코드를 찾아내는 시간에 상당한 영향을 미치게 된다.</p>
<p>이를 위해 페이지에 약 20%의 여유 공간이 있도록 트리를 만드는데, 이때도 트리가 가득 찼을 때 데이터 삽입 시 오버 플로우 체인이 만들어지게 될 것이다.
한편으론 단말 페이지만 수정될 수 있어 동시성 제어에서는 이점을 갖게 되는데, 일반적으로 어떤 페이지를 접근하는 요청자는 자신이 이 페이지를 사용하는 동안 다른 사용자들이 그 내용을 동시 수정 할 수 없도록 Lock을 건다.
페이지 수정시에도 그 페이지를 잠근 사용자가 없을 경우에만 허용되는 전용(exclusive) 잠금을 강제함으로써 그 페이지에 대한 접근을 시도하는 트랜잭션을 기다리도록 할 수 있다.</p>
<p>인덱스 계층 페이지를 잠글 필요가 없는 것은 , B+ 트리와 같은 동적 구조에 비해 커다란 이점이 되며, 데이터의 분포와 크기가 상대적으로 정적일 경우(오버 플로우 체인이 거의 없다면) ISAM은 B+ 트리보다 좋은 구조가 된다.</p>
<p>또한 외래키를 미지원 하며 테이블 레벨 잠금, 읽기 작업에 최적화 되어 있다고 볼 수 있다.</p>
<ul>
<li>시스템 장애시 수동 복구가 필요하며, 데이터 손상 위험이 있다.</li>
</ul>
<p><strong>InnoDB(B+Tree기반 )</strong>
ISAM과 같은 정적인 구조는 파일 커짐에 따라 오버 플로우 체인으로 인해 성능 저하의 단점이 있다.
따라서 삽입, 삭제시에도 깔끔히 조정되는 융통성 있는 동적 구조를 개발하게 되는데 이것이 B+Tree이다.</p>
<p>동적 균형 트리 구조를 가지고 있으며, 모든 단말 노드가 같은 레벨이다.</p>
<ul>
<li>균형 유지로 인해 일관된 성능을 보장하며</li>
<li>동적이기에 오버 플로우 체인이 없어 효율적인 데이터 관리가 가능하다
하지만 구조가 복잡하고 추가 공간이 필요하며, 읽기 성능은 상대적으로 낮다.</li>
</ul>
<pre><code>내부 구조
내부 노드 : 키 값, 포인터만 저장
리프 노드 : 실제 데이터 저장
- 리프 노드들은 Linked Llist로 연결되어 순차 접근이 용이</code></pre><p>자체 버퍼 풀을 사용해 데이터와 인덱스를 모두 캐시하며, ACID 특성을 완벽히 지원한다</p>
<blockquote>
<p>ACID 특성</p>
</blockquote>
<ul>
<li>원자성 : 트랜잭션은 완전히 실행 되거나, 완전히 취소 되어야 한다.</li>
<li>일관성 : 데이터의 무결성을 보장해야한다.</li>
<li>격리성 : 동시 실행 트랜잭션은 간섭을 방지해야한다.</li>
<li>지속성 : 완료된 트랜잭션은 영구히 보존되어야 한다.</li>
</ul>
<p>레코드를 탐색할 땐 루트부터 알맞은 단말 노드까지만 가면 되는데 이걸 높이라고 한다. 균형 트리이기에 어떤 단말 노드든 이 값은 같고 이러한 B+Tree는 레코드 파일이 자주 갱신되며, 정렬된 접근이 중요하면 데이터 엔트리로 레코드를 저장한 B+Tree를 쓰는것이 정렬 파일 이용시보다 성능이 우수하다.</p>
<p>인덱스 엔트리 저장을 위한 공간 오버헤드 대신 정렬 파일의 장점과 효과적인 삽입, 삭제 알고리즘의 장점을 모두 얻게 된다.</p>
<ul>
<li>자동 복구 기능과 크래시 복구를 지원해 데이터 안전성이 높다.</li>
</ul>
<p>** 비교하기 **
MyISAM은 테이블 레벨에서 잠금을 하며, InnoDB는 행레벨에서 잠금을 한다
MyISAM은 정적인 구조, InnoDB는 동적인 구조로 데이터를 관리하며 InnoDB의 경우 외래키, 트랜잭션을 지원한다.</p>
<p>즉 선택 기준을 정리하자면
단순히 <strong>읽기 위주의 작업이고 데이터의 분포, 크기가 상대적으로 정적일 경우</strong>엔 MyISAM이 B+ Tree보다 좋은 구조를 가지지만 보통 InnoDB는 <strong>쓰기, 수정 작업에 특화되어 있으며 트랜잭션이 필요하거나, 높은 동시성을 요구하며 빈번한 데이터 수정이 필요할 때</strong> 선택한다.
파일은 일반적으로 축소보단 확장되며, 이러한 활용도로 인해 ISAM 방식보다 B+Tree가 전반적으로 우수하다고 평가된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Thread ]]></title>
            <link>https://velog.io/@_inho/Thread</link>
            <guid>https://velog.io/@_inho/Thread</guid>
            <pubDate>Wed, 25 Sep 2024 07:13:39 GMT</pubDate>
            <description><![CDATA[<h2 id="thread-object-관리">Thread object 관리</h2>
<p><strong>관리 방법</strong></p>
<ol>
<li>생성 후 종료될 때 자동 삭제 되도록 하기</li>
</ol>
<ul>
<li>Thread는 Runnable interface의 run() 수행이 끝나면 종료되는데, 일정한 작업을 외부 간섭 없이 수행후 종료되면 운영상에 크게 문제가 없다</li>
<li>단 이경우 thread를 원하는 시점에 종료시키거나 관련 정보 확인이 어려움</li>
</ul>
<ol start="2">
<li>구현 되는 클래스 내에서 Thread object를 포함시켜 관리</li>
</ol>
<ul>
<li>Rubbale interface를 구현하는 class가 필요로 하는 Thread instance를 클래스 내에 포함시켜 관리하는데, </li>
<li>이 경우 생성된 object에서 자신과 관련된 Thread instance를 관리하기에, thread 제어와 관련된 처리가 가능</li>
</ul>
<ol start="3">
<li>Thread pool 이용
= 1번 방법과 유사하지만, Thread 생성과 삭제를 반복하는 것이 아닌 생성된 Thread instance를 활용하기에 자원 활용면에서 좋다</li>
</ol>
<ul>
<li>Thread Pool은 스레드 관리를 대신해주지만, 필요에 따라 특정 작업의 취소나 상태 확인 등은 가능하지만, Thread 관리에 대한 권한을 직접적으로 가지고 있지 못하므로 Thread 제어가 필요한 경우 사용이 제한될 수 있다</li>
</ul>
<h2 id="thread-class와-runnable-인터페이스-이용-차이">Thread class와 Runnable 인터페이스 이용 차이</h2>
<h3 id="class-확장">class 확장</h3>
<ul>
<li>다중 상속을 지원하지 않기에, 다른 class로 부터 추가적인 확장이 불가능</li>
<li>instance 생성 후 바로 실행할 수 있다</li>
<li>간단한 class여도 별도의 class 정의가 필요</li>
</ul>
<h3 id="interface-구현">Interface 구현</h3>
<ul>
<li>interface의 경우 여러개를 사용할 수 있기에 구현된 후에도, 해당 class의 확장이 가능</li>
<li>추가적인 Thread object가 있어야 한다(Instance 생성 후 바로 사용이 불가능)</li>
<li>Runnable interface는 functional interface로 Lambda로 구현 가능 </li>
</ul>
<hr>
<h2 id="thread-멈추기">Thread 멈추기</h2>
<p>Java Thread는 start()에 의해 시작 되지만 종료에 대한 명령은 없다.
초기엔 stop()메소드를 지원 했지만, thread가 실행중에 강제 종료시 thread 내부에서 리소스 정리를 제대로 할 수 없게 되며, lock을 해제 하지 않은 채 thread를 종료시켜 다른 thread에서 lock 획득을 위해 무한히 기다리는 deadlock 상태에 빠질 수 있다.</p>
<p>따라서 Thread를 안전하게 종료하기 위해서 thread내에서 확인만 가능하게 상태를 전달해 스스로 종료할 수 있도록 만들어야 한다.</p>
<h2 id="interrupt">Interrupt</h2>
<p>상태 제어를 통해 thread를 중지 시키려할 때 위와 같은 문제가 발생하는데, 이를 해결하기 위해서는 Java Thread 클래스에서 지원하는 <strong>sleep, wait</strong> 상태일 때 외부로부터 이벤트를 전달 받을 수 있는 Interrup가 있다.</p>
<p>Thread Class 상태 정보로도 사용되지만, sleep || wait 같은 대기 상태에서 exception을 발생시킨다.</p>
<hr>
<h2 id="thread-동시성-제어">Thread 동시성 제어</h2>
<h3 id="thread-동기화-문제">Thread 동기화 문제</h3>
<h4 id="1-race-condition경쟁-조건">1. Race Condition(경쟁 조건)</h4>
<ul>
<li>둘 이상의 thread가 동시에 공유 자원 접근시 발생</li>
<li>동시 접근함으로써 문제가 발생할 수 있는 구역을 **Critical Section(임계 구역)</li>
<li><ul>
<li>이라 함</li>
</ul>
</li>
<li>1번 Thread가 변수를 읽고, 2번 Thread도 변수에서 동일한 값을 읽는다.</li>
<li>이후 1,2번 Thread는 각 작업을 수행 후 변수에 마지막으로 값을 쓰기 위해 서로 경쟁하게 되고, 결국 마지막에 덮어쓴 thread 값이 저장 되어 원하는 결과를 얻지 못한다.</li>
</ul>
<h4 id="2-critical-section임계-구역">2. Critical Section(임계 구역)</h4>
<ul>
<li>두 스레드가 동시 접근하여 허용되지 않는 공유 자원에 접근하는 코드의 블록</li>
<li>Critical Section은 Thread에서 작업에 필요한 최소한의 시간만 유지되어야 하며, 작업이 완료된 후에는 반드시 해제 되어야함</li>
<li>따라서 한 thread가 critical section에 들어가 작업이 진행중이라며, 나머지 thread들은 해당 작업 완료까지 대기해야 한다.</li>
<li>Mutual Exclusion(상호배제)를 보장받기 위해서, critical section에 들어가거나, 나올 때를 위한 다양한 동시성 제어 메커니즘 제공</li>
</ul>
<h4 id="3-mutual-exclusion상호-배제">3. Mutual Exclusion(상호 배제)</h4>
<ul>
<li>두개 이상의 process or thread가 동시에 하나의 공유 자원으로 발생할 수 있는 race condition 문제를 해결하기 위해 어느 시점에서의 공유 자원 접근을 하나의 process 혹은 thread로 제한하는 것</li>
</ul>
<h4 id="4-deadlock교착-상태">4. Deadlock(교착 상태)</h4>
<p>mutual exclusion 과정에서 자원 접근 권한 획득과 자원 접근 권한 반환 관계의 꼬임으로 발생</p>
<p>** Hold and wait (점유 대기)**</p>
<ol>
<li>process 2가 resource 1의 접근 권한을 획득한 상태로 resource2의 접근 권한을 기다리는 상태</li>
<li>process2의 수행 과정이 resource 2의 접근 권한을 획득하여 처리 한 후 resource 1의 접근권한을 해제하면 process 3이 접근 권한을 해제하기 전까지 무한 대기 상태에 놓이게 됨</li>
<li>따라서 resource 1의 접근 권한을 요청하는 process 1도 무한대기 상태에 빠짐</li>
</ol>
<p>** Circular Wait(순환 대기) **</p>
<p>순환 대기의 경우</p>
<p> Process1 &amp;nbsp&amp;nbsp&amp;nbsp ⬅️&amp;nbsp&amp;nbsp&amp;nbsp Resource2
 &amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp⬇️    &amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp&amp;nbsp          ⬆️
 Resource1 &amp;nbsp&amp;nbsp&amp;nbsp➡️ &amp;nbsp&amp;nbsp&amp;nbspProcess2</p>
<ul>
<li>process 1은 Resource2에 대한 접근 권한을 가진 상태에서 Resource1에 대한 접근 권한을 기다림</li>
<li>process 2는 Resource1에 대한 접근권한을 가진 상태에서 Resource2에 대한 접근 권한을 기다림</li>
</ul>
<p>두개의 프로세스는 서로가 다른 process가 가지고 있는 접근 권한을 얻기 위해 대기하고 있어, 하나의 프로세스가 먼저 해제하지 않는 이상 대기 상태는 계속 유지됨</p>
<p>** Starvation(기아 상태) **
다른 process || thread가 공유 자원의 접근 권한을 지속적으로 가짐으로써, 우선 순위가 낮은 process || thread는 접근 권한을 획득할만큼의 수행 시간을 갖지 못해 무한 대기 상태에 놓일 수 있다.</p>
<h4 id="5-livelock">5. Livelock</h4>
<ul>
<li><p>deadlock  문제 해결을 위해 공유 자원 접근 요청 후 일정 시간 안에 권한 획득에 실패시 수행 과정을 종료하며 발생한다.</p>
</li>
<li><p>두개의 process나 thread에서 교착 상태를 유지하다가 일정 시간 후 자원 접근 요청을 철회시, 두개의 process || thread가 동시에 수행하여 자신이 확보하고 있던 공유 자원 접근 권한을 반환해 교착 상태를 해결</p>
</li>
<li><p>이경우 교착 상태처럼 아무런 작업도 하지 못하는 것은 아니지만, 해당 자원에 대한 접근 권한을 획득하지 못하므로 관련된 작업을 수행할 수 없다.</p>
</li>
<li><p>즉 교착 상태는 관련 process || thread가 대기 상태를 계속 유지함으로써 타 작업 수행이 불가능하지만, livelock의 경우 <strong>해당 자원에 대한 처리만 못하고 나머지 작업은 처리되는 차이</strong>가 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[점자 이미지 객체 감지 및 변환 : OpenCV & Numpy]]></title>
            <link>https://velog.io/@_inho/%EC%A0%90%EC%9E%90-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B0%9D%EC%B2%B4-%EA%B0%90%EC%A7%80-%EB%B0%8F-%EB%B3%80%ED%99%98-OpenCV-Numpy</link>
            <guid>https://velog.io/@_inho/%EC%A0%90%EC%9E%90-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B0%9D%EC%B2%B4-%EA%B0%90%EC%A7%80-%EB%B0%8F-%EB%B3%80%ED%99%98-OpenCV-Numpy</guid>
            <pubDate>Wed, 19 Jun 2024 06:55:25 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>프로젝트 전체 로직</p>
<ol>
<li>YOLO 모델을 통해 점자 객체를 탐지</li>
<li>이미지 전처리 후 </li>
<li>정면 이미지를 점자 해석 알고리즘에 INPUT</li>
</ol>
<p>이 중 실시간 YOLO 카메라를 통해서 들어오는 이미지를 3번 과정을 위해 정면으로 돌리는 역할을 맡게 되었다.</p>
<h2 id="이미지">이미지</h2>
<p>점자 이미지는 점의 배열 3*2(예외 케이스가 존재)로 구성되어 있고, 각 점은 돌출 형식으로 되어 있다.</p>
<p>이 예시 이미지를 기준으로 구현해보자.
<img src="https://velog.velcdn.com/images/_inho/post/209995e4-81fb-4ad6-b873-f6c15db28afe/image.png" alt=""></p>
<h2 id="이미지-정렬-변환-및-변환-과정">이미지 정렬 변환 및 변환 과정</h2>
<h3 id="1윤곽선-검출-및-경계-상자-설정-detect_object">1.윤곽선 검출 및 경계 상자 설정 detect_object</h3>
<ul>
<li>이미지를 GrayScale </li>
<li>이진화(threshold)를 통한 객체 추출</li>
<li>cv2의 findContours() 메소드를 통해 외곽선을 찾음</li>
<li>외곽선 길이 1%로 근사화 정확도 설정</li>
<li>외곽선을 다각형으로 근사화 </li>
</ul>
<p><img src="https://velog.velcdn.com/images/_inho/post/bda45acf-504d-4a33-8b2d-0150c9f14cb4/image.png" alt=""></p>
<pre><code class="language-Detected"> [  0  49]
 [  0 256]
 [359 228]]</code></pre>
<h3 id="2-경계-상자-기반-이미지-전처리-set_output_shape">2. 경계 상자 기반 이미지 전처리 set_output_shape</h3>
<ul>
<li>경계 박스의 크기를 계산</li>
<li>계산된 가로 세로 길이를 사용해 출력 이미지의 4개의 꼭짓점 좌표 설정</li>
<li><blockquote>
<p>변환 매트릭스 계산시 사용</p>
</blockquote>
</li>
</ul>
<pre><code>Output Shape: [[359   0]
 [  0   0]
 [  0 242]
 [359 242]]</code></pre><h3 id="3-이미지-정렬-warp_img">3. 이미지 정렬 warp_img</h3>
<ul>
<li>cv2.getPerspectiveTransform 메소드를 사용해 객체 경계 박스 점과 출력 형태 점들을 기반으로 변환 매트릭스 계산</li>
<li>wrapPerspective 메소드를 사용해 이미지 변환 </li>
</ul>
<p><img src="https://velog.velcdn.com/images/_inho/post/359f42d3-bcf0-42b6-aade-e99279219d3f/image.png" alt=""></p>
<hr>
<h2 id="이미지-워핑과-객체-감지-한계">이미지 워핑과 객체 감지 한계</h2>
<p>이 방법의 단점은 목표로 정한 예제 사진과 같이 밝은 배경에서는 잘 작동하지만 어두운 배경에서는 문제가 발생</p>
<ol>
<li>이진화 </li>
</ol>
<ul>
<li>밝은 배경에서는 객체, 배경간 명암 차이가 커서 이진화 작업이 효과적</li>
<li>어두운 배경에서는 객체와 배경의 명암 차이가 작아 이진화 작업이 잘 되지 않음 -&gt; 객체 감지 정확도 Down</li>
</ul>
<ol start="2">
<li>외곽선 감지</li>
</ol>
<ul>
<li>객체와 배경의 경계가 명확할 때 잘 작동하지만 어두운 이미지에서는 경계가 불분명해져서 외곽선 감지가 어려움</li>
</ul>
<ol start="3">
<li>타겟 경계 박스 설정</li>
</ol>
<ul>
<li>워핑 작업 수행시 타겟 경계 박스가 필요한데 어두운 배경에서는 1,2와 같은 이유로 타겟 박스 설정이 어려움</li>
</ul>
<h3 id="결론">결론</h3>
<p>예제 사진과 같은 밝은 배경은 정면으로 정렬시키는 작업은 성공했다. 하지만 어두운 배경에서 객체를 감지하여 정면 이미지 변환 작업은 해결하지 못했다.
밝은 배경에서는 비교적 쉽게 가능했지만 이를 위해서 이미지 명암비 조정이나 다른 정보들을 더 찾아보고 공부한 후에 완성해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GitHub가 처음인 사람들을 위해]]></title>
            <link>https://velog.io/@_inho/GitHub-%EC%89%BD%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@_inho/GitHub-%EC%89%BD%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 14 Mar 2024 07:52:59 GMT</pubDate>
            <description><![CDATA[<p><del>새학기가 됐음에도 깃 허브 배우는 것을 두려워한 나를 위해</del></p>
<blockquote>
<h3 id="macos에서-깃-허브-쉽게-설치하기">MacOS에서 깃 허브 쉽게 설치하기</h3>
</blockquote>
<p><img src="https://velog.velcdn.com/images/_inho/post/323bec4b-645b-4ba5-96cc-ad5f671516d0/image.png" alt=""></p>
<p>우선 터미널을 열어줍니다.</p>
<pre><code>❯ git --version</code></pre><p>터미널에 다음과 같이 입력했을 때 <img src="https://velog.velcdn.com/images/_inho/post/e434f437-9d1c-4420-ac8f-e57f2b0d3a66/image.png" alt="">
버전이 뜨지 않았다면 설치를 진행합시다.</p>
<pre><code>$ brew install git</code></pre><p>Homebrew를 이용하면 편하게 설치가 가능합니다.</p>
<p>이후 깃 허브 홈페이지(<a href="https://github.com/)%EC%97%90">https://github.com/)에</a> 방문해서 새로운 레포를 생성하면 끝 <img src="https://velog.velcdn.com/images/_inho/post/52656a01-b9dd-4257-9917-539364c2ec24/image.png" alt=""></p>
<hr>
<blockquote>
<h3 id="vscode와-연동하기">VsCode와 연동하기</h3>
</blockquote>
<p>처음에 깃을 사용하다보면 복잡한 명령어와 개념들이 와닿지 않습니다.
직접 이것저것 깨져보고 사용하다보면 익숙해져 가는데, 제가 상황에 맞게 쉽게 정리해보았습니다.</p>
<h3 id="ssh-키-발급">ssh 키 발급</h3>
<pre><code>1. 기존 발급 받은 ssh키가 있는지 확인하기
cat ~/.ssh/id_rsa.pub 

2. ssh key 생성하기
ssh-keygen

3. ssh 키 클립보드에 복사하기
pbcopy &lt; ~/.ssh/id_rsa.pub</code></pre><p>이후 gitHub 홈페이지 로그인 후 설정에 들어가서
<img src="https://velog.velcdn.com/images/_inho/post/31e1f846-6ef0-4dda-81bb-c09b9e4a4e41/image.png" alt=""></p>
<p>사진 속 SSH and GPG keys에 붙여넣기 하면 끝입니다.</p>
<h3 id="내-파일폴더-깃허브에-올리기">내 파일&amp;폴더 깃허브에 올리기</h3>
<pre><code>git init : git 초기화 
git remote -v : 로컬 ~ 원격을 잇는 정보확인
git remote add origin(변수) &quot;깃 레포지토리 주소&quot;</code></pre><p>레포지토리 주소는 깃 허브 레포 페이지로 이동 후 <img src="https://velog.velcdn.com/images/_inho/post/0edc40dc-460f-4a9b-892f-ee8683b66ca7/image.png" alt="">
이 버튼을 통해 쉽게 복사할 수 있습니다.</p>
<p>우선 여기까지 진행하면 이제 해당 주소의 레포지토리에 파일을 업로드할 수 있게 연결된 상태입니다.</p>
<pre><code>git branch : 현재 로컬 브랜치 확인
을 하면 보통 main이나 master 등으로 되어있는데 이 이름을 기억해둡시다.</code></pre><p>이제 VsCode에서 업로드를 원하는 파일 디렉토리로 이동합니다.
<img src="https://velog.velcdn.com/images/_inho/post/bcccb8aa-3741-4110-af2d-12f839fb1b89/image.png" alt="">
저의 경우 Python -&gt; 백준 폴더만 이동하고 싶었기 때문에 다음과 같이 이동하였습니다
<img src="https://velog.velcdn.com/images/_inho/post/79879f89-f12c-40a9-9af3-493346c1eb83/image.png" alt=""></p>
<p>1-1. git add . : 디렉토리 파일 전체 업로드
1-2. git add 파일명 : 해당 파일만 업로드</p>
<p>이 둘중 원하는 방식을 선택하여 터미널에 입력한 후 </p>
<ol start="2">
<li><p>git status 
를 입력하면 현재 상태를 보여줍니다.
저 같은 경우 변경사항이 없기 때문에 <img src="https://velog.velcdn.com/images/_inho/post/67356659-47de-4e85-9386-a6cdcab1bce7/image.png" alt="">
위의 사진과 같은 결과가 나왔습니다.</p>
</li>
<li><p>git commit -m &quot;Update:: 추가한 내용&quot;
이 단계는 커밋이라고 하는데, 진짜 진짜 마지막 저장 이라고 생각하면 됩니다.
<img src="https://velog.velcdn.com/images/_inho/post/e7c0d2ba-2687-4e4b-85f7-6d4f854b2ab5/image.png" alt="">
이 커밋 메세지는 사진속 &quot;디렉토리 정리&quot;와 같은 형태로 보여집니다.</p>
</li>
<li><p>git push origin master `
로컬 브랜치에 추가한 파일을 원격 브랜치(깃허브 홈페이지)에 저장
을 하면 끝입니다.</p>
</li>
</ol>
<h3 id="그-외에-경우">그 외에 경우</h3>
<ol>
<li>깃을 강제로 덮어 씌우기
예를 들면 깃 허브에는 Baekjoon이라는 폴더가 있고
로컬에는 그 폴더는 수정하여 Python, Data 등 새로운 내용으로 구성했을 경우 깃 허브에 레포지토리를 로컬 폴더 구조로 그대로 덮어 씌우는 방법입니다.</li>
</ol>
<p>git push origin master --force</p>
<ol start="2">
<li>깃 허브의 백준 폴더가 아닌 다른 폴더에 저장하고 싶다면?</li>
</ol>
<ul>
<li><p>기존 origin 원격 저장소 변경
git remote set-url origin 레포지토리링크</p>
</li>
<li><p>기존 origin 원격 저장소 제거 후 새로 추가 
git remote remove origin
git remote add origin 레포지토리링크</p>
</li>
<li><p>다른 이름으로 원격 저장소 추가 -&gt; 즉 origin 이라는 이름을 유지하며 추가적인 원격 저장소를 프로젝트에 등록할 때 
git remote add 아무이름 레포지토리링크</p>
</li>
</ul>
<ol start="3">
<li>branch 관련 명령어
git branch : 현재 로컬 브랜치 확인
git checkout -b main : main이라는 브랜치를 만듬과 동시에 이동
git branch : 현재 로컬 브랜치 재확인하고
git branch -D &lt;로컬 브랜치 이름&gt; : 브랜치 삭제</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1152번 & Python List comprehension]]></title>
            <link>https://velog.io/@_inho/%EB%B0%B1%EC%A4%80-1152%EB%B2%88-Python-List-comprehension</link>
            <guid>https://velog.io/@_inho/%EB%B0%B1%EC%A4%80-1152%EB%B2%88-Python-List-comprehension</guid>
            <pubDate>Thu, 07 Mar 2024 15:47:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/_inho/post/daadd8b5-1428-4e5f-96cd-a23784aec9e4/image.png" alt=""></p>
<h3 id="바보짓">바보짓</h3>
<p>백엔드 공부 + 프로젝트를 병행 하다보니 어느새 C, Java, Python 등.. 열심히 공부했던 내용들을 어느새 까먹고 있음을 인지해서 오늘부터 깃허브에 잔디를 심어보자 하고 백준 문자열부터 다시 풀이를 하고 있었다.</p>
<p>그래도 어느정도 도전과 실패를 반복하다보니 옛날 처음 백준을 접했을 때는 기초 문제마저 여러번 틀리고 그랬는데 오늘 해보니 1번만에 전부 통과를 했다 !! 단 이 문제 빼고..</p>
<p>백준 1152번 문제 단어의 개수를 보자마자 문제를 너무 대충 읽어서인가?</p>
<p>이런식의 코드를 작성했다</p>
<pre><code class="language-python">words = input(&#39;&#39;)
words = words.split(&#39; &#39;)
print(len(words))</code></pre>
<p><img src="https://velog.velcdn.com/images/_inho/post/756b91aa-9e25-4309-ae72-8b5bf4e52215/image.png" alt="">
테스트 케이스 1번만 보고 내가 아무렇게나 입력해봐도 올바른 단어 수가 나오길래 당당히 제출했는데 그렇다.. 당연히 오답이 나왔다
<img src="https://velog.velcdn.com/images/_inho/post/58eb2226-84dc-49ae-ab0c-1152f88a42c8/image.png" alt=""></p>
<p>깜짝 놀랐지만 바로 2번 테스트 케이스를 입력했는데 내 환경에서는 7이 나오더라 
자세히 보니까 맨 앞에 공백이 있었고, 저 상태로 split을 하게 되면 공백이기 때문에  <img src="https://velog.velcdn.com/images/_inho/post/49402528-069d-42b4-afe6-f61fe83a1caa/image.png" alt="">
이렇게 리스트의 요소로 공백이 추가 되어 내가 예상한 길이 +1만큼의 결과가 나온 것이다.</p>
<p>그럼 나는 이 공백을 없앨 방법을 생각해봤다.
내가 떠오른건 첫번째로</p>
<h4 id="1-filter-함수-이용">1. filter 함수 이용</h4>
<p>filter 함수의 1번째 매개 변수로 None을 주게 된다면, 공백이 사라진다.</p>
<pre><code class="language-python">words = input(&#39;&#39;)
words = words.split(&#39; &#39;)
words = list(filter(None, words))
print(len(words))</code></pre>
<p><img src="https://velog.velcdn.com/images/_inho/post/8cc724a1-b423-467b-8991-02d624ee8f1b/image.png" alt=""></p>
<p>이 외에도 다른 방법은 없을까? 고민하다가 리스트임을 고려해서 작년쯤 봤던 파이썬 알고리즘 책에서 공부했던 List Comprehension 개념이 떠올랐다.</p>
<h4 id="2-list-comprehension">2. List Comprehension</h4>
<p>우선 파이썬에서는 빈 문자열 &#39;&#39;이 <strong>False</strong>로 취급된다는 점을 알고 가야한다.</p>
<p>파이썬에서 False로 취급되는 것들</p>
<pre><code>빈 문자열 : &#39;&#39;
숫자 0
숫자 0.0
빈 리스트 : []
빈 튜플 : ()
빈 딕셔너리 : {}
&#39;None&#39;</code></pre><p>즉 위의 값들은 False로 취급하기 때문에 조건문이 실행되지 않는다.</p>
<p><strong>List Comprehension</strong>
구조는 다음과 같다</p>
<pre><code>[ 표현식 for 항목  in 반복 가능 객체 if 조건문 ]</code></pre><ul>
<li>표현식 : 리스트 새로운 요소를 정의 (변수, 함수 호출, 실제 값 등) 
예시로 num*2는 항목의 값을 두배로 하는 표현식</li>
<li>항목 : 반복 가능 객체로부터 하나씩 가져온 개별 요소</li>
<li>반복 가능 객체 : 리스트, 튜플, 문자열, 딕셔너리 등 반복을 통해 그 요소들을 하나씩 순회할 수 있는 모든 객체</li>
</ul>
<pre><code class="language-python">words = input(&#39;&#39;)
words = words.split(&#39; &#39;)
words = [w for w in words if w]
print(len(words))</code></pre>
<p>따라서 위의 코드와 같이 words의 요소들을 하나씩 반복하며 요소 w가 참일 경우만 리스트에 포함 시킴으로써 False를 반환하는 &#39;&#39; 공백은 걸러내는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/_inho/post/cdf3c11c-fe96-477d-931b-67c3032b93f9/image.png" alt="">
1번과 2번 방식 모두 속도는 비슷한 것 같다..??
아무튼 이 개념을 공부할 당시 내가 딱히 쓸 일이 있을까? 했는데 이런식으로 다시 만나게 될 줄은 몰랐다.</p>
<p>다시 알고리즘 공부를 시작하게 된 지금 앞으로 종종 책에 공부한 내용을 여기에 쉽게 정리하고 공유해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[node.js란?]]></title>
            <link>https://velog.io/@_inho/node.js%EB%9E%80</link>
            <guid>https://velog.io/@_inho/node.js%EB%9E%80</guid>
            <pubDate>Sat, 20 Jan 2024 19:27:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/_inho/post/36cba498-4cb8-4a99-a689-2409cab71600/image.png" alt=""></p>
<p>Nodejs는 JavaScript를 서버측에서 실행할 수 있게 해주는 것이라고 알고 천천히 살펴보자</p>
<h2 id="배경">배경</h2>
<p>이전에 JavaScript 엔진을 만들어 웹 브라우저가 JavaScript를 이해하고 실행할 수 있도록 개선되기 시작했다.
이 때 크롬 V8엔진을 중심으로 빠르게 발전하기 시작했는데, 2009년 Ryan Dahl에 의해 처음으로 발표됐다.</p>
<p>즉, Node js는 자바스크립트 런타임 환경이다.</p>
<p>기존 자바스크립트는 HTML에 종속된 언어이기 때문에 웹 브라우저에서 다음과 같이 사용해야 했다.
<img src="https://velog.velcdn.com/images/_inho/post/a7266724-11d9-4784-8b94-1989224b1574/image.png" alt=""></p>
<p>하지만 Node.js를 사용한다면 브라우저 없이도 쉽게 사용할 수 있다!</p>
<hr>
<h2 id="node-js-기본-구조와-특징">Node js 기본 구조와 특징</h2>
<p>Node js 애플리케이션은 메모리 힙과 콜 스택을 포함한다.</p>
<ul>
<li>메모리 힙 : 데이터를 저장하는 공간</li>
<li>콜 스택 : 실행중인 코드의 위치를 추적</li>
</ul>
<p>Node js는 비동기 언어인데, 그 차이점을 한번 비교해보자.</p>
<p><strong>동기 vs 비동기</strong></p>
<p>동기 : 요청을 보낸 후 해당 요청의 응답을 받아야만 다음 동작을 실행하는데, 이는 요청 처리가 완료될 때까지 기다려야 하므로 순차적이고 직렬적인 작업 처리에 적합</p>
<p>비동기 : 요청을 보낸 후 응답과 관계없이 다음 동작을 실행하는 방식으로 응답이 오는 대로 처리할 수 있기 때문에 병렬적인 작업 처리가 가능하며 시스템 자원을 효율적으로 활용 가능</p>
<p>Node.js는 자바스크립트 기반이므로 싱글 스레드이지만 그럼에도 불구하고 굉장히 빠르고 효율적인 서버를 만들 수 있다. 
세계적으로 유명한 기업들부터 우리나라의 대기업들도 많이 사용하는 것이 근거이다.</p>
<p><strong>그런데 Node.js는 싱글 스레드이지만 어떻게 좋은 성능을 낼 수 있을까?</strong></p>
<p>바로 Non-Blocking I/O와 Event-Driven 방식으로 되어있기 때문에 멀티스레딩 방식보다 더 효율적으로 I/O 처리를 담당할 수 있다.</p>
<hr>
<h3 id="non-blocking-io">Non-Blocking I/O</h3>
<p>입출력 작업을 할 때, 한 작업이 완료될 때까지 기다리지 않고 다음 작업을 바로 진행하는 방식이다. 쉽게 말하면 나는 음식점 요리사라고 생각해보자.</p>
<p>동기적 처리의 경우 내가 스테이크를 구울 때 10분이 걸린다면, 이 스테이크가 다 구워질 때까지 아무것도 하지 못하고 기다려야 한다.
비동기적 처리의 경우 스테이크를 굽기 시작하고 그동안 주문을 받는다거나 설거지를 한다.
이후 알람이 울려서 스테이크를 꺼내고 다시 다른 작업을 진행하는 방식이다.</p>
<p>이와 같이 비동기적 처리를 한다면 시스템 자원을 효율적으로 사용할 수 있고, 처리 속도도 빨라진다는 장점이 있다.</p>
<h3 id="event-driven">Event-Driven</h3>
<p>이벤트가 발생시 미리 정의된 콜백 함수를 실행하는 구조이다.
내가 음식점 알바생이라고 생각해보자.
손님 A가 물을 달라고 해서 물을 가져다주는 동안, 손님 B는 주문을 기다리고 있다.
그러면 나는 손님 B의 주문을 받고 주방에 가서 주문을 전달한다. 
그리고 다른 손님 C가 계산을 요청하면, 나는 계산을 처리하는데 이 때 이 모든 일들은 순차적으로 이루어지지 않아도 된다.
즉 결론적으로 어떤 이벤트(손님 요청)이 발생하면, 그에 맞는 적절한 행동(콜백 함수)을 하는거다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>Node js는 싱글 스레드이지만 위의 방식들을 이용해 멀티스레딩 방식보다 더 효율적으로 I/O처리를 할 수 있다.
이러한 구조 덕분에 높은 동시성을 요구하는 웹 애플리케이션 개발에 적합하다.
하지만 싱글 스레드 모델은 CPU 집약적인 작업에는 한계를 가지고 있다. 이미지&amp;비디오 처리, 대규모 데이터 처리와 같은 작업은 CPU를 많이 사용하기 때문에 Node.js는 적합하지 않다.</p>
]]></description>
        </item>
    </channel>
</rss>