<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>johnny_debt.log</title>
        <link>https://velog.io/</link>
        <description>I'm a musician who wants to be a developer.</description>
        <lastBuildDate>Fri, 20 Oct 2023 12:27:16 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>johnny_debt.log</title>
            <url>https://velog.velcdn.com/images/johnny_debt/profile/9a165078-23b1-4365-9d5c-ad66b52651a5/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. johnny_debt.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/johnny_debt" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[MarbleUs Project #4: JPA를 사용하는 Entity: Member
]]></title>
            <link>https://velog.io/@johnny_debt/MarbleUs-Project-4-JPA%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-Entity-Member</link>
            <guid>https://velog.io/@johnny_debt/MarbleUs-Project-4-JPA%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-Entity-Member</guid>
            <pubDate>Fri, 20 Oct 2023 12:27:16 GMT</pubDate>
            <description><![CDATA[<p><em>table of contents</em></p>
<hr>
<h5 id="1-member-앤티티-회원관리-crud">1. Member 앤티티: 회원관리 CRUD</h5>
<h5 id="2-member-앤티티-관련-문제파악-및-개선사항">2. Member 앤티티 관련 문제파악 및 개선사항</h5>
<h5 id="3-회고">3. 회고</h5>
<hr>
<h3 id="1-member-앤티티-회원관리-crud-1">1. Member 앤티티: 회원관리 CRUD</h3>
<p>회원가입을 포함한 관리를 위한 기본 CRUD기능을 구현하였다. 우리 서비스의 게임적인 기능의 특성상 맴버 앤티티에 담기는 내용이 비교적 많고 복잡도도 비교적 높았다. 필수적으로 담겨야 하는 내용으로는 1. email, 2. password, 3. roles, 4. nickname, 5. birth, 6.nationality가 있었고</p>
<p>특이사항으로는 1. 유저가 보드판을 한바퀴 돌때마다 1씩 올라가는 게임 플레이를 위한 level, 2. 유저의 현재 위치를 저장하는 currentLocation Collection 콜럼, 3. 방문 도시 기록을 위한 visitedCities Collection 콜럼, 4. 유저가 배정받은 미션을 저장하기 위한 MemberMission객체를 저장할 myMissions, 5. 유저가 미션을 달성했을때 리워드로서 발급할 Stamp객체를 저장하는 myStamps, 6. 해당 유저가 쓴 블로그 객체를 저장하기 위한 myBlogs, 7. 유저가 쓴 Comment(댓글)를 저장할 myComments, 8. 타 유저들의 블로그를 저장하기 위한 bookmarks 9.팔로잉과 팔로워(맴버간의 조인테이블)를 저장하는 follows와 followers 가 있다.</p>
<p>이를 구현하는데 많은 고민점들과 난관들이 있었지만 그 첫번째는 유저의 위치를 어떻게 저장할 것인가였다. 많은 아이디어를 가지고 많은 고민을 하였다. 그리고 채택한 방법은 부루마블 보드에 위치하는 도시의 이름과 위치정보를 담은 ENUM을 만들어 유저의 위치 정보를 도시앤티티와 분리하여 최대한 가볍게 기록 조회하도록 하는 방법이었다. 하지만 이때 한가지 크게 대두되는 문제가 있었다. 블록의 위치 정보와 해당 하는 도시를 고정적으로 만들면 변경사항들에 있어서 유연하지 못하다는 점이였다. 자세히 설명하자면 가령 도시의 순서를 바꿀때 그 위치 ENUM 전체를 고쳐야 한다는 큰 제약사항이 있었다.</p>
<p>이런 불편 사항을 해소시키기 위해 나는 도시와 블록의 위치를 완전히 분리 시키기로 하였다. 부루마블 블록의 정보를 ENUM으로 관리하되 해당 칸에 위치하는 도시의 정보는 공백으로 두고, 유저가 부루마블 판의 해당하는 칸에 도착할때 클라이언트는 도시 앤티티의 name과 일치하는 도시의 이름를 해당 member 앤티티에 대한 PATCH 요청을 통해 동적으로 블록과 해당 블록의 도시이름을 매핑하여 유저의 currentCity 및 visitedCities 콜럼에 저장한다. 이를 통해 최소한의 코드의 수정으로 도시의 블록 위치를 유연하게 수정할 수 있게 되었다.</p>
<p>아래는 실제적으로 어떻게 이를 적용하였는지에 대한 코드이다.</p>
<pre><code>public enum UserLocations {

    BLOCK_0(0, &quot;시작점&quot;),
    BLOCK_A(1, &quot;&quot;),
    BLOCK_B(2, &quot;&quot;),
    BLOCK_C(3, &quot;&quot;),
    BLOCK_D(4, &quot;&quot;),
    BLOCK_E(5, &quot;&quot;),
    BLOCK_F(6, &quot;&quot;),
    BLOCK_G(7, &quot;&quot;),
    BLOCK_H(8, &quot;&quot;),
    BLOCK_I(9, &quot;&quot;),
    BLOCK_J(10, &quot;&quot;),
    BLOCK_K(11, &quot;&quot;),
    BLOCK_L(12, &quot;&quot;),
    BLOCK_M(13, &quot;&quot;),
    BLOCK_N(14, &quot;&quot;),
    BLOCK_O(15, &quot;&quot;),
    BLOCK_P(16, &quot;&quot;),
    BLOCK_Q(17, &quot;&quot;),
    BLOCK_R(18, &quot;&quot;),
    BLOCK_S(19, &quot;&quot;);

    @Getter
    private final int num;

    @Getter
    @Setter
    private String cityName;

    UserLocations(int num, String cityName) {
        this.num = num;
        this.cityName = cityName;
    }
}</code></pre><p>이처럼 유저가 해당 블록의 위치에 도착을 하고 해당하는 블록위의 도시의 이름을 PATCH 요청을 통해 서버에 보내게 되면 공백으로 관리되고 있는 cityName을 매퍼를 통해 실제 앤티티로 변환시에 매핑시켜 저장한다. </p>
<p>아래는 이를 실현하고 있는 매퍼의 메소드이다.</p>
<pre><code>default Member patchToMember(MemberDto.Patch patch){
        if ( patch == null ) {
            return null;
        }

        Member member = new Member();
        Blog blog = new Blog();
        blog.setId(patch.getBookmarkId());

        member.setNickname( patch.getNickname() );
        member.setPassword( patch.getPassword() );
        member.addBookMarks(blog);

        UserLocations currentLocation = patch.getCurrentLocation();
        if (currentLocation != null) {
            currentLocation.setCityName(patch.getCurrentCityCode()); // dto를 통해 받아온 도시이름을 ENUM에 매핑시켜 저장하는 코드이다.
            member.setCurrentLocation(currentLocation);
        }
        member.setNationality( patch.getNationality() );

        return member;
    }</code></pre><p>두번째 난관은 북마크에 관한 부분이였다. 원래의 계획은 앤티티간의 매핑을 통해 클라이언트의 요청에 따라 실제 blog객체를 찾아 저장하는 식으로 이를 구현하였다. 물론 큰 문제없이 잘 동작하였다. 하지만, 프로그램의 성능과 실제로 북마크를 사용하는 곳인 마이페이지에서 북마크한 블로그들을 로딩할때 맴버와 분리된 블로그의 아이디를 이용하는 API를 이용하는것이 페이지네이션을 비롯한 효율적이고 이를 위해 저장되어야할 정보는 사실 해당 블로그들의 아이디뿐이였다. 또한, 무엇보다 북마크를 위한 유저의 액션인 굉장히 순간적인것으로 그 속도를 빠르게 할 필요가 있다는 고민을 하게 되었다. 복잡한 관계는 쿼리의 속도를 저하시키는것 자명한 사실이였다. 더해서, 팔로우와 팔로워와는 다르게 북마크는 양방향으로 매핑이 되어야 할 필요도 없다는 점 또한 연관관계 매핑을 사용하지 않는 결정을 내리는데 큰 이유가 되었다. 그래서 나는 관계를 매핑하여 북마크정보를 저장하기 보다는 아이디만을 ElementCollection 콜럼으로 저장하기로 수정하였다. </p>
<pre><code>    ...
    //해당 사용자가 작성한 블로그 객체가 저장된다.

    @OneToMany(mappedBy = &quot;member&quot;, cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List&lt;Blog&gt; myBlogs = new ArrayList&lt;&gt;();

    //해당 사용자가 북마크한 블로그의 아이디만 저장된다.

    @ElementCollection
    private List&lt;Long&gt; bookmarks = new ArrayList&lt;&gt;();

    ...

    //북마크를 추가하기 위한 member Entity의 메소드이다.

        public void addBookMarks(Blog blog) {
        if (myBlogs.contains(blog)) throw new BusinessLogicException(ExceptionCode.NOT_ALLOWED_BOOKMARK); //해당 사용자 본인이 작성한 블로그는북마크할 수 없다.
        if (bookmarks.contains(blog.getId())) throw new BusinessLogicException(ExceptionCode.ALREADY_BOOKMARKED);
        //이미 추가되어 있는 블로그는 추가할 수 없다.
        bookmarks.add(blog.getId());
    }
    public void deleteBookmark(Long id) {

        if (!bookmarks.contains(id)) throw new BusinessLogicException(ExceptionCode.BLOG_NOT_FOUND);
        bookmarks.removeIf(blogId -&gt; blogId.equals(id));
    }

    ...
</code></pre><p>다음은 마이페이지에서 사용자의 북마크 목록을 추가/조회/삭제하기 위한 맴버의 서비스 코드이다.</p>
<pre><code>public Page&lt;Blog&gt; findBookMarks(Member findMember,Long loginMember,int page,int size) {

        verifyIsSameMember(findMember,loginMember);

        PageRequest pageRequest = PageRequest.of(page-1, size, Sort.by(&quot;createdAt&quot;).descending());

        List&lt;Long&gt; myBookmarks = findMember.getBookmarks();
        //연관관계 매핑을 사용하지 않기때문에 조회시마다 실제 블로그가 존재하는지를 검증하여 지워진 블로그라면 이를 맴버의 북마크 콜럼에서 제거해주어야 했다.
        List&lt;Blog&gt; bookMarks = findMember.getBookmarks().stream().map(id-&gt;{
            Optional&lt;Blog&gt; findBlog = blogRepository.findById(id);
            if (findBlog.isEmpty()) {
                myBookmarks.remove(id);
            }
            return findBlog.orElse(null);
        }).collect(Collectors.toList());
        List&lt;Blog&gt; result = bookMarks.stream().filter(Objects::nonNull).collect(Collectors.toList());
        //
        return new PageImpl&lt;&gt;(result,pageRequest,bookMarks.size());
    }

    public void addBookMark(Long memberId, Long loginMember ,Long blogId) {

        Member findMember = findVerifiedMember(memberId);
        verifyIsSameMember(findMember,loginMember);
        findMember.addBookMarks(blogRepository.findById(blogId).orElseThrow(()-&gt; new BusinessLogicException(ExceptionCode.BLOG_NOT_FOUND)));
        saveMember(findMember);
    }

    public void deleteBookMark(Long memberId,Long loginMember, Long blogId) {
        Member findMember = findVerifiedMember(memberId);
        verifyIsSameMember(findMember,loginMember);
        findMember.deleteBookmark(blogId);
        saveMember(findMember);
    }</code></pre><p>마지막으로 팔로우와 팔로워를 구현하기위해 맴버간의 조인테이블을 만들어 이를 저장하는 형태로 이를 구현하였다. 아래는 이를 구현한 코드이다.</p>
<pre><code>@Entity
@Getter
@Setter
public class Follow extends Auditable {
    @ManyToOne
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @ManyToOne
    @JoinColumn(name = &quot;followed_id&quot;)
    private Member followedMember;

}</code></pre><pre><code>@Entity
@Getter
@Setter
public class Follower extends BaseEntity {
    @ManyToOne
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @ManyToOne
    @JoinColumn(name = &quot;follower_id&quot;)
    private Member follower;
}
</code></pre><p>또한 아래는 이를 사용하는 맴버의 서비스 레이어 코드이다.</p>
<pre><code> public void saveFollowing(Member findMember, Member followedMember, Long loginMember) {

        verifyIsSameMember(findMember,loginMember);

        //사용자가 누군가를 팔로잉할때 팔로우와 팔로워 인스턴스 만들어 서로의 follow 와 follower 콜럼에 저장한다. 쿼리의 커스텀을 통해 팔로우와 팔로워를 분리하지 않고서도 서로를 조회할 수 있지만, 조회시 맴버 앤티티에 저장되어 있는 정보만으로 추가적이 쿼리없이 빠르게 조회할 수 있도록 하기 위해 이를 분리하여 저장하는 방식으로 구현하였다. 이를 통해 JPA의 최대 장점중 하나인 양방향 연관관계 매핑을 통한 cascade정책을 사용할 수 있고 동시에 쿼리의 복잡도를 낮춤과 동시에 조금더 명시적인 코드와 관계를 정의할 수 있었다. 
        if(memberVerifier.verifyIsMemberActive(followedMember)){

            Follow follow = new Follow();
            follow.setMember(findMember);
            follow.setFollowedMember(followedMember);
            followRepository.save(follow);
            findMember.addFollow(follow);
            saveMember(findMember);

            Follower follower = new Follower();
            follower.setMember(followedMember);
            follower.setFollower(findMember);
            followerRepository.save(follower);
            followedMember.addFollower(follower);
            saveMember(followedMember);}
        else throw new BusinessLogicException(ExceptionCode.MEMBER_INACTIVE);
    }

    public void unfollowMember(Member findMember, Member followedMember, Long loginMember) {

        verifyIsSameMember(findMember,loginMember);

        Follow follow = followRepository.findByMemberAndFollowedMember(findMember,followedMember).get();
        findMember.unFollow(follow);
        followRepository.delete(follow);
        saveMember(findMember);

        Follower follower = followerRepository.findByMemberAndFollower(followedMember,findMember).get();
        followedMember.deleteFollower(follower);
        followerRepository.delete(follower);
        saveMember(followedMember);

    }

    public Page&lt;Member&gt; findFollows(Member findMember,Long loginMember,int page,int size) {

        verifyIsSameMember(findMember,loginMember);

        //팔로우 조회시 Follow 객체로 저장되어 있는 follows의 정보들에서 Member정보를 찾아 스트림을 통해 변환 시켜준다. 이때, 맴버의 상태가 Inactive(비활성중)이라면 null값을 리턴하고 필터를 통해 널값을 제거한다.

        List&lt;Member&gt; follows = findMember.getFollows().stream().map(
                follow-&gt; {
                    Member followedMember = follow.getFollowedMember();
                    if (memberVerifier.verifyIsMemberActive(followedMember)) {return followedMember;
                    }else return null;
                }).filter(Objects::nonNull).collect(Collectors.toList());
        PageRequest pageRequest = PageRequest.of(page-1, size, Sort.by(&quot;createdAt&quot;).descending());

        return new PageImpl&lt;&gt;(follows,pageRequest,follows.size());
    }

    public Page&lt;Member&gt; findFollowers(Member findMember,Long loginMember, int page,int size) {

        verifyIsSameMember(findMember,loginMember);

        List&lt;Member&gt; followers = findMember.getFollowers().stream().map(
                follower-&gt; {
                    Member findFollower = follower.getFollower();
                    if (memberVerifier.verifyIsMemberActive(findFollower)) {return findFollower;}
                    else return null;
                }).filter(Objects::nonNull).collect(Collectors.toList());
        PageRequest pageRequest = PageRequest.of(page-1, size, Sort.by(&quot;createdAt&quot;).descending());

        return new PageImpl&lt;&gt;(followers,pageRequest,followers.size());
    }</code></pre><h4 id="1-1-member-앤티티-관련-문제파악-및-개선사항">1-1. Member 앤티티 관련 문제파악 및 개선사항</h4>
<p>사실 문제점이라기 보다는 서비스 운영관련 개선점이라고 하는 것이 맞을것이다. 나는 서비스의 운영면에서도 고민을 많이하고 그 불편을 최소화하기 위한 노력을 하는 것 또한 서버 개발자의 중요한 덕목이라 생각한다. 따라서 어떻게 하면 맴버 관리를 최적화하고 운영적으로 원할하게 개선할 수 있을까를 놓고 기능 구현 후에 많이 고민하였다.</p>
<h4 id="첫번째-개선점">첫번째 개선점</h4>
<p>첫번째 개선점은 dto의 용도에 따른 분리였다. 맴버 정보를 담아 클라이언트에게 전달하는 Response Dto는 그 용도와 사용 위치에 따라 최적화된 정보만을 전달할 수 있도록 담는 정보의 가짓수를 차별화여 분리할 필요가 있었다. 가령 블로그나 댓글등의 작성자의 정보를 클라이언트에게 전달할때 기존의 맴버 리스폰스 DTO를 사용하면 쓸데없는 정보들이 너무 많이 담긴다는것이 문제였다. 리스폰스 바디의 내용을 최대한 줄이는것은 쿼리의 속도를 개선하는데 고려되어야 할 첫번째 사항이기에 이는 성능면에서 굉장히 중요한 부분이였다. 따라서 나는 아래와 같이 Response DTO를 분리하였다.</p>
<p>기존의 맴버 리스폰스</p>
<pre><code>
 @Getter
    @Setter
    public static class Response{

        private Long id;
        private String nickname;
        private String email;
        private int level;
        private List&lt;String&gt; roles;
        private List&lt;ImageResponseDto&gt; profilePics;
        private String nationality;
        private LocalDate birth;
        private int follows;
        private int followers;
        private List&lt;Stamps&gt; myStamps;
        private UserLocations currentLocation;
        private List&lt;UserLocations&gt; visitedCities;

        private List&lt;Long&gt; bookmarks;

        private LocalDateTime createdAt;
        private LocalDateTime modifiedAt;
    }</code></pre><p>추가된 맴버 리스폰스, MemberSummarizedResponse</p>
<pre><code>@Getter
@Setter
public class MemberSummarizedResponse {
    private Long id;
    private String nickname;
    private String profile;

}</code></pre><p>이를 통해 블로그/커맨트 등의 리스폰스에 담을 맴버정보를 필요한 정보만을 담아 전송할 수 있게 되었다.</p>
<h4 id="두번째-개선점">두번째 개선점</h4>
<p>두번째 개선점은 운영적인 측면이였다. 사용자의 마음은 갈대이기에 언제든 탈퇴를 하고 또 탈퇴 이후에도 빠르게 계정을 복구하는 면으로 유저를 관리하면 편리할것 같다는 생각이 들었다. 이에 유저가 탈퇴를 원할때 곧바로 이를 삭제하기 보다는 그 상태값을 Active와 Inactive로 두고 일정 시간동안 이를 복구할 기회를 주도록 관리하면 어떨까하는 아이디어가 떠올랐고 또한 장시간 사용기록이 없는 사용자들을 판별하고 이를 삭제해주는 로직 또한 편의성과 서버의 리소스 관리를 위해 필요하다는 생각이 들어 이를 위한 연관된 필수 기능들을 고민하였다. 이를 위한 기능들은 다음과 같다. 1. 맴버 탈퇴시 맴버 상태값을 변경하는 메소드, 2. 맴버가 오랜기간 로그인이 기록이 없는 경우 이를 잠재적 탈퇴한 사용자로 보고 상태값을 inactive로 변경한다. 3. 탈퇴/휴면 맴버가 돌아오지 않을경우 이를 실제로 삭제하는 로직, 4. 맴버를 조회할때 또 로그인시 맴버의 상태값이 active인지 검증하는 로직</p>
<p>이를위해 먼저 Member Entity에 그 상태값을 가진 Enum 클래스를 만들어 주었다.</p>
<pre><code>...

    @Enumerated(EnumType.STRING)//이넘 타입을 String으로 설정하여 출력되는 값이 문자열이 나올 수 있도록 설정한다.
    private Status memberStatus = Status.MEMBER_ACTIVE;
    //맴버가 태어날때 기본 상태값을 ACTIVE로 생성한다.

...
 public enum Status{

        MEMBER_ACTIVE(&quot;member is active&quot;),

        MEMBER_INACTIVE(&quot;member is deactivated&quot;);

        @Getter
        private String description;

        Status(String description) {
            this.description = description;
        }
    }</code></pre><p>그런후에 이를 검증하는 코드들을 필요한 메소드들에 추가해 주었다. 블로그와 댓글에서는 이를 검증하지 않고 있는데 그 이유는 작성자가 탈퇴하여도 그 작성물들을 같이 삭제하여야 하는가에 대한 부분은 아직 고민하고 있는 부분이기에 그렇다. 많은 서비스들이 맴버가 탈퇴하더라도 작성물들은 삭제하지 않는 경우들이 꽤 있었고 이또한 다른 사용자들의 편의를 위해 괜찮은 정책인것 같았고 만일 개인정보에 대한 부분이 추후 문제가 된다면 이를 삭제하는 BATCH기능을 추가해 이를 처리할 예정이다. </p>
<p>다음으로 위에서 설명한 2과 3번 기능을 구현하기 위해 Srping Scheduler를 이용한 BATCH 기능을 사용하였다. 그 내용은 다음과 같다. a. 로그인할때 마다 맴버 앤티티에 lastLogin 콜럼을 두고 그 시간을 기록한다. b. Scheduler를 통해 유저들의 로그인 기록을 조회해 일년동안 그 기록이 없는 맴버의 status를 Inactive로 바꾼다. c. Scheduler를 통해 그 상태값이 Inactive 인지를 확인하고 이를 삭제한다.</p>
<p>아래는 a. 기능 구현을 위한 로그인시에 맴버의 상태를 검증하고 로그인 시간을 기록하는 부분이다.. </p>
<p>일반 로그인시</p>
<pre><code>@SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        ObjectMapper objectMapper = new ObjectMapper();
        LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);

        Member member = memberService.findMemberByEmail(loginDto.getEmail());
        //로그인 시도시 맴버의 상태값이 ACTIVE인지 검증
        if (member.getMemberStatus() == Member.Status.MEMBER_INACTIVE) throw new AuthException(&quot;Member is inactive. Please contact us.&quot;);

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());

        return authenticationManager.authenticate(authenticationToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Member member = (Member) authResult.getPrincipal();  //

        String accessToken = delegateAccessToken(member);   //
        String ip = extractor.getClientIP(request);
        delegateRefreshToken(member,ip); //
        log.info(&quot;accessToken is generated&quot;);


        //로그인 히스토리 생성
        Member findMember = memberService.findVerifiedMember(member.getId());
        findMember.setLastLogin(LocalDateTime.now());
        memberService.saveMember(findMember);


        response.setHeader(&quot;Authorization&quot;, accessToken);

    }</code></pre><p>오어스 로그인시 이메일을 이용해 이미 저장된 맴버인지 일차 검증 후 이미 가입된 맴버라면(DB에 존재하면) 저장하지 않고 최초 가입시에는 맴버정보를 DB에 저장한다. 그리고 해당 맴버의 상태값의 유효성을 검증하고 Active 상태라면 로그인 시간을 기록하고 Inactive 라면 ExceptionCode를 전송한다. </p>
<pre><code>...
@Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        var oAuth2User = (OAuth2User) authentication.getPrincipal();

        String email = String.valueOf(oAuth2User.getAttributes().get(&quot;email&quot;));
        List&lt;String&gt; authorities = authorityUtils.createRoles(email);
        if (!memberVerifier.verifyExistMember(email)) {saveMember(email, authorities);}


        //로그인 히스토리 생성
        Member findMember = memberService.findMemberByEmail(email);
        if (memberVerifier.verifyIsMemberActive(findMember)){
        findMember.setLastLogin(LocalDateTime.now());
        memberService.saveMember(findMember);
        } else {throw new BusinessLogicException(ExceptionCode.MEMBER_INACTIVE);
        }

        redirect(request,response,email,authorities);
    }
    ...</code></pre><p>또한 b., c. 기능 구현을 위한 BATCH기능의 일환으로 Spring Scheduler를 사용하여 상태값과 맴버를 일괄 변경 삭제하는 부분이다.</p>
