<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yangwon-park.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 31 Mar 2025 03:03:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yangwon-park.log</title>
            <url>https://velog.velcdn.com/images/yangwon-park/profile/7cdd2f93-bcf3-4fe1-8f81-94c47fba2b3a/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yangwon-park.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yangwon-park" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[AOP를 활용한 분산락 최적화]]></title>
            <link>https://velog.io/@yangwon-park/AOP%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B6%84%EC%82%B0%EB%9D%BD-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@yangwon-park/AOP%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B6%84%EC%82%B0%EB%9D%BD-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Mon, 31 Mar 2025 03:03:33 GMT</pubDate>
            <description><![CDATA[<h2 id="topic">Topic</h2>
<p>AOP를 활용한 분산락 최적화</p>
<h2 id="issue">Issue</h2>
<p>Redisson 적용 후 특정 트래픽 이상으로 상품 교환 API가 호출되면 Hikaripool Connection이 고갈되는 문제 발생</p>
<h2 id="step">Step</h2>
<h3 id="1-발생-원인">1. 발생 원인</h3>
<ul>
<li>회원별 상품 교환 포인트를 조회 API 로직 최적화 실패</li>
<li>트랜잭션 범위 과대 설정으로 인한 상품 교환 로직 병목 현상</li>
</ul>
<h3 id="2-어떻게-해결하나">2. 어떻게 해결하나?</h3>
<p>일단 포인트 조회 API 로직 최적화를 수행한 후, 이번 포스팅의 주제인 상품 교환 로직 최적화를 수행하였다.</p>
<p>기존 코드는 아래와 같이 API가 호출하는 Service 메소드가 하나의 트랜잭션으로 관리되고 있었다.</p>
<pre><code class="language-java">@Transactional(rollbackFor = Exception.class)
public Long makeOrder(Long memberId, Long itemId, OrderInfoAndDeliveryHistoryDTO.SaveRequest request) {
    log.info(&quot;MAKE ORDER :: {}&quot;, itemId);

    RLock lock = acquireLock(itemId);

    try {
        return performOrderTransaction(memberId, itemId, request);
    } finally {
        releaseLock(lock);
    }
}

private RLock acquireLock(Long itemId) {
    RLock lock = redissonClient.getLock(&quot;itemLock:&quot; + itemId);

    try {
        if (!lock.tryLock(10, 30, TimeUnit.SECONDS)) {
            log.error(&quot;COULD NOT ACQUIRE A LOCK FOR ITEM :: {}&quot;, itemId);
            throw new RuntimeException(&quot;COULD NOT ACQUIRE A LOCK FOR ITEM :: &quot; + itemId);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        log.error(&quot;LOCK ACQUISITION INTERRUPTED&quot;, e);
        throw new RuntimeException(&quot;LOCK ACQUISITION INTERRUPTED&quot;, e);
    }

    return lock;
}
</code></pre>
<p>위 코드에는 크게 2가지의 문제점이 있다.</p>
<ul>
<li>트랜잭션 범위가 너무 넓어 makeOrder 메소드 내부 로직 수행 시간이 RLock의 임대 시간보다 긴 경우 원하는 결과를 얻을 수 없음</li>
<li>트랜잭션이 열린 후 RLock을 획득하여 Hikaripool 고갈 문제가 발생할 수 있음</li>
</ul>
<p>위에 언급한 문제들을 해결하기 위해 아래의 과정을 수행해보았다.</p>
<h4 id="2-1-트랜잭션-분리">2-1. 트랜잭션 분리</h4>
<p>일단 트랜잭션의 범위를 메소드 전체가 아닌 로직 별로 분리하는 과정을 거쳤다.</p>
<pre><code class="language-kotlin">@Component
class ItemOrderFacade(
    private val itemStockService: ItemStockService,
    ...
) {
    fun makeOrder(
        ...
    ) {
        // Validation 로직

        // 재고 감소 로직 수행
        itemStockService.reduceItemStock(itemId)

        // 남은 Business 로직 수행
    }
}</code></pre>
<pre><code class="language-kotlin">@Service
@Transactional
class ItemStockService(
    ...
) {
    val lock = acquireLock(itemId)

    try {
        // 재고 감소 수행
    } finally {
        releaseLock(lock)
    }
}

// RLock 획득 메소드
private fun acquireLock(itemId: Long) {
    ...
}
</code></pre>
<p>현재 프로젝트는 Service 최상단 부분에 @Transactional Annotation을 붙이는게 컨벤션이기에 메소드별로 Trasaction을 개별로 컨트롤하기 위해 별도의 Facade Layer를 두고 재고 감소 전체 로직을 수행하는 형식으로 바꾸었다.</p>
<h3 id="새롭게-발생한-이슈">새롭게 발생한 이슈</h3>
<p>위와 같이 트랜잭션을 분리한 후, 재고 감소가 정상적이게 반영되지 않는 이슈가 발생하였다. 한참을 고생하다가 찾은 원인은 아래 이유였다.</p>
<ul>
<li>트랜잭션 종료 시점과 락의 종료 시점 제어 이슈</li>
</ul>
<p>쉽게 말해 트랜잭션이 종료되기 전에 락이 먼저 종료되어 우리가 원하던 동시성 제어에 실패하게 된 것이다.</p>
<h3 id="어떻게-해결할까">어떻게 해결할까??</h3>
<p>내가 원하는 플로우는 다음과 같다.</p>
<pre><code>분산락 획득 
    -&gt; 트랜잭션 시작 
        -&gt; 비즈니스 로직 수행 
    -&gt; 트랜잭션 종료 
-&gt; 분산락 종료</code></pre><p>따라서 분산락 획득을 트랜잭션 시작 시점보다 더 앞으로 땡길 수 있는 방법을 모색하다가 관련하여 좋은 포스팅을 발견하였다.</p>
<blockquote>
<p><a href="https://helloworld.kurly.com/blog/distributed-redisson-lock/">마켓컬리 분산락 관련 포스팅</a></p>
</blockquote>
<p>해당 포스팅은 나에게 단비와 같은 존재였고 이를 참고하여 AOP와 Annotation을 활용한 나만의 재고 감소 플로우를 완성할 수 있었다.</p>
<pre><code class="language-kotlin">@Aspect
@Component
class DistributedLockAspect(
    private val redissonClient: RedissonClient,
    private val transactionForAOP: TransactionForAOP,
) {
    @Around(&quot;@annotation(com.ppfriends.api.common.annotation.DistributedLock)&quot;)
    fun getDistributedLock(joinPoint: ProceedingJoinPoint): Any {
        val signature = joinPoint.signature as MethodSignature
        val method = signature.method

        val distributedLock = method.getAnnotation(DistributedLock::class.java)

        val key =
            REDISSON_LOCK_PREFIX +
                CustomSpringELParser.getDynamicValue(
                    signature.parameterNames,
                    joinPoint.args,
                    distributedLock.key,
                )

        val lock = redissonClient.getLock(key)

        try {
            log.info { &quot;Lock 획득 시도 :: $key&quot; }

            val available = lock.tryLock(distributedLock.waitTime, distributedLock.leaseTime, distributedLock.timeUnit)

            if (!available) {
                // Lock 획득에 실패한 경우 핸들링
            }

            log.info { &quot;Lock 획득 성공 :: $key&quot; }

            return transactionForAOP.proceed(joinPoint)
        } catch (e: Exception) {
            // 예외 발생 시 핸들링
        } finally {
            try {
                if (lock.isHeldByCurrentThread &amp;&amp; lock.isLocked) {
                    lock.unlock()
                    log.info { &quot;Lock 해제 성공 :: $key&quot; }
                } else {
                    // 정상 종료 실패시 핸들링
                }
            } catch (e: IllegalMonitorStateException) {
                // 해제된 락에 접근하려는 시점 핸들링 
            }
        }
    }

    companion object {
        private const val REDISSON_LOCK_PREFIX = &quot;LOCK:&quot;
    }
}</code></pre>
<pre><code class="language-kotlin">@Component
class TransactionForAOP {
    // 트랜잭션을 이 시점에 열어줌
    // REQUIRES_NEW 옵션으로 항상 새로운 트랜잭션을 획득함
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    fun proceed(joinPoint: ProceedingJoinPoint): Any {
        return joinPoint.proceed()
    }
}</code></pre>
<pre><code class="language-kotlin">@Component
class ItemOrderFacade(
    private val itemOrderService: ItemOrderService,
    private val itemStockService: ItemStockService,
    ...
) {
    fun makeOrder(
        memberId: Long,
        itemId: Long,
        request: OrderInfoAndDeliveryHistoryDTO.SaveRequest,
    ) {
        // Validation 로직

        // 재고 감소
        itemStockService.reduceItemStock(itemId)

        try {
            // 비즈니스 로직 수행
        } catch (e: Exception) {
            // 비즈니스 로직 수행 실패 시, 재고 복구
            itemStockService.restoreItemStock(itemId)
            throw e
        }
    }
}</code></pre>
<pre><code class="language-kotlin">import java.util.concurrent.TimeUnit

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class DistributedLock(
    // Lock Name
    val key: String,

    // Lock Time 단위
    val timeUnit: TimeUnit = TimeUnit.SECONDS,

    // Lock 대기 시간
    val waitTime: Long = 5L,

    // Lock 임대 시간 (획득 이후 leaseTime이 경과하면 Lock을 해제함)
    // 0으로 설정 시, WatchDog 동작 (릴리즈 타임 자동)
    val leaseTime: Long = 3L,
)</code></pre>
<pre><code class="language-kotlin">@DistributedLock(key = &quot;#itemId&quot;)
fun reduceItemStock(itemId: Long): Item {
    ...
    // 재고 감소
    item.reduceStock(1)

    return item
}

@DistributedLock(key = &quot;#itemId&quot;)
fun restoreItemStock(itemId: Long): Item {
    ...
    // 재고 복구
    item.restoreStock(1)

    return item
}</code></pre>
<h2 id="reflection">Reflection</h2>
<p>좋은 래퍼런스를 참고하여 분산락과 트랜잭션에 관한 좋은 학습 및 이를 실제 운영에 적용할 수 있어 매우 뿌듯한 경험을 할 수 있었다. 또한 Annotation을 활용한 분산락 적용 방식을 채택하여 보다 간결하게 분산락을 사용할 수 있게 되었다.
이 포스팅이 분산락과 트랜잭션의 순서를 제어하고자 하는 다른 개발자 분들에게 도움이 되었으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flyway 적용기]]></title>
            <link>https://velog.io/@yangwon-park/Flyway-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@yangwon-park/Flyway-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Mon, 08 Jul 2024 06:11:18 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>회사 프로젝트 규모가 커지고 팀원이 많아질수록 데이터베이스(이하 DB) 싱크를 맞추는게 생각보다 쉽지가 않았다. 이로 인한 스트레스를 줄이고자 코드를 형상 관리해주는 Git과 같은 좋은 툴이 있나 찾아보았고, 최종적으로 Spring 진영에서 많이들 사용하고 있는 <code>Flyway</code>를 적용해보기로 결정하였다.</p>
<h1 id="flyway">Flyway</h1>
<p>앞서 설명한 것처럼 Flyway는 DB의 형상 관리를 담당해주는 오픈소스 DB Migration Tool이다.</p>
<blockquote>
<p><a href="https://flywaydb.org/">공식 홈페이지</a></p>
</blockquote>
<h2 id="convention">Convention</h2>
<p>Flyway가 읽어들이는 SQL 스크립트의 경우 정해져있는 네이밍 규칙이 있다.</p>
<blockquote>
<p><a href="https://www.red-gate.com/blog/database-devops/flyway-naming-patterns-matter">Naming Convetion 관련 링크</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yangwon-park/post/4f150493-d406-4d0d-ad7b-b4620cbd1880/image.png" alt=""></p>
<ul>
<li><p>공통</p>
<ul>
<li>V, U, R (PREFIX) + Version, __ (언더바 2개) + Description + .sql 형식을 따라야함</li>
</ul>
</li>
<li><p>ex) V202407021214__init.sql</p>
</li>
</ul>
<ol>
<li>Versioned Migrations<ul>
<li>주로 사용하게 되는 기능</li>
<li>흔하게들 작업하는 내용을 SQL로 작성</li>
</ul>
</li>
<li>Undo Migrations<ul>
<li>유료 기능</li>
<li>기존에 적용된 Migration을 롤백할 수 있음</li>
<li>V로 롤백하는 것보다 변경 추적 및 테스트에 용이함</li>
</ul>
</li>
<li>Repeatable Migration<ul>
<li>반복적으로 실행시키고 싶은 SQL을 작성</li>
<li>주로 뷰, 프로시저, 함수, 트러기와 같은 SQL을 작성</li>
</ul>
</li>
</ol>
<h2 id="step">Step</h2>
<p>현재 진행 중인 프로젝트에 Flyway를 적용하려면 아래와 같은 과정을 거쳐야한다.</p>
<ol>
<li><p>build.gradle 의존성 추가</p>
<pre><code class="language-kotlin"> implementation(&quot;org.flywaydb:flyway-core&quot;)
 runtimeOnly(&quot;org.flywaydb:flyway-sqlserver&quot;)</code></pre>
</li>
<li><p>환경 설정</p>
<pre><code class="language-yaml">flyway:
 enabled: true
 baseline-on-migrate: true #flyway_schema_history 자동 생성
 baseline-version: 1       # 시작 버전
 locations: classpath:db/migration # default</code></pre>
</li>
<li><p>아래와 같이 path directory 설정 (flyway.locations)</p>
 <img src="https://velog.velcdn.com/images/yangwon-park/post/5eb2aef2-9460-419e-a7a8-27477a8f9630/image.png" width="70%">
 - 이미지와 같이 Active Profile 기준으로 sql을 별도로 관리하고 싶다면 `classpath:db/migration/local`과 같이 yml 파일에 설정해줘야 함</li>
<li><p>DB 형상 관리를 추적하기 위한 SQL 스크립트 작성</p>
<img src="https://velog.velcdn.com/images/yangwon-park/post/7e56ccaa-2ef2-41ac-a2f0-e8654aaedbed/image.png" width="100%">
```sql
-- V202406111208__init.sql 파일 내부

<p>-- init Flyway SQL Script
-- 기존에 존재하는 DB에 Baseline을 설정해주기 위한 공백 파일</p>
<pre><code></code></pre></li>
</ol>
<ul>
<li>SQL 스크립트가 존재하지 않는 경우 서버가 실행되지 않기에 실제 DB에 변동 사항이 없더라도 <strong>반드시 1개의 SQL 스크립트</strong>가 존재해야 함</li>
</ul>
<ol start="5">
<li>서버 실행 후 flyway_schema_history 테이블 확인<ul>
<li>아래와 같이 DB 형상 관리를 위한 Table이 생성되어있는 것을 확인할 수 있음
<img src="https://velog.velcdn.com/images/yangwon-park/post/82d122cd-f049-4c06-9992-378a6d4c6b06/image.png" alt=""></li>
<li>첫 번째 로우를 통해 Flyway Baseline이 설정된 것을 확인할 수 있고, 두 번째 로우를 통해 해당 프로젝트의 DB의 형상 관리를 시작했음을 알 수 있음</li>
</ul>
</li>
</ol>
<h2 id="주의할-점">주의할 점</h2>
<ol start="0">
<li>상단에 언급한 것처럼 DB를 추적하기 위해서 반드시 비어있는 SQL 스크립트라도 작성을 해줘야 함</li>
<li>한 번 생성한 SQL 스크립트는 ** 지워선 안 됨**<ul>
<li>Flyway는 스크립트 파일의 Version에 적힌 숫자값과 <code>flyway_schema_history</code> 테이블에 저장된 <code>checksum</code> 값을 보고 형상 관리를 수행하기에 삭제를 하면 추적이 불가능해짐</li>
</ul>
</li>
<li>Version의 값은 오름차순으로 적어야 함<ul>
<li>형상 관리를 추적할 때 Version의 값이 작은 순부터 큰 순으로 추적을 함</li>
<li>이 때, Version의 값이 오름차순이 아닌 경우 에러가 발생함</li>
</ul>
</li>
<li>Version은 단순한 숫자보단 복잡한 숫자로 표기를 추천<ul>
<li>혼자 관리하는 경우 1, 2 이렇게 간단한 숫자로 표기해도 중복의 우려가 적겠지만 여러명이 함께 관리하는 프로젝트인 경우 Version의 중복 가능성을 배제하지 못함</li>
<li>현재 우리팀의 경우 <code>연도, 월, 일, 시간, 분</code>을 합쳐 Version으로 표기</li>
</ul>
</li>
<li>이유는 모르겠으나 가끔씩 Version이 가지고 있는 checksum의 값이 Flyway가 내부적으로 가지고 있는 값과 일치하지 않아 에러가 발생함<ul>
<li>아직까지 이유를 찾지 못하였으나 위 문제가 발생하는 경우, 해당 Version의 checksum 값을 서버 로그에 찍힌 값으로 강제 업데이트 후 적용 시 정상 동작</li>
</ul>
</li>
</ol>
<h2 id="마치며">마치며</h2>
<p>현재까지 Flywaya를 적용하고 나서의 만족도는 매우 크다. 물론 아직까지 적용 초기 단계이고 모든 팀원들이 DB 변경에 참여하고 있지는 않아 발견되지 않은 문제들이 많겠지만, 개발 후 실서버 배포 시점에 항상 느꼈던 스트레스가 현저히 줄었고 Human Risk 또한 확실하게 줄어든게 체감이 된다.</p>
<blockquote>
<p>Flyway 도입을 망설이고 계시거나 도입을 하려고 하지만 선뜻 방법을 모르시겠는 개발자분들에게 도움이 되셨으면 좋겠습니다!!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Entity 연관 관계에서의 Set vs List]]></title>
            <link>https://velog.io/@yangwon-park/Entity-%EC%97%B0%EA%B4%80-%EA%B4%80%EA%B3%84%EC%97%90%EC%84%9C%EC%9D%98-Set-vs-List</link>
            <guid>https://velog.io/@yangwon-park/Entity-%EC%97%B0%EA%B4%80-%EA%B4%80%EA%B3%84%EC%97%90%EC%84%9C%EC%9D%98-Set-vs-List</guid>
            <pubDate>Thu, 09 May 2024 08:51:27 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>jpa를 사용하면서 일대다(다대일)의 연관 관계에 있는 Entity를 구현하는 경우, 항상 아래와 같이 <code>List</code>를 사용하여 구현하였다.</p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;targetEntity&quot;)
List&lt;Entity&gt; entities = new ArraryList()</code></pre>
<p>jpa는 java에서 기본적으로 제공하는 Collection, List, Set, Map을 지원하며 이를 Entity 연관 관계를 매핑하거나 ElementCollection을 사용할 때 이를 적용할 수 있다.
하지만 처음 jpa를 공부하고, 현업에서 사용하고 있는 지금까지 List만으로 Entity를 설계하였다. 그러다 문득 코드를 짜다 보면 성능상의 이유로 <code>List</code>가 아닌 <code>Set</code>을 사용하는 경우가 있다는 것이 생각났고 Entity를 설계하는 과정에서 <code>List</code>만을 쓰는 이유가 궁금해졌다.</p>
<h1 id="list를-쓰는-이유">List를 쓰는 이유</h1>
<p>가장 큰 이유는 Set과 List의 근본적인 차이를 이해한다면 쉽게 파악할 수 있었다.</p>
<blockquote>
<p>Set: 중복 불가
List: 중복 허용</p>
</blockquote>
<p>얼핏 보기엔 Set을 사용하여 중복 체크 여부를 생략하고 값 타입 컬렉션을 사용할 때 발생하는 모든 Row [Delete -&gt; Save] 과정 또한 해결할 수 있어 좋은 선택이 되는 것 같았다. 하지만 실제로 jpa를 쓰다 보면 대부분의 Entity 연관 관계는 <code>Lazy Loading</code>으로 돼있는데 이 경우에 문제가 발생한다.
풀어서 설명하자면 Set의 경우 기존에 가지고 있던 Entity(값 타입)에 중복된 데이터가 있는지 비교를 해야 하는데 이 시점에 Set에 있는 모든 데이터를 로딩해야만 하고 이 때 Proxy가 강제로 초기화된다. 결론적으로는 <code>Lazy Loading</code>을 사용할 수가 없어 성능적으로 좋지 못한 결과를 얻게 된다.</p>
<h1 id="결론">결론</h1>
<p>하나를 쓰더라도 제대로 알고 쓰는게 중요한 것 같다. 단순히 Set의 장점만을 생각하여 코드를 구현하는 것이 아니라 왜 List를 써야하는지, 썼을 때 어떤 이점을 가지는지 명확하게 알 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Enum Class: enumEntries]]></title>
            <link>https://velog.io/@yangwon-park/Enum-Class-enumEntries</link>
            <guid>https://velog.io/@yangwon-park/Enum-Class-enumEntries</guid>
            <pubDate>Thu, 09 May 2024 06:49:08 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>사내 프로젝트의 Kotlin Migration을 진행하던 도중 Java와는 달리 새롭게 알게 된 Enum 전체 요소를 가져오는 방식과 그 차이에 대하여 정리하고자 한다.</p>
<h1 id="kotlin의-요소-탐색">Kotlin의 요소 탐색</h1>
<p>java에서 Enum의 전체 요소를 가져오는 경우 흔히 아래와 같은 식의 방법을 사용한다.</p>
<pre><code class="language-java">Enum.values()</code></pre>
<p>하지만 이를 kotlin에서 그대로 사용하면 IDE에서 아래와 같은 경고(?)가 뜬다.</p>
<p align="center"><img src="https://velog.velcdn.com/images/yangwon-park/post/d15b3083-8bdc-4cd3-b2d8-bf4d8ecf3d86/image.png" alt="enum values warning" width="100%"></p>
친절하게도 values() 메소드를 entries() 메소드로 대체하기를 추천해준다. 대체 두 방식의 차이가 무엇이고 어떠한 이점이 있길래 IDE에서 이렇게까지 추천해주는지 알아보자.

<h2 id="values">values()</h2>
<p>java와 kotlin 모두 Enum.values()를 사용하게 되면 Array를 반환하게 된다. Array를 반환하는 부분에 있어서 무슨 문제가 있는지 처음에는 잘 와닿지 않았으나 정리된 글을 보니 아래와 같이 제법 심각한 문제들이 존재하였다.</p>
<ul>
<li><strong>호출이 일어날 때 마다 배열을 할당하고 복제</strong>하여 성능상의 이슈가 있음</li>
<li><strong>Mutable</strong>하여 배열 자체를 변경 가능하며 Array 자체가 java 및 kotlin Collections와 호환이 좋지 않아 유연성이 떨어짐</li>
<li>kotlin에서 유용하게 쓸 수 있는 확장 함수의 개념을 적용하기에 Array는 유연하지 못함</li>
</ul>
<p>위와 같은 단점들로 인하여 새롭게 등장하게 된 메소드가 바로 entries()이다.</p>
<h2 id="entries">entries()</h2>
<p>entries() 메소드의 경우 kotlin 1.8.2에서 실험적인 모델로 추가되었으며 (1.9 이후 버전의 경우 stable) List$\lt$E$\gt$를 확장한  EnumEntries$\lt$E$\gt$ 인터페이스를 반환해준다.</p>
<ul>
<li>사전에 할당되어 있는 List를 반환하기에 항상 동일한 List를 반환하여 values()보다 성능이 좋음</li>
<li><strong>Immutable</strong></li>
<li>Array가 아닌 EnumEntries를 변환해주기에 List의 확장 함수 및 Enum을 위한 커스텀 확장 기능을 쉽게 구현할 수 있음</li>
</ul>
<h1 id="결론">결론</h1>
<p>직접 차이를 비교하고 공부해보니 왜 values() 대신 entries()를 사용해야 하는지 확실하게 와닿았다. 아직 kotlin에 대한 이해도가 높지 않은 채로 코드를 작성하고 있는 단계이지만 지금처럼 하나 하나 공부하여 전반적인 kotlin의 이해도를 높여나가야겠다.</p>
<blockquote>
<p>참고 포스팅
<a href="https://engineering.teknasyon.com/kotlin-enums-replace-values-with-entries-bbc91caffb2a">Kotlin Enums — Replace values() with entries</a>
<a href="https://kotlinlang.org/docs/enum-classes.html#working-with-enum-constants">Kotlin 공식 문서</a>
<a href="https://velog.io/@shjung53/kotlin-kotlin-1.9.0%EC%9D%98-enum-class%EC%9D%98-entries">참고 블로그</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[지도 클러스터링 적용]]></title>
            <link>https://velog.io/@yangwon-park/%EC%A7%80%EB%8F%84-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81-%EC%A0%81%EC%9A%A9-1</link>
            <guid>https://velog.io/@yangwon-park/%EC%A7%80%EB%8F%84-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EB%A7%81-%EC%A0%81%EC%9A%A9-1</guid>
            <pubDate>Thu, 21 Mar 2024 05:54:36 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<ul>
<li>서비스하고 있는 앱에 회원들이 늘어남에 따라 지도에 노출시키는 회원들의 마커를 로드하는 속도가 많이 느려지게 되었다. 단순 백엔드 API에서 데이터를 호출하는 문제뿐만 아니라 마커 하나 하나를 앱 단에 그려주는 과정에서 소요되는 시간 역시 UX 차원에서 좋지 못했기에 <strong>클러스터링</strong>을 적용하여 UI/UX를 개선시키고자 프로젝트를 진행하게 되었다.</li>
</ul>
<blockquote>
<p>클러스터링이란?
쉽게 말해 유사한 성격(?)을 가진 데이터를 하나의 군집으로 묶어 구성하는 것</p>
</blockquote>
<h1 id="클러스터링-적용하기">클러스터링 적용하기</h1>
<h2 id="지도-데이터에-알맞은-클러스터링-알고리즘-선정">지도 데이터에 알맞은 클러스터링 알고리즘 선정</h2>
<h3 id="0-ml-라이브러리-적용">0. ML 라이브러리 적용</h3>
<p>현재 백엔드는 SpringBoot를 사용하고 있었기에 ML 라이브러리로 난 Smile을 선택하였다.</p>
<blockquote>
<p><a href="https://haifengl.github.io/">Smile 공식 홈페이지</a></p>
</blockquote>
<h3 id="1-주소값-기준으로-데이터-묶기">1. 주소값 기준으로 데이터 묶기</h3>
<p>이 방식은 별도의 알고리즘을 적용하지 않고 시도별 행정 구역의 지자체 좌푯값과 행정 구역, 회원들이 등록한 주소 데이터를 기준으로 클러스터를 생성한 방법이다.</p>
<ul>
<li><p>시도별 지자체 좌푯값과 행정 구역 데이터를 미리 별도의 테이블에 저장하고 지도 레벨 별 행정 구역 기준을 지정 후 지정한 기준으로 주소 데이터를 묶어 지도 상에 그려주는 방식으로 구현</p>
</li>
<li><p>장점</p>
<ul>
<li>별도의 복잡한 알고리즘을 고려할 필요가 없음</li>
</ul>
</li>
<li><p>단점</p>
<ul>
<li>사전 데이터를 별도로 관리해야함</li>
<li>행정 구역 명칭이 변경될 시 데이터 모두 다 변경해야 함</li>
<li>이로 인해 유지보수가 힘들며 데이터에 종속적임</li>
<li>지자체 좌푯값에 클러스터를 생성하므로 하위 레벨로 접근 시 데이터가 미노출될 우려가 있음</li>
</ul>
</li>
<li><p>결론        </p>
<ul>
<li>데이터를 정제하고 관리하는 시점부터 뭔가 잘못됐다는 것을 크게 느꼈으며 앞서 언급한 단점이 너무나 컸기에 테스트까지만 진행하고 실제로 적용하진 않음</li>
</ul>
</li>
</ul>
<h3 id="2-kmeans-알고리즘-활용">2. KMeans 알고리즘 활용</h3>
<p>클러스터링 알고리즘을 활용하고자 했을 시점에 가장 먼저 떠오른 것은 KMeans 알고리즘이었다.</p>
<blockquote>
<p>Kmeans 알고리즘이란?
쉽게 말해 주어진 데이터를 K개의 클러스터로 묶는 방식으로 목표 클러스터 개수인 K값에 따라 결과가 크게 달라짐
Hyper Parameter: K (목표 클러스터 개수)
<a href="https://ko.wikipedia.org/wiki/K-%ED%8F%89%EA%B7%A0_%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98">위키백과 참고</a></p>
</blockquote>
<p>Smile을 활용하여 KMeans를 Java/Kotlin에서 사용하는 간단한 방식은 아래와 같다.</p>
<pre><code class="language-java">import smile.clustering.KMeans;

private static List&lt;ClusteringDto&gt; getClusterInfoWithKMeans(double[][] coordinates) {
        log.warn(&quot;Start KMeans Clustering&quot;);

        int maxK = 10;
        double[] distortions = new double[maxK];
        double lastDistortion = Double.MAX_VALUE;

        List&lt;KMeans&gt; kMeansResults = new ArrayList&lt;&gt;();
        List&lt;ClusteringDto&gt; clustering = new ArrayList&lt;&gt;();

        // 각 K에 대한 클러스터링 수행 및 결과 저장
        for (int k = 2; k &lt;= maxK; k++) {
            KMeans kMeans = KMeans.fit(coordinates, k);
            kMeansResults.add(kMeans);

            distortions[k - 1] = kMeans.distortion;

            double distortionDrop = lastDistortion - distortions[k - 1];

            log.warn(&quot;distortion :: {}&quot;, distortionDrop);

            if (distortionDrop &lt; 0.10 * lastDistortion) {
                int optimalK = k - 1;

                KMeans optimalKMeans = kMeansResults.get(optimalK - 1);

                double[][] centroids = optimalKMeans.centroids;
                int[] sizes = optimalKMeans.size;

                getPointsInClusterWithKMeans(coordinates, optimalKMeans, k);

                for (int i = 0; i &lt; centroids.length; i++) {
                        ClusteringDTO.builder()
                            .count(sizes[i])
                            .latitude(centroids[i][0])
                            .longitude(centroids[i][1])
                            .build();
                }

                return clustering;
            }

            lastDistortion = distortions[k - 1];
        }

        return clustering;
    }

    private static void getPointsInClusterWithKMeans(double[][] coordinates, KMeans optimalKMeans, int k) {
        int kk = optimalKMeans.k;
        int[] labels = optimalKMeans.y;

        ArrayList&lt;double[]&gt;[] clusters = new ArrayList[kk];

        for (int i = 0; i &lt; k; i++) {
            clusters[i] = new ArrayList&lt;&gt;();
        }

        for (int i = 0; i &lt; coordinates.length; i++) {
            int clusterId = labels[i];

            double[] point = new double[2];
            point[0] = coordinates[i][0];
            point[1] = coordinates[i][1];

            clusters[clusterId].add(point);
        }
    }</code></pre>
<p>최적의 K값을 선정하기 위해 Distortion(왜곡)의 차이가 가장 작은 시점의 K값을 도출한 후 (Elbow Method), 이 시점에 존재하는 각각의 클러스터 중심점과 크기를 가진 객체를<code>getPointsInClusterWithKmeans</code> 메소드로 생성하고 이를 최종 반환값으로 사용한다.
(사실 후술하게 되는 단점으로 인하여 유지 보수가 이루어지지 않은 프로토타입에 가까운 코드이기에 고도화 및 리팩토링이 필요한 상태이다)
위 코드를 활용하여 지도에 클러스터를 적용하였으나 큰 단점이 있다는 것을 적용한 후에 깨달았다. KMeans 알고리즘의 특성상 <span style="color: red"><strong>알고리즘을 동일한 데이터셋으로 호출하더라도 동일한 결과를 보장받을 수 없다.</strong> </span> 회원 입장에서 지도에 접근할 때 마다 다른 클러스터링을 접하게 된다면 UX 차원에서 좋지 못할 것이라는 생각이 크게 들었다.</p>
<ul>
<li>장점<ul>
<li>1번의 방식과 달리 사전 데이터를 관리할 필요없음</li>
</ul>
</li>
<li>단점<ul>
<li>Elbow Method가 적용되어 클러스터 생성에 소요되는 시간이 보다 길게 소요됨</li>
<li>선정된 K값에 따라 클러스터의 값이 크게 좌우됨</li>
<li><span style="color: red"><strong>알고리즘을 실행할 때 마다 동일한 클러스터를 생성한다고 보장할 수 없음</strong></span></li>
</ul>
</li>
<li>결론<ul>
<li>동일한 클러스터링 결과를 노출할 수 없다는 문제가 너무 크게 와닿아 사용하지 않음</li>
</ul>
</li>
</ul>
<h3 id="3-dbscan">3. DBSCAN</h3>
<p>KMeans를 활용하는 경우 UX를 해치는 문제가 있기에 이를 해결하고자 도입한 알고리즘은 DBSCAN이다.</p>
<blockquote>
<p>DBSCAN 알고리즘이란?
주어진 데이터셋의 밀도를 기반으로 서로 더 가까운 데이터 포인트끼리 군집화 하는 알고리즘
Hyper Parameter: Epsilon, minPts
Epsilon: 클러스터를 이루는 최소 거리
minPts: 클러스터를 이루는데 필요한 최소 데이터 포인트 수
noise(outlier): 클러스터에 속하지 못한 이상치</p>
</blockquote>
<p>KMeans와는 달리 DBSCAN은 주어진 데이터셋과 Hyper Paramter가 같은 경우 항상 같은 결괏값을 반환한다. 또한 위치 데이터의 경우 데이터들의 밀도와 분포 모양이 불규칙한데 이러한 경우에 더욱 효과적이게 사용할 수 있다.
Kotlin으로 구현한 smile.clustering.DBSCAN의 간략한 코드는 아래와 같다.</p>
<pre><code class="language-kotlin">import smile.clustering.DBSCAN
private fun convertClusterInfoToDto(
        coordinates: Array&lt;DoubleArray&gt;,
        epsilon: Double
    ): MutableList&lt;ClusteringDto&gt; {
        logger.warn { &quot;Start DBSCAN Clustering&quot; }

        val minPts = 1;
        val clustering: MutableList&lt;ClusteringDto&gt; = mutableListOf()

        val result: DBSCAN&lt;DoubleArray&gt; = DBSCAN.fit(coordinates, EuclideanDistance(), minPts, epsilon)

        val labels: IntArray = result.y;

        logger.info { &quot;DBSCAN Result :: $result&quot; }

        val centroidMap = calculateClusterCentroidsWithDBSCAN(labels, coordinates)

        for ((_, value) in centroidMap) {
            clustering.add(value)
        }

        return clustering
    }

// outlier를 포함한 centroid 계산
    private fun calculateClusterCentroidsWithDBSCAN(
        labels: IntArray,
        dataset: Array&lt;DoubleArray&gt;
    ): Map&lt;Int, ClusteringDto&gt; {
        val clusters: MutableMap&lt;Int, MutableList&lt;DoubleArray&gt;&gt; = mutableMapOf()
        val centroids: MutableMap&lt;Int, ClusteringDto&gt; = mutableMapOf()
        var outlierId = -1

        // outlier 포함
        for (i in labels.indices) {
            val clusterId = labels[i]

            // outlier: Int.MAX_VALUE (2147483647)로 clusterId가 들어옴
            if (clusterId != Int.MAX_VALUE) {
                clusters.getOrPut(clusterId) { mutableListOf() }.add(dataset[i])
            } else {
                clusters.getOrPut(outlierId--) { mutableListOf() }.add(dataset[i])
            }
        }

        // 좌표의 평균을 활용하여 centroid 구함
        for ((key, value) in clusters) {
            val sum = DoubleArray(dataset[0].size) { 0.0 } // DoubleArray 0.0으로 초기화
            val clusterSize = value.size

            value.forEach {
                it.forEachIndexed { i, coordinate -&gt;
                    sum[i] += coordinate
                }
            }

            sum.forEachIndexed { i, v -&gt;
                sum[i] = v / clusterSize
            }

            centroids[key] = ClusteringDto(
                count = clusterSize,
                latitude = sum[0],
                longitude = sum[1]
            )
        }

        return centroids
    }</code></pre>
<p>DBSCAN의 경우 outlier(noise, 클러스터에 포함되지 못한 데이터)가 존재하는데, 위치 데이터의 경우 이러한 데이터들도 노출되어야 하기에 해당 데이터들 또한 클러스터링 결괏값에 포함시켜줬다.</p>
<pre><code class="language-kotlin">// 위 코드 중 이부분
for (i in labels.indices) {
            val clusterId = labels[i]

            // outlier: Int.MAX_VALUE (2147483647)로 clusterId가 들어옴
            if (clusterId != Int.MAX_VALUE) {
                clusters.getOrPut(clusterId) { mutableListOf() }.add(dataset[i])
            } else {
                clusters.getOrPut(outlierId--) { mutableListOf() }.add(dataset[i])
            }
        }</code></pre>
<p>smile의 경우 outlier의 clusterId를 <code>INT.MAX_VALUE</code>로 지정하며 clusterId값이 이런 경우에 관하여 <code>-1</code>부터 음수의 clusterId를 부여함으로써 각각의 클러스터로 취급할 수 있게 구현하였다.
또한 DBSCAN의 경우 별도의 centroids값을 지원해주지 않기 때문에 직접 해당 클러스터 내부 데이터의 좌푯값을 활용하여 평균값을 구하고 이를 클러스터의 중심 좌표로 활용하였다.</p>
<ul>
<li>장점<ul>
<li>동일한 데이터셋에 대하여 동일한 클러스터 결과를 반환함</li>
<li>밀도 기반으로 다룬 데이터라 위치 데이터에 적합한 클러스터링 결과를 도출 가능</li>
<li></li>
</ul>
</li>
<li>단점<ul>
<li>smile 알고리즘에서 centroid를 지원을 하지 않기에 centroid를 구하는 로직을 별도로 작성해야함</li>
<li>outlier를 별도의 클러스터로 다루는 로직을 별도로 작성해야함</li>
<li>디바이스별 지도 레벨에 맞는 최적의 epsilon을 찾기 어려움</li>
</ul>
</li>
</ul>
<h1 id="결론">결론</h1>
<p>최종적으로 3가지 방법 중 <code>DBSCAN</code>을 활용한 방법을 선택하였고 이를 실제 서비스에 적용하여 <code>최대 4400%</code>의 성능 개선율을 이루었다.
실제 API의 호출에서의 차이보다 앱 단의 차이가 훨씬 컸으며 단순히 백엔드 적인 성능 개선을 뛰어넘어 전체 비즈니스 차원의 성능 개선을 이루어서 개인적으로 만족스러운 프로젝트 결과를 얻었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ElementCollection 대체하기 (2)]]></title>
            <link>https://velog.io/@yangwon-park/ElementCollection-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-2</link>
            <guid>https://velog.io/@yangwon-park/ElementCollection-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-2</guid>
            <pubDate>Wed, 21 Feb 2024 01:24:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="읽기에-앞서">읽기에 앞서</h1>
<ul>
<li>이전 포스팅을 먼저 보고 오시는 걸 추천드립니다!<ul>
<li><strong><em><a href="https://velog.io/@yangwon-park/ElementCollection-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-1">관련 포스팅</a></em></strong></li>
</ul>
</li>
</ul>
<h1 id="대체하며-마주하게-된-문제">대체하며 마주하게 된 문제</h1>
<ul>
<li>@ElementCollection을 변경하고 별도의 엔티티로 이를 대체하면서 정말 많은 고생을 하였다.</li>
</ul>
<h2 id="1-만들어진-기능들을-생각보다-많이-리팩토링해야-함">1. 만들어진 기능들을 생각보다 많이 리팩토링해야 함</h2>
<ul>
<li>기존의 방식을 버리고 새로운 방식을 도입해서 그런지 기존의 매장 save, update 로직 및 프론트 단 모두 대대적인 리팩토링 작업이 필요했다.</li>
<li>사실 <strong>@ElementCollection의 문제점을 알고도 몸소 직접 겪어보고자</strong> 이를 사용했었기에 바꿔야한다는 것은 인지하고 있었으나, 막상 이를 시도하려니 연관 관계 매핑부터 기존의 비즈니스 로직까지 정말 많은 부분을 수정해야 했다.</li>
</ul>
<h2 id="2-update-기능에서-조건-분기가-상당히-까다로움">2. Update 기능에서 조건 분기가 상당히 까다로움</h2>
<ul>
<li>Save 기능은 새로 만든 엔티티들을 기준으로 발생할 수 있는 예외만 잘 처리하면 크게 어려운 것이 없으나 문제는 Update 쪽에 있다.</li>
<li>Update 로직의 조건 분기는 아래와 같다.</li>
</ul>
<pre><code class="language-java">/*
    StoreService 내부
*/    

/*
        기존의 매장과 매장의 휴무 데이터를 갖고 있는 리스트
 */
List&lt;DaysOfStore&gt; prevDaysOfStoreList = findStore.getDaysOfStoreList();

/*
    체크박스가 하나도 체크돼있지 않은데 기존의 휴무는 있는 경우
    휴무가 존재했으나 없앴다 =&gt; 매장에 등록된 휴무 데이터를 다 삭제함
 */
if (daysIdList == null) {
    if (prevDaysOfStoreList != null) {
        daysRepository.deleteByStoreId(findStore.getId());
    }
} else {
    List&lt;Days&gt; daysList = daysIdList.stream().map(id -&gt; daysRepository.findById(id)
                                                  .orElseThrow(() -&gt; new IllegalArgumentException(&quot;잘못된 요일값입니다.&quot;))).collect(Collectors.toList());

    if (prevDaysOfStoreList.size() == daysList.size()) {
        for (int i = 0; i &lt; prevDaysOfStoreList.size(); i++) {
            prevDaysOfStoreList.get(i).updateDays(daysList.get(i));
        }
    }

    if (prevDaysOfStoreList.size() &lt; daysList.size()) {
        int i;

        for (i = 0; i &lt; prevDaysOfStoreList.size(); i++) {
            prevDaysOfStoreList.get(i).updateDays(daysList.get(i));
        }

        for (int j = i; j &lt; daysList.size(); j++) {
            linkDaysAndStore(daysList.get(j), findStore);
        }
    }

    // 현재 여기 버그발생
    if (prevDaysOfStoreList.size() &gt; daysList.size()) {
        int i;

        for (i = 0; i &lt; daysList.size(); i++) {
            prevDaysOfStoreList.get(i).updateDays(daysList.get(i));
        }

        for (int j = i; j &lt; prevDaysOfStoreList.size(); j++) {
            daysRepository.deleteByDaysIdLinkedDaysOfStore(prevDaysOfStoreList.get(j).getDays().getId());
        }
    }
}</code></pre>
<h3 id="2-1-기존의-휴무와-새로-입력된-휴무의-개수가-같거나-새로-입력된-휴무의-개수가-더-많다면">2-1. 기존의 휴무와 새로 입력된 휴무의 개수가 같거나 새로 입력된 휴무의 개수가 더 많다면?</h3>
<ul>
<li>매장의 휴무는 일~토까지 총 7개이다.(일요일이 1번, 토요일이 6번) </li>
<li>만약 매장의 휴무가 <strong>[토, 일]</strong>이었다가 <strong>[금, 토, 일]</strong>로 변경됐다고 가정해보자.</li>
<li>단순하게 생각하면 기존의 데이터에 <strong>[금]</strong>에 해당하는 값이 DaysOfStore에 들어간다고 느낀다. 하지만 이를 정말 단순하게 구현하면 <strong>[금 -&gt; 일], [토 -&gt; 금], [토]</strong> 이렇게 총 3번의 쿼리가 날라간다.</li>
<li>즉, Store에서 가지고 있던 DaysOfStoreList의 요소들의 순서가 새로 들어온 Days와 일치하지 않기 때문에 값이 다르면 매번 Update 쿼리가 날라가거나 Delete 쿼리가 날라갈 수 있다.</li>
</ul>
<h3 id="2-2-만약-기존의-휴무보다-새로-입력된-휴무의-개수가-더-작다면">2-2. 만약 기존의 휴무보다 새로 입력된 휴무의 개수가 더 작다면?</h3>
<ul>
<li>이 경우엔 더 문제인게, 단순하게 구현하면 데이터 수정조차 똑바로 이루어지지 않는다.</li>
<li>기존 휴무가 <strong>[수, 목, 금, 토]</strong>인 경우 이를 <strong>[수, 목]</strong>으로 변경하는 경우엔 정상적으로 기능이 작동한다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/207483718-70bcb66f-49ef-46ff-ae03-00b37ccec7e9.jpg" alt="휴무" width="70%"></p>

<ul>
<li>앞의 2개의 데이터는 같은 Days를 의미하므로 업데이트 쿼리가 나가지 않고 (정상), <strong>[금, 토]</strong> 2개의 데이터는 지워졌으므로 DaysOfStore 테이블에 Delete 쿼리가 2번 날라가면서 정상적으로 삭제된다. (정상)</li>
<li>문제는 기존 휴무의 데이터를 순서에 맞게 줄이는 것이 아니라 다른 식으로 줄이면 발생한다.</li>
<li>예를 들어  <strong>[수, 목, 금, 토]</strong>인 경우 이를 <strong>[금, 토]</strong>로 변경하는 경우 문제가 발생한다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/207484665-507726c7-eaf1-46cb-be98-b1cfcca84327.jpg" alt="휴무" width="70%"></p>

<ul>
<li>현재 내가 사용하는 로직을 보면 연결된 DaysOfStoreList의 요소 삭제 조건을 Days의 ID로 사용하고 있는데, 위의 상황에서 보다시피 같은 Days가 존재하는 경우에 기존값만 삭제하는 것이 아닌 새로 바뀐 값마저 삭제하고 있다. 이로 인해 최종적으로 결과가 <strong>NULL</strong>이 반환된다.</li>
<li>또한 만약 NULL이 되지 않게 뒤의 <strong>[금, 토]</strong>만 remove하는 경우, 쿼리가 Update 2번, Delete 2번 총 4번의 쿼리가 날라가므로 성능상으로도 이로울 것이 하나도 없다.</li>
</ul>
<br />

<h1 id="위의-문제들로-인하여-고민하게-된-점">위의 문제들로 인하여 고민하게 된 점</h1>
<ul>
<li>@ElementCollection을 사용하지 않고 대체하는 것이 정말 올바른 설계인가에 대한 고민에 빠졌다.</li>
<li>사실 앞서 언급한 다양한 문제점이 있기에 가급적으로 사용하지 않는 것이 좋은 건 맞겠지만, 만약 정말 단순히 값만 담을 별 의미가 없는 필드라면 <strong>많은 에너지를 쏟지 않고 @ElementCollection을 사용하는 것이 더 좋지 않나</strong>라는 생각이 든다.</li>
<li>내가 아직 올바른 방향을 잡지 못하였기에 코드도 난잡하고 어려워 그런지 유지보수에서도 좋지 못한 느낌이고 생각보다 Update의 조건 분기가 너무 까다롭기에 그냥 <strong>통째로 삭제하고 새로 값을 넣는 것이 오히려 더 좋은 방법이지 않을까?</strong>라는 느낌...</li>
<li>결과적으론 조건 분기만 잘 잡는다면 DB와의 통신 횟수를 기존보다 확실하게 줄일 수 있는 것도 맞고, 현재 프로젝트에서 휴무는 아무런 기능도 하지 않는 데이터가 아니기에 (휴무 기준 조회 등등) 데이터 추적을 위해서라도 별도의 엔티티를 사용하는 방향을 고집하였다.</li>
</ul>
<br />

<h1 id="결론">결론</h1>
<ul>
<li>한계점이 있는 것을 알기에도 이 기능을 사용한 것은 내가 직접 이러한 문제점들을 겪어보고 개선하는 과정이 필요하다고 생각됐기 때문이다.</li>
<li>실제 나가는 쿼리들과 식별자의 부재로 인한 불편사항들을 몸소 겪으면서 서비스의 성능 개선에 대하여 더욱 진지하게 고민하게 되었고, 의지를 가져 이를 개선하는 것 또한 즐길 수 있게 되었다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ElementCollection 대체하기 (1)]]></title>
            <link>https://velog.io/@yangwon-park/ElementCollection-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@yangwon-park/ElementCollection-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Wed, 21 Feb 2024 01:22:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="elementcollection">ElementCollection</h1>
<ul>
<li>JPA에서 <strong>값타입을 컬렉션에 담아서 사용</strong>하는 경우, 해당 컬렉션을 <code>ElemenctCollection</code> 이라고 한다.</li>
<li>여기서 값타입이란 Integer, String과 같은 java의 기본 자료형 또는 임베디드 타입을 의미한다.</li>
<li>현재 진행 중인 프로젝트에서 매장(Store)의 휴무일(offDays)을 ElementCollection으로 사용하고 있다.</li>
</ul>
<pre><code class="language-java">@Entity
public class Store extends BaseEntity {

    // ...

    @ElementCollection(fetch = FetchType.LAZY)  // default: LAZY라 사실 명시할 필요없음
    private List&lt;String&gt; offDays = new ArrayList&lt;&gt;();

    // ...
}</code></pre>
<ul>
<li><p>위처럼 @ElementCollection 애노테이션만 붙여주면 해당 필드가 Collection이라는 것을 알게 되고, RDB에서 이 필드를 관리할 별도의 테이블을 생성해준다.</p>
</li>
<li><p>함께 @CollectionTable을 사용하여 매핑할 테이블의 정보를 직접 설정해줄 수도 있다.</p>
</li>
<li><p>보다싶이 애노테이션 하나 혹은 둘 만으로 RDB가 다루지 못하는 Colleciton 형태의 데이터를 쉽게 처리할 수 있게 도와주는 편리한 기능이다.</p>
</li>
<li><p>하지만 ElementCollection은 <strong>문제점</strong>을 몇가지 가지고 있고, 나 또한 이를 사용하면서 해당 문제들로 인한 <strong>성능 개선의 필요성</strong>을 크게 느꼈기에 ElementCollection을 대체하고자 한다.</p>
</li>
</ul>
<br/>

<h1 id="elemenctcollection의-한계">ElemenctCollection의 한계</h1>
<h2 id="1-엔티티가-값타입이기-때문에-식별자-개념이-없다">1. 엔티티가 값타입이기 때문에 식별자 개념이 없다.</h2>
<ul>
<li><code>식별자</code>가 존재하지 않기 때문에 값이 변경되는 경우, 이를 추적하는 것이 어렵다.</li>
<li>사실 이 문제점으로 인한 에로사항은 크게 느끼지 못하였다. (추적한 적이 아직 없음)</li>
<li>하지만 식별자의 부재로 인한 문제점은 겪어보지 않아도 충분히 와닿는다.</li>
</ul>
<h2 id="2-식별자가-별도로-없으므로-pk를-구성하는-경우-조건에-맞게-모든-컬럼을-묶어서-pk를-구성해야-한다">2. 식별자가 별도로 없으므로 PK를 구성하는 경우 조건에 맞게 모든 컬럼을 묶어서 PK를 구성해야 한다.</h2>
<ul>
<li>값타입을 저장할 @Collection 테이블의 구성을 보면 아래와 같다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206891164-f26425bd-41e4-4efd-9009-8bda1c7dcafa.PNG" alt="collection table" width="50%"></p>

<ul>
<li>테이블의 구성을 보면 앞서 언급한 1번의 문제대로 <strong>식별자가 존재하지 않고</strong>, 값타입을 가지고 있는 Store 엔티티의 id값 (PK)와 offDays의 값만 존재한다.</li>
<li>결국 식별자가 없기에 해당 데이터의 값이 변경되면 어떤 레코드가 변경됐는지 추적하기가 어렵다.</li>
<li>또한 PK의 조건 <strong>(Not null, Unique)</strong>을 갖추려면 id와 off_days 두 컬럼을 묶어야만 가능하므로 PK를 생성할 때 <strong>복합키</strong> 방식을 강제로 사용하여야 한다.</li>
</ul>
<h2 id="3-변경-사항이-발생하면-연관-테이블의-데이터를-모두-삭제하고-남아있거나-새로-추가된-데이터를-다시-넣는다">3. 변경 사항이 발생하면 연관 테이블의 데이터를 모두 삭제하고 남아있거나 새로 추가된 데이터를 다시 넣는다</h2>
<ul>
<li>무엇보다 가장 직관적이게 느껴지는 문제점은 3번 문제점인 것 같다.</li>
<li>만약 위 테이블에서 36번 Store의 offDays를 변경하는 경우, update 쿼리가 하나 날라가거나 더 많은 offDays가 들어온 경우 insert 쿼리가 함께 날라가는 것을 생각하게 된다.</li>
<li>하지만 현실은 <strong>delete 쿼리</strong>가 날라간 후, 기존의 데이터와 함께 값으로 들어온 모든 데이터를 insert 쿼리로 입력한다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206891499-3b8bbd83-026d-4773-ac44-90b380d9355d.PNG" alt="elementcollection 수정" width="100%"></p>

<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206891523-6f25824b-891d-43ed-94d1-244e70918eec.PNG" alt="elementcollection 수정" width="100%"></p>

<ul>
<li>위처럼 delete 쿼리로 값타입이 저장된 테이블의 모든 데이터를 날린 후, insert 쿼리가 추가된 데이터의 수만큼 반복하여 날라간다.<ul>
<li>ex) 기존 화요일 + 수요일, 목요일인 경우 =&gt; delete후 (화, 수, 목) insert 쿼리가 <strong>총 3번</strong> 날라감.</li>
</ul>
</li>
<li>이는 전혀 예상치 못한 방향으로 DB가 동작하고 있는 것이다.</li>
</ul>
<br/>

<h1 id="entity-연관-관계-매핑으로-대체하자">Entity 연관 관계 매핑으로 대체하자</h1>
<ul>
<li>따라서 값타입 자체를 엔티티로 생각하고 데이터를 처리하는 방식으로 변환하기로 마음을 먹었다.</li>
<li>강의나 기타 블로그를 보면 <strong>일대다 관계를 고려하라</strong>고 하는데, 현재 내 기능의 경우 다대다의 관계가 필요하므로 이를 사용할 것이다.</li>
<li>물론 @ManyToMany 에노테이션을 그대로 사용하는 것이 아닌 @OneToMany, @ManyToOne 관계로 풀어내어 사용한다.</li>
</ul>
<h2 id="변경된-엔티티-관계">변경된 엔티티 관계</h2>
<ul>
<li>기존의 Collection 테이블이 아닌 엔티티간의 연관 관계로 이를 풀어냈다.</li>
</ul>
<pre><code class="language-java">@Entity
public class Store extends BaseEntity {
     // ...
    // Days (N:N)
    @OneToMany(mappedBy = &quot;store&quot;)
    private List&lt;DaysOfStore&gt; daysOfStoreList = new ArrayList&lt;&gt;();

    // ...
}

@Entity
public class DaysOfStore {
    @Id
    @GeneratedValue
    @Column(name = &quot;days_of_store_id&quot;)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;days_id&quot;)
    private Days days;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;store_id&quot;)
    private Store store;

    // ...
}

@Entity
public class Days {

    @Id
    @GeneratedValue
    @Column(name = &quot;days_id&quot;)
    private Long id;

    @Enumerated(EnumType.STRING)
    private DaysType days;

    @OneToMany(mappedBy = &quot;days&quot;)
    private List&lt;DaysOfStore&gt; daysOfStoreList = new ArrayList&lt;&gt;();

    // ...
}

</code></pre>
<ul>
<li>@ManyToMany를 DaysOfStore 엔티티를 활용하여 @OneToMany, @ManyToOne 관계로 풀어주었다.</li>
<li>이로인해 겪게될 문제점과 해결 방법은 다음 포스팅에서 마저 정리하겠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[502 Bad Gateway]]></title>
            <link>https://velog.io/@yangwon-park/502-Bad-Gateway</link>
            <guid>https://velog.io/@yangwon-park/502-Bad-Gateway</guid>
            <pubDate>Wed, 21 Feb 2024 01:20:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="502-bad-gateway">502 Bad Gateway</h1>
<ul>
<li>배포 중인 서비스가 <strong>504 Gateway Timeout</strong> 에러를 일으킨 후 뻗어서 EC2를 재부팅하고 나니 <strong>502 Bad Gateway</strong> 에러가 발생하였다. </li>
<li>구글링을 해보니 502에러는 EC2의 Load Balancer의 문제인 경우라고 한다.</li>
<li><strong><em><a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-troubleshooting.html#http-502-issues">AWS 공식 문서 트러블 슈핑</a></em></strong></li>
</ul>
<h2 id="에러-발생-원인이-뭘까">에러 발생 원인이 뭘까?</h2>
<h3 id="504-에러-원인">504 에러 원인</h3>
<ul>
<li>용량이 큰 이미지를 등록하는 과정(리사이징 동작)에서 서버 트래픽에 과부하가 걸려 <strong>504 에러</strong>가 먼저 발생하였다. EC2에 접속도 되지 않고 배포된 서비스에서 역시 아무런 동작이 가능하지 않았기에 강제로 AWS 콘솔에 접속하여 <strong>EC2 인스턴스를 강제 종료 후 재부팅시켰더니</strong> 그 후엔 <strong>502 에러</strong>가 발생하였다.</li>
<li>CloudWatch 모니터링 결과를 보면 <strong>특정 시점에 요청이 몰려있는 것</strong>이 보이는데 이 시점에 내가 이미지를 업로드하고 있었고, 그 밑의 사진에서 보이다싶이 3분 뒤 서버가 뻗어버렸다. (용량이 큰 이미지를 처리하는 것이 요청의 개수와 무관하지 않는듯)</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206151079-efa41687-d8a6-4ac0-a8b9-ab73a8716aba.png" alt="CloudWatch 요청" width="50%"></p>

<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206151980-2c7158ea-c2bf-42c4-8100-5f0d79197f50.png" alt="CloudWatch 504" width="50%"></p>

<ul>
<li>따라서 용량이 큰 이미지를 리사이징하는 과정에서 과부하가 걸려 504 에러가 발생한 것으로 유추할 수 있다.</li>
</ul>
<h3 id="그럼-502-에러는-왜">그럼 502 에러는 왜??</h3>
<ul>
<li>난 504 에러가 발생한 후 EC2 인스턴스를 재시작하였고, 완전히 재시작이 되고 나니 504 에러가 504 에러로 변경됐다.</li>
<li>앞서 언급한 것처럼 로드 밸런서의 문제인 것 같아 [EC2] - [로드 밸런싱] - [대상 그룹] 탭에 들어가보니 아래와 같이 <span style="color: red"><strong>unhealthy</strong></span>로 상태가 나와있는 것을 알 수 있었다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206148927-7cd88140-b138-4a32-a992-261780c3404a.png" alt="그룹 상태" width="70%"></p>

<ul>
<li>쉽게 얘기하면 대상 인스턴스(여기서는 배포 중인 서비스 EC2)의 80포트의 상태가 좋지 않다 이런 의미이다.</li>
<li>먼저 로드 밸런서의 로그를 살펴보고 싶었으나 <strong>로드 밸랜서 액세스 로그를 활성화시키면 비용이 발생하기에</strong> 이를 활성화 시킨 적이 없어 로그를 따로 살펴볼 수 없었다.</li>
<li>또한 EC2와 서비스 자체는 정상적으로 실행 중인 상태였기에 별 다른 에러를 볼 수 있는 곳이 없었다.</li>
</ul>
<h2 id="뭘-잘못했을까">뭘 잘못했을까?</h2>
<ul>
<li>결론부터 말하자면 <code>nginx</code>가 꺼져있었다.</li>
<li>위의 과정까지 도달했을 때, 난 로드 밸런서가 80포트를 사용하지 못하는 점에 주목을 하였다. 즉, <strong>EC2도 멀쩡하고 Spring Server 역시 멀쩡한데 80포트를 사용하지 못하는 이유가 뭘까?</strong> 라는 점에서 접근하였고 매우 당연하기에 신경쓰지 않은, 바보같은 곳에서 문제의 원인을 찾을 수 있었다.</li>
<li>EC2에 jar 파일이 실행 중인 것은 맞았으나 nginx가 꺼져있었기에 로드 밸런서가 EC2의 80포트에 접근하지 못하였던 것이다.</li>
<li><code>ps -ef | grep java</code>를 입력했을 때 실행 중인 프로세스가 너무 명확하게 보였기에 당연히 nginx 역시 켜져있을 거라 생각했는데 아니었다..ㅠㅠ</li>
<li>nginx를 시작해주고 나니 정상적으로 서비스에 접근할 수 있는 것을 확인하였다!</li>
</ul>
<h1 id="결론">결론</h1>
<ul>
<li>EC2 인스턴스를 재부팅한 후라면 nginx 상태 체크를 무조건 하자!!</li>
<li>아직 nginx, 로드 밸런서, 포트 포워딩같은 네트워크 관련 지식이 부족하다는 것을 깨달았습니다.</li>
<li>위 내용들을 다시 한 번 정리를 하는 시간을 갖도록 해야겠습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버 성능 최적화]]></title>
            <link>https://velog.io/@yangwon-park/%EC%84%9C%EB%B2%84-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@yangwon-park/%EC%84%9C%EB%B2%84-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 21 Feb 2024 01:18:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="서버-성능을-최적화해보자">서버 성능을 최적화해보자</h1>
<ul>
<li>이미지 리사이징을 구현하고 보니, 서버 성능 및 서비스 최적화를 최소한이라도 진행해야겠다 싶어서 아래의 사이트에서 현재 배포중인 서비스를 테스트해보았다.</li>
<li><strong><em><a href="https://pagespeed.web.dev/">속도 개선 피드백 사이트</a></em></strong></li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206098425-28215073-efb7-495f-ad9e-c8124d564a81.PNG" alt="속도 체크 개선" width="70%"></p>

<ul>
<li>위 사이트의 결과를 토대로 기본적인 사이트 최적화를 적용해보자.</li>
</ul>
<h2 id="1-텍스트-압축-사용">1. 텍스트 압축 사용</h2>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206099068-f75b0bde-b789-4c31-9c08-747c135029a8.PNG" alt="텍스트압축" width="100%"></p>

<ul>
<li><strong><em><a href="https://developer.chrome.com/ko/docs/lighthouse/performance/uses-text-compression/">참고 사이트</a></em></strong></li>
</ul>
<h3 id="1-1-gzip을-활용하여-텍스트-압축">1-1. GZIP을 활용하여 텍스트 압축</h3>
<ul>
<li><strong><em><a href="https://www.gnu.org/software/gzip/">GZIP 공식 사이트</a></em></strong></li>
<li>SpringBoot의 설정 파일에서 손쉽게 텍스트 데이터를 압축하여 서버에 전송할 수 있다.</li>
</ul>
<pre><code class="language-yaml">server:
  compression:
    enabled: true</code></pre>
<ul>
<li><code>Brotli</code> 이라는 보다 최신의 압축 알고리즘이 있으나, 아래의 두가지 이유로 <code>GZIP</code>을 선택했다.<ol>
<li>기본이 <code>GZIP</code>이고, <code>Brotli</code>를 적용하려면 설정을 많이 건드려야 함</li>
<li>IOS의 <strong>Safari에서 동작하지 않는다!</strong></li>
</ol>
</li>
<li>압축 전 텍스트 파일들의 크기</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206100796-eabe531e-c8f0-463c-8cb9-8e521d2c200b.PNG" alt="텍스트압축전" width="70%"></p>

<ul>
<li>압축 후 텍스트 파일의 크기 및 Response Headers</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206100858-758689b0-8f51-4b05-af96-1d15e1d17320.PNG" alt="텍스트압축후" width="70%"></p>

<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206100978-80a86a83-4a73-4991-bf12-7ca54e2747f8.PNG" alt="텍스트압축후 Response Header" width="70%"></p>

<ul>
<li>[크롬 개발자 도구] - [Network] 탭의 리소스들을 보면 위의 결과들이 나온다.</li>
<li>Response Headers에 Content-Encoding으로 <code>GZIP</code>이 정상적으로 적용된 것을 확인할 수 있고, 간단한 설정만으로도 텍스트 파일들의 크기가 기존 대비 <span style="color: red"><strong>12.5%</strong></span>까지 줄어든 것을 확인할 수 있다.</li>
</ul>
<h2 id="2-검색엔진-최적화를-위한-태그-추가">2. 검색엔진 최적화를 위한 태그 추가</h2>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206106682-d26b2ccd-8ecd-4719-8296-5fb1627eb4b8.PNG" alt="검색엔진 최적화" width="100%"></p>

<h3 id="2-1-모바일-친화적인-페이지-지원">2-1. 모바일 친화적인 페이지 지원</h3>
<ul>
<li><strong><em><a href="https://developers.google.com/search/mobile-sites/get-started?utm_source=lighthouse&amp;utm_medium=lr">구글 검색 센터</a></em></strong></li>
</ul>
<pre><code class="language-html">&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;</code></pre>
<ul>
<li>위의 코드를 추가하여 모바일에 친화적인 페이지로 만든다.</li>
</ul>
<h3 id="2-2-문서에-메타-설명-추가">2-2. 문서에 메타 설명 추가</h3>
<pre><code class="language-html">&lt;meta name=&quot;description&quot;
          content=&quot;현재 위치하는 주변의 핫플레이스를 기호와 날씨에 맞게 추천해드립니다.&quot;&gt;</code></pre>
<ul>
<li>웹사이트 전체를 간략하게 설명해주는 요소로 검색 결과에서 웹사이트의 이름과 함께 표시되는 부분이다.</li>
</ul>
<h3 id="2-3-이미지-요소에-alt-속성-추가">2-3. 이미지 요소에 alt 속성 추가</h3>
<ul>
<li>이미지마다 alt 속성값을 부여해줘야 하는데, 이를 간과하여 놓친 부분들에 속성을 다 부여해주었다.</li>
</ul>
<h2 id="3-대표이미지-리사이징-이미지로-변경">3. 대표이미지 리사이징 이미지로 변경</h2>
<ul>
<li>기존 리사이징 하지 않은 이미지들을 로드할 때, 많은 시간이 소요되고 있었다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206113309-c9039180-c950-4a5e-afce-131ed26997f6.PNG" alt="리사이징 전 소요 시간" width="100%"></p>

<ul>
<li>최대 이미지 로드 소요 시간에 400ms ~ 2200ms까지 약 1800ms 소요되는 것을 확인할 수 있다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/206116184-77d241d6-1210-40de-a48f-cafc4c956df3.PNG" alt="리사이징 후 소요 시간" width="100%"></p>

<ul>
<li>동일한 대표 이미지를 리사이징 한 후, 로드 소요 시간이 약 50ms로 <span style="color: red"><strong>3500%</strong></span> 성능 개선율을 보이는 것을 알 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 리사이징 (2)]]></title>
            <link>https://velog.io/@yangwon-park/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-2</link>
            <guid>https://velog.io/@yangwon-park/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-2</guid>
            <pubDate>Wed, 21 Feb 2024 01:16:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<ul>
<li><strong><em><a href="https://velog.io/@yangwon-park/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-1">관련 포스팅 1</a></em></strong></li>
</ul>
<h1 id="버그-발견">버그 발견!!</h1>
<ul>
<li>이미지 리사이징을 적용한 후 중대한 버그를 발견하게 되었다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/205448412-a4f347ac-295c-4cfc-b991-f55ff2939e9d.PNG" alt="리사이징버그" width="50%"></p>

<ul>
<li><p>png 타입의 이미지를 등록하는 경우 이렇게 이미지가 깨져버린다. (S3에 보면 0B로 용량도 없음)</p>
</li>
<li><p>jpg의 타입을 등록하면 정상적으로 동작하는데 해결을 한 지금 시점까지도 사실 정확한 버그 이유와 해결 방법을 알지 못한다.</p>
</li>
<li><p><strong><em><a href="https://stackoverflow.com/questions/16002167/using-imageio-write-to-create-a-jpeg-creates-a-0-byte-file">StackOverFlow 참고</a></em></strong></p>
<pre><code class="language-java">public static MultipartFile getResizedMultipartFile(MultipartFile multipartFile, String originalFileName) throws IOException {
        final int TARGET_IMAGE_WIDTH = 450;
        final int TARGET_IMAGE_HEIGHT = 450;
        final String IMAGE_TYPE = &quot;jpg&quot;;

        BufferedImage bi = ImageIO.read(multipartFile.getInputStream());
        bi = resizeImages(bi, TARGET_IMAGE_WIDTH, TARGET_IMAGE_HEIGHT);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        boolean check = ImageIO.write(bi, IMAGE_TYPE, baos);

        log.info(&quot;check={}&quot;,check);

        baos.flush();

        return new CustomMultipartFile(baos.toByteArray(), multipartFile.getName(), originalFileName, &quot;image/*&quot;, false, baos.toByteArray().length);
    }</code></pre>
</li>
<li><p>위의 게시글과 기존의 내 코드를 따르면 ImageIO에서 wrtie 타입을 <code>jpg</code>로 설정해줬다. 하지만 OpenJDK가 JPEG Encoder를 지원하지 않기 때문에 png 파일을 입력받으면 동작하지 않는다고 한다.</p>
</li>
<li><p>ImageIO.write의 결괏값이 false로 나옴</p>
</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/205449644-8fc62b43-a2f5-4397-8d29-34afc823330c.PNG" alt="리사이징버그" width="100%"></p>

<ul>
<li><p>이를 해결하기 위해 새로운 BufferedImage의 imageType을 변경하여 만들어도 보는 등 다양한 방식을 시도해봤으나 그냥 검은색 이미지가 만들어지거나 색이 흑백으로 나오는 등 문제가 많았다.</p>
</li>
<li><p>시행착오 끝에 선택한 해결법은 ImageIo.wrtie를 하는 시점에 IMAGE_TYPE을 png로 설정하는 것이었다.</p>
<pre><code class="language-java">public static MultipartFile getResizedMultipartFile(MultipartFile multipartFile, String originalFileName) throws IOException {
        final int TARGET_IMAGE_WIDTH = 450;
        final int TARGET_IMAGE_HEIGHT = 450;
        final String IMAGE_TYPE = &quot;png&quot;;              // 달라진 부분

        BufferedImage bi = ImageIO.read(multipartFile.getInputStream());
        bi = resizeImages(bi, TARGET_IMAGE_WIDTH, TARGET_IMAGE_HEIGHT);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        boolean check = ImageIO.write(bi, IMAGE_TYPE, baos);

        log.info(&quot;check={}&quot;,check);

        baos.flush();

        return new CustomMultipartFile(baos.toByteArray(), multipartFile.getName(), originalFileName, &quot;image/*&quot;, false, baos.toByteArray().length);
    }</code></pre>
</li>
</ul>
<ul>
<li>결괏값이 true로 나옴</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/205449755-75d1c3c0-3d75-4d9e-b59c-16f4f301ef75.PNG" alt="리사이징버그" width="100%"></p>

<h1 id="결론">결론</h1>
<ul>
<li>이 문제는 내부적인 Image 처리 방법과 <code>PNG</code>와 <code>JPG</code>의 차이같은 이미지 파일별 특징 및 차이를 파악해야 확실하게 이해가 가능할 것 같습니다.</li>
<li>또한, IMAGE_TYPE을 <code>png</code>로 설정하였다는 이유 만으로 왜 모든 이미지에 대한 처리가 해결되는지 의문점이 크게 남았습니다.</li>
<li>현재 제 프로젝트에서 <code>png</code>를 사용하나 <code>jpg</code>를 사용하나 아무런 문제가 없고 새로운 BufferedImage를 생성하여 이를 <code>jpg</code>로 변환하여 업로드하는 과정보다 그냥 java의 png encoder를 활용하여 <code>png</code>, <code>jpg</code> 둘 다 처리할 수 있는 최종적인 해결법이 가장 적절하다고 생각하여 이를 사용하여 버그를 해결하였습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[이미지 리사이징 (1)]]></title>
            <link>https://velog.io/@yangwon-park/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-1</link>
            <guid>https://velog.io/@yangwon-park/%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-1</guid>
            <pubDate>Wed, 21 Feb 2024 01:11:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="이미지-리사이징">이미지 리사이징?</h1>
<ul>
<li>이미지의 크기를 재조정하는 것으로 서버에서 사용할 때 원본의 이미지를 보여줄 필요가 없는 경우 이미지의 크기를 줄여 서버의 부하를 줄일 수 있다.</li>
</ul>
<h2 id="기능-추가를-생각하게-된-이유">기능 추가를 생각하게 된 이유</h2>
<ul>
<li>사실 처음엔 이미지에 대한 처리를 고민을 한 적이 아예 없었다. 업로드가 잘 된다고 좋아라~ 하기만 했을뿐 &#39;&#39;이미지&#39;&#39;라는 포인트에 별다른 생각 자체가 없었다.</li>
<li>헌데 AWS에 배포를 시작한 후부터 3가지 문제에 직면하게 되어 이들을 해결하고자 방법을 찾게 됐고, 해결법으로 알게 된 방법이 바로 <strong>이미지 리사이징</strong>이다. (3가지 이유가 모두 다 얽혀있음)</li>
</ul>
<h3 id="1-s3에-올라가는-이미지-크기가-들쑥날쑥">1. S3에 올라가는 이미지 크기가 들쑥날쑥</h3>
<ul>
<li>이미지의 크기가 화면이 가득찰만큼 큰 경우부터 되게 작은 경우까지 크기가 너무 중구난방이다.</li>
<li>특히 이 문제는 메뉴판을 모달로 띄울 때 크기가 너무 압도적인 경우가 있어 사소하지만 문제가 됐다.</li>
</ul>
<h3 id="2-위의-이유로-인한-이미지-용량-증가">2. 위의 이유로 인한 이미지 용량 증가</h3>
<ul>
<li>결국 크기가 매우 큰 이미지들이 존재하므로 자연스럽게 용량 역시 커진다.</li>
<li>이로 인해 AWS S3 스토리지 공간을 효율적이게 사용하지 못 하게 되고, 무엇보다 서버에서 이미지를 불러오거나 업로드를 하는 과정에서 많은 부하가 발생한다.</li>
</ul>
<h3 id="3-결국-배포된-서비스가-자주-뻗음">3. 결국 배포된 서비스가 자주 뻗음</h3>
<ul>
<li>배포돼있는 EC2 서버는 프리티어라 RAM 용량이 1GB밖에 되지 않는다. 따라서 이미지를 단시간에 많이 업로드 하거나 출력을 자주 하는 경우 서비스가 정말 자주 뻗는 경험을 하게 되었다.</li>
</ul>
<br />

<h1 id="구현-방법-선정">구현 방법 선정</h1>
<ul>
<li>프론트엔드, 백엔드 모두에서 구현이 가능하고 AWS의 Lambda@Edge를 활용하여 구현하는 방법도 있다.</li>
</ul>
<h2 id="lambdaedge란">Lambda@Edge란?</h2>
<ul>
<li><code>AWS의 CloudFront의 기능 중 하나로 애플리케이션의 사용자에게 더 가까운 위치에서 코드를 실행하여 성능을 개선하고 지연 시간을 단축할 수 있게 해줍니다. (AWS Lambda@Edge 소개글)</code></li>
<li>아직 사용해본적 없는 AWS의 CloudFront와 그에 속한 Lambda 기능으로, 많은 분들이 이 기능을 토대로 이미지 프로세싱을 구현하시는 듯 하다.</li>
<li>이 프로젝트에선 해당 기능을 사용하지 않고 순수 백엔드 라이브러리를 사용하여 리사이징을 구현하기로 했다.</li>
<li>추후 Lambda를 활용한 리사이징 또한 구현 후 포스팅하겠다.</li>
</ul>
<br />

<h2 id="라이브러리-선택">라이브러리 선택</h2>
<ul>
<li><strong><em><a href="https://www.baeldung.com/java-resize-image">Java 이미지 리사이징 참고 게시글</a></em></strong></li>
<li>막상 찾아보니 java 기본적으로 지원하는 기능과 함께 다양한 라이브러리들이 존재하는 것을 알게 되었는데 라이브러리들이 오래전부터 업데이트가 이루어지지 않고 있었다. (Lambda@Edge같은 <strong>Edge Computing</strong> 기술들의 보급으로 인한 것 같음)</li>
<li>고민 끝에 준수한 품질과 처리 속도를 가진 <code>Imgscarl</code> 라이브러리를 선택하였다.</li>
</ul>
<pre><code class="language-groovy">/*
    build.gradle 추가
    Imgscalr 이미지 리사이징 라이브러리
    https://mvnrepository.com/artifact/org.imgscalr/imgscalr-lib
*/
implementation group: &#39;org.imgscalr&#39;, name: &#39;imgscalr-lib&#39;, version: &#39;4.2&#39;</code></pre>
<br />

<h1 id="백엔드로-구현해보자">백엔드로 구현해보자</h1>
<ul>
<li>기존 이미지 업로드 코드에 아래의 로직을 작성한다.</li>
</ul>
<pre><code class="language-java">/*
    이미지 리사이징 메소드
 */
public static MultipartFile getResizedMultipartFile(MultipartFile multipartFile, String originalFileName) throws IOException {
    final int TARGET_IMAGE_WIDTH = 420;
    final int TARGET_IMAGE_HEIGHT = 200;
    final String TARGET_IMAGE_TYPE = &quot;jpg&quot;;

    BufferedImage bi = ImageIO.read(multipartFile.getInputStream());
    bi = resizeImages(bi, TARGET_IMAGE_WIDTH, TARGET_IMAGE_HEIGHT);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ImageIO.write(bi, TARGET_IMAGE_TYPE, baos);
    baos.flush();

    // MultipartFile 인터페이스를 구현한 구현체를 통해 MultipartFile을 생성해줌
    return new CustomMultipartFile(baos.toByteArray(), multipartFile.getName(), originalFileName, TARGET_IMAGE_TYPE, false, baos.toByteArray().length);
}

private static BufferedImage resizeImages(BufferedImage originalImage, int width, int height) {
    return Scalr.resize(originalImage, width, height);
}</code></pre>
<h2 id="multipartfile--bufferedimage--multipartfile로-변환">MultipartFile =&gt; BufferedImage =&gt; MultipartFile로 변환</h2>
<ul>
<li>입력받은 MultipartFile을 BufferedImage로 변환한 후, Scalr.resize 메소드를 이용하여 BufferedImage를 리사이징해준다.</li>
<li>문제는 이렇게 변환한 BufferdImage를 다시 MultipartFile로 변환할 방법이 구현되어 있지 않다. 따라서 이를 위해선 MultipartFile 인터페이스를 구현할 별도의 구현체를 만들어야 한다.</li>
<li><code>org.springframework.mock.web.MockMultipartFile</code>를 사용해도 되는 것 같은데, 이는 너무 Spring에 의존되는 방법이라 구현체를 만들기로 결정하였다.</li>
<li><strong><em><a href="https://stackoverflow.com/questions/41163648/how-to-convert-bufferedimage-to-a-multipart-file-without-saving-file-to-disk">StackOverFlow 참고</a></em></strong></li>
<li>직접 만든 구현체</li>
</ul>
<pre><code class="language-java">import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

/*
    MultipartFile을 생성하기 위해
    MultipartFile 인터페이스를 상속받은 클래스
 */
public class CustomMultipartFile implements MultipartFile {

    private final byte[] bytes;
    private final String name;
    private final String originalFilename;
    private final String contentType;
    private final boolean isEmpty;
    private final long size;

    public CustomMultipartFile(byte[] bytes, String name, String originalFilename, String contentType, boolean isEmpty, long size) {
        this.bytes = bytes;
        this.name = name;
        this.originalFilename = originalFilename;
        this.contentType = contentType;
        this.isEmpty = isEmpty;
        this.size = size;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getOriginalFilename() {
        return originalFilename;
    }

    @Override
    public String getContentType() {
        return contentType;
    }

    @Override
    public boolean isEmpty() {
        return isEmpty;
    }

    @Override
    public long getSize() {
        return size;
    }

    @Override
    public byte[] getBytes() throws IOException {
        return bytes;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return null;
    }

    @Override
    public void transferTo(File dest) throws IOException, IllegalStateException {

    }
}</code></pre>
<h2 id="반환-받은-multipartfile을-이후-로직에서-사용">반환 받은 MultipartFile을 이후 로직에서 사용</h2>
<ul>
<li>아래는 AWS S3에 이미지 업로드를 하는 로직의 일부이다. 위에서 만든 메소드를 활용하여 리사이징된 새로운 MultipartFile을 만들어준 후, 이를 File로 변환하여 이후 로직에 사용하니 정상적으로 작동하는 것을 확인하였다.</li>
</ul>
<pre><code class="language-java">/*
    이미지 리사이징
*/

        // 위는 생략...
        MultipartFile resizedMultipartFile = getResizedMultipartFile(multipartFile, originalFileName);

        File uploadFile = convert(resizedMultipartFile).orElseThrow(
            () -&gt; new IllegalArgumentException(&quot;전환 실패&quot;));

        imageUrl = getImageUrl(uploadFile, storeFileName);
    }

return new UploadFile(originalFileName, storeFileName, imageUrl);
}</code></pre>
<br />

<h1 id="적용-결과">적용 결과</h1>
<ul>
<li>기존에 업로드한 이미지와 같은 이미지를 리사이징 후 올린 이미지의 크기를 비교해보자.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/204911602-476ee8f0-fc5c-4c65-88c8-bd0259db9d82.PNG" alt="리사이징" width="100%"></p>

<ul>
<li>위에서 보는 것처럼 대략 <strong>3%</strong> 정도로 용량이 줄어든 것을 알 수 있다.</li>
<li>물론 이는 모든 이미지에 대해서 해당할 수 있는 수치도 아니며 아직 완벽하게 리사이징 조건을 설정한 것도 아니라서 시행착오를 거친 후, 최적화된 옵션과 이미지 크기를 선정하기로 했다.</li>
</ul>
<br />

<h1 id="결론">결론</h1>
<ul>
<li>이미지 리사이징을 구현하는 시간 투자 대비 그 효과는 생각보다 매우 큰 것 같습니다. 현재 메인 페이지에서 노출되는 이미지의 수가 많은데, 이들을 원본 그대로 로드하는 시간이 길어 User Interaction에까지 도달하는 시간이 생각보다 길었으며 이로 인한 UX 역시 좋지 못하였습니다.</li>
<li>하지만 이를 도입 후 이미지들을 바꿔주니 확실히 성능적인 체감이 컸기에 만족도가 매우 큽니다.</li>
<li>현재 현실과 타협하여 Lambda@Edage를 활용한 On-thy-fly 방식의 리사이징을 채택하지 못하였으나 여유가 될 때 그 부분을 따로 공부하고 다시 한 번 정리하여 포스팅을 하도록 하겠습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA에서의 랜덤 레코드 조회 방법]]></title>
            <link>https://velog.io/@yangwon-park/JPA%EC%97%90%EC%84%9C%EC%9D%98-%EB%9E%9C%EB%8D%A4-%EB%A0%88%EC%BD%94%EB%93%9C-%EC%A1%B0%ED%9A%8C-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@yangwon-park/JPA%EC%97%90%EC%84%9C%EC%9D%98-%EB%9E%9C%EB%8D%A4-%EB%A0%88%EC%BD%94%EB%93%9C-%EC%A1%B0%ED%9A%8C-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 21 Feb 2024 01:05:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="읽기에-앞서">읽기에 앞서</h1>
<ul>
<li>프로젝트를 진행하는 도중 무작위 랜덤 조회 기능이 필요하여 어떤 방식으로 사용하는지에 대해 공부한 내용을 정리한 포스팅</li>
</ul>
<h1 id="sql에서의-랜덤-조회-방식">SQL에서의 랜덤 조회 방식</h1>
<ul>
<li>이 포스팅에서의 원하는 내용이 아니므로 간단하게 정리하고 넘어간다.</li>
<li>현재 DB로 사용하고 있는 MariaDB의 경우, 랜덤으로 필드를 조회하는 쿼리는 아래와 같다.</li>
<li>이는 인덱스를 사용하지 못하여 데이터가 방대해질 수록 성능의 저하가 심한 듯 하다.</li>
</ul>
<pre><code class="language-sql"># 참고 블로그 [https://creds.tistory.com/158]

# 랜덤 데이터 조회
SELECT * FROM [TABLE] ORDER BY RAND();

# 랜덤으로 N개의 데이터 조회
SELECT * FROM [TABLE] ORDER BY RAND() LIMIT 1;

# ORDER BY 2번 사용 시
# DESC를 먼저 수행 후, 중복 데이터 중 하나를 랜덤으로 뽑아서 사용
SELECT * FROM [TABLE] ORDER BY COLUMN1 DESC, RAND() LIMIT 1;</code></pre>
<h2 id="jpa에서는">JPA에서는?</h2>
<ul>
<li><strong><em><a href="https://www.inflearn.com/questions/284950">인프런 질문 - 김영한님 답변</a></em></strong></li>
<li>아쉽게도 JPA에서는 지원하지 않는다고 하며 <strong>네이티브 쿼리</strong> 또는 <strong>jdbcTemplate</strong>을 사용해야 하는듯 하다.</li>
</ul>
<h2 id="그렇다면-방법이-없을까">그렇다면 방법이 없을까..?</h2>
<ul>
<li>ORDER BY RAND()의 성능 이슈 문제도 있고, 네이티브 쿼리를 사용하고 싶지 않아 방법을 계속 찾던 중, 유사한 방식의 조회법이 있다는 것을 아래의 게시글을 통해 알게 되었다.</li>
<li><strong><em><a href="https://stackoverflow.com/questions/24279186/fetch-random-records-using-spring-data-jpa">StackOverFlow 참고</a></em></strong></li>
<li>위의 게시글을 참고하면 JPA의 페이징 기법을 활용하여 마치 랜덤으로 조회하는 듯한 효과를 낼 수 있다!</li>
</ul>
<br />

<h1 id="페이징을-활용하여-랜덤-조회를-구현해보자">페이징을 활용하여 랜덤 조회를 구현해보자</h1>
<ul>
<li>위의 게시글은 JPA를 바탕으로 구현돠었으나, 난 프로젝트의 환경에 맞게 QueryDSL을 활용하여 필요한 기능을 구현하고자 한다.</li>
<li>(페이징에 관한 포스팅은 추후에 작성 예정)</li>
<li>구현하고자 하는 기능 : <strong>특정 범위 내의 7개 매장 정보를 랜덤으로 조회</strong></li>
</ul>
<h2 id="repository-구현체">Repository 구현체</h2>
<pre><code class="language-java">/*
    특정 범위 상위 7개 조회
*/
@Override
public List&lt;SimpleSearchStoreDTO&gt; searchTop7Random(Polygon&lt;G2D&gt; polygon, Pageable pageable) {
    final int dist = 3;

    return queryFactory
                .select(Projections.constructor(SimpleSearchStoreDTO.class,
                        store.id, store.name, store.lon, store.lat, store.phoneNumber,
                        store.status, store.businessTime, store.address, store.ratingTotal, store.file.uploadImageUrl
                )).distinct()
                .from(store)
                .where(stContains(polygon), stDistance(polygon).loe(dist))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
}

/*
    특정 범위 개수 조회
*/
@Override
public Long countStoreInPolygon(Polygon&lt;G2D&gt; polygon) {
    return queryFactory
        .select(store.count())
        .from(store)
        .where(store.file.uploadImageUrl.isNotNull())
        .where(stContains(polygon), stDistance(polygon).loe(3))
        .fetchOne();
}
</code></pre>
<ul>
<li>QueryDSL의 사용법은 별도로 설명하지 않는다.</li>
</ul>
<h2 id="service">Service</h2>
<pre><code class="language-java">public List&lt;SimpleSearchStoreDTO&gt; searchTop7Random(double lat, double lon, double dist) {
    final int size = 7;             // 한 페이지 당 데이터 개수

    /*
        범위 생성
    */ 
    Polygon&lt;G2D&gt; polygon = getPolygon(lat, lon, dist);

    /*
        특정 범위 개수 조회
    */ 
    Long count = storeRepository.countStoreInPolygon(polygon);

    /*
        참고 블로그 - https://mine-it-record.tistory.com/141
        Math.random()을 활용하여 난수를 생성
           +1을 해주면 0 ~ (count / n)까지의 값이 선택됨
           아니면 0 ~ (count / n) - 1까지의 값이 선택됨
    */
    int pageNum = count % n != 0
                    ? (int) (Math.random() * (count / size + 1))
                    : (int) (Math.random() * (count / size));

    /*
        페이징 정보를 넘겨줌
        idx쪽의 n개 데이터를 세팅함
    */
    PageRequest pageRequest = PageRequest.of(pageNum, size);

    return storeRepository.searchTop7Random(polygon, pageRequest);
}</code></pre>
<ul>
<li>다소 헷갈릴 수 있는 부분이 있는데 바로 <strong>idx</strong>의 값의 연산이다.</li>
<li>count가 n으로 나누어 떨어지지 않는 경우, 나머지가 존재한다는 의미이다.</li>
<li>나머지 데이터들 또한 하나의 페이지에 담아줘야 하므로 페이지가 하나 더 필요해진다. 따라서 +1을 하여 난수 생성 범위를 1 늘려준다.</li>
</ul>
<h2 id="어떤-방식인가">어떤 방식인가?</h2>
<ol>
<li>전체 데이터의 개수를 구하고 이를 원하는 데이터 크기만큼 나누어 전체 페이지 개수를 구함</li>
<li>Math.random()을 활용한 난수 생성을 이용하여 하나의 페이지 넘버를 구함</li>
<li>이를 PageRequest에 사이즈와 함께 담아서 넘겨줌</li>
</ol>
<h1 id="결론">결론</h1>
<ul>
<li><strong>흡사 책을 들고 아무런 페이지나 열어보는 경우와 비슷합니다.</strong></li>
<li>데이터를 랜덤 조회한다기 보단 추출한 데이터들의 페이지를 랜덤 조회하는 것입니다..</li>
<li>현재 제 프로젝트에선 완전한 랜덤 조회 기능은 필요없기에 위의 방식에 만족하고 알맞은 용도로 잘 사용하고 있으나, 100% 완벽하게 랜덤 데이터를 조회하는 방법이 아닙니다. 해당 방법을 구현하고 하는 경우라면 네이티브 쿼리나 jdbcTemplate을 사용해야 하는 것 같습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 환경 구성 for Mac (with. Docker)]]></title>
            <link>https://velog.io/@yangwon-park/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1-for-Mac-with.-Docker-zqrktfhq</link>
            <guid>https://velog.io/@yangwon-park/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%84%B1-for-Mac-with.-Docker-zqrktfhq</guid>
            <pubDate>Wed, 21 Feb 2024 01:03:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="프로젝트-환경을mac에서-세팅해보자">프로젝트 환경을Mac에서 세팅해보자</h1>
<ul>
<li>집에서 쓰는 Windows Desktop으로 주로 프로젝트를 진행하다 보니 <strong>AWS 인프라 구축 및 DB Migration (MySQL -&gt; MariaDB)</strong> 이후 변경된 사항들을 MacBook에는 반영하지 않고 방치해둔 상태라 이를 재설정하면서 공부하게 된 것들을 정리하고자 한다.</li>
</ul>
<h2 id="재설정해야-하는-요소들">재설정해야 하는 요소들</h2>
<ul>
<li>Mac Local DB는 아직 MySQL이다. (Docker 사용)</li>
<li>DB GUI Tool이 아예 없다. Windows에선 MySQL Workbench를 사용하고 있었는데, 다른 분들의 추천도 있고 새로운 Tool도 알아볼겸 Mac OS에서는 Sequel Pro을 사용하려고 한다.</li>
</ul>
<h2 id="설정은-했지만-정리하고자-하는-요소들">설정은 했지만 정리하고자 하는 요소들</h2>
<ul>
<li>Docker 설치 및 환경 설정<ul>
<li>무지에 가까운 레벨이고 항상 구글링을 하면서 사용하였기 때문에 나의 패턴에 맞는 방식으로 정리를 할 기회를 가져야겠다.</li>
<li>Windows와 달리 Mac은 같은 유닉스 계열이라 Linux에 친화적이고, 한 번쯤 공부를 하고 싶어 Docker를 간단하게라도 사용해보기로 다짐했다.</li>
<li>실제로 Windows에서 WSL2를 사용하거나 도커를 써보려고 했을 때 버그때문에 스트레스 받은 기억이 너무 커서 Mac에서만 사용하기로...</li>
</ul>
</li>
<li>Mac에서 EC2 접속 설정<ul>
<li>별도로 정리한 적이 없는 것 같아 이참에 정리를 하고자 한다.</li>
</ul>
</li>
</ul>
<br>

<h1 id="docker-설치-및-db-환경-설정">Docker 설치 및 DB 환경 설정</h1>
<h2 id="docker란">Docker란?</h2>
<ul>
<li>Docker에 관한 좋은 설명들이 인터넷에 많으므로 여기선 간단하게 설명하고 바로 다음 과정으로 넘어간다 - <strong><em><a href="https://hanamon.kr/%EB%8F%84%EC%BB%A4%EB%9E%80-docker-%ED%95%84%EC%9A%94%EC%84%B1/">참고 블로그</a></em></strong></li>
<li><strong>리눅스 컨테이너 기반의 오픈소스 가상화 플랫폼.</strong></li>
<li>필요한 라이브러리, 실행환경, 프로그램 등을 별도의 서버처럼 추상화하여 구성한 것으로, 각각의 컨테이너마다 <strong>독립적인</strong> 시스템 자원을 소유한다.</li>
<li>VM (가상 머신)과는 다르게 OS 단위의 가상화가 아닌 Application 단위의 가상화를 한다.</li>
<li>쉽게 얘기하면 Application의 파일과 설정값 등을 <code>Image</code>라는 <strong>Immutable</strong>한 친구에 모두 담아서 이를 <code>Conatiner</code>로 실행시켜 실제 로컬에 설치한 것처럼 원하는 Application을 호스트 OS에서 간편하게 가져다 쓸 수 있는 플랫폼이다. </li>
</ul>
<h2 id="docker-설치">Docker 설치</h2>
<h3 id="hombrew-cask-설치">Hombrew Cask 설치</h3>
<ul>
<li><p>공홈에서 설치하는 방법은 너무 간단하고 개인적으로 Mac인 이상 Terminal을 적극 활용하는 것이 좋아서 homebrew를 활용하여 설치 진행 예정.</p>
</li>
<li><p>Homebrew 설치는 <strong><em><a href="https://brew.sh/index_ko">Homebrew 홈페이지</a></em></strong>에 매우 친절하게 나와있다.</p>
</li>
<li><p>또한 brew로 설치하는 방법보다 Mac OS 자체에 Docker를 띄울 수 있는 brew cask를 이용.</p>
<ul>
<li>cask를 이용하면 Mac의 /Applications에 원하는 Application이 설치된다.</li>
<li><strong><em><a href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=sarang2594&amp;logNo=221246170677">Homebrew &amp; Homebrew Cask 참고 블로그</a></em></strong></li>
</ul>
</li>
<li><pre><code class="language-bash">brew install cask  # cask 설치

# 완료 후

brek install --cask docker    # Homebrew 2.6.0 버전 이후 사용 방식</code></pre>
</li>
</ul>
<h3 id="mariadb-image-설치-및-container-생성-과정">MariaDB Image 설치 및 Container 생성 과정</h3>
<ol>
<li>원하는 이미지 검색 후, 버전에 맞게 설치</li>
</ol>
<pre><code class="language-bash">docker search mariadb                            # docker search image_name (또는 docker hub 들어가서 직접 검색)
docker pull mariadb:10.6.10                # docker pull image_name:version (default: latest)</code></pre>
<ol start="2">
<li>이미지 정상 설치 확인 후, 컨테이너 생성과 동시에 실행</li>
</ol>
<pre><code class="language-bash">docker images                                            # 설치된 images 리스트 출력됨

# run 이미지를 기반으로 새로운 컨테이너를 생성하고 이후의 cmd를 실행
# --name container_name
# -d 컨테이너를 백그라운드에서 실행
# -p host-port:container-port (호스트 3306 포트 연결 시, 컨테이너 3306 포트로 포워딩)
# --restart=always 도커가 실행되면 컨테이너도 항상 같이 실행
# -e 기타 환경설정 (environment)
# MYSQL_ROOT_PASSWORD=password DB root 사용자 초기 비밀번호 설정
# mariadb:10.6.10 컨테이너에 쓸 이미지 이름 (버전을 명시하지 않으면 latest로 동작함. latest가 없으면 이걸 설치하므로 주의)
docker run --name mariadb -d -p 3306:3306 --restart=always -e MYSQL_ROOT_PASSWORD=root mariadb:10.6.10</code></pre>
<ol start="3">
<li>실행중인 container 확인</li>
</ol>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203490541-96c30a0b-8016-42dc-ab86-41bff406dcf5.png" alt="docker ps" width="100%"></p>

<h2 id="mariadb-접속-후-db-환경-설정">MariaDB 접속 후 DB 환경 설정</h2>
<ol>
<li>MariaDB 컨테이너 접속</li>
</ol>
<pre><code class="language-bash"># exec container_name 실행중인 컨테이너에서 이후의 cmd를 실행 (여기선 /bin/bash)
# -i 접근 권한 부여 (?) 이 옵션이 없으면 접속이 안 되고 막힘
# -t tty(TeleTYpewrite)를 열어줌 (그냥 텍스트 편집기 열어준다고 생각)
# /bin/bash 실행할 cmd 부분
# docker 컨테이너에서 bash shell을 열어줌
docker exec -it mariadb /bin/bash</code></pre>
<ol start="2">
<li>bash shell 접속 후 루트 계정으로 로그인 후 DB 세팅</li>
</ol>
<pre><code class="language-bash">mysql -u root -p

# 패스워드 입력 창에서 패스워드 입력 : 위에서 컨테이너 생성 시 root로 만들어둠

create database db_name                                                                             # DB 생성

# IP 부분에 %가 들어가면 모든 IP에서 접속이 가능함
create user &#39;user_name&#39;@&#39;IP&#39; identified by &#39;password&#39;;        # 유저 생성 (&#39;&#39; &lt;- 이거 필수)


# grant 권한 종류 on db_name.table(*.* 사용시 모든 DB의 모든 테이블) to &#39;user_name&#39;@&#39;IP&#39;;
grant all privileges on db_name.* to &#39;user_name&#39;@&#39;IP&#39;;                # 권한 부여
flush privileges;                                                                                            # 권한 refresh</code></pre>
<h2 id="기타-매우-매우-기본적인-docker-기능들">기타 매우 매우 기본적인 Docker 기능들</h2>
<pre><code class="language-bash">docker ps                                       # 실행중인 컨테이너 출력
docker ps -a                                  # 호스트 OS에 존재하는 모든 컨테이너 출력

docker images                                  # 이미지 리스트 출력

docker rmi image_name                  # 이미지 삭제

docker stop container_name     # 컨테이너 중지

docker start container_name # 컨테이너 시작

docker rm container_name         # 컨테이너 삭제</code></pre>
<br>

<h1 id="sequel-pro설치-및-local-db-rds-db와-연동">Sequel Pro설치 및 Local DB, RDS DB와 연동</h1>
<h2 id="sequel-pro-사용-이유">Sequel Pro 사용 이유</h2>
<ul>
<li>개인적으로 MySQL Workbench가 불편했기에 Mac에서 사용할 수 있는 GUI Tool을 찾고 있었다.</li>
<li>처음엔 HeidiSQL을 사용하려고 했으나, Mac에선 지원을 안 하는듯하여 다른 대안을 찾던 중 많은 선배 개발자 분들이 Mac용 GUI Tool로 Sequel Pro를 추천하셨기에 사용해보려고 한다.</li>
<li>쿼리 실행 단축키 : command + R</li>
</ul>
<h2 id="설치">설치</h2>
<pre><code class="language-bash">brew install --cask sequel-pro</code></pre>
<h2 id="local-db와-연동">Local DB와 연동</h2>
<ul>
<li>위에서 Docker로 설치한 MariaDB와 연동</li>
<li>Host에 IP 주소 또는 URL을 입력</li>
<li>별다른 설정을 할 필요없이 위에서 만든 계정으로 접속하면 정상적으로 작동한다.<ul>
<li>이 과정에서 처음 안 사실 : <strong>MySQL Server에서 127.0.0.1과 localhost는 다른 방식으로 작동</strong></li>
<li>둘 모두 루프백을 의미하는 주소인 것은 동일하나, 통신 방식에 있어서 차이가 있다.</li>
<li>이 부분에 대하여 더 공부를 해봐야겠다. (일단 현시점에서 localhost를 사용 시, docker 컨테이너에 연결을 하지 못함)</li>
<li><strong><em><a href="https://mimah.tistory.com/entry/MySQL-Server-%EC%97%B0%EA%B2%B0-%EC%8B%9C-localhost%EC%99%80-127001-%EC%B0%A8%EC%9D%B4">참고 링크</a></em></strong></li>
</ul>
</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203498363-526f84a3-f09d-45d4-8af8-9c5d3e04d35e.png" alt="sequel pro" width="50%"></p>

<h2 id="rds와-연동">RDS와 연동</h2>
<ul>
<li>Host에 RDS의 엔드포인트를 입력</li>
<li>당연하게 RDS의 보안 그룹 인바운드 규칙에 본인의 IP가 접근 허용이 되어있어야 한다!</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203501271-e41a17b2-b098-46a2-bfc8-8ab064663a94.png" alt="sequel pro" width="50%"></p>

<br>

<h1 id="rds-db-데이터-export-후-docker-db로-import">RDS DB 데이터 Export 후 Docker DB로 Import</h1>
<ul>
<li>Windows에서 또는 Mac에서 잠시나마 Workbench를 사용했을 때 Import / Export 과정에서 에러가 발생하는 경우가 정말 잦았다.</li>
<li>예를 들어, MariaDB를 사용하기 시작하면서 Workbench가 지원하는 버전과 맞지 않는 경우가 되게 많았으며 이 경우 데이터가 이상하게 Import 되던가 Encoding 에러가 발생하던가 아예 Export부터 막혀버리는 등 다양한 문제를 겪었다.</li>
<li>또한, RDS의 데이터를 Export해서 Docker DB로 Import 하는 과정을 CLI로 수행하기엔 너무 과정이 난잡하고 비효율적이라는 생각이 들어 어떻게 해야할까 고민을 많이 했었다. (물론 이 부분은 도움이 될 것 같아 추후에 공부할 예정)</li>
<li>근데 위의 모든 고민을 Sequel Pro에서 수행하니 에러가 하나도 발생하지 않았다! (MariaDB 버전까지 호환되게 잘 만들어졌나?)</li>
</ul>
<h2 id="rds-db에서-export-하기">RDS DB에서 Export 하기</h2>
<ul>
<li>Sequel Pro로 RDS DB에 접속 후 <strong>[상단 메뉴바] - [File] - [Export]</strong> 클릭 (단축키 : command + shift + E)</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203509226-6886bd5e-9086-4f4c-99b8-c1afecfcbc1c.jpg" alt="sequel pro" width="50%"></p>

<ul>
<li>상단에서 Export 형식을 정할 수 있음 (난 SQL 사용)</li>
<li>S (Structure)<ul>
<li>Import 시 Create Table 쿼리가 포함</li>
</ul>
</li>
<li>C (Content)<ul>
<li>Export 시 DB의 데이터도 함께 Export 됨</li>
<li>Import 시 insert 쿼리가 포함</li>
</ul>
</li>
<li>D (DROP TABLE syntax)<ul>
<li>Import 시 Drop Table 쿼리가 포함</li>
<li>중복인 경우, 기존 Table들을 날리고 새로 테이블을 생성함</li>
</ul>
</li>
</ul>
<h2 id="local-db에서-import">Local DB에서 Import</h2>
<ul>
<li>Sequel Pro로 Local DB에 접속 후 <strong>[상단 메뉴바] - [File] - [Import]</strong> 클릭 (단축키 : command + shift + I)</li>
<li>Export한 SQL 파일을 Import 시키면 끝</li>
</ul>
<h2 id="사용-후기">사용 후기</h2>
<ul>
<li>확실히 Workbench보다 가벼운 느낌이고 UI가 훨씬 이뻐서 전체적인 <strong>만족감이 너무 좋다.</strong></li>
<li>골머리 앓았던 에러들이 너무 간단하게 해결된 느낌이라 찜찜하기도 하면서도 기분이 좋다.</li>
<li>물론 현재까진 에러가 발생할 상황이 없었을 뿐이기에 에러가 발생하면 그에 따른 트러블 슈팅 과정도 추후에 기록해야겠다.</li>
<li>또한 성능적인 부분이나 기타 이슈 등은 더 오래 써보고 느낀 점 역시 함께 기록해야겠다.</li>
</ul>
<br>

<h1 id="mac-terminal에서-ec2-접속-환경-설정">Mac Terminal에서 EC2 접속 환경 설정</h1>
<ul>
<li>AWS EC2가 구축되어 있다는 가정하에서 진행합니다.</li>
<li><strong>이동욱 님의 [스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 책을 참고하였습니다</strong>.</li>
<li><strong><em><a href="http://www.yes24.com/Product/Goods/83849117">스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - yes24 링크</a></em></strong></li>
</ul>
<h2 id="기본적인-ssh-접속-커맨드">기본적인 SSH 접속 커맨드</h2>
<ul>
<li>일반적으로 Mac (Linux) Terminal에서 SSH에 접속하고 싶은 경우 아래와 같은 긴 커맨드를 매번 입력해줘야 하는 번거로움이 존재한다. (대괄호는 구분을 위해서 썼을 뿐 아무런 의미가 없습니다.)</li>
</ul>
<pre><code class="language-bash">ssh -i [pem 키 위치] [EC2의 탄력적 IP주소]</code></pre>
<h2 id="ssh-접속-커맨드를-간소화해보자">SSH 접속 커맨드를 간소화해보자</h2>
<ul>
<li>앞서 커맨드를 <strong>~/.ssh/ 경로에 pem 키를 옮겨 놓음으로써</strong> ssh 실행 시 자동으로 pem키 파일을 읽어 들이게 하여 커맨드를 간소화 시킬 수 있다!</li>
</ul>
<pre><code class="language-bash">cp [pem 키 절대 경로/pem키] ~/.ssh/

cd ~/.ssh/                    # /.ssh/로 이동
ll                                # ll을 이용하여 파일 목록 확인하여 pem 파일이 존재하는지 체크

# 600의 의미
# 각 자리마다 자신 / 그룹 / 전체 권한을 의미
# 권한 종류 : 읽기 (4), 쓰기 (2), 실행(1)
# 즉 600이라면 자신에게 읽기 + 쓰기의 권한을 준다는 의미
chmod 600 ~/.ssh/[pem키]                # pem키의 권한 변경

# 권한 변경 이후 ssh 구성 정보를 설정할 config 파일 생성
vim ~/.ssh/config

    # vim에 들어왔다는 가정하여 들여쓰기 사용했음
    # 아래 설정값 입력
    Host [서비스 이름]
        HostName [EC2의 탄력적 IP주소]
        User ec2-user # EC2 Amazon Linux의 default 유저명 - OS마다 상이할 수 있음
        IdentityFile ~/.ssh/[pem키]

# :wq로 저장 후 vim 종료

# config 파일은 실행 권한이 필요
chmod 700 ~/.ssh/config</code></pre>
<ul>
<li>위의 과정까지 성공적으로 마무리 했다면 아래와 같은 간단한 커맨드로 EC2에 접속할 수 있다.</li>
</ul>
<pre><code class="language-bash">ssh [config에 등록한 서비스 이름]</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Hibernate, QueryDSL 그리고 Spatial DB (4)]]></title>
            <link>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-4-657062hg</link>
            <guid>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-4-657062hg</guid>
            <pubDate>Tue, 20 Feb 2024 09:58:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="현재-범위로-잡은-polygon의-문제점">현재 범위로 잡은 Polygon의 문제점</h1>
<ul>
<li>현재 기준 Point에서 부터 Nkm 떨어진 공간을 표현하는 Geometry로 Polygon을 사용하고 있는데, 이는 대략 아래와 같은 그림이다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203723373-7e9c6b3c-0ee9-4aff-a316-5b9eb4aa16f1.jpg" alt="polygon" width="50%"></p>

<ul>
<li><span style="color:red">빨간점</span>이 기준 Point라고 할 때, Point로부터 Nkm 떨어진 [북서 북동 남동 남서] 좌표를 찾아 이를 기준으로 정사각형 모양의 Polygon을 생성하고 있는 것을 알 수 있다.</li>
<li>이제 우린 저 범위 내의 좌표점들을 얻을 수 있게 되는 것인데, 곰곰히 생각해보니 여기엔 문제점이 있다. 과연 우리가 얻고자 하는 Nkm의 모든 범위 이내의 모든 좌표를 얻을 수 있는 것일까?</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203726217-b21e0259-4850-494b-b124-ccd8706f2474.jpg" alt="polygon" width="70%"></p>

<ul>
<li>너무나 당연한 얘기겠지만 <strong>직각이등변삼각형에서 대각선의 길이는 두 밑변의 길이보다 무조건 길다.</strong> 따라서 같은 거리 Nkm에 위치한 <span style="color:blue">파란점</span>은 만들어진 Polygon의 내부에 포함되지 않아서 조건에 부합하지 않고 결국 <strong>Select 쿼리의 결괏값에 포함되지 않는다.</strong></li>
<li>따라서 아래와 같이 <span style="color:red">빨간점</span>에서부터 4개의 기준 좌표들까지의 거리를 Nkm가 아닌 <strong>N√2</strong>로 잡고 Polygon을 생성한 후, 그 범위 내에서 Nkm안에 포함되는 좌표들을 구해야 한다. 즉, r=N인 원의 범위가 최종적으로 우리가 찾고자 하는 공간 데이터의 탐색 범위가 될 것이다. (피타고라스의 정리)</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203729251-150db2d8-ff91-40e7-8ec6-660c85ed5e66.jpg" alt="polygon" width="70%"></p>

<br>

<h1 id="어떻게-원을-표현할-것인가">어떻게 원을 표현할 것인가?</h1>
<ul>
<li>결국 정확한 거리를 기반으로 탐색을 수행하려면 <strong>탐색 범위를 원으로 만들어 사용</strong>하는 방법을 찾아야했고 현재까지 할 수 있는 방법을 총 2가지 찾았다.</li>
</ul>
<h2 id="1-orglocationtechjts의-geometricshapefactory-활용">1. org.locationtech.jts의 GeometricShapeFactory 활용</h2>
<ul>
<li><strong><em><a href="https://yangwon-park.github.io/troubleshooting/project03/#2-geometricshapefactory">이전 포스팅 참고 - GeometricShapeFactory</a></em></strong></li>
<li><strong><em><a href="https://yangwon-park.github.io/troubleshooting/project03/#5-orggeolattegeomjtsjts">이전 포스팅 참고 - org.geolatte.geom.jts.JTS</a></em></strong></li>
<li>locationtech.jts의 GeometricShapeFactory를 활용하면 다양한 도형의 Geometry 객체를 만들 수 있다.</li>
<li>생성한 Geometry 객체를 org.geolatte.geom.jts.JTS를 활용하여 Geolatte의 Geometry 객체로 변환하고 이를 탐색 범위로 사용하면 된다.</li>
</ul>
<h2 id="2-st_distance를-함께-사용">2. ST_Distance를 함께 사용</h2>
<ul>
<li><strong>N√2</strong>로 4개 좌표를 구하여 이를 첫번째 탐색 범위로 잡고 (ST_Contains), 앞서 생성한 범위 중ST_Distance를 추가 조건으로 활용하여 Nkm 이내인 좌표들만 탐색한다.</li>
<li>QueryDSL 활용 예시</li>
</ul>
<pre><code class="language-java">public List&lt;Store&gt; getStoresByStContains(Geometry&lt;G2D&gt; polygon) {
    return queryFactory
        .select(store)
        .from(store)
        .leftJoin(store.file, uploadFile)
        .fetchJoin()
        .where(stContains(polygon), stDistance(polygon).loe(3))
        .fetch();
}

private BooleanExpression stContains(Geometry&lt;G2D&gt; polygon) {
    return GeometryExpressions
        .asGeometry(polygon)
        .contains(store.point);
}

private NumberExpression&lt;Double&gt; stDistance(Geometry&lt;G2D&gt; polygon) {
    return GeometryExpressions
        .asGeometry(store.point)
        .distance(polygon);
}</code></pre>
<h2 id="geolatte에서-원을-생성하는-방법은">Geolatte에서 원을 생성하는 방법은?</h2>
<ul>
<li>JTS의 GeometricShapeFactory처럼 Geolatte에도 비슷한 기능이 있을까 싶어 찾아보았으나 찾지 못하였다.  있을 것 같긴 한데 생각보다 내용이 복잡하여 보류하였다.</li>
<li>비슷하게 org.geolatte.geom.cga 패키지 내부에 보면 <strong>Circle이라는 클래스</strong>가 있다. 이를 활용하면 Circle 객체를 만들 수는 있으나 얘를 어디에 사용할 수 있는지 찾지 못하였다... Circle 클래스는 Geometry를 상속받은 클래스가 아닌 독립적인 Class여서 <strong>(그냥 부모 클래스 자체가 하나도 없음)</strong> Geometry를 사용하는 메소드에 사용할 수도 없고, qureydsl-spatial 내부를 검색해봐도 Circle 객체가 사용되는 곳은 CircularArcLinearizer라는 클래스 밖에 없는데 이마저도 Circle 객체를 선형화 시키는 기능을 할 뿐이고 이를 Geometry로 사용하는 방법은 찾지 못하였다.</li>
<li>아직 Spatial에 관하여 공부가 부족하다는 것을 느낀다.</li>
</ul>
<br>

<h1 id="어떤-방법을-사용할까">어떤 방법을 사용할까?</h1>
<ul>
<li>결론부터 말하자면 1번이 아닌 <strong>2번 방식</strong>을 사용하는게 좋은 것 같다고 생각한다.</li>
</ul>
<h2 id="1-두가지-라이브러리에-종속적이지-않아도-된다">1. 두가지 라이브러리에 종속적이지 않아도 된다.</h2>
<ul>
<li>GeometricShapeFactory 기능은 Geolatte의 기능이 아니다. 결국 JTS와 Geolatte 두 라이브러리 모두에 종속적인 기능이 만들어지게 되는데 개인적으로 난 대안이 있거나 성능상 확실한 이점이 없다면 여러가지 라이브러리를 쓰는 것을 선호하지 않는다.</li>
</ul>
<h2 id="2-원하는-거리-기준이-뭔지-모르겠다">2. 원하는 거리 기준이 뭔지 모르겠다.</h2>
<pre><code class="language-java">private org.locationtech.jts.geom.Geometry createCircle(double x, double y, double radius) {
    GeometricShapeFactory factory = new GeometricShapeFactory();

    factory.setNumPoints(32);                            // ?
    factory.setCentre(new Coordinate(x, y));
    factory.setSize(radius * 2);                          // ?

    return factory.createCircle();
}</code></pre>
<ul>
<li>setNumPoints는 설명이라도 친절해서 활용은 못했지만 용도가 뭔지는 알 수 있다.</li>
<li><code>Sets the total number of points in the created {@link Geometry}. The created geometry will have no more than this number of points, unless more are needed to create a valid geometry.</code></li>
<li>하지만 정작 중요한 거리 기준인 반지름(radius)의 값이 어떤 단위로 작동하는지 알 수 없어 정확한 거리 계산을 할 수가 없다.</li>
<li>실제 테스트에서도 어림짐작하여 값을 넣어서 사용했을 뿐 아직까지 정확한 기준 단위를 찾지 못하였다.</li>
</ul>
<h2 id="3-심지어-2번이-더-빠르다">3. 심지어 2번이 더 빠르다.</h2>
<ul>
<li>몇번을 실행해봐도 <strong>쿼리 실행 속도도 더 빠르고 전체 로직 수행 시간 역시 더 빠르다</strong>.</li>
<li>GeometryShapeFactory로 Circle을 생성하는 시간이 생각보다 좀 걸리는 것 같다.</li>
<li>전체 로직 수행 속도 비교</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203744355-9add82bf-8b5f-41a8-b3bd-27ec20ed3c95.PNG" alt="circle" width="100%"></p>

<ul>
<li>각각 쿼리 수행 속도 비교<ul>
<li>1번: 5343ms, 2번: 4461ms</li>
</ul>
</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203744356-c0181c51-52d9-4bf7-9142-a0da08cbd539.PNG" alt="polygon" width="100%"></p>

<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203744351-81457a6f-5be5-4062-a5c3-8cb6bdd6984c.PNG" alt="circle" width="100%"></p>

<br>

<h1 id="마무리">마무리</h1>
<ul>
<li>최종적으로 <strong>Geolatte + QueryDSL Spatial + (ST_Contains + ST_Distance)</strong>를 사용하여 공간 데이터를 다루기로 결정하였습니다.</li>
<li>제가 처음 Spatial DB를 사용하기로 마음 먹고 공부를 시작하였을 때 많이 막막하고 자료도 없어서 힘들었기에 저와 같은 초보이시거나 새로이 도입해보고자 하시는 분이 이 포스팅들을 보시면서 Spatial DB의 첫걸음 용 나침반으로나마 사용될 수 있었으면 좋겠습니다.</li>
<li>아직 많이 미숙하고 저 또한 초보이기에 틀린 정보가 있거나 더 좋은 방법이 있다면 댓글 달아주시면 감사하겠습니다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Hibernate, QueryDSL 그리고 Spatial DB (3)]]></title>
            <link>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-3</link>
            <guid>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-3</guid>
            <pubDate>Tue, 20 Feb 2024 09:58:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="읽기에-앞서">읽기에 앞서</h1>
<ul>
<li>아직 QueryDSL Spatial의 내부 동작까지 이해한 상태가 아니라 깊은 내용은 없습니다. 단지 프로젝트에 적용하면서 발생한 에러들과 그것을 해결한 과정, 그리고 프로젝트에 필요한 부분을 성공적으로 QueryDSL로 이식시킨 내용을 기록하였습니다.</li>
</ul>
<h1 id="querydsl로-변경하기">QueryDSL로 변경하기</h1>
<ul>
<li>앞서 구현한 모든 기능들은 JPQL + Geolatte 조합이었다.</li>
<li>현재 진행하는 프로젝트에선 QueryDSL을 사용 중이고, 위의 조합으론 QueryDSL의 장점을 만끽하지 못하기에 결국 QueryDSL용 Spatial에 대해서 공부하기로 다짐하였다.</li>
</ul>
<h2 id="querydsl의-장점을-간단하게-알고-가자">QueryDSL의 장점을 간단하게 알고 가자</h2>
<ol>
<li>JPQL은 문자로 쿼리를 작성하지만 QueryDSL은 java 코드로 쿼리를 작성하기 때문에 <strong>컴파일 시점에 오류를 쉽게 발견할 수 있음</strong></li>
<li>또한 java 코드이기 때문에 IDE의 자동 완성 기능을 도움받을 수 있음</li>
<li><strong>동적 쿼리 작성에 상당히 편리함</strong> (주어지는 조건을 메서드로 추출하여 재사용성 또한 증가시킬 수 있음)</li>
<li>Projection을 사용하는 경우 (DTO 조회) JPQL에서처럼 패키지 경로를 모두 적어주는 번거로운 과정을 겪지 않아도 됨</li>
</ol>
<h2 id="querydsl-spatial-환경-설정">QueryDSL Spatial 환경 설정</h2>
<ul>
<li><p>QueryDSL Spatial 역시 별도의 Dependency를 추가해줘야 한다.</p>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203614659-2b584d55-5cc2-412e-baff-f1be2eaf4b11.PNG" alt="querydsl spatial dependency" width="100%"></p>
</li>
<li><p>아래 트리를 보면 Hibernate Spatial과 마찬가지로 Geolatte와 jts가 같이 설치되는 것을 확인할 수 있다.</p>
</li>
<li><p>우리는 이 중 빨간색 원으로 표시한 querydsl-spatial 라이브러리만을 사용할 것이다.</p>
</li>
<li><p>querydsl-sql-spatial과 querydsl-sql가 없어도 필요한 기능은 일단 구현할 수 있었기에 이 두 라이브러리는 일단 배제하였다. (둘에 대한 정보를 찾는 것이 너무 힘듬 -&gt; 추후에 공부하게 된다면 사용할 수 있으므로 굳이 제거하진 않았음)</p>
</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203616222-79f18c1f-4d5e-4b44-8544-7a571e267e90.PNG" alt="querydsl spatial tree" width="100%"></p>

<h2 id="querydsl-spatial-라이브러리-훑어보기">querydsl-spatial 라이브러리 훑어보기</h2>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203618454-b2d956b6-caf7-41fe-ad19-5db3e98e86ad.jpg" alt="querydsl spatial library" width="70%"></p>

<ul>
<li>위의 사진을 보면 총 3가지의 마커로 표시를 해놓은 것을 확인할 수 있는데, 패키지에 있는 메소드들 마다 사용하는 Geometry 라이브러리가 다르다.<ul>
<li>jts<ul>
<li><code>com.vividsolutions.jts.geom</code> 사용 (얘는 같이 설치조차 안 됨)</li>
</ul>
</li>
<li><span style="color:blue">locationtech</span><ul>
<li><code>org.locationtech.jts.geom</code> 사용</li>
</ul>
</li>
<li><span style="color:red">바깥의 나머지 쭉</span><ul>
<li><code>org.geolatte.geom</code> 사용</li>
</ul>
</li>
</ul>
</li>
<li>앞서 보았듯이 것처럼 우린 Geolatte를 쓰고 있으므로 바깥쪽에 있는 기능들 즉, com.querydsl.spatial 경로에 있는 기능들만 사용하면 되며, 그 중 빨간 물결로 표시해둔 <strong>GeometryExpressions</strong>가 핵심 기능이다.</li>
</ul>
<h2 id="geometryexpressionsasgeometry-사용하기">GeometryExpressions.asGeometry 사용하기</h2>
<ul>
<li>과정은 아래와 같다.<ol>
<li>GeometryExpressions의 메소드 중 <strong>asGeometry(Geometry)</strong>를 사용하여 복잡한 내부 과정을 거침</li>
<li>위 과정으로 반환된 객체 (<strong>추상 클래스 GeometryExpression를 상속</strong>함)에 정의된 메소드를 활용</li>
<li>최종적으로 QueryDSL에서 동적 쿼리 조건에 사용하는 <strong>BooleanExpresion으로 반환</strong>됨</li>
</ol>
</li>
<li>asGeometry 메소드는 제네릭 메소드로 Geometry를 받기로 명시되어 있다.</li>
<li>예제 코드</li>
</ul>
<pre><code class="language-java">/*
    아래 코드를 사용하면 ST_Contains Query가 나감
*/
private BooleanExpression pointContains(Geometry&lt;G2D&gt; polygon) {
    return GeometryExpressions
            .asGeometry(polygon)
               .contains(entity.point);
}</code></pre>
<h3 id="주의점-1">주의점 1</h3>
<ul>
<li>엔티티에 Spatial Type을 가진 컬럼이 있어야한다.</li>
<li>만약 엔티티의 좌표가 Point가 아닌 Lat, Lon로 구분된 형식으로 되어있으면 위 메소드들의 파라미터에 들어오는 Geometry 객체를 올바르게 생성할 수 없다.</li>
</ul>
<pre><code class="language-java">/*
    안 되는 예시
*/
private BooleanExpression pointContains(Geometry&lt;G2D&gt; polygon) {
    return GeometryExpressions
            .asGeometry(polygon)
               .contains(Wkt.fromWkt(&quot;Point(&quot; + entity.lat + &quot; &quot; + entity.lon &quot;)&quot;);
}</code></pre>
<ul>
<li>위처럼 사용하면 아래의 에러가 발생한다. Wkt Decoder가 fromWkt에 주어진 String값을 디코딩 못해서 발생하는 에러인듯 하다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203628124-19519f8e-6788-41d8-a3c4-a1e8261d871b.PNG" alt="wkt decode error" width="100%"></p>

<ul>
<li>처음 이 기능을 사용한 엔티티에 Point 컬럼이 따로 없이 Lat Lon만 가지고 있었기에 위의 예시처럼 사용했다가 발생한 에러하였다.</li>
<li>이유를 유추해보자면 디코딩하는 시점에 entity.lat, entity.lon의 실제값은 NumberPath 클래스를 가지고 있다. 이는 QueryDSL만의 특수한 클래스로 (타고 타고 올라가면 DslExpression 클래스의 자식) 내부 로직이 어떤식으로 동작하는지 아직은 잘 모르지만 확실한건 JPAQueryFactory로 생성한 쿼리가 실행되는 시점에서 Q타입 엔티티의 값을 가져다 쓰는 것이 일반적인 방법으론 불가능하다는 것이다. (방법이 있다면 가르쳐주세요!)</li>
<li>실제로 값을 출력해보면 아래와 같이 나온다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203630593-f261ca30-6abb-45a2-8d5c-56490a461967.PNG" alt="wkt decode error" width="100%"></p>

<ul>
<li>즉, Wkt Decoder는 String으로 주어진 WKT를 토대로 Geometry를 만들어내는 것인데 Lat, Lon 자리에 숫자가 아닌 엉뚱한 값이 들어가서 에러가 발생하는 것이다.</li>
<li>이 방법 저 방법 다 사용해봤으나 결국 나도 Point 컬럼을 추가하는 방법을 선택하였고 문제를 해결하였다. (사실 처음부터 그랬으면 겪지도 않았을 에러였겠지만 덕분에 이렇게 해선 안 된다는 것을 배웠고 이를 해결하고 공부가 되어 더 뿌듯하고 만족도도 높았다.)</li>
</ul>
<h3 id="주의점-2">주의점 2</h3>
<ul>
<li>현재 5.0.0 버전에서 MBRContains를 지원하지 않는 것 같다.</li>
<li>GeometryExpression 추상 클래스에 들어가보면 정말 많은 기능들을 지원하는데 MBRContains는 없다.</li>
<li>따라서 MBRContains를 굳이 써야겠다면 JPQL을 사용해야 한다.</li>
<li>신기한 건 이전 포스팅에서 언급했던 것처럼 MBRContains는 <strong>Hibernate Spatial에서도 지원한다는 내용이 명시되어 있지 않은데 JPQL을 이용하면 작동을 하였다.</strong> 그렇기 때문에 QueryDSL Spatial에도 방법이 있지 않을까 싶어서 찾아봤으나 이쪽은 정말 정보가 없어도 너무 없었고, 꼭 MBRContains를 써야하는 것도 아니기에 다음으로 미뤄두었다.</li>
</ul>
<h1 id="결론">결론</h1>
<ul>
<li>현재까지 QueryDSL Spatial이 Hibernate Spatial을 대체하지 못한 부분은 경험하지 못하였고, 성능적인 저하 또한 없습니다. 따라서 QueryDSL의 도입을 고려하시거나 또는 벌써 QueryDSL의 장점을 충분히 만끽하고 계신 분들이라면 QueryDSL Spatial 또한 가볍게 사용해보시는 것도 좋을 것 같습니다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Hibernate, QueryDSL 그리고 Spatial DB (2)]]></title>
            <link>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-2</link>
            <guid>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-2</guid>
            <pubDate>Tue, 20 Feb 2024 09:56:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="실제-프로젝트에-적용해보자">실제 프로젝트에 적용해보자</h1>
<ul>
<li>현재 진행하고 있는 프로젝트에 Hibernate Spatial을 적용 후, 테스트를 진행해보았다.</li>
</ul>
<h2 id="도입-목적">도입 목적</h2>
<ul>
<li><p>기존의 거리 기반 매장 탐색 방식을 개선하고자 <strong>Hibernate Spatial</strong>을 사용하기로 함</p>
</li>
<li><p>기존보다 성능이 나은지를 기대하고 테스트를 진행</p>
<ul>
<li><p>기존 조회 방식</p>
<ul>
<li>Full Table Scan 후, 현재 위치와 매장의 위치 간의 거리를 계산하여 조건에 맞는 데이터 별도 선정</li>
</ul>
</li>
<li><p>기대한 이유</p>
<ul>
<li>ST_Contains와 같은 포함 관계 함수에서의 공간 인덱스로 <strong>R-Tree</strong> 알고리즘이 사용되므로 데이터가 늘어날수록 Full Table Scan보다 더 빠를 것이라고 예상함</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="디테일한-테스트-설정">디테일한 테스트 설정</h2>
<ul>
<li><p>목적</p>
<ul>
<li><strong>현재 중심 좌표에서 3km 이내에 존재하는 모든 매장의 정보</strong>를 얻고자 함</li>
</ul>
</li>
<li><p>기댓값</p>
<ul>
<li>기존의 조회 방식보다 단축된 시간</li>
</ul>
</li>
<li><p>구분</p>
<ol>
<li>기존 조회 방식 (아래 두 방식과 달리 직선 거리를 모두 계산하므로 데이터 수가 더 많을 수 있음)</li>
<li>MBRContains를 활용한 경우</li>
<li>ST_Contains (혹은 ST_Within)을 활용한 경우 (둘은 상반된 기능이며 성능 차이는 없음)</li>
</ol>
</li>
<li><p>테스트 데이터</p>
<ul>
<li>총 1천만 111개</li>
<li>101만개, 901만개, 109개 - 3개의 그룹으로 나누어짐. <strong>(즉, Point 주소는 총 111개밖에 없음)</strong><ul>
<li>109개 (id 범위: 10000000 미만의 숫자 109개)</li>
<li>약 500만개 (id 범위: 10000000 ~ 15000000)<ul>
<li>우리 집 기준 3km 이내에 반드시 포함되는 좌표</li>
</ul>
</li>
<li>500만개 (id 범위: 15000001 ~ 20000000)<ul>
<li>우리 집 기준 3km 이내에 반드시 포함되지 않는 좌표</li>
</ul>
</li>
<li>중복 데이터로 인한 올바른 테스트 성능이 나올지 파악하고자 함</li>
<li>범위를 정하여 테스트 데이터의 개수를 조정해가며 전체적인 코드 성능을 확인하고자 진행할 예정</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="사전에-알아야-할-내용">사전에 알아야 할 내용</h2>
<h3 id="1-mbr이란">1. MBR이란?</h3>
<ul>
<li>Minimum Bounding Rectangle (최소 경계 사각형)</li>
<li>쉽게 얘기하면 <strong>주어진 Geometry를 모두 포함할 수 있는 최소 영역의 직사각형</strong>이다.</li>
<li>MBRContains(g1, g2)<ul>
<li>g1의 범위에 g2가 포함된다면 1(True)을 반환, 그렇지 않다면 0(False)을 반환</li>
<li>예제에서 g1으로 LineString을 사용했는데, 대각선으로 주어진 선(두 점이 남서, 북동에 있음)의 MBR을 구하면 내가 찾고자 하는 범위가 생성됨</li>
<li>생성한 범위에 g2(Point)가 존재하면 1을 반환함</li>
</ul>
</li>
<li>MBRContains의 경우, 공식 문서에서 지원하는 Dialect 메소드에 존재하지 않는데 써보면 정상 작동을 하는데, 왜 동작하는지 정확하게 설명된 정보를 아직까지 찾지 못하였다.</li>
</ul>
<h3 id="2-그-밖의-공간-관계-함수">2. 그 밖의 공간 관계 함수</h3>
<ul>
<li><strong><em><a href="https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#spatial-configuration-dialect">지원하는 Dialect 메소드 리스트</a></em></strong></li>
</ul>
<h3 id="3-jts-geolatte-간의-성능-차이">3. JTS, Geolatte 간의 성능 차이</h3>
<ul>
<li>사전에 Geometry로 두 타입을 모두 사용해봤으며 그 결과 <strong>유효한 성능 차이는 찾을 수 없었다.</strong></li>
<li>따라서 기능이 좀 더 많은 <code>Geolatte</code>로 결정하였다.</li>
</ul>
<h3 id="4-동일한-조건으로-테스트를-수행해도-쿼리-수행-시간이-일정하지는-않음">4. 동일한 조건으로 테스트를 수행해도 쿼리 수행 시간이 일정하지는 않음</h3>
<ul>
<li>각각의 테스트 수행 시간은 큰 변화가 없으나 쿼리 수행 시간은 Worst Case인 경우와 Best Case인 경우의 성능 차이가 생각보다 크다.</li>
<li>이를 감안하여 여러번 수행 한 후, 그 결과를 기록하고자 한다.</li>
</ul>
<h2 id="사용할-메소드들">사용할 메소드들</h2>
<ul>
<li><pre><code class="language-java">import org.geolatte.geom.G2D;
import org.geolatte.geom.Geometry;
import org.locationtech.jts.io.ParseException;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface StoreRepository extends JpaRepository&lt;Store, Long&gt;, StoreRepositoryCustom {

    /*
        범위 내의 모든 데이터 조회
     */
    @Query(&quot;select s from Store s &quot; +
            &quot;left outer join fetch s.file &quot; +
            &quot;where s.id &lt; :range&quot;)
    List&lt;Store&gt; findAllStoresLt(@Param(&quot;range&quot;) Long range);

    /*
        MBCContains
     */
    @Query(&quot;select s from Store s &quot; +
            &quot;left outer join fetch s.file &quot; +
            &quot;where mbrcontains(:lineString, s.point) = true and s.id &lt; 20000000&quot;)
    List&lt;Store&gt; getStoresByMbrContains(@Param(&quot;lineString&quot;) Geometry&lt;G2D&gt; lineString) throws ParseException;

    /*
        ST_Contains (Polygon)
     */
    @Query(&quot;select s from Store s &quot; +
            &quot;left outer join fetch s.file &quot; +
            &quot;where st_contains(:polygon, s.point) = true and s.id &lt; 20000000&quot;)
    List&lt;Store&gt; getStoresBySTContains(@Param(&quot;polygon&quot;) Geometry&lt;G2D&gt; polygon) throws ParseException;
}</code></pre>
</li>
<li><p>위의 기능들을 사용<strong>(JPQL)</strong>했으며, Geometry를 만드는 코드는 1부에 있으므로 별도로 첨부하지 않았다.</p>
</li>
<li><p>범위 내 모든 데이터 조회의 경우, 테스트 단에서 별도의 거리 계산 메소드가 따로 존재한다.</p>
</li>
<li><p>위에 조건으로 주어진 store_id의 범위만 조정하면서 테스트를 진행할 예정이다.</p>
</li>
<li><p>조회된 결과의 개수까지 구한 후 테스트 종료.</p>
</li>
</ul>
<h2 id="테스트-결과">테스트 결과</h2>
<h3 id="조건-1-조건-범위-내에-100-포함되는-테스트-약-100만개">조건 1. 조건 범위 내에 100% 포함되는 테스트 약 100만개</h3>
<h4 id="전체-테스트-코드-수행-시간">전체 테스트 코드 수행 시간</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203258746-4110964a-5950-486a-b9c0-36413f8834bd.PNG" alt="case1-1" width="100%"></p>

</li>
</ul>
<h4 id="단순-조회-후-거리-필터-적용">단순 조회 후 거리 필터 적용</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203259636-c555efe5-011b-4365-8c0e-8a1756abcc06.PNG" alt="case1-2" width="100%"></p>

</li>
</ul>
<h4 id="mbrcontains">MBRContains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203260310-c2bb2ac9-9d44-4966-860d-ec69d9921afb.PNG" alt="case1-3" width="100%"></p>

</li>
</ul>
<h4 id="st_contains">ST_Contains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203260489-aa06f932-8b3f-426a-9361-df2a08baceca.PNG" alt="case1-4" width="100%"></p>

</li>
</ul>
<h4 id="결론">결론</h4>
<ul>
<li>데이터 100만개 모두가 범위안에 포함되어있기 때문인지 아니면 데이터가 100만개 밖에 되지 않아서 그런건지 쿼리 수행 속도에는 차이가 없다... <strong>(오히려 기존 조회 방식이 가장 빨랐음)</strong></li>
<li>전체 수행 시간은 대략 1초 차이가 나는데 찾은 데이터 수가 9개 더 많으므로 유의미한 수치는 아닌듯 하다.</li>
</ul>
<h3 id="조건-2-조건-범위-내-포함-50만-미포함-50만-테스트-총-100만개">조건 2. 조건 범위 내 포함 50만, 미포함 50만 테스트 총 100만개</h3>
<h4 id="전체-테스트-코드-수행-시간-1">전체 테스트 코드 수행 시간</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203262936-8c936176-ada4-4b09-bb8f-bc6d5b2eb463.PNG" alt="case2-1" width="100%"></p>

</li>
</ul>
<h4 id="단순-조회-후-거리-필터-적용-1">단순 조회 후 거리 필터 적용</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203263266-076194c3-79c0-445b-ac2a-0444f8cb39b9.PNG" alt="case2-2" width="100%"></p>

</li>
</ul>
<h4 id="mbrcontains-1">MBRContains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203263284-c352a6d0-7d40-4895-812b-04551f4448e4.PNG" alt="case2-3" width="100%"></p>

</li>
</ul>
<h4 id="st_contains-1">ST_Contains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203263335-33f0f0ea-525b-4e6f-9201-ac91d0d64dd8.PNG" alt="case2-4" width="100%"></p>

</li>
</ul>
<h4 id="결론-1">결론</h4>
<ul>
<li>범위 내 데이터 포함 비율이 50 : 50인 경우, 성능상 이점이 뚜렷하게 나타났다.</li>
<li>테스트 별 로직 수행 시간: 기존 대비 <span style="color: red"><strong>최대 164%</strong></span> 성능 개선율을 보임</li>
<li>쿼리 수행 속도 : 기존 대비 <span style="color: red"><strong>최대 185%</strong></span> 성능 개선율을 보임</li>
</ul>
<h3 id="조건-3-조건-범위-내-포함-100만-미포함-100만-테스트-총-200만개">조건 3. 조건 범위 내 포함 100만, 미포함 100만 테스트 총 200만개</h3>
<h4 id="전체-테스트-코드-수행-시간-2">전체 테스트 코드 수행 시간</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203266409-ff2e8460-5537-4813-86cc-651238c19679.PNG" alt="case3-1" width="100%"></p>

</li>
</ul>
<h4 id="단순-조회-후-거리-필터-적용-2">단순 조회 후 거리 필터 적용</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203266467-226abbbd-f4b8-4ad8-b4ea-ba02f5efba99.PNG" alt="case3-2" width="100%"></p>

</li>
</ul>
<h4 id="mbrcontains-2">MBRContains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203267254-d8bc714b-662c-4e70-984d-9431e0c580dc.PNG" alt="case3-3" width="100%"></p>

</li>
</ul>
<h4 id="st_contains-2">ST_Contains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203267291-e47f4709-5fe8-42d6-b7df-91e9b69db998.PNG" alt="case3-4" width="100%"></p>

</li>
</ul>
<h4 id="결론-2">결론</h4>
<ul>
<li>범위 내 데이터 포함 비율이 50 : 50이면서 데이터의 개수를 2배 늘렸다.</li>
<li>테스트 별 로직 수행 시간: 기존 대비 <span style="color: red"><strong>최대 120%</strong></span> 성능 개선율을 보임</li>
<li>쿼리 수행 속도 : 기존 대비 <span style="color: red"><strong>최대 30%</strong></span> 성능 개선율을 보임</li>
<li>조건에 부합하는 데이터의 개수가 많아져 쿼리 수행속도의 이점은 줄어들었으나, 전체 데이터의 개수가 늘어 전체 수행 시간 속도의 이점은 여전히 강력하다.</li>
</ul>
<h3 id="조건-4-조건-범위-내-포함-20만-미포함-80만-테스트-총-100만개">조건 4. 조건 범위 내 포함 20만, 미포함 80만 테스트 총 100만개</h3>
<h4 id="전체-테스트-코드-수행-시간-3">전체 테스트 코드 수행 시간</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203268911-ba36a835-53e8-4835-9a6f-a566bd61d3d2.PNG" alt="case4-1" width="100%"></p>

</li>
</ul>
<h4 id="단순-조회-후-거리-필터-적용-3">단순 조회 후 거리 필터 적용</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203268949-fbdedf3a-ce5a-42bb-a17d-397a030d2f42.PNG" alt="case4-2" width="100%"></p>

</li>
</ul>
<h4 id="mbrcontains-3">MBRContains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203269024-7a417aaa-9f39-4b50-966e-f03aa040155b.PNG" alt="case4-3" width="100%"></p>

</li>
</ul>
<h4 id="st_contains-3">ST_Contains</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203269050-cfd642bc-27bc-47a2-94ae-6fb739885a63.PNG" alt="case4-4" width="100%"></p>

</li>
</ul>
<h4 id="결론-3">결론</h4>
<ul>
<li>범위 내 데이터 포함 비율이 20: 80</li>
<li>테스트 별 로직 수행 시간: 기존 대비 <span style="color: red"><strong>최대 367%</strong></span> 성능 개선율을 보임</li>
<li>쿼리 수행 속도 : 기존 대비 <span style="color: red"><strong>최대 77%</strong></span> 성능 개선율을 보임</li>
<li>이쯤 되니 보이는 것이, 테스트 별 로직 수행 시간은 확실히 주어진 데이터의 조건에 따라 크게 개선되고, 쿼리 수행 속도는 DB와의 연결 상태에 따라 생각보다 큰 오차 범위를 가지는 것 같다.</li>
</ul>
<h3 id="조건-5-전체-데이터-조회-1000만개---비율-약-5--5">조건 5. 전체 데이터 조회 (1000만개 - 비율 약 5 : 5)</h3>
<h4 id="전체-테스트-코드-수행-시간-4">전체 테스트 코드 수행 시간</h4>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203271577-511c8ce3-e2a6-4f90-89b1-798729b6b5fc.PNG" alt="case5-1" width="100%"></p>
</li>
<li><p><strong>Out Of Memory</strong> 에러 발생</p>
</li>
<li><p>로컬에서도 에러가 발생하면 이보다 열악한 EC2 환경(가용 메모리 1GB)에서 무조건 OOM이 발생할 것이다.</p>
</li>
<li><p><strong>이렇게 데이터가 많을 땐 사실상 사용할 수가 없는 방법</strong>.</p>
</li>
<li><p>잘 보면 쿼리 자체는 수행이 됐다.</p>
</li>
<li><p>OOM을 해결하기 위한 방법을 추가적으로 공부해봐야겠다.</p>
</li>
</ul>
<h3 id="최종-결론">최종 결론</h3>
<ul>
<li>기존 조회 방법에 비하여 확실하게 성능이 개선됐다.</li>
<li>테스트 별 로직 수행 시간은 눈에 띄게 개선됐고, 쿼리 수행 속도 역시 Worst Case에서도 성능 개선이 항상 이루어졌으므로 기존 로직을 변경하고 Hibernate Spatial 기능을 사용하는 것으로 결정하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Hibernate, QueryDSL 그리고 Spatial DB (1)]]></title>
            <link>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-1</link>
            <guid>https://velog.io/@yangwon-park/JPA-Hibernate-QueryDSL-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Spatial-DB-1</guid>
            <pubDate>Tue, 20 Feb 2024 09:55:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="읽기에-앞서">읽기에 앞서</h1>
<ol>
<li>총 4개의 포스팅으로 이루어져 있습니다.</li>
<li>이 게시글은 Spatial DB 자체에 대하여 심도있게 공부하고 정리하는 목적이 아닌 구글링을 통해서 얻을 수 있는 정보가 많이 없고 오래된 경우가 허다하여 직접 Spatial Data를 사용하기 위하여 공부하고 알게 된 것들을 정리하는 목적임을 미리 알려드립니다.</li>
<li><strong><em><a href="https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#spatial">Hibernate Spatial 공식 문서</a></em></strong>는 5.6 버전으로 설정되어 있습니다. (최신 버전은 Spatial 설명이 좀 부실한 것 같습니다 ㅠㅠ) 보다 최신의 문서를 보고 싶으시면 좌측 상단의 current로 버전을 변경해서 확인하세요.</li>
<li>Dependency Tree 보는 법은 아래와 같습니다.</li>
</ol>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203046888-e8f05d3b-1b14-4067-be0c-5421d27568a9.PNG" alt="dependency tress" width="100%"></p>

<h2 id="개발-환경">개발 환경</h2>
<ul>
<li>JPA Hibernate 5.6.5</li>
<li>QueryDSL 5.0.0</li>
<li>MariaDB 10.6.10</li>
<li>빌드 관리 도구 : Gradle</li>
</ul>
<br/>

<h1 id="spatial-db란">Spatial DB란?</h1>
<ul>
<li><p><strong><em><a href="https://en.wikipedia.org/wiki/Spatial_database">위키피디아 링크</a></em></strong></p>
</li>
<li><p>Spatial DB, 우리 말로 공간 DB란 <strong>공간 데이터</strong>를 다루기 위한 특수 목적으로 사용되는 DB</p>
</li>
<li><p>목적</p>
<ul>
<li>좌표계로 표현할 수 있는 공간 객체들을 데이터화하여 보다 쉽게 저장하기 위함.</li>
<li>저장한 공간 데이터에 손쉬운 연산을 수행하기 위함.</li>
</ul>
</li>
<li><p>흔히 접할 수 있는 RDBMS에 공간 데이터를 다루기 위한 기능들이 탑재되어 있음.</p>
</li>
</ul>
<h2 id="공간-데이터-타입">공간 데이터 타입</h2>
<ul>
<li><p>출처 : <strong><em><a href="https://sparkdia.tistory.com/24">참고 블로그</a></em></strong></p>
</li>
<li><p>자주 사용되는 공간 데이터 타입</p>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/202889336-40978b30-3165-460f-8ec7-27c4ab60f14c.png" alt="공간 데이터 타입" width="100%"></p>
</li>
<li><table>
<thead>
<tr>
<th align="left"><strong>공간 데이터 타입</strong></th>
<th align="left"><strong>정의</strong></th>
<th align="left"><strong>SQL 예</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="left">Point</td>
<td align="left">좌표 공간 한 지점의 위치 <br /> (경도, 위도 순서로 입력)</td>
<td align="left">POINT(10 10)</td>
</tr>
<tr>
<td align="left">LineString</td>
<td align="left">다수의 Point를 연결해주는 선분</td>
<td align="left">LINESTRING(10 10, 20 20, 30 30)</td>
</tr>
<tr>
<td align="left">Polygon</td>
<td align="left">다수의 선분들이 연결되어 닫혀 있는 다각형<br />각각의 Point의 처음과 끝은 같은 좌표를 공유해야 하며<br /> Polygon 전체에서 처음과 끝은 같은 Point로 이루어져야 함</td>
<td align="left">POLYGON((10 10, 10 20, 20 20, 20, 10, 10 10))</td>
</tr>
<tr>
<td align="left">Multi-Point</td>
<td align="left">다수의 Point 집합</td>
<td align="left">MULTIPOINT(10 10, 30 20)</td>
</tr>
<tr>
<td align="left">Multi-LineString</td>
<td align="left">다수의 LineString 집합</td>
<td align="left">MULTILINESTRING((10 10, 20 20), (20 15, 30 40))</td>
</tr>
<tr>
<td align="left">Mulit-Polygon</td>
<td align="left">다수의 Polygon 집합</td>
<td align="left">MULTIPOLYGON ((( 10 10, 15 10, 20 15, 20 25, 15 20, 10 10 )) , (( 40 25, 50 40, 35 35, 25 10, 40 25 )) )</td>
</tr>
<tr>
<td align="left">GeomCollection</td>
<td align="left">모든 공간 데이터들의 집합</td>
<td align="left">GEOMETRYCOLLECTION ( POINT (10 10), LINESTRING (20 20, 30 40), POINT (30 15) )</td>
</tr>
</tbody></table>
</li>
</ul>
<h2 id="공간-관계-함수">공간 관계 함수</h2>
<ul>
<li><p>출처 : <strong><em><a href="https://sparkdia.tistory.com/25">참고 블로그</a></em></strong></p>
</li>
<li><p>두 공간 객체 간의 관계를 일반 데이터 타입으로 반환해주는 함수 (Boolean 또는 Number)</p>
</li>
<li><p>MySQL에서 제공해주는 공간 관계 함수 중 자주 사용되는 함수들</p>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/202889704-6167a808-4531-47f5-9db9-fcebd0660d44.png" alt="공간 관계 함수" width="100%"></p>
</li>
<li><table>
<thead>
<tr>
<th align="left"><strong>공간 관계 함수</strong></th>
<th align="left"><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td align="left">ST_Equals (g1 Geometry, g2 Geometry)  : Boolean</td>
<td align="left">g1과 g2가 동일하면 True를 반환하고 상이하다면 False를 반환</td>
</tr>
<tr>
<td align="left">ST_Disjoint (g1 Geometry, g2 Geometry)    : Boolean</td>
<td align="left">g1과 g2가 겹치는 곳 없다면 True를 반환하고, 겹치는 곳이 있으면 False를 반환</td>
</tr>
<tr>
<td align="left">ST_Within (g1 Geometry, g2 Geometry)  : Boolean</td>
<td align="left">g1가 g2 영역 안에 포함된 경우 True를 반환하고 그렇지 않은 경우 False를 반환 (Contains와 반대)</td>
</tr>
<tr>
<td align="left">ST_Overlaps (g1 Geometry, g2 Geometry)   : Boolean</td>
<td align="left">g1과 g2 영역 중 교집합 영역이 존재하는 경우 True를 반환하고 존재하지 않는 경우 False를 반환</td>
</tr>
<tr>
<td align="left">ST_Intersects (g1 Geometry, g2 Geometry)   : Boolean</td>
<td align="left">g1과 g2 영역 간에 교집합이 존재하는 경우 True를 반환하고 그렇지 않은 경우 False를 반환</td>
</tr>
<tr>
<td align="left">ST_Contains (g1 Geometry, g2 Geometry)  : Boolean</td>
<td align="left">g2가 g1 영역 안에 포함된 경우 True를 반환하고 그렇지 않은 경우 False를 반환 (Within과 반대)</td>
</tr>
<tr>
<td align="left">ST_Touches (g1 Geometry, g2 Geometry)  : Boolean</td>
<td align="left">g1과 g2가 경계 영역에서만 겹치는 경우 결과 값으로 True를 반환하며 경계 영역 외에서 겹치거나 겹치는 곳이 없다면 False를 반환</td>
</tr>
<tr>
<td align="left">ST_Distance (g1 Geometry, g2 Geometry)  : Double</td>
<td align="left">g1과 g2간의 거리를 반환</td>
</tr>
</tbody></table>
</li>
</ul>
<h2 id="공간-연산-함수">공간 연산 함수</h2>
<ul>
<li><p>출처 - <strong><em><a href="https://sparkdia.tistory.com/26">참고 블로그</a></em></strong></p>
</li>
<li><p>두 공간 객체의 연산 결과를 <strong>새로운 공간 객체</strong>로 반환해주는 함수</p>
</li>
<li><p>MySQL에서 제공해주는 공간 연산 함수 중 자주 사용되는 함수들</p>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/202889832-570df40c-2a39-4911-a282-d59ef3d11992.png" alt="공간 관계 함수" width="100%"></p>
</li>
<li><table>
<thead>
<tr>
<th><strong>공간 연산 함수</strong></th>
<th><strong>설명</strong></th>
</tr>
</thead>
<tbody><tr>
<td>ST_Intersection (g1 Geometry, g2 Geometry)  : Geometry</td>
<td>g1과 g2의 교집합인 공간 객체를 반환</td>
</tr>
<tr>
<td>ST_Union (g1 Geometry, g2 Geometry)  : Geometry</td>
<td>g1과 g2의 합집합인 공간 객체를 반환</td>
</tr>
<tr>
<td>ST_Difference (g1 Geometry, g2 Geometry)  : Geometry</td>
<td>g1과 g2의 차집합인 공간 객체를 반환</td>
</tr>
<tr>
<td>ST_Buffer (g1 Geometry, d Double )  : Geometry</td>
<td>g1에서 d 거리만큼 확장된 공간 객체를 반환</td>
</tr>
<tr>
<td>ST_Envelope (g1 Geometry)  : Polygon</td>
<td>g1을 포함하는 최소 MBR인 Polygon을 반환</td>
</tr>
<tr>
<td>ST_StartPoint (l1 LineString)  : Point</td>
<td>l1의 첫 번째 Point를 반환</td>
</tr>
<tr>
<td>ST_EndPoint (l1 LineString)  : Point</td>
<td>l1의 마지막 Point를 반환</td>
</tr>
<tr>
<td>ST_PointN (l1 LineString)  : Point</td>
<td>l1의 n 번째 Point를 반환</td>
</tr>
</tbody></table>
</li>
</ul>
<br>

<h1 id="jpa-hibernate와-spatial-data">JPA Hibernate와 Spatial Data</h1>
<h2 id="hibernate-spatial">Hibernate Spatial</h2>
<ul>
<li><p><strong><em><a href="https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#spatial">Hibernate Spatial 공식 문서</a></em></strong></p>
</li>
<li><p>Hibernate에서 Spatial Data를 사용하려면 <strong>org.hibernate:hibernate-core</strong> 의존성 외에 아래처럼 별도의 의존성, <strong>org.hiberate:hibernate-spatial</strong>을 추가해줘야만 한다.</p>
</li>
<li><p>공식 문서에 따르면 Hibernate 5.0에 들어서야 정식 Hibernate ORM Project로 소속되었다고 한다.</p>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203054915-36a074b9-6b63-48ac-b6d2-e508086045c4.PNG" alt="hibernate spatial dependency" width="100%"></p>
</li>
<li><p>아래 트리와 같이 라이브러리들이 주입되는데 이 중 유의깊게 봐야할 것들은 총 3개가 있다.</p>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203046413-f4f89d36-de9c-43cd-81cd-5103a4d5a27c.PNG" alt="hibernate spatial dependency tree" width="100%"></p>
</li>
<li><p><strong>org.hibernate:hibernate-core</strong></p>
<ul>
<li>hibernate 핵심 기능들이 들어있음</li>
</ul>
</li>
<li><p><strong>org.geolatte:geolatte-geom</strong>, <strong>org.locationtech.jts:jts-core</strong></p>
<ul>
<li>hibernate spatial에서 지원하는 Geometry Model</li>
</ul>
</li>
<li><p>그 밖에 log 관련 라이브러리와 PostgreSQL 라이브러리가 들어오는데, PostgreSql 라이브러리가 들어오는 이유는 유추하기로 WKB/WKT를 위한 Default Dialects가 Postgis에서 비롯되어서인듯 하다.(?)</p>
<ul>
<li>For historical and practical reasons. The default dialects for WKB/WKT are those used in <a href="http://postgis.org/">Postgis</a>.</li>
<li>Geolatte Github 참고</li>
</ul>
</li>
<li><p>위의 사진을 보면, jts 의존성 주입 부분을 주석 처리 해놓은 것을 볼 수 있는데, 이는 hibernate spatial에 대해서 하나도 모르고 구글링으로 찾은 관련된 라이브러리를 다 받아서 사용해보던 중 org.locationtech.jts 라이브러리를 별도로 추가해야만 사용할 수 있다고 착각한 흔적이다. (hibernate 5.2 이전 버전까지는 별도로 추가했어야 되는 것 같긴 한데 정확히는 잘 모르겠음. 만약 자신이 사용하는 hibernate 버전이 5.6.5가 아니라면 나처럼 Dependency Tree를 확인하여 함께 설치되었는지 꼭 확인해보자.)</p>
</li>
</ul>
<h2 id="공간-데이터-타입-지원-라이브러리-jts-vs-geolatte">공간 데이터 타입 지원 라이브러리: JTS vs Geolatte</h2>
<ul>
<li>앞서 살펴봤다 싶이 hibernate spatial 라이브러리를 추가하면 2개의 공간 데이터 타입을 지원해주는 라이브러리가 별도로 들어온다. 이에 대해서 알아보자.</li>
</ul>
<h3 id="그전에-잠깐">그전에 잠깐!</h3>
<ul>
<li>구글링을 하다보면 작성된지 좀 지난 문서의 경우 jts의 패키지 경로가 org.locationtech.jts.geom이 아닌 com.vividsolutions.jts.geom인 경우가 있다.</li>
<li>아래 링크에서 확인할 수 있듯이 vividsolutions이 이전 버전의 산물이고 2016년 11월 3일 기준으로 the Eclipse Location Tech working group으로 이관(?)되어 개발되고 있다고 한다. 이로 인해 이관일 이후에 배포된 라이브러리 패키지명 또한 함께 바뀐 것이다.</li>
<li>2022년 11월인 작성일 기준에서 vividsolutions 시절 기능을 사용할 일은 없다고 판단하므로 hibernate spatial 라이브러리만 추가했을 때 별도의 Geometry Type이 함께 추가되지 않는다면 org.locationtech.jts.geom을 받아서 설치하면 될 것 같다.</li>
<li><strong><em><a href="https://gis.stackexchange.com/questions/246588/difference-in-jts-from-vivid-solution-and-locationtech">참고 링크 - Difference in JTS from vividsolutions and locationtech</a></em></strong></li>
</ul>
<h3 id="jts">JTS</h3>
<ul>
<li><strong>Spatial data types are not part of the Java standard library, and they are absent from the JDBC specification. Over the years <a href="http://tsusiatsoftware.net/jts/main.html">JTS</a> has emerged the <em>de facto</em> standard to fill this gap.</strong> - 공식 문서</li>
<li>공식 문서에서 보면 알 수 있듯이, JTS는 사실상 Spatial data type의 <code>표준</code>이다.</li>
<li><strong><em><a href="https://www.tsusiatsoftware.net/jts/main.html">공식 링크</a></em></strong></li>
</ul>
<h3 id="geolatte-geom">Geolatte Geom</h3>
<ul>
<li><strong>Geolatte-geom (also written by the lead developer of Hibernate Spatial) is a more recent library that supports many features specified in SQL/MM but not available in JTS (such as support for 4D geometries, and support for extended WKT/WKB formats). Geolatte-geom also implements encoders/decoders for the database native types. Geolatte-geom has good interoperability with JTS. Converting a Geolatte <code>geometry</code> to a JTS `geometry, for instance, doesn’t require copying of the coordinates. It also delegates spatial processing to JTS.</strong> - 공식 문서</li>
<li>Geolatte Geom 또한 마찬가지로 Hibernate Spatial 쪽에서 개발한 것이며, JTS에 비하여 더 최신 라이브러리이기에 보다 더 많은 기능을 지원한다고 나와있다.</li>
<li><strong><em><a href="https://github.com/GeoLatte/geolatte-geom">Geolatte Geom Github</a></em></strong></li>
</ul>
<h3 id="그래서-뭘-써야할까">그래서 뭘 써야할까?</h3>
<ul>
<li>사실 이 부분에 있어서 정답은 없다고 생각한다. 두 라이브러리 모두 최근까지도 Maven Repository에 업데이트 됐을 정도(JTS - Jun 21, 2022, Geolatte Geom - Oct 08, 2022) 로 꾸준하게 업데이트되고 있을 만큼 기술적인 지원이 꾸준하기 때문이다.</li>
<li>나의 경우, 두가지 모두를 사용해본 후 결국 <strong>Geolatte</strong>를 선택했는데 이유는 아래와 같다.<ul>
<li>보다 최신에 나온 라이브러리이므로 지원하는 기능이 더 많음.</li>
<li><strong>JTS의 기능과 상호운용성이 매우 좋음.</strong></li>
</ul>
</li>
<li>조심!<ul>
<li>두 Geometry 타입 모두 기본 자료형이 <code>Geometry</code>이다. 이로 인하여 처음 구글링을 생각없이 하다 보면 지금 내가 사용하고 있는 Geometry가 둘 중 어느 <code>Geometry</code>인지 알 수 없게 된다. 아니 정확하겐 둘이 다른지 조차 모르고 사용한다. (둘을 변환할 순 있어도 서로 다른 자료형이므로 에러가 발생함.)</li>
<li>두 타입을 모두 사용하는 것은 아무런 문제가 없으나 정확하게 본인이 사용하는 <code>Geometry</code>가 무엇인지는 정확히 알고 있어야 공부를 할 때 당황하지 않을 수 있다. (나 역시 같은 <code>Geometry</code>인데 왜 안 될까 하면서 시간을 많이 허비하였다.)</li>
</ul>
</li>
</ul>
<h2 id="dialect-설정">Dialect 설정</h2>
<ul>
<li>의존성 주입을 완료한 후, 한가지 설정을 더 해줘야 한다. 바로 사용하는 DB 벤더에 해당하는 Dialect를 설정해주는 것인데 기본적으로 Default로 설정되는 Dialect에선 Spatial Data를 지원하지 않으므로 본인의 버전에 맞는 Dialect를 찾아서 본인 프로젝트 설정 파일에 아래와 같이 설정해주면 된다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203067655-9af3a640-4d60-4c31-9702-9fa2f0e74223.PNG" alt="spatial dialect" width="100%"></p>

<ul>
<li>현재 MariaDB 버전이 10.6이지만 지금 사용하는 hibernate spatial에서 지원하는 MariaDB용 SpatialDialect의 최신 버전이 10.3이라 사용 중이며 현재까지 문제없이 정상 작동 중이다.</li>
<li>대부분의 DB 벤더에 따른 Dialect 패키지는 <strong><em><a href="https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#spatial-configuration-dialect">공식 문서</a></em></strong>에 명시되어 있으나 MariaDB의 경우 별다른 설명이 없어서 당황하였다. (참고할 자료 자체를 찾는게 너무 어려웠음)</li>
<li>만약 나처럼 MariaDB를 사용 중이거나 본인의 DB 벤더에 알맞은 Dialect를 찾고 싶다면 아래의 사진처럼 라이브러리를 직접 들어가서 확인해보길 바란다.</li>
</ul>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203073383-a4602a36-1ffa-4aaa-bd7f-2abee9c3726a.PNG" alt="dialect 패키지" width="70%"></p>

<h3 id="만약-dialect를-선언해주지-않는다면">만약 Dialect를 선언해주지 않는다면?</h3>
<ul>
<li><p>MariaDB와 MySQL 두 경우 모두 서버 자체는 실행이 되나 발생하는 에러가 좀 다르다.</p>
<ul>
<li>(다른 DB 벤더는 확인해보지 못했습니다. 만약 어떤 에러가 발생하는지 아시는 분들은 저한테 가르쳐주시면 감사하겠습니다!)</li>
</ul>
</li>
<li><p>MariaDB의 경우</p>
<ul>
<li>아무런 에러도 발생하지 않음. 쿼리도 정상적으로 날라가는 것을 확인하였음. 그래서 정상 작동이 되는 줄 알고 놀라워 했으나, 데이터 자체가 하나도 조회되지 않음.</li>
<li>추후에 작성하겠지만, MBRContains는 지원 Dialect에 없는데도 동작이 됨. 따라서 Dialect와 관계없는 기본적으로 제공하는 기능으로 알았으나 데이터가 조회되지 않는 것을 확인하여 MBRContains가 왜 지원되는지 이유를 알 수 없어짐.</li>
</ul>
</li>
<li><p>MySQL의 경우</p>
<ul>
<li><p>콘솔에 정직하게 Spatial 관련 에러가 발생함.</p>
</li>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/203242052-df94b832-d4d5-47b1-ae83-78371eb568d9.png" alt="no spatial error" width="100%"></p>

</li>
</ul>
</li>
</ul>
<br/>

<h1 id="코드로-알아보자">코드로 알아보자</h1>
<ul>
<li>공부하면서 알게 된 JTS, Geolatte Geom(이하 Geolatte)에 관한 세팅과 사용법을 알아볼 차례다.</li>
<li>다양한 기능들이 존재하지만 현재까지 내가 직접 사용하면서 검증이 된 것들 위주로 정리하고자 한다.</li>
</ul>
<h2 id="공통">공통</h2>
<ul>
<li>엔티티에 Geometry Type을 추가하려면 다른 Type들과 똑같이 원하는 Geometry 타입을 필드에 선언해주면 된다.</li>
<li>이때, JTS와 달리 Geolatte는 Geometry Type이 Generic으로 선언돼있어서 타입을 명시해줘야 하는데, 이 부분은 아래에서 다시 정리하도록 하겠다.</li>
</ul>
<h2 id="jts-1">JTS</h2>
<ul>
<li><strong><em><a href="https://www.baeldung.com/hibernate-spatial">참고 링크</a></em></strong></li>
</ul>
<h3 id="1-wellknowntext-이하-wkt-읽기">1. WellKnownText (이하 WKT) 읽기</h3>
<ul>
<li><p><strong><em><a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">WKT - 위키피디아</a></em></strong></p>
</li>
<li><p>WKT란 공간 데이터를 표현해주는 텍스트 마크업 언어이다.</p>
<ul>
<li>ex) 좌표 (10, 20)에 점이 하나 찍혀있음 - POINT(10 20)</li>
</ul>
</li>
<li><p>JTS에선 WKTReader().read(text)를 활용하여 이를 읽어들일 수 있다.</p>
<pre><code class="language-java">@Test
@DisplayName(&quot;WKT 읽기&quot;)
void hibernate_spatial_test() throws ParseException {
        String pointFormat = String.format(&quot;POINT(%f %f)&quot;, 129.175759994618, 35.1710366410643);
        String lineStringFormat = String.format(&quot;LINESTRING(%f %f, %f %f)&quot;, 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965);
        String polygonFormat = String.format(&quot;POLYGON((%f %f, %f %f, %f %f))&quot;, 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965, 129.20790463400292, 35.182416023937336);

        Geometry point = wktToGeometry(pointFormat);
        Geometry lineString = wktToGeometry(lineStringFormat);
        // polygon : startPoint와 endPoint가 일치해야만 함
        Geometry polygon = wktToGeometry(polygonFormat);

        assertThat(point.getGeometryType()).isEqualTo(&quot;Point&quot;);
        assertThat(lineString.getGeometryType()).isEqualTo(&quot;LineString&quot;);
        assertThat(polygon.getGeometryType()).isEqualTo(&quot;Polygon&quot;);
    }

    /*
        WKT를 읽어들이는 메소드
    */
    private Geometry wktToGeometry(String text) throws ParseException {
        return new WKTReader().read(text);
    }</code></pre>
</li>
</ul>
<h3 id="2-geometricshapefactory">2. GeometricShapeFactory</h3>
<ul>
<li><p>Geometry Type을 원하는 모양으로 만들 수 있게 해주는 클래스.</p>
</li>
<li><p>모든 도형이 가능한 것은 아니고 가능한 메소드가 미리 구현되어 있다.</p>
</li>
<li><p>각각의 메소드 명이 매우 직관적이라 이해하기가 쉬운 편이다.</p>
</li>
<li><p>Geolatte에서 이 클래스에 해당하는 기능을 아직 찾지 못하였다.</p>
<pre><code class="language-java">/*
    GeomertricShapeFactory를 활용하여 원을 만듬
*/
private Geometry createCircle(double x, double y, double radius) {
        GeometricShapeFactory factory = new GeometricShapeFactory();

        factory.setNumPoints(32);    // 만들어진 Geometry 내부에 생성되는 Point의 최대 개수
        factory.setCentre(new Coordinate(x, y));
        factory.setSize(radius * 2);

        return factory.createCircle();
}</code></pre>
</li>
</ul>
<h2 id="geolatte-geom-1">Geolatte Geom</h2>
<h3 id="1-wkt-읽기">1. WKT 읽기</h3>
<ul>
<li><pre><code class="language-java">@Test
@DisplayName(&quot;WKT 읽기&quot;)
void fromWkt_test() {
    String pointFormat = String.format(&quot;POINT(%f %f)&quot;, 129.175759994618, 35.1710366410643);
    String lineStringFormat = String.format(&quot;LINESTRING(%f %f, %f %f)&quot;, 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965);
    String polygonFormat = String.format(&quot;POLYGON((%f %f, %f %f, %f %f))&quot;, 129.20790463400292, 35.182416023937336, 129.16123271344156, 35.14426110121965, 129.20790463400292, 35.182416023937336);

    Geometry&lt;?&gt; point = Wkt.fromWkt(pointFormat);
    Geometry&lt;?&gt; lineString = Wkt.fromWkt(lineStringFormat);
    // polygon : startPoint와 endPoint가 일치해야만 함
    Geometry&lt;?&gt; polygon = Wkt.fromWkt(polygonFormat);

    assertThat(point.getGeometryType()).isEqualTo(GeometryType.POINT);
    assertThat(lineString.getGeometryType()).isEqualTo(GeometryType.LINESTRING);
    assertThat(polygon.getGeometryType()).isEqualTo(GeometryType.POLYGON);
}</code></pre>
</li>
</ul>
<h3 id="2-position">2. Position</h3>
<ul>
<li><p>JTS와는 달리 Geolatte의 Geometry는 제네릭으로 선언되어있으며, 제네릭의 타입으로 설정할 수 있는 특별한 추상 클래스가 Position이다.</p>
</li>
<li><p>Position을 상속받은 클래스들 (쉽게 말해 Position의 종류)</p>
<p align="center"><img src="https://user-images.githubusercontent.com/97505799/203162428-8aea4cda-eb80-4a98-a94f-449fa4a52a75.PNG" alt="position" width="70%"></p>
</li>
<li><p>각각의 클래스에 들어가보면 매우 친절하게 설명이 다 적혀있으나 M, V 그리고 M이 붙어있는 클래스들의 용도는 아직 직접 사용해본 적이 없어 감이 잘 오지 않는다.</p>
<ul>
<li>G2D : Geographic Coordinate 즉, 지리 좌표계 (위도와 경도로 이루어진 좌표계)</li>
<li>C2D : Cartesian Coordinate 즉, 데카르트 좌표계 (수학에서 보던 x, y로 이루어진 좌표계)</li>
<li>G3D : G2D + Altitude (고도)</li>
<li>C3D : C2D + Z value (3차원)</li>
</ul>
</li>
<li><p>Position을 활용하여 엔티티에 Point를 생성하면 아래와 같다.</p>
</li>
<li><pre><code class="language-java">@Column(columnDefinition = &quot;Point&quot;)
private Point&lt;G2D&gt; point;</code></pre>
</li>
</ul>
<h3 id="3-coordinatereferencesystem-class-crs---좌표-참조-시스템">3. CoordinateReferenceSystem Class (CRS - 좌표 참조 시스템)</h3>
<ul>
<li><strong><em><a href="https://docs.qgis.org/3.22/en/docs/gentle_gis_introduction/coordinate_reference_systems.html">CRS 문서</a></em></strong></li>
<li>Geolatte는 WKT를 활용하지 않고 Geometry를 생성할 수 있는 DSL(도메인 특화 언어) Class를 가지고 있다.</li>
<li>이 때, 생성할 Geometry의 기준 CRS를 정할 수 있는데, 이를 정의해놓은 클래스가 바로 CoordinateReferenceSystem Class이다.</li>
<li>난 WGS84 (세계 지구 좌표 시스템)을 사용하였다. (G2D와 연동)</li>
</ul>
<h3 id="4-dsl-domain-specific-language">4. DSL (Domain Specific Language)</h3>
<ul>
<li><p>Geloatte에서 Geometry 객체를 생성할 수 있는 도메인 특화 언어.</p>
</li>
<li><p>DSL.java 클래스를 들여다보면 상세히 설명이 적혀있으므로 자세한 설명은 생략하고 예시를 바로 보자.</p>
</li>
<li><pre><code class="language-java">@Test
@DisplayName(&quot;DSL 사용&quot;)
void dsl_test() {
    Point&lt;G2D&gt; point = point(WGS84, g(4.33,53.21));
    LineString&lt;G2D&gt; lineString = linestring(WGS84,g(4.43,53.21),g(4.44,53.20),g(4.45,53.19));
    Polygon&lt;G2D&gt; polygon = polygon(WGS84,ring(g(4.43,53.21),g(4.44,53.22),g(4.43,53.21)));

    assertThat(point.getGeometryType()).isEqualTo(GeometryType.POINT);
    assertThat(lineString.getGeometryType()).isEqualTo(GeometryType.LINESTRING);
    assertThat(polygon.getGeometryType()).isEqualTo(GeometryType.POLYGON);
}</code></pre>
</li>
<li><p>위처럼 WKT를 사용하지 않고 Geometry 객체를 메소드로 직접 생성할 수 있다.</p>
</li>
</ul>
<h3 id="5-orggeolattegeomjtsjts">5. org.geolatte.geom.jts.JTS</h3>
<ul>
<li><p>이 클래스의 from 메소드를 활용하면 JTS Geometry 객체를 Geolatte Geometry 객체로 아주 간단하게 변경할 수 있다.</p>
</li>
<li><pre><code class="language-java">/*
    GeomertricShapeFactory를 활용하여 원을 만듬
*/
private Geometry createCircle(double x, double y, double radius) {
        GeometricShapeFactory factory = new GeometricShapeFactory();

        /*
            만들어진 Geometry 내부에 생성되는 Point의 최대 개수 
            용도를 아직 잘 모르겠음
        */
        factory.setNumPoints(32);                            
        factory.setCentre(new Coordinate(x, y));
        factory.setSize(radius * 2);

        return factory.createCircle();
}

// 이 Geometry는 JTS의 Geometry이다!!
Geometry circle = createCircle(lat, lon, dist);
org.geolatte.geom.Geometry&lt;?&gt; geoLatteCircle = JTS.from(circle);</code></pre>
</li>
</ul>
<br/>]]></description>
        </item>
        <item>
            <title><![CDATA[Migrating to Github Actions]]></title>
            <link>https://velog.io/@yangwon-park/Migrating-to-Github-Actions</link>
            <guid>https://velog.io/@yangwon-park/Migrating-to-Github-Actions</guid>
            <pubDate>Tue, 20 Feb 2024 09:51:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="cicd-툴-변경">CI/CD 툴 변경</h1>
