<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>no-int.log</title>
        <link>https://velog.io/</link>
        <description>no-intelli 개발자 입니다. 그래도 intellij는 씁니다.</description>
        <lastBuildDate>Sun, 17 Aug 2025 15:56:33 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>no-int.log</title>
            <url>https://velog.velcdn.com/images/no-int/profile/fa3b8455-15b2-42a8-a843-2dbb467e4afb/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. no-int.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/no-int" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[프리티어 서버에서 배우는 JVM GC: SerialGC vs G1GC]]></title>
            <link>https://velog.io/@no-int/%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-JVM-GC-SerialGC-vs-G1GC</link>
            <guid>https://velog.io/@no-int/%ED%94%84%EB%A6%AC%ED%8B%B0%EC%96%B4-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-JVM-GC-SerialGC-vs-G1GC</guid>
            <pubDate>Sun, 17 Aug 2025 15:56:33 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><a href="https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C">픽블엽</a>의로그/지표를 보던 중 <strong>약 20초 동안 서버가 멈춘 듯한 현상을 발견</strong>했다. 원인을 확인해보니,** GC(Garbage Collection) 과정에서 Stop The World(STW) 가 무려 20초 동안 발생한 것**이었다...</p>
<h1 id="문제-확인">문제 확인</h1>
<p><img src="https://velog.velcdn.com/images/no-int/post/6dffe3fc-ea12-4752-a0b7-a65034eb793f/image.png" alt="">
위 지표를 보면, Old 영역에서 Major GC가 발생했는데 그 과정이 약 20초가 걸린 것을 확인할 수 있다.</p>
<h1 id="원인-분석">원인 분석</h1>
<p>아쉽게도 GC 로그를 남기지 않고 있었기 때문에 정확한 분석은 어려웠다.
하지만 20초라는 긴 STW는 비정상적이기에 원인을 추적해봤다.</p>
<p><img src="https://velog.velcdn.com/images/no-int/post/380bce3d-9165-42f8-96d6-b7f48a317941/image.png" alt="">
분석 결과, <strong>GC 알고리즘이 SerialGC로 동작</strong>하고 있었다.</p>
<blockquote>
<p>Java 21을 쓰고 있는데 왜 기본 GC가 G1GC가 아니라 SerialGC일까?”</p>
</blockquote>
<p><strong>원인은 서버 스펙(t2.micro)</strong> 때문이었다.
<strong>메모리가 적은 환경에서는 JVM이 기본적으로 SerialGC를 선택</strong>하는 경우가 있다고 한다.</p>
<h1 id="문제-해결">문제 해결</h1>
<h3 id="✅-g1gc적용">✅ G1GC적용</h3>
<p>서버 실행 시 GC를 직접 지정하여 G1GC를 강제하도록 설정했다.</p>
<pre><code class="language-bash">JAVA_OPTS=&quot;
  -XX:+UseG1GC
  -Xms384m -Xmx384m
  -XX:MaxGCPauseMillis=200
  -XX:InitiatingHeapOccupancyPercent=30
  -XX:ParallelGCThreads=1
  -XX:ConcGCThreads=1
  -Xlog:gc*,safepoint,age=debug:file=$LOG_DIR/gc.log:time,uptime,level,tags:filecount=5,filesize=20m
&quot;</code></pre>
<p>ec2에서 서버를 띄울 때 GC를 직접 변수로 지정하여 G1GC로 띄우고 GC의 상한선을 두었다.
<img src="https://velog.velcdn.com/images/no-int/post/44d83306-d3e6-498e-98a0-391e6fd37cae/image.png" alt=""></p>
<h3 id="✅-gc-로그-기록">✅ GC 로그 기록</h3>
<p>이전에는 GC 로그를 남기지 않았지만, 이번 기회에 <strong>GC 로그를 파일로 기록</strong>하도록 설정했다.
앞으로 문제가 발생했을 때 훨씬 빠르게 원인을 추적할 수 있을 것이다.</p>
<pre><code class="language-bash">-Xlog:gc*,safepoint,age=debug:file=$LOG_DIR/gc.log:time,uptime,level,tags:filecount=5,filesize=20m</code></pre>
<h3 id="✅-gc-간단-비교">✅ GC 간단 비교</h3>
<ul>
<li><strong>SerialGC</strong>: 단일 스레드로 GC 작업을 처리 → 속도가 느리고, STW가 길어짐</li>
<li><strong>G1GC</strong>: 메모리를 여러 Region으로 분할하고, 필요한 부분만 빠르게 수거 → STW 시간이 획기적으로 줄어듦</li>
</ul>
<h1 id="결론">결론</h1>
<p>이번 장애는 GC 로그가 없어 정확한 원인을 파악하기 어려웠다.
하지만 <strong>SerialGC에서 G1GC로 전환</strong>하고, <strong>GC 로그를 기록</strong>하도록 개선하면서 앞으로는 긴 STW 현상을 줄이고 원인 분석도 용이하게 만들었다.</p>
<blockquote>
<p>👉 교훈: “GC는 강제 설정하는게 좋고, GC 로그는 반드시 남겨라.”</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 응답 18초 → 500ms까지, 스프링 비동기 적용기]]></title>
            <link>https://velog.io/@no-int/HTTP-%EC%9D%91%EB%8B%B5-18%EC%B4%88-500ms%EA%B9%8C%EC%A7%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@no-int/HTTP-%EC%9D%91%EB%8B%B5-18%EC%B4%88-500ms%EA%B9%8C%EC%A7%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Sun, 17 Aug 2025 15:31:48 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><a href="https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C">픽블엽</a> 서비스를 운영하면서 처음으로 <strong>Prometheus + Grafana</strong> 조합을 활용해 로그/지표 환경을 구성했다. 이 과정에서 <strong>HTTP 응답 속도가 최대 18초까지 지연</strong>되는 현상을 발견했는데, <strong>비동기(Async) 처리를 적용하여 이를 500ms 수준으로 단축</strong>했다.</p>
<h1 id="문제-구간">문제 구간</h1>
<p>아래 그래프를 보면, 특정 구간에서 응답이 최대 18초까지 지연되는 스파이크를 확인 할 수 있다.
<img src="https://velog.velcdn.com/images/no-int/post/ae5df708-99c2-4ca3-9bda-8aa51484d036/image.png" alt=""></p>
<p>해당 시간대의 로그를 추적해보니 <strong>엽서 등록 신청 API에서 발생한 문제</strong>였다. 구체적인 처리 흐름은 다음과 같았다.</p>
<ul>
<li>유저가 엽서 등록 신청</li>
<li>DB insert</li>
<li>관리자에게 이메일 발송</li>
</ul>
<p>모든 작업이 동기적으로 처리되면서, 이메일 발송이 끝날 때까지 클라이언트가 응답을 받지 못하는 문제가 있었다.</p>
<h3 id="원인-분석">원인 분석</h3>
<p>사실 <strong>관리자 메일 발송</strong>은 성공/실패 여부와 상관없이 <strong>엽서 등록 자체에는 영향이 없는 부가 기능</strong>이다.
즉, 유저 입장에서는 메일 발송이 완료되기를 기다릴 필요가 전혀 없다.
→ 그런데도 동기 흐름에 묶여 있어 응답 지연을 초래했던 것.</p>
<h1 id="해결-방안">해결 방안</h1>
<p>메일 발송을 비동기(Async) 처리로 분리하자!</p>
<h3 id="asyncconfig-설정">AsyncConfig 설정</h3>
<pre><code class="language-java">@Configuration
@EnableAsync     //&lt;&lt; 필수
public class AsyncConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix(&quot;Async_Tread-&quot;);
        executor.setTaskDecorator(new MdcTaskDecorator());
        executor.initialize();
        return executor;
    }
}</code></pre>
<h3 id="메일-발송-비동기-처리">메일 발송 비동기 처리</h3>
<pre><code class="language-java">@Async
public void sendRegisterMail(RegisterPrePost event) {
    ...
}</code></pre>
<p>설정은 간단하지만, <strong>ExecutorThreadPool을 직접 정의</strong>하면 로깅/모니터링, 스레드 풀 사이즈 조정 등 운영 측면에서 더 유연하게 대응할 수 있으니 꼭 커스텀해서 사용하길 추천한다.</p>
<h1 id="결과">결과</h1>
<p><img src="https://velog.velcdn.com/images/no-int/post/161a9c75-be92-448a-9567-1a077e13a9f5/image.png" alt=""></p>
<ul>
<li>응답 속도: <strong>최대 18s → 최대 500ms</strong></li>
<li>사용자 경험: 즉시 개선</li>
<li>서버 부하: 감소</li>
</ul>
<h1 id="결론">결론</h1>
<p>단순히 비동기를 도입했을 뿐인데 응답속도를 획기적으로 개선할 수 있었다.</p>
<p>이번 경험을 통해 다시금 느낀 점은,
👉 <strong>“중요한 것은 코드를 잘 짜는 것뿐만 아니라, 로직을 어떻게 분리하고 책임을 어디까지 둘 것인지 추상화하는 것”</strong> 이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[📬 Spring Event 도입과 Class의 책임 분리]]></title>
            <link>https://velog.io/@no-int/Spring-Event-%EB%8F%84%EC%9E%85%EA%B3%BC-Class%EC%9D%98-%EC%B1%85%EC%9E%84-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@no-int/Spring-Event-%EB%8F%84%EC%9E%85%EA%B3%BC-Class%EC%9D%98-%EC%B1%85%EC%9E%84-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Sun, 15 Jun 2025 08:15:07 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><a href="https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C">픽블엽</a>을 진행하면서 하나의 기능에 여러 부가 기능이 따라오게 되었다.<br>예: 엽서 등록 → DB 저장 → 관리자에게 메일 발송</p>
<p>이처럼 하나의 액션에 여러 책임이 딸려붙기 시작하면 객체지향 5원칙 중 하나인 <strong>단일 책임 원칙(SRP)</strong>이 쉽게 깨진다.</p>
<p>이 문제를 해결하기 위해 <strong>Spring Event</strong>를 도입했고, 적용 방법과 실전 적용기(픽블엽 예제)를 아래에 정리해봤다.</p>
<h1 id="spring-event란">Spring Event란?</h1>
<p>Spring Event는 <strong>컴포넌트 간의 느슨한 연결(loose coupling)</strong>을 가능하게 해주는 구조다.<br>내부적으로는 <strong>Observer 패턴</strong>으로 구성돼있다.</p>
<blockquote>
<p>누군가 어떤 이벤트를 발행하면,<br>관심 있는 컴포넌트가 리스너로서 그 이벤트를 받아 처리.</p>
</blockquote>
<h3 id="🧠-핵심-개념">🧠 핵심 개념</h3>
<table>
<thead>
<tr>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>Publisher</code></td>
<td>이벤트를 발생시키는 쪽</td>
</tr>
<tr>
<td><code>Event</code></td>
<td>전달되는 데이터 객체</td>
</tr>
<tr>
<td><code>Listener</code></td>
<td>이벤트를 구독하고 처리하는 쪽</td>
</tr>
</tbody></table>
<h1 id="🔨-적용-방법">🔨 적용 방법</h1>
<h3 id="이벤트-객체">이벤트 객체</h3>
<pre><code class="language-java">public class UserRegisteredEvent {
    private final String email;

    public UserRegisteredEvent(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }
}</code></pre>
<h3 id="이벤트-발행">이벤트 발행</h3>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class UserService {

    private final ApplicationEventPublisher publisher;

    public void registerUser(String email) {
        // 사용자 등록 로직 ...

        // 이벤트 발행
        publisher.publishEvent(new UserRegisteredEvent(email));
    }
}</code></pre>
<h3 id="이벤트-리스너">이벤트 리스너</h3>
<pre><code class="language-java">@Component
public class WelcomeEmailListener {

