<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>평생쓰는 IT 개발 노트</title>
    <link>https://soonmin.tistory.com/</link>
    <description>새로운 블로그로 이사했습니다   &amp;rarr; https://kmindev.github.io</description>
    <language>ko</language>
    <pubDate>Thu, 30 Jul 2026 19:18:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kmindev</managingEditor>
    <image>
      <title>평생쓰는 IT 개발 노트</title>
      <url>https://tistory1.daumcdn.net/tistory/5936354/attach/986dd008c0ab41fdb37772b99a09e58c</url>
      <link>https://soonmin.tistory.com</link>
    </image>
    <item>
      <title>[JPA] 주인이 아닌 OneToOne 관계에서 LAZY가 동작하지 않는 이유</title>
      <link>https://soonmin.tistory.com/184</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글(board)과 게시글통계정보(boardStatistic) 1:1 양방향 연관관계로 설계된 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계의 주인은 boardStatistic이며, board는 mappedBy를 통해 참조만 하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로딩 전략은 모두 LAZY로 설정되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1767427525727&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Table(name = &quot;board&quot;)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board extends BaseEntity {

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

    @OneToOne(mappedBy = &quot;board&quot;, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private BoardStatistic boardStatistic;

  	// ..
}

@Entity
@Table(name = &quot;board_statistic&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BoardStatistic {

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

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;board_id&quot;, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Board board;

    // ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예상했던 동작&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Board를 페이징 조회할 때, BoardStatistic은 LAZY로 설정되어 있기에, 실제 board.getBoardStatistic()을 호출하기 전까지 boardStatistic을 조회하는 쿼리가 발생하지 않을 것으로 예상했다.&lt;/p&gt;
&lt;pre id=&quot;code_1773575328156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Page&amp;lt;Board&amp;gt; findAll(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 동작(N+1 발생)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 로그를 확인해보니, 의도와 다르게 Board를 한 건마다 BoardStatistic을 조회하는 쿼리가 발생하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;board가 20개라면, boardStatistic 조회하는 쿼리가 20개 발생될 수 있는 상황이다.&lt;/p&gt;
&lt;pre id=&quot;code_1773575894621&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 1. Board 페이징 조회
select * from board b1_0 order by b1_0.created_at desc limit ?

-- 2. 각 Board마다 BoardStatistic을 개별 조회 (N+1 발생!)
select * from board_statistic bs1_0 where bs1_0.board_id = ?
select * from board_statistic bs1_0 where bs1_0.board_id = ?
select * from board_statistic bs1_0 where bs1_0.board_id = ?&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;내가 평소 알고 있던, JPA 지식과 실제 동작에서 차이가 나는 지점은 2가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Lazy로 설정했음에도, 사용여부와 상관없이 연관 엔티티를 조회하는 것일까?&lt;/li&gt;
&lt;li&gt;default_batch_fetch_size = 100으로 설정했음에도, 왜 IN쿼리가 아닌 단건 조회가 나가는 것일까?&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 원인 파악&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 Lazy 설정을 무시하고 즉시 쿼리가 나가는걸까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 JPA가 사용하는 프록시 객체의 한계 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계의 주인인 boardStatstic은 board_id(FK)를 직접 가지기에 Board의 존재 여부를 즉시 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, board는 boardStatstic이 나를 참조하고 있는지에 대한 정보를 전혀 갖고 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jpa 입장에서는 board를 조회할 때 boardStaistic 필드에 프록시를 넣어야 할지, null을 넣어야 할지 결정해야 하는데, board 테이블만 봐서 알 방법이 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 jpa는 존재 여부를 확인하기 위해 boardStatistic을 직접 조회하게 되고, 그로 인해 LAZY 설정이 무시된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그럼 왜 IN 쿼리는 안나가고, 단건 조회로 N번만큼 조회하는걸까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;default_batch_fetch_size는 이미 생성된 프록시들을 한꺼번에 초기화할 때 사용되는 설정이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상황은 프록시 자체를 생성할 수 있는지 (null 여부) 를 확인하려고 쿼리를 날리는 단계로, 해당 설정과 무관한 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 방안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 생각한 방안은 3가지이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Fetch Join 또는 @EntityGraph 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;board를 조회할 때 &lt;span style=&quot;text-align: start;&quot;&gt;boardStatistic을&lt;span&gt; 함께 조회하도록 설정하는 방법이다. 1번의 쿼리로 데이터를 모두 조회할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;하지만, 구조적인 문제를 고치지 않으면, board를 조회하는 모든 메서드에서 해당 설정을 추가해줘야 하므로 매우 번거롭다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773577066507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = {&quot;boardStatistic&quot;})
Page&amp;lt;Board&amp;gt; findAll(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773578531028&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select 
    b1_0.id, 
    -- ... Board의 다른 컬럼들,
    -- ... BoardStatistic의 컬럼들
from 
    board b1_0 
left join 
    board_statistic bs1_0 
on 
    b1_0.id = bs1_0.board_id 
order by 
    b1_0.created_at desc 
limit ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 연관관계 주인(FK) 위치 수정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직상 board에서 boardStatistic을 조회하는 빈도가 훨씬 높다면, DB설계를 변경하여 board 테이블이 board_statistic_id(FK)를 갖도록 설계를 변경하는 방법이다.&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;이렇게 하면, board 테이블에 FK가 존재하기에, JPA는 DB 조회시점에 해당 컬럼이 null인지 아닌지 바로 알 수 있으므로 불필요한 쿼리가 발생하지 않는다.&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;하지만 게시글통계가 게시글의 종속적인 구조이기에 DB관점에서 boardStatistic이 board_id(FK)를 갖는 것이 더 자연스럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBA가 별로 좋아하지 않을 것 같다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 양방향 관계 고민&lt;/h4&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;양방향 매핑을 포기하는 것이다.&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;/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;지금 같은 게시글 - 게시글통계 구조에서 굳이 양방향 매핑을 유지해 예상치 못한 쿼리가 발생하여 성능이 저하되거나, 매번 fetch join 등을 고민하기 보다는, 단방향으로 끊어내고 필요한 시점에만 따로 조회하는 것이 훨씬 안전하고 깔끔한 설계라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773578685531&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Table(name = &quot;board&quot;)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board extends BaseEntity {

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

}

@Entity
@Table(name = &quot;board_statistic&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BoardStatistic {

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

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;board_id&quot;, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    private Board board;

    // ..
}&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;&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>
      <category>Programming/Spring</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/184</guid>
      <comments>https://soonmin.tistory.com/184#entry184comment</comments>
      <pubDate>Sun, 15 Mar 2026 21:58:15 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] JPA Soft Delete 지원하는 여러 방식들에 대한 고민(@SoftDelete, @SqlDelete, @SQLRestriction)</title>
      <link>https://soonmin.tistory.com/185</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 실제 물리적으로 삭제하지 않고, 논리적 삭제(soft delete) 해야하는 상황이 자주 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황에 대처하기 위해 JPA에서는 여러 soft delete 적용 방식이 사용된다.&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;h2 data-ke-size=&quot;size26&quot;&gt;2. Soft Delete 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 더티 체킹을 이용한 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티의 상태를 변경하여, 트랜잭션 종료 시점에 JPA의 더티 체킹에 의해 UPDATE 쿼리를 발생시키는 방법이다.&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;pre id=&quot;code_1767511863719&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Table
@Entity
@Getter
public class Review extends SoftDeleteBaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = &quot;is_deleted&quot;, nullable = false, columnDefinition = &quot;tinyint&quot;)
    private boolean isDeleted = false;

    public void delete() {
        this.isDeleted = true;
    }
}

review.delete(); // 삭제 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 SQL&lt;/p&gt;
&lt;pre id=&quot;code_1767513600088&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE review SET is_deleted = true WHERE id = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. @SqlDelete를 이용한 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@SqlDelete 애너테이션과 jpa repository의 delete() 메서드를 호출하면 delete 대신 update sql이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;{h-schema}, {h-table}을 사용 시 사용시 상속 구조에서도 재사용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자의 실수로 물리 삭제하는 사고를 방지할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1767512140184&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Table
@Entity
@SQLDelete(sql = &quot;UPDATE {h-schema}{h-table} SET is_deleted = true WHERE id = ?&quot;)
@Getter
public class Review {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = &quot;is_deleted&quot;, nullable = false, columnDefinition = &quot;tinyint&quot;)
    private boolean isDeleted = false;
}

