<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>u_lim.log</title>
        <link>https://velog.io/</link>
        <description>개인 공부 기록장</description>
        <lastBuildDate>Fri, 19 May 2023 07:42:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. u_lim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/u_lim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[프로그래머스] Lv.1 신규 아이디 추천 / JAVA]]></title>
            <link>https://velog.io/@u_lim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.1-%EC%8B%A0%EA%B7%9C-%EC%95%84%EC%9D%B4%EB%94%94-%EC%B6%94%EC%B2%9C</link>
            <guid>https://velog.io/@u_lim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.1-%EC%8B%A0%EA%B7%9C-%EC%95%84%EC%9D%B4%EB%94%94-%EC%B6%94%EC%B2%9C</guid>
            <pubDate>Fri, 19 May 2023 07:42:17 GMT</pubDate>
            <description><![CDATA[<h2 id="🔗-문제-링크">🔗 문제 링크</h2>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/72410">https://school.programmers.co.kr/learn/courses/30/lessons/72410</a></p>
<h2 id="👩🏻💻-코드">👩🏻‍💻 코드</h2>
<pre><code class="language-java">class Solution {
    public String solution(String new_id) {
        String answer = new_id.toLowerCase(); // 1단계
        answer = answer.replaceAll(&quot;[^a-z0-9\\-_.]&quot;, &quot;&quot;) // 2단계
        .replaceAll(&quot;\\.{2,}&quot;, &quot;.&quot;) // 3단계
        .replaceAll(&quot;^\\.|\\.$&quot;, &quot;&quot;); // 4단계

        if(answer.isEmpty()) answer = &quot;a&quot;; // 5단계

        if(answer.length()&gt;=16) answer = answer.substring(0,15).replaceAll(&quot;\\.$&quot;, &quot;&quot;); // 6단계

        char lastChar = answer.charAt(answer.length()-1); // 7단계
        for (; answer.length()&lt;=2;) {
            answer+=lastChar;
        }

        return answer;
    }
}</code></pre>
<h2 id="🧩-해결-과정">🧩 해결 과정</h2>
<p>프로그래머스 Lv.1에 <code>2021 KAKAO BLIND RECRUITMENT</code> 문제라고 해서 보다가 정규 표현식만 신경 써주면 풀 수 있을 거 같아 도전하게 되었다.
또한, 정규식을 공부할 때 chatGPT를 이용하면 잘 알려준다고 들었던 기억이 있어서 지피티 사용법도 익힐겸 활용해서 풀었다.</p>
<p>주석 2단계 부분의 정규식 <code>answer.replaceAll(&quot;[^a-z0-9\\-_.]&quot;, &quot;&quot;)</code>은 알파벳 소문자, 숫자, &#39;-&#39;, &#39;_&#39;, &#39;.&#39;를 제외한 모든 문자 제거하는 식이다.</p>
<p>정규식에서 보이는 <code>\\</code> 두개의 역슬래시의 의미 : 큰따옴표(&quot;)내에서 escape문자 <code>\</code>를 표현하려면 escape문자를 <code>\\</code>와 같이 두 번 사용해야 한다 (출처 : Java의 정석_남궁 성)</p>
<h2 id="⌨-정리">⌨ 정리</h2>
<p>chatGPT없이 풀었다면 정규식을 찾아보고 공부하느라 오래걸렸을 것이다. 지피티를 어느 부분에서 어떻게 활용할지 감을 잡을 수 있었다.
<strong>그래도 나중에 따로 시간을 내어 정규식 공부를 해야겠다.</strong>
그리고 마지막의 for문을 while문으로 고치는게 가독성에 더 좋을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] RedisReadOnlyException 해결]]></title>
            <link>https://velog.io/@u_lim/Redis-RedisReadOnlyException-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@u_lim/Redis-RedisReadOnlyException-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Tue, 02 May 2023 16:09:36 GMT</pubDate>
            <description><![CDATA[<p>메인 프로젝트 배포 서버에 맞는 아이디와 비밀번호를 입력해도 일치하지 않는다는 팝업이 뜨고, 포스트맨으로는 로그인에 500 error가 발생 했다.
<img src="https://velog.velcdn.com/images/u_lim/post/1a81b642-6695-47bb-a4b9-3ed84dcca4d7/image.png" alt=""></p>
<p>배포 중인 동안 문제 되지 않았던 부분이 갑자기 이러니 의아했다. 
고민해보니 프론트에선 500에러 처리가 아이디와 비밀번호가 일치하지 않는다는 팝업으로 뜨는 것 같았고, 포스트맨에선 다른 API는 잘 실행이 되는데 로그인 API와 회원가입 시 이메일 인증을 받는 API만 되지 않아 둘의 공통점인 Redis문제라는걸 직감하게 되었다.</p>
<p>EC2 서버 로그를 확인하니 Redis 문제가 맞았다.</p>
<pre><code class="language-java">2023-05-02 14:02:36.602 ERROR 24985 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisReadOnlyException: READONLY You can&#39;t write against a read only replica.</code></pre>
<blockquote>
<p>위 오류는 Redis 클러스터의 읽기 전용 레플리카에 쓰기 작업을 시도할 때 발생하는 것으로, Redis는 읽기 전용 레플리카에 대해 쓰기 작업을 허용하지 않기 때문에 해당 오류가 발생한다고 한다.</p>
</blockquote>
<h2 id="💻-해결">💻 해결</h2>
<p>redis-cli에서 <code>info</code> 명령어를 내리면 아래와 같이 나온다.</p>
<pre><code>&gt; info
slave_read_only:1</code></pre><p><code>config set slave-read-only no</code> 명령어를 사용하여 Redis의 읽기 전용 설정을 해제할 수 있다. 이 명령어를 실행하면 Redis 서버는 읽기 전용 설정을 해제하고 쓰기 작업을 허용한다.</p>
<pre><code>&gt; config set slave-read-only no</code></pre><hr>
<blockquote>
<p>(Chat GPT) &gt; 이 명령어를 사용하기 전에 Redis 클러스터의 구성과 요구 사항을 신중히 고려하고 적절한 조치를 취해야 합니다. 예를 들어, 읽기 전용 레플리카가 데이터의 일관성을 보장하는 역할을 하는 경우에는 이 설정을 변경하면 데이터의 무결성에 영향을 줄 수 있습니다. 따라서 변경하기 전에 신중하게 검토해야 합니다.</p>
</blockquote>
<p><em>Redis에 대해 더 공부해서 적절한 조치를 취한 건지 돌아봐야겠다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] Spring Boot와 Redis 연동]]></title>
            <link>https://velog.io/@u_lim/Redis-Spring-Boot%EC%99%80-Redis-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@u_lim/Redis-Spring-Boot%EC%99%80-Redis-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Mon, 24 Apr 2023 05:23:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/u_lim/post/4c95fb06-1d03-4013-9cdc-1065c004ffff/image.png" alt=""></p>
<blockquote>
<p>부트캠프 메인 프로젝트에서 사용한 Redis를 정리해보려고 합니다.</p>
</blockquote>
<h1 id="💻-redis">💻 Redis</h1>
<p>Redis는 In-memory 기반의 NoSQL DBMS로서 Key-Value의 구조를 가지고있다. 또한 속도가 빠르고 사용이 간편하다.
보통 캐싱 / 세션관리 등으로 쓰지만 부트캠프 메인 브로젝트에서 임시데이터를 잠시 저장하고 그걸 빠르게 가져오기 위해 사용했다.</p>
<hr>
<h1 id="💻-redis-client--jedis와-lettuce">💻 Redis Client : Jedis와 Lettuce</h1>
<p>redis는 connectionFactory로 Lettuce와 Jedis 두 가지를 제공한다.
default로 Lettuce를 제공하고 Jedis는 Deprecated된다고 한다.
성능적인 면에서도 Lettuce가 좋다고 해서 Lettuce를 사용했다.</p>
<hr>
<h1 id="💻-spring-data-redis-라이브러리">💻 Spring Data Redis 라이브러리</h1>
<p>Spring Boot에서 Redis를 사용하려면 Spring Data Redis 라이브러리가 필요하다.
Spring Data Redis 라이브러리는 2가지 방식을 제공한다.</p>
<ul>
<li>RedisTemplate</li>
<li>RedisRepository</li>
</ul>
<p>메인 프로젝트에선 RedisTemplate 방식을 사용했다.</p>
<hr>
<h2 id="✔-buildgradle-의존성-추가">✔ build.gradle 의존성 추가</h2>
<pre><code class="language-java">implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
<hr>
<h2 id="✔-yml-설정">✔ .yml 설정</h2>
<p><code>application.yml</code>에 <code>host</code>와 <code>port</code>를 설정한다.</p>
<pre><code class="language-yml">spring:
  redis:
    host: localhost
    port: 6379</code></pre>