    @EventListener(UserRegisteredEvent.class)
    public void handleUserRegistered(UserRegisteredEvent event) {
        System.out.println(&quot;환영 메일 전송 대상: &quot; + event.getEmail());
        // 메일 전송 로직 ...
    }
}</code></pre>
<h1 id="✍️-픽블엽-실전-적용기">✍️ 픽블엽 실전 적용기</h1>
<p>픽블엽에서는 엽서를 승인할 때 다음 3가지 기능이 한 번에 실행된다.</p>
<ol>
<li>엽서 상태 변경 (pre_posts테이블 status컬럼 수정)</li>
<li>실제 엽서 등록 (posts 테이블)</li>
<li>작성자에게 승인 메일 전송</li>
</ol>
<p>이때 1~2번은 트랜잭션 안에서 안전하게 실행되어야 하고, 3번은 부가기능이므로 <strong>성공/실패가 트랜잭션 작업에 영향이 없어야하는 조건</strong>이 있었다.</p>
<hr>
<h3 id="엽서-승인-class">엽서 승인 Class</h3>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class ConfirmPrePostService {

    private final ApplicationEventPublisher eventPublisher;
    private final GetPrePostService getPrePostService;
    private final ConfirmPrePostValidator confirmPrePostValidator;

    public void confirm(UpdatePrePostStatusDto dto) {
        log.info(&quot;Confirm pre post: {}&quot;, dto);
        PrePost prePost = getPrePostService.getPrePost(dto.prePostId());
        confirmPrePostValidator.validateStatus(prePost.getStatus());
        prePost.updateStatus(dto.status(), dto.updatedBy());
        eventPublisher.publishEvent(ConfirmPrePostStatus.confirmPrePost(prePost, dto.updatedBy()));
    }</code></pre>
<h3 id="엽서-등록-class">엽서 등록 Class</h3>
<pre><code class="language-java">@TransactionalEventListener(classes = ConfirmPrePostStatus.class, phase = TransactionPhase.BEFORE_COMMIT)
    public void registerPost(ConfirmPrePostStatus event) {
        log.info(&quot;EVENT - Register post: {}&quot;, event);
        String geohash = geoHashUtil.encode(event.latitude(), event.longitude());
        MultipartFile img = fileCodecUtil.decodeToMultipartFile(geohash, event.img());
        s3Util.uploadFile(event.type() + &quot;-&quot; + geohash, img);
        Post post = new Post(
                geohash,
                event.name(),
                event.latitude(),
                event.longitude(),
                event.type(),
                event.requester(),
                event.noImg(),
                event.confirmedBy()
        );
        postRepository.save(post);
    }</code></pre>
<p>여기서 @TransactionalEventListener어노테이션과 phase = TransactionPhase.BEFORE_COMMIT 두개를 잘 봐야한다.</p>
<ul>
<li><strong>@TransactionalEventListener</strong> : 이벤트를 트랜젝션에 포함시키는 Listener.</li>
<li><strong>phase = TransactionPhase.BEFORE_COMMIT</strong> : 트랜잭션이 작업이 커밋 되기전에 실행.<ul>
<li>실패 시 전체 작업이 rollback 된다.</li>
</ul>
</li>
</ul>
<h3 id="mail-전송-class">Mail 전송 Class</h3>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
public class ConfirmPrePostMailService {

    private final JavaMailSender javaMailSender;
    private final ConfirmPrePostTemplate confirmPrePostTemplate;

    @Value(&quot;${spring.mail.username}&quot;)
    private String from;

    @TransactionalEventListener(classes = ConfirmPrePostStatus.class, phase = TransactionPhase.AFTER_COMMIT)
    public void confirmPrePost(ConfirmPrePostStatus event) {
        log.info(&quot;EVENT - Sending confirm prePost mail: {}&quot;, event);
        String html = confirmPrePostTemplate.build(event);
        MimeMessage message = javaMailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true, &quot;UTF-8&quot;);
            helper.setTo(event.requester().getEmail());
            helper.setSubject(&quot;엽서 등록 승인&quot;);
            helper.setFrom(from);
            helper.setText(html, true);
            helper.addInline(&quot;img&quot;, confirmPrePostTemplate.bindImg(event.img()));
        } catch (MessagingException e) {
            log.error(&quot;sendConfirmPrePostMail Exception&quot;, e);
            throw new MessagingBuildException(&quot;ConfirmPrePostMail&quot;);
        }
        javaMailSender.send(message);
    }
}</code></pre>
<p>여기서 중요하게 봐야하는 건 phase = TransactionPhase.AFTER_COMMIT이다.</p>
<ul>
<li><strong>phase = TransactionPhase.AFTER_COMMIT</strong> : 트랜잭션 작업을 커밋 후 작업을 진행.<ul>
<li>메일 발송 실패가 DB 작업에 영향을 주지 않는다.</li>
</ul>
</li>
</ul>
<h1 id="결론">결론</h1>
<p>기존에는 하나의 엽서 승인 Class에서 승인 처리 + 등록 + 메일 전송을 모두 처리했다.
이 방식은 클래스가 3가지 책임을 동시에 가지게 되어 유지보수와 추가 기능을 붙이기가 어려웠다.</p>
<p>Spring Event를 도입한 후에는</p>
<ul>
<li>승인 클래스는 오직 상태 변경만 수행</li>
<li>등록/메일 등 후처리는 별도 리스너에서 처리</li>
<li>결합도는 낮고, 기능 확장성은 높아짐</li>
</ul>
<p>✔️ 새로운 후처리를 붙일 때는 기존 클래스 수정 없이
@TransactionalEventListener만 추가하면 끝!</p>
<p>이 처럼 Spring Event를 도입 후 유지보수와 기능 추가 시 이점을 함깨 가져가게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot에서 SMTP로 HTML 메일 보내기 (Feat.Gmail)]]></title>
            <link>https://velog.io/@no-int/Spring-Boot%EC%97%90%EC%84%9C-SMTP%EB%A1%9C-HTML-%EB%A9%94%EC%9D%BC-%EB%B3%B4%EB%82%B4%EA%B8%B0-Feat.Gmail</link>
            <guid>https://velog.io/@no-int/Spring-Boot%EC%97%90%EC%84%9C-SMTP%EB%A1%9C-HTML-%EB%A9%94%EC%9D%BC-%EB%B3%B4%EB%82%B4%EA%B8%B0-Feat.Gmail</guid>
            <pubDate>Sun, 15 Jun 2025 07:38:31 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><a href="https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C">픽블엽</a>을 운영하면서 점점 유저들이 엽서를 등록해주기 시작했다.
엽서 등록 신청이 들어오면 관리자가 확인 후 승인(또는 거절)해주는 흐름인데, 지금까지는 신청한 유저가 결과를 알 수 없다는 단점이 있었다.</p>
<p>이 불편함을 개선하기 위해 <strong>메일 알림 기능을 도입</strong>하게 되었다.</p>
<h1 id="smtp">SMTP</h1>
<p><code>SMTP(Simple Mail Transfer Protocol)</code>는 말 그대로 <strong>간단한 메일 전송 프로토콜</strong>이다.</p>
<blockquote>
<p>이메일을 보낸다고 해서 바로 상대방 메일함에 도착하는 건 아니다.<br>이메일은 반드시 <strong>서버 간의 이동과 중계 절차</strong>를 거친다.<br>이때 메일을 전송하는 역할을 담당하는 것이 바로 <strong>SMTP</strong>다.</p>
</blockquote>
<h3 id="이메일의-흐름">이메일의 흐름</h3>
<p>예를 들어, <code>user1@gmail.com</code>이 <code>user2@naver.com</code>에게 메일을 보낸다고 가정해보자.</p>
<ol>
<li>사용자가 Gmail에서 메일을 작성  </li>
<li>Gmail 서버 → SMTP를 통해 메일 전송  </li>
<li>Naver 메일 서버 → POP3/IMAP으로 수신  </li>
<li>user2가 메일 클라이언트에서 확인</li>
</ol>
<p>👉 메일을 <strong>보내는 역할</strong>은 SMTP,
👉 메일을 <strong>받는 역할</strong>은 POP3/IMAP</p>
<h1 id="실전">실전</h1>
<h2 id="gmail을-기준으로-설명한다">GMAIL을 기준으로 설명한다.</h2>
<h3 id="✅-step-0-사전-준비">✅ Step 0: 사전 준비</h3>
<ul>
<li><p><strong>2단계 인증 활성화</strong><br>Gmail SMTP는 2단계 인증이 활성화된 계정만 사용 가능하다.</p>
</li>
<li><p><strong>앱 비밀번호 생성</strong><br>메일 전송 시 Google 계정의 로그인 비밀번호 대신 <strong>앱 전용 비밀번호</strong>를 사용해야 한다.</p>
</li>
</ul>
<h3 id="✅-step-1-의존성-추가">✅ Step 1: 의존성 추가</h3>
<pre><code class="language-groovy">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-mail&#39;
}</code></pre>
<p>Spring Boot의 JavaMailSender를 자동으로 등록해주는 의존성이다.</p>
<h3 id="✅-step-2-applicationyml-설정">✅ Step 2: application.yml 설정</h3>
<pre><code class="language-yaml">spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: your-email@gmail.com
    password: 앱 비밀번호
    properties:
      mail.smtp.auth: true
      mail.smtp.starttls.enable: true</code></pre>
<p>📌 앱 비밀번호는 .env 외부 설정으로 분리하는 것이 좋다. <strong>노출 절대 금지!</strong></p>
<h3 id="✅-step-3-간단한-메일-전송-클래스-만들기">✅ Step 3: 간단한 메일 전송 클래스 만들기</h3>
<pre><code class="language-java">@RequiredArgsConstructor
public class MailService {

    private final JavaMailSender mailSender;

    public void sendSimpleEmail(String to, String subject, String text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);                                // 받는사람
        message.setSubject(subject);                    // 메일 제목
        message.setText(text);                            // 메일 본문
        message.setFrom(&quot;your-email@gmail.com&quot;);        // 보내는 사람

        mailSender.send(message);
    }
}</code></pre>
<h3 id="✅-step-4-html-템플릿으로-메일-보내기예시는-thymeleaf사용">✅ Step 4: HTML 템플릿으로 메일 보내기(예시는 Thymeleaf사용)</h3>
<p>템플릿 파일 (resources/templates/email.html)</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;이메일 제목&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h2 th:text=&quot;${title}&quot;&gt;기본 제목&lt;/h2&gt;
    &lt;p th:text=&quot;${message}&quot;&gt;기본 메시지&lt;/p&gt;

    &lt;table border=&quot;1&quot; cellpadding=&quot;5&quot;&gt;
        &lt;tr&gt;&lt;td&gt;이름&lt;/td&gt;&lt;td th:text=&quot;${name}&quot;&gt;홍길동&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;이메일&lt;/td&gt;&lt;td th:text=&quot;${email}&quot;&gt;email@example.com&lt;/td&gt;&lt;/tr&gt;
    &lt;/table&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MailService {

    private final JavaMailSender mailSender;
    private final TemplateEngine templateEngine;

    public void sendTemplatedMail(String to, String subject, String name, String email) throws MessagingException {
        // Thymeleaf context 설정
        Context context = new Context();
        context.setVariable(&quot;title&quot;, &quot;이메일 제목&quot;);
        context.setVariable(&quot;message&quot;, &quot;이메일 메시지&quot;);
        context.setVariable(&quot;name&quot;, name);
        context.setVariable(&quot;email&quot;, email);

        // 템플릿 렌더링
        String html = templateEngine.process(&quot;email&quot;, context); // email.html 사용

        // 메일 전송 준비
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, &quot;UTF-8&quot;);

        helper.setTo(to);
        helper.setSubject(subject);
        helper.setFrom(&quot;your-email@gmail.com&quot;);
        helper.setText(html, true); // HTML 메일로 전송

        mailSender.send(message);
    }
}</code></pre>
<p>Thymeleaf 템플릿엔진을 사용할 때 Controller에서 Model에 실어 보낸 정보를 <code>context</code>에 담아서 정보를 전달한다.
templateEngine.process(...)는 .html 확장자를 자동으로 붙이며, 기본 경로는 resources/templates/이다.
다른 경로라면 mail/이메일.html처럼 경로 포함해 작성 가능하다.</p>
<h1 id="결론">결론</h1>
<p>이렇게 하면 Gmail SMTP를 통해 하루 500건 한도 내에서 무료로 메일 전송이 가능하며 현재 픽블엽의 규모에서는 충분히 커버 가능한 수준이라 이 기능만으로도 잘 돌아가고 있다.
SMTP는 빠르게 메일 전송 기능을 붙이기에 좋은 선택이다. 때문에 픽블엽에서는 알림용도로 적극 활용할 생각이다.</p>
<h3 id="번외-유료-메일-서비스">번외. 유료 메일 서비스</h3>
<p>향후 서비스가 성장하거나, 메일 트래픽이 많아지면 다음과 같은 유료 메일 전송 서비스도 고려할 수 있다.</p>
<table>
<thead>
<tr>
<th>서비스</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SendGrid</strong></td>
<td>대량 메일 발송, 마케팅 이메일, A/B 테스트 지원</td>
</tr>
<tr>
<td><strong>Amazon SES</strong></td>
<td>저렴한 가격, AWS 인프라와의 강력한 통합</td>
</tr>
<tr>
<td><strong>Mailgun</strong></td>
<td>트랜잭션 메일 특화, 분석 기능 제공</td>
</tr>
</tbody></table>
<p>유저에게 이메일 전송을 할 땐 HTML 템플릿을 쓰면 메일의 품질과 일관성이 좋아지니 반드시 적용하는게 좋다고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Apple Touch Icon 대응기]]></title>
            <link>https://velog.io/@no-int/Apple-Touch-Icon-%EB%8C%80%EC%9D%91%EA%B8%B0</link>
            <guid>https://velog.io/@no-int/Apple-Touch-Icon-%EB%8C%80%EC%9D%91%EA%B8%B0</guid>
            <pubDate>Thu, 05 Jun 2025 05:22:45 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><img src="https://velog.velcdn.com/images/no-int/post/7e3457dc-997e-4644-8d59-0fb17a1b3f20/image.jpg" alt="">