<ul>
<li><p>기존 인프라 구축 당시, CI 담당 툴로 Travis CI를 선택하였다.</p>
</li>
<li><p>허나 한달이 지난 시점에 Trial 기간이 끝나면서 더 이상 사용하지 못 하게 되었다...</p>
<ul>
<li>(1만 크레딧을 다 소모하지 않아도 1달의 기간이 지나면 자동으로 정지됨)</li>
</ul>
</li>
<li><p>위의 이유로 인해, 무료 CI 툴인 Github Actions으로 Migration 하기로 결정하였다.</p>
</li>
</ul>
<h2 id="01-github-actions이란">01. Github Actions이란?</h2>
<ul>
<li>Github에서 제공하는 CI(Continuous Integration)와 CD(Continuous Deployment)를 위한 서비스.</li>
<li>Github를 사용하던 일반 개발자들 입장에서 다른 CI/CD 서비스에 비하여 좋은 접근성과 직관적이며 간단한 설정으로 인하여 큰 호응을 얻고 있다.</li>
<li><strong><em><a href="https://docs.github.com/en/actions">Github Actions Document Link</a></em></strong></li>
</ul>
<h2 id="02-핵심-개념">02. 핵심 개념</h2>
<h3 id="workflows-워크플로우">Workflows (워크플로우)</h3>
<ul>
<li><p>가장 최상위 개념으로 자동화시키고자 하는 작업 과정을 명시하는 파일.</p>
</li>
<li><p>YAML파일로 설정하며, 프로젝트 내부 <strong>.github/workflows</strong> 디렉토리 아래에 위치시킴.</p>
</li>
<li><p>on 속성과 jobs 속성, 이 두가지 속성으로 워크플로우의 전체 과정을 정의함.</p>
</li>
<li><p><strong><em><a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions">워크플로우 문법</a></em></strong></p>
</li>
<li><p>기본적인 YAML 파일 예시 (java - using gradle)</p>
</li>
<li><pre><code class="language-yaml">name: [ 워크플로우 이름 ]
on:
  push:
    branches: [ 브랜치 이름 ]
  pull_request:
    branches: [ 브랜치 이름 ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: &#39;11&#39;
        distribution: &#39;temurin&#39;
    - name: Build with Gradle
      uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
      with:
        arguments: build</code></pre>
</li>
<li><p>name: 전체 워크플로우 이름</p>
</li>
</ul>
<h4 id="on">On</h4>
<ul>
<li>워크플로우의 실행 시점을 설정하는 속성</li>
<li>event: 어떤 event가 발생할 때 실행될 것인가를 정하는 속성 (위의 예시에서 push, PR에 해당) <ul>
<li><strong><em><a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows">공식 문서 링크</a></em></strong></li>
</ul>
</li>
<li>event.branches: 이벤트가 발생할 브랜치 이름</li>
</ul>
<h4 id="jobs">Jobs</h4>
<ul>
<li>독립된 가상 머신 또는 컨테이너에서 돌아가는 하나의 처리 단위.</li>
<li>최소 하나의 작업이 있어야 함.<ul>
<li><strong><em><a href="https://www.daleseo.com/github-actions-basics/">참고 블로그</a></em></strong></li>
</ul>
</li>
<li>name: 추가하고자 하는 작업의 이름을 명시 (위의 예시에서 build에 해당)<ul>
<li>build라는 옵션이 아니라 name의 값을 build로 정한 것 =&gt; 헷갈릴 수 있음</li>
</ul>
</li>
<li>name.runs-on: 워크플로우가 실행될 환경을 설정하는 옵션<ul>
<li><strong><em><a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on">공식 문서 링크</a></em></strong></li>
</ul>
</li>
</ul>
<h4 id="jobsnamesteps">jobs.name.steps</h4>
<ul>
<li>해당 작업의 명령을 단계 별로 기술하는 옵션</li>
<li>name: stpe의 이름을 부여하는 옵션 (생략 가능)</li>
<li>uses: Github Actions에서 제공하는 action이라는 명령어를 사용할 때 쓰는 옵션</li>
<li>run: 커맨드나 스크립트를 실행할 때 사용하는 옵션 (예시에 없음)</li>
</ul>
<h3 id="actions">Actions</h3>
<ul>
<li>CI/CD 과정은 아무래도 반복되는 단계를 많이 거치게 될 수 밖에 없음.</li>
<li>이를 재사용하기 위해 제공되는 일종의 작업 공유 메커니즘.</li>
<li><strong><em><a href="https://github.com/marketplace?type=actions">Actions들을 모아둔 Marketplace 링크</a></em></strong></li>
</ul>
<h3 id="03-migration-이후-느낀점">03. Migration 이후 느낀점</h3>
<ul>
<li>처음 Migration을 하기로 마음 먹었을 때는 지레 겁을 많이 먹었다. 하지만 공식 문서를 찬찬히 살펴보고 몇번의 시행착오를 거쳐보니 느낀점은 <strong>오히려 Travis CI보다 직관적이고 편하다</strong>였다!</li>
<li>기존의 .travis.yml 파일을 대신할 YAML 파일을 .github/workflows 내부에 생성하고 주어진 메뉴얼들을 참고하여 적용해보니 생각보다 수월하게 Migration에 성공하였다.</li>
<li>두 툴을 모두 찍먹(?) 느낌으로 맛만 본 입장에서 느낀 점을 간략하게 정리해보자면 아래와 같다.</li>
</ul>
<h4 id="1-가장-큰-장점-일단-무료다">1. 가장 큰 장점! 일단 무료다!!!</h4>
<ul>
<li>1인 개발자이거나 나처럼 프로젝트를 준비하는 취준생의 입장에서 비용 절감은 매우 중요한 부분이다.</li>
<li>물론 모든 부분에서 무료인 것은 아니지만, 현재 내가 원하는 수준에서 이 정도로 간편하게 사용할 수 있으면서 가격적 부담감을 느끼지 않게 해주는 것만으로도 너무 완벽한 선택인 것 같다.</li>
<li><a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions">Github Actions 공식 가격 정책 링크</a></li>
</ul>
<h4 id="2-일단-써봐야-안다">2. 일단 써봐야 안다.</h4>
<ul>
<li>위에 서술한 것처럼 Migration을 직접 진행하기 전까진 두려웠고 되게 어렵게 느껴졌던 것이 사실이다.</li>
<li>하지만 공식 문서와 여러 좋은 블로그 게시글들을 참고하여 직접 작성해보니 생각보다 쉽게 성공하였다.</li>
<li>물론 현재로써 내 지식 수준은 겉 핥기 레벨이겠지만, 성공을 했다는 그 경험이 있기에 더 깊고 넓은 공부를 할 수 있다는 자신감을 얻었다.</li>
</ul>
<h4 id="3--steps-옵션이-정말-직관적이다">3.  Steps 옵션이 정말 직관적이다.</h4>
<ul>
<li>jobs의 steps 옵션이 정말 너무 직관적이다.</li>
<li>처음 예시들을 봤을 땐 이해 자체를 하지 못했지만, 알고 보니 steps 옵션이 너무 간결하고 이뻐보였다(?).</li>
<li>Travis CI를 사용할 땐, YAML 파일에서 주어지는 옵션들을 하나 하나 파악해가면서 그 값을 일일히 세팅하는 불편함이 있었다면, Github Actions에선 steps 내부에 내가 원하는 로직을 순서에 맞게 기입하고 run 옵션을 통해 원하는 커맨드를 실행시켜주면 끝이다. (물론 uses를 통한 action 호출은 별도의 공부가 필요함.)</li>
<li>이 장점은 곧, 개발자들이 한 눈에 워크플로우 실행 과정을 파악할 수 있다는 큰 장점으로 작용할 것이다.<br>
</li>
</ul>
<hr>
<h3 id="참고-블로그">참고 블로그</h3>
<ul>
<li><strong><em><a href="https://goodgid.github.io/Github-Action-CI-CD-AWS-EC2/">Github Actions 구축 과정 참고</a></em></strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS를 활용한 무중단 배포]]></title>
            <link>https://velog.io/@yangwon-park/AWS-%EA%B5%AC%EC%B6%95%EC%9D%98-%EC%96%B4%EB%A0%A4%EC%9B%80</link>
            <guid>https://velog.io/@yangwon-park/AWS-%EA%B5%AC%EC%B6%95%EC%9D%98-%EC%96%B4%EB%A0%A4%EC%9B%80</guid>
            <pubDate>Tue, 20 Feb 2024 09:44:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.</p>