<p>Redis의 <code>localhost:6379</code>는 기본값이다.</p>
<hr>
<h1 id="💻-redis-configuraiton">💻 Redis Configuraiton</h1>
<pre><code class="language-java">@Configuration
@EnableRedisRepositories
public class RedisConfiguration {

    @Value(&quot;${spring.redis.host}&quot;)
    private String host;

    @Value(&quot;${spring.redis.port}&quot;)
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration(host, port);
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public RedisTemplate&lt;String, Object&gt; redisTemplate() {
        RedisTemplate&lt;String, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}</code></pre>
<h2 id="✔-redisconnectionfactory">✔ RedisConnectionFactory</h2>
<p><code>redisConnectionFactory()</code>를 통해 Lettuce로 Redis와 연결한다.
<code>RedisStandaloneConfiguration</code> 객체를 통해 redis 접속 정보(host, port 등)를 갖고 있는 객체를 생성한다.
<code>RedisStandaloneConfiguration</code>은 single node에 redis를 연결하기 위한 설정 정보를 가지고 있는 기본 클래스이다.
redis 설정 정보가 담긴 <code>RedisStandaloneConfiguration</code>을 <code>LettuceConnectionFactory</code>에 담아서 반환한다.</p>
<h2 id="✔-redistemplate">✔ RedisTemplate</h2>
<p>Spring Boot 2.0부터 <code>RedisTemplate</code>와 <code>StringTemplate</code> 두 가지 <code>Bean</code>을 자동으로 생성하여 제공하고 있다고 한다.
<code>RedisTemplate</code>에는 <code>serializer</code>를 설정해주는데 설정하지 않는 다면 스프링에서 조회할 때는 값이 정상으로 보이지만 redis-cli로 데이터 확인이 어렵다.
따라서 개별 설정을 하고자 하는 경우 그냥 해당 <code>Bean</code>을 별도로 생성하면 된다.</p>
<pre><code class="language-java">@Bean
    public RedisTemplate&lt;String, Object&gt; redisTemplate() {
        RedisTemplate&lt;String, Object&gt; redisTemplate = new RedisTemplate&lt;&gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        // 기본적인 key:value의 경우 시리얼라이저
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        // Hash를 사용할 경우 시리얼라이저
        // redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        // 모든 경우
        // redisTemplate.setDefaultSerializer(new StringRedisSerializer());

        return redisTemplate;
    }</code></pre>
<h3 id="stringredisserializer">StringRedisSerializer</h3>
<p>redis에 객체를 저장할 때 redis-cli로 확인해 보면 byte코드로 저장되어 <code>serializer</code>를 통해 직렬화해 주어야 한다.
다양한 직렬화 방법 중 <code>StringRedisSerializer</code>를 사용했다.
String 값을 그대로 저장하는 Serializer이다. 따라서 객체를 Json형태로 변환하여 Redis에 저장하기 위해서는 직접 Encoding, Decoding을 해주어야 한다는 단점이 존재한다.
이때 <code>StringRedisSerializer</code>를 사용하고 직접 Json Parser를 적용하는 방식으로 <code>RedisTemplate</code>을 사용하여 단점을 보완한다.</p>
<hr>
<h1 id="💻-redisservice">💻 RedisService</h1>
<pre><code class="language-java">@Service
@Slf4j
@RequiredArgsConstructor
public class RedisService {
    private final RedisTemplate&lt;String, String&gt; redisTemplate;
    private final JwtTokenizer jwtTokenizer;

    // key-value = RefrshToken-Email
    public void setRefreshToken(String refreshToken, String email, long expirationMinutes) {
        ValueOperations&lt;String, String&gt; valueOperations = redisTemplate.opsForValue();

        // 만료시간 이후 삭제
        valueOperations.set(refreshToken, email, Duration.ofMinutes(expirationMinutes));
        log.info(&quot;만료 시간, 분: {}&quot;, Duration.ofMinutes(expirationMinutes));
    }

    // AccessToken 로그아웃
    public void setAccessTokenLogout(String accessToken, long expiration) {
        ValueOperations&lt;String, String&gt; valueOperations = redisTemplate.opsForValue();
        valueOperations.set(accessToken, &quot;logout&quot;, expiration, TimeUnit.MILLISECONDS);
        String expireFormatString = String.format(&quot;%d min, %d sec&quot;,
                TimeUnit.MILLISECONDS.toMinutes(expiration),
                TimeUnit.MILLISECONDS.toSeconds(expiration) -
                        TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(expiration))
        );
        log.info(&quot;access token 만료 시간, {}&quot;, expireFormatString);
    }


    // get RefreshToken
    public String getRefreshToken(String refreshToken) {
        ValueOperations&lt;String, String&gt; valueOperations = redisTemplate.opsForValue();
        // RefreshToken 없으면 null 반환
        return valueOperations.get(refreshToken);
    }

    // get AccessToken
    public String getAccessToken(String accessToken) {
        ValueOperations&lt;String, String&gt; valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(accessToken);
    }

    // delete RefreshToken
    public void deleteRefreshToken(String refreshToken) {
        // delete 메서드 삭제 시 true 반환
        redisTemplate.delete(refreshToken);
    }
}</code></pre>
<h2 id="✔-redis-method">✔ Redis Method</h2>
<p>Redis는 자료구조에 따라 사용하는 메서드가 다르다.</p>
<ul>
<li><code>메서드 명</code>    | 반환 타입(오퍼레이션) | Redis 자료구조</li>
<li><code>opsForValue</code> |   ValueOperations  |  String</li>
<li><code>opsForList</code>  |   ListOperations   |   List</li>
<li><code>opsForSet</code>   |    SetOperations     |    Set</li>
<li><code>opsForZSet</code>  |   ZSetOperations   | Sorted Set</li>
<li><code>opsForHash</code>  |   HashOperations     |   Hash</li>
</ul>
<h4 id="duration-지속-지속되는기간">Duration (지속, (지속되는)기간)</h4>
<p><img src="https://velog.velcdn.com/images/u_lim/post/f43db316-8197-4bda-b663-829a16d113f9/image.png" alt=""></p>
<p><code>Duration</code>을 설정해주어 메모리에서 데이터가 증발하는 시간을 정해준다.</p>
<h4 id="timeunit">TimeUnit</h4>
<p><img src="https://velog.velcdn.com/images/u_lim/post/d22d52e2-7dbe-4895-bf31-7fce6558098f/image.png" alt=""></p>
<p>데이터를 저장할 때 만료 시간 지정할 시에는 해당 시간의 단위까지 지정해주면 된다. RedisService.java에서는 <code>TimeUnit.MILLISECONDS</code>(밀리 초)로 적용되어 있다.</p>
<h4 id="method">Method</h4>
<ul>
<li><code>redisTemplate.opsForValue().get(key)</code> : key 값으로 value 를 가져온다.<ul>
<li>Redis DB 에서 값을 가져올 때는 <code>valueOperations.get(key)</code> 의 구조로 가져오게 되고, 데이터 조회를 하여 값이 없는 경우 null 을 가져온다. 이때 <code>StringUtils.isBlank(valueOp.get(key))</code> 형식으로 가져오는 것이 안전한 방법이다.</li>
</ul>
</li>
<li><code>redisTemplate.opsForValue().set(key, value, time, unit)</code> : key값으로 value를 저장한다 만약 만료 시간을 지정하는 경우엔 time 과 단위를 함께 세팅한다.</li>
<li><code>redisTemplate.hasKey(key)</code> : 키의 존재유무를 return 한다.</li>
<li><code>redisTemplate.delete(key)</code> : key값에 대한 데이터를 삭제한다.</li>
</ul>
<hr>
<h4 id="version">version</h4>
<p>java 11
Spring Boot 2.7.9
Gradle 7.6.1</p>
<p>❗ 틀린 부분 댓글 첨언 환영합니다 ❗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Security] 회원 가입에서 401을 만나다]]></title>
            <link>https://velog.io/@u_lim/Spring-Security-%ED%9A%8C%EC%9B%90-%EA%B0%80%EC%9E%85%EC%97%90%EC%84%9C-401%EC%9D%84-%EB%A7%8C%EB%82%98%EB%8B%A4</link>
            <guid>https://velog.io/@u_lim/Spring-Security-%ED%9A%8C%EC%9B%90-%EA%B0%80%EC%9E%85%EC%97%90%EC%84%9C-401%EC%9D%84-%EB%A7%8C%EB%82%98%EB%8B%A4</guid>
            <pubDate>Mon, 13 Mar 2023 17:24:59 GMT</pubDate>
            <description><![CDATA[<p>메인 프로젝트에서 Spring Security, JWT를 맡게 되었다
Spring Security 설정과 기본 구현을 하고, JWT로 열심히 로그인 Token관련 코드를 짜고 나서 포스트 맨으로 중간 확인을 하는데
<img src="https://velog.velcdn.com/images/u_lim/post/4396980f-05cc-4c6d-a79c-f9946fc170b8/image.png" alt=""></p>
<p>회원가입에서 401에러가 나왔다.</p>
<p><code>401 Unauthorized</code>는 이름 때문에 <code>인가</code>,<code>권한</code> 문제 같지만 요청에 <strong>인증 자격이 없다</strong>는 것이다</p>
<p>하지만 로그인에서 인증 자격이 없는 것도 아니고 회원가입에서 401로 막히는게 너무 의문이었다
일단 인증이 안된다니 JWT토큰에 생각이 머물렀는데, 토큰은 로그인 시 생성 되는 것 아닌가?
그래서 Security로 갔다, SecurityConfiguration을 열심히 들여다보고 있는데... </p>
<h2 id="💻-해결">💻 해결</h2>
<pre><code class="language-java">public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

...

    }</code></pre>
<p>허전한<code>filterChain()</code> ... 바로 <code>@Bean</code>애너테이션으로 ApplicationContext에 Bean객체로 등록해주지 않아서 그랬던 거였다🎊
당연히 SecurityFilter자체가 스프링에 빈으로 등록되지 않았으니 Spring Security 회원가입이 안됐던 것이다. 
결국 <code>@Bean</code>을 추가해주고 해결 완료 했다!</p>
<pre><code class="language-java">@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

...

    }</code></pre>
