<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jeasung5_2.log</title>
        <link>https://velog.io/</link>
        <description>하이요</description>
        <lastBuildDate>Fri, 19 Jul 2024 11:53:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jeasung5_2.log</title>
            <url>https://velog.velcdn.com/images/jeasung5_2/profile/3ee6eecc-59b3-4ae7-9f69-b740a15ab1b1/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jeasung5_2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jeasung5_2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2024-07-19 Git ignore]]></title>
            <link>https://velog.io/@jeasung5_2/Git-ignore</link>
            <guid>https://velog.io/@jeasung5_2/Git-ignore</guid>
            <pubDate>Fri, 19 Jul 2024 11:53:33 GMT</pubDate>
            <description><![CDATA[<h4 id="git-ignor란">git ignor란</h4>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/0c0cf4fe-7ad4-473b-a6e8-b31f01008ed0/image.png" alt=""></p>
<p>Git 저장소에서 추적하지 않을 파일이나 디렉토리를 지정하는 설정 파일입니다. Git은 파일 변경 내역을 추적하고 버전을 관리하는데, 때때로 특정 파일이나 디렉토리는 버전 관리에서 제외하고 싶을 때 이용하여 Git이 이 파일들을 무시하도록 설정하게 하는 파일입니다.</p>
<h4 id="왜-사용할까">왜 사용할까?</h4>
<p>Gitignore 파일을 추가하면 Git은 해당 패턴에 맞는 파일이나 디렉토리의 변경 사항을 추적하지 않고, 버전 관리에서 제외합니다. 이는 프로젝트를 깨끗하게 유지하고, 필요 없는 파일이나 개인 정보가 노출되는 것을 방지하는 데 도움을 준다.</p>
<h4 id="작성법">작성법</h4>
<p>각 프로젝트의 요구에 맞게 적절히 작성되어야 하며, 주석은 # 기호로 시작하여 추가</p>
<p>예시) gitignore.io</p>
<pre><code>### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# AWS User-specific
.idea/**/aws.xml

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn.  Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

</code></pre><h4 id="오류">오류</h4>
<p>처음 하다보면 적용이 안된 것 처럼 커밋에 안떠야 되는 파일들이 계속 커밋하라고 나오게된다..
그 이유는 처음 생성되었을때 설정을 지니고 있는 git 캐시가 원인 이다.</p>
<h4 id="해결">해결</h4>
<p>git에 있는 캐시 파일을 다 지우고 커밋을 진행하면 대부분이 해결된다.</p>
<pre><code>git rm -r --cached .
git add .
git commit -m &quot;removed cached&quot;</code></pre><p>이렇게 오늘 프로젝트 레포를 팠을 때 왜 이그노어가 작동하지 않았는지 알 수 있었고
다음에는 이런 오류가 나와도 당황하지 않을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[POSTMAN API 별 자동으로 토큰 넣기]]></title>
            <link>https://velog.io/@jeasung5_2/POSTMAN-API-%EB%B3%84-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%ED%86%A0%ED%81%B0-%EB%84%A3%EA%B8%B0</link>
            <guid>https://velog.io/@jeasung5_2/POSTMAN-API-%EB%B3%84-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%ED%86%A0%ED%81%B0-%EB%84%A3%EA%B8%B0</guid>
            <pubDate>Mon, 15 Jul 2024 04:07:04 GMT</pubDate>
            <description><![CDATA[<p>&lt;2024-07-15&gt;
늘 포스트맨으로 테스트를 할때 일일이 토큰 값을 넣어줘서 테스트를 진행하였는데
이번에 자동으로 넣을 수 있는 정보를 알게 되었다.</p>
<h3 id="설정">설정</h3>
<h4 id="1-토큰을-request하는-api에-scripts를-들어가서-로직을-작성한다">1. 토큰을 REQUEST하는 API에 Scripts를 들어가서 로직을 작성한다.</h4>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/655d6408-93e8-4e12-a439-a7e962540f42/image.PNG" alt=""></p>
<h4 id="2-포스트맨-좌측에-있는-environments-탭에-들어가서-새로운-환경변수-생성">2. 포스트맨 좌측에 있는 Environments 탭에 들어가서 새로운 환경변수 생성</h4>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/322ce355-f356-4ca1-b03c-240f63725629/image.PNG" alt=""></p>
<h4 id="3-api가-담겨있는-폴더에-edit-클릭">3. API가 담겨있는 폴더에 Edit 클릭</h4>
<h4 id="4-authorization-탭에서-설정하기">4. Authorization 탭에서 설정하기</h4>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/cb269b3c-e593-4690-b65f-05638fc51cb6/image.PNG" alt=""></p>
<h4 id="5-그후에-테스트-진행하기">5. 그후에 테스트 진행하기</h4>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/21cd935d-57ea-482d-9572-18deb25b2924/image.png" alt=""></p>
<p>위와 같이 자동으로 설정이 되는것을 볼수 있다.</p>
<p>앞으로 일일이 할 필요 없이 자동으로 지정해주어서 테스트가 한결 편해 질 수 있어서 다행이다....</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-20 오늘의 TIL 아웃소싱(2)]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-21-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%EC%95%84%EC%9B%83%EC%86%8C%EC%8B%B12</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-21-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%EC%95%84%EC%9B%83%EC%86%8C%EC%8B%B12</guid>
            <pubDate>Fri, 21 Jun 2024 07:31:34 GMT</pubDate>
            <description><![CDATA[<h2 id="사용자에-대한-service">사용자에 대한 Service</h2>
<h3 id="userservice">UserService</h3>
<pre><code>@RequiredArgsConstructor
@Service
public class UserService {
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final UserRepository userRepository;
    //주문 리포지토리
    public ResponseEntity&lt;String&gt; signUp(UserDto userDto, UserRoleEnum userRoleEnum) {
        User user = new User(
                userDto.getUsername(),
                bCryptPasswordEncoder.encode(userDto.getPassword()),
                userDto.getNickname(),
                userDto.getUserinfo(),
                userRoleEnum
        );
        userRepository.save(user);
        return ResponseEntity.status(HttpStatus.OK).body(&quot;가입 완료&quot;);
    }


    public ResponseEntity&lt;ProfileDto&gt; getProfile(Long userId) {
        Optional&lt;User&gt; user = userRepository.findById(userId);
        ProfileDto profileDto = new ProfileDto(
                user.get().getNickname(),
                user.get().getUserinfo()
        );
        return ResponseEntity.status(HttpStatus.OK).body(profileDto);
    }


    @Transactional
    public ResponseEntity&lt;String&gt; updateProfile(Long userId, User user, ProfileDto profileDto) {
        if(!validateUser(userId,user.getId())){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(&quot;다른 유저를 수정할 수 없습니다.&quot;);
        }
        if(validatePassword(user,profileDto.getPassword())){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(&quot;최근 3번안에 사용한 비밀번호는 사용할 수 없습니다.&quot;);
        }
        if(bCryptPasswordEncoder.matches(user.getPassword(),profileDto.getPassword())){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(&quot;현제 같은 비밀번호는 변경할수 없습니다.&quot;);
        }
        ProfileDto newProfileDto = new ProfileDto(
                profileDto.getNickname(), profileDto.getUserinfo(), bCryptPasswordEncoder.encode(profileDto.getPassword())
        );
        Optional&lt;User&gt; originUser = userRepository.findById(userId);
        originUser.get().updateProfile(newProfileDto);
        return ResponseEntity.status(HttpStatus.OK).body(&quot;수정완료&quot;);
    }


    public ResponseEntity&lt;String&gt; signOut(Long userId, User user) {
        if(!validateUser(userId,user.getId())){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(&quot;다른 유저를 탈퇴할 수 없습니다.&quot;);
        }
        userRepository.findById(userId).get().deleteUser();
        return ResponseEntity.status(HttpStatus.OK).body(&quot;탈퇴 완료&quot;);
    }

    // 아이디 일치하는지 검증
    private boolean validateUser(Long userId, Long originId) {
        if (!Objects.equals(userId, originId)) {
            return false;
        }
        return true;
    }
    //이전 변경한 비밀번호 검증
    private  boolean validatePassword(User user, String password) {
        List&lt;String&gt; userPassword = user.getDeniedPassword();

        for(int i=0; i&lt;userPassword.size(); i++){
            if(bCryptPasswordEncoder.matches(userPassword.get(i),password)){
                return true;
            }
        }
        return false;
    }
}
</code></pre><h2 id="추가적인-entity-생성을-해야되는-상황발생">추가적인 entity 생성을 해야되는 상황발생</h2>
<h3 id="erd-수정중-정규화에-대해">ERD 수정중 정규화에 대해</h3>
<ul>
<li>정규화
데이터베이스 설계 과정에서 중복 데이터를 줄이고 데이터 무결성을 높이기 위해 데이터 구조를 정리하는 과정</li>
</ul>
<p>주로 제 3정규형 까지 한다고 한다.
예시)
<img src="https://velog.velcdn.com/images/jeasung5_2/post/231a4abf-5c22-47c0-b206-e5c7a79bebae/image.png" alt=""></p>
<h4 id="제1정규형">제1정규형</h4>
<p>테이블의 모든 필드가 원자값(더 이상 나눌 수 없는 값)을 갖도록 보장합니다. 중복된 데이터나 반복되는 그룹이 없어야 함</p>
<p>스프링에서는 주로 하나의 필드에 리스트를 담을 수 없다 그러므로 보통은 1정규형에는 만족함 </p>
<h4 id="제2정규형">제2정규형</h4>
<p>제1정규형을 만족하면서, 기본 키가 아닌 모든 속성이 기본 키에 대해 완전 함수적 종속을 만족하도록 합니다. (부분적 종속성을 제거)</p>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/6a1b9002-5e4b-41ff-b4b6-ab7b1a0501eb/image.png" alt=""></p>
<h4 id="제3정규형">제3정규형</h4>
<p>제2정규형을 만족하면서, 기본 키가 아닌 모든 속성이 기본 키에 대해 이행적 종속을 만족하지 않도록 합니다. (이행적 종속성을 제거)</p>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/eab6f2b9-03c3-491a-8c81-2f0e61ff3255/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-19 오늘의 TIL - User(1)]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-20-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-User1</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-20-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-User1</guid>
            <pubDate>Thu, 20 Jun 2024 07:31:58 GMT</pubDate>
            <description><![CDATA[<h2 id="user관련-crud-만들기">User관련 CRUD 만들기</h2>
<ul>
<li><p>조건</p>
<ul>
<li>username: <code>최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)</code></li>
<li>password: <code>최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9), 특수문자</code></li>
</ul>
</li>
<li><p>회원가입 기능</p>
<ul>
<li>성공<ul>
<li>DB에 중복된 <code>username</code>이 없다면 회원을 저장한다.</li>
<li>클라이언트에 성공 메시지와 상태코드를 반환한다.</li>
<li>응답은 content-type application/json 형식입니다.</li>
<li>회원 권한 부여<ul>
<li>ADMIN<ul>
<li>모든 게시글, 댓글 수정과 삭제 가능</li>
</ul>
</li>
<li>USER<ul>
<li>본인이 작성한 게시글, 댓글에 한하여 수정과 삭제 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>⚠️ 필수 예외처리<ul>
<li>DB에 중복된 <code>username</code>이 이미 존재하는 경우</li>
<li><code>username</code>,<code>password</code> 조건에 맞지 않는 경우</li>
</ul>
</li>
</ul>
<ul>
<li><p>비밀번호 수정 조건</p>
<ul>
<li>비밀번호 수정 시, 본인 확인을 위해 현재 비밀번호를 입력하여 올바른 경우에만 수정할 수 있습니다.</li>
<li>비밀번호는 현재 비밀번호 및 최근 사용한 세 개의 비밀번호와 다르게 설정해야 합니다.<h3 id="controller">Controller</h3>
<pre><code>@RequiredArgsConstructor
@RequestMapping(&quot;/api/user&quot;)
@RestController
public class UserController {
private final UserService userService;
</code></pre></li>
</ul>
<p>@PostMapping(&quot;/sign-up&quot;)
public ResponseEntity<String> signUp(@RequestBody UserDto userDto , UserRoleEnum userRoleEnum) {</p>
<pre><code>return userService.signUp(userDto, userRoleEnum);</code></pre><p>}
@GetMapping(&quot;/{userId}&quot;)
public ResponseEntity<ProfileDto> getProfile(@PathVariable Long userId) {</p>
<pre><code>return userService.getProfile(userId);</code></pre><p>}
@PatchMapping(&quot;/{userId}&quot;)
public ResponseEntity<String> updateProfile(@PathVariable(&quot;userId&quot;) Long userId,</p>
<pre><code>    CustomUserDetails user ,ProfileDto profileDto) {
return userService.updateProfile(userId,user.getUser(),profileDto);</code></pre><p>}
@PostMapping(&quot;/{userId}/sign-out&quot;)
public ResponseEntity<String> signOut(@PathVariable(&quot;userId&quot;) Long userId, CustomUserDetails user) {</p>
<pre><code>return userService.signOut(userId, user.getUser());</code></pre><p>}
}</p>
<pre><code>### Entity
</code></pre></li>
</ul>
</li>
</ul>
<h4 id="user">User</h4>
<pre><code>@Getter
@Entity
@NoArgsConstructor
@Table(name = &quot;users&quot;)
public class User extends Timestamped {

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

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

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

    @Column(nullable = false)
    private String nickname;

    private String userinfo;

    @Column(nullable = false)
    private UserStatusEnum status = UserStatusEnum.ACTIVE;

    @Column(nullable = false)
    private UserRoleEnum role;

    private String refreshToken;
    @ElementCollection(fetch = FetchType.LAZY)
    private List&lt;String&gt; deniedPassword = new ArrayList&lt;&gt;(3);
    @Column
    private boolean expired = false;

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

    // 프로필 저장
    public void updateProfile(ProfileDto profileDto) {
        this.password = profileDto.getPassword();
        this.nickname = profileDto.getNickname();
        this.userinfo = profileDto.getUserinfo();
        setDeniedPassword(profileDto.getPassword());
    }

    //최근 변경한 비밀번호 저장
    private void setDeniedPassword(String password){
        if(deniedPassword.size() &gt; 2){
            deniedPassword.remove(0);
        }
        deniedPassword.add(password);
    }

    //삭제처리 soft delete
    public void deleteUser() {
        this.status = UserStatusEnum.DENIED;
    }
}</code></pre><h4 id="timestamped">Timestamped</h4>
<pre><code>@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)

