<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>peace_e.log</title>
        <link>https://velog.io/</link>
        <description>더 성장하자.</description>
        <lastBuildDate>Sun, 03 May 2026 03:55:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>peace_e.log</title>
            <url>https://velog.velcdn.com/images/peace_e/profile/38bc6b2e-4e57-41d2-a828-a17179b9158b/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. peace_e.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/peace_e" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[AWS] 인스턴스를 삭제했는데 계속해서 과금이 되는 경우]]></title>
            <link>https://velog.io/@peace_e/AWS-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EB%A5%BC-%EC%82%AD%EC%A0%9C%ED%96%88%EB%8A%94%EB%8D%B0-%EA%B3%84%EC%86%8D%ED%95%B4%EC%84%9C-%EA%B3%BC%EA%B8%88%EC%9D%B4-%EB%90%98%EB%8A%94-%EA%B2%BD%EC%9A%B0</link>
            <guid>https://velog.io/@peace_e/AWS-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4%EB%A5%BC-%EC%82%AD%EC%A0%9C%ED%96%88%EB%8A%94%EB%8D%B0-%EA%B3%84%EC%86%8D%ED%95%B4%EC%84%9C-%EA%B3%BC%EA%B8%88%EC%9D%B4-%EB%90%98%EB%8A%94-%EA%B2%BD%EC%9A%B0</guid>
            <pubDate>Sun, 03 May 2026 03:55:54 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 서버를 오라클로 옮기면서 기존에 사용하던 AWS 인스턴스를 전부 삭제했다. 그런데 계속해서 과금이 되고 있었다. 
