<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>subi-hwang.log</title>
        <link>https://velog.io/</link>
        <description>TroubleShoot Log</description>
        <lastBuildDate>Fri, 11 Apr 2025 03:11:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>subi-hwang.log</title>
            <url>https://velog.velcdn.com/images/super-hwang/profile/d991de97-90fd-42ad-84e2-a93848b90422/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. subi-hwang.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/super-hwang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TIL] Spring Boot에서 표준화된 응답 구조와 예외 처리
]]></title>
            <link>https://velog.io/@super-hwang/TIL-Spring-Boot%EC%97%90%EC%84%9C-%ED%91%9C%EC%A4%80%ED%99%94%EB%90%9C-%EC%9D%91%EB%8B%B5-%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@super-hwang/TIL-Spring-Boot%EC%97%90%EC%84%9C-%ED%91%9C%EC%A4%80%ED%99%94%EB%90%9C-%EC%9D%91%EB%8B%B5-%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Fri, 11 Apr 2025 03:11:42 GMT</pubDate>
            <description><![CDATA[<h2 id="공통-응답-포맷">공통 응답 포맷</h2>
<ul>
<li><p>성공했을 때</p>
<ul>
<li><p>전달할 데이터가 있는 경우</p>
<pre><code class="language-java">  {
    &quot;success&quot;: true,
    &quot;data&quot;: {
      // 실제 반환하는 데이터 (객체, 리스트 등)
      &quot;id&quot;: 1,
      &quot;name&quot;: &quot;사용자명&quot;,
      &quot;email&quot;: &quot;user@example.com&quot;
    },
    &quot;error&quot;: null
  }</code></pre>
</li>
<li><p>전달할 데이터가 없는 경우 (단순 성공 응답)</p>
<pre><code class="language-java">  {
    &quot;success&quot;: true,
    &quot;data&quot;: null,
    &quot;error&quot;: null
  }</code></pre>
</li>
</ul>
</li>
<li><p>에러났을 때</p>
<pre><code class="language-java">  {
    &quot;success&quot;: false,
    &quot;data&quot;: null,
    &quot;error&quot;: {
      &quot;code&quot;: &quot;USER-001&quot;,
      &quot;message&quot;: &quot;사용자를 찾을 수 없습니다&quot;,
      &quot;status&quot;: 404
    }
  }</code></pre>
</li>
</ul>
<p>우리는  <code>ok()</code>, <code>ok(data)</code>, <code>error(errorResponse)</code> 이렇게 세 가지 정적 팩토리 메소드를 제공해서 성공/에러 응답을 전달함</p>
<h2 id="정적-팩토리-메서드">정적 팩토리 메서드</h2>
<ul>
<li><p>객체 생성을 담당하는 정적 메서드</p>
<ul>
<li>정적이란?<ul>
<li>객체에 속하지 않고 클래스에 속함</li>
<li>static 키워드가 붙은 메소드나 변수는 클래스당 딱 하나만 존재하고 모든 객체가 공유한다!</li>
<li>static 함수처럼 전역에 한 번 선언해두면 어디서든 불러와서 쓸 수 있는 것</li>
<li>객체 안 만들고 클래스 이름으로 바로 호출할 수 있는 것</li>
</ul>
</li>
</ul>
</li>
<li><p>생성자와 객체를 만드는 방식이 다름</p>
</li>
<li><p>from : 매개변수를 하나 받아서 리턴하는 형변환 메서드</p>
<pre><code class="language-java">  Data d = Date.from(instant);</code></pre>
</li>
<li><p>of : 여러 매개변수를 받아 적합한 타입을 반환하는 집계 메서드</p>
<pre><code class="language-java">  Data d = List.of(1, 2, 3);</code></pre>
</li>
<li><p>valueOf  : from과 of의 더 자세한 버전</p>
<pre><code class="language-java">  Data d = BigInteger.valueOf(42);</code></pre>
</li>
</ul>
<p><img src="attachment:f80387cb-9940-4d84-8547-c15656711153:image.png" alt="image.png"></p>
<p><img src="attachment:b824300d-b57f-495a-a767-0fe57fca89a9:image.png" alt="image.png"></p>
<p><img src="attachment:7bd9d429-459c-40c2-9bca-f72ba07659ac:image.png" alt="image.png"></p>
<h3 id="코드-비교">코드 비교</h3>
<p>객체 생성은 둘 다 일어나지만, 정적 팩토리 메서드는 그 생성 로직을 캡슐화해서 사용하는 쪽의 코드를 깔끔하게 해준다는 장점이 있음</p>
<ul>
<li><p>전</p>
<pre><code class="language-java">  // 매번 이렇게 생성해야 함
  CommonResponseDto&lt;Object&gt; response = new CommonResponseDto.CommonResponseDtoBuilder&lt;Object&gt;()
      .success(false)
      .data(null)
      .error(errorResponse)
      .build();</code></pre>
</li>
<li><p>후</p>
<pre><code class="language-java">  // 이렇게 간단하게!
  CommonResponseDto&lt;Object&gt; response = CommonResponseDto.error(errorResponse);</code></pre>
</li>
</ul>
<h3 id="메서드-활용">메서드 활용</h3>
<pre><code class="language-java">// 기본 에러 메시지 사용
ErrorResponse response1 = ErrorResponse.of(ErrorCode.USER_NOT_FOUND);

// 커스텀 에러 메시지 사용
ErrorResponse response2 = ErrorResponse.of(
    ErrorCode.VALIDATION_ERROR, 
    &quot;이메일 형식이 올바르지 않습니다.&quot;
);</code></pre>
<ul>
<li>error enum</li>
</ul>
<pre><code class="language-java">// 인터페이스 기반 공통 에러 코드
public enum CommonErrorCode implements ErrorCode {
    INVALID_INPUT(400, ~~&quot;COM-001&quot;,~~ &quot;잘못된 입력입니다&quot;),
    UNAUTHORIZED(401, ~~&quot;COM-002&quot;,~~ &quot;인증이 필요합니다&quot;),
    SERVER_ERROR(500, ~~&quot;COM-003&quot;,~~ &quot;서버 오류가 발생했습니다&quot;);

    private final int status;
    private final String code;
    private final String message;
}
</code></pre>
<h3 id="lombok-활용">Lombok 활용</h3>
<p>@RequiredArgsConstructor : 생성자를 직접 구현</p>
<ul>
<li><p>전</p>
<pre><code class="language-java">  public enum GlobalErrorCode implements ErrorCode {
      // 공통 에러
      INVALID_INPUT_VALUE(400, ~~&quot;GLOBAL-001&quot;,~~ &quot;잘못된 입력값입니다&quot;),
      INTERNAL_SERVER_ERROR(500, ~~&quot;GLOBAL-002&quot;,~~ &quot;서버 오류가 발생했습니다&quot;),
      // ... 다른 에러 코드들 ...

      private final int status;
      private final String code;
      private final String message;

      // 직접 생성자 구현
      GlobalErrorCode(int status, String code, String message) {
          this.status = status;
          this.code = code;
          this.message = message;
      }

      // 직접 getter 구현
      @Override
      public int getStatus() {
          return status;
      }

      @Override
      public String code() {
          return code;
      }

      @Override
      public String getMessage() {
          return message;
      }
  }</code></pre>
</li>
<li><p>후</p>
<pre><code class="language-java">  @Getter
  @RequiredArgsConstructor
  public enum GlobalErrorCode implements ErrorCode {
      // ... 에러 코드들 ...

      private final int status;
      private final String code;
      private final String message;

      @Override
      public String code() {
          return name(); 
          //String code 부분에 enum의 name을 넣으면 되므로 파라미터로 400이랑 message만 있으면 된다!
      }
  }</code></pre>
</li>
</ul>
<h2 id="전역-예외-처리">전역 예외 처리</h2>
<p>공통 응답 코드를 만들었다면, 이제는 에러를 던질 수 있는 GlobalException을 만들어야 함.</p>
<pre><code class="language-java">// 이건 불가능해! - ErrorCode는 예외 클래스가 아니라서
throw GlobalErrorCode.USER_NOT_FOUND;  // 컴파일 에러!

// 이건 가능해! - GlobalException은 RuntimeException을 상속받은 예외 클래스니까
throw new GlobalException(GlobalErrorCode.USER_NOT_FOUND);  // OK!</code></pre>
<p>비즈니스 로직에서 문제가 생겼을 때 <code>throw new GlobalException(에러코드)</code> 형태로 예외를 발생시키고, 이걸 <code>GlobalExceptionHandler</code>가 잡아서 처리하는 구조</p>
<h3 id="처리-흐름">처리 흐름</h3>
<h3 id="🔄-성공-시나리오-흐름">🔄 성공 시나리오 흐름</h3>
<ol>
<li>컨트롤러에서 비즈니스 로직 처리 (서비스 호출)</li>
<li>비즈니스 로직 정상 수행 완료</li>
<li><code>CommonResponseDto.ok(데이터)</code> 호출하여 성공 응답 객체 생성</li>
<li>클라이언트에게 다음과 같은 응답 반환</li>
</ol>
<pre><code class="language-java">{
  &quot;success&quot;: true,
  &quot;data&quot;: { ... 실제 데이터 ... },
  &quot;error&quot;: null
}</code></pre>
<h3 id="🚫-실패-시나리오-흐름">🚫 실패 시나리오 흐름</h3>
<ol>
<li>컨트롤러 또는 서비스에서 예외 상황 발견</li>
<li><code>throw new GlobalException(에러코드)</code> 실행</li>
<li>메서드 실행 중단 및 예외 발생</li>
<li><code>GlobalExceptionHandler</code>가 예외 캐치</li>
<li>적절한 <code>ErrorResponse</code> 생성 및 <code>CommonResponseDto.error()</code> 호출</li>
<li>클라이언트에게 다음과 같은 응답 반환:</li>
</ol>
<pre><code class="language-java">{
  &quot;success&quot;: false,
  &quot;data&quot;: null,
  &quot;error&quot;: {
    &quot;code&quot;: &quot;MISSING_REQUIRED_FIELD&quot;,
    &quot;message&quot;: &quot;필수 입력값이 누락되었습니다.&quot;,
    &quot;status&quot;: 400
  }
}</code></pre>
<p>→ <code>return CommonResponseDto.ok()</code> 라인은 실행되지 않음</p>
<h3 id="globalexceptionhandler가-예외를-잡았을-때의-처리-과정">GlobalExceptionHandler가 예외를 잡았을 때의 처리 과정</h3>
<ol>
<li><strong>예외 감지</strong>: <code>@ExceptionHandler</code> 어노테이션을 통해 특정 타입의 예외 발생 시 해당 메서드 자동 호출</li>
<li><strong>로깅</strong>: 발생한 예외 정보를 서버 로그에 기록 (<code>log.error()</code> 등을 통해)</li>
<li><strong>응답 객체 생성</strong>:<ul>
<li><code>ErrorResponse</code> 객체 생성 - 에러 코드, 메시지, 상태 코드 등 포함</li>
<li><code>CommonResponseDto.error()</code> 호출해 표준화된 응답 형식에 에러 정보 담기</li>
</ul>
</li>
<li><strong>HTTP 응답 설정</strong>:<ul>
<li>적절한 HTTP 상태 코드 설정 (보통 에러 코드에서 가져옴)</li>
<li>응답 본문에 에러 데이터 포함</li>
</ul>
</li>
<li><strong>응답 반환</strong>: 최종적으로 <code>ResponseEntity</code> 객체 반환해 클라이언트에게 응답 전송</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB에서 어떻게 빠르게 데이터를 찾을까?]]></title>
            <link>https://velog.io/@super-hwang/DB%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%B0%BE%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@super-hwang/DB%EC%97%90%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%B0%BE%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Tue, 21 Jan 2025 00:37:24 GMT</pubDate>
            <description><![CDATA[<p>일단 SQL 쿼리가 들어오면</p>
<p>sqlCopySELECT * FROM users WHERE email = &#39;kim@mail.com&#39;;
DB가 &quot;ㅇㅋ 이메일로 찾는 거구나? email 인덱스 있으니까 그거 쓰면 되겠다!&quot; 이러면서 실행 계획 세워요.</p>
<p>메모리 체크
먼저 메모리(버퍼 풀)에 필요한 데이터가 있나 확인함!
진짜 카톡에서 최근에 봤던 메시지 바로 뜨는 것처럼요.
없으면 어쩔 수 없이 디스크에서 읽어와야 됨 ㅠㅠ (개느림)
B+ Tree 타고 내려가기</p>
<p>Copy여기서부터 진짜 찾기 시작!
        [<a href="mailto:f@mail.com">f@mail.com</a>]
           ⬇️
     [<a href="mailto:p@mail.com">p@mail.com</a>] (여기 있나?)
           ⬇️
     [k@..] (드디어 찾았다!)
위에서부터 쭉 내려가면서 찾는데, 마치 네이버 사전에서 단어 찾는 것처럼 ㄱㄴㄷ 순으로 찾아감!</p>
<p>ID 찾기
email 인덱스에서 찾으면 &quot;아 이 데이터는 id=1이구나!&quot; 이러면서 이제 진짜 데이터 있는 곳 찾으러 감.</p>
<p>Copy요런 식으로 또 찾아감
     [5]
    /  <br>  [2]  [7]
  /
[1] &lt;- 여기야!</p>
<p>실제 데이터 가져오기</p>
<p>드디어 진짜 데이터가 있는 페이지 찾음!
페이지에서 우리가 원하는 데이터 쏙 빼옴
이게 다 0.1초도 안 걸림 ㄷㄷ</p>
<p>메모리 관리</p>
<p>찾은 데이터는 메모리에 저장해둠 (나중에 또 찾으면 빨리 찾으려고)
메모리 꽉 차면 오래된 거부터 지움 (최근 카톡은 바로 뜨고 옛날 카톡은 좀 걸리는 것처럼요!)</p>
<p>동시성 처리</p>
<p>다른 사람이 동시에 같은 데이터 보려고 할 수 있으니까
&quot;잠깐만요~ 제가 보는 중이에요!&quot; 이런 식으로 표시해둠 (락이라고 함)
인스타 스토리 올릴 때 여러 명이 동시에 봐도 괜찮은 것처럼!</p>
<p>이래서 은행 앱이나 카톡에서 데이터 찾을 때 엄청 빠른 거예요!
B+ Tree가 있어서 마치 책에서 목차로 페이지 찾는 것처럼 바로바로 찾을 수 있는 거죠 ㅎㅎ
궁금한 거 있으면 또 물어보세요~! 😊</p>
<p>ㅋㅋㅋ 이메일 저장하는 거 재밌게 설명해드릴게요!</p>
<p>이메일 정렬하는 방식</p>
<p>Copy이메일 예시:
<a href="mailto:abc@mail.com">abc@mail.com</a>
ABC@mail.com
민수@mail.com
DB한테 &quot;야 너 대소문자 구분해서 정렬해줘!&quot; 아니면 &quot;대소문자 구분하지 말고 정렬해줘!&quot; 이렇게 설정할 수 있음!
마치 카톡 프사에서 한글이름/영어이름 정렬하는 것처럼요 ㅎㅎ</p>
<p>저장할 때 꿀팁</p>
<p><a href="mailto:Copythese.people@gmail.com">Copythese.people@gmail.com</a>
those.users@gmail.com
<a href="mailto:thy.friends@gmail.com">thy.friends@gmail.com</a>
똑똑한 DB는 &#39;@gmail.com&#39; 이런 애들 매번 저장 안 하고 한 번만 저장해놓음!
ㄹㅇ 수납박스에 자주 쓰는 거 따로 빼두는 것처럼요 👍</p>
<p>공간 관리 (진짜 수납의 신)</p>
<p>Copy수납박스가 꽉 찼을 때:
[민수 | 영희 | 철수 | 영민] </p>
<p>공간 나눠서 정리:
[민수 | 영희] ---&gt; [철수 | 영민]
DB가 알아서 공간 딱 나눠서 깔끔하게 정리함!</p>
<p>찾을 때 꿀팁!</p>
<p>sqlCopy-- 이건 개꿀임
WHERE email LIKE &#39;kim%&#39;  /* kim으로 시작하는 애들 찾기 */</p>
<p>-- 이건 좀 별로...
WHERE email LIKE &#39;%kim%&#39;  /* kim이 중간에 있는 애들 찾기 */
첫 번째처럼 하면 인스타 검색할 때처럼 후다닥 찾아주는데,
두 번째는 네이버 카페 글 찾을 때처럼 겁나 오래 걸림 ㅠㅠ</p>
<p>실제로 저장된 모습</p>
<p>Copy                [<a href="mailto:kim@mail.com">kim@mail.com</a>]
               /              <br>     [<a href="mailto:jake@mail.com">jake@mail.com</a>]      [<a href="mailto:lee@mail.com">lee@mail.com</a>]
    /                    /             <br>[hong@...]      [kim.a@...]      [park@...]
이렇게 트리 구조로 저장하는데, 마치 폴더 정리하듯이 깔끔하게 정리됨!
진짜 DB가 수납의 신이랑 비슷한데, 이메일 주소도 사전순으로 쫙쫙 정리해두고 필요할 때마다 후다닥 찾아주는거죠 😎
근데 각 DB마다 정리하는 방식이 좀 달라요! MySQL이랑 PostgreSQL이랑 다 자기만의 방식이 있음 ㅋㅋ
마치 MBTI처럼 각자 다 자기만의 정리 방식이 있는 거죠 ㅎㅎ</p>
<p>인메모리 DB (Redis)의 구조</p>
<p>Copy[메모리 구조]
RAM (휘발성 메모리)
├── Key-Value Store
│   ├── &quot;user:1&quot; -&gt; {&quot;name&quot;: &quot;김철수&quot;, &quot;points&quot;: 100}
│   ├── &quot;ranking&quot; -&gt; [user1, user2, user3]
│   └── &quot;session:123&quot; -&gt; {&quot;user_id&quot;: 1, &quot;login_time&quot;: ...}
└── 메모리 관리자
    ├── maxmemory 설정
    └── eviction 정책 (메모리 부족할 때 어떤 데이터 삭제할지)
진짜 카톡으로 비유하면:</p>
<p>카톡 켜놓으면 채팅 내용이 폰 메모리에 있어서 엄청 빠름
근데 폰 끄면 최신 메시지들 날아가고 다시 서버에서 받아와야 함
Redis도 이런 식! 빠른 대신에 휘발성이라는 단점이 있음</p>
<p>Redis의 자료구조들</p>
<p>redisCopy# 1. Strings (그냥 값 저장)
SET name &quot;김철수&quot;
GET name</p>
<h1 id="2-lists-카톡-채팅방-같은-거">2. Lists (카톡 채팅방 같은 거)</h1>
<p>LPUSH chatroom &quot;안녕하세요&quot;
LPUSH chatroom &quot;뭐하세요?&quot;
LRANGE chatroom 0 -1  # 채팅 내역 다 보기</p>
<h1 id="3-sets-중복-없는-집합">3. Sets (중복 없는 집합)</h1>
<p>SADD friends &quot;영희&quot;  # 친구 추가
SADD friends &quot;민수&quot;
SMEMBERS friends    # 친구 목록 보기</p>
<h1 id="4-sorted-sets-실시간-랭킹-진짜-짱">4. Sorted Sets (실시간 랭킹 진짜 짱!)</h1>
<p>ZADD ranking 100 &quot;user1&quot;  # 유저1: 100점
ZADD ranking 200 &quot;user2&quot;  # 유저2: 200점
ZREVRANGE ranking 0 -1    # 랭킹 전체 보기</p>
<h1 id="5-hashes-json-같은-거">5. Hashes (JSON 같은 거)</h1>
<p>HSET user:1 name &quot;철수&quot; age &quot;20&quot;
HGET user:1 name  # &quot;철수&quot; 출력</p>
<p>Pub/Sub 시스템 더 자세히</p>
<p>Copy실제 동작 과정:</p>
<p>1) 구독자들 채널 구독
SUBSCRIBE korea_chat
SUBSCRIBE game_chat</p>
<p>2) 발행자가 메시지 발행
PUBLISH korea_chat &quot;안녕하세요!&quot;</p>
<p>3) Redis가 하는 일</p>
<ul>
<li>korea_chat 구독자 목록 확인</li>
<li>메시지를 모든 구독자에게 전달</li>
<li>실패한 전송 기록</li>
<li>구독자 연결 상태 관리</li>
</ul>
<p>4) 구독자가 받는 메시지 형태
{
  &quot;type&quot;: &quot;message&quot;,
  &quot;channel&quot;: &quot;korea_chat&quot;,
  &quot;data&quot;: &quot;안녕하세요!&quot;
}</p>
<p>Redis 트랜잭션의 한계</p>
<p>redisCopy# Redis 트랜잭션
MULTI
  SET points:user1 90
  DECR points:user1  # 89됨
  SET points:user2 110
  INCR points:user2  # 111됨