</blockquote>
<h1 id="aws와의-연동">AWS와의 연동</h1>
<ul>
<li>사이드로 진행한 프로젝트 서버 개발 환경을 로컬에서 AWS로 이전하는 과정에서의 Trouble Shooting 과정에 관한 기록</li>
<li>AWS EC2, RDS, S3, CodeDeploy, nginx 활용</li>
</ul>
<h2 id="01-처음-부딪힌-난관--applicationyml의-profile-설정">01. 처음 부딪힌 난관 : application.yml의 profile 설정</h2>
<ul>
<li><p>이 때까지 개발 과정은 별다른 구분없이 로컬에서만 개발을 진행하였기에 문제가 없었으나, AWS에 배포 서버를 옮기게 되면서 yml 파일의 profile 설정에 대한 무지함이 발목을 잡았다.</p>
</li>
<li><p>spring의 profile 기능을 활용하여 로컬 개발 환경과 AWS의 배포 환경을 나누어 관리할 수 있다.</p>
</li>
<li><p>이 과정에서 SpringBoot 2.4.0 이전의 방법과 이후의 방법이 달라졌음을 알게 됐고, 구글에서 찾은 게시글이 이를 명확히 구분하여 사용하지 않고 있는 것을 깨달아 혼자 직접 헤딩하면서 profile 관리법을 익혔다.</p>
</li>
<li><p>SpringBoot 2.4.0 이전</p>
<ul>
<li><pre><code class="language-yaml">spring:
  profiles: local
  include:
      real_db, local_db</code></pre>