<p>아래의 코드는 매일 자정에 실행되며 마지막 로그인 날짜와 현재의 날짜를 비교하여 일년이 지났다면 맴버의 상태를 Inactive로 바꾸어 주는 역할을 한다.</p>
<pre><code>    @Scheduled(cron = &quot;0 0 0 * * *&quot;, zone = &quot;Asia/Seoul&quot;)// Run every day
//    @Scheduled(cron =&quot;0 * * * * *&quot;)
    private void updateMemberStatus() {
        LocalDateTime thresholdDate = LocalDateTime.now().minus(1, ChronoUnit.YEARS); //inactive member if last login date is passed more than 1 year
//       LocalDateTime thresholdDate = LocalDateTime.now().minus(1,ChronoUnit.MINUTES);
//        if (lastLogin.isBefore(thresholdDate)){

        List&lt;Member&gt; members = repository.findAllByLastLogin(thresholdDate);
        members.stream().map(m -&gt;{
            m.setMemberStatus(Member.Status.MEMBER_INACTIVE);
            repository.save(m);
            return m;
        }).collect(Collectors.toList());

//        }
    }
}</code></pre><p>아래의 코드 또한 매일 자정에 실행되며 Inactive 상태의 맴버를 일괄 삭제한다.</p>
<pre><code>@Component
@RequiredArgsConstructor
public class MemberCleanupTask {

    private final MemberRepository memberRepository;


    @Scheduled(cron = &quot;0 0 0 * * *&quot;, zone = &quot;Asia/Seoul&quot;) //every day
//    @Scheduled(cron = &quot;0 */2 * * * *&quot;)
    private void deleteInactiveUsers() {
        List&lt;Member&gt; membersToDelete = memberRepository.findAllByMemberStatus(Member.Status.MEMBER_INACTIVE);
        memberRepository.deleteAll(membersToDelete);
    }
}</code></pre><h4 id="문제발생">문제발생!</h4>
<p>처음에는 단순히 로그인 기록의 유효기간이 일녕이니 일년에 한번 이를 검증해서 상태를 바꾸고 오년에 한번 일괄되게 이를 삭제하면 된다고 생각하였고 그렇게 설정을 하였다. 하지만 그렇게 할 경우 오차의 범위가 굉장히 커져서 &#39;거의&#39; 일년동안 활동이 없던 맴버가 삭제되지 않고 다시 오년을 기다려 삭제될 수 도 있는 등의 크나큰 오류가 나올 수 있다는 점을 발견하였고 이를 매일 자정에 실행되도록 수정하여 그 오차 범위를 줄이는 식으로 수정하여 해결하였다. 하지만!! 새로운 문제를 조우하였다. </p>
<p>Dang it! 같은 시간에 설정된 task의 우선순위를 보장할 수 없다는것이 그것이였다. 둘다 같은 시간에 실행되게 설정되어있고 우선순위는 분명 맴버의 상태값을 바꾸는것이 우선되어야 하는 task였고 이를 설정하는 방법을 알아보았다. 알아본 방법은 대략 4가지였다. </p>
<p><strong>해결법 1.</strong> 하나의 메소드에 순차적으로 두가지 task를 정의하여 실행한다.
<strong>해결법 2.</strong> fixedDelay를 사용한다. ex) @Scheduled(fixedDelay= &quot;&quot;) 앞서 실행된 task보다 명시한 시간만큼 딜레된 시간에 해당 task를 실행한다.
<strong>해결법 3.</strong> Queue를 만들어 task를 먼저 순차적으로 큐에 넣고 TaskQueue에서 순차적으로 꺼내어 실행한다.
<strong>해결법 4.</strong> 크론 시간 설정을 애초에 일정 시간 딜레이된 시간으로 설정한다.</p>
<p>이중에 내가 채택한 방법은 4번째였다. 그 이유는 우선 너무 많은 리소스를 사용하고 싶지 않았다는것이 그 이유이고 이 방법이 가장 간단하고 또한 확실한 방법이라는 판단에서였다.</p>
<p>따라서 Inactive 맴버를 일괄 삭제하는 코드를 아래와 같이 수정해 주었다.</p>
<pre><code>    @Scheduled(cron = &quot;0 2 0 * * *&quot;, zone = &quot;Asia/Seoul&quot;) //every 12:02
    private void deleteInactiveUsers() {
        List&lt;Member&gt; membersToDelete = memberRepository.findAllByMemberStatus(Member.Status.MEMBER_INACTIVE);
        memberRepository.deleteAll(membersToDelete);
    }
</code></pre><p>위의 작업을 통해 우선 순위를 설정할 수 있었다.</p>
<h3 id="3-회고-생각해봐야-할-개선점">3. 회고: 생각해봐야 할 개선점</h3>
<p>유저 관리와 서버스의 핵심 기능들을 사용자가 사용하기 위해선 맴버 앤티티를 필연적으로 다소 복잡한 연관관계 매핑을 할 필요가 있었고 이를 최적화하여 최대한 가볍게 만드는것이 가장큰 숙제였고 아직도 남아있는 숙제라 느껴진다. 내가 구현한 방법들이 정답이라 절대 생각하지 않는다. 다만, 지금 나의 수준에서 많은 고민과 조사 및 공부를 하였고 내가 그 효율성을 판단해볼 수 있는 귀중한 기회들이였고 내 판단 속에서 최선이라 생각되어지는 방법들로 다 구현을 하였기 때문에 상당부분 만족하였다. </p>
<p>리소스에 대해 끊임없이 고민하고 최적화하는 것이 서버 개발에 있어서의 핵심이고 개발자의 숙명이라 생각한다. 이번 작업을 통해 특히, BATCH기능을 구현하면서 느낀 가장 큰 부분은 생각보다 Batch를 위한 Scheduling 기능의 리소스가 크다는 것이다. 회원의 수가 늘어나고 많아 진다면 매일 돌아가는 이 기능이 큰 서비스 장애를 유발할 수 있을거란 우려가 남았다. 이는 이 기능을 통한 득보다 실이 더 커질 수 있다는 것이다. 따라서 서비스가 커지고 유저가 늘어난다면 Batch 작업들만을 실행하는 서버를 따로 분리하여 처리하는 방법으로 해결할 수 있을 것이다. </p>
<p>복잡할수록 재미있고 어려울수록 흥미롭다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MarbleUs Project #3:  공통기능 구현: Advice, Auditable, etc.]]></title>
            <link>https://velog.io/@johnny_debt/MarbleUs-Project-3-%EA%B3%B5%ED%86%B5%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-Advice-Auditable-etc</link>
            <guid>https://velog.io/@johnny_debt/MarbleUs-Project-3-%EA%B3%B5%ED%86%B5%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-Advice-Auditable-etc</guid>
            <pubDate>Tue, 10 Oct 2023 08:10:14 GMT</pubDate>
            <description><![CDATA[<p><em>table of contents</em></p>
<hr>
<h5 id="1-예외처리">1. 예외처리</h5>
<h5 id="2-기본-entity-정의">2. 기본 Entity 정의</h5>
<h5 id="3-auditable-기능-정의">3. Auditable 기능 정의</h5>
<h5 id="4-argument-resolver">4. Argument Resolver</h5>
<h5 id="5-회고">5. 회고</h5>
<hr>
<h3 id="1-예외처리-1">1. 예외처리</h3>
<p>나는 비지니스 로직을 처리하면서 사용자의 실수로 인해 일어나는 예외 사항들을 고지하고 팀 프로젝트에의 빠른 디버깅을 돕기위해 기본 예외들에 대한 메세지들을 편집하고 응답해 줄 수 있는 예외처리기능을 가진 ExceptionHandler들을 정의한 GlobalExceptionAdvice를 만들었다. </p>
<p>이를 위해 우선 비지니스 로직을 처리하면서 생길 수 있는 예외사항들을 처리하기위해 ExceptionCode Enum 클래스를 만들고 발생 가능한 예외사항들을 정의해주었다. </p>
<pre><code>public enum ExceptionCode {
    MISSION_EXISTS(404, &quot;Mission not found&quot;),
    MISSION_NOT_FOUND(409, &quot;Mission exists&quot;),
    MEMBER_NOT_FOUND(404, &quot;Member not found&quot;),
    MEMBER_INACTIVE(404,&quot;Member is inactive&quot;),

    ...

    @Getter
    private int status;

    @Getter
    private String message;

    ExceptionCode(int status, String message)         {
        this.status = status;
        this.message = message;
    }

 }
</code></pre><p>그리고 이를 명시적으로 담아줄 수 있는 자바의 RuntimeException을 상속하는 BusinessLogicException을 만들어 ExceptionCode객체를 파라미터로 갖는 생성자를 만들었다.</p>
<pre><code>public class BusinessLogicException extends RuntimeException{
    @Getter
    ExceptionCode exceptionCode;

    public BusinessLogicException(ExceptionCode exceptionCode) {
        super(exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }
}</code></pre><p>이를 통해 서버에서 비지니스 로직중에 에상가능한 예외들을 정의해 throw 할 수 있다. </p>
<p>다음으로 예외사항들을 편집해 Response해 줄 수 있는 일종의 Dto인 ErrorResponse를 정의해 예외사항에 대한 메세지중 필요한 부분만 편집하여 클라이언트에 응답해 줄 수있게 하였다. 이때 생성자 대신 static, of 메서드를 활용하여 보다 명시적으로 에러를 외부 코드 단게에서 주입하여 ErrorResponse 객체를 생성시킬 수 있게 하였다. 또한, 비지니스 로직에서 발생하는 예러외에 데이터의 객체를 생성하는데 validation을 통과하지 못한 예외 사항(MethodArgumentNotValidException)을 사용자에게 핵심적으로 알리기 위해 자바에서 제공하는 BindingResult를 이용하는 FieldError 클래스를 정의하여 오류가 발생한 필드와 통과하지 못한 value 그리고 그 이유를 담아 편집해주고 ErrorResponse의 필드값으로 넣어 주었다.@A 또한 ConstraintViolationException (PathVariable의 유효성검사 실패)의 예외를 처리해 주는 편집된 리스폰스또한 만들어 ErrorResponse의 필드값아 담아주었다.@B 이렇게 함으로써 모든 예외사항에 대하여 그 종류에 따라 핵심적인 부분만을 담아 하나의 ErrorResponse 객체로 다룰 수 있게 되었다.</p>
<pre><code>@Getter
public class ErrorResponse {

    private int status;

    private String message;

    private List&lt;FieldError&gt; fieldErrors;

    private List&lt;ConstraintViolationError&gt; constraintViolationErrors;

    private ErrorResponse(int status, String message) { //of 메서드가 사용할 생성자 1
        this.status = status;
        this.message = message;
    }

    private ErrorResponse(List&lt;FieldError&gt; fieldErrors, List&lt;ConstraintViolationError&gt; constraintViolationErrors) { //of 메서드가 사용할 생성자 2
        this.fieldErrors = fieldErrors;
        this.constraintViolationErrors = constraintViolationErrors;
    }



    public static ErrorResponse of(BindingResult bindingResult){
        return new ErrorResponse(FieldError.of(bindingResult),null);
    }

    public static ErrorResponse of(Set&lt;ConstraintViolation&lt;?&gt;&gt; constraintViolations){
        return new ErrorResponse(null, ConstraintViolationError.of(constraintViolations));
    }

    // 비지니스 로직 에러들만 따로 처러
    public static ErrorResponse of(ExceptionCode exceptionCode){
        return new ErrorResponse(exceptionCode.getStatus(),exceptionCode.getMessage());
    }

    public static ErrorResponse of(HttpStatus httpStatus){
        return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase());
    }
    public static ErrorResponse of(HttpStatus httpStatus, String message){
        return new ErrorResponse(httpStatus.value(), message);
    }

    @Getter
    public static class FieldError { //@A
        private String field;
        private Object rejectedValue;
        private String reason;

        public FieldError(String field, Object rejectedValue, String reason) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }



        public static List&lt;FieldError&gt; of(BindingResult bindingResult) {
            final List&lt;org.springframework.validation.FieldError&gt; fieldErrors =
                    bindingResult.getFieldErrors();

            return fieldErrors.stream()
                    .map(fieldError -&gt; new FieldError(
                            fieldError.getField(),
                            fieldError.getRejectedValue() == null ? &quot;&quot; : fieldError.getRejectedValue().toString(),
                            fieldError.getDefaultMessage()))
                    .collect(Collectors.toList());
        }

    }

    @Getter
    public static class ConstraintViolationError { //@B

        private String propertyPath;
        private Object rejectedValue;
        private String reason;

        public ConstraintViolationError(String propertyPath, Object rejectedValue, String reason) {
            this.propertyPath = propertyPath;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List&lt;ConstraintViolationError&gt; of(
                Set&lt;ConstraintViolation&lt;?&gt;&gt; constraintViolations){
            return constraintViolations.stream()
                    .map(
                            constraintViolation -&gt;
                                    new ConstraintViolationError(
                                            constraintViolation.getPropertyPath().toString(),
                                            constraintViolation.getInvalidValue().toString(),
                                            constraintViolation.getMessage()
                                    )
                    ).collect(Collectors.toList());
        }
    }</code></pre><h3 id="2-기본-entity-정의equasl--hashcode">2. 기본 Entity 정의(equasl &amp; hashcode)</h3>
<p>JPA의 기능을 사용하면서 하나의 테이블의 인스턴스를 만들고 이의 동일성을 판별하는 경우가 많은데 이때 Java의 최상위 클래스인 Object의 equals()와 hashCode()를 사용한다. 그런데 equals가 기본적으로 구현된 방법은 2개의 객체가 참조하는 것이 동일한지를 확인하는 것으로, 이는 동일성(Identity)을 비교하는 것이다. 즉, 인스턴스가 만들어질때 변수가 참조하고 있는 그 주소값을 가지고 equals메소드는 비교를 하기 때문에 프로그래밍 상으로는 같은 값을 지님에도 다른 객체로 인식되게 된다. </p>
<p>HashTable과 같은 자료구조를 사용할 때 데이터가 저장되는 위치를 결정하기 위해 사용되는 Object클래스의 hashCode() 또한 기본적으로 heap에 저장된 객체의 메모리 주소를 반환하도록 되어있다. 따라서 동일한 객체(저장된 같은 값을 가지는 객체)는 동일한 메모리 주소를 갖는다는 것을 의미하므로, 동일한 객체는 동일한 해시코드를 가져야 한다. 그래야만 같은 값을 가진 객체의 인스턴스가 생기더라도 hash 자료구조에 저장될때 같은 주소값으로 하나만 저장될 수 있다.</p>
<p>이러한 동등성(Equality)문제를 해결하기 위해 나는 우리 서비스의 모든 앤티티들의 최상위 앤티티를 만들어 eqauls()와 hashCode()메소드를 재정의 해주어야 했다. 객체의 아이디 값으로만 객체를 비교하고 저장하도록 equals와 hashCode 메소드를 오버라이딩해주었다.</p>
<p>그리고 모든 앤티티에서 이를 extends하여 모든 앤티티들에 이를 적용하였다.</p>
<pre><code>public class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;

    // Getter and setter for id

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o))
            return false;

        BaseEntity that = (BaseEntity) o;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}</code></pre><h3 id="3-auditable-기능-정의-1">3. Auditable 기능 정의</h3>
