<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>A Lamp Fairy</title>
        <link>https://velog.io/</link>
        <description>Coding Duck</description>
        <lastBuildDate>Sun, 03 Aug 2025 13:09:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>A Lamp Fairy</title>
            <url>https://images.velog.io/images/jduckling_1024/profile/5d93e4cb-6728-4023-9979-0bc5d29f592d/오구2.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. A Lamp Fairy. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jduckling_1024" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[OpenAI 'you must provide a model parameter' 400 에러 원인 파악 (feat. application/json)]]></title>
            <link>https://velog.io/@jduckling_1024/OpenAI-you-must-provide-a-model-parameter-400-%EC%97%90%EB%9F%AC-%EC%9B%90%EC%9D%B8-%ED%8C%8C%EC%95%85-feat.-applicationjson</link>
            <guid>https://velog.io/@jduckling_1024/OpenAI-you-must-provide-a-model-parameter-400-%EC%97%90%EB%9F%AC-%EC%9B%90%EC%9D%B8-%ED%8C%8C%EC%95%85-feat.-applicationjson</guid>
            <pubDate>Sun, 03 Aug 2025 13:09:02 GMT</pubDate>
            <description><![CDATA[<p>1년 넘게 잘 동작하던 RestTemplate을 통해 <a href="https://platform.openai.com/docs/api-reference/introduction">OpenAI API</a>를 call 하는 로직에서 아래와 같은 400 에러가 갑자기 뜨게 되었다.</p>
<pre><code>BadRequestError: Error code: 400 - {&#39;error&#39;: {&#39;message&#39;: &#39;you must provide a model parameter&#39;, &#39;type&#39;: &#39;invalid_request_error&#39;, &#39;param&#39;: null, &#39;code&#39;: null}}</code></pre><p>처음엔 외부 API 서버 측 일시적인 문제겠거니... 하고 우선 모니터링에 더 집중했다. (건드린 사람이 없었고 코드에서도 이상이 없어보였으니까!!!)
그런데 잠깐... OpenAI status도 특이사항 없었고 여러 가지 상황을 가정해보았는데 모두 말이 안 되었다. 400이면 클라이언트(=나) 잘못인데... 다시 분석해보았다.</p>
<br />

<h1 id="1-언제-you-must-provide-a-model-parameter가-뜨는가">1. 언제 you must provide a model parameter가 뜨는가?</h1>
<h2 id="1-body에-model-값을-보내주지-않았을-때">1. body에 model 값을 보내주지 않았을 때</h2>
<p>메시지 그대로다. 정말로 model을 null로 보내주거나 아예 보내주지 않으면 해당 메시지가 뜨게 된다. OpenAI에서 지원하지 않는 모델명을 설정하여 보내줄 경우 다른 메시지가 뜬다.</p>
<h2 id="2-header에-content-type을-applicationjson으로-설정하지-않았을-때">2. header에 Content-Type을 application/json으로 설정하지 않았을 때</h2>
<p>header에 Content-Type을 아예 누락시키거나 application/json으로 정확히 설정하지 않으면 해당 메시지가 뜨게 된다 (오타도 조심하자).</p>
<blockquote>
<p>리서치 및 실험 결과 위 두 가지 케이스를 발견하였는데 더 있을 수도...</p>
</blockquote>
<br />

<h1 id="2-resttemplate-객체-생성-및-초기화-부분-확인">2. RestTemplate 객체 생성 및 초기화 부분 확인</h1>
<blockquote>
<p><strong>여기!</strong> 를 검색하면 원인 코드를 좀 더 빠르게 확인할 수 있을 것이다.</p>
</blockquote>
<p>model을 명시적으로 보내주지 않는 케이스가 아니라면 Content-Type을 header에 설정하는 부분을 살펴볼 필요가 있다. (Spring Boot 기준 보통 RestTemplate을 bean으로 관리하거나 서비스 생성자 레벨에서 초기화하여 사용할테니 그 부분을 유심히 살펴보면 될 것 같다.) 본인은 아래와 같이 RestTemplate를 초기화하여 사용했었다.</p>
<pre><code class="language-java">HttpHeaders headers = new HttpHeaders();
       headers.setContentType(MediaType.APPLICATION_JSON);
       headers.setBearerAuth(API_KEY);

this.restTemplate = restTemplateBuilder
       .rootUri(&quot;https://api.openai.com&quot;)
       .interceptors((request, body, execution) -&gt; {
               request.getHeaders().addAll(headers);
               return execution.execute(request, body);
       })

                ...

       .build();</code></pre>
<br />

<p>대충 봤을 때는 이상 없어보이는데 결론부터 이야기하자면 <code>Content-Type application/json</code>이 두 번 설정된다. 그 이유가 궁금하여 내부 코드를 살짝 열어보았다.</p>
<br />

<p><strong>RestTemplateBuilder.java</strong></p>
<pre><code class="language-java">public RestTemplate build() {
    return configure(new RestTemplate());
}</code></pre>
<p>RestTemplateBuilder를 통해 RestTemplate()을 만들게 되는 경우 RestTemplate() 기본 생성자를 호출한다.</p>
<br />

<p><strong>RestTemplate.java</strong></p>
<pre><code class="language-java">public RestTemplate() {
        ...
        if (jackson2Present) {
            this.messageConverters.add(new MappingJackson2HttpMessageConverter());
        }
        ...
    }</code></pre>
<p>RestTemplate 생성자의 일부다. jackson2Present가 true인 조건은 ObjectMapper 클래스의 존재 여부인데 본인 프로젝트에서는 사용하고 있었기 때문에 <code>messageConverters</code>에 <code>MappingJackson2HttpMessageConverter</code>가 추가된다.</p>
<br />


<p><strong>MappingJackson2HttpMessageConverter.java</strong></p>
<pre><code class="language-java">/**
 * Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
* You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
* @see Jackson2ObjectMapperBuilder#json()
*/
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
    super(objectMapper, MediaType.APPLICATION_JSON, new MediaType(&quot;application&quot;, &quot;*+json&quot;));
}

protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
    this(objectMapper);
    setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}</code></pre>
<p>MappingJackson2HttpMessageConverter 생성자를 따라가보면 supportedMediaTypes에 <code>application/json</code>, <code>application.*+json</code>이 추가되는 걸 볼 수 있다. 이제 이 값들이 어떻게 사용되는지 알아보자.</p>
<br />

<p><strong>RestTemplate.java</strong></p>
<pre><code class="language-java">@Override
@SuppressWarnings(&quot;unchecked&quot;)
public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
    super.doWithRequest(httpRequest);
    Object requestBody = this.requestEntity.getBody();
    if (requestBody == null) {
        HttpHeaders httpHeaders = httpRequest.getHeaders();
        HttpHeaders requestHeaders = this.requestEntity.getHeaders();
        if (!requestHeaders.isEmpty()) {
            requestHeaders.forEach((key, values) -&gt; httpHeaders.put(key, new ArrayList&lt;&gt;(values)));
        }
        if (httpHeaders.getContentLength() &lt; 0) {
            httpHeaders.setContentLength(0L);
        }
    }
    else {
        Class&lt;?&gt; requestBodyClass = requestBody.getClass();
        Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
                ((RequestEntity&lt;?&gt;)this.requestEntity).getType() : requestBodyClass);
        HttpHeaders httpHeaders = httpRequest.getHeaders();
        HttpHeaders requestHeaders = this.requestEntity.getHeaders();
        MediaType requestContentType = requestHeaders.getContentType();
        for (HttpMessageConverter&lt;?&gt; messageConverter : getMessageConverters()) {
            if (messageConverter instanceof GenericHttpMessageConverter) {
                GenericHttpMessageConverter&lt;Object&gt; genericConverter =
                        (GenericHttpMessageConverter&lt;Object&gt;) messageConverter;
                if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
                    if (!requestHeaders.isEmpty()) {
                        requestHeaders.forEach((key, values) -&gt; httpHeaders.put(key, new ArrayList&lt;&gt;(values)));
                    }
                    logBody(requestBody, requestContentType, genericConverter);
                    genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
                    return;
                }
            }
            else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
                if (!requestHeaders.isEmpty()) {
                    requestHeaders.forEach((key, values) -&gt; httpHeaders.put(key, new ArrayList&lt;&gt;(values)));
                }
                logBody(requestBody, requestContentType, messageConverter);
                ((HttpMessageConverter&lt;Object&gt;) messageConverter).write(
                        requestBody, requestContentType, httpRequest); // 여기!
                return;
            }
        }
        String message = &quot;No HttpMessageConverter for &quot; + requestBodyClass.getName();
        if (requestContentType != null) {
            message += &quot; and content type \&quot;&quot; + requestContentType + &quot;\&quot;&quot;;
        }
        throw new RestClientException(message);
    }
}</code></pre>
<p>REST API 요청을 하게 되면 결국 위 메소드를 호출하게 되는데 여기서 messageConverter의 write 메소드에 집중해보자.</p>
<br />


<p><strong>AbstractHttpMessageConverter.java</strong></p>
<pre><code class="language-java">@Override
public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
        throws IOException, HttpMessageNotWritableException {

    final HttpHeaders headers = outputMessage.getHeaders();
    addDefaultHeaders(headers, t, contentType); // 여기!

    if (outputMessage instanceof StreamingHttpOutputMessage) {
        StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
        streamingOutputMessage.setBody(outputStream -&gt; writeInternal(t, new HttpOutputMessage() {
            @Override
            public OutputStream getBody() {
                return outputStream;
            }
            @Override
            public HttpHeaders getHeaders() {
                return headers;
            }
        }));
    }
    else {
        writeInternal(t, outputMessage);
        outputMessage.getBody().flush();
    }
}

/**
 * Add default headers to the output message.
 * &lt;p&gt;This implementation delegates to {@link #getDefaultContentType(Object)} if a
 * content type was not provided, set if necessary the default character set, calls
 * {@link #getContentLength}, and sets the corresponding headers.
 * @since 4.2
 */
protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException {
    if (headers.getContentType() == null) {
        MediaType contentTypeToUse = contentType;
        if (contentType == null || !contentType.isConcrete()) {
            contentTypeToUse = getDefaultContentType(t); // 여기!
        }
        else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
            MediaType mediaType = getDefaultContentType(t);
            contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
        }
        if (contentTypeToUse != null) {
            if (contentTypeToUse.getCharset() == null) {
                Charset defaultCharset = getDefaultCharset();
                if (defaultCharset != null) {
                    contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
                }
            }
            headers.setContentType(contentTypeToUse); // 여기!
        }
    }
    if (headers.getContentLength() &lt; 0 &amp;&amp; !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
        Long contentLength = getContentLength(t, headers.getContentType());
        if (contentLength != null) {
            headers.setContentLength(contentLength);
        }
    }
}</code></pre>
<p>getDefaultContentType() 를 통해 contentType을 가져와서 헤더에 Content-Type을 설정해준다. </p>
<br />


<p><strong>HttpHeaders.java</strong></p>
<pre><code class="language-java">/**
 * Set the {@linkplain MediaType media type} of the body,
 * as specified by the {@code Content-Type} header.
 */
