<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>그럼에도 불구하고</title>
    <link>https://beoks.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 28 Jun 2026 03:01:00 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>BEOKS</managingEditor>
    <image>
      <title>그럼에도 불구하고</title>
      <url>https://tistory1.daumcdn.net/tistory/3127533/attach/a0e1219e1c5946559578db3f6aa1c620</url>
      <link>https://beoks.tistory.com</link>
    </image>
    <item>
      <title>복잡한 정책을 고려한 코드 작성법</title>
      <link>https://beoks.tistory.com/entry/%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%A0%95%EC%B1%85%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EB%B2%95</link>
      <description>&lt;p&gt;기획자와 협의를 하다보면 다음과 같은 말을 종종 들을때가 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;이 정책은 우선 이렇게 결정되긴 했는데, 담당자 확인후 확정 드리겠습니다.&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;고객에 따라서 이 정책은 다르게 적용될 수도 있습니다.&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;특정한 조건에 해당하는 고객은 이 정책을 적용해주세요&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;물론 대부분의 경우 기획에 정확한 정책을 요구하는게 이상적이지만, 기한이 얼마 남지 않았거나, 고객 별로 딱 맞는 정책들을 적용하고 싶은 경우 등 어쩔수 없이 복잡하게 개발을 해야만하는 순간이 오게 됩니다.&lt;/p&gt;
