<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hyoshin.log</title>
        <link>https://velog.io/</link>
        <description>비즈니스 성장을 함께 고민하는 개발자가 되고 싶습니다.</description>
        <lastBuildDate>Wed, 17 Aug 2022 01:36:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hyoshin.log</title>
            <url>https://velog.velcdn.com/images/mindfulness_22/profile/ae958c23-6665-4a65-9883-6ed1b6826201/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hyoshin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/mindfulness_22" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[N:M 다대다 관계 매핑, 어떻게 풀어낼 것인가?]]></title>
            <link>https://velog.io/@mindfulness_22/NM-%EB%8B%A4%EB%8C%80%EB%8B%A4-%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%92%80%EC%96%B4%EB%82%BC-%EA%B2%83%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@mindfulness_22/NM-%EB%8B%A4%EB%8C%80%EB%8B%A4-%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%92%80%EC%96%B4%EB%82%BC-%EA%B2%83%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 17 Aug 2022 01:36:32 GMT</pubDate>
            <description><![CDATA[<p>Member와 StudyRoom의 관계 매핑을 지으려고 생각하다보니, 두 엔티티의 관계는 일대다, 다대일 관계가 아니라 다대다임을 알게 되었다. 물론 @ManyToMany를 써서 두 엔티티를 매핑하는 것이 이론적으로 불가능한 것은 아니다.(오히려 편리해보이기도 한다.) 하지만 여기에는 <strong>고려할 사항이 몇 가지 더 있었다.</strong></p>
<h2 id="💡-manytomany-적용의-실질적-문제들">💡 ManyToMany 적용의 실질적 문제들</h2>
<ul>
<li>객체는 Collection을 사용해서 객체만 2개 있어도 다대다 매핑이 가능하다. 하지만 관계형 데이터 베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.</li>
<li>편리해보이긴 해도, 중간 테이블이 숨겨져 있어 쿼리가 나가는 것을 예측하기 어렵다.</li>
<li>(나중에라도) 필드를 추가적으로 넣을 수 없다. 예를 들어, MemberStudyRoom 엔티티를 만들게 되면 해당 엔티티에 필드값을 추가적으로 넣을 수가 있는데 ManyToMany 관계에서는 그렇게 할 수가 없어서 비즈니스가 커지고 새로운 로직들이 추가될 수록 불편해질 것이다.</li>
</ul>
<p>이러한 문제점들을 이해하고, 나는 Member와 StudyRoom을 잇는 MemberStudyRoom이라는 엔티티를 만들었다. 객체는 아래와 같이 구성했다.</p>
<pre><code>@Getter
@NoArgsConstructor
@Entity
public class MemberStudyRoom {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;STUDYROOM_ID&quot;)
    private StudyRoom studyRoom;

    public MemberStudyRoom(Member member, StudyRoom studyRoom) {
        setMember(member);
        setStudyRoom(studyRoom);
    }

    public void setMember(Member member) {
        this.member = member;
        member.getMemberStudyRooms().add(this);
    }

    public void setStudyRoom(StudyRoom studyRoom) {
        this.studyRoom = studyRoom;
        studyRoom.getMemberStudyRooms().add(this);
    }
}</code></pre><p>그리고 생성자를 통해서 어떤 Member가 어떤 StudyRoom에 Join할 때 서로에게 연관관계가 잘 매핑될 수 있도록 연관 편의 메소드도 추가해주었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자동 로그인 기능 구현 with remember-me]]></title>
            <link>https://velog.io/@mindfulness_22/%EC%9E%90%EB%8F%99-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-with-remember-me</link>
            <guid>https://velog.io/@mindfulness_22/%EC%9E%90%EB%8F%99-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-with-remember-me</guid>
            <pubDate>Tue, 16 Aug 2022 13:33:06 GMT</pubDate>
            <description><![CDATA[<p>자동 로그인 기능을 구현하기 위해 filter 와 interceptor 를 활용해서 자동 로그인 체크를 한 사용자와 그렇지 않은 사용자를 걸러내려고 했습니다. 하지만 이 과정 자체를 일일이 로직으로 쳐내는 것이 복잡했고, 이미 Spring Security 를 사용하고 있었기 때문에 Spring Security 의 기능 중 하나인 remember me를 사용하기로 결정했습니다. </p>
<p>아래는 제가 Remember Me 기능을 구현한 방식을 정리해둔 것입니다.</p>
<p>전체코드</p>
<pre><code>.rememberMe(rememberMe -&gt; rememberMe
                                .key(&quot;rememberMeKey&quot;)
                                .tokenRepository(persistentTokenRepository())
                                .rememberMeServices(rememberMeServices(persistentTokenRepository()))
                                .userDetailsService(new UserDetailsServiceImpl(memberRepository)))

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(dataSource);
    return repo;
}

@Bean
public PersistentTokenBasedRememberMeServices rememberMeServices(PersistentTokenRepository tokenRepository) {
    PersistentTokenBasedRememberMeServices rememberMeServices = new
            PersistentTokenBasedRememberMeServices(&quot;rememberMeKey&quot;, new UserDetailsServiceImpl(memberRepository), tokenRepository);
    rememberMeServices.setParameter(&quot;remember-me&quot;);
    rememberMeServices.setAlwaysRemember(true);
    return rememberMeServices;
}

@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = &quot;bcrypt&quot;;
    Map encoders = new HashMap&lt;&gt;();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}</code></pre><h2 id="🔑-remember-me-key-값">🔑 Remember Me Key 값</h2>
<p>저는 임의로 Remember Me의 key 값을 rememberMeKey 로 대충 지었지만, 사실 이 값은 조금 더 신경써서 지어야 할 것 같긴 합니다… 😂 왜냐하면 애플리케이션 내에서 토큰이 생성될 때마다 항상 사용되는 private 한 value이기 때문입니다.</p>
<h2 id="👛-token-repository-가-필요한-이유는">👛 Token Repository 가 필요한 이유는?</h2>
<p>RememberMeServices 를 구현하는 방식은 2개가 있는데, <code>TokenBasedRememberMeServices</code> 와 <code>PersistentTokenBasedRememberMeServices</code> 입니다.</p>
<p><code>TokenBasedRememberMeServices</code> 은 토큰을 브라우저에 저장하는 반면 <code>PersistentTokenBasedRememberMeServices</code> 은 서버에 토큰을 저장해서 이용합니다. 또한 전자의 방식은 유저네임, 쿠키만료시간, 비밀번호의 값이 토큰에 포함되어 있기 때문에, 토큰 탈취시 해당 정보들이 노출되는 취약점을 안고 있습니다. 그에 비해 후자의 방식은 series 값과 token 값을 따로 만들어서 저장하기 때문에 유저네임과 만료시간이 노출되지 않습니다. 또한 다른 브라우저에 로그인 할 때마다 series 값이 바뀌는데, 최근 로그인 한 계정의 series 값을 기준으로 token 값을 검사하기 때문에 보안상 좀 더 안전하다고 할 수 있습니다.</p>
<p>그래서 결과적으로 저는 후자의 방식 즉 <code>PersistentTokenBasedRememberMeServices</code> 을 통해 remember me 기능을 구현하였습니다.</p>
<p>그렇기 때문에 이 Token Repository가 필요했고, 이 Token을 저장할 테이블을 먼저 생성해주었습니다. 그리고 이 테이블의 이름은 고정이 되어있기 때문에, persistentLogins 라는 이름의 Entity를 만들어주었습니다. 그리고 아래는 해당 Table이 생성될 때 나가는 SQL DDL문입니다.</p>
<pre><code>create table if not exists persistent_logins ( 
  username varchar_ignorecase(100) not null, 
  series varchar(64) primary key, 
  token varchar(64) not null, 
  last_used timestamp not null 
);</code></pre><h2 id="💡-jdbctokenrepositoryimpl-과-datasource의-연결">💡 JdbcTokenRepositoryImpl 과 dataSource의 연결</h2>
<p>그런 다음, JdbcTokenRepositoryImpl 객체를 만들어서 MySQL 데이터베이스와 연결해주었습니다. 이미 <code>application.properties</code> 파일에 dataSource에 관한 정보를 기입해두었기 때문에 스프링은 이미 해당 dataSource를 Bean으로 등록한 상태입니다. 그래서 SecurityConfiguration 클래스에 dataSource 의존성을 주입해주었고, 해당 객체에 기존에 연결되어있는 dataSource 를 연결해 준 것입니다.</p>
<pre><code>@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(dataSource);
    return repo;
}</code></pre><p>그 후 위에서 언급한대로 PersistentTokenBasedRememberMeServices 를 생성하여 빈으로 만들어 주었는데, 이렇게 랜덤하게 생성된 series 와 token 값의 조합은 브루트 포스(brute force) 공격에 굉장히 강하다고 합니다.</p>
<pre><code>@Bean
public PersistentTokenBasedRememberMeServices rememberMeServices(PersistentTokenRepository tokenRepository) {
    PersistentTokenBasedRememberMeServices rememberMeServices = new
            PersistentTokenBasedRememberMeServices(&quot;rememberMeKey&quot;, new UserDetailsServiceImpl(memberRepository), tokenRepository);
    rememberMeServices.setParameter(&quot;remember-me&quot;);
    rememberMeServices.setAlwaysRemember(true);
    return rememberMeServices;
}</code></pre><p>또한 Spring Security 5.0부터 사용하는 Password Encoder 형식에 맞춰 Custom DelegatingPasswordEncoder 를 생성해주었습니다. <code>DelegatingPasswordEncoder</code> Storage Format 은 다음과 같은데, 회원가입 시 사용자가 비밀번호를 입력하면 아래와 같은 방식으로 저장되게 됩니다.</p>
<p>password 형식 : {bcrypt}$2a$10$bVcGLHvjjOGC8qs7Bfr7bupA4NFyZPv8n3h2tgokZsTpe3gdm7XQ</p>
<pre><code>@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = &quot;bcrypt&quot;;
    Map encoders = new HashMap&lt;&gt;();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}</code></pre><p>위 과정을 다 잘 마친 후에, 이 passwordEncoder 방식을 정의할 때 DelegatingPasswordEncoder 저장 포맷에 맞게 해야하는 것을 몰라서 한참을 헤매었습니다. 결과적으로는 위와 같이 설정하여 remember-me cookie 생성을 잘 완료할 수 있었습니다!</p>