reviewRepository.delete(review);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 조회 문제와 자동 필터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;soft delete 정책 사용 시 가장 큰 고민은 데이터를 조회할 때이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제되지 않은 데이터를 조회하려면 where is_deleted = false 와 같은 조건을 항상 붙혀줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 hibernate는 자동 필터링 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. @Where, @SQLRestriction을 통한 자동 필터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 애너테이션을 사용하면 자동으로 필터링이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Where은 hibernate 6.3부터 deprecated 되었고, 이를 대체하는 애너테이션이&amp;nbsp; @SQLRestriction이다.&lt;/p&gt;
&lt;pre id=&quot;code_1767514173845&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
// @Where(clause = &quot;is_deleted = false&quot;) // deprecated
@SQLRestriction(&quot;is_deleted = false&quot;)
public class Review {
}&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;조회 SQL&lt;/p&gt;
&lt;pre id=&quot;code_1767514262398&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM review WHERE is_deleted = false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;ctext-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-2. @SoftDelete 애너테이션&lt;/h3&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;hibernate 6.4에 도입된 soft delete 통합 애너테이션으로 @SqlDelete + @SQLRestriction 동일한 효과를 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1767514384748&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@SoftDelete
public class Review {
}&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;&lt;br /&gt;해당 애너테이션도 주의할 점이 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;xxxToOne 관계에서 패치 전략을 Lazy로 사용하면&amp;nbsp; 아래와 같은 예외가 발생한다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 LAZY 로딩은 프록시 객체로 전제로 하지만, @SoftDelete는 프록시 객체로는 엔티티가 유효한지 (is_deleted = false) 검증된 객체가 필요하기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767517750602&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UnsupportedMappingException: To-one attribute (com.bbangle.bbangle.seller.domain.Seller.store) cannot be mapped as LAZY as its associated entity is defined with @SoftDelete&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@NotFound를 사용하여 조건에 부합하지 않는 객체라면 예외를 던지지 말고 null로 처리하도록 해결할 수 있지만, 근본적인 해결책은 아니다.&lt;/p&gt;
&lt;pre id=&quot;code_1767518301930&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
private Store store;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;#333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 자동 필터링 방식이 항상 좋은가?&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 그렇지 않다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Where, @SQLRestriction, @SoftDelete와 같은 자동 필터링 방식은 분명히 편리하긴하다.&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;p data-ke-size=&quot;size16&quot;&gt;자동 필터링이 적용된 엔티티는 모든 조회 쿼리에 is_deleted = false 조건이 강제로 적용된다.&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;또한, 현실적으로 삭제된 데이터만 100% 조회하는 서비스는 거의 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러한 경우라면 soft delete 자체를 사용할 필요가 없으며, hard delete가 더 명확한 선택이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&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;무조건 좋은 방식은 없고, 서비스의 성격과 요구사항에 맞는 soft delete 전략을 선택하는 것이 중요하다.&lt;/p&gt;</description>
      <category>Programming/Spring</category>
      <category>JPA</category>
      <category>soft delete</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/185</guid>
      <comments>https://soonmin.tistory.com/185#entry185comment</comments>
      <pubDate>Sun, 4 Jan 2026 17:47:29 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Filter vs Interceptor 차이</title>
      <link>https://soonmin.tistory.com/174</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 횡단 관심사(로깅, 트랜잭션, 인증/인가 등)를 분리하기 위해 Spring AOP를 많이 사용한다. Spring AOP 방식 외에도 관심사를 분리하기 위해 Filter 또는 Interceptor를 사용하기도 한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Filter와 Interceptor가 무엇인지 알아보고, 어떤 차이가 있는지 살펴보자.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 요청 처리 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터와 Interceptor를 알아보기 전에 일반적인 Spring Boot 서버는 클라이언트 요청을 어떻게 처리하는지 간략하게 짚고 넘어가자.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;클라이언트가 요청을 보내면 어떻게 처리될까?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 브라우저에서 요청을 보내면, 가장 뭔저 톰캣(WAS)이 이를 받는다. 톰캣은 웹 서버이자 서블릿 컨테이너로서 요청을 적절한 서블릿을 찾아 전달한다. 이때 Spring 컨테이너에 의해 등록된 서블릿이 DispatcherServlet이며, 톰캣은 해당 서블릿에 요청을 전달한다. DispatcherServlet은 프런트 컨트롤러 역할을 하면 HandlerMapping을 통해 적절한 Controller를 찾는다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요청 흐름 구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tNcxP/dJMb9LRv3xx/aNDVUOMMmelhNUYgJqHnk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tNcxP/dJMb9LRv3xx/aNDVUOMMmelhNUYgJqHnk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tNcxP/dJMb9LRv3xx/aNDVUOMMmelhNUYgJqHnk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtNcxP%2FdJMb9LRv3xx%2FaNDVUOMMmelhNUYgJqHnk1%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;3328&quot; height=&quot;1148&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Filter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 서블릿 컨테이너 레벨에서 동작하며, 요청이 DispatcherServlet에 도달하기 전이나 응답이 나가는 시점에 실행된다.&lt;br /&gt;즉, 톰캣이 요청을 받은 뒤 DispatcherServlet에 전달하기 전에 URL이 패턴이 일치하면 필터를 거치게 된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또한, Filter는 Spring 컨테이너에 의해 관리되지 않는다.&lt;br /&gt;그럼에도, Spring 빈과 연계가 가능한데, DelatingFilterProxy가 등장하고 가능해졌다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요청 흐름 구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccijCJ/dJMb9P0GIRd/z5DdQBuIGq8OSdRkFpvKd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccijCJ/dJMb9P0GIRd/z5DdQBuIGq8OSdRkFpvKd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccijCJ/dJMb9P0GIRd/z5DdQBuIGq8OSdRkFpvKd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccijCJ%2FdJMb9P0GIRd%2Fz5DdQBuIGq8OSdRkFpvKd0%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;3328&quot; height=&quot;1148&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter의 메서드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Filter는 인터페이스이며 Spring Security에 사용하는 많은 필터들은 기본적으로 Filter 인터페이스를 구현하여 사용하고 있다.&lt;/li&gt;
