<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>2_minus.log</title>
        <link>https://velog.io/</link>
        <description>둘뺌</description>
        <lastBuildDate>Sat, 06 Jul 2024 12:41:48 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>2_minus.log</title>
            <url>https://velog.velcdn.com/images/2_minus/profile/31e13d58-0e0c-481a-9c61-43c17e35a555/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 2_minus.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/2_minus" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[혼공학습단] 1주차]]></title>
            <link>https://velog.io/@2_minus/%ED%98%BC%EA%B3%B5%ED%95%99%EC%8A%B5%EB%8B%A8-1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@2_minus/%ED%98%BC%EA%B3%B5%ED%95%99%EC%8A%B5%EB%8B%A8-1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sat, 06 Jul 2024 12:41:48 GMT</pubDate>
            <description><![CDATA[<h2 id="❗기본숙제">❗기본숙제</h2>
<ul>
<li>p.51 확인 문제 3번<blockquote>
<p>프로그램이 실행되려면 반드시 <code>[       ]</code>에 저장되어 있어야 합니다.</p>
</blockquote>
</li>
</ul>
<p>프로그램이 실행되려면 <strong>메모리</strong>에 저장되어 있어야합니다.</p>
<ul>
<li><p>p.65 확인 문제 3번</p>
<blockquote>
<p>$1101_{(2)}$의 음수를 2의 보수 표현법으로 구해 보세요.</p>
</blockquote>
<p><code>1101</code> -&gt; 0과 1 뒤집기
<code>0010</code> -&gt; 1 더하기
$1101_{(2)}$의 음수는 $0011_{(2)}$ 입니다.</p>
<h2 id="✨-추가숙제">✨ 추가숙제</h2>
<ul>
<li>p.100 스택과 큐의 개념을 정리하기</li>
</ul>
<h3 id="스택stack">스택(Stack)</h3>
<p><strong>후입선출(LIFO : Last-In First-Out)</strong></p>
<ul>
<li>가장 최근에 들어온 데이터가 가장 먼저 나감
<img src="https://velog.velcdn.com/images/2_minus/post/79e8d0f9-5080-4fab-b9f3-396ecf756574/image.png" alt=""></li>
</ul>
</li>
<li><p>스택의 용도</p>
<ul>
<li>되돌리기</li>
<li>함수호출</li>
<li>괄호검사</li>
<li>계산기 : 후위표기식 계산, 중위 표기식의 후위 표기식 변환</li>
<li>미로탐색</li>
</ul>
</li>
</ul>
<h3 id="큐queue">큐(Queue)</h3>
<p><strong>선입선출(FIFO : First-In First-Out)</strong></p>
<ul>
<li>가장 먼저 들어온 데이터가 가장 먼저 나감
<img src="https://velog.velcdn.com/images/2_minus/post/e70e06bc-71a4-4ff5-9517-7bfd2b59c651/image.png" alt=""></li>
<li>큐의 용도<ul>
<li>컴퓨터의 작업 큐 : 버퍼링</li>
<li>시뮬레이션의 대기열</li>
<li>통신에서의 데이터 패킷들의 모델링에 이용</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[github 연동 오류 : github 계정의 연결 불안정, 계정 접근 제한]]></title>
            <link>https://velog.io/@2_minus/github-%EC%97%B0%EB%8F%99-%EC%98%A4%EB%A5%98-github-%EA%B3%84%EC%A0%95%EC%9D%98-%EC%97%B0%EA%B2%B0-%EB%B6%88%EC%95%88%EC%A0%95-%EA%B3%84%EC%A0%95-%EC%A0%91%EA%B7%BC-%EC%A0%9C%ED%95%9C</link>
            <guid>https://velog.io/@2_minus/github-%EC%97%B0%EB%8F%99-%EC%98%A4%EB%A5%98-github-%EA%B3%84%EC%A0%95%EC%9D%98-%EC%97%B0%EA%B2%B0-%EB%B6%88%EC%95%88%EC%A0%95-%EA%B3%84%EC%A0%95-%EC%A0%91%EA%B7%BC-%EC%A0%9C%ED%95%9C</guid>
            <pubDate>Thu, 20 Jun 2024 14:23:20 GMT</pubDate>
            <description><![CDATA[<p>nteliJ 에서 push를 해야 할 때 마다 github 로그인 을 요구하고, 연결 후에는 권한이 없어 거부되는
<code>remote: Permission to {project_URL} denied to {Github_username}. unable to access  {project_URL}: The requested URL returned error: 403</code> 메세지가 반복되어 push가 불가능한 상황.</p>
<p>git bash: git remote 프로젝트에 재연결시키기로는 해결이 되지 않아 github 계정을 연동하는 방법을 다르게 했다.
일반적인 웹에서 로그인하여 연결시키는 것이 아닌 SSH 방식으로 연결.</p>
<p>먼저 gitbash에서 ssh를 발급받기 위한 키를 생성받는다.
<code>$ ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;</code></p>
<p>그 후 콘솔에서 경로 설정, 비밀번호 설정을 거친 뒤 (✨비밀번호는 입력되는 게 보이지 않아야 정상)
ssh키를 발급받은 경로에 가서 이 아이들이 예쁘게 있는지 확인하고</p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/f453c9eb-5da0-4398-9a8e-542ec4f2dcdd/image.png" alt=""></p>
<p><code>.pub</code> 확장자 파일을 메모장 등으로 열어준다.
내부에 보이는 코드를 복사 한 뒤 <code>github &gt; 내 프로필 사진 클릭 &gt; Settings &gt; SSH and GPG keys &gt; New SSH key</code></p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/b6cdbcc7-fe9d-4b50-a542-93d98054cc68/image.png" alt=""></p>
<p><code>key</code>에 붙여넣기를 한 후 <code>Add SSH key</code>를 누르고 이런 <code>key</code>가 생성이 되었다면 성공.</p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/d485c734-edd2-4c8e-917e-40c76d724b51/image.png" alt=""></p>
<p>생성이 되었다면 앞으로 git 원격 연결을 할 때,
<img src="https://velog.velcdn.com/images/2_minus/post/a5557bd4-c730-4f59-8814-172290757e83/image.png" alt=""></p>
<p>이렇게 있어보이는 ssh 탭에서 멋있는 URL을 기존 연결 방식 처럼 사용하면 되겠다.</p>
<p>🫠</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker Container 무한 재시작 현상]]></title>
            <link>https://velog.io/@2_minus/Docker-Container-%EB%AC%B4%ED%95%9C-%EC%9E%AC%EC%8B%9C%EC%9E%91-%ED%98%84%EC%83%81</link>
            <guid>https://velog.io/@2_minus/Docker-Container-%EB%AC%B4%ED%95%9C-%EC%9E%AC%EC%8B%9C%EC%9E%91-%ED%98%84%EC%83%81</guid>
            <pubDate>Wed, 19 Jun 2024 14:32:18 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘의-학습-🌠">오늘의 학습 🌠</h1>
<h1 id="docker-실행시-cmd에-mysql-생성-안-되는-에러-해결">docker 실행시 cmd에 mysql 생성 안 되는 에러 해결</h1>
<hr>
<ul>
<li><p><strong>mysql-data 파일 권한 설정.</strong>  </p>
<ul>
<li><code>sudo chown -R 1000:1000 ./db/mysql/data</code>  </li>
<li>권한 설정은 정상적으로 들어갔지만 여전히 파일을 찾지 못하거나 권한을 부여받지 못함.  = 실패 😭</li>
</ul>
</li>
<li><p><strong>📌 명시적 접근 설정.</strong>  </p>
</li>
<li><p><code>docker-compose.yml</code> 파일 마지막 줄에 추가</p>
</li>
</ul>
<pre><code class="language-java">-&gt; volume:  
mysql-data:</code></pre>
<ul>
<li>제대로 파일을 찾고 권한을 부여 받았지만 타 팀원분들은 이 볼륨 세션을 등록할 경우 접근권한 없음 에러 발생.  = 규성님만 성공 🙄  = 결과적으로 실패😭</li>
</ul>
<br>

<ul>
<li>📌 <strong>파일 중복 문제.</strong>  </li>
</ul>
<p><a  href="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/4447880f-fde2-4bcb-a1fa-be81c1a58362"  class="popup img-link"><img  src="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/4447880f-fde2-4bcb-a1fa-be81c1a58362"  alt="1"  loading="lazy"></a></p>
<ul>
<li><p>서로 다른 두개의 파일이 존재하기에 하위 폴더를 삭제 후 docker 재실행  = 실패 😭</p>
</li>
<li><p>💡 <strong>프로젝트 제거 후 다시 받아오기</strong>  </p>
</li>
<li><ul>
<li><p>프로젝트 파일의 모든 내용을 제거하고 새로 프로젝트 파일을 생성하여 다시 clone.</p>
<ul>
<li>파일이 삭제된 후 다시 다운로드 된 경로를 살펴보니 오류가 존재하던 때의 파일 경로와 다른 것을 발견.<br>정확한 파일 경로로 리빌드 되면서 오류 해결.<br>= 성공 😁</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h1 id="applicationyml-oauth-오류-해결-과정">application.yml OAuth 오류 해결 과정</h1>
<hr>
<p><a  href="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/1557d7e2-31a1-4df1-a084-81437d99346d"  class="popup img-link"><img  src="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/1557d7e2-31a1-4df1-a084-81437d99346d"  alt="1"  loading="lazy"></a></p>
<ul>
<li>해당 사진의 오류로 애플리케이션 구성의 문제임을 확인함.</li>
</ul>
<p><a  href="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/082c8dbe-33ea-45f7-b8d3-a0b370cea396"  class="popup img-link"><img  src="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/082c8dbe-33ea-45f7-b8d3-a0b370cea396"  alt="1"  loading="lazy"></a></p>
<ul>
<li>카카오 provider Id를 찾을 수 없음을 확인</li>
</ul>
<p><a  href="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/c18a93ca-ae8a-4606-a08e-045ae01ac0d5"  class="popup img-link"><img  src="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/c18a93ca-ae8a-4606-a08e-045ae01ac0d5"  alt="1"  loading="lazy"></a></p>
<ul>
<li>code With Me를 통해 문제점 파악</li>
</ul>
<p><a  href="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/7f206911-1fae-4241-8e6c-b7687156e2e1"  class="popup img-link"><img  src="https://github.com/LeeNaYoung240/LeeNaYoung240.github.io/assets/107848521/7f206911-1fae-4241-8e6c-b7687156e2e1"  alt="1"  loading="lazy"></a></p>
<ul>
<li>오래 걸렸지만 원인은 들여쓰기 문제였음😭</li>
</ul>
<p>🐱‍🏍--- ---🤸🏻‍♀️ <del>~ 야</del>호~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트 코드 작성하기]]></title>
            <link>https://velog.io/@2_minus/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@2_minus/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 18 Jun 2024 01:05:49 GMT</pubDate>
            <description><![CDATA[<h2 id="❗필수-구현-기능">❗필수 구현 기능</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> <strong>🆕 AOP 추가하기</strong><ul>
<li>모든 API(Controller)가 호출될 때, Request 정보(Request URL, HTTP Method)를
<strong>@Slf4J Logback</strong> 라이브러리를  활용하여 Log로 출력해주세요.</li>
<li>컨트롤러 마다 로그를 출력하는 코드를 추가하는것이 아닌, AOP로 구현해야만 합니다.<pre><code class="language-java">@Pointcut(&quot;execution(* com.prac.music.domain.board.controller.*.*(..))&quot;)
private void board() {}
</code></pre>
</li>
</ul>
</li>
</ul>
<p>@Pointcut(&quot;execution(* com.prac.music.domain.comment.controller.<em>.</em>(..))&quot;)
private void comment() {}</p>
<p>@Pointcut(&quot;execution(* com.prac.music.domain.like.controller.<em>.</em>(..))&quot;)
private void like() {}</p>
<p>@Pointcut(&quot;execution(* com.prac.music.domain.mail.controller.<em>.</em>(..))&quot;)
private void mail() {}</p>
<p>@Pointcut(&quot;execution(* com.prac.music.domain.user.controller.<em>.</em>(..))&quot;)
private void user() {}</p>
<p>@Around(&quot;board() || comment() || like() || mail() || user()&quot; )
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(joinPoint.getSignature().toShortString() + &quot; start&quot;);</p>
<pre><code>    try {

        return joinPoint.proceed();

    } finally {

        log.info(joinPoint.getSignature().toShortString() + &quot; end&quot;);

    }
}</code></pre><pre><code>![](https://velog.velcdn.com/images/2_minus/post/5d5741fc-ef56-465c-bdf3-c0d4984289d5/image.png)

- api가 실행될때, 끝날때 api의 이름을 출력하는 log 
  - domain 방식으로 설계된 프로젝트여서 각각의 controller를 지정하는 @Pointcut이 많은 것이 아닌가라는 생각이 든다.




- [X]  **🆕 DTO, Entity Test 추가하기**
    - `@Test` 를 사용해서 DTO 와 Entity Test 를 추가합니다.
    - User, Post, Comment, DTO 에 존재하는 메서드들에 대해서 “**단위 테스트”** 를 추가합니다.
    - 특정 상황에 예외가 정상적으로 발생하고 있는지도 테스트 합니다.
```java
@Test
    @DisplayName(&quot;User Entity Test&quot;)
    void test1() throws IOException {
        // given
        SignupRequestDto requestDto = new SignupRequestDto(
                &quot;testId1&quot;,
                &quot;testPassword1!&quot;,
                &quot;testName&quot;,
                &quot;test@email.com&quot;,
                &quot;test Introduce&quot;
        );

        MultipartFile file = null;

        // when
        User user = userService.createUser(requestDto, file);

        // then
        assertEquals(requestDto.getUserId(), user.getUserId());
        assertTrue(passwordEncoder.matches(requestDto.getPassword(), user.getPassword()));
        assertEquals(requestDto.getName(), user.getName());
        assertEquals(requestDto.getEmail(), user.getEmail());
        assertEquals(requestDto.getIntro(), user.getIntro());
    }</code></pre><ul>
<li><p>given 에 입력값, when에 생성 메서드(Post api)를 동작시켜 비교시켜 테스트
<img src="https://velog.velcdn.com/images/2_minus/post/c380f575-7636-4641-90b9-066dbdd929ac/image.png" alt=""></p>
<ul>
<li>게시글과 댓글은 종속된 엔티티가 테스트 코드 내에서 제대로 존재하지 않아서 통과하지 못하는 것으로 보인다.</li>
</ul>
</li>
</ul>
<ul>
<li><input checked="" disabled="" type="checkbox"> <strong>🆕 Controller Test 추가하기</strong><ul>
<li><code>@WebMvcTest</code> 를 사용하여 Controller Test 를 추가합니다.</li>
<li>Post, Comment Controller 에 대해서 테스트를 추가합니다.</li>
<li>특정 상황에 예외가 정상적으로 발생하고 있는지도 테스트 합니다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><img src="https://velog.velcdn.com/images/2_minus/post/26e2c6a3-181d-4b7e-aeb7-d56a3a731c59/image.png" alt="">
🛠️ 오류해결</p>
<ul>
<li>✨ JpaAuditing 관련 어노테이션 부착 후 해결</li>
</ul>
</blockquote>
<pre><code class="language-java">@WebMvcTest(
        controllers = UserController.class,
        excludeFilters = {
                @ComponentScan.Filter(
                        type = FilterType.ASSIGNABLE_TYPE,
                        classes = SecurityConfiguration.class
                )
        }
)
@MockBean(JpaMetamodelMappingContext.class)
class UserControllerTest {
    private MockMvc mvc;
    private Principal mockPrincipal;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private ObjectMapper objectMapper;

    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @MockBean
    private UserService userService;

    private JwtService jwtService = new JwtService();

    @BeforeEach
    public void setup() {
        mvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(springSecurity(new MockSpringSecurityFilter()))
                .build();

    }

    @Test
    @DisplayName(&quot;Signup Request&quot;)
    void test1() throws Exception {

        //given
        SignupRequestDto requestDto = SignupRequestDto.builder()
                .userId(&quot;testId1&quot;)
                .password(&quot;testPassword1!&quot;)
                .name(&quot;testName&quot;)
                .email(&quot;test@email.com&quot;)
                .intro(&quot;test Introduce&quot;)
                .build();

        MockMultipartFile file = new MockMultipartFile(
                &quot;file&quot;,
                &quot;profileImage.png&quot;,
                &quot;multipart/form-data&quot;,
                &quot;some image content&quot;.getBytes()
        );

        MockMultipartFile user = new MockMultipartFile(
                &quot;user&quot;,
                &quot;&quot;,
                &quot;application/json&quot;,
                objectMapper.writeValueAsBytes(requestDto)
        );

        // when - then
        mvc.perform(multipart(&quot;/api/users/signup&quot;)
                        .file(user)
                        .file(file))
                .andExpect(status().isOk())
                .andDo(print());
    }

    @Test
    @DisplayName(&quot;Login Request&quot;)
    public void test2() throws Exception {

        //given
        LoginRequestDto requestDto = LoginRequestDto.builder()
                .userId(&quot;testId1&quot;)
                .password(&quot;testPassword1!&quot;)
                .build();

        String jsonRequest = objectMapper.writeValueAsString(requestDto);

        //when - then
        mvc.perform(post(&quot;/api/users/login&quot;)
                        .contentType(&quot;application/json&quot;)
                        .content(jsonRequest))
                .andExpect(status().isOk())
                .andDo(print());
    }

    @Test
    @DisplayName(&quot;Logout Request&quot;)
    public void test3() throws Exception {

        // given
        User user = User.builder()
                .userId(&quot;testId1&quot;)
                .build();
        UserDetailsImpl userDetails = new UserDetailsImpl(user);
        mockPrincipal = new UsernamePasswordAuthenticationToken(userDetails, &quot;&quot;);

        // when
        userService.logoutUser(userDetails.getUser());

        // then
        mvc.perform(put(&quot;/api/users/logout&quot;)
                        .principal(mockPrincipal))
                .andExpect(status().isOk())
                .andDo(print());
    }

    @Test
    @DisplayName(&quot;Signout Request&quot;)
    public void test4() throws Exception {

        // given
        User user = User.builder()
                .id(1L)
                .userId(&quot;testId1&quot;)
                .password(&quot;testPassword1!&quot;)
                .name(&quot;testName&quot;)
                .email(&quot;test@email.com&quot;)
                .intro(&quot;test Introduce&quot;)
                .status(UserStatusEnum.NORMAL)
                .refreshToken(&quot;fidsbafilubasdjiklfboia&quot;)
                .profileImage(null)
                .build();

        SignoutRequestDto requestDto = SignoutRequestDto.builder()
                .password(&quot;testPassword1!&quot;)
                .build();

        UserDetailsImpl userDetails = new UserDetailsImpl(user);
        mockPrincipal = new UsernamePasswordAuthenticationToken(userDetails, &quot;&quot;);
        // when

        // then
        mvc.perform(put(&quot;/api/users/signout&quot;)
                        .principal(mockPrincipal)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(requestDto)))
                .andExpect(status().isOk())
                .andDo(print());
    }


}</code></pre>
<p><img src="https://velog.velcdn.com/images/2_minus/post/465bdf45-52d4-49c6-8fbd-8051b2918a4b/image.png" alt=""></p>
<blockquote>
<p>JpaAuditing 관련 오류 외에도 기존 프로젝트에서 SignOut api 가 실행이 안되는 500코드 오류 발생</p>
<ul>
<li>기존 requestDto의 필드가 하나 밖에 없어서 생기는 오류였음 </li>
<li><blockquote>
<p><code>@Jacksonized</code> 어노테이션으로 해결</p>
</blockquote>
</li>
</ul>
</blockquote>
<ul>
<li><input checked="" disabled="" type="checkbox"> <strong>🆕 Service Test 추가하기</strong><ul>
<li><code>@ExtendWith</code> 를 사용하여 Service Test 를 추가합니다.</li>
<li>User, UserDetails, Post, Comment Service 에 대해서 <strong>“통합 테스트”</strong> 를 추가합니다.</li>
<li>단순 DB CRUD 와 별개로 코드 레벨에서의 비즈니스 로직에 대한 테스트가 필요한 경우라면 “<strong>단위 테스트</strong>”를 추가합니다.<ul>
<li>ex) 비밀번호가 암호화 되었는가</li>
</ul>
</li>
<li>특정 상황에 예외가 정상적으로 발생하고 있는지도 테스트 합니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">package com.prac.music.service;

import com.prac.music.domain.board.dto.BoardRequestDto;
import com.prac.music.domain.board.dto.BoardResponseDto;
import com.prac.music.domain.board.service.BoardService;
import com.prac.music.domain.user.entity.User;
import com.prac.music.domain.user.entity.UserStatusEnum;
import com.prac.music.domain.user.service.JwtService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

import static org.hibernate.validator.internal.util.Contracts.assertNotNull;

@ExtendWith(MockitoExtension.class) // @Mock 사용을 위해 설정합니다.
public class BoardServiceTest {

    @Mock
    JwtService jwtService;

    @InjectMocks
    BoardService boardService;

    User user;

    @BeforeEach
    public void setUp() {
        user = User.builder()
                .id(1L)
                .userId(&quot;testId1&quot;)
                .password(&quot;testPassword1!&quot;)
                .name(&quot;testName&quot;)
                .email(&quot;test@email.com&quot;)
                .intro(&quot;test Introduce&quot;)
                .status(UserStatusEnum.NORMAL)
                .refreshToken(jwtService.createRefreshToken(&quot;testId1&quot;))
                .profileImage(null)
                .build();

    }

    @Test
    @DisplayName(&quot;createBoard Success&quot;)
    void test1() throws IOException {
        //given
        BoardRequestDto requestDto = BoardRequestDto.builder()
                .title(&quot;test title&quot;)
                .contents(&quot;test contents&quot;)
                .build();
        List&lt;MultipartFile&gt; files = List.of();
        //when
        BoardResponseDto createdBoard = boardService.createBoard(requestDto, user, files);

        //then
        assertNotNull(createdBoard);
    }
}</code></pre>
<blockquote>
<p><code>@Setup</code> 어노테이션으로 기존재해야할 엔티티를 미리 작성하고 테스트를 진행하는 방식으로 작성했다.
그런데 작성하고 보니 기존 Entity Test에서 진행했던 방식과 동일한 것으로 여겨져서 Entity Test를 더 단순하게 생각했어야 하는 구나 하고 생각했다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/2_minus/post/2af1ac31-87a8-41a2-abb4-0b859fbc2757/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot : 메일 인증 기능 구현하기 (2)]]></title>
            <link>https://velog.io/@2_minus/Spring-Boot-%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-2</link>
            <guid>https://velog.io/@2_minus/Spring-Boot-%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-2</guid>
            <pubDate>Fri, 14 Jun 2024 01:27:14 GMT</pubDate>
            <description><![CDATA[<p>지난 포스팅에서 Gmail SMTP에 관련된 설정을 완료했다.</p>
<p>이제 우리 서버에서 인증코드를 담은 메일을 전송하고, 그것으로 어떻게 사용자를 인증할 것인지에 대해 생각해 봐야한다.</p>
<h2 id="메일-인증-기능-흐름">메일 인증 기능 흐름</h2>
<h3 id="메일-전송">메일 전송</h3>
<p>사용자 : 메일 입력 -&gt; 서버 : 입력된 메일로 코드 전송</p>
<h3 id="메일-검증">메일 검증</h3>
<p>사용자 : 전송된 코드를 서버에 입력 -&gt; 서버 : 입력 받은 코드와 발송한 코드가 일치하는 지 검증</p>
<h3 id="고려사항">고려사항</h3>
<ul>
<li>사용자에게 보낸 인증 코드를 검증하기 위해서는 서버에 발송된 코드를 저장 할 필요가 있다.</li>
<li>인증 코드는 한 가지만 유효하게 한다. : 재발송시 기존 코드는 무효화</li>
</ul>
<h2 id="기능-구현">기능 구현</h2>
<h3 id="mail-entity">Mail Entity</h3>
<pre><code class="language-java">@Getter
@NoArgsConstructor
@Entity
@Table(name = &quot;mail&quot;)
public class Mail extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;
    // 인증 여부
    @Column
    private boolean status;
    // 메일 주소
    @Column
    private String email;
    // 인증 코드
    @Column
    private String code;
    // 사용자와 1대1 연결
    @OneToOne
    @JoinColumn(name = &quot;user_id&quot;, nullable = false)
    private User user;

    @Builder
    public Mail(User user) {
        this.user = user;
        this.email = user.getEmail();
        this.code = &quot;&quot;;
        this.status = false;
    }
    // 인증 코드 업데이트
    public void mailAddCode(String code) {
        this.code = code;
    }</code></pre>
<ul>
<li>id: 메일 엔티티의 고유키 </li>
<li>status:  인증 여부 상태를 나타내는 필드</li>
<li>email: 메일을 발송한 메일 주소</li>
<li>code: 인증 코드</li>
<li>User: 인증 대상 사용자</li>
</ul>
<h3 id="mailcontroller">MailController</h3>
<pre><code class="language-java">@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/users/email&quot;)
public class MailController {
    private final MailService mailService;
    // 메일 전송
    @PostMapping(&quot;/send&quot;)
    public ResponseEntity&lt;String&gt; sendMail(@RequestBody MailRequestDto requestDto) throws MessagingException {
        mailService.sendMail(requestDto);
        return ResponseEntity.ok(requestDto.getEmail() + &quot;로 인증코드가 발송되었습니다.&quot;);
    }
    // 메일 검증
    @PostMapping(&quot;/verify&quot;)
    public ResponseEntity&lt;String&gt; verifyMail(@RequestBody VerifyRequestDto requestDto) {
        mailService.verifyMail(requestDto);
        return ResponseEntity.ok(&quot;입력하신 메일 &quot; + requestDto.getEmail() + &quot;이 정상적으로 인증되었습니다.&quot;);
    }
}</code></pre>
<h3 id="mailservice">MailService</h3>
<h4 id="sendmail">sendMail</h4>
<pre><code class="language-java">
    public void sendMail(MailRequestDto requestDto) {
        Mail mail = checkAndSaveMail(requestDto.getEmail());
        MimeMessage emailForm = createMailForm(mail);
        javaMailSender.send(emailForm);

    }</code></pre>
<p>MailRequestDto 객체를 매개변수로 받는 sendMail 메서드</p>
<details>
<summary>MailRequestDto.java</summary>

<pre><code class="language-java">// MailRequestDto.java
@Getter
@NoArgsConstructor
public class MailRequestDto {
    private String email;

    @Builder
    public MailRequestDto(String email) {
        this.email = email;
    }
}</code></pre>
</details>

<p>requestDto 객체에서 메일 주소를 받아서 checkAndSaveMail 메서드를 수행</p>
<details>
  <summary>checkAndSaveMail</summary>

<pre><code class="language-java">private Mail checkAndSaveMail(String email) {
        User user = getUserByEmail(email);
        Optional&lt;Mail&gt; checkMail = mailRepository.findByEmail(user.getEmail());
        checkMail.ifPresent(mailRepository::delete);
        Mail mail = Mail.builder()
                .user(user)
                .build();

        return mailRepository.save(mail);
    }</code></pre>
<p>저장된 사용자 정보 조회 (회원가입 도중이지만 임시 상태로 저장되어 있음)
메일 주소를 받아서 MailRepository에서 같은 메일 주소를 조회 -&gt; 존재하면 메일 엔티티 삭제
새로운 메일 엔티티 생성 후 저장</p>
</details>

<p>  사용자에게 보낼 메일 형태 생성 후 전송</p>
<details>
  <summary>createMailForm</summary>

<pre><code class="language-java">private MimeMessage createMailForm(Mail mail) {
        String code = createCode();
        mail.mailAddCode(code);

        MimeMessage message = javaMailSender.createMimeMessage();

        try {
            MimeMessageHelper messageHelper = new MimeMessageHelper(message, true);
            String senderEmail = &quot;noreply_88@gmail.com&quot;;
            messageHelper.setFrom(senderEmail);
            messageHelper.setTo(mail.getEmail());
            messageHelper.setSubject(&quot;[88하게] 이메일 인증 번호 발송&quot;);

            String body = &quot;&lt;html&gt;&lt;body style=&#39;background-color: #000000 !important; margin: 0 auto; max-width: 600px; word-break: break-all; padding-top: 50px; color: #ffffff;&#39;&gt;&quot;;
            body += &quot;&lt;h1 style=&#39;padding-top: 50px; font-size: 30px;&#39;&gt;이메일 주소 인증&lt;/h1&gt;&quot;;
            body += &quot;&lt;p style=&#39;padding-top: 20px; font-size: 18px; opacity: 0.6; line-height: 30px; font-weight: 400;&#39;&gt;서비스 사용을 위해 회원가입 시 고객님께서 입력하신 이메일 주소의 인증이 필요합니다.&lt;br /&gt;&quot;;
            body += &quot;하단의 인증 번호로 이메일 인증을 완료하시면, 정상적으로 서비스를 이용하실 수 있습니다.&lt;br /&gt;&quot;;
            body += &quot;&lt;div class=&#39;code-box&#39; style=&#39;margin-top: 50px; padding-top: 20px; color: #000000; padding-bottom: 20px; font-size: 25px; text-align: center; background-color: #f4f4f4; border-radius: 10px;&#39;&gt;&quot; + code + &quot;&lt;/div&gt;&quot;;
            body += &quot;&lt;/body&gt;&lt;/html&gt;&quot;;
            messageHelper.setText(body, true);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
        return message;
    }</code></pre>
<p>createCode 메서드로 임의의 문자로 이루어진 인증 코드 생성</p>
<details>
  <summary>createCode</summary>

<pre><code class="language-java">// 이메일 인증코드 생성
    private String createCode() {
        int numberzero = 48; // 0 아스키 코드
        int alphbetz = 122; // z 아스키 코드
        int codeLength = 8; // 인증코드의 길이
        Random rand = new Random(); // 임의 생성

        return rand.ints(numberzero, alphbetz + 1)
                .filter(i -&gt; (i &lt;= 57 || i &gt;= 65) &amp;&amp; (i &lt;= 90 || i &gt;= 97)) // 숫자와 알파벳만 허용
                .limit(codeLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();
    }</code></pre>
  </details>

<p>  생성된 인증 코드를 메일 엔티티에 추가</p>
<p>  Spring boot에 포함된 MimeMessage class를 사용하여 이메일 구조 생성 후 반환</p>
<ul>
<li>생성된 메일 폼</li>
</ul>
<p><img src="https://velog.velcdn.com/images/2_minus/post/322ac6c5-4d3b-473c-b367-e9e2abefc951/image.png" alt=""></p>
</details>


<h4 id="verifymail">verifyMail</h4>
<pre><code class="language-java">public void verifyMail(VerifyRequestDto requestDto) {
        LocalDateTime now = LocalDateTime.now(); // 유효시간 체크를 위한 현재시간 체크
        User user = getUserByEmail(requestDto.getEmail());

          // 해당하는 메일주소가 없는 경우.
        Mail mail = mailRepository.findByEmail(user.getEmail()).orElseThrow(
                () -&gt; new MailServiceException(&quot;잘못된 이메일입니다.&quot;)
        );

          // 인증 유효시간 180초가 지난 경우
        LocalDateTime timeLimit = mail.getCreatedAt().plusSeconds(EXPIRED_TIME);
        String targetCode = mail.getCode();
        String code = requestDto.getCode();

        if (now.isAfter(timeLimit)) {
            throw new MailServiceException(&quot;만료된 인증코드입니다.&quot;);
        }

          // 발송된 코드와 입력받은 코드가 일치하는 경우
        if (targetCode.equals(code)) {
            user.updateStatusVeryfied(); // 사용자의 상태를 임시 -&gt; 정상으로 변경
        }
    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[팀프로젝트 KPT 회고]]></title>
            <link>https://velog.io/@2_minus/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-KPT-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@2_minus/%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-KPT-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 11 Jun 2024 23:26:15 GMT</pubDate>
            <description><![CDATA[<h2 id="keep">Keep</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/40abf81b-bdf5-47f0-96e9-a3223694d56e/image.png" alt=""></p>
<blockquote>
<p>서로 맡은 바를 잘 이루어 냈고 소통도 원활히 진행하여 서로의 진행사항을 잘 알았다.</p>
</blockquote>
<h2 id="problem">Problem</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/b6d17118-159a-4ff8-a4b6-0913b7b4b8e0/image.png" alt=""></p>
<blockquote>
<p>코드 컨벤션이 잘 이루어지지 않아서 기능 역할 분배가 잘 이루어지지 않아서 아쉬웠다. 그리고 깃 이슈, 코멘트를 사용하지 못 해서 아쉬웠다.</p>
</blockquote>
<h2 id="try">Try</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/4446e0cc-dc1c-4ce8-9c74-19f1e53a753a/image.png" alt=""></p>
<blockquote>
<p>코드 컨벤션을 사전에 팀원들과 충분히 이야기한 후에 작업을 진행하는 것이 좋을 것 같다.
깃에 있는 기능들 comment 라던가 lssues 를 더 활용하는 방식으로 진행해면 좋을 것 같다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring boot : 메일 인증 기능 구현하기 (1)]]></title>
            <link>https://velog.io/@2_minus/Spring-boot-%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@2_minus/Spring-boot-%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Mon, 10 Jun 2024 06:03:13 GMT</pubDate>
            <description><![CDATA[<p>spring boot 프로젝트 중 회원가입 절차에 메일이 유효한지, 인증 절차를 추가하려고 한다.
그러려면 서버에서 메일을 발송해야 하는 방법이 필요.
방법 중 하나인 Gmail에서 제공하는 SMTP를 이용하려고 한다.</p>
<h2 id="gmail-smtp-설정">Gmail SMTP 설정</h2>
<p>G메일에서 우측상단 톱니바퀴를 누르고, [ 모든 설정 보기 ] 버튼을 눌러줍니다.
<img src="https://velog.velcdn.com/images/2_minus/post/e3e5db78-85b8-451e-857d-c2047c91112c/image.png" alt="">
전달 및 POP/IMAP 탭을 살펴보고 아래 사항을 확인해둡시다.</p>
<ul>
<li>SMTP를 이용하려면, IMAP사용이 체크 되어 있어야 합니다. IMAP(Internet Message Access Protocol)은 메일을 읽어오는 클라이언트/서버 프로토콜인데, &quot;발송&quot; 기능과 관련이 있을까 싶긴 했는데, 아무튼 사용으로 체크해둡니다.
<img src="https://velog.velcdn.com/images/2_minus/post/5f2a3787-d11c-4f84-95dc-3030263157f8/image.png" alt="">
그러면, 아래처럼 상태: IMAP를 사용할 수 있습니다.로 표시됩니다.
<img src="https://velog.velcdn.com/images/2_minus/post/6a45ee6b-4f57-46df-93ca-6bdec6c2d339/image.png" alt=""> 
구글 계정으로 들어갑니다.(SMTP 사용을 위한 인증을 진행해줘야 합니다.)
<img src="https://velog.velcdn.com/images/2_minus/post/94988b12-8d75-48c4-be35-c4cfe27cc524/image.png" alt="">
 
왼쪽 [보안] 메뉴를 선택하고, 2단계 인증을 진행해줘야 합니다.
저는 휴대폰을 이용해서 2단계 인증을 진행했습니다. 그리고 2단계 인증 메뉴를 선택하고 하위로 들어가보면,
<img src="https://velog.velcdn.com/images/2_minus/post/4c87640c-7b81-4308-9b48-467259d164c2/image.png" alt="">
 
하단에, &quot;앱 비밀번호&quot;라는 게 있습니다. 당장 클릭!
 
<img src="https://velog.velcdn.com/images/2_minus/post/a8a7a9f7-b76f-4bad-82ae-f4e54b9a9908/image.png" alt=""></li>
</ul>
<p> 
 
대충 MY-SMTP 정도로 이름을 지어서 [생성] 버튼을 클릭해주도록 합니다.
 
아래처럼, &#39;기기용 앱 비밀번호&#39;가 생성되는데, 잘 메모해두도록 합니다.
이 기기용 앱 비밀번호가 G메일 SMTP를 이용해서 메일링을 할 때 계정에 로그인하는 pw가 됩니다.
<img src="https://velog.velcdn.com/images/2_minus/post/c73b93b2-4668-4d45-808c-97342e34d341/image.png" alt=""></p>
<p> </p>
<p> 
아래와 같이 MY-SMTP 라는 이름의 App이 생성되었습니다.
 
<img src="https://velog.velcdn.com/images/2_minus/post/2ffbe32a-9a2d-4421-9818-21a139e2db83/image.png" alt=""></p>
<p>그리고 SMTP 발송에 필요한 정보들을 변수에 담아, 코딩해서 활용하시면 되지 않을까 싶습니다.</p>
<pre><code># GMAIL SMTP
spring.mail.host = smtp.gmail.com
spring.mail.port = 587
spring.mail.username = ${SMTP_USER}
spring.mail.password = ${SMTP_PASSWORD}
spring.mail.properties.mail.smtp.auth = true
spring.mail.properties.mail.smtp.starttls.enable = true</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Refresh Token(with mySQL)]]></title>
            <link>https://velog.io/@2_minus/Refresh-Tokenwith-mySQL</link>
            <guid>https://velog.io/@2_minus/Refresh-Tokenwith-mySQL</guid>
            <pubDate>Fri, 07 Jun 2024 02:27:02 GMT</pubDate>
            <description><![CDATA[<p>요 며칠간 refresh token을 어떻게 프로젝트에 적용 시킬까 고민을 열심히 하고 수많은 포스팅을 들락 거렸다.
머리를 싸매고, 약간 뒹굴거리며 생각해낸 방법</p>
<ul>
<li><p>인증 성공 시 2가지 token을 생성</p>
</li>
<li><p>HttpResponse의 header 부분에 access token 과 refresh token을 추가</p>
<pre><code class="language-java">protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
      User user = ((UserDetailsImpl) authResult.getPrincipal()).getUser();
      String userId = user.getUserId();

      // 토큰 생성시 유효기간 차이를 두고 생성
      String token = jwtService.createToken(userId);
      String refreshToken = jwtService.createRefreshToken(userId);

      // 헤더의 이름을 변경해서 2가지 토큰을 response header에 추가
      response.addHeader(JwtService.AUTHORIZATION_HEADER, token);
      response.addHeader(JwtService.REFRESH_TOKEN_HEADER, refreshToken);

      user.setRefreshToken(refreshToken);
      userRepository.save(user);
  }</code></pre>
</li>
<li><p>동시에 생성된 refresh token을 사용자 엔티티에 추가
  -&gt; 데이터 베이스에 refresh token이 저장됨</p>
</li>
<li><p>이후 인가 단계에서 access token 만료시, refresh token에 대한 검증 단계를 추가
  -&gt; refresh token의 존재여부 (isNull)
  -&gt; 사용자의 토큰 (DB) HttpResponse에 담겼던 토큰이 같은지</p>
<pre><code class="language-java">//JwtAuthorizationFilter.java
  protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
      ...
          String userRefreshToken = jwtService.substringToken(user.getRefreshToken());
          String refreshToken = jwtService.getRefreshJwtFromHeader(req);

          log.info(&quot;User&#39;s Refresh Token: &quot; + userRefreshToken);
          log.info(&quot;response&#39;s Refresh Token: &quot; + refreshToken);

          // 유저가 보유한 토큰과 httpservlet의 토큰이 같은지 비교
          if (userRefreshToken.equals(refreshToken)) {
              // 토큰이 만료된 경우
              if (jwtService.isTokenExpired(tokenValue)) {
                  // 리프레시 토큰이 유효한 경우, 새로운 토큰 생성
                  if (!jwtService.isRefreshTokenExpired(userRefreshToken)) {
                      jwtService.createToken(user.getUserId());
                      System.out.println(&quot;jwtService.createToken(user.getUserId()) = &quot; + jwtService.createToken(user.getUserId()));
                      jwtService.createRefreshToken(user.getUserId());
                      log.info(&quot;새로운 토큰이 생성되었습니다.&quot;);
                      setAuthentication(claims.getSubject());
                      filterChain.doFilter(req, res);
                  } else {
                      // 리프레시 토큰도 만료된 경우
                      throw new IllegalArgumentException(&quot;다시 재로그인해주세요&quot;);
                  }
              } else {
                  // 토큰이 유효한 경우
                  setAuthentication(claims.getSubject());
              }
          } else {
              throw new IllegalArgumentException(&quot;토큰의 정보가 일치하지 않습니다.&quot;);
          }
      }
      ...</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Refresh Token]]></title>
            <link>https://velog.io/@2_minus/Refresh-Token</link>
            <guid>https://velog.io/@2_minus/Refresh-Token</guid>
            <pubDate>Wed, 05 Jun 2024 07:06:49 GMT</pubDate>
            <description><![CDATA[<h3 id="refresh-token이-왜-필요한가">Refresh token이 왜 필요한가</h3>
<p>Access Token 만을 통한 인증 방식의 문제는 만일 제 3자에게 탈취당할 경우 보안에 취약하다는 점이다.</p>
<p>Access Token은 발급된 이후, 서버에 저장되지 않고 토큰 자체로 검증을 하며 사용자 권한을 인증하기 떄문에, Access Token이 탈취되면 토큰이 만료되기 전 까지, 토큰을 획득한 사람은 누구나 권한 접근이 가능해 지기 때문이다.</p>
<p>JWT는 발급한 후 삭제가 불가능하기 때문에, 접근에 관여하는 토큰에 유효시간을 부여하는 식으로 탈취 문제에 대해 대응을 하여야 한다.</p>
<p>이처럼 토큰 유효기간을 짧게하면 토큰 남용을 방지하는 것이 해결책이 될 수 있지만, 유효기간이 짧은 Token의 경우 그만큼 사용자는 로그인을 자주 해서 새롭게 Token을 발급받아야 하므로 불편하다는 단점이 있다. 그렇다고 무턱대고 유효기간을 늘리자면, 토큰을 탈취당했을 때 보안에 더 취약해지게 된다.</p>
<p>이때 “그러면 유효기간을 짧게 하면서  좋은 방법이 있지는 않을까?”라는 질문의 답이 바로 Refresh Token이다.</p>
<p> 
이름이 다르지만 형태 자체는 Refresh Token은 Access Token과 똑같은 JWT다. 단지 Access Token은 접근에 관여하는 토큰이고, Refresh Token은 재발급에 관여하는 토큰 이므로 행하는 역할이 다르다고 보면 된다.</p>
<p>예를 들면 처음에 로그인을 했을 때, 서버는 로그인을 성공시키면서 클라이언트에게 Access Token과 Refresh Token을 동시에 발급한다.  서버는 데이터베이스에 Refresh Token을 저장하고, 클라이언트는 Access Token과 Refresh Token을 쿠키, 세션 혹은 웹스토리지에 저장하고 요청이 있을때마다 이 둘을 헤더에 담아서 보낸다.</p>
<p>이 Refresh Token은 긴 유효기간을 가지면서, Access Token이 만료됐을 때 새로 재발급해주는 열쇠가 된다. 따라서 만일 만료된 Access Token을 서버에 보내면, 서버는 같이 보내진 Refresh Token을  DB에 있는 것과 비교해서 일치하면 다시 Access Token을 재발급하는 간단한 원리이다. 그리고 사용자가 로그아웃을 하면 저장소에서 Refresh Token을 삭제하여 사용이 불가능하도록 하고 새로 로그인하면 서버에서 다시 재발급해서 DB에 저장한다.</p>
<h3 id="access--refresh-token-재발급-원리">Access / Refresh Token 재발급 원리</h3>
<p> </p>
<ol>
<li><p>기본적으로 로그인 같은 과정을 하면 Access Token과 Refresh Token을 모두 발급한다.
이때, Refresh Token만 서버측의 DB에 저장하며, Refresh Token과 Access Token을 쿠키 혹은 웹스토리지에 저장한다.
 </p>
</li>
<li><p>사용자가 인증이 필요한 API에 접근하고자 하면, 가장 먼저 토큰을 검사한다.
이때, 토큰을 검사함과 동시에 각 경우에 대해서 토큰의 유효기간을 확인하여 재발급 여부를 결정한다.</p>
<p> case1 : access token과 refresh token 모두가 만료된 경우 → 에러 발생 (재 로그인하여 둘다 새로 발급)
 case2 : access token은 만료됐지만, refresh token은 유효한 경우 →  refresh token을 검증하여 access token 재발급
 case3 : access token은 유효하지만, refresh token은 만료된 경우 →  access token을 검증하여 refresh token 재발급
 case4 : access token과 refresh token 모두가 유효한 경우 → 정상 처리</p>
</li>
<li><p>로그아웃을 하면 Access Token과 Refresh Token을 모두 만료시킨다.</p>
</li>
</ol>
<h3 id="refresh-token-인증-과정">Refresh Token 인증 과정</h3>
<p><img src="https://velog.velcdn.com/images/2_minus/post/96692c01-b235-4c05-958e-f326cf59324c/image.png" alt=""></p>
<pre><code>1. 사용자가 ID , PW를 통해 로그인.

2. 서버에서는 회원 DB에서 값을 비교

3~4. 로그인이 완료되면 Access Token, Refresh Token을 발급한다. 이때 회원DB에도 Refresh Token을 저장해둔다.

5. 사용자는 Refresh Token은 안전한 저장소에 저장 후, Access Token을 헤더에 실어 요청을 보낸다.

6~7. Access Token을 검증하여 이에 맞는 데이터를 보낸다.

8. 시간이 지나 Access Token이 만료됐다.

9. 사용자는 이전과 동일하게 Access Token을 헤더에 실어 요청을 보낸다.

10~11. 서버는 Access Token이 만료됨을 확인하고 권한없음을 신호로 보낸다.

12. 사용자는 Refresh Token과 Access Token을 함께 서버로 보낸다.

13. 서버는 받은 Access Token이 조작되지 않았는지 확인한후, Refresh Token과 사용자의 DB에 저장되어 있던 Refresh Token을 비교한다. Token이 동일하고 유효기간도 지나지 않았다면 새로운 Access Token을 발급해준다.

14. 서버는 새로운 Access Token을 헤더에 실어 다시 API 요청 응답을 진행한다. </code></pre><p>출처: <a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Access-Token-Refresh-Token-%EC%9B%90%EB%A6%AC-feat-JWT">https://inpa.tistory.com/entry/WEB-📚-Access-Token-Refresh-Token-원리-feat-JWT</a> [Inpa Dev 👨‍💻:티스토리]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[일정 관리 앱 서버 기능 추가하기 : 댓글 기능 구현]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0-%EB%8C%93%EA%B8%80-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0-%EB%8C%93%EA%B8%80-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 04 Jun 2024 01:38:54 GMT</pubDate>
            <description><![CDATA[<h2 id="1단계--일정과-댓글의-연관관계">1단계 : 일정과 댓글의 연관관계</h2>
<details>

<h3 id="설명">설명</h3>
<ul>
<li>지난 과제에서 만든 일정에 댓글을 추가할 수 있습니다.</li>
<li>ERD에도 댓글 모델을 추가합니다.</li>
<li>각 일정에 댓글을 작성할 수 있도록 관련 클래스를 추가하고 연관 관계를 설정합니다.</li>
<li>매핑 관계를 설정합니다. (1:1 or N:1 or N:M)</li>
</ul>
<h3 id="추가되는-entity--comment">추가되는 entity : Comment</h3>
<table>
<thead>
<tr>
<th align="center">댓글필드</th>
<th align="center">데이터 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="center">아이디(고유번호)</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">댓글내용</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">사용자 아이디</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">일정 아이디</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">작성일자</td>
<td align="center">timestamp</td>
</tr>
</tbody></table>
<ul>
<li><p>User</p>
<ul>
<li>하나의 user는 todo를 여러개 작성할 수 있으므로 OneToMany</li>
<li>하나의 user는 comment를 여러개 작성 할 수 있으므로 OneToMany</li>
</ul>
</li>
<li><p>todo</p>
<ul>
<li>여러개의 todo가 하나의 user에 의해 작성되므로 ManyToOne</li>
<li>하나의 todo는 여러개의 comment를 가질 수 있으므로 OneToMany</li>
</ul>
</li>
<li><p>comment</p>
<ul>
<li>여러개의 comment가 하나의 user에 의해 작성되므로 ManyToOne </li>
<li>여러개의 comment가 하나의 todo에 의해 작성되므로 ManyToOne</li>
</ul>
</li>
</ul>
<h3 id="수정되는-entity--user">수정되는 entity : User</h3>
<table>
<thead>
<tr>
<th align="center">사용자 필드</th>
<th align="center">데이터 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="center">아이디(고유번호)</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">별명</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">사용자 이름</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">비밀번호</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">권한</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">생성일</td>
<td align="center">timestamp</td>
</tr>
</tbody></table>
<h3 id="수정되는-entity--todo">수정되는 entity : Todo</h3>
<p>User 테이블이 생기면서 기존 Todo에 존재하는 password field가 중복된다고 판단.</p>
<table>
<thead>
<tr>
<th align="center">일정 필드</th>
<th align="center">데이터 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="center">아이디(고유번호)</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">사용자 이름</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">제목</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">내용</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center"><span style="color:red">비밀번호</span></td>
<td align="center"><span style="color:red">varchar</span></td>
</tr>
<tr>
<td align="center">생성일</td>
<td align="center">timestamp</td>
</tr>
<tr>
<td align="center"></details></td>
<td align="center"></td>
</tr>
</tbody></table>
<h3 id="entity-구현-및-수정">Entity 구현 및 수정</h3>
<details>
  <summary> 구현 : entity.Comment </summary>

<pre><code class="language-java">package io._2minus.todoapp.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


@Entity
@Getter
@Setter
@NoArgsConstructor
public class Comment extends Timestamped{

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

    @Column(nullable = false)
    private String content;

    @ManyToOne
    @JoinColumn(name = &quot;user_id&quot;, nullable = false)
    private User user;

    @ManyToOne
    @JoinColumn(name = &quot;todo_id&quot;,nullable = false)
    private Todo todo;

    @Builder
    public Comment(User user, Todo todo, String content) {
        this.user = user;
        this.todo = todo;
        this.content = content;
    }
}
</code></pre>
</details>

<details>
  <summary> 수정 : entity.Todo </summary>

<pre><code class="language-java">package io._2minus.todoapp.entity;

import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Entity
@NoArgsConstructor
@Table(name = &quot;todo&quot;)
public class Todo extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;todo_id&quot;, nullable = false)
    private long todoId;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    @Column(nullable = false)
    private String userName;

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

    @Builder
    public Todo(User user, String title, String content, String userName) {
        this.user = user;
        this.title = title;
        this.content = content;
        this.userName = userName;
    }
}
</code></pre>
</details>

<details>
  <summary> 구현 : entity.User </summary>

<pre><code class="language-java">package io._2minus.todoapp.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = &quot;user&quot;)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String nickname;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

    public User(String nickname, String username, String password, UserRoleEnum role) {
        this.nickname = nickname;
        this.username = username;
        this.password = password;
        this.role = role;
    }


}
</code></pre>
</details>

<h2 id="2단계--댓글-등록">2단계 : 댓글 등록</h2>
<details>

<h3 id="기능">기능</h3>
<ul>
<li>선택한 일정이 있다면 댓글을 등록합니다.</li>
</ul>
<h3 id="조건">조건</h3>
<ul>
<li>댓글이 등록되었다면 client에게 반환합니다.</li>
<li>선택한 일정이 DB에 저장되어 있어야 합니다.</li>
<li>댓글을 식별하는 <code>고유번호</code>, <code>댓글 내용</code>, 댓글을 작성한 <code>사용자 아이디</code>, 댓글이 작성된 <code>일정 아이디</code>, <code>작성일자</code>를 저장할 수 있습니다.</li>
</ul>
<h3 id="⚠️-예외-처리">⚠️ 예외 처리</h3>
<ul>
<li>선택한 일정의 ID를 입력 받지 않은 경우<pre><code class="language-java">CommentService.createComment
// todoRepository에서 id로 조회 및 예외처리
Todo todo = todoRepository.findById(dto.getTodo().getTodoId()).orElseThrow(()-&gt;
               new NullPointerException(&quot;잘못된 접근입니다.&quot;));</code></pre>
</li>
<li>댓글 내용이 비어 있는 경우<pre><code class="language-java">CommentRequestDTO
</code></pre>
</li>
</ul>
<p>// validation으로 예외처리
@NotBlank
private String content;</p>
<pre><code>- 일정이 DB에 저장되지 않은 경우
```java
CommentService.createComment

// save 후 commentRepository에서 조회 및 예외처리
commentRepository.save(comment);
        return commentRepository.findById(comment.getCommentId()).orElseThrow(()
        -&gt; new IllegalArgumentException(&quot;저장에 오류가 발생했습니다.&quot;));</code></pre></details>

<h3 id="기능-구현">기능 구현</h3>
<details>
  <summary> repository.CommentRepository </summary>

<pre><code class="language-java">package io._2minus.todoapp.repository;

import io._2minus.todoapp.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository&lt;Comment, Long&gt; {
}
</code></pre>
  </details>

  <details>
    <summary> service.CommentService.createComment </summary>

<pre><code class="language-java">import io._2minus.todoapp.repository.CommentRepository;
import io._2minus.todoapp.repository.TodoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class CommentService {
    private final CommentRepository commentRepository;
    private final TodoRepository todoRepository;

    @Transactional
    public Comment createComment(Long todoId, User user, CommentRequestDTO dto) {

        Todo todo = todoRepository.findById(todoId).orElseThrow(()-&gt;
                 new NullPointerException(&quot;잘못된 접근입니다.&quot;));

        var comment = dto.toEntity(user, todo);
        commentRepository.save(comment);
        return commentRepository.findById(comment.getCommentId()).orElseThrow(()
        -&gt; new IllegalArgumentException(&quot;저장에 오류가 발생했습니다.&quot;));
    }
}</code></pre>
</details>

<details>
  <summary> controller.CommentController.postComment </summary>

<pre><code class="language-java">@RequestMapping(&quot;/v1.0/todo/{todoId}/comments&quot;)
@RestController
@RequiredArgsConstructor
public class CommentController {

    public final CommentService commentService;

    @PostMapping
    public ResponseEntity&lt;CommonResponse&lt;CommentResponseDTO&gt;&gt; postComment(@PathVariable Long todoId, @AuthenticationPrincipal UserDetailsImpl userDetails, @RequestBody CommentRequestDTO dto) {
        Comment comment = commentService.createComment(todoId, userDetails.getUser(), dto);
        CommentResponseDTO response = new CommentResponseDTO(comment);
        return ResponseEntity.ok().body(CommonResponse.&lt;CommentResponseDTO&gt;builder()
                .statusCode(HttpStatus.OK.value())
                .msg(&quot;생성이 완료되었습니다.&quot;)
                .data(response)
                .build());
    }
}</code></pre>
  </details>


<h2 id="3단계--댓글-수정">3단계 : 댓글 수정</h2>
<details>
### 기능

<ul>
<li>선택한 일정의 댓글을 수정합니다.</li>
</ul>
<h3 id="조건-1">조건</h3>
<ul>
<li>댓글이 수정되었다면 수정된 댓글을 반환합니다.</li>
<li><code>댓글 내용</code>만 수정 가능합니다.</li>
<li>선택한 일정과 댓글이 DB에 저장되어 있어야 합니다.</li>
</ul>
<h3 id="⚠️-예외-처리-1">⚠️ 예외 처리</h3>
<ul>
<li>선택한 일정이나 댓글의 ID를 입력 받지 않은 경우</li>
<li>일정이나 댓글이 DB에 저장되지 않은 경우</li>
<li>선택한 댓글의 사용자가 현재 사용자와 일치하지 않은 경우</li>
</ul>
<p>위와 다르지 않음.
  </details></p>
<h3 id="기능-구현-1">기능 구현</h3>
<details>
    <summary> service.CommentService.updateComment </summary>

<pre><code class="language-java">@Transactional
    public Comment updateComment(Long todoId, User user, Long commentId, CommentRequestDTO dto) {
        Comment comment = checkUserAndGetComment(todoId, user, commentId);
        comment.setContent(dto.getContent());
        return commentRepository.save(comment);

    }</code></pre>
</details>

<details>
  <summary> controller.CommentController.putComment </summary>

<pre><code class="language-java">@PutMapping(&quot;/{commentId}&quot;)
    public ResponseEntity&lt;CommonResponse&lt;CommentResponseDTO&gt;&gt; putComment(@PathVariable Long todoId, @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long commentId, @RequestBody CommentRequestDTO dto) {
        Comment comment = commentService.updateComment(todoId, userDetails.getUser(), commentId, dto);
        CommentResponseDTO response = new CommentResponseDTO(comment);
        return ResponseEntity.ok().body(CommonResponse.&lt;CommentResponseDTO&gt;builder()
                .statusCode(HttpStatus.OK.value())
                .msg(&quot;수정 내용 : &quot; + comment.getContent())
                .data(response)
                .build());
    }</code></pre>
  </details>

<h2 id="4단계--댓글-삭제">4단계 : 댓글 삭제</h2>
<details>
### 기능

<ul>
<li>선택한 일정의 댓글을 삭제합니다.</li>
</ul>
<h3 id="조건-2">조건</h3>
<ul>
<li>성공했다는 메시지와 상태 코드 반환하기</li>
<li>선택한 일정과 댓글이 DB에 저장되어 있어야 합니다.</li>
</ul>
<h3 id="⚠️-예외-처리-2">⚠️ 예외 처리</h3>
<ul>
<li>선택한 일정이나 댓글의 ID를 입력받지 않은 경우</li>
<li>일정이나 댓글이 DB에 저장되지 않은 경우</li>
<li>선택한 댓글의 사용자가 현재 사용자와 일치하지 않은 경우</details>

</li>
</ul>
<h3 id="기능-구현-2">기능 구현</h3>
  <details>
    <summary> service.CommentService.deleteComment </summary>

<pre><code class="language-java">@Transactional
    public void deleteComment(Long todoId, User user, Long commentId) {
        Comment comment = checkUserAndGetComment(todoId, user, commentId);
        commentRepository.delete(comment);
    }</code></pre>
</details>

<details>
  <summary> controller.CommentController.deleteComment </summary>

<pre><code class="language-java">@DeleteMapping(&quot;/{commentId}&quot;)
    public ResponseEntity&lt;CommonResponse&gt; deleteComment(@PathVariable Long todoId, @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable Long commentId, @RequestBody CommentRequestDTO dto) {
        commentService.deleteComment(todoId, userDetails.getUser(), commentId);
        return ResponseEntity.ok().body(CommonResponse.&lt;CommentResponseDTO&gt;builder()
                .statusCode(HttpStatus.OK.value())
                .msg(&quot;삭제가 완료되었습니다.&quot;)
                .build());
    }</code></pre>
  </details>


]]></description>
        </item>
        <item>
            <title><![CDATA[일정 관리 앱 서버 기능 추가하기 : 구현 전 이것 저것]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%A0%84-%EC%9D%B4%EA%B2%83-%EC%A0%80%EA%B2%83</link>
            <guid>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EA%B8%B0%EB%8A%A5-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%A0%84-%EC%9D%B4%EA%B2%83-%EC%A0%80%EA%B2%83</guid>
            <pubDate>Mon, 03 Jun 2024 00:15:35 GMT</pubDate>
            <description><![CDATA[<h3 id="goal">Goal</h3>
<p>회원가입, 로그인 기능이 있는 투두앱 백엔드 서버 만들기</p>
<h2 id="erd">ERD</h2>
<h3 id="수정된-erd">수정된 ERD</h3>
<p><img src="https://velog.velcdn.com/images/2_minus/post/66959937-920e-406f-bd36-8d1f8ec7f2a1/image.png" alt=""></p>
<h2 id="api-명세서">API 명세서</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/e275c824-6ab3-4b7b-879b-d5ce34fee394/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/50937fae-6a48-4cef-b15e-e6a1ff91597e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/67801fca-3f8e-4591-aa8b-29ecb672f7f0/image.png" alt=""></p>
<h2 id="1단계--일정과-댓글의-연관관계">1단계 : 일정과 댓글의 연관관계</h2>
<h3 id="설명">설명</h3>
<ul>
<li>지난 과제에서 만든 일정에 댓글을 추가할 수 있습니다.</li>
<li>ERD에도 댓글 모델을 추가합니다.</li>
<li>각 일정에 댓글을 작성할 수 있도록 관련 클래스를 추가하고 연관 관계를 설정합니다.</li>
<li>매핑 관계를 설정합니다. (1:1 or N:1 or N:M)</li>
</ul>
<h3 id="추가되는-entity--comment">추가되는 entity : Comment</h3>
<table>
<thead>
<tr>
<th align="center">댓글필드</th>
<th align="center">데이터 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="center">아이디(고유번호)</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">댓글내용</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">사용자 아이디</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">일정 아이디</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">작성일자</td>
<td align="center">timestamp</td>
</tr>
</tbody></table>
<ul>
<li><p>User</p>
<ul>
<li>하나의 user는 todo를 여러개 작성할 수 있으므로 OneToMany</li>
<li>하나의 user는 comment를 여러개 작성 할 수 있으므로 OneToMany</li>
</ul>
</li>
<li><p>todo</p>
<ul>
<li>여러개의 todo가 하나의 user에 의해 작성되므로 ManyToOne</li>
<li>하나의 todo는 여러개의 comment를 가질 수 있으므로 OneToMany</li>
</ul>
</li>
<li><p>comment</p>
<ul>
<li>여러개의 comment가 하나의 user에 의해 작성되므로 ManyToOne </li>
<li>여러개의 comment가 하나의 todo에 의해 작성되므로 ManyToOne</li>
</ul>
</li>
</ul>
<h3 id="수정되는-entity--user">수정되는 entity : User</h3>
<table>
<thead>
<tr>
<th align="center">사용자 필드</th>
<th align="center">데이터 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="center">아이디(고유번호)</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">별명</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">사용자 이름</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">비밀번호</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">권한</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">생성일</td>
<td align="center">timestamp</td>
</tr>
</tbody></table>
<h3 id="수정되는-entity--todo">수정되는 entity : Todo</h3>
<p>User 테이블이 생기면서 기존 Todo에 존재하는 password field가 중복된다고 판단.</p>
<table>
<thead>
<tr>
<th align="center">일정 필드</th>
<th align="center">데이터 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="center">아이디(고유번호)</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">사용자 이름</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">제목</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">내용</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center"><span style="color:red">비밀번호</span></td>
<td align="center"><span style="color:red">varchar</span></td>
</tr>
<tr>
<td align="center">생성일</td>
<td align="center">timestamp</td>
</tr>
</tbody></table>
<h2 id="2단계--댓글-등록">2단계 : 댓글 등록</h2>
<h3 id="기능">기능</h3>
<ul>
<li>선택한 일정이 있다면 댓글을 등록합니다.</li>
</ul>
<h3 id="조건">조건</h3>
<ul>
<li>댓글이 등록되었다면 client에게 반환합니다.</li>
<li>선택한 일정이 DB에 저장되어 있어야 합니다.</li>
<li>댓글을 식별하는 <code>고유번호</code>, <code>댓글 내용</code>, 댓글을 작성한 <code>사용자 아이디</code>, 댓글이 작성된 <code>일정 아이디</code>, <code>작성일자</code>를 저장할 수 있습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/2_minus/post/9bd4186a-3d80-4105-b90d-8d24467fe21f/image.png" alt=""></p>
<h3 id="⚠️-예외-처리">⚠️ 예외 처리</h3>
<ul>
<li>선택한 일정의 ID를 입력 받지 않은 경우<pre><code class="language-java">CommentService.createComment
// todoRepository에서 id로 조회 및 예외처리
Todo todo = todoRepository.findById(dto.getTodo().getTodoId()).orElseThrow(()-&gt;
               new NullPointerException(&quot;잘못된 접근입니다.&quot;));</code></pre>
</li>
<li>댓글 내용이 비어 있는 경우<pre><code class="language-java">CommentRequestDTO
</code></pre>
</li>
</ul>
<p>// validation으로 예외처리
@NotBlank
private String content;</p>
<pre><code>- 일정이 DB에 저장되지 않은 경우
```java
CommentService.createComment

// save 후 commentRepository에서 조회 및 예외처리
commentRepository.save(comment);
        return commentRepository.findById(comment.getCommentId()).orElseThrow(()
        -&gt; new IllegalArgumentException(&quot;저장에 오류가 발생했습니다.&quot;));</code></pre><h2 id="3단계--댓글-수정">3단계 : 댓글 수정</h2>
<h3 id="기능-1">기능</h3>
<ul>
<li>선택한 일정의 댓글을 수정합니다.</li>
</ul>
<h3 id="조건-1">조건</h3>
<ul>
<li>댓글이 수정되었다면 수정된 댓글을 반환합니다.</li>
<li><code>댓글 내용</code>만 수정 가능합니다.</li>
<li>선택한 일정과 댓글이 DB에 저장되어 있어야 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/2_minus/post/8941350e-239a-4d28-a447-99c764fa0c1e/image.png" alt=""></p>
<h3 id="⚠️-예외-처리-1">⚠️ 예외 처리</h3>
<ul>
<li>선택한 일정이나 댓글의 ID를 입력 받지 않은 경우</li>
<li>일정이나 댓글이 DB에 저장되지 않은 경우</li>
<li>선택한 댓글의 사용자가 현재 사용자와 일치하지 않은 경우</li>
</ul>
<p>위와 다르지 않음.</p>
<h2 id="4단계--댓글-삭제">4단계 : 댓글 삭제</h2>
<h3 id="기능-2">기능</h3>
<ul>
<li>선택한 일정의 댓글을 삭제합니다.</li>
</ul>
<h3 id="조건-2">조건</h3>
<ul>
<li>성공했다는 메시지와 상태 코드 반환하기</li>
<li>선택한 일정과 댓글이 DB에 저장되어 있어야 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/2_minus/post/0dbed1b8-cfa9-4e5e-a0ab-03f58d331d7c/image.png" alt=""></p>
<h3 id="⚠️-예외-처리-2">⚠️ 예외 처리</h3>
<ul>
<li>선택한 일정이나 댓글의 ID를 입력받지 않은 경우</li>
<li>일정이나 댓글이 DB에 저장되지 않은 경우</li>
<li>선택한 댓글의 사용자가 현재 사용자와 일치하지 않은 경우</li>
</ul>
<h2 id="5단계--jwt">5단계 : JWT</h2>
<h3 id="기능-3">기능</h3>
<ul>
<li>JWT를 이용한 인증/인가를 구현한다.</li>
<li>위 1~4 단계에서 인증/인가가 완료된 후에만 기능이 동작하도록 수정한다.</li>
</ul>
<h3 id="조건-3">조건</h3>
<ul>
<li>Access Token 만료시간 60분</li>
<li>Refresh Token 구현은 8단계이므로 이번에는 하지 않습니다.</li>
</ul>
<h3 id="⚠️-예외-처리-3">⚠️ 예외 처리</h3>
<ul>
<li>공통조건<ul>
<li>StatusCode : 400</li>
<li>client에 반환</li>
</ul>
</li>
<li>토큰이 필요한 API 요청에서 토큰을 전달하지 않았거나 정상 토큰이 아닐 때<ul>
<li>에러 메세지 : 토큰이 유효하지 않습니다.</li>
</ul>
</li>
<li>토큰이 있고, 유효한 토큰이지만 해당 사용자가 작성한 게시글/댓글이 아닐 때<ul>
<li>에러 메세지 : 작성자만 삭제/수정할 수 있습니다.</li>
</ul>
</li>
<li>DB에 이미 존재하는 <code>username</code>으로 회원가입을 요청할 때<ul>
<li>에러 메세지 : 중복된 <code>username</code> 입니다.</li>
</ul>
</li>
<li>로그인 시, 전달된 <code>username</code>과 <code>password</code> 중 맞지 않는 정보가 있을 때<ul>
<li>에러 메시지 : 회원을 찾을 수 없습니다.</li>
</ul>
</li>
</ul>
<h2 id="6단계--회원가입">6단계 : 회원가입</h2>
<h3 id="기능-4">기능</h3>
<ul>
<li>사용자의 정보를 전달 받아 유저 정보를 저장한다.</li>
</ul>
<h3 id="조건-4">조건</h3>
<ul>
<li><p>패스워드 암호화는 하지 않습니다.</p>
<ul>
<li><code>PasswordEncoder()</code> 사용할 필요없음</li>
</ul>
</li>
<li><p><code>username</code>은  <code>최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)</code>로 구성되어야 한다.</p>
</li>
<li><p><code>password</code>는  <code>최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)</code>로 구성되어야 한다.</p>
<pre><code class="language-java">// 정규식을 이용한 제약조건
  @NotBlank
  @Pattern(regexp = &quot;^[a-z0-9]{4,10}$&quot;)
  private String username;

  @NotBlank
  @Pattern(regexp = &quot;^[A-Za-z0-9]{8,15}$&quot;)
  private String password;</code></pre>
</li>
<li><p>DB에 중복된 <code>username</code>이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/2_minus/post/5c4a0f7c-0d1e-4d7a-8f0f-07c700e0e065/image.png" alt=""></p>
<h2 id="7️⃣단계---로그인">7️⃣단계 - 로그인</h2>
<p>-</p>
<h3 id="기능-5">기능</h3>
<ul>
<li><code>username</code>, <code>password</code> 정보를 client로부터 전달받아 토큰을 반환한다.</li>
</ul>
<h3 id="설명-1">설명</h3>
<ul>
<li>DB에서 <code>username</code>을 사용하여 저장된 회원의 유무를 확인한다.<ul>
<li>저장된 회원이 있다면 <code>password</code> 를 비교하여 로그인 성공 유무를 체크한다.</li>
</ul>
</li>
</ul>
<h3 id="조건-5">조건</h3>
<ul>
<li>패스워드 복호화는 하지 않습니다.</li>
<li>로그인 성공 시 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급한다.</li>
<li>발급한 토큰을 <code>Header</code>에 추가하고 성공했다는 메시지 및 상태코드와 함께 client에 반환한다.</li>
</ul>
<h2 id="7단계--로그인">7단계 : 로그인</h2>
<h3 id="기능-6">기능</h3>
<ul>
<li><code>username</code>, <code>password</code> 정보를 client로부터 전달받아 토큰을 반환한다.</li>
</ul>
<h3 id="설명-2">설명</h3>
<ul>
<li>DB에서 <code>username</code>을 사용하여 저장된 회원의 유무를 확인한다.<ul>
<li>저장된 회원이 있다면 <code>password</code> 를 비교하여 로그인 성공 유무를 체크한다.</li>
</ul>
</li>
</ul>
<h3 id="조건-6">조건</h3>
<ul>
<li>패스워드 복호화는 하지 않습니다.</li>
<li>로그인 성공 시 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급한다.</li>
<li>발급한 토큰을 <code>Header</code>에 추가하고 성공했다는 메시지 및 상태코드와 함께 client에 반환한다.
<img src="https://velog.velcdn.com/images/2_minus/post/fe974385-6b3a-4bff-81c4-82e24243fece/image.png" alt=""></li>
</ul>
<h2 id="8단계--refresh-token-미구현">8단계 : Refresh Token (미구현)</h2>
<h3 id="기능-7">기능</h3>
<ul>
<li>5단계에서 구현한 JWT를 Refresh Token을 사용하도록 변경</li>
</ul>
<h3 id="설명-3">설명</h3>
<ul>
<li>세션 관리<ul>
<li>자주 로그인을 반복하지 않으면서 긴 유효 기간을 가진 Refresh Token을 통해 사용자가 시스템에 지속적으로 연결되어 있을 수 있습니다.</li>
</ul>
</li>
<li>리소스 관리<ul>
<li>서버가 세션 상태를 유지할 필요 없습니다. 클라이언트 측에서 JWT를 관리하기 때문에 서버는 세션을 위해 추가적인 자원을 사용하지 않아도 됩니다.</li>
<li>여기서 클라이언트 → Postman</li>
</ul>
</li>
</ul>
<h3 id="조건-7">조건</h3>
<ul>
<li>Access Token 유효기간이 지난 후 Refresh Token 갱신하지 않으면 접근 불가</li>
<li>Refresh Token 만료가 되면 인증 실패 처리 및 재로그인 유도</li>
<li>스프링 시큐리티 사용하지 않고 5단계에서 구현한 JWT 기능에 Refresh Token만 추가 구현</li>
</ul>
<h2 id="9단계--일정과-댓글-수정">9단계 : 일정과 댓글 수정</h2>
<h3 id="기능-8">기능</h3>
<ul>
<li>로그인 한 사용자에 한하여 일정과 댓글을 작성하고 수정할 수 있는 기능</li>
<li>‘만료되지 않은 유효 토큰’인 경우에만 일정과 댓글 ‘생성’이 가능하도록 변경</li>
<li>조회는 누구나 할 수 있습니다!</li>
</ul>
<h3 id="설명-4">설명</h3>
<ul>
<li>보안<ul>
<li>사용자 인증을 통해 접근을 제어하여 보안을 강화합니다. 또한 개인 로그를 적재하고 분석하면 모니터링도 가능합니다.</li>
</ul>
</li>
<li>사용자 경험<ul>
<li>로그인을 통해 개인을 식별하고 개인에 맞춘 서비스를 제공할 수 있습니다. 이를 통해 사용자 경험을 향상시킬 수 있습니다. 또한 사용자 추적을 통해 맞춤 서비스를 개발할 수 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="조건-8">조건</h3>
<ul>
<li>로그인을 하지 않으면 기능을 사용할 수 없다.</li>
<li>유효한 토큰인 경우에만 일정과 댓글을 작성할 수 있다.</li>
<li>일정을 생성한 사용자와 동일한 <code>username</code>이면 수정할 수 있다.</li>
<li>댓글을 작성한 사용자와 동일한 <code>username</code>이면 수정할 수 있다.</li>
</ul>
<h2 id="10단계--파일-첨부">10단계 : 파일 첨부</h2>
<h3 id="기능-9">기능</h3>
<ul>
<li>각 일정에 파일 첨부 가능</li>
</ul>
<h3 id="설명-5">설명</h3>
<ul>
<li>데이터 형식 이해<ul>
<li>파일을 데이터베이스에 저장할 때 사용하는 데이터 형식인 이진 데이터 <code>blob</code>에 대해 이해할 수 있습니다.</li>
</ul>
</li>
<li>텍스트 데이터 처리<ul>
<li>이진 데이터를 처리하면서 파일 입출력과 데이터 스트림 처리 등 기술에 대해 이해할 수 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="조건-9">조건</h3>
<ul>
<li>파일 테이블 생성</li>
<li>일정에 파일을 첨부할 수 있는 기능</li>
<li>일정과 파일 테이블 간 연간관계 형성<ul>
<li>1:1 매핑</li>
<li>ERD에도 파일 모델을 추가합니다.</li>
</ul>
</li>
</ul>
<h3 id="entity-추가--file">Entity 추가 : File</h3>
<table>
<thead>
<tr>
<th align="center">파일 필드</th>
<th align="center">데이터 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="center">아이디</td>
<td align="center">bigint</td>
</tr>
<tr>
<td align="center">파일이름</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">파일 확장자</td>
<td align="center">varchar</td>
</tr>
<tr>
<td align="center">파일 크기</td>
<td align="center">int</td>
</tr>
<tr>
<td align="center">생성일자</td>
<td align="center">timestamp</td>
</tr>
<tr>
<td align="center">파일 콘텐츠</td>
<td align="center">blob</td>
</tr>
<tr>
<td align="center"><img src="https://velog.velcdn.com/images/2_minus/post/01b8ff7c-1ad3-459c-b197-8b2f245f2943/image.png" alt=""></td>
<td align="center"></td>
</tr>
</tbody></table>
<ul>
<li>일정을 생성할 때 파일을 첨부할 수 있다.</li>
<li>일정을 수정할 때 파일을 첨부된 파일을 수정할 수 있다.</li>
</ul>
<h3 id="⚠️-예외-처리-4">⚠️ 예외 처리</h3>
<ul>
<li>일정을 삭제할 때 파일도 함께 삭제된다.</li>
<li>파일 형식은 <code>png</code>, <code>jpg</code>만 가능하다.</li>
<li>용량은 최대 <code>5MB</code>까지만 가능하다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증과 인가 (5) : JWT 다루기 (수정중)]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-5-JWT-%EB%8B%A4%EB%A3%A8%EA%B8%B0-%EC%88%98%EC%A0%95%EC%A4%91</link>
            <guid>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-5-JWT-%EB%8B%A4%EB%A3%A8%EA%B8%B0-%EC%88%98%EC%A0%95%EC%A4%91</guid>
            <pubDate>Tue, 28 May 2024 00:22:58 GMT</pubDate>
            <description><![CDATA[<p>직전 포스팅에 이어서 JWT를 활용하는 코드를 리뷰하는 포스팅</p>
<h2 id="jwtutil">JWTutil</h2>
<p>JWT를 다루기 위해 그와 관련된 기능-메서드들을 모아 정리하는 클래스</p>
<p>필요한 기능들</p>
<ul>
<li>생성</li>
<li>쿠키에 저장</li>
<li>쿠키의 JWT를 Substring -&gt; 검증을 위해서</li>
<li>검증</li>
<li>정보 추출<pre><code class="language-java">// Header KEY 값
public static final String AUTHORIZATION_HEADER = &quot;Authorization&quot;;
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = &quot;auth&quot;;
// Token 식별자
public static final String BEARER_PREFIX = &quot;Bearer &quot;;
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
</code></pre>
</li>
</ul>
<p>@Value(&quot;${jwt.secret.key}&quot;) // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;</p>
<p>// 로그 설정
public static final Logger logger = LoggerFactory.getLogger(&quot;JWT 관련 로그&quot;);</p>
<p>@PostConstruct
public void init() {
    byte[] bytes = Base64.getDecoder().decode(secretKey);
    key = Keys.hmacShaKeyFor(bytes);
}</p>
<pre><code>- Base64로 Encode된 Secret Key를 properties에 작성해두고 @Value를 통해 가져옵니다.
- JWT를 생성할 때 가져온 Secret Key로 암호화합니다.
    - 이때 Encode된 Secret Key를 Decode 해서 사용합니다.
    - Key는 Decode된 Secret Key를 담는 객체입니다.
    - @PostConstruct는 딱 한 번만 받아오면 되는 값을 사용 할 때마다 요청을 새로 호출하는 실수를 방지하기 위해 사용됩니다.
        - JwtUtil 클래스의 생성자 호출 이후에 실행되어 Key 필드에 값을 주입 해줍니다.
- 암호화 알고리즘은 HS256 알고리즘을 사용합니다.
- Bearer 란 JWT 혹은 OAuth에 대한 토큰을 사용한다는 표시입니다.
- 로깅이란 애플리케이션이 동작하는 동안 프로젝트의 상태나 동작 정보를 시간순으로 기록하는 것을 의미합니다.

</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[인증과 인가 (4) : JWT]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-4-JWT</link>
            <guid>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-4-JWT</guid>
            <pubDate>Mon, 27 May 2024 01:03:05 GMT</pubDate>
            <description><![CDATA[<p>지난 쿠키와 세션에 이어서 토큰 방식의 인증과 인가 구현 방법 중 JWT에 대해 공부하는 포스트</p>
<h2 id="토큰이란">토큰이란?</h2>
<p>사용자가 자신의 아이덴티티를 확인하고 고유한 액세스 토큰을 받을 수 있는 프로토콜. 사용자는 토큰 유효 기간 동안 동일한 웹페이지나 앱, 혹은 그 밖에 해당 토큰으로 보호를 받는 리소스로 돌아갈 때마다 자격 증명을 다시 입력할 필요 없이 토큰이 발급된 웹사이트나 앱에 액세스할 수 있다.</p>
<p>인증 토큰은 도장이 찍힌 티켓이라고 생각할 수 있다. 토큰이 유효하다면 사용자는 계속해서 액세스할 수 있다. 사용자가 로그아웃하거나 앱을 종료하면 토큰도 무효화된다.</p>
<p>토큰 기반 인증은 비밀번호 기반 또는 서버 기반 인증 기법과는 다르다. 토큰이 두 번째 보안 계층을 제공하여 관리자가 각 작업과 트랜잭션을 정밀하게 제어할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/93680921-b84c-4e23-8edf-d78506efae40/image.png" alt=""></p>
<p>토큰 인증 방식에서는 보조 서비스를 통해 서버 요청을 확인해야한다. 확인이 완료되면 서버가 토큰을 발급하여 요청에 응답한다.</p>
<p>사용자는 여전히  적어도 한 개의 비밀번호 를 기억해야 하지만 토큰이 액세스 계층을 하나 더 제공하기 때문에 도용하거나 침투하기가 훨씬 더 어렵고 세션 레코드가 서버에서 공간을 전혀 차지하지 않는다.</p>
<h2 id="토큰의-종류">토큰의 종류</h2>
<p>모든 인증 토큰이 액세스를 허용하지만 각 유형마다 약간씩 차이가 있다.</p>
<p>일반적으로 다음과 같은 세 가지 유형의 인증 토큰이 있다.</p>
<ul>
<li>연결형: 키, 디스크, 드라이브 및 기타 물리적 장치가 시스템에 연결되어 액세스를 허용. USB 디바이스나 스마트카드를 사용해 시스템에 로그인한 경험이 있다면 연결식 토큰을 사용해본 것이다.</li>
<li>비접촉형: 디바이스가 서버와 통신하려면 거리가 충분히 가까워야 하지만 연결할 필요는 없다. Microsoft의 &quot;매직 링&quot;이라고 하는 토큰이 여기에 해당.</li>
<li>분리형: 디바이스가 다른 디바이스와 접촉하지 않고도 먼 거리에서 서버와 통신할 수 있다. 예를 들어 이중 요소 인증 프로세스에 휴대전화를 사용한 경험이 있다면 이러한 유형의 토큰을 사용해본 것이다.</li>
</ul>
<p>위의 세 가지 시나리오 모두 사용자가 무언가를 해야만 프로세스가 시작된다. 비밀번호를 입력해야 할 수도 있고, 혹은 질문에 답변해야 할 수도 있다. 하지만 예비 단계를 완벽하게 마쳤다고 해도 액세스 토큰이 없으면 액세스할 수 없다. </p>
<h2 id="jwt-json-web-token">JWT (Json Web Token)</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/05968f3b-4f1e-40a9-a53d-80ffce9b2537/image.png" alt=""></p>
<p>JWT는 Json Web Token의 약자로 일반적으로 클라이언트와 서버 사이에서 통신할 때 권한을 위해 사용하는 토큰이다. 웹 상에서 정보를 Json형태로 주고 받기 위해 표준규약에 따라 생성한 암호화된 토큰으로 복잡하고 읽을 수 없는 string 형태로 저장되어있다. 누구나 복호화를 할 수는 있지만 약속된 Secret Key가 없다면 수정이 불가능한 읽기 전용 데이터다. </p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/6d5dd66d-80da-4915-99e4-80f630daa2b4/image.png" alt=""></p>
<p>헤더, 페이로드, 서명의 세가지 부분으로 나누어져 있다.</p>
<ul>
<li>헤더 (Header)<pre><code class="language-Json">{
&quot;alg&quot;: &quot;HS256&quot;,
&quot;typ&quot;: &quot;JWT&quot;
}</code></pre>
어떠한 알고리즘으로 암호화 할 것인지, 어떠한 토큰을 사용할 것 인지에 대한 정보가 들어있다.</li>
<li>정보 (Payload)<pre><code class="language-Json">{
&quot;sub&quot;: &quot;1234567890&quot;,
&quot;username&quot;: &quot;카즈하&quot;,
&quot;admin&quot;: true
}</code></pre>
전달하려는 정보(사용자 id나 다른 데이터들, 이것들을 클레임이라고 부른다)가 들어있다.
payload에 있는 내용은 수정이 가능하여 더 많은 정보를 추가할 수 있다. 그러나 노출과 수정이 가능한 지점이기 때문에 인증이 필요한 최소한의 정보(아이디, 비밀번호 등 개인정보가 아닌 이 토큰을 가졌을 때 권한의 범위나 토큰의 발급일과 만료일자 등)만을 담아야한다.</li>
<li>서명 (Signature)<pre><code class="language-Json">HMACSHA256(
base64UrlEncode(header) + &quot;.&quot; +
base64UrlEncode(payload),
secret)</code></pre>
가장 중요한 부분으로 헤더와 정보를 합친 후 발급해준 서버가 지정한 secret key로 암호화 시켜 토큰을 변조하기 어렵게 만들어준다.
한가지 예를 들어보자면 토큰이 발급된 후 누군가가 Payload의 정보를 수정하면 Payload에는 다른 누군가가 조작된 정보가 들어가 있지만 Signatute에는 수정되기 전의 Payload 내용을 기반으로 이미 암호화 되어있는 결과가 저장되어 있기 때문에 조작되어있는 Payload와는 다른 결과값이 나오게 된다.
이러한 방식으로 비교하면 서버는 토큰이 조작되었는지 아닌지를 쉽게 알 수 있고, 다른 누군가는 조작된 토큰을 악용하기가 어려워진다.</li>
</ul>
<h2 id="jwt-장-단점">JWT 장, 단점</h2>
<h3 id="장점">장점</h3>
<ul>
<li>동시 접속자가 많을 때 서버의 부하를 감소시킨다.</li>
<li>범용성이 높아 client, server 간 도메인이 달라도 사용가능.<ul>
<li>예) 카카오 OAuth2 로그인 시 JWT Token 사용<h3 id="단점">단점</h3>
</li>
</ul>
</li>
<li>구현의 복잡도 증가</li>
<li>JWT에 담는 내용이 커질수록 네트워크 비용 증가</li>
<li>생성 완료된 JWT를 일부만 만료시킬 방법이 없음</li>
<li>Secret key 하나로 보안이 취약해진다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증과 인가 (3) : 쿠키와 세션 다루기]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-3-%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-3-%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Thu, 23 May 2024 20:40:14 GMT</pubDate>
            <description><![CDATA[<p>지난 포스트에서 다뤘던 쿠키와 세션을 어떻게 코드로 구현하는 지 작성해볼 예정이다.</p>
<h1 id="쿠키">쿠키</h1>
<h2 id="쿠키-생성">쿠키 생성</h2>
<pre><code class="language-java">public static void addCookie(String cookieValue, HttpServletResponse res) {
    try {
        cookieValue = URLEncoder.encode(cookieValue, &quot;utf-8&quot;).replaceAll(&quot;\\+&quot;, &quot;%20&quot;); // Cookie Value 에는 공백이 불가능해서 encoding 진행

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
        cookie.setPath(&quot;/&quot;);
        cookie.setMaxAge(30 * 60);

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e.getMessage());
    }
}</code></pre>
<ul>
<li><p><code>new Cookie(AUTHORIZATION_HEADER, cookieValue)</code></p>
<ul>
<li><code>AUTHORIZATION_HEADER</code> : 쿠키의 이름</li>
<li><code>cookieValue</code> : 쿠키의 값</li>
</ul>
</li>
<li><p><code>cookie.setPath(&quot;/&quot;)</code>  : 쿠키의 Path</p>
</li>
<li><p><code>cookie.setMaxAge(30 * 60)</code> : 쿠키 만료시간 지정</p>
</li>
<li><p><code>res.addCookie(cookie)</code> : <code>res = HttpServletResponse</code> 객체에 Cookie 객체를 추가</p>
</li>
</ul>
<h2 id="쿠키-읽기">쿠키 읽기</h2>
<pre><code class="language-java">@GetMapping(&quot;/get-cookie&quot;)
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
    System.out.println(&quot;value = &quot; + value);

    return &quot;getCookie : &quot; + value;
}</code></pre>
<ul>
<li><code>@CookieValue(AUTHORIZATION_HEADER) String value</code><ul>
<li><code>AUTHORIZATION_HEADER</code> 값에 해당하는 <code>cookieValue</code>를 반환한다.</li>
</ul>
</li>
</ul>
<h1 id="세션">세션</h1>
<p><code>jakarta.Servlet</code>에서 세션을 간편하게 다룰수 있는 <code>HttpSession</code>을 제공</p>
<h2 id="세션-생성">세션 생성</h2>
<pre><code class="language-java">@GetMapping(&quot;/create-session&quot;)
public String createSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
    HttpSession session = req.getSession(true);

    // 세션에 저장될 정보 Name - Value 를 추가합니다.
    session.setAttribute(AUTHORIZATION_HEADER, &quot;Robbie Auth&quot;);

    return &quot;createSession&quot;;
}</code></pre>
<ul>
<li><code>HttpServletRequest</code>를 사용하여 세션을 생성 및 반환할 수 있다.</li>
<li><code>req=HttpServletRequest.getSession(true)</code><ul>
<li>세션이 존재할 경우 세션을 반환하고 없을 경우 새로운 세션을 생성.</li>
</ul>
</li>
<li>세션에 저장할 정보를 Name-Value 형식으로 추가.</li>
</ul>
<h2 id="세션-읽기">세션 읽기</h2>
<pre><code class="language-java">@GetMapping(&quot;/get-session&quot;)
public String getSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
    HttpSession session = req.getSession(false);

    String value = (String) session.getAttribute(AUTHORIZATION_HEADER); 
    // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
    System.out.println(&quot;value = &quot; + value);

    return &quot;getSession : &quot; + value;
}</code></pre>
<ul>
<li><code>req=HttpServletRequest.getSession(false)</code><ul>
<li>세션이 존재할 경우 세션을 반환하고 없을 경우 null을 반환.</li>
</ul>
</li>
<li><code>session.getAttribute(”세션에 저장된 정보 Name”)</code><ul>
<li>Name을 사용하여 세션에 저장된 Value를 반환.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증과 인가 (2) : 쿠키와 세션]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-2-%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98</link>
            <guid>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-2-%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98</guid>
            <pubDate>Thu, 23 May 2024 01:07:12 GMT</pubDate>
            <description><![CDATA[<h1 id="쿠키">쿠키</h1>
<h2 id="쿠키란">쿠키란?</h2>
<p>쿠키는 웹 서버가 생성하여 웹 브라우저로 전송하는 작은 정보 파일입니다. 웹 브라우저는 수신한 쿠키를 미리 정해진 기간 동안 또는 웹 사이트에서의 사용자 세션 기간 동안 저장합니다. 웹 브라우저는 향후 사용자가 웹 서버에 요청할 때 관련 쿠키를 첨부합니다.</p>
<p>쿠키는 웹 사이트에 사용자에 대한 정보를 제공하여 웹 사이트에서 사용자 경험을 맞춤화하는 데 도움이 됩니다. 예를 들어, 전자 상거래 웹 사이트에서는 쿠키를 사용하여 사용자가 장바구니에 어떤 상품을 담았는지 파악합니다. 또한 인증 쿠키(아래 참조)와 같이 보안을 위해 필요한 쿠키도 있습니다.</p>
<p>인터넷에서 사용되는 쿠키를 &quot;HTTP 쿠키&quot;라고도 합니다.대부분의 웹과 마찬가지로 쿠키는 HTTP 프로토콜을 사용하여 전송됩니다.</p>
<h2 id="쿠키는-어디">쿠키는 어디?</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/1af22028-4c17-467a-b139-8b865546e6a6/image.png" alt=""></p>
<ul>
<li>크롬의 개발자도구 - F12
웹 브라우저는 사용자 기기의 지정된 파일에 쿠키를 저장합니다.예를 들어, Google Chrome 웹 브라우저는 모든 쿠키를 &quot;Cookies&quot;라는 파일에 저장합니다.Chrome 사용자는 개발자 도구를 열고 &quot;애플리케이션&quot; 탭을 클릭한 다음 왼쪽 메뉴에서 &quot;쿠키&quot;를 클릭하여 브라우저에 저장된 쿠키를 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/2_minus/post/896a61a2-27d9-48a7-80a8-13a3c07a3dfe/image.png" alt=""></li>
<li>구성요소<ul>
<li><strong>Name</strong> (이름): 쿠키를 구별하는 데 사용되는 키 (중복될 수 없음)</li>
<li><strong>Value</strong> (값): 쿠키의 값</li>
<li>Domain (도메인): 쿠키가 저장된 도메인</li>
<li>Path (경로): 쿠키가 사용되는 경로</li>
<li>Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제됩니다.)</li>
</ul>
</li>
</ul>
<h2 id="왜-쿠키">왜 쿠키?</h2>
<h4 id="사용자-세션">사용자 세션</h4>
<p>쿠키는 웹 사이트 활동을 특정 사용자와 연결하는 데 도움이 됩니다.세션 쿠키에는 사용자 세션과 해당 사용자의 관련 데이터 및 콘텐츠를 일치시키는 고유 문자열(문자와 숫자의 조합)이 포함되어 있습니다.</p>
<p>Alice가 쇼핑 웹 사이트에 계정이 있다고 가정해 보겠습니다. Alice가 웹 사이트 홈페이지에서 자신의 계정에 로그인합니다. Alice가 로그인하면 웹 사이트 서버에서 세션 쿠키가 생성되고 이 쿠키가 Alice의 브라우저로 전송됩니다. 이 쿠키에서 웹 사이트에 Alice의 계정 콘텐츠를 로드하도록 지시되므로 이제 홈페이지에 &quot;환영합니다, Alice&quot;라는 메시지가 표시됩니다.</p>
<p>그런 다음 Alice는 청바지 한 켤레가 표시된 제품 페이지를 클릭합니다. Alice의 웹 브라우저에서 청바지 제품 페이지에 대한 HTTP 요청이 웹 사이트로 전송되면 요청에 Alice의 세션 쿠키가 포함됩니다. 웹 사이트에 이 쿠키가 있으므로 사용자가 Alice로 인식되고 새 페이지가 로드될 때 다시 로그인할 필요가 없습니다.</p>
<h4 id="개인화">개인화</h4>
<p>쿠키가 웹 사이트에서 사용자 행동 또는 사용자 기본 설정이 &quot;기억&quot;되는 데 도움이 되므로 웹 사이트에서 사용자 경험이 맞춤화될 수 있습니다.</p>
<p>Alice가 쇼핑 웹 사이트에서 로그아웃하면 사용자 이름이 쿠키에 저장되어 웹 브라우저로 전송될 수 있습니다. 다음에 해당 웹 사이트를 로드할 때 웹 브라우저에서는 이 쿠키가 웹 서버로 전송되고, 웹 서버에는 Alice에게 지난번에 사용한 사용자 이름으로 로그인하라는 메시지가 표시됩니다.</p>
<h4 id="추적">추적</h4>
<p>일부 쿠키에는 사용자가 방문한 웹 사이트가 기록됩니다.이 정보는 다음에 브라우저가 해당 서버에서 콘텐츠를 로드할 때 쿠키를 생성한 서버로 전송됩니다.타사 추적 쿠키를 사용하면 브라우저가 해당 추적 서비스를 사용하는 웹 사이트를 로드할 때마다 이 프로세스가 수행됩니다.</p>
<p>Alice가 이전에 브라우저에 추적 쿠키를 전송한 웹 사이트를 방문한 적이 있는 경우, 이 쿠키에는 Alice가 현재 청바지 제품 페이지를 보고 있다는 사실이 기록될 수 있습니다. 다음에 Alice가 이 추적 서비스를 사용하는 웹 사이트를 로드할 때 청바지 광고가 표시될 수 있습니다.</p>
<h1 id="세션">세션</h1>
<p><img src="https://velog.velcdn.com/images/2_minus/post/17d9d8f1-f279-49e8-a051-2156a03b40a1/image.png" alt=""></p>
<h2 id="세션이란">세션이란?</h2>
<ul>
<li>클라이언트로부터 오는 일련의 요청을 하나의 상태로 보고 그 상태를 일정하게 유지하는 기술</li>
<li>클라이언트가 웹 서버에 접속해있는 상태가 하나의 단위</li>
</ul>
<p>세션은 웹서버에 웹 컨테이너의 상태를 유지하기 위한 정보를 저장합니다. 브라우저를 닫거나 서버에서 세션을 삭제하면 세션이 삭제됩니다. 세션은 각 클라이언트의 고유세션 ID를 부여하는데, 이것으로 클라이언트를 구분하여 각 클라이언트의 요구에 맞는 응답을 반환합니다.</p>
<h2 id="세션은-어떻게">세션은 어떻게?</h2>
<ol>
<li><p>클라이언트 요청</p>
</li>
<li><p>Request-Header 필드의 Cookie 에서 세션ID를 보냈는지 확인</p>
</li>
<li><p>세션ID가 없을 경우, 서버에서 생성하여 클라이언트에게 전송</p>
</li>
<li><p>쿠키를 사용해 세션ID를 서버에 저장</p>
</li>
<li><p>클라이언트 재접속 시, 쿠키를 이용하여 세션ID 값을 서버에 전달</p>
</li>
</ol>
<h2 id="쿠키-vs-세션">쿠키 vs 세션</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/f19772ac-c980-4d9e-9bdc-4214fb5b610c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증과 인가 (1) : web에서의 인증과 인가]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-1-web%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@2_minus/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80-1-web%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Tue, 21 May 2024 21:01:28 GMT</pubDate>
            <description><![CDATA[<h2 id="인증과-인가란">인증과 인가란?</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/59196871-797c-4d05-a144-0fa065d66132/image.png" alt=""></p>
<h3 id="인증--authentication">인증 : Authentication</h3>
<p>사용자의 신원을 검증하는 프로세스를 의미.
ID와 PW로 로그인하는 행위 자체.</p>
<h3 id="인가--authorization">인가 : Authorization</h3>
<p>사용자의 권한을 확인해 접근 가능 영역을 확인하는 절차.
로그인을 통해 자신의 정보 페이지에 접근하는 권한을 획득하는 것.</p>
<p><img src="https://velog.velcdn.com/images/2_minus/post/63dc38d1-d7f3-4be3-8823-6d918adb2e0c/image.png" alt=""></p>
<h3 id="web에서의-인증과-인가">web에서의 인증과 인가</h3>
<p>웹 사이트는 HTTP 통신 위에서 동작합니다. 때문에 웹 사이트 내의 모든 요청과 응답은 비연결성(Connectionless)과 무상태(stateless)한 특성을 가지죠. 즉, 서버에서 Client의 이전 상태를 기억하고 있지 않습니다.</p>
<pre><code>비연결성(Connectionless)은 서버와 클라이언트가 연결되어 있지 않다는 것 입니다.
채팅이나 게임 같은 것들을 하지 않는 이상 서버와 클라이언트는 실제로 연결되어 있지 않습니다.
그 이유는 리소스를 절약하기 위해서 인데, 만약 서버와 클라이언트가 실제로 계속 연결되어있다면 
클라이언트는 그렇다고 쳐도, 서버의 비용이 기하급수적으로 늘어나기 때문입니다.
그래서 서버는 실제로 하나의 요청에 하나의 응답을 내버리고 연결을 끊어버리고있다 라고 생각하시면 좋습니다.

무상태(Stateless)는 서버가 클라이언트의 상태를 저장하지 않는다는 것입니다.
기존의 상태를 저장하는 것들도 마찬가지로 서버의 비용과 부담을 증가시키는 것 이기 때문에 
기존의 상태가 없다고 가정하는 프로토콜을 이용해 구현되어 있습니다.
실제로 서버는 클라이언트가 직전에, 혹은 그 전에 어떠한 요청을 보냈는지 관심도 없고 전혀 알지 못합니다.</code></pre><p>HTTP의 무상태라는 특성을 인증과 함께 생각해보면 로그인을 통해 인증을 거쳐도 이후 요청에서는 이전의 인증된 상태를 유지하지 않게 됩니다. 이러한 상황에서 웹 사이트를 이용하려면 인증/인가가 필요한 모든 상황에서 사용자는 반복적으로 ID/PW를 입력해야 하는 불상사가 생기게 되겠죠.</p>
<h2 id="web에서의-구현">web에서의 구현</h2>
<h3 id="쿠키-세션-방식의-인증">쿠키-세션 방식의 인증</h3>
<p><img src="https://velog.velcdn.com/images/2_minus/post/27973ecb-1202-4b41-9115-a775422e196c/image.png" alt=""></p>
<blockquote>
<p>쿠키-세션 방식은 서버가 ‘특정 유저가 로그인 되었다’는 상태를 저장하는 방식입니다. 인증과 관련된 아주 약간의 정보만 서버가 가지고 있게 되고 유저의 이전 상태의 전부는 아니더라도 인증과 관련된 최소한의 정보는 저장해서 로그인을 유지시킨다는 개념입니다.</p>
</blockquote>
<h3 id="jwt-기반-인증">JWT 기반 인증</h3>
<p><img src="https://velog.velcdn.com/images/2_minus/post/9a94ab63-2891-4374-884e-26dd7ec8dfea/image.png" alt=""></p>
<blockquote>
<p>JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 토큰을 의미합니다. JWT 기반 인증은 쿠키/세션 방식과 유사하게 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[DTO : Data Transfer Object]]></title>
            <link>https://velog.io/@2_minus/DTO-Data-Transfer-Object</link>
            <guid>https://velog.io/@2_minus/DTO-Data-Transfer-Object</guid>
            <pubDate>Mon, 20 May 2024 22:59:42 GMT</pubDate>
            <description><![CDATA[<h2 id="📢-dto-란">📢 DTO 란</h2>
<p><strong>DTO(Data Transfer Object, 데이터 전송 객체)</strong>란 프로세스 간에 데이터를 전달하는 객체를 의미한다. 말 그대로 데이터를 전송하기 위해 사용하는 객체라서 그 안에 비즈니스 로직 같은 복잡한 코드는 없고 순수하게 전달하고 싶은 데이터만 담겨있다. 아래의 그림을 통해 DTO는 주로 클라이언트와 서버가 데이터를 주고받을 때 사용하는 객체임이 보여진다.
<img src="https://velog.velcdn.com/images/2_minus/post/ae71cd94-a10c-429a-b43f-35153e9c7930/image.png" alt=""></p>
<h2 id="📢-dto와-entity의-분리">📢 DTO와 Entity의 분리</h2>
<p>DTO가 왜 필요할까?</p>
<ul>
<li>그냥 서버 안에서 대상이 되는 Entity 객체를 직접 Get, Set 해도 되는거 아닐까?</li>
</ul>
<p>그래도 된다. 하지만 그에 따라 Entity 객체가 가지는 책임이 커지게 되고, 코드의 규모가 커지면 커질수록 Entity의 수정이나 변경은 아예 불가능하게 되어 버린다. 그리고 Entity 자체를 다루다 보니 만약 Entity에 비밀번호와 같은 민감한 정보가 쉽게 노출되는 경우가 발생해 버린다.</p>
<ul>
<li><p>DTO대신 엔티티를 사용하면 엔티티 구조가 모두 노출될 수 있다.
   DTO를 사용함으로서 엔티티 내부 구현을 캡슐화할 수 있습니다.</p>
</li>
<li><p>클라이언트로 넘겨줘야 할 데이터는 API마다 다를 수 있습니다. 때문에 엔티티를 반환값으로 사용하면 유지보수가 힘들어집니다.</p>
</li>
<li><p>엔티티와 DTO가 항상 동일한 상황이라면 DTO대신 엔티티를 사용해도 됩니다. 하지만 그런 일은 거의 없습니다.</p>
</li>
<li><p>요청 &amp; 응답시마다 DTO를 생성하는 것은 엔티티만 사용했을 경우보다 더 많은 코드를 관리해야 합니다. 
하지만 엔티티만 썼을 때 발생하는 코드 버그들과 유지보수의 난이도를 생각하면 훨씬 값싼 노동입니다.</p>
</li>
<li><p>DTO를 사용하면 클라이언트에 전달해야 할 데이터의 크기를 조절할 수 있습니다. 
엔티티를 반환하면 불필요한 데이터가 클라이언트에 전송될 수 있습니다.</p>
</li>
<li><p>validation, swagger 등의 코드들과 엔티티 코드를 분리할 수 있습니다. 더 깔끔한 코드가 돼서 읽고 관리하기 용이해집니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[일정 관리 앱 서버 만들기 : 구현 전 이것저것]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%A0%84-%EC%9D%B4%EA%B2%83%EC%A0%80%EA%B2%83</link>
            <guid>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EA%B5%AC%ED%98%84-%EC%A0%84-%EC%9D%B4%EA%B2%83%EC%A0%80%EA%B2%83</guid>
            <pubDate>Mon, 20 May 2024 02:07:51 GMT</pubDate>
            <description><![CDATA[<p>본격적인 구현 전, 원활한 개발을 위해 했던 이것저것을 올리는 포스팅</p>
<h2 id="user-case-diagram">User Case Diagram</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/84e0f2a9-c8f1-434a-a555-a0f320bfb008/image.png" alt="">
요구사항에 맞춰 시스템이 어떤 흐름으로 진행되는지에 대한 간단한 스케치.
어떻게 하는지 정확히 알고 있지 않아서 의식의 흐름대로 작성해봤다. </p>
<ul>
<li>사용할 유저와 유저가 이용할 기능</li>
<li>조회(View)시 비밀번호를 제외해서 응답</li>
<li>수정(Update), 삭제(Delete)시 비밀번호가 필요</li>
</ul>
<h2 id="api-명세서">API 명세서</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/256d597b-071f-44f2-a61e-f74b0e1ccc9f/image.png" alt="">
남들것을 보고 만든 API 명세서 id가 필요한 부분은 RequestParam의 형태로 URL에 받는 것으로 구현할 예정이다.</p>
<h2 id="erd">ERD</h2>
<p><img src="https://velog.velcdn.com/images/2_minus/post/b2795001-5ee3-4db3-9299-e3ad6f33923c/image.png" alt="">
유저 테이블을 구현할 것 같진 않지만, Schedule 테이블 하나로는 너무 조촐해서 추가했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[일정 관리 앱 서버 만들기 : 요구사항 분석]]></title>
            <link>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@2_minus/%EC%9D%BC%EC%A0%95-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Thu, 16 May 2024 23:33:54 GMT</pubDate>
            <description><![CDATA[<p>Spring에 입문하면서 학습에 시간을 많이 쓰게 되어서 급하게 시작한 개인과제. 시작은 먼저 요구사항에 대해 알아보면서 흐린 그림을 그려보자.</p>
<h2 id="요구사항-확인--필수-구현-기능">요구사항 확인 : 필수 구현 기능</h2>
<h2 id="공통-조건">공통 조건</h2>
<ol>
<li>일정 작성, 수정, 조회 시 반환 받은 일정 정보에 비밀번호는 제외 되어있습니다. </li>
</ol>
<ul>
<li>POST, PUT, GET : password를 반환하지 않음 -&gt; responseDto에서 제외</li>
</ul>
<ol start="2">
<li>일정 수정, 삭제 시 선택한 일정의 비밀번호와 요청할 때 함께 보낸 비밀번호가 일치할 경우에만 가능합니다.</li>
</ol>
<ul>
<li>PUT, DELETE : password를 검증해야하는 과정 추가 필요</li>
</ul>
<h2 id="1단계">1단계</h2>
<p>기능: 일정 작성
-&gt; CRUD 중 Create-POST 기능 구현</p>
<h3 id="조건">조건</h3>
<ol>
<li><code>할일 제목</code>, <code>할일 내용</code>, <code>담당자</code>, <code>비밀번호</code>, <code>작성일</code>을 저장할 수 있습니다.<ol>
<li>저장된 일정 정보를 반환 받아 확인할 수 있습니다.</li>
</ol>
</li>
</ol>
<ul>
<li>CREATE TABLE &amp; Entity 생성 :
| 식별자 | 할일 제목 | 할일 내용 | 담당자 | 비밀번호 | 작성일 |</li>
</ul>
<h2 id="2단계">2단계</h2>
<p>기능 : 선택한 일정 조회
-&gt; CRUD 중 Read-Get 기능 구현</p>
<h3 id="조건-1">조건</h3>
<ol>
<li>선택한 일정의 정보를 조회할 수 있습니다.</li>
</ol>
<ul>
<li>식별자로 일정 객체 선택 후 읽어오기. (단, 공통 조건에 따라 비밀번호는 안됨)</li>
</ul>
<h2 id="3단계">3단계</h2>
<p>기능: 일정 목록 조회 
-&gt; CRUD 중 Read-Get 기능의 변형</p>
<h3 id="조건-2">조건</h3>
<ol>
<li>등록된 일정 전체를 조회할 수 있습니다.</li>
</ol>
<ul>
<li>findAll 이용</li>
</ul>
<ol start="2">
<li>조회된 일정 목록은 <code>작성일</code> 기준 내림차순으로 정렬 되어있습니다.</li>
</ol>
<ul>
<li>repository-interface 에서 Query Methods 활용</li>
</ul>
<h2 id="4단계">4단계</h2>
<p>기능: 선택한 일정 수정 -&gt; CRUD 중 Update-Put 기능 구현</p>
<h3 id="조건-3">조건</h3>
<ol>
<li>선택한 일정의 <code>할일 제목</code>, <code>할일 내용</code>, <code>담당자</code>을 수정할 수 있습니다.<ol>
<li>서버에 일정 수정을 요청할 때 <code>비밀번호</code>를 함께 전달합니다.</li>
</ol>
</li>
</ol>
<ul>
<li>수정가능 한것은 3가지 필드만, request에 비밀번호까지
비밀번호가 일치하는지에 대한 메서드 필요</li>
</ul>
<ol start="2">
<li>수정된 일정의 정보를 반환 받아 확인할 수 있습니다.</li>
</ol>
<ul>
<li>반환 값은 일정정보 response로 반환</li>
</ul>
<h2 id="5단계">5단계</h2>
<p>기능: 선택한 일정 삭제 -&gt; CRUD 중 Delete-Delete 기능 구현</p>
<h3 id="조건-4">조건</h3>
<ol>
<li>선택한 일정을 삭제할 수 있습니다.<ol>
<li>서버에 일정 삭제를 요청할 때 <code>비밀번호</code>를 함께 전달합니다.</li>
</ol>
</li>
</ol>
<ul>
<li>4단계와 마찬가지로 비밀번호 요청과 검증이 필요</li>
</ul>
<p>지금 포스트를 작성하면서는 5단계까지는 구현이 완료, 다만 html 페이지가 없기 때문에 정확히 실행이 되는지는 알아봐야한다. 
이틀밤을 새면서 학습 및 과제를 하다보니 정신이 없기 때문에 과제 중간중간 작성한 마크다운을 포스팅했다. 이거라도 있어서 너무나 다행이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java Stream (2) : 가공하기]]></title>
            <link>https://velog.io/@2_minus/Java-Stream-2-%EA%B0%80%EA%B3%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@2_minus/Java-Stream-2-%EA%B0%80%EA%B3%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 16 May 2024 01:27:08 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@2_minus/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-14%EC%9D%BC%EC%B0%A8-TIL-Java-Stream-1">Java Stream (1)</a>에서 stream 생성하기를 이어서 생성한 스트림을 가공하는 간단한 메서드들을 정리하는 두번째 포스트.</p>
<h2 id="가공하기">가공하기</h2>
<p>전체 요소 중에서 다음과 같은 API 를 이용해서 내가 원하는 것만 뽑아낼 수 있습니다. 이러한 가공 단계를 중간 작업(intermediate operations)이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있습니다.</p>
<pre><code class="language-java">List&lt;String&gt; names = Arrays.asList(&quot;Eric&quot;, &quot;Elena&quot;, &quot;Java&quot;);</code></pre>
<p>아래 나오는 예제 코드는 위와 같은 리스트를 대상으로 합니다.</p>
<h3 id="filtering">Filtering</h3>
<p>필터(filter)은 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업입니다. 인자로 받는 Predicate 는 boolean 을 리턴하는 함수형 인터페이스로 평가식이 들어가게 됩니다.</p>
<pre><code class="language-java">Stream&lt;T&gt; filter(Predicate&lt;? super T&gt; predicate);</code></pre>
<p>간단한 예제입니다.</p>
<pre><code class="language-java">Stream&lt;String&gt; stream = 
  names.stream()
  .filter(name -&gt; name.contains(&quot;a&quot;));
// [Elena, Java]</code></pre>
<p>스트림의 각 요소에 대해서 평가식을 실행하게 되고 ‘a’ 가 들어간 이름만 들어간 스트림이 리턴됩니다.</p>
<h3 id="mapping">Mapping</h3>
<p>맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해줍니다. 이 때 값을 변환하기 위한 람다를 인자로 받습니다.</p>
<pre><code class="language-java">&lt;R&gt; Stream&lt;R&gt; map(Function&lt;? super T, ? extends R&gt; mapper);</code></pre>
<p>스트림에 들어가 있는 값이 input 이 되어서 특정 로직을 거친 후 output 이 되어 (리턴되는) 새로운 스트림에 담기게 됩니다. 이러한 작업을 맵핑(mapping)이라고 합니다.</p>
<p>간단한 예제입니다. 스트림 내 String 의 toUpperCase 메소드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴합니다.</p>
<pre><code class="language-java">Stream&lt;String&gt; stream = 
  names.stream()
  .map(String::toUpperCase);
// [ERIC, ELENA, JAVA]</code></pre>
<p>다음처럼 요소 내 들어있는 Product 개체의 수량을 꺼내올 수도 있습니다. 각 ‘상품’을 ‘상품의 수량’으로 맵핑하는거죠.</p>
<pre><code class="language-java">Stream&lt;Integer&gt; stream = 
  productList.stream()
  .map(Product::getAmount);
// [23, 14, 13, 23, 13]</code></pre>
<p>map 이외에도 조금 더 복잡한 flatMap 메소드도 있습니다.</p>
<pre><code class="language-java">&lt;R&gt; Stream&lt;R&gt; flatMap(Function&lt;? super T, ? extends Stream&lt;? extends R&gt;&gt; mapper);</code></pre>
<p>인자로 mapper를 받고 있는데, 리턴 타입이 Stream 입니다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야합니다. flatMap 은 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 합니다. 이러한 작업을 플래트닝(flattening)이라고 합니다.</p>
<p>다음과 같은 중첩된 리스트가 있습니다.</p>
<pre><code class="language-java">List&lt;List&lt;String&gt;&gt; list = 
  Arrays.asList(Arrays.asList(&quot;a&quot;), 
                Arrays.asList(&quot;b&quot;));
// [[a], [b]]</code></pre>
<p>이를 flatMap을 사용해서 중첩 구조를 제거한 후 작업할 수 있습니다.</p>
<pre><code class="language-java">List&lt;String&gt; flatList = 
  list.stream()
  .flatMap(Collection::stream)
  .collect(Collectors.toList());
// [a, b]</code></pre>
<p>이번엔 객체에 적용해보겠습니다.</p>
<pre><code class="language-java">students.stream()
  .flatMapToInt(student -&gt; 
                IntStream.of(student.getKor(), 
                             student.getEng(), 
                             student.getMath()))
  .average().ifPresent(avg -&gt; 
                       System.out.println(Math.round(avg * 10)/10.0));</code></pre>
<p>위 예제에서는 학생 객체를 가진 스트림에서 학생의 국영수 점수를 뽑아 새로운 스트림을 만들어 평균을 구하는 코드입니다. 이는 map 메소드 자체만으로는 한번에 할 수 없는 기능입니다.</p>
<h3 id="sorting">Sorting</h3>
<p>정렬의 방법은 다른 정렬과 마찬가지로 Comparator 를 이용합니다.</p>
<pre><code class="language-java">Stream&lt;T&gt; sorted();
Stream&lt;T&gt; sorted(Comparator&lt;? super T&gt; comparator);</code></pre>
<p>인자 없이 그냥 호출할 경우 오름차순으로 정렬합니다.</p>
<pre><code class="language-java">IntStream.of(14, 11, 20, 39, 23)
  .sorted()
  .boxed()
  .collect(Collectors.toList());
// [11, 14, 20, 23, 39]</code></pre>
<p>인자를 넘기는 경우와 비교해보겠습니다. 스트링 리스트에서 알파벳 순으로 정렬한 코드와 Comparator 를 넘겨서 역순으로 정렬한 코드입니다.</p>
<pre><code class="language-java">List&lt;String&gt; lang = 
  Arrays.asList(&quot;Java&quot;, &quot;Scala&quot;, &quot;Groovy&quot;, &quot;Python&quot;, &quot;Go&quot;, &quot;Swift&quot;);

lang.stream()
  .sorted()
  .collect(Collectors.toList());
// [Go, Groovy, Java, Python, Scala, Swift]

lang.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
// [Swift, Scala, Python, Java, Groovy, Go]</code></pre>
<p>Comparator 의 compare 메소드는 두 인자를 비교해서 값을 리턴합니다.</p>
<pre><code class="language-java">int compare(T o1, T o2)</code></pre>
<p>기본적으로 Comparator 사용법과 동일합니다. 이를 이용해서 문자열 길이를 기준으로 정렬해보겠습니다.</p>
<pre><code class="language-java">lang.stream()
  .sorted(Comparator.comparingInt(String::length))
  .collect(Collectors.toList());
// [Go, Java, Scala, Swift, Groovy, Python]

lang.stream()
  .sorted((s1, s2) -&gt; s2.length() - s1.length())
  .collect(Collectors.toList());
// [Groovy, Python, Scala, Swift, Java, Go]</code></pre>
<h3 id="iterating">Iterating</h3>
<p>스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드로는 peek 이 있습니다. ‘peek’ 은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer 를 인자로 받습니다.</p>
<pre><code class="language-java">Stream&lt;T&gt; peek(Consumer&lt;? super T&gt; action);</code></pre>
<p>따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않습니다. 다음처럼 작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있습니다.</p>
<pre><code class="language-java">int sum = IntStream.of(1, 3, 5, 7, 9)
  .peek(System.out::println)
  .sum();</code></pre>
<p><a href="https://futurecreator.github.io/2018/08/26/java-8-streams/">참고 포스트</a></p>
]]></description>
        </item>
    </channel>
</rss>