<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>곽코딩</title>
    <link>https://kwakscoding.tistory.com/</link>
    <description>개발자 루카입니다.  끊임없이 새로운 기술에 도전하며 성장하는 개발자입니다.
다양한 분야에서의 경험과 배움을 공유하며, 더 나은 개발자가 되기 위해 노력하고 있습니다</description>
    <language>ko</language>
    <pubDate>Wed, 13 May 2026 04:00:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>곽코딩루카</managingEditor>
    <image>
      <title>곽코딩</title>
      <url>https://tistory1.daumcdn.net/tistory/4919574/attach/36757d825fcb458fa4186ed7cc03d046</url>
      <link>https://kwakscoding.tistory.com</link>
    </image>
    <item>
      <title>카프카(Kafka) 기본 개념 정리 &amp;ndash; 컨슈머와 리스너는 어떻게 다를까?</title>
      <link>https://kwakscoding.tistory.com/94</link>
      <description>&lt;blockquote data-end=&quot;106&quot; data-start=&quot;34&quot; data-ke-style=&quot;style3&quot;&gt;목차&lt;br /&gt;&lt;a href=&quot;#c1&quot;&gt;1. 카프카(kafka)란 무엇인가?&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c2&quot;&gt;2. 초보 개발자가 가장 많이 헷갈리는 질문&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c3&quot;&gt;3. 컨슈머와 리스너의 진짜 역할&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c4&quot;&gt;4. 실제 동작 구조를 하나의 흐름으로 정리해보면&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c5&quot;&gt;5. 헷갈리는 개념들을 한 번 더 정리하면&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c6&quot;&gt;6. 카프카를 처음 도입하는 팀이라면&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;카프카.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baA4Uw/dJMcajgm1K6/WGefRdqRZTZJO13x9dEYpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baA4Uw/dJMcajgm1K6/WGefRdqRZTZJO13x9dEYpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baA4Uw/dJMcajgm1K6/WGefRdqRZTZJO13x9dEYpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaA4Uw%2FdJMcajgm1K6%2FWGefRdqRZTZJO13x9dEYpk%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;1536&quot; height=&quot;1024&quot; data-filename=&quot;카프카.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 data-end=&quot;184&quot; data-start=&quot;142&quot;&gt;카프카(Kafka) 기본 개념 정리 &amp;ndash; 컨슈머와 리스너는 어떻게 다를까?&lt;/h1&gt;
&lt;p data-end=&quot;394&quot; data-start=&quot;186&quot; data-ke-size=&quot;size16&quot;&gt;최근에 회사 프로젝트에서 카프카(Kafka)를 도입하면서 가장 많이 부딪힌 부분이 바로 &amp;ldquo;컨슈머(Consumer)와 리스너(Listener)가 뭐가 다른가?&amp;rdquo;였다.&lt;br /&gt;설계 문서를 보면 둘 다 메시지를 받고 처리하는 것처럼 보이는데, 실제로는 서로 완전히 다른 역할을 담당한다.&lt;br /&gt;초보 개발자들이 가장 헷갈려 하는 부분이기도 해서, 처음 접하는 사람 기준으로 정리해본다.&lt;/p&gt;
&lt;p data-end=&quot;394&quot; data-start=&quot;186&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c1&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-start=&quot;401&quot; data-end=&quot;424&quot;&gt;1. 카프카(Kafka)란 무엇인가?&lt;/h2&gt;
&lt;h2 data-end=&quot;424&quot; data-start=&quot;401&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oz72o/dJMcahwakuC/a2WSDGRAaTcqnpkT2sxK51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oz72o/dJMcahwakuC/a2WSDGRAaTcqnpkT2sxK51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oz72o/dJMcahwakuC/a2WSDGRAaTcqnpkT2sxK51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foz72o%2FdJMcahwakuC%2Fa2WSDGRAaTcqnpkT2sxK51%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;272&quot; height=&quot;143&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;545&quot; data-start=&quot;426&quot; data-ke-size=&quot;size16&quot;&gt;카프카는 여러 시스템 사이에서 데이터를 빠르고 안정적으로 전달하기 위한 &lt;b&gt;메시지 스트리밍 플랫폼&lt;/b&gt;이다. 쉽게 말하면, 서비스들 사이에서 주고받는 데이터를 한 곳에 모아두고, 필요한 서비스가 가져다 쓰는 구조다.&lt;/p&gt;
&lt;p data-end=&quot;709&quot; data-start=&quot;547&quot; data-ke-size=&quot;size16&quot;&gt;HTTP API처럼 &amp;ldquo;서버 &amp;harr; 서버&amp;rdquo;가 실시간으로 직접 통신하는 방식과 달리, 카프카는 &lt;b&gt;중간 저장소(토픽)&lt;/b&gt; 에 데이터를 쌓아두고 다른 서비스가 이를 언제든 읽어갈 수 있게 만든다.&lt;br /&gt;이 방식은 서비스 간 결합도를 낮추고, 장애나 지연이 생겨도 전체 시스템을 보호해주는 장점이 있다.&lt;/p&gt;
&lt;p data-end=&quot;733&quot; data-start=&quot;711&quot; data-ke-size=&quot;size16&quot;&gt;카프카의 핵심 구성 요소는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;995&quot; data-start=&quot;735&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;773&quot; data-start=&quot;735&quot;&gt;&lt;b&gt;토픽(Topic)&lt;/b&gt; : 메시지를 종류별로 모아놓는 저장소&lt;/li&gt;
&lt;li data-end=&quot;803&quot; data-start=&quot;774&quot;&gt;&lt;b&gt;메시지(Message)&lt;/b&gt; : 실제 데이터&lt;/li&gt;
&lt;li data-end=&quot;844&quot; data-start=&quot;804&quot;&gt;&lt;b&gt;프로듀서(Producer)&lt;/b&gt; : 메시지를 카프카에 보내는 쪽&lt;/li&gt;
&lt;li data-end=&quot;886&quot; data-start=&quot;845&quot;&gt;&lt;b&gt;컨슈머(Consumer)&lt;/b&gt; : 메시지를 카프카에서 가져오는 쪽&lt;/li&gt;
&lt;li data-end=&quot;915&quot; data-start=&quot;887&quot;&gt;&lt;b&gt;브로커(Broker)&lt;/b&gt; : 카프카 서버&lt;/li&gt;
&lt;li data-end=&quot;963&quot; data-start=&quot;916&quot;&gt;&lt;b&gt;파티션(Partition)&lt;/b&gt; : 토픽을 병렬 처리하기 위해 쪼개놓은 단위&lt;/li&gt;
&lt;li data-end=&quot;995&quot; data-start=&quot;964&quot;&gt;&lt;b&gt;오프셋(Offset)&lt;/b&gt; : 메시지의 번호(위치)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1048&quot; data-start=&quot;997&quot; data-ke-size=&quot;size16&quot;&gt;여기까지는 공식 문서에서도 흔히 나오는 설명이지만, 문제는 이 다음 단계에서 많이 헷갈린다.&lt;/p&gt;
&lt;h2 data-end=&quot;1082&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c2&quot; data-end=&quot;1082&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size26&quot;&gt;2. 초보 개발자가 가장 많이 헷갈리는 질문&lt;/h2&gt;
&lt;p data-end=&quot;1158&quot; data-start=&quot;1084&quot; data-ke-size=&quot;size16&quot;&gt;카프카에서 메시지가 들어오면, 스프링 애플리케이션에 있는 리스너가 자동으로 실행된다.&lt;br /&gt;이 때문에 흔히 이런 흐름을 상상하게 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1240&quot; data-start=&quot;1160&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1186&quot; data-start=&quot;1160&quot;&gt;서비스 A가 메시지를 Kafka에 보냄&lt;/li&gt;
&lt;li data-end=&quot;1208&quot; data-start=&quot;1187&quot;&gt;Kafka가 토픽을 확인한 뒤&lt;/li&gt;
&lt;li data-end=&quot;1240&quot; data-start=&quot;1209&quot;&gt;스프링 서비스의 리스너에게 메시지를 &amp;ldquo;전달&amp;rdquo;해줌&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;1293&quot; data-start=&quot;1242&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 동작 방식은 이와 정반대다.&lt;br /&gt;카프카는 메시지를 &lt;b&gt;직접 보내주지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;1321&quot; data-start=&quot;1300&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c3&quot; data-end=&quot;1321&quot; data-start=&quot;1300&quot; data-ke-size=&quot;size26&quot;&gt;3. 컨슈머와 리스너의 진짜 역할&lt;/h2&gt;
&lt;h3 data-end=&quot;1347&quot; data-start=&quot;1323&quot; data-ke-size=&quot;size23&quot;&gt;3-1. 컨슈머(Consumer)란?&lt;/h3&gt;
&lt;p data-end=&quot;1381&quot; data-start=&quot;1349&quot; data-ke-size=&quot;size16&quot;&gt;컨슈머는 &lt;b&gt;카프카에서 메시지를 직접 가져오는 주체&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-end=&quot;1391&quot; data-start=&quot;1383&quot; data-ke-size=&quot;size16&quot;&gt;중요한 사실은, &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Kafka는 메시지를 애플리케이션에게 &amp;ldquo;푸시&amp;rdquo;하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1469&quot; data-start=&quot;1395&quot; data-ke-size=&quot;size16&quot;&gt;컨슈머가 지속적으로 &amp;ldquo;폴링(polling)&amp;rdquo;하여 메시지를 가져간다.&lt;/p&gt;
&lt;p data-end=&quot;1522&quot; data-start=&quot;1471&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;새 메시지 있나요?&amp;rdquo; 하고 Kafka에 계속 물어보는 역할을 담당하는 것이 컨슈머다.&lt;/p&gt;
&lt;p data-end=&quot;1550&quot; data-start=&quot;1524&quot; data-ke-size=&quot;size16&quot;&gt;이를 이해하는 순간 전체 구조가 확실히 잡힌다.&lt;/p&gt;
&lt;h3 data-end=&quot;1581&quot; data-start=&quot;1557&quot; data-ke-size=&quot;size23&quot;&gt;3-2. 리스너(Listener)란?&lt;/h3&gt;
&lt;p data-end=&quot;1681&quot; data-start=&quot;1583&quot; data-ke-size=&quot;size16&quot;&gt;리스너는 스프링에서 제공하는 일종의 &lt;b&gt;콜백 함수&lt;/b&gt;다.&lt;br /&gt;컨슈머가 Kafka에서 메시지를 가져오면, 그 메시지를 전달받아서 비즈니스 로직을 처리하는 메소드가 바로 리스너다.&lt;/p&gt;
&lt;p data-end=&quot;1697&quot; data-start=&quot;1683&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 아래 코드처럼:&lt;/p&gt;
&lt;pre id=&quot;code_1764923426866&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(topics = &quot;order-created&quot;)
public void handleOrder(String message) {
    // 메시지가 들어왔을 때 실행되는 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1942&quot; data-start=&quot;1824&quot; data-ke-size=&quot;size16&quot;&gt;이 메소드 자체가 &amp;ldquo;리스너&amp;rdquo;다.&lt;br /&gt;이 리스너는 메시지를 직접 읽지 않는다.&lt;br /&gt;메시지를 읽는 행위는 &lt;b&gt;컨슈머&lt;/b&gt;가 하고, 읽은 메시지를 리스너에게 전달해주는 역할을 스프링의 &lt;b&gt;리스너 컨테이너&lt;/b&gt;가 대신한다.&lt;/p&gt;
&lt;h2 data-end=&quot;1979&quot; data-start=&quot;1949&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c4&quot; data-end=&quot;1979&quot; data-start=&quot;1949&quot; data-ke-size=&quot;size26&quot;&gt;4. 실제 동작 구조를 하나의 흐름으로 정리해보면&lt;/h2&gt;
&lt;p data-end=&quot;2023&quot; data-start=&quot;1981&quot; data-ke-size=&quot;size16&quot;&gt;카프카를 처음 배울 때 가장 중요한 이해 포인트는 이 그림 하나로 정리된다.&lt;/p&gt;
&lt;pre id=&quot;code_1764923450045&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Kafka  &amp;larr; (pull) &amp;larr;  Consumer  &amp;rarr;  Listener 호출&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2097&quot; data-start=&quot;2078&quot; data-ke-size=&quot;size16&quot;&gt;흐름을 순서대로 보면 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2333&quot; data-start=&quot;2099&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2141&quot; data-start=&quot;2099&quot;&gt;Producer가 Kafka의 특정 Topic에 메시지를 저장한다.&lt;/li&gt;
&lt;li data-end=&quot;2194&quot; data-start=&quot;2142&quot;&gt;Spring 애플리케이션 내부의 Consumer가 Kafka에게 지속적으로 폴링한다.&lt;/li&gt;
&lt;li data-end=&quot;2243&quot; data-start=&quot;2195&quot;&gt;Kafka에서 새 메시지가 발견되면 Consumer가 해당 메시지를 가져온다.&lt;/li&gt;
&lt;li data-end=&quot;2295&quot; data-start=&quot;2244&quot;&gt;Consumer가 가져온 메시지를 Spring이 Listener 메소드에 전달한다.&lt;/li&gt;
&lt;li data-end=&quot;2333&quot; data-start=&quot;2296&quot;&gt;Listener가 메시지를 기반으로 비즈니스 로직을 실행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;2425&quot; data-start=&quot;2335&quot; data-ke-size=&quot;size16&quot;&gt;즉, 핵심은 &lt;b&gt;카프카는 메시지를 직접 보내주지 않는다는 점&lt;/b&gt;이다.&lt;br /&gt;메시지를 읽는 주체는 항상 &lt;b&gt;컨슈머&lt;/b&gt;,&lt;br /&gt;메시지를 처리하는 주체는 &lt;b&gt;리스너&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-end=&quot;2446&quot; data-start=&quot;2427&quot; data-ke-size=&quot;size16&quot;&gt;둘은 역할이 명확히 분리되어 있다.&lt;/p&gt;
&lt;h2 data-end=&quot;2479&quot; data-start=&quot;2453&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c5&quot; data-end=&quot;2479&quot; data-start=&quot;2453&quot; data-ke-size=&quot;size26&quot;&gt;5. 헷갈리는 개념들을 한 번 더 정리하면&lt;/h2&gt;
&lt;h3 data-end=&quot;2505&quot; data-start=&quot;2481&quot; data-ke-size=&quot;size23&quot;&gt;● 카프카는 API 서버가 아니다&lt;/h3&gt;
&lt;p data-end=&quot;2569&quot; data-start=&quot;2506&quot; data-ke-size=&quot;size16&quot;&gt;카프카에 &amp;ldquo;요청&amp;rdquo;을 보내는 것이 아니라,&lt;br /&gt;Producer가 메시지를 보내고 Consumer가 가져가는 구조다.&lt;/p&gt;
&lt;h3 data-end=&quot;2595&quot; data-start=&quot;2571&quot; data-ke-size=&quot;size23&quot;&gt;● 리스너는 메시지를 읽지 않는다&lt;/h3&gt;
&lt;p data-end=&quot;2664&quot; data-start=&quot;2596&quot; data-ke-size=&quot;size16&quot;&gt;리스너는 오직 &amp;ldquo;메시지가 전달되었을 때 실행되는 메소드&amp;rdquo;일 뿐이다.&lt;br /&gt;실제 읽기(read)는 Consumer가 담당한다.&lt;/p&gt;
&lt;h3 data-end=&quot;2698&quot; data-start=&quot;2666&quot; data-ke-size=&quot;size23&quot;&gt;● 카프카는 push가 아니라 pull 방식이다&lt;/h3&gt;
&lt;p data-end=&quot;2780&quot; data-start=&quot;2699&quot; data-ke-size=&quot;size16&quot;&gt;카프카가 Spring에 &amp;ldquo;보내주는 것처럼&amp;rdquo; 보이지만,&lt;br /&gt;실제로는 스프링 내부의 Consumer가 카프카에서 계속 메시지를 가져오고 있을 뿐이다.&lt;/p&gt;
&lt;h3 data-end=&quot;2816&quot; data-start=&quot;2782&quot; data-ke-size=&quot;size23&quot;&gt;● Listener는 Spring이 만든 편의 기능&lt;/h3&gt;
&lt;p data-end=&quot;2917&quot; data-start=&quot;2817&quot; data-ke-size=&quot;size16&quot;&gt;원래 Kafka에는 Listener라는 개념이 없다.&lt;br /&gt;Spring Kafka가 Consumer에서 읽어온 메시지를 자동으로 메소드에 연결해주기 위해 만든 사용성 개선 기능이다.&lt;/p&gt;
&lt;h2 data-end=&quot;2947&quot; data-start=&quot;2924&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c6&quot; data-end=&quot;2947&quot; data-start=&quot;2924&quot; data-ke-size=&quot;size26&quot;&gt;6. 카프카를 처음 도입하는 팀이라면&lt;/h2&gt;
&lt;p data-end=&quot;3087&quot; data-start=&quot;2949&quot; data-ke-size=&quot;size16&quot;&gt;실제 업무에서 카프카를 다루다 보면 Producer보다 Consumer가 훨씬 중요하다는 걸 느끼게 된다.&lt;br /&gt;메시지 중복 처리, 오프셋 관리, 장애 복구 전략, 컨슈머 그룹 운영 등은 모두 Consumer 구조를 정확히 이해하는 것에서 출발한다.&lt;/p&gt;
&lt;p data-end=&quot;3178&quot; data-start=&quot;3089&quot; data-ke-size=&quot;size16&quot;&gt;특히 스프링을 사용한다면 &amp;ldquo;컨슈머가 메시지를 가져온 뒤 리스너에게 넘겨준다&amp;rdquo;는 구조를 명확히 이해하는 것이 전체 로직을 안정적으로 유지하는 데 큰 도움이 된다.&lt;/p&gt;
&lt;h2 data-end=&quot;3191&quot; data-start=&quot;3185&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3191&quot; data-start=&quot;3185&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-end=&quot;3319&quot; data-start=&quot;3193&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 나 역시 카프카가 메시지를 스프링 애플리케이션으로 직접 &amp;ldquo;보내주는&amp;rdquo; 방식이라고 오해했다.&lt;br /&gt;하지만 내부 구조를 정확히 알고 나면, 컨슈머와 리스너가 왜 분리되어 있고 어떤 역할을 맡고 있는지 훨씬 명확하게 정리된다.&lt;/p&gt;
&lt;p data-end=&quot;3428&quot; data-start=&quot;3321&quot; data-ke-size=&quot;size16&quot;&gt;이 글이 카프카를 처음 접하는 개발자들에게 도움이 되길 바라며,&lt;br /&gt;향후에는 실제 예제 코드와 함께 컨슈머 그룹 운영 방식, 파티션 전략, 재처리 로직 등 좀 더 깊은 내용을 다뤄볼 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>consumer</category>
      <category>Kafka</category>
      <category>리스너</category>
      <category>메시지큐</category>
      <category>카프카</category>
      <category>카프카 개념정리</category>
      <category>카프카 구조</category>
      <category>카프카 메시징</category>
      <category>카프카 토픽</category>
      <category>컨슈머</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/94</guid>
      <comments>https://kwakscoding.tistory.com/94#entry94comment</comments>
      <pubDate>Fri, 5 Dec 2025 17:36:27 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] JUnit5 + Mockito 단위 테스트 기초 정리</title>
      <link>https://kwakscoding.tistory.com/93</link>
      <description>&lt;blockquote data-end=&quot;106&quot; data-start=&quot;34&quot; data-ke-style=&quot;style3&quot;&gt;목차&lt;br /&gt;&lt;a href=&quot;#c0&quot;&gt;0. 서론&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c1&quot;&gt;1. 단위 테스트에서 우리가 하고 싶은 일&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c2&quot;&gt;2. 아주 쉬운 예제로 시작해 보기&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c3&quot;&gt;3. Mockito 기본 세팅&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c4&quot;&gt;4. @Test, @DisplayName, @Nested &amp;ndash; 테스트 구조 잡기&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c5&quot;&gt;5. Mockito 기본 동작 &amp;ndash; when/thenReturn, verify, verifyNoInteractions&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c6&quot;&gt;6. 정리: 테스트 코드 작성 흐름&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;#c7&quot;&gt;7. 마무리&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-end=&quot;106&quot; data-start=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yamIX/dJMcaiIuqsy/mBlU8WPHlMR6PZGWFVSj31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yamIX/dJMcaiIuqsy/mBlU8WPHlMR6PZGWFVSj31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yamIX/dJMcaiIuqsy/mBlU8WPHlMR6PZGWFVSj31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyamIX%2FdJMcaiIuqsy%2FmBlU8WPHlMR6PZGWFVSj31%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;611&quot; height=&quot;235&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-end=&quot;106&quot; data-start=&quot;34&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c0&quot; data-end=&quot;106&quot; data-start=&quot;34&quot; data-ke-size=&quot;size26&quot;&gt;0. 서론&lt;/h2&gt;
