<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>wj-dominic.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 31 Aug 2022 16:28:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>wj-dominic.log</title>
            <url>https://images.velog.io/images/wj-dominic/profile/14d8c4bc-2bcc-4911-b27b-1445d2f44f0c/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. wj-dominic.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/wj-dominic" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[@Transactional과 Spring AOP self-invocation]]></title>
            <link>https://velog.io/@wj-dominic/Transactional%EA%B3%BC-Spring-AOP-self-invocation</link>
            <guid>https://velog.io/@wj-dominic/Transactional%EA%B3%BC-Spring-AOP-self-invocation</guid>
            <pubDate>Wed, 31 Aug 2022 16:28:31 GMT</pubDate>
            <description><![CDATA[<p>멘토링 과정에서 질문으로 받았던 @Transactional의 사용과 관련된 내용에 대해 정리하려고 한다. 질문에 내용은 Spring AOP와 self-invocation에 관련된 내용이었으며 해당 상황을 코드로 표현하면 다음과 같다.</p>
<pre><code class="language-java">@Service
public class UserService {
    @Transactional
    public User findUserByEmail(String email) {
        return userMapper.findUserByEmail(email);    
    }

    public void loginUser(LoginUser loginUser) {
        User user = findUserByEmail(loginUser.getEmail());
        .. 생략 ..
    }
}</code></pre>
<p>위 코드는 현재 진행 중인 프로젝트의 과거 버전에 해당하는데 질문 내용에 부합되어 이를 통해 내용을 살펴보려 한다.</p>
<p>코드를 보면 loginUser에서 호출하는 findUserByEmail 메서드에 @Transactional annotation을 확인할 수 있다. 하지만 loginUser에는 아무런 annotation이 없는 상태다. 이때 findUserByEmail에 트랜잭션이 적용될 수 있는지가 질문이었고 대답을 하지 못했다.</p>
<p>결론을 먼저 이야기하자면 프록시 방식의 Spring AOP에서 findUserByEmail에는 트랜잭션이 적용되지 않는다.</p>
<h2 id="-원인"># 원인</h2>
<p>Spring 공식 문서(<a href="https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations)%EC%97%90%EC%84%9C">https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations)에서</a> @Transactional을 살펴보면 다음 내용을 확인할 수 있다. </p>
<blockquote>
<p>💡 In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime even if the invoked method is marked with . Also, the proxy must be fully initialized to provide the expected behavior, so you should not rely on this feature in your initialization code — for example, in a method.@Transactional @PostConstruct</p>
</blockquote>
<p>프록시 모드에서는 외부 메서드 호출 시에만 프록시가 동작하며 self-invocation(프록시 대상 객체 내에서 대상 객체의 다른 메서드를 호출하는 것) 과정에서는 실제 트랜잭션으로 이어지지 않는다. self-invocation을 예제에서 살펴보면 loginUser 메서드 내에서 findUserByEmail 메서드를 호출하는 것을 의미한다. 즉, loginUser에서 findUserByEmail을 호출하는 경우에는 @Transactional이 적용되지 않는 것이다.</p>
<p>그럼 loginUser 메서드의 @Transactional 적용 여부에 따라 프록시를 통한 호출이 어떤 차이를 보이는지 콜스택을 통해 살펴보자.</p>
<p><strong>## loginUser 메서드에 @Transactional 적용 O</strong></p>
<ul>
<li>loginUser 메서드가 호출되는 과정에서 TransactionInterceptor를 통해 트랜잭션 처리가 적용된다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/e37d3845-5868-4873-b507-a307da646b66/image.png" alt=""></li>
</ul>
<p><strong>## loginUser 메서드에 @Transactional 적용 X</strong></p>
<ul>
<li>findUserByEmail이 호출되기까지 콜스택을 살펴봐도 트랜잭션과 관련된 프록시 처리가 적용되지 않았음을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/0a5a291f-1950-431e-9c5e-7d6f5f756a38/image.png" alt=""></li>
</ul>
<h2 id="-해결-방법"># 해결 방법</h2>
<p><strong>## AopContext 사용</strong></p>
<pre><code class="language-java">public class UserService {
    @Transactional
    public User findUserByEmail(String email) {
        return userMapper.findUserByEmail(email);    
    }

    public void loginUser(LoginUser loginUser) {
        User user = ((UserService)AopContext.currentProxy()).findUserByEmail(loginUser.getEmail());
        .. 생략 ..
    }
}</code></pre>
<p><strong>## self injection 객체 사용</strong></p>
<pre><code class="language-java">public class UserService {
    @Autowired ApplicationContext applicationContext;
    private UserService self;

    @PostConstructor
    private void init() {
        self = applicationContext.getBean(UserService.class);
    }

    @Transactional
    public User findUserByEmail(String email) {
        return userMapper.findUserByEmail(email);    
    }

    public void loginUser(LoginUser loginUser) {
        User user = self.findUserByEmail(loginUser.getEmail());
        .. 생략 ..
    }
}</code></pre>
<p>위 두 가지 방식을 사용하면 findUserByEmail 메서드 호출 과정에서 TransactionInterceptor를 거치는 것을 확인할 수 있다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/wj-dominic/post/20b63b63-3fe0-498a-8a9d-531395521450/image.png" alt=""></li>
</ul>
<p><strong>## AspectJ</strong>
마지막으로 Spring AOP의 Weaving 방식을 AspectJ Weaving 방식으로 바꾸는 방법이 있는데 공식 문서를 보면 이 방법을 권장한다. 간단히 설명하면 Spring AOP 방식과 다르게 바이트 코드를 직접 조작하기 때문에 self-invocation이 발생하지 않는다.</p>
<h2 id="-마무리"># 마무리</h2>
<p>지금까지 프록시 방식의 AOP에서 발생하는 self-invocation과 이로 인해 발생하는 @Transaction이 적용되지 않는 문제에 대해 살펴보았다. 해결 방법 중 AspectJ를 간략하게 설명했는데 추후 포스팅에서 Spring의 AOP 기능 제공 방식에 대해 좀 더 깊이 공부하고 정리하는 시간을 갖도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[http와 소켓의 관계를 살펴보자]]></title>
            <link>https://velog.io/@wj-dominic/tcp-http-socket</link>
            <guid>https://velog.io/@wj-dominic/tcp-http-socket</guid>
            <pubDate>Mon, 01 Aug 2022 14:26:45 GMT</pubDate>
            <description><![CDATA[<p>최근 스터디 모임에서 주소창에 <a href="http://www.naver.com%EC%9D%84">www.naver.com을</a> 입력하면 일어나는 일에 대해 발표를 진행했다. http와 tcp를 연결하여 설명하는 과정에서 브라우저와 웹 서버는 각자의 소켓을 생성하고 연결을 한다는 내용을 언급했고 http는 연결을 유지하지 않는데 왜 연결지향인 소켓을 만드는지 질문을 받았다.</p>
<p>준비 과정에서 http, 소켓으로 검색한 내용을 봤을 때, 마치 http는 비연결지향이기 때문에 소켓을 사용하지 않는 것처럼 느껴지는 글들을 꽤 보았기에 충분히 오해가 생길 수 있다는 생각이 들었고 이 부분을 정리하기 위해 Tomcat의 코드를 활용하여 http 역시 tcp를 기반으로 동작하는 소켓 통신임을 확인하고자 한다. 내용을 진행하기 전에 _<strong>소켓은 통신을 위한 인터페이스, http는 프로토콜</strong>_임을 인지하자.</p>
<h3 id="-http-통신-흐름"># http 통신 흐름</h3>
<p>http는 3-way handshake(TCP Established) 후 요청/응답을 주고받고 4-way handshake(TCP Terminated)를 거치는 흐름을 갖는다. 1.0 버전의 경우 매 요청마다 연결 수립, 종료 과정을 거치며 1.1버전은 keep-alive 기능을 통해 세션을 일정 시간 유지할 수 있다.</p>
<p align="center">
  <img src="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw77D5%2FbtrysCLUmNX%2FCrTBAmxrKsSNyWA5VeYimk%2Fimg.webp" alt="출처: https://code-lab1.tistory.com/196?category=1269341"/>
출처: <a href="url">https://code-lab1.tistory.com/196?category=1269341</a>
</p> 

<h3 id="-소켓이란"># 소켓이란?</h3>
<p>소켓은 원격지에 존재하는 두 프로세스가 서로 통신을 하기 위한 인터페이스이다.</p>
<h3 id="-소켓-프로그래밍-흐름"># 소켓 프로그래밍 흐름</h3>
<p>클라이언트와 통신을 위해 서버는 다음 과정을 거친다. socket()을 통해 생성되는 소켓은 서버에서 연결 처리를 위해 생성하는 소켓이고 실제 클라이언트와 통신에 사용되는 소켓은 클라이언트의 연결이 발생하면 accept()를 통해 얻게 된다.</p>
<p align="center">
  <img src="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrUyPL%2FbtqVzRam1o8%2FGHLxbczS6smatE4ob7raYk%2Fimg.png" alt="출처: https://sean.tistory.com/93"/>
출처: <a href="url">https://sean.tistory.com/93</a>
</p> 

<h3 id="-테스트-환경"># 테스트 환경</h3>
<p>포스팅 진행을 위해 진행했던 Spring Boot 프로젝트에서 디버깅을 통해 Tomcat 코드를 살펴보고 실제 송/수신되는 패킷은 와이어샤크를 통해 확인한다.</p>
<p>Tomcat의 소켓 생성 부분에 bp(break point) 설정을 하기 위해 우선 컨트롤러에서 응답을 리턴하는 코드에 bp를 걸고 리턴 이후 흐름을 추적한 후 socket 생성하는 코드를 확인할 수 있었다.</p>
<h3 id="-tomcat-코드-확인하기"># Tomcat 코드 확인하기</h3>
<p>먼저 소켓에 생성과 연결 준비 부분은 Tomcat의 NioEndpoint 클래스의 initServerSocket 메서드에서 확인할 수 있고 코드를 따라가면 실제 소켓을 관리하고 다루는 객체는 sun.nio.ch 패키지의 ServerSocketChannelImpl 객체임을 알 수 있다.</p>
<pre><code class="language-java">// https://github.com/apache/tomcat/blob/main/java/org/apache/tomcat/util/net/NioEndpoint.java
protected void initServerSocket() throws Exception {
        if (getUseInheritedChannel()) {
           ...
        } else {
            serverSock = ServerSocketChannel.open(); // 소켓 생성
            socketProperties.setProperties(serverSock.socket());
            InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
            serverSock.bind(addr, getAcceptCount()); // bind, listen
        }
        serverSock.configureBlocking(true); //mimic APR behavior
}</code></pre>
<p>초기화를 거치면 Tomcat은 Acceptor 스레드(<a href="https://github.com/apache/tomcat/blob/main/java/org/apache/tomcat/util/net/Acceptor.java)%EB%A5%BC">https://github.com/apache/tomcat/blob/main/java/org/apache/tomcat/util/net/Acceptor.java)를</a> 만들고 루프를 돌면서 연결되는 세션에 대해 accept 처리를 한다. 이때 실제 accept 처리는 sun.nio.ch 패키지의 ServerSocketChannelImpl 클래스의 implAccept 메서드에서 native 함수인 Net.accept를 통해 처리된다.</p>
<pre><code class="language-java">// sun.nio.ch 패키지의 ServerSocketChannelImpl 클래스
public SocketChannel accept() throws IOException {
        int n = 0;
        FileDescriptor newfd = new FileDescriptor();
        SocketAddress[] saa = new SocketAddress[1];

        acceptLock.lock();
        try {
            boolean blocking = isBlocking();
            try {
                begin(blocking);
                n = implAccept(this.fd, newfd, saa);
                if (blocking) {
                    while (IOStatus.okayToRetry(n) &amp;&amp; isOpen()) {
                        park(Net.POLLIN);
                        n = implAccept(this.fd, newfd, saa);
                    }
                }
            } finally {
                end(blocking, n &gt; 0);
                assert IOStatus.check(n);
            }
        } finally {
            acceptLock.unlock();
        }

        if (n &gt; 0) {
            return finishAccept(newfd, saa[0]);
        } else {
            return null;
        }
}

private int implAccept(FileDescriptor fd, FileDescriptor newfd, SocketAddress[] saa)
        throws IOException
    {
        if (isUnixSocket()) {
        ...
        } else {
            InetSocketAddress[] issa = new InetSocketAddress[1];
            int n = Net.accept(fd, newfd, issa); // 연결 요청이 발생하면 클라이언트와 연결된 소켓을 얻는다.
            if (n &gt; 0)
                saa[0] = issa[0];
            return n;
        }
}</code></pre>
<blockquote>
<p>[참고] Acceptor 스레드를 통해 accept를 처리하는 이유는 accept가 블로킹되기 때문이다.</p>
</blockquote>
<p>흐름이 Net.accept에 도달하면 스레드는 블로킹이 되고 다음 라인에 bp를 설정하고 브라우저에서 요청을 전달하면 연결이 수립되고 와이어샤크에서 3-way handshake 과정을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/da785a9d-a7c6-425b-8cec-153085dad46e/image.png" alt=""></p>
<p>연결 종료의 경우 NioEndpoint 클래스의 timeout 메서드에서 확인할 수 있는데, 이 로직에서 일정 시간 요청을 기다리고 요청이 없으면 소켓을 종료할 수 있도록 처리한다.</p>
<pre><code class="language-java">protected void timeout(int keyCount, boolean hasEvents) {
    long now = System.currentTimeMillis();
    // This method is called on every loop of the Poller. Don&#39;t process
    // timeouts on every loop of the Poller since that would create too
    // much load and timeouts can afford to wait a few seconds.
    // However, do process timeouts if any of the following are true:
    // - the selector simply timed out (suggests there isn&#39;t much load)
    // - the nextExpiration time has passed
    // - the server socket is being closed
    if (nextExpiration &gt; 0 &amp;&amp; (keyCount &gt; 0 || hasEvents) &amp;&amp; (now &lt; nextExpiration) &amp;&amp; !close) {
        return;
    }
    int keycount = 0;
    try {
        for (SelectionKey key : selector.keys()) {
            keycount++;
            NioSocketWrapper socketWrapper = (NioSocketWrapper) key.attachment();
            try {
                if (socketWrapper == null) {
                    ... 생략 ...
                } else if (close) {
                    ... 생략 ... 
                } else if ((socketWrapper.interestOps() &amp; SelectionKey.OP_READ) == SelectionKey.OP_READ ||
                                  (socketWrapper.interestOps() &amp; SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) {
                    boolean readTimeout = false;
                    boolean writeTimeout = false;
                    // Check for read timeout
                    if ((socketWrapper.interestOps() &amp; SelectionKey.OP_READ) == SelectionKey.OP_READ) {
                          ... 생략 ...
                       }
                        ... 생략 ...

                    // 이 흐름에서 소켓을 닫게 된다.
                    if (readTimeout || writeTimeout) {
                        key.interestOps(0);
                        // Avoid duplicate timeout calls
                        socketWrapper.interestOps(0);
                        socketWrapper.setError(new SocketTimeoutException());
                        if (readTimeout &amp;&amp; socketWrapper.readOperation != null) {
                            ... 생략 ...
                        } else if (writeTimeout &amp;&amp; socketWrapper.writeOperation != null) {
                            ... 생략 ...
                        } else if (!processSocket(socketWrapper, SocketEvent.ERROR, true)) {                            
                            cancelledKey(key, socketWrapper);
                        }
                    }                        
                    ...
                }
            }
            ...
        }
}</code></pre>
<p>종료 과정에서 발생하는 4-way handshake 또한 확인할 수 있다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/8d4428bd-2ac7-416c-85cb-abf4af356b62/image.png" alt=""></p>
<h3 id="-http는-tcp-기반에서-소켓을-이용해-통신한다"># http는 tcp 기반에서 소켓을 이용해 통신한다</h3>
<p>코드를 살펴본 결과 Tomcat에서 소켓을 사용해 http 요청을 처리한다는 것을 알 수 있다. 다만 프로토콜의 특성에 따라 서버 측에서 요청을 처리 후 연결을 종료한다. 그러므로 http가 tcp 기반에서 소켓을 이용해 통신한다는 사실을 기억하자.</p>
<p>마지막으로 하나의 요청에 대해서 발생하는 패킷의 흐름을 살펴보고 내용을 마무리 한다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/0bf9f1ba-0243-4b1d-acd2-0934e45b15eb/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mybatis column mapping 과정에서 발생한 name conflict 해결 일지]]></title>
            <link>https://velog.io/@wj-dominic/mysql-jdbc-%EC%B9%BC%EB%9F%BC-%EC%9D%B4%EB%A6%84%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%82%BD%EC%A7%88%EA%B8%B0</link>
            <guid>https://velog.io/@wj-dominic/mysql-jdbc-%EC%B9%BC%EB%9F%BC-%EC%9D%B4%EB%A6%84%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%82%BD%EC%A7%88%EA%B8%B0</guid>
            <pubDate>Tue, 21 Jun 2022 18:43:28 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/f-lab-edu/somoim-clone">소모임 클론 프로젝트</a>를 진행 중 self join 쿼리를 수행하고 조회한 결과를 객체에 매핑하는 과정에서 발생한 문제를 정리해 보려 한다.</p>
<p>먼저 조회에 사용된 테이블 구성은 다음과 같다.
<strong>category 테이블</strong></p>
<ul>
<li>사용자가 설정할 수 있는 관심사들의 모음이다.</li>
<li>id, parent, name을 갖는다. parent는 자신의 상위 관심사를 나타낸다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/wj-dominic/post/a8fc0d7d-25e5-4e14-aec7-1cd018357a97/image.png" alt=""></p>
<p><strong>interest 테이블</strong></p>
<ul>
<li>사용자의 관심사가 등록되는 테이블이다.</li>
<li>id, user_id, category_id를 갖는다. category_id는 하위 관심사에 해당하는 id이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/wj-dominic/post/a390a9fb-bf1b-4df6-ac8e-e0426c31d2df/image.png" alt=""></p>
<p>구현하려는 기능은 사용자의 관심사 정보를 조회하는 기능으로 작성한 쿼리는 다음과 같다.</p>
<pre><code class="language-sql">select parent.id, child.id, parent.name, child.name 
from category child 
  join category parent on child.parent = parent.id and child.id in
    (select i.category_id from interest i where i.user_id = 1);</code></pre>
<p>DBeaver를 통해 쿼리를 수행하면 user_id가 1인 사용자의 상위 관심사를 포함한 정보를 얻을 수 있다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/17aed269-6694-4319-8335-448b7a33a15f/image.png" alt=""></p>
<p>쿼리를 확인한 후 spring boot, mybatis 환경에서 해당 쿼리를 수행해서 결과를 리턴하는 api를 만들고 테스트를 진행했다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/70640f91-2142-4ff0-9d93-f4b2bb905e59/image.png" alt=""></p>
<p>UserInterest 객체에 결과를 저장하도록 처리했고 다음과 같이 선언되어 있다.</p>
<pre><code class="language-java">@Getter
@Setter
@Builder
public class UserInterest {
    private Long parentId;
    private Long categoryId;
    private String parentName;
    private String categoryName;
}</code></pre>
<br/>

<p>이해하기 어려운 결과가 나왔다. 원인을 찾기 위해 데이터를 담는 객체의 타입도 바꿔보고 쿼리도 다시 살피고 해봤으나 도저히 알 수가 없었다. 결국은 디버깅을 하게 됐다. InterestMapper의 해당 메서드를 호출하는 위치에 bp를 시작으로 쿼리 수행 후resultset 상태, 도메인 객체에 매핑된 상태 위주로 포인트가 될만한 부분에 bp를 걸면서 천천히 확인을 했고 결국 객체에 데이터가 담길 때 문제가 있음을 확인했다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/ad593a82-c89f-4078-99fd-5a50d2a1c3a9/image.png" alt="">
이정도 콜스택을 따라가보니 데이터를 담을 객체를 생성하고 결과를 가져오는 메소드를 찾았다.</p>
<pre><code class="language-java">private Object createUsingConstructor(...) throws SQLException {
    boolean foundValues = false;
    for (int i = 0; i &lt; constructor.getParameterTypes().length; i++) {     
      String columnName = rsw.getColumnNames().get(i);
      Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
      ...
    }
    return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
  }</code></pre>
<p>코드를 생략해뒀지만 매핑하려는 클래스의 생성자를 사용해서 객체를 생성해 주는 메서드이다. for 문으로 생성자의 파라미터 길이만큼 반복하면서 값을 하나씩 채워준다. typeHandler.getResult 메서드에는 ResultSet과 columnName이 전달되는데 이때 columnName은 조회 대상이 되는 columnName이 들어간다. 여기서는 [id, id, name, name] 순서로 값을 채운다.</p>
<p>첫 번째 id는 parentId에 1이 잘 채워졌다. <img src="https://velog.velcdn.com/images/wj-dominic/post/b660f13e-49ce-4db0-bf9f-b42c1e562b2f/image.png" alt=""></p>
<p>두 번째 id도 1이 나왔고 categoryId에 채워졌다. <img src="https://velog.velcdn.com/images/wj-dominic/post/1c411189-4540-42e7-a7ed-9faaa5da29bb/image.png" alt=""></p>
<p>ResultSet 상태를 더 살펴보기로 한다. 조회된 데이터는 정상이다. ResultSetImpl 객체에서 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/2223a68a-3081-4abe-b256-789198234239/image.png" alt=""></p>
<p>좀 더 들어가서 ResultSet에서 실제 Long 값을 가져오는 부분을 확인했다. columnIndex라는 값을 사용해서 값을 가져오는데 실제 columnIndex - 1 위치의 칼럼의 값을 가져온다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/9925a9af-638a-403c-802f-6352d1a79656/image.png" alt=""></p>
<p>지금까지 내용을 보면 첫 번째 두 번째 모두 ResultSet의 첫 번째 칼럼을 가져왔다고 생각해 볼 수 있겠다. 이제는 columnIndex에 집중해서 살펴보고자 한다.</p>
<p>다시 돌아가서 createUsingConstructor 메서드에 bp를 걸고 쿼리를 수행했다. ResultSetWrapper 객체 내부를 확인하니 resultSet이 있고 그 안에는 ResultSetImpl 객체에 해당하는 delegate가 있다. delegate 안에서 자세한 정보를 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/4f788a17-c707-4d62-a3e1-92ffe8e2b394/image.png" alt=""></p>
<p>columnDefinition이 눈에 띈다. columnDefinition은 현재 비어있지만 칼럼을 읽으면서 갱신될 느낌이다.<img src="https://velog.velcdn.com/images/wj-dominic/post/135cc740-6d92-43e1-a947-9a4faaadb67d/image.png" alt=""></p>
<p>createUsingConstructor 메서드에서 호출되는 getResult를 따라가면 위에 이미지에서 본 getLong을 호출하는 getLong을 만날 수 있다.</p>
<pre><code class="language-java">@Override
public long getLong(String columnName) throws SQLException {
    return getLong(findColumn(columnName));
}</code></pre>
<p>여기서 findColumn을 통해 columnIndex를 얻게 된다. findColumn을 따라가면 columnDefinition에서 index를 찾아오는 코드를 확인할 수 있다.(위에 언급한 useColumnNamesInFindColumn도 같이 사용된다.)</p>
<pre><code class="language-java">Integer index = this.columnDefinition.findColumn(columnName, this.useColumnNamesInFindColumn, 1);</code></pre>
<p>다시 findColumn을 따라가면 columnDefinition을 초기화하게 되는데 조회된 칼럼을 기준으로 columnIndex를 얻도록 데이터를 초기화한다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/70a58131-3ca4-406f-becb-d6c49a9a00b6/image.png" alt=""></p>
<p>columnDefinition은 columnIndex를 담는 4개의 map을 갖는데 Cache 용도의 map만 HashMap이고 나머지는 TreeMap으로 구성되어 있다. findColumn 메서드는 columnToIndexCache - columnLabelToIndex - columnNameToIndex - fullColumnNameToIndex 순서로 index를 찾고 존재하는 칼럼이면 columnToIndexCache에 해당 칼럼 정보를 추가한다.</p>
<p>각 map들을 보면 가장 우선순위가 낮은 fullColumnNameToIndex만 실제 조회하려는 칼럼이 구분돼 있는 걸 알 수 있다. 이 내용만으로도 우선순위가 앞쪽에 있는 map을 통해서 columnIndex를 가져오게 되고, 내가 쿼리에 지정한 p.id, c.id와 p.name, c.name은 서로 구분이 불가능하단 것을 알 수 있다.</p>
<p>그래서 쿼리에서 조회되는 컬럼에 별칭을 붙여 컬럼을 구분 짓도록 변경을 했고</p>
<pre><code class="language-sql">select parent.id as parent_id, child.id, parent.name as parent_name, child.name 
from category child 
  join category parent on child.parent = parent.id and child.id in
    (select i.category_id from interest i where i.user_id = 1);</code></pre>
<p>columnDefiniton을 확인하니 columnLabelToIndex 내용이 앞선 상황과 바뀐 것을 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/ab7b0681-48d2-4901-9e4b-25d55d9c1123/image.png" alt=""></p>
<p>데이터도 역시 정상이었다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/094721e6-05b7-451d-9c60-b9fd59a66826/image.png" alt=""></p>
<p>시간은 걸렸지만 mysql jdbc에서 조회된 데이터를 채우기 위해 어떤 방식으로 클래스 정보를 사용해 객체를 만들고 데이터를 매핑하는지, 데이터를 얻기 위해 칼럼 정보를 어떻게 관리하는지 알 수 있는 시간이 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 데드락(deadlock)을 발생시켜보자]]></title>
            <link>https://velog.io/@wj-dominic/MySQL-Deadlock</link>
            <guid>https://velog.io/@wj-dominic/MySQL-Deadlock</guid>
            <pubDate>Sun, 05 Jun 2022 17:29:58 GMT</pubDate>
            <description><![CDATA[<p>데드락은 서로 다른 트랜잭션들이 서로에 대한 락을 소유한 상태로 대기 상태가 되어 더 이상 요청에 대한 응답을 수행하지 못하는 상황을 말합니다.</p>
<p>여기서는 데드락을 발생시켜보고 MySQL에서 제공하는 정보들을 확인해 보고자 합니다.</p>
<hr>
<h3 id="환경-구성">환경 구성</h3>
<ul>
<li><p>MySQL Version: 도커로 구동한 8.0 버전의 MySQL</p>
</li>
<li><p>테이블: real mysql 서적에서 사용되는 예제 테이블</p>
<pre><code class="language-sql">CREATE TABLE `employees` (
  `emp_no` int NOT NULL,
  `birth_date` date NOT NULL,
  `first_name` varchar(14) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `last_name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `gender` enum(&#39;M&#39;,&#39;F&#39;) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `hire_date` date NOT NULL,
  PRIMARY KEY (`emp_no`),
  KEY `ix_hiredate` (`hire_date`),
  KEY `ix_gender_birthdate` (`gender`,`birth_date`),
  KEY `ix_firstname` (`first_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci STATS_PERSISTENT=0;</code></pre>
</li>
<li><p>innodb_deadlock_detect 설정: Off</p>
<pre><code class="language-sql">set global innodb_deadlock_detect=off;</code></pre>
</li>
<li><p>클라이언트 A: Workbench</p>
</li>
<li><p>클라이언트 B: DBeaver</p>
</li>
<li><p>모니터 클라이언트: 트랜잭션 상태 조회를 위한 별도 세션</p>
<pre><code class="language-sql"># 사용되는 쿼리
SELECT * FROM performance_schema.data_locks;
SELECT * FROM information_schema.INNODB_TRX;</code></pre>
</li>
</ul>
<hr>
<h3 id="데드락-테스트">데드락 테스트</h3>
<ol>
<li>클라이언트 A에서 트랜잭션을 시작하고 select ... for update 문장으로 락을 걸어줍니다.<pre><code class="language-sql"># 클라이언트 A
BEGIN TRANSACTION;
SELECT * FROM employees WHERE gender=&#39;M&#39; AND birth_date=&#39;2020-07-30&#39; FOR UPDATE;</code></pre>
</li>
<li>클라이언트 B에서 트랜잭션을 시작하고 &#39;1&#39;과 같은 쿼리지만 검색 조건을 다르게 하여 수행합니다.<pre><code class="language-sql"># 클라이언트 B
BEGIN TRANSACTION;
SELECT * FROM employees WHERE gender=&#39;F&#39; AND birth_date=&#39;2020-07-31&#39; FOR UPDATE;</code></pre>
</li>
<li>모니터 클라이언트에서 각 트랜잭션의 상태를 조회할 수 있습니다.<pre><code class="language-sql">SELECT * FROM information_schema.INNODB_TRX;</code></pre>
<img src="https://velog.velcdn.com/images/wj-dominic/post/ae3a70fb-5d67-491a-bc3c-0081de3b5dda/image.png" alt=""> 현재는 클라이언트 A(trx_id:6413), 클라이언트 B(trx_id:6415) 모두 RUNNING 상태입니다.</li>
<li>클라이언트 A에서 &#39;2&#39;에 사용된 쿼리로 Lock을 걸어줍니다. 하지만 클라이언트 B로 인해 이미 락이 걸려 있어 대기 상태가 됩니다.<pre><code class="language-sql"># 클라이언트 A
SELECT * FROM employees WHERE gender=&#39;F&#39; AND birth_date=&#39;2020-07-31&#39; FOR UPDATE;</code></pre>
<img src="https://velog.velcdn.com/images/wj-dominic/post/3135ab3c-7344-4d4c-9447-14406ca33bd1/image.png" alt=""></li>
<li>모니터 클라이언트에서 상태를 확인하면 클라이언트 A(trx_id:6413)의 Lock이 대기중임을 알 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/e3845d57-ca10-4ade-bf26-2360d4e71cc1/image.png" alt=""><img src="https://velog.velcdn.com/images/wj-dominic/post/95b1df6d-8c51-45b5-a315-ed3ad74edd8d/image.png" alt=""></li>
<li>클라이언트 B에서 &#39;1&#39;에 사용된 쿼리로 락을 걸어줍니다. 이때, 클라이언트 A로 인해 이미 Lock 걸려 있어 대기 상태가 되고 클라이언트 A, B 모두 대기 상태에 빠지게 됩니다.(※ 진행 과정에서 &#39;1&#39;의 과정부터 다시 수행하게 되어 trx_id가 변경되었습니다.)
<img src="https://velog.velcdn.com/images/wj-dominic/post/13e6113d-fe05-4d09-a70d-2b04241e950c/image.png" alt=""></li>
</ol>
<hr>
<h3 id="데드락-감지-기능-활성">데드락 감지 기능 활성</h3>
<p>MySQL은 데드락을 감지하면 트랜잭션을 롤백해 데드락 상태에서 빠져나갈 수 있도록 기능을 제공합니다. 앞선 테스트에서는 데드락을 발생시키고 락의 상태를 확인하기 위해 기능을 비활성화했지만 이번에는 활성화된 상태에서 데드락이 발생하는 경우를 확인해 보려 합니다.</p>
<ul>
<li>innodb_deadlock_detect 설정: On<pre><code class="language-sql">set global innodb_deadlock_detect=on;</code></pre>
</li>
</ul>
<p>기능을 활성화하고 위에서 진행한 내용을 동일하게 진행하면 데드락이 발생되는 쿼리에서 에러를 보여줍니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/bfc6ac41-6cc6-4692-9e2b-8e2c23a07943/image.png" alt="">이렇게 데드락이 감지된 경우에는 InnoDB 모니터 정보를 통해 내용을 확인할 수 있습니다. (1)과 (2)는 각 트랜잭션을 나타내고 각 트랜잭션의 락 정보 등이 담겨 있으며, 어떤 트랜잭션을 롤백 하여 데드락 상태에서 빠져나갔는지 확인이 가능합니다.</p>
<pre><code class="language-sql">SHOW ENGINE INNODB STATUS;</code></pre>
<pre><code>------------------------
LATEST DETECTED DEADLOCK
------------------------
2022-06-05 16:59:29 139921995212544
*** (1) TRANSACTION:
TRANSACTION 6422, ACTIVE 11 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 4 row lock(s)
MySQL thread id 629, OS thread handle 139921418274560, query id 6566 172.17.0.1 root executing
select * from employees where gender=&#39;F&#39; and birth_date=&#39;2020-07-31&#39; 
LIMIT 0, 1000
for update

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 20 page no 1122 n bits 1272 index ix_gender_birthdate of table `employees`.`employees` trx id 6422 lock_mode X
Record lock, heap no 1203 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 1; hex 01; asc  ;;
 1: len 3; hex 8fc8fe; asc    ;;
 2: len 4; hex 8007a120; asc     ;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 20 page no 1320 n bits 768 index ix_gender_birthdate of table `employees`.`employees` trx id 6422 lock_mode X waiting
Record lock, heap no 695 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 1; hex 02; asc  ;;
 1: len 3; hex 8fc8ff; asc    ;;
 2: len 4; hex 8007a121; asc    !;;


*** (2) TRANSACTION:
TRANSACTION 6423, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 4 row lock(s)
MySQL thread id 561, OS thread handle 139921426728704, query id 6568 172.17.0.1 root executing
/* ApplicationName=DBeaver 21.1.2 - SQLEditor &lt;Script-4.sql&gt; */ select * from employees where gender=&#39;M&#39; and birth_date=&#39;2020-07-30&#39; for update

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 20 page no 1320 n bits 768 index ix_gender_birthdate of table `employees`.`employees` trx id 6423 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 695 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 1; hex 02; asc  ;;
 1: len 3; hex 8fc8ff; asc    ;;
 2: len 4; hex 8007a121; asc    !;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 20 page no 1122 n bits 1272 index ix_gender_birthdate of table `employees`.`employees` trx id 6423 lock_mode X waiting
Record lock, heap no 1203 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 1; hex 01; asc  ;;
 1: len 3; hex 8fc8fe; asc    ;;
 2: len 4; hex 8007a120; asc     ;;

*** WE ROLL BACK TRANSACTION (2)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins를 사용해 CI/CD 환경 구축해 보기(2)]]></title>
            <link>https://velog.io/@wj-dominic/Jenkins%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%B4-%EB%B3%B4%EA%B8%B02</link>
            <guid>https://velog.io/@wj-dominic/Jenkins%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%B4-%EB%B3%B4%EA%B8%B02</guid>
            <pubDate>Sun, 22 May 2022 14:54:23 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@wj-dominic/Jenkins%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%B4-%EB%B3%B4%EA%B8%B01">Jenkins를 사용해 CI/CD 환경 구축해 보기(1)</a></p>
<hr>
<h2 id="github-webhook-설정">Github Webhook 설정</h2>
<p>Github은 repository에서 발생한 변경을 외부 서비스에 알리기 위해 Webhook 기능을 제공합니다. Webhook을 설정하고 해당 이벤트가 발생하면 등록된 URL에 요청을 전달하여 변경을 알립니다.</p>
<p>Webhook은 Github 프로젝트 페이지에서 Settings - Webhooks - Add webhook을 선택하면 설정이 가능합니다. Payload URL에는 요청을 전달할 Jenkins 서버의 주소에 /github-webhook/을 추가하고 pull request를 감지하기 위해 push event에 대한 hook을 설정하여 추가하면 됩니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/ef7dc153-820f-4381-9157-dae2d7ac87fe/image.png" alt=""></p>
<p>설정을 추가하고 등록된 Webhook 목록을 눌러 Recent Deliveries 탭을 확인하면 요청을 Jenkins에 전달한 내역을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/eb5ed824-4caa-48f8-84fe-18fc54dc3f7b/image.png" alt=""></p>
<p>※ 만약 테스트 용도로 로컬에서 Jenkins 환경을 만들었다면 github에서 Jenkins에 접속이 가능하도록 공유기 설정에서 포트 포워딩 설정과 방화벽 설정을 진행해야 테스트가 가능합니다.</p>
<hr>
<h2 id="jenkins에서-github-webhook-적용하기">Jenkins에서 Github Webhook 적용하기</h2>
<p>Github은 repository에서 발생한 변경을 외부 서비스에 알리기 위해 Webhook 기능을 제공합니다. Webhook을 설정하고 해당 이벤트가 발생하면 등록된 URL에 요청을 전달하여 변경을 알립니다.</p>
<p>Jenkins에서는 알림을 받으면 빌드 할 수 있도록 Build Trigger를 설정할 수 있습니다. <a href="https://velog.io/@wj-dominic/Jenkins%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%B4-%EB%B3%B4%EA%B8%B01">1편</a>에서 만든 프로젝트의 구성 화면에서 Build Triggers를 GitHub hook trigger for GITScm polling으로 선택합니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/d2ed3db8-0b4e-4743-bf09-9941552a14ab/image.png" alt=""></p>
<p>다음으로 Pipeline을 설정합니다. Pipeline 스크립트인 Jenkinsfile을 Git에서 가져올 수 있도록 설정하고 Repository URL과 인증 정보, 빌드를 진행할 브랜치를 설정합니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/9161e3fb-7c58-4ac4-9c55-a58c041c2ecf/image.png" alt=""></p>
<p>이렇게 설정하면 Repository 변경 이벤트 발생 시 Jenkinsfile에 정의된 작업을 진행하게 됩니다.</p>
<hr>
<h2 id="github-토큰-발급과-인증-정보-설정">Github 토큰 발급과 인증 정보 설정</h2>
<p>Jenkins에서 Github 접근을 위해 인증 정보 설정이 필요한데, 이를 위해서 Github의 액세스 토큰을 발급받아야 합니다. 발급된 토큰으로 Jenkins에서 인증 정보를 등록하면 Jenkins에서 Github에 접근이 가능합니다.</p>
<p>토큰은 Github의 사용자 - Settings - Developer settings - Personal access tokens에서 Generate new token을 선택해 만들 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/19cb7771-38e8-4cd2-9ec6-413ddcd5aa72/image.png" alt=""></p>
<p>토큰을 만들 때 액세스 범위를 설정해야 하는데 여기서는 repository와 hook에 대해 범위를 지정했습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/9e5c9374-744c-4ffd-908a-191d9f2f7045/image.png" alt=""></p>
<p>설정을 하고 하단의 Generate token 버튼을 누르면 토큰 값이 나타나게 됩니다. 토큰은 한 번만 확인이 가능하니 따로 값을 저장해 두거나 잃어버리면 다시 발급을 받아야 합니다.</p>
<p>토큰을 발급했으면 이제 Jenkins에서 정보를 추가해야 합니다. 인증 정보를 추가하는 페이지에서 Username과 Password를 사용하는 방식을 선택하고 Username에 Github ID를 Password에는 토큰 값을 넣고 저장하면 Jenkins에서 Github에 접근이 가능합니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/ec2f88af-d499-4145-928b-faa5b42d69a7/image.png" alt=""></p>
<hr>
<p>이제 Repository에 push 이벤트를 발생시키면 잠시 뒤 Jenkins에서 빌드를 진행하는 것을 확인할 수 있습니다. 다음 포스팅에는 배포 과정을 정리하도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins를 사용해 CI/CD 환경 구축해 보기(1)]]></title>
            <link>https://velog.io/@wj-dominic/Jenkins%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%B4-%EB%B3%B4%EA%B8%B01</link>
            <guid>https://velog.io/@wj-dominic/Jenkins%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%B4-%EB%B3%B4%EA%B8%B01</guid>
            <pubDate>Mon, 16 May 2022 12:49:56 GMT</pubDate>
            <description><![CDATA[<p>이 글은 소모임을 클론한 프로젝트의 진행에 CI/CD 환경을 적용하기 위한 과정을 기록한 글입니다.</p>
<p>툴은 Jenkins를 사용하며 Docker를 이용해 Jenkins 이미지를 구동하여 환경을 구성합니다.</p>
<p>※ Docker를 통해 Jenkins를 구동하는 과정은 이 글에서는 다루지 않습니다.</p>
<hr>
<h2 id="jenkins-시작하기">Jenkins 시작하기</h2>
<p>Jenkins를 구동하고 최초로 페이지에 접속을 하면 다음과 같은 화면이 나타납니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/b4060991-bbea-4986-94d2-9cfca5536053/image.png" alt=""></p>
<p>안내대로 나타난 경로의 파일을 확인하면 관리자 비밀번호를 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/87e06c2f-a7b9-4bf7-92d7-9eb39d425dca/image.png" alt=""></p>
<p>비밀번호를 입력 후 플러그인 구성과 사용자 계정을 생성하면 Jenkins의 Dashboard를 볼 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/wj-dominic/post/f732953a-aecd-4a7a-b329-589d2f2696c5/image.png" alt=""></p>
<hr>
<h2 id="pipeline-프로젝트-만들고-빌드하기">Pipeline 프로젝트 만들고 빌드하기</h2>
<p>먼저 Dashboard에서 새로운 item을 선택하고 Pipeline 프로젝트를 만들어줍니다. 프로젝트가 생성되면 설정 화면이 나타나고 General 탭에서 필요한 설정을 진행합니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/8bc2afeb-916d-41d4-be3b-bad94c5893d3/image.png" alt=""></p>
<p>GitHub project에 체크 후 프로젝트의 url을 넣어줍니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/8de57557-a51b-4091-adeb-bde1dd81d511/image.png" alt=""></p>
<p>Build Triggers 항목에서 GitHub hook trigger for GITScm polling 항목에 체크합니다. 이 항목을 설정하면 Github의 Webhook 기능을 통해 Jenkins가 프로젝트의 변경을 감지해 CI를 진행할 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/8315bee7-de05-4a93-a163-5d98f02e0613/image.png" alt=""></p>
<p>Pipeline 항목에서 Definition을 Pipeline script from SCM으로 선택하고 Repository URL과 원하는 Branch 정보를 채워줍니다. Github 프로젝트에서 Jenkinsfile을 가져와 적용할 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/b33075b4-9341-4c25-b8df-747a13f6956a/image.png" alt=""></p>
<p>Jenkinsfile을 간단히 설정하여 Repository에 추가합니다.</p>
<pre><code>pipeline {
    agent any

    stages {
        stage(&#39;Test&#39;) {
            steps {
                sh &#39;./gradlew test&#39;  // Test 단계에서 test를 실행합니다.
            }
        }

        stage(&#39;Build&#39;) {
            steps {
                sh &#39;./gradlew clean build&#39; // Build 단계에서 빌드를 진행합니다.
            }
        }
    }
}</code></pre><p>이제 Jenkins에서 빌드를 할 수 있습니다. 지금 빌드를 선택하면 Jenkins에서 빌드를 진행하게 됩니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/c6e22072-05b9-4c8c-83a9-9d74205e37b7/image.png" alt=""></p>
<p>첫 빌드 시도에서 Test 과정이 실패했습니다. 실패한 항목에 마우스를 올리면 Log를 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/51b52fdc-37ba-40cf-babd-70063e3a8c3a/image.png" alt=""></p>
<p>로그 내용을 보면 gradlew를 실행하는데 권한이 없다고 나옵니다.</p>
<pre><code>git ls-tree HEAD</code></pre><p>위 명령을 사용해서 gradlew의 권한을 확인하면 644로 실행 권한이 빠져 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/23911132-f5ae-4367-b3a2-68270bfce514/image.png" alt=""></p>
<p>권한을 부여하기 위해 다음 명령을 실행하고 커밋, 푸시를 진행합니다.</p>
<pre><code>git update-index --add --chmod=+x gradlew</code></pre><p>다시 빌드를 진행하면 성공적으로 완료되는 것을 확인할 수 있습니다.
<img src="https://velog.velcdn.com/images/wj-dominic/post/e19cc606-6feb-40e2-b597-ba1365e7b6ac/image.png" alt=""></p>
<hr>
<p>여기까지 Jenkins Pipeline을 통해 빌드를 진행해 봤습니다. 편의상 포스팅의 내용을 나눠 다음 포스팅에서 Github Webhook을 적용해 Pull Request가 발생하면 Jenkins에서 빌드를 진행하도록 해보겠습니다.</p>
]]></description>
        </item>
    </channel>
</rss>