<p>테이블에 저장되는 시간과 변경사항이 적용되는 시간을 기록하는 기능을 가진 추상 클래스 Auditable을 만들어 프로그램 전방위적으로 적용하여 코드의 수를 줄이고자 하였다. 이때 만들어 놓은 BaseEntity의 재정의된 메소드들은 필수적으로 모든 앤티티에 적용되어야 하기 때문에 Auditable 추상클래스 또한 BaseEntity를 상속하여 Auditable 만 상속하더라도 BaseEntity의 역할도 함께 적용될 수 있도록 하였다.</p>
<pre><code>@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable extends BaseEntity {

    @CreatedDate
    @Column(name = &quot;created_at&quot;, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = &quot;last_modified_at&quot;)
    private LocalDateTime modifiedAt;

}</code></pre><p>그리고 메인 Application 클래스에 @EnableJpaAuditing를 붙혀 기능 사용을 가능하게 하였다.</p>
<h3 id="4-argument-resolver-1">4. Argument Resolver</h3>
<p>기본적인 애플리케이션 특성상 앤티티를 수정할때에 필연적으로 작성자와 수정자가 같은지를 판별하여야 했다. 이는 쿼리파라미터 혹은 리퀘스트바디로 받은 유저 아이디()와 로그인되어 있는 사용자의 아이디가 같은지를 판별하는 식으로 처리하기로 하였다.</p>
<h4 id="문제해결-1">문제해결 1</h4>
<p>이를 위해 로그인 사용자의 정보를 전역적으로 읽어올 필요가 있었고 JWT 토큰을 검증하는 과정에서 성공시에 SecurityContextHolder에 사용자 정보를 저장하고 있기 때문에 SecurityContextHolder를 통해 정보를 읽어와 아이디를 비교할 수 있었다. 하지만 매번 이를 위한 똑같은 반복된 코드가 거의 모든 컨트롤러에서 필요하였고 이는 굉장한 비효율이라 생각이 되었다. 그래서 이를 공통기능으로 묶어 처리할 수 있는 방법을 고민하였다.</p>
<h4 id="문제해결-최종">문제해결 최종</h4>
<p>이를 편리하게 모든 곳에서 반복된 코드없이 처리하기위해 나는 HandlerMethodArgumentResolver를 구현하는 ArgumentResolver 만들어 전방위 메소드에서 파라미터로 로그인 유저의 아이디가 사용될 수 있게 하였다.</p>
<p>이를 위해 우선 파라미터에 사용될 어노테이션을 만들어 주었다.</p>
<pre><code>@Target(ElementType.PARAMETER) // 메서드의 파라미터에 적용
@Retention(RetentionPolicy.RUNTIME) // 어노테이션의 수명주기를 런타임 동안
public @interface LoginMemberId {
}</code></pre><p>그리고 이 어노테이션이 가지게 될 값을 지정해주는 HandlerMethodArgumentResolver를 구현하는 ArgumentResolver 만들어 해당 어노테이션의 값을 정의해 주었다.</p>
<pre><code>@Component
@RequiredArgsConstructor
public class LoginUserIdArgumentResolver implements HandlerMethodArgumentResolver { // 컨트롤러 메서드의 파라미터 해석하여 값 전달

    private final MemberService service;



    @Override
    public boolean supportsParameter(MethodParameter parameter) {  // 구현한 argument resolver가 특정 파라미터를 지원할지 여부 판단
        parameter.getParameterAnnotations(); //현재 파라미터에 @LoginMemberId 어노테이션이 있는지 검증 있으면 아래의 코드 실행(return true)
        return true;
    }

    @Override // 파라미터를 해석하여 값을 반환하는 역할
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();           
        // 사용자 인증 정보
        // 익명이면 0L 리턴(비로그인)
        if(principal.equals(&quot;anonymousUser&quot;)){
            return 0L;
        }
        Member member = service.findMemberByEmail(principal.toString());
        return member.getId();
    }
}</code></pre><h3 id="5-회고-1">5. 회고</h3>
<p>필수적으로 선행되어야 하는 프로그램의 뼈대를 잡는 작업을 마무리하였다. 이로써 프로그램 전역에서 사용할 공통기능들을 만들 수 있었고 작업을 하면서 더 필요함을 느끼는 기능들이 있다면 공통기능으로 묶어 추가적으로 처리하기로 하였다.  </p>
<p>수정자와 작성자를 검증하는 기능을 구현하면서 단지 기능이 구현됨에 만족하지 않고 팀원들이 어떻게 하면 편리하게 중복되는 코드를 피해 효율적으로 사용할 수 있을까 고민하였고 이가 잘 구현된거 같아 뿌듯하였다. 그리고 이런 AOP를 만드는 것으로 전반적인 프로그램의 완성도가 높아지고 훨씬 가독성있는, Layer가 나뉨으로 오류가 생겼을때 모든 해당하는 코드를 수정하는것이 아닌 해당 기능만 수정하면 되는것 처럼 수정하기 또한 용이한 효율적인 프로그램을 만들 수 있음을 다시 느꼈고 앞으로의 작업에서도 끊임없이 고민해야하는 부분임을 느꼈다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MarbleUs Project #2 Spring Security/JWT Token /Redis
인증/인가]]></title>
            <link>https://velog.io/@johnny_debt/MarbleUs-Project-2-Spring-SecurityJWT-Token-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@johnny_debt/MarbleUs-Project-2-Spring-SecurityJWT-Token-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Tue, 03 Oct 2023 10:11:02 GMT</pubDate>
            <description><![CDATA[<p><em>table of contents</em></p>
<hr>
<h5 id="1-인증-시나리오">1. 인증 시나리오</h5>
<h5 id="2-spring-security-filter">2. Spring Security Filter</h5>
<h5 id="3-jwtauthenticationfilter--oauth2loginauthenticationfilter">3. JwtAuthenticationFilter &amp; OAuth2LoginAuthenticationFilter</h5>
<h5 id="4-authenticationfilter">4. AuthenticationFilter</h5>
<h5 id="5-trouble-shooting-1-문제-파악">5. trouble Shooting #1 문제 파악</h5>
<h5 id="6-trouble-shooting-2-적용-및-결과">6. trouble Shooting #2 적용 및 결과</h5>
<h5 id="7-회고">7. 회고</h5>
<hr>
<h3 id="1-인증-시나리오-1">1. 인증 시나리오</h3>
<p>내가 맡은 기능 역할 중 가장 중요하고 선행이 되어 했던 부분은 역시 보안 인증에 관한 부분이였고 이를 달성하기 위해 우선 사용자가 어떻게 인증을 진행할지 그 시나리오를 먼저 생각해보기로 하였다. <strong>기본적으로 우리 서비스는 장바구니등의 유저의 상태를 유지시킬 필요가 크게 없는 서비스라 생각이 들었고 무엇보다 AWS에서 제공하는 프리티어로 진행하는 프로젝트였기 때문에 서버의 메모리 사용량을 최소화 할 필요가 있었고</strong> 이에 나는 세션을 사용한 로그인 인증 방식 보다는 <strong>JWT 토큰을 이용한 인증 방식이 더 효율적이라 생각하고 이를 채택하였다.</strong> 내가 처음 생각한 시나리오는 이렇게 진행되었다.</p>
<ol>
<li>사용자(유저)는 기본 <strong>회원가입</strong> 혹은 구글의 <strong>OAuth2.0서비스</strong>를 이용해 본인의 정보를 등록하게 된다.</li>
<li>서버에서는 회원이 기입한 비밀번호를 <strong>Base64인코딩</strong>을 통해서 암호화 한 후 DB의 Member 테이블에 저장한다.</li>
<li>유저는 회원가입을 통해 저장된 이메일과 비밀번호를 이용하여 로그인을 시도하고 <strong>Sping Security의 필터</strong>에서 credential를 검증하게 된다.</li>
<li>유저의 credential이 검증이 되었다면 서버에서는 30분의 만료시간을 가진 유저의 이메일만을 저장한 <strong>Access Token</strong>과 짧은 만료시간을 가진 액세스 토큰의 자동 재발급을 위한 <strong>Refresh Token</strong>을 secretkey를 이용하여 발급하고 이를 <strong>헤더 혹은 Redirect되는 URL에 담아 전송</strong>한다.</li>
<li>클라이언트는 발급받은 두 토큰을 <strong>localStorage에</strong> 저장한다.</li>
<li>클라이언트 단에서 해당 유저의 모든 요청에 이 <strong>두가지 토큰을 헤더에 담아 보내고</strong> 서버의 시큐리티 필터에서 이를 잡아 Access Token의 유효성을 검증하고 만약 만료된 토큰이라면 Refresh 토큰을 검증하여 Access Token을 갱신한 후 이를 Response헤더에 담아 보낸다. </li>
<li>클라이언트는 갱신된 Access Token으로 요청을 재게한다.</li>
</ol>
<p><strong>모든 요청에 Access와 Refresh 토큰을 항상 모두 담아 보냄으로 새로운 토큰을 발급하는 과정을 간략화 하였다.</strong></p>
<p>해당 시나리오를 현실화하기 위해 먼저 보안의 기본 시나리오를 짜기위해 우선 Spring Security Filter를 사용하기위한 Configuration 클래스를 만들어야하였다.
우선 기본적인 http요청에 대한 시큐리티 필터의 흐름고 설정들을 위한 filterChain메서드를 만들어 @Beane등록을 해 주었다.</p>
<h3 id="2-spring-security-filter-1">2. Spring Security Filter</h3>
<pre><code>    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .headers().frameOptions().sameOrigin() //h2 이용하기위한 설정
                .and()
                .csrf().disable()
                .cors().configurationSource(corsConfigurationSource())
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .exceptionHandling()
                .authenticationEntryPoint(new MemberAuthenticationEntryPoint(jwtTokenizer,authorityUtils,extractor))
                .and()
                .apply(new CustomFilterConfigurer())
                .and()
                .authorizeHttpRequests(authorize -&gt; authorize.anyRequest().permitAll()
                )
                .oauth2Login(oauth2 -&gt; oauth2.successHandler(new OAuth2memberSuccessHandler(jwtTokenizer,authorityUtils,memberService,extractor,memberVerifier,nickNameGenerator,passwordEncoder)))
                .logout()
                .logoutUrl(&quot;/logout&quot;)
                .logoutRequestMatcher(new AntPathRequestMatcher(&quot;/logout&quot;, &quot;POST&quot;))
                .addLogoutHandler(new CustomLogoutHandler(redisServiceUtil,extractor))
                .logoutSuccessUrl(&quot;http://marbleus-s3.s3-website.ap-northeast-2.amazonaws.com&quot;);
        return http.build();
    }</code></pre><p>시큐리티 필터에서 커스펌 필터를 사용하기 위한 AbstractHttpConfigurer를 상속하는 CustomFilterConfigurer 또한 이너클래스로 정의해 주었다.</p>
<pre><code>public class CustomFilterConfigurer extends AbstractHttpConfigurer&lt;CustomFilterConfigurer,HttpSecurity&gt; {
        @Override
        public void configure(HttpSecurity builder) throws Exception {
            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer,authorityUtils);

            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer, memberService,redisServiceUtil,extractor);
            jwtAuthenticationFilter.setFilterProcessesUrl(&quot;/auth/login&quot;);  //기본 로그인 시도 주소 프론트에서 이 URL로 로그인을 시도한다.       //

               builder.addFilter(jwtAuthenticationFilter);
            builder.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
            builder.addFilterAfter(jwtVerificationFilter, OAuth2LoginAuthenticationFilter.class);
        }
    }</code></pre><p>커스텀해 사용할 필터들을 DI받고 이를 builder를 사용하여 순서를 정의하여 주었다. </p>
<h3 id="3-jwtauthenticationfilter--oauth2loginauthenticationfilter-1">3. JwtAuthenticationFilter &amp; OAuth2LoginAuthenticationFilter</h3>
<p>JwtVerificationFilter에서는 모든 요청의 헤더에서 Access Token을 검증하고 인증를 하는 역할을 한다.
그리고 JwtAuthenticationFilter 와 OAuth2LoginAuthenticationFilter는 최초의 인증(로그인)을 담당한다. 따라서 이 두 필터를 통해 기본 로그인을 시도하고 다음 모든 요청에 JwtVerificationFilter에서 요청헤더의 토큰을 검증한다.</p>
<p>이렇게 기본 설정을 위한 클래스들을 만들고 다음으로 정의한 커스텀 필터들 또한 만들어 주었다.</p>
<pre><code>@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenizer jwtTokenizer;
    private final MemberService memberService;


//1. 로그인 요청이 들어올때 사용자가 기입한 크레덴셜을 검증한다.

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        ObjectMapper objectMapper = new ObjectMapper();
        LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
        // loginDto를 통해 유저가 credential의 검증을 요청할때 ObjectMapper를 통해 그 값을 읽어온다. 

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());

        return authenticationManager.authenticate(authenticationToken); //실제적인 인증은 현재 필터가 아닌 AuthenticationManager가 대신한다. 이때, 우리 서비스는 email을 유저네임으로 사용할 것이기 때문에 데이터베이스에서 사용자의 크리덴셜을 조회한 후, 조회한 크리덴셜을 AuthenticationManager에게 전달하는 UserDetailsService를 커스텀하여야 한다. 이후 이어서 설명하곘다.
    }

//2. 크레덴셜의 검증이 성공했을때 액세스 토큰과 리프레쉬 토큰을 JwtTokenizer클래스를 통해 generate하여 response의 헤더에 담아 클라이언트에 전송한다.

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Member member = (Member) authResult.getPrincipal();  

        String accessToken = delegateAccessToken(member);   
        delegateRefreshToken(member); 
        log.info(&quot;accessToken is generated&quot;);


        response.setHeader(&quot;Authorization&quot;, accessToken);  
        response.setHeader(&quot;Refresh&quot;, refreshToken);
    }

// * Jwt 토큰을 생성하는 메소드들 *

    private String delegateAccessToken(Member member) {
        Map&lt;String, Object&gt; claims = new HashMap&lt;&gt;();
        claims.put(&quot;username&quot;, member.getEmail());
        claims.put(&quot;roles&quot;, member.getRoles());

        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());

        String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());

        String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);

        return accessToken;
    }


    private String delegateRefreshToken(Member member) {
        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());

        String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);

        return refreshToken;
    }
}</code></pre><p>OAuth2.0인증을 위한 핸들러 또한 크게 다르지 않다. 다만, 오어스 로그인은 구글의 로그인 서비스 로직을 따라야 하므로 응답 헤더에 토큰을 담기보다는 url에 두가지 토큰을 담아 클라이언트로 넘겨주고 이를 프론트단에서 노출되지 않고 localStorage에 저장만 하는 역할을 가진 페이지를 만들어 최초 로그인 과정을 구현하였다. 또한 이때 사용자의 최소한의 정보를 DB에 저장하였다. </p>
<p>다음으로 최초의 로그인이 성공한 후의 모든 요청에 대해 토큰을 검증하기 위한 OncePerRequestFilter를 상속한 JwtVerificationFilter을 만들어 주었다.</p>
<pre><code>@Slf4j
public class JwtVerificationFilter extends OncePerRequestFilter {

    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;