<h2 id="reference">Reference</h2>
<p><a href="https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html">https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html</a>
<a href="https://www.baeldung.com/spring-security-remember-me">https://www.baeldung.com/spring-security-remember-me</a>
<a href="https://www.baeldung.com/spring-security-persistent-remember-me">https://www.baeldung.com/spring-security-persistent-remember-me</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우리 서비스의 톤&매너가 바뀐다면? 글로벌 서비스가 된다면? (cf. 메세지, 국제화)]]></title>
            <link>https://velog.io/@mindfulness_22/spring-message</link>
            <guid>https://velog.io/@mindfulness_22/spring-message</guid>
            <pubDate>Tue, 16 Aug 2022 12:32:26 GMT</pubDate>
            <description><![CDATA[<p>이번 프로젝트는 상용화 서비스를 만들기 위함은 아니었지만, 학습 차원에서 메세지, 국제화 기능을 적용해보았습니다. 
또한 제대로 작동하는지 확인을 위해 테스트 코드 작성까지 완료했습니다.</p>
<p>만약에 고객들에게 “안녕~”이라고 인사하던 텍스트가 모두 “안녕하세요”라고 바뀌어야 한다면, “상품명”이라고 적힌 단어들이 모두 “상품이름”으로 바뀌어야 한다면? 🙀 수십, 수백장의 페이지 속에 수천개의 단어들을 바꿔야 하는 상황이 올 수도 있겠죠? 이런 상황을 대비하여 사용하는 것이 메세지 기능입니다. </p>
<p>html 에 사용되는 단어들을 properties 파일에 key, value 형식으로 저장해둔 뒤, 가져와 쓰는 것이죠. </p>
<pre><code>member.username=유저명
member.loginId=아이디
member.password=패스워드

mypage={0}님 안녕하세요!</code></pre><pre><code>&lt;label for=&quot;username&quot; th:text=&quot;#{member.username}&quot;&gt;닉네임&lt;/label&gt;</code></pre><p>또한, Globally 🌐 서비스를 하는 회사 웹사이트의 경우 구글 크롬의 언어 변경에 따라, 언어가 변경되는 것을 경험하신 적이 있을 거에요! 그 기능도 이 메세지 기능을 조금 응용해서 적용할 수가 있는데요.</p>
<pre><code>member.username=Username
member.loginId=ID
member.password=PW

mypage=Hello, {0}!</code></pre><p>이런식으로 똑같은 <code>message.properties</code> 파일을 국가적으로 관리하는 것을 국제화라고 합니다.</p>
<p>그리고 이 2가지의 파일 모두를 인식할 수 있도록 <code>application.properties</code> 파일에서 <code>spring.messages.basename=messages,messages_en</code> 을 추가해두었습니다. 이렇게 함으로써 MessageSource 도 스프링 빈으로 등록된다고 합니다. 그래서 이 부분까지 테스트 코드 작성으로 제대로 작동되는지 확인해주었습니다.</p>
<pre><code>@SpringBootTest
public class MessageSourceTest {

    @Autowired
    MessageSource ms;

    @Test
    void MessageInKorea() {
        String message = ms.getMessage(&quot;member.loginId&quot;, null, Locale.KOREA);
        Assertions.assertThat(&quot;아이디&quot;).isEqualTo(message);
    }

    @Test
    void MessageInEn() {
        String message = ms.getMessage(&quot;member.loginId&quot;, null, Locale.ENGLISH);
        Assertions.assertThat(&quot;ID&quot;).isEqualTo(message);
    }

    @Test
    void MessageInUnknown() {
        String message = ms.getMessage(&quot;member.loginId&quot;, null, null);
        Assertions.assertThat(&quot;아이디&quot;).isEqualTo(message);
    }

    @Test
    void CodeNotFound() {
        Assertions.assertThatThrownBy(()-&gt; ms.getMessage(&quot;no_code&quot;, null, Locale.KOREA))
                .isInstanceOf(NoSuchMessageException.class);
    }
}</code></pre><p>MessageSource가 Bean으로 등록된 스프링 컨테이너를 가져오기 위해 통합테스트를 실시하였고, Bean은 <code>@Autowired</code>로 주입 받았습니다.</p>
<p>이렇게 국가별 언어를 브라우저에서 어떻게 인식하는지 확인하기 위해서는 Request Headers에 있는 accept-language 를 확인하면 되고, 테스트에서는 <code>ms.getMessage(&quot;member.loginId&quot;, null, Locale.ENGLISH)</code> 에서 세번째 인자값 Locale에 국가를 넣어서 확인해주었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FE없이 SSR, 좌충우돌 Thymeleaf 사용기]]></title>
            <link>https://velog.io/@mindfulness_22/thymeleaf</link>
            <guid>https://velog.io/@mindfulness_22/thymeleaf</guid>
            <pubDate>Tue, 16 Aug 2022 12:19:46 GMT</pubDate>
            <description><![CDATA[<h2 id="1-thymeleaf-템플릿-경로-관련-에러-해결">1. Thymeleaf 템플릿 경로 관련 에러 해결</h2>
<p>스프링 부트는 <code>src/main/resources/templates</code> 경로에서 템플릿을 찾는다. 하지만 만약에 모든 템플릿이 예를 들어 <code>new-templates</code> 폴더 안에 들어가도록 변경해야하는 요구사항이 들어왔다고 하자. 그러면 어떻게 해야할까? 아래와 같이 spring.thymeleaf.prefix를 변경해주면 된다. 기본적으로 suffix는 <code>.html</code> 로 되어 있다.</p>
<p>spring.thymeleaf.prefix=classpath:/new-templates/
spring.thymeleaf.suffix=.html</p>
<p>하지만 나는 프로젝트 진행 중 아래와 같은 에러 메세지를 만나게 되었다. </p>
<pre><code>Exception processing template &quot;/login/loginPage&quot;: 
Error resolving template [/login/loginPage], 
template might not exist or might not be accessible 
by any of the configured Template Resolvers</code></pre><h3 id="why">Why?</h3>
<p>왜 일까? 더욱이 문제의 갈피를 잘 못 찾았던 것은, Local에서는 잘 실행이 되었는데, 호스팅을 했을 때 문제가 생긴 까닭이었다. 무엇이 원인이었을까? 그것은 내가 Spring MVC에서 view 를 반환할 때 그 경로를 <code>/login/loginPage</code> 이렇게 해두었기 때문. 인텔리제이 IDE에서는 중복 슬래시(//)도 알아서 처리를 해주지만 jar 파일로 배포를 할 때는 그러한 처리 과정이 없기 때문에 오류가 난 것이었다. 모든 view에서 앞에 있는 /(slash)를 제거해준 뒤 문제를 해결할 수 있었다.</p>
<h2 id="2-delete-put-method-처리-방법-해결">2. Delete, Put Method 처리 방법 해결</h2>
<pre><code>&lt;form th:action=&quot;@{/posts/{postId}/delete(postId=${postAndComments.id})}&quot; 
th:object=&quot;${postAndComments}&quot; th:method=&quot;delete&quot;&gt;</code></pre><p>Thymeleaf 에서는 Delete, Put 방식의 경우 post 방식으로 처리되지만, <code>&lt;input type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;delete&quot;&gt;</code> or <code>&lt;input type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;put&quot;&gt;</code> 이 추가되어서 Spring MVC에 의해 적절히 처리된다. 그래서 delete 방식의 경우 <code>th:method=”delete”</code> 로 하고, Controller 단에서는 <code>@PostMapping</code>으로 처리해야 한다.</p>
<pre><code>// 게시글 삭제
@PostMapping(&quot;/posts/{postId}/delete&quot;)
public String deletePost(@AuthenticationPrincipal UserDetailsImpl userDetails,
                             @PathVariable Long postId) {
    boardService.deletePost(postId, userDetails.getUsername());
    return &quot;redirect:/posts&quot;;
    }</code></pre><h2 id="3-기타-특이점">3. 기타 특이점</h2>
<h3 id="thobject">th:object</h3>
<p>form에서 submit을 할 때, form의 데이터가 th:object에 설정한 객체로 받아진다.</p>
<h3 id="thfield">th:field</h3>
<p>각 필드를 매핑해주는 역할을 한다. 설정해 준 값으로, th:object의 객체내부 값과 매칭해준다. </p>
<p>아래는 로그인 페이지 예시이다.</p>
<pre><code>    &lt;form th:object=&quot;${loginForm}&quot; th:action=&quot;@{login}&quot; method=&quot;post&quot;&gt;
        &lt;div&gt;
            &lt;label for=&quot;username&quot; th:text=&quot;#{member.loginId}&quot;&gt;아이디&lt;/label&gt;
            &lt;input th:type=&quot;text&quot; id=&quot;username&quot; th:field=&quot;*{username}&quot; name=&quot;username&quot;&gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;label for=&quot;password&quot; th:text=&quot;#{member.password}&quot;&gt;비밀번호&lt;/label&gt;
            &lt;input th:type=&quot;password&quot; id=&quot;password&quot; th:field=&quot;*{password}&quot; name=&quot;password&quot;&gt;
        &lt;/div&gt;
        &lt;div class=&quot;checkbox&quot;&gt;
            &lt;label&gt;&lt;input class=&quot;checkbox&quot; type=&quot;checkbox&quot; id=&quot;remember&quot; name=&quot;remember-me&quot;&gt;Remember me&lt;/label&gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;div class=&quot;form-check&quot;&gt;
                &lt;input type=&quot;checkbox&quot; id=&quot;open&quot; th:field=&quot;*{open}&quot;&gt;
                &lt;label for=&quot;open&quot;&gt;자동 로그인&lt;/label&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;button type=&quot;submit&quot;&gt;로그인&lt;/button&gt;
    &lt;/form&gt;</code></pre><h2 id="reference">Reference</h2>
<p><a href="https://www.baeldung.com/spring-thymeleaf-template-directory">https://www.baeldung.com/spring-thymeleaf-template-directory</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[통합 테스트 말고 단위 테스트만 하고 싶으면 어떻게 하죠?]]></title>
            <link>https://velog.io/@mindfulness_22/unit-test-code-first-experience</link>
            <guid>https://velog.io/@mindfulness_22/unit-test-code-first-experience</guid>
            <pubDate>Tue, 16 Aug 2022 12:06:44 GMT</pubDate>
            <description><![CDATA[<p>처음으로 Controller, Service, Repository 별로 격리된 환경에서 단위 테스트 코드를 작성하다보니 모든 게 낯설고 생소했습니다. 처음에는 어노테이션 하나, Mock 객체 주입 하나도 제대로 이해하지 못하고 사용하니 Failed to load Application Context 등 셀수 없는 오류를 만났습니다. 
아래는 각 단위 테스트 코드를 작성하며 공부하고 이해한 내용을 정리한 것입니다.</p>
<h2 id="🧪-controller-test">🧪 Controller Test</h2>
<pre><code>@WebMvcTest(controllers = {BoardController.class})
    class BoardControllerTest {

        @Autowired
        MockMvc mockMvc;

        @MockBean
        @Autowired
        BoardService boardServiceMock;

        @Autowired
        WebApplicationContext was;

        @BeforeEach
        public void setUp() {
                    // 1번 - webAppContextSetup
                    this.mockMvc = MockMvcBuilders
                            .webAppContextSetup(was)
                            .build();

                    // 2번 - standaloneSetup
            this.mockMvc = MockMvcBuilders
                    .standaloneSetup(new BoardController(boardServiceMock))
                    .build();
        }

        @Test
        @DisplayName(&quot;GET : 전체 게시글 조회&quot;)
        void getPosts() throws Exception {
            mockMvc.perform(get(&quot;/posts&quot;))
                    .andExpect(status().isOk())
                    .andExpect(view().name(&quot;board/posts&quot;));
        }

        @Test
        @DisplayName(&quot;GET : 검색 게시글 조회&quot;)
        @WithMockCustomAccount
        void getSearchPosts() throws Exception {
            mockMvc.perform(get(&quot;/search&quot;))
                    .andExpect(status().isOk())
                    .andExpect(view().name(&quot;board/posts&quot;));
        }
    }</code></pre><p>BoardController Class의 단위테스트 코드를 먼저 보자.</p>
<h3 id="📗-webmvctest">📗 WebMvcTest</h3>
<p><a href="https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.html">스프링 공식문서</a>를 보면 이 어노테이션에 대해 제대로 이해할 수가 있다.</p>
<p>Annotation that can be used for a Spring MVC test that focuses <strong>only</strong> on Spring MVC components.</p>
<p>Spring MVC 컴포넌트들에만 집중해서 MVC 테스트를 진행하고 싶을 때 사용하는 어노테이션이라고 한다.</p>
<p>Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. <code>@Controller</code>, <code>@ControllerAdvice</code>, <code>@JsonComponent</code>, <code>Converter</code>/<code>GenericConverter</code>, <code>Filter</code>, <code>WebMvcConfigurer</code> and <code>HandlerMethodArgumentResolver</code> beans but not <code>@Component</code>, <code>@Service</code> or <code>@Repository</code> beans).</p>
<p>이 어노테이션을 사용하면 MVC 테스트에 필요한 설정, 빈 정보들만 가져오게 되고 통합테스트 처럼 전체 설정내용을 다 가져오지는 않는다. 예를 들어, Controller, WebMvcConfigurer 와 같은 컴포넌트들은 가져오지만 예를 들어 Controller 테스트에 필요없는 일반 Component, Service, Repository 빈들은 가져오지 않는다. (그래서 원래 Controller가 Service 의존성을 주입받고 있다면 MockBean을 붙여줘서 객체를 생성해줘야 하는 것이다.)</p>
<p>By default, tests annotated with <code>@WebMvcTest</code> will also auto-configure Spring Security and <code>MockMvc</code> (include support for HtmlUnit WebClient and Selenium WebDriver). For more fine-grained control of MockMVC the <code>@AutoConfigureMockMvc</code> annotation can be used.</p>
<p>그리고 WebMvcTest 어노테이션이 붙은 테스트는 자동으로 Spring Security 와 MockMvc를 자동설정해준다고 한다. (그래서 http.antMatcher에 없는 즉 로그인 인증이 필요한 url의 경우 접근이 막혀 있는 것까지 테스트가 가능했다.)</p>
<p>Typically <code>@WebMvcTest</code> is used in combination with <code>@MockBean</code> or <code>@Import</code> to create any collaborators required by your <code>@Controller</code> beans.</p>
<p>그래서 위에 말했던 것처럼 Controller 관련 되지 않은 Bean들은 가져오지 않기 때문에 MockBean 이런 어노테이션과 함께 쓰인다고 한다.</p>
<p>If you are looking to load your full application configuration and use MockMVC, you should consider <code>@SpringBootTest</code> combined with <code>@AutoConfigureMockMvc</code> rather than this annotation.</p>
<p>만약에 애플리케이션 전체의 configuration 을 다 가져오고 싶다면 이 어노테이션 보다는 SpringBootTest 즉 통합 테스트 때 쓰이는 이 어노테이션 사용을 한번 고려해보라고 한다.</p>
<p>When using JUnit 4, this annotation should be used in combination with <code>@RunWith(SpringRunner.class)</code>.</p>
<p>JUnit 4에서는 이 어노테이션은 @Runwith(SpringRunner.class)와 함께 쓰였다고 하는데 나는 JUnit 5를 사용했고, 해당없는 사항이었다.</p>
<p>어쨌든, 단위테스트의 목적으로 위와 같은 여러 사항들을 고려해서 WebMvcTest 어노테이션을 달아주었고 어떤 Controller를 테스트할 것인지도 옆에 적어주었다.</p>
<h3 id="📗-mockmvc">📗 MockMvc</h3>
<blockquote>
<p>MockMvc는 컨트롤러 테스트를 할 때, 실제 서버에 구현한 애플리케이션을 올리지 않고(실제 서블릿 컨테이너를 사용하지 않고) 테스트용 MVC환경을 만들어 요청 및 전송, 응답기능을 제공해주는 유틸리티 클래스다.</p>
</blockquote>
<p>이 기능으로 실제 서블릿 컨테이너에서 컨트롤러를 실행하지 않고도 컨트롤러에 HTTP 요청을 할 수 있다. </p>
<h3 id="📗-webappcontextsetup-vs-standalonesetup">📗 webAppContextSetup vs standaloneSetup</h3>
<p>mockMvc를 설정하기 위해서는 MockMvcBuilder를 사용해야 하는데, standaloneSetup과 webAppContextSetup이라는 정적 메서드 중 하나를 선택해야 한다.</p>
<ul>
<li>standaloneSetup : 테스트할 Controller를 하나 만들고 수동으로 종속성을 주입할 수가 있다.</li>
<li>webAppContextSetup : WebApplicationContext를 사용하여 MockMvc Context를 작성한다.</li>
</ul>
<p>처음에는 standaloneSetup으로만 하면 충분할 거라고 생각했다.</p>
<pre><code>// 2번 - standaloneSetup
            this.mockMvc = MockMvcBuilders
                    .standaloneSetup(new BoardController(boardServiceMock))
                    .build();</code></pre><p>그래서 2번 방식으로 setup을 해주었는데 문제가 생겼다. 게시글 전체 조회 기능의 경우 로그인 하지 않은 사용자도 접근이 가능하기 때문에 테스트에 통과했는데, 나머지 기능의 경우 Authentication 정보를 가져와야 했다. 그러다 보니 1번 방식으로 바꿀 수 밖에 없었다.</p>
<pre><code>// 1번 - webAppContextSetup
                this.mockMvc = MockMvcBuilders
                        .webAppContextSetup(was)
                        .build();</code></pre><h3 id="📗-withmockcustomaccount">📗 @WithMockCustomAccount</h3>
<p><code>WithSecurityContextFactory</code> 인터페이스를 상속받은 <code>WithMockCustomAccountSecurityContextFactory</code> 클래스를 하나 만들어주었다. 이를 통해 SecurityContext를 임의로 만들어주었고, 이를 적용한 Annotation을 하나 만들었다. 코드는 아래와 같다.</p>
<p><code>WithMockCustomAccountSecurityContextFactory</code> </p>
<pre><code>    public class WithMockCustomAccountSecurityContextFactory implements WithSecurityContextFactory&lt;WithMockCustomAccount&gt; {
        @Override
        public SecurityContext createSecurityContext(WithMockCustomAccount annotation) {
            // 1
            SecurityContext context = SecurityContextHolder.createEmptyContext();

            // 2
            Map&lt;String, Object&gt; attributes = new HashMap&lt;&gt;();
            attributes.put(&quot;username&quot;, annotation.username());
            attributes.put(&quot;loginId&quot;, annotation.loginId());
            attributes.put(&quot;password&quot;, annotation.password());

            Member member = new Member((String) attributes.get(&quot;username&quot;),
                    (String) attributes.get(&quot;loginId&quot;),
                    (String) attributes.get(&quot;password&quot;)
            );

            // 3
            UserDetailsImpl principal = new UserDetailsImpl(member);

            // 4
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                    principal,
                    principal.getAuthorities(),
                    Collections.emptyList());

            // 5
            context.setAuthentication(token);
            return context;
        }
    }</code></pre><p><em><code>WithMockCustomAccount</code></em></p>
<pre><code>    @Retention(RetentionPolicy.RUNTIME)
    @WithSecurityContext(factory = WithMockCustomAccountSecurityContextFactory.class)
    public @interface WithMockCustomAccount {

        String username() default &quot;username&quot;;

        String loginId() default &quot;loginId&quot;;

        String password() default &quot;password&quot;;
    }</code></pre><h2 id="🧪-service-test">🧪 Service Test</h2>
<pre><code>    @ExtendWith(MockitoExtension.class)
    class BoardServiceImplTest {

        @InjectMocks
        BoardServiceImpl boardService;
        @Mock
        BoardRepository boardRepository;
        @Mock
        MemberRepository memberRepository;
        @Mock
        CommentRepository commentRepository;

        @Test
        void findPostAndComments() {
            // given
            Member member = new Member(&quot;username&quot;, &quot;loginId&quot;, &quot;password&quot;);
            Post post = new Post(100L, &quot;title&quot;, &quot;content&quot;, MainCategory.FRAMEWORK, SubCategory.DJANGO);
            post.setMember(member);
            Comment comment = new Comment(100L, &quot;comment-content&quot;);
            comment.setMember(member);
            comment.setPost(post);

            // stub
            when(boardRepository.findById(100L)).thenReturn(Optional.of(post));
            when(memberRepository.findByUsername(member.getUsername())).thenReturn(Optional.of(member));

            // when
            PostAndCommentResponseDto dtoCorrect = boardService.findPostAndComments(100L, member.getUsername());

            // then
            assertThat(100L).isEqualTo(dtoCorrect.getId());
            assertThat(1).isEqualTo(dtoCorrect.getComments().size());
            assertThatThrownBy(() -&gt; boardService.findPostAndComments(101L, member.getUsername())).isInstanceOf(CustomException.class);
        }
    }</code></pre><h3 id="📙-extendwithmockitoextensionclass">📙 @ExtendWith(MockitoExtension.class)</h3>
<p>Mockito의 Mock, InjectMocks 어노테이션을 사용하기 위해서 Mockito의 테스트 실행을 확장해주어야 한다.</p>
<ul>
<li>injectMocks : @Mock이나 @Spy 객체를 생성해서 자신의 멤버 클래스와 일치하면 주입한다.</li>
<li>Mock : Mock 객체를 만들어서 반환한다. 실제 인스턴스가 아니라 가상의 인스턴스를 만든 것이다.</li>
</ul>
<h3 id="📙-stub">📙 Stub</h3>
<p>보통 단위테스트에서는 격리된 단위 테스트 케이스를 작성하기 위해 의존하고 있는 다른 객체를  대체하여 미리 만들어진 응답 결과(canned answer)를 반환하게 해야 한다. 그래서 우리는 단위테스트에서 테스트 스텁을 사용한다. 쉽게 말해 스텁은 메소드의 결과를 미리 지정하는 것이다.</p>
<p>만일, 테스트에서 사용하지 않는 Stub이 있을 경우 <code>Unnecessary stubbings detected.</code> 에러가 발생했다.</p>
<h2 id="🧪-repository-test">🧪 Repository Test</h2>
<pre><code>    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    class BoardRepositoryTest {

        @Autowired
        BoardRepository boardRepository;
        @Autowired
        MemberRepository memberRepository;
        @Autowired
        CommentRepository commentRepository;

        @Test
        @DisplayName(&quot;ID &amp; 게시글 작성자 일치여부 확인&quot;)
        void findByIdAndMember_Username() {
            // given
            Member member = new Member(&quot;username&quot;, &quot;loginId&quot;, &quot;password&quot;);
            memberRepository.save(member);
            Post post = new Post(&quot;title1&quot;, &quot;content1&quot;, MainCategory.FRAMEWORK, SubCategory.DJANGO);
            post.setMember(member);
            Post savedPost = boardRepository.save(post);
            // when
            Post foundPost = boardRepository.findByIdAndMember_Username(savedPost.getId(), &quot;username&quot;)
                    .orElseThrow(() -&gt; new CustomException(Error.NO_AUTHORITY_POST));
            // then
            assertThat(post.getTitle()).isEqualTo(foundPost.getTitle());
            assertThatThrownBy(() -&gt; boardRepository.findByIdAndMember_Username(savedPost.getId(), &quot;wrong-username&quot;)
                    .orElseThrow(() -&gt; new CustomException(Error.NO_AUTHORITY_POST)))
                    .isInstanceOf(CustomException.class);
        }
    }</code></pre><h3 id="📘-datajpatest-아래는-스프링-공식문서-내용">📘 @DataJpaTest (아래는 스프링 공식문서 내용)</h3>
<p>Annotation for a JPA test that focuses <strong>only</strong> on JPA components.</p>
<p>오직 JPA 컴포넌트들에만 집중해서 JPA 테스트용 어노테이션이다.</p>
<p>Using this annotation will disable full auto-configuration and instead apply only configuration relevant to JPA tests.</p>
<p>JPA 테스트와 관련한 설정들에만 적용되고 전체 설정 파일들을 다 불러오지는 않는다. 맥락은 위에서 본 WebMvcTest와 비슷하다고 보면 될 것 같다.</p>
<p>By default, tests annotated with <code>@DataJpaTest</code> are transactional and roll back at the end of each test. They also use an embedded in-memory database (replacing any explicit or usually auto-configured DataSource). The <code>@AutoConfigureTestDatabase</code> annotation can be used to override these settings.</p>
<p>기본적으로 @DataJpaTest라는 어노테이션이 붙어 있으면 Transactional이고 각 테스트 종료 후 롤백이 된다. 여기서는 인메모리 데이터 베이스를 사용한다고 하는데 이것때문에 에러가 나기도 해서, 나는 실제 내가 사용하는 MySQL 메모리로 테스트 하기위해 적힌 대로 @AutoConfigureTestDatabase를 써서 replace.None 값으로 변경해주었다.</p>
<h3 id="📘-autoconfiguretestdatabase">📘 @AutoConfigureTestDatabase</h3>
<p>실제 연결된 DB(MySQL)을 통해 테스트를 진행하려고 하면 <code>@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)</code> 을 붙여줘야 하는데 뭘 replace 하지 않겠다는 것인지 잘 몰라서 코드를 뜯어보았다. </p>
<p>우선 <code>@DataJpaTest</code> 를 보면 AutoConfigure를 해주는 부분이 상당히 많다.</p>
<pre><code>    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @BootstrapWith(DataJpaTestContextBootstrapper.class)
    @ExtendWith(SpringExtension.class)
    @OverrideAutoConfiguration(enabled = false)
    @TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
    @Transactional
    @AutoConfigureCache
    @AutoConfigureDataJpa
    @AutoConfigureTestDatabase
    @AutoConfigureTestEntityManager
    @ImportAutoConfiguration
    public @interface DataJpaTest {</code></pre><p>이 중에서 <code>@AutoConfigureTestDatabase</code> 이 어노테이션을 살펴보면, default 값이 <em>`Replace</em>.ANY<code>로 되어 있는 것을 알 수 있다. 만일,</code>H2<code>,</code>DERBY<code>,</code>HSQLDB<code>로 자동연결되고 싶지 않다면,</code>@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)` 를 넣어주면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[특정 기능에만 로그를 찍고 싶어! 그럼 어떻게 하지? 답은 AOP!]]></title>
            <link>https://velog.io/@mindfulness_22/%ED%8A%B9%EC%A0%95-%EA%B8%B0%EB%8A%A5%EC%97%90%EB%A7%8C-%EB%A1%9C%EA%B7%B8%EB%A5%BC-%EC%B0%8D%EA%B3%A0-%EC%8B%B6%EC%96%B4-%EA%B7%B8%EB%9F%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EC%A7%80-%EB%8B%B5%EC%9D%80-AOP</link>
            <guid>https://velog.io/@mindfulness_22/%ED%8A%B9%EC%A0%95-%EA%B8%B0%EB%8A%A5%EC%97%90%EB%A7%8C-%EB%A1%9C%EA%B7%B8%EB%A5%BC-%EC%B0%8D%EA%B3%A0-%EC%8B%B6%EC%96%B4-%EA%B7%B8%EB%9F%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EC%A7%80-%EB%8B%B5%EC%9D%80-AOP</guid>
            <pubDate>Tue, 16 Aug 2022 11:38:25 GMT</pubDate>
            <description><![CDATA[<p>애플리케이션 로직은 2가지로 나눌 수 있다. </p>
<ol>
<li>핵심 기능(ex. BoardSerivce의 핵심 기능은 게시판 로직)</li>
<li>부가 기능(ex. 로그 추적)</li>
</ol>
<p>부가 기능을 핵심 기능이 있는 Controller, Service, Repository Layer에 넣어도 좋을까? 답은 No이다. 생각해봐도 반복 코드가 넘칠 것 같고, 수정할 일이 생기면 너무 복잡할 것 같았다. 결국 핵심은 1. 부가 기능과 핵심 기능을 분리하는 것, 2. 부가 기능을 어디에 적용할지 결정하는 것이었다. 
사실 AOP라는 단어는 종종 들어보기는 했지만 이번 프로젝트에서 실제 적용을 해보기 전까지는 굉장히 추상적이고 막연한 개념이었다. </p>
<h3 id="aop-적용-방식">AOP 적용 방식</h3>
<p>공부를 해보니, AOP 적용 방식은 크게 3가지가 있었다. 컴파일 시점, 클래스 로딩 시점, 런타임 시점에 적용하는 것이다. 하지만 프록시 방식을 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다.</p>
<p>-- 추가 설명 </p>
<ol>
<li>컴파일 시점(.java 소스 코드를 컴파일러를 사용해 .class 파일로 만드는 시점에 부가기능 로직을 추가)</li>
<li>클래스 로딩 시점(.class 파일은 JVM의 클래스 로더에 보관한다. 이 때 조작한 파일을 JVM에 올린다.)</li>
<li>런타임 시점(프록시) (자바의 main 메서드가 이미 실행된 다음, 프록시를 통해 스프링 빈에 부가 기능을 적용할 수 있다.)</li>
</ol>
<h3 id="🔎-이번-프로젝트에-aop를-적용한-코드">🔎 이번 프로젝트에 AOP를 적용한 코드</h3>
<p>프록시 방식을 사용하는 스프링 AOP는 스프링 컨테이너가 관리할 수 있는 스프링 빈에만 AOP를 적용할 수 있다. Controller 는 스프링 컨테이너가 관리하는 Component이기 때문에 AOP를 적용할 수 있었고, 나는 Controller 의 코드가 실행되고 끝나는 지점에 AOP를 적용하였다.</p>
<pre><code class="language-java">@Slf4j
@Aspect
public class RequestLoggingAspect {

    @Pointcut(&quot;execution(* com.example.letsgongbu.controller..*(..))&quot;)
    private void allController() {}

    @Around(&quot;allController()&quot;)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {

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

        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            long end = System.currentTimeMillis();
            log.debug(&quot;Request : {}, {}, total : {}&quot;, request.getMethod(), request.getRequestURI(), end-start);
        }
        return joinPoint.proceed();
    }
}</code></pre>
<h3 id="aop-용어-정리">AOP 용어 정리</h3>
<ol>
<li>조인 포인트: AOP를 적용할 수 있는 모든 지점이자 어드바이스가 적용될 수 있는 모든 위치</li>
<li>포인트컷 : 조인 포인트 중 어드바이스(부가기능)가 적용될 위치를 선별하는 기능</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹소켓에서 유저 정보를 가져오려면?]]></title>
            <link>https://velog.io/@mindfulness_22/websocket-userinfo</link>
            <guid>https://velog.io/@mindfulness_22/websocket-userinfo</guid>
            <pubDate>Tue, 16 Aug 2022 11:20:16 GMT</pubDate>
            <description><![CDATA[<h2 id="❓-문제상황">❓ 문제상황</h2>
<p>채팅 기능을 구현하는데, 어떤 사용자가 어떤 메세지를 입력했는지 보여주려고 한다. 메세지 내용 전달에는 성공했으나, 어떤 사용자가 그 내용을 입력했는지 즉 채팅방의 유저 정보를 전달하는데 어려움을 겪고 있었다.</p>
<h2 id="💡-문제-해결을-위한-사고의-흐름">💡 문제 해결을 위한 사고의 흐름</h2>
<ol>
<li>웹소켓 또한 웹서버를 통해 웹브라우저와 통신한다. 하지만 서블릿 세션과 웹소켓 세션은 다르다. </li>
<li>그렇다면 <code>HttpSession</code>에 담긴 정보들을 웹소켓 통신에서 가져올 수 있는 방법은 무엇일까 생각해 보았다.<ol>
<li>interceptor를 통해 handshake를 하기 전에 <code>HttpSession</code> 정보를 가져와서 <code>WebSocketSession</code>이나 <code>StompHeaderAccessor</code> 에 그 값을 넣어주기</li>
<li>기존에 사용하고 있던 <code>ChannelInterceptor</code> 의 핸들러를 사용해 <code>presend</code>나 <code>postsend</code> 단계에서 <code>HttpSession</code>정보를 <code>StompHeaderAccessor</code> 에 넣어주기</li>
</ol>
</li>
</ol>
<p>이런 식으로 생각을 해보다가 <code>HttpSessionHandshakeInterceptor</code> 의 존재를 알게 되었다.</p>
<h3 id="🔍-httpsessionhandshakeinterceptorclass">🔍 HttpSessionHandshakeInterceptor.class</h3>
<p>자바 공식 문서에 따르면, 이 클래스에 대한 설명은 다음과 같다.</p>
<aside>
💡 An interceptor to copy information from the HTTP session to the "handshake attributes" map to made available via `WebSocketSession.getAttributes()`. Copies a subset or all HTTP session attributes and/or the HTTP session id under the key `HTTP_SESSION_ID_ATTR_NAME`.

</aside>

<p><code>HttpSessionHandshakeInterceptor</code> 의 기능은 <code>HttpSession</code>에 있던 정보들을 copy 해주는 것이다. 그래서 적합한 기능을 찾았다고 생각해서 해당 인터셉터를 추가하고 핸들러를 추가해주었다.</p>
<pre><code class="language-java">    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler, &quot;/portfolio&quot;)
                .addInterceptors(new HttpSessionHandshakeInterceptor())
                .withSockJS();
    }</code></pre>
<p>테스트로 <code>WebSocketSession</code>을 통해 <code>SpringSecurityContext</code>의 값을 가져올 수 있었고 유저명 정보도 가져올 수 있음을 확인했다.</p>
<pre><code class="language-java">@Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        SecurityContextImpl o = (SecurityContextImpl) session.getAttributes().get(&quot;SPRING_SECURITY_CONTEXT&quot;);
        UserDetailsImpl principal = (UserDetailsImpl) o.getAuthentication().getPrincipal();
        System.out.println(&quot;username = &quot; + principal.getUsername());
    }</code></pre>