<p><img src="https://velog.velcdn.com/images/u_lim/post/2b9e7aa8-3ce0-41d8-b0a5-e161cba9bc26/image.png" alt="">
<img src="https://velog.velcdn.com/images/u_lim/post/d0123583-54d8-423c-87f0-61b30c62944f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot, MySQL 연동]]></title>
            <link>https://velog.io/@u_lim/Spring-Boot-MySQL-%EC%97%B0%EB%8F%99</link>
            <guid>https://velog.io/@u_lim/Spring-Boot-MySQL-%EC%97%B0%EB%8F%99</guid>
            <pubDate>Wed, 01 Mar 2023 17:26:01 GMT</pubDate>
            <description><![CDATA[<p><code>javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.SQLGrammarException: Unable to open JDBC Connection for DDL execution</code></p>
<p>...(중략)</p>
<p><code>Caused by: java.sql.SQLSyntaxErrorException: Unknown database</code></p>
<p>Spring Boot에 MySQL연동을 시도했는데 에러가 발생했습니다.
에러를 쭉 읽어보니 database를 알 수 없다는 즉, 없다는 말 같아서 MySQL로 DB를 생성해 주었습니다.</p>
<h1 id="💻-해결">💻 해결</h1>
<h2 id="✔-mysql에-db생성">✔ MySQL에 DB생성</h2>
<p>MySQL Workbrench에 Spring Boot 애플리케이션에서 사용할 DB를 생성합니다.
.yml파일의 <code>url: jdbc:mysql://localhost:3306/데이터베이스이름?useSSL=false&amp;serverTimezone=Asia/Seoul</code>의 데이터베이스 이름과 같은 이름으로 생성해야 합니다.</p>
<h2 id="✔-yml설정">✔ .yml설정</h2>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/데이터베이스이름?useSSL=false&amp;serverTimezone=Asia/Seoul
    username: 유저네임
    password: 비밀번호
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database: mysql   # 추가
    database-platform: org.hibernate.dialect.MySQL8Dialect # 추가 

    hibernate:
      ddl-auto: create  # 스키마 자동 생성 
    show-sql: true      # SQL 쿼리 출력</code></pre>
<p><code>MySQL8Dialect</code>는 <code>MySQL버전번호Dialect</code>로 써줍니다.
<code>ddl-auto: create</code>는 한 번 실행 후 <code>update</code>로 바꿔줍니다. </p>
<h2 id="✔-table-not-exist-오류-해결">✔ table not exist 오류 해결</h2>
<p><img src="https://velog.velcdn.com/images/u_lim/post/24c5751d-c4ef-40df-a6c2-7e6f2653ac02/image.png" alt=""></p>
<p>바로 위에서 <code>ddl-auto: create</code>는 한 번 실행 후 <code>update</code>로 바꿔줍니다. 라고 했지만.... 저는 <code>create</code>로 실행을 돌리면 바로 몇몇 테이블이 존재하지 않는다는 에러가 발생했습니다.
<code>Caused by : java.sql.SQLSyntaxErrorException : Table &#39;DB명.테이블명&#39; dosen&#39;t exist</code>
그래서 처음부터 <code>ddl-auto: update</code>로 만들어 주니 해당 에러는 사라졌습니다.</p>
<p>⭐ <em>더 공부해서 이유 찾아보기!!</em> ⭐</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Caused by: java.lang.IllegalStateException: Ambiguous mapping. ]]></title>
            <link>https://velog.io/@u_lim/Spring-Boot-Caused-by-java.lang.IllegalStateException-Ambiguous-mapping</link>
            <guid>https://velog.io/@u_lim/Spring-Boot-Caused-by-java.lang.IllegalStateException-Ambiguous-mapping</guid>
            <pubDate>Mon, 27 Feb 2023 07:05:29 GMT</pubDate>
            <description><![CDATA[<p><code>Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map &#39;answerController&#39; method</code></p>
<p>프리프로젝트 중 위와 같은 에러가 발생했습니다💥</p>
<p><code>answerController</code>에 모호한 매핑이 있다 즉, 해당 컨트롤러 mapping에서 문제가 발생한 것으로 </p>
<pre><code class="language-java">@PatchMapping(&quot;/method1/{answer-id}&quot;)
    public ResponseEntity 메서드1(@PathVariable(&quot;answer-id&quot;) long answerId) {
        ...
        return new ResponseEntity&lt;&gt;(
                new SingleResponseDto&lt;&gt;(response), HttpStatus.OK);
    }

@PatchMapping(&quot;/method1/{answer-id}&quot;)
public ResponseEntity 메서드2(@PathVariable(&quot;answer-id&quot;) long answerId) {
      ...
        return new ResponseEntity&lt;&gt;(
                new SingleResponseDto&lt;&gt;(response), HttpStatus.OK);
    }</code></pre>
<p>같은 <code>@PatchMapping</code>이 중복되어 있었습니다.</p>
<h2 id="해결">해결</h2>
<p>HTTP Method를 바꿔주거나 URI를 바꿔주면 됩니다!
저는 URI를 바꿔주었습니다.</p>
<pre><code class="language-java">@PatchMapping(&quot;/method1/{answer-id}&quot;)
    public ResponseEntity 메서드1(@PathVariable(&quot;answer-id&quot;) long answerId) {
        ...
        return new ResponseEntity&lt;&gt;(
                new SingleResponseDto&lt;&gt;(response), HttpStatus.OK);
    }

@PatchMapping(&quot;/method2/{answer-id}&quot;)
public ResponseEntity 메서드2(@PathVariable(&quot;answer-id&quot;) long answerId) {
      ...
        return new ResponseEntity&lt;&gt;(
                new SingleResponseDto&lt;&gt;(response), HttpStatus.OK);
    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] Mac ↔ Window OS 에러]]></title>
            <link>https://velog.io/@u_lim/Git-Mac-Window-OS-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@u_lim/Git-Mac-Window-OS-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Sat, 25 Feb 2023 11:42:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/u_lim/post/ed29e7e4-e2a9-44e8-845b-468c7b782494/image.png" alt=""></p>