<a href="https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C">픽블엽</a>을 운영하던 중 이미지와 같은 에러가 났다는 제보를 받았다.</p>
<p>에러 메시지를 보니 <code>apple-touch-icon.png</code>라는 정적 리소스를 찾지 못했다는 의미였다.
사실 픽블엽을 만들 때 사실 모바일 최적화는 아직 신경 쓰지 않았던 터라... 이 제보를 받고 잠깐 멘붕이 왔다😅</p>
<h1 id="apple-touch-icon">Apple Touch Icon</h1>
<p><strong>Apple Touch Icon</strong>은 iOS 기기에서 웹사이트를 <strong>홈 화면에 추가할 때 표시되는 아이콘</strong>이다.</p>
<p>iOS기기(특히 Safari)는 자동으로 사이트 루트(<code>/</code>)에서 <code>/apple-touch-icon.png</code>를 요청하며 이 파일이 존재하지 않으면, 자동으로 404 요청이 발생하게 된다.</p>
<p>즉, 유저 입장에선 직접 보이진 않지만  <strong>iOS 기기가 항상 찾으려는 기본 이미지</strong>다.</p>
<h3 id="권장-크기">권장 크기</h3>
<table>
<thead>
<tr>
<th>사이즈(px)</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>180x180</strong></td>
<td>최신 iPhone (Retina)</td>
</tr>
<tr>
<td><strong>167x167</strong></td>
<td>iPad Pro</td>
</tr>
<tr>
<td><strong>152x152</strong></td>
<td>iPad Retina</td>
</tr>
<tr>
<td><strong>120x120</strong></td>
<td>iPhone Retina (iPhone 4 이후)</td>
</tr>
<tr>
<td><strong>76x76</strong></td>
<td>구형 iPad</td>
</tr>
<tr>
<td><strong>72x72</strong></td>
<td>구형 iPad</td>
</tr>
<tr>
<td><strong>60x60</strong></td>
<td>구형 iPhone</td>
</tr>
<tr>
<td><strong>57x57</strong></td>
<td>iPhone 3 및 이전 모델</td>
</tr>
</tbody></table>
<h1 id="대응">대응</h1>
<h3 id="간단-대응">간단 대응</h3>
<p>가장 기본적인 방식은 <code>static</code> 폴더 또는 루트에 <code>apple-touch-icon.png</code> 파일을 두고, HTML <code>&lt;head&gt;</code>에 다음 한 줄을 추가하는 것이다.</p>
<pre><code class="language-html">&lt;link rel=&quot;apple-touch-icon&quot; href=&quot;/apple-touch-icon.png&quot;&gt;</code></pre>
<p>해당 한 줄로 Safari나 iOS에서 발생하는 404 요청을 막을 수 있다.</p>
<h3 id="확장-대응">확장 대응</h3>
<p>좀 더 세밀하게 대응하고 싶다면, 각 디바이스에 맞는 아이콘을 사이즈별로 준비하고 아래와 같이 명시해줄 수 있다:</p>
<pre><code class="language-html">&lt;head&gt;
    &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;180x180&quot; href=&quot;/apple-touch-icon-180x180.png&quot;&gt;
    &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;167x167&quot; href=&quot;/apple-touch-icon-167x167.png&quot;&gt;
    &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;152x152&quot; href=&quot;/apple-touch-icon-152x152.png&quot;&gt;
    &lt;link rel=&quot;apple-touch-icon&quot; sizes=&quot;120x120&quot; href=&quot;/apple-touch-icon-120x120.png&quot;&gt;
&lt;/head&gt;</code></pre>
<p>이렇게 하면 사용자의 디바이스에 맞는 해상도의 아이콘이 자동으로 적용된다.
웹앱으로 홈 화면에 추가했을 때 훨씬 자연스럽고 깔끔한 UX를 제공할 수 있다.</p>
<h1 id="결론">결론</h1>
<p>이번 경험을 통해 모바일 환경의 기본적인 대응도 중요하다는 걸 체감했다.</p>
<p>사실 처음 개인 프로젝트를 운영해보는 입장이라 이 부분은 신경 쓰지 못했는데, 왜 개발자들이 &quot;자기만의 서비스를 꼭 한번 운영해보라&quot;는 얘기를 하는지 몸소 느꼈다.</p>
<p>다행이 픽블엽은 꽤 많은 유저들이 이용해주는 서비스다보니 이런 제보도 받을 수 있게 됐다고 생각한다.
이런 고마운 유저들에게 좀 더 UX적으로 쾌적한 서비스를 제공하는 노력이 필요할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[☠️ 내 서버를 노리는 수상한 요청들: /index.php 로그 분석과 방어]]></title>
            <link>https://velog.io/@no-int/%EB%82%B4-%EC%84%9C%EB%B2%84%EB%A5%BC-%EB%85%B8%EB%A6%AC%EB%8A%94-%EC%88%98%EC%83%81%ED%95%9C-%EC%9A%94%EC%B2%AD%EB%93%A4-index.php-%EB%A1%9C%EA%B7%B8-%EB%B6%84%EC%84%9D%EA%B3%BC-%EB%B0%A9%EC%96%B4</link>
            <guid>https://velog.io/@no-int/%EB%82%B4-%EC%84%9C%EB%B2%84%EB%A5%BC-%EB%85%B8%EB%A6%AC%EB%8A%94-%EC%88%98%EC%83%81%ED%95%9C-%EC%9A%94%EC%B2%AD%EB%93%A4-index.php-%EB%A1%9C%EA%B7%B8-%EB%B6%84%EC%84%9D%EA%B3%BC-%EB%B0%A9%EC%96%B4</guid>
            <pubDate>Thu, 29 May 2025 11:03:01 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><img src="https://velog.velcdn.com/images/no-int/post/ab30c6d6-84bd-4b49-9d43-fc592784aa30/image.png" alt="">
<a href="https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C">픽블엽</a> 프로젝트의 서버 로그를 확인하던 중, 익숙하지 않은 경로 요청을 발견했다.
하루에도 몇 번씩 <code>/index.php</code> 요청이 들어오고 있었고, 이건 우리 프로젝트에서는 단 한 번도 사용한 적이 없는 경로였다.</p>
<p>픽블엽은 <strong>Spring Boot + Thymeleaf</strong> 기반 웹 프로젝트이기 때문에 <code>.php</code> 파일은 존재하지 않는다.
처음엔 단순한 오타인가 싶었지만, 같은 패턴의 요청이 계속 반복되고 있었고… 찾아보니 이것은 명백한 <strong>봇 기반 공격 시도</strong>였다.</p>
<h1 id="🤔정체를-파해쳐보자">🤔정체를 파해쳐보자</h1>
<pre><code>/index.php?s=/index/\think\app/invokefunction&amp;function=call_user_func_array&amp;vars[0]=md5&amp;vars[1][]=Hello</code></pre><p>이 요청은 <strong>PHP 기반의 프레임워크(예: ThinkPHP)</strong>에서의 취약점을 노리는 것이다.
Spring Boot에서는 아무 의미 없는 요청이지만, 만약 해당 경로에 취약한 PHP 코드가 존재한다면 공격자는 다음을 시도할 수 있다.</p>
<ul>
<li>서버 명령 실행</li>
<li>파일 업로드</li>
<li>DB초기화 or 탈취</li>
<li>웹 쉘 삽입</li>
</ul>
<h1 id="🔍nginx-로그에서도-확인">🔍Nginx 로그에서도 확인</h1>
<p><img src="https://velog.velcdn.com/images/no-int/post/8dcfecdb-63c6-4b07-8ae9-e5eee5a68c2b/image.png" alt="">
NginX로그를 확인해보니 <code>/index.php</code>, <code>/wp-admin/setup-config.php</code> 등 존재하지 않는 경로에 대한 요청이 주기적으로 발생하고 있었다. <strong>명백한 공격 패턴</strong>이다.</p>
<p>봇이 여러 경로를 자동으로 스캔하며 <strong>‘운 좋게 하나라도 걸려라’</strong> 하는 방식의 <strong>무차별 경로 스캐닝 공격</strong>이다.</p>
<h1 id="🎯공격의-목적">🎯공격의 목적</h1>
<p>이런 요청은 단순한 트래픽이 아니라, 실제로 서버의 취약점을 찾기 위한 공격 시도다. 목적은 다음과 같다.</p>
<ul>
<li>취약한 PHP 코드 실행</li>
<li>서버 권한 획득</li>
<li>민감한 DB정보 탈취</li>
<li>시스템 자원 사용(암호화폐 채굴 등)</li>
</ul>
<h1 id="🔐어떻게-방어할까">🔐어떻게 방어할까?</h1>
<h2 id="aws-waf"><a href="https://docs.aws.amazon.com/ko_kr/waf/latest/developerguide/what-is-aws-waf.html">AWS WAF</a></h2>
<p>AWS WAF는 고객이 정의한 조건에 따라 웹 요청을 허용, 차단 또는 모니터링(계수)하는 규칙을 구성하여 공격으로부터 웹 애플리케이션을 보호하는 웹 애플리케이션 방화벽이다. 해당 서비스를 이용하면 다음과 같은 리소스 유형을 보호 할 수 있다.</p>
<ul>
<li>Amazon CloudFront 배포</li>
<li>Amazon API Gateway REST API</li>
<li>Application Load Balancer</li>
<li>AWS AppSync GraphQL API</li>
<li>Amazon Cognito 사용자 풀</li>
<li>AWS App Runner 서비스</li>
<li>AWS Verified·Access 인스턴스</li>
<li>AWS Amplify</li>
</ul>
<p>단점은 요금이 발생한다는 점 이다.</p>
<h2 id="nginx에서-직접-차단하기-내가-선택한-방법">Nginx에서 직접 차단하기 (내가 선택한 방법)</h2>
<p>WAF는 강력하지만 유료이기 때문에, 프리티어 요금제를 사용하는 입장에서는 부담스럽다고 느꼈다.
그래서 비용이 들지 않는 Nginx 필터링 방식을 적용했다.</p>
<h3 id="적용하기">적용하기</h3>
<p>Ngnix 설정 파일(/etc/nginx/nginx.conf)에 아래의 내용을 추가해주면 끝이다.
<img src="https://velog.velcdn.com/images/no-int/post/b8b15ad4-e120-4514-beb7-4ca45c243aca/image.png" alt=""></p>
<h1 id="결론">결론</h1>
<p>픽블엽의 경우 기본적으로 Spring boot 프로젝트라 사실 아무 대응을 하지 않아도 큰 문제는 없지만 그래도 찜찜한 로그가 계속 남기기 싫어 공격을 차단한 방법을 찾아보고 적용했다.</p>
<p>보안은 &quot;별일 없겠지&quot;보다 &quot;미리 차단하자&quot;가 훨씬 싸게 먹힌다. Spring 프로젝트라고 해서 PHP 공격을 방치하면 안 되고, 로그에서 수상한 흔적을 발견했다면 <strong>바로 대응하는 습관이 중요</strong> 한 것 같다.</p>
<p>픽블엽을 통해 서버를 처음 운용해보면서 이런 간단한 것도 이제야 배우는 것 같다.ㅠ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[t2.micro OOM 예방 – SWAP 설정으로 안전하게!]]></title>
            <link>https://velog.io/@no-int/t2.micro-OOM-%EC%98%88%EB%B0%A9-SWAP-%EC%84%A4%EC%A0%95%EC%9C%BC%EB%A1%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C</link>
            <guid>https://velog.io/@no-int/t2.micro-OOM-%EC%98%88%EB%B0%A9-SWAP-%EC%84%A4%EC%A0%95%EC%9C%BC%EB%A1%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C</guid>
            <pubDate>Tue, 27 May 2025 12:25:35 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><a href="https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C">픽블엽</a>의 로그를 확인하던 중, 서버가 예상치 않게 재시작된 흔적을 발견했다. 원인을 추적한 결과, OOM(Out Of Memory)을 의심하게 되었고, 이를 방지하기 위해 SWAP 설정을 적용하게 됐다.</p>
<h1 id="oom">OOM</h1>
<p><strong>OOM (Out Of Memory)</strong> 은 시스템에 할당할 수 있는 물리 메모리가 부족해질 때 발생하는 현상으로, 커널이 이를 감지하고 메모리를 많이 사용하는 프로세스를 <strong>강제 종료</strong>시킨다. 이 경우 서버가 예기치 않게 중단되거나, 중요한 프로세스가 죽는 일이 발생할 수 있다.</p>
<h1 id="swap">SWAP</h1>
<p><strong>SWAP</strong>은 메모리가 부족할 때, 디스크의 일부 공간을 메모리처럼 사용하는 방법으로 <strong>OOM을 예방</strong>하는 유용한 기법이다.</p>
<h1 id="왜-oom을-방지해야-했을까">왜 OOM을 방지해야 했을까?</h1>
<p>현재 픽블엽 서비스는 t2.micro 인스턴스에 nginx와 애플리케이션 서버를 함께 운영 중이다.
t2.micro는 메모리가 1GB에 불과하지만, 초기 트래픽이 많지 않기 때문에 저렴하게 시스템을 구축할 수 있다고 판단하고 ec2 한대에 모든 서버를 같이 운영 하기로 했다.</p>
<p>하지만 서버의 재시작 흔적을 본 후 다음과 같은 가능성들을 고려하게 됐다.</p>
<ul>
<li>메모리 누수 (Memory Leak)</li>
<li>추후 사용자 증가 및 서버 증설</li>
<li>Redis 같은 추가 서비스 설치</li>
</ul>
<p>즉, <strong>지금은 문제가 없어도 언제든지 메모리 부족 상황</strong>이 올 수 있다는 판단이 들었고, 그 대비책으로 SWAP 선택 했다.</p>
<h1 id="메모리-확인">메모리 확인</h1>
<p><img src="https://velog.velcdn.com/images/no-int/post/2f54c396-1d85-4732-b434-f2ffb614d96b/image.png" alt=""></p>
<pre><code class="language-bash">bash