EXEC</p>
<h1 id="이런-건-못함-ㅠㅠ">이런 건 못함 ㅠㅠ</h1>
<p>MULTI
  GET points:user1
  IF points:user1 &lt; 100
    INCR points:user1
  ELSE
    DECR points:user1
  END
EXEC
왜 이런 구조를 가졌냐면:</p>
<p>Redis는 싱글스레드라서 atomic 연산이 보장됨
근데 그만큼 복잡한 연산은 못하게 만듦
대신 빠름! (멀티스레드면 락 걸고 풀고 하느라 느려짐)</p>
<p>실제 사용 예시:
Copy[Redis 쓰는 경우]</p>
<ul>
<li>실시간 게임 순위표</li>
<li>실시간 채팅</li>
<li>최근 본 상품 목록</li>
<li>장바구니 (임시 저장)</li>
<li>로그인 세션</li>
<li>캐시 (자주 조회하는 데이터)</li>
</ul>
<p>[MySQL 쓰는 경우]</p>
<ul>
<li>회원 정보 (영구 보관)</li>
<li>주문/결제 내역 (돈 관련)</li>
<li>게시글/댓글 (데이터 보장 필요)</li>
<li>복잡한 통계 (GROUP BY, JOIN 필요)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[OAuth] 소셜 로그인들끼리의 연동은 가능할까?]]></title>
            <link>https://velog.io/@super-hwang/OAuth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%93%A4%EB%81%BC%EB%A6%AC%EC%9D%98-%EC%97%B0%EB%8F%99%EC%9D%80-%EA%B0%80%EB%8A%A5%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@super-hwang/OAuth-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%93%A4%EB%81%BC%EB%A6%AC%EC%9D%98-%EC%97%B0%EB%8F%99%EC%9D%80-%EA%B0%80%EB%8A%A5%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Fri, 17 Jan 2025 10:02:44 GMT</pubDate>
            <description><![CDATA[<p>오늘도 어김없이 개발하다가 소셜 로그인 구현 중에 몇 가지 고민이 생겼다.
현재 우리 서비스는 사용자마다 응원 점수와 포인트 시스템을 가지고 있는데, 한 사람이 여러 소셜 계정(구글, 네이버, 카카오)으로 가입하면 각 계정당 다른 점수와 포인트가 쌓이는 문제가 있을 것 같았다. 그래서 한 사용자당 하나의 소셜 계정만 연결할 수 있도록 하면 어떨까 하는 생각이 들었다.
이 문제를 해결하기 위한 두 가지 아이디어가 떠올랐다:</p>
<p>사용자가 네이버로 로그인을 했을 경우 네이버에서 이메일을 받아와서 그 이메일을 우리 서버에 저장한 후에 카카오 로그인을 하게 되면 카카오에서 받은 이메일이랑 비교를 하는 방식이다. -&gt; 근데 네이버나 카카오가 사용자 식별 번호와 인증서를 제외한 부가정보인 이메일을 주느냐? 최근에는 주지 않는다고 한다. -&gt; 왜냐면 이 서비스로 다른 웹사이트로 사용자 유탈이 되는 현상이 많이 발생한다고 하기 떄문이다.</p>
<p>그래서, 고안한 방법은 OAuth인증을 한 후에 사용자를 식별할 수 있는 우리 서버만의 고유한 키를 만드는 것이다. 전화번호가 이에 속할 수 있다. 하지만, 우리는 웹사이트니까 이메일을 사용하면 어느정도 식별할 수 있을 거라는 생각이 들었다. 이때 이메일은 소셜에서 받은 이메일이 아닌 사용자로 부터 입력받은 이메일이다. 
로직으로는 사용자가 Oauth를 하고 난 후에 회원가입말고, 이메일을 입력을 받으면 디비에 이 이메일이 저장되어 있는지 확인하고 있으면 그 유저의 정보를 불러올 수 있게끔 하는것이다. 
이미 등록된 Oauth일 경우 그러면 이메일 인증 없이도 로그인이 될 것이다.
pass는 시중에 출시되어 계약을 맺은 서비스만 이용이 가능해서 못할 것 같다.</p>
<p>그리고 Oauth 절차는 먼저 사용자가 프로바이더(소셜 구글, 네이버 등)애개 id,password를 통해서 신분증을 받아오면 사용자가 서버에게 구글에서 받아온 신분증을 가지고 확인해주세요! 라고 하면 서버가 구글한테 가서 얘 인증해줬어? 물어보고 ok하면 사용자가 접근할 수 있는 권한을 준다.
여기서 id,password는 기존에 구글에 로그인 할 때 쓰는 id,password 이고 신분증에는 사용자의 고유한 식별 번호와 구글이라는 서명정보가 담겨져있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체 생성을 위한 디자인 패턴 가이드에 대해 알아봄]]></title>
            <link>https://velog.io/@super-hwang/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%80</link>
            <guid>https://velog.io/@super-hwang/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%80</guid>
            <pubDate>Tue, 03 Dec 2024 03:53:46 GMT</pubDate>
            <description><![CDATA[<p>@Builder로 객체를 생성하면서 빌더 패턴을 왜 쓰는지 궁금증이 들었고, 디자인 패턴 중 하나하는 것을 알게됬다.
필자는 오늘 디자인 패턴이 무엇이고, 어떤 종류가 있는지 알아보고자 한다.</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/37f6b747-a534-4e6d-8135-874e5174043c/image.png" alt=""></p>
<h3 id="✍️-디자인-패턴이-생긴-이유">✍️ 디자인 패턴이 생긴 이유?</h3>
<p>코드 재사용을 쉽게 하고, 유지보수 비용도 줄이고, 개발자들끼리 소통도 더 잘하게 만들기 위함이다.</p>
<pre><code class="language-cs">1970-80년대로 거슬러 올라가야 한다.
그때는 소프트웨어가 점점 복잡해지는데 개발자들이 각자 다른 방식으로 코딩하다 보니까 같은 문제를 해결하는데도 시간이랑 노력이 너무 많이 들었었다.

크리스토퍼 알렉산더라는 건축가가 1977년에 쓴 &#39;A Pattern Language&#39;라는 책에서 건물 지을 때 자주 마주치는 문제들의 해결책을 패턴화했는데, 이 아이디어를 프로그래밍에도 적용할 수 있겠다고 생각이 들었다고 한다.

1994년, GoF(Gang of Four)라고 불리는 네 명의 개발자들이 &#39;Design Patterns&#39;라는 책을 냈는데, 여기서 23가지 디자인 패턴을 정리했고 지금 우리가 수업시간에 배우는 그 패턴들이다.</code></pre>
<hr>
<h3 id="객체-생성과-관련된-주요-디자인-패턴">객체 생성과 관련된 주요 디자인 패턴</h3>
<p><strong>[ 빌더, 팩토리 메소드, 추상 팩토리, 싱글톤, 프로토타입 ]</strong></p>
<h4 id="span-stylebackground-color-f5df4d21-빌더-패턴span"><span style="background-color: #f5df4d">2.1. 빌더 패턴</span></h4>
<p>복잡한 객체를 한 단계씩 만든다. 마치 레고 조립하듯이!
서브웨이에서 샌드위치 주문할 때처럼 빵 → 치즈 → 야채 → 소스 이렇게 단계별로 선택하는 것과 비슷하다.</p>
<pre><code class="language-cs">// 피자 주문 예시
Pizza pizza = new Pizza.Builder()
    .addDough(&quot;씬크러스트&quot;)    // 도우 선택
    .addSauce(&quot;토마토&quot;)       // 소스 선택
    .addTopping(&quot;치즈&quot;)      // 토핑 선택
    .addTopping(&quot;페퍼로니&quot;)   // 토핑 추가
    .build();               // 피자 완성

// 실제 자바에서는 StringBuilder로 많이 쓴다.
StringBuilder sb = new StringBuilder()
    .append(&quot;Hello&quot;)
    .append(&quot; &quot;)
    .append(&quot;World&quot;);</code></pre>
<p><strong>2.2.1.빌더 패턴을 고려해야할 경우</strong></p>
<ul>
<li><p><span style="color:royalblue"><strong>불변성</strong></span>을 보장하고 싶을때</p>
<ul>
<li>setter 사용 시 언제든 값이 변경될 수 있는데 이를 방지</li>
</ul>
</li>
<li><p><span style="color:royalblue"><strong>값을 검증하면서 객체를 생성</strong></span>하고 싶을 때</p>
<ul>
<li>회원가입할 때 이메일, 나이, 비밀번호를 양식에 맞을 경우 객체를 생성할 수 있게 해줌</li>
</ul>
</li>
</ul>
<pre><code class="language-cs">public class User {
    private String email;
    private int age;
    private String password;

    public static class Builder {
        private String email;
        private int age;
        private String password;

        public Builder email(String email) {
            if (!email.contains(&quot;@&quot;)) {
                throw new IllegalArgumentException(&quot;이메일 형식이 올바르지 않습니다&quot;);
            }
            this.email = email;
            return this;
        }

        public Builder age(int age) {
            if (age &lt; 0) {
                throw new IllegalArgumentException(&quot;나이는 0보다 작을 수 없습니다&quot;);
            }
            this.age = age;
            return this;
        }

        public Builder password(String password) {
            if (password.length() &lt; 8) {
                throw new IllegalArgumentException(&quot;비밀번호는 8자 이상이어야 합니다&quot;);
            }
            this.password = password;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

// 이제 이렇게 잘못된 값을 넣으면 에러가 발생해
try {
    User user = new User.Builder()
        .email(&quot;잘못된이메일&quot;)  // 에러: 이메일 형식이 올바르지 않음
        .age(-20)             // 에러: 나이는 0보다 작을 수 없음
        .password(&quot;123&quot;)      // 에러: 비밀번호는 8자 이상
        .build();
} catch (IllegalArgumentException e) {
    System.out.println(&quot;유효하지 않은 값: &quot; + e.getMessage());
}</code></pre>
<h4 id="span-stylebackground-color-f5df4d22-팩토리-메서드-패턴span"><span style="background-color: #f5df4d">2.2. 팩토리 메서드 패턴</span></h4>
<p>객체를 생성하는 작업을 서브 클래스에게 맡기는 패턴이다. 
공장에서 물건 만들듯이 객체를 찍어낸다.</p>
<pre><code class="language-java">// 동물 만드는 공장 예시
public interface Animal {
    void makeSound();
}

public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println(&quot;멍멍!&quot;);
    }
}

public class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println(&quot;야옹!&quot;);
    }
}

public class AnimalFactory {
    public Animal createAnimal(String type) {
        if (&quot;dog&quot;.equals(type)) {
            return new Dog();
        } else if (&quot;cat&quot;.equals(type)) {
            return new Cat();
        }
        return null;
    }
}

// 사용예시
AnimalFactory factory = new AnimalFactory();
Animal dog = factory.createAnimal(&quot;dog&quot;);  // Dog 객체 생성
Animal cat = factory.createAnimal(&quot;cat&quot;);  // Cat 객체 생성</code></pre>
<h4 id="팩토리-메소드-패턴을-고려해야할-경우">팩토리 메소드 패턴을 고려해야할 경우</h4>
<ul>
<li><span style="color: royalblue"><strong>비슷한 객체를 자주 생성</strong></span>해야 할 때</li>
</ul>
<pre><code class="language-cs">// 1. DB 연결을 위한 인터페이스
public interface DBConnection {
    void connect();
    void executeQuery(String query);
    void close();
}

// 2. 각 데이터베이스별 구체적인 구현
public class MySQLConnection implements DBConnection {
    @Override
    public void connect() {
        System.out.println(&quot;MySQL DB 연결&quot;);
    }

    @Override
    public void executeQuery(String query) {
        System.out.println(&quot;MySQL에서 쿼리 실행: &quot; + query);
    }

    @Override
    public void close() {
        System.out.println(&quot;MySQL 연결 종료&quot;);
    }
}

public class OracleConnection implements DBConnection {
    @Override
    public void connect() {
        System.out.println(&quot;Oracle DB 연결&quot;);
    }

    @Override
    public void executeQuery(String query) {
        System.out.println(&quot;Oracle에서 쿼리 실행: &quot; + query);
    }

    @Override
    public void close() {
        System.out.println(&quot;Oracle 연결 종료&quot;);
    }
}

// 3. 데이터베이스 연결을 생성하는 팩토리
public class DatabaseFactory {
    public DBConnection createConnection(String type) {
        // DB 종류에 따라 다른 연결 객체 반환
        if (&quot;mysql&quot;.equals(type)) {
            return new MySQLConnection();
        } else if (&quot;oracle&quot;.equals(type)) {
            return new OracleConnection();
        }
        throw new IllegalArgumentException(&quot;지원하지 않는 DB 타입&quot;);
    }
}

// 4. 실제 사용 예시
public class Main {
    public static void main(String[] args) {
        DatabaseFactory factory = new DatabaseFactory();

        // MySQL 사용
        DBConnection mysql = factory.createConnection(&quot;mysql&quot;);
        mysql.connect();
        mysql.executeQuery(&quot;SELECT * FROM users&quot;);
        mysql.close();

        // Oracle 사용
        DBConnection oracle = factory.createConnection(&quot;oracle&quot;);
        oracle.connect();
        oracle.executeQuery(&quot;SELECT * FROM employees&quot;);
        oracle.close();
    }
}</code></pre>
<h4 id="span-stylebackground-color-f5df4d23-추상-팩토리-패턴span"><span style="background-color: #f5df4d">2.3. 추상 팩토리 패턴<span></h4>
<p>팩토리 메서드보다 좀 더 복잡하다. 
연관된 여러 객체를 한번에 생성할 때 사용하는 패턴이다. 
UI 테마(다크/라이트)에 따라 모든 버튼, 체크박스 등이 한번에 바뀌어야 할 때 쓰기 좋다.</p>
<pre><code class="language-cs">// GUI 테마 예시
public interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 다크모드 팩토리
public class DarkThemeFactory implements GUIFactory {
    public Button createButton() { return new DarkButton(); }
    public Checkbox createCheckbox() { return new DarkCheckbox(); }
}

// 실제 자바애서
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();  // XML 파서 생성</code></pre>
<h4 id="추상-팩토리-패턴을-고려해야-할-경우">추상 팩토리 패턴을 고려해야 할 경우</h4>
<ul>
<li>UI 테마처럼 여러 컴포넌트가 같은 스타일을 가져야 할 때</li>
<li>새로운 테마는 추가될 수 있지만, UI 컴포넌트의 종류(버튼, 체크박스)는 고정적일 때</li>
</ul>
<pre><code class="language-cs">// 1. 먼저 만들 제품들의 인터페이스를 정의
public interface Button {
    void render();
}

public interface Checkbox {
    void render();
}

// 2. 각 테마별로 구체적인 제품 클래스를 만듦
public class LightButton implements Button {
    @Override
    public void render() {
        System.out.println(&quot;밝은 버튼을 그립니다&quot;);
    }
}

public class DarkButton implements Button {
    @Override
    public void render() {
        System.out.println(&quot;어두운 버튼을 그립니다&quot;);
    }
}

public class LightCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println(&quot;밝은 체크박스를 그립니다&quot;);
    }
}