public void setContentType(@Nullable MediaType mediaType) {
    if (mediaType != null) {
        Assert.isTrue(!mediaType.isWildcardType(), &quot;Content-Type cannot contain wildcard type &#39;*&#39;&quot;);
        Assert.isTrue(!mediaType.isWildcardSubtype(), &quot;Content-Type cannot contain wildcard subtype &#39;*&#39;&quot;);
        set(CONTENT_TYPE, mediaType.toString());
    }
    else {
        remove(CONTENT_TYPE);
    }
}

@Override
public void set(String headerName, @Nullable String headerValue) {
    this.headers.set(headerName, headerValue);
}

final MultiValueMap&lt;String, String&gt; headers; // 여기!
</code></pre>
<p>그리고 headers는 MultiValueMap이었다. 따라서 중복 값이 들어갈 수 있었던 것이다. 실제로 postman에서도 Content-Type을 중복으로 요청하였더니 진짜 동일한 메시지가 뜨는 것을 확인할 수 있었다!!!</p>
<br />

<h1 id="3-정리를-마치며">3. 정리를 마치며</h1>
<p>오랫동안 정상 동작했던 코드라도 너무 믿지 말고 외부 Call이 끼어있다면 다시 한 번 확인해보자. 400 에러면 본인 잘못일 확률이 매우 높다...
오류 찾기 전까지는 매우 답답했지만 원인을 알아내고 RestTemplate 내부 코드를 열어보는 과정은 재미있었다 🙂</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[제 7회 Kakao Tech Meet 후기]]></title>
            <link>https://velog.io/@jduckling_1024/%EC%A0%9C-7%ED%9A%8C-Kakao-Tech-Meet-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@jduckling_1024/%EC%A0%9C-7%ED%9A%8C-Kakao-Tech-Meet-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 23 Dec 2024 08:52:35 GMT</pubDate>
            <description><![CDATA[<p>이번에 좋은 기회로 <a href="https://tech.kakao.com/posts/669">제 7회 Kakao Tech Meet</a>에 참석하게 되었다.
<br /></p>
<p>주제는 크게 AI를 활용한 프로젝트 및 프론트엔드에 TDD를 적용한 사례 소개였으며 백엔드 개발자인 본인에게도 도움이 될 만한 주제라고 생각했는데 이유는 다음과 같다.</p>
<ul>
<li>회사에서 AI를 활용한 프로젝트를 진행한 적이 있었는데 구조, 성능, 예외처리 등 고민할 사항이 많았다.</li>
<li>프로덕트 팀에 속하게 되면서 예전보다는 직군이 다른 엔지니어 분들과 소통할 기회가 많아졌고 자연스레 개발 이야기를 나누는 일이 많아졌는데 전부 이해하지는 못하더라도 이야기를 듣는 게 재밌다.</li>
<li>프론트에서는 어떻게 테스트를 진행하는지 궁금했다.</li>
</ul>
<br />
<br />

<hr>
<h2 id="1-내용">1. 내용</h2>
<p>이번 세미나는 지난 if(kakaoAI)2024 컨퍼런스에서 진행되었던 기술 세션들을 압축한 형태로 보여주었다고 하여 내용은 영상으로 첨부하였다. (그리고 복습하고 싶어서...ㅎㅎ)</p>
<br />

<h3 id="1-ai-agent-기반-스마트-ai-마이-노트-링크">(1) AI Agent 기반 스마트 AI 마이 노트 (<a href="https://www.youtube.com/watch?v=C2BLY7GlO2o&amp;ab_channel=kakaotech">링크</a>)</h3>
<p>사용자가 어떤 질문을 해야하는지 자체를 고민해야 한다는 건 쉬운 서비스는 아니라는 말씀이 인상 깊었다. (가령 chat GPT가 처음 출시되었을 때 뭘 어떻게 질문해야지 등을 고민하는 것)
기능 조직에만 속해있을 때는 단순히 들어오는 요청만 수동적으로 처리했었는데 프로덕트 조직에 속하게 된 이상 기능을 만들기 전에 왜 필요한지에 대해 한 번 더 생각하는 것이 필요하겠다는 점을 다시 한 번 느꼈다.
(기술적으로 불가능하기 때문에 다 해드릴 수는 없겠지만...)</p>
<br />

<h3 id="2-tdd로-앞서가는-프론트엔드-디자인-api-없이도-개발을-시작하는-방법-링크">(2) TDD로 앞서가는 프론트엔드: 디자인, API 없이도 개발을 시작하는 방법 (<a href="https://www.youtube.com/watch?v=P9ItzDrPlso&amp;t=2s&amp;ab_channel=kakaotech">링크</a>)</h3>
<p>다른 세션은 기술적으로 신기하다 (또는 어렵다) 라고 생각했더라면 이번 세션은 재밌게 들으면서 반성을 많이 했던 것 같다. 클라이언트 역할로 Back To Back API 연동을 해본 경험에 이어서 프론트엔드 개발자의 고충을 간접적으로나마 경험할 수 있었다. 결론적으로 API 문서 초안 작성 시 검토를 신중하게 해야겠다고 다짐하였다. </p>
<p>또한 프론트엔드라고는 사이드 프로젝트만 끄적거려본 사람으로써 테스트가 어떻게 이루어지는지 궁금했는데 그 궁금증이 조금은 해소된 것 같다. 특히 API Layer와 View Layer 사이에 ViewModel Layer를 둠으로써 api 스펙이 바뀌어도 viewModel의 구조 변환 없이 데이터 매핑 부분만 바꾸는 방향을 제시하면서 예제를 보여주셨는데 이 부분이 가장 재밌으면서도 고민이 많아졌던 것 같다. (외부 API 호출할 때 백엔드 입장으로는 view 대신 service로 본다면 비슷한 것 같기도...?)</p>
<br />


<h3 id="3-업무-효율화를-위한-카카오-사내봇-개발기-링크">(3) 업무 효율화를 위한 카카오 사내봇 개발기 (<a href="https://www.youtube.com/watch?v=P9ItzDrPlso&amp;t=2s&amp;ab_channel=kakaotech">링크</a>)</h3>
<p>프로젝트 진행 중 간혹 다른 팀의 도메인을 알아야 하는 순간이 종종 오는데 그 때마다 담당자를 찾아뵙기도 죄송스럽고 부재 시 난감했던 적도 있었는데 이런 기능이 있다면 일의 효율성이 높아질 것으로 보여 쉽지 않겠지만 개인적으로 사내에도 도입하고 싶었던 프로젝트였다.</p>
<br />
<br />


<hr>
<h2 id="2-질의응답">2. 질의응답</h2>
<p>올해 외부 API를 통해 생성형 AI를 사용할 일이 있었으며 신규 프로젝트 리서치 시 AI를 사용해야 할 일이 있었다. 그런데 배경지식 없이 사용하기에는 실무에 도입해도 되는지에 대한 확신이 없었고 깊게 공부하자니 다른 백엔드 기술들도 공부할 게 많기 때문에 막막해서 아래와 같은 질문을 하게 되었다.</p>
<blockquote>
<p>프로젝트에서 직간접적으로 AI를 활용하는 케이스가 늘어나고 있는데 백엔드 개발자로서 적어도 어디까지 대비하면 좋을지에 대한 조언을 해주시면 감사하겠습니다. </p>
</blockquote>
<br />

<p>많은 멋진 질문 사이에서 감사하게도 본인 질문에 대해 답변해주셨다. 그것도 연차별로 나누어서... 🥹</p>
<blockquote>
<p><strong>신입</strong>
LLM으로 인해 개발에 대한 진입장벽이 낮아졌으며 데이터 엔지니어링, k8s, docker, AI 등 쌓아야 할 기술이 늘어나 취업이 어려워진 건 사실이다.
<strong>경력</strong>
모델 형성 방법 등 딥하게 들어갈 필요까지는 없지만 ML 파이프라인, 필요한 컴포넌트 등 큰 흐름 위주로 파악할 필요는 있다.</p>
</blockquote>
<br />
<br />

<hr>
<p>직장과 거리가 가깝지 않아 퇴근 후 바로 찾아가느라 그날 하루는 정신 없었지만 그래도 참여하기를 매우 잘한 것 같다. 이 글을 마치면서 다시 한 번 참여할 기회를 주셔서 감사하다는 말씀 드리고 싶다. </p>
<br />
<br />
<br />
<br />
<br />
<br />


<p><strong>p.s. 죠르디는 귀엽다.</strong>
<img src="https://velog.velcdn.com/images/jduckling_1024/post/b0cb08fa-365b-492c-b110-68fb20b357e3/image.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[(mac m1) vscode에서 python이 실행되지 않는 문제 해결]]></title>
            <link>https://velog.io/@jduckling_1024/mac-m1-vscode%EC%97%90%EC%84%9C-python%EC%9D%B4-%EC%8B%A4%ED%96%89%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@jduckling_1024/mac-m1-vscode%EC%97%90%EC%84%9C-python%EC%9D%B4-%EC%8B%A4%ED%96%89%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sun, 12 May 2024 12:36:31 GMT</pubDate>
            <description><![CDATA[<h3 id="오류-발생">오류 발생</h3>
<p>명령어 라인 개발자 도구는 설치된 상태에서 vscode에서 python 코드 작성 후 실행하는데 오류가 떴다.</p>
<pre><code>[Running] python -u &quot;(...).py&quot;
xcode-select: Failed to locate &#39;python&#39;, requesting installation of command line developer tools.

[Done] exited with code=72 in 0.025 seconds</code></pre><br />
<br />


<h3 id="해결-방법">해결 방법</h3>
<p><strong>현재 xcode 확인</strong></p>
<pre><code>xcode-select -p

/Library/Developer/CommandLineTools
</code></pre><br />

<p><strong>심볼릭 링크</strong></p>
<pre><code>sudo ln -s /Library/Developer/CommandLineTools/usr/bin/python3 /Library/Developer/CommandLineTools/usr/bin/python</code></pre><br />

<p>확인해보니 python binary가 없었고 python3 심볼릭 링크를 통해 해결하였다. 혹시 나중에 똑같은 문제에 직면했을 때 슬쩍 떠올리도록 정리해보았다.</p>
<br />
<br />



<blockquote>
<p><strong>참고 링크</strong>
<a href="https://forums.developer.apple.com/forums/thread/704099">https://forums.developer.apple.com/forums/thread/704099</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Transactional과 Exception 간의 관계]]></title>
            <link>https://velog.io/@jduckling_1024/Transactional%EA%B3%BC-Exception-%EA%B0%84%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@jduckling_1024/Transactional%EA%B3%BC-Exception-%EA%B0%84%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Mon, 01 Apr 2024 12:32:17 GMT</pubDate>
            <description><![CDATA[<h1 id="1-트랜잭션-내-메소드-실행-중-예외-발생">1. 트랜잭션 내 메소드 실행 중 예외 발생</h1>
<p>특정 메소드를 트랜잭션 내에서 수행하도록 하고싶을 때 @Transactional 메소드를 선언하여 사용할 수 있다. 아래와 같이 말이다.</p>
<pre><code>@Transactional
public void test() {
    // 구현
}</code></pre><br /> 
<br /> 
그런데 로직 수행 중 예외가 발생하면 지금까지 수행했던 로직은 어떻게 처리될까?
<br /> 
<br /> 

<blockquote>
<p>By default, a transaction will be rolled back on {@link RuntimeException} and {@link Error} but not on checked exceptions (business exceptions).</p>
</blockquote>
<p>기본적으로 스프링 프레임워크에서는 CheckedException일 경우 지금까지의 내용은 커밋하고 UnCheckedException일 경우 모두 롤백한다. <a href="https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative.html#page-title">스프링은 EJB 규칙을 따르기 때문이다.</a></p>
<br />
<br />


<h3 id="실제-코드">실제 코드</h3>
<p>내부가 궁금해서 열고 들어가봤더니 예외가 발생된 이후 롤백하는 메소드가 존재한다. txInfo.transactionAttribute.rollbackOn(ex) 에 주목하자. 해당 메소드에서 UnCheckedException이나 Error가 발생하면 던지는 것을 볼 수 있다.</p>
<pre><code class="language-java">/**
 * TransactionAspectSupport.java
 */
private Mono&lt;Void&gt; completeTransactionAfterThrowing(@Nullable ReactiveTransactionInfo txInfo, Throwable ex) {
    if (txInfo != null &amp;&amp; txInfo.getReactiveTransaction() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace(&quot;Completing transaction for [&quot; + txInfo.getJoinpointIdentification() +
                    &quot;] after exception: &quot; + ex);
        }
        if (txInfo.transactionAttribute != null &amp;&amp; txInfo.transactionAttribute.rollbackOn(ex)) {
            return txInfo.getTransactionManager().rollback(txInfo.getReactiveTransaction()).onErrorMap(ex2 -&gt; {
                        logger.error(&quot;Application exception overridden by rollback exception&quot;, ex);
                        if (ex2 instanceof TransactionSystemException systemException) {
                            systemException.initApplicationException(ex);
                        }
                        else {
                            ex2.addSuppressed(ex);
                        }
                        return ex2;
                    }
            );
        }
        else {
            // We don&#39;t roll back on this exception.
            // Will still roll back if TransactionStatus.isRollbackOnly() is true.
            return txInfo.getTransactionManager().commit(txInfo.getReactiveTransaction()).onErrorMap(ex2 -&gt; {
                        logger.error(&quot;Application exception overridden by commit exception&quot;, ex);
                        if (ex2 instanceof TransactionSystemException systemException) {
                            systemException.initApplicationException(ex);
                        }
                        else {
                            ex2.addSuppressed(ex);
                        }
                        return ex2;
                    }
            );
        }
    }
    return Mono.empty();
}</code></pre>
<pre><code class="language-java">/**
 * DefaultTransactionAttribute
 */
@Override
public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}</code></pre>
<br />
<br />
<br />
<br />








<h1 id="checked-exception-발생-시-롤백하고-싶다면">checked exception 발생 시 롤백하고 싶다면?</h1>
<p>Transactional 메소드는 rollbackFor 속성을 제공한다. (자매품 rollbackForClassName())</p>
<pre><code class="language-java">public @interface Transactional {
    /**
     * Defines zero (0) or more exception {@linkplain Class types}, which must be
     * subclasses of {@link Throwable}, indicating which exception types must cause
     * a transaction rollback.
     * &lt;p&gt;By default, a transaction will be rolled back on {@link RuntimeException}
     * and {@link Error} but not on checked exceptions (business exceptions). See
     * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)}
     * for a detailed explanation.
     * &lt;p&gt;This is the preferred way to construct a rollback rule (in contrast to
     * {@link #rollbackForClassName}), matching the exception type and its subclasses
     * in a type-safe manner. See the {@linkplain Transactional class-level javadocs}
     * for further details on rollback rule semantics.
     * @see #rollbackForClassName
     * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class)
     * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
     */
    Class&lt;? extends Throwable&gt;[] rollbackFor() default {};
}</code></pre>
<br />
<br />

<h3 id="실제-코드-1">실제 코드</h3>
<p>RuleBasedTransactionAttribute의 rollbackOn()으로 동작하게 된다. 안에 보이는 rollbackRules에 해당하는 값들이 위 어노테이션에서 설정한 값들이다. 만약 발생한 exception이 rollbackRules에 해당하지 않는다면 상위 rollbackOn()을 호출하게 되는데 이는 위에서 봤던 DefaultTransactionAttribute가 되어 UnCheckedException 혹은 Error일 경우 롤백하도록 한다.</p>
<pre><code class="language-java">    /**
     * Winning rule is the shallowest rule (that is, the closest in the
     * inheritance hierarchy to the exception). If no rule applies (-1),
     * return false.
     * @see TransactionAttribute#rollbackOn(java.lang.Throwable)
     */
    @Override
    public boolean rollbackOn(Throwable ex) {
        RollbackRuleAttribute winner = null;
        int deepest = Integer.MAX_VALUE;

        if (this.rollbackRules != null) {
            for (RollbackRuleAttribute rule : this.rollbackRules) {
                int depth = rule.getDepth(ex);
                if (depth &gt;= 0 &amp;&amp; depth &lt; deepest) {
                    deepest = depth;
                    winner = rule;
                }
            }
        }

        // User superclass behavior (rollback on unchecked) if no rule matches.
        if (winner == null) {
            return super.rollbackOn(ex);
        }

        return !(winner instanceof NoRollbackRuleAttribute);
    }</code></pre>
<br />



]]></description>
        </item>
        <item>
            <title><![CDATA[ObjectMapper에 대해 알아보자.]]></title>
            <link>https://velog.io/@jduckling_1024/ObjectMapper%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@jduckling_1024/ObjectMapper%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 23 Mar 2024 06:55:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Spring Boot를 이용해 개발을 하면서 ObjectMapper를 직간접적으로 사용하고 있었다. 기본 ObjectMapper를 사용했을 때 자주 잊어버리는 항목인 것 같아 이번 기회에 정리해보았다.</p>
</blockquote>
<br />
이제부터 아래 클래스를 가지고 놀 예정이다.

<pre><code class="language-java">class TestObject {
    private Long id;
    private String name;
}</code></pre>
<hr>
<h1 id="1-역직렬화-string---object">1. 역직렬화 (String -&gt; Object)</h1>
<blockquote>
<p>Object에 해당하는 클래스에 @NoArgsConstructor가 필요하고 @Getter 또는 @Setter가 필요하다.</p>
</blockquote>
<p>spring boot에서 request json 문자열을 DTO 객체로 변환하는 과정이다.</p>
<br />

<h2 id="예시">예시</h2>
<h3 id="예시-코드">예시 코드</h3>
<pre><code>ObjectMapper mapper = new ObjectMapper();
final String value = &quot;{\&quot;name\&quot;:\&quot;test\&quot;,\&quot;id\&quot;:1}&quot;;

TestObject resObject = mapper.readValue(value, TestObject.class);</code></pre><br />

<h3 id="1-testobject에-getter-또는-setter를-붙이지-않은-경우">1) TestObject에 @Getter 또는 @Setter를 붙이지 않은 경우</h3>
<pre><code>Exception in thread &quot;main&quot; com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field &quot;name&quot; (class com.test.teamlog.global.security.TestObject), not marked as ignorable (0 known properties: ])
 at [Source: (String)&quot;{&quot;name&quot;:&quot;test&quot;,&quot;id&quot;:1}&quot;; line: 1, column: 10] (through reference chain: com.test.teamlog.global.security.TestObject[&quot;name&quot;])</code></pre><p>필드가 없다면서 UnrecognizedPropertyException이 발생한다.</p>
<br />



<h3 id="2-testobject에-noargsconstructor를-붙이지-않은-경우">2) TestObject에 @NoArgsConstructor를 붙이지 않은 경우</h3>
<pre><code>Exception in thread &quot;main&quot; com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.test.teamlog.global.security.TestObject` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)</code></pre><p>no Creators, like default constructor 라면서 기본 생성자를 만들어달라고 에러를 뱉는다.</p>
<br />
<br />

<h2 id="추가-내용">추가 내용</h2>
<p>인턴 시절 실무에서 뭣모르고 했다가 종종 버그가 발생했던 사례가 떠올라 적어보기로 했다. 당시 특정 필드의 값을 request로부터 받는 시점에 변환하고 싶었던 모양이었는지 request dto에 해당하는 클래스의 필드에 대한 setter를 재정의했다. (왜 그랬지...) 아래와 같이 말이다.</p>
<pre><code>@Getter
class TestObject {
    private Long id;
    private String name;

    public void setName(String name) {
        this.name = &quot;Hello, my name is &quot; + name;
    }
}
</code></pre><br />
이렇게 되면 request에서 {"id":1,"name":"name"} 와 같이 보내면 name은 Hello, my name is test가 된다. ObjectMapper를 통해 역직렬화할 때 setter의 우선순위가 더 높은 것 같다. 히스토리를 모르는 다른 개발자를 당황시키기 딱 좋은 코드다. 기본 Setter, Getter는 건드리지 말자. 



<br />
<br />
<br />
<br />


<hr>
<h1 id="2-직렬화-object---string">2. 직렬화 (Object -&gt; String)</h1>
<p>spring boot에서 DTO 객체 -&gt; response로 변환하는 과정이다.  </p>
<h3 id="1-getter가-없는-경우">1) @Getter가 없는 경우</h3>
<pre><code>Exception in thread &quot;main&quot; com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.test.teamlog.global.security.TestObject and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)</code></pre><p>기본적으로 public인 필드에 접근하게 된다. 즉, 필드를 public으로 열어주거나 getter 메소드를 열어주어 접근 가능하도록 해야한다.</p>
<br />
<br />
<br />
<br />

]]></description>
        </item>
        <item>
            <title><![CDATA[Restful API란?]]></title>
            <link>https://velog.io/@jduckling_1024/Restful-API%EB%9E%80</link>
            <guid>https://velog.io/@jduckling_1024/Restful-API%EB%9E%80</guid>
            <pubDate>Sat, 27 May 2023 19:54:04 GMT</pubDate>
            <description><![CDATA[<p>며칠 전, 오랜만에 대학교 선배를 만났고 이런 대화가 오고 갔다.</p>
<blockquote>
<p>&quot;진아, Rest API가 뭐야?&quot;
&quot;어... 저도 정확하게 정의하기는 어렵지만... (어쩌구 저쩌구)&quot;</p>
</blockquote>
<p>그러게... Restful API가 뭘까? 1년 넘게 많은 API를 접했지만 정작 &#39;Rest API&#39;가 무엇인지 고민해보는 시간은 갖지 못했던 것 같다. 그래서 이번 기회에 고민해보기로 했다.</p>
<br />
<br />
<br />
<br />
<br />
<br />

<hr>
<h1 id="1-보통-개발자가-아는-rest-api의-정의">1. 보통 개발자가 아는 Rest API의 정의</h1>
<blockquote>
<p>REST란 Representational State Transfer의 약자로 분산 하이퍼미디어 시스템을 위한 아키텍쳐 스타일이다. 즉, 자원의 이름으로 구분하여 해당 자원의 상태를 주고받는 것이다.</p>
</blockquote>
<br />
<br />
<br />

<h2 id="a-rest-api란">a. Rest API란?</h2>
<p>REST(Representational State Transfer) API는 요청을 보내는 API 주소만으로도 대략 어떤 요청인지 파악이 가능하도록 설계한 API다.</p>
<br />

<h3 id="1rest의-구성">1)REST의 구성</h3>
<ul>
<li>자원(Resource) - HTTP URI</li>
<li>행위(Verb) - HTTP Method</li>
<li>표현(Representations) - HTTP Message PayLoad</li>
</ul>
<br />
<br />
<br />

<h2 id="b-사실-rest-api가-아니여도-기능-개발에는-문제가-없다">b. 사실 Rest API가 아니여도 기능 개발에는 문제가 없다.</h2>
<p>사실 Restful하게 설계하지 않아도 기능이 동작하도록 구현하는 데에는 아무 문제가 없다. 아래 Spring Boot를 이용해 작성한 코드를 보자.</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/accounts&quot;)
@RequiredArgsConstructor
public class AccountApiController {
    @GetMapping(&quot;/duckling/{idx}&quot;)
    public int delete(@PathVariable Long idx) {
        // 회원 탈퇴하는 로직
        return 1;
    }
}</code></pre>
<p>탈퇴할 계정의 idx를 알고, 비즈니스 로직에만 이상이 없다면 이 API는 정상적으로 동작할 것이다. </p>
<br />
<br />
<br />


<h2 id="c-그럼에도-불구하고-rest-api가-필요한-이유">c. 그럼에도 불구하고 Rest API가 필요한 이유</h2>
<p>하지만... </p>
<blockquote>
<p>GET <code>/api/accounts/duckling/{idx}</code></p>
</blockquote>
<p>이렇게 API를 제시했을 때 해당 API를 사용할 다른 개발자가, 혹은 추후 유지보수 할 다른 동료 개발자가 과연 어떤 API인지 알 수 있을까? <del>일단 엉망진창으로 예시를 든 본인도 모르겠다.</del></p>
<br />

<p>위에서 언급한 REST 구성을 잘 이용해 API를 개선해보자.</p>
<blockquote>
<p>AS-IS
GET <code>/api/accounts/duckling/{idx}</code>
<br />
TO-BE
DELETE <code>/api/accounts/{idx}</code></p>
</blockquote>
<p>훨씬 나아졌다. API만 보고도 특정 idx에 해당하는 계정을 탈퇴할 떄 사용한다는 것을 짐작할 수 있다.</p>
<br />
<br />
<br />
<br />

<hr>
<h1 id="2-좀-더-깊게-알아보자">2. 좀 더 깊게 알아보자</h1>
<h2 id="a-rest-제약조건">a. REST 제약조건</h2>
<ul>
<li>client-server</li>
<li>stateless</li>
<li>cache</li>
<li><strong>uniform interface</strong></li>
<li>layered system</li>
<li>code-on-demand (optional)</li>
</ul>
<br />

<p>위 조건들은 uniform interface 빼고 웬만한 HTTP API에서도 모두 지켜질 수 있다. 그렇다면 uniform interface가 무엇인지 알아보자.</p>
<br />
<br />
<br />

<h2 id="b-uniform-interface">b. uniform interface</h2>
<p>uniform interface 정의를 찾아보면 아래와 같이 나온다.</p>
<blockquote>
<p><strong>URI로 지정한 리소스에 대한 조작</strong>을 <strong>통일되고 한정적인 인터페이스로 수행</strong>하는 아키텍처 스타일을 말한다.</p>
</blockquote>
<p>하지만 본인은 저 한 줄로는 이해하기 어려웠다. 따라서 uniform interface를 만족시키는 조건에 대입해서 생각해보려고 한다.</p>
<br />
<br />
<br />


<h3 id="1-uniform-interface를-만족시키는-조건">1) uniform interface를 만족시키는 조건</h3>
<h4 id="a-identifiaction-of-resources">a) identifiaction of resources</h4>
<ul>
<li>resource가 URI로 식별되면 된다. </li>
<li><strong>URI로 지정한 리소스에 대한 조작</strong></li>
<li><blockquote>
<p>ex) &#39;회원&#39;이라는 리소스에 접근하기 위한 API를 설계할 때     <code>localhost:8080/api/accounts</code> 처럼 설계할 수 있다.</p>
</blockquote>
</li>
</ul>
<h4 id="b-manipulation-of-resources-through-representations">b) manipulation of resources through representations</h4>
<ul>
<li>representations 전송을 통해 resources를 조작해야 한다.</li>
<li>여기서 representations이란 GET, POST, PUT, DELETE 등의 HTTP 메소드를 뜻한다.</li>
<li><strong>통일되고 한정적인 인터페이스로 수행</strong><ul>
<li>보편적으로 <code>등록(POST), 수정(PUT, PATCH), 삭제(DELETE), 조회(GET)</code>  와 같이 사용한다.</li>
<li>ex) 회원 탈퇴 API 생성 시 <code>GET localhost:8080/api/accounts/delete</code> 처럼 명시할 수도 있겠지만 약속한 대로 <code>DELETE localhost:8080/api/accounts</code> 처럼 나타내면 더 깔끔하고 명확해진다.</li>
</ul>
</li>
</ul>
<h4 id="c-self-descriptive-messages">c) self-descriptive messages</h4>
<ul>
<li>메시지가 스스로 설명해야 한다.</li>
<li>(지켜지기 어렵다)<h4 id="d-hateoas">d) HATEOAS</h4>
</li>
<li>application의 상태는 하이프링크를 이용해서 전이되어야 한다.</li>
<li>(지켜지기 어렵다)</li>
</ul>
<br />

<p>사실 <strong>identifiaction of resources</strong>와 <strong>manipulation of resources through representations</strong>는 흔히 알고있는 REST API의 특징이다. 하지만 REST API에서 uniform interface가 지켜지지 않는다고 하는 이유는 <strong>self-descriptive messages</strong>와 <strong>HATEOAS</strong> 때문이다.</p>
<br />
<br />
<br />

<h3 id="2-self-descriptive-messages-만족하기">2) self-descriptive messages 만족하기</h3>
<p>self-descriptive는 메시지가 스스로 설명되어야 한다 즉, 메시지의 모든 요소는 메시지만 보고 그 뜻을 알아야 한다는 것이다.</p>
<br />
보통 API를 작성하고 response를 받을 때 아래와 같은 형식으로 받는다.

<pre><code class="language-json">HTTP/1.1 200 OK
Content-Type: application/json
{&quot;idx&quot;:1,&quot;name&quot;:&quot;윤진&quot;}</code></pre>
<p>사람이 봤을 떄는 json 타입으로 idx는 1이고 이름은 &#39;윤진&#39;이구나 하고 알겠지만, 기계는 이를 알지 못한다. 따라서 RESTful하지 않는 설계라고 할 수 있다. 그렇다면 어떻게 해야 만족할 수 있을까? </p>
<br />
<br />

<h4 id="a-media-type-정의-iana">a) Media type 정의 (<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA</a>)</h4>
<pre><code class="language-json">HTTP/1.1 200 OK
Content-Type: (사용자 정의 media type)
{&quot;idx&quot;:1,&quot;name&quot;:&quot;윤진&quot;}</code></pre>
<ol>
<li>미디어 타입을 정의 후 각 속성의 의미가 담긴 미디어 타입 문서를 작성한다.
(위 예제에서는 idx, name이 의미하는 것을 적는다.)</li>
<li>IANA에 미디어 타입을 등록한다. 위에서 작성한 문서를 미디어 타입 명세로 등록한다.</li>
<li>IAMA에 등록된 명세를 찾을 수 있으므로 메시지의 의미를 해석할 수 있게 된다.</li>
</ol>
<br />
단점: IANA에 등록해야 한다는 번거로움이 있다


<br />
<br />

<h4 id="b-link-header에-명세를-확인할-수-있는-링크-첨부">b) link header에 명세를 확인할 수 있는 링크 첨부</h4>
<pre><code class="language-json">HTTP/1.1 200 OK
Content-Type: application/json
Link: &lt;https://...&gt;;  rel=&quot;profile&quot;
{&quot;idx&quot;:1,&quot;name&quot;:&quot;윤진&quot;}</code></pre>
<ol>
<li>json을 구성하는 요소들이 무엇을 의미하는지를 나타내는 문서를 작성한다.</li>
<li>link 헤더에 명세를 확인할 수 있는 링크를 넣어 응답에 넘긴다.</li>
</ol>
<br />

<p>단점: 클라이언트가 Link 헤더와 profile을 이해해야 하며 미디어 타입이 아닌 Link 헤더를 통해 판단하기 때문에 <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Content_negotiation">content negotiation</a>할 수 없다.</p>
<br />
<br />
<br />


<h3 id="3-hateoas-만족하기">3) HATEOAS 만족하기</h3>
<p>HATEOAS는 Hypermedia As The Engine Of Application State의 약자로 애플리케이션은 Hyperlink를 이용해서 전이가 되어야 한다는 뜻이다. html처럼 말이다.
<br />
예를 들어 아래의 html에서는 a 태그를 통해 다음 상태가 결정된 것을 확인할 수 있다. 따라서 HTML은 HATEOAS를 만족한다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;사용자별 소속 팀&lt;/h1&gt;
    &lt;a href=&quot;localhost:8080/accounts/1/teams&quot;&gt;유저1의 소속 팀&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<br />

<p>하지만 response가 아래와 같은 json 형식이라면?</p>
<pre><code class="language-json">HTTP/1.1 200 OK
Content-Type: application/json
{&quot;idx&quot;:1,&quot;name&quot;:&quot;윤진&quot;}</code></pre>
<p>응답을 받는 시점에 response만 담기게 되고 다음 상태를 알 수 없다. 따라서 RESTful하지 못하다. 어떻게 해야 HATEOAS를 만족할 수 있을까?</p>
<br />

<p>단순 JSON이 아닌, HAL JSON을 이용한다. </p>
<blockquote>
<p><strong>HAL</strong>
Hypertext Application Language의 약자로 JSON, XML 코드 내의 외부 리소스에 대한 링크를 추가하기 위한 특별한 데이터 타입이다.</p>
</blockquote>
<pre><code>HTTP/1.1 200 OK
Content-Type: application/hal+json
{
  &quot;data&quot;: { // HAL JSON의 리소스 필드
    &quot;id&quot;: 1000,
    &quot;name&quot;: &quot;게시글 1&quot;,
    &quot;content&quot;: &quot;HAL JSON을 이용한 예시 JSON&quot;
  },
  &quot;_links&quot;: { // HAL JSON의 링크 필드
    &quot;self&quot;: {
      &quot;href&quot;: &quot;http://localhost:8080/api/article/1000&quot; // 현재 api 주소
    },
    ...
  }
}</code></pre><ul>
<li>data: 요청에 대한 응답 데이터</li>
<li>_links: 접근 가능한 추가 API</li>
</ul>
<p>이렇게 하면 클라이언트에서 API를 호출하는 부분을 하드코딩할 필요가 없어지며 접근 가능한 추가 API가 바뀌어도 해당 API를 호출하는 쪽은 변경할 필요 또한 없어진다.</p>
<br />
<br />
<br /><br />

<hr>
<h1 id="3-느낀-점">3. 느낀 점</h1>
<p>공부하는 과정에서 uniform interface의 한 줄 정의만 봤을 때는 내가 아는 REST API랑 같은 것 같은데 왜 만족하지 못한다는 거지...? 부터 시작해서 생각이 많았다. 나름대로 정리는 해봤지만 솔직하게 말하자면 REST API에 대해 아직 완전히 이해하지는 못했다고 생각한다. </p>
<p>하지만 분명한 건 개발하면서 지금까지 REST API라고 알고 설계했던 것들은 전혀 RESTful하지 못했던 것 같다. 이번에 RestAPI에 대해 알아보면서 만족시킬 수 있는 방법 중 하나인 <code>self-descriptive message를 만족하는 방법 중 하나인 link header에 명세 링크 첨부</code> 가 가장 인상깊었던 것 같다. 프론트엔드 개발자 입장에서도 그럴 것이고 기능 개발하는 본인도 한 번씩 문서가 어딨었더라...? 하는 경우가 종종 있었는데 이를 적용한다면 그럴 일도 없고 RestAPI를 만족하는 데 한 발짝 나아갈 수 있을 것 같다. </p>
<p>HATEOAS는 오 그러겠네...! 싶다가도
그러면 프론트엔드 개발자 입장에서는 _links에 있는 API가 만약에 get이 아니라면 body에 들어갈 데이터 명세는 어떻게 알지? 명세를 알더라도 위에서 언급한 하드코딩할 필요가 없어진다는 장점이 여기에서도 똑같이 작용하는가? 정도의 궁금증은 남아있는데 이건 본인이 아직 지식이 부족해서 그런 것 같다.
<br /></p>
<p>+ 서점에서 지나가다 발견한 RESTful <a href="https://product.kyobobook.co.kr/detail/S000001033018">WEB API 웹 API를 위한 모범 전략 가이드</a> 도 기회가 된다면 나중에 한 번 읽어보고 싶다.</p>
<br />
<br />
<br />
<br />
<br />
<br />

<blockquote>
<p>** References**
<br />
<a href="https://www.oreilly.com/content/how-a-restful-api-represents-resources/">Restful API가 리소스를 나타내는 방법</a>
<a href="https://jcon.tistory.com/88">REST API란?</a>
<a href="https://tv.naver.com/v/2292653">그런 REST API로 괜찮은가</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot에 S3 연동하여 파일 업로드하기 (+ 403 에러 해결)]]></title>
            <link>https://velog.io/@jduckling_1024/Spring-Boot%EC%97%90-S3-%EC%97%B0%EB%8F%99%ED%95%98%EC%97%AC-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0-403-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@jduckling_1024/Spring-Boot%EC%97%90-S3-%EC%97%B0%EB%8F%99%ED%95%98%EC%97%AC-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0-403-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sun, 25 Dec 2022 05:55:35 GMT</pubDate>
            <description><![CDATA[<h1 id="1-spring-boot에-s3-연동">1. Spring Boot에 S3 연동</h1>
<h2 id="1-1-s3-퍼블릭-액세스-차단-비활성화">1-1. S3 퍼블릭 액세스 차단 비활성화</h2>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/862cfa81-a739-4cda-8070-1a60efd08ed0/image.png" alt=""></p>
<br />
<br />

<h2 id="1-2-버킷-정책-action에-s3putobject-넣기">1-2. 버킷 정책 Action에 s3:PutObject 넣기</h2>
<pre><code>{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Id&quot;: &quot;ExamplePolicy01&quot;,
    &quot;Statement&quot;: [
        {
            ...
            &quot;Action&quot;: [
                &quot;s3:PutObject&quot;,
            ],
            ...
        }
    ]
}</code></pre><p><a href="https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/access-policy-language-overview.html">버킷 정책 설정 참고</a></p>
<br />
<br />

<h2 id="1-3-코드-내-설정">1-3. 코드 내 설정</h2>
<p>build.gradle</p>
<pre><code>implementation group: &#39;io.awspring.cloud&#39;, name: &#39;spring-cloud-starter-aws&#39;, version: &#39;2.4.2&#39;</code></pre><br />
<br />

<p>AmazonS3 bean 생성 및 주입</p>
<pre><code class="language-java">@PostConstruct
public void setS3Client() {
    AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);

    final AWSStaticCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
    s3Client = AmazonS3ClientBuilder.standard()
            .withRegion(this.region)
            .disableChunkedEncoding()
            .withCredentials(credentialsProvider)
            .build();
}</code></pre>
<ul>
<li>bean으로 등록한 후 해당 빈을 주입받아 사용하는 것이 바람직하겠지만, 본인은 테스트를 위해 일단 @PostConstruct를 사용하여 해당 객체를 만들어주었다</li>
<li>위에서 사용한 accessKey, secretKey, region은 yaml에서 관리하도록 하였다.</li>
</ul>
<br />
<br />

<p>파일 업로드 메소드</p>
<pre><code class="language-java">public void upload(MultipartFile file) {
    ObjectMetadata objectMetadata = new ObjectMetadata();
    objectMetadata.setContentType(file.getContentType());
    objectMetadata.setContentLength(file.getSize());

    String key = &quot;item/&quot; + file.getOriginalFilename();

    try (InputStream inputStream = file.getInputStream()) {
        final PutObjectRequest putObjectRequest = new PutObjectRequest(
                bucket,
                key,
                inputStream,
                objectMetadata);
        putObjectRequest.withCannedAcl(CannedAccessControlList.PublicRead);

        s3Client.putObject(putObjectRequest);
    } catch (IOException e) {
        e.printStackTrace();
    }
}</code></pre>
<ul>
<li>bucket : 파일이 저장될 S3 버킷의 이름</li>
<li>key : S3에 저장될 경로 포함 파일 이름</li>
<li><code>CannedAccessControlList.PublicRead</code> : 외부에 공개할 이미지로 public read 권한 추가</li>
</ul>
<br />
<br />
<br />
<br />

<h1 id="2-403-에러-해결">2. 403 에러 해결</h1>
<blockquote>
<p>이대로 실행 시켰는데 403???</p>
</blockquote>
<p>그대로 따라했는데 403 에러를 계속 보게 되었다. 
S3 퍼블릭 액세스 차단도 모두 비활성하고 본인 IAM Access Key와 Secret Key도 전부 맞는데...?</p>
<br />
<br />

<blockquote>
<p>여기서 Access Key와 Secret Key는 root 계정이 아니라 S3와 연결된 IAM의 것이라고 한다.</p>
</blockquote>
<br />
<br />

<h2 id="2-1-s3-접근을-위한-iam-생성하기">2-1. S3 접근을 위한 IAM 생성하기</h2>
<ol>
<li>사용자 추가 버튼 클릭
<img src="https://velog.velcdn.com/images/jduckling_1024/post/d2924916-76b4-4abe-9184-34f4827f858b/image.png" alt=""></li>
</ol>
<br />


<ol start="2">
<li>사용자 이름을 적어주고 AWS 자격 증명 유형을 <code>액세스 키-프로그래밍 방식 액세스</code>로 선택
<img src="https://velog.velcdn.com/images/jduckling_1024/post/ad1deadc-db77-4d54-a0b8-0cd0b4eb3785/image.png" alt=""></li>
</ol>
<br />

<ol start="3">
<li>기존 정책 직접 연결 클릭 후 AmazonS3FullAccess 선택
<img src="https://velog.velcdn.com/images/jduckling_1024/post/6f34955d-b280-49ee-8b96-c4792119c758/image.png" alt=""></li>
</ol>
<br />

<ol start="4">
<li>다음 버튼을 계속 누른 후 액세스 키와 비밀 액세스 키를 확인한다.
<img src="https://velog.velcdn.com/images/jduckling_1024/post/4f2965c8-5d15-4694-ba42-93edeac8edd2/image.png" alt=""></li>
</ol>
<ul>
<li>단, 이 페이지에서 반드시 키를 복사해놓고 마쳐야 한다.</li>
</ul>
<br />
<br />
<br />
<br />

<blockquote>
<p><strong>참고</strong>
<a href="https://programming-workspace.tistory.com/61">https://programming-workspace.tistory.com/61</a>
<a href="https://artiiicy.tistory.com/16">https://artiiicy.tistory.com/16</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java9 불변 컬렉션]]></title>
            <link>https://velog.io/@jduckling_1024/Java9-%EB%B6%88%EB%B3%80-%EC%BB%AC%EB%A0%89%EC%85%98</link>
            <guid>https://velog.io/@jduckling_1024/Java9-%EB%B6%88%EB%B3%80-%EC%BB%AC%EB%A0%89%EC%85%98</guid>
            <pubDate>Mon, 07 Nov 2022 19:21:46 GMT</pubDate>
            <description><![CDATA[<h1 id="1-java9의-불변-컬렉션-생성">1. Java9의 불변 컬렉션 생성</h1>
<h2 id="1-list">1. List</h2>
<h3 id="1-listof">1) List.of()</h3>
<pre><code class="language-java">List&lt;String&gt; animalList = List.of(&quot;Dog&quot;, &quot;Cat&quot;, &quot;Duck&quot;);</code></pre>
<ul>
<li>비어있는 리스트를 만들기 위해서는 List.of() 를 사용하면 된다.</li>
<li>animalList.add(&quot;???&quot;) 처럼 뭔가를 넣으면 UnsupportedOperationException이 터진다.<br />


</li>
</ul>
<h2 id="2-set">2. Set</h2>
<h3 id="1-setof">1) Set.of()</h3>
<pre><code class="language-java">Set&lt;String&gt; animalSet = Set.of(&quot;Dog&quot;, &quot;Cat&quot;, &quot;Duck&quot;);</code></pre>
<ul>
<li>of()에 중복 인자가 들어갈 경우 IllegalArgumentException이 발생한다.</li>
</ul>
<br />


<h2 id="3-map">3. Map</h2>
<h3 id="1-mapof">1) Map.of()</h3>
<pre><code class="language-java">Map&lt;Integer, String&gt; animalMap = Map.of(1, &quot;Dog&quot;, 2, &quot;Cat&quot;, 3, &quot;Duck&quot;);</code></pre>
<br />

<h3 id="2-mapofentries">2) Map.ofEntries()</h3>
<pre><code class="language-java">Map&lt;Integer, String&gt; animalMap = Map.ofEntries(entry(1,&quot;Dog&quot;), entry(2,&quot;Cat&quot;), entry(3,&quot;Duck&quot;));</code></pre>
<ul>
<li>둘 다 animalMap.add(4, &quot;???&quot;) 처럼 뭔가를 넣으면 UnsupportedOperationException이 터진다.</li>
</ul>
<br />
<br />
<br />
<br />

<hr>
<h1 id="2-불변-컬렉션-구성">2. 불변 컬렉션 구성</h1>
<p>왜 UnsupportedOperationException이 터지는지 알아보자.</p>
<br />

<h2 id="1-listof-setof-mapof">1. List.of(), Set.of(), Map.of()</h2>
<h3 id="1-불변-list-set-map">1) 불변 List, Set, Map</h3>
<p>ImmutableCollections.java</p>
<pre><code class="language-java">class ImmutableCollections {

    ...

    @jdk.internal.ValueBased
    static abstract class AbstractImmutableCollection&lt;E&gt; extends AbstractCollection&lt;E&gt; {
        // all mutating methods throw UnsupportedOperationException
        @Override public boolean add(E e) { throw uoe(); }
        @Override public boolean addAll(Collection&lt;? extends E&gt; c) { throw uoe(); }
        @Override public void    clear() { throw uoe(); }
        @Override public boolean remove(Object o) { throw uoe(); }
        @Override public boolean removeAll(Collection&lt;?&gt; c) { throw uoe(); }
        @Override public boolean removeIf(Predicate&lt;? super E&gt; filter) { throw uoe(); }
        @Override public boolean retainAll(Collection&lt;?&gt; c) { throw uoe(); }
    }

    ...

    @jdk.internal.ValueBased
    static abstract class AbstractImmutableList&lt;E&gt; extends AbstractImmutableCollection&lt;E&gt;
            implements List&lt;E&gt;, RandomAccess {

    }

    ...

    @jdk.internal.ValueBased
    static abstract class AbstractImmutableSet&lt;E&gt; extends AbstractImmutableCollection&lt;E&gt;
            implements Set&lt;E&gt; {

    }

    @jdk.internal.ValueBased
    abstract static class AbstractImmutableMap&lt;K,V&gt; extends AbstractMap&lt;K,V&gt; implements Serializable {

    }
}
</code></pre>
<ul>
<li>ImmutableCollections 클래스 내 AbstractImmutableList와 AbstractImmutableSet은 위의 AbstractImmutableCollection을 상속받고 있다.</li>
<li>상위 클래스인 AbstractImmutableCollection은 불변 컬렉션에 대해 변형시키는 메소드를 호출할 시 <code>UnsupportedOperationException</code>을 던지고 있다.</li>
<li>AbstractImmutableMap은 AbstractMap을 상속받고 있는데 기본적으로 AbstractMap에서 <code>put()</code> 과 같은 연산을 하면 <code>UnsupportedOperationException</code>을 던진다.</li>
</ul>
<br />
<br />

<h3 id="2-static-of로-객체-생성-후-add-put-했을-때-unsupportedoperationexception-발생하는-이유">2) static of()로 객체 생성 후 add(), put() 했을 때 UnsupportedOperationException 발생하는 이유</h3>
<ul>
<li>List, Set, Map은 공통적으로 of()를 통해 객체를 생성할 수 있다.</li>
<li>해당 불변 컬렉션 어떻게 구성되어 있는지 대표적으로 Set을 통해 알아볼 예정이다. 
(나머지도 거의 유사하게 구성되어 있다.)</li>
</ul>
<br />

<p>ImmutableCollections.java</p>
<pre><code class="language-java">class ImmutableCollections {
    @jdk.internal.ValueBased
    static final class Set12&lt;E&gt; extends AbstractImmutableSet&lt;E&gt;
            implements Serializable {
}</code></pre>
<br />


<p>Set.java</p>
<pre><code class="language-java">public interface Set&lt;E&gt; extends Collection&lt;E&gt; {
    @SuppressWarnings(&quot;unchecked&quot;)
    static &lt;E&gt; Set&lt;E&gt; of() {
        return (Set&lt;E&gt;) ImmutableCollections.EMPTY_SET;
    }

    static &lt;E&gt; Set&lt;E&gt; of(E e1) {
        return new ImmutableCollections.Set12&lt;&gt;(e1);
    }
}</code></pre>
<ul>
<li>of() 메소드에서 Set12 객체를 생성하고 있다.</li>
<li>Set12는 위의 AbstractImmutableSet을 상속받고 있고 이는 불변객체가 생성됨을 의미한다.</li>
</ul>
<br />
<br />
<br />
<br />


<blockquote>
<p><strong>참고</strong>
Java9의 불변 컬렉션 생성</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Oracle Consistent 모드 vs Current 모드]]></title>
            <link>https://velog.io/@jduckling_1024/Oracle-Consistent-%EB%AA%A8%EB%93%9C-vs-Current-%EB%AA%A8%EB%93%9C</link>
            <guid>https://velog.io/@jduckling_1024/Oracle-Consistent-%EB%AA%A8%EB%93%9C-vs-Current-%EB%AA%A8%EB%93%9C</guid>
            <pubDate>Wed, 31 Aug 2022 16:01:35 GMT</pubDate>
            <description><![CDATA[<h1 id="1-consistent-모드-읽기">1. Consistent 모드 읽기</h1>
<ul>
<li>SCN 확인 과정을 거치며 쿼리가 시작된 시점을 기준으로 일관성 있는 상태로 블록을 액세스하는 것</li>
<li>SQL 트레이스 Call 통계의 query 항목, AutoTrace에서의 consistent gets</li>
</ul>
<br />
<br />
<br />
<br />

<h1 id="2-current-모드-읽기">2. Current 모드 읽기</h1>
<ul>
<li>SQL문이 시작된 시점이 아니라 데이터를 찾아간 바로 그 시점의 최종 값을 읽으려고 블록을 액세스하는 것</li>
<li>SQL 트레이스 Call 통계의 current 항목, AutoTrace에서의 db block gets</li>
</ul>
<br />
<br />
<br />
<br />


<h1 id="3-실습">3. 실습</h1>
<h2 id="1-테이블-생성">1) 테이블 생성</h2>
<pre><code>create table 계좌1
nologging
as
select 7788 계좌번호, &#39;JIN&#39; 계좌명, 1000 잔고 from dual;

create table 계좌2
nologging
as
select 7788 계좌번호, &#39;JIN&#39; 계좌명, 1000 잔고, 2000 총잔고 from dual;

alter table 계좌1 add constraint 계좌1_PK primary key (계좌번호);
alter table 계좌2 add constraint 계좌2_PK primary key (계좌번호);</code></pre><br />

<p><strong>계좌1</strong></p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/6f19cab5-b7fc-48a6-b274-5bdf69b64bb1/image.png" alt=""></p>
<p><strong>계좌2</strong>
<img src="https://velog.velcdn.com/images/jduckling_1024/post/2cbae579-64d3-4211-8673-ce651cd1f020/image.png" alt=""></p>
<br />
<br />
<br />

<h2 id="1-실습-1">1) 실습 1</h2>
<p><strong>세션 1</strong></p>
<pre><code>update 계좌1 set 잔고 = 잔고 + 100 where 계좌번호 = 7788; -- 1100
update 계좌2 set 잔고 = 잔고 + 200 where 계좌번호 = 7788; -- 1200</code></pre><ul>
<li>두 쿼리를 실행하고 commit하지 않는다. 이 상태에서 다른 세션에서 다른 update문을 날린다.</li>
</ul>
<br />
<br />
<br />

<p><strong>세션 2</strong></p>
<pre><code>update 계좌2
set 총잔고 = 계좌2.잔고 + (select 잔고 from 계좌1 where 계좌번호 = 계좌2.계좌번호)
where 계좌번호 = 7788;</code></pre><ul>
<li>계좌번호가 7788인 row에 lock이 걸린 상태에서 세션 1에서 커밋하면 세션 2의 update문도 수행된다.</li>
</ul>
<br />
<br />
<br />

<p><strong>결과</strong>
<img src="https://velog.velcdn.com/images/jduckling_1024/post/96f81dee-fba9-4166-a504-e11534d2cc7d/image.png" alt=""></p>
<ul>
<li>위 순서대로 실행시킨 결과 총잔고는 2200이 나온다.</li>
<li>계좌2.잔고 = 1200</li>
<li><blockquote>
<p>Current모드로 읽게 되어 세션1에서 commit한 이후의 값을 읽은 것이다.</p>
</blockquote>
</li>
<li>계좌1의 잔고 = 1000 </li>
<li><blockquote>
<p>select문은 Consistent 모드로 읽게 되어 세션1에서 commit하기 전의 값인 1000을 읽는 것이다.</p>
</blockquote>
</li>
</ul>
<br />
<br />
<br />

<h2 id="2-세션2에서-다른-쿼리를-수행한다면">2) 세션2에서 다른 쿼리를 수행한다면?</h2>
<pre><code>update 계좌2
set 총잔고 = (select 잔고 + 계좌2.잔고 from 계좌1 where 계좌번호 = 계좌2.계좌번호)
where 계좌번호 = 7788;</code></pre><ul>
<li>위 쿼리와의 차이점은 계좌2.잔고를 select문 안으로 넣었다.</li>
</ul>
<br />
<br />
<br />


<p><strong>결과</strong>
<img src="https://velog.velcdn.com/images/jduckling_1024/post/018aafff-d0db-4446-83ed-741806834a3f/image.png" alt=""></p>
<ul>
<li>위 순서대로 실행시킨 결과 총잔고는 2300이 나온다.</li>
<li>계좌1.잔고(1100)와 계좌2.잔고(1200) 모두 current 모드로 읽었기 때문에 일관성 있게 갱신되었다.</li>
<li>current 모드로 읽힐 계좌2의 잔고가 서브쿼리로 들어가게 되면서 스칼라 서브쿼리 또한 current 모드로 동작하게 된다. (원래 스칼라 서브쿼리는 consistent 모드로 읽는다.)</li>
</ul>
<br />
<br />
<br />
<br />

<blockquote>
<h3 id="정리">정리</h3>
</blockquote>
<ul>
<li>오라클은 update문 수행 시 대상 레코드를 읽을 때는 Consistent 모드로 읽고 실제 값을 변경할 때는 Current 모드로 읽는다.</li>
<li>select문은 Consistent 모드로 읽고, insert, update, delete, merge는 Current 모드로 읽고 쓴다.</li>
<li>다만, 갱신할 대상 레코드를 식별하는 작업은 Consistent 모드로 이루어진다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mac M1에서 MSSQL 사용하기]]></title>
            <link>https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-MSSQL-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-MSSQL-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 21 Aug 2022 04:57:15 GMT</pubDate>
            <description><![CDATA[<h1 id="1-docker-설치">1. Docker 설치</h1>
<pre><code>brew install --cask docker
</code></pre><br />
<br />

<h1 id="2-mssql-docker-container-생성">2. MSSQL Docker Container 생성</h1>
<h3 id="도커-이미지-가져오기">도커 이미지 가져오기</h3>
<pre><code>docker pull mcr.microsoft.com/azure-sql-edge
</code></pre><br />


<h3 id="컨테이너-실행">컨테이너 실행</h3>
<pre><code>docker run -e &quot;ACCEPT_EULA=1&quot; -e &quot;MSSQL_SA_PASSWORD=Rajhans123@&quot; -e &quot;MSSQL_PID=Developer&quot; -e &quot;MSSQL_USER=SA&quot; -p 1433:1433 -d --name=mssql mcr.microsoft.com/azure-sql-edge</code></pre><br />
<br />

<h1 id="3-datagrip을-이용해-mssql에-접속">3. DataGrip을 이용해 MSSQL에 접속</h1>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/80c5226d-333a-4fcf-b111-77ad8123fa32/image.png" alt=""></p>
<br />
<br />
<br />
<br />


<blockquote>
<p>패러렐즈를 이용해 Window11에 SQL Server 2019 설치를 시도했으나 <code>0xe0000235</code> 와 같은 오류코드와 함께 실패했다. (찾아보니 확실하지는 않지만 버전 문제라고 하는 것 같다.)
<br />
참고 자료 : <a href="https://macguided.ngontinh24.com/articles/can-i-install-sql-server-on-mac-m1">https://macguided.ngontinh24.com/articles/can-i-install-sql-server-on-mac-m1</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[IN vs EXISTS에 대한 고찰]]></title>
            <link>https://velog.io/@jduckling_1024/IN-vs-EXISTS%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@jduckling_1024/IN-vs-EXISTS%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Sat, 23 Jul 2022 16:22:33 GMT</pubDate>
            <description><![CDATA[<h1 id="1-in">1. IN</h1>
<h3 id="정의">정의</h3>
<ul>
<li>조건에 해당하는 ROW의 컬럼을 비교하여 체크한다.</li>
<li>서브 쿼리의 결괏값을 메인 쿼리에 대입하여 조건 비교 후 결과를 출력한다.</li>
</ul>
<br />
<br />

<h3 id="예제">예제</h3>
<pre><code>SELECT a.empno
     , a.ename
     , a.deptno
  FROM emp a
 WHERE a.job = &#39;MANAGER&#39;
   AND a.empno IN (SELECT aa.empno
                 FROM dept_history aa)</code></pre><br />
<br />
<br />
<br />


<h1 id="2-exists">2. EXISTS</h1>
<h3 id="정의-1">정의</h3>
<ul>
<li>조건에 해당하는 ROW의 존재 유무 체크 후 더이상 수행하지 않으며 SELECT절을 평가하지 않으므로 일반적으로 IN에 비해 성능이 좋다.</li>
<li>메인 쿼리의 결괏값을 서브 쿼리에 대입하여 조건 비교 후 결과를 출력한다.</li>
</ul>
<br />
<br />

<h3 id="예제-1">예제</h3>
<pre><code>SELECT a.empno
     , a.ename
     , a.deptno
  FROM emp a
 WHERE a.job = &#39;MANAGER&#39;
   AND EXISTS (SELECT 1
                 FROM dept_history aa
                WHERE aa.empno = a.empno)</code></pre><br />
<br />

<h3 id="exists를-사용하면서-저지른-실수">EXISTS를 사용하면서 저지른 실수</h3>
<p><strong>원래 목적</strong></p>
<pre><code>select *
from 월별계좌상태 B
where 상태구분코드 &lt;&gt; &#39;01&#39;
  and 기준연월 = :base_dt
  and exists(select &#39;X&#39;
             from 계좌원장 A
             where A.계좌번호 = B.계좌번호
               and A.계좌일련번호 = B.계좌일련번호
               AND A.개설일자 like :std_ym || &#39;%&#39;);</code></pre><br />

<p><strong>실수</strong></p>
<pre><code>select *
from 월별계좌상태
where 상태구분코드 &lt;&gt; &#39;01&#39;
  and 기준연월 = :base_dt
  and exists(select &#39;X&#39;
             from 계좌원장 A
             where A.계좌번호 = 계좌번호
               and A.계좌일련번호 = 계좌일련번호
               AND A.개설일자 like :std_ym || &#39;%&#39;);</code></pre><p>여기서 서브쿼리에 alias를 달지 않은 컬럼은 서브쿼리 내 테이블의 컬럼으로 인식하는 것 같다. 서브쿼리를 사용할 때는 alias를 달아주는 것이 안전할 것 같다.</p>
<br />
<br />
<br />
<br />
<br />

<h1 id="3-sql-튜닝-관점에서-in과-exists의-차이점">3. SQL 튜닝 관점에서 IN과 EXISTS의 차이점</h1>
<p><strong>[인덱스 구성]</strong>
월말계좌상태_PK : 계좌번호 + 계좌일련번호 + 기준연월
월말계좌상태_X1 : 기준연월 + 상태구분코드</p>
<p><strong>[SQL]</strong></p>
<pre><code>select *
from 월별계좌상태
where 상태구분코드 &lt;&gt; &#39;01&#39;
  and 기준연월 = :base_dt
  and 계좌번호 || 계좌일련번호 in (select 계좌번호 || 계좌일련번호 from 계좌원장 where 개설일자 like :std_ym || &#39;%&#39;);</code></pre><p>해당 SQL을 월말계좌상태_PK 인덱스로 Range Scan 가능하도록 재작성하는 문제였다.</p>
<br />
<br />

<h3 id="정답">정답</h3>
<pre><code>select *
from 월별계좌상태
where 상태구분코드 &lt;&gt; &#39;01&#39;
  and 기준연월 = :base_dt
  and (계좌번호, 계좌일련번호) in (select A.계좌번호, A.계좌일련번호
                         from 계좌원장 A
                         where A.개설일자 like :std_ym || &#39;%&#39;);</code></pre><p><img src="https://velog.velcdn.com/images/jduckling_1024/post/5e0aa8d3-f4ff-475b-8906-c6258dba95c2/image.png" alt=""></p>
<br />
<br />

<h3 id="오답">오답</h3>
<pre><code>select *
from 월별계좌상태 B
where 상태구분코드 &lt;&gt; &#39;01&#39;
  and 기준연월 = :base_dt
  and exists(select &#39;X&#39;
             from 계좌원장 A
             where A.계좌번호 = B.계좌번호
               and A.계좌일련번호 = B.계좌일련번호
               AND A.개설일자 like :std_ym || &#39;%&#39;);</code></pre><ul>
<li>위의 IN과 EXISTS를 실행시켰을 때 결과는 동일하게 나온다. 하지만 EXISTS는 실행계획이 원하는대로 나오지 않는 것을 알 수 있었다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/36e1ce02-992c-4252-bffa-9c2e0ce5f452/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mac M1에서 Parallels Windows 환경에 설치한 Oracle DB에 접속하기]]></title>
            <link>https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-Parallels-Windows-%ED%99%98%EA%B2%BD%EC%97%90-%EC%84%A4%EC%B9%98%ED%95%9C-Oracle-DB%EC%97%90-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-Parallels-Windows-%ED%99%98%EA%B2%BD%EC%97%90-%EC%84%A4%EC%B9%98%ED%95%9C-Oracle-DB%EC%97%90-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 10 Jun 2022 15:26:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>Mac M1에서 Oracle Database 사용하기</strong>
<a href="https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-Oracle-DB-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-Oracle-DB-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</a></p>
</blockquote>
<p>하지만 Parallels Windows 환경에서 Datagrip을 사용하기 개인적으로 약간 불편했다. 그래서 Mac에 Datagrip을 설치하고 Windows 환경에 설치한 Oracle에 접속해서 사용하기로 했다.</p>
<br />
<br />

<h1 id="1-parallels가-사용하는-사설-ip-주소-확인하기">1. Parallels가 사용하는 사설 IP 주소 확인하기</h1>
<p>Windows 환경에서 사용하는 사설 IP 주소는 cmd에서 <code>ipconfig</code> 명령어를 통해 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/2f7c4219-58ce-41bb-9b18-a6d33c2856ff/image.png" alt=""></p>
<p>여기서 IPv4 주소를 사용하면 된다.</p>
<br />
<br />
<br />
<br />

<hr>
<h1 id="2-접속-시도-1---실패">2. 접속 시도 (1) - 실패</h1>
<p>Datagrip의 좌측 상단에 + 버튼을 누르면 새 Data Source를 생성할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/477a1727-e631-4474-a325-61b96ae1da15/image.png" alt=""></p>
<br />
<br />

<p>그럼 이런 창이 뜨는데,</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/8c713dad-40c9-4f3d-b1fb-a8f03380e9dd/image.png" alt=""></p>
<p>Host에 위에서 확인한 IPv4 주소를 넣어주면 될 것 같았다.</p>
<br />


<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/6375f9a4-426c-4967-9ea7-99ad13b29925/image.png" alt=""></p>
<p>하지만 이런 오류와 함께 접속 실패하였다.</p>
<br />
<br />
<br />
<br />

<hr>
<h1 id="3-해결-방법">3. 해결 방법</h1>
<h2 id="1-새-인바운드-규칙-추가">1. 새 인바운드 규칙 추가</h2>
<h3 id="1-제어판에서-좌측-고급-설정-클릭">(1) 제어판에서 좌측 고급 설정 클릭</h3>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/b8f0dbea-576b-4515-9e70-420a6d157fe0/image.png" alt=""></p>
<br />

<h3 id="2-인바운드-규칙-설정">(2) 인바운드 규칙 설정</h3>
<p>인바운드 규칙에서 우측 새 규칙을 클릭해준다.<img src="https://velog.velcdn.com/images/jduckling_1024/post/1b2d4492-e6c6-4e97-a75f-68730488a19f/image.png" alt=""></p>
<br />

<p>포트를 선택하고 다음으로 넘어간다.
<img src="https://velog.velcdn.com/images/jduckling_1024/post/ff84a67a-66eb-4147-95be-a1f5874868aa/image.png" alt=""></p>
<br />
특정 로컬 포트를 선택해준다. Oracle 기본 포트는 1521이다.

<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/57bceb3c-58e9-4d1e-9f0f-75e1eccf19db/image.png" alt=""></p>
<br />


<p>연결 허용 누르고 다음으로 넘어간다.</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/e523e797-2ba3-4e1a-9944-5d92790646c3/image.png" alt=""></p>
<br />

<p>도메인, 개인, 공용 모두 선택해준 후 다음으로 넘어간다.</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/086cc1be-d8eb-448a-a34f-0f88ed915de7/image.png" alt=""></p>
<br />

<p>이름과 설명 추가 후 마친다.</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/d86548cf-1c12-41e5-aba1-159e36740d1e/image.png" alt=""></p>
<br />
<br />





<h3 id="3-listenerora-파일-수정">(3) listener.ora 파일 수정</h3>
<p>이제 Listener 파일을 수정해야 한다. listener.ora 파일은 <code>Oracle 설치 위치\WINDOWS.X64_193000_db_home\network\admin</code> 에 있다.</p>
<pre><code>LISTENER =
  (DESCRIPTION_LIST =
    (DESCRIPTION =
      (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))
      (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1521))
    )
  )</code></pre><p>여기서 <code>HOST = localhost</code>를 볼 수 있는데 localhost 대신 호스트명을 넣어주면 된다. 호스트명은 cmd에서 <code>hostname</code>이라고 치면 알 수 있다.</p>
<br />
<br />
<br />
<br />


<hr>
<h1 id="4-접속-시도-2---성공">4. 접속 시도 (2) - 성공</h1>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/640aa394-1090-44da-b20c-75b9c91148bb/image.png" alt=""></p>
<p>이 과정을 마치면 성공적으로 접속한 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mac M1에서 Oracle DB 사용하기]]></title>
            <link>https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-Oracle-DB-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@jduckling_1024/Mac-M1%EC%97%90%EC%84%9C-Oracle-DB-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 07 Jun 2022 13:19:10 GMT</pubDate>
            <description><![CDATA[<h1 id="1-parallels-desktop">1. Parallels Desktop</h1>
<p><a href="https://www.parallels.com/blogs/parallels-desktop-apple-silicon-mac/">여기</a>에서 Parallel Desktop을 설치한다. </p>
<br />
<br />
<br />
<br />

<hr>
<h1 id="2-oracle-설치">2. Oracle 설치</h1>
<h3 id="1-oracle-홈페이지에서-다운로드">1. Oracle 홈페이지에서 다운로드</h3>
<p><a href="https://www.oracle.com/database/technologies/oracle-database-software-downloads.html#19c">여기</a>에서 Oracle을 설치한다. Parallels에서 사용할 운영체제는 Windows이므로 Microsoft Windows x64 (64-bit) zip 파일을 다운로드해준다.</p>
<br /> 

<p>설치하면 이런 압축파일이 생기는데 압축을 풀어준다.
<img src="https://velog.velcdn.com/images/jduckling_1024/post/6456ac60-a130-478e-8182-7cc4df774b68/image.png" alt=""></p>
<br />
<br/ >

<p>압축을 풀어주고 적당한 곳에 넣어준다. 본인은 보기 편하게 바탕화면의 oracle 폴더를 만들고 그 안에 넣어주었다.</p>
<br />

<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/355a2223-d7eb-4733-913e-2113cc0e77e1/image.png" alt=""></p>
<blockquote>
<h3 id="주의할-점">주의할 점</h3>
<p>단, 여기서 주의할 점이 있다.
<img src="https://velog.velcdn.com/images/jduckling_1024/post/77bd0c2b-b0da-4d64-b394-5fddc0bb6e2e/image.png" alt="">
내 PC의 바탕화면은 본인 MAC이랑 같이 쓰는 바탕화면이다. 즉, MAC에서 바탕화면에 뭔가를 추가하면 여기에도 동기화되어 같이 추가된다.
<img src="https://velog.velcdn.com/images/jduckling_1024/post/db395408-0aef-4a96-bad6-9b4cfc6bf7c8/image.png" alt="">
위와 동일하게 파일이 저장되어 있음을 MAC에서 확인할 수 있다. 
<br />
위에서 말한 적당한 곳이 Windows 상의 경로여야 한다. 그 이유는 아래에서 알 수 있다.</p>
</blockquote>
<br />
<br />
<br />

<h3 id="2-oracle-설치하기">2. Oracle 설치하기</h3>
<p>폴더를 열면 아래와 같이 구성되어 있는 것을 확인할 수 있는데 여기서  setup 응용 프로그램을 실행시켜준다. 
<img src="https://velog.velcdn.com/images/jduckling_1024/post/7ec29088-fc6c-407d-91bd-6a3c33526bf4/image.png" alt=""></p>
<br />
<br />


<p>이렇게 설치 프로그램이 실행되는데 별다른 설정이 필요없다면 계속하여 다음을 눌러준다.
<img src="https://velog.velcdn.com/images/jduckling_1024/post/39c571e6-5b2b-4c47-9a4f-9ea812b266fb/image.png" alt=""></p>
<br />
<br />

<p>계속해서 진행하고 5번째 단계에 도달하면 데이터베이스 설정 창이 아래와 같이 나오게 된다.</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/9a38cf8d-f9ae-427b-aaa5-2f4ee8d14c7a/image.png" alt=""></p>
<blockquote>
<p><strong>참고</strong>
이미 설치를 해버려서 다시 이 페이지를 띄우려니 뜨지 않았다.
 (해당 <a href="https://itsjh.tistory.com/38">블로그</a>를 보고 설치하였으며 이미지 출처 또한 이 블로그에 있습니다.)</p>
</blockquote>
<p>여기서 주의할 점! Oracle Base를 실제 MAC 상에 존재하는 경로로 잡으면 오류가 발생한다. 따라서 Window 상의 경로로 잡아야 다음 단계로 넘어갈 수 있다.</p>
<br />


<p>계속하여 다음으로 넘어가면 설치를 시작하게 된다. 
(약 1시간 걸린다고 한다. <del>본인은 새벽에 해서 맥북 켜놓고 잤다</del>)</p>
<br />
<br />
<br />

<h3 id="3-설치-확인">3. 설치 확인</h3>
<p>cmd에 들어가 sqlplus를 입력한다.
<img src="https://velog.velcdn.com/images/jduckling_1024/post/bee20611-12f8-4551-8724-f73c530964cc/image.png" alt=""></p>
<br />
사용자명과 비밀번호를 입력하면 접속이 완료된다.

<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/c71df57a-df17-452c-9c35-1c72e391abc4/image.png" alt=""></p>
<br />

<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/5f8723a0-2dea-483b-8f19-24d40df9313d/image.png" alt=""></p>
<p>성공!</p>
<br />
<br />

<h3 id="4-번외-datagrip에서-oracle-사용하기">4. (번외) Datagrip에서 oracle 사용하기</h3>
<p>개인적으로 CLI보다는 더 편리한 툴을 사용하고 싶다. <a href="https://www.jetbrains.com/ko-kr/datagrip/download/#section=mac">여기</a>에서 Datagrip을 설치해준다.</p>
<br />
<br />

<p>설치 후 Datagrip을 열어준다.</p>
<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/11304970-b634-463b-a11c-22f9a52b6e14/image.png" alt=""></p>
<p>좌측 + -&gt; Data Source -&gt; Oracle을 클릭해준다.
(본인은 이미 세팅을 완료한 상태이기 때문에 맨 위에 Oracle이 뜬다.)</p>
<br />
<br />

<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/6569583f-9d91-4636-b5be-cda53b4499e5/image.png" alt=""></p>
<p>처음에 SID가 XE로 되어있는데 이를 orcl로 변경해줘야 connection이 성공한다. 처음에는 driver가 설치되지 않았을텐데, driver도 설치해준다.</p>
<br />
<br />
<br />

<p><img src="https://velog.velcdn.com/images/jduckling_1024/post/c26006dd-0c0b-45b8-a267-677d29f1d1e4/image.png" alt=""></p>
<p>성공!</p>
<br />
<br />
<br />
<br />

<hr>
<h1 id="3-m1에서-oracle을-사용할-수-있는-다른-방법">3. M1에서 Oracle을 사용할 수 있는 다른 방법</h1>
<br />

<ul>
<li>Oracle Cloud 회원가입 후 사용</li>
<li><a href="https://plankim.com/blog/2022/04/18/m1-%EB%A7%A5-%EC%9C%88%EB%8F%84%EC%9A%B0-11-%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95/">UTM</a></li>
<li><a href="https://stackoverflow.com/questions/68605011/oracle-12c-docker-setup-on-apple-m1">lima</a></li>
<li><a href="https://developers.ascendcorp.com/how-to-install-oracle-instant-client-on-apple-silicon-m1-24b67f2dc743">Rosetta</a></li>
</ul>
<br />

<p>몇 시간동안 삽질한 결과 크게 이런 결과를 얻을 수 있었는데 다 실패했다... 위 방법들을 시도하면서 놓치고 있는 것이 있을 수도, 더 좋은 방법이 있을 수도 있을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파일 및 패키지 이름 변경으로 인해 Git branch 이동이 안될 때]]></title>
            <link>https://velog.io/@jduckling_1024/%ED%8C%8C%EC%9D%BC-%EB%B0%8F-%ED%8C%A8%ED%82%A4%EC%A7%80-%EC%9D%B4%EB%A6%84-%EB%B3%80%EA%B2%BD%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%95%B4-Git-branch-%EC%9D%B4%EB%8F%99%EC%9D%B4-%EC%95%88%EB%90%A0-%EB%95%8C</link>
            <guid>https://velog.io/@jduckling_1024/%ED%8C%8C%EC%9D%BC-%EB%B0%8F-%ED%8C%A8%ED%82%A4%EC%A7%80-%EC%9D%B4%EB%A6%84-%EB%B3%80%EA%B2%BD%EC%9C%BC%EB%A1%9C-%EC%9D%B8%ED%95%B4-Git-branch-%EC%9D%B4%EB%8F%99%EC%9D%B4-%EC%95%88%EB%90%A0-%EB%95%8C</guid>
            <pubDate>Tue, 01 Mar 2022 05:58:28 GMT</pubDate>
            <description><![CDATA[<h3 id="에러-발생">에러 발생</h3>
<p>클래스 명이나 패키지 명이 네이밍 컨벤션에 맞지 않아 변경해야 할 경우가 있다. 해당 이슈를 수행한 후 다음 이슈를 처리하러 feature -&gt; develop 브랜치로 이동하고자 할 때 아래와 같은 에러를 접하게 된다.</p>
<pre><code>error: The following untracked working tree files would be overwritten by checkout:</code></pre><br />
<br />
<br />
<br />

<h3 id="해결-방법">해결 방법</h3>
<p>원래 master에 아래와 같은 class가 있었다고 가정해보자.</p>
<pre><code class="language-java">package com.example.test.testpackage;

public class testclass {

}</code></pre>
<br />

<p>컨벤션을 지키지 않은 파일 및 패키지 명을 변경하는 이슈를 받고 master로부터 새 브랜치(new-branch)를 파서...</p>
<br />

<pre><code class="language-java">package com.example.test.testpackage;

public class TestClass {

}</code></pre>
<p><img src="https://images.velog.io/images/jduckling_1024/post/3d152bb4-0716-46aa-b407-309145d9557c/image.png" alt=""></p>
<br />

<p>이렇게 수정하고 push하고 PR까지 올렸다.</p>
<br />
<br />

<p>merge 승인받을 때까지 다른 이슈를 처리해야지! 하고 이제 다시 master로 switch해볼 것이다.
<code>git switch master</code></p>
<p><img src="https://images.velog.io/images/jduckling_1024/post/8d4343f0-69e3-4f05-9e51-24a2b2c6567e/image.png" alt=""></p>
<p>그럼 이런 오류를 볼 수 있다. new-branch(현재 브랜치)에는 TestClass.java가 있지만 master 브랜치에는 testclass.java가 있기 때문에 브랜치 이동이 불가능한 상황이다.</p>
<p><img src="https://images.velog.io/images/jduckling_1024/post/d36136d9-3a71-4a6c-8b79-8329b3bdb375/image.png" alt=""></p>
<blockquote>
<h4 id="참고-1">참고 1</h4>
<p>이것저것 하면서 local의 master를 삭제했기 때문에 원격의 master를 추적하는 로컬 master 브랜치를 생성하면서 switch하기 위해 위의 캡쳐본에서는 <code>checkout</code>을 사용한 것이다. 로컬 기준 특정 파일명이 현재 브랜치와 이동할 브랜치 간 다르면 똑같은 에러를 볼 수 있을 것이다.</p>
</blockquote>
<br />
<br />
<br />
<br />

<p>이럴 때는 HEAD의 위치를 조정하여 이동할 브랜치와 현재 브랜치의 파일명 대소문자를 맞춰줘야 한다. </p>
<p><img src="https://images.velog.io/images/jduckling_1024/post/b31145ae-8038-4529-9230-edf491b9699d/image.png" alt=""></p>
<p>현재 new-branch의 HEAD는 &#39;컨벤션 지킨 클래스명&#39; 에 있다. (new-branch와 origin/new-branch가 보이는데 로컬 기준으로 봐야하기 때문에 new-branch의 위치를 봐주면 된다.)</p>
<br />
<br />
<br />
<br />

<p>이제 HEAD의 위치를 master로 변경하여 현 브랜치와 이동할 브랜치를 맞춰줄 것이다.</p>
<pre><code>git fetch --all
git reset --hard origin/master</code></pre><p><img src="https://images.velog.io/images/jduckling_1024/post/14d19483-b524-4cbe-9a32-7badeb894e24/image.png" alt=""></p>
<p>브랜치 이동이 제대로 된 것을 확인할 수 있다.</p>
<br />
<br />
<br />
<br />

<blockquote>
<h4 id="참고-2">참고 2</h4>
<p>reset --hard의 대상은 원격 브랜치든 로컬 브랜치든 상관 없다. 이동하고 싶은 브랜치 기준으로 HEAD를 맞추는 데에 중점을 두면 될 것 같다. </p>
</blockquote>
<blockquote>
<h4 id="참고-3">참고 3</h4>
<p>혹시 대소문자 변경 이슈를 처음 하는거라면... <code>git config core.ignorecase false</code> 이 설정 한 번 해야 변경된 이름이 반영되는 것 같다.
<a href="https://papababo.tistory.com/entry/git-%EC%9D%80-%ED%8F%B4%EB%8D%94%ED%8C%8C%EC%9D%BC%EB%AA%85%EC%9D%98-%EB%8C%80%EC%86%8C%EB%AC%B8%EC%9E%90%EB%A5%BC-%EA%B0%9C%EB%AC%B4%EC%8B%9C%ED%95%9C%EB%8B%A4-%EA%B7%B8%EB%9F%BC-%EC%9A%B0%EC%A7%B8">git이 파일 혹은 폴더 이름을 🐶 무시할 때</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TM Lock]]></title>
            <link>https://velog.io/@jduckling_1024/TM-Lock</link>
            <guid>https://velog.io/@jduckling_1024/TM-Lock</guid>
            <pubDate>Sat, 26 Feb 2022 16:55:46 GMT</pubDate>
            <description><![CDATA[<h3 id="tm-lock">TM Lock</h3>
<p>TM Lock은 DML 테이블 Lock으로 테이블을 보호하는 Lock이다.</p>
<br />
<br />


<h4 id="emp-테이블">emp 테이블</h4>
<pre><code>select * from emp;</code></pre><p><img src="https://images.velog.io/images/jduckling_1024/post/a9fe922c-aa79-42bd-931f-0d6ca39f9697/image.png" alt=""></p>
<br />
여기서 empno가 1인 emp의 sal을 1024로 갱신하려고 한다.
(참고로 사전에 이미 업데이트 해놓은 상태라 위의 캡처본에는 적용이 되어있다.)

<br />
<br />
<br />
<br />

<p>이제부터 이 트랜잭션을 <strong>트랜잭션A</strong>라고 부를 것이다.</p>
<pre><code>update emp
set sal = 1024
where empno = 1;</code></pre><p><img src="https://images.velog.io/images/jduckling_1024/post/f4e8e899-7809-401a-959b-5289da37e692/image.png" alt="">
그러면 이렇게 정상적으로 업데이트 된다. <strong>아직 commit은 하지 않은 상태다.</strong></p>
<br />
<br />
<br />
<br />

<p>이때, 다른 곳(새 cmd창)에서 empno가 1인 emp의 sal을 4021로 업데이트하고자 한다. 이 트랜잭션은 <strong>트랜잭션B</strong>라고 할 것이다.</p>
<pre><code>update emp
set sal = 1024
where empno = 1;</code></pre><p>이 쿼리를 날리면 날라가지 않고 그대로 멈춰있다. 트랜잭션A에서 이 행에 대해 Lock을 걸고 아직 commit을 하지 않았기 때문이다.</p>
<br />
<br />
<br />
<br />

<p>또 다른 cmd창을 열어 empno가 1인 emp의 sal을 0으로 업데이트 해볼 것이다. 이 트랜잭션은 <strong>트랜잭션C</strong>라고 할 것이다.</p>
<pre><code>update emp 
set sal = 0 
where empno = 1;</code></pre><p>이 쿼리 같은 이유로 역시 날라가지 않은 상태로 멈춰있게 된다.</p>
<br />
<br />
<br />
<br />

<h3 id="모니터링-방법">모니터링 방법</h3>
<p>현재 어떤 상황인지 확인할 때는 아래와 같은 쿼리를 날려주면 된다.</p>
<pre><code> SELECT DISTINCT T1.SESSION_ID
     , T2.SERIAL#
     , T4.OBJECT_NAME
     , T2.MACHINE
     , T2.TERMINAL
     , T2.PROGRAM
     , T3.ADDRESS
     , T3.PIECE
     , T3.SQL_TEXT
  FROM V$LOCKED_OBJECT T1
     , V$SESSION T2
     , V$SQLTEXT T3
     , DBA_OBJECTS T4
 WHERE 1=1
   AND T1.SESSION_ID = T2.SID
   AND T1.OBJECT_ID = T4.OBJECT_ID
   AND T2.SQL_ADDRESS = T3.ADDRESS
 ORDER BY T3.ADDRESS, T3.PIECE;</code></pre><p>결과는 다음과 같다.
<img src="https://images.velog.io/images/jduckling_1024/post/e1afd31c-bff4-40a2-bec6-03e4f16ea7f3/image.png" alt="">
맨 아래 두 줄의 SQL_TEXT를 보면 트랜잭션A에 의해 Lock이 발생하여 수행되지 않은 UPDATE 쿼리를 확인할 수 있다.</p>
<br />
<br />
<br />
<br />

<h3 id="commit">COMMIT</h3>
<p>지금과 같은 상황은 트랜잭션A가 commit하지 않았기 때문에 발생하는 현상이다. 이제 트랜잭션A에서 commit을 날려볼 예정이다.</p>
<br />

<p>트랜잭션 A에서 commit을 날렸을 때,</p>
<h4 id="트랜잭션b">트랜잭션B</h4>
<p><img src="https://images.velog.io/images/jduckling_1024/post/decd526e-17e2-4762-bbd7-25ba09005ca6/image.png" alt=""></p>
<br />

<h4 id="트랜잭션c">트랜잭션C</h4>
<p><img src="https://images.velog.io/images/jduckling_1024/post/eab7fdf2-8791-4f34-8a9c-38413c1605d2/image.png" alt=""></p>
<p>트랜잭션B에서 UPDATE 쿼리가 날라가게 된다. 트랜잭션C는 그대로다. 트랜잭션B가 먼저 수행되었기 때문에 위와 같은 결과가 나타난 것이다.</p>
<br />
<br />
<br />
<br />



]]></description>
        </item>
        <item>
            <title><![CDATA[Oracle System 계정 잃어버렸을 때]]></title>
            <link>https://velog.io/@jduckling_1024/Oracle-System-%EA%B3%84%EC%A0%95-%EC%9E%83%EC%96%B4%EB%B2%84%EB%A0%B8%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@jduckling_1024/Oracle-System-%EA%B3%84%EC%A0%95-%EC%9E%83%EC%96%B4%EB%B2%84%EB%A0%B8%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Sat, 26 Feb 2022 05:14:09 GMT</pubDate>
            <description><![CDATA[<h3 id="oracle-system-계정-잃어버렸을-때">Oracle System 계정 잃어버렸을 때</h3>
<ol>
<li>cmd 실행</li>
<li>sqlplus 입력</li>
<li>Enter user name : sys as sysdba, Password: (아무것도 치지 않고 바로 Enter)</li>
<li>alter user system identified by 새 비밀번호
ex) <code>alter user system identified by 1234;</code></li>
<li>conn system/새 비밀번호
ex) <code>conn system/1234</code></li>
</ol>
<br />
<br />

<h3 id="oracle-system-계정-잠겼을-때">Oracle System 계정 잠겼을 때</h3>
<p>몇 번 틀린 비밀번호를 입력하여 로그인을 시도한다면 계정이 잠기게 된다.</p>
<ol>
<li><code>select username, account_status, lock_date from dba_users;</code>
<img src="https://images.velog.io/images/jduckling_1024/post/8550b0e9-ad2e-479f-8e32-3512336e086e/image.png" alt="">
지금은 본인이 SYSTEM 계정 LOCK을 풀었기 때문에 SYSTEM의 ACCOUNT_STATUS가 OPEN으로 되어있지만 잠겨있을 때는 LOCKED로 표시된다.</li>
</ol>
<ol start="2">
<li><code>alter user system account unlock;</code></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[TDD? BDD?]]></title>
            <link>https://velog.io/@jduckling_1024/TDD-BDD</link>
            <guid>https://velog.io/@jduckling_1024/TDD-BDD</guid>
            <pubDate>Sun, 23 Jan 2022 13:21:53 GMT</pubDate>
            <description><![CDATA[<h2 id="테스트케이스">테스트케이스</h2>
<p>테스트케이스의 정의는 다음과 같다.</p>
<blockquote>
<p><strong>테스트케이스</strong></p>
</blockquote>
<ul>
<li>본인이 작성한 코드가 예상한 대로 잘 동작하는지 예상되는 값을 부여하여 어플리케이션 빌드 시 동작을 검증하는 것</li>
<li>타겟 코드에 대한 기대 행동을 제공하는 시나리오</li>
</ul>
<br />
<br />
<br />
<br />

<h2 id="tdd-test-driven-development">TDD (Test Driven Development)</h2>
<p>TDD는 <strong>테스트 주도 개발</strong>의 줄임말로 우선 테스트케이스를 작성한 뒤 해당 기능에 대한 코드를 작성하는 방법이다. 
테스트를 먼저 작성하다 보니 기능을 만들기 전에 본인이 만들어야 하는 기능들에 대해 좀 더 명확하게 그릴 수 있다는 장점이 있다.</p>
<br />
<br />
<br />
<br />

<h2 id="bdd-behavior-driven-development">BDD (Behavior Driven Development)</h2>
<p>BDD는 <strong>행동 주도 개발</strong>의 줄임말로 비즈니스 요구사항에 집중하여 테스트케이스를 작성하는 방법이다.</p>
<p><em>도대체 이게 무슨 말이지? TDD랑 똑같은거 아닌가?</em></p>
<p>TDD랑 그렇게 다르지는 않다. BDD 역시 TDD로부터 파생되어 나온 것이다. </p>
<br />
<br />

<h3 id="bdd가-나오게-된-배경">BDD가 나오게 된 배경</h3>
<ul>
<li>TDD 방식으로 테스트 케이스를 작성하면서 어떤 케이스가 있을지를 생각하면서 작성해야 한다. </li>
<li>흐음 어떤 경우가 있을까??? 아 더 생각나는 케이스가 없는데 누군가가 말해줬으면 좋겠다...</li>
<li>테스트케이스를 상상해서 만드는 것보다 이미 작성된 요구사항이 테스트 케이스가 된다면 부담이 줄어들 것이다.</li>
<li>그렇다면 무작정 테스트 케이스를 코드로 작성하기 보다는 글로 작성해보자!</li>
</ul>
<p>이렇게 나온 것이 BDD다!</p>
<br />
<br />

<h3 id="주로-사용하는-bdd-구조">주로 사용하는 BDD 구조</h3>
<p>BDD는 테스트 케이스를 작성하는 대신, 동작 작성으로 시작하는 방법으로 TDD의 확장이다. 테스트 케이스를 작성하기에 앞서 문장으로 풀어 써볼 것인데, 아래와 같은 형식을 지킬 것이다.</p>
<blockquote>
<p><strong>주어진 환경에서 어떠한 행위를 했을 때 이런 결과가 나올 것이다.</strong></p>
</blockquote>
<p>이 문장을 세 부분으로 나눌 수 있겠다.</p>
<blockquote>
<p> <strong>Given</strong> - 주어진 환경에서
<strong>When</strong> - 어떠한 행위를 했을 때 
<strong>Then</strong> - 이런 결과가 나올 것이다.</p>
</blockquote>
<p>이제 한 예시에 적용해볼 것이다.</p>
<br />
<br />

<h4 id="예시">예시</h4>
<p>MyMath라는 클래스를 만들어 Math.pow(double a, double b)와 동일한 결과를 반환하는 함수 pow(double a, double b)를 구현해본다고 가정해볼 것이다.</p>
<pre><code class="language-java">public class MyMath {
  public static double pow(int a, int b) {
    double result = 1;
    for (int i = 0; i &lt; b; i++) {
      result *= a;
    }
    return result;
  }
}</code></pre>
<br />
위의 형식처럼 문장으로 작성한다면 이렇게 작성할 수 있을 것 같다.

<blockquote>
<p>a=10, b=3이 준비된 상태에서 MyMath.pow(double a, double b)를 호출하였을 때 Math.pow(10, 3)과 결과가 동일한 1000이 나올 것이다.</p>
</blockquote>
<br />

<p>이제 요구사항(?)도 작성해봤으니 이를 테스트 코드로 옮기기만 하면 끝이다!</p>
<pre><code class="language-java">public class TestControllerTest {

  @Test
  void test() {
    // given
    int a = 10;
    int b = 3;

    // when
    double result = MyMath.pow(a, b);

    // then
    assertEquals(result, Math.pow(a, b));
  }
}</code></pre>
<br />
<br />


<h2 id="본인의-생각">본인의 생각</h2>
<p>지금까지 Junit5를 이용하여 테스트 코드는 작성해 보았지만 사실 명확하게 TDD였다 혹은 BDD였다고 말하기가 어려울 것 같다고 생각했다. 테스트 코드를 작성하고 기능을 구현한 것이 아니라, 이미 구현된 기능에 대한(혹은 기능을 구현하고 나서) 테스트 코드를 작성하곤 하였다.</p>
<br />

<p>사실 머리로는 받아들이겠지만 TDD든 BDD든 정석으로 적용하기에는 어려움이 있을 것이라고 생각한다. 테스트 코드 작성하는 데 테스트 케이스를 생각하는 것부터 시작해서 작성하는데 꽤 오래 걸리는 것 같다. (본인이 초보라서 그럴 수도 있겠지만...) 하지만 테스트 코드 작성 자체는 중요한 역할을 한다는 점 자체는 받아들일 수 있을 것 같다. 예전에 로직 변경하면서 리팩토링하다가 작성해뒀던 테스트 코드 다 터뜨려먹고 일부 기능이 제대로 작동하지 않는 것을 확인하여 급하게 수정한 경험이 있었기 때문이다.</p>
<br />

<p><strong>기능 구현 전에 테스트 코드를 작성하든 구현 이후에 작성하든 테스트 코드는 작성하는 습관을 들이자는 것이 결론이다.</strong></p>
<br />
<br />
<br />
<br />

<blockquote>
<p><strong>Reference</strong>
<a href="https://mingule.tistory.com/43">TDD, BDD란?</a>
<a href="https://wonit.tistory.com/493">[Spring Boot] JUnit5 BDDMockito로 알아보는 TDD와 BDD 의 차이 및 BDD 실습</a></p>
</blockquote>
<p>+ 이해한 내용을 바탕으로 작성한 글이니 추가적인 의견 및 정보 있으시다면 댓글로 공유 좀 부탁드립니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JUnit5 @ParameterizedTest]]></title>
            <link>https://velog.io/@jduckling_1024/JUnit5-ParameterizedTest</link>
            <guid>https://velog.io/@jduckling_1024/JUnit5-ParameterizedTest</guid>
            <pubDate>Sat, 22 Jan 2022 14:02:36 GMT</pubDate>
            <description><![CDATA[<p>테스트 코드를 작성하던 중, 한 기능에서 로직은 같지만 입력 파라미터만 다른 테스트케이스를 작성해야 할 일이 있었다. 코드는 중복되지만, 각 케이스에 대한 검증은 필요하므로 테스트 코드를 작성하지 않을 수는 없다. </p>
<br />

<p>이러한 경우 조금 더 코드를 간결하게 작성하는 방법이 있지 않을까 고민해보고 찾아보는 중 Junit5에서 제공하는 <code>@ParameterizedTest</code> 라는 것을 알게 되었다.</p>
<br />
<br />

<h2 id="parameterizedtest">@ParameterizedTest</h2>
<p>@ParameterizedTest는 하나의 테스트를 여러 번 돌려야 할 때 사용한다. </p>
<h4 id="parameterizedtest-적용-방법">@ParameterizedTest 적용 방법</h4>
<ul>
<li>메소드 위에 <code>@ParameterizedTest</code>를 명시한다.</li>
<li>메소드의 파라미터로 순서대로 넘겨줄 배열을 <code>@ValueSource</code>를 이용해 명시해준다. (short, byte, int, long, float, double, char, String, Class 명시 가능)</li>
<li>넘겨줄 값들의 타입을 메소드의 파라미터로 적어준다.</li>
</ul>
<h4 id="예시">예시</h4>
<pre><code class="language-java">import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class TestControllerTest {

  @ParameterizedTest
  @ValueSource(strings = {&quot;test1&quot;, &quot;test2&quot;, &quot;test3&quot;})
  void test(String value) {
    System.out.println(value);
  }
}</code></pre>
<p>이 테스트 코드의 실행 결과는 다음과 같다.
<img src="https://images.velog.io/images/jduckling_1024/post/c18206c7-2565-4bac-adea-f972a9df0b18/image.png" alt=""></p>
<br />

<p>하지만, 이 방법은 파라미터를 하나만 전달할 때만 가능하며 두 개 이상의 파라미터는 전달할 수 없다.</p>
<br />
<br />
<br />
<br />


<h2 id="methodsource">@MethodSource</h2>
<p><code>@MethodSource</code>를 이용하면 두 개 이상의 파라미터 또한 전달받을 수 있도록 할 수 있다.</p>
<h4 id="예시-1">예시</h4>
<pre><code class="language-java">import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class TestControllerTest {

  // test method
  @ParameterizedTest
  @MethodSource(&quot;provideKeyAndValue&quot;)
  void test(Integer key, String value) {
    System.out.println(key + &quot; &quot; + value);
  }

  //source method
  private static Stream&lt;Arguments&gt; provideKeyAndValue() {
    return Stream.of(
        Arguments.of(1, &quot;value1&quot;),
        Arguments.of(2, &quot;value2&quot;),
        Arguments.of(3, &quot;value3&quot;)
    );
  }
}</code></pre>
<br />

<p>단, 이를 사용하기 위해서는 source method가 static이어야 한다.</p>
<br />

<p>하지만, 성격이 같은 테스트케이스끼리 묶을 경우 종종 inner class를 사용하는 경우가 있고, inner class에서는 static method를 선언하지 못한다. </p>
<pre><code class="language-java">import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class TestControllerTest {

  @Nested
  public class NestedTestControllerTest {

    @ParameterizedTest
    @MethodSource(&quot;provideKeyAndValue&quot;)
    public void test(Integer key, String value) {
      System.out.println(key + &quot; &quot; + value);
    }

    // 에러 발생!
    private static Stream&lt;Arguments&gt; provideKeyAndValue() {
      return Stream.of(
          Arguments.of(1, &quot;value1&quot;),
          Arguments.of(2, &quot;value2&quot;),
          Arguments.of(3, &quot;value3&quot;)
      );
    }
  }
}</code></pre>
<p>이렇게 불가능하다는 뜻이다... 이럴 때는 <code>@TestInstance</code>를 사용하면 되는데, 이제부터 <code>@TestInstance</code>에 대해 알아볼 예정이다.</p>
<br />
<br />

<h3 id="testinstance">@TestInstance</h3>
<p>@TestInstance는 테스트 인스턴스의 라이프 사이클을 설정할 때 사용한다.</p>
<ul>
<li>Lifecycle.PER_METHOD (Default) : 테스트 함수 당 인스턴스가 생성된다. </li>
<li>Lifecycle.PER_CLASS : 테스트 클래스 당 인스턴스가 생성된다.</li>
</ul>
<br />

<pre><code class="language-java">import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class TestControllerTest {

  @Nested
  @TestInstance(Lifecycle.PER_CLASS)
  public class NestedTestControllerTest {

    @ParameterizedTest
    @MethodSource(&quot;provideKeyAndValue&quot;)
    public void test(Integer key, String value) {
      System.out.println(key + &quot; &quot; + value);
    }

    private Stream&lt;Arguments&gt; provideKeyAndValue() {
      return Stream.of(
          Arguments.of(1, &quot;value1&quot;),
          Arguments.of(2, &quot;value2&quot;),
          Arguments.of(3, &quot;value3&quot;)
      );
    }
  }
}</code></pre>
<p>이렇게 <code>@TestInstance</code>를 Lifecycle.PER_CLASS로 설정해주면 동일한 test instance에서 모든 test method를 실행할 수 있게 되며 provideKeyAndValue()를 static으로 두지 않고도 @MethodSource를 사용할 수 있게 된다. </p>
<br />
<br />
<br />


<p>사실 위와 같이 Unit Test를 작성해도 괜찮을지는 모르겠지만, 한 번도 접해보지 못한 어노테이션을 사용하였고 알게 되었다는 것에 의의를 둘 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[BDDMockito의 willThrow에 대하여]]></title>
            <link>https://velog.io/@jduckling_1024/BDDMockito%EC%9D%98-willThrow%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@jduckling_1024/BDDMockito%EC%9D%98-willThrow%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sat, 22 Jan 2022 08:19:32 GMT</pubDate>
            <description><![CDATA[<p>테스트 코드를 작성하면서 예외가 발생했을 경우에 대한 케이스 쪽에서 한 가지 궁금한 점이 생겨 찾아보고 정리하게 되었다.</p>
<br />
<br />

<p>BDDMockito의 willThrow를 사용하던 도중, 파라미터로 다음과 같이 작성할 수 있음을 알 수 있었다.</p>
<pre><code class="language-java">given(testService.func(any())).willThrow(SomeException.class);</code></pre>
<p>이처럼 willThrow의 파라미터로 class를 넣을 수도 있고,</p>
<pre><code class="language-java">given(testService.func(any())).willThrow(new SomeException(&quot;message&quot;));</code></pre>
<p>이렇게 예외 관련 객체를 생성하여 파라미터로 넣을 수도 있다.</p>
<br />
<br />

<p><code>willThrow()</code> 함수 안으로 들어가보니...
<img src="https://images.velog.io/images/jduckling_1024/post/464d216a-f7a5-4635-b553-ae339c8933af/image.png" alt="">
...와 같이 이렇게 함수가 오버로딩 되어있음을 확인할 수 있었다.</p>
<p>그렇다면 둘의 차이점은 무엇일까? 직접 돌려봤을 때 stack trace 정보를 나타낼 수 있는지의 여부에서 차이가 발생함을 알 수 있었다.</p>
<br />
<br />
<br />
<br />

<h2 id="1-willthrowclass--extends-throwable-throwabletype">1. willThrow(Class&lt; ? extends Throwable throwableType)</h2>
<p><img src="https://images.velog.io/images/jduckling_1024/post/45b4106a-79b1-4dd0-88cb-5dcea03310d7/image.png" alt=""></p>
<blockquote>
<p><strong>If you require stack trace information, use thenThrow(Throwable...) instead.</strong>
stack trace 정보를 알고 싶다면, thenThrow(Throwable...)을 사용하세요.</p>
</blockquote>
<p>해당 메소드로는 발생한 예외에 대한 stack trace를 알 수 없다.</p>
<br />
<br />
<br />
<br />

<h2 id="2-willthrowthrowable-thorwables">2. willThrow(Throwable... thorwables)</h2>
<p><img src="https://images.velog.io/images/jduckling_1024/post/621746ee-1571-4248-91e7-79bebd8523b5/image.png" alt=""></p>
<blockquote>
<p><strong>you can specify throwables to be thrown for consecutive calls. in that case the last throwable determines the behavior of further consecutive calls.</strong>
연속 호출에 대해 throw될 throwable을 지정할 수 있습니다. 이 경우 마지막 throwable은 추가 연속 호출의 동작을 결정합니다.</p>
</blockquote>
<p>무슨 말일까... 잠시 고민했었는데 본인은 이렇게 이해했다.</p>
<br />
<br />

<h3 id="연속-호출에-대해-throw될-throwable을-지정할-수-있습니다">연속 호출에 대해 throw될 throwable을 지정할 수 있습니다.</h3>
<p>이런 사용자 지정 예외가 있다고 가정해볼 것이다.</p>
<pre><code class="language-java">class CustomException extends RuntimeException {
    public RuntimeException(String message) {
        super(message);
    }
}</code></pre>
<p>그리고 실제로 로직을 수행하다가 최종적으로 던져지는 Exception은 CustomException이다.</p>
<br />
<br />

<h3 id="이-경우-마지막-throwable은-추가-연속-호출의-동작을-결정합니다">이 경우 마지막 throwable은 추가 연속 호출의 동작을 결정합니다.</h3>
<p>마지막 throwable은 CustomException 최상위 class는 Throwable이다.</p>
<pre><code class="language-java">public class Throwable implements Serializable {
...
    public Throwable(String message) {
        fillInStackTrace();
        detailMessage = message;
    }
...
}</code></pre>
<p>생성자에서 <code>fillInStackTrace()</code>가 호출될 것이고...</p>
<br />

<pre><code class="language-java">public class Throwable implements Serializable {
...
    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }
        return this;
    }
...
}</code></pre>
<p><code>fileInStackTrace()</code>는 <code>fillnStackTrace()</code>를 호출할 것이다.</p>
<br />

<pre><code class="language-java">public class Throwable implements Serializable {
...
    private native Throwable fillInStackTrace(int dummy);
...
}</code></pre>
<br />

<p>이렇게 예외를 던진 이후에 순차적으로 함수를 호출한다.</p>
<br />
<br />
<br />
<br />

<p>본인의 생각이 틀릴 수도 있다. 하지만 결론적으로 테스트 도중 stack trace를 보고싶다면 이 <code>willThrow</code>를 사용하면 된다는 사실을 알 수 있었다.</p>
<blockquote>
<h4 id="reference">Reference</h4>
<p><a href="https://escapefromcoding.tistory.com/258">BDD의 willThrow 2가지 비교하기</a>
본인의 궁금증을 해결해준 블로그다. 여기서는 thenThrow에 대해서 정리한 듯 하지만 같은 경우, 같은 내용이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[git reset vs git revert]]></title>
            <link>https://velog.io/@jduckling_1024/git-reset-vs-git-revert</link>
            <guid>https://velog.io/@jduckling_1024/git-reset-vs-git-revert</guid>
            <pubDate>Sun, 16 Jan 2022 16:02:40 GMT</pubDate>
            <description><![CDATA[<p>git reset과 git revert 둘 다 결과적으로는 되돌릴 때 사용하는 것이지만 둘 사이에는 차이가 있다. 이제부터 이 둘의 차이를 알아볼 예정이다. </p>
<br />
<br />
<br />
<br />


<h2 id="i-git-reset">I. git reset</h2>
<p><code>git reset</code>은 좀 전의 commit 내역에 남기지 않고 좀 전의 commit을 취소하고 싶을 때 사용한다.</p>
<blockquote>
<p>현재 commit 이력은 다음과 같다.
<img src="https://images.velog.io/images/jduckling_1024/post/6e9743e8-e4c7-42c8-bf5e-836efa4581c0/image.png" alt="">
모두 a.txt를 가지고 있으며 각 commit의 a.txt 내용은 아래와 같다.
 a : -1
b : 1
c : 0
<br />
각각의 옵션을 붙여 b로 돌아갔을 때 무슨 일이 발생할지 알아볼 것이다.</p>
</blockquote>
<p>참고로, repository, staging area, working directory가 무엇인지는 여기에서 따로 기재하지 않을 예정이다.</p>
<p><code>git reset</code>에는 세 가지 옵션이 있다. <code>--hard</code>, <code>--mixed</code>, <code>--soft</code>가 있는데 각각을 붙였을 때 어떤 일이 일어나는지 살펴보자. </p>
<br />
<br />

<h3 id="1-git-reset---hard">1. git reset --hard</h3>
<p>git reset --hard를 사용하면 repository, staging area, working directory 모두 b의 상태로 돌아가게 된다.</p>
<blockquote>
</blockquote>
<p><code>git reset 6872413 --hard</code>
<img src="https://images.velog.io/images/jduckling_1024/post/b029b2f5-62cc-408b-b9cc-c5daaa5d282c/image.png" alt=""></p>
<ul>
<li>일단 확실히 repository는 b의 상태로 돌아갔을 것이다.</li>
<li><code>cat a.txt</code>로 확인했을 때, -1을 출력하는 것을 볼 수 있고 working directory는 b의 상태로 돌아갔음을 알 수 있다.
(c의 상태였다면 0을 출력했을 것이다.)</li>
<li><code>git status</code>로 확인했을 때, nothing to commit, working tree clean이라는 문구를 볼 수 있다. 즉, staging area 또한 b의 상태로 돌아갔음을 알 수 있다.</li>
</ul>
<br />
<br />

<h3 id="2-git-reset---mixed">2. git reset --mixed</h3>
<blockquote>
<p><code>git reset 6872413 --mixed</code>
<img src="https://images.velog.io/images/jduckling_1024/post/6abc163a-d478-461b-a898-00a4d276b3bb/image.png" alt=""></p>
</blockquote>
<ul>
<li>일단 확실히 repository는 b의 상태로 돌아갔을 것이다.</li>
<li><code>cat a.txt</code>로 확인했을 때, 그대로 0을 출력하는 것으로 보아 working directory 또한 c의 상태에 머물러있음을 알 수 있다.</li>
<li><code>git status</code>로 확인했을 때, a.txt가 modified가 되었으며 빨간색이므로 staging area는 b의 상태로 돌아갔음을 알 수 있다.</li>
</ul>
<br />

<p>참고로 reset시 어떤 옵션도 주지 않게 되면 --mixed가 default다.
<br />
<br /></p>
<h3 id="3-git-reset---soft">3. git reset --soft</h3>
<blockquote>
<p><code>git reset 6872413 --soft</code>
<img src="https://images.velog.io/images/jduckling_1024/post/fa816e6f-c053-451e-8cf2-c221b54a6fd5/image.png" alt=""></p>
</blockquote>
<ul>
<li>일단 확실히 repository는 b의 상태로 돌아갔을 것이다.</li>
<li><code>cat a.txt</code>로 확인했을 때, 그대로 0을 출력하는 것으로 보아 working directory 또한 c의 상태에 머물러있음을 알 수 있다.</li>
<li><code>git status</code>로 확인했을 때, a.txt가 modified가 되었으며 초록색이므로 staging area는 c의 상태에 머물러있음을 알 수 있다.</li>
</ul>
<br />
<br />

<h3 id="4-정리-표">4. 정리 표</h3>
<table>
<thead>
<tr>
<th></th>
<th>reopsitory</th>
<th>staging area</th>
<th>working directory</th>
</tr>
</thead>
<tbody><tr>
<td><strong>hard</strong></td>
<td>b</td>
<td>b</td>
<td>b</td>
</tr>
<tr>
<td><strong>mixed</strong></td>
<td>b</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td><strong>soft</strong></td>
<td>b</td>
<td>c</td>
<td>c</td>
</tr>
</tbody></table>
<br />
<br />
<br />
<br />

<h2 id="ii-git-revert">II. git revert</h2>
<p><code>git revert</code>는 결과적으로 봤을 때 이전 상태로 되돌리는 것을 의미하는 것은 맞지만, 되돌린 내역까지 기록으로 남게 된다.</p>
<blockquote>
<p><code>git revert 4aadc32</code>
<img src="https://images.velog.io/images/jduckling_1024/post/a0f09f1b-0137-42e2-803d-43e54cc8be34/image.png" alt="">
이렇게 새로운 commit이 남게 되고,
<img src="https://images.velog.io/images/jduckling_1024/post/0addbda1-23a0-4f6f-b2ad-60b237b2853a/image.png" alt="">
결과적으로 봤을 때, c의 상태를 revert 하였으니 a.txt 파일에는 -1이 남게 된다.</p>
</blockquote>
<br />
<br />
<br />
<br />



<p>학과에서 프로젝트를 할 때는 변경사항을 되돌릴 때 무조건 revert 것 같다(사실 아무것도 모르고 git desktop을 사용했기에... 뭔지도 모르고 일단 쓴게 저거다...). 실무에서는 git을 끼고 살아야 하니 열심히 따라가는 중이다!</p>
]]></description>
        </item>
    </channel>
</rss>