</li>
<li><p>spring.profiles.[name] 형식처럼 사용할 프로필을 명시하고, include 키워드를 이용하여 명시한 프로필에 포함시킬 다른 프로필을 선언해줌</p>
</li>
<li><p>처음 이 방식을 사용했으나, deprecated 됐다는 경고를 보고 아래와 같이 변경</p>
</li>
</ul>
</li>
<li><p>SpringBoot 2.4.0 이후</p>
<ul>
<li><pre><code class="language-java">spring:
  profiles:
    active: local
    group:
      &quot;local&quot;: &quot;local_db, default, oauth&quot;
      &quot;real&quot;: &quot;real_db, default, oauth&quot;

--- # (yml 구분선)

spring:
  config:
    activate:
      on-profile: default

      # 밑으로 yml 설정들 기재
---

spring:
  config:
    activate:
      on-profile: local_db

---

spring:
  config:
    activate:
      on-profile: real_db       </code></pre>
</li>
<li><p>spring.config.activate.on-profile.[name] 의 형식으로 프로필을 선언한다.</p>
</li>
<li><p>spring.profiles.group 기능을 활용하여 그룹을 부여, 그룹명을 이용하여 해당 그룹에 속한 모든 요소들을 한 번에 프로필로 사용할 수 있다.</p>
</li>
<li><p>spring.profiles.active.[name]으로 별다른 호출이 없는 경우 초기에 불러 사용할 프로필 값을 정해준다.</p>
</li>
<li><p>EC2의 스크립트 단에서 아래의 스크립트를 통해 profile 명을 호출하면 위에서 on-profile에 설정한 yml profile 설정들을 맞춰 사용할 수 있다.</p>
<ul>
<li>nohup -jar -<em>Dspring</em>.<em>profiles</em>.<em>active</em>=[프로필 명]</li>
</ul>
</li>
</ul>
</li>
<li><p>2.4.0 이후 프로필 버젼 관리 방법을 터득한 후, EC2에서 서버를 구동시키면 아래에서 보는 것과 같이 원하는 profile 설정들이 등록된 것을 확인할 수 있었다.</p>
<ul>
<li>The following 4 profiles are active: &quot;real&quot;, &quot;real_db&quot;, &quot;default&quot;, &quot;oauth&quot;</li>
</ul>
</li>
</ul>
<h2 id="02-배포-성공은-하지만-테스트-코드는-계속-실패">02. 배포 성공은 하지만, 테스트 코드는 계속 실패</h2>
<ul>
<li><p>배포까진 힘겹게 성공했지만, 이상하게 테스트 코드는 계속 실패로 뜬다 (로컬에선 성공)</p>
</li>
<li><p>gradle build를 --debug 옵션을 부여하여 debug 로그를 모두 보게 설정</p>
</li>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/194839855-6b7c8346-01fc-42d3-a515-57cfde032f36.PNG" alt="gradleError" width="100%"></p>
</li>
<li><p>위처럼 무수히 많은 로그가 찍히는 중... (구글링을 해봐도 이유를 파악하지 못 했음)</p>
</li>
<li><p>Test 코드에 Profile을 별도로 설정하여도 똑같이 에러 발생</p>
</li>
<li><p>임시 방편으로 gradle에서 아래 코드를 주석처리 함</p>
<ul>
<li><pre><code>//tasks.named(&#39;test&#39;) {
//    useJUnitPlatform()
//}</code></pre></li>
</ul>
</li>
<li><p>이유를 아시는 분들은 댓글 달아주시면 너무 감사하겠습니다 ㅠㅠ...</p>
</li>
</ul>
<h2 id="03-travis-ci-자동화-배포-설정-중-permission-denied">03. Travis CI (자동화 배포) 설정 중, Permission denied</h2>
<ul>
<li><p>Travis CI에서 gradlew의 실행 권한이 없어서 발생하는 에러</p>
</li>
<li><p>EC2에 실행 권한을 부여했으나, Travis CI 쪽에서의 접근은 별개로 취급하는 것 같다.</p>
</li>
<li><p>아래의 코드들로 해결할 수 있음</p>
</li>
<li><pre><code>git update-index --chmod=+x gradlew
git commit -m &quot;Travis CI Permission 등록&quot;

