<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ShiningDr.log</title>
        <link>https://velog.io/</link>
        <description>Java Web Developer</description>
        <lastBuildDate>Fri, 30 Apr 2021 04:31:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ShiningDr.log</title>
            <url>https://images.velog.io/images/shining_dr/profile/ec6bf73f-4616-4e60-ac9a-e1fa1a608d9a/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ShiningDr.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/shining_dr" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Thymeleaf 문자열 조합법 (간단)]]></title>
            <link>https://velog.io/@shining_dr/Thymeleaf-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%A1%B0%ED%95%A9%EB%B2%95-%EA%B0%84%EB%8B%A8</link>
            <guid>https://velog.io/@shining_dr/Thymeleaf-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%A1%B0%ED%95%A9%EB%B2%95-%EA%B0%84%EB%8B%A8</guid>
            <pubDate>Fri, 30 Apr 2021 04:31:39 GMT</pubDate>
            <description><![CDATA[<p>Thymeleaf 에서 문자열 조합하기</p>
<p>처음에는</p>
<pre><code class="language-html">&lt;h1 th:text=&quot;&#39;Item name : &#39; + &#39;${item.name}&#39;&quot;&gt;&lt;/h1&gt;</code></pre>
<p>이렇게 했는데 더 간단하게 해보고 싶어서 조사해본 결과..</p>
<pre><code class="language-html">&lt;h1 th:text=&quot;|Item name : ${item.name}|&quot;&gt;&lt;/h1&gt;</code></pre>
<p>간단하다.
까먹지 않게 기록.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring MVC : Servlet 개요]]></title>
            <link>https://velog.io/@shining_dr/Spring-MVC-Servlet-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@shining_dr/Spring-MVC-Servlet-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Tue, 16 Mar 2021 08:18:27 GMT</pubDate>
            <description><![CDATA[<p>HTTP 프로토콜을 통해서 대부분의 데이터를 주고 받는다.</p>
<p><strong>웹 서버란?</strong></p>
<p>HTTP 를 기반으로 동작하며 정적 리소스 (HTML, CSS, JS, 이미지, 영상, 등)를 제공</p>
<p><strong>웹 애플리케이션 서버란?</strong></p>
<p>HTTP 기반으로 동작하며 웹 서버 기능을 포함한다. 프로그램 코드를 실행해서 애플리케이션 로직을 수행해서 동적인 HTML을 생성하며, HTTP API (JSON)도 제공이 된다
서블릿, JSP, 스프링 MVC가 WAS 서버에서 동작.</p>
<p><strong>웹 시스템 구성</strong></p>
<p>-&gt; WAS, DB만 구성할 수 있긴함
WAS가 정적인 리소스까지 담당하게 되기 떄문에 너무 많은 역할을 담당하게 되어 과부하가 우려됨
WAS 가 장애가 오면 정적 리소스 노출도 불가능해짐
서버끼리 데이터만 주고 받는 API 의 경우에는 WAS 서버만으로도 ok.</p>
<p>-&gt; WEB Server, WAS, DB 로 구성
정적 리소스는 웹서버가 처리하고, WAS는 중요한 애플리케이션 로직을 담당
업무 분담!
시스템 리소스를 효율적으로 관리
정적 리소스 관리가 필요 -&gt; WEB Server 증설
애필리케이션 로직 담당 필요 -&gt; WAS 증설
만약 WAS 서버가 죽으면 WEB Server는 오류화면 HTML을 뿌리도록 설정</p>
<hr>
<p><strong>웹 애플리케이션 서버를 직접 구현하려면...?</strong></p>
<p>HTTP 메세지가 클라이언트에서 만들어져서 서버로 전송되면,
만약에 우리가 웹 애플리케이션 서버를 전부 다 구현해야 한다면</p>
<p>HTTP 메세지를 쭉 풀어 해쳐야 함
-&gt; POST /save HTTP/1.1 이었을 경우</p>
<p>TCP/IP 연결 대기 후 소캣 연결
HTTP 메세지도 사실 하나의 텍스트기 때문에 잘라서 파싱을 일일히 해야함
정보를 파싱한 뒤에 POST 방식, URL 인지
Content-Type 확인, HTTP 메세지 바디 내용 파싱,,
/save URL 이었기 때문에 저장 프로세스 실행</p>
<hr>
<p>비즈니스 로직 실행 (데이터 베이스 저장 요청)</p>
<hr>
<p>HTTP 응답 메세지 생성 시작
HTTP 시작 라인 생성
Header 생성
메세지 바디에 HTML 생성해서 입력
TCP/IP 응답 전달, 소캣 종료...</p>
<p>전세계 개발자가 이걸 개발하고 있으면 비효율 적
이걸 서블릿이 가운데 비즈니스 로직 빼고 다 해줌
서블릿을 제공하는 WAS들이 이 기능을 다 지원해준다.</p>
<p>urlPatterns(/hello) 의 URL이 호출되면 서블릿 코드가 실행.
HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest
HTTP 응답 정보를 편리하게 제공할 수 있는 HttpServletResponse</p>
<p>HTTP 요청이 오면
WAS는 Request, Response 객체를 새로 만들어서 서블릿 객체 호출
개발자는 Request 객체에서 HTTP 요청 정보를 편리하게 꺼내서 사용
개발자는 Rseponse 객체에 HTTP 응답 정보를 편리하게 입력
WAS는 Response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성</p>
<p><strong>서블릿 컨테이너</strong></p>
<p>WAS 안에 서블릿 컨테이너
서블릿 컨테이너가 자동으로 생성, 호출, 종료
생명 주기 관리!</p>
<p>서블릿 객체는 <strong>싱글톤</strong>으로 관리!!</p>
<p>request, response 객체는 항상 요청이 올때마다 새로 생성해야 함
그러나 서블릿은 같은 기능을 수행하므로, 고객의 요청이 올 때 마다 new 해서 객체를 새로 생성하는 것은 비효율
최초 로딩 시점에 서블릿 객체를 미리 만들어 두고 재활용</p>
<p><strong>공유 변수 사용 주의!</strong></p>
<p>참고) JSP 도 서블릿으로 변환되서 사용</p>
<p><strong>동시 요청을 위한 멀티 쓰레드 처리 지원!!</strong></p>
<p>** 스레드란?</p>
<p>애플리케이션 코드 하나하나 순차적으로 실행하는 것은 쓰레드
(자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행)</p>
<p>동시처리가 필요하다면 쓰레드 추가 생성</p>
<ul>
<li><p>요청마다 쓰레드 생성한다면..?
장점 : 동시 요청을 처리 가능, 리소스가 허용될 때 까지 처리 가능, 하나의 쓰레드가 지연되도 나머지 쓰레드는 정상 동작</p>
<p>단점 : 쓰레드 생성 비용은 비쌈
쓰레드 컨텍스트 스위칭 비용이 발생
쓰레드 생성에 제한이 없어서 고객의 요청이 너무 많이 오면 CPU, 메모리 임계점을 넘어서 서버가 죽어버림</p>
</li>
</ul>
<ul>
<li>그래서 쓰레드 풀 사용! (요청 마다 쓰레드 생성의 단점 보완)
쓰레드 들이 있는 풀장이라고 생각
쓰레드 풀에 있는 쓰레드를 사용하고 사용 후에 반납하는 방식</li>
</ul>
<p>쓰레드 풀에 쓰레드가 없으면 대기 or 거절</p>
<p>쓰레드가 미리 생성되어 있으므로, 쓰레드를 생성하고 종료하는 비용이 절약되고 응답시간이 빠르다</p>
<ul>
<li>쓰레드 풀 실무 팁</li>
</ul>
<p>너무 낮게 설정 : CPU 적게 사용.. 동시 요청이 많으면, 서버 리소스는 여유롭지만 클라이언트는 금방 응답 지연</p>
<p>너무 높게 설정 : 동시 요청이 많으면 CPU, 메모리 리소스 임계점 초과로 서버 다운</p>
<p>적정 숫자는 성능 테스트를 시도해서 찾는다.</p>
<p>--
결론은 멀티 쓰레드에 대한 부분은 WAS가 처리하므로 개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 된다.
개발자는 마치 싱글 쓰레드 프로그래밍을 하듯이 편리하게 소스코드 개발
멀티 쓰레드 환경이므로 싱글톤 객체 (서블릿, 스프링 빈)는 주의해서 사용!</p>
<p>출처 : 인프런, 김영한 강사님의 스프링 MVC 1편</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Builder 패턴과 NullPointException]]></title>
            <link>https://velog.io/@shining_dr/Builder-%ED%8C%A8%ED%84%B4%EA%B3%BC-NullPointException</link>
            <guid>https://velog.io/@shining_dr/Builder-%ED%8C%A8%ED%84%B4%EA%B3%BC-NullPointException</guid>
            <pubDate>Thu, 11 Feb 2021 15:03:45 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>개인 웹 프로젝트를 하면서 NullPointException으로 인해 고통받았다...