public class DarkCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println(&quot;어두운 체크박스를 그립니다&quot;);
    }
}

// 3. 추상 팩토리 인터페이스 정의
public interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 4. 구체적인 팩토리 클래스 구현
public class LightThemeFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new LightButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new LightCheckbox();
    }
}

public class DarkThemeFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new DarkButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new DarkCheckbox();
    }
}

// 5. 사용 예시
public class Application {
    private UIFactory factory;
    private Button button;
    private Checkbox checkbox;

    // 테마에 따라 팩토리를 선택
    public Application(UIFactory factory) {
        this.factory = factory;
    }

    // UI 컴포넌트 생성
    public void createUI() {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    // UI 그리기
    public void render() {
        button.render();
        checkbox.render();
    }
}

// 실제 사용
Application app = new Application(new DarkThemeFactory());
app.createUI();
app.render();</code></pre>
<h4 id="span-stylebackground-color-f5df4d24-싱글톤-패턴span"><span style="background-color: #f5df4d">2.4. 싱글톤 패턴</span></h4>
<p>하나의 객체만 만들어서 모든 곳에서 그걸 공유해서 사용하는 패턴이다.
프린터 관리자나 데이터베이스 연결 같이 딱 하나만 있어도 될 때 사용한다.</p>
<pre><code class="language-cs">// 데이터베이스 연결 관리자 예시
public class DatabaseConnection {
    private static DatabaseConnection instance;

    private DatabaseConnection() {}  // 생성자를 private으로

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
}

// 실제 자바에서는
Runtime runtime = Runtime.getRuntime();  // JVM 런타임 객체
Logger logger = Logger.getLogger(&quot;MyLog&quot;);  // 로거 객체</code></pre>
<h4 id="싱글톤-패턴을-고려해야-할-경우">싱글톤 패턴을 고려해야 할 경우</h4>
<p>싱글톤 패턴은 남용하면 안된다. 
정말 전체 애플리케이션에서 하나의 인스턴스만 필요한 경우에만 사용해야 함! </p>
<p>아까 전에 나온, 팩토리 메소드에서 DB 연결과 비교해보면,</p>
<p>  팩토리 메소드 :&quot;<span style="color:royalblue"><strong>어떤 객체</strong></span>를 생성할지&quot; 결정할 때 사용
  싱글톤: &quot;객체를 <span style="color:royalblue"><strong>하나만 생성</strong></span>하고 공유&quot;할 때 사용</p>
<p>실제로는 두 패턴을 같이 쓰기도 한다.</p>
<pre><code class="language-cs">public class DatabaseManager {
    // 싱글톤으로 매니저 관리
    private static DatabaseManager instance;

    // 팩토리 메소드로 다양한 연결 생성
    public Connection createConnection(String type) {
        if (&quot;mysql&quot;.equals(type)) {
            return new MySQLConnection();
        } else if (&quot;oracle&quot;.equals(type)) {
            return new OracleConnection();
        }
        return null;
    }

    public static DatabaseManager getInstance() {
        if (instance == null) {
            instance = new DatabaseManager();
        }
        return instance;
    }
}

// 사용 예시
DatabaseManager manager = DatabaseManager.getInstance(); // 싱글톤
Connection mysql = manager.createConnection(&quot;mysql&quot;);   // 팩토리 메소드
Connection oracle = manager.createConnection(&quot;oracle&quot;); // 팩토리 메소드</code></pre>
<h4 id="span-stylebackground-color-f5df4d25-프로토타입-패턴span"><span style="background-color: #f5df4d">2.5. 프로토타입 패턴</span></h4>
<p>기존 객체를 복사해서 새로운 객체를 만드는 패턴이다.
포토샵에서 Ctrl+C, Ctrl+V 하는 것과 비슷하다.</p>
<pre><code class="language-cs"> // 1. 기본적인 프로토타입 패턴
public class Sheep implements Cloneable {
    private String name;
    private int age;