<p>Git에서 MacOS사용자가 Push한 내용을 WindowOS사용자가 Pull 했을 때</p>
<pre><code class="language-bash">error: invalid path &#39;client/src/?components/Header/Head.js&#39;
error: invalid path &#39;client/src/?components/Header/HeadLogout.js&#39;</code></pre>
<p>이와 같은 오류가 발생했다.
OS간 폴더 관리 방식이 다른 이유로 발생하는 문제로, 해당 폴더에 <code>?</code> 등의 특수문자가 들어가 생기는 오류이다.</p>
<h2 id="해결">해결</h2>
<p>MacOS사용자(해당 폴더를 갖고 있는 사용자)가 터미널에서 해당 폴더의 상위 디렉토리로 들어가</p>
<pre><code class="language-bash">mv ?components components </code></pre>
<p>명령어로 폴더명을 수정 한 뒤 다시 Push, Pull 하면 해결된다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IntelliJ] 터미널에서 vi 편집기 esc 사용]]></title>
            <link>https://velog.io/@u_lim/IntelliJ-%ED%84%B0%EB%AF%B8%EB%84%90%EC%97%90%EC%84%9C-vi-%ED%8E%B8%EC%A7%91%EA%B8%B0-esc-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@u_lim/IntelliJ-%ED%84%B0%EB%AF%B8%EB%84%90%EC%97%90%EC%84%9C-vi-%ED%8E%B8%EC%A7%91%EA%B8%B0-esc-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Sat, 25 Feb 2023 08:41:01 GMT</pubDate>
            <description><![CDATA[<p>프리프로젝트 중 커밋 메시지를 잘 못 적어서 이 정도는 금방 고치지😎하면서 인텔리제이의 터미널을 이용하는데.... vi창에서 esc키가 안먹힌다... 정확히는 인텔리제이의 터미널 vi 편집기에서 esc를 눌렀을 때 코드 편집창으로 이동하는 상황이었다...
찾아보니 인텔리제이 터미널 escape단축키를 해제하면 된다고 해서 해결되었다 휴😅</p>
<h2 id="해결">해결</h2>
<p>인텔리제이 File &gt; Settings &gt; Tools &gt; Terminal &gt; Configure terminal keybindings 클릭! &gt; Switch Focus To Editor의 Escape 해제
<img src="https://velog.velcdn.com/images/u_lim/post/cdbee7fc-e4bd-498c-a3e8-c3b9c0e229e6/image.png" alt="">
(나는 이미 해제 해서 Escape표시는 없어졌다)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SoloProject] Caused by: org.hibernate.AnnotationException: No identifier specified for entity]]></title>
            <link>https://velog.io/@u_lim/SoloProject-Caused-by-org.hibernate.AnnotationException-No-identifier-specified-for-entity</link>
            <guid>https://velog.io/@u_lim/SoloProject-Caused-by-org.hibernate.AnnotationException-No-identifier-specified-for-entity</guid>
            <pubDate>Tue, 21 Feb 2023 12:16:26 GMT</pubDate>
            <description><![CDATA[<p>스프링 부트로 솔로 프로젝트(투두리스트 작성 앱)의 백엔드를 구현중입니다.
구현 중 <code>Caused by: org.hibernate.AnnotationException: No identifier specified for entity</code>와 같은 에러가 발생 했고 찾아보니 Entity <code>@Id</code>의 <code>import</code>문제라고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/u_lim/post/cbcacf88-8479-4d39-8d76-3245bffeaa1f/image.png" alt="">
에러가 난 부분은 <code>import org.springframework.data.annotation.Id;</code></p>
<h2 id="해결">해결</h2>
<p><code>import org.springframework.data.annotation.Id;</code> -&gt; <code>import javax.persistence.Id;</code> 로 바꿔주면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] Pulling is not possible because you have unmerged files]]></title>
            <link>https://velog.io/@u_lim/Git-conflict-both-modified</link>
            <guid>https://velog.io/@u_lim/Git-conflict-both-modified</guid>
            <pubDate>Sun, 19 Feb 2023 07:33:32 GMT</pubDate>
            <description><![CDATA[<p>프리프로젝트로 깃&amp;깃허브를 팀원분들과 사용하게 되었습니다.
팀 단위 작업은 처음이라 conflict처리도 처음으로 해결하게 되어서 기록으로 남깁니다!</p>
<hr>
<p><img src="https://velog.velcdn.com/images/u_lim/post/65aca115-8944-486a-ba8c-e7b65d069ec6/image.png" alt="">
<code>git pull</code>을 받는데 위와 같은 에러가 발생했습니다.
로컬 리포지토리의 변경 사항과 원격 리포지토리의 변경 사항 간에 충돌이 있으며 Git이 자동으로 병합할 수 없음을 나타낸다고 합니다.</p>
<p>뭐가 문제일지 생각을 해보니 어제 팀원분이 수정하고 remote에 올리신 스프링 부트의 <code>.yml</code>파일을 <code>pull</code>받을 때 제가 이미 local에서 <code>.yml</code>을 수정해놔서 conflict가 난 걸 수정하고 <code>git add</code>와 <code>git commit</code>을 처리해주지 않았구나! 싶었습니다. 
그래서 <code>git status</code>를 보면 아래와 같이 <code>both modified</code>가 보입니다.
<img src="https://velog.velcdn.com/images/u_lim/post/894807e2-4579-478f-8aaa-9c695e7ea12b/image.png" alt=""></p>
<h2 id="해결">해결</h2>
<pre><code class="language-bash">&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
Changes made in your local repository
=======
Changes made in the remote repository
&gt;&gt;&gt;&gt;&gt;&gt;&gt; [commit SHA from remote repository]</code></pre>
<p><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD</code>는 로컬 변경의 시작을 나타내고 <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code>는 원격 저장소에서 변경의 끝을 나타냅니다 <code>=======</code>는 충돌하는 변경 사항을 구분합니다.</p>
<p>팀원분과 상의 후 로컬을 남길거면 로컬 코드만, 원격을 남길거면 원격 코드만, 둘 다 남길거면 <code>&lt;&lt;&lt;</code>, <code>===</code>, <code>&gt;&gt;&gt;</code> 표시만 지우고 저장합니다.</p>
<pre><code class="language-bash"># 원격 코드만 남긴 예시
Changes made in the remote repository</code></pre>
<p>후에 충돌된 부분 수정 반영으로 <code>git add</code>와 <code>git commit</code>을 해주면 됩니다.</p>
<pre><code class="language-bash">git add both_modified파일명
git commit -m &quot;커밋메시지&quot;</code></pre>
<p>다시 <code>git status</code>를 해주면 both modified파일이 사라진걸 확인할 수 있습니다.
즉, merge가 정상적으로 마무리 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] 인터페이스 static메서드와, default메서드]]></title>
            <link>https://velog.io/@u_lim/JAVA-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@u_lim/JAVA-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Thu, 16 Feb 2023 15:45:27 GMT</pubDate>
            <description><![CDATA[<h1 id="💻-인터페이스interface">💻 인터페이스(Interface)</h1>
<p>일종의 추상클래스이다. 하지만 추상클래스보다 추상화 정도가 높아서 오직 추상메서드와 상수만을 멤버로 가질 수 있다.</p>
<p>인터페이스 멤버들의 제약사항</p>
<ul>
<li>모든 멤버변수는 <code>public static final</code>이어야 하며, 이는 생략 가능하다.</li>
<li>모든 메서드는 <code>public abstract</code> 이어야 하며, 이는 생략 가능하다.</li>
<li><strong>단, static메서드와 default메서드는 예외(JDK1.8 부터)</strong></li>
</ul>
<hr>
<h2 id="📝-static-메서드">📝 static 메서드</h2>
<p>인스턴스와 관계가 없는 독립적인 메서드
static메서드 역시 접근 제어자가 항상 <code>public</code>이며, 이는 생략 가능하다.</p>
<pre><code class="language-java">interface Myinterface {
    static void staticMethod() {
        System.out.println(&quot;staticMethod() of interface&quot;); // 구현부 존재
    }
}</code></pre>
<p>사용
인터페이스명.static메서드명
<code>Myinterface.staticMethod();</code></p>
<blockquote>
<p><strong>static멤버의 상속</strong>
static멤버들은 자신들이 정의된 클래스에 묶여있다고 생각해야 한다.
호출할 때 꼭 <code>클래스이름.static멤버이름</code>으로 호출</p>
</blockquote>
<hr>
<h2 id="📝-default-메서드">📝 default 메서드</h2>
<p>default메서드 역시 접근 제어자가 항상 <code>public</code>이며, 이는 생략 가능하다.</p>
<pre><code class="language-java">interface Myinterface {
    default void defaultMethod() {
        System.out.println(&quot;defaultMethod() of interface&quot;); // 구현부 존재
    }
}</code></pre>
<p>인터페이스의 default메서드는 구현 클래스에서 직접 구현하지 않아도 된다. 
인터페이스의 구현 클래스에 default메서드가 자동으로 구현된다.(마치 상속처럼)</p>
<ul>
<li>default메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우 충돌 해결 규칙<ul>
<li>여러 인터페이스의 default메서드 간의 충돌<ul>
<li>인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.</li>
</ul>
</li>
<li>default메서드와 조상 클래스의 메서드 간의 충돌<ul>
<li>조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<p>참고
Java의 정석_남궁성</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git&GitHub] branch 삭제]]></title>
            <link>https://velog.io/@u_lim/GitGitHub-branch-%EC%82%AD%EC%A0%9C</link>
            <guid>https://velog.io/@u_lim/GitGitHub-branch-%EC%82%AD%EC%A0%9C</guid>
            <pubDate>Tue, 14 Feb 2023 16:30:33 GMT</pubDate>
            <description><![CDATA[<h1 id="🌿-로컬-브랜치-삭제">🌿 로컬 브랜치 삭제</h1>
<p>삭제 대상이 아닌 브랜치에서 삭제해야 한다.</p>
<pre><code class="language-bash">git branch -d 로컬브랜치명</code></pre>
<p>브랜치가 merge된 상태가 아니면 삭제할 수 없다.
강제 삭제</p>
<pre><code class="language-bash">git branch -D 로컬브랜치명</code></pre>
<h1 id="🌿-원격-브랜치-삭제">🌿 원격 브랜치 삭제</h1>
<p>로컬 브랜치와 원격 브랜치는 별개이다.</p>
<pre><code class="language-bash">git push 원격저장소 -d 원격브랜치명
git push 원격저장소 --delete 원격브랜치명</code></pre>
<h1 id="🌿-브랜치-목록-조회">🌿 브랜치 목록 조회</h1>
<p>로컬+원격 브랜치 목록 조회</p>
<pre><code class="language-bash">git branch -a</code></pre>
<p>로컬 브랜치 목록 조회</p>
<pre><code class="language-bash">git branch</code></pre>
<p>원격 브랜치 목록 조회</p>
<pre><code class="language-bash">git branch -r</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GitHub] Protect this branch]]></title>
            <link>https://velog.io/@u_lim/GitHub-Protect-this-branch</link>
            <guid>https://velog.io/@u_lim/GitHub-Protect-this-branch</guid>
            <pubDate>Tue, 14 Feb 2023 16:14:59 GMT</pubDate>
            <description><![CDATA[<p>GitHub 레포에 아래와 같은 창이 떴다.</p>
<p><img src="https://velog.velcdn.com/images/u_lim/post/e8706558-1525-495c-a54f-e87ac6a160e6/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Your main branch isn&#39;t protected
Protect this branch from force pushing or deletion, or require status checks before merging.
해석하면
main 브랜치가 보호되지 않습니다
이 branch를 강제로 누르거나 삭제하지 않도록 보호하거나 병합하기 전에 상태 확인이 필요합니다.</p>
<p>버튼을 누르면 아래와 같은 창이 나온다.
<img src="https://velog.velcdn.com/images/u_lim/post/95f2e068-74e6-4e8e-9594-01ae3da8d559/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>Protect your most important branches
Branch protection rules define whether collaborators can delete or force push to the branch and set requirements for any pushes to the branch, such as passing status checks or a linear commit history.
Your GitHub Free plan can only enforce rules on its public repositories, like this one.
해석
가장 중요한 branchs 보호
branch 보호 규칙은 공동작업자가 branch를 삭제하거나 강제로 푸시할 수 있는지 여부를 정의하고 상태 검사 또는 선형 커밋 기록 전달과 같은 branch 푸시에 대한 요구 사항을 설정합니다.
GitHub Free 요금제는 이와 같은 public repositories에 대해서만 규칙을 적용할 수 있습니다.</p>
<p>Rule을 적용할 branch에 직접 push를 하지 않고 PR로만 push를 주게 하는 등의 제약을 걸 수 있다.</p>
<p>※ GitHub에서만 주는 제약으로 로컬에서는 제약을 받지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[IntelliJ 터미널 Git Bash로 변경하는 방법]]></title>
            <link>https://velog.io/@u_lim/IntelliJ-%ED%84%B0%EB%AF%B8%EB%84%90-Git-Bash%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@u_lim/IntelliJ-%ED%84%B0%EB%AF%B8%EB%84%90-Git-Bash%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 14 Feb 2023 14:39:53 GMT</pubDate>
            <description><![CDATA[<p>File &gt; Settings &gt; Terminal &gt; Shell path</p>
<p>Shell Path에 
Git Bash Shell 경로 적어주기
<code>&quot;C:\Program Files\Git\bin\sh.exe&quot; -login -i</code>
경로는 큰 따옴표로 감싸고 <code>-login -i</code>를 추가해준다.
<img src="https://velog.velcdn.com/images/u_lim/post/ae0423fd-6a03-46fe-b106-a429bb4183fb/image.png" alt=""></p>
<p>APPLY &gt; OK &gt; IDE 재구동 하면 끝!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SoloProject] To-Do App]]></title>
            <link>https://velog.io/@u_lim/SoloProject-To-Do-App</link>
            <guid>https://velog.io/@u_lim/SoloProject-To-Do-App</guid>
            <pubDate>Wed, 08 Feb 2023 12:29:40 GMT</pubDate>
            <description><![CDATA[<h1 id="💻-to-do-list를-구현하는-기본-crud-구현-솔로-프로젝트">💻 To-Do List를 구현하는 기본 CRUD 구현 솔로 프로젝트</h1>
<hr>
<h2 id="🔎-api-계층">🔎 API 계층</h2>
<h3 id="✔-controller-listcontrollerjava">✔ Controller (ListController.java)</h3>
<p>Handler Method(핸들러 메서드)</p>
<ul>
<li>클라이언트의 요청을 처리하는 Controller의 메서드</li>
<li>클라이언트의 요청 데이터를 Service 클래스로 전달하고, 응답 데이터를 클라이언트로 다시 전송하는 역할</li>
</ul>
<h4 id="didependency-injection">DI(Dependency Injection)</h4>
<p>생성자 DI(생성자 의존성 주입)
Spring의 DI 기능을 이용해서 Controller생성자 파라미터로 Service의 객체를 주입 받는다.
Spring이 애플리케이션 로드 시, <strong>ApplicationContext(스프링 컨테이너)</strong>에 있는 Service객체를 주입해준다.</p>
<blockquote>
<p>Spring의 DI는 주입을 받는 클래스와 주입 대상 클래스 모두 <strong>Spring Bean</strong>이어야 한다.
Controller : <code>@RestController</code>로 <strong>Spring Bean</strong>이다.
Service : <code>@Service</code>로 <strong>Spring Bean</strong>이다. <br>
※ 생성자가 하나일 경우 Spring이 자동으로 DI를 적용한다.
하지만, 생성자가 둘 이상일 경우 DI를 적용하기 위한 생성자에 반드시 <code>@Autowired</code>를 붙여야 한다.</p>
</blockquote>
<h4 id="crud">CRUD</h4>
<ul>
<li><strong>C</strong>reate(Post)<ul>
<li>PostList() : 할 일 목록 등록</li>
</ul>
</li>
<li><strong>R</strong>ead(Get)<ul>
<li>getList() : 특정 id 목록 조회</li>
<li>getLists() : 전체 목록 조회</li>
</ul>
</li>
<li><strong>U</strong>pdate(Patch)<ul>
<li><del>patchCheck() : 할 일 완료 표시</del></li>
<li>patchList() : 내용 수정</li>
</ul>
</li>
<li><strong>D</strong>elete(Delete)<ul>
<li>deleteList() : 특정 id 목록 삭제</li>
<li>deleteLists() : 전체 목록 삭제</li>
</ul>
</li>
</ul>
<h4 id="dto">DTO</h4>
<p><strong>D</strong>ata <strong>T</strong>ransfer <strong>O</strong>bject
데이터를 전송하기 위한 용도의 객체
요청 데이터를 하나의 객체로 전달 받는 역할(클라이언트 측으로부터 전달 받은 <code>request body</code>에 매핑되는 DTO 클래스)
즉, <code>@RequestParam</code>을 이용해 각각의 파라미터를 전달 받는 대신에 DTO클래스를 이용해 한번에 전달</p>
<ul>
<li>getter 사용 이유 : Controller에서 response dto를 response body로 전달<ul>
<li><code>MappingJackson2HttpMessageConverter</code>가 response dto를 JSON 문자열로 변환할 때 getter 사용</li>
</ul>
</li>
<li>setter 사용 이유 : 핸들러 메서드에서 <code>path variable</code>로 전달 받은 필드를 setter를 통해 채움 </li>
</ul>
<h4 id="requestbody">@RequestBody</h4>
<p>JSON 형식의 Request Body를 DTO클래스의 객체로 변환 시켜주는 역할(JSON 역직렬화, Deserialization)</p>
<h4 id="responseentity">ResponseEntity</h4>
<p>ex) <code>return new ResponseEntity&lt;&gt;(dto, HttpStatus.CREATED);</code> 처럼 <code>ResponseEntity</code> 객체를 생성하면서 생성자 파라미터로 <strong>응답 데이터</strong>(dto)와 <strong>HTTP 응답 상태</strong>를 함께 전달할 수 있다.
클라이언트 측에 전송하는 <code>response body</code>용 정보(DTO, HTTP 응답 상태)가 포함된 ResponseEntity</p>
<p>핸들러 메서드에 <code>@ResponseBody</code>가 붙거나 핸들러 메서드의 리턴 값이 <code>ResponseEntity</code>일 경우, 내부적으로 <code>HttpMessageConverter</code>가 동작하게 되어 응답 객체(DTO 클래스의 객체)를 JSON 형식으로 바꿔준다.(JSON 직렬화, Serialization)</p>
<ul>
<li>메서드를 이용하는 방법<ul>
<li><code>ResponseEntity.created(location).build();</code></li>
</ul>
</li>
<li>생성자를 이용하는 방법<ul>
<li><code>new ResponseEntity&lt;&gt;(dto, HttpStatus.CREATED);</code></li>
</ul>
</li>
</ul>
<h4 id="responseentity와-locationuri">ResponseEntity와 location(URI)</h4>
<p>일반적으로 클라이언트 측에서 백엔드 애플리케이션 측에 리소스의 등록(POST)을 요청할 경우, 백엔드 애플리케이션은 해당 리소스를 DB에 저장한 후 요청한 리소스가 성공적으로 저장되었음을 알리는 <code>201 Created</code> Http Status를 <code>reponse header</code>에 추가해서 클라이언트 측에 응답으로 전달한다.
<strong>그리고 추가적으로 DB에 저장된 리소스의 위치를 알려주는 위치 정보 URI도 <code>response header</code>에 추가해서 응답으로 전달한다.</strong>
클라이언트 측에서는 <code>response header</code>에 포함된 리소스의 위치정보 URI를 얻은 후에 해당 리소스의 URI로 다시 요청을 전송해서 리소스의 정보를 얻어온다.</p>
<blockquote>
<p><strong>UriComponentsBuilder</strong>
URI를 직접 작성하는 것보다 편리하고 정확하게 URI를 생성한다.
UriComponents(URI를 구성하는 Components들을 다룰 수 있도록 하는 클래스, 생성자가 private이기 때문에 직접 구현 불가)를 Build할 수 있도록 도와주는 클래스이다.
UriComponentsBuilder <strong>newInstance()</strong> : UriComponentsBuilder 객체 생성
UriComponentsBuilder <strong>path(String)</strong> : URI 구성요소 설정
UriComponents <strong>buildAndExpand(Object... uriVariableValues)</strong> : URI템플릿 변수를 설정한 후, UriComponents 인스턴스 Build
URI <strong>toUri()</strong> : UriComponents인트턴스를 URI로 변환
<br>
※ URI { } 템플릿 변수를 지정하면 buildAndExpand()에 순서대로 실제 값 지정 필요
※ UriComponentsBuilder의 메서드들은 반환타입이 UriComponentsBuilder이므로 메서드 체이닝으로 호출 가능하다.</p>
</blockquote>
<hr>
<h2 id="🔎-mapper">🔎 Mapper</h2>
<p>DTO클래스와 Entity클래스를 서로 변환해주는 클래스
<code>@Component</code>로 Spring Bean에 등록 필요</p>
<h4 id="mapstruct">MapStruct</h4>
<p>매퍼 클래스를 자동으로 구현해준다.
객체들 간의 변환 기능을 제공하는 Mapper 인터페이스</p>
<pre><code class="language-java">@Mapper(componentModel = &quot;spring&quot;)
public interface ListMapper {
    ...
}</code></pre>
<p><code>@Mapper</code> : 해당 인터페이스는 MapStruct의 매퍼 인터페이스로 정의
<code>@Mapper(componentModel = &quot;spring&quot;)</code> : <code>componentModel = &quot;spring&quot;</code> 애트리뷰트 지정 시 <strong>Spring Bean으로 등록</strong></p>
<ul>
<li><p>MapStruct 인터페이스의 구현 클래스(MapperImpl.java) 생성</p>
<ul>
<li>MapStruct가 Mapper인터페이스를 기반으로 Mapper구현 클래스를 자동으로 생성해준다.</li>
<li>[Gradle] 탭의 [프로젝트 명 &gt; Tasks 디렉토리 &gt; build 디렉토리 &gt; build task]를 실행하면 자동으로 생성됨</li>
<li>[Project 탭 &gt; 프로젝트 명 &gt; build] 디렉토리 내 Mapper인터페이스가 위치한 패키지 안에 클래스 생성</li>
</ul>
</li>
<li><p>MapStruct 인터페이스의 구현 클래스인 MapperImpl 클래스에서 사용되므로 DTO와 Entity에 각각 필요한 롬복 애너테이션 활용</p>
<ul>
<li>DTO(Post, Patch) 필드.get(<code>@Getter</code>) &gt; 변환 &gt; <code>@NoArgsConstructor</code> + Entity 필드.set(<code>@Setter</code>)</li>
<li>Entity 필드.get(<code>@Getter</code>) &gt; 변환 &gt; <code>@AllArgsConstructor</code> DTO(Response)<ul>
<li>ResponseDto는 JSON직렬화를 위해서 <code>@Getter</code>도 필요</li>
</ul>
</li>
<li><em>각 DTO와 Entity에 <code>@AllArgsConstructor</code> 테스팅에서 필요하다는데 확인해보고 추가하기</em></li>
</ul>
</li>
</ul>
<p>복잡한 DTO클래스와 Entity클래스의 매핑은 MapStruct에 default메서드를 직접 구현해서 개발자가 직접 매핑 로직을 작성할 수 있다.</p>
<hr>
<h2 id="🔎-service-계층-business-logic">🔎 Service 계층 (Business Logic)</h2>
<h3 id="✔-service-listservicejava">✔ Service (ListService.java)</h3>
<p>API 계층과 Service 계층을 연동한다. = API 계층에서 구현한 Controller 클래스가 Service 계층의 Service 클래스와 메서드 호출을 통해 상호 작용한다.</p>
<p>Controller 클래스의 핸들러 메서드와 <strong>1대1로 매치</strong> 된다.</p>
<p>Spring의 DI 기능을 이용해서 Service생성자 파라미터로 Repository의 객체를 주입 받는다.</p>
<h4 id="entity">Entity</h4>
<p>Service 계층에서 Data Access 계층과 연동하면서 비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 역할을 하는 클래스</p>
<p>API 계층에서 사용한 모든 DTO클래스의 멤버 변수들이 포함되어 있다.</p>
<hr>
<h2 id="🔎-data-access-계층">🔎 Data Access 계층</h2>
<h3 id="✔-jdbc">✔ JDBC</h3>
<pre><code class="language-java">public interface ListRepository extends CrudRepository&lt;ToDoList, Integer&gt; {

}</code></pre>
<p>Data Access 계층에서 DB와 연동을 담당하는 Repository 인터페이스이다.</p>
<p><code>CrudRepository&lt;ToDoList, Integer&gt;</code>에서 <code>ToDoList</code>는 ToDoList 엔티티 클래스를 가리키며, <code>Integer</code>은 ToDoList 엔티티 클래스에서 식별자를 의미하는 <code>@Id</code>애너테이션이 붙은 멤버 변수의 타입을 가리킨다.
(<code>@Id</code>애너테이션이 붙은 필드는 DB 테이블의 PK컬럼과 매핑된다)
이와 같이 제너릭 타입을 지정함으로써 해당 엔티티 객체의 데이터를 DB 테이블에 생성, 조회하거나 DB에서 조회한 데이터를 해당 엔티티 클래스로 변환할 수 있다.</p>
<p><code>CrudRepository</code>는 DB에 CRUD(데이터 생성, 조회, 수정, 삭제) 작업을 위해 Spring에서 지원해주는 인터페이스이다.
CRUD에 대한 기본적인 메서드가 정의되어 있다.</p>
<p><code>listRepository</code>인터페이스 구현 클래스 객체는 Spring Data JDBC에서 내부적으로 Java의 리플렉션 기술과 Proxy기술을 이용해서 생성해준다.</p>
<h4 id="entity-1">Entity</h4>
<p><code>ToDoList</code> 엔티티 클래스 명은 DB의 <strong>테이블 명</strong>에 해당한다.</p>
<p><code>@Id</code>애너테이션을 추가한 필드는 해당 엔티티의 <strong>고유 식별자</strong> 역할을 하고, 이 식별자는 DB의 <strong>Primary key</strong>로 지정한 컬럼에 해당한다.</p>
<h4 id="schemasql">schema.sql</h4>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS TODOLIST (
    id bigint NOT NULL AUTO_INCREMENT,
    ...
    PRIMARY KEY (id)
);</code></pre>
<p><code>id</code>는 TODOLIST테이블의 PK이고 AUTO_INCREMENT를 지정했으므로 데이터가 insert될 때마다 자동으로 증가된다.
즉, 애플리케이션에서 DB에 데이터를 insert할 때 <code>id</code>컬럼에 값을 지정해주지 않아야 한다.</p>
<p>ToDoList(엔티티)클래스 명은 DB의 TODOLIST테이블 명과 매핑되고 엔티티 클래스의 각각의 필드들은 DB 테이블 컬럼에 1:1 매핑된다.
대소문자 구분없이(SQL 대소문자 구분 &#39;X&#39;) ToDoList엔티티 명과 DB 테이블 TODOLIST 명과 이름이 같으면 매핑된다.</p>
<p><code>@Table</code>애너테이션을 엔티티에 추가하지 않으면 기본적으로 엔티티 클래스 명이 테이블의 이름과 매핑된다. 
<code>@Table(&quot;TODO&quot;)</code>와 같이 매핑되는 테이블 이름을 <code>TODO</code>로 변경할 수 있다.</p>
<h3 id="✔-jpa">✔ JPA</h3>
<p>JPA(Java Persistence API, Jakarta Persistence) : Java진영에서 사용하는 ORM(Object Relational Mapping)기술의 표준 사양(또는 명세)이다.
표준 사양(또는 명세)이라는 의미는 Java의 인터페이스로 사양이 정의되어 있어 구현체는 따로 있다는 것을 의미한다.
즉, JPA의 구현체에 대해서 공부하면 된다고 합니다.</p>
<h4 id="hibernate-orm">Hibernate ORM</h4>
<p>JPA 표준 사양을 구현한 구현체
JPA에서 지원하는 기능 외에 Hibernate자체적으로 사용 가능한 API도 지원합니다.</p>
<p>Data Access 계층에서 JPA는 Service 계층과 바로 상호작용 하는 상단에 위치한다. 데이터 저장, 조회 등의 작업은 JPA를 거쳐 구현체인 Hibernate ORM을 통해 이루어지며 Hibernate ORM은 내부적으로 JDBC API를 이용해서 DB에 접근하게 된다.
<code>Service 계층 ↔ Data Access 계층 [JPA | Hibernate ORM | JDBC API] ↔ DB</code></p>
<h4 id="영속성-컨텍스트persistence-context">영속성 컨텍스트(Persistence Context)</h4>
<p>테이블과 매핑되는 Entity 객체 정보를 영속성 컨텍스트에 보관해서 오래 지속되게 합니다.
이렇게 보관된 Entity 정보는 DB 테이블에 데이터를 insert, update, select, delete하는데 사용된다.
<code>1차 캐시</code>라는 영역과 <code>쓰기 지연 SQL 저장소</code>라는 영역이 있다.</p>
<h4 id="엔티티-매핑">엔티티 매핑</h4>
<ul>
<li><strong>Entity(객체)와 테이블 매핑</strong><pre><code class="language-java">@Entity
@Table
public class Memo {
  @Id
  private Long id;
}</code></pre>
<code>@Entity</code> : JPA 관리 대상 Entity가 된다.
<code>@Entity(name = &quot;MEMOS&quot;)</code> : Entity 이름을 설정할 수 있다, name을 지정하지 않으면 기본값으로 클래스 이름을 Entity이름으로 사용</li>
</ul>
<p><code>@Table</code> : 옵션이다, <code>@Table</code>을 써주지 않으면 클래스 이름을 테이블 이름으로 사용(주로 Entity와 테이블 이름이 달라야 할 때 추가)
<code>@Table(name = &quot;MEMOS&quot;)</code> : 테이블 이름을 설정할 수 있다, name을 지정하지 않으면 기본값으로 클래스 이름을 테이블 이름으로 사용 </p>
<p><code>@Entity</code>와 <code>@Id</code>는 필수이며 함께 사용해야한다.
<code>@NoArgsConstructor</code> 즉, 기본 생성자는 필수로 추가해야 한다, Spring Data JPA에서 기본 생성자가 없으면 에러가 발생하는 경우가 있다.</p>
<hr>
<ul>
<li><p><strong>기본키(PK) 매핑</strong>
필드에 <code>@Id</code>를 추가하면 기본키(PK)가 된다.
JPA에서는 기본키(식별자, PK) 생성 전략을 지원한다.
기본키를 직접 할당하는 전략과 기본키를 자동 생성하는 전략이 있고 기본키 자동 생성 전략에는 IDENTITY, SEQUENCE, TABLE이 있다.</p>
<ul>
<li><p>기본키 직접 할당 전략</p>
<ul>
<li>코드에서 개발자가 기본키를 직접 써서 할당하는 방식</li>
<li>필드에 <code>@Id</code>만 추가하면 기본키 직접 할당 전략 적용</li>
<li><code>new Memo(1L)</code>과 같이 기본키를 직접 할당해서 Entity객체 생성</li>
</ul>
</li>
<li><p>IDNEITY 전략</p>
<ul>
<li><p>기본키 생성을 DB에 위임하는 전략으로 DB에서 기본키를 대신 생성</p>
</li>
<li><p>AUTO_INCREMENT를 이용해 기본키 생성</p>
</li>
<li><p>영속성 컨텍스트에서 DB로 commit전엔 기본키 값을 알 수 없음</p>
<pre><code class="language-java">@NoArgsConstructor
@Getter
@Entity
public class Memo {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY) // IDENTITY 전략
 private Long id;

 public Memo(Long id) { // 생성자
     this.id = id;
 }
}</code></pre>
</li>
</ul>
</li>
<li><p>SEQUENCE 전략</p>
<ul>
<li><code>@GeneratedValue(strategy = GenerationType.SEQUENCE)</code> 지정</li>
<li>DB의 시퀀스 이용</li>
<li>Entity가 영속성 컨텍스트에 저장되기 전에 DB가 시퀀스에서 기본키에 해당하는 값 제공 즉, commit 전에 확인 가능</li>
</ul>
</li>
<li><p>AUTO 전략</p>
<ul>
<li><code>@GeneratedValue(strategy = GenerationType.AUTO)</code></li>
<li>JPA가 DB에 따라 적절한 전략 자동 선택(mysql : auto_increment, oracle : sequence 등)</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><p><strong>필드(멤버 변수)와 컬럼 매핑</strong></p>
<pre><code class="language-java">@Getter
@Setter
@NoArgsConstructor // 필수
@AllArgsConstructor // 테스트를 위해 추가됨
@Entity
public class ToDoList { // Entity
  @Id // 식별자 지정
  @GeneratedValue(strategy = GenerationType.IDENTITY) // 식별자 생성 전략 지정
  private Integer id;

  @Column(nullable = false)
  private String title;

  ...
}</code></pre>
<p><code>@Column</code>은 필드와 컬럼을 매핑해주는 애너테이션이다.
<code>@Column</code>생략 시 JPA는 기본적으로 해당 필드가 테이블의 컬럼과 매핑되는 필드라고 간주하며, <code>@Column</code>의 애트리뷰트 기본값이 모두 적용된다.</p>
</li>
<li><p>nullable</p>
<ul>
<li>컬럼에 <code>null</code>값을 허용할지 여부</li>
<li>기본값 : <code>true</code> <ul>
<li>그러나 필드가 int나 long과 같은 기본형(primitive type)일 경우 <code>@Column</code>이 생략되면 기본적으로 <code>nullable = false</code>이다.</li>
</ul>
</li>
<li>즉, 필수 항목이면 <code>nullable = false</code></li>
</ul>
</li>
<li><p>updatable</p>
<ul>
<li>컬럼 값 수정 가능 여부</li>
<li>기본값 : <code>true</code></li>
<li>즉, 수정 불가하면 <code>updatable = false</code></li>
</ul>
</li>
<li><p>unique</p>
<ul>
<li>컬럼에 유니크 제약 조건 설정</li>
<li>기본값 : <code>false</code></li>
<li>즉, 고유한 값이면 <code>unique = true</code></li>
</ul>
</li>
</ul>
<blockquote>
<p>추가
<code>@Transient</code> : 필드에 추가하면 테이블 컬럼과 매핑하지 않겠다는 의미이다. 즉, DB에 저장 X
<br>
<code>@Enumerated</code> : enum 타입과 매핑</p>
</blockquote>
<ul>
<li><code>@Enumerated(EnumType.ORDINAL)</code> : enum의 순서를 나타내는 숫자를 테이블에 저장<ul>
<li>기존 enum 사이에 새로 enum이 추가 되면, 테이블에 저장된 enum의 순서 번호와 실제 enum의 순서 번호가 일치하지 않게 된다.</li>
</ul>
</li>
<li><code>@Enumerated(EnumType.STRING)</code> : enum의 이름을 테이블에 저장<ul>
<li>권장</li>
</ul>
</li>
</ul>
<hr>
<ul>
<li><strong>Entity 간의 연관 관계 매핑</strong></li>
</ul>
<hr>
<blockquote>
<p><strong>repository.save()</strong></p>
</blockquote>
<pre><code class="language-java">public ToDoList createList(ToDoList toDoList) { 
        return listRepository.save(toDoList); 
    }
public ToDoList updateList(ToDoList toDoList) {
        ...
        return listRepository.save(findList);
    }</code></pre>
<p><code>@Id</code> 애너테이션이 추가된 엔티티 클래스의 멤버 변수 값이 0 또는 null이면 신규 데이터라고 판단하여 테이블에 <code>insert</code> 쿼리를 전송한다.
반면에 <code>@Id</code> 애너테이션이 추가된 엔티티 클래스의 멤버 변수 값이 0 또는 null이 아니라면 이미 테이블에 존재하는 데이터라고 판단하여 테이블에 <code>update</code> 쿼리를 전송한다.</p>
<blockquote>
<p><strong>쿼리 메서드(Query Method)</strong>
Spring Data JDBC 지원, Spring Data JPA 지원
<code>find + By + SQL 쿼리문에서 WHERE 절의 컬럼명 + (WHERE 절 컬럼의 조건 데이터)</code> 형식으로 쿼리 메서드를 정의하면 조건에 맞는 데이터를 테이블에서 조회한다.
ex) <code>Optional&lt;엔티티&gt; findByListId(Integer listId);</code>
listId컬럼을 WHERE절의 조건으로 지정해서 해당 엔티티 테이블에서 하나의 row를 정의한다.
즉, <code>SELECT * FROM 엔티티 WHERE LIST_ID = ?</code>으로 DB의 해당 엔티티 테이블에 질의를 보낸다.
SQL 질의를 통한 결과 데이터를 엔티티 클래스의 객체로 리턴한다.
<br>
WHERE 절의 조건 컬럼을 여러 개 지정하는 방법 : <code>And</code> 사용
ex) <code>findByEmailAndName(String email, String name)</code>
<br>
※주의
<code>WHERE 절의 컬럼명</code>은 내부적으로 테이블의 컬럼명으로 변경되지만 실질적으론 엔티티 클래스로 작업을 하기 때문에 <strong>엔티티 클래스의 멤버 변수명</strong>을 적어야 한다.</p>
</blockquote>
<blockquote>
<p><strong>Optional</strong>
Spring Data JDBC 지원, Spring Data JPA 지원
<code>Optional&lt;엔티티&gt; findByEmail(String email);</code>는 리턴값을 Optional로 래핑한 것이다.
JDK1.8부터 생기면서 null 값 처리에 편리해서 많이 쓴다.</p>
</blockquote>
<hr>
<p>추가중...</p>
<p>❗ <strong>틀린 부분 댓글 지적 환영합니다</strong> ❗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] LF will be replaced by CRLF ~]]></title>
            <link>https://velog.io/@u_lim/Git-LF-will-be-replaced-by-CRLF</link>
            <guid>https://velog.io/@u_lim/Git-LF-will-be-replaced-by-CRLF</guid>
            <pubDate>Mon, 06 Feb 2023 16:34:26 GMT</pubDate>
            <description><![CDATA[<p><code>git add</code> 시에 <code>LF will be replaced by CRLF</code> the next time Git touches it 이라는 경고가 발생했습니다.
<img src="https://velog.velcdn.com/images/u_lim/post/1226ca86-3e6f-4fad-a74a-cd65dedcba0b/image.png" alt=""></p>
<p>운영체제마다 다른 줄바꿈에 대한 경고입니다.</p>
<h2 id="해결">해결</h2>
<ul>
<li>Window 명령어<pre><code>git config --global core.autocrlf true</code></pre></li>
<li>Linux 명령어<pre><code>git config --global core.autocrlf input</code></pre></li>
</ul>
<p>-&gt; 줄바꿈을 자동으로 변환해줍니다.</p>
<blockquote>
<p><strong>--global</strong>
시스템 전체 적용 옵션으로, 시스템 전체가 아닌 해당 프로젝트에만 적용하고 싶다면 
--global을 빼주면 됩니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Github] Github Action Build 에러2]]></title>
            <link>https://velog.io/@u_lim/Github-Github-Action-Build-%EC%97%90%EB%9F%AC2</link>
            <guid>https://velog.io/@u_lim/Github-Github-Action-Build-%EC%97%90%EB%9F%AC2</guid>
            <pubDate>Mon, 06 Feb 2023 15:19:31 GMT</pubDate>
            <description><![CDATA[<p>Github Action Build 시에 아래와 같은 에러를 만났습니다.
<code>Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain</code>
<code>Caused by: java.lang.ClassNotFoundException: org.gradle.wrapper.GradleWrapperMain</code></p>
<p><code>gradle/wrapper</code>에 <code>gradle-wrapper.jar</code> 파일이 없어서 발생하는 오류라고 합니다.</p>
<h2 id="해결">해결</h2>
<p><code>.gitignore</code>에 <code>.jar</code>파일을 해제 후 <code>gradle-wrapper.jar</code>파일을 push 해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/u_lim/post/c865a5ef-d2e0-4a86-9bc9-cc85fd0f03f4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/u_lim/post/6ee471f4-1cce-4ba4-ad5e-8dec666c5d3e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Github] Github Action Build 에러]]></title>
            <link>https://velog.io/@u_lim/Github-Github-Action-Build-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@u_lim/Github-Github-Action-Build-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Mon, 06 Feb 2023 14:45:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/u_lim/post/79740e0a-eda2-4a99-a5ca-d5d9ca785756/image.png" alt=""></p>