&lt;p data-end=&quot;106&quot; data-start=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;@Mock, @InjectMocks, @ExtendWith, @Nested, @DisplayName까지 한 번에&lt;/p&gt;
&lt;p data-end=&quot;210&quot; data-start=&quot;108&quot; data-ke-size=&quot;size16&quot;&gt;요즘 스프링 개발을 하다 보면 &amp;ldquo;테스트 코드도 같이 작성해 주세요&amp;rdquo;라는 말을 자주 듣는다.&lt;br /&gt;문제는 제대로 배워본 적이 없어서, 처음 테스트 코드를 보면 주술처럼 느껴진다는 점이다.&lt;/p&gt;
&lt;p data-end=&quot;210&quot; data-start=&quot;108&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764207828518&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
@Mock
@InjectMocks
@Nested
@DisplayName
when().thenReturn(...)
verify(...)
verifyNoInteractions(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;534&quot; data-start=&quot;362&quot; data-ke-size=&quot;size16&quot;&gt;이런 것들이 한꺼번에 쏟아지면 &amp;ldquo;그냥 서비스 하나 더 만드는 게 빠르겠다&amp;rdquo;는 생각이 들 수도 있다.&lt;br /&gt;이번 글에서는 내가 실제로 테스트 코드를 공부하면서 정리한 내용을 바탕으로, 위 어노테이션과 메소드들이 &lt;b&gt;무엇을 하는지, 어떤 흐름으로 단위 테스트를 작성하면 되는지&lt;/b&gt;를 쉬운 예제와 함께 정리해 본다.&lt;/p&gt;
&lt;p data-end=&quot;570&quot; data-start=&quot;536&quot; data-ke-size=&quot;size16&quot;&gt;특히 JUnit5 + Mockito 조합을 기준으로 설명한다.&lt;/p&gt;
&lt;p data-end=&quot;570&quot; data-start=&quot;536&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c1&quot; data-end=&quot;603&quot; data-start=&quot;577&quot; data-ke-size=&quot;size26&quot;&gt;1. 단위 테스트에서 우리가 하고 싶은 일&lt;/h2&gt;
&lt;p data-end=&quot;630&quot; data-start=&quot;605&quot; data-ke-size=&quot;size16&quot;&gt;단위 테스트의 목표는 결국 한 줄로 요약된다.&lt;/p&gt;
&lt;p data-end=&quot;630&quot; data-start=&quot;605&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;630&quot; data-start=&quot;605&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;ldquo;이 메서드를 이렇게 호출했을 때, &lt;/span&gt;&lt;/b&gt;&lt;b&gt;이 결과와 이 동작이 일어나야 한다.&amp;rdquo;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;693&quot; data-start=&quot;682&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;693&quot; data-start=&quot;682&quot; data-ke-size=&quot;size16&quot;&gt;조금 더 세분화하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;819&quot; data-start=&quot;695&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;735&quot; data-start=&quot;695&quot;&gt;&lt;b&gt;파라미터 검증&lt;/b&gt;: 잘못된 값이 들어오면 예외가 발생해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;785&quot; data-start=&quot;736&quot;&gt;&lt;b&gt;도메인/레포지토리 호출 검증&lt;/b&gt;: 내부적으로 어떤 메서드를 호출했는지 확인한다.&lt;/li&gt;
&lt;li data-end=&quot;819&quot; data-start=&quot;786&quot;&gt;&lt;b&gt;결과 검증&lt;/b&gt;: 리턴 값이 기대한 값인지 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;889&quot; data-start=&quot;821&quot; data-ke-size=&quot;size16&quot;&gt;이걸 도와주는 도구가 바로&lt;br /&gt;JUnit5(테스트 프레임워크)와 Mockito(가짜 객체, Mock 생성용 라이브러리)다.&lt;/p&gt;
&lt;h2 data-end=&quot;918&quot; data-start=&quot;896&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDX5zZ/dJMcaa4NElq/JyUjsBFqIagYWvc306hY81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDX5zZ/dJMcaa4NElq/JyUjsBFqIagYWvc306hY81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDX5zZ/dJMcaa4NElq/JyUjsBFqIagYWvc306hY81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDX5zZ%2FdJMcaa4NElq%2FJyUjsBFqIagYWvc306hY81%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;618&quot; height=&quot;236&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;c2&quot; data-end=&quot;918&quot; data-start=&quot;896&quot; data-ke-size=&quot;size26&quot;&gt;2. 아주 쉬운 예제로 시작해 보기&lt;/h2&gt;
&lt;p data-end=&quot;992&quot; data-start=&quot;920&quot; data-ke-size=&quot;size16&quot;&gt;실제 업무 코드는 도메인 규칙이 복잡해서 처음 학습용으로 쓰기엔 조금 무겁다.&lt;br /&gt;그래서 최대한 단순한 예제를 하나 만들어 보자.&lt;/p&gt;
&lt;p data-end=&quot;992&quot; data-start=&quot;920&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;992&quot; data-start=&quot;920&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1022&quot; data-start=&quot;994&quot; data-ke-size=&quot;size23&quot;&gt;2-1. 예제 도메인: 사용자 인사말 서비스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1091&quot; data-start=&quot;1024&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1052&quot; data-start=&quot;1024&quot;&gt;UserRepository에서 사용자를 찾고&lt;/li&gt;
&lt;li data-end=&quot;1091&quot; data-start=&quot;1053&quot;&gt;사용자 이름으로 &amp;ldquo;Hello {이름}&amp;rdquo; 메세지를 만들어주는 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1764207958188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// src/main/java/.../User.java
public class User {
    private final Long id;
    private final String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() { return id; }

    public String getName() { return name; }
}







// src/main/java/.../UserRepository.java
public interface UserRepository {
    User findById(Long id);
}







// src/main/java/.../GreetingService.java
public class GreetingService {

    private final UserRepository userRepository;

    public GreetingService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String getGreeting(Long userId) {
        if (userId == null) {
            throw new IllegalArgumentException(&quot;userId는 null일 수 없습니다.&quot;);
        }

        User user = userRepository.findById(userId);
        if (user == null) {
            throw new IllegalArgumentException(&quot;사용자를 찾을 수 없습니다.&quot;);
        }

        return &quot;Hello &quot; + user.getName();
    }
}&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;우리가 테스트하고 싶은 메서드는 GreetingService#getGreeting 하나다.&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 id=&quot;c3&quot; data-end=&quot;2247&quot; data-start=&quot;2193&quot; data-ke-size=&quot;size26&quot;&gt;3. Mockito 기본 세팅 &amp;ndash; @ExtendWith, @Mock, @InjectMocks&lt;/h2&gt;
&lt;h3 data-end=&quot;2293&quot; data-start=&quot;2249&quot; data-ke-size=&quot;size23&quot;&gt;3-1. @ExtendWith(MockitoExtension.class)&lt;/h3&gt;
&lt;pre id=&quot;code_1764207993704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class GreetingServiceTest {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2490&quot; data-start=&quot;2382&quot; data-ke-size=&quot;size16&quot;&gt;Unit5에서는 @ExtendWith를 통해 테스트에 &amp;ldquo;확장 기능&amp;rdquo;을 붙일 수 있는데,&lt;br /&gt;MockitoExtension은 말 그대로 &amp;ldquo;Mockito를 쓸 수 있는 환경&amp;rdquo;을 만들어준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2576&quot; data-start=&quot;2492&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2522&quot; data-start=&quot;2492&quot;&gt;@Mock으로 가짜 객체를 만들 수 있게 해주고&lt;/li&gt;
&lt;li data-end=&quot;2576&quot; data-start=&quot;2523&quot;&gt;@InjectMocks로 테스트 대상 객체를 생성할 때 Mock들을 자동으로 주입해 준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2589&quot; data-start=&quot;2578&quot; data-ke-size=&quot;size16&quot;&gt;라고 생각하면 된다.&lt;/p&gt;
&lt;p data-end=&quot;2589&quot; data-start=&quot;2578&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2617&quot; data-start=&quot;2591&quot; data-ke-size=&quot;size23&quot;&gt;3-2. @Mock &amp;ndash; 가짜 객체 만들기&lt;/h3&gt;
&lt;pre id=&quot;code_1764208057837&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Mock
UserRepository userRepository;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2722&quot; data-start=&quot;2669&quot; data-ke-size=&quot;size16&quot;&gt;@Mock이 붙은 필드는 진짜 구현이 아니라 &lt;b&gt;Mockito가 만들어주는 가짜 객체&lt;/b&gt;다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2870&quot; data-start=&quot;2724&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2745&quot; data-start=&quot;2724&quot;&gt;실제 DB는 전혀 사용하지 않는다.&lt;/li&gt;
&lt;li data-end=&quot;2817&quot; data-start=&quot;2746&quot;&gt;when(&amp;hellip;).thenReturn(&amp;hellip;) 형태로&lt;br /&gt;&amp;ldquo;이 메서드가 호출되면 이런 값을 돌려줘&amp;rdquo;라고 지정해 줄 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2870&quot; data-start=&quot;2818&quot;&gt;어떤 메서드가 어떻게 호출되었는지 기록해 두었다가 verify(&amp;hellip;)로 검증할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2919&quot; data-start=&quot;2872&quot; data-ke-size=&quot;size23&quot;&gt;3-3. @InjectMocks &amp;ndash; Mock들을 끼워 넣어서 테스트 대상 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1764208085687&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@InjectMocks
GreetingService greetingService;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GreetingService는 생성자에서 UserRepository를 받는다. (생성자주입)&lt;/p&gt;
&lt;pre id=&quot;code_1764208154096&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public GreetingService(UserRepository userRepository) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트에서는 UserRepository를 @Mock으로 만들어두었기 때문에, 아래 코드와 동일한 효과가 난다&lt;/p&gt;
&lt;pre id=&quot;code_1764208187188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Mockito가 내부적으로 해주는 일
greetingService = new GreetingService(userRepository);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3265&quot; data-start=&quot;3263&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3362&quot; data-start=&quot;3267&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3293&quot; data-start=&quot;3267&quot;&gt;@Mock &amp;rarr; &lt;b&gt;부품(가짜 의존성)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;3362&quot; data-start=&quot;3294&quot;&gt;@InjectMocks &amp;rarr; 그 부품을 끼워 넣은 &lt;b&gt;테스트 대상 객체&lt;/b&gt;(GreetingService 인스턴스) (System&amp;nbsp;Under&amp;nbsp;Test&amp;rdquo;&amp;nbsp;=&amp;nbsp;테스트할&amp;nbsp;대상&amp;nbsp;클래스.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3373&quot; data-start=&quot;3364&quot; data-ke-size=&quot;size16&quot;&gt;라고 보면 된다.&lt;/p&gt;
&lt;p data-end=&quot;3373&quot; data-start=&quot;3364&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;c4&quot; data-end=&quot;3426&quot; data-start=&quot;3380&quot; data-ke-size=&quot;size26&quot;&gt;4. @Test, @DisplayName, @Nested &amp;ndash; 테스트 구조 잡기&lt;/h2&gt;
&lt;h3 data-end=&quot;3459&quot; data-start=&quot;3428&quot; data-ke-size=&quot;size23&quot;&gt;4-1. @Test &amp;ndash; 이 메서드는 테스트 함수다&lt;/h3&gt;
&lt;pre id=&quot;code_1764208255164&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void 성공_정상_사용자_인사말_반환() { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3586&quot; data-start=&quot;3512&quot; data-ke-size=&quot;size16&quot;&gt;JUnit5에게 &amp;ldquo;이 메서드는 테스트로 실행해 달라&amp;rdquo;고 알려주는 어노테이션이다.&lt;br /&gt;@Test가 붙은 메서드만 테스트로 인식된다.&lt;/p&gt;
&lt;h3 data-end=&quot;3625&quot; data-start=&quot;3588&quot; data-ke-size=&quot;size23&quot;&gt;4-2. @DisplayName &amp;ndash; 실행 결과에 보여줄 이름&lt;/h3&gt;
&lt;pre id=&quot;code_1764208293441&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;정상 사용자 ID를 넣으면 Hello {이름} 문자열을 반환한다&quot;)
void 성공_정상_사용자_인사말_반환() { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3798&quot; data-start=&quot;3730&quot; data-ke-size=&quot;size16&quot;&gt;DE나 콘솔에서 테스트가 실행될 때,&lt;br /&gt;성공_정상_사용자_인사말_반환 대신 사람이 읽기 좋은 문장으로 표시해 준다.&lt;/p&gt;
&lt;p data-end=&quot;3815&quot; data-start=&quot;3800&quot; data-ke-size=&quot;size16&quot;&gt;실행 결과가 이렇게 보인다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3874&quot; data-start=&quot;3817&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3874&quot; data-start=&quot;3817&quot;&gt;getGreeting
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3874&quot; data-start=&quot;3835&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3874&quot; data-start=&quot;3835&quot;&gt;정상 사용자 ID를 넣으면 Hello {이름} 문자열을 반환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3915&quot; data-start=&quot;3876&quot; data-ke-size=&quot;size16&quot;&gt;테스트 결과를 나중에 다시 볼 때, 어떤 시나리오였는지 한눈에 보인다.&lt;/p&gt;
&lt;h3 data-end=&quot;3948&quot; data-start=&quot;3917&quot; data-ke-size=&quot;size23&quot;&gt;4-3. @Nested &amp;ndash; 테스트를 기능별로 묶기&lt;/h3&gt;
&lt;p data-end=&quot;3984&quot; data-start=&quot;3950&quot; data-ke-size=&quot;size16&quot;&gt;@Nested는 &amp;ldquo;테스트를 그룹으로 묶고 싶을 때&amp;rdquo; 쓴다.&lt;/p&gt;
&lt;pre id=&quot;code_1764208385863&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Nested
@DisplayName(&quot;getGreeting 메서드&quot;)
class GetGreeting {

    @Test
    @DisplayName(&quot;정상 사용자 ID를 넣으면 Hello {이름} 문자열을 반환한다&quot;)
    void 성공_정상_사용자_인사말_반환() { ... }

    @Test
    @DisplayName(&quot;userId가 null이면 예외를 던지고 UserRepository는 호출하지 않는다&quot;)
    void 실패_userId_null() { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4300&quot; data-start=&quot;4277&quot; data-ke-size=&quot;size16&quot;&gt;실행 결과 계층도는 다음과 같이 정리된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4417&quot; data-start=&quot;4302&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4417&quot; data-start=&quot;4302&quot;&gt;getGreeting 메서드
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4417&quot; data-start=&quot;4324&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4363&quot; data-start=&quot;4324&quot;&gt;정상 사용자 ID를 넣으면 Hello {이름} 문자열을 반환한다&lt;/li&gt;
&lt;li data-end=&quot;4417&quot; data-start=&quot;4366&quot;&gt;userId가 null이면 예외를 던지고 UserRepository는 호출하지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4485&quot; data-start=&quot;4419&quot; data-ke-size=&quot;size16&quot;&gt;여러 메서드를 테스트할 때는 이렇게 기능 단위로 그룹을 나눠두면,&lt;br /&gt;테스트 클래스가 커져도 구조가 눈에 잘 들어온다.&lt;/p&gt;
&lt;h2 data-end=&quot;4559&quot; data-start=&quot;4492&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c5&quot; data-end=&quot;4559&quot; data-start=&quot;4492&quot; data-ke-size=&quot;size26&quot;&gt;5. Mockito 기본 동작 &amp;ndash; when/thenReturn, verify, verifyNoInteractions&lt;/h2&gt;
&lt;p data-end=&quot;4595&quot; data-start=&quot;4561&quot; data-ke-size=&quot;size16&quot;&gt;이제 실제 예제 테스트 코드를 보면서 메소드들을 정리해 보자.&lt;/p&gt;
&lt;h3 data-end=&quot;4616&quot; data-start=&quot;4597&quot; data-ke-size=&quot;size23&quot;&gt;5-1. 성공 케이스 테스트&lt;/h3&gt;
&lt;pre id=&quot;code_1764208431880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class GreetingServiceTest {

    @Mock
    UserRepository userRepository;

    @InjectMocks
    GreetingService greetingService;

    @Nested
    @DisplayName(&quot;getGreeting 메서드&quot;)
    class GetGreeting {

        @Test
        @DisplayName(&quot;정상 사용자 ID를 넣으면 Hello {이름} 문자열을 반환한다&quot;)
        void 성공_정상_사용자_인사말_반환() {
            // given
            Long userId = 1L;
            User user = new User(userId, &quot;치영&quot;);

            // userRepository.findById(1L)가 호출되면 user를 반환하도록 설정
            when(userRepository.findById(userId))
                    .thenReturn(user);

            // when
            String result = greetingService.getGreeting(userId);

            // then
            assertEquals(&quot;Hello 치영&quot;, result);

            // userRepository.findById(1L)가 실제로 한 번 호출되었는지 검증
            verify(userRepository).findById(userId);

            // 그 외 다른 메서드는 호출되지 않았는지 검증
            verifyNoMoreInteractions(userRepository);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5631&quot; data-start=&quot;5613&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 포인트만 짚어보자.&lt;/p&gt;
&lt;h4 data-end=&quot;5659&quot; data-start=&quot;5633&quot; data-ke-size=&quot;size20&quot;&gt;when(&amp;hellip;).thenReturn(&amp;hellip;)&lt;/h4&gt;
&lt;pre id=&quot;code_1764208458202&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;when(userRepository.findById(userId))
        .thenReturn(user);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5859&quot; data-start=&quot;5739&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5783&quot; data-start=&quot;5739&quot;&gt;Mock 객체는 실제 구현이 없기 때문에 우리가 동작을 &amp;ldquo;설정&amp;rdquo;해줘야 한다.&lt;/li&gt;
&lt;li data-end=&quot;5859&quot; data-start=&quot;5784&quot;&gt;&amp;ldquo;테스트 중에 userRepository.findById(1L)가 호출되면,&lt;br /&gt;user 객체를 돌려줘라&amp;rdquo; 라는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;5888&quot; data-start=&quot;5861&quot; data-ke-size=&quot;size20&quot;&gt;assertEquals(기대값, 실제값)&lt;/h4&gt;
&lt;pre id=&quot;code_1764208483113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;assertEquals(&quot;Hello 치영&quot;, result);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5982&quot; data-start=&quot;5937&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5955&quot; data-start=&quot;5937&quot;&gt;JUnit의 기본 검증 메소드&lt;/li&gt;
&lt;li data-end=&quot;5982&quot; data-start=&quot;5956&quot;&gt;기대값과 실제값이 같으면 성공, 다르면 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;6009&quot; data-start=&quot;5984&quot; data-ke-size=&quot;size20&quot;&gt;verify(mock).메서드(인자)&lt;/h4&gt;
&lt;pre id=&quot;code_1764208505828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;verify(userRepository).findById(userId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6122&quot; data-start=&quot;6065&quot;&gt;&amp;ldquo;테스트를 실행하는 동안 userRepository.findById(1L)가 호출되었는지&amp;rdquo; 확인&lt;/li&gt;
&lt;li data-end=&quot;6187&quot; data-start=&quot;6123&quot;&gt;Mock 객체는 모든 호출을 기록해 두었다가,&lt;br /&gt;verify가 그 기록을 검사한다고 생각하면 이해가 쉽다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;메소드 형태가 조금 햇갈릴 수 있는데 이렇게 이해하면 쉽다.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;userRepository객체를 verify메소드로 검증하라 findById메소드가 호출되었는지!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1764208609439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;verifyNoMoreInteractions(userRepository);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6393&quot; data-start=&quot;6281&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6342&quot; data-start=&quot;6281&quot;&gt;위에서 검증한 호출(여기서는 findById) 외에&lt;br /&gt;다른 메서드가 호출되면 테스트를 실패시킨다.&lt;/li&gt;
&lt;li data-end=&quot;6393&quot; data-start=&quot;6343&quot;&gt;&amp;ldquo;의도한 것 이외의 추가 호출이 없어야 한다&amp;rdquo;는 걸 강하게 보장하고 싶을 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;6461&quot; data-start=&quot;6400&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;6461&quot; data-start=&quot;6400&quot; data-ke-size=&quot;size23&quot;&gt;5-2. 실패 케이스 &amp;ndash; 파라미터 검증과 assertThrows, verifyNoInteractions&lt;/h3&gt;
&lt;p data-end=&quot;6495&quot; data-start=&quot;6463&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 userId가 null일 때를 테스트해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1764208633448&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Nested
@DisplayName(&quot;getGreeting 메서드&quot;)
class GetGreeting {

    @Test
    @DisplayName(&quot;userId가 null이면 예외를 던지고 UserRepository는 호출하지 않는다&quot;)
    void 실패_userId_null() {
        // when &amp;amp; then
        IllegalArgumentException ex = assertThrows(
                IllegalArgumentException.class,
                () -&amp;gt; greetingService.getGreeting(null)
        );

        assertEquals(&quot;userId는 null일 수 없습니다.&quot;, ex.getMessage());

        // 어떤 메서드도 호출되면 안 된다.
        verifyNoInteractions(userRepository);
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; assertThrows &lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764208654238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;IllegalArgumentException ex = assertThrows(
        IllegalArgumentException.class,
        () -&amp;gt; greetingService.getGreeting(null)
);&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7263&quot; data-start=&quot;7203&quot;&gt;&amp;ldquo;이 코드를 실행했을 때 반드시 IllegalArgumentException이 발생해야 한다&amp;rdquo;는 의미&lt;/li&gt;
&lt;li data-end=&quot;7301&quot; data-start=&quot;7264&quot;&gt;예외가 발생하지 않거나 다른 예외가 발생하면 테스트는 실패한다.&lt;/li&gt;
&lt;li data-end=&quot;7341&quot; data-start=&quot;7302&quot;&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;&lt;b&gt;assertEquals&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764208761422&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;assertEquals(&quot;userId는 null일 수 없습니다.&quot;, ex.getMessage());&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;ldquo;예외 메시지가 내가 기대한 문자열과 정확히 같은지&amp;rdquo;**를 검증하는 코드다.&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;b&gt; verifyNoInteractions &lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764208786276&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;verifyNoInteractions(userRepository);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7693&quot; data-start=&quot;7490&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7536&quot; data-start=&quot;7490&quot;&gt;해당 Mock 객체에 대해 &lt;b&gt;어떠한 메서드 호출도 없어야 한다&lt;/b&gt;는 검증이다.&lt;/li&gt;
&lt;li data-end=&quot;7621&quot; data-start=&quot;7537&quot;&gt;userId == null이면 서비스 메서드는 바로 예외를 던지고 끝나야 하기 때문에&lt;br /&gt;findById 같은 호출이 일어나면 안 된다.&lt;/li&gt;
&lt;li data-end=&quot;7693&quot; data-start=&quot;7622&quot;&gt;이런 상황에 verifyNoInteractions를 사용하면 &amp;ldquo;파라미터 검증에서 바로 빠져나왔는지&amp;rdquo;까지 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;7722&quot; data-start=&quot;7700&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c6&quot; data-end=&quot;7722&quot; data-start=&quot;7700&quot; data-ke-size=&quot;size26&quot;&gt;6. 정리: 테스트 코드 작성 흐름&lt;/h2&gt;
&lt;p data-end=&quot;7798&quot; data-start=&quot;7724&quot; data-ke-size=&quot;size16&quot;&gt;이제까지 나온 내용을 정리하면,&lt;br /&gt;JUnit5 + Mockito로 단위 테스트를 작성할 때 대략 이런 흐름으로 코드를 짤 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;8431&quot; data-start=&quot;7800&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;8004&quot; data-start=&quot;7800&quot;&gt;&lt;b&gt;테스트 클래스 기본 구조&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8004&quot; data-start=&quot;7824&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7876&quot; data-start=&quot;7824&quot;&gt;@ExtendWith(MockitoExtension.class)로 Mockito 활성화&lt;/li&gt;
&lt;li data-end=&quot;7926&quot; data-start=&quot;7880&quot;&gt;@Mock으로 의존성(Repository, 외부 서비스 등) 가짜 객체 생성&lt;/li&gt;
&lt;li data-end=&quot;7961&quot; data-start=&quot;7930&quot;&gt;@InjectMocks로 테스트 대상 서비스 생성&lt;/li&gt;
&lt;li data-end=&quot;8004&quot; data-start=&quot;7965&quot;&gt;기능별로 @Nested + @DisplayName으로 그룹화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;8280&quot; data-start=&quot;8006&quot;&gt;&lt;b&gt;성공 케이스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8280&quot; data-start=&quot;8023&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8090&quot; data-start=&quot;8023&quot;&gt;given
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8090&quot; data-start=&quot;8036&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8090&quot; data-start=&quot;8036&quot;&gt;when(mock.method(...)).thenReturn(...)로 Mock 동작 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;8118&quot; data-start=&quot;8094&quot;&gt;when
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8118&quot; data-start=&quot;8106&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8118&quot; data-start=&quot;8106&quot;&gt;서비스 메서드 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;8280&quot; data-start=&quot;8122&quot;&gt;then
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8280&quot; data-start=&quot;8134&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8174&quot; data-start=&quot;8134&quot;&gt;assertEquals, assertThat 등으로 결과 검증&lt;/li&gt;
&lt;li data-end=&quot;8225&quot; data-start=&quot;8180&quot;&gt;verify(mock).method(...)로 필요한 호출이 있었는지 검증&lt;/li&gt;
&lt;li data-end=&quot;8280&quot; data-start=&quot;8231&quot;&gt;verifyNoMoreInteractions(mock)로 불필요한 추가 호출 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;8431&quot; data-start=&quot;8282&quot;&gt;&lt;b&gt;실패 케이스(파라미터 검증, 예외)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8431&quot; data-start=&quot;8312&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8345&quot; data-start=&quot;8312&quot;&gt;assertThrows로 특정 예외가 발생하는지 확인&lt;/li&gt;
&lt;li data-end=&quot;8378&quot; data-start=&quot;8349&quot;&gt;예외 메시지까지 assertEquals로 검증&lt;/li&gt;
&lt;li data-end=&quot;8431&quot; data-start=&quot;8382&quot;&gt;verifyNoInteractions(mock)로 내부 호출이 전혀 없었는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;8538&quot; data-start=&quot;8433&quot; data-ke-size=&quot;size16&quot;&gt;테스트 코드도 결국 &amp;ldquo;코드&amp;rdquo;이기 때문에,&lt;br /&gt;한 번에 완벽하게 이해하려고 하기보다는&lt;br /&gt;이런 작은 예제에서 흐름을 익히고,&lt;br /&gt;실제 업무 코드에 하나씩 적용해 보는 게 제일 빠른 것 같다.&lt;/p&gt;
&lt;h2 data-end=&quot;8554&quot; data-start=&quot;8545&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 id=&quot;c7&quot; data-end=&quot;8554&quot; data-start=&quot;8545&quot; data-ke-size=&quot;size26&quot;&gt;7. 마무리&lt;/h2&gt;
&lt;p data-end=&quot;8581&quot; data-start=&quot;8556&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 다음 내용을 중심으로 정리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;8894&quot; data-start=&quot;8583&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;8662&quot; data-start=&quot;8583&quot;&gt;@ExtendWith(MockitoExtension.class), @Mock, @InjectMocks가 각각 어떤 역할을 하는지&lt;/li&gt;
&lt;li data-end=&quot;8712&quot; data-start=&quot;8663&quot;&gt;@Nested, @DisplayName으로 테스트 구조를 어떻게 잡으면 좋은지&lt;/li&gt;
&lt;li data-end=&quot;8838&quot; data-start=&quot;8713&quot;&gt;when().thenReturn(), verify(), verifyNoInteractions(), verifyNoMoreInteractions() 같은 Mockito 메서드들이 실제로 어떤 상황에서 쓰이는지&lt;/li&gt;
&lt;li data-end=&quot;8894&quot; data-start=&quot;8839&quot;&gt;assertThrows, assertEquals를 이용해 예외와 결과를 어떻게 검증하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;8986&quot; data-start=&quot;8896&quot; data-ke-size=&quot;size16&quot;&gt;처음 테스트 코드를 볼 때는 문법 하나하나가 낯설지만,&lt;br /&gt;실제로는 &amp;ldquo;서비스가 어떤 메서드를 호출해야 하고, 어떤 결과를 내야 하는지&amp;rdquo;를 코드로 정리한 것뿐이다.&lt;/p&gt;
&lt;p data-end=&quot;9016&quot; data-start=&quot;8988&quot; data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서도 오늘 사용한 예제 패턴 그대로:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;9075&quot; data-start=&quot;9018&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9034&quot; data-start=&quot;9018&quot;&gt;파라미터 검증 실패 케이스&lt;/li&gt;
&lt;li data-end=&quot;9050&quot; data-start=&quot;9035&quot;&gt;정상 흐름(성공 케이스)&lt;/li&gt;
&lt;li data-end=&quot;9075&quot; data-start=&quot;9051&quot;&gt;레포지토리/도메인 메서드 호출 여부 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;9120&quot; data-start=&quot;9077&quot; data-ke-size=&quot;size16&quot;&gt;을 쌓아 나가다 보면, 어느 순간 테스트 코드를 짜는 게 자연스러워질 것이다.&lt;/p&gt;
&lt;p data-end=&quot;9270&quot; data-start=&quot;9122&quot; data-ke-size=&quot;size16&quot;&gt;필요하다면 다음 글에서는&lt;br /&gt;Spring Boot 환경에서 @SpringBootTest와 Mock 없이 실제 Bean을 띄워 놓고 검증하는 통합 테스트,&lt;br /&gt;혹은 도메인 레벨에서 엔티티/VO만 가지고 순수 자바 단위 테스트를 작성하는 방법도 정리해 볼 예정이다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>@InjectMocks</category>
      <category>@Mock</category>
      <category>java 테스트</category>
      <category>Junit5</category>
      <category>mockito</category>
      <category>Spring Boot Test</category>
      <category>단위테스트</category>
      <category>스프링부트</category>
      <category>테스트 코드 입문</category>
      <category>테스트코드</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/93</guid>
      <comments>https://kwakscoding.tistory.com/93#entry93comment</comments>
      <pubDate>Thu, 27 Nov 2025 11:26:56 +0900</pubDate>
    </item>
    <item>
      <title>Git Rebase vs Merge, HEAD&amp;middot;base&amp;middot;hash까지 한 번에 이해하기</title>
      <link>https://kwakscoding.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;h2 data-end=&quot;759&quot; data-start=&quot;754&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;759&quot; data-start=&quot;754&quot; data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;932&quot; data-start=&quot;760&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;774&quot; data-start=&quot;760&quot;&gt;왜 리베이스를 쓰나?&lt;/li&gt;
&lt;li data-end=&quot;799&quot; data-start=&quot;775&quot;&gt;그림으로 보는 rebase와 merge&lt;/li&gt;
&lt;li data-end=&quot;837&quot; data-start=&quot;800&quot;&gt;HEAD / base / hash &amp;mdash; 꼭 알아야 할 내부 개념&lt;/li&gt;
&lt;li data-end=&quot;870&quot; data-start=&quot;838&quot;&gt;실전: 팀에서 쓰는 안전한 절차(FF-only 기준)&lt;/li&gt;
&lt;li data-end=&quot;897&quot; data-start=&quot;871&quot;&gt;리베이스 vs 병합, 언제 어떤 걸 쓰나?&lt;/li&gt;
&lt;li data-end=&quot;914&quot; data-start=&quot;898&quot;&gt;자주 묻는 질문(FAQ)&lt;/li&gt;
&lt;li data-end=&quot;932&quot; data-start=&quot;915&quot;&gt;용어 정리(초급 개발자용)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;937&quot; data-start=&quot;934&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;956&quot; data-start=&quot;939&quot; data-ke-size=&quot;size26&quot;&gt;1) 왜 리베이스를 쓰나?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1079&quot; data-start=&quot;958&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;994&quot; data-start=&quot;958&quot;&gt;&lt;b&gt;히스토리를 직선(Linear)으로 정리&lt;/b&gt;하고 싶을 때&lt;/li&gt;
&lt;li data-end=&quot;1028&quot; data-start=&quot;995&quot;&gt;&lt;b&gt;충돌을 커밋 단위로 깔끔하게&lt;/b&gt; 해결하고 싶을 때&lt;/li&gt;
&lt;li data-end=&quot;1079&quot; data-start=&quot;1029&quot;&gt;저장소가 &lt;b&gt;Fast-forward only&lt;/b&gt; 정책일 때(merge 커밋 없이 병합)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1090&quot; data-start=&quot;1081&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1090&quot; data-start=&quot;1081&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;핵심 한 줄 : &lt;span style=&quot;color: #666666; text-align: center;&quot;&gt;내(feature) 커밋들을 최신 dev 뒤로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;다시 붙여&lt;/b&gt;&lt;span style=&quot;color: #666666; text-align: center;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;병합을 깔끔하게 만든다.&lt;/span&gt; &amp;nbsp;&lt;/u&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;1178&quot; data-start=&quot;1151&quot; data-ke-size=&quot;size26&quot;&gt;2) 그림으로 보는 rebase와 merge&lt;/h2&gt;
&lt;p data-end=&quot;1198&quot; data-start=&quot;1180&quot; data-ke-size=&quot;size16&quot;&gt;아래 그림들을 순서대로 넣으세요.&lt;/p&gt;
&lt;h3 data-end=&quot;1227&quot; data-start=&quot;1200&quot; data-ke-size=&quot;size23&quot;&gt;2-1. 분기된 상태 (리베이스/병합 전)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;01_before_branch.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdAeww/dJMcacg9qCU/5Xp41t6nPP4teCum2R0gQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdAeww/dJMcacg9qCU/5Xp41t6nPP4teCum2R0gQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdAeww/dJMcacg9qCU/5Xp41t6nPP4teCum2R0gQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdAeww%2FdJMcacg9qCU%2F5Xp41t6nPP4teCum2R0gQK%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;900&quot; height=&quot;400&quot; data-filename=&quot;01_before_branch.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1368&quot; data-start=&quot;1319&quot; data-ke-size=&quot;size16&quot;&gt;이 상태에서 feature는 &lt;b&gt;예전 dev(C)&lt;/b&gt; 를 기준으로 작업된 상태입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1412&quot; data-start=&quot;1370&quot; data-ke-size=&quot;size23&quot;&gt;2-2. Rebase 후 (feature를 dev 최신 뒤로 재배치)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;02_rebase_result.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zjtWO/dJMcagqjSOK/UVxI49qw4nMRpssU0uCXh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zjtWO/dJMcagqjSOK/UVxI49qw4nMRpssU0uCXh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zjtWO/dJMcagqjSOK/UVxI49qw4nMRpssU0uCXh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzjtWO%2FdJMcagqjSOK%2FUVxI49qw4nMRpssU0uCXh0%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;900&quot; height=&quot;400&quot; data-filename=&quot;02_rebase_result.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1599&quot; data-start=&quot;1504&quot; data-ke-size=&quot;size16&quot;&gt;feature의 커밋들이 &lt;b&gt;dev 최신 뒤&lt;/b&gt;로 &lt;b&gt;다시 쌓임&lt;/b&gt;(D&amp;rarr;D&amp;rsquo;, E&amp;rarr;E&amp;rsquo;).&lt;br /&gt;&lt;b&gt;바뀌는 건 feature&lt;/b&gt;이고, &lt;b&gt;dev는 움직이지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;1644&quot; data-start=&quot;1601&quot; data-ke-size=&quot;size23&quot;&gt;2-3. Merge 결과 (FF가 아닐 때는 merge 커밋 G 생성)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;03_merge_result.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egkpXj/dJMcadtAXpA/dkZ59i75uLK0EZDmb7aGh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egkpXj/dJMcadtAXpA/dkZ59i75uLK0EZDmb7aGh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egkpXj/dJMcadtAXpA/dkZ59i75uLK0EZDmb7aGh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FegkpXj%2FdJMcadtAXpA%2FdkZ59i75uLK0EZDmb7aGh0%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;900&quot; height=&quot;400&quot; data-filename=&quot;03_merge_result.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1815&quot; data-start=&quot;1734&quot; data-ke-size=&quot;size16&quot;&gt;리베이스 없이 바로 병합하면 &lt;b&gt;merge 커밋(G)&lt;/b&gt; 이 생깁니다.&lt;br /&gt;히스토리가 &amp;ldquo;분기-합류&amp;rdquo; 모양으로 남아, 로그가 복잡해질 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;1862&quot; data-start=&quot;1822&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1862&quot; data-start=&quot;1822&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1862&quot; data-start=&quot;1822&quot; data-ke-size=&quot;size26&quot;&gt;3) HEAD / base / hash &amp;mdash; 꼭 알아야 할 내부 개념&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;04_head_base_hash.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cy7zdL/dJMcaaXWnet/ZWaYTqUDGYCTknmH874UH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cy7zdL/dJMcaaXWnet/ZWaYTqUDGYCTknmH874UH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cy7zdL/dJMcaaXWnet/ZWaYTqUDGYCTknmH874UH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcy7zdL%2FdJMcaaXWnet%2FZWaYTqUDGYCTknmH874UH1%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;900&quot; height=&quot;400&quot; data-filename=&quot;04_head_base_hash.png&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2235&quot; data-start=&quot;1957&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2031&quot; data-start=&quot;1957&quot;&gt;&lt;b&gt;base(출발점)&lt;/b&gt;: 브랜치가 &lt;b&gt;어느 커밋에서 갈라졌는지&lt;/b&gt;. rebase는 이 &lt;b&gt;출발점을 최신으로 바꾸는 작업&lt;/b&gt;.&lt;/li&gt;
&lt;li data-end=&quot;2090&quot; data-start=&quot;2032&quot;&gt;&lt;b&gt;HEAD&lt;/b&gt;: &lt;b&gt;지금 내가 보고/작업 중인 위치&lt;/b&gt;를 가리키는 포인터(내 &amp;ldquo;현재 페이지&amp;rdquo;).&lt;/li&gt;
&lt;li data-end=&quot;2235&quot; data-start=&quot;2091&quot;&gt;&lt;b&gt;hash&lt;/b&gt;: 커밋의 &lt;b&gt;고유 ID(SHA-1)&lt;/b&gt;. 내용/부모/작성자/시간이 포함되어 &lt;b&gt;조금만 바뀌어도 값이 달라짐&lt;/b&gt;.
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2235&quot; data-start=&quot;2169&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2235&quot; data-start=&quot;2169&quot;&gt;rebase로 부모(출발점)가 바뀌면 &lt;b&gt;D&amp;rarr;D&amp;prime;&lt;/b&gt; 처럼 &lt;b&gt;새 커밋&lt;/b&gt;이 만들어지고, &lt;b&gt;해시도 달라집니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2277&quot; data-start=&quot;2242&quot; data-ke-size=&quot;size26&quot;&gt;4) 실전: 팀에서 쓰는 안전한 절차(FF-only 기준)&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;&lt;span style=&quot;color: #666666; text-align: center;&quot;&gt;GitLab + SourceTree 기준. 콘솔 예시도 같이 둡니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2337&quot; data-start=&quot;2323&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2337&quot; data-start=&quot;2323&quot;&gt;&lt;b&gt;dev 최신화&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762997215361&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git checkout dev
git pull origin dev&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;b&gt;2. feature를 dev로 rebase&lt;/b&gt; (feature 브랜치에서 실행)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762997243418&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git checkout feature/xxx
git rebase dev

# 충돌 시:
# 파일 수정 &amp;rarr; 저장
git add &amp;lt;해결한 파일들&amp;gt;
git rebase --continue
# 반복...&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;3. (필요 시) 원격 feature 갱신&lt;br /&gt;리베이스하면 커밋 해시가 바뀌므로 보통 &lt;b&gt;force-with-lease&lt;/b&gt;를 씁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1762997305803&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git push --force-with-lease origin feature/xxx&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;4. dev로 병합(FF)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1762997315808&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git checkout dev
git pull origin dev     # 혹시 또 dev가 변했는지 한 번 더 최신화
git merge --ff-only feature/xxx
git push origin dev&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;b&gt;FF-only&lt;/b&gt; 가 아니라면 --ff-only 대신 일반 merge 로 진행 가능하지만, merge 커밋이 생길 수 있습니다.&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-end=&quot;2964&quot; data-start=&quot;2935&quot; data-ke-size=&quot;size26&quot;&gt;5) 리베이스 vs 병합, 언제 어떤 걸 쓰나?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3372&quot; data-start=&quot;2966&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3093&quot; data-start=&quot;2966&quot;&gt;&lt;b&gt;리베이스 + FF 병합&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3093&quot; data-start=&quot;2989&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3017&quot; data-start=&quot;2989&quot;&gt;히스토리를 &lt;b&gt;직선으로 유지&lt;/b&gt;하고 싶을 때&lt;/li&gt;
&lt;li data-end=&quot;3052&quot; data-start=&quot;3020&quot;&gt;커밋 단위로 &lt;b&gt;충돌을 명확히&lt;/b&gt; 처리하고 싶을 때&lt;/li&gt;
&lt;li data-end=&quot;3093&quot; data-start=&quot;3055&quot;&gt;&lt;b&gt;FF-only&lt;/b&gt; 정책인 저장소(보호 브랜치)에서 사실상 표준&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3205&quot; data-start=&quot;3095&quot;&gt;&lt;b&gt;리베이스 없이 병합(merge)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3205&quot; data-start=&quot;3123&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3155&quot; data-start=&quot;3123&quot;&gt;&lt;b&gt;히스토리 재작성 없이&lt;/b&gt; 안전하게 가고 싶을 때&lt;/li&gt;
&lt;li data-end=&quot;3186&quot; data-start=&quot;3158&quot;&gt;대신 &lt;b&gt;merge 커밋&lt;/b&gt;이 쌓일 수 있음&lt;/li&gt;
&lt;li data-end=&quot;3205&quot; data-start=&quot;3189&quot;&gt;팀 합의/정책에 따라 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3372&quot; data-start=&quot;3207&quot;&gt;&lt;b&gt;GitLab MR에서 Rebase / Squash &amp;amp; Merge&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3372&quot; data-start=&quot;3253&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3304&quot; data-start=&quot;3253&quot;&gt;MR 화면에서 &amp;ldquo;Update branch / Rebase&amp;rdquo; 버튼으로 dev 최신 반영&lt;/li&gt;
&lt;li data-end=&quot;3352&quot; data-start=&quot;3307&quot;&gt;&amp;ldquo;Squash &amp;amp; merge&amp;rdquo;로 커밋 압축 후 머지 &amp;rarr; &lt;b&gt;로그 간결화&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;3372&quot; data-start=&quot;3355&quot;&gt;보호 브랜치 정책에 잘 맞음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;3398&quot; data-start=&quot;3379&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3398&quot; data-start=&quot;3379&quot; data-ke-size=&quot;size26&quot;&gt;6) 자주 묻는 질문(FAQ)&lt;/h2&gt;
&lt;p data-end=&quot;3532&quot; data-start=&quot;3400&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q1. 리베이스만 하고 바로 dev를 푸시하면 안 되나요?&lt;/b&gt;&lt;br /&gt;A. &lt;b&gt;안 됩니다.&lt;/b&gt; 리베이스는 &lt;b&gt;feature만 바뀌고 dev는 그대로&lt;/b&gt;입니다. dev가 바뀌려면 &lt;b&gt;dev에서 feature를 병합(FF)&lt;/b&gt; 해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;3664&quot; data-start=&quot;3534&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q2. 리베이스 중에도 충돌이 나나요?&lt;/b&gt;&lt;br /&gt;A. 납니다. 다만 &lt;b&gt;커밋별로 재적용(replay)&lt;/b&gt; 되기 때문에, 어느 커밋에서 깨졌는지 명확하게 보고 해결할 수 있습니다. 해결 후 git rebase --continue.&lt;/p&gt;
&lt;p data-end=&quot;3789&quot; data-start=&quot;3666&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q3. 해시가 왜 바뀌나요?&lt;/b&gt;&lt;br /&gt;A. 커밋 해시는 &lt;b&gt;부모 커밋, 내용, 작성자/시간&lt;/b&gt; 등을 기반으로 계산됩니다. rebase로 &lt;b&gt;부모(출발점)가 바뀌면&lt;/b&gt; 커밋이 &lt;b&gt;새로 생성&lt;/b&gt;되기 때문에 해시도 바뀝니다.&lt;/p&gt;
&lt;p data-end=&quot;3928&quot; data-start=&quot;3791&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q4. 공유 브랜치를 rebase해도 되나요?&lt;/b&gt;&lt;br /&gt;A. 일반적으로 &lt;b&gt;공유(public) 브랜치 리베이스는 금지&lt;/b&gt;합니다. 히스토리 재작성으로 동료에게 피해를 줄 수 있기 때문입니다. &lt;b&gt;개인 feature 브랜치에서만&lt;/b&gt; 리베이스하세요.&lt;/p&gt;
&lt;p data-end=&quot;4068&quot; data-start=&quot;3930&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q5. force와 force-with-lease 차이는?&lt;/b&gt;&lt;br /&gt;A. --force는 무조건 덮어쓰고, --force-with-lease는 &lt;b&gt;내가 파악한 최신 상태일 때만&lt;/b&gt; 덮어써서 &lt;b&gt;다른 사람 작업을 덮는 사고를 줄여줍니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-end=&quot;4068&quot; data-start=&quot;3930&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;4095&quot; data-start=&quot;4075&quot; data-ke-size=&quot;size26&quot;&gt;7) 용어 정리(초급 개발자용)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4689&quot; data-start=&quot;4097&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4131&quot; data-start=&quot;4097&quot;&gt;&lt;b&gt;Commit(커밋)&lt;/b&gt;: 특정 시점의 변경 스냅샷.&lt;/li&gt;
&lt;li data-end=&quot;4179&quot; data-start=&quot;4132&quot;&gt;&lt;b&gt;Hash(해시)&lt;/b&gt;: 커밋의 고유 ID(SHA-1). 커밋의 &amp;ldquo;주민번호&amp;rdquo;.&lt;/li&gt;
&lt;li data-end=&quot;4242&quot; data-start=&quot;4180&quot;&gt;&lt;b&gt;Branch(브랜치)&lt;/b&gt;: 커밋을 가리키는 이름(책갈피). 예: dev, feature/xxx&lt;/li&gt;
&lt;li data-end=&quot;4284&quot; data-start=&quot;4243&quot;&gt;&lt;b&gt;HEAD&lt;/b&gt;: &lt;b&gt;현재 내가 보고/작업 중인 위치&lt;/b&gt;(포인터).&lt;/li&gt;
&lt;li data-end=&quot;4351&quot; data-start=&quot;4285&quot;&gt;&lt;b&gt;base(출발점)&lt;/b&gt;: 브랜치가 갈라져 나온 커밋. rebase는 이 &lt;b&gt;출발점&lt;/b&gt;을 최신으로 바꾸는 과정.&lt;/li&gt;
&lt;li data-end=&quot;4405&quot; data-start=&quot;4352&quot;&gt;&lt;b&gt;merge(병합)&lt;/b&gt;: 두 갈래를 &lt;b&gt;합치는 작업&lt;/b&gt;. 대상 브랜치의 포인터가 이동.&lt;/li&gt;
&lt;li data-end=&quot;4473&quot; data-start=&quot;4406&quot;&gt;&lt;b&gt;rebase(리베이스)&lt;/b&gt;: 내 커밋들을 &lt;b&gt;다른 출발점 위에 다시 쌓는 작업&lt;/b&gt;. &lt;b&gt;내 브랜치만 변함&lt;/b&gt;.&lt;/li&gt;
&lt;li data-end=&quot;4533&quot; data-start=&quot;4474&quot;&gt;&lt;b&gt;Fast-forward(FF)&lt;/b&gt;: merge 커밋 없이 &lt;b&gt;포인터만 앞으로 이동&lt;/b&gt;하는 병합.&lt;/li&gt;
&lt;li data-end=&quot;4592&quot; data-start=&quot;4534&quot;&gt;&lt;b&gt;Conflict(충돌)&lt;/b&gt;: 같은 부분을 다르게 수정해서 Git이 자동 병합을 못 하는 상태.&lt;/li&gt;
&lt;li data-end=&quot;4636&quot; data-start=&quot;4593&quot;&gt;&lt;b&gt;Remote / origin&lt;/b&gt;: 원격 저장소 / 기본 원격 이름.&lt;/li&gt;
&lt;li data-end=&quot;4689&quot; data-start=&quot;4637&quot;&gt;&lt;b&gt;Upstream&lt;/b&gt;: 내 브랜치가 추적하는 원격 브랜치(예: origin/dev).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;4702&quot; data-start=&quot;4696&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;4702&quot; data-start=&quot;4696&quot; data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4919&quot; data-start=&quot;4704&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4749&quot; data-start=&quot;4704&quot;&gt;리베이스는 &lt;b&gt;내 브랜치의 출발점(base)을 최신으로 바꾸는 것&lt;/b&gt;이고,&lt;/li&gt;
&lt;li data-end=&quot;4792&quot; data-start=&quot;4750&quot;&gt;병합은 &lt;b&gt;대상 브랜치의 포인터를 실제로 움직여 합치는 것&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li data-end=&quot;4847&quot; data-start=&quot;4793&quot;&gt;팀 정책이 &lt;b&gt;FF-only&lt;/b&gt;라면 &amp;ldquo;리베이스 &amp;rarr; (FF) 머지&amp;rdquo; 흐름이 가장 깔끔합니다.&lt;/li&gt;
&lt;li data-end=&quot;4919&quot; data-start=&quot;4848&quot;&gt;보호 브랜치/CI 규칙이 있다면 &lt;b&gt;MR에서 Rebase + Squash &amp;amp; merge&lt;/b&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;&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;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-6000548210126205&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>형상관리</category>
      <category>base</category>
      <category>Fast-forward</category>
      <category>git merge</category>
      <category>git rebase</category>
      <category>gitlab</category>
      <category>hash</category>
      <category>HEAD</category>
      <category>SourceTree</category>
      <category>충돌해결</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/91</guid>
      <comments>https://kwakscoding.tistory.com/91#entry91comment</comments>
      <pubDate>Thu, 13 Nov 2025 10:31:16 +0900</pubDate>
    </item>
    <item>
      <title>[DDD] Value Object의 동일성과 동등성 완벽 이해하기 Value Object의 동일성과 동등성 완벽 이해하기</title>
      <link>https://kwakscoding.tistory.com/90</link>
      <description>&lt;p data-end=&quot;314&quot; data-start=&quot;141&quot; data-ke-size=&quot;size16&quot;&gt;DDD(도메인 주도 설계)에서 &lt;b&gt;Value Object(VO)&lt;/b&gt; 는 도메인 모델을 설계할 때 꼭 등장하는 개념입니다.&lt;br /&gt;하지만 많은 개발자들이 &amp;ldquo;VO가 뭐야?&amp;rdquo; 혹은 &amp;ldquo;동등성으로 비교한다는 게 무슨 뜻이야?&amp;rdquo;라는 부분에서 헷갈려하죠.&lt;br /&gt;오늘은 이 개념을 &lt;b&gt;실제 코드와 함께 명확히&lt;/b&gt; 정리해보겠습니다.&lt;/p&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;141&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;314&quot; data-start=&quot;141&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;341&quot; data-start=&quot;321&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;Value Object란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Value Object(VO)&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 는 고유한 ID가 없고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;412&quot; data-start=&quot;345&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그 값 자체로 동일성을 판단하는 객체&lt;/b&gt;입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-end=&quot;521&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;즉, 값이 같다면 다른 객체여도 &amp;ldquo;같은 것으로 본다&amp;rdquo;는 의미예요.&lt;br /&gt;대표적인 예로는 &lt;b&gt;Money(금액)&lt;/b&gt;, &lt;b&gt;Address(주소)&lt;/b&gt;, &lt;b&gt;Email(이메일)&lt;/b&gt; 같은 객체가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;521&quot; data-start=&quot;414&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;546&quot; data-start=&quot;528&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;동일성과 동등성의 차이&lt;/h2&gt;
&lt;p data-end=&quot;578&quot; data-start=&quot;548&quot; data-ke-size=&quot;size16&quot;&gt;자바에서는 객체를 비교할 때 두 가지 기준이 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;구분의미자바 비교 방식
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;733&quot; data-start=&quot;580&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;733&quot; data-start=&quot;636&quot;&gt;
&lt;tr data-end=&quot;684&quot; data-start=&quot;636&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;657&quot; data-start=&quot;636&quot;&gt;&lt;b&gt;동일성 (Identity)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;676&quot; data-start=&quot;657&quot; data-col-size=&quot;sm&quot;&gt;메모리 상에서 같은 객체인가?&lt;/td&gt;
&lt;td data-end=&quot;684&quot; data-start=&quot;676&quot; data-col-size=&quot;sm&quot;&gt;==&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;733&quot; data-start=&quot;685&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;706&quot; data-start=&quot;685&quot;&gt;&lt;b&gt;동등성 (Equality)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;719&quot; data-start=&quot;706&quot; data-col-size=&quot;sm&quot;&gt;내부 값이 같은가?&lt;/td&gt;
&lt;td data-end=&quot;733&quot; data-start=&quot;719&quot; data-col-size=&quot;sm&quot;&gt;equals()&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;754&quot; data-start=&quot;740&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;예시로 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761099460420&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Money m1 = new Money(1000, &quot;KRW&quot;);
Money m2 = new Money(1000, &quot;KRW&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;898&quot; data-start=&quot;839&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;868&quot; data-start=&quot;839&quot;&gt;m1 == m2 &amp;rarr; 틀림 (메모리 주소가 다름)&lt;/li&gt;
&lt;li data-end=&quot;898&quot; data-start=&quot;869&quot;&gt;m1.equals(m2) &amp;rarr; 같음 (값이 같음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;990&quot; data-start=&quot;900&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;VO는 값이 같으면 같은 객체로 본다&lt;/b&gt;는 원칙을 따릅니다.&lt;br /&gt;그래서 VO에서는 항상 equals()와 hashCode()를 재정의해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;990&quot; data-start=&quot;900&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;990&quot; data-start=&quot;900&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1019&quot; data-start=&quot;997&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;예시 코드 &amp;mdash; Money VO&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761099488985&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Money {

    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException(&quot;통화 단위가 다릅니다.&quot;);
        }
        return new Money(this.amount + other.amount, currency);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount == money.amount &amp;amp;&amp;amp;
               Objects.equals(currency, money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1853&quot; data-start=&quot;1842&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;특징 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1958&quot; data-start=&quot;1854&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1869&quot; data-start=&quot;1854&quot;&gt;id가 없습니다.&lt;/li&gt;
&lt;li data-end=&quot;1894&quot; data-start=&quot;1870&quot;&gt;값이 같으면 같은 객체로 취급합니다.&lt;/li&gt;
&lt;li data-end=&quot;1958&quot; data-start=&quot;1895&quot;&gt;&lt;b&gt;불변(Immutable)&lt;/b&gt; 객체로 설계합니다.&lt;br /&gt;&amp;rarr; 한 번 생성되면 내부 값이 변하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1990&quot; data-start=&quot;1965&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;또 다른 예 &amp;mdash; Address VO&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761099519860&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address() {} // JPA 기본 생성자

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) &amp;amp;&amp;amp;
               Objects.equals(street, address.street) &amp;amp;&amp;amp;
               Objects.equals(zipcode, address.zipcode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }
}&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;pre id=&quot;code_1761099550759&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded
    private Address address;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3027&quot; data-start=&quot;2965&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 Member는 &lt;b&gt;Entity&lt;/b&gt;,&lt;br /&gt;Address는 &lt;b&gt;Value Object&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;3027&quot; data-start=&quot;2965&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3027&quot; data-start=&quot;2965&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3065&quot; data-start=&quot;3034&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;Entity vs Value Object 비교&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;구분EntityValue Object
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3330&quot; data-start=&quot;3067&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;3330&quot; data-start=&quot;3133&quot;&gt;
&lt;tr data-end=&quot;3158&quot; data-start=&quot;3133&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3147&quot; data-start=&quot;3133&quot;&gt;&lt;b&gt;식별자(ID)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;3152&quot; data-start=&quot;3147&quot; data-col-size=&quot;sm&quot;&gt;있음&lt;/td&gt;
&lt;td data-end=&quot;3158&quot; data-start=&quot;3152&quot; data-col-size=&quot;sm&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3193&quot; data-start=&quot;3159&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3171&quot; data-start=&quot;3159&quot;&gt;&lt;b&gt;비교 기준&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;3182&quot; data-start=&quot;3171&quot; data-col-size=&quot;sm&quot;&gt;동일성 (ID)&lt;/td&gt;
&lt;td data-end=&quot;3193&quot; data-start=&quot;3182&quot; data-col-size=&quot;sm&quot;&gt;동등성 (값)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3218&quot; data-start=&quot;3194&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3204&quot; data-start=&quot;3194&quot;&gt;&lt;b&gt;가변성&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;3212&quot; data-start=&quot;3204&quot; data-col-size=&quot;sm&quot;&gt;변경 가능&lt;/td&gt;
&lt;td data-end=&quot;3218&quot; data-start=&quot;3212&quot; data-col-size=&quot;sm&quot;&gt;불변&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3260&quot; data-start=&quot;3219&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3231&quot; data-start=&quot;3219&quot;&gt;&lt;b&gt;DB 매핑&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;3243&quot; data-start=&quot;3231&quot; data-col-size=&quot;sm&quot;&gt;@Entity&lt;/td&gt;
&lt;td data-end=&quot;3260&quot; data-start=&quot;3243&quot; data-col-size=&quot;sm&quot;&gt;@Embeddable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3330&quot; data-start=&quot;3261&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3270&quot; data-start=&quot;3261&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;3292&quot; data-start=&quot;3270&quot; data-col-size=&quot;sm&quot;&gt;주문(Order), 회원(User)&lt;/td&gt;
&lt;td data-end=&quot;3330&quot; data-start=&quot;3292&quot; data-col-size=&quot;sm&quot;&gt;주소(Address), 금액(Money), 이메일(Email)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;3358&quot; data-start=&quot;3337&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3358&quot; data-start=&quot;3337&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;VO를 설계할 때 주의할 점&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3543&quot; data-start=&quot;3360&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3423&quot; data-start=&quot;3360&quot;&gt;&lt;b&gt;항상 불변(Immutable)&lt;/b&gt; 으로 만들 것&lt;br /&gt;&amp;rarr; setter 금지, 생성자에서만 값 설정&lt;/li&gt;
&lt;li data-end=&quot;3484&quot; data-start=&quot;3424&quot;&gt;&lt;b&gt;equals() / hashCode() 반드시 재정의&lt;/b&gt;&lt;br /&gt;&amp;rarr; 값 기반 비교를 위해 필요&lt;/li&gt;
&lt;li data-end=&quot;3543&quot; data-start=&quot;3485&quot;&gt;&lt;b&gt;비즈니스 규칙은 VO 안으로 응집&lt;/b&gt;&lt;br /&gt;&amp;rarr; 예: Email 형식 검증, 금액 계산 등&lt;/li&gt;
&lt;/ol&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-end=&quot;3558&quot; data-start=&quot;3550&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;정리&lt;/h2&gt;
&lt;h2 data-end=&quot;3558&quot; data-start=&quot;3550&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3558&quot; data-start=&quot;3550&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;  &lt;/span&gt;&lt;b&gt;동일성(Identity)&lt;/b&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt; &amp;rarr; 같은 객체인가? (==)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;3650&quot; data-start=&quot;3562&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;동등성(Equality)&lt;/b&gt; &amp;rarr; 값이 같은가? (equals())&lt;/p&gt;
&lt;p data-end=&quot;3709&quot; data-start=&quot;3657&quot; data-ke-size=&quot;size16&quot;&gt;VO는 &amp;ldquo;값이 같으면 같은 객체로 본다.&amp;rdquo;&lt;br /&gt;즉, &lt;b&gt;동등성으로 판단하는 객체&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-end=&quot;3772&quot; data-start=&quot;3716&quot; data-ke-size=&quot;size16&quot;&gt;반면 Entity는 ID가 같으면 같은 객체로 본다.&lt;br /&gt;즉, &lt;b&gt;동일성으로 판단하는 객체&lt;/b&gt;다.&lt;/p&gt;
&lt;h2 data-end=&quot;3792&quot; data-start=&quot;3779&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3792&quot; data-start=&quot;3779&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;마무리 한 줄&lt;/h2&gt;
&lt;h2 data-end=&quot;3792&quot; data-start=&quot;3779&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3792&quot; data-start=&quot;3779&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;VO는 단순한 데이터 컨테이너가 아니라,&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;3868&quot; data-start=&quot;3796&quot; data-ke-size=&quot;size16&quot;&gt;도메인의 의미를 담고, 불변성과 동등성을 보장하는 &amp;lsquo;작은 규칙의 단위&amp;rsquo;다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ddd</category>
      <category>value object</category>
      <category>Vo</category>
      <category>객체</category>
      <category>동등성</category>
      <category>동일성</category>
      <category>불변</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/90</guid>
      <comments>https://kwakscoding.tistory.com/90#entry90comment</comments>
      <pubDate>Wed, 22 Oct 2025 11:21:17 +0900</pubDate>
    </item>
    <item>
      <title>[DDD] 자바에서의 참조 공유와 (Value Object, Entity) 정리</title>
      <link>https://kwakscoding.tistory.com/89</link>
      <description>&lt;h2 data-end=&quot;197&quot; data-start=&quot;184&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-end=&quot;344&quot; data-start=&quot;198&quot; data-ke-size=&quot;size16&quot;&gt;DDD(Domain-Driven Design)를 공부하다 보면 &amp;ldquo;&lt;b&gt;밸류(Value Object)는 불변 객체여야 한다&lt;/b&gt;&amp;rdquo;는 말을 자주 듣습니다.&lt;br /&gt;그런데 막상 자바 코드로 보면 &amp;ldquo;왜 불변이어야 하지?&amp;rdquo;, &amp;ldquo;참조 공유가 왜 문제지?&amp;rdquo; 같은 의문이 생기죠.&lt;/p&gt;
&lt;p data-end=&quot;354&quot; data-start=&quot;346&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;506&quot; data-start=&quot;355&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;385&quot; data-start=&quot;355&quot;&gt;&lt;b&gt;자바의 참조 구조(Stack, Heap)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;406&quot; data-start=&quot;386&quot;&gt;&lt;b&gt;참조 공유 문제의 원리&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;429&quot; data-start=&quot;407&quot;&gt;&lt;b&gt;불변 객체로 해결하는 이유&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;506&quot; data-start=&quot;430&quot;&gt;&lt;b&gt;DDD에서 밸류(Value Object)와 엔티티(Entity)의 차이&lt;/b&gt;&lt;br /&gt;를 실제 예제 코드와 함께 명확히 정리해보겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760664018286&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 금액
class Money {
    private int value;

    public Money(int value) {
        if (value &amp;lt; 0) throw new IllegalArgumentException();
        this.value = value;
    }
    public int getValue() { return value; }
    public void setValue(int value) { this.value = value; } // ❗ 가변 포인트
    @Override public String toString() { return &quot;Money(&quot; + value + &quot;)&quot;; }
}

// 주문 목록
class OrderLine {
    private final String productName;
    private final Money price; // 같은 인스턴스를 그대로 참조
    private final int quantity;

    public OrderLine(String productName, Money price, int quantity) {
        this.productName = productName;
        this.price = price; // ❗ 방어적 복사 없음 &amp;rarr; 참조 공유 발생
        this.quantity = quantity;
    }
    public Money getPrice() { return price; }
    public int total() { return price.getValue() * quantity; }
   
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-end=&quot;545&quot; data-start=&quot;513&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;545&quot; data-start=&quot;513&quot; data-ke-size=&quot;size26&quot;&gt;자바의 메모리 구조: Stack과 Heap&lt;/h2&gt;
&lt;p data-end=&quot;577&quot; data-start=&quot;547&quot; data-ke-size=&quot;size16&quot;&gt;자바는 데이터를 저장할 때 두 가지 영역을 사용합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;구분저장 위치예시저장 내용
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;811&quot; data-start=&quot;579&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;811&quot; data-start=&quot;650&quot;&gt;
&lt;tr data-end=&quot;734&quot; data-start=&quot;650&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;662&quot; data-start=&quot;650&quot;&gt;&lt;b&gt;Stack&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;683&quot; data-start=&quot;662&quot;&gt;메서드 호출 시 생기는 임시 공간&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;718&quot; data-start=&quot;683&quot;&gt;Money price = new Money(1000);&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;734&quot; data-start=&quot;718&quot;&gt;변수명과 참조값(주소)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;811&quot; data-start=&quot;735&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;746&quot; data-start=&quot;735&quot;&gt;&lt;b&gt;Heap&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;771&quot; data-start=&quot;746&quot;&gt;new로 만든 객체들이 저장되는 공간&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;791&quot; data-start=&quot;771&quot;&gt;new Money(1000)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;811&quot; data-start=&quot;791&quot;&gt;객체의 실제 데이터(필드 값)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;817&quot; data-start=&quot;813&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760664029384&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Money price = new Money(1000);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;886&quot; data-start=&quot;861&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;886&quot; data-start=&quot;861&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;886&quot; data-start=&quot;861&quot; data-ke-size=&quot;size16&quot;&gt;이 한 줄의 실행 결과는 다음과 같습니다 (아래참고)&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760664069161&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Stack]           [Heap]
price ───► [ Money 객체 | value = 1000 ]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;961&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;961&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;961&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;961&quot; data-ke-size=&quot;size16&quot;&gt;Stack에는 &lt;b&gt;참조값(reference)&lt;/b&gt;,&lt;br /&gt;Heap에는 &lt;b&gt;객체의 실제 데이터(value)&lt;/b&gt; 가 저장됩니다.&lt;/p&gt;
&lt;h2 data-end=&quot;1071&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1071&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1071&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1071&quot; data-start=&quot;1035&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;참조 공유(Reference Sharing) 문제란?&lt;/h2&gt;
&lt;p data-end=&quot;1151&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size16&quot;&gt;자바의 참조형 변수는 &lt;b&gt;객체를 가리키는 주소값&lt;/b&gt;을 저장합니다.&lt;br /&gt;이 때문에 여러 변수가 &lt;b&gt;같은 객체를 동시에 참조&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760664111513&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Money price = new Money(1000);
OrderLine line = new OrderLine(price);
price.setValue(2000); //   외부에서 금액 변경&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1294&quot; data-start=&quot;1275&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1294&quot; data-start=&quot;1275&quot; data-ke-size=&quot;size16&quot;&gt;이때 메모리 구조는 이렇게 됩니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760664129161&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Stack]
price ─┐
       └──► [Heap] Money 객체 { value = 2000 }
line.price ─┘&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1465&quot; data-start=&quot;1381&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;price와 line.price가 &lt;b&gt;같은 객체(Heap 주소)&lt;/b&gt; 를 가리키고 있어서&lt;br /&gt;한쪽을 수정하면 다른 쪽에도 영향을 줍니다.&lt;/p&gt;
&lt;p data-end=&quot;1510&quot; data-start=&quot;1467&quot; data-ke-size=&quot;size16&quot;&gt;이걸 &lt;b&gt;참조 공유 문제(reference aliasing)&lt;/b&gt; 라고 합니다.&lt;/p&gt;
&lt;h2 data-end=&quot;137&quot; data-start=&quot;99&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;137&quot; data-start=&quot;99&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;137&quot; data-start=&quot;99&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;불변 객체(Immutable Object)로 해결하기&lt;/h2&gt;
&lt;p data-end=&quot;216&quot; data-start=&quot;139&quot; data-ke-size=&quot;size16&quot;&gt;OrderLine이 외부에서 받은 Money 객체를 그대로 저장하면,&lt;br /&gt;참조 공유 때문에 외부 변경이 내부에도 영향을 줍니다.&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;218&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결 방법:&lt;/b&gt; OrderLine 내부에 복사본을 만들어 보관합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2410&quot; data-start=&quot;2369&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760664328625&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Money {
    private int value;

    public Money(int value) { this.value = value; }
    public int getValue() { return value; }
    public void setValue(int value) { this.value = value; }
}

class OrderLine {
    private final Money price;

    public OrderLine(Money price) {
        // ✅ 불변처럼 동작하도록 복사본 생성
        this.price = new Money(price.getValue());
    }

    public Money getPrice() { return price; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-end=&quot;2454&quot; data-start=&quot;2417&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;2454&quot; data-start=&quot;2417&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;2454&quot; data-start=&quot;2417&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;2454&quot; data-start=&quot;2417&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;DDD의 밸류(Value Object)란 무엇인가?&lt;/h2&gt;
&lt;p data-end=&quot;2544&quot; data-start=&quot;2456&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;밸류(Value Object)&lt;/b&gt; 는 &amp;ldquo;도메인 안의 개념을 값으로 표현하는 객체&amp;rdquo;입니다.&lt;br /&gt;즉, &lt;b&gt;&amp;lsquo;누구인지&amp;rsquo;보다 &amp;lsquo;무엇인지&amp;rsquo;가 중요한 객체&lt;/b&gt;입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;구분엔티티(Entity)밸류(Value Object)
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2820&quot; data-start=&quot;2546&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;2820&quot; data-start=&quot;2632&quot;&gt;
&lt;tr data-end=&quot;2667&quot; data-start=&quot;2632&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2640&quot; data-start=&quot;2632&quot;&gt;구분 기준&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2652&quot; data-start=&quot;2640&quot;&gt;고유 ID로 구분&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2667&quot; data-start=&quot;2652&quot;&gt;값의 동등성으로 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2709&quot; data-start=&quot;2668&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2677&quot; data-start=&quot;2668&quot;&gt;동일성 판단&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2690&quot; data-start=&quot;2677&quot;&gt;id가 같으면 동일&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2709&quot; data-start=&quot;2690&quot;&gt;모든 필드 값이 같으면 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2751&quot; data-start=&quot;2710&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2718&quot; data-start=&quot;2710&quot;&gt;상태 변경&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2733&quot; data-start=&quot;2718&quot;&gt;가능 (mutable)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2751&quot; data-start=&quot;2733&quot;&gt;불변 (immutable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2820&quot; data-start=&quot;2752&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2757&quot; data-start=&quot;2752&quot;&gt;예시&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2779&quot; data-start=&quot;2757&quot;&gt;주문(Order), 회원(User)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2820&quot; data-start=&quot;2779&quot;&gt;금액(Money), 주소(Address), 주문번호(OrderNo)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;2852&quot; data-start=&quot;2827&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;2852&quot; data-start=&quot;2827&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;2852&quot; data-start=&quot;2827&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;2852&quot; data-start=&quot;2827&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;밸류 타입 예시: OrderNo&lt;/h3&gt;
&lt;p data-end=&quot;2932&quot; data-start=&quot;2853&quot; data-ke-size=&quot;size16&quot;&gt;주문 번호를 단순히 String으로 저장하면 도메인 의미가 드러나지 않습니다.&lt;br /&gt;그래서 도메인 의미를 담은 &lt;b&gt;밸류 타입&lt;/b&gt;을 만듭니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760664359952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record OrderNo(String value) {
    public OrderNo {
        if (!value.matches(&quot;^ORD-\\d{8}-\\d{4}$&quot;))
            throw new IllegalArgumentException(&quot;잘못된 주문번호 형식입니다.&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3230&quot; data-start=&quot;3132&quot; data-ke-size=&quot;size16&quot;&gt;record는 자바 16부터 추가된 &lt;b&gt;불변 객체 전용 문법&lt;/b&gt;으로,&lt;br /&gt;모든 필드가 final이며, equals/hashCode도 자동으로 값 기반으로 생성됩니다.&lt;/p&gt;
&lt;p data-end=&quot;3237&quot; data-start=&quot;3232&quot; data-ke-size=&quot;size16&quot;&gt;사용 예:&lt;/p&gt;
&lt;pre id=&quot;code_1760664386439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;OrderNo id = new OrderNo(&quot;ORD-20251017-0001&quot;);
Order order = new Order(id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3394&quot; data-start=&quot;3327&quot; data-ke-size=&quot;size16&quot;&gt;이제 코드를 보지 않아도&lt;br /&gt;OrderNo라는 타입 이름만으로 &lt;b&gt;이게 주문번호라는 의미&lt;/b&gt;를 바로 알 수 있습니다.&lt;/p&gt;
&lt;h2 data-end=&quot;3425&quot; data-start=&quot;3401&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3425&quot; data-start=&quot;3401&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3425&quot; data-start=&quot;3401&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3425&quot; data-start=&quot;3401&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3425&quot; data-start=&quot;3401&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;밸류 객체와 엔티티 객체의 차이&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;구분엔티티(Entity)밸류(Value Object)
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3739&quot; data-start=&quot;3427&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;3739&quot; data-start=&quot;3513&quot;&gt;
&lt;tr data-end=&quot;3560&quot; data-start=&quot;3513&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3521&quot; data-start=&quot;3513&quot;&gt;존재 이유&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3540&quot; data-start=&quot;3521&quot;&gt;시스템 안에서 &amp;ldquo;고유한 존재&amp;rdquo;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3560&quot; data-start=&quot;3540&quot;&gt;도메인 개념을 &amp;ldquo;값으로 표현&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3586&quot; data-start=&quot;3561&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3567&quot; data-start=&quot;3561&quot;&gt;식별자&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3580&quot; data-start=&quot;3567&quot;&gt;있음 (id 필드)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3586&quot; data-start=&quot;3580&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3648&quot; data-start=&quot;3587&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3596&quot; data-start=&quot;3587&quot;&gt;동등성 판단&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3621&quot; data-start=&quot;3596&quot;&gt;식별자 비교 (id.equals())&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3648&quot; data-start=&quot;3621&quot;&gt;값 비교 (equals() 모든 필드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3668&quot; data-start=&quot;3649&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3657&quot; data-start=&quot;3649&quot;&gt;상태 변화&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3662&quot; data-start=&quot;3657&quot;&gt;허용&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3668&quot; data-start=&quot;3662&quot;&gt;불변&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3739&quot; data-start=&quot;3669&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3676&quot; data-start=&quot;3669&quot;&gt;사용 예&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3699&quot; data-start=&quot;3676&quot;&gt;주문(Order), 사용자(User)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3739&quot; data-start=&quot;3699&quot;&gt;금액(Money), 이메일(Email), 주문번호(OrderNo)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;3758&quot; data-start=&quot;3746&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3758&quot; data-start=&quot;3746&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3758&quot; data-start=&quot;3746&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3758&quot; data-start=&quot;3746&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3758&quot; data-start=&quot;3746&quot; data-ke-size=&quot;size26&quot;&gt;실전 예시&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760664431632&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Order {
    @Id
    private OrderNo id;

    @Embedded
    private Money totalAmount;

    public Order(OrderNo id, Money totalAmount) {
        this.id = id;
        this.totalAmount = totalAmount;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4088&quot; data-start=&quot;4001&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; DB에는 VARCHAR 값으로 저장되지만,&lt;br /&gt;코드에서는 OrderNo, Money로 표현되어 &lt;b&gt;의미가 살아 있는 도메인 모델&lt;/b&gt;이 됩니다.&lt;/p&gt;
&lt;h2 data-end=&quot;4106&quot; data-start=&quot;4095&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;4106&quot; data-start=&quot;4095&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;4106&quot; data-start=&quot;4095&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;4106&quot; data-start=&quot;4095&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;4106&quot; data-start=&quot;4095&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;마무리 요약&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;개념핵심 요약
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;4368&quot; data-start=&quot;4108&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;4368&quot; data-start=&quot;4145&quot;&gt;
&lt;tr data-end=&quot;4205&quot; data-start=&quot;4145&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;4157&quot; data-start=&quot;4145&quot;&gt;&lt;b&gt;참조 공유&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;4205&quot; data-start=&quot;4157&quot;&gt;여러 변수가 같은 객체 주소를 가리켜서 한쪽 변경이 다른 쪽에 영향을 주는 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;4254&quot; data-start=&quot;4206&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;4218&quot; data-start=&quot;4206&quot;&gt;&lt;b&gt;불변 객체&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;4254&quot; data-start=&quot;4218&quot;&gt;내부 상태를 변경할 수 없는 객체. 참조 공유로부터 안전함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;4311&quot; data-start=&quot;4255&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;4278&quot; data-start=&quot;4255&quot;&gt;&lt;b&gt;밸류(Value Object)&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;4311&quot; data-start=&quot;4278&quot;&gt;도메인 개념을 값으로 표현하는 불변 객체. ID 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;4368&quot; data-start=&quot;4312&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;4330&quot; data-start=&quot;4312&quot;&gt;&lt;b&gt;엔티티(Entity)&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;4368&quot; data-start=&quot;4330&quot;&gt;고유 식별자를 갖고, 상태가 변해도 같은 객체로 인식되는 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;정리하면,&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4552&quot; data-start=&quot;4372&quot; data-ke-size=&quot;size16&quot;&gt;자바의 객체는 참조로 다뤄지기 때문에 &lt;b&gt;참조 공유&lt;/b&gt;가 필연적으로 발생합니다.&lt;br /&gt;따라서 DDD에서의 밸류 객체는 &lt;b&gt;불변 객체(Immutable Object)&lt;/b&gt; 로 만들어&lt;br /&gt;참조 공유의 부작용을 원천 차단하고,&lt;br /&gt;도메인의 의미가 드러나는 &lt;b&gt;안전한 모델링&lt;/b&gt;을 구현하는 것입니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ddd</category>
      <category>Domain-Driven Design</category>
      <category>value object</category>
      <category>밸류</category>
      <category>불변 객체</category>
      <category>엔티티</category>
      <category>참조공유</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/89</guid>
      <comments>https://kwakscoding.tistory.com/89#entry89comment</comments>
      <pubDate>Fri, 17 Oct 2025 10:28:00 +0900</pubDate>
    </item>
    <item>
      <title>[Java기술면접] ArrayList vs LinkedList 차이 및 시간 복잡도란?</title>
      <link>https://kwakscoding.tistory.com/88</link>
      <description>&lt;h2 data-end=&quot;47&quot; data-start=&quot;32&quot; data-ke-size=&quot;size26&quot;&gt;시간 복잡도란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;167&quot; data-start=&quot;48&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;92&quot; data-start=&quot;48&quot;&gt;어떤 &lt;b&gt;코드/알고리즘이 얼마나 빨리 동작하는지&lt;/b&gt;를 나타내는 단위예요.&lt;/li&gt;
&lt;li data-end=&quot;167&quot; data-start=&quot;93&quot;&gt;그런데 &quot;몇 초&quot; 같은 절대 시간이 아니라, &lt;b&gt;데이터 양(n)이 늘어날 때 연산 횟수가 얼마나 늘어나는지&lt;/b&gt;를 보는 거예요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;196&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;즉, &lt;b&gt;성능의 성장률&lt;/b&gt;을 나타낸 것.&lt;/p&gt;
&lt;p data-end=&quot;196&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;227&quot; data-start=&quot;203&quot; data-ke-size=&quot;size26&quot;&gt;O(1), O(n) 이건 뭔데?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;299&quot; data-start=&quot;228&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;299&quot; data-start=&quot;228&quot;&gt;O(...) 표기법 = &lt;b&gt;Big-O 표기법&lt;/b&gt;이라고 부르고, &lt;b&gt;최악의 경우에 연산이 얼마나 걸리는지&lt;/b&gt; 표현한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;314&quot; data-start=&quot;301&quot; data-ke-size=&quot;size23&quot;&gt;자주 나오는 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;640&quot; data-start=&quot;315&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;407&quot; data-start=&quot;315&quot;&gt;&lt;b&gt;O(1)&lt;/b&gt; : 데이터 개수와 상관없이 항상 일정한 시간.&lt;br /&gt;&amp;rarr; &quot;한 번에 바로 찾는다.&quot;&lt;br /&gt;예: 배열에서 arr[5]처럼 인덱스로 값 꺼내기.&lt;/li&gt;
&lt;li data-end=&quot;518&quot; data-start=&quot;408&quot;&gt;&lt;b&gt;O(n)&lt;/b&gt; : 데이터 개수(n)에 비례해서 시간이 늘어남.&lt;br /&gt;&amp;rarr; &quot;처음부터 끝까지 다 뒤져야 한다.&quot;&lt;br /&gt;예: 배열에서 &quot;값이 7인 원소 찾기&quot; (최악의 경우 전부 확인해야 함).&lt;/li&gt;
&lt;li data-end=&quot;582&quot; data-start=&quot;519&quot;&gt;&lt;b&gt;O(log n)&lt;/b&gt; : 데이터가 늘어나도 조금만 늘어남.&lt;br /&gt;&amp;rarr; &quot;이진탐색처럼 반씩 줄여가며 찾는다.&quot;&lt;/li&gt;
&lt;li data-end=&quot;640&quot; data-start=&quot;583&quot;&gt;&lt;b&gt;O(n&amp;sup2;)&lt;/b&gt; : 데이터가 10개면 100번, 100개면 1만 번.&lt;br /&gt;&amp;rarr; &quot;이중 반복문.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;686&quot; data-start=&quot;647&quot; data-ke-size=&quot;size26&quot;&gt;ArrayList vs LinkedList 예시로 이해하기&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;185&quot; data-start=&quot;134&quot;&gt;&lt;b&gt;ArrayList&lt;/b&gt;: 내부적으로 &lt;b&gt;동적 배열&lt;/b&gt; 기반. (연속된 메모리 블록)&lt;/li&gt;
&lt;li data-end=&quot;237&quot; data-start=&quot;186&quot;&gt;&lt;b&gt;LinkedList&lt;/b&gt;: 내부적으로 &lt;b&gt;이중 연결 리스트&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;실제_성능_측정&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실제 성능 측정&lt;/b&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;조회(get)시에는 arraylist가 우위 지만 삽입/삭제(add/remove) 시에는 linkedlist가 뛰어난 성능을 보여준다.&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;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;848&quot; data-start=&quot;687&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;762&quot; data-start=&quot;687&quot;&gt;&lt;b&gt;ArrayList 접근 (O(1))&lt;/b&gt;&lt;br /&gt;&amp;rarr; &quot;책장에서 5번째 칸에 있는 책 바로 뽑기.&quot; (인덱스로 바로 접근 가능)&lt;/li&gt;
&lt;li data-end=&quot;848&quot; data-start=&quot;763&quot;&gt;&lt;b&gt;LinkedList 접근 (O(n))&lt;/b&gt;&lt;br /&gt;&amp;rarr; &quot;책장 맨 앞에서부터 하나씩 넘겨서 5번째 책까지 가야 함.&quot; (앞에서 순서대로 따라가야 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;873&quot; data-start=&quot;855&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;873&quot; data-start=&quot;855&quot; data-ke-size=&quot;size26&quot;&gt;표를 쉽게 보면&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deiLQ6/btsQqLYIOt4/iQMXkTpwT0cdabO1M862D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deiLQ6/btsQqLYIOt4/iQMXkTpwT0cdabO1M862D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deiLQ6/btsQqLYIOt4/iQMXkTpwT0cdabO1M862D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeiLQ6%2FbtsQqLYIOt4%2FiQMXkTpwT0cdabO1M862D0%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;873&quot; height=&quot;354&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;354&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;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1445&quot; data-start=&quot;1435&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1445&quot; data-start=&quot;1435&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1445&quot; data-start=&quot;1435&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1445&quot; data-start=&quot;1435&quot; data-ke-size=&quot;size16&quot;&gt;핵심 요약:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1566&quot; data-start=&quot;1446&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1483&quot; data-start=&quot;1446&quot;&gt;&lt;b&gt;O(1)&lt;/b&gt; = 바로 찾음 (빠름, 데이터 크기와 무관)&lt;/li&gt;
&lt;li data-end=&quot;1523&quot; data-start=&quot;1484&quot;&gt;&lt;b&gt;O(n)&lt;/b&gt; = 데이터가 많아질수록 느려짐 (하나하나 확인)&lt;/li&gt;
&lt;li data-end=&quot;1566&quot; data-start=&quot;1524&quot;&gt;&lt;b&gt;시간 복잡도&lt;/b&gt; = &quot;데이터가 커질 때 걸리는 시간의 증가 속도&quot;&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;참고:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/JCF-&quot;&gt;https://inpa.tistory.com/entry/JCF-&lt;/a&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://inpa.tistory.com/entry/JCF-%F0%9F%A7%B1-ArrayList-vs-LinkedList-%ED%8A%B9%EC%A7%95-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90&quot;&gt; -ArrayList-vs-LinkedList-특징-성능-비교&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;[Inpa Dev  &amp;zwj; :티스토리]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT5&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술면접</category>
      <category>arrayList</category>
      <category>Iterator</category>
      <category>java</category>
      <category>LinkedList</category>
      <category>시간복잡도</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/88</guid>
      <comments>https://kwakscoding.tistory.com/88#entry88comment</comments>
      <pubDate>Mon, 8 Sep 2025 23:25:05 +0900</pubDate>
    </item>
    <item>
      <title>[Java 기술면접] ConcurrentModificationException 발생</title>
      <link>https://kwakscoding.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이건 자바 면접에서 정말 자주 나오는 &lt;b&gt;ConcurrentModificationException&lt;/b&gt; 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757340124093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(&quot;a&quot;,&quot;b&quot;,&quot;c&quot;));

for (String s : list) {
  if (s.equals(&quot;n&quot;)) {
    list.remove(s);   // 여기서 문제 !!
  }
}&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;1. 왜 오류가 날까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;431&quot; data-start=&quot;270&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;323&quot; data-start=&quot;270&quot;&gt;for-each 문은 내부적으로 &lt;b&gt;Iterator&lt;/b&gt;를 사용해 리스트를 순회합니다.&lt;/li&gt;
&lt;li data-end=&quot;431&quot; data-start=&quot;324&quot;&gt;그런데 순회 도중에 list.remove(s)로 &lt;b&gt;직접 리스트를 수정&lt;/b&gt;하면,&lt;br /&gt;&lt;b&gt;Iterator가 감지 &amp;rarr; ConcurrentModificationException 발생&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;479&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;Iterator로 순회 중인데, 너 몰래 리스트를 건드렸다&amp;rdquo;라는 상황이다.&lt;/p&gt;
&lt;p data-end=&quot;479&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;504&quot; data-start=&quot;486&quot; data-ke-size=&quot;size26&quot;&gt;2. 안전하게 수정하는 방법&lt;/h2&gt;
&lt;h3 data-end=&quot;527&quot; data-start=&quot;506&quot; data-ke-size=&quot;size23&quot;&gt;방법 1) Iterator 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757339896683&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(&quot;a&quot;,&quot;b&quot;,&quot;c&quot;));

Iterator&amp;lt;String&amp;gt; it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals(&quot;n&quot;)) {
        it.remove();   //  안전: Iterator의 remove는 허용
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;479&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;479&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;814&quot; data-start=&quot;786&quot; data-ke-size=&quot;size23&quot;&gt;방법 2) removeIf (Java 8+)&lt;/h3&gt;
&lt;p data-end=&quot;836&quot; data-start=&quot;815&quot; 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_1757339911969&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(&quot;a&quot;,&quot;b&quot;,&quot;c&quot;));
list.removeIf(s -&amp;gt; s.equals(&quot;n&quot;));&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;h3 data-end=&quot;977&quot; data-start=&quot;955&quot; data-ke-size=&quot;size23&quot;&gt;방법 3) 새로운 리스트로 필터링&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1757339929780&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;(Arrays.asList(&quot;a&quot;,&quot;b&quot;,&quot;c&quot;));
List&amp;lt;String&amp;gt; filtered = list.stream()
                            .filter(s -&amp;gt; !s.equals(&quot;n&quot;))
                            .toList();   // Java 16+에서는 불변 리스트 반환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1249&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1249&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1249&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1249&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size26&quot;&gt;면접 답변 예시&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h2 data-end=&quot;1249&quot; data-start=&quot;1222&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;&amp;ldquo;for-each는 내부적으로 Iterator를 쓰는데, 순회 중에 list.remove()를 호출하면 ConcurrentModificationException이 발생합니다. 안전하게 제거하려면 Iterator의 remove()를 사용하거나, Java 8 이후엔 removeIf 메서드를 쓰는 게 일반적입니다.&amp;rdquo;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1426&quot; data-start=&quot;1252&quot; 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>기술면접</category>
      <category>ConcurrentModificationException</category>
      <category>Iterator</category>
      <category>java</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/87</guid>
      <comments>https://kwakscoding.tistory.com/87#entry87comment</comments>
      <pubDate>Mon, 8 Sep 2025 23:05:02 +0900</pubDate>
    </item>
    <item>
      <title>[SourceTree]소스트리 조직(organization) 저장소 403 오류 발생 해결</title>
      <link>https://kwakscoding.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소스트리(Sourcetree)를 설치 후 깃허브(Github)의 저장소들을 클론(clone)할 때 문제가 발생하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유효한 소스 경로 URL이 아닙니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYXZ4Q/btsNS92KWtu/Em7lZnFfwIxaX1HP6aZxQk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYXZ4Q/btsNS92KWtu/Em7lZnFfwIxaX1HP6aZxQk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYXZ4Q/btsNS92KWtu/Em7lZnFfwIxaX1HP6aZxQk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYXZ4Q%2FbtsNS92KWtu%2FEm7lZnFfwIxaX1HP6aZxQk%2Fimg.jpg&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;675&quot; height=&quot;269&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;1-problem&quot; style=&quot;background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. 문제발생&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스트리(Sourcetree)를 설치 후 깃허브(Github)의 저장소들을 클론(clone)할 때 문제가 발생하였습니다.&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;유효한 소스 경로 URL이 아닙니다.&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;1. 문제발생&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;분명히 존재하는 저장소인데 해당 URL을 찾지 못하는 것이 이상했습니다. 개인(private) 저장소들은 정상적으로 탐색되었기 때문에 조직(organization) 저장소라는 점이 수상했습니다. 문제 원인은 소스트리 어플리케이션이 조직에 대한 접근 승인이 되지 않았던 것이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;2-solve-the-problem&quot; style=&quot;background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 해결방법&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음과 같은 과정을 통해 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Settings &amp;gt; Applications &amp;gt; Authorized OAuth Apps&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 경로로 접근하여 소스트리 어플리케이션을 선택합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wduJp/btsNSAfJeRH/yMoBdWdF5UjH2pJNkscMDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wduJp/btsNSAfJeRH/yMoBdWdF5UjH2pJNkscMDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wduJp/btsNSAfJeRH/yMoBdWdF5UjH2pJNkscMDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwduJp%2FbtsNSAfJeRH%2FyMoBdWdF5UjH2pJNkscMDK%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;866&quot; height=&quot;528&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;528&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;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 아래 빨간 네모 칸에&lt;span&gt;&amp;nbsp;&lt;/span&gt;grant&lt;span&gt;&amp;nbsp;&lt;/span&gt;버튼을 눌러 해당 어플리케이션 접근을 승인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/efOrpU/btsNUy8mVp4/K2T0Jx9meKtkNr5rQEjHP1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/efOrpU/btsNUy8mVp4/K2T0Jx9meKtkNr5rQEjHP1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/efOrpU/btsNUy8mVp4/K2T0Jx9meKtkNr5rQEjHP1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FefOrpU%2FbtsNUy8mVp4%2FK2T0Jx9meKtkNr5rQEjHP1%2Fimg.jpg&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;962&quot; height=&quot;1125&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>형상관리</category>
      <category>403</category>
      <category>SourceTree</category>
      <category>소스트리</category>
      <category>에러</category>
      <category>조직</category>
      <category>팀레파지토리</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/85</guid>
      <comments>https://kwakscoding.tistory.com/85#entry85comment</comments>
      <pubDate>Mon, 12 May 2025 14:04:26 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Security] 해시란? Salt란? BCrypt이해하기</title>
      <link>https://kwakscoding.tistory.com/84</link>
      <description>&lt;p data-end=&quot;308&quot; data-start=&quot;175&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;안녕하세요, &lt;/span&gt;&lt;span&gt;이번 &lt;/span&gt;&lt;span&gt;글에서는 &lt;/span&gt;&lt;span&gt;실무에서도 &lt;/span&gt;&lt;span&gt;많이 &lt;/span&gt;&lt;span&gt;사용하는 &lt;/span&gt;BCrypt&lt;span&gt; &lt;/span&gt;&lt;span&gt;해시 &lt;/span&gt;&lt;span&gt;알고리즘에 &lt;/span&gt;&lt;span&gt;대해 &lt;/span&gt;&lt;span&gt;정리해보려고 &lt;/span&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;단순히 &lt;/span&gt;&lt;span&gt;라이브러리만 &lt;/span&gt;&lt;span&gt;사용하는 &lt;/span&gt;&lt;span&gt;것을 &lt;/span&gt;&lt;span&gt;넘어서, &lt;/span&gt;&lt;b&gt;&lt;span&gt;왜 &lt;/span&gt;&lt;span&gt;써야 &lt;/span&gt;&lt;span&gt;하는지&lt;/span&gt;&lt;/b&gt;&lt;span&gt;, &lt;/span&gt;&lt;b&gt;&lt;span&gt;어떻게 &lt;/span&gt;&lt;span&gt;동작하는지&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;까지 &lt;/span&gt;&lt;span&gt;이해하는 &lt;/span&gt;&lt;span&gt;것이 &lt;/span&gt;&lt;span&gt;목표입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;345&quot; data-start=&quot;315&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;345&quot; data-start=&quot;315&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;345&quot; data-start=&quot;315&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;비밀번호 &lt;/span&gt;&lt;span&gt;저장은 &quot;&lt;/span&gt;&lt;span&gt;암호화&quot;&lt;/span&gt;&lt;span&gt;가 &lt;/span&gt;&lt;span&gt;아니라 &quot;&lt;/span&gt;&lt;span&gt;해시&quot;&lt;/span&gt;&lt;span&gt;로&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;409&quot; data-start=&quot;347&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;많은 &lt;/span&gt;&lt;span&gt;분들이 &lt;/span&gt;&lt;b&gt;&lt;span&gt;비밀번호를 &lt;/span&gt;&lt;span&gt;암호화해서 &lt;/span&gt;&lt;span&gt;저장&lt;/span&gt;&lt;/b&gt;&lt;span&gt;한다고 &lt;/span&gt;&lt;span&gt;말하지만, &lt;/span&gt;&lt;span&gt;정확히는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;해시(&lt;/span&gt;&lt;span&gt;Hash)&quot;&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;해야 &lt;/span&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;구분해시 (Hash)암호화 (Encryption)
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;594&quot; data-start=&quot;411&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;594&quot; data-start=&quot;494&quot;&gt;
&lt;tr data-end=&quot;531&quot; data-start=&quot;494&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;500&quot; data-start=&quot;494&quot;&gt;&lt;span&gt;방향성&lt;/span&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;515&quot; data-start=&quot;500&quot;&gt;&lt;span&gt;단방향 (&lt;/span&gt;&lt;span&gt;복호화 &lt;/span&gt;&lt;span&gt;불가)&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;531&quot; data-start=&quot;515&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;양방향 (&lt;/span&gt;&lt;span&gt;복호화 &lt;/span&gt;&lt;span&gt;가능)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;563&quot; data-start=&quot;532&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;540&quot; data-start=&quot;532&quot;&gt;&lt;span&gt;사용 &lt;/span&gt;&lt;span&gt;목적&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;553&quot; data-start=&quot;540&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;비교용, &lt;/span&gt;&lt;span&gt;위조 &lt;/span&gt;&lt;span&gt;방지&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;563&quot; data-start=&quot;553&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;데이터 &lt;/span&gt;&lt;span&gt;보호&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;594&quot; data-start=&quot;564&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;579&quot; data-start=&quot;564&quot;&gt;&lt;span&gt;비밀번호 &lt;/span&gt;&lt;span&gt;저장에 &lt;/span&gt;&lt;span&gt;적합?&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;585&quot; data-start=&quot;579&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;예&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;594&quot; data-start=&quot;585&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;아니오&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;699&quot; data-start=&quot;596&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;암호화&lt;/span&gt;&lt;/b&gt;&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;키가 &lt;/span&gt;&lt;span&gt;유출되면 &lt;/span&gt;&lt;span&gt;누구나 &lt;/span&gt;&lt;span&gt;복호화할 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있기 &lt;/span&gt;&lt;span&gt;때문에 &lt;/span&gt;&lt;b&gt;&lt;span&gt;비밀번호 &lt;/span&gt;&lt;span&gt;저장용으로는 &lt;/span&gt;&lt;span&gt;부적절&lt;/span&gt;&lt;/b&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;따라서 &lt;/span&gt;&lt;span&gt;우리는 &lt;/span&gt;&lt;span&gt;복호화가 &lt;/span&gt;&lt;span&gt;불가능한 &lt;/span&gt;&lt;b&gt;&lt;span&gt;단방향 &lt;/span&gt;&lt;span&gt;해시 &lt;/span&gt;&lt;span&gt;함수&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;사용해야 &lt;/span&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;722&quot; data-start=&quot;706&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;722&quot; data-start=&quot;706&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;722&quot; data-start=&quot;706&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;해시(&lt;/span&gt;&lt;span&gt;Hash)&lt;/span&gt;&lt;span&gt;란?&lt;/span&gt;&lt;/h2&gt;
&lt;h2 data-end=&quot;722&quot; data-start=&quot;706&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;입력값(&lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;비밀번호 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;등)&lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;을 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;고정된 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;길이의 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;문자열로 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;변환하는 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;함수&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;838&quot; data-start=&quot;761&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;772&quot; data-start=&quot;761&quot;&gt;&lt;span&gt;복호화가 &lt;/span&gt;&lt;span&gt;불가능함&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;791&quot; data-start=&quot;773&quot;&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;입력 &amp;rarr; &lt;/span&gt;&lt;span&gt;항상 &lt;/span&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;출력&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;838&quot; data-start=&quot;792&quot;&gt;&lt;span&gt;대표 &lt;/span&gt;&lt;span&gt;알고리즘: &lt;/span&gt;SHA-256&lt;span&gt;, &lt;/span&gt;SHA-1&lt;span&gt;, &lt;/span&gt;MD5&lt;span&gt;, &lt;/span&gt;BCrypt&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;901&quot; data-start=&quot;840&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 &lt;/span&gt;SHA-256&lt;span&gt;, &lt;/span&gt;MD5&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;빠르게 &lt;/span&gt;&lt;span&gt;계산되기 &lt;/span&gt;&lt;span&gt;때문에 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Brute-&lt;/span&gt;&lt;span&gt;force &lt;/span&gt;&lt;span&gt;공격에 &lt;/span&gt;&lt;span&gt;취약&lt;/span&gt;&lt;/b&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;901&quot; data-start=&quot;840&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;901&quot; data-start=&quot;840&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQUF2A/btsNTkJk2to/bFdJI7ibCekGKsRk7geGkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQUF2A/btsNTkJk2to/bFdJI7ibCekGKsRk7geGkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQUF2A/btsNTkJk2to/bFdJI7ibCekGKsRk7geGkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQUF2A%2FbtsNTkJk2to%2FbFdJI7ibCekGKsRk7geGkk%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;961&quot; height=&quot;351&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;901&quot; data-start=&quot;840&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7pe0p/btsNSLgaKjn/K7VkTD1H9vmlU9eWbY2SIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7pe0p/btsNSLgaKjn/K7VkTD1H9vmlU9eWbY2SIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7pe0p/btsNSLgaKjn/K7VkTD1H9vmlU9eWbY2SIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7pe0p%2FbtsNSLgaKjn%2FK7VkTD1H9vmlU9eWbY2SIk%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;848&quot; height=&quot;280&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-end=&quot;930&quot; data-start=&quot;908&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;930&quot; data-start=&quot;908&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;930&quot; data-start=&quot;908&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;span&gt;나온 &lt;/span&gt;&lt;span&gt;것이 &lt;/span&gt;&lt;span&gt;BCrypt&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1006&quot; data-start=&quot;932&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;BCrypt&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;Blowfish&lt;span&gt; &lt;/span&gt;&lt;span&gt;알고리즘을 &lt;/span&gt;&lt;span&gt;기반으로 &lt;/span&gt;&lt;span&gt;만든 &lt;/span&gt;&lt;b&gt;&lt;span&gt;비밀번호 &lt;/span&gt;&lt;span&gt;저장용 &lt;/span&gt;&lt;span&gt;해시 &lt;/span&gt;&lt;span&gt;함수&lt;/span&gt;&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;다음과 &lt;/span&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;특징을 &lt;/span&gt;&lt;span&gt;가집니다:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;특징설명
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1259&quot; data-start=&quot;1008&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1259&quot; data-start=&quot;1036&quot;&gt;
&lt;tr data-end=&quot;1084&quot; data-start=&quot;1036&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1046&quot; data-start=&quot;1036&quot;&gt;&lt;span&gt;Salt &lt;/span&gt;&lt;span&gt;내장&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1084&quot; data-start=&quot;1046&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;매번 &lt;/span&gt;&lt;span&gt;다른 &lt;/span&gt;&lt;span&gt;salt&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;생성하여 &lt;/span&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;비밀번호도 &lt;/span&gt;&lt;span&gt;다른 &lt;/span&gt;&lt;span&gt;해시 &lt;/span&gt;&lt;span&gt;생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1126&quot; data-start=&quot;1085&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1102&quot; data-start=&quot;1085&quot;&gt;&lt;span&gt;Cost &lt;/span&gt;&lt;span&gt;factor &lt;/span&gt;&lt;span&gt;지원&lt;/span&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1126&quot; data-start=&quot;1102&quot;&gt;&lt;span&gt;연산 &lt;/span&gt;&lt;span&gt;횟수를 &lt;/span&gt;&lt;span&gt;조절해 &lt;/span&gt;&lt;span&gt;공격을 &lt;/span&gt;&lt;span&gt;어렵게 &lt;/span&gt;&lt;span&gt;함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1151&quot; data-start=&quot;1127&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1137&quot; data-start=&quot;1127&quot;&gt;&lt;span&gt;복호화 &lt;/span&gt;&lt;span&gt;불가능&lt;/span&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1151&quot; data-start=&quot;1137&quot;&gt;&lt;span&gt;철저한 &lt;/span&gt;&lt;span&gt;단방향 &lt;/span&gt;&lt;span&gt;해시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1199&quot; data-start=&quot;1152&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1168&quot; data-start=&quot;1152&quot;&gt;&lt;span&gt;결과 &lt;/span&gt;&lt;span&gt;문자열에 &lt;/span&gt;&lt;span&gt;정보 &lt;/span&gt;&lt;span&gt;포함&lt;/span&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1199&quot; data-start=&quot;1168&quot;&gt;&lt;span&gt;salt, &lt;/span&gt;&lt;span&gt;cost, &lt;/span&gt;&lt;span&gt;해시값을 &lt;/span&gt;&lt;span&gt;모두 &lt;/span&gt;&lt;span&gt;문자열에 &lt;/span&gt;&lt;span&gt;포함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1259&quot; data-start=&quot;1200&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1222&quot; data-start=&quot;1200&quot;&gt;&lt;span&gt;Spring &lt;/span&gt;&lt;span&gt;Security &lt;/span&gt;&lt;span&gt;기본값&lt;/span&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1259&quot; data-start=&quot;1222&quot;&gt;BCryptPasswordEncoder&lt;span&gt;로 &lt;/span&gt;&lt;span&gt;바로 &lt;/span&gt;&lt;span&gt;사용 &lt;/span&gt;&lt;span&gt;가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;1278&quot; data-start=&quot;1266&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1278&quot; data-start=&quot;1266&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1278&quot; data-start=&quot;1266&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Salt&lt;/span&gt;&lt;span&gt;란?&lt;/span&gt;&lt;/h2&gt;
&lt;h2 data-end=&quot;1278&quot; data-start=&quot;1266&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;Salt&lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;는 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;해시 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;전에 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;붙이는 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;무작위 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;문자열입니다.&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1353&quot; data-start=&quot;1311&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;비밀번호라도 &lt;/span&gt;&lt;span&gt;다른 &lt;/span&gt;&lt;span&gt;salt&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;사용하면 &lt;/span&gt;&lt;span&gt;완전히 &lt;/span&gt;&lt;span&gt;다른 &lt;/span&gt;&lt;span&gt;해시값이 &lt;/span&gt;&lt;span&gt;생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;사용자입력값Salt해시결과
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1509&quot; data-start=&quot;1355&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1509&quot; data-start=&quot;1422&quot;&gt;
&lt;tr data-end=&quot;1465&quot; data-start=&quot;1422&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1430&quot; data-start=&quot;1422&quot;&gt;&lt;span&gt;userA&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1444&quot; data-start=&quot;1430&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;password123&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1453&quot; data-start=&quot;1444&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;abc123&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1465&quot; data-start=&quot;1453&quot; data-col-size=&quot;sm&quot;&gt;XYZ...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1509&quot; data-start=&quot;1466&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1474&quot; data-start=&quot;1466&quot;&gt;&lt;span&gt;userB&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1488&quot; data-start=&quot;1474&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;password123&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1497&quot; data-start=&quot;1488&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;zxc987&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;1509&quot; data-start=&quot;1497&quot; data-col-size=&quot;sm&quot;&gt;QWE...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1560&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 &lt;/span&gt;&lt;span&gt;하면 &lt;/span&gt;&lt;span&gt;해커가 &lt;/span&gt;&lt;span&gt;사전 &lt;/span&gt;&lt;span&gt;만들어둔 &lt;/span&gt;&lt;span&gt;레인보우 &lt;/span&gt;&lt;span&gt;테이블로 &lt;/span&gt;&lt;span&gt;역추적하는 &lt;/span&gt;&lt;span&gt;것을 &lt;/span&gt;&lt;span&gt;방지할 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1560&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1523&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HmRu6/btsNSyBtQ7S/fXdE5JvrpeWpPHn5bov4J0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HmRu6/btsNSyBtQ7S/fXdE5JvrpeWpPHn5bov4J0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HmRu6/btsNSyBtQ7S/fXdE5JvrpeWpPHn5bov4J0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHmRu6%2FbtsNSyBtQ7S%2FfXdE5JvrpeWpPHn5bov4J0%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;1523&quot; height=&quot;452&quot; data-origin-width=&quot;1523&quot; data-origin-height=&quot;452&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;hr data-end=&quot;1565&quot; data-start=&quot;1562&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1584&quot; data-start=&quot;1567&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;레인보우 &lt;/span&gt;&lt;span&gt;테이블이란?&lt;/span&gt;&lt;/h2&gt;
&lt;h2 data-end=&quot;1584&quot; data-start=&quot;1567&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;자주 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;사용되는 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;비밀번호에 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;대한 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;해시값을 &lt;/span&gt;&lt;b&gt;미리 계산해서 저장해둔 테이블&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1636&quot; data-start=&quot;1632&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1636&quot; data-start=&quot;1632&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746979516433&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;123456 &amp;rarr; e10adc3949ba59abbe56e057f20f883e  
qwer1234 &amp;rarr; ab56b4d92b40713acc5af89985d4b786&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1362&quot; data-start=&quot;1278&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1362&quot; data-start=&quot;1278&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;rarr; &lt;/span&gt;&lt;span&gt;해시만 &lt;/span&gt;&lt;span&gt;보고도 &lt;/span&gt;&lt;span&gt;어떤 &lt;/span&gt;&lt;span&gt;비밀번호인지 &lt;/span&gt;&lt;span&gt;알아낼 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있음&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;rarr; &lt;/span&gt;&lt;span&gt;Salt&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;쓰지 &lt;/span&gt;&lt;span&gt;않으면 &lt;/span&gt;&lt;span&gt;이 &lt;/span&gt;&lt;span&gt;공격에 &lt;/span&gt;&lt;span&gt;매우 &lt;/span&gt;&lt;span&gt;취약합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1377&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ehynku/btsNTAyuvKg/cKTeAkoojpaSh2aRmVEGV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ehynku/btsNTAyuvKg/cKTeAkoojpaSh2aRmVEGV1/img.png&quot; data-alt=&quot;단순화한 레인보 테이블&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ehynku/btsNTAyuvKg/cKTeAkoojpaSh2aRmVEGV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fehynku%2FbtsNTAyuvKg%2FcKTeAkoojpaSh2aRmVEGV1%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;1377&quot; height=&quot;540&quot; data-origin-width=&quot;1377&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;단순화한 레인보 테이블&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;1362&quot; data-start=&quot;1278&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1362&quot; data-start=&quot;1278&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1817&quot; data-start=&quot;1802&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;그런데 &lt;/span&gt;&lt;span&gt;궁금하죠?&lt;/span&gt;&lt;/h2&gt;
&lt;h2 data-end=&quot;1817&quot; data-start=&quot;1802&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;같은 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;비밀번호라도 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;매번 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;해시값이 &lt;/span&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;달라진다며?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1871&quot; data-start=&quot;1821&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 &lt;/span&gt;&lt;span&gt;로그인할 &lt;/span&gt;&lt;span&gt;때 &lt;/span&gt;&lt;span&gt;어떻게 &lt;/span&gt;&lt;span&gt;비교해요?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1901&quot; data-start=&quot;1873&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서부터가 &lt;/span&gt;&lt;b&gt;&lt;span&gt;BCrypt&lt;/span&gt;&lt;span&gt;의 &lt;/span&gt;&lt;span&gt;핵심 &lt;/span&gt;&lt;span&gt;기술&lt;/span&gt;&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;1939&quot; data-start=&quot;1908&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1939&quot; data-start=&quot;1908&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1939&quot; data-start=&quot;1908&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;1939&quot; data-start=&quot;1908&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;BCrypt&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;해시 &lt;/span&gt;&lt;span&gt;안에 &lt;/span&gt;&lt;span&gt;salt&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;포함시킨다!&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1978&quot; data-start=&quot;1941&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;BCrypt&lt;/span&gt;&lt;span&gt;로 &lt;/span&gt;&lt;span&gt;비밀번호를 &lt;/span&gt;&lt;span&gt;해싱하면 &lt;/span&gt;&lt;span&gt;다음처럼 &lt;/span&gt;&lt;span&gt;생긴 &lt;/span&gt;&lt;span&gt;문자열이 &lt;/span&gt;&lt;span&gt;나옵니다:&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1978&quot; data-start=&quot;1941&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746979559998&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$2a$10$WqRf8g1D33tX6SPZzAsT0OZADZ5POZNUKj2HNGIrBxkOcLCUo3GcK&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$2a$&lt;/td&gt;
&lt;td&gt;알고리즘 버전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10$&lt;/td&gt;
&lt;td&gt;Cost factor (2&amp;sup1;⁰ = 1024번 반복)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WqRf8g1D33tX6SPZzAsT0O&lt;/td&gt;
&lt;td&gt;&lt;b&gt;22자리 salt&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;나머지&lt;/td&gt;
&lt;td&gt;최종 해시값 (salt + 비밀번호 해싱 결과)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2252&quot; data-start=&quot;2228&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;로그인할 &lt;/span&gt;&lt;span&gt;때 &lt;/span&gt;&lt;span&gt;비교는 &lt;/span&gt;&lt;span&gt;어떻게 &lt;/span&gt;&lt;span&gt;하나? (스프링 JAVA)&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1746979614515&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;passwordEncoder.matches(&quot;입력한 비밀번호&quot;, &quot;DB에 저장된 해시값&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2350&quot; data-start=&quot;2319&quot; data-ke-size=&quot;size16&quot;&gt;matches()&lt;span&gt; &lt;/span&gt;&lt;span&gt;내부에서는 &lt;/span&gt;&lt;span&gt;다음 &lt;/span&gt;&lt;span&gt;과정을 &lt;/span&gt;&lt;span&gt;수행합니다:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2464&quot; data-start=&quot;2352&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2386&quot; data-start=&quot;2352&quot;&gt;&lt;span&gt;저장된 &lt;/span&gt;&lt;span&gt;해시값에서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;salt, &lt;/span&gt;&lt;span&gt;cost &lt;/span&gt;&lt;span&gt;정보&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;추출&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2425&quot; data-start=&quot;2387&quot;&gt;&lt;span&gt;입력한 &lt;/span&gt;&lt;span&gt;비밀번호 + &lt;/span&gt;&lt;span&gt;추출된 &lt;/span&gt;&lt;span&gt;salt &amp;rarr; &lt;/span&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;방식으로 &lt;/span&gt;&lt;span&gt;다시 &lt;/span&gt;&lt;span&gt;해싱&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2464&quot; data-start=&quot;2426&quot;&gt;&lt;span&gt;새로 &lt;/span&gt;&lt;span&gt;계산한 &lt;/span&gt;&lt;span&gt;해시값과 &lt;/span&gt;&lt;span&gt;기존 &lt;/span&gt;&lt;span&gt;해시값이 &lt;/span&gt;&lt;b&gt;&lt;span&gt;일치하면 &lt;/span&gt;&lt;span&gt;로그인 &lt;/span&gt;&lt;span&gt;성공&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;2554&quot; data-start=&quot;2466&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✔️ &lt;/span&gt;&lt;span&gt;즉, &lt;/span&gt;&lt;span&gt;BCrypt&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;비교에 &lt;/span&gt;&lt;span&gt;필요한 &lt;/span&gt;&lt;span&gt;모든 &lt;/span&gt;&lt;span&gt;정보(&lt;/span&gt;&lt;span&gt;salt &lt;/span&gt;&lt;span&gt;포함)&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;해시 &lt;/span&gt;&lt;span&gt;문자열에 &lt;/span&gt;&lt;span&gt;내장해놓습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;❌ &lt;/span&gt;&lt;span&gt;별도로 &lt;/span&gt;&lt;span&gt;salt&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;저장하거나 &lt;/span&gt;&lt;span&gt;비교할 &lt;/span&gt;&lt;span&gt;필요가 &lt;/span&gt;&lt;span&gt;없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2554&quot; data-start=&quot;2466&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2846&quot; data-start=&quot;2839&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;정리&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;질문핵심 요약
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3079&quot; data-start=&quot;2848&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;3079&quot; data-start=&quot;2884&quot;&gt;
&lt;tr data-end=&quot;2928&quot; data-start=&quot;2884&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2913&quot; data-start=&quot;2884&quot;&gt;&lt;span&gt;왜 &lt;/span&gt;&lt;span&gt;비밀번호는 &lt;/span&gt;&lt;span&gt;암호화가 &lt;/span&gt;&lt;span&gt;아니라 &lt;/span&gt;&lt;span&gt;해시해야 &lt;/span&gt;&lt;span&gt;하나요?&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;2928&quot; data-start=&quot;2913&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;복호화되면 &lt;/span&gt;&lt;span&gt;안 &lt;/span&gt;&lt;span&gt;되니까&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2964&quot; data-start=&quot;2929&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2946&quot; data-start=&quot;2929&quot;&gt;&lt;span&gt;해시만 &lt;/span&gt;&lt;span&gt;저장해도 &lt;/span&gt;&lt;span&gt;괜찮나요?&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;2964&quot; data-start=&quot;2946&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;salt&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;쓰면 &lt;/span&gt;&lt;span&gt;안전합니다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3018&quot; data-start=&quot;2965&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2993&quot; data-start=&quot;2965&quot;&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;span&gt;비밀번호인데 &lt;/span&gt;&lt;span&gt;해시값이 &lt;/span&gt;&lt;span&gt;매번 &lt;/span&gt;&lt;span&gt;다른 &lt;/span&gt;&lt;span&gt;이유는?&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;3018&quot; data-start=&quot;2993&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;salt&lt;/span&gt;&lt;span&gt;가 &lt;/span&gt;&lt;span&gt;무작위로 &lt;/span&gt;&lt;span&gt;들어가기 &lt;/span&gt;&lt;span&gt;때문입니다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3079&quot; data-start=&quot;3019&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;3035&quot; data-start=&quot;3019&quot;&gt;&lt;span&gt;그럼 &lt;/span&gt;&lt;span&gt;어떻게 &lt;/span&gt;&lt;span&gt;비교하나요?&lt;/span&gt;&lt;/td&gt;
&lt;td data-end=&quot;3079&quot; data-start=&quot;3035&quot; data-col-size=&quot;sm&quot;&gt;&lt;span&gt;해시 &lt;/span&gt;&lt;span&gt;안에 &lt;/span&gt;&lt;span&gt;salt&lt;/span&gt;&lt;span&gt;가 &lt;/span&gt;&lt;span&gt;포함되어 &lt;/span&gt;&lt;span&gt;있고, &lt;/span&gt;&lt;span&gt;이를 &lt;/span&gt;&lt;span&gt;기반으로 &lt;/span&gt;&lt;span&gt;재해싱하여 &lt;/span&gt;&lt;span&gt;비교합니다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;3095&quot; data-start=&quot;3086&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-end=&quot;3215&quot; data-start=&quot;3097&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;BCrypt&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;단순히 &quot;&lt;/span&gt;&lt;span&gt;라이브러리를 &lt;/span&gt;&lt;span&gt;쓰면 &lt;/span&gt;&lt;span&gt;되는 &lt;/span&gt;&lt;span&gt;기술&quot;&lt;/span&gt;&lt;span&gt;이 &lt;/span&gt;&lt;span&gt;아닙니다.&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;원리까지 &lt;/span&gt;&lt;span&gt;이해해야 &lt;/span&gt;&lt;span&gt;보안 &lt;/span&gt;&lt;span&gt;사고 &lt;/span&gt;&lt;span&gt;없이 &lt;/span&gt;&lt;span&gt;안전한 &lt;/span&gt;&lt;span&gt;서비스를 &lt;/span&gt;&lt;span&gt;설계&lt;/span&gt;&lt;/b&gt;&lt;span&gt;할 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;오늘 &lt;/span&gt;&lt;span&gt;포스팅이 &lt;/span&gt;&lt;span&gt;여러분의 &lt;/span&gt;&lt;span&gt;보안 &lt;/span&gt;&lt;span&gt;지식에 &lt;/span&gt;&lt;span&gt;도움이 &lt;/span&gt;&lt;span&gt;되었길 &lt;/span&gt;&lt;span&gt;바랍니다!&lt;/span&gt;&lt;/p&gt;</description>
      <category>보안</category>
      <category>bcrypt</category>
      <category>Salt</category>
      <category>spring security</category>
      <category>단방향 해시</category>
      <category>레인보우테이블</category>
      <category>비밀번호 해시</category>
      <category>웹 보안 기초</category>
      <category>자바 보안</category>
      <category>해시</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/84</guid>
      <comments>https://kwakscoding.tistory.com/84#entry84comment</comments>
      <pubDate>Mon, 12 May 2025 01:14:07 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security 기반 CSRF 공격 실습 및 방어 방법 정리</title>
      <link>https://kwakscoding.tistory.com/83</link>
      <description>&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;유튜브 영상도 촬영하였습니다.&amp;nbsp; 아래 영상을 참고하면서 블로그글을 참고해주세요.&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h4&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=YGLwx_uzPgw&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/baInxk/hyYL5IQrVx/WUCemKhhnvys0ch9BA2K2K/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/d0V1tS/hyYMSB4EpG/OhJomzmUdI0gB5fWyR2up0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;Spring Security CSRF 공격 시연부터 방어까지 실습&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/YGLwx_uzPgw&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CSRF (Cross-Site Request Forgery)란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSRF (Cross-Site Request Forgery) &lt;b&gt;사이트 간 요청 위조&lt;/b&gt;&lt;/b&gt;는 웹 애플리케이션의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;취약점&lt;/b&gt;&lt;/span&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;악용&lt;/b&gt;하는 공격 방식 중 하나로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사용자&lt;/b&gt;가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;의도하지 않은 요청&lt;/b&gt;&lt;/u&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수행하게 만드는 공격&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CSRF 공격의 목적은 사용자가 웹 애플리케이션에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #99cefa;&quot;&gt;&lt;b&gt;인증된 세션을 가지고 있는 상태&lt;/b&gt;&lt;/span&gt;에서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;공격자&lt;/b&gt;&lt;/span&gt;가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의도한 행동&lt;/b&gt;을 사용자로 하여금&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실행&lt;/b&gt;하게 하는 것입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 사용자가 현재&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;로그인한 세션&lt;/b&gt;&lt;/u&gt;을 이용하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;악의적인 요청&lt;/b&gt;&lt;/span&gt;이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버에 전달&lt;/b&gt;되도록 하여, 사용자의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;의도와 무관&lt;/b&gt;하게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터 변경, 거래 발생&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 작업이 수행되게 만듭니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;463&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvKO4q/btsNJZzxpH5/XTTNA1VjfbmhXOki1KxNS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvKO4q/btsNJZzxpH5/XTTNA1VjfbmhXOki1KxNS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvKO4q/btsNJZzxpH5/XTTNA1VjfbmhXOki1KxNS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvKO4q%2FbtsNJZzxpH5%2FXTTNA1VjfbmhXOki1KxNS0%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;693&quot; height=&quot;463&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;463&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSRF(Cross-Site Request Forgery)의 동작 원리&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;CSRF 공격이 성공하기 위해서는 아래의 조건을 충족해야 합니다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;b&gt;CSRF 공격이 성공하기 위한 조건&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #cedada;&quot;&gt;사용자 인증&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;사용자가 공격 대상 웹사이트에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로그인&lt;/b&gt;하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;유효한 세션 쿠키&lt;/b&gt;&lt;/u&gt;를 가지고 있어야 합니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;이 세션 쿠키는 공격자가 생성한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;악성 요청에 포함&lt;/b&gt;&lt;/span&gt;되어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버에서 인증된 요청&lt;/b&gt;으로&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;인식&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #cedada;&quot;&gt;쿠키&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #3d4144; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;b&gt;쿠키 기반&lt;/b&gt;으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;서버 세션 정보&lt;/b&gt;&lt;/u&gt;를 획득할 수 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #cedada;&quot;&gt;요청의 구조 이해&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;공격자는 서버가 어떤 URL 패턴과 요청 파라미터를 사용하는지 알고 있어야 합니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;예를 들어, 사용자의 계좌에서 돈을 이체하는 요청이 POST /transfer 엔드포인트를 사용하고, amount와 recipient이라는 파라미터를 요구하는 경우, 공격자는 이 정보를 알고 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;CSRF 공격이 일어나기 위한 조건&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;사용자가 이미 인증된 상태여야 함 (세션, 쿠키 기반 로그인)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;브라우저가 자동으로 쿠키를 함께 전송&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;공격자가 form, img, script 태그 등을 통해 요청을 유도&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqMeTX/btsNLmApR9G/fVSb0KH3GwSIDwzGWsSrw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqMeTX/btsNLmApR9G/fVSb0KH3GwSIDwzGWsSrw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqMeTX/btsNLmApR9G/fVSb0KH3GwSIDwzGWsSrw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqMeTX%2FbtsNLmApR9G%2FfVSb0KH3GwSIDwzGWsSrw0%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;1126&quot; height=&quot;501&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;실습: CSRF 공격을 재현해보기&lt;/span&gt;&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;SecurityConfig.java&lt;/p&gt;
&lt;pre id=&quot;code_1746279498303&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.luca.csrf.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -&amp;gt; csrf.disable()) // CSRF 보호 기능을 비활성화 (실습을 위해 OFF) //Spring Security는 기본적으로 CSRF 공격 방지를 위해 폼 요청에 토큰을 요구함.
            .authorizeHttpRequests(auth -&amp;gt; auth
                    .requestMatchers(&quot;/login&quot;,&quot;/css/**&quot;).permitAll() // /login,/css로 시작하는 경로는 누구나 접근 가능
                    .anyRequest().authenticated()                             //그 외에 모든 요청은 인증된 사용자만 접근 가능
            )
            .formLogin(form -&amp;gt; form
                    .loginPage(&quot;/login&quot;)  //로그인 폼 URL을 커스텀
                    .defaultSuccessUrl(&quot;/post&quot;,true) //로그인 성공 후 이동할 페이지 설정
                    .permitAll()  //로그인 폼 자체는 인증 없이 누구나 접근 가능
            );
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        //메모리에 사용자 정보 하나 생성
        //비밀번호를 단순 인코딩 (실습용). 실제 서비스에서는 BCrypt 같은 강력한 해시 사용 권장
        UserDetails user = User.withDefaultPasswordEncoder()
                .username(&quot;user&quot;)
                .password(&quot;1234&quot;)
                .roles(&quot;USER&quot;)
                .build();
        // DB 없이 메모리에서 사용자 인증 정보를 관리함 (간단한 테스트 용도에 적합)
        return new InMemoryUserDetailsManager(user); // 메모리 기반 사용자 저장소 반환
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;PostController.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746279532352&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.luca.csrf.post.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PostController {

    @GetMapping(&quot;/post&quot;)
    public String postForm() {
        return &quot;post&quot;;
    }

    @PostMapping(&quot;/post&quot;)
    @ResponseBody
    public String submitPost(@RequestParam(&quot;content&quot;) String content) {
        System.out.println(&quot;작성된 글: &quot; + content);
        return &quot;글 작성 완료!&quot;;
    }

    @GetMapping(&quot;/login&quot;)
    public String loginPage() {
        return &quot;login&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;login.html&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746279585083&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1&amp;gt;로그인&amp;lt;/h1&amp;gt;
&amp;lt;form method=&quot;post&quot; th:action=&quot;@{/login}&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; name=&quot;username&quot; placeholder=&quot;user&quot; /&amp;gt;
    &amp;lt;input type=&quot;password&quot; name=&quot;password&quot; placeholder=&quot;1234&quot; /&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;post.html&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746279984204&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1&amp;gt;게시글 작성&amp;lt;/h1&amp;gt;
&amp;lt;form action=&quot;/post&quot; method=&quot;post&quot;&amp;gt;
  &amp;lt;input type=&quot;text&quot; name=&quot;content&quot; value=&quot;정상 글입니다.&quot; /&amp;gt;

  
  &amp;lt;button type=&quot;submit&quot;&amp;gt;작성&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&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;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1746279667200&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.5'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.luca'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
//	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	compileOnly 'org.projectlombok:lombok'
//	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;203&quot; data-start=&quot;177&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;1. 정상적인 게시글 작성 테스트&lt;/h3&gt;
&lt;p data-end=&quot;257&quot; data-start=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;먼저, 아래와 같은 간단한 로그인 및 게시글 작성 기능이 있는 웹 애플리케이션을 준비했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;300&quot; data-start=&quot;259&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;278&quot; data-start=&quot;259&quot;&gt;로그인 페이지: /login&lt;/li&gt;
&lt;li data-end=&quot;300&quot; data-start=&quot;279&quot;&gt;게시글 작성 페이지: /post&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;사용자 정보는 메모리 기반 사용자 저장소를 사용하고 있으며, user / 1234 계정으로 로그인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cD9ozp/btsNKirYvMi/82mAzqMtK5UktazvhtmsXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cD9ozp/btsNKirYvMi/82mAzqMtK5UktazvhtmsXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cD9ozp/btsNKirYvMi/82mAzqMtK5UktazvhtmsXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcD9ozp%2FbtsNKirYvMi%2F82mAzqMtK5UktazvhtmsXk%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;1166&quot; height=&quot;570&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;로그인 후 /post 페이지에서 글을 작성하면 서버에서는 정상적으로 글 내용을 로그에 출력합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFjNTq/btsNJP4IWse/civNoR4Fc0vxQFwUhREl21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFjNTq/btsNJP4IWse/civNoR4Fc0vxQFwUhREl21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFjNTq/btsNJP4IWse/civNoR4Fc0vxQFwUhREl21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFjNTq%2FbtsNJP4IWse%2FcivNoR4Fc0vxQFwUhREl21%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;747&quot; height=&quot;465&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvdGJ7/btsNJXuSSk5/9nzRCLjgnxzClwCfZJa690/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvdGJ7/btsNJXuSSk5/9nzRCLjgnxzClwCfZJa690/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvdGJ7/btsNJXuSSk5/9nzRCLjgnxzClwCfZJa690/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvdGJ7%2FbtsNJXuSSk5%2F9nzRCLjgnxzClwCfZJa690%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;662&quot; height=&quot;369&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;849&quot; data-start=&quot;816&quot; data-ke-size=&quot;size23&quot;&gt;2. 공격자 사이트 접근 (evil.html)&lt;/h3&gt;
&lt;p data-end=&quot;928&quot; data-start=&quot;851&quot; data-ke-size=&quot;size16&quot;&gt;이제 공격자가 만든 사이트(evil.html)를 준비해봅니다. 예를 들어, &amp;ldquo;쿠폰 받기&amp;rdquo;라는 문구로 유인하는 단순한 HTML 페이지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746280168648&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- evil.html --&amp;gt;
&amp;lt;h2&amp;gt;쿠폰 1000원 받기!&amp;lt;/h2&amp;gt;
&amp;lt;form action=&quot;http://localhost:8080/post&quot; method=&quot;POST&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;content&quot; value=&quot;  해커의 글  &quot; /&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;쿠폰 받기&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;p data-end=&quot;1235&quot; data-start=&quot;1153&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 이 페이지를 방문해 버튼을 클릭하면 브라우저는 현재 로그인된 세션 쿠키를 자동으로 첨부하여 /post로 POST 요청을 보내게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;1296&quot; data-start=&quot;1237&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 사용자의 의도와 상관없이 &lt;b&gt;&amp;ldquo;  해커의 글  &amp;rdquo;&lt;/b&gt; 이라는 내용이 A 사이트에 등록됩니다.&lt;/p&gt;
&lt;p data-end=&quot;1296&quot; data-start=&quot;1237&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1333&quot; data-start=&quot;1303&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;3.&amp;nbsp; CSRF 공격이 실제로 성공한 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1425&quot; data-start=&quot;1335&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1362&quot; data-start=&quot;1335&quot;&gt;사용자는 이미 A 사이트에 로그인한 상태였고,&lt;/li&gt;
&lt;li data-end=&quot;1392&quot; data-start=&quot;1363&quot;&gt;브라우저는 세션 쿠키를 자동으로 전송했기 때문에,&lt;/li&gt;
&lt;li data-end=&quot;1425&quot; data-start=&quot;1393&quot;&gt;서버는 이를 &lt;b&gt;정상적인 요청으로 인식&lt;/b&gt;하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;nbsp;CSRF 방어 방법 정리&lt;/span&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;CSRF Token (기본)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;서버에서 토큰 발급, 클라이언트에서 폼/헤더에 담아 전송&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;가장 강력한 방어&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;AJAX 요청 시 수동 삽입 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Referer 체크&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;요청의 출처(도메인) 확인&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;구현 간단&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;일부 브라우저에서 헤더 없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;CAPTCHA&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;사용자가 직접 입력 or 클릭&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;자동 요청 차단&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;UX 저하 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첫번째 방어 방법인 CSRF Token을 사용해 보겠습니다.&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1746280285518&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
//            .csrf(csrf -&amp;gt; csrf.disable()) // CSRF 보호 기능을 비활성화 (실습을 위해 OFF) //Spring Security는 기본적으로 CSRF 공격 방지를 위해 폼 요청에 토큰을 요구함.
            .authorizeHttpRequests(auth -&amp;gt; auth
                    .requestMatchers(&quot;/login&quot;,&quot;/css/**&quot;).permitAll() // /login,/css로 시작하는 경로는 누구나 접근 가능
                    .anyRequest().authenticated()                             //그 외에 모든 요청은 인증된 사용자만 접근 가능
            )
            .formLogin(form -&amp;gt; form
                    .loginPage(&quot;/login&quot;)  //로그인 폼 URL을 커스텀
                    .defaultSuccessUrl(&quot;/post&quot;,true) //로그인 성공 후 이동할 페이지 설정
                    .permitAll()  //로그인 폼 자체는 인증 없이 누구나 접근 가능
            );
        return http.build();
    }

...그외 소스들 냅둘것&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;csrf 부분을 주석을 넣어줍니다. 스프링 시큐리티는 기본적으로 csrf보호 기본값이 적용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1695&quot; data-start=&quot;1663&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;CSRF 토큰을 폼에 삽입 (Thymeleaf)&lt;/h3&gt;
&lt;p data-end=&quot;1818&quot; data-start=&quot;1697&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 HTML 폼에서 CSRF 토큰을 자동으로 처리할 수 있도록 지원합니다. Thymeleaf를 사용한다면 아래와 같이 &amp;lt;input type=&quot;hidden&quot;&amp;gt;으로 쉽게 삽입할 수 있습니다:&lt;/p&gt;
&lt;p data-end=&quot;1818&quot; data-start=&quot;1697&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746280337170&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form action=&quot;/post&quot; method=&quot;post&quot;&amp;gt;
  &amp;lt;input type=&quot;text&quot; name=&quot;content&quot; value=&quot;정상 글입니다.&quot; /&amp;gt;
  &amp;lt;input type=&quot;hidden&quot; th:name=\&quot;${_csrf.parameterName}\&quot; th:value=\&quot;${_csrf.token}\&quot; /&amp;gt;
  &amp;lt;button type=&quot;submit&quot;&amp;gt;작성&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&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;h3 data-end=&quot;2092&quot; data-start=&quot;2063&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;4.&amp;nbsp; evil.html 공격 다시 시도&lt;/h3&gt;
&lt;p data-end=&quot;2144&quot; data-start=&quot;2094&quot; data-ke-size=&quot;size16&quot;&gt;앞서 사용했던 evil.html 파일을 다시 실행한 뒤 쿠폰 받기 버튼을 클릭해봅니다.&lt;/p&gt;
&lt;p data-end=&quot;2186&quot; data-start=&quot;2146&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이번에는 결과가 다릅니다. 서버는 다음과 같은 응답을 반환합니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1746284711355&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP 403 Forbidden&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;2234&quot; data-start=&quot;2220&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;왜 차단됐을까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2367&quot; data-start=&quot;2236&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2296&quot; data-start=&quot;2236&quot;&gt;Spring Security는 이제 모든 POST 요청에 대해 CSRF 토큰이 포함되었는지를 검사합니다.&lt;/li&gt;
&lt;li data-end=&quot;2367&quot; data-start=&quot;2297&quot;&gt;evil.html에서는 CSRF 토큰이 포함되어 있지 않기 때문에, 서버는 이 요청을 악의적인 것으로 간주하고 차단합니다.&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;&lt;span style=&quot;background-color: #ffffff; color: #0f0f0f; text-align: start;&quot;&gt;1.Spring Security에서 CSRF 토큰은 이렇게 동작합니다 1.토큰 생성 시점 사용자가 최초로 GET 요청(예: /post, /login)으로 접근하면 Spring Security는 내부적으로 CsrfTokenRepository를 통해 고유한 토큰을 생성합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #0f0f0f; text-align: start;&quot;&gt;기본 구현체는 HttpSessionCsrfTokenRepository이며, 이 토큰은 서버 세션에 저장됩니다. &lt;/span&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;span style=&quot;background-color: #ffffff; color: #0f0f0f; text-align: start;&quot;&gt;2.토큰 전달 방식 Thymeleaf를 사용한다면 ${_csrf.parameterName}과 ${_csrf.token} 값을 통해 Spring Security가 세션에서 가져온 토큰을 &amp;lt;form&amp;gt;에 자동으로 삽입해줍니다. &lt;/span&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;span style=&quot;background-color: #ffffff; color: #0f0f0f; text-align: start;&quot;&gt;3.토큰 검증 사용자가 POST, PUT, DELETE 요청을 보내면, CsrfFilter가 요청에 포함된 토큰과 세션에 저장된 토큰을 비교합니다. 둘이 일치하면 요청 허용, 불일치하거나 누락되면 403 Forbidden.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2517&quot; data-start=&quot;2445&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2517&quot; data-start=&quot;2445&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2517&quot; data-start=&quot;2445&quot; data-ke-size=&quot;size16&quot;&gt;참고 :&lt;br /&gt;&lt;a href=&quot;https://pixx.tistory.com/344&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pixx.tistory.com/344&lt;/a&gt;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://jaykaybaek.tistory.com/29&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jaykaybaek.tistory.com/29 &lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://innovation123.tistory.com/243&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://innovation123.tistory.com/243&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>CSRF</category>
      <category>CSRF 공격</category>
      <category>csrf 방어</category>
      <category>CSRF 예제</category>
      <category>Spring Boot</category>
      <category>Thymeleaf</category>
      <category>web security</category>
      <category>백엔드 개발</category>
      <category>스프링 시큐리티</category>
      <author>곽코딩루카</author>
      <guid isPermaLink="true">https://kwakscoding.tistory.com/83</guid>
      <comments>https://kwakscoding.tistory.com/83#entry83comment</comments>
      <pubDate>Sun, 4 May 2025 02:43:21 +0900</pubDate>
    </item>
  </channel>
</rss>