현재 진행중인 프로젝트에서 Time 엔티티와 Seat 엔티티가 존재하고, 서로 양방향 연관관계 매핑이 되어있는 상황이다. 
(Time (1) &lt;--&gt; Seat (다))</p>
<ul>
<li>Time 엔티티 클래스</li>
</ul>
<pre><code class="language-java">@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString(exclude = &quot;seats&quot;)
public class Time extends BaseEntity{

    /** 다른 필드는 생략 **/

    @OneToMany(mappedBy = &quot;time&quot;, cascade = CascadeType.REMOVE)
    private List&lt;Seat&gt; seats = new ArrayList&lt;&gt;();

}</code></pre>
<p>@OneToMany 어노테이션으로 Seat 엔티티와 일대다 매핑하였고, cascade = CascadeType.REMOVE를 통해 Time 엔티티 객체가 지워지면 연관된 Seat 엔티티 객체가 지워지도록 하였다.</p>
<ul>
<li><p>Seat 엔티티 클래스</p>
<pre><code class="language-java">@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString(exclude = &quot;time&quot;)
public class Seat {

  /** 다른 필드는 생략 **/

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = &quot;time_id&quot;)
  private Time time;

  // 연관관계 편의 메서드
  public void setTime(Time time) {
      if(this.time != null){
          this.time.getSeats().remove(this);
      }
      this.time = time;
      time.getSeats().add(this);
  }
}</code></pre>
</li>
</ul>
<p>@ManyToOne 어노테이션으로 Time 엔티티와 다대일 매핑하였고, 연관관계 편의 메소드를 작성했다. 양방향으로 참조를 저장한다.</p>
<p>TimeService 클래스에서 Time을 저장하면서 동시에 Seat 객체를 만들어서 저장한다. (최적화가 덜 된 코드임을 감안해주세요..)</p>
<ul>
<li>TimeService 구현 객체</li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Log4j2
public class TimeServiceImpl implements TimeService{

    private final TimeRepository timeRepository;
    private final CinemaRepository cinemaRepository;
    private final SeatService seatService;