free -t</code></pre>
<p>free -t 명령어를 통해 시스템의 메모리 상태를 확인 할 수 있다.</p>
<ul>
<li>사용중인 메모리(used)</li>
<li>사용중이지 않는 메모리(free)</li>
<li>실제 사용가능한 여유 메모리(available)</li>
</ul>
<h1 id="swap-적용">SWAP 적용</h1>
<p>SWAP 크기는 <a href="https://repost.aws/ko/knowledge-center/ec2-memory-partition-hard-drive">AWS의 공식문서</a>를 보면 권장 수준이 있다.</p>
<pre><code class="language-bash">bash

sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 그 다음에
echo &#39;/swapfile none swap sw 0 0&#39; | sudo tee -a /etc/fstab</code></pre>
<p>위 명령어들을 차례대로 실행하면 1GB의 스왑 공간이 할당된다.
마지막 명령어는 SWAP의 영구 적용시켜 시스템이 재부팅돼도 자동으로 SWAP이 활성화 시켜준다. 하지만 반복 실행하면 부팅오류가 나는 원인이 되니 <strong>반드시 한번만 실행</strong>해야 한다.</p>
<h1 id="swap-적용-확인">SWAP 적용 확인</h1>
<p><img src="https://velog.velcdn.com/images/no-int/post/c9bcbe5b-4c07-4002-89de-9e4428af7d0b/image.png" alt=""></p>
<pre><code class="language-bash">bash

swapon --show</code></pre>
<p>SWAP이 잘 적용 됐다면 위 이미지 같이 확인 가능하다.</p>
<h1 id="프리티어-사용자에게-swap은-필수">프리티어 사용자에게 SWAP은 필수</h1>
<p>t2.micro를 사용하더라도 실사용자가 많지 않다면 큰 문제는 없지만, <strong>스왑은 거의 필수</strong>라고 생각한다.
특히, 예기치 않은 트래픽 급증이나 백그라운드 서비스가 늘어날 가능성이 있는 경우, 최소한의 SWAP 설정은 시스템을 안정적으로 운영하는 데 큰 도움이 될거라 예상한다.</p>
<h1 id="결론">결론</h1>
<p>t2.micro 같이 저사양 인스턴스는 메모리가 매우 제한적이니 OOM예방을 위한 SWAP설정은 권장을 넘어 필수라고 생각한다.
문제라고 한다면 실제로 디스크를 사용하는 시점에는 디스크 I/O에 부담이 있겠지만 서비스가 죽는 거랑 비교한다면 문제도 아니라고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[피크민 블룸 엽서 지도 프로젝트 - "픽블엽" 소개]]></title>
            <link>https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C</link>
            <guid>https://velog.io/@no-int/%ED%94%BD%EB%B8%94%EC%97%BD.-%ED%94%BC%ED%81%AC%EB%AF%BC-%EB%B8%94%EB%A3%B8%EC%9D%84-%EC%A2%80-%EB%8D%94-%ED%8E%B8%ED%95%98%EA%B2%8C</guid>
            <pubDate>Tue, 27 May 2025 11:20:24 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><a href="https://pickmin-map.com/">픽블엽 바로가기</a>
피크민 블룸이란 게임을 아시나요?
피크민 블룸은 포켓몬고와 같은 AR기반 게임입니다!
이 게임의 컨텐츠 중 하나는 엽서(이미지)를 얻어서 게임 친구들와 교환을 하는 것 인데 예쁜 엽서, 못난엽서(일명 똥엽서)가 있어서 많은 유저들이 예쁜엽서를 얻으려고 노력을 많이 하고 있습니다.</p>
<p>때문에 현실에서 사는 곳, 직장 등의 위치에 예쁜 엽서가 없으면 GPS조작으로 타지에 엽서를 얻어오기도 하는데, 이 때 엽서의 정보가 공식적으론 없어서 일반 유저들이 좌표를 공유하는 수 밖에 없습니다.</p>
<p>그래서 엽서의 이미지와 좌표를 유저들에게 얻어서 지도에 보여주는 픽블엽 프로젝트를 만들어 봤습니다!</p>
<h1 id="프로젝트">프로젝트</h1>
<h3 id="미리보기">미리보기</h3>
<p><img src="https://velog.velcdn.com/images/no-int/post/dd91926b-e50c-4bc6-a387-eeb1db858a6f/image.gif" alt=""></p>
<h3 id="사용-기술-스택">사용 기술 스택</h3>
<ul>
<li><strong>Backend</strong>: Java Spring Boot</li>
<li><strong>Frontend</strong>: Thymeleaf, JS</li>
<li><strong>DB</strong>: MySQL</li>
<li><strong>Infra</strong>: AWS EC2(t2.micro), S3, CloudWatch</li>
<li><strong>CI/CD</strong>: GitHub Actions</li>
<li><strong>지도 시각화</strong>: OpenLayers</li>
</ul>
<h3 id="프로젝트-구조">프로젝트 구조</h3>
<p><img src="https://velog.velcdn.com/images/no-int/post/9ff32699-949b-4f02-bbd1-88253fd51562/image.png" alt=""></p>
<ol>
<li>AWS 프리티어 요금제를 사용중이라 ec2 한대에 NginX와 어플리케이션 서버를 같이 사용했습니다.</li>
<li>이미지는 S3를 통해 저장하고 URL를 이용해서 불러옵니다.</li>
<li>로그는 CloudWatch사용 했습니다.</li>
</ol>
<h3 id="프로젝트-기능">프로젝트 기능</h3>
<ul>
<li>사용자로부터 <strong>엽서 이미지 + 위도/경도 좌표</strong>를 입력받아 저장</li>
<li>지도에는 엽서 아이콘이 표시되며, 3가지 타입을 구분<ul>
<li>🍄 버섯 엽서</li>
<li>🌸 빅플라워 엽서</li>
<li>🧭 탐험 엽서</li>
</ul>
</li>
<li>지도는 OpenLayers 기반으로 구현되었고,
일정 거리 이상에서는 <strong>클러스터링 처리</strong>로 성능과 가시성 확보</li>
</ul>
<h1 id="앞으로의-계획">앞으로의 계획</h1>
<p><strong>엽서 조회가 주된 목적</strong>이라 많은 기능을 상정하고 만든 프로젝트는 아닙니다.
때문에 업데이트 방향성은 엽서 데이터의 정확성을 높이는 방향으로 할 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[첫 프리랜서로 근무하며 느낀점.]]></title>
            <link>https://velog.io/@no-int/%EC%B2%AB-%ED%94%84%EB%A6%AC%EB%9E%9C%EC%84%9C%EB%A1%9C-%EA%B7%BC%EB%AC%B4%ED%95%98%EB%A9%B0-%EB%8A%90%EB%82%80%EC%A0%90</link>
            <guid>https://velog.io/@no-int/%EC%B2%AB-%ED%94%84%EB%A6%AC%EB%9E%9C%EC%84%9C%EB%A1%9C-%EA%B7%BC%EB%AC%B4%ED%95%98%EB%A9%B0-%EB%8A%90%EB%82%80%EC%A0%90</guid>
            <pubDate>Mon, 25 Nov 2024 08:08:21 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>개발자로 근무하면서 첫 3년 9개월 정도의 정직원으로 근무했었는데 이때는 수동적인 개발자였던걸, 프리랜서 개발자로 근무한 5개월 정도에 많이 느끼게 되었다. 이걸 공유해 보려고 한다.</p>
<h1 id="첫-프리랜서로-근무하며-느낀점">첫 프리랜서로 근무하며 느낀점</h1>
<h3 id="난-수동적인-개발자였다">난 수동적인 개발자였다.</h3>
<p>서빙로봇 도메인을 가지고 있는 회사에서 관리자 페이지를 개발하게 되는 기회를 갖게 됐다.
기존 정직원으로 일할 때는 내가 기획자, 프론트 분들과 직접적인 얘기를 하고 회의를 하며 기능을 개발한 적이 없고, 파트장님을 통해 업무지시만 받았고 문제가 있을 때도 리더에게 공유를 하면 파트장님이 타 부서 사람들과 이야기를 하는 방식으로 진행했었다. 이게 핑계가 되진 않겠지만 파트장님이 시키신 일만하게 됐고, 버그 픽스 정도가 아니면 내가 서비스를 개선하고자 더 생각해보진 않았었다.</p>
<h3 id="더-나은-서비스를-위해-끊임-없는-생각과-의견-공유">더 나은 서비스를 위해 끊임 없는 생각과 의견 공유</h3>
<p>프리랜서로 근무하는 곳은 PL이 있고 기획자, 프론트, 백엔드가 한팀이 되어 직접적으로 이야기하고 진행하는 방식의 환경이었다.</p>
<p>이런 상황이다 보니 기능에 대한 의문점, 개선점이 있는 경우 내가 먼저 얘기를 꺼내고 이야기를 통해 좀 더 나은 방향으로 진행했다. 이런 환경을 접하다 보니 PL에게 지시받은 기능구현을 넘어서 어떻게 해야 좀 더 나은 방향이 있을지 생각하게 됐고 의견이 받아지기도 때로는 기각 되기도 하면서 서비스를 어떻게 더 좋아지게 할 지 생각 하게 됐다.</p>
<blockquote>
<p>물론 내 모든 생각이 받아들여 지진 않았고, 바꾸지 못한 것에 대한 아쉬움도 있는 부분이 있다.<a href="https://velog.io/@no-int/Spring-Event%EC%82%AC%EC%9A%A9%EB%B2%95%EC%97%90-%EA%B4%80%ED%95%9C-%EB%82%B4-%EC%83%9D%EA%B0%81">(링크)</a></p>
</blockquote>
<h3 id="내가-나가고-난-그-후는---남들이-보기-좋은-코드는-무엇이지">내가 나가고 난 그 후는? -&gt; 남들이 보기 좋은 코드는 무엇이지?</h3>
<p>프리랜서로 1~2주 근무 하면서 새로운 기능을 계속 개발하는 도중 <strong>내가 나간 후 이 코드는 남으신 분들이 개선작업 또는 버그가 있을 경우 쉽게 건들 수 있는 코드인가?</strong> 라는 고민을 하게 됐다.</p>
<p>이 때부터 단일 책임 원칙을 최대한 지키면서 클래스에 기능을 최소한으로 하고 한 클래스에서 여러가지 작업을 하지 않도록 클래스별 기능을 많이 나눴다. 이제 정답인지는 잘 모르겠지만 코드 분석을 할 때, 해당 클래스가 어떤 역할을 하는지 클래스부터 인지하고 볼 수 있도록 하기 위해서 였다.</p>
<p>또한 메서드에 코드를 나열하기 보단 메서드도 잘게 쪼개서 메서드명으로 어떤 기능을 가지고 있는지 메서드명의 흐름으로 기능을 읽기 편하도록 코드를 짜기 시작했다. 이게 클린코드에서 함수를 최대한 간결하게 였던것 같다.</p>
<h1 id="결론">결론</h1>
<p>3년 9개월의 정직원으로 개발한 시간보다 5개월이지만 잠깐의 프리랜서로 근무하면서 내 코드의 품질을 생각하게 됐고, 더 나은 서비스를 위해 어떤 방향성으로 코드를 작성해야하는지 생각하게 됐었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Event사용법에 관한 내 생각]]></title>
            <link>https://velog.io/@no-int/Spring-Event%EC%82%AC%EC%9A%A9%EB%B2%95%EC%97%90-%EA%B4%80%ED%95%9C-%EB%82%B4-%EC%83%9D%EA%B0%81</link>
            <guid>https://velog.io/@no-int/Spring-Event%EC%82%AC%EC%9A%A9%EB%B2%95%EC%97%90-%EA%B4%80%ED%95%9C-%EB%82%B4-%EC%83%9D%EA%B0%81</guid>
            <pubDate>Fri, 22 Nov 2024 07:54:40 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>프리랜서로 근무하면서 Spring Event를 사용하게 됐고 이 때 내가 생각한 점을 정리 해보려고 한다.</p>