<h4 id="❓-문제-발생">❓ 문제 발생</h4>
<p>하지만 여기서 문제는 이렇게 되면 Front에서 Socket을 시작하는 버튼과 STOMP Message Broker를 시작하는 <strong>버튼이 따로 분리가 될 수 밖에 없는 문제</strong>가 발생했다.</p>
<h4 id="💡-새로운-solution">💡 새로운 Solution</h4>
<p>그래서 2-a에서 이야기한 것처럼 기존의 <code>StompEndpoints</code>에 <code>MyHttpSessionHandshakeInterceptor</code> 를 추가하고 <code>HttpSessionHandshakeInterceptor</code> 를 상속받은 클래스를 만들어서 handshake 전에 <code>HttpSession</code> 값을 가져오기로 했다.</p>
<pre><code class="language-java">@Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(&quot;/portfolio&quot;)
                .addInterceptors(new MyHttpSessionHandShakeInterceptor())
                .withSockJS();
    }</code></pre>
<pre><code class="language-java">public class MyHttpSessionHandShakeInterceptor extends HttpSessionHandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map&lt;String, Object&gt; attributes) throws Exception {
        HttpSession session = getSession(request);
        SecurityContextImpl o = (SecurityContextImpl) session.getAttribute(&quot;SPRING_SECURITY_CONTEXT&quot;);
        UserDetailsImpl principal = (UserDetailsImpl) o.getAuthentication().getPrincipal();
        System.out.println(&quot;principal.getUsername() = &quot; + principal.getUsername());
        return true;
    }

    @Nullable
    private HttpSession getSession(ServerHttpRequest request) {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
            return serverRequest.getServletRequest().getSession(isCreateSession());
        }
        return null;
    }
}</code></pre>
<h4 id="❓-문제-발생-1">❓ 문제 발생</h4>
<p>하지만 이렇게 구현을 했을 때, 2-b 단계에서처럼 header 값에 정보를 넣어주기 위해서는 그 값들을 그 다음 인터셉터로 전달하는 과정이 필요한데 그 부분에서 해답이 떠오르지 않았다.</p>
<p>아니면 프론트 단에 전달하는 message의 payload 안에 그 값을 넣으려고 <code>@MessageMapping</code> 된 Controller 단에서 방법을 찾아보려고 했지만 그 또한 방법을 찾지 못했다.</p>
<p>그러던 중 공식문서에서 <code>MessageMapping</code> 이 가질 수 있는 여러 arguments 들을 재확인하게되었다.</p>
<h4 id="💡-최종-solution-발견">💡 최종 Solution 발견</h4>
<p>이미 <code>@Payload</code>, <code>@DestinationVariable</code> 은 사용하고 있었고, <code>java.security.Principal</code> 라는 것이 눈에 들어왔다. 이에 대한 설명은 <code>Reflects the user logged in at the time of the WebSocket HTTP handshake.</code> 라고 되어 있었다. </p>
<p>Spring Security 프레임워크를 쓰다보니 <code>SecurityContextHolder</code> 안에 <code>SecurityContext</code>가, 그리고 그 안에 <code>Authentication</code>이 들어가고 그를 통해 <code>Principal</code> 을 얻어 현재 로그인된 사용자의 정보를 추출할 수 있다. 나는 이걸 Session 값을 통해서 얻으려고 했는데, STOMP의 Message Mapping 기능에서 파라미터로 Principal 을 바로 쓸 수 있도록 제공을 해주니… 참 최종적으로 여러 삽질을 하다가 굉장히 쉽게 문제가 해결된 셈이다. (공식문서를 처음부터 꼼꼼하게 읽었으면 더 좋았을 걸…) </p>
<pre><code class="language-java">@MessageMapping(&quot;/chat/{chatroom}&quot;)
@SendTo(&quot;/topic/{chatroom}&quot;)
public ChatMessage handle(@Payload ChatMessage chatMessage, 
                                                    @DestinationVariable String chatroom, 
                                                    java.security.Principal principal) {
    chatMessage.addWriter(principal.getName());
    return chatMessage;
}</code></pre>
<p>하지만 이번 계기를 통해 <strong>웹소켓의 동작원리, 일반 WebSocket과 SockJs의 차이, STOMP의 역할 뿐 아니라 웹소켓 VS 서블릿 세션의 통신 방법 등 HTTP 프로토콜과 ws 프로토콜</strong>에 대해 더 명확하게 알 수 있는 시간이 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SLF4J 이해하기 3탄 - SLF4J 그리고 Logback]]></title>
            <link>https://velog.io/@mindfulness_22/slf4j-slf4j-logback-3</link>
            <guid>https://velog.io/@mindfulness_22/slf4j-slf4j-logback-3</guid>
            <pubDate>Fri, 22 Jul 2022 02:40:30 GMT</pubDate>
            <description><![CDATA[<p>이전 글에서 갑자기 블로그에 한번도 올린 적 없던 디자인 패턴을 다루었었죠? 그건 바로 제가 SLF4J 를 공부하던 도중 Facade에 대한 기본적인 이해가 우선적으로 필요하다고 생각했기 때문이에요.</p>
<h2 id="slf4j">SLF4J</h2>
<p>SLF4J는 Simple Logging Facade for Java 라는 이름에서부터 알 수 있듯이, Logback, Log4j2와 같은 Logging Framework의 추상화 역할을 해요. 추상화 로깅 라이브러리이기 때문에 단독으로는 사용할 수 없어요. 배포 시에 개발자가 로깅 프레임워크를 선택해서 연결해줘야 비로소 사용할 수가 있죠.</p>
<p>제일 처음에, Java에서 Log를 남기기 위해서는 slf4j-api dependency를 추가해줘야해요.</p>
<pre><code>&lt;dependency&gt;
    &lt;groupId&gt;org.slf4j&lt;/groupId&gt;
    &lt;artifactId&gt;slf4j-api&lt;/artifactId&gt;
    &lt;version&gt;1.7.25&lt;/version&gt;
&lt;/dependency&gt;</code></pre><p>그리고 아래의 코드로 실행을 해 보았더니…!</p>
<pre><code>import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
    public static void main(String[] args) {
                Logger logger = LoggerFactory.getLogger(App.class);
        logger.info(&quot;INFO {}&quot;, &quot;: Logging Started&quot;);
    }
}</code></pre><p>이런… 에러가 뜨고 말았네요. 앞서 말했든 SLF4J는 추상체이기 때문에, 단독으로 사용할 수가 없어요.</p>
<pre><code>SLF4J: Failed to load class &quot;org.slf4j.impl.StaticLoggerBinder&quot;.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.</code></pre><p>이럴 때 SLF4J 공식문서(<a href="https://www.slf4j.org/manual.html">링크</a>)에서는 이렇게 말하고 있어요. </p>
<p>This warning is printed because no slf4j binding could be found on your class path.</p>
<p>즉, 이 경고는 SLF4J 바인딩이 없기 때문에 생기는 거라고 말해주고 있죠? 
그래서 저는 아래와 같은 dependency를 또 추가해주었어요.</p>
<pre><code>&lt;dependency&gt;
    &lt;groupId&gt;org.slf4j&lt;/groupId&gt;
    &lt;artifactId&gt;slf4j-simple&lt;/artifactId&gt;
    &lt;version&gt;1.7.25&lt;/version&gt;
&lt;/dependency&gt;</code></pre><p>그리고 나서 다시 실행하니, 두둥! </p>
<pre><code>[main] INFO com.example.java8.App - INFO : Logging Started</code></pre><p>정상적인 로그가 찍히고 있네요.</p>
<p>그리고 Logback 프레임워크를 사용하기 위해서 binding은 logback-classic을 쓸 수가 있어요. 또한, logback-classic dependency를 추가하면 자동으로 slf4j-api, logback-core가 추가돼요. 하지만 명시적으로 artifactId를 지정해주는 것이 더 좋기 때문에, 아래처럼 exclude 한 다음에 따로 dependency를 추가할 수도 있어요.</p>
<pre><code>&lt;dependency&gt;
    &lt;groupId&gt;ch.qos.logback&lt;/groupId&gt;
    &lt;artifactId&gt;logback-core&lt;/artifactId&gt;
    &lt;version&gt;1.2.6&lt;/version&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
    &lt;groupId&gt;ch.qos.logback&lt;/groupId&gt;
    &lt;artifactId&gt;logback-classic&lt;/artifactId&gt;
    &lt;version&gt;1.2.6&lt;/version&gt;
    &lt;exclusions&gt;
        &lt;exclusion&gt;
            &lt;groupId&gt;org.slf4j&lt;/groupId&gt;
            &lt;artifactId&gt;slf4j-api&lt;/artifactId&gt;
        &lt;/exclusion&gt;
        &lt;exclusion&gt;
            &lt;groupId&gt;ch.qos.logback&lt;/groupId&gt;
            &lt;artifactId&gt;logback-core&lt;/artifactId&gt;
        &lt;/exclusion&gt;
    &lt;/exclusions&gt;
&lt;/dependency&gt;</code></pre><h2 id="잠깐-근데-왜-slf4j를-사용해야-할까요">잠깐, 근데 왜 SLF4J를 사용해야 할까요?</h2>
<p>예를 들어, log4j를 걷어내고 logback으로 교체하는 업무가 주어졌다고 가정해봐요. (Java 버전이 올라가면서 log4j가 문제를 일으키는 상황들이 있다고 해요.) </p>
<p>그럼, 우리는 우선 maven이나 gradle에서 log4j의 dependency를 exclude하고 다시 logback을 추가를 합니다. 그리고 log4j로 import 된 것을 logback으로 바꾸고… 수작업으로 처리해줘야 할 일이 상당히 많아질 수 있어요. 여기서 SLF4J를 사용한다면 이런 일을 방지할 수가 있어요. 왜냐하면 <strong>SLF4J는 Java의 로깅 모듈들의 추상체이기 때문이에요.</strong></p>
<h4 id="bad👎-하나의-라이브러리에-의존적인-코드-예시">(Bad👎) 하나의 라이브러리에 의존적인 코드 예시</h4>
<pre><code>import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggerFactory;

Logger log = Logger.getLogger(this.getClass());
log.warn(&quot;=====NO, log4j=====&quot;)</code></pre><h4 id="good👍-추상체에-의존적인-코드-예시---framework-손쉽게-변경-가능">(Good👍) 추상체에 의존적인 코드 예시 - framework 손쉽게 변경 가능</h4>
<pre><code>import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Logger logger = LoggerFactory.getLogger(App.class);
logger.info(&quot;=====YES, slf4j=====&quot;);</code></pre><p>또, 대부분의 자바 로깅 모듈은 slf4j의 브릿지를 이미 제공해주고 있어서 slf4j와 다른 프레임워크를 연결하기 위해 추가로 구현 작업이 필요없습니다. 이미 만들어진 API를 찾아 그냥 넣기주기만 하면 됩니다. logback을 쓰고 싶으면 slf4j-api를, log4j2를 쓰고 싶다면 log4j-slf4j-impl과 log4j-api를 추가하면 됩니다. <strong>즉 slf4j를 사용함으로써 하나의 라이브러리에 종속적일 필요가 없게 되는 거죠.</strong></p>
<h2 id="slf4j의-동작-과정">SLF4J의 동작 과정</h2>
<ol>
<li>*<em>SLF4J Bridging Modules *</em><ol>
<li>다른 로깅 API로의 Logger 호출을 SLF4J 인터페이스로 리다이렉트하여 SLF4J API가 대신 처리할 수 있도록 일종의 어댑터 역할을 하는 라이브러리</li>
<li>log4j-over-slf4j, jcl-over-slf4j, jul-to-slf4j</li>
</ol>
</li>
<li><strong>SLF4J API(인터페이스)</strong><ol>
<li>로깅에 대한 추상 레이어를 제공합니다. 즉 추상 메서드를 제공해요.</li>
<li>하지만 위에서 봤듯이 이 dependency만 추가하면 binding 이 없다는 에러를 만나게 돼요. 추상 클래스이기 때문에 단독적으로 사용할 수가 없는 것이죠.</li>
<li>slf4j-api</li>
</ol>
</li>
<li><strong>SLF4J Binding</strong><ol>
<li>SLF4J 인터페이스를 Logging Framework와 연결하는 어댑터 역할을 하는 라이브러리</li>
<li>SLF4J API를 구현한 클래스에서 Binding으로 연결될 Logger의 API를 호출해요. 하나에 API에는 하나의 Binding만을 두어야 해요.</li>
<li>logback: logback-classic</li>
</ol>
</li>
<li><strong>Logging Framework</strong><ol>
<li>logback: logback-core</li>
</ol>
</li>
</ol>
<p>slf4j는 컴파일 시 Logging Framework를 선택할 수 있다는 장점이 있어요. 
만일 이를 변경하고자 한다면, 위에서 Logging Framework 와 Binding을 바꾸면 돼요.</p>
<h2 id="logback">Logback</h2>
<p>Logback은 Log4j의 improved 버전이에요. 심지어 Log4j를 만든 개발자가 직접 만들었죠.</p>
<p>Logback을 Spring에서 사용하기 위해서는 resources 디렉토리 안에 logback-spring.xml 파일을 만들어서 설정을 해주면 돼요.</p>
<pre><code>&lt;configuration&gt;
  &lt;appender name=&quot;stdout&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
    &lt;layout class=&quot;ch.qos.logback.classic.PatternLayout&quot;&gt;
      &lt;Pattern&gt;%d{yyyy-MM-dd HH:mm:ss} %-5p %m%n&lt;/Pattern&gt;
    &lt;/layout&gt;
  &lt;/appender&gt;

  &lt;appender name=&quot;fout&quot; class=&quot;ch.qos.logback.core.FileAppender&quot;&gt;
    &lt;file&gt;baeldung.log&lt;/file&gt;
    &lt;append&gt;false&lt;/append&gt;
    &lt;encoder&gt;
      &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss} %-5p %m%n&lt;/pattern&gt;
    &lt;/encoder&gt;
  &lt;/appender&gt;

  &lt;logger name=&quot;com.baeldung.log4j&quot; level=&quot;TRACE&quot;/&gt;

  &lt;root level=&quot;INFO&quot;&gt;
    &lt;appender-ref ref=&quot;stdout&quot; /&gt;
    &lt;appender-ref ref=&quot;fout&quot; /&gt;
  &lt;/root&gt;