    @Override
    @Transactional
    public Long save(TimeSaveDTO timeSaveDTO) {
        Cinema cinema = cinemaRepository.findById(timeSaveDTO.getCinemaId())
                .orElseThrow(() -&gt; new IllegalStateException(&quot;해당 Cinema가 없음&quot;));

        Time time = dtoToEntity(timeSaveDTO, cinema);

        // 처음 극장 시간 정할 때, 좌석을 모두 사용 가능한 상태로 넣어줌.
        time = seatService.makeSeats(timeSaveDTO.getSeatNum(), time);

        timeRepository.save(time);

        return time.getId();
    }
}</code></pre>
<ul>
<li>SeatService 구현 객체</li>
</ul>
<pre><code class="language-java">@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Log4j2
public class SeatServiceImpl implements SeatService{

    private final SeatRepository seatRepository;

    @Override
    @Transactional
    public Time makeSeats(int seatNum, Time time) {
        log.info(&quot;Make Seats.... : &quot; + seatNum);
        log.info(&quot;Time : &quot; + time);

        // 좌석이 100석일 때.
        if(seatNum == 100) {
            for(int i = 1; i &lt;= 10; i++) {
                for(int j = 1; j &lt;= 10; j++) {
                    Seat seat = Seat.builder().row(i).col(j).isAvailable(true).build();
                    seat.setTime(time);
                    seatRepository.save(seat);
                }
            }
        }
        // 좌석이 120석일 때.
        else if(seatNum == 120) {
            for(int i = 1; i &lt;= 10; i++) {
                for (int j = 1; j &lt;= 12; j++) {
                    Seat seat = Seat.builder().row(i).col(j).isAvailable(true).build();
                    seat.setTime(time);
                    seatRepository.save(seat);
                }
            }
        }
        return time;
    }
}</code></pre>
<p>Controller를 통해 TimeSaveDTO 객체를 받아서 entity로 변환 후 Time을 만든다.
SeatService의 makeSeats 메소드를 통해 방금 만든 Time 객체 참조를 넘겨준다.
SeatService에서 받은 Time 객체 참조를 통해 Seat 객체들을 만든다. 이 때 위에서 봤던 연관관계 메소드를 사용한다.
Seat 객체들을 주입받은 Repository 객체를 통해 영속상태로 만든 뒤 DB에 저장한다.
그 후에 Time 객체도 Repository 객체를 통해 저장한다.</p>
<p>여기서 문제가 발생한다...
공포의 NullPointException이 뜬 것!</p>
<p>처음에는 순서가 잘못되었나 싶어서 Time 객체를 먼저 영속상태로 만들고, 그 후에 Seat객체를 만들어보았지만 문제는 없었다.
Time 객체가 null인가 싶어 로깅을 해보았지만 정상이었다.
도대체 무엇이 문제일까... 계속 시도 끝에 드디어 발견했다.</p>
<p>바로 Builder와 관련된 문제였다. Builder에 대한 이해가 부족했다..</p>
<h2 id="builder-패턴">Builder 패턴</h2>
<p>객체를 생성할 때 인자들을 선택적으로 받아서 생성하고 싶을 때 여러개의 생성자를 만들게 된다. 만약 인자 개수가 많아지면, 생성자를 많이 생성해야 하고, 같은 자료형의 인자라면 실수로 값이 바뀌어서 객체가 생성 될 수 있다.</p>
<p>Setter를 통해 객체 필드값들을 채울 수 있지만, 이렇게 하면 &quot;객체 일관성&quot;이 깨진다. 언제 어디서든 Setter 를 통해서 객체의 필드값을 변경해버릴 수 있어서 위험하다.</p>
<p>이러한 문제들을 보완하기 위해 나온 것이 Builder 패턴이다.
Builder 패턴은 메소드 체이닝 기법을 사용해서 Builder 객체를 활용해 객체를 생성하는 방법이다. </p>
<p>예를 들어 Time 객체를 TimeSaveDTO 객체의 필드들을 통해 생성하는 코드는 다음과 같다.</p>
<pre><code class="language-java">default Time dtoToEntity(TimeSaveDTO timeSaveDTO) {
        return Time.builder()
                .time(LocalTime.of(timeSaveDTO.getHour(),timeSaveDTO.getMinute()))
                .seatNum(timeSaveDTO.getSeatNum())
                .availableNum(timeSaveDTO.getSeatNum())
                .build();
    }</code></pre>
