<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>호기심 많은 개발자</title>
    <link>https://curious-kang.tistory.com/</link>
    <description>하나하나 쌓아나가다 보면, 분명 쌓인다.
천리길도 한 걸음이 시작이니까</description>
    <language>ko</language>
    <pubDate>Mon, 6 Apr 2026 18:46:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>나는 나야</managingEditor>
    <item>
      <title>[Spring Batch] JpaPagingItemReader 성능 차이 확인 필수 (Fetch Join OOM부터 transacted의 숨겨진 함정까지)</title>
      <link>https://curious-kang.tistory.com/25</link>
      <description>&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;Spring Batch에서 대용량 데이터를 처리할 때 JpaCursorItemReader에서 JpaPagingItemReader로 넘어오는 것은 흔한 수순이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;하지만 JpaPagingItemReader를 만만하게 보고 기존 웹 애플리케이션에서 하던 것처럼 쿼리를 짰다가는, 시스템이 혼란에 빠지는 것을 목격하게 될 것이다. 오늘은 이 녀석이 숨기고 있는 치명적인 메모리 함정과 프레임워크 내부의 기이한 동작 방식까지 전부 파헤쳐본다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size26&quot;&gt;1. Fetch Join과 페이징의 끔찍한 혼종 (OOM의 서막)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;JPA를 좀 다뤄본 개발자라면 N+1 문제를 피하기 위해 숨 쉬듯 Fetch Join을 사용할 것이다. JpaPagingItemReader의 queryString()에도 자연스럽게 JOIN FETCH를 집어넣게 되는데, 여기서부터 비극이 시작된다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;결론부터 말하자면, JpaPagingItemReader에서 Fetch Join을 사용하면 큰 코 다친다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;왜 그런지 설명해주지. Hibernate는 컬렉션 Fetch Join과 페이징(LIMIT/OFFSET)을 함께 사용하는 것을 허용하지 않는다. 만약 이 둘을 같이 쓰면, DB 쿼리에는 페이징을 적용하지 않고 &lt;b data-index-in-node=&quot;116&quot; data-path-to-node=&quot;9&quot;&gt;일단 조건을 만족하는 전체 데이터를 메모리에 모두 로드한 후, 애플리케이션(WAS) 단에서 페이징을 수행&lt;/b&gt;해 버린다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어보자. 전체 Post 데이터가 13개이고, pageSize = 5라면 쿼리는 5개, 5개, 3개씩 총 3번 실행되어야 정상이다. 하지만 Fetch Join이 들어간 순간, 페이지를 조회할 때마다 &lt;b data-index-in-node=&quot;114&quot; data-path-to-node=&quot;10&quot;&gt;매번 전체 데이터 13개와 연관된 Report 객체를 전부 다 메모리에 퍼 올린 뒤 거기서 5개를 잘라낸다.&lt;/b&gt; 데이터가 수백만 건이라면? 배치가 시작되자마자 OutOfMemory(OOM)와 함께 서버가 장렬히 전사할 것이다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size26&quot;&gt;2. Fetch Join을 뺐더니 찾아온 N+1의 저주&lt;/h2&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;메모리 폭발을 막기 위해 queryString()에서 Fetch Join을 제거했다 치자. 페이징은 DB 단에서 예쁘게 먹히겠지만, 이제는 &lt;b data-index-in-node=&quot;78&quot; data-path-to-node=&quot;12&quot;&gt;N+1 쿼리&lt;/b&gt;라는 새로운 적이 등장한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;Post 엔티티를 5개 읽어왔는데, Processor에서 연관된 Report를 건드리는 순간 추가 쿼리가 5번 더 나간다. 이를 해결하기 위한 정석적인 방법은 @BatchSize를 적용하여 IN 절로 연관 데이터를 한 번에 쓸어오는 것이다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj8g4qzovuSAxUAAAAAHQAAAAAQzQE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;posts&quot;)
@Getter
public class Post {
    @Id
    private Long id;
    
    // ...