&lt;/configuration&gt;</code></pre><p>Spring Boot에는 기본적으로 logback이 포함되어 있어요. 그래서 Application을 실행시킬 때 IDE console 창을 보면 LogbackApplication이 시작되었다는 문구를 확인할 수가 있는데요. </p>
<p>Spring Boot에서는 logback.xml로 설정을 하면 스프링 부트 설정 전에 logback 설정이 되므로 제어할 수가 없게 돼요. 그래서 Spring boot에서는 resources/logback-spring.xml 에서 설정을 하거나 혹은 application.properties를 통해 logback을 제어할 수가 있어요. 
Spring 혹은 일반 java 프로그램의 경우 그냥 logback.xml을 resources 경로 안에 생성해주면 돼요.</p>
<h3 id="logback-appenders">Logback Appenders</h3>
<p>그리고 만일, &quot;개발 환경에서는 로그를 Debug 레벨까지 모두 Console에 찍어주고, 배포 환경에서는 파일로 따로 관리하도록 시스템을 구축해주세요!&quot; 라는 요구사항이 들어왔다면 이 부분을 잘 읽어봐야할 것같아요. 자세한 내용은 공식문서(<a href="https://logback.qos.ch/manual/appenders.html">링크</a>)를 참고해주세요!</p>
<h2 id="reference">Reference</h2>
<p><a href="https://www.baeldung.com/java-logging-intro">https://www.baeldung.com/java-logging-intro</a>
<a href="https://www.baeldung.com/slf4j-with-log4j2-logback">https://www.baeldung.com/slf4j-with-log4j2-logback</a>
<a href="https://logback.qos.ch/">https://logback.qos.ch/</a>
<a href="https://livenow14.tistory.com/63">https://livenow14.tistory.com/63</a>
<a href="https://inyl.github.io/programming/2017/05/05/slf4j.html">https://inyl.github.io/programming/2017/05/05/slf4j.html</a>
<a href="https://velog.io/@stella6767/logback-spring.xml-%EC%84%A4%EC%A0%95%EB%B0%A9%EB%B2%95">https://velog.io/@stella6767/logback-spring.xml-%EC%84%A4%EC%A0%95%EB%B0%A9%EB%B2%95</a>
<a href="https://dadadamarine.github.io/java/spring/2019/05/01/spring-logging-xml.html">https://dadadamarine.github.io/java/spring/2019/05/01/spring-logging-xml.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SLF4J 이해하기 2탄 - Logging이란?]]></title>
            <link>https://velog.io/@mindfulness_22/slf4j-logging-2</link>
            <guid>https://velog.io/@mindfulness_22/slf4j-logging-2</guid>
            <pubDate>Fri, 22 Jul 2022 02:20:02 GMT</pubDate>
            <description><![CDATA[<p>프로그램 개발이나 운영 시 발생하는 문제점을 추적하거나 운영 상태를 모니터링하기 위해 작성하는 텍스트를 로그라고 해요. 그리고 이를 남기도록 시스템을 만드는 것을 로깅이라고 하죠.</p>
<p>운영 중인 웹 어플리케이션에 문제가 발생했을 경우, 그 문제의 원인을 파악하려면 문제가 발생했을 때 당시의 정보가 필요해요. 그러기 위해서 예외가 발생하거나, 중요 기능이 실행되는 부분에 적절한 로그를 남기는 것이고 이는 빠르고 효율적인 문제 해결을 위해서 굉장히 중요한 과정이겠죠?</p>
<p>또한, logback 공식 사이트에서는 로깅의 비중이 코드의 4% 정도를 차지한다고 해요. 코드 수 자체만봤을 때도 상당한 비중을 차지하고 있다고 할 수 있겠네요. </p>
<p>근데 이런 의문이 들 수가 있어요. </p>
<h3 id="왜-systemoutprintln-은-안-될까">왜 System.out.println() 은 안 될까?</h3>
<p>그러게요. 왜 안될까요? 누구든 개발언어를 처음 배울 때, System.out.println(); 혹은 console.log(), print() 와 같이 Console 창에 원하는 내용을 찍어보게 될 거에요. 그리고 그 때부터 로깅을 정확히 이해하기 전까지 System.out.println()을 통해 직접 짠 로직이 잘 동작하는지를 확인할 거에요. 물론 저도 그랬고요. </p>
<p>하지만 이건 로그를 남기는 적절한 방식이 아니에요. 
왜냐하면 로그는 운영 중인 웹 애플리케이션에 문제가 생겼을 때, 문제 해결을 위해 남기는 당시의 기록인데 <strong>그렇게 쓰기에 System.out은 충분한 정보를 담고 있지 않아요</strong>. 
또한, 아래를 보면 println을 찍을 때 <strong>synchronized 라는 단어가 붙어있는 것을 볼 수가 있어요. 즉, System.out을 처리하는동안 다른 Thread가 Block 이 걸리게 되고, 이러한 현상이 많아진다면 성능저하의 원인이 될 수 있다는 말이죠.</strong></p>
<pre><code>System.out.println();
---------------------------------------------------
public void println() {
        newLine();
    }
---------------------------------------------------
private void newLine() {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.newLine();
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush)
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }</code></pre><h3 id="로그-레벨">로그 레벨</h3>
<p>Logging Framework 중 Logback은 5단계의 로그 레벨을 가져요. 
심각도 수준은 Error &gt; Warn &gt; Info &gt; Debug &gt; Trace 순이에요.</p>
<ul>
<li><p>Error : 예상치 못한 심각한 문제일 경우. 즉시 조치가 필요한 레벨.</p>
</li>
<li><p>Warn : 에러는 아님. 당장의 서비스 운영에는 영향이 없으나, 주의가 필요.</p>
</li>
<li><p>Info : 운영에 참고할만한 사항.</p>
</li>
<li><p>Debug : 개발 단계에서 사용. 일반 정보를 상세하게 나타낼 때 사용.</p>
</li>
<li><p>Trace : 경로 추적을 위해 사용. 개발 단계에서 사용.</p>
</li>
</ul>
<p>그럼, 다음 시간에는 slf4j와 logback에 대해 알아보도록 할게요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SLF4J 이해하기 1탄 - Facade Pattern]]></title>
            <link>https://velog.io/@mindfulness_22/slf4j-facade-pattern-1</link>
            <guid>https://velog.io/@mindfulness_22/slf4j-facade-pattern-1</guid>
            <pubDate>Fri, 22 Jul 2022 02:13:37 GMT</pubDate>
            <description><![CDATA[<h2 id="facade-pattern">Facade Pattern</h2>
<aside>
💡 The facade pattern is a software-design pattern commonly used in object-oriented programming. Analogous to a facade in architecture, a facade is an object that serves as a front-facing interface masking more complex underlying or structural code. - wikipedia
</aside>

<p>퍼사드 패턴은 객체 지향 프로그래밍에서 자주 사용되는 소프트웨어 디자인 패턴이에요. 건축에서의 facade(건물의 정면)와 유사하게, facade(퍼사드)는 내부적으로 혹은 구조적으로 더 복잡한 코드를 가려주는 상위 수준의 인터페이스의 역할을 하는 객체라고 하네요.</p>
<p>저는 디자인 패턴을 공부할 때는, 반드시 “이 디자인 패턴은 어떤 문제를 해결하기 위해 등장했을까?” 라는 의문을 던져야 한다고 생각해요. 이 고민 없이 단순히 코드와 도식만 본다면, 시간이 지나 잘 기억나지도 않을 뿐더러 해당 디자인 패턴에 맞춰 구현된 코드를 봐도 눈에 잘 들어오지도 않더라고요. 그러면 이 퍼사드 패턴은 어떤 문제를 해결하기 위해 등장하게 되었을까요?</p>
<p>첫번째로, 퍼사드 패턴은 시스템의 복잡성을 줄이고 사용성을 높이기 위해서 등장했어요. 어떻게 복잡성을 줄이고 사용성을 높였는지 이해하기 위해서는 아래 그림을 보는게 좋을 것 같아요. 퍼사드 디자인 패턴은 하나의 시스템을 서브 시스템들의 조합으로 구성하고 있는데요.</p>
<p><img src="https://velog.velcdn.com/images/mindfulness_22/post/72438850-8f2d-430b-96cb-713db58186c2/image.png" alt=""></p>
<ul>
<li>Client : 클라이언트는 서브 시스템을 알지 못해요. 단지 Facade를 호출할 뿐이죠.</li>
<li>Facade : 퍼사드는 클라이언트의 요청에 맞는 서브 시스템이 무엇인지 알고 있어요. 그래서 클라이언트의 요청을 적절한 서브 시스템에 전달하죠.</li>
<li>Subsystem : 서브 시스템들은 또 각자의 기능을 수행할 뿐, 서로서로 알지 못해요. 또 Facade 객체에 대해서도 알지 못 하고 오직 facade에 의해서 사용될 뿐이죠.</li>
</ul>
<p>이렇게 여러 서브 시스템이 복잡하게 얽히면서 구현한 로직을, facade와 서브 시스템으로 분리하게 되면 복잡성을 확연히 줄일 수 있게 돼요. 그리고 클라이언트 입장에서는 훨씬 시스템을 사용하기가 쉬워지죠. 그리고 퍼사드를 사용함으로써 기존에 없던 facade-subsystem 간의 계층도 생기게 되었어요. </p>
<p>또 퍼사드 패턴을 적용하면 서브 시스템 클래스에서 변화가 생겨도 클라이언트 코드에 영향이 가지 않기 때문에 종속성을 낮출 수가 있게 돼요. 즉 퍼사드 패턴의 핵심은 클래스 간 상호작용의 복합도를 낮추는 데 있다고 말할 수 있는거죠!</p>
<h3 id="facade-vs-adapter">Facade vs Adapter</h3>
<p>(비교정리 : Coming soon)</p>
<h3 id="facade-vs-front-controller">Facade vs Front Controller</h3>
<p>퍼사드 패턴의 핵심은 내부 시스템의 복잡도를 감추고 상호작용할 더 단순한 메소드를 제공하는 것이에요. 다시 말해, <strong>퍼사드는 클라이언트의 요청을 듣고 서브 시스템을 적절히 이용하여 요청을 들어주는 이외의 로직을 담고 있어선 안 되는 거죠.</strong></p>
<p>반면, 컨트롤러는 컨트롤러 자체가 자신만의 로직을 가질 수가 있어요. Servlet에서 오는 요청을 바로 처리할 수도 있고, 서비스 단으로 이어주는 요청을 처리할 수도 있죠. 프론트 컨트롤러 패턴은 퍼사드 패턴처럼 앞 단에서 클라이언트의 요청을 받아들인다는 점에서 구조적으로 비슷해 보일 수는 있어요. 하지만 그 등장 목적은 내부 시스템의 복잡도를 감추기 위함이 아니에요. <strong>모든 요청을 먼저 받는 컨트롤러를 둠으로써</strong> 모든 요청에 <strong>공통적으로 처리해야하는 로직을 효과적으로 처리하는데 있죠.</strong></p>
<p>그럼, 다음 시간에는 본격적으로 Logging에 대해 알아보도록 할게요!</p>
<h2 id="reference">Reference</h2>
<p><a href="https://en.wikipedia.org/wiki/Facade_pattern">https://en.wikipedia.org/wiki/Facade_pattern</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TCP Congestion Control]]></title>
            <link>https://velog.io/@mindfulness_22/TCP-Congestion-Control</link>
            <guid>https://velog.io/@mindfulness_22/TCP-Congestion-Control</guid>
            <pubDate>Thu, 23 Jun 2022 09:43:04 GMT</pubDate>
            <description><![CDATA[<p>네트워크의 혼잡 원인을 해결하기 위해서는 네트워크의 혼잡을 일으키는 송신자를 억제하는 매커니즘이 필요하다. 
네트워크의 혼잡이 데이터 송신과 수신속도 그리고 효율에 어떻게 영향을 미칠 수 있는지 알기 위해 세 가지 시나리오를 살펴본다.</p>
<h3 id="시나리오-1-2개의-송신자--무한-버퍼를-갖는-1개의-라우터">시나리오 1. 2개의 송신자 + 무한 버퍼를 갖는 1개의 라우터</h3>
<p>비현실적이지만, 라우터가 무한 버퍼를 갖는다고 가정해본다. 그리고 2개의 호스트는 1개의 라우터를 사용하기 위해서 라우터의 Capacity(R)를 나누어서 사용하게 된다. 그러면 연결당 처리량이 R/2 사이일 경우 수신자 측의 처리량은 송신자의 전송률과 같다. 하지만 전송량이 R/2가 넘어갈 경우 처리량은 R/2를 넘을 수 없다. 이 시나리오에서 혼잡 네트워크의 비용은 패킷의 도착률이 링크 용량에 근접함에 따라서 큐잉 지연이 커지게 되는 점이다.</p>
<h3 id="시나리오-2-2개의-송신자--유한버퍼를-가진-1개의-라우터">시나리오 2. 2개의 송신자 + 유한버퍼를 가진 1개의 라우터</h3>
<p>이 경우 문제는 송신자는 버퍼 오버 플로우 때문에 버려진 패킷을 보상하기 위해 재전송을 해야 한다. 즉 처리량은 똑같은데 전송량이 많아지게 되는 것이다. 심지어 버려지지 않았는데 송신자 측에서 일찍 타임아웃이 되어버려 재전송을 하게 되면 큐에서 지연된 패킷 + 재전송된 패킷 두 개가 모두 전달되는 비효율적인 경우도 발생한다.
이 시나리오에서 혼잡 네트워크의 비용은 라우터가 패킷의 불필요한 재전송을 함으로 인해 링크 대역폭을 더 사용하게 되는 것이다.</p>
<h3 id="시나리오-3-4개의-송신자--유한버퍼를-가지는-4개의-라우터">시나리오 3. 4개의 송신자 + 유한버퍼를 가지는 4개의 라우터</h3>
<p>호스트가 다른 호스트로 데이터를 보내기 위해 2개의 라우터를 거쳐야 하는 경우, 트래픽이 과부하될 수록 B Host-&gt;D Host 전송 데이터를 처리하느라 A Host -&gt;C Host 전송 데이터는 라우터에 들어가지 못하고 패킷이 전부 버려지게 된다. 이 경우, 버려지는 지점까지 패킷을 전송하는데 사용된 상위 라우터에서 사용된 전송 용량은 모두 헛된 것이 된다.</p>
<p>TCP의 특징 3가지는 바로 1. Reliability 2. Flow Control 3. Congestion Control이다. TCP는 다른 호스트에서 동작하는 두 프로세스 사이의 신뢰적인 전송 서비스를 제공한다. </p>
<h3 id="1-tcp-송신자는-전송-트래픽-전송률을-어떻게-제한하는가">1. TCP 송신자는 전송 트래픽 전송률을 어떻게 제한하는가?</h3>
<p>TCP 연결의 양 끝 호스트들은 송신버퍼, 수신버퍼로 구성되고 송신 측에서는 혼잡윈도우 (Congestion Window)를 기록한다. 그리고 이 cwnd를 통해 네트워크로 트래픽을 전달할 수 있는 비율을 제한한다. 송신 데이터의 양은 cwnd와 rwnd의 최솟값을 넘어서는 안 된다.</p>
<h3 id="2-tcp-송신자는-자신과-목적지-사이-경로혼잡을-어떻게-감지하는가">2. TCP 송신자는 자신과 목적지 사이 경로혼잡을 어떻게 감지하는가?</h3>
<p>크게 두가지가 있다. 1. 타임아웃 발생 2. 수신자로부터 3개의 중복된 ACK들의 수신이 발생했을 때 감지할 수 있다. 하지만 타임아웃 발생이 네트워크 Congestion의 정도가 더 높다고 예상해볼 수 있다. 왜냐하면 2번의 경우에는 그 뒷 부분의 segment들은 다 도착한 경우겠지만 1번의 경우에는 아예 그 segment 부터 다 막혀있다고 볼 수 있기 때문이다.</p>
<h3 id="3-송신자는-종단간의-혼잡에-따른-송신율-변화를-위해-어떤-알고리즘을-사용하는가">3. 송신자는 종단간의 혼잡에 따른 송신율 변화를 위해 어떤 알고리즘을 사용하는가?</h3>
<p>이 알고리즘은 세 가지 중요한 구성요소를 가지는데, 1. 슬로우 스타트 2. 혼잡 회피 3. 빠른 회복이 있다.</p>
<ul>
<li><p>슬로우 스타트
TCP 연결이 시작될 때, 가장 처음 TCP 전송률은 1MSS에서 시작해서 지수적으로 증가하게 된다. 그리고 지수적 증가가 끝나는 시점은 혼잡이 발생되는 경우이다. 그 때 TCP 송신자는 cwnd의 값을 1로 하고 ssthresh(슬로우 스타트 임계점)의 값을 cwnd/2로 정한다. 그리고 다시 값을 지수적으로 증가시키고 임계점에 도달하면 혼잡 회피 모드로 들어간다. </p>
</li>
<li><p>혼잡 회피
여기서 매 RTT마다 cwnd의 값을 두배로 하기 보다 TCP는 조금 더 보수적인 방법을 채택해서 매 RTT마다 하나의 MSS만큼 cwnd를 증가시킨다. 그리고 타임아웃이 발생하면 시작점은 다시 1MSS로 떨어뜨리고 임계점은 기존 cwnd의 1/2로 줄인다. 그리고 나서 빠른 회복 상태로 들어가게 된다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA에서 엔티티 매핑을 하는 방법(필드와 컬럼 매핑까지)]]></title>
            <link>https://velog.io/@mindfulness_22/JPA%EC%97%90%EC%84%9C-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91%EC%9D%84-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%ED%95%84%EB%93%9C%EC%99%80-%EC%BB%AC%EB%9F%BC-%EB%A7%A4%ED%95%91%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@mindfulness_22/JPA%EC%97%90%EC%84%9C-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91%EC%9D%84-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%ED%95%84%EB%93%9C%EC%99%80-%EC%BB%AC%EB%9F%BC-%EB%A7%A4%ED%95%91%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 30 May 2022 13:26:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>자바 ORM 표준 JPA 프로그래밍(책),
자바 ORM 표준 JPA 프로그래밍 - 기본편(인프런강의)
을 공부하면서 정리하는 글입니다.</p>
</blockquote>
<p>JPA에서 객체와 테이블을 매핑하는 방법은 두가지가 있다. 첫번째는 XML을 사용하는 것 두번째는 어노테이션을 사용하는 것이다. 이번 글에서는 어노테이션 사용법에 대해서만 알아볼 것이다.</p>
<h2 id="entity">@Entity</h2>
<p>테이블과 매핑할 클래스는 반드시 엔티티 어노테이션을 붙여야 한다. 여기서 주의해야 할 점은, JPA는 엔티티 객체를 생성할 때 기본 생성자를 사용한다는 것이다. 만약에 생성자가 하나도 없을 경우에는 자동으로 만들어주기도 하지만, *<em>기본적으로 사용되는 생성자가 한개 이상 있을 경우에는 기본 생성자를 직접, 수동으로 만들어주어야 한다. *</em></p>
<h2 id="table">@Table</h2>
<p>생략해도 되는 어노테이션이다. 하지만 테이블 어노테이션을 생략하게 되면 엔티티명이 테이블명으로 그대로 사용된다.</p>
<h2 id="엔티티만-만들면-스키마는-자동으로-생성된다고">엔티티만 만들면 스키마는 자동으로 생성된다고?</h2>
<p>persistence.xml 에서 hibernate.hbm2ddl.auto 속성을 활용하면 프로그램을 실행할 때 자동으로 데이터베이스의 스키마가 생성되도록 할 수 있다. 코드는 아래와 같다.</p>
<pre><code class="language-java">&lt;property name=&quot;hibernate.hbm2ddl.auto&quot; value=&quot;create&quot; /&gt;</code></pre>
<p>여기서 속성은 create, create-drop, update, validate, none이 있다. create 와 create-drop은 둘 다 생성시점에 drop을 하고 create를 하는 점은 동일하다. 하지만  create-drop의 경우에는 프로그램 종료 시 다시 한번 drop을 하는 차이점이 있다. <strong>validate와 none의 경우에는 실질적으로 데이터 베이스를 변경하지 않기 때문에 운영서버에서 사용해도 되지만, 나머지 특히 create가 포함된 속성은 개발 초기 단계가 아니라면 절대 사용해서는 안 된다.</strong></p>
<h2 id="기본-키를-매핑하는-다양한-방법">기본 키를 매핑하는 다양한 방법</h2>
<p>JPA가 제공하는 데이터베이스 기본 키 생성전략은 총 2가지이다.</p>
<ol>
<li>직접 할당</li>
<li>자동 생성<ul>
<li>IDENTITY</li>
<li>SEQUENCE</li>
<li>TABLE</li>
</ul>
</li>
</ol>
<p>직접 할당의 경우 엔티티를 em.persist()로 영속시키기 전에 기본키 값을 직접 할당해주어야 한다. 그리고 이 Id값이 없을 경우에는 예외가 발생하기 때문에 실무에서 쓰기 불편하다. </p>
<h3 id="identity-전략">IDENTITY 전략</h3>
<p><strong>기본 키 생성을 데이터베이스에 위임하는 전략이다.</strong> 주로 MySQL, PostgreSQL 등에 사용한다. 예를 들어, MySQL에서 IDENTITY 전략을 사용하게 되면 AUTO_INCREMENT로 기본 키 생성 전략이 취해진다. 사용방법은 아래와 같다.</p>
<pre><code class="language-java">@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long Id;</code></pre>
<p><strong>여기서 가장 중요하게 기억해야 할 점은 트랜잭션을 지원하는 쓰기 지연이 불가능하다는 점이다.</strong> 이유는 이렇다. 객체를 생성하고 em.persist()를 호출하게 되면 엔티티가 영속상태가 되는데 이 때 반드시 식별자가 필요하다. 하지만 IDENTITY 전략을 쓰고 있을 경우, 식별자는 DB에 저장되는 시점에 생성이 된다. 이 문제를 해결하기 위해 JPA는 IDENTITY 전략을 사용할 때, em.persist()를 호출하는 즉시 INSERT 쿼리가 나가게 해 둔 것이다. </p>
<h2 id="기본키는-어떤-기준으로-선택해야할까">기본키는 어떤 기준으로 선택해야할까?</h2>
<p>기본키는 기본적으로 1. NULL이 되지 않을 것, 2. 유일할 것, 3. 변하지 않을 것. 이 세 가지 기준을 만족해야 한다. 그리고 이 기준을 만족하는 것 중에 선택할 수 있는 것이 크게 2가지가 있다. </p>
<p>첫째는 자연키이고 둘째는 대리키이다. 하지만 실무에서는 대리키를 사용하는 것이 장기적인 관점에서 보았을 때 훨씬 이득이다. 자연키의 경우 비즈니스적으로 의미가 있기 때문에, 추후에 변동이 되는 로직이 생기거나 법, 정책 상의 이유로 기업에서 저장할 수 없게 되는 등의 조치가 취해질 수가 있다. 그때 가서 수십, 수백개의 테이블과 이미 매핑되어 있는 키 값을 변경하는 것은 대단히 골치아픈 일이 될 수 밖에 없다.</p>
<h2 id="컬럼과-필드를-매핑해주는-어노테이션">컬럼과 필드를 매핑해주는 어노테이션</h2>
<h3 id="column">@Column</h3>
<p>컬럼 어노테이션을 쓰면 객체 필드를 테이블 컬럼에 매핑해준다. 엔티티 어노테이션을 붙인 클래스의 각 필드마다 컬럼 어노테이션을 붙이다 보니 이런 생각이 들었다. 컬럼 어노테이션을 안 붙이면 어떻게 될까? 그러면 컬럼 속성의 기본값이 적용이 되고, 물론 테이블 컬럼에는 생성이 된다. 하지만 여기서 <strong>주의해야할 점</strong>이 있다. 예를 들어 데이터 타입이 int인 경우, 자바 기본 타입이기 때문에 null을 입력할 수가 없다. 하지만 데이터 타입이 Integer인 경우 null값이 허용이 된다. 그런데 컬럼 어노테이션의 경우 nullable=true가 기본값이다. 그렇기 때문에 데이터 타입이 int인 필드에 컬럼 어노테이션을 생략하면 기본값이 그대로 적용되어 문제가 발생할 수 있다.(객체와 테이블 속성이 다르다) 그래서 이러한 경우에는 꼭 어노테이션을 달아주고 nullable=false로 지정을 해주는 것이 안전하다.</p>
<h3 id="enumerated">@Enumerated</h3>
<p>자바의 enum 타입을 매핑하기 위해 붙이는 어노테이션이다. Enumerated 어노테이션에는 두가지 속성이 있다. 첫째는 EnumType.ORDINAL 둘째는 EnumType.STRING 이다. 하지만 앞의 ORDINAL 속성은 사용하지 않는 것이 좋다. 왜냐하면 이는 정의된 순서대로 0부터 숫자로 표시가 되는데 Enum이 더 추가되고 순서가 변경될 경우 기존의 값들과 뒤섞여서, 추후 문제가 생길 수 있다. 예를 들면 이렇다. </p>
<pre><code class="language-java">public enum UserType {
        ADMIN, USER
}</code></pre>
<p>이렇게 고객의 종류가 딱 2개 즉 관리자, 일반 고객으로만 나뉘어서 운영하는 초기 서비스가 있다고 치자. 그러면 고객 타입이 일반 유저일 때, DB에는 그 값이 1로 들어가 있을 것이다. 하지만 서비스의 규모가 점점 커져서 고객 등급이 세분화되고 그 등급에 맞는 혜택과 권한이 주어질 경우 ENUM 값이 추가되게 된다. ADMIN보다 권한은 부족하지만 일반 유저보다는 권한이 큰 LEADER라는 등급이 생겼다고 했을 때 ENUM 순서를 ADMIN, LEADER, USER로 할 경우 이제 USER 값은 2로 저장이 되고 LEADER가 1로 저장이 되는 사태가 발생한다. 이러한 사고를 미연에 방지하기 위하여, <strong>EnumType은 꼭 String으로 저장하자.</strong></p>
<h3 id="transient">@Transient</h3>
<p>이 어노테이션을 붙인 필드는 매핑을 하지 않는다. 데이터베이스에 저장하지도, 조회하지도 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 설정과 애플리케이션 개발 시작]]></title>
            <link>https://velog.io/@mindfulness_22/JPA-%EC%84%A4%EC%A0%95%EA%B3%BC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@mindfulness_22/JPA-%EC%84%A4%EC%A0%95%EA%B3%BC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B0%9C%EB%B0%9C-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Fri, 27 May 2022 10:38:45 GMT</pubDate>
            <description><![CDATA[<h2 id="어떻게-jpa는-db-정보를-인식할까">어떻게 JPA는 DB 정보를 인식할까?</h2>
<p>JPA는 persistence.xml 을 사용해서 필요한 설정 정보를 관리한다. 이 설정 파일은 반드시 META-INF/persistence.xml 경로에 있어야 한다. </p>
<p>property 즉 속성부분에 적어줘야 하는 것은 필수 값과 옵션이 있다. </p>
<p>필수값</p>
<ul>
<li><p>JPA 표준 속성</p>
<ul>
<li><p>javax.persistence.jdbc.driver</p>
</li>
<li><p>javax.persistence.jdbc.user</p>
</li>
<li><p>javax.persistence.jdbc.driver.password</p>
</li>
<li><p>javax.persistence.jdbc.driver.url</p>
<p>이렇게 javax.persistence 로 시작하는 것은 JPA 표준 속성이다. 구현체에 영향을 받지 않는다.</p>
</li>
</ul>
</li>
<li><p>하이버네이트 속성</p>
<ul>
<li><p>hibernate.dialect</p>
<p>이것은 하이버네이트에서만 사용할 수 있는 하이버네이트 전용 속성이다. 앞선 글에서 말했던 것처럼 JPA는 특정 데이터베이스 기술에 종속되지 않는다. 그래서 데이터 베이스 교체가 굉장히 단순하다. 그리고 그 방법은 바로 이 방언 설정으로 손쉽게 교체 가능하다.</p>
<p><code>&lt;property name=”hibernate.dialect” value = “org.hibernate.dialect.H2Dialect” /&gt;</code></p>
<p>위 처럼 설정하면 H2 데이터베이스를 쓰겠다는 뜻이고 MySQL은 H2Dialect 부분을 MySQL5InnoDBDialect로 변경해주면 된다.</p>
</li>
</ul>
</li>
</ul>
<p>옵션</p>
<ul>
<li>hibernate.show_sql : 하이버네이트가 실행한 SQL 출력</li>
<li>hibernate.format_sql : SQL 출력시 보기좋게 정렬</li>
<li>hibernate.use_sql_comments : 주석도 함께 출력</li>
</ul>
<h2 id="개발-시작-전-entitymanagerfactory와-entitymanager-이해하기">개발 시작 전, EntityManagerFactory와 EntityManager 이해하기</h2>
<h3 id="엔티티-매니저-팩토리">엔티티 매니저 팩토리</h3>
<p>앞선 persistence.xml 파일에서 <persistence-unit name=”jpabook”>으로 설정을 해두었다. 이 jpabook 이라는 것은 persistence unit의 이름이므로 원하는 것으로 등록하면 된다. 책에서는 jpabook이라고 되어 있어 따라해보았다.</p>
<p>그래서 이 이름으로 우선 엔티티 매니저 팩토리를 생성할 수 있다. 그리고 그 엔티티 매니저 팩토리를 통해서 엔티티 매니저를 생성할 수 있다.</p>
<h3 id="엔티티-매니저-팩토리-생성">엔티티 매니저 팩토리 생성</h3>
<p><code>EntityManagerFactory emf = Persistence.createEntityManagerFactory(”jpabook”);</code></p>
<p>(중요) 엔티티 매니저 팩토리 생성은 비용이 크기 때문에 애플리케이션 전체에 한번 생성하고 공유해서 사용한다. </p>
<h3 id="엔티티-매니저-생성">엔티티 매니저 생성</h3>
<p><code>EntityManager em = emf.createEntityManager();</code></p>
<p>엔티티 매니저를 사용해서 엔티티를 데이터 베이스에 등록/수정/삭제/조회할 수 있다. 대부분의 JPA 기능은 이 엔티티 매니저를 통해 이루어지는데, 데이터베이스 커넥션과 밀접한 관계가 있기 때문에, 스레드 간에 공유하거나 재사용해서는 안 된다.</p>
<p>그래서 크게 보면 전체 순서는 이렇게 된다.</p>
<ol>
<li>엔티티 매니저 팩토리 생성</li>
<li>엔티티 매니저 생성</li>
<li>트랜잭션 시작</li>
<li>비즈니스 로직 실행</li>
<li>트랜잭션 커밋 or 실패 시 롤백 </li>
<li>엔티티 매니저 종료</li>
<li>엔티티 매니저 팩토리 종료</li>
</ol>
<h2 id="jpa로만-개발하기-가능할까">JPA로만 개발하기 가능할까?</h2>
<p>대답은 No이다. JPA보다 SQL을 직접 작성하거나 혹은 JPQL을 쓰는 것이 더 낫거나 혹은 유일한 방법일 때도 있다. </p>
<p>예를 들어 데이터를 검색할 때, 애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 검색 조건이 포함된 SQL을 사용해야 한다. 그래서 JPA는 객체 지향 쿼리 언어인 JPQL을 제공한다. JPQL은 테이블이 어떻게 생긴지 모르고, 엔티티 객체를 대상으로 쿼리를 한다. </p>
<p>JPQL을 사용하는 코드는 아래와 같다.</p>
<p><code>em.createQuery(JPQL, 리턴 타입).getResultList();</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동적 파라미터화는 무엇인가]]></title>
            <link>https://velog.io/@mindfulness_22/%EB%8F%99%EC%A0%81-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%ED%99%94%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@mindfulness_22/%EB%8F%99%EC%A0%81-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%ED%99%94%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Thu, 26 May 2022 07:49:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Modern Java in Action(책)을 공부하면서 정리하는 글입니다.</p>
