<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yeong_do.log</title>
        <link>https://velog.io/</link>
        <description>개발 블로그</description>
        <lastBuildDate>Fri, 02 Feb 2024 03:51:01 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. yeong_do.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yeong_do" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TIL 2023/02/02 최종 프로젝트 주酒총회 회고]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230202-N1-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@yeong_do/TIL-20230202-N1-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Fri, 02 Feb 2024 03:51:01 GMT</pubDate>
            <description><![CDATA[<h2 id="✅-프로젝트-소개">✅ 프로젝트 소개</h2>
<p>다양한 종류의 술들을 검색하고, 평점과 리뷰를 통해 원하는 상품을 찾을 수 있도록 도와주는 웹 서비스</p>
<h3 id="내용">내용</h3>
<ul>
<li>주류 카테고리로 나누고 그 안에서 평점순/ 찜많은 순 /리뷰 많은 순/ (신제품순 으로 비교할 수 있고</li>
<li>편의점 별 상품 필터</li>
<li>찜 기능과 인증을 통해 마셔본 술을 등록할 수도 있다. ⇒ 배찌 기능/사용자 간 랭킹</li>
<li>주류를 클릭하면 주류에 대한 설명(성분정보) 과 (판매처/지역) 등이 있고, 어울리는 안주 추천, 평점과 리뷰를 등록할 수 있다.</li>
<li>상품은 관리자가 등록하고, 사용자는 제보할 수 있다.</li>
<li>추천 기능(판매량 정보 가져와서)</li>
<li>실시간 알림 및 실시간 채팅</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/bc5b1172-2912-4f62-ae98-2d690f098456/image.png" alt=""></p>
<h2 id="🏝️-ground-rules">🏝️ Ground Rules</h2>
<ul>
<li>기술면접 9시-10시 사이에 2문제씩 하고 면접 진행시 카메라 키도록 한다!</li>
<li>자리를 비울 때나 일정이 있을 경우 팀 슬랙에 공유한다. (연락이 안되면 카톡으로 호출하겠습니다!!)</li>
<li>학습을 하며 막히는게 있다면 팀원과 튜터님께 공유하며 해결한다.</li>
<li>대화를 할 때는 캠도 켜고 화면공유도 잘 한다.</li>
<li>파이팅 넘치겠습니다!</li>
<li>TIL 꼭 쓰기 - 다음날 오전 검사 (벌칙: 캠 &amp; 화면공유-&gt; TIL 쓰기)</li>
<li>지각은 미리 슬랙에 공유 -&gt; 공유 없이 지각하면 패널티(1시간에 천원, 30분까진 허용)</li>
<li>아프지 않게 컨디션 관리 잘하기</li>
</ul>
<h2 id="🚩-goals">🚩 Goals</h2>
<ul>
<li>CI / CD 도입</li>
<li>JPA/AWS 강의 + 특강 1.10일까지 완강 (20시, 강의 수강 계획 및 이행 현황 공유)</li>
<li>동시성, 트래픽 문제 해결</li>
<li>도커 사용해 보기</li>
<li>코드 리뷰 - PR에 댓글</li>
<li>-&gt; 각 사람에 대해 맡은 사람이 리뷰해주기 [이민주 -&gt; 오수식 -&gt; 김재현 -&gt; 정영도 -&gt; 박연우 -&gt; 이민주]</li>
<li>프론트 - 타임리프 적용시키기</li>
</ul>
<h2 id="🚦-project-rules">🚦 Project Rules</h2>
<p><strong>백엔드</strong></p>
<ul>
<li>GVS : Github</li>
<li>IDE : IntelliJ</li>
<li>SDK : JAVA 17</li>
<li>Spring Boot 3.2.1</li>
<li>Spring Web</li>
<li>Spring Security</li>
<li>Validation</li>
<li>thymeleaf</li>
<li><em>DB*</em></li>
<li>Spring Data JPA</li>
<li>MySQL</li>
<li>H2</li>
<li>Redis</li>
<li>AWS RDS</li>
<li>Imagae Stroage</li>
<li>AWS S3</li>
<li><em>배포 환경*</em></li>
<li>ec2, S3, GithubAction , code Deploy</li>
<li><em>프론트엔드*</em></li>
<li>HTML/CSS</li>
<li>Bootstrap5</li>
<li>JS</li>
<li>JQuery</li>
</ul>
<h2 id="🤝-우리들의-약속">🤝 우리들의 약속</h2>
<p><strong>Code Convention</strong></p>
<ul>
<li><p>구글 코드 포매터 적용 [intellij] google code 포매터 적용</p>
</li>
<li><p>구글 자바 스타일 가이드 Google Java Style Guide
Github Rules</p>
</li>
<li><p><em>깃허브 규칙*</em> 우린 Git-flow를 사용하고 있어요 | 우아한형제들 기술블로그</p>
</li>
<li><p>PR 전 코드리뷰 필수!!
(2명이상 승인 해야 merge 가능하게 지정)</p>
</li>
<li><p>git branch 전략
main : 제품으로 출시될 수 있는 브랜치
dev : 다음 출시 버전을 개발하는 브랜치
feature : 기능을 개발하는 브랜치
release : 이번 출시 버전을 준비하는 브랜치
hotfix : 출시 버전에서 발생한 버그를 수정 하는 브랜치
feature로 각자 작업하면서 dev에 합친 후 중간 출시 때 release로 복사 hotfix로 유지보수하면서 최종 출시 때 main으로 합치기!</p>
</li>
<li><p>git commit message 작성
(타입 : 내용 으로 통일)</p>
</li>
</ul>
<h2 id="erd">ERD</h2>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/f488f31a-c884-4204-b9bb-6b311e27a303/image.png" alt=""></p>
<h2 id="와이어프레임">와이어프레임</h2>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/8cc9665d-9c2c-4120-b806-6e08585990ad/image.png" alt=""></p>
<h2 id="더보기">더보기</h2>
<ul>
<li>서비스 사이트: <a href="https://jujuassembly.store/">https://jujuassembly.store/</a></li>
<li>Notion Page: <a href="https://www.notion.so/teamsparta/5216eb8a22a5478990fdfe34b34988dd">https://www.notion.so/teamsparta/5216eb8a22a5478990fdfe34b34988dd</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/22 중간발표]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230122-%EC%A4%91%EA%B0%84%EB%B0%9C%ED%91%9C</link>
            <guid>https://velog.io/@yeong_do/TIL-20230122-%EC%A4%91%EA%B0%84%EB%B0%9C%ED%91%9C</guid>
            <pubDate>Tue, 23 Jan 2024 02:26:26 GMT</pubDate>
            <description><![CDATA[<h2 id="느낀점">느낀점</h2>
<p>오늘은 최종 프로젝트 중간발표날이였다! 최종 발표도 아닌데 너무 떨렸다.. 그래도 맡은 기능들은 기한 내에 완벽히! 구현 했으니까 걱정은 없었다. 내가 맡은 기능은 상품 목록에 대한 CRUD 였다. 기본적인 기능이라고 말할 수 있지만 그래도 열심히 기능을 구현했던 기억이.. 있다.. 최종 발표때까지 더 다양한 기능을 만들어보고 많은 경험을 해보고싶다. 이상 ~!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/17 프로젝트에 SSE 녹여내기]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230117-SSE</link>
            <guid>https://velog.io/@yeong_do/TIL-20230117-SSE</guid>
            <pubDate>Wed, 17 Jan 2024 12:14:30 GMT</pubDate>
            <description><![CDATA[<h2 id="sse란">SSE란?</h2>
<p>SSE(Server-Sent Events)는 웹 애플리케이션에서 서버로부터 데이터를 비동기적으로 전송받을 수 있는 기술 중 하나이다. 클라이언트의 별도의 요청이 없이도 알림처럼 실시간으로 서버에서 데이터를 전달해야할때가 있다. 이럴때 단방향으로 통신을 지원하며 서버로 데이터를 보낼수없다는 단점이 있지만, 실시간 업데이트가 필요할때는 효율적으로 데이터를 전달할 수 있다.</p>
<p>물론, SSE 방식외에도 클라이언트가 주기적으로 서버로 요청을 보내서 데이터를 받는 Short Polling , 서버의 변경이 일어날때까지 대기하는 Long Polling 방식이 있지만, 해당 프로젝트는 실시간으로 반영되어야하고 빈번하게 발생 될 수있는 상황이기에 SSE를 선택하였다. SSE는 서버와의 한번 연결을 하고나면 HTTP 1.1의 keep alive와 비슷하게 서버에서 변경이 일어날떄마다 데이터를 전송하는 방법이다.</p>
<h2 id="sse-구조와-구현">SSE 구조와 구현</h2>
<p>Client 하나당 sseemitter하나</p>
<p>SSE의 기본적인 흐름은 클라이언트가 SSE요청을 보내면 서버에서는 클라이언트와 매핑되는 SSE 통신객체를 만든다(SseEmitter) 해당객체가 이벤트 발생시 eventsource를 client에게 전송하면서 데이터가 전달되는 방식이다. sseemitter는 SSE 통신을 지원하는 스프링에서 지원하는 API이다.</p>
<h2 id="--controller">- Controller</h2>
<pre><code>@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/v1/notification&quot;)
public class NotificationController {

  private final NotificationService notificationService;

  /**
   * 로그인 한 유저의 Server-Sent Events(SSE) 연결을 관리합니다. SseEmitter를 사용하여 클라이언트에게 비동기적으로 이벤트를 보냅니다.
   *
   * @param userDetails 현재 인증된 사용자의 세부 정보
   * @param lastEventId 마지막으로 수신한 이벤트의 ID (필수는 아님)
   * @return SSE 연결을 위한 SseEmitter 객체
   */
  @GetMapping(value = &quot;/subscribe&quot;, produces = &quot;text/event-stream&quot;)
  public SseEmitter subscribe(@AuthenticationPrincipal UserDetailsImpl userDetails,
      @RequestParam(value = &quot;lastEventId&quot;, required = false, defaultValue = &quot;&quot;) String lastEventId) {
    // lastEventId : 마지막 이벤트 ID, 클라이언트가 마지막으로 수신한 이벤트를 식별하는데 사용 (필수는 아님)
    return notificationService.subscribe(userDetails.getUser(), lastEventId);
  }

  /**
   * 로그인한 사용자의 모든 알림을 조회합니다. ResponseEntity를 통해 알림 데이터와 HTTP 상태 코드를 함께 반환합니다.
   *
   * @param userDetails 현재 인증된 사용자의 세부 정보
   * @return 사용자의 모든 알림을 포함하는 ApiResponse 객체
   */
  @GetMapping(&quot;/notifications&quot;)
  public ResponseEntity&lt;ApiResponse&gt; notifications(
      @AuthenticationPrincipal UserDetailsImpl userDetails) {
    NotificationsResponseDto responseDto = notificationService.findAllById(userDetails.getUser());
    return ResponseEntity.ok()
        .body(new ApiResponse&lt;&gt;(&quot;알림 조회에 성공하였습니다.&quot;, HttpStatus.OK.value(), responseDto));
  }

  /**
   * 지정된 알림의 읽음 상태를 변경합니다. 성공적으로 변경되면 상태 메시지와 함께 HTTP 상태 코드를 반환합니다.
   *
   * @param notificationId 변경할 알림의 ID
   * @return 알림 상태 변경에 대한 ApiResponse 객체
   */
  @PatchMapping(&quot;/notifications/{notificationId}&quot;)
  public ResponseEntity&lt;ApiResponse&gt; readNotification(@PathVariable Long notificationId) {
    notificationService.readNotification(notificationId);
    return ResponseEntity.ok()
        .body(new ApiResponse&lt;&gt;(&quot;알림 읽음 상태 변경에 성공하였습니다.&quot;, HttpStatus.OK.value()));
  }

  /**
   * 지정된 알림을 삭제합니다. 성공적으로 삭제ㄴ되면 상태 메시지와 함께 HTTP 상태 코드를 반환합니다.
   *
   * @param notificationId 변경할 알림의 ID
   * @return 알림 상태 변경에 대한 ApiResponse 객체
   */
  @DeleteMapping(&quot;/notifications/{notificationId}&quot;)
  public ResponseEntity&lt;ApiResponse&gt; deleteNotification(@PathVariable Long notificationId) {
    notificationService.deleteNotification(notificationId);
    return ResponseEntity.ok()
        .body(new ApiResponse&lt;&gt;(&quot;알림 읽음 상태 변경에 성공하였습니다.&quot;, HttpStatus.OK.value()));
  }</code></pre><h2 id="--service">- Service</h2>
<pre><code>@Service
@RequiredArgsConstructor
@Slf4j
public class NotificationService {

  private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; // SSE 연결의 기본 타임아웃 값 (60분)

  private final EmitterRepository emitterRepository;
  private final NotificationRepository notificationRepository;
  private final ReviewRepository reviewRepository;
  private final ReportRepository reportRepository;
  private final RoomRepository roomRepository;
  private final ChatRepository chatRepository;


  // 사용자가 SSE를 통해 알림을 실시간으로 받을 수 있게 설정하는 메서드
  public SseEmitter subscribe(User user, String lastEventId) {
    Long userId = user.getId();

    // 사용자 ID와 현재 시간을 조합해 고유한 emitter 식별자 생성
    String id = userId + &quot;_&quot; + System.currentTimeMillis();

    SseEmitter emitter = emitterRepository.save(id, new SseEmitter(DEFAULT_TIMEOUT));

    // 연결이 완료되거나 타임아웃 되면 emitter를 레포지토리에서 제거
    emitter.onCompletion(() -&gt; emitterRepository.deleteById(id));
    emitter.onTimeout(() -&gt; emitterRepository.deleteById(id));

    // 503 에러를 방지하기 위한(SSE 연결을 유지하기 위한) 더미 이벤트 전송
    sendToClient(emitter, id, &quot;EventStream Created. [userId=&quot; + userId + &quot;]&quot;);

    // 클라이언트가 미수신한 Event 목록이 존재할 경우 전송하여 Event 유실을 예방
    if (!lastEventId.isEmpty()) {
      Map&lt;String, Object&gt; events = emitterRepository.findAllEventCacheStartWithId(
          String.valueOf(userId));
      events.entrySet().stream()
          .filter(entry -&gt; lastEventId.compareTo(entry.getKey()) &lt; 0)
          .forEach(entry -&gt; sendToClient(emitter, entry.getKey(), entry.getValue()));
    }
    return emitter;
  }

  // 사용자에게 새 알림을 생성하고 저장한 후, 해당 사용자의 모든 SSE 연결에 이 알림을 전송
  @Transactional
  public void send(User user, String type, Long entityId, User actionUser) {
    // 새로운 createNotification 메서드를 사용하여 알림 생성
    Notification notification = createNotification(user, type, entityId, actionUser);

    // 알림 저장 및 전송 로직
    saveAndSendNotification(notification, user.getId());
  }

  private void saveAndSendNotification(Notification notification, Long userId) {
    // 생성된 알림 데이터베이스 저장
    notificationRepository.save(notification);

    // 사용자 ID를 기반으로 모든 SSE 연결 찾음
    Map&lt;String, SseEmitter&gt; sseEmitters = emitterRepository.findAllStartWithById(
        String.valueOf(userId));

    // 각 SSE 연결에 대해, 생성된 알림을 캐시에 저장 후 클라이언트에게 전송
    sseEmitters.forEach(
        (key, emitter) -&gt; {
          emitterRepository.saveEventCache(key, notification);
          sendToClient(emitter, key, new NotificationResponseDto(notification));
        }
    );
  }

  // 알림 생성
  public Notification createNotification(User user, String entityType, Long entityId,
      User actionUser) {
    String url = &quot;&quot;;
    String content = &quot;&quot;;

    switch (entityType) {
      case &quot;REVIEW&quot;:
        Optional&lt;Review&gt; optionalReview = reviewRepository.findById(entityId);
        if (optionalReview.isPresent()) {
          Review review = optionalReview.get();
          if (actionUser != null &amp;&amp; review.getUser() != null) {
            url = &quot;/productDetails?productId=&quot; + review.getProduct().getId()
                + &quot;&amp;categoryId=&quot; + review.getProduct().getCategory().getId();
            content = actionUser.getNickname() + &quot;님이 &quot; + review.getUser().getNickname()
                + &quot;님의 리뷰에 좋아요를 눌렀습니다.&quot;;
          }
        }
        break;

      case &quot;REPORT&quot;:
        Optional&lt;Report&gt; optionalReport = reportRepository.findById(entityId);
        if (optionalReport.isPresent()) {
          Report report = optionalReport.get();
          if (report.getUser() != null) {
            url = &quot;/main/report&quot;;
            String statusString = report.getStatus().name().toString();

            switch (statusString) {
              case &quot;PROCEEDING&quot;:
                content = report.getUser().getNickname() + &quot;님이 제보한 상품 &quot; + report.getName()
                    + &quot;가(이) 진행중입니다.&quot;;
                break;
              case &quot;ADOPTED&quot;:
                content = report.getUser().getNickname() + &quot;님이 제보한 상품 &quot; + report.getName()
                    + &quot;가(이) 채택되었습니다.&quot;;
                break;
              case &quot;UN_ADOPTED&quot;:
                content = report.getUser().getNickname() + &quot;님이 제보한 상품 &quot; + report.getName()
                    + &quot;가(이) 비채택되었습니다.&quot;;
                break;
              default:
                content = &quot;알 수 없는 상태입니다.&quot;;
            }
          }
        }
        break;

      case &quot;ROOM&quot;:
        // 채팅방 ID를 기반으로 채팅방 정보 조회
        Optional&lt;Room&gt; optionalRoom = roomRepository.findById(entityId);
        if (optionalRoom.isPresent()) {
          Room room = optionalRoom.get();
          User partner = room.getPartner(user.getId()); // 상대방 정보

          if (partner.getRole().equals(UserRoleEnum.ADMIN)) {
            url = &quot;/main/chat?userId=&quot; + partner.getId();
          } else {
            url = &quot;/admin/chat?userId=&quot; + partner.getId();
          }
          content = partner.getNickname() + &quot;님에게 문의가 왔습니다.&quot;;
        }
        break;

      // 다른 케이스에 대한 처리...

      default:
        // 유효하지 않은 entityType인 경우
        return null;
    }

    NotificationRequestDto requestDto = NotificationRequestDto.builder()
        .user(user)
        .content(content)
        .url(url)
        .entityType(entityType)
        .entityId(entityId)
        .isRead(false)
        .build();

    return new Notification(requestDto);
  }


  // SseEmitter 객체 사용하여 클라이언트에게 이벤트 전송하는 메서드
  public void sendToClient(SseEmitter emitter, String id, Object data) {
    try {
      String jsonData = new ObjectMapper().writeValueAsString(data);
      emitter.send(SseEmitter.event().id(id).name(&quot;sse&quot;).data(jsonData));
    } catch (IOException exception) {
      emitterRepository.deleteById(id);
      log.error(&quot;SSE 연결 오류&quot;, exception);
    }
  }


  // 모든 알림 조회
  @Transactional
  public NotificationsResponseDto findAllById(User user) {
    List&lt;NotificationResponseDto&gt; responses = notificationRepository.findAllByUserId(user.getId())
        .stream()
        .map(NotificationResponseDto::new) // 생성자를 사용하여 객체 생성
        .collect(Collectors.toList());
    long unreadCount = responses.stream()
        .filter(notification -&gt; !notification.isRead())
        .count();

    return new NotificationsResponseDto(responses, unreadCount);
  }

  // 지정된 ID의 알림을 찾아 &#39;읽음&#39; 상태로 변경
  @Transactional
  public void readNotification(Long notificationId) {
    Notification notification = notificationRepository.findById(notificationId)
        .orElseThrow(() -&gt; new ApiException(&quot;존재하지 않는 알림입니다.&quot;, HttpStatus.NOT_FOUND));
    notification.read();
  }

  // 알림 서비스 내에 알림 삭제 메서드 추가
  @Transactional
  public void deleteNotification(Long notificationId) {
    Notification notification = notificationRepository.findById(notificationId)
        .orElseThrow(() -&gt; new ApiException(&quot;존재하지 않는 알림입니다.&quot;, HttpStatus.NOT_FOUND));
    notificationRepository.delete(notification);
  }


  // 알림 삭제
  public void deleteNotificationByEntity(String entityType, Long entityId) {
    List&lt;Notification&gt; notificationsToDelete = notificationRepository.findByEntityTypeAndEntityId(
        entityType, entityId);

    // 해당 항목(예: 리뷰 또는 제보)과 관련된 모든 알림을 삭제
    notificationRepository.deleteAll(notificationsToDelete);
  }
}</code></pre><h2 id="--emitterrepository">- EmitterRepository</h2>
<pre><code>// SSE를 위한 emitter 객체와 관련된 이벤트 데이터를 관리하는 클래스
// &#39;Map&#39;을 사용하여 인스턴스 이벤트 데이터를 메모리 내 관리하는 특정한 방식의 데이터 관리를 구현
@Repository
public class EmitterRepository {

  // ID-KEY / SseEmitter객체-value, SSE 연결을 저장하고 관리
  public final Map&lt;String, SseEmitter&gt; emitters = new ConcurrentHashMap&lt;&gt;();

  // 이벤트 데이터를 캐싱하는 데 사용 / 각 이벤트에는 고유한 ID 존재
  private final Map&lt;String, Object&gt; eventCache = new ConcurrentHashMap&lt;&gt;();


  // SseEmitter 객체 저장
  public SseEmitter save(String id, SseEmitter sseEmitter) {
    emitters.put(id, sseEmitter);
    return sseEmitter;
  }

  // 이벤트 데이터를 캐시에 저장
  public void saveEventCache(String id, Object event) {
    eventCache.put(id, event);
  }

  // 주어진 ID로 시작하는 모든 SseEmitter 객체를 찾아 반환
  public Map&lt;String, SseEmitter&gt; findAllStartWithById(String id) {
    return emitters.entrySet().stream()
        .filter(entry -&gt; entry.getKey().startsWith(id))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
  }

  // 주어진 ID로 시작하는 모든 이벤트 데이터를 찾아 반환
  public Map&lt;String, Object&gt; findAllEventCacheStartWithId(String id) {
    return eventCache.entrySet().stream()
        .filter(entry -&gt; entry.getKey().startsWith(id))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
  }

  // 주어진 ID로 시작하는 모든 SseEmitter 객체 삭제
  public void deleteAllStartWithId(String id) {
    emitters.forEach(
        (key, emitter) -&gt; {
          if (key.startsWith(id)) {
            emitters.remove(key);
          }
        }
    );
  }</code></pre><p><strong>나머지 dto와 repository 등은 구조는 다른 기능들과 비슷하다!!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/16 SSE]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230116-SSE</link>
            <guid>https://velog.io/@yeong_do/TIL-20230116-SSE</guid>
            <pubDate>Tue, 16 Jan 2024 12:48:17 GMT</pubDate>
            <description><![CDATA[<p>인터넷은 웹 브라우저와 웹 서버 간의 데이터 통신을 위해서 HTTP 표준 위에 구축되어 있다. 대부분의 경우 웹 브라우저인 클라이언트가 HTTP 요청을 서버에 보내고, 서버는 적절한 응답을 반환하는데 이런 왕복 통신은 주소를 브라우저에 입력했을 때 웹 페이지를 받게 되는 과정이다.</p>
<p>이러한 HTTP 표준은 광범위하게 지원되지만, 애플리케이션이 연속적인 정보를 서버에 전송하거나, 실시간으로 업데이트된 서버의 정보를 클라이언트에게 보내야 하는 경우 지속적인 HTTP 요청을 하게 되기에 비용면에서 매우 비 효율적이다. 이런 상황에서 폴링, 웹소켓, 그리고 SSE가 등장했는데, 이들은 데이터 스트림의 속도와 메모리 효율성에 중점을 둔 프로토콜</p>
<p><strong>1. Short Polling
2. Long Polling
3. SSE(Server-Sent-Events)
4. WebSocket</strong></p>
<h2 id="1-short-polling">1. Short Polling</h2>
<p>Short Polling은 클라이언트가 서버로부터 정기적인 정보를 받기 위한 프로토콜이다. 클라이언트가 주기적으로 서버로 요청을 보내 데이터를 받아오는 방법이다.</p>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/85439ee7-de76-408a-be4e-f97dafd3a556/image.png" alt=""></p>
<p>클라이언트가 Http Request를 서버로 계속 보내서 이벤트 내용을 전달받는 방식이며, 클라이언트가 지속적으로 Request를 서버에 보내기 때문에 클라이언트가 많아지면 서버의 부담이 증가. 또한 TCP의 connection을 맺고 끊는 것 자체가 매우 무겁고, 이에 따라 실시간으로 변화되는 빠른 정보의 응답을 기대하기는 어렵다.</p>
<p><strong>하지만 클라이언트와 서버의 구현이 모두 단순하다는 장점이 있고, 서버가 요청에 대한 부담이 크지 않고 요청 주기를 넉넉하게 잡아도 될 정도로 실시간성이 중요하지 않다면 고려해 볼 만한 방법이다.</strong></p>
<h2 id="2-long-polling">2. Long Polling</h2>
<p>Long Polling은 Short Polling 보다는 더 효율적인 방식이다.</p>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/bcdef162-57de-457d-8347-d8927b38e965/image.png" alt=""></p>
<p>Long Polling은 Short Polling에 비해 동일한 양의 데이터를 클라이언트에게 전송하는데 필요한 HTTP 요청 수를 줄일 수 있다. 하지만 서버로부터 응답을 받고 나면 다시 연결 요청을 하기 때문에, 상태가 빈번하게 바뀐다면 연결 요청도 늘어나 서버에 부담이 가는 것은 변하지 않는다.</p>
<p><strong>따라서 실시간 메시지 전달이 중요하지만 서버의 상태가 빈번하게 변하지 않는 경우에 적합.</strong></p>
<h2 id="3-sseserver-sent-events">3. SSE(Server-Sent-Events)</h2>
<p>SSE는 서버와 한번 연결을 맺고 나면, 일정 시간 동안 서버에서 변경이 발생할 때마다 서버에서 클라이언트로 데이터를 전송하는 방법.</p>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/5be3dcac-6a1b-428f-9656-d2c1f0295692/image.png" alt=""></p>
<p>SSE는 상황에 따라서 응답마다 다시 요청을 해야 하는 Long Polling 방식보다 효율적이다. SSE는 서버에서 클라이언트로 text message를 보내는 브라우저 기반 웹 애플리케이션 기술이며 HTTP의 persistent connections을 기반으로 하는 HTML5 표준 기술그러나 HTTP를 통한 SSE(HTTP/2가 아닐 경우)는 브라우저 당 6개의 연결로 제한되므로, 사용자가 웹 사이트의 여러 탭을 열면 첫 6개의 탭 이후에는 SSE가 작동하지 않는다는 단점도 있음. (HTTP/2에서는 100개까지의 접속을 허용)</p>
<p><strong>전반적으로 SSE는 클라이언트가 서버와 크게 통신할 필요 없이 단지 업데이트된 데이터만 받아야 하는 실시간 데이터 스트림에 대한 구현이 필요할 때는 매우 훌륭한 선택이다.</strong></p>
<h2 id="4-websocket">4. WebSocket</h2>
<p>WebSocket은 OSI 네트워크 모델의 4 계층 프로토콜인 TCP에 기반한 양방향 메시지 전달 프로토콜 WebSockets은 프로토콜 오버헤드가 적고, 네트워크 스택에서 더 낮은 수준에서 동작하기 때문에 HTTP보다 데이터 전송이 빠르다.</p>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/06f477ea-51e8-4264-86c6-c668ee92516f/image.png" alt=""></p>
<p>웹 소켓의 가장 큰 장점은 속도이다. 클라이언트와 서버는 메시지를 전송할 때마다 서로의 연결을 찾아서 다시 설정할 필요가 없고 웹 소켓 연결이 설정되면 데이터는 어느 방향으로든 즉시 안전하게 전송될 수 있다. (TCP이기에 메시지가 항상 순서대로 도착하도록 보장됨)</p>
<p>하지만 단점은 초기 구현에 상당히 많은 비용이 들어간다는 점. 웹소켓은 멀티 온라인 게임과 같은 실시간 상태 업데이트와 긴밀한 동기화를 통해 분산된 사용자에게 전송해야 하는 경우 유용하다.</p>
<p><strong>따라서 전반적으로 웹 소켓은 빠른 고품질의 양방향 연결이 필요하다면 좋은 선택이지만, 시스템에 상당한 복잡성을 추가하고 구현하는 데 많은 투자가 필요하므로 폴링이나 SSE가 적합하지 않은 경우에만 사용하는 것을 권장</strong></p>
<h4 id="느낀점">느낀점</h4>
<p>나는 이번 프로젝트에 실시간 알림을 구현할 예정인데 SSE가 적합한 거 같다!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/09 Redis]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230109-Redis</link>
            <guid>https://velog.io/@yeong_do/TIL-20230109-Redis</guid>
            <pubDate>Tue, 09 Jan 2024 11:06:18 GMT</pubDate>
            <description><![CDATA[<h1 id="redis란">Redis란?</h1>
<ul>
<li>레디스(Redis)는 메모리 기반의 데이터 저장소이다. 키-밸류(key-value) 데이터 구조에 기반한 다양한 형태의 자료 구조를 제공하며, 데이터들을 저장할 수 있는 저장소이다. 최신 버전의 레디스는 PUB/SUB 형태의 기능을 제공하여 메세지를 전달할 수 있다. 즉, 데이터 저장 뿐만 아니라 다양한 목적으로 사용할 수 있다.레디스는 메모리에 데이터를 저장하기 때문에 저장 공간에 제약이 있어, 주로 보조 데이터 저장소로 사용한다. 이를 극복하기 위해 레디스 클러스터 기능을 제공하고 있어 저장 공간을 확장할 수 있다. 또한 저장된 데이터를 영구적으로 디스크에 저장할 수 있는 백업 기능을 제공하므로 애플리케이션의 주 저장소로도 사용할 수 있다. 또한 메모리에 데이터를 저장하기 때문에 빠른 처리 속도가 장점이다. 레디스 내부에서 명령어를 처리하는 부분은 싱글 스레드 아키텍처로 구현되어 있다. 멀티 스레드 아키텍처보다 구조가 단단하게 설계되어 여러 장점이 있다.</li>
</ul>
<h3 id="장점은-빠른-처리-속도-단점은-저장-공간-제약">장점은 빠른 처리 속도, 단점은 저장 공간 제약</h3>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/f12b036b-3ad0-4cd2-a4fd-d1ebda4fcdb2/image.png" alt=""></p>
<h2 id="레디스의-특징">레디스의 특징</h2>
<ul>
<li>영속성을 지원하는 인메모리 데이터 저장소</li>
<li>읽기 성능 증대를 위한 서버 측 복제를 지원</li>
<li>쓰기 성능 증대를 위한 클라이언트 측 샤딩(Sharding) 지원</li>
<li>다양한 서비스에서 사용되며 검증된 기술</li>
<li>문자열, 리스트, 해시, 셋, 정렬된 셋과 같은 다양한 데이터형을 지원. 메모리 저장소임에도 불구하고 많은 데이터형을 지원하므로 다양한 기능을 구현</li>
</ul>
<h3 id="캐시란">캐시란?</h3>
<ul>
<li>사용자에 입장에서 데이터를 더 빠르게, 더 효율적으로 액세스를 할 수 있는 임시 데이터 저장소를 뜻한다. 대부분의 어플리케이션에서 속도 향상을 위해 캐시를 사용한다고 한다.</li>
</ul>
<h3 id="캐싱-전략-caching-strategies">캐싱 전략 (Caching Strategies)</h3>
<ul>
<li>보통 레디스를 cache로 사용할 때 레디스를 어떻게 배치하냐에 따라 시스템 전체 성능에 큰 영향을 끼친다고 한다. 그래서 레디스를 어디에 어떻게 배치할 지에 대한 전략을 세워야 하는데! 이를, 캐싱 전략(Caching Strategies)이라고 한다. 캐싱 전략은 데이터의 유형과 해당 데이터에 대한 액세스 패턴을 잘 고려해서 선택해야 한다.</li>
</ul>
<h3 id="읽기-전략read-cache-strategy">읽기 전략(Read Cache Strategy)</h3>
<p><strong>1. Look Aside 전략</strong>
일반적으로 많이 사용되는 전략</p>
<p>기본적으로 Cache에서 데이터를 확인하고, 여기 없다면 DB를 통해 조회해 오는 방식</p>
<ul>
<li>먼저 Cache에 데이터가 있는지 찾아봄</li>
<li>없으면 다음으로 DB에 데이터를 찾아봄</li>
<li>DB에 데이터가 있는 경우 DB에서 가져온 데이터를 캐시에 저장</li>
<li>이후 데이터를 사용자에게 전달</li>
</ul>
<p><strong>2. Read Through 전략</strong>
캐시에서만 데이터를 불러오는 전략</p>
<p>사실 위의 Look Aside와 비슷하지만, 다른점은 Cache에 데이터가 없을 때에 Server가 아니라, Cache가 DB에 데이터를 조회하여 업데이트 한 후에 그 데이터를 보여주는 방식</p>
<ul>
<li>먼저 Cache에 데이터가 있는지 찾아봄</li>
<li>없으면 Cache가 DB에서 데이터를 찾아봄</li>
<li>DB에 데이터가 있는 경우 DB에서 가져온 데이터를 캐시에 저장</li>
<li>캐시를 통해 이 데이터를 사용자에게 전달</li>
</ul>
<br>

<h3 id="쓰기-전략write-cache-strategy">쓰기 전략(Write Cache Strategy)</h3>
<p><strong>1. Write Back 전략</strong>
데이터를 삽입할 때에 이를 캐시에 보관해 두고, 일정 시간마다 DB에 저장시키는 방식</p>
<p>쓰기가 굉장히 빈번하게 일어나는 경우 사용하는 방식
-&gt; 로그 데이터의 경우 굉장히 빈번하게 발생되고, 이를 DB에 매번 저장하기 그러니까 이 방식이 요긴하게 쓰인다고 한다.</p>
<ul>
<li>Cache에 데이터를 저장시켜 준다.</li>
<li>특정 시간마다 DB에 해당 데이터를 저장시킨다.</li>
<li>DB에 데이터가 저장되면 Cache에서는 이걸 지울수도 있고, 가지고 있을수도 있다.</li>
</ul>
<p><strong>2. Write Through 전략</strong>
DB와 Cache에 동시에 데이터를 저장하는 전략</p>
<ul>
<li>데이터 write시 Cache에 데이터 저장</li>
<li>Cache에서 이를 바로 DB에 저장(batch 아님)</li>
<li>Cache에는 데이터 저장되어 있음.</li>
</ul>
<p><strong>3. Write Around 전략</strong>
데이터를 DB에 저장하고, 캐시에 값이 없는 경우 저장시켜주는 방식</p>
<ul>
<li>데이터 write시 DB에 데이터 저장</li>
<li>데이터 read시 Cache를 통해 찾아봄</li>
<li>Cache에 해당 데이터가 없는 경우 DB를 통해 찾고, 서버가 해당 데이터를 DB에 저장해준다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/08 ElasticSearch]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230108-ElasticSearch</link>
            <guid>https://velog.io/@yeong_do/TIL-20230108-ElasticSearch</guid>
            <pubDate>Mon, 08 Jan 2024 12:04:37 GMT</pubDate>
            <description><![CDATA[<p>프로젝트에서 성능 향상을 위해 이리저리 많이 찾아보다가 엘라스틱서치를 발견했다.</p>
<h2 id="elasticsearch-란">Elasticsearch 란?</h2>
<p>Elasticsearch는 텍스트, 숫자, 위치 기반 정보, 정형 및 비정형 데이터 등 모든 유형의 데이터를 위한 무료 검색 및 분석 엔진으로 분산형과 개방형을 특징으로 합니다. Elasticsearch는 Apache Lucene을 기반으로 구축되었으며, Elasticsearch N.V.(현재 명칭 Elastic)가 2010년에 최초로 출시했습니다. 간단한 REST API, 분산형 특징, 속도, 확장성으로 유명한 Elasticsearch는 데이터 수집, 보강, 저장, 분석, 시각화를 위한 무료 개방형 도구 모음인 Elastic Stack의 핵심 구성 요소입니다. 보통 ELK Stack(Elasticsearch, Logstash, Kibana의 머리글자)이라고 하는 Elastic Stack에는 이제 데이터를 Elasticsearch로 전송하기 위한 경량의 다양한 데이터 수집 에이전트인 Beats가 포함되어 있습니다.
**
Elasticsearch는 단독 검색을 위해 사용하거나, ELK(Elasticsearch &amp; Logstash &amp; Kibana) 스택을 기반으로 사용합니다.**</p>
<ul>
<li>프로젝트에서 Elasticsearch(이하 ES)의 사용 이유는 ES에서 제공하는 특정 검색 기능을 쓰기 위해서이다.</li>
<li>이 기능을 쓰기 위해서 우선 테이블 역할을 하는 인덱스(index)를 생성하고 데이터를 저장해야하고, 검색 API를 요청해야 한다.</li>
</ul>
<p><strong>- gradle 이용</strong></p>
<pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-data-elasticsearch:2.6.2&#39;</code></pre><p><strong>- ELK를 이용한 로깅 구성</strong>
마이크로서비스 아키텍처에서 가장 많이 사용하는 로깅 방식으로는 ELK 와 EFK가 있으며 ELK는 Elasticsearch, Logstash, Kibana로 구성되고 EFK는 Elasticsearch, Fluentd, Kibana로 로깅을 구성한다. ELK의 각 솔루션별 기능을 보면 logstash를 통해 데이터를 집계하고 처리한 다음 elasticsearch를 통해 원하는 데이터 항목을 인덱싱하고 저장하고 kibana를 통해 이렇게 인덱싱된 데이터를 보여주는(Visualization) 구조를 가진다.</p>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/1f33995c-26e1-4611-ab3f-6dfeb5fdceac/image.png" alt=""></p>
<p>마치며..
검색을 할 때 성능을 높이는 엔진을 사용 해보고싶긴 하지만 음.. 대용량에서 성능이 좋아진다고하긴 하는데 소규모라면 소규모프로젝트에 굳이 적용할 필요가 있나(과한가?) 싶은 생각도 있어서 아직 보류중! 화이팅~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/05 QueryDSL]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230105-QueryDSL</link>
            <guid>https://velog.io/@yeong_do/TIL-20230105-QueryDSL</guid>
            <pubDate>Fri, 05 Jan 2024 11:43:02 GMT</pubDate>
            <description><![CDATA[<h2 id="querydsl이란">QueryDSL이란?</h2>
<p>QueryDSL은 하이버네이트 쿼리 언어(HQL: Hibernate Query Language)의 쿼리를 타입에 안전하게 생성 및 관리해주는 프레임워크이다.
QueryDSL은 정적 타입을 이용하여 SQL과 같은 쿼리를 생성할 수 있게 해 준다.
 
자바 백엔드 기술은 Spring Boot와 Spring Data JPA를 함께 사용한다. 하지만, 복잡한 쿼리, 동적 쿼리를 구현하는 데 있어 한계가 있다. 이러한 문제점을 해결할 수 있는 것이 QueryDSL이다.
 
QueryDSL이 등장하기 이전에는 Mybatis, JPQL, Criteria 등 문자열 형태로 쿼리문을 작성하여 컴파일 시에 오류를 발견하는 것이 불가능했다.
하지만, QueryDSL은 자바 코드로 SQL 문을 작성할 수 있어 컴파일 시에 오류를 발생하여 잘못된 쿼리가 실행되는 것을 방지할 수 있다.</p>
<h2 id="jpql과-querydsl-비교">JPQL과 QueryDSL 비교</h2>
<p>**
JPQL**</p>
<pre><code>String username = &quot;java&quot;;
String jpql = &quot;select m from Member m where m.username = :username&quot;;

List&lt;Member&gt; result = em.createQuery(query, Member.class).getResultList();</code></pre><p><strong>QueryDSL</strong></p>
<pre><code>String username = &quot;java&quot;;

List&lt;Member&gt; result = queryFactory
        .select(member)
        .from(member)
        .where(usernameEq(username))
        .fetch();</code></pre><h2 id="qclass-란">QClass 란?</h2>
<p>QueryDSL 은 컴파일 단계에서 엔티티를 기반으로 QClass 를 생성하는데 JPAAnnotationProcessor 가 컴파일 시점에 작동해서 @Entity 등등의 어노테이션을 찾아 해당 파일들을 분석해서 QClass 를 만든다.</p>
<p>QClass 는 Entity 와 형태가 똑같은 Static Class 이다.
QueryDSL 은 쿼리를 작성할 때 QClass 를 기반으로 쿼리를 실행한다.</p>
<h2 id="querydsl을-사용하는-이유">QueryDSL을 사용하는 이유</h2>
<ul>
<li>자바코드로 작성하게 됨으로서 컴파일 에러로 쿼리의 실수를 잡을 수 있다.
JPQL을 사용하면 쿼리를 문자열에 감싸서 작성해야한다. 문자열은 결국 오타가 나거나 실수로 잘못된 입력을 해도 그것을 실행시키기 전까지는 잘못된 부분을 잡을 수 있는 방법이 없다. 하지만 QueryDSL을 사용하면 자바코드로 작성하기 때문에 실수를 했을 때 컴파일 에러로 모두 잡을 수 있다. 에러중 가장 좋은 에러는 역시 컴파일 에러다.</li>
<li>복잡한 동적쿼리를 쉽게 다룰 수 있다. JPQL을 이용해서 동적쿼리를 다루려면 문자열을 조건에 맞게 조합해서 사용해야한다. 물론 이렇게도 사용할 수는 있지만 굉장히 복잡하고 코드도 난해해진다.</li>
</ul>
<p><strong>문자열로 코드를 작성해야하기 때문에 컴파일 에러도 잡을 수 없는데 복잡하고 난해하기까지 하면 어쩔 수 없이 실수가 나올 수 밖에 없다. 그렇게 발생하는 오타나 잘못된 입력으로 인한 실수는 찾기도 쉽지 않기 때문에 동적쿼리의 문제를 극복하는 것은 굉장히 큰 의미가 있다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/04 최종프로젝트]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230104-%EC%B5%9C%EC%A2%85%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@yeong_do/TIL-20230104-%EC%B5%9C%EC%A2%85%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Fri, 05 Jan 2024 01:07:03 GMT</pubDate>
            <description><![CDATA[<h2 id="1-프로젝트">1. 프로젝트</h2>
<ul>
<li>프로젝트 명 :   주酒총회</li>
<li>소개 (●&#39;◡&#39;●)💗<ul>
<li>한 줄 정리 :  다양한 종류의 술들을 검색하고, 평점과 리뷰를 통해 원하는 상품을 찾을 수 있도록 도와주는 웹 서비스<ul>
<li>내용 :<ul>
<li>주류 카테고리로 나누고 그 안에서 평점순/ 찜많은 순 /리뷰 많은 순/ (신제품순 으로 비교할 수 있고</li>
<li>편의점 별 상품 필터</li>
<li>찜 기능과 <strong>인증</strong>을 통해 마셔본 술을 등록할 수도 있다. ⇒ <strong>배찌 기능/사용자 간 랭킹</strong></li>
<li>주류를 클릭하면 주류에 대한 설명(성분정보) 과 (판매처/지역) 등이 있고, 어울리는 안주 추천, 평점과 리뷰를 등록할 수 있다.</li>
<li>상품은 관리자가 등록하고, 사용자는 제보할 수 있다.</li>
<li>추천 기능(판매량 정보 가져와서)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="2-기획-관련-메모">2. 기획 관련 메모</h2>
<ul>
<li><strong>기획 배경, 기획 의도</strong><ul>
<li>포스트 코로나 시대 다양한 주류에 대한 관심은 높아지고 탐색과 구매의 허들은 낮아졌습니다. 특히 와인, 위스키 등의 주류는 매니아 층만 향유하는 분야에서 벗어나 다양한 사람들이 즐길 수 있는 분야가 되어가고 있습니다.</li>
<li>이에 다양한 주류에 대한 정보를 원하는 상황에 검색하고, 비슷한 취향을 가진 사람들이 모여 리뷰를 나눌 수 있고,  마셔본 사용자의 인증을 통해 더 신뢰있는 리뷰 서비스를 제공하고자 합니다.</li>
</ul>
</li>
<li><strong>서비스 타겟</strong><ul>
<li>다양한 주류를 경험하고 싶지만 정보 획득이 어려운 사용자</li>
<li>이벤트나 선물을 위한 주류 정보를 찾고 있는 사용자</li>
<li>우연히 발견한 제품에 대한 정보(구매처, 가격대 등)를 얻고 싶은 사용자</li>
<li>술과 어울리는 안주에 대한 정보를 찾는 사용자</li>
</ul>
</li>
<li><strong>기대 효과</strong><ul>
<li>사용자들이 선호하는 제품을 용이하게 찾을 수 있어 구매자의 만족도 향상이 기대됩니다.</li>
<li>정보의 쉬운 접근성으로 빠른 구매 결정을 도와주어, 사용자들의 편의성을 향상시킬 것으로 예상합니다.</li>
<li>다양한 주류에 대한 정보를 획득함으로써 사용자들의 주류 관련 지식과 흥미 증가 기대됩니다.</li>
<li>사용자들은 다양한 리뷰와 사용자들의 인증을 통해 제품에 대한 더욱 높은 신뢰도를 가지고 구매 결정에 도움이 될 것입니다.</li>
</ul>
</li>
<li><strong>프로젝트 확장성</strong><ul>
<li>커뮤니티 기능을 통해 사용자들은 서로 소통하고 주류에 대한 경험을 공유할 수 있고  다양한 의견과 정보를 얻을 수 있습니다.</li>
<li>스토어 기능을 통해 다양한 제휴 및 광고 수익을 창출할 수 있고, 판매수익을 얻을 수 있습니다.</li>
</ul>
</li>
</ul>
<h2 id="3-figma">3. Figma</h2>
<p><a href="https://www.figma.com/proto/j0LU5OYqxKP1qBBMyEDQSm/%EC%A3%BC%EC%A3%BC%EC%B4%9D%ED%9D%AC?kind=proto&amp;node-id=1-5&amp;page-id=0%3A1&amp;scaling=scale-down&amp;show-proto-sidebar=1&amp;starting-point-node-id=1%3A5&amp;t=CIaaQw1Z141EKwk2-0&amp;type=design">https://www.figma.com/proto/j0LU5OYqxKP1qBBMyEDQSm/%EC%A3%BC%EC%A3%BC%EC%B4%9D%ED%9D%AC?kind=proto&amp;node-id=1-5&amp;page-id=0%3A1&amp;scaling=scale-down&amp;show-proto-sidebar=1&amp;starting-point-node-id=1%3A5&amp;t=CIaaQw1Z141EKwk2-0&amp;type=design</a></p>
<h2 id="4-erd">4. ERD</h2>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/92b75b0f-17de-4529-ac2b-969619f72fe7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/01/03 코딩에정신팔렸조 KPT]]></title>
            <link>https://velog.io/@yeong_do/TIL-20230103-%EC%BD%94%EB%94%A9%EC%97%90%EC%A0%95%EC%8B%A0%ED%8C%94%EB%A0%B8%EC%A1%B0-KPT</link>
            <guid>https://velog.io/@yeong_do/TIL-20230103-%EC%BD%94%EB%94%A9%EC%97%90%EC%A0%95%EC%8B%A0%ED%8C%94%EB%A0%B8%EC%A1%B0-KPT</guid>
            <pubDate>Wed, 03 Jan 2024 11:09:07 GMT</pubDate>
            <description><![CDATA[<h2 id="심화프로젝트-팀과제-trello-만들기-kpt-회고">심화프로젝트 팀과제 &quot;Trello&quot; 만들기 KPT 회고</h2>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/609dcf2a-aaa8-4ceb-bb49-424bfe0177f0/image.png" alt=""></p>
<p><a href="https://github.com/SpartaTrelloA08/SpartaTrelloA08Backend">https://github.com/SpartaTrelloA08/SpartaTrelloA08Backend</a></p>
<h2 id="keep">KEEP</h2>
<ul>
<li>협업시에 정형화된 PR양식과 코드리뷰를 활용하니 누가 어떤 기능을 개발했는지와 전체적인 프로젝트 진행 흐름을 파악할 수 있어 좋았습니다.</li>
<li>Postman을 사용하지않고 Service, Controller 테스트 코드로만 테스트를 진행했는데 문제가 발생하지 않는다는 것을 보고 테스트 코드 작성의 중요성을 알 수 있었습니다.</li>
<li>팀 협업 및 소통: 팀원 간의 소통이 원활했다. 자신이 할 수 있는 기능을 맡아서 구현하는 방식으로 업무를 분담했기 때문에, 업무 배분이 순조로웠다. 또한, 서로 밝은 분위기로 소통하여 편하게 이야기를 나눌 수 있었던 점 또한 긍정적이었다.</li>
<li>깃허브 활용력 상승: 이전 팀 프로젝트에서보다 깃허브를 조금 더 능숙하게 사용할 수 있게 되었음을 느꼈다. 팀원들이 커밋한 내용을 읽고 진척도를 확인할 수 있는 점이나 미숙했던 /stash 기능의 활용을 해볼 수 있는 기회였다.</li>
<li>이번프로젝트에서 spring의 구조와 돌아가는방식등에 대해서 감을 조금 잡은것 같아서 좋았습니다. 사실 git을 사용한제대로된 협업이 거의 이번이 처음이었는데 브랜치를 생성해 pl과 코멘트를 남기는 과정이 도움이 되었습니다.</li>
</ul>
<h2 id="problem">PROBLEM</h2>
<ul>
<li>와이어프레임을 작성하고 개발에 들어갔지만 프론트엔드를 구현하지못해 상호작용하며 개발하지 못한것이 아쉽습니다.</li>
<li>API 명세서 작성시에 domain을 타고들어가는 형식으로 작성했는데, 실제로 개발을 하다보니 url에는 PathVariable이 있는데 안쓰는 변수가 많았습니다.</li>
<li>각 기능에대해서 필요한 필드값이 처음 생각한 견적보다 늘어났다. erd설계를 좀더 면밀히할 필요가 있었다.</li>
<li>프론트를 생성하지 않고 api테스트등만 거치다보니 내가만든 api 서비스등이 실제로 어떤식으로 화면에 보여질지에 대한 생각이 부족했습니다.</li>
</ul>
<h2 id="try">TRY</h2>
<ul>
<li>백엔드 개발도 중요하지만, 프론트엔드에 대한 지식도 꾸준히 학습하고 쉽게 개발할 수 있는 형식이나 AI툴을 알아두면 좋을 것 같습니다.</li>
<li>사용하지 않는 변수를 어떻게 처리할지 고민해보고 Restful API를 설계하는 방법에 대해 더 공부하면 좋을 것 같습니다.</li>
<li>다음 프로젝트에는 테스트를 위해 제대로 테스트 코드를 작성하고 개발에 임하고 싶습니다. 내가 만든 코드가 효율적인 코드인지 설계는 제대로 되었는지 계속 체크해야겠습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/12/11 안경제비 조 KPT]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231211-KPT</link>
            <guid>https://velog.io/@yeong_do/TIL-20231211-KPT</guid>
            <pubDate>Tue, 12 Dec 2023 01:35:11 GMT</pubDate>
            <description><![CDATA[<h2 id="팀-안경-제비-조">팀 안경 제비 조</h2>
<h3 id="멤버-소개">멤버 소개</h3>
<p>팀 노션 : <a href="https://mighty-hoverfly-ed2.notion.site/534ed5e444b94c90b202140fc949b2e6?pvs=4">팀노션 링크</a>
팀 GIT : <a href="https://github.com/Youkamii/stairs">github</a>
<a href="https://github.com/MoonKiHyun">문기현</a> <a href="https://github.com/kkamjjing2">정유진</a> <a href="https://github.com/rawfk">안주환</a> <a href="https://github.com/yeongdo99">정영도</a> <a href="https://github.com/Youkamii">최혁</a></p>
<h3 id="프로젝트-소개">프로젝트 소개</h3>
<blockquote>
<p>1차 목표 -&gt; <strong>익명 커뮤니티 사이트 구현</strong>
최종 목표 -&gt; <strong>백오피스 SNS 구현</strong> ( 완성도 95% )
개발 일정 -&gt; 2023.12.05 ~ 2023.12.11</p>
</blockquote>
<h2 id="kpt-회고">KPT 회고</h2>
<h2 id="keep">KEEP</h2>
<ul>
<li>팀원들 각자 맡은 부분의 구현을 끝 마쳤다.</li>
<li>구현 부분에서 다양하고 새로운 시도를 하는데 주저함이 없었다.</li>
<li>구두와 문서의 두가지 소통 방법으로 개발을 진행할 때 막히는 부분 없이 빠르게 해결할 수 있었다.</li>
<li>부드러운 소통으로 본인이 하고싶어했던 기능들을 각자 맡아 구현할 수 있었고, 그 기능의 최선의 결과물을 위해 각자 맡은 기능을 열심히 구현하려고 노력했다.<h2 id="problem">PROBLEM</h2>
</li>
<li>목표한 부분까지의 완성을 하지 못했다. (팔로우 기능)</li>
<li>API 명세가 있지만 변수명사용에서 파트별 상호 이슈가 있었다.</li>
<li>목표 부분이 있지만 다양한 기능들을 추가하다보니 놓치는 부분들이 생겼다.</li>
<li>아무래도 일주일이라는 시간안에 맡은 기능을 구현해 다른 분들 코드와 합쳐야하다보니 다른 팀원분들의 진행상황이나 어떻게 설계했고 코드를 구성했는 지 파악을 잘 하지 못했다.<h3 id="trouble-shooting">TROUBLE SHOOTING</h3>
</li>
<li><code>swagger</code> 적용을 하면서 의존성별 충돌과 삽입된 api들의 활용 부분에서 처음 시도하는 부분이다보니 결과적으로는 간단한 문제였으나 쉽게 해결하지 못했다. 다양한 라이브러리를 사용하기보다 확실한 라이브러리 하나만을 사용하면서 내부 기능을 완벽하게 사용하기를 지향하기로 한다.</li>
<li><code>RefreshToken</code>에 대한 값을 <code>@RequestHeader</code>로 받아 올 때 반드시 넣지 않고 안 넣을 수 있는 경우가 존재했다. required 를<code>true</code> 에서 <code>false</code> 로 변경하여 해결할 수 있었다.</li>
<li>aws를 통하여 CI/CD 배포를 시도해보았다. 구현에는 성공했으나 완성된 프로젝트 결과를 업데이트 하여 발표에 이용하지 못한 부분이 아쉬움으로 남는다.</li>
<li>스프링 웹이 실행에서 에러를 겪었다.
ZonedDateTime 을 사용할 때 JPA의 TemporalType.TIME으로 지원이 되지 않기 때문에
ZonedDateTime를 JPA에서 사용하려면 Hibernate와 같은 JPA 구현체가 이를 해석할 수 있도록 설정을 해줌으로 해결하였다.<pre><code class="language-java"> @Temporal(TemporalType.TIME)
 protected ZonedDateTime modifiedAt = ZonedDateTime.now();
 해결 -&gt; @Temporal(TemporalType.TIME) -&gt; @Temporal(TemporalType.TimeStamp)</code></pre>
<h2 id="try">TRY</h2>
</li>
<li>맡은 기능도 열심히 구현하면서 중간중간 계속해서 팀원분들의 의견이나 코드들을 체크하면서 서로의 생각을 공유하고 코드를 보여주며 전체적으로 병합할 때 수월하게 합칠 수 있도록 노력한다.</br>
## 소감
#### 안주환
>처음에 프로젝트를 도메인 별로 설계하여 기능 별 구현 후 머지 시 충돌을 최대한 피해 협업을 진행할 수 있었습니다. 또 모두 열정적으로 프로젝트에 참여하여 Swagger, AWS 등 다양한 경험을 할 수 있어 즐거웠습니다.
#### 정유진
>CI/CD 를 하는 데 예전보다 익숙해졌다. 이제 에러도 나름 어느정도 해결할 줄 알게 됐다. 하지만 프로젝트 초반에 CI/CD 파이프 라인 구축을 하지 못한 점이 아쉽다.
이번 프로젝트에서 TDD를 적용해보고 싶었는데 잘 되지 않았던 것도 아쉽다. 다음 프로젝트를 시작하기 전에 미리 어느 정도 설계도 해보고 연습을 많이 해놔야겠다는 생각이 든다.
다음 프로젝트를 시작하기 전에 미리 어느 정도 설계도 해보고 연습을 많이 해놔야겠다는 생각이 든다.
팀 프로젝트에서 소통이 중요하다는 사실을 느꼈다. 그리고 프로젝트 기간이 짧은 만큼 시간 분배를 잘해야겠다는 생각이 들었다.
#### 문기현
> 이번 프로젝트를 통해 다양한 기술을 사용해 볼 수 있어서 좋았습니다. 또한 협업에 있어서 원활한 의사 소통이 얼마나 중요한지 다시한번 깨닫게 되는 계기가 되었습니다. Swagger를 경험해 볼 수 있었던 것이 좋았습니다. POSTMAN도 좋지만 Swagger를 사용했을 때 조금더 직관적이고 편리하게 구현한 API들을 테스트해 볼수 있었습니다.
#### 정영도
> 경험이나 숙련도가 다른 사람들과 새로운 협업을 하며 다양한 것들을 많이 배울 수 있어서 좋았다. 이번 프로젝트로 부족한 점이나 깨달은 점이 있다면 다음 프로젝트에 실천해봐야겠다.
#### 최 혁
> 모두 프로젝트에 열정적이라 진행함에 있어 즐거웠습니다.
프로젝트 시작 전 설계 부분에서 애매한 부분이 있다면 반드시 해당 부분을
확실하게 처리하고 프로젝트를 시작해야 추후 이슈가 생기지 않는다는 점을 배웠습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/12/08 JPA N+1 문제?]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231208-Spring</link>
            <guid>https://velog.io/@yeong_do/TIL-20231208-Spring</guid>
            <pubDate>Fri, 08 Dec 2023 13:07:20 GMT</pubDate>
            <description><![CDATA[<p>JPA를 사용하면 자주 만나게 되는 것이 <strong>N + 1 문제이다.</strong></p>
<h2 id="n1-문제란">N+1 문제란?</h2>
<p>연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다. 즉, 1번의 쿼리를 날렸을 때 의도하지 않은 N번의 쿼리가 추가적으로 실행되는 것이다. 이를 N+1 문제라고 한다.</p>
<h4 id="eager즉시-로딩인-경우">EAGER(즉시 로딩)인 경우</h4>
<p>JPQL에서 만든 SQL을 통해 데이터를 조회
이후 JPA에서 Fetch 전략을 가지고 해당 데이터의 연관 관계인 하위 엔티티들을 추가 조회
2번 과정으로 N + 1 문제 발생</p>
<h4 id="lazy지연-로딩인-경우">LAZY(지연 로딩)인 경우</h4>
<p>JPQL에서 만든 SQL을 통해 데이터를 조회
JPA에서 Fetch 전략을 가지지만, 지연 로딩이기 때문에 추가 조회는 하지 않음
하지만, 하위 엔티티를 가지고 작업하게 되면 추가 조회가 발생하기 때문에 결국 N + 1 문제 발생</p>
<h2 id="n1-문제-해결-방법">N+1 문제 해결 방법</h2>
<h4 id="1-fetch-join">1. Fetch join</h4>
<p>N+1 자체가 발생하는 이유는 한쪽 테이블만 조회하고 연결된 다른 테이블은 따로 조회하기 때문이다. 미리 두 테이블을 JOIN 하여 한 번에 모든 데이터를 가져올 수 있다면 애초에 N+1 문제가 발생하지 않을 것이다.</p>
<p>사실 우리가 원하는 코드는 select * from owner left join cat on cat.owner_id = owner.id 일 것이다. 최적화된 쿼리를 우리가 직접 사용할 수 있다. 그렇게 나온 해결 방법이 FetchJoin 방법이다. 하지만 이는 jpaRepository에서 제공해주는 것은 아니고 JPQL로 작성해야 한다.</p>
<p><strong>Fetch Join(패치 조인)의 단점</strong>
Fetch Join도 언뜻보면 유용해보이지만 단점은 있다. 우선은 우리가 연관관계 설정해놓은 FetchType을 사용할 수 없다는 것이다. Fetch Join을 사용하게 되면 데이터 호출 시점에 모든 연관 관계의 데이터를 가져오기 때문에 FetchType을 Lazy로 해놓는것이 무의미하다.</p>
<p>또한, 페이징 쿼리를 사용할 수 없다. 하나의 쿼리문으로 가져오다 보니 페이징 단위로 데이터를 가져오는것이 불가능하다.</p>
<h4 id="2-entity-graph">2. @Entity Graph</h4>
<p>@EntityGraph 의 attributePaths에 쿼리 수행시 바로 가져올 필드명을 지정하면 Lazy가 아닌 Eager 조회로 가져오게 된다. Fetch join과 동일하게 JPQL을 사용하여 query 문을 작성하고 필요한 연관관계를 EntityGraph에 설정하면 된다. 그리고 Fetch join과는 다르게 join 문이 outer join으로 실행되는 것을 확인할 수 있다.</p>
<h4 id="3-batchsize">3. BatchSize</h4>
<p>하이버네이트가 제공하는 org.hibernate.annotations.BatchSize 어노테이션을 이용하면 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회한다.</p>
<p>더 다양한 방법이 있지만 이정도 정리면 충분할 거 같다!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/12/05 Spring 의존성]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231207</link>
            <guid>https://velog.io/@yeong_do/TIL-20231207</guid>
            <pubDate>Thu, 07 Dec 2023 11:48:03 GMT</pubDate>
            <description><![CDATA[<h2 id="의존성이란"><strong>의존성이란?</strong></h2>
<p>의존성은 하나의 객체가 다른 객체의 기능이나 데이터에 의존하는 관계를 의미한다. 예를 들어, &#39;A&#39; 클래스가 &#39;B&#39; 클래스의 메소드를 사용한다면, &#39;A&#39;는 &#39;B&#39;에 의존하는 관계가 된다.</p>
<h2 id="의존성-주입의-기본-개념"><strong>의존성 주입의 기본 개념</strong></h2>
<p>스프링에서 의존성 주입 방식은 세 가지 </p>
<ol>
<li>생성자 주입 </li>
<li>세터 주입 </li>
<li>필드 주입</li>
</ol>
<h2 id="zoneddatetime">ZonedDateTime</h2>
<ul>
<li>ZonedDateTime은 한마디로 ZoneId + Instant이다.
즉, 특정 지역에 사는 사람이 현재 자신이 보고 있는 시간이 곳 ZonedDateTime이다.</li>
</ul>
<pre><code>ZoneId seoulZoneId = ZoneId.of(&quot;Asia/Seoul&quot;);
ZonedDateTime seoulTime = ZonedDateTime.now(seoulZoneId); // 실행 순간의 아시아-서울의 시간

// 존재하지 않는 곳의 시간에 접근하면 예외가 발생한다.
ZoneId noZone = ZoneId.of(&quot;Asia/Seoul&quot;);
ZonedDateTime noTime = ZonedDateTime.now(noZone); // 예외 발생</code></pre><p>모든 시간 작업은 UTC기준으로 이뤄져야 한다. 그러나 유저에게는 자신이 위치한 지역의 시간으로 보여져야 한다. 이런 목적을 위해 ZonedDateTime을 사용하는 것이다.</p>
<pre><code>ZoneId seoulZoneId = ZoneId.of(&quot;Asia/Seoul&quot;);
ZonedDateTime seoulTime = ZonedDateTime.now(seoulZoneId);
String seoulNow = seoulTime.toString(); // 2023-01-23T23:14:54.738995+09:00[Asia/Seoul]</code></pre><p>ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ</p>
<h1 id="알고리즘">알고리즘</h1>
<h3 id="모음제거">모음제거</h3>
<p><strong>- 문제 설명</strong>
영어에선 a, e, i, o, u 다섯 가지 알파벳을 모음으로 분류합니다. 문자열 my_string이 매개변수로 주어질 때 모음을 제거한 문자열을 return하도록 solution 함수를 완성해주세요.</p>
<pre><code>class Solution {
    public String solution(String my_string) {
        String answer = my_string;
        String[] vowels = {&quot;a&quot;, &quot;e&quot;, &quot;i&quot;, &quot;o&quot;, &quot;u&quot;};

        for (String v :vowels) {
            answer = answer.replaceAll(v,&quot;&quot;);
        }
        return answer;
    }
}</code></pre><h3 id="배열-원소의-길이">배열 원소의 길이</h3>
<p><strong>- 문제 설명</strong>
문자열 배열 strlist가 매개변수로 주어집니다. strlist 각 원소의 길이를 담은 배열을 retrun하도록 solution 함수를 완성해주세요.</p>
<pre><code>class Solution {
    public int[] solution(String[] strlist) {
        int[] answer = new int[strlist.length];

        for (int i=0; i&lt;answer.length; i++) {
            answer[i] = strlist[i].length();
        }
        return answer;
    }
}</code></pre><h3 id="배열-뒤집기">배열 뒤집기</h3>
<p><strong>- 문제 설명</strong>
정수가 들어 있는 배열 num_list가 매개변수로 주어집니다. num_list의 원소의 순서를 거꾸로 뒤집은 배열을 return하도록 solution 함수를 완성해주세요.</p>
<pre><code>class Solution {
    public int[] solution(int[] num_list) {
        int[] answer = new int[num_list.length];

        for (int i=0; i&lt;num_list.length; i++) {
            answer[num_list.length - i -1] = num_list[i];
        }

        return answer;
    }
}</code></pre><h2 id="마치며"><strong>마치며...</strong></h2>
<p>오늘은 오전에 코드카타 후 12시에 베이직반 수업을 들었다! 실습을 했는데 이해가 잘 되는 거 같다. 또 알고리즘 스터디가 있었는데 조금밖에 풀지 않았다. 그 후 팀프로젝트의 내가 맡은 역할을 다 하기 위해 열심히 공부하였다. 확실히 팀프로젝트가 지식을 습득하기엔 좋은 거 같다~~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/12/05 Enum]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231205-enum</link>
            <guid>https://velog.io/@yeong_do/TIL-20231205-enum</guid>
            <pubDate>Wed, 06 Dec 2023 11:55:13 GMT</pubDate>
            <description><![CDATA[<h1 id="enum-class란">Enum class란?</h1>
<p>우리가 흔히 상수를 정의할 때 final static string 과 같은 방식으로 상수를 정의한다. 하지만 이렇게 상수를 정의해서 코딩하는 경우 다양한 문제가 발생된다.
따라서 이러한 문제점들을 보완하기 위해 자바 1.5버전부터 새롭게 추가된 것이 바로 &quot;Enum&quot; 이다.</p>
<p>Enum은 열거형이라고 불리며, 서로 연관된 상수들의 집합을 의미한다.
기존에 상수를 정의하는 방법이였던 final static string 과 같이 문자열이나 숫자들을 나타내는 기본자료형의 값을 enum을 이용해서 같은 효과를 낼 수 있다.</p>
<h3 id="enum의-장점">Enum의 장점</h3>
<ol>
<li>코드가 단순해지며, 가독성이 좋다.</li>
<li>인스턴스 생성과 상속을 방지하여 상수값의 타입안정성이 보장된다.</li>
<li>enum class를 사용해 새로운 상수들의 타입을 정의함으로 정의한 타입이외의 타입을 가진 데이터값을 컴파일시 체크한다.</li>
<li>키워드 enum을 사용하기 때문에 구현의 의도가 열거임을 분명하게 알 수 있다.</li>
</ol>
<pre><code>public enum Season {
    SPRING, SUMMER, FALL, WINTER
}</code></pre><h2 id="마치며">마치며...</h2>
<p>팀 프로젝트를 하면서 열거형(enum)에 대해 다시 공부하게 되었다. 들어는 봤지만 자세히는 잘 몰랐어서 다시 한 번 공부했다. 오늘은 팀프로젝트를 위해 게시판에 대한 구조도 살펴보고, 전에 하다가 자꾸 오류나서 손 놔버린 TodoList를 제대로 동작하게끔 만들어놨다! 깃허브도 잘 정리해두었고 README.md 파일도 정리했다. 별로 배운 건 없는 거 같지만 시간은 순삭됐다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/12/05 REST API]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231205-Spring</link>
            <guid>https://velog.io/@yeong_do/TIL-20231205-Spring</guid>
            <pubDate>Tue, 05 Dec 2023 11:55:55 GMT</pubDate>
            <description><![CDATA[<h2 id="1-restful-api-설계">1. RESTful API 설계</h2>
<h4 id="1-restful-api란">1) RESTful API란</h4>
<ul>
<li>REST(Representational State Transfer)는 웹 표준을 기반으로 하는 API 설계 아키텍처</li>
<li>RESTful API는 네트워크 상에서 클라이언트와 서버 간의 통신을 위해 자원(Resource)의 상태를 전달하는 방법을 정의</li>
<li>클라이언트가 서버의 자원에 접근하고 이를 활용할 수 있도록 하는 역할을 한다.</li>
</ul>
<h4 id="2-자원-중심의-uri-설계">2) 자원 중심의 URI 설계</h4>
<ul>
<li><strong>자원의 식별</strong>: 각 자원은 명확한 URI로 식별되어야 한다.</li>
<li><strong>URI는 명사를 사용</strong>: 자원은 동사가 아닌 명사를 사용하여 표현 예: <code>/getUsers</code> 대신 <code>/users</code></li>
</ul>
<h4 id="3-http-메서드를-통한-행위-정의">3) HTTP 메서드를 통한 행위 정의</h4>
<ul>
<li><strong>GET</strong>: 자원을 조회 서버에 데이터를 변경하지 않는 안전한(read-only) 작업에 사용된다.</li>
<li><strong>POST</strong>: 새로운 자원을 생성 예를 들어, 새 사용자 추가에 사용된다.</li>
<li><strong>PUT</strong>: 자원을 업데이트. 자원의 전체를 교체하는 데 사용된다.</li>
<li><strong>DELETE</strong>: 자원을 삭제한다.</li>
</ul>
<h3 id="4-상태-코드의-적절한-사용">4) 상태 코드의 적절한 사용</h3>
<h4 id="-200번대-상태-코드">** 200번대 상태 코드**</h4>
<pre><code>- **의미**: 클라이언트의 요청이 성공적으로 수행되었음을 나타낸다.
- **예시**:
    - **200 OK**: 요청이 성공적으로 처리됨. GET, PUT 요청에 대한 표준 응답
    - **201 Created**: 요청이 성공적으로 수행되어 새로운 리소스가 생성됨.
    - **204 No Content**: 요청이 성공적이지만, 클라이언트에 보낼 콘텐츠가 없음.</code></pre><h4 id="-300번대-상태-코드-리다이렉션-메시지">** 300번대 상태 코드 (리다이렉션 메시지)**</h4>
<pre><code>- **의미**: 요청한 리소스가 다른 URI로 이동되었음을 나타낸다.
- **예시**:
    - **301 Moved Permanently**: 요청한 리소스가 영구적으로 새 위치로 이동됨.
    - **302 Found**: 요청한 리소스가 일시적으로 다른 위치로 이동됨.
    - **304 Not Modified**: 리소스가 변경되지 않았으므로 클라이언트의 캐시된 버전을 사용할 수 있음.</code></pre><h4 id="400번대-상태-코드-클라이언트-오류"><strong>400번대 상태 코드 (클라이언트 오류)</strong></h4>
<pre><code>- **의미**: 클라이언트의 잘못된 요청으로 인해 서버가 요청을 처리할 수 없음을 나타낸다.
- **예시**:
    - **400 Bad Request**: 서버가 요청을 이해할 수 없음.
    - **401 Unauthorized**: 인증이 필요한 요청임.
    - **403 Forbidden**: 서버가 요청을 거부함.
    - **404 Not Found**: 요청한 리소스를 찾을 수 없음.</code></pre><h4 id="500번대-상태-코드-서버-오류">500번대 상태 코드 (서버 오류)</h4>
<pre><code>- **의미**: 서버 측 문제로 인해 요청을 처리할 수 없음을 나타낸다.
- **예시**:
    - **500 Internal Server Error**: 서버 내부 오류로 요청을 처리할 수 없음.
    - **501 Not Implemented**: 서버가 요청 메서드를 지원하지 않음.
    - **503 Service Unavailable**: 서버가 일시적으로 요청을 처리할 수 없음 (예: 과부하 또는 유지보수로 인해).</code></pre><br>

<h3 id="5-데이터-포맷">5) 데이터 포맷</h3>
<p>JSON(JavaScript Object Notation) 형식을 통해 데이터를 교환하는 것이 트렌드
 XML보다 가볍고, 인간이 읽기 쉽다.</p>
<h3 id="6-pathvariablerequestparam-modelattribute-requestbody">6) @PathVariable,@RequestParam, @ModelAttribute, @RequestBody</h3>
<p><strong>@PathVariable</strong></p>
<ul>
<li><strong>개념</strong>: <code>@PathVariable</code>은 URL 경로에 포함된 변수를 컨트롤러 메서드의 매개변수로 바인딩하는 데 사용된다.</li>
<li><strong>특징</strong>:<ul>
<li>URL 경로의 일부를 변수로 사용하여, 동적으로 변하는 URL 경로를 처리할 수 있다.</li>
<li>간결하고 직관적인 API 경로를 설계할 수 있다.</li>
</ul>
</li>
</ul>
<p><strong>@RequestParam</strong></p>
<ul>
<li><strong>개념</strong>: <code>@RequestParam</code>은 클라이언트가 전송하는 HTTP 요청 파라미터를 컨트롤러 메서드의 매개변수로 바인딩하는 데 사용된다.</li>
<li><strong>특징</strong>:<ul>
<li><ul>
<li>URL에서 지정된 이름의 파라미터를 메서드 매개변수로 전달</li>
</ul>
</li>
<li>필수 여부, 기본값 설정 등의 추가적인 설정이 가능하다.</li>
</ul>
</li>
</ul>
<p><strong>@ModelAttribute</strong></p>
<ul>
<li><strong>개념</strong>: <code>@ModelAttribute</code>는 요청 파라미터를 객체로 매핑하여, 복잡한 데이터 구조를 쉽게 다루게 해준다.</li>
<li><strong>특징</strong>:<ul>
<li>폼 데이터의 각 필드가 객체의 필드와 자동으로 매핑</li>
<li>복잡한 객체 구조의 데이터를 간편하게 처리할 수 있다.</li>
</ul>
</li>
</ul>
<p><strong>@RequestBody</strong></p>
<ul>
<li><strong>개념</strong>: <code>@RequestBody</code>는 클라이언트가 전송하는 HTTP 요청의 본문(body)을 Java 객체로 변환하여 받는다.</li>
<li><strong>특징</strong>:<ul>
<li>HTTP 요청 본문의 내용을 자바 객체로 역직렬화한다.</li>
<li>주로 JSON 또는 XML 형식의 데이터 처리에 사용된다.</li>
</ul>
</li>
</ul>
<h2 id="마치며">마치며..</h2>
<p>오늘은 Spring basic 반 수업을 들으면서 스프링에 대해 다시 한 번 복습할 수 있었고, Todo List에 테스트 코드를 작성하는 개인과제를 마치고 팀 과제를 시작하였다. S.A를 다 같이 작성하는 데 시간이 꽤나 소요가 됐다. 열정적인 팀원분들이 많은 거 같아 좋은 거 같다!!! 나는 게시판 CRUD를 맡았다. TodoList에서 물론 구현은 해봤지만 아직 모르는 게 많은 거 같아서 복습하고자 게시판 CRUD를 선택했다. 맡은 기능 무사히 다 구현해서 팀원분들에게 피해가 안가게끔 노력할 것이다! 이상.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/12/01 Spring 단위테스트 자세히]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231201-Spring-Test</link>
            <guid>https://velog.io/@yeong_do/TIL-20231201-Spring-Test</guid>
            <pubDate>Fri, 01 Dec 2023 12:09:44 GMT</pubDate>
            <description><![CDATA[<h2 id="테스트-코드-작성-공통-준수-사항">테스트 코드 작성 공통 준수 사항</h2>
<ul>
<li>보통 테스트를 위한 라이브러리로 JUnit과 AssertJ 조합을 사용하여 테스트를 한다.</li>
<li>Given/When/Then 패턴<ul>
<li>Given : 어떠한 데이터가 주어질 때.</li>
<li>When : 어떠한 기능을 실행하면.</li>
<li>Then : 어떠한 결과를 기대한다.</li>
</ul>
</li>
</ul>
<h2 id="mockito를-사용한-단위-테스트">Mockito를 사용한 단위 테스트</h2>
<ul>
<li>모키토는, 개발자가 동작을 직접적으로 제어할 수 있는 가짜 객체를 지원하는 테스트 프레임웍이다.</li>
<li>Spring 어플리케이션은 여러 객체들 간의 의존성이 생기는데 이러한 의존성을 모키토를 이용하여 단절 시킴으로 단위 테스트를 쉽게 작성하는 것을 도와준다.</li>
<li>앞으로 작성할 예제 테스트 에서는 모키토와 JUnit5의 조합으로 테스트 코드를 작성할 것 이다.</li>
</ul>
<h2 id="컨트롤러-계층-단위-테스트">컨트롤러 계층 단위 테스트</h2>
<ul>
<li>컨트롤러의 단위 테스트를 하기 위해서 Mockito를 이용하여 다른 계층과 의존관계를 단절 시켜 주어야한다.</li>
<li>컨트롤러가 의존하고 있는 객체는 MemberService와 MemberMapper 객체다.</li>
<li>컨트롤러를 테스트 하기 위해서는 HTTP 호출이 필요하다. 스프링 부트는 컨트롤러 테스트를 위한 @WebMvcTest 어노테이션을 제공한다.</li>
<li>이를 이용하면 MockMvc 객체가 자동으로 생성될 뿐만 아니라 테스트에 필요한 요소들을 빈으로 등록해 스프링 컨텍스트 환경을 구성 해 준다.</li>
</ul>
<h2 id="서비스-계층-단위-테스트">서비스 계층 단위 테스트</h2>
<ul>
<li>예제 서비스 코드에서 MemberService는 MemberRepository에 의존 하고 있다.</li>
<li>서비스 계층은 HTTP 호출과 상관 없으며 단순한 로직 검증만 하면 된다.</li>
<li>Repository에 저장하는 로직은 단순하기 때문에 입력된 회원 정보가 중복 될시 예외가 발생하는 경우를 테스트 하였다.</li>
</ul>
<h2 id="레포지토리-계층-단위-테스트">레포지토리 계층 단위 테스트</h2>
<ul>
<li>@DataJpaTest 어노테이션은, 스프링 부트에서 JPA 레포지토리를 쉽게 테스트 할수 있게 지원 한다.</li>
<li>해당 프로젝트에서는 H2 인메모리 DB를 구축해 놓았고, @DataJpaTest 어노테이션은 기본적으로 H2를 기반으로 테스트하며, 테스트가 끝나면 트랜잭션 롤백을 한다.</li>
<li>실제 DB와 통신을 하지 않으면 테스트의 의미가 없으므로, 목킹은 하지 않는다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/11/30 Spring 단위 테스트 & 통합 테스트]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231130-Spring-Test</link>
            <guid>https://velog.io/@yeong_do/TIL-20231130-Spring-Test</guid>
            <pubDate>Thu, 30 Nov 2023 11:44:55 GMT</pubDate>
            <description><![CDATA[<h2 id="단위-테스트와-통합-테스트">단위 테스트와 통합 테스트</h2>
<h3 id="단위-테스트">단위 테스트</h3>
<ul>
<li>단위테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트다.</li>
<li>하나의 모듈이란 각 계층에서의 하나의 기능 또는 메소드로 이해할 수 있다.</li>
<li>하나의 기능이 올바르게 동작하는지를 독립적으로 테스트하는 것이다.</li>
</ul>
<h3 id="단위-테스트의-필요성">단위 테스트의 필요성</h3>
<ul>
<li>일반적으로 테스트 코드를 작성한다고 하면 거의 단위 테스트를 의미한다.</li>
<li>통합 테스트는 실제 여러 컴포넌트들 간의 상호작용을 테스트 하기 때문에 모든 컴포넌트들이 구동된 상태에서 테스트를 하게 되므로, 캐시나 데이터베이스 등 다른 컴포넌트들과 실제 연결을 해야하고 어플리케이션을 구성하는 컴포넌트들이 많아 질수록 테스트를 위한 시간이 커진다.</li>
<li>하지만, 단위 테스트는 테스트하고자 하는 부분만 독립적으로 테스트를 하기 때문에 해당 단위를 유지 보수 또는 리팩토링 하더라도 빠르게 문제 여부를 확인 할 수 있다.</li>
</ul>
<h3 id="단위-테스트의-한계">단위 테스트의 한계</h3>
<ul>
<li>일반적으로 어플리케이션은 하나의 기능을 처리하기 위해 다른 객체들과 데이터를 주고 받는 복잡한 통신이 일어난다.</li>
<li>단위 테스트는 해당 기능에 대한 독립적인 테스트기 때문에 다른 객체와 데이터를 주고 받는 경우에 문제가 발생한다.</li>
<li>그래서, 이 문제를 해결하기 위해 테스트하고자 하는 기능과 연관된 모듈에서 가짜 데이터, 정해진 반환값이 필요하다.</li>
<li>즉 단위 테스트에서는, 테스트 하고자 하는 기능과 연관된 다른 모듈은 연결이 단절 되어야 비로소 독립적인 단위 테스트가 가능해 진다.</li>
</ul>
<h3 id="단위-테스트의-특징">단위 테스트의 특징</h3>
<ul>
<li>좋은 테스트 코드란, 계속해서 변하는 요구사항에 맞춰 변경된 코드는 버그의 가능성을 항상 내포하고 있으며, 이를 테스트 코드로 검증함으로써 해결할 수 있어야 한다.</li>
<li>실제 코드가 변경되면 테스트 코드도 변경이 필요할 수 있으며, 테스트 코드 역시 가독성 있게 작성하여 일관된 규칙과 일관된 목적으로 테스트 코드를 작성 해야한다.</li>
<li>FIRST 규칙<ul>
<li>Fast : 테스트는 빠르게 동작하고 자주 가동 해야한다.</li>
<li>Independent : 각각의 테스트는 독립적어이야 하며, 서로에 대한 의존성은 없어야 한다.</li>
<li>Repeatable : 어느 환경에서도 반복이 가능해야 한다.</li>
<li>Self-Validating : 테스트는 성공 또는 실패 값으로 결과를 내어 자체적으로 검증 되어야 한다.</li>
<li>Timely : 테스트는 테스트 하려는 실제 코드를 구현하기 직전에 구현 해야한다.</li>
</ul>
</li>
</ul>
<h2 id="통합-테스트">통합 테스트</h2>
<ul>
<li>모듈을 통합하는 과정에서 모듈 간 호환성을 확인하기 위한 테스트다.</li>
<li>다른 객체들과 데이터를 주고받으며 복잡한 기능이 수행 될때, 연관된 객체들과 올바르게 동작하는지 검증하고자 하는 테스트다.</li>
<li>독립적인 기능보다 전체적인 연관 기능과 웹 페이지로 부터 API를 호출하여 올바르게 동작하는지 확인한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/11/27 뉴스피드 회고]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231127-%EB%89%B4%EC%8A%A4%ED%94%BC%EB%93%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@yeong_do/TIL-20231127-%EB%89%B4%EC%8A%A4%ED%94%BC%EB%93%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 27 Nov 2023 11:58:27 GMT</pubDate>
            <description><![CDATA[<h2 id="📇-개요">📇 개요</h2>
<p>뉴스피드(New Speed 아님!) 팀 프로젝트
코딩하면서 어떤 노래를 들으시나요? 한눈에 볼 수 있는 개발자들의 플레이리스트
서로의 플레이리스트를 공유하며 다양한 음악을 접해보자!</p>
<h2 id="💾-디렉토리-구조">💾 디렉토리 구조</h2>
<p><strong>계층별로 나누지 않고 도메인으로 나누어 관리하는 전략을 선택했다.</strong></p>
<pre><code>
── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── sparta/
│   │   │           └── newsfeed/
│   │   │               ├── NewsFeedApplication.java
│   │   │               ├── advice/
│   │   │               │   └── CustomRestAdvice.java
│   │   │               ├── auth/
│   │   │               │   └── MainController.java
│   │   │               ├── board/
│   │   │               │   ├── controller/
│   │   │               │   ├── domain/
│   │   │               │   ├── dto/
│   │   │               │   ├── repository/
│   │   │               │   └── service/
│   │   │               ├── comment/
│   │   │               │   ├── controller/
│   │   │               │   ├── domain/
│   │   │               │   ├── dto/
│   │   │               │   ├── repository/
│   │   │               │   └── service/
│   │   │               ├── config/
│   │   │               │   ├── CustomServletConfig.java
│   │   │               │   └── RootConfig.java
│   │   │               ├── domain/
│   │   │               │   └── BaseEntity.java
│   │   │               ├── file/
│   │   │               │   ├── controller/
│   │   │               │   ├── domain/
│   │   │               │   └── dto/
│   │   │               ├── member/
│   │   │               │   ├── controller/
│   │   │               │   ├── domain/
│   │   │               │   ├── dto/
│   │   │               │   ├── repository/
│   │   │               │   └── service/
│   │   │               └── security/
│   │   │                   ├── config/
│   │   │                   ├── jwt/
│   │   │                   └── service/
│   │   └── resources/
│   │       ├── application.properties
│   │       ├── static/
│   │       └── templates/
│   │           ├── login.html
│   │           ├── mainpage.html
│   │           └── signup.html
│   └── test/
│       └── java/
│           └── com/
│               └── sparta/
│                   └── newsfeed/
│                       ├── DataSourceTests.java
│                       ├── NewsFeedApplicationTests.java
│                       ├── board/
│                       │   ├── domain/
│                       │   ├── repository/
│                       │   └── service/
│                       ├── comment/
│                       │   ├── repository/
│                       │   └── service/
│                       ├── member/
│                       │   └── repository/
│                       └── security/
│                           └── jwt/
└── upload/
    ├── b7dea34d-3338-47e9-bfa9-91204954fe6a_2620.jpg
    ├── s_9fae02c9-f0f5-4d97-a97e-3ff64f5b8585_2620.jpg
    └── s_b7dea34d-3338-47e9-bfa9-91204954fe6a_2620.jpg
</code></pre><h2 id="⚙-기능">⚙ 기능</h2>
<p><strong>회원가입</strong></p>
<ul>
<li>사용자는 해당 서비스에 아이디와 패스워드로 가입할 수 있다.</li>
<li><em>로그인/로그아웃*</em></li>
<li>사용자는 회원가입한 아이디로 로그인하여 해당 서비스를 이용할 수 있다.
토큰 기반 인증</li>
<li>로그인에 성공하면 서버에서 JWT토큰을 발급해주고 토큰이 필요한 경로에 요청을 보내 서비스를 이용할 수 있다.</li>
<li><em>뉴스피드 작성하기*</em></li>
<li>사용자는 로그인한 뒤 이미지등의 파일과 함께 자신의 플레이리스트를 동료 개발자들과 공유할 수 있다.</li>
<li><em>뉴스피드 수정하기*</em></li>
<li>사용자는 자신이 작성한 뉴스피드를 수정할 수 있다.</li>
<li><em>뉴스피드 삭제하기*</em></li>
<li>사용자는 자신이 작성한 뉴스피드를 삭제할 수 있다.</li>
<li><em>뉴스피드에 댓글 남기기*</em></li>
<li>댓글을 통해 자신의 의견을 나타낼 수 있다.</li>
<li><em>댓글 수정/삭제*</em></li>
<li>자신이 남긴 댓글을 수정 또는 삭제할 수 있다.</li>
</ul>
<h2 id="📚-api-명세서">📚 API 명세서</h2>
<p><strong>게시글</strong></p>
<pre><code>POST /api/boards 게시글 작성
PUT /api/boards/{boardId} 게시글 수정
GET /api/boards/{boardId} 게시글 조회
DELETE /api/boards/{boardId} 게시글 삭제</code></pre><p><strong>댓글</strong></p>
<pre><code>POST /api/boards/{boardId}/comments 댓글 작성
PUT /api/boards/{boardId}/comments/{commentId} 댓글 수정
DELETE /api/boards/{boardId}/comments/{commentId} 댓글 삭제</code></pre><p><strong>회원</strong></p>
<pre><code>GET /api/user/{username} 유저 정보 조회
PUT /api/user/{username} 유저 정보 수정</code></pre><p><strong>파일 업로드</strong></p>
<pre><code>POST /api/files/ 이미지 파일 업로드
GET /api/files/{fileName} 이미지 파일 조회
DELETE /api/files/{fileName} 이미지 파일 삭제</code></pre><h2 id="📔-documents">📔 Documents</h2>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/aaa8c3b6-b0fd-40da-a846-945ba5dce4d8/image.png" alt="">
<img src="https://velog.velcdn.com/images/yeong_do/post/118a145f-418c-42a5-a919-a9210a5bed59/image.png" alt="">
<img src="https://velog.velcdn.com/images/yeong_do/post/3a690aac-1a11-48e7-9322-477856db114a/image.png" alt=""></p>
<h2 id="📸-스크린샷">📸 스크린샷</h2>
<p><img src="https://velog.velcdn.com/images/yeong_do/post/e503aac9-a2e6-4b11-8fc7-92ff7b67f87d/image.png" alt="">
<img src="https://velog.velcdn.com/images/yeong_do/post/d4fb0253-d07d-4ad4-9915-f6a7f4717aa0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/11/24 SQL 조건에 부합하는 중고거래 상태 조회하기]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231124-Spring</link>
            <guid>https://velog.io/@yeong_do/TIL-20231124-Spring</guid>
            <pubDate>Fri, 24 Nov 2023 10:58:32 GMT</pubDate>
            <description><![CDATA[<h2 id="조건에-부합하는-중고거래-상태-조회하기">조건에 부합하는 중고거래 상태 조회하기</h2>
<p>다음은 중고거래 게시판 정보를 담은 USED_GOODS_BOARD 테이블입니다. USED_GOODS_BOARD 테이블은 다음과 같으며 BOARD_ID, WRITER_ID, TITLE, CONTENTS, PRICE, CREATED_DATE, STATUS, VIEWS은 게시글 ID, 작성자 ID, 게시글 제목, 게시글 내용, 가격, 작성일, 거래상태, 조회수를 의미합니다.</p>
<pre><code>Column name    Type    Nullable
BOARD_ID    VARCHAR(5)    FALSE
WRITER_ID    VARCHAR(50)    FALSE
TITLE    VARCHAR(100)    FALSE
CONTENTS    VARCHAR(1000)    FALSE
PRICE    NUMBER    FALSE
CREATED_DATE    DATE    FALSE
STATUS    VARCHAR(10)    FALSE
VIEWS    NUMBER</code></pre><h4 id="문제">문제</h4>
<p>USED_GOODS_BOARD 테이블에서 2022년 10월 5일에 등록된 중고거래 게시물의 게시글 ID, 작성자 ID, 게시글 제목, 가격, 거래상태를 조회하는 SQL문을 작성해주세요. 거래상태가 SALE 이면 판매중, RESERVED이면 예약중, DONE이면 거래완료 분류하여 출력해주시고, 결과는 게시글 ID를 기준으로 내림차순 정렬해주세요.</p>
<h4 id="정답">정답</h4>
<pre><code>SELECT BOARD_ID, WRITER_ID, TITLE, PRICE, CASE WHEN STATUS = &#39;SALE&#39; THEN &#39;판매중&#39;
WHEN STATUS = &#39;RESERVED&#39; THEN &#39;예약중&#39;
WHEN STATUS = &#39;DONE&#39; THEN &#39;거래완료&#39; END AS STATUS
FROM USED_GOODS_BOARD
WHERE TO_CHAR(CREATED_DATE, &#39;YYYYMMDD&#39;) = &#39;20221005&#39;
ORDER BY BOARD_ID DESC;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/11/23 알고리즘 이진 변환 반복하기]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231123-TestCode</link>
            <guid>https://velog.io/@yeong_do/TIL-20231123-TestCode</guid>
            <pubDate>Thu, 23 Nov 2023 10:00:59 GMT</pubDate>
            <description><![CDATA[<h2 id="이진-변환-반복하기">이진 변환 반복하기</h2>
<p><strong>문제 설명</strong>
0과 1로 이루어진 어떤 문자열 x에 대한 이진 변환을 다음과 같이 정의합니다.</p>
<p>x의 모든 0을 제거합니다.
x의 길이를 c라고 하면, x를 &quot;c를 2진법으로 표현한 문자열&quot;로 바꿉니다.
예를 들어, x = &quot;0111010&quot;이라면, x에 이진 변환을 가하면 x = &quot;0111010&quot; -&gt; &quot;1111&quot; -&gt; &quot;100&quot; 이 됩니다.</p>
<p>0과 1로 이루어진 문자열 s가 매개변수로 주어집니다. s가 &quot;1&quot;이 될 때까지 계속해서 s에 이진 변환을 가했을 때, 이진 변환의 횟수와 변환 과정에서 제거된 모든 0의 개수를 각각 배열에 담아 return 하도록 solution 함수를 완성해주세요.</p>
<p><strong>제한사항</strong></p>
<ul>
<li>s의 길이는 1 이상 150,000 이하입니다.</li>
<li>s에는 &#39;1&#39;이 최소 하나 이상 포함되어 있습니다.</li>
</ul>
<p><strong>정답</strong></p>
<pre><code>import java.util.*;
class Solution {
    public int[] solution(String s) {
        int[] answer = new int[2];

        int cnt = 0; // 변환 횟수
        int zero = 0; // 제거된 0의 개수

        while(!s.equals(&quot;1&quot;)) {
            int length = s.length(); // 원래 문자열의 길이
            s = s.replace(&quot;0&quot;, &quot;&quot;); // 0 제거
            zero += length - s.length();
            length = s.length();

            // 이진수 변환
            List&lt;Integer&gt; list = new ArrayList&lt;&gt;();
            while (length &gt; 1) {
                list.add(length % 2);
                    length /= 2;
                }

                StringBuilder sb = new StringBuilder();
                sb.append(&quot;1&quot;);
                for (int i = list.size() - 1; i &gt;= 0; i--) {
                    sb.append(list.get(i));
                }

                s = sb.toString();
                cnt++;
            }

            answer[0] = cnt;
            answer[1] = zero;
        return answer;
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 2023/11/22 알고리즘 구명보트]]></title>
            <link>https://velog.io/@yeong_do/TIL-20231122-Spring</link>
            <guid>https://velog.io/@yeong_do/TIL-20231122-Spring</guid>
            <pubDate>Wed, 22 Nov 2023 11:32:11 GMT</pubDate>
            <description><![CDATA[<h2 id="구명보트">구명보트</h2>
<p><strong>문제 설명</strong>
무인도에 갇힌 사람들을 구명보트를 이용하여 구출하려고 합니다. 구명보트는 작아서 한 번에 최대 2명씩 밖에 탈 수 없고, 무게 제한도 있습니다.</p>
<p>예를 들어, 사람들의 몸무게가 [70kg, 50kg, 80kg, 50kg]이고 구명보트의 무게 제한이 100kg이라면 2번째 사람과 4번째 사람은 같이 탈 수 있지만 1번째 사람과 3번째 사람의 무게의 합은 150kg이므로 구명보트의 무게 제한을 초과하여 같이 탈 수 없습니다.</p>
<p>구명보트를 최대한 적게 사용하여 모든 사람을 구출하려고 합니다.</p>
<p>사람들의 몸무게를 담은 배열 people과 구명보트의 무게 제한 limit가 매개변수로 주어질 때, 모든 사람을 구출하기 위해 필요한 구명보트 개수의 최솟값을 return 하도록 solution 함수를 작성해주세요.</p>
<p><strong>제한사항</strong></p>
<ul>
<li>무인도에 갇힌 사람은 1명 이상 50,000명 이하입니다.</li>
<li>각 사람의 몸무게는 40kg 이상 240kg 이하입니다.</li>
<li>구명보트의 무게 제한은 40kg 이상 240kg 이하입니다.</li>
<li>구명보트의 무게 제한은 항상 사람들의 몸무게 중 최댓값보다 크게 주어지므로 사람들을 구출할 수 없는 경우는 없습니다.</li>
</ul>
<p><strong>정답</strong></p>
<pre><code>import java.util.*;
class Solution {
    public int solution(int[] people, int limit) {
        int answer = 0;

        Arrays.sort(people);

        int min = 0;
        for (int max=people.length -1; min &lt;= max; max--) {
            if (people[min] + people[max] &lt;= limit) min++;

            answer++;
        }
        return answer;
    }
}</code></pre>]]></description>
        </item>
    </channel>
</rss>