&lt;!-- 아래의 명령어로 권한 확인 가능  --&gt;
git ls-tree HEAD

100755 가 나오면 허가가 부여된 것</code></pre></li>
<li><p>2번에서 Test를 끄지 않으면 위 과정에서도 Test 실패로 인한 에러가 발생함</p>
</li>
</ul>
<h2 id="04-travis-ci-자동화-중-pr-수행-시-error발생">04. Travis CI 자동화 중, PR 수행 시 Error발생</h2>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/194873108-ad51f0b6-c529-4e82-9369-25bbec8dca49.PNG" alt="PRErrorr" width="100%"></p>
</li>
<li><p>Travis CI가 작동하는 중, Github에서 PR을 완료해버리면 위의 에러가 발생한다.</p>
</li>
<li><p>사실, 작동을 모두 기다려도 어차피 S3과 CodeDeploy는 PR로 인식하여 작동하지 않는다.</p>
</li>
<li><p>따라서 그냥 무시하고 진행해도 상관없을 듯 하다.</p>
</li>
</ul>
<h2 id="05-1-codedeploy-s3-travis-ci-연동-과정에서-에러-발생">05-1. CodeDeploy, S3, Travis CI 연동 과정에서 에러 발생</h2>
<ul>
<li><pre><code>Version 2 of the Ruby SDK will enter maintenance mode as of November 20, 2020. To continue receiving service updates and new features, please upgrade to Version 3. More information can be found here: https://aws.amazon.com/blogs/developer/deprecation-schedule-for-aws-sdk-for-ruby-v2/
Triggered deployment &quot;d-P0UMRF7JJ&quot;.</code></pre></li>
<li><p>위의 코드가 나오면서 deployment에 실패. (사실 위의 코드와는 별개로 발생한 문제)</p>
</li>
<li><p>해결법은 아래에서!!</p>
</li>
</ul>
<h2 id="05-2-codedeploy-작동-안-함">05-2. CodeDeploy 작동 안 함</h2>
<ul>
<li>Travis CI, S3, CodeDeploy를 연동하는 과정에서 에러 발생.</li>
<li>정확하겐 S3는 정상 동작하지만, CodeDeploy로 인한 자동화 배포가 하나도 이루어지지 않고 있었다.</li>
<li><a href="https://github.com/jojoldu/freelec-springboot2-webservice/issues/474">참고링크</a></li>
<li>위의 링크를 통하여 해결 방법을 찾아 해결하였다ㅠㅠ<ul>
<li>CodeDeploy와 EC2의 연결은 EC2에서 선언한 태그를 사용한다.</li>
<li>EC2, RDS, S3 모두 태그를 부여만 했지 부여한 태그를 사용한 적이 없어 이번 경우에도 아무런 생각도 없이 새로운 태그를 부여하듯이 작성하여 에러 발생!!!</li>
<li>즉, Name이라는 태그를 부여한 EC2를 불러와서 CodeDeploy가 동작해야 하는데, 존재하지도 않는 EC2를 찾아서 동작시키려고 했으므로 Deployment에 실패한 것이다.</li>
</ul>
</li>
</ul>
<h2 id="06-nginx-설치">06. nginx 설치</h2>
<ul>
<li>sudo yum install nginx를 통해 nginx 패키지 검색을 할 수 없음</li>
<li>sudo amazon-linux-extras install nginx1 을 사용해야 함</li>
<li>sudo service nginx start =&gt; sudo service nginx status 로 확인까지 해주면 됨</li>
</ul>
<h2 id="07-배포-후-사라진-node_modules">07. 배포 후 사라진 node_modules</h2>
<ul>
<li>현재 node_modules 디렉토리는 .gitignore에 등록돼있어 git이 추적하지 않는다.</li>
<li>생각해보면 이때까지 수동으로 항상 npm install을 해주고 있었다...</li>
<li>이제 배포 과정이 무중단 + 자동이므로 이 부분 또한 내가 맞게 바꿔줘야 한다!!!</li>
<li><a href="https://jundragon.tistory.com/8">참고 블로그</a></li>
<li>역시 많은 분들이 같은 문제를 겪으셨고, 그렇기에 벌써 Gradle이 Build되는 시점에 npm install을 수행할 수 있는 plugin이 있었다.<ul>
<li>package.json은 함께 관리되고 있음 =&gt; npm install 시, package.json에 있는 패키지들을 설치해줌</li>
</ul>
</li>
<li>gradle에 알맞는 코드를 적용 후, 해결 완료!!!</li>
</ul>
<h2 id="08-s3-이미지-url-access_denied">08. S3 이미지 URL Access_Denied</h2>
<ul>
<li><p>S3의 퍼블릭 접근을 모두 다 차단하여서 발생한 문제.</p>
</li>
<li><p>S3에 이미지를 업로드 하는 과정에서</p>
</li>
<li><pre><code class="language-java">PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, uploadFile)
                .withCannedAcl(CannedAccessControlList.PublicRead);</code></pre>