</blockquote>
<p>시시각각 변하는 사용자의 요구에도 엔지니어링적인 비용을 최소화할 수 있다면, 정말 좋을 것이다. 이렇게 되기 위해서는 첫번째, 기능의 구현 자체가 간결해야 하며, 두번째, 유지보수가 쉬워야 한다.</p>
<p>그래서 동적 파라미터화가 등장했다.</p>
<h2 id="동적-파라미터화-너는-누구야">동적 파라미터화, 너는 누구야?</h2>
<p>동적 파라미터화는 아직 어떻게 실행할지 결정하지 않은 코드 블록을 의미한다. 코드 블록의 실행은 나중에 이루어지겠지만, 코드 블록 자체는 미리 만들어놓을 수 있다. 그리고 이 코드 블록은 메소드의 인수로 쓰일 수 있다.</p>
<p>원래는 동적 파라미터화를 추가하기 위해서 써야하는 코드가 많았다. 하지만 자바 8에서는 람다를 통해 이를 훨씬 간결하게 해결했다.</p>
<p>지금부터 메서드의 동작을 파라미터화한 과정을 코드로 정리해보겠다.</p>
<pre><code class="language-java">public interface ApplePredicate { 
    boolean test (Apple apple);
}
public static List&lt;Apple&gt; filterApples(List&lt;Apple&gt; inventory, ApplePredicate p) {
    List&lt;Apple&gt; result = new ArrayList&lt;&gt;();
    for (Apple apple : inventory) {
        if(p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}</code></pre>
<p><code>ApplePredicate</code> 라는 이름의 인터페이스를 만들어서 <code>test</code>  라는 이름의 메소드를 정의했으니 해당 인터페이스를 상속한 클래스를 자유자재로 만들어서 <code>test</code> 메소드를 오버라이딩할 수 있다. 그리고 <code>filterApples</code>의 메소드에 그를 동작 파라미터화 해서 인자로 줄 수 있다. </p>
<pre><code class="language-java">public class AppleLightWeightRedColor implements ApplePredicate {
    public boolean test(Apple apple) {
        return RED.equals(apple.getColor()) 
                &amp;&amp; apple.getWeight() &lt; 150;
    }
}</code></pre>
<p>예를 들어 이렇게 만들어서 위에서 만든 사과 필터 메소드의 인자로 전달해줄 수가 있게 되는 것이다. <code>filterApples</code> 메서드 내부에서 컬렉션을 반복하는 로직과 컬렉션의 각 요소에 적용할 동작을 분리할 수 있다는 점에서 소프트웨어 엔지니어링적인  이점을 지닌다. 그리고 이것은 동작 파라미터화의 가장 큰 장점이다.</p>
<h2 id="인터페이스-생성-→-클래스-생성-→-오버라이딩-과정을-좀-더-간소화할-수는-없을까">인터페이스 생성 → 클래스 생성 → 오버라이딩… 과정을 좀 더 간소화할 수는 없을까?</h2>
<p>그래서 자바는 클래스 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스 기법을 제공한다. 익명 클래스를 사용하면 위에서 적은 코드를 이렇게 단순화할 수 있다.</p>
<pre><code class="language-java">List&lt;Apple&gt; redApples = filterApples(inventory, new ApplePredicate() {
            public boolean test(Apple apple) {
                return RED.equals(apple.getColor());
        }
});</code></pre>
<p>새롭게 인터페이스를 상속받은 클래스를 만들 필요가 없어서 코드의 줄은 조금 줄었으나 여전히 보기가 불편한 점이 있다. 하지만 동작 파라미터화의 경우 분명한 장점이 있다. 그래서 자바 8은 람다식을 통해 동작 파라미터화의 사용을 좀 더 편리하고, 간결하고 또 직관적으로 만들어주었다.</p>
<h2 id="람다식-한번-써보자">람다식, 한번 써보자!</h2>
<pre><code class="language-java">List&lt;Apple&gt; redApples = filterApples(inventory,
        (Apple apple) -&gt; RED.equals(apple.getColor()));</code></pre>
<p>아래는 Comparator를 구현해서 메서드의 동작을 다양화한 예시이다.</p>
<pre><code class="language-java">public interface Comparator&lt;T&gt; {
        int compare(T o1, T o2);
}

inventory.sort(new Comparator&lt;Apple&gt;() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
        }
        });