<h1 id="spring-event">Spring Event</h1>
<p>Spring Event는 간단히 말해서 행위에 대한 Event를 발행하고 그 Event를 구독하고 있는 부차적인 로직을 실행하도록 해준다. 이 때 장점은 메인 비지니스 로직에서 부차적인 로직에 대한 의존성을 분리 할 수 있다는 점이다.</p>
<h1 id="사용하면서-생각한-문제점">사용하면서 생각한 문제점</h1>
<p>실제 프리고 근무한 곳에서는 Spring Event를 좀 철학에 안맞게 사용하고 있다고 생각했다.</p>
<p>이유는 하나의 메인로직에 모든 부차적인 로직에 대한 Event 각각 발행했고, 부차적인 로직 또한 또 다른 로직에 Event를 발행에서 하나의 로직에 Event가 체이닝 되어 있는 상황이었다.</p>
<h3 id="위-상황에-대한-문제">위 상황에 대한 문제</h3>
<ol>
<li><p>하나의 메인 로직에 어떤 부차적인 로직이 있는지 파악하기가 힘들다. 각각 부차적인 로직에 대한 Event 및 체이닝으로 걸려있는 Event까지 확인하기가 여간 힘든게 아니었다. 마치 Event가 트리형식으로 물려있었다.</p>
</li>
<li><p>유닛 테스트를 진행하지는 않았지만 메인로직만 하더라도 1~n개의 Event가 발행 중이어서 유닛 테스트를 진행했다면 모든 Event를 테스트 더블을 만들어 줘야지만 한다.</p>
</li>
<li><p>메서드 재사용를 재사용 하기가 무서웠다. 재사용하고 싶은 메서드에 어떤 Event가 발생하고 있고 어떤 Event가 체이닝으로 물려있는지 파악을 매번해야했고, 때문에 같은 로직에 Event만 없는 메서드를 만들기도 햇다.</p>
</li>
</ol>
<h1 id="내가-제시한-해결책">내가 제시한 해결책</h1>
<h3 id="하나의-메인-로직은-1개의-event만-발행하자">하나의 메인 로직은 1개의 Event만 발행하자</h3>
<p>하나의 메인 로직은 1개의 Event만 발행하고 모든 부차적인 로직들이 구독을 해야 어떤 기능들이 물려있는지 파학하기가 쉽다고 생각했다. 또한 유닛 테스트를 진행할 때도 Event하나만 테스트 더블을 만들면 되기 때문이었다.</p>
<p>그리고 가장 중요한건 Spring Event의 철학이라고 생각했다. Event를 구독 중인 로직들은 EventListener를 통해 구독하는데 워딩 그대로 Listener가 Event에 종속적이 어야하지, Listener에 대해 각각 Event를 만들어 넘겨주는건 종속이 역전된다고 생각했다.</p>
<p>결론은 기각됐고 다음 의견을 제시했다.</p>
<h3 id="타-도메인의-부차적인-로직만-event를-발행해주자">타 도메인의 부차적인 로직만 Event를 발행해주자</h3>
<p>DDD방식의 프로젝트 구성이었기 때문에 여러 Event를 발행할 것이라면 타 도메인의 기능에 관한 것들만 Event를 발행하고, 같은 도메인의 부차적인 로직은 그냥 DI를 받아서 사용하면 Event를 최대한 줄일 수 있다고 생각했기 때문이다.</p>
<p>수많은 Event을 발행하고 그 Event 역시 Event를 발행하는 무수한 트리형식의 Event는 관리도 어렵기 때문에 최대한 Event를 줄이기 위한 차선책으로 제시를 했다.</p>
<p>하지만 이 역시 기각되어 좀 아쉬웠다.</p>
<h1 id="결론">결론</h1>
<p>내 제안이 정답이 아닐수도 있다는건 인정한다. </p>
<p>하지만 모든 제안이 기각되어 변한게 없었지만 프리를 하면서 어떤 마음가짐을 가져야할 지 많은 생각을 하게 됐었다. 
프리로 근무하면서 이렇게까지 기존의 방식을 바꾸자고 어필하는게 나한테 득이 될까? 괜히 트집으로만 보이는게 아닐까? 하는 생각들을 했다.</p>
<p>그래도 몇 달을 같이 일하게 될 동료고 내가 프리를 그만 두고도 남은 사람들이 좀 더 편하게 일했으면 좋겠다는 생각으로 했던 제안들이었고 아직 내 제안이 유지보수하고 메서드를 재사용 하기가 편리하다는 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring]Spring Event 맛보기]]></title>
            <link>https://velog.io/@no-int/SpringSpring-Event-%EB%A7%9B%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@no-int/SpringSpring-Event-%EB%A7%9B%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 12 Jun 2024 12:19:51 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>프리랜서로 근무하게 된 회사에서 코드 분석 중인데 클래스간 책임을 분리하기 위하여 Spring Event를 사용하고 있었다. 때문에 Spring Event를 알아보지 않고 넘어갈 수가 없어서 정리하려 한다.</p>
<h1 id="spring-event">Spring Event</h1>
<p>Spring Event는 스프링프레임 워크에서 내부적으로 데이터를 전달 하는 방법이다.</p>
<blockquote>
<p>예를 들어 결제 후 Log데이터를 기록해야 한다고 생각한다면, 간단히 결제Service에서 LogServcie를 의존하고 있다가 logService.write()로 실행할 수 있지만 구조가 복잡해지고 결제 후 추가로 해야할 작업이 많아진다면 결제Service가 의존하게되는 타 도메인들이 많아지게 되고 책임이 많아 지게 된다.</p>
</blockquote>
<p>이 때 클래스간 결합을 느슨하게 하여 클래스의 책임을 좀 더 간결하도록 하기 위해 Spring Event를 생각해 볼 수 있다.</p>
<p>보통 Event를 발행한다고 하면 마치 Queue같은 곳에 메시지를 발행하면 타 도메인에서 구독하고 있다가 컨슘하는 것 처럼 생각할 수 있는데 Spring Event는 Observer Pettern을 기반으로 구현하였다.</p>
<h3 id="사용법">사용법</h3>
<ul>
<li>발행할 정보</li>
</ul>
<p>이벤트를 통해 전달하고 싶은 정보 클래스.</p>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class PopEvent {
    private Long id;
}</code></pre>
<ul>
<li>이벤트 발행</li>
</ul>
<p>ApplicationEventPublisher클래스의 publishEvent메서드를 통해 정보를 발행한다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class EventService {

    private final ApplicationEventPublisher applicationEventPublisher;

    public void popEvent(Long id) {
        PopEvent popEvent = new PopEvent(id);
        applicationEventPublisher.publishEvent(popEvent);
    }
}</code></pre>
<ul>
<li>이벤트 리스너</li>
</ul>
<p>implements ApplicationListener<CustomSpringEvent>를 사용해서 구독할 수도 있지만 아마 왠만하면 다들 Spring 버전이 높아서 @EventListener 어노테이션을 통해 구독할 정보를 듣고 있으면 더 간단하게 구현이 가능하다.</p>
<p>리스너를 통해 구독 중인 정보의 데이터를 가져다 쓰면 끗!</p>
<pre><code class="language-java">@Getter
@Component
public class Listener {

    @EventListener(value = PopEvent.class)
    public void doSomething(PopEvent event) {
        System.out.println(&quot;event.getId() = &quot; + event.getId());
    }

}</code></pre>
<h3 id="주의점">주의점</h3>
<p>Spring Event를 통해 클래스간의 결합도를 낮추고 클래스의 책임을 더 명확하게 할 수 있지만 발행클래스와 구독클래스가 항상 정상작동한다고 보장하지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JMeter로 캐싱 적용 전후의 성능차이 분석]]></title>
            <link>https://velog.io/@no-int/JMeter%EB%A1%9C-%EC%BA%90%EC%8B%B1-%EC%A0%81%EC%9A%A9-%EC%A0%84%ED%9B%84%EC%9D%98-%EC%84%B1%EB%8A%A5%EC%B0%A8%EC%9D%B4-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@no-int/JMeter%EB%A1%9C-%EC%BA%90%EC%8B%B1-%EC%A0%81%EC%9A%A9-%EC%A0%84%ED%9B%84%EC%9D%98-%EC%84%B1%EB%8A%A5%EC%B0%A8%EC%9D%B4-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Thu, 18 Apr 2024 19:45:16 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>캐싱을 하는 이유는 더 좋은 성능을 기대하기 때문일 것이다. 서버를 실행시키는 환경마다 성능이 얼마나 개선 될 지 차이가 있겠지만 중요한 건 같은 환경에서 캐싱이 적용된다면 얼마나 성능이 개선되는지 확인하고 서버를 구성해야할 것이니 이것을 테스트 해보고자 한다.</p>
<h1 id="테스트-조건">테스트 조건</h1>
<p>클라우드 서버를 띄워서 테스트 하면 좋겠지만 비용이슈가 있어서 로컬에서 진행하였다.</p>
<ul>
<li>3분동안 1초당 500요청</li>
<li>힙 사이즈 512M ~ 1024M<ul>
<li>-Xms512M, -Xms1024M</li>
</ul>
</li>
<li>스택 사이즈 25M<ul>
<li>-Xss25M</li>
</ul>
</li>
<li>JVM CPU 코어 할당 수 2<ul>
<li>XX:ActiveProcessorCount=2</li>
</ul>
</li>
</ul>
<h3 id="jmemter-사용이유">JMemter 사용이유</h3>
<p>성능 측정 도구로 많은 툴이 있지만 빠른 테스트는 설치가 쉬운 JMemter가 적합하다고 생각했고 plugin을 통해 필요한 결과들을 그래프로 확인도 가능해 유용하다고 생각했다.</p>
<h1 id="tps">TPS</h1>
<p>TPS는 Throughput Per Seconds로 처리량을 의미하는 지표다. 초당 얼마만큼의 트랜잭션을 처리 할 수 있는지를 보여주므로 성능과 직결된 지표다.</p>
<h3 id="non-cache">Non-Cache</h3>
<p><img src="https://velog.velcdn.com/images/no-int/post/ed435f35-06bb-4288-a8e6-aadddf08d58b/image.png" alt=""></p>
<h3 id="cache">Cache</h3>
<p><img src="https://velog.velcdn.com/images/no-int/post/0f7f52f5-31fa-45c6-a97b-561220d18f9a/image.png" alt=""></p>
<h3 id="비교결과">비교결과</h3>
<p>Y축을 보면 숫자의 단위부터가 차이가 확 나는데 cache의 결과가 약 2~3배의 높은 처리량을 보여준다.</p>
<h1 id="response-time">Response Time</h1>
<p>응답시간은 유저가 작업을 요청하고 응답을 받기 까지 얼마나 걸리는지 나타내는 지표다. 응답시간이 오래 걸린다면 유저경험이 안 좋아 지기 때문에 반드시 확인해야하는 지표다.</p>
<h3 id="non-cache-1">Non-Cache</h3>
<p><img src="https://velog.velcdn.com/images/no-int/post/fb61f30c-aaad-4424-9385-e1529124213f/image.png" alt=""></p>
<h3 id="cache-1">Cache</h3>
<p><img src="https://velog.velcdn.com/images/no-int/post/89a07aa9-d2f4-4e28-8114-6a966f30fdc0/image.png" alt=""></p>
<h3 id="비교결과-1">비교결과</h3>
<p>Y축을 보면 숫자의 단위가 확 낮아진걸 확인 할 수 있는데 cache를 사용함으로써 약 2~3배 빠른 응답을 반환하는 것을 알 수 있다.</p>
<h1 id="결과">결과</h1>
<p>성능 테스트에서 가장 중요하다고 생각 되는 처리량과 응답시간을 그래프로 비교하니 최소 2배에서 최대 3배가량 성능향상을 보여준 것을 확인 할 수 있다.</p>
<ul>
<li><p>Non-Cache
<img src="https://velog.velcdn.com/images/no-int/post/beff4ce8-ab00-4a7f-b876-848f5b11a06e/image.png" alt=""></p>
</li>
<li><p>Cache
<img src="https://velog.velcdn.com/images/no-int/post/a9490b40-8b0d-4acf-a31b-20e1642bb174/image.png" alt=""></p>
</li>
</ul>
<p>좀 더 정확하게 성능향상을 눈으로 보고 싶어 성능 측정 수치 총 합량을 비교해보니 300% 성능이 높아 진 걸 확인 할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[조회 성능 개선하기 Redis 캐싱 적용기]]></title>
            <link>https://velog.io/@no-int/%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-Redis-%EC%BA%90%EC%8B%B1-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@no-int/%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-Redis-%EC%BA%90%EC%8B%B1-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Wed, 17 Apr 2024 10:27:48 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>실무에선 직접 캐싱 작업을 해본적이 없었다. 때문에 캐싱을 통한 조회성능 개선해보는 경험이 부족하다고 느껴서 진행 중인 프로젝트에 적용해 봤다.</p>