</li>
<li><p>위의 코드로 업로드 하는 각각의 이미지에 PublicRead 권한을 부여해준다.</p>
</li>
<li><p>이 경우에도 Access Denied 403 에러가 발생하였다.</p>
</li>
<li><p>결국 아래의 사진처럼 버킷에 대한 권한 제한을 어느 정도 풀고 나서야 문제를 해결하였다.</p>
</li>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/195078474-0d6ade26-b6e8-4dec-bfc8-a74d069c7e61.PNG" alt="ACLErrorr" width="100%"></p>
</li>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/195078283-dce92c7a-b291-45a1-9248-143a801998d2.PNG" alt="ACLErrorr" width="100%"></p>
</li>
<li><p>퍼블릭 액세스 권한을 해제하지 않고 접근하는 방법을 아직까지 찾지 못하여 넘어갔으나, Best Practice를 알게 되면 그 방법으로 접근 권한을 수정해야겠다ㅠㅠ</p>
</li>
</ul>
<h2 id="09-ec2-환경에서-파일-업로드-시-권한-문제로-에러-발생">09. EC2 환경에서 파일 업로드 시 권한 문제로 에러 발생</h2>
<ul>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/195114185-0108eb51-781e-4d68-8b64-8b3c89cc3288.PNG" alt="IOException" width="100%"></p>
</li>
<li><p>이미지 업로드 시, createFile로 새로운 File 객체를 생성한다. 하지만 이 과정에서 EC2 파일 생성 권한이 없어서 에러가 발생...</p>
</li>
<li><p>심지어 S3 쪽 권한 문제인줄 알고 한참을 헛돌았다...ㅠㅠ</p>
</li>
<li><pre><code class="language-java">private Optional&lt;File&gt; convert(MultipartFile file) throws IOException {
        File convertFile = new File(&quot;/tmp/&quot;+file.getOriginalFilename());

        if(convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();
    }</code></pre>
</li>
<li><p>파일을 생성하는 시점에 &quot;/tmp/&quot; 경로를 부여해주니 Permission이 통과됐다.</p>
</li>
<li><p>File convertFile = new File(file.getOriginalFilename()); 이렇게 이름만 부여하면 Tomcat 내부에 파일을 임시 생성하고, 그 과정에 파일을 생성하는 권한이 EC2 계정에 부여돼있지 않아 일어난 문제인 것 같다.</p>
</li>
<li><p>tmp 디렉토리는 권한이 모두 열려있으므로 파일 생성에 권한이 따로 필요없기에 경로로 선택했다.</p>
</li>
</ul>
<h2 id="10-access-key-노출로-인한-iam-권한-제한">10. Access Key 노출로 인한 IAM 권한 제한</h2>
<ul>
<li><p>Github에 application.yml이 올라가는 바람에 EC2 접근 및 IAM Access key 보안에 문제가 생겼다...!</p>
</li>
<li><p>AWS Health Dashboard에 경고가 날라와서 문제가 발생했다는 것을 한 눈에 파악이 가능했다.</p>
</li>
<li><p align="center"><img src="https://user-images.githubusercontent.com/97505799/195246280-fe67cffd-1678-4e96-82f0-fd4b8da242a7.PNG" alt="IAM권한" width="100%"></p>
</li>
<li><p>노출이 되면 IAM 사용자 권한에 위의 AWSCompromisedKeyQuarantineV2라는 권한이 하나 추가된다.</p>
</li>
<li><p>위의 친구가 AWS의 정상적인 사용을 금지한다. (Access key 노출로 인한 보안을 위해서)</p>
</li>
<li><p>권한을 제거해주고, 기존의 Access Key는 폐기 후 재발급 받아서 사용하면 된다하여 그대로 진행했으나...</p>
</li>
<li><p>폐기까지 했는데도 Health에 경고가 떠있다. 또한 EC2는 여전히 정상 접근이 불가능했다.</p>
</li>
<li><p>AWS의 가이드라인을 보고 Access key를 콘솔로도 교체해보고 웹에서도 교체해봤으나 해결 자체가 되지 않아 결국 EC2 자체를 삭제하고 새로운 EC2를 만드는 무식한 방법으로 해결하였다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 핵심 원리 - 다양한 의존 관계 주입]]></title>
            <link>https://velog.io/@yangwon-park/Spring-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%9D%98%EC%A1%B4-%EA%B4%80%EA%B3%84-%EC%A3%BC%EC%9E%85</link>
            <guid>https://velog.io/@yangwon-park/Spring-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%9D%98%EC%A1%B4-%EA%B4%80%EA%B3%84-%EC%A3%BC%EC%9E%85</guid>
            <pubDate>Tue, 20 Feb 2024 09:32:02 GMT</pubDate>
            <description><![CDATA[<h1 id="다양한-의존-관계-주입">다양한 의존 관계 주입</h1>
<h2 id="수정자-주입-setter">수정자 주입 (Setter)</h2>
<ul>
<li>자바빈 프로터티 규약의 setter 메소드에 @Autowired를 사용</li>
<li>테스트 코드에서 의존 관계를 한 눈에 파악하기 힘듬</li>
<li>옵션이 필요한 경우에만 특별히 사용</li>
<li>옵션 (Spring Bean과 관계없이 동작해야 하는 경우에 사용)<ul>
<li>@Autowired(required = false) : 호출 자체가 일어나지 않음</li>
<li>파라미터에 @Nullable : 호출은 일어나나, null로 나옴</li>
<li>Optional 파라미터 : 호출은 일어나나, Optional.empty로 옴</li>
</ul>
</li>
</ul>
<h2 id="필드-주입">필드 주입</h2>
<ul>
<li>필드에 바로 @Autowired 사용</li>
<li>간결하다는 큰 장점이 있으나, 변경이 불가능한 단점이 있어 사용하지 않음</li>
<li>DI 프레임 워크가 없으면 할 수 있는게 없음</li>
</ul>
<h2 id="일반-메소드-주입">일반 메소드 주입</h2>
<ul>
<li>메소드를 하나 만들고, 그 안에서 주입을 받음</li>
<li>사용 X</li>
</ul>
<h2 id="생성자-주입">생성자 주입</h2>
<ul>
<li><strong>Best Practice (얘를 사용하자)</strong> </li>
<li>생성자에 @Autowired를 붙여 의존 관계를 주입</li>
<li>오류가 컴파일 시점에 찾을 수 있음<ul>
<li>필드에 final 키워드를 사용할 수 있음 =&gt; 필드의 값을 누락하지 않음</li>
<li>객체를 생성할 때, 데이터 누락을 방지할 수 있음</li>
</ul>
</li>
<li>대부분의 의존 관계는 불변해야 하는데, 이를 유지할 수 있음</li>
<li>프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살릴 수 있음<br>
</li>
</ul>
<hr>
<blockquote>
<p>인프런 김영한님의 <strong>모든 개발자를 위한 HTTP 웹 기본 지식</strong>을 정리한 내용입니다.
<a href="https://www.inflearn.com/course/http-%EC%9B%B9-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/dashboard" class="btn btn--info">김영한님 인프런 강의</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>