<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jsang_log.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 09 Mar 2023 21:47:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jsang_log.log</title>
            <url>https://velog.velcdn.com/images/jsang_log/profile/1bc8da37-a007-4fd3-8f69-42e6421602cd/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jsang_log.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jsang_log" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[팀프로젝트] Thymeleaf를 사용한 onclick JavaScript 함수 호출 
(여러 개의 파라미터 넘겨주기)]]></title>
            <link>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B0%B8%EC%97%AC%EC%9E%90-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B0%95%ED%87%B4%EA%B8%B0%EB%8A%A5-ui</link>
            <guid>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%B0%B8%EC%97%AC%EC%9E%90-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%EA%B0%95%ED%87%B4%EA%B8%B0%EB%8A%A5-ui</guid>
            <pubDate>Thu, 09 Mar 2023 21:47:53 GMT</pubDate>
            <description><![CDATA[<h2 id="✔-view">✔ view</h2>
<h3 id="❗-방-생성자를-제외한-참여자-강퇴하기-버튼-출력">❗ 방 생성자를 제외한 참여자 강퇴하기 버튼 출력</h3>
<ul>
<li>방장의 화면
<img src="https://velog.velcdn.com/images/jsang_log/post/3f8e169e-9c7b-45ed-8c83-2bcb9df335c8/image.png" alt=""></li>
<li>view<pre><code class="language-html">&lt;!-- 생성자인 경우 다른 참여자 강퇴 기능 --&gt;
&lt;div th:if=&quot;${details.getUserName == #authentication.getName()}&quot;&gt;
&lt;button th:if=&quot;${member.getUserName != details.getUserName}&quot; type=&quot;button&quot; class=&quot;btn btn-outline-secondary btn-sm&quot; th:crewId=&quot;${crewId}&quot; th:userId=&quot;${member.joinUserId}&quot;
        th:onclick=&quot;deleteUserFromCrew(this.getAttribute(&#39;crewId&#39;),this.getAttribute(&#39;userId&#39;))&quot;&gt;
  강퇴하기
&lt;/button&gt;
&lt;/div&gt;</code></pre>
</li>
<li>흐름도
<img src="https://velog.velcdn.com/images/jsang_log/post/d061a01f-ce2b-4825-afa3-0066bee3b858/image.png" alt=""><br>


</li>
</ul>
<h3 id="❗-현재-로그인-한-참여자에게만-나가기-버튼-출력">❗ 현재 로그인 한 참여자에게만 나가기 버튼 출력</h3>
<ul>
<li>참가자 화면
<img src="https://velog.velcdn.com/images/jsang_log/post/e1c345de-f27b-45f4-b6ce-b55608c1769c/image.png" alt=""><ul>
<li>view<pre><code class="language-html">&lt;!-- 일반 참여자인 경우 자기 자신만 나가기 기능--&gt;
&lt;div th:if=&quot;${member.getUserName() == #authentication.getName() and details.getUserName != #authentication.getName()}&quot;&gt;
&lt;button type=&quot;button&quot; class=&quot;btn btn-outline-danger btn-sm&quot; th:crewId=&quot;${crewId}&quot; th:userId=&quot;${member.joinUserId}&quot;
     th:onclick=&quot;deleteUserFromCrew(this.getAttribute(&#39;crewId&#39;),this.getAttribute(&#39;userId&#39;))&quot;&gt;
나가기
&lt;/button&gt;
&lt;/div&gt;</code></pre>
</li>
<li>흐름도
<img src="https://velog.velcdn.com/images/jsang_log/post/578266b6-a5ac-4557-901e-0649edd22b4f/image.png" alt=""></li>
</ul>
</li>
</ul>
<br>

<h3 id="javascript">javascript</h3>
<pre><code class="language-javascript">async function deleteUserFromCrew(crewId, userId){
    console.log(crewId);
    console.log(userId);

    if(confirm(&quot;삭제하시겠습니까?&quot;)){
        location.href = &quot;/view/v1/manage/crews/&quot; + crewId + &quot;/&quot; + userId+&quot;/delete&quot;;

        return true;
    } else {
        return false;
    }

}</code></pre>
<br>
<br>

<h2 id="✔-여러-개의-파라미터를-가진-javascript-함수-호출">✔ 여러 개의 파라미터를 가진 JavaScript 함수 호출</h2>
<p><code>&lt;button&gt;</code>안에서 <strong>th:onclick</strong>으로 javascript함수인 deleteUserFromCrew()가 호출된다.</p>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/8c2ca94c-e066-4999-8b2b-2853fd081f5b/image.png" alt=""></p>
<ol>
<li>받아야 할 파라매터 값을 태그 안에서 response로부터 가져오고, 태그 안에서 변수에 저장한 다음  <code>ex) th:crewId=&quot;${crewId}&quot;</code></li>
<li>함수 안에서 this.getAttribute(&#39;<code>파라미터 변수</code>&#39;)를 통해 값을 넘겨준다. 
th:onclick=&quot;<strong>method(</strong><code>this.getAttribute(&#39;parameter1&#39;)</code>, <code>this.getAttribute(&#39;parameter2&#39;)</code><strong>)</strong>&quot;</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/db7e0384-ab1c-47a3-8ff1-2ed25824f75a/image.png" alt=""></p>
<p>*타임리프에서 인식을 못하는 에러가 존재하는데, 그냥 인텔리제이에서 인식을 못하는 거라서 실행에는 문제가 없다.
<br></p>
<h3 id="마무리">마무리</h3>
<p>타임리프의 버전에 따라 호출 방법이 조금씩 다르고, 
한 개 이상 파라미터를 대입하는 방법을 알려준 곳이 잘 보이지 않았기 때문에 찾는데 시간이 오래걸렸던 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[팀프로젝트] 비동기 댓글 CRUD💬, 대댓글 관계 매핑 - 1]]></title>
            <link>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8C%93%EA%B8%80-%EB%8C%80%EB%8C%93%EA%B8%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8C%93%EA%B8%80-%EB%8C%80%EB%8C%93%EA%B8%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 04 Mar 2023 15:09:43 GMT</pubDate>
            <description><![CDATA[<p>📍 이번 포스트에서는 비동기 방식으로 댓글 crud 기능을 구현하고, 
댓글 엔터티 self join으로 대댓글을 구현하기 위해 관계를 매핑하는 과정을 담았다.</p>
<p>개발환경
개발 툴 : SpringBoot 2.7.7
자바 : JAVA 11
빌드 : Gradle
템플릿 엔진 : Thymeleaf
spring MVC 구조
<br></p>
<h2 id="✔-self-join이란">✔ self join이란?</h2>
<p>이름그대로 자기자신 테이블과 조인을 하는 것을 말한다. 원래 조인이 두개의 테이블에 대해 연관된 행들을 조인 칼럼을 기준으로 비교하여 새로운 행 집합을 만드는 것인데 두개의 테이블이 같은 테이블인 경우를 Self Join 이라고 한다.
<img src="https://velog.velcdn.com/images/jsang_log/post/a916c841-7656-4928-a456-3e68e70a96b6/image.png" alt=""></p>
<p>즉, 한 개의 테이블을 두 개의 별도의 테이블처럼 이용하여 서로 조인 하는 형태로, 대댓글을 위한 테이블을 만들지는 않고 댓글 테이블을 참조한다.</p>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/3c024ddb-8945-449d-af5a-26a8ee47b98d/image.png" alt="">
<br></p>
<h2 id="✔-순환-참조-관계-맺기">✔ 순환 참조 관계 맺기</h2>
<p> 한 개의 부모 댓글에 여러 답글이 달릴 수 있으므로 1:N 관계로 매핑해준다.
-&gt; 하나의 <code>Comment</code>를 부모로 가지고 <code>List&lt;Comment&gt;</code>를 자식으로 가져야한다.
-&gt; Comment 엔티티 안에서 <code>@ManyToOne</code>과 <code>@OneToMany</code> 관계를 정의해야한다.</p>
<p>parentId 칼럼을 추가하고 자식댓글인 경우 부모댓글의 아이디를 저장한다.</p>
<h3 id="comment-entity">Comment Entity</h3>
<pre><code class="language-java">@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@Where(clause = &quot;deleted_at is null&quot;)
public class Comment extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String comment;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_id&quot;)
    User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;crew_id&quot;)
    Crew crew;

    // 부모 정의
    @Setter
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;parent_id&quot;) // parent_id 이름으로 칼럼 추가
    private Comment parent;

    // 자식 정의
    @Setter
    @OneToMany(mappedBy = &quot;parent&quot;, orphanRemoval = true)
    private List&lt;Comment&gt; children = new ArrayList&lt;&gt;();


    public void setComment(String comment) {
        this.comment = comment;
    }
    public static List&lt;CommentViewResponse&gt; from(List&lt;Comment&gt; comments) {
        return comments.stream()
                .map(CommentViewResponse::of)
                .collect(Collectors.toList());
    }
}
</code></pre>
<br>

<h2 id="✔-부모-댓글-crud">✔ 부모 댓글 CRUD</h2>
<p>가장 먼저 상세 페이지에 들어가면
👆 이전에 작성된 댓글리스트가 바로 출력되고
✌ 댓글 입력창에서 추가할 수 있도록 구성하고자 했다.
<img src="https://velog.velcdn.com/images/jsang_log/post/3f2455ff-bfb0-4a60-9e13-2483fe4784ee/image.png" alt="">
<em><strong>read-crew.html</strong></em></p>
<pre><code class="language-java">&lt;p class=&quot;comment&quot;&gt;댓글&lt;/p&gt;
    &lt;!--댓글 작성 부분--&gt;
&lt;form method=&quot;post&quot;&gt;
   &lt;div class=&quot;input-group&quot; style=&quot;width:auto&quot;&gt;
      &lt;label class=&quot;form-label mt-4&quot; hidden&gt;댓글 작성&lt;/label&gt;
      &lt;input type=&quot;text&quot;  class=&quot;form-control&quot; id=&quot;commentContent&quot; name=&quot;commentContent&quot; required minlength=&quot;2&quot; maxlength=&quot;100&quot;placeholder=&quot;댓글을 입력해주세요&quot;&gt;
      &lt;input type=&quot;text&quot; hidden th:value=&quot;${crewId}&quot; id=&quot;crewIdComment&quot;&gt;
      &lt;input type=&quot;text&quot; hidden th:value=&quot;${#authentication.getName()}&quot; id=&quot;principal&quot;&gt;
      &lt;button type=&quot;button&quot; id=&quot;commentBtn&quot;  onclick=&quot;commentCheck()&quot; class=&quot;btn btn-light&quot;&gt;작성&lt;/button&gt;
      &lt;br&gt;
   &lt;/div&gt;
&lt;/form&gt;
&lt;p class=&quot;field-error commentContentCheck&quot;&gt;&lt;/p&gt;
&lt;br&gt;
    &lt;!--댓글 출력 부분--&gt;
&lt;div id=&quot;commentList&quot;&gt; 댓글&lt;/div&gt;
</code></pre>
<pre><code class="language-javascript">const crewId = [[${crewId}]];
    const username = [[${#authentication.name}]];

    $(function() {
        getComment();
    });</code></pre>
<p>read-crew의 javascript 부분으로,
위의 코드에 의해 상세 페이지에 들어가자마자 댓글 출력 함수가 실행된다.
<br></p>
<h3 id="✔️-부모-댓글-입력-로직">✔️ 부모 댓글 입력 로직</h3>
<p>가장 먼저 댓글 입력을 했을 때 데이터를 전송하면 <code>/view/v1/crews/{crewID}/comments</code> POST가 서버로 요청된다.
<em><strong>CommentViewController</strong></em></p>
<pre><code class="language-java">    // 댓글 작성
    @PostMapping(&quot;/view/v1/crews/{crewId}/comments&quot;)
    public ResponseEntity addComment(@RequestBody CommentRequest commentRequest,@PathVariable Long crewId, Authentication authentication) {
        CommentResponse commentResponse = commentService.addComment(commentRequest, crewId, authentication.getName());
        return new ResponseEntity&lt;&gt;(commentResponse, HttpStatus.OK);
    }</code></pre>
<p><em><strong>CommentService</strong></em></p>
<pre><code class="language-java">public CommentResponse addComment(CommentRequest commentRequest, Long crewId, String userName) {
        User user = getUser(userName);
        Crew crew = getCrew(crewId);

        Comment comment = commentRepository.save(commentRequest.toEntity(user, crew));
        alarmRepository.save(Alarm.toEntity(user, crew, AlarmType.ADD_COMMENT, comment.getComment()));

        ...
        }

        return CommentResponse.of(comment);
    }</code></pre>
<p><em><strong>CommentResponse</strong></em></p>
<pre><code class="language-java">@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class CommentResponse {
    private Long id;
    private String comment;
    private String userName;
    private Long crewId;
    private LocalDateTime createdAt;
    public static CommentResponse of(Comment comment) {
        return CommentResponse.builder()
                .id(comment.getId())
                .comment(comment.getComment())
                .userName(comment.getUser().getUsername())
                .crewId(comment.getCrew().getId())
                .createdAt(comment.getCreatedAt())
                .build();
    }
}</code></pre>
<p><em><strong>ajax</strong></em></p>
<pre><code class="language-javascript">function commentCheck() {
        const content = $(&quot;#commentContent&quot;).val();
        const crewId = $(&quot;#crewIdComment&quot;).val();
        $.ajax({
            type : &quot;POST&quot;,
            url: &#39;/view/v1/crews/&#39;+ crewId +&#39;/comments&#39;,
            async: false,
            data: JSON.stringify({
                &quot;comment&quot;: content,
                &quot;crewId&quot;: crewId,
                &quot;parentId&quot; : null
            }),
            contentType : &quot;application/json; charset=utf-8&quot;,
            success: function(data) {
                alert(&quot;등록 되었습니다.&quot;) ;
                getComment();
                $(&#39;.commentContentCheck&#39;).text(&#39;&#39;);
                $(&#39;.commentContentCheck&#39;).css(&#39;display&#39;, &#39;none&#39;);
            },
            error: function (status) {
                alert(&quot;로그인 후 작성이 가능합니다.&quot;);
                $(status.responseJSON).each(function(){
                    $(&#39;.commentContentCheck&#39;).text(this.message);
                    $(&#39;.commentContentCheck&#39;).css(&#39;display&#39;, &#39;block&#39;);
                })
            }
        })
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/4f7c5f5a-0694-41c8-9d80-2fd67cd91885/image.png" alt=""></p>
<br>

<h3 id="✔️-부모-댓글-출력-로직">✔️ 부모 댓글 출력 로직</h3>
<p>getComment() 메소드가 실행되면서 <code>/view/v1/crews/{crewId}/comments</code> GET URL을 요청.
아래는 서버에서 실행되는 로직이다.
<em><strong>CommentViewController</strong></em></p>
<pre><code class="language-java">    // 댓글 리스트 출력
    @GetMapping(&quot;/view/v1/crews/{crewId}/comments&quot;)
    public ResponseEntity getCommentList(@PathVariable Long crewId) {
        List&lt;CommentViewResponse&gt; list = commentViewService.getCommentViewList(crewId);
        return new ResponseEntity&lt;&gt;(list, HttpStatus.OK);
    }</code></pre>
<p><em><strong>CommentViewService</strong></em>
comment entity의 from 메소드를 통해 List&lt;<code>Comment</code>&gt;를 List&lt;<code>CommentViewResponse</code>&gt;로 매핑해준다.</p>
<pre><code class="language-java">    // 댓글 리스트
    @Transactional(readOnly = true)
    public List&lt;CommentViewResponse&gt; getCommentViewList(Long crewId) {
        List&lt;Comment&gt; list = commentRepository.findByCrewId(crewId);
        return Comment.from(list);
    }</code></pre>
<p><em><strong>CommentViewResponse</strong></em></p>
<pre><code class="language-java">package teamproject.pocoapoco.domain.dto.comment.ui;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import teamproject.pocoapoco.domain.entity.Comment;

import java.time.LocalDateTime;
import java.util.LinkedList;
import java.util.List;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class CommentViewResponse {
    private Long id;
    private String comment;
    private String userName;
    private String nickName;
    private Long crewId;
    private boolean isParent;
    private boolean isDeleted;
    private List&lt;CommentViewResponse&gt; children;
    private LocalDateTime createdAt;

    public static CommentViewResponse of(Comment comment) {
        return CommentViewResponse.builder()
                .id(comment.getId())
                .comment(comment.getComment())
                .userName(comment.getUser().getUsername())
                .nickName(comment.getUser().getNickName())
                .crewId(comment.getCrew().getId())
                .isParent(comment.getParent()==null)    // true라면 부모댓글
                .isDeleted(comment.isSoftDeleted()==true) // true라면 삭제된 댓글
                .children(comment.getChildren() != null ? Comment.from(comment.getChildren()) : new LinkedList&lt;&gt;())
                .createdAt(comment.getCreatedAt())
                .build();
    }
}</code></pre>
<p><em><strong>ajax</strong></em>
data값은 List&lt;<code>commentViewResponse</code>&gt;</p>
<ul>
<li><p>$(data).each를 통해 댓글 리스트 하나하나가 아래의 html코드형식에 맞게 출력된다. </p>
</li>
<li><p>부모 댓글 <code>parent</code>과 삭제되지 않은 댓글 !<code>deleted</code>만 출력되도록 조건을 걸었다. </p>
</li>
<li><blockquote>
<p>조건설정을 위해 dto인 <strong>commentViewResponse에 추가한 필드</strong></p>
</blockquote>
</li>
<li><p>작성자와 로그인한 사용자가 같은 user일 경우 <code>삭제</code>, <code>수정</code> 버튼이 출력되도록 조건을 걸었다.</p>
<pre><code class="language-javascript">  // 부모댓글 리스트 출력
  function getComment(){

      $(&quot;#commentList&quot;).empty();  // commentList가 비어있는지 확인
      const principal = $(&quot;#principal&quot;).val(); // username
      const crewId = document.getElementById(&quot;crewIdComment&quot;).value;
      $.ajax({
          type:&quot;get&quot;,
          url:&quot;/view/v1/crews/&quot;+ crewId +&quot;/comments&quot;,
          dataType:&quot;json&quot;,
          success:function (data) {
              var html = &quot; &quot;;
              $(data).each(function(){ // data == commentViewList
                  if(this.parent===true &amp;&amp; this.deleted ===false){
                      html += &quot;&lt;ul class=&#39;list-group&#39;&gt;&quot;;
                      html += &quot;&lt;li class=&#39;list-group-item comments&#39;&gt;&quot;;
                      html += &quot;&lt;div class=&#39;comment&#39; id=&#39;&quot;+this.id+&quot;comment&#39;&gt;&quot;;
                      html += &quot;&lt;a href=&#39;javascript:; class=&#39;userImg&#39;&gt;&quot;;
                      html += &quot;&lt;/a&gt;&quot;;
                      html += &quot;&lt;a href=&#39;javascript:;&#39; class=&#39;writer&#39; style=&#39;display:inline&#39;&gt;&quot; + this.nickName + &quot;&lt;/a&gt;&quot;;
                      html += &quot;&lt;div class=&#39;comment-info&#39;&gt;&quot;;
                      html += &quot;&lt;span class=&#39;comment4 date&#39;&gt;&quot; + getFormatDate(new Date(this.createdAt)) + &quot;&lt;/span&gt;&quot;;
                      html += &quot;&lt;div class=&#39;comment-text&#39; id=&#39;&quot;+this.id+&quot;content&#39;&gt; &quot; + this.comment + &quot;&lt;/div&gt;&quot;;
                      html += &quot;&lt;div class=&#39;comment_etc&#39;&gt;&quot;;
                      html += &quot;&lt;div class=&#39;comment-info&#39;&gt;&quot;;
                      html += &quot;&lt;a href=&#39;javascript:;&#39; class=&#39;btn btn-secondary btn-icon-split comment_delete&#39; id=&#39;&quot;+this.id+&quot;getChildrenBtn&#39; &gt;&quot;;
                      html += &quot;&lt;button id=&#39;&quot;+this.id+&quot;getChildrenBtn&#39; onclick=&#39;getChildrenComment(&quot;+this.id+&quot;)&#39; class=&#39;btn btn&#39;&gt;답글(&quot;+this.children.length+&quot;)&lt;/button&gt;&quot;;
                      html += &quot;&lt;/a&gt;&quot;;
                      if(principal === this.userName) {
                          html += &quot;&lt;a href=&#39;javascript:;&#39;&gt;&quot;;
                          html += &quot;&lt;button type=&#39;button&#39; onclick=&#39;commentDelete(&quot; + this.id + &quot;)&#39; class=&#39;delete btn btn-outline-danger&#39;  id=&#39;&quot; + this.id + &quot;deleteBtn&#39;&gt;삭제&quot;;
                          html += &quot;&lt;/button&gt;&quot;;
                          html += &quot;&lt;/a&gt;&quot;;
                      }if(principal === this.userName) {
                          html += &quot;&lt;a href=&#39;javascript:;&#39;&gt;&quot;;
                          html += &quot;&lt;button type=&#39;button&#39;  onclick=&#39;commentUpdateForm(&quot; + this.id + &quot;)&#39; class=&#39;btn btn-outline-secondary&#39;  id=&#39;&quot; + this.id + &quot;updateBtn&#39;&gt;수정&quot;;
                          html += &quot;&lt;/button&gt;&quot;;
                          html += &quot;&lt;/a&gt;&quot;;
                      }
                      html += &quot;&lt;/div&gt;&quot;;
                      html += &quot;&lt;/div&gt;&quot;;
                      html += &quot;&lt;/div&gt;&quot;;
                      html += &quot;&lt;/li&gt;&quot;;
                      html += &quot;&lt;div id=&#39;&quot;+this.id+&quot;children&#39; class=&#39;children&#39;&gt;&lt;/div&gt;&quot;;
                  }

              });
              html += &quot;&lt;/ul&gt;&quot;;

              $(&quot;#commentList&quot;).append(html);

          }
      });
  }</code></pre>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/16c5551f-f900-4aec-8a65-bf35322b0488/image.png" alt=""></p>
<br>

</li>
</ul>
<h3 id="✔️--댓글-수정-및-삭제">✔️  댓글 수정 및 삭제</h3>
<ul>
<li><p>수정, 삭제 로직은 댓글, 대댓글 api가 동일하다
<em><strong>CommentViewController</strong></em></p>
<pre><code class="language-java">  // 댓글, 대댓글 수정
  @PutMapping(&quot;/view/v1/crews/{crewId}/comments/{commentId}&quot;)
  public ResponseEntity modifyComment(@RequestBody CommentRequest commentRequest, @PathVariable Long crewId, @PathVariable Long commentId , Authentication authentication) {
      commentService.modifyComment(commentRequest, crewId, commentId, authentication.getName());
      return new ResponseEntity&lt;&gt;(&quot;수정되었습니다.&quot;, HttpStatus.OK);

  }
  // 댓글, 대댓글 삭제
  @DeleteMapping(&quot;/view/v1/crews/{crewId}/comments/{commentId}&quot;)
  public ResponseEntity&lt;String&gt; deleteComment(@PathVariable Long crewId, @PathVariable Long commentId , Authentication authentication) {
      try {
          if (commentService.getDetailComment(crewId, commentId).getUserName().equals(authentication.getName()) ||
                  authentication.getAuthorities().stream().anyMatch(a -&gt; a.getAuthority().equals(&quot;ROLE_ADMIN&quot;))) {
              commentService.deleteComment(crewId, commentId, authentication.getName());
              return new ResponseEntity&lt;&gt;(&quot;articleCommentDeleting Success&quot;, HttpStatus.OK);
          } else {
              return new ResponseEntity&lt;&gt;(ErrorCode.INVALID_PERMISSION.getMessage(), HttpStatus.BAD_REQUEST);
          }
      }
      catch (EntityNotFoundException e){
          return new ResponseEntity&lt;&gt;(e.getMessage(), HttpStatus.BAD_REQUEST);
      }
  }</code></pre>
<p><em><strong>CommentService</strong></em></p>
<pre><code class="language-java">  // 수정
  public CommentResponse modifyComment(CommentRequest commentRequest, Long crewId, Long commentId, String userName) {
      Comment comment = checkCommentAndCrew(crewId, commentId);
      // 본인이 작성한 댓글이 아니면 에러
      isWriter(userName, comment);

      comment.setComment(commentRequest.getComment());
      return CommentResponse.of(comment);
  }
  // 삭제
  public CommentDeleteResponse deleteComment(Long crewId, Long commentId, String userName) {
      Comment comment = checkCommentAndCrew(crewId, commentId);
      // 본인이 작성한 댓글이 아니면 에러
      isWriter(userName, comment);

      comment.deleteSoftly(LocalDateTime.now());
      commentRepository.deleteAll(comment.getChildren());
      return CommentDeleteResponse.of(commentId);
  }
</code></pre>
</li>
</ul>
<pre><code>&lt;br&gt;

- - - 
- 아래는 view

_**getComment() `수정 부분`**_
수정 버튼을 누르면 수정 폼을 보여주는 commentUpdateForm()이 실행된다.  
    - text area 
    - 수정 버튼 
    - 수정 취소 버튼이 있다.</code></pre><p>html += &quot;<a href='javascript:;'>&quot;;
html += &quot;<button type='button'  onclick='commentUpdateForm(" + this.id + ")' class='btn btn-outline-secondary'  id='" + this.id + "updateBtn'>수정&quot;;
html += &quot;</button>&quot;;
html += &quot;</a>&quot;;</p>
<pre><code>_**ajax**_
```javascript
    function commentUpdateForm(id){
        $(&quot;#&quot;+id+&quot;updateBtn&quot;).hide();
        $.ajax({
            type: &quot;GET&quot;,
            url: &quot;/view/v1/crews/&quot;+crewId+&quot;/comments/&quot;+id,
            dataType: &quot;json&quot;,
            success: function (data) {
                var html = &quot;&lt;div class=&#39;comment-update&#39;&gt;&quot;;
                html += &quot;&lt;div class=&#39;comment-update-form&#39;&gt;&quot;;
                html += &quot;&lt;textarea class=&#39;form-control&#39; id=&#39;&quot;+id+&quot;ucommentContent&#39; rows=&#39;3&#39; placeholder=&#39;댓글을 입력하세요.&#39;&gt;&quot;+data.comment+&quot;&lt;/textarea&gt;&quot;;
                html += &quot;&lt;div class=&#39;field-error &quot;+id+&quot;ucommentContentCheck&#39;&gt;&quot;;
                html += &quot;&lt;/div&gt;&quot;;
                html += &quot;&lt;div class=&#39;comment-update-btn&#39;&gt;&quot;;
                html += &quot;&lt;button type=&#39;button&#39; class=&#39;btn btn-secondary&#39;  onclick=&#39;commentUpdate(&quot;+data.id+&quot;)&#39;&gt;수정&lt;/button&gt;&quot;;
                html += &quot;&lt;button type=&#39;button&#39; class=&#39;btn btn-secondary&#39; onclick=&#39;commentUpdateCancel(&quot;+data.id+&quot;)&#39;&gt;취소&lt;/button&gt;&quot;;
                html += &quot;&lt;/div&gt;&quot;;
                html += &quot;&lt;/div&gt;&quot;;
                html += &quot;&lt;/div&gt;&quot;;
                $(&quot;#&quot;+data.id+&quot;content&quot;).html(html);
            },
            error: function (xhr, status, error) {
                alert(status.message);
            }
        });
    }
    function commentUpdate (id) {
        {
            const crewId = $(&#39;#crewIdComment&#39;).val();
            const ucommentContent = $(&#39;#&#39;+id+&#39;ucommentContent&#39;).val();
            $.ajax({
                type: &quot;PUT&quot;,
                url: &quot;/view/v1/crews/&quot;+ crewId +&quot;/comments/&quot;+id,
                data: JSON.stringify({&quot;crewId&quot;: crewId,
                    &quot;comment&quot;: ucommentContent,
                }),
                contentType: &quot;application/json; charset=utf-8&quot;,
                success: function(data) {
                    alert(data) ;
                    getComment();
                    $(&#39;.&#39;+id+&#39;ucommentContentCheck&#39;).text(&#39;&#39;);
                },
                error: function (status) {
                    $(status.responseJSON).each(function(){
                        $(&#39;.&#39;+id+&#39;ucommentContentCheck&#39;).text(this.message);
                    })
                }
            });
        }
    }
    function commentUpdateCancel(id){
        $(&quot;.comment-update&quot;).html(&quot;&quot;);
        $(&quot;#&quot;+id+&quot;updateBtn&quot;).show();
        getComment();
    }</code></pre><p><img src="https://velog.velcdn.com/images/jsang_log/post/d288acfe-73c2-4fc9-9e59-a4dcdb92fb6e/image.gif" alt=""></p>
<p><em><strong>getComment() <code>삭제 부분</code></strong></em>
삭제 버튼을 누르면 commentDelete()가 실행된다.
이때 id는 commentViewResponse의 id이다.</p>
<pre><code class="language-html">html += &quot;&lt;a href=&#39;javascript:;&#39;&gt;&quot;;
html += &quot;&lt;button type=&#39;button&#39; onclick=&#39;commentDelete(&quot; + this.id + &quot;)&#39; class=&#39;delete btn btn-outline-danger&#39;  id=&#39;&quot; + this.id + &quot;deleteBtn&#39;&gt;삭제&quot;;
html += &quot;&lt;/button&gt;&quot;;
html += &quot;&lt;/a&gt;&quot;;</code></pre>
<p><em><strong>ajax</strong></em></p>
<pre><code class="language-javascript">function confirmDelete() {
        if(confirm(&quot;정말 삭제하시겠습니까?&quot;)) {
            $(&quot;#form&quot;).submit();
        }
        return false;
    }
function commentDelete (id) {
        {
            $.ajax({
                type: &quot;DELETE&quot;,
                url: &quot;/view/v1/crews/&quot;+crewId+&quot;/comments/&quot;+ id,
                contentType: &quot;application/json; charset=utf-8&quot;,
                success: function () {
                    alert(&#39;댓글이 삭제되었습니다.&#39;);
                    getComment();
                }
                ,
                error: function (xhr, status, error) {
                    alert(status.message);
                }
            });
        }
    }</code></pre>
<br>


<p>여기까지 부모댓글 CRUD, 대댓글 update,delete까지 구현되었다.
분량이 길어지는 것 같아 대댓글 생성 및 조회 기능은 다음 포스트에서 마무리하려고 한다!</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://velog.io/@guswns3371/JPA-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0-self-%EC%B0%B8%EC%A1%B0">[JPA] Self Reference (순환 참조, 셀프 참조)</a>
<a href="https://chaelin1211.github.io/study/2021/04/14/thymeleaf-ajax.html">[Thymeleaf] ajax를 이용해 비동기식 화면 수정</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis 개념 정리]]></title>
            <link>https://velog.io/@jsang_log/Redis-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jsang_log/Redis-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 01 Mar 2023 23:01:14 GMT</pubDate>
            <description><![CDATA[<h3 id="키워드">키워드</h3>
<p><code>In-memoryDB</code> : 모든 데이터를 메모리에 저장하고 조회하는 인메모리 데이터베이스
<code>NoSQL</code> : Not Only SQL, RDBMS가 갖고 있는 특성뿐만 아니라, 다른 특성들을 부가적으로 지원한다
<code>싱글 스레드</code> : 하나의 트랜잭션은 하나의 명령만 실행
<code>cache</code> : 한 번 읽어온 데이터를 임의의 공간에 저장하여, 다음에 읽을 때는 빠르게 결과값을 받을 수 있도록 도와주는 공간
<code>영속성</code> : 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성
<code>global cache</code> : 여러 서버에서 Cache Server에 접근하여 사용하는 캐시
<code>local cache</code> : Local 장비 내에서만 사용 되는 캐시, Local 장비의 Resource를 이용한다 (Memory, Disk)
<code>트랜잭션</code> : 쪼갤 수 없는 업무 처리의 최소 단위</p>
<h1 id="redis">Redis</h1>
<h3 id="✔-redis란">✔ Redis란?</h3>
<p>redis는 오픈소스로서 데이터베이스(NOSQL DBMS)로 분류가 되기도 하고 Memcached와 같이 인메모리 솔루션으로 분류되기도 한다. </p>
<p>성능은 memcached에 버금가면서 다양한 데이터 구조체를 지원함으로써 Message Queue, Shared memory, Remote Dictionary 용도로도 사용될 수 있으며, 이런 이유로 인스타그램, 네이버 재팬의 LINE 메신져 서비스, StackOverflow,Blizzard,digg 등 여러 소셜 서비스에 널리 사용되고 있다.</p>
<p>✔️** NoSQL 관점**
NoSQL관점에서 봤을 때 redis는 가장 단순한 키-밸류 타입을 사용하고 있다. 데이터 모델은 복잡할수록 성능이 떨어지므로 redis는 단순한 구조를 통해 높은 성능을 보장한다고 할 수 있다.</p>
<p>NoSQL에는 다양한 제품이 있지만 이 중, redis가 주목받는 이유는 다음과 같다.</p>
<ul>
<li>데이터 저장소로 가장 입/출력이 빠른 메모리를 채택</li>
<li>단순한 구조의 데이터 모델인 키- 밸류 방식을 통해 빠른 속도를 보장</li>
<li>캐시 및 데이터스토어에 유리</li>
<li>다양한 API지원</li>
</ul>
<p>✔️ <strong>캐싱 솔루션 관점</strong>
redis, memcached, 구아바 라이브러리등 인메모리 캐시방식을 적용한 제품 중 redis는 <code>global cache</code> 방식을 채택하였다. global cache 방식은 네트워크 트래픽이 발생하기 때문에 java heap영역에서 조회되는 local cache의 성능이 더 낫지만, WAS 인스턴스가 증가할 경우엔 캐시에 저장되는 데이터 크기가 커질수록 redis 방식이 더 유리하다.</p>
<p>redis는 급격한 사용자가 집중되는 상황이나 대규모의 확장이 예정되어 있는 환경에 적합하다. global cache 방식이 적용되어 was 인스턴스 확장에는 유리하지만 cache 및 redis 세션 등 관리 포인트가 늘어난다는 단점이 존재한다.</p>
<h3 id="✔-nosql과-redis">✔ NoSQL과 Redis</h3>
<p>✔️ <strong>사례로 보는 NoSQL로써의 Redis</strong>
사용자 증가로 인한 서비스 중단의 원인이 DB 서버일때, 너무 많은 SQL 문 처리 요청을 받아 MySQL이 동시에 처리할 수 있는 한계치를 넘어섰고 그로 인해 응답시간이 길어질 수 있다. MySQL 서버의 응답시간 지연이 발생하여 톰캣 서버에서 DB 서버로의 네트워크 연결이 모두 끊어졌다. 이런 상황은 어떻게 대처해야 할까? 
2가지의 방안이 있다.</p>
<p>1.서비스를 멈추고 <strong>단일 서버의 성능을 증가</strong>시켜서 더 많은 요청을 처리하는 방법을 <code>스케일 업</code>이라고 한다.<br><code>-&gt;</code> 통상적으로 스케일 업을 적용하면 서비스의 중단이나 추가 하드웨어 비용이 발생한다.
2.서비스를 멈추고 새로운 서버에 기존 서버의 데이터를 옮겨 다시 새롭게 서비스를 시작한다. 
<code>-&gt;</code> 서비스 중지 상태에서 데이터를 옮기고 데이터를 정리하느라 많은 시간소요</p>
<p>이에 반해서 대부분의 NoSQL은 처음부터 <code>스케일 아웃</code>을 염두에 두고 설계되었기 때문에 데이터의 증가나 요청량이 증가하더라도 <strong>동일하거나 비슷한 사양의 새로운 하드웨어를 추가</strong>하면 대응이 가능하다. 데이터가 적거나 또는 요청량이 적을때는 일반 RDBMS를 사용하더라도 서비스를 제공하는데 문제가 없다. 하지만 데이터 증가량을 측정하기 불가능하거나 서비스 요청량의 증가를 예측하기 어려운 상황에서는 NoSQL을 저장소로 사용하는 것이 현명한 선택이 될것이다.</p>
<p>멤캐시드와 레디스를 같은 캐시 캐시 시스템으로서 동등한 위치에서 비교하게 된것은 레디스가 멤캐시드와 동일한 기능을 제공하면서 영속성, 다양한 데이터 구조와 같은 부가적인 기능을 지원하기 때문이다. 또한 특정한 조건에서는 멤캐시드에 비해서 더 나은 성능을 보여주기도 한다. 이와 같은 이유로 레디스는 멤캐시드를 대체하는 솔루션으로 주목 받았다.</p>
<p>레디스는 모든 데이터를 메모리에 저장하고 조회한다. 즉, 인메모리 데이터베이스 솔루션이다. 빠른 성능은 레디스의 특징 중 일부분에 지나지 않는다. 다른 인메모리 솔루션들과의 차이점 중 가장 특별한 점은 레디스의 &#39;다양한 자료구조&#39;다. 
<br></p>
<p>✔️ <strong>Key/Value Store</strong>
특정 키 값에 값을 저장하는 구조로 되어 있고 기본적인 PUT/ GET Operation을 지원한다. 단, 이 모든 데이터는 메모리에 저장되고 이로 인해 매우 빠른 write/read 속도를 보장한다. 그래서 전체 저장 가능한 데이터 용량은 물리적인 메모리 크기를 넘어설 수 있다(물론 OS의 disk swapping 영역 등을 사용해 확장은 가능하겠지만 성능이 급격하게 떨어지기 때문에 의미가 없다). 데이터 액세스는 메모리에서 일어나지만 서버 재시작(server restart)과 같이 서버가 내려갔다가 올라오는 상황에 데이터의 저장을 보장하기 위해 디스크를 persistence store로 사용한다.</p>
<p>✔️ <strong>레디스의 데이터타입</strong>
모든 데이터를 메모리에 저장하고 조회하기때문에 빠른 Read, Write 속도를 보장하고 또 다양한 자료구조를 지원한다는 점이다.</p>
<blockquote>
<p>📑 Redis가 지원하는 데이터 형식
String, List, Set, Sorted Set, Hash</p>
</blockquote>
<table>
<thead>
<tr>
<th>메소드명</th>
<th>반환 오퍼레이션</th>
<th>Redis 자료구조</th>
</tr>
</thead>
<tbody><tr>
<td><code>opsForValue()</code></td>
<td>ValueOperations</td>
<td>String</td>
</tr>
<tr>
<td><code>opsForList()</code></td>
<td>ListOperations</td>
<td>List</td>
</tr>
<tr>
<td><code>opsForSet()</code></td>
<td>SetOperations</td>
<td>Set</td>
</tr>
<tr>
<td><code>opsForZSet()</code></td>
<td>ZSetOperations</td>
<td>Sorted Set</td>
</tr>
<tr>
<td><code>opsForHash()</code></td>
<td>HashOperations</td>
<td>Hash</td>
</tr>
<tr>
<td>다양한 자료구조를 지원하게 되면 개발의 편의성이 좋아지고 난이도가 낮아진다.</td>
<td></td>
<td></td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/jsang_log/post/0c9a9508-22e4-4a9f-b094-c241d3962aab/image.png" alt=""></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>예를들어 어떤 데이터를 정렬을 해야하는 상황이 있을 때 DBMS를 이용한다면 DB에 데이터를 저장하고 -&gt; 저장된 데이터를 정렬하여 -&gt; 다시 읽어오는 과정은 디스크에 직접 접근을 해야하기 때문에 시간이 더 걸린다는 단점이 있다. 이 때 In Memory 데이터베이스인 Redis를 이용하고 레디스에서 제공하는 Sorted Set이라는 자료구조를 사용하면 더 빠르고 간단하게 데이터를 정렬할 수 있다.</p>
<h3 id="✔-싱글-스레드-기반의-redis">✔ 싱글 스레드 기반의 Redis</h3>
<p>Redis 4.0 부터 4개의 스레드로 동작합니다. 메인스레드 1개, 시스템 명령들을 처리하는 thread 3개
메인스레드에서 사용자 명령어를 처리하기에 싱글 스레드로 동작한다고 이해하면 됩니다.
싱글 스레드로 동작하고 해시 기반의 get/set 을 지원하기에 Redis 의 주요 기능들이 O(1) 시간복잡도로 빠르게 처리되며, 데이터를 일관성있게 유지할 수 있습니다.</p>
<p>(자주 비교되는 맴캐쉬드는 멀티 스레드 지원)</p>
<p>Keys(저장된 모든키를 보여주는 명령어)나 flushall(모든 데이터 삭제)등의 명령어를 사용할 때, 맴캐쉬드의 경우 1ms정도 소요되지만 레디스의 경우 100만건의 데이터 기준 1초로 엄청난 속도 차이가 있다.
<code>-&gt;</code> 많은 키(100,000,000,000개) 건 적은 키(1개) 건 스키마가 많건 적건 동일한 시간이 사용된다.</p>
<p>✔️싱글 스레드 기반의 Redis 주의사항
주의해야할 점은 redis 는 single thread 기반이기에 시간이 오래 걸리는 명령어를 사용할 경우 뒤에 있는 명령어들은 기다려야하므로 성능에 영향을 줄 수 있습니다.</p>
<h3 id="✔-서버-복제-지원">✔ 서버 복제 지원</h3>
<p>Master - slave 구조로, 여러개의 복제본을 만들 수 있다. 데이터베이스 읽기를 확장할 수 있기 때문에 높은 가용성(오랜 시간동안 고장나지 않음) 클러스터를 제공한다.</p>
<h3 id="✔-트랜젝션">✔ 트랜젝션</h3>
<p>트렌잭션이란.. 트랜잭션으로 묶게 되면 트랜잭션 내부에서 하나의 로직이 실패하여 오류가 나게되면 모두 취소시키며 그렇지 않으면 모두 성공시키는 것이다. Redis는 이 트랜잭션 기능을 지원한다.</p>
<h3 id="✔-pub--sub-messaging">✔ Pub / Sub messaging</h3>
<p>Publish(발행)과 Sub(구독)방식의 메시지를 패턴 검색이 가능하다. 따라서 높은 성능을 요구하는 채팅, 실시간 스트리밍, SNS 피드 그리고 서버상호통신에 사용할 수 있다.</p>
<p>메시지들을 queue로 관리하지 않고, publish 하는 시점 기준으로 미리 subscribe 등록 대기 중인 클라이언트들을 대상으로만 메시지를 전달한다.</p>
<h3 id="✔-위치기반-데이터-타입-지원">✔ 위치기반 데이터 타입 지원</h3>
<p>Redis는 실시간 위치기반데이터를 지원한다. 두 위치의 거리를 찾거나, 사이에 있는 요소 찾기등의 작업을 수행할 수 있다.</p>
<hr>
<br>

<h1 id="면접-예상-질문">면접 예상 질문</h1>
<h3 id="q1-rdbms-nosql-차이점">Q1. RDBMS, NOSQL 차이점</h3>
<ol>
<li>RDBMS, NOSQL 차이점</li>
</ol>
<ul>
<li>RDBMS</li>
</ul>
<p>RDBMS는 관계형 데이터베이스 관리 시스템으로서, 구성된 테이블끼리 관계를 맺고 모여있는 집합체입니다. 테이블 형식으로 속성, 값으로 이루어져 있습니다. 트랜잭션 처리가 가능하고, 스키마를 미리 정의해 줘야해서 테이블 생성문으로 정의해줍니다.</p>
<pre><code>CREATE TABLE member(
    id int not null auto_increment primary key,
    name varchar(10) nut null,
    password varchar(12) not null,
    reg_date datetime not null
 );</code></pre><ul>
<li>NOSQL</li>
</ul>
<p>NoSQL은 테이블 간의 관계를 정의하지 않고 key-value로 이루어져있습니다. 정해진 스키마가 없고 데이터의 입출이 자유로워서 대용량 데이터를 저장하는데 용이합니다. 몽고디비, 레디스 등이 있습니다.</p>
<p>오토 샤딩 기능이 있어서 대용량 데이터를 자동으로 분산 처리합니다. (RDB도 비슷한 기능인 클러스터링이 있지만 설정이 복잡)</p>
<p>명확한 스키마가 필요하고, 데이터 중복을 허용하지 않는 경우 RDBMS를 사용합니다. 반대로 정확한 데이터 구조가 있지 않고 데이터가 확장이 될 수 있는 경우 NoSql을 사용합니다.</p>
<h3 id="q2-memcached-vs-redis">Q2. Memcached vs Redis</h3>
<p>맴캐쉬드는 명료하고 단순함을 위하여 개발된 반면,</p>
<p>레디스는 다양한 용도에 효과적으로 사용할 수 있도록 많은 특징을 가지고 개발되었다.</p>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/3814f8a7-a338-489e-9a9d-410f27c65a22/image.png" alt=""></p>
<h3 id="q3-redis는-왜-사용해야할까">Q3. Redis는 왜 사용해야할까?</h3>
<p>Redis는 아직도 활발하게 현재 시장에서 많이 사용되고 있습니다.</p>
<p>기본적으로 서비스 환경에서는 확장성을 고려해서 Redis를 사용하는 것이 일반적으로 좋아 보입니다.</p>
<ul>
<li>보다 광범위한 데이터 구조 및 스트림 처리 기능에 대한 액세스가 필요합니다.</li>
<li>키와 값을 제자리에서 수정하고 변경할 수 있는 기능이 필요합니다.</li>
<li>사용자 지정 데이터 제거 정책이 필요합니다. (예: 시스템의 메모리가 부족하더라도 키를 더 긴 TTL로 유지해야 함)</li>
<li>백업 및 웜 재시작을 위해 데이터를 디스크에 유지해야 합니다.</li>
<li>복제본 및 클러스터링을 통해 애플리케이션의 높은 가용성 또는 확장성을 확보해야 합니다.</li>
</ul>
<h3 id="참고-문헌">참고 문헌</h3>
<p><a href="https://steady-coding.tistory.com/586">[데이터베이스] Redis란?</a>
<a href="https://sudo-minz.tistory.com/101">Redis 레디스 특징, 장단점, Memcached와 redis 비교</a>
<a href="https://12bme.tistory.com/615">[Redis] 이것이 레디스다(1) - NoSQL</a>
<a href="https://dataonair.or.kr/db-tech-reference/d-lounge/technical-data/?mod=document&amp;uid=236126">주목받는 NoSQL &amp; Cache 솔루션 : Redis</a>
<a href="https://dev-jj.tistory.com/entry/%EC%BA%90%EC%8B%9CCache-Local-Cache-Global-Cache">캐시(Cache) Local Cache &amp; Global Cache</a>
<a href="https://brunch.co.kr/@skykamja24/575">레디스는 언제 어떻게 사용하면 좋을까</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[팀프로젝트] Springboot 프로젝트에 Redis 연동하기(AWS ec2에 띄워보자!)]]></title>
            <link>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Springboot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Redis-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0AWS-ec2%EC%97%90-%EB%9D%84%EC%9B%8C%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Springboot-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Redis-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0AWS-ec2%EC%97%90-%EB%9D%84%EC%9B%8C%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 01 Mar 2023 22:45:02 GMT</pubDate>
            <description><![CDATA[<h2 id="❗-redis를-ec2에서-도커-컨테이너로-띄우는-방법">❗ Redis를 ec2에서 도커 컨테이너로 띄우는 방법</h2>
<p>*AWS ec2에 도커를 띄우는 방법은 정리해두었으니 참고해보자
[AWS Ec2 스팟 인스턴스설정 + MySql 도커 띄우기]
(<a href="https://velog.io/@jsang_log/AWS-Ec2-%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">https://velog.io/@jsang_log/AWS-Ec2-%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</a>)
<br></p>
<h4 id="📑-xshell에-아래의-명령어들을-입력한다">📑 xshell에 아래의 명령어들을 입력한다</h4>
<p>✔️ redis 이미지 파일 다운</p>
<pre><code>docker pull redis </code></pre><p><img src="https://velog.velcdn.com/images/jsang_log/post/728f1d9e-2037-4883-9a4a-8b2c1e179612/image.png" alt=""></p>
<p>✔️ 이미지 파일 확인</p>
<pre><code>docker images</code></pre><p><img src="https://velog.velcdn.com/images/jsang_log/post/0d8a3fef-0c5a-46fa-a440-d801410aaca1/image.png" alt="">
✔️ redis 컨테이너 실행</p>
<pre><code>docker run -p 6379:6379 --name (redis 컨테이너 이름) -d redis</code></pre><p>ex) docker run -p 6379:6379 --name spring-redis -d redis
<img src="https://velog.velcdn.com/images/jsang_log/post/b87eef74-a76c-49c6-a468-1e52fa071f3b/image.png" alt="">
✔️ AWS 보안그룹 인바운드/아웃바인드 설정
<img src="https://velog.velcdn.com/images/jsang_log/post/56f5c7cb-14b9-4706-aacd-8d2680d3f68f/image.png" alt="">
*AWS 보안그룹 인바운드/아웃바인드 설정방법은 아래 포스트를 참고해보자
[AWS Ec2 스팟 인스턴스설정 + MySql 도커 띄우기]
(<a href="https://velog.io/@jsang_log/AWS-Ec2-%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">https://velog.io/@jsang_log/AWS-Ec2-%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</a>)
<br>
✔️ 실행중인 컨테이너 spring-redis와 상호작용하고(-i) 명령어는 redis-cli로 redis와 접근</p>
<pre><code>docker exec -i -t (redis 컨테이너 이름) redis-cli</code></pre><p><img src="https://velog.velcdn.com/images/jsang_log/post/449c2c94-7dfa-4f13-83f6-287ab137af03/image.png" alt="">
ping pong이 잘 오가면 클라이언트와 서버가 제대로 작동하고 있는 것이다.
❓ Aws 컨테이너 관점에서 localhost (또는 127.0.0.1)
<br>
<br></p>
<blockquote>
<p>🎫** 비밀번호 인증이 필요한 이유**
난데없는 backup은 뭐지?
redis에 값을 넣고 key를 조회하다보면 내 key값들은 사라지고 아래 화면처럼 알 수 없는 키들을 보곤 한다.
악의적이고 단조로운 해킹에 당한 것을 축하한다 🥂
<img src="https://velog.velcdn.com/images/jsang_log/post/9e1d8b25-2d6c-469f-8c04-db73d7fd3ab8/image.png" alt=""></p>
</blockquote>
<ul>
<li>redis의 잘 알려진 6379 포트가 열려있고 </li>
<li>redis에 비밀번호를 설정하지 않는다면 해킹의 표적이 되기 쉽다.</li>
</ul>
<p>그렇다면 우리는 다음과같이 위의 두가지 조건을 만족하지 않게하면 될텐데
시스템에서 대체적으로 많이 알려져있는 포트는 가급적이면 변경하지 않고 사용하는 것이 좋으므로 6379번 그대로 유지하자.
단, redis 서버에 접근하려면 필수로 인증을 하게끔 설정하는 것을 권유한다.
<br>
<br></p>
<h2 id="❗-비밀번호-설정">❗ 비밀번호 설정</h2>
<p>docker image로 실행하고 있기에 간단하게 도커이미지에 환경변수를 넘기는 것만으로 requirepass가 설정할 수 있다. (간단한 환경설정이 도커 방식의 장점)</p>
<p><strong>✔️ 기존의 docker (비밀번호 설정되지 않은) 컨테이너 삭제 후 명령어를 재실행</strong></p>
<ul>
<li><p>도커 컨테이너 중지</p>
<pre><code> Ctrl + Insert로 `복사` / Shift + Insert로 `붙여넣기`</code></pre><pre><code>docker ps
docker stop (redis containerID)</code></pre><p><img src="https://velog.velcdn.com/images/jsang_log/post/63e7ef29-95e4-4a7b-ba17-99e321d91f5a/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>도커 컨테이너 삭제<pre><code>docker rm (redis containerID)</code></pre><img src="https://velog.velcdn.com/images/jsang_log/post/9ec43e2a-fca7-4bfa-9a8c-6d30167daa05/image.png" alt=""></li>
</ul>
<p><strong>✔️ docker를 사용하여 redis 이미지를 만들고 redis 비밀번호를 설정</strong></p>
<pre><code>   docker run -p 6379:6379 --name (redis 컨테이너 이름) -d redis:latest --requirepass &quot;(비밀번호)&quot;</code></pre><p>ex)  docker run -p 6379:6379 --name spring-redis -d redis:latest --requirepass &quot;123456&quot;
<br></p>
<p><strong>✔️ redis-cli에서 인증 후 접속 확인</strong></p>
<pre><code>docker exec -i -t (redis 컨테이너 이름) redis-cli
AUTH (비밀번호)
</code></pre><p><img src="https://velog.velcdn.com/images/jsang_log/post/5b7e9096-cd30-457b-b3e0-5faf21bd4cce/image.png" alt=""></p>
<br>
redis 설치 완료!
<br>

<h2 id="❗-springboot과-연동하기">❗ Springboot과 연동하기</h2>
<p><code>라이브러리</code>
Spring Boot에서는 Spring Date Redis를 통해 Lettuce, Jedis라는 오픈소스 Java 라이브러리를 사용할 수 있다. 
<code>접근방식</code>
Spring Data Redis는 Redis에 두 가지 접근 방식을 제공한다.</p>
<p>하나는 RedisTemplate을 이용한 방식이며, 다른 하나는 RedisRepository를 이용한 방식이다.</p>
<hr>
<p>_ *해당 포스트는 Lettuce를 통해 Redis를 사용한다._
_ *또한 RedisTemplate 방식으로 Redis에 접근한다._</p>
<hr>
<p> Redis에 접근하기 위해서는 Redis 저장소와 연결하는 과정이 필요하다.</p>
<p><strong>✔️ build.gradle 의존성 추가</strong></p>
<pre><code class="language-java">// redis
implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
<p><strong>✔️ Redis 저장소와 연결</strong></p>
<pre><code class="language-java">
@Configuration
@EnableRedisRepositories // redis 활성화
public class RedisConfig {
    @Value(&quot;${spring.redis.host}&quot;)
    private String redisHost;

    @Value(&quot;${spring.redis.port}&quot;)
    private int redisPort;

    @Value(&quot;${spring.redis.password}&quot;)
    private String redisPassword;

    /*
    RedisTemplate을 이용한 방식

    RedisConnectionFactory 인터페이스를 통해
    LettuceConnectionFactory를 생성하여 반환
     */

    @Bean
    public RedisConnectionFactory redisConnectionFactory(int dbIndex) {
        final RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisHost);
        redisStandaloneConfiguration.setPort(redisPort);
        redisStandaloneConfiguration.setPassword(redisPassword);
        return new LettuceConnectionFactory(redisStandaloneConfiguration);

    @Bean
    public RedisTemplate&lt;String, String&gt; redisTemplate() {
        // redisTemplate를 받아와서 set, get, delete를 사용
        RedisTemplate&lt;String, String&gt; redisTemplate = new RedisTemplate&lt;&gt;();
        /**
         * setKeySerializer, setValueSerializer 설정
         * redis-cli을 통해 직접 데이터를 조회 시 알아볼 수 없는 형태로 출력되는 것을 방지
         */
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }
}</code></pre>
<p><strong>✔️ 환경 설정</strong></p>
<ul>
<li>application.yml 에 해당 내용 추가
application.yml 파일은 깃허브에 그대로 노출되므로 임시의 값을 넣어주고 Environment Variables 설정을 통해 실제 값을 주입한다.</li>
</ul>
<pre><code>spring:
  redis:
    host: localhost
    port: 6379
    password: hello</code></pre><ul>
<li>Environment Variables
<img src="https://velog.velcdn.com/images/jsang_log/post/1ac9aede-37b2-4f62-9db9-7d9de9b4f95b/image.png" alt=""></li>
<li>⭐ SPRING_REDIS_HOST 의 주소는 ec2 인스턴스의 퍼블릭 IPv4 주소로 값을 넣어주어야 한다.
<img src="https://velog.velcdn.com/images/jsang_log/post/0589764c-55ea-441c-819e-6db5eccd5310/image.png" alt=""></li>
</ul>
<br>
<br>
여기까지 docker에 Redis 띄우기 및 Springboot 연동 실습이 끝났다.
<br>
<br>


<h2 id="-마무리">‼ 마무리</h2>
<p>Redis는 build.gradle dependencies에서만 적용을 하면 redis가 되는 줄 알았지만, 따로 다운로드 및 설치 후 port 실행을 해야지 연결이 가능했다.
그리고 redis를 ec2에 띄웠기 때문에 spring.reids.host주소를 IPv4주소로 주입해주는 것을 알기까지 오래 헤맸던 것 같다.
막상 정리하고 보니 설치 과정이 너무 복잡하지는 않다는 생각이 들고, redis를 설정함으로써 이번 팀프로젝트에 다양한 기능을 확장해나갈 수 있었기에 보람찬 마음도 든다!  </p>
<br>

<h3 id="참고-문헌">참고 문헌</h3>
<p><a href="https://ratseno.tistory.com/93">https://ratseno.tistory.com/93</a>
<a href="https://thxwelchs.github.io/%EB%82%B4redis%EA%B0%80%ED%95%B4%ED%82%B9%EB%8B%B9%ED%96%88%EB%8B%A4%EA%B3%A0%EC%8B%AC%EC%A7%80%EC%96%B4local%EC%9D%B8%EB%8D%B0/">내redis가해킹당했다고심지어local인데/</a>
<a href="https://wildeveloperetrain.tistory.com/32">https://wildeveloperetrain.tistory.com/32</a>
<a href="https://jojoldu.tistory.com/297">https://jojoldu.tistory.com/297</a></p>
<p>*ec2 docker 컨테이너가 아닌 localhost 환경에 Redis를 설치하고 싶다면 이 링크를 참조해서 설정할 수 있다 
<a href="https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-Window10-%ED%99%98%EA%B2%BD%EC%97%90-Redis-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0">[REDIS] 📚 Window10 환경에 Redis 설치 &amp; 설정</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[팀프로젝트] 비동기방식으로 좋아요👍 구현하기 ]]></title>
            <link>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%B9%84%EB%8F%99%EA%B8%B0%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%A2%8B%EC%95%84%EC%9A%94-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jsang_log/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%B9%84%EB%8F%99%EA%B8%B0%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%A2%8B%EC%95%84%EC%9A%94-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 01 Mar 2023 14:00:21 GMT</pubDate>
            <description><![CDATA[<p>📍 이번 포스트에서는 좋아요 기능을 구현해보며 </p>
<ul>
<li>동기 방식과 비동기 방식의 차이</li>
<li>비동기로 구현하는 이유</li>
<li>언어나 프레임워크가 아닌 비동기를 구현하는 방식인 <code>Ajax</code></li>
</ul>
<p>등등 에 대한 간단한 설명을 곁들이려고 한다.
<br></p>
<h3 id="개발환경">개발환경</h3>
<p>개발 툴 : SpringBoot 2.7.7
자바 : JAVA 11
빌드 : Gradle
템플릿 엔진 : Thymeleaf
spring MVC 구조
<img src="https://velog.velcdn.com/images/jsang_log/post/89275875-625e-42b7-b0ac-1a062149d509/image.png" alt=""></p>
<br>



<h3 id="💬-비동기-방식은-왜-필요할까">💬 비동기 방식은 왜 필요할까?</h3>
<p>HTML 파일을 전체 새로고침하게 되면 그 안에 있는 많은 script, jpg 등의 컨텐츠를 새로 불러와야 한다. CDN이나 많은 javascript 이벤트가 있을 경우 서버가 매우 버벅거릴 것이다.</p>
<p>댓글을 다는 경우에는 해당 댓글 파트만 변경이 되면 되는데 굳이 다른 파트까지 변경시키게 되는 것이다. 이는 심각한 자원 낭비이다. 따라서 비동기 통신으로 이를 처리한다.</p>
<h3 id="✔-ajax란">✔ Ajax란?</h3>
<p>** Ajax(Asynchronous JavaScript And XML)**</p>
<ul>
<li>브라우저 내에서 비동기 기능(신호를 보냈을 때 새로운 html페이지를 로딩 할 필요 없이 동작 수행)을 제공하는 기법</li>
<li>동적으로 페이지를 변화 시켜주는 기능
즉, Ajax는 웹 페이지 전체를 <strong>다시 로딩하지 않고도, 웹 페이지의 일부분만을 갱신</strong>할 수 있게 해준다.</li>
</ul>
<ul>
<li>최근에는 XML 보다  JSON을 더 많이 사용한다.</li>
</ul>
<p><strong><code>동기 방식</code> vs <code>비동기 방식</code></strong></p>
<ul>
<li>동기 : 서버에 신호를 보냈을 때 응답이 들어와야 다음 동작 수행 가능</li>
<li>비동기 : 신호를 보냈을 때 응답 상태와 상관없이 동작 수행 가능<ul>
<li>자료를 요청할 때 클라이언트의 진행 시간을 기다릴 필요 없이 작업을 수행할 수 있다는 장점이 있다.<br>

</li>
</ul>
</li>
</ul>
<h3 id="✔-ajax-동작-방식">✔ Ajax 동작 방식</h3>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/04de3c3c-cc33-4956-b33a-1b77f06b2363/image.png" alt=""></p>
<p>브라우저가 Data를 요청 및 전송한다.
ex) 댓글로 예를 들면, 댓글 내용과 해당 댓글이 달린 글의 id값을 서버로 보낸다. 그리고 처리 결과를 받기를 기다린다.)</p>
<p>서버는 받은 Data를 로직을 따라 처리한다. 그리고 Json이라는 형식으로 다시 브라우저에게 Data를 전송한다.</p>
<p>브라우저는 처리 결과를 받고 event를 발생시켜 비동기적으로 html 파일의 일부를 변경시킨다.</p>
<p>이 때 Json 이라는 건 무엇일까?
데이터를 전달할 목적으로 사용하는 하나의 &#39;형식&#39; 중 하나이며
중괄호 ({})같은 형식으로 하고, 값을 &#39;,&#39;로 나열하는 포맷이다.
<br></p>
<p>실습으로 들어가보자!</p>
<hr>
<h2 id="🤗-실습">🤗 실습</h2>
<h3 id="❗-연관-관계-매핑">❗ 연관 관계 매핑</h3>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/d9454fd3-a221-4bc6-9d6e-da032e0fd400/image.png" alt="">
모임-좋아요, 회원-좋아요 테이블 관계는 모두 다대일 양방향 매핑으로 설정했다.</p>
<p>*JPA에서는 양쪽에서 단방향 매핑하는 방법으로 양방향 매핑을 구현한다.</p>
<h3 id="❗-좋아요-개수-출력하기동기-방식">❗ 좋아요 개수 출력하기(동기 방식)</h3>
<p>게시글 &quot;<code>상세 페이지</code>&quot;에 들어갔을 때 현재 좋아요 개수가 출력(응답)되어야 한다.
<img src="https://velog.velcdn.com/images/jsang_log/post/1cf2be20-d758-40f6-a53b-6f14bff75ced/image.png" alt=""></p>
<p>흐름도는 위 사진과 같다.
<br>
<em><strong>CrewViewController</strong></em></p>
<pre><code class="language-java">// 크루 게시물 상세 페이지
    @GetMapping(&quot;/view/v1/crews/{crewId}&quot;)
    public String detailCrew(@PathVariable Long crewId, Model model, @ModelAttribute(&quot;sportRequest&quot;) CrewSportRequest crewSportRequest,
                             @PageableDefault(page = 0, size = 1, sort = &quot;lastModifiedAt&quot;, direction = Sort.Direction.DESC) Pageable pageable, Authentication authentication) {
            ...

        try {
            // 좋아요
            int count = likeViewService.getLikeCrew(crewId);
            model.addAttribute(&quot;likeCnt&quot;, count);

            // 상세게시글 정보
            CrewDetailResponse details = list.getContent().get(0);
            model.addAttribute(&quot;details&quot;, details);
            ...
            }

        } catch (Exception e) {
            return &quot;redirect:/index&quot;;
        }

        return &quot;crew/read-crew&quot;;
    }</code></pre>
<p>crewId를 파라메터로 넘긴다. 
<br></p>
<p><em><strong>LikeViewService</strong></em></p>
<pre><code class="language-java">public int getLikeCrew(Long crewId){
        Crew crew = crewRepository.findById(crewId).orElseThrow(()-&gt;new AppException(ErrorCode.CREW_NOT_FOUND,ErrorCode.CREW_NOT_FOUND.getMessage()));
        List&lt;Like&gt; num = likeRepository.findByCrew(crew);
        return num.size();
    }</code></pre>
<p>해당 crew가 가진 like를 list로 받고, list.size()를 출력</p>
<p><em><strong>LikeRepository</strong></em></p>
<pre><code class="language-java">public interface LikeRepository extends JpaRepository&lt;Like, Long&gt; {
    List&lt;Like&gt; findByCrew(Crew crew);
}</code></pre>
<br>

<p><em><strong>read-crew.html</strong></em></p>
<pre><code class="language-html">&lt;div id=&quot;likeCnt&quot; th:text=&quot;${likeCnt}&quot;&gt;&lt;/div&gt; </code></pre>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/896c6ffc-a017-4b1b-a645-6ca7c8b90723/image.png" alt="">
현재 crew가 가진 좋아요 개수를 출력한다. 
<br></p>
<h3 id="❗-좋아요-누르기⭐비동기-방식">❗ 좋아요 누르기(⭐비동기 방식)</h3>
<p> &quot;👍&quot; 버튼을 눌렀을 때 Ajax를 이용하여 API를 호출한 후
 그 결과로 비동기 방식을 통해 좋아요 개수가 변경되어야 한다.</p>
<p>아래는 좋아요 버튼까지 포함한 html코드
<em><strong>read-crew.html</strong></em></p>
<pre><code class="language-html">&lt;div align=&quot;center&quot;&gt;
  &lt;form class=&quot;container&quot;&gt;
    &lt;button type=&quot;button&quot; class=&quot;btn btn-light btn-lg&quot; id=&quot;like-btn&quot; onclick=&quot;likeBtn(crewId)&quot;&gt;👍
    &lt;/button&gt;
    &lt;tr&gt;&lt;div id=&quot;likeCnt&quot; th:text=&quot;${likeCnt}&quot;&gt;&lt;/div&gt; &lt;/tr&gt;
  &lt;/form&gt;
&lt;/div&gt;</code></pre>
<p><em><strong>ajax</strong></em></p>
<pre><code class="language-javascript">function likeBtn(crewId){
        $.ajax({
            type : &quot;POST&quot;,
            url: &#39;/view/v1/crews/&#39;+crewId+&#39;/like&#39;,
            success: function(data) {
                console.log(data);                 // likeResponse값
                if(data.likeCheck == 1){         // likeResponse.likeCheck로 값에 접근
                    alert(&quot;좋아요를 눌렀습니다💕&quot;)
                    $(&quot;#likeCnt&quot;).empty();
                    $(&quot;#likeCnt&quot;).append(data.count);
                }
                else if (data.likeCheck == 0){
                    alert(&quot;좋아요를 취소했습니다✔&quot;)
                    $(&quot;#likeCnt&quot;).empty();
                    $(&quot;#likeCnt&quot;).append(data.count);

                }
            },
            error: function (request, status, error) {
                alert(&quot;로그인 후 좋아요가 가능합니다.&quot;)
            }
        }) ;
    }</code></pre>
<p>url이 호출된 후 흐름도는 다음과 같다
<img src="https://velog.velcdn.com/images/jsang_log/post/ca1817ed-08a1-411c-998c-9b5c99519eb8/image.png" alt="">
<em><strong>CrewViewController</strong></em></p>
<pre><code class="language-java">// 크루 좋아요 누르기
    @PostMapping(&quot;/view/v1/crews/{crewId}/like&quot;)
    public ResponseEntity likeCrew(@PathVariable Long crewId, Authentication authentication) {
        LikeViewResponse likeViewResponse = likeViewService.pressLike(crewId, authentication.getName());
        return new ResponseEntity&lt;&gt;(likeViewResponse, HttpStatus.OK);
    }</code></pre>
<p><em><strong>LikeResponse</strong></em></p>
<pre><code class="language-java">@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class LikeViewResponse {
    private int likeCheck;    // like를 눌렀는지 확인하기 위한 필드
    private int count;          // 현재 likeCnt
    private String userName;  // 좋아요 버튼을 누른 회원의 이름
}</code></pre>
<p><em><strong>LikeService</strong></em>
crewId와 userName을 파라메터로 넘겨받고, 해당 crew에 현재 로그인 되어있는 회원이 좋아요를 눌렀는지를 확인해본다.</p>
<pre><code class="language-java">@Transactional
    public LikeViewResponse pressLike(Long crewId, String userName){
        User user = userRepository.findByUserName(userName).orElseThrow(()-&gt;new AppException(ErrorCode.USERID_NOT_FOUND,ErrorCode.USERID_NOT_FOUND.getMessage()));
        Crew crew = crewRepository.findById(crewId).orElseThrow(()-&gt;new AppException(ErrorCode.CREW_NOT_FOUND,ErrorCode.CREW_NOT_FOUND.getMessage()));
        LikeViewResponse likeViewResponse = new LikeViewResponse();
        // 이미 좋아요한 상태라면 -&gt; 좋아요 취소
        if(user.getLikes().stream().anyMatch(like -&gt; like.getCrew().equals(crew))){
            likeRepository.deleteByUserAndCrew(user,crew);
            likeViewResponse.setLikeCheck(0); 

        } else {
            likeRepository.save(Like.builder().crew(crew).user(user).build());
            likeViewResponse.setLikeCheck(1);
                     ...
                }
            }
        }

        List&lt;Like&gt; num = likeRepository.findByCrew(crew);

        likeViewResponse.setCount(num.size());
        likeViewResponse.setUserName(user.getUsername());
        return likeViewResponse;
    }</code></pre>
<p><em><strong>LikeRepository</strong></em></p>
<pre><code class="language-java">public interface LikeRepository extends JpaRepository&lt;Like, Long&gt; {
    void deleteByUserAndCrew(User user, Crew crew);</code></pre>
<br>


<h3 id="✔️-결과-확인">✔️ 결과 확인</h3>
<p>좋아요를 처음 눌렀을 때 출력되는 json</p>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/53a59d40-ae5f-4d00-8178-b0ef9e55b601/image.png" alt=""></p>
<p>좋아요를 다시 눌러서 취소할 경우 json
<img src="https://velog.velcdn.com/images/jsang_log/post/86517db7-5550-42b9-ae48-2b7dad76b3e0/image.png" alt=""></p>
<ul>
<li>좋아요 버튼을 누르면 새로고침 없이 likeCnt의 값을 likeResponse의 count 값으로 변경하여 출력(비동기 방식)
<img src="https://velog.velcdn.com/images/jsang_log/post/1dbe4c1a-b809-4e2f-b79d-ed7d03162083/image.png" alt=""></li>
</ul>
<br>



<h3 id="참고-문헌">참고 문헌</h3>
<p><a href="https://devlogofchris.tistory.com/14">https://devlogofchris.tistory.com/14</a>
<a href="https://lshdv.tistory.com/73">https://lshdv.tistory.com/73</a>
<a href="https://chaelin1211.github.io/study/2021/04/14/thymeleaf-ajax.html">https://chaelin1211.github.io/study/2021/04/14/thymeleaf-ajax.html</a></p>
<p>자세한 코드는 깃허브에서 올려놨으니 참고해주세요
<a href="https://github.com/sanghee2359/WorkoutMate">https://github.com/sanghee2359/WorkoutMate</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS Ec2 스팟 인스턴스설정 + MySql 도커 띄우기(Xshell)]]></title>
            <link>https://velog.io/@jsang_log/AWS-Ec2-%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jsang_log/AWS-Ec2-%EC%8A%A4%ED%8C%9F-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 22 Feb 2023 15:59:25 GMT</pubDate>
            <description><![CDATA[<p>이번 포스트에서 </p>
<ul>
<li>AWS EC2 인스턴스를 생성 </li>
<li>Xshell ssh 접속 프로그램으로 우분투 서버 접속</li>
<li>docker 설치</li>
<li>docker 컨테이너에 mysql 띄우기까지 과정을 정리할 것입니다.</li>
</ul>
<p>📍 스팟 인스턴스 설정 이유
: 스팟으로 설정된 인스턴스는 서버가 갑자기 종료될 수 있다는 단점이 존재하지만, 가격이 t3.small기준 거의 1/6로 줄어들므로 가성비가 좋습니다!  (약 3만원 -&gt; 약 5천원)</p>
<h2 id="1-aws-인스턴스-생성">1. AWS 인스턴스 생성</h2>
<p><strong>1. aws에서 EC2를 검색하고 들어가면 인스턴스를 생성하는 버튼을 볼 수 있습니다.</strong>
<img src="https://velog.velcdn.com/images/jsang_log/post/da72363c-1461-404a-85fe-0a3b3f89e561/image.png" alt="">
<strong>2. 인스턴스 생성하며 정보를 설정하는 화면입니다.</strong></p>
<ul>
<li>설정한 부분은 모두 박스로 표시</li>
<li>인스턴스 유형과 이름 설정 부분은 원하는대로 설정 해주세요👨‍🔧
<img src="https://velog.velcdn.com/images/jsang_log/post/02b36d50-e73b-454a-ab57-b1755e093b8b/image.png" alt=""></li>
</ul>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/9eeccfda-970b-4cef-b78a-2c8887d48270/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/0c428639-c277-4182-b297-30910edb81bc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/2fa37f90-094a-4e9e-93a7-458d694e4f5b/image.png" alt="">
마지막으로 고급 세부 정보에서 스팟 인스턴스 요청을 설정합니다
<img src="https://velog.velcdn.com/images/jsang_log/post/6ed10cdf-0c21-464a-999f-ba98dfaa8052/image.png" alt=""></p>
<p>인스턴스의 ID를 클릭하면 더 많은 세부정보를 볼 수 있습니다
<img src="https://velog.velcdn.com/images/jsang_log/post/3dbda0be-0a8b-44b5-931e-8aa6b3edee02/image.png" alt="">
<br></p>
<h2 id="2-xshell-연결">2. Xshell 연결</h2>
<p><strong>1. xshell 설치 후 파일 새로 만들기</strong>
<img src="https://velog.velcdn.com/images/jsang_log/post/4da4cfc7-7e22-4b1e-9c4e-1d6474cad989/image.png" alt="">
<strong>2. 인스턴스 ID를 클릭해서 들어가고 퍼블릭 DNS 주소를 복사</strong>
<img src="https://velog.velcdn.com/images/jsang_log/post/457403bd-c2a6-4539-a5a8-99404542640f/image.png" alt="">
<strong>3. xshell 세션 등록 정보 중 호스트 칸에 붙여 넣어줍니다.</strong>
<img src="https://velog.velcdn.com/images/jsang_log/post/5f84070b-52d0-4cb6-a6bb-e78752cca055/image.png" alt="">
<strong>4. 저장한 다음 새로 생성한 세션을 더블 클릭하면 아래의 화면이 띄워집니다</strong>
<img src="https://velog.velcdn.com/images/jsang_log/post/3a83111b-85cc-4cca-8571-33d0aed5c0d3/image.png" alt="">
수락 및 저장을 누른 후 ec2의 인스턴스 이름을 적어주었습니다.
<img src="https://velog.velcdn.com/images/jsang_log/post/df539716-409d-446d-862f-1ffdecc304ff/image.png" alt="">
<strong>5. ec2를 생성하며 다운받았던 키페어를 찾아옵니다.</strong>
찾아보기 &gt; 사용자 키 &gt; 가져오기 
<img src="https://velog.velcdn.com/images/jsang_log/post/bb3b3284-822b-4964-9dfe-e82e56011537/image.png" alt="">
<code>키 선택</code> -&gt; <code>확인</code>
<img src="https://velog.velcdn.com/images/jsang_log/post/cd3e7046-0965-4c36-9747-11c3eabfc59b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/93696055-dea4-4c59-92d7-003f5a7fa395/image.png" alt="">
우분투 서버 접속이 확인되었습니다.
<br></p>
<h2 id="3-docker-설치">3. docker 설치</h2>
<p><a href="https://github.com/Kyeongrok/docker_minikube_kubectl_install">https://github.com/Kyeongrok/docker_minikube_kubectl_install</a>
<strong>1. 위 깃허브에 접속해서 주소를 복사합니다.</strong>
<img src="https://velog.velcdn.com/images/jsang_log/post/3a704f7d-b247-4d0b-8e10-4b2167022bee/image.png" alt="">
<strong>2. xshell에 다음 명령어를 차례로 입력합니다.</strong></p>
<pre><code>sudo su -    // root 계정 변경
mkdir git    // &#39;git&#39; 이라는 폴더 생성
cd git        // &#39;git&#39; 폴더로 이동
git clone https://github.com/Kyeongrok/docker_minikube_kubectl_install </code></pre><p>git clone + (복사된 깃허브 주소) 
명령어를 통해 깃허브의 Repository에 있는 파일을 내 로컬 컴퓨터로 복사했습니다.</p>
<pre><code>cd docker_minikube_kubectl_install // 파일 이동
sh docker_install.sh                // docker 설치 스크립트 실행
docker -v                           // docker 버전 확인</code></pre><p><img src="https://velog.velcdn.com/images/jsang_log/post/408e9da3-af01-4a8e-8582-713808537ba2/image.png" alt="">
버전이 보인다면 성공적으로 도커설치가 완료된 것입니다.
<br></p>
<h3 id="3_1-docker-컨테이너에-mysql-띄우기">3_1. docker 컨테이너에 MySql 띄우기</h3>
<p><strong>1. docker 컨테이너 실행</strong></p>
<pre><code>docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=(비밀번호) -d mysql</code></pre><p>도커 옵션 설명</p>
<ul>
<li>-e <code>environment</code> : 컨테이너 내부의 환경변수를 설정</li>
<li>-d <code>Detached</code> : 컨테이너를 백그라운드에서 동작하는 어플리케이션으로써 실행</li>
<li>-p <code>port</code> : 포트 포워딩 설정 (<code>docker 호스트의 포트번호</code> : <code>컨테이너 내에서 사용되는 포트번호</code>)
위 설정은 호스트 시스템의 3306번 포트로 유입되는 트래픽을 모두 도커 컨테이너의 3306번 포트로 전달해줌으로써 컨테이너 외부와 내부를 연결해줍니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/33dcdc4b-5f66-42c0-828e-3424f397c42d/image.png" alt="">
mysql docker image가 컨테이너 위에 잘 떠있는 것을 확인할 수 있습니다.
<br>
<strong>2. AWS 보안그룹 인바운드/아웃바인드 설정</strong></p>
<p>AWS의 보안그룹에 올라가지 않은 포트는 방화벽에서 필터링되듯이 걸러지기 때문에 
반드시 사용할 포트를 AWS 보안그룹에 추가해주어야 접근 가능합니다.
<img src="https://velog.velcdn.com/images/jsang_log/post/ac9a46cc-cf4f-434d-8c45-afe96358b8ae/image.png" alt="">
<img src="https://velog.velcdn.com/images/jsang_log/post/de048d3f-9387-42f5-910b-fa6caf516c08/image.png" alt="">
<code>인바운드 규칙 편집</code> -&gt; <code>규칙 추가</code>
<img src="https://velog.velcdn.com/images/jsang_log/post/001cfe26-e0a2-4360-b5e4-1432a740148d/image.png" alt="">
<code>아웃바인드 규칙 편집</code> -&gt; <code>규칙 추가</code>
<img src="https://velog.velcdn.com/images/jsang_log/post/e59b2e78-61f4-4a30-a93e-81df11554c74/image.png" alt=""></p>
<h3 id="3_2-mysql-workbench-연결">3_2. MySql Workbench 연결</h3>
<ol>
<li><p>mysql workbench를 설치한 후 </p>
</li>
<li><p>Hostname에 ec2 퍼블릭 DNS 주소를 넣어줍니다. 
<img src="https://velog.velcdn.com/images/jsang_log/post/6d7397fb-a137-4e35-874d-11cd5d32cf6c/image.png" alt=""></p>
</li>
<li><p>xshell에서 docker MySql을 띄울 때 설정했던 비밀번호를 입력해줍니다.</p>
<pre><code> docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=(비밀번호) -d mysql</code></pre></li>
</ol>
<p>mysql이 성공적으로 연결되었다면 성공적으로 workbench editor이 열립니다.
<img src="https://velog.velcdn.com/images/jsang_log/post/103d29af-df1c-47c4-ad4c-73bc82215325/image.png" alt=""></p>
<h3 id="에러-로그">에러 로그</h3>
<p>1_ 로그인이 안될 때 확인해 볼 것</p>
<ul>
<li>ec2의 인스턴스 생성할 때 AMI(Amazon Machine Image)가 ubuntu로 설정했었는지</li>
<li>private key(.pem)를 맞게 설정했는지</li>
<li>Xshell에서 로그인 할 때 로그인 할 사용자 이름을 <code>ubuntu</code>로 설정했는지
<img src="https://velog.velcdn.com/images/jsang_log/post/16738f39-648c-47fc-9ed6-931319cf4cba/image.png" alt="">
<img src="https://velog.velcdn.com/images/jsang_log/post/2c996506-be48-4f04-a382-e9f51e973269/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Security Filter 예외처리하기 - JWT]]></title>
            <link>https://velog.io/@jsang_log/Security-Filter-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-JWT</link>
            <guid>https://velog.io/@jsang_log/Security-Filter-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-JWT</guid>
            <pubDate>Mon, 16 Jan 2023 17:57:35 GMT</pubDate>
            <description><![CDATA[<p>Spring Security에서 토큰 기반 인증 중 예외가 발생한다면 어떤 일이 일어나는지, 어떻게 핸들링 해야하는지에 대해 알아보자.</p>
<h3 id="이전에-알아야-할-지식">이전에 알아야 할 지식</h3>
<blockquote>
<ul>
<li><p>토큰 인증 방식</p>
<p>인증받은 사용자에게 토큰을 발급해주고,</p>
<p>서버에 요청을 할 때 HTTP 헤더에 토큰을 함께 보내 인증받은 사용자(유효성 검사)인지 확인한다.</p>
</li>
</ul>
</blockquote>
<blockquote>
<ul>
<li><p>Spring boot 예외처리 방식</p>
<ul>
<li><p><code>@ControllerAdvice</code>와 <code>@RestControllerAdvice</code>를 이용해서 컴포넌트를 생성하고 예외처리 메서드를 작성해놓으면 모든 클래스에 전역적으로 적용이 가능하다.</p>
</li>
<li><p><code>@ExceptionHandler</code>을 통해 특정 컨트롤러의 예외를 처리한다.</p>
</li>
</ul>
</li>
</ul>
</blockquote>
<br>
<br>


<h3 id="q-spring-security에서-토큰을-검증할-경우-예외가-발생한다면-기존에-사용-중이던-custom-exception으로-처리가-될까">Q. Spring Security에서 토큰을 검증할 경우, 예외가 발생한다면 기존에 사용 중이던 Custom Exception으로 처리가 될까?</h3>
<p>A. 가능하다면 좋겠지만 불가능하다!</p>
<blockquote>
<p> spring security와 spring boot 예외 처리구간이 다르다고 생각해보면 간단하다.</p>
<p> <code>Filter</code>는 <code>Dispatcher Servlet</code> 보다 앞단에 존재하며 <code>Handler Intercepter</code>는 뒷단에 존재하기 때문에 <code>Filter</code> 에서 보낸 예외는 <code>Exception Handler</code>로 처리를 못한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jsang_log/post/89fe56e4-0c3a-45ea-a226-2d1c411bf433/image.png" alt=""></p>
<p>따라서, 토큰 예외처리를 위해선 새로운 Filter를 정의해서 Filter Chain에 추가해줘야 한다.</p>
<br>

<h3 id="1-securityconfig-클래스-수정">1. SecurityConfig 클래스 수정</h3>
<pre><code class="language-java">public class SecurityConfig  {
    private final UserService userService;
    @Value(&quot;${jwt.token.secret}&quot;)
    private String secretKey;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers(&quot;/api/v1/users/join&quot;, &quot;/api/v1/users/login&quot;).permitAll()
                .antMatchers( &quot;/api/v1/users/list&quot;,&quot;/api/v1/users/{userId}/role/change&quot;).hasAnyRole(&quot;ADMIN&quot;)
                .antMatchers(HttpMethod.GET,&quot;/api/v1/posts/my&quot;, &quot;/api/v1/alarms&quot;).authenticated()
                .antMatchers(HttpMethod.POST, &quot;/api/v1/**&quot;).authenticated()
                .antMatchers(HttpMethod.PUT, &quot;/api/v1/**&quot;).authenticated()
                .antMatchers(HttpMethod.DELETE, &quot;/api/v1/**&quot;).authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPointHandler())
                .accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(new JwtTokenFilter(jwtProvider, redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .build();

    }

</code></pre>
<br>

<p><strong>addFilterBefore(Filter, beforeFilter)</strong></p>
<blockquote>
<p><strong>beforeFilter</strong>가 실행되기 이전에 <strong>Filter</strong>을 먼저 실행시키도록 설정하는 메소드이다.</p>
<pre><code class="language-java">.addFilterBefore(new JwtTokenFilter(jwtProvider, redisTemplate), UsernamePasswordAuthenticationFilter.class)</code></pre>
</blockquote>
<blockquote>
<p>그 외 추가한 메소드 설명</p>
<pre><code class="language-java">.exceptionHandling()
 // 인증 과정에서 예외가 발생할 경우 예외를 전달한다.
             .authenticationEntryPoint(new CustomAuthenticationEntryPointHandler())
 // 권한을 확인하는 과정에서 통과하지 못하는 예외가 발생하는 경우 예외를 전달한다.
             .accessDeniedHandler(new CustomAccessDeniedHandler())</code></pre>
</blockquote>
<p>메소드를 살펴보면 <code>인가 과정의 예외 상황</code>에서 CustomAccessDeniedHandler와 CustomAuthenticationEntryPointHandler 로 예외를 전달하고 있었다.</p>
<p>다음은 이러한 클래스를 작성하는 방법이다.</p>
<br>


<h3 id="2--customaccessdeniedhandler클래스-생성">2.  CustomAccessDeniedHandler클래스 생성</h3>
<pre><code class="language-java">@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        // 4. 토큰 인증 후 권한 거부
        ErrorCode errorCode = ErrorCode.FORBIDDEN_REQUEST;
        JwtTokenFilter.setErrorResponse(response, errorCode);
    }
}</code></pre>
<br>

<p><strong>AccessDeniedHandler</strong></p>
<blockquote>
<p> 액세스 권한이 없는 리소스에 접근할 경우 발생하는 예외</p>
<p> handle() 메소드를 오버라이딩한다.</p>
</blockquote>
<br>


<h3 id="3-customauthenticationentrypointhandler-클래스-생성">3. CustomAuthenticationEntryPointHandler 클래스 생성</h3>
<pre><code class="language-java">@Slf4j
@Component
public class CustomAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

        // 1. 토큰 없음 2. 시그니처 불일치
        if (authorization == null || !authorization.startsWith(&quot;Bearer &quot;)) {
            log.error(&quot;토큰이 존재하지 않거나 Bearer로 시작하지 않는 경우&quot;);
            ErrorCode errorCode = ErrorCode.INVALID_TOKEN;
            JwtTokenFilter.setErrorResponse(response, errorCode);
        } else if (authorization.equals(ErrorCode.EXPIRED_TOKEN)) {
            log.error(&quot;토큰이 만료된 경우&quot;);

        // 3. 토큰 만료
            ErrorCode errorCode = ErrorCode.EXPIRED_TOKEN;
            JwtTokenFilter.setErrorResponse(response,errorCode);
        }
    }
}
</code></pre>
<br>

<p><strong>AuthenticationEntryPoint</strong></p>
<blockquote>
<p>인증이 실패한 상황을 처리한다.</p>
<p>commence() 메서드를 오버라이딩해서 코드를 구현한다.</p>
</blockquote>
<br>

<h3 id="4-예외처리">4. 예외처리</h3>
<p>에러코드는 enum으로 관리한다</p>
<pre><code class="language-java">@AllArgsConstructor
@Getter
public enum ErrorCode {

    INVALID_TOKEN(HttpStatus.UNAUTHORIZED, &quot;잘못된 토큰입니다.&quot;),
    EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, &quot;만료된 토큰입니다.&quot;),
    INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, &quot;사용자가 권한이 없습니다.&quot;),
    FORBIDDEN_REQUEST(HttpStatus.FORBIDDEN, &quot;ADMIN 회원만 접근할 수 있습니다.&quot;);

    private final HttpStatus httpStatus;
    private final String message;
}</code></pre>
<p>JwtTokenFilter에 메소드를 추가로 작성해서 가독성을 높였다.</p>
<pre><code class="language-java">      /**
     * Security Chain 에서 발생하는 에러 응답 구성
     */
    public static void setErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException {
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        response.setStatus(errorCode.getHttpStatus().value());
        ObjectMapper objectMapper = new ObjectMapper();

        ErrorResponse errorResponse = new ErrorResponse
                (errorCode, errorCode.getMessage());

        Response&lt;ErrorResponse&gt; error = Response.error(errorResponse);
        String s = objectMapper.writeValueAsString(error);

        /**
         * 한글 출력을 위해 getWriter() 사용
         */
        response.getWriter().write(s);
    }
</code></pre>
<p>JwtTokenFilter의 전체 코드를 추가겠습니다</p>
<pre><code class="language-java">
@RequiredArgsConstructor
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {
    /**
     * request 에서 전달받은 Jwt 토큰을 확인
     */

    private final String BEARER = &quot;Bearer &quot;;
    private final JwtProvider jwtProvider;
    private final RedisTemplate redisTemplate;



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

        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info(&quot;First Authorization = {}&quot;, token);
        request.setAttribute(&quot;existsToken&quot;, true); // 토큰 존재 여부 초기화
        if (isEmptyToken(token)) request.setAttribute(&quot;existsToken&quot;, false); // 토큰이 없는 경우 false로 변경

        //쿠키 값 셋팅
        if (token == null) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null) for (Cookie cookie : cookies) if(cookie.getName().equals(&quot;jwt&quot;)) token = cookie.getValue().replace(&quot;+&quot;, &quot; &quot;);
            else {
                HttpSession session = request.getSession(false);
                if (session == null || session.getAttribute(&quot;Authorization&quot;) == null) {
                } else {
                    token = session.getAttribute(&quot;Authorization&quot;).toString();
                }
            }
        }
        // 쿠키 조회했는데도 null이거나 &#39;Bearer &#39; 로 시작하지 않으면 에러
        if (token == null || !token.startsWith(BEARER)) {
            log.info(&quot;Error At nullCheck Authorization = {}&quot;, token);
            filterChain.doFilter(request, response);
            return;
        }

        token = parseBearer(token);
        log.info(&quot;After remove Bearer Authorization = {}&quot;, token);

        if (jwtProvider.validateToken(token)) {
            // Redis 에 해당 accessToken logout 여부 확인
            String isLogout = (String)redisTemplate.opsForValue().get(token);
            log.info(&quot;isLogout?:{}&quot;,isLogout);

            if (ObjectUtils.isEmpty(isLogout)) {
                // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
                Authentication authentication = jwtProvider.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);

            } else if(isLogout.equals(&quot;logout&quot;)) {
                MakeError(response,ErrorCode.INVALID_PERMISSION);
                filterChain.doFilter(request, response);
                return;
            }
        }
        log.info(&quot;finish add Authorization to Security ContextHolder= {}&quot;, token);
        filterChain.doFilter(request, response);
    }

    private boolean isEmptyToken(String token) {
        return token == null || &quot;&quot;.equals(token);
    }

    private String parseBearer(String token) {
        return token.substring(BEARER.length());
    }

    /**
     * Security Chain 에서 발생하는 에러 응답 구성
     */
    public static void setErrorResponse(HttpServletResponse response , ErrorCode errorCode) throws IOException {
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        response.setStatus(errorCode.getHttpStatus().value());
        ObjectMapper objectMapper = new ObjectMapper();

        ErrorResponse errorResponse = new ErrorResponse
                (errorCode, errorCode.getMessage());
        Response&lt;ErrorResponse&gt; resultResponse = Response.error(errorResponse);

        // 한글 출력을 위해 getWriter()
        response.getWriter().write(objectMapper.writeValueAsString(resultResponse));
    }
}</code></pre>
<p>필터 클래스를 직접 만들어 사용할 때 만약 filterChain.doFilter() 메소드를 호출해주지 않으면 다음 필터로 넘어가지도 않을 뿐더러 서블릿으로 요청이 가지도 못한다는 것에 주의해야 한다.</p>
<blockquote>
<ul>
<li><code>JwtTokenFilter</code>는 JWT토큰으로 인증하고 SecurityContextHolder에 추가하는 필터를 설정하는 클래스.<ul>
<li>HttpServletRequest에서 토큰 추출</li>
<li>토큰에 대한 유효성을 검사 <code>&gt;</code> 유효하다면 Authentication 객체를 생성해서 SecurityContextHolder에 추가
<img src="https://velog.velcdn.com/images/jsang_log/post/7e7c23d2-72d0-4a18-a7a7-c52562518207/image.png" alt="인증된 사용자 점보를 관리"></li>
</ul>
</li>
</ul>
</blockquote>
<p>필터 수행 순서 : <code>JwtTokenFilter</code> &gt; <code>UsernamePasswordAuthenticationFilter</code> =&gt; Dispatcher Servlet
<br>
<br></p>
<h2 id="마무리">마무리</h2>
<p>시큐리티를 처음 설정할 땐 낯설게 느껴지지만 핵심적인 클래스와 메서드를 짚어보면 큰 그림이 그려진다.</p>
<p>어려운 내용을 만났을 때 잘 모르고 다음으로 넘어가는 것보다 이렇게 하나씩 정리해두면</p>
<p>두고두고 이용해먹을 수 있겠다.</p>
<p><br><br><br></p>
<h4 id="참고문헌">[참고문헌]</h4>
<p><a href="https://inkyu-yoon.github.io/docs/Language/SpringBoot/FilterExceptionHandle">Security Filter에서 발생하는 Exception 처리하기</a></p>
<p><a href="https://velog.io/@hellonayeon/spring-boot-jwt-expire-exception">[Spring Boot] JWT 토큰 만료에 대한 예외처리</a></p>
<p> <a href="http://www.yes24.com/Product/Goods/110142898">스프링부트핵심가이드</a>(397~403)
 <a href="https://junjunrecord.tistory.com/94">[Spring] 스프링 Filter, DoFilter</a>
 <a href="https://codevang.tistory.com/275">스프링 필터와 스프링 시큐리티(Spring Security)의 동작 구조</a></p>
]]></description>
        </item>
    </channel>
</rss>