<h1 id="캐싱">캐싱</h1>
<p>캐싱은 작업의 결과를 임시 저장하는 것을 말한다. 보통 메모리에 저장해두고 사용하며 캐시된 데이터가 있다면 같은 작업을 여러번 하지 않고 빠르게 결과를 얻을 수 있는 장점이 있다.</p>
<h1 id="어디에-저장-하면-좋을까">어디에 저장 하면 좋을까?</h1>
<p>데이터를 캐시해서 사용하는 것은 같은 작업을 여러번 하지 않도록 하는 것도 있지만 응답 속도를 높이는 목적도 있다 때문에 디스크보단 메모리에 저장해서 사용하는게 일반적이다.</p>
<p>그렇다면 전역변수를 하나 만들어서 저장하고 사용하면 되는 걸까? 서버를 하나만 띄운다면 별 상관은 없다고 생각하지만 다중서버를 고려한다면 틀린 답이 될 것이다.</p>
<p>때문에 제3의 영역에 메모리 엔진을 사용해서 모든 서버가 접근해서 사용하도록 하는 것이 일반적이다.</p>
<p>대표적으로 많이 사용되는 메모리 엔진은 Redis, Memcached 등 이 있다.</p>
<h1 id="캐싱-적용기">캐싱 적용기</h1>
<p>Gathering프로젝트는 Redis를 사용해서 캐싱하기로 했다.</p>
<ul>
<li>이미 많은 사람들이 추천하고 사용할 정도로 Redis는 검증이 됐다</li>
<li>spring boot에서 starter로 의존성을 제공해주는 만큼 적용이 간단하다.</li>
</ul>
<p>위 2가지 이유로 Redis를 선택했다.</p>
<h3 id="캐싱이-필요한-곳">캐싱이 필요한 곳</h3>
<ul>
<li>많은 사용량이 있을 것이라고 예상 되는 곳</li>
<li>작업에 많은 비용이 들어가는 곳</li>
<li>데이터의 변경이 적은 곳</li>
</ul>
<p>Gathering은 위 같은 정도로 기준을 잡고 모임 예약 장소와 시간을 조회하는 API에 사용하였다.</p>
<h3 id="cacheable">@Cacheable</h3>
<pre><code class="language-java">    @Cacheable(key = &quot;#date.toString()&quot;, value = &quot;RoomSchedule&quot;, cacheManager = &quot;cacheManager&quot;)
    public List&lt;RoomScheduleResDto&gt; getDayRoomSchedule(LocalDate date) {
        checkDateRange(date);
        return reservationQueryRepository.findRoomScheduleByDate(date);
    }</code></pre>
<p>@Cacheable은 캐시 저장소에 값이 있다면 비즈니스 로직을 타지 않고 바로 값을 반환하고 값이 없다면 작업 후 반환된 값을 캐시 저장소에 저장 후 반환해준다.</p>
<p>때문에 제일 첫 작업은 비교적 느릴 수 있지만 그 이후 부터는 캐시된 데이터를 사용하니 빠른 응답을 받을 수 있다.</p>
<p>Gathering의 경우 예약 장소와 시간이 날짜 기준으로 항상 같은 값여서 DB에 접근하는 횟수를 줄이고자 캐싱하여 사용하기로 했다.</p>
<h3 id="cacheput">@CachePut</h3>
<pre><code class="language-java">@CachePut(key = &quot;#date.toString()&quot;, value = &quot;RoomSchedule&quot;, cacheManager = &quot;cacheManager&quot;)
    public List&lt;RoomScheduleResDto&gt; updateDayRoomSchedule(LocalDate date) {
        return reservationQueryRepository.findRoomScheduleByDate(date);
    }</code></pre>
<p>@CachePut은 캐시 저장소에 데이터가 있어도 반드시 작업을 진행하고 캐시 저장소에 업데이트 해주기 때문에 캐시 데이터에 변경사항이 적용 되야 할 때 사용해줘야한다.</p>
<p>Gathering의 경우 예약 장소와 시간을 날짜 기준으로 모두 뿌려주는데 해당 장소와 시간이 예약 된 경우 예약이 불가능 하다고 정보를 변경해 줘야 하기 때문에 사용하였다.</p>
<h1 id="캐시-데이터-확인">캐시 데이터 확인</h1>
<p>Medis라는 Redis GUI도구를 이용해 캐시데이터를 확인한다.</p>
<h3 id="조회">조회</h3>
<p><img src="https://velog.velcdn.com/images/no-int/post/ed338a85-d77b-4c28-b268-39de6a5f8f89/image.png" alt=""></p>
<h3 id="데이터-변경">데이터 변경</h3>
<p>모임 장소를 예약하면 후 해당 장소와 시간예약에 불가능 하다는 정보가 업데이트 되어야한다.
<img src="https://velog.velcdn.com/images/no-int/post/ed037247-ee22-4155-b395-da1f0dfcd8c7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CQRS 적용하여 비즈니스 로직 분리]]></title>
            <link>https://velog.io/@no-int/CQRS-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@no-int/CQRS-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Tue, 16 Apr 2024 17:17:01 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>Mybatis를 사용했을 땐 사실 CQRS의 필요성을 느끼지 못 했다. 근데 JPA를 공부하고 JPQL을 공부 해보니 CQS에 대해 알게 되었고 실제로 적용해보니 좀 자연스럽게 커맨드와 쿼리를 나누는 것을 생각하게 되었다. 근데 왜 CQS가 아닌 CQRS를 이용해서 분리 했는지 말해보고자 한다.</p>
<h1 id="cqscommand-query-separation-패턴">CQS(Command Query Separation) 패턴</h1>
<p>CQS는 명령과 조회는 분리 되어야 한다는 개념이다.</p>
<p>클래스의 필드 값을 조회할 때 getter, 수정 할 때 setter를 사용하는데 이 것도 하나의 CQS 패턴이다. 하지만 setter는 값을 수정만 해야지 반환 하는 값이 있으면 안된다.</p>
<pre><code class="language-java">public class Member {
    private String name;
    private int age;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}</code></pre>
<h1 id="cqrscommand-and-query-responsibility-segregation">CQRS(Command and Query Responsibility Segregation)</h1>
<p>CQRS는 CQS 개념을 포함하고 있고 있으며 CQS는 연산 수준에서 명령과 조회를 분리 했다면 CQRS는 객체 수준에서 명령과 조회를 분리하는 것이다.</p>
<p>CQRS를 이용한다면 명령(CUD)과 조회(R) 기능이 객체 수준에서 분리 되니, 기능이 많아져도 유지보수 하는 점에 있어서 분리되어 있어 유리하고 코드의 명확성이 높아진다.</p>
<pre><code class="language-java">// CQRS 적용 전
class MemberService{};


// CQRS 적용 후
class MemberCommendService{};

class MemberQueryService{};</code></pre>
<h1 id="왜-crqs를-적용-했나">왜 CRQS를 적용 했나</h1>
<p>서론에서 한번 언급 했듯 Mybatis를 사용했을 땐 명령과 조회를 분리 해야겠다는 필요성을 잘 못느꼈다.</p>
<p>근데 JPA를 사용하니 복잡한 조회는 JPQL을 사용을 해야 했고, JPQL의 프레임워크로 QueryDSL을 사용하게 되니 자연스럽게 Repository 부터 분리하여 사용하게 됐다.</p>
<p>Repository를 분리하여 사용하게 되니 Service 레이어 역시 같이 나눠서 사용하는게 관리하는 게 편할 것 같았기 때문에 CRQS를 Service 레이어 부터 적용하게 됐다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[낙관적 락과 비관적 락으로 동시성 제어(with. Retry)]]></title>
            <link>https://velog.io/@no-int/%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EA%B3%BC-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD%EC%9C%BC%EB%A1%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4with.-Retry</link>
            <guid>https://velog.io/@no-int/%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD%EA%B3%BC-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD%EC%9C%BC%EB%A1%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4with.-Retry</guid>
            <pubDate>Tue, 16 Apr 2024 16:29:45 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>Gathering은 간단한 프로젝트지만 진생하면서 동시성 문제를 가지고 있는 곳이 2군데가 있었다. 이걸 각각 낙관적 락과 비관적 락으로 나눠서 이슈를 관리한 이유와 방법을 나눠 보려고 한다.</p>
<h1 id="문제">문제</h1>
<h3 id="첫-번째-문제">첫 번째 문제</h3>
<p>첫 번째 문제는 동시에 여러유저가 같은 모임에 참가할 때 최대 인원이 넘는 유저가 참가 되는 문제다.</p>
<blockquote>
<p>Gathering의 모임 참가 로직은 <code>현재 참여인원 &lt; 최대 참여인원</code>의 조건이 만족해야 참가 할 수 있다.
참가 완료시 현재 참여인원을 +1 해주고, 모임참가 table에 유저를 삽입한다.</p>
</blockquote>
<p>예상 되겠지만 모임 참가 요청이 동시에 여러 유저가 요청한다면 현재 참가 인원과 모임참가 table에는 유저의 수가 일치 하지 않는 문제가 발행하여 이 것을 낙관적 락과 Retry를 통해 최대 인원까지만 참여 되도록 해결했다.</p>
<h3 id="두-번째-문제">두 번째 문제</h3>
<p>두 번째 문제는 모임의 예약 문제였다. 당연하지만 여러 모임이 같은 장소, 같은 시간에 예약 할 수 없도록 해줘야만 했다.</p>
<blockquote>
<p>Gathering은 미리 정해진 장소와 1시간 단위로 정해진 운영시간을 예약하도록 되어있어, 예약 할 수 있는 공간과 장소는 미리 정해져서 제공되고 있다.</p>
</blockquote>
<p>모임을 가질 장소와 시간 예약은 당연히 선착순이라고 생각하며, 단 하나의 모임만 사용할 수 있으니 비관적 락을 통해 먼저 접근한 요청을 예약 할 수 있도록 했다.</p>
<h1 id="문제-1-해결--낙관적-락optimistic-lock">문제 1 해결 : 낙관적 락(Optimistic Lock)</h1>
<p>낙관적 락을 간단히 설명하자면 version컬럼을 이용해 통해 DB가 아닌 서버에서 동시성을 제어하는 방법이다.</p>
<ul>
<li>데이터 조회(version 컬럼 필수)</li>
<li>데이터 업데이트 시 version 체크<ul>
<li>조회한 version과 데이터 업데이트 시점의 version값이 같다면 업데이트 후 version +1</li>
<li>조회한 version과 데이터 업데이트 시점의 version값이 다르다면 업데이트 하지 않음.</li>
</ul>
</li>
</ul>
<p>JPA는 간단한 version관리 기능을 제공해준다.</p>
<h3 id="왜-낙관적-락을-적용했나">왜 낙관적 락을 적용했나?</h3>
<p>문제1은 모임의 최대인원을 넘지 않도록 하는게 제1목적이었다.  </p>
<p>비관적 락을 사용해서 핸들링 할 수도 있지만 DB에서 베타락 이용하기 때문에 데드락의 원인이 될 수도 있고, 모임 테이블은 가장 많은 조회가 예상되어서 최대한 조회에 영향을 주고 싶지 않았다.</p>
<p>때문에 version 컬럼을 추가하고 version관리를 통한 낙관적 락으로 모임 참가의 동시성 문제를 해결 했다.</p>
<p>그리고 version이 맞지 않다고 무조건 모임참가 실패라는 응답을 주는 것 보단, version이 맞진 않아도 최대 참가 인원에 도달하지 않았다면 Retry 로직을 통해 참가 재시도를 할 수 있도록 했다.</p>
<blockquote>
<p>JPA를 이용한 version관리는 @Version 어노테이션으로 간단하게 구현이 가능하다.</p>
</blockquote>
<pre><code class="language-java">@Slf4j
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(indexes = {
        @Index(name = &quot;IX_member&quot;, columnList = &quot;member_id&quot;)
})
public class Gathering extends BaseTimeEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;gathering_id&quot;)
    private Long id;
    ...
    @Version
    @ColumnDefault(value = &quot;0&quot;)
    private int version;
    ...
}</code></pre>
<p>Retry는 @Retry 어노테이션을 하나 만들어서 AOP를 이용해 낙관적 락을 재시도 하고 싶은 곳에 달아 주었다.</p>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    int value() default 10;
}</code></pre>
<blockquote>
</blockquote>
<pre><code class="language-java">@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE - 1)    //@Transactional annotation보다 먼저 실행되어야하므로.
@Aspect
@Component
public class RetryAspect {
    @Around(&quot;@annotation(retry)&quot;)
    public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
        int maxRetry = retry.value();
        Exception exceptionHolder = null;
        for (int retryCount = 1; retryCount &lt;= maxRetry; retryCount++) {
            try{
                return joinPoint.proceed();
            }catch (Exception e){
                log.error(&quot;[retry] try count ={}/{}&quot;, retryCount, maxRetry);
                exceptionHolder = e;
            }
        }
        throw exceptionHolder;
    }
}</code></pre>
<h1 id="문제-1-해결--비관적-락pessimistic-lock">문제 1 해결 : 비관적 락(Pessimistic Lock)</h1>
<p>비관적 락을 간단히 설명하자면 반드시 충돌이 일어난다고 생각 되는 곳에 DB의 lock기능인 Shared Lock(읽기 락) 또는 Exclusive Lock(쓰기 락)을 걸고 시작하는 것 이다.</p>
<p>Shared Lock 사용하면 다른 트랜젝션에선 <code>조회만 가능</code>하고, Exclusive Lock 사용하면 다른 트랙젝션에선 <code>조회도 불가능</code>하게 된다. 이 중 Gathering의 모임예약에는 Exclusive Lock락을 사용했다.</p>
<h3 id="왜-비관적-락을-사용했나">왜 비관적 락을 사용했나?</h3>
<p>모임 참가에서 동시성 문제는 사실 최대인원을 1~2명 더 참가해도 사전에 알 수 있어, 핸들링이 충분이 가능해 비관적 락을 이용할 만큼 데이터 정합성을 맞춰줄 필요가 없다고 생각했다.</p>
<p>하지만 모임 장소와 시간을 예약하는 기능은 다른 모임과 겹치게 된다면 유저 입장에서 미리 알 수있는 방법이 없다. 때문에 예약 시간에 하나 이상의 모임이 겹처 자리싸움이 날 수 있기에 확실한 데이터 정합성을 맞춰줄 필요가 있다고 생각했다.</p>
<blockquote>
<p>JPA에서 비관적 락을 사용하는 방법은 @Lock 어노테이션을 사용하면 된다.</p>
</blockquote>
<pre><code class="language-java">    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;SELECT rs FROM RoomSchedule rs WHERE rs.id IN (:ids)&quot;)
    List&lt;RoomSchedule&gt; findAllByIdForUpdate(@Param(&quot;ids&quot;) Iterable&lt;Long&gt; ids);</code></pre>
<p>LockModeType.PESSIMISTIC_WRITE  = for update사용</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DTO객체는 Record 클래스로!]]></title>
            <link>https://velog.io/@no-int/DTO%EA%B0%9D%EC%B2%B4%EB%8A%94-Record-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%A1%9C</link>
            <guid>https://velog.io/@no-int/DTO%EA%B0%9D%EC%B2%B4%EB%8A%94-Record-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%A1%9C</guid>
            <pubDate>Mon, 15 Apr 2024 17:40:29 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>실무에선 DTO객체를 POJO 클래스로 사용했지만, 그 때마다 생각했던 반복되는 코드에서 오는 피로감을 줄이고 싶다는 생각을 했다. 하지만 팀내의 관성이 있었고 또한, JAVA 11을 사용했기에 Record 클래스를 사용하기는 무리가 있었다.</p>