<p>Builder 객체를 build 한 뒤에는  필드 값을 변경하기 위해선 setter 메소드를 만들거나, 리플렉션 기법밖에 방법이 없다. 
그래서 &quot;객체 일관성&quot;이 깨지지 않는다. 또한 인자를 선택적으로 넣을 수 있고, 가독성도 좋다.</p>
<p>프로젝트에서는 Lombok의 @Builder annotation을 통해서 더욱 손쉽게 구현하였다. (Lombok의 @Builder 를 사용하려면 @AllArgsConstructor와 @NoArgsConstructor를 넣어주어야 함)</p>
<h2 id="문제-해결">문제 해결</h2>
<p>Builder 패턴에 대해 간단히 알아보았으니 무엇이 문제였을지 알아보자..
범인이 된 코드를 여기 적겠다</p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;time&quot;, cascade = CascadeType.REMOVE)
private List&lt;Seat&gt; seats = new ArrayList&lt;&gt;();</code></pre>
<p>List는 Java의 Collections 인터페이스로, Collection에 객체들을 저장하려면 (정확히는 참조들) 구현체를 지정해주어야 한다.
위와 같은 경우에는 ArrayList를 구현체로 지정해 준 상황이다.
그런데 @Builder를 통해 Builder 패턴을 사용할 때 위처럼만 작성하면 ArrayList가 저장이 안되는 문제점이 발생했다.
@Builder사용시 Class Type 기본 값은 null이 된다. 위와 같이 작성한다고 기본값 지정이 안되었다.</p>
<p>위의 의도대로 ArrayList 구현체를 default로 지정해 주고 싶으면
@Builder.Default 라는 어노테이션을 추가로 붙여주어야만 했다!</p>
<p>밑의 코드처럼 @Builder.Default 어노테이션을 통해 기본값이 설정 가능하다.</p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;time&quot;, cascade = CascadeType.REMOVE)
@Builder.Default
private List&lt;Seat&gt; seats = new ArrayList&lt;&gt;();</code></pre>
<p>사실 얼핏 보면 간단하게 해결한 것 같지만, 필자는 jpa의 연관관계 매핑에 문제가 있다고 생각해 엉뚱한 곳을 건드리느라 해결하는데 시간이 많이 걸렸다. (삽질했다..) 혹시라도 이와 같은 문제가 있는 사람이 있다면 이글을 보고 손쉽게 해결할 수 있었으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Annotation기반 스프링 AOP]]></title>
            <link>https://velog.io/@shining_dr/Annotation%EA%B8%B0%EB%B0%98-%EC%8A%A4%ED%94%84%EB%A7%81-AOP</link>
            <guid>https://velog.io/@shining_dr/Annotation%EA%B8%B0%EB%B0%98-%EC%8A%A4%ED%94%84%EB%A7%81-AOP</guid>
            <pubDate>Thu, 04 Feb 2021 04:35:12 GMT</pubDate>
            <description><![CDATA[<h2 id="aop-탄생-배경">AOP 탄생 배경</h2>
<p>AOP (Aspect Oriented Programming) 를 우리말 그대로 해석해보면 관점지향 프로그래밍이다. 지식이 없는 상태에서 이 정의를 보았을 때 어떤 의미인지 와닿지 않았다. 그래서 AOP가 왜 필요한지 필요한 상황을 먼저 알아보고자 한다. 다음 그림을 보자.</p>
<p><img src="https://images.velog.io/images/shining_dr/post/4a842833-0c1f-47cb-8478-3d95306f98a6/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-02-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.40.54.png" alt=""></p>
<p>현재 Class A, B, C 는 각각 자신만의 핵심 관심사항(Core Concern)이 되는 비즈니스 로직을 가지고 있다. 개발자는 이 비즈니스 로직들이 런타임 시 어느정도 시간이 소요되는지 측정하고 싶어 시간 측정 로직을 각각의 Class들에 추가했다. 
여기서 문제점이 발생한다. 
우선, 거의 동일한 시간 측정 로직이 모든 Class에 중복되서 발생했다는 점이다. 그리고, 이러한 시간 측정 로직이 각 Class별 핵심 비즈니스 로직과 맞물려 유지보수성과 가독성이 나빠졌다. 각각의 비즈니스 로직 사이에서 시간측정을 어떻게 해야할까? 바로 이러한 이유로 AOP가 등장한다.</p>
<p><img src="https://images.velog.io/images/shining_dr/post/d6fc4b17-7f42-4502-a88c-e299f3ba290c/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-02-01%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%208.46.40.png" alt=""></p>
<p>공통 관심사항(Cross-Cutting-Concern)인 시간 측정 로직을 마치 자바 Class로 따로 작성하듯 추려내고 이를 필요할 때 외부 Class에서 사용하도록 한다. 그러나 이는 단순히 Class를 추려서 하는 방법과는 틀리다. 여기서 프록시 패턴이 등장한다.</p>
<h2 id="프록시-패턴">프록시 패턴</h2>
<p>프록시 패턴을 알아보기 위해 간단한 예제를 작성해 보았다.</p>
<pre><code class="language-java">public interface MyEvent {
    void beforeEvent();
    void middleEvent();
    void afterEvent();
}</code></pre>
<p>먼저 이벤트를 처리하는 용도라 가정된 인터페이스 타입을 정의한다.</p>
<pre><code class="language-java">@Service
public class MyEventImpl implements MyEvent{

    @Override
    public void beforeEvent() {
        System.out.println(&quot;Before Event&quot;);
    }

    @Override
    public void middleEvent() {
        System.out.println(&quot;Middle Event&quot;);
    }

    @Override
    public void afterEvent() {
        System.out.println(&quot;After Event&quot;);
    }
}</code></pre>
<p>MyEvent 구현체를 간단히 만들었다. 각 메소드에는 비즈니스 로직이 있다고 가정하고, 각각의 메소드의 실행시간을 측정해보겠다.</p>
<pre><code class="language-java">@Service
public class MyEventImpl implements MyEvent{

    @Override
    public void beforeEvent() throws InterruptedException {
        long before = System.currentTimeMillis();
        Thread.sleep(1000);
        System.out.println(&quot;Before Event&quot;);
        System.out.println(System.currentTimeMillis() - before);
    }

    @Override
    public void middleEvent() throws InterruptedException {
        long before = System.currentTimeMillis();
        Thread.sleep(2000);
        System.out.println(&quot;Middle Event&quot;);
        System.out.println(System.currentTimeMillis() - before);
    }

    @Override
    public void afterEvent() throws InterruptedException {
        long before = System.currentTimeMillis();
        Thread.sleep(3000);
        System.out.println(&quot;After Event&quot;);
        System.out.println(System.currentTimeMillis() - before);
    }
}</code></pre>
<p><img src="https://images.velog.io/images/shining_dr/post/77f22c61-a90a-4ac2-8d45-b48326de9aa5/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-02-04%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2012.34.25.png" alt=""></p>
<p>측정시간이 매우 적어 Thread.sleep() 을 통해 일부러 실행 시간을 늘려봤다. 작동은 잘 되지만 코드를 보면 똑같은 실행시간 측정 로직이 반복되고 있다. 이를 프록시 패턴을 통해 리펙토링을 해보자.</p>
<p>우선 프록시 패턴을 그림을 통해 살펴보겠다.</p>
<p><img src="https://images.velog.io/images/shining_dr/post/955e4104-0bf8-441a-9972-77f61a7b0fdf/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-02-04%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%2011.23.47.png" alt=""></p>
<p>클라이언트는 인터페이스 타입 객체를 사용하는 상황이다.
그림의 Real Subject가 실제 기능이 있는 인터페이스의 구현 객체이며, 같은 인터페이스 타입의 Proxy 객체가 Real Subject 객체를 참조하고 있다.
클라이언트는 프록시 객체를 주입받아 사용하게 되며, 프록시 객체는 실제 기능이 담긴 구현 객체를 감싸서 클라이언트의 요청을 처리하게 된다.
이 패턴의 목적은 접근제어, 혹은 부가기능을 위한 목적으로 사용된다.</p>
<pre><code class="language-java">@Primary
@Service
public class ProxyMyEvent implements MyEvent{

    @Autowired MyEvent myEventImpl;

    @Override
    public void beforeEvent() throws InterruptedException {
        long before = System.currentTimeMillis();
        myEventImpl.beforeEvent();
        System.out.println(System.currentTimeMillis() - before);
    }

    @Override
    public void middleEvent() throws InterruptedException {
        long before = System.currentTimeMillis();
        myEventImpl.middleEvent();
        System.out.println(System.currentTimeMillis() - before);
    }

    @Override
    public void afterEvent() throws InterruptedException {
        long before = System.currentTimeMillis();
        myEventImpl.afterEvent();
        System.out.println(System.currentTimeMillis() - before);
    }
}</code></pre>
<p>@Primary 어노테이션을 통해 Proxy 객체가 MyEvent Type으로 클라이언트에서 주입받도록 설정한다. 그리고 Proxy에서 MyEventImpl 구현체를 구현체 이름으로 주입 받고, 기능을 위임(Delegate)하는 방식으로 구현한다. 
그 대신 Proxy 객체가 시간을 측정하는 로직을 대신 실행한다. </p>
<p>이를통해 클라이언트 코드를 건드리지 않고 부가기능을 추가할 수 있으며, 핵심 비즈니스 로직과 시간 측정 로직을 서로 분리할 수 있다.</p>
<p>하지만, 아직 문제점이 많다. 중복이 여전히 생긴다는 점과, 이로 인한 Proxy Class를 매번 만드는 비용과 수고를 감수해야 한다. 만약 시간을 측정하고 싶은 인터페이스의 구현체가 많다면 이를 위해 각각의 Proxy 클래스를 만들어 주어야 할 것이다.</p>
<h2 id="annotation-기반-spring-aop">Annotation 기반 Spring AOP</h2>
<p>이번 포스팅에서는 위와 같은 문제점을 Spring Annotation 기반의 AOP를 통해 해결해 볼 것이다. </p>
<p>그 전에, 기본 개념을 알게 된 상태에서 AOP 용어를 살펴보자.</p>
<p>*<em>Aspect *</em>
AOP 기술을 통해 흩어진 기능을 모은 모듈</p>
<p><strong>Advice</strong>
Aspect 모듈이 실행해야 하는 일</p>
<p><strong>JoinPoint</strong>
advice가 실행되어야 하는 지점
보통 생성자 생성 전, 생성 후, 필드에 접근 전, 메소드 실행시점, 등이 JoinPoint가 될 수 있음</p>
<p>다음은 코드 부분이다.
build.gradle 파일에 의존성을 하나 추가해준다.</p>
<pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-aop&#39;</code></pre><p>그 후 코드를 작성해 준다. </p>
<pre><code class="language-java">@Component
@Aspect
public class SimpleAspect {

    @Around(&quot;@annotation(MyAnnotation)&quot;)
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
        long before = System.currentTimeMillis();
        Object ret = pjp.proceed();
        System.out.println(System.currentTimeMillis() - before);
        return ret;
    }
}</code></pre>
<p>Aspect 모듈임을 알려주기 위한 @Aspect를 명시하고, @Component를 통해 bean으로 등록해준다.
logPerf 메소드의 매개변수인 ProceedingJoinPoint는 advice가 적용되는 대상이다.
그 후 proceed()를 통해 메소드를 실행해 주고, 실행 결과를 리턴해준다.
여기서 advice로 시간 측정 로직을 넣어주었다.</p>
<p>@Around 어노테이션은 메소드를 감싸는 형태로 정의된다.
메소드 호출 이전, 이후에도 advice를 적용할 수 있고, 예외 처리도 할 수 있는 다용도 어노테이션이다.</p>
<p>우리는 @Around 어노테이션에 이 advice를 어디에 적용할 지 JoinPoint를 지정해 줄 수 있다. </p>
<p>여기서 우리는 사용자 정의 어노테이션을 만들어서 @Around 어노테이션에 JoinPoint를 알려주는 방법을 사용해 볼 것이다. </p>
<p>이를 위해 간단한 어노테이션을 만든다.</p>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface MyAnnotation {
}</code></pre>
<p>그 후에 시간을 측정하고 싶은 메소드에 방금 만든 어노테이션을 명시해준다.</p>
<pre><code class="language-java">@Service
public class MyEventImpl implements MyEvent{

    @Override
    @MyAnnotation
    public void beforeEvent() throws InterruptedException {
        Thread.sleep(1000);
        System.out.println(&quot;Before Event&quot;);
    }

    @Override
    @MyAnnotation
    public void middleEvent() throws InterruptedException {
        Thread.sleep(2000);
        System.out.println(&quot;Middle Event&quot;);
    }

    @Override
    public void afterEvent() throws InterruptedException {
        Thread.sleep(3000);
        System.out.println(&quot;After Event&quot;);
    }
}</code></pre>
<p><img src="https://images.velog.io/images/shining_dr/post/4792f75f-a318-4306-9df3-64f44d583088/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-02-04%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%2012.57.04.png" alt=""></p>
<p>어노테이션을 붙이고 실행해보면, 어노테이션을 붙였던 beforeEvent와 middleEvent 메소드에서는 시간측정이 되었지만, afterEvent에서는 시간측정이 안되고 있는 모습을 확인할 수 있다.</p>
<p>@Around 어노테이션 뿐 아니라 @Before 어노테이션을 통해 해당 메소드 시작 전에 AOP를 적용할 수 있으며,
@AfterReturning, @AfterThrowing 도 마찬가지로 적용할 수 있다.</p>
<p>이 Aspect 들은 특정 도메인에 관한 것을 추상화 한다기 보다는, 여러 계층, 여러 도메인에 거쳐서 등장하는 코드들(Cross-cutting concerns)을 Aspect로 만들어서 적용한다는 사실을 기억해야 한다.</p>
<p>지금까지 간단하게 AOP 탄생 배경과, 스프링 AOP 의 간단한 활용 방법을 공부해보았다.
사실 AOP 는 깊게 파고들면 파고들수록 더 수많은 지식과 알아야 할 점들이 존재한다. 공부를 거듭하고 나서 더 많은 지식을 얻게 되면 다른 포스팅에서 내용을 추가해 보도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Querydsl Repository 확장에 대한 고찰]]></title>
            <link>https://velog.io/@shining_dr/Querydsl-Repository-expansion</link>
            <guid>https://velog.io/@shining_dr/Querydsl-Repository-expansion</guid>
            <pubDate>Sat, 16 Jan 2021 11:14:23 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>Querydsl이 존재하기 이전의 JPA를 사용하는 Spring 프로젝트에서는 검색 조건에 따라 게시물을 검색하는 등의 <strong>Dynamic Query (동적 쿼리)</strong> 를 처리하기 위해서는 <strong>JPQL을 통한 문자열 처리</strong>나, <strong>JPA Criteria</strong> 등으로 처리하였다. 하지만 JPQL 쿼리를 문자열로 처리하는 행위는 번거러울 뿐더러 문자열 오타와 같은 실수로 인한 버그 발생 우려가 높다. 뿐만 아니라 JPA Criteria 같은 경우는 JPA 표준 스펙이지만 실무에서 사용하기 복잡하고 코드의 가독성이 매우 떨어진다.</p>