// 람다식으로 간단하게 표현하기
inventory.sort(
(Apple a1, Apple a2) -&gt; a2.getWeight().compareTo(a2.getWeight()));</code></pre>
<h2 id="정리해보자">정리해보자.</h2>
<ul>
<li>동작 파리미터화를 쓰면, 메서드의 인수로 코드를 전달함으로써 내부적으로 더 다양한 동작을 가능하게 한다.</li>
<li>동작 파라미터화를 사용하게 되면, 요구사항에 더 잘 대응하는 유연한 코드를 짤 수가 있다.</li>
<li>동작을 메소드의 인수로 전달하는 코드 전달 기법에는 익명 클래스, 람다식 등이 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체는 관계형 DB와 태생부터 다르기 때문에…]]></title>
            <link>https://velog.io/@mindfulness_22/%EA%B0%9D%EC%B2%B4%EB%8A%94-%EA%B4%80%EA%B3%84%ED%98%95-DB%EC%99%80-%ED%83%9C%EC%83%9D%EB%B6%80%ED%84%B0-%EB%8B%A4%EB%A5%B4%EA%B8%B0-%EB%95%8C%EB%AC%B8%EC%97%90</link>
            <guid>https://velog.io/@mindfulness_22/%EA%B0%9D%EC%B2%B4%EB%8A%94-%EA%B4%80%EA%B3%84%ED%98%95-DB%EC%99%80-%ED%83%9C%EC%83%9D%EB%B6%80%ED%84%B0-%EB%8B%A4%EB%A5%B4%EA%B8%B0-%EB%95%8C%EB%AC%B8%EC%97%90</guid>
            <pubDate>Wed, 25 May 2022 02:20:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>자바 ORM 표준 JPA 프로그래밍(책),