7000원 정도 소액 결제라 <del>귀찮아서 방치하고 있었는데</del>
결제됐다는 알람이 뜰 때마다 AWS 콘솔 들어가서 확인해봐야지 싶다가 현생이 너무 바빠서 귀가하면 까먹기 일쑤였다...</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/1e735bec-70a0-42f6-bd56-a806ab9b87ee/image.png" alt="">
과금된 내역은 이러하다.
두 가지 부분에서 과금이 된 걸 확인할 수 있다.</p>
<ul>
<li>Amazon Virtual Private Cloud Public IPv4 Addresses</li>
<li>EBS</li>
</ul>
<h2 id="public-ip-릴리즈-하기">Public IP 릴리즈 하기</h2>
<p>기존 인스턴스 서버에 Public IP를 연결해서 쓰고 있었다면, 인스턴스만 삭제한다고 해서 Public IP가 저절로 삭제되지 않는다.
Public IP는 따로 릴리즈 해주어야 한다.</p>
<p>콘솔 &gt; VPC &gt; 탄력적 IP 탭에서 본인이 사용하고 있는 탄력적 IP 주소를 확인할 수 있다.
리전 선택에 따라 달라지니, 본인이 사용하고 있는 리전에서 조회하자.
<img src="https://velog.velcdn.com/images/peace_e/post/cf703e16-c3d3-458a-b1d0-2babc0a64dff/image.png" alt=""></p>
<p>존재하던 IP를 릴리즈한 직후의 모습이다. 이미 인스턴스는 삭제한 후라 콘솔 화면에 빠르게 반영이 되었다.</p>
<h2 id="ebs-스냅샷-삭제">EBS 스냅샷 삭제</h2>
<p>인스턴스를 삭제할 때, 인스턴스 스냅샷은 함께 삭제되지 않는다.
인스턴스를 삭제하면서 혹시 몰라 스냅샷을 저장했던 것 같은데 해당 스냅샷이 계속 남아있어서 과금이 되었다.</p>
<p>콘솔 &gt; EC2 &gt; Elastic Block Store &gt; 스냅샷
에서 저장된 스냅샷들을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/72cd1ba7-3f41-4c51-920d-9e61c085f42f/image.png" alt=""></p>
<p>남아있는 스냅샷을 삭제하려 했는데 아래처럼 메세지가 떴다.
<img src="https://velog.velcdn.com/images/peace_e/post/09f52dc7-94ec-4010-81e0-549c047c722d/image.png" alt="">
스냅샷이 AMI에 연결이 되어있어 삭제가 되지 않았다.
문제가 된 ami-099...를 클릭하면 자동으로 페이지가 이동된다. </p>
<p>해당 AMI를 삭제해준다.
<img src="https://velog.velcdn.com/images/peace_e/post/1858615b-cbd5-4c19-95be-e172854e0a5a/image.png" alt=""></p>
<p>AMI를 삭제할 때, 연결된 스냅샷도 자동으로 삭제할 수 있게 해준다.
<img src="https://velog.velcdn.com/images/peace_e/post/41a0fbac-bced-4c1f-9db7-b2fc107918b8/image.png" alt=""></p>
<hr>
<p>이제 지긋지긋한 AWS와도 이별이다..ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PostgreSQL] 쿼리 속도 개선하기]]></title>
            <link>https://velog.io/@peace_e/PostgreSQL-%EC%BF%BC%EB%A6%AC-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/PostgreSQL-%EC%BF%BC%EB%A6%AC-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 09 Feb 2026 16:22:14 GMT</pubDate>
            <description><![CDATA[<h1 id="겪은-상황">겪은 상황</h1>
<ul>
<li><p>크롤링한 가게 데이터들을 추가하고 나서 인덱스를 작업하지 않았다. (4만건 정도)</p>
</li>
<li><p>추가로 distange_range라는 값을 추가로 주어야했다.</p>
<blockquote>
<p>ex.) 가게는 250m/500m/750m 단위로 검색할 수 있게 해둔 상황. sorting을 위해 
현재 위치와 100m 떨어져있으면 &quot;distance_range&quot;: &quot;250&quot;, 
현재 위치와 450m 떨어져있으면 &quot;distance_range&quot;: &quot;500&quot; 
이런 식으로 값을 추가로 보내야 했다.</p>
</blockquote>
</li>
<li><p>쿼리를 수정하니 원래 1~2초 단위로 실행되던 api가 15초 이상이 걸렸다.</p>
</li>
</ul>
<h1 id="해결방법">해결방법</h1>
<h2 id="1-서비스-단에서-계산해서-반환하기">1. 서비스 단에서 계산해서 반환하기</h2>
<ul>
<li>쿼리에서 distance를 계산하지 않고, 서비스 단에서 계산하는 것으로 수정했다.<pre><code class="language-java">return stores.stream()
          .map(store -&gt; {
              int distance = store.getDistance();
              int distanceRange = distance &lt;= 250 ? 250 
                            : distance &lt;= 500 ? 500 
                            : distance &lt;= 750 ? 750 
                            : 0;</code></pre>
</li>
<li><blockquote>
<p>속도에 큰 변화가 없었다.(여전히 10초 대)</p>
</blockquote>
</li>
</ul>
<h2 id="2-시간-측정-추가하기">2. 시간 측정 추가하기</h2>
<ul>
<li>근본적으로 어디 부분에서 시간이 많이 소요되는지를 알아야했다.<pre><code class="language-java">long totalStart = System.currentTimeMillis();
</code></pre>
</li>
</ul>
<p>long start = System.currentTimeMillis();
List<String> storeIds = findStoreService.메서드명1(변수...);
System.out.println(&quot;메서드명1: &quot; + (System.currentTimeMillis() - start) + &quot;ms&quot;);</p>
<p>start = System.currentTimeMillis();
List<StoreDistanceDTO> storeDetails = findStoreService.메서드명2(변수...);
System.out.println(&quot;메서드명2: &quot; + (System.currentTimeMillis() - start) + &quot;ms&quot;);</p>
<p>System.out.println(&quot;Total time: &quot; + (System.currentTimeMillis() - totalStart) + &quot;ms&quot;);</p>
<pre><code>이런 식으로 실제 시간을 측정하는 로그를 추가했다.
이 방법으로 서비스 단에서 객체 변환이 아닌 DB 조회가 오래 걸리는 것임을 확인했다. (객체 변환에서는 2ms 정도밖에 소요되지 않았다.)


## 3. 쿼리 개선
### 3-1) CTE 절에서 조건 추가하여 전부 스캔하지 않도록 변경
- CTE 절에서 모든 건수를 조회하지 않도록 조건을 추가했다.
```sql
-- 원래 사용하던 CTE절
WITH menu_counts AS (
    SELECT store_id, COUNT(*) AS cnt
    FROM menu
    WHERE represent = &#39;Y&#39;  -- 전체 menu 테이블 스캔!
    GROUP BY store_id
)

-- 개선한 CTE 절
WITH menu_counts AS (
    SELECT store_id, COUNT(*) AS cnt
    FROM menu
    WHERE represent = &#39;Y&#39;
      AND store_id IN (N개)  -- 해당하는 N개만 조회
    GROUP BY store_id
)</code></pre><h3 id="3-2-불필요한-서브쿼리를-직접-join으로-수정">3-2) 불필요한 서브쿼리를 직접 JOIN으로 수정</h3>
<ul>
<li>store 테이블을 2번 조회 (메인 쿼리 + 서브쿼리) 하던 것을 직접 JOIN으로 바꾸어 한번만 조회하도록 수정했다.<pre><code class="language-sql">LEFT JOIN
  (SELECT st.store_id, f.type1, f.food_type_id
   FROM store st
   JOIN food_type f ON st.category = f.type2
   WHERE st.store_id IN (N개)  -- store 테이블 또 조회
  ) f ON s.store_id = f.store_id

</code></pre>
</li>
</ul>
<p>LEFT JOIN food_type f ON s.category = f.type2  -- 직접 JOIN</p>
<pre><code>-&gt; 2초대로 줄었다.

## 4. 인덱스 추가
- 가게 테이블의 가게 PK
- 메뉴 테이블의 가게 FK
- 방송 테이블의 가게 FK
- 음식 타입 테이블의 타입 ID
- ... 등 JOIN 이나 WHERE 에서 자주 쓰일 컬럼에 인덱스를 추가하였다.
- 인덱스가 없으면 `TABLE FULL SCAN`이 일어나 데이터 조회에 많은 시간이 소요될 수 있다.
&gt; 가게를 조회할 때 마다, 메뉴/방송/가게/음식 타입 테이블을 전부 조회한다.  

```sql
-- 인덱스가 있는지 확인하고 없으면 만든다.
CREATE INDEX IF NOT EXISTS idx_store_id ON store(store_id);

-- 인덱스가 있는지만 확인한다.
SELECT indexname, indexdef 
FROM pg_indexes 
WHERE tablename = &#39;store&#39;;</code></pre><p>-&gt; 실행시간이 30ms 대로 줄었다.</p>
<h2 id="5-쿼리-개선2">5. 쿼리 개선2</h2>
<p>다른 메서드에서도 병목현상으로 3초 정도로 실행되고 있어서 쿼리를 개선했다.</p>
<h3 id="5-1-현재-상황">5-1) 현재 상황</h3>
<ul>
<li>가게 영업시간 테이블에 저장된 값이 시간만 들어가 있거나 정기 휴무&lt;이런 식으로 텍스트가 들어가 있는경우도 있었다. (매일 달라지는 시간을 어떻게 관리하는게 좋을지 모르겠어서 이렇게 시작했으나 더 좋은 방법이 있다면 알려주실 분...)</li>
</ul>
<ul>
<li>where 절에서 영업시간이 텍스트인 행들을 먼저 필터링 했어도 PostgreSQL 옵티마이저가 실행 순서를 바꿔서 <code>hh24:mi</code> 부분에서 에러가 났었던 적이 있었다.</li>
</ul>
<h3 id="5-2-문제점1-3개의-중첩된-exists--테이블을-3번-스캔">5-2) 문제점1. 3개의 중첩된 EXISTS = 테이블을 3번 스캔</h3>
<ul>
<li>그래서 텍스트인 행들을 필터링 하겠다고 테이블들을 반복해서 접근 하고 있었다.<pre><code class="language-sql">SELECT DISTINCT bh.store_id
FROM business_hours bh
WHERE ...
AND EXISTS (
    SELECT 1 FROM business_hours bh2  -- 1번째 스캔
    WHERE bh2.store_id = bh.store_id
    AND bh2.business_type = &#39;O0001&#39;
    ...
)
AND EXISTS (
    SELECT 1 FROM business_hours bh3, bh4  -- 2번째, 3번째 스캔
    WHERE bh3.store_id = bh.store_id
    AND bh4.store_id = bh.store_id
    ...
)</code></pre>
</li>
</ul>
<h3 id="5-3-문제점2-동적-컬럼명-currentday---인덱스-활용-불가">5-3) 문제점2. 동적 컬럼명 (${currentDay}) - 인덱스 활용 불가</h3>
<ul>
<li>요일별로 동적인 컬럼명을 사용하고 있었는데 이 부분에서 인덱스 활용이 불가했다.<pre><code class="language-sql">WHERE bh2.${currentDay} IS NOT NULL  -- currentDay = &#39;mon&#39;, &#39;tue&#39; 등</code></pre>
<h3 id="5-4-문제점3-like-패턴-검색---인덱스-활용-불가">5-4) 문제점3. LIKE 패턴 검색 - 인덱스 활용 불가</h3>
<pre><code class="language-sql">WHERE bh2.${currentDay}::text NOT LIKE &#39;%휴무%&#39;</code></pre>
</li>
<li>%휴무%는 앞뒤에 와일드카드가 있어서 인덱스가 있어도 전체 스캔이 필요하다.</li>
</ul>
<h3 id="5-5-해결책1-cte로-한번에-조회하여-해결">5-5) 해결책1. CTE로 한번에 조회하여 해결</h3>
<pre><code class="language-sql">WITH valid_stores AS (
    -- 폐업하지 않은 store만 먼저 필터링
),
dinner_hours AS (
    SELECT 
        store_id,
        MAX(CASE WHEN business_type = &#39;O0001&#39; THEN ${currentDay} END) as open_time,
        MAX(CASE WHEN business_type = &#39;C0001&#39; THEN ${currentDay} END) as close_time
    FROM business_hours
    WHERE store_id IN (SELECT store_id FROM valid_stores)
      AND business_type IN (&#39;O0001&#39;, &#39;C0001&#39;)
      AND ${currentDay} IS NOT NULL
      AND ${currentDay} NOT IN (&#39;휴무&#39;, &#39;정기휴무&#39;, &#39;&#39;)
    GROUP BY store_id  -- 핵심: 한 번에 집계!
)</code></pre>
<ul>
<li>CTE절을 사용하여 1번의 조회로 모든 테이블을 가져오도록 수정했다.</li>
<li>GROUP BY로 각 store의 open_time, close_time을 메모리에서 집계하고 있다.</li>
</ul>
<h3 id="5-6-해결책2-cte절-내에서-case-문으로-데이터-피봇">5-6) 해결책2. CTE절 내에서 CASE 문으로 데이터 피봇</h3>
<pre><code class="language-sql">MAX(CASE WHEN business_type = &#39;O0001&#39; THEN ${currentDay} END) as open_time,
MAX(CASE WHEN business_type = &#39;C0001&#39; THEN ${currentDay} END) as close_time</code></pre>
<pre><code class="language-sql">## 개선 전:
-- O0001 찾기 위해 테이블 스캔
SELECT ... WHERE business_type = &#39;O0001&#39;
-- C0001 찾기 위해 또 테이블 스캔  
SELECT ... WHERE business_type = &#39;C0001&#39;

### 개선 후:
-- 한 번에 가져와서 메모리에서 분리
CASE WHEN business_type = &#39;O0001&#39; THEN ... 
CASE WHEN business_type = &#39;C0001&#39; THEN ...</code></pre>
<h3 id="5-7-해결책3-cte절-내에서-휴무-조건을-한번에-필터링">5-7) 해결책3. CTE절 내에서 휴무 조건을 한번에 필터링</h3>
<pre><code class="language-sql">## 개선 전 :
EXISTS (
    SELECT 1 FROM business_hours bh2
    WHERE ... AND bh2.${currentDay} NOT LIKE &#39;%휴무%&#39;
)
-- 각 store마다 반복 검사!

## 개선 후 :
WHERE business_type IN (&#39;O0001&#39;, &#39;C0001&#39;)
  AND ${currentDay} IS NOT NULL
  AND ${currentDay} NOT IN (&#39;휴무&#39;, &#39;정기휴무&#39;, &#39;&#39;)
  AND ${currentDay}::text NOT LIKE &#39;%휴무%&#39;
-- 한 번만 검사!</code></pre>
<h2 id="6-쿼리-개선3">6. 쿼리 개선3</h2>
<h3 id="6-1-문제점-1-인덱스-사용이-불가한st_distancesphere">6-1) 문제점 1. 인덱스 사용이 불가한<code>ST_DistanceSphere</code></h3>
<pre><code class="language-sql">WHERE ST_DistanceSphere(location, ST_GeomFromText(...)) &lt;= 750</code></pre>
<ul>
<li><code>GIST 인덱스</code>는 <strong>공간 범위 검색</strong>에 최적화되어 있다.</li>
<li>그러나 <code>ST_DistanceSphere</code>는 <strong>함수 실행 결과</strong>를 비교한다.</li>
<li>그러므로 PostgreSQL은 함수 실행 결과를 비교하려면 전체 스캔을 하게된다.</li>
</ul>
<h3 id="6-2-해결책-1-st_dwithin으로-공간-인덱스-직접-활용">6-2) 해결책 1. <code>ST_DWithin</code>으로 공간 인덱스 직접 활용</h3>
<pre><code class="language-sql">WHERE ST_DWithin(
    s.location::geography,
    ST_SetSRID(ST_MakePoint(#{currentLong}, #{currentLat}), 4326)::geography,
    750
)</code></pre>
<h3 id="6-3-gist-인덱스의-작동-원리">6-3) GIST 인덱스의 작동 원리</h3>
<p><strong>GIST (Generalized Search Tree):</strong></p>
<ul>
<li>범위 기반 또는 위치 기반 검색에 최적화된 트리 구조이다.</li>
<li>데이터의 일부만 빠르게 필터링하여 검색 시간을 단축하는 트리 구조를 구축할 수 있다.</li>
<li>공간 데이터를 직사각형으로 묶어 그룹화하여 데이터를 구성한다. 이를 <code>MBR(Minimum Bounding Region)</code> <code>(최소 경계 영역)</code>이라고 한다.</li>
<li>직사각형의 좌표를 알기 때문에 검색하려는 지점이 특정 그룹 내에 있는지 쉽게 확인할 수 있다.</li>
</ul>
<h3 id="6-4-geography-타입---geometry-타입">6-4) geography 타입 -&gt; geometry 타입</h3>
<ul>
<li>geometry는 평면 좌표계 계산용</li>
<li>geography는 지구 표면(위/경도) 기반 계산용으로, 보통 위도/경도 데이터를 다룰 때 더 적합하다.</li>
</ul>
<pre><code class="language-sql">## 원래 (geometry):
sqlST_DistanceSphere(location, ST_GeomFromText(...))
-- geometry 타입, 거리는 &quot;도&quot; 단위 → 미터로 변환 필요

## 개선 (geography):
sqlST_DWithin(s.location::geography, ...::geography, 750)
-- geography 타입, 거리가 바로 &quot;미터&quot; 단위</code></pre>
<pre><code class="language-sql">-- geometry: 1도 ≈ 111km (위도에 따라 다름)
WHERE ST_Distance(location, point) &lt;= 0.0067  -- 750m를 도로 변환?

-- geography: 직관적!
WHERE ST_DWithin(location, point, 750)  -- 750미터</code></pre>
<hr>
<h2 id="출처">출처</h2>
<p><a href="https://support.servbay.com/ko/database-management/postgresql-extensions/postgis">ServBay에서 PostGIS 사용하기: PostgreSQL에 공간 기능 추가</a>
<a href="https://jaeuk97.tistory.com/96">[Postgresql] Gist 인덱스란?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CORS] CORS 요청의 종류와  CURL로 CORS 간단 테스트하기]]></title>
            <link>https://velog.io/@peace_e/CORS-CORS-%EC%9A%94%EC%B2%AD%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-CURL%EB%A1%9C-CORS-%EA%B0%84%EB%8B%A8-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/CORS-CORS-%EC%9A%94%EC%B2%AD%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-CURL%EB%A1%9C-CORS-%EA%B0%84%EB%8B%A8-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 27 Jan 2026 15:46:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/peace_e/post/b727e233-fd52-4cab-a5f1-869ef31722d0/image.png" alt=""></p>
<p>서버 옮기면서 CORS 에러가 또 발생했다..
<a href="https://velog.io/@peace_e/Spring-boot-CORS-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0">https://velog.io/@peace_e/Spring-boot-CORS-해결하기</a>
↑ 이전에 스프링부트 수정하여 해결했던 건은 여기 참고</p>
<hr>
<p>CORS 요청의 종류와 테스트 방법에 대해 정리하고자 한다.</p>
<h2 id="cors-요청의-종류">CORS 요청의 종류</h2>
<h3 id="1-simple-request-단순-요청">1. Simple Request (단순 요청)</h3>
<p>다음 조건을 모두 만족하면 preflight 없이 바로 실제 요청을 보낸다</p>
<p>메서드: <code>GET</code>, <code>POST</code>, <code>HEAD</code> 중 하나
헤더: <code>Accept</code>, <code>Accept-Language</code>, <code>Content-Language</code>, <code>Content-Type</code> 등 기본 헤더만
Content-Type: <code>application/x-www-form-urlencoded</code>, <code>multipart/form-data</code>, <code>text/plain</code> 중 하나</p>
<h3 id="2-preflight-request-사전-요청">2. Preflight Request (사전 요청)</h3>
<p>단순 요청에 해당하지 않으면 Preflight(OPTIONS) 요청을 먼저 보낸다.</p>
<p>메서드: <code>PUT</code>, <code>DELETE</code>, <code>PATCH</code> 등
커스텀 헤더: <code>Authorization</code>, <code>X-Custom-Header</code> 등
Content-Type: <code>application/json</code> 등</p>
<p>예비 요청으로 <code>OPTIONS</code> 메서드를 보내서 이 요청이 안전한지 판단한 후, 단순 요청을 요청한다.</p>
<h2 id="access-control-allow--access-control-request-헤더">Access-Control-Allow / Access-Control-Request 헤더</h2>
<h3 id="1-access-control-allow--헤더">1. Access-Control-Allow-* 헤더</h3>
<ul>
<li>서버가 응답할 때 포함하는 헤더</li>
<li>모든 CORS 요청의 응답에 포함되어야 함</li>
</ul>
<blockquote>
<p>Simple Request(단순 요청)의 응답 ✅
Preflight Request(OPTIONS)의 응답 ✅
Preflight 이후 실제 요청의 응답 ✅</p>
</blockquote>
<h3 id="2-access-control-request--헤더">2. Access-Control-Request-* 헤더</h3>
<ul>
<li>브라우저(클라이언트)가 요청할 때 포함하는 헤더</li>
</ul>
<blockquote>
<p>Preflight Request(OPTIONS)에만 포함됨 ✅
실제 요청(POST, GET 등)에는 포함 안 됨 ❌</p>
</blockquote>
<h2 id="curl-명령어로-테스트하기">CURL 명령어로 테스트하기</h2>
<h3 id="curl--i">curl -i</h3>
<pre><code class="language-bash">curl -i &quot;https://URL...&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;Origin: http://localhost:3000&quot; \</code></pre>
<p>CURL 요청은 메서드 지정하지 않을 시, 기본적으로 GET 요청이다.</p>
<p>※ 단, <code>-d</code> (또는 <code>--data</code>) 옵션을 사용하면 자동으로 POST 요청이 된다. 아래 두 요청은 동일한 POST 요청이다.</p>
<h3 id="curl--i--x-post">curl -i -X POST</h3>
<pre><code class="language-bash">curl -i &quot;https://URL...&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;Origin: http://localhost:3000&quot; \
  -d &#39;{&quot;test&quot;:&quot;test1234&quot;}&#39;

curl -i -X POST &quot;https://URL...&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;Origin: http://localhost:3000&quot; \
  -d &#39;{&quot;test&quot;:&quot;test1234&quot;}&#39;</code></pre>
<h3 id="curl--i--x-options">curl -i -X OPTIONS</h3>
<p>Preflight 요청을 테스트하고 싶다면 OPTIONS 메서드를 써야한다.</p>
<pre><code class="language-bash">curl -i -X OPTIONS &quot;https://144.24.86.170.nip.io/api/user/login&quot; \
  -H &quot;Origin: http://localhost:3000&quot; \
  -H &quot;Access-Control-Request-Method: POST&quot; \
  -H &quot;Access-Control-Request-Headers: content-type&quot;</code></pre>
<h3 id="응답반환">응답반환</h3>
<p>성공적이라면 이런 응답을 받게 될 것이다.</p>
<pre><code class="language-bash">HTTP/2 200
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:3000
Date: Tue, 27 Jan 2026 15:01:02 GMT
...</code></pre>
<blockquote>
<p>여기서 Date의 경우 HTTP 프로토콜 표준(RFC 7231)에 따라서, Date 헤더는 반드시 GMT(UTC) 형식으로 표시된다. 이는 서버의 타임존 설정과 무관하다.</p>
</blockquote>
<hr>
<h2 id="내가-겪은-상황">내가 겪은 상황</h2>
<ul>
<li>CURL로 테스트했을 때, response 가 호출되고 있어서 정상적으로 되는 줄 알았다.</li>
<li>그러나 <code>OPTIONS 요청</code>에는 <code>Access-Control-Allow-Origin 헤더</code>에  <a href="http://localhost:3000%EC%9D%B4">http://localhost:3000이</a> 있었지만</li>
<li><code>POST 요청</code>에는 <code>Access-Control-Allow-Origin 헤더</code>가 없었다.</li>
</ul>
<h2 id="해결방법">해결방법</h2>
<ul>
<li><code>setAllowedOrigins</code> 대신 <code>SetAllowedOriginPatterns</code> 메서드로 바꿨다. (<code>setAllowCredentials(true)</code> 와 사용할 때 좀 더 안정적이라고 함.)</li>
<li><code>setAllowedHeaders(Arrays.asList(&quot;*&quot;)</code> 로 단순화하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PostgreSQL] 데이터 dump 뜨기]]></title>
            <link>https://velog.io/@peace_e/PostgreSQL-%EB%8D%B0%EC%9D%B4%ED%84%B0-dump-%EB%9C%A8%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/PostgreSQL-%EB%8D%B0%EC%9D%B4%ED%84%B0-dump-%EB%9C%A8%EA%B8%B0</guid>
            <pubDate>Sun, 25 Jan 2026 17:44:53 GMT</pubDate>
            <description><![CDATA[<h2 id="기본-덤프-명령문">기본 덤프 명령문</h2>
<pre><code class="language-bash">pg_dump -h ${RDS ENDPOINT} -U ${DB유저명} -d ${DB명} -Fc -f ${DUMP 파일명}
</code></pre>
<p>\ (역슬래시)를 사용해서 엔터를 칠 수도 있다. \를 사용하는 경우에는 한줄로 입력하면 제대로 인식이 안될 수도 있으니 주의</p>
<pre><code class="language-bash">pg_dump \
 -h ${RDS ENDPOINT} \ 
 -U ${DB유저명} \ 
 -d ${DB명} \
 -Fc \
 -f ${DUMP 파일명}</code></pre>
<blockquote>
<p>역슬래쉬 사용하여 한 줄로 입력 시
pg_dump: error: too many command-line arguments ...</p>
</blockquote>
<hr>
<h1 id="삽질의-기록">삽질의 기록</h1>
<h2 id="1-pg_dump">1. pg_dump</h2>
<p>실제 DB 인스턴스에 깔려있는 PostgreSQL의 버전이
덤프를 뜰 인스턴스에 깔려있는 PostgreSQL의 버전보다 높거나 같아야한다. (당연하지만...)</p>
<p>기존 서버를 구현할 때, 서버에 PostgreSQL을 설치를 안하고, 로컬 DB로만 붙어서 모든 걸 하다보니 나중에 덤프를 뜰 때 굉장히 곤란한 상황이 발생했다..
PostgreSQL16을 쓰고 있었는데, Amazon-Linux23에는 PostgreSQL16버전이 없었다.(못 찾았음)</p>
<pre><code class="language-bash">sudo dnf install postgresql16 # -&gt; X
## postgresql16을 찾을 수 없다고 뜬다.

sudo dnf install postgresql15 # -&gt; O
## 15버전까지는 가능하다.

sudo dnf search postgresql 
## 이 명령어로 설치 가능한 postgresql을 찾을 수 있다.</code></pre>
<p><img src="https://velog.velcdn.com/images/peace_e/post/1925e548-976d-49b6-be74-108610c5d9c6/image.png" alt=""></p>
<h3 id="linux-23-에서-postgresql16-설치-시도">Linux 23 에서 PostgreSQL16 설치 시도</h3>
<p>그래서 PGDG(RHEL용 PostgreSQL 공식 repo)를 그대로 설치하려고 했는데</p>
<p>Amazon-Linux23는 RHEL/CentOS 호환이 아니라서</p>
<ul>
<li>/etc/redhat-release 파일이 없다</li>
<li>그래서 PGDG 를 그대로 설치하면 깨진다</li>
<li><blockquote>
<p>AL2023에서는 PGDG repo 방식이 공식적으로 지원되지 않았다..</p>
</blockquote>
</li>
</ul>
<p>그래서 Docker로 우회하여 PostgreSQL16을 설치했다.</p>
<h3 id="ubuntu-에서-postgresql16-설치-시도">Ubuntu 에서 PostgreSQL16 설치 시도</h3>
<pre><code class="language-bash"># https://www.postgresql.org/download/linux/ubuntu/ 
# 공식 문서 참고
sudo apt install postgresql-16 # -&gt; O</code></pre>
<h3 id="docker를-사용한-dump-명령문">Docker를 사용한 dump 명령문</h3>
<p><strong>dump를 뜰 서버 인스턴스에서 실행</strong></p>
<pre><code class="language-bash">docker run --rm \
  -v &quot;$PWD:/work&quot; \
  -e PGPASSWORD=&#39;DB비밀번호&#39; \
  postgres:16 \
  pg_dump \
    -h ${RDS의 ENDPOINT} \
    -U ${유저명} \
    -d ${DB명} \
    -Fc \
    -f /work/${덤프파일명} ## 출력 파일 경로</code></pre>
<blockquote>
<p><code>$PWD</code>: 현재 작업 디렉토리 (Present Working Directory)
<code>/work</code>: Docker 컨테이너 내부의 경로
<code>:</code>로 연결하여 호스트와 컨테이너를 연결</p>
</blockquote>
<p>덤프 생성 후에는 </p>
<pre><code class="language-bash">ls -lh ${덤프파일명}</code></pre>
<p>으로 파일이 제대로 생성되었는지 반드시 확인하자
<strong>(파일 용량이 0kb라면 제대로 생성되지 않은 것)</strong></p>
<p>구 app 서버 -&gt; 로컬 -&gt; 신 app 서버
순으로 dump 파일을 옮겨준다.</p>
<p><strong>내려받은 로컬에서 실행</strong></p>
<pre><code class="language-bash">scp -i [SSH키파일] [원본] [대상] ## 기본 구조

scp -i &quot;C:/Users/.ssh/이전서버.key&quot; ec2-user@[PUBLIC IP]:/home/ec2-user/[DUMP파일명] .</code></pre>
<blockquote>
<p>키파일 경로는 <code>&quot;&quot;</code>로 감싸준다.
<code>.</code> : 현재 디렉토리</p>
</blockquote>
<ul>
<li>ec2-user 및 ip에는 dump 파일이 존재하는 서버 인스턴스의 유저네임과 ip를 작성한다.</li>
<li>현재 로컬 디렉토리에 dump 파일이 복사된다.</li>
</ul>
<p>로컬에 잘 복사되었다면 아래 명령어를 실행하여 덤프 파일이 필요한 서버에 다시 옮긴다.</p>
<pre><code class="language-bash">scp -i &quot;C:/Users/.ssh/현재서버.key&quot; [DUMP파일명] ubuntu@[PUBLIC IP]:/home/ubuntu/</code></pre>
<h2 id="2-pg_restore">2. pg_restore</h2>
<p>dump 파일이 준비가 되었으면, 파일을 토대로 restore를 해야한다.</p>
<p>단, postgis를 사용하고 있는 DB를 옮기는 경우 restore할 서버인스턴스에서도 postgis를 사용해야만 정상적으로 restore가 된다.</p>
<blockquote>
<p>pg_restore: error: could not execute query: ERROR: could not open extension control file .
.. No such file or directory Command was: CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA ...; 
pg_restore: error: could not execute query: 
ERROR: extension &quot;postgis&quot; does not exist Command was: COMMENT ON EXTENSION postgis IS &#39;PostGIS geometry and geography spatial types and functions&#39;;</p>
</blockquote>
<pre><code class="language-bash">sudo -i -u postgres psql -d [DB명] -c &quot;CREATE EXTENSION IF NOT EXISTS postgis;&quot;

sudo -i -u postgres psql -d [DB명] -c &quot;SELECT postgis_full_version();&quot; # 확인 쿼리</code></pre>
<p>postgis가 설치되어있지 않다면 해당 DB에 설치를 postgres 유저로 진행한다.(<strong>PostgreSQL 설치 시 자동으로 생성되는 슈퍼유저(superuser)</strong>)</p>
<pre><code class="language-bash">  pg_restore \
    -h 10.0.0.114 \
    -U nnzz_user \
    -d nnzz \
    --clean \
    --if-exists \
    --no-owner \
    --no-privileges \
    /work/[dump파일명]</code></pre>
<hr>
<h2 id="정리">정리</h2>
<h3 id="pg_dump-백업-명령어">pg_dump: 백업 명령어</h3>
<p>데이터베이스를 파일로 저장하는 명령어
기본 구조</p>
<pre><code class="language-bash">pg_dump [옵션] -d 데이터베이스명 -f 출력파일명</code></pre>
<p>주요 옵션
<strong>1. 출력 형식 (-F)</strong>
<strong>bash# 커스텀 포맷 (압축됨, 가장 많이 쓰임)</strong></p>
<pre><code class="language-bash">pg_dump -U postgres -d [db명] -Fc -f backup.dump</code></pre>
<p><strong>SQL 텍스트 포맷 (읽을 수 있음)</strong></p>
<pre><code class="language-bash">pg_dump -U postgres -d [db명] -Fp -f backup.sql</code></pre>
<p><strong>tar 포맷</strong></p>
<pre><code class="language-bash">pg_dump -U postgres -d [db명] -Ft -f backup.tar</code></pre>
<p><strong>2. 데이터만 / 스키마만</strong>
<strong>bash# 데이터만 백업 (테이블 구조 제외)</strong></p>
<pre><code class="language-bash">pg_dump -U postgres -d [db명] --data-only -f data.dump</code></pre>
<p><strong>스키마만 백업 (데이터 제외)</strong></p>
<pre><code class="language-bash">pg_dump -U postgres -d [db명] --schema-only -f schema.dump</code></pre>
<p><strong>3. 특정 테이블만</strong>
<strong>bash# users 테이블만 백업</strong></p>
<pre><code class="language-bash">pg_dump -U postgres -d [db명] -t users -f users_backup.dump</code></pre>
<p><strong>여러 테이블</strong></p>
<pre><code class="language-bash">pg_dump -U postgres -d [db명] -t users -t orders -f tables_backup.dump</code></pre>
<h3 id="pg_restore-복구-명령어">pg_restore: 복구 명령어</h3>
<p>커스텀 포맷(-Fc)으로 백업한 파일을 복구하는 명령어
기본 구조</p>
<pre><code class="language-bash">bashpg_restore [옵션] -d 데이터베이스명 백업파일</code></pre>
<p>주요 옵션
<strong>1. 기본 복구</strong>
<strong>bash# 전체 복구</strong></p>
<pre><code class="language-bash">pg_restore -U postgres -d nnzz backup.dump</code></pre>
<p><strong>2. 클린 복구 (기존 데이터 삭제 후 복구)</strong>
<strong>bash# 기존 객체 삭제 후 복구</strong></p>
<pre><code class="language-bash">pg_restore -U postgres -d nnzz --clean backup.dump</code></pre>
<p><strong>데이터베이스까지 새로 생성</strong></p>
<pre><code class="language-bash">pg_restore -U postgres -d nnzz --clean --create backup.dump</code></pre>
<p><strong>3. 특정 테이블만 복구</strong>
<strong>bash# users 테이블만 복구</strong></p>
<pre><code class="language-bash">pg_restore -U postgres -d nnzz -t users backup.dump</code></pre>
<p><strong>4. 병렬 처리 (빠른 복구)</strong>
<strong>bash# 4개의 작업을 동시에 실행</strong></p>
<pre><code class="language-bash">pg_restore -U postgres -d nnzz -j 4 backup.dump</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[OCI] 앱 서버와 DB 서버 분리 + DB 서버 로컬에 연결하기]]></title>
            <link>https://velog.io/@peace_e/OCI-%EC%95%B1-%EC%84%9C%EB%B2%84%EC%99%80-DB-%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC-DB-%EC%84%9C%EB%B2%84-%EB%A1%9C%EC%BB%AC%EC%97%90-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/OCI-%EC%95%B1-%EC%84%9C%EB%B2%84%EC%99%80-DB-%EC%84%9C%EB%B2%84-%EB%B6%84%EB%A6%AC-DB-%EC%84%9C%EB%B2%84-%EB%A1%9C%EC%BB%AC%EC%97%90-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 17 Jan 2026 12:02:14 GMT</pubDate>
            <description><![CDATA[<p>AWS에서 생성한 프로젝트의 프리티어 기간이 끝나서 OCI로 옮기기로 결정했습니다.
프로젝트를 옮기면서 겪은 일들을 기록하고자 합니다.</p>
<blockquote>
<p><a href="https://velog.io/@peace_e/SSH-config-%ED%8C%8C%EC%9D%BC-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">https://velog.io/@peace_e/SSH-config-파일-여러-개-사용하기</a>
위 내용에서 이어집니다.
OCI 계정을 발급받고 인스턴스 두 개를 만든 이후의 상황을 전제로 하고 있으며,
SSH 접속 설정을 끝내고 접속이 성공적으로 되어야 합니다.</p>
</blockquote>
<h2 id="1-기본-세팅">1. 기본 세팅</h2>
<p><strong>업그레이드 및 timezone 설정</strong></p>
<pre><code class="language-bash">sudo apt update &amp;&amp; sudo apt upgrade -y
sudo timedatectl set-timezone Asia/Seoul
</code></pre>
<p><strong>기본사항 체크하기</strong></p>
<pre><code class="language-bash">git --version
curl --version</code></pre>
<p><strong>DB서버에서 PostgreSQL 설치 + APP 서버에서 PostgreSQL 명령어만 동작 가능하게 설치</strong>
진행중인 프로젝트의 DB가 PostgreSQL이다.</p>
<pre><code class="language-bash">sudo apt install -y postgresql &lt;- db 서버
sudo apt install -y postgresql-client &lt;- app 서버
psql --version &lt;- 버전 확인용</code></pre>
<p>** db 유저 생성 및 권한 부여 **</p>
<pre><code class="language-sql">sudo -i -u postgres psql -- 로컬접속

CREATE USER ${유저명} WITH PASSWORD &#39;비밀번호&#39;; -- 유저 생성

CREATE DATABASE ${db명} OWNER ${유저명}; -- db 생성


GRANT ALL PRIVILEGES ON DATABASE ${db명} TO ${유저명}; -- 권한부여
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${유저명};
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${유저명};</code></pre>
<h2 id="2-vcn-설정-및-os-방화벽">2. VCN 설정 및 OS 방화벽</h2>
<p>OCI Ubuntu 서버는 
<strong>오라클 클라우드 방화벽(VCN)</strong>과 <strong>Ubuntu OS 내부 방화벽</strong>
두 가지를 전부 설정해야 비로소 외부에서 접근할 수 있다.</p>
<h3 id="2-1-vcn-설정">2-1. VCN 설정</h3>
<p><img src="https://velog.velcdn.com/images/peace_e/post/43b6eaa5-9c23-4366-9cd0-f096392b4cfe/image.png" alt="">
OCI는 국내에 사용하는 케이스가 많이 없어서 찾는데 애를 먹었다.
왼쪽 햄버거 메뉴 클릭 &gt; Networking &gt; <strong>Virtual cloud networks</strong>
여기서 VCN 목록이 아무것도 없다면 <code>Create VCN</code> 버튼을 클릭하여 새롭게 VCN을 만들어준다.</p>
<p>생성한 VCN을 클릭 &gt; ** Security 탭 **
<img src="https://velog.velcdn.com/images/peace_e/post/a3e86ca4-ee26-4d43-98e0-d6c19ad20625/image.png" alt=""></p>
<p>Security 탭의 <strong>Security List</strong> 및 <strong>Network Security Groups(NSG)</strong> 생성
<img src="https://velog.velcdn.com/images/peace_e/post/1cbd0c05-e3a8-49e4-8973-560367a4d7df/image.png" alt=""></p>
<p>생성했으면 각각 5432(PostgreSQL) 포트를 열어준다.
Security List
<img src="https://velog.velcdn.com/images/peace_e/post/8b3ed64b-fd34-4d6d-83f7-b00745a7ae67/image.png" alt=""></p>
<p>Network Security
<img src="https://velog.velcdn.com/images/peace_e/post/a1603a56-58ea-4734-93bc-29f9f9a7c746/image.png" alt=""></p>
<h3 id="2-2-os-방화벽">2-2. OS 방화벽</h3>
<p>OCI에서는 Ubuntu OS의 경우, UFW(Uncomplicated Firewall)을 사용하지 않을 것을 권장한다.</p>
<p><code>/etc/iptables/rules.v4</code> 파일을 수정하여 방화벽 권한을 제어할 수 있다.</p>
<blockquote>
<p><strong>Ubuntu 인스턴스가 UFW를 활성화한 이후 재부팅에 실패합니다.</strong>
...
<strong>해결 방법</strong>
방화벽 규칙을 수정하기 위해 UFW를 사용하지 마세요.
플랫폼 이미지는 인스턴스가 인스턴스의 부팅 및 블록 볼륨에 나가는 연결을 할 수 있도록 방화벽 규칙으로 미리 구성되어 있습니다.
자세한 내용은 필수 방화벽 규칙을 참조하십시오. 
UFW는 재부팅하는 동안 인스턴스가 부팅 및 블록 볼륨에 연결할 수 없도록 이러한 규칙을 제거할 수 있습니다.
방화벽 규칙을 수정하거나 새로 만들기 위해선, 대신 <strong>/etc/iptables/rules.v4</strong> 파일을 업데이트하세요.
여기에 수정된 방화벽 규칙은 재부팅 이후 적용될 것입니다. 규칙을 즉시 적용하기 위해서는 다음을 실행하세요:</p>
</blockquote>
<pre><code>$ sudo su -
# iptables-restore &lt; /etc/iptables/rules.v4</code></pre><p>nano 명령어를 사용해서 etc/iptables/rules.v4 파일을 수정했다.</p>
<pre><code class="language-bash">sudo nano /etc/iptables/rules.v4</code></pre>
<p><strong>※ 자주 쓴 명령어</strong></p>
<table>
<thead>
<tr>
<th>명령</th>
<th>단축키</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>파일 저장</td>
<td>Ctrl + O</td>
<td>파일을 저장한 후 Enter 키로 확정</td>
</tr>
<tr>
<td>편집 종료</td>
<td>Ctrl + X</td>
<td>파일 닫기. 변경 사항이 있으면 저장 여부를 물음</td>
</tr>
<tr>
<td>검색</td>
<td>Ctrl + W</td>
<td>특정 문자열 검색</td>
</tr>
<tr>
<td>잘라내기</td>
<td>Ctrl + K</td>
<td>커서가 위치한 줄을 잘라내기</td>
</tr>
<tr>
<td>붙여넣기</td>
<td>Ctrl + U</td>
<td>복사하거나 잘라낸 텍스트를 붙여넣기</td>
</tr>
</tbody></table>
<p>5432 포트를 허용하도록 설정했다.</p>
<pre><code class="language-bash">-A INPUT -s ${허용해줄 ip} -p tcp -m tcp --dport 5432 -j ACCEPT</code></pre>
<p>설정하고 즉시 적용되도록 저장한다.</p>
<pre><code class="language-bash">sudo -i &amp;&amp; iptables-restore &lt; /etc/iptables/rules.v4</code></pre>
<p>테스트를 위해 db 서버를 재시작했다.
(재시작 시 iptables 설정이 초기화 되는 경우가 있음. 제대로 저장 된 건지 확인용)</p>
<pre><code class="language-bash">sudo reboot</code></pre>
<p>방화벽 설정이 제대로 적용되지 않았다면, app 서버에서 아래 명령어를 실행했을 때 db 서버에 접근할 수가 없다.</p>
<pre><code class="language-bash">nc -zv ${db서버의 private ip} 5432</code></pre>
<p><code>No route to host</code> 또는 <code>connection refused</code>  -&gt; 연결 실패 X
<code>Connection to ... port ... succeeded!</code> -&gt; 연결 성공 O </p>
<p>succeeded 가 떴다면 app 서버에서 db 유저명과 db명을 넣어서 제대로 조회해보자.</p>
<pre><code class="language-bash">psql -h ${db서버의 private ip} -U ${db 유저명} -d ${db명}</code></pre>
<p>비밀번호를 입력하라고 뜬다면 설정해준 비밀번호를 입력하고</p>
<pre><code class="language-bash">Password for user ${db 유저명}: </code></pre>
<p>성공적으로 db 연결 시 ubuntu 버전 및 <code>Type &quot;help&quot; for help.</code>가 표시된다.</p>
<h2 id="3-로컬-dbeaver에-db-서버-연결하기">3. 로컬 DBeaver에 db 서버 연결하기</h2>
<p>Main 탭에서 DB 서버 기본 설정 
<img src="https://velog.velcdn.com/images/peace_e/post/bd94e827-fe73-406a-b05e-96e8fca9749e/image.png" alt="">
SSH 탭에서 APP 서버 설정
<img src="https://velog.velcdn.com/images/peace_e/post/cfb49811-789e-466a-be6c-575b00adb810/image.png" alt=""></p>
<p>test connection 시도 -&gt; 연결 되는 지 확인 필요</p>
<hr>
<p>참고
<a href="https://velog.io/@doubledeltas/OCI-%EC%84%9C%EB%B2%84%EA%B0%80-%EC%9E%98-%EC%97%B4%EB%A0%B8%EB%8A%94%EB%8D%B0-%EC%99%B8%EB%B6%80%EC%97%90%EC%84%9C-%EC%A0%91%EC%86%8D%EC%9D%B4-%EC%95%88%EB%90%A0-%EB%95%8C">https://velog.io/@doubledeltas/OCI-서버가-잘-열렸는데-외부에서-접속이-안될-때</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SSH] config 파일 여러 개 사용하기 + ProxyJump 사용하기]]></title>
            <link>https://velog.io/@peace_e/SSH-config-%ED%8C%8C%EC%9D%BC-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/SSH-config-%ED%8C%8C%EC%9D%BC-%EC%97%AC%EB%9F%AC-%EA%B0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 17 Jan 2026 05:53:23 GMT</pubDate>
            <description><![CDATA[<p>글 시작에 앞서, 모든 ssh key 파일에는 600 권한을 주는 것이 권장됩니다.</p>
<pre><code>chmod 600 #{키 파일 경로}</code></pre><blockquote>
<p>600 : <strong>파일 소유자만 읽기/쓰기가 가능</strong>하고, 그룹 및 다른 사용자는 접근할 수 없도록</p>
</blockquote>
<hr>
<p>이전에 사용하던 AWS 서버와, 새롭게 사용할 OCI 서버의 config 를 분리하고 싶었다.</p>
<ul>
<li>메인 config 파일의 경로는 반드시 <code>~/.ssh/config</code> 여야 한다.</li>
</ul>
<h2 id="0-디렉터리-구조">0. 디렉터리 구조</h2>
<pre><code>~/.ssh/
 ├─ config          ← 메인 (Include만 있음)
 ├─ config_aws      ← aws 서버 config
 ├─ config_oci      ← oci 서버 config
 ├─ AWS KEY/        ← aws 서버 키가 저장된 폴더
 ├─ OCI KEY/        ← oci 서버 키가 저장된 폴더
</code></pre><p>각 서버당 config 파일을 별개로 작성하고 싶어서 이런 구조를 선택하게 되었다.</p>
<h2 id="1-include-사용">1. Include 사용</h2>
<p><code>~/.ssh/config</code> 수정하기</p>
<pre><code>Include config_*</code></pre><p>해당 디렉터리에서 이름이 <code>config_</code> 로 시작하는 모든 파일을 읽으라는 명령이다.</p>
<h2 id="2-인스턴스-별-config-추가하기">2. 인스턴스 별 config 추가하기</h2>
<p><strong>2-1.</strong> <code>~/.ssh/config_aws</code>
aws 서버는 linux로 생성하여서 User가 ec2-user이다.</p>
<pre><code>Host #{설정하고싶은 호스트명 작성}
  HostName #{각 public IP}
  User ec2-user
  IdentityFile ~/.ssh/AWS\ KEY/...key ← 키 파일 경로 </code></pre><p><strong>2-2.</strong> <code>~/.ssh/config_oci</code>
oci 서버는 ubuntu로 생성하여서 User가 ubuntu이다.</p>
<pre><code>Host #{설정하고싶은 호스트명 작성}
  HostName #{각 public IP}
  User ubuntu
  IdentityFile ~/.ssh/OCI\ KEY/...key ← 키 파일 경로 </code></pre><p>경로에 띄어쓰기가 포함된 경우에는 ** \ (역슬래쉬) ** 가 들어가야 한다.
되도록이면 경로에는 한글 또는 띄어쓰기를 안 쓰는 것을 추천한다.</p>
<p><strong>2-3.</strong> <code>~/.ssh/config_oci_db</code>
오라클 클라우드는 서버 인스턴스와 db 인스턴스를 분리했다.
그래서 각 인스턴스에 접근하기 위해서는 key 파일이 별도로 필요하다.</p>
<p>서버 인스턴스에는 public ip를 붙여주었지만, 
<strong>db 인스턴스</strong>는 외부에서 접속할 필요가 없으므로 <strong>서버 인스턴스를 통해서 접근</strong>하도록 설정했다.
<strong>※ 이 때, 반드시 서버 인스턴스와 db 인스턴스의 VCN이 같아야한다.</strong></p>
<p><strong><code>ProxyJump</code>란 ?</strong></p>
<ul>
<li>중간 서버를 통해서 remote로 ssh에 연결할 수 있는 기능이다.</li>
<li>사용하면 두 번 연결을 하지 않아도 된다.</li>
<li>** 접속시 사용하는 <code>IdentityFile</code> 은 둘 다 <code>Local</code> 에 있어야 한다! ** </li>
<li><code>ProxyJump</code> 는 아래처럼 작성할 수 있다.</li>
</ul>
<pre><code>Host oci-db
  HostName #{각 public IP}
  User ubuntu
  IdentityFile ~/.ssh/OCI\ KEY/...key ← 키 파일 경로
  ProxyJump #{서버 인스턴스의 호스트명}</code></pre><p>위처럼 작성한 경우, <code>ssh oci-db</code> 로 한 번에 db 서버에 접속할 수 있다.</p>
<h2 id="3-ssh-접속하기">3. ssh 접속하기</h2>
<p>ssh + #{등록한 호스트명} 으로 간단하게 접속할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/829f6b18-fe4b-421b-8d18-b099d31fe072/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[EC2] prune 명령어 사용으로 인한 ci/cd 이슈 해결]]></title>
            <link>https://velog.io/@peace_e/EC2</link>
            <guid>https://velog.io/@peace_e/EC2</guid>
            <pubDate>Sat, 23 Aug 2025 16:41:30 GMT</pubDate>
            <description><![CDATA[<h1 id="error-상황">ERROR 상황</h1>
<p>사플 서버를 재부팅하면서 기존의 yml파일을 수정했었다.
그 이후로 3달 만에 새롭게 프로젝트를 커밋했더니 ci/cd에 실패했다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/01331327-fcb2-4a68-b7a7-604f02f07c83/image.png" alt="">
원래 길어봐야 2분 안으로 완료되던 ci/cd가 10분이 넘어도 완료되지 못했다.
SSH Action 기본 타임아웃은 10분이기에 Pull이 10분 넘게 오래 걸리면 Action이 실패하는 것이다.</p>
<h1 id="원인">원인</h1>
<p>yml 파일을 수정하면서 CD 부분에 아래를 추가했었다.</p>
<pre><code class="language-bash">sudo docker system prune -f</code></pre>
<h2 id="prune-란">prune 란?</h2>
<p><code>prune</code>은 사용하지 않는 리소스를 삭제하는 명령어이다.</p>
<p><strong>사용하지 않는 리소스의 기준</strong></p>
<ul>
<li>모든 멈춰있는 컨테이너</li>
<li>최소 한 개의 컨테이너에서 사용되지 않는 네트워크</li>
<li>dangling(매달려있지 않은) 이미지</li>
<li>dangling 빌드 캐시</li>
</ul>
<p><strong>prune은 볼륨을 삭제하지 않는다!</strong></p>
<pre><code class="language-bash">sudo docker system prune</code></pre>
<p><code>prune</code> 명령어를 사용하게 되면 기본적으로 진짜 정리할거냐는 경고 메세지가 뜬다. 그만큼 prune 명령어는 조심스럽게 사용해야한다.
그럼에도 삭제하길 원한다면 <strong>y</strong>를 입력하면 삭제가 진행된다.</p>
<p>※ <code>-f</code>를 붙이게 되면 확인 메세지 없이 강제로 실행하겠다는 뜻이다.
※ <code>-af</code>를 붙이게 되면 <code>-a</code> = <code>-all</code> dangling 이미지 뿐만 아니라 태그가 붙었지만 현재 어떤 컨테이너에서도 사용되지 않는 모든 이미지를 확인 메세지 없이 강제로 실행한다.</p>
<h1 id="해결-방안">해결 방안</h1>
<p>사용하지 않는 리소스들을 ci/cd 때마다 관리하면 좋을 것 같아서 추가한 부분이었다. <code>prune</code>은 그대로 유지를 하고 싶어서 이를 <code>nohup</code>을 사용하여 백그라운드로 돌리는 방향으로 수정하였다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/b2f74675-62af-440c-969b-18d53ccdd149/image.png" alt=""></p>
<p>yml 파일 수정 후 ci/cd가 정상적으로 이루어진 모습이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[pycharm 에서 github 연동 해제하기]]></title>
            <link>https://velog.io/@peace_e/pycharm-%EC%97%90%EC%84%9C-github-%EC%97%B0%EB%8F%99-%ED%95%B4%EC%A0%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/pycharm-%EC%97%90%EC%84%9C-github-%EC%97%B0%EB%8F%99-%ED%95%B4%EC%A0%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 20 Jul 2025 10:57:44 GMT</pubDate>
            <description><![CDATA[<p>로컬에서 작업 중인 프로젝트를 github에 연동시켜놓았다.
그런데 여러가지 레포지토리에 연결하다보니 관리하기가 번거로워져서 연동을 해제하기로 했다.</p>
<h2 id="1-터미널에서-연동-중인-repo-확인">1. 터미널에서 연동 중인 repo 확인</h2>
<pre><code class="language-bash">git remote -v</code></pre>
<p>pycharm 내에 터미널을 열어서 위 명령어를 입력한다.
연동 중인 repo의 alias를 확인한다.</p>
<h2 id="2-연동-중인-repo를-해제">2. 연동 중인 repo를 해제</h2>
<pre><code class="language-bash">git remote remove 레포지토리명</code></pre>
<p>레포지토리명에 아까 확인한 alias를 넣어주면 된다.
위처럼 명령어를 입력하여 연동 중인 레포지토리를 해제한다.</p>
<h2 id="3-연동-해제되었는지-다시-확인">3. 연동 해제되었는지 다시 확인</h2>
<pre><code class="language-bash">git remote -v</code></pre>
<p>연동 중인 repo를 다시 확인한다.
아래처럼 해제하지 않은 레포지토리만 보인다면 정상적으로 연동 해제된 것이다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/0c61fc84-bdc2-433d-81eb-e0b1c7d10d85/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[EC2] 서버 재부팅 시 docker restart 하는 법]]></title>
            <link>https://velog.io/@peace_e/EC2-%EC%84%9C%EB%B2%84-%EC%9E%AC%EB%B6%80%ED%8C%85-%EC%8B%9C-docker-restart-%ED%95%98%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@peace_e/EC2-%EC%84%9C%EB%B2%84-%EC%9E%AC%EB%B6%80%ED%8C%85-%EC%8B%9C-docker-restart-%ED%95%98%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Fri, 30 May 2025 15:07:32 GMT</pubDate>
            <description><![CDATA[<p>EC2 서버가 뻗어서 재부팅했다
EC2에 엮여있는 RDS도 DBeaver에서 로딩이 안 돼서 재부팅할 수 밖에 없었다..
(안 하고 싶었지만...)</p>
<h2 id="error-상황">ERROR 상황</h2>
<p>서버를 재부팅했더니 잘 되던 api들이 <strong>502 Bad Gateway</strong>가 발생했다.
502는 다른 서버로부터 유효한 응답을 받지 못했을 때, 발생한다.</p>
<p>github actions 와 docker를 사용해서 올려놨었는데, 컨테이너가 중단되어 있는게 아닐까 싶어서 아래의 명령어로 확인해보았다.</p>
<pre><code class="language-bash">$ sudo docker ps -a</code></pre>
<p><strong>8일 전에 만들어진 컨테이너가 Exited 상태인 것을 확인할 수 있었다.</strong></p>
<h2 id="컨테이너-재시작하기">컨테이너 재시작하기</h2>
<pre><code class="language-bash">$ sudo docker restart {컨테이너 id}</code></pre>
<p>restart 명령어를 사용하여 컨테이너를 재시작시켰다.</p>
<p>api가 정상적으로 작동되는 것을 확인했으나, CI/CD가 제대로 이루어지지 않았다.
CI 단계에서 부터 문제가 생겨서 무한 로딩상태였다.</p>
<h2 id="트러블-슈팅gradle-관련">트러블 슈팅(Gradle 관련)</h2>
<p>로그를 확인해보니 <strong>Gradle Setup 단계에서 “The operation was canceled”</strong> 에러가 발생했다.</p>
<blockquote>
<p>Run gradle/actions/setup-gradle@v3
...
Restore Gradle state from cache
Error: The operation was canceled.</p>
</blockquote>
<p>Setup Gradle이라는 이름의 액션에 
<strong>gradle/actions/setup-gradle@v3</strong> 을 사용하고 있었는데, 
<strong>gradle을 직접 커맨드라인에서 실행하도록 수정</strong>했다. 아래처럼 순서대로 명령어만 사용했다.</p>
<pre><code class="language-bash">- name: Make gradlew executable
  run: chmod +x ./gradlew

- name: Build with Gradle
  run: ./gradlew clean build --stacktrace</code></pre>
<p>이 부분 때문에 고민을 많이 했는데, GPT에게 물어봐서 해결했다..</p>
<p>※ <strong>gradle/actions/setup-gradle</strong> </p>
<ul>
<li>Gradle 환경 세팅과 캐싱을 자동으로 해준다.</li>
<li>그러나, 내부에서 네트워크 문제, 캐시 복원 문제, 혹은 환경 문제가 생기면 실패할 수 있다. </li>
</ul>
<p>※ <strong>./gradlew를 직접 실행</strong></p>
<ul>
<li>Gradle Wrapper가 이미 repo 안에 포함되어 있다고 가정하기 때문에,</li>
<li>별도의 환경 설정 없이도 gradle 빌드가 가능하고,</li>
<li>네트워크나 캐시 이슈로부터 자유롭고,</li>
<li>단순 명령 실행이라 에러 발생 지점이 명확하다</li>
</ul>
<p>그래서 yml 파일을 이처럼 수정하였고, 이후 push를 진행했을 때 CI/CD가 정상적으로 진행되었다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/1761e917-350b-46f0-95ca-937a6a40d8d7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PostGIS] SHP 파일 import 하기]]></title>
            <link>https://velog.io/@peace_e/PostGIS-SHP-%ED%8C%8C%EC%9D%BC-import-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/PostGIS-SHP-%ED%8C%8C%EC%9D%BC-import-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 31 Mar 2025 16:39:25 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하면서 SHP 파일을 import 한 과정을 기록한다.
DBeaver 를 DB 툴로 사용하고 있는 상황이었는데, DBeaver로는 .csv 파일 불러오듯이 바로 .shp 파일을 불러올 수가 없다. postGIS나 qGIS 등을 이용해야 한다. 나는 postGIS를 사용해서 파일을 import 했다.</p>
<h1 id="0-shp-파일-구하기">0. shp 파일 구하기</h1>
<p>지역정보를 shp 파일은 여러 곳에서 구할 수 있다.
필자는 <a href="https://www.geoservice.co.kr/">GEOSERVICE_WEB</a> 을 사용했다.
회원가입하면 코인이 충전되고 코인으로 shp 파일을 구매할 수 있다.
지역구 파일이 필요해서 시/군/구 파일을 다운로드 받았다.
자세한 사용법은 제작자의 블로그를 참고하자</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/5390914e-5bea-48d1-a24f-46b245eaa406/image.png" alt=""></p>
<h1 id="1-postgis-설치">1. postGIS 설치</h1>
<p>shp 데이터베이스가 필요한 건 로컬이 아니고, AWS 에 올라가있는 서버였다. SSH로 연결하는 방식인데 postGIS나 qGIS에서 서버를 연결하는 방법을 모르겠어서 로컬에 일단 올리고, DBeaver를 통해서 DDL과 데이터베이스 내용을 서버로 가져오기로 했다. </p>
<p>그러려면 일단 로컬에 postGIS가 설치되어있어야 한다.
로컬에 postgreSQL 17버전이 세팅되어있었는데, postgis 익스텐션이 설치가 안되어있어서 postgis 설치만 다시 진행하려고 했다.
그런데 17 버전에서 postgis 익스텐션을 다운로드하려고 하면 계속 응답없음이 뜨길래 버전을 다운그레이드 하여 진행했다.</p>
<p>기존에 있던 17버전을 싹 지우고 16.7버전을 설치하니, postgis 익스텐션이 다운로드가 잘 되었다. </p>
<h1 id="2-pgadmin">2. pgAdmin</h1>
<p>이 과정은 생략해도 된다.
다들 pgAdmin을 쓰던데 한 번도 안써봐서 사용해봤다.
어차피 shp 파일을 변환해서 넣는 부분은 postGIS로 진행할 거라, 여기서는 새로운 서버를 연결하기만 할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/cdd62611-20f0-4b7a-b060-cfeee89e3d46/image.png" alt=""></p>
<p>Servers -&gt; Register -&gt; server 순으로 진행하여 서버를 등록해준다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/a3e09179-6f78-4b8f-b47d-f7056479a501/image.png" alt=""></p>
<p>서버를 등록할 때 필요한 정보는 아래와 같다.</p>
<ul>
<li>Name : 서버 이름</li>
<li>Server group : 서버 그룹 설정</li>
<li>Backgroud, Foregroupd : 색상 지정</li>
<li>Connect Now : 설정 후 PostgreSQL에 접속</li>
<li>Comments : 서버 설명</li>
</ul>
<p><img src="https://velog.velcdn.com/images/peace_e/post/e7e0ac9f-20d9-4b29-813f-c6a0c28acd10/image.png" alt=""></p>
<ul>
<li>Host name/address : PostgreSQL 서버의 주소</li>
<li>Port : 포트 값 (postgreSQL 기본값 : 5432) </li>
<li>Maintenance databse : 접속할 Database명</li>
<li>Username : 사용자 이름</li>
<li>Password : PostgreSQL 비밀번호</li>
</ul>
<p>제대로 서버를 연결했다면 왼쪽에 이렇게 추가된다.
테이블은 Schemas &gt; public 안에 있다.
<img src="https://velog.velcdn.com/images/peace_e/post/1de1ecce-bf8a-4f76-9925-55624051040d/image.png" alt=""></p>
<p><strong>※ 참고</strong>
<img src="https://velog.velcdn.com/images/peace_e/post/b93c7b6d-3e6b-4243-8b9e-ce8988b12df9/image.png" alt="">
이미 postgis 익스텐션이 실행된 상태에서 해당 쿼리를 작성하면 에러가 발생한다.
postgis가 가능한 상태이면 spatial_ref_sys 테이블이 이미 생성되어 있을 것이다. </p>
<h1 id="3-postgis">3. postGIS</h1>
<p><img src="https://velog.velcdn.com/images/peace_e/post/e5d56567-9905-42d9-92a0-ba39cc75a58d/image.png" alt="">
VIEW CONNECTION DETAILS을 클릭하여 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/14f85134-1195-48bb-8493-80a2f1a4a7f5/image.png" alt="">
pgAdmin에서 만든 새 서버를 넣었다. shp 파일이 필요한 서버정보를 작성하여 연결한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/6476df1a-a6ac-435b-89ac-4df6fe8d2f8c/image.png" alt="">
연결이 완료되면 이렇게 뜰 것이다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/02f6e9fe-db0f-46c0-b7ef-3ed781ad9673/image.png" alt="">
shp 파일에 한국어가 포함되어있다면 Options에서 UTF-8을 EUC-KR로 바꿔준다.</p>
<p>Options 설정이 끝났다면 add file을 클릭하여 파일을 추가하고 import를 클릭한다.</p>
<p><strong>※ 참고</strong>
<strong>dbf file (.dbf) can not be opened. Shapefile import failed.</strong>
에러가 발생하면 파일의 경로 중 한글이 포함되어 있는 경로가 있을 수 있다.
필자도 이 에러를 겪었고, 이 경우에는 바탕화면/C 로 파일들을 빼서 해결했다.</p>
<p><strong>SRID(공간 참조 식별자, Spatial Reference Identifier)는 필요시 설정해주는 것이 좋다.</strong>
사용한 shp 파일은 5179 좌표계를 사용하고 있다. 그래서 5179 좌표계로 설정하여 import 하였다.</p>
<h1 id="4-dbeaver">4. DBeaver</h1>
<p>import 가 정상적으로 완료되었다면 DBeaver를 비롯한 DB 툴에서도 테이블이 조회될 것이다. (DBeaver가 익숙해서 사용했다. 본인에게 맞는 툴을 사용하면 된다.)</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/f842d152-b818-4fdf-b06a-720489589abf/image.png" alt="">
gid : 지역구 인덱스
sig_cd : 지역구 아이디
sig_eng_nm : 지역구 영문명
sig_kor_nm : 지역구 한글명
geom : 지역구 범위 (Multipolygon, 5179)</p>
<p>제대로 테이블이 들어왔음을 확인했으면, 값을 실 사용하기 전에 중요한 과정이 남았다. 이 테이블은 5179좌표계를 사용하고 있으므로 필요한 좌표계가 다르다면 그에 맞춰 수정을 해줘야한다.</p>
<p>진행중인 프로젝트는 4326 좌표계를 필요로 했다. 좌표계 변경에 앞서 아래의 쿼리로 테스트를 했다.</p>
<pre><code class="language-sql">SELECT ST_AsText(ST_Transform(ST_PointOnSurface(geom), 4326)) as transformed_coord,
       ST_AsText(ST_PointOnSurface(geom)) as original_coord
FROM sig_5179 LIMIT 1;</code></pre>
<p><img src="https://velog.velcdn.com/images/peace_e/post/96ea319a-b82d-413e-8ddb-e5096a1f5571/image.png" alt=""></p>
<blockquote>
<p><strong>ST_PointOnSurface</strong> : 범위의 중앙점을 반환한다.
<strong>ST_AsText</strong> : 지오메트리 값을 매개변수로 받으면 텍스트로 반환한다.
<strong>ST_Transform(좌표, 좌표계)</strong> : 지오메트리 값을 특정 좌표계로 변환한다. </p>
</blockquote>
<p>실행 결과가 이렇게 나왔다. 얼추 맞게 변환된듯 하다. </p>
<p>ST_Transform() 함수가 제대로 동작하는 걸 확인했으니 원래 테이블을 복사해서 4326 좌표계로 변환할 것이다.</p>
<pre><code class="language-sql">-- 1. 기존 테이블 이름 변경 (백업용)
ALTER TABLE public.sig RENAME TO sig_5179;

-- 2. 새로운 테이블 생성 (EPSG:4326 좌표계 사용)
CREATE TABLE public.sig (
    gid serial4 NOT NULL,
    sig_cd varchar(5) NULL,
    sig_eng_nm varchar(40) NULL,
    sig_kor_nm varchar(40) NULL,
    geom public.geometry(multipolygon, 4326) NULL,
    CONSTRAINT sig_pkey2 PRIMARY KEY (gid)
);

-- 3. 데이터 변환하여 복사
INSERT INTO public.sig (sig_cd, sig_eng_nm, sig_kor_nm, geom)
SELECT sig_cd, sig_eng_nm, sig_kor_nm, ST_Transform(geom, 4326)
FROM sig_5179;

-- 4. 공간 인덱스 생성
CREATE INDEX sig_geom_idx2 ON public.sig USING gist (geom);</code></pre>
<ol>
<li>기존 테이블 이름을 5179로 변경하고 4326좌표계를 사용하는 새로운 테이블을 생성했다.(구조는 같음)</li>
<li>그리고 기존 테이블의 데이터를 ST_Transform() 함수를 사용해서 4326 좌표계로 변환하여 그대로 복사했다.</li>
<li>공간 인덱스도 빠트리지 않고 똑같이 생성해주었다.</li>
</ol>
<p><strong>※ 참고</strong>
Multipolygon 타입은 이런 식으로 값이 들어가는데 굉장히 길기 때문에 DBeaver에서 컬럼값을 전부 명시해주는 INSERT 문을 복사해서 사용하지 않길 바란다. 무조건 렉이 걸린다.</p>
<pre><code>MULTIPOLYGON (((956615.4532424484 1953567.1989686124, 956621.5787365452 1953565.2711694315, .... )))
</code></pre><h1 id="5-지역구-계산하는-쿼리-작성">5. 지역구 계산하는 쿼리 작성</h1>
<p>유저의 위치가 특정 지역구에 속하는 경우에만 서비스 이용이 가능하게끔 제한해야했다. 그래서 쿼리를 아래와 같이 작성했다.</p>
<pre><code class="language-sql">SELECT EXISTS (
    SELECT 1
    FROM sig
    WHERE (sig_cd = &#39;11650&#39; or sig_cd = &#39;11680&#39; or sig_cd = &#39;11710&#39; or sig_cd = &#39;11740&#39;)
    and ST_Contains(geom, ST_SetSRID(ST_MakePoint(127.02800140627488, 37.49808633653005), 4326))
);</code></pre>
<blockquote>
<p><strong>ST_MakePoint</strong> : x좌표, y좌표를 매개변수로 넣으면 공간 데이터로 반환한다.
<strong>ST_SetSRID</strong> : 공간데이터에 원하는 좌표계값을 넣어준다.
<strong>ST_Contains</strong> : 한 지오메트리가 다른 지오메트리에 완전히 포함되는지를 확인한다.</p>
</blockquote>
<p>sig_cd값에 원하는 지역구를 넣었다.
필요한 sig_cd 조건을 or 로 연결하고 있으므로 괄호를 작성해주어야 한다. 그렇지 않으면 and 가 먼저 실행되어 다른 값이 나오게 되므로 유의하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] EC2 서버에 Spring boot 파일 배포하기 + Github Actions와 Docker로 자동화하기]]></title>
            <link>https://velog.io/@peace_e/AWS-EC2-%EC%84%9C%EB%B2%84%EC%97%90-Spring-boot-%ED%8C%8C%EC%9D%BC-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Github-Actions%EC%99%80-Docker%EB%A1%9C-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/AWS-EC2-%EC%84%9C%EB%B2%84%EC%97%90-Spring-boot-%ED%8C%8C%EC%9D%BC-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0-Github-Actions%EC%99%80-Docker%EB%A1%9C-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 18 Dec 2024 18:21:33 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@peace_e/AWS-%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4%EB%A1%9C-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1-RDS-%EC%97%B0%EA%B2%B0-%EB%B0%8F-SSH%EB%A1%9C-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0">[AWS] 프리티어로 EC2 인스턴스 생성 및 SSH로 접속하기 / EC2와 RDS 연결하기</a></p>
<p>지난 글에서 AWS EC2 서버를 만들고, RDS 와 연결하는 과정까지 진행했다.
개발 중인 Spring boot 프로젝트를 EC2 서버에서 실행시켜보자.</p>
<h2 id="filezila로-jar-파일-직접-전달하기">FileZila로 .jar 파일 직접 전달하기</h2>
<p>서버에서 프로젝트를 직접 배포하는 것은 크게 두 가지가 있다.</p>
<ol>
<li>서버에서 git 레포지토리를 clone 하여 build 하는 방법</li>
<li>서버에 jar파일을 직접 전달하는 방법</li>
</ol>
<p>나는 이미 FileZila를 쓰고 있었기 때문에, jar 파일을 직접 전달하는 방법을 선택했다.</p>
<blockquote>
<p><strong>FileZila</strong>
사용자의 PC와 호스팅 서버 간 파일 송수신을 위한 위한 FTP(File Transfer Protocol) 소프트웨어</p>
</blockquote>
<p>FileZila Client 는 여기서 다운받을 수 있다.
<a href="https://filezilla-project.org/download.php?type=client">https://filezilla-project.org/download.php?type=client</a></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/a9b45a0b-1f22-4d13-9b39-da19273ab674/image.png" alt=""></p>
<p>다운받은 FileZila를 실행하고 버튼을 클릭해서 EC2 서버와 연결한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/2e4e56d9-4546-4c2e-8714-d1ed18de02f4/image.png" alt=""></p>
<ol>
<li>프로토콜 : SFTP - SSH File Transfer Protocol 선택</li>
<li>호스트 : EC2의 퍼블릭 IPv4 주소 입력</li>
<li>포트 : 비워도 된다</li>
<li>로그온 유형 : 키 파일 선택</li>
<li>사용자 : 사용자 이름 입력 (linux 서버의 경우 : ec2-user, ubuntu 서버의 경우 : ubuntu)</li>
<li>키파일 : 앞서 저장한 .pem 키 파일이 저장된 경로를 선택</li>
<li>연결 선택</li>
</ol>
<p>정상적으로 연결이 되었다면 <strong>왼쪽엔 로컬PC 화면</strong>이, <strong>오른쪽에는 FTP 서버</strong>가 보일 것이다. </p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/9312602a-53c5-4c38-a024-c99a75f489a2/image.png" alt=""></p>
<ol>
<li>Spring boot에서 파일을 빌드 한 후, libs 폴더의 빌드된 jar 파일을 더블클릭한다. (서버의 저장경로로 전송)</li>
<li>전송이 완료되면 오른쪽 FTP 서버 화면에서도 jar 파일이 보인다.</li>
</ol>
<p>서버에 jar 파일을 제대로 전송해주었다면, 아래처럼 입력해서 jar 파일을 실행한다.</p>
<pre><code class="language-bash">nohup java -jar &lt;프로젝트 이름&gt;-0.0.1-SNAPSHOT.jar &amp;
</code></pre>
<p><code>nohup</code> 은 백그라운드에서 무중단으로 실행하기 위해 사용한다. 마지막에 <code>&amp;</code> 도 붙여주어야 한다.
<code>nohup</code>을 사용하지 않으면 bash 창을 닫고 종료할때마다 어플리케이션도 같이 종료된다.</p>
<p>이미 실행중인 어플리케이션을 중지시켜야한다면 아래처럼 입력한다.</p>
<pre><code class="language-bash">ps -ef | grep .jar # 실행중인 .jar 파일을 조회
kill &lt;pid&gt; # 조회한 pid를 입력</code></pre>
<p>이 방법으로 계속 사용했는데, 변경 사항이 생길때마다 종료하고 새로 실행시키는 과정이 매우 번거로웠다. 그리고 github push는 별개로 해주어야하기에 불편했다.</p>
<p>그래서 Docker와 Github Actions를 활용해서 CI/CD 를 구축하기로 했다.</p>
<h2 id="docker와-github-actions로-배포하기">Docker와 Github Actions로 배포하기</h2>
<p>도커를 활용한 배포 흐름은 아래와 같다.</p>
<ol>
<li>Dockerfile을 작성하여 build 시 docker image가 생성된다.</li>
<li>생성된 docker image를 docker hub에 push 한다.</li>
<li>서버(ec2)에 docker hub의 docker image를 pull 한다.</li>
<li>docker image를 실행시켜 docker container를 생성한다.(어플리케이션은 container 상에서 실행된다.)</li>
</ol>
<p>이 과정을 자동화 할 것이고, CI/CD 파이프라인 구축에는 Github Actions를 사용할 것이다.</p>
<blockquote>
<p><strong>Github Actions</strong>
Github 저장소를 기반으로 소프트웨어 개발 Workflow를 자동화할 수 있는 도구. 특정한 브랜치에 대한 이벤트(트리거)를 통해 설정된 Workflow를 따라, 빌드, 테스트, 배포 등의 다양한 동작을 자동으로 실행되게 할 수 있다.</p>
</blockquote>
<h3 id="github-actions의-secrets-관리">Github Actions의 Secrets 관리</h3>
<p>프로젝트 레포의 <code>Settings</code>-<code>Security</code>-<code>Secrets and Variables</code>-<code>Actions</code> 경로에서 보안상의 이유로 Github에 올릴 수 없는 환경 변수들을 작성해준다.
<img src="https://velog.velcdn.com/images/peace_e/post/6ba38c3c-c14f-49c8-b96a-59a9ad86c77e/image.png" alt=""></p>
<p>등록한 환경변수들은 값을 볼 수 없고, 수정 또는 삭제만 가능하다.</p>
<ol>
<li>APPLICATION : github에 올라가지 않은 application.properties 의 내용</li>
<li>AWS_EC2_HOST : EC2 인스턴스의 PUBLIC IP</li>
<li>AWS_EC2_KEY : 발급받은 .pem 키 파일</li>
<li>DOCKER_USERNAME : Docker Hub 로그인 시 사용하는 username </li>
<li>DOCKER_PASSWORD : Docker Hub 로그인 시 사용하는 password</li>
</ol>
<p>Docker Hub 로그인 시 구글 로그인으로 가입하면 username과 password 만으로는 로그인이 불가능하다. 토큰 방식을 사용해야만 로그인이 가능한 듯 하다.</p>
<h3 id="workflow-파일-생성">Workflow 파일 생성</h3>
<p>프로젝트 레포의 <code>Actions</code>-<code>New Workflow</code>를 통해 workflow 파일 작성을 위한 양식을 선택할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/6f91b0b9-3efd-4eb1-b4e6-507de5fc8391/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/da392814-a7ad-45ec-bf3d-a1a08a1dbc11/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/a02dd246-340e-464b-a2b0-929777a73370/image.png" alt=""></p>
<p><code>Java with Gradle</code>을 선택하면 Java-Gradle 환경에서 사용할 수 있는 Workflow 파일의 기본 양식을 보여준다. 이걸로 생성해도 되고, 직접 .github &gt; workflows &gt; 폴더 안에 workflow를 작성해도 된다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/b1bfc4c8-6e25-49b3-83d4-d33c3b11e5e7/image.png" alt=""></p>
<h3 id="workflow-파일-작성">Workflow 파일 작성</h3>
<p>아래처럼 작성했다.</p>
<pre><code class="language-yml"># Github Actions 에 표시되는 Workflow 이름
name: NNZZ CI/CD

# master 브랜치로 push, pull request 가 발생하면 workflow 를 실행
on:
  push:
    branches: [ &quot;master&quot; ]
  pull_request:
    branches: [ &quot;master&quot; ]

permissions:
  contents: read

# workflow 에서 실행할 동작
jobs:
  CI:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      # JDK 세팅
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: &#39;17&#39;
          distribution: &#39;temurin&#39;

      # gradle caching - 빌드 시간 향상
      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles(&#39;**/*.gradle*&#39;, &#39;**/gradle-wrapper.properties&#39;) }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      # application.properties 파일 생성
      - name: Make application.properties
        run: mkdir ./src/main/resources |
          touch ./src/main/resources/application.properties

      - name: Deliver application.properties
        run: echo &quot;${{ secrets.APPLICATION }}&quot; &gt; ./src/main/resources/application.properties

      # Gradle 설정 및 Build
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Change gradlew permissions
        run: chmod +x ./gradlew

      - name: Build with Gradle Wrapper
        run: ./gradlew clean build

      - name: List build/libs contents
        run: ls build/libs


      # Docker 로그인
      - name: Docker Login
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Docker 이미지 생성 및 Push
      - name: Docker build &amp; Push
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz .
          docker push ${{ secrets.DOCKER_USERNAME }}/nnzz


  CD:
    needs: CI
    runs-on: ubuntu-latest

    steps:
      # EC2 접근 후 docker 이미지 pull &amp; run
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.AWS_EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.AWS_EC2_KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker stop $(sudo docker ps -qa)
            sudo docker rm $(sudo docker ps -qa)
            sudo docker run -d -p 8080:8080 \
            -v /etc/localtime:/etc/localtime:ro \
            -e TZ=Asia/Seoul \
            ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker system prune -f
</code></pre>
<p>하나씩 살펴보자.</p>
<pre><code class="language-yml"># Github Actions 에 표시되는 Workflow 이름
name: NNZZ CI/CD

# master 브랜치로 push, pull request 가 발생하면 workflow 를 실행
on:
  push:
    branches: [ &quot;master&quot; ]
  pull_request:
    branches: [ &quot;master&quot; ]

permissions:
  contents: read</code></pre>
<ul>
<li><strong>name</strong> : Github Actions에 표시되는 Workflow 이름이다. 자유롭게 설정 가능하다.</li>
<li><strong>on</strong> : Workflow 의 트리거가 될 이벤트를 설정한다. master 브랜치로 push, pull request가 발생하면 workflow를 실행하도록 작성했다.</li>
<li><strong>permissions</strong>: Workflow 작업에 적용되는 권한을 설정한다. 권한에 대한 자세한 사항은 <a href="https://docs.github.com/ko/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token">공식문서</a> 참조</li>
</ul>
<pre><code class="language-yml">jobs:
  CI:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      # JDK 세팅
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: &#39;17&#39;
          distribution: &#39;temurin&#39;

      # gradle caching - 빌드 시간 향상
      - name: Gradle Caching
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles(&#39;**/*.gradle*&#39;, &#39;**/gradle-wrapper.properties&#39;) }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      # application.properties 파일 생성
      - name: Make application.properties
        run: mkdir ./src/main/resources |
          touch ./src/main/resources/application.properties

      - name: Deliver application.properties
        run: echo &quot;${{ secrets.APPLICATION }}&quot; &gt; ./src/main/resources/application.properties</code></pre>
<ul>
<li><p><strong>jobs</strong> : job은 Workflow의 실행 단위이다. 각 job은 다시 step으로 순차적인 실행 단계가 구분된다. CI와 CD로 job을 나누어 작성했다. (job 과 step의 이름은 자유롭게 설정 가능) </p>
</li>
<li><p><strong>runs-on</strong> : Workflow를 실제로 실행하는 서버를 설정한다. GitHub-hosted runner(Github에서 자체적으로 제공)/ Self-hosted runner(사용자가 직접 설정하여 사용) 으로 나뉜다. 여기서는 GitHub-hosted runner를 사용하였다.</p>
</li>
<li><p><strong>Set up JDK 17</strong> : 프로젝트에서 사용하는 것과 동일한 JDK 17을 사용하도록 설정하였다.</p>
</li>
<li><p><strong>Gradle Caching</strong> : 빌드 시간 향상을 위해 Gradle을 캐싱하도록 했다. 작성하지 않아도 Workflow를 실행하는데 문제는 없다.</p>
</li>
<li><p><strong>Make application.properties</strong> : 프로젝트 레포에서 제외된 application.properties 을 생성한다.</p>
</li>
<li><p><strong>Deliver application.properties</strong> : 생성된 application.properties에 Secrets에 등록한 APPLICATION의 값을 덮어씌운다.</p>
</li>
</ul>
<pre><code class="language-yml">      # Gradle 설정 및 Build
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

      - name: Change gradlew permissions
        run: chmod +x ./gradlew

      - name: Build with Gradle Wrapper
        run: ./gradlew clean build

      - name: List build/libs contents
        run: ls build/libs


      # Docker 로그인
      - name: Docker Login
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Docker 이미지 생성 및 Push
      - name: Docker build &amp; Push
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz .
          docker push ${{ secrets.DOCKER_USERNAME }}/nnzz</code></pre>
<ul>
<li><strong>Setup Gradle ~</strong> : Gradle을 설정하고 권한 변경 후 프로젝트를 clean build 한다.</li>
<li><strong>Docker Login</strong> : Secrets에 등록된 DOCKER_USERNAME과 DOCKER_PASSWORD로 Docker에 로그인한다.</li>
<li><strong>Docker build &amp; Push</strong> : Docker Image를 build 하여 생성 후 Docker Hub에 프로젝트를 push한다.</li>
</ul>
<pre><code class="language-yml">  CD:
    needs: CI
    runs-on: ubuntu-latest

    steps:
      # EC2 접근 후 docker 이미지 pull &amp; run
      - name: Deploy to EC2 Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.AWS_EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.AWS_EC2_KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker stop $(sudo docker ps -qa)
            sudo docker rm $(sudo docker ps -qa)
            sudo docker run -d -p 8080:8080 \
            -v /etc/localtime:/etc/localtime:ro \
            -e TZ=Asia/Seoul \
            ${{ secrets.DOCKER_USERNAME }}/nnzz
            sudo docker system prune -f</code></pre>
<ul>
<li><strong>needs</strong> : 적어둔 작업(CI)이 완료된 후 실행된다.</li>
<li><strong>Deploy to EC2 Server</strong> : EC2 서버에서 Docker Image를 pull하여 실행하는 작업을 수행한다.</li>
<li><strong>uses</strong> : 원격 접속을 위해 appleboy 를 사용한다.</li>
<li><strong>script</strong> : Docker Hub에서 최신 Image를 pull 한 후 실행 중인 컨테이너를 중지, 삭제한다.
타임존을 한국시간대로 설정하여 image를 run하여 컨테이너를 실행시킨다.
마지막으로 불필요한 오래된 image는 삭제한다.</li>
</ul>
<h3 id="docker-설치">Docker 설치</h3>
<p><strong>윈도우 10 홈 에디션 기준</strong></p>
<ol>
<li><p>제어판 &gt; 프로그램 &gt; 프로그램 및 기능 &gt; Windows 기능 켜기/끄기 버튼을 클릭한다.</p>
</li>
<li><p><strong>Linux 용 Windows 하위 시스템, 가상 머신 플랫폼</strong>에 체크하고 확인 버튼을 클릭한다. -&gt; 체크한 기능을 활성화하려면 컴퓨터를 다시 시작해야한다.</p>
</li>
<li><p><a href="https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi">https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi</a>
에 접속하여 리눅스 커널을 다운받는다. 다운로드 받은 파일을 실행하여 리눅스 커널을 업데이트 할 수 있다. </p>
</li>
<li><p><a href="https://docs.docker.com/desktop/install/windows-install/">https://docs.docker.com/desktop/install/windows-install/</a>
윈도우용 도커 데스크톱을 다운받는다. </p>
</li>
<li><p>설치 중, Configuration 화면에서 모든 항목에 체크한 다음 OK 버튼을 진행한다.</p>
</li>
<li><p>설치가 완료되면 Close and log out 버튼을 클릭해 윈도우에 다시 로그인한다. (컴퓨터가 재부팅된다.)</p>
</li>
<li><p>바탕화면에 Docker Desktop이 추가되어있다.</p>
</li>
</ol>
<h3 id="ec2에서-docker-설치">EC2에서 Docker 설치</h3>
<pre><code class="language-bash"># 도커 설치
sudo yum install docker -y

# 도커 서비스 실행
sudo service docker start

# /var/run/docker.sock 파일 권한 변경
sudo chmod 666 /var/run/docker.sock

# ec2-user를 docker 그룹에 추가, sudo 명령어 없이 docker 사용가능
sudo usermod -a -G docker ec2-user
</code></pre>
<h3 id="docker-hub-레포지토리-생성">Docker Hub 레포지토리 생성</h3>
<p><code>Docker Hub</code>에 docker image를 push할 레포지토리를 생성한다. public으로 생성하자. (private의 경우 계정 당 1개까지만 무료로 생성이 가능하다.)</p>
<h3 id="dockerfile-생성">Dockerfile 생성</h3>
<p>프로젝트 최상위 경로 아래에 Dockerfile을 생성한다. (※ src 아래에 생성하면 안 된다!)
<img src="https://velog.velcdn.com/images/peace_e/post/7546f71e-f69c-4552-8d1a-a215b00ae9ca/image.png" alt=""></p>
<table>
<thead>
<tr>
<th align="left">지시어</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">FROM</td>
<td align="left">베이스 이미지 지정</td>
</tr>
<tr>
<td align="left">RUN</td>
<td align="left">이미지를 지정하면서 실행할 명령 지정</td>
</tr>
<tr>
<td align="left">ENTRYPOINT</td>
<td align="left">컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어)</td>
</tr>
<tr>
<td align="left">EXPOSE</td>
<td align="left">컨테이너의 포트 지정</td>
</tr>
<tr>
<td align="left">ADD</td>
<td align="left">이미지 생성 시 파일 추가</td>
</tr>
<tr>
<td align="left">COPY</td>
<td align="left">이미지 생성시 파일 복사</td>
</tr>
<tr>
<td align="left">WORKDIR</td>
<td align="left">컨테이너 작업 디렉토리 지정</td>
</tr>
<tr>
<td align="left">MAINTAINER</td>
<td align="left">이미지 작성자 명시</td>
</tr>
<tr>
<td align="left">CMD</td>
<td align="left">컨테이너의 어플 지정 (컨테이너 시작할 때 실행할 명령어)</td>
</tr>
<tr>
<td align="left">LABEL</td>
<td align="left">이미지의 라벨 지정</td>
</tr>
<tr>
<td align="left">ENV</td>
<td align="left">컨테이너의 환경 변수 지정</td>
</tr>
<tr>
<td align="left">VOLUME</td>
<td align="left">컨테이너의 볼륨 지정</td>
</tr>
<tr>
<td align="left">USER</td>
<td align="left">컨테이너의 사용자 지정</td>
</tr>
<tr>
<td align="left">ARG</td>
<td align="left">인자 설정</td>
</tr>
</tbody></table>
<h3 id="dockerfile">Dockerfile</h3>
<pre><code class="language-docker"># jdk 17(amazoncorretto:17) 환경으로 구성
FROM amazoncorretto:17

# 인자 설정 :: 변수명 JAR_FILE
# build/libs(빌드 시 jar파일 생성 경로) 하위의 모든 jar파일
ARG JAR_FILE=build/libs/*.jar

# Docker Image 생성 시 JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar

# 실행 명령어
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre>
<h3 id="결과-확인하기">결과 확인하기</h3>
<p>master 브랜치에 push 했을 때, 배포가 잘 되는지 확인해보았다.</p>
<h4 id="트러블-슈팅">트러블 슈팅</h4>
<p><img src="https://velog.velcdn.com/images/peace_e/post/c06b5122-bcee-416b-9921-c0638c80c496/image.png" alt=""></p>
<ol>
<li><strong>Docker build &amp; Push 실패</strong><pre><code class="language-yml">   - name: Docker build &amp; Push
   # 레포 명 뒤에 . 이 빠짐
     run: |
       docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz
       docker push ${{ secrets.DOCKER_USERNAME }}/nnzz

</code></pre>
</li>
</ol>
<pre><code>  - name: Docker build &amp; Push
  # 변경 후
    run: |
      docker build -t ${{ secrets.DOCKER_USERNAME }}/nnzz .
      docker push ${{ secrets.DOCKER_USERNAME }}/nnzz</code></pre><pre><code>명령어 끝에 `.` 을 추가해준다. Dockerfile을 현재 디렉토리에서 찾을 수 있게 된다.


2. **Github Actions은 성공하나 테스트를 위해 api를 추가하여 푸시했는데 404 No static resource api/test. 라고 뜸**
(문제를 해결하는 데 도움을 주신 승등님께 감사의 인사를ㅎㅎ)

![](https://velog.velcdn.com/images/peace_e/post/c5c481ca-0392-4710-b8c8-62e879ae8997/image.png)

CI/CD job 전부 error 하나 없이 잘 실행된 터라 원인을 찾는데 오래 걸렸다.

파일을 빌드하는 부분을 Workflow에 넣었기 때문에, 
FileZila에서 조회할 때 갱신된 .jar 파일이 로컬이든 서버든 있어야 한다고 생각을 했는데
build 파일은 수동 배포할 때 것 그대로였다.
-&gt; `./gradlew clean build`가 실행이 안 됐다고 생각했고 gradle build 부분에 문제가 있다고 여겨 그 부분을 계속 수정해봤는데 해결이 안 됐다.


&gt; **사실 로컬과 서버 둘다 빌드 파일이 업데이트 안 되는게 맞다.**
Github Actions은 **가상머신** 위에서 돌아가기 때문에 로컬에선 빌드 안되는게 맞고,
EC2 서버에는 docker image를 docker hub 통해서 받아오는거니까 빌드 파일이 없는게 맞다.
파일이 있다면 이전에 파일질라로 직접 넘겼던 것뿐이어야 한다.

그래서
- **push 했을 때 docker hub에 image 가 올라갔는지 확인하기**
-&gt; image는 잘 있었다.

- **push 전후로 ec2 서버에서 docker 컨테이너 id를 확인하기.**
``` bash
sudo docker ps -a</code></pre><p><img src="https://velog.velcdn.com/images/peace_e/post/4aa9cd47-74c9-4c6d-8380-525c6277e701/image.png" alt="">
컨테이너 자체가 없었다..</p>
<p>이미 8080을 써서 어플리케이션이 실행되고 있는 와중에 별개의 도커 컨테이너를 같은 8080으로 실행시키려고 하니
<strong>포트 충돌</strong> 때문에 컨테이너 자체가 실행되지 않은 것이다.</p>
<p>별 생각 없이 도커 설정만 해주면 되겠거니라고 생각했다.. 반성 </p>
<p>실행중이던 8080포트를 종료하고 push 하니 정상적으로 실행되었다.
혹시 나와 같은 오류를 겪는 분들이 있다면 도움이 되길 바란다.
<img src="https://velog.velcdn.com/images/peace_e/post/3021d086-44b3-461c-a5f2-a5de244a3cd4/image.png" alt=""></p>
<h2 id="레퍼런스">레퍼런스</h2>
<p><a href="https://velog.io/@sdeung01/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%AC-RDS-S3-%EC%84%A4%EC%A0%95-%EB%B0%8F-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B9%8C%EB%93%9C#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%AC">[프로젝트 배포] RDS, S3 설정 및 프로젝트 배포</a></p>
<p><a href="https://velog.io/@sdeung01/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B0%B0%ED%8F%AC-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B0%B0%ED%8F%AC-%EB%B0%8F-CICD-%EA%B5%AC%EC%B6%95">[프로젝트 배포] Docker를 활용한 배포 및 CI/CD 구축</a></p>
<p><a href="https://velog.io/@gmlstjq123/Docker-%EC%84%A4%EC%B9%98">Docker 설치 및 기본 명령어</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[EC2] linux 한국 시간으로 서버 시간 변경하기]]></title>
            <link>https://velog.io/@peace_e/EC2-linux-%ED%95%9C%EA%B5%AD-%EC%8B%9C%EA%B0%84%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B2%84-%EC%8B%9C%EA%B0%84-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/EC2-linux-%ED%95%9C%EA%B5%AD-%EC%8B%9C%EA%B0%84%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B2%84-%EC%8B%9C%EA%B0%84-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 08 Dec 2024 17:36:39 GMT</pubDate>
            <description><![CDATA[<h2 id="시간값이-이상한데요">시간값이 이상한데요?</h2>
<p>분명 <code>Timestamp</code> 컬럼으로 테이블을 만들었고 <code>LocalDateTime</code>으로 선언해서 시간을 넣어주었는데 시간 값이 이상하게 들어갔다.
DBeaver에서 <code>SHOW TIMEZONE;</code> 실행해봐도 <code>Asia/Seoul</code>이라고 잘 뜨는데도 그랬다. 도대체 원인이 뭘까?</p>
<p>그런데,
테스트 용으로 돌리던 로컬 DB에서는, 시간 값이 정확했다.</p>
<p>원인은 AWS EC2였다...</p>
<h2 id="ec2-기본-타임존">EC2 기본 타임존</h2>
<p><strong>EC2의 기본 타임존</strong>은 <code>UCT</code>이다.
아시아 태평양, 서울 리전으로 개설했어도 똑같다. DB의 타임존이 한국시간으로 되어있더라도 서버의 기본타임존은 UCT다.</p>
<blockquote>
<p>UCT는 한국시간과 9시간 차이난다. 이 기본 타임존을 한국 시간으로 바꾸지 않으면 Java에서 생성되는 시간도 모두 9시간 전으로 설정된다. </p>
</blockquote>
<h2 id="한국-시간으로-설정하는-법">한국 시간으로 설정하는 법</h2>
<p>EC2 서버 실행 하고 <code>date</code>로 시간대를 확인한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/86a05ffe-2955-43b9-be6f-a0384f1369fe/image.png" alt=""></p>
<pre><code>sudo timedatectl set-timezone Asia/Seoul</code></pre><p>timedatectl 명령어를 사용해서 Asia/Seoul timeZone으로 바꾼다.</p>
<p>다시 <code>date</code>를 사용해서 시간대를 확인한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/6c054e7d-898f-4a16-a843-1c9c591caf35/image.png" alt="">
KST로 바뀐 모습을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] Problem Details 로 쉽게 에러 처리하기]]></title>
            <link>https://velog.io/@peace_e/Spring-Boot-Problem-Details-%EB%A1%9C-%EC%89%BD%EA%B2%8C-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/Spring-Boot-Problem-Details-%EB%A1%9C-%EC%89%BD%EA%B2%8C-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 28 Nov 2024 19:19:23 GMT</pubDate>
            <description><![CDATA[<p>에러 응답을 쉽고 간편하게 커스텀할 수 있는 problemDetails에 대해서 소개한다.</p>
<h1 id="problem-details">Problem Details</h1>
<p>Problem Details는 Spring boot 3.0.x (Spring Framework 6.0.x)부터 사용가능하다.</p>
<p>Problem Details는 <strong><code>RFC 7807</code></strong>를 따른다. 
<strong><code>RFC 7807</code></strong> 은 API 에러 응답에 대한 규약을 정의한 문서이다. 
<a href="https://www.rfc-editor.org/rfc/rfc7807.html">https://www.rfc-editor.org/rfc/rfc7807.html</a></p>
<p><code>RFC 7807</code> 의 내용은 에러 발생 시 안내할 응답 내용을 문서 세부 정보 개체 (&quot;type&quot;, &quot;title&quot;, &quot;status&quot;, &quot;detail&quot;, &quot;instance&quot;)와 확장 멤버로 구성하자는 것이다.</p>
<p>이 구성을 Problem Details는 그대로 따르고 있다.</p>
<p><a href="https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/http/ProblemDetail.java">https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/http/ProblemDetail.java</a>
ProblemDetail의 클래스의 구조는 깃허브에서 확인가능하다.</p>
<pre><code class="language-java">public class ProblemDetail {

    private static final URI BLANK_TYPE = URI.create(&quot;about:blank&quot;);

    // 문제 유형을 식별하는 URI 참조
    private URI type = BLANK_TYPE;

    // 문제 유형에 대한 사람이 읽을 수 있는 간단한 요약
    @Nullable
    private String title;

    // 이 문제의 응답 Http status 코드
    private int status;

    // 문제 유형에 대한 사람이 읽을 수 있는 간단한 설명
    @Nullable
    private String detail;

    // 문제가 발생한 URI
    @Nullable
    private URI instance;

    // 위에 선언한 문서 세부 정보 필드 이외에 추가적으로 사용할 확장 필드를 저장하는 곳
    @Nullable
    private Map&lt;String, Object&gt; properties;

}</code></pre>
<h1 id="problem-detail-사용하기">Problem Detail 사용하기</h1>
<h2 id="applicationyml">application.yml</h2>
<pre><code class="language-yml">spring:
  mvc:
    problemdetails:
      enabled: true</code></pre>
<p>위처럼 true 값으로 설정해준다.</p>
<h2 id="예외-핸들러-작성">예외 핸들러 작성</h2>
<p>예외도 처리하고, 객체도 리턴해야해서 <code>@RestControllerAdvice</code>를 사용했다.</p>
<p>핸들러는 <code>@ExceptionHandler()</code> 를 사용해서 구현할 수 있다.</p>
<pre><code class="language-java">@RestControllerAdvice
public class ExceptionHandler {
    @ExceptionHandler(UserNotExistsException.class)
        public ProblemDetail handleUserNotExistsException(UserNotExistsException ex) {
            ProblemDetail pd = ProblemDetail.forStatusAndDetail(
                    HttpStatus.NOT_FOUND, ex.getMessage());
            pd.setTitle(&quot;존재하지 않는 유저입니다.&quot;);
            pd.setProperty(&quot;timestamp&quot;, LocalDateTime.now());
            pd.setProperty(&quot;message&quot;, message);
            return pd;
    }</code></pre>
<h3 id="--forstatusanddetail">- forStatusAndDetail()</h3>
<p> status와 detail을 지정할 수 있다.
 forStatus(), setDetail()로 따로 지정할 수도 있다.</p>
<h3 id="--settitle">- setTitle()</h3>
<p>타이틀을 지정한다.</p>
<h3 id="--setproperty">- setProperty()</h3>
<p>추가적으로 확장필드를 지정할 수 있다.</p>
<h3 id="--settype">- setType()</h3>
<p>타입을 지정한다.</p>
<h3 id="--setinstance">- setInstance()</h3>
<p>인스턴스를 지정한다.</p>
<h1 id="응답-확인">응답 확인</h1>
<p><img src="https://velog.velcdn.com/images/peace_e/post/31b03974-253f-49f9-898a-46e20363b36f/image.png" alt="">
type은 따로 지정하지 않으면 기본값 &quot;about:blank&quot;가 출력된다.
title의 경우 따로 설정하지 않아도 HttpStatus의 값에 맞게 가져온다.
instance도 자동으로 에러가 발생한 uri를 가져온다.</p>
<hr>
<p>참고
<a href="https://luvstudy.tistory.com/220">https://luvstudy.tistory.com/220</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring boot] CORS 해결하기]]></title>
            <link>https://velog.io/@peace_e/Spring-boot-CORS-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/Spring-boot-CORS-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 28 Nov 2024 18:42:57 GMT</pubDate>
            <description><![CDATA[<p>프론트엔드와 API 통신을 하면서 CORS 를 많이 겪었다. 혼자서 테스트할 땐 포스트맨으로는 잘 동작하던 API였는데, 프론트에서 받을 때마다 CORS 정책에 위배된다면서 연결이 되지 않았다.</p>
<p>프론트와 협업하는 것도 처음이라 아무것도 모르는 상태였기에 해결하는 데 제법 오랜 시간이 걸렸다.</p>
<h2 id="cors-란">CORS 란?</h2>
<blockquote>
<p><strong>CORS ( Cross-Origin Resource Sharing )</strong>는 서버가 브라우저가 로딩 리소스를 허용해야 하는 자체 출처가 아닌 다른 출처 (도메인, 스킴 또는 포트)를 나타낼 수 있도록 하는 HTTP 헤더 기반 메커니즘 
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS</a></p>
</blockquote>
<p>우리가 겪는 CORS 이슈는 모두 이 메커니즘을 위반했기 때문에 발생한다. 번거롭긴 하지만 CORS 덕분에 우리가 주고받는 리소스가 안전하다는 최소한의 보장을 받을 수 있다. </p>
<h2 id="cross-origin">Cross-Origin</h2>
<p>브라우저는 <strong>보안</strong>상의 이유로 cross-origin HTTP 요청을 제한한다. 여기서 cross-origin이란 <code>다른 출처</code>를 의미한다. 즉, 다른 출처로부터의 리소스 요청을 제한하고 동일한 출처에서만 리소스 요청이 가능하다는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/1385a68c-3ded-43bf-b541-17ab507fd84a/image.png" alt="">
<a href="https://developer.mozilla.org/ko/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL">https://developer.mozilla.org/ko/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL</a></p>
<p>전체 URL에서 <strong><code>Scheme</code></strong>, <strong><code>Domain</code></strong>, <strong><code>Port</code></strong>만 동일하면 된다.</p>
<p>프론트엔드와 백엔드가 협업하는 경우, 각자 서버를 띄우게 되면 포트가 3000/8000으로 서로 다르다. 서로 다른 서버에서 리소스를 주고 받으려하기 때문에, CORS 이슈가 발생하는 것이다.</p>
<p><strong>※ postman에서는 동작하는 이유</strong>
origin이 다른지 판단하는 것은 <code>브라우저</code>다. 서버는 cors를 위반하더라도 정상적으로 응답을 해준다. 응답의 파기 여부는 브라우저에 의해 결정된다. 즉, 브라우저는 통하지 않는 통신인 postman에서는 정상동작한다.</p>
<hr>
<h2 id="해결방법">해결방법</h2>
<p>Spring Security를 사용중이었기에 filterChain 메서드에 cors 관련 설정을 추가했다.</p>
<pre><code class="language-java">@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.httpBasic(HttpBasicConfigurer::disable) // HTTP 기본 인증 비활성화
                .formLogin(AbstractHttpConfigurer::disable) // httpBasic 인증 방식 사용 x
                .csrf(AbstractHttpConfigurer::disable) // csrf 비활성화
                .sessionManagement(httpSecuritySessionManagementConfigurer -&gt; httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                .cors(corsCustomizer -&gt; corsCustomizer.configurationSource(request -&gt; {
                    CorsConfiguration config = new CorsConfiguration();
                    config.setAllowedOrigins(Collections.singletonList(&quot;http://localhost:3000&quot;));
                    config.setAllowedMethods(Arrays.asList(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;, &quot;PATCH&quot;));
                    config.setAllowCredentials(true);
                    config.setAllowedHeaders(Arrays.asList(&quot;Authorization&quot;, &quot;Authorization-refresh&quot;, &quot;Cache-Control&quot;, &quot;Content-Type&quot;));
                    config.setExposedHeaders(Arrays.asList(&quot;Authorization&quot;, &quot;Authorization-refresh&quot;));
                    config.setMaxAge(3600L); // 1시간
                    return config;
                })) // cors
                ;

        return http.build();
    }

</code></pre>
<ul>
<li><strong>setAllowedOrigins()</strong> : 허용할 URL</li>
<li><strong>setAllowedMethods()</strong> : 허용할 Http Method</li>
<li><strong>setAllowCredentials()</strong> : 쿠키 인증 요청 허용</li>
<li><strong>setAllowedHeaders()</strong> : 허용할 Header</li>
<li><strong>setExposedHeaders()</strong> : 응답할 Header</li>
<li><strong>setMaxAge()</strong> : 허용 시간</li>
</ul>
<p>맨 처음에는 setAllowedOrigins() 메서드를 제외하고 전부 <code>&quot;*&quot;</code> 와일드카드를 사용해서 전부 허용했었다.</p>
<p>그래도 안 되길래 구글링해보니 <code>setAllowCredentials()</code> 메서드를 true로 설정한 경우에는 와일드카드를 사용하면 안 된다는 얘기가 있길래, 값을 전부 수정했다.</p>
<p>그리고 WebConfig 에도 cors 설정을 적어주었다.</p>
<pre><code class="language-java">@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
                .allowedOrigins(&quot;https://localhost:3000&quot;);
    }
}</code></pre>
<p>이렇게 설정을 바꿔주니 정상적으로 api 통신이 가능했다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PostgreSQL] Geometry 사용하기]]></title>
            <link>https://velog.io/@peace_e/PostgreSQL-Geometry-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/PostgreSQL-Geometry-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 26 Nov 2024 18:10:30 GMT</pubDate>
            <description><![CDATA[<h2 id="geometry">Geometry</h2>
<p>PostgreSQL에서는 geometry 타입 컬럼과 공간 데이터 계산이 가능한 PostGIS 함수를 제공한다. </p>
<pre><code class="language-sql">CREATE EXTENSION postgis; </code></pre>
<p>위 쿼리를 통해 postgis 확장 모듈을 설치해주어야만 geometry 타입 컬럼이 사용가능하다.</p>
<p>postgis 설치가 완료되면 아래처럼 테이블과 뷰가 생긴다. 삭제하면 postgis를 사용할 수 없으므로 주의하자.
<img src="https://velog.velcdn.com/images/peace_e/post/b98886a7-7ffb-4ed9-bf8a-e4b9fd8f2f80/image.png" alt=""><img src="https://velog.velcdn.com/images/peace_e/post/5e09d697-30f6-40a0-8bcd-41948d9ee4cf/image.png" alt=""></p>
<h2 id="geometry-타입-컬럼-추가하기">Geometry 타입 컬럼 추가하기</h2>
<p>이미 만들어진 테이블에 lat, lng 값이 있다. 그 값을 사용하여 Point 값을 업데이트 할 것이다.</p>
<pre><code class="language-sql">ALTER TABLE 테이블명
ADD COLUMN 추가할컬럼명 GEOMETRY(Point, 4326);</code></pre>
<p>좌표계는 4326을 사용한다.</p>
<pre><code class="language-sql">UPDATE 테이블명
SET 추가한컬럼명 = ST_SetSRID(ST_MakePoint(lng, lat), 4326;</code></pre>
<p><code>ST_SetSRID()</code>함수와 <code>ST_MakePoint()</code>함수를 사용해서 값을 넣는다. lng(경도), lat(위도) 순으로 작성하지 않으면 오류가 발생하므로 유의</p>
<h2 id="geometry-타입-컬럼의-값을-검증하기">Geometry 타입 컬럼의 값을 검증하기</h2>
<pre><code class="language-sql">SELECT ST_IsValid(컬럼명) from 테이블명; 
UPDATE 테이블명 SET 컬럼명 = ST_MakeValid(컬럼명);</code></pre>
<p><code>ST_IsValid()</code> 함수로 값을 검증할 수 있다. 유효한 값이라면 true를 반환한다.
<img src="https://velog.velcdn.com/images/peace_e/post/17fa2e88-734b-4123-808c-2f233e47ae24/image.png" alt=""></p>
<hr>
<p>유효하지 않은 값이 있다면, <code>ST_MakeValid()</code> 잘못된 형식을 유효한 형식으로 변환할 수 있다.
<img src="https://velog.velcdn.com/images/peace_e/post/09659ed1-6427-4332-94d5-fce56000533f/image.png" alt=""></p>
<p>업데이트해서 데이터를 잃을 수 있는데 괜찮냐고 묻는다. 확인을 눌러야 진행가능하다.</p>
<h2 id="geometry-타입-컬럼의-값을-수정하기">Geometry 타입 컬럼의 값을 수정하기</h2>
<p>lng, lat 컬럼의 값이 잘못되어 있었다고 가정하자.
두 컬럼의 값은 수정하더라도 새로 생성한 geometry 타입 컬럼의 값은 수정되지 않는다. </p>
<pre><code class="language-sql">update store
 SET 컬럼명 = ST_SetSRID(ST_MakePoint(lng,lat), 4326)
 where id = id값</code></pre>
<p><code>ST_SetSRID()</code>와 <code>ST_MakePoint()</code>함수를 사용해서 값을 다시 변경할 수 있다.</p>
<h2 id="자동으로-geometry-타입-컬럼의-값을-추가하기">자동으로 Geometry 타입 컬럼의 값을 추가하기</h2>
<p>위와 같은 테이블이 만들어진 상태에서 바로 새 행을 추가하려고 하면, 오류가 발생한다.
geometry 타입 컬럼은 null을 허용하지 않기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/226da550-7c6a-431f-b2ba-69a45a84dace/image.png" alt=""></p>
<p>lng, lat 값만 추가하면 자동으로 geometry 타입 컬럼에도 값이 등록되게끔 하고 싶다면 트리거 함수를 이용하면 된다.</p>
<pre><code class="language-sql">CREATE OR REPLACE FUNCTION 함수명()
RETURNS TRIGGER AS $$
BEGIN
    NEW.컬럼명 := ST_SetSRID(ST_MakePoint(NEW.lng, NEW.lat), 4326);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql; -- 트리거 함수 생성</code></pre>
<p>&#39;$$&#39; 사이에 함수 내용을 작성한다. 
<code>LANGUAGE plpsql</code>은 PL/pgSQL(Procedural Language/PostgreSQL)을 사용하겠다는 뜻이다. 이를 사용해서 데이터베이스를 관리 및 조작할 수 있다.</p>
<pre><code class="language-sql">CREATE TRIGGER 트리거명
BEFORE INSERT OR UPDATE ON 테이블명
FOR EACH ROW
EXECUTE FUNCTION 함수명(); -- 위에서 만든 트리거 함수를 호출</code></pre>
<p>작성된 함수를 트리거를 지정해서 각 행이 INSERT 또는 UPDATE 될 때, 호출되도록 한다.</p>
<p>두 쿼리를 실행시켜주면 오류 없이 행을 추가할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Mybatis] DB에 INSERT 시 ID 값 가져오기]]></title>
            <link>https://velog.io/@peace_e/Mybatis-DB%EC%97%90-INSERT-%EC%8B%9C-ID-%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/Mybatis-DB%EC%97%90-INSERT-%EC%8B%9C-ID-%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</guid>
            <pubDate>Sun, 24 Nov 2024 16:30:39 GMT</pubDate>
            <description><![CDATA[<p>Mybatis를 너무 오랜만에 써봐서 멍청 이슈가 있었다...</p>
<h2 id="mapperxml">mapper.xml</h2>
<p>mapper.xml에서 <strong><code>useGeneratedKeys=&quot;true&quot;</code></strong>, <strong><code>keyProperty=&quot;&quot;</code></strong> 를 추가해준다. 
keyProperty는 가져올 ID의 프로퍼티를 적어주면 된다.</p>
<pre><code class="language-xml">&lt;insert id=&quot;createCard&quot; parameterType=&quot;SaveCardDTO&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;cardId&quot;&gt;
        INSERT INTO card(user_id, store_id, food_type_id)
        VALUES (#{userId}, #{storeId}, #{foodTypeId})
&lt;/insert&gt;</code></pre>
<h2 id="dto">DTO</h2>
<p>DTO에는 키프로퍼티가 반드시 포함되어있어야한다.</p>
<pre><code class="language-java">@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SaveCardDTO {
    private Integer cardId; // keyproperty
    private Integer userId;
    private String storeId;
    private Integer foodTypeId;
}
</code></pre>
<hr>
<h2 id="올바른-예">올바른 예</h2>
<p>값을 insert 후에, 생성된 객체에서 get프로퍼티() 를 통해서 값을 가져올 수 있다.</p>
<pre><code class="language-java">cardService.createCard(newCard);
System.out.println(&quot;cardId : &quot; + newCard.getCardId());</code></pre>
<h2 id="잘못된-예">잘못된 예</h2>
<p><strong>insert 이후의 int 값은 useGeneratedKeys 옵션에 따라서가 아닌, 결과 여부이다.(0 혹은 1)</strong></p>
<pre><code class="language-java">Integer cardId = cardService.createCard(newCard);
System.out.println(&quot;cardId : &quot; + cardId);</code></pre>
<p>이렇게 하고 실행하니 계속 똑같은 결과만 storeId가 1로만 나왔다 (당연함)</p>
<hr>
<h2 id="참고">참고</h2>
<p><a href="https://wwwnghks.tistory.com/170">https://wwwnghks.tistory.com/170</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PostgreSQL] 컬럼 순서 바꾸기]]></title>
            <link>https://velog.io/@peace_e/PostgreSQL-%EC%BB%AC%EB%9F%BC-%EC%88%9C%EC%84%9C-%EB%B0%94%EA%BE%B8%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/PostgreSQL-%EC%BB%AC%EB%9F%BC-%EC%88%9C%EC%84%9C-%EB%B0%94%EA%BE%B8%EA%B8%B0</guid>
            <pubDate>Sun, 24 Nov 2024 16:17:41 GMT</pubDate>
            <description><![CDATA[<p>결론부터 말하자면 <strong>PostgreSQL의 경우, 기존 테이블의 열 순서 변경을 허용하지 않는다.</strong> 불가능하다.</p>
<p>DBeaver에서는 컬럼의 순서를 쉽게 바꿀 수 있다.
아래는 MySQL을 사용하는 테이블이다. 컬럼을 선택하면 순서를 바꿀 수 있는 아이콘이 뜬다.
<img src="https://velog.velcdn.com/images/peace_e/post/52230c03-1289-4a2d-8bbf-5e5071940dbc/image.png" alt=""></p>
<p>아래는 PostgreSQL을 사용하는 테이블이다. 컬럼을 선택해도 순서를 바꿀 수 있는 아이콘이 뜨지않는다. 
<img src="https://velog.velcdn.com/images/peace_e/post/ddb8893e-0dc3-482c-b32b-f0057a0edbef/image.png" alt=""></p>
<p>반드시 특정 위치에 있어야 하는 경우라면 테이블을 삭제하고 다시 생성해야 한다.
이 방법이 내키지 않는다면 뷰를 생성하는 것이 최선의 방법이지만 제약조건을 다시 설정해야 하므로 유의</p>
<hr>
<p>참고
<a href="https://stackoverflow.com/questions/285733/how-do-i-alter-the-position-of-a-column-in-a-postgresql-database-table">https://stackoverflow.com/questions/285733/how-do-i-alter-the-position-of-a-column-in-a-postgresql-database-table</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Swagger와 Spring Security 충돌 해결하기]]></title>
            <link>https://velog.io/@peace_e/Swagger%EC%99%80-Spring-Security-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/Swagger%EC%99%80-Spring-Security-%EC%B6%A9%EB%8F%8C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 20 Nov 2024 15:13:16 GMT</pubDate>
            <description><![CDATA[<p>API 명세서를 일일히 작성하기 번거로워서 Swagger를 통해서 API 명세서를 작성하고 있었다.</p>
<p>Swagger 사용방법에 대해서는 추후에 정리할 예정이다.</p>
<h2 id="spring-security">Spring Security</h2>
<p>프로젝트에서 카카오 Oauth를 통해 회원 로그인을 구현하기 위해 oauth 의존성을 build.gradle에 추가해주었다.</p>
<pre><code>// oauth
implementation(&quot;org.springframework.boot:spring-boot-starter-oauth2-client&quot;)
implementation(&quot;org.springframework.boot:spring-boot-starter-security&quot;)
testImplementation(&quot;org.springframework.security:spring-security-test&quot;)</code></pre><p>application.yml에도 정보를 추가해주었다.</p>
<pre><code class="language-yml">spring:
    security:
        oauth2:
            client:
                registration:
                    kakao:
                        clientId: &#39;{카카오 client-id}&#39;
                        clientSecret: &#39;{카카오 client-secret}&#39;
                        client-authentication-method: post
                        authorization-grant-type: authorization_code
                        redirect-uri: &quot;{url}&quot;
                        ## 서버에서 인증을 마치고 돌아가는 callback uri
                        scope:
                              - account_email
                        client-name: Kakao
                provider:
                      kakao:
                        authorization-uri: https://kauth.kakao.com/oauth/authorize
                        token-uri: https://kauth.kakao.com/oauth/token
                        user-info-uri: https://kapi.kakao.com/v2/user/me
                        user-name-attribute: id
</code></pre>
<h2 id="문제">문제</h2>
<p>당시, 서버와 로컬 두 곳에서 프로젝트를 돌리고 있었고 로컬에서 spring security 추가 이후에 실행이 잘 되는 것을 확인하고 서버에서도 똑같이 실행했다.
(로컬에서는 swagger를 쓰지 않았다.)</p>
<p>서버에서도 문제 없이 api가 응답하는 것을 확인했고 swagger api문서를 들어갔는데 <strong>swagger api failed to load remote configuration.</strong> 라고 뜨면서 페이지가 로딩이 안 됐다...</p>
<h2 id="해결방안">해결방안</h2>
<p>원인을 찾는 것은 어렵지 않았다. spring security 설정 시, 환경설정을 구성하기 위해 작성하는 SecurityConfig에서 <code>requestMatchers()</code>로 swagger에 관한 보안설정도 지정해줘야한다. 설정하지  않은 url 요청은 전부 거부된다. </p>
<p><code>requestMathcers()</code>에 <code>&quot;/swagger-ui/**&quot;</code>, <code>&quot;/v3/api-docs/**&quot;</code> 를 추가했다. <strong>swagger api failed to load remote configuration.</strong> 는 사라졌으나 swagger 페이지가 무한 로딩되는 상황에 빠졌다.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);

        http.authorizeHttpRequests(authorize -&gt; authorize
                .requestMatchers(&quot;/swagger&quot;, &quot;/swagger-ui.html&quot;, &quot;/swagger-ui/**&quot;, &quot;/api-docs&quot;, &quot;/api-docs/**&quot;, &quot;/v3/api-docs/**&quot;).permitAll()
                .requestMatchers(&quot;/api/**&quot;).permitAll()
                .anyRequest().authenticated());

        http.oauth2Login(Customizer.withDefaults());

        return http.build();
    }
}
</code></pre>
<p>맨 위에는 swagger와 관련된 url을 전부 허용해주고,
그 아래에 프로젝트 내에서 사용하는 api url을 분리하여 허용해주었다.</p>
<p>그렇게 하니 swagger api 문서가 잘 보였다!
spring security 사용 시에는 보안 설정을 꼼꼼히 해야겠다는 생각이 들었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] 프리티어로 EC2 인스턴스 생성 및 SSH로 접속하기 / EC2와 RDS 연결하기]]></title>
            <link>https://velog.io/@peace_e/AWS-%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4%EB%A1%9C-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1-RDS-%EC%97%B0%EA%B2%B0-%EB%B0%8F-SSH%EB%A1%9C-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@peace_e/AWS-%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4%EB%A1%9C-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%EC%83%9D%EC%84%B1-RDS-%EC%97%B0%EA%B2%B0-%EB%B0%8F-SSH%EB%A1%9C-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 06 Nov 2024 15:55:26 GMT</pubDate>
            <description><![CDATA[<p>AWS EC2 인스턴스와 RDS 인스턴스를 프리티어로 생성하고 SSH로 EC2에 접속하게 하는 방법을 정리한다.
프리티어로 개설하기 위해 온갖 생쇼를 다 했기에... 다른 사람들에게도 이 포스트가 도움이 되길 바란다!</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/cbd64776-0aef-4386-b847-14b24b45398e/image.png" alt=""></p>
<h1 id="ec2-생성">EC2 생성</h1>
<p>이미 사용중이라 화면 좌측에 프리티어 사용 중이라고 뜨는 상태이다.
좌측의 화면은 생성하고 바로는 표시되지 않는다.
나는 EC2 신청 직후, 사용 중인 프리티어가 없다고 뜨길래 프리티어로 생성이 제대로 안 되었을까봐 엄청 겁먹었는데 원래 하루 정도는 소요되는 것 같다. 청구서도 하루 뒤에 볼 수 있으니 유의하자.
<img src="https://velog.velcdn.com/images/peace_e/post/97c9c046-a6af-4b1e-adee-5805ad2d9e23/image.png" alt="">
처음 EC2를 개설할 때에는 상단바에서 리전 설정을 <code>아시아 태평양(서울)</code> 로 변경하고 <code>인스턴스 시작</code> 버튼을 눌러 인스턴스를 개설한다. 
<img src="https://velog.velcdn.com/images/peace_e/post/3b8c2484-3af2-4f6f-a366-7a0590b8b02d/image.png" alt="">
<img src="https://velog.velcdn.com/images/peace_e/post/e5bd0dc7-13e6-48fe-88cc-9651bd5c810f/image.png" alt=""></p>
<blockquote>
<p><strong>AMI(Amazon Machine Image)</strong>
Amazon EC2 인스턴스를 설정하고 부팅하는 데 필요한 소프트웨어를 제공하는 이미지</p>
</blockquote>
<p>인스턴스의 이름을 설정하고 AMI를 지정한다. 나는 Amazon Linux로 진행했으나 다시 개설한다면 Ubuntu로 진행할 것 같다. 각 AMI마다 유저명과 실행 가능한 명령어가 달라지므로 잘 알아보고 선택하는 것을 추천한다.
(※ 예 : Linux 유저명 : ec2-user, Ubuntu 유저명 : ubuntu
        Linux 명령어 : yum, Ubuntu 명령어 : apt-get)
<img src="https://velog.velcdn.com/images/peace_e/post/c77576a4-c3a1-4655-9795-25845736a1cd/image.png" alt=""></p>
<p>해당 AMI중에서도 프리티어 사용 가능이라고 적혀있는 것을 선택한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/f3e73257-b04e-4a06-bf22-bfc1963cb671/image.png" alt="">
인스턴스 유형도 프리티어 사용가능한 것(t2.micro 뿐임)으로 선택해준다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/feb72b73-de83-4ea5-b657-26e95d390041/image.png" alt="">
인스턴스 접속을 위해서는 키 페어가 반드시 필요하다. 새 키 페어 생성을 클릭하여 키 페어를 생성한다. 키 정보가 유출되지 않도록 주의해서 관리해야 한다.
<img src="https://velog.velcdn.com/images/peace_e/post/90331220-ccd3-4654-a9f9-2816f802d521/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/5972d750-6cb4-4d4c-8af1-359cf7bdfb33/image.png" alt="">
기존에 생성해둔 보안그룹을 선택하거나, 새로운 보안그룹을 생성할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/4495b1cc-1de6-4915-8893-9da8ba4e0f87/image.png" alt="">
프리티어에서는 <code>최대 30gb</code>의 스토리지를 사용할 수 있다</p>
<p>고급 세부 정보는 변경하지 않고 <code>인스턴스 시작</code>을 눌러 새로 인스턴스를 생성한다.</p>
<p>처음 생성하고 몇 분정도 소요되며, 생성 중에는 인스턴스의 상태가 대기 중으로 표시된다.</p>
<h2 id="ec2-보안그룹-편집">EC2 보안그룹 편집</h2>
<p>최초 생성 시, SSH 연결을 위한 22 포트만 존재하는데 Spring boot 연결을 위해 8080 포트와, HTTP 80 포트를 추가해준다.
<img src="https://velog.velcdn.com/images/peace_e/post/5ffece3f-2a53-46e3-a249-b14a167239b9/image.png" alt=""></p>
<h2 id="탄력적-ip-할당">탄력적 IP 할당</h2>
<p>AWS EC2는 매번 서버를 시작하고 종료할 때마다 새로운 IP가 할당되므로 탄력적 IP를 할당받아야한다. EC2 서버 하나에 탄력적 IP 하나는 프리티어로 사용가능하다. <strong>단, 탄력적 IP를 생성하고 EC2 서버에 연결하지 않는다면 요금이 부과되므로 주의하자.</strong></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/4214889e-3787-454f-887f-d08989f88b4f/image.png" alt=""><img src="https://velog.velcdn.com/images/peace_e/post/980d6b4e-15d7-4240-b666-89440473191a/image.png" alt=""></p>
<p>EC2 탭 &gt; <code>탄력적 IP</code> &gt; <code>탄력적 IP 주소 할당</code> 을 눌러서 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/99b11b89-0a39-4ab9-b31f-d7fc24473e2a/image.png" alt=""></p>
<p>기본 설정대로 생성하고 <code>탄력적 IP 주소 연결</code>을 클릭한다.
<img src="https://velog.velcdn.com/images/peace_e/post/a944f947-319f-4a3d-95dd-28d093995b78/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/bb76c3ee-c7ad-4211-bd0b-de346b1f5c1e/image.png" alt=""></p>
<p>앞서 생성한 인스턴스에 탄력적 IP를 연결한다.
그러고 EC2 인스턴스의 <code>퍼블릭 IPv4 주소</code>를 확인해보면 방금 생성한 <code>탄력적 IP 주소</code>로 연결되어 있는 것을 확인할 수 있다.</p>
<h2 id="로컬에서-ec2-서버-접속">로컬에서 EC2 서버 접속</h2>
<p>설치된 git bash로 접속을 진행한다.
우선 홈 디렉토리에 .ssh 디렉토리를 만들고 생성해둔 키 페어를 .ssh 디렉토리로 복사한다.
복사한 키 권한을 바꿔주고, <code>vim</code> 명령어로 config 파일을 생성한다.</p>
<blockquote>
<p><code>vim</code> 명령어 사용 시</p>
</blockquote>
<ul>
<li><code>i</code>키를 눌러 insert를 진행하고</li>
<li>입력이 끝났을 땐 <code>esc</code> 키를 누르고 <code>:eq</code>를 입력하여 종료한다.</li>
</ul>
<p>config 파일에는 접속 시 필요한 호스트 정보가 들어간다.</p>
<pre><code>Host nnzz
        Hostname 탄력적주소IP
        User  ec2-user(Ubuntu 사용자라면 ubuntu)
        IdentityFile ~/.ssh/nnzz-key.pem</code></pre><p><img src="https://velog.velcdn.com/images/peace_e/post/bed24ca4-94c9-4769-a936-1f30b5096746/image.png" alt=""></p>
<p>ssh [설정한 호스트] 를 입력해서 접속하면 되는데, 첫 접속시에는 항상 신뢰여부를 묻는다. <code>yes</code> 를 입력하면 연결해주지만 known hosts에 등록해둘것을 경고한다.
<img src="https://velog.velcdn.com/images/peace_e/post/b9d1dc8d-6bea-49b9-9f9b-9b66bf7a3543/image.png" alt=""></p>
<p><code>ssh-keyscan</code> 명령어를 통해 known_hosts 파일을 생성해서 등록한다.</p>
<pre><code>ssh-keyscan -t rsa [탄력적IP주소] &gt;&gt; known_hosts</code></pre><h2 id="jdk-설치">JDK 설치</h2>
<p>프로젝트에서는 Amazon Corretto 17버전을 사용하고 있어서 동일한 버전으로 설치해주었다.</p>
<p>ec2 생성 시 선택한 AMI가 Amazon Linux 2023 버전인데 이 버전은 <code>yum</code> 명령어 뿐만 아니라 <code>dnf</code> 명령어도 사용 가능하다. 초기 세팅할 때, 명령어 때문에 애를 먹었는데 노트북에서 새롭게 해당 환경을 세팅하기 위해 다시 진행하면서 <code>dnf</code> 명령어를 사용하니 한번에 설치되었다.</p>
<p>ssh로 접속 후에 아래처럼 입력해서 Amazon Corretto 17을 설치한다.</p>
<pre><code>sudo dnf install java-17-amazon-corretto-devel.x86_64</code></pre><p><img src="https://velog.velcdn.com/images/peace_e/post/5baaab3f-656a-44ca-9ce0-da0c2f59cbed/image.png" alt=""></p>
<p>다운로드 받을 때 y/n으로 묻는데 y를 누르면 계속 진행한다.
<img src="https://velog.velcdn.com/images/peace_e/post/e80de19a-81e1-44b6-aec6-ae7ff9ee90b5/image.png" alt="">
사진처럼 뜨면 설치완료된 것.</p>
<h1 id="rds-생성">RDS 생성</h1>
<p><img src="https://velog.velcdn.com/images/peace_e/post/ee514e89-7c47-42c6-92a6-351fca11ca4c/image.png" alt="">
<img src="https://velog.velcdn.com/images/peace_e/post/fa8d15ab-dfbe-4ee1-a84e-cfa7cc3eafeb/image.png" alt="">
<img src="https://velog.velcdn.com/images/peace_e/post/1272a623-9a23-49fe-b089-85ed66d25c7e/image.png" alt=""></p>
<p>표준생성, PostgreSQL, 16.3버전을 선택했다.
RDS 확장 지원 활성화는 유료 오퍼링이므로 선택하지 않았다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/ebeec9af-3961-4990-b04d-f95338ce63b5/image.png" alt="">
템플릿을 프리티어로 선택하게 되면 가용성 및 내구성은 제한된다. 과금할 거 아니니 이렇게 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/d39f8970-bc39-4e2e-a7ef-413ea38248a9/image.png" alt="">
데이터베이스 이름과 마스터 사용자 이름, 비밀번호를 설정한다.
설정한 이름과 비밀번호는 나중에 필요하므로 기억해둬야한다. 이름은 기본 설정인 postgres 그대로 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/b1cfff27-1b34-4ea7-869e-b6987d313437/image.png" alt="">
인스턴스 구성의 경우 db.t4g.micro로 진행했다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/97a28735-c7b6-4b2b-a718-e053204c0781/image.png" alt="">
스토리지는 기본 설정이 범용  SSD(gp3)이라 이대로 생성했는데 다음날 청구서를 보니 
<strong>$0.131 per GB-month of provisioned GP3 storage running PostgreSQL</strong> 로 청구된 것을 확인할 수 있었다..
<img src="https://velog.velcdn.com/images/peace_e/post/50b142b7-2dc8-48e5-b5da-5720e57236a8/image.png" alt=""></p>
<p>gp2로 바꾸었더니 추가로 청구된 것은 없었다. 프리티어로 생성할 거라면 gp2로 생성하자.
<img src="https://velog.velcdn.com/images/peace_e/post/f86c0a10-c1bb-40da-956e-3ba761e6c3c4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/18abd5f8-8be6-40a4-ad4f-4542635abf61/image.png" alt="">
스토리지 자동 조정의 경우 반드시 체크 해제해주어야 한다. 체크 시 프리티어여도 과금 대상이다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/d30d6343-cbcf-4059-b9ee-58ca28f2f7f9/image.png" alt="">
EC2 서버와 RDS를 연결해서 사용할 것이므로 <code>EC2 컴퓨팅 리소스에 연결</code>을 체크하고 연결할 EC2 인스턴스를 선택해준다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/bf177fc5-aa43-45b1-92ac-8efb98078815/image.png" alt="">
서브넷 그룹이나, VPC 보안 그룹은 미리 생성해둔 게 있다면 기존 항목을 선택할 수 있다. 새로 생성도 가능하다.
퍼블릭 액세스의 경우 <code>EC2 컴퓨팅 리소스에 연결</code> 선택 시 자동으로 <code>아니오</code>에 체크된다. 퍼블릭 액세스 못 하는 거 아니냐고 걱정할 필요 없다. 데이터베이스도 ssh 통해서 연결하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/c684974c-6eb1-4c28-a2ff-9be23496c822/image.png" alt="">
가용영역의 경우, 이미 개설한 ec2 인스턴스와 연결하게 되면 ec2 인스턴스가 사용하는 리전 지역으로 연결해준다.</p>
<blockquote>
<p><strong>※ 주의</strong>
rds를 생성할 때, 아무리 서울 지역으로 생성했더라도 ec2에 연결하지 않고 생성을 끝내면, 가용영역이 달라져 나중에 ec2에 연결되지 않을 수 있으므로 반드시 생성할 때 연결하는 것을 추천한다.</p>
</blockquote>
<p>인증기관과 데이터베이스 포트는 기본값으로 둔다. 데이터베이스 포트의 경우, 각 데이터베이스 별로 기본 포트 값이 표시된다. postgresql은 5432 포트를 사용한다. </p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/894c91db-07ef-40f1-b4c9-59961f52daa1/image.png" alt="">
따로 설정하지 않고 넘어간다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/d21930ce-8d2f-4308-bde9-c8a33f44ac05/image.png" alt="">
<img src="https://velog.velcdn.com/images/peace_e/post/37ee8645-63c5-4fee-9cb3-f97447afea40/image.png" alt="">
<img src="https://velog.velcdn.com/images/peace_e/post/4f7e9fab-76d4-4b72-ad40-11d99b8ac966/image.png" alt=""></p>
<p>모니터링과 추가구성의 자동 백업, 삭제 방지는 전부 체크 해제 해주었다. </p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/abd0fba5-7298-4dc8-a595-493b97e6c1b8/image.png" alt="">
월별 추정 요금을 알려주기는 하나, 나도 20gb의 범용 스토리지는 괜찮으니까 gp3도 무료구나 싶었는데 아니었다. 되도록이면 요금 페이지에서 자세한 설명을 보고 진행하는 것을 추천한다. 불안하다면 생성하고나서 매일 같이 청구서를 들여다보고 과금될 때마다 하나씩 변경하자. <del>내가 그랬다</del></p>
<p>설정을 완료하고 데이터베이스 생성을 클릭하고 완전히 생성될 때까지 기다린다.</p>
<h2 id="dbeaver나-intellij에-db-연결하기">DBeaver나 Intellij에 DB 연결하기</h2>
<p>생성한 데이터베이스 종류의 맞게 선택하고 연결을 설정한다.
<img src="https://velog.velcdn.com/images/peace_e/post/8506310c-ffda-4a1f-ba37-677a7ecde542/image.png" alt=""></p>
<ul>
<li>호스트에 생성된 RDS 인스턴스의 엔드포인트를 복사해 넣는다.</li>
<li><code>Username</code>과 <code>Password</code>에는 RDS 설정시 넣은 사용자명과 비밀번호를 넣어준다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/peace_e/post/5c837ed1-6e61-4144-8ee4-f05573023e79/image.png" alt=""></p>
<ul>
<li><code>SSH</code> 탭으로 넘어와서 Host/IP 에 EC2에서 설정한 <code>탄력적 IP</code>를 넣어준다.</li>
<li>포트는 <code>22</code>, User Name은 EC2 인스턴스를 생성할 때, 선택한 AMI에  따라 유저명이 달라진다.</li>
<li>Authentication Method는 <code>Public Key</code>를 선택하고 <code>Private Key</code>를 키 페어 파일로 지정한다.</li>
<li>Test Connection 을 클릭해 연결이 잘 되는지 확인하고 필요한 드라이버를 다운받은 뒤, 최종적으로 연결한다.</li>
</ul>
<p>Intellij에 DB를 연결할 때도, RDS의 엔드포인트를 호스트로 넣고, SSH로 통신할 때 키 페어를 사용하면 된다.</p>
<h2 id="한국시간으로-바꾸기">한국시간으로 바꾸기</h2>
<p>EC2 의 기본 시간은 UTC이므로 한국 표준시인 KST로 바꾸고 작업해야한다.
<a href="https://velog.io/@peace_e/EC2-linux-%ED%95%9C%EA%B5%AD-%EC%8B%9C%EA%B0%84%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B2%84-%EC%8B%9C%EA%B0%84-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0">https://velog.io/@peace_e/EC2-linux-한국-시간으로-서버-시간-변경하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사이드 프로젝트를 MySQL에서 PostgreSQL 로 마이그레이션 하게 된 이유]]></title>
            <link>https://velog.io/@peace_e/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-MySQL%EC%97%90%EC%84%9C-PostgreSQL-%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%ED%95%98%EA%B2%8C-%EB%90%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@peace_e/%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-MySQL%EC%97%90%EC%84%9C-PostgreSQL-%EB%A1%9C-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%ED%95%98%EA%B2%8C-%EB%90%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 03 Nov 2024 14:54:57 GMT</pubDate>
            <description><![CDATA[<h2 id="진행배경">진행배경</h2>
<p>사용자의 위치 정보를 받아서 DB에 저장된 근처에 있는 음식점들 중, 유저가 선택한 카테고리에 맞는 가게들만 찾아서 가게의 상세 정보들을 불러와야했다. 이 때, 유저가 선택할 수 있는 카테고리는 총 30가지다. &quot;나는 한식만 먹고싶어&quot;라고 확실히 마음을 정한 유저들도 있겠지만 아닌 경우들이 더 많다고 생각한다. <del>일단 나는 그렇다.</del>
점심, 저녁 메뉴 정하기 고민을 줄여보자는 마음에서 계획한 프로젝트지만 분명히 이것도 좋고, 저것도 좋은데라고 생각할 유저는 있을 것이다. 그럼 30가지 카테고리를 전부 선택했을 때에도! 주변에 그 카테고리에 해당하는 가게가 많더라도! 최대한 빨리 정보를 불러와야지.</p>
<h2 id="문제-상황">문제 상황</h2>
<p>MySQL이 익숙하기에 이번 프로젝트에서도 사용했다. 데이터베이스에서 해당하는 가게의 리스트(가게의 ID 정보만)를 먼저 뽑고, 각 가게당 메뉴, 방송출연 여부 등 기타 정보를 가져오도록 코드를 작성했다. 이 과정에서는 결국 가게의 리스트의 길이만큼 반복문을 돌 수 밖에 없다. 이건 데이터베이스의 문제보다도 작성한 쿼리의 문제긴 하지만 2000여 곳의 데이터가 들어있는 환경에서 카테고리에 해당하는 470개의 리스트를 가져올 때, 기본적으로 30초가 넘는 시간이 소요되었다. 여러 번 실행해볼 때, 심하면 40초까지 걸리기도 했다.</p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/29de8263-9f03-496c-a1be-a1bbda6f976d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/peace_e/post/31f23b41-c54f-4857-b650-bfce80a6e151/image.png" alt=""></p>
<h2 id="해결-방안">해결 방안</h2>
<p><a href="https://steemit.com/kr-dev/@tmkor/db-2-mysql-vs-postgis-postgresql">https://steemit.com/kr-dev/@tmkor/db-2-mysql-vs-postgis-postgresql</a>
PostgreSQL + PostGIS를 사용하면 공간 검색 기능면에서 이점이 있다는 글들을 보았고 테스트를 해보기로 했다.</p>
<ol>
<li>반복문을 돌지 않고 조인을 통해서 한 번에 조인하게끔 쿼리를 수정한다.</li>
<li>PostgreSQL에서 PostGIS를 사용하고 똑같은 쿼리로 실행한다.</li>
</ol>
<p>두 가지를 전부 테스트 해 보았다. 기존의 DB는 AWS 서버에 연결된 RDS였고, PostgreSQL 사용을 위해 로컬에 PostgreSQL을 설치하고 테스트를 진행했다. 설치시에는 StackBuilder를 통해 PostGIS도 추가로 설치를 진행해주었다.</p>
<h3 id="1-postgresql과-postgis-사용하기">1. PostgreSQL과 PostGIS 사용하기</h3>
<blockquote>
<p><a href="https://velog.io/@peace_e/PostgreSQL-Geometry-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">https://velog.io/@peace_e/PostgreSQL-Geometry-사용하기</a>
geometry 컬럼 사용법은 이 게시글에 정리해두었다.</p>
</blockquote>
<p>build.gradle에 postgre 의존성을 추가하고, </p>
<pre><code class="language-gradle">dependencies {
    implementation &#39;org.postgresql:postgresql:42.7.3&#39;
}</code></pre>
<p>application.yml에도 postgre로 바꿔주었다. </p>
<pre><code class="language-yml">spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/postgres
    username: 유저이름
    password: 비번</code></pre>
<p>※ MySQL 포트번호 : 3306
※ PostgreSQL 포트번호 : 5432</p>
<blockquote>
<p><strong>PostGIS</strong>
GiST 기반 R-TREE 공간 인덱스를 지원하고 GIS 객체의 분석 및 공간 처리를 위한 기능을 포함하고 있다. 다양한 공간 데이터 형식과 공간 쿼리를 지원한다는 이점이 있다.</p>
</blockquote>
<p>PostgreSQL에서는 PostGIS 사용을 위해 아래처럼 작성하고 실행해줘야 한다.</p>
<pre><code class="language-sql">CREATE EXTENSION postgis;</code></pre>
<p>ST_DistanceSphere() 함수를 사용하여 거리를 계산했다. </p>
<pre><code class="language-sql">SELECT store_id
  FROM store
 WHERE ST_DistanceSphere(location, ST_GeomFromText(&#39;POINT(경도 위도)&#39;, 4326)) &lt;= 750
</code></pre>
<ol>
<li>PostgreSQL에서 똑같이 반복문을 타는 쿼리로 실행했을 때, 7초 정도가 소요됐다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/peace_e/post/3d0d110f-8a98-4d47-b978-505d8293504a/image.png" alt=""></p>
<ol start="2">
<li>반복문 대신 JOIN을 통해서 다른 테이블에 저장된 가게의 정보값까지 전부 가져오기 위해 json_agg를 사용했다. 반복문을 사용할 때랑 별반 차이가 나지 않는다... 이게 아닌데..</li>
</ol>
<p><img src="https://velog.velcdn.com/images/peace_e/post/9b07e638-6f40-4fda-a2ed-f0197e6355c0/image.png" alt=""></p>
<ol start="3">
<li>PostgreSQL은 json 데이터를 저장하기위해 json뿐만 아니라 jsonb 타입을 제공한다. json 타입은 원본을 저장하고, jsonb 타입은 바이너리 형식으로 저장하기에 json 타입보다 속도가 빠르다. 또한 인덱싱을 지원한다는 이점도 있다. jsonb_agg와 with 절을 사용해 시간을 줄여보기로 했다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/peace_e/post/4ced51ba-3dae-4b50-8529-e991b15eb9fa/image.png" alt=""></p>
<p>결과는 성공적이었다. 100ms 내외로 응답이 나온다.</p>
<h3 id="2-mysql에서-쿼리만-변경하기">2. MySQL에서 쿼리만 변경하기</h3>
<p>MySQL에서는 위/경도 값을 POINT형태로 location 컬럼 값에 넣었고, 공간인덱스를 추가했다.
공간 인덱스의 경우 ST_DistanceSphere() 함수를 지원하지 않아 거리를 측정하기 위해 ST_Distance() 함수를 사용했다. 750M 내에 있는 가게들만 우선 골라낸다.</p>
<pre><code class="language-sql">SELECT store_id
  FROM store
 WHERE ST_Distance(ST_GeomFromText(&#39;POINT(경도 위도)&#39;, 4326)) &lt;= 750
</code></pre>
<ol>
<li><p>json을 사용해서 한번에 조회하도록 했다.
<img src="https://velog.velcdn.com/images/peace_e/post/df10efec-d21d-407f-833c-1ed8a36fe2a0/image.png" alt="">
편차가 좀 있었지만 300ms ~ 2s 정도의 시간이 소요되었다.</p>
</li>
<li><p>같은 json인데 쿼리를 with 절로 바꾸었다.
<img src="https://velog.velcdn.com/images/peace_e/post/6c1acb93-7600-4970-b339-b6f452aa1b51/image.png" alt="">
200ms ~ 300ms 정도로 걸렸다.</p>
</li>
</ol>
<hr>
<h2 id="결론">결론</h2>
<p>쿼리를 바꾸는 것만으로도 성능은 크게 개선되었지만 가게 DB가 늘어나는 것도 고려하면 PostgreSQL로 마이그레이션 하지 않을 이유가 없었다.
게다가 추후 프로젝트를 확장시에 &#39;00동&#39;, &#39;00구&#39; 단위로 해당하는 가게 리스트를 보여줄 계획도 있었기에, shp 파일을 db화 하기에는 PostgreSQL이 적합하다고 생각했다. </p>
<p>DB에서 넘긴 json형식의 데이터는 처음 받아봤는데, Mybatis로 json형식의 데이터를 조회하려면 타입핸들러를 구현해야한다는 것도 알게 되었다. 이점에 대해서는 추후에 기록할 예정이다.</p>
]]></description>
        </item>
    </channel>
</rss>