<pre><code class="language-java">// 복잡하고 가독성이 떨어져 유지보수성이 낮은 Criteria 방식 예시
public List&lt;Order&gt; findAll(OrderSearch orderSearch) {

        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery&lt;Order&gt; cq = cb.createQuery(Order.class);
        Root&lt;Order&gt; o = cq.from(Order.class);
        Join&lt;Order, Member&gt; m = o.join(&quot;member&quot;, JoinType.INNER); 
        List&lt;Predicate&gt; criteria = new ArrayList&lt;&gt;();

        if (orderSearch.getOrderStatus() != null) {
            Predicate status = cb.equal(o.get(&quot;status&quot;), orderSearch.getOrderStatus());
            criteria.add(status); }

        if (StringUtils.hasText(orderSearch.getMemberName())) {
            Predicate name = cb.like(m.&lt;String&gt;get(&quot;name&quot;), &quot;%&quot; +
                    orderSearch.getMemberName() + &quot;%&quot;);
            criteria.add(name);
        }
        cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
        TypedQuery&lt;Order&gt; query = em.createQuery(cq).setMaxResults(1000); 
        return query.getResultList();
}</code></pre>
<p>오늘날 이러한 동적쿼리 작성의 문제점들을 <strong>Querydsl</strong>이 멋지게 해결해준다. 쿼리를 자바 코드로 작성할 수 있게 해주며, 문법 오류를 컴파일 시점에 체크할 수 있게 해줌으로써 개발자에게 편리함을 선사해준다. </p>
<h2 id="spring-data-jpa-와-querydsl">Spring Data JPA 와 Querydsl</h2>
<p>필자는 <strong>Spring boot</strong> 프로젝트에서 <strong>Spring Data JPA</strong> 와 <strong>Querydsl</strong>을 함께 사용해 repository를 구축하고 있다. </p>
<pre><code class="language-java">// 예시 코드
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;{

    @Query(&quot;select m from Member m left join fetch m.team&quot;)
    List&lt;Member&gt; findMemberFetchJoin();</code></pre>
<p>Spring Data JPA를 사용하면 자바 인터페이스에서 Spring Data JPA의 <strong>JpaRepository을 상속</strong>받는 방식으로 <strong>기본적인 CRUD</strong> 기능들을 직접 코드를 작성할 필요없이 가져다 쓸 수 있으며, join이 필요한 복잡한 쿼리들은 위의 코드처럼 <strong>@Query</strong> annotation을 활용한 JPQL 작성 방식으로 interface안에 추가할 수 있다.</p>
<h2 id="querydslrepositorysupport를-사용한-방식">QuerydslRepositorySupport를 사용한 방식</h2>
<p>Spring Data JPA와 함께 Querydsl 코드를 작성하려면 Spring Data JPA의 Repository를 확장해야만 한다. 확장 방법으로 <strong>QuerydslRepositorySupport</strong> 클래스를 이용하는 방법을 우선 알아보자. 다음은 검색을 위한 동적쿼리를 예시 코드들이다.</p>
<pre><code class="language-java">public interface SearchBoardRepository {

    Page&lt;Object[]&gt; SearchPage(String type, String keyword, Pageable pageable);

}

public interface BoardRepository extends JpaRepository&lt;Board, Long&gt;, SearchBoardRepository{}</code></pre>
<p>우선 인터페이스를 하나 더 추가하고, 구현할 메소드를 작성한다. 또한 JpaRepository를 상속받은 repository interface에서 이를 상속받는다.</p>
<p>(<em>상기 코드는 Board, Member, Reply의 3가지 Entity가 존재함 Board 와 Member 사이의 다 : 1,<br>Reply 와 Board 사이의 다 : 1 매핑이 되어있는 상황
type 인자는 board의 제목, 내용, 작성자중 어느 것으로 검색 조건을 할지를 나타냄
keyword 인자는 포함된 내용을 찾기위한 검색 내용임 
pageable은 Spring의 페이징과 정렬을 위한 인자임</em>)</p>
<p>그 후 구현 클래스를 작성한다. <strong>제목은 반드시 인터페이스의 이름 + Impl로 작성</strong>하도록 한다. 또한 구현 클래스는 <strong>QuerydslRepositorySupport를 상속</strong> 받아야 하며, <strong>부모의 생성자에 도메인 클래스를 인자</strong>로 넘겨주어야 한다.</p>
<pre><code class="language-java">public class SearchBoardRepositoryImpl extends QuerydslRepositorySupport implements SearchBoardRepository {

    public SearchBoardRepositoryImpl() {
        super(Board.class);
    }
}</code></pre>
<p>메소드를 구현한 코드는 다음과 같다. 
(다음 코드는 &quot;남가람북스&quot; 출판사의 &quot;코드로 배우는 스프링 부트 웹 프로젝트&quot; 에서 참고하였다)</p>
<pre><code class="language-java">@Override
public Page&lt;Object[]&gt; SearchPage(String type, String keyword, Pageable pageable) {

    QBoard board = QBoard.board;
    QReply reply = QReply.reply;
    QMember member = QMember.member;

    JPQLQuery&lt;Board&gt; jpqlQuery = from(board);
    jpqlQuery.leftJoin(member).on(board.writer.eq(member));
    jpqlQuery.leftJoin(reply).on(reply.board.eq(board));

    JPQLQuery&lt;Tuple&gt; tuple = jpqlQuery.select(board, member, reply.count());

    BooleanBuilder booleanBuilder = new BooleanBuilder();
    BooleanExpression expression = board.id.gt(0L);
    booleanBuilder.and(expression);

    if(type != null) {
        String[] typeArr = type.split(&quot;&quot;);
        BooleanBuilder conditionBuilder = new BooleanBuilder();

        for (String t : typeArr) {
            switch (t) {
                case &quot;t&quot;:
                    conditionBuilder.or(board.title.contains(keyword));
                    break;
                case &quot;c&quot;:
                    conditionBuilder.or(board.content.contains(keyword));
                    break;
                case &quot;w&quot;:
                    conditionBuilder.or(member.email.contains(keyword));
                    break;
                default:
                    break;
            }
        }
        booleanBuilder.and(conditionBuilder);
    }

    tuple.where(booleanBuilder);

    Sort sort = pageable.getSort();

    sort.stream().forEach(order -&gt; {
        Order direction = order.isAscending() ? Order.ASC : Order.DESC;
        String prop = order.getProperty();

        PathBuilder orderByExpression = new PathBuilder(Board.class, &quot;board&quot;);
        tuple.orderBy(new OrderSpecifier(direction, orderByExpression.get(prop)));
        });

    tuple.groupBy(board);

    tuple.offset(pageable.getOffset());
    tuple.limit(pageable.getPageSize());

    List&lt;Tuple&gt; result = tuple.fetch();

    long count = tuple.fetchCount();

    return new PageImpl&lt;Object[]&gt;(result.stream().map(t -&gt; t.toArray()).collect(Collectors.toList()),
                                        pageable, count);
}</code></pre>
<p>Criteria를 사용한 복잡한 동적 쿼리 코드보다는 조금 나아진 느낌이지만 여전히 복잡하고 가독성이 떨어져 보인다. 
기존의 SQL query들과 다르게 from절로 시작해야만 한다는 단점도 존재한다. 이를 조금 더 가독성이 좋고, 재사용성을 늘려 유지보수성이 좋게 만드는 방법이 없을까 고민해보았다. </p>
<h2 id="jpaqueryfactory-와-booleanexpression을-활용한-where절-orderby절-parameter-방식">JPAQueryFactory 와 BooleanExpression을 활용한 where절, orderBy절 parameter 방식</h2>
<p>위의 코드를 리팩토링 해보자.
필자는 이를 JPAQueryFactory를 통한 메서드 체인 방식으로 querydsl 쿼리를 작성해 볼 것이다.</p>
<pre><code class="language-java">public interface BoardRepositoryCustom {

    Page&lt;Object[]&gt; searchPage(String type, String keyword, Pageable pageable);

}</code></pre>
<p>인터페이스를 추가로 만들어주고 구현할 기능을 마찬가지로 작성한다.</p>
<pre><code class="language-java">public interface BoardRepository extends JpaRepository&lt;Board, Long&gt;, BoardRepositoryCustom {}</code></pre>
<p>JpaRepository를 상속받은 repository interface에서 이를 상속받는다.</p>
<p>Spring으로 부터 JpaQueryFactory를 주입 받는 방식을 사용할 것이다. <strong>@SpringBootApplicaton</strong> annotation이 붙은 <strong>애플리케이션 부트스트랩 클래스</strong>에 <strong>@Bean</strong> annotation을 통해 사용자 지정 의존성 주입 코드를 추가해준다.</p>
<pre><code class="language-java">@EnableJpaAuditing
@SpringBootApplication
public class CommentBoardApplication {

    public static void main(String[] args) {
        SpringApplication.run(CommentBoardApplication.class, args);
    }

    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager em){
        return new JPAQueryFactory(em);
    }

}</code></pre>
<p>그 후 BoardRepositoryCustom 인터페이스 구현 클래스에서 이를 주입받고 사용한다. 리팩토링한 코드는 다음과 같다.</p>
<pre><code class="language-java">import static HCY.CommentBoard.entity.QBoard.board;
import static HCY.CommentBoard.entity.QMember.member;
import static HCY.CommentBoard.entity.QReply.reply;
@RequiredArgsConstructor
public class BoardRepositoryImpl implements BoardRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    @Override
    public Page&lt;Object[]&gt; searchPage(String type, String keyword, Pageable pageable) {

        QueryResults&lt;Tuple&gt; queryResult = queryFactory
                .select(board, member, reply.count())
                .from(board)
                .leftJoin(member).on(board.writer.eq(member))
                .leftJoin(reply).on(reply.board.eq(board))
                .where(
                        board.id.gt(0L),
                        titleEq(type, keyword),
                        contentEq(type, keyword),
                        writerEq(type, keyword)
                )
                .orderBy(
                        getOrderSpecifier(pageable.getSort())
                                .stream().toArray(OrderSpecifier[]::new)
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .groupBy(board)
                .fetchResults();

        List&lt;Tuple&gt; content = queryResult.getResults();
        long totalCount = queryResult.getTotal();

        return new PageImpl&lt;Object[]&gt;(content.stream().map(tuple -&gt; tuple.toArray()).collect(Collectors.toList()),
                                        pageable, totalCount);
    }
}</code></pre>
<p>QuerydslRepositorySupport 클래스를 사용할 때보다 searchPage 메서드가 한층 간결해 졌음을 볼 수 있다. Spring으로 부터 주입받은 JPAQueryFactory를 활용해서 query를 마치 sql query를 작성하듯 <strong>메서드 체인</strong>으로 작성 하였다. 여기서 눈여겨 볼 점은 where절과 orderBy에 존재하는 메서드들인데 이는 <strong>다른 메서드들에서 재사용하기 쉽도록 따로 메서드로 작성</strong>한다.</p>
<pre><code class="language-java">private BooleanExpression titleEq(String type, String keyword) {
        if(type != null) return type.contains(&quot;t&quot;) ? board.title.containsIgnoreCase(keyword) : null;
        else return null;
}