<p>하지만 개인 사이드 프로젝트에선 이런 제약없이 반복되는 코드의 피로감을 줄이기 위해 DTO는 Record클래스를 사용했다.</p>
<h1 id="dto객체">DTO객체</h1>
<p>DTO란 <code>Data Transfer Object</code>의 약자로 데이터를 요청, 반환 할 때 사용하는 JAVA 객체다. DTO객체는 보통 데이터를 담고, 조회하기 위한 용도로 getter, setter, 생성자를 추가해서 사용한다.</p>
<pre><code class="language-java">@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class XxxDto {
    ...
}</code></pre>
<p>Lombok을 사용한다면 보통 위와 비슷한 형태로 사용할 텐데 DTO가 많아 진다면 위와 같이 어노테이션 어러게를 사용한 것도 반복되는 코드가 되고 무엇보다 귀찮다고 생각했다. 때문에 이러한 반복된 코드를 줄이는 방법으로 Record 클래스를 사용하기로 결정 했다.</p>
<blockquote>
<p>@Data 어노테이션를 사용할 수도 있지만 이 방법은 지양하도록 하자.</p>
</blockquote>
<h1 id="record-클래스">Record 클래스</h1>
<p>Record 클래스는 자바17부터 정식 제공되는 특수한 class다.  Record 클래스는 특수한 class 답게 생성자, getter, hashCode(), equals() ,toString()를 기본적으로 제공해준다. 필요한 메서드가 있다면 추가해서 사용하면 된다.</p>
<p>하지만 잘 보면 setter클래스는 제공해주지 않는데 Record 클래스는 기본적으로 <code>불변객체</code>를 지향하기 때문에 객체를 생성할 땐 생성자를 통해 만들고 값이 변한다면 새로운 객체로 바꿔서 사용해야한다.</p>
<blockquote>
<p>setter는 기본적으로 안티패턴으로 지양해야하는 것 중 하나다.</p>
</blockquote>
<h1 id="dto를-record-클래스로-사용한-이유">DTO를 Record 클래스로 사용한 이유</h1>
<p>Record 클래스를 사용한다면 필드 값을 수정할 수 없다는 단점이 있지만 반복되는 코드량의 감소, 코드의 명확성, 불변성 같은 장점들이 DTO객체로 사용할 때 더욱 큰 장점으로 작용한다고 생각하였다.</p>
<h3 id="record-클래스와-pojo를-사용한-dto-비교">Record 클래스와 POJO를 사용한 DTO 비교</h3>
<pre><code class="language-java">// 기존 POJO 방식의 DTO
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class XxxDto {
    private Long id;
    private String name;
    private String age;
    ...

    public void custom(){
        ...
        ...
        ...
    };
}


// Record 클래스를 사용한 DTO
public record XxxDto(Long id, String name, String age, ...) {
    public void custom(){
        ...
        ...
        ...
    };
}</code></pre>
<p>Lombock을 사용했어도 Record 클래스의 간결함을 따라갈 순 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OSIV와 영속성 컨텍스트 범위]]></title>
            <link>https://velog.io/@no-int/OSIV%EC%99%80-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B2%94%EC%9C%84</link>
            <guid>https://velog.io/@no-int/OSIV%EC%99%80-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%B2%94%EC%9C%84</guid>
            <pubDate>Mon, 11 Mar 2024 16:36:08 GMT</pubDate>
            <description><![CDATA[<h1 id="영속성-컨텍스트의-범위">영속성 컨텍스트의 범위</h1>
<p>영속성 컨텍스트는 트랜젝션이 시작 할 때 만들어지고 트랜젝션이 끝니면 사라진다. 때문에 Spring에서 JPA를 사용할 때 @Transactional 어노테이션을 사용하여 영속성 컨텍스트를 사용해 db에 접근하도록 한다.</p>
<p>영속성 컨텍스트가 살아 있다 = db커넥션이 유지되고 있다 = 리소스를 사용한다 라고 보면된다.</p>
<h1 id="osiv">OSIV</h1>
<p>OSIV 전략은 트랜잭션 시작처럼 최초 데이터베이스 커넥션 시작 시점부터 API 응답이 끝날 때 까지 영속성 컨텍스트
와 데이터베이스 커넥션을 유지하게 되며 트랜잭션의 범위를 벗어난 Controller레이어, VIEW에서도 지연로딩과 같은 JPA 기능을 사용할 수 있도록 해준다.</p>
<p>하지만 OSIV를 키게 되면 트랜젝션을 넘어선 범위에서도 DB커넥션을 유지하게 되니 당연히 리소스를 계속 점유하게 되니 잘 생각해보고 사용해야한다.</p>
<p><code>spring.jpa.open-in-view
 : true(기본값)</code>
 <img src="https://velog.velcdn.com/images/no-int/post/54d30eb4-3ba9-4497-837a-a510e39e024a/image.PNG" alt="">
<img src="https://velog.velcdn.com/images/no-int/post/7834baa0-ee2c-4eec-b82e-afe87bd3a52e/image.PNG" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA를 사용할 때 나는 어떤걸 고려해봐야 할까?]]></title>
            <link>https://velog.io/@no-int/JPA%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C-%EB%82%98%EB%8A%94-%EC%96%B4%EB%96%A4%EA%B1%B8-%EA%B3%A0%EB%A0%A4%ED%95%B4%EB%B4%90%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@no-int/JPA%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EB%95%8C-%EB%82%98%EB%8A%94-%EC%96%B4%EB%96%A4%EA%B1%B8-%EA%B3%A0%EB%A0%A4%ED%95%B4%EB%B4%90%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sat, 09 Mar 2024 19:30:47 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>아직 JPA를 사용해본적이 없어 JPA공부해보고 내가 JPA를 사용하게 된다면 고려해봐야 할 것들을 정리해본다. 순서와 중요도는 전혀 상관이 없다.</p>