&lt;li&gt;필요하다면, Filter를 구현하여 등록할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface Filter {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;init()&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터 객체를 초기화하고 서비스에 추가하기 위해 메서드&lt;/li&gt;
&lt;li&gt;서블릿 컨테이너가 필터를 처음 생성할 때 1회 호출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;default void init(FilterConfig filterConfig) throws ServletException {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;doFilter()&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL 패턴이 일치하는 요청이 DispatcherServlet에 전달되기 전에 실행되는 메서드이다.&lt;/li&gt;
&lt;li&gt;FilterChain 객체의 doFilter를 통해 다음 대상으로 요청을 전달한다.&lt;/li&gt;
&lt;li&gt;doFilter() 메서드에 요청 전/후로 필요한 과정을 작성해 처리하도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;destory()&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터 객체를 제거하고 자원을 반환하기 위한 메서드이다.&lt;/li&gt;
&lt;li&gt;서블릿 컨테이너가 필터를 종료할 때 한 번 호출되며, 이후에는 더 이상 doFilter()가 실행되지 않는다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;default void destroy() {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Interceptor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉트는 Spring에서 제공하는 기술로써, DispatcherServlet이 컨트롤러를 호출하기 전이나 응답이 나가는 시점에 실행된다.&lt;br /&gt;즉, Spring 컨테이너 위에서 동작한다. Interceptor는 HandlerExecutionChain에 의해 관리되는데, 여기서 하나 이상의 Interceptor가 있다면 순처적으로 Interceptor를 거쳐 Controller를 실행한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;요청 흐름 구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0pc2L/dJMb8XqO733/7gnZUmvUIK2Obp4YNjtJhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0pc2L/dJMb8XqO733/7gnZUmvUIK2Obp4YNjtJhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0pc2L/dJMb8XqO733/7gnZUmvUIK2Obp4YNjtJhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0pc2L%2FdJMb8XqO733%2F7gnZUmvUIK2Obp4YNjtJhK%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;3328&quot; height=&quot;1148&quot; data-origin-width=&quot;3328&quot; data-origin-height=&quot;1148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Interceptor 메서드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Interceptor는 Spring에서 제공하는 인터페이스로, 이를 구현해서 사용하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface HandlerInterceptor {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;preHandle()&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러가 호출되기 전에 실행된다.&lt;/li&gt;
&lt;li&gt;따라서, 인증, 권한 검증, 로깅 등 컨트롤러 이전에 처리해야 하는 작업을 추가하면 된다.&lt;/li&gt;
&lt;li&gt;메서드의 handler 파라미터는 HandlerMethod 타입으로, 요청을 처리할 컨트롤러 메서드 정보를 담고있다.&lt;/li&gt;
&lt;li&gt;반환값이 boolean 형식이며 true이면 다음 단계로 진행되고, false이면 작업이 중단되며 컨트롤러나 다음 interceptor가 실행되지 않는다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;postHandle()&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러 호출된 후(view 렌더링 전, 응답 전)에 실행된다.&lt;/li&gt;
&lt;li&gt;컨트롤러 반환한 데이터를 가공하거나, View에 전달할 model 속성을 조작할 수 있다.&lt;/li&gt;
&lt;li&gt;메서드의 ModelAndView 파라미터는 view와 model 정보를 포함하고 있으며, 템플릿 엔진 기반 view를 사용할 때 필요한 객체이다.&lt;/li&gt;
&lt;li&gt;하위 계층에서 예외가 발생하면 호출되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;afterCompletion()&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 작업이 완료된 후에 실행된다.&lt;/li&gt;
&lt;li&gt;요청 처리 중에 사용한 리소스 반환할 때 사용하기 적합하다.&lt;/li&gt;
&lt;li&gt;하위 계층에서 예외가 발생하더라도 반드시 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Filter vs Interceptor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 Filter와 Interceptor 모두 요청을 전&amp;middot;후 처리를 담당할 수 있다.&lt;br /&gt;하지만, Filter와 Interceptor를 관리하는 컨테이너가 다르기 때문에 동작 방식과 활용 범위에서 차이가 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;Filter는 서블릿 컨테이너에서 관리되고, Interceptor는 스프링 컨테이너에서 관리된다.&lt;br /&gt;Filter는 서블릿 컨테이너에서 관리되기 때문에 스프링에서 처리하는 기능들을 적용받을 수 없다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그럼에도 Spring에서 Filter를 스프링 빈으로 등록하거나, 빈을 주입받는 경우가 있다. 이는 DelegatingFilterProxy가 등장하고부터 가능하게 되었다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외 핸들링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 보통 RestControllerAdvice를 통해 예외를 처리한다.&lt;br /&gt;interceptor는 스프링 컨테이너에서 동작하기 때문에 Interceptor에서 예외가 발생하면 RestControllerAdvice가 예외를 잡아 처리한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class CustomExceptionHandler {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ExceptionHandler
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public ResponseEntity&amp;lt;?&amp;gt; handleException(Exception e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 예외 처리
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;반면, Filter는 서블릿 컨테이널 레벨에서 동작하기 때문에 Spring MVC 위에서 동작하는 RestControllerAdvice는 예외를 잡을 수가 없다. 필터에서 발생한 예외는 필터 내부에서 직접 처리해야 500 에러를 방지할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 필터 로직
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chain.doFilter(request, response);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (Exception e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 예외 처리 추가 필요
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Request / Response 조작 여부&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter와 Interceptor 모두 Request와 Response 내부 상태(헤더, 속성)를 변경할 수 있다.&lt;br /&gt;하지만, 객체 자체를 교체할 수 있는지 여부에 차이가 있다.&lt;br /&gt;Filter는 내부 상태 변경뿐 아니라 doFilter() 메서드를 통해 request / response 객체를 전달하여 다른 객체로 바꿔칠 수 있다.&amp;nbsp;&lt;br /&gt;반면, Interceptor는 메서드 처리 방식으로 인해 Interceptor는 다른 객체로 바꿔칠 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// Filter
public class CustomFilter implements Filter {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Request / Response를 다른 객체로 교체 가능
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chain.doFilter(new CustomRequest(), new CustomeResponse());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}

// Interceptor
public class CustomInterceptor implements HandlerInterceptor {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Override
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filter, Interceptor 용도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter는 클라이언트의 정적 리소스 요청을 포함한 모든 요청을 거친다.&lt;br /&gt;반면, Interceptor는 Spring MVC 레벨에서 동작하며 Controller에 매핑되는 요청에만 거친다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;예를 들어, Spring Security를 사용하여 login / logout과 같은 작업을 필터(서블릿) 레벨에서 처리한다면, login / logout 요청에 대해서는 interceptor가 호출되지 않는다. 이러한 특징을 이해하면 상황에 따라 interceptor, filter 적절히 선택하여 사용할 수 있다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Filter의 용도&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 요청에 대해 공통된 작업을 수행해야 하는 경우&lt;/li&gt;
&lt;li&gt;공통된 보안 및 인증 인가 관련 작업&lt;/li&gt;
&lt;li&gt;모든 요청에 대한 로깅&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Interceptor의 용도&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller 요청에 대한 공통된 작업을 수행해야 하는 경우&lt;/li&gt;
&lt;li&gt;세부적인 보안 및 인증/인가 공통 작업&lt;/li&gt;
&lt;li&gt;API 호출에 대한 로깅 또는 감사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 정리&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 148px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 21.8217%; height: 16px; text-align: justify;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 34.3798%; height: 16px; text-align: justify;&quot;&gt;Filter&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%; height: 16px; text-align: justify;&quot;&gt;Interceptor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 21.8217%; height: 22px; text-align: justify;&quot;&gt;관리 주체&lt;/td&gt;
&lt;td style=&quot;width: 34.3798%; height: 22px; text-align: justify;&quot;&gt;서블릿 컨테이너&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%; height: 22px; text-align: justify;&quot;&gt;스프링 컨테이너&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 21.8217%; height: 22px; text-align: justify;&quot;&gt;실행 시점&lt;/td&gt;
&lt;td style=&quot;width: 34.3798%; height: 22px; text-align: justify;&quot;&gt;&amp;nbsp;DispatcherServlet 전&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%; height: 22px; text-align: justify;&quot;&gt;DispatcherServlet 이후&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 21.8217%; height: 22px; text-align: justify;&quot;&gt;예외 처리&lt;/td&gt;
&lt;td style=&quot;width: 34.3798%; height: 22px; text-align: justify;&quot;&gt;내부 처리 필요&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%; height: 22px; text-align: justify;&quot;&gt;RestControllerAdvice 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 44px;&quot;&gt;
&lt;td style=&quot;width: 21.8217%; height: 44px; text-align: justify;&quot;&gt;request / response 객체 교체 여부&lt;/td&gt;
&lt;td style=&quot;width: 34.3798%; height: 44px; text-align: justify;&quot;&gt;가능&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%; height: 44px; text-align: justify;&quot;&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 21.8217%; height: 22px; text-align: justify;&quot;&gt;주요 활용&lt;/td&gt;
&lt;td style=&quot;width: 34.3798%; height: 22px; text-align: justify;&quot;&gt;모든 요청에 대한 공통된 작업을 수행&lt;/td&gt;
&lt;td style=&quot;width: 43.7984%; height: 22px; text-align: justify;&quot;&gt;Controller 요청에 대한 공통된 작업을 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;참고자료&lt;br /&gt;&lt;a href=&quot;https://gowoonsori.com/blog/spring/architecture/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://gowoonsori.com/blog/spring/architecture&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://sigridjin.medium.com/servletcontainer%EC%99%80-springcontainer%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%B8%EA%B0%80-626d27a80fe5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://sigridjin.medium.com/servletcontainer%EC%99%80-springcontainer%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%B8%EA%B0%80-626d27a80fe5&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://mangkyu.tistory.com/173&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://mangkyu.tistory.com/173&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming/Spring</category>
      <category>C</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/174</guid>
      <comments>https://soonmin.tistory.com/174#entry174comment</comments>
      <pubDate>Mon, 20 Oct 2025 19:31:45 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 1238.파티 - Java</title>
      <link>https://soonmin.tistory.com/181</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1238&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1238&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제 설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 N개 숫자로 구분된 각각의 마을이 존재하며, 각 마을에는 한 명의 학생이 살고있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 마을을 이어주는 시간이 T만큼 걸리는 단방향 도로가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학생들은 파티를 위해 X마을로 모여서 파티하고 끝나며 다시 자기가 살던 마을로 돌아간다.&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;다익스트라를 풀어본 적이 없다면, 아래 문제를 먼저 풀어보는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://soonmin.tistory.com/179&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://soonmin.tistory.com/179&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759149555777&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;[백준] 1753. 최단경로 - JAVA&quot; data-og-description=&quot;1. 문제https://www.acmicpc.net/problem/1753 2. 풀이 과정이 문제는 방향 그래프와 시작점 K가 주어진다. 시작점에서 출발해 다른 모든 정점까지의 최단 경로를 구하는 문제이다.경로가 존재하지 않으면 IN&quot; data-og-host=&quot;soonmin.tistory.com&quot; data-og-source-url=&quot;https://soonmin.tistory.com/179&quot; data-og-url=&quot;https://soonmin.tistory.com/179&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bf5aEQ/hyZJ1kU6DE/0zDvYxn3bfkAyRXDKcIOO1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bGzaxl/hyZJ3bZKDH/EmOQJ9ihD2hp9ebdRnHba1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/V0ddK/hyZKrWDySy/EEEpwPDFmFavd5iq6w3CxK/img.png?width=1152&amp;amp;height=580&amp;amp;face=0_0_1152_580&quot;&gt;&lt;a href=&quot;https://soonmin.tistory.com/179&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://soonmin.tistory.com/179&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bf5aEQ/hyZJ1kU6DE/0zDvYxn3bfkAyRXDKcIOO1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bGzaxl/hyZJ3bZKDH/EmOQJ9ihD2hp9ebdRnHba1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/V0ddK/hyZKrWDySy/EEEpwPDFmFavd5iq6w3CxK/img.png?width=1152&amp;amp;height=580&amp;amp;face=0_0_1152_580');&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;[백준] 1753. 최단경로 - JAVA&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 문제https://www.acmicpc.net/problem/1753 2. 풀이 과정이 문제는 방향 그래프와 시작점 K가 주어진다. 시작점에서 출발해 다른 모든 정점까지의 최단 경로를 구하는 문제이다.경로가 존재하지 않으면 IN&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;soonmin.tistory.com&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;이 문제를 푸는 기본 아이디어는 각 마을 i에서 X까지의 최단 거리 + X에서 i까지의 최단 거리가 i 학생의 왕복 시간이다.&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;h3 data-ke-size=&quot;size23&quot;&gt;처음 풀었던 방식 (비효율적)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 마을 i에서 X까지 다익스트라를 N번 실행하여 각각의 i 마을에 대한 최단 거리를 구해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, X 마을에서 모든 마을까지 다익스트라 1번을 실행하여 최단 거리를 구해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 다익스트라를 N + 1번 실행하여 왕복거리를 구했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759149900248&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// i 마을 =&amp;gt; X
int[] result = new int[N + 1];
for (int i = 1; i &amp;lt;= N; i++) {
    int[] dist = dijkstra(i); // i = 출발지점
    result[i] = dist[X];
}

// X =&amp;gt; i 마을
int[] dist = dijkstra(X); // X = 출발지점
for (int i = 1; i &amp;lt;= N; i++) {
    result[i] += dist[i];
}&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;결과는 성공!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;29&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EvZ7J/btsQVDMKhfx/WbkqFljT566Fo22ZhlToK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EvZ7J/btsQVDMKhfx/WbkqFljT566Fo22ZhlToK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EvZ7J/btsQVDMKhfx/WbkqFljT566Fo22ZhlToK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEvZ7J%2FbtsQVDMKhfx%2FWbkqFljT566Fo22ZhlToK0%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;638&quot; height=&quot;29&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;29&quot;/&gt;&lt;/span&gt;&lt;/figure&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;pre id=&quot;code_1759150067694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    static int N;
    static int M;
    static int X;
    static int INF = 10000 * 100;
    static List&amp;lt;List&amp;lt;Point&amp;gt;&amp;gt; graph = new ArrayList&amp;lt;&amp;gt;();

    public static void main(String[] args) throws IOException {
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        X = Integer.parseInt(st.nextToken());

        for (int i = 0; i &amp;lt;= N; i++) {
            graph.add(new ArrayList&amp;lt;&amp;gt;());
        }

        for (int i = 0; i &amp;lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            int t = Integer.parseInt(st.nextToken());
            graph.get(start).add(new Point(end, t));
        }

        // N -&amp;gt; X
        int[] result = new int[N + 1];
        for (int i = 1; i &amp;lt;= N; i++) {
            int[] dist = dijkstra(i);
            result[i] = dist[X];
        }

        // X -&amp;gt; N
        int[] dist = dijkstra(X);
        for (int i = 1; i &amp;lt;= N; i++) {
            result[i] += dist[i];
        }

        int max = Arrays.stream(result).max().getAsInt();
        bw.write(max + &quot;\n&quot;);
        bw.flush();
        bw.close();
        br.close();
    }

    static int[] dijkstra(int start) {
        int[] dist = new int[N + 1];
        boolean[] visited = new boolean[N + 1];
        Arrays.fill(dist, INF);

        PriorityQueue&amp;lt;Point&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;((a, b) -&amp;gt; a.w - b.w);
        pq.offer(new Point(start, 0));
        dist[start] = 0;

        while (!pq.isEmpty()) {
            Point cur = pq.poll();
            if (visited[cur.x]) {
                continue;
            }
            visited[cur.x] = true;
            for (Point next : graph.get(cur.x)) {
                if (dist[next.x] &amp;gt; dist[cur.x] + next.w) {
                    dist[next.x] = dist[cur.x] + next.w;
                    pq.offer(new Point(next.x, dist[next.x]));
                }
            }
        }

        return dist;
    }

    static class Point {
        int x;
        int w;

        Point(int x, int w) {
            this.x = x;
            this.w = w;
        }
    }
}&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;h2 data-ke-size=&quot;size26&quot;&gt;다른 사람 풀이 참고(효율적)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 다른 사람의 풀이를 보고 내가 작성한 알고리즘은 비효율적이라고 생각하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프를 정방향, 역방향 두개로 구성하여 해결할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;toX 는 원래 그래프고, fromX는 간선 방향을 뒤집은 그래프이다.&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;pre id=&quot;code_1759150110683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; M; i++) {
    st = new StringTokenizer(br.readLine());
    int start = Integer.parseInt(st.nextToken());
    int end = Integer.parseInt(st.nextToken());
    int t = Integer.parseInt(st.nextToken());
    toX.get(start).add(new Point(end, t));
    fromX.get(end).add(new Point(start, t));
}

int[] toXDist = dijkstra(toX); // N -&amp;gt; X
int[] fromXDist = dijkstra(fromX); // X -&amp;gt; N&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;28&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nm0wd/btsQSO3hfSo/hsIU5rVBMMgTTNToDnZd41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nm0wd/btsQSO3hfSo/hsIU5rVBMMgTTNToDnZd41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nm0wd/btsQSO3hfSo/hsIU5rVBMMgTTNToDnZd41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnm0wd%2FbtsQSO3hfSo%2FhsIU5rVBMMgTTNToDnZd41%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;644&quot; height=&quot;28&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;28&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 코드&lt;/p&gt;
&lt;pre id=&quot;code_1759150210939&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    static int N;
    static int M;
    static int X;
    static int INF = 10000 * 100;
    static List&amp;lt;List&amp;lt;Point&amp;gt;&amp;gt; toX = new ArrayList&amp;lt;&amp;gt;();
    static List&amp;lt;List&amp;lt;Point&amp;gt;&amp;gt; fromX = new ArrayList&amp;lt;&amp;gt;();


    public static void main(String[] args) throws IOException {
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        X = Integer.parseInt(st.nextToken());

        for (int i = 0; i &amp;lt;= N; i++) {
            toX.add(new ArrayList&amp;lt;&amp;gt;());
            fromX.add(new ArrayList&amp;lt;&amp;gt;());
        }

        for (int i = 0; i &amp;lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            int t = Integer.parseInt(st.nextToken());
            toX.get(start).add(new Point(end, t));
            fromX.get(end).add(new Point(start, t));
        }

        int[] toXDist = dijkstra(toX); // N -&amp;gt; X
        int[] fromXDist = dijkstra(fromX); // X -&amp;gt; N

        int max = Integer.MIN_VALUE;
        for (int i = 1; i &amp;lt;= N; i++) {
            max = Math.max(max, toXDist[i] + fromXDist[i]);
        }

        bw.write(max + &quot;\n&quot;);
        bw.flush();
        bw.close();
        br.close();
    }

    static int[] dijkstra(List&amp;lt;List&amp;lt;Point&amp;gt;&amp;gt; graph) {
        int[] dist = new int[N + 1];
        boolean[] visited = new boolean[N + 1];
        Arrays.fill(dist, INF);

        PriorityQueue&amp;lt;Point&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;((a, b) -&amp;gt; a.w - b.w);
        pq.offer(new Point(X, 0));
        dist[X] = 0;

        while (!pq.isEmpty()) {
            Point cur = pq.poll();
            if (visited[cur.x]) {
                continue;
            }
            visited[cur.x] = true;
            for (Point next : graph.get(cur.x)) {
                if (dist[next.x] &amp;gt; dist[cur.x] + next.w) {
                    dist[next.x] = dist[cur.x] + next.w;
                    pq.offer(new Point(next.x, dist[next.x]));
                }
            }
        }

        return dist;
    }

    static class Point {
        int x;
        int w;

        Point(int x, int w) {
            this.x = x;
            this.w = w;
        }
    }
}&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;</description>
      <category>Algorithm</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/181</guid>
      <comments>https://soonmin.tistory.com/181#entry181comment</comments>
      <pubDate>Mon, 29 Sep 2025 21:59:53 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 1504. 특정한 최단 경로</title>
      <link>https://soonmin.tistory.com/180</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1504&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1504&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 풀이 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무방향 그래프가 주어질 때 시작점(1)에서 도착점(N)으로 가는 최단 경로를 구하는 문제이다. 여기까지는 일반적인 최단 경로를 구해서 문제를 해결 할 수 있다. 하지만 이 문제는 조건이 있다. 시작점에서 출발해 반드시 v1, v2 지점을 통과한 뒤 도착점까지의 최단 경로를 구해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;문제 접근 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;v1, v2 지점을 통과하는 최단 경로를 구하기 위해 두가지 케이스를 고려해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1(시작점) -&amp;gt; v1 -&amp;gt; v2 -&amp;gt; N(도착점)&lt;/li&gt;
&lt;li&gt;1(시작점) -&amp;gt; v2 -&amp;gt; v1 -&amp;gt; N(도착점)&amp;nbsp;&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 두 케이스 모두 INF 이상이라면 도달할 수 없기에 -1을 출력한다.&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;h2 data-ke-size=&quot;size26&quot;&gt;3. 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1758787314319&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    static int N;
    static int E;

    static List&amp;lt;List&amp;lt;Point&amp;gt;&amp;gt; graph = new ArrayList&amp;lt;&amp;gt;();
    static int[] dist;
    static boolean[] visited;

    static final int INF = 200000 * 1000;

    public static void main(String[] args) throws IOException {
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        E = Integer.parseInt(st.nextToken());
        visited = new boolean[N + 1];
        dist = new int[N + 1];

        for (int i = 0; i &amp;lt;= N; i++) {
            graph.add(new ArrayList&amp;lt;&amp;gt;());
        }

        for (int i = 0; i &amp;lt; E; i++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            int c = Integer.parseInt(st.nextToken());
            graph.get(a).add(new Point(b, c));
            graph.get(b).add(new Point(a, c));
        }

        st = new StringTokenizer(br.readLine());
        int v1 = Integer.parseInt(st.nextToken());
        int v2 = Integer.parseInt(st.nextToken());

        int result1 = 0;
        result1 += dijkstra(1, v1);
        result1 += dijkstra(v1, v2);
        result1 += dijkstra(v2, N);

        int result2 = 0;
        result2 += dijkstra(1, v2);
        result2 += dijkstra(v2, v1);
        result2 += dijkstra(v1, N);

        if (result1 &amp;gt;= INF &amp;amp;&amp;amp; result2 &amp;gt;= INF) {
            bw.write(&quot;-1\n&quot;);
        } else {
            bw.write(Math.min(result1, result2) + &quot;\n&quot;);
        }

        bw.flush();
        bw.close();
        br.close();
    }

    static int dijkstra(int start, int end) {
        Arrays.fill(dist, INF);
        Arrays.fill(visited, false);
        PriorityQueue&amp;lt;Point&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;((o1, o2) -&amp;gt; o1.w - o2.w);

        pq.offer(new Point(start, 0));
        dist[start] = 0;

        while (!pq.isEmpty()) {
            Point cur = pq.poll();
            if (visited[cur.end]) {
                continue;
            }
            visited[cur.end] = true;

            for (Point next : graph.get(cur.end)) {
                if (dist[next.end] &amp;gt; dist[cur.end] + next.w) {
                    dist[next.end] = dist[cur.end] + next.w;
                    pq.offer(new Point(next.end, dist[next.end]));
                }
            }
        }
        return dist[end];
    }

    static class Point {
        int end;
        int w;

        public Point(int end, int w) {
            this.end = end;
            this.w = w;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/180</guid>
      <comments>https://soonmin.tistory.com/180#entry180comment</comments>
      <pubDate>Thu, 25 Sep 2025 17:02:37 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 1753. 최단경로 - JAVA</title>
      <link>https://soonmin.tistory.com/179</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1753&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1753&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 풀이 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 방향 그래프와 시작점 K가 주어진다. 시작점에서 출발해 다른 모든 정점까지의 최단 경로를 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로가 존재하지 않으면 INF를 출력한다.&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;이 문제는 다익스트라(Dijkstra) 알고리즘을 활용하는 가장 기본적인 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다익스트라 알고리즘&lt;/h3&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;동작과정&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시작 노드의 최단 거리를 0으로 설정, 다른 모든 노드는 INF로 초기화&lt;/li&gt;
&lt;li&gt;방문하지 않은 노드 중 최단 거리가 가장 짧은 노드 선택&lt;/li&gt;
&lt;li&gt;그 노드를 거쳐서 다른 노드로 가는 비용을 계산하고, 기존 비용보다 짧으면 갱신&lt;/li&gt;
&lt;li&gt;모든 노드가 방문될 때까지 2, 3번 과정을 반복&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀이 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력&lt;/p&gt;
&lt;pre id=&quot;code_1758595994468&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;5 6
1
5 1 1
1 2 2
1 3 3
2 3 4
2 4 5
3 4 6&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;문제 입력에 대한 그래프 형태는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정점:1~5&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간선 개수: 6개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 정점: 1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정점 간 방향과 가중치(w)를 가진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/18I6X/btsQIVulqmm/1UZ6zK1UnsM6OSAvsqK6kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/18I6X/btsQIVulqmm/1UZ6zK1UnsM6OSAvsqK6kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/18I6X/btsQIVulqmm/1UZ6zK1UnsM6OSAvsqK6kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F18I6X%2FbtsQIVulqmm%2F1UZ6zK1UnsM6OSAvsqK6kk%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;548&quot; height=&quot;276&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&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;각 정점에 대한 최단 거리를 저장하는 배열 result를 INF로 초기화 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;result의 index는 정점까지의 거리를 뜻한다. index = 1~5&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[INF, INF, INF, INF, INF]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 시작점(1): result[1] = 0&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;result = [0, INF, INF, INF, INF]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 1번 정점과 연결된 정점 갱신&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1 -&amp;gt; 2 (w=2)&lt;/li&gt;
&lt;li&gt;1 -&amp;gt; 3 (w=3)&lt;/li&gt;
&lt;li&gt;result = [&lt;span style=&quot;color: #ee2323;&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, INF, INF]&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 2번 정점과 연결된 정점 갱신&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2 -&amp;gt; 3 (기존 3 vs 2 + 4 = 6 -&amp;gt; 유지)&lt;/li&gt;
&lt;li&gt;2 -&amp;gt; 4 (2 + 5 = 7)&lt;/li&gt;
&lt;li&gt;result = [0, &lt;span style=&quot;color: #ee2323;&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;7&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, INF]&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 3번 정점과 연결된 정점 갱신&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3 -&amp;gt; 4 (기존 7 vs 3 + 6 = 9 -&amp;gt; 유지)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;result = [0, 2, &lt;span style=&quot;color: #ee2323;&quot;&gt;3&lt;/span&gt;, &lt;span style=&quot;color: #f3c000;&quot;&gt;7&lt;/span&gt;, INF]&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 4번 연결된 정점 갱신&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4번과 연결된 정점 없음.&lt;/li&gt;
&lt;li&gt;5번 정점은 시작점에서 도달 불가 -&amp;gt; INF 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 최종 결과&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;result = [0,&amp;nbsp;2,&amp;nbsp;3,&amp;nbsp;7&lt;span&gt;, INF]&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 코드&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최단 거리를 가진 정점을 빠르게 뽑기 위해 우선순위 큐를 사용한다.&lt;/li&gt;
&lt;li&gt;이미 확정된 정점은 다시 처리하지 않도록 visited 배열 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758594801594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    static int V;
    static int E;
    static int K;

    static List&amp;lt;List&amp;lt;Point&amp;gt;&amp;gt; graph = new ArrayList&amp;lt;&amp;gt;();
    static int[] result;

    public static void main(String[] args) throws IOException {
        StringTokenizer st = new StringTokenizer(br.readLine());
        V = Integer.parseInt(st.nextToken());
        E = Integer.parseInt(st.nextToken());
        K = Integer.parseInt(br.readLine());
        result = new int[V + 1];
        for (int i = 0; i &amp;lt;= V; i++) {
            graph.add(new ArrayList&amp;lt;&amp;gt;());
            result[i] = Integer.MAX_VALUE;
        }

        for (int i = 0; i &amp;lt; E; i++) {
            st = new StringTokenizer(br.readLine());
            int u = Integer.parseInt(st.nextToken());
            int v = Integer.parseInt(st.nextToken());
            int w = Integer.parseInt(st.nextToken());
            graph.get(u).add(new Point(v, w));
        }

        dijkstra();

        for (int i = 1; i &amp;lt;= V; i++) {
            if (result[i] == Integer.MAX_VALUE) {
                bw.write(&quot;INF\n&quot;);
            } else {
                bw.write(result[i] + &quot;\n&quot;);
            }
        }

        bw.flush();
        bw.close();
    }

    static void dijkstra() {
        PriorityQueue&amp;lt;Point&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;((o1, o2) -&amp;gt; o1.w - o2.w);
        boolean[] visited = new boolean[V + 1];
        pq.offer(new Point(K, 0));
        result[K] = 0;

        while (!pq.isEmpty()) {
            Point cur = pq.poll();

            if (visited[cur.x]) {
                continue;
            }
            visited[cur.x] = true;

            for (Point next : graph.get(cur.x)) {
                if (result[next.x] &amp;gt; result[cur.x] + next.w) {
                    result[next.x] = result[cur.x] + next.w;
                    pq.add(new Point(next.x, result[next.x]));
                }
            }
        }
    }

    static class Point {
        int x;
        int w;

        public Point(int x, int w) {
            this.x = x;
            this.w = w;
        }
    }
}&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;</description>
      <category>Algorithm</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/179</guid>
      <comments>https://soonmin.tistory.com/179#entry179comment</comments>
      <pubDate>Tue, 23 Sep 2025 12:19:47 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 13460. 구슬 탈출2 - JAVA</title>
      <link>https://soonmin.tistory.com/178</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/13460&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/13460&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 풀이 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N x M 크기의 게임판이 주어지며, 각 영역은 빨간구슬(R), 파란구슬(B), 빈칸(.), 벽(#), 구멍(O)으로 구분된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간구슬 파란구슬 각각 1개 씩 주어지며, 가장 바깥 행과 열이 벽으로 막혀있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, 게임판을 상,하,좌,우로 기울이며, 구슬은 벽을 만날 때까지 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임판을 몇 번을 기울여야 &lt;span style=&quot;text-align: start;&quot;&gt;빨간구슬을 구명(O)에 넣을 수 있는 지 횟수를 구하는 문제이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;단, 파란 구슬이 구멍에 들어가면 실패이고, 빨간구슬과 파란구슬이 동시에 구멍에 들어가도 실패이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;또한, 10회 이상 시도해도 실패이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;실패는 -1을 반환한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;문제 접근&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;어려운 문제였고, 이 문제는 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;BFS를 통해 최단 거리를 구하는 문제이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;고려할 사항이 많아서 머리가 아팠다..&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;1. 두 구슬을 모두 굴린다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 문제와 달리, 구슬이 벽을 만날 때까지 이동하므로 그 부분을 신경써줘야 한다.&lt;/li&gt;
&lt;li&gt;한 방향을 지정해서 벽을 만날때까지 굴린다.&lt;/li&gt;
&lt;li&gt;구슬이 구멍을 만나는 경우를 고려한다.&lt;/li&gt;
&lt;li&gt;dist는 구슬의 이동거리로 두 구슬이 같은 영역에 있을 경우를 처리하기 위해 카운트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758170590682&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MoveResult r = move(cur.rx, cur.ry, i);
MoveResult b = move(cur.bx, cur.by, i);
                
private static MoveResult move(int x, int y, int dir) {
    int dist = 0;
    while (true) {
        int nx = x + dx[dir];
        int ny = y + dy[dir];
        if (map[nx][ny] == '#') {
            break;
        }
        x = nx;
        y = ny;
        dist++;
        if (map[x][y] == 'O') {
            return new MoveResult(x, y, dist, true);
        }
    }
    return new MoveResult(x, y, dist, false);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;2. 구슬을 굴린 결과에 따라 결과 반환&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파란 구슬이 구멍에 들어가면 무조건 실패이다. =&amp;gt; 다른 방향 탐색을 시작한다.&lt;/li&gt;
&lt;li&gt;빨간 구슬만 들어간 경우 결과(굴린 횟수)를 리턴한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758170961444&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; // 파란 구슬이 먼저 빠지면 실패
if (b.inHole) {
    continue;
}

// 빨간 구슬만 빠지면 성공
if (r.inHole) {
    return cur.depth + 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span&gt;3. 같은 영역에 두 구슬이 들어간 경우&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빨간 구슬, 파란 구슬이 같은 영역에 있을 수 없으므로 빨간 구슬, 파란 구슬이 같은 영역에 있는 케이스를 고려해줘야 한다.&lt;/li&gt;
&lt;li&gt;같은 영역에 있을 경우 앞에서 구한 dist를 가지고 구슬의 위치를 조정해준다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758170703546&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; // 같은 위치면 더 많이 이동한 쪽을 한 칸 뒤로
if (r.x == b.x &amp;amp;&amp;amp; r.y == b.y) {
    if (r.dist &amp;gt; b.dist) {
        r.x -= dx[i];
        r.y -= dy[i];
    } else {
        b.x -= dx[i];
        b.y -= dy[i];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1758169808932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    // 북, 동, 남, 서
    static int[] dx = {-1, 0, 1, 0};
    static int[] dy = {0, 1, 0, -1};

    static int N;
    static int M;
    static char[][] map;
    static boolean[][][][] visited;

    public static void main(String[] args) throws IOException {
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        map = new char[N][M];
        int rx = 0, ry = 0, bx = 0, by = 0;
        for (int i = 0; i &amp;lt; N; i++) {
            String line = br.readLine();
            for (int j = 0; j &amp;lt; M; j++) {
                map[i][j] = line.charAt(j);
                if (map[i][j] == 'R') {
                    rx = i;
                    ry = j;
                } else if (map[i][j] == 'B') {
                    bx = i;
                    by = j;
                }
            }
        }

        int result = bfs(rx, ry, bx, by);
        bw.write(result + &quot;\n&quot;);
        bw.flush();
        bw.close();
    }

    private static int bfs(int rx, int ry, int bx, int by) {
        visited = new boolean[N][M][N][M];
        Queue&amp;lt;State&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
        q.offer(new State(rx, ry, bx, by, 0));
        visited[rx][ry][bx][by] = true;

        while (!q.isEmpty()) {
            State cur = q.poll();
            if (cur.depth &amp;gt;= 10) {
                continue;
            }

            for (int i = 0; i &amp;lt; 4; i++) {
                MoveResult r = move(cur.rx, cur.ry, i);
                MoveResult b = move(cur.bx, cur.by, i);

                // 파란 구슬이 먼저 빠지면 실패
                if (b.inHole) {
                    continue;
                }

                // 빨간 구슬만 빠지면 성공
                if (r.inHole) {
                    return cur.depth + 1;
                }

                // 같은 위치면 더 많이 이동한 쪽을 한 칸 뒤로
                if (r.x == b.x &amp;amp;&amp;amp; r.y == b.y) {
                    if (r.dist &amp;gt; b.dist) {
                        r.x -= dx[i];
                        r.y -= dy[i];
                    } else {
                        b.x -= dx[i];
                        b.y -= dy[i];
                    }
                }

                if (!visited[r.x][r.y][b.x][b.y]) {
                    visited[r.x][r.y][b.x][b.y] = true;
                    q.offer(new State(r.x, r.y, b.x, b.y, cur.depth + 1));
                }
            }
        }
        return -1;
    }

    private static MoveResult move(int x, int y, int dir) {
        int dist = 0;
        while (true) {
            int nx = x + dx[dir];
            int ny = y + dy[dir];
            if (map[nx][ny] == '#') {
                break;
            }
            x = nx;
            y = ny;
            dist++;
            if (map[x][y] == 'O') {
                return new MoveResult(x, y, dist, true);
            }
        }
        return new MoveResult(x, y, dist, false);
    }

    static class State {
        int rx, ry, bx, by, depth;

        State(int rx, int ry, int bx, int by, int depth) {
            this.rx = rx;
            this.ry = ry;
            this.bx = bx;
            this.by = by;
            this.depth = depth;
        }
    }

    static class MoveResult {
        int x;
        int y;
        int dist;
        boolean inHole;

        MoveResult(int x, int y, int dist, boolean inHole) {
            this.x = x;
            this.y = y;
            this.dist = dist;
            this.inHole = inHole;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/178</guid>
      <comments>https://soonmin.tistory.com/178#entry178comment</comments>
      <pubDate>Thu, 18 Sep 2025 13:53:15 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring MVC에서 Pageable 파라미터가 동작하는 방식</title>
      <link>https://soonmin.tistory.com/177</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot로 페이지네이션 API를 구현할 때 보통, Pageable을 사용하여 간편하게 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pageable을 컨트롤러의 메서드의 인자로 선언할 때, Spring MVC는 요청 파라미터를 자동으로 해석하여 Pageable 객체를 주입해준다. 어떤식으로 동작하는지 자세히 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1758010784338&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@RequestMapping(&quot;/v1/articles&quot;)
@RestController
public class ArticleController {
   @GetMapping
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;Page&amp;lt;SearchArticleResponse&amp;gt;&amp;gt;&amp;gt; getArticles(
            @PageableDefault(size = 10, sort = &quot;createdAt&quot;, direction = Sort.Direction.DESC)
            Pageable pageable,
         
    ) {
        // 구현 생략..
        return ResponseEntity.ok().body(response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 동작 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC 구조를 모르는 사람을 위해 간단하게 짚고 넘어간다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring MVC 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 요청을 보내면, 서블릿 컨테이너에 의해 DispatcherServlet.doService() 메서드로 요청을 전달한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서, 요청을 처리할 핸들러(컨트롤러)를 찾고, 핸들러를 실행할 수 있는 HandlerAdapter를 탐색한다. 이 때, @RequestMapping 기반 컨트롤러이면 RequestMappingHandlerAdpater가 선택된다. RequestMappingHandlerAdapter는 InvocableHandlerMethod 객체를 생성하여 실제 요청을 컨트롤러에 전달한다.&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;InvocableHandlerMethods는 HandlerMethodArgumentResolverComposite를 가진다.&lt;/p&gt;
&lt;pre id=&quot;code_1758015087463&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class InvocableHandlerMethod extends HandlerMethod {
    private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
    
    // ..
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HadnlerMethodArgumentResloverComposite 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerMethodArgumentResolverComposite는 여러 HandlerMethodArgumentResolver 구현체를 리스트로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때, 성능을 위해 동일한 MethodParamter에 대해서는 Map으로 캐싱하여 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정, MethodParameter를 통해 적절한 &lt;span style=&quot;text-align: start;&quot;&gt;HandlerMethodArgumentResolver&lt;/span&gt;를 찾았으면, 해당 Resolver를 실제 요청 파라미터를 파라미터를 객체(Object)로 변환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758006389150&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {

    private final List&amp;lt;HandlerMethodArgumentResolver&amp;gt; argumentResolvers = new ArrayList&amp;lt;&amp;gt;();
    private final Map&amp;lt;MethodParameter, HandlerMethodArgumentResolver&amp;gt; argumentResolverCache =
			new ConcurrentHashMap&amp;lt;&amp;gt;(256);
    
    @Override
    @Nullable
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
        if (resolver == null) {
            throw new IllegalArgumentException(&quot;Unsupported parameter type [&quot; +
            parameter.getParameterType().getName() + &quot;]. supportsParameter should be called first.&quot;);
        }
        return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
            
    @Nullable
    public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        if (result == null) {
		    for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
			    if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, result);
                    break;
                }
            }
        }
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. HanlderMethodArgumentResolver(실제 Pageable로 변환)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MethodParameter가 Pageable인 경우 PageableHandlerMethodArgumentResolver가 Resolver로서 역할을 하며, 요청 파라미터를 파싱해서 PageRequest를 반환한다. PageRequest는 Pageable의 구현체이다.&lt;/p&gt;
&lt;pre id=&quot;code_1758008938486&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolverSupport implements PageableArgumentResolver {
    @Override
    public Pageable resolveArgument(MethodParameter methodParameter, @Nullable ModelAndViewContainer mavContainer,
	    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {

        String page = webRequest.getParameter(getParameterNameToUse(getPageParameterName(), methodParameter));
        String pageSize = webRequest.getParameter(getParameterNameToUse(getSizeParameterName(), methodParameter));

        Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
        Pageable pageable = getPageable(methodParameter, page, pageSize);

        if (sort.isSorted()) {
            return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
        }

        return pageable;
    }
}&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;aricles/?page=0&amp;amp;size=10&amp;amp;sort=creadeAt,DESC를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PageRequest로 변환하면 PageRequest(page=0, size=10, Sort.by(&quot;createdAt&quot;).descending())&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;h2 data-ke-size=&quot;size26&quot;&gt;3. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC 동작과정에 의해 Pageable은 PageableHandlerMethodArgumentResolver가 자동으로 요청 파라미터를 파싱해 Pageable의 구현체인 PageRequest로 변환해준다.&lt;/p&gt;</description>
      <category>Programming/Spring</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/177</guid>
      <comments>https://soonmin.tistory.com/177#entry177comment</comments>
      <pubDate>Tue, 16 Sep 2025 18:48:01 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 14502. 연구소 - JAVA</title>
      <link>https://soonmin.tistory.com/176</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/14502&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/14502&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 풀이 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 N x M 크기의 맵이 주어지고, 각 영역은 빈 칸(0), 벽(1), 바이러스(2)로 구분된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이러스는 벽을 통과 못한다. 벽 3개를 추가로 설치할 수 있으며, 이 때 안전 영역의 최대 개수를 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N, M의 최대크기는 8인데 제한시간이 2초로 비교적 여유롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 접근&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 3개의 벽을 치는 모든 케이스를 모두 구한다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DFS, 백트래킹을 통해 3개의 벽을 치는 모든 케이스를 구한다.&lt;/li&gt;
&lt;li&gt;각 케이스 마다 BFS를 통해 감염처리를 하고 안전영역의 개수를 구해서 최대값을 갱신한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757987875434&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static void dfs(int depth) {
    if (depth == 3) {
        // 3개의 벽을 모두 설치한 상태
        MAX = Math.max(MAX, safeZone);
        return;
    }
    for (int i = 0; i &amp;lt; N; i++) {
        for (int j = 0; j &amp;lt; M; j++) {
            if (map[i][j] == 0) {
                map[i][j] = 1;
                dfs(depth + 1);
                map[i][j] = 0;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 각 케이스마다 BFS를 통해 감염 영역을 확장해간다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 중요한 것은 여러 케이스를 고려해야 하기 때문에, map(원본 배열)을 사용하지 않는다.&lt;/li&gt;
&lt;li&gt;복사본을 사용하여 감염처리를 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757987968131&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static int bfs() {
    Queue&amp;lt;Point&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
    for (int i = 0; i &amp;lt; N; i++) {
        for (int j = 0; j &amp;lt; M; j++) {
            if (map[i][j] == 2) {
                q.offer(new Point(i, j));
            }
        }
    }
    int[][] copyMap = new int[N][M];
    for (int i = 0; i &amp;lt; N; i++) {
        copyMap[i] = map[i].clone();
    }

    while (!q.isEmpty()) {
        Point cur = q.poll();
        for (int i = 0; i &amp;lt; 4; i++) {
            int nx = cur.x + dx[i];
            int ny = cur.y + dy[i];
            if (nx &amp;lt; 0 || nx &amp;gt;= N || ny &amp;lt; 0 || ny &amp;gt;= M || copyMap[nx][ny] != 0) {
                continue;
            }
            q.offer(new Point(nx, ny));
            copyMap[nx][ny] = 2;
        }
    }
    
    return getSafeZone(copyMap); // 안전 영역 개수를 구한다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 각 케이스마다 안전 영역의 개수를 구하고, 최댓값으로 갱신하다.&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0인 영역을 카운트 해서 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757988094466&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static int getSafeZone(int[][] copyMap) {
    int safeZone = 0;
    for (int i = 0; i &amp;lt; N; i++) {
        for (int j = 0; j &amp;lt; M; j++) {
            if (copyMap[i][j] == 0) {
                safeZone++;
            }
        }
    }
    return safeZone;
}

MAX = Math.max(MAX, safeZone); // 최댓값 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1757987769453&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    static int[] dx = {-1, 0, 1, 0};
    static int[] dy = {0, 1, 0, -1};

    static int N;
    static int M;
    static int[][] map;

    static int MAX = Integer.MIN_VALUE;

    public static void main(String[] args) throws IOException {
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());
        map = new int[N][M];
        for (int i = 0; i &amp;lt; N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j &amp;lt; M; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        // 벽을 3개 생성하는 모든 방법을 구한다.(DFS)
        dfs(0);

        bw.write(MAX + &quot;\n&quot;);
        bw.flush();
        bw.close();
    }

    static void dfs(int depth) {
        if (depth == 3) {
            int safeZone = bfs(); // BFS를 통해 감염 시킨다.
            MAX = Math.max(MAX, safeZone);
            return;
        }
        for (int i = 0; i &amp;lt; N; i++) {
            for (int j = 0; j &amp;lt; M; j++) {
                if (map[i][j] == 0) {
                    map[i][j] = 1;
                    dfs(depth + 1);
                    map[i][j] = 0;
                }
            }
        }
    }

    static int bfs() {
        Queue&amp;lt;Point&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; N; i++) {
            for (int j = 0; j &amp;lt; M; j++) {
                if (map[i][j] == 2) {
                    q.offer(new Point(i, j));
                }
            }
        }
        int[][] copyMap = new int[N][M];
        for (int i = 0; i &amp;lt; N; i++) {
            copyMap[i] = map[i].clone();
        }

        while (!q.isEmpty()) {
            Point cur = q.poll();
            for (int i = 0; i &amp;lt; 4; i++) {
                int nx = cur.x + dx[i];
                int ny = cur.y + dy[i];
                if (nx &amp;lt; 0 || nx &amp;gt;= N || ny &amp;lt; 0 || ny &amp;gt;= M || copyMap[nx][ny] != 0) {
                    continue;
                }
                q.offer(new Point(nx, ny));
                copyMap[nx][ny] = 2;
            }
        }
        return getSafeZone(copyMap); // 안전 영역 개수를 구한다.
    }

    static int getSafeZone(int[][] copyMap) {
        int safeZone = 0;
        for (int i = 0; i &amp;lt; N; i++) {
            for (int j = 0; j &amp;lt; M; j++) {
                if (copyMap[i][j] == 0) {
                    safeZone++;
                }
            }
        }
        return safeZone;
    }

    static class Point {
        int x;
        int y;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/176</guid>
      <comments>https://soonmin.tistory.com/176#entry176comment</comments>
      <pubDate>Tue, 16 Sep 2025 11:05:14 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 3190. 뱀 -JAVA</title>
      <link>https://soonmin.tistory.com/175</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/3190&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/3190&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 풀이 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N x N이 BOARD가 주어졌을 때, 사과가 있는 영역(1)과 빈 영역(0)이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뱀은 (0,0)에서 오른쪽 방향을 보면서 시작한다. 그리고 뱀은 주어진 시간마다 왼쪽(L) 또는 오른쪽(D)로 방향을 튼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뱀은 자기가 바라보고 있는 방향으로 1초에 1칸 이동한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사과가 있는 영역으로 이동: 사과를 먹고, 몸의 길이는 1만큼 늘어난다.&lt;/li&gt;
&lt;li&gt;빈 영역으로 이동: 몸의 길이는 늘어나지 않고 뱀이 이동만 한다.&lt;/li&gt;
&lt;/ul&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;처음에는 DFS로 접근했었다.. DFS로 푸니깐 도저히 뱀이 길이가 늘어나는 성질이 구현이 안되서 다른 풀이 방법으로 접근했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 접근 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 큐를 활용한 시뮬레이션 방식으로 문제를 해결할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 먼저, BOARD 각 영역의 상태를 정리해보자. 총 3가지로 구분할 수 있다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0 = 빈 영역&lt;/li&gt;
&lt;li&gt;1 = 사과가 있는 영역&lt;/li&gt;
&lt;li&gt;2 = 뱀이 차지하고 있는 영역&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 큐를 활용&lt;/h4&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;2-1. 초기 세팅&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뱀의 시작점은 (0, 0)이다.&lt;/li&gt;
&lt;li&gt;방향은 북(0), 동(1), 남(2), 서(3)로 표현할 수 있다.&lt;/li&gt;
&lt;li&gt;뱀이 초기에 바라보고 방향은 동쪽이다.&lt;/li&gt;
&lt;li&gt;경과시간을 나타내는 변수 time을 0으로 초기화 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757840055510&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static int d = 1; // 초기 동쪽
int time = 0;
// 머리 위치
int headX = 0;
int headY = 0;
Queue&amp;lt;Point&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
q.offer(new Point(headX, headY));
BOARD[0][0] = 2;&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-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;/li&gt;
&lt;/ul&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-2-1. 이동 영역 구하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 방향을 기준으로 방향 백터를 사용하여 다음 이동 영역(nx, ny)를 구한다.&lt;/li&gt;
&lt;li&gt;경과시간을 1씩 증가시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757840394883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static int[] dx = {-1, 0, 1, 0};
static int[] dy = {0, 1, 0, -1};

int nx = headX + dx[d];
int ny = headY + dy[d];
time++;&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-2-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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757840498189&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (nx &amp;lt; 0 || nx &amp;gt;= N || ny &amp;lt; 0 || ny &amp;gt;= N || BOARD[nx][ny] == 2) { // 벽이거나 몸통이 부딪힐 때
    break;
}&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;2-2-3. 다음 이동영역이 사과 또는 빈 영역인 경우&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사과인 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큐에 이동영역을 저장&lt;/li&gt;
&lt;li&gt;이동영역을 뱀의 영역으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;빈 영역인 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큐에서 꼬리 영역을 꺼낸다.&lt;/li&gt;
&lt;li&gt;꼬리 영역을 빈 영역으로 만들어 준다.&lt;/li&gt;
&lt;li&gt;큐에 이동영역을 저장&lt;/li&gt;
&lt;li&gt;이동영역을 뱀의 영역으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757840018963&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Point next = new Point(nx, ny);
if (BOARD[nx][ny] == 1) {
    BOARD[nx][ny] = 2;
    q.offer(next);
} else if (BOARD[nx][ny] == 0) {
    Point tail = q.poll();
    BOARD[tail.x][tail.y] = 0;
    BOARD[nx][ny] = 2;
    q.offer(next);
}&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-2-4. 뱀 회전&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;direction은 Map 형태로, key=시간, value=방향&lt;/li&gt;
&lt;li&gt;현재 시간이 Map에 key 형태로 존재한다면, value에 따라 뱀의 방향을 회전시킨다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757840817581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (direction.containsKey(time)) {
    if (direction.get(time) == 'L') {
        d = (d + 3) % 4;  // 왼쪽
    } else if (direction.get(time) == 'D') {
        d = (d + 1) % 4;  // 오른쪽
    }
}&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-2-5. 뱀 머리 위치 설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 이동 영역이 뱀의 머리 위치가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757840973120&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;headX = nx;
headY = ny;&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;경과시간(time)을 리턴하면 끝!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1757839399198&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

    static int N; // 보드 크기
    static int K; // 사과 개수
    static int L; // 뱀의 방향 변환 횟수

    // 북(0), 동(1), 남(2), 서(3)
    static int[] dx = {-1, 0, 1, 0};
    static int[] dy = {0, 1, 0, -1};
    static int d = 1; // 초기 동쪽

    static int[][] BOARD;
    static Map&amp;lt;Integer, Character&amp;gt; direction = new HashMap&amp;lt;&amp;gt;();

    public static void main(String[] args) throws IOException {
        N = Integer.parseInt(br.readLine());
        BOARD = new int[N][N];
        K = Integer.parseInt(br.readLine());

        for (int i = 0; i &amp;lt; K; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int x = Integer.parseInt(st.nextToken()) - 1;
            int y = Integer.parseInt(st.nextToken()) - 1;
            BOARD[x][y] = 1; // 사과
        }

        L = Integer.parseInt(br.readLine());

        for (int i = 0; i &amp;lt; L; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int x = Integer.parseInt(st.nextToken());
            char c = st.nextToken().charAt(0);
            direction.put(x, c);
        }

        int result = solve();

        bw.write(result + &quot;\n&quot;);
        bw.flush();
        bw.close();
    }

    static int solve() {
        int time = 0;
        // 머리 위치
        int headX = 0;
        int headY = 0;
        Queue&amp;lt;Point&amp;gt; q = new LinkedList&amp;lt;&amp;gt;();
        q.offer(new Point(headX, headY));
        BOARD[0][0] = 2;

        while (true) {
            int nx = headX + dx[d];
            int ny = headY + dy[d];
            time++;

            if (nx &amp;lt; 0 || nx &amp;gt;= N || ny &amp;lt; 0 || ny &amp;gt;= N || BOARD[nx][ny] == 2) { // 벽이거나 몸통이 부딪힐 때
                break;
            }

            Point next = new Point(nx, ny);
            if (BOARD[nx][ny] == 1) {
                BOARD[nx][ny] = 2;
                q.offer(next);
            } else if (BOARD[nx][ny] == 0) {
                Point tail = q.poll();
                BOARD[tail.x][tail.y] = 0;
                BOARD[nx][ny] = 2;
                q.offer(next);
            }

            if (direction.containsKey(time)) {
                if (direction.get(time) == 'L') {
                    d = (d + 3) % 4;  // 왼쪽
                } else if (direction.get(time) == 'D') {
                    d = (d + 1) % 4;  // 오른쪽
                }
            }
            headX = nx;
            headY = ny;
        }
        return time;
    }

    static class Point {
        int x;
        int y;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <author>kmindev</author>
      <guid isPermaLink="true">https://soonmin.tistory.com/175</guid>
      <comments>https://soonmin.tistory.com/175#entry175comment</comments>
      <pubDate>Sun, 14 Sep 2025 18:15:19 +0900</pubDate>
    </item>
  </channel>
</rss>