자바 ORM 표준 JPA 프로그래밍 - 기본편(인프런강의)
을 공부하면서 정리하는 글입니다.</p>
</blockquote>
<p>객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 다양한 장치들을 통해 어플리케이션의 복잡도를 낮춘다. 반면 관계형 데이터 베이스는 위 장치와 같은 개념이 없고, 테이블, 열, 행, 속성 등을 통해 데이터를 저장할 뿐이다. 이렇게 객체와 관계형 데이터 베이스는 각각 완전히 다른 목적과 장점을 지닌 패러다임을 기반으로 등장했기 때문에, 그 중간에서 개발자들은 이러한 불일치 문제를 해결하기 위해 많은 노력을 했다.</p>
<p>그 가운데 JPA는 이러한 창의적인 발상으로부터 등장하였다. &#39;자바 컬렉션 예를 들어, 리스트에 값을 추가, 탐색, 삭제하는 것처럼 그렇게 쉽고 자연스럽게 데이터의 CRUD를 실행할 수 있다면 얼마나 좋을까?&#39; 그리고 JPA는 객체지향 프로그래밍 패러다임에 기반한 엔티티 객체와 관계형 데이터 베이스의 테이블 간의 간극을 자바 컬렉션에 데이터를 저장하는 방식을 차용하여 해결했다.</p>
<p>이를 구체적인 문제와 해결방법을 통해 알아보자.</p>
<h2 id="패러다임-불일치로-인해-발생하는-문제는-무엇인가">패러다임 불일치로 인해 발생하는 문제는 무엇인가</h2>
<h4 id="1-객체는-상속이라는-기능이-있으나-테이블은-상속이라는-기능이-없다">1. 객체는 상속이라는 기능이 있으나 테이블은 상속이라는 기능이 없다.</h4>
<p>그렇기 때문에 Item이라는 클래스를 상속받은 Book이 있다고 했을 때, Book 객체를 저장하기 위해서는 Item 과 Book 테이블 각각에 해당 인스턴스를 저장해야 한다. 즉 개발자 입장에서는 두 줄의 코드를 작성해야 하는 것이다. </p>
<p>JPA의 문제해결방식 : 아래 코드 참고</p>
<p><code>jpa.persist(book);</code> → <code>INSERT INTO BOOK</code>, <code>INSERT INTO ITEM</code>
<code>jpa.find(Book.class, book.getId);</code> → <code>SELECT I.*, B.* FROM ITEM I JOIN BOOK B ON I.ITEM_ID = B.ITEM_ID</code></p>
<h4 id="2-객체는-참조를-통해-다른-객체와-연관관계를-가지고-참조에-접근해서-연관된-객체를-조회한다">2. 객체는 참조를 통해 다른 객체와 연관관계를 가지고, 참조에 접근해서 연관된 객체를 조회한다.</h4>
<p>하지만 테이블은 외래키를 통해 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다. 그래서 동작하는 방식이 완전히 다른 것이, 외래키만 있으면 &#39;on Foreign key&#39; SQL 문을 추가해서 다른 테이블 조회가 가능하지만 객체는 참조가 있는 방향으로만 참조가 가능하고 그 반대는 불가능하다.(양방향 매핑은 결국 2개의 단방향 매핑이다. 라는 말과 관련이 있다.) </p>
<p>JPA의 문제해결방식 : 연관관계 매핑 후 해당 객체를 영속화</p>
<h4 id="3-객체-그래프-탐색이-불가능하다">3. 객체 그래프 탐색이 불가능하다.</h4>
<p>객체를 테이블이 데이터를 저장하는 방식에 맞추어 외래키처럼 특정 값 필드만 가지도록 설계를 해두면, 연관된 객체를 찾을 수가 없어 객체 지향의 특징을 완전히 잃게 된다.</p>
<p>JPA의 문제해결방식 : 객체 그래프 탐색</p>
<p>객체와 RDB는 처음부터 그 탄생 이유가 다르다. 하지만 JPA를 쓰면 그 간극을 메워줄 수 있다. 개발자로 하여금 SQL문을 반복적으로 작성하는 단순 노동이 아니라 그 시간에 설계, 테스트 코드 작성, 더 나은 코드 작성을 위한 고민을 할 수 있게 해준 JPA에 감사하며 JPA 숙련도를 높여 나가자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA란 무엇인가]]></title>
            <link>https://velog.io/@mindfulness_22/JPA%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@mindfulness_22/JPA%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 25 May 2022 01:29:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>자바 ORM 표준 JPA 프로그래밍(책),