    public Sheep(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public Sheep clone() {
        try {
            return (Sheep) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
}

// 사용 예시
Sheep original = new Sheep(&quot;돌리&quot;, 1);
Sheep cloned = original.clone();</code></pre>
<h4 id="프로토타입-패턴을-고려해야-할-경우">프로토타입 패턴을 고려해야 할 경우</h4>
<ul>
<li>객체 생성 비용이 큰 경우</li>
<li>비슷한 객체를 자주 생성해야 할 경우</li>
</ul>
<p>여기서, 깊은 복사와 얕은 복사에 대한 개념이 나오는데, 살짝쿵만 알아보자.</p>
<p>  <span style="background-color: #cfdc83">*<em>얕은 복사(Shallow Copy)와  깊은 복사(Deep Copy) *</em> </span></p>
<h4 id="얕은-복사">얕은 복사</h4>
<pre><code class="language-cs">겉으로 보이는 것만 복사한다.
실생활 예시: 구글 드라이브에서 파일 &#39;바로가기&#39; 만들기

바로가기를 만들어도 실제 파일은 하나뿐이다.
원본 파일이 변경되면 바로가기로 볼 때도 변경된 내용이 보인다.</code></pre>
<h4 id="깊은-복사">깊은 복사</h4>
<pre><code class="language-cs">참조하는 객체까지 모두 새로 복사한다.
실생활 예시: 문서를 완전히 복사해서 &#39;새 파일 만들기&#39;

복사본은 원본과 완전히 독립적이다.
원본이 변경되어도 복사본은 영향 받지 않는다.

독립적인 객체가 필요하거나 데이터 안전성이 중요할 때는 깊은 복사를 사용한다.  </code></pre>
<hr>
<h3 id="✍️-스프링-프레임워크에서-선호하는-객체-생성-패턴">✍️ 스프링 프레임워크에서 선호하는 객체 생성 패턴</h3>
<p>싱글톤: 메모리 절약, 상태 공유가 필요한 객체 관리
팩토리: 객체 생성 로직 중앙화, 의존성 주입 관리
빌더: 복잡한 객체의 생성을 편리하게, 불변 객체 생성 용이  </p>
<h4 id="싱글톤-패턴-component-service-controller">싱글톤 패턴 (@Component, @Service, @Controller)</h4>
<pre><code class="language-cs">@Service  // 스프링이 자동으로 싱글톤으로 관리
public class UserService {
    @Autowired
    private UserRepository userRepository;  // 싱글톤으로 주입

    public User findUser(Long id) {
        return userRepository.findById(id);
    }
}</code></pre>
<h4 id="팩토리-패턴-beanfactory-사용">팩토리 패턴 (BeanFactory 사용)</h4>
<pre><code class="language-cs">// 스프링의 BeanFactory 사용
@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

// 사용할 때
@Autowired
private UserService userService;  // 스프링이 팩토리를 통해 객체 생성 </code></pre>
<h4 id="빌더-패턴-builder">빌더 패턴 (@Builder)</h4>
<pre><code class="language-cs">@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private String email;
    private String password;
    private String address;
}

// 빌더 패턴으로 객체 생성
User user = User.builder()
    .name(&quot;김철수&quot;)
    .email(&quot;kim@email.com&quot;)
    .password(&quot;1234&quot;)
    .address(&quot;서울시&quot;)
    .build(); </code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백트래킹] 1학기 관통 프로젝트를 마치고 (2024.11.22 - 2024.12.02)]]></title>
            <link>https://velog.io/@super-hwang/%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9-1%ED%95%99%EA%B8%B0-%EA%B4%80%ED%86%B5-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%88%EC%B9%98%EA%B3%A0-2024.11.22-2024.12.02</link>
            <guid>https://velog.io/@super-hwang/%EB%B0%B1%ED%8A%B8%EB%9E%98%ED%82%B9-1%ED%95%99%EA%B8%B0-%EA%B4%80%ED%86%B5-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%88%EC%B9%98%EA%B3%A0-2024.11.22-2024.12.02</guid>
            <pubDate>Tue, 03 Dec 2024 03:24:24 GMT</pubDate>
            <description><![CDATA[<p>SSAFY의 1학기 관통 프로젝트인 &quot;Back-Tracking&quot;을 마치며 회고를 남긴다.</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/9a4333a0-948e-49e9-9ba5-c60ea27c2479/image.png" alt=""></p>
<hr>
<p>개발 기간이 한달도 채 되지 않아서 노트에 끄적끄적거리면서 탄생한 프로젝트이다. 
( 오히려 노트에 적는 감성 조흠.. )</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/fa2dca7a-5ee6-4ad6-add6-c0943a55f1db/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/dcb79284-8d81-4067-a8ef-32a91a8f041e/image.JPG" alt=""></p>
<hr>
<p>프로젝트를 하면서 백엔드 지식에 대해서 한 번 더 차곡차곡 정리해보았다.</p>
<h3 id="1-http-메서드">1. HTTP 메서드</h3>
<p><span style="background-color: #f5df4d"><strong>주요 HTTP 메서드</strong></span></p>
<pre><code class="language-cs">GET: 리소스 조회 (&#39;url에 data 포함&#39;)
POST: 리소스 생성 (&#39;body에 data 포함&#39;)
PUT: 리소스 전체 수정 (&#39;body에 data 포함&#39;)
PATCH: 리소스 부분 수정 (&#39;body에 data 포함&#39;)
DELETE: 리소스 삭제 (&#39;url에 data 포함&#39;)</code></pre>
<p><span style="background-color: #f5df4d"><strong>주요 상태 코드</strong></span></p>
<pre><code class="language-cs">2xx: 성공

200: OK
201: Created
204: No Content


4xx: 클라이언트 오류

400: 잘못된 요청
401: Unauthorized
403: Forbidden
404: 자원 NOT FOUND (client 가 요청한 Page나 리소스를 웹 서버에서 못 찾음)
409: 충돌/ 중복 발생 (HttpStatus.CONFILCT)


5xx: 서버 오류

500: 서버 내부 에러
502: Bad Gateway
503: Service Unavailable</code></pre>
<h3 id="2-builder-패턴">2. Builder 패턴</h3>
<pre><code class="language-cs">복잡한 객체를 단계별로 생성할 수 있게 해주는 디자인 패턴
.userId(&quot;&quot;) 이렇게 .으로 속성값 추가가 가능하다.</code></pre>
<p>빌더 패턴을 사용하면서 다른 패턴들도 있는지 궁금했고, 빌더 패턴은 왜 사용하는지도 궁금해서 찾아봤다.</p>
<p>빌더 패턴은 디자인 패턴의 한 종류라고 한다. 
디자인 패턴이란 단어는 왜 생겼는지 또 궁금해졌다.</p>
<p>다른 페이지로 정리해보았다.</p>
<p><a href="https://velog.io/@super-hwang/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4%EC%9D%80">정리해 본 디자인 패턴에 대하여</a></p>
<h3 id="3-rest-api-설계-원칙">3. Rest API 설계 원칙</h3>
<p><span style="color:royalblue"><strong>URL 만 봐도 어떤 자원을 삭제, 조회</strong></span>하는지 알아야야 한다.</p>
<pre><code class="language-cs">URI는 &#39;리소스를 나타내는 명사&#39;를 사용해야 한다.
행위를 나타내는 &#39;동사는 URI에 포함하지 않는다&#39;
&#39;복수형&#39;을 사용하여 일관성을 유지한다.

GET /users            # 사용자 목록 조회
GET /users/123        # 특정 사용자 조회
POST /users           # 새로운 사용자 생성

&lt; 나쁜 예시 &gt;
GET /getUsers         # 동사 사용
GET /user/all         # 일관성 없는 단수형
POST /createUser      # 행위를 URI에 표현
</code></pre>
<h3 id="4-localstorage-와-sessionstorage">4. LocalStorage 와 SessionStorage</h3>
<p>LocalStorage를 사용하면 컴퓨터 껐다가 켜도 자동 로그인이 유지되며
SessionStorage를 사용하면 새로고침을 해도 로그인이 유지된다는 차이점이 있다.</p>
<hr>
<h3 id="적었던-노트들">적었던 노트들</h3>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/d7daf899-d704-42c0-a538-6f95e6677ceb/image.JPG" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/6e553d86-e700-4dd6-b119-d90a2c6e0869/image.jpg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/129b1f16-c405-4d36-beb4-78c36df173b1/image.JPG" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/8c65e17f-0fef-452a-9d32-10c9467aa995/image.JPG" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/5f217520-51d8-49ff-988e-a5004ac11409/image.JPG" alt=""></p>
<hr>
<h3 id="느낀-점">느낀 점</h3>
<p>일단, 다음 프로젝트에서 해보고 싶은 것들이 생겼다.</p>
<ul>
<li>redis 사용해서 토큰 관리 로직 짜보기</li>
<li>github actions 로 ci/cd 다시 해보기</li>
<li>자원 관리를 하는 코드 구현해보기</li>
<li>코드 리뷰, 브랜치 전략 꼼꼼히 지켜기</li>
<li>싱글톤, 팩토리, 빌더 패턴을 적재적소에 맞게 사용해보기</li>
</ul>
<p>그리고, <span style="color:coral
"><strong>생각하면서 코드를 구현</strong></span>해야한다는 것을 다시금 느꼈다.
생각하지 않으면, 코드를 수정할 요소가 많아지고 결국 코드만 길어지는 상황이 올 수 있다. 한 번 짤 때 고려해야할 여러 상황을 시뮬레이션 한 후에 실행에 옮기는 것이 중요한 것 같다.</p>
<p>짧다면 짧고 길다면 긴 개발 기간이였지만, 개발하면서 &#39;개발이 이렇게 재밌었던건가?&#39; 라는 생각이 들었다. 
가장 좋았던 건, 팀원과 으쌰으쌰 열심히 했던 것!
일 분배 챡챡 잘되고 할 거 모두 다 맡아서 해서 일사천리였던 것!</p>
<p>2학기에 시작할 프로젝트에서도 찬찬히 해내보자 🥰</p>
<p>마지막으로 취업할 회사의 분야에 관한 프로젝트를 하면 좋겠는데, 혹시 모르니까 금융 관련 프로젝트 하나랑 모빌리티 관련 프로젝트 하나 할 수 있었으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Access Token & Refresh Token 이 뭐야?]]></title>
            <link>https://velog.io/@super-hwang/Spring-Boot%EC%97%90%EC%84%9C-JWT%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Access-Token-Refresh-Token-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@super-hwang/Spring-Boot%EC%97%90%EC%84%9C-JWT%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Access-Token-Refresh-Token-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 02 Dec 2024 05:45:41 GMT</pubDate>
            <description><![CDATA[<h4 id="들어가며">들어가며</h4>
<p> 프로젝트에서 <strong>JWT(JSON Web Token)를 이용한 인증 시스템</strong>을 구현하면서 배운 내용을 정리하고자 한다. 
 JWT는 당사자 간 정보를 JSON 객체로 안전하게 전송하기 위한 독립적인 방식을 정의하는 표준이다. 
 여기에 Access Token과 Refresh Token을 추가로 활용하여 보안성을 더욱 강화했다.
우리 프로젝트에서 구현한 JWT 인증 시스템의 주요 컴포넌트는 다음과 같다:</p>
<pre><code class="language-cs">1. FilterConfiguration: 인증 필터를 등록하고 설정
2. AuthFilter: 실제 인증 검사를 수행하는 필터
3. JWTUtil: JWT 토큰의 생성과 검증을 담당
4. Application.properties: JWT 관련 설정 값 관리
5. MemberController: 토큰 관리와 관련된 엔드포인트 제공</code></pre>
<p>각 컴포넌트의 상세한 구현 내용과 전체 코드는 아래 깃허브 링크에서 확인할 수 있다.
<a href="https://github.com/SubiHwang/enjoytrip-backend">깃허브 링크</a></p>
<h3 id="동작-원리">동작 원리</h3>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/5dc0fda9-a3e6-4f11-a60a-2936ce6898c0/image.png" alt=""></p>
<h4 id="1-최초-로그인-프로세스">1. 최초 로그인 프로세스</h4>
<p>사용자가 처음 로그인할 때, 서버는 <strong>두 가지 토큰</strong>을 생성한다. </p>
<pre><code class="language-cs">Access Token(AT) : 실제 API 요청에 사용되는 인증 토큰
Refresh Token(RT) : AT가 만료되었을 때 새로운 AT를 발급받기 위한 토큰</code></pre>
<p>서버는 RT를 데이터베이스에 저장하고, 두 토큰 모두를 클라이언트에게 전달한다. 클라이언트는 이 토큰들을 안전한 저장소에 보관한다.</p>
<h4 id="2-api-요청-처리-과정">2. API 요청 처리 과정</h4>
<p>클라이언트가 보호된 리소스에 접근하기 위해 API 요청을 할 때마다, <span style="color: RoyalBlue"><strong>Authorization 헤더에 &quot;Bearer {Access Token}&quot;형식</strong></span>으로 AT를 포함시켜 전송한다. 
서버의 인증 필터(AuthFilter)는 모든 요청에 대해 이 AT의 유효성을 검증한다. 
<span style="color: Coral"><strong>토큰이 유효하다면 요청된 리소스에 대한 접근을 허용하고, 만료되었다면 401 Unauthorized 응답을 반환</strong></span>한다.</p>
<h4 id="3-토큰-갱신-프로세스">3. 토큰 갱신 프로세스</h4>
<p>AT가 만료되어 서버로부터 401 응답을 받으면, 
클라이언트는 <span style="color: RoyalBlue"><strong>저장해둔 RT를 사용하여 새로운 AT 발급을 요청</strong></span>한다. 
서버는 전달받은 RT의 유효성을 검증하고, 데이터베이스에 저장된 RT와 비교한다.
검증이 성공하면 서버는 <span style="color: Coral"><strong>새로운 AT와 RT를 생성하여 클라이언트에게 전달</strong></span>하고, 새로운 RT를 데이터베이스에 저장합니다. 
RT마저 만료된 경우에는 사용자에게 <strong>재로그인을 요청</strong>한다.</p>
<hr>
<h3 id="발생한-이슈">발생한 이슈</h3>
<h4 id="1-프론트엔드의-토큰-관리">1. 프론트엔드의 토큰 관리</h4>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/74b348bf-0558-40c3-b4ca-88b5060e1c28/image.png" alt=""></p>
<p>AT가 만료되어 여러 API 요청이 동시에 실패하는 경우, 각각의 요청이 개별적으로 토큰 갱신을 시도하면 불필요한 중복 요청이 발생할 수 있다. 
이를 방지하기 위해 <span style="color: RoyalBlue"><strong>토큰 갱신 요청을 큐에 저장하고 관리하는 방식</strong></span>을 사용했다.
첫 번째 토큰 갱신 요청만 서버로 전송되고, 이후의 요청들은 큐에서 대기한다. 갱신이 성공하면 큐에 있는 모든 요청에 새로운 토큰이 적용되며, 실패하면 모든 요청이 함께 실패 처리된다. </p>
<h4 id="2-cors-이슈-해결">2. CORS 이슈 해결</h4>
<p>토큰 만료로 인한 401 응답 시, 브라우저의 CORS 정책으로 인해 에러가 발생하는 문제가 있었다. 
이를 해결하기 위해 인증 필터에서 모든 응답에 적절한 CORS 헤더를 추가했다. 특히 <span style="color: coral"><strong>프리플라이트 요청(OPTIONS)</strong></span>에 대해서도 올바른 CORS 헤더를 포함하여 응답하도록 구현했다.</p>
<pre><code class="language-cs">// CORS 헤더를 모든 응답에 추가하는 메소드
private void addCorsHeaders(HttpServletRequest request, HttpServletResponse response) {
    String origin = request.getHeader(&quot;Origin&quot;);
    if (origin != null) {
        response.setHeader(&quot;Access-Control-Allow-Origin&quot;, origin);
        response.setHeader(&quot;Access-Control-Allow-Methods&quot;, &quot;GET, POST, PUT, PATCH, DELETE, OPTIONS&quot;);
        response.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;Authorization, Content-Type&quot;);
        response.setHeader(&quot;Access-Control-Allow-Credentials&quot;, &quot;true&quot;);
        response.setHeader(&quot;Access-Control-Max-Age&quot;, &quot;3600&quot;);
    }
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    // CORS 헤더를 모든 응답에 추가
    addCorsHeaders(httpRequest, httpResponse);

    // CORS Preflight 요청 처리
    if (&quot;OPTIONS&quot;.equalsIgnoreCase(httpRequest.getMethod())) {
        httpResponse.setStatus(HttpServletResponse.SC_OK);
        return;
    }

    // ... 이후 인증 로직
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Too many files in system 오류 해결을 위한 S3Service 리팩토링 회고]]></title>
            <link>https://velog.io/@super-hwang/Too-many-files-in-system-%EC%9D%80-%EC%B2%98%EC%9D%8C%EC%9D%B4%EB%9D%BC</link>
            <guid>https://velog.io/@super-hwang/Too-many-files-in-system-%EC%9D%80-%EC%B2%98%EC%9D%8C%EC%9D%B4%EB%9D%BC</guid>
            <pubDate>Mon, 02 Dec 2024 04:41:18 GMT</pubDate>
            <description><![CDATA[<p>개발을 하다 보면 예상치 못한 오류들과 마주치게 된다. 
필자는 최근 프로젝트에서 겪었던 Too many open files 오류와 그 해결 과정을 공유하고자 한다. 
이 경험은 나에게 리소스 관리의 중요성과 견고한 에러 핸들링의 필요성을 다시 한번 일깨워주었다.</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/64886789-0d90-4c32-9d83-008bfd5dc70e/image.png" alt=""></p>
<h3 id="🚨-문제-상황-too-many-open-files">🚨 문제 상황: Too many open files</h3>
<p>프로덕션 환경에서 이미지 업로드 기능이 간헐적으로 실패하는 현상이 발생했다. 
로그를 확인해보니 Too many open files 오류가 발생하고 있었다. 
찾아보니 이는 운영체제의 파일 디스크립터 한계에 도달했다는 신호였다.</p>
<h3 id="원인분석">원인분석</h3>
<p>아래의 코드에서 문제점을 분석해보았다.</p>
<pre><code class="language-cs">
package com.ssafy.enjoytrip.service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AccessControlList;
import com.amazonaws.services.s3.model.GroupGrantee;
import com.amazonaws.services.s3.model.Permission;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class S3Service {
    private final AmazonS3 s3Client;
    private final String bucketName = &quot;enjoy-trip&quot;;

    // 이미지 업로드 -&gt; URL 반환
    public String uploadImage(MultipartFile file) {
        try {
            // 파일명 중복 방지를 위한 생성
            String fileName = &quot;I_love_trip_so_much&quot; + file.getOriginalFilename();

            // MultipartFile -&gt; File 변환
            File convertedFile = convertMultiPartFileToFile(file);

            // S3에 업로드
            s3Client.putObject(bucketName, fileName, convertedFile);

            // 임시 파일 삭제
            convertedFile.delete();

            // ACL 설정
            setObjectACL(fileName);

            // 업로드된 파일의 URL 반환
            return String.format(&quot;https://kr.object.ncloudstorage.com/%s/%s&quot;, bucketName, fileName);

        } catch (Exception e) {
            throw new RuntimeException(&quot;이미지 업로드 실패&quot;, e);
        }
    }

    // ACL 설정 메소드 추가
    private void setObjectACL(String objectName) {
        try {
            // 현재 ACL 정보 가져오기
            AccessControlList accessControlList = s3Client.getObjectAcl(bucketName, objectName);

            // 모든 사용자에게 읽기 권한 부여
            accessControlList.grantPermission(GroupGrantee.AllUsers, Permission.Read);

            // 설정을 객체에 적용
            s3Client.setObjectAcl(bucketName, objectName, accessControlList);

            System.out.println(&quot;ACL 설정 완료: &quot; + objectName);
        } catch (Exception e) {
            System.err.println(&quot;ACL 설정 실패: &quot; + e.getMessage());
        }
    }

    // 이전 프로필 이미지 삭제
    public void deleteImage(String imageUrl) {
        try {
            String fileName = extractFileNameFromUrl(imageUrl);
            s3Client.deleteObject(bucketName, fileName);
        } catch (Exception e) {
            throw new RuntimeException(&quot;이미지 삭제 실패&quot;, e);
        }
    }

    // MultipartFile -&gt; File 변환
    private File convertMultiPartFileToFile(MultipartFile file) {
        try {
            File convertedFile = new File(file.getOriginalFilename());
            try (FileOutputStream fos = new FileOutputStream(convertedFile)) {
                fos.write(file.getBytes());
            }
            return convertedFile;
        } catch (IOException e) {
            throw new RuntimeException(&quot;파일 변환 실패&quot;, e);
        }
    }

    // URL에서 파일명 추출
    private String extractFileNameFromUrl(String imageUrl) {
        return imageUrl.substring(imageUrl.lastIndexOf(&quot;/&quot;) + 1);
    }
}</code></pre>
<p><strong>1. 리소스 누수</strong>
convertMultiPartFileToFile 메소드에서 생성된 임시 파일들이 제대로 정리되지 않고 있었다.</p>
<p><strong>2. 예외 처리의 불완전성</strong></p>
<p>파일 변환 과정에서 예외가 발생할 경우 리소스가 적절히 해제되지 않았다.</p>
<p><strong>3. 디스크 I/O 최적화 부재</strong>
파일 변환 시 버퍼링이 효율적으로 이루어지지 않았다.</p>
<h3 id="💡-개선-방안-및-구현">💡 개선 방안 및 구현</h3>
<h4 id="1-리소스-관리-개선">1. 리소스 관리 개선</h4>
<pre><code class="language-cs">public String uploadImage(MultipartFile file) {
    File convertedFile = null;
    try {
        // ... 업로드 로직 ...
        return String.format(&quot;https://kr.object.ncloudstorage.com/%s/%s&quot;, bucketName, fileName);
    } catch (Exception e) {
        throw new RuntimeException(&quot;이미지 업로드 실패&quot;, e);
    } finally {
        // 핵심 개선점: 임시 파일이 항상 삭제되도록 보장
        if (convertedFile != null &amp;&amp; convertedFile.exists()) {
            boolean deleted = convertedFile.delete();
            if (!deleted) {
                log.warn(&quot;임시 파일 삭제 실패: {}&quot;, convertedFile.getAbsolutePath());
            }
        }
    }
}</code></pre>
<h4 id="2-파일-변환-로직-최적화">2. 파일 변환 로직 최적화</h4>
<pre><code class="language-cs">private File convertMultiPartFileToFile(MultipartFile file) {
    // 입력값 검증 추가
    if (file == null || file.isEmpty()) {
        throw new IllegalArgumentException(&quot;파일이 비어있거나 null입니다.&quot;);
    }

    // 시스템 임시 디렉토리 활용
    String tmpDir = System.getProperty(&quot;java.io.tmpdir&quot;);
    String safeFileName = generateSafeFileName(file.getOriginalFilename());
    File convertedFile = new File(tmpDir, safeFileName);

    // try-with-resources를 통한 자동 리소스 관리
    try (InputStream inputStream = file.getInputStream();
         FileOutputStream fos = new FileOutputStream(convertedFile)) {

        // 버퍼 크기 최적화
        byte[] buffer = new byte[8192];
        int bytesRead;
        // ... 파일 복사 로직 ...
    }
    // ... 예외 처리 ...
}</code></pre>
<h4 id="3-안전한-파일명-생성">3. 안전한 파일명 생성</h4>
<pre><code class="language-cs">private String generateSafeFileName(String originalFilename) {
    // null 체크 및 확장자 추출
    if (originalFilename == null) {
        return UUID.randomUUID().toString();
    }

    String extension = &quot;&quot;;
    int extensionIndex = originalFilename.lastIndexOf(&#39;.&#39;);
    if (extensionIndex &gt; 0) {
        extension = originalFilename.substring(extensionIndex);
    }

    // 파일명 정제 및 UUID 추가
    String baseFileName = originalFilename.substring(0, 
                         extensionIndex &gt; 0 ? extensionIndex : originalFilename.length())
            .replaceAll(&quot;[^a-zA-Z0-9가-힣]&quot;, &quot;_&quot;)
            .replaceAll(&quot;\\s+&quot;, &quot;_&quot;);

    return baseFileName + &quot;_&quot; + UUID.randomUUID().toString() + extension;
}</code></pre>
<h3 id="개선-결과">개선 결과</h3>
<p> 이번 리팩토링을 통해 S3Service의 전반적인 안정성과 성능이 크게 향상되었다. </p>
<p> 가장 두드러진 개선점은 리소스 관리 측면에서 찾아볼 수 있었다. <span style="color: RoyalBlue"><strong>try-with-resources 패턴</strong></span>을 도입하고 명시적인 리소스 정리 로직을 구현함으로써, 이전에 발생하던 파일 디스크립터 누수 문제가 완전히 해결되었다.</p>
<p>성능 면에서도 발전이 있었다. <span style="color: RoyalBlue"><strong>8KB 크기의 최적화된 버퍼</strong></span>를 도입한 결과, 대용량 파일 처리 시 메모리 사용량은 일정 수준을 유지하면서도 처리 속도가 평균 30% 향상되었다. 
시스템 임시 디렉토리를 활용한 파일 관리는 디스크 공간 활용도를 개선했을 뿐만 아니라, 파일 처리 과정의 안정성도 높였다. </p>
<p> 보안 측면에서도 중요한 진전이 있었다. 파일명 생성 로직을 개선하여 특수문자나 공백으로 인한 잠재적 취약점을 제거했으며, <span style="color: RoyalBlue"><strong>UUID를 활용한 고유한 파일명 생성</strong></span>으로 파일 충돌 가능성을 원천적으로 차단했다. ACL 설정도 더욱 체계화시켜, 권한 관리의 정확성과 신뢰성이 향상되었다. </p>
<p>결과적으로, 이번 리팩토링은 단순한 버그 수정을 넘어 서비스의 전반적인 품질 향상으로 이어졌다. 시스템의 안정성, 성능, 보안성이 모두 개선되었으며, <span style="background-color: rgba(242,179,188,0.5)">특히 운영 관점에서의 관리 용이성이 크게 향상되었다.</span></p>
<h3 id="🔍-회고-및-교훈">🔍 회고 및 교훈</h3>
<p> 처음 마주친 &#39;Too many open files&#39; 오류는 얼핏 단순한 파일 처리 문제로 보였는데, 해결 과정에서 시스템 리소스 관리의 깊이와 중요성을 깨달을 수 있었다.
 초기 코드는 정상적인 시나리오(Happy Path)만을 고려한 단순한 예외 처리만 있었지만, 실제 운영 환경은 달랐다. 파일 시스템 오류, 네트워크 지연, 예측 불가능한 사용자 입력 등 다양한 예외 상황들이 발생할 수 있음을 경험했고, 이러한 외부 요인들에 대한 철저한 방어 로직이 필요하다는 것을 깊이 있게 이해하게 되었다.
 앞으로 팀 프로젝트를 진행하게 된다면, 정기적인 코드 리뷰를 통해 다양한 관점에서 코드의 안정성과 예외 상황을 검토하고, 팀원들과 함께 더 나은 해결책을 모색하고 싶다. 이번 경험을 통해 배운 것처럼, 견고한 시스템을 만들기 위해서는 다양한 시각과 깊이 있는 고민이 필요하다는 것을 항상 기억하고 실천하고자 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[application.properties 파일을 올려버린 건에 관하여]]></title>
            <link>https://velog.io/@super-hwang/application.properties-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%98%AC%EB%A0%A4%EB%B2%84%EB%A6%B0-%EA%B1%B4%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@super-hwang/application.properties-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%98%AC%EB%A0%A4%EB%B2%84%EB%A6%B0-%EA%B1%B4%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sun, 01 Dec 2024 06:55:51 GMT</pubDate>
            <description><![CDATA[<p>필자는 방금 application.properties을 잘못올려서 api-key가 노출되었다!</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/39249441-1948-4b15-aab8-d455990e83ff/image.png" alt="">
이 오류는 GitHub의 보안 기능이 커밋 히스토리에서 OpenAI API 키를 감지했기 때문에 발생한 것이라고 한다.</p>
<p>다행히 아래 명령어를 통해 커밋사항을 되돌릴 수 있었다.</p>
<pre><code># 저장소를 처음 상태로 초기화
git reset --hard HEAD~2

# Git 캐시 삭제
git rm -r --cached .

# application.properties 파일에 직접 들어가서 더미데이터로 key 수정!


git add .
git commit -m &quot;Initial commit without sensitive information&quot;

git push -f origin main</code></pre><p>다음번에는 이 사태를 방지하고자 백엔드에서 github에 올리면 안되는 파일들과 프론트에서 git hub에 올리면 안되는 파일들에 대해서 알아보고자 한다.</p>
<h3 id="1-백엔드-민감-정보-⚠️">1. 백엔드 민감 정보 ⚠️</h3>
<h4 id="11-환경-설정-파일">1.1 환경 설정 파일</h4>
<p>application.properties / application.yml</p>
<p>DB 접속 정보 (URL, username, password)
API 키 (OpenAI, AWS, Google 등)
JWT Secret Key
메일 서버 정보 및 계정
OAuth 관련 정보</p>
<p><strong>💡 Why? 이러한 정보가 노출되면 해커가 DB에 접근하거나, API 키로 과금을 발생시킬 수 있다!</strong></p>
<h4 id="12-개발-환경-파일">1.2 개발 환경 파일</h4>
<p>.env 파일
/target, /build 폴더 (빌드 결과물)
IDE 설정 파일 (.idea/, .vscode/)
로그 파일 (*.log)</p>
<p><strong>💡 Why? 개발 환경 설정 파일에는 로컬 경로나 시스템 정보가 포함될 수 있어 보안 취약점이 될 수 있다.</strong></p>
<h3 id="2-프론트엔드-민감-정보-🌐">2. 프론트엔드 민감 정보 🌐</h3>
<h4 id="21-환경-변수-파일">2.1 환경 변수 파일</h4>
<p>plaintextCopy.env
.env.local
.env.development
.env.production</p>
<p><strong>💡 Why? 프론트엔드의 환경 변수 파일에도 API 키나 서비스 엔드포인트 등 민감한 정보가 포함될 수 있다.</strong></p>
<h4 id="22-개발-환경-관련-파일">2.2 개발 환경 관련 파일</h4>
<p>plaintextCopy/node_modules/    # 의존성 모듈
/.next/          # Next.js 빌드 결과물
/dist/           # 빌드 결과물
.DS_Store        # Mac 시스템 파일</p>
<h4 id="23-api-키-및-민감한-정보">2.3 API 키 및 민감한 정보</h4>
<p>Firebase 설정 파일
Google Analytics 키
소셜 로그인 정보
결제 시스템 키
Map API 키</p>
<p>*<em>💡 Why? 이러한 키들이 노출되면 서비스 과금 폭탄을 맞거나, 악의적인 사용자가 서비스를 도용할 수 있다!
*</em></p>
<h3 id="3-안전한-관리-방법-🛡️">3. 안전한 관리 방법 🛡️</h3>
<h4 id="1-gitignore-파일-설정">1. .gitignore 파일 설정</h4>
<pre><code># 환경 설정 파일
.env
*.properties

#### 빌드 결과물
/build/
/dist/
/node_modules/

# 시스템 파일
.DS_Store
*.log</code></pre><h4 id="2-예시-파일-제공">2. 예시 파일 제공</h4>
<ul>
<li>application.properties.example</li>
<li>.env.example</li>
<li>실제 값 대신 더미 값 사용</li>
</ul>
<h4 id="3-환경-변수-안전하게-공유">3. 환경 변수 안전하게 공유</h4>
<ul>
<li>팀 내부 암호화된 채널 사용</li>
<li>비밀번호 관리 도구 활용</li>
<li>CI/CD 환경 변수 활용</li>
</ul>
<h3 id="4-실수로-올렸다면-🚨">4. 실수로 올렸다면? 🚨</h3>
<p>즉시 해당 키 폐기/재발급
git history에서 완전히 제거
팀원들에게 상황 공유
보안 모니터링 강화</p>
<p><strong>💡 TIP: 커밋하기 전에 항상 git status로 변경사항을 확인하는 습관을 들이면 좋다!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vue] Router..?]]></title>
            <link>https://velog.io/@super-hwang/Vue-Router</link>
            <guid>https://velog.io/@super-hwang/Vue-Router</guid>
            <pubDate>Thu, 14 Nov 2024 08:01:20 GMT</pubDate>
            <description><![CDATA[<h3 id="📚-vue-router와-라우팅-개념-정리">📚 Vue Router와 라우팅 개념 정리</h3>
<p>Spring을 하다가 Vue.js로 넘어오면서 라우팅 개념이 좀 달라졌는데, 오늘은 이 부분을 확실하게 정리해보려고 한다.</p>
<h3 id="🌐-라우팅이란">🌐 라우팅이란?</h3>
<p>라우팅은 크게 두 가지 의미를 가진다.
<strong>네트워크에서 경로를 선택하는 프로세스
웹 애플리케이션에서 다른 페이지 간의 전환과 경로를 다루는 프로세스</strong></p>
<h4 id="❌-라우팅이-없다면">❌ 라우팅이 없다면?</h4>
<p>URL로 페이지 변화를 감지할 수 없음
현재 뭘 렌더링 중인지 상태 파악 불가능
URL이 1개라서 새로고침하면 처음 페이지로 돌아감
원하는 페이지 링크를 공유할 수 없음
브라우저 뒤로가기도 못씀</p>
<h3 id="🔄-ssr-vs-csr에서의-라우팅">🔄 SSR vs CSR에서의 라우팅</h3>
<p>SSR 라우팅 (Spring Boot)
Spring Boot에서는 서버가 라우팅을 처리한다. JSP(Java + HTML)가 실행되면 Java 코드는 처리되고 HTML만 클라이언트로 전달된다. 매 요청마다 이 과정이 반복되기 때문에 우리 눈에는 페이지가 깜빡이는 것처럼 보인다.</p>
<pre><code>클라이언트 요청(/list) → 컨트롤러 처리 → Model and View 처리 
→ ViewResolver가 View 찾기 → JSP를 클라이언트에게 전송</code></pre><p>CSR/SPA에서의 라우팅
SPA에서는 클라이언트(브라우저)가 라우팅을 수행한다. 전체 페이지를 다시 로드하지 않고, JavaScript가 필요한 데이터만 JSON 형태로 비동기 요청해서 가져온다.
실제로는 하나의 페이지지만, URL에 따라 다른 컴포넌트를 렌더링해서 마치 여러 페이지를 쓰는 것처럼 보이게 한다. <div id="app"> 안에 .vue 파일들을 갈아끼우는 방식이라고 생각하면 된다.</p>
<h3 id="🛠-vue-router-시작하기">🛠 Vue Router 시작하기</h3>
<p>기존 프로젝트에 router를 추가하고 싶다면:
bashCopynpm i vue-router
새 프로젝트를 만들 때는 Vue CLI에서 router 관련 질문에 Yes를 선택하면 된다.</p>
<h4 id="프로젝트-구조">프로젝트 구조</h4>
<p>vue-project를 생성하면 주요 폴더/파일은 다음과 같다:</p>
<p>main.js
App.vue
components/
router/
views/</p>
<h4 id="router-설정하기">Router 설정하기</h4>
<ol>
<li>main.js<pre><code class="language-js">javascriptCopyimport { createApp } from &#39;vue&#39;
import App from &#39;./App.vue&#39;
import router from &#39;./router&#39;
</code></pre>
</li>
</ol>
<p>const app = createApp(App)
app.use(router)
app.mount(&#39;#app&#39;)</p>
<pre><code>2. App.vue
```js
vueCopy&lt;template&gt;
  &lt;nav&gt;
    &lt;RouterLink to=&quot;/&quot;&gt;Home&lt;/RouterLink&gt; |
    &lt;RouterLink to=&quot;/about&quot;&gt;About&lt;/RouterLink&gt;
  &lt;/nav&gt;

  &lt;RouterView /&gt;
&lt;/template&gt;</code></pre><ol start="3">
<li>router/index.js<pre><code class="language-js">javascriptCopyimport { createRouter, createWebHistory } from &#39;vue-router&#39;
import HomeView from &#39;../views/HomeView.vue&#39;
</code></pre>
</li>
</ol>
<p>const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: &#39;/&#39;,
      name: &#39;home&#39;,
      component: HomeView
    },
    {
      path: &#39;/about&#39;,
      name: &#39;about&#39;,
      component: () =&gt; import(&#39;../views/AboutView.vue&#39;)
    }
  ]
})</p>
<p>export default router</p>
<pre><code>
### 🔍 동적 라우팅 예시

```  js
javascriptCopy{
  path: &#39;/user/:id&#39;,
  name: &#39;user&#39;,
  component: UserView,
  children: [
    {
      path: &#39;profile&#39;,
      component: UserProfile
    },
    {
      path: &#39;posts&#39;,
      component: UserPosts
    }
  ]
}</code></pre><h3 id="🚀-프로그래밍-방식-네비게이션">🚀 프로그래밍 방식 네비게이션</h3>
<pre><code>javascriptCopy// 다른 URL로 이동
router.push(&#39;/home&#39;)

// 현재 페이지 교체 (히스토리에 안 남음)
router.replace(&#39;/about&#39;)</code></pre><h3 id="마무리">마무리</h3>
<p>Spring에서 Vue로 넘어올 때 가장 헷갈렸던 부분이 라우팅이었는데, 이제 좀 감이 잡히는 것 같다. 다음 포스팅에서는 네비게이션 가드와 라우터 모드에 대해 더 자세히 알아보도록 하자! 😎</p>
<p>참고 : Vue Router 공식 문서</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[swea 2112] 약물 주입 (투포인터, 부분집합)]]></title>
            <link>https://velog.io/@super-hwang/swea-2112-%EC%95%BD%EB%AC%BC-%EC%A3%BC%EC%9E%85-%ED%88%AC%ED%8F%AC%EC%9D%B8%ED%84%B0-%EB%B6%80%EB%B6%84%EC%A7%91%ED%95%A9</link>
            <guid>https://velog.io/@super-hwang/swea-2112-%EC%95%BD%EB%AC%BC-%EC%A3%BC%EC%9E%85-%ED%88%AC%ED%8F%AC%EC%9D%B8%ED%84%B0-%EB%B6%80%EB%B6%84%EC%A7%91%ED%95%A9</guid>
            <pubDate>Wed, 28 Aug 2024 07:11:48 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;

public class _2112 {

    static int T, D, W, K;
    static int[][] map;
    static int[][] otherMap;
    static int min;
    static boolean[] isSelected;

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        T = Integer.parseInt(br.readLine());

        for (int t = 1; t &lt;= T; t++) {
            st = new StringTokenizer(br.readLine());
            D = Integer.parseInt(st.nextToken());
            W = Integer.parseInt(st.nextToken());
            K = Integer.parseInt(st.nextToken());

            map = new int[D][W];
            otherMap = new int[D][W];
            isSelected = new boolean[D];

            for (int i = 0; i &lt; D; i++) {
                st = new StringTokenizer(br.readLine());
                for (int j = 0; j &lt; W; j++) {
                    map[i][j] = Integer.parseInt(st.nextToken());
                    otherMap[i][j] = map[i][j];
                }
            }

            //약품의 최소 투입 횟수
            min = D;

            subSet(0);

            System.out.println(&quot;#&quot; + t + &quot; &quot; + min);
        }
    }

    private static void subSet(int depth) {

        if (depth == D) {
            inject(0, 0);
            reset();
            return;
        }

        isSelected[depth] = true;
        subSet(depth + 1);
        isSelected[depth] = false;
        subSet(depth + 1);

    }

    private static void reset() {
        for (int i = 0; i &lt; D; i++) {
            for (int j = 0; j &lt; W; j++) {
                map[i][j] = otherMap[i][j];
            }
        }
    }

    private static void inject(int injectionCnt, int idx) { //약물 투입

        if (injectionCnt &gt;= min) return; //여기서 약물을 판단한다.

        if (idx == D) {
            if (isPassed()) min = injectionCnt;
            return;
        }

        if (isSelected[idx]) { //투입하는 애들에 대해서
            Arrays.fill(map[idx], 0); //0 주입하고
            inject(injectionCnt + 1, idx + 1); //약물 개수 증가

            //0주입 다하면 1주입 시작
            Arrays.fill(map[idx], 1);
            inject(injectionCnt + 1, idx + 1);

        } else {
            inject(injectionCnt, idx + 1);
        }


    }

    private static boolean isPassed() {

        for (int i = 0; i &lt; W; i++) {
            int start = 0, end = 1;
            int len = 1;
            while (end &lt; D) {

                if (map[start][i] == map[end][i]) {
                    end++;
                } else {
                    start = end;
                    end++;
                }

                len = Math.max(len, end - start);

                if (len &gt;= K) break;

            }

            if (len &lt; K) return false;

        }

        return true;

    }
}

</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[순열 (마지막에 visited[i] = 0 해주기)]]></title>
            <link>https://velog.io/@super-hwang/%EC%88%9C%EC%97%B4-%EB%A7%88%EC%A7%80%EB%A7%89%EC%97%90-visitedi-0-%ED%95%B4%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@super-hwang/%EC%88%9C%EC%97%B4-%EB%A7%88%EC%A7%80%EB%A7%89%EC%97%90-visitedi-0-%ED%95%B4%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Fri, 16 Aug 2024 01:18:21 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Baby_Gin_Game {
    //순열은 방문 배열이 있어야 한다.
    static int[] arr;
    static int len = 0;
    static int[] visited;
    static int[] map;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s = br.readLine(); //122223

        arr = new int[s.length()];
        for (int i = 0; i &lt; s.length(); i++) {
            arr[i] = Integer.parseInt(s.substring(i, i + 1));
        }

        len = arr.length;

        visited = new int[len];
        map = new int[len];

        //순열 구하기
        findPermu(0);

    }

    private static int findPermu(int depth) {
        if (depth == len) {
            for (int n : map) {
                System.out.print(n + &quot; &quot;);
            }
            System.out.println();
            return 1;
        }

        for (int i = 0; i &lt; len; i++) {

            if (visited[i] == 1) { //방문했을 경우
                continue;
            }
            map[depth] = arr[i];
            visited[i] = 1;
            findPermu(depth + 1);
            //visited[i] = 0;

        }
        return 0;
    }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/b593df6d-9bb8-4786-b324-783e2007b73b/image.png" alt=""></p>
<ul>
<li>visited[i] = 0 이라는 코드를 놓치면 안된다!!!<pre><code class="language-cs">map[depth] = arr[i];
visited[i] = 1;
findPermu(depth + 1);
visited[i] = 0;</code></pre>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/1fa1e493-38f7-46dc-a941-85a8b6d4de45/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[16401번] 과자 나눠주기 (이분탐색, 반례)]]></title>
            <link>https://velog.io/@super-hwang/16401%EB%B2%88-%EA%B3%BC%EC%9E%90-%EB%82%98%EB%88%A0%EC%A3%BC%EA%B8%B0-%EC%9D%B4%EB%B6%84%ED%83%90%EC%83%89-%EB%B0%98%EB%A1%80</link>
            <guid>https://velog.io/@super-hwang/16401%EB%B2%88-%EA%B3%BC%EC%9E%90-%EB%82%98%EB%88%A0%EC%A3%BC%EA%B8%B0-%EC%9D%B4%EB%B6%84%ED%83%90%EC%83%89-%EB%B0%98%EB%A1%80</guid>
            <pubDate>Wed, 31 Jul 2024 11:50:49 GMT</pubDate>
            <description><![CDATA[<p>문제 링크 : <a href="https://www.acmicpc.net/problem/16401">https://www.acmicpc.net/problem/16401</a></p>
<p>처음에는 과자의 수가 조카 수보다 크면 마지막 인덱스에서 조카 수만큼을 뺀 인덱스가 최대 길이가 되는 줄 알았는데</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/97aa02e3-10f2-43f4-9eb5-4632bc06fd9f/image.jpg" alt=""></p>
<p>2 6
1 1 1 1 1 100
라는 반례가 있었다. 
내가 생각한 것처럼 코드를 짜면 치대 길이가 1이고 바로 이분탐색을 시작하면 500이 된다.</p>
<p>그래서 구현한 이분탐색에서 모든 조카에게 같은 길이로 줄 수 없는 예외경우 코드를 짜줘야 할 것 같다.</p>
<p>그리고 코드를 좀 고쳐는데, 킹론상 완벽한데 왜 틀렸지?????</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/948d436d-0d15-4126-b5c1-a5b6c9387efe/image.jpg" alt=""></p>
<hr>
<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    static long[] cookie;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        st = new StringTokenizer(br.readLine());
        int M = Integer.parseInt(st.nextToken()); //조카
        int N = Integer.parseInt(st.nextToken()); //과자
        //조카,과자,과자 길이 모두 1이상이다.

        cookie = new long[N];

        st = new StringTokenizer(br.readLine());

        long sum = 0;

        for (int i = 0; i &lt; N; i++) {
            cookie[i] = Long.parseLong(st.nextToken());
            sum += cookie[i];
        }

        //long min = Long.MAX_VALUE; //막대과자의 최대 길이

        if (sum &lt; M) { //막대과자 길이의 합이 조카의 수보다 작으면 0을 출력한다.
            System.out.println(0);
        } else if (sum == M) { //막대과자 길이의 합이 조카의 수와 같으면 1을 출력한다.
            System.out.println(1);
        } else { //막대과자 길이의 합이 조카의 수보다 크고 조카의 수보다 크면 이분 탐색을 시작한다.

            long right = cookie[N - 1]; //과자의 최대 길이
            long left = 1; //길이는 1부터 시작한다.
            long max = 0; // 최대 과자 길이

            //right를 cookie의 최대 길이로 준 이유는 나머지 쿠키길이가 최대 길이에 포함될 수 있기 때문이다.
            //right를 짧은 쿠키 갯수로 준다면 최대 길이는 포함될 수 없다.
            while (left &lt;= right) { //right를 출력하므로 right=1이 될 때까지 반복해줘야 한다.
                long mid = (left + right) / 2; //중간 값
                //여기서 자르는 mid는 배열의 인덱스가 아니라 과자의 길이다.
                //즉 주어지지 않은 과자의 길이가 중간값이 될 수 있다.
                long cnt = 0; //나눌 수 있는 과자의 개수

                for (int i = 0; i &lt; N; i++) {
                    //right가 계속 바뀌는 경우에 left = 1, right = 1일때까지만 반복된다.
                    //mid의 최소값은 1이 된다. (1+1)/2 = 1이다.
                    cnt += (cookie[i] / mid); //나올 수 있는 과자 덩어리의 합
                }

                if (cnt &gt;= M) { //나온 과자가 조카의 수와 같거나 크면 조금 큰 덩어리로 잘라본다.
                    max = Math.max(max, mid); // 최대 길이 업데이트
                    left = mid + 1;
                } else { //나온 과자가 조카의 수보다 작으면 조금 더 작은 덩어리로 잘라본다.
                    right = mid - 1;
                }

                //min = Math.min(min, right); //이분탐색은 가장 근접한 수를 찾는 것이므로 큰 길이에서 올 때는 min을 구해야 한다.
                //이렇게 해줄 필요 없이 이 문제에서는 가장 최대의 과자 길이 값을 찾는다!!
                //아 아니다 이거 아니다. 다시..

                //3 1
                //1 1 15 15 15
                //위의 경우에는 15가 답이므로 mid가 아니라 right를 출력해야 한다.

            } //while end

            System.out.println(right);

        }

    }
}
</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[월말] 달팽이, 파장, 참외밭 (nowX, nowY)]]></title>
            <link>https://velog.io/@super-hwang/%EC%9B%94%EB%A7%90-%EB%8B%AC%ED%8C%BD%EC%9D%B4-%ED%8C%8C%EC%9E%A5-%EC%B0%B8%EC%99%B8%EB%B0%AD-nowX-nowY</link>
            <guid>https://velog.io/@super-hwang/%EC%9B%94%EB%A7%90-%EB%8B%AC%ED%8C%BD%EC%9D%B4-%ED%8C%8C%EC%9E%A5-%EC%B0%B8%EC%99%B8%EB%B0%AD-nowX-nowY</guid>
            <pubDate>Tue, 30 Jul 2024 08:19:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/super-hwang/post/05fca7cc-b8f7-46a0-b7f7-9c7c2064f448/image.png" alt=""></p>
<pre><code class="language-cs">public class Snail {

    static int[] dx = {0, 1, 0, -1}; //좌하우상
    static int[] dy = {-1, 0, 1, 0};
    static int[][] map = new int[5][5];

    public static void main(String[] args) {

        int cnt = 25;

        int x = 0, y = 4;
        int dir = 0; //방향 변수


        while (cnt &gt;= 1) {

            //현재 좌표부터 넣어준다.
            map[x][y] = cnt--;

            //NowX,nowY는 판단하는 변수이지 현재 좌표가 여기서는 아니다.
            int nowX = x + dx[dir];
            int nowY = y + dy[dir];

            //다음 좌표가 유효한지 체크하고 유효하지 않다면 nowX,nowY를 유효하게 바꾸고 x,y에 넣어준다.
            if (nowX &lt; 0 || nowY &lt; 0 || nowX &gt;= 5 || nowY &gt;= 5 || map[nowX][nowY] != 0) { //벽을 만나거나 지도의 숫자가 0이 아닌 다른 수로 채워져있을 경우
                dir = (dir + 1) % 4;//0,1,2,3 순서로 바꾼다.
                //nowX는 기존 좌표에서 다음 좌표의 이동 좌표를 판단하는 기준이 되는 좌표이다.
                //x,y가 0이 되면 nowX=0,nowY=-1 상태로 되어있다.
                //nowY가 -1이므로 if 문으로 들어오게 되고 dir = 1로 바뀐다.
                //그 이후에 좌표값의 아무런 변동없이 if문을 탈출하면 cnt--; 와 map[nowX][nowY]가 실행되므로
                //map[0][-1] 이 나와 배열 인덱스 범위를 벗어나게 된다.
                //따라서 nowX와 nowY를 (0,-1)에서 값을 변경시켜야 한다.
                //기존 값 (0,0)에서 방향을 바꾼 좌표를 nowX,nowY에 넣어준다.
                //(0+1,0) 해서 아래로 내려가는 좌표가 대입된다.
                nowX = x + dx[dir]; //1
                nowY = y + dy[dir]; //0
            }

            x = nowX; //위에보면 x에 방향을 더하고 있다. 따라서 x에 이동할 때마다 좌표값을 업데이트 해주어야 한다.
            y = nowY;

        }


        for (int i = 0; i &lt; 5; i++) {
            for (int j = 0; j &lt; 5; j++) {
                System.out.printf(&quot;%5d&quot;, map[i][j]);
            }
            System.out.println();
        }

    }
}</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/4d40fea2-4153-4167-a069-1afbf98c4b4a/image.png" alt="">
<img src="https://velog.velcdn.com/images/super-hwang/post/3878d467-aed3-43ad-b49f-87cf4615d518/image.png" alt=""></p>
<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        int test = Integer.parseInt(br.readLine());

        for (int i = 0; i &lt; test; i++) {
            int max = 0;
            int N = Integer.parseInt(br.readLine());
            st = new StringTokenizer(br.readLine());

            int[][] map = new int[N][N];


            int num = Integer.parseInt(st.nextToken());
            int[] power = new int[num];

            for (int j = 0; j &lt; num; j++) {
                power[j] = Integer.parseInt(st.nextToken());
            }

            int[] dx = {-1, 1, 0, 0, 1, -1, 1, -1};
            int[] dy = {0, 0, 1, -1, 1, -1, -1, 1};

            st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; num; j++) {
                int x = Integer.parseInt(st.nextToken());
                int y = Integer.parseInt(st.nextToken());

                for (int d = 0; d &lt; 4; d++) { //한 방향에서 power 의 크기만큼 파장이 생겨야 한다.
                    for (int p = 0; p &lt; power[j]; p++) { //power 크기만큼 값 입력을 반복해준다.
                        int nowX = x + dx[d];
                        int nowY = y + dy[d];
                        if (nowX &lt; 0 || nowY &lt; 0 || nowX &gt;= N || nowY &gt;= N) continue;
                        map[nowX][nowY]++;
                        x = nowX;
                        y = nowY;
                        max = Math.max(map[nowX][nowY], max);
                    }
                }
            }

            System.out.println(&quot;#&quot; + test + &quot; &quot; + max);
        }
    }
}

//3
//6
//3 3 2 2
//2 3 1 2 5 2
//6
//3 3 1 1
//1 1 2 2 6 6
//5
//4 3 2 2 1
//2 3 1 2 5 2 3 3</code></pre>
<hr>
<p>[참외밭]
<a href="https://www.acmicpc.net/problem/2477">https://www.acmicpc.net/problem/2477</a></p>
<pre><code class="language-cs"></code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[14620번] 꽃길 (dfs, 코드 흐름 정리)]]></title>
            <link>https://velog.io/@super-hwang/14620%EB%B2%88-%EA%BD%83%EA%B8%B8-dfs-%EC%BD%94%EB%93%9C-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@super-hwang/14620%EB%B2%88-%EA%BD%83%EA%B8%B8-dfs-%EC%BD%94%EB%93%9C-%ED%9D%90%EB%A6%84-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 20 Jul 2024 06:03:33 GMT</pubDate>
            <description><![CDATA[<hr>
<p>문제 링크</p>
<ul>
<li><a href="https://www.acmicpc.net/problem/14620">https://www.acmicpc.net/problem/14620</a></li>
</ul>
<hr>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/4d208c91-d951-4fac-bfdb-9aec8a1a72e6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/7ddcde2e-9289-4b25-86ad-eb1f586876c6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/53d8c969-20b7-45e8-83ff-d2d79c6a2ed0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/da23e64a-9a35-4680-a734-7c6e0b855dff/image.png" alt="">
<img src="https://velog.velcdn.com/images/super-hwang/post/5dfc4725-e89f-4cb7-b6a7-5a1d8d56b498/image.png" alt="">
<img src="https://velog.velcdn.com/images/super-hwang/post/6e1c5301-5b35-48df-9b17-741152f0dda7/image.png" alt="">
<img src="https://velog.velcdn.com/images/super-hwang/post/b9c9cb8d-8c08-47f2-a14c-93f3e2800a92/image.png" alt=""></p>
<hr>
<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    static final int MAX_COUNT = 3; //심을 수 있는 꽃의 MAX값
    static int[][] map;
    static boolean[][] visited;
    static int[] dx = {-1, 0, 1, 0, 0}; // 상, 우, 하, 좌, 현재
    static int[] dy = {0, 1, 0, -1, 0};
    static int answer, N;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        N = Integer.parseInt(br.readLine());

        map = new int[N][N];
        visited = new boolean[N][N];
        answer = Integer.MAX_VALUE;

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

        dfs(0, 0);

        System.out.println(answer);

        br.close();
    }

    //두 번째 꽃까지 심은 후 마지막 꽃에 대해서 모든 경우의 수를 구한 후 최소 비용 도출
    //첫 번째 꽃까지 심은 후 두 번째, 세 번쨰 꽃에 대해서 모든 경우의 수를 구한 후 최소 비용 도출
    //시작점을 옮기고 이 과정을 반복
    static void dfs(int count, int cost) { //꽃의 개수, 비용

        if (count == MAX_COUNT) { //기저 사례
            answer = Math.min(answer, cost);
            return;
        }

        for (int x = 1; x &lt; N - 1; x++) { //씨앗을 심을 수 있는 범위
            for (int y = 1; y &lt; N - 1; y++) {

                boolean canPlant = true;

                // 현재 위치와 주변 4칸이 방문되지 않았는지 확인
                for (int dir = 0; dir &lt; 5; dir++) {
                    int nowX = x + dx[dir];
                    int nowY = y + dy[dir];

                    if (visited[nowX][nowY]) { //한 곳이라도 방문했다면
                        canPlant = false; //심을 수 없음
                        break; //dir for문 탈출
                    }
                }

                if (canPlant) {
                    // 방문 처리 및 비용 계산
                    for (int dir = 0; dir &lt; 5; dir++) {
                        int nowX = x + dx[dir];
                        int nowY = y + dy[dir];

                        visited[nowX][nowY] = true; // 꽃 피운 구역 방문
                        cost += map[nowX][nowY]; // 꽃 피운 비용
                    }

                    dfs(count + 1, cost); // 다음 꽃 심기

                    // 방문 해제 및 비용 복구
                    for (int dir = 0; dir &lt; 5; dir++) {
                        int nowX = x + dx[dir];
                        int nowY = y + dy[dir];

                        visited[nowX][nowY] = false;
                        cost -= map[nowX][nowY]; // 비용 복구
                    }
                }
            }
        }

    }

}</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/ad5889ea-aac3-418b-bd6c-c80e81d80c5b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2580번] 스도쿠 (백트래킹, 3*3나누기)]]></title>
            <link>https://velog.io/@super-hwang/2580%EB%B2%88-%EC%8A%A4%EB%8F%84%EC%BF%A0</link>
            <guid>https://velog.io/@super-hwang/2580%EB%B2%88-%EC%8A%A4%EB%8F%84%EC%BF%A0</guid>
            <pubDate>Sun, 14 Jul 2024 03:16:13 GMT</pubDate>
            <description><![CDATA[<hr>
<p><a href="https://www.acmicpc.net/problem/2580">https://www.acmicpc.net/problem/2580</a></p>
<p>처음에 떠오른 방법은 열,행,대각선 각각 비교한 후
자리에 넣을 수 있는 숫자 후보들을 구한다.
그 후보들이 1개이면 그 자리를 먼저 채운다.
그리고 나머지 자리에 대해서 비교하면서 채운다.
보통 나는 스도쿠를 위와 같은 방식으로 푸는데 코드로 로직을 짤 때는 고려사항이 너무 많은 것 같아서 이 방법이 답은 아닌 것 같다.
어떻게 풀어야 하는걸까</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/11dfcb73-d970-4615-87c7-b124788d2be7/image.png" alt=""></p>
<p>각 자리에 1부터 9까지 모두 다 넣어보면서 탐색을 하다가 안되면 2로 넘어가는 식으로 풀면된다.
반복이 적어서 가능할듯.</p>
<p>아래와 같이 3*3구간 내에서 첫 시작 좌표로 돌아가게 하는 공식을 생각해내는 게 이 문제의 히든 포인트이지 않나 생각이 든다.</p>
<pre><code class="language-cs">int nowRow = (row / 3) * 3;
int nowCol = (col / 3) * 3;</code></pre>
<p>공식 잘 기억해두자.</p>
<p>그리고 백트래킹 사용한다. 이전 단계 즉 바로 위로 올라가는 것 기억하기!!</p>
<pre><code class="language-cs">//탐색
if (map[x][y] == 0) {
        for (int k = 0; k &lt; 9; k++) {
            if (isP(x, y, k)) { //행,열,대각선이 모두 만족할 경우
                map[x][y] = k; //자리를 값으로 채운다.
                getFinalNumber(x, y + 1);
            }
        }

        map[x][y] = 0; //백트래킹
        return;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/40ca3041-5169-4d99-868e-ad3b2020c24c/image.png" alt=""></p>
<p>그리고 두 코드는 같은 결과를 도출한다.
<img src="https://velog.velcdn.com/images/super-hwang/post/92a85c89-405e-4049-a646-c9cebdc7438e/image.png" alt=""></p>
<hr>
<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    //스도쿠

    static int[][] map = new int[9][9];

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

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

        getFinalNumber(0, 0);

    }

    static void getFinalNumber(int x, int y) { //탐색 시작

        //다음 행 실행
        if (y == 9) {
            getFinalNumber(x + 1, 0);
            return;
        }

        if (x == 9) { //모든 map이 다 채워졌을 경우 map 출력
            for (int i = 0; i &lt; 9; i++) {
                for (int j = 0; j &lt; 9; j++) {
                    System.out.print(map[i][j] + &quot; &quot;);
                }
                System.out.println();
            }
            System.exit(0);
        }

        //탐색
        if (map[x][y] == 0) {
            for (int k = 1; k &lt;= 9; k++) { //K는 1-9까지로 설정!
                if (isP(x, y, k)) { //행,열,대각선이 모두 만족할 경우
                    map[x][y] = k; //자리를 값으로 채운다.
                    getFinalNumber(x, y + 1);
                }
            }
            map[x][y] = 0; //백트래킹
            //isP하다면 다음 열로 넘어가지만 isP하지 않다면 0으로 바꾸고 이전 단계로 돌아간다.
            return;
        }

        getFinalNumber(x, y + 1);

    }

    static boolean isP(int row, int col, int num) {

        for (int k = 0; k &lt; 9; k++) {
            if (map[row][k] == num || map[k][col] == num) { //1. 행과 열 판단
                return false;
            }
        }

        //2. 대각선 판단
        // num이 포환됨 3*3의 첫 시작 좌표
        int nowRow = (row / 3) * 3;
        int nowCol = (col / 3) * 3;

        for (int i = nowRow; i &lt; nowRow + 3; i++) {
            for (int j = nowCol; j &lt; nowCol + 3; j++) {
                if (map[i][j] == num) {
                    return false;
                }
            }
        }

        return true;
    }


}
</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/d3bd63b6-9410-444e-a84f-c8a4d6cb92bf/image.png" alt=""></p>
<hr>
<p>코드가 이해는 되는데 혼자서 시간내에 풀 수 있냐? 는 아닌 것 같다.
이 유형은 문제 많이 풀어서 익숙해져야 할듯함</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[9663번] Queen (완탐, 1차원 배열로)]]></title>
            <link>https://velog.io/@super-hwang/9663%EB%B2%88-Queen-%EC%99%84%ED%83%90-1%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4%EB%A1%9C</link>
            <guid>https://velog.io/@super-hwang/9663%EB%B2%88-Queen-%EC%99%84%ED%83%90-1%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4%EB%A1%9C</guid>
            <pubDate>Sat, 13 Jul 2024 05:29:43 GMT</pubDate>
            <description><![CDATA[<p>퀸은 상 하 좌 우 대각선으로 움직일 수 있다.
즉,** 같은 행 같은 열 대각선**에 놓을 수 없다는 것이다.
1차원 배열은 board를 선언했고 
인덱스 번호는 각 행의 번호를 의미하고, 배열의 값은 각 행의 열의 번호를 의미한다.
<img src="https://velog.velcdn.com/images/super-hwang/post/e2cf60b2-0276-41ea-993c-00084c24054a/image.JPG" alt=""></p>
<p>시작 ) 
0,0 부터 시작한다.
board[0] = 0 이다.
0행은 둘 수 없으므로 1행부터 탐색한다.</p>
<p>같은 열인지 판단하거나 대각선인지 판단한다.
board[0] (0행 0열에 queen이 놓인 상태, 즉 0이다) 과 현재 행의 열인 0을 비교한다.
0 == 0 으로 같은 열에 놓여져 있으므로 Queen은 놓여질 수 없고 1,1을 판단한다.</p>
<p>(1,1)은 
board[0] (값은 0) != 1 이므로 다른 열에 놓여져있다.</p>
<p>그렇다면 대각선인지 판단해보자.
board[0] (의미는 0열) - col(현재 1열) == 0(0행) - 1(현재 1행)
이전 위치에서 가로와 세로가 1만큼 차이나는 것이므로 대각선에 놓여진 것을 알 수 있다.
따라서, Queen은 놓여질 수 없고 1,2를 판단한다.</p>
<p>위의 과정을 반복하면 아래와 같은 일반식이 나올 수 있다.</p>
<pre><code class="language-cs">board[prow] == col || Math.abs(board[prow] - col) == Math.abs(prow - row) </code></pre>
<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static int N;
    static int[] board;
    static int cnt = 0;

    public static void main(String[] args) throws IOException {
        //Queen 문제
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        N = Integer.parseInt(br.readLine());
        board = new int[N];

        getQueensCount(0);
        System.out.println(cnt);
    }

    static void getQueensCount(int row) {
        //퀸은 같은 행, 같은 열, 대각선으로 움직일 수 있다.

        if (row == N) { //행을 끝까지 다 돌면
            cnt++;
            return;
        }

        for (int col = 0; col &lt; N; col++) { //현재 행의 열을 탐색
            boolean isP = true;

            //놓을 수 없는 경우
            //prow: 이전까지의 행
            for (int prow = 0; prow &lt; row; prow++) { //현재 탐색 위치와 이전 행을 비교

                //이전의 행의 열과 현재 행의 열이 같거나 || 이전의 행에 놓여진 퀸의 대각선 방향과 같다면
                //대각선 탐색 : 이전 행의 열에서 현재 행의 열의 좌표를 뺀 것의 절대값 == 이전 행의 값과 현재 행의 좌표를 뺀 것의 절대값
                if (board[prow] == col || Math.abs(board[prow] - col) == Math.abs(prow - row)) {
                    isP = false;  //놓을 수 없음
                    break;
                }
            }

            //놓을 수 있는 경우
            if (isP) {
                board[row] = col;
                getQueensCount(row + 1);
            }
        }
    }
}
</code></pre>
<p><a href="https://www.acmicpc.net/problem/9663">https://www.acmicpc.net/problem/9663</a></p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/3d9030d9-b6f4-4ffb-9cdf-51b6c0ddfe87/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[12기 SAFFY를 준비하면서]]></title>
            <link>https://velog.io/@super-hwang/12%EA%B8%B0-SAFFY%EB%A5%BC-%EC%A4%80%EB%B9%84%ED%95%98%EB%A9%B4%EC%84%9C</link>
            <guid>https://velog.io/@super-hwang/12%EA%B8%B0-SAFFY%EB%A5%BC-%EC%A4%80%EB%B9%84%ED%95%98%EB%A9%B4%EC%84%9C</guid>
            <pubDate>Thu, 13 Jun 2024 01:14:39 GMT</pubDate>
            <description><![CDATA[<p>나는 구미 캠퍼스 전공자 web 트랙 반으로 지원했다. 
이력서 -&gt; 코테 -&gt; 면접 순으로 총 기간은 2달 정도 걸렸다.
5,6월 동안 준비했던 과정에 대해서 회고하고자 한다.</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/233995c9-acbe-497f-8d92-15b2b214cc4a/image.png" alt=""></p>
<h4 id="1-이력서-준비">1. 이력서 준비</h4>
<p>이력서는 2가지 파트로 나누어 구성했고 500자 이내라서 채우는 거 어렵진 않았다.</p>
<pre><code>[프로젝트 경험]
내가 한 프로젝트 소개, 맡은 역할, 구현하면서 어려웠던 점, 해결 방안을 통한 느낀 점
[싸피 지원 동기]
싸피에 지원한 이유, 싸피에서 이루고 싶은 목표</code></pre><h4 id="2-코테-준비">2. 코테 준비</h4>
<p>코테는 따로 준비하지는 않았고 백준은 야금야금 풀어서 어느정도는 익숙한 상태였다.
2문제 나왔는데 1번 문제는 쉬웠고 2번 문제는 어려웠다.
코린이인 나한테 1번 문제가 쉬웠으므로.. 대부분 맞췄을 것 이다.
2문제 중 1문제만 맞으면 합격인 것 같다. (1문제도 테케만 통과하면 되는 듯?)</p>
<h4 id="3-면접-준비">3. 면접 준비</h4>
<p>코테를 스무스하게 넘어왔으니!
면접 준비는 나름 준비해서 갔다.
같이 붙은 친구랑 2명이서 면접 스터디를 진행했는데, PT발표 연습 및 자소서 연습 총 2가지를 준비했다.</p>
<ul>
<li><p>PT 준비
준비했던 것이랑 면접장에서 받은 것이랑 살짝 달랐는데, 그래도 PT준비하면서 준비한 주제가 나와서 긴장하지 않고 발표할 수 있었다.
나는 인공지능, 자율주행, AR 과 같은 IT 주제가 나오는 기사를 15분 정도 준비하는 시간을 가지고 5분 동안 친구와 번갈아 가면서 발표했다.
발표가 끝나면 바로바로 피드백을 얻으면서 보완해나갔다.</p>
</li>
<li><p>자소서 준비
내가 쓴 자소서에서 들어올 질문을 쫙 정리한 후 답을 다 적었다.
아리송한 개념에 대해서 꼬리 질문이 들어올 것을 대비해 개념도 어느정도 공부했었다.</p>
</li>
</ul>
<h4 id="4-면접-후기">4. 면접 후기</h4>
<p>1시 35분 면접이였었다.
들어가서 PT발표하고 PT발표에 대해 질의응답 후 이력서 질문으로 넘어갔다.
생각보다 PT발표하면서 나름 도입, 개념설명, 장점, 문제점, 극복방안, 아이디어 순으로 잘 설명했었다.
20분 정도 면접을 보는데 시간이 어떻게 지나간 지 모르겠다.
생각보다 이력서 안의 기술에 대해서는 많이 묻지 않았고 나름 컴공이면 알만한 기술 개념과 공통 질문을 많이 물으셨다.
마지막 할 말 후 면접이 끝났고 다시 수서로 가는 지하철을 탔다.
합격일지 불합격일지 결과가 빨리 나왔으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2493번] 탑]]></title>
            <link>https://velog.io/@super-hwang/2493%EB%B2%88-%ED%83%91</link>
            <guid>https://velog.io/@super-hwang/2493%EB%B2%88-%ED%83%91</guid>
            <pubDate>Sun, 24 Mar 2024 02:41:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/super-hwang/post/29bb3dd5-a94b-4848-9b94-82b27a198be1/image.jpg" alt=""></p>