    public JwtVerificationFilter(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    //1. 일차적으로 토큰이 우리 서버에서 발행한 형식에 맞는 토큰인지(Bearer)를 검증하고 맞지 않거나 토큰이 없다면 필터를 실행시키지 않고 인증을 실패시킨다.

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String authorization = request.getHeader(&quot;Authorization&quot;);

        return authorization == null || !authorization.startsWith(&quot;Bearer&quot;);
    }

//2. 서버에서 발행한 토큰이 존재한다면 다음으로 JwtTokenizer를 이용하여 서버의 Secret 키를 이용해 디코딩하고 디코딩에 성공했다면 그 유효시간을 다음으로 확인하여 유효시간이 지나지 않은 토큰이라면 인증을 성공시킨다.


    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            Map&lt;String, Object&gt; claims = verifyJws(request); // 1. 토큰을 검증
            setAuthenticationToContext(claims);           // 2. 검증이 성공하면 ContextHolder에 유저 정보를 담은 UsernamePasswordAuthenticationToken을 만들어 올린다.

        } catch (SignatureException se) {  //* 서버에서 만든 시크릿 키로 디코딩을 할 수 없을때 유효하지 않은 토큰임을 인지하고 인증을 실패시킨다.(실제적인 실패처리는 AuthenticationEntryPoint에서 진행된다.)
            request.setAttribute(&quot;exception&quot;, se);
        } catch (ExpiredJwtException ee) { //* 토큰의 만료시간이 지났을 경우 AuthenticationEntryPoint애서 이를 캐치하여 Refresh 토큰을 확인하고 Access 토큰을 재발급한다. 만약, 리프레쉬 토큰 또한 만료되었다면 인증을 실패 시킨다.
            request.setAttribute(&quot;exception&quot;, ee);
        } catch (Exception e) { //* 그외의 기타 예외들을 처리한다.
            request.setAttribute(&quot;exception&quot;, e);
        }



        filterChain.doFilter(request,response); // 필터체인에서 현재 필터를 실행 시킨다.

    }

    private void setAuthenticationToContext(Map&lt;String, Object&gt; claims) {
        String username = (String) claims.get(&quot;username&quot;);
        List&lt;GrantedAuthority&gt; authorities = authorityUtils.createAuthorities((List)claims.get(&quot;roles&quot;));

        Authentication authentication = new UsernamePasswordAuthenticationToken(username,null,authorities);

        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    private Map&lt;String, Object&gt; verifyJws(HttpServletRequest request) {
        String jws = request.getHeader(&quot;Authorization&quot;).replace(&quot;Bearer &quot;, &quot;&quot;);
        String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());
        Map&lt;String,Object&gt; claims = jwtTokenizer.getClaims(jws,base64EncodedSecretKey).getBody();
        return claims;
    }
}
</code></pre><p>doFilterInternal에서 발생한 예외들은 현재 필터에서 처리하지 않고 Attribute만 발생한 exception으로 바꾼뒤 AuthenticationEntryPoint에 그 처리를 위임한다. 나는 Refresh 토큰을 검증하여 새로운 Access 토큰을 발급해 줄 위치를 고민하며 그 흐름을 파악하였고 AuthenticationEntryPoint에서 만료된 토큰을 캐치하는 부분에서 이 일을 수행하면 될 것 이라 생각하였고 이를 적용하기 위해 커스텀 AuthenticationEntryPoint를 만들어 주었다.</p>
<h3 id="4-authenticationentrypoint">4. AuthenticationEntryPoint</h3>
<pre><code>@Slf4j
@Component
@RequiredArgsConstructor
public class MemberAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;


    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        Exception exception = (Exception) request.getAttribute(&quot;exception&quot;); // Exception을 담을 요청에서 해당 exception을 읽어 온다.

        if (exception instanceof ExpiredJwtException) { // 해당 Exception이 ExpiredJwtException에 해당한다면 아래의 코드들을 실행한다.

            Map&lt;String, Object&gt; claims = verifyJws(request);//리퀘스트 헤더에서 Refresh토큰을 읽어 유효성을 검증하고 유저 정보를 담은 claims를 읽어온다.

            // Refresh 토큰의 검증이 성공했다면 다음에서 새로운 Access 토큰을 발급한다.

            Map&lt;String, Object&gt;  newClaims = new HashMap&lt;&gt;();
            String username = (String) claims.get(&quot;sub&quot;);
            List&lt;String&gt; roles = authorityUtils.createRoles(username);
            newClaims.put(&quot;username&quot;,username);
            newClaims.put(&quot;roles&quot;, roles);

            Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
            String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());

            String accessToken = jwtTokenizer.generateAccessToken(newClaims,username,expiration, base64EncodedSecretKey);


            setAuthenticationToContext(newClaims); // ContextHolder 유저 정보를 저장해 로그인 상태를 등록한다. 


            //새로운 액세스 토큰 응답 헤더에 담아 전송

            response.setHeader(&quot;Authorization&quot;,accessToken);


        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Set the unauthorized status code
            response.getWriter().write(&quot;Token expired or invalid&quot;); // Set the error message
            logExceptionMessage(authException, exception);
        }

    }

    private void setAuthenticationToContext(Map&lt;String, Object&gt; claims) {
        String username = (String) claims.get(&quot;username&quot;);
        List&lt;GrantedAuthority&gt; authorities = authorityUtils.createAuthorities((List)claims.get(&quot;roles&quot;));

        Authentication authentication = new UsernamePasswordAuthenticationToken(username,null,authorities)

        SecurityContextHolder.getContext().setAuthentication(authentication);
    }



    private Map&lt;String, Object&gt; verifyJws(HttpServletRequest request) {
        String jws = request.getHeader(&quot;Refresh&quot;).replace(&quot;Bearer &quot;, &quot;&quot;);
        String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());
        Map&lt;String,Object&gt; claims = jwtTokenizer.getClaims(jws,base64EncodedSecretKey).getBody();
        return claims;
    }

    private void logExceptionMessage(AuthenticationException authException, Exception exception) {
        String message = exception != null ? exception.getMessage() : authException.getMessage();
        log.warn(&quot;Unauthorized error happened: {}&quot;, message);
    }

}</code></pre><p>여기까지 인증을 담당하는 필터들을 그 흐름에 맞게 설명하였다. 다음은 필터들 내부에서 공통적으로 사용되고 있는 토큰을 만들고 유효성을 검증하는 JwtTokenizer, AuthenicationManager가 DB에 저장된 사용자를 로드하여 로그인 정보와 비교하여 실제적인 인증을 실행하기 위해 커스텀해 주었던 커스텀 UserDetailsService 클래스를 설명하겠다.</p>
<p>먼저 JwtTokenizer의 모습이다.</p>
<pre><code>package com.marbleUs.marbleUs.common.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;

@Component
public class JwtTokenizer {

    @Getter
    @Value(&quot;${jwt.key.secret}&quot;)
    private String secretKey;

    @Getter
    @Value(&quot;${jwt.access-token-expiration-minutes}&quot;)
    private int accessTokenExpirationMinutes;

    @Getter
    @Value(&quot;${jwt.refresh-token-expiration-minutes}&quot;)
    private int refreshTokenExpirationMinutes;

    public String encodedBasedSecretKey(String secretKey){
        return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    public String generateAccessToken(Map&lt;String,Object&gt; claims,
                                      String subject,
                                      Date expiration,
                                      String base64EncodedSecretKey){
        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    public String generateRefreshToken(
                                      String subject,
                                      Date expiration,
                                      String base64EncodedSecretKey){
        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
        return Jwts.builder()
                .setSubject(subject)
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    public Jws&lt;Claims&gt; getClaims(String jws, String base64EncodedSecretKey){
        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        Jws&lt;Claims&gt; claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jws);

        return claims;
    }

    public void verifySignature(String jws, String base64EncodedSecretKey){

        Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

        Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jws);
    }

    public Date getTokenExpiration(int expirationMinutes){
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, expirationMinutes);
        Date expiration = calendar.getTime();
        return expiration;
    }

    private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) {
    byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
    Key key = Keys.hmacShaKeyFor( keyBytes );
    return key;
    }
}
</code></pre><p>다음은 UserDetailsService 인터페이스를 구현한 MemberDetailService의 모습이다. 유저네임이 이메일과 같음을 정의해 주어야 로그인시 받은 이메일 정보로 인증이 가능하다.</p>
<pre><code>@Component
@RequiredArgsConstructor
public class MemberDetailService implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final CustomAuthorityUtils authorityUtils;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional&lt;Member&gt; optionalMember = memberRepository.findByEmail(username);
        Member findMember = optionalMember.orElseThrow(() -&gt; new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

        return new MemberDetails(findMember);
    }

    private class MemberDetails extends Member implements UserDetails {
        public MemberDetails(Member findMember) {
                setId(findMember.getId());
                setEmail(findMember.getEmail());
                setPassword(findMember.getPassword());
                setRoles(findMember.getRoles());
        }

        @Override
        public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
            return authorityUtils.createAuthorities(this.getRoles());
        }


        @Override
        public String getUsername() {
            return getEmail(); // 해당 서비스에서는 유저네임이 이메일임을 정의한다.
        }

        @Override
        public boolean isAccountNonExpired() {
            return true;
        }

        @Override
        public boolean isAccountNonLocked() {
            return true;
        }

        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }

        @Override
        public boolean isEnabled() {
            return true;
        }
    }
}</code></pre><p>여기까지 일차적으로 JWT토큰인증 방식으로 로그인 기능을 구현하였다. 기능을 완성하였고 테스트도 성공을 하였지만 시나리오를 검증하고 프론트와 합을 맞추면서 몇가지 문제점들과 개선되어야 할 점들이 발견이 되었다. 다음에서 어떠한 문제점들이 발견하였는지와 그 해결책을 논의해 보겠다.</p>
<h3 id="5-trouble-shooting-1-문제-파악-1">5. trouble Shooting #1 문제 파악</h3>
<ol>
<li><p>가장 큰 문제점은 무엇보다도 보안이였다. 해당 로그인 인증파트를 맡은 프론트 협업자분이 토큰을 핸들링하는 부분을 굉장히 어려워 하셨다. 원래의 계획한 시나리오는 Access Token이 만료되었을 시 서버는 이를 캐치하여 Refresh 토큰을 요청하게 되고 프론트는 따로 다른 위치에 저장해 놓은 Refresh토큰을 서버로 보내주는 보안을 위한 추가적인 과정을 계획하였으나 이를 핸들링하는 부분에서 굉장히 어려워 하셨고 나는 이를 해결하기 위해 두가지 토큰을 모든 요청에 같이 보내고 이를 Access토큰 만료시에 Refresh토큰을 바로 검증하고 새로운 토큰을 발급해 주는 식으로 간단화 하였다. 하지만 Refresh토큰이 탈취될 수 있다는 굉장히 큰 리스크라 판단되어지는 문제가 대두되었다.</p>
</li>
<li><p>또한 네트워크의 비용에 대한 문제였다. 사실 큰 문제는 아니였지만 쿼리의 수와 그 양을 최대한 줄여 성능을 향상 시키는것은 언제나 서버 개발자의 숙명과도 같은 의무라 생각하는 나에게는 큰 고심거리였다. 모든 요청에 헤더 하나가 추가된다는 것은 나에게 결코 유쾌하지 않은 부분이였다. </p>
</li>
</ol>
<p><strong>해결책</strong></p>
<p>근본적인 해결책은 Refresh토큰의 핸들링을 서버에서 오롯이 전담하자는 것이였다. 제한된 시간과 리소스를 생각해 봤을때 프론트분이 이 문제를 다루는 부분에 익숙해 지기를 기다리는 것 보다는 서버에서 이를 핸들링하여 프론트의 수고를 덜어드리는게 나은 선택이라 판단되었다. 프론트의 작업량이 상당하다는것을 충분히 경험을 통해 인지하고 있었기에 내린 결정이였다. 또한 보안적인 측면에서 생각해 보았을때도 서버에서 Refresh 토큰을 보관하고 로딩하는 것이 훨씬 안전하다고 판단이 되어 더욱 확신을 가지고 진행 할 수 있었다. </p>
<p><strong>첫번째 시도</strong>: 처음에는 Refresh 토큰을 DB에 저장하는 쪽으로 시나리오를 수정하였다. DB에 Refresh 토큰을 저장하고 Access 토큰의 만료시 해당 Refresh토큰을 로드하여 검증/토큰 재발급하면 되므로 프론트는 딱히 이를 핸들링하는 부분에 신경을 쓰지 않아도 되었고 새롭게 헤더에 담겨오는 Access 토큰을 업데이트만 해주면 되는 부분이였고 보안적으로도 훨씬 안전해졌기에 충분히 만족할만한 부분이였다. 하지만 나는 여기서 새로운 문제, 혹은 개선사항들을 발견할 수 있었다. 그것들은 아래와 같다.</p>
<p><strong>1. 관계형 DB를 사용하고 있었기에 간단한 토큰하나를 읽어오는대도 맴버(유저)와 매핑된 토큰을 찾기위해 쓸데없이 추가적인 쿼리들이 발생한다는것</strong></p>
<p><strong>2. 450분이라는 짧은 만료시간을 가진 웹 애플리케이션임에 리프레쉬 토큰이 만료시에 필요한 토큰 삭제가 빈번하게 발생해야 했고 이를 위해 추가적인 쿼리들을 자주 발생시켜야 했기에 비효율적이라 느껴졌다.</strong></p>
<p>위의 두 문제들은 사소해 보였지만 향후 서비스가 커지고 사용자가 많아짐에 따라 서비스의 속도와 성능이 저하 될 수 있다고 생각이 들었다. 특히나 사용자가 인식하지 못하고 진행되어야 할 토큰 갱신부분 때문에 하나의 트랜잭션으로 묶인 비지니스 로직들의 속도가 저하될 수 있다는것은 바람직하지 않다고 생각되었다. 그래서 생각하였다. <strong>인메모리를</strong> 사용하자!</p>
<p><strong>두번째 시도</strong>: 그래서 나는 빠르게 그 값을 저장하고 읽어올 수 있는 자바의 인메모리, HashMap를 사용하여 이 문제를 해결하기로 했다. <strong>HashMap를</strong> 사용한다면 굉장히 향상된 속도로 토큰을 다른 테이블과 매핑할 필요없이 저장 및 삭제를 할 수 있었고 굉장히 효과적인 대안이 될 수 있을것으로 보였다. 그래서 키값으로 유저의 아이디를 두고 value값으로 토큰을 저장하는 형식으로 코드를 업데이트 하였고 이는 굉장히 효과적이였다. 하.지.만.. 상상도 못했던 아주 커다란 문제를 직면하고 말았다. 그것은 바로 <strong>데이터의 동시성</strong> 문제였다. 내가 이번 프로젝트 동안 가장 중점시 하였던것을 확장에 유연하게 대처할 수 있는 서버와 테이블을 만드는것이였는데 이 부분에서 아주 크게 이 해결책이 문제가 되었다. <strong>서비스가 스케일 아웃될 경우 자바의 인메모리를 사용한다면 모든 인스턴스가 같은 값을 공유하지 못하고 Refresh 토큰을 저장한 인스턴스와 사용자가 보낸 요청을 처리하는 인스턴스가 다르다면 Refresh 토큰을 읽어올 수 없다는 점! 이였다.</strong> 내가 원한 것은 인메모리 처럼 <strong>빠른 저장 삭제가 가능함</strong>과 동시에 그 <strong>데이터의 동시성을 보장</strong>해야 하는 저장소였다.</p>
<p>기존 처음 변경한 방식대로 관계형DB인 메인 RDS 데이터베이스에 저장한다면 속도는 조금 느려도 스케일 아웃시 문제가 없을 것이고 병목현상이 생긴다면 DB를 Sharding 함으로 해결 할 수 있으니 다시 이 방식으로 돌아가야 하는가 심각하게 고민을 하였다..</p>
<p><strong>마지막 시도</strong>: 언제나 그렇듯 답을 존재하였다. 그것을 바로 NoSQL을 사용하는것! 그중에서도 cache를 사용하는 <strong>Redis의</strong> 존재를 알게 되었고 이는 나의 서비스를 신세계로 인도하였다. 내가 원했듯이 저장 삭제가 간편하고 빠르고, 언제든 Remote 서버로 따로 띄워서 동시성문제도 해결할 수 있는 Redis의 존재가 바로 내가 찾은 최적의 답이었다. 그래서 나는 유저가 로그인을 시도하고 성공한 시점에 Redis에 유저의 Ip + Refresh를 키값으로 리프레쉬토큰을 저장하였고 이때 만료시간을 설정하여 만료시간이 다하면 자동으로 삭제가 되도록 구현하였고 로그아웃을 할때도 이가 삭제되도록 구현을 하였다. 이는 굉장히 효과적으로 내가 원했던 점들을 모두 충족시켰고 지금까지는 최고의 대안이라 여겨진다.</p>
<h3 id="6-trouble-shooting-2-적용-및-결과-1">6. trouble Shooting #2 적용 및 결과</h3>
<p>아래는 Redis를 이용해 만료시간을 두고 값을 저장하는 서비스 Layer를 구현한 코드이다.</p>
<pre><code>@Service
@RequiredArgsConstructor
public class RedisServiceUtil {

    private final StringRedisTemplate stringRedisTemplate;

    public String getData(String key) {
        ValueOperations&lt;String, String&gt; valueOperations = stringRedisTemplate.opsForValue();
        return valueOperations.get(key);
    }

    public void setDateExpire(String key, String value, long duration) {
        ValueOperations&lt;String, String&gt; valueOperations = stringRedisTemplate.opsForValue();
        Duration expireDuration = Duration.ofSeconds(duration);
        valueOperations.set(key, value, expireDuration);
    }

    public long expirationSecondGenerator(Instant now, Instant dueDate){
        long secondsBetween = ChronoUnit.SECONDS.between(now,dueDate);
        return secondsBetween;
    }

    public void deleteData(String key){
        if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(key))) stringRedisTemplate.delete(key);
    }
}</code></pre><p>해당 서비스 클래스는 StringRedisTemplate을 통해 값을 읽어오고 만료시간을 설정하여 값을 저장하고 마지막으로 삭제하는 역할을 한다. 해당 서비스 클래스를 사용하기 위해 인증처리 과정에서 AuthenticationFilter의 내용을 다음과 같이 수정해 주었다.</p>
<pre><code>    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Member member = (Member) authResult.getPrincipal();  

        String accessToken = delegateAccessToken(member);   
        String ip = extractor.getClientIP(request);
        delegateRefreshToken(member,ip); //
        log.info(&quot;accessToken is generated&quot;);


        response.setHeader(&quot;Authorization&quot;, accessToken);  
//        response.setHeader(&quot;Refresh&quot;, refreshToken); 삭제
    }

    private void delegateRefreshToken(Member member, String ip) {
        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());

        String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);

        Instant now = Instant.now();
        Instant expirationDate = expiration.toInstant();
        long secondsBetween = redisServiceUtil.expirationSecondGenerator(now,expirationDate);
        redisServiceUtil.setDateExpire(ip+&quot;_Refresh&quot;,refreshToken,secondsBetween);

//        return refreshToken; 삭제
    }

</code></pre><p>다음은 Access Token 만료시 Refresh 토큰을 읽어 새로운 토큰을 발급해 주는 AuthenticationEntryPoint의 수정 내용이다.</p>
<pre><code>    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        Exception exception = (Exception) request.getAttribute(&quot;exception&quot;);

        if (exception instanceof ExpiredJwtException) {
//            String refreshToken = request.getHeader(&quot;Refresh&quot;); 삭제



            Map&lt;String, Object&gt; claims = verifyJws(request); &lt;1&gt;
            Map&lt;String, Object&gt;  newClaims = new HashMap&lt;&gt;();
            String username = (String) claims.get(&quot;sub&quot;);
            List&lt;String&gt; roles = authorityUtils.createRoles(username);
            newClaims.put(&quot;username&quot;,username);
            newClaims.put(&quot;roles&quot;, roles);

            Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
            String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());
            String accessToken = jwtTokenizer.generateAccessToken(newClaims,username,expiration, base64EncodedSecretKey);


            setAuthenticationToContext(newClaims);


            //새로운 액세스 토큰 응답 헤더에 담아 전송

            response.setHeader(&quot;NewAccessToken&quot;,accessToken);


        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Set the unauthorized status code
            response.getWriter().write(&quot;Token expired or invalid&quot;); // Set the error message
            logExceptionMessage(authException, exception);
        }

     private Map&lt;String, Object&gt; verifyJws(HttpServletRequest request) {

        //&lt;1&gt; 레디스에 저장되어 있는 리프레쉬 토큰을 읽어와 검증하는 부분

        String ip = interceptor.getClientIP(request);
        String jws = redisServiceUtil.getData(ip+&quot;_Refresh&quot;).replace(&quot;Bearer &quot;, &quot;&quot;);
        String base64EncodedSecretKey = jwtTokenizer.encodedBasedSecretKey(jwtTokenizer.getSecretKey());
        Map&lt;String,Object&gt; claims = jwtTokenizer.getClaims(jws,base64EncodedSecretKey).getBody();
        return claims;
    }

    }</code></pre><p>인메모리를 사용하는 cache기반의 NoSQL, Redis를 사용하여 Refresh토큰을 저장하고 관리함으로 다음과 같은 장점을 가지게 되었다. 1.클라이언트에 Refresh토큰을 보관하지 않아 보다 보안성을 높이고 2. 인메모리 NoSQL DB의 이점인 빠른 속도로 저장 조회 할 수 있고 3. 만료 시간을 설정하여 자동으로 삭제를 가능하게하고 4. 언제든지 Remote서버로 분리시켜 데이터의 동시성을 지켜서 Scale Out에 유리한 구조를 가져갈 수 있다. </p>