자바 ORM 표준 JPA 프로그래밍 - 기본편(인프런강의)
을 공부하면서 정리하는 글입니다.</p>
</blockquote>
<p>JPA는 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스의 모음이다. JPA를 구현한 대표적인 오픈소스로는 Hibernate가 존재한다. 인터페이스이기 때문에 실제로는 Hibernate와 같이 JPA 구현체가 존재한다.</p>
<p>그렇다면 ORM이란 무엇인가?</p>
<p>ORM은 말그대로 객체(Object)와 관계형 데이터베이스(RDB)의 데이터를 자동으로 매핑(연결)해주는 것을 말한다. </p>
<h2 id="jpa는-왜-필요한가">JPA는 왜 필요한가?</h2>
<p>앞서 말했듯, JPA는 자바 진영의 ORM 표준이다. 그리고 ORM은 객체와 관계형 DB를 매핑해준다. 그렇기 때문에 JPA는 객체와 RDB 사이의 패러다임 불일치 문제를 해결해주는 강력한 역할을 한다. ORM 중에서도 아직 성숙단계에 이르지 못한 경우 단순히 자동매핑만을 해결해주는 경우도 많은데, Hibernate는 패러다임 불일치 문제를 거의 완벽히 해결해주고 있을만큼 성숙도가 높은 ORM이자 JPA 구현 기술이다.</p>
<p>하나의 어플리케이션을 만들고 운영하기 위해서는 데이터 베이스가 반드시 필요하고, 데이터를 관리하기 위해서는 SQL을 사용해야 한다. 자바 애플리케이션은 JDBC API를 사용해서 SQL을 데이터베이스에 전달하게 되는데, 그러면 개발자가 직접 SQL을 작성한 후, JDBC API를 이용해서 SQL을 실행하고 또 매핑 작업까지 일일이 해주어야 한다. 단순히 조회 뿐 아니라 삽입, 수정, 삭제까지 CRUD 작업을 위해 너무 많은 SQL과 JDBC API를 코드로 작성해야 하는 것이다. 하지만 JPA를 사용하게 되면 자바의 컬렉션을 사용하듯 SQL 문을 작성하지 않아도 되고 또 어떤 쿼리가 나갈지 예측하는 것도 쉽다.</p>
<p>또 실무에서 중요한 한가지는 RDB는 기능이 같아도 벤더마다 그 사용법이 다른 경우가 많은데, JPA는 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 도와주기 때문에 로컬과 운영, DB 결정 전, 후를 크게 고민하지 않고 단순히 DBdialect 변경만으로 손쉽게 연결 DB를 바꿀 수 있다.</p>
<h2 id="sql에-의존적인-개발은-왜-좋지-못한가">SQL에 의존적인 개발은 왜 좋지 못한가?</h2>
<p>비즈니스 로직 변경 시 반드시 SQL의 수정이 동반되고, 이 과정에서 개발자도 사람이기 때문에 실수가 발생할 수 밖에 없다. 만일 한개라도 쿼리 수정을 빠트린다면 예상했던 비즈니스 로직과 다르게 동작하는 문제가 발생할 수 밖에 없을 뿐더러, 이것보다 더 큰 문제는 개발자 입장에서 엔티티에 대한 신뢰를 갖고 개발하는 것 자체가 불가능하게 되는 것이다.</p>
<p>즉 요약하자면,</p>
<ul>
<li>진정한 의미의 계층 분할이 어렵다는 것</li>
<li>엔티티 신뢰 저하</li>
<li>SQL에 의존적인 개발 자체를 피할 수가 없다는 것</li>
</ul>
<p>이 문제점이 되겠다.</p>
<p>ORM은 객체지향 프로그래밍과 관계형 데이터베이스에 대한 이해가 탄탄할 때 더욱 쉽게 이해하고 실무에 적용할 만큼 활용할 수 있다. JPA에 대한 공부 뿐 아니라 Java와 SQL에 대한 공부도 꾸준히 열심히 하자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[예외처리는 왜 Custom해서 써야할까?]]></title>
            <link>https://velog.io/@mindfulness_22/%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%EB%8A%94-%EC%99%9C-Custom%ED%95%B4%EC%84%9C-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@mindfulness_22/%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC%EB%8A%94-%EC%99%9C-Custom%ED%95%B4%EC%84%9C-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Fri, 15 Apr 2022 06:36:14 GMT</pubDate>
            <description><![CDATA[<p>Java의 예외 클래스 구조를 살펴보면 모든 예외 클래스는 <strong>Throwable 클래스</strong>를 상속받고 있으며, Throwable은 최상위 클래스 Object의 자식 클래스이다. 그리고 Throwable을 상속받는 클래스는 Error와 Exception이 있는데, Error 같은 경우는 개발자가 예상하지 못한 시스템 레벨의 오류이기 때문에 미리 대비하여 처리하는 것은 어려운 부분이 있다. 
*<em>반면 Exception은 개발자가 로직을 추가하여 처리할 수 있다. *</em>
개발을 하면서 예외처리를 위한 Class를 생성할 때 RuntimeException을 상속받는 것을 볼 수 있는데 모든 Exception이 RuntimeException인 것은 아니고 우선 CheckedException과 UncheckedException으로 나뉜다.</p>
<h2 id="checked-exception-vs-unchecked-exception">Checked Exception VS Unchecked Exception</h2>
<h3 id="checked-exception">Checked Exception</h3>
<ul>
<li>반드시 예외처리를 해야 한다.</li>
<li>컴파일 단계에서 에러발생</li>
<li>예외 발생 시 트랜잭션 처리(Roll-back X)</li>
<li>RuntimeException 제외 모든 Exception(ex. IOException)</li>
</ul>
<h3 id="unchecked-exception">Unchecked Exception</h3>
<ul>
<li>예외처리를 강제하지는 않음</li>
<li>실행단계에서 에러발생</li>
<li>예외 발생 시 트랜잭션 처리(Roll-back O)</li>
<li>RuntimeExcpetion(ex. NullPointerException etc)</li>
</ul>
<p>CheckedException이 발생할 수 있는 경우라면 반드시 try-catch 문으로 감싸서 처리해줘야 하며, 이부분은 IntelliJ를 쓰면 자동으로 IntelliJ가 빨간줄을 띄워서 알려주기도 한다. 하지만 UncheckedException의 경우 실행단계에서 발생할 수 있는 에러이기 때문에 꼼꼼한 에러 핸들링 없이는 놓칠 수 있는 부분이 크다. 하지만 고객에게 보여서는 안 되는 정보노출을 방지하거나 혹은 고객에게 못생긴 에러 창을 띄우지 않게 하려면 꼼꼼히 처리해줘야 하는 부분이다. (개발자에게 대단한 기능을 구현하는 것보다 더 중요한 것은 안정된 서비스를 만드는 일이니까!)</p>
<p>처음에는 나도 이러한 에러들을 만났을 때, throw new NullPointerException(&quot;error message&quot;) 이런식으로 처리를 해준 적이 있었다. (당시에는 예외처리를 했다는 뿌듯함도 있었지만 지금 생각해보면, 통일성이 전혀 없고 또 정확한 에러 원인과 이유도 밝히지 않은 문구들로 프론트에 혼란을 초래할 수 있는 코드였다.) 
개발자는 팀원들과 구현한 비즈니스 로직에서 발생하는 RuntimeExcxeption은 반드시 따로 Custom하여 처리를 해주어야 한다. 이 부분은 컴파일 단계에서 에러를 내지 않고 실행단계에서 에러를 내기 때문에 더욱 꼼꼼히 처리를 해주는 것이 안정된 서비스를 만드는 좋은 개발자의 역량이라고 생각한다. 
나는 이번 실전 프로젝트 시작 전 팀원들과 함께 예외 처리 방식에 대해 고민해보고 방식을 통일하여 사용하기로 결정했다.</p>
<p>우리가 고민한 기준은 총 2가지인데 프론트에게 통일되고 정확한 에러 메세지 전달하기가 그것이었다. 그리고 우리는 ExceptionHandling의 방식을 1. JSON형식으로 전달, 2. 통일된 에러 메세지 작성, 3. 상태 코드까지 함께 전달하는 방법으로 하기로 했다. 그리고 @ControllerAdvice와 @RestControllerAdvice라는 어노테이션을 사용해서 하나의 ExceptionController를 만들었고 이전 미니 프로젝트 때보다 좀더 가독성과 편의성이 높은 방법으로 예외처리를 할 수 있었다.
(아래는 이번 프로젝트에서 사용한 Exception 처리 방식 코드이다.)</p>
<pre><code>public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException() { super(); }
}</code></pre><pre><code>@RequiredArgsConstructor
@RestControllerAdvice
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity&lt;Fail&gt; UserNotFoundException(UserNotFoundException e) {
        return new ResponseEntity&lt;&gt;(new Fail(&quot;닉네임이 존재하지 않습니다.&quot;), HttpStatus.BAD_REQUEST);
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Redis는 언제 써야할까?]]></title>
            <link>https://velog.io/@mindfulness_22/Redis%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@mindfulness_22/Redis%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 14 Apr 2022 03:36:12 GMT</pubDate>
            <description><![CDATA[<p>실전 프로젝트에서 아직 코딩으로 기능을 구현하기 전, 팀원들과 함께 데이터 모델링을 할 때였다. User 테이블에는 Refresh Token 필드값을 넣어뒀었고, 채팅 기능을 구현하기 위해 Chatroom과 Chat 테이블까지 만들어뒀었는데 한 팀원이 이런 질문을 던졌다. </p>
<blockquote>
<p>&quot;실시간으로 채팅을 주고 받을 때마다 MySQL에 값을 다 넣으면 DB가 아마 터지고 말껄요?&quot;</p>
</blockquote>
<p>맞는 말이었다. MySQL을 채팅 메세지 보관용으로 바로 쓰기에는 속도, 응답성 2가지의 측면에서 문제가 있었다. 일정 시간이 지난 데이터들을 MySQL과 같은 RDBMS에 저장하는 것은 검색 등의 기능을 구현하기 위해 더 나은 선택이겠지만, 즉각적인 채팅 데이터들을 바로 데이터베이스에 접근하게 하는 것은 비효율적이었다.</p>
<p>그래서 우리는 Redis를 프로젝트에 도입했다.</p>
<h3 id="redis">Redis</h3>
<blockquote>
<p>Redis는 고성능 key-value 저장소로서 문자열, 리스트, 해시, 셋, 정렬된 셋 형식의 데이터를 지원하는 NoSQL이다. Redis는 In-Memory 저장소로서 기본적으로 빠른 데이터 입출력을 제공하며 키 등록, 조회에 있어서는 O(1)의 성능을 보장한다. </p>
</blockquote>
<p>하지만 이후, 단순히 입출력이 잦은 기능 구현에 필요하다고 해서 Redis를 도입하고 나서 우리 팀은 또다시 위기에 직면했다. 서버가 내려갈 때마다 Redis는 저장된 데이터를 모두 지웠던 것이다. 다른 것은 몰라도 채팅 같은 경우에는 불시에 이전 내역이 다 날라가있으면, 유저들이 큰 불편함을 느낄 수 있는 부분이었다. 그제서야 우리 팀은 이 문제를 해결하고자 &#39;Redis의 데이터 백업 방식&#39; 등을 검색하기 시작했고, Redis가 데이터 영속이라는 기능도 지원하는 오픈 소스라는 것을 알게 되었다. </p>
<h3 id="redis를-통한-데이터-영속화">Redis를 통한 데이터 영속화</h3>
<p>Redis는 데이터 관리 방법으로 3가지가 있는데,
첫번째는 메모리에서 데이터를 관리하는 방식(휘발성 기반), 
두번째는 특정 시간에 데이터를 디스크에 저장하는 방식(RDB 기반), 
세번째는 수시로 데이터를 디스크에 저장하는 방식(AOF 기반)이 있다. 
종류에 따라 성능저하를 유발할 수 있으므로 상황에 맞게 선택해야 하는데, 채팅 같은 경우에는 데이터 분실률을 0%로 가져가고 싶었기 때문에 우리는 AOF 기반의 데이터 저장방식을 선택했고, 유실되는 데이터 없이 채팅, 알림 서비스 기능을 구축할 수 있었다.</p>
<p>Redis를 처음 접했을 때는 Cache 저장소로 자주 사용된다는 것만 알았는데, 우리 서비스의 경우에는 Cache 보다는 Refresh Token 관리, 채팅, 알림 데이터 저장소로 활용을 했다. 프로젝트를 끝내고 이제 Redis를 생각하면 떠오르는 키워드는 다음과 같다. </p>
<h3 id="redis-의-핵심-keywords">Redis 의 핵심 Keywords</h3>
<ul>
<li>message broker기능을 수행하는 pubsub 구조</li>
<li>key-value 값 데이터 저장소</li>
<li>싱글 쓰레드로 실행되기 때문에 keys * 등의 명령어 쓸 때 주의할 것</li>
<li>영속화 기능(RDB, AOF)</li>
<li>연속적으로 발생하는 데이터를 보관하고 처리하는 Streams</li>
</ul>
<p>이번 프로젝트를 통해 처음 접한 Redis 였지만, 상당히 많은 기능 사용에 도전을 해보고 공부를 해본 것 같다. 다음 번에는 Cache Memory로도 제대로 사용을 해보고 싶고 무엇보다 우리 서비스에 적합한 Redis 성능을 측정하기 위한 테스트도 수행해보고 싶다.</p>
<p>이상, 끝!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데이터베이스] 조회 쿼리 성능 개선을 위해 노력했던 삽질...]]></title>
            <link>https://velog.io/@mindfulness_22/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%A1%B0%ED%9A%8C-%EC%BF%BC%EB%A6%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%9D%84-%EC%9C%84%ED%95%B4-%EB%85%B8%EB%A0%A5%ED%96%88%EB%8D%98-%EC%82%BD%EC%A7%88</link>
            <guid>https://velog.io/@mindfulness_22/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%A1%B0%ED%9A%8C-%EC%BF%BC%EB%A6%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EC%9D%84-%EC%9C%84%ED%95%B4-%EB%85%B8%EB%A0%A5%ED%96%88%EB%8D%98-%EC%82%BD%EC%A7%88</guid>
            <pubDate>Mon, 11 Apr 2022 10:25:05 GMT</pubDate>
            <description><![CDATA[<p>문제의 발단은 이러한 고민에서부터 시작되었다. </p>
<blockquote>
<p>&quot;프론트에서 요청한 데이터가 짜여진 DB 구조상 엄청나게 중첩되어 있을 때, DTO를 여러개 만들어서 프론트에 데이터를 주고 있는데, 이렇게 되면 쿼리가 여러개 날라가게 되잖아. 물론 불필요한 쿼리를 조회하는 것은 아니니 N+1 문제는 아니지만 이걸 하나로 합쳐서 해결할 수는 없을까?&quot;</p>
</blockquote>
<p>이러한 고민이다. 그래서 처음에는 <strong>JPQL</strong>로 해결을 하고자 했다. 하지만 거기에는 치명적인 문제가 있었으니, Left Fetch Join을 2개 이상의 OneToMany 자식 테이블에 선언하다보니 </p>
<pre><code>MultipleBagFetchException</code></pre><p>이 발생했다. JPA에서 Fetch Join의 조건은 1. ToOne은 몇개든 사용가능, 하지만 2. ToMany 는 1개만 사용가능하다. 물론 N+1문제를 해결하기 위해서는 application.yml파일에서 default_batch_fetch_size 옵션을 줘서 해결할 수 있을 것 같다. 해당 옵션은 지정된 수만큼 in 절에 부모 key를 사용하게 해주니까 쿼리 수행 수가 1/n로 줄어들게 된다. 하지만 이건 N+1 문제가 아니다.</p>
<p>그래서 그 다음에는 <strong>MySQL</strong>에서 직접 쿼리를 짜는 것으로 생각을 했다. 하지만 나는 프론트가 요청한 특정 필드 값들만 뽑아서 줘야 했기 때문에 MySQL에서 그런 식으로 쿼리문을 짜는 것은 어려웠다. </p>
<p>그래서 <strong>QueryDSL</strong>의 <strong>DTO Projection</strong>이 있다는 이야기를 접하고 바로 QueryDSL을 공부했다. QueryDSL은 SQL과 JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API이다. 이거면 내가 원하는 값들만 서브쿼리를 써서 뽑아올 수 있을 것 같았다. 그런데 계속해서 </p>
<pre><code>Nullpointerexception</code></pre><p>이 떴다. 하루 이틀 삽질을 하면서 알게 된 것이, QueryDSL은 관계가 2 depth 이상 깊어질 경우 연관 관계를 초기화시켜줘야 한다고 한다. 그렇다. Deep Initializing이 필요한 부분에서 Entity의 필드값에 @QueryInit을 써서 어노테이션을 달아줘야 했던 것이다. 나의 경우 User -&gt; Feed -&gt; FeedDetail -&gt; FeedDetailLoc 이렇게 자식관계로 매핑된 DB 구조였는데 FeedDetailLoc에서 해당 글 작성자인 User까지 타고 올라가려 하니 3번 테이블을 타고 들어가기 때문에 @QueryInit 어노테이션을 달아줘야 했던 것이다. 
그런 다음 또 Inner Join으로 맺어줘야 하는 Table을 Left Join으로 해주는 바람에 또 NullPointerException이 떠서 또 거의 반나절을 날렸다. 그리고 나서 문제를 다 해결했다고 생각했는데 아뿔싸... OneToMany와 ManyToOne이 뒤엉킨 10개의 Table을 Join하는 과정에서 당연히 중복되는 데이터들이 반복되어서 뽑히는 현상이 일어났다. 당연히 처음부터 발견했어야 하는 문제였는데 QueryDSL에서 DTO Projection을 쓸 수 있다는 기쁨과 당장의 NullPointerException만 해결하면 된다는 생각에 사로잡혀서 가장 근본적인 것을 놓치고 있었다.</p>
<p>결국 이렇게 JPQL부터 QueryDSL까지 조회 쿼리 수를 줄여보겠다고 안간힘을 썼지만 결국 공부만 많이 하고 이뤄낸 성능 개선은 없었다. 결국 DB 인덱싱을 적용하여 속도면에서 성능 개선을 이뤄내는 방법 밖에 없었다.</p>
<p>(내 프로젝트에서 과감히 사라진 마지막 QueryDSL문. 처음으로 써 본 QueryDSL 적용을 못 해서 슬펐지만 많이 배울 수 있던 시간이었다...)
<img src="https://velog.velcdn.com/images/mindfulness_22/post/ad03949a-af26-4184-af6c-5b914446dc65/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[데이터베이스] 정규화의 원칙]]></title>
            <link>https://velog.io/@mindfulness_22/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%A0%95%EA%B7%9C%ED%99%94%EC%9D%98-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@mindfulness_22/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%A0%95%EA%B7%9C%ED%99%94%EC%9D%98-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Mon, 11 Apr 2022 09:13:50 GMT</pubDate>
            <description><![CDATA[<h3 id="정규화의-원칙">정규화의 원칙</h3>
<p><strong>1차 정규화</strong>: <strong>1차 정규형은 각 로우마다 컬럼의 값이 1개씩만 있어야 한다.</strong> 이를 컬림이 원자값을 갖는다고 말하는데 만일 하나의 컬럼이 2개 이상의 값을 가지면 1차 정규형을 만족하지 못한다고 말한다. 그리고 2개의 값이 있었던 것을 1차 정규화하게 되면 데이터 redundancy가 증가하게 되는데, 이는 데이터의 논리적 구성을 위해 희생하는 것으로 볼 수 있다.</p>
<p><strong>2차 정규화</strong>: 2차 정규형은 테이블의 모든 컬럼이 <strong>완전 함수적 종속을 만족하는 것</strong>이다. 즉 기본키 중에 특정 컬럼에만 종속된 컬럼(부분적 종속)이 없어야 한다는 것인데, 만일 테이블 컬럼이 Student, Age, Subject가 있다면 Age는 Subject와는 별개로 Student에만 종속되어 있다. 즉 Student 컬럼의 값을 알면 Age값을 알수 있기 때문에, <strong>Student와 Age, Student와 Subject 이렇게 테이블을 별도로 관리</strong>하여 제 2 정규형을 만족시킬 수가 있게 된다.</p>
<p>3차 정규화: 3차 정규형은 2차 정규화를 진행한 테이블에 대해 <strong>이행적 종속을 없애도록 테이블을 분해하는 것</strong>이다. 여기서 <strong>이행적 종속이란 A -&gt; B, B -&gt; C가 성립할 때, A -&gt; C가 성립되는 것</strong>을 의미한다. 이 또한 2차 정규화와 마찬가지로 테이블 분리로 해결 가능한데, 이를 통해 데이터가 논리적인 단위로 분리될 수 있고 데이터의 redundancy도 줄일 수 있게 된다.</p>
<p>BCNF : 이는 3차 정규형을 조금 더 강화한 버전으로 볼 수 있다. 이를 통해 3차 정규형으로 해결 불가능한 이상현상을 해결할 수 있는데, <strong>BCNF는 3차 정규형을 모두 만족하면서 모든 결정자가 후보키가 되도록 테이블을 분해하는 것</strong>이다.</p>
]]></description>
        </item>
    </channel>
</rss>