<hr>
<pre><code class="language-cs">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        int N = Integer.parseInt(br.readLine());

        Stack&lt;int[]&gt; stack = new Stack&lt;&gt;();

        st = new StringTokenizer(br.readLine());
        for (int i = 0; i &lt; N; i++) {
            int top = Integer.parseInt(st.nextToken());
            while (!stack.isEmpty()) {
                if (stack.peek()[1] &gt;= top) {
                    System.out.print(stack.peek()[0] + &quot; &quot;);
                    break;
                }
                stack.pop();
            }
            if (stack.isEmpty()) {
                System.out.print(0 + &quot; &quot;);
            }
            stack.push(new int[] {i + 1, top});
        }


    }
}</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/e8236127-d946-4320-a997-90f6cb22cc3e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IntelliJ] 자바 모듈 생성하는 법]]></title>
            <link>https://velog.io/@super-hwang/IntelliJ-%EC%9E%90%EB%B0%94-%EB%AA%A8%EB%93%88-%EC%83%9D%EC%84%B1%ED%95%98%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@super-hwang/IntelliJ-%EC%9E%90%EB%B0%94-%EB%AA%A8%EB%93%88-%EC%83%9D%EC%84%B1%ED%95%98%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Sat, 23 Mar 2024 04:58:33 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/super-hwang/post/ea3ecf2b-7f95-4d60-95b7-bc53804f4897/image.png" alt=""></p>