&lt;p&gt;오늘은 이러한 경우에 어떻게 회사에서 해결했는지 얘기해보려고 한다. 설명에서는 실제 도메인이 아닌 할인 정책 예시 도메인을 사용하겠습니다.&lt;/p&gt;
&lt;h3&gt;1. 전략 패턴&lt;/h3&gt;
&lt;p&gt;가장 처음 적용한 해결책은 전략 패턴입니다.&lt;br&gt;전략 패턴은 정책의 종류나 상태에 따라 동작이 달라지는 경우 적용하기 좋은 디자인 패턴입니다.&lt;/p&gt;
&lt;p&gt;우선 정책 표준을 인터페이스로 정의합니다. 이 경우 입력된 값을 얼마나 할인할지가 인터페이스가 됩니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface DiscountPolicy {
    double applyDiscount(double price);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;그런 다음, 논의된 여러 정책 고려 사항을 구현체로 작성합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import org.springframework.stereotype.Component;

@Component(&amp;quot;normalPolicy&amp;quot;)
public class NormalDiscountPolicy implements DiscountPolicy {
    @Override
    public double applyDiscount(double price) {
        // 할인 없음
        return price;
    }
}

@Component(&amp;quot;specialPolicy&amp;quot;)
public class SpecialDiscountPolicy implements DiscountPolicy {
    @Override
    public double applyDiscount(double price) {
        // 10% 할인
        return price * 0.9;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 기획에서 2번 정책으로 확정되었다는 연락이 오면 바로 &lt;code&gt;@Primary&lt;/code&gt; 어노테이션을 추가해서 배포하면 매우 빠르게 기획 요구사항을 반영할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Primary
@Component(&amp;quot;specialPolicy&amp;quot;)
public class SpecialDiscountPolicy implements DiscountPolicy {
    @Override
    public double applyDiscount(double price) {
        // 10% 할인
        return price * 0.9;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.  프록시 패턴&lt;/h3&gt;
&lt;p&gt;이제 기획에서 &lt;code&gt;개인 고객은 5% 할인, 기업 고객은 10% 할인&lt;/code&gt;을 하도록 수정해달라는 요청이 왔습니다.&lt;br&gt;즉, 고객의 유형에 따라 다른 정책을 적용해 달라는 말이다. 이는 원래 객체에 대해 접근을 제어하는 프록시 패턴을 이용해 해결하는게 좋습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Primary
public class ProxyDiscountPolicy implements DiscountPolicy {

    private Map&amp;lt;UserType, DiscountPolicy&amp;gt; policyMap;

    public ProxyDiscountPolicy() {
        policyMap = new HashMap&amp;lt;&amp;gt;();
        policyMap.put(UserType.INDIVIDUAL, new IndividualDiscountPolicy());
        policyMap.put(UserType.CORPORATE, new CorporateDiscountPolicy());
    }

    @Override
    public double applyDiscount(User user, double price) {
        DiscountPolicy policy = policyMap.getOrDefault(user.getUserType(), price1 -&amp;gt; price1);
        return policy.applyDiscount(user, price);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 스펙 패턴&lt;/h3&gt;
&lt;p&gt;운영을 하다 보면, 특별한 경우가 생기고 그에 맞는 정책도 새로 생깁니다. &amp;quot;가입한 지 3개월 이내인 개인 고객은 20% 할인을 해주세요&amp;quot;, &amp;quot;개인 중 VIP 고객인 경우 30% 할인을 해주세요&amp;quot; 심지어 &amp;quot;그냥 A 고객은 2달 동안 10% 추가 할인해 주세요&amp;quot;와 같은 정책도 생깁니다. (B2B 기업인 경우 협상을 하면서 이런 예외 조건이 자주 생깁니다.)&lt;/p&gt;
&lt;p&gt;이러한 복잡하고 다양한 조건을 코드로 효율적으로 관리하기 위해 &lt;strong&gt;스펙 패턴(Specification Pattern)&lt;/strong&gt;을 적용할 수 있습니다. 스펙 패턴은 비즈니스 규칙을 캡슐화하여 재사용성과 조합성을 높이는 데 도움을 줍니다.&lt;/p&gt;
&lt;p&gt;우선, 할인 적용 여부를 판단하는 스펙을 인터페이스로 정의합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface DiscountSpecification {
    boolean isSatisfiedBy(User user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각각의 조건을 구현체로 만듭니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class NewUserSpecification implements DiscountSpecification {
    @Override
    public boolean isSatisfiedBy(User user) {
        return user.getJoinDate().isAfter(LocalDate.now().minusMonths(3));
    }
}

public class VipUserSpecification implements DiscountSpecification {
    @Override
    public boolean isSatisfiedBy(User user) {
        return user.isVip();
    }
}

public class SpecificUserSpecification implements DiscountSpecification {
    private String userId;

    public SpecificUserSpecification(String userId) {
        this.userId = userId;
    }

    @Override
    public boolean isSatisfiedBy(User user) {
        return user.getId().equals(userId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;스펙들을 조합하여 복잡한 조건을 구성할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class AndSpecification implements DiscountSpecification {
    private DiscountSpecification spec1;
    private DiscountSpecification spec2;

    public AndSpecification(DiscountSpecification spec1, DiscountSpecification spec2) {
        this.spec1 = spec1;
        this.spec2 = spec2;
    }

    @Override
    public boolean isSatisfiedBy(User user) {
        return spec1.isSatisfiedBy(user) &amp;amp;&amp;amp; spec2.isSatisfiedBy(user);
    }
}

public class OrSpecification implements DiscountSpecification {
    private DiscountSpecification spec1;
    private DiscountSpecification spec2;

    public OrSpecification(DiscountSpecification spec1, DiscountSpecification spec2) {
        this.spec1 = spec1;
        this.spec2 = spec2;
    }

    @Override
    public boolean isSatisfiedBy(User user) {
        return spec1.isSatisfiedBy(user) || spec2.isSatisfiedBy(user);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 다양한 스펙을 조합하여 정책을 적용할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SpecificationDiscountPolicy implements DiscountPolicy {
    @Override
    public double applyDiscount(User user, double price) {
        DiscountSpecification newUserSpec = new NewUserSpecification();
        DiscountSpecification vipUserSpec = new VipUserSpecification();
        DiscountSpecification specificUserSpec = new SpecificUserSpecification(&amp;quot;A&amp;quot;);

        DiscountSpecification specialSpec = new AndSpecification(newUserSpec, vipUserSpec);

        if (specialSpec.isSatisfiedBy(user)) {
            return price * 0.7; 
        } else if (vipUserSpec.isSatisfiedBy(user)) {
            return price * 0.8; 
        } else if (specificUserSpec.isSatisfiedBy(user)) {
            return price * 0.9; 
        } else {
            return price;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;스펙 패턴을 활용하면 비즈니스 로직을 유연하게 관리하고 확장하기 쉬워집니다. 새로운 조건이 추가되더라도 기존 코드를 크게 수정하지 않고도 대응할 수 있습니다. 또한 조건들을 조합하여 복잡한 정책도 간단하게 구현할 수 있습니다.&lt;/p&gt;
&lt;p&gt;실제 현업에서는 이 외에도 더 복잡한 요구사항들이 많이 발생합니다. 이럴 때일수록 디자인 패턴을 적절히 활용하여 코드의 유지보수성과 확장성을 높이는 것이 중요합니다.&lt;/p&gt;</description>
      <category>Computer Science/Project Management</category>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/123</guid>
      <comments>https://beoks.tistory.com/entry/%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%A0%95%EC%B1%85%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%9C-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EB%B2%95#entry123comment</comments>
      <pubDate>Mon, 23 Dec 2024 16:44:28 +0900</pubDate>
    </item>
    <item>
      <title>QueryDSL 데드락 해결하기</title>
      <link>https://beoks.tistory.com/entry/QueryDSL-%EB%8D%B0%EB%93%9C%EB%9D%BD-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
      <description>&lt;h1&gt;문제&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;프론트에서 특정 페이지에 접속하면 결과가 계속 넘어오지 않는 이슈를 전달&lt;/li&gt;
&lt;li&gt;디버깅을 위해 페이지에서 호출하는 각 쿼리를 일일이 호출한 결과 문제 없이 수행&lt;/li&gt;
&lt;li&gt;정말 딱 해당 페이지에 접속할때만 결과가 넘어오지 않음&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/YcsixYD.png&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;더 큰 문제는, 이러한 과정이 반복되면 모든 요청이 처리되지 않는 문제가 발생&lt;h1&gt;원인 파악&lt;/h1&gt;
&lt;h2&gt;1. 커넥션 확인&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;현 프로젝트는 JPA 를 사용하고 있으며, connection leak 이 발생하면 로그를 출력하도록 설정해둠&lt;/li&gt;
&lt;li&gt;위 버그를 재현하면 다른 로그 없이 connection leak 로그가 출력&lt;/li&gt;
&lt;li&gt;커넥션의 상태를 추적하고자 함&lt;/li&gt;
&lt;li&gt;라이브러리에서 커넥션을 흭득하는 과정을 보면 다음과 같이 &lt;code&gt;metricTracker&lt;/code&gt;에 로그를 남기는 모습을 볼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Connection getConnection(final long hardTimeout) throws SQLException  
{  
   suspendResumeLock.acquire();  
   final var startTime = currentTime();  

   try {  
      var timeout = hardTimeout;  
      do {  
         var poolEntry = connectionBag.borrow(timeout, MILLISECONDS);  
         if (poolEntry == null) {  
            break; // We timed out... break and throw exception  
         }  

         final var now = currentTime();  
         if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) &amp;gt; aliveBypassWindowMs &amp;amp;&amp;amp; isConnectionDead(poolEntry.connection))) {  
            closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);  
            timeout = hardTimeout - elapsedMillis(startTime);  
         }  
         else {  
            metricsTracker.recordBorrowStats(poolEntry, startTime);  
            return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry));  
         }  
      } while (timeout &amp;gt; 0L);  

      metricsTracker.recordBorrowTimeoutStats(startTime);  
      throw createTimeoutException(startTime);  
   }  
   catch (InterruptedException e) {  
      Thread.currentThread().interrupt();  
      throw new SQLException(poolName + &amp;quot; - Interrupted during connection acquisition&amp;quot;, e);  
   }  
   finally {  
      suspendResumeLock.release();  
   }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;이를 이용하기 위해서 로컬에 로그를 남기는 구현체를 새로 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package com.gabia.securityportal.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.zaxxer.hikari.metrics.IMetricsTracker;
import com.zaxxer.hikari.metrics.PoolStats;

public class LoggingMetricsTracker implements IMetricsTracker {

    private static final Logger logger = LoggerFactory.getLogger(LoggingMetricsTracker.class);
    private final String poolName;
    private final PoolStats poolStats;

    public LoggingMetricsTracker(String poolName, PoolStats poolStats) {
        this.poolName = poolName;
        this.poolStats = poolStats;
    }

    private String getPoolStatsLog() {
        return String.format(&amp;quot;Pool Name: %s, Total Connections: %d, Active Connections: %d, Idle Connections: %d, Threads Awaiting Connection: %d&amp;quot;,
                poolName,
                poolStats.getTotalConnections(),
                poolStats.getActiveConnections(),
                poolStats.getIdleConnections(),
                poolStats.getThreadsAwaitingConnection());
    }

    @Override
    public void recordConnectionCreatedMillis(long connectionCreatedMillis) {
        logger.debug(&amp;quot;Connection created in {} ms, {}&amp;quot;, connectionCreatedMillis, getPoolStatsLog());
    }

    @Override
    public void recordConnectionAcquiredNanos(long elapsedAcquiredNanos) {
        logger.debug(&amp;quot;Connection acquired in {} ns, {}&amp;quot;, elapsedAcquiredNanos, getPoolStatsLog());
    }

    @Override
    public void recordConnectionUsageMillis(long elapsedBorrowedMillis) {
        logger.debug(&amp;quot;Connection used for {} ms, {}&amp;quot;, elapsedBorrowedMillis, getPoolStatsLog());
    }

    @Override
    public void recordConnectionTimeout() {
        logger.warn(&amp;quot;Connection timeout occurred, {}&amp;quot;, getPoolStatsLog());
    }

    @Override
    public void close() {
        // No resources to close
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;구현체를 등록하기 위해 구현체 인스턴스를 반환하는 &lt;code&gt;MetricsTrackerFactory&lt;/code&gt; 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.zaxxer.hikari.metrics.IMetricsTracker;
import com.zaxxer.hikari.metrics.MetricsTrackerFactory;
import com.zaxxer.hikari.pool.PoolStats;

public class LoggingMetricsTrackerFactory implements MetricsTrackerFactory {

    @Override
    public IMetricsTracker create(String poolName, PoolStats poolStats) {
        return new LoggingMetricsTracker();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BeanPostProcessor&lt;/code&gt;를 이용해 구현한 &lt;code&gt;LoggingMetricsTracker&lt;/code&gt;를 &lt;code&gt;HikariDataSource&lt;/code&gt;의 &lt;code&gt;MetricsTracker&lt;/code&gt;로 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.metrics.MetricsTrackerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HikariMetricsConfiguration {

    @Bean
    public MetricsTrackerFactory metricsTrackerFactory() {
        return new LoggingMetricsTrackerFactory();
    }

    @Bean
    public BeanPostProcessor hikariMetricsPostProcessor(MetricsTrackerFactory metricsTrackerFactory) {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if (bean instanceof HikariDataSource) {
                    ((HikariDataSource) bean).setMetricsTrackerFactory(metricsTrackerFactory);
                }
                return bean;
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;위 기능을 이용해서 커넥션의 상태를 확인한 결과 pending 이 걸리는 요청 개수 만큼 active 상태로 유지되는 커넥션이 지속해서 쌓이는 것을 확인.&lt;/li&gt;
&lt;li&gt;이 과정이 누적되면서 사용가능한 커넥션이 줄어들고 결국엔 사용할 수 있는 커넥션이 없어 모든 조회에서 pending 이 발생하는 것을 확인&lt;/li&gt;
&lt;li&gt;DB 세션을 조회한 결과 해당 커넥션은 아무 작업도 수행하고 있지 않는 것을 확인&lt;/li&gt;
&lt;li&gt;이는, 커넥션을 흭득한 작업이 무한 루프에 빠져 탈출하고 있지 못하고 있다는 것을 의미, 다른 원인을 찾아보기로함&lt;h2&gt;2. Step Debugging&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;무한 루프에 걸리는 부분을 찾기 위해서 디버깅 모드를 켜고 한 줄씩 실행&lt;/li&gt;
&lt;li&gt;그 중, queryDSL 로 쿼리를 생성하는 부분에서 다음 스텝으로 넘어가지 않는 현상을 확인&lt;/li&gt;
&lt;li&gt;좀더 정확한 부분을 찾기 위해 스텝인 진행&lt;/li&gt;
&lt;li&gt;라이브러리가 만들어준 QEntity 인스턴스를 참고하는 과정에서 예외 발생&lt;br&gt;&lt;img src=&quot;https://i.imgur.com/azyVhZD.png&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;구글링을 통해 유사한 이슈 검색&lt;/li&gt;
&lt;li&gt;queryDSL 공식문서에서 &lt;a href=&quot;http://querydsl.com/static/querydsl/4.1.3/reference/html_single/#d0e2756&quot;&gt;멀티스레딩 환경에서 Q-type 초기화 문제&lt;/a&gt; 를 확인&lt;/li&gt;
&lt;li&gt;멀티스레딩 환경에서 Q-type 을 초기화하면 데드락이 발생할 수 있다는 문제&lt;/li&gt;
&lt;li&gt;정확히 나의 현재 상황과 일치했기때문에 Repository 에서 별도의 QEntity 인스턴스를 생성해 사용&lt;/li&gt;
&lt;li&gt;그 결과 기존 문제가 해결되고 커넥션도 잘 반환되는 것을 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/ABymFbG.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</description>
      <category>Computer Science/Database</category>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/122</guid>
      <comments>https://beoks.tistory.com/entry/QueryDSL-%EB%8D%B0%EB%93%9C%EB%9D%BD-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0#entry122comment</comments>
      <pubDate>Tue, 3 Dec 2024 22:17:05 +0900</pubDate>
    </item>
    <item>
      <title>왜 개발자는 블로깅을 해야할까?</title>
      <link>https://beoks.tistory.com/entry/%EC%99%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%B8%94%EB%A1%9C%EA%B9%85%EC%9D%84-%EC%9E%91%EC%84%B1%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C</link>
      <description>&lt;p&gt;주위에서 개발자는 블로깅을 작성해야한다는 말을 여러번 듣는다.&lt;/p&gt;
&lt;p&gt;처음에 블로그 환경을 세팅하고 글을 작성할때는 재매있지만, 곧 흥미를 잃어버리면 글을 쓰는 것을 주저하게 된다.&lt;/p&gt;
&lt;p&gt;이 글에서는 정말로 블로그 글을 작성하는게 도움이되는지 확인하고자 한다.&lt;/p&gt;
&lt;p&gt;우선 다른 사람들이 블로그 글을 작성하는 이유를 살펴보자.&lt;/p&gt;
&lt;p&gt;hackernews 에는 &lt;a href=&quot;https://news.ycombinator.com/item?id=41646531&quot;&gt;Why I still blog after 15 years&lt;/a&gt;포스트와 댓글에 여러 토론이 있다.&lt;br&gt;굉장히 양이 많지만 블로깅의 이유에 대한 내용을 요약하면 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;블로깅의 즐거움 : 글쓰는 과정 자체가 즐겁다.&lt;/li&gt;
&lt;li&gt;생각 정리 : 글쓰기를 통해 생각을 정리하고 다양한 관점에서 고려할 수 있다. &lt;/li&gt;
&lt;li&gt;노력 : 공개할 글의 완성도를 높이기 위해 노력하는 과정에서 코드, 아이디어를 더욱 다듬게 합니다.&lt;/li&gt;
&lt;li&gt;기록 : 개인 프로젝트를 기록할 수 있는 공간입니다.&lt;/li&gt;
&lt;li&gt;성취: 블로그를 통해 내가 해온 일들의 성취감을 느끼게 됩니다.&lt;/li&gt;
&lt;li&gt;자기만족 : 블로그 개발은 저만의 문제를 해결하는 프로젝트로 개발 즐거움을 제공합니다.&lt;/li&gt;
&lt;li&gt;글쓰기 연습 : 글쓰기를 통해 더 나은 커뮤니케이터가 되고, 이는 개발자로서도 중요한 능력이라고 생각합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;개인적으로 여기서 가장 중요한 것은 &lt;code&gt;만족&lt;/code&gt;이라고 생각한다. 어떤 일을 지속적으로 수행하기 위해서는 보상이 지속되어야 하는데, 가장 강력한 보상이 그 일을 하는 과정에서 나오는 만족감이기 때문이다. &lt;/p&gt;
&lt;p&gt;나는 개인적으로 글을 쓰는 과정에서 고민을 많이 하게 된다. 한 문장, 한 문장을 작성할때마다 &lt;code&gt;이것보단 더 잘 작성할 수 있을 것 같은데&lt;/code&gt;라는 생각에 휩싸여, 지웠다 썻다를 반복하게 되고 시간이 지나면 굉장히 짧은 글이 되었다는 생각이 들어서다. 사실 글을 길게 쓸 필요는 없는데 투자한 노력에 비해서 결과물이 비루해서 실망감을 느끼는것 같다.&lt;/p&gt;
&lt;p&gt;사실 개발도 매번 코드를 작성할때마다 완벽하게 하려면 속도가 굉장히 느려지는 경우가 다반사이다. 매번 완벽하게하는것과 생각나는데로 작성하는것 둘 다 나중에가면 더 나은 방법이 있다고 아쉬워하는게 마찬가지인데 말이다. 글을 쓰는 과정도 동일하다고 생각하고 가볍게 내 생각들을 정리해서 최대한 만족감을 느끼는 방향으로 가야겠다. 지금 단계에서 참고한 자료에 나오는 성취, 글쓰기 연습 등은 유지보수처럼 나중에 생각하도록하자.&lt;/p&gt;</description>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/121</guid>
      <comments>https://beoks.tistory.com/entry/%EC%99%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%8A%94-%EB%B8%94%EB%A1%9C%EA%B9%85%EC%9D%84-%EC%9E%91%EC%84%B1%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C#entry121comment</comments>
      <pubDate>Mon, 2 Dec 2024 22:59:24 +0900</pubDate>
    </item>
    <item>
      <title>Logback 로그 출력 검사 방법</title>
      <link>https://beoks.tistory.com/entry/Logback-%EB%A1%9C%EA%B7%B8-%EC%B6%9C%EB%A0%A5-%EA%B2%80%EC%82%AC-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;h1&gt;검증을 위한 Logback 로그 출력 테스트 방법&lt;/h1&gt;
&lt;p&gt;로그 출력을 검증하는 것은 테스트 코드 작성 시 중요한 부분입니다. 특히, 로그 레벨에 따른 조건적인 출력을 검증할 때는 정확성이 요구됩니다. 이 문서는 Logback 라이브러리를 사용하여 로그 출력을 효과적으로 검사하는 방법을 설명합니다.&lt;/p&gt;
&lt;h2&gt;Appender란?&lt;/h2&gt;
&lt;p&gt;Appender는 Logback의 핵심 구성 요소로 로그 이벤트를 기록하는 역할을 합니다. 공식 문서에 따르면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Logback은 로깅 이벤트를 작성하는 작업을 &amp;#39;appender&amp;#39;라고 불리는 컴포넌트에 위임합니다. Appender는 ch.qos.logback.core.Appender 인터페이스를 구현해야 합니다. - &lt;a href=&quot;https://logback.qos.ch/manual/appenders.html&quot;&gt;Logback 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;기본적으로, 스프링 부트에는 콘솔에 로그 이벤트를 출력하는 콘솔 Appender가 포함되어 있습니다.&lt;/p&gt;
&lt;h2&gt;로그 검사를 위한 사용자 정의 Appender 구현&lt;/h2&gt;
&lt;p&gt;로그 출력을 검사하기 위해서는 사용자 정의 Appender와 &lt;code&gt;RecordCheckable&lt;/code&gt; 인터페이스를 구현해야 합니다. Logback 라이브러리는 리스트 형태로 이벤트를 기록하는 &lt;code&gt;ListAppender&lt;/code&gt;를 제공합니다. 아래는 &lt;code&gt;ListAppender&lt;/code&gt;를 확장하고 &lt;code&gt;RecordCheckable&lt;/code&gt; 인터페이스를 구현한 예시 코드입니다.&lt;/p&gt;
&lt;h3&gt;ListAppender&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.ArrayList;  
import java.util.List;  
import ch.qos.logback.core.AppenderBase;  

public class ListAppender&amp;lt;E&amp;gt; extends AppenderBase&amp;lt;E&amp;gt; {  
    public List&amp;lt;E&amp;gt; list = new ArrayList&amp;lt;E&amp;gt;();  

    protected void append(E e) {  
        list.add(e);  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;RecordCheckable 인터페이스와 RecordCheckAppender 구현&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import ch.qos.logback.classic.Level;  
import ch.qos.logback.classic.Logger;  
import ch.qos.logback.classic.spi.ILoggingEvent;  
import ch.qos.logback.core.read.ListAppender;  

interface RecordCheckable {  
    boolean isRecord(String message, Level level);  
}  

public class RecordCheckAppender extends ListAppender&amp;lt;ILoggingEvent&amp;gt; implements RecordCheckable {  
    public static RecordCheckAppender addAppender(Logger logger) {  
        RecordCheckAppender newAppender = new RecordCheckAppender();  
        logger.addAppender(newAppender);  
        newAppender.start();  
        return newAppender;  
    }  

    @Override  
    public boolean isRecord(String message, Level level) {  
        return this.list.stream()  
            .filter(iLoggingEvent -&amp;gt; iLoggingEvent.getLevel().equals(level))  
            .anyMatch(iLoggingEvent -&amp;gt; iLoggingEvent.getMessage().contains(message));  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;테스트 코드에서의 사용&lt;/h3&gt;
&lt;p&gt;테스트를 진행하기 전에 테스트 대상 클래스의 Logger에 &lt;code&gt;RecordCheckAppender&lt;/code&gt;를 추가합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@BeforeEach  
void beforeEach() {  
    this.recordCheckAppender =  
        RecordCheckAppender.addAppender((Logger) LoggerFactory.getLogger(Foo.class));  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 아래와 같이 로그 레벨과 메시지를 통해 로그의 출력 여부를 검증할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test  
public void testAlertUnspecifiedAuthError() {  
    Foo foo = new Foo();
    foo.log();
    assertTrue(recordCheckAppender.isRecord(&amp;quot;foo log가 출력되었습니다.&amp;quot;, Level.WARN));  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위의 방법을 통해 로그 출력의 정확성을 테스트 코드 내에서 쉽고 명확하게 검증할 수 있습니다.&lt;/p&gt;</description>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/120</guid>
      <comments>https://beoks.tistory.com/entry/Logback-%EB%A1%9C%EA%B7%B8-%EC%B6%9C%EB%A0%A5-%EA%B2%80%EC%82%AC-%EB%B0%A9%EB%B2%95#entry120comment</comments>
      <pubDate>Mon, 29 Jan 2024 13:29:26 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Request Body 를 직접 읽을 때 주의할 점</title>
      <link>https://beoks.tistory.com/entry/Spring-Request-Body-%EB%A5%BC-%EC%A7%81%EC%A0%91-%EC%9D%BD%EC%9D%84-%EB%95%8C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90</link>
      <description>&lt;h1&gt;[Spring] Request Body 를 직접 읽을 때 주의할 점&lt;/h1&gt;
&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;Filter 를 이용해 로그에 Request Body 를 기록하는 기능을 추가하자&lt;br&gt;기존 API의 Request Body가 비어있는 오류가 발생했다. 원인과 해결방안에 대해서 알아보자&lt;/p&gt;
&lt;h2&gt;예시 HTTP 코드&lt;/h2&gt;
&lt;p&gt;다음과 같은 테스트 HTTP API 코드가 있다고 하자&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@RestController
class TestController {
    companion object{
        val log:Logger=LoggerFactory.getLogger(this::class.java)
    }

    @PostMapping(&amp;quot;/test/requestBody&amp;quot;)
    fun test(@RequestBody message: Message): Message {
        return message
    }

    data class Message(
        val id: Long,
        val message: String
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 경우 아래와 같은 테스트 코드를 실행하면 당연히 성공해야한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@SpringBootTest
@AutoConfigureMockMvc
class TestControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Autowired
    private lateinit var objectMapper: ObjectMapper

    @Test
    fun `test endpoint should return correct message`() {
        val message = TestController.Message(1, &amp;quot;test&amp;quot;)

        mockMvc.perform(
            post(&amp;quot;/test/requestBody&amp;quot;)
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(message)
            )
            .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk)
            .andExpect(jsonPath(&amp;quot;$.id&amp;quot;, Matchers.`is`(message.id.toInt())))
            .andExpect(jsonPath(&amp;quot;$.message&amp;quot;, Matchers.`is`(message.message)))
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Request Body를 사용하는 필터 추가&lt;/h2&gt;
&lt;p&gt;Sentry (에러 모니터링 도구)에 Request Body를 기록하는 필터를 추가한다.&lt;br&gt;request 에서 request body를 읽어오는 모습을 볼 수 있다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Component
class RequestBodyLogger: OncePerRequestFilter() {

    val log: Logger=LoggerFactory.getLogger(this::class.java)
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        Sentry.configureScope{scope -&amp;gt;
            scope.setExtra(&amp;quot;requestBody&amp;quot;,request.reader.lines().collect(Collectors.joining(System.lineSeparator())))
        }
        filterChain.doFilter(request,response)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;에러&lt;/h2&gt;
&lt;p&gt;필터를 추가하고 다시 테스트 코드를 실행하면 아래와 같은 에러를 마주친다. &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java.lang.IllegalStateException: Cannot call getInputStream() after getReader() has already been called for the current request&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 에러는 @RequestBody 어노테이션을 처리하는 과정에서 request body의 getReader()가 이미 사용되었기 때문에 더 이상 request body를 읽어올 수 없다는 내용이다.&lt;/p&gt;
&lt;p&gt;Servlet의 Request Body는 메모리에 저장하는 것이 아니라 BufferReader 또는 InputStream으로 처리된다. 즉, 한 번 소비가 되면&lt;br&gt;다른 곳에 저장하지 않는 이상 더 이상 데이터를 가져올 수가 없다.&lt;/p&gt;
&lt;p&gt;따라서 필터에서 이미 소비가 되었기 때문에 컨트롤러에서 RequestBody를 해석하려고 하면 데이터를 읽어올 수 없기 때문에 에러가 발생한다.&lt;/p&gt;
&lt;h2&gt;해결방안&lt;/h2&gt;
&lt;p&gt;해결방안은 &lt;code&gt;ContentCachingRequestWrapper&lt;/code&gt;를 사용하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ContentCachingRequestWrapper&lt;/code&gt;는 HttpServletRequest 에서 스트림이나 리더를 이용해 소비되는 데이터를 캐시에 저장해&lt;br&gt;byte array를 이용해 조회할 수 있도록 하는 래퍼 클래스이다.&lt;/p&gt;
&lt;p&gt;이 클래스를 이용하면 필터에서 request body 데이터를 가져와도 이후에도 다시 데이터를 가져올 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Component
class RequestBodyLogger: OncePerRequestFilter() {

    val log: Logger=LoggerFactory.getLogger(this::class.java)
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        //use cache wrapper for request
        val wrappedRequest = ContentCachingRequestWrapper(request)
        filterChain.doFilter(request,response)

        Sentry.configureScope{scope -&amp;gt;
            val buf=wrappedRequest.contentAsByteArray
            val length = min(buf.size, request.contentLength)
            scope.setExtra(&amp;quot;requestBody&amp;quot;, String(buf, 0, length, charset(request.characterEncoding)))
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 중요한 점은 &lt;code&gt;contentAsByteArray&lt;/code&gt;를 가져오기 전에 &lt;code&gt;filterChain.doFilter&lt;/code&gt;를 호출해야한다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;contentAsByteArray&lt;/code&gt; 데이터는 request가 리더나 버퍼를 통해서 데이터가 읽어질 때 중간에 데이터를 가로채 저장함으로써 조회가 가능하다.&lt;br&gt;따라서, request body를 조회하기 전에 &lt;code&gt;contentAsByteArray&lt;/code&gt; 데이터를 조회하면 빈 값이 있으므로 &lt;code&gt;filterChain.doFilter&lt;/code&gt;를 먼저 호출해야한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;    /**
     * Return the cached request content as a byte array.
     * &amp;lt;p&amp;gt;The returned array will never be larger than the content cache limit.
     * &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Note:&amp;lt;/strong&amp;gt; The byte array returned from this method
     * reflects the amount of content that has been read at the time when it
     * is called. If the application does not read the content, this method
     * returns an empty array.
     * @see #ContentCachingRequestWrapper(HttpServletRequest, int)
     */
    public byte[] getContentAsByteArray() {
        return this.cachedContent.toByteArray();
    }&lt;/code&gt;&lt;/pre&gt;</description>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/119</guid>
      <comments>https://beoks.tistory.com/entry/Spring-Request-Body-%EB%A5%BC-%EC%A7%81%EC%A0%91-%EC%9D%BD%EC%9D%84-%EB%95%8C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90#entry119comment</comments>
      <pubDate>Thu, 28 Dec 2023 13:34:32 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin] Companion Object vs Inner Object</title>
      <link>https://beoks.tistory.com/entry/Companion-Object-vs-Inner-Object</link>
      <description>&lt;h1&gt;Companion Object vs Inner Object&lt;/h1&gt;
&lt;h2&gt;들어가며&lt;/h2&gt;
&lt;p&gt;companon object를 단순히 java의 static을 대체한다고만 하는 경우가 많아 좀 더 자세히 알아보도록 하자.&lt;/p&gt;
&lt;p&gt;companon object 에 대한 정의는 &lt;a href=&quot;https://kotlinlang.org/docs/object-declarations.html#companion-objects&quot;&gt;공식 문서&lt;/a&gt;에서 확인할 수 있다.&lt;br&gt;공식문서에서는 원리보다는 용례에 대해 중점적으로 설명하고 있는데, 여기에서는 companon object의 원리와 inner object와의 차이에 대해서 알아보고자 한다.&lt;/p&gt;
&lt;h2&gt;예시코드&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class CompanionObject {
    companion object{
        const val LIMIT=100
        fun add(a: Int,b: Int): Int = a+b
    }
}

class InnerObject {
    object Companion{
        const val LIMIT=100
        fun add(a: Int,b: Int): Int = a+b
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;분석&lt;/h2&gt;
&lt;p&gt;바이트 코드를 살펴보면 static inner 클래스가 생성되는 것은 동일하지만, 인스턴스 선언방식과 상수의 위치 차이를 알 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// CompanionObject.java
public final class CompanionObject {
   public static final int LIMIT = 100;
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      public final int add(int a, int b) {
         return a + b;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

// InnerObject.java
public final class InnerObject {

   public static final class Companion {
      public static final int LIMIT = 100;
      @NotNull
      public static final Companion INSTANCE;

      public final int add(int a, int b) {
         return a + b;
      }

      private Companion() {
      }

      static {
         Companion var0 = new Companion();
         INSTANCE = var0;
      }
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;자바에서 코틀린 companion object 호출&lt;/h2&gt;
&lt;p&gt;바이트 코드를 보면 알 수 있듯, 함수는 Companion 클래스 내부에 선언되어 있으므로 직접 호출이 불가능하다.&lt;br&gt;호출을 하려면 Companion 클래스까지 정의를 해야하는데 이는 java static 메서드의 정의와 상이하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import com.example.kotlinspring.companionobject.CompanionObject;

public class CompanionJavaTest {
    public static void main(String[] args) {
        int result = CompanionObject.add(1, 2); // 에러 발생
        int result = CompanionObject.Companion.add(1,2);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;직접 호출을 하기 위해서는 &lt;code&gt;@JVMStatic&lt;/code&gt; 어노테이션을 코틀린에서 정의하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class CompanionObject {
    companion object : Factory&amp;lt;CompanionObject&amp;gt;{
        const val LIMIT=100
        @JvmStatic fun add(a: Int,b: Int): Int = a+b
        override fun create(): CompanionObject = CompanionObject()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 아래처럼 바이트 코드에 static 함수가 생기고 이 함수는 Companion 클래스의 함수를 다시 호출한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final class CompanionObject {
    public static final int LIMIT = 100;
    @NotNull
    public static final Companion Companion = new Companion((DefaultConstructorMarker) null);

    @JvmStatic
    public static final int add(int a, int b) {
        return Companion.add(a, b);
    }
    //중략...
}&lt;/code&gt;&lt;/pre&gt;</description>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/118</guid>
      <comments>https://beoks.tistory.com/entry/Companion-Object-vs-Inner-Object#entry118comment</comments>
      <pubDate>Wed, 27 Dec 2023 14:44:57 +0900</pubDate>
    </item>
    <item>
      <title>MySQL TestContainer 적용기</title>
      <link>https://beoks.tistory.com/entry/MySQL-TestContainer-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 데이터에베이스에 연결해 테스트를 진행할 때는 로컬에 설치하거나 H2 인메모리 데이터베이스를 테스트 코드와 같이 실행시키는 등 다양한 방법이 있다. 이번에는 TestContainer를 이용해서 테스트 코드를 실행할 떄 MySQL 컨테이너를 실행시키고 연결해 통합테스트코드를 수행하는 방법에 대해서 알아보자. 완성된 프로젝트 코드는 &lt;a href=&quot;https://github.com/BEOKS/What-I-Learn/tree/main/spring-codekata/testcontainer-mysql&quot;&gt;링크&lt;/a&gt;에서 찾을 수 있다.&lt;/p&gt;
&lt;h1&gt;준비&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도커 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 컨테이너를 실행시키기 위해선 당연히 컨테이너 실행 도구가 필요하다. 이 중 가장 대표적인 도커를 설치해보자. &lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;링크&lt;/a&gt;를 통해 OS에 맞는 도커를 설치하도록 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경 변수 설정&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;To run Testcontainers-based tests, you need a Docker-API compatible container runtime, such as using &lt;a href=&quot;https://www.testcontainers.cloud/&quot;&gt;Testcontainers Cloud&lt;/a&gt; or installing Docker locally. During development, Testcontainers is actively tested against recent versions of Docker on Linux, as well as against Docker Desktop on Mac and Windows. These Docker environments are automatically detected and used by Testcontainers without any additional configuration being necessary.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestContainer가 설치된 컨테이너 실행도구를 찾기 위해서는 정보를 알려주어야 한다. TestContainer는 기본적으로 도커를 알아서 찾아 연결하려고 한다. 도커 설치 후 따로 설정을 하지 않는 이상 테스트 컨테이너를 위해 추가적으로 설정해야할 일은 없다. 만약 컨테이너 실행 도구를 도커로 사용하지 않거나 추가 설정이 필요하다면, &lt;a href=&quot;https://java.testcontainers.org/supported_docker_environment/&quot;&gt;링크&lt;/a&gt;를 참고하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gradle 종속성 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프로젝트에서 테스트 코드 실행시 도커를 활용하기 위해서는 TestContainer 모듈이 필요하다. 아래 종속성 코드를 참고해 필요한 모듈을 추가하도록 한다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web' // 예시 웹 서비스 용
    runtimeOnly 'com.mysql:mysql-connector-j'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.testcontainers:junit-jupiter' //테스트 컨테이너를 Junit 테스트 라이프사이클에 활용하기 위한 모듈
    testImplementation &quot;org.testcontainers:mysql:1.19.1&quot; //MySQL 컨테이너를 테스트에 활용하기 위한 모듈
    testImplementation 'io.rest-assured:rest-assured' //API 테스트를 위한 모듈
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;예시 웹 서비스 구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해서 고객 정보를 저장하고 조회하는 서비스를 간단하게 구현해보자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;학생 스키마 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;resources/schema.sql&lt;/code&gt;에 MySQL 에 정의할 학생 테이블 스키마 DDL을 작성하자&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE students
(
    id    BIGINT AUTO_INCREMENT NOT NULL,
    name  VARCHAR(255)          NULL,
    age   INT                   NOT NULL,
    email VARCHAR(255)          NULL,
    CONSTRAINT pk_students PRIMARY KEY (id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;학생 엔티티 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학생의 이름과 이메일을 저장하는 엔티티를 아래와 같이 정의하자.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package com.example.testcontainermysql.domain;  

import jakarta.persistence.*;  

@Entity  
@Table(name = &quot;students&quot;)  
public class Student {  

    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  

    private String name;  
    private int age;  
    private String email;  

    // 기본 생성자  
    public Student() {  
    }  
    // 모든 필드를 포함한 생성자  
    public Student(String name, int age, String email) {  
        this.name = name;  
        this.age = age;  
        this.email = email;  
    }  

    // getter와 setter 메소드  
    public Long getId() {  
        return id;  
    }  

    public void setId(Long id) {  
        this.id = id;  
    }  

    public String getName() {  
        return name;  
    }  

    public void setName(String name) {  
        this.name = name;  
    }  

    public int getAge() {  
        return age;  
    }  

    public void setAge(int age) {  
        this.age = age;  
    }  

    public String getEmail() {  
        return email;  
    }  

    public void setEmail(String email) {  
        this.email = email;  
    }  

    // toString 메소드  
    @Override  
    public String toString() {  
        return &quot;Student{&quot; +  
                &quot;id=&quot; + id +  
                &quot;, name='&quot; + name + '\'' +  
                &quot;, age=&quot; + age +  
                &quot;, email='&quot; + email + '\'' +  
                '}';  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 학생 레포지토리 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학생 정보를 저장, 조회하는 레포지토리를 정의하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;package com.example.testcontainermysql.domain;  

import org.springframework.data.jpa.repository.JpaRepository;  

public interface StudentRepository extends JpaRepository&amp;lt;Student, Long&amp;gt; {  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;학생 컨트롤러 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학생 정보를 조회하는 컨트롤러를 정의하자.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package com.example.testcontainermysql.api;

import com.example.testcontainermysql.domain.Student;
import com.example.testcontainermysql.domain.StudentRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class StudentController {
    private final StudentRepository studentRepository;

    public StudentController(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    @GetMapping(&quot;/student/all&quot;)
    List&amp;lt;Student&amp;gt; findAll(){
        return studentRepository.findAll();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;테스트&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 리소스 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;test/resources/spring.yaml&lt;/code&gt;에 테스트시 사용할 스프링 설정을 정의하자. 아래와 같이 정의하면 이전에 정의한 학생 스키마를 MySQL에 자동으로 전달해 학생 테이블을 생성한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:  
  sql:  
    init:  
      mode: always&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 코드 작성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 MySQL 컨테이너를 실행시키고 예시 학생 데이터를 생성한 다음 API를 통해 학생 데이터를 조회하는 테스트 코드를 작성해보자. 테스트 코드의 역할을 주석을 참고해보자.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;package com.example.testcontainermysql;  

import static io.restassured.RestAssured.given;  
import static org.hamcrest.Matchers.hasSize;  

import com.example.testcontainermysql.domain.Student;  
import com.example.testcontainermysql.domain.StudentRepository;  
import io.restassured.RestAssured;  
import io.restassured.http.ContentType;  
import java.util.List;  
import org.junit.jupiter.api.AfterAll;  
import org.junit.jupiter.api.BeforeAll;  
import org.junit.jupiter.api.BeforeEach;  
import org.junit.jupiter.api.Test;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.test.context.SpringBootTest;  
import org.springframework.boot.test.web.server.LocalServerPort;  
import org.springframework.test.context.DynamicPropertyRegistry;  
import org.springframework.test.context.DynamicPropertySource;  
import org.testcontainers.containers.MySQLContainer;  

/**  
 * 1. 통합 테스트를 위해 임의의 포트 번호를 할당한 웹 어플리케이션을 실행합니다.  
 */@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)  
class StudentControllerTest {  

    //현재 서버의 포트 번호  
    @LocalServerPort  
    private Integer port;  

    /**  
     * 2. 도커 이미지 명을 명시해 컨테이너를 정의합니다.  
     */    static MySQLContainer&amp;lt;?&amp;gt; mysql = new MySQLContainer&amp;lt;&amp;gt;(  
            &quot;mysql:8.0.35&quot;  
    );  

    /**  
     * 3. 테스트 전, 컨테이너를 실행합니다, 이미지가 없다면 풀링을 먼저 수행합니다.  
     */    @BeforeAll  
    static void beforeAll() {  
        mysql.start();  
    }  

    /**  
     * 4. 테스트 후, 컨테이너를 종료합니다.  
     */    @AfterAll  
    static void afterAll() {  
        mysql.stop();  
    }  

    /**  
     * 5. 통합 테스트에서 동적으로 데이터베이스 관련 프로퍼티를 정의합니다. 이를 통해, JPA가 어떤 데이터베이스 연결 정보를 알 수 있습니다.  
     * @param registry  
     */  
    @DynamicPropertySource  
    static void configureProperties(DynamicPropertyRegistry registry) {  
        registry.add(&quot;spring.datasource.url&quot;, mysql::getJdbcUrl);  
        registry.add(&quot;spring.datasource.username&quot;, mysql::getUsername);  
        registry.add(&quot;spring.datasource.password&quot;, mysql::getPassword);  
    }  

    @Autowired  
    StudentRepository studentRepository;  

    /**  
     * 6. 각 테스트 수행 전, 학생 테이블을 초기화합니다.  
     */    @BeforeEach  
    void setUp() {  
        RestAssured.baseURI = &quot;http://localhost:&quot; + port;  
        studentRepository.deleteAll();  
    }  

    /**  
     * 7. 예시 데이터를 입력 후, API 테스트를 수행합니다.  
     */    @Test  
    void shouldGetAllStudents() {  
        List&amp;lt;Student&amp;gt; customers = List.of(  
                new Student(&quot;foo&quot;, 11, &quot;john@mail.com&quot;),  
                new Student(&quot;bar&quot;, 12, &quot;dennis@mail.com&quot;)  
        );  
        studentRepository.saveAll(customers);  

        given()  
                .contentType(ContentType.JSON)  
                .when()  
                .get(&quot;/student/all&quot;)  
                .then()  
                .statusCode(200)  
                .body(&quot;.&quot;, hasSize(2));  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 코드 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 테스트 코드를 실행하면 아래와 같이 컨테이너가 실행되는 모습을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/2C5OVyl.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;컨테이너 재활용하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위와 같은 테스트 코드가 여러개라면 어떨까? 각 테스트 클래스가 실행될 때마다 컨테이너를 시작하고 종료하기를 반복할 것이다. 이는 테스트의 멱등성을 보장하지만, 똑같은 환경에서 읽기만 수행하는 테스트가 여러개 있을 경우에는 하나의 컨테이너만 사용해도 된다. 하나의 컨테이너만 사용하면 컨테이너를 시작하고 종료하는 시간을 줄일 수 있기 때문에 매우 효과적이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 재사용을 위해서는 &lt;code&gt;.testcontainers.properties&lt;/code&gt;파일 설정이 필요하다. 이 파일은 MAC OS 기준으로 &lt;code&gt;/Users/&amp;lt;사용자명&amp;gt;/.testcontainers.properties&lt;/code&gt;에 위치해 있다. 여기에 &lt;code&gt;testcontainers.reuse.enable=true&lt;/code&gt;를 추가해주면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 수정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 위해 &lt;code&gt;StudentControllerTest&lt;/code&gt;와 똑같은 클래스 &lt;code&gt;StudentControllerTest2&lt;/code&gt;, &lt;code&gt;StudentControllerTest3&lt;/code&gt;을 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 만든 코드에서 &lt;code&gt;afterAll&lt;/code&gt; 메서드를 삭제하고 컨테이너를 생성하는 코드를 다음과 같이 수정하자.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;static MySQLContainer&amp;lt;?&amp;gt; mysql = new MySQLContainer&amp;lt;&amp;gt;(  
        &quot;mysql:8.0.35&quot;  
).withReuse(true);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하고 테스트 코드를 수행하면 아래 로그처럼 컨테이너를 재활용하는 것을 확인할 수 있다!&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2023-11-14T15:44:34.462+09:00  INFO 69374 --- [    Test worker] tc.mysql:8.0.35                          : Reusing container with ID: 81a0dea4b23a4239dfad1b4d2c587d118a98d2bcf463155479563ebfc6c2e608 and hash: 97a15af525be79af393ee3c604797b631872c409
2023-11-14T15:44:34.463+09:00  INFO 69374 --- [    Test worker] tc.mysql:8.0.35                          : Reusing existing container (81a0dea4b23a4239dfad1b4d2c587d118a98d2bcf463155479563ebfc6c2e608) and not creating a new one&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시작 가이드, &lt;a href=&quot;https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/&quot;&gt;https://testcontainers.com/guides/testing-spring-boot-rest-api-using-testcontainers/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;General Container runtime requirements, &lt;a href=&quot;https://java.testcontainers.org/supported_docker_environment/&quot;&gt;https://java.testcontainers.org/supported_docker_environment/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Docker</category>
      <category>mysql</category>
      <category>test</category>
      <category>testcontainer</category>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/117</guid>
      <comments>https://beoks.tistory.com/entry/MySQL-TestContainer-%EC%A0%81%EC%9A%A9%EA%B8%B0#entry117comment</comments>
      <pubDate>Tue, 14 Nov 2023 16:16:38 +0900</pubDate>
    </item>
    <item>
      <title>[Gradle] MySQL JDBC 연결 방법</title>
      <link>https://beoks.tistory.com/entry/Gradle-MySQL-JDBC-%EC%97%B0%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&amp;nbsp;(Introduction)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 JDBC를 이용해 MySQL에 연결하는 방법을 알아보고, MySQL 드라이버의 연결과정에 대해서 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본문 (Body)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;MySQL을&amp;nbsp;실행한다.&lt;br /&gt;아래 Docker Compose를 이용하면 localhost:3306을 통해서 mysql에 접속이 가능하다. 물론 도커가 아니라 직접 설치해도 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1698649304711&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  
services:  
  
  db:  
    image: mysql:8.2.0  
    command: --default-authentication-plugin=mysql_native_password  
    restart: always  
    environment:  
      MYSQL_ROOT_PASSWORD: example  
      MYSQL_DATABASE: testDB  
    ports:  
      - 3306:3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2. gradle 종속성에 mysql JDBC connector 라이브러리를 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 어플리케이션을 시작할 때, MySQL JDBC 라이브러리의 클래스가 등록된다.&lt;/p&gt;
&lt;pre id=&quot;code_1698649327566&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;dependencies {  
    implementation 'org.springframework.boot:spring-boot-starter'  
    runtimeOnly 'com.mysql:mysql-connector-j:8.2.0'  
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2.&amp;nbsp;테스트&amp;nbsp;코드&amp;nbsp;작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 연결 테스트를 위한 코드를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1698649356684&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.Test;  
import org.springframework.boot.test.context.SpringBootTest;  
  
import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.SQLException;  
  
@SpringBootTest  
public class ConnectionTest {  
    @Test  
    void test(){  
        Connection connection=null;  
        try {  
             connection = DriverManager.getConnection(&quot;jdbc:mysql://localhost:3306/testDB&quot;, &quot;root&quot;, &quot;example&quot;);  
            Class&amp;lt;? extends Connection&amp;gt; aClass = connection.getClass();  
            System.out.println(&quot;connection = &quot; + connection);  
            System.out.println(&quot;aClass = &quot; + aClass);  
        } catch (SQLException e) {  
            throw new RuntimeException(e);  
        }  
        finally {  
            if(connection!=null){  
                try {  
                    connection.close();  
                } catch (SQLException e) {  
                    throw new RuntimeException(e);  
                }  
            }  
        }  
    }  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;3.&amp;nbsp;결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과를 보면, 커넥션 인스턴스가 생성되었고 그 클래스는 com.mysql.cj.jdbc.ConnectionImpl 인 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1698649434846&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;connection = com.mysql.cj.jdbc.ConnectionImpl@1f916219
aClass = class com.mysql.cj.jdbc.ConnectionImpl&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 분석&lt;br /&gt;DriverManager.getConnection는 다음과 같이 정의되어있다.&lt;/p&gt;
&lt;pre id=&quot;code_1698649559365&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@CallerSensitive  
public static Connection getConnection(String url,  
    String user, String password) throws SQLException {  
    java.util.Properties info = new java.util.Properties();  
  
    if (user != null) {  
        info.put(&quot;user&quot;, user);  
    }  
    if (password != null) {  
        info.put(&quot;password&quot;, password);  
    }  
  
    return (getConnection(url, info, Reflection.getCallerClass()));  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래 getConnection을&amp;nbsp;좀&amp;nbsp;더&amp;nbsp;들어가보면&amp;nbsp;&lt;b&gt;registeredDrivers&lt;/b&gt;에 있는 드라이버들에 접속 시도를 하는 것을 볼 수 있다. 여기까지 보면 java.sql.connection.DriverManager는 자신한테 등록되어 있는 드라이버들에서 하나씩 조회 시도를 하고 성공한 경우를 반환하는 것을 볼 수 있다. 그렇다면 MySQL 드라이버는 언제 등록되는 걸까?&lt;/p&gt;
&lt;pre id=&quot;code_1698649577388&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static Connection getConnection(
//...
for (DriverInfo aDriver : registeredDrivers) {  
    // If the caller does not have permission to load the driver then  
    // skip it.    if (isDriverAllowed(aDriver.driver, callerCL)) {  
        try {  
            println(&quot;    trying &quot; + aDriver.driver.getClass().getName());  
            Connection con = aDriver.driver.connect(url, info);  
            if (con != null) {  
                // Success!  
                println(&quot;getConnection returning &quot; + aDriver.driver.getClass().getName());  
                return (con);  
            }  
        } catch (SQLException ex) {  
            if (reason == null) {  
                reason = ex;  
            }  
        }  
  
    } else {  
        println(&quot;    skipping: &quot; + aDriver.driver.getClass().getName());  
    }  
  
}
//...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아래 com.mysql.cj.jdbc.Driver&amp;nbsp;파일을&amp;nbsp;보면&amp;nbsp;static&amp;nbsp;선언을&amp;nbsp;통해서&amp;nbsp;클래스&amp;nbsp;로드시&amp;nbsp;자기&amp;nbsp;자신을&amp;nbsp;등록하고&amp;nbsp;있는&amp;nbsp;모습을&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;즉,&amp;nbsp;gradle에&amp;nbsp;라이브러리를&amp;nbsp;추가하고&amp;nbsp;코드를&amp;nbsp;실행시킬&amp;nbsp;때&amp;nbsp;MySQL&amp;nbsp;드라이버가&amp;nbsp;java.sql.DriverManager에&amp;nbsp;등록되며,&amp;nbsp;DriverManager.getConnection&amp;nbsp;호출&amp;nbsp;시&amp;nbsp;MySQL&amp;nbsp;드라이버를&amp;nbsp;가져와&amp;nbsp;연결을&amp;nbsp;시도할&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;pre id=&quot;code_1698649693524&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;```java
package com.mysql.cj.jdbc; 
  
import java.sql.SQLException;  
  
/**  
 * The Java SQL framework allows for multiple database drivers. Each driver should supply a class that implements the Driver interface. * * &amp;lt;p&amp;gt;  
 * The DriverManager will try to load as many drivers as it can find and then for any given connection request, it will ask each driver in turn to try to  
 * connect to the target URL. * * &amp;lt;p&amp;gt;  
 * It is strongly recommended that each Driver class should be small and standalone so that the Driver class can be loaded and queried without bringing in vast  
 * quantities of supporting code. * * &amp;lt;p&amp;gt;  
 * When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a  
 * driver by doing Class.forName(&quot;foo.bar.Driver&quot;). */
 public class Driver extends NonRegisteringDriver implements java.sql.Driver {  
  
    // Register ourselves with the DriverManager.  
    static {  
        try {  
            java.sql.DriverManager.registerDriver(new Driver());  
        } catch (SQLException E) {  
            throw new RuntimeException(&quot;Can't register driver!&quot;);  
        }  
    }  
  
    /**  
     * Construct a new driver and register it with DriverManager     *     * @throws SQLException  
     *             if a database error occurs.     */    public Driver() throws SQLException {  
        // Required for Class.forName().newInstance().  
    }  
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&amp;nbsp;(Conclusion)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 JDBC를 이용해 MySQ에 접속하는 과정에 대해서 알아보았다. 뜻밖의 수확으로, MySQL에서 드라이버를 등록할 때 static 방식으로 진행한다는 건데 이런 용법은 처음보아 다른 코드에 유용하게 활용할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/116</guid>
      <comments>https://beoks.tistory.com/entry/Gradle-MySQL-JDBC-%EC%97%B0%EA%B2%B0-%EB%B0%A9%EB%B2%95#entry116comment</comments>
      <pubDate>Mon, 30 Oct 2023 16:11:39 +0900</pubDate>
    </item>
    <item>
      <title>Arc browser 한국어 번역 적용 방법</title>
      <link>https://beoks.tistory.com/entry/Arc-browser-%ED%95%9C%EA%B5%AD%EC%96%B4-%EB%B2%88%EC%97%AD-%EC%A0%81%EC%9A%A9-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Arc browser를 이용해 번역을 사용하려고 하면 영어로만 번역하려고 한다. 다음 절차를 통해 한국어로 번역하도록 수정 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 언어 설정 페이지 이동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;command + L 을 입력하고 arc://settings/lanugages 를 입력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfK7Lb/btsyMza2qYt/fXI9Hnftac6OzknPkXKkuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfK7Lb/btsyMza2qYt/fXI9Hnftac6OzknPkXKkuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfK7Lb/btsyMza2qYt/fXI9Hnftac6OzknPkXKkuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfK7Lb%2FbtsyMza2qYt%2FfXI9Hnftac6OzknPkXKkuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1998&quot; height=&quot;766&quot; data-origin-width=&quot;1998&quot; data-origin-height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 한국어 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Add languages 버튼 클릭 후 다음과 같이 한국어를 추가한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cC7w3x/btsyMqSXKmo/1gjrfBl9MxEFqFYllzZWFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cC7w3x/btsyMqSXKmo/1gjrfBl9MxEFqFYllzZWFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cC7w3x/btsyMqSXKmo/1gjrfBl9MxEFqFYllzZWFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcC7w3x%2FbtsyMqSXKmo%2F1gjrfBl9MxEFqFYllzZWFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1444&quot; height=&quot;592&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 번역 언어 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 한국어를 번역어로 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eoubtH/btsyMfDUO35/2rXXUN0vdlZQ2NcXa7MvX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eoubtH/btsyMfDUO35/2rXXUN0vdlZQ2NcXa7MvX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eoubtH/btsyMfDUO35/2rXXUN0vdlZQ2NcXa7MvX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeoubtH%2FbtsyMfDUO35%2F2rXXUN0vdlZQ2NcXa7MvX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;378&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 한국어로 번역대상이 설정된 경우 성공이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OyrrI/btsyLG2Q0DW/ZeTHnWgFS4KjrvjJBXytS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OyrrI/btsyLG2Q0DW/ZeTHnWgFS4KjrvjJBXytS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OyrrI/btsyLG2Q0DW/ZeTHnWgFS4KjrvjJBXytS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOyrrI%2FbtsyLG2Q0DW%2FZeTHnWgFS4KjrvjJBXytS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;824&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Etc</category>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/115</guid>
      <comments>https://beoks.tistory.com/entry/Arc-browser-%ED%95%9C%EA%B5%AD%EC%96%B4-%EB%B2%88%EC%97%AD-%EC%A0%81%EC%9A%A9-%EB%B0%A9%EB%B2%95#entry115comment</comments>
      <pubDate>Thu, 19 Oct 2023 23:40:44 +0900</pubDate>
    </item>
    <item>
      <title>Gradle - QueryDSL JPA 가장 간단하게 설정하기!</title>
      <link>https://beoks.tistory.com/entry/Gradle-QueryDSL-JPA-%EA%B0%80%EC%9E%A5-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&amp;nbsp;(Introduction)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL은 ORM을 사용하다 보면 언젠가는 사용하게 될 기술 중 하나입니다. 이를 설정하는 법을 구글링해보면 표준적인 설정 방법이 없기 때문에 각기 다른 방법을 제시하고 그만큼 설정에 에러를 많이 겪는 경우가 보입니다. 여기에서는 자바 버전에 따라 가장 간단하게 QueryDSL을 설정하는 방법을 알아보고, 각 설정의 의미에 대해서 서술해보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본문 (Body)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;간단한 QueryDSL 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정에 대해 알아보기전, 간단하게 QueryDSL의 원리에 대해 알면 설정을 이해하기 쉽습니다. 만약, 빠르게 설정만 하고 싶다면 아래 (QueryDSL 설정)를 바로 참고하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL은 &quot;쿼리를 안전하고 쉽게 하기 위한 도메인 특화언어(DSL, Domain Specific Language)&quot;입니다. 이를 위해선, 기존 자바코드를 바탕으로 JPA, MongoDB 같은 도메인에 맞는 언어를 생성하는 과정이 필요합니다. QueryDSL은 다음 과정을 통해 도메인 언어를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. compileJava 태스크를 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. @Entity와 같이 쿼리와 관련된 어노테이션과 클래스를 스캔합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 스캔한 클래스를 활용해 도메인에 맞는 언어를 생성합니다. (ex. Student.class -&amp;gt;&amp;nbsp; QStudent.class)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 개발자는 QueryDSL이 생성한 언어를 이용해 자바언어를 토대로 쿼리를 작성할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;QueryDSL 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL은 다음 코드를 build.gradle 파일에 추가해 설정할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1697272766364&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies{
    //Java EE를 사용하는 경우, (javax 패키지를 참조할 수 있는 경우)
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
    annotationProcessor 'javax.persistence:javax.persistence-api:2.2.0'

    
    //Jakarta EE를 사용하는 경우, (jakarta 패키지를 참조할 수 있는 경우)
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    
 }
 
 ...
 
 task deleteGenerated(type: Delete) {
    delete &quot;src/main/generated&quot;
}

clean.dependsOn deleteGenerated&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 이유에 대해서 설명해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 아래 의존성은 queryDSL에서 쿼리 기능을 구현하는데 필요한 클래스를 불러오기 위해 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jakarta 의 경우 기존에 javax.persistence.* 로 선언된 패키지들이 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;jakarta.persistence.*&lt;span&gt; 로 변경되었기 때문에, 올바른 참조를 위해 추가로 선언한 필요가 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697273385411&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'com.querydsl:querydsl-jpa:5.0.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;annotationProcessor는 Gradle에서 컴파일 타임에 어노테이션을 처리해 코드를 생성 또는 검증하는데 사용하기 위한 키워드입니다. APT는 Annotation Processing Tool의 약자입니다. 즉, querydsl에서 어노테이션 처리를 위한 기능을 가져오기 위해 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 또한, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;jakarta 의 경우 기존에 javax.persistence.* 로 선언된 패키지들이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;jakarta.persistence.*&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 변경되었기 때문에, 올바른 참조를 위해 추가로 선언한 필요가 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1697273480281&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Entity와 같은 기본 어노테이션들은 자바에서 표준적으로 정의되어 있습니다. 따라서 각 환경에 따라 필요한 정의를 annotationProcessor를 사용해 정의할 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1697273481886&quot; class=&quot;bash&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;annotationProcessor 'javax.persistence:javax.persistence-api:2.2.0'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;QueryDSL 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정이 올바르게 완료되면, compileJava 라는 태스크를 실행할 수 있습니다. ./gradlew compileJava 커맨드를 이용해 태스크를 실행하면, 위에서 설명한 DSL 생성과정이 진행됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2. @Entity와 같이 쿼리와 관련된 어노테이션과 클래스를 스캔합니다.&lt;br /&gt;3. 스캔한 클래스를 활용해 도메인에 맞는 언어를 생성합니다. (ex. Student.class -&amp;gt;&amp;nbsp; QStudent.class)&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 이제 queryDSL 문법에 따라 설정이 가능합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Gradle</category>
      <category>JPA</category>
      <category>QueryDSL</category>
      <category>QueryDsl 설정</category>
      <category>Spring</category>
      <author>BEOKS</author>
      <guid isPermaLink="true">https://beoks.tistory.com/114</guid>
      <comments>https://beoks.tistory.com/entry/Gradle-QueryDSL-JPA-%EA%B0%80%EC%9E%A5-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0#entry114comment</comments>
      <pubDate>Sat, 14 Oct 2023 18:03:07 +0900</pubDate>
    </item>
  </channel>
</rss>