private BooleanExpression contentEq(String type, String keyword) {
        if(type != null) return type.contains(&quot;c&quot;) ? board.content.containsIgnoreCase(keyword) : null;
        else return null;
}

private BooleanExpression writerEq(String type, String keyword) {
        if(type != null) return type.contains(&quot;w&quot;) ? board.writer.name.containsIgnoreCase(keyword) : null;
        else return null;
}

private List&lt;OrderSpecifier&gt; getOrderSpecifier(Sort sort) {
        List&lt;OrderSpecifier&gt; orders = new ArrayList&lt;&gt;();
        // Sort
        sort.stream().forEach(order -&gt; {
            Order direction = order.isAscending() ? Order.ASC : Order.DESC;
            String prop = order.getProperty();
            PathBuilder orderByExpression = new PathBuilder(Board.class, &quot;board&quot;);
            orders.add(new OrderSpecifier(direction, orderByExpression.get(prop)));
        });
        return orders;
}</code></pre>
<p>BooleanExpression을 통해 여러 조건의 where절을 합칠 수 있으며, pageable 에 존재하는 Sort도 OrderSpecifier를 통해 한꺼번에 처리할 수 있다.
만약 다른 조건으로 검색하는 새로운 메서드를 작성한다면 위의 코드를 재사용 할 수 있어 개발 효율이 높아진다.</p>
<h2 id="결론">결론</h2>
<p>공부를 거듭하며 객체지향적인 코드가 무엇일까 고민을 많이하고 적용하려고 노력하고 있다. 이를 위한 기본은 <strong>코드의 가독성과 재사용성을 용이하게 하고, 유지보수성을 높이는 방법</strong>이라고 생각한다. 이번 포스트 작성을 통해 Querydsl을 활용해 동적 쿼리를 작성하는 법과 유연한 소프트웨어를 추구하는 방법을 정리해 볼 수 있는 계기가 된 것 같다. 블로그의 첫 단추를 채웠으니 앞으로 공부 했던 내용들을 틈틈히 포스트 해봐야겠다!</p>
]]></description>
        </item>
    </channel>
</rss>