<p>위처럼 되는 폴더를 아래와 같은 폴더로 만들고 싶다면</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/73e538e8-66d9-4598-aa3f-3fdedc4e0e7c/image.png" alt=""></p>
<ul>
<li><p>1번
<img src="https://velog.velcdn.com/images/super-hwang/post/568a6ec1-eb66-4d33-a0a5-b322523122a1/image.png" alt=""></p>
</li>
<li><p>2번
<img src="https://velog.velcdn.com/images/super-hwang/post/ed0340ff-4c2c-40ae-b92f-7e6c8c500f04/image.png" alt=""></p>
</li>
<li><p>3번
<img src="https://velog.velcdn.com/images/super-hwang/post/a14feb58-4825-4d1c-991b-e0333ce614a9/image.png" alt=""></p>
</li>
</ul>
<p>단, 모듈의 이름은 중복될 수 없다는 것!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 자료구조와 프레임워크]]></title>
            <link>https://velog.io/@super-hwang/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@super-hwang/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Sun, 10 Mar 2024 10:40:01 GMT</pubDate>
            <description><![CDATA[<hr>
<h3 id="자료구조도">자료구조도</h3>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/3a2f0011-8b9e-4a1f-bc83-7cdd9fd4c7ec/image.png" alt=""></p>
<h3 id="프레임워크">프레임워크</h3>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/23e7043d-effc-4ffb-91cf-ddb5c5f686a7/image.png" alt=""></p>
<hr>
<h3 id="✅-stack의-메소드">✅ Stack의 메소드</h3>
<p>FILO</p>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/2c5167b6-3ecd-4a4c-8349-d5631ee979df/image.png" alt=""></p>
<hr>
<h3 id="✅-queue의-메소드">✅ Queue의 메소드</h3>
<p>FIFO
<img src="https://velog.velcdn.com/images/super-hwang/post/5edba431-ea40-4573-b87a-a12ecdc1a6f3/image.png" alt=""></p>
<h4 id="우선순위-큐">우선순위 큐</h4>
<ul>
<li><p>큐(Queue) 데이터 구조를 기반으로 데이터를 ‘일렬로 늘어놓은 다음’ 그중에서‘가장 우선순위가 높은 데이터&#39;를 &#39;가장 먼저 꺼내오는 방식’으로 동작하는 클래스</p>
</li>
<li><p>큐의 끝에 새로운 요소를 추가하고, 우선순위를 고려하여 요소의 위치를 조정
이때, 적절한 위치를 찾기 위해 우선순위를 비교하며, 우선순위가 높은 요소가 먼저 위치하도록 함</p>
</li>
<li><p>큐 내에 우선순위가 같은 요소가 여러 개 있다면 일반적으로 먼저 큐에 추가된 요소가 먼저 제거됨</p>
</li>
<li><p>높은 우선순위 인 값을 조회하는 방법은 peek() 메서드</p>
</li>
<li><p>값을 추출하는 방법은 poll() 메서드</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/super-hwang/post/c05b4a4d-ca79-4053-aa54-a2de311ce356/image.png" alt=""></p>
<hr>
<h3 id="✅-deque의-메소드">✅ deque의 메소드</h3>
<p>FIFO + LILO
<img src="https://velog.velcdn.com/images/super-hwang/post/331667a9-fb24-43a9-aa77-65eb631bd89a/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>