<p><code>~/gradlew&#39; is not executable.</code> 에러가 발생하면 <code>Build with Gradle</code> 전에 <code>./gradlew</code>에 권한을 부여하는 단계를 추가해야 합니다.</p>
<h2 id="해결">해결</h2>
<pre><code>- name: Add permission
  run: chmod +x ./gradlew</code></pre><p><code>gradle.yml</code>에 추가
<img src="https://velog.velcdn.com/images/u_lim/post/fe4fbe0f-3e7a-4eee-81bf-b6634ac2aa0b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Git] error: The following untracked working tree files would be overwritten by merge:]]></title>
            <link>https://velog.io/@u_lim/Git-error-The-following-untracked-working-tree-files-would-be-overwritten-by-merge</link>
            <guid>https://velog.io/@u_lim/Git-error-The-following-untracked-working-tree-files-would-be-overwritten-by-merge</guid>
            <pubDate>Mon, 06 Feb 2023 07:58:46 GMT</pubDate>
            <description><![CDATA[<p>remote repository를 만들고 <code>.gitignore</code>를 수정
local repository가 될 폴더에도 <code>.gitignore</code> 존재
이 둘을 <code>git remote add ~</code>로 연결 후 <code>git pull origin main</code>을 했을 때 발생한 오류입니다.</p>
<hr>
<p><code>git pull</code>을 할 때 오류가 발생했습니다.
<img src="https://velog.velcdn.com/images/u_lim/post/5f9997bb-fb83-46b6-a7b0-675ad19021b8/image.png" alt="">
<code>error: The following untracked working tree files would be overwritten by merge:</code>
에러를 해석하면(파파고) : 다음 추적되지 않은 작업 트리 파일을 병합하여 덮어씁니다
untracked files (git에 추척되지 않은 파일들)이 있어서 머지할 때 덮어 쓸 수 있어 에러가 난다는 것 같습니다.</p>
<h2 id="해결">해결</h2>
<p><code>Please meve or remove them before you merge.</code>
<code>.gitignore</code>는 local에서 삭제(remove)해도 애플리케이션에 영향을 받지 않아 지우고 <code>git pull</code>로 다시 받아줬습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] 이미지 삭제 에러]]></title>
            <link>https://velog.io/@u_lim/Docker-image-is-being-used-by-stopped-container-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@u_lim/Docker-image-is-being-used-by-stopped-container-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Thu, 02 Feb 2023 08:01:36 GMT</pubDate>
            <description><![CDATA[<p>도커 이미지를 삭제하는데 에러가 발생했다.</p>
<ul>
<li><p>IMAGE NAME, Repository NAME으로 삭제 했을 때 (<code>docker image rm docker/whalesay</code>)
에러 메시지
<code>Error response from daemon: conflict: unable to remove repository reference &quot;docker/whalesay&quot; (must force) - container [container_id] is using its referenced image [image_id]</code></p>
</li>
<li><p>IMAGE ID로 삭제 했을 때 (<code>docker image rm [image_id]</code>)
에러 메시지
<code>Error response from daemon: conflict: unable to delete [image_id] (must be forced) - image is being used by stopped container [container_id]</code></p>
</li>
</ul>
<h2 id="해결">해결</h2>
<p>image를 사용하는 container를 먼저 삭제 하면 된다.</p>
<ul>
<li><p>컨테이너 삭제
<code>docker container rm whalesay</code></p>
</li>
<li><p>이미지 삭제
<code>docker image rm docker/whalesay</code></p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>