<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>silica_o3o.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 04 Mar 2025 18:48:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>silica_o3o.log</title>
            <url>https://velog.velcdn.com/images/silica_o3o/profile/eb59d360-8f19-4533-b843-f3b7654ef861/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. silica_o3o.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/silica_o3o" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[SpringBoot] Elastic Search 도입]]></title>
            <link>https://velog.io/@silica_o3o/SpringBoot-Elastic-Search-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@silica_o3o/SpringBoot-Elastic-Search-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Tue, 04 Mar 2025 18:48:42 GMT</pubDate>
            <description><![CDATA[<p>사이드 프로젝트에서 게시물 검색을 할 수 있는 기능이 필요했다. 
관계형 DB로만 구현할지 ElasticSearch를 도입할지 고민을 많이 했었다.</p>
<p>일단 둘의 차이점부터 설명하겠습니다</p>
<h3 id="관계형-db-활용">관계형 DB 활용</h3>
<p>먼저 관계형 DB를 활용하는 방법입니다. 장점은 매우 명확하게 단순하게 구현을 할 수 있습니다. 하지만 단점도 존재합니다 </p>
<ul>
<li>제한된 검색 기능 : Like 연산자로 복잡한 검색 요구사항 충족하기 힘듬</li>
<li>성능 이슈 : 데이터가 많아질수록 검색 쿼리 성능이 떨어짐</li>
<li>확장성 제약 : 검색 기능 확장 시 노력 필요</li>
<li>한글 검색 한계 : 한글 형태소 분석 불가능</li>
</ul>
<p>이렇게 단점이 존재합니다. 제일 먼저 눈에 들어온 것은 제한된 검색 기능과 성능 이슈쪽이였습니다.
기술스택과 포지션 등으로 복잡한 검색이 많아질 시 요구사항을 완전히 충족하기 힘들다고 생각하였습니다. 
비록 지금 MVP 단계로 개발하고 있고 지금은 제목과 글 내용에 대한 검색만 하기에는 충분하지만, 확장성을 고려했을 때는 조금 기능이 떨어진다고 생각하였습니다. 
그래서 결국에는 ElasticSearch를 도입하기로 하였습니다.</p>
<h3 id="elasticsearch-활용">ElasticSearch 활용</h3>
<p>엘라스틱 서치의 장점을 간략하게 말해보겠습니다.</p>
<ul>
<li>고급 검색 기능 : 부분 일치, 유사 검색, 오타 교정 기능</li>
<li>검색 성능 : 인덱싱을 통한 최적화된 검색 성능</li>
<li>검색 품질 : 연관성 점수를 통한 더 정확한 검색 결과 제공</li>
<li>필터링과 정렬 : 다양한 조건으로 필터링</li>
</ul>
<p>위 4개의 장점이 지금 하고 있는 프로젝트에 꼭 필요한 부분들이라고 생각했습니다.
ElasticSearch 설정 및 기능 도입에 대해서도 설명하겠습니다.</p>
<h3 id="elasticsearch-어떻게-쓰는거야">ElasticSearch 어떻게 쓰는거야?</h3>
<p>먼저 저희 프로젝트는 Spring 3.3.x와 자바 17, Gradle을 사용하고 있습니다.</p>
<p>Gradle과 yml설정부터 해줍니다.</p>
<pre><code class="language-Gradle">implementation &#39;org.springframework.boot:spring-boot-starter-data-elasticsearch&#39;</code></pre>
<pre><code class="language-yml">spring:
  elasticsearch:
    uris: http://localhost:9200
    username: your_username
    password: your_password</code></pre>
<p>그런 다음 검색을 할 데이터에 관해서 Document를 만들어줘야합니다.</p>
<pre><code class="language-java">@Document(indexName = &quot;projects&quot;)
@NoArgsConstructor
@Data
public class ProjectDocument {
    @Id
    private Long id;

    @Field(type = FieldType.Text, analyzer = &quot;standard&quot;)
    private String title;

    @Field(type = FieldType.Text, analyzer = &quot;standard&quot;)
    private String content;
}</code></pre>
<p>그리고 엘라스틱 서치에 관한 Repository를 따로 만들어줘야합니다.</p>
<pre><code class="language-java">public interface SearchProjectRepository extends ElasticsearchRepository&lt;ProjectDocument, Long&gt;{
    List&lt;ProjectDocument&gt; findByTitleContainingOrContentContaining(String title, String content);
}</code></pre>
<p>보통은 JPA를 상속받지만 저희는 엘라스틱 서치를 사용하기 때문에 ElasticsearchRepository를 상속받아줍니다. 그리고 찾을 document를 가져와 줍니다.</p>
<p>검색을 하기위한 앤드포인트와 서비스를 만들어보겠습니다. 검색에 관한 키워드는 requestparam으로 넘겨줍니다</p>
<pre><code class="language-java">public class SearchController {


    private final SearchService searchService;

    @GetMapping(&quot;/search&quot;)
    public List&lt;SearchResultDto&gt; search(@RequestParam String query, @RequestParam(required = false) String type) {
        return searchService.search(query, type);
    }
}

public class SearchService {
    private final SearchProjectRepository searchProjectRepository;

    public List&lt;SearchResultDto&gt; search(String query, String type) {
        List&lt;SearchResultDto&gt; results = new ArrayList&lt;&gt;();

        if (type == null || type.equalsIgnoreCase(&quot;project&quot;)) {
            List&lt;ProjectDocument&gt; projects = searchProjectRepository.findByTitleContainingOrContentContaining(query, query);
            log.info(&quot;검색 쿼리 &#39;{}&#39; 결과: {} 개의 프로젝트 찾음&quot;, query, projects.size());

            results.addAll(projects.stream()
                    .map(SearchResultDto::fromProjectDocument)
                    .toList());
        }
        //todo pr 검색도 추가

        return results;
    }
}</code></pre>
<p>앤드포인트에서는 제일 중요한건 query이기에 필수로 받아줍니다. 얘네가 후에 제목과 내용중에서 해당되는 것을 찾아줍니다. type은 저희 프로젝트 특성상 어떤 글인지 타입을 나눈거라서 필수로 받아올 필요는 없습니당.</p>
<p>그리고 이제 service에서 결과를 list로 저장을 해서 뿌려줄겁니다.</p>
<p>게시글을 올릴 때마다 JPA는 저장이 되는데 엘라스틱 서치에도 동기화가 필요합니다. 그래야 검색을 할 수 있으니까... 그래서 메서드를 하나 추가해줄겁니다.</p>
<pre><code class="language-java">public void saveProject(Project project) {
        ProjectDocument projectDocument = ProjectDocument.fromProject(project);
        searchProjectRepository.save(projectDocument);
        log.info(&quot;프로젝트 ID {} Elasticsearch에 인덱싱 완료&quot;, project.getId());
    }</code></pre>
<p> 이 메서드를 생성, 업데이트되는 곳에 JPA에서 생성되거나 업데이트된 후 다시 세이브를 해줘야합니다. </p>
<p> 이렇게 하면 간단하게 엘라스틱 서치로 검색이 가능합니다!</p>
<p> <img src="https://velog.velcdn.com/images/silica_o3o/post/f86c8b6b-3a5f-456f-937f-0ac93339e5a1/image.png" alt=""></p>
<h3 id="트러블-슈팅">트러블 슈팅</h3>
<ol>
<li>repository 상속 문제</li>
</ol>
<p>elasticsearch를 도입하고 서버를 실행할 때 문제가 생겼습니다.</p>
<p>에러 로그</p>
<pre><code class="language-java">The bean &#39;projectRepository&#39;, defined in com.example.sideproject.domain.project.repository.ProjectRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration, could not be registered. A bean with that name has already been defined in com.example.sideproject.domain.project.repository.ProjectRepository defined in @EnableElasticsearchRepositories declared on ElasticsearchRepositoriesRegistrar.EnableElasticsearchRepositoriesConfiguration and overriding is disabled.</code></pre>
<p>원인은 JPARepository와 ElasticSearchRepository는 둘다 Repository 인터페이스를 상속받아 구현된 인터페이스입니다. 그래서 auto configuration을 통해 Spring 서버를 실행시킬 시 Spring은 Spring Data JPA를 실행시켜 repository 인터페이스를 상속받아 구현된 repository bean들을 전부 찾아 configure 하려고 하기 때문에 충돌이 나는겁니다.</p>
<p>해결방안은 패키지 분리를 하면 되긴하는데 레포지토리가 많으면 하나하나 다 해야한다고 하더라구요...</p>
<pre><code class="language-java">@EnableJpaRepositories(basePackages = {
        &quot;com.example.sideproject.domain.project.repository&quot;,
        &quot;com.example.sideproject.domain.user.repository&quot;,
       ....

})
@EnableElasticsearchRepositories(basePackages = &quot;com.example.sideproject.domain.search.repository&quot;)</code></pre>
<p>이걸 해결하니 다른 문제가 발생했습니다.</p>
<ol start="2">
<li>인덱싱 문제</li>
</ol>
<p>위에 한 것처럼 처음에 구현을 해놓고 신나게 api 테스트를 했지만 결과는 [] 빈 배열이였습니다.
왜일까 생각을 해보다가 데이터가 안들어갔구나라는 결론이 나왔습니다.
그래서 해결책으로는 서버가 먼저 켜질 때 데이터를 인덱싱하는 방법을 선택하였습니다.</p>
<p>ElasticsearchConfig를 하나 만들어줍니다.</p>
<pre><code class="language-java">public class ElasticsearchConfig {

    private final ProjectRepository projectRepository;
    private final SearchProjectRepository searchProjectRepository;

    @Bean
    //서버 실행될 때 JPA에서 인덱싱한걸 엘라스틱 서치에 저장
    public CommandLineRunner indexProjectsOnStartup() {
        return args -&gt; {
            try {
                long count = searchProjectRepository.count();
                if (count == 0) {
                    log.info(&quot;Elasticsearch에 인덱싱된 프로젝트가 없습니다. 초기 인덱싱을 시작합니다...&quot;);
                    List&lt;Project&gt; allProjects = projectRepository.findAll();
                    List&lt;ProjectDocument&gt; documents = allProjects.stream()
                                    .map(ProjectDocument::fromProject)
                            .toList();
                    searchProjectRepository.saveAll(documents);
                    log.info(&quot;초기 데이터 인덱싱 완료: {} 개의 프로젝트 인덱싱됨&quot;, allProjects.size());
                } else {
                    log.info(&quot;이미 {} 개의 프로젝트가 인덱싱되어 있습니다.&quot;, count);
                }
            } catch (Exception e) {
                log.error(&quot;초기 데이터 인덱싱 중 오류 발생: {}&quot;, e.getMessage(), e);
            }
        };
    }
}</code></pre>
<p>이렇게 해두면 스프링 서버가 부팅될 때 알아서 가져와줍니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시 결제 문제 해결]]></title>
            <link>https://velog.io/@silica_o3o/%EB%8F%99%EC%8B%9C-%EA%B2%B0%EC%A0%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@silica_o3o/%EB%8F%99%EC%8B%9C-%EA%B2%B0%EC%A0%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Sat, 22 Feb 2025 11:31:52 GMT</pubDate>
            <description><![CDATA[<p>현재 운영중인 서비스가 배포되기 QA를 하던 중에 결제 시스템에서 동시에 결제 요청을 할 시, 중복 결제가 되는 문제가 발생하였고 이 문제를 풀어보기로 했다. 
고민한 방법은 총 3가지이며 비즈니스 로직 변경, 트랜잭션 격리수준 향상, 데이터베이스 락 적용 이렇게 방안이 나왔다. 
실질적으로는 비즈니스 로직으로만 설정하였지만 다른 방법을 선택할 시 고민할 부분과 왜 비즈니스 로직으로만 설정했는지도 같이 풀어보려고 한다.</p>
<h3 id="첫번째-방법-비즈니스-로직-수정">첫번째 방법 &quot;비즈니스 로직 수정&quot;</h3>
<p>근본적인 문제를 가장 간단하게 해결할 수 있는 방법이 아닐까 생각한다.
결제 프로세스에 대해서 예외처리를 강하게 두는 방법이다. 기능 개발을 하면서 동시성을 고려하지 않았고 이 부분을 QA하면서 찾았었다. </p>
<p>서비스 로직에서도 고민했던 부분은 &quot;어떻게 하면 결제가 완료되는지 알 수 있을까&quot;였고 결제 상태를 주문번호로 가져오기로 했다  </p>
<p>먼저 이미 결제가 된 상품에 대해서는 주문번호로 payment테이블을 가져와 결제 상태를 가지고 이미 결제된 상품인지 판별한다.
그리고 payment테이블이 생성이 되면 article의 상태는 sold로 바뀌기 때문에 결제를 하기전에 article의 상태가 sold인지 판별하고 예외를 발생시키도록 추가해주었다.</p>
<pre><code class="language-python">async def update_order(
        request: CreateOrderRequest,
        usecase: OrderUseCase = Depends(Provide[Container.order_service]),
        toss_payment_service: TossPaymentService = Depends(Provide[Container.toss_payment_service]),
        alimtalk_sender: AlimtalkSenderPort = Depends(Provide[Container.alimtalk_service]),
):
    try:
        verified_payment = await usecase.verify_payment(request=request)
        if verified_payment:
            raise Exception(verified_payment)

        if (request.payment_type == PaymentType.NORMAL and
            request.payment_status == PaymentStatus.PAYMENT_COMPLETED and
            request.payment_key):
            response = await toss_payment_service.confirm_payment(
                payment_key=request.payment_key,
                order_id=request.origin_order_id,
                amount=request.amount
            )
            request = _handle_payment_response(request, response)

 ~~~
 @Transactional()
    async def verify_payment(self, *, request: &quot;CreateOrderRequest&quot;) -&gt; Optional[str]:
        payment = await self.repository.get_payment_by_origin_order_id(origin_order_id=request.origin_order_id, payment_status=request.payment_status)
        if payment:
            return &quot;이미 결제가 완료된 주문입니다.&quot;
        article = await self.article_repository.get_article_by_id(article_id=request.article_id)
        if article is None:
            return &quot;상품이 존재하지 않습니다.&quot;
        if article.article_status == ArticleStatus.SOLD:
            return &quot;이미 판매된 상품입니다.&quot;
        return None</code></pre>
<p>하지만 이 방법만으로는 동시성을 해결할 수 없다. 그 이유는 article의 상태가 sold로 바뀌기전에 다른 트랜잭션이 상태를 읽고 결제를 진행시킬 수 있기 때문이다. 그리고 결제는 실패되더라도 Order테이블은 만들어지고 Article에 매핑되는 Order가 업데이트 되기 때문에 실제로 결제한 사람은 구매내역에서 구매한 것을 보지 못하고 이후 실패한 사람의 구매내역에 구매했다고 뜨게된다. 
추가적으로 아티클의 상태는 결제 테이블을 만들 때가 아니라 먼저 Order테이블을 만들 때 검증을 해야한다고 깨달았다.</p>
<pre><code class="language-python"> @Transactional()
    async def create_order(self, *, command: CreateOrderCommand) -&gt; Optional[&quot;Order&quot;]:
        try:
            article = await self.article_repository.get_article_by_id(article_id=command.article_id)
            if article is None:
                raise Exception(&quot;상품이 존재하지 않습니다.&quot;)
            if article.article_status == ArticleStatus.SOLD:
                raise Exception(&quot;이미 판매된 상품입니다.&quot;)

            order = await self.repository.find_by_origin_order_id(origin_order_id=command.origin_order_id)

~~~
@Transactional()
    async def verify_payment(self, *, request: &quot;CreateOrderRequest&quot;) -&gt; Optional[str]:
        payment = await self.repository.get_payment_by_origin_order_id(origin_order_id=request.origin_order_id, payment_status=request.payment_status)
        order = await self.repository.get_order_by_origin_order_id(origin_order_id=request.origin_order_id)
        if payment:
            return &quot;이미 결제가 완료된 주문입니다.&quot;</code></pre>
<p> 이러한 방법도 완전한 방법은 아니다 그 이유는 아티클의 상태가 바뀌지 않는다면? 동시 결제가 이뤄지기 때문이다. 그래서 다른 방법을 찾아보았다.</p>
<h3 id="두번째-방법-트랜잭션-격리-수준-상향">두번째 방법 &quot;트랜잭션 격리 수준 상향&quot;</h3>
<p> 두번째 방법은 트랜잭션의 격리 수준을 상향하는 방법이다. 
 트랜잭션 격리 수준의 종류에 대해서는 아래 링크를 참고하였다.
 <a href="https://velog.io/@combi_jihoon/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-DB-Concurrency">트랜잭션 격리 수준</a></p>
<p> 현재 문제를 해결하기 위해서는 격리 수준을 최소 <strong>Repetable Read</strong> 이상이 필요하다는 것을 알았다. 하지만 이 방법을 사용하면 <strong>팬텀리드</strong>가 발생한다고 하여 더 높은 격리수준이 필요한가?라는 생각을 가지게 되었다. 이 다음으로 높은 격리수준은 Serializable으로 최상의 격리수준을 가진다. 하지만 내 서비스는 대규모 트래픽까지 고려해야했고 대규모 트래픽에서는 성능 저하로 부적합하다는 것을 깨달았다. Repetable Read에서 팬텀리드가 발생하기 때문에 당연히 최상의 격리수준을 설정해야한다고 생각했지만, 서비스 특성에 따라 적합성이 달라진다는 것을 깨달았다. 그러면 이것을 보완하기 위한 방법은 없을까?</p>
<h3 id="세번째-방법-데이터베이스-락">세번째 방법 &quot;데이터베이스 락&quot;</h3>
<p> 마지막으로는 데이터베이스에 락을 거는 것이다. 락의 종류는 크게 3가지로 있다.</p>
<ul>
<li><p>비관적 락</p>
</li>
<li><p>낙관적 락</p>
</li>
<li><p>분산 락</p>
<p>락의 설명또한 위의 링크를 참고하였다. 
트랜잭션의 격리수준을 보완하기 위해서는 비관적 락을 적용하는 것이 옳은 방법이라고 생각했고, 찾아보니 실무에서도 쓰이는 방법이라고 나와있었다. 하지만 알아볼수록 비관적락도 완전하지 않다는 것을 깨달았다. 그 이유는 바로 <strong>데드락</strong>때문이다. 대규모 트래픽 환경에서는 비관적 락을 적용했을 시, 락 대기 시간이 길어져 데드락의 위험이 있다는 것을 알게되었다. 이를 보완하기 위한 락 방법이 바로 Redis를 활용한 분산락과 큐이다. 분산락은 데이터베이스 부하를 줄이고 확장성을 확보하는 장점이 있고, 큐 기반 처리는 비동기 처리로 사용자에게 빠른 응답 제공이 가능하며 오버셀링이 방지되는 장점이 있다.
분산락과 큐도 단점이 있지만 이 부분은 다음 포스트에 작성해보려고 한다. </p>
<h3 id="왜-첫번째-방법을-적용했나">왜 첫번째 방법을 적용했나?</h3>
<p>일단 작은 스타트업이고 처음 목표로 했던 것이 한달의 천명 방문이였다. 현재 상황에서는 고도의 기술을 도입하면서 시간을 할애하는 것보다는 서비스의 전반적인 안정성이 중요했고 개발 인원도 적었고 더군다나 모바일 앱까지 만드는 상황이였기 때문에 환경이 좋지 않았다. 그래서 결국에는 첫번째 방법을 적용하면서 후에 고객응대 CS로 처리하자고 이야기가 매듭지어졌다. </p>
<h3 id="느낀점">느낀점</h3>
<p>이번에 동시성 문제를 해결하려고 많은 고민을하고 기술을 찾아보았지만, 완벽하게 해결되지 못한 점과 새로운 기술을 도입하지 못한 점이 아쉬움으로 남는다. 하지만 주어진 환경과 서비스의 크기에 따라서 기술 도입의 필요성을 체크할 필요가 있다는 것을 배우는 계기가 되었다. 후에는 서비스가 더 커져서 공부했던 부분을 적용해보는 날이 왔으면 좋을 것 같다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[React-Native 배너 만들기]]></title>
            <link>https://velog.io/@silica_o3o/React-Native-%EB%B0%B0%EB%84%88-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@silica_o3o/React-Native-%EB%B0%B0%EB%84%88-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 13 Feb 2025 03:22:21 GMT</pubDate>
            <description><![CDATA[<h2 id="요구사항">요구사항</h2>
<p>노쇼핑 앱 마이너 업데이트 중에서 앱 화면에 배너를 다는 요구사항이 들어왔었다. 
맨 위인 탭 바와 필터를 적용하는 곳 아래에 배너를 위치시켜야했고 홈 화면에서 스크롤을 올릴 시, 배너는 사라지게 만들어야하는 작업이다.</p>
<h2 id="배너-만들기">배너 만들기</h2>
<p>React-native에서 배너를 만들 때 swiper를 이용해서 만든다고 하여 swiper로 배너를 만들거다</p>
<pre><code class="language-npm">npm install react-native-swiper</code></pre>
<p>먼저 위 명령어로 swiper부터 추가를 해주자</p>
<p>다음으로는 배너 아이템과 배너 props를 구성합니다</p>
<pre><code class="language-typescript">interface BannerItem {
  id: string
  image: any
  title?: string
  link?: string
}

interface BannerProps {
  items: BannerItem[]
  autoplay?: boolean
  showPagination?: boolean
  loop?: boolean
  autoplayTimeout?: number
  onPressBanner?: (item: BannerItem) =&gt; void
  imageStyle?: object
}</code></pre>
<p>banneritem은 배너를 구성하고 있는 것들을 정해주면 됩니다! 
배너를 눌렀을 때 설정해둔 링크로 이동시키기 위해 handlePress를 만들고 swiper에 넣어보겠습니다</p>
<pre><code class="language-typescript">const handlePress = (item: BannerItem) =&gt; {
    if (onPressBanner) {
      onPressBanner(item)
    } else if (item.link) {
      Linking.openURL(item.link).catch((err) =&gt; {
        console.error(&quot;링크를 여는데 실패했습니다:&quot;, err)
      })
    }
  }

~~~~

&lt;Swiper
        className=&quot;h-full&quot;
        showsPagination={false}
        autoplay={autoplay}
        loop={loop}
        autoplayTimeout={autoplayTimeout}
        scrollEnabled={true}
        automaticallyAdjustContentInsets={false}
        showsHorizontalScrollIndicator={false}
        loadMinimal={true}
        loadMinimalSize={1}
        horizontal={true}
        pagingEnabled={true}
      &gt;
        {items.map((item) =&gt; (
          &lt;TouchableOpacity
            key={item.id}
            onPress={() =&gt; handlePress(item)}
            activeOpacity={0.9}
            style={{ flex: 1 }}
          &gt;
            &lt;Image
              source={typeof item.image === &quot;string&quot; ? { uri: item.image } : item.image}
              style={[
                {
                  width: screenWidth,
                  height: &quot;100%&quot;,
                },
                imageStyle,
              ]}
              resizeMode=&quot;contain&quot;
            /&gt;
          &lt;/TouchableOpacity&gt;</code></pre>
<p> 이렇게하면 배너에 있는 아이템을 터치했을 시 넣어둔 url링크로 이동하게 됩니다!</p>
<h3 id="properites">Properites</h3>
<ul>
<li>loop: 자식으로 내려주는 컴포넌트가 끝에 도달했을 때, 처음으로 다시 loop할지를 관여합니다. defalut값이 true여서 무한루프를 설정하시려면 false로 해주세요.</li>
<li>autoplay: 자동으로 컴포넌트들이 슬라이드가 가능하게 합니다. default가 false이기 때문에 배너 광고의 효과를 내기 위해서는 true를 주어야 합니다.</li>
<li>width, height: 설정을 해주지 않는다면 { flex: 1 } 값을 갖게 되어 화면을 꽉 채우니 본인의 배너 광고의 크기에 맞게 설정하시면 됩니다.</li>
<li>showsPagination: 페이징 처리를 보여주는 역할을 합니다. default가 true입니다.</li>
<li>autoplayTimeout: 자동으로 슬라이드 되는 시간 간격을 의미합니다. default값은 2.5초입니다.</li>
<li>scrollEnabled : 사용자의 스와이프 동작으로 슬라이드 전환 가능 여부</li>
</ul>
<h3 id="배너-적용">배너 적용</h3>
<p>배너를 보여줄 스크린 화면에 배너 아이템을 만들어 줍니다.</p>
<pre><code class="language-typescript">const bannerItems = [
    {
      id: &quot;1&quot;,
      image: image,
      link: &quot;link&quot;,
    },
    {
      id: &quot;2&quot;,
      image: image2,
      link: &quot;link&quot;,
    },
  ]</code></pre>
<p>그리고 이제 스크롤할 때 배너만 사라지게 해야하기 때문에 리스트 뷰에 ListHeaderComponent로 넣어주면 됩니다!</p>
<pre><code class="language-typescript">&lt;ListView
        ref={listViewRef}
        data={articles.slice()}
        refreshing={refreshing}
        estimatedItemSize={177}
        onRefresh={() =&gt;
          refresh({
            detail_category: selectedTab,
            sort_by: selectedSort.value,
            page: 1,
            should_replace: true,
          })
        }
        onEndReached={handleLoadMore}
        onEndReachedThreshold={0.5}
        ListHeaderComponent={() =&gt; (
          &lt;Banner items={bannerItems} autoplay={true} autoplayTimeout={4} loop={true} /&gt;
        )}</code></pre>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/1dcc6291-2e75-408f-b0a3-d12931ca8b43/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[토스 페이먼츠 트러블 슈팅]]></title>
            <link>https://velog.io/@silica_o3o/%ED%86%A0%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@silica_o3o/%ED%86%A0%EC%8A%A4-%ED%8E%98%EC%9D%B4%EB%A8%BC%EC%B8%A0-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Sat, 21 Dec 2024 14:37:45 GMT</pubDate>
            <description><![CDATA[<p>사실 트러블 슈팅이라고 하기엔 좀 어이없는? 트러블 슈팅이였지만 적어봅니다..</p>
<h1 id="토스페이먼츠-오류">토스페이먼츠 오류</h1>
<p>시뮬레이터로 결제 테스트를 하던 와중에 갑자기 결제가 제대로 되지 않은 문제가 발생했다. 분명히 잘 작동하던 로직들이였는데 무슨 문제인가 싶었고 로그를 찍어둔게 있어서 확인해봤다.</p>
<pre><code class="language-json">&quot;data&quot;: {
    &quot;message&quot;: &quot;주문 수정 중 오류가 발생했습니다: fromisoformat: argument must be str&quot;,
    &quot;strror&quot;
}</code></pre>
<p>결제완료 시점에 문제가 발생했는데 시간 포멧이 잘못된거 같았다... (뭐가 문제지..?) 에러 확인하고 좀 찾아봤는데 시간을 넣을 때 none으로 들어와서 로그를 다시 찍어봤더니 좀 황당했다..</p>
<pre><code>{
  &quot;mId&quot;: &quot;~~~~~~~&quot;,
  &quot;status&quot;: &quot;WAITING_FOR_DEPOSIT&quot;, 
  &quot;method&quot;: &quot;가상계좌&quot;,
  &quot;virtualAccount&quot;: {
    &quot;accountNumber&quot;: &quot;~~~~~~~~&quot;,
    &quot;dueDate&quot;: &quot;2024-12-28T22:17:45+09:00&quot;,
    &quot;expired&quot;: false
  }
}</code></pre><p>로그를 찍었을 때 토스쪽에서 받는 response를 보니까 status에서 문제를 알 수 있었다. 그 전에는 다른 결제 방식으로 결제를 했지만 가상 계좌로 결제를 하면 status처럼 결제 대기 상태를 받아오고 있지만 그 전에 코드에서는 </p>
<pre><code class="language-python">request.payment_status = PaymentStatus.PAYMENT_COMPLETED
    request.order_status = OrderStatus.ORDER_COMPLETED
    request.approved_at = _convert_to_kst(response[&#39;approvedAt&#39;])</code></pre>
<p>이런 식으로 결제가 완료되면 approved_at을 한국 시간으로 변경을 하고 있었지만, 가상 계좌는 결제 대기중이라 approved_at이 None으로 받아오고 있어서 난 에러였다. </p>
<p>찾아보니 토스에서 주는 status가 꽤 세분화되어 있었다</p>
<pre><code>토스페이먼츠 주요 상태값
READY: 결제 생성
PENDING: 결제 진행 중
WAITING_FOR_DEPOSIT: 가상계좌 입금 대기
SUCCESS: 결제 완료
EXPIRED: 가상계좌 입금 기한 만료
CANCELED: 결제 취소
PARTIAL_CANCELED: 부분 취소
ABORTED: 결제 중단
FAILED: 결제 실패</code></pre><p>WAITING_FOR_DEPOSIT: 가상계좌 입금 대기 이것때문에 오류가 난거라니 다행히 에러처리를 진작에 해서 좀 다행이긴하다. </p>
<p>위 코드를 가상계좌 결제인 상태일 때도 처리하기 위해서 조금 바꿨다.</p>
<pre><code class="language-python"># approvedAt이 None인 경우 처리 추가
    if response.get(&#39;approvedAt&#39;) is None:
        current_time = datetime.now().isoformat()
        request.approved_at = _convert_to_kst(current_time)
    else:
        request.approved_at = _convert_to_kst(response[&#39;approvedAt&#39;])</code></pre>
<p>이렇게 none일때랑 정상적으로 들어올 때 처리를 하면 해결완료!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Solapi를 사용하여 카카오 알림톡 개발]]></title>
            <link>https://velog.io/@silica_o3o/Solapi%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%95%8C%EB%A6%BC%ED%86%A1-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@silica_o3o/Solapi%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%95%8C%EB%A6%BC%ED%86%A1-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Sat, 21 Dec 2024 14:27:01 GMT</pubDate>
            <description><![CDATA[<h2 id="카카오-알림톡-도입이유">카카오 알림톡 도입이유</h2>
<p>회사에서 현재 프로젝트의 MVP를 세웠을 때 많은 부분들이 자동화가 빠져있고 CS적으로 처리하려고 했고, 이럴 때 사용하기 좋은 툴 같은 것은 없을까 생각했을 때 카카오 알림톡이라는 기능을 이야기했고 우리가 CS로 일일히 추적할 수 없는 부분을 카카오 알림톡이라는 것을 사용해서 추적가능하게 만들자라는 취지로 카카오 알림톡을 사용하기로 했다.</p>
<h2 id="카카오-알림톡-어떻게-쓰는거야">카카오 알림톡 어떻게 쓰는거야?</h2>
<p>카카오 알림톡은 카카오 비즈니스 채널에서 생성할 수 있다고 한다. 하지만 사내 프로젝트에서는 자동화의 니즈가 크기 때문에 반복적인 메시지가 많아 솔라피라는 것을 쓰게 되었다. </p>
<h2 id="solapi">Solapi</h2>
<p>solapi는 다양한 마케팅 채널을 관리하는 CRM도구라고 한다. 사용하면서 느꼈던 점인데 굉장히 쉽게(?) 구현이 가능하다. solapi자체에서도 각 코드마다 예제가 있기 때문에 학습하기 좀 편했다. 근데 초반에 좀 헷갈리는게 많아서 헤멨다고..ㅠㅠㅠ</p>
<pre><code class="language-python"># 변수가 있는 경우
            {
                &#39;to&#39;: &#39;01000000001&#39;,
                &#39;from&#39;: &#39;029302266&#39;,
                &#39;kakaoOptions&#39;: {
                    &#39;pfId&#39;: &#39;KA01PF200323182344986oTFz9CIabcx&#39;,
                    &#39;templateId&#39;: &#39;KA01TP200323182345741y9yF20aabcx&#39;,
                    # 변수: 값 형식으로 모든 변수에 대한 변수값 입력
                    &#39;variables&#39;: {
                        &#39;#{변수1}&#39;: &#39;변수1의 값&#39;,
                        &#39;#{변수2}&#39;: &#39;변수2의 값&#39;,
                        &#39;#{버튼링크1}&#39;: &#39;버튼링크1의 값&#39;,
                        &#39;#{버튼링크2}&#39;: &#39;버튼링크2의 값&#39;,
                        &#39;#{강조문구}&#39;: &#39;강조문구의 값&#39;
                    }
                }
            },
# 변수가 없는 경우
            {
                &#39;to&#39;: &#39;01000000001&#39;,
                &#39;from&#39;: &#39;029302266&#39;,
                &#39;kakaoOptions&#39;: {
                    &#39;pfId&#39;: &#39;KA01PF200323182344986oTFz9CIabcx&#39;,
                    &#39;templateId&#39;: &#39;KA01TP200323182345741y9yF20aabcx&#39;,
                    &#39;variables&#39;: {}  # 변수가 없는 경우에도 입력
                }
            },</code></pre>
<p>   위에 처럼 값들을 전부 처리해야한다 
   to는 수신자 번호이고 from은 발송자 번호이다
pfId는 카카오 알림톡채널 id이다.
templateId는 알림톡의 템플릿id다. 알맞게 값을 넣어주면 된다.
아래 변수쪽이 조금 헷갈렸다.</p>
<p>예제를 보면 왼쪽엔 변수이름 오른쪽엔 값이라고 되어있지만, 자리는 그다지 중요하지 않다. 변수명에는 #{변수명}이 처리만 해두면 왼쪽이든 오른쪽이든 알아서 잘 작동한다.
이후엔 알림톡 발송하기 위한 코드를 조금 알아보자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 순환 참조 해결하기]]></title>
            <link>https://velog.io/@silica_o3o/FastAPI-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@silica_o3o/FastAPI-%EC%88%9C%ED%99%98-%EC%B0%B8%EC%A1%B0-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 24 Nov 2024 17:05:45 GMT</pubDate>
            <description><![CDATA[<p>부트캠프 이후 정말 오랜만에 블로그를 써본다. 캠프 이후 운좋게 한달인턴 프로그램을 신청해서 뽑힌 후, 출근한지 이제 2달 반 넘어가는 시점동안 블로그에 내가 학습한 것들을 충분히 쓸 수 있었지만 미루고 미루다 보니 이제 처음 쓰게 된다
spring으로 시작했지만 어쩌보다보니 FastAPI를 하고 있는데 나름 재미있고 배운 것들도 많은 것 같다 서론은 여기까지하고 최근에 있었던 트러블 슈팅을 포스팅하려고 합니다!!</p>
<h3 id="순환-참조가-생긴-이유">순환 참조가 생긴 이유!!</h3>
<p>spring을 하면서도 본 적이 없는 오류였다. 이번에 참여하는 프로젝트에서 매핑 관계가 좀 얽혀있다 보니까 생긴 문제인 것 같다. 이번 프로젝트에서는 상위 테이블과 하위 테이블 개념이 있다. 
예를 들어서 artist라는 유저가 만들어지기 전에 기본적인 user라는 테이블이 만들어지면서 동시에 생성되는 구조라고 해보자
이럴 때 두 엔티티에는 각자의 클래스를 참조해야 하기 때문에 나는 처음에 서로 설정을 이렇게 했다.</p>
<pre><code class="language-python">user.py

if TYPE_CHECKING:
    from app.artist.domain.entity.artist import Artist

artist.py
if TYPE_CHECKING:
    from app.user.domain.entity.user import User</code></pre>
<p>엔티티 설계를 할 때 user는 artist의 객체를 가지고 있어야 하고, 반대로도 객체를 불러와야하는 설계를 했었다. </p>
<h3 id="해결-방안들">해결 방안들</h3>
<p>첫 번째로 해결 할 수 있는 방법은 TYPE_CHECKING을 사용하는 것이였다.</p>
<pre><code class="language-python">user.py

if TYPE_CHECKING:
    from app.artist.domain.entity.artist import Artist

artist.py
if TYPE_CHECKING:
    from app.user.domain.entity.user import User</code></pre>
<p>이 방법은 TYPE_CHECKING이 런타임때 실행되지 않고 타입 체크 시에만 사용되기 때문에 순환 참조 문제를 피할 수 있는 방법이다</p>
<p>두 번째로는 method에서 Pydantic 모델들에서 artist타입 힌트를 문자열로 변경시키는 방법도 있었다.</p>
<pre><code class="language-python">@classmethod
def create(
    cls,
    name: str,
    username: str,   
    password: str,
    user_role: UserRole
    service_product: &quot;Artist&quot;,  # 문자열로 변경
) -&gt; &quot;User&quot;:

class UserCreate(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)
    # ... 다른 필드들 ...
    artist: Optional[&quot;Artist&quot;] = Field(None, description=&quot;아티스트 객체&quot;)  # 문자열로 변경)</code></pre>
<p>세 번째는 from future import annotations를 사용하는 방법이다</p>
<pre><code class="language-python">from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, Field, ConfigDict
from sqlalchemy import BigInteger, String, TIMESTAMP, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.entity.base_entity import BaseEntity

class Artist(BaseEntity):
    __tablename__ = &quot;Artist&quot;

    artist_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
    user_id: Mapped[int] = mapped_column(BigInteger, ForeignKey(&quot;user.id&quot;), nullable=False)
    user: Mapped[User] = relationship(&quot;User&quot;, back_populates=&quot;user&quot;, uselist=False)
    nickname: Mapped[str] = mapped_column(String(50), nullable=False)</code></pre>
<p>여기서</p>
<pre><code class="language-python">from __future__ import annotations</code></pre>
<p>이 녀석의 하는 일이 TYPE_CHECKING을 사용해서 수동으로 문자열 타입 힌트를 할 필요가 없고 임포트 하나로 자동으로 처리해준다!! 근데 여기서 궁금증이 하나 생겼는데 객체가 필요한데 왜 문자열로 타입 힌트를 하는 것일까?</p>
<h3 id="순환-참조가-일어나는-과정">순환 참조가 일어나는 과정</h3>
<p>먼저 순환 참조가 일어나는 과정을 알 필요가 있다.</p>
<ul>
<li>Python이 user.py를 import 시작</li>
<li><code>user.py</code>에서 <code>from app.artist.domain.entity.artist import Artist</code>를 실행</li>
<li>user.py에서 Artist import 시작</li>
<li><code>artist.py</code>에서 <code>from app.user.domain.entity.user import User</code>를 실행</li>
<li><code>user.py</code>가 아직 완전히 로드되지 않은 상태(맨 처음이 아직 실행중)</li>
<li>서로가 서로를 필요로 하는 상황에서 순환참조 에러 발생</li>
</ul>
<p>이렇기 때문에 순환 참조가 발생한다. 근데 이 문제를 어떻게 타입 힌트를 문자열로 바꾸는 것만으로 해결이 되는걸까?</p>
<h3 id="왜-문자열-처리가-방법일까">왜 문자열 처리가 방법일까?</h3>
<pre><code class="language-python">user: Mapped[&quot;User&quot;] = relationship(&quot;User&quot;, back_populates=&quot;artist&quot;, uselist=False)</code></pre>
<ul>
<li>Python은 이 라인을 실행할 때 실제 User 클래스를 즉시 필요X</li>
<li>문자열 “User”은 나중에 SQLAlchemy가 실제로 관계를 설정할 때 평가 됨</li>
<li>그 시점에는 이미 모든 클래스가 로드되어 있어서 문제가 발생 X
→  즉 문자열 처리는 지연 평가(lazy evaluation)를 가능하게 해서 순환참조 문제를 피할 수 있음!</li>
</ul>
<p>굳이 스프링으로 비교하면 lazy loading이라고 볼 수 있을 것 같다.
그럼 파이썬 orm인 sqlalchemy에서는 문자열을 어떻게 처리하는지도 궁금해져서 찾아보았다</p>
<h3 id="orm에서-어떻게-다시-클래스로-평가될까">ORM에서 어떻게 다시 클래스로 평가될까?</h3>
<ul>
<li><p>SQLAlchemy의 relationship 동작 과정</p>
<ul>
<li>초기 클래스 정의 시<pre><code class="language-python">class Artist(BaseEntity):
user: Mapped[&quot;User&quot;] = relationship(&quot;User&quot;, back_populates=&quot;artist&quot;, uselist=False)</code></pre>
</li>
<li><blockquote>
<p>이 때는 &quot;User&quot;는 문자열</p>
</blockquote>
</li>
</ul>
</li>
<li><p>SQLAlchemy가 모든 모델을 초기화 할 때
  → <code>metadata.create_all()</code> 또는 첫 DB 세션 생성 시점에서 SQLAlchemy는 내부적으로 모든 선언된 모델들의 레지스트리를 가지고 있음
  → 이 레지스트리에서 &quot;User&quot; 문자열과 매칭되는 실제 클래스를 찾아서 연결    </p>
</li>
<li><p>실제 사용 시점</p>
<pre><code class="language-python"># 이런 코드 실행할 때
artist = session.query(Artist).first()
user = artist.user  # 여기서 실제 User 객체 반환</code></pre>
</li>
</ul>
<p>이렇게 사용이 된다. 조금 더 심연으로 가서 SQLAlchemy 내부 과정도 한번 봤다</p>
<pre><code class="language-python">class Registry:
    def __init__(self):
        self._models = {}  # 모든 모델 클래스 저장

    def register(self, model_class):
        self._models[model_class.__name__] = model_class

    def resolve_relationship(self, model_name: str):
        return self._models[model_name]  # 문자열 -&gt; 실제 클래스

# SQLAlchemy 내부에서
def setup_relationships():
    for model in registry._models.values():
        for relationship in model.__relationships__:
            if isinstance(relationship.target, str):
                # 문자열을 실제 클래스로 변환
                real_class = registry.resolve_relationship(relationship.target)
                relationship.target = real_class</code></pre>
<p>이렇게 실행이 되고 있는 것을 볼 수 있다.</p>
<ul>
<li>결론(SQLAlchemy에서)<ul>
<li>모든 모델 클래스들을 자신의 레지스트리에 등록</li>
<li>실제 DB 연결이나 쿼리 실행 전에 문자열로 된 관계들을 실제 클래스로 해석</li>
<li>이후 쿼리 실행 시 완전히 해석된 관계 정보를 사용</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python : string 타입을 datetime으로]]></title>
            <link>https://velog.io/@silica_o3o/Python-string-%ED%83%80%EC%9E%85%EC%9D%84-datetime%EC%9C%BC%EB%A1%9C</link>
            <guid>https://velog.io/@silica_o3o/Python-string-%ED%83%80%EC%9E%85%EC%9D%84-datetime%EC%9C%BC%EB%A1%9C</guid>
            <pubDate>Wed, 16 Oct 2024 10:01:19 GMT</pubDate>
            <description><![CDATA[<h2 id="datetime-파싱-에러-발생">datetime 파싱 에러 발생</h2>
<p>출근하기 전에, datetime 쪽 파싱 에러가 났다고 전달을 받았다</p>
<pre><code>TypeError: argument of type ‘NoneType’ is not iterable</code></pre><p>저장을 할 때 빈 값으로 들어가는 문제가 생겼고 출근하자마자 디버깅을 해보기로 했다.</p>
<h2 id="원인">원인</h2>
<p>프로그램 디버깅을 하기 전에, 크롤링하는 사이트에 들어가서 확인을 해봤다.
내가 만든 로직에서 사이트마다 게시물 시간을 &#39;년 전&#39;, &#39;일 전&#39; 이런식으로 되어 있어 이 기준으로 파싱을 하였다. 하지만, 내가 크롤링하는 사이트에서는 29일 전을 마지막으로 년-월-일 날짜가 찍혀있었고 이것은 우리가 파싱할 때 string 타입으로 가져오기 때문에 저장할 때 오류가 났었던거다</p>
<h2 id="conver-string-to-datetime">Conver String to datetime</h2>
<p>파이썬에서는 문자열을 날짜로 바꿔주는 작업은 strptime이라는 것을 사용하면 된다고 그랬다.</p>
<pre><code>datetime.strptime(post_time_str, &#39;%Y-%m-%d %H:%M:%S&#39;)</code></pre><p>post_time_str을 입력한  포맷에 맞춰 타입을 변경시켜준다.</p>
<h1 id="strptime-과-strftime">strptime 과 strftime</h1>
<ol>
<li>기본 기능</li>
</ol>
<ul>
<li>strftime: datetime 객체를 문자열로 변환 (Formatting)</li>
<li>strptime: 문자열을 datetime 객체로 변환 (Parsing)</li>
</ul>
<ol start="2">
<li>사용 방향:</li>
</ol>
<ul>
<li>strftime: datetime → 문자열</li>
<li>strptime: 문자열 → datetime</li>
</ul>
<ol start="3">
<li>메서드 소속</li>
</ol>
<ul>
<li>strftime: datetime 객체의 메서드</li>
<li>strptime: datetime 클래스의 메서드</li>
</ul>
<p>예제 </p>
<pre><code class="language-python">from datetime import datetime

# strftime 예제
now = datetime.now()
formatted_date = now.strftime(&quot;%Y-%m-%d %H:%M:%S&quot;)
print(formatted_date)  # 예: &#39;2024-10-16 14:30:00&#39;

# strptime 예제
date_string = &quot;2024-10-16 14:30:00&quot;
parsed_date = datetime.strptime(date_string, &quot;%Y-%m-%d %H:%M:%S&quot;)
print(parsed_date)  # 예: datetime(2024, 10, 16, 14, 30)</code></pre>
<ol start="4">
<li>파라미터:</li>
</ol>
<ul>
<li>strftime(format): format 문자열을 받아 그에 맞게 datetime을 포맷팅</li>
<li>strptime(date_string, format): 날짜 문자열과 그 문자열의 형식을 나타내는 format 문자열을 받음</li>
</ul>
<ol start="5">
<li>에러 처리:</li>
</ol>
<ul>
<li>strftime: 잘못된 형식 지정자를 사용하면 ValueError 발생</li>
<li>strptime: 문자열이 지정된 형식과 일치하지 않으면 ValueError 발생</li>
</ul>
<ol start="6">
<li>용도:</li>
</ol>
<ul>
<li>strftime: 날짜와 시간을 특정 형식의 문자열로 표시할 때 사용 (예: 로그 기록, 사용자 인터페이스 표시)</li>
<li>strptime: 문자열로 된 날짜와 시간을 파싱하여 datetime 객체로 변환할 때 사용 (예: 사용자 입력 처리, 데이터 파싱)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Batch Trouble Shoot2]]></title>
            <link>https://velog.io/@silica_o3o/Spring-Batch-Trouble-Shoot2</link>
            <guid>https://velog.io/@silica_o3o/Spring-Batch-Trouble-Shoot2</guid>
            <pubDate>Mon, 12 Aug 2024 14:08:21 GMT</pubDate>
            <description><![CDATA[<h2 id="trouble-shoot---휴면계정-활성화-오류">Trouble Shoot - 휴면계정 활성화 오류</h2>
<pre><code class="language-java">public void activateUser(EmailCheckRequestDto requestDto) {
        User user = userRepository.findByLoginIdAndEmailAndStatus(requestDto.getLoginId(),
            requestDto.getEmail(), UserStatusEnum.INACTIVE_USER).orElseThrow(()
                -&gt; new NotFoundException(USER_NOT_FOUND));

        mailSendService.CheckAuthNum(requestDto.getLoginId(),requestDto);
        verifyEmail(requestDto.getLoginId(), requestDto.getEmail());

        user.fromInactiveToActive();
        userRepository.save(user);
        redisUtil.deleteData(requestDto.getLoginId());
    }</code></pre>
<p> 해당 로직에서 save하기 전 user의 상태에는 휴면유저가 아닌 활성화 유저로 값이 들어오는 것을 확인했지만, DB는 바뀌지 않는 오류가 계속 생겼다. 
 <img src="https://velog.velcdn.com/images/silica_o3o/post/36dacfbc-e60c-4390-b096-9e8b19e90384/image.png" alt="">
<img src="https://velog.velcdn.com/images/silica_o3o/post/ec6e824c-ef4e-4c55-bbc2-d66feaa5dcf1/image.png" alt=""></p>
<p>디버그를 돌리면서 들어오는 값을 전부 검증했지만 문제가 없었기 때문에 참 의아했었지만, 의외로 엉뚱한 곳에서 문제가 생겼었다.</p>
<h2 id="오류-해결">오류 해결</h2>
<p>오류의 원인은 Batch Scheduler가 10초마다 돌고 있었고 Batch의 로직이 마지막 로그인 날짜를 기준으로 3일동안 접속하지 않았을 때 휴면유저로 바꾸고 있었다. </p>
<pre><code class="language-java"> @Bean
  @StepScope
  public JpaPagingItemReader&lt;User&gt; inactiveUserReader(
      @Value(&quot;#{jobParameters[&#39;timeLimitDays&#39;]}&quot;) Long timeLimitDays,
      @Value(&quot;#{jobParameters[&#39;currentDate&#39;]}&quot;) String currentDateStr) {

    // 기본 비활성 기준 일수 설정 (3일)
    if (timeLimitDays == null) {
      timeLimitDays = 3L;
    }

    // 현재 날짜 설정 (파라미터로 받거나 현재 시간 사용)
    LocalDateTime currentDate = currentDateStr != null ?
        LocalDateTime.parse(currentDateStr) : LocalDateTime.now();

    // 쿼리 파라미터 설정
    Map&lt;String, Object&gt; parameterValues = new HashMap&lt;&gt;();
    parameterValues.put(&quot;timeLimit&quot;, currentDate.minusDays(timeLimitDays));
    parameterValues.put(&quot;activeStatus&quot;, UserStatusEnum.ACTIVE_USER);

    // JPA를 사용하여 비활성 사용자를 페이징 방식으로 조회하는 reader 생성
    return new JpaPagingItemReaderBuilder&lt;User&gt;()
        .name(&quot;inactiveUserReader&quot;)
        .entityManagerFactory(entityManagerFactory)
        .queryString(&quot;SELECT u FROM User u WHERE u.lastLoginDate &lt; :timeLimit AND u.status = :activeStatus&quot;)
        .parameterValues(parameterValues)
        .pageSize(100)
        .build();
  }</code></pre>
<p>  이 부분에서 위의 정상 유저로 바뀌는 로직을 돌려도 로그인 날짜가 변하지 않기 때문에 Batch는 해당 이벤트를 인식하고 계속 휴면 유저로 바꾸고 있었다. 로직의 문제라고 보긴 어렵지만? 설마했던 부분이여서 찾기 어려웠다. 이러한 문제를 인식하고 활성화 로직에서 값을 set 해주는 부분에 localDate를 넣어 계정을 다시 활성화한 날짜로 바꿔줬다.</p>
<pre><code class="language-java">  public void fromInactiveToActive(){
        this.status = UserStatusEnum.ACTIVE_USER;
        this.lastLoginDate = LocalDateTime.now();
    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Batch Trouble Shoot]]></title>
            <link>https://velog.io/@silica_o3o/Spring-Batch-Trouble-Shoot</link>
            <guid>https://velog.io/@silica_o3o/Spring-Batch-Trouble-Shoot</guid>
            <pubDate>Fri, 09 Aug 2024 14:22:42 GMT</pubDate>
            <description><![CDATA[<h2 id="trouble-shoot---meta-table-자동-생성오류">Trouble Shoot - Meta Table 자동 생성오류</h2>
<pre><code class="language-yml">  batch:
    jdbc:
      initialize-schema: always</code></pre>
<p>yml 파일에 initialize-schema를 always로 해두면 Meta Table이 자동생성 되어야한다.</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/1209322c-c21a-4e93-a201-4160d2919eb5/image.png" alt=""></p>
<p>하지만 테이블을 찾지 못하는 오류가 발생해서 DB를 보면
<img src="https://velog.velcdn.com/images/silica_o3o/post/6a70a907-4d27-414b-a077-29dd1b52b6a9/image.png" alt=""></p>
<p>이렇게 생성이 되지 않는 것을 볼 수 있다.
이러한 문제를 찾아봤는데 Spring Batch 3.0.0 이후 버전에서 생기는 버그라고 많은 사람들이 이와 같은 문제를 겪고 있다는 것을 알 수 있었다.</p>
<h2 id="해결법은-무엇인가">해결법은 무엇인가?</h2>
<p>해결법은 간단하게 쿼리콘솔에 테이블을 생성하는 로직을 직접 실행시키면 된다. 
이건 인텔리제이 검색을 활용해서 batch를 실행시키는데 필요한 sql문을 직접 가져오면된다.
<img src="https://velog.velcdn.com/images/silica_o3o/post/158101ba-9c0c-45df-92f8-41c1ce5e8fcb/image.png" alt=""></p>
<p>이런식으로 찾아서 sql문을 따로 실행시키면 Meta Table이 완성된다.</p>
<p>하지만 이런 방법에는 문제가 있다.</p>
<p>바로 배포할 때 sql문을 계속 넣어줘야한다는 문제가 있다.
이 방법은 매우 귀찮기 때문에 DB를 하나 더 쓰고 자동적으로 채울 수 있게 하는 방법을 택했다.</p>
<h2 id="flyway-도입">Flyway 도입</h2>
<p>위의 문제 해결방법은 Flyway에서 찾을 수 있었다.
Flyway는 데이터 형성관리 툴로 따로 설정해둔 테이블만 관리를 해주는 툴이다.
이걸 통해서 Meta Table을 자동적으로 만들고 배포까지 실행시킬 수 있다!</p>
<pre><code class="language-yml">  flyway:
    enabled: true
    baseline-on-migrate: true
    locations: classpath:/db/migration/sql
    sql-migration-suffixes: sql
    baseline-version: 1</code></pre>
<p>yml 파일에 flyway를 추가해주고 <img src="https://velog.velcdn.com/images/silica_o3o/post/ce03895b-adfa-4719-a19a-593ac8e2ff22/image.png" alt=""></p>
<p>resources 파일 안에 따로 sql 파일을 담는 폴더도 만들어 설정했다. 
Flyway 설정을 마치고 돌려보면<img src="https://velog.velcdn.com/images/silica_o3o/post/e6e9ca12-2bd9-4b77-9d4b-5f204f9eaa51/image.png" alt=""></p>
<p>batch에 관한 테이블이 생겨났고 flyway스키마를 들어가보면
<img src="https://velog.velcdn.com/images/silica_o3o/post/d74f080d-9ff9-4d7f-9c44-dab32f69c37d/image.png" alt="">
우리가 따로 만들어둔 sql문을 관리하는 것을 볼 수 있다.</p>
<p>사실 Flyway를 쓰고 바로 해결된 문제는 아니였다. Flyway가 인식이 안되는 문제가 발생했는데 이 문제의 원인은</p>
<pre><code class="language-yml">  flyway:
    enabled: true
    baseline-on-migrate: true
    locations: classpath:/db/migration/sql
    sql-migration-suffixes: sql
    baseline-version: 1</code></pre>
<p>경로 설정해두는 곳에서 오타가 났었기 때문이다..</p>
<pre><code class="language-yml">  flyway:
    enabled: true
    baseline-on-migrate: true
    locations: classpath:db/migration/sql
    sql-migration-suffixes: sql
    baseline-version: 1</code></pre>
<p>이런식으로 classpath:db로 되어있어서 / 하나 빠져있는걸 발견을 바로 하지 못해서 무슨 문제인지 하면서 10분을 더 할애했다.. 개발을 할 때 오타 하나 때문에 시간을 많이 잡아먹는다고 하였는데 나도 겪게 될 줄이야..ㅠㅠㅠ </p>
<p>이렇게 Batch까지 사용하면서 사용해본 기능들이 하나 둘 늘고 있다~!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Batch]]></title>
            <link>https://velog.io/@silica_o3o/Spring-Batch</link>
            <guid>https://velog.io/@silica_o3o/Spring-Batch</guid>
            <pubDate>Tue, 06 Aug 2024 14:52:51 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-batch">Spring Batch</h2>
<p>스프링 배치(Spring Batch)는 대용량 데이터 처리 작업을 쉽게 관리하고 실행할 수 있게 해주는 스프링 프레임워크의 모듈입니다. 주로 대량의 데이터 처리, 배치 작업 및 일괄 작업을 자동화하고 효율적으로 처리하는 데 사용됩니다.</p>
<h2 id="구성-요소">구성 요소</h2>
<ul>
<li><p>Job: 배치 작업의 최상위 컨테이너. 하나 이상의 Step으로 구성됩니다.</p>
</li>
<li><p>Step: 독립적으로 실행 가능한 배치 작업의 단위. 통상적으로 ItemReader, ItemProcessor, - ItemWriter로 구성됩니다.</p>
</li>
<li><p>ItemReader: 데이터를 읽어오는 역할을 합니다.</p>
</li>
<li><p>ItemProcessor: 읽어온 데이터를 가공하는 역할을 합니다.</p>
</li>
<li><p>ItemWriter: 가공된 데이터를 저장하는 역할을 합니다.</p>
</li>
</ul>
<h2 id="예제">예제</h2>
<ul>
<li>간단한 콘솔 출력 배치 작업</li>
</ul>
<pre><code class="language-java">import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

    @Bean
    public Job job(JobBuilderFactory jobBuilderFactory, Step step) {
        return jobBuilderFactory.get(&quot;job&quot;)
                .incrementer(new RunIdIncrementer())
                .listener(listener())
                .flow(step)
                .end()
                .build();
    }

    @Bean
    public Step step(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory.get(&quot;step&quot;)
                .tasklet(tasklet())
                .build();
    }

    @Bean
    public Tasklet tasklet() {
        return (contribution, chunkContext) -&gt; {
            System.out.println(&quot;Hello, Spring Batch!&quot;);
            return RepeatStatus.FINISHED;
        };
    }

    @Bean
    public JobExecutionListener listener() {
        return new JobExecutionListenerSupport() {};
    }
}</code></pre>
<ul>
<li>데이터베이스 읽고 처리 후 다른 데이터베이스에 쓰기</li>
</ul>
<pre><code class="language-java">import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
@EnableBatchProcessing
public class DatabaseBatchConfiguration {

    @Bean
    public JdbcCursorItemReader&lt;Person&gt; reader(DataSource dataSource) {
        return new JdbcCursorItemReaderBuilder&lt;Person&gt;()
                .dataSource(dataSource)
                .name(&quot;personItemReader&quot;)
                .sql(&quot;SELECT first_name, last_name FROM people&quot;)
                .rowMapper((rs, rowNum) -&gt; new Person(rs.getString(&quot;first_name&quot;), rs.getString(&quot;last_name&quot;)))
                .build();
    }

    @Bean
    public ItemProcessor&lt;Person, Person&gt; processor() {
        return person -&gt; {
            person.setFirstName(person.getFirstName().toUpperCase());
            person.setLastName(person.getLastName().toUpperCase());
            return person;
        };
    }

    @Bean
    public JdbcBatchItemWriter&lt;Person&gt; writer(DataSource dataSource) {
        return new</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿼리 최적화]]></title>
            <link>https://velog.io/@silica_o3o/%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@silica_o3o/%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Mon, 05 Aug 2024 13:10:42 GMT</pubDate>
            <description><![CDATA[<h2 id="쿼리-최적화">쿼리 최적화</h2>
<p>쿼리 최적화는 데이터베이스 쿼리의 성능을 향상시키는 과정입니다. 주요 목표는 쿼리 실행 시간을 단축하고 리소스 사용을 줄이는 것입니다. </p>
<h2 id="핵심-요소">핵심 요소</h2>
<ul>
<li><p>인덱스 사용: 적절한 인덱스를 생성하여 데이터 검색 속도를 높입니다.</p>
</li>
<li><p>실행 계획 분석: 데이터베이스의 쿼리 실행 계획을 검토하여 비효율적인 부분을 식별합니다.</p>
</li>
<li><p>쿼리 재작성: 복잡한 쿼리를 더 효율적인 형태로 재구성합니다.</p>
</li>
<li><p>조인 최적화: 테이블 간 조인 순서와 방식을 최적화합니다.</p>
</li>
<li><p>서브쿼리 최적화: 서브쿼리를 조인으로 변환하거나 더 효율적인 형태로 수정합니다.</p>
</li>
<li><p>데이터 분할: 대규모 테이블을 smaller 파티션으로 나누어 관리합니다.</p>
</li>
<li><p>캐싱: 자주 사용되는 쿼리 결과를 캐시하여 반복적인 계산을 줄입니다.</p>
</li>
<li><p>통계 정보 최신화: 데이터베이스의 통계 정보를 주기적으로 업데이트하여 옵티마이저가 더 나은 실행 계획을 수립할 수 있도록 합니다.</p>
</li>
</ul>
<h2 id="사용법">사용법</h2>
<ol>
<li>인덱스 활용</li>
</ol>
<ul>
<li>자주 검색되는 컬럼에 인덱스를 생성</li>
</ul>
<pre><code class="language-SQL">CREATE INDEX idx_lastname ON employees(last_name);</code></pre>
<ol start="2">
<li>EXPLAIN 명령어 사용</li>
</ol>
<ul>
<li>쿼리 실행 계획을 분석하여 비효율적인 부분을 찾음<pre><code class="language-SQL">EXPLAIN SELECT * FROM employees WHERE salary &gt; 50000;</code></pre>
</li>
</ul>
<p>3.불필요한 DISTINCT와 서브쿼리 제거</p>
<ul>
<li>DISTINCT나 서브쿼리는 때때로 성능을 저하시킬 수 있습니다. 가능하면 JOIN으로 대체하거나 제거<pre><code class="language-SQL">SELECT DISTINCT department_id 
FROM employees 
WHERE salary &gt; (SELECT AVG(salary) FROM employees);</code></pre>
해당 쿼리를 다음과 같이 변경 가능<pre><code class="language-SQL">SELECT department_id 
FROM employees e
JOIN (SELECT AVG(salary) as avg_salary FROM employees) a
WHERE e.salary &gt; a.avg_salary
GROUP BY department_id;</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[암호화된 비밀번호 비교하기]]></title>
            <link>https://velog.io/@silica_o3o/%EC%95%94%ED%98%B8%ED%99%94%EB%90%9C-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@silica_o3o/%EC%95%94%ED%98%B8%ED%99%94%EB%90%9C-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 01 Aug 2024 14:56:56 GMT</pubDate>
            <description><![CDATA[<h2 id="암호화된-비밀번호-비교">암호화된 비밀번호 비교</h2>
<p>암호화된 비밀번호를 비교하기 위해서는 passwordEncoder.matches() 메서드를 사용해야 합니다.
해당 메서드는 Spring Security에서 제공하는 PasswordEncoder 인터페이스의 일부입니다.</p>
<h2 id="사용-방법">사용 방법</h2>
<pre><code class="language-java">boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);</code></pre>
<ul>
<li><p>rawPassword는 사용자가 입력한 평문 비밀번호입니다.</p>
</li>
<li><p>encodedPassword는 데이터베이스에 저장된 암호화된 비밀번호입니다.</p>
</li>
<li><p>반환값은 두 비밀번호가 일치하면 true, 일치하지 않으면 false입니다.</p>
</li>
</ul>
<h2 id="사용-예시">사용 예시</h2>
<pre><code class="language-java">public SignUpResponseDto signUp(SignUpRequestDto requestDto) {
        String id = requestDto.getId();
        String password = requestDto.getPassword();
        String checkPassowrd = passwordEncoder.encode(requestDto.getCheckPassword());
        String name = requestDto.getName();
        String email = requestDto.getEmail();
        String nickname = requestDto.getNickname();
        String profile = requestDto.getProfileImage();

        //ID 중복확인
        duplicatedId(id);

        //닉네임 중복확인
        duplicatedNickName(nickname);

        //비밀번호 재입력 및 확인
        checkPassword(password, checkPassowrd);

        String encodePassword = passwordEncoder.encode(password);</code></pre>
<pre><code class="language-java">private void checkPassword(String password, String checkPassword){
      if(!passwordEncoder.matches(password, checkPassword)){
        throw new MismatchException(ErrorType.MISMATCH_PASSWORD);
      }
  }</code></pre>
<p>  checkPassword 메서드에서 password를 평문, checkPassword는 암호화된 값으로 받아와서 사용한 뒤, 다시 평문인 password를 암호화시키면 통과~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회원가입할 때 이메일 인증 구현]]></title>
            <link>https://velog.io/@silica_o3o/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%ED%95%A0-%EB%95%8C-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@silica_o3o/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%ED%95%A0-%EB%95%8C-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 30 Jul 2024 16:07:55 GMT</pubDate>
            <description><![CDATA[<h2 id="이메일-인증-구현-모식도">이메일 인증 구현 모식도</h2>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/b4592187-0695-4a31-ac62-18c9ce7076fe/image.png" alt=""></p>
<p>회원가입을 할 때, 이메일 인증 확인이 된 사람만 회원가입을 완료할 수 있게 코드를 짜고 싶었다.
그렇게 하기 위해서는 먼저 회원가입 창에서 이메일 인증 전 상태와 인증 확인이 된 상태를 나눌 필요가 있었고, 그 정보를 저장해둘 데이터베이스도 필요했다. 
인증 상태를 나누기 위해서 MailAuth라는 Entity를 따로 구현했고, 잠시 동안 정보를 담고 있을 DB는 Redis를 사용하기로 했다.</p>
<h2 id="구현한-코드">구현한 코드</h2>
<p>회원가입을 하는 도중, 이메일 인증 했는지 알기 위한 절차에서 필요한 테이블은 다음과 같이 만들었다.
<img src="https://velog.velcdn.com/images/silica_o3o/post/d79ea46a-3b28-4979-bdca-f264875cd589/image.png" alt=""></p>
<p>이메일 인증이 되지 않은 회원 상태를 NON_AUTH_USER라는 유저 상태를 Enum으로 관리를 하였고 
인증번호 검증이 완료되면 상태를 바꾸는 로직을 생각했다. </p>
<p>Redis에서 키 값을 loginId로 두었고 value는 MailAuth 객체로 받아오기 위해서는 Value에 객체가 들어오게끔 코드를 짜야했고 아래의 코드가 그 코드이다.</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/5adf461e-b9fb-4720-936d-c4800b04c03a/image.png" alt=""></p>
<p>이렇게 받아오기 위해서는 Config를 따로 설정도 해뒀어야했다. 
<img src="https://velog.velcdn.com/images/silica_o3o/post/f2c8e87d-ad83-4568-8086-325087586d64/image.png" alt=""></p>
<p>이렇게 준비는 다 끝났다. </p>
<h2 id="트러블-슈팅">트러블 슈팅</h2>
<p>가장 큰 트러블 슈팅은 Key에 해당하는 Value의 값이 들어오지 않는 점이였다. 
<img src="https://velog.velcdn.com/images/silica_o3o/post/a161ef6e-d688-42ca-b741-6186ff58a249/image.png" alt=""></p>
<p>이메일 검증 로직을 거치게끔 해서 받은 인증번호와 입력한 인증번호가 같은지 판별하는 곳에서 문제가 생겼다. 에러 메세지를 보면 mailAuth의 값이 null이 찍히면서 NPE가 발생하였다. 
처음에는 Redis가 제대로 연결이 되어있지 않은지 의심을 했고 그거부터 확인을 했었다. 
<img src="https://velog.velcdn.com/images/silica_o3o/post/4bcdace0-f333-4920-bfac-940e3adf0807/image.png" alt=""></p>
<p>Redis-cli에서 연결을 확인했지만 잘 연결이 되어있었고, 다른 문제를 생각한건 Key에 해당하는 Value값이 제대로 들어가있지 않은건 아닌가 의심했었다.</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/b67e8e26-9d52-45a5-9bfc-58ad6065909b/image.png" alt=""></p>
<p>하지만 예상과 다르게 너무나 잘 들어와있는 모습이 보여서 더욱 더 멘탈이 흔들렸다. 
문제점은 굉장히 쉬운 곳에서 로직 실수가 있었다.
<img src="https://velog.velcdn.com/images/silica_o3o/post/26d125b2-fa69-4a45-ba34-cda940f92d52/image.png" alt=""></p>
<p>분명 나는 Key 값을 loginId로 받아오기로 했었고 위 코드의 로그인 id는 aespa2인것을 확인할 수 있다. 하지만 지금 디버그를 하면서 찾은 loginId는 이메일이 들어오고 있었다.</p>
<p>어디가 잘못되었을까 확인하면서 Controller를 다시 확인하였다. </p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/ba0db60a-b3b8-4aa2-88fb-cbc834d11f19/image.png" alt=""></p>
<p>인증번호 확인하는 로직을 호출하는 곳에서 CheckAuthNum에서의 Key 값을 email로 받아오고 있었다. 그래서 value가 null로 계속 에러가 떴었던 것이다. 
<img src="https://velog.velcdn.com/images/silica_o3o/post/4887b382-5867-46fe-b22b-7c4ece4600be/image.png" alt=""></p>
<p>수정을 다시 하였고 실행을 시키면 원래 생각한대로 로직이 작동하는지 마무리 검증을 하면 된다. 
<img src="https://velog.velcdn.com/images/silica_o3o/post/4d19e709-fdbf-4c80-afd4-a7908e3b4d00/image.png" alt=""></p>
<p>Postman으로 이메일 인증을 요청하였고, 검증 로직을 타서 확인을 하면 끝난다.</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/380f525f-48ad-4929-b0e6-74af4df13688/image.png" alt=""></p>
<p>로그인id와 이메일 인증번호를 입력하고 포스트맨을 실행시키면서 디버그를 확인해보면</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/8ccb0fc8-73cd-465e-baf6-42e859d47f64/image.png" alt=""></p>
<p>value에 해당 값이 제대로 확인되는 것을 볼 수 있다. 그러면 이제 끝이다!!</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/a013edb4-69f0-4b20-bb74-3b84c811bab3/image.png" alt=""></p>
<p>엥???????</p>
<p>value값과 request로 입력받는 값의 형태가 다른지 확인을 해서 달라서 생기는 예외인지 확인할 필요가 생겼다 
<img src="https://velog.velcdn.com/images/silica_o3o/post/70845250-6c3d-4886-a11f-e55f3f8a6c2e/image.png" alt=""></p>
<p>하지만 두 값은 String으로 타입이 같이 들어온다... </p>
<p>문제점은 정말 간단했다.</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/2bb07dc6-5fd3-49a3-87fa-9a906204d15b/image.png" alt=""></p>
<p>해당 로직에서 Redis에 저장된 인증번호와 입력한 인증번호가 다르면 예외를 던져야하지만
지금 로직은 같으면 예외를 던지는 로직을 타게 되어있어서 조건식에 !만 붙혀주면 해결되는 문제다.</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/38472e19-89d2-4a01-b9e7-ccd22c243854/image.png" alt=""></p>
<p>인증은 성공되었으니 해당 이메일로 회원가입이 되는지도 확인해봐야한다.</p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/9b4ea273-01f0-409e-900c-2cca65d7bfc1/image.png" alt="">
회원가입에 성공하였고, </p>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/88dcf2a7-43e3-4130-a419-c73c0d8f061b/image.png" alt="">
로그인도 문제없이 성공하였다!! </p>
<p>오늘로 깨달은 것은 디버깅이 정말정말 중요하고 디버깅으로 모든 문제를 찾을 수 있다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[응집도(Cohesion)]]></title>
            <link>https://velog.io/@silica_o3o/%EC%9D%91%EC%A7%91%EB%8F%84Cohesion</link>
            <guid>https://velog.io/@silica_o3o/%EC%9D%91%EC%A7%91%EB%8F%84Cohesion</guid>
            <pubDate>Fri, 26 Jul 2024 12:11:50 GMT</pubDate>
            <description><![CDATA[<h2 id="응집도">응집도</h2>
<ul>
<li>모듈 내부의 요소들이 서로 관련되어 있는 정도</li>
<li>하나의 모듈이 단일 목적 또는 책임을 얼마나 잘 수행하는지를 나타냄</li>
</ul>
<h2 id="중요성">중요성</h2>
<ul>
<li>높은 응집도는 모듈의 독립성과 재사용성을 향상시킴</li>
<li>유지보수성과 이해도를 높임</li>
<li>변경의 영향을 최소화하고 테스트를 용이하게 함</li>
</ul>
<h2 id="응집도의-유형낮은-순에서-높은-순">응집도의 유형(낮은 순에서 높은 순)</h2>
<ul>
<li><p>우연적 응집도</p>
<ul>
<li>모듈 내 요소들 사이에 아무런 의미 있는 관계가 없음</li>
</ul>
</li>
<li><p>논리적 응집도</p>
<ul>
<li>논리적으로 관련된 기능들을 모아놓은 경우</li>
</ul>
</li>
<li><p>시간적 응집도</p>
<ul>
<li>특정 시점에 함께 실행되는 기능들을 모아놓은 경우</li>
</ul>
</li>
<li><p>절차적 응집도</p>
<ul>
<li>특정 수행 순서로 관련된 요소들을 모아놓은 경우</li>
</ul>
</li>
<li><p>통신적 응집도 </p>
<ul>
<li>동일한 입력과 출력을 사용하는 요소들을 모아놓은 경우</li>
</ul>
</li>
<li><p>순차적 응집도</p>
<ul>
<li>한 요소의 출력이 다른 요소의 입력으로 사용되는 경우</li>
</ul>
</li>
<li><p>기능적 응집도 </p>
<ul>
<li>모듈의 모든 요소가 단일 잘 정의된 목적을 위해 작동하는 경우</li>
<li>가장 높은 수준의 응집도</li>
</ul>
</li>
</ul>
<h2 id="높은-응집도를-위한-설계-원칙">높은 응집도를 위한 설계 원칙</h2>
<ul>
<li>단일 책임 원칙 준수</li>
<li>관련 기능을 그룹화</li>
<li>불필요한 기능 제거 또는 분리</li>
<li>인터페이스 분리 원칙 적용</li>
</ul>
<h2 id="응집도와-결합도의-관계">응집도와 결합도의 관계</h2>
<ul>
<li>일반적으로 높은 응집도는 낮은 결합도로 이어짐</li>
<li>응집도가 높은 모듈은 다른 모듈과의 의존성이 적어짐</li>
</ul>
<h2 id="높은-응집도를-만들기-위해선-어떻게-해야할까">높은 응집도를 만들기 위해선 어떻게 해야할까?</h2>
<ul>
<li>지속적인 리팩토링과 설계 개선이 필요</li>
<li>모듈의 목적을 명확히 정의해야함</li>
<li>관련 없는 기능은 분리</li>
<li>단일 책임 원칙을 준수하는 것이 중요</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[RestTemPlate vs RestClient]]></title>
            <link>https://velog.io/@silica_o3o/RestTemPlate-vs-RestClient</link>
            <guid>https://velog.io/@silica_o3o/RestTemPlate-vs-RestClient</guid>
            <pubDate>Thu, 25 Jul 2024 12:28:30 GMT</pubDate>
            <description><![CDATA[<h2 id="resttemplate">RestTemPlate</h2>
<p> Spring Framework에서 제공하는 HTTP 클라이언트 라이브러리다. RESTful 웹 서비스를 호출하는 데 사용된다.</p>
<h2 id="주요-특징">주요 특징</h2>
<ul>
<li><p>동기식 작동: RestTemplate은 기본적으로 동기 방식으로 작동</p>
</li>
<li><p>다양한 HTTP 메서드 지원: GET, POST, PUT, DELETE 등 모든 주요 HTTP 메서드를 지원</p>
</li>
<li><p>요청/응답 변환: JSON, XML 등 다양한 형식의 데이터를 자동으로 객체로 변환가능</p>
</li>
<li><p>에러 처리: ResponseErrorHandler를 통해 HTTP 에러를 처리할 수 있음</p>
</li>
<li><p>인터셉터 지원: ClientHttpRequestInterceptor를 사용하여 요청/응답을 가로채고 수정할 수 있음</p>
</li>
<li><p>URI 템플릿 지원: URL에 변수를 쉽게 삽입할 수 있음.</p>
<h2 id="주요-메서드">주요 메서드</h2>
</li>
</ul>
<ul>
<li>getForObject(): GET 요청을 보내고 응답을 객체로 반환</li>
<li>getForEntity(): GET 요청을 보내고 ResponseEntity를 반환</li>
<li>postForObject(): POST 요청을 보내고 응답을 객체로 반환</li>
<li>postForEntity(): POST 요청을 보내고 ResponseEntity를 반환</li>
<li>put(): PUT 요청을 보냄</li>
<li>delete(): DELETE 요청을 보냄</li>
<li>exchange(): 모든 HTTP 메서드에 대해 사용할 수 있는 일반적인 메서드</li>
</ul>
<h2 id="코드-예제">코드 예제</h2>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/295efbc2-f2be-46a2-8cb6-fa42204928ae/image.png" alt=""></p>
<h2 id="장점">장점</h2>
<ul>
<li>사용하기 쉽고 직관적</li>
<li>Spring 프레임워크와 잘 통합됨</li>
<li>다양한 HTTP 클라이언트 라이브러리를 추상화 함</li>
</ul>
<h2 id="단점">단점</h2>
<ul>
<li>비동기 작업을 기본적으로 지원하지 않음</li>
<li>테스트하기가 상대적으로 어려움</li>
<li>최신 버전의 Spring에서는 유지보수 모드로 전환</li>
</ul>
<h2 id="restclient">RestClient</h2>
<p> Spring Framework 6.1에서 새롭게 도입된 HTTP 클라이언트 라이브러리입니다. RestTemplate의 후속 버전으로, 더 현대적이고 유연한 API를 제공</p>
<h2 id="주요-특징-1">주요 특징</h2>
<ul>
<li>동기 및 비동기 지원: 동기 및 비동기 작업을 모두 지원</li>
<li>유연한 API: 메서드 체이닝을 통해 더 읽기 쉽고 유연한 API를 제공</li>
<li>향상된 에러 처리: onStatus() 메서드를 통해 세밀한 에러 처리가 가능</li>
<li>URI 템플릿: URI 변수를 더 쉽게 처리할 수 있음</li>
<li>응답 처리: ResponseSpec을 통해 다양한 응답 처리 옵션을 제공</li>
<li>헤더 처리: 헤더를 더 쉽게 추가하고 관리할 수 있음</li>
<li>인터셉터 지원: 요청/응답을 가로채고 수정할 수 있음</li>
</ul>
<h2 id="주요-메서드-1">주요 메서드</h2>
<ul>
<li>create(): RestClient 인스턴스를 생성</li>
<li>get(), post(), put(), delete() 등: HTTP 메서드를 지정합</li>
<li>uri(): 요청 URI를 설정</li>
<li>header(), headers(): 헤더를 추가</li>
<li>body(): 요청 본문을 설정</li>
<li>retrieve(): 응답을 받아옴</li>
<li>exchange(): 요청을 보내고 ResponseEntity를 반환</li>
</ul>
<h2 id="코드-예제-1">코드 예제</h2>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/1fd61b01-459c-4fe7-9f1d-067c7f185690/image.png" alt=""></p>
<h2 id="장점-1">장점</h2>
<ul>
<li>더 현대적이고 유연한 API를 제공</li>
<li>동기 및 비동기 작업을 모두 지원</li>
<li>메서드 체이닝을 통해 코드 가독성이 향상됨</li>
<li>더 세밀한 에러 처리가 가능합</li>
<li>RestTemplate보다 성능이 향상됨</li>
</ul>
<h2 id="주의-사항">주의 사항</h2>
<ul>
<li>Spring Framework 6.1 이상이 필요</li>
<li>RestTemplate에서 마이그레이션할 때 일부 코드 변경이 필요할 수 있음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TCP와 UDP]]></title>
            <link>https://velog.io/@silica_o3o/TCP%EC%99%80-UDP</link>
            <guid>https://velog.io/@silica_o3o/TCP%EC%99%80-UDP</guid>
            <pubDate>Wed, 24 Jul 2024 12:30:08 GMT</pubDate>
            <description><![CDATA[<p>TCP (Transmission Control Protocol)와 UDP (User Datagram Protocol)는 인터넷 프로토콜 스위트의 전송 계층에서 사용되는 두 가지 주요 프로토콜입니다. </p>
<h2 id="tcp">TCP:</h2>
<ul>
<li>연결 지향적: 데이터 전송 전에 연결을 설정합니다.</li>
<li>신뢰성: 데이터 손실 시 재전송을 보장합니다.</li>
<li>순서 보장: 패킷을 보낸 순서대로 수신합니다.</li>
<li>흐름 제어: 수신자의 처리 능력에 맞춰 전송 속도를 조절합니다.</li>
<li>오류 검사: 체크섬을 통해 데이터 무결성을 확인합니다.</li>
</ul>
<h2 id="udp">UDP:</h2>
<ul>
<li>비연결 지향적: 연결 설정 없이 데이터를 전송합니다.</li>
<li>비신뢰성: 데이터 손실 시 재전송을 보장하지 않습니다.</li>
<li>순서 보장 없음: 패킷이 도착한 순서대로 처리합니다.</li>
<li>단순성: 최소한의 프로토콜 메커니즘을 사용합니다.</li>
<li>낮은 오버헤드: 헤더가 작아 빠른 전송이 가능합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠키와 세션]]></title>
            <link>https://velog.io/@silica_o3o/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98</link>
            <guid>https://velog.io/@silica_o3o/%EC%BF%A0%ED%82%A4%EC%99%80-%EC%84%B8%EC%85%98</guid>
            <pubDate>Tue, 23 Jul 2024 14:20:28 GMT</pubDate>
            <description><![CDATA[<h2 id="쿠키cookie">쿠키(Cookie):</h2>
<ul>
<li><p>정의: 웹 서버가 사용자의 브라우저에 저장하는 작은 텍스트 파일입니다.</p>
</li>
<li><p>저장 위치: 클라이언트 측(사용자의 컴퓨터)에 저장됩니다.</p>
</li>
<li><p>생명주기: 만료 기간을 설정할 수 있으며, 브라우저가 종료되어도 유지될 수 있습니다.</p>
</li>
<li><p>용량: 일반적으로 4KB 정도로 제한됩니다.</p>
</li>
<li><p>보안: 클라이언트 측에 저장되므로 보안에 취약할 수 있습니다.</p>
</li>
</ul>
<h2 id="세션session">세션(Session):</h2>
<ul>
<li><p>정의: 서버 측에서 유지되는 사용자별 정보 저장소입니다.</p>
</li>
<li><p>저장 위치: 서버 측에 저장됩니다.</p>
</li>
<li><p>생명주기: 일반적으로 사용자가 브라우저를 종료하면 삭제됩니다.</p>
</li>
<li><p>용량: 서버 리소스에 따라 다르지만, 쿠키보다 큰 데이터를 저장할 수 있습니다.</p>
</li>
<li><p>보안: 서버에 저장되므로 쿠키보다 안전합니다.</p>
</li>
</ul>
<h2 id="주요-차이점">주요 차이점:</h2>
<ul>
<li><p>저장 위치: 쿠키는 클라이언트 측, 세션은 서버 측에 저장됩니다.</p>
</li>
<li><p>보안: 세션이 쿠키보다 더 안전합니다.</p>
</li>
<li><p>용량: 세션이 더 큰 데이터를 저장할 수 있습니다.</p>
</li>
<li><p>생명주기: 쿠키는 만료 기간을 설정할 수 있지만, 세션은 일반적으로 브라우저 종료 시 삭제됩니다.</p>
</li>
<li><p>성능: 쿠키는 매 요청마다 서버로 전송되므로 데이터 양이 많으면 성능에 영향을 줄 수 있습니다.</p>
</li>
</ul>
<p>실제 사용에서는 쿠키와 세션을 조합하여 사용하는 경우가 많다. 예를 들어, 세션 ID를 쿠키에 저장하고 실제 데이터는 서버의 세션에 저장하는 방식으로 활용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 메서드]]></title>
            <link>https://velog.io/@silica_o3o/HTTP-%EB%A9%94%EC%84%9C%EB%93%9C</link>
            <guid>https://velog.io/@silica_o3o/HTTP-%EB%A9%94%EC%84%9C%EB%93%9C</guid>
            <pubDate>Mon, 22 Jul 2024 13:52:44 GMT</pubDate>
            <description><![CDATA[<h2 id="get">GET</h2>
<ul>
<li>목적: 리소스 조회</li>
<li>특징<ul>
<li>요청 본문(body)을 가지지 않음</li>
<li>URL에 데이터를 포함하여 전송 (쿼리 파라미터)</li>
<li>멱등성(idempotent): 여러 번 호출해도 결과가 동일</li>
</ul>
</li>
</ul>
<p>예시: GET /users?id=123</p>
<h2 id="post">POST</h2>
<ul>
<li>목적: 새로운 리소스 생성</li>
<li>특징:<ul>
<li>요청 본문에 데이터를 포함</li>
<li>멱등성이 없음: 여러 번 호출하면 여러 리소스가 생성될 수 있음</li>
</ul>
</li>
</ul>
<p>예시: POST /users (본문에 사용자 정보 포함)</p>
<h2 id="put">PUT</h2>
<ul>
<li>목적: 리소스 전체 수정 또는 생성</li>
<li>특징:<ul>
<li>요청 본문에 수정할 전체 데이터를 포함</li>
<li>멱등성 있음: 여러 번 호출해도 결과가 동일</li>
</ul>
</li>
</ul>
<p>예시: PUT /users/123 (본문에 전체 사용자 정보 포함)</p>
<h2 id="patch">PATCH</h2>
<ul>
<li>목적: 리소스 부분 수정</li>
<li>특징:<ul>
<li>요청 본문에 수정할 일부 데이터만 포함</li>
<li>멱등성이 없을 수 있음</li>
</ul>
</li>
</ul>
<p>예시: PATCH /users/123 (본문에 변경할 필드만 포함)</p>
<h2 id="delete">DELETE</h2>
<ul>
<li>목적: 리소스 삭제</li>
<li>특징:<ul>
<li>일반적으로 요청 본문 없음</li>
<li>멱등성 있음: 여러 번 호출해도 결과 동일 (리소스가 이미 없어도 됨)</li>
</ul>
</li>
</ul>
<p>예시: DELETE /users/123</p>
<h2 id="head">HEAD</h2>
<ul>
<li>목적: GET과 동일하나 응답 본문을 제외한 헤더만 반환</li>
<li>특징:<ul>
<li>리소스를 받지 않고 메타데이터만 확인할 때 유용</li>
</ul>
</li>
</ul>
<p>예시: HEAD /files/image.jpg (파일 크기, 타입 등만 확인)</p>
<h2 id="options">OPTIONS</h2>
<ul>
<li>목적: 서버가 지원하는 메서드 확인 또는 CORS preflight 요청</li>
<li>특징:<ul>
<li>서버의 기능을 알아보는 데 사용</li>
</ul>
</li>
</ul>
<p>예시: OPTIONS /api/data</p>
<h2 id="connect">CONNECT</h2>
<ul>
<li>목적: 프록시를 통한 터널 연결 수립</li>
<li>특징:<ul>
<li>주로 HTTPS 연결을 위해 사용</li>
</ul>
</li>
</ul>
<p>예시: CONNECT example.com:443 HTTP/1.1</p>
<h2 id="trace">TRACE</h2>
<ul>
<li>목적: 요청 메시지 루프백 테스트</li>
<li>특징:<ul>
<li>디버깅 용도로 사용, 보안상 비활성화되는 경우가 많음</li>
</ul>
</li>
</ul>
<p>예시: TRACE /index.html</p>
<p>각 메서드는 HTTP/1.1 스펙에 정의되어 있으며, RESTful API 설계에서 중요한 역할을 합니다. 메서드 선택 시 그 의미와 용도를 잘 고려해야 하며, 보안과 성능 측면도 함께 고려해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Transactional]]></title>
            <link>https://velog.io/@silica_o3o/Transactional</link>
            <guid>https://velog.io/@silica_o3o/Transactional</guid>
            <pubDate>Thu, 11 Jul 2024 11:11:06 GMT</pubDate>
            <description><![CDATA[<h2 id="transactional이란">Transactional이란?</h2>
<p>@Transactional은 스프링 프레임워크에서 제공하는 어노테이션으로, 주로 데이터베이스 트랜잭션 관리를 위해 사용된다. 이 어노테이션을 사용하면 메서드나 클래스가 트랜잭션 내에서 실행되도록 설정할 수 있다. 트랜잭션은 일련의 데이터베이스 작업들을 하나의 단위로 묶어서, 전체 작업이 성공하면 커밋(commit)하고, 중간에 실패하면 롤백(rollback)하여 데이터의 일관성과 무결성을 보장한다.</p>
<h2 id="주요-기능">주요 기능</h2>
<ul>
<li><p>트랜잭션 시작과 종료: @Transactional이 적용된 메서드가 호출되면, 스프링은 트랜잭션을 시작하고, 메서드가 정상적으로 완료되면 트랜잭션을 커밋한다. 만약 예외가 발생하면 트랜잭션을 롤백한다.</p>
</li>
<li><p>예외 처리: 기본적으로 RuntimeException 및 그 하위 클래스가 발생할 경우 트랜잭션이 롤백된다. 그러나 특정 예외에 대해 롤백 또는 커밋 동작을 커스터마이징할 수 있다.</p>
</li>
<li><p>전파(Propagation): 트랜잭션이 이미 진행 중일 때 새로운 트랜잭션을 시작할지, 기존 트랜잭션에 참여할지, 아니면 별도의 트랜잭션으로 처리할지 등을 설정할 수 있다.</p>
</li>
<li><p>격리 수준(Isolation Level): 데이터 일관성을 유지하기 위해 트랜잭션 격리 수준을 설정할 수 있다. 이는 트랜잭션이 다른 트랜잭션과 어떤 방식으로 상호작용할지 정의한다.</p>
</li>
</ul>
<h2 id="사용-예제">사용 예제</h2>
<ul>
<li>클래스 수준에서 사용</li>
</ul>
<p>클래스에 @Transactional을 적용하면 클래스의 모든 public 메서드에 트랜잭션이 적용된다.</p>
<pre><code>@Service
@Transactional
public class UserService {

    public void createUser(User user) {
        // 트랜잭션 시작
        // 사용자 생성 로직
        // 트랜잭션 커밋 또는 롤백
    }

    public User getUser(Long id) {
        // 트랜잭션 시작
        // 사용자 조회 로직
        // 트랜잭션 커밋 또는 롤백
    }
}</code></pre><ul>
<li>메서드 수준에서 사용</li>
</ul>
<p>특정 메서드에만 트랜잭션을 적용하고자 할 때는 메서드에 @Transactional을 직접 지정할 수 있다.</p>
<pre><code>@Service
public class OrderService {

    @Transactional
    public void placeOrder(Order order) {
        // 트랜잭션 시작
        // 주문 처리 로직
        // 트랜잭션 커밋 또는 롤백
    }

    public Order getOrder(Long id) {
        // 이 메서드에는 트랜잭션이 적용되지 않음
        // 주문 조회 로직
    }
}</code></pre><h2 id="트랜잭션-전파와-격리-수준-설정">트랜잭션 전파와 격리 수준 설정</h2>
<p>트랜잭션 전파 및 격리 수준을 설정하여 더욱 정밀하게 트랜잭션 동작을 제어할 수 있다.</p>
<pre><code>@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
public void processPayment(Payment payment) {
    // 트랜잭션 시작
    // 결제 처리 로직
    // 트랜잭션 커밋 또는 롤백
}</code></pre><h2 id="언제-사용하는가">언제 사용하는가?</h2>
<ul>
<li><p>데이터 무결성 보장: 데이터베이스 작업을 수행할 때, 일관성 있는 상태를 유지해야 할 경우에 사용한다. 예를 들어, 은행 계좌 이체에서 출금과 입금 작업이 모두 성공해야 하는 경우.</p>
</li>
<li><p>에러 처리: 작업 중 오류가 발생했을 때, 이전 작업들을 원상태로 돌려야 할 때 사용한다. 예를 들어, 여러 테이블에 데이터를 삽입하는 작업에서 중간에 오류가 발생했을 때, 이미 삽입된 데이터를 모두 롤백해야 하는 경우.</p>
</li>
<li><p>복잡한 비즈니스 로직: 여러 데이터베이스 연산이 하나의 논리적 작업 단위로 묶여야 하는 경우에 사용한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Lazy Loading과 Eager Loading]]></title>
            <link>https://velog.io/@silica_o3o/Lazy-Loading%EA%B3%BC-Eager-Loading</link>
            <guid>https://velog.io/@silica_o3o/Lazy-Loading%EA%B3%BC-Eager-Loading</guid>
            <pubDate>Tue, 02 Jul 2024 13:44:24 GMT</pubDate>
            <description><![CDATA[<h2 id="lazy-loading-지연-로딩">Lazy Loading (지연 로딩)</h2>
<p>Lazy Loading은 연관된 엔티티를 실제로 필요할 때까지 로드하지 않는 전략이다. 즉, 연관된 엔티티에 접근할 때 해당 데이터를 데이터베이스에서 가져온다.</p>
<h2 id="특징">특징</h2>
<ul>
<li><p>지연 로딩을 위한 프록시 사용: JPA는 연관된 엔티티에 대한 프록시 객체를 생성하고, 실제 데이터에 접근할 때 데이터베이스에서 데이터를 가져온다.</p>
</li>
<li><p>성능 최적화: 필요할 때만 데이터를 로드하므로 초기 로딩 시 성능이 향상될 수 있다. 대규모 데이터셋에서 유용하다.</p>
</li>
<li><p>N+1 문제 발생 가능성: 부모 엔티티를 조회한 후 각 자식 엔티티에 접근할 때 추가 쿼리가 발생할 수 있다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/ef443309-bf0d-478b-a2e6-5ac71d271325/image.png" alt=""></p>
<p>여기서 User 엔티티의 posts 컬렉션은 Lazy 로딩된다. User 엔티티를 조회할 때 posts 컬렉션은 로드되지 않고, posts에 접근하는 시점에 데이터베이스 쿼리가 실행된다.</p>
<h2 id="eager-loading-즉시-로딩">Eager Loading (즉시 로딩)</h2>
<p>Eager Loading은 엔티티를 조회할 때 연관된 엔티티를 즉시 함께 로드하는 전략이다. 즉, 부모 엔티티를 조회할 때 자식 엔티티도 즉시 로드된다.</p>
<h2 id="특징-1">특징</h2>
<ul>
<li><p>즉시 로딩: 부모 엔티티를 로드할 때 연관된 모든 엔티티를 함께 로드한다.</p>
</li>
<li><p>쿼리 수 감소: 초기 로딩 시 한 번의 쿼리로 연관된 데이터를 모두 가져오므로 N+1 문제를 피할 수 있다.</p>
</li>
<li><p>초기 성능 부하 증가: 초기 로딩 시 모든 연관 데이터를 로드하므로 불필요한 데이터를 미리 로드하여 성능이 저하될 수 있다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/silica_o3o/post/85d1b981-9325-4876-bb73-ec8e84b6909a/image.png" alt=""></p>
<p>여기서 User 엔티티의 posts 컬렉션은 Eager 로딩된다. User 엔티티를 조회할 때 posts 컬렉션도 함께 로드된다.</p>
<h2 id="비교">비교</h2>
<ul>
<li><p>로딩 시점: Lazy Loading은 실제 데이터에 접근할 때 로드되며, Eager Loading은 엔티티를 조회할 때 즉시 로드된다.</p>
</li>
<li><p>쿼리 수: Lazy Loading은 여러 쿼리가 발생할 수 있으며, Eager Loading은 한 번의 쿼리로 모든 데이터를 로드한다.</p>
</li>
<li><p>성능: Lazy Loading은 초기 로딩 성능이 좋지만, 필요할 때마다 쿼리를 실행하여 N+1 문제가 발생할 수 있다. Eager Loading은 초기 로딩 시 성능 부하가 크지만, 추가적인 쿼리가 적다.</p>
</li>
<li><p>메모리 사용: Lazy Loading은 필요한 데이터만 로드하므로 메모리 사용이 적을 수 있고, Eager Loading은 불필요한 데이터를 함께 로드하여 메모리 사용이 많아질 수 있다.</p>
</li>
</ul>
<h2 id="결론">결론</h2>
<p>Lazy Loading은 대규모 데이터셋에서 필요한 데이터만 로드하여 메모리 사용을 줄이는 데 유리합니다. 하지만 N+1 문제를 주의해야 한다.</p>
<p>Eager Loading은 데이터 접근 패턴이 예측 가능하고 연관된 데이터를 한 번에 로드하는 것이 효율적일 때 유용하다. 그러나 초기 로딩 시 성능 부하가 증가할 수 있다.</p>
]]></description>
        </item>
    </channel>
</rss>