<p><strong>다만,</strong> Redis 서버가 정상적으로 동작하지 않을 경우 모든 로그인/인증 서비스가 진행되지 않는다는 점과 인메모리를 지속적으로 사용하기에 사용자의 메모리 사용량을 예측/분석하여 메모리를 관리하여야 한다는 고민점들이 여전히 남아있고 개선/최적화해 나갈 부분이라 생각한다. </p>
<h3 id="7-회고-1">7. 회고</h3>
<p>사용자 인증에 대한 기능은 어떠한 서비스에서도 기반이 되는 핵심 기능중 하나이다. 따라서 그 속도와 안정성, 그리고 보안 부분에서 그 기능을 항상 최적화 시키고 안정화 시켜야 하는 기능이다. 특히나 보안은 서비스를 운영하는데 있어서 근반이 되는 가장 중요한 부분이기에 기능을 구현하는데 지속적으로 개선점과 문제점을 찾고 고민해왔고 지금도 개선점들을 찾고 있다. </p>
<p>그 과정중에 대표적인 cache기반 데이터 저장소인 Redis를 공부하게 되었고 Redis와 cache에 대해 더욱 깊이 공부해보려한다. </p>
<p>이번 프로젝트에서 인증 부분에 Redis를 사용하게 되었던 이유들에 따라 개선점을 찾고 서비스의 질을 높일 수 있었던거 같아서 어느정도 만족하고 있다. 캐시는 굉장히 매력적인 기술이고 서비스의 속도를 개선하는데 있어서 최고의 기술임에는 분명하다. 사실, 로그인 부분보다는 사실 반복적으로 동일한 결과를 돌려주는 이미지나 썸네일등을 리스폰스해야 하는 경우에 더욱 효과적으로 장점을 활용할 수 있을것이다. </p>
<p>그 방법은 중간에 Redis를 두어 한번 조회될때 이를 메인 DB에서 읽어옴과 동시에 Redis에 저장하고 Redis를 중간에 조회하여 기록이 있으면 빠르게 같은 응답값을 리턴하는 등의 방법으로 사용하여 서비스의 로딩 속도를 확실히 개선 시킬 수 있을 것이다. </p>
<p>다만, 어떠한 기술이든 장점만 있지 않았다. 캐시는 읽고 쓰는데 있어 빠른 성능을 제공하지만 저장 공간이 작고 비용이 비싸다는 단점이 있고 이를 고려하지 않고 캐시만을 고집한다면 분명 서버는 높은 메모리 사용량을 감당하지 못하고 다운되어 버리고 말 것이다. </p>
<p>따라서 서비스의 환경, 혹은 고려해야 할 여러가지 측면들에 따라 기존의 방식 혹은 다른 기술들이 더 나을 경우가 분명 있을것이고 알고있는 기술만을 사용하는 것이 아니라 그 니즈에 따라 적절한 기술을 잘 취사선택 하는것이 서버 개발자의 가장 중요한 덕목 중 하나라고 생각한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MarbleUs Project #1 기능 정의 및 테이블 설계]]></title>
            <link>https://velog.io/@johnny_debt/MarbleUs-Project-1</link>
            <guid>https://velog.io/@johnny_debt/MarbleUs-Project-1</guid>
            <pubDate>Tue, 05 Sep 2023 04:32:48 GMT</pubDate>
            <description><![CDATA[<h4 id="table-of-contents">table of contents</h4>
<hr>
<h5 id="1-프로젝트-소개">1. 프로젝트 소개</h5>
<h5 id="2-사용자-기능-정의서">2. 사용자 기능 정의서</h5>
<h5 id="3-테이블-설계">3. 테이블 설계</h5>
<hr>
<h3 id="week1">Week#1</h3>
<h3 id="1-프로젝트-소개-1">1. 프로젝트 소개</h3>
<p>현실판 부루마블 게임을 기반으로 하는 여행 게시판.
주사위를 굴려 랜덤하게 도착하는 한국의 주요 도시 및 지역들을 여행을 조금더 풍요롭게해 줄 미션을 수행하며 실제로 여행하고 이를 해당 도시게시판에 블로깅하는 게임적인 요소가 가미된 여행 커뮤니티 게시판을 기획하였다. </p>
<p>해당 서비스는 크게 두가지로 구성된다.
첫째, 게임부. 게임부는 유저가 실제로 여행을 계획하는 단계에서 주사위를 굴리고 그 눈만큼을 유저의 캐릭터가 이동하게 되고 도착한 도시에서 미션을 수행하며 실제 여행을 다녀오고 이를 블로깅을 통해 인증한다면 리워드로 스탬프를 받고 다음 도시를 여행하기 위해 주사위를 굴린다. 캐릭터가 한바퀴를 돌게되면 레벨이 1 상승한다. 만약 같은 도시를 또 방문하게 된다면 유저는 두가지 선택을 할 수 있다. 1. 주사위를 다시 굴려 다른 지역을 여행하기, 2. 해당 도시를 새로운 레벨2의 미션을 수행하며 다시 여행하기.  </p>
<p>둘째, 커뮤니티 게시판. 유저가 게임을 굳이 플레이하지 않더라도 해당 서비스는 여행 커뮤니티 게시판으로서 그역할을 지속한다. 해당 게임보드에 존재하는 도시 및 도들은 해당 여행 리뷰들을 게시한 게시판의 역할을 하고 유저들은 태그를 바탕으로 경험과 재미를 공유하는 커뮤니티 활동을 지속할 수 있다. </p>
<h3 id="2-사용자-기능-정의">2. 사용자 기능 정의</h3>
<ol>
<li>Bare Minimun(기본 필수 기능)</li>
</ol>
<ul>
<li><p>인증 및 보안</p>
<ul>
<li>Jwt 토큰 인증 방식: 회원가입/로그인을 통해 서버는 Access&amp;Refresh 토큰을 발급하고 이를 통해 로그인 인증을 수행한다.</li>
<li>OAuth2.0 기반 Google로그인 지원</li>
<li>로그인/비로그인의 권한 분리: 로그인 사용자: 마이페이지, 블로깅 / 비로그인 사용자: 주사위 굴리기 및 도착도시와 미션내용 확인만 가능(회원가입을 유도하기 위함)</li>
<li>https 프로토콜 적용</li>
</ul>
</li>
<li><p>공통 기능</p>
<ul>
<li>인기글 정렬: 게시글의 조회수에 따라 페이지네이션처리 후 정렬</li>
<li>데이터 초기화: 각 DB의 데이터를 일괄 삭제</li>
<li>데이터 전체 초기화: 모든 DB데이터 초기화</li>
<li>게시물 조회수 카운트</li>
<li>예외처리: 예외 발생시 메세지를 유저 친화적으로 정리</li>
<li>작성/수정시간 기록: 데이터의 생성 수정 시간 기록</li>
</ul>
</li>
<li><p>맴버쉽</p>
<ul>
<li>회원 가입/탈퇴</li>
<li>회원 프로필 이미지 등록</li>
<li>회원간의 프로필 조회</li>
<li>닉네임 자동 생성 후 수정 가능</li>
</ul>
</li>
<li><p>메인 게임부</p>
<ul>
<li>공통기능: <pre><code>  - 주사위 굴리기
  - 도시별/맴버별 랜덤 미션 배정
  - 랜덤여행 결과 공유</code></pre><ul>
<li>회원기능: <ul>
<li>마이페이지</li>
<li>게시물 열람</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>게시판</p>
<ul>
<li>도시 게시판:<pre><code>   - 도시 여행후 블로깅:
       에디터 사용 이미지 및 파일 업로드
   - 태그를 이용한 카테고리별 정렬
   - 날씨정보
   - 작성된 블로그 상세 페이지 보기
   - 블로그 작성/수정/삭제
   - 북마크 기능
      - 작성된 블로그 댓글 작성/조회/수정/삭제</code></pre></li>
</ul>
<ul>
<li><p>마이페이지(여권)</p>
<ul>
<li>필수 기능<ul>
<li>회원 정보 조회</li>
</ul>
</li>
<li>회원 정보 수정</li>
<li>책갈피 클릭 시 페이지 이동</li>
<li>내가 쓴 후기 조회</li>
<li>내가 쓴 후기 삭제</li>
<li>내 북마크 조회</li>
<li>내 북마크 삭제</li>
<li>유저 팔로우 조회</li>
<li>유저 팔로우 추가</li>
<li>유저 팔로우 삭제</li>
<li>팔로우 수/팔로워 수 조회</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="3-테이블-설계-1">3. 테이블 설계</h3>
<p><img src="https://velog.velcdn.com/images/johnny_debt/post/bce4b93d-81a5-480f-8e1d-de228650a196/image.png" alt=""></p>
<hr>
<h3 id="설명-및-회고">설명 및 회고</h3>
<hr>
<p>설계가 무엇보다 어렵고 중요하다는걸 많이 깨달았다. 특히 백앤드 프로그래머로써 테이블 설계가 얼마나 중요한지 느꼈다. 테이블 설계에 있어 가장 중점을 두었던 점은 최대한 수정과 업데이트에 용이하도록 만드는 것 그리고 내부 쿼리수를 최대한 줄이는 설계가 핵심이었다. </p>
<p>예를들어, city게시판을 만드는데 있어 각각의 도시 게시판을 만들고 블로그들을 콜럼으로 관리할 것인가 아니면 도시게시판 자체도 컬럼으로 관리할 것인가가 첫번째 난관이였다. 전자의 경우에는 조인되는 추가 쿼리를 줄이고 블로그의 수가 많아지는 경우를 대비할 수 있다는 장점이 있다는 생각이 들었지만 단점이 명확해 보여 후자를 선택하였다. 단점으로는 블로그만 분리해서 쿼리를 할 수 없어 불필요한 네트워크 비용을 발생시킬 수 있다는 생각을 했다(다양한 쿼리를 발생시켜야 하는데 복잡성이 올라서 속도 저하를 초래할 수 있다).</p>
<p>또는 미션 부분에서 각각의 도시의 특수한 미션들과 공통된 미션, 두가지 성질을 가진 미션들이 있는데 이를 어떻게 관리할 것인가. 처음에는 mission과 special_mission 두가지로 테이블을 나누고 공통미션은 시티와 조인테이블을 만들어 매핑하여 다양한 도시에서 공통된 미션을 사용 할 수 있게 하고 일대다의 형태로 시티와 special_mission을 묶어 special_mission이 시티에 고정값으로 종속되도록 설계하였으나 거의 비슷한 내용의 두 미션테이블이 쓸데없는 복잡성을 나았고 결국 이또한 성능 저하로 이어질 가능성이 대두되었다. 따라서, 미션을 모두 공통으로 두고 타입을 이넘으로 관리하며 부여하여 이 두가지 미션의 역할을 모두 충족시킬 수 있도록 하였다. </p>
<p>또한 이미지 또한 처음에는 member_image와 blog_image의 두 조인테이블을 만들어 분류하기 쉽게 관리하고자 하였는데 이 또한 불필요한 쿼리 수의 증가로 이어짐을 발견하여 이미지에 맴버와 블로그의 Nullable한 Fk를 두어(매핑하여) 관리하는 식으로 변경하였다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Pre-Project 회고]]></title>
            <link>https://velog.io/@johnny_debt/Pre-Project-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@johnny_debt/Pre-Project-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 23 Aug 2023 05:46:05 GMT</pubDate>
            <description><![CDATA[<h3 id="혼자-잘한다고-되는게-아닌걸-뼈저리게-느낀-pre-project">혼자 잘한다고 되는게 아닌걸 뼈저리게 느낀 pre-project!</h3>
<p><em>Abstract</em></p>
<hr>
<p>사실 프로젝트를 시작함에 있어서 자신감이 충만했던 나였다. 전체적으로 어떻게 만들면 될지 머리속에 잡힐듯이 그려졌고 아무런 문제도 예상되지 않았다. 하지만 막상 시작해보니 모든 팀원들의 이해도도 또 관점들도 많이 달랐고 소통의 중요성을 한번더 깨달았다.</p>
<hr>
<h4 id="내가-분석한-나의-잘한점">내가 분석한 나의 잘한점</h4>
<hr>
<p><strong>1. 업무 이해도가 높고 실행력이 좋다.</strong>
    정확히 사용자 요구사항 정의서를 바탕으로 필요한 기능들을 파악할 수 있고 이를 구현할 수 있다.</p>
<p><strong>2. 문제 해결 능력</strong>
    전혀 생소한 문제일라도 다양한 시각에서 문제를 분석하고  조사하여 해결해 나가는 능력이 뛰어나다.</p>
<p><strong>3. 창의성</strong>
    문제를 해결할때 기존의 지식에 얽메이지 않고 다양한 시각에서 고민하고 이를 적용해 문제를 해결하는 능력이 뛰어나다.</p>
<p><strong>3. 코드 파악 및 활용능력</strong>
    기존의 코드들의 플로우를 파악하고 이해함이 빠르고 이를 잘 응용하여 필요에 맞는 코드로 활용하는 능력이 뛰어나다.</p>
<p><strong>4. 빠른 코딩 능력</strong>
    비교적 빠른 코딩 능력이 있다. 기존의 코드들을 활용하고 응용하여 빠르게 기능을 만들 수 있다.</p>
<p><strong>5. 문제 해결을 위한 맨탈과 리더쉽</strong>
    예측할 수 없는 상황이나 어쩔 수 없는 문제들을 만났을때 당황하지 않고 새로운 목표와 path를 빠르게 제시하여 팀원들의 최대 퍼포먼스를 끌어낼 수 있다.</p>
<hr>
<h4 id="내가-분석한-나의-부족했던-점">내가 분석한 나의 부족했던 점</h4>
<hr>
<p><strong>1. 세밀함 부족</strong>
세밀하게 타임라인을 바탕으로 계획을 짜고 기록을 남기는 등의 세심함이 많이 부족하다.</p>
<p><strong>2. 팀빌딩 능력의 부족</strong>
조금더 끈끈하고 합심된 팀을 만들기 위해 다양한 방법과 시도가 부족했음을 많이 느낀다.</p>
<p><strong>3. 부족한 소통</strong>
조금더 적극적으로 소통에 참여하지 않는 맴버들과 소통하기를 시도했어야 한다고 생각한다. 또한 더 이야기를 잘 들어주는 리더가 되어야 한다고 반성한다.</p>
<p><strong>4. 부실한 기획 &amp; 설계 단계</strong>
내가 알고 생각한 것들을 다들 알고 이해할거라고 생각하고 구현을 서둘렀지만 사실 구현보다 더 중요한것은 세밀하고 꼼꼼하게 시나리오를 세우고 점검하고 팀에서 통용되어질 약속들을 만드는 작업들이 였고 이 부분이 먼저 튼튼하게 선행되었어야 했음을 느꼈다.</p>
<hr>
<p><strong>회고</strong></p>
<hr>
<p>만약 메인프로젝트에서 한번더 팀장을 하게 된다면 기획하고 설계하는 시간을 좀더 투자해서 튼튼한 주춧돌을 만들고 구현을 시작해야 할 것 같다. 또한 팀원들의 멘탈과 애로사항들을 잘 파악하고 progress를 기록하는 일이 매우 중요함을 느끼고 이를 실현해보고 싶다.   </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security: Authority]]></title>
            <link>https://velog.io/@johnny_debt/Spring-Security-Authority</link>
            <guid>https://velog.io/@johnny_debt/Spring-Security-Authority</guid>
            <pubDate>Wed, 12 Jul 2023 13:27:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Index</p>