public abstract class Timestamped {

    // 상속받는 클래스에 모두 creatAt, modifiedAt
    @CreatedDate
    // updatable = 생성 후 변경 불가 여부
    @Column(updatable = false, nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime deletedAt;

    // 탈퇴를 할 때 값 저장.
    protected void setDeletedAt(LocalDateTime now) {
        this.deletedAt = now;
    }
}</code></pre><h3 id="enum">Enum</h3>
<h4 id="role">Role</h4>
<pre><code>public enum UserRoleEnum {
    USER(&quot;user&quot;),
    ADMIN(&quot;admin&quot;);

    private final String userRole;

    UserRoleEnum(String value) {
        this.userRole = value;
    }
}</code></pre><h4 id="status">Status</h4>
<pre><code>public enum UserStatusEnum {
    ACTIVE(&quot;Active&quot;),
    DENIED(&quot;Denied&quot;);

    private final String userStatus;
    UserStatusEnum(String userStatus) {
        this.userStatus = userStatus;
    }
}</code></pre><h3 id="repository">Repository</h3>
<pre><code>@Repository
public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
    Optional&lt;User&gt; findById(Long userId);
}</code></pre><h3 id="dto">DTO</h3>
<h4 id="profiledto">ProfileDTO</h4>
<pre><code>@Getter
public class ProfileDto {
    @NotBlank(message = &quot;필수로 입력해야됩니다.&quot;)
    @Pattern(regexp = &quot;^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-z0-9](?=.*[!@#$%^&amp;*()]).{8,15}$&quot; , message = &quot;비밀번호는 특수문자포함 최소 8자, 최대 15자입니다.&quot;)
    private String password;
    @NotBlank(message = &quot;필수로 입력해야됩니다.&quot;)
    private String nickname;
    private String userinfo;

    public ProfileDto(String nickname, String userinfo) {
        this.nickname = nickname;
        this.userinfo = userinfo;
    }
    public ProfileDto(String nickname, String userinfo, String password) {
        this.nickname = nickname;
        this.userinfo = userinfo;
        this.password = password;
    }
}</code></pre><h4 id="userdto">UserDTO</h4>
<pre><code>@Getter
public class UserDto {
    @NotBlank(message = &quot;필수로 입력해야됩니다.&quot;)
    @Pattern(regexp = &quot;^(?=.*[a-z])(?=.*[0-9])[a-z0-9]{4,10}$&quot; , message = &quot;아이디는 최소 4자, 최대 10자입니다.&quot;)
    private String username;
    @NotBlank(message = &quot;필수로 입력해야됩니다.&quot;)
    @Pattern(regexp = &quot;^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-z0-9](?=.*[!@#$%^&amp;*()]).{8,15}$&quot; , message = &quot;비밀번호는 특수문자포함 최소 8자, 최대 15자입니다.&quot;)
    private String password;
    @NotBlank(message = &quot;필수로 입력해야됩니다.&quot;)
    private String nickname;
    private String userinfo;
    private UserRoleEnum role;

}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-18 오늘의 TIL 코드리뷰,리팩토링]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-18-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-18-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%EC%BD%94%EB%93%9C%EB%A6%AC%EB%B7%B0%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</guid>
            <pubDate>Thu, 20 Jun 2024 06:04:35 GMT</pubDate>
            <description><![CDATA[<h2 id="코드-리뷰">코드 리뷰</h2>
<p>코드의 품질을 높이고, 오류를 발견하며, 코드의 유지보수성을 개선하는 중요한 과정!
코드 리뷰는 팀 협업을 촉진하고 개발자들이 서로의 코드를 이해하도록 도울 수 있다.
그러면 어떻게 코드리뷰를 해야 적절하게 리뷰를 진행했다 할 수 있을지 알아보았다.</p>
<h3 id="방법">방법</h3>
<h4 id="준비">준비</h4>
<p>코드 리뷰의 목적을 명확히 정한다.
(오류 발견, 코드 스타일 준수, 성능 개선, 보안 강화 등의 목적 설정) </p>
<h4 id="코드-이해">코드 이해</h4>
<p>코드가 속한 기능을 이해하는 시간을 가진다. 커밋 컨벤션에 따른
PR(Pull Request) 설명을 읽고 이슈 등을 참고한다.</p>
<h4 id="코드-스타일-및-규칙-확인">코드 스타일 및 규칙 확인</h4>
<p>팀에서 정한 코드 컨벤션에 맞게 작성했는지 확인
(java = Checkstyle 이용해 문제 찾기)</p>
<h4 id="코드가-잘-작성됬는지-확인">코드가 잘 작성됬는지 확인</h4>
<ul>
<li>코드가 읽기 쉽고 이해하기 쉬운지 확인</li>
<li>중복된 코드가 있는지 확인</li>
<li>단일 책임 원칙을 잘 따르고 있는지 확인</li>
<li>캡슐화가 잘 되었는지 확인</li>
</ul>
<h4 id="기능-확인">기능 확인</h4>
<ul>
<li>요구사항을 제대로 구현하고 있는지 확인</li>
<li>예외 처리 로직이 적절하게 구현되어 있는지 확인</li>
<li>성능적으로 최적화할 수 있는 방법이 있는지 확인</li>
</ul>
<h4 id="피드백">피드백</h4>
<ol>
<li>잘 작성된 부분에 대한 피드백</li>
<li>개선이 필요한 부분에 대해 구체적인 방법 피드백</li>
</ol>
<h4 id="리팩토링">리팩토링</h4>
<p>코드 작성자가 피드백을 반영해 수정한 후 다시 검토한다.</p>
<h2 id="리팩토링-1">리팩토링</h2>
<p>코드를 더 이해하기 쉽고 유지보수하기 쉽게 개선하는 과정이다.
기능을 변경하지 않으면서 구조를 개선하는 것인데 어떻게 해야될지 알아보겠다.</p>
<h3 id="방법-1">방법</h3>
<h4 id="사전-준비">사전 준비</h4>
<p>리팩토링 전에 코드의 기능을 보장하기 위해 충분한 테스트를 진행한다.</p>
<h4 id="코드-식별">코드 식별</h4>
<p>중복 코드가 있는지, 메서드가 적절한지, 클래스가 너무 많은 책임을 지고 있는지 등등을 확인해본다.</p>
<h4 id="리팩토링-하기">리팩토링 하기</h4>
<p>긴 메서드를 여러 개의 작은 메서드로 나누거나 필요 없는 메서드를 호출하는 부분에 직접 코드를 삽입하는 등등 여러 리팩토링 기술을 적용시켜 코드를 재 작성 한다.</p>
<h4 id="검증-하기">검증 하기</h4>
<p>리팩토링한 결과를 테스트하여 모든 기능이 정상적으로 작동하는지 확인한다</p>
<h4 id="반복하기">반복하기</h4>
<p>리팩토링은 한 번으로 끝나지 않고, 지속적으로 코드베이스를 개선하는 과정이므로 한번에 끝내지 않고 지속적으로 리팩토링을 진행한다.</p>
<ul>
<li>리팩토링 방법 제공 사이트 :  <a href="https://refactoring.guru/">https://refactoring.guru/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-17 오늘의 TIL - 테스트 코드(1)]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-17-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C1</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-17-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C1</guid>
            <pubDate>Tue, 18 Jun 2024 07:34:48 GMT</pubDate>
            <description><![CDATA[<h2 id="테스트-코드">테스트 코드</h2>
<p>주 목적은 오류를 줄이고 버그에 빠르게 대처하기위해서이다.</p>
<ul>
<li>JUnit</li>
<li>Mockito 등 사용</li>
</ul>
<p>스프링에서는 단위 테스트와 통합테스트가 있다.</p>
<h3 id="작성법">작성법</h3>
<p>Given/When/Then 패턴</p>
<p>Given : 어떠한 데이터가 주어질 때.
When : 어떠한 기능을 실행하면.
Then : 어떠한 결과를 기대한다.</p>
<pre><code>  @Test
  @DisplayName(&quot;TestName&quot;)
  void test() {
      // Given

      // When

      // Then
  }</code></pre><h3 id="단위-테스트">단위 테스트</h3>
<p>단위테스트는 하나의 기능 또는 메서드를 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트</p>
<ul>
<li><p>일반적인 테스트 코드 작성</p>
</li>
<li><p>테스트하고자 하는 부분만 독립적으로 테스트를 진행</p>
</li>
<li><blockquote>
<p>빠른 작성과 문제 여부를 확인</p>
</blockquote>
</li>
</ul>
<h4 id="한계">한계</h4>
<p>독립적인 테스트 즉, 다른 객체와 데이터를 주고 받는 경우에 문제가 발생
기능과 연관된 모듈에서 가짜 데이터, 정해진 반환값을 넣어주어야함</p>
<h3 id="통합-테스트">통합 테스트</h3>
<p>다른 객체들과 데이터를 주고받으며 기능이 수행 될때, 연관된 객체들과 올바르게 동작하는지 검증
단위 테스트와는 달리 주로 연관기능 동작 확인하며 실제 운영 환경과 유사한 조건에서 테스트를 수행한다.</p>
<h4 id="한계-1">한계?</h4>
<p>전반적인 동작과 상호작용을 검증하는 것이기에 엄청나게 긴 시간이 소요되며 에러가 났을 때 어디서 나는 에러인지 감을 잡기가 힘들다.</p>
<h3 id="dto-테스트-해보기">DTO 테스트 해보기</h3>
<pre><code>public class DtoTest implements CommonTest {
    private Validator validator = null;
    @BeforeEach
    public void setUp() {
         validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Nested
    @DisplayName(&quot;보드 dto 테스트&quot;)
    class boardRequestTest{
        @Test
        @DisplayName(&quot;요청 dto 생성 성공&quot;)
        void boardRequestTest_ok(){
            //given
            BoardRequestDto boardRequestDto = new BoardRequestDto();
            boardRequestDto.setTitle(BOARD_TITLE);
            boardRequestDto.setContent(BOARD_CONTENT);

            //when
            Set&lt;ConstraintViolation&lt;BoardRequestDto&gt;&gt; violations = validator.validate(boardRequestDto);

            //then
            assertThat(violations).isEmpty();
        }
        @Test
        @DisplayName(&quot;Dto 제목 비어있을경우&quot;)
        void boardRequestTest_badTitle(){
            //given
            BoardRequestDto boardRequestDto = new BoardRequestDto();
            boardRequestDto.setTitle(null);
            boardRequestDto.setContent(BOARD_CONTENT);

            //when
            Set&lt;ConstraintViolation&lt;BoardRequestDto&gt;&gt; violations = validator.validate(boardRequestDto);

            //then
            assertThat(violations).hasSize(1);
            assertThat(violations).extracting(&quot;message&quot;).contains(&quot;제목을 입력해주세요.&quot;);
        }
        @Test
        @DisplayName(&quot;Dto 내용 비어있을경우&quot;)
        void boardRequestTest_badContent(){
            //given
            BoardRequestDto boardRequestDto = new BoardRequestDto();
            boardRequestDto.setTitle(BOARD_TITLE);
            boardRequestDto.setContent(null);

            //when
            Set&lt;ConstraintViolation&lt;BoardRequestDto&gt;&gt; violations = validator.validate(boardRequestDto);

            //then
            assertThat(violations).hasSize(1);
            assertThat(violations).extracting(&quot;message&quot;).contains(&quot;내용을 입력해주세요.&quot;);
        }
    }
    @Nested
    @DisplayName(&quot;SignRequestDto Test&quot;)
    class signRequestTest{

        @Test
        @DisplayName(&quot;요청 성공&quot;)
        void signRequestTest_ok(){
            //given
            SignupRequestDto signupRequestDto = new SignupRequestDto();
            signupRequestDto.setUsername(USERNAME);
            signupRequestDto.setPassword(PASSWORD);
            signupRequestDto.setNickname(NICKNAME);
            signupRequestDto.setEmail(EMAIL);
            signupRequestDto.setInfo(INFO);

            //when
            Set&lt;ConstraintViolation&lt;SignupRequestDto&gt;&gt; violations = validator.validate(signupRequestDto);

            //then
            assertThat(violations).isEmpty();
        }
        @Test
        @DisplayName(&quot;요청 실패(username)&quot;)
        void signRequestTest_badUsername(){
            //given
            SignupRequestDto signupRequestDto = new SignupRequestDto();
            signupRequestDto.setUsername(&quot;INVALIDNAME&quot;);
            signupRequestDto.setPassword(PASSWORD);
            signupRequestDto.setNickname(NICKNAME);
            signupRequestDto.setEmail(EMAIL);
            signupRequestDto.setInfo(INFO);

            //when
            Set&lt;ConstraintViolation&lt;SignupRequestDto&gt;&gt; violations = validator.validate(signupRequestDto);

            //then
            assertThat(violations).hasSize(1);
            assertThat(violations).extracting(&quot;message&quot;).contains(&quot;사용자 ID는 최소 10글자 이상, 최대 20글자 이하여야 합니다.&quot;);
        }
        @Test
        @DisplayName(&quot;요청 실패(password)&quot;)
        void signRequestTest_badPassword(){
            //given
            SignupRequestDto signupRequestDto = new SignupRequestDto();
            signupRequestDto.setUsername(USERNAME);
            signupRequestDto.setPassword(&quot;sdfsdfsdf12&quot;);
            signupRequestDto.setNickname(NICKNAME);
            signupRequestDto.setEmail(EMAIL);
            signupRequestDto.setInfo(INFO);

            //when
            Set&lt;ConstraintViolation&lt;SignupRequestDto&gt;&gt; violations = validator.validate(signupRequestDto);

            //then
            assertThat(violations).hasSize(1);
            assertThat(violations).extracting(&quot;message&quot;).contains(&quot;대소문자 포함 영문 + 숫자 + 특수문자를 최소 1글자씩 포함합니다. \n비밀번호는 최소 10글자 이상이어야 합니다.&quot;);
        }
        @Test
        @DisplayName(&quot;요청 실패(nickname)&quot;)
        void signRequestTest_badNickname(){
            //given
            SignupRequestDto signupRequestDto = new SignupRequestDto();
            signupRequestDto.setUsername(USERNAME);
            signupRequestDto.setPassword(PASSWORD);
            signupRequestDto.setNickname(null);
            signupRequestDto.setEmail(EMAIL);
            signupRequestDto.setInfo(INFO);

            //when
            Set&lt;ConstraintViolation&lt;SignupRequestDto&gt;&gt; violations = validator.validate(signupRequestDto);

            //then
            assertThat(violations).hasSize(1);
            assertThat(violations).extracting(&quot;message&quot;).contains(&quot;Required Nickname&quot;);
        }


    }
    @Nested
    @DisplayName(&quot;Like Dto Test&quot;)
    class likeDtoTest{
        @Test
        @DisplayName(&quot;Dto 요청 성공&quot;)
        void likeDtoTest_ok(){
            //given
            LikeDto likeDto = new LikeDto();
            likeDto.setContentId(CONTENT_ID);
            likeDto.setContentType(LIKE_TYPE_ENUM.toString());

            //when
            Set&lt;ConstraintViolation&lt;LikeDto&gt;&gt; violations = validator.validate(likeDto);

            //then
            assertThat(violations).isEmpty();
        }
        @Test
        @DisplayName(&quot;Dto 요청 실패(id null)&quot;)
        void likeDtoTest_badId(){
            //given
            LikeDto likeDto = new LikeDto();
            likeDto.setContentId(null);
            likeDto.setContentType(LIKE_TYPE_ENUM.toString());

            //when
            Set&lt;ConstraintViolation&lt;LikeDto&gt;&gt; violations = validator.validate(likeDto);

            //then
            assertThat(violations).hasSize(1);
            assertThat(violations).extracting(&quot;message&quot;)
                    .contains(&quot;Content ID는 필수입니다.&quot;);
        }
        @Test
        @DisplayName(&quot;Dto 요청 실패(type)&quot;)
        void likeDtoTest_badType(){
            //given
            LikeDto likeDto = new LikeDto();
            likeDto.setContentId(CONTENT_ID);
            likeDto.setContentType(&quot;45623&quot;);

            //when
            Set&lt;ConstraintViolation&lt;LikeDto&gt;&gt; violations = validator.validate(likeDto);

            //then
            assertThat(violations).hasSize(1);
            assertThat(violations).extracting(&quot;message&quot;)
                    .contains(&quot;영어만 입력 가능합니다.&quot;);
        }
    }

}
</code></pre><h3 id="entity-test-해보기">Entity Test 해보기</h3>
<pre><code>public class EntityTest implements CommonTest {
    @Nested
    @DisplayName(&quot;User Entity 테스트&quot;)
    class UserEntity {
        User user;
        @BeforeEach
        void setUp() {
            user = CommonTest.user;
        }
        @Test
        @DisplayName(&quot;softDelete Test&quot;)
        void softDeleteTest() {
            //no given
            //when
            user.softDelete();
            //then
            assertEquals(user.getStatus(),DELETED);
            assertNotNull(user.getDeletedAt());
        }
        @Test
        @DisplayName(&quot;updateToken Test&quot;)
        void updateTokenTest() {
            //given
            String refreshToken = &quot;test1234&quot;;
            //when
            user.updateToken(refreshToken);
            //then
            assertEquals(refreshToken, user.getRefreshToken());
        }
        @Test
        @DisplayName(&quot;setExpired Test&quot;)
        void setExpiredTest() {
            //given
            boolean expired = true;
            //when
            user.setExpired(expired);
            //then
            assertEquals(expired, user.isExpired());
        }
    }
    @Nested
    @DisplayName(&quot;Board Entity 테스트&quot;)
    class BoardEntity{
        Board board;
        @BeforeEach
        void setUp() {
            board = new Board();
        }
        @Test
        @DisplayName(&quot;hitsUp Test&quot;)
        void hitsUpTest() {
            //given
            board.setHits(0L);
            //when
            board.hitsUp();
            //then
            assertEquals(1, board.getHits());
        }
        @Test
        @DisplayName(&quot;update Test&quot;)
        void updateTest() {
            //given
            BoardRequestDto boardRequestDto = new BoardRequestDto();
            boardRequestDto.setTitle(&quot;게시물제목&quot;);
            boardRequestDto.setContent(&quot;게시물내용&quot;);
            //when
            board.update(boardRequestDto);
            //then
            assertEquals(&quot;게시물제목&quot;, board.getTitle());
            assertEquals(&quot;게시물내용&quot;, board.getContent());
            assertNotNull(board.getModifiedAt());
        }
    }
    @Nested
    @DisplayName(&quot;Comment Test&quot;)
    class CommentEntity{
        Comment comment;
        @BeforeEach
        void setUp() {
            comment = new Comment();
        }
        @Test
        @DisplayName(&quot;update Test&quot;)
        void updateTest() {
            //given
            CommentRequestDto commentRequestDto = new CommentRequestDto();
            commentRequestDto.setContent(&quot;댓글내용&quot;);
            //when
            comment.update(commentRequestDto);
            //then
            assertEquals(&quot;댓글내용&quot;, comment.getContent());
        }
        @Test
        @DisplayName(&quot;delete Test&quot;)
        void deleteTest() {
            //no given
            //when
            comment.delete();
            //then
            assertNotNull(comment.getDeletedAt());
        }
        @Test
        @DisplayName(&quot;isCommentAuthor Test&quot;)
        void isCommentAuthorTest() {
            //given
            User user = new User();
            Long userId = 1L;
            //when
            boolean answer =  comment.isCommentAuthor(userId);
            //then
            assertTrue(true, String.valueOf(answer));
        }
    }
}
</code></pre><h2 id="회고">회고</h2>
<p>뭔가 검증하는게 더 머리가 아픈 일인거 같다.
엔티티와 DTO 테스트는 그래도 필드 주입하고 예외 처리가 잘 되는지만
체크하면되서 에러가 거의 발생 하지 않았는데
컨트롤러 테스트를 진행하자마자 엄청난 에러 메세지를 보고 있다.
왜 필터관련 에러가 계속 나오는지 알아보아야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-14 오늘의 TIL - AOP]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-14-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-AOP</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-14-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-AOP</guid>
            <pubDate>Sun, 16 Jun 2024 21:23:26 GMT</pubDate>
            <description><![CDATA[<h3 id="aop를-이용한-controller">AOP를 이용한 Controller</h3>
<h4 id="어노테이션">어노테이션</h4>
<ul>
<li>@Aspect : Spring 빈(Bean) 클래스에만 적용 가능<pre><code>@Slf4j  //로그를 출력하기 위해서
@Aspect 
@Component  // 빈 클래스 선언을 하기 위해서</code></pre></li>
<li>@Around: &#39;핵심기능&#39; 수행 전과 후 (@Before + @After)</li>
<li>@Before: &#39;핵심기능&#39; 호출 전 (ex. Client 의 입력값 Validation 수행)</li>
<li>@After:  &#39;핵심기능&#39; 수행 성공/실패 여부와 상관없이 언제나 동작
(try, catch 의 finally() 처럼 동작)</li>
<li>@AfterReturning: &#39;핵심기능&#39; 호출 성공 시 (함수의 Return 값 사용 가능)</li>
<li>@AfterThrowing: &#39;핵심기능&#39; 호출 실패 시.
즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)<h4 id="pointcut">Pointcut</h4>
어드바이스가 실행되는 시기를 제어함
포인트컷 재사용 가능
포인트컷 결합 (combine) 가능</li>
</ul>
<pre><code>@Pointcut(&quot;execution(* com.sparta.areadevelopment.controller.*.*(..))&quot;)
 public void controller() {}</code></pre><h4 id="전체코드">전체코드</h4>
<pre><code>@Slf4j
@Aspect
@Component
public class ControllerLogger {
    @Pointcut(&quot;execution(* com.sparta.areadevelopment.controller.*.*(..))&quot;)
    public void controller() {
    }
    @Around(&quot;controller()&quot;)
    public Object logging(ProceedingJoinPoint joinPoint) throws Throwable {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest();

        String methodName = joinPoint.getSignature().getName();
        Map&lt;String, Object&gt; param = new HashMap&lt;&gt;();

        try{
            param.put(&quot;methodName&quot;, methodName);
            param.put(&quot;logTime&quot;, LocalDateTime.now());
            param.put(&quot;requestURL&quot;, request.getRequestURL());
            param.put(&quot;HTTP_METHOD&quot;, request.getMethod());
        }catch (Exception e){
            log.error(&quot;Error logging&quot;, e);
        }
        log.info(&quot;log info: {}&quot;, param);
        Object result = joinPoint.proceed();
        return result;
        }

}</code></pre><h3 id="aop-적용-후-생긴-에러">AOP 적용 후 생긴 에러</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/50b27cbf-0271-425b-9e70-a357b6553c4c/image.png" alt=""></p>
<p>에러 내용을 보니 commentController 하나만 콕 찝어서 에러가 났다고 알려주고 있었다.
왜 이런 내용을 보냈는지 몰라서 다른 controller들과 비교를 해보니
다른 controller들은 @RequiredArgsConstructor를 사용하였다. 하지만 다른 하나는 
사용하지 않고 생성자 주입을 하였는데 에러가 나지 않은 것이다..</p>
<p>둘의 차이가 뭘까?? 했더니 commentController는 생성자 주입을 private로 생성을 한 것 밖에 차이가 없었다.</p>
<h4 id="왜-이-차이가-에러를-만드는가">왜 이 차이가 에러를 만드는가</h4>
<p>spring2.5이후 부터는 default로 CGLIB을 사용하는데 상속을 통해 프록시를 구현한다
상속은 private를 상속할 수없다...
즉, public이외의 메서드는 AOP가 걸리지 않는다.</p>
<h3 id="해결-후-실행">해결 후 실행</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/9c95e0e2-929e-4148-b9e7-3c2149434fbe/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-13 오늘의 TIL]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-13-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-13-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</guid>
            <pubDate>Fri, 14 Jun 2024 07:38:05 GMT</pubDate>
            <description><![CDATA[<h2 id="과제전-개념-정리">과제전 개념 정리</h2>
<h4 id="목표">목표</h4>
<p> Controller, Service, Entity, DTO 레이어에 맞는 단위 테스트 작성하기</p>
<ul>
<li><strong>Controller 테스트.</strong></li>
<li><strong>Service 테스트.</strong></li>
<li><strong>Mockito를 사용해서 테스트용 객체를 만들기.</strong></li>
</ul>
<h3 id="aop">AOP</h3>
<p>aspect-oriented programming의 약자로 관점 지향 프로그래밍이라고도 말함.
AOP는 프로그램 구조에 대한 또 다른 사고 방식을 제공하여 객체 지향 프로그래밍(OOP)을 보완한다.
OOP에서 모듈화의 주 단위가 &quot;클래스&quot;라고 한다면, AOP에서는 핵심 단위가 &quot;관점&quot;이다.
반복되고 공통적으로 사용되는 부분을 분리함으로써 모듈성을 증가시키는 프로그래밍 방법.</p>
<h3 id="junit5">JUnit5</h3>
<p>Java 언어에서 독립된 단위 테스트(Unit Test)를 지원해 주는 프레임워크</p>
<h3 id="mockito">Mockito</h3>
<p>단위 테스트를 위해 모의 객체를 생성하고 관리하는 데 사용되는 Java 오픈소스 프레임워크를 의미함.
사용하면 실제 객체의 동작을 모방하는 모의 객체(Mock Object)를 생성하여 코드의 ‘특정 부분을 격리’시키고 테스트하기 쉽게 만들어줌.</p>
<h4 id="모의-객체">모의 객체</h4>
<p>실제 사용되는 객체 생성을 대체하기 위해 테스트에 사용되는 객체를 의미.
일반적으로 모의 객체의 변수 값은 null, 0, false와 같은 기본 타입의 값이 반환,
메서드는 기본적으로 null을 구성</p>
<h3 id="테스트-코드-원칙">테스트 코드 원칙</h3>
<h4 id="first">F.I.R.S.T</h4>
<p> 효율적이고 좋은 단위 테스트를 하기 위한 5가지 요소</p>
<ul>
<li><p>Fast
테스트는 빠르게 실행되고 빠르게 결과를 알아야한다.</p>
</li>
<li><p>Isolated / Independent
테스트는 그 자체만으로 실행되어야 한다.
(독립적이어야 하고 다른 테스트에 의존하거나 영향을 주면 안됨)</p>
</li>
<li><p>Repeatable
테스트는 반복 가능해야함.
(몇 번을 진행하든 똑같은 결과가 나와야함)</p>
</li>
<li><p>Self-validating
테스트는 자체 검증이 가능해야한다.
(자체로 통과인지 실패인지 결과가 나와야 하며 이것이 자동으로 이루어져야함)</p>
</li>
<li><p>Thorough / Timely
테스트는 철저하고 적절한 때에 작성해야한다.
실제 코드를 작성하기 바로 전에 작성해야 한다.
(사용자일때 관리자일 때 모두 테스트가 가능해야 하고,
어떤 케이스가 실패(예외나 오류)하는지도 테스트해야함)</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-12 오늘의 TIL jwt(4)]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-12-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-12-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</guid>
            <pubDate>Thu, 13 Jun 2024 07:23:37 GMT</pubDate>
            <description><![CDATA[<h3 id="토큰-재발급-서비스-부분">토큰 재발급 서비스 부분</h3>
<p>하면서 조금씩 전의 코드가 변경되었음..</p>
<h4 id="로그인">로그인</h4>
<pre><code>/**
 * 로그인 인증 관련 서비스
 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService implements LogoutHandler {

    /**
     * 관련 클래스 호출
     */
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final MailManager mailManager;
    private static String magickey=&quot;&quot;;

    /**
     * 로그인 메서드
     * @param username
     * @param password
     * @return
     */
    @Transactional
    public TokenDto login(String username, String password) {
        if (!userRepository.existsByUsername(username)) {
            throw new UsernameNotFoundException(username);
        }
        Optional&lt;User&gt; user = userRepository.findUserByUsernameAndStatus(username, StatusEnum.ACTIVE);

        bCryptPasswordEncoder.matches(password, user.get().getPassword());
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                username,password);
        // false가 활성화임
        user.get().setExpired(false);

        Authentication authentication = authenticationManagerBuilder.getObject()
                .authenticate(authenticationToken);
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        TokenDto tokenDto = tokenProvider.generateToken(authentication);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        user.get().updateToken(tokenDto.getRefreshToken());

        return tokenDto;
    }</code></pre><h4 id="토큰-재발급">토큰 재발급</h4>
<p>리프레시 토큰을 비교하여 새 엑세스 토큰과 리프레시 토큰을 발급함</p>
<pre><code>/**
     * 토큰 재발급 메서드
     * @param refreshToken
     * @return
     */
    @Transactional
    public TokenDto reissue(String refreshToken) {
        Optional&lt;User&gt; user = userRepository.findByRefreshToken(refreshToken);
        if(user!=null &amp;&amp; !user.get().getRefreshToken().equals(refreshToken)){
            throw new RuntimeException(&quot;잘못된 토큰입니다.&quot;);
        }else if(user.get().isExpired()){
            throw new RuntimeException(&quot;폐지된 토큰입니다.&quot;);
        }
        Authentication authentication = tokenProvider.getAuthentication(refreshToken.substring(7));
        TokenDto tokenDto = tokenProvider.generateToken(authentication);
        user.get().updateToken(tokenDto.getRefreshToken());
        return tokenDto;
    }
</code></pre><p>헤더의 토큰을 가져와서 비교해서 새 토큰을 발급하는 매서드
&quot;bearer &quot;이 앞에 붙어 있기 때문에 잘라서 비교하지 않으면 에러가 난다
(공백을 받으면 에러가남)</p>
<h4 id="로그아웃-추가">로그아웃 추가</h4>
<pre><code> /**
     * 로그아웃 메서드
     * @param request
     * @param response
     * @param authentication
     */
    @Transactional
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response , Authentication authentication) {
        String authHeader = request.getHeader(AuthEnum.ACCESS_TOKEN.getValue());

        if (authHeader == null &amp;&amp; !authHeader.startsWith(AuthEnum.GRANT_TYPE.getValue())) {
            throw new RuntimeException(&quot;알수 없는 access token.&quot;);
        }
        String accessToken = authHeader.substring(7);
        String username = tokenProvider.getUsername(accessToken);
        User refreshToken = userRepository.findByUsername(username).orElse(null);
        refreshToken.setExpired(true);
    }</code></pre><p>로그아웃 엑세스 토큰을 검증하고 검증에 성공시 유저아이디를 비교해서 
맞다면 expired 의 값을 true로 변경시켜 로그아웃 처리를 진행한다.</p>
<p>이에 따라 추가적으로 config파일에 추가해본게 있다.</p>
<pre><code>   /**
         *  로그아웃 URL시 호출성공시
         *  SecurityContextHolder를 비움
         */
        http.logout(auth -&gt; auth
                .logoutUrl(&quot;/api/auth/logout&quot;)
                .addLogoutHandler(authService)
                .logoutSuccessHandler(
                        (((request, response, authentication) -&gt; SecurityContextHolder.clearContext()))));
</code></pre><p>시큐리티에서 제공하는 기능인데 url의 메서드가 성공적으로 마쳤을 경우 
SecurityContextHolder를 비워 주는 기능이다.
테스트 해본 결과 정상적으로 다 비워 주지만 http status도 출력이 되지 않았다...
메세지를 보내야 되는 경우에는 다른 방법을 알아봐야겠다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-11 프로젝트 KPT 회고]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-11-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-KPT-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-11-%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 08:07:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/dddab340-b140-4d98-9747-f02cdccf64a7/image.webp" alt=""></p>
<h3 id="keep">Keep</h3>
<ol>
<li>컨벤션</li>
</ol>
<ul>
<li>지정이 잘됬다.</li>
<li>서로 침범하는 부분없이 효율적이였음.</li>
</ul>
<ol start="2">
<li>깃허브</li>
</ol>
<ul>
<li>이슈로 브렌치를 관리해서 편리했음.</li>
</ul>
<ol start="3">
<li>코드</li>
</ol>
<ul>
<li>주석처리가 유지보수적으로 잘 만들어졌다.</li>
<li>enum 관리가 잘되었다.</li>
<li>일관성을 지키려고 노력함.</li>
<li>쿼리문 잘씀.</li>
</ul>
<ol start="4">
<li>협업</li>
</ol>
<ul>
<li><p>본인의 시간을 더 할애하는 마음가짐.</p>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/54f970f1-188b-449e-a7a0-2e1ad7bb081e/image.jpg" alt=""></p>
</li>
</ul>
<h3 id="problem">Problem</h3>
<ol>
<li>컨벤션</li>
</ol>
<ul>
<li>변수명, 클래스명에 대한 약속이 부족했다.</li>
<li>정확한 일정 분리가 안됬음.</li>
<li>주석 방식이 각각 달라서 혼잡함.</li>
</ul>
<ol start="2">
<li>깃허브</li>
</ol>
<ul>
<li>ignore 설정이 부족했음.</li>
<li>코드리뷰를 약속 했지만 시간이 촉박했음.</li>
</ul>
<ol start="3">
<li>코드</li>
</ol>
<ul>
<li>리팩토링 못함</li>
<li>exception 메세지가 각기 다름.</li>
<li>원래 계획된 코드 개발 일정 부분을 못지킴.</li>
<li>불필요한 파일 생성</li>
<li>패키지 분류를 광범위하게 잡았다.</li>
</ul>
<ol start="4">
<li>협업</li>
</ol>
<ul>
<li><p>정확한 역할 분담에 대해 모호함.</p>
</li>
<li><p>너무 성급하게 진행함으로서 판단력이 흐려짐.</p>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/3f5a7ad2-9f88-45e3-a748-4f6c11f07c37/image.jfif" alt=""></p>
</li>
</ul>
<h3 id="try">Try</h3>
<ol>
<li>컨벤션</li>
</ol>
<ul>
<li>문단 관리, 주석하는 방법에 대한 조금더 공부해보자</li>
<li>일정을 정할때 세세하게 꼼꼼하게 정하기</li>
<li>주석방식을 미리 지정하기</li>
</ul>
<ol start="2">
<li>깃허브</li>
</ol>
<ul>
<li>브렌치 관리를 깃플로우 방식으로 해보자.</li>
<li>커밋을 가볍게 머지는 무겁게</li>
<li>defult 브렌치에 있는 ignore 지정파일 삭제</li>
<li>코드 리뷰를 진행한다.</li>
</ul>
<ol start="3">
<li>코드</li>
</ol>
<ul>
<li>리팩토링 하기</li>
<li>exception 메세지 통일하기</li>
<li>메서드 체이닝 방식으로 리턴받아서 구현해보기</li>
<li>지금 로직 외에 다른 여러방법으로 코드를 짜보기</li>
</ul>
<ol start="4">
<li>협업</li>
</ol>
<ul>
<li>명상하기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-04~2024-06-11 프로젝트 정리 보고서]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-042024-06-11-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A0%95%EB%A6%AC-%EB%B3%B4%EA%B3%A0%EC%84%9C</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-042024-06-11-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A0%95%EB%A6%AC-%EB%B3%B4%EA%B3%A0%EC%84%9C</guid>
            <pubDate>Tue, 11 Jun 2024 00:37:04 GMT</pubDate>
            <description><![CDATA[<h1 id="결과-보고서">결과 보고서</h1>
<h2 id="프로젝트">프로젝트</h2>
<h3 id="프로젝트-설명">프로젝트 설명</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/29b25a23-ca68-49f7-b021-b64f58996cb6/image.png" alt="">
프로젝트 : 영역전개</p>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/4f11e396-1a56-4da3-a6cf-22411dd28d8d/image.png" alt=""></p>
<p> 최근 직면한 팬데믹과 사회적 분위기에 더불어 개인의 공간에 대한 가치 인식이 증대되었습니다. 이에 따라, 개인의 취향과 요구를 반영한 공간 디자인의 중요성이 부각되고 있습니다. 이러한 상황을 반영하여, 저희 팀은 &#39;오늘의 집&#39;과 같은 컨셉을 기반으로 한 플랫폼을 구상해보았습니다. 이 플랫폼을 통해서 사용자들이 자신의 인테리어 디자인을 공유하고, 다른 사용자들의 디자인을 참조함으로써 서로의 공간을 재창조할 수 있는 영감을 제공하는 소셜 피드를 개발하는 것을 목표로 하여 개인의 생활 공간을 개선하고, 새로운 디자인 아이디어를 교류할 수 있는 기회를 제공함으로써, 개인 공간의 중요성을 더욱 강화할 것</p>
<h3 id="팀소개">팀소개</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/544b47ec-7bfa-454b-ae8a-a1e6a28114d3/image.jpg" alt=""></p>
<p>못말리는 개발자들
이재성 : 팀장
한호진 : 팀원
이정빈 : 팀원
최재원 : 팀원
조경민 : 팀원</p>
<h2 id="프로젝트-개요">프로젝트 개요</h2>
<h3 id="기술-스택">기술 스택</h3>
<p>IDE : IntelliJ
언어 : Java 17version
DB : mySQL
ORM : JPA
프레임워크 : Spring Boot 3.3.0</p>
<h3 id="컨벤션">컨벤션</h3>
<p>코드 컨벤션
    <a href="https://teamsparta.notion.site/d649c18e3ef3465d9e24c29228765ac4?v=b3189e943118403db06316710b6bb50a&amp;pvs=25">https://teamsparta.notion.site/d649c18e3ef3465d9e24c29228765ac4?v=b3189e943118403db06316710b6bb50a&amp;pvs=25</a></p>
<p>깃허브 커밋 컨벤션
<a href="https://teamsparta.notion.site/Github-Rules-82bd2468f56a479192cddf7772b22d89?pvs=25">https://teamsparta.notion.site/Github-Rules-82bd2468f56a479192cddf7772b22d89?pvs=25</a></p>
<h3 id="와이어프레임">와이어프레임</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/a8a5a5cf-3854-48cf-a2af-7756087f97b8/image.png" alt=""></p>
<p><a href="https://www.figma.com/embed?embed_host=notion&amp;url=https%3A%2F%2Fwww.figma.com%2Fboard%2FVGdSGB9KhqdqdHZY4oXlvE%2F%EC%98%81%EC%97%AD%EC%A0%84%EA%B0%9C%3Fnode-id%3D0-1%26t%3DXZur0aUtoHMZsCRA-0">https://www.figma.com/embed?embed_host=notion&amp;url=https%3A%2F%2Fwww.figma.com%2Fboard%2FVGdSGB9KhqdqdHZY4oXlvE%2F%EC%98%81%EC%97%AD%EC%A0%84%EA%B0%9C%3Fnode-id%3D0-1%26t%3DXZur0aUtoHMZsCRA-0</a></p>
<h3 id="erd-다이어그램">ERD 다이어그램</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/e7af9e98-4f9b-48a5-99b9-721c5ef7cc61/image.png" alt=""></p>
<p><a href="https://www.erdcloud.com/d/DorZ9XX4NctjHJsuD">https://www.erdcloud.com/d/DorZ9XX4NctjHJsuD</a></p>
<h3 id="api">API</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/208a8cfb-d520-4895-9b89-46c18ad760d9/image.png" alt=""></p>
<p><a href="https://www.figma.com/embed?embed_host=notion&amp;url=https%3A%2F%2Fwww.figma.com%2Fboard%2FVGdSGB9KhqdqdHZY4oXlvE%2F%EC%98%81%EC%97%AD%EC%A0%84%EA%B0%9C%3Fnode-id%3D0-1%26t%3DXZur0aUtoHMZsCRA-0">https://www.figma.com/embed?embed_host=notion&amp;url=https%3A%2F%2Fwww.figma.com%2Fboard%2FVGdSGB9KhqdqdHZY4oXlvE%2F%EC%98%81%EC%97%AD%EC%A0%84%EA%B0%9C%3Fnode-id%3D0-1%26t%3DXZur0aUtoHMZsCRA-0</a></p>
<h3 id="프로젝트-수행-추진-일정">프로젝트 수행 추진 일정</h3>
<p>개발 기간
2024.06.04(화) ~ 2024.06.11(화)
2024.06.04 </p>
<ul>
<li>아이디어 회의</li>
<li>코드 컨벤션 및 커밋 컨벤션</li>
<li>ERD, 와이어프레임 작성</li>
<li>API 상세설명 및 명세서 작성</li>
</ul>
<p>2024.06.05</p>
<ul>
<li>작업 분활 및 개발 작업</li>
</ul>
<p>2024.06.06 ~ 2024.06.09</p>
<ul>
<li>개발 작업</li>
</ul>
<p>2024.06.10</p>
<ul>
<li>발생한 에러 처리</li>
<li>readMe 작성</li>
<li>발표준비</li>
</ul>
<p>2024.06.11</p>
<ul>
<li>발표</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/1b6707f4-723d-4978-b672-1f830220d676/image.png" alt=""></p>
<h3 id="조직도-및-업무-분담">조직도 및 업무 분담</h3>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/18833f79-7b1c-43d5-841d-9377500811d6/image.png" alt=""></p>
<h2 id="결과물">결과물</h2>
<h3 id="주요-기능">주요 기능</h3>
<p>게시판 등록</p>
<pre><code>게시판을 작성이 가능하다. 본인만 수정 삭제 가능하다</code></pre><p>게시판 검색</p>
<pre><code>텍스트로 검색하면 텍스트와 일치하는 인테리어에 대한 정보가 사용자에게 반환된다.</code></pre><p>순위별 페이지네이션</p>
<pre><code>조회수와 좋아요 수를 기준으로 순서대로 사용자에게 보여준다.</code></pre><p>사용자 관리</p>
<pre><code>본인의 프로필 설정이 가능하다.</code></pre><p>조회수와 좋아요 기능</p>
<pre><code>유저들이 마음에 드는 게시물을 좋아요 할 수 있다.</code></pre><p>깃허브 링크
<a href="https://github.com/unstoppableDevelopers/Areadevelopment">https://github.com/unstoppableDevelopers/Areadevelopment</a>
시연영상
<a href="https://youtu.be/ywL1QlOgVfU">https://youtu.be/ywL1QlOgVfU</a></p>
<h1 id="유지-보수">유지 보수</h1>
<h2 id="디자인-측면">디자인 측면</h2>
<p>  클라이언트의 요구가 가장 많은 부분으로, 부분 이미지 교체나 페이 지 추가, 계절 분위기 조정, 콘텐츠 변경과 교체, 팝업 등이 해당한다. 디자인팀 영역 -&gt; 모두 백엔드 중점이기 때문에 추후 보수 할 예정이다.</p>
<h2 id="유지-측면">유지 측면</h2>
<p>향후 유사한 프로젝트를 진행하거나 담당자가 교체되더라도 충분히 이해될 수 있도록 관계적으로 위에 기술하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-07 오늘의 TIL -점검]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-07-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%EC%A0%90%EA%B2%80</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-07-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-%EC%A0%90%EA%B2%80</guid>
            <pubDate>Mon, 10 Jun 2024 07:41:47 GMT</pubDate>
            <description><![CDATA[<h4 id="프로젝트로-인한-til-휴식">프로젝트로 인한 TIL 휴식</h4>
<p> 오늘은 바쁜 나머지 정신 없이 하면서 짰던 UserDeteils를 커스텀해서 다루는 로직을 알아보겠다.</p>
<h4 id="간단한-userdeteils-커스텀">간단한 UserDeteils 커스텀</h4>
<ul>
<li>CustomUserDetails</li>
</ul>
<pre><code>@Getter
public class CustomUserDetails implements UserDetails {
    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }
//    /**
//     * 해당 유저의 권한 목록
//     */
    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        Collection&lt;GrantedAuthority&gt; authorities = new ArrayList&lt;GrantedAuthority&gt;();
        authorities.add(() -&gt; &quot;ROLE_USER&quot;);
        return authorities;
    }

 //    /**
 //     *  이후의 필드들도 오버라이딩으로 주입
 //     */</code></pre><ul>
<li>CustomUserDetailsService</li>
</ul>
<pre><code>@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    @Transactional
    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username).orElseThrow(() -&gt; new UsernameNotFoundException(username));

        return new CustomUserDetails(user);

    }


}</code></pre><ul>
<li><p>SecurityConfig</p>
<pre><code>    @Bean  빈주입
  public AuthenticationManager
  authenticationManager(AuthenticationConfiguration     
  authenticationConfiguration) throws Exception {

  return authenticationConfiguration.getAuthenticationManager();

  }</code></pre><ul>
<li>TokenProvider</li>
</ul>
<pre><code>@Slf4j
@Component
public class TokenProvider {
  private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;            // 30분
  private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 14;// 2주
  String token = AuthEnum.GRANT_TYPE.getValue();
  private final Key key;
  private CustomUserDetailsService detailsService;

  public TokenProvider(@Value(&quot;${JWT_SECRET_KEY}&quot;) String secretKey , CustomUserDetailsService detailsService) {
      byte[] keyBytes = Decoders.BASE64.decode(secretKey);
      this.key = Keys.hmacShaKeyFor(keyBytes);
      this.detailsService = detailsService;
  }

  /**
   * 유저 정보를 통해 토큰 생성
   */
  public TokenDto generateToken(Authentication authentication) {
      log.info(&quot;generateToken start&quot;);

      long now = (new Date()).getTime();
      Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME); // 30분
      Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME); // 14일

      String accessToken = token + Jwts.builder()
              .setSubject(authentication.getName())
              .claim(&quot;auth&quot;, &quot;USER&quot;)
              .setExpiration(accessTokenExpiresIn)
              .signWith(key, SignatureAlgorithm.HS256)
              .setIssuedAt(new Date(now))
              .compact();

      log.info(accessToken);

      String refreshToken = token + Jwts.builder()
              .setExpiration(refreshTokenExpiresIn)
              .signWith(key, SignatureAlgorithm.HS256)
              .setIssuedAt(new Date(now))
              .setSubject(authentication.getName())
              .compact();

      log.info(&quot;accessToken: {}&quot;, accessToken);
      return TokenDto.builder()
              .grantType(&quot;Bearer&quot;)
              .accessToken(accessToken)
              .refreshToken(refreshToken)
              .build();

</code></pre></li>
</ul>
<pre><code>}

/**
 * 토큰에서 유저 정보 추출
 */
public Authentication getAuthentication(String token) {
    String username = parseClaims(token).getSubject();
    CustomUserDetails userDetails = detailsService.loadUserByUsername(username);
    return new UsernamePasswordAuthenticationToken(userDetails,&quot;&quot;, userDetails.getAuthorities());
}



/**
 * 토큰 정보 검증
 */
public boolean validateToken(String token) {
    log.info(&quot;validateToken start&quot;);
    log.info(&quot;token: {}&quot;, token);
    try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
        log.info(&quot;Invalid JWT Token&quot;, e);
    } catch (ExpiredJwtException e) {
        // refresh token 활용해서 재발급
        log.info(&quot;Expired JWT Token&quot;, e);
        throw e;
    } catch (UnsupportedJwtException e) {
        log.info(&quot;Unsupported JWT Token&quot;, e);
    } catch (IllegalArgumentException e) {
        log.info(&quot;JWT claims string is empty.&quot;, e);
    }
    return false;
}

private Claims parseClaims(String token) {
    try {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    } catch (ExpiredJwtException e) {
        return e.getClaims();
    }
}```</code></pre><h4 id="이것을-이용한-토큰-재발급-로직">이것을 이용한 토큰 재발급 로직</h4>
<ul>
<li>AuthService</li>
</ul>
<pre><code>@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService implements LogoutHandler {
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final MailManager mailManager;


    @Transactional
    // Access Token 리프레시
    public TokenDto reissue(String refreshToken) {
        Optional&lt;User&gt; user = userRepository.findByRefreshToken(refreshToken);
        if(user!=null &amp;&amp; !user.get().getRefreshToken().equals(refreshToken)){
            throw new RuntimeException(&quot;잘못된 토큰입니다.&quot;);
        }else if(user.get().isExpired()){
            throw new RuntimeException(&quot;폐지된 토큰입니다.&quot;);
        }

        Authentication authentication = tokenProvider.getAuthentication(refreshToken.substring(7));
//        String resolveToken = resolveToken(user.get().getRefreshToken());


        TokenDto tokenDto = tokenProvider.generateToken(authentication);
        user.get().updateToken(tokenDto.getRefreshToken());

        return tokenDto;
    }

    @Transactional
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response , Authentication authentication) {
        String authHeader = request.getHeader(AuthEnum.ACCESS_TOKEN.getValue());

        if (authHeader == null &amp;&amp; !authHeader.startsWith(AuthEnum.GRANT_TYPE.getValue())) {
            throw new RuntimeException(&quot;알수 없는 access token.&quot;);
        }
        String accessToken = authHeader.substring(7);
        String username = tokenProvider.getUsername(accessToken);
        User refreshToken = userRepository.findByUsername(username).orElse(null);

        refreshToken.setExpired(true);

    }

</code></pre><ul>
<li>AuthController</li>
</ul>
<pre><code>@PostMapping(&quot;/reissue&quot;)
    public ResponseEntity&lt;String&gt; reissue(HttpServletRequest request,HttpServletResponse response) {
        String refreshToken = request.getHeader(&quot;refresh-token&quot;);
        TokenDto token = authService.reissue(refreshToken);
        response.setHeader(AuthEnum.ACCESS_TOKEN.getValue(), token.getAccessToken());
        response.setHeader(AuthEnum.REFRESH_TOKEN.getValue(), token.getRefreshToken());
        return ResponseEntity.ok(&quot;재발급완료&quot;);
    }
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[오늘의 TIL 2024-06-05 jwt(3)]]></title>
            <link>https://velog.io/@jeasung5_2/%EC%98%A4%EB%8A%98%EC%9D%98-TIL-2024-06-05-jwt3</link>
            <guid>https://velog.io/@jeasung5_2/%EC%98%A4%EB%8A%98%EC%9D%98-TIL-2024-06-05-jwt3</guid>
            <pubDate>Fri, 07 Jun 2024 07:21:50 GMT</pubDate>
            <description><![CDATA[<h3 id="스프링-시큐리티를-이용한-로그인-토큰-재발급-1">스프링 시큐리티를 이용한 로그인 토큰 재발급 (1)</h3>
<ul>
<li>mySQL</li>
<li>스프링 부트</li>
<li>인텔리제이</li>
</ul>
<h4 id="진행과정">진행과정</h4>
<ul>
<li><p>SecurityConfig</p>
</li>
<li><p>SecurityProvider</p>
</li>
<li><p>JwtFilter</p>
</li>
<li><p>Entity</p>
</li>
<li><p>Controller</p>
</li>
<li><p>Service</p>
</li>
<li><p>UserDetails</p>
</li>
<li><p>Repository</p>
<h4 id="예외상황">예외상황</h4>
</li>
<li><p>환경변수를 잘못 입력하면 에러가남</p>
</li>
<li><p>클라이언트 요청 할때 필터를 제일 먼저 거쳐가기때문에 인가 설정을 잘해놓아야됨</p>
<h4 id="securityconfig">SecurityConfig</h4>
<pre><code>@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@ComponentScan(basePackages = &quot;com.sparta.areadevelopment.jwt&quot;)
public class SecurityConfig {
private final TokenProvider tokenProvider;
 //암호화
 @Bean
 public BCryptPasswordEncoder passwordEncoder() {
     return new BCryptPasswordEncoder();
 }

 @Bean
 public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
     //csrf disable
     http
             .csrf(AbstractHttpConfigurer::disable);
     //폼을통한 로그인 방식 disable
     http
             .formLogin(AbstractHttpConfigurer::disable);
     //http basic 인증방식 disable
     http
             .httpBasic(AbstractHttpConfigurer::disable);
     //경로 별 인가
     http
             .authorizeHttpRequests((auth) -&gt;auth
                             .requestMatchers(&quot;/auth/reissue&quot;,&quot;/**&quot;,&quot;/auth/login&quot;).permitAll()
                             .anyRequest().authenticated()
                     );
     http
             .addFilterBefore(new JwtAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);

     //세션 jwt를 통해 인증 인가를 위해 stateless 상태 설정
     http
             .sessionManagement((session) -&gt;session
                     .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

     return http.build();
 }
}</code></pre><h4 id="securityprovider">SecurityProvider</h4>
<pre><code>@Slf4j
@Component
public class TokenProvider {
 private static final String AUTHORITIES_KEY = &quot;auth&quot;;
 private static final String BEARER_TYPE = &quot;Bearer&quot;;
 private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;            // 30분
 private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 14;  // 2주

 private final Key key;

 public TokenProvider(@Value(&quot;${JWT_SECRET_KEY}&quot;) String secretKey) {
     byte[] keyBytes = Decoders.BASE64.decode(secretKey);
     this.key = Keys.hmacShaKeyFor(keyBytes);
 }

 /**
  * 유저 정보를 통해 토큰 생성
  */
 public TokenDto generateToken(Authentication authentication) {
     log.info(&quot;generateToken start&quot;);

     long now = (new Date()).getTime();
     Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME); // 30분
     Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME); // 14일

     String accessToken = Jwts.builder()
             .setSubject(authentication.getName())
             .claim(&quot;auth&quot;, &quot;USER&quot;)
             .setExpiration(accessTokenExpiresIn)
             .signWith(key, SignatureAlgorithm.HS256)
             .compact();

     log.info(parseClaims(accessToken).toString());

     String refreshToken = Jwts.builder()
             .setExpiration(refreshTokenExpiresIn)
             .signWith(key, SignatureAlgorithm.HS256)
             .compact();

     log.info(&quot;accessToken: {}&quot;, accessToken);
     return TokenDto.builder()
             .grantType(&quot;Bearer&quot;)
             .accessToken(accessToken)
             .refreshToken(refreshToken)
             .build();
 }

 /**
  * 토큰에서 유저 정보 추출
  */
 public Authentication getAuthentication(String accessToken) {
     Claims claims = parseClaims(accessToken);

     if (claims.get(&quot;auth&quot;) == null) {
         throw new RuntimeException(&quot;권한 정보가 없는 토큰입니다.&quot;);
     }
     log.info(&quot;claims: {}&quot;, claims);
     Collection&lt;? extends GrantedAuthority&gt; authorities =
             Arrays.stream(claims.get(&quot;auth&quot;).toString().split(&quot;,&quot;))
                     .map(SimpleGrantedAuthority::new)
                     .collect(Collectors.toList());

     log.info(&quot;authorities: {}&quot;, authorities);
     UserDetails principal = new User(claims.getSubject(), &quot;&quot;, authorities);
     return new UsernamePasswordAuthenticationToken(principal, &quot;&quot;, authorities);
 }

 /**
  * 토큰 정보 검증
  */
 public boolean validateToken(String token) {
     log.info(&quot;validateToken start&quot;);
     log.info(&quot;token: {}&quot;, token);
     try {
         Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
         return true;
     } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
         log.info(&quot;Invalid JWT Token&quot;, e);
     } catch (ExpiredJwtException e) {
         // refresh token 활용해서 재발급
         log.info(&quot;Expired JWT Token&quot;, e);
         throw e;
     } catch (UnsupportedJwtException e) {
         log.info(&quot;Unsupported JWT Token&quot;, e);
     } catch (IllegalArgumentException e) {
         log.info(&quot;JWT claims string is empty.&quot;, e);
     }
     return false;
 }

 private Claims parseClaims(String accessToken) {
     try {
         return Jwts.parserBuilder()
                 .setSigningKey(key)
                 .build()
                 .parseClaimsJws(accessToken)
                 .getBody();
     } catch (ExpiredJwtException e) {
         return e.getClaims();
     }
 }

 public String getUsername(String refreshToken) {
     Claims claims = Jwts.parserBuilder()
             .setSigningKey(key)
             .build()
             .parseClaimsJws(refreshToken)
             .getBody();
     return claims.getSubject();
 }
}</code></pre></li>
</ul>
<h4 id="jwtfilter">JwtFilter</h4>
<pre><code>@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
    private final TokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);


        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null &amp;&amp; jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    // Request Header 에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (StringUtils.hasText(bearerToken) &amp;&amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }

}</code></pre><h4 id="entity">Entity</h4>
<ul>
<li>테이블 이름은 User로 만들었더니
나중에 UserDetails에서 같은 단어를 쓰게 되어 혼동도 오고 import가 동시에 안되서 불편했음.</li>
</ul>
<pre><code>@Getter
@NoArgsConstructor
@Entity
public class RefreshToken {
    @Id
    @Column(name = &quot;rt_key&quot;)
    private String key;

    @Column(name = &quot;rt_value&quot;)
    private String value;

    @Builder
    public RefreshToken(String key, String value) {
        this.key = key;
        this.value = value;
    }

    public RefreshToken updateValue(String token) {
        this.value = token;
        return this;
    }</code></pre><h4 id="controller">Controller</h4>
<p>@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping(&quot;/auth&quot;)
public class AuthController {
    private final AuthService authService;
    @PostMapping(&quot;/login&quot;)
    public ResponseEntity<String> login(@RequestBody UserLoginRequestDto userLoginRequestDto, HttpServletResponse response) {</p>
<pre><code>    String username = userLoginRequestDto.getUsername();
    String password = userLoginRequestDto.getPassword();
    TokenDto token = authService.login(username,password);
    response.setHeader(&quot;refresh-token&quot;, token.getRefreshToken());
    response.setHeader(&quot;access-token&quot;, token.getAccessToken());
    return ResponseEntity.ok(&quot;로그인 완료!&quot;);
}
@PostMapping(&quot;/reissue&quot;)
public ResponseEntity&lt;String&gt; reissue(HttpServletRequest request,HttpServletResponse response) {
    String refreshToken = request.getHeader(&quot;refresh-token&quot;);
    String accessToken = request.getHeader(&quot;access-token&quot;);
    TokenDto token = authService.reissue(refreshToken,accessToken);
    response.setHeader(&quot;access-token&quot;, token.getAccessToken());
    response.setHeader(&quot;refresh-token&quot;, token.getRefreshToken());
    return ResponseEntity.ok(&quot;재발급완료&quot;);
}</code></pre><h4 id="이어서-다음에는-서비스쪽과-나머지-구현을-완료해-오도록-해보겠다">이어서 다음에는 서비스쪽과 나머지 구현을 완료해 오도록 해보겠다.</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[오늘의 TIL 2024-06-04 협업]]></title>
            <link>https://velog.io/@jeasung5_2/%EC%98%A4%EB%8A%98%EC%9D%98-TIL-2024-06-04-%ED%98%91%EC%97%85</link>
            <guid>https://velog.io/@jeasung5_2/%EC%98%A4%EB%8A%98%EC%9D%98-TIL-2024-06-04-%ED%98%91%EC%97%85</guid>
            <pubDate>Wed, 05 Jun 2024 07:54:45 GMT</pubDate>
            <description><![CDATA[<h2 id="프로젝트-시작-협업-중-알게된-내용">프로젝트 시작 협업 중 알게된 내용</h2>
<h3 id="컨벤션">~~컨벤션</h3>
<p>~~컨벤션이라는 것을 이번에 처음 접하게 되었는데
개발자들이 프로젝트 시작전에 하는 약속을 미리 정하는 것이다. 즉 쉽게 말하자면 이름 짓기 약속이라고 보면 된다.</p>
<p>이번에 하게 된 것은 깃허브 관련 커밋 컨벤션과 코드 컨벤션을 진행해 보았다.</p>
<h4 id="커밋-컨벤션깃허브">커밋 컨벤션(깃허브)</h4>
<p>commit할 때 commit message에 대한 약속을 정하는 것이다.
커밋 컨벤션을 몰라도 commit하는데에는 무리가 없었다. 
하지만 혼자 프로젝트를 하더라도 몇개월 뒤에 다시 보았을때라던가...
협업시에 다른 사람이 보았을 때 혼동없이 한눈에 이해를 할 수 있게 하기위해서 약속을 정하는 것이였다.</p>
<ul>
<li>커밋 메시지 구조</li>
</ul>
<p>커밋 메시지 구조는 크게 3가지로 나뉜다(제목, 본문, 꼬리말)</p>
<p>type: Subject -&gt; 제목<br>(한칸 띄우기)<br>body(생략 가능) -&gt; 본문<br>(한칸 띄우기)<br>footer(생략 가능) -&gt; 꼬리말 </p>
<ul>
<li>제목의 타입 예시</li>
</ul>
<p>Feat:    새로운 기능 추가
Fix:    버그 수정 또는 typo
Refactor:    리팩토링
Comment:    필요한 주석 추가 및 변경
Style:    코드 포맷팅, 세미콜론 누락, 코드 변경이 없는 경우
Test:    테스트(테스트 코드 추가, 수정, 삭제, 비즈니스 로직에 변경이 없는 경우)
Init:    프로젝트 초기 생성
Rename:    파일 혹은 폴더명 수정하거나 옮기는 경우
Remove:    파일을 삭제하는 작업만 수행하는 경우</p>
<ul>
<li>본문의 규칙</li>
</ul>
<p>본문은 한 줄 당 72자 내로 작성한다.
본문 내용은 양에 구애받지 않고 최대한 상세히 작성한다.
본문 내용은 어떻게 변경했는지 보다 무엇을 변경했는지 또는 왜 변경했는지를 설명한다.</p>
<ul>
<li>꼬리말 규칙</li>
</ul>
<p>꼬리말은 optional이고 이슈 트래커 ID를 작성한다.
꼬리말은 &quot;유형: #이슈 번호&quot; 형식으로 사용한다.
여러 개의 이슈 번호를 적을 때는 쉼표(,)로 구분한다.
이슈 트래커 유형은 다음 중 하나를 사용한다.</p>
<ul>
<li>Fixes: 이슈 수정중 (아직 해결되지 않은 경우)</li>
<li>Resolves: 이슈를 해결했을 때 사용</li>
<li>Ref: 참고할 이슈가 있을 때 사용</li>
<li>Related to: 해당 커밋에 관련된 이슈번호 (아직 해결되지 않은 경우)</li>
</ul>
<h4 id="코드-컨벤션">코드 컨벤션</h4>
<p>앞서 ~~컨벤션은 일종의 약속과 같다고 말을했다. 코드 컨벤션은 코딩을 할 때 지켜야할 규칙을 정하는 것이라 보면 될 것 같다.
주로 이름 규칙(Naming Rules) - 변수, 함수, 클래스, 메소드 등,
Boolean 타입의 변수 작명규칙,
들여쓰기 (indent) 등등을 어떻게 작성을 해야되는지 미리 알려주는 것이다.</p>
<p>이번에는 다른 것도 컨벤션에 넣어 보았는데
프로젝트는 스프링과 자바를 사용하는데 다른 사람들의 버전과 다른 것에 의한 충돌이 일어나지 않을까 싶어서 컨벤션에 자바의 버전과 스프링의 버전, MYSQL 버전등등 버전 내용을 추가해서 충돌이 일어나지 않게 약속을 진행 해보았는데 처음 정할때 시간은 오래걸리지만 나중에는 편해질거란 생각이 들었다.</p>
<ul>
<li>소스코드 분석도구 Lint</li>
</ul>
<p>소스코드의 잠재적인 오류를 확인하거나, 코딩 컨벤션을 쉽게 유지할 수 있도록 도와주는 도구</p>
<p>각 언어별 Lint(분석도구)
Python = PyLint
JavaScript = ESLint
Java = CheckStyle 등등 이있다.</p>
<ul>
<li>CheckStyle 사용법(인텔리제이)</li>
</ul>
<ol>
<li>Setting &gt; Plugins 에 들어가서 &#39;checkstyle&#39;을 검색 후 설치
(플러그인에 상단 마켓플레이스 인지 확인)</li>
</ol>
<p>2.RESTART IDE을 통해 Intellij를 재시작 후 Setting &gt; Tools &gt; Checkstyle 로 들어가서 Checkstyle 버전 체크(Google checks 사용하였음)</p>
<ol start="3">
<li><p>Setting &gt; Editor &gt; CodeStyle &gt; java 에 들어가서 scheme 톱니바퀴에 import scheme 클릭</p>
</li>
<li><p>인텔리제이 code style xml 파일 등록</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-06-03 오늘의 TIL - Jwt (2)]]></title>
            <link>https://velog.io/@jeasung5_2/2024-06-03-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-Jwt-2</link>
            <guid>https://velog.io/@jeasung5_2/2024-06-03-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-Jwt-2</guid>
            <pubDate>Tue, 04 Jun 2024 07:36:37 GMT</pubDate>
            <description><![CDATA[<h3 id="기초설정">기초설정</h3>
<h4 id="securityconfig-클래스-기본설정">SecurityConfig 클래스 기본설정</h4>
<p>JWT를 통한 인증/인가를 위해서 세션을 STATELESS 상태로 설정</p>
<pre><code>@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

                //csrf disable
        http
                .csrf((auth) -&gt; auth.disable());

                //From 로그인 방식 disable
        http
                .formLogin((auth) -&gt; auth.disable());

                //http basic 인증 방식 disable
        http
                .httpBasic((auth) -&gt; auth.disable());

                //경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -&gt; auth
                .requestMatchers(&quot;/&quot;).permitAll()
                .requestMatchers(&quot;/admin&quot;).hasRole(&quot;ADMIN&quot;)
                .anyRequest().authenticated());

                //세션 설정
        http
                .sessionManagement((session) -&gt; session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}</code></pre><h4 id="bcryptpaasswordencoder-등록">BCryptPaasswordEncoder 등록</h4>
<pre><code>@Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }</code></pre><h4 id="entity-작성-user">Entity 작성: user</h4>
<pre><code>@Entity
@Setter
@Getter
public class User {

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

    private String username;
    private String password;
    private String nickname;

    private String role;
}</code></pre><h4 id="repository-작성">Repository 작성</h4>
<pre><code>public interface UserRepository extends JpaRepository&lt;User, Integer&gt; {

}</code></pre><h4 id="applicationproperties-설정">application.properties 설정</h4>
<pre><code>spring.application.name=demo
spring.datasource.url=jdbc:{}
spring.datasource.username={}
spring.datasource.password={}

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver


spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.use_sql_comments=true

spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.generate-ddl=false
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

jwt.secret.key={}
jwt.expiration = {}</code></pre><h3 id="dto-controller-service-작성">Dto, Controller, Service 작성</h3>
<h4 id="dto">Dto</h4>
<pre><code>@Setter
@Getter
public class LoginDTO {

    private String username;
    private String password;
}</code></pre><h4 id="controller">Controller</h4>
<pre><code>@Controller
@ResponseBody
public class LoginController {

    private final JoinService joinService;

    public JoinController(LoginService loginService) {

        this.loginService = loginService;
    }

    @PostMapping(&quot;/login&quot;)
    public String joinProcess(LoginDTO loginDTO) {

        System.out.println(loginDTO.getUsername());
        joinService.joinProcess(loginDTO);

        return &quot;ok&quot;;
    }
}</code></pre><h4 id="service">Service</h4>
<pre><code>@Service
public class JoinService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public JoinService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {

        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    public void loginProcess(LoginDTO loginDTO) {

        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();

        Boolean isExist = userRepository.existsByUsername(username);

        if (isExist) {

            return;
        }

        UserEntity data = new UserEntity();

        data.setUsername(username);
        data.setPassword(bCryptPasswordEncoder.encode(password));
        data.setRole(&quot;ROLE_ADMIN&quot;);

        userRepository.save(data);
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[2024-05-31 오늘의 TIL - Jwt (1)]]></title>
            <link>https://velog.io/@jeasung5_2/2024-05-31-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-Jwt-1</link>
            <guid>https://velog.io/@jeasung5_2/2024-05-31-%EC%98%A4%EB%8A%98%EC%9D%98-TIL-Jwt-1</guid>
            <pubDate>Mon, 03 Jun 2024 07:47:52 GMT</pubDate>
            <description><![CDATA[<h3 id="스프링-jwt-시큐리티-구현">스프링 JWT 시큐리티 구현</h3>
<p>MySQL 데이터베이스를 활용 JWT 기반의 인증/인가를 구현하고 회원 정보 저장하기</p>
<ul>
<li>인증 = 로그인</li>
<li>인가 = JWT를 통한 경로별 접근 권한</li>
</ul>
<p>회원가입 : 내부 회원 가입 로직은 세션 방식과 JWT 방식의 차이가 없다.</p>
<p>로그인 (인증) : 로그인 요청을 받은 후 세션 방식은 서버 세션이 유저 정보를 저장하지만 JWT 방식은 토큰을 생성하여 응답한다.</p>
<h4 id="필수-dependencies">필수 Dependencies</h4>
<ul>
<li>Lombok</li>
<li>Spring Web</li>
<li>Spring Security</li>
<li>Spring Data JPA</li>
<li>MySQL Driver</li>
</ul>
<pre><code> // JWT
    compileOnly group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-api&#39;, version: &#39;0.11.5&#39;
    runtimeOnly group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-impl&#39;, version: &#39;0.11.5&#39;
    runtimeOnly group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-jackson&#39;, version: &#39;0.11.5&#39;
</code></pre><h4 id="spring-security-란">Spring Security 란?</h4>
<p>Spring의 보안(인증과 권한, 인가 등)을 담당하는 서블릿 필터의 집합이다.
서블릿 필터는 서블릿 실행 전에 실행되는 클래스들로 토큰 인증을 위해 컨트롤러 메서드의 첫 부분마다 인증 코드를 작성하는 고민을 해결하기 위해 서블릿 필터를 사용</p>
<h4 id="jwt-란">JWT 란?</h4>
<p>전자 서명 토큰 중 하나, 서버가 헤더와 페이로드를 생성한 후 전자 서명을 한다는 특징이 있다.
JSON 형태의 토큰으로 {header}.{payload}.{signature}로 구성되어있고, 토큰 기반 인증이므로 서버가 생성함.</p>
<ul>
<li><p>장점
사용자 인증에 필요한 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소가 필요 없다.
URL  파라미터와 헤더로 사용
수평 스케일이 용이하다
디버깅 및 관리가 편하다.
REST 서비스로 제공이 가능하다</p>
</li>
<li><p>단점
토큰은 클라이언트에 저장되어 데이터베이스에서 사용자 정보를 조작하더라도 토큰에 직접 적용할 수 없다.
더 많은 필드가 추가되면 토큰이 커지게 된다.</p>
</li>
</ul>
<ul>
<li>구성</li>
</ul>
<ol>
<li>Header
typ : Type을 줄인 말로 토큰의 타입
alg : Alogrithm을 줄인 말로 토큰의 서명을 발행하는 데 사용된 해시 알고리즘 종류를 의미함</li>
</ol>
<p>2.Payload
sub : Subject를 줄인 말, 토큰의 주인을 의미 sub는 ID처럼 유일한 식별자여야함.
iss : Issuer를 줄인 말, 토큰을 발행한 주체를 의미한다.
iat : issued at을 줄인 말, 토큰이 발행된 날짜와 시간
exp : expiration을 줄인 말, 토큰이 만료되는 시간</p>
<p>3.Signature
토큰의 유효성 검사에 사용함.</p>
<h4 id="jwt-토큰-발행">JWT 토큰 발행</h4>
<p>토큰 발행 &gt; 로그인 시 토큰 반환 &gt; 토큰을 이용해 API 인증</p>
<ol>
<li><p>JWT 라이브러리 추가(gradle)</p>
</li>
<li><p>토큰 클래스 생성(TokenProvider.java)</p>
</li>
<li><p>토큰을 생성한 후 반환할 DTO 생성(UserDTO.java)</p>
</li>
<li><p>사용자 Controller 생성</p>
</li>
</ol>
<h4 id="spring-security--jwt">Spring Security + JWT</h4>
<ol>
<li>토큰 인증 필터 생성( JwtFilter.java)</li>
</ol>
<p>1) 요청의 헤더에서 Bearer 토큰을 가져온다. 
2) 클래스를 이용해 토큰을 인증한다.
3) 오브젝트에 사용자의 인증 정보를 저장하고 Security에 인증된 사용자를 등록한다. 등록하여 요청을 처리하는 과정에서 사용자가 인증됐는지 여부나 인증된 사용자가 누군지 알 수 있다.</p>
<ol start="2">
<li><p>Spring Security 설정</p>
</li>
<li><p>Controller 인증 적용</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-05-30 오늘의 TIL]]></title>
            <link>https://velog.io/@jeasung5_2/2024-05-30-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</link>
            <guid>https://velog.io/@jeasung5_2/2024-05-30-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</guid>
            <pubDate>Fri, 31 May 2024 02:53:52 GMT</pubDate>
            <description><![CDATA[<h3 id="jwt-인증-관련-테스트중-일어난일">jwt 인증 관련 테스트중 일어난일</h3>
<p>설정을 다 마치고 실행해 테스트 해보았는데 key값을 못받는 일이 발생했다.</p>
<ul>
<li>AuthController</li>
</ul>
<pre><code>
@RestController
@RequestMapping(&quot;/api&quot;)
public class AuthController {

    public static final String AUTHORIZATION_HEADER = &quot;Authorization&quot;;

    private final JwtUtil jwtUtil = new JwtUtil();

    @GetMapping(&quot;/create-cookie&quot;)
    public String createCookie(HttpServletResponse res) {
        addCookie(&quot;Robbie Auth&quot;, res);

        return &quot;createCookie&quot;;
    }

    @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;
    }

    @GetMapping(&quot;/create-jwt&quot;)
    public String createJwt(HttpServletResponse res) {
        // Jwt 생성
        String token = jwtUtil.createToken(&quot;Robbie&quot;, UserRoleEnum.USER);

        // Jwt 쿠키 저장
        jwtUtil.addJwtToCookie(token, res);

        return &quot;createJwt : &quot; + token;
    }

    @GetMapping(&quot;/get-jwt&quot;)
    public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
        // JWT 토큰 substring
        String token = jwtUtil.substringToken(tokenValue);

        // 토큰 검증
        if(!jwtUtil.validateToken(token)){
            throw new IllegalArgumentException(&quot;Token Error&quot;);
        }

        // 토큰에서 사용자 정보 가져오기
        Claims info = jwtUtil.getUserInfoFromToken(token);
        // 사용자 username
        String username = info.getSubject();
        System.out.println(&quot;username = &quot; + username);
        // 사용자 권한
        String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
        System.out.println(&quot;authority = &quot; + authority);

        return &quot;getJwt : &quot; + username + &quot;, &quot; + authority;
    }

    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>JwtUtill</li>
</ul>
<pre><code> // 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분

//    @Value(&quot;${jwt.secret.key}&quot;) // Base64 Encode 한 SecretKey
    private String secretKey = &quot;7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==&quot;;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 로그 설정
    public static final Logger logger = LoggerFactory.getLogger(&quot;JWT 관련 로그&quot;);

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username) // 사용자 식별자값(ID)
                        .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }

    // JWT Cookie 에 저장
    public void addJwtToCookie(String token, HttpServletResponse res) {
        try {
            token = URLEncoder.encode(token, &quot;utf-8&quot;).replaceAll(&quot;\\+&quot;, &quot;%20&quot;); // Cookie Value 에는 공백이 불가능해서 encoding 진행

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
            cookie.setPath(&quot;/&quot;);

            // Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage());
        }
    }

    // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) &amp;&amp; tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        logger.error(&quot;Not Found Token&quot;);
        throw new NullPointerException(&quot;Not Found Token&quot;);
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            logger.error(&quot;Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.&quot;);
        } catch (ExpiredJwtException e) {
            logger.error(&quot;Expired JWT token, 만료된 JWT token 입니다.&quot;);
        } catch (UnsupportedJwtException e) {
            logger.error(&quot;Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.&quot;);
        } catch (IllegalArgumentException e) {
            logger.error(&quot;JWT claims is empty, 잘못된 JWT 토큰 입니다.&quot;);
        }
        return false;
    }

    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}</code></pre><ul>
<li><p>실행결과
<img src="https://velog.velcdn.com/images/jeasung5_2/post/de755611-687a-450c-ba91-3db402f63222/image.png" alt=""></p>
</li>
<li><p>디버그결과</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jeasung5_2/post/4d015414-fd18-4274-a8e6-83087955a5ff/image.png" alt=""></p>
<h4 id="해결-방안">해결 방안</h4>
<p>AuthController의 private final JwtUtil jwtUtil = new JwtUtil(); 문장이 문제였다.</p>
<p>JwtUtil이 @Component을 이용해 빈으로 설정해 스프링에서 이 객체를 생성하는 것으로 만들었는데 컨트롤러에서 또 생성을 해버려서 null로 나온 것이였다. </p>
<ul>
<li>고친 코드</li>
</ul>
<pre><code>@RestController
@RequestMapping(&quot;/api&quot;)
@RequiredArgsConstructor
public class AuthController {
    private final JwtUtil jwtUtil;
</code></pre><p>이미 유틸에서 헤더를
public static final String AUTHORIZATION_HEADER = &quot;Authorization&quot;;
이렇게 필드값을 가지고 있기때문에 불필요한 코드여서 삭제를 하였고 static을 가지고 있기때문에 컨트롤러에서도 불러올 수 있기 때문에 
jwtUtil.AUTHORIZATION_HEADER로 고쳤다.
<img src="https://velog.velcdn.com/images/jeasung5_2/post/00169312-7fcb-4d3d-8b9a-f2562e9ed261/image.png" alt="">
정상적으로 출력되는 모습이다.</p>
<p>런타임 에러가 나지 않더라도 에러가 발생한게 처음이라 많이 당황 했지만 다음에는 이런일은 않일어날 것같다....</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-05-29 오늘의 TIL]]></title>
            <link>https://velog.io/@jeasung5_2/2024-05-29-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</link>
            <guid>https://velog.io/@jeasung5_2/2024-05-29-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</guid>
            <pubDate>Thu, 30 May 2024 07:44:03 GMT</pubDate>
            <description><![CDATA[<h3 id="객체지향-프로그래밍">객체지향 프로그래밍</h3>
<p>객체지향 프로그램이 뭐냐 라는 물음에 딱 정의 하지못해 한번 알아보는 시간을 가졌다.</p>
<h4 id="객체">객체</h4>
<p>객체는 프로그램에서 사용되는 데이터 또는 식별자에 의해 참조되는 공간을 의미하며 값을 저장 할 변수와 작업을 수행 할 메소드를 서로 연관된 것들끼리 묶어서 만든 것을 객체라고한다.
하지만 자바에서는 객체(object)란 클래스의 인스턴스나 배열을 말한다고 정의하지만 쉽게 생각하면 사물이나 실체를 의미 한다고 생각한다. (사람, 자동차, 비행기)</p>
<h4 id="객체-지향-프로그래밍이란">객체 지향 프로그래밍이란?</h4>
<p>프로그램을 객체 단위로 나눠 객체들의 작용을 통해 프로그램을 설계하고 개발하는 것, 
즉 프로그래밍에서 필요한 데이터를 추상화 시켜 상태와 행위를 가진 객체로 만들고, 객체들간의 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다.</p>
<h4 id="장점">장점</h4>
<ul>
<li>재사용이 용이해 실직적으로 코드가 줄어들게된다.</li>
<li>객체 형태로 바로바로 필드와 로직을 받아서 범용적인 사용이 가능하다</li>
<li>캡슐화를 통해 불필요한 정보를 숨김으로서 보안적으로 용이함</li>
</ul>
<h4 id="특징">특징</h4>
<ol>
<li><p>추상화
객체에서 공통된 속성과 행위를 찾아서 타입과 행위를 정의하는 과정</p>
</li>
<li><p>캡슐화
관련이 있는 변수와 함수를 하나의 클래스로 묶고 외부에서 쉽게 접근하지 못하도록 은닉하는 것
(접근제어자 : private)</p>
</li>
<li><p>상속
클래스의 속성과 행위를 하위 클래스에 물려주거나 하위 클래스가 상위 클래스의 속성과 행위를 물려받는 것으로 코드의 재사용이 용이하다.</p>
</li>
<li><p>다형성
변수명, 함수명이 상황에 따라 다른 의미로 해석
하나의 클래스 내부에 같은 이름의 행위를 여러개 정의하거나 상위 클래스의 행위를 하위 클래스에서 재정의하여 사용할 수 있다.</p>
</li>
</ol>
<h4 id="solid-객체-지향-설계-원칙">SOLID (객체 지향 설계 원칙)</h4>
<p>객체지향 프로그래밍에 대해 알아보는 중 원칙이 있다는 것을 알게 되었다.
객체 지향적으로 설계하기 위해 SOLID 라 불리는 다섯 가지 원칙이 있는데</p>
<ol>
<li><p>단일 책임 원칙 (SRP, Single Responsibility Principle)
하나의 클래스는 단 하나의 책임만 가져야 한다.
단일 책임 원칙을 지키지 않을 경우 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향이 갈 수 있다.</p>
</li>
<li><p>개방-폐쇄 원칙 (OCP, Open/Closed Principle)
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
기능을 변경하거나 확장할 수 있으면서 기능을 사용하는 코드는 수정하지 않는다.</p>
</li>
<li><p>리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
상위 타입의 객체를 하위 타입의 객체로 치환해도, 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.</p>
</li>
<li><p>인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
범용 인터페이스 하나보다 클라이언트를 위한 여러 개의 인터페이스로 구성하는 것이 좋다.
인터페이스는 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.
클라이언트가 필요로 하는 인터페이스로 분리함으로써 각 클라이언트가 사용하지 않는 인터페이스에 변경이 있어도 영향을 받지 않도록 만들어야 한다.</p>
</li>
<li><p>의존관계 역전 원칙 (DIP), Dependency Inversion Principle)
추상화에 의존해야지 구체화에 의존하면 안된다.
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되고 저수준 모듈은 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-05-28 오늘의 TIL]]></title>
            <link>https://velog.io/@jeasung5_2/2024-05-28-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</link>
            <guid>https://velog.io/@jeasung5_2/2024-05-28-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</guid>
            <pubDate>Wed, 29 May 2024 07:37:15 GMT</pubDate>
            <description><![CDATA[<h3 id="rdbms-와-nosql">RDBMS 와 NoSQL</h3>
<ul>
<li>Databse
일반적으로 컴퓨터 시스템에 전자 방식으로 저장된 구조화된 정보 또는 데이터의 체계적인 집합을 의미합니다.</li>
<li>DataBase Management System
DBMS사용자와 데이터베이스 사이에서 사용자의 요구에 따라 정보를 생성해 주고 데이터베이스를 관리해 주는 소프트웨어</li>
<li>SQL
Strucured Query Language 관계형 데이터베이스 관리 시스템의 데이터를 관리하기 위해 설계된 특수 목적의 프로그래밍 언어</li>
</ul>
<h4 id="rdbms">RDBMS</h4>
<p>DBMS앞에 R이 붙어 있는데 이 R은(Relational)의 약자로 RDBMS는 관계형 데이터베이스 관리 시스템을 의미함. RDB를 관리하는 시스템이라고 할수 있다.
(구성된 테이블이 다른 테이블들과 관계를 맺고 모여있는 집합체)
테이블간의 관계에서 외래 키를 이용한 테이블 간 Join이 가능하다는 게 RDBMS의 가장 큰 특징.</p>
<p>정해진 스키마에 따라 데이터를 저장하여야 하므로 명확한 데이터 구조를 보장하고 있습니다. 또한 관계는 각 데이터를 중복없이 한 번만 저장할 수 있음.
데이터간의 중복체크를 빡빡하게 하기 때문에 저장속도가 느림.
또한 스키마로 인해 데이터가 유연하지 못해 나중에 스키마가 변경 될 경우 번거롭고 어려움.</p>
<h4 id="nosql">NoSQL</h4>
<p>관계형 데이터베이스가 아닌 다른 형태의 데이터 저장 기술.
또한 NoSQL에서는 RDBMS와는 달리 테이블 간 관계를 정의하지 않음. 데이터 테이블은 그냥 하나의 테이블이며 테이블 간의 관계를 정의하지 않아 일반적으로 테이블 간 Join도 불가능함.</p>
<p>데이터 일관성은 포기하되 비용을 고려하여 여러 대의 데이터에 분산하여 저장하는 Scale-Out을 목표로 등장</p>
<p>데이터의 일관성을 고려하지 않고 바로 DB에 저장하기 때문에 저장속도가 빠름
스키마가 없기 때문에 유연하며 자유로운 데이터 구조를 가질 수 있습니다. 언제든 저장된 데이터를 조정하고 새로운 필드를 추가가능.</p>
<p>하지만 데이터 중복이 발생할 수 있으며 중복된 데이터가 변경 될 경우 수정을 모든 컬렉션에서 수행을 해야 하고 스키마가 존재하지 않기에 명확한 데이터 구조를 보장하지 않으며 데이터 구조 결정이 어려울 수 있다.</p>
<h4 id="어떤걸-사용해야할까">어떤걸 사용해야할까</h4>
<p>사실 둘다 사용하는 조건이 다르고 각각 장단점은 있지만 어떤게 우월하다 이런게 아니기 때문에 환경에 따라 사용을 결정하는게 중요하다.
<img src="https://velog.velcdn.com/images/jeasung5_2/post/898196f6-50d4-4454-a892-c9844cebfa15/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024-05-27 오늘의 TIL]]></title>
            <link>https://velog.io/@jeasung5_2/2024-05-27-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</link>
            <guid>https://velog.io/@jeasung5_2/2024-05-27-%EC%98%A4%EB%8A%98%EC%9D%98-TIL</guid>
            <pubDate>Tue, 28 May 2024 07:59:47 GMT</pubDate>
            <description><![CDATA[<h3 id="서블릿servlet">서블릿(Servlet)</h3>
<p>웹 요청(request)을 처리하고 응답(response)을 생성하는 서버측 컴포넌트 즉,
클라이언트의 요청을 처리하고 그 결과를 반환하는 기술</p>
<h4 id="특징">특징</h4>
<p>웹 서버가 동적인 페이지를 제공할 수 있도록 돕는다.
html을 사용하여 요청에 응답한다.
Java Thread를 이용하여 동작한다.
MVC 패턴에서 Controller로 이용된다.
HTTP 프로토콜 서비스를 지원하는 javax.servlet.http.HttpServlet 클래스를 상속받는다.
UDP보다 처리 속도가 느리다.
HTML 변경 시 Servlet을 재컴파일해야 하는 단점이 있다.
서블릿은 서블릿 컨테이너에서 관리한다.</p>
<h4 id="서블릿-컨테이너ioc">서블릿 컨테이너(IoC)</h4>
<p>서블릿들의 생성, 실행, 파괴를 담당한다.
구현되어 있는 servlet 클래스의 규칙에 맞게 서블릿은 관리해주며 클라이언트에서 요청을 하면 컨테이너는 HttpServletRequest, HttpServletResponse 두 객체를 생성하며 GET,POST여부에 따라 동적인 페이지를 생성하여 응답을 보냅니다.
클라이언트의 요청(Request)을 받아주고 응답(Response)할 수 있게, 웹서버와 소켓으로 통신(ex)톰캣)</p>
<ul>
<li>서블릿 컨테이너의 역할</li>
</ul>
<ol>
<li>웹서버와의 통신 지원</li>
</ol>
<p>서블릿 컨테이너는 서블릿과 웹서버가 손쉽게 통신할 수 있게 해줌</p>
<ol start="2">
<li>서블릿 생명주기(Life Cycle) 관리</li>
</ol>
<p>서블릿 컨테이너는 서블릿의 탄생과 죽음을 관리합니다. 서블릿이 생명을 다 한 순간에는 적절하게 Garbage Collection(가비지 컬렉션)까지 진행하여 편의를 제공함.</p>
<ol start="3">
<li><p>멀티쓰레드 지원 및 관리</p>
</li>
<li><p>선언적인 보안 관리</p>
</li>
</ol>
<p>서블릿 컨테이너를 사용하면 개발자는 보안에 관련된 내용을 서블릿 또는 자바 클래스에 구현해 놓지 않아도 됩니다.</p>
<ul>
<li>서블릿을 다루는 과정</li>
</ul>
<ol>
<li><p>Servlet Request, Servlet Response 객체를 생성합니다.</p>
</li>
<li><p>설정 파일을 참고하여 매핑할 Servlet을 확인합니다.</p>
</li>
<li><p>해당 서블릿 인스턴스 존재의 유무를 확인하여 없으면 init() 메소드(한번만 실행됨)를 호출하여 생성합니다.</p>
</li>
<li><p>Servlet Container에 스레드를 생성하고 service를 실행합니다.</p>
</li>
<li><p>응답을 처리하였으면 distory() 메소드를 실행하여(한번만 실행됨) Servlet Request, Servlet Response 객체를 소멸합니다. 서블릿 소멸 시 Garbage Collection(가비지 컬렉션)이 진행됩니다.</p>
</li>
</ol>
<h4 id="장점">장점</h4>
<p>다양한 설정 방법(XML, Java Config, 어노테이션)을 제공하여, 개발자가 선호하는 방식으로 설정 가능.
Spring의 다른 모듈(Spring Security, Spring Data 등)과 쉽게 통합할 수 있어 기능 확장이 편하다.
외부 요청에 대해 스레드로 응답하기 때문에 프로세스로 응답하는 경우보다 가벼운 편이다.</p>
<h4 id="서블릿의-동작-방식스프링">서블릿의 동작 방식(스프링)</h4>
<ul>
<li>요청 수신
클라이언트로부터 HTTP 요청이 들어오면, 요청은 먼저 DispatcherServlet에 의해 수신됩니다.</li>
<li>핸들러 매핑
DispatcherServlet은 요청 URL을 기반으로 적절한 컨트롤러를 찾기 위해 핸들러 매핑(Handler Mapping)을 사용합니다.</li>
<li>컨트롤러 실행
적절한 컨트롤러가 요청을 처리하고, 결과 데이터를 생성합니다.</li>
<li>뷰 리졸버
컨트롤러는 모델 데이터를 뷰로 전달하기 위해 뷰 이름을 반환합니다. DispatcherServlet은 뷰 리졸버(View Resolver)를 사용해 실제 뷰를 찾습니다.</li>
<li>응답 생성
최종적으로 뷰는 모델 데이터를 사용해 HTML 등의 형식으로 응답을 생성하고, DispatcherServlet은 이를 클라이언트에게 반환합니다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>