<h1 id="tostring은-조심히">ToString은 조심히</h1>
<p>엔티티를 @ToString 어노테이션으로 간단히 사용하려하면 연관관계끼리 toString을 하려고해서 무한루프에 빠질 수 있다. 때문에 연관관계 엔티티 끼리는 toString에서 제외 시켜주자</p>
<h1 id="레이지-로딩-사용">레이지 로딩 사용</h1>
<p>XXXToOne 관계는 기본 EAGER인데 LAZY로 변경해서 사용하는게 좋다. 둘다 N+1문제를 야기하지만 LAZY는 튜닝의 여지가 있고 필요에 따라 프록시를 사용해야 할 경우도 있기으니 LAZY를 기본으로 사용하자.</p>
<h1 id="단방향-설계부터-하자">단방향 설계부터 하자</h1>
<p>단뱡향 설계가 양방향 설계보다 무조건 좋다는 건 아니다. 하지만 우선 단방향 설계부터 하고 필요시에 양방향 설계를 추가하자.</p>
<h1 id="다대다-관계는-사용하지-말자">다대다 관계는 사용하지 말자</h1>
<p>다대다 관계는 RDB에서 사용되는 개념이지 객체지향과는 거리가 있기도 하고 성능상 좋지도 않다. 그러니 다대다 관계는 중간에 엔티티를 하나 더 사용해서 일대다, 다대일로 관계를 풀어서 사용하자.</p>
<h1 id="값-타입value-type은-변경불가능하게-설계">값 타입(value type)은 변경불가능하게 설계</h1>
<p>값 타입은 변경이 불가능 해야한다. 값 타입을 클래스로 만들어서 참조해서 사용해서 헷갈릴 수도 있지만 클래스 자체가 값이라고 간주해야하며, 값은 항상 통채로 바꿔야지 클래스 속성 하나만 변경하고 그러면 안된다.</p>
<h1 id="세터-사용을-지양하자">세터 사용을 지양하자</h1>
<p>비단 JPA에서만 통용되는 말은 아니지만, 엔티티의 속성을 변경할 땐 세터로 하나씩 변경하는 것 보단 의미있는 메서드를 사용해서 변경하는 것이 유지보수 측면에서도 좋으니 이 방법을 추천한다.</p>
<h1 id="컬렉션은-필드에서-초기화-하자">컬렉션은 필드에서 초기화 하자</h1>
<p>JPA의 구현체로 Hibernate을 많이 사용할 텐데 하이버네트는 컬랙션을 한번 더 감싸서 사용하는데 이 때 초기화 되어 있지 않다면 제대로 동작을 안할 수 도 있다고 한다.</p>
<h1 id="엔티티-업데이트는-변경감지를-사용하자">엔티티 업데이트는 변경감지를 사용하자</h1>
<p>엔티티 업데이트는 변경감지와 marge()를 사용할 수 있는데 marge는 준영속 상태의 엔티티를 영속상태로 변경시키는 것으로 결국 변경감지를 사용하긴 한다. 하지만 변경감지는 필요한 속성만 변경할 수 있지만 marge는 엔티티를 통으로 업데이트 되기 때문에 엔티티를 잘 못 설정하면 Null값을 업데이트를 할 수 도 있다.</p>
<h1 id="jpql은-querydsl을-사용하자">JPQL은 QueryDSL을 사용하자</h1>
<p>순수 JPQL은 결국 스트링 값이기에 런타임에서 오류를 발견하게 되고 동적 쿼리를 구성하기도 힘들다. 이 모든걸 해결해주는 것이 QueryDSL이며 컴파일 타임에 에러나 동적 쿼리에 아주 강력하다.</p>
<h1 id="api요청반환은-dto를-사용하자">api요청,반환은 DTO를 사용하자</h1>
<p>일단 api요청, 반환을 엔티티로 사용하게 될 경우 엔티티 자체가 노출 되는데 이건 지양해야하는 것 이다.</p>
<h3 id="request를-엔티티로-사용하게-될-경우">Request를 엔티티로 사용하게 될 경우</h3>
<ul>
<li>api 스펙이 엔티티가 되므로 엔티티 변경이 있다면 사용한 모든 api에 영향이 감.</li>
<li>여러곳에서 사용하게 되면 유효성 검사를 모두 공용으로 하게 될 수 밖에 없음. 이러면 안됨.</li>
</ul>
<h3 id="response를-엔티티로-사용하게-될-경우">Response를 엔티티로 사용하게 될 경우</h3>
<ul>
<li>api 스펙이 엔티티가 되므로 엔티티 변경이 있다면 사용한 모든 api에 영향이 감.</li>
<li>양방향 관계가 있는 엔티티가 반환타입이 되면 무한 루프에 빠질 수 있음.</li>
</ul>
<h1 id="쿼리-리턴타입을-정하는-법">쿼리 리턴타입을 정하는 법</h1>
<p>쿼리의 리턴타입을 엔티티를 받을지 DTO로 받을지는 서로 장단점이 있다. 엔티티로 받으면 일단 쿼리가 간단해지며 그래프 탐색도 하기 편하며 DTO로 받으면 쿼리문이 좀 지저분 해지고 그래프 탐색은 불가능하지만 fit한 결과를 얻을 수 있다.</p>
<p>JPA 일타강사이신 김영한님이 추천하는 방법은 다음과 같다.</p>
<ul>
<li>우선 엔티티로 리턴받고 필요한 값을 DTO로 변환.</li>
<li>필요하면 페치 조인으로 성능을 최적화.<ul>
<li>대부분의 성능 이슈 해결.</li>
</ul>
</li>
<li>그래도 안되면 DTO로 직접 조회하는 방법 사용.</li>
<li>최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용.</li>
</ul>
<h1 id="컬렉션-조회">컬렉션 조회</h1>
<p>XXXToMany관계를 가진다면 엔티티에서 컬렉션으로 참조하게 되는데 이게 참 골때려서 생각을 잘 해봐야한다.</p>
<p>우선 페치조인을 사용하면 모든 값을 읽고 메모리에서 페이징을 하려 하기 때문에 페이징이 안된다고 봐야함. 2개 이상의 컬랙션은 페치조인 안됨 등 문제가 많으니 다른 방식을 생각해볼 필요가 있다.</p>
<ul>
<li>먼저 XXXToOne관계만 페치 조인으로 값을 가져옴.<ul>
<li>페이징도 여기서 함.</li>
</ul>
</li>
<li>레이지로딩을 이용해서 컬렉션 값들을 필요한 만큼 가져옴.<ul>
<li>이때 그냥 가져오면 N+1문제가 있음.</li>
<li>hibernate.default_batch_fetch_size 또는 @BatchSize를 사용해서 최소한의 쿼리만 사용해서 컬렉션값들을 가져올 수 있도록 최적화 한다면 1+1~2정도의 쿼리로 값들을 가져올 수 있음.</li>
</ul>
</li>
</ul>
<h1 id="osiv는-끄는게-기본같다">OSIV는 끄는게 기본같다.</h1>
<p>OSIV를 간단하게 설명하면 영속성 컨텍스트를 트랜잭션범위를 넘겨서도 사용할 수 있게 하는 방법이다. MVC패턴을 사용하면 VIEW단에서도 레이지로딩을 사용할 수 있다.</p>
<p>OSIV는 default값이 ture며 OSIV가 켜져있다면 리소스를 계속 사용하게 된다는 단점이 있다. 때문에 많은 처리량이 요구되는 api 서버는 OSIV를 끄고 처리량이 적은 관리자 페이지는 열어서 사용해도 얼추 괜찮고 영속성 컨텍스트를 유연하게 사용할 수 있어 편리함도 가져갈 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA.페치조인(fetch join)]]></title>
            <link>https://velog.io/@no-int/JPA.%ED%8E%98%EC%B9%98%EC%A1%B0%EC%9D%B8fetch-join</link>
            <guid>https://velog.io/@no-int/JPA.%ED%8E%98%EC%B9%98%EC%A1%B0%EC%9D%B8fetch-join</guid>
            <pubDate>Thu, 29 Feb 2024 01:12:39 GMT</pubDate>
            <description><![CDATA[<h1 id="fetch-join">fetch join</h1>
<p>페치조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다. SQL의 join 처럼 연관된 엔티티나 컬렉션을 한번에 조회할 때 사용한다.</p>
<h3 id="사용법">사용법</h3>
<pre><code class="language-sql">SELECT m FROM Member m JOIN FETCH m.team</code></pre>
<p>이름은 페치 조인이지만 <code>join fetch</code>로 사용해야한다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>연관된 엔티티도 한번에 조회할 수 있음.<ul>
<li>일반 조인과는 다름.</li>
</ul>
</li>
<li>지연로딩 보다 우선적으로 적용됨.</li>
<li>일대다 관계로 패치 조인 사용시 페이징api 사용하기 힒듬.</li>
<li>페치조인 대상에 별칭(as)사용 X</li>
<li>두개 이상 컬렉션 페치조인 불가</li>
</ul>
<h3 id="일반-조인과-페치조인의-차이점">일반 조인과 페치조인의 차이점</h3>
<pre><code class="language-java">// fetch join
em.createQuery(&quot;SELECT m FROM Member m join fetch m.team&quot;, Member.class)
.getResultList();

// 일반 조인
em.createQuery(&quot;SELECT m, t FROM Member m join Team t ON t = m.team&quot;, MemberAndTeamDTO.class)
.getResultList();</code></pre>
<p>페치 조인과 일반 조인후 완성된 쿼리문을 보면 비슷해서 뭐가 다른지 헷갈리기 쉬운데 차이점은 분명하다.</p>
<ol>
<li>리턴 타입</li>
</ol>
<ul>
<li>페치조인의 경우 리턴타입을 엔티티로 사용하여 페치조인한 엔티티로 그래프 탐색이 가능한 반면, 일반 조인은 DTO클래스를 만들어 맵핑 시켜줘야하며 엔티티 그래프 탐색을 못 한다.</li>
</ul>
<ol start="2">
<li>조회 컬럼 차이</li>
</ol>
<ul>
<li>JPQL은 연관관계를 고려하지 않음. 때문에 일반 join으로 값을 가져오면 조회할 컬럼을 다 입력해줘야함.</li>
</ul>
<h1 id="왜-사용할까">왜 사용할까?</h1>
<p>페치조인을 사용하는 가장 큰 이유는 N+1문제를 거의 대부분 해결 해주기 때문에 최적화 용도로 사용한다.</p>
<p>지연로딩으로 설정해도 페치조인 쿼리를 보면 join을 사용해 한번에 다 가져오니 즉시로딩과 같은 기능이라고 생각할 수 있지만, 즉시로딩 역시 N+1문제를 야기하기에 페치조인을 통해 필요한 데이터를 한번에 가져오는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA. JPQL]]></title>
            <link>https://velog.io/@no-int/JPA.-JPQL</link>
            <guid>https://velog.io/@no-int/JPA.-JPQL</guid>
            <pubDate>Mon, 26 Feb 2024 21:25:57 GMT</pubDate>
            <description><![CDATA[<h1 id="jpa-쿼리">JPA 쿼리</h1>
<p>JPA는 아래와 같은 다양한 쿼리방법을 지원한다.</p>
<ul>
<li>JPQL</li>
<li>JPA Criteria</li>
<li>QueryDSL</li>
<li>네이티브 SQL</li>
</ul>
<p>JPQL은 SQL을 추상화한 객체지향 쿼리이며 문자열인 JPQL을 자바코드로 사용하도록 지원하는 것이 Criteria과 QueryDSL이다.</p>
<h1 id="jpql">JPQL</h1>
<p>JPQL은 JPA에서 SQL을 추상화한 쿼리로 SQL문법과 상당히 유사하다. SQL은 테이블을 기준으로 데이터에 접근하지만 JPQL은 객체지향 쿼리여서 엔티티를 기준으로 데이터에 접근한다.</p>
<pre><code class="language-sql">// sql
select * from member;
// jpql
select m from Member m;</code></pre>
<h3 id="특징">특징</h3>
<ul>
<li>엔티티 대상 쿼리</li>
<li>sql을 추상화 하여서 특정 DB문법에 의존하지 않음</li>
<li>아주 복잡한 쿼리를 짜기에는 부적합<ul>
<li>mybatis 또는 jdbc 등으로 네이티브 SQL을 사용해야함.</li>
<li>네이티브 SQL을 사용하면 영속성 컨텍스트의 데이터를 적절한 시점에 flush하여 db를 갱신시켜주고 사용해야함.</li>
</ul>
</li>
<li>결국 SQL문으로 바껴서 쿼리됨</li>
</ul>
<h1 id="jpql-사용법">JPQL 사용법</h1>
<h3 id="기본-문법">기본 문법</h3>
<pre><code class="language-jpql">select_문 :: =  
    select_절 
    from_절 
    [where_절] 
    [group by_절] 
    [having_절] 
    [order by_절]
update_문 :: = update_절 [where_절] 
delete_문 :: = delete_절 [where_절]</code></pre>
<h3 id="리턴-타입">리턴 타입</h3>
<ul>
<li>TypeQuery: 반환 타입이 명확할 때 사용<pre><code class="language-java">TypedQuery&lt;Member&gt; query = em.createQuery(&quot;select m from Member m&quot;, Member.class);</code></pre>
</li>
<li>Query: 반환 타입이 명확하지 않을 때 사용<pre><code class="language-java">Query&lt;Member&gt; query = em.createQuery(&quot;select m.username, m.age from Member m&quot;);</code></pre>
<h3 id="결과-조회-api">결과 조회 API</h3>
<ul>
<li>query.getResultList() : 결과가 하나 이상일 때, List반환</li>
<li>query.getSingleResult() : 결과가 정확히 하나, 단일 객체 반환</li>
<li>없거나, 하나 이상이면 Exception발생</li>
</ul>
</li>
</ul>
<h3 id="파라미터-바인딩">파라미터 바인딩</h3>
<p>파라미터 바인딩은 Prepared Statement와 매우 유사하다.</p>
<ul>
<li>이름 기준<pre><code class="language-java">SELECT m FROM Member m where m.username=:username 
query.setParameter(&quot;username&quot;, usernameParam);</code></pre>
</li>
<li>위치 기준<pre><code class="language-java">SELECT m FROM Member m where m.username=:?1 
query.setParameter(1, usernameParam);</code></pre>
추천하는 방식은 이름 기준 바인딩이다. 위치 기준 바인딩은 중간에 파라미터가 추가 된다면 뒤에 번호들도 다 밀어 써야하는 단점이 있다.</li>
</ul>
<h3 id="프로젝션">프로젝션</h3>
<p>프로젝션은 SELECT 절에 조회하르 대상을 지정하는 것으로 엔티티, 임베디드 타입, 스칼라 타입이 있다.</p>
<p>스칼라 타입은 <code>select m.name, m.age from Member m</code>과 같이 스트링 컬럼, 숫자 컬럼 등 다양한 컬럼을 조회하는 것을 말하는데 이 때 리턴 타입을 특정해서 맞춰주기가 애매하다.</p>
<p>방법으로는 몇 가자가 있는데 개인적으론 DTO를 사용하는 것이 좋아보인다.</p>
<ul>
<li>Query 타입으로 조회</li>
<li>Object[] 타입으로 조회<ul>
<li>object[0] 문자, object[1]숫자 같은 방식</li>
</ul>
</li>
<li>DTO사용<ul>
<li>SELECT new package.UserDTO(m.username, m.age) FROM  Member m</li>
<li>package이름까지 다 적어줘야함.</li>
<li>조회 컬럼 순서와 타입이 일치하는 생성자가 필요<h1 id="jpql-쿼리-예문">JPQL 쿼리 예문</h1>
<h3 id="페이징">페이징</h3>
JPQL은 페이징을 추상화한 API를 제공하여 DB에 종속적이지 않고 아주 간단하게 페이징 쿼리를 작성할 수 있다.<pre><code class="language-java">em.createQuery(SELECT m FROM Member m, Member.class)
.setFristResult(0)
.setMaxResults(10)</code></pre>
<h3 id="조인">조인</h3>
JPQL도 조인을 지원하며 SQL과 아주 유사하다.</li>
</ul>
</li>
<li>inner join<ul>
<li>SELECT m FROM Member m [INNER] JOIN m.team t</li>
</ul>
</li>
<li>outer join<ul>
<li>SELECT m FROM Member m LEFT [OUTER] JOIN m.team t</li>
</ul>
</li>
<li>세타 join<ul>
<li>select count(m) from Member m, Team t where m.username = t.name</li>
</ul>
</li>
</ul>
<h3 id="on절을-사용한-조인">ON절을 사용한 조인</h3>
<p>JPQL도 SQL과 동일하게 조인시 필더링을 위해 ON절을 사용할 수 있다.</p>
<pre><code class="language-sql">SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = &#39;A&#39; </code></pre>
<h3 id="서브쿼리">서브쿼리</h3>
<p>JPQL은 SQL과 비슷하게 서브쿼리도 지원하지만 <strong>하이버네트6 버전 이하는 FROM절에서 서브쿼리는 지원하지 않는다.</strong></p>
<p>때문에 FROM절에 서브쿼리를 사용해야한다면 조인으로 풀어본다, 각각 쿼리를 여러번 날려서 조합한다, 네이티브 SQL을 활용한다 정도가 있다.</p>
<p>나중에 사용한다면 하이버네트6을 사용하도록 해야겠다.</p>
<h3 id="case-조건">CASE 조건</h3>
<ul>
<li>범위 CASE식<pre><code class="language-sql">select
   case when m.age &lt;= 10 then &#39;학생요금&#39; 
      when m.age &gt;= 60 then &#39;경로요금&#39; 
      else &#39;일반요금&#39;
   end
from Member m</code></pre>
</li>
<li>단순 CASE식<pre><code class="language-sql">select 
  case t.name 
      when &#39;팀A&#39; then &#39;인센티브110%&#39;
       when &#39;팀B&#39; then &#39;인센티브120%&#39;
       else &#39;인센티브105%&#39; 
  end 
from Team t</code></pre>
</li>
<li>COALESCE : 하나씩 조회해서 null이 아니면 반환<pre><code class="language-sql">// 엔티티의 m.username이 null이면 &#39;이름 없는 회원&#39;반환
select coalesce(m.username,&#39;이름 없는 회원&#39;) from Member m</code></pre>
</li>
<li>NULLIF : 두 값이 같으면 null, 다르면 원래 값 반환<ul>
<li>IFNULL과 다르므로 헷갈리지 말자<pre><code class="language-sql">// 엔티티의 m.username이 관리자면 null반환
select NULLIF(m.username, &#39;관리자&#39;) from Member m</code></pre>
<h1 id="jpql-함수">JPQL 함수</h1>
JPQL은 기본적인 ANSI함수는 다 제공 된다고 보면 되며, 특정 DB에 종속적이여서 제공되지 않는 함수는 사용자가 추가해서 사용할 수 있다.<h3 id="hibernate6-이하">hibernate6 이하</h3>
</li>
</ul>
</li>
<li>Dialect 구현 class생성<pre><code class="language-java">// 본인 DB에 맞는 Dialect상속 받아서 구현
public class MyH2Dialect extends H2Dialect {
  public MySQLDialect() {
      registerFunction(&quot;group_concat&quot;, new StandardSQLFunction(&quot;group_concat&quot;, StandardBasicTypes.STRING));
  }
}</code></pre>
</li>
<li>hibernate.dialect 변경 <ul>
<li>org.hibernate.dialect.H2Dialect에서 package.MyH2Dialect로 변경</li>
</ul>
</li>
</ul>
<h3 id="hibernate6-이상">hibernate6 이상</h3>
<ul>
<li>FunctionContributer의 구현 class생성<pre><code class="language-java">public class CustomFunctionContributor implements FunctionContributor {
  @Override
  public void contributeFunctions(FunctionContributions functionContributions) {
      functionContributions.getFunctionRegistry()
              .register(&quot;group_concat&quot;, new StandardSQLFunction(&quot;group_concat&quot;, StandardBasicTypes.STRING));
  }
}</code></pre>
</li>
<li>src/main/resources/META-INF/services 경로에 org.hibernate.boot.model.FunctionContributor파일 생성<ul>
<li>파일명이 org.hibernate.boot.model.FunctionContributor임</li>
</ul>
</li>
<li>org.hibernate.boot.model.FunctionContributor파일에 CustomFunctionContributor 등록<ul>
<li>패키지경로.CustomFunctionContributor 방식으로 저장.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>