</blockquote>
<ol>
<li>Spring Security의 권한 부여 처리 흐름</li>
<li>Spring Security의 권한 부여 컴포넌트</li>
</ol>
<h3 id="1-권한-부여-처리-흐름">1. 권한 부여 처리 흐름</h3>
<ol>
<li><p>AuthorizationFilter가 SecurityContextHolder로부터 Authentication을 획득</p>
</li>
<li><p>SecurityContextHolder로부터 획득한 Authentication과 HttpServletRequest를 AuthorizationManager에게 전달</p>
</li>
<li><p>AuthorizationManager의 구현체인 RequestMatcherDelegatingAuthorizationManager는 RequestMatcher 평가식을 기반으로 해당 평가식에 매치되는 AuthorizationManager에게 권한 부여 처리를 위임(RequestMatcherDelegatingAuthorizationManager가 직접 권한 부여 처리를 하는 것이 아니라 RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스에게 위임)</p>
</li>
<li><p>적절한 권한 여부를 판단하여 User의 Access의 허용 여부를 결정하여 다음 프로세스의 실행 여부를 결정</p>
</li>
</ol>
<h3 id="2-spring-security의-권한-부여-컴포넌트">2. Spring Security의 권한 부여 컴포넌트</h3>
<blockquote>
<ol>
<li>AuthorizationFilter</li>
</ol>
</blockquote>
<ul>
<li>AuthorizationFilter는 URL을 통해 사용자의 액세스를 제한하는 권한 부여 Filter: AuthorizationFilter 객체가 생성될 때, AuthorizationManager를 DI 받아 AuthorizationManager를 통해 권한 부여 처리를 진행</li>
<li>AuthorizationManager의 구현 클래스에 따라 권한 체크 로직이 다르다.</li>
</ul>
<blockquote>
<ol start="2">
<li>AuthorizationManager</li>
</ol>
</blockquote>
<ul>
<li>권한 부여 처리를 총괄하는 매니저 역할을 하는 인터페이스</li>
</ul>
<blockquote>
<ol start="3">
<li>RequestMatcherDelegatingAuthorizationManager</li>
</ol>
</blockquote>
<ul>
<li>AuthorizationManager의 구현 클래스중 하나로, 직접 권한 부여 처리를 수행하지 않고 RequestMatcher를 통해 매치되는 AuthorizationManager 구현 클래스에게 권한 부여 처리를 위임</li>
<li>RequestMatcher는 SecurityConfiguration에서 .antMatchers(&quot;/orders/**&quot;).hasRole(&quot;ADMIN&quot;) 와 같은 메서드 체인 정보를 기반으로 생성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security: Authentication]]></title>
            <link>https://velog.io/@johnny_debt/Spring-Security-Authentication</link>
            <guid>https://velog.io/@johnny_debt/Spring-Security-Authentication</guid>
            <pubDate>Tue, 11 Jul 2023 13:31:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>++Table of Contents</p>
</blockquote>
<ol>
<li>Spring Security 기본 구조 및 웹 요청 처리 흐름</li>
<li>인증 처리 흐름</li>
<li>인증 컴포넌트</li>
</ol>
<h3 id="1-spring-security-기본-구조와-처리-흐름">1. Spring Security 기본 구조와 처리 흐름</h3>
<ul>
<li>기본적인 흐름 이해</li>
</ul>
<blockquote>
<p>사용자가 어떠한 리소스에 접근하기 원할때:
사용자가 Credential(Password)와 함께 보낸 인증 요청을 Spring Security Filter중 하나인, 인증관리자 역할을 하는 AbstractAuthenticationProcessingFilter를 상속하는 필터가 잡아 크리텐셜 저장소에서 사용자의 크리덴셜을 조회, 비교 검증을 수행한다. 유효한 크리덴셜이라면 접근 결정 관리자가 사용자의 Authoritiy를 검증하고 접근의 허용여부를 결정한다.</p>
</blockquote>
<blockquote>
<ul>
<li>자바는 클라이언트에서 엔드포인트에 요청이 도달하기 전에 중간에서 요청을 가로채 특정 처리를 할 수 있게 하는 적절한 포인트인 Servelet Filter를 제공한다. 이때 하나이상의 필터들을 연결해 Filter Chain을 구성할 수 있다.</li>
</ul>
</blockquote>
<ul>
<li>Spring Security는 DelegatingFilterProxy와 FilterChainProxy 클래스를 이용해 느슨하게 Servelet Filter들의 중간에 연결되어 보안 관련 기능을 수행한다.</li>
</ul>
<h4 id="1-인증관리자">1. 인증관리자</h4>
<blockquote>
<ol>
<li>UsernamePasswordAuthenticationFilter
(AbstractAuthenticationProcessingFilter상속): </li>
</ol>
</blockquote>
<ul>
<li>전달받는 인증 데이터를 기반으로 인증을 처리;
UsernamePasswordAuthenticationFilter의 경우 로그인 폼에서 제출되는 Username과 Password를 통한 인증을 처리한다.</li>
<li>UsernamePasswordAuthenticationFilter는 클라이언트로부터 전달받은 Username과 Password를 Spring Security가 인증 프로세스에서 이용할 수 있도록 UsernamePasswordAuthenticationToken을 생성</li>
</ul>
<blockquote>
<ol start="2">
<li>AbstractAuthenticationProcessingFilter:</li>
</ol>
</blockquote>
<ul>
<li>HTTP 기반의 인증 요청을 처리</li>
<li>실질적인 인증 시도는 하위 클래스에 맡기고, 인증에 성공하면 인증된 사용자의 정보를 SecurityContext에 저장하는 역할을 한다.
(SecurityContextHolder를 통해 사용자의 인증 정보를 SecurityContext에 저장한 뒤, SecurityContext를 HttpSession에 저장)</li>
</ul>
<blockquote>
<ol start="3">
<li>UsernamePasswordAuthenticationToken</li>
</ol>
</blockquote>
<ul>
<li>Spring Security에서 Username/Password로 인증을 수행하기 위해 필요한 토큰으로, 인증 성공 후 인증에 성공한 사용자의 인증 정보가 UsernamePasswordAuthenticationToken에 포함되어 Authentication 객체 형태로 SecurityContext에 저장한다.</li>
</ul>
<blockquote>
<ol start="4">
<li>Authentication</li>
</ol>
</blockquote>
<ul>
<li>Authentication은 Spring Security에서의 인증 자체를 표현하는 인터페이스로 인증을 위해 생성되는 인증 토큰 또는 인증 성공 후 생성되는 토큰은 UsernamePasswordAuthenticationToken과 같은 하위 클래스의 형태로 생성되지만 생성된 토큰을 리턴 받거나 SecurityContext에 저장될 경우에 Authentication 형태로 리턴 받거나 저장된다.<ul>
<li>Principal</li>
</ul>
</li>
</ul>
<p>Principal은 사용자를 식별하는 고유 정보</p>
<p>일반적으로 Username/Password 기반 인증에서는 Username이 Principal이 되며, 다른 인증 방식에서는 UserDetails가 Principal이 된다.</p>
<ul>
<li>Credentials</li>
</ul>
<p>사용자 인증에 필요한 Password를 의미하며 인증이 이루어지고 난 직후, ProviderManager가 해당 Credentials를 삭제</p>
<ul>
<li>Authorities</li>
</ul>
<p>AuthenticationProvider에 의해 부여된 사용자의 접근 권한 목록.
일반적으로 GrantedAuthority 인터페이스의 구현 클래스는 SimpleGrantedAuthority이다.</p>
<blockquote>
<ol start="5">
<li>AuthenticationManager</li>
</ol>
</blockquote>
<ul>
<li>인증 처리를 총괄하는 매니저 역할을 하는 인터페이스로 Filter는 AuthenticationManager를 통해 느슨한 결합을 유지하고 있으며, 인증을 위한 실질적인 관리는 AuthenticationManager를 구현하는 구현 클래스를 통해 이루어진다.</li>
</ul>
<blockquote>
<ol start="6">
<li>ProviderManager</li>
</ol>
</blockquote>
<ul>
<li>AuthenticationManager 인터페이스의 구현 클래스</li>
<li>AuthenticationProvider를 관리하고, AuthenticationProvider에게 인증 처리를 위임</li>
</ul>
<blockquote>
<ol start="7">
<li>AuthenticationProvider</li>
</ol>
</blockquote>
<ul>
<li><p>AuthenticationProvider는 AuthenticationManager로부터 인증 처리를 위임받아 실질적인 인증 수행을 담당하는 컴포넌트</p>
</li>
<li><p>Username/Password 기반의 인증 처리는 DaoAuthenticationProvider가 담당하고 있으며, DaoAuthenticationProvider는 UserDetailsService로부터 전달받은 UserDetails를 이용해 인증을 처리</p>
</li>
</ul>
<blockquote>
<ol start="8">
<li>UserDetails</li>
</ol>
</blockquote>
<ul>
<li>데이터베이스 등의 저장소에 저장된 사용자의 Username과 사용자의 자격을 증명해 주는 크리덴셜(Credential)인 Password 그리고 사용자의 권한 정보를 포함하는 컴포넌트이며, AuthenticationProvider는 UserDetails를 이용해 자격 증명을 수행</li>
</ul>
<blockquote>
<ol start="9">
<li>UserDetailsService</li>
</ol>
</blockquote>
<ul>
<li><p>UserDetails를 로드(load)하는 핵심 인터페이스</p>
</li>
<li><p>loadUserByUsername(String username)을 통해 사용자의 정보를 로드하여 AuthenticationProvider에 인증정보를 UserDetails로 전달</p>
</li>
</ul>
<blockquote>
<ol start="10">
<li>SecurityContext &amp; SecurityContextHolder</li>
</ol>
</blockquote>
<ul>
<li>SecurityContext는 인증된 Authentication 객체를 저장하는 컴포넌트이고, SecurityContextHolder는 SecurityContext를 관리하는 역할을 담당</li>
<li>Spring Security는 SecurityContext에 값이 채워져 있다면 인증된 사용자로 간주</li>
<li>SecurityContextHolder를 통해 인증된 Authentication을 SecurityContext에 설정할 수 있고 또한 SecurityContextHolder를 통해 인증된 Authentication 객체에 접근할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Section3 회고]]></title>
            <link>https://velog.io/@johnny_debt/Section3-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@johnny_debt/Section3-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 06 Jul 2023 08:35:18 GMT</pubDate>
            <description><![CDATA[<p>너무 재미있었던 Section3
머리속에서 조각 조각 정리되어있지 않았던 지식과 배움들이 하나로 합쳐지며 스스로 하나의 애플리케이션을 만들 수 있게된 뿌듯하고 재밌었던 섹션이였다. 하나 하나 구현해보고 싶었던 기능들이 구현됨을 경험하면서 자신감도 생기고 아직 배워야할것들이 까마득하게 많음을 느껴 한편으로는 그 지식의 방대함에 압도되는 그런 섹션이였다.</p>
<p>이번 섹션에는 사소한 배움이라도 꼭 기록하자 다짐했지만 결국 몇일가지는 못했던것이 가장 큰 아쉬움인듯 하다. 심기일전하며 새로운 KPT를 만들어보자.</p>
<hr>
<p>KEEP</p>
<hr>
<ol>
<li>만들고 만들고 만들어보자! 몸으로 체화시키는것 만큼 빠른 배움은 없는듯 하다.</li>
<li>오류 메세지들을 정독하자. 나의 장점인 영어를 모국어처럼 쓴다는 점을 살려, 먼저, 오류 메세지들을 잘 읽어보니 디버깅이 한결 수월해짐을 느꼈다. 사소한 Syntax 오류부터 처음 만나는 에러들까지 생각보다 금방 해결할 수 있었다.</li>
<li>끊임없이 리펙토링 해보자. 어디서 어느 계층에서, 클래스에서 이 기능들이 구현되어야 할까? 다른 사람들이 읽기에 가독성이 떨어지지는 않을까 고민해보며 리펙토링하는 시간들이 가장 큰 배움과 공부가되었던것 같다.</li>
</ol>
<hr>
<p>Problem</p>
<hr>
<ol>
<li>솔직히 블로깅이 너무 귀찮다...</li>
<li>시간이 부족하다.. 하루에 5시간만 더 있다면..</li>
</ol>
<hr>
<p>Try</p>
<hr>
<ol>
<li>스터디 그룹에 참여해 같이 공부를 진행해 볼 생각이다. </li>
<li>프로젝트를 위해 아이디어를 구체화하고 설계해본다.</li>
<li>블로깅 최소한 픽스한 버그에 한해서라도 하자 제발!</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[@ExceptionHandler를 이용한 예외 처리
]]></title>
            <link>https://velog.io/@johnny_debt/ExceptionHandler%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@johnny_debt/ExceptionHandler%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Thu, 15 Jun 2023 13:58:44 GMT</pubDate>
            <description><![CDATA[<p>DTO의 Validation의 유효성검사 혹은 클라이언트 및 서버에서 원인이 되는 다양한 에러들을 처리하기 위해 스프링에서 지원하는 @ExceptionHandler를 이용할 수 있다.</p>
<p>또한 모든 클래스에 공통적으로 적용되는 예외처리를 위해 스프링은 @RestControllerAdvice를 지원하여 공통사항을 모아 예외처리할 수 있도록 해준다. 이때 에러 정보들을 기반으로 정보를 담아 클라이언트에 전달하는 ErrorResponse 클래스를 만들어 각각의 @ExceptionHandler가 잡은 예외사항들의 정보를 필요한 정보만 매핑하여 클라이언트에 전달한다.</p>
<ul>
<li>Checked Exception vs Unchecked Exception</li>
</ul>
<p>Checked Exception : 발생한 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피하든가 등의 어떤 구체적인 처리를 해야 하는 예외</p>
<p>Unchecked Exception : 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외</p>
<ul>
<li>개발자가 의도적으로 예외를 던저서 처리할 수 도 있다. 그 예로</li>
</ul>
<ol>
<li>백엔드 서버와 외부 시스템과의 연동에서 발생하는 에러 처리</li>
<li>시스템 내부에 조회하는 리소스가 없는 경우
가 있다. 이때 서버에서 처리할 수 있는 예외사항이 아니므로 클라이언트 측에 이를 알려 처리할 수 있다.</li>
</ol>
<ul>
<li>서비스 계층에서 throw 키워드를 통해 예외 사항을 던지고 이를 Exception Advice에서 catch하여 처리한다.</li>
</ul>
<p>Custom Exception 처리</p>
<p>개발자는 Enum 클래스를 만들어 예외 코드를 정의하고 이를 예외 사항 클래스를 만들어 이의 객체를 이용해 BusinessLogic을 처리할때 발생하는 다양한 에러를 원하는 방법대로 처리할 수 있다.</p>
<hr>
<p>요약</p>
<ul>
<li>체크 예외(Checked Exception)는 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피를 하든가 등의 어떤 구체적인 처리를 해야 하는 예외</li>
<li>언체크 예외(Unchecked Exception)는 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외를 의미</li>
<li>RuntimeException을 상속한 예외는 모두 언체크 예외(Unchked Exception)이다.</li>
<li>RuntimeException을 상속해서 개발자가 직접 사용자 정의 예외(Custom Exception)를 만들 수 있다.</li>
<li>사용자 정의 예외(Custom Exception)를 정의해서 서비스 계층의 비즈니스 로직에서 발생하는 다양한 예외를 던질 수 있고, 던져진 예외는 Exception Advice에서 처리할 수 있다.</li>
<li>@ResponseStatus 애너테이션은 고정된 예외를 처리할 경우에 사용할 수 있다.
HttpStatus가 동적으로 변경되는 경우에는 ResponseEntity를 사용한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC/API 계층 + 서비스 계층]]></title>
            <link>https://velog.io/@johnny_debt/Spring-MVCAPI-%EA%B3%84%EC%B8%B5-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@johnny_debt/Spring-MVCAPI-%EA%B3%84%EC%B8%B5-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Tue, 13 Jun 2023 14:23:27 GMT</pubDate>
            <description><![CDATA[<p>오늘은 만들어 놓은 API Controller를 Mapper와 Service 클래스를 이용하여 연결하는 법을 연습했다.</p>
<p>이때 가장 중요한 포인트는 기능적으로 분리된 계층 구조를 만드는 것이였다. </p>
<p>이는 받는 데이터로 만든 Dto클래스들로 담고 다시 응답을 리턴하는 API 계층과 Entity 클래스를 이용하여 비지니스 로직을 처리하는 Service 계층에 연결하는 방법에서 출발한다.</p>
<p>Entity 클래스는 API 계층에서 전달받은 요청 데이터를 기반으로 서비스 계층에서 비지니스 로직 처리를 위한 데이터를 전달받고, 비지니스 로직 처리를 마친  결과 값을 API 계층으로 다시 리턴하는 역할을 한다.</p>
<p>이때 계층별로 그리고 기능적으로 역할을 완전히 분리 시키기 위해 Mapper를 구현하여 DTO to Entity, Entity to ResponseDto 의 역할을 전담할 수 있도록 한다.</p>
<ul>
<li>Spring에서는 MapStruct를 이용한 Mapper 자동 생성 기능을 지원한다. </li>
</ul>
<hr>
<p>맞닥뜨린 주의사항들</p>
<hr>
<p>그래들을 통해 스프링을 빌드할때 프로그램이 롬복의 기능을 사용하고 있다면 build.gradle 파일에서 Dependencies를 세팅할때 롬북 관련 정보들이 먼저 위치하여야 한다.</p>
<p><img src="https://velog.velcdn.com/images/johnny_debt/post/d6e49cb6-ee42-418b-b424-cf3f2d9bf85d/image.png" alt=""></p>
<p>이는 롬복 애너테이션들로 구현한 기능들이 활성화 되기 위해선 롬복 그 자체가 먼저 활성화가 되어야 하기 때문으로 이해할 수 있다.</p>
<hr>
<p>더 알아보아야 할 내용들</p>
<hr>
<p>매퍼사용시 VO에 대해서</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC/API 계층]]></title>
            <link>https://velog.io/@johnny_debt/Spring-MVCAPI-%EA%B3%84%EC%B8%B5</link>
            <guid>https://velog.io/@johnny_debt/Spring-MVCAPI-%EA%B3%84%EC%B8%B5</guid>
            <pubDate>Mon, 12 Jun 2023 15:48:10 GMT</pubDate>
            <description><![CDATA[<p>Spring MVC를 이용하여 API 계층의 Controller 클래스를 구현하는 연습을 하고있다.</p>
<p>Client의 요청을 처리하는 Controller 클래스의 Handler매서드들을 Spring MVC의 어노테이션을 이용하여 만들고 요청받은 혹은 전달받은 데이터들을 Mapping할때 편의와 유지보수를 위해, 또 Validation을 쉽게 추가하기위해 데이터를 객체로 받는 방식인 Dto(Data Transfer Object)를 적용하면서 스프링이 지원하는 엄청난 편의성들에 감탄하고 하루하루 재밌게 즐기고있다. </p>
<p>오늘 Dto클래스들을 구현하면서 저지를 사소한 실수들과 깨달은점, 그리고 남은 의문점 몇가지들을 기술하려한다.</p>
<ol>
<li>Validation</li>
</ol>
<ul>
<li><p>실수: @PathVariable을 통해 받은 파라미터에 대한 Validation조건의 적용 위치</p>
<ul>
<li>@Positive로 path로 받은 Id값을 양수로 한정하는것이 목표였다.
 처음에 다른 조건들과 마찮가지로 RequestBody부분에 @Positive를 적용하였으니 아무리 양수의 아이디값을 넣어도 오류메세지를 리스폰스 하였다!</li>
<li>이를 해결하기 위해 많은 시도를 했는데, 그첫번째가 Id의 타입인 long을 Wrapper Class, Long 타입으로 바꿔보는 것 이었다. 하지만 실패...</li>
<li>다양한 방법을 시도하다가 문득 @Postitive를 검사하는 위치에 대한 의문이 생겼고 그 위치를 @PathVariable 어노테이션을 받는 곳으로 이동시켰더니 성공!</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/johnny_debt/post/09350455-5c41-40af-ade7-447226f1a7c3/image.png" alt=""></p>
<p>의문점.. : 어차피 같은 long타입이 setCoffeeId()를 통해 초기화 될텐데 그때 검사가 되어도 상관이 없을거라 생각했는데 그렇지 않았다. 이유가 궁굼하다. 더 파헤쳐 봐야 할 듯 하다.</p>
<p>또한 Valid 조건들을 적용하면서 잘 적용한듯 하였는데 몇가지 잘 풀리지 않았던 부분들이 있었다.</p>
<ul>
<li><p>두가지의 상반되는 Validation을 같이 적용할때</p>
<p> <img src="https://velog.velcdn.com/images/johnny_debt/post/81594164-eea6-49ca-8907-9a8a9369d3e4/image.png" alt=""></p>
<ul>
<li><p>@Nullable은 null 값을 허용하고 @Min은 100이상만을 허용하는데 이때 해당 필드의 타입을 int, premitive 타입으로 지정했을때 두가지가 동시에 적용되지 않았다. null 값을 100이상이 아니라고 인식하기 때문이였다.</p>
</li>
<li><p>--&gt; @Nullable은 RequestBody의 Validation으로 쓰이지 않고 파라미터의 Null값 허용 여부를 명시한다.(Jun.13th.2023)</p>
</li>
<li><p>다양한 시도 끝에 필드의 타입을 primitive가 아닌 wrapper 클래스인 Integer로 바꾸면 null값에 대한 적용범위가 다양해져서 가능하지 않을까 생각이 들었고 이는 성공하였다.</p>
<p>의문점: 정확한 이유를 아직 모르겠다. wrapper 클래스에 대한 이해가 더 필요한것 같다. </p>
</li>
<li><p>--&gt; Wrapper 클래스타입으로 바꾸면 레퍼런스값을 갖는 객체가 됨으로 null값을 담을 수 있다. 하지만 int는 불가!(Jun.13th.2023)</p>
</li>
</ul>
<hr>
<p> 공부하면서 느낀 추가적인 의문점</p>
<ol>
<li>나는 ControllerDto클래스를 만들고 이너클래스로 개별적인 Dto 클래스들을 만들고 사용하면 Dto 클래스들을 관리하기 쉽고 더 편하지 않을까 생각에 이를 적용했는데 이너클래스들을 static클래스로 만들지 않으니 오류가 발생했다. 그리고 반드시 static으로 만들어야 한다는 메세지를 출력했는데 이에 대한 의문이 아직 남아있다.</li>
</ol>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVS: Controller]]></title>
            <link>https://velog.io/@johnny_debt/Spring-MVS-Controller</link>
            <guid>https://velog.io/@johnny_debt/Spring-MVS-Controller</guid>
            <pubDate>Fri, 09 Jun 2023 04:46:51 GMT</pubDate>
            <description><![CDATA[<p>Index</p>
<hr>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSBE Section 2 회고]]></title>
            <link>https://velog.io/@johnny_debt/CSBE-Section-2-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@johnny_debt/CSBE-Section-2-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 08 Jun 2023 12:53:02 GMT</pubDate>
            <description><![CDATA[<hr>
<p>본격적인 듯 보이지만 실은 이제 겨우 코딩의 &#39;ㅋ&#39;자를 시작한듯하다. 알고리즘과 다양한 자료구조, 네트워크와 데이터베이스, 그리고 스프링 프레임워크의 기본을 맛본 섹션이였다.</p>
<p>나는 다행히 코딩을 좋아하는듯 하다. 재밌었다. 복잡한 알고리즘이 풀릴때의 강렬한 쾌감과 풀리지 않을듯 막막한 벽을 만난듯한 막막함이 공존하는 static함을 가장한 어디로 튈지모르는 코드들을 길들이느라 굉장히 dynamic한 시간들이 이어졌다. 프로그래밍이란 의외로 도파민 뿜뿜한 액티비티였다!</p>
<p>계층별로 체계적으로 쌓여 서로 유기적으로 일하는 네트워크와 통신체계를 공부하면서 인류가 불과 100년도 걸리지않아 쌓아올린 역사적 유물을 조우한 탐험가처럼, 선조가 만들어놓은 거대한 구조물 앞에 서서 그 입구를 찾아 벽을 더듬거리는 도굴꾼처럼 새로운 정보들을 정신없이 탐닉하였고 나도 이 구조물에 자그마한 블록이라도 쌓아 올릴 수 있을까 기대감에 부풀었다. </p>
<p>도전은 즐겁다. 새로운 지식을 배우는 것 만으로 행복감이 차오른다. 즐기자 어떠한 
벽을 마주하더라도!</p>
<hr>
<p>One of the most impressive things I learned through this section is the greatest fact that &quot;I LOVE PROGRAMMING!&quot; Only with this reason I got so much thankful that I am actually enjoying this path walking to be a programmer. Our world is also &quot;programmed&quot; with certain logics in system, it is not that different how to build codes to how real world&#39;s system and business is built; we programmers just borrow these great logics and apply them to our program. It is because I love Java, OOP. </p>
<hr>
<p>Goals</p>
<ol>
<li>다양한 자료구조를 이해하고 다양한 문제들을 해결하기 위해 적용</li>
<li>데이터베이스와 API의 통신 방법과 구조, 그 규약들을 이해하고 연습</li>
<li>블로깅하는 습관 들이기</li>
</ol>
<hr>
<p>review</p>
<p>1,2. 다행히 나름 내 방식으로 잘 이해한듯 하다. 과정 자체를 즐기고 있다. 다만, 양이 워낙 방대하여 필수적인 내용만 간신히 소화하고 있는데 갈길이 멀다는걸 느낀다.
3. 블로깅을 자주 하지 못해 아직 쓰고 정리하고 싶은 내용이 산더미이다. 조금더 재밌는 글을 써보자 취미삼아</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Framework (Spring Container/DI)]]></title>
            <link>https://velog.io/@johnny_debt/Spring-Framework-Spring-ContainerDI</link>
            <guid>https://velog.io/@johnny_debt/Spring-Framework-Spring-ContainerDI</guid>
            <pubDate>Thu, 08 Jun 2023 08:53:48 GMT</pubDate>
            <description><![CDATA[<p>Index</p>
<hr>
<h4 id="1-spring-container--bean">1. Spring Container &amp; Bean</h4>
<h4 id="2-practice">2. Practice</h4>
<hr>
<h3 id="1-spring-container--bean-1">1. Spring Container &amp; Bean</h3>
<ol>
<li><p>AppConfigurer 클래스 생성및 Bean 등록</p>
<pre><code>@Configuration
public class AppConfigurer {

 @Bean
 public Menu menu() {
     return new Menu(productRepository());
 }

 @Bean
 public ProductRepository productRepository() {
     return new ProductRepository();
 }
</code></pre></li>
</ol>
<pre><code>2. 스프링 컨테이너(ApplicationContext 인터페이스)생성 
3. main에서 Bean 조회 및 의존성 주입
</code></pre><p>// (1) 스프링 컨테이너 생성
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);</p>
<pre><code>        // (2) 빈 조회 
    ProductRepository productRepository = applicationContext.getBean(&quot;productRepository&quot;, ProductRepository.class);
      Menu menu = applicationContext.getBean(&quot;menu&quot;, Menu.class);
    Cart cart = applicationContext.getBean(&quot;cart&quot;, Cart.class);
    Order order = applicationContext.getBean(&quot;order&quot;, Order.class);

        // (3) 의존성 주입
    OrderApp orderApp = new OrderApp(
            productRepository,
            menu,
            cart,
            order
                );

        // (4) 프로그램 실행 
  orderApp.start();</code></pre><pre><code>
2. 스프링에서 지원하는 컴포넌트 스캔과 의존성 자동 주입

- @Configuration와 @ComponentScan어노테이션 사용
</code></pre><p>import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;</p>
<p>@Configuration // Configurer클래스에 붙혀서 해당 기능 사용
@ComponentScan // 스캔의 시작지점 따라서 Configurer 클래스를 프로젝트 최상단에 두는것이 일반적 / @ComponentScan(basePackages = “”) &quot;&quot;를 변경해 범위를 따로 지정할 수 있다.
public class TestConfigurer {
}</p>
<pre><code>- @Component 어노테이션 사용

: 스프링 컨테이너가 해당 클래스의 인스턴스를 생성하여 스프링 빈으로 관리

- @Autowired 어노테이션을 사용하여 의존성 주입
</code></pre><p>@Component
public class Order {
    private Cart cart;
    private Discount discount;</p>
<pre><code>@Autowired
public Order(Cart cart, Discount discount) {
    this.cart = cart;
    this.discount = discount;
}</code></pre><p>   ...
}</p>
<pre><code>:@Autowired 애너테이션을 통해 스프링이 관리하고 있는 해당 타입의 객체가 자동으로 주입되어 의존 관계가 완성
### 2. 스프링 컨테이너의 싱글톤 패턴

#### 1.싱글톤 패턴 적용/비적용 차이

1. singleton 패턴:</code></pre><p>public class AppConfigurer {</p>
<pre><code>private Cart cart = new Cart(productRepository(), menu());

public Cart cart() {
    return cart;
}

--- 생략 ---</code></pre><p>}</p>
<pre><code>
- Configurer클래스에서 카트 클래스를 instantiate한 뒤 cart()메서드를 통해 return: 결과적으로 하나의 객체를 모두가 공유하게 됨

2. Non singleton 패턴:
</code></pre><p>public class AppConfigurer {
public ProductRepository productRepository() {
        return new ProductRepository();
    }</p>
<pre><code>public Menu menu() {
    return new Menu(productRepository().getAllProducts());
}
...
}</code></pre><pre><code>  - Configurer 클래스를 통해 의존성을 주입받고 해당 객체를 사용할때 마다 새로운 객체를 생성해서 return

  -싱글톤 패턴을 이용하면 객체를 매번 새롭게 생성하지 않고 하나의 객체를 공유하여 메모리 낭비를 줄일 수 있다. 하지만 이러한 장점에도 반드시 필요한 상황에만 사용해야 하는데 이는 객체간의 의존도(결합도)가 높아지기 때문이다.


#### 2. Spring Framework를 이용한 싱글톤 패턴 구현 

- 스프링 프레임워크는 자동으로 DI를 할때 기본적으로 싱글톤 패턴으로 구현된다. 스프링 컨테이너는 내부적으로 객체들을 싱글톤으로 관리하고 싱글톤 레리스트리 기능을 수행한다.(CGLIB 라이브러리 사용)
- 이를 통해 싱글톤 패턴이 가지는 메모리의 낭비가 적음을 이용하면서 동시에 객체간의 의존도(결합도)를 느슨하게 유지할 수 있다.

- 스프링의 Bean객체들의 관리 범위는 @Scope(Prototype/Session/ Request) 등의 @Scope 어노테이션을 추가해 변경할 수 있다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Framework]]></title>
            <link>https://velog.io/@johnny_debt/Spring-Framework</link>
            <guid>https://velog.io/@johnny_debt/Spring-Framework</guid>
            <pubDate>Thu, 08 Jun 2023 07:03:44 GMT</pubDate>
            <description><![CDATA[<p>Index</p>
<hr>
<h4 id="1-pojo">1. POJO</h4>
<h4 id="2-ioc--di">2. IoC &amp; DI</h4>
<h4 id="3-aop--psa">3. AOP &amp; PSA</h4>
<hr>
<h3 id="1-pojo-1">1. POJO</h3>
<h4 id="what-is-pojo">What is POJO?</h4>
<ul>
<li>POJO(Plain Old Java Object) refers to a programming using only pure Java Objects without any outer libraries. This also means it should be a regular Java object with no special restrictions other than those forced by the Java Language Specification and does not require any classpath.</li>
</ul>
<ul>
<li>Spring Framework basically requires the POJO Programming. </li>
</ul>
<h3 id="2-ioc--di-1">2. IoC &amp; DI</h3>
<ul>
<li>Spring framework applies IoC(Inversion of Control) through DI.</li>
<li>DI, 즉 의존성 주입이란 기본적인 의미로는 클라이언트에게 클라이언트가 필요한 서비스를 제공하는 것으로 Java의 관점에서 설명하면 어떤 객체를 만들때 해당 클래스가 필요로 하는 메소드와 필드들을 가진  외부 객체를 new연산자와 생성자를 이용하여 주입하여 해당 객체를 사용할 수 있게 만들어 주는 기술이다. 이때 해당하는 클래스는 주입한 객체의 클래스에 대해 높은 의존성을 가지게 되는데 유지보수를 원활하게 하고 수정을 용이하게 하기위해 느슨한 의존도를 지향하는 OOP의 특성상 공통된 기능들을 추상화한 인터페이스와 클래스들의 의존관계를 외부에서 결정하고 주입하는 Configuration클래스를 따로 만들어 의존관계를 주입하고 관리하는 기술을 포함한다. 이를 통해 클래스간의 의존도가 낮아져 코드를 수정 할 때 복잡도가 낮아지고 수정이 쉬워진다. 또한 재사용성이 높은 코드를 짤 수있고 부분적으로 테스트하기 쉬운 코드를 만들 수 있어 오류를 수정함에 있어서도 장점을 가진다. 마지막으로 협업시에OOP가 추구하는 방향대로  코드의 가독성이 좋아지게 된다.</li>
</ul>
<ul>
<li>스프링 프레임워크를 사용하면 스프링 컨테이너가 Annotation을 읽어 자동으로 Scan하고 
DI를 해줘서 개발자가 보다 핵심 비지니스 로직을 개발하는데 집중할 수 있게 도와준다.</li>
</ul>
<h3 id="4-aop--psa">4. AOP &amp; PSA</h3>
<p>  먼저, AOP(Aspect Oriented Programming)가 무엇인지 간단히 설명하면 공통 관심 사항과 핵심 관심 사항을 분리시켜 코드의 중복을 제거하고 재사용성을 높이는 프로그래밍 방법론으로 이는 핵심 기능에 공통기능을 삽입하는 방식으로 구현이 가능하다.</p>
<p>스프링에서는 프록시 객체를 자동으로 생성하여 AOP를 구현하는  방식을 지원하고 있는데 객체 지향 프로그래밍을 지향하고 따르는 스프링은 핵심 관심사 코드들에 섞여있는 부가적인 기능을 하는 공통관심사를 분리시켜 객체 지향 프로그래밍을 최적화 할 수 있고 OOP만으로는 해결되지 않았던 코드 복잡도를 개선하여 비지니스 로직을 빠르게 파악할 수 있게 해준다.</p>
<ul>
<li>PSA, Portable Service Abstraction is mechanism or strcucture that keeps the consistent approaching ways(techniques) and points to use the various services(Objects) using Abstraction(Up&amp;down Casting). </li>
<li>Through PSA, it is possible that a client consistently(without changing) focuses on an abstracted Super class while uses various sub classes&#39; functiond(methods).</li>
<li>The ultimate purpose of this is to make it easy and flexable to change or fix the codes with the least changes and efforts.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Network] Web Application]]></title>
            <link>https://velog.io/@johnny_debt/Network-Web-Application</link>
            <guid>https://velog.io/@johnny_debt/Network-Web-Application</guid>
            <pubDate>Wed, 24 May 2023 14:28:59 GMT</pubDate>
            <description><![CDATA[<p>Index</p>
<hr>
<h4 id="1-what-is-a-web-appplication">1. What is a web appplication?</h4>
<h4 id="2-network">2. Network</h4>
<h4 id="3-http">3. HTTP</h4>
<hr>
<h3 id="1-what-is-a-web-applcation">1. What is a Web Applcation?</h3>
<h4 id="1-web-application-vs-native-applicication">1. Web Application vs Native Applicication</h4>
<ul>
<li><p>Web application is an application that is reachable via WEB browser, but native application is an application which only works upon the device that is installed the application.</p>
</li>
<li><p>WEB Application is distinguished from Web site, in the way it works; it dynamically interacts with its users.</p>
</li>
<li><p>WEB Application only works on the internet but it makes it possible to use it without downloading and installation. It is also easy to maintain it.</p>
</li>
</ul>
<h3 id="2-tcpip">2. TCP/IP</h3>
<h4 id="1-protocol">1. Protocol</h4>
<ol>
<li>IP Address and MAC Address</li>
</ol>
<ul>
<li><p>TCP/IP((TCP(Transmission Control Protocol)/IP(Internet Protocol)) protocol is one of the most representative protocols to communicate with other computers; it refers to the common language how the computers communicate and trade their data on the internet.</p>
</li>
<li><p>IP address:
IP is the address that is used to identify itself; througth IP address, we can connect to a WEB Application with our devices such as computer. Every computer connecting to the internet gets its own IP address.</p>
</li>
<li><p>IP address consists of 4 chunks which are separated with dot(.); it is called IPv4.</p>
</li>
<li><p>MAC address: MAC address is a serial number assigned to devices by their producers(makers). Combinating with IP address and MAC address, we can use network communication(the Internet).</p>
</li>
<li><p>IP Address is divided by network part and host part; network part contains the information about what network it is connected and host part navigates what computer it is.</p>
</li>
</ul>
<ol start="2">
<li><p>TCP and UDP Protocol</p>
<ol>
<li>TCP: 3-way handshake:</li>
</ol>
<p> <strong>Step1:</strong> To connect TCP Sender to Receiver, Synchronize         Sequence Number(SYN) is sent to Receiver with a segment.
  <strong>Step2:</strong> Receiver responses to the Sender upon the request.       It sends back SYN/ACK(Acknowledgement) set to the         receiver, which is the response that the segment is         acceptable and valid. 
 <strong>Step3:</strong> The Sender sends back the ACK that it got received to the receiver to approve that the trusted and safe communication can be started between the sender and receiver.</p>
<ol start="2">
<li>UDP: UDP starts communication without 3-hand shake process for rapid communcation and response.</li>
</ol>
</li>
<li><p>Port </p>
</li>
</ol>
<ul>
<li><p>Port number the number for identifying specific application that shares same IP; which means it is assigned to uniquely identify a connection endpoint and to direct data to a specific service.</p>
</li>
<li><p>0<del>65,535 port numbers can be used, but 0</del>1023 are already assigned for major protocols(Well-known port: 80: HTTP, 443: HTTPS, etc.)</p>
</li>
</ul>
<ol start="4">
<li><p>URL and DNS</p>
<ol>
<li><p>URL(Uniform Resource Locator)</p>
<p> URL is a Web address that specifies its location on a         computer network and a mechanism for retrieving it.</p>
<pre><code> - it consists of 3parts: scheme, hosts, and url-path.
 - URI additionally includes query and bookmark.</code></pre></li>
<li><p>Domain name</p>
<p> Domain name is used instead of IP address for easy-memorization.</p>
<ul>
<li>It consists of scheme/subdomain/domain/directory/file.</li>
</ul>
</li>
</ol>
</li>
</ol>
<pre><code>3. DNS(Domain Name System)

    - To match a domain name to an IP address, we need DNS.

    - Here&#39;s how it works:
    1. Request Initiation: When you type a domain name (e.g., example.com) into your web browser or application, a DNS lookup is triggered to resolve the domain name into an IP address. This lookup is initiated by your device&#39;s operating system.

    2. Local DNS Cache Check: Your device first checks its local DNS cache to see if it has recently resolved the requested domain name. If the IP address is found in the cache and is still valid (not expired), the lookup process ends, and the IP address is used to establish the connection.

    3. Recursive DNS Resolver: If the IP address is not found in the local cache or has expired, the request is sent to a recursive DNS resolver. This resolver is typically provided by your Internet Service Provider (ISP) or a third-party DNS resolver.

    4. Root DNS Servers: The recursive resolver doesn&#39;t have the requested IP address information, so it starts the resolution process by contacting a Root DNS Server. The Root DNS Servers maintain the addresses of all top-level domain (TLD) name servers, such as .com, .org, .net.

    5. TLD Name Servers: The Root DNS Server responds to the recursive resolver with the IP address of the appropriate TLD name server responsible for the requested domain&#39;s extension (e.g., .com).

    6. Authoritative DNS Servers: The recursive resolver then contacts the TLD name server and requests the IP address of the authoritative DNS server for the specific domain being queried (e.g., example.com).

    7. Authoritative DNS Server: The recursive resolver contacts the authoritative DNS server and requests the IP address for the requested domain name.

    8. DNS Response: The authoritative DNS server sends back the IP address of the requested domain to the recursive resolver.

    9. Caching and Response: The recursive resolver caches the IP address obtained from the authoritative DNS server for future use and returns the IP address to your device.

    10. Connection Establishment: Your device receives the IP address from the recursive resolver and uses it to establish a connection with the web server hosting the requested domain. The requested webpage or application data is then retrieved and displayed on your device.</code></pre><h3 id="3-http-messages">3. HTTP Messages</h3>
<ul>
<li>How it looks:
```</li>
</ul>
<ol>
<li>Request<ol>
<li>Headers</li>
<li>Body</li>
</ol>
</li>
<li>Response<ol>
<li>Status line</li>
<li>Headers</li>
<li>Body<pre><code>
</code></pre></li>
</ol>
</li>
</ol>
<ol>
<li><p>Start-line: A start-line tells the requests to be implemented(request), or its status of whether successful or a failure(Response). This start-line is always a single line.</p>
</li>
<li><p>HTTP Headers: An optional set of HTTP headers specifying the request, or describing the body part.</p>
</li>
<li><p>A blank line indicating all meta-information for the request has been sent and separation from the body part.</p>
</li>
<li><p>An optional body containing data associated with the request (like content of an HTML form), or the document associated with a response. The presence of the body and its size is specified by the start-line and HTTP headers.</p>
</li>
</ol>
<ul>
<li>HTTP Request Methods:</li>
</ul>
<p><strong>GET</strong>
The GET method requests a representation of the specified resource. Requests using GET should only retrieve data.</p>
<p><strong>HEAD</strong>
The HEAD method asks for a response identical to a GET request, but without the response body.</p>
<p><strong>POST</strong>
The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.</p>
<p><strong>PUT</strong>
The PUT method replaces all current representations of the target resource with the request payload.</p>
<p><strong>DELETE</strong>
The DELETE method deletes the specified resource.</p>
<p><strong>CONNECT</strong>
The CONNECT method establishes a tunnel to the server identified by the target resource.</p>
<p><strong>OPTIONS</strong>
The OPTIONS method describes the communication options for the target resource.</p>
<p><strong>TRACE</strong>
The TRACE method performs a message loop-back test along the path to the target resource.</p>
<p><strong>PATCH</strong>
The PATCH method applies partial modifications to a resource.
(MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods</a>)</p>
<ul>
<li><p>We can connect each directory&#39;s name with (/) and add parameters with (?).</p>
<p>Ex) </p>
<p>  GET/api.openweathermap.org/data/2.5/weather?id={cityid}&amp;appid={your api key}</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSBE Section 1 회고]]></title>
            <link>https://velog.io/@johnny_debt/CSBE-Section-1-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@johnny_debt/CSBE-Section-1-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 09 May 2023 07:17:09 GMT</pubDate>
            <description><![CDATA[<p>목표</p>
<hr>
<ol>
<li>자바 기본문법을 익히고 체화하기</li>
<li>OPP에 대해 확실하게 이해하고 자바 프로그램의 구조를 이해하기</li>
</ol>
<hr>
<p>Keep</p>
<hr>
<ol>
<li>효과적인 예습과 복습</li>
<li>개념학습에 너무 정신을 쏟기보다는 체화시키면서 자연스럽게 익숙해지기</li>
<li>나만의 언어로 내가 이해한바를 설명하기</li>
<li>미리미리 미루지 않고 공부하기</li>
<li>적극적으로 모르거나 궁금한 사항들을 묻고 검색하기</li>
</ol>
<hr>
<p>Problem</p>
<hr>
<ol>
<li>수면시간이 부족해 너무 피곤했다</li>
<li>책을 읽는등 깊이 있는 공부를 소홀히 한다.</li>
<li>반복학습의 부족</li>
</ol>
<hr>
<p>try</p>
<hr>
<ol>
<li>모든 디바이스를 놓고 최소한 12시에는 잠에든다.</li>
<li>책을 읽는 습관을 계획표를 짜서 만든다.</li>
<li>공부한 내용들이 개념일지라도 이를 활용해 보려 반복적으로 코딩하며 연습한다.</li>
<li>꾸준히 블로깅하기</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Code States BE 부트캠프 #12]]></title>
            <link>https://velog.io/@johnny_debt/Code-States-BE-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-12</link>
            <guid>https://velog.io/@johnny_debt/Code-States-BE-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-12</guid>
            <pubDate>Tue, 02 May 2023 07:12:01 GMT</pubDate>
            <description><![CDATA[<p>Index</p>
<hr>
<h4 id="1-enum">1. Enum</h4>
<h4 id="2-generic">2. Generic</h4>
<h4 id="3-exception-handling">3. Exception Handling</h4>
<h4 id="4-collection-framework">4. Collection Framework</h4>
<hr>
<h3 id="1-enum-1">1. Enum</h3>
<p>Enum is a set of final values which are not changing; it makes it way easy to handle them without some errors occuring by overlaped names. It also made to use with Switch method; it is beacuse Switch method only takes &quot;char, byte, short, int, Character, Byte, Short, Integer, String, enum,&quot; as its parameter.</p>
<p>How to make it:</p>
<pre><code>enum EnumName { Element1, Element2, Element3, Element4 }</code></pre><h3 id="2-generic-1">2. Generic</h3>
<p>Generic is the way used for Class and method, which generalize the its type, so that it can decide its type when it is instantiated. In other words, it makes it possible to make an undecided Class or Method at the moment it is declared; it can be varied when it is newly instantiated.</p>
<p>There is basic grammar how to make Generic Class:</p>
<pre><code>class ClassName&lt;T&gt; {
    private T item;

    public ClassName(T item) { // constructor
        this.item = item;
    }

    public T getItem() { //getter
        return item;
    }

    public void setItem(T item) { //setter
        this.item = item;
    }
}</code></pre><pre><code>Basket&lt;String&gt;  basket1 = new Basket&lt;String&gt;(&quot;String&quot;);
Basket&lt;Integer&gt; basket2 = new Basket&lt;Integer&gt;(int);
Basket&lt;Double&gt;  basket3 = new Basket&lt;Double&gt;(double);</code></pre><pre><code>class Basket {
        ...
        public &lt;T&gt; void add(T element) {
                ...
        }
}</code></pre><pre><code>basket.&lt;Integer&gt;add(int); // or            
basket.add(int); </code></pre><ul>
<li>We can also make restriced Generic class in what specific types it would take, with the keyword &quot;extends.&quot;</li>
</ul>
<pre><code>interface A { ... }
class B implements A { ... } // B implements the interface A
class C extends B implements A { ... } 

// C inherits B which implements A

class Basket&lt;T extends B(Super class) &amp; A(interface)&gt; {
// Basket class can only take the types which is extends B and implements A(inheritance(Super) must declared first than interface)
    private T item;

        ...
}

class Main {
    public static void main(String[] args) {

        // Instantiation
        Basket&lt;B&gt; flowerBasket = new Basket&lt;&gt;();
        Basket&lt;C&gt; roseBasket = new Basket&lt;&gt;();
    }
}</code></pre><ul>
<li>Using Wildcard(?)<pre><code>&lt;? extends T&gt; // T and SubClasses which inherits T only
&lt;? super T&gt; // T and its SuperClass only</code></pre></li>
</ul>
<h3 id="3-exception-handling-1">3. Exception Handling</h3>
<ul>
<li><p>Complie Error(Syntax Error):
It refers to the errors occurring in compiling; it mostly relates to grammaric mistakes(Syntax).</p>
</li>
<li><p>Runtime Error:
It refers to the errors occurring in runtime(When the program starts)</p>
</li>
</ul>
<p>Exception can be handled by using Exception with try/catch block.</p>
<pre><code>try {
    // codes 
} 
catch (ExceptionType1 e1) {
    //operation codes when the codes meet the exception of ExceptionType1 type.
} 
catch (ExceptionType2 e2) {
    // operation codes when the codes meet the exception of ExceptionType2 type.
} 
finally {
    // optional
    // codes that must be operated(Lastly).
}</code></pre><h3 id="4-collection-framework-1">4. Collection Framework</h3>
<h4 id="1-liste">1. List<E></h4>
<ul>
<li>ordered data</li>
<li>duplicated data: ok</li>
<li>implements: ArrayList, Vector, Stack, LinkedList</li>
</ul>
<h4 id="2-sete">2. Set<E></h4>
<ul>
<li>Not ordered data</li>
<li>duplicated data: No</li>
<li>implements: HashSet, TreeSet</li>
</ul>
<h4 id="3-mapkv">3. Map&lt;K,V&gt;</h4>
<ul>
<li>saves a pair of key and value</li>
<li>Not ordered data</li>
<li>key cannot be duplicated/ value can be duplicated</li>
<li>implements: HashMap, HashTable, TreeMap, Properties</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Code States BE 부트캠프 #10]]></title>
            <link>https://velog.io/@johnny_debt/Code-States-BE-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-10</link>
            <guid>https://velog.io/@johnny_debt/Code-States-BE-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-10</guid>
            <pubDate>Mon, 24 Apr 2023 08:01:47 GMT</pubDate>
            <description><![CDATA[<h3 id="index">INDEX</h3>
<hr>
<h4 id="1-constructor">1. Constructor</h4>
<h4 id="2-inner-class">2. Inner Class</h4>
<hr>
<h3 id="1-constructor-1">1. Constructor</h3>
<p>A Constrcutor is the method that initialize the instance variables when the class is instantiated. Whent a class is instantiated with &quot;new&quot; keyword, a constructor initialize instance variables; there are several essential rules for constructor.</p>
<ul>
<li><ol>
<li>The name of constructor must be same to the className.</li>
</ol>
</li>
<li><ol start="2">
<li><p>A constructor has NO RETURN TYPE and NO VOID keyword also. </p>
<p>ClassName(parameter){...}</p>
</li>
</ol>
</li>
<li><ol start="3">
<li>A constructor also cane be overloaded with different parameters.</li>
</ol>
</li>
<li><ol start="4">
<li>If the developer doesn&#39;t make a Default Constructor(No parameters), Java compiler automatically makes one default constrctor when the instance of the class is made.</li>
</ol>
</li>
<li><ol start="5">
<li>Constructor Overloading makes it possible to use same Class in various ways with various variables without making new variable everytime making new instances.</li>
</ol>
</li>
<li><p>this vs this()</p>
</li>
</ul>
<p>this(): In a constructor, overloaded constructors can be called out with the method, this(). </p>
<pre><code>class Ex {
    public Ex() {
    System.out.println(&quot;the first constructor!&quot;)
    }

    public Ex(int x) {
    this(); 
    // this() methods that copies the default constructor in the overloaded second constructor.
    System.out.println(&quot;the second constructor!&quot;)
        }
}
</code></pre><ul>
<li>this() method must be used only in a constructor</li>
<li>this() method must be located the first line in a constructor.</li>
</ul>
<p>this keyoword:</p>
<p>this keywords make it possible to use both the variables and parameters, which have same names, together with distingushing them in the constructors and methods: in the constructors and methods this. refers to the parameter in themselves. </p>
<ul>
<li>This keyword refers to the instance(Object) itself: &lt;this.parameterName = ...;&gt;</li>
<li>It is generally used to distinguish instance field and local variables(or parameters), which have same name.</li>
</ul>
<h3 id="2-inner-class-1">2. Inner Class</h3>
<p>Inner Class is the class that is made inside of a class, which is only used when the outer calss and inner class are related, to reduce the complexity of codes.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Code States BE 부트캠프 #9]]></title>
            <link>https://velog.io/@johnny_debt/Code-States-BE-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-9</link>
            <guid>https://velog.io/@johnny_debt/Code-States-BE-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-9</guid>
            <pubDate>Fri, 21 Apr 2023 02:02:44 GMT</pubDate>
            <description><![CDATA[<h3 id="index">INDEX</h3>
<hr>
<h4 id="1-class--object-oop">1. Class &amp; Object (OOP)</h4>
<h4 id="2-field--method">2. Field &amp; Method</h4>
<hr>
<h3 id="1-class--object-oop-1">1. Class &amp; Object (OOP)</h3>
<p>Object is literally actual thing that we can use for building up the program, and Class is the frame or blueprint that defines the object, which means Class is used to produce the objects, and the objects is created as the class defines and designs.</p>
<p>Most importantly, Class is not an actual thing but the frame to produce the objects.</p>
<p>In easy words, Class is the blueprint, and the certain object(Instance in the other word)—which is made following the class—is the house. In other words, Class is the frame and, the object that is made through the frame is called Instance through the process called &quot;Instantiate.&quot;</p>
<p>The most powerful advantage of this programming system(OOP) is that you can produce thousands of instances through building up the well organized blueprint: Class.</p>
<ol>
<li>Basic Grammar:</li>
</ol>
<pre><code>    Class Name(Capitalized) {
    //contents:
    int i = 0; // field
    void doSomthing() {...} // method
    class InnerClass {...}  // inner class
    }
</code></pre><p>Class is organized with 4 elements: Field, Method, Constructor, and Inner Class.</p>
<p>(1) field: defines &quot;States&quot; through Variables, which are used in the class.
(2) method: dfines the &quot;Functions&quot; 
(3) constructor: makes the &quot;Instances&quot; of certain class.
(4) inner class: the &quot;Classes&quot; inside the class.</p>
<ul>
<li>We call the 3 elements except the constructor as the &quot;Members.&quot;</li>
<li>Field and Method represents the class&#39; state and function, which are the group of data related to the class.</li>
</ul>
<p>As we talked about previously, Instance(Object) is the actual production which is produced by Class.</p>
<p>Instance is also organized with states and functions; generally one instance is made up with various but similar states(Variables) and functions(Methods), and we call those states and functions—including inner classes—as the members of the Instance. </p>
<ul>
<li>How to make an Instance:</li>
</ul>
<p>&quot;new&quot; keyword and &quot;point operator(.)&quot;:</p>
<p>We can make an Instance of the certain class using new keword, and we can approach to use the Instance&#39; members(Variables &amp; Methods) with the point operater(.).</p>
<pre><code>class Modulers {

    public staitc void main(String[] args){

    Module module1 = new Module(); 
    Module module2 = new Module();
    Module module3 = new Module();

    }

}</code></pre><ul>
<li>Modules1 to 3 are the instances—named as module1, module2, and module3(Those are the reference variable names that navigates the address where the data would be saved)—which are instantiated from the same class Module. </li>
<li>Module() is called the Constructor.</li>
<li>Every instances derived from the same class share the same methods and field so that we can simply approach to them—which is saved in the class—through (.) operator without restating the them.<ul>
<li>There is how we use the field and methods after making the instance: </li>
</ul>
</li>
</ul>
<blockquote>
<p>InstanceName.fieldName(VariableName) // calling the field
InstanceName.methodName() // calling the medthod    </p>
</blockquote>
<h3 id="2-field--method-1">2. Field &amp; Method</h3>
<ul>
<li>There are three types in Variables: Class variable, instance variable, and local variable.</li>
<li>Field refers to two types in Variables: Class variable and instance variable.</li>
</ul>
<h4 id="1-class-variable">1. Class variable:</h4>
<p>CV is the variable we can use anytime withour making the instance of the class that the variable is stated. We can approach and use it like this: &quot;ClassName.variableNameInTheClass&quot;</p>
<blockquote>
<ul>
<li>Located in the Class area</li>
</ul>
</blockquote>
<ul>
<li>static keyword</li>
<li>Usable anytime</li>
</ul>
<h4 id="2-instance-variable">2. Instance variable:</h4>
<p>IV is the variables we can use after instantiating the instance of the class that inclues the instance varioable. In other words, it is the variable that is created when the instance of the class is made.
We can approach and use it like this: &quot;instacneName.variableName&quot;</p>
<blockquote>
<ul>
<li>Located in the Class area</li>
</ul>
</blockquote>
<ul>
<li>No static keyword</li>
<li>Usable when the instance is made</li>
</ul>
<h4 id="3-static-keyword">3. static keyword</h4>
<p> Static keyword is used for Class Members: field, method, and inner class. The members with static keyword is called static member, and they are usable without making instance of the class. </p>
<ul>
<li><p>static keyword makes it possible to share the variables or methods between other Objects(instances); it shares the common values or function between the instances where are using them.</p>
</li>
<li><p>static method cannot use instance variables or instance methods; it is because it is not clear that at the moment we use the static method, the instance methods or variables are also already made or not.</p>
</li>
</ul>
<h4 id="4-methods">4. Methods</h4>
<p>Method refers to the group of codes that has a functional role in class: functions.</p>
<p>Method is organize with two parts: head and body</p>
<p>Take a look this code:</p>
<pre><code>public static int substractNums(int x, int y) //&lt;1&gt; { 
    int result = x - y; // &lt;2&gt;
    return result;
}
</code></pre><p>&lt;1&gt;: The first part, head is also called method signature, and it consists of Java modifiers(public &amp; static) - returning type - methodName - parameters ins ().</p>
<p>&lt;2&gt;: The second part, body{}, contains the codes, including local variables, that accomplish the certain functions. </p>
<ul>
<li><p>methodName should be lower-cased to be distinguished from ClassName.</p>
</li>
<li><p>If we do not want to declare the returning type of the method, put &#39;void&#39; instead of type keword, such as &quot;int.&quot;</p>
</li>
<li><p>If the returning type is declared in the method, we must state &#39;return&#39; in the body of the method. </p>
</li>
<li><p>We can also use the method of other classes with first, making the instance of the class, second putting the (.) after the instanceName, and last, stating the methodName to use with ();.</p>
</li>
<li><p>The values we putting inside the () when we call the method, to send them to the method as a parameters, is called arguments. They must be the same to the parameters in its types and order.</p>
</li>
</ul>
<h4 id="5-method-overloading">5. Method Overloading</h4>
<p>Method Overloading is the way stating multiple methods having same methodName, in the same Class.</p>
<p>Method Overloading can be made with stating the same methodName and different parameter types or numbers.</p>
<p>The most significant reason why we do overload the method is that we can accomplish various functions with one method, escaping making new methods with different name but the same/similar function.</p>
<hr>
<p>Next tasks:</p>
<h5 id="1-생성자">1. 생성자</h5>
<h5 id="2-내부-클래스">2. 내부 클래스</h5>
]]></description>
        </item>
    </channel>
</rss>