    @OneToMany(mappedBy = &quot;post&quot;, fetch = FetchType.EAGER) // 왜 EAGER일까?
    @BatchSize(size = 5) // IN 절 최적화!
    private List&amp;lt;Report&amp;gt; reports = new ArrayList&amp;lt;&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;그런데 여기서 의문이 든다. 보통 JPA에서는 성능 최적화를 위해 무조건 LAZY 로딩을 권장하는데, 왜 EAGER로 바꿨을까?&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size26&quot;&gt;3. IDE를 켜라, 범인은 내부에 있다 (doReadPage 해부)&lt;/h2&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;LAZY 로딩 상태에서는 @BatchSize가 전혀 먹히지 않고 N+1이 그대로 터진다. 왜 이런 비정상적인 동작이 일어나는 걸까? 답은 JpaPagingItemReader.doReadPage() 내부 코드에 있다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj8g4qzovuSAxUAAAAAHQAAAAAQzgE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Override
protected void doReadPage() {
    EntityTransaction tx = null;

    if (transacted) { // 기본값이 true다!
        tx = entityManager.getTransaction();
        tx.begin();
        entityManager.flush();
        entityManager.clear();
    } 

    Query query = createQuery().setFirstResult(getPage() * getPageSize()).setMaxResults(getPageSize());
  
    if (!transacted) {
        // ...
    } else {
        results.addAll(query.getResultList()); // 데이터 조회
        tx.commit(); // 곧바로 트랜잭션 종료?!
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19&quot;&gt;문제의 원인은 트랜잭션의 범위다.&lt;/b&gt; 데이터를 조회(getResultList())하자마자 냅다 tx.commit()으로 트랜잭션을 닫아버린다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;이후 ItemProcessor에서 post.getReports()를 호출해 컬렉션에 접근하려 할 때는 &lt;b data-index-in-node=&quot;57&quot; data-path-to-node=&quot;20&quot;&gt;이미 트랜잭션이 끝난 상태&lt;/b&gt;다. @BatchSize는 영속성 컨텍스트와 트랜잭션이 살아있어야 동작하는데, 트랜잭션이 죽어버렸으니 IN 절 최적화가 불가능해지고 N+1 문제가 터지는 것이다. 결국 이 멍청한 구현 때문에 억지로 FetchType.EAGER를 써서 트랜잭션이 닫히기 전에 데이터를 강제로 욱여넣어야 하는 슬픈 상황이 벌어진다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size26&quot;&gt;4. transacted = true의 치명적인 잠재적 위험&lt;/h2&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;여기서 끝이 아니다. transacted = true (기본값)일 때 doReadPage() 상단을 다시 보자.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj8g4qzovuSAxUAAAAAHQAAAAAQzwE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;tx.begin();
entityManager.flush(); //   매우 위험!
entityManager.clear();
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;페이지를 읽기 전에 갑자기 flush()를 때린다. 이게 왜 위험할까? 만약 이전 Chunk의 ItemProcessor에서 엔티티 데이터를 조작해 놨다면? &lt;b data-index-in-node=&quot;87&quot; data-path-to-node=&quot;24&quot;&gt;데이터를 '읽으러' 들어온 Reader가 의도치 않게 DB에 '수정(Update)' 쿼리를 날려버릴 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&quot;ItemReader가 데이터를 변경까지 한다니... 이런 코드는 시스템을 혼란에 빠뜨린다.&quot;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;따라서 JpaPagingItemReader를 쓸 때는 시스템의 안정성을 위해 다음과 같이 transacted 옵션을 강제로 꺼야 한다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwj8g4qzovuSAxUAAAAAHQAAAAAQ0AE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Java&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public JpaPagingItemReader&amp;lt;Post&amp;gt; postBlockReader() {
    return new JpaPagingItemReaderBuilder&amp;lt;Post&amp;gt;()
            // ... 생략 ...
            .transacted(false) 
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Spring</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/25</guid>
      <comments>https://curious-kang.tistory.com/25#entry25comment</comments>
      <pubDate>Sat, 28 Feb 2026 19:11:16 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] 배치의 두 가지 심장: Tasklet vs Chunk 지향 처리</title>
      <link>https://curious-kang.tistory.com/24</link>
      <description>&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 Spring Batch의 Step을 구성하는 두 가지 핵심 처리 모델, &lt;b&gt;Tasklet(태스크릿)&lt;/b&gt;과 &lt;b&gt;Chunk(청크)&lt;/b&gt;에 대해 심층 분석해 보시죠.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size26&quot;&gt;1. 태스크릿(Tasklet) 지향 처리: 단순함의 미학&lt;/h2&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7&quot;&gt;태스크릿(Tasklet)&lt;/b&gt; 모델은 Spring Batch에서 가장 기본적인 Step 구현 방식입니다. 복잡한 데이터 처리보다는 &lt;b data-index-in-node=&quot;70&quot; data-path-to-node=&quot;7&quot;&gt;단순한 시스템 작업&lt;/b&gt;이나 &lt;b data-index-in-node=&quot;83&quot; data-path-to-node=&quot;7&quot;&gt;유틸성 작업&lt;/b&gt;에 주로 사용됩니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;언제 사용하는가?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;대량의 데이터를 읽고 쓰는 ETL 작업이 아닌 경우, 대부분 Tasklet을 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그 파일 삭제&lt;/li&gt;
&lt;li&gt;단순 알림(이메일, 슬랙) 발송&lt;/li&gt;
&lt;li&gt;외부 API 호출 후 단순 로깅&lt;/li&gt;
&lt;li&gt;오래된 파일 아카이빙&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;구현 핵심: Tasklet 인터페이스&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;Tasklet 인터페이스의 execute() 메서드에 로직을 작성하면 됩니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjHxJHeuNeSAxUAAAAAHQAAAAAQxQk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Tasklet {
    @Nullable
    RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size23&quot;&gt;프로세스 제거 예제 (feat. RepeatStatus)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;Tasklet의 핵심은 &lt;b data-index-in-node=&quot;13&quot; data-path-to-node=&quot;15&quot;&gt;RepeatStatus&lt;/b&gt; 반환값에 있습니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjHxJHeuNeSAxUAAAAAHQAAAAAQxgk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
public class ZombieProcessCleanupTasklet implements Tasklet {
    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        log.info(&quot;  시스템 안정화 완료.&quot;);
        
        return RepeatStatus.FINISHED; // 작업 끝! Step 종료
        // return RepeatStatus.CONTINUABLE; // 아직 할 일이 남음 (반복 실행)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,0,0&quot;&gt;RepeatStatus.FINISHED&lt;/b&gt;: &quot;다 끝났다.&quot; Step을 종료하고 다음으로 넘어갑니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,1,0&quot;&gt;RepeatStatus.CONTINUABLE&lt;/b&gt;: &quot;아직이다.&quot; execute() 메서드를 다시 호출합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i data-index-in-node=&quot;0&quot; data-path-to-node=&quot;17,1,1,0,0&quot;&gt;왜 while문을 안 쓰고 CONTINUABLE을 쓸까요?&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;바로 &lt;b data-index-in-node=&quot;3&quot; data-path-to-node=&quot;17,1,1,1,0&quot;&gt;트랜잭션&lt;/b&gt; 때문입니다. execute() 호출 한 번당 하나의 트랜잭션이 생성됩니다. 작업을 쪼개서 실행하여 중간에 실패하더라도 전체가 롤백되는 대참사를 막기 위함입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size23&quot;&gt;Configuration 설정 (Tip: ResourcelessTransactionManager)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;DB 접근이 없는 단순 파일 작업이나 알림 발송의 경우, 굳이 무거운 DB 트랜잭션을 쓸 필요가 없습니다. 이때는 ResourcelessTransactionManager를 사용하면 효율적입니다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjHxJHeuNeSAxUAAAAAHQAAAAAQxwk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public Step zombieCleanupStep() {
    return new StepBuilder(&quot;zombieCleanupStep&quot;, jobRepository)
            .tasklet(zombieProcessCleanupTasklet(), new ResourcelessTransactionManager()) // DB 트랜잭션 생략
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;21&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size26&quot;&gt;2. 청크(Chunk) 지향 처리: 대용량 데이터의 구원자&lt;/h2&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;배치의 꽃, &lt;b data-index-in-node=&quot;7&quot; data-path-to-node=&quot;23&quot;&gt;청크(Chunk) 지향 처리&lt;/b&gt;입니다. 데이터 기반의 &lt;b data-index-in-node=&quot;35&quot; data-path-to-node=&quot;23&quot;&gt;읽기(Read) &amp;rarr; 처리(Process) &amp;rarr; 쓰기(Write)&lt;/b&gt; 패턴을 다룹니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;24&quot; data-ke-size=&quot;size23&quot;&gt;왜 'Chunk(덩어리)'인가?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;100만 건의 데이터를 한 번에 메모리에 올리면 어떻게 될까요?&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b data-index-in-node=&quot;39&quot; data-path-to-node=&quot;25&quot;&gt;OOM(Out Of Memory) 발생 &amp;amp; 시스템 폭발&lt;/b&gt;  &lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;Spring Batch는 이를 방지하기 위해 데이터를 지정한 크기(Chunk Size)만큼 쪼개서 처리합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27,0,0&quot;&gt;메모리 보호:&lt;/b&gt; 한 번에 10개(Chunk Size)씩만 메모리에 올립니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27,1,0&quot;&gt;트랜잭션 관리:&lt;/b&gt; 100만 건을 하나의 트랜잭션으로 묶는 건 자살행위입니다. Chunk 단위로 트랜잭션을 커밋하여 실패 시 해당 Chunk만 롤백합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;28&quot; data-ke-size=&quot;size23&quot;&gt;청크 처리의 3대장 (The Trio)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;29&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,0,0&quot;&gt;ItemReader (읽기)&lt;/b&gt;: 데이터를 &lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;29,0,0&quot;&gt;하나씩&lt;/b&gt; 읽어옵니다. (read())
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;29,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 이상 읽을 데이터가 없으면 null을 반환하며 Step이 종료됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,1,0&quot;&gt;ItemProcessor (처리)&lt;/b&gt;: 읽어온 데이터를 &lt;b data-index-in-node=&quot;29&quot; data-path-to-node=&quot;29,1,0&quot;&gt;하나씩&lt;/b&gt; 가공합니다. (process())
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;29,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터링, 데이터 변환, 검증을 수행합니다. (생략 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;29,2,0&quot;&gt;ItemWriter (쓰기)&lt;/b&gt;: 가공된 데이터를 **Chunk 단위(List)**로 모아서 한 번에 씁니다. (write())
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;29,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reader/Processor와 달리 &lt;b data-index-in-node=&quot;21&quot; data-path-to-node=&quot;29,2,1,0,0&quot;&gt;뭉텅이&lt;/b&gt;로 처리한다는 점이 핵심입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size23&quot;&gt;주의! 실제 동작 흐름&amp;nbsp;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;많은 개발자가 헷갈려 하는 부분입니다.&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;32&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;32,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;32,0&quot;&gt;Chunk Size가 10일 때의 흐름&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;32,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ItemReader가 데이터를 1개 읽습니다.&lt;/li&gt;
&lt;li&gt;ItemReader가 10번 호출될 때까지 반복하여 Chunk(입력)를 채웁니다. (X) -&amp;gt; &lt;b data-index-in-node=&quot;53&quot; data-path-to-node=&quot;32,1,1,0&quot;&gt;(수정: Reader가 읽고 바로 Processor로 가는 게 아닙니다)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;32,2&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;32,2&quot;&gt;정확한 흐름:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;32,3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;32,3,0,0&quot;&gt;Read:&lt;/b&gt; ItemReader.read()를 호출해 아이템을 하나 읽습니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;32,3,1,0&quot;&gt;Process:&lt;/b&gt; 읽은 아이템을 바로 ItemProcessor.process()로 가공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;32,3,2,0&quot;&gt;Repeat:&lt;/b&gt; 위 1-2 과정을 Chunk Size(10번)만큼 반복하여 가공된 결과물 리스트(List)를 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;32,3,3,0&quot;&gt;Write:&lt;/b&gt; 10개가 모인 리스트를 ItemWriter.write()에 전달하여 한 번에 씁니다. (Transaction Commit)&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h3 data-path-to-node=&quot;34&quot; data-ke-size=&quot;size23&quot;&gt;코드 구성 예시&lt;/h3&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjHxJHeuNeSAxUAAAAAHQAAAAAQyAk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Bean
public Step processStep(JobRepository jobRepository, PlatformTransactionManager tm) {
   return new StepBuilder(&quot;processStep&quot;, jobRepository)
           .&amp;lt;Customer, CustomerSummary&amp;gt;chunk(10, tm) // 10개씩 끊어서 처리
           .reader(itemReader())       
           .processor(itemProcessor()) 
           .writer(itemWriter())      
           .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;36&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;36,0,0&quot;&gt;&amp;lt;Input, Output&amp;gt;&lt;/b&gt;: Reader가 읽는 타입, Writer가 쓰는 타입을 제네릭으로 명시합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;36,1,0&quot;&gt;chunk(10)&lt;/b&gt;: 10개 단위로 트랜잭션을 커밋하겠다는 의미입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;37&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;38&quot; data-ke-size=&quot;size26&quot;&gt;3. 요약: Tasklet vs Chunk 선택 가이드&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;39&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;태스크릿(Tasklet)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;청크(Chunk)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;39,1,0,0&quot;&gt;주요 목적&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,1,1,0&quot;&gt;단순 작업, 유틸리티, 알림&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,1,2,0&quot;&gt;대용량 데이터 처리 (ETL)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;39,2,0,0&quot;&gt;처리 방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,2,1,0&quot;&gt;단일 메서드 (execute) 실행&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,2,2,0&quot;&gt;Read &amp;rarr; Process &amp;rarr; Write 반복&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;39,3,0,0&quot;&gt;트랜잭션&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,3,1,0&quot;&gt;execute 전체가 1 트랜잭션 (기본)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,3,2,0&quot;&gt;Chunk 단위로 트랜잭션 분리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;39,4,0,0&quot;&gt;구현 난이도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,4,1,0&quot;&gt;쉬움&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,4,2,0&quot;&gt;설정할 것이 많음 (3대장 구현)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,5,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;39,5,0,0&quot;&gt;비고&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,5,1,0&quot;&gt;RepeatStatus로 반복 제어&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;39,5,2,0&quot;&gt;Chunk Size로 메모리/트랜잭션 제어&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>chunk</category>
      <category>Job</category>
      <category>spring batch</category>
      <category>step</category>
      <category>tasklet</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/24</guid>
      <comments>https://curious-kang.tistory.com/24#entry24comment</comments>
      <pubDate>Sun, 15 Feb 2026 11:07:27 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] DAU ~100, 굳이 Redis를 써야 할까? (Local Cache 도입기)</title>
      <link>https://curious-kang.tistory.com/23</link>
      <description>&lt;h3 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size23&quot;&gt;1. 배경 및 문제 상황 (Background)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;최근 사내 프로젝트 개발 중, 특정 API의 응답 속도가 현저히 느려지는 현상을 발견했습니다. 프로파일링 결과, 범인은 &lt;b data-index-in-node=&quot;67&quot; data-path-to-node=&quot;4&quot;&gt;사용자 정보를 불러오는 로직&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;현상:&lt;/b&gt; 사용자 정보 조회 시 응답 시간이 &lt;b&gt;약 2,000ms (2초)&lt;/b&gt;까지 지연됨.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;원인:&lt;/b&gt; 레거시 시스템 연동 및 복잡한 조인 연산으로 인해 DB 조회 비용이 매우 높음.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;제약 사항:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;5,2,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 다른 핵심 기능 개발로 인해 인프라를 변경하거나 대대적인 리팩토링을 할 &lt;b data-index-in-node=&quot;43&quot; data-path-to-node=&quot;5,2,1,0,0&quot;&gt;시간적 여유가 없음.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;하지만 사용자의 체감 성능 향상을 위해 &lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;5,2,1,1,0&quot;&gt;즉각적인 개선&lt;/b&gt;이 필요함.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;2. 기술 검토: Local Cache vs Global Cache&lt;/h3&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;DB 튜닝이 어렵다면 가장 효과적인 대안은 &lt;b&gt;캐싱(Caching)&lt;/b&gt;입니다. 캐시 저장소를 어디에 둘 것인가를 두고 &lt;b&gt;Local Cache(Memory)&lt;/b&gt;와 &lt;b data-index-in-node=&quot;91&quot; data-path-to-node=&quot;7&quot;&gt;Global Cache(Redis 등)&lt;/b&gt; 두 가지 선택지를 놓고 고민했습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;8&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비교 항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Local Cache (In-Memory)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Global Cache (External - ex. Redis)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0,0&quot;&gt;속도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,1,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,1,0&quot;&gt;매우 빠름&lt;/b&gt; (네트워크 통신 없음)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,1,2,0&quot;&gt;빠름 (네트워크 통신 발생)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,2,0,0&quot;&gt;구현 난이도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,2,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,2,1,0&quot;&gt;매우 낮음&lt;/b&gt; (라이브러리 추가만으로 가능)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,2,2,0&quot;&gt;중간 (별도 서버 구축 및 연동 필요)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,3,0,0&quot;&gt;데이터 정합성&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,3,1,0&quot;&gt;낮음 (서버 간 데이터 불일치 가능성 있음)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,3,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,3,2,0&quot;&gt;높음&lt;/b&gt; (모든 서버가 동일한 데이터 바라봄)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,4,0,0&quot;&gt;인프라 비용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,4,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,4,1,0&quot;&gt;0원&lt;/b&gt; (애플리케이션 메모리 사용)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,4,2,0&quot;&gt;추가 비용 발생 (운영/관리 포인트 증가)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,5,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,5,0,0&quot;&gt;메모리 제한&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,5,1,0&quot;&gt;애플리케이션 힙 메모리에 의존&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;8,5,2,0&quot;&gt;별도 서버 메모리 사용으로 여유로움&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;3. 의사결정: 왜 Local Cache를 선택했는가?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로는 분산 환경에서의 정합성 문제 때문에 Global Cache(Redis)를 선호하지만, 우리 서비스의 현재 상황을 분석해본 결과 &lt;b data-index-in-node=&quot;78&quot; data-path-to-node=&quot;10&quot;&gt;Local Cache가 더 합리적&lt;/b&gt;이라는 결론을 내렸습니다. 이유는 크게 세 가지입니다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;① 낮은 DAU (Daily Active Users)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;현재 서비스의 &lt;b data-index-in-node=&quot;8&quot; data-path-to-node=&quot;12&quot;&gt;DAU는 ~100명 수준&lt;/b&gt;입니다. 트래픽이 적기 때문에 애플리케이션 서버의 메모리에 캐시를 저장하더라도 리소스 부담이 거의 없습니다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size20&quot;&gt;② 변경이 거의 없는 데이터 (Data Volatility)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;캐싱하려는 데이터는 '사용자 프로필'과 같이 &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;14&quot;&gt;한번 생성되면 거의 변경되지 않는 정적인 정보&lt;/b&gt;입니다. 데이터 변경이 빈번하지 않으므로, 서버 간 데이터 불일치(Data Inconsistency) 문제가 발생할 확률이 극히 낮으며, 발생하더라도 치명적이지 않습니다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size20&quot;&gt;③ 오버엔지니어링 방지 (Efficiency)&lt;/h4&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;당장 개발 리소스가 부족한 상황에서, 단순히 캐싱을 위해 Redis 서버를 띄우고 관리 포인트를 늘리는 것은 &lt;b data-index-in-node=&quot;61&quot; data-path-to-node=&quot;16&quot;&gt;배보다 배꼽이 더 큰 오버엔지니어링&lt;/b&gt;입니다. 별도의 인프라 구축 없이 코드 몇 줄로 즉시 적용 가능한 Local Cache가 현시점에서는 'Best Practice'입니다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size23&quot;&gt;4. 적용 및 구현 (Implementation)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 환경에서 가장 간단하게 적용할 수 있는 Caffeine Cache (또는 기본 ConcurrentMapCache)를 사용했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19&quot;&gt;설정 (Configuration)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjb1r6W0LeSAxUAAAAAHQAAAAAQsgE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Java&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@EnableCaching
@Configuration
public class CacheConfig {
    // 10분 뒤 만료, 최대 1000개 항목 저장 등 간단한 정책 설정
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21&quot;&gt;적용 코드 (Service Layer)&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjb1r6W0LeSAxUAAAAAHQAAAAAQswE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;&lt;span&gt;Java&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
public class UserService {

    @Cacheable(value = &quot;userInfoCache&quot;, key = &quot;#userId&quot;)
    public UserDto getUserInfo(String userId) {
        // 기존: 약 2000ms 소요되던 로직
        return userRepository.findComplexUserInfo(userId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-path-to-node=&quot;23&quot; data-ke-size=&quot;size23&quot;&gt;5. 결과 및 향후 계획 (Result)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;Local Cache 도입 후 성능은 드라마틱하게 개선되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;25&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25,0,0&quot;&gt;Before:&lt;/b&gt; 평균 &lt;b data-index-in-node=&quot;11&quot; data-path-to-node=&quot;25,0,0&quot;&gt;2,000ms&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;25,1,0&quot;&gt;After:&lt;/b&gt; 평균 &lt;b data-index-in-node=&quot;10&quot; data-path-to-node=&quot;25,1,0&quot;&gt;0ms ~ 5ms&lt;/b&gt; (Cache Hit 기준)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;개발 소요 시간은 1시간 미만이었으며, 추가적인 인프라 비용 없이 사용자의 답답함을 완벽하게 해소했습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;27&quot;&gt;Next Step:&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;물론 서비스가 성장하여 &lt;b data-index-in-node=&quot;24&quot; data-path-to-node=&quot;27&quot;&gt;DAU가 수만 명 단위로 늘어나거나, 사용자의 정보 변경이 잦아지는 시점&lt;/b&gt;이 오면 그때는 주저 없이 &lt;b&gt;Redis(Global Cache)&lt;/b&gt;로 마이그레이션 할 계획입니다. 하지만 지금 우리 서비스 단계에서는 &lt;b data-index-in-node=&quot;140&quot; data-path-to-node=&quot;27&quot;&gt;Local Cache가 가장 적절하고 실용적인 공학적 선택&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Next Step: 캐시 뒤에 숨은 비효율 제거 (Fetch Optimization)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15,1&quot; data-ke-size=&quot;size16&quot;&gt;캐싱 전략으로 트래픽을 방어할 순 있었지만 쿼리 자체의 효율성을 높이기 위해 최적화 하였습니다. 캐시 히트율(Hit Rate)이 아무리 높아도, 캐시 미스(Miss) 발생 시 DB에 가해지는 부하 자체가 크다면 잠재적인 위험 요소가 되기 때문입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15,2&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,2&quot;&gt;발견된 문제: JPA N+1&lt;/b&gt; 프로파일링 결과, Hibernate가 연관 관계를 조회할 때 불필요한 쿼리를 반복 호출하는 현상 발견&lt;/p&gt;
&lt;p data-path-to-node=&quot;15,4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,4&quot;&gt;최적화 적용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15,5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,5,0,0&quot;&gt;Fetch Join 적용:&lt;/b&gt; join fetch 구문을 사용하여 한 번의 쿼리로 필요한 모든 데이터를 즉시 로딩.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,5,1,0&quot;&gt;Batch Size 설정:&lt;/b&gt; 컬렉션 조회 시 IN 쿼리로 묶어서 조회하도록 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;15,6&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 순수 DB 조회 성능 자체를 &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;15,6&quot;&gt;2초에서 0.2초 대로 단축&lt;/b&gt;시켰으며, 결과적으로 시스템의 전체적인 내구성을(Fault Tolerance) 높였습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>cache</category>
      <category>caffeine</category>
      <category>Global</category>
      <category>local</category>
      <category>optimize</category>
      <category>Redis</category>
      <category>Spring</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/23</guid>
      <comments>https://curious-kang.tistory.com/23#entry23comment</comments>
      <pubDate>Sun, 1 Feb 2026 20:29:47 +0900</pubDate>
    </item>
    <item>
      <title>React Update로 보안 높이기</title>
      <link>https://curious-kang.tistory.com/22</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;1451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lZRIB/dJMcahJJ5o1/5zggiJQdrCszkZwaoojVDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lZRIB/dJMcahJJ5o1/5zggiJQdrCszkZwaoojVDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lZRIB/dJMcahJJ5o1/5zggiJQdrCszkZwaoojVDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlZRIB%2FdJMcahJJ5o1%2F5zggiJQdrCszkZwaoojVDk%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;1040&quot; height=&quot;1451&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;1451&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&amp;amp;pageIndex=1&amp;amp;nttId=71912&amp;amp;menuNo=205020&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&amp;amp;pageIndex=1&amp;amp;nttId=71912&amp;amp;menuNo=205020&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765763658354&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;KISA 보호나라&amp;amp;KrCERT/CC&quot; data-og-description=&quot;KISA 보호나라&amp;amp;KrCERT/CC&quot; data-og-host=&quot;www.boho.or.kr:443&quot; data-og-source-url=&quot;https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&amp;amp;pageIndex=1&amp;amp;nttId=71912&amp;amp;menuNo=205020&quot; data-og-url=&quot;http://www.boho.or.kr:80&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBR3RZ/hyZPnHcThj/kAsBalA9JH9iVvEBKaJ8ck/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270,https://scrap.kakaocdn.net/dn/MquX9/hyZOBmiUq5/GysoBHakH6dGGTjGRcPXv0/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270,https://scrap.kakaocdn.net/dn/RiM5V/hyZPwc4PQl/FFPefK2ussA0Yc2M2MdCpk/img.jpg?width=1098&amp;amp;height=328&amp;amp;face=0_0_1098_328&quot;&gt;&lt;a href=&quot;https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&amp;amp;pageIndex=1&amp;amp;nttId=71912&amp;amp;menuNo=205020&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&amp;amp;pageIndex=1&amp;amp;nttId=71912&amp;amp;menuNo=205020&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBR3RZ/hyZPnHcThj/kAsBalA9JH9iVvEBKaJ8ck/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270,https://scrap.kakaocdn.net/dn/MquX9/hyZOBmiUq5/GysoBHakH6dGGTjGRcPXv0/img.png?width=270&amp;amp;height=270&amp;amp;face=0_0_270_270,https://scrap.kakaocdn.net/dn/RiM5V/hyZPwc4PQl/FFPefK2ussA0Yc2M2MdCpk/img.jpg?width=1098&amp;amp;height=328&amp;amp;face=0_0_1098_328');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;KISA 보호나라&amp;amp;KrCERT/CC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;KISA 보호나라&amp;amp;KrCERT/CC&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.boho.or.kr:443&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=25030&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://news.hada.io/topic?id=25030&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765765935674&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;React 서버 컴포넌트의 DoS 및 소스 코드 노출 취약점 공개 | GeekNews&quot; data-og-description=&quot;React Server Components에서 서비스 거부(DoS) 및 소스 코드 노출 취약점이 새로 발견되어 공개됨이번 취약점은 원격 코드 실행(RCE) 은 불가능하지만, 서버 중단이나 코드 유출 위험이 존재영향을 받는 &quot; data-og-host=&quot;news.hada.io&quot; data-og-source-url=&quot;https://news.hada.io/topic?id=25030&quot; data-og-url=&quot;https://news.hada.io/topic?id=25030&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cK9yRG/hyZPQa40Rv/YnN4FDOgYVSY9BKimrwnD1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/nG88J/hyZPCRT6mf/24a76DkBBMOjakebkbrevk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=25030&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://news.hada.io/topic?id=25030&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cK9yRG/hyZPQa40Rv/YnN4FDOgYVSY9BKimrwnD1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/nG88J/hyZPCRT6mf/24a76DkBBMOjakebkbrevk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;React 서버 컴포넌트의 DoS 및 소스 코드 노출 취약점 공개 | GeekNews&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;React Server Components에서 서비스 거부(DoS) 및 소스 코드 노출 취약점이 새로 발견되어 공개됨이번 취약점은 원격 코드 실행(RCE) 은 불가능하지만, 서버 중단이나 코드 유출 위험이 존재영향을 받는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;news.hada.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 보안 취약점이 최근에 발생했습니다.......&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 없이 서버를 그냥 마구마구 맘대로 할 수 있다는거죠&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;그래서 지금 바.로 업데이트를 해주어야 합니다.&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;&lt;b&gt;1. npm list react react-dom&lt;/b&gt; // 현재 프로젝트에 설치된 React 버전 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. npm install react@19.1.4 react-dom@19.1.4&lt;/b&gt; //React 런타임 버전을 19.1.4 로 교체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. npm install -D @types/react@19 @types/react-dom@19&lt;/b&gt; // TypeScript 용 React 타입 정의를 19에 맞춤&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. npm ls react&lt;/b&gt; // 전체에서 React가 몇 버전으로 설치돼 있는지 확인 !&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;다들 즐거운 코딩 합시둉&lt;/p&gt;</description>
      <category>React</category>
      <category>Security</category>
      <category>레츠고</category>
      <category>보안</category>
      <category>업데이트</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/22</guid>
      <comments>https://curious-kang.tistory.com/22#entry22comment</comments>
      <pubDate>Mon, 15 Dec 2025 11:42:21 +0900</pubDate>
    </item>
    <item>
      <title>[BOJ. 2983] 개구리 공주</title>
      <link>https://curious-kang.tistory.com/21</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문제&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;트럭을 타고 이동하던 중에 상근이는 휴식을 취하기 위해서 호수에 잠시 들렸다. 호수에는 개구리가 살고 있고, 개구리는 호수 위에 떠있는 식물 N개를 점프하면서 다닌다. 오래된 전설에 따르면 개구리에게 키스를 하면 개구리는 아름다운 공주로 변한다고 한다. 일단 개구리를 잡아야 전설이 사실인지 아닌지 확인할 수 있다. 개구리를 잡아보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;호수는 2차원 평면으로 생각할 수 있고, 식물은 그 평면 위의 점으로 나타낼 수 있다. (x, y)위에 있는 개구리는 아래 네 가지 방향 중 한 방향으로 점프할 수 있다.&lt;/span&gt;&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: 'Nanum Gothic';&quot;&gt;임의의 양의 정수 P에 대해서, (x+P, y+P)로 점프할 수 있다. 이 방향을 A라고 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;임의의 양의 정수 P에 대해서, (x+P, y-P)로 점프할 수 있다. 이 방향을 B라고 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;임의의 양의 정수 P에 대해서, (x-P, y+P)로 점프할 수 있다. 이 방향을 C라고 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;임의의 양의 정수 P에 대해서, (x-P, y-P)로 점프할 수 있다. 이 방향을 D라고 한다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개구리는 네 방향 중 한 방향을 고른다. 그 다음 그 방향에 있는 가장 가까운 식물로 점프를 한다. 만약, 고른 방향에 식물이 없다면, 개구리는&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그 위치에 그대로 있는다. 개구리가 점프를 하고 난 이후에, 원래 있던 식물은 호수로 가라앉게되고 사라진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;상근이는 식물의 위치와 개구리가 고른 방향을 모두 알고 있다. 상근이는 개구리의 점프가 끝나는 꽃의 좌표를 알아낸 다음, 거기서 개구리를 잡으려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개구리의 점프가 끝나는 식물의 위치는 어디일까?&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;입력&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;첫째 줄에 식물의 수 N과 점프의 수 K가 주어진다. (1 &amp;le; N, K &amp;le; 100,000)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;둘째 줄에는 개구리가 고른 방향 K개가 주어진다. 이 방향은 'A','B','C','D'로만 이루어져 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;셋째 줄부터 N개 줄에는 식물의 좌표가 X, Y가 주어진다. (0 &amp;le; X, Y &amp;le; 1,000,000,000) 처음으로 주어지는 식물에 개구리가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;출력&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개구리의 점프가 끝나는 식물의 좌표를 출력한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음에는 평면 좌표를 그대로 사용하면서 풀었는데, 이 부분이 굉장히 어려웠고, 탐색 과정에서 시간 초과가 발생했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 결국 gg.. 치고 답을 찾으러 갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;핵심 아이디어는 2차원 평면상의 좌표들을 직선으로 만들어 문제를 풀고, 해당 직선에 있으면 가장 가까운 곳으로 이동하도록 만드는 것이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예를들면 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;원래 좌표계:
(0,2)  (1,1)  (2,0)
   ↖     ↖     ↖
(0,1)  (1,0)

변환 후 좌표 (x - y, x + y):
(0,2) &amp;rarr; (-2,2)
(1,1) &amp;rarr; (0,2)
(2,0) &amp;rarr; (2,2)

이렇게 바뀌면 ↖ 방향 애들이 다 (x + y = 2)로 같은 선에 있다는 것이다!&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;정답 코드&lt;/span&gt;&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {
    static Map&amp;lt;Integer, TreeSet&amp;lt;Integer&amp;gt;&amp;gt; xMap = new HashMap&amp;lt;&amp;gt;();
    static Map&amp;lt;Integer, TreeSet&amp;lt;Integer&amp;gt;&amp;gt; yMap = new HashMap&amp;lt;&amp;gt;();
    static int curX, curY;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int K = Integer.parseInt(st.nextToken());

        char[] dirs = br.readLine().toCharArray();

        for (int i = 0; i &amp;lt; N; i++) {
            st = new StringTokenizer(br.readLine());
            int x = Integer.parseInt(st.nextToken());
            int y = Integer.parseInt(st.nextToken());
            int tx = x - y;
            int ty = x + y;

            xMap.computeIfAbsent(tx, k -&amp;gt; new TreeSet&amp;lt;&amp;gt;()).add(ty);
            yMap.computeIfAbsent(ty, k -&amp;gt; new TreeSet&amp;lt;&amp;gt;()).add(tx);

            if (i == 0) {
                curX = tx;
                curY = ty;
            }
        }

        for (char dir : dirs) {
            move(dir);
        }

        int finalX = (curX + curY) / 2;
        int finalY = (curY - curX) / 2;
        System.out.println(finalX + &quot; &quot; + finalY);
    }

    static void move(char dir) {
        if (dir == 'A') {
            TreeSet&amp;lt;Integer&amp;gt; set = xMap.get(curX);
            if (set == null) return;
            Integer next = set.higher(curY);
            if (next != null) {
                remove(curX, curY);
                curY = next;
            }
        } else if (dir == 'B') {
            TreeSet&amp;lt;Integer&amp;gt; set = yMap.get(curY);
            if (set == null) return;
            Integer next = set.higher(curX);
            if (next != null) {
                remove(curX, curY);
                curX = next;
            }
        } else if (dir == 'C') {
            TreeSet&amp;lt;Integer&amp;gt; set = yMap.get(curY);
            if (set == null) return;
            Integer next = set.lower(curX);
            if (next != null) {
                remove(curX, curY);
                curX = next;
            }
        } else if (dir == 'D') {
            TreeSet&amp;lt;Integer&amp;gt; set = xMap.get(curX);
            if (set == null) return;
            Integer next = set.lower(curY);
            if (next != null) {
                remove(curX, curY);
                curY = next;
            }
        }
    }

    static void remove(int x, int y) {
        TreeSet&amp;lt;Integer&amp;gt; ySet = xMap.get(x);
        if (ySet != null) {
            ySet.remove(y);
            if (ySet.isEmpty()) xMap.remove(x);
        }
        TreeSet&amp;lt;Integer&amp;gt; xSet = yMap.get(y);
        if (xSet != null) {
            xSet.remove(x);
            if (xSet.isEmpty()) yMap.remove(y);
        }
    }
}&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;좌표 평면 상에 있는 점을 직선 상으로 옮겨 찾는 방법을 알게되었다. 이후 다시 복습해서 이런 기술을 완전히 내것으로 만들어야겠다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>알고리즘</category>
      <category>TreeSet</category>
      <category>정렬</category>
      <category>좌표</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/21</guid>
      <comments>https://curious-kang.tistory.com/21#entry21comment</comments>
      <pubDate>Sun, 13 Apr 2025 09:34:38 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 시뮬레이션에서 자주 나오는 것들 정리</title>
      <link>https://curious-kang.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1) 일정 범위 제거 후 빈 칸이 있으면 떨어뜨리기&lt;/p&gt;
&lt;pre id=&quot;code_1727265072739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 중력 작용 메서드
static void applyGravity() {
    // 모든 열에 대해 중력을 적용
    for (int col = 0; col &amp;lt; N; col++) {
        // 아래에서 위로 탐색하며 블록을 이동
        for (int row = N - 2; row &amp;gt;= 0; row--) {
            // 현재 위치가 검정 블록이거나 빈 칸인 경우 건너뜀
            if (map[row][col] == BLACK || map[row][col] == EMPTY) {
                continue;
            }
            // 블록을 아래로 이동
            moveBlock(row, col);
        }
    }
}

// 블록 한 개를 아래로 이동시키는 메서드
static void moveBlock(int x, int y) {
    int targetRow = x;

    // 블록을 아래로 이동 가능한 만큼 이동
    while (true) {
        targetRow++;
        
        // 보드를 벗어나는 경우 이동 중지
        if (targetRow &amp;gt;= N) {
            break;
        }
        // 검정 블록인 경우 이동 중지
        if (map[targetRow][y] == BLACK) {
            break;
        }
        // 빈 칸이 아닌 경우 이동 중지
        if (map[targetRow][y] != EMPTY) {
            break;
        }
    }

    // 이동할 위치가 원래 위치와 동일하면 이동할 필요 없음
    if (targetRow - 1 == x) {
        return;
    }

    // 블록을 아래로 이동시키고 원래 위치는 빈 칸으로 설정
    map[targetRow - 1][y] = map[x][y];
    map[x][y] = EMPTY;
}&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;2) 90도 회전&lt;/p&gt;
&lt;pre id=&quot;code_1727265184718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 90 rotate
static int[][] rotate(int[][] arr) {
    int n = arr.length;
    int m = arr[0].length;
    int[][] rotate = new int[m][n];

    for (int i = 0; i &amp;lt; rotate.length; i++) {
        for (int j = 0; j &amp;lt; rotate[i].length; j++) {
            rotate[i][j] = arr[n-1-j][i];
        }
    }

    return rotate;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/20</guid>
      <comments>https://curious-kang.tistory.com/20#entry20comment</comments>
      <pubDate>Wed, 25 Sep 2024 20:53:27 +0900</pubDate>
    </item>
    <item>
      <title>이분탐색 조금 더 이해하기</title>
      <link>https://curious-kang.tistory.com/18</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;high = mid와 high = mid - 1의 차이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 이진 탐색에서는 low = mid + 1 또는 high = mid - 1을 사용합니다. 이는 &lt;b&gt;정확한 값을 찾는&lt;/b&gt; 경우에 주로 쓰이며, 조건에 따라 탐색 범위를 한 칸씩 좁히는 방식입니다. 하지만, &lt;b&gt;lower bound&lt;/b&gt;와 &lt;b&gt;upper bound&lt;/b&gt;는 찾고자 하는 값이 &lt;b&gt;첫 번째로 등장하는 위치&lt;/b&gt; 또는 &lt;b&gt;값을 초과하는 첫 번째 위치&lt;/b&gt;를 찾는 것이 목적이므로, 다음과 같은 방식으로 처리해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;lower bound와 upper bound에서는 high = mid 또는 low = mid가 사용되는 이유:&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;lower bound&lt;/b&gt;: 배열에서 &lt;b&gt;target 이상이 처음 등장하는 위치&lt;/b&gt;를 찾습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;arr[mid] &amp;gt;= target인 경우, mid 위치가 목표 값 또는 그 이상의 값이 등장할 수 있는 위치일 수 있으므로 high = mid로 설정합니다. 즉, 현재 범위에서 이 이상을 찾으러 더 좁혀 나가는 것이죠.&lt;/li&gt;
&lt;li&gt;만약 arr[mid] &amp;lt; target인 경우, 이는 우리가 찾는 값보다 작으므로, low = mid + 1로 설정해서 왼쪽은 배제하고 오른쪽만 탐색하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;upper bound&lt;/b&gt;: 배열에서 &lt;b&gt;target을 초과하는 첫 번째 위치&lt;/b&gt;를 찾습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;arr[mid] &amp;gt; target인 경우, mid 위치는 우리가 찾는 값보다 큰 값이 있는 첫 번째 위치일 수 있으므로 high = mid로 설정합니다.&lt;/li&gt;
&lt;li&gt;반면, arr[mid] &amp;lt;= target인 경우, low = mid + 1로 설정하여 현재 위치에서 계속 오른쪽을 탐색하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로 high = mid로 설정하면, 범위가 점차 축소되면서 우리가 찾는 &quot;최초로 등장하는 위치&quot; 또는 &quot;초과하는 첫 번째 위치&quot;를 정확하게 찾아낼 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&amp;gt;=와 &amp;gt;의 구분&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;**lower bound**는 **&amp;gt;=**를 사용해서 찾는 값이 처음 등장하는 위치를 구합니다. 즉, 배열에서 찾는 값과 같거나 큰 값이 처음 등장하는 위치를 찾아내는 것이 목적입니다.&lt;/li&gt;
&lt;li&gt;**upper bound**는 **&amp;gt;**를 사용해서 &lt;b&gt;찾는 값보다 큰 값이 처음 등장하는 위치&lt;/b&gt;를 구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 값의 차이는 &lt;b&gt;같은 값이 배열에서 몇 번 나왔는지&lt;/b&gt;를 계산하는 데 사용됩니다. 즉, upper bound - lower bound는 배열에서 해당 값이 몇 번 등장했는지를 계산하게 됩니다. 이 방식은 &lt;b&gt;정렬된 배열에서 중복된 값의 개수를 세는&lt;/b&gt; 데 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시로 이해해보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 배열 arr = [1, 2, 2, 2, 3, 4]에서 target = 2일 때:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;lower bound&lt;/b&gt;는 2 이상이 처음 나오는 위치이므로 arr[1]이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;upper bound&lt;/b&gt;는 2보다 큰 값이 처음 나오는 위치이므로 arr[4]이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, upper bound - lower bound = 4 - 1 = 3이 되며, 배열에서 값 2가 3번 등장한 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, upper - lower를 통해 배열에서 특정 값의 등장 횟수를 정확히 셀 수 있는 것이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;**high = mid**는 lower/upper bound에서 사용되어, 값을 초과하거나 최소 값을 찾기 위해 범위를 좁히는 방식입니다.&lt;/li&gt;
&lt;li&gt;**&amp;gt;=와 &amp;gt;**를 구분하는 이유는 lower bound와 upper bound를 정확히 계산하기 위해서이며, 이 차이를 통해 특정 값의 등장 횟수를 구할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>알고리즘</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/18</guid>
      <comments>https://curious-kang.tistory.com/18#entry18comment</comments>
      <pubDate>Sun, 8 Sep 2024 17:11:50 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘, Java] 꽤나 사용하는 Java 메서드 알아보기</title>
      <link>https://curious-kang.tistory.com/17</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘을 풀다보면 꽤나 유용하게 사용하는 메서드가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것을 알아보고 어떻게 구현돼 있는지 어떻게 최적화 되어 있는지 확인해보도록 하죠.&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;div style=&quot;background-color: #282c34; color: #faecec;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Arrays.binarySearch&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다.&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;이 메서드는 이분탐색을 편리하게 해주는 메서드이며, BOJ 18869번을 풀 때 다른 사람의 풀이를 통해 알게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 내부로 들어가게 되면 binarySearch를 보면 아래와 같습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #faecec;&quot;&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public static int binarySearch(int[] a, int key) {
    return binarySearch0((int[])a, 0, a.length, (int)key);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 내부에 binarySearch0 으로 구현된 로직입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725172178095&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static int binarySearch0(int[] a, int fromIndex, int toIndex, int key) {
    int low = fromIndex;    // 탐색의 시작 인덱스
    int high = toIndex - 1; // 탐색의 끝 인덱스 (toIndex는 범위 밖이므로 -1)

    // low가 high보다 작거나 같은 동안 반복 (탐색 범위가 유효한 동안)
    while (low &amp;lt;= high) {
        // 중간 인덱스를 계산 (overflow 방지를 위해 low와 high의 합을 2로 나누는 대신 shift 연산 사용)
        int mid = low + high &amp;gt;&amp;gt;&amp;gt; 1;
        int midVal = a[mid]; // 중간 인덱스의 값

        if (midVal &amp;lt; key) {
            // 중간 값이 찾고자 하는 값보다 작다면, 탐색 범위를 오른쪽으로 이동
            low = mid + 1;
        } else {
            if (midVal &amp;lt;= key) {
                // 중간 값이 찾고자 하는 값과 같다면, 해당 인덱스를 반환
                return mid;
            }
            // 중간 값이 찾고자 하는 값보다 크다면, 탐색 범위를 왼쪽으로 이동
            high = mid - 1;
        }
    }

    // 키를 찾지 못한 경우, 삽입 위치를 결정하기 위해 -(삽입 인덱스 + 1)을 반환
    return -(low + 1);
}&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;이런 식으로 구현이 돼 있습니다. Java를 완전 깊게 알고있는 전문가들이 짜놓은 메서드기 때문에 이런걸 보고 학습하시는 것도 좋은 방향이라고 생각합니다.&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;2 번째는 깊은 복사가 필요할 때 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 1 번 clone()을 사용하는 경우&lt;/p&gt;
&lt;pre id=&quot;code_1725172576130&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[][] original = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

int[][] copy = new int[original.length][];
for (int i = 0; i &amp;lt; original.length; i++) {
    copy[i] = original[i].clone(); // 각 행을 개별적으로 복사
}&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;- 2번 Arrays.copyOf() 를 사용하는 경우&lt;/p&gt;
&lt;pre id=&quot;code_1725172606034&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Arrays;

int[][] original = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

int[][] copy = new int[original.length][];
for (int i = 0; i &amp;lt; original.length; i++) {
    copy[i] = Arrays.copyOf(original[i], original[i].length); // 각 행을 개별적으로 복사
}&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;여기까지가 생각보다 알고리즘을 풀다보면 꽤나 만나게 되는 유형이고, 좀 더 쉽게 사용할 수 있을 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고하셔서 풀이할 때 도움이 되셨으면 합니다.&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;오늘도 좋은 하루되세요~&lt;/p&gt;</description>
      <category>알고리즘</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/17</guid>
      <comments>https://curious-kang.tistory.com/17#entry17comment</comments>
      <pubDate>Sun, 1 Sep 2024 15:38:01 +0900</pubDate>
    </item>
    <item>
      <title>알고리즘에서 많이 쓰는 방식</title>
      <link>https://curious-kang.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;### 2차원 배열 ArrayList로 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArrayList&amp;lt;ArrayList&amp;lt;Integer&amp;gt;&amp;gt; arr = new ArrayList&amp;lt;&amp;gt;();&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ArrayList&amp;lt;int[]&amp;gt; arr = new ArrayList&amp;lt;&amp;gt;();&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;### 사용자 입력 값 받기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. String[] s = br.readLine().split(&quot; &quot;);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. String s = br.readLine();&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;2의 경우 s.charAt(int) 로 값을 사용하면 됩.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 될 경우 char 값으로 나오기 때문에 일반 동일성(identity)만 따져도 됨&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;즉, char a = 'b', char b = 'b' 라고 할 때 a == b 로 사용이 가능하다.&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;---&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 포스트는 코딩 테스트를 준비하면서 계속 업데이트 할 예정입니다 !&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글로도 알려주시면 추가해서 다른 개발자 분들이 볼 수 있도록 업데이트 하겠습니다.&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;감사합니다.&lt;/p&gt;</description>
      <category>알고리즘</category>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/16</guid>
      <comments>https://curious-kang.tistory.com/16#entry16comment</comments>
      <pubDate>Sun, 25 Aug 2024 23:14:03 +0900</pubDate>
    </item>
    <item>
      <title>JWT mocking In Jest</title>
      <link>https://curious-kang.tistory.com/14</link>
      <description>&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;jest.mock(&quot;@/utils/jwt&quot;, () =&amp;gt; ({
    jwtAuth: (req: any, res: any, next: any) =&amp;gt; {
        req.admin = {adminId : 1, role : &quot;admin&quot;};
        req.user = { userId: 2, role: &quot;manager&quot; };
        next();
    },
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&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;</description>
      <author>나는 나야</author>
      <guid isPermaLink="true">https://curious-kang.tistory.com/14</guid>
      <comments>https://curious-kang.tistory.com/14#entry14comment</comments>
      <pubDate>Tue, 16 Jul 2024 08:50:02 +0900</pubDate>
    </item>
  </channel>
</rss>